Skip to content

Commit 35eeb0e

Browse files
authored
Add Login() method for native authentication. (#22)
* add Native auth support, move req into new file * Make Login work * minor updates, gomock
1 parent 1126b0f commit 35eeb0e

File tree

8 files changed

+223
-144
lines changed

8 files changed

+223
-144
lines changed

go.mod

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,7 @@ module golift.io/starr
22

33
go 1.17
44

5-
require github.com/golang/mock v1.6.0
5+
require (
6+
github.com/golang/mock v1.6.0
7+
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f // publicsuffix, cookiejar.
8+
)

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
77
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
88
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
99
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
10+
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f h1:hEYJvxw1lSnWIl8X9ofsYMklzaDs90JI2az5YMd4fPM=
11+
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
1012
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
1113
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
1214
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=

http.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,12 @@ func (c *Config) setHeaders(req *http.Request) {
7272
req.Header.Set("Content-Type", "application/json")
7373
}
7474

75-
req.Header.Set("Accept", "application/json")
75+
if req.Method == http.MethodPost && strings.HasSuffix(req.URL.RequestURI(), "/login") {
76+
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
77+
} else {
78+
req.Header.Set("Accept", "application/json")
79+
}
80+
7681
req.Header.Set("User-Agent", "go-starr: https://"+reflect.TypeOf(Config{}).PkgPath()) //nolint:exhaustivestruct
7782
req.Header.Set("X-API-Key", c.APIKey)
7883
}
@@ -92,7 +97,7 @@ func (c *Config) getBody(req *http.Request) (int, []byte, http.Header, error) {
9297

9398
// #############################################
9499
// DEBUG: useful for viewing payloads from apps.
95-
// fmt.Println(resp.StatusCode, string(body))
100+
// log.Println(resp.StatusCode, resp.Header.Get("location"), string(body))
96101
// #############################################
97102

98103
if resp.StatusCode < 200 || resp.StatusCode > 299 {

interface.go

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,17 @@ import (
1010
"fmt"
1111
"io"
1212
"net/http"
13+
"net/http/cookiejar"
1314
"net/url"
15+
"strings"
16+
17+
"golang.org/x/net/publicsuffix"
1418
)
1519

1620
// APIer is used by the sub packages to allow mocking the http methods in tests.
1721
// This also allows consuming packages to override methods.
1822
type APIer interface {
23+
Login() error // Only needed for non-API paths, like backup downloads. Requires Username and Password being set.
1924
Get(path string, params url.Values) (respBody []byte, err error)
2025
Post(path string, params url.Values, postBody []byte) (respBody []byte, err error)
2126
Put(path string, params url.Values, putBody []byte) (respBody []byte, err error)
@@ -63,6 +68,39 @@ func (c *Config) log(code int, data, body []byte, header http.Header, path, meth
6368
}
6469
}
6570

71+
// Login POSTs to the login form in a Starr app and saves the authentication cookie for future use.
72+
func (c *Config) Login() error {
73+
if c.Client.Jar == nil {
74+
jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
75+
if err != nil {
76+
return fmt.Errorf("cookiejar.New(publicsuffix): %w", err)
77+
}
78+
79+
c.Client.Jar = jar
80+
}
81+
82+
post := []byte("username=" + c.Username + "&password=" + c.Password)
83+
84+
code, resp, header, err := c.body(context.Background(), "/login", http.MethodPost, nil, bytes.NewBuffer(post))
85+
c.log(code, nil, post, header, c.URL+"/login", http.MethodPost, err)
86+
87+
if err != nil {
88+
return fmt.Errorf("authenticating as user '%s' failed: %w", c.Username, err)
89+
}
90+
defer resp.Close()
91+
92+
_, _ = io.Copy(io.Discard, resp)
93+
94+
if u, _ := url.Parse(c.URL); strings.Contains(header.Get("location"), "loginFailed") ||
95+
len(c.Client.Jar.Cookies(u)) == 0 {
96+
return fmt.Errorf("%w: authenticating as user '%s' failed", ErrRequestError, c.Username)
97+
}
98+
99+
c.cookie = true
100+
101+
return nil
102+
}
103+
66104
// Get makes a GET http request and returns the body.
67105
func (c *Config) Get(path string, params url.Values) ([]byte, error) {
68106
code, data, header, err := c.req(path, http.MethodGet, params, nil)
@@ -74,7 +112,7 @@ func (c *Config) Get(path string, params url.Values) ([]byte, error) {
74112
// Post makes a POST http request and returns the body.
75113
func (c *Config) Post(path string, params url.Values, postBody []byte) ([]byte, error) {
76114
code, data, header, err := c.req(path, http.MethodPost, params, bytes.NewBuffer(postBody))
77-
c.log(code, data, postBody, header, c.setPathParams(path, params), http.MethodPut, err)
115+
c.log(code, data, postBody, header, c.setPathParams(path, params), http.MethodPost, err)
78116

79117
return data, err
80118
}
@@ -130,7 +168,7 @@ func (c *Config) DeleteInto(path string, params url.Values, v interface{}) error
130168
// If it's not 200, it's possible the request had an error or was not authenticated.
131169
func (c *Config) GetBody(ctx context.Context, path string, params url.Values) (io.ReadCloser, int, error) {
132170
code, data, header, err := c.body(ctx, path, http.MethodGet, params, nil)
133-
c.log(code, nil, nil, header, c.setPathParams(path, params), http.MethodGet, err)
171+
c.log(code, nil, nil, header, c.URL+path, http.MethodGet, err)
134172

135173
return data, code, err
136174
}
@@ -142,7 +180,7 @@ func (c *Config) GetBody(ctx context.Context, path string, params url.Values) (i
142180
func (c *Config) PostBody(ctx context.Context, path string, params url.Values,
143181
postBody []byte) (io.ReadCloser, int, error) {
144182
code, data, header, err := c.body(ctx, path, http.MethodPost, params, bytes.NewBuffer(postBody))
145-
c.log(code, nil, postBody, header, c.setPathParams(path, params), http.MethodPut, err)
183+
c.log(code, nil, postBody, header, c.URL+path, http.MethodPost, err)
146184

147185
return data, code, err
148186
}
@@ -153,7 +191,7 @@ func (c *Config) PostBody(ctx context.Context, path string, params url.Values,
153191
func (c *Config) PutBody(ctx context.Context, path string, params url.Values,
154192
putBody []byte) (io.ReadCloser, int, error) {
155193
code, data, header, err := c.body(ctx, path, http.MethodPut, params, bytes.NewBuffer(putBody))
156-
c.log(code, nil, putBody, header, c.setPathParams(path, params), http.MethodPut, err)
194+
c.log(code, nil, putBody, header, c.URL+path, http.MethodPut, err)
157195

158196
return data, code, err
159197
}
@@ -164,7 +202,7 @@ func (c *Config) PutBody(ctx context.Context, path string, params url.Values,
164202
// If it's not 200, it's possible the request had an error or was not authenticated.
165203
func (c *Config) DeleteBody(ctx context.Context, path string, params url.Values) (io.ReadCloser, int, error) {
166204
code, data, header, err := c.body(ctx, path, http.MethodDelete, params, nil)
167-
c.log(code, nil, nil, header, c.setPathParams(path, params), http.MethodDelete, err)
205+
c.log(code, nil, nil, header, c.URL+path, http.MethodDelete, err)
168206

169207
return data, code, err
170208
}

mocks/apier.go

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

paginate.go

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
package starr
2+
3+
import (
4+
"net/url"
5+
"strconv"
6+
"strings"
7+
)
8+
9+
/* This file containers helper methods and types for page-able API calls.
10+
* Like GetHistory() and GetQueue().
11+
*/
12+
13+
// Req is the input to search requests that have page-able responses.
14+
// These are turned into HTTP parameters.
15+
type Req struct {
16+
PageSize int // 10 is default if not provided.
17+
Page int // 1 or higher
18+
SortKey string // date, timeleft, others?
19+
SortDir string // asc, desc
20+
url.Values // Additional values that may be set.
21+
}
22+
23+
// Params returns a brand new url.Values with all request parameters combined.
24+
func (r *Req) Params() url.Values {
25+
params := make(url.Values)
26+
27+
if r.Page > 0 {
28+
params.Set("page", strconv.Itoa(r.Page))
29+
} else {
30+
params.Set("page", "1")
31+
}
32+
33+
if r.PageSize > 0 {
34+
params.Set("pageSize", strconv.Itoa(r.PageSize))
35+
} else {
36+
params.Set("pageSize", "10")
37+
}
38+
39+
if r.SortKey != "" {
40+
params.Set("sortKey", r.SortKey)
41+
} else {
42+
params.Set("sortKey", "date") // timeleft, title, id
43+
}
44+
45+
if r.SortDir != "" {
46+
params.Set("sortDirection", r.SortDir)
47+
} else {
48+
params.Set("sortDirection", "ascending") // descending
49+
}
50+
51+
for k, v := range r.Values {
52+
for _, val := range v {
53+
params.Set(k, val)
54+
}
55+
}
56+
57+
return params
58+
}
59+
60+
// Encode turns our request parameters into a URI string.
61+
func (r *Req) Encode() string {
62+
return r.Params().Encode()
63+
}
64+
65+
// CheckSet sets a request parameter if it's not already set.
66+
func (r *Req) CheckSet(key, value string) { //nolint:cyclop
67+
switch strings.ToLower(key) {
68+
case "page":
69+
if r.Page == 0 {
70+
r.Page, _ = strconv.Atoi(value)
71+
}
72+
case "pagesize":
73+
if r.PageSize == 0 {
74+
r.PageSize, _ = strconv.Atoi(value)
75+
}
76+
case "sortkey":
77+
if r.SortKey == "" {
78+
r.SortKey = value
79+
}
80+
case "sortdirection":
81+
if r.SortDir == "" {
82+
r.SortDir = value
83+
}
84+
default:
85+
if r.Values == nil || r.Values.Get(key) == "" {
86+
r.Values.Set(key, value)
87+
}
88+
}
89+
}
90+
91+
// Set sets a request parameter.
92+
func (r *Req) Set(key, value string) {
93+
switch strings.ToLower(key) {
94+
case "page":
95+
r.Page, _ = strconv.Atoi(value)
96+
case "pagesize":
97+
r.PageSize, _ = strconv.Atoi(value)
98+
case "sortkey":
99+
r.SortKey = value
100+
case "sortdirection":
101+
r.SortDir = value
102+
default:
103+
if r.Values == nil {
104+
r.Values = make(url.Values)
105+
}
106+
107+
r.Values.Set(key, value)
108+
}
109+
}
110+
111+
// SetPerPage returns a proper perPage value that is not equal to zero,
112+
// and not larger than the record count desired. If the count is zero, then
113+
// perPage can be anything other than zero.
114+
// This is used by paginated methods in the starr modules.
115+
func SetPerPage(records, perPage int) int {
116+
const perPageDefault = 500
117+
118+
if perPage <= 1 {
119+
if records > perPageDefault || records == 0 {
120+
perPage = perPageDefault
121+
} else {
122+
perPage = records
123+
}
124+
} else if perPage > records && records != 0 {
125+
perPage = records
126+
}
127+
128+
return perPage
129+
}
130+
131+
// AdjustPerPage to make sure we don't go over, or ask for more records than exist.
132+
// This is used by paginated methods in the starr modules.
133+
// 'records' is the number requested, 'total' is the number in the app,
134+
// 'collected' is how many we have so far, and 'perPage' is the current perPage setting.
135+
func AdjustPerPage(records, total, collected, perPage int) int {
136+
// Do not ask for more than was requested.
137+
if d := records - collected; perPage > d && d > 0 {
138+
perPage = d
139+
}
140+
141+
// Ask for only the known total.
142+
if d := total - collected; perPage > d {
143+
perPage = d
144+
}
145+
146+
return perPage
147+
}

0 commit comments

Comments
 (0)