all repos — honk @ ef186cbebeb6525a27f43d29be14abf2ebf2c1af

my fork of honk

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			accessToken := tokengen()
152			_, err := stmtSaveMastoAccessToken.Exec(app.ClientID, accessToken)
153			if err != nil {
154				elog.Println("oauth: failed to save masto access token", err)
155				rw.WriteHeader(http.StatusInternalServerError)
156				return
157			}
158			j := junk.New()
159			j["access_token"] = accessToken
160			j["token_type"] = "Bearer"
161			j["scope"] = app.Scopes
162			j["created_at"] = time.Now().UTC().Unix()
163			goodjunk(rw, j)
164		}
165	} else {
166		// gaslight the client
167		elog.Println("oauth: bad grant_type: must be authorization_code")
168		rw.WriteHeader(http.StatusBadRequest)
169		return
170	}
171}
172
173// https://docs.joinmastodon.org/methods/instance/#v2
174func instance(rw http.ResponseWriter, r *http.Request) {
175	j := junk.New()
176
177	var servername string
178	if err := getconfig("servername", &servername); err != nil {
179		http.Error(rw, "getting servername", http.StatusInternalServerError)
180		return
181	}
182
183	j["uri"] = servername
184	j["title"] = "honk"
185	j["description"] = "federated honk conveyance"
186	j["version"] = "develop"
187
188	thumbnail := junk.New()
189	thumbnail["url"] = fmt.Sprintf("https://%s/icon.png", servername)
190	j["thumbnail"] = thumbnail
191	j["languages"] = []string{"en"}
192
193	config := junk.New()
194
195	a := junk.New()
196	a["max_featured_tags"] = 10
197	config["accounts"] = a
198
199	s := junk.New()
200	s["max_characters"] = 5000
201	s["max_media_attachments"] = 1
202	s["characters_reserved_per_url"] = 23
203	config["statuses"] = s
204
205	m := junk.New()
206	m["supported_mime_types"] = []string{
207		"image/jpeg",
208		"image/png",
209		"image/gif",
210		"image/heic",
211		"image/heif",
212		"image/webp",
213		"image/avif",
214		"video/webm",
215		"video/mp4",
216		"video/quicktime",
217		"video/ogg",
218		"audio/wave",
219		"audio/wav",
220		"audio/x-wav",
221		"audio/x-pn-wave",
222		"audio/vnd.wave",
223		"audio/ogg",
224		"audio/vorbis",
225		"audio/mpeg",
226		"audio/mp3",
227		"audio/webm",
228		"audio/flac",
229		"audio/aac",
230		"audio/m4a",
231		"audio/x-m4a",
232		"audio/mp4",
233		"audio/3gpp",
234		"video/x-ms-asf",
235	}
236
237	m["image_size_limit"] = 10485760
238	m["image_matrix_limit"] = 16777216
239	m["video_size_limit"] = 41943040
240	m["video_frame_rate_limit"] = 60
241	m["video_matrix_limit"] = 2304000
242	j["media_attachments"] = m
243
244	goodjunk(rw, j)
245
246	return
247}
248
249// https://docs.joinmastodon.org/methods/accounts/#verify_credentials
250func verifycreds(rw http.ResponseWriter, r *http.Request) {
251	user, ok := somenumberedusers.Get(UserID(1))
252	if !ok {
253		elog.Fatalf("masto: no user number 1???")
254	}
255	j := junk.New()
256	j["id"] = user.ID
257	j["username"] = user.Name
258	j["acct"] = user.Name
259	j["display_name"] = user.Options.CustomDisplay
260	j["url"] = user.URL
261	j["note"] = user.HTAbout
262	j["avatar"] = user.Options.Avatar
263	j["header"] = user.Options.Banner
264	j["followers_count"] = 0
265	j["following_count"] = 0
266	j["statuses_count"] = getlocalhonkcount()
267
268	s := junk.New()
269	s["note"] = user.About
270	s["privacy"] = "public"
271	j["source"] = s
272
273	goodjunk(rw, j)
274}