masto.go (view raw)
1package main
2
3import (
4 "database/sql"
5 "fmt"
6 "log"
7 "net/http"
8 "slices"
9 "strings"
10 "time"
11
12 "humungus.tedunangst.com/r/webs/junk"
13 "humungus.tedunangst.com/r/webs/login"
14)
15
16type MastoApp struct {
17 Name string `db:"clientname"`
18 RedirectURI string `db:"redirecturis"`
19 ClientID string `db:"clientid"`
20 ClientSecret string `db:"clientsecret"`
21 VapidKey string `db:"vapidkey"`
22 AuthToken string `db:"authtoken"`
23 Scopes string `db:"scopes"`
24}
25
26func showoauthlogin(rw http.ResponseWriter, r *http.Request) {
27 templinfo := make(map[string]interface{})
28 templinfo = getInfo(r)
29 templinfo["ClientID"] = r.URL.Query().Get("client_id")
30 templinfo["RedirectURI"] = r.URL.Query().Get("redirect_uri")
31
32 if err := readviews.Execute(rw, "oauthlogin.html", templinfo); err != nil {
33 elog.Println(err)
34 }
35}
36
37// https://docs.joinmastodon.org/methods/apps/#create
38func apiapps(rw http.ResponseWriter, r *http.Request) {
39 if err := r.ParseForm(); err != nil {
40 http.Error(rw, "invalid input", http.StatusUnprocessableEntity)
41 elog.Println(err)
42 return
43 }
44 clientName := r.Form.Get("client_name")
45 redirectURI := r.Form.Get("redirect_uris")
46 scopes := r.Form.Get("scopes")
47 website := r.Form.Get("website")
48 clientID := tokengen()
49 clientSecret := tokengen()
50 vapidKey := tokengen()
51
52 _, err := stmtSaveMastoApp.Exec(clientName, redirectURI, scopes, clientID, clientSecret, vapidKey, "")
53 if err != nil {
54 elog.Printf("error saving masto app: %v", err)
55 http.Error(rw, "error saving masto app", http.StatusUnprocessableEntity)
56 return
57 }
58
59 j := junk.New()
60 j["id"] = "19"
61 j["website"] = website
62 j["name"] = clientName
63 j["redirect_uri"] = redirectURI
64 j["client_id"] = clientID
65 j["client_secret"] = clientSecret
66 j["vapid_key"] = vapidKey
67
68 fmt.Println(j.ToString())
69 goodjunk(rw, j)
70}
71
72// https://docs.joinmastodon.org/methods/oauth/#authorize
73func oauthorize(rw http.ResponseWriter, r *http.Request) {
74 clientID := r.FormValue("client_id")
75 redirectURI := r.FormValue("redirect_uri")
76
77 if !checkClientID(clientID) {
78 elog.Println("oauth: no such client:", clientID)
79 rw.WriteHeader(http.StatusUnauthorized)
80 return
81 }
82
83 var nrw NotResponseWriter
84 login.LoginFunc(&nrw, r)
85
86 _, err := stmtSaveMastoAppToken.Exec(nrw.auth)
87 if err != nil {
88 elog.Println("oauth: failed to save masto app token", err)
89 rw.WriteHeader(http.StatusInternalServerError)
90 return
91 }
92
93 uri := fmt.Sprintf("%s?code=%s", redirectURI, nrw.auth)
94
95 log.Println("redirecting to", uri)
96 rw.Header().Set("Content-Type", "")
97 rw.Header().Set("Location", uri)
98 rw.WriteHeader(302)
99}
100
101// https://docs.joinmastodon.org/methods/oauth/#token
102func oauthtoken(rw http.ResponseWriter, r *http.Request) {
103 grantType := r.FormValue("grant_type")
104 code := r.FormValue("code")
105 clientID := r.FormValue("client_id")
106 clientSecret := r.FormValue("client_secret")
107 redirectURI := r.FormValue("redirect_uri")
108 gotScope := r.FormValue("scope")
109
110 if !checkClient(clientID, clientSecret) {
111 elog.Println("oauth: no such client:", clientID)
112 rw.WriteHeader(http.StatusBadRequest)
113 return
114 }
115
116 app := MastoApp{}
117 row := stmtGetMastoApp.QueryRowx(clientID)
118 err := row.StructScan(&app)
119 if err == sql.ErrNoRows {
120 elog.Printf("oauth: invalid client: %s\n", clientID)
121 rw.WriteHeader(http.StatusBadRequest)
122 return
123 }
124 log.Printf("%#v", app)
125
126 possibleScopes := strings.Split(app.Scopes, " ")
127 requestedScopes := strings.Split(gotScope, " ")
128 for _, scope := range requestedScopes {
129 if !slices.Contains(possibleScopes, scope) {
130 elog.Printf("oauth: invalid scope: %s", scope)
131 rw.WriteHeader(http.StatusBadRequest)
132 return
133 }
134 }
135
136 if app.Scopes != gotScope {
137 elog.Printf("oauth: bad scopes: got %s; want %s", gotScope, app.Scopes)
138 rw.WriteHeader(http.StatusBadRequest)
139 return
140 }
141
142 if app.RedirectURI != redirectURI {
143 elog.Println("oauth: incorrect redirect URI")
144 rw.WriteHeader(http.StatusBadRequest)
145 return
146 }
147
148 if grantType == "authorization_code" {
149 // idk if this is ok? should code be reset?
150 if app.AuthToken == code {
151 elog.Println("passed aauthtoken==code check")
152 accessToken := tokengen()
153 _, err := stmtSaveMastoAccessToken.Exec(app.ClientID, accessToken)
154 if err != nil {
155 elog.Println("oauth: failed to save masto access token", err)
156 rw.WriteHeader(http.StatusInternalServerError)
157 return
158 }
159 j := junk.New()
160 j["access_token"] = accessToken
161 j["token_type"] = "Bearer"
162 j["scope"] = app.Scopes
163 j["created_at"] = time.Now().UTC().Unix()
164 elog.Println("aboutta goodjunk XXX")
165 goodjunk(rw, j)
166 }
167 } else {
168 // gaslight the client
169 elog.Println("oauth: bad grant_type: must be authorization_code")
170 rw.WriteHeader(http.StatusBadRequest)
171 return
172 }
173}
174
175// https://docs.joinmastodon.org/methods/instance/#v2
176func instance(rw http.ResponseWriter, r *http.Request) {
177 j := junk.New()
178
179 var servername string
180 if err := getconfig("servername", &servername); err != nil {
181 http.Error(rw, "getting servername", http.StatusInternalServerError)
182 return
183 }
184
185 j["uri"] = servername
186 j["title"] = "honk"
187 j["description"] = "federated honk conveyance"
188 j["version"] = "develop"
189
190 thumbnail := junk.New()
191 thumbnail["url"] = fmt.Sprintf("https://%s/icon.png", servername)
192 j["thumbnail"] = thumbnail
193 j["languages"] = []string{"en"}
194
195 config := junk.New()
196
197 a := junk.New()
198 a["max_featured_tags"] = 10
199 config["accounts"] = a
200
201 s := junk.New()
202 s["max_characters"] = 5000
203 s["max_media_attachments"] = 1
204 s["characters_reserved_per_url"] = 23
205 config["statuses"] = s
206
207 m := junk.New()
208 m["supported_mime_types"] = []string{
209 "image/jpeg",
210 "image/png",
211 "image/gif",
212 "image/heic",
213 "image/heif",
214 "image/webp",
215 "image/avif",
216 "video/webm",
217 "video/mp4",
218 "video/quicktime",
219 "video/ogg",
220 "audio/wave",
221 "audio/wav",
222 "audio/x-wav",
223 "audio/x-pn-wave",
224 "audio/vnd.wave",
225 "audio/ogg",
226 "audio/vorbis",
227 "audio/mpeg",
228 "audio/mp3",
229 "audio/webm",
230 "audio/flac",
231 "audio/aac",
232 "audio/m4a",
233 "audio/x-m4a",
234 "audio/mp4",
235 "audio/3gpp",
236 "video/x-ms-asf",
237 }
238
239 m["image_size_limit"] = 10485760
240 m["image_matrix_limit"] = 16777216
241 m["video_size_limit"] = 41943040
242 m["video_frame_rate_limit"] = 60
243 m["video_matrix_limit"] = 2304000
244 j["media_attachments"] = m
245
246 goodjunk(rw, j)
247
248 return
249}