this repo has no description
at main 265 lines 6.2 kB view raw
1package main 2 3import ( 4 "bytes" 5 "context" 6 "database/sql" 7 "encoding/base64" 8 "encoding/json" 9 "errors" 10 "fmt" 11 "io" 12 "log" 13 "net/http" 14 "os" 15 "text/template" 16 17 petname "github.com/dustinkirkland/golang-petname" 18 _ "github.com/mattn/go-sqlite3" 19 "github.com/sethvargo/go-envconfig" 20) 21 22type ServerConfig struct { 23 Database string `env:"DATABASE, default=./database.db"` 24 PDSHostName string `env:"PDS_HOSTNAME, default=pds.internal.ts.net"` 25 PDSAdminPassword string `env:"PDS_ADMIN_PASSWORD, required"` 26 Domain string `env:"DOMAIN, default=pyroclastic.cloud"` 27 Corefile string `env:"COREFILE, default=./Corefile"` 28} 29 30type createInviteResponse struct { 31 Code string `json:"code"` 32} 33 34type createAccountRequest struct { 35 Email string `json:"email"` 36 Handle string `json:"handle"` 37 Password string `json:"password"` 38 InviteCode string `json:"inviteCode"` 39} 40 41type createAccountResponse struct { 42 DID string `json:"did"` 43} 44 45func createInvite(server, password string) (string, error) { 46 url := fmt.Sprintf("https://%s/xrpc/com.atproto.server.createInviteCode", server) 47 48 requestBody := []byte(`{"useCount":1}`) 49 req, err := http.NewRequest("POST", url, bytes.NewBuffer(requestBody)) 50 if err != nil { 51 return "", err 52 } 53 req.Header.Set("Content-Type", "application/json") 54 req.Header.Add("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:"+password))) 55 56 client := &http.Client{} 57 resp, err := client.Do(req) 58 if err != nil { 59 return "", err 60 } 61 62 decoder := json.NewDecoder(resp.Body) 63 var createInvite createInviteResponse 64 err = decoder.Decode(&createInvite) 65 if err != nil { 66 return "", err 67 } 68 69 return createInvite.Code, nil 70} 71 72func createAccount(server, password, inviteCode, handle, email string) (string, error) { 73 url := fmt.Sprintf("https://%s/xrpc/com.atproto.server.createAccount", server) 74 75 createAccountRequestBody := createAccountRequest{ 76 Email: email, 77 Handle: handle, 78 Password: "password", 79 InviteCode: inviteCode, 80 } 81 requestBody, err := json.Marshal(createAccountRequestBody) 82 if err != nil { 83 return "", err 84 } 85 86 req, err := http.NewRequest("POST", url, bytes.NewBuffer(requestBody)) 87 if err != nil { 88 return "", err 89 } 90 req.Header.Set("Content-Type", "application/json") 91 req.Header.Add("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:"+password))) 92 93 client := &http.Client{} 94 resp, err := client.Do(req) 95 if err != nil { 96 return "", err 97 } 98 99 decoder := json.NewDecoder(resp.Body) 100 var createdAccount createAccountResponse 101 err = decoder.Decode(&createdAccount) 102 if err != nil { 103 return "", err 104 } 105 106 return createdAccount.DID, nil 107} 108 109type handlers struct { 110 config *ServerConfig 111 db *sql.DB 112} 113 114func (h *handlers) indexHandler(w http.ResponseWriter, r *http.Request) { 115 handle := r.PostFormValue("handle") 116 117 if handle == "" { 118 handle = petname.Generate(2, "-") 119 body := fmt.Sprintf(`<html><body><form method="post" action="/"><input type="text" name="handle" value="%s" /><input type="submit" /></form></body></html>`, handle) 120 io.WriteString(w, body) 121 return 122 } 123 124 inviteCode, err := createInvite(h.config.PDSHostName, h.config.PDSAdminPassword) 125 if err != nil { 126 io.WriteString(w, fmt.Sprintf("Error: %s", err)) 127 return 128 } 129 130 email := fmt.Sprintf("%s@%s", handle, h.config.Domain) 131 full_handle := fmt.Sprintf("%s.%s", handle, h.config.Domain) 132 133 did, err := createAccount(h.config.PDSHostName, h.config.PDSAdminPassword, inviteCode, full_handle, email) 134 if err != nil { 135 io.WriteString(w, fmt.Sprintf("Error: %s", err)) 136 return 137 } 138 139 _, err = h.db.Exec(`INSERT INTO handles (did, handle) VALUES (?, ?)`, &did, &handle) 140 if err != nil { 141 io.WriteString(w, fmt.Sprintf("Error: %s", err)) 142 return 143 } 144 145 if err = generateCorefile(h.config, h.db); err != nil { 146 io.WriteString(w, fmt.Sprintf("Error: %s", err)) 147 return 148 } 149 150 body := fmt.Sprintf(`<html><body><p>Created <span>%s</span> with handle <span>%s</span></p><p><a href="/">Back</a></body></html>`, did, full_handle) 151 152 io.WriteString(w, body) 153} 154 155func (h *handlers) newHandler(w http.ResponseWriter, r *http.Request) { 156 handle := r.PostFormValue("handle") 157 if handle == "" { 158 handle = "testy" 159 } 160 161 io.WriteString(w, fmt.Sprintf("Hello, %s!\n", handle)) 162} 163 164func generateCorefile(config *ServerConfig, db *sql.DB) error { 165 corefileTemplate := ` 166. { 167 log 168 errors 169 reload 10s 170 records {{ .Domain }} { 171 @ 60 IN TXT "TEST" 172{{ range .Records }} 173 _atproto.{{ .Handle }} 60 IN TXT "did={{ .DID }}"{{ end }} 174 } 175}` 176 177 corefile, err := template.New("corefile").Parse(corefileTemplate) 178 if err != nil { 179 log.Fatal(err) 180 } 181 182 type corefileValueRecord struct { 183 DID string 184 Handle string 185 } 186 type corefileValues struct { 187 Domain string 188 Records []corefileValueRecord 189 } 190 191 records := make([]corefileValueRecord, 0) 192 193 rows, err := db.Query("SELECT handle, did FROM handles") 194 if err != nil { 195 return err 196 } 197 defer rows.Close() 198 199 for rows.Next() { 200 var handle string 201 var did string 202 err = rows.Scan(&handle, &did) 203 if err != nil { 204 return err 205 } 206 records = append(records, corefileValueRecord{did, handle}) 207 } 208 err = rows.Err() 209 if err != nil { 210 return err 211 } 212 data := corefileValues{ 213 Domain: config.Domain, 214 Records: records, 215 } 216 217 output, err := os.Create(config.Corefile) 218 if err != nil { 219 return err 220 } 221 defer output.Close() 222 223 err = corefile.Execute(output, data) 224 if err != nil { 225 log.Fatal(err) 226 } 227 return nil 228} 229 230func main() { 231 ctx := context.Background() 232 233 var config ServerConfig 234 if err := envconfig.Process(ctx, &config); err != nil { 235 log.Fatal(err) 236 } 237 238 db, err := sql.Open("sqlite3", config.Database) 239 if err != nil { 240 log.Fatal(err) 241 } 242 defer db.Close() 243 244 _, err = db.Exec(`CREATE TABLE IF NOT EXISTS handles (did TEXT NOT NULL PRIMARY KEY, handle TEXT NOT NULL UNIQUE)`) 245 if err != nil { 246 log.Fatal(err) 247 } 248 249 h := handlers{ 250 config: &config, 251 db: db, 252 } 253 254 if err = generateCorefile(h.config, h.db); err != nil { 255 log.Fatal(err) 256 } 257 258 mux := http.NewServeMux() 259 mux.HandleFunc("/", h.indexHandler) 260 mux.HandleFunc("/new", h.newHandler) 261 if err = http.ListenAndServe(":3333", mux); !errors.Is(err, http.ErrServerClosed) { 262 log.Fatal(err) 263 } 264 265}