Skip to content

Commit

Permalink
http: Client#Request changes; JSON
Browse files Browse the repository at this point in the history
  • Loading branch information
gabriel committed Mar 6, 2021
1 parent 18a6b2c commit 0a53ffd
Show file tree
Hide file tree
Showing 13 changed files with 154 additions and 69 deletions.
90 changes: 62 additions & 28 deletions http/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,33 @@ package http

import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net"
"net/http"
"net/url"
"strings"
"time"
"unicode/utf8"
)

// ErrTimeout is a timeout error.
type ErrTimeout struct {
error
}

// Error is an HTTP Error.
type Error struct {
StatusCode int
// Err is an HTTP Error.
type Err struct {
Code int
Message string
}

func (e Error) Error() string {
return fmt.Sprintf("http error %d", e.StatusCode)
func (e Err) Error() string {
if e.Message != "" {
return e.Message
}
return fmt.Sprintf("http error %d", e.Code)
}

func httpClient() *http.Client {
Expand Down Expand Up @@ -55,13 +61,28 @@ func httpClient() *http.Client {
return client
}

func doRequest(client *http.Client, req *Request, headers []Header, options ...func(*http.Request)) (http.Header, []byte, error) {
logger.Debugf("Requesting %s %s", req.Method, req.URL)
// JSON request.
func JSON(req *Request, v interface{}) error {
hcl := &http.Client{
Timeout: time.Second * 30,
Transport: &http.Transport{
Dial: (&net.Dialer{
Timeout: 10 * time.Second,
}).Dial,
TLSHandshakeTimeout: 10 * time.Second,
},
}

req.Header.Set("User-Agent", "keys.pub")
for _, header := range headers {
req.Header.Set(header.Name, header.Value)
b, err := doRequest(hcl, req)
if err != nil {
return err
}
return json.Unmarshal(b, v)
}

// Do HTTP request.
func doRequest(client *http.Client, req *Request, options ...func(*http.Request)) ([]byte, error) {
logger.Debugf("Requesting %s %s", req.Method, req.URL)

for _, opt := range options {
opt(req)
Expand All @@ -70,7 +91,7 @@ func doRequest(client *http.Client, req *Request, headers []Header, options ...f
resp, err := client.Do(req)
switch err := err.(type) {
default:
return nil, nil, err
return nil, err
case nil:
// no error

Expand All @@ -79,33 +100,44 @@ func doRequest(client *http.Client, req *Request, headers []Header, options ...f
// when exceeding it's `Transport`'s `ResponseHeadersTimeout`
e1, ok := err.Err.(net.Error)
if ok && e1.Timeout() {
return nil, nil, ErrTimeout{err}
return nil, ErrTimeout{err}
}

return nil, nil, err
return nil, err

case net.Error:
// `http.Client.Do` will return a `net.Error` directly when Dial times
// out, or when the Client's RoundTripper otherwise returns an err
if err.Timeout() {
return nil, nil, ErrTimeout{err}
return nil, ErrTimeout{err}
}

return nil, nil, err
return nil, err
}

body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
logger.Debugf("Response body (len=%d)", len(body))

defer resp.Body.Close()
if resp.StatusCode/200 != 1 {
return resp.Header, nil, Error{StatusCode: resp.StatusCode}
}
var errMsg string
if len(body) > 1024 {
body = body[0:1024]
}
if utf8.Valid(body) {
errMsg = string(body)
}

respBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, nil, err
return nil, Err{
Code: resp.StatusCode,
Message: errMsg,
}
}
logger.Debugf("Response body (len=%d)", len(respBody))

return resp.Header, respBody, nil
return body, nil
}

// ErrTemporary means there was a temporary error
Expand Down Expand Up @@ -135,7 +167,7 @@ type Header struct {

// Client for HTTP.
type Client interface {
Request(ctx context.Context, req *Request, headers []Header) ([]byte, error)
Request(ctx context.Context, req *Request) ([]byte, error)
SetProxy(urs string, fn ProxyFn)
}

Expand All @@ -149,7 +181,7 @@ func NewClient() Client {
}

// ProxyFn for proxy.
type ProxyFn func(ctx context.Context, req *Request, headers []Header) ProxyResponse
type ProxyFn func(ctx context.Context, req *Request) ProxyResponse

// ProxyResponse ...
type ProxyResponse struct {
Expand All @@ -167,24 +199,26 @@ func (c *client) SetProxy(urs string, fn ProxyFn) {
}

// Request an URL.
func (c *client) Request(ctx context.Context, req *Request, headers []Header) ([]byte, error) {
func (c *client) Request(ctx context.Context, req *Request) ([]byte, error) {
if c.proxies != nil {
fn := c.proxies[req.URL.String()]
if fn != nil {
pr := fn(ctx, req, headers)
pr := fn(ctx, req)
if !pr.Skip {
return pr.Body, pr.Err
}
}
fn = c.proxies[""]
if fn != nil {
pr := fn(ctx, req, headers)
pr := fn(ctx, req)
if !pr.Skip {
return pr.Body, pr.Err
}
}
}
_, body, err := doRequest(httpClient(), req, headers)

req.Header.Set("User-Agent", "keys.pub")
body, err := doRequest(httpClient(), req)
if err != nil {
logger.Warningf("Failed request: %s", err)
}
Expand Down
1 change: 1 addition & 0 deletions http/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package http_test
2 changes: 1 addition & 1 deletion http/github_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ func TestGithubRequest(t *testing.T) {
urs := "https://gist.github.com/gabriel/ceea0f3b675bac03425472692273cf52"
req, err := http.NewRequest("GET", urs, nil)
require.NoError(t, err)
res, err := client.Request(context.TODO(), req, nil)
res, err := client.Request(context.TODO(), req)
require.NoError(t, err)

out, brand := encoding.FindSaltpack(string(res), true)
Expand Down
2 changes: 1 addition & 1 deletion http/reddit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ func TestReddit(t *testing.T) {
urs := "https://old.reddit.com/r/keyspubmsgs/comments/f8g9vd/gabrlh.json"
req, err := http.NewRequest("GET", urs, nil)
require.NoError(t, err)
res, err := client.Request(context.TODO(), req, nil)
res, err := client.Request(context.TODO(), req)
require.NoError(t, err)

var red reddit
Expand Down
63 changes: 57 additions & 6 deletions http/request.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package http

import (
"bytes"
"encoding/json"
"io"
"net/http"
"time"
Expand All @@ -14,24 +16,73 @@ var NewRequest = http.NewRequest
// NewRequestWithContext alias.
var NewRequestWithContext = http.NewRequestWithContext

// NewAuthRequest returns new authorized/signed HTTP request from keys.
func NewAuthRequest(method string, urs string, body io.Reader, contentHash string, tm time.Time, auth *keys.EdX25519Key) (*http.Request, error) {
return newRequest(method, urs, body, contentHash, tm, keys.RandBytes(24), auth)
// NewAuthRequest returns new authorized/signed HTTP request using auth key.
func NewAuthRequest(method string, urs string, body io.Reader, contentHash string, ts time.Time, key *keys.EdX25519Key) (*http.Request, error) {
return newRequest(method, urs, body, contentHash, ts, keys.RandBytes(24), key)
}

func newRequest(method string, urs string, body io.Reader, contentHash string, tm time.Time, nonce []byte, auth *keys.EdX25519Key) (*http.Request, error) {
ur, err := authURL(urs, tm, nonce)
func newRequest(method string, urs string, body io.Reader, contentHash string, ts time.Time, nonce []byte, key *keys.EdX25519Key) (*http.Request, error) {
ur, err := authURL(urs, ts, nonce)
if err != nil {
return nil, err
}
req, err := http.NewRequest(method, ur.String(), body)
if err != nil {
return nil, err
}
a, err := newAuthWithURL(method, ur, contentHash, auth)
a, err := newAuthWithURL(method, ur, contentHash, key)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", a.Header())
return req, nil
}

// NewJSONRequest ...
func NewJSONRequest(method string, urs string, i interface{}, opt ...RequestOption) (*http.Request, error) {
opts := NewRequestOptions(opt...)
b, err := json.Marshal(i)
if err != nil {
return nil, err
}
if opts.Key != nil {
ts := opts.Timestamp
if ts.IsZero() {
ts = time.Now()
}
return NewAuthRequest(method, urs, bytes.NewReader(b), ContentHash(b), ts, opts.Key)
}
return NewRequest(method, urs, bytes.NewReader(b))
}

// RequestOptions ...
type RequestOptions struct {
Timestamp time.Time
Key *keys.EdX25519Key
}

// RequestOption ...
type RequestOption func(*RequestOptions)

// NewRequestOptions parses RequestOption.
func NewRequestOptions(opts ...RequestOption) RequestOptions {
var options RequestOptions
for _, o := range opts {
o(&options)
}
return options
}

// WithTimestamp to overwride timestamp.
func WithTimestamp(ts time.Time) RequestOption {
return func(o *RequestOptions) {
o.Timestamp = ts
}
}

// SignedWith key.
func SignedWith(key *keys.EdX25519Key) RequestOption {
return func(o *RequestOptions) {
o.Key = key
}
}
10 changes: 4 additions & 6 deletions user/services/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,10 @@ func (s *github) Verify(ctx context.Context, b []byte, usr *user.User) (user.Sta
}

func (s *github) headers() []http.Header {
return []http.Header{
{
Name: "Accept",
Value: "application/vnd.github.v3+json",
},
}
return []http.Header{{
Name: "Accept",
Value: "application/vnd.github.v3+json",
}}
}

type file struct {
Expand Down
4 changes: 1 addition & 3 deletions user/services/reddit.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,7 @@ func (s *reddit) headers(urs string) ([]http.Header, error) {
}
// Not sure if this is required anymore.
if strings.HasSuffix(ur.Host, ".reddit.com") {
return []http.Header{
{Name: "Host", Value: "reddit.com"},
}, nil
return []http.Header{{Name: "Host", Value: "reddit.com"}}, nil
}
return nil, nil
}
Expand Down
7 changes: 5 additions & 2 deletions user/services/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@ func Request(ctx context.Context, client http.Client, urs string, headers []http
if err != nil {
return user.StatusFailure, nil, err
}
b, err := client.Request(ctx, req, headers)
for _, h := range headers {
req.Header.Set(h.Name, h.Value)
}
b, err := client.Request(ctx, req)
if err != nil {
if errHTTP, ok := errors.Cause(err).(http.Error); ok && errHTTP.StatusCode == 404 {
if errHTTP, ok := errors.Cause(err).(http.Err); ok && errHTTP.Code == 404 {
return user.StatusResourceNotFound, nil, errors.Errorf("resource not found")
}
return user.StatusConnFailure, nil, err
Expand Down
6 changes: 3 additions & 3 deletions users/github_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func TestResultGithub(t *testing.T) {
_, err = user.NewSigchainStatement(sc, stu, sk, clock.Now())
require.EqualError(t, err, "user set in sigchain already")

usrs.Client().SetProxy("", func(ctx context.Context, req *http.Request, headers []http.Header) http.ProxyResponse {
usrs.Client().SetProxy("", func(ctx context.Context, req *http.Request) http.ProxyResponse {
return http.ProxyResponse{Body: []byte(githubMock("alice", "1", msg))}
})

Expand Down Expand Up @@ -94,7 +94,7 @@ func TestResultGithubWrongName(t *testing.T) {

sc := keys.NewSigchain(sk.ID())

usrs.Client().SetProxy("", func(ctx context.Context, req *http.Request, headers []http.Header) http.ProxyResponse {
usrs.Client().SetProxy("", func(ctx context.Context, req *http.Request) http.ProxyResponse {
return http.ProxyResponse{Body: []byte(githubMock("alice", "1", msg))}
})

Expand Down Expand Up @@ -127,7 +127,7 @@ func TestResultGithubWrongService(t *testing.T) {
msg, err := invalid.Sign(sk)
require.NoError(t, err)

usrs.Client().SetProxy("", func(ctx context.Context, req *http.Request, headers []http.Header) http.ProxyResponse {
usrs.Client().SetProxy("", func(ctx context.Context, req *http.Request) http.ProxyResponse {
return http.ProxyResponse{Body: []byte(githubMock("alice", "1", msg))}
})

Expand Down
2 changes: 1 addition & 1 deletion users/reddit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func TestResultReddit(t *testing.T) {
_, err = user.NewSigchainStatement(sc, stu, sk, clock.Now())
require.EqualError(t, err, "user set in sigchain already")

usrs.Client().SetProxy("", func(ctx context.Context, req *http.Request, headers []http.Header) http.ProxyResponse {
usrs.Client().SetProxy("", func(ctx context.Context, req *http.Request) http.ProxyResponse {
return http.ProxyResponse{Body: testdata(t, "testdata/reddit/charlie.json")}
})

Expand Down
Loading

0 comments on commit 0a53ffd

Please sign in to comment.