Compare commits

...

14 commits

Author SHA1 Message Date
localhost_frssoft 44f8a72a76 fix resize banner 2023-11-06 18:05:24 +03:00
localhost_frssoft 4d240f9ca0 improvment reactions page 2023-11-06 16:45:04 +03:00
localhost_frssoft 6b3240dd9a remove unusable signup feature 2023-11-06 13:11:16 +03:00
localhost_frssoft 1c3cb0f358 footer sign-in changed 2023-11-06 12:46:18 +03:00
localhost_frssoft 53dd0c50ef removed session id from register 2023-11-06 12:17:09 +03:00
localhost_frssoft ab58d8a900 Merge remote-tracking branch 'upstream/master' into localhost_custom 2023-11-06 12:07:24 +03:00
r f4881e7267 Remove form-action CSP directive
Chrome incorrectly restricts the redirect URL to the sources specified
in the form-action value, which prevents the instance oauth page from
loading.
2023-10-25 06:40:34 +00:00
r 597cfc6b1e fluoride: Add image preview for profile image 2023-10-22 11:12:27 +00:00
r 9b053e32ec Fix replace syntax 2023-10-22 11:11:21 +00:00
r 67b13c71ba Use CSP header to restrict resource loading
This helps mitigate XSS exploits.
Users will have to save the settings again to make the custom CSS
work.
2023-10-15 15:53:44 +00:00
r ed521dd33d Restrict instance level custom CSS to static directory 2023-10-15 15:46:54 +00:00
r 927072e26a Remove unused session ID field 2023-10-07 10:20:11 +00:00
r c7f40c1e15 Cleanup oauth redirect URL generation 2023-10-07 09:19:56 +00:00
r d297eb5658 Use stricter cookie attributes 2023-10-07 09:11:43 +00:00
15 changed files with 74 additions and 234 deletions

View file

@ -38,5 +38,4 @@ post_formats=PlainText:text/plain,HTML:text/html,Markdown:text/markdown,BBCode:t
# single_instance=pl.mydomain.com # single_instance=pl.mydomain.com
# Path to custom CSS. Value can be a file path relative to the static directory. # 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 # custom_css=custom.css

View file

