+63
-7
.github/workflows/docker-image.yml
+63
-7
.github/workflows/docker-image.yml
···
5
push:
6
branches:
7
- main
8
9
env:
10
REGISTRY: ghcr.io
···
12
13
jobs:
14
build-and-push-image:
15
-
runs-on: ubuntu-latest
16
# Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job.
17
permissions:
18
contents: read
19
packages: write
20
attestations: write
21
id-token: write
22
-
#
23
steps:
24
- name: Checkout repository
25
uses: actions/checkout@v4
26
# Uses the `docker/login-action` action to log in to the Container registry registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here.
27
- name: Log in to the Container registry
28
-
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
29
with:
30
registry: ${{ env.REGISTRY }}
31
username: ${{ github.actor }}
32
password: ${{ secrets.GITHUB_TOKEN }}
33
# This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a subsequent step. The `images` value provides the base name for the tags and labels.
34
- name: Extract metadata (tags, labels) for Docker
35
id: meta
···
37
with:
38
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
39
tags: |
40
-
type=sha
41
-
type=sha,format=long
42
# This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If the build succeeds, it pushes the image to GitHub Packages.
43
# It uses the `context` parameter to define the build's context as the set of files located in the specified path. For more information, see "[Usage](https://github.com/docker/build-push-action#usage)" in the README of the `docker/build-push-action` repository.
44
# It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step.
45
- name: Build and push Docker image
46
id: push
47
-
uses: docker/build-push-action@v5
48
with:
49
context: .
50
push: true
51
tags: ${{ steps.meta.outputs.tags }}
52
labels: ${{ steps.meta.outputs.labels }}
53
54
# This step generates an artifact attestation for the image, which is an unforgeable statement about where and how it was built. It increases supply chain security for people who consume the image. For more information, see "[AUTOTITLE](/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds)."
55
- name: Generate artifact attestation
56
uses: actions/attest-build-provenance@v1
57
with:
58
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
59
-
subject-digest: ${{ steps.push.outputs.digest }}
60
push-to-registry: true
···
5
push:
6
branches:
7
- main
8
+
tags:
9
+
- 'v*'
10
11
env:
12
REGISTRY: ghcr.io
···
14
15
jobs:
16
build-and-push-image:
17
+
strategy:
18
+
matrix:
19
+
include:
20
+
- arch: amd64
21
+
runner: ubuntu-latest
22
+
- arch: arm64
23
+
runner: ubuntu-24.04-arm
24
+
runs-on: ${{ matrix.runner }}
25
# Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job.
26
permissions:
27
contents: read
28
packages: write
29
attestations: write
30
id-token: write
31
+
outputs:
32
+
digest-amd64: ${{ matrix.arch == 'amd64' && steps.push.outputs.digest || '' }}
33
+
digest-arm64: ${{ matrix.arch == 'arm64' && steps.push.outputs.digest || '' }}
34
steps:
35
- name: Checkout repository
36
uses: actions/checkout@v4
37
+
38
# Uses the `docker/login-action` action to log in to the Container registry registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here.
39
- name: Log in to the Container registry
40
+
uses: docker/login-action@v3
41
with:
42
registry: ${{ env.REGISTRY }}
43
username: ${{ github.actor }}
44
password: ${{ secrets.GITHUB_TOKEN }}
45
+
46
# This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a subsequent step. The `images` value provides the base name for the tags and labels.
47
- name: Extract metadata (tags, labels) for Docker
48
id: meta
···
50
with:
51
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
52
tags: |
53
+
type=raw,value=latest,enable={{is_default_branch}},suffix=-${{ matrix.arch }}
54
+
type=sha,suffix=-${{ matrix.arch }}
55
+
type=sha,format=long,suffix=-${{ matrix.arch }}
56
+
type=semver,pattern={{version}},suffix=-${{ matrix.arch }}
57
+
type=semver,pattern={{major}}.{{minor}},suffix=-${{ matrix.arch }}
58
+
59
# This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If the build succeeds, it pushes the image to GitHub Packages.
60
# It uses the `context` parameter to define the build's context as the set of files located in the specified path. For more information, see "[Usage](https://github.com/docker/build-push-action#usage)" in the README of the `docker/build-push-action` repository.
61
# It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step.
62
- name: Build and push Docker image
63
id: push
64
+
uses: docker/build-push-action@v6
65
with:
66
context: .
67
push: true
68
tags: ${{ steps.meta.outputs.tags }}
69
labels: ${{ steps.meta.outputs.labels }}
70
71
+
publish-manifest:
72
+
needs: build-and-push-image
73
+
runs-on: ubuntu-latest
74
+
permissions:
75
+
packages: write
76
+
attestations: write
77
+
id-token: write
78
+
steps:
79
+
- name: Log in to the Container registry
80
+
uses: docker/login-action@v3
81
+
with:
82
+
registry: ${{ env.REGISTRY }}
83
+
username: ${{ github.actor }}
84
+
password: ${{ secrets.GITHUB_TOKEN }}
85
+
86
+
- name: Extract metadata (tags, labels) for Docker
87
+
id: meta
88
+
uses: docker/metadata-action@v5
89
+
with:
90
+
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
91
+
tags: |
92
+
type=raw,value=latest,enable={{is_default_branch}}
93
+
type=sha
94
+
type=sha,format=long
95
+
type=semver,pattern={{version}}
96
+
type=semver,pattern={{major}}.{{minor}}
97
+
98
+
- name: Create and push manifest
99
+
run: |
100
+
# Split tags into an array
101
+
readarray -t tags <<< "${{ steps.meta.outputs.tags }}"
102
+
103
+
# Create and push manifest for each tag
104
+
for tag in "${tags[@]}"; do
105
+
docker buildx imagetools create -t "$tag" \
106
+
"${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ needs.build-and-push-image.outputs.digest-amd64 }}" \
107
+
"${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ needs.build-and-push-image.outputs.digest-arm64 }}"
108
+
done
109
+
110
# This step generates an artifact attestation for the image, which is an unforgeable statement about where and how it was built. It increases supply chain security for people who consume the image. For more information, see "[AUTOTITLE](/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds)."
111
- name: Generate artifact attestation
112
uses: actions/attest-build-provenance@v1
113
with:
114
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
115
+
subject-digest: ${{ needs.build-and-push-image.outputs.digest-amd64 }}
116
push-to-registry: true
+10
Caddyfile.postgres
+10
Caddyfile.postgres
+1
-1
Dockerfile
+1
-1
Dockerfile
+37
-1
Makefile
+37
-1
Makefile
···
4
GIT_COMMIT := $(shell git rev-parse --short=9 HEAD)
5
VERSION := $(if $(GIT_TAG),$(GIT_TAG),dev-$(GIT_COMMIT))
6
7
.PHONY: help
8
help: ## Print info about all commands
9
@echo "Commands:"
···
14
build: ## Build all executables
15
go build -ldflags "-X main.Version=$(VERSION)" -o cocoon ./cmd/cocoon
16
17
.PHONY: run
18
run:
19
go build -ldflags "-X main.Version=dev-local" -o cocoon ./cmd/cocoon && ./cocoon run
···
43
44
.PHONY: docker-build
45
docker-build:
46
-
docker build -t cocoon .
···
4
GIT_COMMIT := $(shell git rev-parse --short=9 HEAD)
5
VERSION := $(if $(GIT_TAG),$(GIT_TAG),dev-$(GIT_COMMIT))
6
7
+
# Build output directory
8
+
BUILD_DIR := dist
9
+
10
+
# Platforms to build for
11
+
PLATFORMS := \
12
+
linux/amd64 \
13
+
linux/arm64 \
14
+
linux/arm \
15
+
darwin/amd64 \
16
+
darwin/arm64 \
17
+
windows/amd64 \
18
+
windows/arm64 \
19
+
freebsd/amd64 \
20
+
freebsd/arm64 \
21
+
openbsd/amd64 \
22
+
openbsd/arm64
23
+
24
.PHONY: help
25
help: ## Print info about all commands
26
@echo "Commands:"
···
31
build: ## Build all executables
32
go build -ldflags "-X main.Version=$(VERSION)" -o cocoon ./cmd/cocoon
33
34
+
.PHONY: build-release
35
+
build-all: ## Build binaries for all architectures
36
+
@echo "Building for all architectures..."
37
+
@mkdir -p $(BUILD_DIR)
38
+
@$(foreach platform,$(PLATFORMS), \
39
+
$(eval OS := $(word 1,$(subst /, ,$(platform)))) \
40
+
$(eval ARCH := $(word 2,$(subst /, ,$(platform)))) \
41
+
$(eval EXT := $(if $(filter windows,$(OS)),.exe,)) \
42
+
$(eval OUTPUT := $(BUILD_DIR)/cocoon-$(VERSION)-$(OS)-$(ARCH)$(EXT)) \
43
+
echo "Building $(OS)/$(ARCH)..."; \
44
+
GOOS=$(OS) GOARCH=$(ARCH) go build -ldflags "-X main.Version=$(VERSION)" -o $(OUTPUT) ./cmd/cocoon && \
45
+
echo " โ $(OUTPUT)" || echo " โ Failed: $(OS)/$(ARCH)"; \
46
+
)
47
+
@echo "Done! Binaries are in $(BUILD_DIR)/"
48
+
49
+
.PHONY: clean-dist
50
+
clean-dist: ## Remove all built binaries
51
+
rm -rf $(BUILD_DIR)
52
+
53
.PHONY: run
54
run:
55
go build -ldflags "-X main.Version=dev-local" -o cocoon ./cmd/cocoon && ./cocoon run
···
79
80
.PHONY: docker-build
81
docker-build:
82
+
docker build -t cocoon .
+64
-11
README.md
+64
-11
README.md
···
55
docker-compose up -d
56
```
57
58
5. **Get your invite code**
59
60
On first run, an invite code is automatically created. View it with:
···
96
97
### Optional Configuration
98
99
#### SMTP Email Settings
100
```bash
101
COCOON_SMTP_USER="your-smtp-username"
···
107
```
108
109
#### S3 Storage
110
```bash
111
COCOON_S3_BACKUPS_ENABLED=true
112
COCOON_S3_BLOBSTORE_ENABLED=true
113
COCOON_S3_REGION="us-east-1"
114
COCOON_S3_BUCKET="your-bucket"
115
COCOON_S3_ENDPOINT="https://s3.amazonaws.com"
116
COCOON_S3_ACCESS_KEY="your-access-key"
117
COCOON_S3_SECRET_KEY="your-secret-key"
118
```
119
120
### Management Commands
121
···
143
144
### Identity
145
146
-
- [ ] `com.atproto.identity.getRecommendedDidCredentials`
147
-
- [ ] `com.atproto.identity.requestPlcOperationSignature`
148
- [x] `com.atproto.identity.resolveHandle`
149
-
- [ ] `com.atproto.identity.signPlcOperation`
150
-
- [ ] `com.atproto.identity.submitPlcOperation`
151
- [x] `com.atproto.identity.updateHandle`
152
153
### Repo
···
158
- [x] `com.atproto.repo.deleteRecord`
159
- [x] `com.atproto.repo.describeRepo`
160
- [x] `com.atproto.repo.getRecord`
161
-
- [x] `com.atproto.repo.importRepo` (Works "okay". You still have to handle PLC operations on your own when migrating. Use with extreme caution.)
162
- [x] `com.atproto.repo.listRecords`
163
-
- [ ] `com.atproto.repo.listMissingBlobs`
164
165
### Server
166
···
171
- [x] `com.atproto.server.createInviteCode`
172
- [x] `com.atproto.server.createInviteCodes`
173
- [x] `com.atproto.server.deactivateAccount`
174
-
- [ ] `com.atproto.server.deleteAccount`
175
- [x] `com.atproto.server.deleteSession`
176
- [x] `com.atproto.server.describeServer`
177
- [ ] `com.atproto.server.getAccountInviteCodes`
178
-
- [ ] `com.atproto.server.getServiceAuth`
179
- ~~[ ] `com.atproto.server.listAppPasswords`~~ - not going to add app passwords
180
- [x] `com.atproto.server.refreshSession`
181
-
- [ ] `com.atproto.server.requestAccountDelete`
182
- [x] `com.atproto.server.requestEmailConfirmation`
183
- [x] `com.atproto.server.requestEmailUpdate`
184
- [x] `com.atproto.server.requestPasswordReset`
185
-
- [ ] `com.atproto.server.reserveSigningKey`
186
- [x] `com.atproto.server.resetPassword`
187
- ~~[] `com.atproto.server.revokeAppPassword`~~ - not going to add app passwords
188
- [x] `com.atproto.server.updateEmail`
···
203
204
### Other
205
206
-
- [ ] `com.atproto.label.queryLabels`
207
- [x] `com.atproto.moderation.createReport` (Note: this should be handled by proxying, not actually implemented in the PDS)
208
- [x] `app.bsky.actor.getPreferences`
209
- [x] `app.bsky.actor.putPreferences`
···
55
docker-compose up -d
56
```
57
58
+
**For PostgreSQL deployment:**
59
+
```bash
60
+
# Add POSTGRES_PASSWORD to your .env file first!
61
+
docker-compose -f docker-compose.postgres.yaml up -d
62
+
```
63
+
64
5. **Get your invite code**
65
66
On first run, an invite code is automatically created. View it with:
···
102
103
### Optional Configuration
104
105
+
#### Database Configuration
106
+
107
+
By default, Cocoon uses SQLite which requires no additional setup. For production deployments with higher traffic, you can use PostgreSQL:
108
+
109
+
```bash
110
+
# Database type: sqlite (default) or postgres
111
+
COCOON_DB_TYPE="postgres"
112
+
113
+
# PostgreSQL connection string (required if db-type is postgres)
114
+
# Format: postgres://user:password@host:port/database?sslmode=disable
115
+
COCOON_DATABASE_URL="postgres://cocoon:password@localhost:5432/cocoon?sslmode=disable"
116
+
117
+
# Or use the standard DATABASE_URL environment variable
118
+
DATABASE_URL="postgres://cocoon:password@localhost:5432/cocoon?sslmode=disable"
119
+
```
120
+
121
+
For SQLite (default):
122
+
```bash
123
+
COCOON_DB_TYPE="sqlite"
124
+
COCOON_DB_NAME="/data/cocoon/cocoon.db"
125
+
```
126
+
127
+
> **Note**: When using PostgreSQL, database backups to S3 are not handled by Cocoon. Use `pg_dump` or your database provider's backup solution instead.
128
+
129
#### SMTP Email Settings
130
```bash
131
COCOON_SMTP_USER="your-smtp-username"
···
137
```
138
139
#### S3 Storage
140
+
141
+
Cocoon supports S3-compatible storage for both database backups (SQLite only) and blob storage (images, videos, etc.):
142
+
143
```bash
144
+
# Enable S3 backups (SQLite databases only - hourly backups)
145
COCOON_S3_BACKUPS_ENABLED=true
146
+
147
+
# Enable S3 for blob storage (images, videos, etc.)
148
+
# When enabled, blobs are stored in S3 instead of the database
149
COCOON_S3_BLOBSTORE_ENABLED=true
150
+
151
+
# S3 configuration (works with AWS S3, MinIO, Cloudflare R2, etc.)
152
COCOON_S3_REGION="us-east-1"
153
COCOON_S3_BUCKET="your-bucket"
154
COCOON_S3_ENDPOINT="https://s3.amazonaws.com"
155
COCOON_S3_ACCESS_KEY="your-access-key"
156
COCOON_S3_SECRET_KEY="your-secret-key"
157
+
158
+
# Optional: CDN/public URL for blob redirects
159
+
# When set, com.atproto.sync.getBlob redirects to this URL instead of proxying
160
+
COCOON_S3_CDN_URL="https://cdn.example.com"
161
```
162
+
163
+
**Blob Storage Options:**
164
+
- `COCOON_S3_BLOBSTORE_ENABLED=false` (default): Blobs stored in the database
165
+
- `COCOON_S3_BLOBSTORE_ENABLED=true`: Blobs stored in S3 bucket under `blobs/{did}/{cid}`
166
+
167
+
**Blob Serving Options:**
168
+
- Without `COCOON_S3_CDN_URL`: Blobs are proxied through the PDS server
169
+
- With `COCOON_S3_CDN_URL`: `getBlob` returns a 302 redirect to `{CDN_URL}/blobs/{did}/{cid}`
170
+
171
+
> **Tip**: For Cloudflare R2, you can use the public bucket URL as the CDN URL. For AWS S3, you can use CloudFront or the S3 bucket URL directly if public access is enabled.
172
173
### Management Commands
174
···
196
197
### Identity
198
199
+
- [x] `com.atproto.identity.getRecommendedDidCredentials`
200
+
- [x] `com.atproto.identity.requestPlcOperationSignature`
201
- [x] `com.atproto.identity.resolveHandle`
202
+
- [x] `com.atproto.identity.signPlcOperation`
203
+
- [x] `com.atproto.identity.submitPlcOperation`
204
- [x] `com.atproto.identity.updateHandle`
205
206
### Repo
···
211
- [x] `com.atproto.repo.deleteRecord`
212
- [x] `com.atproto.repo.describeRepo`
213
- [x] `com.atproto.repo.getRecord`
214
+
- [x] `com.atproto.repo.importRepo` (Works "okay". Use with extreme caution.)
215
- [x] `com.atproto.repo.listRecords`
216
+
- [x] `com.atproto.repo.listMissingBlobs`
217
218
### Server
219
···
224
- [x] `com.atproto.server.createInviteCode`
225
- [x] `com.atproto.server.createInviteCodes`
226
- [x] `com.atproto.server.deactivateAccount`
227
+
- [x] `com.atproto.server.deleteAccount`
228
- [x] `com.atproto.server.deleteSession`
229
- [x] `com.atproto.server.describeServer`
230
- [ ] `com.atproto.server.getAccountInviteCodes`
231
+
- [x] `com.atproto.server.getServiceAuth`
232
- ~~[ ] `com.atproto.server.listAppPasswords`~~ - not going to add app passwords
233
- [x] `com.atproto.server.refreshSession`
234
+
- [x] `com.atproto.server.requestAccountDelete`
235
- [x] `com.atproto.server.requestEmailConfirmation`
236
- [x] `com.atproto.server.requestEmailUpdate`
237
- [x] `com.atproto.server.requestPasswordReset`
238
+
- [x] `com.atproto.server.reserveSigningKey`
239
- [x] `com.atproto.server.resetPassword`
240
- ~~[] `com.atproto.server.revokeAppPassword`~~ - not going to add app passwords
241
- [x] `com.atproto.server.updateEmail`
···
256
257
### Other
258
259
+
- [x] `com.atproto.label.queryLabels`
260
- [x] `com.atproto.moderation.createReport` (Note: this should be handled by proxying, not actually implemented in the PDS)
261
- [x] `app.bsky.actor.getPreferences`
262
- [x] `app.bsky.actor.putPreferences`
+59
-4
cmd/cocoon/main.go
+59
-4
cmd/cocoon/main.go
···
9
"os"
10
"time"
11
12
"github.com/bluesky-social/indigo/atproto/atcrypto"
13
"github.com/bluesky-social/indigo/atproto/syntax"
14
"github.com/haileyok/cocoon/internal/helpers"
···
17
"github.com/lestrrat-go/jwx/v2/jwk"
18
"github.com/urfave/cli/v2"
19
"golang.org/x/crypto/bcrypt"
20
"gorm.io/driver/sqlite"
21
"gorm.io/gorm"
22
)
···
39
EnvVars: []string{"COCOON_DB_NAME"},
40
},
41
&cli.StringFlag{
42
Name: "did",
43
EnvVars: []string{"COCOON_DID"},
44
},
···
66
Name: "admin-password",
67
EnvVars: []string{"COCOON_ADMIN_PASSWORD"},
68
},
69
&cli.StringFlag{
70
Name: "smtp-user",
71
EnvVars: []string{"COCOON_SMTP_USER"},
···
117
&cli.StringFlag{
118
Name: "s3-secret-key",
119
EnvVars: []string{"COCOON_S3_SECRET_KEY"},
120
},
121
&cli.StringFlag{
122
Name: "session-secret",
···
131
Name: "fallback-proxy",
132
EnvVars: []string{"COCOON_FALLBACK_PROXY"},
133
},
134
},
135
Commands: []*cli.Command{
136
runServe,
···
154
Flags: []cli.Flag{},
155
Action: func(cmd *cli.Context) error {
156
157
s, err := server.New(&server.Args{
158
Addr: cmd.String("addr"),
159
DbName: cmd.String("db-name"),
160
Did: cmd.String("did"),
161
Hostname: cmd.String("hostname"),
162
RotationKeyPath: cmd.String("rotation-key-path"),
···
165
Version: Version,
166
Relays: cmd.StringSlice("relays"),
167
AdminPassword: cmd.String("admin-password"),
168
SmtpUser: cmd.String("smtp-user"),
169
SmtpPass: cmd.String("smtp-pass"),
170
SmtpHost: cmd.String("smtp-host"),
···
179
Endpoint: cmd.String("s3-endpoint"),
180
AccessKey: cmd.String("s3-access-key"),
181
SecretKey: cmd.String("s3-secret-key"),
182
},
183
SessionSecret: cmd.String("session-secret"),
184
BlockstoreVariant: server.MustReturnBlockstoreVariant(cmd.String("blockstore-variant")),
···
279
},
280
},
281
Action: func(cmd *cli.Context) error {
282
-
db, err := newDb()
283
if err != nil {
284
return err
285
}
···
318
},
319
},
320
Action: func(cmd *cli.Context) error {
321
-
db, err := newDb()
322
if err != nil {
323
return err
324
}
···
345
},
346
}
347
348
-
func newDb() (*gorm.DB, error) {
349
-
return gorm.Open(sqlite.Open("cocoon.db"), &gorm.Config{})
350
}
···
9
"os"
10
"time"
11
12
+
"github.com/bluesky-social/go-util/pkg/telemetry"
13
"github.com/bluesky-social/indigo/atproto/atcrypto"
14
"github.com/bluesky-social/indigo/atproto/syntax"
15
"github.com/haileyok/cocoon/internal/helpers"
···
18
"github.com/lestrrat-go/jwx/v2/jwk"
19
"github.com/urfave/cli/v2"
20
"golang.org/x/crypto/bcrypt"
21
+
"gorm.io/driver/postgres"
22
"gorm.io/driver/sqlite"
23
"gorm.io/gorm"
24
)
···
41
EnvVars: []string{"COCOON_DB_NAME"},
42
},
43
&cli.StringFlag{
44
+
Name: "db-type",
45
+
Value: "sqlite",
46
+
Usage: "Database type: sqlite or postgres",
47
+
EnvVars: []string{"COCOON_DB_TYPE"},
48
+
},
49
+
&cli.StringFlag{
50
+
Name: "database-url",
51
+
Aliases: []string{"db-url"},
52
+
Usage: "PostgreSQL connection string (required if db-type is postgres)",
53
+
EnvVars: []string{"COCOON_DATABASE_URL", "DATABASE_URL"},
54
+
},
55
+
&cli.StringFlag{
56
Name: "did",
57
EnvVars: []string{"COCOON_DID"},
58
},
···
80
Name: "admin-password",
81
EnvVars: []string{"COCOON_ADMIN_PASSWORD"},
82
},
83
+
&cli.BoolFlag{
84
+
Name: "require-invite",
85
+
EnvVars: []string{"COCOON_REQUIRE_INVITE"},
86
+
Value: true,
87
+
},
88
&cli.StringFlag{
89
Name: "smtp-user",
90
EnvVars: []string{"COCOON_SMTP_USER"},
···
136
&cli.StringFlag{
137
Name: "s3-secret-key",
138
EnvVars: []string{"COCOON_S3_SECRET_KEY"},
139
+
},
140
+
&cli.StringFlag{
141
+
Name: "s3-cdn-url",
142
+
EnvVars: []string{"COCOON_S3_CDN_URL"},
143
+
Usage: "Public URL for S3 blob redirects (e.g., https://cdn.example.com). When set, getBlob redirects to this URL instead of proxying.",
144
},
145
&cli.StringFlag{
146
Name: "session-secret",
···
155
Name: "fallback-proxy",
156
EnvVars: []string{"COCOON_FALLBACK_PROXY"},
157
},
158
+
telemetry.CLIFlagDebug,
159
+
telemetry.CLIFlagMetricsListenAddress,
160
},
161
Commands: []*cli.Command{
162
runServe,
···
180
Flags: []cli.Flag{},
181
Action: func(cmd *cli.Context) error {
182
183
+
logger := telemetry.StartLogger(cmd)
184
+
telemetry.StartMetrics(cmd)
185
+
186
s, err := server.New(&server.Args{
187
+
Logger: logger,
188
Addr: cmd.String("addr"),
189
DbName: cmd.String("db-name"),
190
+
DbType: cmd.String("db-type"),
191
+
DatabaseURL: cmd.String("database-url"),
192
Did: cmd.String("did"),
193
Hostname: cmd.String("hostname"),
194
RotationKeyPath: cmd.String("rotation-key-path"),
···
197
Version: Version,
198
Relays: cmd.StringSlice("relays"),
199
AdminPassword: cmd.String("admin-password"),
200
+
RequireInvite: cmd.Bool("require-invite"),
201
SmtpUser: cmd.String("smtp-user"),
202
SmtpPass: cmd.String("smtp-pass"),
203
SmtpHost: cmd.String("smtp-host"),
···
212
Endpoint: cmd.String("s3-endpoint"),
213
AccessKey: cmd.String("s3-access-key"),
214
SecretKey: cmd.String("s3-secret-key"),
215
+
CDNUrl: cmd.String("s3-cdn-url"),
216
},
217
SessionSecret: cmd.String("session-secret"),
218
BlockstoreVariant: server.MustReturnBlockstoreVariant(cmd.String("blockstore-variant")),
···
313
},
314
},
315
Action: func(cmd *cli.Context) error {
316
+
db, err := newDb(cmd)
317
if err != nil {
318
return err
319
}
···
352
},
353
},
354
Action: func(cmd *cli.Context) error {
355
+
db, err := newDb(cmd)
356
if err != nil {
357
return err
358
}
···
379
},
380
}
381
382
+
func newDb(cmd *cli.Context) (*gorm.DB, error) {
383
+
dbType := cmd.String("db-type")
384
+
if dbType == "" {
385
+
dbType = "sqlite"
386
+
}
387
+
388
+
switch dbType {
389
+
case "postgres":
390
+
databaseURL := cmd.String("database-url")
391
+
if databaseURL == "" {
392
+
databaseURL = cmd.String("database-url")
393
+
}
394
+
if databaseURL == "" {
395
+
return nil, fmt.Errorf("COCOON_DATABASE_URL or DATABASE_URL must be set when using postgres")
396
+
}
397
+
return gorm.Open(postgres.Open(databaseURL), &gorm.Config{})
398
+
default:
399
+
dbName := cmd.String("db-name")
400
+
if dbName == "" {
401
+
dbName = "cocoon.db"
402
+
}
403
+
return gorm.Open(sqlite.Open(dbName), &gorm.Config{})
404
+
}
405
}
+158
docker-compose.postgres.yaml
+158
docker-compose.postgres.yaml
···
···
1
+
# Docker Compose with PostgreSQL
2
+
#
3
+
# Usage:
4
+
# docker-compose -f docker-compose.postgres.yaml up -d
5
+
#
6
+
# This file extends the base docker-compose.yaml with a PostgreSQL database.
7
+
# Set the following in your .env file:
8
+
# COCOON_DB_TYPE=postgres
9
+
# POSTGRES_PASSWORD=your-secure-password
10
+
11
+
version: '3.8'
12
+
13
+
services:
14
+
postgres:
15
+
image: postgres:16-alpine
16
+
container_name: cocoon-postgres
17
+
environment:
18
+
POSTGRES_USER: cocoon
19
+
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}
20
+
POSTGRES_DB: cocoon
21
+
volumes:
22
+
- postgres_data:/var/lib/postgresql/data
23
+
healthcheck:
24
+
test: ["CMD-SHELL", "pg_isready -U cocoon -d cocoon"]
25
+
interval: 10s
26
+
timeout: 5s
27
+
retries: 5
28
+
restart: unless-stopped
29
+
30
+
init-keys:
31
+
build:
32
+
context: .
33
+
dockerfile: Dockerfile
34
+
image: ghcr.io/haileyok/cocoon:latest
35
+
container_name: cocoon-init-keys
36
+
volumes:
37
+
- ./keys:/keys
38
+
- ./data:/data/cocoon
39
+
- ./init-keys.sh:/init-keys.sh:ro
40
+
environment:
41
+
COCOON_DID: ${COCOON_DID}
42
+
COCOON_HOSTNAME: ${COCOON_HOSTNAME}
43
+
COCOON_ROTATION_KEY_PATH: /keys/rotation.key
44
+
COCOON_JWK_PATH: /keys/jwk.key
45
+
COCOON_CONTACT_EMAIL: ${COCOON_CONTACT_EMAIL}
46
+
COCOON_RELAYS: ${COCOON_RELAYS:-https://bsky.network}
47
+
COCOON_ADMIN_PASSWORD: ${COCOON_ADMIN_PASSWORD}
48
+
entrypoint: ["/bin/sh", "/init-keys.sh"]
49
+
restart: "no"
50
+
51
+
cocoon:
52
+
build:
53
+
context: .
54
+
dockerfile: Dockerfile
55
+
image: ghcr.io/haileyok/cocoon:latest
56
+
container_name: cocoon-pds
57
+
depends_on:
58
+
init-keys:
59
+
condition: service_completed_successfully
60
+
postgres:
61
+
condition: service_healthy
62
+
ports:
63
+
- "8080:8080"
64
+
volumes:
65
+
- ./data:/data/cocoon
66
+
- ./keys/rotation.key:/keys/rotation.key:ro
67
+
- ./keys/jwk.key:/keys/jwk.key:ro
68
+
environment:
69
+
# Required settings
70
+
COCOON_DID: ${COCOON_DID}
71
+
COCOON_HOSTNAME: ${COCOON_HOSTNAME}
72
+
COCOON_ROTATION_KEY_PATH: /keys/rotation.key
73
+
COCOON_JWK_PATH: /keys/jwk.key
74
+
COCOON_CONTACT_EMAIL: ${COCOON_CONTACT_EMAIL}
75
+
COCOON_RELAYS: ${COCOON_RELAYS:-https://bsky.network}
76
+
COCOON_ADMIN_PASSWORD: ${COCOON_ADMIN_PASSWORD}
77
+
COCOON_SESSION_SECRET: ${COCOON_SESSION_SECRET}
78
+
79
+
# Database configuration - PostgreSQL
80
+
COCOON_ADDR: ":8080"
81
+
COCOON_DB_TYPE: postgres
82
+
COCOON_DATABASE_URL: postgres://cocoon:${POSTGRES_PASSWORD}@postgres:5432/cocoon?sslmode=disable
83
+
COCOON_BLOCKSTORE_VARIANT: ${COCOON_BLOCKSTORE_VARIANT:-sqlite}
84
+
85
+
# Optional: SMTP settings for email
86
+
COCOON_SMTP_USER: ${COCOON_SMTP_USER:-}
87
+
COCOON_SMTP_PASS: ${COCOON_SMTP_PASS:-}
88
+
COCOON_SMTP_HOST: ${COCOON_SMTP_HOST:-}
89
+
COCOON_SMTP_PORT: ${COCOON_SMTP_PORT:-}
90
+
COCOON_SMTP_EMAIL: ${COCOON_SMTP_EMAIL:-}
91
+
COCOON_SMTP_NAME: ${COCOON_SMTP_NAME:-}
92
+
93
+
# Optional: S3 configuration
94
+
COCOON_S3_BACKUPS_ENABLED: ${COCOON_S3_BACKUPS_ENABLED:-false}
95
+
COCOON_S3_BLOBSTORE_ENABLED: ${COCOON_S3_BLOBSTORE_ENABLED:-false}
96
+
COCOON_S3_REGION: ${COCOON_S3_REGION:-}
97
+
COCOON_S3_BUCKET: ${COCOON_S3_BUCKET:-}
98
+
COCOON_S3_ENDPOINT: ${COCOON_S3_ENDPOINT:-}
99
+
COCOON_S3_ACCESS_KEY: ${COCOON_S3_ACCESS_KEY:-}
100
+
COCOON_S3_SECRET_KEY: ${COCOON_S3_SECRET_KEY:-}
101
+
102
+
# Optional: Fallback proxy
103
+
COCOON_FALLBACK_PROXY: ${COCOON_FALLBACK_PROXY:-}
104
+
restart: unless-stopped
105
+
healthcheck:
106
+
test: ["CMD", "curl", "-f", "http://localhost:8080/xrpc/_health"]
107
+
interval: 30s
108
+
timeout: 10s
109
+
retries: 3
110
+
start_period: 40s
111
+
112
+
create-invite:
113
+
build:
114
+
context: .
115
+
dockerfile: Dockerfile
116
+
image: ghcr.io/haileyok/cocoon:latest
117
+
container_name: cocoon-create-invite
118
+
volumes:
119
+
- ./keys:/keys
120
+
- ./create-initial-invite.sh:/create-initial-invite.sh:ro
121
+
environment:
122
+
COCOON_DID: ${COCOON_DID}
123
+
COCOON_HOSTNAME: ${COCOON_HOSTNAME}
124
+
COCOON_ROTATION_KEY_PATH: /keys/rotation.key
125
+
COCOON_JWK_PATH: /keys/jwk.key
126
+
COCOON_CONTACT_EMAIL: ${COCOON_CONTACT_EMAIL}
127
+
COCOON_RELAYS: ${COCOON_RELAYS:-https://bsky.network}
128
+
COCOON_ADMIN_PASSWORD: ${COCOON_ADMIN_PASSWORD}
129
+
COCOON_DB_TYPE: postgres
130
+
COCOON_DATABASE_URL: postgres://cocoon:${POSTGRES_PASSWORD}@postgres:5432/cocoon?sslmode=disable
131
+
depends_on:
132
+
cocoon:
133
+
condition: service_healthy
134
+
entrypoint: ["/bin/sh", "/create-initial-invite.sh"]
135
+
restart: "no"
136
+
137
+
caddy:
138
+
image: caddy:2-alpine
139
+
container_name: cocoon-caddy
140
+
ports:
141
+
- "80:80"
142
+
- "443:443"
143
+
volumes:
144
+
- ./Caddyfile.postgres:/etc/caddy/Caddyfile:ro
145
+
- caddy_data:/data
146
+
- caddy_config:/config
147
+
restart: unless-stopped
148
+
environment:
149
+
COCOON_HOSTNAME: ${COCOON_HOSTNAME}
150
+
CADDY_ACME_EMAIL: ${COCOON_CONTACT_EMAIL:-}
151
+
152
+
volumes:
153
+
postgres_data:
154
+
driver: local
155
+
caddy_data:
156
+
driver: local
157
+
caddy_config:
158
+
driver: local
+7
-2
docker-compose.yaml
+7
-2
docker-compose.yaml
···
49
50
# Server configuration
51
COCOON_ADDR: ":8080"
52
-
COCOON_DB_NAME: /data/cocoon/cocoon.db
53
COCOON_BLOCKSTORE_VARIANT: ${COCOON_BLOCKSTORE_VARIANT:-sqlite}
54
55
# Optional: SMTP settings for email
···
68
COCOON_S3_ENDPOINT: ${COCOON_S3_ENDPOINT:-}
69
COCOON_S3_ACCESS_KEY: ${COCOON_S3_ACCESS_KEY:-}
70
COCOON_S3_SECRET_KEY: ${COCOON_S3_SECRET_KEY:-}
71
72
# Optional: Fallback proxy
73
COCOON_FALLBACK_PROXY: ${COCOON_FALLBACK_PROXY:-}
···
97
COCOON_CONTACT_EMAIL: ${COCOON_CONTACT_EMAIL}
98
COCOON_RELAYS: ${COCOON_RELAYS:-https://bsky.network}
99
COCOON_ADMIN_PASSWORD: ${COCOON_ADMIN_PASSWORD}
100
-
COCOON_DB_NAME: /data/cocoon/cocoon.db
101
depends_on:
102
- init-keys
103
entrypoint: ["/bin/sh", "/create-initial-invite.sh"]
···
49
50
# Server configuration
51
COCOON_ADDR: ":8080"
52
+
COCOON_DB_TYPE: ${COCOON_DB_TYPE:-sqlite}
53
+
COCOON_DB_NAME: ${COCOON_DB_NAME:-/data/cocoon/cocoon.db}
54
+
COCOON_DATABASE_URL: ${COCOON_DATABASE_URL:-}
55
COCOON_BLOCKSTORE_VARIANT: ${COCOON_BLOCKSTORE_VARIANT:-sqlite}
56
57
# Optional: SMTP settings for email
···
70
COCOON_S3_ENDPOINT: ${COCOON_S3_ENDPOINT:-}
71
COCOON_S3_ACCESS_KEY: ${COCOON_S3_ACCESS_KEY:-}
72
COCOON_S3_SECRET_KEY: ${COCOON_S3_SECRET_KEY:-}
73
+
COCOON_S3_CDN_URL: ${COCOON_S3_CDN_URL:-}
74
75
# Optional: Fallback proxy
76
COCOON_FALLBACK_PROXY: ${COCOON_FALLBACK_PROXY:-}
···
100
COCOON_CONTACT_EMAIL: ${COCOON_CONTACT_EMAIL}
101
COCOON_RELAYS: ${COCOON_RELAYS:-https://bsky.network}
102
COCOON_ADMIN_PASSWORD: ${COCOON_ADMIN_PASSWORD}
103
+
COCOON_DB_TYPE: ${COCOON_DB_TYPE:-sqlite}
104
+
COCOON_DB_NAME: ${COCOON_DB_NAME:-/data/cocoon/cocoon.db}
105
+
COCOON_DATABASE_URL: ${COCOON_DATABASE_URL:-}
106
depends_on:
107
- init-keys
108
entrypoint: ["/bin/sh", "/create-initial-invite.sh"]
+19
-17
go.mod
+19
-17
go.mod
···
1
module github.com/haileyok/cocoon
2
3
-
go 1.24.1
4
5
require (
6
github.com/Azure/go-autorest/autorest/to v0.4.1
7
github.com/aws/aws-sdk-go v1.55.7
8
github.com/bluesky-social/indigo v0.0.0-20251009212240-20524de167fe
9
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792
10
github.com/domodwyer/mailyak/v3 v3.6.2
11
github.com/go-pkgz/expirable-cache/v3 v3.0.0
12
github.com/go-playground/validator v9.31.0+incompatible
13
github.com/golang-jwt/jwt/v4 v4.5.2
14
-
github.com/google/uuid v1.4.0
15
github.com/gorilla/sessions v1.4.0
16
github.com/gorilla/websocket v1.5.1
17
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b
···
24
github.com/joho/godotenv v1.5.1
25
github.com/labstack/echo-contrib v0.17.4
26
github.com/labstack/echo/v4 v4.13.3
27
-
github.com/lestrrat-go/jwx/v2 v2.0.12
28
github.com/multiformats/go-multihash v0.2.3
29
github.com/samber/slog-echo v1.16.1
30
github.com/urfave/cli/v2 v2.27.6
31
github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e
32
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b
33
-
golang.org/x/crypto v0.38.0
34
gorm.io/driver/sqlite v1.5.7
35
gorm.io/gorm v1.25.12
36
)
···
56
github.com/gorilla/securecookie v1.1.2 // indirect
57
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect
58
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
59
-
github.com/hashicorp/go-retryablehttp v0.7.5 // indirect
60
github.com/hashicorp/golang-lru v1.0.2 // indirect
61
github.com/ipfs/bbloom v0.0.4 // indirect
62
github.com/ipfs/go-blockservice v0.5.2 // indirect
···
76
github.com/ipld/go-ipld-prime v0.21.0 // indirect
77
github.com/jackc/pgpassfile v1.0.0 // indirect
78
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
79
-
github.com/jackc/pgx/v5 v5.5.0 // indirect
80
github.com/jackc/puddle/v2 v2.2.1 // indirect
81
github.com/jbenet/goprocess v0.1.4 // indirect
82
github.com/jinzhu/inflection v1.0.0 // indirect
···
85
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
86
github.com/labstack/gommon v0.4.2 // indirect
87
github.com/leodido/go-urn v1.4.0 // indirect
88
-
github.com/lestrrat-go/blackmagic v1.0.1 // indirect
89
github.com/lestrrat-go/httpcc v1.0.1 // indirect
90
-
github.com/lestrrat-go/httprc v1.0.4 // indirect
91
github.com/lestrrat-go/iter v1.0.2 // indirect
92
github.com/lestrrat-go/option v1.0.1 // indirect
93
github.com/mattn/go-colorable v0.1.14 // indirect
···
102
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
103
github.com/opentracing/opentracing-go v1.2.0 // indirect
104
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f // indirect
105
-
github.com/prometheus/client_golang v1.22.0 // indirect
106
github.com/prometheus/client_model v0.6.2 // indirect
107
-
github.com/prometheus/common v0.63.0 // indirect
108
github.com/prometheus/procfs v0.16.1 // indirect
109
github.com/russross/blackfriday/v2 v2.1.0 // indirect
110
github.com/samber/lo v1.49.1 // indirect
···
114
github.com/valyala/fasttemplate v1.2.2 // indirect
115
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
116
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect
117
-
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect
118
go.opentelemetry.io/otel v1.29.0 // indirect
119
go.opentelemetry.io/otel/metric v1.29.0 // indirect
120
go.opentelemetry.io/otel/trace v1.29.0 // indirect
121
go.uber.org/atomic v1.11.0 // indirect
122
go.uber.org/multierr v1.11.0 // indirect
123
go.uber.org/zap v1.26.0 // indirect
124
-
golang.org/x/net v0.40.0 // indirect
125
-
golang.org/x/sync v0.14.0 // indirect
126
-
golang.org/x/sys v0.33.0 // indirect
127
-
golang.org/x/text v0.25.0 // indirect
128
golang.org/x/time v0.11.0 // indirect
129
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
130
-
google.golang.org/protobuf v1.36.6 // indirect
131
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
132
gopkg.in/inf.v0 v0.9.1 // indirect
133
-
gorm.io/driver/postgres v1.5.7 // indirect
134
lukechampine.com/blake3 v1.2.1 // indirect
135
)
···
1
module github.com/haileyok/cocoon
2
3
+
go 1.24.5
4
5
require (
6
github.com/Azure/go-autorest/autorest/to v0.4.1
7
github.com/aws/aws-sdk-go v1.55.7
8
+
github.com/bluesky-social/go-util v0.0.0-20251012040650-2ebbf57f5934
9
github.com/bluesky-social/indigo v0.0.0-20251009212240-20524de167fe
10
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792
11
github.com/domodwyer/mailyak/v3 v3.6.2
12
github.com/go-pkgz/expirable-cache/v3 v3.0.0
13
github.com/go-playground/validator v9.31.0+incompatible
14
github.com/golang-jwt/jwt/v4 v4.5.2
15
+
github.com/google/uuid v1.6.0
16
github.com/gorilla/sessions v1.4.0
17
github.com/gorilla/websocket v1.5.1
18
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b
···
25
github.com/joho/godotenv v1.5.1
26
github.com/labstack/echo-contrib v0.17.4
27
github.com/labstack/echo/v4 v4.13.3
28
+
github.com/lestrrat-go/jwx/v2 v2.0.21
29
github.com/multiformats/go-multihash v0.2.3
30
+
github.com/prometheus/client_golang v1.23.2
31
github.com/samber/slog-echo v1.16.1
32
github.com/urfave/cli/v2 v2.27.6
33
github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e
34
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b
35
+
golang.org/x/crypto v0.41.0
36
+
gorm.io/driver/postgres v1.5.7
37
gorm.io/driver/sqlite v1.5.7
38
gorm.io/gorm v1.25.12
39
)
···
59
github.com/gorilla/securecookie v1.1.2 // indirect
60
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect
61
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
62
+
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
63
github.com/hashicorp/golang-lru v1.0.2 // indirect
64
github.com/ipfs/bbloom v0.0.4 // indirect
65
github.com/ipfs/go-blockservice v0.5.2 // indirect
···
79
github.com/ipld/go-ipld-prime v0.21.0 // indirect
80
github.com/jackc/pgpassfile v1.0.0 // indirect
81
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
82
+
github.com/jackc/pgx/v5 v5.5.4 // indirect
83
github.com/jackc/puddle/v2 v2.2.1 // indirect
84
github.com/jbenet/goprocess v0.1.4 // indirect
85
github.com/jinzhu/inflection v1.0.0 // indirect
···
88
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
89
github.com/labstack/gommon v0.4.2 // indirect
90
github.com/leodido/go-urn v1.4.0 // indirect
91
+
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
92
github.com/lestrrat-go/httpcc v1.0.1 // indirect
93
+
github.com/lestrrat-go/httprc v1.0.5 // indirect
94
github.com/lestrrat-go/iter v1.0.2 // indirect
95
github.com/lestrrat-go/option v1.0.1 // indirect
96
github.com/mattn/go-colorable v0.1.14 // indirect
···
105
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
106
github.com/opentracing/opentracing-go v1.2.0 // indirect
107
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f // indirect
108
github.com/prometheus/client_model v0.6.2 // indirect
109
+
github.com/prometheus/common v0.66.1 // indirect
110
github.com/prometheus/procfs v0.16.1 // indirect
111
github.com/russross/blackfriday/v2 v2.1.0 // indirect
112
github.com/samber/lo v1.49.1 // indirect
···
116
github.com/valyala/fasttemplate v1.2.2 // indirect
117
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
118
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect
119
+
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
120
go.opentelemetry.io/otel v1.29.0 // indirect
121
go.opentelemetry.io/otel/metric v1.29.0 // indirect
122
go.opentelemetry.io/otel/trace v1.29.0 // indirect
123
go.uber.org/atomic v1.11.0 // indirect
124
go.uber.org/multierr v1.11.0 // indirect
125
go.uber.org/zap v1.26.0 // indirect
126
+
go.yaml.in/yaml/v2 v2.4.2 // indirect
127
+
golang.org/x/net v0.43.0 // indirect
128
+
golang.org/x/sync v0.16.0 // indirect
129
+
golang.org/x/sys v0.35.0 // indirect
130
+
golang.org/x/text v0.28.0 // indirect
131
golang.org/x/time v0.11.0 // indirect
132
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
133
+
google.golang.org/protobuf v1.36.9 // indirect
134
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
135
gopkg.in/inf.v0 v0.9.1 // indirect
136
lukechampine.com/blake3 v1.2.1 // indirect
137
)
+50
-74
go.sum
+50
-74
go.sum
···
16
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
17
github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY=
18
github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k=
19
github.com/bluesky-social/indigo v0.0.0-20251009212240-20524de167fe h1:VBhaqE5ewQgXbY5SfSWFZC/AwHFo7cHxZKFYi2ce9Yo=
20
github.com/bluesky-social/indigo v0.0.0-20251009212240-20524de167fe/go.mod h1:RuQVrCGm42QNsgumKaR6se+XkFKfCPNwdCiTvqKRUck=
21
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
···
34
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
35
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
36
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
37
-
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
38
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
39
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
40
github.com/domodwyer/mailyak/v3 v3.6.2 h1:x3tGMsyFhTCaxp6ycgR0FE/bu5QiNp+hetUuCOBXMn8=
41
github.com/domodwyer/mailyak/v3 v3.6.2/go.mod h1:lOm/u9CyCVWHeaAmHIdF4RiKVxKUT/H5XX10lIKAL6c=
42
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
43
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
44
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
···
77
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
78
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
79
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
80
-
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
81
-
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
82
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
83
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
84
github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o=
···
95
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b/go.mod h1:VzxiSdG6j1pi7rwGm/xYI5RbtpBgM8sARDXlvEvxlu0=
96
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
97
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
98
-
github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
99
-
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
100
-
github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M=
101
-
github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
102
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
103
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
104
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
···
172
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
173
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
174
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
175
-
github.com/jackc/pgx/v5 v5.5.0 h1:NxstgwndsTRy7eq9/kqYc/BZh5w2hHJV86wjvO+1xPw=
176
-
github.com/jackc/pgx/v5 v5.5.0/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
177
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
178
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
179
github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
···
195
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
196
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
197
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
198
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
199
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
200
github.com/koron/go-ssdp v0.0.3 h1:JivLMY45N76b4p/vsWGOKewBQu6uf39y8l+AQ7sDKx8=
···
206
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
207
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
208
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
209
github.com/labstack/echo-contrib v0.17.4 h1:g5mfsrJfJTKv+F5uNKCyrjLK7js+ZW6HTjg4FnDxxgk=
210
github.com/labstack/echo-contrib v0.17.4/go.mod h1:9O7ZPAHUeMGTOAfg80YqQduHzt0CzLak36PZRldYrZ0=
211
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
···
214
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
215
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
216
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
217
-
github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80=
218
-
github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
219
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
220
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
221
-
github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJGdI8=
222
-
github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
223
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
224
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
225
-
github.com/lestrrat-go/jwx/v2 v2.0.12 h1:3d589+5w/b9b7S3DneICPW16AqTyYXB7VRjgluSDWeA=
226
-
github.com/lestrrat-go/jwx/v2 v2.0.12/go.mod h1:Mq4KN1mM7bp+5z/W5HS8aCNs5RKZ911G/0y2qUjAQuQ=
227
-
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
228
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
229
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
230
github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8=
···
289
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
290
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f h1:VXTQfuJj9vKR4TCkEuWIckKvdHFeJH/huIFJ9/cXOB0=
291
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw=
292
-
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
293
-
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
294
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
295
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
296
-
github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k=
297
-
github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18=
298
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
299
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
300
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
···
317
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
318
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
319
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
320
-
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
321
-
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
322
-
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
323
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
324
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
325
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
326
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
327
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
328
-
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
329
-
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
330
-
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
331
-
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
332
github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
333
github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g=
334
github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
···
349
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
350
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
351
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
352
-
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
353
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b h1:CzigHMRySiX3drau9C6Q5CAbNIApmLdat5jPMqChvDA=
354
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b/go.mod h1:/y/V339mxv2sZmYYR64O07VuCpdNZqCTwO8ZcouTMI8=
355
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 h1:qwDnMxjkyLmAFgcfgTnfJrmYKWhHnci3GjDqcZp1M3Q=
356
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02/go.mod h1:JTnUj0mpYiAsuZLmKjTx/ex3AtMowcCgnE7YNyCEP0I=
357
-
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24=
358
-
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo=
359
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
360
go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
361
go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc=
···
367
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
368
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
369
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
370
-
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
371
-
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
372
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
373
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
374
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
···
378
go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
379
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
380
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
381
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
382
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
383
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
384
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
385
-
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
386
-
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
387
-
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
388
-
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
389
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ=
390
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE=
391
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
···
393
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
394
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
395
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
396
-
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
397
-
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
398
-
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
399
-
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
400
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
401
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
402
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
403
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
404
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
405
-
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
406
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
407
-
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
408
-
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
409
-
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
410
-
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
411
-
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
412
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
413
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
414
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
415
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
416
-
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
417
-
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
418
-
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
419
-
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
420
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
421
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
422
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
423
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
424
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
425
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
426
-
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
427
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
428
-
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
429
-
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
430
-
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
431
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
432
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
433
-
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
434
-
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
435
-
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
436
-
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
437
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
438
-
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
439
-
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
440
-
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
441
-
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
442
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
443
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
444
-
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
445
-
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
446
-
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
447
-
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
448
-
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
449
-
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
450
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
451
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
452
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
···
459
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
460
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
461
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
462
-
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
463
-
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
464
-
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
465
-
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
466
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
467
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
468
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
469
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
470
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
471
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
472
-
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
473
-
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
474
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
475
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
476
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
···
16
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
17
github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY=
18
github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k=
19
+
github.com/bluesky-social/go-util v0.0.0-20251012040650-2ebbf57f5934 h1:btHMur2kTRgWEnCHn6LaI3BE9YRgsqTpwpJ1UdB7VEk=
20
+
github.com/bluesky-social/go-util v0.0.0-20251012040650-2ebbf57f5934/go.mod h1:LWamyZfbQGW7PaVc5jumFfjgrshJ5mXgDUnR6fK7+BI=
21
github.com/bluesky-social/indigo v0.0.0-20251009212240-20524de167fe h1:VBhaqE5ewQgXbY5SfSWFZC/AwHFo7cHxZKFYi2ce9Yo=
22
github.com/bluesky-social/indigo v0.0.0-20251009212240-20524de167fe/go.mod h1:RuQVrCGm42QNsgumKaR6se+XkFKfCPNwdCiTvqKRUck=
23
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
···
36
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
37
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
38
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
39
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
40
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
41
github.com/domodwyer/mailyak/v3 v3.6.2 h1:x3tGMsyFhTCaxp6ycgR0FE/bu5QiNp+hetUuCOBXMn8=
42
github.com/domodwyer/mailyak/v3 v3.6.2/go.mod h1:lOm/u9CyCVWHeaAmHIdF4RiKVxKUT/H5XX10lIKAL6c=
43
+
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
44
+
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
45
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
46
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
47
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
···
80
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
81
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
82
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
83
+
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
84
+
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
85
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
86
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
87
github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o=
···
98
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b/go.mod h1:VzxiSdG6j1pi7rwGm/xYI5RbtpBgM8sARDXlvEvxlu0=
99
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
100
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
101
+
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
102
+
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
103
+
github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
104
+
github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
105
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
106
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
107
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
···
175
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
176
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
177
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
178
+
github.com/jackc/pgx/v5 v5.5.4 h1:Xp2aQS8uXButQdnCMWNmvx6UysWQQC+u1EoizjguY+8=
179
+
github.com/jackc/pgx/v5 v5.5.4/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
180
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
181
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
182
github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
···
198
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
199
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
200
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
201
+
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
202
+
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
203
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
204
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
205
github.com/koron/go-ssdp v0.0.3 h1:JivLMY45N76b4p/vsWGOKewBQu6uf39y8l+AQ7sDKx8=
···
211
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
212
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
213
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
214
+
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
215
+
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
216
github.com/labstack/echo-contrib v0.17.4 h1:g5mfsrJfJTKv+F5uNKCyrjLK7js+ZW6HTjg4FnDxxgk=
217
github.com/labstack/echo-contrib v0.17.4/go.mod h1:9O7ZPAHUeMGTOAfg80YqQduHzt0CzLak36PZRldYrZ0=
218
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
···
221
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
222
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
223
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
224
+
github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k=
225
+
github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
226
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
227
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
228
+
github.com/lestrrat-go/httprc v1.0.5 h1:bsTfiH8xaKOJPrg1R+E3iE/AWZr/x0Phj9PBTG/OLUk=
229
+
github.com/lestrrat-go/httprc v1.0.5/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
230
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
231
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
232
+
github.com/lestrrat-go/jwx/v2 v2.0.21 h1:jAPKupy4uHgrHFEdjVjNkUgoBKtVDgrQPB/h55FHrR0=
233
+
github.com/lestrrat-go/jwx/v2 v2.0.21/go.mod h1:09mLW8zto6bWL9GbwnqAli+ArLf+5M33QLQPDggkUWM=
234
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
235
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
236
github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8=
···
295
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
296
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f h1:VXTQfuJj9vKR4TCkEuWIckKvdHFeJH/huIFJ9/cXOB0=
297
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw=
298
+
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
299
+
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
300
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
301
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
302
+
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
303
+
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
304
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
305
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
306
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
···
323
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
324
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
325
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
326
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
327
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
328
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
329
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
330
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
331
+
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
332
+
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
333
github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
334
github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g=
335
github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
···
350
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
351
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
352
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
353
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b h1:CzigHMRySiX3drau9C6Q5CAbNIApmLdat5jPMqChvDA=
354
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b/go.mod h1:/y/V339mxv2sZmYYR64O07VuCpdNZqCTwO8ZcouTMI8=
355
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 h1:qwDnMxjkyLmAFgcfgTnfJrmYKWhHnci3GjDqcZp1M3Q=
356
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02/go.mod h1:JTnUj0mpYiAsuZLmKjTx/ex3AtMowcCgnE7YNyCEP0I=
357
+
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
358
+
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
359
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
360
go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
361
go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc=
···
367
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
368
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
369
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
370
+
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
371
+
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
372
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
373
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
374
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
···
378
go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
379
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
380
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
381
+
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
382
+
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
383
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
384
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
385
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
386
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
387
+
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
388
+
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
389
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ=
390
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE=
391
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
···
393
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
394
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
395
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
396
+
golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
397
+
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
398
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
399
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
400
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
401
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
402
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
403
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
404
+
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
405
+
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
406
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
407
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
408
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
409
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
410
+
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
411
+
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
412
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
413
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
414
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
415
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
416
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
417
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
418
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
419
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
420
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
421
+
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
422
+
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
423
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
424
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
425
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
426
+
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
427
+
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
428
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
429
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
430
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
···
437
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
438
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
439
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
440
+
golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
441
+
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
442
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
443
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
444
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
445
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
446
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
447
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
448
+
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
449
+
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
450
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
451
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
452
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+15
-14
internal/db/db.go
+15
-14
internal/db/db.go
···
1
package db
2
3
import (
4
"sync"
5
6
"gorm.io/gorm"
···
19
}
20
}
21
22
-
func (db *DB) Create(value any, clauses []clause.Expression) *gorm.DB {
23
db.mu.Lock()
24
defer db.mu.Unlock()
25
-
return db.cli.Clauses(clauses...).Create(value)
26
}
27
28
-
func (db *DB) Save(value any, clauses []clause.Expression) *gorm.DB {
29
db.mu.Lock()
30
defer db.mu.Unlock()
31
-
return db.cli.Clauses(clauses...).Save(value)
32
}
33
34
-
func (db *DB) Exec(sql string, clauses []clause.Expression, values ...any) *gorm.DB {
35
db.mu.Lock()
36
defer db.mu.Unlock()
37
-
return db.cli.Clauses(clauses...).Exec(sql, values...)
38
}
39
40
-
func (db *DB) Raw(sql string, clauses []clause.Expression, values ...any) *gorm.DB {
41
-
return db.cli.Clauses(clauses...).Raw(sql, values...)
42
}
43
44
func (db *DB) AutoMigrate(models ...any) error {
45
return db.cli.AutoMigrate(models...)
46
}
47
48
-
func (db *DB) Delete(value any, clauses []clause.Expression) *gorm.DB {
49
db.mu.Lock()
50
defer db.mu.Unlock()
51
-
return db.cli.Clauses(clauses...).Delete(value)
52
}
53
54
-
func (db *DB) First(dest any, conds ...any) *gorm.DB {
55
-
return db.cli.First(dest, conds...)
56
}
57
58
// TODO: this isn't actually good. we can commit even if the db is locked here. this is probably okay for the time being, but need to figure
59
// out a better solution. right now we only do this whenever we're importing a repo though so i'm mostly not worried, but it's still bad.
60
// e.g. when we do apply writes we should also be using a transcation but we don't right now
61
-
func (db *DB) BeginDangerously() *gorm.DB {
62
-
return db.cli.Begin()
63
}
64
65
func (db *DB) Lock() {
···
1
package db
2
3
import (
4
+
"context"
5
"sync"
6
7
"gorm.io/gorm"
···
20
}
21
}
22
23
+
func (db *DB) Create(ctx context.Context, value any, clauses []clause.Expression) *gorm.DB {
24
db.mu.Lock()
25
defer db.mu.Unlock()
26
+
return db.cli.WithContext(ctx).Clauses(clauses...).Create(value)
27
}
28
29
+
func (db *DB) Save(ctx context.Context, value any, clauses []clause.Expression) *gorm.DB {
30
db.mu.Lock()
31
defer db.mu.Unlock()
32
+
return db.cli.WithContext(ctx).Clauses(clauses...).Save(value)
33
}
34
35
+
func (db *DB) Exec(ctx context.Context, sql string, clauses []clause.Expression, values ...any) *gorm.DB {
36
db.mu.Lock()
37
defer db.mu.Unlock()
38
+
return db.cli.WithContext(ctx).Clauses(clauses...).Exec(sql, values...)
39
}
40
41
+
func (db *DB) Raw(ctx context.Context, sql string, clauses []clause.Expression, values ...any) *gorm.DB {
42
+
return db.cli.WithContext(ctx).Clauses(clauses...).Raw(sql, values...)
43
}
44
45
func (db *DB) AutoMigrate(models ...any) error {
46
return db.cli.AutoMigrate(models...)
47
}
48
49
+
func (db *DB) Delete(ctx context.Context, value any, clauses []clause.Expression) *gorm.DB {
50
db.mu.Lock()
51
defer db.mu.Unlock()
52
+
return db.cli.WithContext(ctx).Clauses(clauses...).Delete(value)
53
}
54
55
+
func (db *DB) First(ctx context.Context, dest any, conds ...any) *gorm.DB {
56
+
return db.cli.WithContext(ctx).First(dest, conds...)
57
}
58
59
// TODO: this isn't actually good. we can commit even if the db is locked here. this is probably okay for the time being, but need to figure
60
// out a better solution. right now we only do this whenever we're importing a repo though so i'm mostly not worried, but it's still bad.
61
// e.g. when we do apply writes we should also be using a transcation but we don't right now
62
+
func (db *DB) BeginDangerously(ctx context.Context) *gorm.DB {
63
+
return db.cli.WithContext(ctx).Begin()
64
}
65
66
func (db *DB) Lock() {
+30
metrics/metrics.go
+30
metrics/metrics.go
···
···
1
+
package metrics
2
+
3
+
import (
4
+
"github.com/prometheus/client_golang/prometheus"
5
+
"github.com/prometheus/client_golang/prometheus/promauto"
6
+
)
7
+
8
+
const (
9
+
NAMESPACE = "cocoon"
10
+
)
11
+
12
+
var (
13
+
RelaysConnected = promauto.NewGaugeVec(prometheus.GaugeOpts{
14
+
Namespace: NAMESPACE,
15
+
Name: "relays_connected",
16
+
Help: "number of connected relays, by host",
17
+
}, []string{"host"})
18
+
19
+
RelaySends = promauto.NewCounterVec(prometheus.CounterOpts{
20
+
Namespace: NAMESPACE,
21
+
Name: "relay_sends",
22
+
Help: "number of events sent to a relay, by host",
23
+
}, []string{"host", "kind"})
24
+
25
+
RepoOperations = promauto.NewCounterVec(prometheus.CounterOpts{
26
+
Namespace: NAMESPACE,
27
+
Name: "repo_operations",
28
+
Help: "number of operations made against repos",
29
+
}, []string{"kind"})
30
+
)
+21
models/models.go
+21
models/models.go
···
8
"github.com/bluesky-social/indigo/atproto/atcrypto"
9
)
10
11
type Repo struct {
12
Did string `gorm:"primaryKey"`
13
CreatedAt time.Time
···
19
EmailUpdateCodeExpiresAt *time.Time
20
PasswordResetCode *string
21
PasswordResetCodeExpiresAt *time.Time
22
Password string
23
SigningKey []byte
24
Rev string
25
Root []byte
26
Preferences []byte
27
Deactivated bool
28
}
29
30
func (r *Repo) SignFor(ctx context.Context, did string, msg []byte) ([]byte, error) {
···
115
Idx int `gorm:"primaryKey"`
116
Data []byte
117
}
···
8
"github.com/bluesky-social/indigo/atproto/atcrypto"
9
)
10
11
+
type TwoFactorType string
12
+
13
+
var (
14
+
TwoFactorTypeNone = TwoFactorType("none")
15
+
TwoFactorTypeEmail = TwoFactorType("email")
16
+
)
17
+
18
type Repo struct {
19
Did string `gorm:"primaryKey"`
20
CreatedAt time.Time
···
26
EmailUpdateCodeExpiresAt *time.Time
27
PasswordResetCode *string
28
PasswordResetCodeExpiresAt *time.Time
29
+
PlcOperationCode *string
30
+
PlcOperationCodeExpiresAt *time.Time
31
+
AccountDeleteCode *string
32
+
AccountDeleteCodeExpiresAt *time.Time
33
Password string
34
SigningKey []byte
35
Rev string
36
Root []byte
37
Preferences []byte
38
Deactivated bool
39
+
TwoFactorCode *string
40
+
TwoFactorCodeExpiresAt *time.Time
41
+
TwoFactorType TwoFactorType `gorm:"default:none"`
42
}
43
44
func (r *Repo) SignFor(ctx context.Context, did string, msg []byte) ([]byte, error) {
···
129
Idx int `gorm:"primaryKey"`
130
Data []byte
131
}
132
+
133
+
type ReservedKey struct {
134
+
KeyDid string `gorm:"primaryKey"`
135
+
Did *string `gorm:"index"`
136
+
PrivateKey []byte
137
+
CreatedAt time.Time `gorm:"index"`
138
+
}
+43
-28
oauth/client/manager.go
+43
-28
oauth/client/manager.go
···
22
cli *http.Client
23
logger *slog.Logger
24
jwksCache cache.Cache[string, jwk.Key]
25
-
metadataCache cache.Cache[string, Metadata]
26
}
27
28
type ManagerArgs struct {
···
40
}
41
42
jwksCache := cache.NewCache[string, jwk.Key]().WithLRU().WithMaxKeys(500).WithTTL(5 * time.Minute)
43
-
metadataCache := cache.NewCache[string, Metadata]().WithLRU().WithMaxKeys(500).WithTTL(5 * time.Minute)
44
45
return &Manager{
46
cli: args.Cli,
···
57
}
58
59
var jwks jwk.Key
60
-
if metadata.JWKS != nil && len(metadata.JWKS.Keys) > 0 {
61
-
// TODO: this is kinda bad but whatever for now. there could obviously be more than one jwk, and we need to
62
-
// make sure we use the right one
63
-
b, err := json.Marshal(metadata.JWKS.Keys[0])
64
-
if err != nil {
65
-
return nil, err
66
-
}
67
68
-
k, err := helpers.ParseJWKFromBytes(b)
69
-
if err != nil {
70
-
return nil, err
71
-
}
72
73
-
jwks = k
74
-
} else if metadata.JWKSURI != nil {
75
-
maybeJwks, err := cm.getClientJwks(ctx, clientId, *metadata.JWKSURI)
76
-
if err != nil {
77
-
return nil, err
78
}
79
-
80
-
jwks = maybeJwks
81
-
} else {
82
-
return nil, fmt.Errorf("no valid jwks found in oauth client metadata")
83
}
84
85
return &Client{
···
89
}
90
91
func (cm *Manager) getClientMetadata(ctx context.Context, clientId string) (*Metadata, error) {
92
-
metadataCached, ok := cm.metadataCache.Get(clientId)
93
if !ok {
94
req, err := http.NewRequestWithContext(ctx, "GET", clientId, nil)
95
if err != nil {
···
117
return nil, err
118
}
119
120
return validated, nil
121
} else {
122
-
return &metadataCached, nil
123
}
124
}
125
···
204
return nil, fmt.Errorf("error unmarshaling metadata: %w", err)
205
}
206
207
u, err := url.Parse(metadata.ClientURI)
208
if err != nil {
209
return nil, fmt.Errorf("unable to parse client uri: %w", err)
210
}
211
212
if isLocalHostname(u.Hostname()) {
213
-
return nil, errors.New("`client_uri` hostname is invalid")
214
}
215
216
if metadata.Scope == "" {
···
349
if u.Scheme != "http" {
350
return nil, fmt.Errorf("loopback redirect uri %s must use http", ruri)
351
}
352
-
353
-
break
354
case u.Scheme == "http":
355
return nil, errors.New("only loopbvack redirect uris are allowed to use the `http` scheme")
356
case u.Scheme == "https":
357
if isLocalHostname(u.Hostname()) {
358
return nil, fmt.Errorf("redirect uri %s's domain must not be a local hostname", ruri)
359
}
360
-
break
361
case strings.Contains(u.Scheme, "."):
362
if metadata.ApplicationType != "native" {
363
return nil, errors.New("private-use uri scheme redirect uris are only allowed for native apps")
···
22
cli *http.Client
23
logger *slog.Logger
24
jwksCache cache.Cache[string, jwk.Key]
25
+
metadataCache cache.Cache[string, *Metadata]
26
}
27
28
type ManagerArgs struct {
···
40
}
41
42
jwksCache := cache.NewCache[string, jwk.Key]().WithLRU().WithMaxKeys(500).WithTTL(5 * time.Minute)
43
+
metadataCache := cache.NewCache[string, *Metadata]().WithLRU().WithMaxKeys(500).WithTTL(5 * time.Minute)
44
45
return &Manager{
46
cli: args.Cli,
···
57
}
58
59
var jwks jwk.Key
60
+
if metadata.TokenEndpointAuthMethod == "private_key_jwt" {
61
+
if metadata.JWKS != nil && len(metadata.JWKS.Keys) > 0 {
62
+
// TODO: this is kinda bad but whatever for now. there could obviously be more than one jwk, and we need to
63
+
// make sure we use the right one
64
+
b, err := json.Marshal(metadata.JWKS.Keys[0])
65
+
if err != nil {
66
+
return nil, err
67
+
}
68
69
+
k, err := helpers.ParseJWKFromBytes(b)
70
+
if err != nil {
71
+
return nil, err
72
+
}
73
74
+
jwks = k
75
+
} else if metadata.JWKSURI != nil {
76
+
maybeJwks, err := cm.getClientJwks(ctx, clientId, *metadata.JWKSURI)
77
+
if err != nil {
78
+
return nil, err
79
+
}
80
+
81
+
jwks = maybeJwks
82
+
} else {
83
+
return nil, fmt.Errorf("no valid jwks found in oauth client metadata")
84
}
85
}
86
87
return &Client{
···
91
}
92
93
func (cm *Manager) getClientMetadata(ctx context.Context, clientId string) (*Metadata, error) {
94
+
cached, ok := cm.metadataCache.Get(clientId)
95
if !ok {
96
req, err := http.NewRequestWithContext(ctx, "GET", clientId, nil)
97
if err != nil {
···
119
return nil, err
120
}
121
122
+
cm.metadataCache.Set(clientId, validated, 10*time.Minute)
123
+
124
return validated, nil
125
} else {
126
+
return cached, nil
127
}
128
}
129
···
208
return nil, fmt.Errorf("error unmarshaling metadata: %w", err)
209
}
210
211
+
if metadata.ClientURI == "" {
212
+
u, err := url.Parse(metadata.ClientID)
213
+
if err != nil {
214
+
return nil, fmt.Errorf("unable to parse client id: %w", err)
215
+
}
216
+
u.RawPath = ""
217
+
u.RawQuery = ""
218
+
metadata.ClientURI = u.String()
219
+
}
220
+
221
u, err := url.Parse(metadata.ClientURI)
222
if err != nil {
223
return nil, fmt.Errorf("unable to parse client uri: %w", err)
224
}
225
226
+
if metadata.ClientName == "" {
227
+
metadata.ClientName = metadata.ClientURI
228
+
}
229
+
230
if isLocalHostname(u.Hostname()) {
231
+
return nil, fmt.Errorf("`client_uri` hostname is invalid: %s", u.Hostname())
232
}
233
234
if metadata.Scope == "" {
···
367
if u.Scheme != "http" {
368
return nil, fmt.Errorf("loopback redirect uri %s must use http", ruri)
369
}
370
case u.Scheme == "http":
371
return nil, errors.New("only loopbvack redirect uris are allowed to use the `http` scheme")
372
case u.Scheme == "https":
373
if isLocalHostname(u.Hostname()) {
374
return nil, fmt.Errorf("redirect uri %s's domain must not be a local hostname", ruri)
375
}
376
case strings.Contains(u.Scheme, "."):
377
if metadata.ApplicationType != "native" {
378
return nil, errors.New("private-use uri scheme redirect uris are only allowed for native apps")
+1
-1
oauth/dpop/jti_cache.go
+1
-1
oauth/dpop/jti_cache.go
+3
-2
oauth/dpop/nonce.go
+3
-2
oauth/dpop/nonce.go
+31
-15
plc/client.go
+31
-15
plc/client.go
···
55
}
56
57
func (c *Client) CreateDID(sigkey *atcrypto.PrivateKeyK256, recovery string, handle string) (string, *Operation, error) {
58
-
pubsigkey, err := sigkey.PublicKey()
59
if err != nil {
60
return "", nil, err
61
}
62
63
-
pubrotkey, err := c.rotationKey.PublicKey()
64
if err != nil {
65
return "", nil, err
66
}
67
68
// todo
69
rotationKeys := []string{pubrotkey.DIDKey()}
70
if recovery != "" {
···
77
}(recovery)
78
}
79
80
-
op := Operation{
81
-
Type: "plc_operation",
82
VerificationMethods: map[string]string{
83
"atproto": pubsigkey.DIDKey(),
84
},
···
92
Endpoint: "https://" + c.pdsHostname,
93
},
94
},
95
-
Prev: nil,
96
}
97
98
-
if err := c.SignOp(sigkey, &op); err != nil {
99
-
return "", nil, err
100
-
}
101
-
102
-
did, err := DidFromOp(&op)
103
-
if err != nil {
104
-
return "", nil, err
105
-
}
106
-
107
-
return did, &op, nil
108
}
109
110
func (c *Client) SignOp(sigkey *atcrypto.PrivateKeyK256, op *Operation) error {
···
55
}
56
57
func (c *Client) CreateDID(sigkey *atcrypto.PrivateKeyK256, recovery string, handle string) (string, *Operation, error) {
58
+
creds, err := c.CreateDidCredentials(sigkey, recovery, handle)
59
if err != nil {
60
return "", nil, err
61
}
62
63
+
op := Operation{
64
+
Type: "plc_operation",
65
+
VerificationMethods: creds.VerificationMethods,
66
+
RotationKeys: creds.RotationKeys,
67
+
AlsoKnownAs: creds.AlsoKnownAs,
68
+
Services: creds.Services,
69
+
Prev: nil,
70
+
}
71
+
72
+
if err := c.SignOp(sigkey, &op); err != nil {
73
+
return "", nil, err
74
+
}
75
+
76
+
did, err := DidFromOp(&op)
77
if err != nil {
78
return "", nil, err
79
}
80
81
+
return did, &op, nil
82
+
}
83
+
84
+
func (c *Client) CreateDidCredentials(sigkey *atcrypto.PrivateKeyK256, recovery string, handle string) (*DidCredentials, error) {
85
+
pubsigkey, err := sigkey.PublicKey()
86
+
if err != nil {
87
+
return nil, err
88
+
}
89
+
90
+
pubrotkey, err := c.rotationKey.PublicKey()
91
+
if err != nil {
92
+
return nil, err
93
+
}
94
+
95
// todo
96
rotationKeys := []string{pubrotkey.DIDKey()}
97
if recovery != "" {
···
104
}(recovery)
105
}
106
107
+
creds := DidCredentials{
108
VerificationMethods: map[string]string{
109
"atproto": pubsigkey.DIDKey(),
110
},
···
118
Endpoint: "https://" + c.pdsHostname,
119
},
120
},
121
}
122
123
+
return &creds, nil
124
}
125
126
func (c *Client) SignOp(sigkey *atcrypto.PrivateKeyK256, op *Operation) error {
+8
plc/types.go
+8
plc/types.go
···
8
cbg "github.com/whyrusleeping/cbor-gen"
9
)
10
11
+
12
+
type DidCredentials struct {
13
+
VerificationMethods map[string]string `json:"verificationMethods"`
14
+
RotationKeys []string `json:"rotationKeys"`
15
+
AlsoKnownAs []string `json:"alsoKnownAs"`
16
+
Services map[string]identity.OperationService `json:"services"`
17
+
}
18
+
19
type Operation struct {
20
Type string `json:"type"`
21
VerificationMethods map[string]string `json:"verificationMethods"`
+10
-8
server/common.go
+10
-8
server/common.go
···
1
package server
2
3
import (
4
"github.com/haileyok/cocoon/models"
5
)
6
7
-
func (s *Server) getActorByHandle(handle string) (*models.Actor, error) {
8
var actor models.Actor
9
-
if err := s.db.First(&actor, models.Actor{Handle: handle}).Error; err != nil {
10
return nil, err
11
}
12
return &actor, nil
13
}
14
15
-
func (s *Server) getRepoByEmail(email string) (*models.Repo, error) {
16
var repo models.Repo
17
-
if err := s.db.First(&repo, models.Repo{Email: email}).Error; err != nil {
18
return nil, err
19
}
20
return &repo, nil
21
}
22
23
-
func (s *Server) getRepoActorByEmail(email string) (*models.RepoActor, error) {
24
var repo models.RepoActor
25
-
if err := s.db.Raw("SELECT r.*, a.* FROM repos r LEFT JOIN actors a ON r.did = a.did WHERE r.email= ?", nil, email).Scan(&repo).Error; err != nil {
26
return nil, err
27
}
28
return &repo, nil
29
}
30
31
-
func (s *Server) getRepoActorByDid(did string) (*models.RepoActor, error) {
32
var repo models.RepoActor
33
-
if err := s.db.Raw("SELECT r.*, a.* FROM repos r LEFT JOIN actors a ON r.did = a.did WHERE r.did = ?", nil, did).Scan(&repo).Error; err != nil {
34
return nil, err
35
}
36
return &repo, nil
···
1
package server
2
3
import (
4
+
"context"
5
+
6
"github.com/haileyok/cocoon/models"
7
)
8
9
+
func (s *Server) getActorByHandle(ctx context.Context, handle string) (*models.Actor, error) {
10
var actor models.Actor
11
+
if err := s.db.First(ctx, &actor, models.Actor{Handle: handle}).Error; err != nil {
12
return nil, err
13
}
14
return &actor, nil
15
}
16
17
+
func (s *Server) getRepoByEmail(ctx context.Context, email string) (*models.Repo, error) {
18
var repo models.Repo
19
+
if err := s.db.First(ctx, &repo, models.Repo{Email: email}).Error; err != nil {
20
return nil, err
21
}
22
return &repo, nil
23
}
24
25
+
func (s *Server) getRepoActorByEmail(ctx context.Context, email string) (*models.RepoActor, error) {
26
var repo models.RepoActor
27
+
if err := s.db.Raw(ctx, "SELECT r.*, a.* FROM repos r LEFT JOIN actors a ON r.did = a.did WHERE r.email= ?", nil, email).Scan(&repo).Error; err != nil {
28
return nil, err
29
}
30
return &repo, nil
31
}
32
33
+
func (s *Server) getRepoActorByDid(ctx context.Context, did string) (*models.RepoActor, error) {
34
var repo models.RepoActor
35
+
if err := s.db.Raw(ctx, "SELECT r.*, a.* FROM repos r LEFT JOIN actors a ON r.did = a.did WHERE r.did = ?", nil, did).Scan(&repo).Error; err != nil {
36
return nil, err
37
}
38
return &repo, nil
+4
-2
server/handle_account.go
+4
-2
server/handle_account.go
···
12
13
func (s *Server) handleAccount(e echo.Context) error {
14
ctx := e.Request().Context()
15
repo, sess, err := s.getSessionRepoOrErr(e)
16
if err != nil {
17
return e.Redirect(303, "/account/signin")
···
20
oldestPossibleSession := time.Now().Add(constants.ConfidentialClientSessionLifetime)
21
22
var tokens []provider.OauthToken
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 {
24
-
s.logger.Error("couldnt fetch oauth sessions for account", "did", repo.Repo.Did, "error", err)
25
sess.AddFlash("Unable to fetch sessions. See server logs for more details.", "error")
26
sess.Save(e.Request(), e.Response())
27
return e.Render(200, "account.html", map[string]any{
···
12
13
func (s *Server) handleAccount(e echo.Context) error {
14
ctx := e.Request().Context()
15
+
logger := s.logger.With("name", "handleAuth")
16
+
17
repo, sess, err := s.getSessionRepoOrErr(e)
18
if err != nil {
19
return e.Redirect(303, "/account/signin")
···
22
oldestPossibleSession := time.Now().Add(constants.ConfidentialClientSessionLifetime)
23
24
var tokens []provider.OauthToken
25
+
if err := s.db.Raw(ctx, "SELECT * FROM oauth_tokens WHERE sub = ? AND created_at < ? ORDER BY created_at ASC", nil, repo.Repo.Did, oldestPossibleSession).Scan(&tokens).Error; err != nil {
26
+
logger.Error("couldnt fetch oauth sessions for account", "did", repo.Repo.Did, "error", err)
27
sess.AddFlash("Unable to fetch sessions. See server logs for more details.", "error")
28
sess.Save(e.Request(), e.Response())
29
return e.Render(200, "account.html", map[string]any{
+8
-5
server/handle_account_revoke.go
+8
-5
server/handle_account_revoke.go
···
5
"github.com/labstack/echo/v4"
6
)
7
8
-
type AccountRevokeRequest struct {
9
Token string `form:"token"`
10
}
11
12
func (s *Server) handleAccountRevoke(e echo.Context) error {
13
-
var req AccountRevokeRequest
14
if err := e.Bind(&req); err != nil {
15
-
s.logger.Error("could not bind account revoke request", "error", err)
16
return helpers.ServerError(e, nil)
17
}
18
···
21
return e.Redirect(303, "/account/signin")
22
}
23
24
-
if err := s.db.Exec("DELETE FROM oauth_tokens WHERE sub = ? AND token = ?", nil, repo.Repo.Did, req.Token).Error; err != nil {
25
-
s.logger.Error("couldnt delete oauth session for account", "did", repo.Repo.Did, "token", req.Token, "error", err)
26
sess.AddFlash("Unable to revoke session. See server logs for more details.", "error")
27
sess.Save(e.Request(), e.Response())
28
return e.Redirect(303, "/account")
···
5
"github.com/labstack/echo/v4"
6
)
7
8
+
type AccountRevokeInput struct {
9
Token string `form:"token"`
10
}
11
12
func (s *Server) handleAccountRevoke(e echo.Context) error {
13
+
ctx := e.Request().Context()
14
+
logger := s.logger.With("name", "handleAcocuntRevoke")
15
+
16
+
var req AccountRevokeInput
17
if err := e.Bind(&req); err != nil {
18
+
logger.Error("could not bind account revoke request", "error", err)
19
return helpers.ServerError(e, nil)
20
}
21
···
24
return e.Redirect(303, "/account/signin")
25
}
26
27
+
if err := s.db.Exec(ctx, "DELETE FROM oauth_tokens WHERE sub = ? AND token = ?", nil, repo.Repo.Did, req.Token).Error; err != nil {
28
+
logger.Error("couldnt delete oauth session for account", "did", repo.Repo.Did, "token", req.Token, "error", err)
29
sess.AddFlash("Unable to revoke session. See server logs for more details.", "error")
30
sess.Save(e.Request(), e.Response())
31
return e.Redirect(303, "/account")
+68
-16
server/handle_account_signin.go
+68
-16
server/handle_account_signin.go
···
2
3
import (
4
"errors"
5
"strings"
6
7
"github.com/bluesky-social/indigo/atproto/syntax"
8
"github.com/gorilla/sessions"
···
14
"gorm.io/gorm"
15
)
16
17
-
type OauthSigninRequest struct {
18
-
Username string `form:"username"`
19
-
Password string `form:"password"`
20
-
QueryParams string `form:"query_params"`
21
}
22
23
func (s *Server) getSessionRepoOrErr(e echo.Context) (*models.RepoActor, *sessions.Session, error) {
24
sess, err := session.Get("session", e)
25
if err != nil {
26
return nil, nil, err
···
31
return nil, sess, errors.New("did was not set in session")
32
}
33
34
-
repo, err := s.getRepoActorByDid(did)
35
if err != nil {
36
return nil, sess, err
37
}
···
42
func getFlashesFromSession(e echo.Context, sess *sessions.Session) map[string]any {
43
defer sess.Save(e.Request(), e.Response())
44
return map[string]any{
45
-
"errors": sess.Flashes("error"),
46
-
"successes": sess.Flashes("success"),
47
}
48
}
49
···
60
}
61
62
func (s *Server) handleAccountSigninPost(e echo.Context) error {
63
-
var req OauthSigninRequest
64
if err := e.Bind(&req); err != nil {
65
-
s.logger.Error("error binding sign in req", "error", err)
66
return helpers.ServerError(e, nil)
67
}
68
···
76
idtype = "handle"
77
} else {
78
idtype = "email"
79
}
80
81
// TODO: we should make this a helper since we do it for the base create_session as well
···
83
var err error
84
switch idtype {
85
case "did":
86
-
err = s.db.Raw("SELECT r.*, a.* FROM repos r LEFT JOIN actors a ON r.did = a.did WHERE r.did = ?", nil, req.Username).Scan(&repo).Error
87
case "handle":
88
-
err = s.db.Raw("SELECT r.*, a.* FROM actors a LEFT JOIN repos r ON a.did = r.did WHERE a.handle = ?", nil, req.Username).Scan(&repo).Error
89
case "email":
90
-
err = s.db.Raw("SELECT r.*, a.* FROM repos r LEFT JOIN actors a ON r.did = a.did WHERE r.email = ?", nil, req.Username).Scan(&repo).Error
91
}
92
if err != nil {
93
if err == gorm.ErrRecordNotFound {
···
96
sess.AddFlash("Something went wrong!", "error")
97
}
98
sess.Save(e.Request(), e.Response())
99
-
return e.Redirect(303, "/account/signin")
100
}
101
102
if err := bcrypt.CompareHashAndPassword([]byte(repo.Password), []byte(req.Password)); err != nil {
···
106
sess.AddFlash("Something went wrong!", "error")
107
}
108
sess.Save(e.Request(), e.Response())
109
-
return e.Redirect(303, "/account/signin")
110
}
111
112
sess.Options = &sessions.Options{
···
122
return err
123
}
124
125
-
if req.QueryParams != "" {
126
-
return e.Redirect(303, "/oauth/authorize?"+req.QueryParams)
127
} else {
128
return e.Redirect(303, "/account")
129
}
···
2
3
import (
4
"errors"
5
+
"fmt"
6
"strings"
7
+
"time"
8
9
"github.com/bluesky-social/indigo/atproto/syntax"
10
"github.com/gorilla/sessions"
···
16
"gorm.io/gorm"
17
)
18
19
+
type OauthSigninInput struct {
20
+
Username string `form:"username"`
21
+
Password string `form:"password"`
22
+
AuthFactorToken string `form:"token"`
23
+
QueryParams string `form:"query_params"`
24
}
25
26
func (s *Server) getSessionRepoOrErr(e echo.Context) (*models.RepoActor, *sessions.Session, error) {
27
+
ctx := e.Request().Context()
28
+
29
sess, err := session.Get("session", e)
30
if err != nil {
31
return nil, nil, err
···
36
return nil, sess, errors.New("did was not set in session")
37
}
38
39
+
repo, err := s.getRepoActorByDid(ctx, did)
40
if err != nil {
41
return nil, sess, err
42
}
···
47
func getFlashesFromSession(e echo.Context, sess *sessions.Session) map[string]any {
48
defer sess.Save(e.Request(), e.Response())
49
return map[string]any{
50
+
"errors": sess.Flashes("error"),
51
+
"successes": sess.Flashes("success"),
52
+
"tokenrequired": sess.Flashes("tokenrequired"),
53
}
54
}
55
···
66
}
67
68
func (s *Server) handleAccountSigninPost(e echo.Context) error {
69
+
ctx := e.Request().Context()
70
+
logger := s.logger.With("name", "handleAccountSigninPost")
71
+
72
+
var req OauthSigninInput
73
if err := e.Bind(&req); err != nil {
74
+
logger.Error("error binding sign in req", "error", err)
75
return helpers.ServerError(e, nil)
76
}
77
···
85
idtype = "handle"
86
} else {
87
idtype = "email"
88
+
}
89
+
90
+
queryParams := ""
91
+
if req.QueryParams != "" {
92
+
queryParams = fmt.Sprintf("?%s", req.QueryParams)
93
}
94
95
// TODO: we should make this a helper since we do it for the base create_session as well
···
97
var err error
98
switch idtype {
99
case "did":
100
+
err = s.db.Raw(ctx, "SELECT r.*, a.* FROM repos r LEFT JOIN actors a ON r.did = a.did WHERE r.did = ?", nil, req.Username).Scan(&repo).Error
101
case "handle":
102
+
err = s.db.Raw(ctx, "SELECT r.*, a.* FROM actors a LEFT JOIN repos r ON a.did = r.did WHERE a.handle = ?", nil, req.Username).Scan(&repo).Error
103
case "email":
104
+
err = s.db.Raw(ctx, "SELECT r.*, a.* FROM repos r LEFT JOIN actors a ON r.did = a.did WHERE r.email = ?", nil, req.Username).Scan(&repo).Error
105
}
106
if err != nil {
107
if err == gorm.ErrRecordNotFound {
···
110
sess.AddFlash("Something went wrong!", "error")
111
}
112
sess.Save(e.Request(), e.Response())
113
+
return e.Redirect(303, "/account/signin"+queryParams)
114
}
115
116
if err := bcrypt.CompareHashAndPassword([]byte(repo.Password), []byte(req.Password)); err != nil {
···
120
sess.AddFlash("Something went wrong!", "error")
121
}
122
sess.Save(e.Request(), e.Response())
123
+
return e.Redirect(303, "/account/signin"+queryParams)
124
+
}
125
+
126
+
// if repo requires 2FA token and one hasn't been provided, return error prompting for one
127
+
if repo.TwoFactorType != models.TwoFactorTypeNone && req.AuthFactorToken == "" {
128
+
err = s.createAndSendTwoFactorCode(ctx, repo)
129
+
if err != nil {
130
+
sess.AddFlash("Something went wrong!", "error")
131
+
sess.Save(e.Request(), e.Response())
132
+
return e.Redirect(303, "/account/signin"+queryParams)
133
+
}
134
+
135
+
sess.AddFlash("requires 2FA token", "tokenrequired")
136
+
sess.Save(e.Request(), e.Response())
137
+
return e.Redirect(303, "/account/signin"+queryParams)
138
+
}
139
+
140
+
// if 2FAis required, now check that the one provided is valid
141
+
if repo.TwoFactorType != models.TwoFactorTypeNone {
142
+
if repo.TwoFactorCode == nil || repo.TwoFactorCodeExpiresAt == nil {
143
+
err = s.createAndSendTwoFactorCode(ctx, repo)
144
+
if err != nil {
145
+
sess.AddFlash("Something went wrong!", "error")
146
+
sess.Save(e.Request(), e.Response())
147
+
return e.Redirect(303, "/account/signin"+queryParams)
148
+
}
149
+
150
+
sess.AddFlash("requires 2FA token", "tokenrequired")
151
+
sess.Save(e.Request(), e.Response())
152
+
return e.Redirect(303, "/account/signin"+queryParams)
153
+
}
154
+
155
+
if *repo.TwoFactorCode != req.AuthFactorToken {
156
+
return helpers.InvalidTokenError(e)
157
+
}
158
+
159
+
if time.Now().UTC().After(*repo.TwoFactorCodeExpiresAt) {
160
+
return helpers.ExpiredTokenError(e)
161
+
}
162
}
163
164
sess.Options = &sessions.Options{
···
174
return err
175
}
176
177
+
if queryParams != "" {
178
+
return e.Redirect(303, "/oauth/authorize"+queryParams)
179
} else {
180
return e.Redirect(303, "/account")
181
}
+3
-1
server/handle_actor_put_preferences.go
+3
-1
server/handle_actor_put_preferences.go
···
10
// This is kinda lame. Not great to implement app.bsky in the pds, but alas
11
12
func (s *Server) handleActorPutPreferences(e echo.Context) error {
13
repo := e.Get("repo").(*models.RepoActor)
14
15
var prefs map[string]any
···
22
return err
23
}
24
25
-
if err := s.db.Exec("UPDATE repos SET preferences = ? WHERE did = ?", nil, b, repo.Repo.Did).Error; err != nil {
26
return err
27
}
28
···
10
// This is kinda lame. Not great to implement app.bsky in the pds, but alas
11
12
func (s *Server) handleActorPutPreferences(e echo.Context) error {
13
+
ctx := e.Request().Context()
14
+
15
repo := e.Get("repo").(*models.RepoActor)
16
17
var prefs map[string]any
···
24
return err
25
}
26
27
+
if err := s.db.Exec(ctx, "UPDATE repos SET preferences = ? WHERE did = ?", nil, b, repo.Repo.Did).Error; err != nil {
28
return err
29
}
30
+26
server/handle_identity_get_recommended_did_credentials.go
+26
server/handle_identity_get_recommended_did_credentials.go
···
···
1
+
package server
2
+
3
+
import (
4
+
"github.com/bluesky-social/indigo/atproto/atcrypto"
5
+
"github.com/haileyok/cocoon/internal/helpers"
6
+
"github.com/haileyok/cocoon/models"
7
+
"github.com/labstack/echo/v4"
8
+
)
9
+
10
+
func (s *Server) handleGetRecommendedDidCredentials(e echo.Context) error {
11
+
logger := s.logger.With("name", "handleIdentityGetRecommendedDidCredentials")
12
+
13
+
repo := e.Get("repo").(*models.RepoActor)
14
+
k, err := atcrypto.ParsePrivateBytesK256(repo.SigningKey)
15
+
if err != nil {
16
+
logger.Error("error parsing key", "error", err)
17
+
return helpers.ServerError(e, nil)
18
+
}
19
+
creds, err := s.plcClient.CreateDidCredentials(k, "", repo.Actor.Handle)
20
+
if err != nil {
21
+
logger.Error("error crating did credentials", "error", err)
22
+
return helpers.ServerError(e, nil)
23
+
}
24
+
25
+
return e.JSON(200, creds)
26
+
}
+32
server/handle_identity_request_plc_operation.go
+32
server/handle_identity_request_plc_operation.go
···
···
1
+
package server
2
+
3
+
import (
4
+
"fmt"
5
+
"time"
6
+
7
+
"github.com/haileyok/cocoon/internal/helpers"
8
+
"github.com/haileyok/cocoon/models"
9
+
"github.com/labstack/echo/v4"
10
+
)
11
+
12
+
func (s *Server) handleIdentityRequestPlcOperationSignature(e echo.Context) error {
13
+
ctx := e.Request().Context()
14
+
logger := s.logger.With("name", "handleIdentityRequestPlcOperationSignature")
15
+
16
+
urepo := e.Get("repo").(*models.RepoActor)
17
+
18
+
code := fmt.Sprintf("%s-%s", helpers.RandomVarchar(5), helpers.RandomVarchar(5))
19
+
eat := time.Now().Add(10 * time.Minute).UTC()
20
+
21
+
if err := s.db.Exec(ctx, "UPDATE repos SET plc_operation_code = ?, plc_operation_code_expires_at = ? WHERE did = ?", nil, code, eat, urepo.Repo.Did).Error; err != nil {
22
+
logger.Error("error updating user", "error", err)
23
+
return helpers.ServerError(e, nil)
24
+
}
25
+
26
+
if err := s.sendPlcTokenReset(urepo.Email, urepo.Handle, code); err != nil {
27
+
logger.Error("error sending mail", "error", err)
28
+
return helpers.ServerError(e, nil)
29
+
}
30
+
31
+
return e.NoContent(200)
32
+
}
+105
server/handle_identity_sign_plc_operation.go
+105
server/handle_identity_sign_plc_operation.go
···
···
1
+
package server
2
+
3
+
import (
4
+
"context"
5
+
"strings"
6
+
"time"
7
+
8
+
"github.com/Azure/go-autorest/autorest/to"
9
+
"github.com/bluesky-social/indigo/atproto/atcrypto"
10
+
"github.com/haileyok/cocoon/identity"
11
+
"github.com/haileyok/cocoon/internal/helpers"
12
+
"github.com/haileyok/cocoon/models"
13
+
"github.com/haileyok/cocoon/plc"
14
+
"github.com/labstack/echo/v4"
15
+
)
16
+
17
+
type ComAtprotoSignPlcOperationRequest struct {
18
+
Token string `json:"token"`
19
+
VerificationMethods *map[string]string `json:"verificationMethods"`
20
+
RotationKeys *[]string `json:"rotationKeys"`
21
+
AlsoKnownAs *[]string `json:"alsoKnownAs"`
22
+
Services *map[string]identity.OperationService `json:"services"`
23
+
}
24
+
25
+
type ComAtprotoSignPlcOperationResponse struct {
26
+
Operation plc.Operation `json:"operation"`
27
+
}
28
+
29
+
func (s *Server) handleSignPlcOperation(e echo.Context) error {
30
+
logger := s.logger.With("name", "handleSignPlcOperation")
31
+
32
+
repo := e.Get("repo").(*models.RepoActor)
33
+
34
+
var req ComAtprotoSignPlcOperationRequest
35
+
if err := e.Bind(&req); err != nil {
36
+
logger.Error("error binding", "error", err)
37
+
return helpers.ServerError(e, nil)
38
+
}
39
+
40
+
if !strings.HasPrefix(repo.Repo.Did, "did:plc:") {
41
+
return helpers.InputError(e, nil)
42
+
}
43
+
44
+
if repo.PlcOperationCode == nil || repo.PlcOperationCodeExpiresAt == nil {
45
+
return helpers.InputError(e, to.StringPtr("InvalidToken"))
46
+
}
47
+
48
+
if *repo.PlcOperationCode != req.Token {
49
+
return helpers.InvalidTokenError(e)
50
+
}
51
+
52
+
if time.Now().UTC().After(*repo.PlcOperationCodeExpiresAt) {
53
+
return helpers.ExpiredTokenError(e)
54
+
}
55
+
56
+
ctx := context.WithValue(e.Request().Context(), "skip-cache", true)
57
+
log, err := identity.FetchDidAuditLog(ctx, nil, repo.Repo.Did)
58
+
if err != nil {
59
+
logger.Error("error fetching doc", "error", err)
60
+
return helpers.ServerError(e, nil)
61
+
}
62
+
63
+
latest := log[len(log)-1]
64
+
65
+
op := plc.Operation{
66
+
Type: "plc_operation",
67
+
VerificationMethods: latest.Operation.VerificationMethods,
68
+
RotationKeys: latest.Operation.RotationKeys,
69
+
AlsoKnownAs: latest.Operation.AlsoKnownAs,
70
+
Services: latest.Operation.Services,
71
+
Prev: &latest.Cid,
72
+
}
73
+
if req.VerificationMethods != nil {
74
+
op.VerificationMethods = *req.VerificationMethods
75
+
}
76
+
if req.RotationKeys != nil {
77
+
op.RotationKeys = *req.RotationKeys
78
+
}
79
+
if req.AlsoKnownAs != nil {
80
+
op.AlsoKnownAs = *req.AlsoKnownAs
81
+
}
82
+
if req.Services != nil {
83
+
op.Services = *req.Services
84
+
}
85
+
86
+
k, err := atcrypto.ParsePrivateBytesK256(repo.SigningKey)
87
+
if err != nil {
88
+
logger.Error("error parsing signing key", "error", err)
89
+
return helpers.ServerError(e, nil)
90
+
}
91
+
92
+
if err := s.plcClient.SignOp(k, &op); err != nil {
93
+
logger.Error("error signing plc operation", "error", err)
94
+
return helpers.ServerError(e, nil)
95
+
}
96
+
97
+
if err := s.db.Exec(ctx, "UPDATE repos SET plc_operation_code = NULL, plc_operation_code_expires_at = NULL WHERE did = ?", nil, repo.Repo.Did).Error; err != nil {
98
+
logger.Error("error updating repo", "error", err)
99
+
return helpers.ServerError(e, nil)
100
+
}
101
+
102
+
return e.JSON(200, ComAtprotoSignPlcOperationResponse{
103
+
Operation: op,
104
+
})
105
+
}
+89
server/handle_identity_submit_plc_operation.go
+89
server/handle_identity_submit_plc_operation.go
···
···
1
+
package server
2
+
3
+
import (
4
+
"context"
5
+
"slices"
6
+
"strings"
7
+
"time"
8
+
9
+
"github.com/bluesky-social/indigo/api/atproto"
10
+
"github.com/bluesky-social/indigo/atproto/atcrypto"
11
+
"github.com/bluesky-social/indigo/events"
12
+
"github.com/bluesky-social/indigo/util"
13
+
"github.com/haileyok/cocoon/internal/helpers"
14
+
"github.com/haileyok/cocoon/models"
15
+
"github.com/haileyok/cocoon/plc"
16
+
"github.com/labstack/echo/v4"
17
+
)
18
+
19
+
type ComAtprotoSubmitPlcOperationRequest struct {
20
+
Operation plc.Operation `json:"operation"`
21
+
}
22
+
23
+
func (s *Server) handleSubmitPlcOperation(e echo.Context) error {
24
+
logger := s.logger.With("name", "handleIdentitySubmitPlcOperation")
25
+
26
+
repo := e.Get("repo").(*models.RepoActor)
27
+
28
+
var req ComAtprotoSubmitPlcOperationRequest
29
+
if err := e.Bind(&req); err != nil {
30
+
logger.Error("error binding", "error", err)
31
+
return helpers.ServerError(e, nil)
32
+
}
33
+
34
+
if err := e.Validate(req); err != nil {
35
+
return helpers.InputError(e, nil)
36
+
}
37
+
if !strings.HasPrefix(repo.Repo.Did, "did:plc:") {
38
+
return helpers.InputError(e, nil)
39
+
}
40
+
41
+
op := req.Operation
42
+
43
+
k, err := atcrypto.ParsePrivateBytesK256(repo.SigningKey)
44
+
if err != nil {
45
+
logger.Error("error parsing key", "error", err)
46
+
return helpers.ServerError(e, nil)
47
+
}
48
+
required, err := s.plcClient.CreateDidCredentials(k, "", repo.Actor.Handle)
49
+
if err != nil {
50
+
logger.Error("error crating did credentials", "error", err)
51
+
return helpers.ServerError(e, nil)
52
+
}
53
+
54
+
for _, expectedKey := range required.RotationKeys {
55
+
if !slices.Contains(op.RotationKeys, expectedKey) {
56
+
return helpers.InputError(e, nil)
57
+
}
58
+
}
59
+
if op.Services["atproto_pds"].Type != "AtprotoPersonalDataServer" {
60
+
return helpers.InputError(e, nil)
61
+
}
62
+
if op.Services["atproto_pds"].Endpoint != required.Services["atproto_pds"].Endpoint {
63
+
return helpers.InputError(e, nil)
64
+
}
65
+
if op.VerificationMethods["atproto"] != required.VerificationMethods["atproto"] {
66
+
return helpers.InputError(e, nil)
67
+
}
68
+
if op.AlsoKnownAs[0] != required.AlsoKnownAs[0] {
69
+
return helpers.InputError(e, nil)
70
+
}
71
+
72
+
if err := s.plcClient.SendOperation(e.Request().Context(), repo.Repo.Did, &op); err != nil {
73
+
return err
74
+
}
75
+
76
+
if err := s.passport.BustDoc(context.TODO(), repo.Repo.Did); err != nil {
77
+
logger.Warn("error busting did doc", "error", err)
78
+
}
79
+
80
+
s.evtman.AddEvent(context.TODO(), &events.XRPCStreamEvent{
81
+
RepoIdentity: &atproto.SyncSubscribeRepos_Identity{
82
+
Did: repo.Repo.Did,
83
+
Seq: time.Now().UnixMicro(), // TODO: no
84
+
Time: time.Now().Format(util.ISO8601),
85
+
},
86
+
})
87
+
88
+
return nil
89
+
}
+8
-6
server/handle_identity_update_handle.go
+8
-6
server/handle_identity_update_handle.go
···
22
}
23
24
func (s *Server) handleIdentityUpdateHandle(e echo.Context) error {
25
repo := e.Get("repo").(*models.RepoActor)
26
27
var req ComAtprotoIdentityUpdateHandleRequest
28
if err := e.Bind(&req); err != nil {
29
-
s.logger.Error("error binding", "error", err)
30
return helpers.ServerError(e, nil)
31
}
32
···
41
if strings.HasPrefix(repo.Repo.Did, "did:plc:") {
42
log, err := identity.FetchDidAuditLog(ctx, nil, repo.Repo.Did)
43
if err != nil {
44
-
s.logger.Error("error fetching doc", "error", err)
45
return helpers.ServerError(e, nil)
46
}
47
···
68
69
k, err := atcrypto.ParsePrivateBytesK256(repo.SigningKey)
70
if err != nil {
71
-
s.logger.Error("error parsing signing key", "error", err)
72
return helpers.ServerError(e, nil)
73
}
74
···
82
}
83
84
if err := s.passport.BustDoc(context.TODO(), repo.Repo.Did); err != nil {
85
-
s.logger.Warn("error busting did doc", "error", err)
86
}
87
88
s.evtman.AddEvent(context.TODO(), &events.XRPCStreamEvent{
···
94
},
95
})
96
97
-
if err := s.db.Exec("UPDATE actors SET handle = ? WHERE did = ?", nil, req.Handle, repo.Repo.Did).Error; err != nil {
98
-
s.logger.Error("error updating handle in db", "error", err)
99
return helpers.ServerError(e, nil)
100
}
101
···
22
}
23
24
func (s *Server) handleIdentityUpdateHandle(e echo.Context) error {
25
+
logger := s.logger.With("name", "handleIdentityUpdateHandle")
26
+
27
repo := e.Get("repo").(*models.RepoActor)
28
29
var req ComAtprotoIdentityUpdateHandleRequest
30
if err := e.Bind(&req); err != nil {
31
+
logger.Error("error binding", "error", err)
32
return helpers.ServerError(e, nil)
33
}
34
···
43
if strings.HasPrefix(repo.Repo.Did, "did:plc:") {
44
log, err := identity.FetchDidAuditLog(ctx, nil, repo.Repo.Did)
45
if err != nil {
46
+
logger.Error("error fetching doc", "error", err)
47
return helpers.ServerError(e, nil)
48
}
49
···
70
71
k, err := atcrypto.ParsePrivateBytesK256(repo.SigningKey)
72
if err != nil {
73
+
logger.Error("error parsing signing key", "error", err)
74
return helpers.ServerError(e, nil)
75
}
76
···
84
}
85
86
if err := s.passport.BustDoc(context.TODO(), repo.Repo.Did); err != nil {
87
+
logger.Warn("error busting did doc", "error", err)
88
}
89
90
s.evtman.AddEvent(context.TODO(), &events.XRPCStreamEvent{
···
96
},
97
})
98
99
+
if err := s.db.Exec(ctx, "UPDATE actors SET handle = ? WHERE did = ?", nil, req.Handle, repo.Repo.Did).Error; err != nil {
100
+
logger.Error("error updating handle in db", "error", err)
101
return helpers.ServerError(e, nil)
102
}
103
+15
-12
server/handle_import_repo.go
+15
-12
server/handle_import_repo.go
···
18
)
19
20
func (s *Server) handleRepoImportRepo(e echo.Context) error {
21
urepo := e.Get("repo").(*models.RepoActor)
22
23
b, err := io.ReadAll(e.Request().Body)
24
if err != nil {
25
-
s.logger.Error("could not read bytes in import request", "error", err)
26
return helpers.ServerError(e, nil)
27
}
28
···
30
31
cs, err := car.NewCarReader(bytes.NewReader(b))
32
if err != nil {
33
-
s.logger.Error("could not read car in import request", "error", err)
34
return helpers.ServerError(e, nil)
35
}
36
37
orderedBlocks := []blocks.Block{}
38
currBlock, err := cs.Next()
39
if err != nil {
40
-
s.logger.Error("could not get first block from car", "error", err)
41
return helpers.ServerError(e, nil)
42
}
43
currBlockCt := 1
44
45
for currBlock != nil {
46
-
s.logger.Info("someone is importing their repo", "block", currBlockCt)
47
orderedBlocks = append(orderedBlocks, currBlock)
48
next, _ := cs.Next()
49
currBlock = next
···
53
slices.Reverse(orderedBlocks)
54
55
if err := bs.PutMany(context.TODO(), orderedBlocks); err != nil {
56
-
s.logger.Error("could not insert blocks", "error", err)
57
return helpers.ServerError(e, nil)
58
}
59
60
r, err := repo.OpenRepo(context.TODO(), bs, cs.Header.Roots[0])
61
if err != nil {
62
-
s.logger.Error("could not open repo", "error", err)
63
return helpers.ServerError(e, nil)
64
}
65
66
-
tx := s.db.BeginDangerously()
67
68
clock := syntax.NewTIDClock(0)
69
···
74
cidStr := cid.String()
75
b, err := bs.Get(context.TODO(), cid)
76
if err != nil {
77
-
s.logger.Error("record bytes don't exist in blockstore", "error", err)
78
return helpers.ServerError(e, nil)
79
}
80
···
87
Value: b.RawData(),
88
}
89
90
-
if err := tx.Create(rec).Error; err != nil {
91
return err
92
}
93
94
return nil
95
}); err != nil {
96
tx.Rollback()
97
-
s.logger.Error("record bytes don't exist in blockstore", "error", err)
98
return helpers.ServerError(e, nil)
99
}
100
···
102
103
root, rev, err := r.Commit(context.TODO(), urepo.SignFor)
104
if err != nil {
105
-
s.logger.Error("error committing", "error", err)
106
return helpers.ServerError(e, nil)
107
}
108
109
if err := s.UpdateRepo(context.TODO(), urepo.Repo.Did, root, rev); err != nil {
110
-
s.logger.Error("error updating repo after commit", "error", err)
111
return helpers.ServerError(e, nil)
112
}
113
···
18
)
19
20
func (s *Server) handleRepoImportRepo(e echo.Context) error {
21
+
ctx := e.Request().Context()
22
+
logger := s.logger.With("name", "handleImportRepo")
23
+
24
urepo := e.Get("repo").(*models.RepoActor)
25
26
b, err := io.ReadAll(e.Request().Body)
27
if err != nil {
28
+
logger.Error("could not read bytes in import request", "error", err)
29
return helpers.ServerError(e, nil)
30
}
31
···
33
34
cs, err := car.NewCarReader(bytes.NewReader(b))
35
if err != nil {
36
+
logger.Error("could not read car in import request", "error", err)
37
return helpers.ServerError(e, nil)
38
}
39
40
orderedBlocks := []blocks.Block{}
41
currBlock, err := cs.Next()
42
if err != nil {
43
+
logger.Error("could not get first block from car", "error", err)
44
return helpers.ServerError(e, nil)
45
}
46
currBlockCt := 1
47
48
for currBlock != nil {
49
+
logger.Info("someone is importing their repo", "block", currBlockCt)
50
orderedBlocks = append(orderedBlocks, currBlock)
51
next, _ := cs.Next()
52
currBlock = next
···
56
slices.Reverse(orderedBlocks)
57
58
if err := bs.PutMany(context.TODO(), orderedBlocks); err != nil {
59
+
logger.Error("could not insert blocks", "error", err)
60
return helpers.ServerError(e, nil)
61
}
62
63
r, err := repo.OpenRepo(context.TODO(), bs, cs.Header.Roots[0])
64
if err != nil {
65
+
logger.Error("could not open repo", "error", err)
66
return helpers.ServerError(e, nil)
67
}
68
69
+
tx := s.db.BeginDangerously(ctx)
70
71
clock := syntax.NewTIDClock(0)
72
···
77
cidStr := cid.String()
78
b, err := bs.Get(context.TODO(), cid)
79
if err != nil {
80
+
logger.Error("record bytes don't exist in blockstore", "error", err)
81
return helpers.ServerError(e, nil)
82
}
83
···
90
Value: b.RawData(),
91
}
92
93
+
if err := tx.Save(rec).Error; err != nil {
94
return err
95
}
96
97
return nil
98
}); err != nil {
99
tx.Rollback()
100
+
logger.Error("record bytes don't exist in blockstore", "error", err)
101
return helpers.ServerError(e, nil)
102
}
103
···
105
106
root, rev, err := r.Commit(context.TODO(), urepo.SignFor)
107
if err != nil {
108
+
logger.Error("error committing", "error", err)
109
return helpers.ServerError(e, nil)
110
}
111
112
if err := s.UpdateRepo(context.TODO(), urepo.Repo.Did, root, rev); err != nil {
113
+
logger.Error("error updating repo after commit", "error", err)
114
return helpers.ServerError(e, nil)
115
}
116
+34
server/handle_label_query_labels.go
+34
server/handle_label_query_labels.go
···
···
1
+
package server
2
+
3
+
import (
4
+
"github.com/labstack/echo/v4"
5
+
)
6
+
7
+
type Label struct {
8
+
Ver *int `json:"ver,omitempty"`
9
+
Src string `json:"src"`
10
+
Uri string `json:"uri"`
11
+
Cid *string `json:"cid,omitempty"`
12
+
Val string `json:"val"`
13
+
Neg *bool `json:"neg,omitempty"`
14
+
Cts string `json:"cts"`
15
+
Exp *string `json:"exp,omitempty"`
16
+
Sig []byte `json:"sig,omitempty"`
17
+
}
18
+
19
+
type ComAtprotoLabelQueryLabelsResponse struct {
20
+
Cursor *string `json:"cursor,omitempty"`
21
+
Labels []Label `json:"labels"`
22
+
}
23
+
24
+
func (s *Server) handleLabelQueryLabels(e echo.Context) error {
25
+
svc := e.Request().Header.Get("atproto-proxy")
26
+
if svc != "" || s.config.FallbackProxy != "" {
27
+
return s.handleProxy(e)
28
+
}
29
+
30
+
return e.JSON(200, ComAtprotoLabelQueryLabelsResponse{
31
+
Cursor: nil,
32
+
Labels: []Label{},
33
+
})
34
+
}
+16
-8
server/handle_oauth_par.go
+16
-8
server/handle_oauth_par.go
···
19
}
20
21
func (s *Server) handleOauthPar(e echo.Context) error {
22
var parRequest provider.ParRequest
23
if err := e.Bind(&parRequest); err != nil {
24
-
s.logger.Error("error binding for par request", "error", err)
25
return helpers.ServerError(e, nil)
26
}
27
28
if err := e.Validate(parRequest); err != nil {
29
-
s.logger.Error("missing parameters for par request", "error", err)
30
return helpers.InputError(e, nil)
31
}
32
···
34
dpopProof, err := s.oauthProvider.DpopManager.CheckProof(e.Request().Method, "https://"+s.config.Hostname+e.Request().URL.String(), e.Request().Header, nil)
35
if err != nil {
36
if errors.Is(err, dpop.ErrUseDpopNonce) {
37
return e.JSON(400, map[string]string{
38
"error": "use_dpop_nonce",
39
})
40
}
41
-
s.logger.Error("error getting dpop proof", "error", err)
42
return helpers.InputError(e, nil)
43
}
44
···
48
AllowMissingDpopProof: true,
49
})
50
if err != nil {
51
-
s.logger.Error("error authenticating client", "client_id", parRequest.ClientID, "error", err)
52
return helpers.InputError(e, to.StringPtr(err.Error()))
53
}
54
···
59
} else {
60
if !client.Metadata.DpopBoundAccessTokens {
61
msg := "dpop bound access tokens are not enabled for this client"
62
-
s.logger.Error(msg)
63
return helpers.InputError(e, &msg)
64
}
65
66
if dpopProof.JKT != *parRequest.DpopJkt {
67
msg := "supplied dpop jkt does not match header dpop jkt"
68
-
s.logger.Error(msg)
69
return helpers.InputError(e, &msg)
70
}
71
}
···
81
ExpiresAt: eat,
82
}
83
84
-
if err := s.db.Create(authRequest, nil).Error; err != nil {
85
-
s.logger.Error("error creating auth request in db", "error", err)
86
return helpers.ServerError(e, nil)
87
}
88
···
19
}
20
21
func (s *Server) handleOauthPar(e echo.Context) error {
22
+
ctx := e.Request().Context()
23
+
logger := s.logger.With("name", "handleOauthPar")
24
+
25
var parRequest provider.ParRequest
26
if err := e.Bind(&parRequest); err != nil {
27
+
logger.Error("error binding for par request", "error", err)
28
return helpers.ServerError(e, nil)
29
}
30
31
if err := e.Validate(parRequest); err != nil {
32
+
logger.Error("missing parameters for par request", "error", err)
33
return helpers.InputError(e, nil)
34
}
35
···
37
dpopProof, err := s.oauthProvider.DpopManager.CheckProof(e.Request().Method, "https://"+s.config.Hostname+e.Request().URL.String(), e.Request().Header, nil)
38
if err != nil {
39
if errors.Is(err, dpop.ErrUseDpopNonce) {
40
+
nonce := s.oauthProvider.NextNonce()
41
+
if nonce != "" {
42
+
e.Response().Header().Set("DPoP-Nonce", nonce)
43
+
e.Response().Header().Add("access-control-expose-headers", "DPoP-Nonce")
44
+
}
45
return e.JSON(400, map[string]string{
46
"error": "use_dpop_nonce",
47
})
48
}
49
+
logger.Error("error getting dpop proof", "error", err)
50
return helpers.InputError(e, nil)
51
}
52
···
56
AllowMissingDpopProof: true,
57
})
58
if err != nil {
59
+
logger.Error("error authenticating client", "client_id", parRequest.ClientID, "error", err)
60
return helpers.InputError(e, to.StringPtr(err.Error()))
61
}
62
···
67
} else {
68
if !client.Metadata.DpopBoundAccessTokens {
69
msg := "dpop bound access tokens are not enabled for this client"
70
+
logger.Error(msg)
71
return helpers.InputError(e, &msg)
72
}
73
74
if dpopProof.JKT != *parRequest.DpopJkt {
75
msg := "supplied dpop jkt does not match header dpop jkt"
76
+
logger.Error(msg)
77
return helpers.InputError(e, &msg)
78
}
79
}
···
89
ExpiresAt: eat,
90
}
91
92
+
if err := s.db.Create(ctx, authRequest, nil).Error; err != nil {
93
+
logger.Error("error creating auth request in db", "error", err)
94
return helpers.ServerError(e, nil)
95
}
96
+21
-13
server/handle_oauth_token.go
+21
-13
server/handle_oauth_token.go
···
38
}
39
40
func (s *Server) handleOauthToken(e echo.Context) error {
41
var req OauthTokenRequest
42
if err := e.Bind(&req); err != nil {
43
-
s.logger.Error("error binding token request", "error", err)
44
return helpers.ServerError(e, nil)
45
}
46
47
proof, err := s.oauthProvider.DpopManager.CheckProof(e.Request().Method, e.Request().URL.String(), e.Request().Header, nil)
48
if err != nil {
49
if errors.Is(err, dpop.ErrUseDpopNonce) {
50
return e.JSON(400, map[string]string{
51
"error": "use_dpop_nonce",
52
})
53
}
54
-
s.logger.Error("error getting dpop proof", "error", err)
55
return helpers.InputError(e, nil)
56
}
57
···
59
AllowMissingDpopProof: true,
60
})
61
if err != nil {
62
-
s.logger.Error("error authenticating client", "client_id", req.ClientID, "error", err)
63
return helpers.InputError(e, to.StringPtr(err.Error()))
64
}
65
···
79
80
var authReq provider.OauthAuthorizationRequest
81
// get the lil guy and delete him
82
-
if err := s.db.Raw("DELETE FROM oauth_authorization_requests WHERE code = ? RETURNING *", nil, *req.Code).Scan(&authReq).Error; err != nil {
83
-
s.logger.Error("error finding authorization request", "error", err)
84
return helpers.ServerError(e, nil)
85
}
86
···
105
case "S256":
106
inputChal, err := base64.RawURLEncoding.DecodeString(*authReq.Parameters.CodeChallenge)
107
if err != nil {
108
-
s.logger.Error("error decoding code challenge", "error", err)
109
return helpers.ServerError(e, nil)
110
}
111
···
123
return helpers.InputError(e, to.StringPtr("code_challenge parameter wasn't provided"))
124
}
125
126
-
repo, err := s.getRepoActorByDid(*authReq.Sub)
127
if err != nil {
128
helpers.InputError(e, to.StringPtr("unable to find actor"))
129
}
···
154
return err
155
}
156
157
-
if err := s.db.Create(&provider.OauthToken{
158
ClientId: authReq.ClientId,
159
ClientAuth: *clientAuth,
160
Parameters: authReq.Parameters,
···
166
RefreshToken: refreshToken,
167
Ip: authReq.Ip,
168
}, nil).Error; err != nil {
169
-
s.logger.Error("error creating token in db", "error", err)
170
return helpers.ServerError(e, nil)
171
}
172
···
194
}
195
196
var oauthToken provider.OauthToken
197
-
if err := s.db.Raw("SELECT * FROM oauth_tokens WHERE refresh_token = ?", nil, req.RefreshToken).Scan(&oauthToken).Error; err != nil {
198
-
s.logger.Error("error finding oauth token by refresh token", "error", err, "refresh_token", req.RefreshToken)
199
return helpers.ServerError(e, nil)
200
}
201
···
252
return err
253
}
254
255
-
if err := s.db.Exec("UPDATE oauth_tokens SET token = ?, refresh_token = ?, expires_at = ?, updated_at = ? WHERE refresh_token = ?", nil, accessString, nextRefreshToken, eat, now, *req.RefreshToken).Error; err != nil {
256
-
s.logger.Error("error updating token", "error", err)
257
return helpers.ServerError(e, nil)
258
}
259
···
38
}
39
40
func (s *Server) handleOauthToken(e echo.Context) error {
41
+
ctx := e.Request().Context()
42
+
logger := s.logger.With("name", "handleOauthToken")
43
+
44
var req OauthTokenRequest
45
if err := e.Bind(&req); err != nil {
46
+
logger.Error("error binding token request", "error", err)
47
return helpers.ServerError(e, nil)
48
}
49
50
proof, err := s.oauthProvider.DpopManager.CheckProof(e.Request().Method, e.Request().URL.String(), e.Request().Header, nil)
51
if err != nil {
52
if errors.Is(err, dpop.ErrUseDpopNonce) {
53
+
nonce := s.oauthProvider.NextNonce()
54
+
if nonce != "" {
55
+
e.Response().Header().Set("DPoP-Nonce", nonce)
56
+
e.Response().Header().Add("access-control-expose-headers", "DPoP-Nonce")
57
+
}
58
return e.JSON(400, map[string]string{
59
"error": "use_dpop_nonce",
60
})
61
}
62
+
logger.Error("error getting dpop proof", "error", err)
63
return helpers.InputError(e, nil)
64
}
65
···
67
AllowMissingDpopProof: true,
68
})
69
if err != nil {
70
+
logger.Error("error authenticating client", "client_id", req.ClientID, "error", err)
71
return helpers.InputError(e, to.StringPtr(err.Error()))
72
}
73
···
87
88
var authReq provider.OauthAuthorizationRequest
89
// get the lil guy and delete him
90
+
if err := s.db.Raw(ctx, "DELETE FROM oauth_authorization_requests WHERE code = ? RETURNING *", nil, *req.Code).Scan(&authReq).Error; err != nil {
91
+
logger.Error("error finding authorization request", "error", err)
92
return helpers.ServerError(e, nil)
93
}
94
···
113
case "S256":
114
inputChal, err := base64.RawURLEncoding.DecodeString(*authReq.Parameters.CodeChallenge)
115
if err != nil {
116
+
logger.Error("error decoding code challenge", "error", err)
117
return helpers.ServerError(e, nil)
118
}
119
···
131
return helpers.InputError(e, to.StringPtr("code_challenge parameter wasn't provided"))
132
}
133
134
+
repo, err := s.getRepoActorByDid(ctx, *authReq.Sub)
135
if err != nil {
136
helpers.InputError(e, to.StringPtr("unable to find actor"))
137
}
···
162
return err
163
}
164
165
+
if err := s.db.Create(ctx, &provider.OauthToken{
166
ClientId: authReq.ClientId,
167
ClientAuth: *clientAuth,
168
Parameters: authReq.Parameters,
···
174
RefreshToken: refreshToken,
175
Ip: authReq.Ip,
176
}, nil).Error; err != nil {
177
+
logger.Error("error creating token in db", "error", err)
178
return helpers.ServerError(e, nil)
179
}
180
···
202
}
203
204
var oauthToken provider.OauthToken
205
+
if err := s.db.Raw(ctx, "SELECT * FROM oauth_tokens WHERE refresh_token = ?", nil, req.RefreshToken).Scan(&oauthToken).Error; err != nil {
206
+
logger.Error("error finding oauth token by refresh token", "error", err, "refresh_token", req.RefreshToken)
207
return helpers.ServerError(e, nil)
208
}
209
···
260
return err
261
}
262
263
+
if err := s.db.Exec(ctx, "UPDATE oauth_tokens SET token = ?, refresh_token = ?, expires_at = ?, updated_at = ? WHERE refresh_token = ?", nil, accessString, nextRefreshToken, eat, now, *req.RefreshToken).Error; err != nil {
264
+
logger.Error("error updating token", "error", err)
265
return helpers.ServerError(e, nil)
266
}
267
+21
-8
server/handle_proxy.go
+21
-8
server/handle_proxy.go
···
47
}
48
49
func (s *Server) handleProxy(e echo.Context) error {
50
-
lgr := s.logger.With("handler", "handleProxy")
51
52
repo, isAuthed := e.Get("repo").(*models.RepoActor)
53
···
58
59
endpoint, svcDid, err := s.getAtprotoProxyEndpointFromRequest(e)
60
if err != nil {
61
-
lgr.Error("could not get atproto proxy", "error", err)
62
return helpers.ServerError(e, nil)
63
}
64
···
90
}
91
hj, err := json.Marshal(header)
92
if err != nil {
93
-
lgr.Error("error marshaling header", "error", err)
94
return helpers.ServerError(e, nil)
95
}
96
97
encheader := strings.TrimRight(base64.RawURLEncoding.EncodeToString(hj), "=")
98
99
payload := map[string]any{
100
"iss": repo.Repo.Did,
101
-
"aud": svcDid,
102
-
"lxm": pts[2],
103
"jti": uuid.NewString(),
104
"exp": time.Now().Add(1 * time.Minute).UTC().Unix(),
105
}
106
pj, err := json.Marshal(payload)
107
if err != nil {
108
-
lgr.Error("error marashaling payload", "error", err)
109
return helpers.ServerError(e, nil)
110
}
111
···
116
117
sk, err := secp256k1secec.NewPrivateKey(repo.SigningKey)
118
if err != nil {
119
-
lgr.Error("can't load private key", "error", err)
120
return err
121
}
122
123
R, S, _, err := sk.SignRaw(rand.Reader, hash[:])
124
if err != nil {
125
-
lgr.Error("error signing", "error", err)
126
}
127
128
rBytes := R.Bytes()
···
47
}
48
49
func (s *Server) handleProxy(e echo.Context) error {
50
+
logger := s.logger.With("handler", "handleProxy")
51
52
repo, isAuthed := e.Get("repo").(*models.RepoActor)
53
···
58
59
endpoint, svcDid, err := s.getAtprotoProxyEndpointFromRequest(e)
60
if err != nil {
61
+
logger.Error("could not get atproto proxy", "error", err)
62
return helpers.ServerError(e, nil)
63
}
64
···
90
}
91
hj, err := json.Marshal(header)
92
if err != nil {
93
+
logger.Error("error marshaling header", "error", err)
94
return helpers.ServerError(e, nil)
95
}
96
97
encheader := strings.TrimRight(base64.RawURLEncoding.EncodeToString(hj), "=")
98
99
+
// When proxying app.bsky.feed.getFeed the token is actually issued for the
100
+
// underlying feed generator and the app view passes it on. This allows the
101
+
// getFeed implementation to pass in the desired lxm and aud for the token
102
+
// and then just delegate to the general proxying logic
103
+
lxm, proxyTokenLxmExists := e.Get("proxyTokenLxm").(string)
104
+
if !proxyTokenLxmExists || lxm == "" {
105
+
lxm = pts[2]
106
+
}
107
+
aud, proxyTokenAudExists := e.Get("proxyTokenAud").(string)
108
+
if !proxyTokenAudExists || aud == "" {
109
+
aud = svcDid
110
+
}
111
+
112
payload := map[string]any{
113
"iss": repo.Repo.Did,
114
+
"aud": aud,
115
+
"lxm": lxm,
116
"jti": uuid.NewString(),
117
"exp": time.Now().Add(1 * time.Minute).UTC().Unix(),
118
}
119
pj, err := json.Marshal(payload)
120
if err != nil {
121
+
logger.Error("error marashaling payload", "error", err)
122
return helpers.ServerError(e, nil)
123
}
124
···
129
130
sk, err := secp256k1secec.NewPrivateKey(repo.SigningKey)
131
if err != nil {
132
+
logger.Error("can't load private key", "error", err)
133
return err
134
}
135
136
R, S, _, err := sk.SignRaw(rand.Reader, hash[:])
137
if err != nil {
138
+
logger.Error("error signing", "error", err)
139
}
140
141
rBytes := R.Bytes()
+35
server/handle_proxy_get_feed.go
+35
server/handle_proxy_get_feed.go
···
···
1
+
package server
2
+
3
+
import (
4
+
"github.com/Azure/go-autorest/autorest/to"
5
+
"github.com/bluesky-social/indigo/api/atproto"
6
+
"github.com/bluesky-social/indigo/api/bsky"
7
+
"github.com/bluesky-social/indigo/atproto/syntax"
8
+
"github.com/bluesky-social/indigo/xrpc"
9
+
"github.com/haileyok/cocoon/internal/helpers"
10
+
"github.com/labstack/echo/v4"
11
+
)
12
+
13
+
func (s *Server) handleProxyBskyFeedGetFeed(e echo.Context) error {
14
+
feedUri, err := syntax.ParseATURI(e.QueryParam("feed"))
15
+
if err != nil {
16
+
return helpers.InputError(e, to.StringPtr("invalid feed uri"))
17
+
}
18
+
19
+
appViewEndpoint, _, err := s.getAtprotoProxyEndpointFromRequest(e)
20
+
if err != nil {
21
+
e.Logger().Error("could not get atproto proxy", "error", err)
22
+
return helpers.ServerError(e, nil)
23
+
}
24
+
25
+
appViewClient := xrpc.Client{
26
+
Host: appViewEndpoint,
27
+
}
28
+
feedRecord, err := atproto.RepoGetRecord(e.Request().Context(), &appViewClient, "", feedUri.Collection().String(), feedUri.Authority().String(), feedUri.RecordKey().String())
29
+
feedGeneratorDid := feedRecord.Value.Val.(*bsky.FeedGenerator).Did
30
+
31
+
e.Set("proxyTokenLxm", "app.bsky.feed.getFeedSkeleton")
32
+
e.Set("proxyTokenAud", feedGeneratorDid)
33
+
34
+
return s.handleProxy(e)
35
+
}
+14
-11
server/handle_repo_apply_writes.go
+14
-11
server/handle_repo_apply_writes.go
···
6
"github.com/labstack/echo/v4"
7
)
8
9
-
type ComAtprotoRepoApplyWritesRequest struct {
10
Repo string `json:"repo" validate:"required,atproto-did"`
11
Validate *bool `json:"bool,omitempty"`
12
Writes []ComAtprotoRepoApplyWritesItem `json:"writes"`
···
20
Value *MarshalableMap `json:"value,omitempty"`
21
}
22
23
-
type ComAtprotoRepoApplyWritesResponse struct {
24
Commit RepoCommit `json:"commit"`
25
Results []ApplyWriteResult `json:"results"`
26
}
27
28
func (s *Server) handleApplyWrites(e echo.Context) error {
29
-
repo := e.Get("repo").(*models.RepoActor)
30
31
-
var req ComAtprotoRepoApplyWritesRequest
32
if err := e.Bind(&req); err != nil {
33
-
s.logger.Error("error binding", "error", err)
34
return helpers.ServerError(e, nil)
35
}
36
37
if err := e.Validate(req); err != nil {
38
-
s.logger.Error("error validating", "error", err)
39
return helpers.InputError(e, nil)
40
}
41
42
if repo.Repo.Did != req.Repo {
43
-
s.logger.Warn("mismatched repo/auth")
44
return helpers.InputError(e, nil)
45
}
46
47
-
ops := []Op{}
48
for _, item := range req.Writes {
49
ops = append(ops, Op{
50
Type: OpType(item.Type),
···
54
})
55
}
56
57
-
results, err := s.repoman.applyWrites(repo.Repo, ops, req.SwapCommit)
58
if err != nil {
59
-
s.logger.Error("error applying writes", "error", err)
60
return helpers.ServerError(e, nil)
61
}
62
···
66
results[i].Commit = nil
67
}
68
69
-
return e.JSON(200, ComAtprotoRepoApplyWritesResponse{
70
Commit: commit,
71
Results: results,
72
})
···
6
"github.com/labstack/echo/v4"
7
)
8
9
+
type ComAtprotoRepoApplyWritesInput struct {
10
Repo string `json:"repo" validate:"required,atproto-did"`
11
Validate *bool `json:"bool,omitempty"`
12
Writes []ComAtprotoRepoApplyWritesItem `json:"writes"`
···
20
Value *MarshalableMap `json:"value,omitempty"`
21
}
22
23
+
type ComAtprotoRepoApplyWritesOutput struct {
24
Commit RepoCommit `json:"commit"`
25
Results []ApplyWriteResult `json:"results"`
26
}
27
28
func (s *Server) handleApplyWrites(e echo.Context) error {
29
+
ctx := e.Request().Context()
30
+
logger := s.logger.With("name", "handleRepoApplyWrites")
31
32
+
var req ComAtprotoRepoApplyWritesInput
33
if err := e.Bind(&req); err != nil {
34
+
logger.Error("error binding", "error", err)
35
return helpers.ServerError(e, nil)
36
}
37
38
if err := e.Validate(req); err != nil {
39
+
logger.Error("error validating", "error", err)
40
return helpers.InputError(e, nil)
41
}
42
43
+
repo := e.Get("repo").(*models.RepoActor)
44
+
45
if repo.Repo.Did != req.Repo {
46
+
logger.Warn("mismatched repo/auth")
47
return helpers.InputError(e, nil)
48
}
49
50
+
ops := make([]Op, 0, len(req.Writes))
51
for _, item := range req.Writes {
52
ops = append(ops, Op{
53
Type: OpType(item.Type),
···
57
})
58
}
59
60
+
results, err := s.repoman.applyWrites(ctx, repo.Repo, ops, req.SwapCommit)
61
if err != nil {
62
+
logger.Error("error applying writes", "error", err)
63
return helpers.ServerError(e, nil)
64
}
65
···
69
results[i].Commit = nil
70
}
71
72
+
return e.JSON(200, ComAtprotoRepoApplyWritesOutput{
73
Commit: commit,
74
Results: results,
75
})
+10
-7
server/handle_repo_create_record.go
+10
-7
server/handle_repo_create_record.go
···
6
"github.com/labstack/echo/v4"
7
)
8
9
-
type ComAtprotoRepoCreateRecordRequest struct {
10
Repo string `json:"repo" validate:"required,atproto-did"`
11
Collection string `json:"collection" validate:"required,atproto-nsid"`
12
Rkey *string `json:"rkey,omitempty"`
···
17
}
18
19
func (s *Server) handleCreateRecord(e echo.Context) error {
20
repo := e.Get("repo").(*models.RepoActor)
21
22
-
var req ComAtprotoRepoCreateRecordRequest
23
if err := e.Bind(&req); err != nil {
24
-
s.logger.Error("error binding", "error", err)
25
return helpers.ServerError(e, nil)
26
}
27
28
if err := e.Validate(req); err != nil {
29
-
s.logger.Error("error validating", "error", err)
30
return helpers.InputError(e, nil)
31
}
32
33
if repo.Repo.Did != req.Repo {
34
-
s.logger.Warn("mismatched repo/auth")
35
return helpers.InputError(e, nil)
36
}
37
···
40
optype = OpTypeUpdate
41
}
42
43
-
results, err := s.repoman.applyWrites(repo.Repo, []Op{
44
{
45
Type: optype,
46
Collection: req.Collection,
···
51
},
52
}, req.SwapCommit)
53
if err != nil {
54
-
s.logger.Error("error applying writes", "error", err)
55
return helpers.ServerError(e, nil)
56
}
57
···
6
"github.com/labstack/echo/v4"
7
)
8
9
+
type ComAtprotoRepoCreateRecordInput struct {
10
Repo string `json:"repo" validate:"required,atproto-did"`
11
Collection string `json:"collection" validate:"required,atproto-nsid"`
12
Rkey *string `json:"rkey,omitempty"`
···
17
}
18
19
func (s *Server) handleCreateRecord(e echo.Context) error {
20
+
ctx := e.Request().Context()
21
+
logger := s.logger.With("name", "handleCreateRecord")
22
+
23
repo := e.Get("repo").(*models.RepoActor)
24
25
+
var req ComAtprotoRepoCreateRecordInput
26
if err := e.Bind(&req); err != nil {
27
+
logger.Error("error binding", "error", err)
28
return helpers.ServerError(e, nil)
29
}
30
31
if err := e.Validate(req); err != nil {
32
+
logger.Error("error validating", "error", err)
33
return helpers.InputError(e, nil)
34
}
35
36
if repo.Repo.Did != req.Repo {
37
+
logger.Warn("mismatched repo/auth")
38
return helpers.InputError(e, nil)
39
}
40
···
43
optype = OpTypeUpdate
44
}
45
46
+
results, err := s.repoman.applyWrites(ctx, repo.Repo, []Op{
47
{
48
Type: optype,
49
Collection: req.Collection,
···
54
},
55
}, req.SwapCommit)
56
if err != nil {
57
+
logger.Error("error applying writes", "error", err)
58
return helpers.ServerError(e, nil)
59
}
60
+10
-7
server/handle_repo_delete_record.go
+10
-7
server/handle_repo_delete_record.go
···
6
"github.com/labstack/echo/v4"
7
)
8
9
-
type ComAtprotoRepoDeleteRecordRequest struct {
10
Repo string `json:"repo" validate:"required,atproto-did"`
11
Collection string `json:"collection" validate:"required,atproto-nsid"`
12
Rkey string `json:"rkey" validate:"required,atproto-rkey"`
···
15
}
16
17
func (s *Server) handleDeleteRecord(e echo.Context) error {
18
repo := e.Get("repo").(*models.RepoActor)
19
20
-
var req ComAtprotoRepoDeleteRecordRequest
21
if err := e.Bind(&req); err != nil {
22
-
s.logger.Error("error binding", "error", err)
23
return helpers.ServerError(e, nil)
24
}
25
26
if err := e.Validate(req); err != nil {
27
-
s.logger.Error("error validating", "error", err)
28
return helpers.InputError(e, nil)
29
}
30
31
if repo.Repo.Did != req.Repo {
32
-
s.logger.Warn("mismatched repo/auth")
33
return helpers.InputError(e, nil)
34
}
35
36
-
results, err := s.repoman.applyWrites(repo.Repo, []Op{
37
{
38
Type: OpTypeDelete,
39
Collection: req.Collection,
···
42
},
43
}, req.SwapCommit)
44
if err != nil {
45
-
s.logger.Error("error applying writes", "error", err)
46
return helpers.ServerError(e, nil)
47
}
48
···
6
"github.com/labstack/echo/v4"
7
)
8
9
+
type ComAtprotoRepoDeleteRecordInput struct {
10
Repo string `json:"repo" validate:"required,atproto-did"`
11
Collection string `json:"collection" validate:"required,atproto-nsid"`
12
Rkey string `json:"rkey" validate:"required,atproto-rkey"`
···
15
}
16
17
func (s *Server) handleDeleteRecord(e echo.Context) error {
18
+
ctx := e.Request().Context()
19
+
logger := s.logger.With("name", "handleDeleteRecord")
20
+
21
repo := e.Get("repo").(*models.RepoActor)
22
23
+
var req ComAtprotoRepoDeleteRecordInput
24
if err := e.Bind(&req); err != nil {
25
+
logger.Error("error binding", "error", err)
26
return helpers.ServerError(e, nil)
27
}
28
29
if err := e.Validate(req); err != nil {
30
+
logger.Error("error validating", "error", err)
31
return helpers.InputError(e, nil)
32
}
33
34
if repo.Repo.Did != req.Repo {
35
+
logger.Warn("mismatched repo/auth")
36
return helpers.InputError(e, nil)
37
}
38
39
+
results, err := s.repoman.applyWrites(ctx, repo.Repo, []Op{
40
{
41
Type: OpTypeDelete,
42
Collection: req.Collection,
···
45
},
46
}, req.SwapCommit)
47
if err != nil {
48
+
logger.Error("error applying writes", "error", err)
49
return helpers.ServerError(e, nil)
50
}
51
+8
-5
server/handle_repo_describe_repo.go
+8
-5
server/handle_repo_describe_repo.go
···
20
}
21
22
func (s *Server) handleDescribeRepo(e echo.Context) error {
23
did := e.QueryParam("repo")
24
-
repo, err := s.getRepoActorByDid(did)
25
if err != nil {
26
if err == gorm.ErrRecordNotFound {
27
return helpers.InputError(e, to.StringPtr("RepoNotFound"))
28
}
29
30
-
s.logger.Error("error looking up repo", "error", err)
31
return helpers.ServerError(e, nil)
32
}
33
···
35
36
diddoc, err := s.passport.FetchDoc(e.Request().Context(), repo.Repo.Did)
37
if err != nil {
38
-
s.logger.Error("error fetching diddoc", "error", err)
39
return helpers.ServerError(e, nil)
40
}
41
···
64
}
65
66
var records []models.Record
67
-
if err := s.db.Raw("SELECT DISTINCT(nsid) FROM records WHERE did = ?", nil, repo.Repo.Did).Scan(&records).Error; err != nil {
68
-
s.logger.Error("error getting collections", "error", err)
69
return helpers.ServerError(e, nil)
70
}
71
···
20
}
21
22
func (s *Server) handleDescribeRepo(e echo.Context) error {
23
+
ctx := e.Request().Context()
24
+
logger := s.logger.With("name", "handleDescribeRepo")
25
+
26
did := e.QueryParam("repo")
27
+
repo, err := s.getRepoActorByDid(ctx, did)
28
if err != nil {
29
if err == gorm.ErrRecordNotFound {
30
return helpers.InputError(e, to.StringPtr("RepoNotFound"))
31
}
32
33
+
logger.Error("error looking up repo", "error", err)
34
return helpers.ServerError(e, nil)
35
}
36
···
38
39
diddoc, err := s.passport.FetchDoc(e.Request().Context(), repo.Repo.Did)
40
if err != nil {
41
+
logger.Error("error fetching diddoc", "error", err)
42
return helpers.ServerError(e, nil)
43
}
44
···
67
}
68
69
var records []models.Record
70
+
if err := s.db.Raw(ctx, "SELECT DISTINCT(nsid) FROM records WHERE did = ?", nil, repo.Repo.Did).Scan(&records).Error; err != nil {
71
+
logger.Error("error getting collections", "error", err)
72
return helpers.ServerError(e, nil)
73
}
74
+3
-1
server/handle_repo_get_record.go
+3
-1
server/handle_repo_get_record.go
···
14
}
15
16
func (s *Server) handleRepoGetRecord(e echo.Context) error {
17
repo := e.QueryParam("repo")
18
collection := e.QueryParam("collection")
19
rkey := e.QueryParam("rkey")
···
32
}
33
34
var record models.Record
35
-
if err := s.db.Raw("SELECT * FROM records WHERE did = ? AND nsid = ? AND rkey = ?"+cidquery, nil, params...).Scan(&record).Error; err != nil {
36
// TODO: handle error nicely
37
return err
38
}
···
14
}
15
16
func (s *Server) handleRepoGetRecord(e echo.Context) error {
17
+
ctx := e.Request().Context()
18
+
19
repo := e.QueryParam("repo")
20
collection := e.QueryParam("collection")
21
rkey := e.QueryParam("rkey")
···
34
}
35
36
var record models.Record
37
+
if err := s.db.Raw(ctx, "SELECT * FROM records WHERE did = ? AND nsid = ? AND rkey = ?"+cidquery, nil, params...).Scan(&record).Error; err != nil {
38
// TODO: handle error nicely
39
return err
40
}
+115
server/handle_repo_list_missing_blobs.go
+115
server/handle_repo_list_missing_blobs.go
···
···
1
+
package server
2
+
3
+
import (
4
+
"fmt"
5
+
"strconv"
6
+
7
+
"github.com/bluesky-social/indigo/atproto/atdata"
8
+
"github.com/haileyok/cocoon/internal/helpers"
9
+
"github.com/haileyok/cocoon/models"
10
+
"github.com/ipfs/go-cid"
11
+
"github.com/labstack/echo/v4"
12
+
)
13
+
14
+
type ComAtprotoRepoListMissingBlobsResponse struct {
15
+
Cursor *string `json:"cursor,omitempty"`
16
+
Blobs []ComAtprotoRepoListMissingBlobsRecordBlob `json:"blobs"`
17
+
}
18
+
19
+
type ComAtprotoRepoListMissingBlobsRecordBlob struct {
20
+
Cid string `json:"cid"`
21
+
RecordUri string `json:"recordUri"`
22
+
}
23
+
24
+
func (s *Server) handleListMissingBlobs(e echo.Context) error {
25
+
ctx := e.Request().Context()
26
+
logger := s.logger.With("name", "handleListMissingBlos")
27
+
28
+
urepo := e.Get("repo").(*models.RepoActor)
29
+
30
+
limitStr := e.QueryParam("limit")
31
+
cursor := e.QueryParam("cursor")
32
+
33
+
limit := 500
34
+
if limitStr != "" {
35
+
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 1000 {
36
+
limit = l
37
+
}
38
+
}
39
+
40
+
var records []models.Record
41
+
if err := s.db.Raw(ctx, "SELECT * FROM records WHERE did = ?", nil, urepo.Repo.Did).Scan(&records).Error; err != nil {
42
+
logger.Error("failed to get records for listMissingBlobs", "error", err)
43
+
return helpers.ServerError(e, nil)
44
+
}
45
+
46
+
type blobRef struct {
47
+
cid cid.Cid
48
+
recordUri string
49
+
}
50
+
var allBlobRefs []blobRef
51
+
52
+
for _, rec := range records {
53
+
blobs := getBlobsFromRecord(rec.Value)
54
+
recordUri := fmt.Sprintf("at://%s/%s/%s", urepo.Repo.Did, rec.Nsid, rec.Rkey)
55
+
for _, b := range blobs {
56
+
allBlobRefs = append(allBlobRefs, blobRef{cid: cid.Cid(b.Ref), recordUri: recordUri})
57
+
}
58
+
}
59
+
60
+
missingBlobs := make([]ComAtprotoRepoListMissingBlobsRecordBlob, 0)
61
+
seenCids := make(map[string]bool)
62
+
63
+
for _, ref := range allBlobRefs {
64
+
cidStr := ref.cid.String()
65
+
66
+
if seenCids[cidStr] {
67
+
continue
68
+
}
69
+
70
+
if cursor != "" && cidStr <= cursor {
71
+
continue
72
+
}
73
+
74
+
var count int64
75
+
if err := s.db.Raw(ctx, "SELECT COUNT(*) FROM blobs WHERE did = ? AND cid = ?", nil, urepo.Repo.Did, ref.cid.Bytes()).Scan(&count).Error; err != nil {
76
+
continue
77
+
}
78
+
79
+
if count == 0 {
80
+
missingBlobs = append(missingBlobs, ComAtprotoRepoListMissingBlobsRecordBlob{
81
+
Cid: cidStr,
82
+
RecordUri: ref.recordUri,
83
+
})
84
+
seenCids[cidStr] = true
85
+
86
+
if len(missingBlobs) >= limit {
87
+
break
88
+
}
89
+
}
90
+
}
91
+
92
+
var nextCursor *string
93
+
if len(missingBlobs) > 0 && len(missingBlobs) >= limit {
94
+
lastCid := missingBlobs[len(missingBlobs)-1].Cid
95
+
nextCursor = &lastCid
96
+
}
97
+
98
+
return e.JSON(200, ComAtprotoRepoListMissingBlobsResponse{
99
+
Cursor: nextCursor,
100
+
Blobs: missingBlobs,
101
+
})
102
+
}
103
+
104
+
func getBlobsFromRecord(data []byte) []atdata.Blob {
105
+
if len(data) == 0 {
106
+
return nil
107
+
}
108
+
109
+
decoded, err := atdata.UnmarshalCBOR(data)
110
+
if err != nil {
111
+
return nil
112
+
}
113
+
114
+
return atdata.ExtractBlobs(decoded)
115
+
}
+7
-4
server/handle_repo_list_records.go
+7
-4
server/handle_repo_list_records.go
···
46
}
47
48
func (s *Server) handleListRecords(e echo.Context) error {
49
var req ComAtprotoRepoListRecordsRequest
50
if err := e.Bind(&req); err != nil {
51
-
s.logger.Error("could not bind list records request", "error", err)
52
return helpers.ServerError(e, nil)
53
}
54
···
78
79
did := req.Repo
80
if _, err := syntax.ParseDID(did); err != nil {
81
-
actor, err := s.getActorByHandle(req.Repo)
82
if err != nil {
83
return helpers.InputError(e, to.StringPtr("RepoNotFound"))
84
}
···
93
params = append(params, limit)
94
95
var records []models.Record
96
-
if err := s.db.Raw("SELECT * FROM records WHERE did = ? AND nsid = ? "+cursorquery+" ORDER BY created_at "+sort+" limit ?", nil, params...).Scan(&records).Error; err != nil {
97
-
s.logger.Error("error getting records", "error", err)
98
return helpers.ServerError(e, nil)
99
}
100
···
46
}
47
48
func (s *Server) handleListRecords(e echo.Context) error {
49
+
ctx := e.Request().Context()
50
+
logger := s.logger.With("name", "handleListRecords")
51
+
52
var req ComAtprotoRepoListRecordsRequest
53
if err := e.Bind(&req); err != nil {
54
+
logger.Error("could not bind list records request", "error", err)
55
return helpers.ServerError(e, nil)
56
}
57
···
81
82
did := req.Repo
83
if _, err := syntax.ParseDID(did); err != nil {
84
+
actor, err := s.getActorByHandle(ctx, req.Repo)
85
if err != nil {
86
return helpers.InputError(e, to.StringPtr("RepoNotFound"))
87
}
···
96
params = append(params, limit)
97
98
var records []models.Record
99
+
if err := s.db.Raw(ctx, "SELECT * FROM records WHERE did = ? AND nsid = ? "+cursorquery+" ORDER BY created_at "+sort+" limit ?", nil, params...).Scan(&records).Error; err != nil {
100
+
logger.Error("error getting records", "error", err)
101
return helpers.ServerError(e, nil)
102
}
103
+3
-1
server/handle_repo_list_repos.go
+3
-1
server/handle_repo_list_repos.go
···
21
22
// TODO: paginate this bitch
23
func (s *Server) handleListRepos(e echo.Context) error {
24
+
ctx := e.Request().Context()
25
+
26
var repos []models.Repo
27
+
if err := s.db.Raw(ctx, "SELECT * FROM repos ORDER BY created_at DESC LIMIT 500", nil).Scan(&repos).Error; err != nil {
28
return err
29
}
30
+10
-7
server/handle_repo_put_record.go
+10
-7
server/handle_repo_put_record.go
···
6
"github.com/labstack/echo/v4"
7
)
8
9
-
type ComAtprotoRepoPutRecordRequest struct {
10
Repo string `json:"repo" validate:"required,atproto-did"`
11
Collection string `json:"collection" validate:"required,atproto-nsid"`
12
Rkey string `json:"rkey" validate:"required,atproto-rkey"`
···
17
}
18
19
func (s *Server) handlePutRecord(e echo.Context) error {
20
repo := e.Get("repo").(*models.RepoActor)
21
22
-
var req ComAtprotoRepoPutRecordRequest
23
if err := e.Bind(&req); err != nil {
24
-
s.logger.Error("error binding", "error", err)
25
return helpers.ServerError(e, nil)
26
}
27
28
if err := e.Validate(req); err != nil {
29
-
s.logger.Error("error validating", "error", err)
30
return helpers.InputError(e, nil)
31
}
32
33
if repo.Repo.Did != req.Repo {
34
-
s.logger.Warn("mismatched repo/auth")
35
return helpers.InputError(e, nil)
36
}
37
···
40
optype = OpTypeUpdate
41
}
42
43
-
results, err := s.repoman.applyWrites(repo.Repo, []Op{
44
{
45
Type: optype,
46
Collection: req.Collection,
···
51
},
52
}, req.SwapCommit)
53
if err != nil {
54
-
s.logger.Error("error applying writes", "error", err)
55
return helpers.ServerError(e, nil)
56
}
57
···
6
"github.com/labstack/echo/v4"
7
)
8
9
+
type ComAtprotoRepoPutRecordInput struct {
10
Repo string `json:"repo" validate:"required,atproto-did"`
11
Collection string `json:"collection" validate:"required,atproto-nsid"`
12
Rkey string `json:"rkey" validate:"required,atproto-rkey"`
···
17
}
18
19
func (s *Server) handlePutRecord(e echo.Context) error {
20
+
ctx := e.Request().Context()
21
+
logger := s.logger.With("name", "handlePutRecord")
22
+
23
repo := e.Get("repo").(*models.RepoActor)
24
25
+
var req ComAtprotoRepoPutRecordInput
26
if err := e.Bind(&req); err != nil {
27
+
logger.Error("error binding", "error", err)
28
return helpers.ServerError(e, nil)
29
}
30
31
if err := e.Validate(req); err != nil {
32
+
logger.Error("error validating", "error", err)
33
return helpers.InputError(e, nil)
34
}
35
36
if repo.Repo.Did != req.Repo {
37
+
logger.Warn("mismatched repo/auth")
38
return helpers.InputError(e, nil)
39
}
40
···
43
optype = OpTypeUpdate
44
}
45
46
+
results, err := s.repoman.applyWrites(ctx, repo.Repo, []Op{
47
{
48
Type: optype,
49
Collection: req.Collection,
···
54
},
55
}, req.SwapCommit)
56
if err != nil {
57
+
logger.Error("error applying writes", "error", err)
58
return helpers.ServerError(e, nil)
59
}
60
+13
-10
server/handle_repo_upload_blob.go
+13
-10
server/handle_repo_upload_blob.go
···
32
}
33
34
func (s *Server) handleRepoUploadBlob(e echo.Context) error {
35
urepo := e.Get("repo").(*models.RepoActor)
36
37
mime := e.Request().Header.Get("content-type")
···
51
Storage: storage,
52
}
53
54
-
if err := s.db.Create(&blob, nil).Error; err != nil {
55
-
s.logger.Error("error creating new blob in db", "error", err)
56
return helpers.ServerError(e, nil)
57
}
58
···
69
break
70
}
71
} else if err != nil && err != io.ErrUnexpectedEOF {
72
-
s.logger.Error("error reading blob", "error", err)
73
return helpers.ServerError(e, nil)
74
}
75
···
84
Data: data,
85
}
86
87
-
if err := s.db.Create(&blobPart, nil).Error; err != nil {
88
-
s.logger.Error("error adding blob part to db", "error", err)
89
return helpers.ServerError(e, nil)
90
}
91
}
···
98
99
c, err := cid.NewPrefixV1(cid.Raw, multihash.SHA2_256).Sum(fulldata.Bytes())
100
if err != nil {
101
-
s.logger.Error("error creating cid prefix", "error", err)
102
return helpers.ServerError(e, nil)
103
}
104
···
115
116
sess, err := session.NewSession(config)
117
if err != nil {
118
-
s.logger.Error("error creating aws session", "error", err)
119
return helpers.ServerError(e, nil)
120
}
121
···
126
Key: aws.String(fmt.Sprintf("blobs/%s/%s", urepo.Repo.Did, c.String())),
127
Body: bytes.NewReader(fulldata.Bytes()),
128
}); err != nil {
129
-
s.logger.Error("error uploading blob to s3", "error", err)
130
return helpers.ServerError(e, nil)
131
}
132
}
133
134
-
if err := s.db.Exec("UPDATE blobs SET cid = ? WHERE id = ?", nil, c.Bytes(), blob.ID).Error; err != nil {
135
// there should probably be somme handling here if this fails...
136
-
s.logger.Error("error updating blob", "error", err)
137
return helpers.ServerError(e, nil)
138
}
139
···
32
}
33
34
func (s *Server) handleRepoUploadBlob(e echo.Context) error {
35
+
ctx := e.Request().Context()
36
+
logger := s.logger.With("name", "handleRepoUploadBlob")
37
+
38
urepo := e.Get("repo").(*models.RepoActor)
39
40
mime := e.Request().Header.Get("content-type")
···
54
Storage: storage,
55
}
56
57
+
if err := s.db.Create(ctx, &blob, nil).Error; err != nil {
58
+
logger.Error("error creating new blob in db", "error", err)
59
return helpers.ServerError(e, nil)
60
}
61
···
72
break
73
}
74
} else if err != nil && err != io.ErrUnexpectedEOF {
75
+
logger.Error("error reading blob", "error", err)
76
return helpers.ServerError(e, nil)
77
}
78
···
87
Data: data,
88
}
89
90
+
if err := s.db.Create(ctx, &blobPart, nil).Error; err != nil {
91
+
logger.Error("error adding blob part to db", "error", err)
92
return helpers.ServerError(e, nil)
93
}
94
}
···
101
102
c, err := cid.NewPrefixV1(cid.Raw, multihash.SHA2_256).Sum(fulldata.Bytes())
103
if err != nil {
104
+
logger.Error("error creating cid prefix", "error", err)
105
return helpers.ServerError(e, nil)
106
}
107
···
118
119
sess, err := session.NewSession(config)
120
if err != nil {
121
+
logger.Error("error creating aws session", "error", err)
122
return helpers.ServerError(e, nil)
123
}
124
···
129
Key: aws.String(fmt.Sprintf("blobs/%s/%s", urepo.Repo.Did, c.String())),
130
Body: bytes.NewReader(fulldata.Bytes()),
131
}); err != nil {
132
+
logger.Error("error uploading blob to s3", "error", err)
133
return helpers.ServerError(e, nil)
134
}
135
}
136
137
+
if err := s.db.Exec(ctx, "UPDATE blobs SET cid = ? WHERE id = ?", nil, c.Bytes(), blob.ID).Error; err != nil {
138
// there should probably be somme handling here if this fails...
139
+
logger.Error("error updating blob", "error", err)
140
return helpers.ServerError(e, nil)
141
}
142
+6
-3
server/handle_server_activate_account.go
+6
-3
server/handle_server_activate_account.go
···
18
}
19
20
func (s *Server) handleServerActivateAccount(e echo.Context) error {
21
var req ComAtprotoServerDeactivateAccountRequest
22
if err := e.Bind(&req); err != nil {
23
-
s.logger.Error("error binding", "error", err)
24
return helpers.ServerError(e, nil)
25
}
26
27
urepo := e.Get("repo").(*models.RepoActor)
28
29
-
if err := s.db.Exec("UPDATE repos SET deactivated = ? WHERE did = ?", nil, false, urepo.Repo.Did).Error; err != nil {
30
-
s.logger.Error("error updating account status to deactivated", "error", err)
31
return helpers.ServerError(e, nil)
32
}
33
···
18
}
19
20
func (s *Server) handleServerActivateAccount(e echo.Context) error {
21
+
ctx := e.Request().Context()
22
+
logger := s.logger.With("name", "handleServerActivateAccount")
23
+
24
var req ComAtprotoServerDeactivateAccountRequest
25
if err := e.Bind(&req); err != nil {
26
+
logger.Error("error binding", "error", err)
27
return helpers.ServerError(e, nil)
28
}
29
30
urepo := e.Get("repo").(*models.RepoActor)
31
32
+
if err := s.db.Exec(ctx, "UPDATE repos SET deactivated = ? WHERE did = ?", nil, false, urepo.Repo.Did).Error; err != nil {
33
+
logger.Error("error updating account status to deactivated", "error", err)
34
return helpers.ServerError(e, nil)
35
}
36
+10
-7
server/handle_server_check_account_status.go
+10
-7
server/handle_server_check_account_status.go
···
20
}
21
22
func (s *Server) handleServerCheckAccountStatus(e echo.Context) error {
23
urepo := e.Get("repo").(*models.RepoActor)
24
25
resp := ComAtprotoServerCheckAccountStatusResponse{
···
31
32
rootcid, err := cid.Cast(urepo.Root)
33
if err != nil {
34
-
s.logger.Error("error casting cid", "error", err)
35
return helpers.ServerError(e, nil)
36
}
37
resp.RepoCommit = rootcid.String()
···
41
}
42
43
var blockCtResp CountResp
44
-
if err := s.db.Raw("SELECT COUNT(*) AS ct FROM blocks WHERE did = ?", nil, urepo.Repo.Did).Scan(&blockCtResp).Error; err != nil {
45
-
s.logger.Error("error getting block count", "error", err)
46
return helpers.ServerError(e, nil)
47
}
48
resp.RepoBlocks = blockCtResp.Ct
49
50
var recCtResp CountResp
51
-
if err := s.db.Raw("SELECT COUNT(*) AS ct FROM records WHERE did = ?", nil, urepo.Repo.Did).Scan(&recCtResp).Error; err != nil {
52
-
s.logger.Error("error getting record count", "error", err)
53
return helpers.ServerError(e, nil)
54
}
55
resp.IndexedRecords = recCtResp.Ct
56
57
var blobCtResp CountResp
58
-
if err := s.db.Raw("SELECT COUNT(*) AS ct FROM blobs WHERE did = ?", nil, urepo.Repo.Did).Scan(&blobCtResp).Error; err != nil {
59
-
s.logger.Error("error getting record count", "error", err)
60
return helpers.ServerError(e, nil)
61
}
62
resp.ExpectedBlobs = blobCtResp.Ct
···
20
}
21
22
func (s *Server) handleServerCheckAccountStatus(e echo.Context) error {
23
+
ctx := e.Request().Context()
24
+
logger := s.logger.With("name", "handleServerCheckAccountStatus")
25
+
26
urepo := e.Get("repo").(*models.RepoActor)
27
28
resp := ComAtprotoServerCheckAccountStatusResponse{
···
34
35
rootcid, err := cid.Cast(urepo.Root)
36
if err != nil {
37
+
logger.Error("error casting cid", "error", err)
38
return helpers.ServerError(e, nil)
39
}
40
resp.RepoCommit = rootcid.String()
···
44
}
45
46
var blockCtResp CountResp
47
+
if err := s.db.Raw(ctx, "SELECT COUNT(*) AS ct FROM blocks WHERE did = ?", nil, urepo.Repo.Did).Scan(&blockCtResp).Error; err != nil {
48
+
logger.Error("error getting block count", "error", err)
49
return helpers.ServerError(e, nil)
50
}
51
resp.RepoBlocks = blockCtResp.Ct
52
53
var recCtResp CountResp
54
+
if err := s.db.Raw(ctx, "SELECT COUNT(*) AS ct FROM records WHERE did = ?", nil, urepo.Repo.Did).Scan(&recCtResp).Error; err != nil {
55
+
logger.Error("error getting record count", "error", err)
56
return helpers.ServerError(e, nil)
57
}
58
resp.IndexedRecords = recCtResp.Ct
59
60
var blobCtResp CountResp
61
+
if err := s.db.Raw(ctx, "SELECT COUNT(*) AS ct FROM blobs WHERE did = ?", nil, urepo.Repo.Did).Scan(&blobCtResp).Error; err != nil {
62
+
logger.Error("error getting record count", "error", err)
63
return helpers.ServerError(e, nil)
64
}
65
resp.ExpectedBlobs = blobCtResp.Ct
+6
-3
server/handle_server_confirm_email.go
+6
-3
server/handle_server_confirm_email.go
···
15
}
16
17
func (s *Server) handleServerConfirmEmail(e echo.Context) error {
18
urepo := e.Get("repo").(*models.RepoActor)
19
20
var req ComAtprotoServerConfirmEmailRequest
21
if err := e.Bind(&req); err != nil {
22
-
s.logger.Error("error binding", "error", err)
23
return helpers.ServerError(e, nil)
24
}
25
···
41
42
now := time.Now().UTC()
43
44
-
if err := s.db.Exec("UPDATE repos SET email_verification_code = NULL, email_verification_code_expires_at = NULL, email_confirmed_at = ? WHERE did = ?", nil, now, urepo.Repo.Did).Error; err != nil {
45
-
s.logger.Error("error updating user", "error", err)
46
return helpers.ServerError(e, nil)
47
}
48
···
15
}
16
17
func (s *Server) handleServerConfirmEmail(e echo.Context) error {
18
+
ctx := e.Request().Context()
19
+
logger := s.logger.With("name", "handleServerConfirmEmail")
20
+
21
urepo := e.Get("repo").(*models.RepoActor)
22
23
var req ComAtprotoServerConfirmEmailRequest
24
if err := e.Bind(&req); err != nil {
25
+
logger.Error("error binding", "error", err)
26
return helpers.ServerError(e, nil)
27
}
28
···
44
45
now := time.Now().UTC()
46
47
+
if err := s.db.Exec(ctx, "UPDATE repos SET email_verification_code = NULL, email_verification_code_expires_at = NULL, email_confirmed_at = ? WHERE did = ?", nil, now, urepo.Repo.Did).Error; err != nil {
48
+
logger.Error("error updating user", "error", err)
49
return helpers.ServerError(e, nil)
50
}
51
+76
-41
server/handle_server_create_account.go
+76
-41
server/handle_server_create_account.go
···
25
Handle string `json:"handle" validate:"required,atproto-handle"`
26
Did *string `json:"did" validate:"atproto-did"`
27
Password string `json:"password" validate:"required"`
28
-
InviteCode string `json:"inviteCode" validate:"required"`
29
}
30
31
type ComAtprotoServerCreateAccountResponse struct {
···
36
}
37
38
func (s *Server) handleCreateAccount(e echo.Context) error {
39
var request ComAtprotoServerCreateAccountRequest
40
41
if err := e.Bind(&request); err != nil {
42
-
s.logger.Error("error receiving request", "endpoint", "com.atproto.server.createAccount", "error", err)
43
return helpers.ServerError(e, nil)
44
}
45
46
request.Handle = strings.ToLower(request.Handle)
47
48
if err := e.Validate(request); err != nil {
49
-
s.logger.Error("error validating request", "endpoint", "com.atproto.server.createAccount", "error", err)
50
51
var verr ValidationError
52
if errors.As(err, &verr) {
···
68
}
69
}
70
}
71
-
72
var signupDid string
73
if request.Did != nil {
74
-
signupDid = *request.Did;
75
-
76
token := strings.TrimSpace(strings.Replace(e.Request().Header.Get("authorization"), "Bearer ", "", 1))
77
if token == "" {
78
return helpers.UnauthorizedError(e, to.StringPtr("must authenticate to use an existing did"))
···
80
authDid, err := s.validateServiceAuth(e.Request().Context(), token, "com.atproto.server.createAccount")
81
82
if err != nil {
83
-
s.logger.Warn("error validating authorization token", "endpoint", "com.atproto.server.createAccount", "error", err)
84
return helpers.UnauthorizedError(e, to.StringPtr("invalid authorization token"))
85
}
86
···
90
}
91
92
// see if the handle is already taken
93
-
actor, err := s.getActorByHandle(request.Handle)
94
if err != nil && err != gorm.ErrRecordNotFound {
95
-
s.logger.Error("error looking up handle in db", "endpoint", "com.atproto.server.createAccount", "error", err)
96
return helpers.ServerError(e, nil)
97
}
98
if err == nil && actor.Did != signupDid {
···
104
}
105
106
var ic models.InviteCode
107
-
if err := s.db.Raw("SELECT * FROM invite_codes WHERE code = ?", nil, request.InviteCode).Scan(&ic).Error; err != nil {
108
-
if err == gorm.ErrRecordNotFound {
109
return helpers.InputError(e, to.StringPtr("InvalidInviteCode"))
110
}
111
-
s.logger.Error("error getting invite code from db", "error", err)
112
-
return helpers.ServerError(e, nil)
113
-
}
114
115
-
if ic.RemainingUseCount < 1 {
116
-
return helpers.InputError(e, to.StringPtr("InvalidInviteCode"))
117
}
118
119
// see if the email is already taken
120
-
existingRepo, err := s.getRepoByEmail(request.Email)
121
if err != nil && err != gorm.ErrRecordNotFound {
122
-
s.logger.Error("error looking up email in db", "endpoint", "com.atproto.server.createAccount", "error", err)
123
return helpers.ServerError(e, nil)
124
}
125
if err == nil && existingRepo.Did != signupDid {
···
128
129
// TODO: unsupported domains
130
131
-
k, err := atcrypto.GeneratePrivateKeyK256()
132
-
if err != nil {
133
-
s.logger.Error("error creating signing key", "endpoint", "com.atproto.server.createAccount", "error", err)
134
-
return helpers.ServerError(e, nil)
135
}
136
137
if signupDid == "" {
138
did, op, err := s.plcClient.CreateDID(k, "", request.Handle)
139
if err != nil {
140
-
s.logger.Error("error creating operation", "endpoint", "com.atproto.server.createAccount", "error", err)
141
return helpers.ServerError(e, nil)
142
}
143
144
if err := s.plcClient.SendOperation(e.Request().Context(), did, op); err != nil {
145
-
s.logger.Error("error sending plc op", "endpoint", "com.atproto.server.createAccount", "error", err)
146
return helpers.ServerError(e, nil)
147
}
148
signupDid = did
···
150
151
hashed, err := bcrypt.GenerateFromPassword([]byte(request.Password), 10)
152
if err != nil {
153
-
s.logger.Error("error hashing password", "error", err)
154
return helpers.ServerError(e, nil)
155
}
156
···
169
Handle: request.Handle,
170
}
171
172
-
if err := s.db.Create(&urepo, nil).Error; err != nil {
173
-
s.logger.Error("error inserting new repo", "error", err)
174
return helpers.ServerError(e, nil)
175
}
176
-
177
-
if err := s.db.Create(&actor, nil).Error; err != nil {
178
-
s.logger.Error("error inserting new actor", "error", err)
179
return helpers.ServerError(e, nil)
180
}
181
} else {
182
-
if err := s.db.Save(&actor, nil).Error; err != nil {
183
-
s.logger.Error("error inserting new actor", "error", err)
184
return helpers.ServerError(e, nil)
185
}
186
}
···
191
192
root, rev, err := r.Commit(context.TODO(), urepo.SignFor)
193
if err != nil {
194
-
s.logger.Error("error committing", "error", err)
195
return helpers.ServerError(e, nil)
196
}
197
198
if err := s.UpdateRepo(context.TODO(), urepo.Did, root, rev); err != nil {
199
-
s.logger.Error("error updating repo after commit", "error", err)
200
return helpers.ServerError(e, nil)
201
}
202
···
210
})
211
}
212
213
-
if err := s.db.Raw("UPDATE invite_codes SET remaining_use_count = remaining_use_count - 1 WHERE code = ?", nil, request.InviteCode).Scan(&ic).Error; err != nil {
214
-
s.logger.Error("error decrementing use count", "error", err)
215
-
return helpers.ServerError(e, nil)
216
}
217
218
-
sess, err := s.createSession(&urepo)
219
if err != nil {
220
-
s.logger.Error("error creating new session", "error", err)
221
return helpers.ServerError(e, nil)
222
}
223
224
go func() {
225
if err := s.sendEmailVerification(urepo.Email, actor.Handle, *urepo.EmailVerificationCode); err != nil {
226
-
s.logger.Error("error sending email verification email", "error", err)
227
}
228
if err := s.sendWelcomeMail(urepo.Email, actor.Handle); err != nil {
229
-
s.logger.Error("error sending welcome email", "error", err)
230
}
231
}()
232
···
25
Handle string `json:"handle" validate:"required,atproto-handle"`
26
Did *string `json:"did" validate:"atproto-did"`
27
Password string `json:"password" validate:"required"`
28
+
InviteCode string `json:"inviteCode" validate:"omitempty"`
29
}
30
31
type ComAtprotoServerCreateAccountResponse struct {
···
36
}
37
38
func (s *Server) handleCreateAccount(e echo.Context) error {
39
+
ctx := e.Request().Context()
40
+
logger := s.logger.With("name", "handleServerCreateAccount")
41
+
42
var request ComAtprotoServerCreateAccountRequest
43
44
if err := e.Bind(&request); err != nil {
45
+
logger.Error("error receiving request", "endpoint", "com.atproto.server.createAccount", "error", err)
46
return helpers.ServerError(e, nil)
47
}
48
49
request.Handle = strings.ToLower(request.Handle)
50
51
if err := e.Validate(request); err != nil {
52
+
logger.Error("error validating request", "endpoint", "com.atproto.server.createAccount", "error", err)
53
54
var verr ValidationError
55
if errors.As(err, &verr) {
···
71
}
72
}
73
}
74
+
75
var signupDid string
76
if request.Did != nil {
77
+
signupDid = *request.Did
78
+
79
token := strings.TrimSpace(strings.Replace(e.Request().Header.Get("authorization"), "Bearer ", "", 1))
80
if token == "" {
81
return helpers.UnauthorizedError(e, to.StringPtr("must authenticate to use an existing did"))
···
83
authDid, err := s.validateServiceAuth(e.Request().Context(), token, "com.atproto.server.createAccount")
84
85
if err != nil {
86
+
logger.Warn("error validating authorization token", "endpoint", "com.atproto.server.createAccount", "error", err)
87
return helpers.UnauthorizedError(e, to.StringPtr("invalid authorization token"))
88
}
89
···
93
}
94
95
// see if the handle is already taken
96
+
actor, err := s.getActorByHandle(ctx, request.Handle)
97
if err != nil && err != gorm.ErrRecordNotFound {
98
+
logger.Error("error looking up handle in db", "endpoint", "com.atproto.server.createAccount", "error", err)
99
return helpers.ServerError(e, nil)
100
}
101
if err == nil && actor.Did != signupDid {
···
107
}
108
109
var ic models.InviteCode
110
+
if s.config.RequireInvite {
111
+
if strings.TrimSpace(request.InviteCode) == "" {
112
return helpers.InputError(e, to.StringPtr("InvalidInviteCode"))
113
}
114
+
115
+
if err := s.db.Raw(ctx, "SELECT * FROM invite_codes WHERE code = ?", nil, request.InviteCode).Scan(&ic).Error; err != nil {
116
+
if err == gorm.ErrRecordNotFound {
117
+
return helpers.InputError(e, to.StringPtr("InvalidInviteCode"))
118
+
}
119
+
logger.Error("error getting invite code from db", "error", err)
120
+
return helpers.ServerError(e, nil)
121
+
}
122
123
+
if ic.RemainingUseCount < 1 {
124
+
return helpers.InputError(e, to.StringPtr("InvalidInviteCode"))
125
+
}
126
}
127
128
// see if the email is already taken
129
+
existingRepo, err := s.getRepoByEmail(ctx, request.Email)
130
if err != nil && err != gorm.ErrRecordNotFound {
131
+
logger.Error("error looking up email in db", "endpoint", "com.atproto.server.createAccount", "error", err)
132
return helpers.ServerError(e, nil)
133
}
134
if err == nil && existingRepo.Did != signupDid {
···
137
138
// TODO: unsupported domains
139
140
+
var k *atcrypto.PrivateKeyK256
141
+
142
+
if signupDid != "" {
143
+
reservedKey, err := s.getReservedKey(ctx, signupDid)
144
+
if err != nil {
145
+
logger.Error("error looking up reserved key", "error", err)
146
+
}
147
+
if reservedKey != nil {
148
+
k, err = atcrypto.ParsePrivateBytesK256(reservedKey.PrivateKey)
149
+
if err != nil {
150
+
logger.Error("error parsing reserved key", "error", err)
151
+
k = nil
152
+
} else {
153
+
defer func() {
154
+
if delErr := s.deleteReservedKey(ctx, reservedKey.KeyDid, reservedKey.Did); delErr != nil {
155
+
logger.Error("error deleting reserved key", "error", delErr)
156
+
}
157
+
}()
158
+
}
159
+
}
160
+
}
161
+
162
+
if k == nil {
163
+
k, err = atcrypto.GeneratePrivateKeyK256()
164
+
if err != nil {
165
+
logger.Error("error creating signing key", "endpoint", "com.atproto.server.createAccount", "error", err)
166
+
return helpers.ServerError(e, nil)
167
+
}
168
}
169
170
if signupDid == "" {
171
did, op, err := s.plcClient.CreateDID(k, "", request.Handle)
172
if err != nil {
173
+
logger.Error("error creating operation", "endpoint", "com.atproto.server.createAccount", "error", err)
174
return helpers.ServerError(e, nil)
175
}
176
177
if err := s.plcClient.SendOperation(e.Request().Context(), did, op); err != nil {
178
+
logger.Error("error sending plc op", "endpoint", "com.atproto.server.createAccount", "error", err)
179
return helpers.ServerError(e, nil)
180
}
181
signupDid = did
···
183
184
hashed, err := bcrypt.GenerateFromPassword([]byte(request.Password), 10)
185
if err != nil {
186
+
logger.Error("error hashing password", "error", err)
187
return helpers.ServerError(e, nil)
188
}
189
···
202
Handle: request.Handle,
203
}
204
205
+
if err := s.db.Create(ctx, &urepo, nil).Error; err != nil {
206
+
logger.Error("error inserting new repo", "error", err)
207
return helpers.ServerError(e, nil)
208
}
209
+
210
+
if err := s.db.Create(ctx, &actor, nil).Error; err != nil {
211
+
logger.Error("error inserting new actor", "error", err)
212
return helpers.ServerError(e, nil)
213
}
214
} else {
215
+
if err := s.db.Save(ctx, &actor, nil).Error; err != nil {
216
+
logger.Error("error inserting new actor", "error", err)
217
return helpers.ServerError(e, nil)
218
}
219
}
···
224
225
root, rev, err := r.Commit(context.TODO(), urepo.SignFor)
226
if err != nil {
227
+
logger.Error("error committing", "error", err)
228
return helpers.ServerError(e, nil)
229
}
230
231
if err := s.UpdateRepo(context.TODO(), urepo.Did, root, rev); err != nil {
232
+
logger.Error("error updating repo after commit", "error", err)
233
return helpers.ServerError(e, nil)
234
}
235
···
243
})
244
}
245
246
+
if s.config.RequireInvite {
247
+
if err := s.db.Raw(ctx, "UPDATE invite_codes SET remaining_use_count = remaining_use_count - 1 WHERE code = ?", nil, request.InviteCode).Scan(&ic).Error; err != nil {
248
+
logger.Error("error decrementing use count", "error", err)
249
+
return helpers.ServerError(e, nil)
250
+
}
251
}
252
253
+
sess, err := s.createSession(ctx, &urepo)
254
if err != nil {
255
+
logger.Error("error creating new session", "error", err)
256
return helpers.ServerError(e, nil)
257
}
258
259
go func() {
260
if err := s.sendEmailVerification(urepo.Email, actor.Handle, *urepo.EmailVerificationCode); err != nil {
261
+
logger.Error("error sending email verification email", "error", err)
262
}
263
if err := s.sendWelcomeMail(urepo.Email, actor.Handle); err != nil {
264
+
logger.Error("error sending welcome email", "error", err)
265
}
266
}()
267
+7
-4
server/handle_server_create_invite_code.go
+7
-4
server/handle_server_create_invite_code.go
···
17
}
18
19
func (s *Server) handleCreateInviteCode(e echo.Context) error {
20
var req ComAtprotoServerCreateInviteCodeRequest
21
if err := e.Bind(&req); err != nil {
22
-
s.logger.Error("error binding", "error", err)
23
return helpers.ServerError(e, nil)
24
}
25
26
if err := e.Validate(req); err != nil {
27
-
s.logger.Error("error validating", "error", err)
28
return helpers.InputError(e, nil)
29
}
30
···
37
acc = *req.ForAccount
38
}
39
40
-
if err := s.db.Create(&models.InviteCode{
41
Code: ic,
42
Did: acc,
43
RemainingUseCount: req.UseCount,
44
}, nil).Error; err != nil {
45
-
s.logger.Error("error creating invite code", "error", err)
46
return helpers.ServerError(e, nil)
47
}
48
···
17
}
18
19
func (s *Server) handleCreateInviteCode(e echo.Context) error {
20
+
ctx := e.Request().Context()
21
+
logger := s.logger.With("name", "handleServerCreateInviteCode")
22
+
23
var req ComAtprotoServerCreateInviteCodeRequest
24
if err := e.Bind(&req); err != nil {
25
+
logger.Error("error binding", "error", err)
26
return helpers.ServerError(e, nil)
27
}
28
29
if err := e.Validate(req); err != nil {
30
+
logger.Error("error validating", "error", err)
31
return helpers.InputError(e, nil)
32
}
33
···
40
acc = *req.ForAccount
41
}
42
43
+
if err := s.db.Create(ctx, &models.InviteCode{
44
Code: ic,
45
Did: acc,
46
RemainingUseCount: req.UseCount,
47
}, nil).Error; err != nil {
48
+
logger.Error("error creating invite code", "error", err)
49
return helpers.ServerError(e, nil)
50
}
51
+7
-4
server/handle_server_create_invite_codes.go
+7
-4
server/handle_server_create_invite_codes.go
···
22
}
23
24
func (s *Server) handleCreateInviteCodes(e echo.Context) error {
25
var req ComAtprotoServerCreateInviteCodesRequest
26
if err := e.Bind(&req); err != nil {
27
-
s.logger.Error("error binding", "error", err)
28
return helpers.ServerError(e, nil)
29
}
30
31
if err := e.Validate(req); err != nil {
32
-
s.logger.Error("error validating", "error", err)
33
return helpers.InputError(e, nil)
34
}
35
···
50
ic := uuid.NewString()
51
ics = append(ics, ic)
52
53
-
if err := s.db.Create(&models.InviteCode{
54
Code: ic,
55
Did: did,
56
RemainingUseCount: req.UseCount,
57
}, nil).Error; err != nil {
58
-
s.logger.Error("error creating invite code", "error", err)
59
return helpers.ServerError(e, nil)
60
}
61
}
···
22
}
23
24
func (s *Server) handleCreateInviteCodes(e echo.Context) error {
25
+
ctx := e.Request().Context()
26
+
logger := s.logger.With("name", "handleServerCreateInviteCodes")
27
+
28
var req ComAtprotoServerCreateInviteCodesRequest
29
if err := e.Bind(&req); err != nil {
30
+
logger.Error("error binding", "error", err)
31
return helpers.ServerError(e, nil)
32
}
33
34
if err := e.Validate(req); err != nil {
35
+
logger.Error("error validating", "error", err)
36
return helpers.InputError(e, nil)
37
}
38
···
53
ic := uuid.NewString()
54
ics = append(ics, ic)
55
56
+
if err := s.db.Create(ctx, &models.InviteCode{
57
Code: ic,
58
Did: did,
59
RemainingUseCount: req.UseCount,
60
}, nil).Error; err != nil {
61
+
logger.Error("error creating invite code", "error", err)
62
return helpers.ServerError(e, nil)
63
}
64
}
+65
-9
server/handle_server_create_session.go
+65
-9
server/handle_server_create_session.go
···
1
package server
2
3
import (
4
"errors"
5
"strings"
6
7
"github.com/Azure/go-autorest/autorest/to"
8
"github.com/bluesky-social/indigo/atproto/syntax"
···
32
}
33
34
func (s *Server) handleCreateSession(e echo.Context) error {
35
var req ComAtprotoServerCreateSessionRequest
36
if err := e.Bind(&req); err != nil {
37
-
s.logger.Error("error binding request", "endpoint", "com.atproto.server.serverCreateSession", "error", err)
38
return helpers.ServerError(e, nil)
39
}
40
···
65
var err error
66
switch idtype {
67
case "did":
68
-
err = s.db.Raw("SELECT r.*, a.* FROM repos r LEFT JOIN actors a ON r.did = a.did WHERE r.did = ?", nil, req.Identifier).Scan(&repo).Error
69
case "handle":
70
-
err = s.db.Raw("SELECT r.*, a.* FROM actors a LEFT JOIN repos r ON a.did = r.did WHERE a.handle = ?", nil, req.Identifier).Scan(&repo).Error
71
case "email":
72
-
err = s.db.Raw("SELECT r.*, a.* FROM repos r LEFT JOIN actors a ON r.did = a.did WHERE r.email = ?", nil, req.Identifier).Scan(&repo).Error
73
}
74
75
if err != nil {
···
77
return helpers.InputError(e, to.StringPtr("InvalidRequest"))
78
}
79
80
-
s.logger.Error("erorr looking up repo", "endpoint", "com.atproto.server.createSession", "error", err)
81
return helpers.ServerError(e, nil)
82
}
83
84
if err := bcrypt.CompareHashAndPassword([]byte(repo.Password), []byte(req.Password)); err != nil {
85
if err != bcrypt.ErrMismatchedHashAndPassword {
86
-
s.logger.Error("erorr comparing hash and password", "error", err)
87
}
88
return helpers.InputError(e, to.StringPtr("InvalidRequest"))
89
}
90
91
-
sess, err := s.createSession(&repo.Repo)
92
if err != nil {
93
-
s.logger.Error("error creating session", "error", err)
94
return helpers.ServerError(e, nil)
95
}
96
···
101
Did: repo.Repo.Did,
102
Email: repo.Email,
103
EmailConfirmed: repo.EmailConfirmedAt != nil,
104
-
EmailAuthFactor: false,
105
Active: repo.Active(),
106
Status: repo.Status(),
107
})
108
}
···
1
package server
2
3
import (
4
+
"context"
5
"errors"
6
+
"fmt"
7
"strings"
8
+
"time"
9
10
"github.com/Azure/go-autorest/autorest/to"
11
"github.com/bluesky-social/indigo/atproto/syntax"
···
35
}
36
37
func (s *Server) handleCreateSession(e echo.Context) error {
38
+
ctx := e.Request().Context()
39
+
logger := s.logger.With("name", "handleServerCreateSession")
40
+
41
var req ComAtprotoServerCreateSessionRequest
42
if err := e.Bind(&req); err != nil {
43
+
logger.Error("error binding request", "endpoint", "com.atproto.server.serverCreateSession", "error", err)
44
return helpers.ServerError(e, nil)
45
}
46
···
71
var err error
72
switch idtype {
73
case "did":
74
+
err = s.db.Raw(ctx, "SELECT r.*, a.* FROM repos r LEFT JOIN actors a ON r.did = a.did WHERE r.did = ?", nil, req.Identifier).Scan(&repo).Error
75
case "handle":
76
+
err = s.db.Raw(ctx, "SELECT r.*, a.* FROM actors a LEFT JOIN repos r ON a.did = r.did WHERE a.handle = ?", nil, req.Identifier).Scan(&repo).Error
77
case "email":
78
+
err = s.db.Raw(ctx, "SELECT r.*, a.* FROM repos r LEFT JOIN actors a ON r.did = a.did WHERE r.email = ?", nil, req.Identifier).Scan(&repo).Error
79
}
80
81
if err != nil {
···
83
return helpers.InputError(e, to.StringPtr("InvalidRequest"))
84
}
85
86
+
logger.Error("erorr looking up repo", "endpoint", "com.atproto.server.createSession", "error", err)
87
return helpers.ServerError(e, nil)
88
}
89
90
if err := bcrypt.CompareHashAndPassword([]byte(repo.Password), []byte(req.Password)); err != nil {
91
if err != bcrypt.ErrMismatchedHashAndPassword {
92
+
logger.Error("erorr comparing hash and password", "error", err)
93
}
94
return helpers.InputError(e, to.StringPtr("InvalidRequest"))
95
}
96
97
+
// if repo requires 2FA token and one hasn't been provided, return error prompting for one
98
+
if repo.TwoFactorType != models.TwoFactorTypeNone && (req.AuthFactorToken == nil || *req.AuthFactorToken == "") {
99
+
err = s.createAndSendTwoFactorCode(ctx, repo)
100
+
if err != nil {
101
+
logger.Error("sending 2FA code", "error", err)
102
+
return helpers.ServerError(e, nil)
103
+
}
104
+
105
+
return helpers.InputError(e, to.StringPtr("AuthFactorTokenRequired"))
106
+
}
107
+
108
+
// if 2FA is required, now check that the one provided is valid
109
+
if repo.TwoFactorType != models.TwoFactorTypeNone {
110
+
if repo.TwoFactorCode == nil || repo.TwoFactorCodeExpiresAt == nil {
111
+
err = s.createAndSendTwoFactorCode(ctx, repo)
112
+
if err != nil {
113
+
logger.Error("sending 2FA code", "error", err)
114
+
return helpers.ServerError(e, nil)
115
+
}
116
+
117
+
return helpers.InputError(e, to.StringPtr("AuthFactorTokenRequired"))
118
+
}
119
+
120
+
if *repo.TwoFactorCode != *req.AuthFactorToken {
121
+
return helpers.InvalidTokenError(e)
122
+
}
123
+
124
+
if time.Now().UTC().After(*repo.TwoFactorCodeExpiresAt) {
125
+
return helpers.ExpiredTokenError(e)
126
+
}
127
+
}
128
+
129
+
sess, err := s.createSession(ctx, &repo.Repo)
130
if err != nil {
131
+
logger.Error("error creating session", "error", err)
132
return helpers.ServerError(e, nil)
133
}
134
···
139
Did: repo.Repo.Did,
140
Email: repo.Email,
141
EmailConfirmed: repo.EmailConfirmedAt != nil,
142
+
EmailAuthFactor: repo.TwoFactorType != models.TwoFactorTypeNone,
143
Active: repo.Active(),
144
Status: repo.Status(),
145
})
146
}
147
+
148
+
func (s *Server) createAndSendTwoFactorCode(ctx context.Context, repo models.RepoActor) error {
149
+
// TODO: when implementing a new type of 2FA there should be some logic in here to send the
150
+
// right type of code
151
+
152
+
code := fmt.Sprintf("%s-%s", helpers.RandomVarchar(5), helpers.RandomVarchar(5))
153
+
eat := time.Now().Add(10 * time.Minute).UTC()
154
+
155
+
if err := s.db.Exec(ctx, "UPDATE repos SET two_factor_code = ?, two_factor_code_expires_at = ? WHERE did = ?", nil, code, eat, repo.Repo.Did).Error; err != nil {
156
+
return fmt.Errorf("updating repo: %w", err)
157
+
}
158
+
159
+
if err := s.sendTwoFactorCode(repo.Email, repo.Handle, code); err != nil {
160
+
return fmt.Errorf("sending email: %w", err)
161
+
}
162
+
163
+
return nil
164
+
}
+6
-3
server/handle_server_deactivate_account.go
+6
-3
server/handle_server_deactivate_account.go
···
19
}
20
21
func (s *Server) handleServerDeactivateAccount(e echo.Context) error {
22
var req ComAtprotoServerDeactivateAccountRequest
23
if err := e.Bind(&req); err != nil {
24
-
s.logger.Error("error binding", "error", err)
25
return helpers.ServerError(e, nil)
26
}
27
28
urepo := e.Get("repo").(*models.RepoActor)
29
30
-
if err := s.db.Exec("UPDATE repos SET deactivated = ? WHERE did = ?", nil, true, urepo.Repo.Did).Error; err != nil {
31
-
s.logger.Error("error updating account status to deactivated", "error", err)
32
return helpers.ServerError(e, nil)
33
}
34
···
19
}
20
21
func (s *Server) handleServerDeactivateAccount(e echo.Context) error {
22
+
ctx := e.Request().Context()
23
+
logger := s.logger.With("name", "handleServerDeactivateAccount")
24
+
25
var req ComAtprotoServerDeactivateAccountRequest
26
if err := e.Bind(&req); err != nil {
27
+
logger.Error("error binding", "error", err)
28
return helpers.ServerError(e, nil)
29
}
30
31
urepo := e.Get("repo").(*models.RepoActor)
32
33
+
if err := s.db.Exec(ctx, "UPDATE repos SET deactivated = ? WHERE did = ?", nil, true, urepo.Repo.Did).Error; err != nil {
34
+
logger.Error("error updating account status to deactivated", "error", err)
35
return helpers.ServerError(e, nil)
36
}
37
+150
server/handle_server_delete_account.go
+150
server/handle_server_delete_account.go
···
···
1
+
package server
2
+
3
+
import (
4
+
"context"
5
+
"time"
6
+
7
+
"github.com/Azure/go-autorest/autorest/to"
8
+
"github.com/bluesky-social/indigo/api/atproto"
9
+
"github.com/bluesky-social/indigo/events"
10
+
"github.com/bluesky-social/indigo/util"
11
+
"github.com/haileyok/cocoon/internal/helpers"
12
+
"github.com/labstack/echo/v4"
13
+
"golang.org/x/crypto/bcrypt"
14
+
)
15
+
16
+
type ComAtprotoServerDeleteAccountRequest struct {
17
+
Did string `json:"did" validate:"required"`
18
+
Password string `json:"password" validate:"required"`
19
+
Token string `json:"token" validate:"required"`
20
+
}
21
+
22
+
func (s *Server) handleServerDeleteAccount(e echo.Context) error {
23
+
ctx := e.Request().Context()
24
+
logger := s.logger.With("name", "handleServerDeleteAccount")
25
+
26
+
var req ComAtprotoServerDeleteAccountRequest
27
+
if err := e.Bind(&req); err != nil {
28
+
logger.Error("error binding", "error", err)
29
+
return helpers.ServerError(e, nil)
30
+
}
31
+
32
+
if err := e.Validate(&req); err != nil {
33
+
logger.Error("error validating", "error", err)
34
+
return helpers.ServerError(e, nil)
35
+
}
36
+
37
+
urepo, err := s.getRepoActorByDid(ctx, req.Did)
38
+
if err != nil {
39
+
logger.Error("error getting repo", "error", err)
40
+
return echo.NewHTTPError(400, "account not found")
41
+
}
42
+
43
+
if err := bcrypt.CompareHashAndPassword([]byte(urepo.Repo.Password), []byte(req.Password)); err != nil {
44
+
logger.Error("password mismatch", "error", err)
45
+
return echo.NewHTTPError(401, "Invalid did or password")
46
+
}
47
+
48
+
if urepo.Repo.AccountDeleteCode == nil || urepo.Repo.AccountDeleteCodeExpiresAt == nil {
49
+
logger.Error("no deletion token found for account")
50
+
return echo.NewHTTPError(400, map[string]interface{}{
51
+
"error": "InvalidToken",
52
+
"message": "Token is invalid",
53
+
})
54
+
}
55
+
56
+
if *urepo.Repo.AccountDeleteCode != req.Token {
57
+
logger.Error("deletion token mismatch")
58
+
return echo.NewHTTPError(400, map[string]interface{}{
59
+
"error": "InvalidToken",
60
+
"message": "Token is invalid",
61
+
})
62
+
}
63
+
64
+
if time.Now().UTC().After(*urepo.Repo.AccountDeleteCodeExpiresAt) {
65
+
logger.Error("deletion token expired")
66
+
return echo.NewHTTPError(400, map[string]interface{}{
67
+
"error": "ExpiredToken",
68
+
"message": "Token is expired",
69
+
})
70
+
}
71
+
72
+
tx := s.db.BeginDangerously(ctx)
73
+
if tx.Error != nil {
74
+
logger.Error("error starting transaction", "error", tx.Error)
75
+
return helpers.ServerError(e, nil)
76
+
}
77
+
78
+
status := "error"
79
+
func() {
80
+
if status == "error" {
81
+
if err := tx.Rollback().Error; err != nil {
82
+
logger.Error("error rolling back after delete failure", "err", err)
83
+
}
84
+
}
85
+
}()
86
+
87
+
if err := tx.Exec("DELETE FROM blocks WHERE did = ?", nil, req.Did).Error; err != nil {
88
+
logger.Error("error deleting blocks", "error", err)
89
+
return helpers.ServerError(e, nil)
90
+
}
91
+
92
+
if err := tx.Exec("DELETE FROM records WHERE did = ?", nil, req.Did).Error; err != nil {
93
+
logger.Error("error deleting records", "error", err)
94
+
return helpers.ServerError(e, nil)
95
+
}
96
+
97
+
if err := tx.Exec("DELETE FROM blobs WHERE did = ?", nil, req.Did).Error; err != nil {
98
+
logger.Error("error deleting blobs", "error", err)
99
+
return helpers.ServerError(e, nil)
100
+
}
101
+
102
+
if err := tx.Exec("DELETE FROM tokens WHERE did = ?", nil, req.Did).Error; err != nil {
103
+
logger.Error("error deleting tokens", "error", err)
104
+
return helpers.ServerError(e, nil)
105
+
}
106
+
107
+
if err := tx.Exec("DELETE FROM refresh_tokens WHERE did = ?", nil, req.Did).Error; err != nil {
108
+
logger.Error("error deleting refresh tokens", "error", err)
109
+
return helpers.ServerError(e, nil)
110
+
}
111
+
112
+
if err := tx.Exec("DELETE FROM reserved_keys WHERE did = ?", nil, req.Did).Error; err != nil {
113
+
logger.Error("error deleting reserved keys", "error", err)
114
+
return helpers.ServerError(e, nil)
115
+
}
116
+
117
+
if err := tx.Exec("DELETE FROM invite_codes WHERE did = ?", nil, req.Did).Error; err != nil {
118
+
logger.Error("error deleting invite codes", "error", err)
119
+
return helpers.ServerError(e, nil)
120
+
}
121
+
122
+
if err := tx.Exec("DELETE FROM actors WHERE did = ?", nil, req.Did).Error; err != nil {
123
+
logger.Error("error deleting actor", "error", err)
124
+
return helpers.ServerError(e, nil)
125
+
}
126
+
127
+
if err := tx.Exec("DELETE FROM repos WHERE did = ?", nil, req.Did).Error; err != nil {
128
+
logger.Error("error deleting repo", "error", err)
129
+
return helpers.ServerError(e, nil)
130
+
}
131
+
132
+
status = "ok"
133
+
134
+
if err := tx.Commit().Error; err != nil {
135
+
logger.Error("error committing transaction", "error", err)
136
+
return helpers.ServerError(e, nil)
137
+
}
138
+
139
+
s.evtman.AddEvent(context.TODO(), &events.XRPCStreamEvent{
140
+
RepoAccount: &atproto.SyncSubscribeRepos_Account{
141
+
Active: false,
142
+
Did: req.Did,
143
+
Status: to.StringPtr("deleted"),
144
+
Seq: time.Now().UnixMicro(),
145
+
Time: time.Now().Format(util.ISO8601),
146
+
},
147
+
})
148
+
149
+
return e.NoContent(200)
150
+
}
+4
-2
server/handle_server_delete_session.go
+4
-2
server/handle_server_delete_session.go
···
7
)
8
9
func (s *Server) handleDeleteSession(e echo.Context) error {
10
token := e.Get("token").(string)
11
12
var acctok models.Token
13
-
if err := s.db.Raw("DELETE FROM tokens WHERE token = ? RETURNING *", nil, token).Scan(&acctok).Error; err != nil {
14
s.logger.Error("error deleting access token from db", "error", err)
15
return helpers.ServerError(e, nil)
16
}
17
18
-
if err := s.db.Exec("DELETE FROM refresh_tokens WHERE token = ?", nil, acctok.RefreshToken).Error; err != nil {
19
s.logger.Error("error deleting refresh token from db", "error", err)
20
return helpers.ServerError(e, nil)
21
}
···
7
)
8
9
func (s *Server) handleDeleteSession(e echo.Context) error {
10
+
ctx := e.Request().Context()
11
+
12
token := e.Get("token").(string)
13
14
var acctok models.Token
15
+
if err := s.db.Raw(ctx, "DELETE FROM tokens WHERE token = ? RETURNING *", nil, token).Scan(&acctok).Error; err != nil {
16
s.logger.Error("error deleting access token from db", "error", err)
17
return helpers.ServerError(e, nil)
18
}
19
20
+
if err := s.db.Exec(ctx, "DELETE FROM refresh_tokens WHERE token = ?", nil, acctok.RefreshToken).Error; err != nil {
21
s.logger.Error("error deleting refresh token from db", "error", err)
22
return helpers.ServerError(e, nil)
23
}
+1
-1
server/handle_server_describe_server.go
+1
-1
server/handle_server_describe_server.go
···
22
23
func (s *Server) handleDescribeServer(e echo.Context) error {
24
return e.JSON(200, ComAtprotoServerDescribeServerResponse{
25
-
InviteCodeRequired: true,
26
PhoneVerificationRequired: false,
27
AvailableUserDomains: []string{"." + s.config.Hostname}, // TODO: more
28
Links: ComAtprotoServerDescribeServerResponseLinks{
···
22
23
func (s *Server) handleDescribeServer(e echo.Context) error {
24
return e.JSON(200, ComAtprotoServerDescribeServerResponse{
25
+
InviteCodeRequired: s.config.RequireInvite,
26
PhoneVerificationRequired: false,
27
AvailableUserDomains: []string{"." + s.config.Hostname}, // TODO: more
28
Links: ComAtprotoServerDescribeServerResponseLinks{
+17
-8
server/handle_server_get_service_auth.go
+17
-8
server/handle_server_get_service_auth.go
···
21
Aud string `query:"aud" validate:"required,atproto-did"`
22
// exp should be a float, as some clients will send a non-integer expiration
23
Exp float64 `query:"exp"`
24
-
Lxm string `query:"lxm" validate:"required,atproto-nsid"`
25
}
26
27
func (s *Server) handleServerGetServiceAuth(e echo.Context) error {
28
var req ServerGetServiceAuthRequest
29
if err := e.Bind(&req); err != nil {
30
-
s.logger.Error("could not bind service auth request", "error", err)
31
return helpers.ServerError(e, nil)
32
}
33
···
45
return helpers.InputError(e, to.StringPtr("may not generate auth tokens recursively"))
46
}
47
48
-
maxExp := now + (60 * 30)
49
if exp > maxExp {
50
return helpers.InputError(e, to.StringPtr("expiration too big. smoller please"))
51
}
···
59
}
60
hj, err := json.Marshal(header)
61
if err != nil {
62
-
s.logger.Error("error marshaling header", "error", err)
63
return helpers.ServerError(e, nil)
64
}
65
···
68
payload := map[string]any{
69
"iss": repo.Repo.Did,
70
"aud": req.Aud,
71
-
"lxm": req.Lxm,
72
"jti": uuid.NewString(),
73
"exp": exp,
74
"iat": now,
75
}
76
pj, err := json.Marshal(payload)
77
if err != nil {
78
-
s.logger.Error("error marashaling payload", "error", err)
79
return helpers.ServerError(e, nil)
80
}
81
···
86
87
sk, err := secp256k1secec.NewPrivateKey(repo.SigningKey)
88
if err != nil {
89
-
s.logger.Error("can't load private key", "error", err)
90
return err
91
}
92
93
R, S, _, err := sk.SignRaw(rand.Reader, hash[:])
94
if err != nil {
95
-
s.logger.Error("error signing", "error", err)
96
return helpers.ServerError(e, nil)
97
}
98
···
21
Aud string `query:"aud" validate:"required,atproto-did"`
22
// exp should be a float, as some clients will send a non-integer expiration
23
Exp float64 `query:"exp"`
24
+
Lxm string `query:"lxm"`
25
}
26
27
func (s *Server) handleServerGetServiceAuth(e echo.Context) error {
28
+
logger := s.logger.With("name", "handleServerGetServiceAuth")
29
+
30
var req ServerGetServiceAuthRequest
31
if err := e.Bind(&req); err != nil {
32
+
logger.Error("could not bind service auth request", "error", err)
33
return helpers.ServerError(e, nil)
34
}
35
···
47
return helpers.InputError(e, to.StringPtr("may not generate auth tokens recursively"))
48
}
49
50
+
var maxExp int64
51
+
if req.Lxm != "" {
52
+
maxExp = now + (60 * 60)
53
+
} else {
54
+
maxExp = now + 60
55
+
}
56
if exp > maxExp {
57
return helpers.InputError(e, to.StringPtr("expiration too big. smoller please"))
58
}
···
66
}
67
hj, err := json.Marshal(header)
68
if err != nil {
69
+
logger.Error("error marshaling header", "error", err)
70
return helpers.ServerError(e, nil)
71
}
72
···
75
payload := map[string]any{
76
"iss": repo.Repo.Did,
77
"aud": req.Aud,
78
"jti": uuid.NewString(),
79
"exp": exp,
80
"iat": now,
81
+
}
82
+
if req.Lxm != "" {
83
+
payload["lxm"] = req.Lxm
84
}
85
pj, err := json.Marshal(payload)
86
if err != nil {
87
+
logger.Error("error marashaling payload", "error", err)
88
return helpers.ServerError(e, nil)
89
}
90
···
95
96
sk, err := secp256k1secec.NewPrivateKey(repo.SigningKey)
97
if err != nil {
98
+
logger.Error("can't load private key", "error", err)
99
return err
100
}
101
102
R, S, _, err := sk.SignRaw(rand.Reader, hash[:])
103
if err != nil {
104
+
logger.Error("error signing", "error", err)
105
return helpers.ServerError(e, nil)
106
}
107
+1
-1
server/handle_server_get_session.go
+1
-1
server/handle_server_get_session.go
+9
-6
server/handle_server_refresh_session.go
+9
-6
server/handle_server_refresh_session.go
···
16
}
17
18
func (s *Server) handleRefreshSession(e echo.Context) error {
19
token := e.Get("token").(string)
20
repo := e.Get("repo").(*models.RepoActor)
21
22
-
if err := s.db.Exec("DELETE FROM refresh_tokens WHERE token = ?", nil, token).Error; err != nil {
23
-
s.logger.Error("error getting refresh token from db", "error", err)
24
return helpers.ServerError(e, nil)
25
}
26
27
-
if err := s.db.Exec("DELETE FROM tokens WHERE refresh_token = ?", nil, token).Error; err != nil {
28
-
s.logger.Error("error deleting access token from db", "error", err)
29
return helpers.ServerError(e, nil)
30
}
31
32
-
sess, err := s.createSession(&repo.Repo)
33
if err != nil {
34
-
s.logger.Error("error creating new session for refresh", "error", err)
35
return helpers.ServerError(e, nil)
36
}
37
···
16
}
17
18
func (s *Server) handleRefreshSession(e echo.Context) error {
19
+
ctx := e.Request().Context()
20
+
logger := s.logger.With("name", "handleServerRefreshSession")
21
+
22
token := e.Get("token").(string)
23
repo := e.Get("repo").(*models.RepoActor)
24
25
+
if err := s.db.Exec(ctx, "DELETE FROM refresh_tokens WHERE token = ?", nil, token).Error; err != nil {
26
+
logger.Error("error getting refresh token from db", "error", err)
27
return helpers.ServerError(e, nil)
28
}
29
30
+
if err := s.db.Exec(ctx, "DELETE FROM tokens WHERE refresh_token = ?", nil, token).Error; err != nil {
31
+
logger.Error("error deleting access token from db", "error", err)
32
return helpers.ServerError(e, nil)
33
}
34
35
+
sess, err := s.createSession(ctx, &repo.Repo)
36
if err != nil {
37
+
logger.Error("error creating new session for refresh", "error", err)
38
return helpers.ServerError(e, nil)
39
}
40
+52
server/handle_server_request_account_delete.go
+52
server/handle_server_request_account_delete.go
···
···
1
+
package server
2
+
3
+
import (
4
+
"fmt"
5
+
"time"
6
+
7
+
"github.com/haileyok/cocoon/internal/helpers"
8
+
"github.com/haileyok/cocoon/models"
9
+
"github.com/labstack/echo/v4"
10
+
)
11
+
12
+
func (s *Server) handleServerRequestAccountDelete(e echo.Context) error {
13
+
ctx := e.Request().Context()
14
+
logger := s.logger.With("name", "handleServerRequestAccountDelete")
15
+
16
+
urepo := e.Get("repo").(*models.RepoActor)
17
+
18
+
token := fmt.Sprintf("%s-%s", helpers.RandomVarchar(5), helpers.RandomVarchar(5))
19
+
expiresAt := time.Now().UTC().Add(15 * time.Minute)
20
+
21
+
if err := s.db.Exec(ctx, "UPDATE repos SET account_delete_code = ?, account_delete_code_expires_at = ? WHERE did = ?", nil, token, expiresAt, urepo.Repo.Did).Error; err != nil {
22
+
logger.Error("error setting deletion token", "error", err)
23
+
return helpers.ServerError(e, nil)
24
+
}
25
+
26
+
if urepo.Email != "" {
27
+
if err := s.sendAccountDeleteEmail(urepo.Email, urepo.Actor.Handle, token); err != nil {
28
+
logger.Error("error sending account deletion email", "error", err)
29
+
}
30
+
}
31
+
32
+
return e.NoContent(200)
33
+
}
34
+
35
+
func (s *Server) sendAccountDeleteEmail(email, handle, token string) error {
36
+
if s.mail == nil {
37
+
return nil
38
+
}
39
+
40
+
s.mailLk.Lock()
41
+
defer s.mailLk.Unlock()
42
+
43
+
s.mail.To(email)
44
+
s.mail.Subject("Account Deletion Request for " + s.config.Hostname)
45
+
s.mail.Plain().Set(fmt.Sprintf("Hello %s. Your account deletion code is %s. This code will expire in fifteen minutes. If you did not request this, please ignore this email.", handle, token))
46
+
47
+
if err := s.mail.Send(); err != nil {
48
+
return err
49
+
}
50
+
51
+
return nil
52
+
}
+6
-3
server/handle_server_request_email_confirmation.go
+6
-3
server/handle_server_request_email_confirmation.go
···
11
)
12
13
func (s *Server) handleServerRequestEmailConfirmation(e echo.Context) error {
14
urepo := e.Get("repo").(*models.RepoActor)
15
16
if urepo.EmailConfirmedAt != nil {
···
20
code := fmt.Sprintf("%s-%s", helpers.RandomVarchar(5), helpers.RandomVarchar(5))
21
eat := time.Now().Add(10 * time.Minute).UTC()
22
23
-
if err := s.db.Exec("UPDATE repos SET email_verification_code = ?, email_verification_code_expires_at = ? WHERE did = ?", nil, code, eat, urepo.Repo.Did).Error; err != nil {
24
-
s.logger.Error("error updating user", "error", err)
25
return helpers.ServerError(e, nil)
26
}
27
28
if err := s.sendEmailVerification(urepo.Email, urepo.Handle, code); err != nil {
29
-
s.logger.Error("error sending mail", "error", err)
30
return helpers.ServerError(e, nil)
31
}
32
···
11
)
12
13
func (s *Server) handleServerRequestEmailConfirmation(e echo.Context) error {
14
+
ctx := e.Request().Context()
15
+
logger := s.logger.With("name", "handleServerRequestEmailConfirm")
16
+
17
urepo := e.Get("repo").(*models.RepoActor)
18
19
if urepo.EmailConfirmedAt != nil {
···
23
code := fmt.Sprintf("%s-%s", helpers.RandomVarchar(5), helpers.RandomVarchar(5))
24
eat := time.Now().Add(10 * time.Minute).UTC()
25
26
+
if err := s.db.Exec(ctx, "UPDATE repos SET email_verification_code = ?, email_verification_code_expires_at = ? WHERE did = ?", nil, code, eat, urepo.Repo.Did).Error; err != nil {
27
+
logger.Error("error updating user", "error", err)
28
return helpers.ServerError(e, nil)
29
}
30
31
if err := s.sendEmailVerification(urepo.Email, urepo.Handle, code); err != nil {
32
+
logger.Error("error sending mail", "error", err)
33
return helpers.ServerError(e, nil)
34
}
35
+6
-3
server/handle_server_request_email_update.go
+6
-3
server/handle_server_request_email_update.go
···
14
}
15
16
func (s *Server) handleServerRequestEmailUpdate(e echo.Context) error {
17
urepo := e.Get("repo").(*models.RepoActor)
18
19
if urepo.EmailConfirmedAt != nil {
20
code := fmt.Sprintf("%s-%s", helpers.RandomVarchar(5), helpers.RandomVarchar(5))
21
eat := time.Now().Add(10 * time.Minute).UTC()
22
23
-
if err := s.db.Exec("UPDATE repos SET email_update_code = ?, email_update_code_expires_at = ? WHERE did = ?", nil, code, eat, urepo.Repo.Did).Error; err != nil {
24
-
s.logger.Error("error updating repo", "error", err)
25
return helpers.ServerError(e, nil)
26
}
27
28
if err := s.sendEmailUpdate(urepo.Email, urepo.Handle, code); err != nil {
29
-
s.logger.Error("error sending email", "error", err)
30
return helpers.ServerError(e, nil)
31
}
32
}
···
14
}
15
16
func (s *Server) handleServerRequestEmailUpdate(e echo.Context) error {
17
+
ctx := e.Request().Context()
18
+
logger := s.logger.With("name", "handleServerRequestEmailUpdate")
19
+
20
urepo := e.Get("repo").(*models.RepoActor)
21
22
if urepo.EmailConfirmedAt != nil {
23
code := fmt.Sprintf("%s-%s", helpers.RandomVarchar(5), helpers.RandomVarchar(5))
24
eat := time.Now().Add(10 * time.Minute).UTC()
25
26
+
if err := s.db.Exec(ctx, "UPDATE repos SET email_update_code = ?, email_update_code_expires_at = ? WHERE did = ?", nil, code, eat, urepo.Repo.Did).Error; err != nil {
27
+
logger.Error("error updating repo", "error", err)
28
return helpers.ServerError(e, nil)
29
}
30
31
if err := s.sendEmailUpdate(urepo.Email, urepo.Handle, code); err != nil {
32
+
logger.Error("error sending email", "error", err)
33
return helpers.ServerError(e, nil)
34
}
35
}
+7
-4
server/handle_server_request_password_reset.go
+7
-4
server/handle_server_request_password_reset.go
···
14
}
15
16
func (s *Server) handleServerRequestPasswordReset(e echo.Context) error {
17
urepo, ok := e.Get("repo").(*models.RepoActor)
18
if !ok {
19
var req ComAtprotoServerRequestPasswordResetRequest
···
25
return err
26
}
27
28
-
murepo, err := s.getRepoActorByEmail(req.Email)
29
if err != nil {
30
return err
31
}
···
36
code := fmt.Sprintf("%s-%s", helpers.RandomVarchar(5), helpers.RandomVarchar(5))
37
eat := time.Now().Add(10 * time.Minute).UTC()
38
39
-
if err := s.db.Exec("UPDATE repos SET password_reset_code = ?, password_reset_code_expires_at = ? WHERE did = ?", nil, code, eat, urepo.Repo.Did).Error; err != nil {
40
-
s.logger.Error("error updating repo", "error", err)
41
return helpers.ServerError(e, nil)
42
}
43
44
if err := s.sendPasswordReset(urepo.Email, urepo.Handle, code); err != nil {
45
-
s.logger.Error("error sending email", "error", err)
46
return helpers.ServerError(e, nil)
47
}
48
···
14
}
15
16
func (s *Server) handleServerRequestPasswordReset(e echo.Context) error {
17
+
ctx := e.Request().Context()
18
+
logger := s.logger.With("name", "handleServerRequestPasswordReset")
19
+
20
urepo, ok := e.Get("repo").(*models.RepoActor)
21
if !ok {
22
var req ComAtprotoServerRequestPasswordResetRequest
···
28
return err
29
}
30
31
+
murepo, err := s.getRepoActorByEmail(ctx, req.Email)
32
if err != nil {
33
return err
34
}
···
39
code := fmt.Sprintf("%s-%s", helpers.RandomVarchar(5), helpers.RandomVarchar(5))
40
eat := time.Now().Add(10 * time.Minute).UTC()
41
42
+
if err := s.db.Exec(ctx, "UPDATE repos SET password_reset_code = ?, password_reset_code_expires_at = ? WHERE did = ?", nil, code, eat, urepo.Repo.Did).Error; err != nil {
43
+
logger.Error("error updating repo", "error", err)
44
return helpers.ServerError(e, nil)
45
}
46
47
if err := s.sendPasswordReset(urepo.Email, urepo.Handle, code); err != nil {
48
+
logger.Error("error sending email", "error", err)
49
return helpers.ServerError(e, nil)
50
}
51
+99
server/handle_server_reserve_signing_key.go
+99
server/handle_server_reserve_signing_key.go
···
···
1
+
package server
2
+
3
+
import (
4
+
"context"
5
+
"time"
6
+
7
+
"github.com/bluesky-social/indigo/atproto/atcrypto"
8
+
"github.com/haileyok/cocoon/internal/helpers"
9
+
"github.com/haileyok/cocoon/models"
10
+
"github.com/labstack/echo/v4"
11
+
)
12
+
13
+
type ServerReserveSigningKeyRequest struct {
14
+
Did *string `json:"did"`
15
+
}
16
+
17
+
type ServerReserveSigningKeyResponse struct {
18
+
SigningKey string `json:"signingKey"`
19
+
}
20
+
21
+
func (s *Server) handleServerReserveSigningKey(e echo.Context) error {
22
+
ctx := e.Request().Context()
23
+
logger := s.logger.With("name", "handleServerReserveSigningKey")
24
+
25
+
var req ServerReserveSigningKeyRequest
26
+
if err := e.Bind(&req); err != nil {
27
+
logger.Error("could not bind reserve signing key request", "error", err)
28
+
return helpers.ServerError(e, nil)
29
+
}
30
+
31
+
if req.Did != nil && *req.Did != "" {
32
+
var existing models.ReservedKey
33
+
if err := s.db.Raw(ctx, "SELECT * FROM reserved_keys WHERE did = ?", nil, *req.Did).Scan(&existing).Error; err == nil && existing.KeyDid != "" {
34
+
return e.JSON(200, ServerReserveSigningKeyResponse{
35
+
SigningKey: existing.KeyDid,
36
+
})
37
+
}
38
+
}
39
+
40
+
k, err := atcrypto.GeneratePrivateKeyK256()
41
+
if err != nil {
42
+
logger.Error("error creating signing key", "endpoint", "com.atproto.server.reserveSigningKey", "error", err)
43
+
return helpers.ServerError(e, nil)
44
+
}
45
+
46
+
pubKey, err := k.PublicKey()
47
+
if err != nil {
48
+
logger.Error("error getting public key", "endpoint", "com.atproto.server.reserveSigningKey", "error", err)
49
+
return helpers.ServerError(e, nil)
50
+
}
51
+
52
+
keyDid := pubKey.DIDKey()
53
+
54
+
reservedKey := models.ReservedKey{
55
+
KeyDid: keyDid,
56
+
Did: req.Did,
57
+
PrivateKey: k.Bytes(),
58
+
CreatedAt: time.Now(),
59
+
}
60
+
61
+
if err := s.db.Create(ctx, &reservedKey, nil).Error; err != nil {
62
+
logger.Error("error storing reserved key", "endpoint", "com.atproto.server.reserveSigningKey", "error", err)
63
+
return helpers.ServerError(e, nil)
64
+
}
65
+
66
+
logger.Info("reserved signing key", "keyDid", keyDid, "forDid", req.Did)
67
+
68
+
return e.JSON(200, ServerReserveSigningKeyResponse{
69
+
SigningKey: keyDid,
70
+
})
71
+
}
72
+
73
+
func (s *Server) getReservedKey(ctx context.Context, keyDidOrDid string) (*models.ReservedKey, error) {
74
+
var reservedKey models.ReservedKey
75
+
76
+
if err := s.db.Raw(ctx, "SELECT * FROM reserved_keys WHERE key_did = ?", nil, keyDidOrDid).Scan(&reservedKey).Error; err == nil && reservedKey.KeyDid != "" {
77
+
return &reservedKey, nil
78
+
}
79
+
80
+
if err := s.db.Raw(ctx, "SELECT * FROM reserved_keys WHERE did = ?", nil, keyDidOrDid).Scan(&reservedKey).Error; err == nil && reservedKey.KeyDid != "" {
81
+
return &reservedKey, nil
82
+
}
83
+
84
+
return nil, nil
85
+
}
86
+
87
+
func (s *Server) deleteReservedKey(ctx context.Context, keyDid string, did *string) error {
88
+
if err := s.db.Exec(ctx, "DELETE FROM reserved_keys WHERE key_did = ?", nil, keyDid).Error; err != nil {
89
+
return err
90
+
}
91
+
92
+
if did != nil && *did != "" {
93
+
if err := s.db.Exec(ctx, "DELETE FROM reserved_keys WHERE did = ?", nil, *did).Error; err != nil {
94
+
return err
95
+
}
96
+
}
97
+
98
+
return nil
99
+
}
+7
-4
server/handle_server_reset_password.go
+7
-4
server/handle_server_reset_password.go
···
16
}
17
18
func (s *Server) handleServerResetPassword(e echo.Context) error {
19
urepo := e.Get("repo").(*models.RepoActor)
20
21
var req ComAtprotoServerResetPasswordRequest
22
if err := e.Bind(&req); err != nil {
23
-
s.logger.Error("error binding", "error", err)
24
return helpers.ServerError(e, nil)
25
}
26
···
42
43
hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), 10)
44
if err != nil {
45
-
s.logger.Error("error creating hash", "error", err)
46
return helpers.ServerError(e, nil)
47
}
48
49
-
if err := s.db.Exec("UPDATE repos SET password_reset_code = NULL, password_reset_code_expires_at = NULL, password = ? WHERE did = ?", nil, hash, urepo.Repo.Did).Error; err != nil {
50
-
s.logger.Error("error updating repo", "error", err)
51
return helpers.ServerError(e, nil)
52
}
53
···
16
}
17
18
func (s *Server) handleServerResetPassword(e echo.Context) error {
19
+
ctx := e.Request().Context()
20
+
logger := s.logger.With("name", "handleServerResetPassword")
21
+
22
urepo := e.Get("repo").(*models.RepoActor)
23
24
var req ComAtprotoServerResetPasswordRequest
25
if err := e.Bind(&req); err != nil {
26
+
logger.Error("error binding", "error", err)
27
return helpers.ServerError(e, nil)
28
}
29
···
45
46
hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), 10)
47
if err != nil {
48
+
logger.Error("error creating hash", "error", err)
49
return helpers.ServerError(e, nil)
50
}
51
52
+
if err := s.db.Exec(ctx, "UPDATE repos SET password_reset_code = NULL, password_reset_code_expires_at = NULL, password = ? WHERE did = ?", nil, hash, urepo.Repo.Did).Error; err != nil {
53
+
logger.Error("error updating repo", "error", err)
54
return helpers.ServerError(e, nil)
55
}
56
+3
-1
server/handle_server_resolve_handle.go
+3
-1
server/handle_server_resolve_handle.go
···
10
)
11
12
func (s *Server) handleResolveHandle(e echo.Context) error {
13
type Resp struct {
14
Did string `json:"did"`
15
}
···
28
ctx := context.WithValue(e.Request().Context(), "skip-cache", true)
29
did, err := s.passport.ResolveHandle(ctx, parsed.String())
30
if err != nil {
31
-
s.logger.Error("error resolving handle", "error", err)
32
return helpers.ServerError(e, nil)
33
}
34
···
10
)
11
12
func (s *Server) handleResolveHandle(e echo.Context) error {
13
+
logger := s.logger.With("name", "handleServerResolveHandle")
14
+
15
type Resp struct {
16
Did string `json:"did"`
17
}
···
30
ctx := context.WithValue(e.Request().Context(), "skip-cache", true)
31
did, err := s.passport.ResolveHandle(ctx, parsed.String())
32
if err != nil {
33
+
logger.Error("error resolving handle", "error", err)
34
return helpers.ServerError(e, nil)
35
}
36
+34
-9
server/handle_server_update_email.go
+34
-9
server/handle_server_update_email.go
···
11
type ComAtprotoServerUpdateEmailRequest struct {
12
Email string `json:"email" validate:"required"`
13
EmailAuthFactor bool `json:"emailAuthFactor"`
14
-
Token string `json:"token" validate:"required"`
15
}
16
17
func (s *Server) handleServerUpdateEmail(e echo.Context) error {
18
urepo := e.Get("repo").(*models.RepoActor)
19
20
var req ComAtprotoServerUpdateEmailRequest
21
if err := e.Bind(&req); err != nil {
22
-
s.logger.Error("error binding", "error", err)
23
return helpers.ServerError(e, nil)
24
}
25
···
27
return helpers.InputError(e, nil)
28
}
29
30
-
if urepo.EmailUpdateCode == nil || urepo.EmailUpdateCodeExpiresAt == nil {
31
return helpers.InvalidTokenError(e)
32
}
33
34
-
if *urepo.EmailUpdateCode != req.Token {
35
-
return helpers.InvalidTokenError(e)
36
}
37
38
-
if time.Now().UTC().After(*urepo.EmailUpdateCodeExpiresAt) {
39
-
return helpers.ExpiredTokenError(e)
40
}
41
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 {
43
-
s.logger.Error("error updating repo", "error", err)
44
return helpers.ServerError(e, nil)
45
}
46
···
11
type ComAtprotoServerUpdateEmailRequest struct {
12
Email string `json:"email" validate:"required"`
13
EmailAuthFactor bool `json:"emailAuthFactor"`
14
+
Token string `json:"token"`
15
}
16
17
func (s *Server) handleServerUpdateEmail(e echo.Context) error {
18
+
ctx := e.Request().Context()
19
+
logger := s.logger.With("name", "handleServerUpdateEmail")
20
+
21
urepo := e.Get("repo").(*models.RepoActor)
22
23
var req ComAtprotoServerUpdateEmailRequest
24
if err := e.Bind(&req); err != nil {
25
+
logger.Error("error binding", "error", err)
26
return helpers.ServerError(e, nil)
27
}
28
···
30
return helpers.InputError(e, nil)
31
}
32
33
+
// To disable email auth factor a token is required.
34
+
// To enable email auth factor a token is not required.
35
+
// If updating an email address, a token will be sent anyway
36
+
if urepo.TwoFactorType != models.TwoFactorTypeNone && req.EmailAuthFactor == false && req.Token == "" {
37
return helpers.InvalidTokenError(e)
38
}
39
40
+
if req.Token != "" {
41
+
if urepo.EmailUpdateCode == nil || urepo.EmailUpdateCodeExpiresAt == nil {
42
+
return helpers.InvalidTokenError(e)
43
+
}
44
+
45
+
if *urepo.EmailUpdateCode != req.Token {
46
+
return helpers.InvalidTokenError(e)
47
+
}
48
+
49
+
if time.Now().UTC().After(*urepo.EmailUpdateCodeExpiresAt) {
50
+
return helpers.ExpiredTokenError(e)
51
+
}
52
+
}
53
+
54
+
twoFactorType := models.TwoFactorTypeNone
55
+
if req.EmailAuthFactor {
56
+
twoFactorType = models.TwoFactorTypeEmail
57
}
58
59
+
query := "UPDATE repos SET email_update_code = NULL, email_update_code_expires_at = NULL, two_factor_type = ?, email = ?"
60
+
61
+
if urepo.Email != req.Email {
62
+
query += ",email_confirmed_at = NULL"
63
}
64
65
+
query += " WHERE did = ?"
66
+
67
+
if err := s.db.Exec(ctx, query, nil, twoFactorType, req.Email, urepo.Repo.Did).Error; err != nil {
68
+
logger.Error("error updating repo", "error", err)
69
return helpers.ServerError(e, nil)
70
}
71
+23
-13
server/handle_sync_get_blob.go
+23
-13
server/handle_sync_get_blob.go
···
17
)
18
19
func (s *Server) handleSyncGetBlob(e echo.Context) error {
20
did := e.QueryParam("did")
21
if did == "" {
22
return helpers.InputError(e, nil)
···
32
return helpers.InputError(e, nil)
33
}
34
35
-
urepo, err := s.getRepoActorByDid(did)
36
if err != nil {
37
-
s.logger.Error("could not find user for requested blob", "error", err)
38
return helpers.InputError(e, nil)
39
}
40
···
46
}
47
48
var blob models.Blob
49
-
if err := s.db.Raw("SELECT * FROM blobs WHERE did = ? AND cid = ?", nil, did, c.Bytes()).Scan(&blob).Error; err != nil {
50
-
s.logger.Error("error looking up blob", "error", err)
51
return helpers.ServerError(e, nil)
52
}
53
···
55
56
if blob.Storage == "sqlite" {
57
var parts []models.BlobPart
58
-
if err := s.db.Raw("SELECT * FROM blob_parts WHERE blob_id = ? ORDER BY idx", nil, blob.ID).Scan(&parts).Error; err != nil {
59
-
s.logger.Error("error getting blob parts", "error", err)
60
return helpers.ServerError(e, nil)
61
}
62
···
65
buf.Write(p.Data)
66
}
67
} else if blob.Storage == "s3" {
68
-
if !(s.s3Config != nil && s.s3Config.BlobstoreEnabled) {
69
-
s.logger.Error("s3 storage disabled")
70
return helpers.ServerError(e, nil)
71
}
72
73
config := &aws.Config{
···
82
83
sess, err := session.NewSession(config)
84
if err != nil {
85
-
s.logger.Error("error creating aws session", "error", err)
86
return helpers.ServerError(e, nil)
87
}
88
89
svc := s3.New(sess)
90
if result, err := svc.GetObject(&s3.GetObjectInput{
91
Bucket: aws.String(s.s3Config.Bucket),
92
-
Key: aws.String(fmt.Sprintf("blobs/%s/%s", urepo.Repo.Did, c.String())),
93
}); err != nil {
94
-
s.logger.Error("error getting blob from s3", "error", err)
95
return helpers.ServerError(e, nil)
96
} else {
97
read := 0
···
105
break
106
}
107
} else if err != nil && err != io.ErrUnexpectedEOF {
108
-
s.logger.Error("error reading blob", "error", err)
109
return helpers.ServerError(e, nil)
110
}
111
···
116
}
117
}
118
} else {
119
-
s.logger.Error("unknown storage", "storage", blob.Storage)
120
return helpers.ServerError(e, nil)
121
}
122
···
17
)
18
19
func (s *Server) handleSyncGetBlob(e echo.Context) error {
20
+
ctx := e.Request().Context()
21
+
logger := s.logger.With("name", "handleSyncGetBlob")
22
+
23
did := e.QueryParam("did")
24
if did == "" {
25
return helpers.InputError(e, nil)
···
35
return helpers.InputError(e, nil)
36
}
37
38
+
urepo, err := s.getRepoActorByDid(ctx, did)
39
if err != nil {
40
+
logger.Error("could not find user for requested blob", "error", err)
41
return helpers.InputError(e, nil)
42
}
43
···
49
}
50
51
var blob models.Blob
52
+
if err := s.db.Raw(ctx, "SELECT * FROM blobs WHERE did = ? AND cid = ?", nil, did, c.Bytes()).Scan(&blob).Error; err != nil {
53
+
logger.Error("error looking up blob", "error", err)
54
return helpers.ServerError(e, nil)
55
}
56
···
58
59
if blob.Storage == "sqlite" {
60
var parts []models.BlobPart
61
+
if err := s.db.Raw(ctx, "SELECT * FROM blob_parts WHERE blob_id = ? ORDER BY idx", nil, blob.ID).Scan(&parts).Error; err != nil {
62
+
logger.Error("error getting blob parts", "error", err)
63
return helpers.ServerError(e, nil)
64
}
65
···
68
buf.Write(p.Data)
69
}
70
} else if blob.Storage == "s3" {
71
+
if !(s.s3Config != nil && s.s3Config.BlobstoreEnabled) {
72
+
logger.Error("s3 storage disabled")
73
return helpers.ServerError(e, nil)
74
+
}
75
+
76
+
blobKey := fmt.Sprintf("blobs/%s/%s", urepo.Repo.Did, c.String())
77
+
78
+
if s.s3Config.CDNUrl != "" {
79
+
redirectUrl := fmt.Sprintf("%s/%s", s.s3Config.CDNUrl, blobKey)
80
+
return e.Redirect(302, redirectUrl)
81
}
82
83
config := &aws.Config{
···
92
93
sess, err := session.NewSession(config)
94
if err != nil {
95
+
logger.Error("error creating aws session", "error", err)
96
return helpers.ServerError(e, nil)
97
}
98
99
svc := s3.New(sess)
100
if result, err := svc.GetObject(&s3.GetObjectInput{
101
Bucket: aws.String(s.s3Config.Bucket),
102
+
Key: aws.String(blobKey),
103
}); err != nil {
104
+
logger.Error("error getting blob from s3", "error", err)
105
return helpers.ServerError(e, nil)
106
} else {
107
read := 0
···
115
break
116
}
117
} else if err != nil && err != io.ErrUnexpectedEOF {
118
+
logger.Error("error reading blob", "error", err)
119
return helpers.ServerError(e, nil)
120
}
121
···
126
}
127
}
128
} else {
129
+
logger.Error("unknown storage", "storage", blob.Storage)
130
return helpers.ServerError(e, nil)
131
}
132
+3
-2
server/handle_sync_get_blocks.go
+3
-2
server/handle_sync_get_blocks.go
···
18
19
func (s *Server) handleGetBlocks(e echo.Context) error {
20
ctx := e.Request().Context()
21
22
var req ComAtprotoSyncGetBlocksRequest
23
if err := e.Bind(&req); err != nil {
···
35
cids = append(cids, c)
36
}
37
38
-
urepo, err := s.getRepoActorByDid(req.Did)
39
if err != nil {
40
return helpers.ServerError(e, nil)
41
}
···
52
})
53
54
if _, err := carstore.LdWrite(buf, hb); err != nil {
55
-
s.logger.Error("error writing to car", "error", err)
56
return helpers.ServerError(e, nil)
57
}
58
···
18
19
func (s *Server) handleGetBlocks(e echo.Context) error {
20
ctx := e.Request().Context()
21
+
logger := s.logger.With("name", "handleSyncGetBlocks")
22
23
var req ComAtprotoSyncGetBlocksRequest
24
if err := e.Bind(&req); err != nil {
···
36
cids = append(cids, c)
37
}
38
39
+
urepo, err := s.getRepoActorByDid(ctx, req.Did)
40
if err != nil {
41
return helpers.ServerError(e, nil)
42
}
···
53
})
54
55
if _, err := carstore.LdWrite(buf, hb); err != nil {
56
+
logger.Error("error writing to car", "error", err)
57
return helpers.ServerError(e, nil)
58
}
59
+3
-1
server/handle_sync_get_latest_commit.go
+3
-1
server/handle_sync_get_latest_commit.go
···
12
}
13
14
func (s *Server) handleSyncGetLatestCommit(e echo.Context) error {
15
+
ctx := e.Request().Context()
16
+
17
did := e.QueryParam("did")
18
if did == "" {
19
return helpers.InputError(e, nil)
20
}
21
22
+
urepo, err := s.getRepoActorByDid(ctx, did)
23
if err != nil {
24
return err
25
}
+8
-5
server/handle_sync_get_record.go
+8
-5
server/handle_sync_get_record.go
···
13
)
14
15
func (s *Server) handleSyncGetRecord(e echo.Context) error {
16
did := e.QueryParam("did")
17
collection := e.QueryParam("collection")
18
rkey := e.QueryParam("rkey")
19
20
var urepo models.Repo
21
-
if err := s.db.Raw("SELECT * FROM repos WHERE did = ?", nil, did).Scan(&urepo).Error; err != nil {
22
-
s.logger.Error("error getting repo", "error", err)
23
return helpers.ServerError(e, nil)
24
}
25
26
-
root, blocks, err := s.repoman.getRecordProof(urepo, collection, rkey)
27
if err != nil {
28
return err
29
}
···
36
})
37
38
if _, err := carstore.LdWrite(buf, hb); err != nil {
39
-
s.logger.Error("error writing to car", "error", err)
40
return helpers.ServerError(e, nil)
41
}
42
43
for _, blk := range blocks {
44
if _, err := carstore.LdWrite(buf, blk.Cid().Bytes(), blk.RawData()); err != nil {
45
-
s.logger.Error("error writing to car", "error", err)
46
return helpers.ServerError(e, nil)
47
}
48
}
···
13
)
14
15
func (s *Server) handleSyncGetRecord(e echo.Context) error {
16
+
ctx := e.Request().Context()
17
+
logger := s.logger.With("name", "handleSyncGetRecord")
18
+
19
did := e.QueryParam("did")
20
collection := e.QueryParam("collection")
21
rkey := e.QueryParam("rkey")
22
23
var urepo models.Repo
24
+
if err := s.db.Raw(ctx, "SELECT * FROM repos WHERE did = ?", nil, did).Scan(&urepo).Error; err != nil {
25
+
logger.Error("error getting repo", "error", err)
26
return helpers.ServerError(e, nil)
27
}
28
29
+
root, blocks, err := s.repoman.getRecordProof(ctx, urepo, collection, rkey)
30
if err != nil {
31
return err
32
}
···
39
})
40
41
if _, err := carstore.LdWrite(buf, hb); err != nil {
42
+
logger.Error("error writing to car", "error", err)
43
return helpers.ServerError(e, nil)
44
}
45
46
for _, blk := range blocks {
47
if _, err := carstore.LdWrite(buf, blk.Cid().Bytes(), blk.RawData()); err != nil {
48
+
logger.Error("error writing to car", "error", err)
49
return helpers.ServerError(e, nil)
50
}
51
}
+6
-3
server/handle_sync_get_repo.go
+6
-3
server/handle_sync_get_repo.go
···
13
)
14
15
func (s *Server) handleSyncGetRepo(e echo.Context) error {
16
did := e.QueryParam("did")
17
if did == "" {
18
return helpers.InputError(e, nil)
19
}
20
21
-
urepo, err := s.getRepoActorByDid(did)
22
if err != nil {
23
return err
24
}
···
36
buf := new(bytes.Buffer)
37
38
if _, err := carstore.LdWrite(buf, hb); err != nil {
39
-
s.logger.Error("error writing to car", "error", err)
40
return helpers.ServerError(e, nil)
41
}
42
43
var blocks []models.Block
44
-
if err := s.db.Raw("SELECT * FROM blocks WHERE did = ? ORDER BY rev ASC", nil, urepo.Repo.Did).Scan(&blocks).Error; err != nil {
45
return err
46
}
47
···
13
)
14
15
func (s *Server) handleSyncGetRepo(e echo.Context) error {
16
+
ctx := e.Request().Context()
17
+
logger := s.logger.With("name", "handleSyncGetRepo")
18
+
19
did := e.QueryParam("did")
20
if did == "" {
21
return helpers.InputError(e, nil)
22
}
23
24
+
urepo, err := s.getRepoActorByDid(ctx, did)
25
if err != nil {
26
return err
27
}
···
39
buf := new(bytes.Buffer)
40
41
if _, err := carstore.LdWrite(buf, hb); err != nil {
42
+
logger.Error("error writing to car", "error", err)
43
return helpers.ServerError(e, nil)
44
}
45
46
var blocks []models.Block
47
+
if err := s.db.Raw(ctx, "SELECT * FROM blocks WHERE did = ? ORDER BY rev ASC", nil, urepo.Repo.Did).Scan(&blocks).Error; err != nil {
48
return err
49
}
50
+3
-1
server/handle_sync_get_repo_status.go
+3
-1
server/handle_sync_get_repo_status.go
···
14
15
// TODO: make this actually do the right thing
16
func (s *Server) handleSyncGetRepoStatus(e echo.Context) error {
17
+
ctx := e.Request().Context()
18
+
19
did := e.QueryParam("did")
20
if did == "" {
21
return helpers.InputError(e, nil)
22
}
23
24
+
urepo, err := s.getRepoActorByDid(ctx, did)
25
if err != nil {
26
return err
27
}
+8
-5
server/handle_sync_list_blobs.go
+8
-5
server/handle_sync_list_blobs.go
···
14
}
15
16
func (s *Server) handleSyncListBlobs(e echo.Context) error {
17
did := e.QueryParam("did")
18
if did == "" {
19
return helpers.InputError(e, nil)
···
35
}
36
params = append(params, limit)
37
38
-
urepo, err := s.getRepoActorByDid(did)
39
if err != nil {
40
-
s.logger.Error("could not find user for requested blobs", "error", err)
41
return helpers.InputError(e, nil)
42
}
43
···
49
}
50
51
var blobs []models.Blob
52
-
if err := s.db.Raw("SELECT * FROM blobs WHERE did = ? "+cursorquery+" ORDER BY created_at DESC LIMIT ?", nil, params...).Scan(&blobs).Error; err != nil {
53
-
s.logger.Error("error getting records", "error", err)
54
return helpers.ServerError(e, nil)
55
}
56
···
58
for _, b := range blobs {
59
c, err := cid.Cast(b.Cid)
60
if err != nil {
61
-
s.logger.Error("error casting cid", "error", err)
62
return helpers.ServerError(e, nil)
63
}
64
cstrs = append(cstrs, c.String())
···
14
}
15
16
func (s *Server) handleSyncListBlobs(e echo.Context) error {
17
+
ctx := e.Request().Context()
18
+
logger := s.logger.With("name", "handleSyncListBlobs")
19
+
20
did := e.QueryParam("did")
21
if did == "" {
22
return helpers.InputError(e, nil)
···
38
}
39
params = append(params, limit)
40
41
+
urepo, err := s.getRepoActorByDid(ctx, did)
42
if err != nil {
43
+
logger.Error("could not find user for requested blobs", "error", err)
44
return helpers.InputError(e, nil)
45
}
46
···
52
}
53
54
var blobs []models.Blob
55
+
if err := s.db.Raw(ctx, "SELECT * FROM blobs WHERE did = ? "+cursorquery+" ORDER BY created_at DESC LIMIT ?", nil, params...).Scan(&blobs).Error; err != nil {
56
+
logger.Error("error getting records", "error", err)
57
return helpers.ServerError(e, nil)
58
}
59
···
61
for _, b := range blobs {
62
c, err := cid.Cast(b.Cid)
63
if err != nil {
64
+
logger.Error("error casting cid", "error", err)
65
return helpers.ServerError(e, nil)
66
}
67
cstrs = append(cstrs, c.String())
+82
-50
server/handle_sync_subscribe_repos.go
+82
-50
server/handle_sync_subscribe_repos.go
···
7
"github.com/bluesky-social/indigo/events"
8
"github.com/bluesky-social/indigo/lex/util"
9
"github.com/btcsuite/websocket"
10
"github.com/labstack/echo/v4"
11
)
12
13
func (s *Server) handleSyncSubscribeRepos(e echo.Context) error {
14
-
ctx := e.Request().Context()
15
logger := s.logger.With("component", "subscribe-repos-websocket")
16
17
conn, err := websocket.Upgrade(e.Response().Writer, e.Request(), e.Response().Header(), 1<<10, 1<<10)
···
24
logger = logger.With("ident", ident)
25
logger.Info("new connection established")
26
27
-
evts, cancel, err := s.evtman.Subscribe(ctx, ident, func(evt *events.XRPCStreamEvent) bool {
28
return true
29
}, nil)
30
if err != nil {
31
return err
32
}
33
-
defer cancel()
34
35
header := events.EventHeader{Op: events.EvtKindMessage}
36
for evt := range evts {
37
-
wc, err := conn.NextWriter(websocket.BinaryMessage)
38
-
if err != nil {
39
-
logger.Error("error writing message to relay", "err", err)
40
-
break
41
-
}
42
43
-
if ctx.Err() != nil {
44
-
logger.Error("context error", "err", err)
45
-
break
46
-
}
47
48
-
var obj util.CBOR
49
-
switch {
50
-
case evt.Error != nil:
51
-
header.Op = events.EvtKindErrorFrame
52
-
obj = evt.Error
53
-
case evt.RepoCommit != nil:
54
-
header.MsgType = "#commit"
55
-
obj = evt.RepoCommit
56
-
case evt.RepoIdentity != nil:
57
-
header.MsgType = "#identity"
58
-
obj = evt.RepoIdentity
59
-
case evt.RepoAccount != nil:
60
-
header.MsgType = "#account"
61
-
obj = evt.RepoAccount
62
-
case evt.RepoInfo != nil:
63
-
header.MsgType = "#info"
64
-
obj = evt.RepoInfo
65
-
default:
66
-
logger.Warn("unrecognized event kind")
67
-
return nil
68
-
}
69
70
-
if err := header.MarshalCBOR(wc); err != nil {
71
-
logger.Error("failed to write header to relay", "err", err)
72
-
break
73
-
}
74
75
-
if err := obj.MarshalCBOR(wc); err != nil {
76
-
logger.Error("failed to write event to relay", "err", err)
77
-
break
78
-
}
79
80
-
if err := wc.Close(); err != nil {
81
-
logger.Error("failed to flush-close our event write", "err", err)
82
-
break
83
-
}
84
}
85
86
// we should tell the relay to request a new crawl at this point if we got disconnected
87
// use a new context since the old one might be cancelled at this point
88
-
ctx, cancel = context.WithTimeout(context.Background(), 10*time.Second)
89
-
defer cancel()
90
-
if err := s.requestCrawl(ctx); err != nil {
91
-
logger.Error("error requesting crawls", "err", err)
92
-
}
93
94
return nil
95
}
···
7
"github.com/bluesky-social/indigo/events"
8
"github.com/bluesky-social/indigo/lex/util"
9
"github.com/btcsuite/websocket"
10
+
"github.com/haileyok/cocoon/metrics"
11
"github.com/labstack/echo/v4"
12
)
13
14
func (s *Server) handleSyncSubscribeRepos(e echo.Context) error {
15
+
ctx, cancel := context.WithCancel(e.Request().Context())
16
+
defer cancel()
17
+
18
logger := s.logger.With("component", "subscribe-repos-websocket")
19
20
conn, err := websocket.Upgrade(e.Response().Writer, e.Request(), e.Response().Header(), 1<<10, 1<<10)
···
27
logger = logger.With("ident", ident)
28
logger.Info("new connection established")
29
30
+
metrics.RelaysConnected.WithLabelValues(ident).Inc()
31
+
defer func() {
32
+
metrics.RelaysConnected.WithLabelValues(ident).Dec()
33
+
}()
34
+
35
+
evts, evtManCancel, err := s.evtman.Subscribe(ctx, ident, func(evt *events.XRPCStreamEvent) bool {
36
return true
37
}, nil)
38
if err != nil {
39
return err
40
}
41
+
defer evtManCancel()
42
+
43
+
// drop the connection whenever a subscriber disconnects from the socket, we should get errors
44
+
go func() {
45
+
for {
46
+
select {
47
+
case <-ctx.Done():
48
+
return
49
+
default:
50
+
if _, _, err := conn.ReadMessage(); err != nil {
51
+
logger.Warn("websocket error", "err", err)
52
+
cancel()
53
+
return
54
+
}
55
+
}
56
+
}
57
+
}()
58
59
header := events.EventHeader{Op: events.EvtKindMessage}
60
for evt := range evts {
61
+
func() {
62
+
defer func() {
63
+
metrics.RelaySends.WithLabelValues(ident, header.MsgType).Inc()
64
+
}()
65
66
+
wc, err := conn.NextWriter(websocket.BinaryMessage)
67
+
if err != nil {
68
+
logger.Error("error writing message to relay", "err", err)
69
+
return
70
+
}
71
72
+
if ctx.Err() != nil {
73
+
logger.Error("context error", "err", err)
74
+
return
75
+
}
76
77
+
var obj util.CBOR
78
+
switch {
79
+
case evt.Error != nil:
80
+
header.Op = events.EvtKindErrorFrame
81
+
obj = evt.Error
82
+
case evt.RepoCommit != nil:
83
+
header.MsgType = "#commit"
84
+
obj = evt.RepoCommit
85
+
case evt.RepoIdentity != nil:
86
+
header.MsgType = "#identity"
87
+
obj = evt.RepoIdentity
88
+
case evt.RepoAccount != nil:
89
+
header.MsgType = "#account"
90
+
obj = evt.RepoAccount
91
+
case evt.RepoInfo != nil:
92
+
header.MsgType = "#info"
93
+
obj = evt.RepoInfo
94
+
default:
95
+
logger.Warn("unrecognized event kind")
96
+
return
97
+
}
98
99
+
if err := header.MarshalCBOR(wc); err != nil {
100
+
logger.Error("failed to write header to relay", "err", err)
101
+
return
102
+
}
103
+
104
+
if err := obj.MarshalCBOR(wc); err != nil {
105
+
logger.Error("failed to write event to relay", "err", err)
106
+
return
107
+
}
108
109
+
if err := wc.Close(); err != nil {
110
+
logger.Error("failed to flush-close our event write", "err", err)
111
+
return
112
+
}
113
+
}()
114
}
115
116
// we should tell the relay to request a new crawl at this point if we got disconnected
117
// use a new context since the old one might be cancelled at this point
118
+
go func() {
119
+
retryCtx, retryCancel := context.WithTimeout(context.Background(), 10*time.Second)
120
+
defer retryCancel()
121
+
if err := s.requestCrawl(retryCtx); err != nil {
122
+
logger.Error("error requesting crawls", "err", err)
123
+
}
124
+
}()
125
126
return nil
127
}
+36
server/handle_well_known.go
+36
server/handle_well_known.go
···
2
3
import (
4
"fmt"
5
+
"strings"
6
7
"github.com/Azure/go-autorest/autorest/to"
8
+
"github.com/haileyok/cocoon/internal/helpers"
9
"github.com/labstack/echo/v4"
10
+
"gorm.io/gorm"
11
)
12
13
var (
···
64
},
65
},
66
})
67
+
}
68
+
69
+
func (s *Server) handleAtprotoDid(e echo.Context) error {
70
+
ctx := e.Request().Context()
71
+
logger := s.logger.With("name", "handleAtprotoDid")
72
+
73
+
host := e.Request().Host
74
+
if host == "" {
75
+
return helpers.InputError(e, to.StringPtr("Invalid handle."))
76
+
}
77
+
78
+
host = strings.Split(host, ":")[0]
79
+
host = strings.ToLower(strings.TrimSpace(host))
80
+
81
+
if host == s.config.Hostname {
82
+
return e.String(200, s.config.Did)
83
+
}
84
+
85
+
suffix := "." + s.config.Hostname
86
+
if !strings.HasSuffix(host, suffix) {
87
+
return e.NoContent(404)
88
+
}
89
+
90
+
actor, err := s.getActorByHandle(ctx, host)
91
+
if err != nil {
92
+
if err == gorm.ErrRecordNotFound {
93
+
return e.NoContent(404)
94
+
}
95
+
logger.Error("error looking up actor by handle", "error", err)
96
+
return helpers.ServerError(e, nil)
97
+
}
98
+
99
+
return e.String(200, actor.Did)
100
}
101
102
func (s *Server) handleOauthProtectedResource(e echo.Context) error {
+38
server/mail.go
+38
server/mail.go
···
40
return nil
41
}
42
43
+
func (s *Server) sendPlcTokenReset(email, handle, code string) error {
44
+
if s.mail == nil {
45
+
return nil
46
+
}
47
+
48
+
s.mailLk.Lock()
49
+
defer s.mailLk.Unlock()
50
+
51
+
s.mail.To(email)
52
+
s.mail.Subject("PLC token for " + s.config.Hostname)
53
+
s.mail.Plain().Set(fmt.Sprintf("Hello %s. Your PLC operation code is %s. This code will expire in ten minutes.", handle, code))
54
+
55
+
if err := s.mail.Send(); err != nil {
56
+
return err
57
+
}
58
+
59
+
return nil
60
+
}
61
+
62
func (s *Server) sendEmailUpdate(email, handle, code string) error {
63
if s.mail == nil {
64
return nil
···
96
97
return nil
98
}
99
+
100
+
func (s *Server) sendTwoFactorCode(email, handle, code string) error {
101
+
if s.mail == nil {
102
+
return nil
103
+
}
104
+
105
+
s.mailLk.Lock()
106
+
defer s.mailLk.Unlock()
107
+
108
+
s.mail.To(email)
109
+
s.mail.Subject("2FA code for " + s.config.Hostname)
110
+
s.mail.Plain().Set(fmt.Sprintf("Hello %s. Your 2FA code is %s. This code will expire in ten minutes.", handle, code))
111
+
112
+
if err := s.mail.Send(); err != nil {
113
+
return err
114
+
}
115
+
116
+
return nil
117
+
}
+51
-23
server/middleware.go
+51
-23
server/middleware.go
···
37
38
func (s *Server) handleLegacySessionMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
39
return func(e echo.Context) error {
40
authheader := e.Request().Header.Get("authorization")
41
if authheader == "" {
42
return e.JSON(401, map[string]string{"error": "Unauthorized"})
···
67
if hasLxm {
68
pts := strings.Split(e.Request().URL.String(), "/")
69
if lxm != pts[len(pts)-1] {
70
-
s.logger.Error("service auth lxm incorrect", "lxm", lxm, "expected", pts[len(pts)-1], "error", err)
71
return helpers.InputError(e, nil)
72
}
73
74
maybeDid, ok := claims["iss"].(string)
75
if !ok {
76
-
s.logger.Error("no iss in service auth token", "error", err)
77
return helpers.InputError(e, nil)
78
}
79
did = maybeDid
80
81
-
maybeRepo, err := s.getRepoActorByDid(did)
82
if err != nil {
83
-
s.logger.Error("error fetching repo", "error", err)
84
return helpers.ServerError(e, nil)
85
}
86
repo = maybeRepo
···
94
return s.privateKey.Public(), nil
95
})
96
if err != nil {
97
-
s.logger.Error("error parsing jwt", "error", err)
98
return helpers.ExpiredTokenError(e)
99
}
100
···
107
hash := sha256.Sum256([]byte(signingInput))
108
sigBytes, err := base64.RawURLEncoding.DecodeString(kpts[2])
109
if err != nil {
110
-
s.logger.Error("error decoding signature bytes", "error", err)
111
return helpers.ServerError(e, nil)
112
}
113
114
if len(sigBytes) != 64 {
115
-
s.logger.Error("incorrect sigbytes length", "length", len(sigBytes))
116
return helpers.ServerError(e, nil)
117
}
118
···
121
rr, _ := secp256k1.NewScalarFromBytes((*[32]byte)(rBytes))
122
ss, _ := secp256k1.NewScalarFromBytes((*[32]byte)(sBytes))
123
124
sk, err := secp256k1secec.NewPrivateKey(repo.SigningKey)
125
if err != nil {
126
-
s.logger.Error("can't load private key", "error", err)
127
return err
128
}
129
130
pubKey, ok := sk.Public().(*secp256k1secec.PublicKey)
131
if !ok {
132
-
s.logger.Error("error getting public key from sk")
133
return helpers.ServerError(e, nil)
134
}
135
136
verified := pubKey.VerifyRaw(hash[:], rr, ss)
137
if !verified {
138
-
s.logger.Error("error verifying", "error", err)
139
return helpers.ServerError(e, nil)
140
}
141
}
···
159
Found bool
160
}
161
var result Result
162
-
if err := s.db.Raw("SELECT EXISTS(SELECT 1 FROM "+table+" WHERE token = ?) AS found", nil, tokenstr).Scan(&result).Error; err != nil {
163
if err == gorm.ErrRecordNotFound {
164
return helpers.InvalidTokenError(e)
165
}
166
167
-
s.logger.Error("error getting token from db", "error", err)
168
return helpers.ServerError(e, nil)
169
}
170
···
175
176
exp, ok := claims["exp"].(float64)
177
if !ok {
178
-
s.logger.Error("error getting iat from token")
179
return helpers.ServerError(e, nil)
180
}
181
···
184
}
185
186
if repo == nil {
187
-
maybeRepo, err := s.getRepoActorByDid(claims["sub"].(string))
188
if err != nil {
189
-
s.logger.Error("error fetching repo", "error", err)
190
return helpers.ServerError(e, nil)
191
}
192
repo = maybeRepo
···
207
208
func (s *Server) handleOauthSessionMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
209
return func(e echo.Context) error {
210
authheader := e.Request().Header.Get("authorization")
211
if authheader == "" {
212
return e.JSON(401, map[string]string{"error": "Unauthorized"})
···
232
proof, err := s.oauthProvider.DpopManager.CheckProof(e.Request().Method, "https://"+s.config.Hostname+e.Request().URL.String(), e.Request().Header, to.StringPtr(accessToken))
233
if err != nil {
234
if errors.Is(err, dpop.ErrUseDpopNonce) {
235
-
return e.JSON(400, map[string]string{
236
"error": "use_dpop_nonce",
237
})
238
}
239
-
s.logger.Error("invalid dpop proof", "error", err)
240
return helpers.InputError(e, nil)
241
}
242
243
var oauthToken provider.OauthToken
244
-
if err := s.db.Raw("SELECT * FROM oauth_tokens WHERE token = ?", nil, accessToken).Scan(&oauthToken).Error; err != nil {
245
-
s.logger.Error("error finding access token in db", "error", err)
246
return helpers.InputError(e, nil)
247
}
248
···
251
}
252
253
if *oauthToken.Parameters.DpopJkt != proof.JKT {
254
-
s.logger.Error("jkt mismatch", "token", oauthToken.Parameters.DpopJkt, "proof", proof.JKT)
255
return helpers.InputError(e, to.StringPtr("dpop jkt mismatch"))
256
}
257
258
if time.Now().After(oauthToken.ExpiresAt) {
259
-
return helpers.ExpiredTokenError(e)
260
}
261
262
-
repo, err := s.getRepoActorByDid(oauthToken.Sub)
263
if err != nil {
264
-
s.logger.Error("could not find actor in db", "error", err)
265
return helpers.ServerError(e, nil)
266
}
267
···
37
38
func (s *Server) handleLegacySessionMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
39
return func(e echo.Context) error {
40
+
ctx := e.Request().Context()
41
+
logger := s.logger.With("name", "handleLegacySessionMiddleware")
42
+
43
authheader := e.Request().Header.Get("authorization")
44
if authheader == "" {
45
return e.JSON(401, map[string]string{"error": "Unauthorized"})
···
70
if hasLxm {
71
pts := strings.Split(e.Request().URL.String(), "/")
72
if lxm != pts[len(pts)-1] {
73
+
logger.Error("service auth lxm incorrect", "lxm", lxm, "expected", pts[len(pts)-1], "error", err)
74
return helpers.InputError(e, nil)
75
}
76
77
maybeDid, ok := claims["iss"].(string)
78
if !ok {
79
+
logger.Error("no iss in service auth token", "error", err)
80
return helpers.InputError(e, nil)
81
}
82
did = maybeDid
83
84
+
maybeRepo, err := s.getRepoActorByDid(ctx, did)
85
if err != nil {
86
+
logger.Error("error fetching repo", "error", err)
87
return helpers.ServerError(e, nil)
88
}
89
repo = maybeRepo
···
97
return s.privateKey.Public(), nil
98
})
99
if err != nil {
100
+
logger.Error("error parsing jwt", "error", err)
101
return helpers.ExpiredTokenError(e)
102
}
103
···
110
hash := sha256.Sum256([]byte(signingInput))
111
sigBytes, err := base64.RawURLEncoding.DecodeString(kpts[2])
112
if err != nil {
113
+
logger.Error("error decoding signature bytes", "error", err)
114
return helpers.ServerError(e, nil)
115
}
116
117
if len(sigBytes) != 64 {
118
+
logger.Error("incorrect sigbytes length", "length", len(sigBytes))
119
return helpers.ServerError(e, nil)
120
}
121
···
124
rr, _ := secp256k1.NewScalarFromBytes((*[32]byte)(rBytes))
125
ss, _ := secp256k1.NewScalarFromBytes((*[32]byte)(sBytes))
126
127
+
if repo == nil {
128
+
sub, ok := claims["sub"].(string)
129
+
if !ok {
130
+
s.logger.Error("no sub claim in ES256K token and repo not set")
131
+
return helpers.InvalidTokenError(e)
132
+
}
133
+
maybeRepo, err := s.getRepoActorByDid(ctx, sub)
134
+
if err != nil {
135
+
s.logger.Error("error fetching repo for ES256K verification", "error", err)
136
+
return helpers.ServerError(e, nil)
137
+
}
138
+
repo = maybeRepo
139
+
did = sub
140
+
}
141
+
142
sk, err := secp256k1secec.NewPrivateKey(repo.SigningKey)
143
if err != nil {
144
+
logger.Error("can't load private key", "error", err)
145
return err
146
}
147
148
pubKey, ok := sk.Public().(*secp256k1secec.PublicKey)
149
if !ok {
150
+
logger.Error("error getting public key from sk")
151
return helpers.ServerError(e, nil)
152
}
153
154
verified := pubKey.VerifyRaw(hash[:], rr, ss)
155
if !verified {
156
+
logger.Error("error verifying", "error", err)
157
return helpers.ServerError(e, nil)
158
}
159
}
···
177
Found bool
178
}
179
var result Result
180
+
if err := s.db.Raw(ctx, "SELECT EXISTS(SELECT 1 FROM "+table+" WHERE token = ?) AS found", nil, tokenstr).Scan(&result).Error; err != nil {
181
if err == gorm.ErrRecordNotFound {
182
return helpers.InvalidTokenError(e)
183
}
184
185
+
logger.Error("error getting token from db", "error", err)
186
return helpers.ServerError(e, nil)
187
}
188
···
193
194
exp, ok := claims["exp"].(float64)
195
if !ok {
196
+
logger.Error("error getting iat from token")
197
return helpers.ServerError(e, nil)
198
}
199
···
202
}
203
204
if repo == nil {
205
+
maybeRepo, err := s.getRepoActorByDid(ctx, claims["sub"].(string))
206
if err != nil {
207
+
logger.Error("error fetching repo", "error", err)
208
return helpers.ServerError(e, nil)
209
}
210
repo = maybeRepo
···
225
226
func (s *Server) handleOauthSessionMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
227
return func(e echo.Context) error {
228
+
ctx := e.Request().Context()
229
+
logger := s.logger.With("name", "handleOauthSessionMiddleware")
230
+
231
authheader := e.Request().Header.Get("authorization")
232
if authheader == "" {
233
return e.JSON(401, map[string]string{"error": "Unauthorized"})
···
253
proof, err := s.oauthProvider.DpopManager.CheckProof(e.Request().Method, "https://"+s.config.Hostname+e.Request().URL.String(), e.Request().Header, to.StringPtr(accessToken))
254
if err != nil {
255
if errors.Is(err, dpop.ErrUseDpopNonce) {
256
+
e.Response().Header().Set("WWW-Authenticate", `DPoP error="use_dpop_nonce"`)
257
+
e.Response().Header().Add("access-control-expose-headers", "WWW-Authenticate")
258
+
return e.JSON(401, map[string]string{
259
"error": "use_dpop_nonce",
260
})
261
}
262
+
logger.Error("invalid dpop proof", "error", err)
263
return helpers.InputError(e, nil)
264
}
265
266
var oauthToken provider.OauthToken
267
+
if err := s.db.Raw(ctx, "SELECT * FROM oauth_tokens WHERE token = ?", nil, accessToken).Scan(&oauthToken).Error; err != nil {
268
+
logger.Error("error finding access token in db", "error", err)
269
return helpers.InputError(e, nil)
270
}
271
···
274
}
275
276
if *oauthToken.Parameters.DpopJkt != proof.JKT {
277
+
logger.Error("jkt mismatch", "token", oauthToken.Parameters.DpopJkt, "proof", proof.JKT)
278
return helpers.InputError(e, to.StringPtr("dpop jkt mismatch"))
279
}
280
281
if time.Now().After(oauthToken.ExpiresAt) {
282
+
e.Response().Header().Set("WWW-Authenticate", `DPoP error="invalid_token", error_description="Token expired"`)
283
+
e.Response().Header().Add("access-control-expose-headers", "WWW-Authenticate")
284
+
return e.JSON(401, map[string]string{
285
+
"error": "invalid_token",
286
+
"error_description": "Token expired",
287
+
})
288
}
289
290
+
repo, err := s.getRepoActorByDid(ctx, oauthToken.Sub)
291
if err != nil {
292
+
logger.Error("could not find actor in db", "error", err)
293
return helpers.ServerError(e, nil)
294
}
295
+85
-32
server/repo.go
+85
-32
server/repo.go
···
17
lexutil "github.com/bluesky-social/indigo/lex/util"
18
"github.com/bluesky-social/indigo/repo"
19
"github.com/haileyok/cocoon/internal/db"
20
"github.com/haileyok/cocoon/models"
21
"github.com/haileyok/cocoon/recording_blockstore"
22
blocks "github.com/ipfs/go-block-format"
···
96
}
97
98
// TODO make use of swap commit
99
-
func (rm *RepoMan) applyWrites(urepo models.Repo, writes []Op, swapCommit *string) ([]ApplyWriteResult, error) {
100
rootcid, err := cid.Cast(urepo.Root)
101
if err != nil {
102
return nil, err
···
104
105
dbs := rm.s.getBlockstore(urepo.Did)
106
bs := recording_blockstore.New(dbs)
107
-
r, err := repo.OpenRepo(context.TODO(), bs, rootcid)
108
109
-
entries := []models.Record{}
110
var results []ApplyWriteResult
111
112
for i, op := range writes {
113
if op.Type != OpTypeCreate && op.Rkey == nil {
114
return nil, fmt.Errorf("invalid rkey")
115
} else if op.Type == OpTypeCreate && op.Rkey != nil {
116
-
_, _, err := r.GetRecord(context.TODO(), op.Collection+"/"+*op.Rkey)
117
if err == nil {
118
op.Type = OpTypeUpdate
119
}
120
} else if op.Rkey == nil {
121
op.Rkey = to.StringPtr(rm.clock.Next().String())
122
writes[i].Rkey = op.Rkey
123
}
124
125
_, err := syntax.ParseRecordKey(*op.Rkey)
126
if err != nil {
127
return nil, err
···
129
130
switch op.Type {
131
case OpTypeCreate:
132
-
j, err := json.Marshal(*op.Record)
133
if err != nil {
134
return nil, err
135
}
136
-
out, err := atdata.UnmarshalJSON(j)
137
if err != nil {
138
return nil, err
139
}
140
mm := MarshalableMap(out)
141
142
// HACK: if a record doesn't contain a $type, we can manually set it here based on the op's collection
143
if mm["$type"] == "" {
144
mm["$type"] = op.Collection
145
}
146
147
-
nc, err := r.PutRecord(context.TODO(), op.Collection+"/"+*op.Rkey, &mm)
148
if err != nil {
149
return nil, err
150
}
151
d, err := atdata.MarshalCBOR(mm)
152
if err != nil {
153
return nil, err
154
}
155
entries = append(entries, models.Record{
156
Did: urepo.Did,
157
CreatedAt: rm.clock.Next().String(),
···
160
Cid: nc.String(),
161
Value: d,
162
})
163
results = append(results, ApplyWriteResult{
164
Type: to.StringPtr(OpTypeCreate.String()),
165
Uri: to.StringPtr("at://" + urepo.Did + "/" + op.Collection + "/" + *op.Rkey),
···
167
ValidationStatus: to.StringPtr("valid"), // TODO: obviously this might not be true atm lol
168
})
169
case OpTypeDelete:
170
var old models.Record
171
-
if err := rm.db.Raw("SELECT value FROM records WHERE did = ? AND nsid = ? AND rkey = ?", nil, urepo.Did, op.Collection, op.Rkey).Scan(&old).Error; err != nil {
172
return nil, err
173
}
174
entries = append(entries, models.Record{
175
Did: urepo.Did,
176
Nsid: op.Collection,
177
Rkey: *op.Rkey,
178
Value: old.Value,
179
})
180
-
err := r.DeleteRecord(context.TODO(), op.Collection+"/"+*op.Rkey)
181
if err != nil {
182
return nil, err
183
}
184
results = append(results, ApplyWriteResult{
185
Type: to.StringPtr(OpTypeDelete.String()),
186
})
187
case OpTypeUpdate:
188
-
j, err := json.Marshal(*op.Record)
189
if err != nil {
190
return nil, err
191
}
192
-
out, err := atdata.UnmarshalJSON(j)
193
if err != nil {
194
return nil, err
195
}
196
mm := MarshalableMap(out)
197
-
nc, err := r.UpdateRecord(context.TODO(), op.Collection+"/"+*op.Rkey, &mm)
198
if err != nil {
199
return nil, err
200
}
201
d, err := atdata.MarshalCBOR(mm)
202
if err != nil {
203
return nil, err
204
}
205
entries = append(entries, models.Record{
206
Did: urepo.Did,
207
CreatedAt: rm.clock.Next().String(),
···
210
Cid: nc.String(),
211
Value: d,
212
})
213
results = append(results, ApplyWriteResult{
214
Type: to.StringPtr(OpTypeUpdate.String()),
215
Uri: to.StringPtr("at://" + urepo.Did + "/" + op.Collection + "/" + *op.Rkey),
···
219
}
220
}
221
222
-
newroot, rev, err := r.Commit(context.TODO(), urepo.SignFor)
223
if err != nil {
224
return nil, err
225
}
226
227
buf := new(bytes.Buffer)
228
229
hb, err := cbor.DumpObject(&car.CarHeader{
230
Roots: []cid.Cid{newroot},
231
Version: 1,
232
})
233
-
234
if _, err := carstore.LdWrite(buf, hb); err != nil {
235
return nil, err
236
}
237
238
-
diffops, err := r.DiffSince(context.TODO(), rootcid)
239
if err != nil {
240
return nil, err
241
}
242
243
ops := make([]*atproto.SyncSubscribeRepos_RepoOp, 0, len(diffops))
244
-
245
for _, op := range diffops {
246
var c cid.Cid
247
switch op.Op {
···
270
})
271
}
272
273
-
blk, err := dbs.Get(context.TODO(), c)
274
if err != nil {
275
return nil, err
276
}
277
278
if _, err := carstore.LdWrite(buf, blk.Cid().Bytes(), blk.RawData()); err != nil {
279
return nil, err
280
}
281
}
282
283
for _, op := range bs.GetWriteLog() {
284
if _, err := carstore.LdWrite(buf, op.Cid().Bytes(), op.RawData()); err != nil {
285
return nil, err
286
}
287
}
288
289
var blobs []lexutil.LexLink
290
for _, entry := range entries {
291
var cids []cid.Cid
292
if entry.Cid != "" {
293
-
if err := rm.s.db.Create(&entry, []clause.Expression{clause.OnConflict{
294
Columns: []clause.Column{{Name: "did"}, {Name: "nsid"}, {Name: "rkey"}},
295
UpdateAll: true,
296
}}).Error; err != nil {
297
return nil, err
298
}
299
300
-
cids, err = rm.incrementBlobRefs(urepo, entry.Value)
301
if err != nil {
302
return nil, err
303
}
304
} else {
305
-
if err := rm.s.db.Delete(&entry, nil).Error; err != nil {
306
return nil, err
307
}
308
-
cids, err = rm.decrementBlobRefs(urepo, entry.Value)
309
if err != nil {
310
return nil, err
311
}
312
}
313
314
for _, c := range cids {
315
blobs = append(blobs, lexutil.LexLink(c))
316
}
317
}
318
319
-
rm.s.evtman.AddEvent(context.TODO(), &events.XRPCStreamEvent{
320
RepoCommit: &atproto.SyncSubscribeRepos_Commit{
321
Repo: urepo.Did,
322
Blocks: buf.Bytes(),
···
330
},
331
})
332
333
-
if err := rm.s.UpdateRepo(context.TODO(), urepo.Did, newroot, rev); err != nil {
334
return nil, err
335
}
336
···
345
return results, nil
346
}
347
348
-
func (rm *RepoMan) getRecordProof(urepo models.Repo, collection, rkey string) (cid.Cid, []blocks.Block, error) {
349
c, err := cid.Cast(urepo.Root)
350
if err != nil {
351
return cid.Undef, nil, err
···
354
dbs := rm.s.getBlockstore(urepo.Did)
355
bs := recording_blockstore.New(dbs)
356
357
-
r, err := repo.OpenRepo(context.TODO(), bs, c)
358
if err != nil {
359
return cid.Undef, nil, err
360
}
361
362
-
_, _, err = r.GetRecordBytes(context.TODO(), collection+"/"+rkey)
363
if err != nil {
364
return cid.Undef, nil, err
365
}
···
367
return c, bs.GetReadLog(), nil
368
}
369
370
-
func (rm *RepoMan) incrementBlobRefs(urepo models.Repo, cbor []byte) ([]cid.Cid, error) {
371
cids, err := getBlobCidsFromCbor(cbor)
372
if err != nil {
373
return nil, err
374
}
375
376
for _, c := range cids {
377
-
if err := rm.db.Exec("UPDATE blobs SET ref_count = ref_count + 1 WHERE did = ? AND cid = ?", nil, urepo.Did, c.Bytes()).Error; err != nil {
378
return nil, err
379
}
380
}
···
382
return cids, nil
383
}
384
385
-
func (rm *RepoMan) decrementBlobRefs(urepo models.Repo, cbor []byte) ([]cid.Cid, error) {
386
cids, err := getBlobCidsFromCbor(cbor)
387
if err != nil {
388
return nil, err
···
393
ID uint
394
Count int
395
}
396
-
if err := rm.db.Raw("UPDATE blobs SET ref_count = ref_count - 1 WHERE did = ? AND cid = ? RETURNING id, ref_count", nil, urepo.Did, c.Bytes()).Scan(&res).Error; err != nil {
397
return nil, err
398
}
399
400
if res.Count == 0 {
401
-
if err := rm.db.Exec("DELETE FROM blobs WHERE id = ?", nil, res.ID).Error; err != nil {
402
return nil, err
403
}
404
-
if err := rm.db.Exec("DELETE FROM blob_parts WHERE blob_id = ?", nil, res.ID).Error; err != nil {
405
return nil, err
406
}
407
}
···
17
lexutil "github.com/bluesky-social/indigo/lex/util"
18
"github.com/bluesky-social/indigo/repo"
19
"github.com/haileyok/cocoon/internal/db"
20
+
"github.com/haileyok/cocoon/metrics"
21
"github.com/haileyok/cocoon/models"
22
"github.com/haileyok/cocoon/recording_blockstore"
23
blocks "github.com/ipfs/go-block-format"
···
97
}
98
99
// TODO make use of swap commit
100
+
func (rm *RepoMan) applyWrites(ctx context.Context, urepo models.Repo, writes []Op, swapCommit *string) ([]ApplyWriteResult, error) {
101
rootcid, err := cid.Cast(urepo.Root)
102
if err != nil {
103
return nil, err
···
105
106
dbs := rm.s.getBlockstore(urepo.Did)
107
bs := recording_blockstore.New(dbs)
108
+
r, err := repo.OpenRepo(ctx, bs, rootcid)
109
110
var results []ApplyWriteResult
111
112
+
entries := make([]models.Record, 0, len(writes))
113
for i, op := range writes {
114
+
// updates or deletes must supply an rkey
115
if op.Type != OpTypeCreate && op.Rkey == nil {
116
return nil, fmt.Errorf("invalid rkey")
117
} else if op.Type == OpTypeCreate && op.Rkey != nil {
118
+
// we should conver this op to an update if the rkey already exists
119
+
_, _, err := r.GetRecord(ctx, fmt.Sprintf("%s/%s", op.Collection, *op.Rkey))
120
if err == nil {
121
op.Type = OpTypeUpdate
122
}
123
} else if op.Rkey == nil {
124
+
// creates that don't supply an rkey will have one generated for them
125
op.Rkey = to.StringPtr(rm.clock.Next().String())
126
writes[i].Rkey = op.Rkey
127
}
128
129
+
// validate the record key is actually valid
130
_, err := syntax.ParseRecordKey(*op.Rkey)
131
if err != nil {
132
return nil, err
···
134
135
switch op.Type {
136
case OpTypeCreate:
137
+
// HACK: this fixes some type conversions, mainly around integers
138
+
// first we convert to json bytes
139
+
b, err := json.Marshal(*op.Record)
140
if err != nil {
141
return nil, err
142
}
143
+
// then we use atdata.UnmarshalJSON to convert it back to a map
144
+
out, err := atdata.UnmarshalJSON(b)
145
if err != nil {
146
return nil, err
147
}
148
+
// finally we can cast to a MarshalableMap
149
mm := MarshalableMap(out)
150
151
// HACK: if a record doesn't contain a $type, we can manually set it here based on the op's collection
152
+
// i forget why this is actually necessary?
153
if mm["$type"] == "" {
154
mm["$type"] = op.Collection
155
}
156
157
+
nc, err := r.PutRecord(ctx, fmt.Sprintf("%s/%s", op.Collection, *op.Rkey), &mm)
158
if err != nil {
159
return nil, err
160
}
161
+
162
d, err := atdata.MarshalCBOR(mm)
163
if err != nil {
164
return nil, err
165
}
166
+
167
entries = append(entries, models.Record{
168
Did: urepo.Did,
169
CreatedAt: rm.clock.Next().String(),
···
172
Cid: nc.String(),
173
Value: d,
174
})
175
+
176
results = append(results, ApplyWriteResult{
177
Type: to.StringPtr(OpTypeCreate.String()),
178
Uri: to.StringPtr("at://" + urepo.Did + "/" + op.Collection + "/" + *op.Rkey),
···
180
ValidationStatus: to.StringPtr("valid"), // TODO: obviously this might not be true atm lol
181
})
182
case OpTypeDelete:
183
+
// try to find the old record in the database
184
var old models.Record
185
+
if err := rm.db.Raw(ctx, "SELECT value FROM records WHERE did = ? AND nsid = ? AND rkey = ?", nil, urepo.Did, op.Collection, op.Rkey).Scan(&old).Error; err != nil {
186
return nil, err
187
}
188
+
189
+
// TODO: this is really confusing, and looking at it i have no idea why i did this. below when we are doing deletes, we
190
+
// check if `cid` here is nil to indicate if we should delete. that really doesn't make much sense and its super illogical
191
+
// when reading this code. i dont feel like fixing right now though so
192
entries = append(entries, models.Record{
193
Did: urepo.Did,
194
Nsid: op.Collection,
195
Rkey: *op.Rkey,
196
Value: old.Value,
197
})
198
+
199
+
// delete the record from the repo
200
+
err := r.DeleteRecord(ctx, fmt.Sprintf("%s/%s", op.Collection, *op.Rkey))
201
if err != nil {
202
return nil, err
203
}
204
+
205
+
// add a result for the delete
206
results = append(results, ApplyWriteResult{
207
Type: to.StringPtr(OpTypeDelete.String()),
208
})
209
case OpTypeUpdate:
210
+
// HACK: same hack as above for type fixes
211
+
b, err := json.Marshal(*op.Record)
212
if err != nil {
213
return nil, err
214
}
215
+
out, err := atdata.UnmarshalJSON(b)
216
if err != nil {
217
return nil, err
218
}
219
mm := MarshalableMap(out)
220
+
221
+
nc, err := r.UpdateRecord(ctx, fmt.Sprintf("%s/%s", op.Collection, *op.Rkey), &mm)
222
if err != nil {
223
return nil, err
224
}
225
+
226
d, err := atdata.MarshalCBOR(mm)
227
if err != nil {
228
return nil, err
229
}
230
+
231
entries = append(entries, models.Record{
232
Did: urepo.Did,
233
CreatedAt: rm.clock.Next().String(),
···
236
Cid: nc.String(),
237
Value: d,
238
})
239
+
240
results = append(results, ApplyWriteResult{
241
Type: to.StringPtr(OpTypeUpdate.String()),
242
Uri: to.StringPtr("at://" + urepo.Did + "/" + op.Collection + "/" + *op.Rkey),
···
246
}
247
}
248
249
+
// commit and get the new root
250
+
newroot, rev, err := r.Commit(ctx, urepo.SignFor)
251
if err != nil {
252
return nil, err
253
}
254
255
+
for _, result := range results {
256
+
if result.Type != nil {
257
+
metrics.RepoOperations.WithLabelValues(*result.Type).Inc()
258
+
}
259
+
}
260
+
261
+
// create a buffer for dumping our new cbor into
262
buf := new(bytes.Buffer)
263
264
+
// first write the car header to the buffer
265
hb, err := cbor.DumpObject(&car.CarHeader{
266
Roots: []cid.Cid{newroot},
267
Version: 1,
268
})
269
if _, err := carstore.LdWrite(buf, hb); err != nil {
270
return nil, err
271
}
272
273
+
// get a diff of the changes to the repo
274
+
diffops, err := r.DiffSince(ctx, rootcid)
275
if err != nil {
276
return nil, err
277
}
278
279
+
// create the repo ops for the given diff
280
ops := make([]*atproto.SyncSubscribeRepos_RepoOp, 0, len(diffops))
281
for _, op := range diffops {
282
var c cid.Cid
283
switch op.Op {
···
306
})
307
}
308
309
+
blk, err := dbs.Get(ctx, c)
310
if err != nil {
311
return nil, err
312
}
313
314
+
// write the block to the buffer
315
if _, err := carstore.LdWrite(buf, blk.Cid().Bytes(), blk.RawData()); err != nil {
316
return nil, err
317
}
318
}
319
320
+
// write the writelog to the buffer
321
for _, op := range bs.GetWriteLog() {
322
if _, err := carstore.LdWrite(buf, op.Cid().Bytes(), op.RawData()); err != nil {
323
return nil, err
324
}
325
}
326
327
+
// blob blob blob blob blob :3
328
var blobs []lexutil.LexLink
329
for _, entry := range entries {
330
var cids []cid.Cid
331
+
// whenever there is cid present, we know it's a create (dumb)
332
if entry.Cid != "" {
333
+
if err := rm.s.db.Create(ctx, &entry, []clause.Expression{clause.OnConflict{
334
Columns: []clause.Column{{Name: "did"}, {Name: "nsid"}, {Name: "rkey"}},
335
UpdateAll: true,
336
}}).Error; err != nil {
337
return nil, err
338
}
339
340
+
// increment the given blob refs, yay
341
+
cids, err = rm.incrementBlobRefs(ctx, urepo, entry.Value)
342
if err != nil {
343
return nil, err
344
}
345
} else {
346
+
// as i noted above this is dumb. but we delete whenever the cid is nil. it works solely becaue the pkey
347
+
// is did + collection + rkey. i still really want to separate that out, or use a different type to make
348
+
// this less confusing/easy to read. alas, its 2 am and yea no
349
+
if err := rm.s.db.Delete(ctx, &entry, nil).Error; err != nil {
350
return nil, err
351
}
352
+
353
+
// TODO:
354
+
cids, err = rm.decrementBlobRefs(ctx, urepo, entry.Value)
355
if err != nil {
356
return nil, err
357
}
358
}
359
360
+
// add all the relevant blobs to the blobs list of blobs. blob ^.^
361
for _, c := range cids {
362
blobs = append(blobs, lexutil.LexLink(c))
363
}
364
}
365
366
+
// NOTE: using the request ctx seems a bit suss here, so using a background context. i'm not sure if this
367
+
// runs sync or not
368
+
rm.s.evtman.AddEvent(context.Background(), &events.XRPCStreamEvent{
369
RepoCommit: &atproto.SyncSubscribeRepos_Commit{
370
Repo: urepo.Did,
371
Blocks: buf.Bytes(),
···
379
},
380
})
381
382
+
if err := rm.s.UpdateRepo(ctx, urepo.Did, newroot, rev); err != nil {
383
return nil, err
384
}
385
···
394
return results, nil
395
}
396
397
+
// this is a fun little guy. to get a proof, we need to read the record out of the blockstore and record how we actually
398
+
// got to the guy. we'll wrap a new blockstore in a recording blockstore, then return the log for proof
399
+
func (rm *RepoMan) getRecordProof(ctx context.Context, urepo models.Repo, collection, rkey string) (cid.Cid, []blocks.Block, error) {
400
c, err := cid.Cast(urepo.Root)
401
if err != nil {
402
return cid.Undef, nil, err
···
405
dbs := rm.s.getBlockstore(urepo.Did)
406
bs := recording_blockstore.New(dbs)
407
408
+
r, err := repo.OpenRepo(ctx, bs, c)
409
if err != nil {
410
return cid.Undef, nil, err
411
}
412
413
+
_, _, err = r.GetRecordBytes(ctx, fmt.Sprintf("%s/%s", collection, rkey))
414
if err != nil {
415
return cid.Undef, nil, err
416
}
···
418
return c, bs.GetReadLog(), nil
419
}
420
421
+
func (rm *RepoMan) incrementBlobRefs(ctx context.Context, urepo models.Repo, cbor []byte) ([]cid.Cid, error) {
422
cids, err := getBlobCidsFromCbor(cbor)
423
if err != nil {
424
return nil, err
425
}
426
427
for _, c := range cids {
428
+
if err := rm.db.Exec(ctx, "UPDATE blobs SET ref_count = ref_count + 1 WHERE did = ? AND cid = ?", nil, urepo.Did, c.Bytes()).Error; err != nil {
429
return nil, err
430
}
431
}
···
433
return cids, nil
434
}
435
436
+
func (rm *RepoMan) decrementBlobRefs(ctx context.Context, urepo models.Repo, cbor []byte) ([]cid.Cid, error) {
437
cids, err := getBlobCidsFromCbor(cbor)
438
if err != nil {
439
return nil, err
···
444
ID uint
445
Count int
446
}
447
+
if err := rm.db.Raw(ctx, "UPDATE blobs SET ref_count = ref_count - 1 WHERE did = ? AND cid = ? RETURNING id, ref_count", nil, urepo.Did, c.Bytes()).Scan(&res).Error; err != nil {
448
return nil, err
449
}
450
451
+
// TODO: this does _not_ handle deletions of blobs that are on s3 storage!!!! we need to get the blob, see what
452
+
// storage it is in, and clean up s3!!!!
453
if res.Count == 0 {
454
+
if err := rm.db.Exec(ctx, "DELETE FROM blobs WHERE id = ?", nil, res.ID).Error; err != nil {
455
return nil, err
456
}
457
+
if err := rm.db.Exec(ctx, "DELETE FROM blob_parts WHERE blob_id = ?", nil, res.ID).Error; err != nil {
458
return nil, err
459
}
460
}
+87
-28
server/server.go
+87
-28
server/server.go
···
39
"github.com/haileyok/cocoon/oauth/provider"
40
"github.com/haileyok/cocoon/plc"
41
"github.com/ipfs/go-cid"
42
echo_session "github.com/labstack/echo-contrib/session"
43
"github.com/labstack/echo/v4"
44
"github.com/labstack/echo/v4/middleware"
45
slogecho "github.com/samber/slog-echo"
46
"gorm.io/driver/sqlite"
47
"gorm.io/gorm"
48
)
···
59
Bucket string
60
AccessKey string
61
SecretKey string
62
}
63
64
type Server struct {
···
82
requestCrawlMu sync.Mutex
83
84
dbName string
85
s3Config *S3Config
86
}
87
88
type Args struct {
89
Addr string
90
DbName string
91
-
Logger *slog.Logger
92
Version string
93
Did string
94
Hostname string
···
97
ContactEmail string
98
Relays []string
99
AdminPassword string
100
101
SmtpUser string
102
SmtpPass string
···
121
EnforcePeering bool
122
Relays []string
123
AdminPassword string
124
SmtpEmail string
125
SmtpName string
126
BlockstoreVariant BlockstoreVariant
···
202
}
203
204
func New(args *Args) (*Server, error) {
205
if args.Addr == "" {
206
return nil, fmt.Errorf("addr must be set")
207
}
···
230
return nil, fmt.Errorf("admin password must be set")
231
}
232
233
-
if args.Logger == nil {
234
-
args.Logger = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{}))
235
-
}
236
-
237
if args.SessionSecret == "" {
238
panic("SESSION SECRET WAS NOT SET. THIS IS REQUIRED. ")
239
}
···
241
e := echo.New()
242
243
e.Pre(middleware.RemoveTrailingSlash())
244
-
e.Pre(slogecho.New(args.Logger))
245
e.Use(echo_session.Middleware(sessions.NewCookieStore([]byte(args.SessionSecret))))
246
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
247
AllowOrigins: []string{"*"},
248
AllowHeaders: []string{"*"},
···
288
IdleTimeout: 5 * time.Minute,
289
}
290
291
-
gdb, err := gorm.Open(sqlite.Open(args.DbName), &gorm.Config{})
292
-
if err != nil {
293
-
return nil, err
294
}
295
dbw := db.NewDB(gdb)
296
···
333
var nonceSecret []byte
334
maybeSecret, err := os.ReadFile("nonce.secret")
335
if err != nil && !os.IsNotExist(err) {
336
-
args.Logger.Error("error attempting to read nonce secret", "error", err)
337
} else {
338
nonceSecret = maybeSecret
339
}
···
354
EnforcePeering: false,
355
Relays: args.Relays,
356
AdminPassword: args.AdminPassword,
357
SmtpName: args.SmtpName,
358
SmtpEmail: args.SmtpEmail,
359
BlockstoreVariant: args.BlockstoreVariant,
···
363
passport: identity.NewPassport(h, identity.NewMemCache(10_000)),
364
365
dbName: args.DbName,
366
s3Config: args.S3Config,
367
368
oauthProvider: provider.NewProvider(provider.Args{
369
Hostname: args.Hostname,
370
ClientManagerArgs: client.ManagerArgs{
371
Cli: oauthCli,
372
-
Logger: args.Logger,
373
},
374
DpopManagerArgs: dpop.ManagerArgs{
375
NonceSecret: nonceSecret,
376
NonceRotationInterval: constants.NonceMaxRotationInterval / 3,
377
OnNonceSecretCreated: func(newNonce []byte) {
378
if err := os.WriteFile("nonce.secret", newNonce, 0644); err != nil {
379
-
args.Logger.Error("error writing new nonce secret", "error", err)
380
}
381
},
382
-
Logger: args.Logger,
383
Hostname: args.Hostname,
384
},
385
}),
···
416
s.echo.GET("/", s.handleRoot)
417
s.echo.GET("/xrpc/_health", s.handleHealth)
418
s.echo.GET("/.well-known/did.json", s.handleWellKnown)
419
s.echo.GET("/.well-known/oauth-protected-resource", s.handleOauthProtectedResource)
420
s.echo.GET("/.well-known/oauth-authorization-server", s.handleOauthAuthorizationServer)
421
s.echo.GET("/robots.txt", s.handleRobots)
···
425
s.echo.POST("/xrpc/com.atproto.server.createAccount", s.handleCreateAccount)
426
s.echo.POST("/xrpc/com.atproto.server.createSession", s.handleCreateSession)
427
s.echo.GET("/xrpc/com.atproto.server.describeServer", s.handleDescribeServer)
428
429
s.echo.GET("/xrpc/com.atproto.repo.describeRepo", s.handleDescribeRepo)
430
s.echo.GET("/xrpc/com.atproto.sync.listRepos", s.handleListRepos)
···
439
s.echo.GET("/xrpc/com.atproto.sync.listBlobs", s.handleSyncListBlobs)
440
s.echo.GET("/xrpc/com.atproto.sync.getBlob", s.handleSyncGetBlob)
441
442
// account
443
s.echo.GET("/account", s.handleAccount)
444
s.echo.POST("/account/revoke", s.handleAccountRevoke)
···
459
s.echo.GET("/xrpc/com.atproto.server.getSession", s.handleGetSession, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
460
s.echo.POST("/xrpc/com.atproto.server.refreshSession", s.handleRefreshSession, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
461
s.echo.POST("/xrpc/com.atproto.server.deleteSession", s.handleDeleteSession, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
462
s.echo.POST("/xrpc/com.atproto.identity.updateHandle", s.handleIdentityUpdateHandle, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
463
s.echo.POST("/xrpc/com.atproto.server.confirmEmail", s.handleServerConfirmEmail, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
464
s.echo.POST("/xrpc/com.atproto.server.requestEmailConfirmation", s.handleServerRequestEmailConfirmation, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
465
s.echo.POST("/xrpc/com.atproto.server.requestPasswordReset", s.handleServerRequestPasswordReset) // AUTH NOT REQUIRED FOR THIS ONE
···
470
s.echo.GET("/xrpc/com.atproto.server.checkAccountStatus", s.handleServerCheckAccountStatus, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
471
s.echo.POST("/xrpc/com.atproto.server.deactivateAccount", s.handleServerDeactivateAccount, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
472
s.echo.POST("/xrpc/com.atproto.server.activateAccount", s.handleServerActivateAccount, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
473
474
// repo
475
s.echo.POST("/xrpc/com.atproto.repo.createRecord", s.handleCreateRecord, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
476
s.echo.POST("/xrpc/com.atproto.repo.putRecord", s.handlePutRecord, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
477
s.echo.POST("/xrpc/com.atproto.repo.deleteRecord", s.handleDeleteRecord, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
···
482
// stupid silly endpoints
483
s.echo.GET("/xrpc/app.bsky.actor.getPreferences", s.handleActorGetPreferences, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
484
s.echo.POST("/xrpc/app.bsky.actor.putPreferences", s.handleActorPutPreferences, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
485
486
// admin routes
487
s.echo.POST("/xrpc/com.atproto.server.createInviteCode", s.handleCreateInviteCode, s.handleAdminMiddleware)
···
493
}
494
495
func (s *Server) Serve(ctx context.Context) error {
496
s.addRoutes()
497
498
-
s.logger.Info("migrating...")
499
500
s.db.AutoMigrate(
501
&models.Actor{},
···
507
&models.Record{},
508
&models.Blob{},
509
&models.BlobPart{},
510
&provider.OauthToken{},
511
&provider.OauthAuthorizationRequest{},
512
)
513
514
-
s.logger.Info("starting cocoon")
515
516
go func() {
517
if err := s.httpd.ListenAndServe(); err != nil {
···
523
524
go func() {
525
if err := s.requestCrawl(ctx); err != nil {
526
-
s.logger.Error("error requesting crawls", "err", err)
527
}
528
}()
529
···
541
542
logger.Info("requesting crawl with configured relays")
543
544
-
if time.Now().Sub(s.lastRequestCrawl) <= 1*time.Minute {
545
return fmt.Errorf("a crawl request has already been made within the last minute")
546
}
547
···
564
}
565
566
func (s *Server) doBackup() {
567
start := time.Now()
568
569
-
s.logger.Info("beginning backup to s3...")
570
571
var buf bytes.Buffer
572
if err := func() error {
573
-
s.logger.Info("reading database bytes...")
574
s.db.Lock()
575
defer s.db.Unlock()
576
···
586
587
return nil
588
}(); err != nil {
589
-
s.logger.Error("error backing up database", "error", err)
590
return
591
}
592
593
if err := func() error {
594
-
s.logger.Info("sending to s3...")
595
596
currTime := time.Now().Format("2006-01-02_15-04-05")
597
key := "cocoon-backup-" + currTime + ".db"
···
621
return fmt.Errorf("error uploading file to s3: %w", err)
622
}
623
624
-
s.logger.Info("finished uploading backup to s3", "key", key, "duration", time.Now().Sub(start).Seconds())
625
626
return nil
627
}(); err != nil {
628
-
s.logger.Error("error uploading database backup", "error", err)
629
return
630
}
631
···
633
}
634
635
func (s *Server) backupRoutine() {
636
if s.s3Config == nil || !s.s3Config.BackupsEnabled {
637
return
638
}
639
640
if s.s3Config.Region == "" {
641
-
s.logger.Warn("no s3 region configured but backups are enabled. backups will not run.")
642
return
643
}
644
645
if s.s3Config.Bucket == "" {
646
-
s.logger.Warn("no s3 bucket configured but backups are enabled. backups will not run.")
647
return
648
}
649
650
if s.s3Config.AccessKey == "" {
651
-
s.logger.Warn("no s3 access key configured but backups are enabled. backups will not run.")
652
return
653
}
654
655
if s.s3Config.SecretKey == "" {
656
-
s.logger.Warn("no s3 secret key configured but backups are enabled. backups will not run.")
657
return
658
}
659
···
681
}
682
683
func (s *Server) UpdateRepo(ctx context.Context, did string, root cid.Cid, rev string) error {
684
-
if err := s.db.Exec("UPDATE repos SET root = ?, rev = ? WHERE did = ?", nil, root.Bytes(), rev, did).Error; err != nil {
685
return err
686
}
687
···
39
"github.com/haileyok/cocoon/oauth/provider"
40
"github.com/haileyok/cocoon/plc"
41
"github.com/ipfs/go-cid"
42
+
"github.com/labstack/echo-contrib/echoprometheus"
43
echo_session "github.com/labstack/echo-contrib/session"
44
"github.com/labstack/echo/v4"
45
"github.com/labstack/echo/v4/middleware"
46
slogecho "github.com/samber/slog-echo"
47
+
"gorm.io/driver/postgres"
48
"gorm.io/driver/sqlite"
49
"gorm.io/gorm"
50
)
···
61
Bucket string
62
AccessKey string
63
SecretKey string
64
+
CDNUrl string
65
}
66
67
type Server struct {
···
85
requestCrawlMu sync.Mutex
86
87
dbName string
88
+
dbType string
89
s3Config *S3Config
90
}
91
92
type Args struct {
93
+
Logger *slog.Logger
94
+
95
Addr string
96
DbName string
97
+
DbType string
98
+
DatabaseURL string
99
Version string
100
Did string
101
Hostname string
···
104
ContactEmail string
105
Relays []string
106
AdminPassword string
107
+
RequireInvite bool
108
109
SmtpUser string
110
SmtpPass string
···
129
EnforcePeering bool
130
Relays []string
131
AdminPassword string
132
+
RequireInvite bool
133
SmtpEmail string
134
SmtpName string
135
BlockstoreVariant BlockstoreVariant
···
211
}
212
213
func New(args *Args) (*Server, error) {
214
+
if args.Logger == nil {
215
+
args.Logger = slog.Default()
216
+
}
217
+
218
+
logger := args.Logger.With("name", "New")
219
+
220
if args.Addr == "" {
221
return nil, fmt.Errorf("addr must be set")
222
}
···
245
return nil, fmt.Errorf("admin password must be set")
246
}
247
248
if args.SessionSecret == "" {
249
panic("SESSION SECRET WAS NOT SET. THIS IS REQUIRED. ")
250
}
···
252
e := echo.New()
253
254
e.Pre(middleware.RemoveTrailingSlash())
255
+
e.Pre(slogecho.New(args.Logger.With("component", "slogecho")))
256
e.Use(echo_session.Middleware(sessions.NewCookieStore([]byte(args.SessionSecret))))
257
+
e.Use(echoprometheus.NewMiddleware("cocoon"))
258
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
259
AllowOrigins: []string{"*"},
260
AllowHeaders: []string{"*"},
···
300
IdleTimeout: 5 * time.Minute,
301
}
302
303
+
dbType := args.DbType
304
+
if dbType == "" {
305
+
dbType = "sqlite"
306
+
}
307
+
308
+
var gdb *gorm.DB
309
+
var err error
310
+
switch dbType {
311
+
case "postgres":
312
+
if args.DatabaseURL == "" {
313
+
return nil, fmt.Errorf("database-url must be set when using postgres")
314
+
}
315
+
gdb, err = gorm.Open(postgres.Open(args.DatabaseURL), &gorm.Config{})
316
+
if err != nil {
317
+
return nil, fmt.Errorf("failed to connect to postgres: %w", err)
318
+
}
319
+
logger.Info("connected to PostgreSQL database")
320
+
default:
321
+
gdb, err = gorm.Open(sqlite.Open(args.DbName), &gorm.Config{})
322
+
if err != nil {
323
+
return nil, fmt.Errorf("failed to open sqlite database: %w", err)
324
+
}
325
+
logger.Info("connected to SQLite database", "path", args.DbName)
326
}
327
dbw := db.NewDB(gdb)
328
···
365
var nonceSecret []byte
366
maybeSecret, err := os.ReadFile("nonce.secret")
367
if err != nil && !os.IsNotExist(err) {
368
+
logger.Error("error attempting to read nonce secret", "error", err)
369
} else {
370
nonceSecret = maybeSecret
371
}
···
386
EnforcePeering: false,
387
Relays: args.Relays,
388
AdminPassword: args.AdminPassword,
389
+
RequireInvite: args.RequireInvite,
390
SmtpName: args.SmtpName,
391
SmtpEmail: args.SmtpEmail,
392
BlockstoreVariant: args.BlockstoreVariant,
···
396
passport: identity.NewPassport(h, identity.NewMemCache(10_000)),
397
398
dbName: args.DbName,
399
+
dbType: dbType,
400
s3Config: args.S3Config,
401
402
oauthProvider: provider.NewProvider(provider.Args{
403
Hostname: args.Hostname,
404
ClientManagerArgs: client.ManagerArgs{
405
Cli: oauthCli,
406
+
Logger: args.Logger.With("component", "oauth-client-manager"),
407
},
408
DpopManagerArgs: dpop.ManagerArgs{
409
NonceSecret: nonceSecret,
410
NonceRotationInterval: constants.NonceMaxRotationInterval / 3,
411
OnNonceSecretCreated: func(newNonce []byte) {
412
if err := os.WriteFile("nonce.secret", newNonce, 0644); err != nil {
413
+
logger.Error("error writing new nonce secret", "error", err)
414
}
415
},
416
+
Logger: args.Logger.With("component", "dpop-manager"),
417
Hostname: args.Hostname,
418
},
419
}),
···
450
s.echo.GET("/", s.handleRoot)
451
s.echo.GET("/xrpc/_health", s.handleHealth)
452
s.echo.GET("/.well-known/did.json", s.handleWellKnown)
453
+
s.echo.GET("/.well-known/atproto-did", s.handleAtprotoDid)
454
s.echo.GET("/.well-known/oauth-protected-resource", s.handleOauthProtectedResource)
455
s.echo.GET("/.well-known/oauth-authorization-server", s.handleOauthAuthorizationServer)
456
s.echo.GET("/robots.txt", s.handleRobots)
···
460
s.echo.POST("/xrpc/com.atproto.server.createAccount", s.handleCreateAccount)
461
s.echo.POST("/xrpc/com.atproto.server.createSession", s.handleCreateSession)
462
s.echo.GET("/xrpc/com.atproto.server.describeServer", s.handleDescribeServer)
463
+
s.echo.POST("/xrpc/com.atproto.server.reserveSigningKey", s.handleServerReserveSigningKey)
464
465
s.echo.GET("/xrpc/com.atproto.repo.describeRepo", s.handleDescribeRepo)
466
s.echo.GET("/xrpc/com.atproto.sync.listRepos", s.handleListRepos)
···
475
s.echo.GET("/xrpc/com.atproto.sync.listBlobs", s.handleSyncListBlobs)
476
s.echo.GET("/xrpc/com.atproto.sync.getBlob", s.handleSyncGetBlob)
477
478
+
// labels
479
+
s.echo.GET("/xrpc/com.atproto.label.queryLabels", s.handleLabelQueryLabels)
480
+
481
// account
482
s.echo.GET("/account", s.handleAccount)
483
s.echo.POST("/account/revoke", s.handleAccountRevoke)
···
498
s.echo.GET("/xrpc/com.atproto.server.getSession", s.handleGetSession, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
499
s.echo.POST("/xrpc/com.atproto.server.refreshSession", s.handleRefreshSession, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
500
s.echo.POST("/xrpc/com.atproto.server.deleteSession", s.handleDeleteSession, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
501
+
s.echo.GET("/xrpc/com.atproto.identity.getRecommendedDidCredentials", s.handleGetRecommendedDidCredentials, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
502
s.echo.POST("/xrpc/com.atproto.identity.updateHandle", s.handleIdentityUpdateHandle, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
503
+
s.echo.POST("/xrpc/com.atproto.identity.requestPlcOperationSignature", s.handleIdentityRequestPlcOperationSignature, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
504
+
s.echo.POST("/xrpc/com.atproto.identity.signPlcOperation", s.handleSignPlcOperation, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
505
+
s.echo.POST("/xrpc/com.atproto.identity.submitPlcOperation", s.handleSubmitPlcOperation, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
506
s.echo.POST("/xrpc/com.atproto.server.confirmEmail", s.handleServerConfirmEmail, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
507
s.echo.POST("/xrpc/com.atproto.server.requestEmailConfirmation", s.handleServerRequestEmailConfirmation, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
508
s.echo.POST("/xrpc/com.atproto.server.requestPasswordReset", s.handleServerRequestPasswordReset) // AUTH NOT REQUIRED FOR THIS ONE
···
513
s.echo.GET("/xrpc/com.atproto.server.checkAccountStatus", s.handleServerCheckAccountStatus, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
514
s.echo.POST("/xrpc/com.atproto.server.deactivateAccount", s.handleServerDeactivateAccount, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
515
s.echo.POST("/xrpc/com.atproto.server.activateAccount", s.handleServerActivateAccount, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
516
+
s.echo.POST("/xrpc/com.atproto.server.requestAccountDelete", s.handleServerRequestAccountDelete, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
517
+
s.echo.POST("/xrpc/com.atproto.server.deleteAccount", s.handleServerDeleteAccount)
518
519
// repo
520
+
s.echo.GET("/xrpc/com.atproto.repo.listMissingBlobs", s.handleListMissingBlobs, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
521
s.echo.POST("/xrpc/com.atproto.repo.createRecord", s.handleCreateRecord, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
522
s.echo.POST("/xrpc/com.atproto.repo.putRecord", s.handlePutRecord, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
523
s.echo.POST("/xrpc/com.atproto.repo.deleteRecord", s.handleDeleteRecord, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
···
528
// stupid silly endpoints
529
s.echo.GET("/xrpc/app.bsky.actor.getPreferences", s.handleActorGetPreferences, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
530
s.echo.POST("/xrpc/app.bsky.actor.putPreferences", s.handleActorPutPreferences, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
531
+
s.echo.GET("/xrpc/app.bsky.feed.getFeed", s.handleProxyBskyFeedGetFeed, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
532
533
// admin routes
534
s.echo.POST("/xrpc/com.atproto.server.createInviteCode", s.handleCreateInviteCode, s.handleAdminMiddleware)
···
540
}
541
542
func (s *Server) Serve(ctx context.Context) error {
543
+
logger := s.logger.With("name", "Serve")
544
+
545
s.addRoutes()
546
547
+
logger.Info("migrating...")
548
549
s.db.AutoMigrate(
550
&models.Actor{},
···
556
&models.Record{},
557
&models.Blob{},
558
&models.BlobPart{},
559
+
&models.ReservedKey{},
560
&provider.OauthToken{},
561
&provider.OauthAuthorizationRequest{},
562
)
563
564
+
logger.Info("starting cocoon")
565
566
go func() {
567
if err := s.httpd.ListenAndServe(); err != nil {
···
573
574
go func() {
575
if err := s.requestCrawl(ctx); err != nil {
576
+
logger.Error("error requesting crawls", "err", err)
577
}
578
}()
579
···
591
592
logger.Info("requesting crawl with configured relays")
593
594
+
if time.Since(s.lastRequestCrawl) <= 1*time.Minute {
595
return fmt.Errorf("a crawl request has already been made within the last minute")
596
}
597
···
614
}
615
616
func (s *Server) doBackup() {
617
+
logger := s.logger.With("name", "doBackup")
618
+
619
+
if s.dbType == "postgres" {
620
+
logger.Info("skipping S3 backup - PostgreSQL backups should be handled externally (pg_dump, managed database backups, etc.)")
621
+
return
622
+
}
623
+
624
start := time.Now()
625
626
+
logger.Info("beginning backup to s3...")
627
628
var buf bytes.Buffer
629
if err := func() error {
630
+
logger.Info("reading database bytes...")
631
s.db.Lock()
632
defer s.db.Unlock()
633
···
643
644
return nil
645
}(); err != nil {
646
+
logger.Error("error backing up database", "error", err)
647
return
648
}
649
650
if err := func() error {
651
+
logger.Info("sending to s3...")
652
653
currTime := time.Now().Format("2006-01-02_15-04-05")
654
key := "cocoon-backup-" + currTime + ".db"
···
678
return fmt.Errorf("error uploading file to s3: %w", err)
679
}
680
681
+
logger.Info("finished uploading backup to s3", "key", key, "duration", time.Now().Sub(start).Seconds())
682
683
return nil
684
}(); err != nil {
685
+
logger.Error("error uploading database backup", "error", err)
686
return
687
}
688
···
690
}
691
692
func (s *Server) backupRoutine() {
693
+
logger := s.logger.With("name", "backupRoutine")
694
+
695
if s.s3Config == nil || !s.s3Config.BackupsEnabled {
696
return
697
}
698
699
if s.s3Config.Region == "" {
700
+
logger.Warn("no s3 region configured but backups are enabled. backups will not run.")
701
return
702
}
703
704
if s.s3Config.Bucket == "" {
705
+
logger.Warn("no s3 bucket configured but backups are enabled. backups will not run.")
706
return
707
}
708
709
if s.s3Config.AccessKey == "" {
710
+
logger.Warn("no s3 access key configured but backups are enabled. backups will not run.")
711
return
712
}
713
714
if s.s3Config.SecretKey == "" {
715
+
logger.Warn("no s3 secret key configured but backups are enabled. backups will not run.")
716
return
717
}
718
···
740
}
741
742
func (s *Server) UpdateRepo(ctx context.Context, did string, root cid.Cid, rev string) error {
743
+
if err := s.db.Exec(ctx, "UPDATE repos SET root = ?, rev = ? WHERE did = ?", nil, root.Bytes(), rev, did).Error; err != nil {
744
return err
745
}
746
+4
-3
server/session.go
+4
-3
server/session.go
···
1
package server
2
3
import (
4
"time"
5
6
"github.com/golang-jwt/jwt/v4"
···
13
RefreshToken string
14
}
15
16
-
func (s *Server) createSession(repo *models.Repo) (*Session, error) {
17
now := time.Now()
18
accexp := now.Add(3 * time.Hour)
19
refexp := now.Add(7 * 24 * time.Hour)
···
49
return nil, err
50
}
51
52
-
if err := s.db.Create(&models.Token{
53
Token: accessString,
54
Did: repo.Did,
55
RefreshToken: refreshString,
···
59
return nil, err
60
}
61
62
-
if err := s.db.Create(&models.RefreshToken{
63
Token: refreshString,
64
Did: repo.Did,
65
CreatedAt: now,
···
1
package server
2
3
import (
4
+
"context"
5
"time"
6
7
"github.com/golang-jwt/jwt/v4"
···
14
RefreshToken string
15
}
16
17
+
func (s *Server) createSession(ctx context.Context, repo *models.Repo) (*Session, error) {
18
now := time.Now()
19
accexp := now.Add(3 * time.Hour)
20
refexp := now.Add(7 * 24 * time.Hour)
···
50
return nil, err
51
}
52
53
+
if err := s.db.Create(ctx, &models.Token{
54
Token: accessString,
55
Did: repo.Did,
56
RefreshToken: refreshString,
···
60
return nil, err
61
}
62
63
+
if err := s.db.Create(ctx, &models.RefreshToken{
64
Token: refreshString,
65
Did: repo.Did,
66
CreatedAt: now,
+4
server/templates/signin.html
+4
server/templates/signin.html
···
26
type="password"
27
placeholder="Password"
28
/>
29
+
{{ if .flashes.tokenrequired }}
30
+
<br />
31
+
<input name="token" id="token" placeholder="Enter your 2FA token" />
32
+
{{ end }}
33
<input name="query_params" type="hidden" value="{{ .QueryParams }}" />
34
<button class="primary" type="submit" value="Login">Login</button>
35
</form>
+3
-3
sqlite_blockstore/sqlite_blockstore.go
+3
-3
sqlite_blockstore/sqlite_blockstore.go
···
45
return maybeBlock, nil
46
}
47
48
-
if err := bs.db.Raw("SELECT * FROM blocks WHERE did = ? AND cid = ?", nil, bs.did, cid.Bytes()).Scan(&block).Error; err != nil {
49
return nil, err
50
}
51
···
71
Value: block.RawData(),
72
}
73
74
-
if err := bs.db.Create(&b, []clause.Expression{clause.OnConflict{
75
Columns: []clause.Column{{Name: "did"}, {Name: "cid"}},
76
UpdateAll: true,
77
}}).Error; err != nil {
···
94
}
95
96
func (bs *SqliteBlockstore) PutMany(ctx context.Context, blocks []blocks.Block) error {
97
-
tx := bs.db.BeginDangerously()
98
99
for _, block := range blocks {
100
bs.inserts[block.Cid()] = block
···
45
return maybeBlock, nil
46
}
47
48
+
if err := bs.db.Raw(ctx, "SELECT * FROM blocks WHERE did = ? AND cid = ?", nil, bs.did, cid.Bytes()).Scan(&block).Error; err != nil {
49
return nil, err
50
}
51
···
71
Value: block.RawData(),
72
}
73
74
+
if err := bs.db.Create(ctx, &b, []clause.Expression{clause.OnConflict{
75
Columns: []clause.Column{{Name: "did"}, {Name: "cid"}},
76
UpdateAll: true,
77
}}).Error; err != nil {
···
94
}
95
96
func (bs *SqliteBlockstore) PutMany(ctx context.Context, blocks []blocks.Block) error {
97
+
tx := bs.db.BeginDangerously(ctx)
98
99
for _, block := range blocks {
100
bs.inserts[block.Cid()] = block
+1
-1
test.go
+1
-1
test.go