fork from codeberg.org
This commit is contained in:
commit
50a258ea59
67 changed files with 4587 additions and 0 deletions
70
server/upstream/domains.go
Normal file
70
server/upstream/domains.go
Normal file
|
@ -0,0 +1,70 @@
|
|||
package upstream
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"pages-server/server/cache"
|
||||
"pages-server/server/gitea"
|
||||
)
|
||||
|
||||
// canonicalDomainCacheTimeout specifies the timeout for the canonical domain cache.
|
||||
var canonicalDomainCacheTimeout = 15 * time.Minute
|
||||
|
||||
const canonicalDomainConfig = ".domains"
|
||||
|
||||
// CheckCanonicalDomain returns the canonical domain specified in the repo (using the `.domains` file).
|
||||
func (o *Options) CheckCanonicalDomain(giteaClient *gitea.Client, actualDomain, mainDomainSuffix string, canonicalDomainCache cache.ICache) (domain string, valid bool) {
|
||||
// Check if this request is cached.
|
||||
if cachedValue, ok := canonicalDomainCache.Get(o.TargetOwner + "/" + o.TargetRepo + "/" + o.TargetBranch); ok {
|
||||
domains := cachedValue.([]string)
|
||||
for _, domain := range domains {
|
||||
if domain == actualDomain {
|
||||
valid = true
|
||||
break
|
||||
}
|
||||
}
|
||||
return domains[0], valid
|
||||
}
|
||||
|
||||
body, err := giteaClient.GiteaRawContent(o.TargetOwner, o.TargetRepo, o.TargetBranch, canonicalDomainConfig)
|
||||
if err != nil && !errors.Is(err, gitea.ErrorNotFound) {
|
||||
log.Error().Err(err).Msgf("could not read %s of %s/%s", canonicalDomainConfig, o.TargetOwner, o.TargetRepo)
|
||||
}
|
||||
|
||||
var domains []string
|
||||
for _, domain := range strings.Split(string(body), "\n") {
|
||||
domain = strings.ToLower(domain)
|
||||
domain = strings.TrimSpace(domain)
|
||||
domain = strings.TrimPrefix(domain, "http://")
|
||||
domain = strings.TrimPrefix(domain, "http://")
|
||||
if domain != "" && !strings.HasPrefix(domain, "#") && !strings.ContainsAny(domain, "\t /") && strings.ContainsRune(domain, '.') {
|
||||
domains = append(domains, domain)
|
||||
}
|
||||
if domain == actualDomain {
|
||||
valid = true
|
||||
}
|
||||
}
|
||||
|
||||
// Add [owner].[pages-domain] as valid domain.
|
||||
domains = append(domains, o.TargetOwner+mainDomainSuffix)
|
||||
if domains[len(domains)-1] == actualDomain {
|
||||
valid = true
|
||||
}
|
||||
|
||||
// If the target repository isn't called pages, add `/[repository]` to the
|
||||
// previous valid domain.
|
||||
if o.TargetRepo != "" && o.TargetRepo != "pages" {
|
||||
domains[len(domains)-1] += "/" + o.TargetRepo
|
||||
}
|
||||
|
||||
// Add result to cache.
|
||||
_ = canonicalDomainCache.Set(o.TargetOwner+"/"+o.TargetRepo+"/"+o.TargetBranch, domains, canonicalDomainCacheTimeout)
|
||||
|
||||
// Return the first domain from the list and return if any of the domains
|
||||
// matched the requested domain.
|
||||
return domains[0], valid
|
||||
}
|
28
server/upstream/header.go
Normal file
28
server/upstream/header.go
Normal file
|
@ -0,0 +1,28 @@
|
|||
package upstream
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"pages-server/server/context"
|
||||
"pages-server/server/gitea"
|
||||
)
|
||||
|
||||
// setHeader set values to response header
|
||||
func (o *Options) setHeader(ctx *context.Context, header http.Header) {
|
||||
if eTag := header.Get(gitea.ETagHeader); eTag != "" {
|
||||
ctx.RespWriter.Header().Set(gitea.ETagHeader, eTag)
|
||||
}
|
||||
if cacheIndicator := header.Get(gitea.PagesCacheIndicatorHeader); cacheIndicator != "" {
|
||||
ctx.RespWriter.Header().Set(gitea.PagesCacheIndicatorHeader, cacheIndicator)
|
||||
}
|
||||
if length := header.Get(gitea.ContentLengthHeader); length != "" {
|
||||
ctx.RespWriter.Header().Set(gitea.ContentLengthHeader, length)
|
||||
}
|
||||
if mime := header.Get(gitea.ContentTypeHeader); mime == "" || o.ServeRaw {
|
||||
ctx.RespWriter.Header().Set(gitea.ContentTypeHeader, rawMime)
|
||||
} else {
|
||||
ctx.RespWriter.Header().Set(gitea.ContentTypeHeader, mime)
|
||||
}
|
||||
ctx.RespWriter.Header().Set(headerLastModified, o.BranchTimestamp.In(time.UTC).Format(http.TimeFormat))
|
||||
}
|
47
server/upstream/helper.go
Normal file
47
server/upstream/helper.go
Normal file
|
@ -0,0 +1,47 @@
|
|||
package upstream
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"pages-server/server/gitea"
|
||||
)
|
||||
|
||||
// GetBranchTimestamp finds the default branch (if branch is "") and save branch and it's last modification time to Options
|
||||
func (o *Options) GetBranchTimestamp(giteaClient *gitea.Client) (bool, error) {
|
||||
log := log.With().Strs("BranchInfo", []string{o.TargetOwner, o.TargetRepo, o.TargetBranch}).Logger()
|
||||
|
||||
if o.TargetBranch == "" {
|
||||
// Get default branch
|
||||
defaultBranch, err := giteaClient.GiteaGetRepoDefaultBranch(o.TargetOwner, o.TargetRepo)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Couldn't fetch default branch from repository")
|
||||
return false, err
|
||||
}
|
||||
log.Debug().Msgf("Successfully fetched default branch %q from Gitea", defaultBranch)
|
||||
o.TargetBranch = defaultBranch
|
||||
}
|
||||
|
||||
timestamp, err := giteaClient.GiteaGetRepoBranchTimestamp(o.TargetOwner, o.TargetRepo, o.TargetBranch)
|
||||
if err != nil {
|
||||
if !errors.Is(err, gitea.ErrorNotFound) {
|
||||
log.Error().Err(err).Msg("Could not get latest commit timestamp from branch")
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
if timestamp == nil || timestamp.Branch == "" {
|
||||
return false, fmt.Errorf("empty response")
|
||||
}
|
||||
|
||||
log.Debug().Msgf("Successfully fetched latest commit timestamp from branch: %#v", timestamp)
|
||||
o.BranchTimestamp = timestamp.Timestamp
|
||||
o.TargetBranch = timestamp.Branch
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (o *Options) ContentWebLink(giteaClient *gitea.Client) string {
|
||||
return giteaClient.ContentWebLink(o.TargetOwner, o.TargetRepo, o.TargetBranch, o.TargetPath) + "; rel=\"canonical\""
|
||||
}
|
107
server/upstream/redirects.go
Normal file
107
server/upstream/redirects.go
Normal file
|
@ -0,0 +1,107 @@
|
|||
package upstream
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"pages-server/server/cache"
|
||||
"pages-server/server/context"
|
||||
"pages-server/server/gitea"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type Redirect struct {
|
||||
From string
|
||||
To string
|
||||
StatusCode int
|
||||
}
|
||||
|
||||
// rewriteURL returns the destination URL and true if r matches reqURL.
|
||||
func (r *Redirect) rewriteURL(reqURL string) (dstURL string, ok bool) {
|
||||
// check if from url matches request url
|
||||
if strings.TrimSuffix(r.From, "/") == strings.TrimSuffix(reqURL, "/") {
|
||||
return r.To, true
|
||||
}
|
||||
// handle wildcard redirects
|
||||
if strings.HasSuffix(r.From, "/*") {
|
||||
trimmedFromURL := strings.TrimSuffix(r.From, "/*")
|
||||
if reqURL == trimmedFromURL || strings.HasPrefix(reqURL, trimmedFromURL+"/") {
|
||||
if strings.Contains(r.To, ":splat") {
|
||||
matched := strings.TrimPrefix(reqURL, trimmedFromURL)
|
||||
matched = strings.TrimPrefix(matched, "/")
|
||||
return strings.ReplaceAll(r.To, ":splat", matched), true
|
||||
}
|
||||
return r.To, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// redirectsCacheTimeout specifies the timeout for the redirects cache.
|
||||
var redirectsCacheTimeout = 10 * time.Minute
|
||||
|
||||
const redirectsConfig = "_redirects"
|
||||
|
||||
// getRedirects returns redirects specified in the _redirects file.
|
||||
func (o *Options) getRedirects(giteaClient *gitea.Client, redirectsCache cache.ICache) []Redirect {
|
||||
var redirects []Redirect
|
||||
cacheKey := o.TargetOwner + "/" + o.TargetRepo + "/" + o.TargetBranch
|
||||
|
||||
// Check for cached redirects
|
||||
if cachedValue, ok := redirectsCache.Get(cacheKey); ok {
|
||||
redirects = cachedValue.([]Redirect)
|
||||
} else {
|
||||
// Get _redirects file and parse
|
||||
body, err := giteaClient.GiteaRawContent(o.TargetOwner, o.TargetRepo, o.TargetBranch, redirectsConfig)
|
||||
if err == nil {
|
||||
for _, line := range strings.Split(string(body), "\n") {
|
||||
redirectArr := strings.Fields(line)
|
||||
|
||||
// Ignore comments and invalid lines
|
||||
if strings.HasPrefix(line, "#") || len(redirectArr) < 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Get redirect status code
|
||||
statusCode := 301
|
||||
if len(redirectArr) == 3 {
|
||||
statusCode, err = strconv.Atoi(redirectArr[2])
|
||||
if err != nil {
|
||||
log.Info().Err(err).Msgf("could not read %s of %s/%s", redirectsConfig, o.TargetOwner, o.TargetRepo)
|
||||
}
|
||||
}
|
||||
|
||||
redirects = append(redirects, Redirect{
|
||||
From: redirectArr[0],
|
||||
To: redirectArr[1],
|
||||
StatusCode: statusCode,
|
||||
})
|
||||
}
|
||||
}
|
||||
_ = redirectsCache.Set(cacheKey, redirects, redirectsCacheTimeout)
|
||||
}
|
||||
return redirects
|
||||
}
|
||||
|
||||
func (o *Options) matchRedirects(ctx *context.Context, giteaClient *gitea.Client, redirects []Redirect, redirectsCache cache.ICache) (final bool) {
|
||||
reqURL := ctx.Req.RequestURI
|
||||
// remove repo and branch from request url
|
||||
reqURL = strings.TrimPrefix(reqURL, "/"+o.TargetRepo)
|
||||
reqURL = strings.TrimPrefix(reqURL, "/@"+o.TargetBranch)
|
||||
|
||||
for _, redirect := range redirects {
|
||||
if dstURL, ok := redirect.rewriteURL(reqURL); ok {
|
||||
// do rewrite if status code is 200
|
||||
if redirect.StatusCode == 200 {
|
||||
o.TargetPath = dstURL
|
||||
o.Upstream(ctx, giteaClient, redirectsCache)
|
||||
} else {
|
||||
ctx.Redirect(dstURL, redirect.StatusCode)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
36
server/upstream/redirects_test.go
Normal file
36
server/upstream/redirects_test.go
Normal file
|
@ -0,0 +1,36 @@
|
|||
package upstream
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRedirect_rewriteURL(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
redirect Redirect
|
||||
reqURL string
|
||||
wantDstURL string
|
||||
wantOk bool
|
||||
}{
|
||||
{Redirect{"/", "/dst", 200}, "/", "/dst", true},
|
||||
{Redirect{"/", "/dst", 200}, "/foo", "", false},
|
||||
{Redirect{"/src", "/dst", 200}, "/src", "/dst", true},
|
||||
{Redirect{"/src", "/dst", 200}, "/foo", "", false},
|
||||
{Redirect{"/src", "/dst", 200}, "/src/foo", "", false},
|
||||
{Redirect{"/*", "/dst", 200}, "/", "/dst", true},
|
||||
{Redirect{"/*", "/dst", 200}, "/src", "/dst", true},
|
||||
{Redirect{"/src/*", "/dst/:splat", 200}, "/src", "/dst/", true},
|
||||
{Redirect{"/src/*", "/dst/:splat", 200}, "/src/", "/dst/", true},
|
||||
{Redirect{"/src/*", "/dst/:splat", 200}, "/src/foo", "/dst/foo", true},
|
||||
{Redirect{"/src/*", "/dst/:splat", 200}, "/src/foo/bar", "/dst/foo/bar", true},
|
||||
{Redirect{"/src/*", "/dst/:splatsuffix", 200}, "/src/foo", "/dst/foosuffix", true},
|
||||
{Redirect{"/src/*", "/dst:splat", 200}, "/src/foo", "/dstfoo", true},
|
||||
{Redirect{"/src/*", "/dst", 200}, "/srcfoo", "", false},
|
||||
// This is the example from FEATURES.md:
|
||||
{Redirect{"/articles/*", "/posts/:splat", 302}, "/articles/2022/10/12/post-1/", "/posts/2022/10/12/post-1/", true},
|
||||
} {
|
||||
if dstURL, ok := tc.redirect.rewriteURL(tc.reqURL); dstURL != tc.wantDstURL || ok != tc.wantOk {
|
||||
t.Errorf("%#v.rewriteURL(%q) = %q, %v; want %q, %v",
|
||||
tc.redirect, tc.reqURL, dstURL, ok, tc.wantDstURL, tc.wantOk)
|
||||
}
|
||||
}
|
||||
}
|
225
server/upstream/upstream.go
Normal file
225
server/upstream/upstream.go
Normal file
|
@ -0,0 +1,225 @@
|
|||
package upstream
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"pages-server/html"
|
||||
"pages-server/server/cache"
|
||||
"pages-server/server/context"
|
||||
"pages-server/server/gitea"
|
||||
)
|
||||
|
||||
const (
|
||||
headerLastModified = "Last-Modified"
|
||||
headerIfModifiedSince = "If-Modified-Since"
|
||||
|
||||
rawMime = "text/plain; charset=utf-8"
|
||||
)
|
||||
|
||||
var upstreamIndexPages = []string{
|
||||
"index.html",
|
||||
}
|
||||
|
||||
var upstreamNotFoundPages = []string{
|
||||
"404.html",
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
TargetOwner string
|
||||
TargetRepo string
|
||||
TargetBranch string
|
||||
TargetPath string
|
||||
|
||||
Host string
|
||||
|
||||
TryIndexPages bool
|
||||
BranchTimestamp time.Time
|
||||
|
||||
appendTrailingSlash bool
|
||||
redirectIfExists string
|
||||
|
||||
ServeRaw bool
|
||||
}
|
||||
|
||||
func (o *Options) Upstream(ctx *context.Context, giteaClient *gitea.Client, redirectsCache cache.ICache) bool {
|
||||
log := log.With().Strs("upstream", []string{o.TargetOwner, o.TargetRepo, o.TargetBranch, o.TargetPath}).Logger()
|
||||
|
||||
log.Debug().Msg("Start")
|
||||
|
||||
if o.TargetOwner == "" || o.TargetRepo == "" {
|
||||
html.ReturnErrorPage(ctx, "forge client: either repo owner or name info is missing", http.StatusBadRequest)
|
||||
return false
|
||||
}
|
||||
|
||||
if o.BranchTimestamp.IsZero() {
|
||||
branchExist, err := o.GetBranchTimestamp(giteaClient)
|
||||
if err != nil && errors.Is(err, gitea.ErrorNotFound) || !branchExist {
|
||||
html.ReturnErrorPage(ctx,
|
||||
fmt.Sprintf("branch <code>%q</code> for <code>%s/%s</code> not found", o.TargetBranch, o.TargetOwner, o.TargetRepo),
|
||||
http.StatusNotFound)
|
||||
return false
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
html.ReturnErrorPage(ctx,
|
||||
fmt.Sprintf("could not get timestamp of branch <code>%q</code>: '%v'", o.TargetBranch, err),
|
||||
http.StatusFailedDependency)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if ctx.Response() != nil {
|
||||
ifModifiedSince, err := time.Parse(time.RFC1123, ctx.Response().Header.Get(headerIfModifiedSince))
|
||||
if err == nil && ifModifiedSince.After(o.BranchTimestamp) {
|
||||
ctx.RespWriter.WriteHeader(http.StatusNotModified)
|
||||
log.Trace().Msg("check response against last modified: valid")
|
||||
return true
|
||||
}
|
||||
log.Trace().Msg("check response against last modified: outdated")
|
||||
}
|
||||
|
||||
reader, header, statusCode, err := giteaClient.ServeRawContent(o.TargetOwner, o.TargetRepo, o.TargetBranch, o.TargetPath)
|
||||
if err != nil {
|
||||
handleGiteaError(ctx, log, err, statusCode)
|
||||
return false
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if reader != nil {
|
||||
reader.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
if errors.Is(err, gitea.ErrorNotFound) {
|
||||
handleNotFound(ctx, log, giteaClient, redirectsCache, o)
|
||||
return false
|
||||
}
|
||||
|
||||
if err != nil || reader == nil || statusCode != http.StatusOK {
|
||||
handleUnexpectedError(ctx, log, err, statusCode)
|
||||
return false
|
||||
}
|
||||
|
||||
handleRedirects(ctx, log, o, redirectsCache)
|
||||
setHeaders(ctx, header)
|
||||
writeResponse(ctx, reader)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func handleGiteaError(ctx *context.Context, log zerolog.Logger, err error, statusCode int) {
|
||||
var msg string
|
||||
if err != nil {
|
||||
msg = "forge client: returned unexpected error"
|
||||
log.Error().Err(err).Msg(msg)
|
||||
msg = fmt.Sprintf("%s: '%v'", msg, err)
|
||||
}
|
||||
if statusCode != http.StatusOK {
|
||||
msg = fmt.Sprintf("forge client: couldn't fetch contents: <code>%d - %s</code>", statusCode, http.StatusText(statusCode))
|
||||
log.Error().Msg(msg)
|
||||
}
|
||||
|
||||
html.ReturnErrorPage(ctx, msg, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
func handleNotFound(ctx *context.Context, log zerolog.Logger, giteaClient *gitea.Client, redirectsCache cache.ICache, o *Options) {
|
||||
log.Debug().Msg("Handling not found error")
|
||||
redirects := o.getRedirects(giteaClient, redirectsCache)
|
||||
if o.matchRedirects(ctx, giteaClient, redirects, redirectsCache) {
|
||||
log.Trace().Msg("redirect")
|
||||
return
|
||||
}
|
||||
|
||||
if o.TryIndexPages {
|
||||
log.Trace().Msg("try index page")
|
||||
optionsForIndexPages := *o
|
||||
optionsForIndexPages.TryIndexPages = false
|
||||
optionsForIndexPages.appendTrailingSlash = true
|
||||
for _, indexPage := range upstreamIndexPages {
|
||||
optionsForIndexPages.TargetPath = strings.TrimSuffix(o.TargetPath, "/") + "/" + indexPage
|
||||
if optionsForIndexPages.Upstream(ctx, giteaClient, redirectsCache) {
|
||||
return
|
||||
}
|
||||
}
|
||||
log.Trace().Msg("try html file with path name")
|
||||
optionsForIndexPages.appendTrailingSlash = false
|
||||
optionsForIndexPages.redirectIfExists = strings.TrimSuffix(ctx.Path(), "/") + ".html"
|
||||
optionsForIndexPages.TargetPath = o.TargetPath + ".html"
|
||||
if optionsForIndexPages.Upstream(ctx, giteaClient, redirectsCache) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
log.Trace().Msg("not found")
|
||||
ctx.StatusCode = http.StatusNotFound
|
||||
|
||||
if o.TryIndexPages {
|
||||
log.Trace().Msg("try not found page")
|
||||
optionsForNotFoundPages := *o
|
||||
optionsForNotFoundPages.TryIndexPages = false
|
||||
optionsForNotFoundPages.appendTrailingSlash = false
|
||||
for _, notFoundPage := range upstreamNotFoundPages {
|
||||
optionsForNotFoundPages.TargetPath = "/" + notFoundPage
|
||||
if optionsForNotFoundPages.Upstream(ctx, giteaClient, redirectsCache) {
|
||||
return
|
||||
}
|
||||
}
|
||||
log.Trace().Msg("not found page missing")
|
||||
}
|
||||
}
|
||||
|
||||
func handleUnexpectedError(ctx *context.Context, log zerolog.Logger, err error, statusCode int) {
|
||||
var msg string
|
||||
if err != nil {
|
||||
msg = "forge client: returned unexpected error"
|
||||
log.Error().Err(err).Msg(msg)
|
||||
msg = fmt.Sprintf("%s: '%v'", msg, err)
|
||||
}
|
||||
if statusCode != http.StatusOK {
|
||||
msg = fmt.Sprintf("forge client: couldn't fetch contents: <code>%d - %s</code>", statusCode, http.StatusText(statusCode))
|
||||
log.Error().Msg(msg)
|
||||
}
|
||||
|
||||
html.ReturnErrorPage(ctx, msg, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
func handleRedirects(ctx *context.Context, log zerolog.Logger, o *Options, redirectsCache cache.ICache) {
|
||||
if o.appendTrailingSlash && !strings.HasSuffix(ctx.Path(), "/") {
|
||||
log.Trace().Msg("append trailing slash and redirect")
|
||||
ctx.Redirect(ctx.Path()+"/", http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
if strings.HasSuffix(ctx.Path(), "/index.html") && !o.ServeRaw {
|
||||
log.Trace().Msg("remove index.html from path and redirect")
|
||||
ctx.Redirect(strings.TrimSuffix(ctx.Path(), "index.html"), http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
if o.redirectIfExists != "" {
|
||||
ctx.Redirect(o.redirectIfExists, http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func setHeaders(ctx *context.Context, header http.Header) {
|
||||
ctx.RespWriter.Header().Set("ETag", header.Get("ETag"))
|
||||
ctx.RespWriter.Header().Set("Content-Type", header.Get("Content-Type"))
|
||||
}
|
||||
|
||||
func writeResponse(ctx *context.Context, reader io.Reader) {
|
||||
ctx.RespWriter.WriteHeader(ctx.StatusCode)
|
||||
if reader != nil {
|
||||
_, err := io.Copy(ctx.RespWriter, reader)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Couldn't write body for %q", ctx.Path())
|
||||
html.ReturnErrorPage(ctx, "", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
}
|
220
server/upstream/upstream.gov1
Normal file
220
server/upstream/upstream.gov1
Normal file
|
@ -0,0 +1,220 @@
|
|||
package upstream
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"pages-server/html"
|
||||
"pages-server/server/cache"
|
||||
"pages-server/server/context"
|
||||
"pages-server/server/gitea"
|
||||
)
|
||||
|
||||
const (
|
||||
headerLastModified = "Last-Modified"
|
||||
headerIfModifiedSince = "If-Modified-Since"
|
||||
|
||||
rawMime = "text/plain; charset=utf-8"
|
||||
)
|
||||
|
||||
// upstreamIndexPages lists pages that may be considered as index pages for directories.
|
||||
var upstreamIndexPages = []string{
|
||||
"index.html",
|
||||
}
|
||||
|
||||
// upstreamNotFoundPages lists pages that may be considered as custom 404 Not Found pages.
|
||||
var upstreamNotFoundPages = []string{
|
||||
"404.html",
|
||||
}
|
||||
|
||||
// Options provides various options for the upstream request.
|
||||
type Options struct {
|
||||
TargetOwner string
|
||||
TargetRepo string
|
||||
TargetBranch string
|
||||
TargetPath string
|
||||
|
||||
// Used for debugging purposes.
|
||||
Host string
|
||||
|
||||
TryIndexPages bool
|
||||
BranchTimestamp time.Time
|
||||
// internal
|
||||
appendTrailingSlash bool
|
||||
redirectIfExists string
|
||||
|
||||
ServeRaw bool
|
||||
}
|
||||
|
||||
// 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, redirectsCache cache.ICache) bool {
|
||||
log := log.With().Strs("upstream", []string{o.TargetOwner, o.TargetRepo, o.TargetBranch, o.TargetPath}).Logger()
|
||||
|
||||
log.Debug().Msg("Start")
|
||||
|
||||
if o.TargetOwner == "" || o.TargetRepo == "" {
|
||||
html.ReturnErrorPage(ctx, "forge client: either repo owner or name info is missing", http.StatusBadRequest)
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if the branch exists and when it was modified
|
||||
if o.BranchTimestamp.IsZero() {
|
||||
branchExist, err := o.GetBranchTimestamp(giteaClient)
|
||||
// handle 404
|
||||
if err != nil && errors.Is(err, gitea.ErrorNotFound) || !branchExist {
|
||||
html.ReturnErrorPage(ctx,
|
||||
fmt.Sprintf("branch <code>%q</code> for <code>%s/%s</code> not found", o.TargetBranch, o.TargetOwner, o.TargetRepo),
|
||||
http.StatusNotFound)
|
||||
return true
|
||||
}
|
||||
|
||||
// handle unexpected errors
|
||||
if err != nil {
|
||||
html.ReturnErrorPage(ctx,
|
||||
fmt.Sprintf("could not get timestamp of branch <code>%q</code>: '%v'", o.TargetBranch, err),
|
||||
http.StatusFailedDependency)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the browser has a cached version
|
||||
if ctx.Response() != nil {
|
||||
if ifModifiedSince, err := time.Parse(time.RFC1123, ctx.Response().Header.Get(headerIfModifiedSince)); err == nil {
|
||||
if ifModifiedSince.After(o.BranchTimestamp) {
|
||||
ctx.RespWriter.WriteHeader(http.StatusNotModified)
|
||||
log.Trace().Msg("check response against last modified: valid")
|
||||
return true
|
||||
}
|
||||
}
|
||||
log.Trace().Msg("check response against last modified: outdated")
|
||||
}
|
||||
|
||||
log.Debug().Msg("Preparing")
|
||||
|
||||
reader, header, statusCode, err := giteaClient.ServeRawContent(o.TargetOwner, o.TargetRepo, o.TargetBranch, o.TargetPath)
|
||||
if reader != nil {
|
||||
defer reader.Close()
|
||||
}
|
||||
|
||||
log.Debug().Msg("Aquisting")
|
||||
|
||||
// Handle not found error
|
||||
if err != nil && errors.Is(err, gitea.ErrorNotFound) {
|
||||
log.Debug().Msg("Handling not found error")
|
||||
// Get and match redirects
|
||||
redirects := o.getRedirects(giteaClient, redirectsCache)
|
||||
if o.matchRedirects(ctx, giteaClient, redirects, redirectsCache) {
|
||||
log.Trace().Msg("redirect")
|
||||
return true
|
||||
}
|
||||
|
||||
if o.TryIndexPages {
|
||||
log.Trace().Msg("try index page")
|
||||
// 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, redirectsCache) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
log.Trace().Msg("try html file with path name")
|
||||
// compatibility fix for GitHub Pages (/example → /example.html)
|
||||
optionsForIndexPages.appendTrailingSlash = false
|
||||
optionsForIndexPages.redirectIfExists = strings.TrimSuffix(ctx.Path(), "/") + ".html"
|
||||
optionsForIndexPages.TargetPath = o.TargetPath + ".html"
|
||||
if optionsForIndexPages.Upstream(ctx, giteaClient, redirectsCache) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
log.Trace().Msg("not found")
|
||||
|
||||
ctx.StatusCode = http.StatusNotFound
|
||||
if o.TryIndexPages {
|
||||
log.Trace().Msg("try not found page")
|
||||
// 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, redirectsCache) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
log.Trace().Msg("not found page missing")
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// handle unexpected client errors
|
||||
if err != nil || reader == nil || statusCode != http.StatusOK {
|
||||
log.Debug().Msg("Handling error")
|
||||
var msg string
|
||||
|
||||
if err != nil {
|
||||
msg = "forge client: returned unexpected error"
|
||||
log.Error().Err(err).Msg(msg)
|
||||
msg = fmt.Sprintf("%s: '%v'", msg, err)
|
||||
}
|
||||
if reader == nil {
|
||||
msg = "forge client: returned no reader"
|
||||
log.Error().Msg(msg)
|
||||
}
|
||||
if statusCode != http.StatusOK {
|
||||
msg = fmt.Sprintf("forge client: couldn't fetch contents: <code>%d - %s</code>", statusCode, http.StatusText(statusCode))
|
||||
log.Error().Msg(msg)
|
||||
}
|
||||
|
||||
html.ReturnErrorPage(ctx, msg, 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 && !strings.HasSuffix(ctx.Path(), "/") {
|
||||
log.Trace().Msg("append trailing slash and redirect")
|
||||
ctx.Redirect(ctx.Path()+"/", http.StatusTemporaryRedirect)
|
||||
return true
|
||||
}
|
||||
if strings.HasSuffix(ctx.Path(), "/index.html") && !o.ServeRaw {
|
||||
log.Trace().Msg("remove index.html from path and redirect")
|
||||
ctx.Redirect(strings.TrimSuffix(ctx.Path(), "index.html"), http.StatusTemporaryRedirect)
|
||||
return true
|
||||
}
|
||||
if o.redirectIfExists != "" {
|
||||
ctx.Redirect(o.redirectIfExists, http.StatusTemporaryRedirect)
|
||||
return true
|
||||
}
|
||||
|
||||
// Set ETag & MIME
|
||||
o.setHeader(ctx, header)
|
||||
|
||||
log.Debug().Msg("Prepare response")
|
||||
|
||||
ctx.RespWriter.WriteHeader(ctx.StatusCode)
|
||||
|
||||
// Write the response body to the original request
|
||||
if reader != nil {
|
||||
_, err := io.Copy(ctx.RespWriter, reader)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Couldn't write body for %q", o.TargetPath)
|
||||
html.ReturnErrorPage(ctx, "", http.StatusInternalServerError)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
log.Debug().Msg("Sending response")
|
||||
|
||||
return true
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue