Skip to content

Commit

Permalink
rework how auth options work + add browser flag
Browse files Browse the repository at this point in the history
  • Loading branch information
rusq committed Nov 21, 2022
1 parent 7fc5f22 commit 0445418
Show file tree
Hide file tree
Showing 11 changed files with 342 additions and 222 deletions.
50 changes: 18 additions & 32 deletions auth/browser.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import (
"os"
"strings"

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

"github.com/rusq/slackdump/v2/auth/auth_ui"
"github.com/rusq/slackdump/v2/auth/browser"
)
Expand All @@ -18,58 +16,46 @@ var defaultFlow = &auth_ui.Survey{}

type BrowserAuth struct {
simpleProvider
flow BrowserAuthUI
opts browserOpts
}

type browserOpts struct {
workspace string
browser browser.Browser
flow BrowserAuthUI
}

type BrowserAuthUI interface {
RequestWorkspace(w io.Writer) (string, error)
Stop()
}

type BrowserOption func(*BrowserAuth)

func BrowserWithAuthFlow(flow BrowserAuthUI) BrowserOption {
return func(ba *BrowserAuth) {
if flow == nil {
return
}
ba.flow = flow
}
}

func BrowserWithWorkspace(name string) BrowserOption {
return func(ba *BrowserAuth) {
ba.workspace = name
}
}

func NewBrowserAuth(ctx context.Context, opts ...BrowserOption) (BrowserAuth, error) {
func NewBrowserAuth(ctx context.Context, opts ...Option) (BrowserAuth, error) {
var br = BrowserAuth{
flow: defaultFlow,
opts: browserOpts{
flow: defaultFlow,
browser: browser.Bfirefox,
},
}
for _, opt := range opts {
opt(&br)
opt(&options{browserOpts: &br.opts})
}

if err := playwright.Install(&playwright.RunOptions{Browsers: []string{"chromium"}}); err != nil {
return br, err
}
if br.workspace == "" {
if br.opts.workspace == "" {
var err error
br.workspace, err = br.flow.RequestWorkspace(os.Stdout)
br.opts.workspace, err = br.opts.flow.RequestWorkspace(os.Stdout)
if err != nil {
return br, err
}
defer br.flow.Stop()
defer br.opts.flow.Stop()
}
if wsp, err := sanitize(br.workspace); err != nil {
if wsp, err := sanitize(br.opts.workspace); err != nil {
return br, err
} else {
br.workspace = wsp
br.opts.workspace = wsp
}

auther, err := browser.New(br.workspace)
auther, err := browser.New(br.opts.workspace, browser.OptBrowser(br.opts.browser))
if err != nil {
return br, err
}
Expand Down
212 changes: 33 additions & 179 deletions auth/browser/browser.go
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
}
24 changes: 24 additions & 0 deletions auth/browser/browser_string.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 0445418

Please sign in to comment.