Compare commits

..

No commits in common. "f4881e72675e87a9eae716436c3ac18a788d596d" and "81bdc7c705d5d21f62927167d5b2c8e4932c9570" have entirely different histories.

11 changed files with 148 additions and 94 deletions

View File

@ -38,4 +38,5 @@ 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,6 +8,7 @@ import (
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"bloat/config" "bloat/config"
"bloat/renderer" "bloat/renderer"
@ -46,8 +47,14 @@ 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, config.CustomCSS, config.SingleInstance, config.ClientWebsite, 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

@ -2,14 +2,18 @@
package mastodon package mastodon
import ( import (
"bytes"
"context" "context"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"io" "io"
"mime/multipart"
"net/http" "net/http"
"net/url" "net/url"
"os"
"path" "path"
"path/filepath"
"strings" "strings"
"github.com/tomnomnom/linkheader" "github.com/tomnomnom/linkheader"
@ -57,6 +61,83 @@ func (c *Client) doAPI(ctx context.Context, method string, uri string, params in
if err != nil { if err != nil {
return err return err
} }
} else if file, ok := params.(string); ok {
f, err := os.Open(file)
if err != nil {
return err
}
defer f.Close()
var buf bytes.Buffer
mw := multipart.NewWriter(&buf)
part, err := mw.CreateFormFile("file", filepath.Base(file))
if err != nil {
return err
}
_, err = io.Copy(part, f)
if err != nil {
return err
}
err = mw.Close()
if err != nil {
return err
}
req, err = http.NewRequest(method, u.String(), &buf)
if err != nil {
return err
}
ct = mw.FormDataContentType()
} else if file, ok := params.(*multipart.FileHeader); ok {
f, err := file.Open()
if err != nil {
return err
}
defer f.Close()
var buf bytes.Buffer
mw := multipart.NewWriter(&buf)
fname := filepath.Base(file.Filename)
err = mw.WriteField("description", fname)
if err != nil {
return err
}
part, err := mw.CreateFormFile("file", fname)
if err != nil {
return err
}
_, err = io.Copy(part, f)
if err != nil {
return err
}
err = mw.Close()
if err != nil {
return err
}
req, err = http.NewRequest(method, u.String(), &buf)
if err != nil {
return err
}
ct = mw.FormDataContentType()
} else if reader, ok := params.(io.Reader); ok {
var buf bytes.Buffer
mw := multipart.NewWriter(&buf)
part, err := mw.CreateFormFile("file", "upload")
if err != nil {
return err
}
_, err = io.Copy(part, reader)
if err != nil {
return err
}
err = mw.Close()
if err != nil {
return err
}
req, err = http.NewRequest(method, u.String(), &buf)
if err != nil {
return err
}
ct = mw.FormDataContentType()
} else if mr, ok := params.(*multipartRequest); ok { } else if mr, ok := params.(*multipartRequest); ok {
req, err = http.NewRequest(method, u.String(), mr.Data) req, err = http.NewRequest(method, u.String(), mr.Data)
if err != nil { if err != nil {
@ -138,16 +219,6 @@ func (c *Client) AuthenticateToken(ctx context.Context, authCode, redirectURI st
return c.authenticate(ctx, params) return c.authenticate(ctx, params)
} }
func (c *Client) RevokeToken(ctx context.Context) error {
params := url.Values{
"client_id": {c.config.ClientID},
"client_secret": {c.config.ClientSecret},
"token": {c.GetAccessToken(ctx)},
}
return c.doAPI(ctx, http.MethodPost, "/oauth/revoke", params, nil, nil)
}
func (c *Client) authenticate(ctx context.Context, params url.Values) error { func (c *Client) authenticate(ctx context.Context, params url.Values) error {
u, err := url.Parse(c.config.Server) u, err := url.Parse(c.config.Server)
if err != nil { if err != nil {

View File

@ -1,14 +1,12 @@
package mastodon package mastodon
import ( import (
"bytes"
"context" "context"
"fmt" "fmt"
"io" "io"
"mime/multipart" "mime/multipart"
"net/http" "net/http"
"net/url" "net/url"
"path/filepath"
"time" "time"
) )
@ -295,35 +293,30 @@ func (c *Client) Search(ctx context.Context, q string, qType string, limit int,
return &results, nil return &results, nil
} }
func (c *Client) UploadMediaFromMultipartFileHeader(ctx context.Context, fh *multipart.FileHeader) (*Attachment, error) { // UploadMedia upload a media attachment from a file.
f, err := fh.Open() func (c *Client) UploadMedia(ctx context.Context, file string) (*Attachment, error) {
if err != nil {
return nil, err
}
defer f.Close()
var buf bytes.Buffer
mw := multipart.NewWriter(&buf)
fname := filepath.Base(fh.Filename)
err = mw.WriteField("description", fname)
if err != nil {
return nil, err
}
part, err := mw.CreateFormFile("file", fname)
if err != nil {
return nil, err
}
_, err = io.Copy(part, f)
if err != nil {
return nil, err
}
err = mw.Close()
if err != nil {
return nil, err
}
params := &multipartRequest{Data: &buf, ContentType: mw.FormDataContentType()}
var attachment Attachment var attachment Attachment
err = c.doAPI(ctx, http.MethodPost, "/api/v1/media", params, &attachment, nil) err := c.doAPI(ctx, http.MethodPost, "/api/v1/media", file, &attachment, nil)
if err != nil {
return nil, err
}
return &attachment, nil
}
// UploadMediaFromReader uploads a media attachment from a io.Reader.
func (c *Client) UploadMediaFromReader(ctx context.Context, reader io.Reader) (*Attachment, error) {
var attachment Attachment
err := c.doAPI(ctx, http.MethodPost, "/api/v1/media", reader, &attachment, nil)
if err != nil {
return nil, err
}
return &attachment, nil
}
// UploadMediaFromReader uploads a media attachment from a io.Reader.
func (c *Client) UploadMediaFromMultipartFileHeader(ctx context.Context, fh *multipart.FileHeader) (*Attachment, error) {
var attachment Attachment
err := c.doAPI(ctx, http.MethodPost, "/api/v1/media", fh, &attachment, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -1,6 +1,7 @@
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"`
@ -27,7 +28,6 @@ type Settings struct {
AntiDopamineMode bool `json:"adm,omitempty"` AntiDopamineMode bool `json:"adm,omitempty"`
HideUnsupportedNotifs bool `json:"hun,omitempty"` HideUnsupportedNotifs bool `json:"hun,omitempty"`
CSS string `json:"css,omitempty"` CSS string `json:"css,omitempty"`
CSSHash string `json:"cssh,omitempty"`
} }
func NewSettings() *Settings { func NewSettings() *Settings {
@ -44,6 +44,5 @@ func NewSettings() *Settings {
AntiDopamineMode: false, AntiDopamineMode: false,
HideUnsupportedNotifs: false, HideUnsupportedNotifs: false,
CSS: "", CSS: "",
CSSHash: "",
} }
} }

View File

@ -34,8 +34,6 @@ 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),
}) })
@ -55,7 +53,6 @@ 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,8 +1,6 @@
package service package service
import ( import (
"crypto/sha256"
"encoding/base64"
"errors" "errors"
"fmt" "fmt"
"mime/multipart" "mime/multipart"
@ -842,6 +840,10 @@ 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
@ -857,14 +859,28 @@ 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
} }
@ -886,10 +902,6 @@ func (s *service) Signin(c *client, code string) (err error) {
return c.setSession(c.s) return c.setSession(c.s)
} }
func (s *service) Signout(c *client) (err error) {
return c.RevokeToken(c.ctx)
}
func (s *service) Post(c *client, content string, replyToID string, func (s *service) Post(c *client, content string, replyToID string,
format string, visibility string, isNSFW bool, format string, visibility string, isNSFW bool,
files []*multipart.FileHeader) (id string, err error) { files []*multipart.FileHeader) (id string, err error) {
@ -1016,19 +1028,9 @@ 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,15 +26,6 @@ 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()
@ -67,14 +58,14 @@ func NewHandler(s *service, verbose bool, staticDir string) http.Handler {
}(time.Now()) }(time.Now())
} }
h := c.w.Header() var ct string
switch rt { switch rt {
case HTML: case HTML:
h.Set("Content-Type", "text/html; charset=utf-8") ct = "text/html; charset=utf-8"
h.Set("Content-Security-Policy", csp)
case JSON: case JSON:
h.Set("Content-Type", "application/json") ct = "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 {
@ -82,13 +73,6 @@ 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)
@ -692,10 +676,6 @@ func NewHandler(s *service, verbose bool, staticDir string) http.Handler {
}, CSRF, HTML) }, CSRF, HTML)
signout := handle(func(c *client) error { signout := handle(func(c *client) error {
err := s.Signout(c)
if err != nil {
return err
}
c.unsetSession() c.unsetSession()
c.redirect("/") c.redirect("/")
return nil return nil

View File

@ -325,7 +325,7 @@ document.addEventListener("DOMContentLoaded", function() {
links[j].target = "_blank"; 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++) { for (var j = 0; j < links.length; j++) {
handleImgPreview(links[j]); handleImgPreview(links[j]);
} }

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="/static/{{.CustomCSS}}"> <link rel="stylesheet" href="{{.CustomCSS}}">
{{end}} {{end}}
{{if $.Ctx.FluorideMode}} {{if $.Ctx.FluorideMode}}
<script src="/static/fluoride.js"></script> <script src="/static/fluoride.js"></script>

View File

@ -16,6 +16,10 @@ 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)
} }