package main import ( "database/sql" "fmt" "log" "net/http" "slices" "strings" "time" "humungus.tedunangst.com/r/webs/junk" "humungus.tedunangst.com/r/webs/login" ) type MastoApp struct { Name string `db:"clientname"` RedirectURI string `db:"redirecturis"` ClientID string `db:"clientid"` ClientSecret string `db:"clientsecret"` VapidKey string `db:"vapidkey"` AuthToken string `db:"authtoken"` Scopes string `db:"scopes"` } func showoauthlogin(rw http.ResponseWriter, r *http.Request) { templinfo := make(map[string]interface{}) templinfo = getInfo(r) templinfo["ClientID"] = r.URL.Query().Get("client_id") templinfo["RedirectURI"] = r.URL.Query().Get("redirect_uri") if err := readviews.Execute(rw, "oauthlogin.html", templinfo); err != nil { elog.Println(err) } } // https://docs.joinmastodon.org/methods/apps/#create func apiapps(rw http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { http.Error(rw, "invalid input", http.StatusUnprocessableEntity) elog.Println(err) return } clientName := r.Form.Get("client_name") redirectURI := r.Form.Get("redirect_uris") scopes := r.Form.Get("scopes") website := r.Form.Get("website") clientID := tokengen() clientSecret := tokengen() vapidKey := tokengen() _, err := stmtSaveMastoApp.Exec(clientName, redirectURI, scopes, clientID, clientSecret, vapidKey, "") if err != nil { elog.Printf("error saving masto app: %v", err) http.Error(rw, "error saving masto app", http.StatusUnprocessableEntity) return } j := junk.New() j["id"] = "19" j["website"] = website j["name"] = clientName j["redirect_uri"] = redirectURI j["client_id"] = clientID j["client_secret"] = clientSecret j["vapid_key"] = vapidKey fmt.Println(j.ToString()) goodjunk(rw, j) } // https://docs.joinmastodon.org/methods/oauth/#authorize func oauthorize(rw http.ResponseWriter, r *http.Request) { clientID := r.FormValue("client_id") redirectURI := r.FormValue("redirect_uri") if !checkClientID(clientID) { elog.Println("oauth: no such client:", clientID) rw.WriteHeader(http.StatusUnauthorized) return } var nrw NotResponseWriter login.LoginFunc(&nrw, r) _, err := stmtSaveMastoAppToken.Exec(nrw.auth) if err != nil { elog.Println("oauth: failed to save masto app token", err) rw.WriteHeader(http.StatusInternalServerError) return } uri := fmt.Sprintf("%s?code=%s", redirectURI, nrw.auth) log.Println("redirecting to", uri) rw.Header().Set("Content-Type", "") rw.Header().Set("Location", uri) rw.WriteHeader(302) } // https://docs.joinmastodon.org/methods/oauth/#token func oauthtoken(rw http.ResponseWriter, r *http.Request) { grantType := r.FormValue("grant_type") code := r.FormValue("code") clientID := r.FormValue("client_id") clientSecret := r.FormValue("client_secret") redirectURI := r.FormValue("redirect_uri") gotScope := r.FormValue("scope") if !checkClient(clientID, clientSecret) { elog.Println("oauth: no such client:", clientID) rw.WriteHeader(http.StatusBadRequest) return } app := MastoApp{} row := stmtGetMastoApp.QueryRowx(clientID) err := row.StructScan(&app) if err == sql.ErrNoRows { elog.Printf("oauth: invalid client: %s\n", clientID) rw.WriteHeader(http.StatusBadRequest) return } log.Printf("%#v", app) possibleScopes := strings.Split(app.Scopes, " ") requestedScopes := strings.Split(gotScope, " ") for _, scope := range requestedScopes { if !slices.Contains(possibleScopes, scope) { elog.Printf("oauth: invalid scope: %s", scope) rw.WriteHeader(http.StatusBadRequest) return } } if app.Scopes != gotScope { elog.Printf("oauth: bad scopes: got %s; want %s", gotScope, app.Scopes) rw.WriteHeader(http.StatusBadRequest) return } if app.RedirectURI != redirectURI { elog.Println("oauth: incorrect redirect URI") rw.WriteHeader(http.StatusBadRequest) return } if grantType == "authorization_code" { // idk if this is ok? should code be reset? if app.AuthToken == code { accessToken := tokengen() _, err := stmtSaveMastoAccessToken.Exec(app.ClientID, accessToken) if err != nil { elog.Println("oauth: failed to save masto access token", err) rw.WriteHeader(http.StatusInternalServerError) return } j := junk.New() j["access_token"] = accessToken j["token_type"] = "Bearer" j["scope"] = app.Scopes j["created_at"] = time.Now().UTC().Unix() goodjunk(rw, j) } } else { // gaslight the client elog.Println("oauth: bad grant_type: must be authorization_code") rw.WriteHeader(http.StatusBadRequest) return } } // https://docs.joinmastodon.org/methods/instance/#v2 func instance(rw http.ResponseWriter, r *http.Request) { j := junk.New() var servername string if err := getconfig("servername", &servername); err != nil { http.Error(rw, "getting servername", http.StatusInternalServerError) return } j["uri"] = servername j["title"] = "honk" j["description"] = "federated honk conveyance" j["version"] = "develop" thumbnail := junk.New() thumbnail["url"] = fmt.Sprintf("https://%s/icon.png", servername) j["thumbnail"] = thumbnail j["languages"] = []string{"en"} config := junk.New() a := junk.New() a["max_featured_tags"] = 10 config["accounts"] = a s := junk.New() s["max_characters"] = 5000 s["max_media_attachments"] = 1 s["characters_reserved_per_url"] = 23 config["statuses"] = s m := junk.New() m["supported_mime_types"] = []string{ "image/jpeg", "image/png", "image/gif", "image/heic", "image/heif", "image/webp", "image/avif", "video/webm", "video/mp4", "video/quicktime", "video/ogg", "audio/wave", "audio/wav", "audio/x-wav", "audio/x-pn-wave", "audio/vnd.wave", "audio/ogg", "audio/vorbis", "audio/mpeg", "audio/mp3", "audio/webm", "audio/flac", "audio/aac", "audio/m4a", "audio/x-m4a", "audio/mp4", "audio/3gpp", "video/x-ms-asf", } m["image_size_limit"] = 10485760 m["image_matrix_limit"] = 16777216 m["video_size_limit"] = 41943040 m["video_frame_rate_limit"] = 60 m["video_matrix_limit"] = 2304000 j["media_attachments"] = m goodjunk(rw, j) return } // https://docs.joinmastodon.org/methods/accounts/#verify_credentials func verifycreds(rw http.ResponseWriter, r *http.Request) { user, ok := somenumberedusers.Get(UserID(1)) if !ok { elog.Fatalf("masto: no user number 1???") } j := junk.New() j["id"] = user.ID j["username"] = user.Name j["acct"] = user.Name j["display_name"] = user.Options.CustomDisplay j["url"] = user.URL j["note"] = user.HTAbout j["avatar"] = user.Options.Avatar j["header"] = user.Options.Banner j["followers_count"] = 0 j["following_count"] = 0 j["statuses_count"] = getlocalhonkcount() s := junk.New() s["note"] = user.About s["privacy"] = "public" j["source"] = s goodjunk(rw, j) }