all repos — honk @ 9449ce73f0f818e7634da9f97fa32f59cb386dfc

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			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}
250
251// https://docs.joinmastodon.org/methods/apps/#verify_credentials
252func verifycreds(rw http.ResponseWriter, r *http.Request) {
253	app := r.Context().Value("app").(MastoApp)
254	j := junk.New()
255	j["name"] = app.Name
256	j["vapid_key"] = app.VapidKey
257	goodjunk(rw, j)
258}