-
-
Notifications
You must be signed in to change notification settings - Fork 73
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
rework how auth options work + add browser flag
- Loading branch information
Showing
11 changed files
with
342 additions
and
222 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,201 +1,55 @@ | ||
package browser | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"fmt" | ||
"net/http" | ||
"net/url" | ||
"regexp" | ||
"runtime/trace" | ||
"strings" | ||
"time" | ||
|
||
"github.com/playwright-community/playwright-go" | ||
"github.com/rusq/slackdump/v2/logger" | ||
) | ||
|
||
const slackDomain = ".slack.com" | ||
//go:generate stringer -type Browser -trimprefix=B browser.go | ||
type Browser int | ||
|
||
// Client is the client for Browser Auth Provider. | ||
type Client struct { | ||
workspace string | ||
pageClosed chan bool // will receive a notification that the page is closed prematurely. | ||
} | ||
|
||
var Logger logger.Interface = logger.Default | ||
|
||
// New create new browser based client | ||
func New(workspace string) (*Client, error) { | ||
if workspace == "" { | ||
return nil, errors.New("workspace can't be empty") | ||
} | ||
if err := playwright.Install(&playwright.RunOptions{ | ||
Browsers: []string{"firefox"}, | ||
}); err != nil { | ||
return nil, err | ||
} | ||
return &Client{workspace: workspace, pageClosed: make(chan bool, 1)}, nil | ||
} | ||
|
||
func (cl *Client) Authenticate(ctx context.Context) (string, []http.Cookie, error) { | ||
|
||
ctx, task := trace.NewTask(ctx, "Authenticate") | ||
defer task.End() | ||
|
||
pw, err := playwright.Run() | ||
if err != nil { | ||
return "", nil, err | ||
} | ||
defer pw.Stop() | ||
|
||
opts := playwright.BrowserTypeLaunchOptions{ | ||
Headless: playwright.Bool(false), | ||
} | ||
browser, err := pw.Firefox.Launch(opts) | ||
if err != nil { | ||
return "", nil, err | ||
} | ||
defer browser.Close() | ||
|
||
context, err := browser.NewContext() | ||
if err != nil { | ||
return "", nil, err | ||
} | ||
defer context.Close() | ||
|
||
var ( | ||
_s = playwright.String | ||
_f = playwright.Float | ||
) | ||
if err := context.AddCookies(playwright.BrowserContextAddCookiesOptionsCookies{ | ||
Domain: _s(slackDomain), | ||
Path: _s("/"), | ||
Name: _s("OptanonAlertBoxClosed"), | ||
Value: _s(time.Now().Add(-10 * time.Minute).Format(time.RFC3339)), | ||
Expires: _f(float64(time.Now().AddDate(0, 0, 30).Unix())), | ||
}); err != nil { | ||
return "", nil, err | ||
} | ||
|
||
page, err := context.NewPage() | ||
if err != nil { | ||
return "", nil, err | ||
} | ||
page.On("close", func() { trace.Log(ctx, "user", "page closed"); close(cl.pageClosed) }) | ||
|
||
uri := fmt.Sprintf("https://%s"+slackDomain, cl.workspace) | ||
l().Debugf("opening browser URL=%s", uri) | ||
|
||
if _, err := page.Goto(uri); err != nil { | ||
return "", nil, err | ||
} | ||
|
||
var r playwright.Request | ||
if err := cl.withBrowserGuard(ctx, func() { | ||
r = page.WaitForRequest(uri + "/api/api.features*") | ||
}); err != nil { | ||
return "", nil, err | ||
} | ||
|
||
token, err := extractToken(r.URL()) | ||
if err != nil { | ||
return "", nil, err | ||
} | ||
|
||
state, err := context.StorageState() | ||
if err != nil { | ||
return "", nil, err | ||
} | ||
if len(state.Cookies) == 0 { | ||
return "", nil, errors.New("empty cookies") | ||
} | ||
|
||
return token, convertCookies(state.Cookies), nil | ||
} | ||
|
||
func (cl *Client) withBrowserGuard(ctx context.Context, fn func()) error { | ||
var done = make(chan struct{}) | ||
go func() { | ||
defer close(done) | ||
fn() | ||
}() | ||
select { | ||
case <-ctx.Done(): | ||
return ctx.Err() | ||
case <-cl.pageClosed: | ||
return errors.New("browser closed") | ||
case <-done: | ||
} | ||
return nil | ||
} | ||
const ( | ||
Bfirefox Browser = iota | ||
Bchromium | ||
) | ||
|
||
// 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}`) | ||
type Option func(*Client) | ||
|
||
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") | ||
func OptBrowser(b Browser) Option { | ||
return func(c *Client) { | ||
if b < Bfirefox || Bchromium < b { | ||
b = Bfirefox | ||
} | ||
c.br = b | ||
} | ||
return token, nil | ||
} | ||
|
||
func convertCookies(pwc []playwright.Cookie) []http.Cookie { | ||
var ret = make([]http.Cookie, 0, len(pwc)) | ||
for _, p := range pwc { | ||
if !strings.HasSuffix(p.Domain, slackDomain) { | ||
// ignoring filth (thirdparty tracking cookies) | ||
continue | ||
func (e *Browser) Set(v string) error { | ||
v = strings.ToLower(v) | ||
for i := 0; i < len(_Browser_index)-1; i++ { | ||
if strings.ToLower(_Browser_name[_Browser_index[i]:_Browser_index[i+1]]) == v { | ||
*e = Browser(i) | ||
return nil | ||
} | ||
ret = append(ret, http.Cookie{ | ||
Name: p.Name, | ||
Value: p.Value, | ||
Path: p.Path, | ||
Domain: p.Domain, | ||
Expires: float2time(p.Expires), | ||
MaxAge: 0, | ||
Secure: p.Secure, | ||
HttpOnly: p.HttpOnly, | ||
SameSite: sameSite(p.SameSite), | ||
}) | ||
} | ||
return ret | ||
} | ||
|
||
var str2samesite = map[string]http.SameSite{ | ||
"": http.SameSiteDefaultMode, | ||
"Lax": http.SameSiteLaxMode, | ||
"None": http.SameSiteNoneMode, | ||
"Strict": http.SameSiteStrictMode, | ||
} | ||
|
||
// sameSite returns the constant value that maps to the string value of SameSite. | ||
func sameSite(val string) http.SameSite { | ||
return str2samesite[val] | ||
} | ||
|
||
// float2time converts a float value of Unix time to time, nanoseconds value | ||
// is discarded. If v == -1, it returns the date approximately 5 years from | ||
// Now(). | ||
func float2time(v float64) time.Time { | ||
if v == -1.0 { | ||
return time.Now().Add(5 * 365 * 24 * time.Hour) | ||
var allowed []string | ||
for i := 0; i < len(_Browser_index)-1; i++ { | ||
allowed = append(allowed, _Browser_name[_Browser_index[i]:_Browser_index[i+1]]) | ||
} | ||
return time.Unix(int64(v), 0) | ||
return fmt.Errorf("unknown browser: %s, allowed: %v", v, allowed) | ||
} | ||
|
||
func l() logger.Interface { | ||
if Logger == nil { | ||
return logger.Default | ||
// client returns the appropriate client from playwright.Playwright. | ||
func (br Browser) client(pw *playwright.Playwright) playwright.BrowserType { | ||
switch br { | ||
default: | ||
fallthrough | ||
case Bfirefox: | ||
return pw.Firefox | ||
case Bchromium: | ||
return pw.Chromium | ||
} | ||
return Logger | ||
// unreachable | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Oops, something went wrong.