+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
+3
.gitignore
+3
.gitignore
+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
GIT_COMMIT := $(shell git rev-parse --short=9 HEAD)
5
VERSION := $(if $(GIT_TAG),$(GIT_TAG),dev-$(GIT_COMMIT))
6
7
.PHONY: help
8
help: ## Print info about all commands
9
@echo "Commands:"
···
14
build: ## Build all executables
15
go build -ldflags "-X main.Version=$(VERSION)" -o cocoon ./cmd/cocoon
16
17
.PHONY: run
18
run:
19
go build -ldflags "-X main.Version=dev-local" -o cocoon ./cmd/cocoon && ./cocoon run
···
40
41
.env:
42
if [ ! -f ".env" ]; then cp example.dev.env .env; fi
···
4
GIT_COMMIT := $(shell git rev-parse --short=9 HEAD)
5
VERSION := $(if $(GIT_TAG),$(GIT_TAG),dev-$(GIT_COMMIT))
6
7
+
# Build output directory
8
+
BUILD_DIR := dist
9
+
10
+
# Platforms to build for
11
+
PLATFORMS := \
12
+
linux/amd64 \
13
+
linux/arm64 \
14
+
linux/arm \
15
+
darwin/amd64 \
16
+
darwin/arm64 \
17
+
windows/amd64 \
18
+
windows/arm64 \
19
+
freebsd/amd64 \
20
+
freebsd/arm64 \
21
+
openbsd/amd64 \
22
+
openbsd/arm64
23
+
24
.PHONY: help
25
help: ## Print info about all commands
26
@echo "Commands:"
···
31
build: ## Build all executables
32
go build -ldflags "-X main.Version=$(VERSION)" -o cocoon ./cmd/cocoon
33
34
+
.PHONY: build-release
35
+
build-all: ## Build binaries for all architectures
36
+
@echo "Building for all architectures..."
37
+
@mkdir -p $(BUILD_DIR)
38
+
@$(foreach platform,$(PLATFORMS), \
39
+
$(eval OS := $(word 1,$(subst /, ,$(platform)))) \
40
+
$(eval ARCH := $(word 2,$(subst /, ,$(platform)))) \
41
+
$(eval EXT := $(if $(filter windows,$(OS)),.exe,)) \
42
+
$(eval OUTPUT := $(BUILD_DIR)/cocoon-$(VERSION)-$(OS)-$(ARCH)$(EXT)) \
43
+
echo "Building $(OS)/$(ARCH)..."; \
44
+
GOOS=$(OS) GOARCH=$(ARCH) go build -ldflags "-X main.Version=$(VERSION)" -o $(OUTPUT) ./cmd/cocoon && \
45
+
echo " โ $(OUTPUT)" || echo " โ Failed: $(OS)/$(ARCH)"; \
46
+
)
47
+
@echo "Done! Binaries are in $(BUILD_DIR)/"
48
+
49
+
.PHONY: clean-dist
50
+
clean-dist: ## Remove all built binaries
51
+
rm -rf $(BUILD_DIR)
52
+
53
.PHONY: run
54
run:
55
go build -ldflags "-X main.Version=dev-local" -o cocoon ./cmd/cocoon && ./cocoon run
···
76
77
.env:
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
# Cocoon
2
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.
5
6
Cocoon is a PDS implementation in Go. It is highly experimental, and is not ready for any production use.
7
8
## Implemented Endpoints
9
10
> [!NOTE]
···
12
13
### Identity
14
15
-
- [ ] `com.atproto.identity.getRecommendedDidCredentials`
16
-
- [ ] `com.atproto.identity.requestPlcOperationSignature`
17
- [x] `com.atproto.identity.resolveHandle`
18
-
- [ ] `com.atproto.identity.signPlcOperation`
19
-
- [ ] `com.atproto.identity.submitPlcOperation`
20
- [x] `com.atproto.identity.updateHandle`
21
22
### Repo
···
27
- [x] `com.atproto.repo.deleteRecord`
28
- [x] `com.atproto.repo.describeRepo`
29
- [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.)
31
- [x] `com.atproto.repo.listRecords`
32
-
- [ ] `com.atproto.repo.listMissingBlobs`
33
34
### Server
35
36
-
- [ ] `com.atproto.server.activateAccount`
37
- [x] `com.atproto.server.checkAccountStatus`
38
- [x] `com.atproto.server.confirmEmail`
39
- [x] `com.atproto.server.createAccount`
40
- [x] `com.atproto.server.createInviteCode`
41
- [x] `com.atproto.server.createInviteCodes`
42
-
- [ ] `com.atproto.server.deactivateAccount`
43
-
- [ ] `com.atproto.server.deleteAccount`
44
- [x] `com.atproto.server.deleteSession`
45
- [x] `com.atproto.server.describeServer`
46
- [ ] `com.atproto.server.getAccountInviteCodes`
47
-
- [ ] `com.atproto.server.getServiceAuth`
48
- ~~[ ] `com.atproto.server.listAppPasswords`~~ - not going to add app passwords
49
- [x] `com.atproto.server.refreshSession`
50
-
- [ ] `com.atproto.server.requestAccountDelete`
51
- [x] `com.atproto.server.requestEmailConfirmation`
52
- [x] `com.atproto.server.requestEmailUpdate`
53
- [x] `com.atproto.server.requestPasswordReset`
54
-
- [ ] `com.atproto.server.reserveSigningKey`
55
- [x] `com.atproto.server.resetPassword`
56
- ~~[] `com.atproto.server.revokeAppPassword`~~ - not going to add app passwords
57
- [x] `com.atproto.server.updateEmail`
···
72
73
### Other
74
75
-
- [ ] `com.atproto.label.queryLabels`
76
- [x] `com.atproto.moderation.createReport` (Note: this should be handled by proxying, not actually implemented in the PDS)
77
- [x] `app.bsky.actor.getPreferences`
78
- [x] `app.bsky.actor.putPreferences`
···
1
# Cocoon
2
3
> [!WARNING]
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
6
Cocoon is a PDS implementation in Go. It is highly experimental, and is not ready for any production use.
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
+
192
## Implemented Endpoints
193
194
> [!NOTE]
···
196
197
### Identity
198
199
+
- [x] `com.atproto.identity.getRecommendedDidCredentials`
200
+
- [x] `com.atproto.identity.requestPlcOperationSignature`
201
- [x] `com.atproto.identity.resolveHandle`
202
+
- [x] `com.atproto.identity.signPlcOperation`
203
+
- [x] `com.atproto.identity.submitPlcOperation`
204
- [x] `com.atproto.identity.updateHandle`
205
206
### Repo
···
211
- [x] `com.atproto.repo.deleteRecord`
212
- [x] `com.atproto.repo.describeRepo`
213
- [x] `com.atproto.repo.getRecord`
214
+
- [x] `com.atproto.repo.importRepo` (Works "okay". Use with extreme caution.)
215
- [x] `com.atproto.repo.listRecords`
216
+
- [x] `com.atproto.repo.listMissingBlobs`
217
218
### Server
219
220
+
- [x] `com.atproto.server.activateAccount`
221
- [x] `com.atproto.server.checkAccountStatus`
222
- [x] `com.atproto.server.confirmEmail`
223
- [x] `com.atproto.server.createAccount`
224
- [x] `com.atproto.server.createInviteCode`
225
- [x] `com.atproto.server.createInviteCodes`
226
+
- [x] `com.atproto.server.deactivateAccount`
227
+
- [x] `com.atproto.server.deleteAccount`
228
- [x] `com.atproto.server.deleteSession`
229
- [x] `com.atproto.server.describeServer`
230
- [ ] `com.atproto.server.getAccountInviteCodes`
231
+
- [x] `com.atproto.server.getServiceAuth`
232
- ~~[ ] `com.atproto.server.listAppPasswords`~~ - not going to add app passwords
233
- [x] `com.atproto.server.refreshSession`
234
+
- [x] `com.atproto.server.requestAccountDelete`
235
- [x] `com.atproto.server.requestEmailConfirmation`
236
- [x] `com.atproto.server.requestEmailUpdate`
237
- [x] `com.atproto.server.requestPasswordReset`
238
+
- [x] `com.atproto.server.reserveSigningKey`
239
- [x] `com.atproto.server.resetPassword`
240
- ~~[] `com.atproto.server.revokeAppPassword`~~ - not going to add app passwords
241
- [x] `com.atproto.server.updateEmail`
···
256
257
### Other
258
259
+
- [x] `com.atproto.label.queryLabels`
260
- [x] `com.atproto.moderation.createReport` (Note: this should be handled by proxying, not actually implemented in the PDS)
261
- [x] `app.bsky.actor.getPreferences`
262
- [x] `app.bsky.actor.putPreferences`
-163
blockstore/blockstore.go
-163
blockstore/blockstore.go
···
1
-
package blockstore
2
-
3
-
import (
4
-
"context"
5
-
"fmt"
6
-
7
-
"github.com/bluesky-social/indigo/atproto/syntax"
8
-
"github.com/haileyok/cocoon/internal/db"
9
-
"github.com/haileyok/cocoon/models"
10
-
blocks "github.com/ipfs/go-block-format"
11
-
"github.com/ipfs/go-cid"
12
-
"gorm.io/gorm/clause"
13
-
)
14
-
15
-
type SqliteBlockstore struct {
16
-
db *db.DB
17
-
did string
18
-
readonly bool
19
-
inserts map[cid.Cid]blocks.Block
20
-
}
21
-
22
-
func New(did string, db *db.DB) *SqliteBlockstore {
23
-
return &SqliteBlockstore{
24
-
did: did,
25
-
db: db,
26
-
readonly: false,
27
-
inserts: map[cid.Cid]blocks.Block{},
28
-
}
29
-
}
30
-
31
-
func NewReadOnly(did string, db *db.DB) *SqliteBlockstore {
32
-
return &SqliteBlockstore{
33
-
did: did,
34
-
db: db,
35
-
readonly: true,
36
-
inserts: map[cid.Cid]blocks.Block{},
37
-
}
38
-
}
39
-
40
-
func (bs *SqliteBlockstore) Get(ctx context.Context, cid cid.Cid) (blocks.Block, error) {
41
-
var block models.Block
42
-
43
-
maybeBlock, ok := bs.inserts[cid]
44
-
if ok {
45
-
return maybeBlock, nil
46
-
}
47
-
48
-
if err := bs.db.Raw("SELECT * FROM blocks WHERE did = ? AND cid = ?", nil, bs.did, cid.Bytes()).Scan(&block).Error; err != nil {
49
-
return nil, err
50
-
}
51
-
52
-
b, err := blocks.NewBlockWithCid(block.Value, cid)
53
-
if err != nil {
54
-
return nil, err
55
-
}
56
-
57
-
return b, nil
58
-
}
59
-
60
-
func (bs *SqliteBlockstore) Put(ctx context.Context, block blocks.Block) error {
61
-
bs.inserts[block.Cid()] = block
62
-
63
-
if bs.readonly {
64
-
return nil
65
-
}
66
-
67
-
b := models.Block{
68
-
Did: bs.did,
69
-
Cid: block.Cid().Bytes(),
70
-
Rev: syntax.NewTIDNow(0).String(), // TODO: WARN, this is bad. don't do this
71
-
Value: block.RawData(),
72
-
}
73
-
74
-
if err := bs.db.Create(&b, []clause.Expression{clause.OnConflict{
75
-
Columns: []clause.Column{{Name: "did"}, {Name: "cid"}},
76
-
UpdateAll: true,
77
-
}}).Error; err != nil {
78
-
return err
79
-
}
80
-
81
-
return nil
82
-
}
83
-
84
-
func (bs *SqliteBlockstore) DeleteBlock(context.Context, cid.Cid) error {
85
-
panic("not implemented")
86
-
}
87
-
88
-
func (bs *SqliteBlockstore) Has(context.Context, cid.Cid) (bool, error) {
89
-
panic("not implemented")
90
-
}
91
-
92
-
func (bs *SqliteBlockstore) GetSize(context.Context, cid.Cid) (int, error) {
93
-
panic("not implemented")
94
-
}
95
-
96
-
func (bs *SqliteBlockstore) PutMany(ctx context.Context, blocks []blocks.Block) error {
97
-
tx := bs.db.BeginDangerously()
98
-
99
-
for _, block := range blocks {
100
-
bs.inserts[block.Cid()] = block
101
-
102
-
if bs.readonly {
103
-
continue
104
-
}
105
-
106
-
b := models.Block{
107
-
Did: bs.did,
108
-
Cid: block.Cid().Bytes(),
109
-
Rev: syntax.NewTIDNow(0).String(), // TODO: WARN, this is bad. don't do this
110
-
Value: block.RawData(),
111
-
}
112
-
113
-
if err := tx.Clauses(clause.OnConflict{
114
-
Columns: []clause.Column{{Name: "did"}, {Name: "cid"}},
115
-
UpdateAll: true,
116
-
}).Create(&b).Error; err != nil {
117
-
tx.Rollback()
118
-
return err
119
-
}
120
-
}
121
-
122
-
if bs.readonly {
123
-
return nil
124
-
}
125
-
126
-
tx.Commit()
127
-
128
-
return nil
129
-
}
130
-
131
-
func (bs *SqliteBlockstore) AllKeysChan(ctx context.Context) (<-chan cid.Cid, error) {
132
-
panic("not implemented")
133
-
}
134
-
135
-
func (bs *SqliteBlockstore) HashOnRead(enabled bool) {
136
-
panic("not implemented")
137
-
}
138
-
139
-
func (bs *SqliteBlockstore) UpdateRepo(ctx context.Context, root cid.Cid, rev string) error {
140
-
if err := bs.db.Exec("UPDATE repos SET root = ?, rev = ? WHERE did = ?", nil, root.Bytes(), rev, bs.did).Error; err != nil {
141
-
return err
142
-
}
143
-
144
-
return nil
145
-
}
146
-
147
-
func (bs *SqliteBlockstore) Execute(ctx context.Context) error {
148
-
if !bs.readonly {
149
-
return fmt.Errorf("blockstore was not readonly")
150
-
}
151
-
152
-
bs.readonly = false
153
-
for _, b := range bs.inserts {
154
-
bs.Put(ctx, b)
155
-
}
156
-
bs.readonly = true
157
-
158
-
return nil
159
-
}
160
-
161
-
func (bs *SqliteBlockstore) GetLog() map[cid.Cid]blocks.Block {
162
-
return bs.inserts
163
-
}
···
+109
-56
cmd/cocoon/main.go
+109
-56
cmd/cocoon/main.go
···
9
"os"
10
"time"
11
12
-
"github.com/bluesky-social/indigo/atproto/crypto"
13
"github.com/bluesky-social/indigo/atproto/syntax"
14
"github.com/haileyok/cocoon/internal/helpers"
15
"github.com/haileyok/cocoon/server"
···
17
"github.com/lestrrat-go/jwx/v2/jwk"
18
"github.com/urfave/cli/v2"
19
"golang.org/x/crypto/bcrypt"
20
"gorm.io/driver/sqlite"
21
"gorm.io/gorm"
22
)
···
39
EnvVars: []string{"COCOON_DB_NAME"},
40
},
41
&cli.StringFlag{
42
-
Name: "did",
43
-
Required: true,
44
-
EnvVars: []string{"COCOON_DID"},
45
},
46
&cli.StringFlag{
47
-
Name: "hostname",
48
-
Required: true,
49
-
EnvVars: []string{"COCOON_HOSTNAME"},
50
},
51
&cli.StringFlag{
52
-
Name: "rotation-key-path",
53
-
Required: true,
54
-
EnvVars: []string{"COCOON_ROTATION_KEY_PATH"},
55
},
56
&cli.StringFlag{
57
-
Name: "jwk-path",
58
-
Required: true,
59
-
EnvVars: []string{"COCOON_JWK_PATH"},
60
},
61
&cli.StringFlag{
62
-
Name: "contact-email",
63
-
Required: true,
64
-
EnvVars: []string{"COCOON_CONTACT_EMAIL"},
65
},
66
&cli.StringSliceFlag{
67
-
Name: "relays",
68
-
Required: true,
69
-
EnvVars: []string{"COCOON_RELAYS"},
70
},
71
&cli.StringFlag{
72
-
Name: "admin-password",
73
-
Required: true,
74
-
EnvVars: []string{"COCOON_ADMIN_PASSWORD"},
75
},
76
&cli.StringFlag{
77
-
Name: "smtp-user",
78
-
Required: false,
79
-
EnvVars: []string{"COCOON_SMTP_USER"},
80
},
81
&cli.StringFlag{
82
-
Name: "smtp-pass",
83
-
Required: false,
84
-
EnvVars: []string{"COCOON_SMTP_PASS"},
85
},
86
&cli.StringFlag{
87
-
Name: "smtp-host",
88
-
Required: false,
89
-
EnvVars: []string{"COCOON_SMTP_HOST"},
90
},
91
&cli.StringFlag{
92
-
Name: "smtp-port",
93
-
Required: false,
94
-
EnvVars: []string{"COCOON_SMTP_PORT"},
95
},
96
&cli.StringFlag{
97
-
Name: "smtp-email",
98
-
Required: false,
99
-
EnvVars: []string{"COCOON_SMTP_EMAIL"},
100
},
101
&cli.StringFlag{
102
-
Name: "smtp-name",
103
-
Required: false,
104
-
EnvVars: []string{"COCOON_SMTP_NAME"},
105
},
106
&cli.BoolFlag{
107
Name: "s3-backups-enabled",
108
EnvVars: []string{"COCOON_S3_BACKUPS_ENABLED"},
109
},
110
&cli.StringFlag{
111
Name: "s3-region",
···
128
EnvVars: []string{"COCOON_S3_SECRET_KEY"},
129
},
130
&cli.StringFlag{
131
Name: "session-secret",
132
EnvVars: []string{"COCOON_SESSION_SECRET"},
133
},
134
&cli.StringFlag{
135
-
Name: "default-atproto-proxy",
136
-
EnvVars: []string{"COCOON_DEFAULT_ATPROTO_PROXY"},
137
-
Value: "did:web:api.bsky.app#bsky_appview",
138
},
139
},
140
Commands: []*cli.Command{
141
runServe,
···
158
Usage: "Start the cocoon PDS",
159
Flags: []cli.Flag{},
160
Action: func(cmd *cli.Context) error {
161
s, err := server.New(&server.Args{
162
Addr: cmd.String("addr"),
163
DbName: cmd.String("db-name"),
164
Did: cmd.String("did"),
165
Hostname: cmd.String("hostname"),
166
RotationKeyPath: cmd.String("rotation-key-path"),
···
169
Version: Version,
170
Relays: cmd.StringSlice("relays"),
171
AdminPassword: cmd.String("admin-password"),
172
SmtpUser: cmd.String("smtp-user"),
173
SmtpPass: cmd.String("smtp-pass"),
174
SmtpHost: cmd.String("smtp-host"),
···
176
SmtpEmail: cmd.String("smtp-email"),
177
SmtpName: cmd.String("smtp-name"),
178
S3Config: &server.S3Config{
179
-
BackupsEnabled: cmd.Bool("s3-backups-enabled"),
180
-
Region: cmd.String("s3-region"),
181
-
Bucket: cmd.String("s3-bucket"),
182
-
Endpoint: cmd.String("s3-endpoint"),
183
-
AccessKey: cmd.String("s3-access-key"),
184
-
SecretKey: cmd.String("s3-secret-key"),
185
},
186
-
SessionSecret: cmd.String("session-secret"),
187
-
DefaultAtprotoProxy: cmd.String("default-atproto-proxy"),
188
})
189
if err != nil {
190
fmt.Printf("error creating cocoon: %v", err)
···
211
},
212
},
213
Action: func(cmd *cli.Context) error {
214
-
key, err := crypto.GeneratePrivateKeyK256()
215
if err != nil {
216
return err
217
}
···
281
},
282
},
283
Action: func(cmd *cli.Context) error {
284
-
db, err := newDb()
285
if err != nil {
286
return err
287
}
···
320
},
321
},
322
Action: func(cmd *cli.Context) error {
323
-
db, err := newDb()
324
if err != nil {
325
return err
326
}
···
347
},
348
}
349
350
-
func newDb() (*gorm.DB, error) {
351
-
return gorm.Open(sqlite.Open("cocoon.db"), &gorm.Config{})
352
}
···
9
"os"
10
"time"
11
12
+
"github.com/bluesky-social/go-util/pkg/telemetry"
13
+
"github.com/bluesky-social/indigo/atproto/atcrypto"
14
"github.com/bluesky-social/indigo/atproto/syntax"
15
"github.com/haileyok/cocoon/internal/helpers"
16
"github.com/haileyok/cocoon/server"
···
18
"github.com/lestrrat-go/jwx/v2/jwk"
19
"github.com/urfave/cli/v2"
20
"golang.org/x/crypto/bcrypt"
21
+
"gorm.io/driver/postgres"
22
"gorm.io/driver/sqlite"
23
"gorm.io/gorm"
24
)
···
41
EnvVars: []string{"COCOON_DB_NAME"},
42
},
43
&cli.StringFlag{
44
+
Name: "db-type",
45
+
Value: "sqlite",
46
+
Usage: "Database type: sqlite or postgres",
47
+
EnvVars: []string{"COCOON_DB_TYPE"},
48
},
49
&cli.StringFlag{
50
+
Name: "database-url",
51
+
Aliases: []string{"db-url"},
52
+
Usage: "PostgreSQL connection string (required if db-type is postgres)",
53
+
EnvVars: []string{"COCOON_DATABASE_URL", "DATABASE_URL"},
54
},
55
&cli.StringFlag{
56
+
Name: "did",
57
+
EnvVars: []string{"COCOON_DID"},
58
},
59
&cli.StringFlag{
60
+
Name: "hostname",
61
+
EnvVars: []string{"COCOON_HOSTNAME"},
62
+
},
63
+
&cli.StringFlag{
64
+
Name: "rotation-key-path",
65
+
EnvVars: []string{"COCOON_ROTATION_KEY_PATH"},
66
+
},
67
+
&cli.StringFlag{
68
+
Name: "jwk-path",
69
+
EnvVars: []string{"COCOON_JWK_PATH"},
70
},
71
&cli.StringFlag{
72
+
Name: "contact-email",
73
+
EnvVars: []string{"COCOON_CONTACT_EMAIL"},
74
},
75
&cli.StringSliceFlag{
76
+
Name: "relays",
77
+
EnvVars: []string{"COCOON_RELAYS"},
78
},
79
&cli.StringFlag{
80
+
Name: "admin-password",
81
+
EnvVars: []string{"COCOON_ADMIN_PASSWORD"},
82
+
},
83
+
&cli.BoolFlag{
84
+
Name: "require-invite",
85
+
EnvVars: []string{"COCOON_REQUIRE_INVITE"},
86
+
Value: true,
87
},
88
&cli.StringFlag{
89
+
Name: "smtp-user",
90
+
EnvVars: []string{"COCOON_SMTP_USER"},
91
},
92
&cli.StringFlag{
93
+
Name: "smtp-pass",
94
+
EnvVars: []string{"COCOON_SMTP_PASS"},
95
},
96
&cli.StringFlag{
97
+
Name: "smtp-host",
98
+
EnvVars: []string{"COCOON_SMTP_HOST"},
99
},
100
&cli.StringFlag{
101
+
Name: "smtp-port",
102
+
EnvVars: []string{"COCOON_SMTP_PORT"},
103
},
104
&cli.StringFlag{
105
+
Name: "smtp-email",
106
+
EnvVars: []string{"COCOON_SMTP_EMAIL"},
107
},
108
&cli.StringFlag{
109
+
Name: "smtp-name",
110
+
EnvVars: []string{"COCOON_SMTP_NAME"},
111
},
112
&cli.BoolFlag{
113
Name: "s3-backups-enabled",
114
EnvVars: []string{"COCOON_S3_BACKUPS_ENABLED"},
115
+
},
116
+
&cli.BoolFlag{
117
+
Name: "s3-blobstore-enabled",
118
+
EnvVars: []string{"COCOON_S3_BLOBSTORE_ENABLED"},
119
},
120
&cli.StringFlag{
121
Name: "s3-region",
···
138
EnvVars: []string{"COCOON_S3_SECRET_KEY"},
139
},
140
&cli.StringFlag{
141
+
Name: "s3-cdn-url",
142
+
EnvVars: []string{"COCOON_S3_CDN_URL"},
143
+
Usage: "Public URL for S3 blob redirects (e.g., https://cdn.example.com). When set, getBlob redirects to this URL instead of proxying.",
144
+
},
145
+
&cli.StringFlag{
146
Name: "session-secret",
147
EnvVars: []string{"COCOON_SESSION_SECRET"},
148
},
149
&cli.StringFlag{
150
+
Name: "blockstore-variant",
151
+
EnvVars: []string{"COCOON_BLOCKSTORE_VARIANT"},
152
+
Value: "sqlite",
153
+
},
154
+
&cli.StringFlag{
155
+
Name: "fallback-proxy",
156
+
EnvVars: []string{"COCOON_FALLBACK_PROXY"},
157
},
158
+
telemetry.CLIFlagDebug,
159
+
telemetry.CLIFlagMetricsListenAddress,
160
},
161
Commands: []*cli.Command{
162
runServe,
···
179
Usage: "Start the cocoon PDS",
180
Flags: []cli.Flag{},
181
Action: func(cmd *cli.Context) error {
182
+
183
+
logger := telemetry.StartLogger(cmd)
184
+
telemetry.StartMetrics(cmd)
185
+
186
s, err := server.New(&server.Args{
187
+
Logger: logger,
188
Addr: cmd.String("addr"),
189
DbName: cmd.String("db-name"),
190
+
DbType: cmd.String("db-type"),
191
+
DatabaseURL: cmd.String("database-url"),
192
Did: cmd.String("did"),
193
Hostname: cmd.String("hostname"),
194
RotationKeyPath: cmd.String("rotation-key-path"),
···
197
Version: Version,
198
Relays: cmd.StringSlice("relays"),
199
AdminPassword: cmd.String("admin-password"),
200
+
RequireInvite: cmd.Bool("require-invite"),
201
SmtpUser: cmd.String("smtp-user"),
202
SmtpPass: cmd.String("smtp-pass"),
203
SmtpHost: cmd.String("smtp-host"),
···
205
SmtpEmail: cmd.String("smtp-email"),
206
SmtpName: cmd.String("smtp-name"),
207
S3Config: &server.S3Config{
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"),
216
},
217
+
SessionSecret: cmd.String("session-secret"),
218
+
BlockstoreVariant: server.MustReturnBlockstoreVariant(cmd.String("blockstore-variant")),
219
+
FallbackProxy: cmd.String("fallback-proxy"),
220
})
221
if err != nil {
222
fmt.Printf("error creating cocoon: %v", err)
···
243
},
244
},
245
Action: func(cmd *cli.Context) error {
246
+
key, err := atcrypto.GeneratePrivateKeyK256()
247
if err != nil {
248
return err
249
}
···
313
},
314
},
315
Action: func(cmd *cli.Context) error {
316
+
db, err := newDb(cmd)
317
if err != nil {
318
return err
319
}
···
352
},
353
},
354
Action: func(cmd *cli.Context) error {
355
+
db, err := newDb(cmd)
356
if err != nil {
357
return err
358
}
···
379
},
380
}
381
382
+
func newDb(cmd *cli.Context) (*gorm.DB, error) {
383
+
dbType := cmd.String("db-type")
384
+
if dbType == "" {
385
+
dbType = "sqlite"
386
+
}
387
+
388
+
switch dbType {
389
+
case "postgres":
390
+
databaseURL := cmd.String("database-url")
391
+
if databaseURL == "" {
392
+
databaseURL = cmd.String("database-url")
393
+
}
394
+
if databaseURL == "" {
395
+
return nil, fmt.Errorf("COCOON_DATABASE_URL or DATABASE_URL must be set when using postgres")
396
+
}
397
+
return gorm.Open(postgres.Open(databaseURL), &gorm.Config{})
398
+
default:
399
+
dbName := cmd.String("db-name")
400
+
if dbName == "" {
401
+
dbName = "cocoon.db"
402
+
}
403
+
return gorm.Open(sqlite.Open(dbName), &gorm.Config{})
404
+
}
405
}
+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
+17
-15
go.mod
+17
-15
go.mod
···
1
module github.com/haileyok/cocoon
2
3
-
go 1.24.1
4
5
require (
6
github.com/Azure/go-autorest/autorest/to v0.4.1
7
github.com/aws/aws-sdk-go v1.55.7
8
-
github.com/bluesky-social/indigo v0.0.0-20250414202759-826fcdeaa36b
9
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792
10
github.com/domodwyer/mailyak/v3 v3.6.2
11
github.com/go-pkgz/expirable-cache/v3 v3.0.0
12
github.com/go-playground/validator v9.31.0+incompatible
13
github.com/golang-jwt/jwt/v4 v4.5.2
14
-
github.com/google/uuid v1.4.0
15
github.com/gorilla/sessions v1.4.0
16
github.com/gorilla/websocket v1.5.1
17
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b
18
github.com/hashicorp/golang-lru/v2 v2.0.7
19
github.com/ipfs/go-block-format v0.2.0
20
github.com/ipfs/go-cid v0.4.1
21
github.com/ipfs/go-ipld-cbor v0.1.0
22
github.com/ipld/go-car v0.6.1-0.20230509095817-92d28eb23ba4
23
github.com/joho/godotenv v1.5.1
···
25
github.com/labstack/echo/v4 v4.13.3
26
github.com/lestrrat-go/jwx/v2 v2.0.12
27
github.com/multiformats/go-multihash v0.2.3
28
github.com/samber/slog-echo v1.16.1
29
github.com/urfave/cli/v2 v2.27.6
30
github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e
31
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b
32
-
golang.org/x/crypto v0.38.0
33
gorm.io/driver/sqlite v1.5.7
34
gorm.io/gorm v1.25.12
35
)
···
55
github.com/gorilla/securecookie v1.1.2 // indirect
56
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect
57
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
58
-
github.com/hashicorp/go-retryablehttp v0.7.5 // indirect
59
github.com/hashicorp/golang-lru v1.0.2 // indirect
60
github.com/ipfs/bbloom v0.0.4 // indirect
61
github.com/ipfs/go-blockservice v0.5.2 // indirect
62
github.com/ipfs/go-datastore v0.6.0 // indirect
63
-
github.com/ipfs/go-ipfs-blockstore v1.3.1 // indirect
64
github.com/ipfs/go-ipfs-ds-help v1.1.1 // indirect
65
github.com/ipfs/go-ipfs-exchange-interface v0.2.1 // indirect
66
github.com/ipfs/go-ipfs-util v0.0.3 // indirect
···
102
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
103
github.com/opentracing/opentracing-go v1.2.0 // indirect
104
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f // indirect
105
-
github.com/prometheus/client_golang v1.22.0 // indirect
106
github.com/prometheus/client_model v0.6.2 // indirect
107
-
github.com/prometheus/common v0.63.0 // indirect
108
github.com/prometheus/procfs v0.16.1 // indirect
109
github.com/russross/blackfriday/v2 v2.1.0 // indirect
110
github.com/samber/lo v1.49.1 // indirect
···
114
github.com/valyala/fasttemplate v1.2.2 // indirect
115
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
116
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect
117
-
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect
118
go.opentelemetry.io/otel v1.29.0 // indirect
119
go.opentelemetry.io/otel/metric v1.29.0 // indirect
120
go.opentelemetry.io/otel/trace v1.29.0 // indirect
121
go.uber.org/atomic v1.11.0 // indirect
122
go.uber.org/multierr v1.11.0 // indirect
123
go.uber.org/zap v1.26.0 // indirect
124
-
golang.org/x/net v0.40.0 // indirect
125
-
golang.org/x/sync v0.14.0 // indirect
126
-
golang.org/x/sys v0.33.0 // indirect
127
-
golang.org/x/text v0.25.0 // indirect
128
golang.org/x/time v0.11.0 // indirect
129
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
130
-
google.golang.org/protobuf v1.36.6 // indirect
131
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
132
gopkg.in/inf.v0 v0.9.1 // indirect
133
-
gorm.io/driver/postgres v1.5.7 // indirect
134
lukechampine.com/blake3 v1.2.1 // indirect
135
)
···
1
module github.com/haileyok/cocoon
2
3
+
go 1.24.5
4
5
require (
6
github.com/Azure/go-autorest/autorest/to v0.4.1
7
github.com/aws/aws-sdk-go v1.55.7
8
+
github.com/bluesky-social/go-util v0.0.0-20251012040650-2ebbf57f5934
9
+
github.com/bluesky-social/indigo v0.0.0-20251009212240-20524de167fe
10
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792
11
github.com/domodwyer/mailyak/v3 v3.6.2
12
github.com/go-pkgz/expirable-cache/v3 v3.0.0
13
github.com/go-playground/validator v9.31.0+incompatible
14
github.com/golang-jwt/jwt/v4 v4.5.2
15
+
github.com/google/uuid v1.6.0
16
github.com/gorilla/sessions v1.4.0
17
github.com/gorilla/websocket v1.5.1
18
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b
19
github.com/hashicorp/golang-lru/v2 v2.0.7
20
github.com/ipfs/go-block-format v0.2.0
21
github.com/ipfs/go-cid v0.4.1
22
+
github.com/ipfs/go-ipfs-blockstore v1.3.1
23
github.com/ipfs/go-ipld-cbor v0.1.0
24
github.com/ipld/go-car v0.6.1-0.20230509095817-92d28eb23ba4
25
github.com/joho/godotenv v1.5.1
···
27
github.com/labstack/echo/v4 v4.13.3
28
github.com/lestrrat-go/jwx/v2 v2.0.12
29
github.com/multiformats/go-multihash v0.2.3
30
+
github.com/prometheus/client_golang v1.23.2
31
github.com/samber/slog-echo v1.16.1
32
github.com/urfave/cli/v2 v2.27.6
33
github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e
34
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b
35
+
golang.org/x/crypto v0.41.0
36
+
gorm.io/driver/postgres v1.5.7
37
gorm.io/driver/sqlite v1.5.7
38
gorm.io/gorm v1.25.12
39
)
···
59
github.com/gorilla/securecookie v1.1.2 // indirect
60
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect
61
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
62
+
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
63
github.com/hashicorp/golang-lru v1.0.2 // indirect
64
github.com/ipfs/bbloom v0.0.4 // indirect
65
github.com/ipfs/go-blockservice v0.5.2 // indirect
66
github.com/ipfs/go-datastore v0.6.0 // indirect
67
github.com/ipfs/go-ipfs-ds-help v1.1.1 // indirect
68
github.com/ipfs/go-ipfs-exchange-interface v0.2.1 // indirect
69
github.com/ipfs/go-ipfs-util v0.0.3 // indirect
···
105
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
106
github.com/opentracing/opentracing-go v1.2.0 // indirect
107
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f // indirect
108
github.com/prometheus/client_model v0.6.2 // indirect
109
+
github.com/prometheus/common v0.66.1 // indirect
110
github.com/prometheus/procfs v0.16.1 // indirect
111
github.com/russross/blackfriday/v2 v2.1.0 // indirect
112
github.com/samber/lo v1.49.1 // indirect
···
116
github.com/valyala/fasttemplate v1.2.2 // indirect
117
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
118
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect
119
+
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
120
go.opentelemetry.io/otel v1.29.0 // indirect
121
go.opentelemetry.io/otel/metric v1.29.0 // indirect
122
go.opentelemetry.io/otel/trace v1.29.0 // indirect
123
go.uber.org/atomic v1.11.0 // indirect
124
go.uber.org/multierr v1.11.0 // indirect
125
go.uber.org/zap v1.26.0 // indirect
126
+
go.yaml.in/yaml/v2 v2.4.2 // indirect
127
+
golang.org/x/net v0.43.0 // indirect
128
+
golang.org/x/sync v0.16.0 // indirect
129
+
golang.org/x/sys v0.35.0 // indirect
130
+
golang.org/x/text v0.28.0 // indirect
131
golang.org/x/time v0.11.0 // indirect
132
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
133
+
google.golang.org/protobuf v1.36.9 // indirect
134
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
135
gopkg.in/inf.v0 v0.9.1 // indirect
136
lukechampine.com/blake3 v1.2.1 // indirect
137
)
+44
-37
go.sum
+44
-37
go.sum
···
16
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
17
github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY=
18
github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k=
19
-
github.com/bluesky-social/indigo v0.0.0-20250414202759-826fcdeaa36b h1:elwfbe+W7GkUmPKFX1h7HaeHvC/kC0XJWfiEHC62xPg=
20
-
github.com/bluesky-social/indigo v0.0.0-20250414202759-826fcdeaa36b/go.mod h1:yjdhLA1LkK8VDS/WPUoYPo25/Hq/8rX38Ftr67EsqKY=
21
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
22
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
23
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 h1:R8vQdOQdZ9Y3SkEwmHoWBmX1DNXhXZqlTpq6s4tyJGc=
···
39
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
40
github.com/domodwyer/mailyak/v3 v3.6.2 h1:x3tGMsyFhTCaxp6ycgR0FE/bu5QiNp+hetUuCOBXMn8=
41
github.com/domodwyer/mailyak/v3 v3.6.2/go.mod h1:lOm/u9CyCVWHeaAmHIdF4RiKVxKUT/H5XX10lIKAL6c=
42
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
43
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
44
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
···
77
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
78
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
79
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
80
-
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
81
-
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
82
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
83
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
84
github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o=
···
95
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b/go.mod h1:VzxiSdG6j1pi7rwGm/xYI5RbtpBgM8sARDXlvEvxlu0=
96
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
97
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
98
-
github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
99
-
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
100
-
github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M=
101
-
github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
102
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
103
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
104
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
···
113
github.com/ipfs/go-block-format v0.2.0/go.mod h1:+jpL11nFx5A/SPpsoBn6Bzkra/zaArfSmsknbPMYgzM=
114
github.com/ipfs/go-blockservice v0.5.2 h1:in9Bc+QcXwd1apOVM7Un9t8tixPKdaHQFdLSUM1Xgk8=
115
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
github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s=
119
github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk=
120
github.com/ipfs/go-datastore v0.6.0 h1:JKyz+Gvz1QEZw0LsX1IBn+JFCJQH4SJVFtM4uWU0Myk=
···
197
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
198
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
199
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
200
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
201
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
202
github.com/koron/go-ssdp v0.0.3 h1:JivLMY45N76b4p/vsWGOKewBQu6uf39y8l+AQ7sDKx8=
···
208
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
209
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
210
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
211
github.com/labstack/echo-contrib v0.17.4 h1:g5mfsrJfJTKv+F5uNKCyrjLK7js+ZW6HTjg4FnDxxgk=
212
github.com/labstack/echo-contrib v0.17.4/go.mod h1:9O7ZPAHUeMGTOAfg80YqQduHzt0CzLak36PZRldYrZ0=
213
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
···
291
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
292
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f h1:VXTQfuJj9vKR4TCkEuWIckKvdHFeJH/huIFJ9/cXOB0=
293
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=
296
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
297
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=
300
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
301
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
302
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
···
321
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
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
326
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
327
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
···
329
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=
334
github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
335
github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g=
336
github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
···
356
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b/go.mod h1:/y/V339mxv2sZmYYR64O07VuCpdNZqCTwO8ZcouTMI8=
357
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 h1:qwDnMxjkyLmAFgcfgTnfJrmYKWhHnci3GjDqcZp1M3Q=
358
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=
361
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
362
go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
363
go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc=
···
369
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
370
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
371
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=
374
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
375
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
376
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
···
380
go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
381
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
382
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
383
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
384
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
385
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
386
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
387
golang.org/x/crypto v0.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=
391
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ=
392
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE=
393
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
···
397
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=
402
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
403
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
404
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
···
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=
414
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
415
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
416
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
417
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=
422
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
423
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
424
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
···
434
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=
439
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=
···
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=
452
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
453
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
454
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
···
463
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=
468
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
469
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
470
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
471
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
472
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
473
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=
476
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
477
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
478
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
···
16
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
17
github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY=
18
github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k=
19
+
github.com/bluesky-social/go-util v0.0.0-20251012040650-2ebbf57f5934 h1:btHMur2kTRgWEnCHn6LaI3BE9YRgsqTpwpJ1UdB7VEk=
20
+
github.com/bluesky-social/go-util v0.0.0-20251012040650-2ebbf57f5934/go.mod h1:LWamyZfbQGW7PaVc5jumFfjgrshJ5mXgDUnR6fK7+BI=
21
+
github.com/bluesky-social/indigo v0.0.0-20251009212240-20524de167fe h1:VBhaqE5ewQgXbY5SfSWFZC/AwHFo7cHxZKFYi2ce9Yo=
22
+
github.com/bluesky-social/indigo v0.0.0-20251009212240-20524de167fe/go.mod h1:RuQVrCGm42QNsgumKaR6se+XkFKfCPNwdCiTvqKRUck=
23
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
24
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
25
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 h1:R8vQdOQdZ9Y3SkEwmHoWBmX1DNXhXZqlTpq6s4tyJGc=
···
41
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
42
github.com/domodwyer/mailyak/v3 v3.6.2 h1:x3tGMsyFhTCaxp6ycgR0FE/bu5QiNp+hetUuCOBXMn8=
43
github.com/domodwyer/mailyak/v3 v3.6.2/go.mod h1:lOm/u9CyCVWHeaAmHIdF4RiKVxKUT/H5XX10lIKAL6c=
44
+
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
45
+
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
46
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
47
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
48
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
···
81
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
82
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
83
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
84
+
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
85
+
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
86
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
87
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
88
github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o=
···
99
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b/go.mod h1:VzxiSdG6j1pi7rwGm/xYI5RbtpBgM8sARDXlvEvxlu0=
100
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
101
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
102
+
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
103
+
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
104
+
github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
105
+
github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
106
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
107
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
108
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
···
117
github.com/ipfs/go-block-format v0.2.0/go.mod h1:+jpL11nFx5A/SPpsoBn6Bzkra/zaArfSmsknbPMYgzM=
118
github.com/ipfs/go-blockservice v0.5.2 h1:in9Bc+QcXwd1apOVM7Un9t8tixPKdaHQFdLSUM1Xgk8=
119
github.com/ipfs/go-blockservice v0.5.2/go.mod h1:VpMblFEqG67A/H2sHKAemeH9vlURVavlysbdUI632yk=
120
github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s=
121
github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk=
122
github.com/ipfs/go-datastore v0.6.0 h1:JKyz+Gvz1QEZw0LsX1IBn+JFCJQH4SJVFtM4uWU0Myk=
···
199
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
200
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
201
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
202
+
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
203
+
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
204
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
205
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
206
github.com/koron/go-ssdp v0.0.3 h1:JivLMY45N76b4p/vsWGOKewBQu6uf39y8l+AQ7sDKx8=
···
212
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
213
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
214
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
215
+
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
216
+
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
217
github.com/labstack/echo-contrib v0.17.4 h1:g5mfsrJfJTKv+F5uNKCyrjLK7js+ZW6HTjg4FnDxxgk=
218
github.com/labstack/echo-contrib v0.17.4/go.mod h1:9O7ZPAHUeMGTOAfg80YqQduHzt0CzLak36PZRldYrZ0=
219
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
···
297
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
298
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f h1:VXTQfuJj9vKR4TCkEuWIckKvdHFeJH/huIFJ9/cXOB0=
299
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw=
300
+
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
301
+
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
302
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
303
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
304
+
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
305
+
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
306
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
307
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
308
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
···
327
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
328
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
329
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
330
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
331
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
332
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
···
334
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
335
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
336
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
337
+
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
338
+
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
339
github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
340
github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g=
341
github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
···
361
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b/go.mod h1:/y/V339mxv2sZmYYR64O07VuCpdNZqCTwO8ZcouTMI8=
362
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 h1:qwDnMxjkyLmAFgcfgTnfJrmYKWhHnci3GjDqcZp1M3Q=
363
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02/go.mod h1:JTnUj0mpYiAsuZLmKjTx/ex3AtMowcCgnE7YNyCEP0I=
364
+
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
365
+
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
366
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
367
go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
368
go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc=
···
374
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
375
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
376
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
377
+
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
378
+
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
379
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
380
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
381
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
···
385
go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
386
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
387
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
388
+
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
389
+
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
390
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
391
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
392
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
393
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
394
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
395
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
396
+
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
397
+
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
398
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ=
399
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE=
400
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
···
404
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
405
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
406
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
407
+
golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
408
+
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
409
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
410
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
411
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
···
416
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
417
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
418
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
419
+
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
420
+
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
421
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
422
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
423
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
424
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
425
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
426
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
427
+
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
428
+
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
429
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
430
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
431
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
···
441
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
442
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
443
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
444
+
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
445
+
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
446
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
447
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
448
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
···
454
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
455
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
456
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
457
+
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
458
+
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
459
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
460
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
461
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
···
470
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
471
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
472
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
473
+
golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
474
+
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
475
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
476
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
477
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
478
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
479
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
480
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
481
+
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
482
+
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
483
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
484
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
485
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+74
-55
identity/identity.go
+74
-55
identity/identity.go
···
13
"github.com/bluesky-social/indigo/util"
14
)
15
16
-
func ResolveHandle(ctx context.Context, cli *http.Client, handle string) (string, error) {
17
-
if cli == nil {
18
-
cli = util.RobustHTTPClient()
19
}
20
21
-
var did string
22
23
-
_, err := syntax.ParseHandle(handle)
24
if err != nil {
25
-
return "", err
26
}
27
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)
38
}
39
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
-
}
50
51
-
resp, err := http.DefaultClient.Do(req)
52
-
if err != nil {
53
-
return "", nil
54
-
}
55
-
defer resp.Body.Close()
56
57
-
if resp.StatusCode != http.StatusOK {
58
-
io.Copy(io.Discard, resp.Body)
59
-
return "", fmt.Errorf("unable to resolve handle")
60
-
}
61
62
-
b, err := io.ReadAll(resp.Body)
63
-
if err != nil {
64
-
return "", err
65
-
}
66
67
-
maybeDid := string(b)
68
69
-
if _, err := syntax.ParseDID(maybeDid); err != nil {
70
-
return "", fmt.Errorf("unable to resolve handle")
71
-
}
72
73
-
did = maybeDid
74
}
75
76
-
return did, nil
77
}
78
79
func FetchDidDoc(ctx context.Context, cli *http.Client, did string) (*DidDoc, error) {
···
81
cli = util.RobustHTTPClient()
82
}
83
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")
91
}
92
93
req, err := http.NewRequestWithContext(ctx, "GET", ustr, nil)
···
95
return nil, err
96
}
97
98
-
resp, err := http.DefaultClient.Do(req)
99
if err != nil {
100
return nil, err
101
}
···
103
104
if resp.StatusCode != 200 {
105
io.Copy(io.Discard, resp.Body)
106
-
return nil, fmt.Errorf("could not find identity in plc registry")
107
}
108
109
var diddoc DidDoc
···
127
return nil, err
128
}
129
130
-
resp, err := http.DefaultClient.Do(req)
131
if err != nil {
132
return nil, err
133
}
···
13
"github.com/bluesky-social/indigo/util"
14
)
15
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)
45
}
46
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()
52
53
+
b, err := io.ReadAll(resp.Body)
54
if err != nil {
55
+
return "", fmt.Errorf("handle could not be resolved via web: %w", err)
56
}
57
58
+
if resp.StatusCode != http.StatusOK {
59
+
return "", fmt.Errorf("handle could not be resolved via web: invalid status code %d", resp.StatusCode)
60
}
61
62
+
maybeDid := string(b)
63
64
+
if _, err := syntax.ParseDID(maybeDid); err != nil {
65
+
return "", fmt.Errorf("handle could not be resolved via web: invalid did in document")
66
+
}
67
68
+
return maybeDid, nil
69
+
}
70
71
+
func ResolveHandle(ctx context.Context, cli *http.Client, handle string) (string, error) {
72
+
if cli == nil {
73
+
cli = util.RobustHTTPClient()
74
+
}
75
76
+
_, err := syntax.ParseHandle(handle)
77
+
if err != nil {
78
+
return "", err
79
+
}
80
81
+
if maybeDidFromTxt, err := ResolveHandleFromTXT(ctx, handle); err == nil {
82
+
return maybeDidFromTxt, nil
83
+
}
84
85
+
if maybeDidFromWeb, err := ResolveHandleFromWellKnown(ctx, cli, handle); err == nil {
86
+
return maybeDidFromWeb, nil
87
}
88
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
+
}
100
}
101
102
func FetchDidDoc(ctx context.Context, cli *http.Client, did string) (*DidDoc, error) {
···
104
cli = util.RobustHTTPClient()
105
}
106
107
+
ustr, err := DidToDocUrl(did)
108
+
if err != nil {
109
+
return nil, err
110
}
111
112
req, err := http.NewRequestWithContext(ctx, "GET", ustr, nil)
···
114
return nil, err
115
}
116
117
+
resp, err := cli.Do(req)
118
if err != nil {
119
return nil, err
120
}
···
122
123
if resp.StatusCode != 200 {
124
io.Copy(io.Discard, resp.Body)
125
+
return nil, fmt.Errorf("unable to find did doc at url. did: %s. url: %s", did, ustr)
126
}
127
128
var diddoc DidDoc
···
146
return nil, err
147
}
148
149
+
resp, err := cli.Do(req)
150
if err != nil {
151
return nil, err
152
}
+15
-5
identity/passport.go
+15
-5
identity/passport.go
···
19
type Passport struct {
20
h *http.Client
21
bc BackingCache
22
-
lk sync.Mutex
23
}
24
25
func NewPassport(h *http.Client, bc BackingCache) *Passport {
···
30
return &Passport{
31
h: h,
32
bc: bc,
33
-
lk: sync.Mutex{},
34
}
35
}
36
···
38
skipCache, _ := ctx.Value("skip-cache").(bool)
39
40
if !skipCache {
41
cached, ok := p.bc.GetDoc(did)
42
if ok {
43
return cached, nil
44
}
45
}
46
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
doc, err := FetchDidDoc(ctx, p.h, did)
51
if err != nil {
52
return nil, err
53
}
54
55
p.bc.PutDoc(did, doc)
56
57
return doc, nil
58
}
···
61
skipCache, _ := ctx.Value("skip-cache").(bool)
62
63
if !skipCache {
64
cached, ok := p.bc.GetDid(handle)
65
if ok {
66
return cached, nil
67
}
···
72
return "", err
73
}
74
75
p.bc.PutDid(handle, did)
76
77
return did, nil
78
}
79
80
func (p *Passport) BustDoc(ctx context.Context, did string) error {
81
return p.bc.BustDoc(did)
82
}
83
84
func (p *Passport) BustDid(ctx context.Context, handle string) error {
85
return p.bc.BustDid(handle)
86
}
···
19
type Passport struct {
20
h *http.Client
21
bc BackingCache
22
+
mu sync.RWMutex
23
}
24
25
func NewPassport(h *http.Client, bc BackingCache) *Passport {
···
30
return &Passport{
31
h: h,
32
bc: bc,
33
}
34
}
35
···
37
skipCache, _ := ctx.Value("skip-cache").(bool)
38
39
if !skipCache {
40
+
p.mu.RLock()
41
cached, ok := p.bc.GetDoc(did)
42
+
p.mu.RUnlock()
43
+
44
if ok {
45
return cached, nil
46
}
47
}
48
49
doc, err := FetchDidDoc(ctx, p.h, did)
50
if err != nil {
51
return nil, err
52
}
53
54
+
p.mu.Lock()
55
p.bc.PutDoc(did, doc)
56
+
p.mu.Unlock()
57
58
return doc, nil
59
}
···
62
skipCache, _ := ctx.Value("skip-cache").(bool)
63
64
if !skipCache {
65
+
p.mu.RLock()
66
cached, ok := p.bc.GetDid(handle)
67
+
p.mu.RUnlock()
68
+
69
if ok {
70
return cached, nil
71
}
···
76
return "", err
77
}
78
79
+
p.mu.Lock()
80
p.bc.PutDid(handle, did)
81
+
p.mu.Unlock()
82
83
return did, nil
84
}
85
86
func (p *Passport) BustDoc(ctx context.Context, did string) error {
87
+
p.mu.Lock()
88
+
defer p.mu.Unlock()
89
return p.bc.BustDoc(did)
90
}
91
92
func (p *Passport) BustDid(ctx context.Context, handle string) error {
93
+
p.mu.Lock()
94
+
defer p.mu.Unlock()
95
return p.bc.BustDid(handle)
96
}
+1
-1
identity/types.go
+1
-1
identity/types.go
+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
package db
2
3
import (
4
"sync"
5
6
"gorm.io/gorm"
···
19
}
20
}
21
22
-
func (db *DB) Create(value any, clauses []clause.Expression) *gorm.DB {
23
db.mu.Lock()
24
defer db.mu.Unlock()
25
-
return db.cli.Clauses(clauses...).Create(value)
26
}
27
28
-
func (db *DB) Exec(sql string, clauses []clause.Expression, values ...any) *gorm.DB {
29
db.mu.Lock()
30
defer db.mu.Unlock()
31
-
return db.cli.Clauses(clauses...).Exec(sql, values...)
32
}
33
34
-
func (db *DB) Raw(sql string, clauses []clause.Expression, values ...any) *gorm.DB {
35
-
return db.cli.Clauses(clauses...).Raw(sql, values...)
36
}
37
38
func (db *DB) AutoMigrate(models ...any) error {
39
return db.cli.AutoMigrate(models...)
40
}
41
42
-
func (db *DB) Delete(value any, clauses []clause.Expression) *gorm.DB {
43
db.mu.Lock()
44
defer db.mu.Unlock()
45
-
return db.cli.Clauses(clauses...).Delete(value)
46
}
47
48
-
func (db *DB) First(dest any, conds ...any) *gorm.DB {
49
-
return db.cli.First(dest, conds...)
50
}
51
52
// 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
// 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
// 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()
57
}
58
59
func (db *DB) Lock() {
···
1
package db
2
3
import (
4
+
"context"
5
"sync"
6
7
"gorm.io/gorm"
···
20
}
21
}
22
23
+
func (db *DB) Create(ctx context.Context, value any, clauses []clause.Expression) *gorm.DB {
24
db.mu.Lock()
25
defer db.mu.Unlock()
26
+
return db.cli.WithContext(ctx).Clauses(clauses...).Create(value)
27
}
28
29
+
func (db *DB) Save(ctx context.Context, value any, clauses []clause.Expression) *gorm.DB {
30
db.mu.Lock()
31
defer db.mu.Unlock()
32
+
return db.cli.WithContext(ctx).Clauses(clauses...).Save(value)
33
}
34
35
+
func (db *DB) Exec(ctx context.Context, sql string, clauses []clause.Expression, values ...any) *gorm.DB {
36
+
db.mu.Lock()
37
+
defer db.mu.Unlock()
38
+
return db.cli.WithContext(ctx).Clauses(clauses...).Exec(sql, values...)
39
+
}
40
+
41
+
func (db *DB) Raw(ctx context.Context, sql string, clauses []clause.Expression, values ...any) *gorm.DB {
42
+
return db.cli.WithContext(ctx).Clauses(clauses...).Raw(sql, values...)
43
}
44
45
func (db *DB) AutoMigrate(models ...any) error {
46
return db.cli.AutoMigrate(models...)
47
}
48
49
+
func (db *DB) Delete(ctx context.Context, value any, clauses []clause.Expression) *gorm.DB {
50
db.mu.Lock()
51
defer db.mu.Unlock()
52
+
return db.cli.WithContext(ctx).Clauses(clauses...).Delete(value)
53
}
54
55
+
func (db *DB) First(ctx context.Context, dest any, conds ...any) *gorm.DB {
56
+
return db.cli.WithContext(ctx).First(dest, conds...)
57
}
58
59
// TODO: this isn't actually good. we can commit even if the db is locked here. this is probably okay for the time being, but need to figure
60
// out a better solution. right now we only do this whenever we're importing a repo though so i'm mostly not worried, but it's still bad.
61
// e.g. when we do apply writes we should also be using a transcation but we don't right now
62
+
func (db *DB) BeginDangerously(ctx context.Context) *gorm.DB {
63
+
return db.cli.WithContext(ctx).Begin()
64
}
65
66
func (db *DB) Lock() {
+16
internal/helpers/helpers.go
+16
internal/helpers/helpers.go
···
32
return genericError(e, 400, msg)
33
}
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
+
51
func InvalidTokenError(e echo.Context) error {
52
return InputError(e, to.StringPtr("InvalidToken"))
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
"context"
5
"time"
6
7
-
"github.com/bluesky-social/indigo/atproto/crypto"
8
)
9
10
type Repo struct {
···
18
EmailUpdateCodeExpiresAt *time.Time
19
PasswordResetCode *string
20
PasswordResetCodeExpiresAt *time.Time
21
Password string
22
SigningKey []byte
23
Rev string
24
Root []byte
25
Preferences []byte
26
}
27
28
func (r *Repo) SignFor(ctx context.Context, did string, msg []byte) ([]byte, error) {
29
-
k, err := crypto.ParsePrivateBytesK256(r.SigningKey)
30
if err != nil {
31
return nil, err
32
}
···
39
return sig, nil
40
}
41
42
type Actor struct {
43
Did string `gorm:"primaryKey"`
44
Handle string `gorm:"uniqueIndex"`
···
92
Did string `gorm:"index;index:idx_blob_did_cid"`
93
Cid []byte `gorm:"index;index:idx_blob_did_cid"`
94
RefCount int
95
}
96
97
type BlobPart struct {
···
100
Idx int `gorm:"primaryKey"`
101
Data []byte
102
}
···
4
"context"
5
"time"
6
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")
16
)
17
18
type Repo struct {
···
26
EmailUpdateCodeExpiresAt *time.Time
27
PasswordResetCode *string
28
PasswordResetCodeExpiresAt *time.Time
29
+
PlcOperationCode *string
30
+
PlcOperationCodeExpiresAt *time.Time
31
+
AccountDeleteCode *string
32
+
AccountDeleteCodeExpiresAt *time.Time
33
Password string
34
SigningKey []byte
35
Rev string
36
Root []byte
37
Preferences []byte
38
+
Deactivated bool
39
+
TwoFactorCode *string
40
+
TwoFactorCodeExpiresAt *time.Time
41
+
TwoFactorType TwoFactorType `gorm:"default:none"`
42
}
43
44
func (r *Repo) SignFor(ctx context.Context, did string, msg []byte) ([]byte, error) {
45
+
k, err := atcrypto.ParsePrivateBytesK256(r.SigningKey)
46
if err != nil {
47
return nil, err
48
}
···
55
return sig, nil
56
}
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
+
70
type Actor struct {
71
Did string `gorm:"primaryKey"`
72
Handle string `gorm:"uniqueIndex"`
···
120
Did string `gorm:"index;index:idx_blob_did_cid"`
121
Cid []byte `gorm:"index;index:idx_blob_did_cid"`
122
RefCount int
123
+
Storage string `gorm:"default:sqlite"`
124
}
125
126
type BlobPart struct {
···
129
Idx int `gorm:"primaryKey"`
130
Data []byte
131
}
132
+
133
+
type ReservedKey struct {
134
+
KeyDid string `gorm:"primaryKey"`
135
+
Did *string `gorm:"index"`
136
+
PrivateKey []byte
137
+
CreatedAt time.Time `gorm:"index"`
138
+
}
+46
-23
oauth/client/manager.go
+46
-23
oauth/client/manager.go
···
22
cli *http.Client
23
logger *slog.Logger
24
jwksCache cache.Cache[string, jwk.Key]
25
-
metadataCache cache.Cache[string, Metadata]
26
}
27
28
type ManagerArgs struct {
···
40
}
41
42
jwksCache := cache.NewCache[string, jwk.Key]().WithLRU().WithMaxKeys(500).WithTTL(5 * time.Minute)
43
-
metadataCache := cache.NewCache[string, Metadata]().WithLRU().WithMaxKeys(500).WithTTL(5 * time.Minute)
44
45
return &Manager{
46
cli: args.Cli,
···
57
}
58
59
var jwks jwk.Key
60
-
if metadata.JWKS != nil {
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
66
}
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
}
76
77
return &Client{
···
81
}
82
83
func (cm *Manager) getClientMetadata(ctx context.Context, clientId string) (*Metadata, error) {
84
-
metadataCached, ok := cm.metadataCache.Get(clientId)
85
if !ok {
86
req, err := http.NewRequestWithContext(ctx, "GET", clientId, nil)
87
if err != nil {
···
109
return nil, err
110
}
111
112
return validated, nil
113
} else {
114
-
return &metadataCached, nil
115
}
116
}
117
···
196
return nil, fmt.Errorf("error unmarshaling metadata: %w", err)
197
}
198
199
u, err := url.Parse(metadata.ClientURI)
200
if err != nil {
201
return nil, fmt.Errorf("unable to parse client uri: %w", err)
202
}
203
204
if isLocalHostname(u.Hostname()) {
205
-
return nil, errors.New("`client_uri` hostname is invalid")
206
}
207
208
if metadata.Scope == "" {
···
262
return nil, errors.New("private_key_jwt auth method requires jwks or jwks_uri")
263
}
264
265
-
if metadata.JWKS != nil && len(*metadata.JWKS) == 0 {
266
return nil, errors.New("private_key_jwt auth method requires atleast one key in jwks")
267
}
268
···
341
if u.Scheme != "http" {
342
return nil, fmt.Errorf("loopback redirect uri %s must use http", ruri)
343
}
344
-
345
-
break
346
case u.Scheme == "http":
347
return nil, errors.New("only loopbvack redirect uris are allowed to use the `http` scheme")
348
case u.Scheme == "https":
349
if isLocalHostname(u.Hostname()) {
350
return nil, fmt.Errorf("redirect uri %s's domain must not be a local hostname", ruri)
351
}
352
-
break
353
case strings.Contains(u.Scheme, "."):
354
if metadata.ApplicationType != "native" {
355
return nil, errors.New("private-use uri scheme redirect uris are only allowed for native apps")
···
22
cli *http.Client
23
logger *slog.Logger
24
jwksCache cache.Cache[string, jwk.Key]
25
+
metadataCache cache.Cache[string, *Metadata]
26
}
27
28
type ManagerArgs struct {
···
40
}
41
42
jwksCache := cache.NewCache[string, jwk.Key]().WithLRU().WithMaxKeys(500).WithTTL(5 * time.Minute)
43
+
metadataCache := cache.NewCache[string, *Metadata]().WithLRU().WithMaxKeys(500).WithTTL(5 * time.Minute)
44
45
return &Manager{
46
cli: args.Cli,
···
57
}
58
59
var jwks jwk.Key
60
+
if metadata.TokenEndpointAuthMethod == "private_key_jwt" {
61
+
if metadata.JWKS != nil && len(metadata.JWKS.Keys) > 0 {
62
+
// TODO: this is kinda bad but whatever for now. there could obviously be more than one jwk, and we need to
63
+
// make sure we use the right one
64
+
b, err := json.Marshal(metadata.JWKS.Keys[0])
65
+
if err != nil {
66
+
return nil, err
67
+
}
68
+
69
+
k, err := helpers.ParseJWKFromBytes(b)
70
+
if err != nil {
71
+
return nil, err
72
+
}
73
+
74
+
jwks = k
75
+
} else if metadata.JWKSURI != nil {
76
+
maybeJwks, err := cm.getClientJwks(ctx, clientId, *metadata.JWKSURI)
77
+
if err != nil {
78
+
return nil, err
79
+
}
80
+
81
+
jwks = maybeJwks
82
+
} else {
83
+
return nil, fmt.Errorf("no valid jwks found in oauth client metadata")
84
}
85
}
86
87
return &Client{
···
91
}
92
93
func (cm *Manager) getClientMetadata(ctx context.Context, clientId string) (*Metadata, error) {
94
+
cached, ok := cm.metadataCache.Get(clientId)
95
if !ok {
96
req, err := http.NewRequestWithContext(ctx, "GET", clientId, nil)
97
if err != nil {
···
119
return nil, err
120
}
121
122
+
cm.metadataCache.Set(clientId, validated, 10*time.Minute)
123
+
124
return validated, nil
125
} else {
126
+
return cached, nil
127
}
128
}
129
···
208
return nil, fmt.Errorf("error unmarshaling metadata: %w", err)
209
}
210
211
+
if metadata.ClientURI == "" {
212
+
u, err := url.Parse(metadata.ClientID)
213
+
if err != nil {
214
+
return nil, fmt.Errorf("unable to parse client id: %w", err)
215
+
}
216
+
u.RawPath = ""
217
+
u.RawQuery = ""
218
+
metadata.ClientURI = u.String()
219
+
}
220
+
221
u, err := url.Parse(metadata.ClientURI)
222
if err != nil {
223
return nil, fmt.Errorf("unable to parse client uri: %w", err)
224
}
225
226
+
if metadata.ClientName == "" {
227
+
metadata.ClientName = metadata.ClientURI
228
+
}
229
+
230
if isLocalHostname(u.Hostname()) {
231
+
return nil, fmt.Errorf("`client_uri` hostname is invalid: %s", u.Hostname())
232
}
233
234
if metadata.Scope == "" {
···
288
return nil, errors.New("private_key_jwt auth method requires jwks or jwks_uri")
289
}
290
291
+
if metadata.JWKS != nil && len(metadata.JWKS.Keys) == 0 {
292
return nil, errors.New("private_key_jwt auth method requires atleast one key in jwks")
293
}
294
···
367
if u.Scheme != "http" {
368
return nil, fmt.Errorf("loopback redirect uri %s must use http", ruri)
369
}
370
case u.Scheme == "http":
371
return nil, errors.New("only loopbvack redirect uris are allowed to use the `http` scheme")
372
case u.Scheme == "https":
373
if isLocalHostname(u.Hostname()) {
374
return nil, fmt.Errorf("redirect uri %s's domain must not be a local hostname", ruri)
375
}
376
case strings.Contains(u.Scheme, "."):
377
if metadata.ApplicationType != "native" {
378
return nil, errors.New("private-use uri scheme redirect uris are only allowed for native apps")
+20
-16
oauth/client/metadata.go
+20
-16
oauth/client/metadata.go
···
1
package client
2
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"`
20
}
···
1
package client
2
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 *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"`
24
}
+1
-1
oauth/dpop/jti_cache.go
+1
-1
oauth/dpop/jti_cache.go
+6
-2
oauth/dpop/manager.go
+6
-2
oauth/dpop/manager.go
···
36
Hostname string
37
}
38
39
func NewManager(args ManagerArgs) *Manager {
40
if args.Logger == nil {
41
args.Logger = slog.Default()
···
194
nonce, _ := claims["nonce"].(string)
195
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")
198
}
199
200
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")
203
}
204
205
ath, _ := claims["ath"].(string)
···
36
Hostname string
37
}
38
39
+
var (
40
+
ErrUseDpopNonce = errors.New("use_dpop_nonce")
41
+
)
42
+
43
func NewManager(args ManagerArgs) *Manager {
44
if args.Logger == nil {
45
args.Logger = slog.Default()
···
198
nonce, _ := claims["nonce"].(string)
199
if nonce == "" {
200
// WARN: this _must_ be `use_dpop_nonce` for clients know they should make another request
201
+
return nil, ErrUseDpopNonce
202
}
203
204
if nonce != "" && !dm.nonce.Check(nonce) {
205
// WARN: this _must_ be `use_dpop_nonce` so that clients will fetch a new nonce
206
+
return nil, ErrUseDpopNonce
207
}
208
209
ath, _ := claims["ath"].(string)
+3
-2
oauth/dpop/nonce.go
+3
-2
oauth/dpop/nonce.go
+36
-20
plc/client.go
+36
-20
plc/client.go
···
13
"net/url"
14
"strings"
15
16
-
"github.com/bluesky-social/indigo/atproto/crypto"
17
"github.com/bluesky-social/indigo/util"
18
"github.com/haileyok/cocoon/identity"
19
)
···
22
h *http.Client
23
service string
24
pdsHostname string
25
-
rotationKey *crypto.PrivateKeyK256
26
}
27
28
type ClientArgs struct {
···
41
args.H = util.RobustHTTPClient()
42
}
43
44
-
rk, err := crypto.ParsePrivateBytesK256([]byte(args.RotationKey))
45
if err != nil {
46
return nil, err
47
}
···
54
}, nil
55
}
56
57
-
func (c *Client) CreateDID(sigkey *crypto.PrivateKeyK256, recovery string, handle string) (string, *Operation, error) {
58
-
pubsigkey, err := sigkey.PublicKey()
59
if err != nil {
60
return "", nil, err
61
}
62
63
-
pubrotkey, err := c.rotationKey.PublicKey()
64
if err != nil {
65
return "", nil, err
66
}
67
68
// todo
69
rotationKeys := []string{pubrotkey.DIDKey()}
70
if recovery != "" {
···
77
}(recovery)
78
}
79
80
-
op := Operation{
81
-
Type: "plc_operation",
82
VerificationMethods: map[string]string{
83
"atproto": pubsigkey.DIDKey(),
84
},
···
92
Endpoint: "https://" + c.pdsHostname,
93
},
94
},
95
-
Prev: nil,
96
}
97
98
-
if err := c.SignOp(sigkey, &op); err != nil {
99
-
return "", nil, err
100
-
}
101
-
102
-
did, err := DidFromOp(&op)
103
-
if err != nil {
104
-
return "", nil, err
105
-
}
106
-
107
-
return did, &op, nil
108
}
109
110
-
func (c *Client) SignOp(sigkey *crypto.PrivateKeyK256, op *Operation) error {
111
b, err := op.MarshalCBOR()
112
if err != nil {
113
return err
···
13
"net/url"
14
"strings"
15
16
+
"github.com/bluesky-social/indigo/atproto/atcrypto"
17
"github.com/bluesky-social/indigo/util"
18
"github.com/haileyok/cocoon/identity"
19
)
···
22
h *http.Client
23
service string
24
pdsHostname string
25
+
rotationKey *atcrypto.PrivateKeyK256
26
}
27
28
type ClientArgs struct {
···
41
args.H = util.RobustHTTPClient()
42
}
43
44
+
rk, err := atcrypto.ParsePrivateBytesK256([]byte(args.RotationKey))
45
if err != nil {
46
return nil, err
47
}
···
54
}, nil
55
}
56
57
+
func (c *Client) CreateDID(sigkey *atcrypto.PrivateKeyK256, recovery string, handle string) (string, *Operation, error) {
58
+
creds, err := c.CreateDidCredentials(sigkey, recovery, handle)
59
if err != nil {
60
return "", nil, err
61
}
62
63
+
op := Operation{
64
+
Type: "plc_operation",
65
+
VerificationMethods: creds.VerificationMethods,
66
+
RotationKeys: creds.RotationKeys,
67
+
AlsoKnownAs: creds.AlsoKnownAs,
68
+
Services: creds.Services,
69
+
Prev: nil,
70
+
}
71
+
72
+
if err := c.SignOp(sigkey, &op); err != nil {
73
+
return "", nil, err
74
+
}
75
+
76
+
did, err := DidFromOp(&op)
77
if err != nil {
78
return "", nil, err
79
}
80
81
+
return did, &op, nil
82
+
}
83
+
84
+
func (c *Client) CreateDidCredentials(sigkey *atcrypto.PrivateKeyK256, recovery string, handle string) (*DidCredentials, error) {
85
+
pubsigkey, err := sigkey.PublicKey()
86
+
if err != nil {
87
+
return nil, err
88
+
}
89
+
90
+
pubrotkey, err := c.rotationKey.PublicKey()
91
+
if err != nil {
92
+
return nil, err
93
+
}
94
+
95
// todo
96
rotationKeys := []string{pubrotkey.DIDKey()}
97
if recovery != "" {
···
104
}(recovery)
105
}
106
107
+
creds := DidCredentials{
108
VerificationMethods: map[string]string{
109
"atproto": pubsigkey.DIDKey(),
110
},
···
118
Endpoint: "https://" + c.pdsHostname,
119
},
120
},
121
}
122
123
+
return &creds, nil
124
}
125
126
+
func (c *Client) SignOp(sigkey *atcrypto.PrivateKeyK256, op *Operation) error {
127
b, err := op.MarshalCBOR()
128
if err != nil {
129
return err
+10
-2
plc/types.go
+10
-2
plc/types.go
···
3
import (
4
"encoding/json"
5
6
-
"github.com/bluesky-social/indigo/atproto/data"
7
"github.com/haileyok/cocoon/identity"
8
cbg "github.com/whyrusleeping/cbor-gen"
9
)
10
11
type Operation struct {
12
Type string `json:"type"`
···
38
return nil, err
39
}
40
41
-
b, err = data.MarshalCBOR(m)
42
if err != nil {
43
return nil, err
44
}
···
3
import (
4
"encoding/json"
5
6
+
"github.com/bluesky-social/indigo/atproto/atdata"
7
"github.com/haileyok/cocoon/identity"
8
cbg "github.com/whyrusleeping/cbor-gen"
9
)
10
+
11
+
12
+
type DidCredentials struct {
13
+
VerificationMethods map[string]string `json:"verificationMethods"`
14
+
RotationKeys []string `json:"rotationKeys"`
15
+
AlsoKnownAs []string `json:"alsoKnownAs"`
16
+
Services map[string]identity.OperationService `json:"services"`
17
+
}
18
19
type Operation struct {
20
Type string `json:"type"`
···
46
return nil, err
47
}
48
49
+
b, err = atdata.MarshalCBOR(m)
50
if err != nil {
51
return nil, err
52
}
+85
recording_blockstore/recording_blockstore.go
+85
recording_blockstore/recording_blockstore.go
···
···
1
+
package recording_blockstore
2
+
3
+
import (
4
+
"context"
5
+
"fmt"
6
+
7
+
blockformat "github.com/ipfs/go-block-format"
8
+
"github.com/ipfs/go-cid"
9
+
blockstore "github.com/ipfs/go-ipfs-blockstore"
10
+
)
11
+
12
+
type RecordingBlockstore struct {
13
+
base blockstore.Blockstore
14
+
15
+
inserts map[cid.Cid]blockformat.Block
16
+
reads map[cid.Cid]blockformat.Block
17
+
}
18
+
19
+
func New(base blockstore.Blockstore) *RecordingBlockstore {
20
+
return &RecordingBlockstore{
21
+
base: base,
22
+
inserts: make(map[cid.Cid]blockformat.Block),
23
+
reads: make(map[cid.Cid]blockformat.Block),
24
+
}
25
+
}
26
+
27
+
func (bs *RecordingBlockstore) Has(ctx context.Context, c cid.Cid) (bool, error) {
28
+
return bs.base.Has(ctx, c)
29
+
}
30
+
31
+
func (bs *RecordingBlockstore) Get(ctx context.Context, c cid.Cid) (blockformat.Block, error) {
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
38
+
}
39
+
40
+
func (bs *RecordingBlockstore) GetSize(ctx context.Context, c cid.Cid) (int, error) {
41
+
return bs.base.GetSize(ctx, c)
42
+
}
43
+
44
+
func (bs *RecordingBlockstore) DeleteBlock(ctx context.Context, c cid.Cid) error {
45
+
return bs.base.DeleteBlock(ctx, c)
46
+
}
47
+
48
+
func (bs *RecordingBlockstore) Put(ctx context.Context, block blockformat.Block) error {
49
+
if err := bs.base.Put(ctx, block); err != nil {
50
+
return err
51
+
}
52
+
bs.inserts[block.Cid()] = block
53
+
return nil
54
+
}
55
+
56
+
func (bs *RecordingBlockstore) PutMany(ctx context.Context, blocks []blockformat.Block) error {
57
+
if err := bs.base.PutMany(ctx, blocks); err != nil {
58
+
return err
59
+
}
60
+
61
+
for _, b := range blocks {
62
+
bs.inserts[b.Cid()] = b
63
+
}
64
+
65
+
return nil
66
+
}
67
+
68
+
func (bs *RecordingBlockstore) AllKeysChan(ctx context.Context) (<-chan cid.Cid, error) {
69
+
return nil, fmt.Errorf("iteration not allowed on recording blockstore")
70
+
}
71
+
72
+
func (bs *RecordingBlockstore) HashOnRead(enabled bool) {
73
+
}
74
+
75
+
func (bs *RecordingBlockstore) GetWriteLog() map[cid.Cid]blockformat.Block {
76
+
return bs.inserts
77
+
}
78
+
79
+
func (bs *RecordingBlockstore) GetReadLog() []blockformat.Block {
80
+
var blocks []blockformat.Block
81
+
for _, b := range bs.reads {
82
+
blocks = append(blocks, b)
83
+
}
84
+
return blocks
85
+
}
+30
server/blockstore_variant.go
+30
server/blockstore_variant.go
···
···
1
+
package server
2
+
3
+
import (
4
+
"github.com/haileyok/cocoon/sqlite_blockstore"
5
+
blockstore "github.com/ipfs/go-ipfs-blockstore"
6
+
)
7
+
8
+
type BlockstoreVariant int
9
+
10
+
const (
11
+
BlockstoreVariantSqlite = iota
12
+
)
13
+
14
+
func MustReturnBlockstoreVariant(maybeBsv string) BlockstoreVariant {
15
+
switch maybeBsv {
16
+
case "sqlite":
17
+
return BlockstoreVariantSqlite
18
+
default:
19
+
panic("invalid blockstore variant provided")
20
+
}
21
+
}
22
+
23
+
func (s *Server) getBlockstore(did string) blockstore.Blockstore {
24
+
switch s.config.BlockstoreVariant {
25
+
case BlockstoreVariantSqlite:
26
+
return sqlite_blockstore.New(did, s.db)
27
+
default:
28
+
return sqlite_blockstore.New(did, s.db)
29
+
}
30
+
}
+10
-8
server/common.go
+10
-8
server/common.go
···
1
package server
2
3
import (
4
"github.com/haileyok/cocoon/models"
5
)
6
7
-
func (s *Server) getActorByHandle(handle string) (*models.Actor, error) {
8
var actor models.Actor
9
-
if err := s.db.First(&actor, models.Actor{Handle: handle}).Error; err != nil {
10
return nil, err
11
}
12
return &actor, nil
13
}
14
15
-
func (s *Server) getRepoByEmail(email string) (*models.Repo, error) {
16
var repo models.Repo
17
-
if err := s.db.First(&repo, models.Repo{Email: email}).Error; err != nil {
18
return nil, err
19
}
20
return &repo, nil
21
}
22
23
-
func (s *Server) getRepoActorByEmail(email string) (*models.RepoActor, error) {
24
var repo models.RepoActor
25
-
if err := s.db.Raw("SELECT r.*, a.* FROM repos r LEFT JOIN actors a ON r.did = a.did WHERE r.email= ?", nil, email).Scan(&repo).Error; err != nil {
26
return nil, err
27
}
28
return &repo, nil
29
}
30
31
-
func (s *Server) getRepoActorByDid(did string) (*models.RepoActor, error) {
32
var repo models.RepoActor
33
-
if err := s.db.Raw("SELECT r.*, a.* FROM repos r LEFT JOIN actors a ON r.did = a.did WHERE r.did = ?", nil, did).Scan(&repo).Error; err != nil {
34
return nil, err
35
}
36
return &repo, nil
···
1
package server
2
3
import (
4
+
"context"
5
+
6
"github.com/haileyok/cocoon/models"
7
)
8
9
+
func (s *Server) getActorByHandle(ctx context.Context, handle string) (*models.Actor, error) {
10
var actor models.Actor
11
+
if err := s.db.First(ctx, &actor, models.Actor{Handle: handle}).Error; err != nil {
12
return nil, err
13
}
14
return &actor, nil
15
}
16
17
+
func (s *Server) getRepoByEmail(ctx context.Context, email string) (*models.Repo, error) {
18
var repo models.Repo
19
+
if err := s.db.First(ctx, &repo, models.Repo{Email: email}).Error; err != nil {
20
return nil, err
21
}
22
return &repo, nil
23
}
24
25
+
func (s *Server) getRepoActorByEmail(ctx context.Context, email string) (*models.RepoActor, error) {
26
var repo models.RepoActor
27
+
if err := s.db.Raw(ctx, "SELECT r.*, a.* FROM repos r LEFT JOIN actors a ON r.did = a.did WHERE r.email= ?", nil, email).Scan(&repo).Error; err != nil {
28
return nil, err
29
}
30
return &repo, nil
31
}
32
33
+
func (s *Server) getRepoActorByDid(ctx context.Context, did string) (*models.RepoActor, error) {
34
var repo models.RepoActor
35
+
if err := s.db.Raw(ctx, "SELECT r.*, a.* FROM repos r LEFT JOIN actors a ON r.did = a.did WHERE r.did = ?", nil, did).Scan(&repo).Error; err != nil {
36
return nil, err
37
}
38
return &repo, nil
+4
-2
server/handle_account.go
+4
-2
server/handle_account.go
···
12
13
func (s *Server) handleAccount(e echo.Context) error {
14
ctx := e.Request().Context()
15
repo, sess, err := s.getSessionRepoOrErr(e)
16
if err != nil {
17
return e.Redirect(303, "/account/signin")
···
20
oldestPossibleSession := time.Now().Add(constants.ConfidentialClientSessionLifetime)
21
22
var tokens []provider.OauthToken
23
-
if err := s.db.Raw("SELECT * FROM oauth_tokens WHERE sub = ? AND created_at < ? ORDER BY created_at ASC", nil, repo.Repo.Did, oldestPossibleSession).Scan(&tokens).Error; err != nil {
24
-
s.logger.Error("couldnt fetch oauth sessions for account", "did", repo.Repo.Did, "error", err)
25
sess.AddFlash("Unable to fetch sessions. See server logs for more details.", "error")
26
sess.Save(e.Request(), e.Response())
27
return e.Render(200, "account.html", map[string]any{
···
12
13
func (s *Server) handleAccount(e echo.Context) error {
14
ctx := e.Request().Context()
15
+
logger := s.logger.With("name", "handleAuth")
16
+
17
repo, sess, err := s.getSessionRepoOrErr(e)
18
if err != nil {
19
return e.Redirect(303, "/account/signin")
···
22
oldestPossibleSession := time.Now().Add(constants.ConfidentialClientSessionLifetime)
23
24
var tokens []provider.OauthToken
25
+
if err := s.db.Raw(ctx, "SELECT * FROM oauth_tokens WHERE sub = ? AND created_at < ? ORDER BY created_at ASC", nil, repo.Repo.Did, oldestPossibleSession).Scan(&tokens).Error; err != nil {
26
+
logger.Error("couldnt fetch oauth sessions for account", "did", repo.Repo.Did, "error", err)
27
sess.AddFlash("Unable to fetch sessions. See server logs for more details.", "error")
28
sess.Save(e.Request(), e.Response())
29
return e.Render(200, "account.html", map[string]any{
+8
-5
server/handle_account_revoke.go
+8
-5
server/handle_account_revoke.go
···
5
"github.com/labstack/echo/v4"
6
)
7
8
-
type AccountRevokeRequest struct {
9
Token string `form:"token"`
10
}
11
12
func (s *Server) handleAccountRevoke(e echo.Context) error {
13
-
var req AccountRevokeRequest
14
if err := e.Bind(&req); err != nil {
15
-
s.logger.Error("could not bind account revoke request", "error", err)
16
return helpers.ServerError(e, nil)
17
}
18
···
21
return e.Redirect(303, "/account/signin")
22
}
23
24
-
if err := s.db.Exec("DELETE FROM oauth_tokens WHERE sub = ? AND token = ?", nil, repo.Repo.Did, req.Token).Error; err != nil {
25
-
s.logger.Error("couldnt delete oauth session for account", "did", repo.Repo.Did, "token", req.Token, "error", err)
26
sess.AddFlash("Unable to revoke session. See server logs for more details.", "error")
27
sess.Save(e.Request(), e.Response())
28
return e.Redirect(303, "/account")
···
5
"github.com/labstack/echo/v4"
6
)
7
8
+
type AccountRevokeInput struct {
9
Token string `form:"token"`
10
}
11
12
func (s *Server) handleAccountRevoke(e echo.Context) error {
13
+
ctx := e.Request().Context()
14
+
logger := s.logger.With("name", "handleAcocuntRevoke")
15
+
16
+
var req AccountRevokeInput
17
if err := e.Bind(&req); err != nil {
18
+
logger.Error("could not bind account revoke request", "error", err)
19
return helpers.ServerError(e, nil)
20
}
21
···
24
return e.Redirect(303, "/account/signin")
25
}
26
27
+
if err := s.db.Exec(ctx, "DELETE FROM oauth_tokens WHERE sub = ? AND token = ?", nil, repo.Repo.Did, req.Token).Error; err != nil {
28
+
logger.Error("couldnt delete oauth session for account", "did", repo.Repo.Did, "token", req.Token, "error", err)
29
sess.AddFlash("Unable to revoke session. See server logs for more details.", "error")
30
sess.Save(e.Request(), e.Response())
31
return e.Redirect(303, "/account")
+68
-16
server/handle_account_signin.go
+68
-16
server/handle_account_signin.go
···
2
3
import (
4
"errors"
5
"strings"
6
7
"github.com/bluesky-social/indigo/atproto/syntax"
8
"github.com/gorilla/sessions"
···
14
"gorm.io/gorm"
15
)
16
17
-
type OauthSigninRequest struct {
18
-
Username string `form:"username"`
19
-
Password string `form:"password"`
20
-
QueryParams string `form:"query_params"`
21
}
22
23
func (s *Server) getSessionRepoOrErr(e echo.Context) (*models.RepoActor, *sessions.Session, error) {
24
sess, err := session.Get("session", e)
25
if err != nil {
26
return nil, nil, err
···
31
return nil, sess, errors.New("did was not set in session")
32
}
33
34
-
repo, err := s.getRepoActorByDid(did)
35
if err != nil {
36
return nil, sess, err
37
}
···
42
func getFlashesFromSession(e echo.Context, sess *sessions.Session) map[string]any {
43
defer sess.Save(e.Request(), e.Response())
44
return map[string]any{
45
-
"errors": sess.Flashes("error"),
46
-
"successes": sess.Flashes("success"),
47
}
48
}
49
···
60
}
61
62
func (s *Server) handleAccountSigninPost(e echo.Context) error {
63
-
var req OauthSigninRequest
64
if err := e.Bind(&req); err != nil {
65
-
s.logger.Error("error binding sign in req", "error", err)
66
return helpers.ServerError(e, nil)
67
}
68
···
76
idtype = "handle"
77
} else {
78
idtype = "email"
79
}
80
81
// TODO: we should make this a helper since we do it for the base create_session as well
···
83
var err error
84
switch idtype {
85
case "did":
86
-
err = s.db.Raw("SELECT r.*, a.* FROM repos r LEFT JOIN actors a ON r.did = a.did WHERE r.did = ?", nil, req.Username).Scan(&repo).Error
87
case "handle":
88
-
err = s.db.Raw("SELECT r.*, a.* FROM actors a LEFT JOIN repos r ON a.did = r.did WHERE a.handle = ?", nil, req.Username).Scan(&repo).Error
89
case "email":
90
-
err = s.db.Raw("SELECT r.*, a.* FROM repos r LEFT JOIN actors a ON r.did = a.did WHERE r.email = ?", nil, req.Username).Scan(&repo).Error
91
}
92
if err != nil {
93
if err == gorm.ErrRecordNotFound {
···
96
sess.AddFlash("Something went wrong!", "error")
97
}
98
sess.Save(e.Request(), e.Response())
99
-
return e.Redirect(303, "/account/signin")
100
}
101
102
if err := bcrypt.CompareHashAndPassword([]byte(repo.Password), []byte(req.Password)); err != nil {
···
106
sess.AddFlash("Something went wrong!", "error")
107
}
108
sess.Save(e.Request(), e.Response())
109
-
return e.Redirect(303, "/account/signin")
110
}
111
112
sess.Options = &sessions.Options{
···
122
return err
123
}
124
125
-
if req.QueryParams != "" {
126
-
return e.Redirect(303, "/oauth/authorize?"+req.QueryParams)
127
} else {
128
return e.Redirect(303, "/account")
129
}
···
2
3
import (
4
"errors"
5
+
"fmt"
6
"strings"
7
+
"time"
8
9
"github.com/bluesky-social/indigo/atproto/syntax"
10
"github.com/gorilla/sessions"
···
16
"gorm.io/gorm"
17
)
18
19
+
type OauthSigninInput struct {
20
+
Username string `form:"username"`
21
+
Password string `form:"password"`
22
+
AuthFactorToken string `form:"token"`
23
+
QueryParams string `form:"query_params"`
24
}
25
26
func (s *Server) getSessionRepoOrErr(e echo.Context) (*models.RepoActor, *sessions.Session, error) {
27
+
ctx := e.Request().Context()
28
+
29
sess, err := session.Get("session", e)
30
if err != nil {
31
return nil, nil, err
···
36
return nil, sess, errors.New("did was not set in session")
37
}
38
39
+
repo, err := s.getRepoActorByDid(ctx, did)
40
if err != nil {
41
return nil, sess, err
42
}
···
47
func getFlashesFromSession(e echo.Context, sess *sessions.Session) map[string]any {
48
defer sess.Save(e.Request(), e.Response())
49
return map[string]any{
50
+
"errors": sess.Flashes("error"),
51
+
"successes": sess.Flashes("success"),
52
+
"tokenrequired": sess.Flashes("tokenrequired"),
53
}
54
}
55
···
66
}
67
68
func (s *Server) handleAccountSigninPost(e echo.Context) error {
69
+
ctx := e.Request().Context()
70
+
logger := s.logger.With("name", "handleAccountSigninPost")
71
+
72
+
var req OauthSigninInput
73
if err := e.Bind(&req); err != nil {
74
+
logger.Error("error binding sign in req", "error", err)
75
return helpers.ServerError(e, nil)
76
}
77
···
85
idtype = "handle"
86
} else {
87
idtype = "email"
88
+
}
89
+
90
+
queryParams := ""
91
+
if req.QueryParams != "" {
92
+
queryParams = fmt.Sprintf("?%s", req.QueryParams)
93
}
94
95
// TODO: we should make this a helper since we do it for the base create_session as well
···
97
var err error
98
switch idtype {
99
case "did":
100
+
err = s.db.Raw(ctx, "SELECT r.*, a.* FROM repos r LEFT JOIN actors a ON r.did = a.did WHERE r.did = ?", nil, req.Username).Scan(&repo).Error
101
case "handle":
102
+
err = s.db.Raw(ctx, "SELECT r.*, a.* FROM actors a LEFT JOIN repos r ON a.did = r.did WHERE a.handle = ?", nil, req.Username).Scan(&repo).Error
103
case "email":
104
+
err = s.db.Raw(ctx, "SELECT r.*, a.* FROM repos r LEFT JOIN actors a ON r.did = a.did WHERE r.email = ?", nil, req.Username).Scan(&repo).Error
105
}
106
if err != nil {
107
if err == gorm.ErrRecordNotFound {
···
110
sess.AddFlash("Something went wrong!", "error")
111
}
112
sess.Save(e.Request(), e.Response())
113
+
return e.Redirect(303, "/account/signin"+queryParams)
114
}
115
116
if err := bcrypt.CompareHashAndPassword([]byte(repo.Password), []byte(req.Password)); err != nil {
···
120
sess.AddFlash("Something went wrong!", "error")
121
}
122
sess.Save(e.Request(), e.Response())
123
+
return e.Redirect(303, "/account/signin"+queryParams)
124
+
}
125
+
126
+
// if repo requires 2FA token and one hasn't been provided, return error prompting for one
127
+
if repo.TwoFactorType != models.TwoFactorTypeNone && req.AuthFactorToken == "" {
128
+
err = s.createAndSendTwoFactorCode(ctx, repo)
129
+
if err != nil {
130
+
sess.AddFlash("Something went wrong!", "error")
131
+
sess.Save(e.Request(), e.Response())
132
+
return e.Redirect(303, "/account/signin"+queryParams)
133
+
}
134
+
135
+
sess.AddFlash("requires 2FA token", "tokenrequired")
136
+
sess.Save(e.Request(), e.Response())
137
+
return e.Redirect(303, "/account/signin"+queryParams)
138
+
}
139
+
140
+
// if 2FAis required, now check that the one provided is valid
141
+
if repo.TwoFactorType != models.TwoFactorTypeNone {
142
+
if repo.TwoFactorCode == nil || repo.TwoFactorCodeExpiresAt == nil {
143
+
err = s.createAndSendTwoFactorCode(ctx, repo)
144
+
if err != nil {
145
+
sess.AddFlash("Something went wrong!", "error")
146
+
sess.Save(e.Request(), e.Response())
147
+
return e.Redirect(303, "/account/signin"+queryParams)
148
+
}
149
+
150
+
sess.AddFlash("requires 2FA token", "tokenrequired")
151
+
sess.Save(e.Request(), e.Response())
152
+
return e.Redirect(303, "/account/signin"+queryParams)
153
+
}
154
+
155
+
if *repo.TwoFactorCode != req.AuthFactorToken {
156
+
return helpers.InvalidTokenError(e)
157
+
}
158
+
159
+
if time.Now().UTC().After(*repo.TwoFactorCodeExpiresAt) {
160
+
return helpers.ExpiredTokenError(e)
161
+
}
162
}
163
164
sess.Options = &sessions.Options{
···
174
return err
175
}
176
177
+
if queryParams != "" {
178
+
return e.Redirect(303, "/oauth/authorize"+queryParams)
179
} else {
180
return e.Redirect(303, "/account")
181
}
+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
// This is kinda lame. Not great to implement app.bsky in the pds, but alas
11
12
func (s *Server) handleActorPutPreferences(e echo.Context) error {
13
repo := e.Get("repo").(*models.RepoActor)
14
15
var prefs map[string]any
···
22
return err
23
}
24
25
-
if err := s.db.Exec("UPDATE repos SET preferences = ? WHERE did = ?", nil, b, repo.Repo.Did).Error; err != nil {
26
return err
27
}
28
···
10
// This is kinda lame. Not great to implement app.bsky in the pds, but alas
11
12
func (s *Server) handleActorPutPreferences(e echo.Context) error {
13
+
ctx := e.Request().Context()
14
+
15
repo := e.Get("repo").(*models.RepoActor)
16
17
var prefs map[string]any
···
24
return err
25
}
26
27
+
if err := s.db.Exec(ctx, "UPDATE repos SET preferences = ? WHERE did = ?", nil, b, repo.Repo.Did).Error; err != nil {
28
return err
29
}
30
+26
server/handle_identity_get_recommended_did_credentials.go
+26
server/handle_identity_get_recommended_did_credentials.go
···
···
1
+
package server
2
+
3
+
import (
4
+
"github.com/bluesky-social/indigo/atproto/atcrypto"
5
+
"github.com/haileyok/cocoon/internal/helpers"
6
+
"github.com/haileyok/cocoon/models"
7
+
"github.com/labstack/echo/v4"
8
+
)
9
+
10
+
func (s *Server) handleGetRecommendedDidCredentials(e echo.Context) error {
11
+
logger := s.logger.With("name", "handleIdentityGetRecommendedDidCredentials")
12
+
13
+
repo := e.Get("repo").(*models.RepoActor)
14
+
k, err := atcrypto.ParsePrivateBytesK256(repo.SigningKey)
15
+
if err != nil {
16
+
logger.Error("error parsing key", "error", err)
17
+
return helpers.ServerError(e, nil)
18
+
}
19
+
creds, err := s.plcClient.CreateDidCredentials(k, "", repo.Actor.Handle)
20
+
if err != nil {
21
+
logger.Error("error crating did credentials", "error", err)
22
+
return helpers.ServerError(e, nil)
23
+
}
24
+
25
+
return e.JSON(200, creds)
26
+
}
+32
server/handle_identity_request_plc_operation.go
+32
server/handle_identity_request_plc_operation.go
···
···
1
+
package server
2
+
3
+
import (
4
+
"fmt"
5
+
"time"
6
+
7
+
"github.com/haileyok/cocoon/internal/helpers"
8
+
"github.com/haileyok/cocoon/models"
9
+
"github.com/labstack/echo/v4"
10
+
)
11
+
12
+
func (s *Server) handleIdentityRequestPlcOperationSignature(e echo.Context) error {
13
+
ctx := e.Request().Context()
14
+
logger := s.logger.With("name", "handleIdentityRequestPlcOperationSignature")
15
+
16
+
urepo := e.Get("repo").(*models.RepoActor)
17
+
18
+
code := fmt.Sprintf("%s-%s", helpers.RandomVarchar(5), helpers.RandomVarchar(5))
19
+
eat := time.Now().Add(10 * time.Minute).UTC()
20
+
21
+
if err := s.db.Exec(ctx, "UPDATE repos SET plc_operation_code = ?, plc_operation_code_expires_at = ? WHERE did = ?", nil, code, eat, urepo.Repo.Did).Error; err != nil {
22
+
logger.Error("error updating user", "error", err)
23
+
return helpers.ServerError(e, nil)
24
+
}
25
+
26
+
if err := s.sendPlcTokenReset(urepo.Email, urepo.Handle, code); err != nil {
27
+
logger.Error("error sending mail", "error", err)
28
+
return helpers.ServerError(e, nil)
29
+
}
30
+
31
+
return e.NoContent(200)
32
+
}
+105
server/handle_identity_sign_plc_operation.go
+105
server/handle_identity_sign_plc_operation.go
···
···
1
+
package server
2
+
3
+
import (
4
+
"context"
5
+
"strings"
6
+
"time"
7
+
8
+
"github.com/Azure/go-autorest/autorest/to"
9
+
"github.com/bluesky-social/indigo/atproto/atcrypto"
10
+
"github.com/haileyok/cocoon/identity"
11
+
"github.com/haileyok/cocoon/internal/helpers"
12
+
"github.com/haileyok/cocoon/models"
13
+
"github.com/haileyok/cocoon/plc"
14
+
"github.com/labstack/echo/v4"
15
+
)
16
+
17
+
type ComAtprotoSignPlcOperationRequest struct {
18
+
Token string `json:"token"`
19
+
VerificationMethods *map[string]string `json:"verificationMethods"`
20
+
RotationKeys *[]string `json:"rotationKeys"`
21
+
AlsoKnownAs *[]string `json:"alsoKnownAs"`
22
+
Services *map[string]identity.OperationService `json:"services"`
23
+
}
24
+
25
+
type ComAtprotoSignPlcOperationResponse struct {
26
+
Operation plc.Operation `json:"operation"`
27
+
}
28
+
29
+
func (s *Server) handleSignPlcOperation(e echo.Context) error {
30
+
logger := s.logger.With("name", "handleSignPlcOperation")
31
+
32
+
repo := e.Get("repo").(*models.RepoActor)
33
+
34
+
var req ComAtprotoSignPlcOperationRequest
35
+
if err := e.Bind(&req); err != nil {
36
+
logger.Error("error binding", "error", err)
37
+
return helpers.ServerError(e, nil)
38
+
}
39
+
40
+
if !strings.HasPrefix(repo.Repo.Did, "did:plc:") {
41
+
return helpers.InputError(e, nil)
42
+
}
43
+
44
+
if repo.PlcOperationCode == nil || repo.PlcOperationCodeExpiresAt == nil {
45
+
return helpers.InputError(e, to.StringPtr("InvalidToken"))
46
+
}
47
+
48
+
if *repo.PlcOperationCode != req.Token {
49
+
return helpers.InvalidTokenError(e)
50
+
}
51
+
52
+
if time.Now().UTC().After(*repo.PlcOperationCodeExpiresAt) {
53
+
return helpers.ExpiredTokenError(e)
54
+
}
55
+
56
+
ctx := context.WithValue(e.Request().Context(), "skip-cache", true)
57
+
log, err := identity.FetchDidAuditLog(ctx, nil, repo.Repo.Did)
58
+
if err != nil {
59
+
logger.Error("error fetching doc", "error", err)
60
+
return helpers.ServerError(e, nil)
61
+
}
62
+
63
+
latest := log[len(log)-1]
64
+
65
+
op := plc.Operation{
66
+
Type: "plc_operation",
67
+
VerificationMethods: latest.Operation.VerificationMethods,
68
+
RotationKeys: latest.Operation.RotationKeys,
69
+
AlsoKnownAs: latest.Operation.AlsoKnownAs,
70
+
Services: latest.Operation.Services,
71
+
Prev: &latest.Cid,
72
+
}
73
+
if req.VerificationMethods != nil {
74
+
op.VerificationMethods = *req.VerificationMethods
75
+
}
76
+
if req.RotationKeys != nil {
77
+
op.RotationKeys = *req.RotationKeys
78
+
}
79
+
if req.AlsoKnownAs != nil {
80
+
op.AlsoKnownAs = *req.AlsoKnownAs
81
+
}
82
+
if req.Services != nil {
83
+
op.Services = *req.Services
84
+
}
85
+
86
+
k, err := atcrypto.ParsePrivateBytesK256(repo.SigningKey)
87
+
if err != nil {
88
+
logger.Error("error parsing signing key", "error", err)
89
+
return helpers.ServerError(e, nil)
90
+
}
91
+
92
+
if err := s.plcClient.SignOp(k, &op); err != nil {
93
+
logger.Error("error signing plc operation", "error", err)
94
+
return helpers.ServerError(e, nil)
95
+
}
96
+
97
+
if err := s.db.Exec(ctx, "UPDATE repos SET plc_operation_code = NULL, plc_operation_code_expires_at = NULL WHERE did = ?", nil, repo.Repo.Did).Error; err != nil {
98
+
logger.Error("error updating repo", "error", err)
99
+
return helpers.ServerError(e, nil)
100
+
}
101
+
102
+
return e.JSON(200, ComAtprotoSignPlcOperationResponse{
103
+
Operation: op,
104
+
})
105
+
}
+89
server/handle_identity_submit_plc_operation.go
+89
server/handle_identity_submit_plc_operation.go
···
···
1
+
package server
2
+
3
+
import (
4
+
"context"
5
+
"slices"
6
+
"strings"
7
+
"time"
8
+
9
+
"github.com/bluesky-social/indigo/api/atproto"
10
+
"github.com/bluesky-social/indigo/atproto/atcrypto"
11
+
"github.com/bluesky-social/indigo/events"
12
+
"github.com/bluesky-social/indigo/util"
13
+
"github.com/haileyok/cocoon/internal/helpers"
14
+
"github.com/haileyok/cocoon/models"
15
+
"github.com/haileyok/cocoon/plc"
16
+
"github.com/labstack/echo/v4"
17
+
)
18
+
19
+
type ComAtprotoSubmitPlcOperationRequest struct {
20
+
Operation plc.Operation `json:"operation"`
21
+
}
22
+
23
+
func (s *Server) handleSubmitPlcOperation(e echo.Context) error {
24
+
logger := s.logger.With("name", "handleIdentitySubmitPlcOperation")
25
+
26
+
repo := e.Get("repo").(*models.RepoActor)
27
+
28
+
var req ComAtprotoSubmitPlcOperationRequest
29
+
if err := e.Bind(&req); err != nil {
30
+
logger.Error("error binding", "error", err)
31
+
return helpers.ServerError(e, nil)
32
+
}
33
+
34
+
if err := e.Validate(req); err != nil {
35
+
return helpers.InputError(e, nil)
36
+
}
37
+
if !strings.HasPrefix(repo.Repo.Did, "did:plc:") {
38
+
return helpers.InputError(e, nil)
39
+
}
40
+
41
+
op := req.Operation
42
+
43
+
k, err := atcrypto.ParsePrivateBytesK256(repo.SigningKey)
44
+
if err != nil {
45
+
logger.Error("error parsing key", "error", err)
46
+
return helpers.ServerError(e, nil)
47
+
}
48
+
required, err := s.plcClient.CreateDidCredentials(k, "", repo.Actor.Handle)
49
+
if err != nil {
50
+
logger.Error("error crating did credentials", "error", err)
51
+
return helpers.ServerError(e, nil)
52
+
}
53
+
54
+
for _, expectedKey := range required.RotationKeys {
55
+
if !slices.Contains(op.RotationKeys, expectedKey) {
56
+
return helpers.InputError(e, nil)
57
+
}
58
+
}
59
+
if op.Services["atproto_pds"].Type != "AtprotoPersonalDataServer" {
60
+
return helpers.InputError(e, nil)
61
+
}
62
+
if op.Services["atproto_pds"].Endpoint != required.Services["atproto_pds"].Endpoint {
63
+
return helpers.InputError(e, nil)
64
+
}
65
+
if op.VerificationMethods["atproto"] != required.VerificationMethods["atproto"] {
66
+
return helpers.InputError(e, nil)
67
+
}
68
+
if op.AlsoKnownAs[0] != required.AlsoKnownAs[0] {
69
+
return helpers.InputError(e, nil)
70
+
}
71
+
72
+
if err := s.plcClient.SendOperation(e.Request().Context(), repo.Repo.Did, &op); err != nil {
73
+
return err
74
+
}
75
+
76
+
if err := s.passport.BustDoc(context.TODO(), repo.Repo.Did); err != nil {
77
+
logger.Warn("error busting did doc", "error", err)
78
+
}
79
+
80
+
s.evtman.AddEvent(context.TODO(), &events.XRPCStreamEvent{
81
+
RepoIdentity: &atproto.SyncSubscribeRepos_Identity{
82
+
Did: repo.Repo.Did,
83
+
Seq: time.Now().UnixMicro(), // TODO: no
84
+
Time: time.Now().Format(util.ISO8601),
85
+
},
86
+
})
87
+
88
+
return nil
89
+
}
+10
-17
server/handle_identity_update_handle.go
+10
-17
server/handle_identity_update_handle.go
···
7
8
"github.com/Azure/go-autorest/autorest/to"
9
"github.com/bluesky-social/indigo/api/atproto"
10
-
"github.com/bluesky-social/indigo/atproto/crypto"
11
"github.com/bluesky-social/indigo/events"
12
"github.com/bluesky-social/indigo/util"
13
"github.com/haileyok/cocoon/identity"
···
22
}
23
24
func (s *Server) handleIdentityUpdateHandle(e echo.Context) error {
25
repo := e.Get("repo").(*models.RepoActor)
26
27
var req ComAtprotoIdentityUpdateHandleRequest
28
if err := e.Bind(&req); err != nil {
29
-
s.logger.Error("error binding", "error", err)
30
return helpers.ServerError(e, nil)
31
}
32
···
41
if strings.HasPrefix(repo.Repo.Did, "did:plc:") {
42
log, err := identity.FetchDidAuditLog(ctx, nil, repo.Repo.Did)
43
if err != nil {
44
-
s.logger.Error("error fetching doc", "error", err)
45
return helpers.ServerError(e, nil)
46
}
47
···
66
Prev: &latest.Cid,
67
}
68
69
-
k, err := crypto.ParsePrivateBytesK256(repo.SigningKey)
70
if err != nil {
71
-
s.logger.Error("error parsing signing key", "error", err)
72
return helpers.ServerError(e, nil)
73
}
74
···
82
}
83
84
if err := s.passport.BustDoc(context.TODO(), repo.Repo.Did); err != nil {
85
-
s.logger.Warn("error busting did doc", "error", err)
86
}
87
88
s.evtman.AddEvent(context.TODO(), &events.XRPCStreamEvent{
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
RepoIdentity: &atproto.SyncSubscribeRepos_Identity{
99
Did: repo.Repo.Did,
100
Handle: to.StringPtr(req.Handle),
···
103
},
104
})
105
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)
108
return helpers.ServerError(e, nil)
109
}
110
···
7
8
"github.com/Azure/go-autorest/autorest/to"
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/identity"
···
22
}
23
24
func (s *Server) handleIdentityUpdateHandle(e echo.Context) error {
25
+
logger := s.logger.With("name", "handleIdentityUpdateHandle")
26
+
27
repo := e.Get("repo").(*models.RepoActor)
28
29
var req ComAtprotoIdentityUpdateHandleRequest
30
if err := e.Bind(&req); err != nil {
31
+
logger.Error("error binding", "error", err)
32
return helpers.ServerError(e, nil)
33
}
34
···
43
if strings.HasPrefix(repo.Repo.Did, "did:plc:") {
44
log, err := identity.FetchDidAuditLog(ctx, nil, repo.Repo.Did)
45
if err != nil {
46
+
logger.Error("error fetching doc", "error", err)
47
return helpers.ServerError(e, nil)
48
}
49
···
68
Prev: &latest.Cid,
69
}
70
71
+
k, err := atcrypto.ParsePrivateBytesK256(repo.SigningKey)
72
if err != nil {
73
+
logger.Error("error parsing signing key", "error", err)
74
return helpers.ServerError(e, nil)
75
}
76
···
84
}
85
86
if err := s.passport.BustDoc(context.TODO(), repo.Repo.Did); err != nil {
87
+
logger.Warn("error busting did doc", "error", err)
88
}
89
90
s.evtman.AddEvent(context.TODO(), &events.XRPCStreamEvent{
91
RepoIdentity: &atproto.SyncSubscribeRepos_Identity{
92
Did: repo.Repo.Did,
93
Handle: to.StringPtr(req.Handle),
···
96
},
97
})
98
99
+
if err := s.db.Exec(ctx, "UPDATE actors SET handle = ? WHERE did = ?", nil, req.Handle, repo.Repo.Did).Error; err != nil {
100
+
logger.Error("error updating handle in db", "error", err)
101
return helpers.ServerError(e, nil)
102
}
103
+17
-15
server/handle_import_repo.go
+17
-15
server/handle_import_repo.go
···
9
10
"github.com/bluesky-social/indigo/atproto/syntax"
11
"github.com/bluesky-social/indigo/repo"
12
-
"github.com/haileyok/cocoon/blockstore"
13
"github.com/haileyok/cocoon/internal/helpers"
14
"github.com/haileyok/cocoon/models"
15
blocks "github.com/ipfs/go-block-format"
···
19
)
20
21
func (s *Server) handleRepoImportRepo(e echo.Context) error {
22
urepo := e.Get("repo").(*models.RepoActor)
23
24
b, err := io.ReadAll(e.Request().Body)
25
if err != nil {
26
-
s.logger.Error("could not read bytes in import request", "error", err)
27
return helpers.ServerError(e, nil)
28
}
29
30
-
bs := blockstore.New(urepo.Repo.Did, s.db)
31
32
cs, err := car.NewCarReader(bytes.NewReader(b))
33
if err != nil {
34
-
s.logger.Error("could not read car in import request", "error", err)
35
return helpers.ServerError(e, nil)
36
}
37
38
orderedBlocks := []blocks.Block{}
39
currBlock, err := cs.Next()
40
if err != nil {
41
-
s.logger.Error("could not get first block from car", "error", err)
42
return helpers.ServerError(e, nil)
43
}
44
currBlockCt := 1
45
46
for currBlock != nil {
47
-
s.logger.Info("someone is importing their repo", "block", currBlockCt)
48
orderedBlocks = append(orderedBlocks, currBlock)
49
next, _ := cs.Next()
50
currBlock = next
···
54
slices.Reverse(orderedBlocks)
55
56
if err := bs.PutMany(context.TODO(), orderedBlocks); err != nil {
57
-
s.logger.Error("could not insert blocks", "error", err)
58
return helpers.ServerError(e, nil)
59
}
60
61
r, err := repo.OpenRepo(context.TODO(), bs, cs.Header.Roots[0])
62
if err != nil {
63
-
s.logger.Error("could not open repo", "error", err)
64
return helpers.ServerError(e, nil)
65
}
66
67
-
tx := s.db.BeginDangerously()
68
69
clock := syntax.NewTIDClock(0)
70
···
75
cidStr := cid.String()
76
b, err := bs.Get(context.TODO(), cid)
77
if err != nil {
78
-
s.logger.Error("record bytes don't exist in blockstore", "error", err)
79
return helpers.ServerError(e, nil)
80
}
81
···
88
Value: b.RawData(),
89
}
90
91
-
if err := tx.Create(rec).Error; err != nil {
92
return err
93
}
94
95
return nil
96
}); err != nil {
97
tx.Rollback()
98
-
s.logger.Error("record bytes don't exist in blockstore", "error", err)
99
return helpers.ServerError(e, nil)
100
}
101
···
103
104
root, rev, err := r.Commit(context.TODO(), urepo.SignFor)
105
if err != nil {
106
-
s.logger.Error("error committing", "error", err)
107
return helpers.ServerError(e, nil)
108
}
109
110
-
if err := bs.UpdateRepo(context.TODO(), root, rev); err != nil {
111
-
s.logger.Error("error updating repo after commit", "error", err)
112
return helpers.ServerError(e, nil)
113
}
114
···
9
10
"github.com/bluesky-social/indigo/atproto/syntax"
11
"github.com/bluesky-social/indigo/repo"
12
"github.com/haileyok/cocoon/internal/helpers"
13
"github.com/haileyok/cocoon/models"
14
blocks "github.com/ipfs/go-block-format"
···
18
)
19
20
func (s *Server) handleRepoImportRepo(e echo.Context) error {
21
+
ctx := e.Request().Context()
22
+
logger := s.logger.With("name", "handleImportRepo")
23
+
24
urepo := e.Get("repo").(*models.RepoActor)
25
26
b, err := io.ReadAll(e.Request().Body)
27
if err != nil {
28
+
logger.Error("could not read bytes in import request", "error", err)
29
return helpers.ServerError(e, nil)
30
}
31
32
+
bs := s.getBlockstore(urepo.Repo.Did)
33
34
cs, err := car.NewCarReader(bytes.NewReader(b))
35
if err != nil {
36
+
logger.Error("could not read car in import request", "error", err)
37
return helpers.ServerError(e, nil)
38
}
39
40
orderedBlocks := []blocks.Block{}
41
currBlock, err := cs.Next()
42
if err != nil {
43
+
logger.Error("could not get first block from car", "error", err)
44
return helpers.ServerError(e, nil)
45
}
46
currBlockCt := 1
47
48
for currBlock != nil {
49
+
logger.Info("someone is importing their repo", "block", currBlockCt)
50
orderedBlocks = append(orderedBlocks, currBlock)
51
next, _ := cs.Next()
52
currBlock = next
···
56
slices.Reverse(orderedBlocks)
57
58
if err := bs.PutMany(context.TODO(), orderedBlocks); err != nil {
59
+
logger.Error("could not insert blocks", "error", err)
60
return helpers.ServerError(e, nil)
61
}
62
63
r, err := repo.OpenRepo(context.TODO(), bs, cs.Header.Roots[0])
64
if err != nil {
65
+
logger.Error("could not open repo", "error", err)
66
return helpers.ServerError(e, nil)
67
}
68
69
+
tx := s.db.BeginDangerously(ctx)
70
71
clock := syntax.NewTIDClock(0)
72
···
77
cidStr := cid.String()
78
b, err := bs.Get(context.TODO(), cid)
79
if err != nil {
80
+
logger.Error("record bytes don't exist in blockstore", "error", err)
81
return helpers.ServerError(e, nil)
82
}
83
···
90
Value: b.RawData(),
91
}
92
93
+
if err := tx.Save(rec).Error; err != nil {
94
return err
95
}
96
97
return nil
98
}); err != nil {
99
tx.Rollback()
100
+
logger.Error("record bytes don't exist in blockstore", "error", err)
101
return helpers.ServerError(e, nil)
102
}
103
···
105
106
root, rev, err := r.Commit(context.TODO(), urepo.SignFor)
107
if err != nil {
108
+
logger.Error("error committing", "error", err)
109
return helpers.ServerError(e, nil)
110
}
111
112
+
if err := s.UpdateRepo(context.TODO(), urepo.Repo.Did, root, rev); err != nil {
113
+
logger.Error("error updating repo after commit", "error", err)
114
return helpers.ServerError(e, nil)
115
}
116
+34
server/handle_label_query_labels.go
+34
server/handle_label_query_labels.go
···
···
1
+
package server
2
+
3
+
import (
4
+
"github.com/labstack/echo/v4"
5
+
)
6
+
7
+
type Label struct {
8
+
Ver *int `json:"ver,omitempty"`
9
+
Src string `json:"src"`
10
+
Uri string `json:"uri"`
11
+
Cid *string `json:"cid,omitempty"`
12
+
Val string `json:"val"`
13
+
Neg *bool `json:"neg,omitempty"`
14
+
Cts string `json:"cts"`
15
+
Exp *string `json:"exp,omitempty"`
16
+
Sig []byte `json:"sig,omitempty"`
17
+
}
18
+
19
+
type ComAtprotoLabelQueryLabelsResponse struct {
20
+
Cursor *string `json:"cursor,omitempty"`
21
+
Labels []Label `json:"labels"`
22
+
}
23
+
24
+
func (s *Server) handleLabelQueryLabels(e echo.Context) error {
25
+
svc := e.Request().Header.Get("atproto-proxy")
26
+
if svc != "" || s.config.FallbackProxy != "" {
27
+
return s.handleProxy(e)
28
+
}
29
+
30
+
return e.JSON(200, ComAtprotoLabelQueryLabelsResponse{
31
+
Cursor: nil,
32
+
Labels: []Label{},
33
+
})
34
+
}
+24
-9
server/handle_oauth_par.go
+24
-9
server/handle_oauth_par.go
···
1
package server
2
3
import (
4
"time"
5
6
"github.com/Azure/go-autorest/autorest/to"
7
"github.com/haileyok/cocoon/internal/helpers"
8
"github.com/haileyok/cocoon/oauth"
9
"github.com/haileyok/cocoon/oauth/constants"
10
"github.com/haileyok/cocoon/oauth/provider"
11
"github.com/labstack/echo/v4"
12
)
···
17
}
18
19
func (s *Server) handleOauthPar(e echo.Context) error {
20
var parRequest provider.ParRequest
21
if err := e.Bind(&parRequest); err != nil {
22
-
s.logger.Error("error binding for par request", "error", err)
23
return helpers.ServerError(e, nil)
24
}
25
26
if err := e.Validate(parRequest); err != nil {
27
-
s.logger.Error("missing parameters for par request", "error", err)
28
return helpers.InputError(e, nil)
29
}
30
31
// TODO: this seems wrong. should be a way to get the entire request url i believe, but this will work for now
32
dpopProof, err := s.oauthProvider.DpopManager.CheckProof(e.Request().Method, "https://"+s.config.Hostname+e.Request().URL.String(), e.Request().Header, nil)
33
if err != nil {
34
-
s.logger.Error("error getting dpop proof", "error", err)
35
-
return helpers.InputError(e, to.StringPtr(err.Error()))
36
}
37
38
client, clientAuth, err := s.oauthProvider.AuthenticateClient(e.Request().Context(), parRequest.AuthenticateClientRequestBase, dpopProof, &provider.AuthenticateClientOptions{
···
41
AllowMissingDpopProof: true,
42
})
43
if err != nil {
44
-
s.logger.Error("error authenticating client", "error", err)
45
return helpers.InputError(e, to.StringPtr(err.Error()))
46
}
47
···
52
} else {
53
if !client.Metadata.DpopBoundAccessTokens {
54
msg := "dpop bound access tokens are not enabled for this client"
55
-
s.logger.Error(msg)
56
return helpers.InputError(e, &msg)
57
}
58
59
if dpopProof.JKT != *parRequest.DpopJkt {
60
msg := "supplied dpop jkt does not match header dpop jkt"
61
-
s.logger.Error(msg)
62
return helpers.InputError(e, &msg)
63
}
64
}
···
74
ExpiresAt: eat,
75
}
76
77
-
if err := s.db.Create(authRequest, nil).Error; err != nil {
78
-
s.logger.Error("error creating auth request in db", "error", err)
79
return helpers.ServerError(e, nil)
80
}
81
···
1
package server
2
3
import (
4
+
"errors"
5
"time"
6
7
"github.com/Azure/go-autorest/autorest/to"
8
"github.com/haileyok/cocoon/internal/helpers"
9
"github.com/haileyok/cocoon/oauth"
10
"github.com/haileyok/cocoon/oauth/constants"
11
+
"github.com/haileyok/cocoon/oauth/dpop"
12
"github.com/haileyok/cocoon/oauth/provider"
13
"github.com/labstack/echo/v4"
14
)
···
19
}
20
21
func (s *Server) handleOauthPar(e echo.Context) error {
22
+
ctx := e.Request().Context()
23
+
logger := s.logger.With("name", "handleOauthPar")
24
+
25
var parRequest provider.ParRequest
26
if err := e.Bind(&parRequest); err != nil {
27
+
logger.Error("error binding for par request", "error", err)
28
return helpers.ServerError(e, nil)
29
}
30
31
if err := e.Validate(parRequest); err != nil {
32
+
logger.Error("missing parameters for par request", "error", err)
33
return helpers.InputError(e, nil)
34
}
35
36
// TODO: this seems wrong. should be a way to get the entire request url i believe, but this will work for now
37
dpopProof, err := s.oauthProvider.DpopManager.CheckProof(e.Request().Method, "https://"+s.config.Hostname+e.Request().URL.String(), e.Request().Header, nil)
38
if err != nil {
39
+
if errors.Is(err, dpop.ErrUseDpopNonce) {
40
+
nonce := s.oauthProvider.NextNonce()
41
+
if nonce != "" {
42
+
e.Response().Header().Set("DPoP-Nonce", nonce)
43
+
e.Response().Header().Add("access-control-expose-headers", "DPoP-Nonce")
44
+
}
45
+
return e.JSON(400, map[string]string{
46
+
"error": "use_dpop_nonce",
47
+
})
48
+
}
49
+
logger.Error("error getting dpop proof", "error", err)
50
+
return helpers.InputError(e, nil)
51
}
52
53
client, clientAuth, err := s.oauthProvider.AuthenticateClient(e.Request().Context(), parRequest.AuthenticateClientRequestBase, dpopProof, &provider.AuthenticateClientOptions{
···
56
AllowMissingDpopProof: true,
57
})
58
if err != nil {
59
+
logger.Error("error authenticating client", "client_id", parRequest.ClientID, "error", err)
60
return helpers.InputError(e, to.StringPtr(err.Error()))
61
}
62
···
67
} else {
68
if !client.Metadata.DpopBoundAccessTokens {
69
msg := "dpop bound access tokens are not enabled for this client"
70
+
logger.Error(msg)
71
return helpers.InputError(e, &msg)
72
}
73
74
if dpopProof.JKT != *parRequest.DpopJkt {
75
msg := "supplied dpop jkt does not match header dpop jkt"
76
+
logger.Error(msg)
77
return helpers.InputError(e, &msg)
78
}
79
}
···
89
ExpiresAt: eat,
90
}
91
92
+
if err := s.db.Create(ctx, authRequest, nil).Error; err != nil {
93
+
logger.Error("error creating auth request in db", "error", err)
94
return helpers.ServerError(e, nil)
95
}
96
+29
-14
server/handle_oauth_token.go
+29
-14
server/handle_oauth_token.go
···
4
"bytes"
5
"crypto/sha256"
6
"encoding/base64"
7
"fmt"
8
"slices"
9
"time"
···
13
"github.com/haileyok/cocoon/internal/helpers"
14
"github.com/haileyok/cocoon/oauth"
15
"github.com/haileyok/cocoon/oauth/constants"
16
"github.com/haileyok/cocoon/oauth/provider"
17
"github.com/labstack/echo/v4"
18
)
···
36
}
37
38
func (s *Server) handleOauthToken(e echo.Context) error {
39
var req OauthTokenRequest
40
if err := e.Bind(&req); err != nil {
41
-
s.logger.Error("error binding token request", "error", err)
42
return helpers.ServerError(e, nil)
43
}
44
45
proof, err := s.oauthProvider.DpopManager.CheckProof(e.Request().Method, e.Request().URL.String(), e.Request().Header, nil)
46
if err != nil {
47
-
s.logger.Error("error getting dpop proof", "error", err)
48
-
return helpers.InputError(e, to.StringPtr(err.Error()))
49
}
50
51
client, clientAuth, err := s.oauthProvider.AuthenticateClient(e.Request().Context(), req.AuthenticateClientRequestBase, proof, &provider.AuthenticateClientOptions{
52
AllowMissingDpopProof: true,
53
})
54
if err != nil {
55
-
s.logger.Error("error authenticating client", "error", err)
56
return helpers.InputError(e, to.StringPtr(err.Error()))
57
}
58
···
72
73
var authReq provider.OauthAuthorizationRequest
74
// 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)
77
return helpers.ServerError(e, nil)
78
}
79
···
98
case "S256":
99
inputChal, err := base64.RawURLEncoding.DecodeString(*authReq.Parameters.CodeChallenge)
100
if err != nil {
101
-
s.logger.Error("error decoding code challenge", "error", err)
102
return helpers.ServerError(e, nil)
103
}
104
···
116
return helpers.InputError(e, to.StringPtr("code_challenge parameter wasn't provided"))
117
}
118
119
-
repo, err := s.getRepoActorByDid(*authReq.Sub)
120
if err != nil {
121
helpers.InputError(e, to.StringPtr("unable to find actor"))
122
}
···
147
return err
148
}
149
150
-
if err := s.db.Create(&provider.OauthToken{
151
ClientId: authReq.ClientId,
152
ClientAuth: *clientAuth,
153
Parameters: authReq.Parameters,
···
159
RefreshToken: refreshToken,
160
Ip: authReq.Ip,
161
}, nil).Error; err != nil {
162
-
s.logger.Error("error creating token in db", "error", err)
163
return helpers.ServerError(e, nil)
164
}
165
···
187
}
188
189
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)
192
return helpers.ServerError(e, nil)
193
}
194
···
245
return err
246
}
247
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)
250
return helpers.ServerError(e, nil)
251
}
252
···
4
"bytes"
5
"crypto/sha256"
6
"encoding/base64"
7
+
"errors"
8
"fmt"
9
"slices"
10
"time"
···
14
"github.com/haileyok/cocoon/internal/helpers"
15
"github.com/haileyok/cocoon/oauth"
16
"github.com/haileyok/cocoon/oauth/constants"
17
+
"github.com/haileyok/cocoon/oauth/dpop"
18
"github.com/haileyok/cocoon/oauth/provider"
19
"github.com/labstack/echo/v4"
20
)
···
38
}
39
40
func (s *Server) handleOauthToken(e echo.Context) error {
41
+
ctx := e.Request().Context()
42
+
logger := s.logger.With("name", "handleOauthToken")
43
+
44
var req OauthTokenRequest
45
if err := e.Bind(&req); err != nil {
46
+
logger.Error("error binding token request", "error", err)
47
return helpers.ServerError(e, nil)
48
}
49
50
proof, err := s.oauthProvider.DpopManager.CheckProof(e.Request().Method, e.Request().URL.String(), e.Request().Header, nil)
51
if err != nil {
52
+
if errors.Is(err, dpop.ErrUseDpopNonce) {
53
+
nonce := s.oauthProvider.NextNonce()
54
+
if nonce != "" {
55
+
e.Response().Header().Set("DPoP-Nonce", nonce)
56
+
e.Response().Header().Add("access-control-expose-headers", "DPoP-Nonce")
57
+
}
58
+
return e.JSON(400, map[string]string{
59
+
"error": "use_dpop_nonce",
60
+
})
61
+
}
62
+
logger.Error("error getting dpop proof", "error", err)
63
+
return helpers.InputError(e, nil)
64
}
65
66
client, clientAuth, err := s.oauthProvider.AuthenticateClient(e.Request().Context(), req.AuthenticateClientRequestBase, proof, &provider.AuthenticateClientOptions{
67
AllowMissingDpopProof: true,
68
})
69
if err != nil {
70
+
logger.Error("error authenticating client", "client_id", req.ClientID, "error", err)
71
return helpers.InputError(e, to.StringPtr(err.Error()))
72
}
73
···
87
88
var authReq provider.OauthAuthorizationRequest
89
// get the lil guy and delete him
90
+
if err := s.db.Raw(ctx, "DELETE FROM oauth_authorization_requests WHERE code = ? RETURNING *", nil, *req.Code).Scan(&authReq).Error; err != nil {
91
+
logger.Error("error finding authorization request", "error", err)
92
return helpers.ServerError(e, nil)
93
}
94
···
113
case "S256":
114
inputChal, err := base64.RawURLEncoding.DecodeString(*authReq.Parameters.CodeChallenge)
115
if err != nil {
116
+
logger.Error("error decoding code challenge", "error", err)
117
return helpers.ServerError(e, nil)
118
}
119
···
131
return helpers.InputError(e, to.StringPtr("code_challenge parameter wasn't provided"))
132
}
133
134
+
repo, err := s.getRepoActorByDid(ctx, *authReq.Sub)
135
if err != nil {
136
helpers.InputError(e, to.StringPtr("unable to find actor"))
137
}
···
162
return err
163
}
164
165
+
if err := s.db.Create(ctx, &provider.OauthToken{
166
ClientId: authReq.ClientId,
167
ClientAuth: *clientAuth,
168
Parameters: authReq.Parameters,
···
174
RefreshToken: refreshToken,
175
Ip: authReq.Ip,
176
}, nil).Error; err != nil {
177
+
logger.Error("error creating token in db", "error", err)
178
return helpers.ServerError(e, nil)
179
}
180
···
202
}
203
204
var oauthToken provider.OauthToken
205
+
if err := s.db.Raw(ctx, "SELECT * FROM oauth_tokens WHERE refresh_token = ?", nil, req.RefreshToken).Scan(&oauthToken).Error; err != nil {
206
+
logger.Error("error finding oauth token by refresh token", "error", err, "refresh_token", req.RefreshToken)
207
return helpers.ServerError(e, nil)
208
}
209
···
260
return err
261
}
262
263
+
if err := s.db.Exec(ctx, "UPDATE oauth_tokens SET token = ?, refresh_token = ?, expires_at = ?, updated_at = ? WHERE refresh_token = ?", nil, accessString, nextRefreshToken, eat, now, *req.RefreshToken).Error; err != nil {
264
+
logger.Error("error updating token", "error", err)
265
return helpers.ServerError(e, nil)
266
}
267
+23
-10
server/handle_proxy.go
+23
-10
server/handle_proxy.go
···
19
20
func (s *Server) getAtprotoProxyEndpointFromRequest(e echo.Context) (string, string, error) {
21
svc := e.Request().Header.Get("atproto-proxy")
22
-
if svc == "" {
23
-
svc = s.config.DefaultAtprotoProxy
24
}
25
26
svcPts := strings.Split(svc, "#")
···
47
}
48
49
func (s *Server) handleProxy(e echo.Context) error {
50
-
lgr := s.logger.With("handler", "handleProxy")
51
52
repo, isAuthed := e.Get("repo").(*models.RepoActor)
53
···
58
59
endpoint, svcDid, err := s.getAtprotoProxyEndpointFromRequest(e)
60
if err != nil {
61
-
lgr.Error("could not get atproto proxy", "error", err)
62
return helpers.ServerError(e, nil)
63
}
64
···
90
}
91
hj, err := json.Marshal(header)
92
if err != nil {
93
-
lgr.Error("error marshaling header", "error", err)
94
return helpers.ServerError(e, nil)
95
}
96
97
encheader := strings.TrimRight(base64.RawURLEncoding.EncodeToString(hj), "=")
98
99
payload := map[string]any{
100
"iss": repo.Repo.Did,
101
-
"aud": svcDid,
102
-
"lxm": pts[2],
103
"jti": uuid.NewString(),
104
"exp": time.Now().Add(1 * time.Minute).UTC().Unix(),
105
}
106
pj, err := json.Marshal(payload)
107
if err != nil {
108
-
lgr.Error("error marashaling payload", "error", err)
109
return helpers.ServerError(e, nil)
110
}
111
···
116
117
sk, err := secp256k1secec.NewPrivateKey(repo.SigningKey)
118
if err != nil {
119
-
lgr.Error("can't load private key", "error", err)
120
return err
121
}
122
123
R, S, _, err := sk.SignRaw(rand.Reader, hash[:])
124
if err != nil {
125
-
lgr.Error("error signing", "error", err)
126
}
127
128
rBytes := R.Bytes()
···
19
20
func (s *Server) getAtprotoProxyEndpointFromRequest(e echo.Context) (string, string, error) {
21
svc := e.Request().Header.Get("atproto-proxy")
22
+
if svc == "" && s.config.FallbackProxy != "" {
23
+
svc = s.config.FallbackProxy
24
}
25
26
svcPts := strings.Split(svc, "#")
···
47
}
48
49
func (s *Server) handleProxy(e echo.Context) error {
50
+
logger := s.logger.With("handler", "handleProxy")
51
52
repo, isAuthed := e.Get("repo").(*models.RepoActor)
53
···
58
59
endpoint, svcDid, err := s.getAtprotoProxyEndpointFromRequest(e)
60
if err != nil {
61
+
logger.Error("could not get atproto proxy", "error", err)
62
return helpers.ServerError(e, nil)
63
}
64
···
90
}
91
hj, err := json.Marshal(header)
92
if err != nil {
93
+
logger.Error("error marshaling header", "error", err)
94
return helpers.ServerError(e, nil)
95
}
96
97
encheader := strings.TrimRight(base64.RawURLEncoding.EncodeToString(hj), "=")
98
99
+
// When proxying app.bsky.feed.getFeed the token is actually issued for the
100
+
// underlying feed generator and the app view passes it on. This allows the
101
+
// getFeed implementation to pass in the desired lxm and aud for the token
102
+
// and then just delegate to the general proxying logic
103
+
lxm, proxyTokenLxmExists := e.Get("proxyTokenLxm").(string)
104
+
if !proxyTokenLxmExists || lxm == "" {
105
+
lxm = pts[2]
106
+
}
107
+
aud, proxyTokenAudExists := e.Get("proxyTokenAud").(string)
108
+
if !proxyTokenAudExists || aud == "" {
109
+
aud = svcDid
110
+
}
111
+
112
payload := map[string]any{
113
"iss": repo.Repo.Did,
114
+
"aud": aud,
115
+
"lxm": lxm,
116
"jti": uuid.NewString(),
117
"exp": time.Now().Add(1 * time.Minute).UTC().Unix(),
118
}
119
pj, err := json.Marshal(payload)
120
if err != nil {
121
+
logger.Error("error marashaling payload", "error", err)
122
return helpers.ServerError(e, nil)
123
}
124
···
129
130
sk, err := secp256k1secec.NewPrivateKey(repo.SigningKey)
131
if err != nil {
132
+
logger.Error("can't load private key", "error", err)
133
return err
134
}
135
136
R, S, _, err := sk.SignRaw(rand.Reader, hash[:])
137
if err != nil {
138
+
logger.Error("error signing", "error", err)
139
}
140
141
rBytes := R.Bytes()
+35
server/handle_proxy_get_feed.go
+35
server/handle_proxy_get_feed.go
···
···
1
+
package server
2
+
3
+
import (
4
+
"github.com/Azure/go-autorest/autorest/to"
5
+
"github.com/bluesky-social/indigo/api/atproto"
6
+
"github.com/bluesky-social/indigo/api/bsky"
7
+
"github.com/bluesky-social/indigo/atproto/syntax"
8
+
"github.com/bluesky-social/indigo/xrpc"
9
+
"github.com/haileyok/cocoon/internal/helpers"
10
+
"github.com/labstack/echo/v4"
11
+
)
12
+
13
+
func (s *Server) handleProxyBskyFeedGetFeed(e echo.Context) error {
14
+
feedUri, err := syntax.ParseATURI(e.QueryParam("feed"))
15
+
if err != nil {
16
+
return helpers.InputError(e, to.StringPtr("invalid feed uri"))
17
+
}
18
+
19
+
appViewEndpoint, _, err := s.getAtprotoProxyEndpointFromRequest(e)
20
+
if err != nil {
21
+
e.Logger().Error("could not get atproto proxy", "error", err)
22
+
return helpers.ServerError(e, nil)
23
+
}
24
+
25
+
appViewClient := xrpc.Client{
26
+
Host: appViewEndpoint,
27
+
}
28
+
feedRecord, err := atproto.RepoGetRecord(e.Request().Context(), &appViewClient, "", feedUri.Collection().String(), feedUri.Authority().String(), feedUri.RecordKey().String())
29
+
feedGeneratorDid := feedRecord.Value.Val.(*bsky.FeedGenerator).Did
30
+
31
+
e.Set("proxyTokenLxm", "app.bsky.feed.getFeedSkeleton")
32
+
e.Set("proxyTokenAud", feedGeneratorDid)
33
+
34
+
return s.handleProxy(e)
35
+
}
+14
-11
server/handle_repo_apply_writes.go
+14
-11
server/handle_repo_apply_writes.go
···
6
"github.com/labstack/echo/v4"
7
)
8
9
-
type ComAtprotoRepoApplyWritesRequest struct {
10
Repo string `json:"repo" validate:"required,atproto-did"`
11
Validate *bool `json:"bool,omitempty"`
12
Writes []ComAtprotoRepoApplyWritesItem `json:"writes"`
···
20
Value *MarshalableMap `json:"value,omitempty"`
21
}
22
23
-
type ComAtprotoRepoApplyWritesResponse struct {
24
Commit RepoCommit `json:"commit"`
25
Results []ApplyWriteResult `json:"results"`
26
}
27
28
func (s *Server) handleApplyWrites(e echo.Context) error {
29
-
repo := e.Get("repo").(*models.RepoActor)
30
31
-
var req ComAtprotoRepoApplyWritesRequest
32
if err := e.Bind(&req); err != nil {
33
-
s.logger.Error("error binding", "error", err)
34
return helpers.ServerError(e, nil)
35
}
36
37
if err := e.Validate(req); err != nil {
38
-
s.logger.Error("error validating", "error", err)
39
return helpers.InputError(e, nil)
40
}
41
42
if repo.Repo.Did != req.Repo {
43
-
s.logger.Warn("mismatched repo/auth")
44
return helpers.InputError(e, nil)
45
}
46
47
-
ops := []Op{}
48
for _, item := range req.Writes {
49
ops = append(ops, Op{
50
Type: OpType(item.Type),
···
54
})
55
}
56
57
-
results, err := s.repoman.applyWrites(repo.Repo, ops, req.SwapCommit)
58
if err != nil {
59
-
s.logger.Error("error applying writes", "error", err)
60
return helpers.ServerError(e, nil)
61
}
62
···
66
results[i].Commit = nil
67
}
68
69
-
return e.JSON(200, ComAtprotoRepoApplyWritesResponse{
70
Commit: commit,
71
Results: results,
72
})
···
6
"github.com/labstack/echo/v4"
7
)
8
9
+
type ComAtprotoRepoApplyWritesInput struct {
10
Repo string `json:"repo" validate:"required,atproto-did"`
11
Validate *bool `json:"bool,omitempty"`
12
Writes []ComAtprotoRepoApplyWritesItem `json:"writes"`
···
20
Value *MarshalableMap `json:"value,omitempty"`
21
}
22
23
+
type ComAtprotoRepoApplyWritesOutput struct {
24
Commit RepoCommit `json:"commit"`
25
Results []ApplyWriteResult `json:"results"`
26
}
27
28
func (s *Server) handleApplyWrites(e echo.Context) error {
29
+
ctx := e.Request().Context()
30
+
logger := s.logger.With("name", "handleRepoApplyWrites")
31
32
+
var req ComAtprotoRepoApplyWritesInput
33
if err := e.Bind(&req); err != nil {
34
+
logger.Error("error binding", "error", err)
35
return helpers.ServerError(e, nil)
36
}
37
38
if err := e.Validate(req); err != nil {
39
+
logger.Error("error validating", "error", err)
40
return helpers.InputError(e, nil)
41
}
42
43
+
repo := e.Get("repo").(*models.RepoActor)
44
+
45
if repo.Repo.Did != req.Repo {
46
+
logger.Warn("mismatched repo/auth")
47
return helpers.InputError(e, nil)
48
}
49
50
+
ops := make([]Op, 0, len(req.Writes))
51
for _, item := range req.Writes {
52
ops = append(ops, Op{
53
Type: OpType(item.Type),
···
57
})
58
}
59
60
+
results, err := s.repoman.applyWrites(ctx, repo.Repo, ops, req.SwapCommit)
61
if err != nil {
62
+
logger.Error("error applying writes", "error", err)
63
return helpers.ServerError(e, nil)
64
}
65
···
69
results[i].Commit = nil
70
}
71
72
+
return e.JSON(200, ComAtprotoRepoApplyWritesOutput{
73
Commit: commit,
74
Results: results,
75
})
+10
-7
server/handle_repo_create_record.go
+10
-7
server/handle_repo_create_record.go
···
6
"github.com/labstack/echo/v4"
7
)
8
9
-
type ComAtprotoRepoCreateRecordRequest struct {
10
Repo string `json:"repo" validate:"required,atproto-did"`
11
Collection string `json:"collection" validate:"required,atproto-nsid"`
12
Rkey *string `json:"rkey,omitempty"`
···
17
}
18
19
func (s *Server) handleCreateRecord(e echo.Context) error {
20
repo := e.Get("repo").(*models.RepoActor)
21
22
-
var req ComAtprotoRepoCreateRecordRequest
23
if err := e.Bind(&req); err != nil {
24
-
s.logger.Error("error binding", "error", err)
25
return helpers.ServerError(e, nil)
26
}
27
28
if err := e.Validate(req); err != nil {
29
-
s.logger.Error("error validating", "error", err)
30
return helpers.InputError(e, nil)
31
}
32
33
if repo.Repo.Did != req.Repo {
34
-
s.logger.Warn("mismatched repo/auth")
35
return helpers.InputError(e, nil)
36
}
37
···
40
optype = OpTypeUpdate
41
}
42
43
-
results, err := s.repoman.applyWrites(repo.Repo, []Op{
44
{
45
Type: optype,
46
Collection: req.Collection,
···
51
},
52
}, req.SwapCommit)
53
if err != nil {
54
-
s.logger.Error("error applying writes", "error", err)
55
return helpers.ServerError(e, nil)
56
}
57
···
6
"github.com/labstack/echo/v4"
7
)
8
9
+
type ComAtprotoRepoCreateRecordInput struct {
10
Repo string `json:"repo" validate:"required,atproto-did"`
11
Collection string `json:"collection" validate:"required,atproto-nsid"`
12
Rkey *string `json:"rkey,omitempty"`
···
17
}
18
19
func (s *Server) handleCreateRecord(e echo.Context) error {
20
+
ctx := e.Request().Context()
21
+
logger := s.logger.With("name", "handleCreateRecord")
22
+
23
repo := e.Get("repo").(*models.RepoActor)
24
25
+
var req ComAtprotoRepoCreateRecordInput
26
if err := e.Bind(&req); err != nil {
27
+
logger.Error("error binding", "error", err)
28
return helpers.ServerError(e, nil)
29
}
30
31
if err := e.Validate(req); err != nil {
32
+
logger.Error("error validating", "error", err)
33
return helpers.InputError(e, nil)
34
}
35
36
if repo.Repo.Did != req.Repo {
37
+
logger.Warn("mismatched repo/auth")
38
return helpers.InputError(e, nil)
39
}
40
···
43
optype = OpTypeUpdate
44
}
45
46
+
results, err := s.repoman.applyWrites(ctx, repo.Repo, []Op{
47
{
48
Type: optype,
49
Collection: req.Collection,
···
54
},
55
}, req.SwapCommit)
56
if err != nil {
57
+
logger.Error("error applying writes", "error", err)
58
return helpers.ServerError(e, nil)
59
}
60
+10
-7
server/handle_repo_delete_record.go
+10
-7
server/handle_repo_delete_record.go
···
6
"github.com/labstack/echo/v4"
7
)
8
9
-
type ComAtprotoRepoDeleteRecordRequest struct {
10
Repo string `json:"repo" validate:"required,atproto-did"`
11
Collection string `json:"collection" validate:"required,atproto-nsid"`
12
Rkey string `json:"rkey" validate:"required,atproto-rkey"`
···
15
}
16
17
func (s *Server) handleDeleteRecord(e echo.Context) error {
18
repo := e.Get("repo").(*models.RepoActor)
19
20
-
var req ComAtprotoRepoDeleteRecordRequest
21
if err := e.Bind(&req); err != nil {
22
-
s.logger.Error("error binding", "error", err)
23
return helpers.ServerError(e, nil)
24
}
25
26
if err := e.Validate(req); err != nil {
27
-
s.logger.Error("error validating", "error", err)
28
return helpers.InputError(e, nil)
29
}
30
31
if repo.Repo.Did != req.Repo {
32
-
s.logger.Warn("mismatched repo/auth")
33
return helpers.InputError(e, nil)
34
}
35
36
-
results, err := s.repoman.applyWrites(repo.Repo, []Op{
37
{
38
Type: OpTypeDelete,
39
Collection: req.Collection,
···
42
},
43
}, req.SwapCommit)
44
if err != nil {
45
-
s.logger.Error("error applying writes", "error", err)
46
return helpers.ServerError(e, nil)
47
}
48
···
6
"github.com/labstack/echo/v4"
7
)
8
9
+
type ComAtprotoRepoDeleteRecordInput struct {
10
Repo string `json:"repo" validate:"required,atproto-did"`
11
Collection string `json:"collection" validate:"required,atproto-nsid"`
12
Rkey string `json:"rkey" validate:"required,atproto-rkey"`
···
15
}
16
17
func (s *Server) handleDeleteRecord(e echo.Context) error {
18
+
ctx := e.Request().Context()
19
+
logger := s.logger.With("name", "handleDeleteRecord")
20
+
21
repo := e.Get("repo").(*models.RepoActor)
22
23
+
var req ComAtprotoRepoDeleteRecordInput
24
if err := e.Bind(&req); err != nil {
25
+
logger.Error("error binding", "error", err)
26
return helpers.ServerError(e, nil)
27
}
28
29
if err := e.Validate(req); err != nil {
30
+
logger.Error("error validating", "error", err)
31
return helpers.InputError(e, nil)
32
}
33
34
if repo.Repo.Did != req.Repo {
35
+
logger.Warn("mismatched repo/auth")
36
return helpers.InputError(e, nil)
37
}
38
39
+
results, err := s.repoman.applyWrites(ctx, repo.Repo, []Op{
40
{
41
Type: OpTypeDelete,
42
Collection: req.Collection,
···
45
},
46
}, req.SwapCommit)
47
if err != nil {
48
+
logger.Error("error applying writes", "error", err)
49
return helpers.ServerError(e, nil)
50
}
51
+8
-5
server/handle_repo_describe_repo.go
+8
-5
server/handle_repo_describe_repo.go
···
20
}
21
22
func (s *Server) handleDescribeRepo(e echo.Context) error {
23
did := e.QueryParam("repo")
24
-
repo, err := s.getRepoActorByDid(did)
25
if err != nil {
26
if err == gorm.ErrRecordNotFound {
27
return helpers.InputError(e, to.StringPtr("RepoNotFound"))
28
}
29
30
-
s.logger.Error("error looking up repo", "error", err)
31
return helpers.ServerError(e, nil)
32
}
33
···
35
36
diddoc, err := s.passport.FetchDoc(e.Request().Context(), repo.Repo.Did)
37
if err != nil {
38
-
s.logger.Error("error fetching diddoc", "error", err)
39
return helpers.ServerError(e, nil)
40
}
41
···
64
}
65
66
var records []models.Record
67
-
if err := s.db.Raw("SELECT DISTINCT(nsid) FROM records WHERE did = ?", nil, repo.Repo.Did).Scan(&records).Error; err != nil {
68
-
s.logger.Error("error getting collections", "error", err)
69
return helpers.ServerError(e, nil)
70
}
71
···
20
}
21
22
func (s *Server) handleDescribeRepo(e echo.Context) error {
23
+
ctx := e.Request().Context()
24
+
logger := s.logger.With("name", "handleDescribeRepo")
25
+
26
did := e.QueryParam("repo")
27
+
repo, err := s.getRepoActorByDid(ctx, did)
28
if err != nil {
29
if err == gorm.ErrRecordNotFound {
30
return helpers.InputError(e, to.StringPtr("RepoNotFound"))
31
}
32
33
+
logger.Error("error looking up repo", "error", err)
34
return helpers.ServerError(e, nil)
35
}
36
···
38
39
diddoc, err := s.passport.FetchDoc(e.Request().Context(), repo.Repo.Did)
40
if err != nil {
41
+
logger.Error("error fetching diddoc", "error", err)
42
return helpers.ServerError(e, nil)
43
}
44
···
67
}
68
69
var records []models.Record
70
+
if err := s.db.Raw(ctx, "SELECT DISTINCT(nsid) FROM records WHERE did = ?", nil, repo.Repo.Did).Scan(&records).Error; err != nil {
71
+
logger.Error("error getting collections", "error", err)
72
return helpers.ServerError(e, nil)
73
}
74
+5
-3
server/handle_repo_get_record.go
+5
-3
server/handle_repo_get_record.go
···
1
package server
2
3
import (
4
-
"github.com/bluesky-social/indigo/atproto/data"
5
"github.com/bluesky-social/indigo/atproto/syntax"
6
"github.com/haileyok/cocoon/models"
7
"github.com/labstack/echo/v4"
···
14
}
15
16
func (s *Server) handleRepoGetRecord(e echo.Context) error {
17
repo := e.QueryParam("repo")
18
collection := e.QueryParam("collection")
19
rkey := e.QueryParam("rkey")
···
32
}
33
34
var record models.Record
35
-
if err := s.db.Raw("SELECT * FROM records WHERE did = ? AND nsid = ? AND rkey = ?"+cidquery, nil, params...).Scan(&record).Error; err != nil {
36
// TODO: handle error nicely
37
return err
38
}
39
40
-
val, err := data.UnmarshalCBOR(record.Value)
41
if err != nil {
42
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
}
···
1
package server
2
3
import (
4
+
"github.com/bluesky-social/indigo/atproto/atdata"
5
"github.com/bluesky-social/indigo/atproto/syntax"
6
"github.com/haileyok/cocoon/models"
7
"github.com/labstack/echo/v4"
···
14
}
15
16
func (s *Server) handleRepoGetRecord(e echo.Context) error {
17
+
ctx := e.Request().Context()
18
+
19
repo := e.QueryParam("repo")
20
collection := e.QueryParam("collection")
21
rkey := e.QueryParam("rkey")
···
34
}
35
36
var record models.Record
37
+
if err := s.db.Raw(ctx, "SELECT * FROM records WHERE did = ? AND nsid = ? AND rkey = ?"+cidquery, nil, params...).Scan(&record).Error; err != nil {
38
// TODO: handle error nicely
39
return err
40
}
41
42
+
val, err := atdata.UnmarshalCBOR(record.Value)
43
if err != nil {
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?
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
"strconv"
5
6
"github.com/Azure/go-autorest/autorest/to"
7
-
"github.com/bluesky-social/indigo/atproto/data"
8
"github.com/bluesky-social/indigo/atproto/syntax"
9
"github.com/haileyok/cocoon/internal/helpers"
10
"github.com/haileyok/cocoon/models"
···
46
}
47
48
func (s *Server) handleListRecords(e echo.Context) error {
49
var req ComAtprotoRepoListRecordsRequest
50
if err := e.Bind(&req); err != nil {
51
-
s.logger.Error("could not bind list records request", "error", err)
52
return helpers.ServerError(e, nil)
53
}
54
···
78
79
did := req.Repo
80
if _, err := syntax.ParseDID(did); err != nil {
81
-
actor, err := s.getActorByHandle(req.Repo)
82
if err != nil {
83
return helpers.InputError(e, to.StringPtr("RepoNotFound"))
84
}
···
93
params = append(params, limit)
94
95
var records []models.Record
96
-
if err := s.db.Raw("SELECT * FROM records WHERE did = ? AND nsid = ? "+cursorquery+" ORDER BY created_at "+sort+" limit ?", nil, params...).Scan(&records).Error; err != nil {
97
-
s.logger.Error("error getting records", "error", err)
98
return helpers.ServerError(e, nil)
99
}
100
101
items := []ComAtprotoRepoListRecordsRecordItem{}
102
for _, r := range records {
103
-
val, err := data.UnmarshalCBOR(r.Value)
104
if err != nil {
105
return err
106
}
···
4
"strconv"
5
6
"github.com/Azure/go-autorest/autorest/to"
7
+
"github.com/bluesky-social/indigo/atproto/atdata"
8
"github.com/bluesky-social/indigo/atproto/syntax"
9
"github.com/haileyok/cocoon/internal/helpers"
10
"github.com/haileyok/cocoon/models"
···
46
}
47
48
func (s *Server) handleListRecords(e echo.Context) error {
49
+
ctx := e.Request().Context()
50
+
logger := s.logger.With("name", "handleListRecords")
51
+
52
var req ComAtprotoRepoListRecordsRequest
53
if err := e.Bind(&req); err != nil {
54
+
logger.Error("could not bind list records request", "error", err)
55
return helpers.ServerError(e, nil)
56
}
57
···
81
82
did := req.Repo
83
if _, err := syntax.ParseDID(did); err != nil {
84
+
actor, err := s.getActorByHandle(ctx, req.Repo)
85
if err != nil {
86
return helpers.InputError(e, to.StringPtr("RepoNotFound"))
87
}
···
96
params = append(params, limit)
97
98
var records []models.Record
99
+
if err := s.db.Raw(ctx, "SELECT * FROM records WHERE did = ? AND nsid = ? "+cursorquery+" ORDER BY created_at "+sort+" limit ?", nil, params...).Scan(&records).Error; err != nil {
100
+
logger.Error("error getting records", "error", err)
101
return helpers.ServerError(e, nil)
102
}
103
104
items := []ComAtprotoRepoListRecordsRecordItem{}
105
for _, r := range records {
106
+
val, err := atdata.UnmarshalCBOR(r.Value)
107
if err != nil {
108
return err
109
}
+5
-3
server/handle_repo_list_repos.go
+5
-3
server/handle_repo_list_repos.go
···
21
22
// TODO: paginate this bitch
23
func (s *Server) handleListRepos(e echo.Context) error {
24
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 {
26
return err
27
}
28
···
37
Did: r.Did,
38
Head: c.String(),
39
Rev: r.Rev,
40
-
Active: true,
41
-
Status: nil,
42
})
43
}
44
···
21
22
// TODO: paginate this bitch
23
func (s *Server) handleListRepos(e echo.Context) error {
24
+
ctx := e.Request().Context()
25
+
26
var repos []models.Repo
27
+
if err := s.db.Raw(ctx, "SELECT * FROM repos ORDER BY created_at DESC LIMIT 500", nil).Scan(&repos).Error; err != nil {
28
return err
29
}
30
···
39
Did: r.Did,
40
Head: c.String(),
41
Rev: r.Rev,
42
+
Active: r.Active(),
43
+
Status: r.Status(),
44
})
45
}
46
+10
-7
server/handle_repo_put_record.go
+10
-7
server/handle_repo_put_record.go
···
6
"github.com/labstack/echo/v4"
7
)
8
9
-
type ComAtprotoRepoPutRecordRequest struct {
10
Repo string `json:"repo" validate:"required,atproto-did"`
11
Collection string `json:"collection" validate:"required,atproto-nsid"`
12
Rkey string `json:"rkey" validate:"required,atproto-rkey"`
···
17
}
18
19
func (s *Server) handlePutRecord(e echo.Context) error {
20
repo := e.Get("repo").(*models.RepoActor)
21
22
-
var req ComAtprotoRepoPutRecordRequest
23
if err := e.Bind(&req); err != nil {
24
-
s.logger.Error("error binding", "error", err)
25
return helpers.ServerError(e, nil)
26
}
27
28
if err := e.Validate(req); err != nil {
29
-
s.logger.Error("error validating", "error", err)
30
return helpers.InputError(e, nil)
31
}
32
33
if repo.Repo.Did != req.Repo {
34
-
s.logger.Warn("mismatched repo/auth")
35
return helpers.InputError(e, nil)
36
}
37
···
40
optype = OpTypeUpdate
41
}
42
43
-
results, err := s.repoman.applyWrites(repo.Repo, []Op{
44
{
45
Type: optype,
46
Collection: req.Collection,
···
51
},
52
}, req.SwapCommit)
53
if err != nil {
54
-
s.logger.Error("error applying writes", "error", err)
55
return helpers.ServerError(e, nil)
56
}
57
···
6
"github.com/labstack/echo/v4"
7
)
8
9
+
type ComAtprotoRepoPutRecordInput struct {
10
Repo string `json:"repo" validate:"required,atproto-did"`
11
Collection string `json:"collection" validate:"required,atproto-nsid"`
12
Rkey string `json:"rkey" validate:"required,atproto-rkey"`
···
17
}
18
19
func (s *Server) handlePutRecord(e echo.Context) error {
20
+
ctx := e.Request().Context()
21
+
logger := s.logger.With("name", "handlePutRecord")
22
+
23
repo := e.Get("repo").(*models.RepoActor)
24
25
+
var req ComAtprotoRepoPutRecordInput
26
if err := e.Bind(&req); err != nil {
27
+
logger.Error("error binding", "error", err)
28
return helpers.ServerError(e, nil)
29
}
30
31
if err := e.Validate(req); err != nil {
32
+
logger.Error("error validating", "error", err)
33
return helpers.InputError(e, nil)
34
}
35
36
if repo.Repo.Did != req.Repo {
37
+
logger.Warn("mismatched repo/auth")
38
return helpers.InputError(e, nil)
39
}
40
···
43
optype = OpTypeUpdate
44
}
45
46
+
results, err := s.repoman.applyWrites(ctx, repo.Repo, []Op{
47
{
48
Type: optype,
49
Collection: req.Collection,
···
54
},
55
}, req.SwapCommit)
56
if err != nil {
57
+
logger.Error("error applying writes", "error", err)
58
return helpers.ServerError(e, nil)
59
}
60
+59
-14
server/handle_repo_upload_blob.go
+59
-14
server/handle_repo_upload_blob.go
···
2
3
import (
4
"bytes"
5
"io"
6
7
"github.com/haileyok/cocoon/internal/helpers"
8
"github.com/haileyok/cocoon/models"
9
"github.com/ipfs/go-cid"
···
27
}
28
29
func (s *Server) handleRepoUploadBlob(e echo.Context) error {
30
urepo := e.Get("repo").(*models.RepoActor)
31
32
mime := e.Request().Header.Get("content-type")
···
34
mime = "application/octet-stream"
35
}
36
37
blob := models.Blob{
38
Did: urepo.Repo.Did,
39
RefCount: 0,
40
CreatedAt: s.repoman.clock.Next().String(),
41
}
42
43
-
if err := s.db.Create(&blob, nil).Error; err != nil {
44
-
s.logger.Error("error creating new blob in db", "error", err)
45
return helpers.ServerError(e, nil)
46
}
47
···
58
break
59
}
60
} else if err != nil && err != io.ErrUnexpectedEOF {
61
-
s.logger.Error("error reading blob", "error", err)
62
return helpers.ServerError(e, nil)
63
}
64
···
66
read += n
67
fulldata.Write(data)
68
69
-
blobPart := models.BlobPart{
70
-
BlobID: blob.ID,
71
-
Idx: part,
72
-
Data: data,
73
-
}
74
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)
78
}
79
part++
80
···
85
86
c, err := cid.NewPrefixV1(cid.Raw, multihash.SHA2_256).Sum(fulldata.Bytes())
87
if err != nil {
88
-
s.logger.Error("error creating cid prefix", "error", err)
89
return helpers.ServerError(e, nil)
90
}
91
92
-
if err := s.db.Exec("UPDATE blobs SET cid = ? WHERE id = ?", nil, c.Bytes(), blob.ID).Error; err != nil {
93
// there should probably be somme handling here if this fails...
94
-
s.logger.Error("error updating blob", "error", err)
95
return helpers.ServerError(e, nil)
96
}
97
···
2
3
import (
4
"bytes"
5
+
"fmt"
6
"io"
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"
12
"github.com/haileyok/cocoon/internal/helpers"
13
"github.com/haileyok/cocoon/models"
14
"github.com/ipfs/go-cid"
···
32
}
33
34
func (s *Server) handleRepoUploadBlob(e echo.Context) error {
35
+
ctx := e.Request().Context()
36
+
logger := s.logger.With("name", "handleRepoUploadBlob")
37
+
38
urepo := e.Get("repo").(*models.RepoActor)
39
40
mime := e.Request().Header.Get("content-type")
···
42
mime = "application/octet-stream"
43
}
44
45
+
storage := "sqlite"
46
+
s3Upload := s.s3Config != nil && s.s3Config.BlobstoreEnabled
47
+
if s3Upload {
48
+
storage = "s3"
49
+
}
50
blob := models.Blob{
51
Did: urepo.Repo.Did,
52
RefCount: 0,
53
CreatedAt: s.repoman.clock.Next().String(),
54
+
Storage: storage,
55
}
56
57
+
if err := s.db.Create(ctx, &blob, nil).Error; err != nil {
58
+
logger.Error("error creating new blob in db", "error", err)
59
return helpers.ServerError(e, nil)
60
}
61
···
72
break
73
}
74
} else if err != nil && err != io.ErrUnexpectedEOF {
75
+
logger.Error("error reading blob", "error", err)
76
return helpers.ServerError(e, nil)
77
}
78
···
80
read += n
81
fulldata.Write(data)
82
83
+
if !s3Upload {
84
+
blobPart := models.BlobPart{
85
+
BlobID: blob.ID,
86
+
Idx: part,
87
+
Data: data,
88
+
}
89
90
+
if err := s.db.Create(ctx, &blobPart, nil).Error; err != nil {
91
+
logger.Error("error adding blob part to db", "error", err)
92
+
return helpers.ServerError(e, nil)
93
+
}
94
}
95
part++
96
···
101
102
c, err := cid.NewPrefixV1(cid.Raw, multihash.SHA2_256).Sum(fulldata.Bytes())
103
if err != nil {
104
+
logger.Error("error creating cid prefix", "error", err)
105
return helpers.ServerError(e, nil)
106
}
107
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 {
138
// there should probably be somme handling here if this fails...
139
+
logger.Error("error updating blob", "error", err)
140
return helpers.ServerError(e, nil)
141
}
142
+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
}
21
22
func (s *Server) handleServerCheckAccountStatus(e echo.Context) error {
23
urepo := e.Get("repo").(*models.RepoActor)
24
25
resp := ComAtprotoServerCheckAccountStatusResponse{
···
31
32
rootcid, err := cid.Cast(urepo.Root)
33
if err != nil {
34
-
s.logger.Error("error casting cid", "error", err)
35
return helpers.ServerError(e, nil)
36
}
37
resp.RepoCommit = rootcid.String()
···
41
}
42
43
var blockCtResp CountResp
44
-
if err := s.db.Raw("SELECT COUNT(*) AS ct FROM blocks WHERE did = ?", nil, urepo.Repo.Did).Scan(&blockCtResp).Error; err != nil {
45
-
s.logger.Error("error getting block count", "error", err)
46
return helpers.ServerError(e, nil)
47
}
48
resp.RepoBlocks = blockCtResp.Ct
49
50
var recCtResp CountResp
51
-
if err := s.db.Raw("SELECT COUNT(*) AS ct FROM records WHERE did = ?", nil, urepo.Repo.Did).Scan(&recCtResp).Error; err != nil {
52
-
s.logger.Error("error getting record count", "error", err)
53
return helpers.ServerError(e, nil)
54
}
55
resp.IndexedRecords = recCtResp.Ct
56
57
var blobCtResp CountResp
58
-
if err := s.db.Raw("SELECT COUNT(*) AS ct FROM blobs WHERE did = ?", nil, urepo.Repo.Did).Scan(&blobCtResp).Error; err != nil {
59
-
s.logger.Error("error getting record count", "error", err)
60
return helpers.ServerError(e, nil)
61
}
62
resp.ExpectedBlobs = blobCtResp.Ct
···
20
}
21
22
func (s *Server) handleServerCheckAccountStatus(e echo.Context) error {
23
+
ctx := e.Request().Context()
24
+
logger := s.logger.With("name", "handleServerCheckAccountStatus")
25
+
26
urepo := e.Get("repo").(*models.RepoActor)
27
28
resp := ComAtprotoServerCheckAccountStatusResponse{
···
34
35
rootcid, err := cid.Cast(urepo.Root)
36
if err != nil {
37
+
logger.Error("error casting cid", "error", err)
38
return helpers.ServerError(e, nil)
39
}
40
resp.RepoCommit = rootcid.String()
···
44
}
45
46
var blockCtResp CountResp
47
+
if err := s.db.Raw(ctx, "SELECT COUNT(*) AS ct FROM blocks WHERE did = ?", nil, urepo.Repo.Did).Scan(&blockCtResp).Error; err != nil {
48
+
logger.Error("error getting block count", "error", err)
49
return helpers.ServerError(e, nil)
50
}
51
resp.RepoBlocks = blockCtResp.Ct
52
53
var recCtResp CountResp
54
+
if err := s.db.Raw(ctx, "SELECT COUNT(*) AS ct FROM records WHERE did = ?", nil, urepo.Repo.Did).Scan(&recCtResp).Error; err != nil {
55
+
logger.Error("error getting record count", "error", err)
56
return helpers.ServerError(e, nil)
57
}
58
resp.IndexedRecords = recCtResp.Ct
59
60
var blobCtResp CountResp
61
+
if err := s.db.Raw(ctx, "SELECT COUNT(*) AS ct FROM blobs WHERE did = ?", nil, urepo.Repo.Did).Scan(&blobCtResp).Error; err != nil {
62
+
logger.Error("error getting record count", "error", err)
63
return helpers.ServerError(e, nil)
64
}
65
resp.ExpectedBlobs = blobCtResp.Ct
+6
-3
server/handle_server_confirm_email.go
+6
-3
server/handle_server_confirm_email.go
···
15
}
16
17
func (s *Server) handleServerConfirmEmail(e echo.Context) error {
18
urepo := e.Get("repo").(*models.RepoActor)
19
20
var req ComAtprotoServerConfirmEmailRequest
21
if err := e.Bind(&req); err != nil {
22
-
s.logger.Error("error binding", "error", err)
23
return helpers.ServerError(e, nil)
24
}
25
···
41
42
now := time.Now().UTC()
43
44
-
if err := s.db.Exec("UPDATE repos SET email_verification_code = NULL, email_verification_code_expires_at = NULL, email_confirmed_at = ? WHERE did = ?", nil, now, urepo.Repo.Did).Error; err != nil {
45
-
s.logger.Error("error updating user", "error", err)
46
return helpers.ServerError(e, nil)
47
}
48
···
15
}
16
17
func (s *Server) handleServerConfirmEmail(e echo.Context) error {
18
+
ctx := e.Request().Context()
19
+
logger := s.logger.With("name", "handleServerConfirmEmail")
20
+
21
urepo := e.Get("repo").(*models.RepoActor)
22
23
var req ComAtprotoServerConfirmEmailRequest
24
if err := e.Bind(&req); err != nil {
25
+
logger.Error("error binding", "error", err)
26
return helpers.ServerError(e, nil)
27
}
28
···
44
45
now := time.Now().UTC()
46
47
+
if err := s.db.Exec(ctx, "UPDATE repos SET email_verification_code = NULL, email_verification_code_expires_at = NULL, email_confirmed_at = ? WHERE did = ?", nil, now, urepo.Repo.Did).Error; err != nil {
48
+
logger.Error("error updating user", "error", err)
49
return helpers.ServerError(e, nil)
50
}
51
+110
-75
server/handle_server_create_account.go
+110
-75
server/handle_server_create_account.go
···
9
10
"github.com/Azure/go-autorest/autorest/to"
11
"github.com/bluesky-social/indigo/api/atproto"
12
-
"github.com/bluesky-social/indigo/atproto/crypto"
13
-
"github.com/bluesky-social/indigo/atproto/syntax"
14
"github.com/bluesky-social/indigo/events"
15
"github.com/bluesky-social/indigo/repo"
16
"github.com/bluesky-social/indigo/util"
17
-
"github.com/haileyok/cocoon/blockstore"
18
"github.com/haileyok/cocoon/internal/helpers"
19
"github.com/haileyok/cocoon/models"
20
"github.com/labstack/echo/v4"
···
27
Handle string `json:"handle" validate:"required,atproto-handle"`
28
Did *string `json:"did" validate:"atproto-did"`
29
Password string `json:"password" validate:"required"`
30
-
InviteCode string `json:"inviteCode" validate:"required"`
31
}
32
33
type ComAtprotoServerCreateAccountResponse struct {
···
38
}
39
40
func (s *Server) handleCreateAccount(e echo.Context) error {
41
-
var request ComAtprotoServerCreateAccountRequest
42
-
43
-
var signupDid string
44
-
customDidHeader := e.Request().Header.Get("authorization")
45
-
if customDidHeader != "" {
46
-
pts := strings.Split(customDidHeader, " ")
47
-
if len(pts) != 2 {
48
-
return helpers.InputError(e, to.StringPtr("InvalidDid"))
49
-
}
50
51
-
_, err := syntax.ParseDID(pts[1])
52
-
if err != nil {
53
-
return helpers.InputError(e, to.StringPtr("InvalidDid"))
54
-
}
55
-
56
-
signupDid = pts[1]
57
-
}
58
59
if err := e.Bind(&request); err != nil {
60
-
s.logger.Error("error receiving request", "endpoint", "com.atproto.server.createAccount", "error", err)
61
return helpers.ServerError(e, nil)
62
}
63
64
request.Handle = strings.ToLower(request.Handle)
65
66
if err := e.Validate(request); err != nil {
67
-
s.logger.Error("error validating request", "endpoint", "com.atproto.server.createAccount", "error", err)
68
69
var verr ValidationError
70
if errors.As(err, &verr) {
···
87
}
88
}
89
90
// see if the handle is already taken
91
-
_, err := s.getActorByHandle(request.Handle)
92
if err != nil && err != gorm.ErrRecordNotFound {
93
-
s.logger.Error("error looking up handle in db", "endpoint", "com.atproto.server.createAccount", "error", err)
94
return helpers.ServerError(e, nil)
95
}
96
-
if err == nil {
97
return helpers.InputError(e, to.StringPtr("HandleNotAvailable"))
98
}
99
100
-
if did, err := s.passport.ResolveHandle(e.Request().Context(), request.Handle); err == nil && did != "" {
101
return helpers.InputError(e, to.StringPtr("HandleNotAvailable"))
102
}
103
104
var ic models.InviteCode
105
-
if err := s.db.Raw("SELECT * FROM invite_codes WHERE code = ?", nil, request.InviteCode).Scan(&ic).Error; err != nil {
106
-
if err == gorm.ErrRecordNotFound {
107
return helpers.InputError(e, to.StringPtr("InvalidInviteCode"))
108
}
109
-
s.logger.Error("error getting invite code from db", "error", err)
110
-
return helpers.ServerError(e, nil)
111
-
}
112
113
-
if ic.RemainingUseCount < 1 {
114
-
return helpers.InputError(e, to.StringPtr("InvalidInviteCode"))
115
}
116
117
// see if the email is already taken
118
-
_, err = s.getRepoByEmail(request.Email)
119
if err != nil && err != gorm.ErrRecordNotFound {
120
-
s.logger.Error("error looking up email in db", "endpoint", "com.atproto.server.createAccount", "error", err)
121
return helpers.ServerError(e, nil)
122
}
123
-
if err == nil {
124
return helpers.InputError(e, to.StringPtr("EmailNotAvailable"))
125
}
126
127
// TODO: unsupported domains
128
129
-
k, err := crypto.GeneratePrivateKeyK256()
130
-
if err != nil {
131
-
s.logger.Error("error creating signing key", "endpoint", "com.atproto.server.createAccount", "error", err)
132
-
return helpers.ServerError(e, nil)
133
}
134
135
if signupDid == "" {
136
did, op, err := s.plcClient.CreateDID(k, "", request.Handle)
137
if err != nil {
138
-
s.logger.Error("error creating operation", "endpoint", "com.atproto.server.createAccount", "error", err)
139
return helpers.ServerError(e, nil)
140
}
141
142
if err := s.plcClient.SendOperation(e.Request().Context(), did, op); err != nil {
143
-
s.logger.Error("error sending plc op", "endpoint", "com.atproto.server.createAccount", "error", err)
144
return helpers.ServerError(e, nil)
145
}
146
signupDid = did
···
148
149
hashed, err := bcrypt.GenerateFromPassword([]byte(request.Password), 10)
150
if err != nil {
151
-
s.logger.Error("error hashing password", "error", err)
152
return helpers.ServerError(e, nil)
153
}
154
···
161
SigningKey: k.Bytes(),
162
}
163
164
-
actor := models.Actor{
165
-
Did: signupDid,
166
-
Handle: request.Handle,
167
-
}
168
169
-
if err := s.db.Create(&urepo, nil).Error; err != nil {
170
-
s.logger.Error("error inserting new repo", "error", err)
171
-
return helpers.ServerError(e, nil)
172
-
}
173
174
-
if err := s.db.Create(&actor, nil).Error; err != nil {
175
-
s.logger.Error("error inserting new actor", "error", err)
176
-
return helpers.ServerError(e, nil)
177
}
178
179
-
if customDidHeader == "" {
180
-
bs := blockstore.New(signupDid, s.db)
181
r := repo.NewRepo(context.TODO(), signupDid, bs)
182
183
root, rev, err := r.Commit(context.TODO(), urepo.SignFor)
184
if err != nil {
185
-
s.logger.Error("error committing", "error", err)
186
return helpers.ServerError(e, nil)
187
}
188
189
-
if err := bs.UpdateRepo(context.TODO(), root, rev); err != nil {
190
-
s.logger.Error("error updating repo after commit", "error", err)
191
return helpers.ServerError(e, nil)
192
}
193
194
s.evtman.AddEvent(context.TODO(), &events.XRPCStreamEvent{
195
-
RepoHandle: &atproto.SyncSubscribeRepos_Handle{
196
-
Did: urepo.Did,
197
-
Handle: request.Handle,
198
-
Seq: time.Now().UnixMicro(), // TODO: no
199
-
Time: time.Now().Format(util.ISO8601),
200
-
},
201
-
})
202
-
203
-
s.evtman.AddEvent(context.TODO(), &events.XRPCStreamEvent{
204
RepoIdentity: &atproto.SyncSubscribeRepos_Identity{
205
Did: urepo.Did,
206
Handle: to.StringPtr(request.Handle),
···
210
})
211
}
212
213
-
if err := s.db.Raw("UPDATE invite_codes SET remaining_use_count = remaining_use_count - 1 WHERE code = ?", nil, request.InviteCode).Scan(&ic).Error; err != nil {
214
-
s.logger.Error("error decrementing use count", "error", err)
215
-
return helpers.ServerError(e, nil)
216
}
217
218
-
sess, err := s.createSession(&urepo)
219
if err != nil {
220
-
s.logger.Error("error creating new session", "error", err)
221
return helpers.ServerError(e, nil)
222
}
223
224
go func() {
225
if err := s.sendEmailVerification(urepo.Email, actor.Handle, *urepo.EmailVerificationCode); err != nil {
226
-
s.logger.Error("error sending email verification email", "error", err)
227
}
228
if err := s.sendWelcomeMail(urepo.Email, actor.Handle); err != nil {
229
-
s.logger.Error("error sending welcome email", "error", err)
230
}
231
}()
232
···
9
10
"github.com/Azure/go-autorest/autorest/to"
11
"github.com/bluesky-social/indigo/api/atproto"
12
+
"github.com/bluesky-social/indigo/atproto/atcrypto"
13
"github.com/bluesky-social/indigo/events"
14
"github.com/bluesky-social/indigo/repo"
15
"github.com/bluesky-social/indigo/util"
16
"github.com/haileyok/cocoon/internal/helpers"
17
"github.com/haileyok/cocoon/models"
18
"github.com/labstack/echo/v4"
···
25
Handle string `json:"handle" validate:"required,atproto-handle"`
26
Did *string `json:"did" validate:"atproto-did"`
27
Password string `json:"password" validate:"required"`
28
+
InviteCode string `json:"inviteCode" validate:"omitempty"`
29
}
30
31
type ComAtprotoServerCreateAccountResponse struct {
···
36
}
37
38
func (s *Server) handleCreateAccount(e echo.Context) error {
39
+
ctx := e.Request().Context()
40
+
logger := s.logger.With("name", "handleServerCreateAccount")
41
42
+
var request ComAtprotoServerCreateAccountRequest
43
44
if err := e.Bind(&request); err != nil {
45
+
logger.Error("error receiving request", "endpoint", "com.atproto.server.createAccount", "error", err)
46
return helpers.ServerError(e, nil)
47
}
48
49
request.Handle = strings.ToLower(request.Handle)
50
51
if err := e.Validate(request); err != nil {
52
+
logger.Error("error validating request", "endpoint", "com.atproto.server.createAccount", "error", err)
53
54
var verr ValidationError
55
if errors.As(err, &verr) {
···
72
}
73
}
74
75
+
var signupDid string
76
+
if request.Did != nil {
77
+
signupDid = *request.Did
78
+
79
+
token := strings.TrimSpace(strings.Replace(e.Request().Header.Get("authorization"), "Bearer ", "", 1))
80
+
if token == "" {
81
+
return helpers.UnauthorizedError(e, to.StringPtr("must authenticate to use an existing did"))
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
+
95
// see if the handle is already taken
96
+
actor, err := s.getActorByHandle(ctx, request.Handle)
97
if err != nil && err != gorm.ErrRecordNotFound {
98
+
logger.Error("error looking up handle in db", "endpoint", "com.atproto.server.createAccount", "error", err)
99
return helpers.ServerError(e, nil)
100
}
101
+
if err == nil && actor.Did != signupDid {
102
return helpers.InputError(e, to.StringPtr("HandleNotAvailable"))
103
}
104
105
+
if did, err := s.passport.ResolveHandle(e.Request().Context(), request.Handle); err == nil && did != signupDid {
106
return helpers.InputError(e, to.StringPtr("HandleNotAvailable"))
107
}
108
109
var ic models.InviteCode
110
+
if s.config.RequireInvite {
111
+
if strings.TrimSpace(request.InviteCode) == "" {
112
return helpers.InputError(e, to.StringPtr("InvalidInviteCode"))
113
}
114
115
+
if err := s.db.Raw(ctx, "SELECT * FROM invite_codes WHERE code = ?", nil, request.InviteCode).Scan(&ic).Error; err != nil {
116
+
if err == gorm.ErrRecordNotFound {
117
+
return helpers.InputError(e, to.StringPtr("InvalidInviteCode"))
118
+
}
119
+
logger.Error("error getting invite code from db", "error", err)
120
+
return helpers.ServerError(e, nil)
121
+
}
122
+
123
+
if ic.RemainingUseCount < 1 {
124
+
return helpers.InputError(e, to.StringPtr("InvalidInviteCode"))
125
+
}
126
}
127
128
// see if the email is already taken
129
+
existingRepo, err := s.getRepoByEmail(ctx, request.Email)
130
if err != nil && err != gorm.ErrRecordNotFound {
131
+
logger.Error("error looking up email in db", "endpoint", "com.atproto.server.createAccount", "error", err)
132
return helpers.ServerError(e, nil)
133
}
134
+
if err == nil && existingRepo.Did != signupDid {
135
return helpers.InputError(e, to.StringPtr("EmailNotAvailable"))
136
}
137
138
// TODO: unsupported domains
139
140
+
var k *atcrypto.PrivateKeyK256
141
+
142
+
if signupDid != "" {
143
+
reservedKey, err := s.getReservedKey(ctx, signupDid)
144
+
if err != nil {
145
+
logger.Error("error looking up reserved key", "error", err)
146
+
}
147
+
if reservedKey != nil {
148
+
k, err = atcrypto.ParsePrivateBytesK256(reservedKey.PrivateKey)
149
+
if err != nil {
150
+
logger.Error("error parsing reserved key", "error", err)
151
+
k = nil
152
+
} else {
153
+
defer func() {
154
+
if delErr := s.deleteReservedKey(ctx, reservedKey.KeyDid, reservedKey.Did); delErr != nil {
155
+
logger.Error("error deleting reserved key", "error", delErr)
156
+
}
157
+
}()
158
+
}
159
+
}
160
+
}
161
+
162
+
if k == nil {
163
+
k, err = atcrypto.GeneratePrivateKeyK256()
164
+
if err != nil {
165
+
logger.Error("error creating signing key", "endpoint", "com.atproto.server.createAccount", "error", err)
166
+
return helpers.ServerError(e, nil)
167
+
}
168
}
169
170
if signupDid == "" {
171
did, op, err := s.plcClient.CreateDID(k, "", request.Handle)
172
if err != nil {
173
+
logger.Error("error creating operation", "endpoint", "com.atproto.server.createAccount", "error", err)
174
return helpers.ServerError(e, nil)
175
}
176
177
if err := s.plcClient.SendOperation(e.Request().Context(), did, op); err != nil {
178
+
logger.Error("error sending plc op", "endpoint", "com.atproto.server.createAccount", "error", err)
179
return helpers.ServerError(e, nil)
180
}
181
signupDid = did
···
183
184
hashed, err := bcrypt.GenerateFromPassword([]byte(request.Password), 10)
185
if err != nil {
186
+
logger.Error("error hashing password", "error", err)
187
return helpers.ServerError(e, nil)
188
}
189
···
196
SigningKey: k.Bytes(),
197
}
198
199
+
if actor == nil {
200
+
actor = &models.Actor{
201
+
Did: signupDid,
202
+
Handle: request.Handle,
203
+
}
204
205
+
if err := s.db.Create(ctx, &urepo, nil).Error; err != nil {
206
+
logger.Error("error inserting new repo", "error", err)
207
+
return helpers.ServerError(e, nil)
208
+
}
209
210
+
if err := s.db.Create(ctx, &actor, nil).Error; err != nil {
211
+
logger.Error("error inserting new actor", "error", err)
212
+
return helpers.ServerError(e, nil)
213
+
}
214
+
} else {
215
+
if err := s.db.Save(ctx, &actor, nil).Error; err != nil {
216
+
logger.Error("error inserting new actor", "error", err)
217
+
return helpers.ServerError(e, nil)
218
+
}
219
}
220
221
+
if request.Did == nil || *request.Did == "" {
222
+
bs := s.getBlockstore(signupDid)
223
r := repo.NewRepo(context.TODO(), signupDid, bs)
224
225
root, rev, err := r.Commit(context.TODO(), urepo.SignFor)
226
if err != nil {
227
+
logger.Error("error committing", "error", err)
228
return helpers.ServerError(e, nil)
229
}
230
231
+
if err := s.UpdateRepo(context.TODO(), urepo.Did, root, rev); err != nil {
232
+
logger.Error("error updating repo after commit", "error", err)
233
return helpers.ServerError(e, nil)
234
}
235
236
s.evtman.AddEvent(context.TODO(), &events.XRPCStreamEvent{
237
RepoIdentity: &atproto.SyncSubscribeRepos_Identity{
238
Did: urepo.Did,
239
Handle: to.StringPtr(request.Handle),
···
243
})
244
}
245
246
+
if s.config.RequireInvite {
247
+
if err := s.db.Raw(ctx, "UPDATE invite_codes SET remaining_use_count = remaining_use_count - 1 WHERE code = ?", nil, request.InviteCode).Scan(&ic).Error; err != nil {
248
+
logger.Error("error decrementing use count", "error", err)
249
+
return helpers.ServerError(e, nil)
250
+
}
251
}
252
253
+
sess, err := s.createSession(ctx, &urepo)
254
if err != nil {
255
+
logger.Error("error creating new session", "error", err)
256
return helpers.ServerError(e, nil)
257
}
258
259
go func() {
260
if err := s.sendEmailVerification(urepo.Email, actor.Handle, *urepo.EmailVerificationCode); err != nil {
261
+
logger.Error("error sending email verification email", "error", err)
262
}
263
if err := s.sendWelcomeMail(urepo.Email, actor.Handle); err != nil {
264
+
logger.Error("error sending welcome email", "error", err)
265
}
266
}()
267
+7
-4
server/handle_server_create_invite_code.go
+7
-4
server/handle_server_create_invite_code.go
···
17
}
18
19
func (s *Server) handleCreateInviteCode(e echo.Context) error {
20
var req ComAtprotoServerCreateInviteCodeRequest
21
if err := e.Bind(&req); err != nil {
22
-
s.logger.Error("error binding", "error", err)
23
return helpers.ServerError(e, nil)
24
}
25
26
if err := e.Validate(req); err != nil {
27
-
s.logger.Error("error validating", "error", err)
28
return helpers.InputError(e, nil)
29
}
30
···
37
acc = *req.ForAccount
38
}
39
40
-
if err := s.db.Create(&models.InviteCode{
41
Code: ic,
42
Did: acc,
43
RemainingUseCount: req.UseCount,
44
}, nil).Error; err != nil {
45
-
s.logger.Error("error creating invite code", "error", err)
46
return helpers.ServerError(e, nil)
47
}
48
···
17
}
18
19
func (s *Server) handleCreateInviteCode(e echo.Context) error {
20
+
ctx := e.Request().Context()
21
+
logger := s.logger.With("name", "handleServerCreateInviteCode")
22
+
23
var req ComAtprotoServerCreateInviteCodeRequest
24
if err := e.Bind(&req); err != nil {
25
+
logger.Error("error binding", "error", err)
26
return helpers.ServerError(e, nil)
27
}
28
29
if err := e.Validate(req); err != nil {
30
+
logger.Error("error validating", "error", err)
31
return helpers.InputError(e, nil)
32
}
33
···
40
acc = *req.ForAccount
41
}
42
43
+
if err := s.db.Create(ctx, &models.InviteCode{
44
Code: ic,
45
Did: acc,
46
RemainingUseCount: req.UseCount,
47
}, nil).Error; err != nil {
48
+
logger.Error("error creating invite code", "error", err)
49
return helpers.ServerError(e, nil)
50
}
51
+7
-4
server/handle_server_create_invite_codes.go
+7
-4
server/handle_server_create_invite_codes.go
···
22
}
23
24
func (s *Server) handleCreateInviteCodes(e echo.Context) error {
25
var req ComAtprotoServerCreateInviteCodesRequest
26
if err := e.Bind(&req); err != nil {
27
-
s.logger.Error("error binding", "error", err)
28
return helpers.ServerError(e, nil)
29
}
30
31
if err := e.Validate(req); err != nil {
32
-
s.logger.Error("error validating", "error", err)
33
return helpers.InputError(e, nil)
34
}
35
···
50
ic := uuid.NewString()
51
ics = append(ics, ic)
52
53
-
if err := s.db.Create(&models.InviteCode{
54
Code: ic,
55
Did: did,
56
RemainingUseCount: req.UseCount,
57
}, nil).Error; err != nil {
58
-
s.logger.Error("error creating invite code", "error", err)
59
return helpers.ServerError(e, nil)
60
}
61
}
···
22
}
23
24
func (s *Server) handleCreateInviteCodes(e echo.Context) error {
25
+
ctx := e.Request().Context()
26
+
logger := s.logger.With("name", "handleServerCreateInviteCodes")
27
+
28
var req ComAtprotoServerCreateInviteCodesRequest
29
if err := e.Bind(&req); err != nil {
30
+
logger.Error("error binding", "error", err)
31
return helpers.ServerError(e, nil)
32
}
33
34
if err := e.Validate(req); err != nil {
35
+
logger.Error("error validating", "error", err)
36
return helpers.InputError(e, nil)
37
}
38
···
53
ic := uuid.NewString()
54
ics = append(ics, ic)
55
56
+
if err := s.db.Create(ctx, &models.InviteCode{
57
Code: ic,
58
Did: did,
59
RemainingUseCount: req.UseCount,
60
}, nil).Error; err != nil {
61
+
logger.Error("error creating invite code", "error", err)
62
return helpers.ServerError(e, nil)
63
}
64
}
+67
-11
server/handle_server_create_session.go
+67
-11
server/handle_server_create_session.go
···
1
package server
2
3
import (
4
"errors"
5
"strings"
6
7
"github.com/Azure/go-autorest/autorest/to"
8
"github.com/bluesky-social/indigo/atproto/syntax"
···
32
}
33
34
func (s *Server) handleCreateSession(e echo.Context) error {
35
var req ComAtprotoServerCreateSessionRequest
36
if err := e.Bind(&req); err != nil {
37
-
s.logger.Error("error binding request", "endpoint", "com.atproto.server.serverCreateSession", "error", err)
38
return helpers.ServerError(e, nil)
39
}
40
···
65
var err error
66
switch idtype {
67
case "did":
68
-
err = s.db.Raw("SELECT r.*, a.* FROM repos r LEFT JOIN actors a ON r.did = a.did WHERE r.did = ?", nil, req.Identifier).Scan(&repo).Error
69
case "handle":
70
-
err = s.db.Raw("SELECT r.*, a.* FROM actors a LEFT JOIN repos r ON a.did = r.did WHERE a.handle = ?", nil, req.Identifier).Scan(&repo).Error
71
case "email":
72
-
err = s.db.Raw("SELECT r.*, a.* FROM repos r LEFT JOIN actors a ON r.did = a.did WHERE r.email = ?", nil, req.Identifier).Scan(&repo).Error
73
}
74
75
if err != nil {
···
77
return helpers.InputError(e, to.StringPtr("InvalidRequest"))
78
}
79
80
-
s.logger.Error("erorr looking up repo", "endpoint", "com.atproto.server.createSession", "error", err)
81
return helpers.ServerError(e, nil)
82
}
83
84
if err := bcrypt.CompareHashAndPassword([]byte(repo.Password), []byte(req.Password)); err != nil {
85
if err != bcrypt.ErrMismatchedHashAndPassword {
86
-
s.logger.Error("erorr comparing hash and password", "error", err)
87
}
88
return helpers.InputError(e, to.StringPtr("InvalidRequest"))
89
}
90
91
-
sess, err := s.createSession(&repo.Repo)
92
if err != nil {
93
-
s.logger.Error("error creating session", "error", err)
94
return helpers.ServerError(e, nil)
95
}
96
···
101
Did: repo.Repo.Did,
102
Email: repo.Email,
103
EmailConfirmed: repo.EmailConfirmedAt != nil,
104
-
EmailAuthFactor: false,
105
-
Active: true, // TODO: eventually do takedowns
106
-
Status: nil, // TODO eventually do takedowns
107
})
108
}
···
1
package server
2
3
import (
4
+
"context"
5
"errors"
6
+
"fmt"
7
"strings"
8
+
"time"
9
10
"github.com/Azure/go-autorest/autorest/to"
11
"github.com/bluesky-social/indigo/atproto/syntax"
···
35
}
36
37
func (s *Server) handleCreateSession(e echo.Context) error {
38
+
ctx := e.Request().Context()
39
+
logger := s.logger.With("name", "handleServerCreateSession")
40
+
41
var req ComAtprotoServerCreateSessionRequest
42
if err := e.Bind(&req); err != nil {
43
+
logger.Error("error binding request", "endpoint", "com.atproto.server.serverCreateSession", "error", err)
44
return helpers.ServerError(e, nil)
45
}
46
···
71
var err error
72
switch idtype {
73
case "did":
74
+
err = s.db.Raw(ctx, "SELECT r.*, a.* FROM repos r LEFT JOIN actors a ON r.did = a.did WHERE r.did = ?", nil, req.Identifier).Scan(&repo).Error
75
case "handle":
76
+
err = s.db.Raw(ctx, "SELECT r.*, a.* FROM actors a LEFT JOIN repos r ON a.did = r.did WHERE a.handle = ?", nil, req.Identifier).Scan(&repo).Error
77
case "email":
78
+
err = s.db.Raw(ctx, "SELECT r.*, a.* FROM repos r LEFT JOIN actors a ON r.did = a.did WHERE r.email = ?", nil, req.Identifier).Scan(&repo).Error
79
}
80
81
if err != nil {
···
83
return helpers.InputError(e, to.StringPtr("InvalidRequest"))
84
}
85
86
+
logger.Error("erorr looking up repo", "endpoint", "com.atproto.server.createSession", "error", err)
87
return helpers.ServerError(e, nil)
88
}
89
90
if err := bcrypt.CompareHashAndPassword([]byte(repo.Password), []byte(req.Password)); err != nil {
91
if err != bcrypt.ErrMismatchedHashAndPassword {
92
+
logger.Error("erorr comparing hash and password", "error", err)
93
}
94
return helpers.InputError(e, to.StringPtr("InvalidRequest"))
95
}
96
97
+
// if repo requires 2FA token and one hasn't been provided, return error prompting for one
98
+
if repo.TwoFactorType != models.TwoFactorTypeNone && (req.AuthFactorToken == nil || *req.AuthFactorToken == "") {
99
+
err = s.createAndSendTwoFactorCode(ctx, repo)
100
+
if err != nil {
101
+
logger.Error("sending 2FA code", "error", err)
102
+
return helpers.ServerError(e, nil)
103
+
}
104
+
105
+
return helpers.InputError(e, to.StringPtr("AuthFactorTokenRequired"))
106
+
}
107
+
108
+
// if 2FA is required, now check that the one provided is valid
109
+
if repo.TwoFactorType != models.TwoFactorTypeNone {
110
+
if repo.TwoFactorCode == nil || repo.TwoFactorCodeExpiresAt == nil {
111
+
err = s.createAndSendTwoFactorCode(ctx, repo)
112
+
if err != nil {
113
+
logger.Error("sending 2FA code", "error", err)
114
+
return helpers.ServerError(e, nil)
115
+
}
116
+
117
+
return helpers.InputError(e, to.StringPtr("AuthFactorTokenRequired"))
118
+
}
119
+
120
+
if *repo.TwoFactorCode != *req.AuthFactorToken {
121
+
return helpers.InvalidTokenError(e)
122
+
}
123
+
124
+
if time.Now().UTC().After(*repo.TwoFactorCodeExpiresAt) {
125
+
return helpers.ExpiredTokenError(e)
126
+
}
127
+
}
128
+
129
+
sess, err := s.createSession(ctx, &repo.Repo)
130
if err != nil {
131
+
logger.Error("error creating session", "error", err)
132
return helpers.ServerError(e, nil)
133
}
134
···
139
Did: repo.Repo.Did,
140
Email: repo.Email,
141
EmailConfirmed: repo.EmailConfirmedAt != nil,
142
+
EmailAuthFactor: repo.TwoFactorType != models.TwoFactorTypeNone,
143
+
Active: repo.Active(),
144
+
Status: repo.Status(),
145
})
146
}
147
+
148
+
func (s *Server) createAndSendTwoFactorCode(ctx context.Context, repo models.RepoActor) error {
149
+
// TODO: when implementing a new type of 2FA there should be some logic in here to send the
150
+
// right type of code
151
+
152
+
code := fmt.Sprintf("%s-%s", helpers.RandomVarchar(5), helpers.RandomVarchar(5))
153
+
eat := time.Now().Add(10 * time.Minute).UTC()
154
+
155
+
if err := s.db.Exec(ctx, "UPDATE repos SET two_factor_code = ?, two_factor_code_expires_at = ? WHERE did = ?", nil, code, eat, repo.Repo.Did).Error; err != nil {
156
+
return fmt.Errorf("updating repo: %w", err)
157
+
}
158
+
159
+
if err := s.sendTwoFactorCode(repo.Email, repo.Handle, code); err != nil {
160
+
return fmt.Errorf("sending email: %w", err)
161
+
}
162
+
163
+
return nil
164
+
}
+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
)
8
9
func (s *Server) handleDeleteSession(e echo.Context) error {
10
token := e.Get("token").(string)
11
12
var acctok models.Token
13
-
if err := s.db.Raw("DELETE FROM tokens WHERE token = ? RETURNING *", nil, token).Scan(&acctok).Error; err != nil {
14
s.logger.Error("error deleting access token from db", "error", err)
15
return helpers.ServerError(e, nil)
16
}
17
18
-
if err := s.db.Exec("DELETE FROM refresh_tokens WHERE token = ?", nil, acctok.RefreshToken).Error; err != nil {
19
s.logger.Error("error deleting refresh token from db", "error", err)
20
return helpers.ServerError(e, nil)
21
}
···
7
)
8
9
func (s *Server) handleDeleteSession(e echo.Context) error {
10
+
ctx := e.Request().Context()
11
+
12
token := e.Get("token").(string)
13
14
var acctok models.Token
15
+
if err := s.db.Raw(ctx, "DELETE FROM tokens WHERE token = ? RETURNING *", nil, token).Scan(&acctok).Error; err != nil {
16
s.logger.Error("error deleting access token from db", "error", err)
17
return helpers.ServerError(e, nil)
18
}
19
20
+
if err := s.db.Exec(ctx, "DELETE FROM refresh_tokens WHERE token = ?", nil, acctok.RefreshToken).Error; err != nil {
21
s.logger.Error("error deleting refresh token from db", "error", err)
22
return helpers.ServerError(e, nil)
23
}
+1
-1
server/handle_server_describe_server.go
+1
-1
server/handle_server_describe_server.go
···
22
23
func (s *Server) handleDescribeServer(e echo.Context) error {
24
return e.JSON(200, ComAtprotoServerDescribeServerResponse{
25
-
InviteCodeRequired: true,
26
PhoneVerificationRequired: false,
27
AvailableUserDomains: []string{"." + s.config.Hostname}, // TODO: more
28
Links: ComAtprotoServerDescribeServerResponseLinks{
···
22
23
func (s *Server) handleDescribeServer(e echo.Context) error {
24
return e.JSON(200, ComAtprotoServerDescribeServerResponse{
25
+
InviteCodeRequired: s.config.RequireInvite,
26
PhoneVerificationRequired: false,
27
AvailableUserDomains: []string{"." + s.config.Hostname}, // TODO: more
28
Links: ComAtprotoServerDescribeServerResponseLinks{
+17
-8
server/handle_server_get_service_auth.go
+17
-8
server/handle_server_get_service_auth.go
···
21
Aud string `query:"aud" validate:"required,atproto-did"`
22
// exp should be a float, as some clients will send a non-integer expiration
23
Exp float64 `query:"exp"`
24
-
Lxm string `query:"lxm" validate:"required,atproto-nsid"`
25
}
26
27
func (s *Server) handleServerGetServiceAuth(e echo.Context) error {
28
var req ServerGetServiceAuthRequest
29
if err := e.Bind(&req); err != nil {
30
-
s.logger.Error("could not bind service auth request", "error", err)
31
return helpers.ServerError(e, nil)
32
}
33
···
45
return helpers.InputError(e, to.StringPtr("may not generate auth tokens recursively"))
46
}
47
48
-
maxExp := now + (60 * 30)
49
if exp > maxExp {
50
return helpers.InputError(e, to.StringPtr("expiration too big. smoller please"))
51
}
···
59
}
60
hj, err := json.Marshal(header)
61
if err != nil {
62
-
s.logger.Error("error marshaling header", "error", err)
63
return helpers.ServerError(e, nil)
64
}
65
···
68
payload := map[string]any{
69
"iss": repo.Repo.Did,
70
"aud": req.Aud,
71
-
"lxm": req.Lxm,
72
"jti": uuid.NewString(),
73
"exp": exp,
74
"iat": now,
75
}
76
pj, err := json.Marshal(payload)
77
if err != nil {
78
-
s.logger.Error("error marashaling payload", "error", err)
79
return helpers.ServerError(e, nil)
80
}
81
···
86
87
sk, err := secp256k1secec.NewPrivateKey(repo.SigningKey)
88
if err != nil {
89
-
s.logger.Error("can't load private key", "error", err)
90
return err
91
}
92
93
R, S, _, err := sk.SignRaw(rand.Reader, hash[:])
94
if err != nil {
95
-
s.logger.Error("error signing", "error", err)
96
return helpers.ServerError(e, nil)
97
}
98
···
21
Aud string `query:"aud" validate:"required,atproto-did"`
22
// exp should be a float, as some clients will send a non-integer expiration
23
Exp float64 `query:"exp"`
24
+
Lxm string `query:"lxm"`
25
}
26
27
func (s *Server) handleServerGetServiceAuth(e echo.Context) error {
28
+
logger := s.logger.With("name", "handleServerGetServiceAuth")
29
+
30
var req ServerGetServiceAuthRequest
31
if err := e.Bind(&req); err != nil {
32
+
logger.Error("could not bind service auth request", "error", err)
33
return helpers.ServerError(e, nil)
34
}
35
···
47
return helpers.InputError(e, to.StringPtr("may not generate auth tokens recursively"))
48
}
49
50
+
var maxExp int64
51
+
if req.Lxm != "" {
52
+
maxExp = now + (60 * 60)
53
+
} else {
54
+
maxExp = now + 60
55
+
}
56
if exp > maxExp {
57
return helpers.InputError(e, to.StringPtr("expiration too big. smoller please"))
58
}
···
66
}
67
hj, err := json.Marshal(header)
68
if err != nil {
69
+
logger.Error("error marshaling header", "error", err)
70
return helpers.ServerError(e, nil)
71
}
72
···
75
payload := map[string]any{
76
"iss": repo.Repo.Did,
77
"aud": req.Aud,
78
"jti": uuid.NewString(),
79
"exp": exp,
80
"iat": now,
81
+
}
82
+
if req.Lxm != "" {
83
+
payload["lxm"] = req.Lxm
84
}
85
pj, err := json.Marshal(payload)
86
if err != nil {
87
+
logger.Error("error marashaling payload", "error", err)
88
return helpers.ServerError(e, nil)
89
}
90
···
95
96
sk, err := secp256k1secec.NewPrivateKey(repo.SigningKey)
97
if err != nil {
98
+
logger.Error("can't load private key", "error", err)
99
return err
100
}
101
102
R, S, _, err := sk.SignRaw(rand.Reader, hash[:])
103
if err != nil {
104
+
logger.Error("error signing", "error", err)
105
return helpers.ServerError(e, nil)
106
}
107
+3
-3
server/handle_server_get_session.go
+3
-3
server/handle_server_get_session.go
+11
-8
server/handle_server_refresh_session.go
+11
-8
server/handle_server_refresh_session.go
···
16
}
17
18
func (s *Server) handleRefreshSession(e echo.Context) error {
19
token := e.Get("token").(string)
20
repo := e.Get("repo").(*models.RepoActor)
21
22
-
if err := s.db.Exec("DELETE FROM refresh_tokens WHERE token = ?", nil, token).Error; err != nil {
23
-
s.logger.Error("error getting refresh token from db", "error", err)
24
return helpers.ServerError(e, nil)
25
}
26
27
-
if err := s.db.Exec("DELETE FROM tokens WHERE refresh_token = ?", nil, token).Error; err != nil {
28
-
s.logger.Error("error deleting access token from db", "error", err)
29
return helpers.ServerError(e, nil)
30
}
31
32
-
sess, err := s.createSession(&repo.Repo)
33
if err != nil {
34
-
s.logger.Error("error creating new session for refresh", "error", err)
35
return helpers.ServerError(e, nil)
36
}
37
···
40
RefreshJwt: sess.RefreshToken,
41
Handle: repo.Handle,
42
Did: repo.Repo.Did,
43
-
Active: true,
44
-
Status: nil,
45
})
46
}
···
16
}
17
18
func (s *Server) handleRefreshSession(e echo.Context) error {
19
+
ctx := e.Request().Context()
20
+
logger := s.logger.With("name", "handleServerRefreshSession")
21
+
22
token := e.Get("token").(string)
23
repo := e.Get("repo").(*models.RepoActor)
24
25
+
if err := s.db.Exec(ctx, "DELETE FROM refresh_tokens WHERE token = ?", nil, token).Error; err != nil {
26
+
logger.Error("error getting refresh token from db", "error", err)
27
return helpers.ServerError(e, nil)
28
}
29
30
+
if err := s.db.Exec(ctx, "DELETE FROM tokens WHERE refresh_token = ?", nil, token).Error; err != nil {
31
+
logger.Error("error deleting access token from db", "error", err)
32
return helpers.ServerError(e, nil)
33
}
34
35
+
sess, err := s.createSession(ctx, &repo.Repo)
36
if err != nil {
37
+
logger.Error("error creating new session for refresh", "error", err)
38
return helpers.ServerError(e, nil)
39
}
40
···
43
RefreshJwt: sess.RefreshToken,
44
Handle: repo.Handle,
45
Did: repo.Repo.Did,
46
+
Active: repo.Active(),
47
+
Status: repo.Status(),
48
})
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
)
12
13
func (s *Server) handleServerRequestEmailConfirmation(e echo.Context) error {
14
urepo := e.Get("repo").(*models.RepoActor)
15
16
if urepo.EmailConfirmedAt != nil {
···
20
code := fmt.Sprintf("%s-%s", helpers.RandomVarchar(5), helpers.RandomVarchar(5))
21
eat := time.Now().Add(10 * time.Minute).UTC()
22
23
-
if err := s.db.Exec("UPDATE repos SET email_verification_code = ?, email_verification_code_expires_at = ? WHERE did = ?", nil, code, eat, urepo.Repo.Did).Error; err != nil {
24
-
s.logger.Error("error updating user", "error", err)
25
return helpers.ServerError(e, nil)
26
}
27
28
if err := s.sendEmailVerification(urepo.Email, urepo.Handle, code); err != nil {
29
-
s.logger.Error("error sending mail", "error", err)
30
return helpers.ServerError(e, nil)
31
}
32
···
11
)
12
13
func (s *Server) handleServerRequestEmailConfirmation(e echo.Context) error {
14
+
ctx := e.Request().Context()
15
+
logger := s.logger.With("name", "handleServerRequestEmailConfirm")
16
+
17
urepo := e.Get("repo").(*models.RepoActor)
18
19
if urepo.EmailConfirmedAt != nil {
···
23
code := fmt.Sprintf("%s-%s", helpers.RandomVarchar(5), helpers.RandomVarchar(5))
24
eat := time.Now().Add(10 * time.Minute).UTC()
25
26
+
if err := s.db.Exec(ctx, "UPDATE repos SET email_verification_code = ?, email_verification_code_expires_at = ? WHERE did = ?", nil, code, eat, urepo.Repo.Did).Error; err != nil {
27
+
logger.Error("error updating user", "error", err)
28
return helpers.ServerError(e, nil)
29
}
30
31
if err := s.sendEmailVerification(urepo.Email, urepo.Handle, code); err != nil {
32
+
logger.Error("error sending mail", "error", err)
33
return helpers.ServerError(e, nil)
34
}
35
+6
-3
server/handle_server_request_email_update.go
+6
-3
server/handle_server_request_email_update.go
···
14
}
15
16
func (s *Server) handleServerRequestEmailUpdate(e echo.Context) error {
17
urepo := e.Get("repo").(*models.RepoActor)
18
19
if urepo.EmailConfirmedAt != nil {
20
code := fmt.Sprintf("%s-%s", helpers.RandomVarchar(5), helpers.RandomVarchar(5))
21
eat := time.Now().Add(10 * time.Minute).UTC()
22
23
-
if err := s.db.Exec("UPDATE repos SET email_update_code = ?, email_update_code_expires_at = ? WHERE did = ?", nil, code, eat, urepo.Repo.Did).Error; err != nil {
24
-
s.logger.Error("error updating repo", "error", err)
25
return helpers.ServerError(e, nil)
26
}
27
28
if err := s.sendEmailUpdate(urepo.Email, urepo.Handle, code); err != nil {
29
-
s.logger.Error("error sending email", "error", err)
30
return helpers.ServerError(e, nil)
31
}
32
}
···
14
}
15
16
func (s *Server) handleServerRequestEmailUpdate(e echo.Context) error {
17
+
ctx := e.Request().Context()
18
+
logger := s.logger.With("name", "handleServerRequestEmailUpdate")
19
+
20
urepo := e.Get("repo").(*models.RepoActor)
21
22
if urepo.EmailConfirmedAt != nil {
23
code := fmt.Sprintf("%s-%s", helpers.RandomVarchar(5), helpers.RandomVarchar(5))
24
eat := time.Now().Add(10 * time.Minute).UTC()
25
26
+
if err := s.db.Exec(ctx, "UPDATE repos SET email_update_code = ?, email_update_code_expires_at = ? WHERE did = ?", nil, code, eat, urepo.Repo.Did).Error; err != nil {
27
+
logger.Error("error updating repo", "error", err)
28
return helpers.ServerError(e, nil)
29
}
30
31
if err := s.sendEmailUpdate(urepo.Email, urepo.Handle, code); err != nil {
32
+
logger.Error("error sending email", "error", err)
33
return helpers.ServerError(e, nil)
34
}
35
}
+7
-4
server/handle_server_request_password_reset.go
+7
-4
server/handle_server_request_password_reset.go
···
14
}
15
16
func (s *Server) handleServerRequestPasswordReset(e echo.Context) error {
17
urepo, ok := e.Get("repo").(*models.RepoActor)
18
if !ok {
19
var req ComAtprotoServerRequestPasswordResetRequest
···
25
return err
26
}
27
28
-
murepo, err := s.getRepoActorByEmail(req.Email)
29
if err != nil {
30
return err
31
}
···
36
code := fmt.Sprintf("%s-%s", helpers.RandomVarchar(5), helpers.RandomVarchar(5))
37
eat := time.Now().Add(10 * time.Minute).UTC()
38
39
-
if err := s.db.Exec("UPDATE repos SET password_reset_code = ?, password_reset_code_expires_at = ? WHERE did = ?", nil, code, eat, urepo.Repo.Did).Error; err != nil {
40
-
s.logger.Error("error updating repo", "error", err)
41
return helpers.ServerError(e, nil)
42
}
43
44
if err := s.sendPasswordReset(urepo.Email, urepo.Handle, code); err != nil {
45
-
s.logger.Error("error sending email", "error", err)
46
return helpers.ServerError(e, nil)
47
}
48
···
14
}
15
16
func (s *Server) handleServerRequestPasswordReset(e echo.Context) error {
17
+
ctx := e.Request().Context()
18
+
logger := s.logger.With("name", "handleServerRequestPasswordReset")
19
+
20
urepo, ok := e.Get("repo").(*models.RepoActor)
21
if !ok {
22
var req ComAtprotoServerRequestPasswordResetRequest
···
28
return err
29
}
30
31
+
murepo, err := s.getRepoActorByEmail(ctx, req.Email)
32
if err != nil {
33
return err
34
}
···
39
code := fmt.Sprintf("%s-%s", helpers.RandomVarchar(5), helpers.RandomVarchar(5))
40
eat := time.Now().Add(10 * time.Minute).UTC()
41
42
+
if err := s.db.Exec(ctx, "UPDATE repos SET password_reset_code = ?, password_reset_code_expires_at = ? WHERE did = ?", nil, code, eat, urepo.Repo.Did).Error; err != nil {
43
+
logger.Error("error updating repo", "error", err)
44
return helpers.ServerError(e, nil)
45
}
46
47
if err := s.sendPasswordReset(urepo.Email, urepo.Handle, code); err != nil {
48
+
logger.Error("error sending email", "error", err)
49
return helpers.ServerError(e, nil)
50
}
51
+99
server/handle_server_reserve_signing_key.go
+99
server/handle_server_reserve_signing_key.go
···
···
1
+
package server
2
+
3
+
import (
4
+
"context"
5
+
"time"
6
+
7
+
"github.com/bluesky-social/indigo/atproto/atcrypto"
8
+
"github.com/haileyok/cocoon/internal/helpers"
9
+
"github.com/haileyok/cocoon/models"
10
+
"github.com/labstack/echo/v4"
11
+
)
12
+
13
+
type ServerReserveSigningKeyRequest struct {
14
+
Did *string `json:"did"`
15
+
}
16
+
17
+
type ServerReserveSigningKeyResponse struct {
18
+
SigningKey string `json:"signingKey"`
19
+
}
20
+
21
+
func (s *Server) handleServerReserveSigningKey(e echo.Context) error {
22
+
ctx := e.Request().Context()
23
+
logger := s.logger.With("name", "handleServerReserveSigningKey")
24
+
25
+
var req ServerReserveSigningKeyRequest
26
+
if err := e.Bind(&req); err != nil {
27
+
logger.Error("could not bind reserve signing key request", "error", err)
28
+
return helpers.ServerError(e, nil)
29
+
}
30
+
31
+
if req.Did != nil && *req.Did != "" {
32
+
var existing models.ReservedKey
33
+
if err := s.db.Raw(ctx, "SELECT * FROM reserved_keys WHERE did = ?", nil, *req.Did).Scan(&existing).Error; err == nil && existing.KeyDid != "" {
34
+
return e.JSON(200, ServerReserveSigningKeyResponse{
35
+
SigningKey: existing.KeyDid,
36
+
})
37
+
}
38
+
}
39
+
40
+
k, err := atcrypto.GeneratePrivateKeyK256()
41
+
if err != nil {
42
+
logger.Error("error creating signing key", "endpoint", "com.atproto.server.reserveSigningKey", "error", err)
43
+
return helpers.ServerError(e, nil)
44
+
}
45
+
46
+
pubKey, err := k.PublicKey()
47
+
if err != nil {
48
+
logger.Error("error getting public key", "endpoint", "com.atproto.server.reserveSigningKey", "error", err)
49
+
return helpers.ServerError(e, nil)
50
+
}
51
+
52
+
keyDid := pubKey.DIDKey()
53
+
54
+
reservedKey := models.ReservedKey{
55
+
KeyDid: keyDid,
56
+
Did: req.Did,
57
+
PrivateKey: k.Bytes(),
58
+
CreatedAt: time.Now(),
59
+
}
60
+
61
+
if err := s.db.Create(ctx, &reservedKey, nil).Error; err != nil {
62
+
logger.Error("error storing reserved key", "endpoint", "com.atproto.server.reserveSigningKey", "error", err)
63
+
return helpers.ServerError(e, nil)
64
+
}
65
+
66
+
logger.Info("reserved signing key", "keyDid", keyDid, "forDid", req.Did)
67
+
68
+
return e.JSON(200, ServerReserveSigningKeyResponse{
69
+
SigningKey: keyDid,
70
+
})
71
+
}
72
+
73
+
func (s *Server) getReservedKey(ctx context.Context, keyDidOrDid string) (*models.ReservedKey, error) {
74
+
var reservedKey models.ReservedKey
75
+
76
+
if err := s.db.Raw(ctx, "SELECT * FROM reserved_keys WHERE key_did = ?", nil, keyDidOrDid).Scan(&reservedKey).Error; err == nil && reservedKey.KeyDid != "" {
77
+
return &reservedKey, nil
78
+
}
79
+
80
+
if err := s.db.Raw(ctx, "SELECT * FROM reserved_keys WHERE did = ?", nil, keyDidOrDid).Scan(&reservedKey).Error; err == nil && reservedKey.KeyDid != "" {
81
+
return &reservedKey, nil
82
+
}
83
+
84
+
return nil, nil
85
+
}
86
+
87
+
func (s *Server) deleteReservedKey(ctx context.Context, keyDid string, did *string) error {
88
+
if err := s.db.Exec(ctx, "DELETE FROM reserved_keys WHERE key_did = ?", nil, keyDid).Error; err != nil {
89
+
return err
90
+
}
91
+
92
+
if did != nil && *did != "" {
93
+
if err := s.db.Exec(ctx, "DELETE FROM reserved_keys WHERE did = ?", nil, *did).Error; err != nil {
94
+
return err
95
+
}
96
+
}
97
+
98
+
return nil
99
+
}
+7
-4
server/handle_server_reset_password.go
+7
-4
server/handle_server_reset_password.go
···
16
}
17
18
func (s *Server) handleServerResetPassword(e echo.Context) error {
19
urepo := e.Get("repo").(*models.RepoActor)
20
21
var req ComAtprotoServerResetPasswordRequest
22
if err := e.Bind(&req); err != nil {
23
-
s.logger.Error("error binding", "error", err)
24
return helpers.ServerError(e, nil)
25
}
26
···
42
43
hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), 10)
44
if err != nil {
45
-
s.logger.Error("error creating hash", "error", err)
46
return helpers.ServerError(e, nil)
47
}
48
49
-
if err := s.db.Exec("UPDATE repos SET password_reset_code = NULL, password_reset_code_expires_at = NULL, password = ? WHERE did = ?", nil, hash, urepo.Repo.Did).Error; err != nil {
50
-
s.logger.Error("error updating repo", "error", err)
51
return helpers.ServerError(e, nil)
52
}
53
···
16
}
17
18
func (s *Server) handleServerResetPassword(e echo.Context) error {
19
+
ctx := e.Request().Context()
20
+
logger := s.logger.With("name", "handleServerResetPassword")
21
+
22
urepo := e.Get("repo").(*models.RepoActor)
23
24
var req ComAtprotoServerResetPasswordRequest
25
if err := e.Bind(&req); err != nil {
26
+
logger.Error("error binding", "error", err)
27
return helpers.ServerError(e, nil)
28
}
29
···
45
46
hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), 10)
47
if err != nil {
48
+
logger.Error("error creating hash", "error", err)
49
return helpers.ServerError(e, nil)
50
}
51
52
+
if err := s.db.Exec(ctx, "UPDATE repos SET password_reset_code = NULL, password_reset_code_expires_at = NULL, password = ? WHERE did = ?", nil, hash, urepo.Repo.Did).Error; err != nil {
53
+
logger.Error("error updating repo", "error", err)
54
return helpers.ServerError(e, nil)
55
}
56
+3
-1
server/handle_server_resolve_handle.go
+3
-1
server/handle_server_resolve_handle.go
···
10
)
11
12
func (s *Server) handleResolveHandle(e echo.Context) error {
13
type Resp struct {
14
Did string `json:"did"`
15
}
···
28
ctx := context.WithValue(e.Request().Context(), "skip-cache", true)
29
did, err := s.passport.ResolveHandle(ctx, parsed.String())
30
if err != nil {
31
-
s.logger.Error("error resolving handle", "error", err)
32
return helpers.ServerError(e, nil)
33
}
34
···
10
)
11
12
func (s *Server) handleResolveHandle(e echo.Context) error {
13
+
logger := s.logger.With("name", "handleServerResolveHandle")
14
+
15
type Resp struct {
16
Did string `json:"did"`
17
}
···
30
ctx := context.WithValue(e.Request().Context(), "skip-cache", true)
31
did, err := s.passport.ResolveHandle(ctx, parsed.String())
32
if err != nil {
33
+
logger.Error("error resolving handle", "error", err)
34
return helpers.ServerError(e, nil)
35
}
36
+34
-9
server/handle_server_update_email.go
+34
-9
server/handle_server_update_email.go
···
11
type ComAtprotoServerUpdateEmailRequest struct {
12
Email string `json:"email" validate:"required"`
13
EmailAuthFactor bool `json:"emailAuthFactor"`
14
-
Token string `json:"token" validate:"required"`
15
}
16
17
func (s *Server) handleServerUpdateEmail(e echo.Context) error {
18
urepo := e.Get("repo").(*models.RepoActor)
19
20
var req ComAtprotoServerUpdateEmailRequest
21
if err := e.Bind(&req); err != nil {
22
-
s.logger.Error("error binding", "error", err)
23
return helpers.ServerError(e, nil)
24
}
25
···
27
return helpers.InputError(e, nil)
28
}
29
30
-
if urepo.EmailUpdateCode == nil || urepo.EmailUpdateCodeExpiresAt == nil {
31
return helpers.InvalidTokenError(e)
32
}
33
34
-
if *urepo.EmailUpdateCode != req.Token {
35
-
return helpers.InvalidTokenError(e)
36
}
37
38
-
if time.Now().UTC().After(*urepo.EmailUpdateCodeExpiresAt) {
39
-
return helpers.ExpiredTokenError(e)
40
}
41
42
-
if err := s.db.Exec("UPDATE repos SET email_update_code = NULL, email_update_code_expires_at = NULL, email_confirmed_at = NULL, email = ? WHERE did = ?", nil, req.Email, urepo.Repo.Did).Error; err != nil {
43
-
s.logger.Error("error updating repo", "error", err)
44
return helpers.ServerError(e, nil)
45
}
46
···
11
type ComAtprotoServerUpdateEmailRequest struct {
12
Email string `json:"email" validate:"required"`
13
EmailAuthFactor bool `json:"emailAuthFactor"`
14
+
Token string `json:"token"`
15
}
16
17
func (s *Server) handleServerUpdateEmail(e echo.Context) error {
18
+
ctx := e.Request().Context()
19
+
logger := s.logger.With("name", "handleServerUpdateEmail")
20
+
21
urepo := e.Get("repo").(*models.RepoActor)
22
23
var req ComAtprotoServerUpdateEmailRequest
24
if err := e.Bind(&req); err != nil {
25
+
logger.Error("error binding", "error", err)
26
return helpers.ServerError(e, nil)
27
}
28
···
30
return helpers.InputError(e, nil)
31
}
32
33
+
// To disable email auth factor a token is required.
34
+
// To enable email auth factor a token is not required.
35
+
// If updating an email address, a token will be sent anyway
36
+
if urepo.TwoFactorType != models.TwoFactorTypeNone && req.EmailAuthFactor == false && req.Token == "" {
37
return helpers.InvalidTokenError(e)
38
}
39
40
+
if req.Token != "" {
41
+
if urepo.EmailUpdateCode == nil || urepo.EmailUpdateCodeExpiresAt == nil {
42
+
return helpers.InvalidTokenError(e)
43
+
}
44
+
45
+
if *urepo.EmailUpdateCode != req.Token {
46
+
return helpers.InvalidTokenError(e)
47
+
}
48
+
49
+
if time.Now().UTC().After(*urepo.EmailUpdateCodeExpiresAt) {
50
+
return helpers.ExpiredTokenError(e)
51
+
}
52
+
}
53
+
54
+
twoFactorType := models.TwoFactorTypeNone
55
+
if req.EmailAuthFactor {
56
+
twoFactorType = models.TwoFactorTypeEmail
57
}
58
59
+
query := "UPDATE repos SET email_update_code = NULL, email_update_code_expires_at = NULL, two_factor_type = ?, email = ?"
60
+
61
+
if urepo.Email != req.Email {
62
+
query += ",email_confirmed_at = NULL"
63
}
64
65
+
query += " WHERE did = ?"
66
+
67
+
if err := s.db.Exec(ctx, query, nil, twoFactorType, req.Email, urepo.Repo.Did).Error; err != nil {
68
+
logger.Error("error updating repo", "error", err)
69
return helpers.ServerError(e, nil)
70
}
71
+96
-10
server/handle_sync_get_blob.go
+96
-10
server/handle_sync_get_blob.go
···
2
3
import (
4
"bytes"
5
6
"github.com/haileyok/cocoon/internal/helpers"
7
"github.com/haileyok/cocoon/models"
8
"github.com/ipfs/go-cid"
···
10
)
11
12
func (s *Server) handleSyncGetBlob(e echo.Context) error {
13
did := e.QueryParam("did")
14
if did == "" {
15
return helpers.InputError(e, nil)
···
25
return helpers.InputError(e, nil)
26
}
27
28
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)
31
return helpers.ServerError(e, nil)
32
}
33
34
buf := new(bytes.Buffer)
35
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
-
}
41
42
-
// TODO: we can just stream this, don't need to make a buffer
43
-
for _, p := range parts {
44
-
buf.Write(p.Data)
45
}
46
47
e.Response().Header().Set(echo.HeaderContentDisposition, "attachment; filename="+c.String())
···
2
3
import (
4
"bytes"
5
+
"fmt"
6
+
"io"
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"
13
"github.com/haileyok/cocoon/internal/helpers"
14
"github.com/haileyok/cocoon/models"
15
"github.com/ipfs/go-cid"
···
17
)
18
19
func (s *Server) handleSyncGetBlob(e echo.Context) error {
20
+
ctx := e.Request().Context()
21
+
logger := s.logger.With("name", "handleSyncGetBlob")
22
+
23
did := e.QueryParam("did")
24
if did == "" {
25
return helpers.InputError(e, nil)
···
35
return helpers.InputError(e, nil)
36
}
37
38
+
urepo, err := s.getRepoActorByDid(ctx, did)
39
+
if err != nil {
40
+
logger.Error("could not find user for requested blob", "error", err)
41
+
return helpers.InputError(e, nil)
42
+
}
43
+
44
+
status := urepo.Status()
45
+
if status != nil {
46
+
if *status == "deactivated" {
47
+
return helpers.InputError(e, to.StringPtr("RepoDeactivated"))
48
+
}
49
+
}
50
+
51
var blob models.Blob
52
+
if err := s.db.Raw(ctx, "SELECT * FROM blobs WHERE did = ? AND cid = ?", nil, did, c.Bytes()).Scan(&blob).Error; err != nil {
53
+
logger.Error("error looking up blob", "error", err)
54
return helpers.ServerError(e, nil)
55
}
56
57
buf := new(bytes.Buffer)
58
59
+
if blob.Storage == "sqlite" {
60
+
var parts []models.BlobPart
61
+
if err := s.db.Raw(ctx, "SELECT * FROM blob_parts WHERE blob_id = ? ORDER BY idx", nil, blob.ID).Scan(&parts).Error; err != nil {
62
+
logger.Error("error getting blob parts", "error", err)
63
+
return helpers.ServerError(e, nil)
64
+
}
65
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)
131
}
132
133
e.Response().Header().Set(echo.HeaderContentDisposition, "attachment; filename="+c.String())
+16
-13
server/handle_sync_get_blocks.go
+16
-13
server/handle_sync_get_blocks.go
···
2
3
import (
4
"bytes"
5
-
"context"
6
-
"strings"
7
8
"github.com/bluesky-social/indigo/carstore"
9
-
"github.com/haileyok/cocoon/blockstore"
10
"github.com/haileyok/cocoon/internal/helpers"
11
"github.com/ipfs/go-cid"
12
cbor "github.com/ipfs/go-ipld-cbor"
13
"github.com/ipld/go-car"
14
"github.com/labstack/echo/v4"
15
)
16
17
func (s *Server) handleGetBlocks(e echo.Context) error {
18
-
did := e.QueryParam("did")
19
-
cidsstr := e.QueryParam("cids")
20
-
if did == "" {
21
return helpers.InputError(e, nil)
22
}
23
24
-
cidstrs := strings.Split(cidsstr, ",")
25
-
cids := []cid.Cid{}
26
27
-
for _, cs := range cidstrs {
28
c, err := cid.Cast([]byte(cs))
29
if err != nil {
30
return err
···
33
cids = append(cids, c)
34
}
35
36
-
urepo, err := s.getRepoActorByDid(did)
37
if err != nil {
38
return helpers.ServerError(e, nil)
39
}
···
50
})
51
52
if _, err := carstore.LdWrite(buf, hb); err != nil {
53
-
s.logger.Error("error writing to car", "error", err)
54
return helpers.ServerError(e, nil)
55
}
56
57
-
bs := blockstore.New(urepo.Repo.Did, s.db)
58
59
for _, c := range cids {
60
-
b, err := bs.Get(context.TODO(), c)
61
if err != nil {
62
return err
63
}
···
2
3
import (
4
"bytes"
5
6
"github.com/bluesky-social/indigo/carstore"
7
"github.com/haileyok/cocoon/internal/helpers"
8
"github.com/ipfs/go-cid"
9
cbor "github.com/ipfs/go-ipld-cbor"
10
"github.com/ipld/go-car"
11
"github.com/labstack/echo/v4"
12
)
13
+
14
+
type ComAtprotoSyncGetBlocksRequest struct {
15
+
Did string `query:"did"`
16
+
Cids []string `query:"cids"`
17
+
}
18
19
func (s *Server) handleGetBlocks(e echo.Context) error {
20
+
ctx := e.Request().Context()
21
+
logger := s.logger.With("name", "handleSyncGetBlocks")
22
+
23
+
var req ComAtprotoSyncGetBlocksRequest
24
+
if err := e.Bind(&req); err != nil {
25
return helpers.InputError(e, nil)
26
}
27
28
+
var cids []cid.Cid
29
30
+
for _, cs := range req.Cids {
31
c, err := cid.Cast([]byte(cs))
32
if err != nil {
33
return err
···
36
cids = append(cids, c)
37
}
38
39
+
urepo, err := s.getRepoActorByDid(ctx, req.Did)
40
if err != nil {
41
return helpers.ServerError(e, nil)
42
}
···
53
})
54
55
if _, err := carstore.LdWrite(buf, hb); err != nil {
56
+
logger.Error("error writing to car", "error", err)
57
return helpers.ServerError(e, nil)
58
}
59
60
+
bs := s.getBlockstore(urepo.Repo.Did)
61
62
for _, c := range cids {
63
+
b, err := bs.Get(ctx, c)
64
if err != nil {
65
return err
66
}
+3
-1
server/handle_sync_get_latest_commit.go
+3
-1
server/handle_sync_get_latest_commit.go
···
12
}
13
14
func (s *Server) handleSyncGetLatestCommit(e echo.Context) error {
15
+
ctx := e.Request().Context()
16
+
17
did := e.QueryParam("did")
18
if did == "" {
19
return helpers.InputError(e, nil)
20
}
21
22
+
urepo, err := s.getRepoActorByDid(ctx, did)
23
if err != nil {
24
return err
25
}
+8
-5
server/handle_sync_get_record.go
+8
-5
server/handle_sync_get_record.go
···
13
)
14
15
func (s *Server) handleSyncGetRecord(e echo.Context) error {
16
did := e.QueryParam("did")
17
collection := e.QueryParam("collection")
18
rkey := e.QueryParam("rkey")
19
20
var urepo models.Repo
21
-
if err := s.db.Raw("SELECT * FROM repos WHERE did = ?", nil, did).Scan(&urepo).Error; err != nil {
22
-
s.logger.Error("error getting repo", "error", err)
23
return helpers.ServerError(e, nil)
24
}
25
26
-
root, blocks, err := s.repoman.getRecordProof(urepo, collection, rkey)
27
if err != nil {
28
return err
29
}
···
36
})
37
38
if _, err := carstore.LdWrite(buf, hb); err != nil {
39
-
s.logger.Error("error writing to car", "error", err)
40
return helpers.ServerError(e, nil)
41
}
42
43
for _, blk := range blocks {
44
if _, err := carstore.LdWrite(buf, blk.Cid().Bytes(), blk.RawData()); err != nil {
45
-
s.logger.Error("error writing to car", "error", err)
46
return helpers.ServerError(e, nil)
47
}
48
}
···
13
)
14
15
func (s *Server) handleSyncGetRecord(e echo.Context) error {
16
+
ctx := e.Request().Context()
17
+
logger := s.logger.With("name", "handleSyncGetRecord")
18
+
19
did := e.QueryParam("did")
20
collection := e.QueryParam("collection")
21
rkey := e.QueryParam("rkey")
22
23
var urepo models.Repo
24
+
if err := s.db.Raw(ctx, "SELECT * FROM repos WHERE did = ?", nil, did).Scan(&urepo).Error; err != nil {
25
+
logger.Error("error getting repo", "error", err)
26
return helpers.ServerError(e, nil)
27
}
28
29
+
root, blocks, err := s.repoman.getRecordProof(ctx, urepo, collection, rkey)
30
if err != nil {
31
return err
32
}
···
39
})
40
41
if _, err := carstore.LdWrite(buf, hb); err != nil {
42
+
logger.Error("error writing to car", "error", err)
43
return helpers.ServerError(e, nil)
44
}
45
46
for _, blk := range blocks {
47
if _, err := carstore.LdWrite(buf, blk.Cid().Bytes(), blk.RawData()); err != nil {
48
+
logger.Error("error writing to car", "error", err)
49
return helpers.ServerError(e, nil)
50
}
51
}
+6
-3
server/handle_sync_get_repo.go
+6
-3
server/handle_sync_get_repo.go
···
13
)
14
15
func (s *Server) handleSyncGetRepo(e echo.Context) error {
16
did := e.QueryParam("did")
17
if did == "" {
18
return helpers.InputError(e, nil)
19
}
20
21
-
urepo, err := s.getRepoActorByDid(did)
22
if err != nil {
23
return err
24
}
···
36
buf := new(bytes.Buffer)
37
38
if _, err := carstore.LdWrite(buf, hb); err != nil {
39
-
s.logger.Error("error writing to car", "error", err)
40
return helpers.ServerError(e, nil)
41
}
42
43
var blocks []models.Block
44
-
if err := s.db.Raw("SELECT * FROM blocks WHERE did = ? ORDER BY rev ASC", nil, urepo.Repo.Did).Scan(&blocks).Error; err != nil {
45
return err
46
}
47
···
13
)
14
15
func (s *Server) handleSyncGetRepo(e echo.Context) error {
16
+
ctx := e.Request().Context()
17
+
logger := s.logger.With("name", "handleSyncGetRepo")
18
+
19
did := e.QueryParam("did")
20
if did == "" {
21
return helpers.InputError(e, nil)
22
}
23
24
+
urepo, err := s.getRepoActorByDid(ctx, did)
25
if err != nil {
26
return err
27
}
···
39
buf := new(bytes.Buffer)
40
41
if _, err := carstore.LdWrite(buf, hb); err != nil {
42
+
logger.Error("error writing to car", "error", err)
43
return helpers.ServerError(e, nil)
44
}
45
46
var blocks []models.Block
47
+
if err := s.db.Raw(ctx, "SELECT * FROM blocks WHERE did = ? ORDER BY rev ASC", nil, urepo.Repo.Did).Scan(&blocks).Error; err != nil {
48
return err
49
}
50
+5
-3
server/handle_sync_get_repo_status.go
+5
-3
server/handle_sync_get_repo_status.go
···
14
15
// TODO: make this actually do the right thing
16
func (s *Server) handleSyncGetRepoStatus(e echo.Context) error {
17
did := e.QueryParam("did")
18
if did == "" {
19
return helpers.InputError(e, nil)
20
}
21
22
-
urepo, err := s.getRepoActorByDid(did)
23
if err != nil {
24
return err
25
}
26
27
return e.JSON(200, ComAtprotoSyncGetRepoStatusResponse{
28
Did: urepo.Repo.Did,
29
-
Active: true,
30
-
Status: nil,
31
Rev: &urepo.Rev,
32
})
33
}
···
14
15
// TODO: make this actually do the right thing
16
func (s *Server) handleSyncGetRepoStatus(e echo.Context) error {
17
+
ctx := e.Request().Context()
18
+
19
did := e.QueryParam("did")
20
if did == "" {
21
return helpers.InputError(e, nil)
22
}
23
24
+
urepo, err := s.getRepoActorByDid(ctx, did)
25
if err != nil {
26
return err
27
}
28
29
return e.JSON(200, ComAtprotoSyncGetRepoStatusResponse{
30
Did: urepo.Repo.Did,
31
+
Active: urepo.Active(),
32
+
Status: urepo.Status(),
33
Rev: &urepo.Rev,
34
})
35
}
+20
-3
server/handle_sync_list_blobs.go
+20
-3
server/handle_sync_list_blobs.go
···
1
package server
2
3
import (
4
"github.com/haileyok/cocoon/internal/helpers"
5
"github.com/haileyok/cocoon/models"
6
"github.com/ipfs/go-cid"
···
13
}
14
15
func (s *Server) handleSyncListBlobs(e echo.Context) error {
16
did := e.QueryParam("did")
17
if did == "" {
18
return helpers.InputError(e, nil)
···
34
}
35
params = append(params, limit)
36
37
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)
40
return helpers.ServerError(e, nil)
41
}
42
···
44
for _, b := range blobs {
45
c, err := cid.Cast(b.Cid)
46
if err != nil {
47
-
s.logger.Error("error casting cid", "error", err)
48
return helpers.ServerError(e, nil)
49
}
50
cstrs = append(cstrs, c.String())
···
1
package server
2
3
import (
4
+
"github.com/Azure/go-autorest/autorest/to"
5
"github.com/haileyok/cocoon/internal/helpers"
6
"github.com/haileyok/cocoon/models"
7
"github.com/ipfs/go-cid"
···
14
}
15
16
func (s *Server) handleSyncListBlobs(e echo.Context) error {
17
+
ctx := e.Request().Context()
18
+
logger := s.logger.With("name", "handleSyncListBlobs")
19
+
20
did := e.QueryParam("did")
21
if did == "" {
22
return helpers.InputError(e, nil)
···
38
}
39
params = append(params, limit)
40
41
+
urepo, err := s.getRepoActorByDid(ctx, did)
42
+
if err != nil {
43
+
logger.Error("could not find user for requested blobs", "error", err)
44
+
return helpers.InputError(e, nil)
45
+
}
46
+
47
+
status := urepo.Status()
48
+
if status != nil {
49
+
if *status == "deactivated" {
50
+
return helpers.InputError(e, to.StringPtr("RepoDeactivated"))
51
+
}
52
+
}
53
+
54
var blobs []models.Blob
55
+
if err := s.db.Raw(ctx, "SELECT * FROM blobs WHERE did = ? "+cursorquery+" ORDER BY created_at DESC LIMIT ?", nil, params...).Scan(&blobs).Error; err != nil {
56
+
logger.Error("error getting records", "error", err)
57
return helpers.ServerError(e, nil)
58
}
59
···
61
for _, b := range blobs {
62
c, err := cid.Cast(b.Cid)
63
if err != nil {
64
+
logger.Error("error casting cid", "error", err)
65
return helpers.ServerError(e, nil)
66
}
67
cstrs = append(cstrs, c.String())
+70
-56
server/handle_sync_subscribe_repos.go
+70
-56
server/handle_sync_subscribe_repos.go
···
1
package server
2
3
import (
4
-
"fmt"
5
-
"net/http"
6
7
"github.com/bluesky-social/indigo/events"
8
"github.com/bluesky-social/indigo/lex/util"
9
"github.com/btcsuite/websocket"
10
"github.com/labstack/echo/v4"
11
)
12
13
-
var upgrader = websocket.Upgrader{
14
-
ReadBufferSize: 1024,
15
-
WriteBufferSize: 1024,
16
-
CheckOrigin: func(r *http.Request) bool {
17
-
return true
18
-
},
19
-
}
20
-
21
func (s *Server) handleSyncSubscribeRepos(e echo.Context) error {
22
conn, err := websocket.Upgrade(e.Response().Writer, e.Request(), e.Response().Header(), 1<<10, 1<<10)
23
if err != nil {
24
return err
25
}
26
27
-
s.logger.Info("new connection", "ua", e.Request().UserAgent())
28
-
29
-
ctx := e.Request().Context()
30
-
31
ident := e.RealIP() + "-" + e.Request().UserAgent()
32
33
evts, cancel, err := s.evtman.Subscribe(ctx, ident, func(evt *events.XRPCStreamEvent) bool {
34
return true
···
40
41
header := events.EventHeader{Op: events.EvtKindMessage}
42
for evt := range evts {
43
-
wc, err := conn.NextWriter(websocket.BinaryMessage)
44
-
if err != nil {
45
-
return err
46
-
}
47
48
-
var obj util.CBOR
49
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
-
}
78
79
-
if err := header.MarshalCBOR(wc); err != nil {
80
-
return fmt.Errorf("failed to write header: %w", err)
81
-
}
82
83
-
if err := obj.MarshalCBOR(wc); err != nil {
84
-
return fmt.Errorf("failed to write event: %w", err)
85
-
}
86
87
-
if err := wc.Close(); err != nil {
88
-
return fmt.Errorf("failed to flush-close our event write: %w", err)
89
-
}
90
}
91
92
return nil
···
1
package server
2
3
import (
4
+
"context"
5
+
"time"
6
7
"github.com/bluesky-social/indigo/events"
8
"github.com/bluesky-social/indigo/lex/util"
9
"github.com/btcsuite/websocket"
10
+
"github.com/haileyok/cocoon/metrics"
11
"github.com/labstack/echo/v4"
12
)
13
14
func (s *Server) handleSyncSubscribeRepos(e echo.Context) error {
15
+
ctx := e.Request().Context()
16
+
logger := s.logger.With("component", "subscribe-repos-websocket")
17
+
18
conn, err := websocket.Upgrade(e.Response().Writer, e.Request(), e.Response().Header(), 1<<10, 1<<10)
19
if err != nil {
20
+
logger.Error("unable to establish websocket with relay", "err", err)
21
return err
22
}
23
24
ident := e.RealIP() + "-" + e.Request().UserAgent()
25
+
logger = logger.With("ident", ident)
26
+
logger.Info("new connection established")
27
+
28
+
metrics.RelaysConnected.WithLabelValues(ident).Inc()
29
+
defer func() {
30
+
metrics.RelaysConnected.WithLabelValues(ident).Dec()
31
+
}()
32
33
evts, cancel, err := s.evtman.Subscribe(ctx, ident, func(evt *events.XRPCStreamEvent) bool {
34
return true
···
40
41
header := events.EventHeader{Op: events.EvtKindMessage}
42
for evt := range evts {
43
+
func() {
44
+
defer func() {
45
+
metrics.RelaySends.WithLabelValues(ident, header.MsgType).Inc()
46
+
}()
47
48
+
wc, err := conn.NextWriter(websocket.BinaryMessage)
49
+
if err != nil {
50
+
logger.Error("error writing message to relay", "err", err)
51
+
return
52
+
}
53
54
+
if ctx.Err() != nil {
55
+
logger.Error("context error", "err", err)
56
+
return
57
+
}
58
+
59
+
var obj util.CBOR
60
+
switch {
61
+
case evt.Error != nil:
62
+
header.Op = events.EvtKindErrorFrame
63
+
obj = evt.Error
64
+
case evt.RepoCommit != nil:
65
+
header.MsgType = "#commit"
66
+
obj = evt.RepoCommit
67
+
case evt.RepoIdentity != nil:
68
+
header.MsgType = "#identity"
69
+
obj = evt.RepoIdentity
70
+
case evt.RepoAccount != nil:
71
+
header.MsgType = "#account"
72
+
obj = evt.RepoAccount
73
+
case evt.RepoInfo != nil:
74
+
header.MsgType = "#info"
75
+
obj = evt.RepoInfo
76
+
default:
77
+
logger.Warn("unrecognized event kind")
78
+
return
79
+
}
80
+
81
+
if err := header.MarshalCBOR(wc); err != nil {
82
+
logger.Error("failed to write header to relay", "err", err)
83
+
return
84
+
}
85
86
+
if err := obj.MarshalCBOR(wc); err != nil {
87
+
logger.Error("failed to write event to relay", "err", err)
88
+
return
89
+
}
90
91
+
if err := wc.Close(); err != nil {
92
+
logger.Error("failed to flush-close our event write", "err", err)
93
+
return
94
+
}
95
+
}()
96
+
}
97
98
+
// we should tell the relay to request a new crawl at this point if we got disconnected
99
+
// use a new context since the old one might be cancelled at this point
100
+
ctx, cancel = context.WithTimeout(context.Background(), 10*time.Second)
101
+
defer cancel()
102
+
if err := s.requestCrawl(ctx); err != nil {
103
+
logger.Error("error requesting crawls", "err", err)
104
}
105
106
return nil
+36
server/handle_well_known.go
+36
server/handle_well_known.go
···
2
3
import (
4
"fmt"
5
+
"strings"
6
7
"github.com/Azure/go-autorest/autorest/to"
8
+
"github.com/haileyok/cocoon/internal/helpers"
9
"github.com/labstack/echo/v4"
10
+
"gorm.io/gorm"
11
)
12
13
var (
···
64
},
65
},
66
})
67
+
}
68
+
69
+
func (s *Server) handleAtprotoDid(e echo.Context) error {
70
+
ctx := e.Request().Context()
71
+
logger := s.logger.With("name", "handleAtprotoDid")
72
+
73
+
host := e.Request().Host
74
+
if host == "" {
75
+
return helpers.InputError(e, to.StringPtr("Invalid handle."))
76
+
}
77
+
78
+
host = strings.Split(host, ":")[0]
79
+
host = strings.ToLower(strings.TrimSpace(host))
80
+
81
+
if host == s.config.Hostname {
82
+
return e.String(200, s.config.Did)
83
+
}
84
+
85
+
suffix := "." + s.config.Hostname
86
+
if !strings.HasSuffix(host, suffix) {
87
+
return e.NoContent(404)
88
+
}
89
+
90
+
actor, err := s.getActorByHandle(ctx, host)
91
+
if err != nil {
92
+
if err == gorm.ErrRecordNotFound {
93
+
return e.NoContent(404)
94
+
}
95
+
logger.Error("error looking up actor by handle", "error", err)
96
+
return helpers.ServerError(e, nil)
97
+
}
98
+
99
+
return e.String(200, actor.Did)
100
}
101
102
func (s *Server) handleOauthProtectedResource(e echo.Context) error {
+38
server/mail.go
+38
server/mail.go
···
40
return nil
41
}
42
43
+
func (s *Server) sendPlcTokenReset(email, handle, code string) error {
44
+
if s.mail == nil {
45
+
return nil
46
+
}
47
+
48
+
s.mailLk.Lock()
49
+
defer s.mailLk.Unlock()
50
+
51
+
s.mail.To(email)
52
+
s.mail.Subject("PLC token for " + s.config.Hostname)
53
+
s.mail.Plain().Set(fmt.Sprintf("Hello %s. Your PLC operation code is %s. This code will expire in ten minutes.", handle, code))
54
+
55
+
if err := s.mail.Send(); err != nil {
56
+
return err
57
+
}
58
+
59
+
return nil
60
+
}
61
+
62
func (s *Server) sendEmailUpdate(email, handle, code string) error {
63
if s.mail == nil {
64
return nil
···
96
97
return nil
98
}
99
+
100
+
func (s *Server) sendTwoFactorCode(email, handle, code string) error {
101
+
if s.mail == nil {
102
+
return nil
103
+
}
104
+
105
+
s.mailLk.Lock()
106
+
defer s.mailLk.Unlock()
107
+
108
+
s.mail.To(email)
109
+
s.mail.Subject("2FA code for " + s.config.Hostname)
110
+
s.mail.Plain().Set(fmt.Sprintf("Hello %s. Your 2FA code is %s. This code will expire in ten minutes.", handle, code))
111
+
112
+
if err := s.mail.Send(); err != nil {
113
+
return err
114
+
}
115
+
116
+
return nil
117
+
}
+58
-23
server/middleware.go
+58
-23
server/middleware.go
···
3
import (
4
"crypto/sha256"
5
"encoding/base64"
6
"fmt"
7
"strings"
8
"time"
···
11
"github.com/golang-jwt/jwt/v4"
12
"github.com/haileyok/cocoon/internal/helpers"
13
"github.com/haileyok/cocoon/models"
14
"github.com/haileyok/cocoon/oauth/provider"
15
"github.com/labstack/echo/v4"
16
"gitlab.com/yawning/secp256k1-voi"
···
35
36
func (s *Server) handleLegacySessionMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
37
return func(e echo.Context) error {
38
authheader := e.Request().Header.Get("authorization")
39
if authheader == "" {
40
return e.JSON(401, map[string]string{"error": "Unauthorized"})
···
65
if hasLxm {
66
pts := strings.Split(e.Request().URL.String(), "/")
67
if lxm != pts[len(pts)-1] {
68
-
s.logger.Error("service auth lxm incorrect", "lxm", lxm, "expected", pts[len(pts)-1], "error", err)
69
return helpers.InputError(e, nil)
70
}
71
72
maybeDid, ok := claims["iss"].(string)
73
if !ok {
74
-
s.logger.Error("no iss in service auth token", "error", err)
75
return helpers.InputError(e, nil)
76
}
77
did = maybeDid
78
79
-
maybeRepo, err := s.getRepoActorByDid(did)
80
if err != nil {
81
-
s.logger.Error("error fetching repo", "error", err)
82
return helpers.ServerError(e, nil)
83
}
84
repo = maybeRepo
···
92
return s.privateKey.Public(), nil
93
})
94
if err != nil {
95
-
s.logger.Error("error parsing jwt", "error", err)
96
return helpers.ExpiredTokenError(e)
97
}
98
···
105
hash := sha256.Sum256([]byte(signingInput))
106
sigBytes, err := base64.RawURLEncoding.DecodeString(kpts[2])
107
if err != nil {
108
-
s.logger.Error("error decoding signature bytes", "error", err)
109
return helpers.ServerError(e, nil)
110
}
111
112
if len(sigBytes) != 64 {
113
-
s.logger.Error("incorrect sigbytes length", "length", len(sigBytes))
114
return helpers.ServerError(e, nil)
115
}
116
···
119
rr, _ := secp256k1.NewScalarFromBytes((*[32]byte)(rBytes))
120
ss, _ := secp256k1.NewScalarFromBytes((*[32]byte)(sBytes))
121
122
sk, err := secp256k1secec.NewPrivateKey(repo.SigningKey)
123
if err != nil {
124
-
s.logger.Error("can't load private key", "error", err)
125
return err
126
}
127
128
pubKey, ok := sk.Public().(*secp256k1secec.PublicKey)
129
if !ok {
130
-
s.logger.Error("error getting public key from sk")
131
return helpers.ServerError(e, nil)
132
}
133
134
verified := pubKey.VerifyRaw(hash[:], rr, ss)
135
if !verified {
136
-
s.logger.Error("error verifying", "error", err)
137
return helpers.ServerError(e, nil)
138
}
139
}
···
157
Found bool
158
}
159
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 {
161
if err == gorm.ErrRecordNotFound {
162
return helpers.InvalidTokenError(e)
163
}
164
165
-
s.logger.Error("error getting token from db", "error", err)
166
return helpers.ServerError(e, nil)
167
}
168
···
173
174
exp, ok := claims["exp"].(float64)
175
if !ok {
176
-
s.logger.Error("error getting iat from token")
177
return helpers.ServerError(e, nil)
178
}
179
···
182
}
183
184
if repo == nil {
185
-
maybeRepo, err := s.getRepoActorByDid(claims["sub"].(string))
186
if err != nil {
187
-
s.logger.Error("error fetching repo", "error", err)
188
return helpers.ServerError(e, nil)
189
}
190
repo = maybeRepo
···
205
206
func (s *Server) handleOauthSessionMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
207
return func(e echo.Context) error {
208
authheader := e.Request().Header.Get("authorization")
209
if authheader == "" {
210
return e.JSON(401, map[string]string{"error": "Unauthorized"})
···
229
230
proof, err := s.oauthProvider.DpopManager.CheckProof(e.Request().Method, "https://"+s.config.Hostname+e.Request().URL.String(), e.Request().Header, to.StringPtr(accessToken))
231
if err != nil {
232
-
s.logger.Error("invalid dpop proof", "error", err)
233
-
return helpers.InputError(e, to.StringPtr(err.Error()))
234
}
235
236
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)
239
return helpers.InputError(e, nil)
240
}
241
···
244
}
245
246
if *oauthToken.Parameters.DpopJkt != proof.JKT {
247
-
s.logger.Error("jkt mismatch", "token", oauthToken.Parameters.DpopJkt, "proof", proof.JKT)
248
return helpers.InputError(e, to.StringPtr("dpop jkt mismatch"))
249
}
250
251
if time.Now().After(oauthToken.ExpiresAt) {
252
-
return helpers.ExpiredTokenError(e)
253
}
254
255
-
repo, err := s.getRepoActorByDid(oauthToken.Sub)
256
if err != nil {
257
-
s.logger.Error("could not find actor in db", "error", err)
258
return helpers.ServerError(e, nil)
259
}
260
···
3
import (
4
"crypto/sha256"
5
"encoding/base64"
6
+
"errors"
7
"fmt"
8
"strings"
9
"time"
···
12
"github.com/golang-jwt/jwt/v4"
13
"github.com/haileyok/cocoon/internal/helpers"
14
"github.com/haileyok/cocoon/models"
15
+
"github.com/haileyok/cocoon/oauth/dpop"
16
"github.com/haileyok/cocoon/oauth/provider"
17
"github.com/labstack/echo/v4"
18
"gitlab.com/yawning/secp256k1-voi"
···
37
38
func (s *Server) handleLegacySessionMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
39
return func(e echo.Context) error {
40
+
ctx := e.Request().Context()
41
+
logger := s.logger.With("name", "handleLegacySessionMiddleware")
42
+
43
authheader := e.Request().Header.Get("authorization")
44
if authheader == "" {
45
return e.JSON(401, map[string]string{"error": "Unauthorized"})
···
70
if hasLxm {
71
pts := strings.Split(e.Request().URL.String(), "/")
72
if lxm != pts[len(pts)-1] {
73
+
logger.Error("service auth lxm incorrect", "lxm", lxm, "expected", pts[len(pts)-1], "error", err)
74
return helpers.InputError(e, nil)
75
}
76
77
maybeDid, ok := claims["iss"].(string)
78
if !ok {
79
+
logger.Error("no iss in service auth token", "error", err)
80
return helpers.InputError(e, nil)
81
}
82
did = maybeDid
83
84
+
maybeRepo, err := s.getRepoActorByDid(ctx, did)
85
if err != nil {
86
+
logger.Error("error fetching repo", "error", err)
87
return helpers.ServerError(e, nil)
88
}
89
repo = maybeRepo
···
97
return s.privateKey.Public(), nil
98
})
99
if err != nil {
100
+
logger.Error("error parsing jwt", "error", err)
101
return helpers.ExpiredTokenError(e)
102
}
103
···
110
hash := sha256.Sum256([]byte(signingInput))
111
sigBytes, err := base64.RawURLEncoding.DecodeString(kpts[2])
112
if err != nil {
113
+
logger.Error("error decoding signature bytes", "error", err)
114
return helpers.ServerError(e, nil)
115
}
116
117
if len(sigBytes) != 64 {
118
+
logger.Error("incorrect sigbytes length", "length", len(sigBytes))
119
return helpers.ServerError(e, nil)
120
}
121
···
124
rr, _ := secp256k1.NewScalarFromBytes((*[32]byte)(rBytes))
125
ss, _ := secp256k1.NewScalarFromBytes((*[32]byte)(sBytes))
126
127
+
if repo == nil {
128
+
sub, ok := claims["sub"].(string)
129
+
if !ok {
130
+
s.logger.Error("no sub claim in ES256K token and repo not set")
131
+
return helpers.InvalidTokenError(e)
132
+
}
133
+
maybeRepo, err := s.getRepoActorByDid(ctx, sub)
134
+
if err != nil {
135
+
s.logger.Error("error fetching repo for ES256K verification", "error", err)
136
+
return helpers.ServerError(e, nil)
137
+
}
138
+
repo = maybeRepo
139
+
did = sub
140
+
}
141
+
142
sk, err := secp256k1secec.NewPrivateKey(repo.SigningKey)
143
if err != nil {
144
+
logger.Error("can't load private key", "error", err)
145
return err
146
}
147
148
pubKey, ok := sk.Public().(*secp256k1secec.PublicKey)
149
if !ok {
150
+
logger.Error("error getting public key from sk")
151
return helpers.ServerError(e, nil)
152
}
153
154
verified := pubKey.VerifyRaw(hash[:], rr, ss)
155
if !verified {
156
+
logger.Error("error verifying", "error", err)
157
return helpers.ServerError(e, nil)
158
}
159
}
···
177
Found bool
178
}
179
var result Result
180
+
if err := s.db.Raw(ctx, "SELECT EXISTS(SELECT 1 FROM "+table+" WHERE token = ?) AS found", nil, tokenstr).Scan(&result).Error; err != nil {
181
if err == gorm.ErrRecordNotFound {
182
return helpers.InvalidTokenError(e)
183
}
184
185
+
logger.Error("error getting token from db", "error", err)
186
return helpers.ServerError(e, nil)
187
}
188
···
193
194
exp, ok := claims["exp"].(float64)
195
if !ok {
196
+
logger.Error("error getting iat from token")
197
return helpers.ServerError(e, nil)
198
}
199
···
202
}
203
204
if repo == nil {
205
+
maybeRepo, err := s.getRepoActorByDid(ctx, claims["sub"].(string))
206
if err != nil {
207
+
logger.Error("error fetching repo", "error", err)
208
return helpers.ServerError(e, nil)
209
}
210
repo = maybeRepo
···
225
226
func (s *Server) handleOauthSessionMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
227
return func(e echo.Context) error {
228
+
ctx := e.Request().Context()
229
+
logger := s.logger.With("name", "handleOauthSessionMiddleware")
230
+
231
authheader := e.Request().Header.Get("authorization")
232
if authheader == "" {
233
return e.JSON(401, map[string]string{"error": "Unauthorized"})
···
252
253
proof, err := s.oauthProvider.DpopManager.CheckProof(e.Request().Method, "https://"+s.config.Hostname+e.Request().URL.String(), e.Request().Header, to.StringPtr(accessToken))
254
if err != nil {
255
+
if errors.Is(err, dpop.ErrUseDpopNonce) {
256
+
e.Response().Header().Set("WWW-Authenticate", `DPoP error="use_dpop_nonce"`)
257
+
e.Response().Header().Add("access-control-expose-headers", "WWW-Authenticate")
258
+
return e.JSON(401, map[string]string{
259
+
"error": "use_dpop_nonce",
260
+
})
261
+
}
262
+
logger.Error("invalid dpop proof", "error", err)
263
+
return helpers.InputError(e, nil)
264
}
265
266
var oauthToken provider.OauthToken
267
+
if err := s.db.Raw(ctx, "SELECT * FROM oauth_tokens WHERE token = ?", nil, accessToken).Scan(&oauthToken).Error; err != nil {
268
+
logger.Error("error finding access token in db", "error", err)
269
return helpers.InputError(e, nil)
270
}
271
···
274
}
275
276
if *oauthToken.Parameters.DpopJkt != proof.JKT {
277
+
logger.Error("jkt mismatch", "token", oauthToken.Parameters.DpopJkt, "proof", proof.JKT)
278
return helpers.InputError(e, to.StringPtr("dpop jkt mismatch"))
279
}
280
281
if time.Now().After(oauthToken.ExpiresAt) {
282
+
e.Response().Header().Set("WWW-Authenticate", `DPoP error="invalid_token", error_description="Token expired"`)
283
+
e.Response().Header().Add("access-control-expose-headers", "WWW-Authenticate")
284
+
return e.JSON(401, map[string]string{
285
+
"error": "invalid_token",
286
+
"error_description": "Token expired",
287
+
})
288
}
289
290
+
repo, err := s.getRepoActorByDid(ctx, oauthToken.Sub)
291
if err != nil {
292
+
logger.Error("could not find actor in db", "error", err)
293
return helpers.ServerError(e, nil)
294
}
295
+107
-48
server/repo.go
+107
-48
server/repo.go
···
10
11
"github.com/Azure/go-autorest/autorest/to"
12
"github.com/bluesky-social/indigo/api/atproto"
13
-
"github.com/bluesky-social/indigo/atproto/data"
14
"github.com/bluesky-social/indigo/atproto/syntax"
15
"github.com/bluesky-social/indigo/carstore"
16
"github.com/bluesky-social/indigo/events"
17
lexutil "github.com/bluesky-social/indigo/lex/util"
18
"github.com/bluesky-social/indigo/repo"
19
-
"github.com/bluesky-social/indigo/util"
20
-
"github.com/haileyok/cocoon/blockstore"
21
"github.com/haileyok/cocoon/internal/db"
22
"github.com/haileyok/cocoon/models"
23
blocks "github.com/ipfs/go-block-format"
24
"github.com/ipfs/go-cid"
25
cbor "github.com/ipfs/go-ipld-cbor"
···
73
}
74
75
func (mm *MarshalableMap) MarshalCBOR(w io.Writer) error {
76
-
data, err := data.MarshalCBOR(*mm)
77
if err != nil {
78
return err
79
}
···
97
}
98
99
// TODO make use of swap commit
100
-
func (rm *RepoMan) applyWrites(urepo models.Repo, writes []Op, swapCommit *string) ([]ApplyWriteResult, error) {
101
rootcid, err := cid.Cast(urepo.Root)
102
if err != nil {
103
return nil, err
104
}
105
106
-
dbs := blockstore.New(urepo.Did, rm.db)
107
-
r, err := repo.OpenRepo(context.TODO(), dbs, rootcid)
108
109
-
entries := []models.Record{}
110
var results []ApplyWriteResult
111
112
for i, op := range writes {
113
if op.Type != OpTypeCreate && op.Rkey == nil {
114
return nil, fmt.Errorf("invalid rkey")
115
} else if op.Type == OpTypeCreate && op.Rkey != nil {
116
-
_, _, err := r.GetRecord(context.TODO(), op.Collection+"/"+*op.Rkey)
117
if err == nil {
118
op.Type = OpTypeUpdate
119
}
120
} else if op.Rkey == nil {
121
op.Rkey = to.StringPtr(rm.clock.Next().String())
122
writes[i].Rkey = op.Rkey
123
}
124
125
_, err := syntax.ParseRecordKey(*op.Rkey)
126
if err != nil {
127
return nil, err
···
129
130
switch op.Type {
131
case OpTypeCreate:
132
-
j, err := json.Marshal(*op.Record)
133
if err != nil {
134
return nil, err
135
}
136
-
out, err := data.UnmarshalJSON(j)
137
if err != nil {
138
return nil, err
139
}
140
mm := MarshalableMap(out)
141
-
nc, err := r.PutRecord(context.TODO(), op.Collection+"/"+*op.Rkey, &mm)
142
if err != nil {
143
return nil, err
144
}
145
-
d, err := data.MarshalCBOR(mm)
146
if err != nil {
147
return nil, err
148
}
149
entries = append(entries, models.Record{
150
Did: urepo.Did,
151
CreatedAt: rm.clock.Next().String(),
···
154
Cid: nc.String(),
155
Value: d,
156
})
157
results = append(results, ApplyWriteResult{
158
Type: to.StringPtr(OpTypeCreate.String()),
159
Uri: to.StringPtr("at://" + urepo.Did + "/" + op.Collection + "/" + *op.Rkey),
···
161
ValidationStatus: to.StringPtr("valid"), // TODO: obviously this might not be true atm lol
162
})
163
case OpTypeDelete:
164
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 {
166
return nil, err
167
}
168
entries = append(entries, models.Record{
169
Did: urepo.Did,
170
Nsid: op.Collection,
171
Rkey: *op.Rkey,
172
Value: old.Value,
173
})
174
-
err := r.DeleteRecord(context.TODO(), op.Collection+"/"+*op.Rkey)
175
if err != nil {
176
return nil, err
177
}
178
results = append(results, ApplyWriteResult{
179
Type: to.StringPtr(OpTypeDelete.String()),
180
})
181
case OpTypeUpdate:
182
-
j, err := json.Marshal(*op.Record)
183
if err != nil {
184
return nil, err
185
}
186
-
out, err := data.UnmarshalJSON(j)
187
if err != nil {
188
return nil, err
189
}
190
mm := MarshalableMap(out)
191
-
nc, err := r.UpdateRecord(context.TODO(), op.Collection+"/"+*op.Rkey, &mm)
192
if err != nil {
193
return nil, err
194
}
195
-
d, err := data.MarshalCBOR(mm)
196
if err != nil {
197
return nil, err
198
}
199
entries = append(entries, models.Record{
200
Did: urepo.Did,
201
CreatedAt: rm.clock.Next().String(),
···
204
Cid: nc.String(),
205
Value: d,
206
})
207
results = append(results, ApplyWriteResult{
208
Type: to.StringPtr(OpTypeUpdate.String()),
209
Uri: to.StringPtr("at://" + urepo.Did + "/" + op.Collection + "/" + *op.Rkey),
···
213
}
214
}
215
216
-
newroot, rev, err := r.Commit(context.TODO(), urepo.SignFor)
217
if err != nil {
218
return nil, err
219
}
220
221
buf := new(bytes.Buffer)
222
223
hb, err := cbor.DumpObject(&car.CarHeader{
224
Roots: []cid.Cid{newroot},
225
Version: 1,
226
})
227
-
228
if _, err := carstore.LdWrite(buf, hb); err != nil {
229
return nil, err
230
}
231
232
-
diffops, err := r.DiffSince(context.TODO(), rootcid)
233
if err != nil {
234
return nil, err
235
}
236
237
ops := make([]*atproto.SyncSubscribeRepos_RepoOp, 0, len(diffops))
238
-
239
for _, op := range diffops {
240
var c cid.Cid
241
switch op.Op {
···
264
})
265
}
266
267
-
blk, err := dbs.Get(context.TODO(), c)
268
if err != nil {
269
return nil, err
270
}
271
272
if _, err := carstore.LdWrite(buf, blk.Cid().Bytes(), blk.RawData()); err != nil {
273
return nil, err
274
}
275
}
276
277
-
for _, op := range dbs.GetLog() {
278
if _, err := carstore.LdWrite(buf, op.Cid().Bytes(), op.RawData()); err != nil {
279
return nil, err
280
}
281
}
282
283
var blobs []lexutil.LexLink
284
for _, entry := range entries {
285
var cids []cid.Cid
286
if entry.Cid != "" {
287
-
if err := rm.s.db.Create(&entry, []clause.Expression{clause.OnConflict{
288
Columns: []clause.Column{{Name: "did"}, {Name: "nsid"}, {Name: "rkey"}},
289
UpdateAll: true,
290
}}).Error; err != nil {
291
return nil, err
292
}
293
294
-
cids, err = rm.incrementBlobRefs(urepo, entry.Value)
295
if err != nil {
296
return nil, err
297
}
298
} else {
299
-
if err := rm.s.db.Delete(&entry, nil).Error; err != nil {
300
return nil, err
301
}
302
-
cids, err = rm.decrementBlobRefs(urepo, entry.Value)
303
if err != nil {
304
return nil, err
305
}
306
}
307
308
for _, c := range cids {
309
blobs = append(blobs, lexutil.LexLink(c))
310
}
311
}
312
313
-
rm.s.evtman.AddEvent(context.TODO(), &events.XRPCStreamEvent{
314
RepoCommit: &atproto.SyncSubscribeRepos_Commit{
315
Repo: urepo.Did,
316
Blocks: buf.Bytes(),
···
324
},
325
})
326
327
-
if err := dbs.UpdateRepo(context.TODO(), newroot, rev); err != nil {
328
return nil, err
329
}
330
···
339
return results, nil
340
}
341
342
-
func (rm *RepoMan) getRecordProof(urepo models.Repo, collection, rkey string) (cid.Cid, []blocks.Block, error) {
343
c, err := cid.Cast(urepo.Root)
344
if err != nil {
345
return cid.Undef, nil, err
346
}
347
348
-
dbs := blockstore.New(urepo.Did, rm.db)
349
-
bs := util.NewLoggingBstore(dbs)
350
351
-
r, err := repo.OpenRepo(context.TODO(), bs, c)
352
if err != nil {
353
return cid.Undef, nil, err
354
}
355
356
-
_, _, err = r.GetRecordBytes(context.TODO(), collection+"/"+rkey)
357
if err != nil {
358
return cid.Undef, nil, err
359
}
360
361
-
return c, bs.GetLoggedBlocks(), nil
362
}
363
364
-
func (rm *RepoMan) incrementBlobRefs(urepo models.Repo, cbor []byte) ([]cid.Cid, error) {
365
cids, err := getBlobCidsFromCbor(cbor)
366
if err != nil {
367
return nil, err
368
}
369
370
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 {
372
return nil, err
373
}
374
}
···
376
return cids, nil
377
}
378
379
-
func (rm *RepoMan) decrementBlobRefs(urepo models.Repo, cbor []byte) ([]cid.Cid, error) {
380
cids, err := getBlobCidsFromCbor(cbor)
381
if err != nil {
382
return nil, err
···
387
ID uint
388
Count int
389
}
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 {
391
return nil, err
392
}
393
394
if res.Count == 0 {
395
-
if err := rm.db.Exec("DELETE FROM blobs WHERE id = ?", nil, res.ID).Error; err != nil {
396
return nil, err
397
}
398
-
if err := rm.db.Exec("DELETE FROM blob_parts WHERE blob_id = ?", nil, res.ID).Error; err != nil {
399
return nil, err
400
}
401
}
···
409
func getBlobCidsFromCbor(cbor []byte) ([]cid.Cid, error) {
410
var cids []cid.Cid
411
412
-
decoded, err := data.UnmarshalCBOR(cbor)
413
if err != nil {
414
return nil, fmt.Errorf("error unmarshaling cbor: %w", err)
415
}
416
417
-
var deepiter func(interface{}) error
418
-
deepiter = func(item interface{}) error {
419
switch val := item.(type) {
420
-
case map[string]interface{}:
421
if val["$type"] == "blob" {
422
if ref, ok := val["ref"].(string); ok {
423
c, err := cid.Parse(ref)
···
430
return deepiter(v)
431
}
432
}
433
-
case []interface{}:
434
for _, v := range val {
435
deepiter(v)
436
}
···
10
11
"github.com/Azure/go-autorest/autorest/to"
12
"github.com/bluesky-social/indigo/api/atproto"
13
+
"github.com/bluesky-social/indigo/atproto/atdata"
14
"github.com/bluesky-social/indigo/atproto/syntax"
15
"github.com/bluesky-social/indigo/carstore"
16
"github.com/bluesky-social/indigo/events"
17
lexutil "github.com/bluesky-social/indigo/lex/util"
18
"github.com/bluesky-social/indigo/repo"
19
"github.com/haileyok/cocoon/internal/db"
20
+
"github.com/haileyok/cocoon/metrics"
21
"github.com/haileyok/cocoon/models"
22
+
"github.com/haileyok/cocoon/recording_blockstore"
23
blocks "github.com/ipfs/go-block-format"
24
"github.com/ipfs/go-cid"
25
cbor "github.com/ipfs/go-ipld-cbor"
···
73
}
74
75
func (mm *MarshalableMap) MarshalCBOR(w io.Writer) error {
76
+
data, err := atdata.MarshalCBOR(*mm)
77
if err != nil {
78
return err
79
}
···
97
}
98
99
// TODO make use of swap commit
100
+
func (rm *RepoMan) applyWrites(ctx context.Context, urepo models.Repo, writes []Op, swapCommit *string) ([]ApplyWriteResult, error) {
101
rootcid, err := cid.Cast(urepo.Root)
102
if err != nil {
103
return nil, err
104
}
105
106
+
dbs := rm.s.getBlockstore(urepo.Did)
107
+
bs := recording_blockstore.New(dbs)
108
+
r, err := repo.OpenRepo(ctx, bs, rootcid)
109
110
var results []ApplyWriteResult
111
112
+
entries := make([]models.Record, 0, len(writes))
113
for i, op := range writes {
114
+
// updates or deletes must supply an rkey
115
if op.Type != OpTypeCreate && op.Rkey == nil {
116
return nil, fmt.Errorf("invalid rkey")
117
} else if op.Type == OpTypeCreate && op.Rkey != nil {
118
+
// we should conver this op to an update if the rkey already exists
119
+
_, _, err := r.GetRecord(ctx, fmt.Sprintf("%s/%s", op.Collection, *op.Rkey))
120
if err == nil {
121
op.Type = OpTypeUpdate
122
}
123
} else if op.Rkey == nil {
124
+
// creates that don't supply an rkey will have one generated for them
125
op.Rkey = to.StringPtr(rm.clock.Next().String())
126
writes[i].Rkey = op.Rkey
127
}
128
129
+
// validate the record key is actually valid
130
_, err := syntax.ParseRecordKey(*op.Rkey)
131
if err != nil {
132
return nil, err
···
134
135
switch op.Type {
136
case OpTypeCreate:
137
+
// HACK: this fixes some type conversions, mainly around integers
138
+
// first we convert to json bytes
139
+
b, err := json.Marshal(*op.Record)
140
if err != nil {
141
return nil, err
142
}
143
+
// then we use atdata.UnmarshalJSON to convert it back to a map
144
+
out, err := atdata.UnmarshalJSON(b)
145
if err != nil {
146
return nil, err
147
}
148
+
// finally we can cast to a MarshalableMap
149
mm := MarshalableMap(out)
150
+
151
+
// HACK: if a record doesn't contain a $type, we can manually set it here based on the op's collection
152
+
// i forget why this is actually necessary?
153
+
if mm["$type"] == "" {
154
+
mm["$type"] = op.Collection
155
+
}
156
+
157
+
nc, err := r.PutRecord(ctx, fmt.Sprintf("%s/%s", op.Collection, *op.Rkey), &mm)
158
if err != nil {
159
return nil, err
160
}
161
+
162
+
d, err := atdata.MarshalCBOR(mm)
163
if err != nil {
164
return nil, err
165
}
166
+
167
entries = append(entries, models.Record{
168
Did: urepo.Did,
169
CreatedAt: rm.clock.Next().String(),
···
172
Cid: nc.String(),
173
Value: d,
174
})
175
+
176
results = append(results, ApplyWriteResult{
177
Type: to.StringPtr(OpTypeCreate.String()),
178
Uri: to.StringPtr("at://" + urepo.Did + "/" + op.Collection + "/" + *op.Rkey),
···
180
ValidationStatus: to.StringPtr("valid"), // TODO: obviously this might not be true atm lol
181
})
182
case OpTypeDelete:
183
+
// try to find the old record in the database
184
var old models.Record
185
+
if err := rm.db.Raw(ctx, "SELECT value FROM records WHERE did = ? AND nsid = ? AND rkey = ?", nil, urepo.Did, op.Collection, op.Rkey).Scan(&old).Error; err != nil {
186
return nil, err
187
}
188
+
189
+
// TODO: this is really confusing, and looking at it i have no idea why i did this. below when we are doing deletes, we
190
+
// check if `cid` here is nil to indicate if we should delete. that really doesn't make much sense and its super illogical
191
+
// when reading this code. i dont feel like fixing right now though so
192
entries = append(entries, models.Record{
193
Did: urepo.Did,
194
Nsid: op.Collection,
195
Rkey: *op.Rkey,
196
Value: old.Value,
197
})
198
+
199
+
// delete the record from the repo
200
+
err := r.DeleteRecord(ctx, fmt.Sprintf("%s/%s", op.Collection, *op.Rkey))
201
if err != nil {
202
return nil, err
203
}
204
+
205
+
// add a result for the delete
206
results = append(results, ApplyWriteResult{
207
Type: to.StringPtr(OpTypeDelete.String()),
208
})
209
case OpTypeUpdate:
210
+
// HACK: same hack as above for type fixes
211
+
b, err := json.Marshal(*op.Record)
212
if err != nil {
213
return nil, err
214
}
215
+
out, err := atdata.UnmarshalJSON(b)
216
if err != nil {
217
return nil, err
218
}
219
mm := MarshalableMap(out)
220
+
221
+
nc, err := r.UpdateRecord(ctx, fmt.Sprintf("%s/%s", op.Collection, *op.Rkey), &mm)
222
if err != nil {
223
return nil, err
224
}
225
+
226
+
d, err := atdata.MarshalCBOR(mm)
227
if err != nil {
228
return nil, err
229
}
230
+
231
entries = append(entries, models.Record{
232
Did: urepo.Did,
233
CreatedAt: rm.clock.Next().String(),
···
236
Cid: nc.String(),
237
Value: d,
238
})
239
+
240
results = append(results, ApplyWriteResult{
241
Type: to.StringPtr(OpTypeUpdate.String()),
242
Uri: to.StringPtr("at://" + urepo.Did + "/" + op.Collection + "/" + *op.Rkey),
···
246
}
247
}
248
249
+
// commit and get the new root
250
+
newroot, rev, err := r.Commit(ctx, urepo.SignFor)
251
if err != nil {
252
return nil, err
253
}
254
255
+
for _, result := range results {
256
+
if result.Type != nil {
257
+
metrics.RepoOperations.WithLabelValues(*result.Type).Inc()
258
+
}
259
+
}
260
+
261
+
// create a buffer for dumping our new cbor into
262
buf := new(bytes.Buffer)
263
264
+
// first write the car header to the buffer
265
hb, err := cbor.DumpObject(&car.CarHeader{
266
Roots: []cid.Cid{newroot},
267
Version: 1,
268
})
269
if _, err := carstore.LdWrite(buf, hb); err != nil {
270
return nil, err
271
}
272
273
+
// get a diff of the changes to the repo
274
+
diffops, err := r.DiffSince(ctx, rootcid)
275
if err != nil {
276
return nil, err
277
}
278
279
+
// create the repo ops for the given diff
280
ops := make([]*atproto.SyncSubscribeRepos_RepoOp, 0, len(diffops))
281
for _, op := range diffops {
282
var c cid.Cid
283
switch op.Op {
···
306
})
307
}
308
309
+
blk, err := dbs.Get(ctx, c)
310
if err != nil {
311
return nil, err
312
}
313
314
+
// write the block to the buffer
315
if _, err := carstore.LdWrite(buf, blk.Cid().Bytes(), blk.RawData()); err != nil {
316
return nil, err
317
}
318
}
319
320
+
// write the writelog to the buffer
321
+
for _, op := range bs.GetWriteLog() {
322
if _, err := carstore.LdWrite(buf, op.Cid().Bytes(), op.RawData()); err != nil {
323
return nil, err
324
}
325
}
326
327
+
// blob blob blob blob blob :3
328
var blobs []lexutil.LexLink
329
for _, entry := range entries {
330
var cids []cid.Cid
331
+
// whenever there is cid present, we know it's a create (dumb)
332
if entry.Cid != "" {
333
+
if err := rm.s.db.Create(ctx, &entry, []clause.Expression{clause.OnConflict{
334
Columns: []clause.Column{{Name: "did"}, {Name: "nsid"}, {Name: "rkey"}},
335
UpdateAll: true,
336
}}).Error; err != nil {
337
return nil, err
338
}
339
340
+
// increment the given blob refs, yay
341
+
cids, err = rm.incrementBlobRefs(ctx, urepo, entry.Value)
342
if err != nil {
343
return nil, err
344
}
345
} else {
346
+
// as i noted above this is dumb. but we delete whenever the cid is nil. it works solely becaue the pkey
347
+
// is did + collection + rkey. i still really want to separate that out, or use a different type to make
348
+
// this less confusing/easy to read. alas, its 2 am and yea no
349
+
if err := rm.s.db.Delete(ctx, &entry, nil).Error; err != nil {
350
return nil, err
351
}
352
+
353
+
// TODO:
354
+
cids, err = rm.decrementBlobRefs(ctx, urepo, entry.Value)
355
if err != nil {
356
return nil, err
357
}
358
}
359
360
+
// add all the relevant blobs to the blobs list of blobs. blob ^.^
361
for _, c := range cids {
362
blobs = append(blobs, lexutil.LexLink(c))
363
}
364
}
365
366
+
// NOTE: using the request ctx seems a bit suss here, so using a background context. i'm not sure if this
367
+
// runs sync or not
368
+
rm.s.evtman.AddEvent(context.Background(), &events.XRPCStreamEvent{
369
RepoCommit: &atproto.SyncSubscribeRepos_Commit{
370
Repo: urepo.Did,
371
Blocks: buf.Bytes(),
···
379
},
380
})
381
382
+
if err := rm.s.UpdateRepo(ctx, urepo.Did, newroot, rev); err != nil {
383
return nil, err
384
}
385
···
394
return results, nil
395
}
396
397
+
// this is a fun little guy. to get a proof, we need to read the record out of the blockstore and record how we actually
398
+
// got to the guy. we'll wrap a new blockstore in a recording blockstore, then return the log for proof
399
+
func (rm *RepoMan) getRecordProof(ctx context.Context, urepo models.Repo, collection, rkey string) (cid.Cid, []blocks.Block, error) {
400
c, err := cid.Cast(urepo.Root)
401
if err != nil {
402
return cid.Undef, nil, err
403
}
404
405
+
dbs := rm.s.getBlockstore(urepo.Did)
406
+
bs := recording_blockstore.New(dbs)
407
408
+
r, err := repo.OpenRepo(ctx, bs, c)
409
if err != nil {
410
return cid.Undef, nil, err
411
}
412
413
+
_, _, err = r.GetRecordBytes(ctx, fmt.Sprintf("%s/%s", collection, rkey))
414
if err != nil {
415
return cid.Undef, nil, err
416
}
417
418
+
return c, bs.GetReadLog(), nil
419
}
420
421
+
func (rm *RepoMan) incrementBlobRefs(ctx context.Context, urepo models.Repo, cbor []byte) ([]cid.Cid, error) {
422
cids, err := getBlobCidsFromCbor(cbor)
423
if err != nil {
424
return nil, err
425
}
426
427
for _, c := range cids {
428
+
if err := rm.db.Exec(ctx, "UPDATE blobs SET ref_count = ref_count + 1 WHERE did = ? AND cid = ?", nil, urepo.Did, c.Bytes()).Error; err != nil {
429
return nil, err
430
}
431
}
···
433
return cids, nil
434
}
435
436
+
func (rm *RepoMan) decrementBlobRefs(ctx context.Context, urepo models.Repo, cbor []byte) ([]cid.Cid, error) {
437
cids, err := getBlobCidsFromCbor(cbor)
438
if err != nil {
439
return nil, err
···
444
ID uint
445
Count int
446
}
447
+
if err := rm.db.Raw(ctx, "UPDATE blobs SET ref_count = ref_count - 1 WHERE did = ? AND cid = ? RETURNING id, ref_count", nil, urepo.Did, c.Bytes()).Scan(&res).Error; err != nil {
448
return nil, err
449
}
450
451
+
// TODO: this does _not_ handle deletions of blobs that are on s3 storage!!!! we need to get the blob, see what
452
+
// storage it is in, and clean up s3!!!!
453
if res.Count == 0 {
454
+
if err := rm.db.Exec(ctx, "DELETE FROM blobs WHERE id = ?", nil, res.ID).Error; err != nil {
455
return nil, err
456
}
457
+
if err := rm.db.Exec(ctx, "DELETE FROM blob_parts WHERE blob_id = ?", nil, res.ID).Error; err != nil {
458
return nil, err
459
}
460
}
···
468
func getBlobCidsFromCbor(cbor []byte) ([]cid.Cid, error) {
469
var cids []cid.Cid
470
471
+
decoded, err := atdata.UnmarshalCBOR(cbor)
472
if err != nil {
473
return nil, fmt.Errorf("error unmarshaling cbor: %w", err)
474
}
475
476
+
var deepiter func(any) error
477
+
deepiter = func(item any) error {
478
switch val := item.(type) {
479
+
case map[string]any:
480
if val["$type"] == "blob" {
481
if ref, ok := val["ref"].(string); ok {
482
c, err := cid.Parse(ref)
···
489
return deepiter(v)
490
}
491
}
492
+
case []any:
493
for _, v := range val {
494
deepiter(v)
495
}
+164
-59
server/server.go
+164
-59
server/server.go
···
38
"github.com/haileyok/cocoon/oauth/dpop"
39
"github.com/haileyok/cocoon/oauth/provider"
40
"github.com/haileyok/cocoon/plc"
41
echo_session "github.com/labstack/echo-contrib/session"
42
"github.com/labstack/echo/v4"
43
"github.com/labstack/echo/v4/middleware"
44
slogecho "github.com/samber/slog-echo"
45
"gorm.io/driver/sqlite"
46
"gorm.io/gorm"
47
)
···
51
)
52
53
type S3Config struct {
54
-
BackupsEnabled bool
55
-
Endpoint string
56
-
Region string
57
-
Bucket string
58
-
AccessKey string
59
-
SecretKey string
60
}
61
62
type Server struct {
···
74
oauthProvider *provider.Provider
75
evtman *events.EventManager
76
passport *identity.Passport
77
78
dbName string
79
s3Config *S3Config
80
}
81
82
type Args struct {
83
Addr string
84
DbName string
85
-
Logger *slog.Logger
86
Version string
87
Did string
88
Hostname string
···
91
ContactEmail string
92
Relays []string
93
AdminPassword string
94
95
SmtpUser string
96
SmtpPass string
···
103
104
SessionSecret string
105
106
-
DefaultAtprotoProxy string
107
}
108
109
type config struct {
110
-
Version string
111
-
Did string
112
-
Hostname string
113
-
ContactEmail string
114
-
EnforcePeering bool
115
-
Relays []string
116
-
AdminPassword string
117
-
SmtpEmail string
118
-
SmtpName string
119
-
DefaultAtprotoProxy string
120
}
121
122
type CustomValidator struct {
···
194
}
195
196
func New(args *Args) (*Server, error) {
197
if args.Addr == "" {
198
return nil, fmt.Errorf("addr must be set")
199
}
···
222
return nil, fmt.Errorf("admin password must be set")
223
}
224
225
-
if args.Logger == nil {
226
-
args.Logger = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{}))
227
-
}
228
-
229
if args.SessionSecret == "" {
230
panic("SESSION SECRET WAS NOT SET. THIS IS REQUIRED. ")
231
}
···
233
e := echo.New()
234
235
e.Pre(middleware.RemoveTrailingSlash())
236
-
e.Pre(slogecho.New(args.Logger))
237
e.Use(echo_session.Middleware(sessions.NewCookieStore([]byte(args.SessionSecret))))
238
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
239
AllowOrigins: []string{"*"},
240
AllowHeaders: []string{"*"},
···
280
IdleTimeout: 5 * time.Minute,
281
}
282
283
-
gdb, err := gorm.Open(sqlite.Open("cocoon.db"), &gorm.Config{})
284
-
if err != nil {
285
-
return nil, err
286
}
287
dbw := db.NewDB(gdb)
288
···
325
var nonceSecret []byte
326
maybeSecret, err := os.ReadFile("nonce.secret")
327
if err != nil && !os.IsNotExist(err) {
328
-
args.Logger.Error("error attempting to read nonce secret", "error", err)
329
} else {
330
nonceSecret = maybeSecret
331
}
···
339
plcClient: plcClient,
340
privateKey: &pkey,
341
config: &config{
342
-
Version: args.Version,
343
-
Did: args.Did,
344
-
Hostname: args.Hostname,
345
-
ContactEmail: args.ContactEmail,
346
-
EnforcePeering: false,
347
-
Relays: args.Relays,
348
-
AdminPassword: args.AdminPassword,
349
-
SmtpName: args.SmtpName,
350
-
SmtpEmail: args.SmtpEmail,
351
-
DefaultAtprotoProxy: args.DefaultAtprotoProxy,
352
},
353
evtman: events.NewEventManager(events.NewMemPersister()),
354
passport: identity.NewPassport(h, identity.NewMemCache(10_000)),
355
356
dbName: args.DbName,
357
s3Config: args.S3Config,
358
359
oauthProvider: provider.NewProvider(provider.Args{
360
Hostname: args.Hostname,
361
ClientManagerArgs: client.ManagerArgs{
362
Cli: oauthCli,
363
-
Logger: args.Logger,
364
},
365
DpopManagerArgs: dpop.ManagerArgs{
366
NonceSecret: nonceSecret,
367
NonceRotationInterval: constants.NonceMaxRotationInterval / 3,
368
OnNonceSecretCreated: func(newNonce []byte) {
369
if err := os.WriteFile("nonce.secret", newNonce, 0644); err != nil {
370
-
args.Logger.Error("error writing new nonce secret", "error", err)
371
}
372
},
373
-
Logger: args.Logger,
374
Hostname: args.Hostname,
375
},
376
}),
···
382
383
// TODO: should validate these args
384
if args.SmtpUser == "" || args.SmtpPass == "" || args.SmtpHost == "" || args.SmtpPort == "" || args.SmtpEmail == "" || args.SmtpName == "" {
385
-
args.Logger.Warn("not enough smpt args were provided. mailing will not work for your server.")
386
} else {
387
mail := mailyak.New(args.SmtpHost+":"+args.SmtpPort, smtp.PlainAuth("", args.SmtpUser, args.SmtpPass, args.SmtpHost))
388
mail.From(s.config.SmtpEmail)
···
407
s.echo.GET("/", s.handleRoot)
408
s.echo.GET("/xrpc/_health", s.handleHealth)
409
s.echo.GET("/.well-known/did.json", s.handleWellKnown)
410
s.echo.GET("/.well-known/oauth-protected-resource", s.handleOauthProtectedResource)
411
s.echo.GET("/.well-known/oauth-authorization-server", s.handleOauthAuthorizationServer)
412
s.echo.GET("/robots.txt", s.handleRobots)
···
414
// public
415
s.echo.GET("/xrpc/com.atproto.identity.resolveHandle", s.handleResolveHandle)
416
s.echo.POST("/xrpc/com.atproto.server.createAccount", s.handleCreateAccount)
417
-
s.echo.POST("/xrpc/com.atproto.server.createAccount", s.handleCreateAccount)
418
s.echo.POST("/xrpc/com.atproto.server.createSession", s.handleCreateSession)
419
s.echo.GET("/xrpc/com.atproto.server.describeServer", s.handleDescribeServer)
420
421
s.echo.GET("/xrpc/com.atproto.repo.describeRepo", s.handleDescribeRepo)
422
s.echo.GET("/xrpc/com.atproto.sync.listRepos", s.handleListRepos)
···
431
s.echo.GET("/xrpc/com.atproto.sync.listBlobs", s.handleSyncListBlobs)
432
s.echo.GET("/xrpc/com.atproto.sync.getBlob", s.handleSyncGetBlob)
433
434
// account
435
s.echo.GET("/account", s.handleAccount)
436
s.echo.POST("/account/revoke", s.handleAccountRevoke)
···
451
s.echo.GET("/xrpc/com.atproto.server.getSession", s.handleGetSession, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
452
s.echo.POST("/xrpc/com.atproto.server.refreshSession", s.handleRefreshSession, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
453
s.echo.POST("/xrpc/com.atproto.server.deleteSession", s.handleDeleteSession, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
454
s.echo.POST("/xrpc/com.atproto.identity.updateHandle", s.handleIdentityUpdateHandle, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
455
s.echo.POST("/xrpc/com.atproto.server.confirmEmail", s.handleServerConfirmEmail, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
456
s.echo.POST("/xrpc/com.atproto.server.requestEmailConfirmation", s.handleServerRequestEmailConfirmation, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
457
s.echo.POST("/xrpc/com.atproto.server.requestPasswordReset", s.handleServerRequestPasswordReset) // AUTH NOT REQUIRED FOR THIS ONE
···
460
s.echo.POST("/xrpc/com.atproto.server.updateEmail", s.handleServerUpdateEmail, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
461
s.echo.GET("/xrpc/com.atproto.server.getServiceAuth", s.handleServerGetServiceAuth, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
462
s.echo.GET("/xrpc/com.atproto.server.checkAccountStatus", s.handleServerCheckAccountStatus, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
463
464
// repo
465
s.echo.POST("/xrpc/com.atproto.repo.createRecord", s.handleCreateRecord, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
466
s.echo.POST("/xrpc/com.atproto.repo.putRecord", s.handlePutRecord, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
467
s.echo.POST("/xrpc/com.atproto.repo.deleteRecord", s.handleDeleteRecord, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
···
472
// stupid silly endpoints
473
s.echo.GET("/xrpc/app.bsky.actor.getPreferences", s.handleActorGetPreferences, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
474
s.echo.POST("/xrpc/app.bsky.actor.putPreferences", s.handleActorPutPreferences, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
475
476
// admin routes
477
s.echo.POST("/xrpc/com.atproto.server.createInviteCode", s.handleCreateInviteCode, s.handleAdminMiddleware)
···
483
}
484
485
func (s *Server) Serve(ctx context.Context) error {
486
s.addRoutes()
487
488
-
s.logger.Info("migrating...")
489
490
s.db.AutoMigrate(
491
&models.Actor{},
···
497
&models.Record{},
498
&models.Blob{},
499
&models.BlobPart{},
500
&provider.OauthToken{},
501
&provider.OauthAuthorizationRequest{},
502
)
503
504
-
s.logger.Info("starting cocoon")
505
506
go func() {
507
if err := s.httpd.ListenAndServe(); err != nil {
···
511
512
go s.backupRoutine()
513
514
for _, relay := range s.config.Relays {
515
cli := xrpc.Client{Host: relay}
516
-
atproto.SyncRequestCrawl(ctx, &cli, &atproto.SyncRequestCrawl_Input{
517
Hostname: s.config.Hostname,
518
-
})
519
}
520
521
-
<-ctx.Done()
522
-
523
-
fmt.Println("shut down")
524
525
return nil
526
}
527
528
func (s *Server) doBackup() {
529
start := time.Now()
530
531
-
s.logger.Info("beginning backup to s3...")
532
533
var buf bytes.Buffer
534
if err := func() error {
535
-
s.logger.Info("reading database bytes...")
536
s.db.Lock()
537
defer s.db.Unlock()
538
···
548
549
return nil
550
}(); err != nil {
551
-
s.logger.Error("error backing up database", "error", err)
552
return
553
}
554
555
if err := func() error {
556
-
s.logger.Info("sending to s3...")
557
558
currTime := time.Now().Format("2006-01-02_15-04-05")
559
key := "cocoon-backup-" + currTime + ".db"
···
583
return fmt.Errorf("error uploading file to s3: %w", err)
584
}
585
586
-
s.logger.Info("finished uploading backup to s3", "key", key, "duration", time.Now().Sub(start).Seconds())
587
588
return nil
589
}(); err != nil {
590
-
s.logger.Error("error uploading database backup", "error", err)
591
return
592
}
593
···
595
}
596
597
func (s *Server) backupRoutine() {
598
if s.s3Config == nil || !s.s3Config.BackupsEnabled {
599
return
600
}
601
602
if s.s3Config.Region == "" {
603
-
s.logger.Warn("no s3 region configured but backups are enabled. backups will not run.")
604
return
605
}
606
607
if s.s3Config.Bucket == "" {
608
-
s.logger.Warn("no s3 bucket configured but backups are enabled. backups will not run.")
609
return
610
}
611
612
if s.s3Config.AccessKey == "" {
613
-
s.logger.Warn("no s3 access key configured but backups are enabled. backups will not run.")
614
return
615
}
616
617
if s.s3Config.SecretKey == "" {
618
-
s.logger.Warn("no s3 secret key configured but backups are enabled. backups will not run.")
619
return
620
}
621
···
641
go s.doBackup()
642
}
643
}
···
38
"github.com/haileyok/cocoon/oauth/dpop"
39
"github.com/haileyok/cocoon/oauth/provider"
40
"github.com/haileyok/cocoon/plc"
41
+
"github.com/ipfs/go-cid"
42
+
"github.com/labstack/echo-contrib/echoprometheus"
43
echo_session "github.com/labstack/echo-contrib/session"
44
"github.com/labstack/echo/v4"
45
"github.com/labstack/echo/v4/middleware"
46
slogecho "github.com/samber/slog-echo"
47
+
"gorm.io/driver/postgres"
48
"gorm.io/driver/sqlite"
49
"gorm.io/gorm"
50
)
···
54
)
55
56
type S3Config struct {
57
+
BackupsEnabled bool
58
+
BlobstoreEnabled bool
59
+
Endpoint string
60
+
Region string
61
+
Bucket string
62
+
AccessKey string
63
+
SecretKey string
64
+
CDNUrl string
65
}
66
67
type Server struct {
···
79
oauthProvider *provider.Provider
80
evtman *events.EventManager
81
passport *identity.Passport
82
+
fallbackProxy string
83
+
84
+
lastRequestCrawl time.Time
85
+
requestCrawlMu sync.Mutex
86
87
dbName string
88
+
dbType string
89
s3Config *S3Config
90
}
91
92
type Args struct {
93
+
Logger *slog.Logger
94
+
95
Addr string
96
DbName string
97
+
DbType string
98
+
DatabaseURL string
99
Version string
100
Did string
101
Hostname string
···
104
ContactEmail string
105
Relays []string
106
AdminPassword string
107
+
RequireInvite bool
108
109
SmtpUser string
110
SmtpPass string
···
117
118
SessionSecret string
119
120
+
BlockstoreVariant BlockstoreVariant
121
+
FallbackProxy string
122
}
123
124
type config struct {
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
137
}
138
139
type CustomValidator struct {
···
211
}
212
213
func New(args *Args) (*Server, error) {
214
+
if args.Logger == nil {
215
+
args.Logger = slog.Default()
216
+
}
217
+
218
+
logger := args.Logger.With("name", "New")
219
+
220
if args.Addr == "" {
221
return nil, fmt.Errorf("addr must be set")
222
}
···
245
return nil, fmt.Errorf("admin password must be set")
246
}
247
248
if args.SessionSecret == "" {
249
panic("SESSION SECRET WAS NOT SET. THIS IS REQUIRED. ")
250
}
···
252
e := echo.New()
253
254
e.Pre(middleware.RemoveTrailingSlash())
255
+
e.Pre(slogecho.New(args.Logger.With("component", "slogecho")))
256
e.Use(echo_session.Middleware(sessions.NewCookieStore([]byte(args.SessionSecret))))
257
+
e.Use(echoprometheus.NewMiddleware("cocoon"))
258
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
259
AllowOrigins: []string{"*"},
260
AllowHeaders: []string{"*"},
···
300
IdleTimeout: 5 * time.Minute,
301
}
302
303
+
dbType := args.DbType
304
+
if dbType == "" {
305
+
dbType = "sqlite"
306
+
}
307
+
308
+
var gdb *gorm.DB
309
+
var err error
310
+
switch dbType {
311
+
case "postgres":
312
+
if args.DatabaseURL == "" {
313
+
return nil, fmt.Errorf("database-url must be set when using postgres")
314
+
}
315
+
gdb, err = gorm.Open(postgres.Open(args.DatabaseURL), &gorm.Config{})
316
+
if err != nil {
317
+
return nil, fmt.Errorf("failed to connect to postgres: %w", err)
318
+
}
319
+
logger.Info("connected to PostgreSQL database")
320
+
default:
321
+
gdb, err = gorm.Open(sqlite.Open(args.DbName), &gorm.Config{})
322
+
if err != nil {
323
+
return nil, fmt.Errorf("failed to open sqlite database: %w", err)
324
+
}
325
+
logger.Info("connected to SQLite database", "path", args.DbName)
326
}
327
dbw := db.NewDB(gdb)
328
···
365
var nonceSecret []byte
366
maybeSecret, err := os.ReadFile("nonce.secret")
367
if err != nil && !os.IsNotExist(err) {
368
+
logger.Error("error attempting to read nonce secret", "error", err)
369
} else {
370
nonceSecret = maybeSecret
371
}
···
379
plcClient: plcClient,
380
privateKey: &pkey,
381
config: &config{
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,
394
},
395
evtman: events.NewEventManager(events.NewMemPersister()),
396
passport: identity.NewPassport(h, identity.NewMemCache(10_000)),
397
398
dbName: args.DbName,
399
+
dbType: dbType,
400
s3Config: args.S3Config,
401
402
oauthProvider: provider.NewProvider(provider.Args{
403
Hostname: args.Hostname,
404
ClientManagerArgs: client.ManagerArgs{
405
Cli: oauthCli,
406
+
Logger: args.Logger.With("component", "oauth-client-manager"),
407
},
408
DpopManagerArgs: dpop.ManagerArgs{
409
NonceSecret: nonceSecret,
410
NonceRotationInterval: constants.NonceMaxRotationInterval / 3,
411
OnNonceSecretCreated: func(newNonce []byte) {
412
if err := os.WriteFile("nonce.secret", newNonce, 0644); err != nil {
413
+
logger.Error("error writing new nonce secret", "error", err)
414
}
415
},
416
+
Logger: args.Logger.With("component", "dpop-manager"),
417
Hostname: args.Hostname,
418
},
419
}),
···
425
426
// TODO: should validate these args
427
if args.SmtpUser == "" || args.SmtpPass == "" || args.SmtpHost == "" || args.SmtpPort == "" || args.SmtpEmail == "" || args.SmtpName == "" {
428
+
args.Logger.Warn("not enough smtp args were provided. mailing will not work for your server.")
429
} else {
430
mail := mailyak.New(args.SmtpHost+":"+args.SmtpPort, smtp.PlainAuth("", args.SmtpUser, args.SmtpPass, args.SmtpHost))
431
mail.From(s.config.SmtpEmail)
···
450
s.echo.GET("/", s.handleRoot)
451
s.echo.GET("/xrpc/_health", s.handleHealth)
452
s.echo.GET("/.well-known/did.json", s.handleWellKnown)
453
+
s.echo.GET("/.well-known/atproto-did", s.handleAtprotoDid)
454
s.echo.GET("/.well-known/oauth-protected-resource", s.handleOauthProtectedResource)
455
s.echo.GET("/.well-known/oauth-authorization-server", s.handleOauthAuthorizationServer)
456
s.echo.GET("/robots.txt", s.handleRobots)
···
458
// public
459
s.echo.GET("/xrpc/com.atproto.identity.resolveHandle", s.handleResolveHandle)
460
s.echo.POST("/xrpc/com.atproto.server.createAccount", s.handleCreateAccount)
461
s.echo.POST("/xrpc/com.atproto.server.createSession", s.handleCreateSession)
462
s.echo.GET("/xrpc/com.atproto.server.describeServer", s.handleDescribeServer)
463
+
s.echo.POST("/xrpc/com.atproto.server.reserveSigningKey", s.handleServerReserveSigningKey)
464
465
s.echo.GET("/xrpc/com.atproto.repo.describeRepo", s.handleDescribeRepo)
466
s.echo.GET("/xrpc/com.atproto.sync.listRepos", s.handleListRepos)
···
475
s.echo.GET("/xrpc/com.atproto.sync.listBlobs", s.handleSyncListBlobs)
476
s.echo.GET("/xrpc/com.atproto.sync.getBlob", s.handleSyncGetBlob)
477
478
+
// labels
479
+
s.echo.GET("/xrpc/com.atproto.label.queryLabels", s.handleLabelQueryLabels)
480
+
481
// account
482
s.echo.GET("/account", s.handleAccount)
483
s.echo.POST("/account/revoke", s.handleAccountRevoke)
···
498
s.echo.GET("/xrpc/com.atproto.server.getSession", s.handleGetSession, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
499
s.echo.POST("/xrpc/com.atproto.server.refreshSession", s.handleRefreshSession, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
500
s.echo.POST("/xrpc/com.atproto.server.deleteSession", s.handleDeleteSession, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
501
+
s.echo.GET("/xrpc/com.atproto.identity.getRecommendedDidCredentials", s.handleGetRecommendedDidCredentials, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
502
s.echo.POST("/xrpc/com.atproto.identity.updateHandle", s.handleIdentityUpdateHandle, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
503
+
s.echo.POST("/xrpc/com.atproto.identity.requestPlcOperationSignature", s.handleIdentityRequestPlcOperationSignature, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
504
+
s.echo.POST("/xrpc/com.atproto.identity.signPlcOperation", s.handleSignPlcOperation, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
505
+
s.echo.POST("/xrpc/com.atproto.identity.submitPlcOperation", s.handleSubmitPlcOperation, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
506
s.echo.POST("/xrpc/com.atproto.server.confirmEmail", s.handleServerConfirmEmail, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
507
s.echo.POST("/xrpc/com.atproto.server.requestEmailConfirmation", s.handleServerRequestEmailConfirmation, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
508
s.echo.POST("/xrpc/com.atproto.server.requestPasswordReset", s.handleServerRequestPasswordReset) // AUTH NOT REQUIRED FOR THIS ONE
···
511
s.echo.POST("/xrpc/com.atproto.server.updateEmail", s.handleServerUpdateEmail, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
512
s.echo.GET("/xrpc/com.atproto.server.getServiceAuth", s.handleServerGetServiceAuth, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
513
s.echo.GET("/xrpc/com.atproto.server.checkAccountStatus", s.handleServerCheckAccountStatus, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
514
+
s.echo.POST("/xrpc/com.atproto.server.deactivateAccount", s.handleServerDeactivateAccount, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
515
+
s.echo.POST("/xrpc/com.atproto.server.activateAccount", s.handleServerActivateAccount, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
516
+
s.echo.POST("/xrpc/com.atproto.server.requestAccountDelete", s.handleServerRequestAccountDelete, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
517
+
s.echo.POST("/xrpc/com.atproto.server.deleteAccount", s.handleServerDeleteAccount)
518
519
// repo
520
+
s.echo.GET("/xrpc/com.atproto.repo.listMissingBlobs", s.handleListMissingBlobs, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
521
s.echo.POST("/xrpc/com.atproto.repo.createRecord", s.handleCreateRecord, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
522
s.echo.POST("/xrpc/com.atproto.repo.putRecord", s.handlePutRecord, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
523
s.echo.POST("/xrpc/com.atproto.repo.deleteRecord", s.handleDeleteRecord, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
···
528
// stupid silly endpoints
529
s.echo.GET("/xrpc/app.bsky.actor.getPreferences", s.handleActorGetPreferences, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
530
s.echo.POST("/xrpc/app.bsky.actor.putPreferences", s.handleActorPutPreferences, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
531
+
s.echo.GET("/xrpc/app.bsky.feed.getFeed", s.handleProxyBskyFeedGetFeed, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware)
532
533
// admin routes
534
s.echo.POST("/xrpc/com.atproto.server.createInviteCode", s.handleCreateInviteCode, s.handleAdminMiddleware)
···
540
}
541
542
func (s *Server) Serve(ctx context.Context) error {
543
+
logger := s.logger.With("name", "Serve")
544
+
545
s.addRoutes()
546
547
+
logger.Info("migrating...")
548
549
s.db.AutoMigrate(
550
&models.Actor{},
···
556
&models.Record{},
557
&models.Blob{},
558
&models.BlobPart{},
559
+
&models.ReservedKey{},
560
&provider.OauthToken{},
561
&provider.OauthAuthorizationRequest{},
562
)
563
564
+
logger.Info("starting cocoon")
565
566
go func() {
567
if err := s.httpd.ListenAndServe(); err != nil {
···
571
572
go s.backupRoutine()
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
+
598
for _, relay := range s.config.Relays {
599
+
logger := logger.With("relay", relay)
600
+
logger.Info("requesting crawl from relay")
601
cli := xrpc.Client{Host: relay}
602
+
if err := atproto.SyncRequestCrawl(ctx, &cli, &atproto.SyncRequestCrawl_Input{
603
Hostname: s.config.Hostname,
604
+
}); err != nil {
605
+
logger.Error("error requesting crawl", "err", err)
606
+
} else {
607
+
logger.Info("crawl requested successfully")
608
+
}
609
}
610
611
+
s.lastRequestCrawl = time.Now()
612
613
return nil
614
}
615
616
func (s *Server) doBackup() {
617
+
logger := s.logger.With("name", "doBackup")
618
+
619
+
if s.dbType == "postgres" {
620
+
logger.Info("skipping S3 backup - PostgreSQL backups should be handled externally (pg_dump, managed database backups, etc.)")
621
+
return
622
+
}
623
+
624
start := time.Now()
625
626
+
logger.Info("beginning backup to s3...")
627
628
var buf bytes.Buffer
629
if err := func() error {
630
+
logger.Info("reading database bytes...")
631
s.db.Lock()
632
defer s.db.Unlock()
633
···
643
644
return nil
645
}(); err != nil {
646
+
logger.Error("error backing up database", "error", err)
647
return
648
}
649
650
if err := func() error {
651
+
logger.Info("sending to s3...")
652
653
currTime := time.Now().Format("2006-01-02_15-04-05")
654
key := "cocoon-backup-" + currTime + ".db"
···
678
return fmt.Errorf("error uploading file to s3: %w", err)
679
}
680
681
+
logger.Info("finished uploading backup to s3", "key", key, "duration", time.Now().Sub(start).Seconds())
682
683
return nil
684
}(); err != nil {
685
+
logger.Error("error uploading database backup", "error", err)
686
return
687
}
688
···
690
}
691
692
func (s *Server) backupRoutine() {
693
+
logger := s.logger.With("name", "backupRoutine")
694
+
695
if s.s3Config == nil || !s.s3Config.BackupsEnabled {
696
return
697
}
698
699
if s.s3Config.Region == "" {
700
+
logger.Warn("no s3 region configured but backups are enabled. backups will not run.")
701
return
702
}
703
704
if s.s3Config.Bucket == "" {
705
+
logger.Warn("no s3 bucket configured but backups are enabled. backups will not run.")
706
return
707
}
708
709
if s.s3Config.AccessKey == "" {
710
+
logger.Warn("no s3 access key configured but backups are enabled. backups will not run.")
711
return
712
}
713
714
if s.s3Config.SecretKey == "" {
715
+
logger.Warn("no s3 secret key configured but backups are enabled. backups will not run.")
716
return
717
}
718
···
738
go s.doBackup()
739
}
740
}
741
+
742
+
func (s *Server) UpdateRepo(ctx context.Context, did string, root cid.Cid, rev string) error {
743
+
if err := s.db.Exec(ctx, "UPDATE repos SET root = ?, rev = ? WHERE did = ?", nil, root.Bytes(), rev, did).Error; err != nil {
744
+
return err
745
+
}
746
+
747
+
return nil
748
+
}
+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
package server
2
3
import (
4
"time"
5
6
"github.com/golang-jwt/jwt/v4"
···
13
RefreshToken string
14
}
15
16
-
func (s *Server) createSession(repo *models.Repo) (*Session, error) {
17
now := time.Now()
18
accexp := now.Add(3 * time.Hour)
19
refexp := now.Add(7 * 24 * time.Hour)
···
49
return nil, err
50
}
51
52
-
if err := s.db.Create(&models.Token{
53
Token: accessString,
54
Did: repo.Did,
55
RefreshToken: refreshString,
···
59
return nil, err
60
}
61
62
-
if err := s.db.Create(&models.RefreshToken{
63
Token: refreshString,
64
Did: repo.Did,
65
CreatedAt: now,
···
1
package server
2
3
import (
4
+
"context"
5
"time"
6
7
"github.com/golang-jwt/jwt/v4"
···
14
RefreshToken string
15
}
16
17
+
func (s *Server) createSession(ctx context.Context, repo *models.Repo) (*Session, error) {
18
now := time.Now()
19
accexp := now.Add(3 * time.Hour)
20
refexp := now.Add(7 * 24 * time.Hour)
···
50
return nil, err
51
}
52
53
+
if err := s.db.Create(ctx, &models.Token{
54
Token: accessString,
55
Did: repo.Did,
56
RefreshToken: refreshString,
···
60
return nil, err
61
}
62
63
+
if err := s.db.Create(ctx, &models.RefreshToken{
64
Token: refreshString,
65
Did: repo.Did,
66
CreatedAt: now,
+4
server/templates/signin.html
+4
server/templates/signin.html
···
26
type="password"
27
placeholder="Password"
28
/>
29
+
{{ if .flashes.tokenrequired }}
30
+
<br />
31
+
<input name="token" id="token" placeholder="Enter your 2FA token" />
32
+
{{ end }}
33
<input name="query_params" type="hidden" value="{{ .QueryParams }}" />
34
<button class="primary" type="submit" value="Login">Login</button>
35
</form>
+137
sqlite_blockstore/sqlite_blockstore.go
+137
sqlite_blockstore/sqlite_blockstore.go
···
···
1
+
package sqlite_blockstore
2
+
3
+
import (
4
+
"context"
5
+
"fmt"
6
+
7
+
"github.com/bluesky-social/indigo/atproto/syntax"
8
+
"github.com/haileyok/cocoon/internal/db"
9
+
"github.com/haileyok/cocoon/models"
10
+
blocks "github.com/ipfs/go-block-format"
11
+
"github.com/ipfs/go-cid"
12
+
"gorm.io/gorm/clause"
13
+
)
14
+
15
+
type SqliteBlockstore struct {
16
+
db *db.DB
17
+
did string
18
+
readonly bool
19
+
inserts map[cid.Cid]blocks.Block
20
+
}
21
+
22
+
func New(did string, db *db.DB) *SqliteBlockstore {
23
+
return &SqliteBlockstore{
24
+
did: did,
25
+
db: db,
26
+
readonly: false,
27
+
inserts: map[cid.Cid]blocks.Block{},
28
+
}
29
+
}
30
+
31
+
func NewReadOnly(did string, db *db.DB) *SqliteBlockstore {
32
+
return &SqliteBlockstore{
33
+
did: did,
34
+
db: db,
35
+
readonly: true,
36
+
inserts: map[cid.Cid]blocks.Block{},
37
+
}
38
+
}
39
+
40
+
func (bs *SqliteBlockstore) Get(ctx context.Context, cid cid.Cid) (blocks.Block, error) {
41
+
var block models.Block
42
+
43
+
maybeBlock, ok := bs.inserts[cid]
44
+
if ok {
45
+
return maybeBlock, nil
46
+
}
47
+
48
+
if err := bs.db.Raw(ctx, "SELECT * FROM blocks WHERE did = ? AND cid = ?", nil, bs.did, cid.Bytes()).Scan(&block).Error; err != nil {
49
+
return nil, err
50
+
}
51
+
52
+
b, err := blocks.NewBlockWithCid(block.Value, cid)
53
+
if err != nil {
54
+
return nil, err
55
+
}
56
+
57
+
return b, nil
58
+
}
59
+
60
+
func (bs *SqliteBlockstore) Put(ctx context.Context, block blocks.Block) error {
61
+
bs.inserts[block.Cid()] = block
62
+
63
+
if bs.readonly {
64
+
return nil
65
+
}
66
+
67
+
b := models.Block{
68
+
Did: bs.did,
69
+
Cid: block.Cid().Bytes(),
70
+
Rev: syntax.NewTIDNow(0).String(), // TODO: WARN, this is bad. don't do this
71
+
Value: block.RawData(),
72
+
}
73
+
74
+
if err := bs.db.Create(ctx, &b, []clause.Expression{clause.OnConflict{
75
+
Columns: []clause.Column{{Name: "did"}, {Name: "cid"}},
76
+
UpdateAll: true,
77
+
}}).Error; err != nil {
78
+
return err
79
+
}
80
+
81
+
return nil
82
+
}
83
+
84
+
func (bs *SqliteBlockstore) DeleteBlock(context.Context, cid.Cid) error {
85
+
panic("not implemented")
86
+
}
87
+
88
+
func (bs *SqliteBlockstore) Has(context.Context, cid.Cid) (bool, error) {
89
+
panic("not implemented")
90
+
}
91
+
92
+
func (bs *SqliteBlockstore) GetSize(context.Context, cid.Cid) (int, error) {
93
+
panic("not implemented")
94
+
}
95
+
96
+
func (bs *SqliteBlockstore) PutMany(ctx context.Context, blocks []blocks.Block) error {
97
+
tx := bs.db.BeginDangerously(ctx)
98
+
99
+
for _, block := range blocks {
100
+
bs.inserts[block.Cid()] = block
101
+
102
+
if bs.readonly {
103
+
continue
104
+
}
105
+
106
+
b := models.Block{
107
+
Did: bs.did,
108
+
Cid: block.Cid().Bytes(),
109
+
Rev: syntax.NewTIDNow(0).String(), // TODO: WARN, this is bad. don't do this
110
+
Value: block.RawData(),
111
+
}
112
+
113
+
if err := tx.Clauses(clause.OnConflict{
114
+
Columns: []clause.Column{{Name: "did"}, {Name: "cid"}},
115
+
UpdateAll: true,
116
+
}).Create(&b).Error; err != nil {
117
+
tx.Rollback()
118
+
return err
119
+
}
120
+
}
121
+
122
+
if bs.readonly {
123
+
return nil
124
+
}
125
+
126
+
tx.Commit()
127
+
128
+
return nil
129
+
}
130
+
131
+
func (bs *SqliteBlockstore) AllKeysChan(ctx context.Context) (<-chan cid.Cid, error) {
132
+
return nil, fmt.Errorf("iteration not allowed on sqlite blockstore")
133
+
}
134
+
135
+
func (bs *SqliteBlockstore) HashOnRead(enabled bool) {
136
+
panic("not implemented")
137
+
}
+1
-1
test.go
+1
-1
test.go