forked from
willdot.net/cocoon
A fork of the Cocoon PDS but being made more distributed.
1package main
2
3import (
4 "crypto/ecdsa"
5 "crypto/elliptic"
6 "crypto/rand"
7 "encoding/json"
8 "fmt"
9 "os"
10 "time"
11
12 "github.com/bluesky-social/indigo/atproto/atcrypto"
13 "github.com/bluesky-social/indigo/atproto/syntax"
14 "github.com/haileyok/cocoon/internal/helpers"
15 "github.com/haileyok/cocoon/server"
16 _ "github.com/joho/godotenv/autoload"
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)
23
24var Version = "dev"
25
26func main() {
27 app := &cli.App{
28 Name: "cocoon",
29 Usage: "An atproto PDS",
30 Flags: []cli.Flag{
31 &cli.StringFlag{
32 Name: "addr",
33 Value: ":8080",
34 EnvVars: []string{"COCOON_ADDR"},
35 },
36 &cli.StringFlag{
37 Name: "db-name",
38 Value: "cocoon.db",
39 EnvVars: []string{"COCOON_DB_NAME"},
40 },
41 &cli.StringFlag{
42 Name: "did",
43 EnvVars: []string{"COCOON_DID"},
44 },
45 &cli.StringFlag{
46 Name: "hostname",
47 EnvVars: []string{"COCOON_HOSTNAME"},
48 },
49 &cli.StringFlag{
50 Name: "rotation-key-path",
51 EnvVars: []string{"COCOON_ROTATION_KEY_PATH"},
52 },
53 &cli.StringFlag{
54 Name: "jwk-path",
55 EnvVars: []string{"COCOON_JWK_PATH"},
56 },
57 &cli.StringFlag{
58 Name: "contact-email",
59 EnvVars: []string{"COCOON_CONTACT_EMAIL"},
60 },
61 &cli.StringSliceFlag{
62 Name: "relays",
63 EnvVars: []string{"COCOON_RELAYS"},
64 },
65 &cli.StringFlag{
66 Name: "admin-password",
67 EnvVars: []string{"COCOON_ADMIN_PASSWORD"},
68 },
69 &cli.StringFlag{
70 Name: "smtp-user",
71 EnvVars: []string{"COCOON_SMTP_USER"},
72 },
73 &cli.StringFlag{
74 Name: "smtp-pass",
75 EnvVars: []string{"COCOON_SMTP_PASS"},
76 },
77 &cli.StringFlag{
78 Name: "smtp-host",
79 EnvVars: []string{"COCOON_SMTP_HOST"},
80 },
81 &cli.StringFlag{
82 Name: "smtp-port",
83 EnvVars: []string{"COCOON_SMTP_PORT"},
84 },
85 &cli.StringFlag{
86 Name: "smtp-email",
87 EnvVars: []string{"COCOON_SMTP_EMAIL"},
88 },
89 &cli.StringFlag{
90 Name: "smtp-name",
91 EnvVars: []string{"COCOON_SMTP_NAME"},
92 },
93 &cli.BoolFlag{
94 Name: "s3-backups-enabled",
95 EnvVars: []string{"COCOON_S3_BACKUPS_ENABLED"},
96 },
97 &cli.BoolFlag{
98 Name: "s3-blobstore-enabled",
99 EnvVars: []string{"COCOON_S3_BLOBSTORE_ENABLED"},
100 },
101 &cli.StringFlag{
102 Name: "s3-region",
103 EnvVars: []string{"COCOON_S3_REGION"},
104 },
105 &cli.StringFlag{
106 Name: "s3-bucket",
107 EnvVars: []string{"COCOON_S3_BUCKET"},
108 },
109 &cli.StringFlag{
110 Name: "s3-endpoint",
111 EnvVars: []string{"COCOON_S3_ENDPOINT"},
112 },
113 &cli.StringFlag{
114 Name: "s3-access-key",
115 EnvVars: []string{"COCOON_S3_ACCESS_KEY"},
116 },
117 &cli.StringFlag{
118 Name: "s3-secret-key",
119 EnvVars: []string{"COCOON_S3_SECRET_KEY"},
120 },
121 &cli.StringFlag{
122 Name: "session-secret",
123 EnvVars: []string{"COCOON_SESSION_SECRET"},
124 },
125 &cli.StringFlag{
126 Name: "blockstore-variant",
127 EnvVars: []string{"COCOON_BLOCKSTORE_VARIANT"},
128 Value: "sqlite",
129 },
130 &cli.StringFlag{
131 Name: "fallback-proxy",
132 EnvVars: []string{"COCOON_FALLBACK_PROXY"},
133 },
134 },
135 Commands: []*cli.Command{
136 runServe,
137 runCreateRotationKey,
138 runCreatePrivateJwk,
139 runCreateInviteCode,
140 runResetPassword,
141 },
142 ErrWriter: os.Stdout,
143 Version: Version,
144 }
145
146 if err := app.Run(os.Args); err != nil {
147 fmt.Printf("Error: %v\n", err)
148 }
149}
150
151var runServe = &cli.Command{
152 Name: "run",
153 Usage: "Start the cocoon PDS",
154 Flags: []cli.Flag{},
155 Action: func(cmd *cli.Context) error {
156
157 s, err := server.New(&server.Args{
158 Addr: cmd.String("addr"),
159 DbName: cmd.String("db-name"),
160 Did: cmd.String("did"),
161 Hostname: cmd.String("hostname"),
162 RotationKeyPath: cmd.String("rotation-key-path"),
163 JwkPath: cmd.String("jwk-path"),
164 ContactEmail: cmd.String("contact-email"),
165 Version: Version,
166 Relays: cmd.StringSlice("relays"),
167 AdminPassword: cmd.String("admin-password"),
168 SmtpUser: cmd.String("smtp-user"),
169 SmtpPass: cmd.String("smtp-pass"),
170 SmtpHost: cmd.String("smtp-host"),
171 SmtpPort: cmd.String("smtp-port"),
172 SmtpEmail: cmd.String("smtp-email"),
173 SmtpName: cmd.String("smtp-name"),
174 S3Config: &server.S3Config{
175 BackupsEnabled: cmd.Bool("s3-backups-enabled"),
176 BlobstoreEnabled: cmd.Bool("s3-blobstore-enabled"),
177 Region: cmd.String("s3-region"),
178 Bucket: cmd.String("s3-bucket"),
179 Endpoint: cmd.String("s3-endpoint"),
180 AccessKey: cmd.String("s3-access-key"),
181 SecretKey: cmd.String("s3-secret-key"),
182 },
183 SessionSecret: cmd.String("session-secret"),
184 BlockstoreVariant: server.MustReturnBlockstoreVariant(cmd.String("blockstore-variant")),
185 FallbackProxy: cmd.String("fallback-proxy"),
186 })
187 if err != nil {
188 fmt.Printf("error creating cocoon: %v", err)
189 return err
190 }
191
192 if err := s.Serve(cmd.Context); err != nil {
193 fmt.Printf("error starting cocoon: %v", err)
194 return err
195 }
196
197 return nil
198 },
199}
200
201var runCreateRotationKey = &cli.Command{
202 Name: "create-rotation-key",
203 Usage: "creates a rotation key for your pds",
204 Flags: []cli.Flag{
205 &cli.StringFlag{
206 Name: "out",
207 Required: true,
208 Usage: "output file for your rotation key",
209 },
210 },
211 Action: func(cmd *cli.Context) error {
212 key, err := atcrypto.GeneratePrivateKeyK256()
213 if err != nil {
214 return err
215 }
216
217 bytes := key.Bytes()
218
219 if err := os.WriteFile(cmd.String("out"), bytes, 0644); err != nil {
220 return err
221 }
222
223 return nil
224 },
225}
226
227var runCreatePrivateJwk = &cli.Command{
228 Name: "create-private-jwk",
229 Usage: "creates a private jwk for your pds",
230 Flags: []cli.Flag{
231 &cli.StringFlag{
232 Name: "out",
233 Required: true,
234 Usage: "output file for your jwk",
235 },
236 },
237 Action: func(cmd *cli.Context) error {
238 privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
239 if err != nil {
240 return err
241 }
242
243 key, err := jwk.FromRaw(privKey)
244 if err != nil {
245 return err
246 }
247
248 kid := fmt.Sprintf("%d", time.Now().Unix())
249
250 if err := key.Set(jwk.KeyIDKey, kid); err != nil {
251 return err
252 }
253
254 b, err := json.Marshal(key)
255 if err != nil {
256 return err
257 }
258
259 if err := os.WriteFile(cmd.String("out"), b, 0644); err != nil {
260 return err
261 }
262
263 return nil
264 },
265}
266
267var runCreateInviteCode = &cli.Command{
268 Name: "create-invite-code",
269 Usage: "creates an invite code",
270 Flags: []cli.Flag{
271 &cli.StringFlag{
272 Name: "for",
273 Usage: "optional did to assign the invite code to",
274 },
275 &cli.IntFlag{
276 Name: "uses",
277 Usage: "number of times the invite code can be used",
278 Value: 1,
279 },
280 },
281 Action: func(cmd *cli.Context) error {
282 db, err := newDb()
283 if err != nil {
284 return err
285 }
286
287 forDid := "did:plc:123"
288 if cmd.String("for") != "" {
289 did, err := syntax.ParseDID(cmd.String("for"))
290 if err != nil {
291 return err
292 }
293
294 forDid = did.String()
295 }
296
297 uses := cmd.Int("uses")
298
299 code := fmt.Sprintf("%s-%s", helpers.RandomVarchar(8), helpers.RandomVarchar(8))
300
301 if err := db.Exec("INSERT INTO invite_codes (did, code, remaining_use_count) VALUES (?, ?, ?)", forDid, code, uses).Error; err != nil {
302 return err
303 }
304
305 fmt.Printf("New invite code created with %d uses: %s\n", uses, code)
306
307 return nil
308 },
309}
310
311var runResetPassword = &cli.Command{
312 Name: "reset-password",
313 Usage: "resets a password",
314 Flags: []cli.Flag{
315 &cli.StringFlag{
316 Name: "did",
317 Usage: "did of the user who's password you want to reset",
318 },
319 },
320 Action: func(cmd *cli.Context) error {
321 db, err := newDb()
322 if err != nil {
323 return err
324 }
325
326 didStr := cmd.String("did")
327 did, err := syntax.ParseDID(didStr)
328 if err != nil {
329 return err
330 }
331
332 newPass := fmt.Sprintf("%s-%s", helpers.RandomVarchar(12), helpers.RandomVarchar(12))
333 hashed, err := bcrypt.GenerateFromPassword([]byte(newPass), 10)
334 if err != nil {
335 return err
336 }
337
338 if err := db.Exec("UPDATE repos SET password = ? WHERE did = ?", hashed, did.String()).Error; err != nil {
339 return err
340 }
341
342 fmt.Printf("Password for %s has been reset to: %s", did.String(), newPass)
343
344 return nil
345 },
346}
347
348func newDb() (*gorm.DB, error) {
349 return gorm.Open(sqlite.Open("cocoon.db"), &gorm.Config{})
350}