2021-12-05 13:45:17 +00:00
package server
import (
"bytes"
"strings"
2021-12-05 14:02:44 +00:00
"github.com/rs/zerolog/log"
"github.com/valyala/fasthttp"
2021-12-05 13:45:17 +00:00
"codeberg.org/codeberg/pages/html"
2021-12-05 14:02:44 +00:00
"codeberg.org/codeberg/pages/server/cache"
2021-12-05 14:21:05 +00:00
"codeberg.org/codeberg/pages/server/dns"
2021-12-05 13:47:33 +00:00
"codeberg.org/codeberg/pages/server/upstream"
2021-12-03 02:44:21 +00:00
"codeberg.org/codeberg/pages/server/utils"
2021-12-05 13:45:17 +00:00
)
// Handler handles a single HTTP request to the web server.
2021-12-05 14:02:44 +00:00
func Handler ( mainDomainSuffix , rawDomain [ ] byte ,
2021-12-05 17:18:05 +00:00
giteaRoot , rawInfoPage , giteaAPIToken string ,
2021-12-05 14:02:44 +00:00
blacklistedPaths , allowedCorsDomains [ ] [ ] byte ,
2022-03-27 19:54:06 +00:00
dnsLookupCache , canonicalDomainCache , branchTimestampCache , fileResponseCache cache . SetGetKey ,
) func ( ctx * fasthttp . RequestCtx ) {
2021-12-05 13:45:17 +00:00
return func ( ctx * fasthttp . RequestCtx ) {
log := log . With ( ) . Str ( "Handler" , string ( ctx . Request . Header . RequestURI ( ) ) ) . Logger ( )
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" )
// Enable browser caching for up to 10 minutes
ctx . Response . Header . Set ( "Cache-Control" , "public, max-age=600" )
2021-12-03 02:44:21 +00:00
trimmedHost := utils . TrimHostPort ( ctx . Request . Host ( ) )
2021-12-05 13:45:17 +00:00
// Add HSTS for RawDomain and MainDomainSuffix
if hsts := GetHSTSHeader ( trimmedHost , mainDomainSuffix , rawDomain ) ; hsts != "" {
ctx . Response . Header . Set ( "Strict-Transport-Security" , hsts )
}
// 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 ) {
2021-12-05 13:47:33 +00:00
html . ReturnErrorPage ( ctx , fasthttp . StatusForbidden )
2021-12-05 13:45:17 +00:00
return
}
}
// Allow CORS for specified domains
2022-04-10 16:11:00 +00:00
allowCors := false
for _ , allowedCorsDomain := range allowedCorsDomains {
if bytes . Equal ( trimmedHost , allowedCorsDomain ) {
allowCors = true
break
2021-12-05 13:45:17 +00:00
}
2022-04-10 16:11:00 +00:00
}
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" )
if ctx . IsOptions ( ) {
2021-12-05 13:45:17 +00:00
ctx . Response . Header . SetStatusCode ( fasthttp . StatusNoContent )
return
}
// Prepare request information to Gitea
var targetOwner , targetRepo , targetBranch , targetPath string
2022-03-27 19:54:06 +00:00
targetOptions := & upstream . Options {
2021-12-05 13:45:17 +00:00
ForbiddenMimeTypes : map [ string ] struct { } { } ,
TryIndexPages : true ,
}
// 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.
2022-03-27 19:54:06 +00:00
tryBranch := func ( repo , branch string , path [ ] string , canonicalLink string ) bool {
2021-12-05 13:45:17 +00:00
if repo == "" {
return false
}
// Check if the branch exists, otherwise treat it as a file path
2021-12-05 17:18:05 +00:00
branchTimestampResult := upstream . GetBranchTimestamp ( targetOwner , repo , branch , giteaRoot , giteaAPIToken , branchTimestampCache )
2021-12-05 13:45:17 +00:00
if branchTimestampResult == nil {
// branch doesn't exist
return false
}
// Branch exists, use it
targetRepo = repo
targetPath = strings . Trim ( strings . Join ( path , "/" ) , "/" )
2021-12-05 13:47:33 +00:00
targetBranch = branchTimestampResult . Branch
2021-12-05 13:45:17 +00:00
2021-12-05 13:47:33 +00:00
targetOptions . BranchTimestamp = branchTimestampResult . Timestamp
2021-12-05 13:45:17 +00:00
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
}
log . Debug ( ) . Msg ( "preparations" )
if rawDomain != nil && bytes . Equal ( trimmedHost , rawDomain ) {
// Serve raw content from RawDomain
log . Debug ( ) . Msg ( "raw domain" )
targetOptions . TryIndexPages = false
targetOptions . ForbiddenMimeTypes [ "text/html" ] = struct { } { }
targetOptions . DefaultMimeType = "text/plain; charset=utf-8"
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 )
return
}
targetOwner = pathElements [ 0 ]
targetRepo = pathElements [ 1 ]
// raw.codeberg.org/example/myrepo/@main/index.html
if len ( pathElements ) > 2 && strings . HasPrefix ( pathElements [ 2 ] , "@" ) {
log . Debug ( ) . Msg ( "raw domain preparations, now trying with specified branch" )
if tryBranch ( targetRepo , pathElements [ 2 ] [ 1 : ] , pathElements [ 3 : ] ,
2021-12-03 02:05:38 +00:00
giteaRoot + "/" + targetOwner + "/" + targetRepo + "/src/branch/%b/%p" ,
2021-12-05 13:45:17 +00:00
) {
2022-06-10 13:25:33 +00:00
log . Debug ( ) . Msg ( "tryBranch, now trying upstream 1" )
2021-12-05 17:17:28 +00:00
tryUpstream ( ctx , mainDomainSuffix , trimmedHost ,
targetOptions , targetOwner , targetRepo , targetBranch , targetPath ,
2021-12-05 17:18:05 +00:00
giteaRoot , giteaAPIToken ,
2021-12-05 17:17:28 +00:00
canonicalDomainCache , branchTimestampCache , fileResponseCache )
2021-12-05 13:45:17 +00:00
return
}
log . Debug ( ) . Msg ( "missing branch" )
2021-12-05 13:47:33 +00:00
html . ReturnErrorPage ( ctx , fasthttp . StatusFailedDependency )
2021-12-05 13:45:17 +00:00
return
}
2021-12-05 18:53:23 +00:00
log . Debug ( ) . Msg ( "raw domain preparations, now trying with default branch" )
tryBranch ( targetRepo , "" , pathElements [ 2 : ] ,
giteaRoot + "/" + targetOwner + "/" + targetRepo + "/src/branch/%b/%p" ,
)
2022-06-10 13:25:33 +00:00
log . Debug ( ) . Msg ( "tryBranch, now trying upstream 2" )
2021-12-05 18:53:23 +00:00
tryUpstream ( ctx , mainDomainSuffix , trimmedHost ,
targetOptions , targetOwner , targetRepo , targetBranch , targetPath ,
giteaRoot , giteaAPIToken ,
canonicalDomainCache , branchTimestampCache , fileResponseCache )
return
2021-12-05 13:45:17 +00:00
} else if bytes . HasSuffix ( trimmedHost , mainDomainSuffix ) {
// Serve pages from subdomains of MainDomainSuffix
log . Debug ( ) . Msg ( "main domain suffix" )
pathElements := strings . Split ( string ( bytes . Trim ( ctx . Request . URI ( ) . Path ( ) , "/" ) ) , "/" )
targetOwner = string ( bytes . TrimSuffix ( trimmedHost , mainDomainSuffix ) )
targetRepo = pathElements [ 0 ]
targetPath = strings . Trim ( strings . Join ( pathElements [ 1 : ] , "/" ) , "/" )
if targetOwner == "www" {
2021-12-05 18:53:23 +00:00
// www.codeberg.page redirects to codeberg.page // TODO: rm hardcoded - use cname?
2021-12-05 13:45:17 +00:00
ctx . Redirect ( "https://" + string ( mainDomainSuffix [ 1 : ] ) + string ( ctx . Path ( ) ) , fasthttp . StatusPermanentRedirect )
return
}
// 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 ] , "@" ) {
if targetRepo == "pages" {
// example.codeberg.org/pages/@... redirects to example.codeberg.org/@...
ctx . Redirect ( "/" + strings . Join ( pathElements [ 1 : ] , "/" ) , fasthttp . StatusTemporaryRedirect )
return
}
log . Debug ( ) . Msg ( "main domain preparations, now trying with specified repo & branch" )
if tryBranch ( pathElements [ 0 ] , pathElements [ 1 ] [ 1 : ] , pathElements [ 2 : ] ,
"/" + pathElements [ 0 ] + "/%p" ,
) {
2022-06-10 13:25:33 +00:00
log . Debug ( ) . Msg ( "tryBranch, now trying upstream 3" )
2021-12-05 17:17:28 +00:00
tryUpstream ( ctx , mainDomainSuffix , trimmedHost ,
targetOptions , targetOwner , targetRepo , targetBranch , targetPath ,
2021-12-05 17:18:05 +00:00
giteaRoot , giteaAPIToken ,
2021-12-05 17:17:28 +00:00
canonicalDomainCache , branchTimestampCache , fileResponseCache )
2021-12-05 13:45:17 +00:00
} else {
2021-12-05 13:47:33 +00:00
html . ReturnErrorPage ( ctx , fasthttp . StatusFailedDependency )
2021-12-05 13:45:17 +00:00
}
return
}
// Check if the first directory is a branch for the "pages" repo
// example.codeberg.page/@main/index.html
if strings . HasPrefix ( pathElements [ 0 ] , "@" ) {
log . Debug ( ) . Msg ( "main domain preparations, now trying with specified branch" )
if tryBranch ( "pages" , pathElements [ 0 ] [ 1 : ] , pathElements [ 1 : ] , "/%p" ) {
2022-06-10 13:25:33 +00:00
log . Debug ( ) . Msg ( "tryBranch, now trying upstream 4" )
2021-12-05 17:17:28 +00:00
tryUpstream ( ctx , mainDomainSuffix , trimmedHost ,
targetOptions , targetOwner , targetRepo , targetBranch , targetPath ,
2021-12-05 17:18:05 +00:00
giteaRoot , giteaAPIToken ,
2021-12-05 17:17:28 +00:00
canonicalDomainCache , branchTimestampCache , fileResponseCache )
2021-12-05 13:45:17 +00:00
} else {
2021-12-05 13:47:33 +00:00
html . ReturnErrorPage ( ctx , fasthttp . StatusFailedDependency )
2021-12-05 13:45:17 +00:00
}
return
}
// Check if the first directory is a repo with a "pages" branch
// example.codeberg.page/myrepo/index.html
// example.codeberg.page/pages/... is not allowed here.
log . Debug ( ) . Msg ( "main domain preparations, now trying with specified repo" )
if pathElements [ 0 ] != "pages" && tryBranch ( pathElements [ 0 ] , "pages" , pathElements [ 1 : ] , "" ) {
2022-06-10 13:25:33 +00:00
log . Debug ( ) . Msg ( "tryBranch, now trying upstream 5" )
2021-12-05 17:17:28 +00:00
tryUpstream ( ctx , mainDomainSuffix , trimmedHost ,
targetOptions , targetOwner , targetRepo , targetBranch , targetPath ,
2021-12-05 17:18:05 +00:00
giteaRoot , giteaAPIToken ,
2021-12-05 17:17:28 +00:00
canonicalDomainCache , branchTimestampCache , fileResponseCache )
2021-12-05 13:45:17 +00:00
return
}
// Try to use the "pages" repo on its default branch
// example.codeberg.page/index.html
log . Debug ( ) . Msg ( "main domain preparations, now trying with default repo/branch" )
if tryBranch ( "pages" , "" , pathElements , "" ) {
2022-06-10 13:25:33 +00:00
log . Debug ( ) . Msg ( "tryBranch, now trying upstream 6" )
2021-12-05 17:17:28 +00:00
tryUpstream ( ctx , mainDomainSuffix , trimmedHost ,
targetOptions , targetOwner , targetRepo , targetBranch , targetPath ,
2021-12-05 17:18:05 +00:00
giteaRoot , giteaAPIToken ,
2021-12-05 17:17:28 +00:00
canonicalDomainCache , branchTimestampCache , fileResponseCache )
2021-12-05 13:45:17 +00:00
return
}
// Couldn't find a valid repo/branch
2021-12-05 13:47:33 +00:00
html . ReturnErrorPage ( ctx , fasthttp . StatusFailedDependency )
2021-12-05 13:45:17 +00:00
return
} else {
trimmedHostStr := string ( trimmedHost )
// Serve pages from external domains
2021-12-05 14:21:05 +00:00
targetOwner , targetRepo , targetBranch = dns . GetTargetFromDNS ( trimmedHostStr , string ( mainDomainSuffix ) , dnsLookupCache )
2021-12-05 13:45:17 +00:00
if targetOwner == "" {
2021-12-05 13:47:33 +00:00
html . ReturnErrorPage ( ctx , fasthttp . StatusFailedDependency )
2021-12-05 13:45:17 +00:00
return
}
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
log . Debug ( ) . Msg ( "custom domain preparations, now trying with details from DNS" )
if tryBranch ( targetRepo , targetBranch , pathElements , canonicalLink ) {
2021-12-05 17:18:05 +00:00
canonicalDomain , valid := upstream . CheckCanonicalDomain ( targetOwner , targetRepo , targetBranch , trimmedHostStr , string ( mainDomainSuffix ) , giteaRoot , giteaAPIToken , canonicalDomainCache )
2021-12-05 13:45:17 +00:00
if ! valid {
2021-12-05 13:47:33 +00:00
html . ReturnErrorPage ( ctx , fasthttp . StatusMisdirectedRequest )
2021-12-05 13:45:17 +00:00
return
} else if canonicalDomain != trimmedHostStr {
// only redirect if the target is also a codeberg page!
2021-12-05 14:21:05 +00:00
targetOwner , _ , _ = dns . GetTargetFromDNS ( strings . SplitN ( canonicalDomain , "/" , 2 ) [ 0 ] , string ( mainDomainSuffix ) , dnsLookupCache )
2021-12-05 13:45:17 +00:00
if targetOwner != "" {
ctx . Redirect ( "https://" + canonicalDomain + string ( ctx . RequestURI ( ) ) , fasthttp . StatusTemporaryRedirect )
return
}
2021-12-05 18:53:23 +00:00
html . ReturnErrorPage ( ctx , fasthttp . StatusFailedDependency )
return
2021-12-05 13:45:17 +00:00
}
2022-06-10 13:25:33 +00:00
log . Debug ( ) . Msg ( "tryBranch, now trying upstream 7" )
2021-12-05 17:17:28 +00:00
tryUpstream ( ctx , mainDomainSuffix , trimmedHost ,
targetOptions , targetOwner , targetRepo , targetBranch , targetPath ,
2021-12-05 17:18:05 +00:00
giteaRoot , giteaAPIToken ,
2021-12-05 17:17:28 +00:00
canonicalDomainCache , branchTimestampCache , fileResponseCache )
2021-12-05 13:45:17 +00:00
return
}
2021-12-05 18:53:23 +00:00
html . ReturnErrorPage ( ctx , fasthttp . StatusFailedDependency )
return
2021-12-05 13:45:17 +00:00
}
}
}