Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix browser Authentication #194

Merged
merged 1 commit into from
Mar 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@
# IDE
.idea
.vscode
.gonvim

#OS junk
.DS_Store

# temporary files and the rest of the crap
*.json
*.yml
*.yaml
json/*
examples/*
Expand All @@ -39,3 +41,14 @@ cmd/sdconv/sdconv
.env
*.cache
*.log

# goreleaser stuff
dist/
!.goreleaser.yaml
!schema.json
*.jsonl
*.state

# sundry junk used for testing and other fuckery
/tmp
*.dot
24 changes: 1 addition & 23 deletions auth/browser/browser.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,7 @@ import (
"errors"
"fmt"
"net/http"
"net/url"
"regexp"
"runtime/trace"
"strings"
"time"

"github.com/playwright-community/playwright-go"
Expand Down Expand Up @@ -98,7 +95,7 @@ func (cl *Client) Authenticate(ctx context.Context) (string, []http.Cookie, erro
return "", nil, err
}

token, err := extractToken(r.URL())
token, err := extractToken(r)
if err != nil {
return "", nil, err
}
Expand Down Expand Up @@ -130,25 +127,6 @@ func (cl *Client) withBrowserGuard(ctx context.Context, fn func()) error {
return nil
}

// tokenRE is the regexp that matches a valid Slack Client token.
var tokenRE = regexp.MustCompile(`xoxc-[0-9]+-[0-9]+-[0-9]+-[0-9a-z]{64}`)

func extractToken(uri string) (string, error) {
p, err := url.Parse(strings.TrimSpace(uri))
if err != nil {
return "", err
}
q := p.Query()
token := q.Get("token")
if token == "" {
return "", errors.New("token not found")
}
if !tokenRE.MatchString(token) {
return "", errors.New("invalid token value")
}
return token, nil
}

func convertCookies(pwc []playwright.Cookie) []http.Cookie {
var ret = make([]http.Cookie, 0, len(pwc))
for _, p := range pwc {
Expand Down
31 changes: 0 additions & 31 deletions auth/browser/browser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,37 +6,6 @@ import (
"time"
)

func Test_extractToken(t *testing.T) {
type args struct {
uri string
}
tests := []struct {
name string
args args
want string
wantErr bool
}{
{
"ok",
args{"https://ora600.slack.com/api/api.features?_x_id=noversion-1651817410.129&token=xoxc-610187951300-604451271234-3473161557912-4c426dd426a45208707725b710302b32dda0ab002b80ccd8c4c8ac9971a11558&platform=sonic&_x_should_cache=false&_x_allow_cached=true&_x_team_id=THY5HTZ8U&_x_gantry=true&fp=7c\n"},
"xoxc-610187951300-604451271234-3473161557912-4c426dd426a45208707725b710302b32dda0ab002b80ccd8c4c8ac9971a11558",
false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := extractToken(tt.args.uri)
if (err != nil) != tt.wantErr {
t.Errorf("extractToken() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("extractToken() got = %v, want %v", got, tt.want)
}
})
}
}

func Test_float2time(t *testing.T) {
type args struct {
v float64
Expand Down
101 changes: 101 additions & 0 deletions auth/browser/extractors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package browser

import (
"errors"
"mime/multipart"
"net/http"
"net/url"
"regexp"
"strings"

"github.com/playwright-community/playwright-go"
)

//go:generate mockgen -package browser -destination playwright_test.go github.com/playwright-community/playwright-go Request

// tokenRE is the regexp that matches a valid Slack Client token.
var tokenRE = regexp.MustCompile(`xoxc-[0-9]+-[0-9]+-[0-9]+-[0-9a-z]{64}`)

const maxMultipartMem = 65536

var (
ErrNoToken = errors.New("no token found")
ErrInvalidTokenValue = errors.New("invalid token value")
ErrInvalidContentType = errors.New("invalid content-type header")
)

// extractToken extracts token from the request.
func extractToken(r playwright.Request) (string, error) {
if r.Method() == http.MethodGet {
return extractTokenGet(r.URL())
} else if r.Method() == http.MethodPost {
return extractTokenPost(r)
}
return "", errors.New("invalid request method")
}

// extractTokenGet extracts token from the query string.
func extractTokenGet(uri string) (string, error) {
p, err := url.Parse(strings.TrimSpace(uri))
if err != nil {
return "", err
}
q := p.Query()
token := q.Get("token")
if token == "" {
return "", ErrNoToken
}
if !tokenRE.MatchString(token) {
return "", ErrInvalidTokenValue
}
return token, nil
}

// extractTokenPost extracts token from the request body.
func extractTokenPost(r playwright.Request) (string, error) {
boundary, err := boundary(r)
if err != nil {
return "", err
}
data, err := r.PostData()
if err != nil {
return "", err
}
return tokenFromMultipart(data, boundary)
}

// tokenFromMultipart extracts token from the multipart form.
func tokenFromMultipart(s string, boundary string) (string, error) {
mp := multipart.NewReader(strings.NewReader(s), boundary)
form, err := mp.ReadForm(maxMultipartMem)
if err != nil {
return "", err
}
tok, ok := form.Value["token"]
if !ok {
return "", errors.New("token not found")
}
if len(tok) != 1 {
return "", errors.New("invalid token value")
}
if !tokenRE.MatchString(tok[0]) {
return "", errors.New("invalid token value")
}
return tok[0], nil
}

// boundary extracts boundary from the request.
func boundary(r playwright.Request) (string, error) {
values, err := r.HeaderValues("Content-Type")
if err != nil {
return "", err
}
if len(values) != 1 {
return "", ErrInvalidContentType
}
contentType, boundary, found := strings.Cut(values[0], "; boundary=")
if !found || contentType != "multipart/form-data" {
return "", ErrInvalidContentType
}
return boundary, nil
}
134 changes: 134 additions & 0 deletions auth/browser/extractors_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package browser

import (
"errors"
"testing"

gomock "github.com/golang/mock/gomock"
)

const testMultipart = "-----------------------------37168696061856579082739228613\r\nContent-Disposition: form-data; name=\"token\"\r\n\r\nxoxc-888888888888-888888888888-8888888888888-fffffffffffffffa915fe069d70a8ad81743b0ec4ee9c81540af43f5e143264b\r\n-----------------------------37168696061856579082739228613\r\nContent-Disposition: form-data; name=\"platform\"\r\n\r\nsonic\r\n-----------------------------37168696061856579082739228613\r\nContent-Disposition: form-data; name=\"_x_should_cache\"\r\n\r\nfalse\r\n-----------------------------37168696061856579082739228613\r\nContent-Disposition: form-data; name=\"_x_allow_cached\"\r\n\r\ntrue\r\n-----------------------------37168696061856579082739228613\r\nContent-Disposition: form-data; name=\"_x_team_id\"\r\n\r\nTFCSDNRL5\r\n-----------------------------37168696061856579082739228613\r\nContent-Disposition: form-data; name=\"_x_gantry\"\r\n\r\ntrue\r\n-----------------------------37168696061856579082739228613\r\nContent-Disposition: form-data; name=\"_x_sonic\"\r\n\r\ntrue\r\n-----------------------------37168696061856579082739228613--\r\n"

var testHdrValues = []string{
"multipart/form-data; boundary=---------------------------37168696061856579082739228613",
}

const testBoundary = "---------------------------37168696061856579082739228613"

func Test_extractTokenGet(t *testing.T) {
type args struct {
uri string
}
tests := []struct {
name string
args args
want string
wantErr bool
}{
{
"ok",
args{"https://ora600.slack.com/api/api.features?_x_id=noversion-1651817410.129&token=xoxc-610187951300-604451271234-3473161557912-4c426dd426a45208707725b710302b32dda0ab002b80ccd8c4c8ac9971a11558&platform=sonic&_x_should_cache=false&_x_allow_cached=true&_x_team_id=THY5HTZ8U&_x_gantry=true&fp=7c\n"},
"xoxc-610187951300-604451271234-3473161557912-4c426dd426a45208707725b710302b32dda0ab002b80ccd8c4c8ac9971a11558",
false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := extractTokenGet(tt.args.uri)
if (err != nil) != tt.wantErr {
t.Errorf("extractToken() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("extractToken() got = %v, want %v", got, tt.want)
}
})
}
}

func Test_tokenFromMultipart(t *testing.T) {
type args struct {
s string
boundary string
}
tests := []struct {
name string
args args
want string
wantErr bool
}{
{"ok", args{testMultipart, testBoundary}, "xoxc-888888888888-888888888888-8888888888888-fffffffffffffffa915fe069d70a8ad81743b0ec4ee9c81540af43f5e143264b", false},
{"bad boundary", args{testMultipart, "bad"}, "", true},
{"bad multipart", args{"bad", testBoundary}, "", true},
{"empty", args{"", testBoundary}, "", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := tokenFromMultipart(tt.args.s, tt.args.boundary)
if (err != nil) != tt.wantErr {
t.Errorf("extractTokenPost() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("extractTokenPost() = %v, want %v", got, tt.want)
}
})
}
}

func Test_boundary(t *testing.T) {
tests := []struct {
name string
expect func(r *MockRequest)
want string
wantErr bool
}{
{
"ok",
func(r *MockRequest) {
r.EXPECT().HeaderValues("Content-Type").Return(testHdrValues, nil)
},
testBoundary,
false,
},
{
"no header",
func(r *MockRequest) {
r.EXPECT().HeaderValues("Content-Type").Return(nil, nil)
},
"",
true,
},
{
"bad header",
func(r *MockRequest) {
r.EXPECT().HeaderValues("Content-Type").Return([]string{"bad"}, nil)
},
"",
true,
},
{
"error",
func(r *MockRequest) {
r.EXPECT().HeaderValues("Content-Type").Return(nil, errors.New("bad"))
},
"",
true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
mr := NewMockRequest(ctrl)
tt.expect(mr)
got, err := boundary(mr)
if (err != nil) != tt.wantErr {
t.Errorf("boundary() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("boundary() = %v, want %v", got, tt.want)
}
})
}
}
Loading