forked from hailey.at/cocoon
An atproto PDS written in Go

Compare changes

Choose any two refs to compare.

Changed files
+3203 -690
.github
workflows
cmd
cocoon
identity
internal
db
helpers
metrics
models
oauth
plc
server
templates
sqlite_blockstore
+1 -1
.env.example
··· 6 COCOON_RELAYS=https://bsky.network 7 # Generate with `openssl rand -hex 16` 8 COCOON_ADMIN_PASSWORD= 9 - # openssl rand -hex 32 10 COCOON_SESSION_SECRET=
··· 6 COCOON_RELAYS=https://bsky.network 7 # Generate with `openssl rand -hex 16` 8 COCOON_ADMIN_PASSWORD= 9 + # Generate with `openssl rand -hex 32` 10 COCOON_SESSION_SECRET=
+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
··· 4 *.key 5 *.secret 6 .DS_Store
··· 4 *.key 5 *.secret 6 .DS_Store 7 + data/ 8 + keys/ 9 + dist/
+10
Caddyfile
···
··· 1 + {$COCOON_HOSTNAME} { 2 + reverse_proxy localhost:8080 3 + 4 + encode gzip 5 + 6 + log { 7 + output file /data/access.log 8 + format json 9 + } 10 + }
+10
Caddyfile.postgres
···
··· 1 + {$COCOON_HOSTNAME} { 2 + reverse_proxy cocoon:8080 3 + 4 + encode gzip 5 + 6 + log { 7 + output file /data/access.log 8 + format json 9 + } 10 + }
+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
··· 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 .
+196 -12
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 ··· 40 - [x] `com.atproto.server.createInviteCode` 41 - [x] `com.atproto.server.createInviteCodes` 42 - [x] `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 ··· 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`
+103 -51
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", 112 EnvVars: []string{"COCOON_S3_REGION"}, ··· 128 EnvVars: []string{"COCOON_S3_SECRET_KEY"}, 129 }, 130 &cli.StringFlag{ 131 Name: "session-secret", 132 EnvVars: []string{"COCOON_SESSION_SECRET"}, 133 }, ··· 136 EnvVars: []string{"COCOON_BLOCKSTORE_VARIANT"}, 137 Value: "sqlite", 138 }, 139 }, 140 Commands: []*cli.Command{ 141 runServe, ··· 159 Flags: []cli.Flag{}, 160 Action: func(cmd *cli.Context) error { 161 162 s, err := server.New(&server.Args{ 163 Addr: cmd.String("addr"), 164 DbName: cmd.String("db-name"), 165 Did: cmd.String("did"), 166 Hostname: cmd.String("hostname"), 167 RotationKeyPath: cmd.String("rotation-key-path"), ··· 170 Version: Version, 171 Relays: cmd.StringSlice("relays"), 172 AdminPassword: cmd.String("admin-password"), 173 SmtpUser: cmd.String("smtp-user"), 174 SmtpPass: cmd.String("smtp-pass"), 175 SmtpHost: cmd.String("smtp-host"), ··· 177 SmtpEmail: cmd.String("smtp-email"), 178 SmtpName: cmd.String("smtp-name"), 179 S3Config: &server.S3Config{ 180 - BackupsEnabled: cmd.Bool("s3-backups-enabled"), 181 - Region: cmd.String("s3-region"), 182 - Bucket: cmd.String("s3-bucket"), 183 - Endpoint: cmd.String("s3-endpoint"), 184 - AccessKey: cmd.String("s3-access-key"), 185 - SecretKey: cmd.String("s3-secret-key"), 186 }, 187 SessionSecret: cmd.String("session-secret"), 188 BlockstoreVariant: server.MustReturnBlockstoreVariant(cmd.String("blockstore-variant")), 189 }) 190 if err != nil { 191 fmt.Printf("error creating cocoon: %v", err) ··· 212 }, 213 }, 214 Action: func(cmd *cli.Context) error { 215 - key, err := crypto.GeneratePrivateKeyK256() 216 if err != nil { 217 return err 218 } ··· 282 }, 283 }, 284 Action: func(cmd *cli.Context) error { 285 - db, err := newDb() 286 if err != nil { 287 return err 288 } ··· 321 }, 322 }, 323 Action: func(cmd *cli.Context) error { 324 - db, err := newDb() 325 if err != nil { 326 return err 327 } ··· 348 }, 349 } 350 351 - func newDb() (*gorm.DB, error) { 352 - return gorm.Open(sqlite.Open("cocoon.db"), &gorm.Config{}) 353 }
··· 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", 122 EnvVars: []string{"COCOON_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 }, ··· 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, ··· 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
···
··· 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
···
··· 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
···
··· 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
··· 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
··· 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=
+1 -1
identity/types.go
··· 4 Context []string `json:"@context"` 5 Id string `json:"id"` 6 AlsoKnownAs []string `json:"alsoKnownAs"` 7 - VerificationMethods []DidDocVerificationMethod `json:"verificationMethods"` 8 Service []DidDocService `json:"service"` 9 } 10
··· 4 Context []string `json:"@context"` 5 Id string `json:"id"` 6 AlsoKnownAs []string `json:"alsoKnownAs"` 7 + VerificationMethods []DidDocVerificationMethod `json:"verificationMethod"` 8 Service []DidDocService `json:"service"` 9 } 10
+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
··· 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
··· 32 return genericError(e, 400, msg) 33 } 34 35 func InvalidTokenError(e echo.Context) error { 36 return InputError(e, to.StringPtr("InvalidToken")) 37 }
··· 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
···
··· 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 + )
+24 -2
models/models.go
··· 5 "time" 6 7 "github.com/Azure/go-autorest/autorest/to" 8 - "github.com/bluesky-social/indigo/atproto/crypto" 9 ) 10 11 type Repo struct { ··· 19 EmailUpdateCodeExpiresAt *time.Time 20 PasswordResetCode *string 21 PasswordResetCodeExpiresAt *time.Time 22 Password string 23 SigningKey []byte 24 Rev string 25 Root []byte 26 Preferences []byte 27 Deactivated bool 28 } 29 30 func (r *Repo) SignFor(ctx context.Context, did string, msg []byte) ([]byte, error) { 31 - k, err := crypto.ParsePrivateBytesK256(r.SigningKey) 32 if err != nil { 33 return nil, err 34 } ··· 106 Did string `gorm:"index;index:idx_blob_did_cid"` 107 Cid []byte `gorm:"index;index:idx_blob_did_cid"` 108 RefCount int 109 } 110 111 type BlobPart struct { ··· 114 Idx int `gorm:"primaryKey"` 115 Data []byte 116 }
··· 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 } ··· 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
··· 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
··· 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
··· 14 } 15 16 func newJTICache(size int) *jtiCache { 17 - cache := cache.NewCache[string, bool]().WithTTL(24 * time.Hour).WithLRU().WithTTL(constants.JTITtl) 18 return &jtiCache{ 19 cache: cache, 20 mu: sync.Mutex{},
··· 14 } 15 16 func newJTICache(size int) *jtiCache { 17 + cache := cache.NewCache[string, bool]().WithTTL(24 * time.Hour).WithLRU().WithTTL(constants.JTITtl).WithMaxKeys(size) 18 return &jtiCache{ 19 cache: cache, 20 mu: sync.Mutex{},
+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
··· 102 } 103 104 func (n *Nonce) Check(nonce string) bool { 105 - n.mu.RLock() 106 - defer n.mu.RUnlock() 107 return nonce == n.prev || nonce == n.curr || nonce == n.next 108 }
··· 102 } 103 104 func (n *Nonce) Check(nonce string) bool { 105 + n.mu.Lock() 106 + defer n.mu.Unlock() 107 + n.rotate() 108 return nonce == n.prev || nonce == n.curr || nonce == n.next 109 }
+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
··· 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 }
+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
··· 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
··· 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
··· 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
··· 16 err := json.Unmarshal(repo.Preferences, &prefs) 17 if err != nil || prefs["preferences"] == nil { 18 prefs = map[string]any{ 19 - "preferences": map[string]any{}, 20 } 21 } 22
··· 16 err := json.Unmarshal(repo.Preferences, &prefs) 17 if err != nil || prefs["preferences"] == nil { 18 prefs = map[string]any{ 19 + "preferences": []any{}, 20 } 21 } 22
+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
+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
···
··· 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
···
··· 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
··· 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
+15 -12
server/handle_import_repo.go
··· 18 ) 19 20 func (s *Server) handleRepoImportRepo(e echo.Context) error { 21 urepo := e.Get("repo").(*models.RepoActor) 22 23 b, err := io.ReadAll(e.Request().Body) 24 if err != nil { 25 - s.logger.Error("could not read bytes in import request", "error", err) 26 return helpers.ServerError(e, nil) 27 } 28 ··· 30 31 cs, err := car.NewCarReader(bytes.NewReader(b)) 32 if err != nil { 33 - s.logger.Error("could not read car in import request", "error", err) 34 return helpers.ServerError(e, nil) 35 } 36 37 orderedBlocks := []blocks.Block{} 38 currBlock, err := cs.Next() 39 if err != nil { 40 - s.logger.Error("could not get first block from car", "error", err) 41 return helpers.ServerError(e, nil) 42 } 43 currBlockCt := 1 44 45 for currBlock != nil { 46 - s.logger.Info("someone is importing their repo", "block", currBlockCt) 47 orderedBlocks = append(orderedBlocks, currBlock) 48 next, _ := cs.Next() 49 currBlock = next ··· 53 slices.Reverse(orderedBlocks) 54 55 if err := bs.PutMany(context.TODO(), orderedBlocks); err != nil { 56 - s.logger.Error("could not insert blocks", "error", err) 57 return helpers.ServerError(e, nil) 58 } 59 60 r, err := repo.OpenRepo(context.TODO(), bs, cs.Header.Roots[0]) 61 if err != nil { 62 - s.logger.Error("could not open repo", "error", err) 63 return helpers.ServerError(e, nil) 64 } 65 66 - tx := s.db.BeginDangerously() 67 68 clock := syntax.NewTIDClock(0) 69 ··· 74 cidStr := cid.String() 75 b, err := bs.Get(context.TODO(), cid) 76 if err != nil { 77 - s.logger.Error("record bytes don't exist in blockstore", "error", err) 78 return helpers.ServerError(e, nil) 79 } 80 ··· 87 Value: b.RawData(), 88 } 89 90 - if err := tx.Create(rec).Error; err != nil { 91 return err 92 } 93 94 return nil 95 }); err != nil { 96 tx.Rollback() 97 - s.logger.Error("record bytes don't exist in blockstore", "error", err) 98 return helpers.ServerError(e, nil) 99 } 100 ··· 102 103 root, rev, err := r.Commit(context.TODO(), urepo.SignFor) 104 if err != nil { 105 - s.logger.Error("error committing", "error", err) 106 return helpers.ServerError(e, nil) 107 } 108 109 if err := s.UpdateRepo(context.TODO(), urepo.Repo.Did, root, rev); err != nil { 110 - s.logger.Error("error updating repo after commit", "error", err) 111 return helpers.ServerError(e, nil) 112 } 113
··· 18 ) 19 20 func (s *Server) handleRepoImportRepo(e echo.Context) error { 21 + ctx := e.Request().Context() 22 + logger := s.logger.With("name", "handleImportRepo") 23 + 24 urepo := e.Get("repo").(*models.RepoActor) 25 26 b, err := io.ReadAll(e.Request().Body) 27 if err != nil { 28 + logger.Error("could not read bytes in import request", "error", err) 29 return helpers.ServerError(e, nil) 30 } 31 ··· 33 34 cs, err := car.NewCarReader(bytes.NewReader(b)) 35 if err != nil { 36 + logger.Error("could not read car in import request", "error", err) 37 return helpers.ServerError(e, nil) 38 } 39 40 orderedBlocks := []blocks.Block{} 41 currBlock, err := cs.Next() 42 if err != nil { 43 + logger.Error("could not get first block from car", "error", err) 44 return helpers.ServerError(e, nil) 45 } 46 currBlockCt := 1 47 48 for currBlock != nil { 49 + logger.Info("someone is importing their repo", "block", currBlockCt) 50 orderedBlocks = append(orderedBlocks, currBlock) 51 next, _ := cs.Next() 52 currBlock = next ··· 56 slices.Reverse(orderedBlocks) 57 58 if err := bs.PutMany(context.TODO(), orderedBlocks); err != nil { 59 + logger.Error("could not insert blocks", "error", err) 60 return helpers.ServerError(e, nil) 61 } 62 63 r, err := repo.OpenRepo(context.TODO(), bs, cs.Header.Roots[0]) 64 if err != nil { 65 + logger.Error("could not open repo", "error", err) 66 return helpers.ServerError(e, nil) 67 } 68 69 + tx := s.db.BeginDangerously(ctx) 70 71 clock := syntax.NewTIDClock(0) 72 ··· 77 cidStr := cid.String() 78 b, err := bs.Get(context.TODO(), cid) 79 if err != nil { 80 + logger.Error("record bytes don't exist in blockstore", "error", err) 81 return helpers.ServerError(e, nil) 82 } 83 ··· 90 Value: b.RawData(), 91 } 92 93 + if err := tx.Save(rec).Error; err != nil { 94 return err 95 } 96 97 return nil 98 }); err != nil { 99 tx.Rollback() 100 + logger.Error("record bytes don't exist in blockstore", "error", err) 101 return helpers.ServerError(e, nil) 102 } 103 ··· 105 106 root, rev, err := r.Commit(context.TODO(), urepo.SignFor) 107 if err != nil { 108 + logger.Error("error committing", "error", err) 109 return helpers.ServerError(e, nil) 110 } 111 112 if err := s.UpdateRepo(context.TODO(), urepo.Repo.Did, root, rev); err != nil { 113 + logger.Error("error updating repo after commit", "error", err) 114 return helpers.ServerError(e, nil) 115 } 116
+34
server/handle_label_query_labels.go
···
··· 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 + }
+10 -5
server/handle_oauth_authorize.go
··· 13 ) 14 15 func (s *Server) handleOauthAuthorizeGet(e echo.Context) error { 16 reqUri := e.QueryParam("request_uri") 17 if reqUri == "" { 18 // render page for logged out dev ··· 38 } 39 40 var req provider.OauthAuthorizationRequest 41 - if err := s.db.Raw("SELECT * FROM oauth_authorization_requests WHERE request_id = ?", nil, reqId).Scan(&req).Error; err != nil { 42 return helpers.ServerError(e, to.StringPtr(err.Error())) 43 } 44 ··· 72 } 73 74 func (s *Server) handleOauthAuthorizePost(e echo.Context) error { 75 repo, _, err := s.getSessionRepoOrErr(e) 76 if err != nil { 77 return e.Redirect(303, "/account/signin") ··· 79 80 var req OauthAuthorizePostRequest 81 if err := e.Bind(&req); err != nil { 82 - s.logger.Error("error binding authorize post request", "error", err) 83 return helpers.InputError(e, nil) 84 } 85 ··· 89 } 90 91 var authReq provider.OauthAuthorizationRequest 92 - if err := s.db.Raw("SELECT * FROM oauth_authorization_requests WHERE request_id = ?", nil, reqId).Scan(&authReq).Error; err != nil { 93 return helpers.ServerError(e, to.StringPtr(err.Error())) 94 } 95 ··· 113 114 code := oauth.GenerateCode() 115 116 - if err := s.db.Exec("UPDATE oauth_authorization_requests SET sub = ?, code = ?, accepted = ?, ip = ? WHERE request_id = ?", nil, repo.Repo.Did, code, true, e.RealIP(), reqId).Error; err != nil { 117 - s.logger.Error("error updating authorization request", "error", err) 118 return helpers.ServerError(e, nil) 119 } 120
··· 13 ) 14 15 func (s *Server) handleOauthAuthorizeGet(e echo.Context) error { 16 + ctx := e.Request().Context() 17 + 18 reqUri := e.QueryParam("request_uri") 19 if reqUri == "" { 20 // render page for logged out dev ··· 40 } 41 42 var req provider.OauthAuthorizationRequest 43 + if err := s.db.Raw(ctx, "SELECT * FROM oauth_authorization_requests WHERE request_id = ?", nil, reqId).Scan(&req).Error; err != nil { 44 return helpers.ServerError(e, to.StringPtr(err.Error())) 45 } 46 ··· 74 } 75 76 func (s *Server) handleOauthAuthorizePost(e echo.Context) error { 77 + ctx := e.Request().Context() 78 + logger := s.logger.With("name", "handleOauthAuthorizePost") 79 + 80 repo, _, err := s.getSessionRepoOrErr(e) 81 if err != nil { 82 return e.Redirect(303, "/account/signin") ··· 84 85 var req OauthAuthorizePostRequest 86 if err := e.Bind(&req); err != nil { 87 + logger.Error("error binding authorize post request", "error", err) 88 return helpers.InputError(e, nil) 89 } 90 ··· 94 } 95 96 var authReq provider.OauthAuthorizationRequest 97 + if err := s.db.Raw(ctx, "SELECT * FROM oauth_authorization_requests WHERE request_id = ?", nil, reqId).Scan(&authReq).Error; err != nil { 98 return helpers.ServerError(e, to.StringPtr(err.Error())) 99 } 100 ··· 118 119 code := oauth.GenerateCode() 120 121 + if err := s.db.Exec(ctx, "UPDATE oauth_authorization_requests SET sub = ?, code = ?, accepted = ?, ip = ? WHERE request_id = ?", nil, repo.Repo.Did, code, true, e.RealIP(), reqId).Error; err != nil { 122 + logger.Error("error updating authorization request", "error", err) 123 return helpers.ServerError(e, nil) 124 } 125
+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", "client_id", parRequest.ClientID, "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
··· 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", "client_id", req.ClientID, "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
+24 -8
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 23 svcPts := strings.Split(svc, "#") 24 if len(svcPts) != 2 { ··· 44 } 45 46 func (s *Server) handleProxy(e echo.Context) error { 47 - lgr := s.logger.With("handler", "handleProxy") 48 49 repo, isAuthed := e.Get("repo").(*models.RepoActor) 50 ··· 55 56 endpoint, svcDid, err := s.getAtprotoProxyEndpointFromRequest(e) 57 if err != nil { 58 - lgr.Error("could not get atproto proxy", "error", err) 59 return helpers.ServerError(e, nil) 60 } 61 ··· 87 } 88 hj, err := json.Marshal(header) 89 if err != nil { 90 - lgr.Error("error marshaling header", "error", err) 91 return helpers.ServerError(e, nil) 92 } 93 94 encheader := strings.TrimRight(base64.RawURLEncoding.EncodeToString(hj), "=") 95 96 payload := map[string]any{ 97 "iss": repo.Repo.Did, 98 - "aud": svcDid, 99 - "lxm": pts[2], 100 "jti": uuid.NewString(), 101 "exp": time.Now().Add(1 * time.Minute).UTC().Unix(), 102 } 103 pj, err := json.Marshal(payload) 104 if err != nil { 105 - lgr.Error("error marashaling payload", "error", err) 106 return helpers.ServerError(e, nil) 107 } 108 ··· 113 114 sk, err := secp256k1secec.NewPrivateKey(repo.SigningKey) 115 if err != nil { 116 - lgr.Error("can't load private key", "error", err) 117 return err 118 } 119 120 R, S, _, err := sk.SignRaw(rand.Reader, hash[:]) 121 if err != nil { 122 - lgr.Error("error signing", "error", err) 123 } 124 125 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, "#") 27 if len(svcPts) != 2 { ··· 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
···
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
···
··· 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
··· 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 }
+3 -1
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
··· 21 22 // TODO: paginate this bitch 23 func (s *Server) handleListRepos(e echo.Context) error { 24 + ctx := e.Request().Context() 25 + 26 var repos []models.Repo 27 + if err := s.db.Raw(ctx, "SELECT * FROM repos ORDER BY created_at DESC LIMIT 500", nil).Scan(&repos).Error; err != nil { 28 return err 29 } 30
+10 -7
server/handle_repo_put_record.go
··· 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
··· 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
+6 -3
server/handle_server_activate_account.go
··· 18 } 19 20 func (s *Server) handleServerActivateAccount(e echo.Context) error { 21 var req ComAtprotoServerDeactivateAccountRequest 22 if err := e.Bind(&req); err != nil { 23 - s.logger.Error("error binding", "error", err) 24 return helpers.ServerError(e, nil) 25 } 26 27 urepo := e.Get("repo").(*models.RepoActor) 28 29 - if err := s.db.Exec("UPDATE repos SET deactivated = ? WHERE did = ?", nil, false, urepo.Repo.Did).Error; err != nil { 30 - s.logger.Error("error updating account status to deactivated", "error", err) 31 return helpers.ServerError(e, nil) 32 } 33
··· 18 } 19 20 func (s *Server) handleServerActivateAccount(e echo.Context) error { 21 + ctx := e.Request().Context() 22 + logger := s.logger.With("name", "handleServerActivateAccount") 23 + 24 var req ComAtprotoServerDeactivateAccountRequest 25 if err := e.Bind(&req); err != nil { 26 + logger.Error("error binding", "error", err) 27 return helpers.ServerError(e, nil) 28 } 29 30 urepo := e.Get("repo").(*models.RepoActor) 31 32 + if err := s.db.Exec(ctx, "UPDATE repos SET deactivated = ? WHERE did = ?", nil, false, urepo.Repo.Did).Error; err != nil { 33 + logger.Error("error updating account status to deactivated", "error", err) 34 return helpers.ServerError(e, nil) 35 } 36
+10 -7
server/handle_server_check_account_status.go
··· 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
··· 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
+108 -72
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" ··· 26 Handle string `json:"handle" validate:"required,atproto-handle"` 27 Did *string `json:"did" validate:"atproto-did"` 28 Password string `json:"password" validate:"required"` 29 - InviteCode string `json:"inviteCode" validate:"required"` 30 } 31 32 type ComAtprotoServerCreateAccountResponse struct { ··· 37 } 38 39 func (s *Server) handleCreateAccount(e echo.Context) error { 40 var request ComAtprotoServerCreateAccountRequest 41 42 - var signupDid string 43 - customDidHeader := e.Request().Header.Get("authorization") 44 - if customDidHeader != "" { 45 - pts := strings.Split(customDidHeader, " ") 46 - if len(pts) != 2 { 47 - return helpers.InputError(e, to.StringPtr("InvalidDid")) 48 - } 49 - 50 - _, err := syntax.ParseDID(pts[1]) 51 - if err != nil { 52 - return helpers.InputError(e, to.StringPtr("InvalidDid")) 53 - } 54 - 55 - signupDid = pts[1] 56 - } 57 - 58 if err := e.Bind(&request); err != nil { 59 - s.logger.Error("error receiving request", "endpoint", "com.atproto.server.createAccount", "error", err) 60 return helpers.ServerError(e, nil) 61 } 62 63 request.Handle = strings.ToLower(request.Handle) 64 65 if err := e.Validate(request); err != nil { 66 - s.logger.Error("error validating request", "endpoint", "com.atproto.server.createAccount", "error", err) 67 68 var verr ValidationError 69 if errors.As(err, &verr) { ··· 86 } 87 } 88 89 // see if the handle is already taken 90 - _, err := s.getActorByHandle(request.Handle) 91 if err != nil && err != gorm.ErrRecordNotFound { 92 - s.logger.Error("error looking up handle in db", "endpoint", "com.atproto.server.createAccount", "error", err) 93 return helpers.ServerError(e, nil) 94 } 95 - if err == nil { 96 return helpers.InputError(e, to.StringPtr("HandleNotAvailable")) 97 } 98 99 - if did, err := s.passport.ResolveHandle(e.Request().Context(), request.Handle); err == nil && did != "" { 100 return helpers.InputError(e, to.StringPtr("HandleNotAvailable")) 101 } 102 103 var ic models.InviteCode 104 - if err := s.db.Raw("SELECT * FROM invite_codes WHERE code = ?", nil, request.InviteCode).Scan(&ic).Error; err != nil { 105 - if err == gorm.ErrRecordNotFound { 106 return helpers.InputError(e, to.StringPtr("InvalidInviteCode")) 107 } 108 - s.logger.Error("error getting invite code from db", "error", err) 109 - return helpers.ServerError(e, nil) 110 - } 111 112 - if ic.RemainingUseCount < 1 { 113 - return helpers.InputError(e, to.StringPtr("InvalidInviteCode")) 114 } 115 116 // see if the email is already taken 117 - _, err = s.getRepoByEmail(request.Email) 118 if err != nil && err != gorm.ErrRecordNotFound { 119 - s.logger.Error("error looking up email in db", "endpoint", "com.atproto.server.createAccount", "error", err) 120 return helpers.ServerError(e, nil) 121 } 122 - if err == nil { 123 return helpers.InputError(e, to.StringPtr("EmailNotAvailable")) 124 } 125 126 // TODO: unsupported domains 127 128 - k, err := crypto.GeneratePrivateKeyK256() 129 - if err != nil { 130 - s.logger.Error("error creating signing key", "endpoint", "com.atproto.server.createAccount", "error", err) 131 - return helpers.ServerError(e, nil) 132 } 133 134 if signupDid == "" { 135 did, op, err := s.plcClient.CreateDID(k, "", request.Handle) 136 if err != nil { 137 - s.logger.Error("error creating operation", "endpoint", "com.atproto.server.createAccount", "error", err) 138 return helpers.ServerError(e, nil) 139 } 140 141 if err := s.plcClient.SendOperation(e.Request().Context(), did, op); err != nil { 142 - s.logger.Error("error sending plc op", "endpoint", "com.atproto.server.createAccount", "error", err) 143 return helpers.ServerError(e, nil) 144 } 145 signupDid = did ··· 147 148 hashed, err := bcrypt.GenerateFromPassword([]byte(request.Password), 10) 149 if err != nil { 150 - s.logger.Error("error hashing password", "error", err) 151 return helpers.ServerError(e, nil) 152 } 153 ··· 160 SigningKey: k.Bytes(), 161 } 162 163 - actor := models.Actor{ 164 - Did: signupDid, 165 - Handle: request.Handle, 166 - } 167 168 - if err := s.db.Create(&urepo, nil).Error; err != nil { 169 - s.logger.Error("error inserting new repo", "error", err) 170 - return helpers.ServerError(e, nil) 171 - } 172 173 - if err := s.db.Create(&actor, nil).Error; err != nil { 174 - s.logger.Error("error inserting new actor", "error", err) 175 - return helpers.ServerError(e, nil) 176 } 177 178 - if customDidHeader == "" { 179 bs := s.getBlockstore(signupDid) 180 r := repo.NewRepo(context.TODO(), signupDid, bs) 181 182 root, rev, err := r.Commit(context.TODO(), urepo.SignFor) 183 if err != nil { 184 - s.logger.Error("error committing", "error", err) 185 return helpers.ServerError(e, nil) 186 } 187 188 if err := s.UpdateRepo(context.TODO(), urepo.Did, root, rev); err != nil { 189 - s.logger.Error("error updating repo after commit", "error", err) 190 return helpers.ServerError(e, nil) 191 } 192 193 s.evtman.AddEvent(context.TODO(), &events.XRPCStreamEvent{ 194 - RepoHandle: &atproto.SyncSubscribeRepos_Handle{ 195 - Did: urepo.Did, 196 - Handle: request.Handle, 197 - Seq: time.Now().UnixMicro(), // TODO: no 198 - Time: time.Now().Format(util.ISO8601), 199 - }, 200 - }) 201 - 202 - s.evtman.AddEvent(context.TODO(), &events.XRPCStreamEvent{ 203 RepoIdentity: &atproto.SyncSubscribeRepos_Identity{ 204 Did: urepo.Did, 205 Handle: to.StringPtr(request.Handle), ··· 209 }) 210 } 211 212 - if err := s.db.Raw("UPDATE invite_codes SET remaining_use_count = remaining_use_count - 1 WHERE code = ?", nil, request.InviteCode).Scan(&ic).Error; err != nil { 213 - s.logger.Error("error decrementing use count", "error", err) 214 - return helpers.ServerError(e, nil) 215 } 216 217 - sess, err := s.createSession(&urepo) 218 if err != nil { 219 - s.logger.Error("error creating new session", "error", err) 220 return helpers.ServerError(e, nil) 221 } 222 223 go func() { 224 if err := s.sendEmailVerification(urepo.Email, actor.Handle, *urepo.EmailVerificationCode); err != nil { 225 - s.logger.Error("error sending email verification email", "error", err) 226 } 227 if err := s.sendWelcomeMail(urepo.Email, actor.Handle); err != nil { 228 - s.logger.Error("error sending welcome email", "error", err) 229 } 230 }() 231
··· 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" ··· 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
··· 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
··· 22 } 23 24 func (s *Server) handleCreateInviteCodes(e echo.Context) error { 25 var req ComAtprotoServerCreateInviteCodesRequest 26 if err := e.Bind(&req); err != nil { 27 - s.logger.Error("error binding", "error", err) 28 return helpers.ServerError(e, nil) 29 } 30 31 if err := e.Validate(req); err != nil { 32 - s.logger.Error("error validating", "error", err) 33 return helpers.InputError(e, nil) 34 } 35 ··· 50 ic := uuid.NewString() 51 ics = append(ics, ic) 52 53 - if err := s.db.Create(&models.InviteCode{ 54 Code: ic, 55 Did: did, 56 RemainingUseCount: req.UseCount, 57 }, nil).Error; err != nil { 58 - s.logger.Error("error creating invite code", "error", err) 59 return helpers.ServerError(e, nil) 60 } 61 }
··· 22 } 23 24 func (s *Server) handleCreateInviteCodes(e echo.Context) error { 25 + ctx := e.Request().Context() 26 + logger := s.logger.With("name", "handleServerCreateInviteCodes") 27 + 28 var req ComAtprotoServerCreateInviteCodesRequest 29 if err := e.Bind(&req); err != nil { 30 + logger.Error("error binding", "error", err) 31 return helpers.ServerError(e, nil) 32 } 33 34 if err := e.Validate(req); err != nil { 35 + logger.Error("error validating", "error", err) 36 return helpers.InputError(e, nil) 37 } 38 ··· 53 ic := uuid.NewString() 54 ics = append(ics, ic) 55 56 + if err := s.db.Create(ctx, &models.InviteCode{ 57 Code: ic, 58 Did: did, 59 RemainingUseCount: req.UseCount, 60 }, nil).Error; err != nil { 61 + logger.Error("error creating invite code", "error", err) 62 return helpers.ServerError(e, nil) 63 } 64 }
+65 -9
server/handle_server_create_session.go
··· 1 package server 2 3 import ( 4 "errors" 5 "strings" 6 7 "github.com/Azure/go-autorest/autorest/to" 8 "github.com/bluesky-social/indigo/atproto/syntax" ··· 32 } 33 34 func (s *Server) handleCreateSession(e echo.Context) error { 35 var req ComAtprotoServerCreateSessionRequest 36 if err := e.Bind(&req); err != nil { 37 - s.logger.Error("error binding request", "endpoint", "com.atproto.server.serverCreateSession", "error", err) 38 return helpers.ServerError(e, nil) 39 } 40 ··· 65 var err error 66 switch idtype { 67 case "did": 68 - err = s.db.Raw("SELECT r.*, a.* FROM repos r LEFT JOIN actors a ON r.did = a.did WHERE r.did = ?", nil, req.Identifier).Scan(&repo).Error 69 case "handle": 70 - err = s.db.Raw("SELECT r.*, a.* FROM actors a LEFT JOIN repos r ON a.did = r.did WHERE a.handle = ?", nil, req.Identifier).Scan(&repo).Error 71 case "email": 72 - err = s.db.Raw("SELECT r.*, a.* FROM repos r LEFT JOIN actors a ON r.did = a.did WHERE r.email = ?", nil, req.Identifier).Scan(&repo).Error 73 } 74 75 if err != nil { ··· 77 return helpers.InputError(e, to.StringPtr("InvalidRequest")) 78 } 79 80 - s.logger.Error("erorr looking up repo", "endpoint", "com.atproto.server.createSession", "error", err) 81 return helpers.ServerError(e, nil) 82 } 83 84 if err := bcrypt.CompareHashAndPassword([]byte(repo.Password), []byte(req.Password)); err != nil { 85 if err != bcrypt.ErrMismatchedHashAndPassword { 86 - s.logger.Error("erorr comparing hash and password", "error", err) 87 } 88 return helpers.InputError(e, to.StringPtr("InvalidRequest")) 89 } 90 91 - sess, err := s.createSession(&repo.Repo) 92 if err != nil { 93 - s.logger.Error("error creating session", "error", err) 94 return helpers.ServerError(e, nil) 95 } 96 ··· 101 Did: repo.Repo.Did, 102 Email: repo.Email, 103 EmailConfirmed: repo.EmailConfirmedAt != nil, 104 - EmailAuthFactor: false, 105 Active: repo.Active(), 106 Status: repo.Status(), 107 }) 108 }
··· 1 package server 2 3 import ( 4 + "context" 5 "errors" 6 + "fmt" 7 "strings" 8 + "time" 9 10 "github.com/Azure/go-autorest/autorest/to" 11 "github.com/bluesky-social/indigo/atproto/syntax" ··· 35 } 36 37 func (s *Server) handleCreateSession(e echo.Context) error { 38 + ctx := e.Request().Context() 39 + logger := s.logger.With("name", "handleServerCreateSession") 40 + 41 var req ComAtprotoServerCreateSessionRequest 42 if err := e.Bind(&req); err != nil { 43 + logger.Error("error binding request", "endpoint", "com.atproto.server.serverCreateSession", "error", err) 44 return helpers.ServerError(e, nil) 45 } 46 ··· 71 var err error 72 switch idtype { 73 case "did": 74 + err = s.db.Raw(ctx, "SELECT r.*, a.* FROM repos r LEFT JOIN actors a ON r.did = a.did WHERE r.did = ?", nil, req.Identifier).Scan(&repo).Error 75 case "handle": 76 + err = s.db.Raw(ctx, "SELECT r.*, a.* FROM actors a LEFT JOIN repos r ON a.did = r.did WHERE a.handle = ?", nil, req.Identifier).Scan(&repo).Error 77 case "email": 78 + err = s.db.Raw(ctx, "SELECT r.*, a.* FROM repos r LEFT JOIN actors a ON r.did = a.did WHERE r.email = ?", nil, req.Identifier).Scan(&repo).Error 79 } 80 81 if err != nil { ··· 83 return helpers.InputError(e, to.StringPtr("InvalidRequest")) 84 } 85 86 + logger.Error("erorr looking up repo", "endpoint", "com.atproto.server.createSession", "error", err) 87 return helpers.ServerError(e, nil) 88 } 89 90 if err := bcrypt.CompareHashAndPassword([]byte(repo.Password), []byte(req.Password)); err != nil { 91 if err != bcrypt.ErrMismatchedHashAndPassword { 92 + logger.Error("erorr comparing hash and password", "error", err) 93 } 94 return helpers.InputError(e, to.StringPtr("InvalidRequest")) 95 } 96 97 + // if repo requires 2FA token and one hasn't been provided, return error prompting for one 98 + if repo.TwoFactorType != models.TwoFactorTypeNone && (req.AuthFactorToken == nil || *req.AuthFactorToken == "") { 99 + err = s.createAndSendTwoFactorCode(ctx, repo) 100 + if err != nil { 101 + logger.Error("sending 2FA code", "error", err) 102 + return helpers.ServerError(e, nil) 103 + } 104 + 105 + return helpers.InputError(e, to.StringPtr("AuthFactorTokenRequired")) 106 + } 107 + 108 + // if 2FA is required, now check that the one provided is valid 109 + if repo.TwoFactorType != models.TwoFactorTypeNone { 110 + if repo.TwoFactorCode == nil || repo.TwoFactorCodeExpiresAt == nil { 111 + err = s.createAndSendTwoFactorCode(ctx, repo) 112 + if err != nil { 113 + logger.Error("sending 2FA code", "error", err) 114 + return helpers.ServerError(e, nil) 115 + } 116 + 117 + return helpers.InputError(e, to.StringPtr("AuthFactorTokenRequired")) 118 + } 119 + 120 + if *repo.TwoFactorCode != *req.AuthFactorToken { 121 + return helpers.InvalidTokenError(e) 122 + } 123 + 124 + if time.Now().UTC().After(*repo.TwoFactorCodeExpiresAt) { 125 + return helpers.ExpiredTokenError(e) 126 + } 127 + } 128 + 129 + sess, err := s.createSession(ctx, &repo.Repo) 130 if err != nil { 131 + logger.Error("error creating session", "error", err) 132 return helpers.ServerError(e, nil) 133 } 134 ··· 139 Did: repo.Repo.Did, 140 Email: repo.Email, 141 EmailConfirmed: repo.EmailConfirmedAt != nil, 142 + EmailAuthFactor: repo.TwoFactorType != models.TwoFactorTypeNone, 143 Active: repo.Active(), 144 Status: repo.Status(), 145 }) 146 } 147 + 148 + func (s *Server) createAndSendTwoFactorCode(ctx context.Context, repo models.RepoActor) error { 149 + // TODO: when implementing a new type of 2FA there should be some logic in here to send the 150 + // right type of code 151 + 152 + code := fmt.Sprintf("%s-%s", helpers.RandomVarchar(5), helpers.RandomVarchar(5)) 153 + eat := time.Now().Add(10 * time.Minute).UTC() 154 + 155 + if err := s.db.Exec(ctx, "UPDATE repos SET two_factor_code = ?, two_factor_code_expires_at = ? WHERE did = ?", nil, code, eat, repo.Repo.Did).Error; err != nil { 156 + return fmt.Errorf("updating repo: %w", err) 157 + } 158 + 159 + if err := s.sendTwoFactorCode(repo.Email, repo.Handle, code); err != nil { 160 + return fmt.Errorf("sending email: %w", err) 161 + } 162 + 163 + return nil 164 + }
+6 -3
server/handle_server_deactivate_account.go
··· 19 } 20 21 func (s *Server) handleServerDeactivateAccount(e echo.Context) error { 22 var req ComAtprotoServerDeactivateAccountRequest 23 if err := e.Bind(&req); err != nil { 24 - s.logger.Error("error binding", "error", err) 25 return helpers.ServerError(e, nil) 26 } 27 28 urepo := e.Get("repo").(*models.RepoActor) 29 30 - if err := s.db.Exec("UPDATE repos SET deactivated = ? WHERE did = ?", nil, true, urepo.Repo.Did).Error; err != nil { 31 - s.logger.Error("error updating account status to deactivated", "error", err) 32 return helpers.ServerError(e, nil) 33 } 34
··· 19 } 20 21 func (s *Server) handleServerDeactivateAccount(e echo.Context) error { 22 + ctx := e.Request().Context() 23 + logger := s.logger.With("name", "handleServerDeactivateAccount") 24 + 25 var req ComAtprotoServerDeactivateAccountRequest 26 if err := e.Bind(&req); err != nil { 27 + logger.Error("error binding", "error", err) 28 return helpers.ServerError(e, nil) 29 } 30 31 urepo := e.Get("repo").(*models.RepoActor) 32 33 + if err := s.db.Exec(ctx, "UPDATE repos SET deactivated = ? WHERE did = ?", nil, true, urepo.Repo.Did).Error; err != nil { 34 + logger.Error("error updating account status to deactivated", "error", err) 35 return helpers.ServerError(e, nil) 36 } 37
+150
server/handle_server_delete_account.go
···
··· 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
··· 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
··· 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
··· 21 Aud string `query:"aud" validate:"required,atproto-did"` 22 // exp should be a float, as some clients will send a non-integer expiration 23 Exp float64 `query:"exp"` 24 - Lxm string `query:"lxm" validate:"required,atproto-nsid"` 25 } 26 27 func (s *Server) handleServerGetServiceAuth(e echo.Context) error { 28 var req ServerGetServiceAuthRequest 29 if err := e.Bind(&req); err != nil { 30 - s.logger.Error("could not bind service auth request", "error", err) 31 return helpers.ServerError(e, nil) 32 } 33 ··· 45 return helpers.InputError(e, to.StringPtr("may not generate auth tokens recursively")) 46 } 47 48 - maxExp := now + (60 * 30) 49 if exp > maxExp { 50 return helpers.InputError(e, to.StringPtr("expiration too big. smoller please")) 51 } ··· 59 } 60 hj, err := json.Marshal(header) 61 if err != nil { 62 - s.logger.Error("error marshaling header", "error", err) 63 return helpers.ServerError(e, nil) 64 } 65 ··· 68 payload := map[string]any{ 69 "iss": repo.Repo.Did, 70 "aud": req.Aud, 71 - "lxm": req.Lxm, 72 "jti": uuid.NewString(), 73 "exp": exp, 74 "iat": now, 75 } 76 pj, err := json.Marshal(payload) 77 if err != nil { 78 - s.logger.Error("error marashaling payload", "error", err) 79 return helpers.ServerError(e, nil) 80 } 81 ··· 86 87 sk, err := secp256k1secec.NewPrivateKey(repo.SigningKey) 88 if err != nil { 89 - s.logger.Error("can't load private key", "error", err) 90 return err 91 } 92 93 R, S, _, err := sk.SignRaw(rand.Reader, hash[:]) 94 if err != nil { 95 - s.logger.Error("error signing", "error", err) 96 return helpers.ServerError(e, nil) 97 } 98
··· 21 Aud string `query:"aud" validate:"required,atproto-did"` 22 // exp should be a float, as some clients will send a non-integer expiration 23 Exp float64 `query:"exp"` 24 + Lxm string `query:"lxm"` 25 } 26 27 func (s *Server) handleServerGetServiceAuth(e echo.Context) error { 28 + logger := s.logger.With("name", "handleServerGetServiceAuth") 29 + 30 var req ServerGetServiceAuthRequest 31 if err := e.Bind(&req); err != nil { 32 + logger.Error("could not bind service auth request", "error", err) 33 return helpers.ServerError(e, nil) 34 } 35 ··· 47 return helpers.InputError(e, to.StringPtr("may not generate auth tokens recursively")) 48 } 49 50 + var maxExp int64 51 + if req.Lxm != "" { 52 + maxExp = now + (60 * 60) 53 + } else { 54 + maxExp = now + 60 55 + } 56 if exp > maxExp { 57 return helpers.InputError(e, to.StringPtr("expiration too big. smoller please")) 58 } ··· 66 } 67 hj, err := json.Marshal(header) 68 if err != nil { 69 + logger.Error("error marshaling header", "error", err) 70 return helpers.ServerError(e, nil) 71 } 72 ··· 75 payload := map[string]any{ 76 "iss": repo.Repo.Did, 77 "aud": req.Aud, 78 "jti": uuid.NewString(), 79 "exp": exp, 80 "iat": now, 81 + } 82 + if req.Lxm != "" { 83 + payload["lxm"] = req.Lxm 84 } 85 pj, err := json.Marshal(payload) 86 if err != nil { 87 + logger.Error("error marashaling payload", "error", err) 88 return helpers.ServerError(e, nil) 89 } 90 ··· 95 96 sk, err := secp256k1secec.NewPrivateKey(repo.SigningKey) 97 if err != nil { 98 + logger.Error("can't load private key", "error", err) 99 return err 100 } 101 102 R, S, _, err := sk.SignRaw(rand.Reader, hash[:]) 103 if err != nil { 104 + logger.Error("error signing", "error", err) 105 return helpers.ServerError(e, nil) 106 } 107
+1 -1
server/handle_server_get_session.go
··· 23 Did: repo.Repo.Did, 24 Email: repo.Email, 25 EmailConfirmed: repo.EmailConfirmedAt != nil, 26 - EmailAuthFactor: false, // TODO: todo todo 27 Active: repo.Active(), 28 Status: repo.Status(), 29 })
··· 23 Did: repo.Repo.Did, 24 Email: repo.Email, 25 EmailConfirmed: repo.EmailConfirmedAt != nil, 26 + EmailAuthFactor: repo.TwoFactorType != models.TwoFactorTypeNone, 27 Active: repo.Active(), 28 Status: repo.Status(), 29 })
+9 -6
server/handle_server_refresh_session.go
··· 16 } 17 18 func (s *Server) handleRefreshSession(e echo.Context) error { 19 token := e.Get("token").(string) 20 repo := e.Get("repo").(*models.RepoActor) 21 22 - if err := s.db.Exec("DELETE FROM refresh_tokens WHERE token = ?", nil, token).Error; err != nil { 23 - s.logger.Error("error getting refresh token from db", "error", err) 24 return helpers.ServerError(e, nil) 25 } 26 27 - if err := s.db.Exec("DELETE FROM tokens WHERE refresh_token = ?", nil, token).Error; err != nil { 28 - s.logger.Error("error deleting access token from db", "error", err) 29 return helpers.ServerError(e, nil) 30 } 31 32 - sess, err := s.createSession(&repo.Repo) 33 if err != nil { 34 - s.logger.Error("error creating new session for refresh", "error", err) 35 return helpers.ServerError(e, nil) 36 } 37
··· 16 } 17 18 func (s *Server) handleRefreshSession(e echo.Context) error { 19 + ctx := e.Request().Context() 20 + logger := s.logger.With("name", "handleServerRefreshSession") 21 + 22 token := e.Get("token").(string) 23 repo := e.Get("repo").(*models.RepoActor) 24 25 + if err := s.db.Exec(ctx, "DELETE FROM refresh_tokens WHERE token = ?", nil, token).Error; err != nil { 26 + logger.Error("error getting refresh token from db", "error", err) 27 return helpers.ServerError(e, nil) 28 } 29 30 + if err := s.db.Exec(ctx, "DELETE FROM tokens WHERE refresh_token = ?", nil, token).Error; err != nil { 31 + logger.Error("error deleting access token from db", "error", err) 32 return helpers.ServerError(e, nil) 33 } 34 35 + sess, err := s.createSession(ctx, &repo.Repo) 36 if err != nil { 37 + logger.Error("error creating new session for refresh", "error", err) 38 return helpers.ServerError(e, nil) 39 } 40
+52
server/handle_server_request_account_delete.go
···
··· 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
··· 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
··· 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
··· 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
···
··· 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
··· 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
··· 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
··· 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
··· 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())
+3 -2
server/handle_sync_get_blocks.go
··· 18 19 func (s *Server) handleGetBlocks(e echo.Context) error { 20 ctx := e.Request().Context() 21 22 var req ComAtprotoSyncGetBlocksRequest 23 if err := e.Bind(&req); err != nil { ··· 35 cids = append(cids, c) 36 } 37 38 - urepo, err := s.getRepoActorByDid(req.Did) 39 if err != nil { 40 return helpers.ServerError(e, nil) 41 } ··· 52 }) 53 54 if _, err := carstore.LdWrite(buf, hb); err != nil { 55 - s.logger.Error("error writing to car", "error", err) 56 return helpers.ServerError(e, nil) 57 } 58
··· 18 19 func (s *Server) handleGetBlocks(e echo.Context) error { 20 ctx := e.Request().Context() 21 + logger := s.logger.With("name", "handleSyncGetBlocks") 22 23 var req ComAtprotoSyncGetBlocksRequest 24 if err := e.Bind(&req); err != nil { ··· 36 cids = append(cids, c) 37 } 38 39 + urepo, err := s.getRepoActorByDid(ctx, req.Did) 40 if err != nil { 41 return helpers.ServerError(e, nil) 42 } ··· 53 }) 54 55 if _, err := carstore.LdWrite(buf, hb); err != nil { 56 + logger.Error("error writing to car", "error", err) 57 return helpers.ServerError(e, nil) 58 } 59
+3 -1
server/handle_sync_get_latest_commit.go
··· 12 } 13 14 func (s *Server) handleSyncGetLatestCommit(e echo.Context) error { 15 did := e.QueryParam("did") 16 if did == "" { 17 return helpers.InputError(e, nil) 18 } 19 20 - urepo, err := s.getRepoActorByDid(did) 21 if err != nil { 22 return err 23 }
··· 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
··· 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
··· 13 ) 14 15 func (s *Server) handleSyncGetRepo(e echo.Context) error { 16 did := e.QueryParam("did") 17 if did == "" { 18 return helpers.InputError(e, nil) 19 } 20 21 - urepo, err := s.getRepoActorByDid(did) 22 if err != nil { 23 return err 24 } ··· 36 buf := new(bytes.Buffer) 37 38 if _, err := carstore.LdWrite(buf, hb); err != nil { 39 - s.logger.Error("error writing to car", "error", err) 40 return helpers.ServerError(e, nil) 41 } 42 43 var blocks []models.Block 44 - if err := s.db.Raw("SELECT * FROM blocks WHERE did = ? ORDER BY rev ASC", nil, urepo.Repo.Did).Scan(&blocks).Error; err != nil { 45 return err 46 } 47
··· 13 ) 14 15 func (s *Server) handleSyncGetRepo(e echo.Context) error { 16 + ctx := e.Request().Context() 17 + logger := s.logger.With("name", "handleSyncGetRepo") 18 + 19 did := e.QueryParam("did") 20 if did == "" { 21 return helpers.InputError(e, nil) 22 } 23 24 + urepo, err := s.getRepoActorByDid(ctx, did) 25 if err != nil { 26 return err 27 } ··· 39 buf := new(bytes.Buffer) 40 41 if _, err := carstore.LdWrite(buf, hb); err != nil { 42 + logger.Error("error writing to car", "error", err) 43 return helpers.ServerError(e, nil) 44 } 45 46 var blocks []models.Block 47 + if err := s.db.Raw(ctx, "SELECT * FROM blocks WHERE did = ? ORDER BY rev ASC", nil, urepo.Repo.Did).Scan(&blocks).Error; err != nil { 48 return err 49 } 50
+3 -1
server/handle_sync_get_repo_status.go
··· 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 }
··· 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 }
+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
··· 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
··· 2 3 import ( 4 "fmt" 5 6 "github.com/Azure/go-autorest/autorest/to" 7 "github.com/labstack/echo/v4" 8 ) 9 10 var ( ··· 61 }, 62 }, 63 }) 64 } 65 66 func (s *Server) handleOauthProtectedResource(e echo.Context) error {
··· 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
··· 40 return nil 41 } 42 43 func (s *Server) sendEmailUpdate(email, handle, code string) error { 44 if s.mail == nil { 45 return nil ··· 77 78 return nil 79 }
··· 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
··· 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
+90 -37
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/haileyok/cocoon/internal/db" 20 "github.com/haileyok/cocoon/models" 21 "github.com/haileyok/cocoon/recording_blockstore" 22 blocks "github.com/ipfs/go-block-format" ··· 72 } 73 74 func (mm *MarshalableMap) MarshalCBOR(w io.Writer) error { 75 - data, err := data.MarshalCBOR(*mm) 76 if err != nil { 77 return err 78 } ··· 96 } 97 98 // TODO make use of swap commit 99 - func (rm *RepoMan) applyWrites(urepo models.Repo, writes []Op, swapCommit *string) ([]ApplyWriteResult, error) { 100 rootcid, err := cid.Cast(urepo.Root) 101 if err != nil { 102 return nil, err ··· 104 105 dbs := rm.s.getBlockstore(urepo.Did) 106 bs := recording_blockstore.New(dbs) 107 - r, err := repo.OpenRepo(context.TODO(), bs, rootcid) 108 109 - entries := []models.Record{} 110 var results []ApplyWriteResult 111 112 for i, op := range writes { 113 if op.Type != OpTypeCreate && op.Rkey == nil { 114 return nil, fmt.Errorf("invalid rkey") 115 } else if op.Type == OpTypeCreate && op.Rkey != nil { 116 - _, _, err := r.GetRecord(context.TODO(), op.Collection+"/"+*op.Rkey) 117 if err == nil { 118 op.Type = OpTypeUpdate 119 } 120 } else if op.Rkey == nil { 121 op.Rkey = to.StringPtr(rm.clock.Next().String()) 122 writes[i].Rkey = op.Rkey 123 } 124 125 _, err := syntax.ParseRecordKey(*op.Rkey) 126 if err != nil { 127 return nil, err ··· 129 130 switch op.Type { 131 case OpTypeCreate: 132 - j, err := json.Marshal(*op.Record) 133 if err != nil { 134 return nil, err 135 } 136 - out, err := data.UnmarshalJSON(j) 137 if err != nil { 138 return nil, err 139 } 140 mm := MarshalableMap(out) 141 142 // HACK: if a record doesn't contain a $type, we can manually set it here based on the op's collection 143 if mm["$type"] == "" { 144 mm["$type"] = op.Collection 145 } 146 147 - nc, err := r.PutRecord(context.TODO(), op.Collection+"/"+*op.Rkey, &mm) 148 if err != nil { 149 return nil, err 150 } 151 - d, err := data.MarshalCBOR(mm) 152 if err != nil { 153 return nil, err 154 } 155 entries = append(entries, models.Record{ 156 Did: urepo.Did, 157 CreatedAt: rm.clock.Next().String(), ··· 160 Cid: nc.String(), 161 Value: d, 162 }) 163 results = append(results, ApplyWriteResult{ 164 Type: to.StringPtr(OpTypeCreate.String()), 165 Uri: to.StringPtr("at://" + urepo.Did + "/" + op.Collection + "/" + *op.Rkey), ··· 167 ValidationStatus: to.StringPtr("valid"), // TODO: obviously this might not be true atm lol 168 }) 169 case OpTypeDelete: 170 var old models.Record 171 - if err := rm.db.Raw("SELECT value FROM records WHERE did = ? AND nsid = ? AND rkey = ?", nil, urepo.Did, op.Collection, op.Rkey).Scan(&old).Error; err != nil { 172 return nil, err 173 } 174 entries = append(entries, models.Record{ 175 Did: urepo.Did, 176 Nsid: op.Collection, 177 Rkey: *op.Rkey, 178 Value: old.Value, 179 }) 180 - err := r.DeleteRecord(context.TODO(), op.Collection+"/"+*op.Rkey) 181 if err != nil { 182 return nil, err 183 } 184 results = append(results, ApplyWriteResult{ 185 Type: to.StringPtr(OpTypeDelete.String()), 186 }) 187 case OpTypeUpdate: 188 - j, err := json.Marshal(*op.Record) 189 if err != nil { 190 return nil, err 191 } 192 - out, err := data.UnmarshalJSON(j) 193 if err != nil { 194 return nil, err 195 } 196 mm := MarshalableMap(out) 197 - nc, err := r.UpdateRecord(context.TODO(), op.Collection+"/"+*op.Rkey, &mm) 198 if err != nil { 199 return nil, err 200 } 201 - d, err := data.MarshalCBOR(mm) 202 if err != nil { 203 return nil, err 204 } 205 entries = append(entries, models.Record{ 206 Did: urepo.Did, 207 CreatedAt: rm.clock.Next().String(), ··· 210 Cid: nc.String(), 211 Value: d, 212 }) 213 results = append(results, ApplyWriteResult{ 214 Type: to.StringPtr(OpTypeUpdate.String()), 215 Uri: to.StringPtr("at://" + urepo.Did + "/" + op.Collection + "/" + *op.Rkey), ··· 219 } 220 } 221 222 - newroot, rev, err := r.Commit(context.TODO(), urepo.SignFor) 223 if err != nil { 224 return nil, err 225 } 226 227 buf := new(bytes.Buffer) 228 229 hb, err := cbor.DumpObject(&car.CarHeader{ 230 Roots: []cid.Cid{newroot}, 231 Version: 1, 232 }) 233 - 234 if _, err := carstore.LdWrite(buf, hb); err != nil { 235 return nil, err 236 } 237 238 - diffops, err := r.DiffSince(context.TODO(), rootcid) 239 if err != nil { 240 return nil, err 241 } 242 243 ops := make([]*atproto.SyncSubscribeRepos_RepoOp, 0, len(diffops)) 244 - 245 for _, op := range diffops { 246 var c cid.Cid 247 switch op.Op { ··· 270 }) 271 } 272 273 - blk, err := dbs.Get(context.TODO(), c) 274 if err != nil { 275 return nil, err 276 } 277 278 if _, err := carstore.LdWrite(buf, blk.Cid().Bytes(), blk.RawData()); err != nil { 279 return nil, err 280 } 281 } 282 283 for _, op := range bs.GetWriteLog() { 284 if _, err := carstore.LdWrite(buf, op.Cid().Bytes(), op.RawData()); err != nil { 285 return nil, err 286 } 287 } 288 289 var blobs []lexutil.LexLink 290 for _, entry := range entries { 291 var cids []cid.Cid 292 if entry.Cid != "" { 293 - if err := rm.s.db.Create(&entry, []clause.Expression{clause.OnConflict{ 294 Columns: []clause.Column{{Name: "did"}, {Name: "nsid"}, {Name: "rkey"}}, 295 UpdateAll: true, 296 }}).Error; err != nil { 297 return nil, err 298 } 299 300 - cids, err = rm.incrementBlobRefs(urepo, entry.Value) 301 if err != nil { 302 return nil, err 303 } 304 } else { 305 - if err := rm.s.db.Delete(&entry, nil).Error; err != nil { 306 return nil, err 307 } 308 - cids, err = rm.decrementBlobRefs(urepo, entry.Value) 309 if err != nil { 310 return nil, err 311 } 312 } 313 314 for _, c := range cids { 315 blobs = append(blobs, lexutil.LexLink(c)) 316 } 317 } 318 319 - rm.s.evtman.AddEvent(context.TODO(), &events.XRPCStreamEvent{ 320 RepoCommit: &atproto.SyncSubscribeRepos_Commit{ 321 Repo: urepo.Did, 322 Blocks: buf.Bytes(), ··· 330 }, 331 }) 332 333 - if err := rm.s.UpdateRepo(context.TODO(), urepo.Did, newroot, rev); err != nil { 334 return nil, err 335 } 336 ··· 345 return results, nil 346 } 347 348 - func (rm *RepoMan) getRecordProof(urepo models.Repo, collection, rkey string) (cid.Cid, []blocks.Block, error) { 349 c, err := cid.Cast(urepo.Root) 350 if err != nil { 351 return cid.Undef, nil, err ··· 354 dbs := rm.s.getBlockstore(urepo.Did) 355 bs := recording_blockstore.New(dbs) 356 357 - r, err := repo.OpenRepo(context.TODO(), bs, c) 358 if err != nil { 359 return cid.Undef, nil, err 360 } 361 362 - _, _, err = r.GetRecordBytes(context.TODO(), collection+"/"+rkey) 363 if err != nil { 364 return cid.Undef, nil, err 365 } ··· 367 return c, bs.GetReadLog(), nil 368 } 369 370 - func (rm *RepoMan) incrementBlobRefs(urepo models.Repo, cbor []byte) ([]cid.Cid, error) { 371 cids, err := getBlobCidsFromCbor(cbor) 372 if err != nil { 373 return nil, err 374 } 375 376 for _, c := range cids { 377 - if err := rm.db.Exec("UPDATE blobs SET ref_count = ref_count + 1 WHERE did = ? AND cid = ?", nil, urepo.Did, c.Bytes()).Error; err != nil { 378 return nil, err 379 } 380 } ··· 382 return cids, nil 383 } 384 385 - func (rm *RepoMan) decrementBlobRefs(urepo models.Repo, cbor []byte) ([]cid.Cid, error) { 386 cids, err := getBlobCidsFromCbor(cbor) 387 if err != nil { 388 return nil, err ··· 393 ID uint 394 Count int 395 } 396 - if err := rm.db.Raw("UPDATE blobs SET ref_count = ref_count - 1 WHERE did = ? AND cid = ? RETURNING id, ref_count", nil, urepo.Did, c.Bytes()).Scan(&res).Error; err != nil { 397 return nil, err 398 } 399 400 if res.Count == 0 { 401 - if err := rm.db.Exec("DELETE FROM blobs WHERE id = ?", nil, res.ID).Error; err != nil { 402 return nil, err 403 } 404 - if err := rm.db.Exec("DELETE FROM blob_parts WHERE blob_id = ?", nil, res.ID).Error; err != nil { 405 return nil, err 406 } 407 } ··· 415 func getBlobCidsFromCbor(cbor []byte) ([]cid.Cid, error) { 416 var cids []cid.Cid 417 418 - decoded, err := data.UnmarshalCBOR(cbor) 419 if err != nil { 420 return nil, fmt.Errorf("error unmarshaling cbor: %w", err) 421 }
··· 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" ··· 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 ··· 105 106 dbs := rm.s.getBlockstore(urepo.Did) 107 bs := recording_blockstore.New(dbs) 108 + r, err := repo.OpenRepo(ctx, bs, rootcid) 109 110 var results []ApplyWriteResult 111 112 + entries := make([]models.Record, 0, len(writes)) 113 for i, op := range writes { 114 + // updates or deletes must supply an rkey 115 if op.Type != OpTypeCreate && op.Rkey == nil { 116 return nil, fmt.Errorf("invalid rkey") 117 } else if op.Type == OpTypeCreate && op.Rkey != nil { 118 + // we should conver this op to an update if the rkey already exists 119 + _, _, err := r.GetRecord(ctx, fmt.Sprintf("%s/%s", op.Collection, *op.Rkey)) 120 if err == nil { 121 op.Type = OpTypeUpdate 122 } 123 } else if op.Rkey == nil { 124 + // creates that don't supply an rkey will have one generated for them 125 op.Rkey = to.StringPtr(rm.clock.Next().String()) 126 writes[i].Rkey = op.Rkey 127 } 128 129 + // validate the record key is actually valid 130 _, err := syntax.ParseRecordKey(*op.Rkey) 131 if err != nil { 132 return nil, err ··· 134 135 switch op.Type { 136 case OpTypeCreate: 137 + // HACK: this fixes some type conversions, mainly around integers 138 + // first we convert to json bytes 139 + b, err := json.Marshal(*op.Record) 140 if err != nil { 141 return nil, err 142 } 143 + // then we use atdata.UnmarshalJSON to convert it back to a map 144 + out, err := atdata.UnmarshalJSON(b) 145 if err != nil { 146 return nil, err 147 } 148 + // finally we can cast to a MarshalableMap 149 mm := MarshalableMap(out) 150 151 // HACK: if a record doesn't contain a $type, we can manually set it here based on the op's collection 152 + // i forget why this is actually necessary? 153 if mm["$type"] == "" { 154 mm["$type"] = op.Collection 155 } 156 157 + nc, err := r.PutRecord(ctx, fmt.Sprintf("%s/%s", op.Collection, *op.Rkey), &mm) 158 if err != nil { 159 return nil, err 160 } 161 + 162 + d, err := atdata.MarshalCBOR(mm) 163 if err != nil { 164 return nil, err 165 } 166 + 167 entries = append(entries, models.Record{ 168 Did: urepo.Did, 169 CreatedAt: rm.clock.Next().String(), ··· 172 Cid: nc.String(), 173 Value: d, 174 }) 175 + 176 results = append(results, ApplyWriteResult{ 177 Type: to.StringPtr(OpTypeCreate.String()), 178 Uri: to.StringPtr("at://" + urepo.Did + "/" + op.Collection + "/" + *op.Rkey), ··· 180 ValidationStatus: to.StringPtr("valid"), // TODO: obviously this might not be true atm lol 181 }) 182 case OpTypeDelete: 183 + // try to find the old record in the database 184 var old models.Record 185 + if err := rm.db.Raw(ctx, "SELECT value FROM records WHERE did = ? AND nsid = ? AND rkey = ?", nil, urepo.Did, op.Collection, op.Rkey).Scan(&old).Error; err != nil { 186 return nil, err 187 } 188 + 189 + // TODO: this is really confusing, and looking at it i have no idea why i did this. below when we are doing deletes, we 190 + // check if `cid` here is nil to indicate if we should delete. that really doesn't make much sense and its super illogical 191 + // when reading this code. i dont feel like fixing right now though so 192 entries = append(entries, models.Record{ 193 Did: urepo.Did, 194 Nsid: op.Collection, 195 Rkey: *op.Rkey, 196 Value: old.Value, 197 }) 198 + 199 + // delete the record from the repo 200 + err := r.DeleteRecord(ctx, fmt.Sprintf("%s/%s", op.Collection, *op.Rkey)) 201 if err != nil { 202 return nil, err 203 } 204 + 205 + // add a result for the delete 206 results = append(results, ApplyWriteResult{ 207 Type: to.StringPtr(OpTypeDelete.String()), 208 }) 209 case OpTypeUpdate: 210 + // HACK: same hack as above for type fixes 211 + b, err := json.Marshal(*op.Record) 212 if err != nil { 213 return nil, err 214 } 215 + out, err := atdata.UnmarshalJSON(b) 216 if err != nil { 217 return nil, err 218 } 219 mm := MarshalableMap(out) 220 + 221 + nc, err := r.UpdateRecord(ctx, fmt.Sprintf("%s/%s", op.Collection, *op.Rkey), &mm) 222 if err != nil { 223 return nil, err 224 } 225 + 226 + d, err := atdata.MarshalCBOR(mm) 227 if err != nil { 228 return nil, err 229 } 230 + 231 entries = append(entries, models.Record{ 232 Did: urepo.Did, 233 CreatedAt: rm.clock.Next().String(), ··· 236 Cid: nc.String(), 237 Value: d, 238 }) 239 + 240 results = append(results, ApplyWriteResult{ 241 Type: to.StringPtr(OpTypeUpdate.String()), 242 Uri: to.StringPtr("at://" + urepo.Did + "/" + op.Collection + "/" + *op.Rkey), ··· 246 } 247 } 248 249 + // commit and get the new root 250 + newroot, rev, err := r.Commit(ctx, urepo.SignFor) 251 if err != nil { 252 return nil, err 253 } 254 255 + for _, result := range results { 256 + if result.Type != nil { 257 + metrics.RepoOperations.WithLabelValues(*result.Type).Inc() 258 + } 259 + } 260 + 261 + // create a buffer for dumping our new cbor into 262 buf := new(bytes.Buffer) 263 264 + // first write the car header to the buffer 265 hb, err := cbor.DumpObject(&car.CarHeader{ 266 Roots: []cid.Cid{newroot}, 267 Version: 1, 268 }) 269 if _, err := carstore.LdWrite(buf, hb); err != nil { 270 return nil, err 271 } 272 273 + // get a diff of the changes to the repo 274 + diffops, err := r.DiffSince(ctx, rootcid) 275 if err != nil { 276 return nil, err 277 } 278 279 + // create the repo ops for the given diff 280 ops := make([]*atproto.SyncSubscribeRepos_RepoOp, 0, len(diffops)) 281 for _, op := range diffops { 282 var c cid.Cid 283 switch op.Op { ··· 306 }) 307 } 308 309 + blk, err := dbs.Get(ctx, c) 310 if err != nil { 311 return nil, err 312 } 313 314 + // write the block to the buffer 315 if _, err := carstore.LdWrite(buf, blk.Cid().Bytes(), blk.RawData()); err != nil { 316 return nil, err 317 } 318 } 319 320 + // write the writelog to the buffer 321 for _, op := range bs.GetWriteLog() { 322 if _, err := carstore.LdWrite(buf, op.Cid().Bytes(), op.RawData()); err != nil { 323 return nil, err 324 } 325 } 326 327 + // blob blob blob blob blob :3 328 var blobs []lexutil.LexLink 329 for _, entry := range entries { 330 var cids []cid.Cid 331 + // whenever there is cid present, we know it's a create (dumb) 332 if entry.Cid != "" { 333 + if err := rm.s.db.Create(ctx, &entry, []clause.Expression{clause.OnConflict{ 334 Columns: []clause.Column{{Name: "did"}, {Name: "nsid"}, {Name: "rkey"}}, 335 UpdateAll: true, 336 }}).Error; err != nil { 337 return nil, err 338 } 339 340 + // increment the given blob refs, yay 341 + cids, err = rm.incrementBlobRefs(ctx, urepo, entry.Value) 342 if err != nil { 343 return nil, err 344 } 345 } else { 346 + // as i noted above this is dumb. but we delete whenever the cid is nil. it works solely becaue the pkey 347 + // is did + collection + rkey. i still really want to separate that out, or use a different type to make 348 + // this less confusing/easy to read. alas, its 2 am and yea no 349 + if err := rm.s.db.Delete(ctx, &entry, nil).Error; err != nil { 350 return nil, err 351 } 352 + 353 + // TODO: 354 + cids, err = rm.decrementBlobRefs(ctx, urepo, entry.Value) 355 if err != nil { 356 return nil, err 357 } 358 } 359 360 + // add all the relevant blobs to the blobs list of blobs. blob ^.^ 361 for _, c := range cids { 362 blobs = append(blobs, lexutil.LexLink(c)) 363 } 364 } 365 366 + // NOTE: using the request ctx seems a bit suss here, so using a background context. i'm not sure if this 367 + // runs sync or not 368 + rm.s.evtman.AddEvent(context.Background(), &events.XRPCStreamEvent{ 369 RepoCommit: &atproto.SyncSubscribeRepos_Commit{ 370 Repo: urepo.Did, 371 Blocks: buf.Bytes(), ··· 379 }, 380 }) 381 382 + if err := rm.s.UpdateRepo(ctx, urepo.Did, newroot, rev); err != nil { 383 return nil, err 384 } 385 ··· 394 return results, nil 395 } 396 397 + // this is a fun little guy. to get a proof, we need to read the record out of the blockstore and record how we actually 398 + // got to the guy. we'll wrap a new blockstore in a recording blockstore, then return the log for proof 399 + func (rm *RepoMan) getRecordProof(ctx context.Context, urepo models.Repo, collection, rkey string) (cid.Cid, []blocks.Block, error) { 400 c, err := cid.Cast(urepo.Root) 401 if err != nil { 402 return cid.Undef, nil, err ··· 405 dbs := rm.s.getBlockstore(urepo.Did) 406 bs := recording_blockstore.New(dbs) 407 408 + r, err := repo.OpenRepo(ctx, bs, c) 409 if err != nil { 410 return cid.Undef, nil, err 411 } 412 413 + _, _, err = r.GetRecordBytes(ctx, fmt.Sprintf("%s/%s", collection, rkey)) 414 if err != nil { 415 return cid.Undef, nil, err 416 } ··· 418 return c, bs.GetReadLog(), nil 419 } 420 421 + func (rm *RepoMan) incrementBlobRefs(ctx context.Context, urepo models.Repo, cbor []byte) ([]cid.Cid, error) { 422 cids, err := getBlobCidsFromCbor(cbor) 423 if err != nil { 424 return nil, err 425 } 426 427 for _, c := range cids { 428 + if err := rm.db.Exec(ctx, "UPDATE blobs SET ref_count = ref_count + 1 WHERE did = ? AND cid = ?", nil, urepo.Did, c.Bytes()).Error; err != nil { 429 return nil, err 430 } 431 } ··· 433 return cids, nil 434 } 435 436 + func (rm *RepoMan) decrementBlobRefs(ctx context.Context, urepo models.Repo, cbor []byte) ([]cid.Cid, error) { 437 cids, err := getBlobCidsFromCbor(cbor) 438 if err != nil { 439 return nil, err ··· 444 ID uint 445 Count int 446 } 447 + if err := rm.db.Raw(ctx, "UPDATE blobs SET ref_count = ref_count - 1 WHERE did = ? AND cid = ? RETURNING id, ref_count", nil, urepo.Did, c.Bytes()).Scan(&res).Error; err != nil { 448 return nil, err 449 } 450 451 + // TODO: this does _not_ handle deletions of blobs that are on s3 storage!!!! we need to get the blob, see what 452 + // storage it is in, and clean up s3!!!! 453 if res.Count == 0 { 454 + if err := rm.db.Exec(ctx, "DELETE FROM blobs WHERE id = ?", nil, res.ID).Error; err != nil { 455 return nil, err 456 } 457 + if err := rm.db.Exec(ctx, "DELETE FROM blob_parts WHERE blob_id = ?", nil, res.ID).Error; err != nil { 458 return nil, err 459 } 460 } ··· 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 }
+133 -39
server/server.go
··· 39 "github.com/haileyok/cocoon/oauth/provider" 40 "github.com/haileyok/cocoon/plc" 41 "github.com/ipfs/go-cid" 42 echo_session "github.com/labstack/echo-contrib/session" 43 "github.com/labstack/echo/v4" 44 "github.com/labstack/echo/v4/middleware" 45 slogecho "github.com/samber/slog-echo" 46 "gorm.io/driver/sqlite" 47 "gorm.io/gorm" 48 ) ··· 52 ) 53 54 type S3Config struct { 55 - BackupsEnabled bool 56 - Endpoint string 57 - Region string 58 - Bucket string 59 - AccessKey string 60 - SecretKey string 61 } 62 63 type Server struct { ··· 75 oauthProvider *provider.Provider 76 evtman *events.EventManager 77 passport *identity.Passport 78 79 dbName string 80 s3Config *S3Config 81 } 82 83 type Args struct { 84 Addr string 85 DbName string 86 - Logger *slog.Logger 87 Version string 88 Did string 89 Hostname string ··· 92 ContactEmail string 93 Relays []string 94 AdminPassword string 95 96 SmtpUser string 97 SmtpPass string ··· 105 SessionSecret string 106 107 BlockstoreVariant BlockstoreVariant 108 } 109 110 type config struct { ··· 115 EnforcePeering bool 116 Relays []string 117 AdminPassword string 118 SmtpEmail string 119 SmtpName string 120 BlockstoreVariant BlockstoreVariant 121 } 122 123 type CustomValidator struct { ··· 195 } 196 197 func New(args *Args) (*Server, error) { 198 if args.Addr == "" { 199 return nil, fmt.Errorf("addr must be set") 200 } ··· 223 return nil, fmt.Errorf("admin password must be set") 224 } 225 226 - if args.Logger == nil { 227 - args.Logger = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{})) 228 - } 229 - 230 if args.SessionSecret == "" { 231 panic("SESSION SECRET WAS NOT SET. THIS IS REQUIRED. ") 232 } ··· 234 e := echo.New() 235 236 e.Pre(middleware.RemoveTrailingSlash()) 237 - e.Pre(slogecho.New(args.Logger)) 238 e.Use(echo_session.Middleware(sessions.NewCookieStore([]byte(args.SessionSecret)))) 239 e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ 240 AllowOrigins: []string{"*"}, 241 AllowHeaders: []string{"*"}, ··· 281 IdleTimeout: 5 * time.Minute, 282 } 283 284 - gdb, err := gorm.Open(sqlite.Open("cocoon.db"), &gorm.Config{}) 285 - if err != nil { 286 - return nil, err 287 } 288 dbw := db.NewDB(gdb) 289 ··· 326 var nonceSecret []byte 327 maybeSecret, err := os.ReadFile("nonce.secret") 328 if err != nil && !os.IsNotExist(err) { 329 - args.Logger.Error("error attempting to read nonce secret", "error", err) 330 } else { 331 nonceSecret = maybeSecret 332 } ··· 347 EnforcePeering: false, 348 Relays: args.Relays, 349 AdminPassword: args.AdminPassword, 350 SmtpName: args.SmtpName, 351 SmtpEmail: args.SmtpEmail, 352 BlockstoreVariant: args.BlockstoreVariant, 353 }, 354 evtman: events.NewEventManager(events.NewMemPersister()), 355 passport: identity.NewPassport(h, identity.NewMemCache(10_000)), 356 357 dbName: args.DbName, 358 s3Config: args.S3Config, 359 360 oauthProvider: provider.NewProvider(provider.Args{ 361 Hostname: args.Hostname, 362 ClientManagerArgs: client.ManagerArgs{ 363 Cli: oauthCli, 364 - Logger: args.Logger, 365 }, 366 DpopManagerArgs: dpop.ManagerArgs{ 367 NonceSecret: nonceSecret, 368 NonceRotationInterval: constants.NonceMaxRotationInterval / 3, 369 OnNonceSecretCreated: func(newNonce []byte) { 370 if err := os.WriteFile("nonce.secret", newNonce, 0644); err != nil { 371 - args.Logger.Error("error writing new nonce secret", "error", err) 372 } 373 }, 374 - Logger: args.Logger, 375 Hostname: args.Hostname, 376 }, 377 }), ··· 383 384 // TODO: should validate these args 385 if args.SmtpUser == "" || args.SmtpPass == "" || args.SmtpHost == "" || args.SmtpPort == "" || args.SmtpEmail == "" || args.SmtpName == "" { 386 - args.Logger.Warn("not enough smpt args were provided. mailing will not work for your server.") 387 } else { 388 mail := mailyak.New(args.SmtpHost+":"+args.SmtpPort, smtp.PlainAuth("", args.SmtpUser, args.SmtpPass, args.SmtpHost)) 389 mail.From(s.config.SmtpEmail) ··· 408 s.echo.GET("/", s.handleRoot) 409 s.echo.GET("/xrpc/_health", s.handleHealth) 410 s.echo.GET("/.well-known/did.json", s.handleWellKnown) 411 s.echo.GET("/.well-known/oauth-protected-resource", s.handleOauthProtectedResource) 412 s.echo.GET("/.well-known/oauth-authorization-server", s.handleOauthAuthorizationServer) 413 s.echo.GET("/robots.txt", s.handleRobots) 414 415 // public 416 s.echo.GET("/xrpc/com.atproto.identity.resolveHandle", s.handleResolveHandle) 417 - s.echo.POST("/xrpc/com.atproto.server.createAccount", s.handleCreateAccount) 418 s.echo.POST("/xrpc/com.atproto.server.createAccount", s.handleCreateAccount) 419 s.echo.POST("/xrpc/com.atproto.server.createSession", s.handleCreateSession) 420 s.echo.GET("/xrpc/com.atproto.server.describeServer", s.handleDescribeServer) 421 422 s.echo.GET("/xrpc/com.atproto.repo.describeRepo", s.handleDescribeRepo) 423 s.echo.GET("/xrpc/com.atproto.sync.listRepos", s.handleListRepos) ··· 432 s.echo.GET("/xrpc/com.atproto.sync.listBlobs", s.handleSyncListBlobs) 433 s.echo.GET("/xrpc/com.atproto.sync.getBlob", s.handleSyncGetBlob) 434 435 // account 436 s.echo.GET("/account", s.handleAccount) 437 s.echo.POST("/account/revoke", s.handleAccountRevoke) ··· 452 s.echo.GET("/xrpc/com.atproto.server.getSession", s.handleGetSession, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) 453 s.echo.POST("/xrpc/com.atproto.server.refreshSession", s.handleRefreshSession, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) 454 s.echo.POST("/xrpc/com.atproto.server.deleteSession", s.handleDeleteSession, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) 455 s.echo.POST("/xrpc/com.atproto.identity.updateHandle", s.handleIdentityUpdateHandle, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) 456 s.echo.POST("/xrpc/com.atproto.server.confirmEmail", s.handleServerConfirmEmail, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) 457 s.echo.POST("/xrpc/com.atproto.server.requestEmailConfirmation", s.handleServerRequestEmailConfirmation, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) 458 s.echo.POST("/xrpc/com.atproto.server.requestPasswordReset", s.handleServerRequestPasswordReset) // AUTH NOT REQUIRED FOR THIS ONE ··· 463 s.echo.GET("/xrpc/com.atproto.server.checkAccountStatus", s.handleServerCheckAccountStatus, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) 464 s.echo.POST("/xrpc/com.atproto.server.deactivateAccount", s.handleServerDeactivateAccount, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) 465 s.echo.POST("/xrpc/com.atproto.server.activateAccount", s.handleServerActivateAccount, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) 466 467 // repo 468 s.echo.POST("/xrpc/com.atproto.repo.createRecord", s.handleCreateRecord, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) 469 s.echo.POST("/xrpc/com.atproto.repo.putRecord", s.handlePutRecord, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) 470 s.echo.POST("/xrpc/com.atproto.repo.deleteRecord", s.handleDeleteRecord, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) ··· 475 // stupid silly endpoints 476 s.echo.GET("/xrpc/app.bsky.actor.getPreferences", s.handleActorGetPreferences, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) 477 s.echo.POST("/xrpc/app.bsky.actor.putPreferences", s.handleActorPutPreferences, s.handleLegacySessionMiddleware, s.handleOauthSessionMiddleware) 478 479 // admin routes 480 s.echo.POST("/xrpc/com.atproto.server.createInviteCode", s.handleCreateInviteCode, s.handleAdminMiddleware) ··· 486 } 487 488 func (s *Server) Serve(ctx context.Context) error { 489 s.addRoutes() 490 491 - s.logger.Info("migrating...") 492 493 s.db.AutoMigrate( 494 &models.Actor{}, ··· 500 &models.Record{}, 501 &models.Blob{}, 502 &models.BlobPart{}, 503 &provider.OauthToken{}, 504 &provider.OauthAuthorizationRequest{}, 505 ) 506 507 - s.logger.Info("starting cocoon") 508 509 go func() { 510 if err := s.httpd.ListenAndServe(); err != nil { ··· 514 515 go s.backupRoutine() 516 517 for _, relay := range s.config.Relays { 518 cli := xrpc.Client{Host: relay} 519 - atproto.SyncRequestCrawl(ctx, &cli, &atproto.SyncRequestCrawl_Input{ 520 Hostname: s.config.Hostname, 521 - }) 522 } 523 524 - <-ctx.Done() 525 - 526 - fmt.Println("shut down") 527 528 return nil 529 } 530 531 func (s *Server) doBackup() { 532 start := time.Now() 533 534 - s.logger.Info("beginning backup to s3...") 535 536 var buf bytes.Buffer 537 if err := func() error { 538 - s.logger.Info("reading database bytes...") 539 s.db.Lock() 540 defer s.db.Unlock() 541 ··· 551 552 return nil 553 }(); err != nil { 554 - s.logger.Error("error backing up database", "error", err) 555 return 556 } 557 558 if err := func() error { 559 - s.logger.Info("sending to s3...") 560 561 currTime := time.Now().Format("2006-01-02_15-04-05") 562 key := "cocoon-backup-" + currTime + ".db" ··· 586 return fmt.Errorf("error uploading file to s3: %w", err) 587 } 588 589 - s.logger.Info("finished uploading backup to s3", "key", key, "duration", time.Now().Sub(start).Seconds()) 590 591 return nil 592 }(); err != nil { 593 - s.logger.Error("error uploading database backup", "error", err) 594 return 595 } 596 ··· 598 } 599 600 func (s *Server) backupRoutine() { 601 if s.s3Config == nil || !s.s3Config.BackupsEnabled { 602 return 603 } 604 605 if s.s3Config.Region == "" { 606 - s.logger.Warn("no s3 region configured but backups are enabled. backups will not run.") 607 return 608 } 609 610 if s.s3Config.Bucket == "" { 611 - s.logger.Warn("no s3 bucket configured but backups are enabled. backups will not run.") 612 return 613 } 614 615 if s.s3Config.AccessKey == "" { 616 - s.logger.Warn("no s3 access key configured but backups are enabled. backups will not run.") 617 return 618 } 619 620 if s.s3Config.SecretKey == "" { 621 - s.logger.Warn("no s3 secret key configured but backups are enabled. backups will not run.") 622 return 623 } 624 ··· 646 } 647 648 func (s *Server) UpdateRepo(ctx context.Context, did string, root cid.Cid, rev string) error { 649 - if err := s.db.Exec("UPDATE repos SET root = ?, rev = ? WHERE did = ?", nil, root.Bytes(), rev, did).Error; err != nil { 650 return err 651 } 652
··· 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 ··· 118 SessionSecret string 119 120 BlockstoreVariant BlockstoreVariant 121 + FallbackProxy string 122 } 123 124 type config struct { ··· 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 } ··· 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) 457 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 ··· 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 ··· 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
+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
··· 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
··· 26 type="password" 27 placeholder="Password" 28 /> 29 <input name="query_params" type="hidden" value="{{ .QueryParams }}" /> 30 <button class="primary" type="submit" value="Login">Login</button> 31 </form>
··· 26 type="password" 27 placeholder="Password" 28 /> 29 + {{ if .flashes.tokenrequired }} 30 + <br /> 31 + <input name="token" id="token" placeholder="Enter your 2FA token" /> 32 + {{ end }} 33 <input name="query_params" type="hidden" value="{{ .QueryParams }}" /> 34 <button class="primary" type="submit" value="Login">Login</button> 35 </form>
+3 -3
sqlite_blockstore/sqlite_blockstore.go
··· 45 return maybeBlock, nil 46 } 47 48 - if err := bs.db.Raw("SELECT * FROM blocks WHERE did = ? AND cid = ?", nil, bs.did, cid.Bytes()).Scan(&block).Error; err != nil { 49 return nil, err 50 } 51 ··· 71 Value: block.RawData(), 72 } 73 74 - if err := bs.db.Create(&b, []clause.Expression{clause.OnConflict{ 75 Columns: []clause.Column{{Name: "did"}, {Name: "cid"}}, 76 UpdateAll: true, 77 }}).Error; err != nil { ··· 94 } 95 96 func (bs *SqliteBlockstore) PutMany(ctx context.Context, blocks []blocks.Block) error { 97 - tx := bs.db.BeginDangerously() 98 99 for _, block := range blocks { 100 bs.inserts[block.Cid()] = block
··· 45 return maybeBlock, nil 46 } 47 48 + if err := bs.db.Raw(ctx, "SELECT * FROM blocks WHERE did = ? AND cid = ?", nil, bs.did, cid.Bytes()).Scan(&block).Error; err != nil { 49 return nil, err 50 } 51 ··· 71 Value: block.RawData(), 72 } 73 74 + if err := bs.db.Create(ctx, &b, []clause.Expression{clause.OnConflict{ 75 Columns: []clause.Column{{Name: "did"}, {Name: "cid"}}, 76 UpdateAll: true, 77 }}).Error; err != nil { ··· 94 } 95 96 func (bs *SqliteBlockstore) PutMany(ctx context.Context, blocks []blocks.Block) error { 97 + tx := bs.db.BeginDangerously(ctx) 98 99 for _, block := range blocks { 100 bs.inserts[block.Cid()] = block
+1 -1
test.go
··· 32 33 u.Path = "xrpc/com.atproto.sync.subscribeRepos" 34 conn, _, err := dialer.Dial(u.String(), http.Header{ 35 - "User-Agent": []string{fmt.Sprintf("hot-topic/0.0.0")}, 36 }) 37 if err != nil { 38 return fmt.Errorf("subscribing to firehose failed (dialing): %w", err)
··· 32 33 u.Path = "xrpc/com.atproto.sync.subscribeRepos" 34 conn, _, err := dialer.Dial(u.String(), http.Header{ 35 + "User-Agent": []string{"cocoon-test/0.0.0"}, 36 }) 37 if err != nil { 38 return fmt.Errorf("subscribing to firehose failed (dialing): %w", err)