Skip to content
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
70 changes: 60 additions & 10 deletions cmd/playground/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,17 @@ import (

"github.com/joho/godotenv"
"github.com/rusq/chttp"

"github.com/rusq/slackauth/internal/qrslack"

"github.com/rusq/slackauth"
)

var _ = godotenv.Load()

var (
auto = flag.Bool("auto", false, "attempt auto login")
qr = flag.Bool("qr", false, "attempt qr code login")
forceNew = flag.Bool("force-user", false, "force open a user browser, instead of the clean one")
bundled = flag.Bool("bundled", false, "force using a bundled browser")
isDebug = flag.Bool("d", os.Getenv("DEBUG") == "1", "enable debug")
Expand Down Expand Up @@ -73,7 +77,7 @@ func run(ctx context.Context) error {
defer trace.Stop()
}

c, err := initClient(envOrScan("AUTH_WORKSPACE", "Enter workspace: "), *isDebug)
c, err := initClient(envOrScan(ctx, "AUTH_WORKSPACE", "Enter workspace: "), *isDebug)
if err != nil {
return err
}
Expand All @@ -92,11 +96,14 @@ func run(ctx context.Context) error {
token string
cookies []*http.Cookie
)
if *auto {
username := envOrScan("EMAIL", "Enter email: ")
password := envOrScan("PASSWORD", "Enter password: ")
switch {
case *auto:
username := envOrScan(ctx, "EMAIL", "Enter email: ")
password := envOrScan(ctx, "PASSWORD", "Enter password: ")
token, cookies, err = autoLogin(ctx, c, username, password)
} else {
case *qr:
token, cookies, err = qrLogin(ctx, c)
default:
token, cookies, err = browserLogin(ctx, c)
}
if err != nil {
Expand Down Expand Up @@ -202,6 +209,31 @@ func autoLogin(ctx context.Context, c *slackauth.Client, username, password stri
return token, cookies, nil
}

func qrLogin(ctx context.Context, c *slackauth.Client) (string, []*http.Cookie, error) {
ctx, task := trace.NewTask(ctx, "qrLogin")
defer task.End()
// read image data from stdin
imgData := envOrScan(ctx, "QR_CODE", "Paste encoded image data:")

// ctx, cancel := context.WithTimeoutCause(ctx, 180*time.Second, errors.New("user too slow"))
// defer cancel()
// _ = ctx
loginURL, err := qrslack.Decode(imgData)
if err != nil {
return "", nil, err
}
fmt.Println("Decoded:", loginURL)
start := time.Now()
token, cookies, err := c.QRAuth(ctx, imgData)
if err != nil {
return "", nil, err
}
slog.Info("login duration", "d", time.Since(start))
fmt.Println(token)
printCookies(cookies)
return token, cookies, nil
}

func printCookies(cookies []*http.Cookie) {
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0)
defer tw.Flush()
Expand All @@ -211,14 +243,32 @@ func printCookies(cookies []*http.Cookie) {
}
}

func envOrScan(env, prompt string) string {
func envOrScan(ctx context.Context, env, prompt string) string {
v := os.Getenv(env)
if v != "" {
return v
}
for v == "" {
fmt.Print(prompt)
fmt.Scanln(&v)
resC := make(chan string)
errC := make(chan error)
go func() {
for v == "" {
fmt.Print(prompt + " ")
_, err := fmt.Scanln(&v)
if err != nil {
errC <- err
return
}
}
resC <- v
}()
select {
case <-ctx.Done():
return ""
case v := <-resC:
return v
case err := <-errC:
fmt.Println("user chose not to continue this journey:", err)
os.Exit(2)
}
return v
return "" //should never get here
}
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/rusq/slackauth
go 1.22

require (
github.com/caiguanhao/readqr v1.0.0
github.com/go-rod/rod v0.116.2
github.com/joho/godotenv v1.5.1
github.com/rusq/chttp v1.0.2
Expand All @@ -19,5 +20,7 @@ require (
github.com/ysmood/gson v0.7.3 // indirect
github.com/ysmood/leakless v0.9.0 // indirect
golang.org/x/net v0.30.0 // indirect
golang.org/x/text v0.19.0 // indirect
golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
github.com/caiguanhao/readqr v1.0.0 h1:axynewywpUyqZxFjKPtEbr97PzSOMrJsfn9bKkp+22w=
github.com/caiguanhao/readqr v1.0.0/go.mod h1:oaAqEl5Zt0XzeIJf7nCEzJFz4is8rfE+Vgiw8b07vMM=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-rod/rod v0.116.2 h1:A5t2Ky2A+5eD/ZJQr1EfsQSe5rms5Xof/qj296e+ZqA=
Expand Down Expand Up @@ -28,6 +30,10 @@ go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f h1:GGU+dLjvlC3qDwqYgL6UgRmHXhOOgns0bZu2Ty5mm6U=
golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
Expand Down
26 changes: 14 additions & 12 deletions hijacker.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ import (
"github.com/go-rod/rod"
)

// hijacker is a helper for hijacking requests.
// hijacker is a contraption to hijack the request holding the token. Once the
// request is captured, token value is extracted and sent on the credsC
// channel. Caller may retrieve it by calling [Token] method.
type hijacker struct {
r *rod.HijackRouter
credsC chan creds
lg Logger
}

// creds holds token and an error, and is communicated through the credsC
// channel of the hijacker.
type creds struct {
Token string
Err error
Expand All @@ -39,20 +39,21 @@ func newHijacker(ctx context.Context, page *rod.Page, lg Logger) (*hijacker, err
return hj, nil
}

func (hj *hijacker) hook(h *rod.Hijack) {
hj.lg.Debug("hijack api.features")
func (h *hijacker) hook(rh *rod.Hijack) {
h.lg.Debug("hijack api.features")

r := h.Request.Req()
r := rh.Request.Req()

token, err := extractToken(r)
if err != nil {
hj.credsC <- creds{Err: fmt.Errorf("error parsing token out of request: %v", err)}
h.credsC <- creds{Err: fmt.Errorf("error parsing token out of request: %v", err)}
return
}

hj.credsC <- creds{Token: token}
h.credsC <- creds{Token: token}
}

// Stop terminates the hijacker and disables request hooks.
func (h *hijacker) Stop() error {
defer close(h.credsC)
if err := h.r.Stop(); err != nil {
Expand All @@ -61,7 +62,8 @@ func (h *hijacker) Stop() error {
return nil
}

// Token waits for the hijacker to receive a token or an error.
// Token returns the token value or an error. If the token has not yet been
// captured, it blocks until hijacker has captured the token value.
func (h *hijacker) Token(ctx context.Context) (string, error) {
ctx, task := trace.NewTask(ctx, "Token")
defer task.End()
Expand All @@ -74,12 +76,12 @@ func (h *hijacker) Token(ctx context.Context) (string, error) {
}

const (
maxMem = 131072
paramToken = "token"
maxFormParseMem = 131072 // maximum memory for the multipart form parser
paramToken = "token" // token form field name
)

func extractToken(r *http.Request) (string, error) {
if err := r.ParseMultipartForm(maxMem); err != nil {
if err := r.ParseMultipartForm(maxFormParseMem); err != nil {
return "", fmt.Errorf("error parsing request: %w", err)
}
tok := strings.TrimSpace(r.Form.Get(paramToken))
Expand Down
Binary file added internal/qrslack/fixtures/test.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
87 changes: 87 additions & 0 deletions internal/qrslack/qrslack.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Package qrslack contains slack QR decode logic.
package qrslack

import (
"bytes"
"encoding/base64"
"errors"
"fmt"
"image"
"io"
"strings"

"github.com/caiguanhao/readqr"

"image/png"
)

const (
maxDataSz = 1 << 16
)

var (
ErrInvalidQR = errors.New("invalid QR code")

errHdrLen = errors.New("unexpected header length")
errInvalidHdr = errors.New("invalid header")
errNoData = errors.New("no image data")
)

func Decode(urlImgData string) (string, error) {
pngbytes, err := decodeB64(strings.NewReader(urlImgData))
if err != nil {
return "", err
}
img, err := decodeImage(bytes.NewReader(pngbytes))
if err != nil {
return "", err
}
return decodeQR(img)
}

func decodeB64(r io.Reader) ([]byte, error) {
const (
hdr = `data:image/png;base64,`
hdrLen = int64(len(hdr))
)
// read first 22 bytes
data, err := io.ReadAll(io.LimitReader(r, hdrLen))
if err != nil {
if errors.Is(err, io.EOF) {
return nil, errHdrLen
}
return nil, fmt.Errorf("read header: %w", err)
}
if !strings.EqualFold(hdr, string(data)) {
return nil, errInvalidHdr
}

b64r := base64.NewDecoder(base64.StdEncoding, io.LimitReader(r, maxDataSz))
encoded, err := io.ReadAll(b64r)
if err != nil {
if errors.Is(err, io.EOF) {
return nil, errNoData
}
return nil, fmt.Errorf("read data: %w", err)
}
return encoded, nil
}

func decodeImage(r io.Reader) (image.Image, error) {
img, err := png.Decode(r)
if err != nil {
return nil, err
}
if img.Bounds().Dx() != img.Bounds().Dy() {
return nil, ErrInvalidQR
}
return img, nil
}

func decodeQR(m image.Image) (string, error) {
result, err := readqr.DecodeImage(m)
if err != nil {
return "", err
}
return result, nil
}
Loading