├── .dockerignore ├── Makefile ├── .golangci.yml ├── Dockerfile ├── s3_test.go ├── README.md ├── io.go ├── go.mod ├── io_test.go ├── LICENSE ├── s3.go └── go.sum /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .gitignore 3 | *.md 4 | LICENSE 5 | *_test.go 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | test: 3 | go test -v ./... 4 | 5 | .PHONY: test-race 6 | test-race: 7 | go test -race -v ./... 8 | 9 | .PHONY: lint 10 | lint: 11 | go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest run 12 | 13 | .PHONY: fmt 14 | fmt: 15 | go run mvdan.cc/gofumpt@latest -w . 16 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | run: 4 | timeout: 5m 5 | 6 | linters: 7 | enable: 8 | - errcheck 9 | - govet 10 | - ineffassign 11 | - staticcheck 12 | - unused 13 | - misspell 14 | - gocyclo 15 | - gosec 16 | 17 | linters-settings: 18 | gocyclo: 19 | min-complexity: 15 20 | 21 | errcheck: 22 | check-type-assertions: true 23 | 24 | govet: 25 | check-shadowing: true 26 | 27 | issues: 28 | exclude-rules: 29 | - path: _test\.go 30 | linters: 31 | - gosec 32 | 33 | max-issues-per-linter: 0 34 | max-same-issues: 0 35 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM caddy:2.10.0-builder AS builder 2 | 3 | RUN apk add --no-cache git ca-certificates 4 | 5 | WORKDIR /build 6 | 7 | COPY . /build/certmagic-s3/ 8 | 9 | RUN xcaddy build \ 10 | --with github.com/techknowlogick/certmagic-s3=/build/certmagic-s3 11 | 12 | FROM gcr.io/distroless/static-debian12:latest 13 | 14 | COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 15 | COPY --from=builder /build/caddy /usr/bin/caddy 16 | 17 | EXPOSE 80 443 2019 18 | 19 | ENTRYPOINT ["/usr/bin/caddy"] 20 | 21 | CMD ["run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"] 22 | -------------------------------------------------------------------------------- /s3_test.go: -------------------------------------------------------------------------------- 1 | package s3 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestS3_objName(t *testing.T) { 8 | tests := []struct { 9 | name string 10 | prefix string 11 | key string 12 | expected string 13 | }{ 14 | { 15 | name: "empty prefix", 16 | prefix: "", 17 | key: "test.key", 18 | expected: "test.key", 19 | }, 20 | { 21 | name: "with prefix", 22 | prefix: "acme", 23 | key: "test.key", 24 | expected: "acme/test.key", 25 | }, 26 | { 27 | name: "slash normalization", 28 | prefix: "//acme//", 29 | key: "//test.key", 30 | expected: "acme/test.key", 31 | }, 32 | } 33 | 34 | for _, tt := range tests { 35 | t.Run(tt.name, func(t *testing.T) { 36 | s3 := &S3{Prefix: tt.prefix} 37 | result := s3.objName(tt.key) 38 | if result != tt.expected { 39 | t.Errorf("objName() = %v, want %v", result, tt.expected) 40 | } 41 | }) 42 | } 43 | } 44 | 45 | func TestS3_objLockName(t *testing.T) { 46 | s3 := &S3{Prefix: "acme"} 47 | key := "test.key" 48 | expected := "acme/test.key.lock" 49 | 50 | result := s3.objLockName(key) 51 | if result != expected { 52 | t.Errorf("objLockName() = %v, want %v", result, expected) 53 | } 54 | } 55 | 56 | func TestS3_UsePathStyleConfiguration(t *testing.T) { 57 | tests := []struct { 58 | name string 59 | endpoint string 60 | usePathStyle bool 61 | expectPathStyle bool 62 | }{ 63 | { 64 | name: "default AWS (no custom endpoint)", 65 | endpoint: "", 66 | usePathStyle: false, 67 | expectPathStyle: false, 68 | }, 69 | { 70 | name: "explicit path style enabled", 71 | endpoint: "", 72 | usePathStyle: true, 73 | expectPathStyle: true, 74 | }, 75 | { 76 | name: "custom endpoint forces path style", 77 | endpoint: "https://minio.example.com", 78 | usePathStyle: false, 79 | expectPathStyle: true, 80 | }, 81 | } 82 | 83 | for _, tt := range tests { 84 | t.Run(tt.name, func(t *testing.T) { 85 | s3 := &S3{ 86 | Endpoint: tt.endpoint, 87 | UsePathStyle: tt.usePathStyle, 88 | } 89 | 90 | endpoint := tt.endpoint 91 | shouldUsePathStyle := s3.UsePathStyle || endpoint != "" 92 | 93 | if shouldUsePathStyle != tt.expectPathStyle { 94 | t.Errorf("UsePathStyle logic = %v, want %v", shouldUsePathStyle, tt.expectPathStyle) 95 | } 96 | }) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Certmagic Storage Backend for S3 2 | 3 | This library allows you to use any S3-compatible provider as key/certificate storage backend for your [Certmagic](https://github.com/caddyserver/certmagic)-enabled HTTPS server. To protect your keys from unwanted attention, client-side encryption is possible using the [secretbox](https://pkg.go.dev/golang.org/x/crypto/nacl/secretbox?tab=doc) package. 4 | 5 | ## Configuration Options 6 | 7 | - `endpoint`: Custom endpoint URL (optional) 8 | - `host`: **Deprecated** - Use `endpoint` instead. 9 | - `insecure`: Skip TLS certificate verification (optional, defaults to `false`) 10 | - `bucket`: S3 bucket name (required, no default value) 11 | - `region`: AWS region (optional, defaults to `us-east-1`) 12 | - `access_key`: AWS access key (optional) 13 | - `secret_key`: AWS secret key (optional) 14 | - `profile`: AWS profile name (optional) 15 | - `role_arn`: IAM role ARN for role assumption (optional) 16 | - `prefix`: Object key prefix (defaults to "acme") 17 | - `encryption_key`: 32-byte encryption key for client-side encryption (optional, if not set, then files will be plaintext in object storage) 18 | - `use_path_style`: Force path-style URLs (optional, enforced as `true` when a custom endpoint is used) 19 | 20 | If both `host` and `endpoint` are specified, an error is reported. 21 | 22 | ## What is an S3-compatible service? 23 | 24 | Any service must support the following: 25 | 26 | - v4 Signatures 27 | - Basic S3 operations: 28 | - GetObject 29 | - PutObject 30 | - DeleteObject 31 | - HeadObject 32 | - ListObjectsV2 33 | 34 | ## Configuration Examples 35 | 36 | ### Using Static Credentials (AWS S3) 37 | ```caddyfile 38 | { 39 | storage s3 { 40 | bucket "my-certificates" 41 | region "us-west-2" 42 | access_key "AKIAEXAMPLE" 43 | secret_key "EXAMPLE" 44 | prefix "caddy-certs" 45 | encryption_key "your-32-byte-encryption-key-here" 46 | } 47 | } 48 | ``` 49 | 50 | ### Using Custom S3-Compatible Provider 51 | ```caddyfile 52 | { 53 | storage s3 { 54 | endpoint "https://minio.example.com" 55 | bucket "my-certificates" 56 | region "us-east-1" 57 | access_key "minioadmin" 58 | secret_key "minioadmin" 59 | prefix "caddy-certs" 60 | } 61 | } 62 | ``` 63 | 64 | ## Credits & Thanks 65 | 66 | This project was forked from [@thomersch](https://github.com/thomersch)'s wonderful [Certmagic Storage Backend for Generic S3 Providers](https://github.com/thomersch/certmagic-generic-s3) repository. 67 | 68 | ## License 69 | 70 | This project is licensed under [Apache 2.0](https://github.com/thomersch/certmagic-generic-s3/issues/1), an open source license. 71 | -------------------------------------------------------------------------------- /io.go: -------------------------------------------------------------------------------- 1 | package s3 2 | 3 | import ( 4 | "bytes" 5 | "crypto/rand" 6 | "errors" 7 | "io" 8 | 9 | "golang.org/x/crypto/nacl/secretbox" 10 | ) 11 | 12 | const ( 13 | NonceSize = 24 14 | ) 15 | 16 | type IO interface { 17 | WrapReader(io.Reader) io.Reader 18 | ByteReader([]byte) Reader 19 | } 20 | 21 | type Reader struct { 22 | r io.ReadSeeker 23 | l int64 24 | err error 25 | } 26 | 27 | func (r *Reader) Read(buf []byte) (int, error) { 28 | if r.err != nil { 29 | err := r.err 30 | r.err = nil 31 | return 0, err 32 | } 33 | return r.r.Read(buf) 34 | } 35 | 36 | func (r *Reader) Len() int64 { 37 | return r.l 38 | } 39 | 40 | func (r *Reader) Seek(offset int64, whence int) (int64, error) { 41 | if r.err != nil { 42 | return 0, r.err 43 | } 44 | return r.r.Seek(offset, whence) 45 | } 46 | 47 | type CleartextIO struct{} 48 | 49 | func (ci *CleartextIO) WrapReader(r io.Reader) io.Reader { 50 | return r 51 | } 52 | 53 | func (ci *CleartextIO) ByteReader(buf []byte) Reader { 54 | return Reader{bytes.NewReader(buf), int64(len(buf)), nil} 55 | } 56 | 57 | type SecretBoxIO struct { 58 | SecretKey [32]byte 59 | } 60 | 61 | func NewSecretBoxIO(key [32]byte) *SecretBoxIO { 62 | return &SecretBoxIO{SecretKey: key} 63 | } 64 | 65 | func (sb *SecretBoxIO) IsValid() bool { 66 | var zero [32]byte 67 | return sb.SecretKey != zero 68 | } 69 | 70 | func (sb *SecretBoxIO) makeNonce() ([24]byte, error) { 71 | var nonce [24]byte 72 | _, err := io.ReadFull(rand.Reader, nonce[:]) 73 | return nonce, err 74 | } 75 | 76 | func (sb *SecretBoxIO) WrapReader(r io.Reader) io.Reader { 77 | if !sb.IsValid() { 78 | return &Reader{nil, 0, errors.New("SecretBoxIO not properly initialized")} 79 | } 80 | 81 | allData, err := io.ReadAll(r) 82 | if err != nil { 83 | return &Reader{nil, 0, err} 84 | } 85 | 86 | if len(allData) == 0 { 87 | return bytes.NewReader(nil) 88 | } 89 | 90 | if len(allData) < NonceSize { 91 | return &Reader{nil, 0, errors.New("insufficient data for decryption: missing nonce")} 92 | } 93 | 94 | var nonce [NonceSize]byte 95 | copy(nonce[:], allData[:NonceSize]) 96 | encryptedData := allData[NonceSize:] 97 | 98 | bout, ok := secretbox.Open(nil, encryptedData, &nonce, &sb.SecretKey) 99 | if !ok { 100 | return &Reader{nil, 0, errors.New("decryption failed: invalid key or corrupted data")} 101 | } 102 | return bytes.NewReader(bout) 103 | } 104 | 105 | func (sb *SecretBoxIO) ByteReader(msg []byte) Reader { 106 | if !sb.IsValid() { 107 | return Reader{nil, 0, errors.New("SecretBoxIO not properly initialized")} 108 | } 109 | 110 | nonce, err := sb.makeNonce() 111 | if err != nil { 112 | return Reader{nil, 0, err} 113 | } 114 | 115 | out := make([]byte, NonceSize, NonceSize+len(msg)+secretbox.Overhead) 116 | copy(out, nonce[:]) 117 | 118 | out = secretbox.Seal(out, msg, &nonce, &sb.SecretKey) 119 | return Reader{bytes.NewReader(out), int64(len(out)), nil} 120 | } 121 | 122 | var _ io.ReadSeeker = (*Reader)(nil) 123 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/techknowlogick/certmagic-s3 2 | 3 | go 1.24 4 | 5 | toolchain go1.24.4 6 | 7 | require ( 8 | github.com/aws/aws-sdk-go-v2 v1.36.6 9 | github.com/aws/aws-sdk-go-v2/config v1.29.18 10 | github.com/aws/aws-sdk-go-v2/credentials v1.17.71 11 | github.com/aws/aws-sdk-go-v2/service/s3 v1.84.1 12 | github.com/caddyserver/caddy/v2 v2.10.1-0.20250724224000-b7ae39e906a0 13 | github.com/caddyserver/certmagic v0.23.0 14 | go.uber.org/zap v1.27.0 15 | golang.org/x/crypto v0.40.0 16 | ) 17 | 18 | require ( 19 | github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 // indirect 20 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.33 // indirect 21 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.37 // indirect 22 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.37 // indirect 23 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect 24 | github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.37 // indirect 25 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 // indirect 26 | github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.5 // indirect 27 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.18 // indirect 28 | github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.18 // indirect 29 | github.com/aws/aws-sdk-go-v2/service/sso v1.25.6 // indirect 30 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.4 // indirect 31 | github.com/aws/aws-sdk-go-v2/service/sts v1.34.1 // indirect 32 | github.com/aws/smithy-go v1.22.5 // indirect 33 | github.com/beorn7/perks v1.0.1 // indirect 34 | github.com/caddyserver/zerossl v0.1.3 // indirect 35 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 36 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 37 | github.com/francoispqt/gojay v1.2.13 // indirect 38 | github.com/google/uuid v1.6.0 // indirect 39 | github.com/klauspost/cpuid/v2 v2.3.0 // indirect 40 | github.com/libdns/libdns v1.1.0 // indirect 41 | github.com/mholt/acmez/v3 v3.1.2 // indirect 42 | github.com/miekg/dns v1.1.67 // indirect 43 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 44 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 45 | github.com/prometheus/client_golang v1.22.0 // indirect 46 | github.com/prometheus/client_model v0.6.2 // indirect 47 | github.com/prometheus/common v0.65.0 // indirect 48 | github.com/prometheus/procfs v0.17.0 // indirect 49 | github.com/quic-go/qpack v0.5.1 // indirect 50 | github.com/quic-go/quic-go v0.54.0 // indirect 51 | github.com/zeebo/assert v1.3.0 // indirect 52 | github.com/zeebo/blake3 v0.2.4 // indirect 53 | go.uber.org/mock v0.5.2 // indirect 54 | go.uber.org/multierr v1.11.0 // indirect 55 | go.uber.org/zap/exp v0.3.0 // indirect 56 | golang.org/x/mod v0.26.0 // indirect 57 | golang.org/x/net v0.42.0 // indirect 58 | golang.org/x/sync v0.16.0 // indirect 59 | golang.org/x/sys v0.34.0 // indirect 60 | golang.org/x/term v0.33.0 // indirect 61 | golang.org/x/text v0.27.0 // indirect 62 | golang.org/x/time v0.12.0 // indirect 63 | golang.org/x/tools v0.35.0 // indirect 64 | google.golang.org/protobuf v1.36.6 // indirect 65 | ) 66 | -------------------------------------------------------------------------------- /io_test.go: -------------------------------------------------------------------------------- 1 | package s3 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "io" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | var ( 12 | testKey32 = [32]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32} 13 | testKeyStr = "12345678901234567890123456789012" 14 | ) 15 | 16 | func createTestSecretBoxIO() *SecretBoxIO { 17 | sb := &SecretBoxIO{} 18 | copy(sb.SecretKey[:], []byte(testKeyStr)) 19 | return sb 20 | } 21 | 22 | func assertNoError(t *testing.T, err error, operation string) { 23 | if err != nil { 24 | t.Errorf("%s failed: %v", operation, err) 25 | } 26 | } 27 | 28 | func assertError(t *testing.T, err error, expectedMsg, operation string) { 29 | if err == nil || !strings.Contains(err.Error(), expectedMsg) { 30 | t.Errorf("%s should fail with '%s', got error: %v", operation, expectedMsg, err) 31 | } 32 | } 33 | 34 | func TestNewSecretBoxIO(t *testing.T) { 35 | sb := NewSecretBoxIO(testKey32) 36 | if sb == nil { 37 | t.Error("NewSecretBoxIO() returned nil") 38 | return 39 | } 40 | if sb.SecretKey != testKey32 { 41 | t.Error("NewSecretBoxIO() did not set key correctly") 42 | } 43 | } 44 | 45 | func TestSecretBoxIO_IsValid(t *testing.T) { 46 | tests := []struct { 47 | name string 48 | sb *SecretBoxIO 49 | valid bool 50 | }{ 51 | { 52 | name: "uninitialized (zero key)", 53 | sb: &SecretBoxIO{}, 54 | valid: false, 55 | }, 56 | { 57 | name: "valid key", 58 | sb: &SecretBoxIO{ 59 | SecretKey: testKey32, 60 | }, 61 | valid: true, 62 | }, 63 | } 64 | 65 | for _, tt := range tests { 66 | t.Run(tt.name, func(t *testing.T) { 67 | if got := tt.sb.IsValid(); got != tt.valid { 68 | t.Errorf("SecretBoxIO.IsValid() = %v, want %v", got, tt.valid) 69 | } 70 | }) 71 | } 72 | } 73 | 74 | func TestSecretBoxIO_Operations(t *testing.T) { 75 | t.Run("encrypt decrypt roundtrip", func(t *testing.T) { 76 | sb := createTestSecretBoxIO() 77 | msg := []byte("This is a very important message that shall be encrypted...") 78 | r := sb.ByteReader(msg) 79 | 80 | buf, err := io.ReadAll(&r) 81 | assertNoError(t, err, "encrypting") 82 | 83 | w := bytes.NewReader(buf) 84 | wb := sb.WrapReader(w) 85 | 86 | buf, err = io.ReadAll(wb) 87 | assertNoError(t, err, "decrypting") 88 | 89 | if string(buf) != string(msg) { 90 | t.Errorf("did not decrypt, got: %s", buf) 91 | } 92 | }) 93 | 94 | t.Run("empty input handling", func(t *testing.T) { 95 | sb := createTestSecretBoxIO() 96 | 97 | wr := sb.WrapReader(bytes.NewReader(nil)) 98 | buf, err := io.ReadAll(wr) 99 | assertNoError(t, err, "WrapReader with empty input") 100 | if len(buf) != 0 { 101 | t.Errorf("Buffer should be empty, got: %v", buf) 102 | } 103 | 104 | reader := sb.ByteReader([]byte{}) 105 | _, err = io.ReadAll(&reader) 106 | assertNoError(t, err, "ByteReader with empty input") 107 | }) 108 | 109 | t.Run("valid operations", func(t *testing.T) { 110 | sb := createTestSecretBoxIO() 111 | 112 | reader := sb.ByteReader([]byte("test message")) 113 | _, err := io.ReadAll(&reader) 114 | assertNoError(t, err, "ByteReader with valid input") 115 | }) 116 | } 117 | 118 | func TestSecretBoxIO_ErrorCases(t *testing.T) { 119 | t.Run("uninitialized SecretBoxIO", func(t *testing.T) { 120 | sb := &SecretBoxIO{} 121 | 122 | wr := sb.WrapReader(bytes.NewReader([]byte("test"))) 123 | _, err := io.ReadAll(wr) 124 | assertError(t, err, "not properly initialized", "WrapReader") 125 | 126 | reader := sb.ByteReader([]byte("test")) 127 | _, err = io.ReadAll(&reader) 128 | assertError(t, err, "not properly initialized", "ByteReader") 129 | }) 130 | 131 | t.Run("insufficient data for decryption", func(t *testing.T) { 132 | sb := &SecretBoxIO{SecretKey: testKey32} 133 | wr := sb.WrapReader(bytes.NewReader([]byte("short"))) // Less than 24 bytes 134 | _, err := io.ReadAll(wr) 135 | assertError(t, err, "insufficient data for decryption", "decryption") 136 | }) 137 | } 138 | 139 | func TestCleartextIO(t *testing.T) { 140 | ci := &CleartextIO{} 141 | testData := []byte("plain text data") 142 | 143 | reader := ci.ByteReader(testData) 144 | result, err := io.ReadAll(&reader) 145 | if err != nil { 146 | t.Errorf("CleartextIO.ByteReader() error = %v", err) 147 | } 148 | if !bytes.Equal(result, testData) { 149 | t.Errorf("CleartextIO.ByteReader() = %v, want %v", result, testData) 150 | } 151 | 152 | input := bytes.NewReader(testData) 153 | wrapped := ci.WrapReader(input) 154 | result, err = io.ReadAll(wrapped) 155 | if err != nil { 156 | t.Errorf("CleartextIO.WrapReader() error = %v", err) 157 | } 158 | if !bytes.Equal(result, testData) { 159 | t.Errorf("CleartextIO.WrapReader() = %v, want %v", result, testData) 160 | } 161 | } 162 | 163 | func TestReader(t *testing.T) { 164 | t.Run("normal operations", func(t *testing.T) { 165 | testData := []byte("test data") 166 | reader := Reader{ 167 | r: bytes.NewReader(testData), 168 | l: int64(len(testData)), 169 | } 170 | 171 | if reader.Len() != int64(len(testData)) { 172 | t.Errorf("Reader.Len() = %d, want %d", reader.Len(), len(testData)) 173 | } 174 | 175 | buf := make([]byte, len(testData)) 176 | n, err := reader.Read(buf) 177 | assertNoError(t, err, "Reader.Read()") 178 | if n != len(testData) { 179 | t.Errorf("Reader.Read() n = %d, want %d", n, len(testData)) 180 | } 181 | if !bytes.Equal(buf, testData) { 182 | t.Errorf("Reader.Read() buf = %v, want %v", buf, testData) 183 | } 184 | }) 185 | 186 | t.Run("error handling", func(t *testing.T) { 187 | testErr := errors.New("test error") 188 | reader := Reader{ 189 | r: bytes.NewReader(nil), 190 | l: 0, 191 | err: testErr, 192 | } 193 | 194 | buf := make([]byte, 10) 195 | n, err := reader.Read(buf) 196 | if err != testErr { 197 | t.Errorf("Reader.Read() error = %v, want %v", err, testErr) 198 | } 199 | if n != 0 { 200 | t.Errorf("Reader.Read() n = %d, want 0", n) 201 | } 202 | }) 203 | } 204 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /s3.go: -------------------------------------------------------------------------------- 1 | package s3 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "crypto/tls" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "io/fs" 11 | "net/http" 12 | "strconv" 13 | "strings" 14 | "time" 15 | 16 | "github.com/aws/aws-sdk-go-v2/aws" 17 | "github.com/aws/aws-sdk-go-v2/config" 18 | "github.com/aws/aws-sdk-go-v2/credentials" 19 | "github.com/aws/aws-sdk-go-v2/credentials/stscreds" 20 | s3sdk "github.com/aws/aws-sdk-go-v2/service/s3" 21 | "github.com/aws/aws-sdk-go-v2/service/s3/types" 22 | "github.com/aws/aws-sdk-go-v2/service/sts" 23 | "github.com/caddyserver/caddy/v2" 24 | "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" 25 | "github.com/caddyserver/certmagic" 26 | "go.uber.org/zap" 27 | ) 28 | 29 | var ErrInvalidKey = errors.New("invalid key") 30 | 31 | type S3 struct { 32 | Logger *zap.Logger 33 | 34 | // S3 35 | Client *s3sdk.Client 36 | Host string `json:"host"` 37 | Endpoint string `json:"endpoint"` 38 | Insecure bool `json:"insecure"` 39 | Bucket string `json:"bucket"` 40 | Region string `json:"region"` 41 | AccessKey string `json:"access_key"` 42 | SecretKey string `json:"secret_key"` 43 | Profile string `json:"profile"` 44 | RoleARN string `json:"role_arn"` 45 | Prefix string `json:"prefix"` 46 | UsePathStyle bool `json:"use_path_style,omitempty"` 47 | 48 | // EncryptionKey is optional. If you do not wish to encrypt your certficates and key inside the S3 bucket, leave it empty. 49 | EncryptionKey string `json:"encryption_key"` 50 | 51 | iowrap IO 52 | } 53 | 54 | func init() { 55 | caddy.RegisterModule(new(S3)) 56 | } 57 | 58 | func (s3 *S3) Provision(ctx caddy.Context) error { 59 | s3.Logger = ctx.Logger(s3) 60 | 61 | if s3.Host != "" { 62 | s3.Logger.Info("Using deprecated 'host' option, consider switching to 'endpoint'", 63 | zap.String("host", s3.Host), 64 | zap.String("endpoint", s3.Endpoint), 65 | ) 66 | } 67 | 68 | client, err := s3.buildS3Client() 69 | if err != nil { 70 | return fmt.Errorf("failed to create S3 client: %w", err) 71 | } 72 | 73 | s3.Client = client 74 | return s3.setupEncryption() 75 | } 76 | 77 | func (s3 *S3) buildS3Client() (*s3sdk.Client, error) { 78 | configOptions := []func(*config.LoadOptions) error{ 79 | config.WithRegion(s3.Region), 80 | } 81 | 82 | if s3.Endpoint != "" { 83 | // some non-AWS providers do not implement automatic checksums 84 | // see https://github.com/aws/aws-sdk-go-v2/discussions/2960 for more details 85 | configOptions = append(configOptions, config.WithRequestChecksumCalculation(aws.RequestChecksumCalculationWhenRequired)) 86 | } 87 | 88 | if s3.Insecure { 89 | s3.Logger.Warn("TLS certificate verification is disabled - this is insecure and should only be used for testing") 90 | httpClient := &http.Client{ 91 | Transport: &http.Transport{ 92 | TLSClientConfig: &tls.Config{ 93 | InsecureSkipVerify: true, // #nosec G402 94 | }, 95 | }, 96 | } 97 | configOptions = append(configOptions, config.WithHTTPClient(httpClient)) 98 | } 99 | 100 | if s3.AccessKey != "" && s3.SecretKey != "" { 101 | configOptions = append(configOptions, config.WithCredentialsProvider( 102 | credentials.NewStaticCredentialsProvider(s3.AccessKey, s3.SecretKey, ""))) 103 | } else if s3.Profile != "" { 104 | configOptions = append(configOptions, config.WithSharedConfigProfile(s3.Profile)) 105 | } 106 | 107 | cfg, err := config.LoadDefaultConfig(context.Background(), configOptions...) 108 | if err != nil { 109 | return nil, err 110 | } 111 | 112 | if s3.RoleARN != "" { 113 | stsClient := sts.NewFromConfig(cfg) 114 | provider := stscreds.NewAssumeRoleProvider(stsClient, s3.RoleARN) 115 | cfg.Credentials = aws.NewCredentialsCache(provider) 116 | } 117 | 118 | var s3Options []func(*s3sdk.Options) 119 | 120 | if s3.Endpoint != "" { 121 | s3Options = append(s3Options, func(o *s3sdk.Options) { 122 | o.BaseEndpoint = aws.String(s3.Endpoint) 123 | }) 124 | } 125 | 126 | if s3.UsePathStyle { 127 | s3Options = append(s3Options, func(o *s3sdk.Options) { 128 | o.UsePathStyle = true 129 | }) 130 | } 131 | 132 | return s3sdk.NewFromConfig(cfg, s3Options...), nil 133 | } 134 | 135 | func (s3 *S3) setupEncryption() error { 136 | if len(s3.EncryptionKey) == 0 { 137 | s3.Logger.Info("Clear text certificate storage active") 138 | s3.iowrap = &CleartextIO{} 139 | } else if len(s3.EncryptionKey) != 32 { 140 | s3.Logger.Error("encryption key must have exactly 32 bytes") 141 | return errors.New("encryption key must have exactly 32 bytes") 142 | } else { 143 | s3.Logger.Info("Encrypted certificate storage active") 144 | sb := &SecretBoxIO{} 145 | copy(sb.SecretKey[:], []byte(s3.EncryptionKey)) 146 | s3.iowrap = sb 147 | } 148 | 149 | return nil 150 | } 151 | 152 | func (s3 *S3) CaddyModule() caddy.ModuleInfo { 153 | return caddy.ModuleInfo{ 154 | ID: "caddy.storage.s3", 155 | New: func() caddy.Module { 156 | return new(S3) 157 | }, 158 | } 159 | } 160 | 161 | var ( 162 | LockExpiration = 2 * time.Minute 163 | LockPollInterval = 1 * time.Second 164 | LockTimeout = 15 * time.Second 165 | ) 166 | 167 | func (s3 *S3) Lock(ctx context.Context, key string) error { 168 | s3.Logger.Info(fmt.Sprintf("Lock: %v", s3.objName(key))) 169 | startedAt := time.Now() 170 | 171 | for { 172 | input := &s3sdk.GetObjectInput{ 173 | Bucket: aws.String(s3.Bucket), 174 | Key: aws.String(s3.objLockName(key)), 175 | } 176 | 177 | result, err := s3.Client.GetObject(ctx, input) 178 | if err != nil { 179 | var nsk *types.NoSuchKey 180 | if errors.As(err, &nsk) { 181 | return s3.putLockFile(ctx, key) 182 | } 183 | continue 184 | } 185 | 186 | buf, err := io.ReadAll(result.Body) 187 | _ = result.Body.Close() 188 | if err != nil { 189 | continue 190 | } 191 | 192 | lt, err := time.Parse(time.RFC3339, string(buf)) 193 | if err != nil { 194 | // Lock file does not make sense, overwrite. 195 | return s3.putLockFile(ctx, key) 196 | } 197 | if lt.Add(LockTimeout).Before(time.Now()) { 198 | // Existing lock file expired, overwrite. 199 | return s3.putLockFile(ctx, key) 200 | } 201 | 202 | if startedAt.Add(LockTimeout).Before(time.Now()) { 203 | return errors.New("acquiring lock failed") 204 | } 205 | time.Sleep(LockPollInterval) 206 | } 207 | } 208 | 209 | func (s3 *S3) putLockFile(ctx context.Context, key string) error { 210 | // Object does not exist, we're creating a lock file. 211 | lockData := []byte(time.Now().Format(time.RFC3339)) 212 | r := bytes.NewReader(lockData) 213 | 214 | input := &s3sdk.PutObjectInput{ 215 | Bucket: aws.String(s3.Bucket), 216 | Key: aws.String(s3.objLockName(key)), 217 | Body: r, 218 | ContentLength: aws.Int64(int64(len(lockData))), 219 | } 220 | 221 | _, err := s3.Client.PutObject(ctx, input) 222 | return err 223 | } 224 | 225 | func (s3 *S3) Unlock(ctx context.Context, key string) error { 226 | s3.Logger.Info(fmt.Sprintf("Release lock: %v", s3.objName(key))) 227 | 228 | input := &s3sdk.DeleteObjectInput{ 229 | Bucket: aws.String(s3.Bucket), 230 | Key: aws.String(s3.objLockName(key)), 231 | } 232 | 233 | _, err := s3.Client.DeleteObject(ctx, input) 234 | return err 235 | } 236 | 237 | func (s3 *S3) Store(ctx context.Context, key string, value []byte) error { 238 | start := time.Now() 239 | objName := s3.objName(key) 240 | 241 | if len(value) == 0 { 242 | return fmt.Errorf("%w: cannot store empty value", ErrInvalidKey) 243 | } 244 | 245 | s3.Logger.Info("storing object", 246 | zap.String("key", objName), 247 | zap.Int("size", len(value)), 248 | zap.String("bucket", s3.Bucket), 249 | ) 250 | 251 | defer func() { 252 | s3.Logger.Debug("store completed", 253 | zap.String("key", objName), 254 | zap.Duration("duration", time.Since(start)), 255 | ) 256 | }() 257 | 258 | r := s3.iowrap.ByteReader(value) 259 | 260 | input := &s3sdk.PutObjectInput{ 261 | Bucket: aws.String(s3.Bucket), 262 | Key: aws.String(objName), 263 | Body: &r, 264 | ContentLength: aws.Int64(r.Len()), 265 | } 266 | 267 | _, err := s3.Client.PutObject(ctx, input) 268 | if err != nil { 269 | return fmt.Errorf("failed to store key %s: %w", key, err) 270 | } 271 | return nil 272 | } 273 | 274 | func (s3 *S3) Load(ctx context.Context, key string) ([]byte, error) { 275 | start := time.Now() 276 | objName := s3.objName(key) 277 | 278 | s3.Logger.Info("loading object", 279 | zap.String("key", objName), 280 | zap.String("bucket", s3.Bucket), 281 | ) 282 | 283 | defer func() { 284 | s3.Logger.Debug("load completed", 285 | zap.String("key", objName), 286 | zap.Duration("duration", time.Since(start)), 287 | ) 288 | }() 289 | 290 | input := &s3sdk.GetObjectInput{ 291 | Bucket: aws.String(s3.Bucket), 292 | Key: aws.String(objName), 293 | } 294 | 295 | result, err := s3.Client.GetObject(ctx, input) 296 | if err != nil { 297 | var nsk *types.NoSuchKey 298 | if errors.As(err, &nsk) { 299 | return nil, fs.ErrNotExist 300 | } 301 | return nil, fmt.Errorf("failed to load key %s: %w", key, err) 302 | } 303 | defer func() { _ = result.Body.Close() }() 304 | 305 | buf, err := io.ReadAll(s3.iowrap.WrapReader(result.Body)) 306 | if err != nil { 307 | return nil, fmt.Errorf("failed to read/decrypt data for key %s: %w", key, err) 308 | } 309 | return buf, nil 310 | } 311 | 312 | func (s3 *S3) Delete(ctx context.Context, key string) error { 313 | start := time.Now() 314 | objName := s3.objName(key) 315 | 316 | s3.Logger.Info("deleting object", 317 | zap.String("key", objName), 318 | zap.String("bucket", s3.Bucket), 319 | ) 320 | 321 | defer func() { 322 | s3.Logger.Debug("delete completed", 323 | zap.String("key", objName), 324 | zap.Duration("duration", time.Since(start)), 325 | ) 326 | }() 327 | 328 | input := &s3sdk.DeleteObjectInput{ 329 | Bucket: aws.String(s3.Bucket), 330 | Key: aws.String(objName), 331 | } 332 | 333 | _, err := s3.Client.DeleteObject(ctx, input) 334 | if err != nil { 335 | return fmt.Errorf("failed to delete key %s: %w", key, err) 336 | } 337 | return nil 338 | } 339 | 340 | func (s3 *S3) Exists(ctx context.Context, key string) bool { 341 | objName := s3.objName(key) 342 | 343 | s3.Logger.Debug("checking object existence", 344 | zap.String("key", objName), 345 | zap.String("bucket", s3.Bucket), 346 | ) 347 | 348 | input := &s3sdk.HeadObjectInput{ 349 | Bucket: aws.String(s3.Bucket), 350 | Key: aws.String(objName), 351 | } 352 | 353 | _, err := s3.Client.HeadObject(ctx, input) 354 | exists := err == nil 355 | 356 | s3.Logger.Debug("existence check completed", 357 | zap.String("key", objName), 358 | zap.Bool("exists", exists), 359 | ) 360 | 361 | return exists 362 | } 363 | 364 | func (s3 *S3) List(ctx context.Context, prefix string, recursive bool) ([]string, error) { 365 | var keys []string 366 | 367 | input := &s3sdk.ListObjectsV2Input{ 368 | Bucket: aws.String(s3.Bucket), 369 | Prefix: aws.String(s3.objName("")), 370 | } 371 | 372 | paginator := s3sdk.NewListObjectsV2Paginator(s3.Client, input) 373 | for paginator.HasMorePages() { 374 | result, err := paginator.NextPage(ctx) 375 | if err != nil { 376 | return nil, err 377 | } 378 | 379 | for _, obj := range result.Contents { 380 | keys = append(keys, aws.ToString(obj.Key)) 381 | } 382 | } 383 | 384 | return keys, nil 385 | } 386 | 387 | func (s3 *S3) Stat(ctx context.Context, key string) (certmagic.KeyInfo, error) { 388 | s3.Logger.Info(fmt.Sprintf("Stat: %v", s3.objName(key))) 389 | var ki certmagic.KeyInfo 390 | 391 | input := &s3sdk.HeadObjectInput{ 392 | Bucket: aws.String(s3.Bucket), 393 | Key: aws.String(s3.objName(key)), 394 | } 395 | 396 | result, err := s3.Client.HeadObject(ctx, input) 397 | if err != nil { 398 | var nsk *types.NoSuchKey 399 | if errors.As(err, &nsk) { 400 | return ki, fs.ErrNotExist 401 | } 402 | return ki, err 403 | } 404 | 405 | ki.Key = key 406 | ki.Size = aws.ToInt64(result.ContentLength) 407 | ki.Modified = aws.ToTime(result.LastModified) 408 | ki.IsTerminal = true 409 | return ki, nil 410 | } 411 | 412 | func (s3 *S3) objName(key string) string { 413 | prefix := strings.Trim(s3.Prefix, "/") 414 | key = strings.TrimLeft(key, "/") 415 | 416 | if prefix == "" { 417 | return key 418 | } 419 | return prefix + "/" + key 420 | } 421 | 422 | func (s3 *S3) objLockName(key string) string { 423 | return s3.objName(key) + ".lock" 424 | } 425 | 426 | // CertMagicStorage converts s to a certmagic.Storage instance. 427 | func (s3 *S3) CertMagicStorage() (certmagic.Storage, error) { 428 | return s3, nil 429 | } 430 | 431 | func parseBool(value string) (bool, error) { 432 | return strconv.ParseBool(value) 433 | } 434 | 435 | func (s3 *S3) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { 436 | for d.Next() { 437 | key := d.Val() 438 | var value string 439 | 440 | if !d.Args(&value) { 441 | return d.ArgErr() 442 | } 443 | 444 | switch key { 445 | case "host": 446 | s3.Host = value 447 | case "endpoint": 448 | s3.Endpoint = value 449 | case "insecure": 450 | parsed, err := parseBool(value) 451 | if err != nil { 452 | return d.Errf("invalid boolean value for 'insecure': %v", err) 453 | } 454 | s3.Insecure = parsed 455 | case "bucket": 456 | s3.Bucket = value 457 | case "region": 458 | s3.Region = value 459 | case "access_key": 460 | s3.AccessKey = value 461 | case "secret_key": 462 | s3.SecretKey = value 463 | case "profile": 464 | s3.Profile = value 465 | case "role_arn": 466 | s3.RoleARN = value 467 | case "prefix": 468 | s3.Prefix = value 469 | case "encryption_key": 470 | if value != "" && len(value) != 32 { 471 | return d.Errf("encryption_key must be exactly 32 bytes, got %d", len(value)) 472 | } 473 | s3.EncryptionKey = value 474 | case "use_path_style": 475 | parsed, err := parseBool(value) 476 | if err != nil { 477 | return d.Errf("invalid boolean value for 'use_path_style': %v", err) 478 | } 479 | s3.UsePathStyle = parsed 480 | default: 481 | return d.Errf("unknown configuration option: %s", key) 482 | } 483 | } 484 | 485 | if s3.Region == "" { 486 | s3.Region = "us-east-1" 487 | } 488 | if s3.Prefix == "" { 489 | s3.Prefix = "acme" 490 | } 491 | 492 | if s3.Bucket == "" { 493 | return d.Err("bucket is required") 494 | } 495 | 496 | if s3.Host != "" && s3.Endpoint != "" { 497 | return d.Err("cannot specify both 'host' and 'endpoint' options") 498 | } 499 | if s3.Host != "" && s3.Endpoint == "" { 500 | s3.Endpoint = "https://" + s3.Host 501 | } 502 | if s3.Endpoint != "" && !s3.UsePathStyle { 503 | s3.UsePathStyle = true 504 | } 505 | 506 | return nil 507 | } 508 | 509 | var ( 510 | _ caddy.Provisioner = (*S3)(nil) 511 | _ caddy.StorageConverter = (*S3)(nil) 512 | _ caddyfile.Unmarshaler = (*S3)(nil) 513 | ) 514 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 4 | cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo= 5 | dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= 6 | dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= 7 | dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= 8 | dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= 9 | git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= 10 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 11 | github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= 12 | github.com/aws/aws-sdk-go-v2 v1.36.6 h1:zJqGjVbRdTPojeCGWn5IR5pbJwSQSBh5RWFTQcEQGdU= 13 | github.com/aws/aws-sdk-go-v2 v1.36.6/go.mod h1:EYrzvCCN9CMUTa5+6lf6MM4tq3Zjp8UhSGR/cBsjai0= 14 | github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 h1:12SpdwU8Djs+YGklkinSSlcrPyj3H4VifVsKf78KbwA= 15 | github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11/go.mod h1:dd+Lkp6YmMryke+qxW/VnKyhMBDTYP41Q2Bb+6gNZgY= 16 | github.com/aws/aws-sdk-go-v2/config v1.29.18 h1:x4T1GRPnqKV8HMJOMtNktbpQMl3bIsfx8KbqmveUO2I= 17 | github.com/aws/aws-sdk-go-v2/config v1.29.18/go.mod h1:bvz8oXugIsH8K7HLhBv06vDqnFv3NsGDt2Znpk7zmOU= 18 | github.com/aws/aws-sdk-go-v2/credentials v1.17.71 h1:r2w4mQWnrTMJjOyIsZtGp3R3XGY3nqHn8C26C2lQWgA= 19 | github.com/aws/aws-sdk-go-v2/credentials v1.17.71/go.mod h1:E7VF3acIup4GB5ckzbKFrCK0vTvEQxOxgdq4U3vcMCY= 20 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.33 h1:D9ixiWSG4lyUBL2DDNK924Px9V/NBVpML90MHqyTADY= 21 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.33/go.mod h1:caS/m4DI+cij2paz3rtProRBI4s/+TCiWoaWZuQ9010= 22 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.37 h1:osMWfm/sC/L4tvEdQ65Gri5ZZDCUpuYJZbTTDrsn4I0= 23 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.37/go.mod h1:ZV2/1fbjOPr4G4v38G3Ww5TBT4+hmsK45s/rxu1fGy0= 24 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.37 h1:v+X21AvTb2wZ+ycg1gx+orkB/9U6L7AOp93R7qYxsxM= 25 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.37/go.mod h1:G0uM1kyssELxmJ2VZEfG0q2npObR3BAkF3c1VsfVnfs= 26 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= 27 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= 28 | github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.37 h1:XTZZ0I3SZUHAtBLBU6395ad+VOblE0DwQP6MuaNeics= 29 | github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.37/go.mod h1:Pi6ksbniAWVwu2S8pEzcYPyhUkAcLaufxN7PfAUQjBk= 30 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 h1:CXV68E2dNqhuynZJPB80bhPQwAKqBWVer887figW6Jc= 31 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4/go.mod h1:/xFi9KtvBXP97ppCz1TAEvU1Uf66qvid89rbem3wCzQ= 32 | github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.5 h1:M5/B8JUaCI8+9QD+u3S/f4YHpvqE9RpSkV3rf0Iks2w= 33 | github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.5/go.mod h1:Bktzci1bwdbpuLiu3AOksiNPMl/LLKmX1TWmqp2xbvs= 34 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.18 h1:vvbXsA2TVO80/KT7ZqCbx934dt6PY+vQ8hZpUZ/cpYg= 35 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.18/go.mod h1:m2JJHledjBGNMsLOF1g9gbAxprzq3KjC8e4lxtn+eWg= 36 | github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.18 h1:OS2e0SKqsU2LiJPqL8u9x41tKc6MMEHrWjLVLn3oysg= 37 | github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.18/go.mod h1:+Yrk+MDGzlNGxCXieljNeWpoZTCQUQVL+Jk9hGGJ8qM= 38 | github.com/aws/aws-sdk-go-v2/service/s3 v1.84.1 h1:RkHXU9jP0DptGy7qKI8CBGsUJruWz0v5IgwBa2DwWcU= 39 | github.com/aws/aws-sdk-go-v2/service/s3 v1.84.1/go.mod h1:3xAOf7tdKF+qbb+XpU+EPhNXAdun3Lu1RcDrj8KC24I= 40 | github.com/aws/aws-sdk-go-v2/service/sso v1.25.6 h1:rGtWqkQbPk7Bkwuv3NzpE/scwwL9sC1Ul3tn9x83DUI= 41 | github.com/aws/aws-sdk-go-v2/service/sso v1.25.6/go.mod h1:u4ku9OLv4TO4bCPdxf4fA1upaMaJmP9ZijGk3AAOC6Q= 42 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.4 h1:OV/pxyXh+eMA0TExHEC4jyWdumLxNbzz1P0zJoezkJc= 43 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.4/go.mod h1:8Mm5VGYwtm+r305FfPSuc+aFkrypeylGYhFim6XEPoc= 44 | github.com/aws/aws-sdk-go-v2/service/sts v1.34.1 h1:aUrLQwJfZtwv3/ZNG2xRtEen+NqI3iesuacjP51Mv1s= 45 | github.com/aws/aws-sdk-go-v2/service/sts v1.34.1/go.mod h1:3wFBZKoWnX3r+Sm7in79i54fBmNfwhdNdQuscCw7QIk= 46 | github.com/aws/smithy-go v1.22.5 h1:P9ATCXPMb2mPjYBgueqJNCA5S9UfktsW0tTxi+a7eqw= 47 | github.com/aws/smithy-go v1.22.5/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= 48 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 49 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 50 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 51 | github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= 52 | github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= 53 | github.com/caddyserver/caddy/v2 v2.10.1-0.20250724224000-b7ae39e906a0 h1:Jnoh4pAL4MuaSAFRzHLYUyIV6enVC4IMXqFcAQpOLA0= 54 | github.com/caddyserver/caddy/v2 v2.10.1-0.20250724224000-b7ae39e906a0/go.mod h1:DwprmUWd15f3L2G36GlgaZX98UxY/J9bevnafAXPsIc= 55 | github.com/caddyserver/certmagic v0.23.0 h1:CfpZ/50jMfG4+1J/u2LV6piJq4HOfO6ppOnOf7DkFEU= 56 | github.com/caddyserver/certmagic v0.23.0/go.mod h1:9mEZIWqqWoI+Gf+4Trh04MOVPD0tGSxtqsxg87hAIH4= 57 | github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA= 58 | github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4= 59 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 60 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 61 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 62 | github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 63 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 64 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 65 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 66 | github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 67 | github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= 68 | github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= 69 | github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= 70 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 71 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 72 | github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= 73 | github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= 74 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 75 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 76 | github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= 77 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 78 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 79 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 80 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 81 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 82 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 83 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 84 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 85 | github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= 86 | github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= 87 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 88 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 89 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 90 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 91 | github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= 92 | github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= 93 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 94 | github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= 95 | github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= 96 | github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= 97 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 98 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 99 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 100 | github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= 101 | github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= 102 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 103 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 104 | github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 105 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 106 | github.com/libdns/libdns v1.1.0 h1:9ze/tWvt7Df6sbhOJRB8jT33GHEHpEQXdtkE3hPthbU= 107 | github.com/libdns/libdns v1.1.0/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= 108 | github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= 109 | github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 110 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 111 | github.com/mholt/acmez/v3 v3.1.2 h1:auob8J/0FhmdClQicvJvuDavgd5ezwLBfKuYmynhYzc= 112 | github.com/mholt/acmez/v3 v3.1.2/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ= 113 | github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= 114 | github.com/miekg/dns v1.1.67 h1:kg0EHj0G4bfT5/oOys6HhZw4vmMlnoZ+gDu8tJ/AlI0= 115 | github.com/miekg/dns v1.1.67/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps= 116 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 117 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 118 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 119 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 120 | github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= 121 | github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= 122 | github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= 123 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 124 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 125 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 126 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 127 | github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 128 | github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= 129 | github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= 130 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 131 | github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= 132 | github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= 133 | github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 134 | github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE= 135 | github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= 136 | github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 137 | github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= 138 | github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= 139 | github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= 140 | github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= 141 | github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg= 142 | github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= 143 | github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= 144 | github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= 145 | github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= 146 | github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= 147 | github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= 148 | github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= 149 | github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= 150 | github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw= 151 | github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI= 152 | github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU= 153 | github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag= 154 | github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg= 155 | github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw= 156 | github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y= 157 | github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= 158 | github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q= 159 | github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ= 160 | github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I= 161 | github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0= 162 | github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ= 163 | github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk= 164 | github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 165 | github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= 166 | github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= 167 | github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= 168 | github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= 169 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 170 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 171 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 172 | github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= 173 | github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= 174 | github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= 175 | github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= 176 | github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= 177 | github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI= 178 | github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE= 179 | github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= 180 | github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= 181 | go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= 182 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 183 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 184 | go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= 185 | go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= 186 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 187 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 188 | go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 189 | go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 190 | go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U= 191 | go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ= 192 | go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= 193 | golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= 194 | golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 195 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 196 | golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 197 | golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= 198 | golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= 199 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 200 | golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 201 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 202 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 203 | golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= 204 | golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= 205 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 206 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 207 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 208 | golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 209 | golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 210 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 211 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 212 | golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 213 | golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= 214 | golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= 215 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 216 | golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 217 | golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 218 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 219 | golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= 220 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 221 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 222 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 223 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 224 | golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= 225 | golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 226 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 227 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 228 | golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 229 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 230 | golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 231 | golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= 232 | golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 233 | golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= 234 | golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= 235 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 236 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 237 | golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= 238 | golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= 239 | golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 240 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 241 | golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= 242 | golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= 243 | golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 244 | golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 245 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 246 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 247 | golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= 248 | golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= 249 | google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= 250 | google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= 251 | google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= 252 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 253 | google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 254 | google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 255 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 256 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 257 | google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 258 | google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 259 | google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= 260 | google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 261 | google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= 262 | google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= 263 | google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= 264 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 265 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 266 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 267 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 268 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 269 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 270 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 271 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 272 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 273 | grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= 274 | honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 275 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 276 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 277 | sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= 278 | sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= 279 | --------------------------------------------------------------------------------