@ -8,7 +8,6 @@ import (
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"bloat/config" "bloat/config"
"bloat/renderer" "bloat/renderer"
@ -47,14 +46,8 @@ func main() {
errExit(err) 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, s := service.NewService(config.ClientName, config.ClientScope,
config.ClientWebsite, customCSS, config.SingleInstance, config.ClientWebsite, config.CustomCSS, config.SingleInstance,
config.PostFormats, renderer) config.PostFormats, renderer)
handler := service.NewHandler(s, *verbose, config.StaticDirectory) handler := service.NewHandler(s, *verbose, config.StaticDirectory)

View file

@ -92,50 +92,3 @@ func RegisterApp(ctx context.Context, appConfig *AppConfig) (*Application, error
return &app, nil 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
}

View file

@ -1,7 +1,6 @@
package model package model
type Session struct { type Session struct {
ID string `json:"id,omitempty"`
UserID string `json:"uid,omitempty"` UserID string `json:"uid,omitempty"`
Instance string `json:"ins,omitempty"` Instance string `json:"ins,omitempty"`
ClientID string `json:"cid,omitempty"` ClientID string `json:"cid,omitempty"`
@ -30,6 +29,7 @@ type Settings struct {
InstanceEmojiFilter string `json:"iemojfilter,omitempty"` InstanceEmojiFilter string `json:"iemojfilter,omitempty"`
AddReactionsFilter string `json:"reactionfilter,omitempty"` AddReactionsFilter string `json:"reactionfilter,omitempty"`
CSS string `json:"css,omitempty"` CSS string `json:"css,omitempty"`
CSSHash string `json:"cssh,omitempty"`
} }
func NewSettings() *Settings { func NewSettings() *Settings {
@ -48,5 +48,6 @@ func NewSettings() *Settings {
InstanceEmojiFilter: "", InstanceEmojiFilter: "",
AddReactionsFilter: "", AddReactionsFilter: "",
CSS: "", CSS: "",
CSSHash: "",
} }
} }

View file

@ -34,6 +34,8 @@ func (c *client) setSession(sess *model.Session) error {
} }
http.SetCookie(c.w, &http.Cookie{ http.SetCookie(c.w, &http.Cookie{
Name: "session", Name: "session",
Path: "/",
HttpOnly: true,
Value: sb.String(), Value: sb.String(),
Expires: time.Now().Add(365 * 24 * time.Hour), Expires: time.Now().Add(365 * 24 * time.Hour),
}) })
@ -53,6 +55,7 @@ func (c *client) getSession() (sess *model.Session, err error) {
func (c *client) unsetSession() { func (c *client) unsetSession() {
http.SetCookie(c.w, &http.Cookie{ http.SetCookie(c.w, &http.Cookie{
Name: "session", Name: "session",
Path: "/",
Value: "", Value: "",
Expires: time.Now(), Expires: time.Now(),
}) })

View file

@ -1,6 +1,8 @@
package service package service
import ( import (
"crypto/sha256"
"encoding/base64"
"errors" "errors"
"fmt" "fmt"
"mime/multipart" "mime/multipart"
@ -936,10 +938,6 @@ func (s *service) NewSession(c *client, instance string) (rurl string, sess *mod
instanceURL = "https://" + instance instanceURL = "https://" + instance
} }
sid, err := util.NewSessionID()
if err != nil {
return
}
csrf, err := util.NewCSRFToken() csrf, err := util.NewCSRFToken()
if err != nil { if err != nil {
return return
@ -955,28 +953,14 @@ func (s *service) NewSession(c *client, instance string) (rurl string, sess *mod
if err != nil { if err != nil {
return return
} }
rurl = app.AuthURI
sess = &model.Session{ sess = &model.Session{
ID: sid,
Instance: instance, Instance: instance,
ClientID: app.ClientID, ClientID: app.ClientID,
ClientSecret: app.ClientSecret, ClientSecret: app.ClientSecret,
CSRFToken: csrf, CSRFToken: csrf,
Settings: *model.NewSettings(), 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 return
} }
@ -998,71 +982,6 @@ func (s *service) Signin(c *client, code string) (err error) {
return c.setSession(c.s) 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) { func (s *service) Signout(c *client) (err error) {
return c.RevokeToken(c.ctx) return c.RevokeToken(c.ctx)
} }
@ -1213,9 +1132,19 @@ func (s *service) SaveSettings(c *client, settings *model.Settings) (err error)
default: default:
return errInvalidArgument return errInvalidArgument
} }
if len(settings.CSS) > 0 {
if len(settings.CSS) > 1<<20 { if len(settings.CSS) > 1<<20 {
return errInvalidArgument 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 = ""
}
c.s.Settings = *settings c.s.Settings = *settings
return c.setSession(c.s) return c.setSession(c.s)
} }

View file

@ -26,6 +26,15 @@ const (
CSRF 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 { func NewHandler(s *service, verbose bool, staticDir string) http.Handler {
r := mux.NewRouter() r := mux.NewRouter()
@ -58,14 +67,14 @@ func NewHandler(s *service, verbose bool, staticDir string) http.Handler {
}(time.Now()) }(time.Now())
} }
var ct string h := c.w.Header()
switch rt { switch rt {
case HTML: case HTML:
ct = "text/html; charset=utf-8" h.Set("Content-Type", "text/html; charset=utf-8")
h.Set("Content-Security-Policy", csp)
case JSON: case JSON:
ct = "application/json" h.Set("Content-Type", "application/json")
} }
c.w.Header().Add("Content-Type", ct)
err = c.authenticate(at, s.instance) err = c.authenticate(at, s.instance)
if err != nil { if err != nil {
@ -73,6 +82,13 @@ func NewHandler(s *service, verbose bool, staticDir string) http.Handler {
return 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) err = f(c)
if err != nil { if err != nil {
writeError(c, err, rt, req.Method == http.MethodGet) writeError(c, err, rt, req.Method == http.MethodGet)
@ -277,32 +293,6 @@ func NewHandler(s *service, verbose bool, staticDir string) http.Handler {
return nil return nil
}, NOAUTH, HTML) }, 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 { oauthCallback := handle(func(c *client) error {
q := c.r.URL.Query() q := c.r.URL.Query()
token := q.Get("code") token := q.Get("code")
@ -840,7 +830,6 @@ func NewHandler(s *service, verbose bool, staticDir string) http.Handler {
r.HandleFunc("/profile/delavatar", profileDelAvatar).Methods(http.MethodPost) r.HandleFunc("/profile/delavatar", profileDelAvatar).Methods(http.MethodPost)
r.HandleFunc("/profile/delbanner", profileDelBanner).Methods(http.MethodPost) r.HandleFunc("/profile/delbanner", profileDelBanner).Methods(http.MethodPost)
r.HandleFunc("/signin", signin).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("/oauth_callback", oauthCallback).Methods(http.MethodGet)
r.HandleFunc("/post", post).Methods(http.MethodPost) r.HandleFunc("/post", post).Methods(http.MethodPost)
r.HandleFunc("/like/{id}", like).Methods(http.MethodPost) r.HandleFunc("/like/{id}", like).Methods(http.MethodPost)

View file

@ -326,7 +326,7 @@ document.addEventListener("DOMContentLoaded", function() {
links[j].target = "_blank"; links[j].target = "_blank";
} }
var links = document.querySelectorAll(".status-media-container .img-link"); var links = document.querySelectorAll(".status-media-container .img-link, .user-profile-img-container .img-link");
for (var j = 0; j < links.length; j++) { for (var j = 0; j < links.length; j++) {
handleImgPreview(links[j]); handleImgPreview(links[j]);
} }

View file

@ -123,10 +123,19 @@ frame, body {
display: inline-block; display: inline-block;
} }
.pleroma-reactions form { .pleroma-reactions #emoj {
display: inline-block; display: inline-block;
} }
.pleroma-reactions form {
display: block;
}
.scrollable-emoji {
max-height: 240px;
overflow: scroll;
}
.status-action a { .status-action a {
display: inline-block; display: inline-block;
} }
@ -633,6 +642,7 @@ kbd {
.profile-banner { .profile-banner {
width: 100%; width: 100%;
max-height: 500px;
} }
.block-label, .block-label,

View file

@ -20,7 +20,7 @@
<title> {{if gt .Count 0}}({{.Count}}){{end}} {{.Title}} </title> <title> {{if gt .Count 0}}({{.Count}}){{end}} {{.Title}} </title>
<link rel="stylesheet" href="/static/style.css"> <link rel="stylesheet" href="/static/style.css">
{{if .CustomCSS}} {{if .CustomCSS}}
<link rel="stylesheet" href="{{.CustomCSS}}"> <link rel="stylesheet" href="/static/{{.CustomCSS}}">
{{end}} {{end}}
{{if $.Ctx.FluorideMode}} {{if $.Ctx.FluorideMode}}
<script src="/static/fluoride.js"></script> <script src="/static/fluoride.js"></script>

View file

@ -3,24 +3,26 @@
<div class="page-title"> Reactions </div> <div class="page-title"> Reactions </div>
{{$st_id := .ID}} {{$st_id := .ID}}
<div class="page-title"> Add reaction </div> <div class="page-title"> Add reaction </div>
<div class="scrollable-emoji" style="overflow-y: scroll; height:200px;"> <div class="scrollable-emoji">
<div class="pleroma-reactions"> <div class="pleroma-reactions">
{{$emoji_filter := $.Ctx.AddReactionsFilter}}
{{range $shortcode, $code := .ReactionEmojis}}
{{if Allowed_emoji_page $shortcode $emoji_filter}}
<form action="/react-with/{{$st_id}}?emoji={{$code}}" method="post" target="_self" title="{{$shortcode}}">
<input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}">
<input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">
<input type="submit" value="{{$code}}" class="pleroma-emoji">
</form>
{{end}}
{{end}}
<form action="/react-with/{{$st_id}}" method="post" target="_self" title="Custom akkoma reactions (ex. :blobemoji:)"> <form action="/react-with/{{$st_id}}" method="post" target="_self" title="Custom akkoma reactions (ex. :blobemoji:)">
<input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}"> <input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}">
<input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}"> <input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">
<input type="text" label="ex. :pleromaemoji:" id="akkoma-reaction" cols=30 name="akkoma-reaction"> <input type="text" placeholder=":blobfoxexample: or 🤗" id="akkoma-reaction" cols=30 name="akkoma-reaction" autofocus autocomplete="on">
<input type="submit" value="Send custom reaction" class="pleroma-emoji"> <input type="submit" value="Send custom reaction" class="pleroma-emoji">
<a href="/emojis">emoji list</a>
</form> </form>
{{$emoji_filter := $.Ctx.AddReactionsFilter}}
{{range $shortcode, $code := .ReactionEmojis}}
{{if Allowed_emoji_page $shortcode $emoji_filter}}
<form action="/react-with/{{$st_id}}?emoji={{$code}}" method="post" target="_self" >
<input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}">
<input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">
<input type="submit" value="{{$code}}" class="pleroma-emoji">
{{$shortcode}}
</form>
{{end}}
{{end}}
</div> </div>
</div> </div>

View file

@ -13,47 +13,12 @@
<button type="submit"> Signin </button> <button type="submit"> Signin </button>
</form> </form>
<details>
<summary>Sign up</summary>
<p>
<form class="signup-form" action="/signup" method="post">
Enter the domain name of your instance to continue
<br/>
<input type="text" name="instanceup" placeholder="example.com" required>
<br/>
Enter the reason why you want register
<br/>
<textarea type="text" name="reason" cols="80" rows="3" required></textarea>
<br/>
The desired username for the account
<br/>
<input type="text" name="username" placeholder="exampleusernick" required>
<br/>
The email address to be used for login
<br/>
<input type="text" name="email" placeholder="example@example.com" required>
<br/>
The password to be used for login (Please use strong password!)
<br/>
<input type="password" name="password" required>
<br/>
You agrees to the terms, conditions, and policies of the instance
<br/>
<input type="checkbox" value="true" name="agreement" required>
<input type="hidden" name="locale" value="en">
<br/>
<button type="submit"> Signup </button>
</form>
</p>
</details>
<p> <p>
See See
<a href="https://git.freesoftwareextremist.com/bloat" target="_blank">git.freesoftwareextremist.com/bloat</a> <a href="https://git.freesoftwareextremist.com/bloat" target="_blank">git.freesoftwareextremist.com/bloat</a>
for more details. for more details.
<br/> <br/>
<a href="https://gitea.phreedom.club/localhost_frssoft/bloat/src/branch/bloat-gts" target="_blank">bloat-gts fork branch</a> <a href="https://git.phreedom.club/localhost_frssoft/bloat/src/branch/localhost_custom" target="_blank">localhost_custom fork branch</a>
</P> </P>
{{template "footer.tmpl"}} {{template "footer.tmpl"}}

View file

@ -121,7 +121,7 @@
<div class="pleroma-reactions"> <div class="pleroma-reactions">
{{range .Pleroma.Reactions}} {{range .Pleroma.Reactions}}
{{$react := "react"}} {{if .Me}} {{$react = "unreact"}} {{end}} {{$react := "react"}} {{if .Me}} {{$react = "unreact"}} {{end}}
<form action="/{{$react}}-with/{{$st_id}}?emoji={{.Name}}" method="post" target="_self"> <form id="emoj" action="/{{$react}}-with/{{$st_id}}?emoji={{.Name}}" method="post" target="_self">
<input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}"> <input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}">
<input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}"> <input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">
{{if .Url}} {{if .Url}}

View file

@ -135,7 +135,7 @@
<summary>Banner</summary> <summary>Banner</summary>
<div class="user-profile-img-container"> <div class="user-profile-img-container">
<a class="img-link" href="{{.User.Header}}" target="_blank"> <a class="img-link" href="{{.User.Header}}" target="_blank">
<img class="profile-banner" src="{{.User.Header}}" alt="profile-header" height="500" loading="lazy" /> <img class="profile-banner" src="{{.User.Header}}" alt="profile-header" loading="lazy" />
</a> </a>
</div> </div>
</details> </details>

View file

@ -16,10 +16,6 @@ func NewRandID(n int) (string, error) {
return enc.EncodeToString(data), nil return enc.EncodeToString(data), nil
} }
func NewSessionID() (string, error) {
return NewRandID(24)
}
func NewCSRFToken() (string, error) { func NewCSRFToken() (string, error) {
return NewRandID(24) return NewRandID(24)
} }