+1
-1
.env.example
+1
-1
.env.example
+116
.github/workflows/docker-image.yml
+116
.github/workflows/docker-image.yml
···
1
+
name: Docker image
2
+
3
+
on:
4
+
workflow_dispatch:
5
+
push:
6
+
branches:
7
+
- main
8
+
tags:
9
+
- 'v*'
10
+
11
+
env:
12
+
REGISTRY: ghcr.io
13
+
IMAGE_NAME: ${{ github.repository }}
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
49
+
uses: docker/metadata-action@v5
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
+10
Caddyfile
+10
Caddyfile.postgres
+10
Caddyfile.postgres
+25
Dockerfile
+25
Dockerfile
···
1
+
### Compile stage
2
+
FROM golang:1.25.1-bookworm AS build-env
3
+
4
+
ADD . /dockerbuild
5
+
WORKDIR /dockerbuild
6
+
7
+
RUN GIT_VERSION=$(git describe --tags --long --always || echo "dev-local") && \
8
+
go mod tidy && \
9
+
go build -ldflags "-X main.Version=$GIT_VERSION" -o cocoon ./cmd/cocoon
10
+
11
+
### Run stage
12
+
FROM debian:bookworm-slim AS run
13
+
14
+
RUN apt-get update && apt-get install -y dumb-init runit ca-certificates curl && rm -rf /var/lib/apt/lists/*
15
+
ENTRYPOINT ["dumb-init", "--"]
16
+
17
+
WORKDIR /
18
+
RUN mkdir -p data/cocoon
19
+
COPY --from=build-env /dockerbuild/cocoon /
20
+
21
+
CMD ["/cocoon", "run"]
22
+
23
+
LABEL org.opencontainers.image.source=https://github.com/haileyok/cocoon
24
+
LABEL org.opencontainers.image.description="Cocoon ATProto PDS"
25
+
LABEL org.opencontainers.image.licenses=MIT
+40
Makefile
+40
Makefile
···
4
4
GIT_COMMIT := $(shell git rev-parse --short=9 HEAD)
5
5
VERSION := $(if $(GIT_TAG),$(GIT_TAG),dev-$(GIT_COMMIT))
6
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
+
7
24
.PHONY: help
8
25
help: ## Print info about all commands
9
26
@echo "Commands:"
···
14
31
build: ## Build all executables
15
32
go build -ldflags "-X main.Version=$(VERSION)" -o cocoon ./cmd/cocoon
16
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
+
17
53
.PHONY: run
18
54
run:
19
55
go build -ldflags "-X main.Version=dev-local" -o cocoon ./cmd/cocoon && ./cocoon run
···
40
76
41
77
.env:
42
78
if [ ! -f ".env" ]; then cp example.dev.env .env; fi
79
+
80
+
.PHONY: docker-build
81
+
docker-build:
82
+
docker build -t cocoon .
+198
-14
README.md
+198
-14
README.md
···
1
1
# Cocoon
2
2
3
3
> [!WARNING]
4
-
You should not use this PDS. You should not rely on this code as a reference for a PDS implementation. You should not trust this code. Using this PDS implementation may result in data loss, corruption, etc.
4
+
I migrated and have been running my main account on this PDS for months now without issue, however, I am still not responsible if things go awry, particularly during account migration. Please use caution.
5
5
6
6
Cocoon is a PDS implementation in Go. It is highly experimental, and is not ready for any production use.
7
7
8
+
## Quick Start with Docker Compose
9
+
10
+
### Prerequisites
11
+
12
+
- Docker and Docker Compose installed
13
+
- A domain name pointing to your server (for automatic HTTPS)
14
+
- Ports 80 and 443 open in i.e. UFW
15
+
16
+
### Installation
17
+
18
+
1. **Clone the repository**
19
+
```bash
20
+
git clone https://github.com/haileyok/cocoon.git
21
+
cd cocoon
22
+
```
23
+
24
+
2. **Create your configuration file**
25
+
```bash
26
+
cp .env.example .env
27
+
```
28
+
29
+
3. **Edit `.env` with your settings**
30
+
31
+
Required settings:
32
+
```bash
33
+
COCOON_DID="did:web:your-domain.com"
34
+
COCOON_HOSTNAME="your-domain.com"
35
+
COCOON_CONTACT_EMAIL="you@example.com"
36
+
COCOON_RELAYS="https://bsky.network"
37
+
38
+
# Generate with: openssl rand -hex 16
39
+
COCOON_ADMIN_PASSWORD="your-secure-password"
40
+
41
+
# Generate with: openssl rand -hex 32
42
+
COCOON_SESSION_SECRET="your-session-secret"
43
+
```
44
+
45
+
4. **Start the services**
46
+
```bash
47
+
# Pull pre-built image from GitHub Container Registry
48
+
docker-compose pull
49
+
docker-compose up -d
50
+
```
51
+
52
+
Or build locally:
53
+
```bash
54
+
docker-compose build
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:
67
+
```bash
68
+
docker-compose logs create-invite
69
+
```
70
+
71
+
Or check the saved file:
72
+
```bash
73
+
cat keys/initial-invite-code.txt
74
+
```
75
+
76
+
**IMPORTANT**: Save this invite code! You'll need it to create your first account.
77
+
78
+
6. **Monitor the services**
79
+
```bash
80
+
docker-compose logs -f
81
+
```
82
+
83
+
### What Gets Set Up
84
+
85
+
The Docker Compose setup includes:
86
+
87
+
- **init-keys**: Automatically generates cryptographic keys (rotation key and JWK) on first run
88
+
- **cocoon**: The main PDS service running on port 8080
89
+
- **create-invite**: Automatically creates an initial invite code after Cocoon starts (first run only)
90
+
- **caddy**: Reverse proxy with automatic HTTPS via Let's Encrypt
91
+
92
+
### Data Persistence
93
+
94
+
The following directories will be created automatically:
95
+
96
+
- `./keys/` - Cryptographic keys (generated automatically)
97
+
- `rotation.key` - PDS rotation key
98
+
- `jwk.key` - JWK private key
99
+
- `initial-invite-code.txt` - Your first invite code (first run only)
100
+
- `./data/` - SQLite database and blockstore
101
+
- Docker volumes for Caddy configuration and certificates
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"
132
+
COCOON_SMTP_PASS="your-smtp-password"
133
+
COCOON_SMTP_HOST="smtp.example.com"
134
+
COCOON_SMTP_PORT="587"
135
+
COCOON_SMTP_EMAIL="noreply@example.com"
136
+
COCOON_SMTP_NAME="Cocoon PDS"
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
+
175
+
Create an invite code:
176
+
```bash
177
+
docker exec cocoon-pds /cocoon create-invite-code --uses 1
178
+
```
179
+
180
+
Reset a user's password:
181
+
```bash
182
+
docker exec cocoon-pds /cocoon reset-password --did "did:plc:xxx"
183
+
```
184
+
185
+
### Updating
186
+
187
+
```bash
188
+
docker-compose pull
189
+
docker-compose up -d
190
+
```
191
+
8
192
## Implemented Endpoints
9
193
10
194
> [!NOTE]
···
12
196
13
197
### Identity
14
198
15
-
- [ ] `com.atproto.identity.getRecommendedDidCredentials`
16
-
- [ ] `com.atproto.identity.requestPlcOperationSignature`
199
+
- [x] `com.atproto.identity.getRecommendedDidCredentials`
200
+
- [x] `com.atproto.identity.requestPlcOperationSignature`
17
201
- [x] `com.atproto.identity.resolveHandle`
18
-
- [ ] `com.atproto.identity.signPlcOperation`
19
-
- [ ] `com.atproto.identity.submitPlcOperation`
202
+
- [x] `com.atproto.identity.signPlcOperation`
203
+
- [x] `com.atproto.identity.submitPlcOperation`
20
204
- [x] `com.atproto.identity.updateHandle`
21
205
22
206
### Repo
···
27
211
- [x] `com.atproto.repo.deleteRecord`
28
212
- [x] `com.atproto.repo.describeRepo`
29
213
- [x] `com.atproto.repo.getRecord`
30
-
- [x] `com.atproto.repo.importRepo` (Works "okay". You still have to handle PLC operations on your own when migrating. Use with extreme caution.)
214
+
- [x] `com.atproto.repo.importRepo` (Works "okay". Use with extreme caution.)
31
215
- [x] `com.atproto.repo.listRecords`
32
-
- [ ] `com.atproto.repo.listMissingBlobs`
216
+
- [x] `com.atproto.repo.listMissingBlobs`
33
217
34
218
### Server
35
219
36
-
- [ ] `com.atproto.server.activateAccount`
220
+
- [x] `com.atproto.server.activateAccount`
37
221
- [x] `com.atproto.server.checkAccountStatus`
38
222
- [x] `com.atproto.server.confirmEmail`
39
223
- [x] `com.atproto.server.createAccount`
40
224
- [x] `com.atproto.server.createInviteCode`
41
225
- [x] `com.atproto.server.createInviteCodes`
42
-
- [ ] `com.atproto.server.deactivateAccount`
43
-
- [ ] `com.atproto.server.deleteAccount`
226
+
- [x] `com.atproto.server.deactivateAccount`
227
+
- [x] `com.atproto.server.deleteAccount`
44
228
- [x] `com.atproto.server.deleteSession`
45
229
- [x] `com.atproto.server.describeServer`
46
230
- [ ] `com.atproto.server.getAccountInviteCodes`
47
-
- [ ] `com.atproto.server.getServiceAuth`
231
+
- [x] `com.atproto.server.getServiceAuth`
48
232
- ~~[ ] `com.atproto.server.listAppPasswords`~~ - not going to add app passwords
49
233
- [x] `com.atproto.server.refreshSession`
50
-
- [ ] `com.atproto.server.requestAccountDelete`
234
+
- [x] `com.atproto.server.requestAccountDelete`
51
235
- [x] `com.atproto.server.requestEmailConfirmation`
52
236
- [x] `com.atproto.server.requestEmailUpdate`
53
237
- [x] `com.atproto.server.requestPasswordReset`
54
-
- [ ] `com.atproto.server.reserveSigningKey`
238
+
- [x] `com.atproto.server.reserveSigningKey`
55
239
- [x] `com.atproto.server.resetPassword`
56
240
- ~~[] `com.atproto.server.revokeAppPassword`~~ - not going to add app passwords
57
241
- [x] `com.atproto.server.updateEmail`
···
72
256
73
257
### Other
74
258
75
-
- [ ] `com.atproto.label.queryLabels`
259
+
- [x] `com.atproto.label.queryLabels`
76
260
- [x] `com.atproto.moderation.createReport` (Note: this should be handled by proxying, not actually implemented in the PDS)
77
261
- [x] `app.bsky.actor.getPreferences`
78
262
- [x] `app.bsky.actor.putPreferences`
+105
-59
cmd/cocoon/main.go
+105
-59
cmd/cocoon/main.go
···
9
9
"os"
10
10
"time"
11
11
12
-
"github.com/bluesky-social/indigo/atproto/crypto"
12
+
"github.com/bluesky-social/go-util/pkg/telemetry"
13
+
"github.com/bluesky-social/indigo/atproto/atcrypto"
13
14
"github.com/bluesky-social/indigo/atproto/syntax"
14
15
"github.com/haileyok/cocoon/internal/helpers"
15
16
"github.com/haileyok/cocoon/server"
···
17
18
"github.com/lestrrat-go/jwx/v2/jwk"
18
19
"github.com/urfave/cli/v2"
19
20
"golang.org/x/crypto/bcrypt"
21
+
"gorm.io/driver/postgres"
20
22
"gorm.io/driver/sqlite"
21
23
"gorm.io/gorm"
22
24
)
···
39
41
EnvVars: []string{"COCOON_DB_NAME"},
40
42
},
41
43
&cli.StringFlag{
42
-
Name: "did",
43
-
Required: true,
44
-
EnvVars: []string{"COCOON_DID"},
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"},
45
58
},
46
59
&cli.StringFlag{
47
-
Name: "hostname",
48
-
Required: true,
49
-
EnvVars: []string{"COCOON_HOSTNAME"},
60
+
Name: "hostname",
61
+
EnvVars: []string{"COCOON_HOSTNAME"},
50
62
},
51
63
&cli.StringFlag{
52
-
Name: "rotation-key-path",
53
-
Required: true,
54
-
EnvVars: []string{"COCOON_ROTATION_KEY_PATH"},
64
+
Name: "rotation-key-path",
65
+
EnvVars: []string{"COCOON_ROTATION_KEY_PATH"},
55
66
},
56
67
&cli.StringFlag{
57
-
Name: "jwk-path",
58
-
Required: true,
59
-
EnvVars: []string{"COCOON_JWK_PATH"},
68
+
Name: "jwk-path",
69
+
EnvVars: []string{"COCOON_JWK_PATH"},
60
70
},
61
71
&cli.StringFlag{
62
-
Name: "contact-email",
63
-
Required: true,
64
-
EnvVars: []string{"COCOON_CONTACT_EMAIL"},
72
+
Name: "contact-email",
73
+
EnvVars: []string{"COCOON_CONTACT_EMAIL"},
65
74
},
66
75
&cli.StringSliceFlag{
67
-
Name: "relays",
68
-
Required: true,
69
-
EnvVars: []string{"COCOON_RELAYS"},
76
+
Name: "relays",
77
+
EnvVars: []string{"COCOON_RELAYS"},
70
78
},
71
79
&cli.StringFlag{
72
-
Name: "admin-password",
73
-
Required: true,
74
-
EnvVars: []string{"COCOON_ADMIN_PASSWORD"},
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,
75
87
},
76
88
&cli.StringFlag{
77
-
Name: "smtp-user",
78
-
Required: false,
79
-
EnvVars: []string{"COCOON_SMTP_USER"},
89
+
Name: "smtp-user",
90
+
EnvVars: []string{"COCOON_SMTP_USER"},
80
91
},
81
92
&cli.StringFlag{
82
-
Name: "smtp-pass",
83
-
Required: false,
84
-
EnvVars: []string{"COCOON_SMTP_PASS"},
93
+
Name: "smtp-pass",
94
+
EnvVars: []string{"COCOON_SMTP_PASS"},
85
95
},
86
96
&cli.StringFlag{
87
-
Name: "smtp-host",
88
-
Required: false,
89
-
EnvVars: []string{"COCOON_SMTP_HOST"},
97
+
Name: "smtp-host",
98
+
EnvVars: []string{"COCOON_SMTP_HOST"},
90
99
},
91
100
&cli.StringFlag{
92
-
Name: "smtp-port",
93
-
Required: false,
94
-
EnvVars: []string{"COCOON_SMTP_PORT"},
101
+
Name: "smtp-port",
102
+
EnvVars: []string{"COCOON_SMTP_PORT"},
95
103
},
96
104
&cli.StringFlag{
97
-
Name: "smtp-email",
98
-
Required: false,
99
-
EnvVars: []string{"COCOON_SMTP_EMAIL"},
105
+
Name: "smtp-email",
106
+
EnvVars: []string{"COCOON_SMTP_EMAIL"},
100
107
},
101
108
&cli.StringFlag{
102
-
Name: "smtp-name",
103
-
Required: false,
104
-
EnvVars: []string{"COCOON_SMTP_NAME"},
109
+
Name: "smtp-name",
110
+
EnvVars: []string{"COCOON_SMTP_NAME"},
105
111
},
106
112
&cli.BoolFlag{
107
113
Name: "s3-backups-enabled",
108
114
EnvVars: []string{"COCOON_S3_BACKUPS_ENABLED"},
115
+
},
116
+
&cli.BoolFlag{
117
+
Name: "s3-blobstore-enabled",
118
+
EnvVars: []string{"COCOON_S3_BLOBSTORE_ENABLED"},
109
119
},
110
120
&cli.StringFlag{
111
121
Name: "s3-region",
···
128
138
EnvVars: []string{"COCOON_S3_SECRET_KEY"},
129
139
},
130
140
&cli.StringFlag{
131
-
Name: "session-secret",
132
-
EnvVars: []string{"COCOON_SESSION_SECRET"},
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.",
133
144
},
134
145
&cli.StringFlag{
135
-
Name: "default-atproto-proxy",
136
-
EnvVars: []string{"COCOON_DEFAULT_ATPROTO_PROXY"},
137
-
Value: "did:web:api.bsky.app#bsky_appview",
146
+
Name: "session-secret",
147
+
EnvVars: []string{"COCOON_SESSION_SECRET"},
138
148
},
139
149
&cli.StringFlag{
140
150
Name: "blockstore-variant",
141
151
EnvVars: []string{"COCOON_BLOCKSTORE_VARIANT"},
142
152
Value: "sqlite",
143
153
},
154
+
&cli.StringFlag{
155
+
Name: "fallback-proxy",
156
+
EnvVars: []string{"COCOON_FALLBACK_PROXY"},
157
+
},
158
+
telemetry.CLIFlagDebug,
159
+
telemetry.CLIFlagMetricsListenAddress,
144
160
},
145
161
Commands: []*cli.Command{
146
162
runServe,
···
164
180
Flags: []cli.Flag{},
165
181
Action: func(cmd *cli.Context) error {
166
182
183
+
logger := telemetry.StartLogger(cmd)
184
+
telemetry.StartMetrics(cmd)
185
+
167
186
s, err := server.New(&server.Args{
187
+
Logger: logger,
168
188
Addr: cmd.String("addr"),
169
189
DbName: cmd.String("db-name"),
190
+
DbType: cmd.String("db-type"),
191
+
DatabaseURL: cmd.String("database-url"),
170
192
Did: cmd.String("did"),
171
193
Hostname: cmd.String("hostname"),
172
194
RotationKeyPath: cmd.String("rotation-key-path"),
···
175
197
Version: Version,
176
198
Relays: cmd.StringSlice("relays"),
177
199
AdminPassword: cmd.String("admin-password"),
200
+
RequireInvite: cmd.Bool("require-invite"),
178
201
SmtpUser: cmd.String("smtp-user"),
179
202
SmtpPass: cmd.String("smtp-pass"),
180
203
SmtpHost: cmd.String("smtp-host"),
···
182
205
SmtpEmail: cmd.String("smtp-email"),
183
206
SmtpName: cmd.String("smtp-name"),
184
207
S3Config: &server.S3Config{
185
-
BackupsEnabled: cmd.Bool("s3-backups-enabled"),
186
-
Region: cmd.String("s3-region"),
187
-
Bucket: cmd.String("s3-bucket"),
188
-
Endpoint: cmd.String("s3-endpoint"),
189
-
AccessKey: cmd.String("s3-access-key"),
190
-
SecretKey: cmd.String("s3-secret-key"),
208
+
BackupsEnabled: cmd.Bool("s3-backups-enabled"),
209
+
BlobstoreEnabled: cmd.Bool("s3-blobstore-enabled"),
210
+
Region: cmd.String("s3-region"),
211
+
Bucket: cmd.String("s3-bucket"),
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"),
191
216
},
192
-
SessionSecret: cmd.String("session-secret"),
193
-
DefaultAtprotoProxy: cmd.String("default-atproto-proxy"),
194
-
BlockstoreVariant: server.MustReturnBlockstoreVariant(cmd.String("blockstore-variant")),
217
+
SessionSecret: cmd.String("session-secret"),
218
+
BlockstoreVariant: server.MustReturnBlockstoreVariant(cmd.String("blockstore-variant")),
219
+
FallbackProxy: cmd.String("fallback-proxy"),
195
220
})
196
221
if err != nil {
197
222
fmt.Printf("error creating cocoon: %v", err)
···
218
243
},
219
244
},
220
245
Action: func(cmd *cli.Context) error {
221
-
key, err := crypto.GeneratePrivateKeyK256()
246
+
key, err := atcrypto.GeneratePrivateKeyK256()
222
247
if err != nil {
223
248
return err
224
249
}
···
288
313
},
289
314
},
290
315
Action: func(cmd *cli.Context) error {
291
-
db, err := newDb()
316
+
db, err := newDb(cmd)
292
317
if err != nil {
293
318
return err
294
319
}
···
327
352
},
328
353
},
329
354
Action: func(cmd *cli.Context) error {
330
-
db, err := newDb()
355
+
db, err := newDb(cmd)
331
356
if err != nil {
332
357
return err
333
358
}
···
354
379
},
355
380
}
356
381
357
-
func newDb() (*gorm.DB, error) {
358
-
return gorm.Open(sqlite.Open("cocoon.db"), &gorm.Config{})
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
+
}
359
405
}
+56
create-initial-invite.sh
+56
create-initial-invite.sh
···
1
+
#!/bin/sh
2
+
3
+
INVITE_FILE="/keys/initial-invite-code.txt"
4
+
MARKER="/keys/.invite_created"
5
+
6
+
# Check if invite code was already created
7
+
if [ -f "$MARKER" ]; then
8
+
echo "โ Initial invite code already created"
9
+
exit 0
10
+
fi
11
+
12
+
echo "Waiting for database to be ready..."
13
+
sleep 10
14
+
15
+
# Try to create invite code - retry until database is ready
16
+
MAX_ATTEMPTS=30
17
+
ATTEMPT=0
18
+
INVITE_CODE=""
19
+
20
+
while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do
21
+
ATTEMPT=$((ATTEMPT + 1))
22
+
OUTPUT=$(/cocoon create-invite-code --uses 1 2>&1)
23
+
INVITE_CODE=$(echo "$OUTPUT" | grep -oE '[a-zA-Z0-9]{8}-[a-zA-Z0-9]{8}' || echo "")
24
+
25
+
if [ -n "$INVITE_CODE" ]; then
26
+
break
27
+
fi
28
+
29
+
if [ $((ATTEMPT % 5)) -eq 0 ]; then
30
+
echo " Waiting for database... ($ATTEMPT/$MAX_ATTEMPTS)"
31
+
fi
32
+
sleep 2
33
+
done
34
+
35
+
if [ -n "$INVITE_CODE" ]; then
36
+
echo ""
37
+
echo "โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ"
38
+
echo "โ SAVE THIS INVITE CODE! โ"
39
+
echo "โ โ"
40
+
echo "โ $INVITE_CODE โ"
41
+
echo "โ โ"
42
+
echo "โ Use this to create your first โ"
43
+
echo "โ account on your PDS. โ"
44
+
echo "โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ"
45
+
echo ""
46
+
47
+
echo "$INVITE_CODE" > "$INVITE_FILE"
48
+
echo "โ Invite code saved to: $INVITE_FILE"
49
+
50
+
touch "$MARKER"
51
+
echo "โ Initial setup complete!"
52
+
else
53
+
echo "โ Failed to create invite code"
54
+
echo "Output: $OUTPUT"
55
+
exit 1
56
+
fi
+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
+130
docker-compose.yaml
+130
docker-compose.yaml
···
1
+
version: '3.8'
2
+
3
+
services:
4
+
init-keys:
5
+
build:
6
+
context: .
7
+
dockerfile: Dockerfile
8
+
image: ghcr.io/haileyok/cocoon:latest
9
+
container_name: cocoon-init-keys
10
+
volumes:
11
+
- ./keys:/keys
12
+
- ./data:/data/cocoon
13
+
- ./init-keys.sh:/init-keys.sh:ro
14
+
environment:
15
+
COCOON_DID: ${COCOON_DID}
16
+
COCOON_HOSTNAME: ${COCOON_HOSTNAME}
17
+
COCOON_ROTATION_KEY_PATH: /keys/rotation.key
18
+
COCOON_JWK_PATH: /keys/jwk.key
19
+
COCOON_CONTACT_EMAIL: ${COCOON_CONTACT_EMAIL}
20
+
COCOON_RELAYS: ${COCOON_RELAYS:-https://bsky.network}
21
+
COCOON_ADMIN_PASSWORD: ${COCOON_ADMIN_PASSWORD}
22
+
entrypoint: ["/bin/sh", "/init-keys.sh"]
23
+
restart: "no"
24
+
25
+
cocoon:
26
+
build:
27
+
context: .
28
+
dockerfile: Dockerfile
29
+
image: ghcr.io/haileyok/cocoon:latest
30
+
container_name: cocoon-pds
31
+
network_mode: host
32
+
depends_on:
33
+
init-keys:
34
+
condition: service_completed_successfully
35
+
volumes:
36
+
- ./data:/data/cocoon
37
+
- ./keys/rotation.key:/keys/rotation.key:ro
38
+
- ./keys/jwk.key:/keys/jwk.key:ro
39
+
environment:
40
+
# Required settings
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
+
COCOON_SESSION_SECRET: ${COCOON_SESSION_SECRET}
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
58
+
COCOON_SMTP_USER: ${COCOON_SMTP_USER:-}
59
+
COCOON_SMTP_PASS: ${COCOON_SMTP_PASS:-}
60
+
COCOON_SMTP_HOST: ${COCOON_SMTP_HOST:-}
61
+
COCOON_SMTP_PORT: ${COCOON_SMTP_PORT:-}
62
+
COCOON_SMTP_EMAIL: ${COCOON_SMTP_EMAIL:-}
63
+
COCOON_SMTP_NAME: ${COCOON_SMTP_NAME:-}
64
+
65
+
# Optional: S3 configuration
66
+
COCOON_S3_BACKUPS_ENABLED: ${COCOON_S3_BACKUPS_ENABLED:-false}
67
+
COCOON_S3_BLOBSTORE_ENABLED: ${COCOON_S3_BLOBSTORE_ENABLED:-false}
68
+
COCOON_S3_REGION: ${COCOON_S3_REGION:-}
69
+
COCOON_S3_BUCKET: ${COCOON_S3_BUCKET:-}
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:-}
77
+
restart: unless-stopped
78
+
healthcheck:
79
+
test: ["CMD", "curl", "-f", "http://localhost:8080/xrpc/_health"]
80
+
interval: 30s
81
+
timeout: 10s
82
+
retries: 3
83
+
start_period: 40s
84
+
85
+
create-invite:
86
+
build:
87
+
context: .
88
+
dockerfile: Dockerfile
89
+
image: ghcr.io/haileyok/cocoon:latest
90
+
container_name: cocoon-create-invite
91
+
network_mode: host
92
+
volumes:
93
+
- ./keys:/keys
94
+
- ./create-initial-invite.sh:/create-initial-invite.sh:ro
95
+
environment:
96
+
COCOON_DID: ${COCOON_DID}
97
+
COCOON_HOSTNAME: ${COCOON_HOSTNAME}
98
+
COCOON_ROTATION_KEY_PATH: /keys/rotation.key
99
+
COCOON_JWK_PATH: /keys/jwk.key
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"]
109
+
restart: "no"
110
+
111
+
caddy:
112
+
image: caddy:2-alpine
113
+
container_name: cocoon-caddy
114
+
network_mode: host
115
+
volumes:
116
+
- ./Caddyfile:/etc/caddy/Caddyfile:ro
117
+
- caddy_data:/data
118
+
- caddy_config:/config
119
+
restart: unless-stopped
120
+
environment:
121
+
COCOON_HOSTNAME: ${COCOON_HOSTNAME}
122
+
CADDY_ACME_EMAIL: ${COCOON_CONTACT_EMAIL:-}
123
+
124
+
volumes:
125
+
data:
126
+
driver: local
127
+
caddy_data:
128
+
driver: local
129
+
caddy_config:
130
+
driver: local
+21
-19
go.mod
+21
-19
go.mod
···
1
1
module github.com/haileyok/cocoon
2
2
3
-
go 1.24.1
3
+
go 1.24.5
4
4
5
5
require (
6
6
github.com/Azure/go-autorest/autorest/to v0.4.1
7
7
github.com/aws/aws-sdk-go v1.55.7
8
-
github.com/bluesky-social/indigo v0.0.0-20250414202759-826fcdeaa36b
8
+
github.com/bluesky-social/go-util v0.0.0-20251012040650-2ebbf57f5934
9
+
github.com/bluesky-social/indigo v0.0.0-20251009212240-20524de167fe
9
10
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792
10
11
github.com/domodwyer/mailyak/v3 v3.6.2
11
12
github.com/go-pkgz/expirable-cache/v3 v3.0.0
12
13
github.com/go-playground/validator v9.31.0+incompatible
13
14
github.com/golang-jwt/jwt/v4 v4.5.2
14
-
github.com/google/uuid v1.4.0
15
+
github.com/google/uuid v1.6.0
15
16
github.com/gorilla/sessions v1.4.0
16
17
github.com/gorilla/websocket v1.5.1
17
18
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b
18
19
github.com/hashicorp/golang-lru/v2 v2.0.7
19
20
github.com/ipfs/go-block-format v0.2.0
20
21
github.com/ipfs/go-cid v0.4.1
22
+
github.com/ipfs/go-ipfs-blockstore v1.3.1
21
23
github.com/ipfs/go-ipld-cbor v0.1.0
22
24
github.com/ipld/go-car v0.6.1-0.20230509095817-92d28eb23ba4
23
25
github.com/joho/godotenv v1.5.1
24
26
github.com/labstack/echo-contrib v0.17.4
25
27
github.com/labstack/echo/v4 v4.13.3
26
-
github.com/lestrrat-go/jwx/v2 v2.0.12
28
+
github.com/lestrrat-go/jwx/v2 v2.0.21
27
29
github.com/multiformats/go-multihash v0.2.3
30
+
github.com/prometheus/client_golang v1.23.2
28
31
github.com/samber/slog-echo v1.16.1
29
32
github.com/urfave/cli/v2 v2.27.6
30
33
github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e
31
34
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b
32
-
golang.org/x/crypto v0.38.0
35
+
golang.org/x/crypto v0.41.0
36
+
gorm.io/driver/postgres v1.5.7
33
37
gorm.io/driver/sqlite v1.5.7
34
38
gorm.io/gorm v1.25.12
35
39
)
···
55
59
github.com/gorilla/securecookie v1.1.2 // indirect
56
60
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect
57
61
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
58
-
github.com/hashicorp/go-retryablehttp v0.7.5 // indirect
62
+
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
59
63
github.com/hashicorp/golang-lru v1.0.2 // indirect
60
64
github.com/ipfs/bbloom v0.0.4 // indirect
61
65
github.com/ipfs/go-blockservice v0.5.2 // indirect
62
66
github.com/ipfs/go-datastore v0.6.0 // indirect
63
-
github.com/ipfs/go-ipfs-blockstore v1.3.1 // indirect
64
67
github.com/ipfs/go-ipfs-ds-help v1.1.1 // indirect
65
68
github.com/ipfs/go-ipfs-exchange-interface v0.2.1 // indirect
66
69
github.com/ipfs/go-ipfs-util v0.0.3 // indirect
···
76
79
github.com/ipld/go-ipld-prime v0.21.0 // indirect
77
80
github.com/jackc/pgpassfile v1.0.0 // indirect
78
81
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
79
-
github.com/jackc/pgx/v5 v5.5.0 // indirect
82
+
github.com/jackc/pgx/v5 v5.5.4 // indirect
80
83
github.com/jackc/puddle/v2 v2.2.1 // indirect
81
84
github.com/jbenet/goprocess v0.1.4 // indirect
82
85
github.com/jinzhu/inflection v1.0.0 // indirect
···
85
88
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
86
89
github.com/labstack/gommon v0.4.2 // indirect
87
90
github.com/leodido/go-urn v1.4.0 // indirect
88
-
github.com/lestrrat-go/blackmagic v1.0.1 // indirect
91
+
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
89
92
github.com/lestrrat-go/httpcc v1.0.1 // indirect
90
-
github.com/lestrrat-go/httprc v1.0.4 // indirect
93
+
github.com/lestrrat-go/httprc v1.0.5 // indirect
91
94
github.com/lestrrat-go/iter v1.0.2 // indirect
92
95
github.com/lestrrat-go/option v1.0.1 // indirect
93
96
github.com/mattn/go-colorable v0.1.14 // indirect
···
102
105
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
103
106
github.com/opentracing/opentracing-go v1.2.0 // indirect
104
107
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f // indirect
105
-
github.com/prometheus/client_golang v1.22.0 // indirect
106
108
github.com/prometheus/client_model v0.6.2 // indirect
107
-
github.com/prometheus/common v0.63.0 // indirect
109
+
github.com/prometheus/common v0.66.1 // indirect
108
110
github.com/prometheus/procfs v0.16.1 // indirect
109
111
github.com/russross/blackfriday/v2 v2.1.0 // indirect
110
112
github.com/samber/lo v1.49.1 // indirect
···
114
116
github.com/valyala/fasttemplate v1.2.2 // indirect
115
117
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
116
118
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect
117
-
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect
119
+
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
118
120
go.opentelemetry.io/otel v1.29.0 // indirect
119
121
go.opentelemetry.io/otel/metric v1.29.0 // indirect
120
122
go.opentelemetry.io/otel/trace v1.29.0 // indirect
121
123
go.uber.org/atomic v1.11.0 // indirect
122
124
go.uber.org/multierr v1.11.0 // indirect
123
125
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
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
128
131
golang.org/x/time v0.11.0 // indirect
129
132
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
130
-
google.golang.org/protobuf v1.36.6 // indirect
133
+
google.golang.org/protobuf v1.36.9 // indirect
131
134
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
132
135
gopkg.in/inf.v0 v0.9.1 // indirect
133
-
gorm.io/driver/postgres v1.5.7 // indirect
134
136
lukechampine.com/blake3 v1.2.1 // indirect
135
137
)
+52
-78
go.sum
+52
-78
go.sum
···
16
16
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
17
17
github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY=
18
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-20250414202759-826fcdeaa36b h1:elwfbe+W7GkUmPKFX1h7HaeHvC/kC0XJWfiEHC62xPg=
20
-
github.com/bluesky-social/indigo v0.0.0-20250414202759-826fcdeaa36b/go.mod h1:yjdhLA1LkK8VDS/WPUoYPo25/Hq/8rX38Ftr67EsqKY=
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=
21
23
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
22
24
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
23
25
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 h1:R8vQdOQdZ9Y3SkEwmHoWBmX1DNXhXZqlTpq6s4tyJGc=
···
34
36
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
35
37
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
36
38
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
39
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
39
40
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
40
41
github.com/domodwyer/mailyak/v3 v3.6.2 h1:x3tGMsyFhTCaxp6ycgR0FE/bu5QiNp+hetUuCOBXMn8=
41
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=
42
45
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
43
46
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
44
47
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
···
77
80
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
78
81
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
79
82
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=
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=
82
85
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
83
86
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
84
87
github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o=
···
95
98
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b/go.mod h1:VzxiSdG6j1pi7rwGm/xYI5RbtpBgM8sARDXlvEvxlu0=
96
99
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
97
100
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=
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=
102
105
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
103
106
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
104
107
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
···
113
116
github.com/ipfs/go-block-format v0.2.0/go.mod h1:+jpL11nFx5A/SPpsoBn6Bzkra/zaArfSmsknbPMYgzM=
114
117
github.com/ipfs/go-blockservice v0.5.2 h1:in9Bc+QcXwd1apOVM7Un9t8tixPKdaHQFdLSUM1Xgk8=
115
118
github.com/ipfs/go-blockservice v0.5.2/go.mod h1:VpMblFEqG67A/H2sHKAemeH9vlURVavlysbdUI632yk=
116
-
github.com/ipfs/go-bs-sqlite3 v0.0.0-20221122195556-bfcee1be620d h1:9V+GGXCuOfDiFpdAHz58q9mKLg447xp0cQKvqQrAwYE=
117
-
github.com/ipfs/go-bs-sqlite3 v0.0.0-20221122195556-bfcee1be620d/go.mod h1:pMbnFyNAGjryYCLCe59YDLRv/ujdN+zGJBT1umlvYRM=
118
119
github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s=
119
120
github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk=
120
121
github.com/ipfs/go-datastore v0.6.0 h1:JKyz+Gvz1QEZw0LsX1IBn+JFCJQH4SJVFtM4uWU0Myk=
···
174
175
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
175
176
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
176
177
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
177
-
github.com/jackc/pgx/v5 v5.5.0 h1:NxstgwndsTRy7eq9/kqYc/BZh5w2hHJV86wjvO+1xPw=
178
-
github.com/jackc/pgx/v5 v5.5.0/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
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=
179
180
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
180
181
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
181
182
github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
···
197
198
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
198
199
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
199
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=
200
203
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
201
204
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
202
205
github.com/koron/go-ssdp v0.0.3 h1:JivLMY45N76b4p/vsWGOKewBQu6uf39y8l+AQ7sDKx8=
···
208
211
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
209
212
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
210
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=
211
216
github.com/labstack/echo-contrib v0.17.4 h1:g5mfsrJfJTKv+F5uNKCyrjLK7js+ZW6HTjg4FnDxxgk=
212
217
github.com/labstack/echo-contrib v0.17.4/go.mod h1:9O7ZPAHUeMGTOAfg80YqQduHzt0CzLak36PZRldYrZ0=
213
218
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
···
216
221
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
217
222
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
218
223
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
219
-
github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80=
220
-
github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
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=
221
226
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
222
227
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
223
-
github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJGdI8=
224
-
github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
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=
225
230
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
226
231
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
227
-
github.com/lestrrat-go/jwx/v2 v2.0.12 h1:3d589+5w/b9b7S3DneICPW16AqTyYXB7VRjgluSDWeA=
228
-
github.com/lestrrat-go/jwx/v2 v2.0.12/go.mod h1:Mq4KN1mM7bp+5z/W5HS8aCNs5RKZ911G/0y2qUjAQuQ=
229
-
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
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=
230
234
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
231
235
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
232
236
github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8=
···
291
295
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
292
296
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f h1:VXTQfuJj9vKR4TCkEuWIckKvdHFeJH/huIFJ9/cXOB0=
293
297
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw=
294
-
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
295
-
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
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=
296
300
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
297
301
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
298
-
github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k=
299
-
github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18=
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=
300
304
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
301
305
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
302
306
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
···
319
323
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
320
324
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
321
325
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
322
-
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
323
-
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
324
-
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
325
326
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
326
327
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
327
328
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
328
329
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
329
330
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
330
-
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
331
-
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
332
-
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
333
-
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
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=
334
333
github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
335
334
github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g=
336
335
github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
···
351
350
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
352
351
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
353
352
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
354
-
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
355
353
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b h1:CzigHMRySiX3drau9C6Q5CAbNIApmLdat5jPMqChvDA=
356
354
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b/go.mod h1:/y/V339mxv2sZmYYR64O07VuCpdNZqCTwO8ZcouTMI8=
357
355
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 h1:qwDnMxjkyLmAFgcfgTnfJrmYKWhHnci3GjDqcZp1M3Q=
358
356
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02/go.mod h1:JTnUj0mpYiAsuZLmKjTx/ex3AtMowcCgnE7YNyCEP0I=
359
-
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24=
360
-
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo=
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=
361
359
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
362
360
go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
363
361
go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc=
···
369
367
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
370
368
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
371
369
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
372
-
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
373
-
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
370
+
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
371
+
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
374
372
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
375
373
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
376
374
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
···
380
378
go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
381
379
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
382
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
383
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
384
384
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
385
385
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
386
386
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
387
-
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
388
-
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
389
-
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
390
-
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
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=
391
389
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ=
392
390
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE=
393
391
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
···
395
393
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
396
394
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
397
395
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
398
-
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
399
-
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
400
-
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
401
-
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
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=
402
398
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
403
399
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
404
400
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
405
401
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
406
402
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
407
-
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
408
403
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
409
-
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
410
-
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
411
-
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
412
-
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
413
-
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
404
+
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
405
+
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
414
406
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
415
407
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
416
408
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
417
409
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
418
-
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
419
-
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
420
-
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
421
-
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
410
+
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
411
+
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
422
412
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
423
413
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
424
414
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
425
415
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
426
416
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
427
417
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
428
-
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
429
418
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
430
-
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
431
-
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
432
-
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
433
419
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
434
420
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
435
-
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
436
-
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
437
-
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
438
-
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
421
+
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
422
+
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
439
423
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
440
-
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
441
-
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
442
-
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
443
-
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
444
424
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
445
425
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
446
-
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
447
-
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
448
-
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
449
-
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
450
-
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
451
-
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
426
+
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
427
+
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
452
428
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
453
429
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
454
430
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
···
461
437
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
462
438
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
463
439
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
464
-
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
465
-
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
466
-
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
467
-
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
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=
468
442
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
469
443
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
470
444
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
471
445
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
472
446
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
473
447
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
474
-
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
475
-
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
448
+
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
449
+
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
476
450
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
477
451
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
478
452
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+74
-55
identity/identity.go
+74
-55
identity/identity.go
···
13
13
"github.com/bluesky-social/indigo/util"
14
14
)
15
15
16
-
func ResolveHandle(ctx context.Context, cli *http.Client, handle string) (string, error) {
17
-
if cli == nil {
18
-
cli = util.RobustHTTPClient()
16
+
func ResolveHandleFromTXT(ctx context.Context, handle string) (string, error) {
17
+
name := fmt.Sprintf("_atproto.%s", handle)
18
+
recs, err := net.LookupTXT(name)
19
+
if err != nil {
20
+
return "", fmt.Errorf("handle could not be resolved via txt: %w", err)
21
+
}
22
+
23
+
for _, rec := range recs {
24
+
if strings.HasPrefix(rec, "did=") {
25
+
maybeDid := strings.Split(rec, "did=")[1]
26
+
if _, err := syntax.ParseDID(maybeDid); err == nil {
27
+
return maybeDid, nil
28
+
}
29
+
}
30
+
}
31
+
32
+
return "", fmt.Errorf("handle could not be resolved via txt: no record found")
33
+
}
34
+
35
+
func ResolveHandleFromWellKnown(ctx context.Context, cli *http.Client, handle string) (string, error) {
36
+
ustr := fmt.Sprintf("https://%s/.well-known/atproto-did", handle)
37
+
req, err := http.NewRequestWithContext(
38
+
ctx,
39
+
"GET",
40
+
ustr,
41
+
nil,
42
+
)
43
+
if err != nil {
44
+
return "", fmt.Errorf("handle could not be resolved via web: %w", err)
19
45
}
20
46
21
-
var did string
47
+
resp, err := cli.Do(req)
48
+
if err != nil {
49
+
return "", fmt.Errorf("handle could not be resolved via web: %w", err)
50
+
}
51
+
defer resp.Body.Close()
22
52
23
-
_, err := syntax.ParseHandle(handle)
53
+
b, err := io.ReadAll(resp.Body)
24
54
if err != nil {
25
-
return "", err
55
+
return "", fmt.Errorf("handle could not be resolved via web: %w", err)
26
56
}
27
57
28
-
recs, err := net.LookupTXT(fmt.Sprintf("_atproto.%s", handle))
29
-
if err == nil {
30
-
for _, rec := range recs {
31
-
if strings.HasPrefix(rec, "did=") {
32
-
did = strings.Split(rec, "did=")[1]
33
-
break
34
-
}
35
-
}
36
-
} else {
37
-
fmt.Printf("erorr getting txt records: %v\n", err)
58
+
if resp.StatusCode != http.StatusOK {
59
+
return "", fmt.Errorf("handle could not be resolved via web: invalid status code %d", resp.StatusCode)
38
60
}
39
61
40
-
if did == "" {
41
-
req, err := http.NewRequestWithContext(
42
-
ctx,
43
-
"GET",
44
-
fmt.Sprintf("https://%s/.well-known/atproto-did", handle),
45
-
nil,
46
-
)
47
-
if err != nil {
48
-
return "", nil
49
-
}
62
+
maybeDid := string(b)
50
63
51
-
resp, err := http.DefaultClient.Do(req)
52
-
if err != nil {
53
-
return "", nil
54
-
}
55
-
defer resp.Body.Close()
64
+
if _, err := syntax.ParseDID(maybeDid); err != nil {
65
+
return "", fmt.Errorf("handle could not be resolved via web: invalid did in document")
66
+
}
56
67
57
-
if resp.StatusCode != http.StatusOK {
58
-
io.Copy(io.Discard, resp.Body)
59
-
return "", fmt.Errorf("unable to resolve handle")
60
-
}
68
+
return maybeDid, nil
69
+
}
61
70
62
-
b, err := io.ReadAll(resp.Body)
63
-
if err != nil {
64
-
return "", err
65
-
}
71
+
func ResolveHandle(ctx context.Context, cli *http.Client, handle string) (string, error) {
72
+
if cli == nil {
73
+
cli = util.RobustHTTPClient()
74
+
}
66
75
67
-
maybeDid := string(b)
76
+
_, err := syntax.ParseHandle(handle)
77
+
if err != nil {
78
+
return "", err
79
+
}
68
80
69
-
if _, err := syntax.ParseDID(maybeDid); err != nil {
70
-
return "", fmt.Errorf("unable to resolve handle")
71
-
}
81
+
if maybeDidFromTxt, err := ResolveHandleFromTXT(ctx, handle); err == nil {
82
+
return maybeDidFromTxt, nil
83
+
}
72
84
73
-
did = maybeDid
85
+
if maybeDidFromWeb, err := ResolveHandleFromWellKnown(ctx, cli, handle); err == nil {
86
+
return maybeDidFromWeb, nil
74
87
}
75
88
76
-
return did, nil
89
+
return "", fmt.Errorf("handle could not be resolved")
90
+
}
91
+
92
+
func DidToDocUrl(did string) (string, error) {
93
+
if strings.HasPrefix(did, "did:plc:") {
94
+
return fmt.Sprintf("https://plc.directory/%s", did), nil
95
+
} else if after, ok := strings.CutPrefix(did, "did:web:"); ok {
96
+
return fmt.Sprintf("https://%s/.well-known/did.json", after), nil
97
+
} else {
98
+
return "", fmt.Errorf("did was not a supported did type")
99
+
}
77
100
}
78
101
79
102
func FetchDidDoc(ctx context.Context, cli *http.Client, did string) (*DidDoc, error) {
···
81
104
cli = util.RobustHTTPClient()
82
105
}
83
106
84
-
var ustr string
85
-
if strings.HasPrefix(did, "did:plc:") {
86
-
ustr = fmt.Sprintf("https://plc.directory/%s", did)
87
-
} else if strings.HasPrefix(did, "did:web:") {
88
-
ustr = fmt.Sprintf("https://%s/.well-known/did.json", strings.TrimPrefix(did, "did:web:"))
89
-
} else {
90
-
return nil, fmt.Errorf("did was not a supported did type")
107
+
ustr, err := DidToDocUrl(did)
108
+
if err != nil {
109
+
return nil, err
91
110
}
92
111
93
112
req, err := http.NewRequestWithContext(ctx, "GET", ustr, nil)
···
95
114
return nil, err
96
115
}
97
116
98
-
resp, err := http.DefaultClient.Do(req)
117
+
resp, err := cli.Do(req)
99
118
if err != nil {
100
119
return nil, err
101
120
}
···
103
122
104
123
if resp.StatusCode != 200 {
105
124
io.Copy(io.Discard, resp.Body)
106
-
return nil, fmt.Errorf("could not find identity in plc registry")
125
+
return nil, fmt.Errorf("unable to find did doc at url. did: %s. url: %s", did, ustr)
107
126
}
108
127
109
128
var diddoc DidDoc
···
127
146
return nil, err
128
147
}
129
148
130
-
resp, err := http.DefaultClient.Do(req)
149
+
resp, err := cli.Do(req)
131
150
if err != nil {
132
151
return nil, err
133
152
}
+15
-5
identity/passport.go
+15
-5
identity/passport.go
···
19
19
type Passport struct {
20
20
h *http.Client
21
21
bc BackingCache
22
-
lk sync.Mutex
22
+
mu sync.RWMutex
23
23
}
24
24
25
25
func NewPassport(h *http.Client, bc BackingCache) *Passport {
···
30
30
return &Passport{
31
31
h: h,
32
32
bc: bc,
33
-
lk: sync.Mutex{},
34
33
}
35
34
}
36
35
···
38
37
skipCache, _ := ctx.Value("skip-cache").(bool)
39
38
40
39
if !skipCache {
40
+
p.mu.RLock()
41
41
cached, ok := p.bc.GetDoc(did)
42
+
p.mu.RUnlock()
43
+
42
44
if ok {
43
45
return cached, nil
44
46
}
45
47
}
46
48
47
-
p.lk.Lock() // this is pretty pathetic, and i should rethink this. but for now, fuck it
48
-
defer p.lk.Unlock()
49
-
50
49
doc, err := FetchDidDoc(ctx, p.h, did)
51
50
if err != nil {
52
51
return nil, err
53
52
}
54
53
54
+
p.mu.Lock()
55
55
p.bc.PutDoc(did, doc)
56
+
p.mu.Unlock()
56
57
57
58
return doc, nil
58
59
}
···
61
62
skipCache, _ := ctx.Value("skip-cache").(bool)
62
63
63
64
if !skipCache {
65
+
p.mu.RLock()
64
66
cached, ok := p.bc.GetDid(handle)
67
+
p.mu.RUnlock()
68
+
65
69
if ok {
66
70
return cached, nil
67
71
}
···
72
76
return "", err
73
77
}
74
78
79
+
p.mu.Lock()
75
80
p.bc.PutDid(handle, did)
81
+
p.mu.Unlock()
76
82
77
83
return did, nil
78
84
}
79
85
80
86
func (p *Passport) BustDoc(ctx context.Context, did string) error {
87
+
p.mu.Lock()
88
+
defer p.mu.Unlock()
81
89
return p.bc.BustDoc(did)
82
90
}
83
91
84
92
func (p *Passport) BustDid(ctx context.Context, handle string) error {
93
+
p.mu.Lock()
94
+
defer p.mu.Unlock()
85
95
return p.bc.BustDid(handle)
86
96
}
+1
-1
identity/types.go
+1
-1
identity/types.go
···
4
4
Context []string `json:"@context"`
5
5
Id string `json:"id"`
6
6
AlsoKnownAs []string `json:"alsoKnownAs"`
7
-
VerificationMethods []DidDocVerificationMethod `json:"verificationMethods"`
7
+
VerificationMethods []DidDocVerificationMethod `json:"verificationMethod"`
8
8
Service []DidDocService `json:"service"`
9
9
}
10
10
+34
init-keys.sh
+34
init-keys.sh
···
1
+
#!/bin/sh
2
+
set -e
3
+
4
+
mkdir -p /keys
5
+
mkdir -p /data/cocoon
6
+
7
+
if [ ! -f /keys/rotation.key ]; then
8
+
echo "Generating rotation key..."
9
+
/cocoon create-rotation-key --out /keys/rotation.key 2>/dev/null || true
10
+
if [ -f /keys/rotation.key ]; then
11
+
echo "โ Rotation key generated at /keys/rotation.key"
12
+
else
13
+
echo "โ Failed to generate rotation key"
14
+
exit 1
15
+
fi
16
+
else
17
+
echo "โ Rotation key already exists"
18
+
fi
19
+
20
+
if [ ! -f /keys/jwk.key ]; then
21
+
echo "Generating JWK..."
22
+
/cocoon create-private-jwk --out /keys/jwk.key 2>/dev/null || true
23
+
if [ -f /keys/jwk.key ]; then
24
+
echo "โ JWK generated at /keys/jwk.key"
25
+
else
26
+
echo "โ Failed to generate JWK"
27
+
exit 1
28
+
fi
29
+
else
30
+
echo "โ JWK already exists"
31
+
fi
32
+
33
+
echo ""
34
+
echo "โ Key initialization complete!"
+19
-12
internal/db/db.go
+19
-12
internal/db/db.go
···
1
1
package db
2
2
3
3
import (
4
+
"context"
4
5
"sync"
5
6
6
7
"gorm.io/gorm"
···
19
20
}
20
21
}
21
22
22
-
func (db *DB) Create(value any, clauses []clause.Expression) *gorm.DB {
23
+
func (db *DB) Create(ctx context.Context, value any, clauses []clause.Expression) *gorm.DB {
23
24
db.mu.Lock()
24
25
defer db.mu.Unlock()
25
-
return db.cli.Clauses(clauses...).Create(value)
26
+
return db.cli.WithContext(ctx).Clauses(clauses...).Create(value)
26
27
}
27
28
28
-
func (db *DB) Exec(sql string, clauses []clause.Expression, values ...any) *gorm.DB {
29
+
func (db *DB) Save(ctx context.Context, value any, clauses []clause.Expression) *gorm.DB {
29
30
db.mu.Lock()
30
31
defer db.mu.Unlock()
31
-
return db.cli.Clauses(clauses...).Exec(sql, values...)
32
+
return db.cli.WithContext(ctx).Clauses(clauses...).Save(value)
32
33
}
33
34
34
-
func (db *DB) Raw(sql string, clauses []clause.Expression, values ...any) *gorm.DB {
35
-
return db.cli.Clauses(clauses...).Raw(sql, values...)
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...)
36
43
}
37
44
38
45
func (db *DB) AutoMigrate(models ...any) error {
39
46
return db.cli.AutoMigrate(models...)
40
47
}
41
48
42
-
func (db *DB) Delete(value any, clauses []clause.Expression) *gorm.DB {
49
+
func (db *DB) Delete(ctx context.Context, value any, clauses []clause.Expression) *gorm.DB {
43
50
db.mu.Lock()
44
51
defer db.mu.Unlock()
45
-
return db.cli.Clauses(clauses...).Delete(value)
52
+
return db.cli.WithContext(ctx).Clauses(clauses...).Delete(value)
46
53
}
47
54
48
-
func (db *DB) First(dest any, conds ...any) *gorm.DB {
49
-
return db.cli.First(dest, conds...)
55
+
func (db *DB) First(ctx context.Context, dest any, conds ...any) *gorm.DB {
56
+
return db.cli.WithContext(ctx).First(dest, conds...)
50
57
}
51
58
52
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
53
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.
54
61
// e.g. when we do apply writes we should also be using a transcation but we don't right now
55
-
func (db *DB) BeginDangerously() *gorm.DB {
56
-
return db.cli.Begin()
62
+
func (db *DB) BeginDangerously(ctx context.Context) *gorm.DB {
63
+
return db.cli.WithContext(ctx).Begin()
57
64
}
58
65
59
66
func (db *DB) Lock() {
+16
internal/helpers/helpers.go
+16
internal/helpers/helpers.go
···
32
32
return genericError(e, 400, msg)
33
33
}
34
34
35
+
func UnauthorizedError(e echo.Context, suffix *string) error {
36
+
msg := "Unauthorized"
37
+
if suffix != nil {
38
+
msg += ". " + *suffix
39
+
}
40
+
return genericError(e, 401, msg)
41
+
}
42
+
43
+
func ForbiddenError(e echo.Context, suffix *string) error {
44
+
msg := "Forbidden"
45
+
if suffix != nil {
46
+
msg += ". " + *suffix
47
+
}
48
+
return genericError(e, 403, msg)
49
+
}
50
+
35
51
func InvalidTokenError(e echo.Context) error {
36
52
return InputError(e, to.StringPtr("InvalidToken"))
37
53
}
+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
+
)
+38
-2
models/models.go
+38
-2
models/models.go
···
4
4
"context"
5
5
"time"
6
6
7
-
"github.com/bluesky-social/indigo/atproto/crypto"
7
+
"github.com/Azure/go-autorest/autorest/to"
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")
8
16
)
9
17
10
18
type Repo struct {
···
18
26
EmailUpdateCodeExpiresAt *time.Time
19
27
PasswordResetCode *string
20
28
PasswordResetCodeExpiresAt *time.Time
29
+
PlcOperationCode *string
30
+
PlcOperationCodeExpiresAt *time.Time
31
+
AccountDeleteCode *string
32
+
AccountDeleteCodeExpiresAt *time.Time
21
33
Password string
22
34
SigningKey []byte
23
35
Rev string
24
36
Root []byte
25
37
Preferences []byte
38
+
Deactivated bool
39
+
TwoFactorCode *string
40
+
TwoFactorCodeExpiresAt *time.Time
41
+
TwoFactorType TwoFactorType `gorm:"default:none"`
26
42
}
27
43
28
44
func (r *Repo) SignFor(ctx context.Context, did string, msg []byte) ([]byte, error) {
29
-
k, err := crypto.ParsePrivateBytesK256(r.SigningKey)
45
+
k, err := atcrypto.ParsePrivateBytesK256(r.SigningKey)
30
46
if err != nil {
31
47
return nil, err
32
48
}
···
39
55
return sig, nil
40
56
}
41
57
58
+
func (r *Repo) Status() *string {
59
+
var status *string
60
+
if r.Deactivated {
61
+
status = to.StringPtr("deactivated")
62
+
}
63
+
return status
64
+
}
65
+
66
+
func (r *Repo) Active() bool {
67
+
return r.Status() == nil
68
+
}
69
+
42
70
type Actor struct {
43
71
Did string `gorm:"primaryKey"`
44
72
Handle string `gorm:"uniqueIndex"`
···
92
120
Did string `gorm:"index;index:idx_blob_did_cid"`
93
121
Cid []byte `gorm:"index;index:idx_blob_did_cid"`
94
122
RefCount int
123
+
Storage string `gorm:"default:sqlite"`
95
124
}
96
125
97
126
type BlobPart struct {
···
100
129
Idx int `gorm:"primaryKey"`
101
130
Data []byte
102
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
+
}
+46
-23
oauth/client/manager.go
+46
-23
oauth/client/manager.go
···
22
22
cli *http.Client
23
23
logger *slog.Logger
24
24
jwksCache cache.Cache[string, jwk.Key]
25
-
metadataCache cache.Cache[string, Metadata]
25
+
metadataCache cache.Cache[string, *Metadata]
26
26
}
27
27
28
28
type ManagerArgs struct {
···
40
40
}
41
41
42
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)
43
+
metadataCache := cache.NewCache[string, *Metadata]().WithLRU().WithMaxKeys(500).WithTTL(5 * time.Minute)
44
44
45
45
return &Manager{
46
46
cli: args.Cli,
···
57
57
}
58
58
59
59
var jwks jwk.Key
60
-
if metadata.JWKS != nil {
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
-
k, err := helpers.ParseJWKFromBytes((*metadata.JWKS)[0])
64
-
if err != nil {
65
-
return nil, err
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")
66
84
}
67
-
jwks = k
68
-
} else if metadata.JWKSURI != nil {
69
-
maybeJwks, err := cm.getClientJwks(ctx, clientId, *metadata.JWKSURI)
70
-
if err != nil {
71
-
return nil, err
72
-
}
73
-
74
-
jwks = maybeJwks
75
85
}
76
86
77
87
return &Client{
···
81
91
}
82
92
83
93
func (cm *Manager) getClientMetadata(ctx context.Context, clientId string) (*Metadata, error) {
84
-
metadataCached, ok := cm.metadataCache.Get(clientId)
94
+
cached, ok := cm.metadataCache.Get(clientId)
85
95
if !ok {
86
96
req, err := http.NewRequestWithContext(ctx, "GET", clientId, nil)
87
97
if err != nil {
···
109
119
return nil, err
110
120
}
111
121
122
+
cm.metadataCache.Set(clientId, validated, 10*time.Minute)
123
+
112
124
return validated, nil
113
125
} else {
114
-
return &metadataCached, nil
126
+
return cached, nil
115
127
}
116
128
}
117
129
···
196
208
return nil, fmt.Errorf("error unmarshaling metadata: %w", err)
197
209
}
198
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
+
199
221
u, err := url.Parse(metadata.ClientURI)
200
222
if err != nil {
201
223
return nil, fmt.Errorf("unable to parse client uri: %w", err)
202
224
}
203
225
226
+
if metadata.ClientName == "" {
227
+
metadata.ClientName = metadata.ClientURI
228
+
}
229
+
204
230
if isLocalHostname(u.Hostname()) {
205
-
return nil, errors.New("`client_uri` hostname is invalid")
231
+
return nil, fmt.Errorf("`client_uri` hostname is invalid: %s", u.Hostname())
206
232
}
207
233
208
234
if metadata.Scope == "" {
···
262
288
return nil, errors.New("private_key_jwt auth method requires jwks or jwks_uri")
263
289
}
264
290
265
-
if metadata.JWKS != nil && len(*metadata.JWKS) == 0 {
291
+
if metadata.JWKS != nil && len(metadata.JWKS.Keys) == 0 {
266
292
return nil, errors.New("private_key_jwt auth method requires atleast one key in jwks")
267
293
}
268
294
···
341
367
if u.Scheme != "http" {
342
368
return nil, fmt.Errorf("loopback redirect uri %s must use http", ruri)
343
369
}
344
-
345
-
break
346
370
case u.Scheme == "http":
347
371
return nil, errors.New("only loopbvack redirect uris are allowed to use the `http` scheme")
348
372
case u.Scheme == "https":
349
373
if isLocalHostname(u.Hostname()) {
350
374
return nil, fmt.Errorf("redirect uri %s's domain must not be a local hostname", ruri)
351
375
}
352
-
break
353
376
case strings.Contains(u.Scheme, "."):
354
377
if metadata.ApplicationType != "native" {
355
378
return nil, errors.New("private-use uri scheme redirect uris are only allowed for native apps")
+20
-16
oauth/client/metadata.go
+20
-16
oauth/client/metadata.go
···
1
1
package client
2
2
3
3
type Metadata struct {
4
-
ClientID string `json:"client_id"`
5
-
ClientName string `json:"client_name"`
6
-
ClientURI string `json:"client_uri"`
7
-
LogoURI string `json:"logo_uri"`
8
-
TOSURI string `json:"tos_uri"`
9
-
PolicyURI string `json:"policy_uri"`
10
-
RedirectURIs []string `json:"redirect_uris"`
11
-
GrantTypes []string `json:"grant_types"`
12
-
ResponseTypes []string `json:"response_types"`
13
-
ApplicationType string `json:"application_type"`
14
-
DpopBoundAccessTokens bool `json:"dpop_bound_access_tokens"`
15
-
JWKSURI *string `json:"jwks_uri,omitempty"`
16
-
JWKS *[][]byte `json:"jwks,omitempty"`
17
-
Scope string `json:"scope"`
18
-
TokenEndpointAuthMethod string `json:"token_endpoint_auth_method"`
19
-
TokenEndpointAuthSigningAlg string `json:"token_endpoint_auth_signing_alg"`
4
+
ClientID string `json:"client_id"`
5
+
ClientName string `json:"client_name"`
6
+
ClientURI string `json:"client_uri"`
7
+
LogoURI string `json:"logo_uri"`
8
+
TOSURI string `json:"tos_uri"`
9
+
PolicyURI string `json:"policy_uri"`
10
+
RedirectURIs []string `json:"redirect_uris"`
11
+
GrantTypes []string `json:"grant_types"`
12
+
ResponseTypes []string `json:"response_types"`
13
+
ApplicationType string `json:"application_type"`
14
+
DpopBoundAccessTokens bool `json:"dpop_bound_access_tokens"`
15
+
JWKSURI *string `json:"jwks_uri,omitempty"`
16
+
JWKS *MetadataJwks `json:"jwks,omitempty"`
17
+
Scope string `json:"scope"`
18
+
TokenEndpointAuthMethod string `json:"token_endpoint_auth_method"`
19
+
TokenEndpointAuthSigningAlg string `json:"token_endpoint_auth_signing_alg"`
20
+
}
21
+
22
+
type MetadataJwks struct {
23
+
Keys []any `json:"keys"`
20
24
}
+1
-1
oauth/dpop/jti_cache.go
+1
-1
oauth/dpop/jti_cache.go
···
14
14
}
15
15
16
16
func newJTICache(size int) *jtiCache {
17
-
cache := cache.NewCache[string, bool]().WithTTL(24 * time.Hour).WithLRU().WithTTL(constants.JTITtl)
17
+
cache := cache.NewCache[string, bool]().WithTTL(24 * time.Hour).WithLRU().WithTTL(constants.JTITtl).WithMaxKeys(size)
18
18
return &jtiCache{
19
19
cache: cache,
20
20
mu: sync.Mutex{},
+10
-6
oauth/dpop/manager.go
+10
-6
oauth/dpop/manager.go
···
36
36
Hostname string
37
37
}
38
38
39
+
var (
40
+
ErrUseDpopNonce = errors.New("use_dpop_nonce")
41
+
)
42
+
39
43
func NewManager(args ManagerArgs) *Manager {
40
44
if args.Logger == nil {
41
45
args.Logger = slog.Default()
···
71
75
}
72
76
73
77
proof := extractProof(headers)
74
-
75
78
if proof == "" {
76
79
return nil, nil
77
80
}
···
193
196
194
197
nonce, _ := claims["nonce"].(string)
195
198
if nonce == "" {
196
-
// WARN: this _must_ be `use_dpop_nonce` for clients know they should make another request
197
-
return nil, errors.New("use_dpop_nonce")
199
+
// reference impl checks if self.nonce is not null before returning an error, but we always have a
200
+
// nonce so we do not bother checking
201
+
return nil, ErrUseDpopNonce
198
202
}
199
203
200
204
if nonce != "" && !dm.nonce.Check(nonce) {
201
-
// WARN: this _must_ be `use_dpop_nonce` so that clients will fetch a new nonce
202
-
return nil, errors.New("use_dpop_nonce")
205
+
// dpop nonce mismatch
206
+
return nil, ErrUseDpopNonce
203
207
}
204
208
205
209
ath, _ := claims["ath"].(string)
···
233
237
}
234
238
235
239
func extractProof(headers http.Header) string {
236
-
dpopHeaders := headers["Dpop"]
240
+
dpopHeaders := headers.Values("dpop")
237
241
switch len(dpopHeaders) {
238
242
case 0:
239
243
return ""
+3
-2
oauth/dpop/nonce.go
+3
-2
oauth/dpop/nonce.go
+3
-3
oauth/provider/client_auth.go
+3
-3
oauth/provider/client_auth.go
···
19
19
}
20
20
21
21
type AuthenticateClientRequestBase struct {
22
-
ClientID string `form:"client_id" json:"client_id" validate:"required"`
23
-
ClientAssertionType *string `form:"client_assertion_type" json:"client_assertion_type,omitempty"`
24
-
ClientAssertion *string `form:"client_assertion" json:"client_assertion,omitempty"`
22
+
ClientID string `form:"client_id" json:"client_id" query:"client_id" validate:"required"`
23
+
ClientAssertionType *string `form:"client_assertion_type" json:"client_assertion_type,omitempty" query:"client_assertion_type"`
24
+
ClientAssertion *string `form:"client_assertion" json:"client_assertion,omitempty" query:"client_assertion"`
25
25
}
26
26
27
27
func (p *Provider) AuthenticateClient(ctx context.Context, req AuthenticateClientRequestBase, proof *dpop.Proof, opts *AuthenticateClientOptions) (*client.Client, *ClientAuth, error) {
+9
-8
oauth/provider/models.go
+9
-8
oauth/provider/models.go
···
32
32
33
33
type ParRequest struct {
34
34
AuthenticateClientRequestBase
35
-
ResponseType string `form:"response_type" json:"response_type" validate:"required"`
36
-
CodeChallenge *string `form:"code_challenge" json:"code_challenge" validate:"required"`
37
-
CodeChallengeMethod string `form:"code_challenge_method" json:"code_challenge_method" validate:"required"`
38
-
State string `form:"state" json:"state" validate:"required"`
39
-
RedirectURI string `form:"redirect_uri" json:"redirect_uri" validate:"required"`
40
-
Scope string `form:"scope" json:"scope" validate:"required"`
41
-
LoginHint *string `form:"login_hint" json:"login_hint,omitempty"`
42
-
DpopJkt *string `form:"dpop_jkt" json:"dpop_jkt,omitempty"`
35
+
ResponseType string `form:"response_type" json:"response_type" query:"response_type" validate:"required"`
36
+
CodeChallenge *string `form:"code_challenge" json:"code_challenge" query:"code_challenge" validate:"required"`
37
+
CodeChallengeMethod string `form:"code_challenge_method" json:"code_challenge_method" query:"code_challenge_method" validate:"required"`
38
+
State string `form:"state" json:"state" query:"state" validate:"required"`
39
+
RedirectURI string `form:"redirect_uri" json:"redirect_uri" query:"redirect_uri" validate:"required"`
40
+
Scope string `form:"scope" json:"scope" query:"scope" validate:"required"`
41
+
LoginHint *string `form:"login_hint" query:"login_hint" json:"login_hint,omitempty"`
42
+
DpopJkt *string `form:"dpop_jkt" query:"dpop_jkt" json:"dpop_jkt,omitempty"`
43
+
ResponseMode *string `form:"response_mode" json:"response_mode,omitempty" query:"response_mode"`
43
44
}
44
45
45
46
func (opr *ParRequest) Scan(value any) error {
+36
-20
plc/client.go
+36
-20
plc/client.go
···
13
13
"net/url"
14
14
"strings"
15
15
16
-
"github.com/bluesky-social/indigo/atproto/crypto"
16
+
"github.com/bluesky-social/indigo/atproto/atcrypto"
17
17
"github.com/bluesky-social/indigo/util"
18
18
"github.com/haileyok/cocoon/identity"
19
19
)
···
22
22
h *http.Client
23
23
service string
24
24
pdsHostname string
25
-
rotationKey *crypto.PrivateKeyK256
25
+
rotationKey *atcrypto.PrivateKeyK256
26
26
}
27
27
28
28
type ClientArgs struct {
···
41
41
args.H = util.RobustHTTPClient()
42
42
}
43
43
44
-
rk, err := crypto.ParsePrivateBytesK256([]byte(args.RotationKey))
44
+
rk, err := atcrypto.ParsePrivateBytesK256([]byte(args.RotationKey))
45
45
if err != nil {
46
46
return nil, err
47
47
}
···
54
54
}, nil
55
55
}
56
56
57
-
func (c *Client) CreateDID(sigkey *crypto.PrivateKeyK256, recovery string, handle string) (string, *Operation, error) {
58
-
pubsigkey, err := sigkey.PublicKey()
57
+
func (c *Client) CreateDID(sigkey *atcrypto.PrivateKeyK256, recovery string, handle string) (string, *Operation, error) {
58
+
creds, err := c.CreateDidCredentials(sigkey, recovery, handle)
59
59
if err != nil {
60
60
return "", nil, err
61
61
}
62
62
63
-
pubrotkey, err := c.rotationKey.PublicKey()
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)
64
77
if err != nil {
65
78
return "", nil, err
66
79
}
67
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
+
68
95
// todo
69
96
rotationKeys := []string{pubrotkey.DIDKey()}
70
97
if recovery != "" {
···
77
104
}(recovery)
78
105
}
79
106
80
-
op := Operation{
81
-
Type: "plc_operation",
107
+
creds := DidCredentials{
82
108
VerificationMethods: map[string]string{
83
109
"atproto": pubsigkey.DIDKey(),
84
110
},
···
92
118
Endpoint: "https://" + c.pdsHostname,
93
119
},
94
120
},
95
-
Prev: nil,
96
121
}
97
122
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
123
+
return &creds, nil
108
124
}
109
125
110
-
func (c *Client) SignOp(sigkey *crypto.PrivateKeyK256, op *Operation) error {
126
+
func (c *Client) SignOp(sigkey *atcrypto.PrivateKeyK256, op *Operation) error {
111
127
b, err := op.MarshalCBOR()
112
128
if err != nil {
113
129
return err
+10
-2
plc/types.go
+10
-2
plc/types.go
···
3
3
import (
4
4
"encoding/json"
5
5
6
-
"github.com/bluesky-social/indigo/atproto/data"
6
+
"github.com/bluesky-social/indigo/atproto/atdata"
7
7
"github.com/haileyok/cocoon/identity"
8
8
cbg "github.com/whyrusleeping/cbor-gen"
9
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
+
}
10
18
11
19
type Operation struct {
12
20
Type string `json:"type"`
···
38
46
return nil, err
39
47
}
40
48
41
-
b, err = data.MarshalCBOR(m)
49
+
b, err = atdata.MarshalCBOR(m)
42
50
if err != nil {
43
51
return nil, err
44
52
}
+13
-5
recording_blockstore/recording_blockstore.go
+13
-5
recording_blockstore/recording_blockstore.go
···
2
2
3
3
import (
4
4
"context"
5
+
"fmt"
5
6
6
7
blockformat "github.com/ipfs/go-block-format"
7
8
"github.com/ipfs/go-cid"
···
12
13
base blockstore.Blockstore
13
14
14
15
inserts map[cid.Cid]blockformat.Block
16
+
reads map[cid.Cid]blockformat.Block
15
17
}
16
18
17
19
func New(base blockstore.Blockstore) *RecordingBlockstore {
18
20
return &RecordingBlockstore{
19
21
base: base,
20
22
inserts: make(map[cid.Cid]blockformat.Block),
23
+
reads: make(map[cid.Cid]blockformat.Block),
21
24
}
22
25
}
23
26
···
26
29
}
27
30
28
31
func (bs *RecordingBlockstore) Get(ctx context.Context, c cid.Cid) (blockformat.Block, error) {
29
-
return bs.base.Get(ctx, c)
32
+
b, err := bs.base.Get(ctx, c)
33
+
if err != nil {
34
+
return nil, err
35
+
}
36
+
bs.reads[c] = b
37
+
return b, nil
30
38
}
31
39
32
40
func (bs *RecordingBlockstore) GetSize(ctx context.Context, c cid.Cid) (int, error) {
···
58
66
}
59
67
60
68
func (bs *RecordingBlockstore) AllKeysChan(ctx context.Context) (<-chan cid.Cid, error) {
61
-
return bs.AllKeysChan(ctx)
69
+
return nil, fmt.Errorf("iteration not allowed on recording blockstore")
62
70
}
63
71
64
72
func (bs *RecordingBlockstore) HashOnRead(enabled bool) {
65
73
}
66
74
67
-
func (bs *RecordingBlockstore) GetLogMap() map[cid.Cid]blockformat.Block {
75
+
func (bs *RecordingBlockstore) GetWriteLog() map[cid.Cid]blockformat.Block {
68
76
return bs.inserts
69
77
}
70
78
71
-
func (bs *RecordingBlockstore) GetLogArray() []blockformat.Block {
79
+
func (bs *RecordingBlockstore) GetReadLog() []blockformat.Block {
72
80
var blocks []blockformat.Block
73
-
for _, b := range bs.inserts {
81
+
for _, b := range bs.reads {
74
82
blocks = append(blocks, b)
75
83
}
76
84
return blocks
+10
-8
server/common.go
+10
-8
server/common.go
···
1
1
package server
2
2
3
3
import (
4
+
"context"
5
+
4
6
"github.com/haileyok/cocoon/models"
5
7
)
6
8
7
-
func (s *Server) getActorByHandle(handle string) (*models.Actor, error) {
9
+
func (s *Server) getActorByHandle(ctx context.Context, handle string) (*models.Actor, error) {
8
10
var actor models.Actor
9
-
if err := s.db.First(&actor, models.Actor{Handle: handle}).Error; err != nil {
11
+
if err := s.db.First(ctx, &actor, models.Actor{Handle: handle}).Error; err != nil {
10
12
return nil, err
11
13
}
12
14
return &actor, nil
13
15
}
14
16
15
-
func (s *Server) getRepoByEmail(email string) (*models.Repo, error) {
17
+
func (s *Server) getRepoByEmail(ctx context.Context, email string) (*models.Repo, error) {
16
18
var repo models.Repo
17
-
if err := s.db.First(&repo, models.Repo{Email: email}).Error; err != nil {
19
+
if err := s.db.First(ctx, &repo, models.Repo{Email: email}).Error; err != nil {
18
20
return nil, err
19
21
}
20
22
return &repo, nil
21
23
}
22
24
23
-
func (s *Server) getRepoActorByEmail(email string) (*models.RepoActor, error) {
25
+
func (s *Server) getRepoActorByEmail(ctx context.Context, email string) (*models.RepoActor, error) {
24
26
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 {
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 {
26
28
return nil, err
27
29
}
28
30
return &repo, nil
29
31
}
30
32
31
-
func (s *Server) getRepoActorByDid(did string) (*models.RepoActor, error) {
33
+
func (s *Server) getRepoActorByDid(ctx context.Context, did string) (*models.RepoActor, error) {
32
34
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 {
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 {
34
36
return nil, err
35
37
}
36
38
return &repo, nil
+4
-2
server/handle_account.go
+4
-2
server/handle_account.go
···
12
12
13
13
func (s *Server) handleAccount(e echo.Context) error {
14
14
ctx := e.Request().Context()
15
+
logger := s.logger.With("name", "handleAuth")
16
+
15
17
repo, sess, err := s.getSessionRepoOrErr(e)
16
18
if err != nil {
17
19
return e.Redirect(303, "/account/signin")
···
20
22
oldestPossibleSession := time.Now().Add(constants.ConfidentialClientSessionLifetime)
21
23
22
24
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
+
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)
25
27
sess.AddFlash("Unable to fetch sessions. See server logs for more details.", "error")
26
28
sess.Save(e.Request(), e.Response())
27
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
5
"github.com/labstack/echo/v4"
6
6
)
7
7
8
-
type AccountRevokeRequest struct {
8
+
type AccountRevokeInput struct {
9
9
Token string `form:"token"`
10
10
}
11
11
12
12
func (s *Server) handleAccountRevoke(e echo.Context) error {
13
-
var req AccountRevokeRequest
13
+
ctx := e.Request().Context()
14
+
logger := s.logger.With("name", "handleAcocuntRevoke")
15
+
16
+
var req AccountRevokeInput
14
17
if err := e.Bind(&req); err != nil {
15
-
s.logger.Error("could not bind account revoke request", "error", err)
18
+
logger.Error("could not bind account revoke request", "error", err)
16
19
return helpers.ServerError(e, nil)
17
20
}
18
21
···
21
24
return e.Redirect(303, "/account/signin")
22
25
}
23
26
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)
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)
26
29
sess.AddFlash("Unable to revoke session. See server logs for more details.", "error")
27
30
sess.Save(e.Request(), e.Response())
28
31
return e.Redirect(303, "/account")
+68
-16
server/handle_account_signin.go
+68
-16
server/handle_account_signin.go
···
2
2
3
3
import (
4
4
"errors"
5
+
"fmt"
5
6
"strings"
7
+
"time"
6
8
7
9
"github.com/bluesky-social/indigo/atproto/syntax"
8
10
"github.com/gorilla/sessions"
···
14
16
"gorm.io/gorm"
15
17
)
16
18
17
-
type OauthSigninRequest struct {
18
-
Username string `form:"username"`
19
-
Password string `form:"password"`
20
-
QueryParams string `form:"query_params"`
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"`
21
24
}
22
25
23
26
func (s *Server) getSessionRepoOrErr(e echo.Context) (*models.RepoActor, *sessions.Session, error) {
27
+
ctx := e.Request().Context()
28
+
24
29
sess, err := session.Get("session", e)
25
30
if err != nil {
26
31
return nil, nil, err
···
31
36
return nil, sess, errors.New("did was not set in session")
32
37
}
33
38
34
-
repo, err := s.getRepoActorByDid(did)
39
+
repo, err := s.getRepoActorByDid(ctx, did)
35
40
if err != nil {
36
41
return nil, sess, err
37
42
}
···
42
47
func getFlashesFromSession(e echo.Context, sess *sessions.Session) map[string]any {
43
48
defer sess.Save(e.Request(), e.Response())
44
49
return map[string]any{
45
-
"errors": sess.Flashes("error"),
46
-
"successes": sess.Flashes("success"),
50
+
"errors": sess.Flashes("error"),
51
+
"successes": sess.Flashes("success"),
52
+
"tokenrequired": sess.Flashes("tokenrequired"),
47
53
}
48
54
}
49
55
···
60
66
}
61
67
62
68
func (s *Server) handleAccountSigninPost(e echo.Context) error {
63
-
var req OauthSigninRequest
69
+
ctx := e.Request().Context()
70
+
logger := s.logger.With("name", "handleAccountSigninPost")
71
+
72
+
var req OauthSigninInput
64
73
if err := e.Bind(&req); err != nil {
65
-
s.logger.Error("error binding sign in req", "error", err)
74
+
logger.Error("error binding sign in req", "error", err)
66
75
return helpers.ServerError(e, nil)
67
76
}
68
77
···
76
85
idtype = "handle"
77
86
} else {
78
87
idtype = "email"
88
+
}
89
+
90
+
queryParams := ""
91
+
if req.QueryParams != "" {
92
+
queryParams = fmt.Sprintf("?%s", req.QueryParams)
79
93
}
80
94
81
95
// TODO: we should make this a helper since we do it for the base create_session as well
···
83
97
var err error
84
98
switch idtype {
85
99
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
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
87
101
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
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
89
103
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
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
91
105
}
92
106
if err != nil {
93
107
if err == gorm.ErrRecordNotFound {
···
96
110
sess.AddFlash("Something went wrong!", "error")
97
111
}
98
112
sess.Save(e.Request(), e.Response())
99
-
return e.Redirect(303, "/account/signin")
113
+
return e.Redirect(303, "/account/signin"+queryParams)
100
114
}
101
115
102
116
if err := bcrypt.CompareHashAndPassword([]byte(repo.Password), []byte(req.Password)); err != nil {
···
106
120
sess.AddFlash("Something went wrong!", "error")
107
121
}
108
122
sess.Save(e.Request(), e.Response())
109
-
return e.Redirect(303, "/account/signin")
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
+
}
110
162
}
111
163
112
164
sess.Options = &sessions.Options{
···
122
174
return err
123
175
}
124
176
125
-
if req.QueryParams != "" {
126
-
return e.Redirect(303, "/oauth/authorize?"+req.QueryParams)
177
+
if queryParams != "" {
178
+
return e.Redirect(303, "/oauth/authorize"+queryParams)
127
179
} else {
128
180
return e.Redirect(303, "/account")
129
181
}
+1
-1
server/handle_actor_get_preferences.go
+1
-1
server/handle_actor_get_preferences.go
+3
-1
server/handle_actor_put_preferences.go
+3
-1
server/handle_actor_put_preferences.go
···
10
10
// This is kinda lame. Not great to implement app.bsky in the pds, but alas
11
11
12
12
func (s *Server) handleActorPutPreferences(e echo.Context) error {
13
+
ctx := e.Request().Context()
14
+
13
15
repo := e.Get("repo").(*models.RepoActor)
14
16
15
17
var prefs map[string]any
···
22
24
return err
23
25
}
24
26
25
-
if err := s.db.Exec("UPDATE repos SET preferences = ? WHERE did = ?", nil, b, repo.Repo.Did).Error; err != nil {
27
+
if err := s.db.Exec(ctx, "UPDATE repos SET preferences = ? WHERE did = ?", nil, b, repo.Repo.Did).Error; err != nil {
26
28
return err
27
29
}
28
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
+
}
+10
-17
server/handle_identity_update_handle.go
+10
-17
server/handle_identity_update_handle.go
···
7
7
8
8
"github.com/Azure/go-autorest/autorest/to"
9
9
"github.com/bluesky-social/indigo/api/atproto"
10
-
"github.com/bluesky-social/indigo/atproto/crypto"
10
+
"github.com/bluesky-social/indigo/atproto/atcrypto"
11
11
"github.com/bluesky-social/indigo/events"
12
12
"github.com/bluesky-social/indigo/util"
13
13
"github.com/haileyok/cocoon/identity"
···
22
22
}
23
23
24
24
func (s *Server) handleIdentityUpdateHandle(e echo.Context) error {
25
+
logger := s.logger.With("name", "handleIdentityUpdateHandle")
26
+
25
27
repo := e.Get("repo").(*models.RepoActor)
26
28
27
29
var req ComAtprotoIdentityUpdateHandleRequest
28
30
if err := e.Bind(&req); err != nil {
29
-
s.logger.Error("error binding", "error", err)
31
+
logger.Error("error binding", "error", err)
30
32
return helpers.ServerError(e, nil)
31
33
}
32
34
···
41
43
if strings.HasPrefix(repo.Repo.Did, "did:plc:") {
42
44
log, err := identity.FetchDidAuditLog(ctx, nil, repo.Repo.Did)
43
45
if err != nil {
44
-
s.logger.Error("error fetching doc", "error", err)
46
+
logger.Error("error fetching doc", "error", err)
45
47
return helpers.ServerError(e, nil)
46
48
}
47
49
···
66
68
Prev: &latest.Cid,
67
69
}
68
70
69
-
k, err := crypto.ParsePrivateBytesK256(repo.SigningKey)
71
+
k, err := atcrypto.ParsePrivateBytesK256(repo.SigningKey)
70
72
if err != nil {
71
-
s.logger.Error("error parsing signing key", "error", err)
73
+
logger.Error("error parsing signing key", "error", err)
72
74
return helpers.ServerError(e, nil)
73
75
}
74
76
···
82
84
}
83
85
84
86
if err := s.passport.BustDoc(context.TODO(), repo.Repo.Did); err != nil {
85
-
s.logger.Warn("error busting did doc", "error", err)
87
+
logger.Warn("error busting did doc", "error", err)
86
88
}
87
89
88
90
s.evtman.AddEvent(context.TODO(), &events.XRPCStreamEvent{
89
-
RepoHandle: &atproto.SyncSubscribeRepos_Handle{
90
-
Did: repo.Repo.Did,
91
-
Handle: req.Handle,
92
-
Seq: time.Now().UnixMicro(), // TODO: no
93
-
Time: time.Now().Format(util.ISO8601),
94
-
},
95
-
})
96
-
97
-
s.evtman.AddEvent(context.TODO(), &events.XRPCStreamEvent{
98
91
RepoIdentity: &atproto.SyncSubscribeRepos_Identity{
99
92
Did: repo.Repo.Did,
100
93
Handle: to.StringPtr(req.Handle),
···
103
96
},
104
97
})
105
98
106
-
if err := s.db.Exec("UPDATE actors SET handle = ? WHERE did = ?", nil, req.Handle, repo.Repo.Did).Error; err != nil {
107
-
s.logger.Error("error updating handle in db", "error", err)
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)
108
101
return helpers.ServerError(e, nil)
109
102
}
110
103
+15
-12
server/handle_import_repo.go
+15
-12
server/handle_import_repo.go
···
18
18
)
19
19
20
20
func (s *Server) handleRepoImportRepo(e echo.Context) error {
21
+
ctx := e.Request().Context()
22
+
logger := s.logger.With("name", "handleImportRepo")
23
+
21
24
urepo := e.Get("repo").(*models.RepoActor)
22
25
23
26
b, err := io.ReadAll(e.Request().Body)
24
27
if err != nil {
25
-
s.logger.Error("could not read bytes in import request", "error", err)
28
+
logger.Error("could not read bytes in import request", "error", err)
26
29
return helpers.ServerError(e, nil)
27
30
}
28
31
···
30
33
31
34
cs, err := car.NewCarReader(bytes.NewReader(b))
32
35
if err != nil {
33
-
s.logger.Error("could not read car in import request", "error", err)
36
+
logger.Error("could not read car in import request", "error", err)
34
37
return helpers.ServerError(e, nil)
35
38
}
36
39
37
40
orderedBlocks := []blocks.Block{}
38
41
currBlock, err := cs.Next()
39
42
if err != nil {
40
-
s.logger.Error("could not get first block from car", "error", err)
43
+
logger.Error("could not get first block from car", "error", err)
41
44
return helpers.ServerError(e, nil)
42
45
}
43
46
currBlockCt := 1
44
47
45
48
for currBlock != nil {
46
-
s.logger.Info("someone is importing their repo", "block", currBlockCt)
49
+
logger.Info("someone is importing their repo", "block", currBlockCt)
47
50
orderedBlocks = append(orderedBlocks, currBlock)
48
51
next, _ := cs.Next()
49
52
currBlock = next
···
53
56
slices.Reverse(orderedBlocks)
54
57
55
58
if err := bs.PutMany(context.TODO(), orderedBlocks); err != nil {
56
-
s.logger.Error("could not insert blocks", "error", err)
59
+
logger.Error("could not insert blocks", "error", err)
57
60
return helpers.ServerError(e, nil)
58
61
}
59
62
60
63
r, err := repo.OpenRepo(context.TODO(), bs, cs.Header.Roots[0])
61
64
if err != nil {
62
-
s.logger.Error("could not open repo", "error", err)
65
+
logger.Error("could not open repo", "error", err)
63
66
return helpers.ServerError(e, nil)
64
67
}
65
68
66
-
tx := s.db.BeginDangerously()
69
+
tx := s.db.BeginDangerously(ctx)
67
70
68
71
clock := syntax.NewTIDClock(0)
69
72
···
74
77
cidStr := cid.String()
75
78
b, err := bs.Get(context.TODO(), cid)
76
79
if err != nil {
77
-
s.logger.Error("record bytes don't exist in blockstore", "error", err)
80
+
logger.Error("record bytes don't exist in blockstore", "error", err)
78
81
return helpers.ServerError(e, nil)
79
82
}
80
83
···
87
90
Value: b.RawData(),
88
91
}
89
92
90
-
if err := tx.Create(rec).Error; err != nil {
93
+
if err := tx.Save(rec).Error; err != nil {
91
94
return err
92
95
}
93
96
94
97
return nil
95
98
}); err != nil {
96
99
tx.Rollback()
97
-
s.logger.Error("record bytes don't exist in blockstore", "error", err)
100
+
logger.Error("record bytes don't exist in blockstore", "error", err)
98
101
return helpers.ServerError(e, nil)
99
102
}
100
103
···
102
105
103
106
root, rev, err := r.Commit(context.TODO(), urepo.SignFor)
104
107
if err != nil {
105
-
s.logger.Error("error committing", "error", err)
108
+
logger.Error("error committing", "error", err)
106
109
return helpers.ServerError(e, nil)
107
110
}
108
111
109
112
if err := s.UpdateRepo(context.TODO(), urepo.Repo.Did, root, rev); err != nil {
110
-
s.logger.Error("error updating repo after commit", "error", err)
113
+
logger.Error("error updating repo after commit", "error", err)
111
114
return helpers.ServerError(e, nil)
112
115
}
113
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
+
}
+25
-9
server/handle_oauth_par.go
+25
-9
server/handle_oauth_par.go
···
1
1
package server
2
2
3
3
import (
4
+
"errors"
4
5
"time"
5
6
6
7
"github.com/Azure/go-autorest/autorest/to"
7
8
"github.com/haileyok/cocoon/internal/helpers"
8
9
"github.com/haileyok/cocoon/oauth"
9
10
"github.com/haileyok/cocoon/oauth/constants"
11
+
"github.com/haileyok/cocoon/oauth/dpop"
10
12
"github.com/haileyok/cocoon/oauth/provider"
11
13
"github.com/labstack/echo/v4"
12
14
)
···
17
19
}
18
20
19
21
func (s *Server) handleOauthPar(e echo.Context) error {
22
+
ctx := e.Request().Context()
23
+
logger := s.logger.With("name", "handleOauthPar")
24
+
20
25
var parRequest provider.ParRequest
21
26
if err := e.Bind(&parRequest); err != nil {
22
-
s.logger.Error("error binding for par request", "error", err)
27
+
logger.Error("error binding for par request", "error", err)
23
28
return helpers.ServerError(e, nil)
24
29
}
25
30
26
31
if err := e.Validate(parRequest); err != nil {
27
-
s.logger.Error("missing parameters for par request", "error", err)
32
+
logger.Error("missing parameters for par request", "error", err)
28
33
return helpers.InputError(e, nil)
29
34
}
30
35
31
36
// TODO: this seems wrong. should be a way to get the entire request url i believe, but this will work for now
32
37
dpopProof, err := s.oauthProvider.DpopManager.CheckProof(e.Request().Method, "https://"+s.config.Hostname+e.Request().URL.String(), e.Request().Header, nil)
33
38
if err != nil {
34
-
s.logger.Error("error getting dpop proof", "error", err)
35
-
return helpers.InputError(e, to.StringPtr(err.Error()))
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
+
logger.Error("nonce error: use_dpop_nonce", "headers", e.Request().Header)
46
+
return e.JSON(400, map[string]string{
47
+
"error": "use_dpop_nonce",
48
+
})
49
+
}
50
+
logger.Error("error getting dpop proof", "error", err)
51
+
return helpers.InputError(e, nil)
36
52
}
37
53
38
54
client, clientAuth, err := s.oauthProvider.AuthenticateClient(e.Request().Context(), parRequest.AuthenticateClientRequestBase, dpopProof, &provider.AuthenticateClientOptions{
···
41
57
AllowMissingDpopProof: true,
42
58
})
43
59
if err != nil {
44
-
s.logger.Error("error authenticating client", "error", err)
60
+
logger.Error("error authenticating client", "client_id", parRequest.ClientID, "error", err)
45
61
return helpers.InputError(e, to.StringPtr(err.Error()))
46
62
}
47
63
···
52
68
} else {
53
69
if !client.Metadata.DpopBoundAccessTokens {
54
70
msg := "dpop bound access tokens are not enabled for this client"
55
-
s.logger.Error(msg)
71
+
logger.Error(msg)
56
72
return helpers.InputError(e, &msg)
57
73
}
58
74
59
75
if dpopProof.JKT != *parRequest.DpopJkt {
60
76
msg := "supplied dpop jkt does not match header dpop jkt"
61
-
s.logger.Error(msg)
77
+
logger.Error(msg)
62
78
return helpers.InputError(e, &msg)
63
79
}
64
80
}
···
74
90
ExpiresAt: eat,
75
91
}
76
92
77
-
if err := s.db.Create(authRequest, nil).Error; err != nil {
78
-
s.logger.Error("error creating auth request in db", "error", err)
93
+
if err := s.db.Create(ctx, authRequest, nil).Error; err != nil {
94
+
logger.Error("error creating auth request in db", "error", err)
79
95
return helpers.ServerError(e, nil)
80
96
}
81
97
+29
-14
server/handle_oauth_token.go
+29
-14
server/handle_oauth_token.go
···
4
4
"bytes"
5
5
"crypto/sha256"
6
6
"encoding/base64"
7
+
"errors"
7
8
"fmt"
8
9
"slices"
9
10
"time"
···
13
14
"github.com/haileyok/cocoon/internal/helpers"
14
15
"github.com/haileyok/cocoon/oauth"
15
16
"github.com/haileyok/cocoon/oauth/constants"
17
+
"github.com/haileyok/cocoon/oauth/dpop"
16
18
"github.com/haileyok/cocoon/oauth/provider"
17
19
"github.com/labstack/echo/v4"
18
20
)
···
36
38
}
37
39
38
40
func (s *Server) handleOauthToken(e echo.Context) error {
41
+
ctx := e.Request().Context()
42
+
logger := s.logger.With("name", "handleOauthToken")
43
+
39
44
var req OauthTokenRequest
40
45
if err := e.Bind(&req); err != nil {
41
-
s.logger.Error("error binding token request", "error", err)
46
+
logger.Error("error binding token request", "error", err)
42
47
return helpers.ServerError(e, nil)
43
48
}
44
49
45
50
proof, err := s.oauthProvider.DpopManager.CheckProof(e.Request().Method, e.Request().URL.String(), e.Request().Header, nil)
46
51
if err != nil {
47
-
s.logger.Error("error getting dpop proof", "error", err)
48
-
return helpers.InputError(e, to.StringPtr(err.Error()))
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)
49
64
}
50
65
51
66
client, clientAuth, err := s.oauthProvider.AuthenticateClient(e.Request().Context(), req.AuthenticateClientRequestBase, proof, &provider.AuthenticateClientOptions{
52
67
AllowMissingDpopProof: true,
53
68
})
54
69
if err != nil {
55
-
s.logger.Error("error authenticating client", "error", err)
70
+
logger.Error("error authenticating client", "client_id", req.ClientID, "error", err)
56
71
return helpers.InputError(e, to.StringPtr(err.Error()))
57
72
}
58
73
···
72
87
73
88
var authReq provider.OauthAuthorizationRequest
74
89
// get the lil guy and delete him
75
-
if err := s.db.Raw("DELETE FROM oauth_authorization_requests WHERE code = ? RETURNING *", nil, *req.Code).Scan(&authReq).Error; err != nil {
76
-
s.logger.Error("error finding authorization request", "error", err)
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)
77
92
return helpers.ServerError(e, nil)
78
93
}
79
94
···
98
113
case "S256":
99
114
inputChal, err := base64.RawURLEncoding.DecodeString(*authReq.Parameters.CodeChallenge)
100
115
if err != nil {
101
-
s.logger.Error("error decoding code challenge", "error", err)
116
+
logger.Error("error decoding code challenge", "error", err)
102
117
return helpers.ServerError(e, nil)
103
118
}
104
119
···
116
131
return helpers.InputError(e, to.StringPtr("code_challenge parameter wasn't provided"))
117
132
}
118
133
119
-
repo, err := s.getRepoActorByDid(*authReq.Sub)
134
+
repo, err := s.getRepoActorByDid(ctx, *authReq.Sub)
120
135
if err != nil {
121
136
helpers.InputError(e, to.StringPtr("unable to find actor"))
122
137
}
···
147
162
return err
148
163
}
149
164
150
-
if err := s.db.Create(&provider.OauthToken{
165
+
if err := s.db.Create(ctx, &provider.OauthToken{
151
166
ClientId: authReq.ClientId,
152
167
ClientAuth: *clientAuth,
153
168
Parameters: authReq.Parameters,
···
159
174
RefreshToken: refreshToken,
160
175
Ip: authReq.Ip,
161
176
}, nil).Error; err != nil {
162
-
s.logger.Error("error creating token in db", "error", err)
177
+
logger.Error("error creating token in db", "error", err)
163
178
return helpers.ServerError(e, nil)
164
179
}
165
180
···
187
202
}
188
203
189
204
var oauthToken provider.OauthToken
190
-
if err := s.db.Raw("SELECT * FROM oauth_tokens WHERE refresh_token = ?", nil, req.RefreshToken).Scan(&oauthToken).Error; err != nil {
191
-
s.logger.Error("error finding oauth token by refresh token", "error", err, "refresh_token", req.RefreshToken)
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)
192
207
return helpers.ServerError(e, nil)
193
208
}
194
209
···
245
260
return err
246
261
}
247
262
248
-
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 {
249
-
s.logger.Error("error updating token", "error", err)
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)
250
265
return helpers.ServerError(e, nil)
251
266
}
252
267
+23
-10
server/handle_proxy.go
+23
-10
server/handle_proxy.go
···
19
19
20
20
func (s *Server) getAtprotoProxyEndpointFromRequest(e echo.Context) (string, string, error) {
21
21
svc := e.Request().Header.Get("atproto-proxy")
22
-
if svc == "" {
23
-
svc = s.config.DefaultAtprotoProxy
22
+
if svc == "" && s.config.FallbackProxy != "" {
23
+
svc = s.config.FallbackProxy
24
24
}
25
25
26
26
svcPts := strings.Split(svc, "#")
···
47
47
}
48
48
49
49
func (s *Server) handleProxy(e echo.Context) error {
50
-
lgr := s.logger.With("handler", "handleProxy")
50
+
logger := s.logger.With("handler", "handleProxy")
51
51
52
52
repo, isAuthed := e.Get("repo").(*models.RepoActor)
53
53
···
58
58
59
59
endpoint, svcDid, err := s.getAtprotoProxyEndpointFromRequest(e)
60
60
if err != nil {
61
-
lgr.Error("could not get atproto proxy", "error", err)
61
+
logger.Error("could not get atproto proxy", "error", err)
62
62
return helpers.ServerError(e, nil)
63
63
}
64
64
···
90
90
}
91
91
hj, err := json.Marshal(header)
92
92
if err != nil {
93
-
lgr.Error("error marshaling header", "error", err)
93
+
logger.Error("error marshaling header", "error", err)
94
94
return helpers.ServerError(e, nil)
95
95
}
96
96
97
97
encheader := strings.TrimRight(base64.RawURLEncoding.EncodeToString(hj), "=")
98
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
+
99
112
payload := map[string]any{
100
113
"iss": repo.Repo.Did,
101
-
"aud": svcDid,
102
-
"lxm": pts[2],
114
+
"aud": aud,
115
+
"lxm": lxm,
103
116
"jti": uuid.NewString(),
104
117
"exp": time.Now().Add(1 * time.Minute).UTC().Unix(),
105
118
}
106
119
pj, err := json.Marshal(payload)
107
120
if err != nil {
108
-
lgr.Error("error marashaling payload", "error", err)
121
+
logger.Error("error marashaling payload", "error", err)
109
122
return helpers.ServerError(e, nil)
110
123
}
111
124
···
116
129
117
130
sk, err := secp256k1secec.NewPrivateKey(repo.SigningKey)
118
131
if err != nil {
119
-
lgr.Error("can't load private key", "error", err)
132
+
logger.Error("can't load private key", "error", err)
120
133
return err
121
134
}
122
135
123
136
R, S, _, err := sk.SignRaw(rand.Reader, hash[:])
124
137
if err != nil {
125
-
lgr.Error("error signing", "error", err)
138
+
logger.Error("error signing", "error", err)
126
139
}
127
140
128
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
6
"github.com/labstack/echo/v4"
7
7
)
8
8
9
-
type ComAtprotoRepoApplyWritesRequest struct {
9
+
type ComAtprotoRepoApplyWritesInput struct {
10
10
Repo string `json:"repo" validate:"required,atproto-did"`
11
11
Validate *bool `json:"bool,omitempty"`
12
12
Writes []ComAtprotoRepoApplyWritesItem `json:"writes"`
···
20
20
Value *MarshalableMap `json:"value,omitempty"`
21
21
}
22
22
23
-
type ComAtprotoRepoApplyWritesResponse struct {
23
+
type ComAtprotoRepoApplyWritesOutput struct {
24
24
Commit RepoCommit `json:"commit"`
25
25
Results []ApplyWriteResult `json:"results"`
26
26
}
27
27
28
28
func (s *Server) handleApplyWrites(e echo.Context) error {
29
-
repo := e.Get("repo").(*models.RepoActor)
29
+
ctx := e.Request().Context()
30
+
logger := s.logger.With("name", "handleRepoApplyWrites")
30
31
31
-
var req ComAtprotoRepoApplyWritesRequest
32
+
var req ComAtprotoRepoApplyWritesInput
32
33
if err := e.Bind(&req); err != nil {
33
-
s.logger.Error("error binding", "error", err)
34
+
logger.Error("error binding", "error", err)
34
35
return helpers.ServerError(e, nil)
35
36
}
36
37
37
38
if err := e.Validate(req); err != nil {
38
-
s.logger.Error("error validating", "error", err)
39
+
logger.Error("error validating", "error", err)
39
40
return helpers.InputError(e, nil)
40
41
}
41
42
43
+
repo := e.Get("repo").(*models.RepoActor)
44
+
42
45
if repo.Repo.Did != req.Repo {
43
-
s.logger.Warn("mismatched repo/auth")
46
+
logger.Warn("mismatched repo/auth")
44
47
return helpers.InputError(e, nil)
45
48
}
46
49
47
-
ops := []Op{}
50
+
ops := make([]Op, 0, len(req.Writes))
48
51
for _, item := range req.Writes {
49
52
ops = append(ops, Op{
50
53
Type: OpType(item.Type),
···
54
57
})
55
58
}
56
59
57
-
results, err := s.repoman.applyWrites(repo.Repo, ops, req.SwapCommit)
60
+
results, err := s.repoman.applyWrites(ctx, repo.Repo, ops, req.SwapCommit)
58
61
if err != nil {
59
-
s.logger.Error("error applying writes", "error", err)
62
+
logger.Error("error applying writes", "error", err)
60
63
return helpers.ServerError(e, nil)
61
64
}
62
65
···
66
69
results[i].Commit = nil
67
70
}
68
71
69
-
return e.JSON(200, ComAtprotoRepoApplyWritesResponse{
72
+
return e.JSON(200, ComAtprotoRepoApplyWritesOutput{
70
73
Commit: commit,
71
74
Results: results,
72
75
})
+10
-7
server/handle_repo_create_record.go
+10
-7
server/handle_repo_create_record.go
···
6
6
"github.com/labstack/echo/v4"
7
7
)
8
8
9
-
type ComAtprotoRepoCreateRecordRequest struct {
9
+
type ComAtprotoRepoCreateRecordInput struct {
10
10
Repo string `json:"repo" validate:"required,atproto-did"`
11
11
Collection string `json:"collection" validate:"required,atproto-nsid"`
12
12
Rkey *string `json:"rkey,omitempty"`
···
17
17
}
18
18
19
19
func (s *Server) handleCreateRecord(e echo.Context) error {
20
+
ctx := e.Request().Context()
21
+
logger := s.logger.With("name", "handleCreateRecord")
22
+
20
23
repo := e.Get("repo").(*models.RepoActor)
21
24
22
-
var req ComAtprotoRepoCreateRecordRequest
25
+
var req ComAtprotoRepoCreateRecordInput
23
26
if err := e.Bind(&req); err != nil {
24
-
s.logger.Error("error binding", "error", err)
27
+
logger.Error("error binding", "error", err)
25
28
return helpers.ServerError(e, nil)
26
29
}
27
30
28
31
if err := e.Validate(req); err != nil {
29
-
s.logger.Error("error validating", "error", err)
32
+
logger.Error("error validating", "error", err)
30
33
return helpers.InputError(e, nil)
31
34
}
32
35
33
36
if repo.Repo.Did != req.Repo {
34
-
s.logger.Warn("mismatched repo/auth")
37
+
logger.Warn("mismatched repo/auth")
35
38
return helpers.InputError(e, nil)
36
39
}
37
40
···
40
43
optype = OpTypeUpdate
41
44
}
42
45
43
-
results, err := s.repoman.applyWrites(repo.Repo, []Op{
46
+
results, err := s.repoman.applyWrites(ctx, repo.Repo, []Op{
44
47
{
45
48
Type: optype,
46
49
Collection: req.Collection,
···
51
54
},
52
55
}, req.SwapCommit)
53
56
if err != nil {
54
-
s.logger.Error("error applying writes", "error", err)
57
+
logger.Error("error applying writes", "error", err)
55
58
return helpers.ServerError(e, nil)
56
59
}
57
60
+10
-7
server/handle_repo_delete_record.go
+10
-7
server/handle_repo_delete_record.go
···
6
6
"github.com/labstack/echo/v4"
7
7
)
8
8
9
-
type ComAtprotoRepoDeleteRecordRequest struct {
9
+
type ComAtprotoRepoDeleteRecordInput struct {
10
10
Repo string `json:"repo" validate:"required,atproto-did"`
11
11
Collection string `json:"collection" validate:"required,atproto-nsid"`
12
12
Rkey string `json:"rkey" validate:"required,atproto-rkey"`
···
15
15
}
16
16
17
17
func (s *Server) handleDeleteRecord(e echo.Context) error {
18
+
ctx := e.Request().Context()
19
+
logger := s.logger.With("name", "handleDeleteRecord")
20
+
18
21
repo := e.Get("repo").(*models.RepoActor)
19
22
20
-
var req ComAtprotoRepoDeleteRecordRequest
23
+
var req ComAtprotoRepoDeleteRecordInput
21
24
if err := e.Bind(&req); err != nil {
22
-
s.logger.Error("error binding", "error", err)
25
+
logger.Error("error binding", "error", err)
23
26
return helpers.ServerError(e, nil)
24
27
}
25
28
26
29
if err := e.Validate(req); err != nil {
27
-
s.logger.Error("error validating", "error", err)
30
+
logger.Error("error validating", "error", err)
28
31
return helpers.InputError(e, nil)
29
32
}
30
33
31
34
if repo.Repo.Did != req.Repo {
32
-
s.logger.Warn("mismatched repo/auth")
35
+
logger.Warn("mismatched repo/auth")
33
36
return helpers.InputError(e, nil)
34
37
}
35
38
36
-
results, err := s.repoman.applyWrites(repo.Repo, []Op{
39
+
results, err := s.repoman.applyWrites(ctx, repo.Repo, []Op{
37
40
{
38
41
Type: OpTypeDelete,
39
42
Collection: req.Collection,
···
42
45
},
43
46
}, req.SwapCommit)
44
47
if err != nil {
45
-
s.logger.Error("error applying writes", "error", err)
48
+
logger.Error("error applying writes", "error", err)
46
49
return helpers.ServerError(e, nil)
47
50
}
48
51
+8
-5
server/handle_repo_describe_repo.go
+8
-5
server/handle_repo_describe_repo.go
···
20
20
}
21
21
22
22
func (s *Server) handleDescribeRepo(e echo.Context) error {
23
+
ctx := e.Request().Context()
24
+
logger := s.logger.With("name", "handleDescribeRepo")
25
+
23
26
did := e.QueryParam("repo")
24
-
repo, err := s.getRepoActorByDid(did)
27
+
repo, err := s.getRepoActorByDid(ctx, did)
25
28
if err != nil {
26
29
if err == gorm.ErrRecordNotFound {
27
30
return helpers.InputError(e, to.StringPtr("RepoNotFound"))
28
31
}
29
32
30
-
s.logger.Error("error looking up repo", "error", err)
33
+
logger.Error("error looking up repo", "error", err)
31
34
return helpers.ServerError(e, nil)
32
35
}
33
36
···
35
38
36
39
diddoc, err := s.passport.FetchDoc(e.Request().Context(), repo.Repo.Did)
37
40
if err != nil {
38
-
s.logger.Error("error fetching diddoc", "error", err)
41
+
logger.Error("error fetching diddoc", "error", err)
39
42
return helpers.ServerError(e, nil)
40
43
}
41
44
···
64
67
}
65
68
66
69
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)
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)
69
72
return helpers.ServerError(e, nil)
70
73
}
71
74
+5
-3
server/handle_repo_get_record.go
+5
-3
server/handle_repo_get_record.go
···
1
1
package server
2
2
3
3
import (
4
-
"github.com/bluesky-social/indigo/atproto/data"
4
+
"github.com/bluesky-social/indigo/atproto/atdata"
5
5
"github.com/bluesky-social/indigo/atproto/syntax"
6
6
"github.com/haileyok/cocoon/models"
7
7
"github.com/labstack/echo/v4"
···
14
14
}
15
15
16
16
func (s *Server) handleRepoGetRecord(e echo.Context) error {
17
+
ctx := e.Request().Context()
18
+
17
19
repo := e.QueryParam("repo")
18
20
collection := e.QueryParam("collection")
19
21
rkey := e.QueryParam("rkey")
···
32
34
}
33
35
34
36
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 {
37
+
if err := s.db.Raw(ctx, "SELECT * FROM records WHERE did = ? AND nsid = ? AND rkey = ?"+cidquery, nil, params...).Scan(&record).Error; err != nil {
36
38
// TODO: handle error nicely
37
39
return err
38
40
}
39
41
40
-
val, err := data.UnmarshalCBOR(record.Value)
42
+
val, err := atdata.UnmarshalCBOR(record.Value)
41
43
if err != nil {
42
44
return s.handleProxy(e) // TODO: this should be getting handled like...if we don't find it in the db. why doesn't it throw error up there?
43
45
}
+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
+
}
+9
-6
server/handle_repo_list_records.go
+9
-6
server/handle_repo_list_records.go
···
4
4
"strconv"
5
5
6
6
"github.com/Azure/go-autorest/autorest/to"
7
-
"github.com/bluesky-social/indigo/atproto/data"
7
+
"github.com/bluesky-social/indigo/atproto/atdata"
8
8
"github.com/bluesky-social/indigo/atproto/syntax"
9
9
"github.com/haileyok/cocoon/internal/helpers"
10
10
"github.com/haileyok/cocoon/models"
···
46
46
}
47
47
48
48
func (s *Server) handleListRecords(e echo.Context) error {
49
+
ctx := e.Request().Context()
50
+
logger := s.logger.With("name", "handleListRecords")
51
+
49
52
var req ComAtprotoRepoListRecordsRequest
50
53
if err := e.Bind(&req); err != nil {
51
-
s.logger.Error("could not bind list records request", "error", err)
54
+
logger.Error("could not bind list records request", "error", err)
52
55
return helpers.ServerError(e, nil)
53
56
}
54
57
···
78
81
79
82
did := req.Repo
80
83
if _, err := syntax.ParseDID(did); err != nil {
81
-
actor, err := s.getActorByHandle(req.Repo)
84
+
actor, err := s.getActorByHandle(ctx, req.Repo)
82
85
if err != nil {
83
86
return helpers.InputError(e, to.StringPtr("RepoNotFound"))
84
87
}
···
93
96
params = append(params, limit)
94
97
95
98
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)
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)
98
101
return helpers.ServerError(e, nil)
99
102
}
100
103
101
104
items := []ComAtprotoRepoListRecordsRecordItem{}
102
105
for _, r := range records {
103
-
val, err := data.UnmarshalCBOR(r.Value)
106
+
val, err := atdata.UnmarshalCBOR(r.Value)
104
107
if err != nil {
105
108
return err
106
109
}
+5
-3
server/handle_repo_list_repos.go
+5
-3
server/handle_repo_list_repos.go
···
21
21
22
22
// TODO: paginate this bitch
23
23
func (s *Server) handleListRepos(e echo.Context) error {
24
+
ctx := e.Request().Context()
25
+
24
26
var repos []models.Repo
25
-
if err := s.db.Raw("SELECT * FROM repos ORDER BY created_at DESC LIMIT 500", nil).Scan(&repos).Error; err != nil {
27
+
if err := s.db.Raw(ctx, "SELECT * FROM repos ORDER BY created_at DESC LIMIT 500", nil).Scan(&repos).Error; err != nil {
26
28
return err
27
29
}
28
30
···
37
39
Did: r.Did,
38
40
Head: c.String(),
39
41
Rev: r.Rev,
40
-
Active: true,
41
-
Status: nil,
42
+
Active: r.Active(),
43
+
Status: r.Status(),
42
44
})
43
45
}
44
46
+10
-7
server/handle_repo_put_record.go
+10
-7
server/handle_repo_put_record.go
···
6
6
"github.com/labstack/echo/v4"
7
7
)
8
8
9
-
type ComAtprotoRepoPutRecordRequest struct {
9
+
type ComAtprotoRepoPutRecordInput struct {
10
10
Repo string `json:"repo" validate:"required,atproto-did"`
11
11
Collection string `json:"collection" validate:"required,atproto-nsid"`
12
12
Rkey string `json:"rkey" validate:"required,atproto-rkey"`
···
17
17
}
18
18
19
19
func (s *Server) handlePutRecord(e echo.Context) error {
20
+
ctx := e.Request().Context()
21
+
logger := s.logger.With("name", "handlePutRecord")
22
+
20
23
repo := e.Get("repo").(*models.RepoActor)
21
24
22
-
var req ComAtprotoRepoPutRecordRequest
25
+
var req ComAtprotoRepoPutRecordInput
23
26
if err := e.Bind(&req); err != nil {
24
-
s.logger.Error("error binding", "error", err)
27
+
logger.Error("error binding", "error", err)
25
28
return helpers.ServerError(e, nil)
26
29
}
27
30
28
31
if err := e.Validate(req); err != nil {
29
-
s.logger.Error("error validating", "error", err)
32
+
logger.Error("error validating", "error", err)
30
33
return helpers.InputError(e, nil)
31
34
}
32
35
33
36
if repo.Repo.Did != req.Repo {
34
-
s.logger.Warn("mismatched repo/auth")
37
+
logger.Warn("mismatched repo/auth")
35
38
return helpers.InputError(e, nil)
36
39
}
37
40
···
40
43
optype = OpTypeUpdate
41
44
}
42
45
43
-
results, err := s.repoman.applyWrites(repo.Repo, []Op{
46
+
results, err := s.repoman.applyWrites(ctx, repo.Repo, []Op{
44
47
{
45
48
Type: optype,
46
49
Collection: req.Collection,
···
51
54
},
52
55
}, req.SwapCommit)
53
56
if err != nil {
54
-
s.logger.Error("error applying writes", "error", err)
57
+
logger.Error("error applying writes", "error", err)
55
58
return helpers.ServerError(e, nil)
56
59
}
57
60
+59
-14
server/handle_repo_upload_blob.go
+59
-14
server/handle_repo_upload_blob.go
···
2
2
3
3
import (
4
4
"bytes"
5
+
"fmt"
5
6
"io"
6
7
8
+
"github.com/aws/aws-sdk-go/aws"
9
+
"github.com/aws/aws-sdk-go/aws/credentials"
10
+
"github.com/aws/aws-sdk-go/aws/session"
11
+
"github.com/aws/aws-sdk-go/service/s3"
7
12
"github.com/haileyok/cocoon/internal/helpers"
8
13
"github.com/haileyok/cocoon/models"
9
14
"github.com/ipfs/go-cid"
···
27
32
}
28
33
29
34
func (s *Server) handleRepoUploadBlob(e echo.Context) error {
35
+
ctx := e.Request().Context()
36
+
logger := s.logger.With("name", "handleRepoUploadBlob")
37
+
30
38
urepo := e.Get("repo").(*models.RepoActor)
31
39
32
40
mime := e.Request().Header.Get("content-type")
···
34
42
mime = "application/octet-stream"
35
43
}
36
44
45
+
storage := "sqlite"
46
+
s3Upload := s.s3Config != nil && s.s3Config.BlobstoreEnabled
47
+
if s3Upload {
48
+
storage = "s3"
49
+
}
37
50
blob := models.Blob{
38
51
Did: urepo.Repo.Did,
39
52
RefCount: 0,
40
53
CreatedAt: s.repoman.clock.Next().String(),
54
+
Storage: storage,
41
55
}
42
56
43
-
if err := s.db.Create(&blob, nil).Error; err != nil {
44
-
s.logger.Error("error creating new blob in db", "error", err)
57
+
if err := s.db.Create(ctx, &blob, nil).Error; err != nil {
58
+
logger.Error("error creating new blob in db", "error", err)
45
59
return helpers.ServerError(e, nil)
46
60
}
47
61
···
58
72
break
59
73
}
60
74
} else if err != nil && err != io.ErrUnexpectedEOF {
61
-
s.logger.Error("error reading blob", "error", err)
75
+
logger.Error("error reading blob", "error", err)
62
76
return helpers.ServerError(e, nil)
63
77
}
64
78
···
66
80
read += n
67
81
fulldata.Write(data)
68
82
69
-
blobPart := models.BlobPart{
70
-
BlobID: blob.ID,
71
-
Idx: part,
72
-
Data: data,
73
-
}
83
+
if !s3Upload {
84
+
blobPart := models.BlobPart{
85
+
BlobID: blob.ID,
86
+
Idx: part,
87
+
Data: data,
88
+
}
74
89
75
-
if err := s.db.Create(&blobPart, nil).Error; err != nil {
76
-
s.logger.Error("error adding blob part to db", "error", err)
77
-
return helpers.ServerError(e, nil)
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
+
}
78
94
}
79
95
part++
80
96
···
85
101
86
102
c, err := cid.NewPrefixV1(cid.Raw, multihash.SHA2_256).Sum(fulldata.Bytes())
87
103
if err != nil {
88
-
s.logger.Error("error creating cid prefix", "error", err)
104
+
logger.Error("error creating cid prefix", "error", err)
89
105
return helpers.ServerError(e, nil)
90
106
}
91
107
92
-
if err := s.db.Exec("UPDATE blobs SET cid = ? WHERE id = ?", nil, c.Bytes(), blob.ID).Error; err != nil {
108
+
if s3Upload {
109
+
config := &aws.Config{
110
+
Region: aws.String(s.s3Config.Region),
111
+
Credentials: credentials.NewStaticCredentials(s.s3Config.AccessKey, s.s3Config.SecretKey, ""),
112
+
}
113
+
114
+
if s.s3Config.Endpoint != "" {
115
+
config.Endpoint = aws.String(s.s3Config.Endpoint)
116
+
config.S3ForcePathStyle = aws.Bool(true)
117
+
}
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
+
125
+
svc := s3.New(sess)
126
+
127
+
if _, err := svc.PutObject(&s3.PutObjectInput{
128
+
Bucket: aws.String(s.s3Config.Bucket),
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 {
93
138
// there should probably be somme handling here if this fails...
94
-
s.logger.Error("error updating blob", "error", err)
139
+
logger.Error("error updating blob", "error", err)
95
140
return helpers.ServerError(e, nil)
96
141
}
97
142
+48
server/handle_server_activate_account.go
+48
server/handle_server_activate_account.go
···
1
+
package server
2
+
3
+
import (
4
+
"context"
5
+
"time"
6
+
7
+
"github.com/bluesky-social/indigo/api/atproto"
8
+
"github.com/bluesky-social/indigo/events"
9
+
"github.com/bluesky-social/indigo/util"
10
+
"github.com/haileyok/cocoon/internal/helpers"
11
+
"github.com/haileyok/cocoon/models"
12
+
"github.com/labstack/echo/v4"
13
+
)
14
+
15
+
type ComAtprotoServerActivateAccountRequest struct {
16
+
// NOTE: this implementation will not pay attention to this value
17
+
DeleteAfter time.Time `json:"deleteAfter"`
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
+
37
+
s.evtman.AddEvent(context.TODO(), &events.XRPCStreamEvent{
38
+
RepoAccount: &atproto.SyncSubscribeRepos_Account{
39
+
Active: true,
40
+
Did: urepo.Repo.Did,
41
+
Status: nil,
42
+
Seq: time.Now().UnixMicro(), // TODO: bad puppy
43
+
Time: time.Now().Format(util.ISO8601),
44
+
},
45
+
})
46
+
47
+
return e.NoContent(200)
48
+
}
+10
-7
server/handle_server_check_account_status.go
+10
-7
server/handle_server_check_account_status.go
···
20
20
}
21
21
22
22
func (s *Server) handleServerCheckAccountStatus(e echo.Context) error {
23
+
ctx := e.Request().Context()
24
+
logger := s.logger.With("name", "handleServerCheckAccountStatus")
25
+
23
26
urepo := e.Get("repo").(*models.RepoActor)
24
27
25
28
resp := ComAtprotoServerCheckAccountStatusResponse{
···
31
34
32
35
rootcid, err := cid.Cast(urepo.Root)
33
36
if err != nil {
34
-
s.logger.Error("error casting cid", "error", err)
37
+
logger.Error("error casting cid", "error", err)
35
38
return helpers.ServerError(e, nil)
36
39
}
37
40
resp.RepoCommit = rootcid.String()
···
41
44
}
42
45
43
46
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)
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)
46
49
return helpers.ServerError(e, nil)
47
50
}
48
51
resp.RepoBlocks = blockCtResp.Ct
49
52
50
53
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)
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)
53
56
return helpers.ServerError(e, nil)
54
57
}
55
58
resp.IndexedRecords = recCtResp.Ct
56
59
57
60
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)
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)
60
63
return helpers.ServerError(e, nil)
61
64
}
62
65
resp.ExpectedBlobs = blobCtResp.Ct
+6
-3
server/handle_server_confirm_email.go
+6
-3
server/handle_server_confirm_email.go
···
15
15
}
16
16
17
17
func (s *Server) handleServerConfirmEmail(e echo.Context) error {
18
+
ctx := e.Request().Context()
19
+
logger := s.logger.With("name", "handleServerConfirmEmail")
20
+
18
21
urepo := e.Get("repo").(*models.RepoActor)
19
22
20
23
var req ComAtprotoServerConfirmEmailRequest
21
24
if err := e.Bind(&req); err != nil {
22
-
s.logger.Error("error binding", "error", err)
25
+
logger.Error("error binding", "error", err)
23
26
return helpers.ServerError(e, nil)
24
27
}
25
28
···
41
44
42
45
now := time.Now().UTC()
43
46
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)
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)
46
49
return helpers.ServerError(e, nil)
47
50
}
48
51
+108
-72
server/handle_server_create_account.go
+108
-72
server/handle_server_create_account.go
···
9
9
10
10
"github.com/Azure/go-autorest/autorest/to"
11
11
"github.com/bluesky-social/indigo/api/atproto"
12
-
"github.com/bluesky-social/indigo/atproto/crypto"
13
-
"github.com/bluesky-social/indigo/atproto/syntax"
12
+
"github.com/bluesky-social/indigo/atproto/atcrypto"
14
13
"github.com/bluesky-social/indigo/events"
15
14
"github.com/bluesky-social/indigo/repo"
16
15
"github.com/bluesky-social/indigo/util"
···
26
25
Handle string `json:"handle" validate:"required,atproto-handle"`
27
26
Did *string `json:"did" validate:"atproto-did"`
28
27
Password string `json:"password" validate:"required"`
29
-
InviteCode string `json:"inviteCode" validate:"required"`
28
+
InviteCode string `json:"inviteCode" validate:"omitempty"`
30
29
}
31
30
32
31
type ComAtprotoServerCreateAccountResponse struct {
···
37
36
}
38
37
39
38
func (s *Server) handleCreateAccount(e echo.Context) error {
39
+
ctx := e.Request().Context()
40
+
logger := s.logger.With("name", "handleServerCreateAccount")
41
+
40
42
var request ComAtprotoServerCreateAccountRequest
41
43
42
-
var signupDid string
43
-
customDidHeader := e.Request().Header.Get("authorization")
44
-
if customDidHeader != "" {
45
-
pts := strings.Split(customDidHeader, " ")
46
-
if len(pts) != 2 {
47
-
return helpers.InputError(e, to.StringPtr("InvalidDid"))
48
-
}
49
-
50
-
_, err := syntax.ParseDID(pts[1])
51
-
if err != nil {
52
-
return helpers.InputError(e, to.StringPtr("InvalidDid"))
53
-
}
54
-
55
-
signupDid = pts[1]
56
-
}
57
-
58
44
if err := e.Bind(&request); err != nil {
59
-
s.logger.Error("error receiving request", "endpoint", "com.atproto.server.createAccount", "error", err)
45
+
logger.Error("error receiving request", "endpoint", "com.atproto.server.createAccount", "error", err)
60
46
return helpers.ServerError(e, nil)
61
47
}
62
48
63
49
request.Handle = strings.ToLower(request.Handle)
64
50
65
51
if err := e.Validate(request); err != nil {
66
-
s.logger.Error("error validating request", "endpoint", "com.atproto.server.createAccount", "error", err)
52
+
logger.Error("error validating request", "endpoint", "com.atproto.server.createAccount", "error", err)
67
53
68
54
var verr ValidationError
69
55
if errors.As(err, &verr) {
···
86
72
}
87
73
}
88
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"))
82
+
}
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
+
90
+
if authDid != signupDid {
91
+
return helpers.ForbiddenError(e, to.StringPtr("auth did did not match signup did"))
92
+
}
93
+
}
94
+
89
95
// see if the handle is already taken
90
-
_, err := s.getActorByHandle(request.Handle)
96
+
actor, err := s.getActorByHandle(ctx, request.Handle)
91
97
if err != nil && err != gorm.ErrRecordNotFound {
92
-
s.logger.Error("error looking up handle in db", "endpoint", "com.atproto.server.createAccount", "error", err)
98
+
logger.Error("error looking up handle in db", "endpoint", "com.atproto.server.createAccount", "error", err)
93
99
return helpers.ServerError(e, nil)
94
100
}
95
-
if err == nil {
101
+
if err == nil && actor.Did != signupDid {
96
102
return helpers.InputError(e, to.StringPtr("HandleNotAvailable"))
97
103
}
98
104
99
-
if did, err := s.passport.ResolveHandle(e.Request().Context(), request.Handle); err == nil && did != "" {
105
+
if did, err := s.passport.ResolveHandle(e.Request().Context(), request.Handle); err == nil && did != signupDid {
100
106
return helpers.InputError(e, to.StringPtr("HandleNotAvailable"))
101
107
}
102
108
103
109
var ic models.InviteCode
104
-
if err := s.db.Raw("SELECT * FROM invite_codes WHERE code = ?", nil, request.InviteCode).Scan(&ic).Error; err != nil {
105
-
if err == gorm.ErrRecordNotFound {
110
+
if s.config.RequireInvite {
111
+
if strings.TrimSpace(request.InviteCode) == "" {
106
112
return helpers.InputError(e, to.StringPtr("InvalidInviteCode"))
107
113
}
108
-
s.logger.Error("error getting invite code from db", "error", err)
109
-
return helpers.ServerError(e, nil)
110
-
}
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
+
}
111
122
112
-
if ic.RemainingUseCount < 1 {
113
-
return helpers.InputError(e, to.StringPtr("InvalidInviteCode"))
123
+
if ic.RemainingUseCount < 1 {
124
+
return helpers.InputError(e, to.StringPtr("InvalidInviteCode"))
125
+
}
114
126
}
115
127
116
128
// see if the email is already taken
117
-
_, err = s.getRepoByEmail(request.Email)
129
+
existingRepo, err := s.getRepoByEmail(ctx, request.Email)
118
130
if err != nil && err != gorm.ErrRecordNotFound {
119
-
s.logger.Error("error looking up email in db", "endpoint", "com.atproto.server.createAccount", "error", err)
131
+
logger.Error("error looking up email in db", "endpoint", "com.atproto.server.createAccount", "error", err)
120
132
return helpers.ServerError(e, nil)
121
133
}
122
-
if err == nil {
134
+
if err == nil && existingRepo.Did != signupDid {
123
135
return helpers.InputError(e, to.StringPtr("EmailNotAvailable"))
124
136
}
125
137
126
138
// TODO: unsupported domains
127
139
128
-
k, err := crypto.GeneratePrivateKeyK256()
129
-
if err != nil {
130
-
s.logger.Error("error creating signing key", "endpoint", "com.atproto.server.createAccount", "error", err)
131
-
return helpers.ServerError(e, nil)
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
+
}
132
168
}
133
169
134
170
if signupDid == "" {
135
171
did, op, err := s.plcClient.CreateDID(k, "", request.Handle)
136
172
if err != nil {
137
-
s.logger.Error("error creating operation", "endpoint", "com.atproto.server.createAccount", "error", err)
173
+
logger.Error("error creating operation", "endpoint", "com.atproto.server.createAccount", "error", err)
138
174
return helpers.ServerError(e, nil)
139
175
}
140
176
141
177
if err := s.plcClient.SendOperation(e.Request().Context(), did, op); err != nil {
142
-
s.logger.Error("error sending plc op", "endpoint", "com.atproto.server.createAccount", "error", err)
178
+
logger.Error("error sending plc op", "endpoint", "com.atproto.server.createAccount", "error", err)
143
179
return helpers.ServerError(e, nil)
144
180
}
145
181
signupDid = did
···
147
183
148
184
hashed, err := bcrypt.GenerateFromPassword([]byte(request.Password), 10)
149
185
if err != nil {
150
-
s.logger.Error("error hashing password", "error", err)
186
+
logger.Error("error hashing password", "error", err)
151
187
return helpers.ServerError(e, nil)
152
188
}
153
189
···
160
196
SigningKey: k.Bytes(),
161
197
}
162
198
163
-
actor := models.Actor{
164
-
Did: signupDid,
165
-
Handle: request.Handle,
166
-
}
199
+
if actor == nil {
200
+
actor = &models.Actor{
201
+
Did: signupDid,
202
+
Handle: request.Handle,
203
+
}
167
204
168
-
if err := s.db.Create(&urepo, nil).Error; err != nil {
169
-
s.logger.Error("error inserting new repo", "error", err)
170
-
return helpers.ServerError(e, nil)
171
-
}
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
+
}
172
209
173
-
if err := s.db.Create(&actor, nil).Error; err != nil {
174
-
s.logger.Error("error inserting new actor", "error", err)
175
-
return helpers.ServerError(e, nil)
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
+
}
176
219
}
177
220
178
-
if customDidHeader == "" {
221
+
if request.Did == nil || *request.Did == "" {
179
222
bs := s.getBlockstore(signupDid)
180
223
r := repo.NewRepo(context.TODO(), signupDid, bs)
181
224
182
225
root, rev, err := r.Commit(context.TODO(), urepo.SignFor)
183
226
if err != nil {
184
-
s.logger.Error("error committing", "error", err)
227
+
logger.Error("error committing", "error", err)
185
228
return helpers.ServerError(e, nil)
186
229
}
187
230
188
231
if err := s.UpdateRepo(context.TODO(), urepo.Did, root, rev); err != nil {
189
-
s.logger.Error("error updating repo after commit", "error", err)
232
+
logger.Error("error updating repo after commit", "error", err)
190
233
return helpers.ServerError(e, nil)
191
234
}
192
235
193
236
s.evtman.AddEvent(context.TODO(), &events.XRPCStreamEvent{
194
-
RepoHandle: &atproto.SyncSubscribeRepos_Handle{
195
-
Did: urepo.Did,
196
-
Handle: request.Handle,
197
-
Seq: time.Now().UnixMicro(), // TODO: no
198
-
Time: time.Now().Format(util.ISO8601),
199
-
},
200
-
})
201
-
202
-
s.evtman.AddEvent(context.TODO(), &events.XRPCStreamEvent{
203
237
RepoIdentity: &atproto.SyncSubscribeRepos_Identity{
204
238
Did: urepo.Did,
205
239
Handle: to.StringPtr(request.Handle),
···
209
243
})
210
244
}
211
245
212
-
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 {
213
-
s.logger.Error("error decrementing use count", "error", err)
214
-
return helpers.ServerError(e, nil)
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
+
}
215
251
}
216
252
217
-
sess, err := s.createSession(&urepo)
253
+
sess, err := s.createSession(ctx, &urepo)
218
254
if err != nil {
219
-
s.logger.Error("error creating new session", "error", err)
255
+
logger.Error("error creating new session", "error", err)
220
256
return helpers.ServerError(e, nil)
221
257
}
222
258
223
259
go func() {
224
260
if err := s.sendEmailVerification(urepo.Email, actor.Handle, *urepo.EmailVerificationCode); err != nil {
225
-
s.logger.Error("error sending email verification email", "error", err)
261
+
logger.Error("error sending email verification email", "error", err)
226
262
}
227
263
if err := s.sendWelcomeMail(urepo.Email, actor.Handle); err != nil {
228
-
s.logger.Error("error sending welcome email", "error", err)
264
+
logger.Error("error sending welcome email", "error", err)
229
265
}
230
266
}()
231
267
+7
-4
server/handle_server_create_invite_code.go
+7
-4
server/handle_server_create_invite_code.go
···
17
17
}
18
18
19
19
func (s *Server) handleCreateInviteCode(e echo.Context) error {
20
+
ctx := e.Request().Context()
21
+
logger := s.logger.With("name", "handleServerCreateInviteCode")
22
+
20
23
var req ComAtprotoServerCreateInviteCodeRequest
21
24
if err := e.Bind(&req); err != nil {
22
-
s.logger.Error("error binding", "error", err)
25
+
logger.Error("error binding", "error", err)
23
26
return helpers.ServerError(e, nil)
24
27
}
25
28
26
29
if err := e.Validate(req); err != nil {
27
-
s.logger.Error("error validating", "error", err)
30
+
logger.Error("error validating", "error", err)
28
31
return helpers.InputError(e, nil)
29
32
}
30
33
···
37
40
acc = *req.ForAccount
38
41
}
39
42
40
-
if err := s.db.Create(&models.InviteCode{
43
+
if err := s.db.Create(ctx, &models.InviteCode{
41
44
Code: ic,
42
45
Did: acc,
43
46
RemainingUseCount: req.UseCount,
44
47
}, nil).Error; err != nil {
45
-
s.logger.Error("error creating invite code", "error", err)
48
+
logger.Error("error creating invite code", "error", err)
46
49
return helpers.ServerError(e, nil)
47
50
}
48
51
+7
-4
server/handle_server_create_invite_codes.go
+7
-4
server/handle_server_create_invite_codes.go
···
22
22
}
23
23
24
24
func (s *Server) handleCreateInviteCodes(e echo.Context) error {
25
+
ctx := e.Request().Context()
26
+
logger := s.logger.With("name", "handleServerCreateInviteCodes")
27
+
25
28
var req ComAtprotoServerCreateInviteCodesRequest
26
29
if err := e.Bind(&req); err != nil {
27
-
s.logger.Error("error binding", "error", err)
30
+
logger.Error("error binding", "error", err)
28
31
return helpers.ServerError(e, nil)
29
32
}
30
33
31
34
if err := e.Validate(req); err != nil {
32
-
s.logger.Error("error validating", "error", err)
35
+
logger.Error("error validating", "error", err)
33
36
return helpers.InputError(e, nil)
34
37
}
35
38
···
50
53
ic := uuid.NewString()
51
54
ics = append(ics, ic)
52
55
53
-
if err := s.db.Create(&models.InviteCode{
56
+
if err := s.db.Create(ctx, &models.InviteCode{
54
57
Code: ic,
55
58
Did: did,
56
59
RemainingUseCount: req.UseCount,
57
60
}, nil).Error; err != nil {
58
-
s.logger.Error("error creating invite code", "error", err)
61
+
logger.Error("error creating invite code", "error", err)
59
62
return helpers.ServerError(e, nil)
60
63
}
61
64
}
+67
-11
server/handle_server_create_session.go
+67
-11
server/handle_server_create_session.go
···
1
1
package server
2
2
3
3
import (
4
+
"context"
4
5
"errors"
6
+
"fmt"
5
7
"strings"
8
+
"time"
6
9
7
10
"github.com/Azure/go-autorest/autorest/to"
8
11
"github.com/bluesky-social/indigo/atproto/syntax"
···
32
35
}
33
36
34
37
func (s *Server) handleCreateSession(e echo.Context) error {
38
+
ctx := e.Request().Context()
39
+
logger := s.logger.With("name", "handleServerCreateSession")
40
+
35
41
var req ComAtprotoServerCreateSessionRequest
36
42
if err := e.Bind(&req); err != nil {
37
-
s.logger.Error("error binding request", "endpoint", "com.atproto.server.serverCreateSession", "error", err)
43
+
logger.Error("error binding request", "endpoint", "com.atproto.server.serverCreateSession", "error", err)
38
44
return helpers.ServerError(e, nil)
39
45
}
40
46
···
65
71
var err error
66
72
switch idtype {
67
73
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
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
69
75
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
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
71
77
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
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
73
79
}
74
80
75
81
if err != nil {
···
77
83
return helpers.InputError(e, to.StringPtr("InvalidRequest"))
78
84
}
79
85
80
-
s.logger.Error("erorr looking up repo", "endpoint", "com.atproto.server.createSession", "error", err)
86
+
logger.Error("erorr looking up repo", "endpoint", "com.atproto.server.createSession", "error", err)
81
87
return helpers.ServerError(e, nil)
82
88
}
83
89
84
90
if err := bcrypt.CompareHashAndPassword([]byte(repo.Password), []byte(req.Password)); err != nil {
85
91
if err != bcrypt.ErrMismatchedHashAndPassword {
86
-
s.logger.Error("erorr comparing hash and password", "error", err)
92
+
logger.Error("erorr comparing hash and password", "error", err)
87
93
}
88
94
return helpers.InputError(e, to.StringPtr("InvalidRequest"))
89
95
}
90
96
91
-
sess, err := s.createSession(&repo.Repo)
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)
92
130
if err != nil {
93
-
s.logger.Error("error creating session", "error", err)
131
+
logger.Error("error creating session", "error", err)
94
132
return helpers.ServerError(e, nil)
95
133
}
96
134
···
101
139
Did: repo.Repo.Did,
102
140
Email: repo.Email,
103
141
EmailConfirmed: repo.EmailConfirmedAt != nil,
104
-
EmailAuthFactor: false,
105
-
Active: true, // TODO: eventually do takedowns
106
-
Status: nil, // TODO eventually do takedowns
142
+
EmailAuthFactor: repo.TwoFactorType != models.TwoFactorTypeNone,
143
+
Active: repo.Active(),
144
+
Status: repo.Status(),
107
145
})
108
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
+
}
+49
server/handle_server_deactivate_account.go
+49
server/handle_server_deactivate_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/haileyok/cocoon/models"
13
+
"github.com/labstack/echo/v4"
14
+
)
15
+
16
+
type ComAtprotoServerDeactivateAccountRequest struct {
17
+
// NOTE: this implementation will not pay attention to this value
18
+
DeleteAfter time.Time `json:"deleteAfter"`
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
+
38
+
s.evtman.AddEvent(context.TODO(), &events.XRPCStreamEvent{
39
+
RepoAccount: &atproto.SyncSubscribeRepos_Account{
40
+
Active: false,
41
+
Did: urepo.Repo.Did,
42
+
Status: to.StringPtr("deactivated"),
43
+
Seq: time.Now().UnixMicro(), // TODO: bad puppy
44
+
Time: time.Now().Format(util.ISO8601),
45
+
},
46
+
})
47
+
48
+
return e.NoContent(200)
49
+
}
+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
7
)
8
8
9
9
func (s *Server) handleDeleteSession(e echo.Context) error {
10
+
ctx := e.Request().Context()
11
+
10
12
token := e.Get("token").(string)
11
13
12
14
var acctok models.Token
13
-
if err := s.db.Raw("DELETE FROM tokens WHERE token = ? RETURNING *", nil, token).Scan(&acctok).Error; err != nil {
15
+
if err := s.db.Raw(ctx, "DELETE FROM tokens WHERE token = ? RETURNING *", nil, token).Scan(&acctok).Error; err != nil {
14
16
s.logger.Error("error deleting access token from db", "error", err)
15
17
return helpers.ServerError(e, nil)
16
18
}
17
19
18
-
if err := s.db.Exec("DELETE FROM refresh_tokens WHERE token = ?", nil, acctok.RefreshToken).Error; err != nil {
20
+
if err := s.db.Exec(ctx, "DELETE FROM refresh_tokens WHERE token = ?", nil, acctok.RefreshToken).Error; err != nil {
19
21
s.logger.Error("error deleting refresh token from db", "error", err)
20
22
return helpers.ServerError(e, nil)
21
23
}
+1
-1
server/handle_server_describe_server.go
+1
-1
server/handle_server_describe_server.go
···
22
22
23
23
func (s *Server) handleDescribeServer(e echo.Context) error {
24
24
return e.JSON(200, ComAtprotoServerDescribeServerResponse{
25
-
InviteCodeRequired: true,
25
+
InviteCodeRequired: s.config.RequireInvite,
26
26
PhoneVerificationRequired: false,
27
27
AvailableUserDomains: []string{"." + s.config.Hostname}, // TODO: more
28
28
Links: ComAtprotoServerDescribeServerResponseLinks{
+17
-8
server/handle_server_get_service_auth.go
+17
-8
server/handle_server_get_service_auth.go
···
21
21
Aud string `query:"aud" validate:"required,atproto-did"`
22
22
// exp should be a float, as some clients will send a non-integer expiration
23
23
Exp float64 `query:"exp"`
24
-
Lxm string `query:"lxm" validate:"required,atproto-nsid"`
24
+
Lxm string `query:"lxm"`
25
25
}
26
26
27
27
func (s *Server) handleServerGetServiceAuth(e echo.Context) error {
28
+
logger := s.logger.With("name", "handleServerGetServiceAuth")
29
+
28
30
var req ServerGetServiceAuthRequest
29
31
if err := e.Bind(&req); err != nil {
30
-
s.logger.Error("could not bind service auth request", "error", err)
32
+
logger.Error("could not bind service auth request", "error", err)
31
33
return helpers.ServerError(e, nil)
32
34
}
33
35
···
45
47
return helpers.InputError(e, to.StringPtr("may not generate auth tokens recursively"))
46
48
}
47
49
48
-
maxExp := now + (60 * 30)
50
+
var maxExp int64
51
+
if req.Lxm != "" {
52
+
maxExp = now + (60 * 60)
53
+
} else {
54
+
maxExp = now + 60
55
+
}
49
56
if exp > maxExp {
50
57
return helpers.InputError(e, to.StringPtr("expiration too big. smoller please"))
51
58
}
···
59
66
}
60
67
hj, err := json.Marshal(header)
61
68
if err != nil {
62
-
s.logger.Error("error marshaling header", "error", err)
69
+
logger.Error("error marshaling header", "error", err)
63
70
return helpers.ServerError(e, nil)
64
71
}
65
72
···
68
75
payload := map[string]any{
69
76
"iss": repo.Repo.Did,
70
77
"aud": req.Aud,
71
-
"lxm": req.Lxm,
72
78
"jti": uuid.NewString(),
73
79
"exp": exp,
74
80
"iat": now,
81
+
}
82
+
if req.Lxm != "" {
83
+
payload["lxm"] = req.Lxm
75
84
}
76
85
pj, err := json.Marshal(payload)
77
86
if err != nil {
78
-
s.logger.Error("error marashaling payload", "error", err)
87
+
logger.Error("error marashaling payload", "error", err)
79
88
return helpers.ServerError(e, nil)
80
89
}
81
90
···
86
95
87
96
sk, err := secp256k1secec.NewPrivateKey(repo.SigningKey)
88
97
if err != nil {
89
-
s.logger.Error("can't load private key", "error", err)
98
+
logger.Error("can't load private key", "error", err)
90
99
return err
91
100
}
92
101
93
102
R, S, _, err := sk.SignRaw(rand.Reader, hash[:])
94
103
if err != nil {
95
-
s.logger.Error("error signing", "error", err)
104
+
logger.Error("error signing", "error", err)
96
105
return helpers.ServerError(e, nil)
97
106
}
98
107
+3
-3
server/handle_server_get_session.go
+3
-3
server/handle_server_get_session.go
···
23
23
Did: repo.Repo.Did,
24
24
Email: repo.Email,
25
25
EmailConfirmed: repo.EmailConfirmedAt != nil,
26
-
EmailAuthFactor: false, // TODO: todo todo
27
-
Active: true,
28
-
Status: nil,
26
+
EmailAuthFactor: repo.TwoFactorType != models.TwoFactorTypeNone,
27
+
Active: repo.Active(),
28
+
Status: repo.Status(),
29
29
})
30
30
}
+11
-8
server/handle_server_refresh_session.go
+11
-8
server/handle_server_refresh_session.go
···
16
16
}
17
17
18
18
func (s *Server) handleRefreshSession(e echo.Context) error {
19
+
ctx := e.Request().Context()
20
+
logger := s.logger.With("name", "handleServerRefreshSession")
21
+
19
22
token := e.Get("token").(string)
20
23
repo := e.Get("repo").(*models.RepoActor)
21
24
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)
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)
24
27
return helpers.ServerError(e, nil)
25
28
}
26
29
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)
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)
29
32
return helpers.ServerError(e, nil)
30
33
}
31
34
32
-
sess, err := s.createSession(&repo.Repo)
35
+
sess, err := s.createSession(ctx, &repo.Repo)
33
36
if err != nil {
34
-
s.logger.Error("error creating new session for refresh", "error", err)
37
+
logger.Error("error creating new session for refresh", "error", err)
35
38
return helpers.ServerError(e, nil)
36
39
}
37
40
···
40
43
RefreshJwt: sess.RefreshToken,
41
44
Handle: repo.Handle,
42
45
Did: repo.Repo.Did,
43
-
Active: true,
44
-
Status: nil,
46
+
Active: repo.Active(),
47
+
Status: repo.Status(),
45
48
})
46
49
}
+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
11
)
12
12
13
13
func (s *Server) handleServerRequestEmailConfirmation(e echo.Context) error {
14
+
ctx := e.Request().Context()
15
+
logger := s.logger.With("name", "handleServerRequestEmailConfirm")
16
+
14
17
urepo := e.Get("repo").(*models.RepoActor)
15
18
16
19
if urepo.EmailConfirmedAt != nil {
···
20
23
code := fmt.Sprintf("%s-%s", helpers.RandomVarchar(5), helpers.RandomVarchar(5))
21
24
eat := time.Now().Add(10 * time.Minute).UTC()
22
25
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)
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)
25
28
return helpers.ServerError(e, nil)
26
29
}
27
30
28
31
if err := s.sendEmailVerification(urepo.Email, urepo.Handle, code); err != nil {
29
-
s.logger.Error("error sending mail", "error", err)
32
+
logger.Error("error sending mail", "error", err)
30
33
return helpers.ServerError(e, nil)
31
34
}
32
35
+6
-3
server/handle_server_request_email_update.go
+6
-3
server/handle_server_request_email_update.go
···
14
14
}
15
15
16
16
func (s *Server) handleServerRequestEmailUpdate(e echo.Context) error {
17
+
ctx := e.Request().Context()
18
+
logger := s.logger.With("name", "handleServerRequestEmailUpdate")
19
+
17
20
urepo := e.Get("repo").(*models.RepoActor)
18
21
19
22
if urepo.EmailConfirmedAt != nil {
20
23
code := fmt.Sprintf("%s-%s", helpers.RandomVarchar(5), helpers.RandomVarchar(5))
21
24
eat := time.Now().Add(10 * time.Minute).UTC()
22
25
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)
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)
25
28
return helpers.ServerError(e, nil)
26
29
}
27
30
28
31
if err := s.sendEmailUpdate(urepo.Email, urepo.Handle, code); err != nil {
29
-
s.logger.Error("error sending email", "error", err)
32
+
logger.Error("error sending email", "error", err)
30
33
return helpers.ServerError(e, nil)
31
34
}
32
35
}
+7
-4
server/handle_server_request_password_reset.go
+7
-4
server/handle_server_request_password_reset.go
···
14
14
}
15
15
16
16
func (s *Server) handleServerRequestPasswordReset(e echo.Context) error {
17
+
ctx := e.Request().Context()
18
+
logger := s.logger.With("name", "handleServerRequestPasswordReset")
19
+
17
20
urepo, ok := e.Get("repo").(*models.RepoActor)
18
21
if !ok {
19
22
var req ComAtprotoServerRequestPasswordResetRequest
···
25
28
return err
26
29
}
27
30
28
-
murepo, err := s.getRepoActorByEmail(req.Email)
31
+
murepo, err := s.getRepoActorByEmail(ctx, req.Email)
29
32
if err != nil {
30
33
return err
31
34
}
···
36
39
code := fmt.Sprintf("%s-%s", helpers.RandomVarchar(5), helpers.RandomVarchar(5))
37
40
eat := time.Now().Add(10 * time.Minute).UTC()
38
41
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)
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)
41
44
return helpers.ServerError(e, nil)
42
45
}
43
46
44
47
if err := s.sendPasswordReset(urepo.Email, urepo.Handle, code); err != nil {
45
-
s.logger.Error("error sending email", "error", err)
48
+
logger.Error("error sending email", "error", err)
46
49
return helpers.ServerError(e, nil)
47
50
}
48
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
16
}
17
17
18
18
func (s *Server) handleServerResetPassword(e echo.Context) error {
19
+
ctx := e.Request().Context()
20
+
logger := s.logger.With("name", "handleServerResetPassword")
21
+
19
22
urepo := e.Get("repo").(*models.RepoActor)
20
23
21
24
var req ComAtprotoServerResetPasswordRequest
22
25
if err := e.Bind(&req); err != nil {
23
-
s.logger.Error("error binding", "error", err)
26
+
logger.Error("error binding", "error", err)
24
27
return helpers.ServerError(e, nil)
25
28
}
26
29
···
42
45
43
46
hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), 10)
44
47
if err != nil {
45
-
s.logger.Error("error creating hash", "error", err)
48
+
logger.Error("error creating hash", "error", err)
46
49
return helpers.ServerError(e, nil)
47
50
}
48
51
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)
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)
51
54
return helpers.ServerError(e, nil)
52
55
}
53
56
+3
-1
server/handle_server_resolve_handle.go
+3
-1
server/handle_server_resolve_handle.go
···
10
10
)
11
11
12
12
func (s *Server) handleResolveHandle(e echo.Context) error {
13
+
logger := s.logger.With("name", "handleServerResolveHandle")
14
+
13
15
type Resp struct {
14
16
Did string `json:"did"`
15
17
}
···
28
30
ctx := context.WithValue(e.Request().Context(), "skip-cache", true)
29
31
did, err := s.passport.ResolveHandle(ctx, parsed.String())
30
32
if err != nil {
31
-
s.logger.Error("error resolving handle", "error", err)
33
+
logger.Error("error resolving handle", "error", err)
32
34
return helpers.ServerError(e, nil)
33
35
}
34
36
+34
-9
server/handle_server_update_email.go
+34
-9
server/handle_server_update_email.go
···
11
11
type ComAtprotoServerUpdateEmailRequest struct {
12
12
Email string `json:"email" validate:"required"`
13
13
EmailAuthFactor bool `json:"emailAuthFactor"`
14
-
Token string `json:"token" validate:"required"`
14
+
Token string `json:"token"`
15
15
}
16
16
17
17
func (s *Server) handleServerUpdateEmail(e echo.Context) error {
18
+
ctx := e.Request().Context()
19
+
logger := s.logger.With("name", "handleServerUpdateEmail")
20
+
18
21
urepo := e.Get("repo").(*models.RepoActor)
19
22
20
23
var req ComAtprotoServerUpdateEmailRequest
21
24
if err := e.Bind(&req); err != nil {
22
-
s.logger.Error("error binding", "error", err)
25
+
logger.Error("error binding", "error", err)
23
26
return helpers.ServerError(e, nil)
24
27
}
25
28
···
27
30
return helpers.InputError(e, nil)
28
31
}
29
32
30
-
if urepo.EmailUpdateCode == nil || urepo.EmailUpdateCodeExpiresAt == nil {
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 == "" {
31
37
return helpers.InvalidTokenError(e)
32
38
}
33
39
34
-
if *urepo.EmailUpdateCode != req.Token {
35
-
return helpers.InvalidTokenError(e)
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
36
57
}
37
58
38
-
if time.Now().UTC().After(*urepo.EmailUpdateCodeExpiresAt) {
39
-
return helpers.ExpiredTokenError(e)
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"
40
63
}
41
64
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)
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)
44
69
return helpers.ServerError(e, nil)
45
70
}
46
71
+96
-10
server/handle_sync_get_blob.go
+96
-10
server/handle_sync_get_blob.go
···
2
2
3
3
import (
4
4
"bytes"
5
+
"fmt"
6
+
"io"
5
7
8
+
"github.com/Azure/go-autorest/autorest/to"
9
+
"github.com/aws/aws-sdk-go/aws"
10
+
"github.com/aws/aws-sdk-go/aws/credentials"
11
+
"github.com/aws/aws-sdk-go/aws/session"
12
+
"github.com/aws/aws-sdk-go/service/s3"
6
13
"github.com/haileyok/cocoon/internal/helpers"
7
14
"github.com/haileyok/cocoon/models"
8
15
"github.com/ipfs/go-cid"
···
10
17
)
11
18
12
19
func (s *Server) handleSyncGetBlob(e echo.Context) error {
20
+
ctx := e.Request().Context()
21
+
logger := s.logger.With("name", "handleSyncGetBlob")
22
+
13
23
did := e.QueryParam("did")
14
24
if did == "" {
15
25
return helpers.InputError(e, nil)
···
25
35
return helpers.InputError(e, nil)
26
36
}
27
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
+
44
+
status := urepo.Status()
45
+
if status != nil {
46
+
if *status == "deactivated" {
47
+
return helpers.InputError(e, to.StringPtr("RepoDeactivated"))
48
+
}
49
+
}
50
+
28
51
var blob models.Blob
29
-
if err := s.db.Raw("SELECT * FROM blobs WHERE did = ? AND cid = ?", nil, did, c.Bytes()).Scan(&blob).Error; err != nil {
30
-
s.logger.Error("error looking up blob", "error", err)
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)
31
54
return helpers.ServerError(e, nil)
32
55
}
33
56
34
57
buf := new(bytes.Buffer)
35
58
36
-
var parts []models.BlobPart
37
-
if err := s.db.Raw("SELECT * FROM blob_parts WHERE blob_id = ? ORDER BY idx", nil, blob.ID).Scan(&parts).Error; err != nil {
38
-
s.logger.Error("error getting blob parts", "error", err)
39
-
return helpers.ServerError(e, nil)
40
-
}
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
+
}
41
65
42
-
// TODO: we can just stream this, don't need to make a buffer
43
-
for _, p := range parts {
44
-
buf.Write(p.Data)
66
+
// TODO: we can just stream this, don't need to make a buffer
67
+
for _, p := range parts {
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{
84
+
Region: aws.String(s.s3Config.Region),
85
+
Credentials: credentials.NewStaticCredentials(s.s3Config.AccessKey, s.s3Config.SecretKey, ""),
86
+
}
87
+
88
+
if s.s3Config.Endpoint != "" {
89
+
config.Endpoint = aws.String(s.s3Config.Endpoint)
90
+
config.S3ForcePathStyle = aws.Bool(true)
91
+
}
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
108
+
part := 0
109
+
partBuf := make([]byte, 0x10000)
110
+
111
+
for {
112
+
n, err := io.ReadFull(result.Body, partBuf)
113
+
if err == io.ErrUnexpectedEOF || err == io.EOF {
114
+
if n == 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
+
122
+
data := partBuf[:n]
123
+
read += n
124
+
buf.Write(data)
125
+
part++
126
+
}
127
+
}
128
+
} else {
129
+
logger.Error("unknown storage", "storage", blob.Storage)
130
+
return helpers.ServerError(e, nil)
45
131
}
46
132
47
133
e.Response().Header().Set(echo.HeaderContentDisposition, "attachment; filename="+c.String())
+15
-11
server/handle_sync_get_blocks.go
+15
-11
server/handle_sync_get_blocks.go
···
2
2
3
3
import (
4
4
"bytes"
5
-
"context"
6
-
"strings"
7
5
8
6
"github.com/bluesky-social/indigo/carstore"
9
7
"github.com/haileyok/cocoon/internal/helpers"
···
13
11
"github.com/labstack/echo/v4"
14
12
)
15
13
14
+
type ComAtprotoSyncGetBlocksRequest struct {
15
+
Did string `query:"did"`
16
+
Cids []string `query:"cids"`
17
+
}
18
+
16
19
func (s *Server) handleGetBlocks(e echo.Context) error {
17
-
did := e.QueryParam("did")
18
-
cidsstr := e.QueryParam("cids")
19
-
if did == "" {
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 {
20
25
return helpers.InputError(e, nil)
21
26
}
22
27
23
-
cidstrs := strings.Split(cidsstr, ",")
24
-
cids := []cid.Cid{}
28
+
var cids []cid.Cid
25
29
26
-
for _, cs := range cidstrs {
30
+
for _, cs := range req.Cids {
27
31
c, err := cid.Cast([]byte(cs))
28
32
if err != nil {
29
33
return err
···
32
36
cids = append(cids, c)
33
37
}
34
38
35
-
urepo, err := s.getRepoActorByDid(did)
39
+
urepo, err := s.getRepoActorByDid(ctx, req.Did)
36
40
if err != nil {
37
41
return helpers.ServerError(e, nil)
38
42
}
···
49
53
})
50
54
51
55
if _, err := carstore.LdWrite(buf, hb); err != nil {
52
-
s.logger.Error("error writing to car", "error", err)
56
+
logger.Error("error writing to car", "error", err)
53
57
return helpers.ServerError(e, nil)
54
58
}
55
59
56
60
bs := s.getBlockstore(urepo.Repo.Did)
57
61
58
62
for _, c := range cids {
59
-
b, err := bs.Get(context.TODO(), c)
63
+
b, err := bs.Get(ctx, c)
60
64
if err != nil {
61
65
return err
62
66
}
+3
-1
server/handle_sync_get_latest_commit.go
+3
-1
server/handle_sync_get_latest_commit.go
···
12
12
}
13
13
14
14
func (s *Server) handleSyncGetLatestCommit(e echo.Context) error {
15
+
ctx := e.Request().Context()
16
+
15
17
did := e.QueryParam("did")
16
18
if did == "" {
17
19
return helpers.InputError(e, nil)
18
20
}
19
21
20
-
urepo, err := s.getRepoActorByDid(did)
22
+
urepo, err := s.getRepoActorByDid(ctx, did)
21
23
if err != nil {
22
24
return err
23
25
}
+8
-5
server/handle_sync_get_record.go
+8
-5
server/handle_sync_get_record.go
···
13
13
)
14
14
15
15
func (s *Server) handleSyncGetRecord(e echo.Context) error {
16
+
ctx := e.Request().Context()
17
+
logger := s.logger.With("name", "handleSyncGetRecord")
18
+
16
19
did := e.QueryParam("did")
17
20
collection := e.QueryParam("collection")
18
21
rkey := e.QueryParam("rkey")
19
22
20
23
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)
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)
23
26
return helpers.ServerError(e, nil)
24
27
}
25
28
26
-
root, blocks, err := s.repoman.getRecordProof(urepo, collection, rkey)
29
+
root, blocks, err := s.repoman.getRecordProof(ctx, urepo, collection, rkey)
27
30
if err != nil {
28
31
return err
29
32
}
···
36
39
})
37
40
38
41
if _, err := carstore.LdWrite(buf, hb); err != nil {
39
-
s.logger.Error("error writing to car", "error", err)
42
+
logger.Error("error writing to car", "error", err)
40
43
return helpers.ServerError(e, nil)
41
44
}
42
45
43
46
for _, blk := range blocks {
44
47
if _, err := carstore.LdWrite(buf, blk.Cid().Bytes(), blk.RawData()); err != nil {
45
-
s.logger.Error("error writing to car", "error", err)
48
+
logger.Error("error writing to car", "error", err)
46
49
return helpers.ServerError(e, nil)
47
50
}
48
51
}
+6
-3
server/handle_sync_get_repo.go
+6
-3
server/handle_sync_get_repo.go
···
13
13
)
14
14
15
15
func (s *Server) handleSyncGetRepo(e echo.Context) error {
16
+
ctx := e.Request().Context()
17
+
logger := s.logger.With("name", "handleSyncGetRepo")
18
+
16
19
did := e.QueryParam("did")
17
20
if did == "" {
18
21
return helpers.InputError(e, nil)
19
22
}
20
23
21
-
urepo, err := s.getRepoActorByDid(did)
24
+
urepo, err := s.getRepoActorByDid(ctx, did)
22
25
if err != nil {
23
26
return err
24
27
}
···
36
39
buf := new(bytes.Buffer)
37
40
38
41
if _, err := carstore.LdWrite(buf, hb); err != nil {
39
-
s.logger.Error("error writing to car", "error", err)
42
+
logger.Error("error writing to car", "error", err)
40
43
return helpers.ServerError(e, nil)
41
44
}
42
45
43
46
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 {
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 {
45
48
return err
46
49
}
47
50
+5
-3
server/handle_sync_get_repo_status.go
+5
-3
server/handle_sync_get_repo_status.go
···
14
14
15
15
// TODO: make this actually do the right thing
16
16
func (s *Server) handleSyncGetRepoStatus(e echo.Context) error {
17
+
ctx := e.Request().Context()
18
+
17
19
did := e.QueryParam("did")
18
20
if did == "" {
19
21
return helpers.InputError(e, nil)
20
22
}
21
23
22
-
urepo, err := s.getRepoActorByDid(did)
24
+
urepo, err := s.getRepoActorByDid(ctx, did)
23
25
if err != nil {
24
26
return err
25
27
}
26
28
27
29
return e.JSON(200, ComAtprotoSyncGetRepoStatusResponse{
28
30
Did: urepo.Repo.Did,
29
-
Active: true,
30
-
Status: nil,
31
+
Active: urepo.Active(),
32
+
Status: urepo.Status(),
31
33
Rev: &urepo.Rev,
32
34
})
33
35
}
+20
-3
server/handle_sync_list_blobs.go
+20
-3
server/handle_sync_list_blobs.go
···
1
1
package server
2
2
3
3
import (
4
+
"github.com/Azure/go-autorest/autorest/to"
4
5
"github.com/haileyok/cocoon/internal/helpers"
5
6
"github.com/haileyok/cocoon/models"
6
7
"github.com/ipfs/go-cid"
···
13
14
}
14
15
15
16
func (s *Server) handleSyncListBlobs(e echo.Context) error {
17
+
ctx := e.Request().Context()
18
+
logger := s.logger.With("name", "handleSyncListBlobs")
19
+
16
20
did := e.QueryParam("did")
17
21
if did == "" {
18
22
return helpers.InputError(e, nil)
···
34
38
}
35
39
params = append(params, limit)
36
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
+
47
+
status := urepo.Status()
48
+
if status != nil {
49
+
if *status == "deactivated" {
50
+
return helpers.InputError(e, to.StringPtr("RepoDeactivated"))
51
+
}
52
+
}
53
+
37
54
var blobs []models.Blob
38
-
if err := s.db.Raw("SELECT * FROM blobs WHERE did = ? "+cursorquery+" ORDER BY created_at DESC LIMIT ?", nil, params...).Scan(&blobs).Error; err != nil {
39
-
s.logger.Error("error getting records", "error", err)
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)
40
57
return helpers.ServerError(e, nil)
41
58
}
42
59
···
44
61
for _, b := range blobs {
45
62
c, err := cid.Cast(b.Cid)
46
63
if err != nil {
47
-
s.logger.Error("error casting cid", "error", err)
64
+
logger.Error("error casting cid", "error", err)
48
65
return helpers.ServerError(e, nil)
49
66
}
50
67
cstrs = append(cstrs, c.String())
+92
-58
server/handle_sync_subscribe_repos.go
+92
-58
server/handle_sync_subscribe_repos.go
···
1
1
package server
2
2
3
3
import (
4
-
"fmt"
5
-
"net/http"
4
+
"context"
5
+
"time"
6
6
7
7
"github.com/bluesky-social/indigo/events"
8
8
"github.com/bluesky-social/indigo/lex/util"
9
9
"github.com/btcsuite/websocket"
10
+
"github.com/haileyok/cocoon/metrics"
10
11
"github.com/labstack/echo/v4"
11
12
)
12
13
13
-
var upgrader = websocket.Upgrader{
14
-
ReadBufferSize: 1024,
15
-
WriteBufferSize: 1024,
16
-
CheckOrigin: func(r *http.Request) bool {
17
-
return true
18
-
},
19
-
}
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")
20
19
21
-
func (s *Server) handleSyncSubscribeRepos(e echo.Context) error {
22
20
conn, err := websocket.Upgrade(e.Response().Writer, e.Request(), e.Response().Header(), 1<<10, 1<<10)
23
21
if err != nil {
22
+
logger.Error("unable to establish websocket with relay", "err", err)
24
23
return err
25
24
}
26
25
27
-
s.logger.Info("new connection", "ua", e.Request().UserAgent())
26
+
ident := e.RealIP() + "-" + e.Request().UserAgent()
27
+
logger = logger.With("ident", ident)
28
+
logger.Info("new connection established")
28
29
29
-
ctx := e.Request().Context()
30
+
metrics.RelaysConnected.WithLabelValues(ident).Inc()
31
+
defer func() {
32
+
metrics.RelaysConnected.WithLabelValues(ident).Dec()
33
+
}()
30
34
31
-
ident := e.RealIP() + "-" + e.Request().UserAgent()
32
-
33
-
evts, cancel, err := s.evtman.Subscribe(ctx, ident, func(evt *events.XRPCStreamEvent) bool {
35
+
evts, evtManCancel, err := s.evtman.Subscribe(ctx, ident, func(evt *events.XRPCStreamEvent) bool {
34
36
return true
35
37
}, nil)
36
38
if err != nil {
37
39
return err
38
40
}
39
-
defer cancel()
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
+
}()
40
58
41
59
header := events.EventHeader{Op: events.EvtKindMessage}
42
60
for evt := range evts {
43
-
wc, err := conn.NextWriter(websocket.BinaryMessage)
44
-
if err != nil {
45
-
return err
46
-
}
61
+
func() {
62
+
defer func() {
63
+
metrics.RelaySends.WithLabelValues(ident, header.MsgType).Inc()
64
+
}()
47
65
48
-
var obj util.CBOR
66
+
wc, err := conn.NextWriter(websocket.BinaryMessage)
67
+
if err != nil {
68
+
logger.Error("error writing message to relay", "err", err)
69
+
return
70
+
}
49
71
50
-
switch {
51
-
case evt.Error != nil:
52
-
header.Op = events.EvtKindErrorFrame
53
-
obj = evt.Error
54
-
case evt.RepoCommit != nil:
55
-
header.MsgType = "#commit"
56
-
obj = evt.RepoCommit
57
-
case evt.RepoHandle != nil:
58
-
header.MsgType = "#handle"
59
-
obj = evt.RepoHandle
60
-
case evt.RepoIdentity != nil:
61
-
header.MsgType = "#identity"
62
-
obj = evt.RepoIdentity
63
-
case evt.RepoAccount != nil:
64
-
header.MsgType = "#account"
65
-
obj = evt.RepoAccount
66
-
case evt.RepoInfo != nil:
67
-
header.MsgType = "#info"
68
-
obj = evt.RepoInfo
69
-
case evt.RepoMigrate != nil:
70
-
header.MsgType = "#migrate"
71
-
obj = evt.RepoMigrate
72
-
case evt.RepoTombstone != nil:
73
-
header.MsgType = "#tombstone"
74
-
obj = evt.RepoTombstone
75
-
default:
76
-
return fmt.Errorf("unrecognized event kind")
77
-
}
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
+
}
78
103
79
-
if err := header.MarshalCBOR(wc); err != nil {
80
-
return fmt.Errorf("failed to write header: %w", err)
81
-
}
104
+
if err := obj.MarshalCBOR(wc); err != nil {
105
+
logger.Error("failed to write event to relay", "err", err)
106
+
return
107
+
}
82
108
83
-
if err := obj.MarshalCBOR(wc); err != nil {
84
-
return fmt.Errorf("failed to write event: %w", err)
85
-
}
109
+
if err := wc.Close(); err != nil {
110
+
logger.Error("failed to flush-close our event write", "err", err)
111
+
return
112
+
}
113
+
}()
114
+
}
86
115
87
-
if err := wc.Close(); err != nil {
88
-
return fmt.Errorf("failed to flush-close our event write: %w", err)
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)
89
123
}
90
-
}
124
+
}()
91
125
92
126
return nil
93
127
}
+36
server/handle_well_known.go
+36
server/handle_well_known.go
···
2
2
3
3
import (
4
4
"fmt"
5
+
"strings"
5
6
6
7
"github.com/Azure/go-autorest/autorest/to"
8
+
"github.com/haileyok/cocoon/internal/helpers"
7
9
"github.com/labstack/echo/v4"
10
+
"gorm.io/gorm"
8
11
)
9
12
10
13
var (
···
61
64
},
62
65
},
63
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)
64
100
}
65
101
66
102
func (s *Server) handleOauthProtectedResource(e echo.Context) error {
+38
server/mail.go
+38
server/mail.go
···
40
40
return nil
41
41
}
42
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
+
43
62
func (s *Server) sendEmailUpdate(email, handle, code string) error {
44
63
if s.mail == nil {
45
64
return nil
···
77
96
78
97
return nil
79
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
+
}
+58
-23
server/middleware.go
+58
-23
server/middleware.go
···
3
3
import (
4
4
"crypto/sha256"
5
5
"encoding/base64"
6
+
"errors"
6
7
"fmt"
7
8
"strings"
8
9
"time"
···
11
12
"github.com/golang-jwt/jwt/v4"
12
13
"github.com/haileyok/cocoon/internal/helpers"
13
14
"github.com/haileyok/cocoon/models"
15
+
"github.com/haileyok/cocoon/oauth/dpop"
14
16
"github.com/haileyok/cocoon/oauth/provider"
15
17
"github.com/labstack/echo/v4"
16
18
"gitlab.com/yawning/secp256k1-voi"
···
35
37
36
38
func (s *Server) handleLegacySessionMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
37
39
return func(e echo.Context) error {
40
+
ctx := e.Request().Context()
41
+
logger := s.logger.With("name", "handleLegacySessionMiddleware")
42
+
38
43
authheader := e.Request().Header.Get("authorization")
39
44
if authheader == "" {
40
45
return e.JSON(401, map[string]string{"error": "Unauthorized"})
···
65
70
if hasLxm {
66
71
pts := strings.Split(e.Request().URL.String(), "/")
67
72
if lxm != pts[len(pts)-1] {
68
-
s.logger.Error("service auth lxm incorrect", "lxm", lxm, "expected", pts[len(pts)-1], "error", err)
73
+
logger.Error("service auth lxm incorrect", "lxm", lxm, "expected", pts[len(pts)-1], "error", err)
69
74
return helpers.InputError(e, nil)
70
75
}
71
76
72
77
maybeDid, ok := claims["iss"].(string)
73
78
if !ok {
74
-
s.logger.Error("no iss in service auth token", "error", err)
79
+
logger.Error("no iss in service auth token", "error", err)
75
80
return helpers.InputError(e, nil)
76
81
}
77
82
did = maybeDid
78
83
79
-
maybeRepo, err := s.getRepoActorByDid(did)
84
+
maybeRepo, err := s.getRepoActorByDid(ctx, did)
80
85
if err != nil {
81
-
s.logger.Error("error fetching repo", "error", err)
86
+
logger.Error("error fetching repo", "error", err)
82
87
return helpers.ServerError(e, nil)
83
88
}
84
89
repo = maybeRepo
···
92
97
return s.privateKey.Public(), nil
93
98
})
94
99
if err != nil {
95
-
s.logger.Error("error parsing jwt", "error", err)
100
+
logger.Error("error parsing jwt", "error", err)
96
101
return helpers.ExpiredTokenError(e)
97
102
}
98
103
···
105
110
hash := sha256.Sum256([]byte(signingInput))
106
111
sigBytes, err := base64.RawURLEncoding.DecodeString(kpts[2])
107
112
if err != nil {
108
-
s.logger.Error("error decoding signature bytes", "error", err)
113
+
logger.Error("error decoding signature bytes", "error", err)
109
114
return helpers.ServerError(e, nil)
110
115
}
111
116
112
117
if len(sigBytes) != 64 {
113
-
s.logger.Error("incorrect sigbytes length", "length", len(sigBytes))
118
+
logger.Error("incorrect sigbytes length", "length", len(sigBytes))
114
119
return helpers.ServerError(e, nil)
115
120
}
116
121
···
119
124
rr, _ := secp256k1.NewScalarFromBytes((*[32]byte)(rBytes))
120
125
ss, _ := secp256k1.NewScalarFromBytes((*[32]byte)(sBytes))
121
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
+
122
142
sk, err := secp256k1secec.NewPrivateKey(repo.SigningKey)
123
143
if err != nil {
124
-
s.logger.Error("can't load private key", "error", err)
144
+
logger.Error("can't load private key", "error", err)
125
145
return err
126
146
}
127
147
128
148
pubKey, ok := sk.Public().(*secp256k1secec.PublicKey)
129
149
if !ok {
130
-
s.logger.Error("error getting public key from sk")
150
+
logger.Error("error getting public key from sk")
131
151
return helpers.ServerError(e, nil)
132
152
}
133
153
134
154
verified := pubKey.VerifyRaw(hash[:], rr, ss)
135
155
if !verified {
136
-
s.logger.Error("error verifying", "error", err)
156
+
logger.Error("error verifying", "error", err)
137
157
return helpers.ServerError(e, nil)
138
158
}
139
159
}
···
157
177
Found bool
158
178
}
159
179
var result Result
160
-
if err := s.db.Raw("SELECT EXISTS(SELECT 1 FROM "+table+" WHERE token = ?) AS found", nil, tokenstr).Scan(&result).Error; err != nil {
180
+
if err := s.db.Raw(ctx, "SELECT EXISTS(SELECT 1 FROM "+table+" WHERE token = ?) AS found", nil, tokenstr).Scan(&result).Error; err != nil {
161
181
if err == gorm.ErrRecordNotFound {
162
182
return helpers.InvalidTokenError(e)
163
183
}
164
184
165
-
s.logger.Error("error getting token from db", "error", err)
185
+
logger.Error("error getting token from db", "error", err)
166
186
return helpers.ServerError(e, nil)
167
187
}
168
188
···
173
193
174
194
exp, ok := claims["exp"].(float64)
175
195
if !ok {
176
-
s.logger.Error("error getting iat from token")
196
+
logger.Error("error getting iat from token")
177
197
return helpers.ServerError(e, nil)
178
198
}
179
199
···
182
202
}
183
203
184
204
if repo == nil {
185
-
maybeRepo, err := s.getRepoActorByDid(claims["sub"].(string))
205
+
maybeRepo, err := s.getRepoActorByDid(ctx, claims["sub"].(string))
186
206
if err != nil {
187
-
s.logger.Error("error fetching repo", "error", err)
207
+
logger.Error("error fetching repo", "error", err)
188
208
return helpers.ServerError(e, nil)
189
209
}
190
210
repo = maybeRepo
···
205
225
206
226
func (s *Server) handleOauthSessionMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
207
227
return func(e echo.Context) error {
228
+
ctx := e.Request().Context()
229
+
logger := s.logger.With("name", "handleOauthSessionMiddleware")
230
+
208
231
authheader := e.Request().Header.Get("authorization")
209
232
if authheader == "" {
210
233
return e.JSON(401, map[string]string{"error": "Unauthorized"})
···
229
252
230
253
proof, err := s.oauthProvider.DpopManager.CheckProof(e.Request().Method, "https://"+s.config.Hostname+e.Request().URL.String(), e.Request().Header, to.StringPtr(accessToken))
231
254
if err != nil {
232
-
s.logger.Error("invalid dpop proof", "error", err)
233
-
return helpers.InputError(e, to.StringPtr(err.Error()))
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)
234
264
}
235
265
236
266
var oauthToken provider.OauthToken
237
-
if err := s.db.Raw("SELECT * FROM oauth_tokens WHERE token = ?", nil, accessToken).Scan(&oauthToken).Error; err != nil {
238
-
s.logger.Error("error finding access token in db", "error", err)
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)
239
269
return helpers.InputError(e, nil)
240
270
}
241
271
···
244
274
}
245
275
246
276
if *oauthToken.Parameters.DpopJkt != proof.JKT {
247
-
s.logger.Error("jkt mismatch", "token", oauthToken.Parameters.DpopJkt, "proof", proof.JKT)
277
+
logger.Error("jkt mismatch", "token", oauthToken.Parameters.DpopJkt, "proof", proof.JKT)
248
278
return helpers.InputError(e, to.StringPtr("dpop jkt mismatch"))
249
279
}
250
280
251
281
if time.Now().After(oauthToken.ExpiresAt) {
252
-
return helpers.ExpiredTokenError(e)
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
+
})
253
288
}
254
289
255
-
repo, err := s.getRepoActorByDid(oauthToken.Sub)
290
+
repo, err := s.getRepoActorByDid(ctx, oauthToken.Sub)
256
291
if err != nil {
257
-
s.logger.Error("could not find actor in db", "error", err)
292
+
logger.Error("could not find actor in db", "error", err)
258
293
return helpers.ServerError(e, nil)
259
294
}
260
295
+98
-39
server/repo.go
+98
-39
server/repo.go
···
10
10
11
11
"github.com/Azure/go-autorest/autorest/to"
12
12
"github.com/bluesky-social/indigo/api/atproto"
13
-
"github.com/bluesky-social/indigo/atproto/data"
13
+
"github.com/bluesky-social/indigo/atproto/atdata"
14
14
"github.com/bluesky-social/indigo/atproto/syntax"
15
15
"github.com/bluesky-social/indigo/carstore"
16
16
"github.com/bluesky-social/indigo/events"
17
17
lexutil "github.com/bluesky-social/indigo/lex/util"
18
18
"github.com/bluesky-social/indigo/repo"
19
19
"github.com/haileyok/cocoon/internal/db"
20
+
"github.com/haileyok/cocoon/metrics"
20
21
"github.com/haileyok/cocoon/models"
21
22
"github.com/haileyok/cocoon/recording_blockstore"
22
23
blocks "github.com/ipfs/go-block-format"
···
72
73
}
73
74
74
75
func (mm *MarshalableMap) MarshalCBOR(w io.Writer) error {
75
-
data, err := data.MarshalCBOR(*mm)
76
+
data, err := atdata.MarshalCBOR(*mm)
76
77
if err != nil {
77
78
return err
78
79
}
···
96
97
}
97
98
98
99
// TODO make use of swap commit
99
-
func (rm *RepoMan) applyWrites(urepo models.Repo, writes []Op, swapCommit *string) ([]ApplyWriteResult, error) {
100
+
func (rm *RepoMan) applyWrites(ctx context.Context, urepo models.Repo, writes []Op, swapCommit *string) ([]ApplyWriteResult, error) {
100
101
rootcid, err := cid.Cast(urepo.Root)
101
102
if err != nil {
102
103
return nil, err
···
104
105
105
106
dbs := rm.s.getBlockstore(urepo.Did)
106
107
bs := recording_blockstore.New(dbs)
107
-
r, err := repo.OpenRepo(context.TODO(), dbs, rootcid)
108
+
r, err := repo.OpenRepo(ctx, bs, rootcid)
108
109
109
-
entries := []models.Record{}
110
110
var results []ApplyWriteResult
111
111
112
+
entries := make([]models.Record, 0, len(writes))
112
113
for i, op := range writes {
114
+
// updates or deletes must supply an rkey
113
115
if op.Type != OpTypeCreate && op.Rkey == nil {
114
116
return nil, fmt.Errorf("invalid rkey")
115
117
} else if op.Type == OpTypeCreate && op.Rkey != nil {
116
-
_, _, err := r.GetRecord(context.TODO(), op.Collection+"/"+*op.Rkey)
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))
117
120
if err == nil {
118
121
op.Type = OpTypeUpdate
119
122
}
120
123
} else if op.Rkey == nil {
124
+
// creates that don't supply an rkey will have one generated for them
121
125
op.Rkey = to.StringPtr(rm.clock.Next().String())
122
126
writes[i].Rkey = op.Rkey
123
127
}
124
128
129
+
// validate the record key is actually valid
125
130
_, err := syntax.ParseRecordKey(*op.Rkey)
126
131
if err != nil {
127
132
return nil, err
···
129
134
130
135
switch op.Type {
131
136
case OpTypeCreate:
132
-
j, err := json.Marshal(*op.Record)
137
+
// HACK: this fixes some type conversions, mainly around integers
138
+
// first we convert to json bytes
139
+
b, err := json.Marshal(*op.Record)
133
140
if err != nil {
134
141
return nil, err
135
142
}
136
-
out, err := data.UnmarshalJSON(j)
143
+
// then we use atdata.UnmarshalJSON to convert it back to a map
144
+
out, err := atdata.UnmarshalJSON(b)
137
145
if err != nil {
138
146
return nil, err
139
147
}
148
+
// finally we can cast to a MarshalableMap
140
149
mm := MarshalableMap(out)
141
-
nc, err := r.PutRecord(context.TODO(), op.Collection+"/"+*op.Rkey, &mm)
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)
142
158
if err != nil {
143
159
return nil, err
144
160
}
145
-
d, err := data.MarshalCBOR(mm)
161
+
162
+
d, err := atdata.MarshalCBOR(mm)
146
163
if err != nil {
147
164
return nil, err
148
165
}
166
+
149
167
entries = append(entries, models.Record{
150
168
Did: urepo.Did,
151
169
CreatedAt: rm.clock.Next().String(),
···
154
172
Cid: nc.String(),
155
173
Value: d,
156
174
})
175
+
157
176
results = append(results, ApplyWriteResult{
158
177
Type: to.StringPtr(OpTypeCreate.String()),
159
178
Uri: to.StringPtr("at://" + urepo.Did + "/" + op.Collection + "/" + *op.Rkey),
···
161
180
ValidationStatus: to.StringPtr("valid"), // TODO: obviously this might not be true atm lol
162
181
})
163
182
case OpTypeDelete:
183
+
// try to find the old record in the database
164
184
var old models.Record
165
-
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 {
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 {
166
186
return nil, err
167
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
168
192
entries = append(entries, models.Record{
169
193
Did: urepo.Did,
170
194
Nsid: op.Collection,
171
195
Rkey: *op.Rkey,
172
196
Value: old.Value,
173
197
})
174
-
err := r.DeleteRecord(context.TODO(), op.Collection+"/"+*op.Rkey)
198
+
199
+
// delete the record from the repo
200
+
err := r.DeleteRecord(ctx, fmt.Sprintf("%s/%s", op.Collection, *op.Rkey))
175
201
if err != nil {
176
202
return nil, err
177
203
}
204
+
205
+
// add a result for the delete
178
206
results = append(results, ApplyWriteResult{
179
207
Type: to.StringPtr(OpTypeDelete.String()),
180
208
})
181
209
case OpTypeUpdate:
182
-
j, err := json.Marshal(*op.Record)
210
+
// HACK: same hack as above for type fixes
211
+
b, err := json.Marshal(*op.Record)
183
212
if err != nil {
184
213
return nil, err
185
214
}
186
-
out, err := data.UnmarshalJSON(j)
215
+
out, err := atdata.UnmarshalJSON(b)
187
216
if err != nil {
188
217
return nil, err
189
218
}
190
219
mm := MarshalableMap(out)
191
-
nc, err := r.UpdateRecord(context.TODO(), op.Collection+"/"+*op.Rkey, &mm)
220
+
221
+
nc, err := r.UpdateRecord(ctx, fmt.Sprintf("%s/%s", op.Collection, *op.Rkey), &mm)
192
222
if err != nil {
193
223
return nil, err
194
224
}
195
-
d, err := data.MarshalCBOR(mm)
225
+
226
+
d, err := atdata.MarshalCBOR(mm)
196
227
if err != nil {
197
228
return nil, err
198
229
}
230
+
199
231
entries = append(entries, models.Record{
200
232
Did: urepo.Did,
201
233
CreatedAt: rm.clock.Next().String(),
···
204
236
Cid: nc.String(),
205
237
Value: d,
206
238
})
239
+
207
240
results = append(results, ApplyWriteResult{
208
241
Type: to.StringPtr(OpTypeUpdate.String()),
209
242
Uri: to.StringPtr("at://" + urepo.Did + "/" + op.Collection + "/" + *op.Rkey),
···
213
246
}
214
247
}
215
248
216
-
newroot, rev, err := r.Commit(context.TODO(), urepo.SignFor)
249
+
// commit and get the new root
250
+
newroot, rev, err := r.Commit(ctx, urepo.SignFor)
217
251
if err != nil {
218
252
return nil, err
219
253
}
220
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
221
262
buf := new(bytes.Buffer)
222
263
264
+
// first write the car header to the buffer
223
265
hb, err := cbor.DumpObject(&car.CarHeader{
224
266
Roots: []cid.Cid{newroot},
225
267
Version: 1,
226
268
})
227
-
228
269
if _, err := carstore.LdWrite(buf, hb); err != nil {
229
270
return nil, err
230
271
}
231
272
232
-
diffops, err := r.DiffSince(context.TODO(), rootcid)
273
+
// get a diff of the changes to the repo
274
+
diffops, err := r.DiffSince(ctx, rootcid)
233
275
if err != nil {
234
276
return nil, err
235
277
}
236
278
279
+
// create the repo ops for the given diff
237
280
ops := make([]*atproto.SyncSubscribeRepos_RepoOp, 0, len(diffops))
238
-
239
281
for _, op := range diffops {
240
282
var c cid.Cid
241
283
switch op.Op {
···
264
306
})
265
307
}
266
308
267
-
blk, err := dbs.Get(context.TODO(), c)
309
+
blk, err := dbs.Get(ctx, c)
268
310
if err != nil {
269
311
return nil, err
270
312
}
271
313
314
+
// write the block to the buffer
272
315
if _, err := carstore.LdWrite(buf, blk.Cid().Bytes(), blk.RawData()); err != nil {
273
316
return nil, err
274
317
}
275
318
}
276
319
277
-
for _, op := range bs.GetLogMap() {
320
+
// write the writelog to the buffer
321
+
for _, op := range bs.GetWriteLog() {
278
322
if _, err := carstore.LdWrite(buf, op.Cid().Bytes(), op.RawData()); err != nil {
279
323
return nil, err
280
324
}
281
325
}
282
326
327
+
// blob blob blob blob blob :3
283
328
var blobs []lexutil.LexLink
284
329
for _, entry := range entries {
285
330
var cids []cid.Cid
331
+
// whenever there is cid present, we know it's a create (dumb)
286
332
if entry.Cid != "" {
287
-
if err := rm.s.db.Create(&entry, []clause.Expression{clause.OnConflict{
333
+
if err := rm.s.db.Create(ctx, &entry, []clause.Expression{clause.OnConflict{
288
334
Columns: []clause.Column{{Name: "did"}, {Name: "nsid"}, {Name: "rkey"}},
289
335
UpdateAll: true,
290
336
}}).Error; err != nil {
291
337
return nil, err
292
338
}
293
339
294
-
cids, err = rm.incrementBlobRefs(urepo, entry.Value)
340
+
// increment the given blob refs, yay
341
+
cids, err = rm.incrementBlobRefs(ctx, urepo, entry.Value)
295
342
if err != nil {
296
343
return nil, err
297
344
}
298
345
} else {
299
-
if err := rm.s.db.Delete(&entry, nil).Error; err != nil {
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 {
300
350
return nil, err
301
351
}
302
-
cids, err = rm.decrementBlobRefs(urepo, entry.Value)
352
+
353
+
// TODO:
354
+
cids, err = rm.decrementBlobRefs(ctx, urepo, entry.Value)
303
355
if err != nil {
304
356
return nil, err
305
357
}
306
358
}
307
359
360
+
// add all the relevant blobs to the blobs list of blobs. blob ^.^
308
361
for _, c := range cids {
309
362
blobs = append(blobs, lexutil.LexLink(c))
310
363
}
311
364
}
312
365
313
-
rm.s.evtman.AddEvent(context.TODO(), &events.XRPCStreamEvent{
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{
314
369
RepoCommit: &atproto.SyncSubscribeRepos_Commit{
315
370
Repo: urepo.Did,
316
371
Blocks: buf.Bytes(),
···
324
379
},
325
380
})
326
381
327
-
if err := rm.s.UpdateRepo(context.TODO(), urepo.Did, newroot, rev); err != nil {
382
+
if err := rm.s.UpdateRepo(ctx, urepo.Did, newroot, rev); err != nil {
328
383
return nil, err
329
384
}
330
385
···
339
394
return results, nil
340
395
}
341
396
342
-
func (rm *RepoMan) getRecordProof(urepo models.Repo, collection, rkey string) (cid.Cid, []blocks.Block, error) {
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) {
343
400
c, err := cid.Cast(urepo.Root)
344
401
if err != nil {
345
402
return cid.Undef, nil, err
···
348
405
dbs := rm.s.getBlockstore(urepo.Did)
349
406
bs := recording_blockstore.New(dbs)
350
407
351
-
r, err := repo.OpenRepo(context.TODO(), bs, c)
408
+
r, err := repo.OpenRepo(ctx, bs, c)
352
409
if err != nil {
353
410
return cid.Undef, nil, err
354
411
}
355
412
356
-
_, _, err = r.GetRecordBytes(context.TODO(), collection+"/"+rkey)
413
+
_, _, err = r.GetRecordBytes(ctx, fmt.Sprintf("%s/%s", collection, rkey))
357
414
if err != nil {
358
415
return cid.Undef, nil, err
359
416
}
360
417
361
-
return c, bs.GetLogArray(), nil
418
+
return c, bs.GetReadLog(), nil
362
419
}
363
420
364
-
func (rm *RepoMan) incrementBlobRefs(urepo models.Repo, cbor []byte) ([]cid.Cid, error) {
421
+
func (rm *RepoMan) incrementBlobRefs(ctx context.Context, urepo models.Repo, cbor []byte) ([]cid.Cid, error) {
365
422
cids, err := getBlobCidsFromCbor(cbor)
366
423
if err != nil {
367
424
return nil, err
368
425
}
369
426
370
427
for _, c := range cids {
371
-
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 {
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 {
372
429
return nil, err
373
430
}
374
431
}
···
376
433
return cids, nil
377
434
}
378
435
379
-
func (rm *RepoMan) decrementBlobRefs(urepo models.Repo, cbor []byte) ([]cid.Cid, error) {
436
+
func (rm *RepoMan) decrementBlobRefs(ctx context.Context, urepo models.Repo, cbor []byte) ([]cid.Cid, error) {
380
437
cids, err := getBlobCidsFromCbor(cbor)
381
438
if err != nil {
382
439
return nil, err
···
387
444
ID uint
388
445
Count int
389
446
}
390
-
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 {
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 {
391
448
return nil, err
392
449
}
393
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!!!!
394
453
if res.Count == 0 {
395
-
if err := rm.db.Exec("DELETE FROM blobs WHERE id = ?", nil, res.ID).Error; err != nil {
454
+
if err := rm.db.Exec(ctx, "DELETE FROM blobs WHERE id = ?", nil, res.ID).Error; err != nil {
396
455
return nil, err
397
456
}
398
-
if err := rm.db.Exec("DELETE FROM blob_parts WHERE blob_id = ?", nil, res.ID).Error; err != nil {
457
+
if err := rm.db.Exec(ctx, "DELETE FROM blob_parts WHERE blob_id = ?", nil, res.ID).Error; err != nil {
399
458
return nil, err
400
459
}
401
460
}
···
409
468
func getBlobCidsFromCbor(cbor []byte) ([]cid.Cid, error) {
410
469
var cids []cid.Cid
411
470
412
-
decoded, err := data.UnmarshalCBOR(cbor)
471
+
decoded, err := atdata.UnmarshalCBOR(cbor)
413
472
if err != nil {
414
473
return nil, fmt.Errorf("error unmarshaling cbor: %w", err)
415
474
}
+155
-63
server/server.go
+155
-63
server/server.go
···
39
39
"github.com/haileyok/cocoon/oauth/provider"
40
40
"github.com/haileyok/cocoon/plc"
41
41
"github.com/ipfs/go-cid"
42
+
"github.com/labstack/echo-contrib/echoprometheus"
42
43
echo_session "github.com/labstack/echo-contrib/session"
43
44
"github.com/labstack/echo/v4"
44
45
"github.com/labstack/echo/v4/middleware"
45
46
slogecho "github.com/samber/slog-echo"
47
+
"gorm.io/driver/postgres"
46
48
"gorm.io/driver/sqlite"
47
49
"gorm.io/gorm"
48
50
)
···
52
54
)
53
55
54
56
type S3Config struct {
55
-
BackupsEnabled bool
56
-
Endpoint string
57
-
Region string
58
-
Bucket string
59
-
AccessKey string
60
-
SecretKey string
57
+
BackupsEnabled bool
58
+
BlobstoreEnabled bool
59
+
Endpoint string
60
+
Region string
61
+
Bucket string
62
+
AccessKey string
63
+
SecretKey string
64
+
CDNUrl string
61
65
}
62
66
63
67
type Server struct {
···
75
79
oauthProvider *provider.Provider
76
80
evtman *events.EventManager
77
81
passport *identity.Passport
82
+
fallbackProxy string
83
+
84
+
lastRequestCrawl time.Time
85
+
requestCrawlMu sync.Mutex
78
86
79
87
dbName string
88
+
dbType string
80
89
s3Config *S3Config
81
90
}
82
91
83
92
type Args struct {
93
+
Logger *slog.Logger
94
+
84
95
Addr string
85
96
DbName string
86
-
Logger *slog.Logger
97
+
DbType string
98
+
DatabaseURL string
87
99
Version string
88
100
Did string
89
101
Hostname string
···
92
104
ContactEmail string
93
105
Relays []string
94
106
AdminPassword string
107
+
RequireInvite bool
95
108
96
109
SmtpUser string
97
110
SmtpPass string
···
104
117
105
118
SessionSecret string
106
119
107
-
DefaultAtprotoProxy string
108
-
109
120
BlockstoreVariant BlockstoreVariant
121
+
FallbackProxy string
110
122
}
111
123
112
124
type config struct {
113
-
Version string
114
-
Did string
115
-
Hostname string
116
-
ContactEmail string
117
-
EnforcePeering bool
118
-
Relays []string
119
-
AdminPassword string
120
-
SmtpEmail string
121
-
SmtpName string
122
-
DefaultAtprotoProxy string
123
-
BlockstoreVariant BlockstoreVariant
125
+
Version string
126
+
Did string
127
+
Hostname string
128
+
ContactEmail string
129
+
EnforcePeering bool
130
+
Relays []string
131
+
AdminPassword string
132
+
RequireInvite bool
133
+
SmtpEmail string
134
+
SmtpName string
135
+
BlockstoreVariant BlockstoreVariant
136
+
FallbackProxy string
124
137
}
125
138
126
139
type CustomValidator struct {
···
198
211
}
199
212
200
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
+
201
220
if args.Addr == "" {
202
221
return nil, fmt.Errorf("addr must be set")
203
222
}
···
226
245
return nil, fmt.Errorf("admin password must be set")
227
246
}
228
247
229
-
if args.Logger == nil {
230
-
args.Logger = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{}))
231
-
}
232
-
233
248
if args.SessionSecret == "" {
234
249
panic("SESSION SECRET WAS NOT SET. THIS IS REQUIRED. ")
235
250
}
···
237
252
e := echo.New()
238
253
239
254
e.Pre(middleware.RemoveTrailingSlash())
240
-
e.Pre(slogecho.New(args.Logger))
255
+
e.Pre(slogecho.New(args.Logger.With("component", "slogecho")))
241
256
e.Use(echo_session.Middleware(sessions.NewCookieStore([]byte(args.SessionSecret))))
257
+
e.Use(echoprometheus.NewMiddleware("cocoon"))
242
258
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
243
259
AllowOrigins: []string{"*"},
244
260
AllowHeaders: []string{"*"},
···
284
300
IdleTimeout: 5 * time.Minute,
285
301
}
286
302
287
-
gdb, err := gorm.Open(sqlite.Open("cocoon.db"), &gorm.Config{})
288
-
if err != nil {
289
-
return nil, err
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)
290
326
}
291
327
dbw := db.NewDB(gdb)
292
328
···
329
365
var nonceSecret []byte
330
366
maybeSecret, err := os.ReadFile("nonce.secret")
331
367
if err != nil && !os.IsNotExist(err) {
332
-
args.Logger.Error("error attempting to read nonce secret", "error", err)
368
+
logger.Error("error attempting to read nonce secret", "error", err)
333
369
} else {
334
370
nonceSecret = maybeSecret
335
371
}
···
343
379
plcClient: plcClient,
344
380
privateKey: &pkey,
345
381
config: &config{
346
-
Version: args.Version,
347
-
Did: args.Did,
348
-
Hostname: args.Hostname,
349
-
ContactEmail: args.ContactEmail,
350
-
EnforcePeering: false,
351
-
Relays: args.Relays,
352
-
AdminPassword: args.AdminPassword,
353
-
SmtpName: args.SmtpName,
354
-
SmtpEmail: args.SmtpEmail,
355
-
DefaultAtprotoProxy: args.DefaultAtprotoProxy,
356
-
BlockstoreVariant: args.BlockstoreVariant,
382
+
Version: args.Version,
383
+
Did: args.Did,
384
+
Hostname: args.Hostname,
385
+
ContactEmail: args.ContactEmail,
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,
393
+
FallbackProxy: args.FallbackProxy,
357
394
},
358
395
evtman: events.NewEventManager(events.NewMemPersister()),
359
396
passport: identity.NewPassport(h, identity.NewMemCache(10_000)),
360
397
361
398
dbName: args.DbName,
399
+
dbType: dbType,
362
400
s3Config: args.S3Config,
363
401
364
402
oauthProvider: provider.NewProvider(provider.Args{
365
403
Hostname: args.Hostname,
366
404
ClientManagerArgs: client.ManagerArgs{
367
405
Cli: oauthCli,
368
-
Logger: args.Logger,
406
+
Logger: args.Logger.With("component", "oauth-client-manager"),
369
407
},
370
408
DpopManagerArgs: dpop.ManagerArgs{
371
409
NonceSecret: nonceSecret,
372
410
NonceRotationInterval: constants.NonceMaxRotationInterval / 3,
373
411
OnNonceSecretCreated: func(newNonce []byte) {
374
412
if err := os.WriteFile("nonce.secret", newNonce, 0644); err != nil {
375
-
args.Logger.Error("error writing new nonce secret", "error", err)
413
+
logger.Error("error writing new nonce secret", "error", err)
376
414
}
377
415
},
378
-
Logger: args.Logger,
416
+
Logger: args.Logger.With("component", "dpop-manager"),
379
417
Hostname: args.Hostname,
380
418
},
381
419
}),
···
387
425
388
426
// TODO: should validate these args
389
427
if args.SmtpUser == "" || args.SmtpPass == "" || args.SmtpHost == "" || args.SmtpPort == "" || args.SmtpEmail == "" || args.SmtpName == "" {
390
-
args.Logger.Warn("not enough smpt args were provided. mailing will not work for your server.")
428
+
args.Logger.Warn("not enough smtp args were provided. mailing will not work for your server.")
391
429
} else {
392
430
mail := mailyak.New(args.SmtpHost+":"+args.SmtpPort, smtp.PlainAuth("", args.SmtpUser, args.SmtpPass, args.SmtpHost))
393
431
mail.From(s.config.SmtpEmail)
···
412
450
s.echo.GET("/", s.handleRoot)
413
451
s.echo.GET("/xrpc/_health", s.handleHealth)
414
452
s.echo.GET("/.well-known/did.json", s.handleWellKnown)
453
+
s.echo.GET("/.well-known/atproto-did", s.handleAtprotoDid)
415
454
s.echo.GET("/.well-known/oauth-protected-resource", s.handleOauthProtectedResource)
416
455
s.echo.GET("/.well-known/oauth-authorization-server", s.handleOauthAuthorizationServer)
417
456
s.echo.GET("/robots.txt", s.handleRobots)
···
419
458
// public
420
459
s.echo.GET("/xrpc/com.atproto.identity.resolveHandle", s.handleResolveHandle)
421
460
s.echo.POST("/xrpc/com.atproto.server.createAccount", s.handleCreateAccount)
422
-
s.echo.POST("/xrpc/com.atproto.server.createAccount", s.handleCreateAccount)
423
461
s.echo.POST("/xrpc/com.atproto.server.createSession", s.handleCreateSession)
424
462
s.echo.GET("/xrpc/com.atproto.server.describeServer", s.handleDescribeServer)
463
+
s.echo.POST("/xrpc/com.atproto.server.reserveSigningKey", s.handleServerReserveSigningKey)
425
464
426
465
s.echo.GET("/xrpc/com.atproto.repo.describeRepo", s.handleDescribeRepo)
427
466
s.echo.GET("/xrpc/com.atproto.sync.listRepos", s.handleListRepos)
···
436
475
s.echo.GET("/xrpc/com.atproto.sync.listBlobs", s.handleSyncListBlobs)
437
476
s.echo.GET("/xrpc/com.atproto.sync.getBlob", s.handleSyncGetBlob)
438
477
478
+
// labels
479
+
s.echo.GET("/xrpc/com.atproto.label.queryLabels", s.handleLabelQueryLabels)
480
+
439
481
// account
440
482
s.echo.GET("/account", s.handleAccount)
441
483
s.echo.POST("/account/revoke", s.handleAccountRevoke)
···
456
498
s.echo.GET("/xrpc/com.atproto.server.getSession", s.handleGetSession, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
457
499
s.echo.POST("/xrpc/com.atproto.server.refreshSession", s.handleRefreshSession, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
458
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)
459
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)
460
506
s.echo.POST("/xrpc/com.atproto.server.confirmEmail", s.handleServerConfirmEmail, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
461
507
s.echo.POST("/xrpc/com.atproto.server.requestEmailConfirmation", s.handleServerRequestEmailConfirmation, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
462
508
s.echo.POST("/xrpc/com.atproto.server.requestPasswordReset", s.handleServerRequestPasswordReset) // AUTH NOT REQUIRED FOR THIS ONE
···
465
511
s.echo.POST("/xrpc/com.atproto.server.updateEmail", s.handleServerUpdateEmail, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
466
512
s.echo.GET("/xrpc/com.atproto.server.getServiceAuth", s.handleServerGetServiceAuth, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
467
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)
468
518
469
519
// repo
520
+
s.echo.GET("/xrpc/com.atproto.repo.listMissingBlobs", s.handleListMissingBlobs, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
470
521
s.echo.POST("/xrpc/com.atproto.repo.createRecord", s.handleCreateRecord, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
471
522
s.echo.POST("/xrpc/com.atproto.repo.putRecord", s.handlePutRecord, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
472
523
s.echo.POST("/xrpc/com.atproto.repo.deleteRecord", s.handleDeleteRecord, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
···
477
528
// stupid silly endpoints
478
529
s.echo.GET("/xrpc/app.bsky.actor.getPreferences", s.handleActorGetPreferences, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
479
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)
480
532
481
533
// admin routes
482
534
s.echo.POST("/xrpc/com.atproto.server.createInviteCode", s.handleCreateInviteCode, s.handleAdminMiddleware)
···
488
540
}
489
541
490
542
func (s *Server) Serve(ctx context.Context) error {
543
+
logger := s.logger.With("name", "Serve")
544
+
491
545
s.addRoutes()
492
546
493
-
s.logger.Info("migrating...")
547
+
logger.Info("migrating...")
494
548
495
549
s.db.AutoMigrate(
496
550
&models.Actor{},
···
502
556
&models.Record{},
503
557
&models.Blob{},
504
558
&models.BlobPart{},
559
+
&models.ReservedKey{},
505
560
&provider.OauthToken{},
506
561
&provider.OauthAuthorizationRequest{},
507
562
)
508
563
509
-
s.logger.Info("starting cocoon")
564
+
logger.Info("starting cocoon")
510
565
511
566
go func() {
512
567
if err := s.httpd.ListenAndServe(); err != nil {
···
516
571
517
572
go s.backupRoutine()
518
573
574
+
go func() {
575
+
if err := s.requestCrawl(ctx); err != nil {
576
+
logger.Error("error requesting crawls", "err", err)
577
+
}
578
+
}()
579
+
580
+
<-ctx.Done()
581
+
582
+
fmt.Println("shut down")
583
+
584
+
return nil
585
+
}
586
+
587
+
func (s *Server) requestCrawl(ctx context.Context) error {
588
+
logger := s.logger.With("component", "request-crawl")
589
+
s.requestCrawlMu.Lock()
590
+
defer s.requestCrawlMu.Unlock()
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
+
519
598
for _, relay := range s.config.Relays {
599
+
logger := logger.With("relay", relay)
600
+
logger.Info("requesting crawl from relay")
520
601
cli := xrpc.Client{Host: relay}
521
-
atproto.SyncRequestCrawl(ctx, &cli, &atproto.SyncRequestCrawl_Input{
602
+
if err := atproto.SyncRequestCrawl(ctx, &cli, &atproto.SyncRequestCrawl_Input{
522
603
Hostname: s.config.Hostname,
523
-
})
604
+
}); err != nil {
605
+
logger.Error("error requesting crawl", "err", err)
606
+
} else {
607
+
logger.Info("crawl requested successfully")
608
+
}
524
609
}
525
610
526
-
<-ctx.Done()
527
-
528
-
fmt.Println("shut down")
611
+
s.lastRequestCrawl = time.Now()
529
612
530
613
return nil
531
614
}
532
615
533
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
+
534
624
start := time.Now()
535
625
536
-
s.logger.Info("beginning backup to s3...")
626
+
logger.Info("beginning backup to s3...")
537
627
538
628
var buf bytes.Buffer
539
629
if err := func() error {
540
-
s.logger.Info("reading database bytes...")
630
+
logger.Info("reading database bytes...")
541
631
s.db.Lock()
542
632
defer s.db.Unlock()
543
633
···
553
643
554
644
return nil
555
645
}(); err != nil {
556
-
s.logger.Error("error backing up database", "error", err)
646
+
logger.Error("error backing up database", "error", err)
557
647
return
558
648
}
559
649
560
650
if err := func() error {
561
-
s.logger.Info("sending to s3...")
651
+
logger.Info("sending to s3...")
562
652
563
653
currTime := time.Now().Format("2006-01-02_15-04-05")
564
654
key := "cocoon-backup-" + currTime + ".db"
···
588
678
return fmt.Errorf("error uploading file to s3: %w", err)
589
679
}
590
680
591
-
s.logger.Info("finished uploading backup to s3", "key", key, "duration", time.Now().Sub(start).Seconds())
681
+
logger.Info("finished uploading backup to s3", "key", key, "duration", time.Now().Sub(start).Seconds())
592
682
593
683
return nil
594
684
}(); err != nil {
595
-
s.logger.Error("error uploading database backup", "error", err)
685
+
logger.Error("error uploading database backup", "error", err)
596
686
return
597
687
}
598
688
···
600
690
}
601
691
602
692
func (s *Server) backupRoutine() {
693
+
logger := s.logger.With("name", "backupRoutine")
694
+
603
695
if s.s3Config == nil || !s.s3Config.BackupsEnabled {
604
696
return
605
697
}
606
698
607
699
if s.s3Config.Region == "" {
608
-
s.logger.Warn("no s3 region configured but backups are enabled. backups will not run.")
700
+
logger.Warn("no s3 region configured but backups are enabled. backups will not run.")
609
701
return
610
702
}
611
703
612
704
if s.s3Config.Bucket == "" {
613
-
s.logger.Warn("no s3 bucket configured but backups are enabled. backups will not run.")
705
+
logger.Warn("no s3 bucket configured but backups are enabled. backups will not run.")
614
706
return
615
707
}
616
708
617
709
if s.s3Config.AccessKey == "" {
618
-
s.logger.Warn("no s3 access key configured but backups are enabled. backups will not run.")
710
+
logger.Warn("no s3 access key configured but backups are enabled. backups will not run.")
619
711
return
620
712
}
621
713
622
714
if s.s3Config.SecretKey == "" {
623
-
s.logger.Warn("no s3 secret key configured but backups are enabled. backups will not run.")
715
+
logger.Warn("no s3 secret key configured but backups are enabled. backups will not run.")
624
716
return
625
717
}
626
718
···
648
740
}
649
741
650
742
func (s *Server) UpdateRepo(ctx context.Context, did string, root cid.Cid, rev string) error {
651
-
if err := s.db.Exec("UPDATE repos SET root = ?, rev = ? WHERE did = ?", nil, root.Bytes(), rev, did).Error; err != nil {
743
+
if err := s.db.Exec(ctx, "UPDATE repos SET root = ?, rev = ? WHERE did = ?", nil, root.Bytes(), rev, did).Error; err != nil {
652
744
return err
653
745
}
654
746
+91
server/service_auth.go
+91
server/service_auth.go
···
1
+
package server
2
+
3
+
import (
4
+
"context"
5
+
"fmt"
6
+
"strings"
7
+
8
+
"github.com/bluesky-social/indigo/atproto/atcrypto"
9
+
"github.com/bluesky-social/indigo/atproto/identity"
10
+
atproto_identity "github.com/bluesky-social/indigo/atproto/identity"
11
+
"github.com/bluesky-social/indigo/atproto/syntax"
12
+
"github.com/golang-jwt/jwt/v4"
13
+
)
14
+
15
+
type ES256KSigningMethod struct {
16
+
alg string
17
+
}
18
+
19
+
func (m *ES256KSigningMethod) Alg() string {
20
+
return m.alg
21
+
}
22
+
23
+
func (m *ES256KSigningMethod) Verify(signingString string, signature string, key interface{}) error {
24
+
signatureBytes, err := jwt.DecodeSegment(signature)
25
+
if err != nil {
26
+
return err
27
+
}
28
+
return key.(atcrypto.PublicKey).HashAndVerifyLenient([]byte(signingString), signatureBytes)
29
+
}
30
+
31
+
func (m *ES256KSigningMethod) Sign(signingString string, key interface{}) (string, error) {
32
+
return "", fmt.Errorf("unimplemented")
33
+
}
34
+
35
+
func init() {
36
+
ES256K := ES256KSigningMethod{alg: "ES256K"}
37
+
jwt.RegisterSigningMethod(ES256K.Alg(), func() jwt.SigningMethod {
38
+
return &ES256K
39
+
})
40
+
}
41
+
42
+
func (s *Server) validateServiceAuth(ctx context.Context, rawToken string, nsid string) (string, error) {
43
+
token := strings.TrimSpace(rawToken)
44
+
45
+
parsedToken, err := jwt.ParseWithClaims(token, jwt.MapClaims{}, func(token *jwt.Token) (interface{}, error) {
46
+
did := syntax.DID(token.Claims.(jwt.MapClaims)["iss"].(string))
47
+
didDoc, err := s.passport.FetchDoc(ctx, did.String());
48
+
if err != nil {
49
+
return nil, fmt.Errorf("unable to resolve did %s: %s", did, err)
50
+
}
51
+
52
+
verificationMethods := make([]atproto_identity.DocVerificationMethod, len(didDoc.VerificationMethods))
53
+
for i, verificationMethod := range didDoc.VerificationMethods {
54
+
verificationMethods[i] = atproto_identity.DocVerificationMethod{
55
+
ID: verificationMethod.Id,
56
+
Type: verificationMethod.Type,
57
+
PublicKeyMultibase: verificationMethod.PublicKeyMultibase,
58
+
Controller: verificationMethod.Controller,
59
+
}
60
+
}
61
+
services := make([]atproto_identity.DocService, len(didDoc.Service))
62
+
for i, service := range didDoc.Service {
63
+
services[i] = atproto_identity.DocService{
64
+
ID: service.Id,
65
+
Type: service.Type,
66
+
ServiceEndpoint: service.ServiceEndpoint,
67
+
}
68
+
}
69
+
parsedIdentity := atproto_identity.ParseIdentity(&identity.DIDDocument{
70
+
DID: did,
71
+
AlsoKnownAs: didDoc.AlsoKnownAs,
72
+
VerificationMethod: verificationMethods,
73
+
Service: services,
74
+
})
75
+
76
+
key, err := parsedIdentity.PublicKey()
77
+
if err != nil {
78
+
return nil, fmt.Errorf("signing key not found for did %s: %s", did, err)
79
+
}
80
+
return key, nil
81
+
})
82
+
if err != nil {
83
+
return "", fmt.Errorf("invalid token: %s", err)
84
+
}
85
+
86
+
claims := parsedToken.Claims.(jwt.MapClaims)
87
+
if claims["lxm"] != nsid {
88
+
return "", fmt.Errorf("bad jwt lexicon method (\"lxm\"). must match: %s", nsid)
89
+
}
90
+
return claims["iss"].(string), nil
91
+
}
+4
-3
server/session.go
+4
-3
server/session.go
···
1
1
package server
2
2
3
3
import (
4
+
"context"
4
5
"time"
5
6
6
7
"github.com/golang-jwt/jwt/v4"
···
13
14
RefreshToken string
14
15
}
15
16
16
-
func (s *Server) createSession(repo *models.Repo) (*Session, error) {
17
+
func (s *Server) createSession(ctx context.Context, repo *models.Repo) (*Session, error) {
17
18
now := time.Now()
18
19
accexp := now.Add(3 * time.Hour)
19
20
refexp := now.Add(7 * 24 * time.Hour)
···
49
50
return nil, err
50
51
}
51
52
52
-
if err := s.db.Create(&models.Token{
53
+
if err := s.db.Create(ctx, &models.Token{
53
54
Token: accessString,
54
55
Did: repo.Did,
55
56
RefreshToken: refreshString,
···
59
60
return nil, err
60
61
}
61
62
62
-
if err := s.db.Create(&models.RefreshToken{
63
+
if err := s.db.Create(ctx, &models.RefreshToken{
63
64
Token: refreshString,
64
65
Did: repo.Did,
65
66
CreatedAt: now,
+4
server/templates/signin.html
+4
server/templates/signin.html
···
26
26
type="password"
27
27
placeholder="Password"
28
28
/>
29
+
{{ if .flashes.tokenrequired }}
30
+
<br />
31
+
<input name="token" id="token" placeholder="Enter your 2FA token" />
32
+
{{ end }}
29
33
<input name="query_params" type="hidden" value="{{ .QueryParams }}" />
30
34
<button class="primary" type="submit" value="Login">Login</button>
31
35
</form>
+4
-22
sqlite_blockstore/sqlite_blockstore.go
+4
-22
sqlite_blockstore/sqlite_blockstore.go
···
45
45
return maybeBlock, nil
46
46
}
47
47
48
-
if err := bs.db.Raw("SELECT * FROM blocks WHERE did = ? AND cid = ?", nil, bs.did, cid.Bytes()).Scan(&block).Error; err != nil {
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
49
return nil, err
50
50
}
51
51
···
71
71
Value: block.RawData(),
72
72
}
73
73
74
-
if err := bs.db.Create(&b, []clause.Expression{clause.OnConflict{
74
+
if err := bs.db.Create(ctx, &b, []clause.Expression{clause.OnConflict{
75
75
Columns: []clause.Column{{Name: "did"}, {Name: "cid"}},
76
76
UpdateAll: true,
77
77
}}).Error; err != nil {
···
94
94
}
95
95
96
96
func (bs *SqliteBlockstore) PutMany(ctx context.Context, blocks []blocks.Block) error {
97
-
tx := bs.db.BeginDangerously()
97
+
tx := bs.db.BeginDangerously(ctx)
98
98
99
99
for _, block := range blocks {
100
100
bs.inserts[block.Cid()] = block
···
129
129
}
130
130
131
131
func (bs *SqliteBlockstore) AllKeysChan(ctx context.Context) (<-chan cid.Cid, error) {
132
-
panic("not implemented")
132
+
return nil, fmt.Errorf("iteration not allowed on sqlite blockstore")
133
133
}
134
134
135
135
func (bs *SqliteBlockstore) HashOnRead(enabled bool) {
136
136
panic("not implemented")
137
137
}
138
-
139
-
func (bs *SqliteBlockstore) Execute(ctx context.Context) error {
140
-
if !bs.readonly {
141
-
return fmt.Errorf("blockstore was not readonly")
142
-
}
143
-
144
-
bs.readonly = false
145
-
for _, b := range bs.inserts {
146
-
bs.Put(ctx, b)
147
-
}
148
-
bs.readonly = true
149
-
150
-
return nil
151
-
}
152
-
153
-
func (bs *SqliteBlockstore) GetLog() map[cid.Cid]blocks.Block {
154
-
return bs.inserts
155
-
}
+1
-1
test.go
+1
-1
test.go
···
32
32
33
33
u.Path = "xrpc/com.atproto.sync.subscribeRepos"
34
34
conn, _, err := dialer.Dial(u.String(), http.Header{
35
-
"User-Agent": []string{fmt.Sprintf("hot-topic/0.0.0")},
35
+
"User-Agent": []string{"cocoon-test/0.0.0"},
36
36
})
37
37
if err != nil {
38
38
return fmt.Errorf("subscribing to firehose failed (dialing): %w", err)