Skip to content

Commit

Permalink
Merge pull request rusq#92 from rusq/auth
Browse files Browse the repository at this point in the history
Cherry pick auth changes from login
  • Loading branch information
rusq authored Jul 21, 2022
2 parents 32b889d + e76ae80 commit f79c790
Show file tree
Hide file tree
Showing 19 changed files with 447 additions and 156 deletions.
13 changes: 13 additions & 0 deletions auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,25 @@ import (
"net/http"
)

// Type is the auth type.
type Type uint8

// All supported auth types.
const (
TypeInvalid Type = iota
TypeValue
TypeCookieFile
TypeBrowser
)

// Provider is the Slack Authentication provider.
type Provider interface {
// SlackToken should return the Slack Token value.
SlackToken() string
// Cookies should returns a set of Slack Session cookies.
Cookies() []http.Cookie
// Type returns the auth type.
Type() Type
// Validate should return error, in case the token or cookies cannot be
// retrieved.
Validate() error
Expand Down
45 changes: 45 additions & 0 deletions auth/auth_ui/cli.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package auth_ui

import (
"bufio"
"fmt"
"io"
"os"
"strings"

"github.com/fatih/color"
)

type CLI struct{}

func (*CLI) instructions(w io.Writer) {
const welcome = "Welcome to Slackdump EZ-Login 3000"
underline := color.Set(color.Underline)
fmt.Fprintf(w, "%s\n\n", underline.Sprint(welcome))
fmt.Fprintf(w, "Please read these instructions carefully:\n\n")
fmt.Fprintf(w, "1. Enter the slack workspace name or paste the URL of your slack workspace.\n\n HINT: If https://example.slack.com is the Slack URL of your company,\n then 'example' is the Slack Workspace name\n\n")
fmt.Fprintf(w, "2. Browser will open, login as usual.\n\n")
fmt.Fprintf(w, "3. Browser will close and slackdump will be authenticated.\n\n\n")
}

func (cl *CLI) RequestWorkspace(w io.Writer) (string, error) {
cl.instructions(w)
fmt.Fprint(w, "Enter Slack Workspace Name: ")
workspace, err := readln(os.Stdin)
if err != nil {
return "", err
}
return workspace, nil
}

func (*CLI) Stop() {
return
}

func readln(r io.Reader) (string, error) {
line, err := bufio.NewReader(r).ReadString('\n')
if err != nil {
return "", err
}
return strings.TrimSpace(line), nil
}
92 changes: 92 additions & 0 deletions auth/auth_ui/tview.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package auth_ui

import (
"errors"
"io"

"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
"github.com/rusq/dlog"
)

type TView struct {
app *tview.Application

mustStop chan struct{}
inputReceived chan struct{}
done chan struct{}
}

func (tv *TView) RequestWorkspace(w io.Writer) (string, error) {
tv.inputReceived = make(chan struct{}, 1)
tv.mustStop = make(chan struct{}, 1)
tv.done = make(chan struct{}, 1)
tv.app = tview.NewApplication()

var workspace string
var exit bool
input := tview.NewInputField().SetLabel("Slack Workspace").SetFieldWidth(40)
form := tview.NewForm().
AddFormItem(input).
AddButton("OK", func() {
workspace = input.GetText()
tv.wait()
}).
AddButton("Cancel", func() {
exit = true
tv.wait()
})

form.SetBorder(true).
SetTitle(" Slackdump EZ-Login 3000 ").
SetBackgroundColor(tcell.ColorDarkCyan).
SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if !input.HasFocus() {
return event
}
switch event.Key() {
default:
return event
case tcell.KeyCR:
workspace = input.GetText()
case tcell.KeyESC:
exit = true

}
tv.wait()
return nil
})

go func() {
if err := tv.app.SetRoot(modal(form, 60, 7), true).EnableMouse(true).Run(); err != nil {
dlog.Println(err)
}
}()

// waiting for the user to finish interaction
<-tv.inputReceived
if exit {
tv.app.Stop()
return "", errors.New("operation cancelled")
}
return workspace, nil
}

func (tv *TView) wait() {
close(tv.inputReceived)
<-tv.mustStop
tv.app.Stop()
close(tv.done)
}

func (tv *TView) Stop() {
close(tv.mustStop)
<-tv.done
}

func modal(p tview.Primitive, width int, height int) tview.Primitive {
return tview.NewGrid().
SetColumns(0, width, 0).
SetRows(0, height, 0).
AddItem(p, 1, 1, 1, 1, 0, 0, true)
}
103 changes: 59 additions & 44 deletions auth/browser.go
Original file line number Diff line number Diff line change
@@ -1,69 +1,92 @@
package auth

