diff --git a/activityPub.go b/activityPub.go index 1df5462..4e3e6fc 100644 --- a/activityPub.go +++ b/activityPub.go @@ -201,6 +201,7 @@ func (a *goBlog) apCheckActivityPubReply(p *post) { } func (a *goBlog) apHandleInbox(w http.ResponseWriter, r *http.Request) { + // Get blog blogName := chi.URLParam(r, "blog") blog, ok := a.cfg.Blogs[blogName] if !ok || blog == nil { @@ -215,8 +216,7 @@ func (a *goBlog) apHandleInbox(w http.ResponseWriter, r *http.Request) { return } // Parse activity - limit := int64(10 * 1000 * 1000) // 10 MB - body, err := io.ReadAll(io.LimitReader(r.Body, limit)) + body, err := io.ReadAll(r.Body) if err != nil { a.serveError(w, r, "Failed to read body", http.StatusBadRequest) return diff --git a/activityPubTools.go b/activityPubTools.go index 74c4303..626a520 100644 --- a/activityPubTools.go +++ b/activityPubTools.go @@ -10,6 +10,7 @@ import ( "github.com/carlmjohnson/requests" "github.com/go-chi/chi/v5" + "go.goblog.app/app/pkgs/bodylimit" ) func (a *goBlog) apRemoteFollow(w http.ResponseWriter, r *http.Request) { @@ -41,13 +42,17 @@ func (a *goBlog) apRemoteFollow(w http.ResponseWriter, r *http.Request) { Links []*webfingerLinkType `json:"links"` } webfinger := &webfingerType{} - err := requests.URL(fmt.Sprintf("https://%s/.well-known/webfinger?resource=acct:%s@%s", instance, user, instance)). - Client(a.httpClient). - Handle(func(resp *http.Response) error { - defer resp.Body.Close() - return json.NewDecoder(io.LimitReader(resp.Body, 1000*1000)).Decode(webfinger) - }). - Fetch(r.Context()) + pr, pw := io.Pipe() + go func() { + err := requests. + URL(fmt.Sprintf("https://%s/.well-known/webfinger?resource=acct:%s@%s", instance, user, instance)). + Client(a.httpClient). + ToWriter(pw). + Fetch(r.Context()) + _ = pw.CloseWithError(err) + }() + err := json.NewDecoder(io.LimitReader(pr, 100*bodylimit.KB)).Decode(webfinger) + _ = pr.CloseWithError(err) if err != nil { a.serveError(w, r, "Failed to query webfinger", http.StatusInternalServerError) return diff --git a/authentication.go b/authentication.go index bfecd6e..00d57b6 100644 --- a/authentication.go +++ b/authentication.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/pquerna/otp/totp" + "go.goblog.app/app/pkgs/bodylimit" "go.goblog.app/app/pkgs/bufferpool" "go.goblog.app/app/pkgs/contenttype" ) @@ -51,8 +52,6 @@ func (a *goBlog) authMiddleware(next http.Handler) http.Handler { next.ServeHTTP(w, r) return } - // Remember to close body - defer r.Body.Close() // Encode original request headerBuffer, bodyBuffer := bufferpool.Get(), bufferpool.Get() defer bufferpool.Put(headerBuffer, bodyBuffer) @@ -62,13 +61,14 @@ func (a *goBlog) authMiddleware(next http.Handler) http.Handler { _ = headerEncoder.Close() // Encode body bodyEncoder := base64.NewEncoder(base64.StdEncoding, bodyBuffer) - limit := int64(3 * 1000 * 1000) // 3 MB + limit := 3 * bodylimit.MB written, _ := io.Copy(bodyEncoder, io.LimitReader(r.Body, limit)) if written == 0 { // Maybe it's a form _ = r.ParseForm() // Encode form - written, _ = io.Copy(bodyEncoder, strings.NewReader(r.Form.Encode())) + sw, _ := io.WriteString(bodyEncoder, r.Form.Encode()) + written = int64(sw) } bodyEncoder.Close() if written >= limit { diff --git a/blogroll.go b/blogroll.go index aa3e35e..8afe668 100644 --- a/blogroll.go +++ b/blogroll.go @@ -65,23 +65,27 @@ func (a *goBlog) serveBlogrollExport(w http.ResponseWriter, r *http.Request) { } func (a *goBlog) getBlogrollOutlines(blog string) ([]*opml.Outline, error) { + // Get config config := a.cfg.Blogs[blog].Blogroll + // Check cache if cache := a.db.loadOutlineCache(blog); cache != nil { return cache, nil } - rb := requests.URL(config.Opml).Client(a.httpClient) + // Make request and parse OPML + pr, pw := io.Pipe() + rb := requests.URL(config.Opml).Client(a.httpClient).ToWriter(pw) if config.AuthHeader != "" && config.AuthValue != "" { rb.Header(config.AuthHeader, config.AuthValue) } - var o *opml.OPML - err := rb.Handle(func(r *http.Response) (err error) { - defer r.Body.Close() - o, err = opml.Parse(r.Body) - return - }).Fetch(context.Background()) + go func() { + _ = pw.CloseWithError(rb.Fetch(context.Background())) + }() + o, err := opml.Parse(pr) + _ = pr.CloseWithError(err) if err != nil { return nil, err } + // Filter and sort outlines := o.Outlines if len(config.Categories) > 0 { filtered := []*opml.Outline{} @@ -97,6 +101,7 @@ func (a *goBlog) getBlogrollOutlines(blog string) ([]*opml.Outline, error) { } else { outlines = sortOutlines(outlines) } + // Cache a.db.cacheOutlines(blog, outlines) return outlines, nil } diff --git a/captcha.go b/captcha.go index 545fef2..9095b89 100644 --- a/captcha.go +++ b/captcha.go @@ -10,6 +10,7 @@ import ( "time" "github.com/dchest/captcha" + "go.goblog.app/app/pkgs/bodylimit" "go.goblog.app/app/pkgs/bufferpool" "go.goblog.app/app/pkgs/contenttype" ) @@ -40,8 +41,6 @@ func (a *goBlog) captchaMiddleware(next http.Handler) http.Handler { next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), captchaSolvedKey, true))) return } - // Remember to close body - defer r.Body.Close() // Get captcha ID captchaId := "" if sesCaptchaId, ok := ses.Values["captchaid"]; ok { @@ -64,13 +63,14 @@ func (a *goBlog) captchaMiddleware(next http.Handler) http.Handler { _ = headerEncoder.Close() // Encode body bodyEncoder := base64.NewEncoder(base64.StdEncoding, bodyBuffer) - limit := int64(1000 * 1000) // 1 MB + limit := 3 * bodylimit.MB written, _ := io.Copy(bodyEncoder, io.LimitReader(r.Body, limit)) if written == 0 { // Maybe it's a form _ = r.ParseForm() // Encode form - written, _ = io.Copy(bodyEncoder, strings.NewReader(r.Form.Encode())) + sw, _ := io.WriteString(bodyEncoder, r.Form.Encode()) + written = int64(sw) } bodyEncoder.Close() if written >= limit { diff --git a/go.mod b/go.mod index d8e365d..6ae27a1 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( git.jlel.se/jlelse/goldmark-mark v0.0.0-20210522162520-9788c89266a4 git.jlel.se/jlelse/template-strings v0.0.0-20220211095702-c012e3b5045b github.com/PuerkitoBio/goquery v1.8.0 - github.com/alecthomas/chroma/v2 v2.4.0 + github.com/alecthomas/chroma/v2 v2.5.0 github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de github.com/c2h5oh/datasize v0.0.0-20220606134207-859f65c6625b github.com/carlmjohnson/requests v0.23.2 diff --git a/go.sum b/go.sum index 745584e..4fd5897 100644 --- a/go.sum +++ b/go.sum @@ -61,10 +61,10 @@ github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0g github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI= github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= -github.com/alecthomas/assert/v2 v2.2.0 h1:f6L/b7KE2bfA+9O4FL3CM/xJccDEwPVYd5fALBiuwvw= -github.com/alecthomas/chroma/v2 v2.4.0 h1:Loe2ZjT5x3q1bcWwemqyqEi8p11/IV/ncFCeLYDpWC4= -github.com/alecthomas/chroma/v2 v2.4.0/go.mod h1:6kHzqF5O6FUSJzBXW7fXELjb+e+7OXW4UpoPqMO7IBQ= -github.com/alecthomas/repr v0.1.0 h1:ENn2e1+J3k09gyj2shc0dHr/yjaWSHRlrJ4DPMevDqE= +github.com/alecthomas/assert/v2 v2.2.1 h1:XivOgYcduV98QCahG8T5XTezV5bylXe+lBxLG2K2ink= +github.com/alecthomas/chroma/v2 v2.5.0 h1:CQCdj1BiBV17sD4Bd32b/Bzuiq/EqoNTrnIhyQAZ+Rk= +github.com/alecthomas/chroma/v2 v2.5.0/go.mod h1:yrkMI9807G1ROx13fhe1v6PN2DDeaR73L3d+1nmYQtw= +github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk= github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c= github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA= diff --git a/httpRouters.go b/httpRouters.go index aec1c6a..5eef059 100644 --- a/httpRouters.go +++ b/httpRouters.go @@ -3,6 +3,7 @@ package main import ( "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" + "go.goblog.app/app/pkgs/bodylimit" ) // Login @@ -16,8 +17,8 @@ func (a *goBlog) loginRouter(r chi.Router) { func (a *goBlog) micropubRouter(r chi.Router) { r.Use(a.checkIndieAuth) r.Get("/", a.serveMicropubQuery) - r.Post("/", a.serveMicropubPost) - r.Post(micropubMediaSubPath, a.serveMicropubMedia) + r.With(bodylimit.BodyLimit(10*bodylimit.MB)).Post("/", a.serveMicropubPost) + r.With(bodylimit.BodyLimit(30*bodylimit.MB)).Post(micropubMediaSubPath, a.serveMicropubMedia) } // IndieAuth @@ -25,10 +26,10 @@ func (a *goBlog) indieAuthRouter(r chi.Router) { r.Route(indieAuthPath, func(r chi.Router) { r.Get("/", a.indieAuthRequest) r.With(a.authMiddleware).Post("/accept", a.indieAuthAccept) - r.Post("/", a.indieAuthVerificationAuth) - r.Post(indieAuthTokenSubpath, a.indieAuthVerificationToken) + r.With(bodylimit.BodyLimit(100*bodylimit.KB)).Post("/", a.indieAuthVerificationAuth) + r.With(bodylimit.BodyLimit(100*bodylimit.KB)).Post(indieAuthTokenSubpath, a.indieAuthVerificationToken) r.Get(indieAuthTokenSubpath, a.indieAuthTokenVerification) - r.Post(indieAuthTokenRevocationSubpath, a.indieAuthTokenRevokation) + r.With(bodylimit.BodyLimit(100*bodylimit.KB)).Post(indieAuthTokenRevocationSubpath, a.indieAuthTokenRevokation) }) r.With(cacheLoggedIn, a.cacheMiddleware).Get("/.well-known/oauth-authorization-server", a.indieAuthMetadata) } @@ -41,10 +42,10 @@ func (a *goBlog) activityPubRouter(r chi.Router) { } if ap := a.cfg.ActivityPub; ap != nil && ap.Enabled { r.Route("/activitypub", func(r chi.Router) { - r.Post("/inbox/{blog}", a.apHandleInbox) + r.With(bodylimit.BodyLimit(10*bodylimit.MB)).Post("/inbox/{blog}", a.apHandleInbox) r.With(a.checkActivityStreamsRequest).Get("/followers/{blog}", a.apShowFollowers) r.With(a.cacheMiddleware).Get("/remote_follow/{blog}", a.apRemoteFollow) - r.Post("/remote_follow/{blog}", a.apRemoteFollow) + r.With(bodylimit.BodyLimit(100*bodylimit.KB)).Post("/remote_follow/{blog}", a.apRemoteFollow) }) r.Group(func(r chi.Router) { r.Use(cacheLoggedIn, a.cacheMiddleware) @@ -63,7 +64,7 @@ func (a *goBlog) webmentionsRouter(r chi.Router) { return } // Endpoint - r.Post("/", a.handleWebmention) + r.With(bodylimit.BodyLimit(bodylimit.MB)).Post("/", a.handleWebmention) // Authenticated routes r.Group(func(r chi.Router) { r.Use(a.authMiddleware) @@ -123,7 +124,7 @@ func (a *goBlog) otherRoutesRouter(r chi.Router) { // Reactions if a.reactionsEnabled() { r.Get("/reactions", a.getReactions) - r.Post("/reactions", a.postReaction) + r.With(bodylimit.BodyLimit(100*bodylimit.KB)).Post("/reactions", a.postReaction) } } @@ -303,7 +304,7 @@ func (a *goBlog) blogSearchRouter(conf *configBlog) func(r chi.Router) { middleware.WithValue(pathKey, searchPath), ) r.Get("/", a.serveSearch) - r.Post("/", a.serveSearch) + r.With(bodylimit.BodyLimit(100*bodylimit.KB)).Post("/", a.serveSearch) searchResultPath := "/" + searchPlaceholder r.Get(searchResultPath, a.serveSearchResult) r.Get(searchResultPath+feedPath, a.serveSearchResult) @@ -377,7 +378,7 @@ func (a *goBlog) blogCommentsRouter(conf *configBlog) func(r chi.Router) { middleware.WithValue(pathKey, commentsPath), ) r.With(a.cacheMiddleware, noIndexHeader).Get("/{id:[0-9]+}", a.serveComment) - r.With(a.captchaMiddleware).Post("/", a.createCommentFromRequest) + r.With(a.captchaMiddleware, bodylimit.BodyLimit(bodylimit.MB)).Post("/", a.createCommentFromRequest) r.Group(func(r chi.Router) { // Admin r.Use(a.authMiddleware) @@ -441,7 +442,7 @@ func (a *goBlog) blogContactRouter(conf *configBlog) func(r chi.Router) { r.Route(contactPath, func(r chi.Router) { r.Use(a.privateModeHandler, a.cacheMiddleware) r.Get("/", a.serveContactForm) - r.With(a.captchaMiddleware).Post("/", a.sendContactSubmission) + r.With(a.captchaMiddleware, bodylimit.BodyLimit(bodylimit.MB)).Post("/", a.sendContactSubmission) }) } } diff --git a/micropub.go b/micropub.go index 24bb8d6..cee62e7 100644 --- a/micropub.go +++ b/micropub.go @@ -101,7 +101,6 @@ func (a *goBlog) getMicropubChannelsMap() []map[string]any { } func (a *goBlog) serveMicropubPost(w http.ResponseWriter, r *http.Request) { - defer r.Body.Close() blog, _ := a.getBlog(r) p := &post{Blog: blog} switch mt, _, _ := mime.ParseMediaType(r.Header.Get(contentType)); mt { @@ -125,7 +124,7 @@ func (a *goBlog) serveMicropubPost(w http.ResponseWriter, r *http.Request) { a.micropubCreatePostFromForm(w, r, p) case contenttype.JSON: parsedMfItem := µformatItem{} - err := json.NewDecoder(io.LimitReader(r.Body, 10000000)).Decode(parsedMfItem) + err := json.NewDecoder(r.Body).Decode(parsedMfItem) if err != nil { a.serveError(w, r, err.Error(), http.StatusBadRequest) return diff --git a/micropubMedia.go b/micropubMedia.go index 9d73839..94890d6 100644 --- a/micropubMedia.go +++ b/micropubMedia.go @@ -15,7 +15,6 @@ import ( const micropubMediaSubPath = "/media" func (a *goBlog) serveMicropubMedia(w http.ResponseWriter, r *http.Request) { - defer r.Body.Close() // Check scope if !a.micropubCheckScope(w, r, "media") { return diff --git a/pkgs/bodylimit/bodylimit.go b/pkgs/bodylimit/bodylimit.go new file mode 100644 index 0000000..35ede75 --- /dev/null +++ b/pkgs/bodylimit/bodylimit.go @@ -0,0 +1,31 @@ +// package bodylimit provides a HTTP middleware that limits the maximum body size of requests +package bodylimit + +import "net/http" + +const ( + // Decimal + KB int64 = 1000 + MB = 1000 * KB + GB = 1000 * MB + TB = 1000 * GB + PB = 1000 * TB + + // Binary + KiB int64 = 1024 + MiB = 1024 * KiB + GiB = 1024 * MiB + TiB = 1024 * GiB + PiB = 1024 * TiB +) + +func BodyLimit(n int64) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if n > 0 { + r.Body = http.MaxBytesReader(w, r.Body, n) + } + next.ServeHTTP(w, r) + }) + } +} diff --git a/profileImage.go b/profileImage.go index 40ab1cd..9ec8f14 100644 --- a/profileImage.go +++ b/profileImage.go @@ -194,7 +194,6 @@ func (a *goBlog) serveUpdateProfileImage(w http.ResponseWriter, r *http.Request) } _, err = io.Copy(dataFile, file) _ = file.Close() - _ = r.Body.Close() if err != nil { a.serveError(w, r, "Failed to save image", http.StatusBadRequest) return