tangled
alpha
login
or
join now
willdot.net
/
cocoon
forked from
hailey.at/cocoon
An atproto PDS written in Go
0
fork
atom
overview
issues
pulls
pipelines
Compare changes
Choose any two refs to compare.
base:
oauth-stuff
main
hailey/totp
hailey/tidy
hailey/support-jwks-in-metadata
hailey/s3-blobstore
hailey/refactor-identity-package
hailey/move-sqlite-blockstore
hailey/fix-get-blocks
hailey/fix-dpop-nonce-err
hailey/deactivate-activate
hailey/db-ctx
hailey/cleanup-write-records
hailey/attempt-reconnect-websocket
fix-delete-account
email-auth-factor
age-assurance
2fa
list
v0.7.1
v0.7.0
v0.6.0
0.5.1
v0.5.1
0.5.0
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3
0.2
0.1
0.0.6
0.0.5
0.0.4
0.0.3
0.0.2
v0.0.2
v0.0.1
compare:
oauth-stuff
main
hailey/totp
hailey/tidy
hailey/support-jwks-in-metadata
hailey/s3-blobstore
hailey/refactor-identity-package
hailey/move-sqlite-blockstore
hailey/fix-get-blocks
hailey/fix-dpop-nonce-err
hailey/deactivate-activate
hailey/db-ctx
hailey/cleanup-write-records
hailey/attempt-reconnect-websocket
fix-delete-account
email-auth-factor
age-assurance
2fa
list
v0.7.1
v0.7.0
v0.6.0
0.5.1
v0.5.1
0.5.0
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3
0.2
0.1
0.0.6
0.0.5
0.0.4
0.0.3
0.0.2
v0.0.2
v0.0.1
go
+938
-792
34 changed files
expand all
collapse all
unified
split
README.md
blockstore
blockstore.go
cmd
cocoon
main.go
cspell.json
go.mod
go.sum
identity
identity.go
passport.go
internal
helpers
helpers.go
models
models.go
oauth
client
manager.go
helpers.go
provider
models.go
recording_blockstore
recording_blockstore.go
server
blockstore_variant.go
handle_account.go
handle_account_totp_enroll.go
handle_import_repo.go
handle_oauth_authorize.go
handle_oauth_token.go
handle_proxy.go
handle_server_confirm_email.go
handle_server_create_account.go
handle_server_get_service_auth.go
handle_server_reset_password.go
handle_server_update_email.go
handle_sync_get_blocks.go
middleware.go
repo.go
server.go
static
style.css
templates
account.html
totp_enroll.html
sqlite_blockstore
sqlite_blockstore.go
+63
-59
README.md
···
5
5
6
6
Cocoon is a PDS implementation in Go. It is highly experimental, and is not ready for any production use.
7
7
8
8
-
### Impmlemented Endpoints
8
8
+
## Implemented Endpoints
9
9
10
10
> [!NOTE]
11
11
-
Just because something is implemented doesn't mean it is finisehd. Tons of these are returning bad errors, don't do validation properly, etc. I'll make a "second pass" checklist at some point to do all of that.
11
11
+
Just because something is implemented doesn't mean it is finished. Tons of these are returning bad errors, don't do validation properly, etc. I'll make a "second pass" checklist at some point to do all of that.
12
12
13
13
-
#### Identity
14
14
-
- [ ] com.atproto.identity.getRecommendedDidCredentials
15
15
-
- [ ] com.atproto.identity.requestPlcOperationSignature
16
16
-
- [x] com.atproto.identity.resolveHandle
17
17
-
- [ ] com.atproto.identity.signPlcOperation
18
18
-
- [ ] com.atproto.identity.submitPlcOperatioin
19
19
-
- [x] com.atproto.identity.updateHandle
13
13
+
### Identity
20
14
21
21
-
#### Repo
22
22
-
- [x] com.atproto.repo.applyWrites
23
23
-
- [x] com.atproto.repo.createRecord
24
24
-
- [x] com.atproto.repo.putRecord
25
25
-
- [x] com.atproto.repo.deleteRecord
26
26
-
- [x] com.atproto.repo.describeRepo
27
27
-
- [x] com.atproto.repo.getRecord
28
28
-
- [ ] com.atproto.repo.importRepo
29
29
-
- [x] com.atproto.repo.listRecords
30
30
-
- [ ] com.atproto.repo.listMissingBlobs
15
15
+
- [ ] `com.atproto.identity.getRecommendedDidCredentials`
16
16
+
- [ ] `com.atproto.identity.requestPlcOperationSignature`
17
17
+
- [x] `com.atproto.identity.resolveHandle`
18
18
+
- [ ] `com.atproto.identity.signPlcOperation`
19
19
+
- [ ] `com.atproto.identity.submitPlcOperation`
20
20
+
- [x] `com.atproto.identity.updateHandle`
31
21
32
32
-
#### Server
33
33
-
- [ ] com.atproto.server.activateAccount
34
34
-
- [x] com.atproto.server.checkAccountStatus
35
35
-
- [x] com.atproto.server.confirmEmail
36
36
-
- [x] com.atproto.server.createAccount
37
37
-
- [x] com.atproto.server.createInviteCode
38
38
-
- [x] com.atproto.server.createInviteCodes
39
39
-
- [ ] com.atproto.server.deactivateAccount
40
40
-
- [ ] com.atproto.server.deleteAccount
41
41
-
- [x] com.atproto.server.deleteSession
42
42
-
- [x] com.atproto.server.describeServer
43
43
-
- [ ] com.atproto.server.getAccountInviteCodes
44
44
-
- [ ] com.atproto.server.getServiceAuth
45
45
-
- ~[ ] com.atproto.server.listAppPasswords~ - not going to add app passwords
46
46
-
- [x] com.atproto.server.refreshSession
47
47
-
- [ ] com.atproto.server.requestAccountDelete
48
48
-
- [x] com.atproto.server.requestEmailConfirmation
49
49
-
- [x] com.atproto.server.requestEmailUpdate
50
50
-
- [x] com.atproto.server.requestPasswordReset
51
51
-
- [ ] com.atproto.server.reserveSigningKey
52
52
-
- [x] com.atproto.server.resetPassword
53
53
-
- ~[ ] com.atproto.server.revokeAppPassword~ - not going to add app passwords
54
54
-
- [x] com.atproto.server.updateEmail
22
22
+
### Repo
55
23
56
56
-
#### Sync
57
57
-
- [x] com.atproto.sync.getBlob
58
58
-
- [x] com.atproto.sync.getBlocks
59
59
-
- [x] com.atproto.sync.getLatestCommit
60
60
-
- [x] com.atproto.sync.getRecord
61
61
-
- [x] com.atproto.sync.getRepoStatus
62
62
-
- [x] com.atproto.sync.getRepo
63
63
-
- [x] com.atproto.sync.listBlobs
64
64
-
- [x] com.atproto.sync.listRepos
65
65
-
- ~[ ] com.atproto.sync.notifyOfUpdate~ - BGS doesn't even have this implemented lol
66
66
-
- [x] com.atproto.sync.requestCrawl
67
67
-
- [x] com.atproto.sync.subscribeRepos
24
24
+
- [x] `com.atproto.repo.applyWrites`
25
25
+
- [x] `com.atproto.repo.createRecord`
26
26
+
- [x] `com.atproto.repo.putRecord`
27
27
+
- [x] `com.atproto.repo.deleteRecord`
28
28
+
- [x] `com.atproto.repo.describeRepo`
29
29
+
- [x] `com.atproto.repo.getRecord`
30
30
+
- [x] `com.atproto.repo.importRepo` (Works "okay". You still have to handle PLC operations on your own when migrating. Use with extreme caution.)
31
31
+
- [x] `com.atproto.repo.listRecords`
32
32
+
- [ ] `com.atproto.repo.listMissingBlobs`
33
33
+
34
34
+
### Server
35
35
+
36
36
+
- [ ] `com.atproto.server.activateAccount`
37
37
+
- [x] `com.atproto.server.checkAccountStatus`
38
38
+
- [x] `com.atproto.server.confirmEmail`
39
39
+
- [x] `com.atproto.server.createAccount`
40
40
+
- [x] `com.atproto.server.createInviteCode`
41
41
+
- [x] `com.atproto.server.createInviteCodes`
42
42
+
- [ ] `com.atproto.server.deactivateAccount`
43
43
+
- [ ] `com.atproto.server.deleteAccount`
44
44
+
- [x] `com.atproto.server.deleteSession`
45
45
+
- [x] `com.atproto.server.describeServer`
46
46
+
- [ ] `com.atproto.server.getAccountInviteCodes`
47
47
+
- [ ] `com.atproto.server.getServiceAuth`
48
48
+
- ~~[ ] `com.atproto.server.listAppPasswords`~~ - not going to add app passwords
49
49
+
- [x] `com.atproto.server.refreshSession`
50
50
+
- [ ] `com.atproto.server.requestAccountDelete`
51
51
+
- [x] `com.atproto.server.requestEmailConfirmation`
52
52
+
- [x] `com.atproto.server.requestEmailUpdate`
53
53
+
- [x] `com.atproto.server.requestPasswordReset`
54
54
+
- [ ] `com.atproto.server.reserveSigningKey`
55
55
+
- [x] `com.atproto.server.resetPassword`
56
56
+
- ~~[] `com.atproto.server.revokeAppPassword`~~ - not going to add app passwords
57
57
+
- [x] `com.atproto.server.updateEmail`
68
58
69
69
-
#### Other
70
70
-
- [ ] com.atproto.label.queryLabels
71
71
-
- [ ] com.atproto.moderation.createReport
72
72
-
- [x] app.bsky.actor.getPreferences
73
73
-
- [x] app.bsky.actor.putPreferences
59
59
+
### Sync
74
60
61
61
+
- [x] `com.atproto.sync.getBlob`
62
62
+
- [x] `com.atproto.sync.getBlocks`
63
63
+
- [x] `com.atproto.sync.getLatestCommit`
64
64
+
- [x] `com.atproto.sync.getRecord`
65
65
+
- [x] `com.atproto.sync.getRepoStatus`
66
66
+
- [x] `com.atproto.sync.getRepo`
67
67
+
- [x] `com.atproto.sync.listBlobs`
68
68
+
- [x] `com.atproto.sync.listRepos`
69
69
+
- ~~[ ] `com.atproto.sync.notifyOfUpdate`~~ - BGS doesn't even have this implemented lol
70
70
+
- [x] `com.atproto.sync.requestCrawl`
71
71
+
- [x] `com.atproto.sync.subscribeRepos`
72
72
+
73
73
+
### Other
74
74
+
75
75
+
- [ ] `com.atproto.label.queryLabels`
76
76
+
- [x] `com.atproto.moderation.createReport` (Note: this should be handled by proxying, not actually implemented in the PDS)
77
77
+
- [x] `app.bsky.actor.getPreferences`
78
78
+
- [x] `app.bsky.actor.putPreferences`
75
79
76
80
## License
77
81
-163
blockstore/blockstore.go
···
1
1
-
package blockstore
2
2
-
3
3
-
import (
4
4
-
"context"
5
5
-
"fmt"
6
6
-
7
7
-
"github.com/bluesky-social/indigo/atproto/syntax"
8
8
-
"github.com/haileyok/cocoon/internal/db"
9
9
-
"github.com/haileyok/cocoon/models"
10
10
-
blocks "github.com/ipfs/go-block-format"
11
11
-
"github.com/ipfs/go-cid"
12
12
-
"gorm.io/gorm/clause"
13
13
-
)
14
14
-
15
15
-
type SqliteBlockstore struct {
16
16
-
db *db.DB
17
17
-
did string
18
18
-
readonly bool
19
19
-
inserts map[cid.Cid]blocks.Block
20
20
-
}
21
21
-
22
22
-
func New(did string, db *db.DB) *SqliteBlockstore {
23
23
-
return &SqliteBlockstore{
24
24
-
did: did,
25
25
-
db: db,
26
26
-
readonly: false,
27
27
-
inserts: map[cid.Cid]blocks.Block{},
28
28
-
}
29
29
-
}
30
30
-
31
31
-
func NewReadOnly(did string, db *db.DB) *SqliteBlockstore {
32
32
-
return &SqliteBlockstore{
33
33
-
did: did,
34
34
-
db: db,
35
35
-
readonly: true,
36
36
-
inserts: map[cid.Cid]blocks.Block{},
37
37
-
}
38
38
-
}
39
39
-
40
40
-
func (bs *SqliteBlockstore) Get(ctx context.Context, cid cid.Cid) (blocks.Block, error) {
41
41
-
var block models.Block
42
42
-
43
43
-
maybeBlock, ok := bs.inserts[cid]
44
44
-
if ok {
45
45
-
return maybeBlock, nil
46
46
-
}
47
47
-
48
48
-
if err := bs.db.Raw("SELECT * FROM blocks WHERE did = ? AND cid = ?", nil, bs.did, cid.Bytes()).Scan(&block).Error; err != nil {
49
49
-
return nil, err
50
50
-
}
51
51
-
52
52
-
b, err := blocks.NewBlockWithCid(block.Value, cid)
53
53
-
if err != nil {
54
54
-
return nil, err
55
55
-
}
56
56
-
57
57
-
return b, nil
58
58
-
}
59
59
-
60
60
-
func (bs *SqliteBlockstore) Put(ctx context.Context, block blocks.Block) error {
61
61
-
bs.inserts[block.Cid()] = block
62
62
-
63
63
-
if bs.readonly {
64
64
-
return nil
65
65
-
}
66
66
-
67
67
-
b := models.Block{
68
68
-
Did: bs.did,
69
69
-
Cid: block.Cid().Bytes(),
70
70
-
Rev: syntax.NewTIDNow(0).String(), // TODO: WARN, this is bad. don't do this
71
71
-
Value: block.RawData(),
72
72
-
}
73
73
-
74
74
-
if err := bs.db.Create(&b, []clause.Expression{clause.OnConflict{
75
75
-
Columns: []clause.Column{{Name: "did"}, {Name: "cid"}},
76
76
-
UpdateAll: true,
77
77
-
}}).Error; err != nil {
78
78
-
return err
79
79
-
}
80
80
-
81
81
-
return nil
82
82
-
}
83
83
-
84
84
-
func (bs *SqliteBlockstore) DeleteBlock(context.Context, cid.Cid) error {
85
85
-
panic("not implemented")
86
86
-
}
87
87
-
88
88
-
func (bs *SqliteBlockstore) Has(context.Context, cid.Cid) (bool, error) {
89
89
-
panic("not implemented")
90
90
-
}
91
91
-
92
92
-
func (bs *SqliteBlockstore) GetSize(context.Context, cid.Cid) (int, error) {
93
93
-
panic("not implemented")
94
94
-
}
95
95
-
96
96
-
func (bs *SqliteBlockstore) PutMany(ctx context.Context, blocks []blocks.Block) error {
97
97
-
tx := bs.db.BeginDangerously()
98
98
-
99
99
-
for _, block := range blocks {
100
100
-
bs.inserts[block.Cid()] = block
101
101
-
102
102
-
if bs.readonly {
103
103
-
continue
104
104
-
}
105
105
-
106
106
-
b := models.Block{
107
107
-
Did: bs.did,
108
108
-
Cid: block.Cid().Bytes(),
109
109
-
Rev: syntax.NewTIDNow(0).String(), // TODO: WARN, this is bad. don't do this
110
110
-
Value: block.RawData(),
111
111
-
}
112
112
-
113
113
-
if err := tx.Clauses(clause.OnConflict{
114
114
-
Columns: []clause.Column{{Name: "did"}, {Name: "cid"}},
115
115
-
UpdateAll: true,
116
116
-
}).Create(&b).Error; err != nil {
117
117
-
tx.Rollback()
118
118
-
return err
119
119
-
}
120
120
-
}
121
121
-
122
122
-
if bs.readonly {
123
123
-
return nil
124
124
-
}
125
125
-
126
126
-
tx.Commit()
127
127
-
128
128
-
return nil
129
129
-
}
130
130
-
131
131
-
func (bs *SqliteBlockstore) AllKeysChan(ctx context.Context) (<-chan cid.Cid, error) {
132
132
-
panic("not implemented")
133
133
-
}
134
134
-
135
135
-
func (bs *SqliteBlockstore) HashOnRead(enabled bool) {
136
136
-
panic("not implemented")
137
137
-
}
138
138
-
139
139
-
func (bs *SqliteBlockstore) UpdateRepo(ctx context.Context, root cid.Cid, rev string) error {
140
140
-
if err := bs.db.Exec("UPDATE repos SET root = ?, rev = ? WHERE did = ?", nil, root.Bytes(), rev, bs.did).Error; err != nil {
141
141
-
return err
142
142
-
}
143
143
-
144
144
-
return nil
145
145
-
}
146
146
-
147
147
-
func (bs *SqliteBlockstore) Execute(ctx context.Context) error {
148
148
-
if !bs.readonly {
149
149
-
return fmt.Errorf("blockstore was not readonly")
150
150
-
}
151
151
-
152
152
-
bs.readonly = false
153
153
-
for _, b := range bs.inserts {
154
154
-
bs.Put(ctx, b)
155
155
-
}
156
156
-
bs.readonly = true
157
157
-
158
158
-
return nil
159
159
-
}
160
160
-
161
161
-
func (bs *SqliteBlockstore) GetLog() map[cid.Cid]blocks.Block {
162
162
-
return bs.inserts
163
163
-
}
+14
-1
cmd/cocoon/main.go
···
131
131
Name: "session-secret",
132
132
EnvVars: []string{"COCOON_SESSION_SECRET"},
133
133
},
134
134
+
&cli.StringFlag{
135
135
+
Name: "default-atproto-proxy",
136
136
+
EnvVars: []string{"COCOON_DEFAULT_ATPROTO_PROXY"},
137
137
+
Value: "did:web:api.bsky.app#bsky_appview",
138
138
+
},
139
139
+
&cli.StringFlag{
140
140
+
Name: "blockstore-variant",
141
141
+
EnvVars: []string{"COCOON_BLOCKSTORE_VARIANT"},
142
142
+
Value: "sqlite",
143
143
+
},
134
144
},
135
145
Commands: []*cli.Command{
136
146
runServe,
···
153
163
Usage: "Start the cocoon PDS",
154
164
Flags: []cli.Flag{},
155
165
Action: func(cmd *cli.Context) error {
166
166
+
156
167
s, err := server.New(&server.Args{
157
168
Addr: cmd.String("addr"),
158
169
DbName: cmd.String("db-name"),
···
178
189
AccessKey: cmd.String("s3-access-key"),
179
190
SecretKey: cmd.String("s3-secret-key"),
180
191
},
181
181
-
SessionSecret: cmd.String("session-secret"),
192
192
+
SessionSecret: cmd.String("session-secret"),
193
193
+
DefaultAtprotoProxy: cmd.String("default-atproto-proxy"),
194
194
+
BlockstoreVariant: server.MustReturnBlockstoreVariant(cmd.String("blockstore-variant")),
182
195
})
183
196
if err != nil {
184
197
fmt.Printf("error creating cocoon: %v", err)
+45
cspell.json
···
1
1
+
{
2
2
+
"version": "0.2",
3
3
+
"language": "en",
4
4
+
"words": [
5
5
+
"atproto",
6
6
+
"bsky",
7
7
+
"Cocoon",
8
8
+
"PDS",
9
9
+
"Plc",
10
10
+
"plc",
11
11
+
"repo",
12
12
+
"InviteCodes",
13
13
+
"InviteCode",
14
14
+
"Invite",
15
15
+
"Signin",
16
16
+
"Signout",
17
17
+
"JWKS",
18
18
+
"dpop",
19
19
+
"BGS",
20
20
+
"pico",
21
21
+
"picocss",
22
22
+
"par",
23
23
+
"blobs",
24
24
+
"blob",
25
25
+
"did",
26
26
+
"DID",
27
27
+
"OAuth",
28
28
+
"oauth",
29
29
+
"par",
30
30
+
"Cocoon",
31
31
+
"memcache",
32
32
+
"db",
33
33
+
"helpers",
34
34
+
"middleware",
35
35
+
"repo",
36
36
+
"static",
37
37
+
"pico",
38
38
+
"picocss",
39
39
+
"MIT",
40
40
+
"Go"
41
41
+
],
42
42
+
"ignorePaths": [
43
43
+
"server/static/pico.css"
44
44
+
]
45
45
+
}
+1
-2
go.mod
···
14
14
github.com/google/uuid v1.4.0
15
15
github.com/gorilla/sessions v1.4.0
16
16
github.com/gorilla/websocket v1.5.1
17
17
+
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b
17
18
github.com/hashicorp/golang-lru/v2 v2.0.7
18
19
github.com/ipfs/go-block-format v0.2.0
19
20
github.com/ipfs/go-cid v0.4.1
···
24
25
github.com/labstack/echo/v4 v4.13.3
25
26
github.com/lestrrat-go/jwx/v2 v2.0.12
26
27
github.com/multiformats/go-multihash v0.2.3
27
27
-
github.com/pquerna/otp v1.5.0
28
28
github.com/samber/slog-echo v1.16.1
29
29
github.com/urfave/cli/v2 v2.27.6
30
30
github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e
···
38
38
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
39
39
github.com/RussellLuo/slidingwindow v0.0.0-20200528002341-535bb99d338b // indirect
40
40
github.com/beorn7/perks v1.0.1 // indirect
41
41
-
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
42
41
github.com/carlmjohnson/versioninfo v0.22.5 // indirect
43
42
github.com/cespare/xxhash/v2 v2.3.0 // indirect
44
43
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
+2
-4
go.sum
···
20
20
github.com/bluesky-social/indigo v0.0.0-20250414202759-826fcdeaa36b/go.mod h1:yjdhLA1LkK8VDS/WPUoYPo25/Hq/8rX38Ftr67EsqKY=
21
21
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
22
22
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
23
23
-
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
24
24
-
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
25
23
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 h1:R8vQdOQdZ9Y3SkEwmHoWBmX1DNXhXZqlTpq6s4tyJGc=
26
24
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
27
25
github.com/carlmjohnson/versioninfo v0.22.5 h1:O00sjOLUAFxYQjlN/bzYTuZiS0y6fWDQjMRvwtKgwwc=
···
93
91
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
94
92
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8=
95
93
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=
94
94
+
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b h1:wDUNC2eKiL35DbLvsDhiblTUXHxcOPwQSCzi7xpQUN4=
95
95
+
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b/go.mod h1:VzxiSdG6j1pi7rwGm/xYI5RbtpBgM8sARDXlvEvxlu0=
96
96
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
97
97
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
98
98
github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
···
291
291
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
292
292
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f h1:VXTQfuJj9vKR4TCkEuWIckKvdHFeJH/huIFJ9/cXOB0=
293
293
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw=
294
294
-
github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
295
295
-
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
296
294
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
297
295
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
298
296
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
+73
-54
identity/identity.go
···
13
13
"github.com/bluesky-social/indigo/util"
14
14
)
15
15
16
16
-
func ResolveHandle(ctx context.Context, cli *http.Client, handle string) (string, error) {
17
17
-
if cli == nil {
18
18
-
cli = util.RobustHTTPClient()
19
19
-
}
20
20
-
21
21
-
var did string
22
22
-
23
23
-
_, err := syntax.ParseHandle(handle)
16
16
+
func ResolveHandleFromTXT(ctx context.Context, handle string) (string, error) {
17
17
+
name := fmt.Sprintf("_atproto.%s", handle)
18
18
+
recs, err := net.LookupTXT(name)
24
19
if err != nil {
25
25
-
return "", err
20
20
+
return "", fmt.Errorf("handle could not be resolved via txt: %w", err)
26
21
}
27
22
28
28
-
recs, err := net.LookupTXT(fmt.Sprintf("_atproto.%s", handle))
29
29
-
if err == nil {
30
30
-
for _, rec := range recs {
31
31
-
if strings.HasPrefix(rec, "did=") {
32
32
-
did = strings.Split(rec, "did=")[1]
33
33
-
break
23
23
+
for _, rec := range recs {
24
24
+
if strings.HasPrefix(rec, "did=") {
25
25
+
maybeDid := strings.Split(rec, "did=")[1]
26
26
+
if _, err := syntax.ParseDID(maybeDid); err == nil {
27
27
+
return maybeDid, nil
34
28
}
35
29
}
36
36
-
} else {
37
37
-
fmt.Printf("erorr getting txt records: %v\n", err)
38
30
}
39
31
40
40
-
if did == "" {
41
41
-
req, err := http.NewRequestWithContext(
42
42
-
ctx,
43
43
-
"GET",
44
44
-
fmt.Sprintf("https://%s/.well-known/atproto-did", handle),
45
45
-
nil,
46
46
-
)
47
47
-
if err != nil {
48
48
-
return "", nil
49
49
-
}
32
32
+
return "", fmt.Errorf("handle could not be resolved via txt: no record found")
33
33
+
}
50
34
51
51
-
resp, err := http.DefaultClient.Do(req)
52
52
-
if err != nil {
53
53
-
return "", nil
54
54
-
}
55
55
-
defer resp.Body.Close()
35
35
+
func ResolveHandleFromWellKnown(ctx context.Context, cli *http.Client, handle string) (string, error) {
36
36
+
ustr := fmt.Sprintf("https://%s/.well=known/atproto-did", handle)
37
37
+
req, err := http.NewRequestWithContext(
38
38
+
ctx,
39
39
+
"GET",
40
40
+
ustr,
41
41
+
nil,
42
42
+
)
43
43
+
if err != nil {
44
44
+
return "", fmt.Errorf("handle could not be resolved via web: %w", err)
45
45
+
}
56
46
57
57
-
if resp.StatusCode != http.StatusOK {
58
58
-
io.Copy(io.Discard, resp.Body)
59
59
-
return "", fmt.Errorf("unable to resolve handle")
60
60
-
}
47
47
+
resp, err := cli.Do(req)
48
48
+
if err != nil {
49
49
+
return "", fmt.Errorf("handle could not be resolved via web: %w", err)
50
50
+
}
51
51
+
defer resp.Body.Close()
61
52
62
62
-
b, err := io.ReadAll(resp.Body)
63
63
-
if err != nil {
64
64
-
return "", err
65
65
-
}
53
53
+
b, err := io.ReadAll(resp.Body)
54
54
+
if err != nil {
55
55
+
return "", fmt.Errorf("handle could not be resolved via web: %w", err)
56
56
+
}
66
57
67
67
-
maybeDid := string(b)
58
58
+
if resp.StatusCode != http.StatusOK {
59
59
+
return "", fmt.Errorf("handle could not be resolved via web: invalid status code %d", resp.StatusCode)
60
60
+
}
68
61
69
69
-
if _, err := syntax.ParseDID(maybeDid); err != nil {
70
70
-
return "", fmt.Errorf("unable to resolve handle")
71
71
-
}
62
62
+
maybeDid := string(b)
72
63
73
73
-
did = maybeDid
64
64
+
if _, err := syntax.ParseDID(maybeDid); err != nil {
65
65
+
return "", fmt.Errorf("handle could not be resolved via web: invalid did in document")
74
66
}
75
67
76
76
-
return did, nil
68
68
+
return maybeDid, nil
77
69
}
78
70
79
79
-
func FetchDidDoc(ctx context.Context, cli *http.Client, did string) (*DidDoc, error) {
71
71
+
func ResolveHandle(ctx context.Context, cli *http.Client, handle string) (string, error) {
80
72
if cli == nil {
81
73
cli = util.RobustHTTPClient()
82
74
}
83
75
84
84
-
var ustr string
76
76
+
_, err := syntax.ParseHandle(handle)
77
77
+
if err != nil {
78
78
+
return "", err
79
79
+
}
80
80
+
81
81
+
if maybeDidFromTxt, err := ResolveHandleFromTXT(ctx, handle); err == nil {
82
82
+
return maybeDidFromTxt, nil
83
83
+
}
84
84
+
85
85
+
if maybeDidFromWeb, err := ResolveHandleFromWellKnown(ctx, cli, handle); err == nil {
86
86
+
return maybeDidFromWeb, nil
87
87
+
}
88
88
+
89
89
+
return "", fmt.Errorf("handle could not be resolved")
90
90
+
}
91
91
+
92
92
+
func DidToDocUrl(did string) (string, error) {
85
93
if strings.HasPrefix(did, "did:plc:") {
86
86
-
ustr = fmt.Sprintf("https://plc.directory/%s", did)
94
94
+
return fmt.Sprintf("https://plc.directory/%s", did), nil
87
95
} else if strings.HasPrefix(did, "did:web:") {
88
88
-
ustr = fmt.Sprintf("https://%s/.well-known/did.json", strings.TrimPrefix(did, "did:web:"))
96
96
+
return fmt.Sprintf("https://%s/.well-known/did.json", strings.TrimPrefix(did, "did:web:")), nil
89
97
} else {
90
90
-
return nil, fmt.Errorf("did was not a supported did type")
98
98
+
return "", fmt.Errorf("did was not a supported did type")
99
99
+
}
100
100
+
}
101
101
+
102
102
+
func FetchDidDoc(ctx context.Context, cli *http.Client, did string) (*DidDoc, error) {
103
103
+
if cli == nil {
104
104
+
cli = util.RobustHTTPClient()
105
105
+
}
106
106
+
107
107
+
ustr, err := DidToDocUrl(did)
108
108
+
if err != nil {
109
109
+
return nil, err
91
110
}
92
111
93
112
req, err := http.NewRequestWithContext(ctx, "GET", ustr, nil)
···
95
114
return nil, err
96
115
}
97
116
98
98
-
resp, err := http.DefaultClient.Do(req)
117
117
+
resp, err := cli.Do(req)
99
118
if err != nil {
100
119
return nil, err
101
120
}
···
103
122
104
123
if resp.StatusCode != 200 {
105
124
io.Copy(io.Discard, resp.Body)
106
106
-
return nil, fmt.Errorf("could not find identity in plc registry")
125
125
+
return nil, fmt.Errorf("unable to find did doc at url. did: %s. url: %s", did, ustr)
107
126
}
108
127
109
128
var diddoc DidDoc
···
127
146
return nil, err
128
147
}
129
148
130
130
-
resp, err := http.DefaultClient.Do(req)
149
149
+
resp, err := cli.Do(req)
131
150
if err != nil {
132
151
return nil, err
133
152
}
+16
-5
identity/passport.go
···
19
19
type Passport struct {
20
20
h *http.Client
21
21
bc BackingCache
22
22
-
lk sync.Mutex
22
22
+
mu sync.RWMutex
23
23
}
24
24
25
25
func NewPassport(h *http.Client, bc BackingCache) *Passport {
···
30
30
return &Passport{
31
31
h: h,
32
32
bc: bc,
33
33
-
lk: sync.Mutex{},
34
33
}
35
34
}
36
35
···
38
37
skipCache, _ := ctx.Value("skip-cache").(bool)
39
38
40
39
if !skipCache {
40
40
+
p.mu.RLock()
41
41
cached, ok := p.bc.GetDoc(did)
42
42
+
p.mu.RUnlock()
43
43
+
42
44
if ok {
43
45
return cached, nil
44
46
}
45
47
}
46
48
47
47
-
p.lk.Lock() // this is pretty pathetic, and i should rethink this. but for now, fuck it
48
48
-
defer p.lk.Unlock()
49
49
-
49
49
+
// TODO: should coalesce requests here
50
50
doc, err := FetchDidDoc(ctx, p.h, did)
51
51
if err != nil {
52
52
return nil, err
53
53
}
54
54
55
55
+
p.mu.Lock()
55
56
p.bc.PutDoc(did, doc)
57
57
+
p.mu.Unlock()
56
58
57
59
return doc, nil
58
60
}
···
61
63
skipCache, _ := ctx.Value("skip-cache").(bool)
62
64
63
65
if !skipCache {
66
66
+
p.mu.RLock()
64
67
cached, ok := p.bc.GetDid(handle)
68
68
+
p.mu.RUnlock()
69
69
+
65
70
if ok {
66
71
return cached, nil
67
72
}
···
72
77
return "", err
73
78
}
74
79
80
80
+
p.mu.Lock()
75
81
p.bc.PutDid(handle, did)
82
82
+
p.mu.Unlock()
76
83
77
84
return did, nil
78
85
}
79
86
80
87
func (p *Passport) BustDoc(ctx context.Context, did string) error {
88
88
+
p.mu.Lock()
89
89
+
defer p.mu.Unlock()
81
90
return p.bc.BustDoc(did)
82
91
}
83
92
84
93
func (p *Passport) BustDid(ctx context.Context, handle string) error {
94
94
+
p.mu.Lock()
95
95
+
defer p.mu.Unlock()
85
96
return p.bc.BustDid(handle)
86
97
}
+13
internal/helpers/helpers.go
···
7
7
"math/rand"
8
8
"net/url"
9
9
10
10
+
"github.com/Azure/go-autorest/autorest/to"
10
11
"github.com/labstack/echo/v4"
11
12
"github.com/lestrrat-go/jwx/v2/jwk"
12
13
)
···
29
30
msg += ". " + *suffix
30
31
}
31
32
return genericError(e, 400, msg)
33
33
+
}
34
34
+
35
35
+
func InvalidTokenError(e echo.Context) error {
36
36
+
return InputError(e, to.StringPtr("InvalidToken"))
37
37
+
}
38
38
+
39
39
+
func ExpiredTokenError(e echo.Context) error {
40
40
+
// WARN: See https://github.com/bluesky-social/atproto/discussions/3319
41
41
+
return e.JSON(400, map[string]string{
42
42
+
"error": "ExpiredToken",
43
43
+
"message": "*",
44
44
+
})
32
45
}
33
46
34
47
func genericError(e echo.Context, code int, msg string) error {
-9
models/models.go
···
7
7
"github.com/bluesky-social/indigo/atproto/crypto"
8
8
)
9
9
10
10
-
type TwoFactorType string
11
11
-
12
12
-
var (
13
13
-
TwoFactorTypeNone = TwoFactorType("none")
14
14
-
TwoFactorTypeTotp = TwoFactorType("totp")
15
15
-
)
16
16
-
17
10
type Repo struct {
18
11
Did string `gorm:"primaryKey"`
19
12
CreatedAt time.Time
···
30
23
Rev string
31
24
Root []byte
32
25
Preferences []byte
33
33
-
TwoFactorType TwoFactorType `gorm:"default:none"`
34
34
-
TotpSecret *string
35
26
}
36
27
37
28
func (r *Repo) SignFor(ctx context.Context, did string, msg []byte) ([]byte, error) {
+1
-1
oauth/client/manager.go
···
289
289
return nil, errors.New("at least one `redirect_uri` is required")
290
290
}
291
291
292
292
-
if metadata.ApplicationType == "native" && metadata.TokenEndpointAuthMethod == "none" {
292
292
+
if metadata.ApplicationType == "native" && metadata.TokenEndpointAuthMethod != "none" {
293
293
return nil, errors.New("native clients must authenticate using `none` method")
294
294
}
295
295
+32
oauth/helpers.go
···
4
4
"errors"
5
5
"fmt"
6
6
"net/url"
7
7
+
"time"
7
8
8
9
"github.com/haileyok/cocoon/internal/helpers"
9
10
"github.com/haileyok/cocoon/oauth/constants"
11
11
+
"github.com/haileyok/cocoon/oauth/provider"
10
12
)
11
13
12
14
func GenerateCode() string {
···
46
48
47
49
return reqId, nil
48
50
}
51
51
+
52
52
+
type SessionAgeResult struct {
53
53
+
SessionAge time.Duration
54
54
+
RefreshAge time.Duration
55
55
+
SessionExpired bool
56
56
+
RefreshExpired bool
57
57
+
}
58
58
+
59
59
+
func GetSessionAgeFromToken(t provider.OauthToken) SessionAgeResult {
60
60
+
sessionLifetime := constants.PublicClientSessionLifetime
61
61
+
refreshLifetime := constants.PublicClientRefreshLifetime
62
62
+
if t.ClientAuth.Method != "none" {
63
63
+
sessionLifetime = constants.ConfidentialClientSessionLifetime
64
64
+
refreshLifetime = constants.ConfidentialClientRefreshLifetime
65
65
+
}
66
66
+
67
67
+
res := SessionAgeResult{}
68
68
+
69
69
+
res.SessionAge = time.Since(t.CreatedAt)
70
70
+
if res.SessionAge > sessionLifetime {
71
71
+
res.SessionExpired = true
72
72
+
}
73
73
+
74
74
+
refreshAge := time.Since(t.UpdatedAt)
75
75
+
if refreshAge > refreshLifetime {
76
76
+
res.RefreshExpired = true
77
77
+
}
78
78
+
79
79
+
return res
80
80
+
}
+2
oauth/provider/models.go
···
65
65
Code string `gorm:"index"`
66
66
Token string `gorm:"uniqueIndex"`
67
67
RefreshToken string `gorm:"uniqueIndex"`
68
68
+
Ip string
68
69
}
69
70
70
71
type OauthAuthorizationRequest struct {
···
78
79
Sub *string
79
80
Code *string
80
81
Accepted *bool
82
82
+
Ip string
81
83
}
+77
recording_blockstore/recording_blockstore.go
···
1
1
+
package recording_blockstore
2
2
+
3
3
+
import (
4
4
+
"context"
5
5
+
6
6
+
blockformat "github.com/ipfs/go-block-format"
7
7
+
"github.com/ipfs/go-cid"
8
8
+
blockstore "github.com/ipfs/go-ipfs-blockstore"
9
9
+
)
10
10
+
11
11
+
type RecordingBlockstore struct {
12
12
+
base blockstore.Blockstore
13
13
+
14
14
+
inserts map[cid.Cid]blockformat.Block
15
15
+
}
16
16
+
17
17
+
func New(base blockstore.Blockstore) *RecordingBlockstore {
18
18
+
return &RecordingBlockstore{
19
19
+
base: base,
20
20
+
inserts: make(map[cid.Cid]blockformat.Block),
21
21
+
}
22
22
+
}
23
23
+
24
24
+
func (bs *RecordingBlockstore) Has(ctx context.Context, c cid.Cid) (bool, error) {
25
25
+
return bs.base.Has(ctx, c)
26
26
+
}
27
27
+
28
28
+
func (bs *RecordingBlockstore) Get(ctx context.Context, c cid.Cid) (blockformat.Block, error) {
29
29
+
return bs.base.Get(ctx, c)
30
30
+
}
31
31
+
32
32
+
func (bs *RecordingBlockstore) GetSize(ctx context.Context, c cid.Cid) (int, error) {
33
33
+
return bs.base.GetSize(ctx, c)
34
34
+
}
35
35
+
36
36
+
func (bs *RecordingBlockstore) DeleteBlock(ctx context.Context, c cid.Cid) error {
37
37
+
return bs.base.DeleteBlock(ctx, c)
38
38
+
}
39
39
+
40
40
+
func (bs *RecordingBlockstore) Put(ctx context.Context, block blockformat.Block) error {
41
41
+
if err := bs.base.Put(ctx, block); err != nil {
42
42
+
return err
43
43
+
}
44
44
+
bs.inserts[block.Cid()] = block
45
45
+
return nil
46
46
+
}
47
47
+
48
48
+
func (bs *RecordingBlockstore) PutMany(ctx context.Context, blocks []blockformat.Block) error {
49
49
+
if err := bs.base.PutMany(ctx, blocks); err != nil {
50
50
+
return err
51
51
+
}
52
52
+
53
53
+
for _, b := range blocks {
54
54
+
bs.inserts[b.Cid()] = b
55
55
+
}
56
56
+
57
57
+
return nil
58
58
+
}
59
59
+
60
60
+
func (bs *RecordingBlockstore) AllKeysChan(ctx context.Context) (<-chan cid.Cid, error) {
61
61
+
return bs.AllKeysChan(ctx)
62
62
+
}
63
63
+
64
64
+
func (bs *RecordingBlockstore) HashOnRead(enabled bool) {
65
65
+
}
66
66
+
67
67
+
func (bs *RecordingBlockstore) GetLogMap() map[cid.Cid]blockformat.Block {
68
68
+
return bs.inserts
69
69
+
}
70
70
+
71
71
+
func (bs *RecordingBlockstore) GetLogArray() []blockformat.Block {
72
72
+
var blocks []blockformat.Block
73
73
+
for _, b := range bs.inserts {
74
74
+
blocks = append(blocks, b)
75
75
+
}
76
76
+
return blocks
77
77
+
}
+30
server/blockstore_variant.go
···
1
1
+
package server
2
2
+
3
3
+
import (
4
4
+
"github.com/haileyok/cocoon/sqlite_blockstore"
5
5
+
blockstore "github.com/ipfs/go-ipfs-blockstore"
6
6
+
)
7
7
+
8
8
+
type BlockstoreVariant int
9
9
+
10
10
+
const (
11
11
+
BlockstoreVariantSqlite = iota
12
12
+
)
13
13
+
14
14
+
func MustReturnBlockstoreVariant(maybeBsv string) BlockstoreVariant {
15
15
+
switch maybeBsv {
16
16
+
case "sqlite":
17
17
+
return BlockstoreVariantSqlite
18
18
+
default:
19
19
+
panic("invalid blockstore variant provided")
20
20
+
}
21
21
+
}
22
22
+
23
23
+
func (s *Server) getBlockstore(did string) blockstore.Blockstore {
24
24
+
switch s.config.BlockstoreVariant {
25
25
+
case BlockstoreVariantSqlite:
26
26
+
return sqlite_blockstore.New(did, s.db)
27
27
+
default:
28
28
+
return sqlite_blockstore.New(did, s.db)
29
29
+
}
30
30
+
}
+37
-7
server/handle_account.go
···
3
3
import (
4
4
"time"
5
5
6
6
+
"github.com/haileyok/cocoon/oauth"
7
7
+
"github.com/haileyok/cocoon/oauth/constants"
6
8
"github.com/haileyok/cocoon/oauth/provider"
9
9
+
"github.com/hako/durafmt"
7
10
"github.com/labstack/echo/v4"
8
11
)
9
12
10
13
func (s *Server) handleAccount(e echo.Context) error {
14
14
+
ctx := e.Request().Context()
11
15
repo, sess, err := s.getSessionRepoOrErr(e)
12
16
if err != nil {
13
17
return e.Redirect(303, "/account/signin")
14
18
}
15
19
16
16
-
now := time.Now()
20
20
+
oldestPossibleSession := time.Now().Add(constants.ConfidentialClientSessionLifetime)
17
21
18
22
var tokens []provider.OauthToken
19
19
-
if err := s.db.Raw("SELECT * FROM oauth_tokens WHERE sub = ? AND expires_at >= ? ORDER BY created_at ASC", nil, repo.Repo.Did, now).Scan(&tokens).Error; err != nil {
23
23
+
if err := s.db.Raw("SELECT * FROM oauth_tokens WHERE sub = ? AND created_at < ? ORDER BY created_at ASC", nil, repo.Repo.Did, oldestPossibleSession).Scan(&tokens).Error; err != nil {
20
24
s.logger.Error("couldnt fetch oauth sessions for account", "did", repo.Repo.Did, "error", err)
21
25
sess.AddFlash("Unable to fetch sessions. See server logs for more details.", "error")
22
26
sess.Save(e.Request(), e.Response())
···
25
29
})
26
30
}
27
31
32
32
+
var filtered []provider.OauthToken
33
33
+
for _, t := range tokens {
34
34
+
ageRes := oauth.GetSessionAgeFromToken(t)
35
35
+
if ageRes.SessionExpired {
36
36
+
continue
37
37
+
}
38
38
+
filtered = append(filtered, t)
39
39
+
}
40
40
+
41
41
+
now := time.Now()
42
42
+
28
43
tokenInfo := []map[string]string{}
29
44
for _, t := range tokens {
45
45
+
ageRes := oauth.GetSessionAgeFromToken(t)
46
46
+
maxTime := constants.PublicClientSessionLifetime
47
47
+
if t.ClientAuth.Method != "none" {
48
48
+
maxTime = constants.ConfidentialClientSessionLifetime
49
49
+
}
50
50
+
51
51
+
var clientName string
52
52
+
metadata, err := s.oauthProvider.ClientManager.GetClient(ctx, t.ClientId)
53
53
+
if err != nil {
54
54
+
clientName = t.ClientId
55
55
+
} else {
56
56
+
clientName = metadata.Metadata.ClientName
57
57
+
}
58
58
+
30
59
tokenInfo = append(tokenInfo, map[string]string{
31
31
-
"ClientId": t.ClientId,
32
32
-
"CreatedAt": t.CreatedAt.Format("02 Jan 06 15:04 MST"),
33
33
-
"UpdatedAt": t.CreatedAt.Format("02 Jan 06 15:04 MST"),
34
34
-
"ExpiresAt": t.CreatedAt.Format("02 Jan 06 15:04 MST"),
35
35
-
"Token": t.Token,
60
60
+
"ClientName": clientName,
61
61
+
"Age": durafmt.Parse(ageRes.SessionAge).LimitFirstN(2).String(),
62
62
+
"LastUpdated": durafmt.Parse(now.Sub(t.UpdatedAt)).LimitFirstN(2).String(),
63
63
+
"ExpiresIn": durafmt.Parse(now.Add(maxTime).Sub(now)).LimitFirstN(2).String(),
64
64
+
"Token": t.Token,
65
65
+
"Ip": t.Ip,
36
66
})
37
67
}
38
68
-99
server/handle_account_totp_enroll.go
···
1
1
-
package server
2
2
-
3
3
-
import (
4
4
-
"bytes"
5
5
-
"encoding/base64"
6
6
-
"fmt"
7
7
-
"image/png"
8
8
-
9
9
-
"github.com/haileyok/cocoon/internal/helpers"
10
10
-
"github.com/haileyok/cocoon/models"
11
11
-
"github.com/labstack/echo/v4"
12
12
-
"github.com/pquerna/otp/totp"
13
13
-
)
14
14
-
15
15
-
func (s *Server) handleAccountTotpEnrollGet(e echo.Context) error {
16
16
-
urepo, sess, err := s.getSessionRepoOrErr(e)
17
17
-
if err != nil {
18
18
-
return e.Redirect(303, "/account/signin")
19
19
-
}
20
20
-
21
21
-
if urepo.TwoFactorType == models.TwoFactorTypeTotp {
22
22
-
sess.AddFlash("You have already enabled TOTP", "error")
23
23
-
sess.Save(e.Request(), e.Response())
24
24
-
return e.Redirect(303, "/account")
25
25
-
} else if urepo.TwoFactorType != models.TwoFactorTypeNone {
26
26
-
sess.AddFlash("You have already have another 2FA method enabled", "error")
27
27
-
sess.Save(e.Request(), e.Response())
28
28
-
return e.Redirect(303, "/account")
29
29
-
}
30
30
-
31
31
-
secret, err := totp.Generate(totp.GenerateOpts{
32
32
-
Issuer: s.config.Hostname,
33
33
-
AccountName: urepo.Repo.Did,
34
34
-
})
35
35
-
if err != nil {
36
36
-
s.logger.Error("error generating totp secret", "error", err)
37
37
-
return helpers.ServerError(e, nil)
38
38
-
}
39
39
-
40
40
-
sess.Values["totp-secret"] = secret.String()
41
41
-
if err := sess.Save(e.Request(), e.Response()); err != nil {
42
42
-
s.logger.Error("error saving session", "error", err)
43
43
-
return helpers.ServerError(e, nil)
44
44
-
}
45
45
-
46
46
-
var buf bytes.Buffer
47
47
-
img, err := secret.Image(200, 200)
48
48
-
if err != nil {
49
49
-
s.logger.Error("error generating image from secret", "error", err)
50
50
-
return helpers.ServerError(e, nil)
51
51
-
}
52
52
-
png.Encode(&buf, img)
53
53
-
54
54
-
b64img := fmt.Sprintf("data:image/png;base64,%s", base64.StdEncoding.EncodeToString(buf.Bytes()))
55
55
-
56
56
-
return e.Render(200, "totp_enroll.html", map[string]any{
57
57
-
"flashes": getFlashesFromSession(e, sess),
58
58
-
"Image": b64img,
59
59
-
})
60
60
-
}
61
61
-
62
62
-
type TotpEnrollRequest struct {
63
63
-
Code string `form:"code"`
64
64
-
}
65
65
-
66
66
-
func (s *Server) handleAccountTotpEnrollPost(e echo.Context) error {
67
67
-
urepo, sess, err := s.getSessionRepoOrErr(e)
68
68
-
if err != nil {
69
69
-
return e.Redirect(303, "/account/signin")
70
70
-
}
71
71
-
72
72
-
var req TotpEnrollRequest
73
73
-
if err := e.Bind(&req); err != nil {
74
74
-
s.logger.Error("error binding request for enroll totp", "error", err)
75
75
-
return helpers.ServerError(e, nil)
76
76
-
}
77
77
-
78
78
-
secret, ok := sess.Values["totp-secret"].(string)
79
79
-
if !ok {
80
80
-
return helpers.InputError(e, nil)
81
81
-
}
82
82
-
83
83
-
if !totp.Validate(req.Code, secret) {
84
84
-
sess.AddFlash("The provided code was not valid.", "error")
85
85
-
sess.Save(e.Request(), e.Response())
86
86
-
return e.Redirect(303, "/account/totp-enroll")
87
87
-
}
88
88
-
89
89
-
if err := s.db.Exec("UPDATE repos SET two_factor_type = ?, totp_secret = ? WHERE did = ?", nil, models.TwoFactorTypeTotp, secret, urepo.Repo.Did).Error; err != nil {
90
90
-
s.logger.Error("error updating database with totp token", "error", err)
91
91
-
return helpers.ServerError(e, nil)
92
92
-
}
93
93
-
94
94
-
sess.AddFlash("You have successfully enrolled in TOTP!", "success")
95
95
-
delete(sess.Values, "totp-secret")
96
96
-
sess.Save(e.Request(), e.Response())
97
97
-
98
98
-
return e.Redirect(303, "/account")
99
99
-
}
+2
-3
server/handle_import_repo.go
···
9
9
10
10
"github.com/bluesky-social/indigo/atproto/syntax"
11
11
"github.com/bluesky-social/indigo/repo"
12
12
-
"github.com/haileyok/cocoon/blockstore"
13
12
"github.com/haileyok/cocoon/internal/helpers"
14
13
"github.com/haileyok/cocoon/models"
15
14
blocks "github.com/ipfs/go-block-format"
···
27
26
return helpers.ServerError(e, nil)
28
27
}
29
28
30
30
-
bs := blockstore.New(urepo.Repo.Did, s.db)
29
29
+
bs := s.getBlockstore(urepo.Repo.Did)
31
30
32
31
cs, err := car.NewCarReader(bytes.NewReader(b))
33
32
if err != nil {
···
107
106
return helpers.ServerError(e, nil)
108
107
}
109
108
110
110
-
if err := bs.UpdateRepo(context.TODO(), root, rev); err != nil {
109
109
+
if err := s.UpdateRepo(context.TODO(), urepo.Repo.Did, root, rev); err != nil {
111
110
s.logger.Error("error updating repo after commit", "error", err)
112
111
return helpers.ServerError(e, nil)
113
112
}
+1
-1
server/handle_oauth_authorize.go
···
113
113
114
114
code := oauth.GenerateCode()
115
115
116
116
-
if err := s.db.Exec("UPDATE oauth_authorization_requests SET sub = ?, code = ?, accepted = ? WHERE request_id = ?", nil, repo.Repo.Did, code, true, reqId).Error; err != nil {
116
116
+
if err := s.db.Exec("UPDATE oauth_authorization_requests SET sub = ?, code = ?, accepted = ?, ip = ? WHERE request_id = ?", nil, repo.Repo.Did, code, true, e.RealIP(), reqId).Error; err != nil {
117
117
s.logger.Error("error updating authorization request", "error", err)
118
118
return helpers.ServerError(e, nil)
119
119
}
+4
-10
server/handle_oauth_token.go
···
157
157
Code: *authReq.Code,
158
158
Token: accessString,
159
159
RefreshToken: refreshToken,
160
160
+
Ip: authReq.Ip,
160
161
}, nil).Error; err != nil {
161
162
s.logger.Error("error creating token in db", "error", err)
162
163
return helpers.ServerError(e, nil)
···
203
204
return helpers.InputError(e, to.StringPtr("dpop proof does not match expected jkt"))
204
205
}
205
206
206
206
-
sessionLifetime := constants.PublicClientSessionLifetime
207
207
-
refreshLifetime := constants.PublicClientRefreshLifetime
208
208
-
if clientAuth.Method != "none" {
209
209
-
sessionLifetime = constants.ConfidentialClientSessionLifetime
210
210
-
refreshLifetime = constants.ConfidentialClientRefreshLifetime
211
211
-
}
207
207
+
ageRes := oauth.GetSessionAgeFromToken(oauthToken)
212
208
213
213
-
sessionAge := time.Since(oauthToken.CreatedAt)
214
214
-
if sessionAge > sessionLifetime {
209
209
+
if ageRes.SessionExpired {
215
210
return helpers.InputError(e, to.StringPtr("Session expired"))
216
211
}
217
212
218
218
-
refreshAge := time.Since(oauthToken.UpdatedAt)
219
219
-
if refreshAge > refreshLifetime {
213
213
+
if ageRes.RefreshExpired {
220
214
return helpers.InputError(e, to.StringPtr("Refresh token expired"))
221
215
}
222
216
+27
-15
server/handle_proxy.go
···
17
17
secp256k1secec "gitlab.com/yawning/secp256k1-voi/secec"
18
18
)
19
19
20
20
-
func (s *Server) handleProxy(e echo.Context) error {
21
21
-
repo, isAuthed := e.Get("repo").(*models.RepoActor)
22
22
-
23
23
-
pts := strings.Split(e.Request().URL.Path, "/")
24
24
-
if len(pts) != 3 {
25
25
-
return fmt.Errorf("incorrect number of parts")
26
26
-
}
27
27
-
20
20
+
func (s *Server) getAtprotoProxyEndpointFromRequest(e echo.Context) (string, string, error) {
28
21
svc := e.Request().Header.Get("atproto-proxy")
29
22
if svc == "" {
30
30
-
svc = "did:web:api.bsky.app#bsky_appview" // TODO: should be a config var probably
23
23
+
svc = s.config.DefaultAtprotoProxy
31
24
}
32
25
33
26
svcPts := strings.Split(svc, "#")
34
27
if len(svcPts) != 2 {
35
35
-
return fmt.Errorf("invalid service header")
28
28
+
return "", "", fmt.Errorf("invalid service header")
36
29
}
37
30
38
31
svcDid := svcPts[0]
···
40
33
41
34
doc, err := s.passport.FetchDoc(e.Request().Context(), svcDid)
42
35
if err != nil {
43
43
-
return err
36
36
+
return "", "", err
44
37
}
45
38
46
39
var endpoint string
···
50
43
}
51
44
}
52
45
46
46
+
return endpoint, svcDid, nil
47
47
+
}
48
48
+
49
49
+
func (s *Server) handleProxy(e echo.Context) error {
50
50
+
lgr := s.logger.With("handler", "handleProxy")
51
51
+
52
52
+
repo, isAuthed := e.Get("repo").(*models.RepoActor)
53
53
+
54
54
+
pts := strings.Split(e.Request().URL.Path, "/")
55
55
+
if len(pts) != 3 {
56
56
+
return fmt.Errorf("incorrect number of parts")
57
57
+
}
58
58
+
59
59
+
endpoint, svcDid, err := s.getAtprotoProxyEndpointFromRequest(e)
60
60
+
if err != nil {
61
61
+
lgr.Error("could not get atproto proxy", "error", err)
62
62
+
return helpers.ServerError(e, nil)
63
63
+
}
64
64
+
53
65
requrl := e.Request().URL
54
66
requrl.Host = strings.TrimPrefix(endpoint, "https://")
55
67
requrl.Scheme = "https"
···
78
90
}
79
91
hj, err := json.Marshal(header)
80
92
if err != nil {
81
81
-
s.logger.Error("error marshaling header", "error", err)
93
93
+
lgr.Error("error marshaling header", "error", err)
82
94
return helpers.ServerError(e, nil)
83
95
}
84
96
···
93
105
}
94
106
pj, err := json.Marshal(payload)
95
107
if err != nil {
96
96
-
s.logger.Error("error marashaling payload", "error", err)
108
108
+
lgr.Error("error marashaling payload", "error", err)
97
109
return helpers.ServerError(e, nil)
98
110
}
99
111
···
104
116
105
117
sk, err := secp256k1secec.NewPrivateKey(repo.SigningKey)
106
118
if err != nil {
107
107
-
s.logger.Error("can't load private key", "error", err)
119
119
+
lgr.Error("can't load private key", "error", err)
108
120
return err
109
121
}
110
122
111
123
R, S, _, err := sk.SignRaw(rand.Reader, hash[:])
112
124
if err != nil {
113
113
-
s.logger.Error("error signing", "error", err)
125
125
+
lgr.Error("error signing", "error", err)
114
126
}
115
127
116
128
rBytes := R.Bytes()
+2
-2
server/handle_server_confirm_email.go
···
28
28
}
29
29
30
30
if urepo.EmailVerificationCode == nil || urepo.EmailVerificationCodeExpiresAt == nil {
31
31
-
return helpers.InputError(e, to.StringPtr("ExpiredToken"))
31
31
+
return helpers.ExpiredTokenError(e)
32
32
}
33
33
34
34
if *urepo.EmailVerificationCode != req.Token {
···
36
36
}
37
37
38
38
if time.Now().UTC().After(*urepo.EmailVerificationCodeExpiresAt) {
39
39
-
return helpers.InputError(e, to.StringPtr("ExpiredToken"))
39
39
+
return helpers.ExpiredTokenError(e)
40
40
}
41
41
42
42
now := time.Now().UTC()
+2
-3
server/handle_server_create_account.go
···
14
14
"github.com/bluesky-social/indigo/events"
15
15
"github.com/bluesky-social/indigo/repo"
16
16
"github.com/bluesky-social/indigo/util"
17
17
-
"github.com/haileyok/cocoon/blockstore"
18
17
"github.com/haileyok/cocoon/internal/helpers"
19
18
"github.com/haileyok/cocoon/models"
20
19
"github.com/labstack/echo/v4"
···
177
176
}
178
177
179
178
if customDidHeader == "" {
180
180
-
bs := blockstore.New(signupDid, s.db)
179
179
+
bs := s.getBlockstore(signupDid)
181
180
r := repo.NewRepo(context.TODO(), signupDid, bs)
182
181
183
182
root, rev, err := r.Commit(context.TODO(), urepo.SignFor)
···
186
185
return helpers.ServerError(e, nil)
187
186
}
188
187
189
189
-
if err := bs.UpdateRepo(context.TODO(), root, rev); err != nil {
188
188
+
if err := s.UpdateRepo(context.TODO(), urepo.Did, root, rev); err != nil {
190
189
s.logger.Error("error updating repo after commit", "error", err)
191
190
return helpers.ServerError(e, nil)
192
191
}
+8
-6
server/handle_server_get_service_auth.go
···
19
19
20
20
type ServerGetServiceAuthRequest struct {
21
21
Aud string `query:"aud" validate:"required,atproto-did"`
22
22
-
Exp int64 `query:"exp"`
23
23
-
Lxm string `query:"lxm" validate:"required,atproto-nsid"`
22
22
+
// exp should be a float, as some clients will send a non-integer expiration
23
23
+
Exp float64 `query:"exp"`
24
24
+
Lxm string `query:"lxm" validate:"required,atproto-nsid"`
24
25
}
25
26
26
27
func (s *Server) handleServerGetServiceAuth(e echo.Context) error {
···
34
35
return helpers.InputError(e, nil)
35
36
}
36
37
38
38
+
exp := int64(req.Exp)
37
39
now := time.Now().Unix()
38
38
-
if req.Exp == 0 {
39
39
-
req.Exp = now + 60 // default
40
40
+
if exp == 0 {
41
41
+
exp = now + 60 // default
40
42
}
41
43
42
44
if req.Lxm == "com.atproto.server.getServiceAuth" {
···
44
46
}
45
47
46
48
maxExp := now + (60 * 30)
47
47
-
if req.Exp > maxExp {
49
49
+
if exp > maxExp {
48
50
return helpers.InputError(e, to.StringPtr("expiration too big. smoller please"))
49
51
}
50
52
···
68
70
"aud": req.Aud,
69
71
"lxm": req.Lxm,
70
72
"jti": uuid.NewString(),
71
71
-
"exp": req.Exp,
73
73
+
"exp": exp,
72
74
"iat": now,
73
75
}
74
76
pj, err := json.Marshal(payload)
+2
-2
server/handle_server_reset_password.go
···
33
33
}
34
34
35
35
if *urepo.PasswordResetCode != req.Token {
36
36
-
return helpers.InputError(e, to.StringPtr("InvalidToken"))
36
36
+
return helpers.InvalidTokenError(e)
37
37
}
38
38
39
39
if time.Now().UTC().After(*urepo.PasswordResetCodeExpiresAt) {
40
40
-
return helpers.InputError(e, to.StringPtr("ExpiredToken"))
40
40
+
return helpers.ExpiredTokenError(e)
41
41
}
42
42
43
43
hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), 10)
+3
-4
server/handle_server_update_email.go
···
3
3
import (
4
4
"time"
5
5
6
6
-
"github.com/Azure/go-autorest/autorest/to"
7
6
"github.com/haileyok/cocoon/internal/helpers"
8
7
"github.com/haileyok/cocoon/models"
9
8
"github.com/labstack/echo/v4"
···
29
28
}
30
29
31
30
if urepo.EmailUpdateCode == nil || urepo.EmailUpdateCodeExpiresAt == nil {
32
32
-
return helpers.InputError(e, to.StringPtr("InvalidToken"))
31
31
+
return helpers.InvalidTokenError(e)
33
32
}
34
33
35
34
if *urepo.EmailUpdateCode != req.Token {
36
36
-
return helpers.InputError(e, to.StringPtr("InvalidToken"))
35
35
+
return helpers.InvalidTokenError(e)
37
36
}
38
37
39
38
if time.Now().UTC().After(*urepo.EmailUpdateCodeExpiresAt) {
40
40
-
return helpers.InputError(e, to.StringPtr("ExpiredToken"))
39
39
+
return helpers.ExpiredTokenError(e)
41
40
}
42
41
43
42
if err := s.db.Exec("UPDATE repos SET email_update_code = NULL, email_update_code_expires_at = NULL, email_confirmed_at = NULL, email = ? WHERE did = ?", nil, req.Email, urepo.Repo.Did).Error; err != nil {
+1
-2
server/handle_sync_get_blocks.go
···
6
6
"strings"
7
7
8
8
"github.com/bluesky-social/indigo/carstore"
9
9
-
"github.com/haileyok/cocoon/blockstore"
10
9
"github.com/haileyok/cocoon/internal/helpers"
11
10
"github.com/ipfs/go-cid"
12
11
cbor "github.com/ipfs/go-ipld-cbor"
···
54
53
return helpers.ServerError(e, nil)
55
54
}
56
55
57
57
-
bs := blockstore.New(urepo.Repo.Did, s.db)
56
56
+
bs := s.getBlockstore(urepo.Repo.Did)
58
57
59
58
for _, c := range cids {
60
59
b, err := bs.Get(context.TODO(), c)
+268
server/middleware.go
···
1
1
+
package server
2
2
+
3
3
+
import (
4
4
+
"crypto/sha256"
5
5
+
"encoding/base64"
6
6
+
"fmt"
7
7
+
"strings"
8
8
+
"time"
9
9
+
10
10
+
"github.com/Azure/go-autorest/autorest/to"
11
11
+
"github.com/golang-jwt/jwt/v4"
12
12
+
"github.com/haileyok/cocoon/internal/helpers"
13
13
+
"github.com/haileyok/cocoon/models"
14
14
+
"github.com/haileyok/cocoon/oauth/provider"
15
15
+
"github.com/labstack/echo/v4"
16
16
+
"gitlab.com/yawning/secp256k1-voi"
17
17
+
secp256k1secec "gitlab.com/yawning/secp256k1-voi/secec"
18
18
+
"gorm.io/gorm"
19
19
+
)
20
20
+
21
21
+
func (s *Server) handleAdminMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
22
22
+
return func(e echo.Context) error {
23
23
+
username, password, ok := e.Request().BasicAuth()
24
24
+
if !ok || username != "admin" || password != s.config.AdminPassword {
25
25
+
return helpers.InputError(e, to.StringPtr("Unauthorized"))
26
26
+
}
27
27
+
28
28
+
if err := next(e); err != nil {
29
29
+
e.Error(err)
30
30
+
}
31
31
+
32
32
+
return nil
33
33
+
}
34
34
+
}
35
35
+
36
36
+
func (s *Server) handleLegacySessionMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
37
37
+
return func(e echo.Context) error {
38
38
+
authheader := e.Request().Header.Get("authorization")
39
39
+
if authheader == "" {
40
40
+
return e.JSON(401, map[string]string{"error": "Unauthorized"})
41
41
+
}
42
42
+
43
43
+
pts := strings.Split(authheader, " ")
44
44
+
if len(pts) != 2 {
45
45
+
return helpers.ServerError(e, nil)
46
46
+
}
47
47
+
48
48
+
// move on to oauth session middleware if this is a dpop token
49
49
+
if pts[0] == "DPoP" {
50
50
+
return next(e)
51
51
+
}
52
52
+
53
53
+
tokenstr := pts[1]
54
54
+
token, _, err := new(jwt.Parser).ParseUnverified(tokenstr, jwt.MapClaims{})
55
55
+
claims, ok := token.Claims.(jwt.MapClaims)
56
56
+
if !ok {
57
57
+
return helpers.InvalidTokenError(e)
58
58
+
}
59
59
+
60
60
+
var did string
61
61
+
var repo *models.RepoActor
62
62
+
63
63
+
// service auth tokens
64
64
+
lxm, hasLxm := claims["lxm"]
65
65
+
if hasLxm {
66
66
+
pts := strings.Split(e.Request().URL.String(), "/")
67
67
+
if lxm != pts[len(pts)-1] {
68
68
+
s.logger.Error("service auth lxm incorrect", "lxm", lxm, "expected", pts[len(pts)-1], "error", err)
69
69
+
return helpers.InputError(e, nil)
70
70
+
}
71
71
+
72
72
+
maybeDid, ok := claims["iss"].(string)
73
73
+
if !ok {
74
74
+
s.logger.Error("no iss in service auth token", "error", err)
75
75
+
return helpers.InputError(e, nil)
76
76
+
}
77
77
+
did = maybeDid
78
78
+
79
79
+
maybeRepo, err := s.getRepoActorByDid(did)
80
80
+
if err != nil {
81
81
+
s.logger.Error("error fetching repo", "error", err)
82
82
+
return helpers.ServerError(e, nil)
83
83
+
}
84
84
+
repo = maybeRepo
85
85
+
}
86
86
+
87
87
+
if token.Header["alg"] != "ES256K" {
88
88
+
token, err = new(jwt.Parser).Parse(tokenstr, func(t *jwt.Token) (any, error) {
89
89
+
if _, ok := t.Method.(*jwt.SigningMethodECDSA); !ok {
90
90
+
return nil, fmt.Errorf("unsupported signing method: %v", t.Header["alg"])
91
91
+
}
92
92
+
return s.privateKey.Public(), nil
93
93
+
})
94
94
+
if err != nil {
95
95
+
s.logger.Error("error parsing jwt", "error", err)
96
96
+
return helpers.ExpiredTokenError(e)
97
97
+
}
98
98
+
99
99
+
if !token.Valid {
100
100
+
return helpers.InvalidTokenError(e)
101
101
+
}
102
102
+
} else {
103
103
+
kpts := strings.Split(tokenstr, ".")
104
104
+
signingInput := kpts[0] + "." + kpts[1]
105
105
+
hash := sha256.Sum256([]byte(signingInput))
106
106
+
sigBytes, err := base64.RawURLEncoding.DecodeString(kpts[2])
107
107
+
if err != nil {
108
108
+
s.logger.Error("error decoding signature bytes", "error", err)
109
109
+
return helpers.ServerError(e, nil)
110
110
+
}
111
111
+
112
112
+
if len(sigBytes) != 64 {
113
113
+
s.logger.Error("incorrect sigbytes length", "length", len(sigBytes))
114
114
+
return helpers.ServerError(e, nil)
115
115
+
}
116
116
+
117
117
+
rBytes := sigBytes[:32]
118
118
+
sBytes := sigBytes[32:]
119
119
+
rr, _ := secp256k1.NewScalarFromBytes((*[32]byte)(rBytes))
120
120
+
ss, _ := secp256k1.NewScalarFromBytes((*[32]byte)(sBytes))
121
121
+
122
122
+
sk, err := secp256k1secec.NewPrivateKey(repo.SigningKey)
123
123
+
if err != nil {
124
124
+
s.logger.Error("can't load private key", "error", err)
125
125
+
return err
126
126
+
}
127
127
+
128
128
+
pubKey, ok := sk.Public().(*secp256k1secec.PublicKey)
129
129
+
if !ok {
130
130
+
s.logger.Error("error getting public key from sk")
131
131
+
return helpers.ServerError(e, nil)
132
132
+
}
133
133
+
134
134
+
verified := pubKey.VerifyRaw(hash[:], rr, ss)
135
135
+
if !verified {
136
136
+
s.logger.Error("error verifying", "error", err)
137
137
+
return helpers.ServerError(e, nil)
138
138
+
}
139
139
+
}
140
140
+
141
141
+
isRefresh := e.Request().URL.Path == "/xrpc/com.atproto.server.refreshSession"
142
142
+
scope, _ := claims["scope"].(string)
143
143
+
144
144
+
if isRefresh && scope != "com.atproto.refresh" {
145
145
+
return helpers.InvalidTokenError(e)
146
146
+
} else if !hasLxm && !isRefresh && scope != "com.atproto.access" {
147
147
+
return helpers.InvalidTokenError(e)
148
148
+
}
149
149
+
150
150
+
table := "tokens"
151
151
+
if isRefresh {
152
152
+
table = "refresh_tokens"
153
153
+
}
154
154
+
155
155
+
if isRefresh {
156
156
+
type Result struct {
157
157
+
Found bool
158
158
+
}
159
159
+
var result Result
160
160
+
if err := s.db.Raw("SELECT EXISTS(SELECT 1 FROM "+table+" WHERE token = ?) AS found", nil, tokenstr).Scan(&result).Error; err != nil {
161
161
+
if err == gorm.ErrRecordNotFound {
162
162
+
return helpers.InvalidTokenError(e)
163
163
+
}
164
164
+
165
165
+
s.logger.Error("error getting token from db", "error", err)
166
166
+
return helpers.ServerError(e, nil)
167
167
+
}
168
168
+
169
169
+
if !result.Found {
170
170
+
return helpers.InvalidTokenError(e)
171
171
+
}
172
172
+
}
173
173
+
174
174
+
exp, ok := claims["exp"].(float64)
175
175
+
if !ok {
176
176
+
s.logger.Error("error getting iat from token")
177
177
+
return helpers.ServerError(e, nil)
178
178
+
}
179
179
+
180
180
+
if exp < float64(time.Now().UTC().Unix()) {
181
181
+
return helpers.ExpiredTokenError(e)
182
182
+
}
183
183
+
184
184
+
if repo == nil {
185
185
+
maybeRepo, err := s.getRepoActorByDid(claims["sub"].(string))
186
186
+
if err != nil {
187
187
+
s.logger.Error("error fetching repo", "error", err)
188
188
+
return helpers.ServerError(e, nil)
189
189
+
}
190
190
+
repo = maybeRepo
191
191
+
did = repo.Repo.Did
192
192
+
}
193
193
+
194
194
+
e.Set("repo", repo)
195
195
+
e.Set("did", did)
196
196
+
e.Set("token", tokenstr)
197
197
+
198
198
+
if err := next(e); err != nil {
199
199
+
return helpers.InvalidTokenError(e)
200
200
+
}
201
201
+
202
202
+
return nil
203
203
+
}
204
204
+
}
205
205
+
206
206
+
func (s *Server) handleOauthSessionMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
207
207
+
return func(e echo.Context) error {
208
208
+
authheader := e.Request().Header.Get("authorization")
209
209
+
if authheader == "" {
210
210
+
return e.JSON(401, map[string]string{"error": "Unauthorized"})
211
211
+
}
212
212
+
213
213
+
pts := strings.Split(authheader, " ")
214
214
+
if len(pts) != 2 {
215
215
+
return helpers.ServerError(e, nil)
216
216
+
}
217
217
+
218
218
+
if pts[0] != "DPoP" {
219
219
+
return next(e)
220
220
+
}
221
221
+
222
222
+
accessToken := pts[1]
223
223
+
224
224
+
nonce := s.oauthProvider.NextNonce()
225
225
+
if nonce != "" {
226
226
+
e.Response().Header().Set("DPoP-Nonce", nonce)
227
227
+
e.Response().Header().Add("access-control-expose-headers", "DPoP-Nonce")
228
228
+
}
229
229
+
230
230
+
proof, err := s.oauthProvider.DpopManager.CheckProof(e.Request().Method, "https://"+s.config.Hostname+e.Request().URL.String(), e.Request().Header, to.StringPtr(accessToken))
231
231
+
if err != nil {
232
232
+
s.logger.Error("invalid dpop proof", "error", err)
233
233
+
return helpers.InputError(e, to.StringPtr(err.Error()))
234
234
+
}
235
235
+
236
236
+
var oauthToken provider.OauthToken
237
237
+
if err := s.db.Raw("SELECT * FROM oauth_tokens WHERE token = ?", nil, accessToken).Scan(&oauthToken).Error; err != nil {
238
238
+
s.logger.Error("error finding access token in db", "error", err)
239
239
+
return helpers.InputError(e, nil)
240
240
+
}
241
241
+
242
242
+
if oauthToken.Token == "" {
243
243
+
return helpers.InvalidTokenError(e)
244
244
+
}
245
245
+
246
246
+
if *oauthToken.Parameters.DpopJkt != proof.JKT {
247
247
+
s.logger.Error("jkt mismatch", "token", oauthToken.Parameters.DpopJkt, "proof", proof.JKT)
248
248
+
return helpers.InputError(e, to.StringPtr("dpop jkt mismatch"))
249
249
+
}
250
250
+
251
251
+
if time.Now().After(oauthToken.ExpiresAt) {
252
252
+
return helpers.ExpiredTokenError(e)
253
253
+
}
254
254
+
255
255
+
repo, err := s.getRepoActorByDid(oauthToken.Sub)
256
256
+
if err != nil {
257
257
+
s.logger.Error("could not find actor in db", "error", err)
258
258
+
return helpers.ServerError(e, nil)
259
259
+
}
260
260
+
261
261
+
e.Set("repo", repo)
262
262
+
e.Set("did", repo.Repo.Did)
263
263
+
e.Set("token", accessToken)
264
264
+
e.Set("scopes", strings.Split(oauthToken.Parameters.Scope, " "))
265
265
+
266
266
+
return next(e)
267
267
+
}
268
268
+
}
+13
-13
server/repo.go
···
16
16
"github.com/bluesky-social/indigo/events"
17
17
lexutil "github.com/bluesky-social/indigo/lex/util"
18
18
"github.com/bluesky-social/indigo/repo"
19
19
-
"github.com/bluesky-social/indigo/util"
20
20
-
"github.com/haileyok/cocoon/blockstore"
21
19
"github.com/haileyok/cocoon/internal/db"
22
20
"github.com/haileyok/cocoon/models"
21
21
+
"github.com/haileyok/cocoon/recording_blockstore"
23
22
blocks "github.com/ipfs/go-block-format"
24
23
"github.com/ipfs/go-cid"
25
24
cbor "github.com/ipfs/go-ipld-cbor"
···
103
102
return nil, err
104
103
}
105
104
106
106
-
dbs := blockstore.New(urepo.Did, rm.db)
105
105
+
dbs := rm.s.getBlockstore(urepo.Did)
106
106
+
bs := recording_blockstore.New(dbs)
107
107
r, err := repo.OpenRepo(context.TODO(), dbs, rootcid)
108
108
109
109
entries := []models.Record{}
···
274
274
}
275
275
}
276
276
277
277
-
for _, op := range dbs.GetLog() {
277
277
+
for _, op := range bs.GetLogMap() {
278
278
if _, err := carstore.LdWrite(buf, op.Cid().Bytes(), op.RawData()); err != nil {
279
279
return nil, err
280
280
}
···
318
318
Rev: rev,
319
319
Since: &urepo.Rev,
320
320
Commit: lexutil.LexLink(newroot),
321
321
-
Time: time.Now().Format(util.ISO8601),
321
321
+
Time: time.Now().Format(time.RFC3339Nano),
322
322
Ops: ops,
323
323
TooBig: false,
324
324
},
325
325
})
326
326
327
327
-
if err := dbs.UpdateRepo(context.TODO(), newroot, rev); err != nil {
327
327
+
if err := rm.s.UpdateRepo(context.TODO(), urepo.Did, newroot, rev); err != nil {
328
328
return nil, err
329
329
}
330
330
···
345
345
return cid.Undef, nil, err
346
346
}
347
347
348
348
-
dbs := blockstore.New(urepo.Did, rm.db)
349
349
-
bs := util.NewLoggingBstore(dbs)
348
348
+
dbs := rm.s.getBlockstore(urepo.Did)
349
349
+
bs := recording_blockstore.New(dbs)
350
350
351
351
r, err := repo.OpenRepo(context.TODO(), bs, c)
352
352
if err != nil {
···
358
358
return cid.Undef, nil, err
359
359
}
360
360
361
361
-
return c, bs.GetLoggedBlocks(), nil
361
361
+
return c, bs.GetLogArray(), nil
362
362
}
363
363
364
364
func (rm *RepoMan) incrementBlobRefs(urepo models.Repo, cbor []byte) ([]cid.Cid, error) {
···
414
414
return nil, fmt.Errorf("error unmarshaling cbor: %w", err)
415
415
}
416
416
417
417
-
var deepiter func(interface{}) error
418
418
-
deepiter = func(item interface{}) error {
417
417
+
var deepiter func(any) error
418
418
+
deepiter = func(item any) error {
419
419
switch val := item.(type) {
420
420
-
case map[string]interface{}:
420
420
+
case map[string]any:
421
421
if val["$type"] == "blob" {
422
422
if ref, ok := val["ref"].(string); ok {
423
423
c, err := cid.Parse(ref)
···
430
430
return deepiter(v)
431
431
}
432
432
}
433
433
-
case []interface{}:
433
433
+
case []any:
434
434
for _, v := range val {
435
435
deepiter(v)
436
436
}
+39
-281
server/server.go
···
4
4
"bytes"
5
5
"context"
6
6
"crypto/ecdsa"
7
7
-
"crypto/sha256"
8
7
"embed"
9
9
-
"encoding/base64"
10
8
"errors"
11
9
"fmt"
12
10
"io"
···
15
13
"net/smtp"
16
14
"os"
17
15
"path/filepath"
18
18
-
"strings"
19
16
"sync"
20
17
"text/template"
21
18
"time"
22
19
23
23
-
"github.com/Azure/go-autorest/autorest/to"
24
20
"github.com/aws/aws-sdk-go/aws"
25
21
"github.com/aws/aws-sdk-go/aws/credentials"
26
22
"github.com/aws/aws-sdk-go/aws/session"
···
32
28
"github.com/bluesky-social/indigo/xrpc"
33
29
"github.com/domodwyer/mailyak/v3"
34
30
"github.com/go-playground/validator"
35
35
-
"github.com/golang-jwt/jwt/v4"
36
31
"github.com/gorilla/sessions"
37
32
"github.com/haileyok/cocoon/identity"
38
33
"github.com/haileyok/cocoon/internal/db"
···
43
38
"github.com/haileyok/cocoon/oauth/dpop"
44
39
"github.com/haileyok/cocoon/oauth/provider"
45
40
"github.com/haileyok/cocoon/plc"
41
41
+
"github.com/ipfs/go-cid"
46
42
echo_session "github.com/labstack/echo-contrib/session"
47
43
"github.com/labstack/echo/v4"
48
44
"github.com/labstack/echo/v4/middleware"
49
45
slogecho "github.com/samber/slog-echo"
50
50
-
"gitlab.com/yawning/secp256k1-voi"
51
51
-
secp256k1secec "gitlab.com/yawning/secp256k1-voi/secec"
52
46
"gorm.io/driver/sqlite"
53
47
"gorm.io/gorm"
54
48
)
···
109
103
S3Config *S3Config
110
104
111
105
SessionSecret string
106
106
+
107
107
+
DefaultAtprotoProxy string
108
108
+
109
109
+
BlockstoreVariant BlockstoreVariant
112
110
}
113
111
114
112
type config struct {
115
115
-
Version string
116
116
-
Did string
117
117
-
Hostname string
118
118
-
ContactEmail string
119
119
-
EnforcePeering bool
120
120
-
Relays []string
121
121
-
AdminPassword string
122
122
-
SmtpEmail string
123
123
-
SmtpName string
113
113
+
Version string
114
114
+
Did string
115
115
+
Hostname string
116
116
+
ContactEmail string
117
117
+
EnforcePeering bool
118
118
+
Relays []string
119
119
+
AdminPassword string
120
120
+
SmtpEmail string
121
121
+
SmtpName string
122
122
+
DefaultAtprotoProxy string
123
123
+
BlockstoreVariant BlockstoreVariant
124
124
}
125
125
126
126
type CustomValidator struct {
···
197
197
return t.templates.ExecuteTemplate(w, name, data)
198
198
}
199
199
200
200
-
func (s *Server) handleAdminMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
201
201
-
return func(e echo.Context) error {
202
202
-
username, password, ok := e.Request().BasicAuth()
203
203
-
if !ok || username != "admin" || password != s.config.AdminPassword {
204
204
-
return helpers.InputError(e, to.StringPtr("Unauthorized"))
205
205
-
}
206
206
-
207
207
-
if err := next(e); err != nil {
208
208
-
e.Error(err)
209
209
-
}
210
210
-
211
211
-
return nil
212
212
-
}
213
213
-
}
214
214
-
215
215
-
func (s *Server) handleLegacySessionMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
216
216
-
return func(e echo.Context) error {
217
217
-
authheader := e.Request().Header.Get("authorization")
218
218
-
if authheader == "" {
219
219
-
return e.JSON(401, map[string]string{"error": "Unauthorized"})
220
220
-
}
221
221
-
222
222
-
pts := strings.Split(authheader, " ")
223
223
-
if len(pts) != 2 {
224
224
-
return helpers.ServerError(e, nil)
225
225
-
}
226
226
-
227
227
-
// move on to oauth session middleware if this is a dpop token
228
228
-
if pts[0] == "DPoP" {
229
229
-
return next(e)
230
230
-
}
231
231
-
232
232
-
tokenstr := pts[1]
233
233
-
token, _, err := new(jwt.Parser).ParseUnverified(tokenstr, jwt.MapClaims{})
234
234
-
claims, ok := token.Claims.(jwt.MapClaims)
235
235
-
if !ok {
236
236
-
return helpers.InputError(e, to.StringPtr("InvalidToken"))
237
237
-
}
238
238
-
239
239
-
var did string
240
240
-
var repo *models.RepoActor
241
241
-
242
242
-
// service auth tokens
243
243
-
lxm, hasLxm := claims["lxm"]
244
244
-
if hasLxm {
245
245
-
pts := strings.Split(e.Request().URL.String(), "/")
246
246
-
if lxm != pts[len(pts)-1] {
247
247
-
s.logger.Error("service auth lxm incorrect", "lxm", lxm, "expected", pts[len(pts)-1], "error", err)
248
248
-
return helpers.InputError(e, nil)
249
249
-
}
250
250
-
251
251
-
maybeDid, ok := claims["iss"].(string)
252
252
-
if !ok {
253
253
-
s.logger.Error("no iss in service auth token", "error", err)
254
254
-
return helpers.InputError(e, nil)
255
255
-
}
256
256
-
did = maybeDid
257
257
-
258
258
-
maybeRepo, err := s.getRepoActorByDid(did)
259
259
-
if err != nil {
260
260
-
s.logger.Error("error fetching repo", "error", err)
261
261
-
return helpers.ServerError(e, nil)
262
262
-
}
263
263
-
repo = maybeRepo
264
264
-
}
265
265
-
266
266
-
if token.Header["alg"] != "ES256K" {
267
267
-
token, err = new(jwt.Parser).Parse(tokenstr, func(t *jwt.Token) (any, error) {
268
268
-
if _, ok := t.Method.(*jwt.SigningMethodECDSA); !ok {
269
269
-
return nil, fmt.Errorf("unsupported signing method: %v", t.Header["alg"])
270
270
-
}
271
271
-
return s.privateKey.Public(), nil
272
272
-
})
273
273
-
if err != nil {
274
274
-
s.logger.Error("error parsing jwt", "error", err)
275
275
-
// NOTE: https://github.com/bluesky-social/atproto/discussions/3319
276
276
-
return e.JSON(400, map[string]string{"error": "ExpiredToken", "message": "token has expired"})
277
277
-
}
278
278
-
279
279
-
if !token.Valid {
280
280
-
return helpers.InputError(e, to.StringPtr("InvalidToken"))
281
281
-
}
282
282
-
} else {
283
283
-
kpts := strings.Split(tokenstr, ".")
284
284
-
signingInput := kpts[0] + "." + kpts[1]
285
285
-
hash := sha256.Sum256([]byte(signingInput))
286
286
-
sigBytes, err := base64.RawURLEncoding.DecodeString(kpts[2])
287
287
-
if err != nil {
288
288
-
s.logger.Error("error decoding signature bytes", "error", err)
289
289
-
return helpers.ServerError(e, nil)
290
290
-
}
291
291
-
292
292
-
if len(sigBytes) != 64 {
293
293
-
s.logger.Error("incorrect sigbytes length", "length", len(sigBytes))
294
294
-
return helpers.ServerError(e, nil)
295
295
-
}
296
296
-
297
297
-
rBytes := sigBytes[:32]
298
298
-
sBytes := sigBytes[32:]
299
299
-
rr, _ := secp256k1.NewScalarFromBytes((*[32]byte)(rBytes))
300
300
-
ss, _ := secp256k1.NewScalarFromBytes((*[32]byte)(sBytes))
301
301
-
302
302
-
sk, err := secp256k1secec.NewPrivateKey(repo.SigningKey)
303
303
-
if err != nil {
304
304
-
s.logger.Error("can't load private key", "error", err)
305
305
-
return err
306
306
-
}
307
307
-
308
308
-
pubKey, ok := sk.Public().(*secp256k1secec.PublicKey)
309
309
-
if !ok {
310
310
-
s.logger.Error("error getting public key from sk")
311
311
-
return helpers.ServerError(e, nil)
312
312
-
}
313
313
-
314
314
-
verified := pubKey.VerifyRaw(hash[:], rr, ss)
315
315
-
if !verified {
316
316
-
s.logger.Error("error verifying", "error", err)
317
317
-
return helpers.ServerError(e, nil)
318
318
-
}
319
319
-
}
320
320
-
321
321
-
isRefresh := e.Request().URL.Path == "/xrpc/com.atproto.server.refreshSession"
322
322
-
scope, _ := claims["scope"].(string)
323
323
-
324
324
-
if isRefresh && scope != "com.atproto.refresh" {
325
325
-
return helpers.InputError(e, to.StringPtr("InvalidToken"))
326
326
-
} else if !hasLxm && !isRefresh && scope != "com.atproto.access" {
327
327
-
return helpers.InputError(e, to.StringPtr("InvalidToken"))
328
328
-
}
329
329
-
330
330
-
table := "tokens"
331
331
-
if isRefresh {
332
332
-
table = "refresh_tokens"
333
333
-
}
334
334
-
335
335
-
if isRefresh {
336
336
-
type Result struct {
337
337
-
Found bool
338
338
-
}
339
339
-
var result Result
340
340
-
if err := s.db.Raw("SELECT EXISTS(SELECT 1 FROM "+table+" WHERE token = ?) AS found", nil, tokenstr).Scan(&result).Error; err != nil {
341
341
-
if err == gorm.ErrRecordNotFound {
342
342
-
return helpers.InputError(e, to.StringPtr("InvalidToken"))
343
343
-
}
344
344
-
345
345
-
s.logger.Error("error getting token from db", "error", err)
346
346
-
return helpers.ServerError(e, nil)
347
347
-
}
348
348
-
349
349
-
if !result.Found {
350
350
-
return helpers.InputError(e, to.StringPtr("InvalidToken"))
351
351
-
}
352
352
-
}
353
353
-
354
354
-
exp, ok := claims["exp"].(float64)
355
355
-
if !ok {
356
356
-
s.logger.Error("error getting iat from token")
357
357
-
return helpers.ServerError(e, nil)
358
358
-
}
359
359
-
360
360
-
if exp < float64(time.Now().UTC().Unix()) {
361
361
-
return helpers.InputError(e, to.StringPtr("ExpiredToken"))
362
362
-
}
363
363
-
364
364
-
if repo == nil {
365
365
-
maybeRepo, err := s.getRepoActorByDid(claims["sub"].(string))
366
366
-
if err != nil {
367
367
-
s.logger.Error("error fetching repo", "error", err)
368
368
-
return helpers.ServerError(e, nil)
369
369
-
}
370
370
-
repo = maybeRepo
371
371
-
did = repo.Repo.Did
372
372
-
}
373
373
-
374
374
-
e.Set("repo", repo)
375
375
-
e.Set("did", did)
376
376
-
e.Set("token", tokenstr)
377
377
-
378
378
-
if err := next(e); err != nil {
379
379
-
e.Error(err)
380
380
-
}
381
381
-
382
382
-
return nil
383
383
-
}
384
384
-
}
385
385
-
386
386
-
func (s *Server) handleOauthSessionMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
387
387
-
return func(e echo.Context) error {
388
388
-
authheader := e.Request().Header.Get("authorization")
389
389
-
if authheader == "" {
390
390
-
return e.JSON(401, map[string]string{"error": "Unauthorized"})
391
391
-
}
392
392
-
393
393
-
pts := strings.Split(authheader, " ")
394
394
-
if len(pts) != 2 {
395
395
-
return helpers.ServerError(e, nil)
396
396
-
}
397
397
-
398
398
-
if pts[0] != "DPoP" {
399
399
-
return next(e)
400
400
-
}
401
401
-
402
402
-
accessToken := pts[1]
403
403
-
404
404
-
nonce := s.oauthProvider.NextNonce()
405
405
-
if nonce != "" {
406
406
-
e.Response().Header().Set("DPoP-Nonce", nonce)
407
407
-
e.Response().Header().Add("access-control-expose-headers", "DPoP-Nonce")
408
408
-
}
409
409
-
410
410
-
proof, err := s.oauthProvider.DpopManager.CheckProof(e.Request().Method, "https://"+s.config.Hostname+e.Request().URL.String(), e.Request().Header, to.StringPtr(accessToken))
411
411
-
if err != nil {
412
412
-
s.logger.Error("invalid dpop proof", "error", err)
413
413
-
return helpers.InputError(e, to.StringPtr(err.Error()))
414
414
-
}
415
415
-
416
416
-
var oauthToken provider.OauthToken
417
417
-
if err := s.db.Raw("SELECT * FROM oauth_tokens WHERE token = ?", nil, accessToken).Scan(&oauthToken).Error; err != nil {
418
418
-
s.logger.Error("error finding access token in db", "error", err)
419
419
-
return helpers.InputError(e, nil)
420
420
-
}
421
421
-
422
422
-
if oauthToken.Token == "" {
423
423
-
return helpers.InputError(e, to.StringPtr("InvalidToken"))
424
424
-
}
425
425
-
426
426
-
if *oauthToken.Parameters.DpopJkt != proof.JKT {
427
427
-
s.logger.Error("jkt mismatch", "token", oauthToken.Parameters.DpopJkt, "proof", proof.JKT)
428
428
-
return helpers.InputError(e, to.StringPtr("dpop jkt mismatch"))
429
429
-
}
430
430
-
431
431
-
if time.Now().After(oauthToken.ExpiresAt) {
432
432
-
return e.JSON(400, map[string]string{"error": "ExpiredToken", "message": "token has expired"})
433
433
-
}
434
434
-
435
435
-
repo, err := s.getRepoActorByDid(oauthToken.Sub)
436
436
-
if err != nil {
437
437
-
s.logger.Error("could not find actor in db", "error", err)
438
438
-
return helpers.ServerError(e, nil)
439
439
-
}
440
440
-
441
441
-
e.Set("repo", repo)
442
442
-
e.Set("did", repo.Repo.Did)
443
443
-
e.Set("token", accessToken)
444
444
-
e.Set("scopes", strings.Split(oauthToken.Parameters.Scope, " "))
445
445
-
446
446
-
return next(e)
447
447
-
}
448
448
-
}
449
449
-
450
200
func New(args *Args) (*Server, error) {
451
201
if args.Addr == "" {
452
202
return nil, fmt.Errorf("addr must be set")
···
593
343
plcClient: plcClient,
594
344
privateKey: &pkey,
595
345
config: &config{
596
596
-
Version: args.Version,
597
597
-
Did: args.Did,
598
598
-
Hostname: args.Hostname,
599
599
-
ContactEmail: args.ContactEmail,
600
600
-
EnforcePeering: false,
601
601
-
Relays: args.Relays,
602
602
-
AdminPassword: args.AdminPassword,
603
603
-
SmtpName: args.SmtpName,
604
604
-
SmtpEmail: args.SmtpEmail,
346
346
+
Version: args.Version,
347
347
+
Did: args.Did,
348
348
+
Hostname: args.Hostname,
349
349
+
ContactEmail: args.ContactEmail,
350
350
+
EnforcePeering: false,
351
351
+
Relays: args.Relays,
352
352
+
AdminPassword: args.AdminPassword,
353
353
+
SmtpName: args.SmtpName,
354
354
+
SmtpEmail: args.SmtpEmail,
355
355
+
DefaultAtprotoProxy: args.DefaultAtprotoProxy,
356
356
+
BlockstoreVariant: args.BlockstoreVariant,
605
357
},
606
358
evtman: events.NewEventManager(events.NewMemPersister()),
607
359
passport: identity.NewPassport(h, identity.NewMemCache(10_000)),
···
690
442
s.echo.GET("/account/signin", s.handleAccountSigninGet)
691
443
s.echo.POST("/account/signin", s.handleAccountSigninPost)
692
444
s.echo.GET("/account/signout", s.handleAccountSignout)
693
693
-
s.echo.GET("/account/totp-enroll", s.handleAccountTotpEnrollGet)
694
694
-
s.echo.POST("/account/totp-enroll", s.handleAccountTotpEnrollPost)
695
445
696
446
// oauth account
697
447
s.echo.GET("/oauth/jwks", s.handleOauthJwks)
···
728
478
s.echo.GET("/xrpc/app.bsky.actor.getPreferences", s.handleActorGetPreferences, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
729
479
s.echo.POST("/xrpc/app.bsky.actor.putPreferences", s.handleActorPutPreferences, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
730
480
731
731
-
// are there any routes that we should be allowing without auth? i dont think so but idk
732
732
-
s.echo.GET("/xrpc/*", s.handleProxy, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
733
733
-
s.echo.POST("/xrpc/*", s.handleProxy, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
734
734
-
735
481
// admin routes
736
482
s.echo.POST("/xrpc/com.atproto.server.createInviteCode", s.handleCreateInviteCode, s.handleAdminMiddleware)
737
483
s.echo.POST("/xrpc/com.atproto.server.createInviteCodes", s.handleCreateInviteCodes, s.handleAdminMiddleware)
484
484
+
485
485
+
// are there any routes that we should be allowing without auth? i dont think so but idk
486
486
+
s.echo.GET("/xrpc/*", s.handleProxy, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
487
487
+
s.echo.POST("/xrpc/*", s.handleProxy, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
738
488
}
739
489
740
490
func (s *Server) Serve(ctx context.Context) error {
···
896
646
go s.doBackup()
897
647
}
898
648
}
649
649
+
650
650
+
func (s *Server) UpdateRepo(ctx context.Context, did string, root cid.Cid, rev string) error {
651
651
+
if err := s.db.Exec("UPDATE repos SET root = ?, rev = ? WHERE did = ?", nil, root.Bytes(), rev, did).Error; err != nil {
652
652
+
return err
653
653
+
}
654
654
+
655
655
+
return nil
656
656
+
}
-9
server/static/style.css
···
24
24
margin-bottom: 1.5em;
25
25
}
26
26
27
27
-
.center {
28
28
-
justify-content: center;
29
29
-
}
30
30
-
31
27
.centered-body {
32
28
min-height: 100vh;
33
29
justify-content: center;
···
85
81
.alert-danger {
86
82
background-color: var(--danger);
87
83
}
88
88
-
89
89
-
.totp-image {
90
90
-
height: 200;
91
91
-
width: 200;
92
92
-
}
+5
-5
server/templates/account.html
···
12
12
<main class="container base-container authorize-container margin-top-xl">
13
13
<h2>Welcome, {{ .Repo.Handle }}</h2>
14
14
<ul>
15
15
-
<li><a href="/account/totp-enroll">Enroll in TOTP</a></li>
16
15
<li><a href="/account/signout">Sign Out</a></li>
17
16
</ul>
18
17
{{ if .flashes.successes }}
···
25
24
</div>
26
25
{{ else }} {{ range .Tokens }}
27
26
<div class="base-container">
28
28
-
<h4>{{ .ClientId }}</h4>
29
29
-
<p>Created: {{ .CreatedAt }}</p>
30
30
-
<p>Updated: {{ .UpdatedAt }}</p>
31
31
-
<p>Expires: {{ .ExpiresAt }}</p>
27
27
+
<h4>{{ .ClientName }}</h4>
28
28
+
<p>Session Age: {{ .Age}}</p>
29
29
+
<p>Last Updated: {{ .LastUpdated }} ago</p>
30
30
+
<p>Expires In: {{ .ExpiresIn }}</p>
31
31
+
<p>IP Address: {{ .Ip }}</p>
32
32
<form action="/account/revoke" method="post">
33
33
<input type="hidden" name="token" value="{{ .Token }}" />
34
34
<button type="submit" value="">Revoke</button>
-32
server/templates/totp_enroll.html
···
1
1
-
<!doctype html>
2
2
-
<html lang="en">
3
3
-
<head>
4
4
-
<meta charset="utf-8" />
5
5
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
6
6
-
<meta name="color-scheme" content="light dark" />
7
7
-
<link rel="stylesheet" href="/static/pico.css" />
8
8
-
<link rel="stylesheet" href="/static/style.css" />
9
9
-
<title>TOTP Enrollment</title>
10
10
-
</head>
11
11
-
<body class="centered-body">
12
12
-
<main class="container base-container box-shadow-container login-container">
13
13
-
<h2>TOTP Enrollment</h2>
14
14
-
<p>
15
15
-
Enroll in TOTP by adding the below secret to your TOTP manager and
16
16
-
verifying the code.
17
17
-
</p>
18
18
-
{{ if .flashes.errors }}
19
19
-
<div class="alert alert-danger margin-bottom-xs">
20
20
-
<p>{{ index .flashes.errors 0 }}</p>
21
21
-
</div>
22
22
-
{{ end }}
23
23
-
<div class="center">
24
24
-
<img src="{{ .Image }}" class="totp-image" />
25
25
-
</div>
26
26
-
<form action="/account/totp-enroll" method="post">
27
27
-
<input name="code" id="code" placeholder="Code" />
28
28
-
<button class="primary" type="submit" value="Login">Enroll</button>
29
29
-
</form>
30
30
-
</main>
31
31
-
</body>
32
32
-
</html>
+155
sqlite_blockstore/sqlite_blockstore.go
···
1
1
+
package sqlite_blockstore
2
2
+
3
3
+
import (
4
4
+
"context"
5
5
+
"fmt"
6
6
+
7
7
+
"github.com/bluesky-social/indigo/atproto/syntax"
8
8
+
"github.com/haileyok/cocoon/internal/db"
9
9
+
"github.com/haileyok/cocoon/models"
10
10
+
blocks "github.com/ipfs/go-block-format"
11
11
+
"github.com/ipfs/go-cid"
12
12
+
"gorm.io/gorm/clause"
13
13
+
)
14
14
+
15
15
+
type SqliteBlockstore struct {
16
16
+
db *db.DB
17
17
+
did string
18
18
+
readonly bool
19
19
+
inserts map[cid.Cid]blocks.Block
20
20
+
}
21
21
+
22
22
+
func New(did string, db *db.DB) *SqliteBlockstore {
23
23
+
return &SqliteBlockstore{
24
24
+
did: did,
25
25
+
db: db,
26
26
+
readonly: false,
27
27
+
inserts: map[cid.Cid]blocks.Block{},
28
28
+
}
29
29
+
}
30
30
+
31
31
+
func NewReadOnly(did string, db *db.DB) *SqliteBlockstore {
32
32
+
return &SqliteBlockstore{
33
33
+
did: did,
34
34
+
db: db,
35
35
+
readonly: true,
36
36
+
inserts: map[cid.Cid]blocks.Block{},
37
37
+
}
38
38
+
}
39
39
+
40
40
+
func (bs *SqliteBlockstore) Get(ctx context.Context, cid cid.Cid) (blocks.Block, error) {
41
41
+
var block models.Block
42
42
+
43
43
+
maybeBlock, ok := bs.inserts[cid]
44
44
+
if ok {
45
45
+
return maybeBlock, nil
46
46
+
}
47
47
+
48
48
+
if err := bs.db.Raw("SELECT * FROM blocks WHERE did = ? AND cid = ?", nil, bs.did, cid.Bytes()).Scan(&block).Error; err != nil {
49
49
+
return nil, err
50
50
+
}
51
51
+
52
52
+
b, err := blocks.NewBlockWithCid(block.Value, cid)
53
53
+
if err != nil {
54
54
+
return nil, err
55
55
+
}
56
56
+
57
57
+
return b, nil
58
58
+
}
59
59
+
60
60
+
func (bs *SqliteBlockstore) Put(ctx context.Context, block blocks.Block) error {
61
61
+
bs.inserts[block.Cid()] = block
62
62
+
63
63
+
if bs.readonly {
64
64
+
return nil
65
65
+
}
66
66
+
67
67
+
b := models.Block{
68
68
+
Did: bs.did,
69
69
+
Cid: block.Cid().Bytes(),
70
70
+
Rev: syntax.NewTIDNow(0).String(), // TODO: WARN, this is bad. don't do this
71
71
+
Value: block.RawData(),
72
72
+
}
73
73
+
74
74
+
if err := bs.db.Create(&b, []clause.Expression{clause.OnConflict{
75
75
+
Columns: []clause.Column{{Name: "did"}, {Name: "cid"}},
76
76
+
UpdateAll: true,
77
77
+
}}).Error; err != nil {
78
78
+
return err
79
79
+
}
80
80
+
81
81
+
return nil
82
82
+
}
83
83
+
84
84
+
func (bs *SqliteBlockstore) DeleteBlock(context.Context, cid.Cid) error {
85
85
+
panic("not implemented")
86
86
+
}
87
87
+
88
88
+
func (bs *SqliteBlockstore) Has(context.Context, cid.Cid) (bool, error) {
89
89
+
panic("not implemented")
90
90
+
}
91
91
+
92
92
+
func (bs *SqliteBlockstore) GetSize(context.Context, cid.Cid) (int, error) {
93
93
+
panic("not implemented")
94
94
+
}
95
95
+
96
96
+
func (bs *SqliteBlockstore) PutMany(ctx context.Context, blocks []blocks.Block) error {
97
97
+
tx := bs.db.BeginDangerously()
98
98
+
99
99
+
for _, block := range blocks {
100
100
+
bs.inserts[block.Cid()] = block
101
101
+
102
102
+
if bs.readonly {
103
103
+
continue
104
104
+
}
105
105
+
106
106
+
b := models.Block{
107
107
+
Did: bs.did,
108
108
+
Cid: block.Cid().Bytes(),
109
109
+
Rev: syntax.NewTIDNow(0).String(), // TODO: WARN, this is bad. don't do this
110
110
+
Value: block.RawData(),
111
111
+
}
112
112
+
113
113
+
if err := tx.Clauses(clause.OnConflict{
114
114
+
Columns: []clause.Column{{Name: "did"}, {Name: "cid"}},
115
115
+
UpdateAll: true,
116
116
+
}).Create(&b).Error; err != nil {
117
117
+
tx.Rollback()
118
118
+
return err
119
119
+
}
120
120
+
}
121
121
+
122
122
+
if bs.readonly {
123
123
+
return nil
124
124
+
}
125
125
+
126
126
+
tx.Commit()
127
127
+
128
128
+
return nil
129
129
+
}
130
130
+
131
131
+
func (bs *SqliteBlockstore) AllKeysChan(ctx context.Context) (<-chan cid.Cid, error) {
132
132
+
panic("not implemented")
133
133
+
}
134
134
+
135
135
+
func (bs *SqliteBlockstore) HashOnRead(enabled bool) {
136
136
+
panic("not implemented")
137
137
+
}
138
138
+
139
139
+
func (bs *SqliteBlockstore) Execute(ctx context.Context) error {
140
140
+
if !bs.readonly {
141
141
+
return fmt.Errorf("blockstore was not readonly")
142
142
+
}
143
143
+
144
144
+
bs.readonly = false
145
145
+
for _, b := range bs.inserts {
146
146
+
bs.Put(ctx, b)
147
147
+
}
148
148
+
bs.readonly = true
149
149
+
150
150
+
return nil
151
151
+
}
152
152
+
153
153
+
func (bs *SqliteBlockstore) GetLog() map[cid.Cid]blocks.Block {
154
154
+
return bs.inserts
155
155
+
}