2021-03-16 23:34:31 +00:00
package main
import (
"bytes"
2021-07-13 08:28:06 +00:00
debug_stepper "codeberg.org/codeberg/pages/debug-stepper"
2021-03-16 23:34:31 +00:00
"fmt"
2021-03-19 19:58:53 +00:00
"github.com/OrlovEvgeny/go-mcache"
2021-03-16 23:34:31 +00:00
"github.com/valyala/fasthttp"
"github.com/valyala/fastjson"
2021-07-08 21:08:30 +00:00
"io"
2021-03-16 23:34:31 +00:00
"mime"
"path"
"strconv"
"strings"
"time"
)
// handler handles a single HTTP request to the web server.
func handler ( ctx * fasthttp . RequestCtx ) {
2021-07-13 08:28:06 +00:00
s := debug_stepper . Start ( "handler" )
defer s . Complete ( )
2021-03-16 23:34:31 +00:00
ctx . Response . Header . Set ( "Server" , "Codeberg Pages" )
// 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" )
2021-12-02 09:23:03 +00:00
// Enable browser caching for up to 10 minutes
ctx . Response . Header . Set ( "Cache-Control" , "public, max-age=600" )
2021-03-16 23:34:31 +00:00
2021-11-20 14:30:58 +00:00
trimmedHost := TrimHostPort ( ctx . Request . Host ( ) )
2021-07-13 08:28:36 +00:00
// Add HSTS for RawDomain and MainDomainSuffix
2021-11-20 14:30:58 +00:00
if hsts := GetHSTSHeader ( trimmedHost ) ; hsts != "" {
2021-07-13 08:28:36 +00:00
ctx . Response . Header . Set ( "Strict-Transport-Security" , hsts )
}
2021-03-16 23:34:31 +00:00
// Block all methods not required for static pages
if ! ctx . IsGet ( ) && ! ctx . IsHead ( ) && ! ctx . IsOptions ( ) {
ctx . Response . Header . Set ( "Allow" , "GET, HEAD, OPTIONS" )
ctx . Error ( "Method not allowed" , fasthttp . StatusMethodNotAllowed )
return
}
// Block blacklisted paths (like ACME challenges)
for _ , blacklistedPath := range BlacklistedPaths {
if bytes . HasPrefix ( ctx . Path ( ) , blacklistedPath ) {
returnErrorPage ( ctx , fasthttp . StatusForbidden )
return
}
}
// Allow CORS for specified domains
if ctx . IsOptions ( ) {
allowCors := false
for _ , allowedCorsDomain := range AllowedCorsDomains {
2021-11-20 14:30:58 +00:00
if bytes . Equal ( trimmedHost , allowedCorsDomain ) {
2021-03-16 23:34:31 +00:00
allowCors = true
break
}
}
if allowCors {
ctx . Response . Header . Set ( "Access-Control-Allow-Origin" , "*" )
ctx . Response . Header . Set ( "Access-Control-Allow-Methods" , "GET, HEAD" )
}
ctx . Response . Header . Set ( "Allow" , "GET, HEAD, OPTIONS" )
ctx . Response . Header . SetStatusCode ( fasthttp . StatusNoContent )
return
}
// Prepare request information to Gitea
var targetOwner , targetRepo , targetBranch , targetPath string
var targetOptions = & upstreamOptions {
ForbiddenMimeTypes : map [ string ] struct { } { } ,
TryIndexPages : true ,
}
2021-03-19 19:30:08 +00:00
// tryBranch checks if a branch exists and populates the target variables. If canonicalLink is non-empty, it will
// also disallow search indexing and add a Link header to the canonical URL.
var tryBranch = func ( repo string , branch string , path [ ] string , canonicalLink string ) bool {
if repo == "" {
return false
}
// Check if the branch exists, otherwise treat it as a file path
2021-03-19 19:58:53 +00:00
branchTimestampResult := getBranchTimestamp ( targetOwner , repo , branch )
if branchTimestampResult == nil {
2021-03-19 19:30:08 +00:00
// branch doesn't exist
return false
}
2021-03-19 19:58:53 +00:00
// Branch exists, use it
targetRepo = repo
targetPath = strings . Trim ( strings . Join ( path , "/" ) , "/" )
targetBranch = branchTimestampResult . branch
2021-08-22 15:59:30 +00:00
2021-03-19 19:58:53 +00:00
targetOptions . BranchTimestamp = branchTimestampResult . timestamp
if canonicalLink != "" {
// Hide from search machines & add canonical link
ctx . Response . Header . Set ( "X-Robots-Tag" , "noarchive, noindex" )
ctx . Response . Header . Set ( "Link" ,
strings . NewReplacer ( "%b" , targetBranch , "%p" , targetPath ) . Replace ( canonicalLink ) +
"; rel=\"canonical\"" ,
)
}
return true
2021-03-19 19:30:08 +00:00
}
// tryUpstream forwards the target request to the Gitea API, and shows an error page on failure.
var tryUpstream = func ( ) {
2021-07-08 23:16:00 +00:00
// check if a canonical domain exists on a request on MainDomain
2021-11-20 14:30:58 +00:00
if bytes . HasSuffix ( trimmedHost , MainDomainSuffix ) {
canonicalDomain , _ := checkCanonicalDomain ( targetOwner , targetRepo , targetBranch , "" )
2021-07-08 23:16:00 +00:00
if ! strings . HasSuffix ( strings . SplitN ( canonicalDomain , "/" , 2 ) [ 0 ] , string ( MainDomainSuffix ) ) {
canonicalPath := string ( ctx . RequestURI ( ) )
if targetRepo != "pages" {
canonicalPath = "/" + strings . SplitN ( canonicalPath , "/" , 3 ) [ 2 ]
}
2021-11-25 15:12:28 +00:00
ctx . Redirect ( "https://" + canonicalDomain + canonicalPath , fasthttp . StatusTemporaryRedirect )
2021-07-08 23:16:00 +00:00
return
}
}
2021-03-19 19:30:08 +00:00
// Try to request the file from the Gitea API
if ! upstream ( ctx , targetOwner , targetRepo , targetBranch , targetPath , targetOptions ) {
returnErrorPage ( ctx , ctx . Response . StatusCode ( ) )
}
}
2021-07-13 08:28:06 +00:00
s . Step ( "preparations" )
2021-11-20 14:30:58 +00:00
if RawDomain != nil && bytes . Equal ( trimmedHost , RawDomain ) {
2021-03-16 23:34:31 +00:00
// Serve raw content from RawDomain
2021-07-13 08:28:06 +00:00
s . Debug ( "raw domain" )
2021-03-16 23:34:31 +00:00
targetOptions . TryIndexPages = false
targetOptions . ForbiddenMimeTypes [ "text/html" ] = struct { } { }
targetOptions . DefaultMimeType = "text/plain; charset=utf-8"
2021-03-19 19:30:08 +00:00
pathElements := strings . Split ( string ( bytes . Trim ( ctx . Request . URI ( ) . Path ( ) , "/" ) ) , "/" )
2021-03-16 23:34:31 +00:00
if len ( pathElements ) < 2 {
// https://{RawDomain}/{owner}/{repo}[/@{branch}]/{path} is required
ctx . Redirect ( RawInfoPage , fasthttp . StatusTemporaryRedirect )
return
}
targetOwner = pathElements [ 0 ]
targetRepo = pathElements [ 1 ]
2021-11-20 14:37:36 +00:00
// raw.codeberg.org/example/myrepo/@main/index.html
2021-03-19 19:30:08 +00:00
if len ( pathElements ) > 2 && strings . HasPrefix ( pathElements [ 2 ] , "@" ) {
2021-07-13 08:28:06 +00:00
s . Step ( "raw domain preparations, now trying with specified branch" )
2021-03-19 19:30:08 +00:00
if tryBranch ( targetRepo , pathElements [ 2 ] [ 1 : ] , pathElements [ 3 : ] ,
2021-11-26 16:03:58 +00:00
string ( GiteaRoot ) + "/" + targetOwner + "/" + targetRepo + "/src/branch/%b/%p" ,
2021-03-19 19:30:08 +00:00
) {
2021-07-13 08:28:06 +00:00
s . Step ( "tryBranch, now trying upstream" )
2021-03-19 19:30:08 +00:00
tryUpstream ( )
return
2021-03-16 23:34:31 +00:00
}
2021-07-13 08:28:06 +00:00
s . Debug ( "missing branch" )
2021-03-19 19:30:08 +00:00
returnErrorPage ( ctx , fasthttp . StatusFailedDependency )
return
} else {
2021-07-13 08:28:06 +00:00
s . Step ( "raw domain preparations, now trying with default branch" )
2021-03-19 19:30:08 +00:00
tryBranch ( targetRepo , "" , pathElements [ 2 : ] ,
2021-11-26 16:03:58 +00:00
string ( GiteaRoot ) + "/" + targetOwner + "/" + targetRepo + "/src/branch/%b/%p" ,
2021-03-19 19:30:08 +00:00
)
2021-07-13 08:28:06 +00:00
s . Step ( "tryBranch, now trying upstream" )
2021-03-19 19:30:08 +00:00
tryUpstream ( )
return
2021-03-16 23:34:31 +00:00
}
2021-11-20 14:30:58 +00:00
} else if bytes . HasSuffix ( trimmedHost , MainDomainSuffix ) {
2021-03-16 23:34:31 +00:00
// Serve pages from subdomains of MainDomainSuffix
2021-07-13 08:28:06 +00:00
s . Debug ( "main domain suffix" )
2021-03-16 23:34:31 +00:00
2021-03-19 19:30:08 +00:00
pathElements := strings . Split ( string ( bytes . Trim ( ctx . Request . URI ( ) . Path ( ) , "/" ) ) , "/" )
2021-11-20 14:30:58 +00:00
targetOwner = string ( bytes . TrimSuffix ( trimmedHost , MainDomainSuffix ) )
2021-03-16 23:34:31 +00:00
targetRepo = pathElements [ 0 ]
2021-03-19 19:30:08 +00:00
targetPath = strings . Trim ( strings . Join ( pathElements [ 1 : ] , "/" ) , "/" )
2021-12-01 23:00:00 +00:00
if targetOwner == "www" {
// www.codeberg.page redirects to codeberg.page
ctx . Redirect ( "https://" + string ( MainDomainSuffix [ 1 : ] ) + string ( ctx . Path ( ) ) , fasthttp . StatusPermanentRedirect )
return
}
2021-03-19 19:30:08 +00:00
// Check if the first directory is a repo with the second directory as a branch
// example.codeberg.page/myrepo/@main/index.html
if len ( pathElements ) > 1 && strings . HasPrefix ( pathElements [ 1 ] , "@" ) {
2021-03-19 20:33:57 +00:00
if targetRepo == "pages" {
// example.codeberg.org/pages/@... redirects to example.codeberg.org/@...
2021-11-25 15:12:28 +00:00
ctx . Redirect ( "/" + strings . Join ( pathElements [ 1 : ] , "/" ) , fasthttp . StatusTemporaryRedirect )
2021-03-19 20:33:57 +00:00
return
}
2021-07-13 08:28:06 +00:00
s . Step ( "main domain preparations, now trying with specified repo & branch" )
2021-03-19 19:30:08 +00:00
if tryBranch ( pathElements [ 0 ] , pathElements [ 1 ] [ 1 : ] , pathElements [ 2 : ] ,
"/" + pathElements [ 0 ] + "/%p" ,
) {
2021-07-13 08:28:06 +00:00
s . Step ( "tryBranch, now trying upstream" )
2021-03-19 19:30:08 +00:00
tryUpstream ( )
} else {
returnErrorPage ( ctx , fasthttp . StatusFailedDependency )
}
return
}
// Check if the first directory is a branch for the "pages" repo
// example.codeberg.page/@main/index.html
if strings . HasPrefix ( pathElements [ 0 ] , "@" ) {
2021-07-13 08:28:06 +00:00
s . Step ( "main domain preparations, now trying with specified branch" )
2021-03-19 19:30:08 +00:00
if tryBranch ( "pages" , pathElements [ 0 ] [ 1 : ] , pathElements [ 1 : ] , "/%p" ) {
2021-07-13 08:28:06 +00:00
s . Step ( "tryBranch, now trying upstream" )
2021-03-19 19:30:08 +00:00
tryUpstream ( )
} else {
returnErrorPage ( ctx , fasthttp . StatusFailedDependency )
}
return
2021-03-16 23:34:31 +00:00
}
// Check if the first directory is a repo with a "pages" branch
2021-03-19 19:30:08 +00:00
// example.codeberg.page/myrepo/index.html
2021-03-19 20:33:57 +00:00
// example.codeberg.page/pages/... is not allowed here.
2021-07-13 08:28:06 +00:00
s . Step ( "main domain preparations, now trying with specified repo" )
2021-03-19 20:33:57 +00:00
if pathElements [ 0 ] != "pages" && tryBranch ( pathElements [ 0 ] , "pages" , pathElements [ 1 : ] , "" ) {
2021-07-13 08:28:06 +00:00
s . Step ( "tryBranch, now trying upstream" )
2021-03-19 19:30:08 +00:00
tryUpstream ( )
return
}
// Try to use the "pages" repo on its default branch
// example.codeberg.page/index.html
2021-07-13 08:28:06 +00:00
s . Step ( "main domain preparations, now trying with default repo/branch" )
2021-03-19 19:30:08 +00:00
if tryBranch ( "pages" , "" , pathElements , "" ) {
2021-07-13 08:28:06 +00:00
s . Step ( "tryBranch, now trying upstream" )
2021-03-19 19:30:08 +00:00
tryUpstream ( )
return
2021-03-16 23:34:31 +00:00
}
2021-03-19 19:30:08 +00:00
// Couldn't find a valid repo/branch
returnErrorPage ( ctx , fasthttp . StatusFailedDependency )
return
2021-03-16 23:34:31 +00:00
} else {
2021-11-20 14:30:58 +00:00
trimmedHostStr := string ( trimmedHost )
2021-03-16 23:34:31 +00:00
// Serve pages from external domains
2021-11-20 14:30:58 +00:00
targetOwner , targetRepo , targetBranch = getTargetFromDNS ( trimmedHostStr )
2021-03-16 23:34:31 +00:00
if targetOwner == "" {
2021-12-01 21:59:38 +00:00
returnErrorPage ( ctx , fasthttp . StatusFailedDependency )
2021-03-16 23:34:31 +00:00
return
}
2021-07-08 23:16:00 +00:00
pathElements := strings . Split ( string ( bytes . Trim ( ctx . Request . URI ( ) . Path ( ) , "/" ) ) , "/" )
canonicalLink := ""
if strings . HasPrefix ( pathElements [ 0 ] , "@" ) {
targetBranch = pathElements [ 0 ] [ 1 : ]
pathElements = pathElements [ 1 : ]
canonicalLink = "/%p"
}
// Try to use the given repo on the given branch or the default branch
2021-07-13 08:28:06 +00:00
s . Step ( "custom domain preparations, now trying with details from DNS" )
2021-07-08 23:16:00 +00:00
if tryBranch ( targetRepo , targetBranch , pathElements , canonicalLink ) {
2021-11-20 14:30:58 +00:00
canonicalDomain , valid := checkCanonicalDomain ( targetOwner , targetRepo , targetBranch , trimmedHostStr )
if ! valid {
returnErrorPage ( ctx , fasthttp . StatusMisdirectedRequest )
return
} else if canonicalDomain != trimmedHostStr {
2021-07-13 08:28:06 +00:00
// only redirect if the target is also a codeberg page!
2021-07-08 23:16:00 +00:00
targetOwner , _ , _ = getTargetFromDNS ( strings . SplitN ( canonicalDomain , "/" , 2 ) [ 0 ] )
if targetOwner != "" {
ctx . Redirect ( "https://" + canonicalDomain + string ( ctx . RequestURI ( ) ) , fasthttp . StatusTemporaryRedirect )
return
} else {
2021-12-01 21:59:38 +00:00
returnErrorPage ( ctx , fasthttp . StatusFailedDependency )
2021-07-08 23:16:00 +00:00
return
}
}
2021-07-13 08:28:06 +00:00
s . Step ( "tryBranch, now trying upstream" )
2021-07-08 23:16:00 +00:00
tryUpstream ( )
return
} else {
returnErrorPage ( ctx , fasthttp . StatusFailedDependency )
return
}
2021-03-16 23:34:31 +00:00
}
}
// returnErrorPage sets the response status code and writes NotFoundPage to the response body, with "%status" replaced
// with the provided status code.
func returnErrorPage ( ctx * fasthttp . RequestCtx , code int ) {
ctx . Response . SetStatusCode ( code )
2021-03-17 00:16:57 +00:00
ctx . Response . Header . SetContentType ( "text/html; charset=utf-8" )
2021-07-13 08:28:36 +00:00
message := fasthttp . StatusMessage ( code )
2021-11-20 14:30:58 +00:00
if code == fasthttp . StatusMisdirectedRequest {
message += " - domain not specified in <code>.domains</code> file"
}
2021-07-13 08:28:36 +00:00
if code == fasthttp . StatusFailedDependency {
2021-12-01 20:44:54 +00:00
message += " - target repo/branch doesn't exist or is private"
2021-07-13 08:28:36 +00:00
}
ctx . Response . SetBody ( bytes . ReplaceAll ( NotFoundPage , [ ] byte ( "%status" ) , [ ] byte ( strconv . Itoa ( code ) + " " + message ) ) )
2021-03-16 23:34:31 +00:00
}
2021-11-25 15:18:28 +00:00
// DefaultBranchCacheTimeout specifies the timeout for the default branch cache. It can be quite long.
2021-11-25 15:12:28 +00:00
var DefaultBranchCacheTimeout = 15 * time . Minute
2021-07-13 08:28:06 +00:00
// BranchExistanceCacheTimeout specifies the timeout for the branch timestamp & existance cache. It should be shorter
// 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.
2021-11-25 15:12:28 +00:00
var BranchExistanceCacheTimeout = 5 * time . Minute
2021-07-08 21:08:30 +00:00
// branchTimestampCache stores branch timestamps for faster cache checking
var branchTimestampCache = mcache . New ( )
2021-11-25 15:12:28 +00:00
2021-03-19 19:58:53 +00:00
type branchTimestamp struct {
2021-11-25 15:12:28 +00:00
branch string
2021-03-19 19:58:53 +00:00
timestamp time . Time
}
2021-07-08 21:08:30 +00:00
2021-07-13 08:28:06 +00:00
// FileCacheTimeout specifies the timeout for the file content cache - you might want to make this quite long, depending
// on your available memory.
2021-11-25 15:12:28 +00:00
var FileCacheTimeout = 5 * time . Minute
2021-07-13 08:28:06 +00:00
// FileCacheSizeLimit limits the maximum file size that will be cached, and is set to 1 MB by default.
var FileCacheSizeLimit = 1024 * 1024
2021-11-25 15:12:28 +00:00
2021-07-08 21:08:30 +00:00
// fileResponseCache stores responses from the Gitea server
2021-11-24 18:09:37 +00:00
// TODO: make this an MRU cache with a size limit
2021-07-08 21:08:30 +00:00
var fileResponseCache = mcache . New ( )
2021-11-25 15:12:28 +00:00
2021-07-08 21:08:30 +00:00
type fileResponse struct {
2021-11-25 15:12:28 +00:00
exists bool
2021-07-08 21:08:30 +00:00
mimeType string
2021-11-25 15:12:28 +00:00
body [ ] byte
2021-07-08 21:08:30 +00:00
}
2021-03-19 19:58:53 +00:00
2021-03-16 23:34:31 +00:00
// getBranchTimestamp finds the default branch (if branch is "") and returns the last modification time of the branch
2021-08-22 15:59:30 +00:00
// (or nil if the branch doesn't exist)
2021-03-19 19:58:53 +00:00
func getBranchTimestamp ( owner , repo , branch string ) * branchTimestamp {
if result , ok := branchTimestampCache . Get ( owner + "/" + repo + "/" + branch ) ; ok {
2021-07-13 08:28:06 +00:00
if result == nil {
return nil
}
2021-03-19 19:58:53 +00:00
return result . ( * branchTimestamp )
}
result := & branchTimestamp { }
result . branch = branch
2021-03-16 23:34:31 +00:00
if branch == "" {
2021-07-13 08:28:06 +00:00
// Get default branch
2021-03-16 23:34:31 +00:00
var body = make ( [ ] byte , 0 )
2021-11-26 16:10:31 +00:00
status , body , err := fasthttp . GetTimeout ( body , string ( GiteaRoot ) + "/api/v1/repos/" + owner + "/" + repo + "?access_token=" + GiteaApiToken , 5 * time . Second )
2021-03-16 23:34:31 +00:00
if err != nil || status != 200 {
2021-11-25 15:12:28 +00:00
_ = branchTimestampCache . Set ( owner + "/" + repo + "/" + branch , nil , DefaultBranchCacheTimeout )
2021-03-19 19:58:53 +00:00
return nil
2021-03-16 23:34:31 +00:00
}
2021-07-08 21:08:30 +00:00
result . branch = fastjson . GetString ( body , "default_branch" )
2021-03-16 23:34:31 +00:00
}
var body = make ( [ ] byte , 0 )
2021-11-26 16:10:31 +00:00
status , body , err := fasthttp . GetTimeout ( body , string ( GiteaRoot ) + "/api/v1/repos/" + owner + "/" + repo + "/branches/" + branch + "?access_token=" + GiteaApiToken , 5 * time . Second )
2021-03-16 23:34:31 +00:00
if err != nil || status != 200 {
2021-03-19 19:58:53 +00:00
return nil
2021-03-16 23:34:31 +00:00
}
2021-03-19 19:58:53 +00:00
result . timestamp , _ = time . Parse ( time . RFC3339 , fastjson . GetString ( body , "commit" , "timestamp" ) )
2021-11-25 15:12:28 +00:00
_ = branchTimestampCache . Set ( owner + "/" + repo + "/" + branch , result , BranchExistanceCacheTimeout )
2021-03-19 19:58:53 +00:00
return result
}
var upstreamClient = fasthttp . Client {
2021-11-25 15:12:28 +00:00
ReadTimeout : 10 * time . Second ,
MaxConnDuration : 60 * time . Second ,
2021-03-19 19:58:53 +00:00
MaxConnWaitTimeout : 1000 * time . Millisecond ,
2021-11-25 15:12:28 +00:00
MaxConnsPerHost : 128 * 16 , // TODO: adjust bottlenecks for best performance with Gitea!
2021-03-16 23:34:31 +00:00
}
2021-07-08 21:08:30 +00:00
// upstream requests a file from the Gitea API at GiteaRoot and writes it to the request context.
func upstream ( ctx * fasthttp . RequestCtx , targetOwner string , targetRepo string , targetBranch string , targetPath string , options * upstreamOptions ) ( final bool ) {
2021-07-13 08:28:06 +00:00
s := debug_stepper . Start ( "upstream" )
defer s . Complete ( )
2021-03-16 23:34:31 +00:00
if options . ForbiddenMimeTypes == nil {
options . ForbiddenMimeTypes = map [ string ] struct { } { }
}
// Check if the branch exists and when it was modified
if options . BranchTimestamp == ( time . Time { } ) {
2021-03-19 19:58:53 +00:00
branch := getBranchTimestamp ( targetOwner , targetRepo , targetBranch )
2021-03-17 00:16:57 +00:00
2021-03-19 19:58:53 +00:00
if branch == nil {
returnErrorPage ( ctx , fasthttp . StatusFailedDependency )
return true
}
targetBranch = branch . branch
options . BranchTimestamp = branch . timestamp
2021-03-16 23:34:31 +00:00
}
2021-03-19 19:30:08 +00:00
if targetOwner == "" || targetRepo == "" || targetBranch == "" {
returnErrorPage ( ctx , fasthttp . StatusBadRequest )
return true
}
2021-03-16 23:34:31 +00:00
// 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 ( options . BranchTimestamp ) {
ctx . Response . SetStatusCode ( fasthttp . StatusNotModified )
return true
}
}
2021-07-13 08:28:06 +00:00
s . Step ( "preparations" )
2021-03-16 23:34:31 +00:00
// Make a GET request to the upstream URL
2021-07-13 08:28:06 +00:00
uri := targetOwner + "/" + targetRepo + "/raw/" + targetBranch + "/" + targetPath
var req * fasthttp . Request
var res * fasthttp . Response
2021-07-08 21:08:30 +00:00
var cachedResponse fileResponse
var err error
2021-12-02 18:11:13 +00:00
if cachedValue , ok := fileResponseCache . Get ( uri + "?timestamp=" + strconv . FormatInt ( options . BranchTimestamp . Unix ( ) , 10 ) ) ; ok && len ( cachedValue . ( fileResponse ) . body ) > 0 {
2021-07-08 21:08:30 +00:00
cachedResponse = cachedValue . ( fileResponse )
} else {
2021-07-13 08:28:06 +00:00
req = fasthttp . AcquireRequest ( )
2021-11-26 16:10:31 +00:00
req . SetRequestURI ( string ( GiteaRoot ) + "/api/v1/repos/" + uri + "?access_token=" + GiteaApiToken )
2021-07-13 08:28:06 +00:00
res = fasthttp . AcquireResponse ( )
res . SetBodyStream ( & strings . Reader { } , - 1 )
2021-07-08 21:08:30 +00:00
err = upstreamClient . Do ( req , res )
}
2021-07-13 08:28:06 +00:00
s . Step ( "acquisition" )
2021-03-16 23:34:31 +00:00
// Handle errors
2021-07-13 08:28:06 +00:00
if ( res == nil && ! cachedResponse . exists ) || ( res != nil && res . StatusCode ( ) == fasthttp . StatusNotFound ) {
2021-03-16 23:34:31 +00:00
if options . TryIndexPages {
// copy the options struct & try if an index page exists
optionsForIndexPages := * options
optionsForIndexPages . TryIndexPages = false
optionsForIndexPages . AppendTrailingSlash = true
for _ , indexPage := range IndexPages {
if upstream ( ctx , targetOwner , targetRepo , targetBranch , strings . TrimSuffix ( targetPath , "/" ) + "/" + indexPage , & optionsForIndexPages ) {
2021-11-25 15:12:28 +00:00
_ = fileResponseCache . Set ( uri + "?timestamp=" + strconv . FormatInt ( options . BranchTimestamp . Unix ( ) , 10 ) , fileResponse {
2021-07-13 08:28:06 +00:00
exists : false ,
} , FileCacheTimeout )
2021-03-16 23:34:31 +00:00
return true
}
}
2021-12-02 09:16:23 +00:00
// compatibility fix for GitHub Pages (/example → /example.html)
optionsForIndexPages . AppendTrailingSlash = false
2021-12-05 21:12:48 +00:00
optionsForIndexPages . RedirectIfExists = strings . TrimSuffix ( string ( ctx . Request . URI ( ) . Path ( ) ) , "/" ) + ".html"
2021-12-02 09:16:23 +00:00
if upstream ( ctx , targetOwner , targetRepo , targetBranch , targetPath + ".html" , & optionsForIndexPages ) {
_ = fileResponseCache . Set ( uri + "?timestamp=" + strconv . FormatInt ( options . BranchTimestamp . Unix ( ) , 10 ) , fileResponse {
exists : false ,
} , FileCacheTimeout )
return true
}
2021-03-16 23:34:31 +00:00
}
ctx . Response . SetStatusCode ( fasthttp . StatusNotFound )
2021-07-13 08:28:06 +00:00
if res != nil {
2021-07-08 21:08:30 +00:00
// Update cache if the request is fresh
2021-11-25 15:12:28 +00:00
_ = fileResponseCache . Set ( uri + "?timestamp=" + strconv . FormatInt ( options . BranchTimestamp . Unix ( ) , 10 ) , fileResponse {
2021-07-08 21:08:30 +00:00
exists : false ,
} , FileCacheTimeout )
}
2021-03-16 23:34:31 +00:00
return false
}
2021-07-13 08:28:06 +00:00
if res != nil && ( err != nil || res . StatusCode ( ) != fasthttp . StatusOK ) {
2021-03-16 23:34:31 +00:00
fmt . Printf ( "Couldn't fetch contents from \"%s\": %s (status code %d)\n" , req . RequestURI ( ) , err , res . StatusCode ( ) )
returnErrorPage ( ctx , fasthttp . StatusInternalServerError )
return true
}
2021-12-02 09:16:23 +00:00
// Append trailing slash if missing (for index files), and redirect to fix filenames in general
2021-07-08 21:08:30 +00:00
// options.AppendTrailingSlash is only true when looking for index pages
2021-03-16 23:34:31 +00:00
if options . AppendTrailingSlash && ! bytes . HasSuffix ( ctx . Request . URI ( ) . Path ( ) , [ ] byte { '/' } ) {
ctx . Redirect ( string ( ctx . Request . URI ( ) . Path ( ) ) + "/" , fasthttp . StatusTemporaryRedirect )
return true
}
2021-12-02 09:16:23 +00:00
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 options . RedirectIfExists != "" {
ctx . Redirect ( options . RedirectIfExists , fasthttp . StatusTemporaryRedirect )
return true
}
2021-07-13 08:28:06 +00:00
s . Step ( "error handling" )
2021-03-16 23:34:31 +00:00
// Set the MIME type
mimeType := mime . TypeByExtension ( path . Ext ( targetPath ) )
mimeTypeSplit := strings . SplitN ( mimeType , ";" , 2 )
if _ , ok := options . ForbiddenMimeTypes [ mimeTypeSplit [ 0 ] ] ; ok || mimeType == "" {
if options . DefaultMimeType != "" {
mimeType = options . DefaultMimeType
} else {
mimeType = "application/octet-stream"
}
}
ctx . Response . Header . SetContentType ( mimeType )
2021-07-08 21:08:30 +00:00
// Everything's okay so far
2021-03-16 23:34:31 +00:00
ctx . Response . SetStatusCode ( fasthttp . StatusOK )
ctx . Response . Header . SetLastModified ( options . BranchTimestamp )
2021-07-08 21:08:30 +00:00
2021-07-13 08:28:06 +00:00
s . Step ( "response preparations" )
2021-07-08 21:08:30 +00:00
// Write the response body to the original request
var cacheBodyWriter bytes . Buffer
2021-07-13 08:28:06 +00:00
if res != nil {
if res . Header . ContentLength ( ) > FileCacheSizeLimit {
err = res . BodyWriteTo ( ctx . Response . BodyWriter ( ) )
} else {
err = res . BodyWriteTo ( io . MultiWriter ( ctx . Response . BodyWriter ( ) , & cacheBodyWriter ) )
}
2021-07-08 21:08:30 +00:00
} else {
_ , err = ctx . Write ( cachedResponse . body )
}
2021-03-16 23:34:31 +00:00
if err != nil {
fmt . Printf ( "Couldn't write body for \"%s\": %s\n" , req . RequestURI ( ) , err )
returnErrorPage ( ctx , fasthttp . StatusInternalServerError )
return true
}
2021-07-13 08:28:06 +00:00
s . Step ( "response" )
2021-03-16 23:34:31 +00:00
2021-12-02 18:11:13 +00:00
if res != nil && ctx . Err ( ) == nil {
2021-07-08 21:08:30 +00:00
cachedResponse . exists = true
cachedResponse . mimeType = mimeType
cachedResponse . body = cacheBodyWriter . Bytes ( )
2021-11-25 15:12:28 +00:00
_ = fileResponseCache . Set ( uri + "?timestamp=" + strconv . FormatInt ( options . BranchTimestamp . Unix ( ) , 10 ) , cachedResponse , FileCacheTimeout )
2021-07-08 21:08:30 +00:00
}
2021-03-16 23:34:31 +00:00
return true
}
// upstreamOptions provides various options for the upstream request.
type upstreamOptions struct {
DefaultMimeType string
ForbiddenMimeTypes map [ string ] struct { }
TryIndexPages bool
AppendTrailingSlash bool
2021-12-02 09:16:23 +00:00
RedirectIfExists string
2021-03-16 23:34:31 +00:00
BranchTimestamp time . Time
}