An atproto PDS written in Go

refactor: combine admin into main cocoon binary

Changed files
+169 -188
cmd
admin
cocoon
-186
cmd/admin/main.go
··· 1 - package main 2 - 3 - import ( 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/crypto" 13 - "github.com/bluesky-social/indigo/atproto/syntax" 14 - "github.com/haileyok/cocoon/internal/helpers" 15 - "github.com/lestrrat-go/jwx/v2/jwk" 16 - "github.com/urfave/cli/v2" 17 - "golang.org/x/crypto/bcrypt" 18 - "gorm.io/driver/sqlite" 19 - "gorm.io/gorm" 20 - ) 21 - 22 - func main() { 23 - app := cli.App{ 24 - Name: "admin", 25 - Commands: cli.Commands{ 26 - runCreateRotationKey, 27 - runCreatePrivateJwk, 28 - runCreateInviteCode, 29 - runResetPassword, 30 - }, 31 - ErrWriter: os.Stdout, 32 - } 33 - 34 - app.Run(os.Args) 35 - } 36 - 37 - var runCreateRotationKey = &cli.Command{ 38 - Name: "create-rotation-key", 39 - Usage: "creates a rotation key for your pds", 40 - Flags: []cli.Flag{ 41 - &cli.StringFlag{ 42 - Name: "out", 43 - Required: true, 44 - Usage: "output file for your rotation key", 45 - }, 46 - }, 47 - Action: func(cmd *cli.Context) error { 48 - key, err := crypto.GeneratePrivateKeyK256() 49 - if err != nil { 50 - return err 51 - } 52 - 53 - bytes := key.Bytes() 54 - 55 - if err := os.WriteFile(cmd.String("out"), bytes, 0644); err != nil { 56 - return err 57 - } 58 - 59 - return nil 60 - }, 61 - } 62 - 63 - var runCreatePrivateJwk = &cli.Command{ 64 - Name: "create-private-jwk", 65 - Usage: "creates a private jwk for your pds", 66 - Flags: []cli.Flag{ 67 - &cli.StringFlag{ 68 - Name: "out", 69 - Required: true, 70 - Usage: "output file for your jwk", 71 - }, 72 - }, 73 - Action: func(cmd *cli.Context) error { 74 - privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 75 - if err != nil { 76 - return err 77 - } 78 - 79 - key, err := jwk.FromRaw(privKey) 80 - if err != nil { 81 - return err 82 - } 83 - 84 - kid := fmt.Sprintf("%d", time.Now().Unix()) 85 - 86 - if err := key.Set(jwk.KeyIDKey, kid); err != nil { 87 - return err 88 - } 89 - 90 - b, err := json.Marshal(key) 91 - if err != nil { 92 - return err 93 - } 94 - 95 - if err := os.WriteFile(cmd.String("out"), b, 0644); err != nil { 96 - return err 97 - } 98 - 99 - return nil 100 - }, 101 - } 102 - 103 - var runCreateInviteCode = &cli.Command{ 104 - Name: "create-invite-code", 105 - Usage: "creates an invite code", 106 - Flags: []cli.Flag{ 107 - &cli.StringFlag{ 108 - Name: "for", 109 - Usage: "optional did to assign the invite code to", 110 - }, 111 - &cli.IntFlag{ 112 - Name: "uses", 113 - Usage: "number of times the invite code can be used", 114 - Value: 1, 115 - }, 116 - }, 117 - Action: func(cmd *cli.Context) error { 118 - db, err := newDb() 119 - if err != nil { 120 - return err 121 - } 122 - 123 - forDid := "did:plc:123" 124 - if cmd.String("for") != "" { 125 - did, err := syntax.ParseDID(cmd.String("for")) 126 - if err != nil { 127 - return err 128 - } 129 - 130 - forDid = did.String() 131 - } 132 - 133 - uses := cmd.Int("uses") 134 - 135 - code := fmt.Sprintf("%s-%s", helpers.RandomVarchar(8), helpers.RandomVarchar(8)) 136 - 137 - if err := db.Exec("INSERT INTO invite_codes (did, code, remaining_use_count) VALUES (?, ?, ?)", forDid, code, uses).Error; err != nil { 138 - return err 139 - } 140 - 141 - fmt.Printf("New invite code created with %d uses: %s\n", uses, code) 142 - 143 - return nil 144 - }, 145 - } 146 - 147 - var runResetPassword = &cli.Command{ 148 - Name: "reset-password", 149 - Usage: "resets a password", 150 - Flags: []cli.Flag{ 151 - &cli.StringFlag{ 152 - Name: "did", 153 - Usage: "did of the user who's password you want to reset", 154 - }, 155 - }, 156 - Action: func(cmd *cli.Context) error { 157 - db, err := newDb() 158 - if err != nil { 159 - return err 160 - } 161 - 162 - didStr := cmd.String("did") 163 - did, err := syntax.ParseDID(didStr) 164 - if err != nil { 165 - return err 166 - } 167 - 168 - newPass := fmt.Sprintf("%s-%s", helpers.RandomVarchar(12), helpers.RandomVarchar(12)) 169 - hashed, err := bcrypt.GenerateFromPassword([]byte(newPass), 10) 170 - if err != nil { 171 - return err 172 - } 173 - 174 - if err := db.Exec("UPDATE repos SET password = ? WHERE did = ?", hashed, did.String()).Error; err != nil { 175 - return err 176 - } 177 - 178 - fmt.Printf("Password for %s has been reset to: %s", did.String(), newPass) 179 - 180 - return nil 181 - }, 182 - } 183 - 184 - func newDb() (*gorm.DB, error) { 185 - return gorm.Open(sqlite.Open("cocoon.db"), &gorm.Config{}) 186 - }
+169 -2
cmd/cocoon/main.go
··· 1 1 package main 2 2 3 3 import ( 4 + "crypto/ecdsa" 5 + "crypto/elliptic" 6 + "crypto/rand" 7 + "encoding/json" 4 8 "fmt" 5 9 "os" 10 + "time" 6 11 12 + "github.com/bluesky-social/indigo/atproto/crypto" 13 + "github.com/bluesky-social/indigo/atproto/syntax" 14 + "github.com/haileyok/cocoon/internal/helpers" 7 15 "github.com/haileyok/cocoon/server" 8 16 _ "github.com/joho/godotenv/autoload" 17 + "github.com/lestrrat-go/jwx/v2/jwk" 9 18 "github.com/urfave/cli/v2" 19 + "golang.org/x/crypto/bcrypt" 20 + "gorm.io/driver/sqlite" 21 + "gorm.io/gorm" 10 22 ) 11 23 12 24 var Version = "dev" ··· 121 133 }, 122 134 }, 123 135 Commands: []*cli.Command{ 124 - run, 136 + runServe, 137 + runCreateRotationKey, 138 + runCreatePrivateJwk, 139 + runCreateInviteCode, 140 + runResetPassword, 125 141 }, 126 142 ErrWriter: os.Stdout, 127 143 Version: Version, ··· 132 148 } 133 149 } 134 150 135 - var run = &cli.Command{ 151 + var runServe = &cli.Command{ 136 152 Name: "run", 137 153 Usage: "Start the cocoon PDS", 138 154 Flags: []cli.Flag{}, ··· 177 193 return nil 178 194 }, 179 195 } 196 + 197 + var runCreateRotationKey = &cli.Command{ 198 + Name: "create-rotation-key", 199 + Usage: "creates a rotation key for your pds", 200 + Flags: []cli.Flag{ 201 + &cli.StringFlag{ 202 + Name: "out", 203 + Required: true, 204 + Usage: "output file for your rotation key", 205 + }, 206 + }, 207 + Action: func(cmd *cli.Context) error { 208 + key, err := crypto.GeneratePrivateKeyK256() 209 + if err != nil { 210 + return err 211 + } 212 + 213 + bytes := key.Bytes() 214 + 215 + if err := os.WriteFile(cmd.String("out"), bytes, 0644); err != nil { 216 + return err 217 + } 218 + 219 + return nil 220 + }, 221 + } 222 + 223 + var runCreatePrivateJwk = &cli.Command{ 224 + Name: "create-private-jwk", 225 + Usage: "creates a private jwk for your pds", 226 + Flags: []cli.Flag{ 227 + &cli.StringFlag{ 228 + Name: "out", 229 + Required: true, 230 + Usage: "output file for your jwk", 231 + }, 232 + }, 233 + Action: func(cmd *cli.Context) error { 234 + privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 235 + if err != nil { 236 + return err 237 + } 238 + 239 + key, err := jwk.FromRaw(privKey) 240 + if err != nil { 241 + return err 242 + } 243 + 244 + kid := fmt.Sprintf("%d", time.Now().Unix()) 245 + 246 + if err := key.Set(jwk.KeyIDKey, kid); err != nil { 247 + return err 248 + } 249 + 250 + b, err := json.Marshal(key) 251 + if err != nil { 252 + return err 253 + } 254 + 255 + if err := os.WriteFile(cmd.String("out"), b, 0644); err != nil { 256 + return err 257 + } 258 + 259 + return nil 260 + }, 261 + } 262 + 263 + var runCreateInviteCode = &cli.Command{ 264 + Name: "create-invite-code", 265 + Usage: "creates an invite code", 266 + Flags: []cli.Flag{ 267 + &cli.StringFlag{ 268 + Name: "for", 269 + Usage: "optional did to assign the invite code to", 270 + }, 271 + &cli.IntFlag{ 272 + Name: "uses", 273 + Usage: "number of times the invite code can be used", 274 + Value: 1, 275 + }, 276 + }, 277 + Action: func(cmd *cli.Context) error { 278 + db, err := newDb() 279 + if err != nil { 280 + return err 281 + } 282 + 283 + forDid := "did:plc:123" 284 + if cmd.String("for") != "" { 285 + did, err := syntax.ParseDID(cmd.String("for")) 286 + if err != nil { 287 + return err 288 + } 289 + 290 + forDid = did.String() 291 + } 292 + 293 + uses := cmd.Int("uses") 294 + 295 + code := fmt.Sprintf("%s-%s", helpers.RandomVarchar(8), helpers.RandomVarchar(8)) 296 + 297 + if err := db.Exec("INSERT INTO invite_codes (did, code, remaining_use_count) VALUES (?, ?, ?)", forDid, code, uses).Error; err != nil { 298 + return err 299 + } 300 + 301 + fmt.Printf("New invite code created with %d uses: %s\n", uses, code) 302 + 303 + return nil 304 + }, 305 + } 306 + 307 + var runResetPassword = &cli.Command{ 308 + Name: "reset-password", 309 + Usage: "resets a password", 310 + Flags: []cli.Flag{ 311 + &cli.StringFlag{ 312 + Name: "did", 313 + Usage: "did of the user who's password you want to reset", 314 + }, 315 + }, 316 + Action: func(cmd *cli.Context) error { 317 + db, err := newDb() 318 + if err != nil { 319 + return err 320 + } 321 + 322 + didStr := cmd.String("did") 323 + did, err := syntax.ParseDID(didStr) 324 + if err != nil { 325 + return err 326 + } 327 + 328 + newPass := fmt.Sprintf("%s-%s", helpers.RandomVarchar(12), helpers.RandomVarchar(12)) 329 + hashed, err := bcrypt.GenerateFromPassword([]byte(newPass), 10) 330 + if err != nil { 331 + return err 332 + } 333 + 334 + if err := db.Exec("UPDATE repos SET password = ? WHERE did = ?", hashed, did.String()).Error; err != nil { 335 + return err 336 + } 337 + 338 + fmt.Printf("Password for %s has been reset to: %s", did.String(), newPass) 339 + 340 + return nil 341 + }, 342 + } 343 + 344 + func newDb() (*gorm.DB, error) { 345 + return gorm.Open(sqlite.Open("cocoon.db"), &gorm.Config{}) 346 + }