2021-12-05 13:45:17 +00:00
package server
import (
2022-11-07 21:47:03 +00:00
"fmt"
2022-07-27 15:25:08 +00:00
"net/http"
2022-11-07 21:47:03 +00:00
"path"
2021-12-05 13:45:17 +00:00
"strings"
2021-12-05 14:02:44 +00:00
"github.com/rs/zerolog/log"
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"
2022-08-28 13:33:10 +00:00
"codeberg.org/codeberg/pages/server/context"
2021-12-05 14:21:05 +00:00
"codeberg.org/codeberg/pages/server/dns"
2022-06-11 21:02:06 +00:00
"codeberg.org/codeberg/pages/server/gitea"
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"
2022-06-14 18:35:11 +00:00
"codeberg.org/codeberg/pages/server/version"
2021-12-05 13:45:17 +00:00
)
2022-08-28 13:33:10 +00:00
const (
headerAccessControlAllowOrigin = "Access-Control-Allow-Origin"
headerAccessControlAllowMethods = "Access-Control-Allow-Methods"
)
2021-12-05 13:45:17 +00:00
// Handler handles a single HTTP request to the web server.
2022-08-28 14:21:37 +00:00
func Handler ( mainDomainSuffix , rawDomain string ,
2022-06-11 21:02:06 +00:00
giteaClient * gitea . Client ,
2022-11-12 01:54:56 +00:00
rawInfoPage string ,
2022-08-28 13:33:10 +00:00
blacklistedPaths , allowedCorsDomains [ ] string ,
2022-07-27 13:39:46 +00:00
dnsLookupCache , canonicalDomainCache cache . SetGetKey ,
2022-07-27 15:25:08 +00:00
) http . HandlerFunc {
return func ( w http . ResponseWriter , req * http . Request ) {
2022-08-28 12:35:27 +00:00
log := log . With ( ) . Strs ( "Handler" , [ ] string { string ( req . Host ) , req . RequestURI } ) . Logger ( )
2022-08-28 13:33:10 +00:00
ctx := context . New ( w , req )
2021-12-05 13:45:17 +00:00
2022-08-28 13:33:10 +00:00
ctx . RespWriter . Header ( ) . Set ( "Server" , "CodebergPages/" + version . Version )
2021-12-05 13:45:17 +00:00
// 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
2022-08-28 13:33:10 +00:00
ctx . RespWriter . Header ( ) . Set ( "Referrer-Policy" , "strict-origin-when-cross-origin" )
2021-12-05 13:45:17 +00:00
// Enable browser caching for up to 10 minutes
2022-08-28 13:33:10 +00:00
ctx . RespWriter . Header ( ) . Set ( "Cache-Control" , "public, max-age=600" )
2021-12-05 13:45:17 +00:00
2022-08-28 14:21:37 +00:00
trimmedHost := utils . TrimHostPort ( req . Host )
2021-12-05 13:45:17 +00:00
// Add HSTS for RawDomain and MainDomainSuffix
2022-11-07 22:01:31 +00:00
if hsts := getHSTSHeader ( trimmedHost , mainDomainSuffix , rawDomain ) ; hsts != "" {
2022-08-28 13:33:10 +00:00
ctx . RespWriter . Header ( ) . Set ( "Strict-Transport-Security" , hsts )
2021-12-05 13:45:17 +00:00
}
2022-11-12 12:10:07 +00:00
// Handle all http methods
ctx . RespWriter . Header ( ) . Set ( "Allow" , http . MethodGet + ", " + http . MethodHead + ", " + http . MethodOptions )
switch ctx . Req . Method {
case http . MethodOptions :
// return Allow header
ctx . RespWriter . WriteHeader ( http . StatusNoContent )
return
case http . MethodGet ,
http . MethodHead :
// end switch case and handle allowed requests
break
default :
// Block all methods not required for static pages
2022-11-07 20:34:23 +00:00
ctx . String ( "Method not allowed" , http . StatusMethodNotAllowed )
2021-12-05 13:45:17 +00:00
return
}
// Block blacklisted paths (like ACME challenges)
for _ , blacklistedPath := range blacklistedPaths {
2022-08-28 13:33:10 +00:00
if strings . HasPrefix ( ctx . Path ( ) , blacklistedPath ) {
2022-11-07 21:47:03 +00:00
html . ReturnErrorPage ( ctx , "requested blacklisted path" , http . 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 {
2022-08-28 14:21:37 +00:00
if strings . EqualFold ( trimmedHost , allowedCorsDomain ) {
2022-04-10 16:11:00 +00:00
allowCors = true
break
2021-12-05 13:45:17 +00:00
}
2022-04-10 16:11:00 +00:00
}
if allowCors {
2022-08-28 13:33:10 +00:00
ctx . RespWriter . Header ( ) . Set ( headerAccessControlAllowOrigin , "*" )
ctx . RespWriter . Header ( ) . Set ( headerAccessControlAllowMethods , http . MethodGet + ", " + http . MethodHead )
2022-04-10 16:11:00 +00:00
}
2022-07-27 15:25:08 +00:00
2021-12-05 13:45:17 +00:00
// Prepare request information to Gitea
2022-11-12 13:08:08 +00:00
pathElements := strings . Split ( strings . Trim ( ctx . Path ( ) , "/" ) , "/" )
2021-12-05 13:45:17 +00:00
log . Debug ( ) . Msg ( "preparations" )
2022-08-28 14:21:37 +00:00
if rawDomain != "" && strings . EqualFold ( trimmedHost , rawDomain ) {
2021-12-05 13:45:17 +00:00
// Serve raw content from RawDomain
log . Debug ( ) . Msg ( "raw domain" )
if len ( pathElements ) < 2 {
// https://{RawDomain}/{owner}/{repo}[/@{branch}]/{path} is required
2022-07-27 15:25:08 +00:00
ctx . Redirect ( rawInfoPage , http . StatusTemporaryRedirect )
2021-12-05 13:45:17 +00:00
return
}
// 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" )
2022-11-12 16:50:08 +00:00
if targetOpt , works := tryBranch ( log , ctx , giteaClient , & upstream . Options {
TryIndexPages : false ,
ServeRaw : true ,
TargetOwner : pathElements [ 0 ] ,
TargetRepo : pathElements [ 1 ] ,
TargetBranch : pathElements [ 2 ] [ 1 : ] ,
TargetPath : path . Join ( pathElements [ 3 : ] ... ) ,
} , true ) ; works {
log . Trace ( ) . Msg ( "tryUpstream: serve raw domain with specified branch" )
tryUpstream ( ctx , giteaClient , mainDomainSuffix , trimmedHost , targetOpt , canonicalDomainCache )
2021-12-05 13:45:17 +00:00
return
}
2022-11-07 21:47:03 +00:00
log . Debug ( ) . Msg ( "missing branch info" )
html . ReturnErrorPage ( ctx , "missing branch info" , http . 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" )
2022-11-12 16:50:08 +00:00
if targetOpt , works := tryBranch ( log , ctx , giteaClient , & upstream . Options {
TryIndexPages : false ,
ServeRaw : true ,
TargetOwner : pathElements [ 0 ] ,
TargetRepo : pathElements [ 1 ] ,
TargetPath : path . Join ( pathElements [ 2 : ] ... ) ,
} , true ) ; works {
log . Trace ( ) . Msg ( "tryUpstream: serve raw domain with default branch" )
tryUpstream ( ctx , giteaClient , mainDomainSuffix , trimmedHost , targetOpt , canonicalDomainCache )
} else {
html . ReturnErrorPage ( ctx ,
fmt . Sprintf ( "raw domain could not find repo '%s/%s' or repo is empty" , targetOpt . TargetOwner , targetOpt . TargetRepo ) ,
http . StatusNotFound )
2022-11-12 01:54:56 +00:00
}
2021-12-05 18:53:23 +00:00
return
2022-08-28 14:21:37 +00:00
} else if strings . HasSuffix ( trimmedHost , mainDomainSuffix ) {
2021-12-05 13:45:17 +00:00
// Serve pages from subdomains of MainDomainSuffix
log . Debug ( ) . Msg ( "main domain suffix" )
2022-11-12 01:54:56 +00:00
targetOwner := strings . TrimSuffix ( trimmedHost , mainDomainSuffix )
targetRepo := pathElements [ 0 ]
2021-12-05 13:45:17 +00:00
if targetOwner == "www" {
2021-12-05 18:53:23 +00:00
// www.codeberg.page redirects to codeberg.page // TODO: rm hardcoded - use cname?
2022-07-27 15:25:08 +00:00
ctx . Redirect ( "https://" + string ( mainDomainSuffix [ 1 : ] ) + string ( ctx . Path ( ) ) , http . StatusPermanentRedirect )
2021-12-05 13:45:17 +00:00
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/@...
2022-07-27 15:25:08 +00:00
ctx . Redirect ( "/" + strings . Join ( pathElements [ 1 : ] , "/" ) , http . StatusTemporaryRedirect )
2021-12-05 13:45:17 +00:00
return
}
log . Debug ( ) . Msg ( "main domain preparations, now trying with specified repo & branch" )
2022-11-12 16:50:08 +00:00
if targetOpt , works := tryBranch ( log , ctx , giteaClient , & upstream . Options {
TargetOwner : targetOwner ,
TargetRepo : pathElements [ 0 ] ,
TargetBranch : pathElements [ 1 ] [ 1 : ] ,
TargetPath : path . Join ( pathElements [ 2 : ] ... ) ,
} , true ) ; works {
log . Trace ( ) . Msg ( "tryUpstream: serve with specified repo and branch" )
tryUpstream ( ctx , giteaClient , mainDomainSuffix , trimmedHost , targetOpt , canonicalDomainCache )
2021-12-05 13:45:17 +00:00
} else {
2022-11-07 21:47:03 +00:00
html . ReturnErrorPage ( ctx ,
2022-11-12 16:50:08 +00:00
fmt . Sprintf ( "explizite set branch %q do not exist at '%s/%s'" , targetOpt . TargetBranch , targetOpt . TargetOwner , targetOpt . TargetRepo ) ,
2022-11-07 21:47:03 +00:00
http . 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" )
2022-11-12 16:50:08 +00:00
if targetOpt , works := tryBranch ( log , ctx , giteaClient , & upstream . Options {
TargetOwner : targetOwner ,
TargetRepo : "pages" ,
TargetBranch : pathElements [ 0 ] [ 1 : ] ,
TargetPath : path . Join ( pathElements [ 1 : ] ... ) ,
} , true ) ; works {
log . Trace ( ) . Msg ( "tryUpstream: serve default pages repo with specified branch" )
tryUpstream ( ctx , giteaClient , mainDomainSuffix , trimmedHost , targetOpt , canonicalDomainCache )
2021-12-05 13:45:17 +00:00
} else {
2022-11-07 21:47:03 +00:00
html . ReturnErrorPage ( ctx ,
2022-11-12 16:50:08 +00:00
fmt . Sprintf ( "explizite set branch %q do not exist at '%s/%s'" , targetOpt . TargetBranch , targetOpt . TargetOwner , targetOpt . TargetRepo ) ,
2022-11-07 21:47:03 +00:00
http . 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" )
2022-11-12 01:54:56 +00:00
if pathElements [ 0 ] != "pages" {
2022-11-12 16:50:08 +00:00
if targetOpt , works := tryBranch ( log , ctx , giteaClient , & upstream . Options {
TargetOwner : targetOwner ,
TargetRepo : pathElements [ 0 ] ,
TargetBranch : "pages" ,
TargetPath : path . Join ( pathElements [ 1 : ] ... ) ,
} , false ) ; works {
2022-11-12 01:54:56 +00:00
log . Debug ( ) . Msg ( "tryBranch, now trying upstream 5" )
2022-11-12 16:50:08 +00:00
tryUpstream ( ctx , giteaClient , mainDomainSuffix , trimmedHost , targetOpt , canonicalDomainCache )
2022-11-12 01:54:56 +00:00
return
}
2021-12-05 13:45:17 +00:00
}
// 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" )
2022-11-12 16:50:08 +00:00
if targetOpt , works := tryBranch ( log , ctx , giteaClient , & upstream . Options {
TargetOwner : targetOwner ,
TargetRepo : "pages" ,
TargetPath : path . Join ( pathElements ... ) ,
} , false ) ; works {
2022-06-10 13:25:33 +00:00
log . Debug ( ) . Msg ( "tryBranch, now trying upstream 6" )
2022-11-12 16:50:08 +00:00
tryUpstream ( ctx , giteaClient , mainDomainSuffix , trimmedHost , targetOpt , canonicalDomainCache )
2021-12-05 13:45:17 +00:00
return
}
// Couldn't find a valid repo/branch
2022-11-07 21:47:03 +00:00
html . ReturnErrorPage ( ctx ,
2022-11-12 01:54:56 +00:00
fmt . Sprintf ( "couldn't find a valid repo[%s]" , targetRepo ) ,
2022-11-07 21:47:03 +00:00
http . StatusFailedDependency )
2021-12-05 13:45:17 +00:00
return
} else {
trimmedHostStr := string ( trimmedHost )
2022-11-07 21:47:03 +00:00
// Serve pages from custom domains
2022-11-12 01:54:56 +00:00
targetOwner , targetRepo , targetBranch := dns . GetTargetFromDNS ( trimmedHostStr , string ( mainDomainSuffix ) , dnsLookupCache )
2021-12-05 13:45:17 +00:00
if targetOwner == "" {
2022-11-07 21:47:03 +00:00
html . ReturnErrorPage ( ctx ,
"could not obtain repo owner from custom domain" ,
http . StatusFailedDependency )
2021-12-05 13:45:17 +00:00
return
}
2022-11-12 13:08:08 +00:00
pathParts := pathElements
2022-11-12 01:54:56 +00:00
canonicalLink := false
2021-12-05 13:45:17 +00:00
if strings . HasPrefix ( pathElements [ 0 ] , "@" ) {
targetBranch = pathElements [ 0 ] [ 1 : ]
2022-11-12 13:08:08 +00:00
pathParts = pathElements [ 1 : ]
2022-11-12 01:54:56 +00:00
canonicalLink = true
2021-12-05 13:45:17 +00:00
}
// 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" )
2022-11-12 16:50:08 +00:00
if targetOpt , works := tryBranch ( log , ctx , giteaClient , & upstream . Options {
TargetOwner : targetOwner ,
TargetRepo : targetRepo ,
TargetBranch : targetBranch ,
TargetPath : path . Join ( pathParts ... ) ,
} , canonicalLink ) ; works {
canonicalDomain , valid := upstream . CheckCanonicalDomain ( giteaClient , targetOpt . TargetOwner , targetOpt . TargetRepo , targetOpt . TargetBranch , trimmedHostStr , string ( mainDomainSuffix ) , canonicalDomainCache )
2021-12-05 13:45:17 +00:00
if ! valid {
2022-11-07 21:47:03 +00:00
html . ReturnErrorPage ( ctx , "domain not specified in <code>.domains</code> file" , http . 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 != "" {
2022-11-12 16:50:08 +00:00
ctx . Redirect ( "https://" + canonicalDomain + string ( targetOpt . TargetPath ) , http . StatusTemporaryRedirect )
2021-12-05 13:45:17 +00:00
return
}
2021-12-05 18:53:23 +00:00
2022-11-07 21:47:03 +00:00
html . ReturnErrorPage ( ctx , "target is no codeberg page" , http . StatusFailedDependency )
2021-12-05 18:53:23 +00:00
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" )
2022-11-12 16:50:08 +00:00
tryUpstream ( ctx , giteaClient , mainDomainSuffix , trimmedHost , targetOpt , canonicalDomainCache )
2021-12-05 13:45:17 +00:00
return
}
2021-12-05 18:53:23 +00:00
2022-11-07 21:47:03 +00:00
html . ReturnErrorPage ( ctx , "could not find target for custom domain" , http . StatusFailedDependency )
2021-12-05 18:53:23 +00:00
return
2021-12-05 13:45:17 +00:00
}
}
}