2023-03-16 11:43:25 +01:00
|
|
|
package auth
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"encoding/json"
|
|
|
|
"io"
|
|
|
|
"net/http"
|
|
|
|
"net/url"
|
|
|
|
"strings"
|
|
|
|
|
2023-06-03 16:18:47 +02:00
|
|
|
"codeberg.org/pronounscc/pronouns.cc/backend/log"
|
|
|
|
"codeberg.org/pronounscc/pronouns.cc/backend/server"
|
2023-03-16 11:43:25 +01:00
|
|
|
"emperror.dev/errors"
|
|
|
|
"github.com/go-chi/render"
|
|
|
|
)
|
|
|
|
|
|
|
|
type FediResponse struct {
|
|
|
|
URL string `json:"url"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Server) getFediverseURL(w http.ResponseWriter, r *http.Request) error {
|
|
|
|
ctx := r.Context()
|
|
|
|
instance := r.FormValue("instance")
|
|
|
|
if instance == "" {
|
|
|
|
return server.APIError{Code: server.ErrBadRequest, Details: "Instance URL is empty"}
|
|
|
|
}
|
|
|
|
|
2023-04-18 02:15:45 +02:00
|
|
|
// Too many people tried using @username@fediverse.example despite the warning
|
|
|
|
if strings.Contains(instance, "@") {
|
|
|
|
return server.APIError{Code: server.ErrBadRequest, Details: "Instance URL should only be the base URL, without username"}
|
|
|
|
}
|
|
|
|
|
2023-03-16 11:43:25 +01:00
|
|
|
app, err := s.DB.FediverseApp(ctx, instance)
|
|
|
|
if err != nil {
|
|
|
|
return s.noAppFediverseURL(ctx, w, r, instance)
|
|
|
|
}
|
|
|
|
|
2023-03-25 15:54:09 +01:00
|
|
|
if app.Misskey() {
|
|
|
|
_, url, err := s.misskeyURL(ctx, app)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "generating misskey URL")
|
|
|
|
}
|
|
|
|
|
|
|
|
render.JSON(w, r, FediResponse{
|
|
|
|
URL: url,
|
|
|
|
})
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-03-16 11:43:25 +01:00
|
|
|
state, err := s.setCSRFState(r.Context())
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "setting CSRF state")
|
|
|
|
}
|
|
|
|
|
|
|
|
render.JSON(w, r, FediResponse{
|
|
|
|
URL: app.ClientConfig().AuthCodeURL(state),
|
|
|
|
})
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Server) noAppFediverseURL(ctx context.Context, w http.ResponseWriter, r *http.Request, instance string) error {
|
|
|
|
softwareName, err := nodeinfo(ctx, instance)
|
|
|
|
if err != nil {
|
|
|
|
log.Errorf("querying instance %q nodeinfo: %v", instance, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
switch softwareName {
|
2023-07-30 21:33:16 +02:00
|
|
|
case "misskey", "foundkey", "calckey", "firefish":
|
2023-03-25 03:27:40 +01:00
|
|
|
return s.noAppMisskeyURL(ctx, w, r, softwareName, instance)
|
2023-06-16 00:29:49 +02:00
|
|
|
case "mastodon", "pleroma", "akkoma", "pixelfed", "gotosocial":
|
2023-07-30 21:33:16 +02:00
|
|
|
case "glitchcafe", "hometown":
|
2023-06-15 17:23:24 +02:00
|
|
|
// plural.cafe (potentially other instances too?) runs Mastodon but changes the software name
|
2023-07-30 21:33:16 +02:00
|
|
|
// Hometown is a lightweight fork of Mastodon so we can just treat it the same
|
2023-06-15 17:23:24 +02:00
|
|
|
// changing it back to mastodon here for consistency
|
|
|
|
softwareName = "mastodon"
|
2023-03-16 11:43:25 +01:00
|
|
|
default:
|
|
|
|
return server.APIError{Code: server.ErrUnsupportedInstance}
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Debugf("creating application on mastodon-compatible instance %q", instance)
|
|
|
|
|
|
|
|
formData := url.Values{
|
|
|
|
"client_name": {"pronouns.cc (+" + s.BaseURL + ")"},
|
2023-03-16 15:50:39 +01:00
|
|
|
"redirect_uris": {s.BaseURL + "/auth/login/mastodon/" + instance},
|
2023-03-16 11:43:25 +01:00
|
|
|
"scopes": {"read:accounts"},
|
|
|
|
"website": {s.BaseURL},
|
|
|
|
}
|
|
|
|
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "POST", "https://"+instance+"/api/v1/apps", strings.NewReader(formData.Encode()))
|
|
|
|
if err != nil {
|
|
|
|
log.Errorf("creating POST apps request for %q: %v", instance, err)
|
|
|
|
return errors.Wrap(err, "creating POST apps request")
|
|
|
|
}
|
|
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
|
|
req.Header.Set("User-Agent", "pronouns.cc/"+server.Tag)
|
|
|
|
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
log.Errorf("sending POST apps request for %q: %v", instance, err)
|
|
|
|
return errors.Wrap(err, "sending POST apps request")
|
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
jb, err := io.ReadAll(resp.Body)
|
|
|
|
if err != nil {
|
|
|
|
log.Errorf("reading response for request: %v", err)
|
|
|
|
return errors.Wrap(err, "reading response")
|
|
|
|
}
|
|
|
|
|
|
|
|
var ma mastodonApplication
|
|
|
|
err = json.Unmarshal(jb, &ma)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "unmarshaling mastodon app")
|
|
|
|
}
|
|
|
|
|
|
|
|
app, err := s.DB.CreateFediverseApp(ctx, instance, softwareName, ma.ClientID, ma.ClientSecret)
|
|
|
|
if err != nil {
|
|
|
|
log.Errorf("saving app for %q: %v", instance, err)
|
|
|
|
return errors.Wrap(err, "creating app")
|
|
|
|
}
|
|
|
|
|
|
|
|
state, err := s.setCSRFState(r.Context())
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "setting CSRF state")
|
|
|
|
}
|
|
|
|
|
|
|
|
render.JSON(w, r, FediResponse{
|
|
|
|
URL: app.ClientConfig().AuthCodeURL(state),
|
|
|
|
})
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
type mastodonApplication struct {
|
|
|
|
Name string `json:"name"`
|
|
|
|
ClientID string `json:"client_id"`
|
|
|
|
ClientSecret string `json:"client_secret"`
|
|
|
|
}
|