import (
"bufio"
"fmt"
"context"
"io"
"net/url"
"os"
"strings"

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

"github.com/rusq/slackdump/v2/auth/auth_ui"
"github.com/rusq/slackdump/v2/auth/browser"
)

var _ Provider = BrowserAuth{}
var defaultFlow = &auth_ui.TView{}

type BrowserAuth struct {
simpleProvider
flow BrowserAuthUI
workspace string
}

func NewBrowserAuth() (BrowserAuth, error) {
if err := playwright.Install(&playwright.RunOptions{Browsers: []string{"chromium"}}); err != nil {
return BrowserAuth{}, err
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
}
}

instructions(os.Stdout)
workspace, err := requestWorkspace(os.Stdout)
if err != nil {
return BrowserAuth{}, err
func BrowserWithWorkspace(name string) BrowserOption {
return func(ba *BrowserAuth) {
ba.workspace = name
}
}

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

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

auther, err := browser.New(workspace)
auther, err := browser.New(br.workspace)
if err != nil {
return BrowserAuth{}, err
return br, err
}
token, cookies, err := auther.Authenticate()
token, cookies, err := auther.Authenticate(ctx)
if err != nil {
return BrowserAuth{}, err
return br, err
}
br.simpleProvider = simpleProvider{
token: token,
cookies: cookies,
}
return BrowserAuth{
simpleProvider: simpleProvider{
token: token,
cookies: cookies,
},
}, nil
}

func instructions(w io.Writer) {
const welcome = "Welcome to Slackdump EZ-Login 3000"
underline := color.Set(color.Underline)
fmt.Fprintf(w, "%s\n\n", underline.Sprint(welcome))
fmt.Fprintf(w, "Please read these instructions carefully:\n\n")
fmt.Fprintf(w, "1. Enter the slack workspace name or paste the URL of your slack workspace.\n\n HINT: If https://example.slack.com is the Slack URL of your company,\n then 'example' is the Slack Workspace name\n\n")
fmt.Fprintf(w, "2. Browser will open, login as usual.\n\n")
fmt.Fprintf(w, "3. Browser will close and slackdump will be authenticated.\n\n\n")
return br, nil
}

func requestWorkspace(w io.Writer) (string, error) {
fmt.Fprint(w, "Enter Slack Workspace Name: ")
workspace, err := readln(os.Stdin)
if err != nil {
return "", err
}
return sanitize(workspace)
func (BrowserAuth) Type() Type {
return TypeBrowser
}

func sanitize(workspace string) (string, error) {
Expand All @@ -81,11 +104,3 @@ func sanitize(workspace string) (string, error) {
parts := strings.Split(workspace, ".")
return parts[0], nil
}

func readln(r io.Reader) (string, error) {
line, err := bufio.NewReader(r).ReadString('\n')
if err != nil {
return "", err
}
return strings.TrimSpace(line), nil
}
37 changes: 33 additions & 4 deletions auth/browser/browser.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package browser

import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
"regexp"
"runtime/trace"
"strings"
"time"

Expand All @@ -15,7 +17,8 @@ import (

// Client is the client for Browser Auth Provider.
type Client struct {
workspace string
workspace string
pageClosed chan bool // will receive a notification that the page is closed prematurely.
}

var Logger logger.Interface = logger.Default
Expand All @@ -25,10 +28,13 @@ func New(workspace string) (*Client, error) {
if workspace == "" {
return nil, errors.New("workspace can't be empty")
}
return &Client{workspace: workspace}, nil
return &Client{workspace: workspace, pageClosed: make(chan bool, 1)}, nil
}

func (cl *Client) Authenticate() (string, []http.Cookie, error) {
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
Expand All @@ -54,6 +60,7 @@ func (cl *Client) Authenticate() (string, []http.Cookie, error) {
if err != nil {
return "", nil, err
}
page.On("close", func() { trace.Log(ctx, "user", "page closed"); close(cl.pageClosed) })

uri := fmt.Sprintf("https://%s.slack.com", cl.workspace)
l().Debugf("opening browser URL=%s", uri)
Expand All @@ -62,7 +69,13 @@ func (cl *Client) Authenticate() (string, []http.Cookie, error) {
return "", nil, err
}

r := page.WaitForRequest(uri + "/api/api.features*")
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
Expand All @@ -79,6 +92,22 @@ func (cl *Client) Authenticate() (string, []http.Cookie, error) {
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
}

// 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}`)

Expand Down
4 changes: 4 additions & 0 deletions auth/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,7 @@ func NewCookieFileAuth(token string, cookieFile string) (CookieFileAuth, error)
}
return fc, nil
}

func (CookieFileAuth) Type() Type {
return TypeCookieFile
}
Loading

0 comments on commit f79c790

Please sign in to comment.