Skip to content

Commit 06aa6d5

Browse files
[BREAKING] feat: refactor Storage to store a complete *http.Response instead of just response bytes (#4)
2 parents 58985f2 + 0cd102b commit 06aa6d5

File tree

12 files changed

+415
-180
lines changed

12 files changed

+415
-180
lines changed

bbolt/go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@ module github.com/bored-engineer/github-conditional-http-transport/bbolt
22

33
go 1.23.5
44

5-
require go.etcd.io/bbolt v1.3.11
5+
require go.etcd.io/bbolt v1.4.2
66

7-
require golang.org/x/sys v0.4.0 // indirect
7+
require golang.org/x/sys v0.33.0 // indirect

bbolt/go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,13 @@ github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKs
66
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
77
go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0=
88
go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I=
9+
go.etcd.io/bbolt v1.4.2 h1:IrUHp260R8c+zYx/Tm8QZr04CX+qWS5PGfPdevhdm1I=
10+
go.etcd.io/bbolt v1.4.2/go.mod h1:Is8rSHO/b4f3XigBC0lL0+4FwAQv3HXEEIgFMuKHceM=
911
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
1012
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
1113
golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
1214
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
15+
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
16+
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
1317
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
1418
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

bbolt/storage.go

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package bboltstorage
22

33
import (
4+
"bufio"
45
"bytes"
56
"context"
6-
"io"
7+
"fmt"
78
"net/http"
9+
"net/http/httputil"
810
"net/url"
911
"os"
1012

@@ -17,34 +19,51 @@ type Storage struct {
1719
Bucket []byte
1820
}
1921

20-
func (s *Storage) Get(ctx context.Context, u *url.URL) (body io.ReadCloser, header http.Header, err error) {
22+
func (s *Storage) Get(ctx context.Context, u *url.URL) (*http.Response, error) {
23+
var bodyBytes []byte
2124
if err := s.DB.View(func(tx *bbolt.Tx) error {
2225
bucket := tx.Bucket(s.Bucket)
2326
if bucket == nil {
2427
return bbolt.ErrBucketNotFound
2528
}
26-
bodyBytes := bucket.Get([]byte(u.String()))
27-
if bodyBytes == nil {
29+
bodyBytesUnsafe := bucket.Get([]byte(u.String()))
30+
if bodyBytesUnsafe == nil {
2831
return nil
2932
}
30-
bodyBytesSafe := make([]byte, len(bodyBytes))
31-
copy(bodyBytesSafe, bodyBytes)
32-
body = io.NopCloser(bytes.NewReader(bodyBytesSafe))
33+
bodyBytes = make([]byte, len(bodyBytesUnsafe))
34+
copy(bodyBytes, bodyBytesUnsafe)
3335
return nil
3436
}); err != nil {
35-
return nil, nil, err
37+
return nil, fmt.Errorf("(*bbolt.DB).View failed: %w", err)
3638
}
37-
return
39+
if bodyBytes == nil {
40+
return nil, nil
41+
}
42+
resp, err := http.ReadResponse(bufio.NewReader(bytes.NewReader(bodyBytes)), nil)
43+
if err != nil {
44+
return nil, fmt.Errorf("http.ReadResponse failed: %w", err)
45+
}
46+
return resp, nil
3847
}
3948

40-
func (s *Storage) Put(ctx context.Context, u *url.URL, body []byte, header http.Header) (err error) {
41-
return s.DB.Update(func(tx *bbolt.Tx) error {
49+
func (s *Storage) Put(ctx context.Context, u *url.URL, resp *http.Response) error {
50+
b, err := httputil.DumpResponse(resp, true)
51+
if err != nil {
52+
return fmt.Errorf("httputil.DumpResponse failed: %w", err)
53+
}
54+
if err := s.DB.Update(func(tx *bbolt.Tx) error {
4255
bucket := tx.Bucket(s.Bucket)
4356
if bucket == nil {
4457
return bbolt.ErrBucketNotFound
4558
}
46-
return bucket.Put([]byte(u.String()), body)
47-
})
59+
if err := bucket.Put([]byte(u.String()), b); err != nil {
60+
return fmt.Errorf("(*bbolt.Bucket).Put failed: %w", err)
61+
}
62+
return nil
63+
}); err != nil {
64+
return fmt.Errorf("(*bbolt.DB).Update failed: %w", err)
65+
}
66+
return nil
4867
}
4968

5069
// Open is a wrapper around bbolt.Open that returns an initialized Storage.
@@ -54,13 +73,15 @@ func Open(path string, mode os.FileMode, options *bbolt.Options, bucket []byte)
5473
}
5574
db, err := bbolt.Open(path, mode, options)
5675
if err != nil {
57-
return &Storage{}, err
76+
return &Storage{}, fmt.Errorf("bbolt.Open failed: %w", err)
5877
}
5978
if err := db.Update(func(tx *bbolt.Tx) error {
60-
_, err := tx.CreateBucketIfNotExists(bucket)
61-
return err
79+
if _, err := tx.CreateBucketIfNotExists(bucket); err != nil {
80+
return fmt.Errorf("(*bbolt.Tx).CreateBucketIfNotExists failed: %w", err)
81+
}
82+
return nil
6283
}); err != nil {
63-
return &Storage{}, err
84+
return &Storage{}, fmt.Errorf("(*bbolt.DB).Update failed: %w", err)
6485
}
6586
return &Storage{DB: db, Bucket: bucket}, nil
6687
}

hash.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package ghtransport
22

33
import (
4+
"bytes"
45
"crypto/sha256"
6+
"encoding/base64"
57
"hash"
68
"net/http"
9+
"strings"
710
)
811

912
// VaryHeaders are the headers that are used to vary the cache key, this slice _must_ remain sorted.
@@ -24,3 +27,24 @@ func Hash(requestHeaders http.Header) hash.Hash {
2427
}
2528
return h
2629
}
30+
31+
// HashToken returns a hash of the 'Authorization' header matching the 'hashed_token' audit log field from GitHub.
32+
// https://docs.github.com/en/enterprise-cloud@latest/admin/monitoring-activity-in-your-enterprise/reviewing-audit-logs-for-your-enterprise/identifying-audit-log-events-performed-by-an-access-token
33+
func HashToken(authorization string) string {
34+
// If the authorization header is empty, we hash an empty string
35+
var token string
36+
// This is the most common pattern
37+
if bearer, ok := strings.CutPrefix(authorization, "Bearer "); ok && bearer != "" {
38+
token = bearer
39+
}
40+
// This is the second most common pattern
41+
if basic, ok := strings.CutPrefix(authorization, "Basic "); ok && basic != "" {
42+
if decoded, err := base64.StdEncoding.DecodeString(basic); err == nil {
43+
if _, password, ok := bytes.Cut(decoded, []byte{':'}); ok && len(password) > 0 {
44+
token = string(password)
45+
}
46+
}
47+
}
48+
hashed := sha256.Sum256([]byte(token))
49+
return base64.StdEncoding.EncodeToString(hashed[:])
50+
}

hash_test.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package ghtransport
2+
3+
import (
4+
"encoding/hex"
5+
"net/http"
6+
"testing"
7+
)
8+
9+
const testBody = `{"login":"bored-engineer","id":541842,"node_id":"MDQ6VXNlcjU0MTg0Mg==","avatar_url":"https://avatars.githubusercontent.com/u/541842?v=4","gravatar_id":"","url":"https://api.github.com/users/bored-engineer","html_url":"https://github.com/bored-engineer","followers_url":"https://api.github.com/users/bored-engineer/followers","following_url":"https://api.github.com/users/bored-engineer/following{/other_user}","gists_url":"https://api.github.com/users/bored-engineer/gists{/gist_id}","starred_url":"https://api.github.com/users/bored-engineer/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/bored-engineer/subscriptions","organizations_url":"https://api.github.com/users/bored-engineer/orgs","repos_url":"https://api.github.com/users/bored-engineer/repos","events_url":"https://api.github.com/users/bored-engineer/events{/privacy}","received_events_url":"https://api.github.com/users/bored-engineer/received_events","type":"User","user_view_type":"public","site_admin":false,"name":"Luke Young","company":null,"blog":"https://bored.engineer/","location":"San Francisco, CA","email":null,"hireable":true,"bio":"I find bugs and exploit them. Sometimes for money, mainly for free T-Shirts...","twitter_username":null,"public_repos":136,"public_gists":51,"followers":212,"following":13,"created_at":"2010-12-30T17:15:38Z","updated_at":"2025-05-06T02:44:16Z"}`
10+
11+
func TestHash(t *testing.T) {
12+
tests := map[string]struct {
13+
Headers http.Header
14+
Body string
15+
Expected string
16+
}{
17+
"emptyall": {
18+
Headers: http.Header{},
19+
Body: "",
20+
Expected: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
21+
},
22+
"emptyheaders": {
23+
Headers: http.Header{},
24+
Body: testBody,
25+
Expected: "c5542e3ee32c0adf1128a79d80a296d03412415c924e522d9d1c75b17d7c3ef0",
26+
},
27+
"accept": {
28+
Headers: http.Header{
29+
"Accept": []string{"application/vnd.github.v3+json"},
30+
},
31+
Body: testBody,
32+
Expected: "125f46f7d22cd8f41ea1534256ba85a45f4a0e3dcf995da9fecfe3361b93407d",
33+
},
34+
}
35+
for name, test := range tests {
36+
t.Run(name, func(t *testing.T) {
37+
h := Hash(test.Headers)
38+
h.Write([]byte(test.Body))
39+
if got := hex.EncodeToString(h.Sum(nil)); got != test.Expected {
40+
t.Errorf("Hash(%v, %q) = %x, want %x", test.Headers, test.Body, got, test.Expected)
41+
}
42+
})
43+
}
44+
}
45+
46+
func TestHashToken(t *testing.T) {
47+
tests := map[string]struct {
48+
Authorization string
49+
Expected string
50+
}{
51+
"empty": {
52+
Authorization: "",
53+
Expected: "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=",
54+
},
55+
"bearer": {
56+
Authorization: "Bearer hunter2",
57+
Expected: "9S+9MrKzuG/4jvbEkGKChfSCrxXdyylUH5S89Saj9sc=",
58+
},
59+
"basic": {
60+
Authorization: "Basic Ym9yZWQtZW5naW5lZXI6aHVudGVyMg==",
61+
Expected: "9S+9MrKzuG/4jvbEkGKChfSCrxXdyylUH5S89Saj9sc=",
62+
},
63+
}
64+
for name, test := range tests {
65+
t.Run(name, func(t *testing.T) {
66+
if got := HashToken(test.Authorization); got != test.Expected {
67+
t.Errorf("HashToken(%v) = %q, want %q", test.Authorization, got, test.Expected)
68+
}
69+
})
70+
}
71+
}

internal/bufferpool/bufferpool.go

Lines changed: 0 additions & 38 deletions
This file was deleted.

memory/storage.go

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,38 +3,59 @@ package memory
33
import (
44
"bytes"
55
"context"
6+
"fmt"
67
"io"
78
"net/http"
89
"net/url"
910
"sync"
1011
)
1112

13+
// cachedResponse wraps a *http.Response allowing the body bytes to be stored in memory.
14+
type cachedResponse struct {
15+
Response *http.Response
16+
Body []byte
17+
}
18+
1219
// Implements the ghtransport.Storage interface via a simple, un-bound in-memory map.
1320
type Storage struct {
1421
lock *sync.RWMutex
15-
m map[string][]byte
22+
m map[string]cachedResponse
1623
}
1724

18-
func (s Storage) Get(ctx context.Context, u *url.URL) (body io.ReadCloser, header http.Header, err error) {
25+
func (s Storage) Get(ctx context.Context, u *url.URL) (*http.Response, error) {
1926
s.lock.RLock()
2027
defer s.lock.RUnlock()
21-
if body, ok := s.m[u.String()]; ok {
22-
return io.NopCloser(bytes.NewReader(body)), nil, nil
28+
body, ok := s.m[u.String()]
29+
if !ok {
30+
return nil, nil
2331
}
24-
return nil, nil, nil
32+
resp := *body.Response
33+
resp.Body = io.NopCloser(bytes.NewReader(body.Body))
34+
return &resp, nil
2535
}
2636

27-
func (s Storage) Put(ctx context.Context, u *url.URL, body []byte, header http.Header) (err error) {
37+
func (s Storage) Put(ctx context.Context, u *url.URL, resp *http.Response) error {
2838
s.lock.Lock()
2939
defer s.lock.Unlock()
30-
s.m[u.String()] = body
40+
body, err := io.ReadAll(resp.Body)
41+
if err != nil {
42+
return fmt.Errorf("(*http.Response).Body.Read failed: %w", err)
43+
}
44+
if err := resp.Body.Close(); err != nil {
45+
return fmt.Errorf("(*http.Response).Body.Close failed: %w", err)
46+
}
47+
resp.Body = nil
48+
s.m[u.String()] = cachedResponse{
49+
Response: resp,
50+
Body: body,
51+
}
3152
return nil
3253
}
3354

3455
// NewStorage returns a new, empty Storage.
3556
func NewStorage() Storage {
3657
return Storage{
3758
lock: &sync.RWMutex{},
38-
m: make(map[string][]byte),
59+
m: make(map[string]cachedResponse),
3960
}
4061
}

s3/go.mod

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,25 @@ module github.com/bored-engineer/github-conditional-http-transport/s3
33
go 1.23.5
44

55
require (
6-
github.com/aws/aws-sdk-go-v2 v1.36.0
7-
github.com/aws/aws-sdk-go-v2/config v1.29.4
8-
github.com/aws/aws-sdk-go-v2/service/s3 v1.75.2
6+
github.com/aws/aws-sdk-go-v2 v1.36.5
7+
github.com/aws/aws-sdk-go-v2/config v1.29.17
8+
github.com/aws/aws-sdk-go-v2/service/s3 v1.83.0
99
)
1010

1111
require (
12-
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.8 // indirect
13-
github.com/aws/aws-sdk-go-v2/credentials v1.17.57 // indirect
14-
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27 // indirect
15-
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.31 // indirect
16-
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.31 // indirect
17-
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2 // indirect
18-
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.31 // indirect
19-
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2 // indirect
20-
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.5.5 // indirect
21-
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.12 // indirect
22-
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.12 // indirect
23-
github.com/aws/aws-sdk-go-v2/service/sso v1.24.14 // indirect
24-
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13 // indirect
25-
github.com/aws/aws-sdk-go-v2/service/sts v1.33.12 // indirect
26-
github.com/aws/smithy-go v1.22.2 // indirect
12+
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 // indirect
13+
github.com/aws/aws-sdk-go-v2/credentials v1.17.70 // indirect
14+
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32 // indirect
15+
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36 // indirect
16+
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36 // indirect
17+
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
18+
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36 // indirect
19+
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 // indirect
20+
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4 // indirect
21+
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17 // indirect
22+
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17 // indirect
23+
github.com/aws/aws-sdk-go-v2/service/sso v1.25.5 // indirect
24+
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3 // indirect
25+
github.com/aws/aws-sdk-go-v2/service/sts v1.34.0 // indirect
26+
github.com/aws/smithy-go v1.22.4 // indirect
2727
)

0 commit comments

Comments
 (0)