diff --git a/bloat.conf b/bloat.conf index 20d5fef..e583988 100644 --- a/bloat.conf +++ b/bloat.conf @@ -38,4 +38,5 @@ post_formats=PlainText:text/plain,HTML:text/html,Markdown:text/markdown,BBCode:t # single_instance=pl.mydomain.com # Path to custom CSS. Value can be a file path relative to the static directory. +# or a URL starting with either "http://" or "https://". # custom_css=custom.css diff --git a/main.go b/main.go index 9e9ba4e..657912d 100644 --- a/main.go +++ b/main.go @@ -8,6 +8,7 @@ import ( "net/http" "os" "path/filepath" + "strings" "bloat/config" "bloat/renderer" @@ -46,8 +47,14 @@ func main() { errExit(err) } + customCSS := config.CustomCSS + if len(customCSS) > 0 && !strings.HasPrefix(customCSS, "http://") && + !strings.HasPrefix(customCSS, "https://") { + customCSS = "/static/" + customCSS + } + s := service.NewService(config.ClientName, config.ClientScope, - config.ClientWebsite, config.CustomCSS, config.SingleInstance, + config.ClientWebsite, customCSS, config.SingleInstance, config.PostFormats, renderer) handler := service.NewHandler(s, *verbose, config.StaticDirectory) diff --git a/mastodon/apps.go b/mastodon/apps.go index 652be6e..1ce894f 100644 --- a/mastodon/apps.go +++ b/mastodon/apps.go @@ -92,3 +92,50 @@ func RegisterApp(ctx context.Context, appConfig *AppConfig) (*Application, error return &app, nil } + +type AppAuth struct { + http.Client +} + +// RegisterApp make auth application and return app token. +func AuthApp(ctx context.Context, appConfig *Application, instance string) (*string, error) { + var appAuth AppAuth + params := url.Values{} + params.Set("client_id", appConfig.ClientID) + params.Set("client_secret", appConfig.ClientSecret) + params.Set("redirect_uris", "urn:ietf:wg:oauth:2.0:oob") + params.Set("grant_type", "client_credentials") + params.Set("scope", "read write follow") + + u, err := url.Parse("https://" + instance) + if err != nil { + return nil, err + } + u.Path = path.Join(u.Path, "/oauth/token") + + req, err := http.NewRequest(http.MethodPost, u.String(), strings.NewReader(params.Encode())) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + resp, err := appAuth.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, parseAPIError("bad request", resp) + } + + var res struct { + AccessToken string `json:"access_token"` + } + err = json.NewDecoder(resp.Body).Decode(&res) + if err != nil { + return nil, err + } + + return &res.AccessToken, nil +} diff --git a/model/session.go b/model/session.go index cb634ee..7c4ff27 100644 --- a/model/session.go +++ b/model/session.go @@ -1,6 +1,7 @@ package model type Session struct { + ID string `json:"id,omitempty"` UserID string `json:"uid,omitempty"` Instance string `json:"ins,omitempty"` ClientID string `json:"cid,omitempty"` @@ -29,7 +30,6 @@ type Settings struct { InstanceEmojiFilter string `json:"iemojfilter,omitempty"` AddReactionsFilter string `json:"reactionfilter,omitempty"` CSS string `json:"css,omitempty"` - CSSHash string `json:"cssh,omitempty"` } func NewSettings() *Settings { @@ -48,6 +48,5 @@ func NewSettings() *Settings { InstanceEmojiFilter: "", AddReactionsFilter: "", CSS: "", - CSSHash: "", } } diff --git a/service/client.go b/service/client.go index 49bc145..7c5c33c 100644 --- a/service/client.go +++ b/service/client.go @@ -33,11 +33,9 @@ func (c *client) setSession(sess *model.Session) error { return err } http.SetCookie(c.w, &http.Cookie{ - Name: "session", - Path: "/", - HttpOnly: true, - Value: sb.String(), - Expires: time.Now().Add(365 * 24 * time.Hour), + Name: "session", + Value: sb.String(), + Expires: time.Now().Add(365 * 24 * time.Hour), }) return nil } @@ -55,7 +53,6 @@ func (c *client) getSession() (sess *model.Session, err error) { func (c *client) unsetSession() { http.SetCookie(c.w, &http.Cookie{ Name: "session", - Path: "/", Value: "", Expires: time.Now(), }) diff --git a/service/service.go b/service/service.go index 7ac5624..6870f09 100644 --- a/service/service.go +++ b/service/service.go @@ -1,8 +1,6 @@ package service import ( - "crypto/sha256" - "encoding/base64" "errors" "fmt" "mime/multipart" @@ -938,6 +936,10 @@ func (s *service) NewSession(c *client, instance string) (rurl string, sess *mod instanceURL = "https://" + instance } + sid, err := util.NewSessionID() + if err != nil { + return + } csrf, err := util.NewCSRFToken() if err != nil { return @@ -953,14 +955,28 @@ func (s *service) NewSession(c *client, instance string) (rurl string, sess *mod if err != nil { return } - rurl = app.AuthURI sess = &model.Session{ + ID: sid, Instance: instance, ClientID: app.ClientID, ClientSecret: app.ClientSecret, CSRFToken: csrf, Settings: *model.NewSettings(), } + + u, err := url.Parse("/oauth/authorize") + if err != nil { + return + } + + q := make(url.Values) + q.Set("scope", "read write follow") + q.Set("client_id", app.ClientID) + q.Set("response_type", "code") + q.Set("redirect_uri", s.cwebsite+"/oauth_callback") + u.RawQuery = q.Encode() + + rurl = instanceURL + u.String() return } @@ -982,6 +998,71 @@ func (s *service) Signin(c *client, code string) (err error) { return c.setSession(c.s) } +func (s *service) NewSessionRegister(c *client, instance string, reason string, username string, email string, password string, agreement bool, locale string, registerCredintals mastodon.RegisterCredintals) (rurl string, sess *model.Session, err error) { + var instanceURL string + if strings.HasPrefix(instance, "https://") { + instanceURL = instance + instance = strings.TrimPrefix(instance, "https://") + } else { + instanceURL = "https://" + instance + } + + sid, err := util.NewSessionID() + if err != nil { + return + } + csrf, err := util.NewCSRFToken() + if err != nil { + return + } + + app, err := mastodon.RegisterApp(c.ctx, &mastodon.AppConfig{ + Server: instanceURL, + ClientName: s.cname, + Scopes: s.cscope, + Website: s.cwebsite, + RedirectURIs: s.cwebsite + "/oauth_callback", + }) + if err != nil { + return + } + registerCredintals.App = app + bearer, err := mastodon.AuthApp(c.ctx, app, instance) + if err != nil { + return + } + token, err := mastodon.RegisterAccount(c.ctx, instance, reason, username, email, password, agreement, locale, registerCredintals, *bearer) + + if err != nil { + return + } + sess = &model.Session{ + ID: sid, + Instance: instance, + UserID: "1", + ClientID: app.ClientID, + ClientSecret: app.ClientSecret, + AccessToken: *token, + CSRFToken: csrf, + Settings: *model.NewSettings(), + } + + u, err := url.Parse("/oauth/authorize") + if err != nil { + return + } + + q := make(url.Values) + q.Set("scope", "read write follow") + q.Set("client_id", app.ClientID) + q.Set("response_type", "code") + q.Set("redirect_uri", s.cwebsite+"/oauth_callback") + u.RawQuery = q.Encode() + + rurl = instanceURL + u.String() + return +} + func (s *service) Signout(c *client) (err error) { return c.RevokeToken(c.ctx) } @@ -1132,18 +1213,8 @@ func (s *service) SaveSettings(c *client, settings *model.Settings) (err error) default: return errInvalidArgument } - if len(settings.CSS) > 0 { - if len(settings.CSS) > 1<<20 { - return errInvalidArgument - } - // For some reason, browsers convert CRLF to LF before calculating - // the hash of the inline resources. - settings.CSS = strings.Replace(settings.CSS, "\x0d\x0a", "\x0a", -1) - - h := sha256.Sum256([]byte(settings.CSS)) - settings.CSSHash = base64.StdEncoding.EncodeToString(h[:]) - } else { - settings.CSSHash = "" + if len(settings.CSS) > 1<<20 { + return errInvalidArgument } c.s.Settings = *settings return c.setSession(c.s) diff --git a/service/transport.go b/service/transport.go index 2b579e4..c5bdde7 100644 --- a/service/transport.go +++ b/service/transport.go @@ -26,15 +26,6 @@ const ( CSRF ) -const csp = "default-src 'none';" + - " img-src *;" + - " media-src *;" + - " font-src *;" + - " child-src *;" + - " connect-src 'self';" + - " script-src 'self';" + - " style-src 'self'" - func NewHandler(s *service, verbose bool, staticDir string) http.Handler { r := mux.NewRouter() @@ -67,14 +58,14 @@ func NewHandler(s *service, verbose bool, staticDir string) http.Handler { }(time.Now()) } - h := c.w.Header() + var ct string switch rt { case HTML: - h.Set("Content-Type", "text/html; charset=utf-8") - h.Set("Content-Security-Policy", csp) + ct = "text/html; charset=utf-8" case JSON: - h.Set("Content-Type", "application/json") + ct = "application/json" } + c.w.Header().Add("Content-Type", ct) err = c.authenticate(at, s.instance) if err != nil { @@ -82,13 +73,6 @@ func NewHandler(s *service, verbose bool, staticDir string) http.Handler { return } - // Override the CSP header to allow custom CSS - if rt == HTML && len(c.s.Settings.CSS) > 0 && - len(c.s.Settings.CSSHash) > 0 { - v := fmt.Sprintf("%s 'sha256-%s'", csp, c.s.Settings.CSSHash) - h.Set("Content-Security-Policy", v) - } - err = f(c) if err != nil { writeError(c, err, rt, req.Method == http.MethodGet) @@ -293,6 +277,32 @@ func NewHandler(s *service, verbose bool, staticDir string) http.Handler { return nil }, NOAUTH, HTML) + signup := handle(func(c *client) error { + instance := c.r.FormValue("instanceup") + reason := c.r.FormValue("reason") + username := c.r.FormValue("username") + email := c.r.FormValue("email") + password := c.r.FormValue("password") + agreement := c.r.FormValue("agreement") == "true" + locale := c.r.FormValue("locale") + url, sess, err := s.NewSessionRegister(c, instance, reason, username, email, password, agreement, locale, mastodon.RegisterCredintals{ + Server: "https://"+instance, + Reason: reason, + Username: username, + Email: email, + Password: password, + Agreement: agreement, + Locale: locale, + }) + if err != nil { + return err + } + c.setSession(sess) + url = "/confirmation" + c.redirect(url) + return nil + }, NOAUTH, HTML) + oauthCallback := handle(func(c *client) error { q := c.r.URL.Query() token := q.Get("code") @@ -830,6 +840,7 @@ func NewHandler(s *service, verbose bool, staticDir string) http.Handler { r.HandleFunc("/profile/delavatar", profileDelAvatar).Methods(http.MethodPost) r.HandleFunc("/profile/delbanner", profileDelBanner).Methods(http.MethodPost) r.HandleFunc("/signin", signin).Methods(http.MethodPost) + r.HandleFunc("/signup", signup).Methods(http.MethodPost) r.HandleFunc("/oauth_callback", oauthCallback).Methods(http.MethodGet) r.HandleFunc("/post", post).Methods(http.MethodPost) r.HandleFunc("/like/{id}", like).Methods(http.MethodPost) diff --git a/static/fluoride.js b/static/fluoride.js index 70379de..ee3e3d8 100644 --- a/static/fluoride.js +++ b/static/fluoride.js @@ -326,7 +326,7 @@ document.addEventListener("DOMContentLoaded", function() { links[j].target = "_blank"; } - var links = document.querySelectorAll(".status-media-container .img-link, .user-profile-img-container .img-link"); + var links = document.querySelectorAll(".status-media-container .img-link"); for (var j = 0; j < links.length; j++) { handleImgPreview(links[j]); } diff --git a/templates/header.tmpl b/templates/header.tmpl index 7ebf65e..8a1b0ca 100644 --- a/templates/header.tmpl +++ b/templates/header.tmpl @@ -20,7 +20,7 @@
+
+ + +
See
git.freesoftwareextremist.com/bloat
for more details.
- localhost_custom fork branch
+ bloat-gts fork branch