diff --git a/html/error_std.go b/html/error_std.go
index 04bfb06..d87fd68 100644
--- a/html/error_std.go
+++ b/html/error_std.go
@@ -5,14 +5,15 @@ package html
import (
"bytes"
"io"
- "net/http"
+
+ "codeberg.org/codeberg/pages/server/context"
)
// ReturnErrorPage sets the response status code and writes NotFoundPage to the response body, with "%status" replaced
// with the provided status code.
-func ReturnErrorPage(resp *http.Response, code int) {
- resp.StatusCode = code
- resp.Header.Set("Content-Type", "text/html; charset=utf-8")
+func ReturnErrorPage(ctx *context.Context, code int) {
+ ctx.RespWriter.Header().Set("Content-Type", "text/html; charset=utf-8")
+ ctx.RespWriter.WriteHeader(code)
- resp.Body = io.NopCloser(bytes.NewReader(errorBody(code)))
+ io.Copy(ctx.RespWriter, bytes.NewReader(errorBody(code)))
}
diff --git a/server/context/context.go b/server/context/context.go
new file mode 100644
index 0000000..464f84f
--- /dev/null
+++ b/server/context/context.go
@@ -0,0 +1,25 @@
+package context
+
+import (
+ stdContext "context"
+ "net/http"
+)
+
+type Context struct {
+ RespWriter http.ResponseWriter
+ Req *http.Request
+}
+
+func (c *Context) Context() stdContext.Context {
+ if c.Req != nil {
+ return c.Req.Context()
+ }
+ return stdContext.Background()
+}
+
+func (c *Context) Response() *http.Response {
+ if c.Req != nil {
+ return c.Req.Response
+ }
+ return nil
+}
diff --git a/server/gitea/cache.go b/server/gitea/cache.go
index 5b3fb7c..4cded96 100644
--- a/server/gitea/cache.go
+++ b/server/gitea/cache.go
@@ -26,4 +26,12 @@ var (
// than fileCacheTimeout, as that gets invalidated if the branch timestamp has changed. That way, repo changes will be
// picked up faster, while still allowing the content to be cached longer if nothing changes.
branchExistenceCacheTimeout = 5 * time.Minute
+
+ // fileCacheTimeout specifies the timeout for the file content cache - you might want to make this quite long, depending
+ // on your available memory.
+ // TODO: move as option into cache interface
+ fileCacheTimeout = 5 * time.Minute
+
+ // fileCacheSizeLimit limits the maximum file size that will be cached, and is set to 1 MB by default.
+ fileCacheSizeLimit = 1024 * 1024
)
diff --git a/server/gitea/client_std.go b/server/gitea/client_std.go
index 9e66763..5a39150 100644
--- a/server/gitea/client_std.go
+++ b/server/gitea/client_std.go
@@ -37,35 +37,61 @@ func NewClient(giteaRoot, giteaAPIToken string, respCache cache.SetGetKey, follo
}
func (client *Client) GiteaRawContent(targetOwner, targetRepo, ref, resource string) ([]byte, error) {
- rawBytes, resp, err := client.sdkClient.GetFile(targetOwner, targetRepo, ref, resource, client.supportLFS)
+ reader, _, err := client.ServeRawContent(targetOwner, targetRepo, ref, resource)
if err != nil {
return nil, err
}
-
- switch resp.StatusCode {
- case http.StatusOK:
- return rawBytes, nil
- case http.StatusNotFound:
- return nil, ErrorNotFound
- default:
- return nil, fmt.Errorf("unexpected status code '%d'", resp.StatusCode)
- }
+ defer reader.Close()
+ return io.ReadAll(reader)
}
-func (client *Client) ServeRawContent(targetOwner, targetRepo, ref, resource string) (io.ReadCloser, error) {
+func (client *Client) ServeRawContent(targetOwner, targetRepo, ref, resource string) (io.ReadCloser, *http.Response, error) {
+ // if cachedValue, ok := fileResponseCache.Get(uri + "?timestamp=" + o.timestamp()); ok && !cachedValue.(gitea.FileResponse).IsEmpty() {
+ // cachedResponse = cachedValue.(gitea.FileResponse)
reader, resp, err := client.sdkClient.GetFileReader(targetOwner, targetRepo, ref, resource, client.supportLFS)
- if err != nil {
- return nil, err
- }
+ if resp != nil {
+ switch resp.StatusCode {
+ case http.StatusOK:
- switch resp.StatusCode {
- case http.StatusOK:
- return reader, nil
- case http.StatusNotFound:
- return nil, ErrorNotFound
- default:
- return nil, fmt.Errorf("unexpected status code '%d'", resp.StatusCode)
+ // add caching
+
+ // Write the response body to the original request
+ // var cacheBodyWriter bytes.Buffer
+ // if res != nil {
+ // if res.Header.ContentLength() > fileCacheSizeLimit {
+ // // fasthttp else will set "Content-Length: 0"
+ // ctx.Response().SetBodyStream(&strings.Reader{}, -1)
+ //
+ // err = res.BodyWriteTo(ctx.Response.BodyWriter())
+ // } else {
+ // // TODO: cache is half-empty if request is cancelled - does the ctx.Err() below do the trick?
+ // err = res.BodyWriteTo(io.MultiWriter(ctx.Response().BodyWriter(), &cacheBodyWriter))
+ // }
+ // } else {
+ // _, err = ctx.Write(cachedResponse.Body)
+ // }
+
+ // if res != nil && res.Header.ContentLength() <= fileCacheSizeLimit && ctx.Err() == nil {
+ // cachedResponse.Exists = true
+ // cachedResponse.MimeType = mimeType
+ // cachedResponse.Body = cacheBodyWriter.Bytes()
+ // _ = fileResponseCache.Set(uri+"?timestamp="+o.timestamp(), cachedResponse, fileCacheTimeout)
+ // }
+
+ return reader, resp.Response, err
+ case http.StatusNotFound:
+
+ // add not exist caching
+ // _ = fileResponseCache.Set(uri+"?timestamp="+o.timestamp(), gitea.FileResponse{
+ // Exists: false,
+ // }, fileCacheTimeout)
+
+ return nil, resp.Response, ErrorNotFound
+ default:
+ return nil, resp.Response, fmt.Errorf("unexpected status code '%d'", resp.StatusCode)
+ }
}
+ return nil, nil, err
}
func (client *Client) GiteaGetRepoBranchTimestamp(repoOwner, repoName, branchName string) (*BranchTimestamp, error) {
diff --git a/server/handler.go b/server/handler.go
index 2207035..cda0af5 100644
--- a/server/handler.go
+++ b/server/handler.go
@@ -4,6 +4,7 @@ package server
import (
"bytes"
+ "net/http"
"strings"
"github.com/rs/zerolog"
@@ -25,23 +26,23 @@ func Handler(mainDomainSuffix, rawDomain []byte,
giteaRoot, rawInfoPage string,
blacklistedPaths, allowedCorsDomains [][]byte,
dnsLookupCache, canonicalDomainCache cache.SetGetKey,
-) func(ctx *fasthttp.RequestCtx) {
- return func(ctx *fasthttp.RequestCtx) {
- log := log.With().Str("Handler", string(ctx.Request.Header.RequestURI())).Logger()
+) http.HandlerFunc {
+ return func(w http.ResponseWriter, req *http.Request) {
+ log := log.With().Str("Handler", req.RequestURI).Logger()
- ctx.Response.Header.Set("Server", "CodebergPages/"+version.Version)
+ w.Header().Set("Server", "CodebergPages/"+version.Version)
// Force new default from specification (since November 2020) - see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy#strict-origin-when-cross-origin
- ctx.Response.Header.Set("Referrer-Policy", "strict-origin-when-cross-origin")
+ w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
// Enable browser caching for up to 10 minutes
- ctx.Response.Header.Set("Cache-Control", "public, max-age=600")
+ w.Header().Set("Cache-Control", "public, max-age=600")
- trimmedHost := utils.TrimHostPort(ctx.Request.Host())
+ trimmedHost := utils.TrimHostPort([]byte(req.Host))
// Add HSTS for RawDomain and MainDomainSuffix
if hsts := GetHSTSHeader(trimmedHost, mainDomainSuffix, rawDomain); hsts != "" {
- ctx.Response.Header.Set("Strict-Transport-Security", hsts)
+ w.Header().Set("Strict-Transport-Security", hsts)
}
// Block all methods not required for static pages
@@ -68,12 +69,13 @@ func Handler(mainDomainSuffix, rawDomain []byte,
}
}
if allowCors {
- ctx.Response.Header.Set("Access-Control-Allow-Origin", "*")
- ctx.Response.Header.Set("Access-Control-Allow-Methods", "GET, HEAD")
+ w.Header().Set("Access-Control-Allow-Origin", "*")
+ w.Header().Set("Access-Control-Allow-Methods", "GET, HEAD")
}
- ctx.Response.Header.Set("Allow", "GET, HEAD, OPTIONS")
+ w.Header().Set("Allow", "GET, HEAD, OPTIONS")
if ctx.IsOptions() {
- ctx.Response.Header.SetStatusCode(fasthttp.StatusNoContent)
+
+ ctx.Response.Header.SetStatusCode(http.StatusNoContent)
return
}
@@ -137,7 +139,7 @@ func Handler(mainDomainSuffix, rawDomain []byte,
pathElements := strings.Split(string(bytes.Trim(ctx.Request.URI().Path(), "/")), "/")
if len(pathElements) < 2 {
// https://{RawDomain}/{owner}/{repo}[/@{branch}]/{path} is required
- ctx.Redirect(rawInfoPage, fasthttp.StatusTemporaryRedirect)
+ ctx.Redirect(rawInfoPage, http.StatusTemporaryRedirect)
return
}
targetOwner = pathElements[0]
@@ -157,7 +159,7 @@ func Handler(mainDomainSuffix, rawDomain []byte,
return
}
log.Debug().Msg("missing branch")
- html.ReturnErrorPage(ctx, fasthttp.StatusFailedDependency)
+ html.ReturnErrorPage(ctx, http.StatusFailedDependency)
return
}
@@ -183,7 +185,7 @@ func Handler(mainDomainSuffix, rawDomain []byte,
if targetOwner == "www" {
// www.codeberg.page redirects to codeberg.page // TODO: rm hardcoded - use cname?
- ctx.Redirect("https://"+string(mainDomainSuffix[1:])+string(ctx.Path()), fasthttp.StatusPermanentRedirect)
+ ctx.Redirect("https://"+string(mainDomainSuffix[1:])+string(ctx.Path()), http.StatusPermanentRedirect)
return
}
@@ -192,7 +194,7 @@ func Handler(mainDomainSuffix, rawDomain []byte,
if len(pathElements) > 1 && strings.HasPrefix(pathElements[1], "@") {
if targetRepo == "pages" {
// example.codeberg.org/pages/@... redirects to example.codeberg.org/@...
- ctx.Redirect("/"+strings.Join(pathElements[1:], "/"), fasthttp.StatusTemporaryRedirect)
+ ctx.Redirect("/"+strings.Join(pathElements[1:], "/"), http.StatusTemporaryRedirect)
return
}
@@ -206,7 +208,7 @@ func Handler(mainDomainSuffix, rawDomain []byte,
targetOptions, targetOwner, targetRepo, targetBranch, targetPath,
canonicalDomainCache)
} else {
- html.ReturnErrorPage(ctx, fasthttp.StatusFailedDependency)
+ html.ReturnErrorPage(ctx, http.StatusFailedDependency)
}
return
}
@@ -222,7 +224,7 @@ func Handler(mainDomainSuffix, rawDomain []byte,
targetOptions, targetOwner, targetRepo, targetBranch, targetPath,
canonicalDomainCache)
} else {
- html.ReturnErrorPage(ctx, fasthttp.StatusFailedDependency)
+ html.ReturnErrorPage(ctx, http.StatusFailedDependency)
}
return
}
@@ -253,7 +255,7 @@ func Handler(mainDomainSuffix, rawDomain []byte,
}
// Couldn't find a valid repo/branch
- html.ReturnErrorPage(ctx, fasthttp.StatusFailedDependency)
+ html.ReturnErrorPage(ctx, http.StatusFailedDependency)
return
} else {
trimmedHostStr := string(trimmedHost)
@@ -261,7 +263,7 @@ func Handler(mainDomainSuffix, rawDomain []byte,
// Serve pages from external domains
targetOwner, targetRepo, targetBranch = dns.GetTargetFromDNS(trimmedHostStr, string(mainDomainSuffix), dnsLookupCache)
if targetOwner == "" {
- html.ReturnErrorPage(ctx, fasthttp.StatusFailedDependency)
+ html.ReturnErrorPage(ctx, http.StatusFailedDependency)
return
}
@@ -279,17 +281,17 @@ func Handler(mainDomainSuffix, rawDomain []byte,
targetRepo, targetBranch, pathElements, canonicalLink) {
canonicalDomain, valid := upstream.CheckCanonicalDomain(giteaClient, targetOwner, targetRepo, targetBranch, trimmedHostStr, string(mainDomainSuffix), canonicalDomainCache)
if !valid {
- html.ReturnErrorPage(ctx, fasthttp.StatusMisdirectedRequest)
+ html.ReturnErrorPage(ctx, http.StatusMisdirectedRequest)
return
} else if canonicalDomain != trimmedHostStr {
// only redirect if the target is also a codeberg page!
targetOwner, _, _ = dns.GetTargetFromDNS(strings.SplitN(canonicalDomain, "/", 2)[0], string(mainDomainSuffix), dnsLookupCache)
if targetOwner != "" {
- ctx.Redirect("https://"+canonicalDomain+string(ctx.RequestURI()), fasthttp.StatusTemporaryRedirect)
+ ctx.Redirect("https://"+canonicalDomain+string(ctx.RequestURI()), http.StatusTemporaryRedirect)
return
}
- html.ReturnErrorPage(ctx, fasthttp.StatusFailedDependency)
+ html.ReturnErrorPage(ctx, http.StatusFailedDependency)
return
}
@@ -300,7 +302,7 @@ func Handler(mainDomainSuffix, rawDomain []byte,
return
}
- html.ReturnErrorPage(ctx, fasthttp.StatusFailedDependency)
+ html.ReturnErrorPage(ctx, http.StatusFailedDependency)
return
}
}
diff --git a/server/handler_fasthttp.go b/server/handler_fasthttp.go
index 45b944f..764f1b2 100644
--- a/server/handler_fasthttp.go
+++ b/server/handler_fasthttp.go
@@ -24,7 +24,7 @@ func Handler(mainDomainSuffix, rawDomain []byte,
giteaClient *gitea.Client,
giteaRoot, rawInfoPage string,
blacklistedPaths, allowedCorsDomains [][]byte,
- dnsLookupCache, canonicalDomainCache, branchTimestampCache, fileResponseCache cache.SetGetKey,
+ dnsLookupCache, canonicalDomainCache cache.SetGetKey,
) func(ctx *fasthttp.RequestCtx) {
return func(ctx *fasthttp.RequestCtx) {
log := log.With().Str("Handler", string(ctx.Request.Header.RequestURI())).Logger()
@@ -96,7 +96,7 @@ func Handler(mainDomainSuffix, rawDomain []byte,
branch = strings.ReplaceAll(branch, "~", "/")
// Check if the branch exists, otherwise treat it as a file path
- branchTimestampResult := upstream.GetBranchTimestamp(giteaClient, targetOwner, repo, branch, branchTimestampCache)
+ branchTimestampResult := upstream.GetBranchTimestamp(giteaClient, targetOwner, repo, branch)
if branchTimestampResult == nil {
log.Debug().Msg("tryBranch: branch doesn't exist")
return false
@@ -153,7 +153,7 @@ func Handler(mainDomainSuffix, rawDomain []byte,
log.Debug().Msg("tryBranch, now trying upstream 1")
tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost,
targetOptions, targetOwner, targetRepo, targetBranch, targetPath,
- canonicalDomainCache, branchTimestampCache, fileResponseCache)
+ canonicalDomainCache)
return
}
log.Debug().Msg("missing branch")
@@ -169,7 +169,7 @@ func Handler(mainDomainSuffix, rawDomain []byte,
log.Debug().Msg("tryBranch, now trying upstream 2")
tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost,
targetOptions, targetOwner, targetRepo, targetBranch, targetPath,
- canonicalDomainCache, branchTimestampCache, fileResponseCache)
+ canonicalDomainCache)
return
} else if bytes.HasSuffix(trimmedHost, mainDomainSuffix) {
@@ -204,7 +204,7 @@ func Handler(mainDomainSuffix, rawDomain []byte,
log.Debug().Msg("tryBranch, now trying upstream 3")
tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost,
targetOptions, targetOwner, targetRepo, targetBranch, targetPath,
- canonicalDomainCache, branchTimestampCache, fileResponseCache)
+ canonicalDomainCache)
} else {
html.ReturnErrorPage(ctx, fasthttp.StatusFailedDependency)
}
@@ -220,7 +220,7 @@ func Handler(mainDomainSuffix, rawDomain []byte,
log.Debug().Msg("tryBranch, now trying upstream 4")
tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost,
targetOptions, targetOwner, targetRepo, targetBranch, targetPath,
- canonicalDomainCache, branchTimestampCache, fileResponseCache)
+ canonicalDomainCache)
} else {
html.ReturnErrorPage(ctx, fasthttp.StatusFailedDependency)
}
@@ -236,7 +236,7 @@ func Handler(mainDomainSuffix, rawDomain []byte,
log.Debug().Msg("tryBranch, now trying upstream 5")
tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost,
targetOptions, targetOwner, targetRepo, targetBranch, targetPath,
- canonicalDomainCache, branchTimestampCache, fileResponseCache)
+ canonicalDomainCache)
return
}
@@ -248,7 +248,7 @@ func Handler(mainDomainSuffix, rawDomain []byte,
log.Debug().Msg("tryBranch, now trying upstream 6")
tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost,
targetOptions, targetOwner, targetRepo, targetBranch, targetPath,
- canonicalDomainCache, branchTimestampCache, fileResponseCache)
+ canonicalDomainCache)
return
}
@@ -296,7 +296,7 @@ func Handler(mainDomainSuffix, rawDomain []byte,
log.Debug().Msg("tryBranch, now trying upstream 7")
tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost,
targetOptions, targetOwner, targetRepo, targetBranch, targetPath,
- canonicalDomainCache, branchTimestampCache, fileResponseCache)
+ canonicalDomainCache)
return
}
diff --git a/server/try.go b/server/try.go
index a1ee8ac..63904ec 100644
--- a/server/try.go
+++ b/server/try.go
@@ -4,18 +4,18 @@ package server
import (
"bytes"
+ "net/http"
"strings"
- "github.com/valyala/fasthttp"
-
"codeberg.org/codeberg/pages/html"
"codeberg.org/codeberg/pages/server/cache"
+ "codeberg.org/codeberg/pages/server/context"
"codeberg.org/codeberg/pages/server/gitea"
"codeberg.org/codeberg/pages/server/upstream"
)
// tryUpstream forwards the target request to the Gitea API, and shows an error page on failure.
-func tryUpstream(ctx *fasthttp.RequestCtx, giteaClient *gitea.Client,
+func tryUpstream(ctx *context.Context, giteaClient *gitea.Client,
mainDomainSuffix, trimmedHost []byte,
targetOptions *upstream.Options,
@@ -27,14 +27,14 @@ func tryUpstream(ctx *fasthttp.RequestCtx, giteaClient *gitea.Client,
if bytes.HasSuffix(trimmedHost, mainDomainSuffix) {
canonicalDomain, _ := upstream.CheckCanonicalDomain(giteaClient, targetOwner, targetRepo, targetBranch, "", string(mainDomainSuffix), canonicalDomainCache)
if !strings.HasSuffix(strings.SplitN(canonicalDomain, "/", 2)[0], string(mainDomainSuffix)) {
- canonicalPath := string(ctx.RequestURI())
+ canonicalPath := ctx.Req.RequestURI
if targetRepo != "pages" {
path := strings.SplitN(canonicalPath, "/", 3)
if len(path) >= 3 {
canonicalPath = "/" + path[2]
}
}
- ctx.Redirect("https://"+canonicalDomain+canonicalPath, fasthttp.StatusTemporaryRedirect)
+ ctx.Redirect("https://"+canonicalDomain+canonicalPath, http.StatusTemporaryRedirect)
return
}
}
@@ -46,6 +46,6 @@ func tryUpstream(ctx *fasthttp.RequestCtx, giteaClient *gitea.Client,
// Try to request the file from the Gitea API
if !targetOptions.Upstream(ctx, giteaClient) {
- html.ReturnErrorPage(ctx, ctx.Response.StatusCode())
+ html.ReturnErrorPage(w, ctx.Response().StatusCode)
}
}
diff --git a/server/try_fasthttp.go b/server/try_fasthttp.go
index 4b4d8f7..1bc0af2 100644
--- a/server/try_fasthttp.go
+++ b/server/try_fasthttp.go
@@ -21,7 +21,7 @@ func tryUpstream(ctx *fasthttp.RequestCtx, giteaClient *gitea.Client,
targetOptions *upstream.Options,
targetOwner, targetRepo, targetBranch, targetPath string,
- canonicalDomainCache, branchTimestampCache, fileResponseCache cache.SetGetKey,
+ canonicalDomainCache cache.SetGetKey,
) {
// check if a canonical domain exists on a request on MainDomain
if bytes.HasSuffix(trimmedHost, mainDomainSuffix) {
@@ -45,7 +45,7 @@ func tryUpstream(ctx *fasthttp.RequestCtx, giteaClient *gitea.Client,
targetOptions.TargetPath = targetPath
// Try to request the file from the Gitea API
- if !targetOptions.Upstream(ctx, giteaClient, branchTimestampCache, fileResponseCache) {
+ if !targetOptions.Upstream(ctx, giteaClient) {
html.ReturnErrorPage(ctx, ctx.Response.StatusCode())
}
}
diff --git a/server/upstream/const.go b/server/upstream/const.go
index bdb123b..8a772d9 100644
--- a/server/upstream/const.go
+++ b/server/upstream/const.go
@@ -2,14 +2,6 @@ package upstream
import "time"
-// fileCacheTimeout specifies the timeout for the file content cache - you might want to make this quite long, depending
-// on your available memory.
-// TODO: move as option into cache interface
-var fileCacheTimeout = 5 * time.Minute
-
-// fileCacheSizeLimit limits the maximum file size that will be cached, and is set to 1 MB by default.
-var fileCacheSizeLimit = 1024 * 1024
-
// canonicalDomainCacheTimeout specifies the timeout for the canonical domain cache.
var canonicalDomainCacheTimeout = 15 * time.Minute
diff --git a/server/upstream/upstream.go b/server/upstream/upstream.go
index 76dce20..8e50a1c 100644
--- a/server/upstream/upstream.go
+++ b/server/upstream/upstream.go
@@ -1,18 +1,7 @@
package upstream
import (
- "bytes"
- "errors"
- "fmt"
- "io"
- "strings"
"time"
-
- "github.com/rs/zerolog/log"
- "github.com/valyala/fasthttp"
-
- "codeberg.org/codeberg/pages/html"
- "codeberg.org/codeberg/pages/server/gitea"
)
// upstreamIndexPages lists pages that may be considered as index pages for directories.
@@ -40,167 +29,3 @@ type Options struct {
appendTrailingSlash bool
redirectIfExists string
}
-
-// Upstream requests a file from the Gitea API at GiteaRoot and writes it to the request context.
-func (o *Options) Upstream(ctx *fasthttp.RequestCtx, giteaClient *gitea.Client) (final bool) {
- log := log.With().Strs("upstream", []string{o.TargetOwner, o.TargetRepo, o.TargetBranch, o.TargetPath}).Logger()
-
- // Check if the branch exists and when it was modified
- if o.BranchTimestamp.IsZero() {
- branch := GetBranchTimestamp(giteaClient, o.TargetOwner, o.TargetRepo, o.TargetBranch)
-
- if branch == nil {
- html.ReturnErrorPage(ctx, fasthttp.StatusFailedDependency)
- return true
- }
- o.TargetBranch = branch.Branch
- o.BranchTimestamp = branch.Timestamp
- }
-
- if o.TargetOwner == "" || o.TargetRepo == "" || o.TargetBranch == "" {
- html.ReturnErrorPage(ctx, fasthttp.StatusBadRequest)
- return true
- }
-
- // Check if the browser has a cached version
- if ifModifiedSince, err := time.Parse(time.RFC1123, string(ctx.Request.Header.Peek("If-Modified-Since"))); err == nil {
- if !ifModifiedSince.Before(o.BranchTimestamp) {
- ctx.Response.SetStatusCode(fasthttp.StatusNotModified)
- return true
- }
- }
- log.Debug().Msg("preparations")
-
- // Make a GET request to the upstream URL
- uri := o.generateUri()
- var cachedResponse gitea.FileResponse
- // if cachedValue, ok := fileResponseCache.Get(uri + "?timestamp=" + o.timestamp()); ok && !cachedValue.(gitea.FileResponse).IsEmpty() {
- // cachedResponse = cachedValue.(gitea.FileResponse)
- // } else {
- res, err := giteaClient.ServeRawContent(o.generateUriClientArgs())
- log.Debug().Msg("acquisition")
-
- // Handle errors
- if (err != nil && errors.Is(err, gitea.ErrorNotFound)) || (res == nil && !cachedResponse.Exists) {
- if o.TryIndexPages {
- // copy the o struct & try if an index page exists
- optionsForIndexPages := *o
- optionsForIndexPages.TryIndexPages = false
- optionsForIndexPages.appendTrailingSlash = true
- for _, indexPage := range upstreamIndexPages {
- optionsForIndexPages.TargetPath = strings.TrimSuffix(o.TargetPath, "/") + "/" + indexPage
- if optionsForIndexPages.Upstream(ctx, giteaClient, branchTimestampCache) {
- _ = fileResponseCache.Set(uri+"?timestamp="+o.timestamp(), gitea.FileResponse{
- Exists: false,
- }, fileCacheTimeout)
- return true
- }
- }
- // compatibility fix for GitHub Pages (/example → /example.html)
- optionsForIndexPages.appendTrailingSlash = false
- optionsForIndexPages.redirectIfExists = strings.TrimSuffix(string(ctx.Request.URI().Path()), "/") + ".html"
- optionsForIndexPages.TargetPath = o.TargetPath + ".html"
- if optionsForIndexPages.Upstream(ctx, giteaClient, branchTimestampCache) {
- _ = fileResponseCache.Set(uri+"?timestamp="+o.timestamp(), gitea.FileResponse{
- Exists: false,
- }, fileCacheTimeout)
- return true
- }
- }
- ctx.Response.SetStatusCode(fasthttp.StatusNotFound)
- if o.TryIndexPages {
- // copy the o struct & try if a not found page exists
- optionsForNotFoundPages := *o
- optionsForNotFoundPages.TryIndexPages = false
- optionsForNotFoundPages.appendTrailingSlash = false
- for _, notFoundPage := range upstreamNotFoundPages {
- optionsForNotFoundPages.TargetPath = "/" + notFoundPage
- if optionsForNotFoundPages.Upstream(ctx, giteaClient, branchTimestampCache) {
- _ = fileResponseCache.Set(uri+"?timestamp="+o.timestamp(), gitea.FileResponse{
- Exists: false,
- }, fileCacheTimeout)
- return true
- }
- }
- }
- if res != nil {
- // Update cache if the request is fresh
- _ = fileResponseCache.Set(uri+"?timestamp="+o.timestamp(), gitea.FileResponse{
- Exists: false,
- }, fileCacheTimeout)
- }
- return false
- }
- if res != nil && (err != nil || res.StatusCode() != fasthttp.StatusOK) {
- fmt.Printf("Couldn't fetch contents from \"%s\": %s (status code %d)\n", uri, err, res.StatusCode())
- html.ReturnErrorPage(ctx, fasthttp.StatusInternalServerError)
- return true
- }
-
- // Append trailing slash if missing (for index files), and redirect to fix filenames in general
- // o.appendTrailingSlash is only true when looking for index pages
- if o.appendTrailingSlash && !bytes.HasSuffix(ctx.Request.URI().Path(), []byte{'/'}) {
- ctx.Redirect(string(ctx.Request.URI().Path())+"/", fasthttp.StatusTemporaryRedirect)
- return true
- }
- if bytes.HasSuffix(ctx.Request.URI().Path(), []byte("/index.html")) {
- ctx.Redirect(strings.TrimSuffix(string(ctx.Request.URI().Path()), "index.html"), fasthttp.StatusTemporaryRedirect)
- return true
- }
- if o.redirectIfExists != "" {
- ctx.Redirect(o.redirectIfExists, fasthttp.StatusTemporaryRedirect)
- return true
- }
- log.Debug().Msg("error handling")
-
- // Set the MIME type
- mimeType := o.getMimeTypeByExtension()
- ctx.Response.Header.SetContentType(mimeType)
-
- // Set ETag
- if cachedResponse.Exists {
- ctx.Response.Header.SetBytesV(fasthttp.HeaderETag, cachedResponse.ETag)
- } else if res != nil {
- cachedResponse.ETag = res.Header.Peek(fasthttp.HeaderETag)
- ctx.Response.Header.SetBytesV(fasthttp.HeaderETag, cachedResponse.ETag)
- }
-
- if ctx.Response.StatusCode() != fasthttp.StatusNotFound {
- // Everything's okay so far
- ctx.Response.SetStatusCode(fasthttp.StatusOK)
- }
- ctx.Response.Header.SetLastModified(o.BranchTimestamp)
-
- log.Debug().Msg("response preparations")
-
- // Write the response body to the original request
- var cacheBodyWriter bytes.Buffer
- if res != nil {
- if res.Header.ContentLength() > fileCacheSizeLimit {
- // fasthttp else will set "Content-Length: 0"
- ctx.Response.SetBodyStream(&strings.Reader{}, -1)
-
- err = res.BodyWriteTo(ctx.Response.BodyWriter())
- } else {
- // TODO: cache is half-empty if request is cancelled - does the ctx.Err() below do the trick?
- err = res.BodyWriteTo(io.MultiWriter(ctx.Response.BodyWriter(), &cacheBodyWriter))
- }
- } else {
- _, err = ctx.Write(cachedResponse.Body)
- }
- if err != nil {
- fmt.Printf("Couldn't write body for \"%s\": %s\n", uri, err)
- html.ReturnErrorPage(ctx, fasthttp.StatusInternalServerError)
- return true
- }
- log.Debug().Msg("response")
-
- if res != nil && res.Header.ContentLength() <= fileCacheSizeLimit && ctx.Err() == nil {
- cachedResponse.Exists = true
- cachedResponse.MimeType = mimeType
- cachedResponse.Body = cacheBodyWriter.Bytes()
- _ = fileResponseCache.Set(uri+"?timestamp="+o.timestamp(), cachedResponse, fileCacheTimeout)
- }
-
- return true
-}
diff --git a/server/upstream/upstream_fasthttp.go b/server/upstream/upstream_fasthttp.go
new file mode 100644
index 0000000..b27b457
--- /dev/null
+++ b/server/upstream/upstream_fasthttp.go
@@ -0,0 +1,150 @@
+//go:build fasthttp
+
+package upstream
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/rs/zerolog/log"
+ "github.com/valyala/fasthttp"
+
+ "codeberg.org/codeberg/pages/html"
+ "codeberg.org/codeberg/pages/server/gitea"
+)
+
+// Upstream requests a file from the Gitea API at GiteaRoot and writes it to the request context.
+func (o *Options) Upstream(ctx *fasthttp.RequestCtx, giteaClient *gitea.Client) (final bool) {
+ log := log.With().Strs("upstream", []string{o.TargetOwner, o.TargetRepo, o.TargetBranch, o.TargetPath}).Logger()
+
+ // Check if the branch exists and when it was modified
+ if o.BranchTimestamp.IsZero() {
+ branch := GetBranchTimestamp(giteaClient, o.TargetOwner, o.TargetRepo, o.TargetBranch)
+
+ if branch == nil {
+ html.ReturnErrorPage(ctx, fasthttp.StatusFailedDependency)
+ return true
+ }
+ o.TargetBranch = branch.Branch
+ o.BranchTimestamp = branch.Timestamp
+ }
+
+ if o.TargetOwner == "" || o.TargetRepo == "" || o.TargetBranch == "" {
+ html.ReturnErrorPage(ctx, fasthttp.StatusBadRequest)
+ return true
+ }
+
+ // Check if the browser has a cached version
+ if ifModifiedSince, err := time.Parse(time.RFC1123, string(ctx.Request.Header.Peek("If-Modified-Since"))); err == nil {
+ if !ifModifiedSince.Before(o.BranchTimestamp) {
+ ctx.Response.SetStatusCode(fasthttp.StatusNotModified)
+ return true
+ }
+ }
+ log.Debug().Msg("preparations")
+
+ // Make a GET request to the upstream URL
+ uri := o.generateUri()
+ var cachedResponse gitea.FileResponse
+ // if cachedValue, ok := fileResponseCache.Get(uri + "?timestamp=" + o.timestamp()); ok && !cachedValue.(gitea.FileResponse).IsEmpty() {
+ // cachedResponse = cachedValue.(gitea.FileResponse)
+ // } else {
+ res, err := giteaClient.ServeRawContent(o.generateUriClientArgs())
+ log.Debug().Msg("acquisition")
+
+ // Handle errors
+ if (err != nil && errors.Is(err, gitea.ErrorNotFound)) || (res == nil && !cachedResponse.Exists) {
+ if o.TryIndexPages {
+ // copy the o struct & try if an index page exists
+ optionsForIndexPages := *o
+ optionsForIndexPages.TryIndexPages = false
+ optionsForIndexPages.appendTrailingSlash = true
+ for _, indexPage := range upstreamIndexPages {
+ optionsForIndexPages.TargetPath = strings.TrimSuffix(o.TargetPath, "/") + "/" + indexPage
+ if optionsForIndexPages.Upstream(ctx, giteaClient) {
+ return true
+ }
+ }
+ // compatibility fix for GitHub Pages (/example → /example.html)
+ optionsForIndexPages.appendTrailingSlash = false
+ optionsForIndexPages.redirectIfExists = strings.TrimSuffix(string(ctx.Request.URI().Path()), "/") + ".html"
+ optionsForIndexPages.TargetPath = o.TargetPath + ".html"
+ if optionsForIndexPages.Upstream(ctx, giteaClient) {
+ return true
+ }
+ }
+ ctx.Response.SetStatusCode(fasthttp.StatusNotFound)
+ if o.TryIndexPages {
+ // copy the o struct & try if a not found page exists
+ optionsForNotFoundPages := *o
+ optionsForNotFoundPages.TryIndexPages = false
+ optionsForNotFoundPages.appendTrailingSlash = false
+ for _, notFoundPage := range upstreamNotFoundPages {
+ optionsForNotFoundPages.TargetPath = "/" + notFoundPage
+ if optionsForNotFoundPages.Upstream(ctx, giteaClient) {
+ return true
+ }
+ }
+ }
+ return false
+ }
+ if res != nil && (err != nil || res.StatusCode() != fasthttp.StatusOK) {
+ fmt.Printf("Couldn't fetch contents from \"%s\": %s (status code %d)\n", uri, err, res.StatusCode())
+ html.ReturnErrorPage(ctx, fasthttp.StatusInternalServerError)
+ return true
+ }
+
+ // Append trailing slash if missing (for index files), and redirect to fix filenames in general
+ // o.appendTrailingSlash is only true when looking for index pages
+ if o.appendTrailingSlash && !bytes.HasSuffix(ctx.Request.URI().Path(), []byte{'/'}) {
+ ctx.Redirect(string(ctx.Request.URI().Path())+"/", fasthttp.StatusTemporaryRedirect)
+ return true
+ }
+ if bytes.HasSuffix(ctx.Request.URI().Path(), []byte("/index.html")) {
+ ctx.Redirect(strings.TrimSuffix(string(ctx.Request.URI().Path()), "index.html"), fasthttp.StatusTemporaryRedirect)
+ return true
+ }
+ if o.redirectIfExists != "" {
+ ctx.Redirect(o.redirectIfExists, fasthttp.StatusTemporaryRedirect)
+ return true
+ }
+ log.Debug().Msg("error handling")
+
+ // Set the MIME type
+ mimeType := o.getMimeTypeByExtension()
+ ctx.Response.Header.SetContentType(mimeType)
+
+ // Set ETag
+ if cachedResponse.Exists {
+ ctx.Response.Header.SetBytesV(fasthttp.HeaderETag, cachedResponse.ETag)
+ } else if res != nil {
+ cachedResponse.ETag = res.Header.Peek(fasthttp.HeaderETag)
+ ctx.Response.Header.SetBytesV(fasthttp.HeaderETag, cachedResponse.ETag)
+ }
+
+ if ctx.Response.StatusCode() != fasthttp.StatusNotFound {
+ // Everything's okay so far
+ ctx.Response.SetStatusCode(fasthttp.StatusOK)
+ }
+ ctx.Response.Header.SetLastModified(o.BranchTimestamp)
+
+ log.Debug().Msg("response preparations")
+
+ // Write the response body to the original request
+
+ // fasthttp else will set "Content-Length: 0"
+ ctx.Response.SetBodyStream(&strings.Reader{}, -1)
+
+ err = res.BodyWriteTo(ctx.Response.BodyWriter())
+ if err != nil {
+ fmt.Printf("Couldn't write body for \"%s\": %s\n", uri, err)
+ html.ReturnErrorPage(ctx, fasthttp.StatusInternalServerError)
+ return true
+ }
+ log.Debug().Msg("response")
+
+ return true
+}
diff --git a/server/upstream/upstream_std.go b/server/upstream/upstream_std.go
new file mode 100644
index 0000000..3d8aefe
--- /dev/null
+++ b/server/upstream/upstream_std.go
@@ -0,0 +1,146 @@
+//go:build !fasthttp
+
+package upstream
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "strings"
+ "time"
+
+ "github.com/rs/zerolog/log"
+ "github.com/valyala/fasthttp"
+
+ "codeberg.org/codeberg/pages/html"
+ "codeberg.org/codeberg/pages/server/context"
+ "codeberg.org/codeberg/pages/server/gitea"
+)
+
+// Upstream requests a file from the Gitea API at GiteaRoot and writes it to the request context.
+func (o *Options) Upstream(ctx *context.Context, giteaClient *gitea.Client) (final bool) {
+ log := log.With().Strs("upstream", []string{o.TargetOwner, o.TargetRepo, o.TargetBranch, o.TargetPath}).Logger()
+
+ // Check if the branch exists and when it was modified
+ if o.BranchTimestamp.IsZero() {
+ branch := GetBranchTimestamp(giteaClient, o.TargetOwner, o.TargetRepo, o.TargetBranch)
+
+ if branch == nil {
+ html.ReturnErrorPage(ctx, http.StatusFailedDependency)
+ return true
+ }
+ o.TargetBranch = branch.Branch
+ o.BranchTimestamp = branch.Timestamp
+ }
+
+ if o.TargetOwner == "" || o.TargetRepo == "" || o.TargetBranch == "" {
+ html.ReturnErrorPage(ctx, http.StatusBadRequest)
+ return true
+ }
+
+ // Check if the browser has a cached version
+ if ifModifiedSince, err := time.Parse(time.RFC1123, string(ctx.Req.Response.Header.Get("If-Modified-Since"))); err == nil {
+ if !ifModifiedSince.Before(o.BranchTimestamp) {
+ ctx.RespWriter.WriteHeader(http.StatusNotModified)
+ return true
+ }
+ }
+ log.Debug().Msg("preparations")
+
+ reader, res, err := giteaClient.ServeRawContent(o.generateUriClientArgs())
+ log.Debug().Msg("acquisition")
+
+ // Handle errors
+ if (err != nil && errors.Is(err, gitea.ErrorNotFound)) || (res == nil) {
+ if o.TryIndexPages {
+ // copy the o struct & try if an index page exists
+ optionsForIndexPages := *o
+ optionsForIndexPages.TryIndexPages = false
+ optionsForIndexPages.appendTrailingSlash = true
+ for _, indexPage := range upstreamIndexPages {
+ optionsForIndexPages.TargetPath = strings.TrimSuffix(o.TargetPath, "/") + "/" + indexPage
+ if optionsForIndexPages.Upstream(ctx, giteaClient) {
+ return true
+ }
+ }
+ // compatibility fix for GitHub Pages (/example → /example.html)
+ optionsForIndexPages.appendTrailingSlash = false
+ optionsForIndexPages.redirectIfExists = strings.TrimSuffix(ctx.Req.URL.Path, "/") + ".html"
+ optionsForIndexPages.TargetPath = o.TargetPath + ".html"
+ if optionsForIndexPages.Upstream(ctx, giteaClient) {
+ return true
+ }
+ }
+ ctx.Response().SetStatusCode(http.StatusNotFound)
+ if o.TryIndexPages {
+ // copy the o struct & try if a not found page exists
+ optionsForNotFoundPages := *o
+ optionsForNotFoundPages.TryIndexPages = false
+ optionsForNotFoundPages.appendTrailingSlash = false
+ for _, notFoundPage := range upstreamNotFoundPages {
+ optionsForNotFoundPages.TargetPath = "/" + notFoundPage
+ if optionsForNotFoundPages.Upstream(ctx, giteaClient) {
+ return true
+ }
+ }
+ }
+ return false
+ }
+ if res != nil && (err != nil || res.StatusCode() != http.StatusOK) {
+ fmt.Printf("Couldn't fetch contents from \"%s\": %s (status code %d)\n", uri, err, res.StatusCode())
+ html.ReturnErrorPage(ctx, http.StatusInternalServerError)
+ return true
+ }
+
+ // Append trailing slash if missing (for index files), and redirect to fix filenames in general
+ // o.appendTrailingSlash is only true when looking for index pages
+ if o.appendTrailingSlash && !bytes.HasSuffix(ctx.Request().URI().Path(), []byte{'/'}) {
+ ctx.Redirect(string(ctx.Request.URI().Path())+"/", http.StatusTemporaryRedirect)
+ return true
+ }
+ if bytes.HasSuffix(ctx.Request.URI().Path(), []byte("/index.html")) {
+ ctx.Redirect(strings.TrimSuffix(string(ctx.Request.URI().Path()), "index.html"), http.StatusTemporaryRedirect)
+ return true
+ }
+ if o.redirectIfExists != "" {
+ ctx.Redirect(o.redirectIfExists, http.StatusTemporaryRedirect)
+ return true
+ }
+ log.Debug().Msg("error handling")
+
+ // Set the MIME type
+ mimeType := o.getMimeTypeByExtension()
+ ctx.Response.Header.SetContentType(mimeType)
+
+ // Set ETag
+ if cachedResponse.Exists {
+ ctx.Response().Header.SetBytesV(fasthttp.HeaderETag, cachedResponse.ETag)
+ } else if res != nil {
+ cachedResponse.ETag = res.Header.Peek(fasthttp.HeaderETag)
+ ctx.Response().Header.SetBytesV(fasthttp.HeaderETag, cachedResponse.ETag)
+ }
+
+ if ctx.Response().StatusCode() != http.StatusNotFound {
+ // Everything's okay so far
+ ctx.Response().SetStatusCode(http.StatusOK)
+ }
+ ctx.Response().Header.SetLastModified(o.BranchTimestamp)
+
+ log.Debug().Msg("response preparations")
+
+ // Write the response body to the original request
+ if reader != nil {
+ _, err := io.Copy(ctx.RespWriter, reader)
+ if err != nil {
+ fmt.Printf("Couldn't write body for \"%s\": %s\n", uri, err)
+ html.ReturnErrorPage(ctx, http.StatusInternalServerError)
+ return true
+ }
+ }
+
+ log.Debug().Msg("response")
+
+ return true
+}