forked from
smokesignal.events/localdev
this repo has no description
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}