fork from d7z-project/caddy-gitea-pages

This commit is contained in:
“xHuPo” 2024-09-14 11:53:32 +08:00
parent 50a258ea59
commit 9d86fd33c6
86 changed files with 2452 additions and 4500 deletions

15
pages/40x.gohtml Normal file
View file

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>404 Not Found</title>
</head>
<Body>
<div style="text-align: center;"><h1>404 Not Found</h1></div>
<hr>
<div style="text-align: center;">Gitea Pages</div>
</Body>
</html>

15
pages/50x.gohtml Normal file
View file

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>500 Internal Error</title>
</head>
<Body>
<div style="text-align: center;"><h1>500 Internal Error</h1></div>
<hr>
<div style="text-align: center;">Gitea Pages</div>
</Body>
</html>

19
pages/buf.go Normal file
View file

@ -0,0 +1,19 @@
package pages
import (
"bytes"
)
type ByteBuf struct {
*bytes.Buffer
}
func NewByteBuf(byte []byte) *ByteBuf {
return &ByteBuf{
bytes.NewBuffer(byte),
}
}
func (b ByteBuf) Close() error {
return nil
}

370
pages/cache_domain.go Normal file
View file

@ -0,0 +1,370 @@
package pages
import (
"bufio"
"bytes"
"code.gitea.io/sdk/gitea"
"crypto/sha1"
"fmt"
"github.com/patrickmn/go-cache"
"github.com/pkg/errors"
"go.uber.org/zap"
"io"
"net/http"
"slices"
"strconv"
"strings"
"sync"
"time"
)
type DomainCache struct {
ttl time.Duration
*cache.Cache
mutexes sync.Map
}
func (c *DomainCache) Close() error {
if c.Cache != nil {
c.Cache.Flush()
}
return nil
}
type DomainConfig struct {
FetchTime int64 //上次刷新时间
PageDomain PageDomain
Exists bool // 当前项目是否为 Pages
FileCache *cache.Cache // 文件缓存
CNAME []string // 重定向地址
SHA string // 缓存 SHA
DATE time.Time // 文件提交时间
BasePath string // 根目录
Topics map[string]bool // 存储库标记
Index string //默认页面
NotFound string //不存在页面
}
func (receiver *DomainConfig) Close() error {
receiver.FileCache.Flush()
return nil
}
func (receiver *DomainConfig) IsRoutePage() bool {
return receiver.Topics["routes-history"] || receiver.Topics["routes-hash"]
}
func NewDomainCache(ttl time.Duration, refreshTtl time.Duration) DomainCache {
c := cache.New(refreshTtl, 2*refreshTtl)
c.OnEvicted(func(_ string, i interface{}) {
config := i.(*DomainConfig)
if config != nil {
err := config.Close()
if err != nil {
return
}
}
})
return DomainCache{
ttl: ttl,
Cache: c,
mutexes: sync.Map{},
}
}
func fetch(client *GiteaConfig, domain *PageDomain, result *DomainConfig) error {
branches, resp, err := client.Client.ListRepoBranches(domain.Owner, domain.Repo,
gitea.ListRepoBranchesOptions{
ListOptions: gitea.ListOptions{
PageSize: 999,
},
})
// 缓存 404 内容
if resp != nil && resp.StatusCode >= 400 && resp.StatusCode < 500 {
result.Exists = false
return nil
}
if err != nil {
return err
}
topics, resp, err := client.Client.ListRepoTopics(domain.Owner, domain.Repo, gitea.ListRepoTopicsOptions{
ListOptions: gitea.ListOptions{
PageSize: 999,
},
})
if err != nil {
return err
}
branchIndex := slices.IndexFunc(branches, func(x *gitea.Branch) bool { return x.Name == domain.Branch })
if branchIndex == -1 {
return errors.Wrap(ErrorNotFound, "branch not found")
}
currentSHA := branches[branchIndex].Commit.ID
commitTime := branches[branchIndex].Commit.Timestamp
result.Topics = make(map[string]bool)
for _, topic := range topics {
result.Topics[strings.ToLower(topic)] = true
}
if result.SHA == currentSHA {
// 历史缓存一致,跳过
result.FetchTime = time.Now().UnixMilli()
return nil
}
// 清理历史缓存
if result.SHA != currentSHA {
if result.FileCache != nil {
result.FileCache.Flush()
}
result.SHA = currentSHA
result.DATE = commitTime
}
//查询是否为仓库
result.Exists, err = client.FileExists(domain, result.BasePath+"/index.html")
if err != nil {
return err
}
if !result.Exists {
return nil
}
result.Index = "index.html"
//############# 处理 404
if result.IsRoutePage() {
result.NotFound = "/index.html"
} else {
notFound, err := client.FileExists(domain, result.BasePath+"/404.html")
if err != nil {
return err
}
if notFound {
result.NotFound = "/404.html"
}
}
// ############ 拉取 CNAME
cname, err := client.ReadStringRepoFile(domain, "/CNAME")
if err != nil && !errors.Is(err, ErrorNotFound) {
// ignore not fond error
return err
} else if cname != "" {
// 清理重定向
result.CNAME = make([]string, 0)
scanner := bufio.NewScanner(strings.NewReader(cname))
for scanner.Scan() {
alias := scanner.Text()
alias = strings.TrimSpace(alias)
alias = strings.TrimPrefix(strings.TrimPrefix(alias, "https://"), "http://")
alias = strings.Split(alias, "/")[0]
if len(strings.TrimSpace(alias)) > 0 {
result.CNAME = append(result.CNAME, alias)
}
}
}
result.FetchTime = time.Now().UnixMilli()
return nil
}
func (receiver *DomainConfig) tag(path string) string {
return fmt.Sprintf("%x", sha1.Sum([]byte(
fmt.Sprintf("%s|%s|%s", receiver.SHA, receiver.PageDomain.Key(), path))))
}
func (receiver *DomainConfig) withNotFoundPage(
client *GiteaConfig,
response *FakeResponse,
) error {
if receiver.NotFound == "" {
// 没有默认页面
return ErrorNotFound
}
notFound, _ := receiver.FileCache.Get(receiver.NotFound)
if notFound == nil {
// 不存在 notfound
domain := &receiver.PageDomain
fileContext, err := client.OpenFileContext(domain, receiver.BasePath+receiver.NotFound)
if errors.Is(err, ErrorNotFound) {
//缓存 not found 不存在
receiver.FileCache.Set(receiver.NotFound, make([]byte, 0), cache.DefaultExpiration)
return err
} else if err != nil {
return err
}
length, _ := strconv.Atoi(fileContext.Header.Get("Content-Length"))
if length > client.CacheMaxSize {
client.Logger.Debug("default page too large.")
response.Body = fileContext.Body
response.CacheModeIgnore()
} else {
// 保存缓存
client.Logger.Debug("create default error page.")
defer fileContext.Body.Close()
defBuf, _ := io.ReadAll(fileContext.Body)
receiver.FileCache.Set(receiver.NotFound, defBuf, cache.DefaultExpiration)
response.Body = NewByteBuf(defBuf)
response.CacheModeMiss()
}
response.ContentTypeExt(receiver.NotFound)
response.Length(length)
} else {
notFound := notFound.([]byte)
if len(notFound) == 0 {
// 不存在 NotFound
return ErrorNotFound
}
response.Length(len(notFound))
response.Body = NewByteBuf(notFound)
response.CacheModeHit()
}
client.Logger.Debug("use cache error page.")
response.ContentTypeExt(receiver.NotFound)
if receiver.IsRoutePage() {
response.StatusCode = http.StatusOK
} else {
response.StatusCode = http.StatusNotFound
}
return nil
}
// todo: 读写加锁
func (receiver *DomainConfig) getCachedData(
client *GiteaConfig,
path string,
) (*FakeResponse, error) {
result := NewFakeResponse()
if strings.HasSuffix(path, "/") {
path = path + "index.html"
}
for k, v := range client.CustomHeaders {
result.SetHeader(k, v)
}
result.ETag(receiver.tag(path))
result.ContentTypeExt(path)
cacheBuf, _ := receiver.FileCache.Get(path)
// 使用缓存内容
if cacheBuf != nil {
cacheBuf := cacheBuf.([]byte)
if len(cacheBuf) == 0 {
//使用 NotFound 内容
client.Logger.Debug("location not found ,", zap.Any("path", path))
return result, receiver.withNotFoundPage(client, result)
} else {
// 使用缓存
client.Logger.Debug("location use cache ,", zap.Any("path", path))
result.Body = ByteBuf{
bytes.NewBuffer(cacheBuf),
}
result.Length(len(cacheBuf))
result.CacheModeHit()
return result, nil
}
} else {
// 添加缓存
client.Logger.Debug("location add cache ,", zap.Any("path", path))
domain := *(&receiver.PageDomain)
domain.Branch = receiver.SHA
fileContext, err := client.OpenFileContext(&domain, receiver.BasePath+path)
if err != nil && !errors.Is(err, ErrorNotFound) {
return nil, err
} else if errors.Is(err, ErrorNotFound) {
client.Logger.Debug("location not found and src not found,", zap.Any("path", path))
// 不存在且源不存在
receiver.FileCache.Set(path, make([]byte, 0), cache.DefaultExpiration)
return result, receiver.withNotFoundPage(client, result)
} else {
// 源存在,执行缓存
client.Logger.Debug("location found and set cache,", zap.Any("path", path))
length, _ := strconv.Atoi(fileContext.Header.Get("Content-Length"))
if length > client.CacheMaxSize {
client.Logger.Debug("location too large , skip cache.", zap.Any("path", path))
// 超过大小,回源
result.Body = fileContext.Body
result.Length(length)
result.CacheModeIgnore()
return result, nil
} else {
client.Logger.Debug("location saved,", zap.Any("path", path))
// 未超过大小,缓存
body, _ := io.ReadAll(fileContext.Body)
receiver.FileCache.Set(path, body, cache.DefaultExpiration)
result.Body = NewByteBuf(body)
result.Length(len(body))
result.CacheModeMiss()
return result, nil
}
}
}
}
func (receiver *DomainConfig) Copy(
client *GiteaConfig,
path string,
writer http.ResponseWriter,
_ *http.Request,
) (bool, error) {
fakeResp, err := receiver.getCachedData(client, path)
if err != nil {
return false, err
}
for k, v := range fakeResp.Header {
for _, s := range v {
writer.Header().Add(k, s)
}
}
writer.Header().Add("Pages-Server-Hash", receiver.SHA)
writer.Header().Add("Last-Modified", receiver.DATE.UTC().Format(http.TimeFormat))
writer.WriteHeader(fakeResp.StatusCode)
defer fakeResp.Body.Close()
_, err = io.Copy(writer, fakeResp.Body)
return true, err
}
// FetchRepo 拉取 Repo 信息
func (c *DomainCache) FetchRepo(client *GiteaConfig, domain *PageDomain) (*DomainConfig, bool, error) {
nextTime := time.Now().UnixMilli() - c.ttl.Milliseconds()
cacheKey := domain.Key()
result, _ := c.Get(cacheKey)
if result != nil {
result := result.(*DomainConfig)
if nextTime > result.FetchTime {
// 刷新旧的缓存
result = nil
} else {
return result, true, nil
}
}
if result == nil {
lock := c.Lock(domain)
defer lock()
if result, find := c.Get(cacheKey); find {
return result.(*DomainConfig), true, nil
}
result = &DomainConfig{
PageDomain: *domain,
FileCache: cache.New(c.ttl, c.ttl*2),
}
if err := fetch(client, domain, result.(*DomainConfig)); err != nil {
return nil, false, err
}
err := c.Add(cacheKey, result, cache.DefaultExpiration)
if err != nil {
return nil, false, err
}
return result.(*DomainConfig), false, nil
} else {
return result.(*DomainConfig), true, nil
}
}
func (c *DomainCache) Lock(any *PageDomain) func() {
return c.LockAny(any.Key())
}
func (c *DomainCache) LockAny(any string) func() {
value, _ := c.mutexes.LoadOrStore(any, &sync.Mutex{})
mtx := value.(*sync.Mutex)
mtx.Lock()
return func() { mtx.Unlock() }
}

111
pages/cache_owner.go Normal file
View file

@ -0,0 +1,111 @@
package pages
import (
"code.gitea.io/sdk/gitea"
"github.com/patrickmn/go-cache"
"github.com/pkg/errors"
"net/http"
"strings"
"sync"
"time"
)
type OwnerCache struct {
ttl time.Duration
*cache.Cache
mutexes sync.Map
}
func NewOwnerCache(ttl time.Duration, cacheTtl time.Duration) OwnerCache {
return OwnerCache{
ttl: ttl,
mutexes: sync.Map{},
Cache: cache.New(cacheTtl, cacheTtl*2),
}
}
type OwnerConfig struct {
FetchTime int64 `json:"fetch_time,omitempty"`
Repos map[string]bool `json:"repos,omitempty"`
LowerRepos map[string]bool `json:"lower_repos,omitempty"`
}
func NewOwnerConfig() *OwnerConfig {
return &OwnerConfig{
Repos: make(map[string]bool),
LowerRepos: make(map[string]bool),
}
}
// 直接查询 Owner 信息
func getOwner(giteaConfig *GiteaConfig, owner string) (*OwnerConfig, error) {
result := NewOwnerConfig()
repos, resp, err := giteaConfig.Client.ListOrgRepos(owner, gitea.ListOrgReposOptions{
ListOptions: gitea.ListOptions{
PageSize: 999,
},
})
if err != nil && resp.StatusCode == http.StatusNotFound {
// 调用用户接口查询
repos, resp, err = giteaConfig.Client.ListUserRepos(owner, gitea.ListReposOptions{
ListOptions: gitea.ListOptions{
PageSize: 999,
},
})
if err != nil && resp.StatusCode == http.StatusNotFound {
return nil, errors.Wrap(ErrorNotFound, err.Error())
} else if err != nil {
return nil, err
}
} else if err != nil {
return nil, err
}
for _, repo := range repos {
result.Repos[repo.Name] = true
result.LowerRepos[strings.ToLower(repo.Name)] = true
}
result.FetchTime = time.Now().UnixMilli()
return result, nil
}
func (c *OwnerCache) GetOwnerConfig(giteaConfig *GiteaConfig, owner string) (*OwnerConfig, error) {
raw, _ := c.Get(owner)
// 每固定时间刷新一次
nextTime := time.Now().UnixMilli() - c.ttl.Milliseconds()
var result *OwnerConfig
if raw != nil {
result = raw.(*OwnerConfig)
if nextTime > result.FetchTime {
//移除旧数据
c.Delete(owner)
raw = nil
}
}
if raw == nil {
lock := c.Lock(owner)
defer lock()
if raw, find := c.Get(owner); find {
return raw.(*OwnerConfig), nil
}
//不存在缓存
var err error
result, err = getOwner(giteaConfig, owner)
if err != nil {
return nil, errors.Wrap(err, "owner config not found")
}
c.Set(owner, result, cache.DefaultExpiration)
}
return result, nil
}
func (c *OwnerCache) Lock(any string) func() {
value, _ := c.mutexes.LoadOrStore(any, &sync.Mutex{})
mtx := value.(*sync.Mutex)
mtx.Lock()
return func() { mtx.Unlock() }
}
func (c *OwnerConfig) Exists(repo string) bool {
return c.LowerRepos[strings.ToLower(repo)]
}

88
pages/client.go Normal file
View file

@ -0,0 +1,88 @@
package pages
import (
"code.gitea.io/sdk/gitea"
"go.uber.org/zap"
"strconv"
"strings"
)
type AutoRedirect struct {
Enabled bool
Scheme string
Code int
}
type PageClient struct {
BaseDomain string
GiteaConfig *GiteaConfig
DomainAlias *CustomDomains
ErrorPages *ErrorPages
AutoRedirect *AutoRedirect
OwnerCache *OwnerCache
DomainCache *DomainCache
logger *zap.Logger
}
func (p *PageClient) Close() error {
if p.OwnerCache != nil {
p.OwnerCache.Cache.Flush()
}
if p.DomainCache != nil {
_ = p.DomainCache.Close()
}
return nil
}
func NewPageClient(
config *MiddlewareConfig,
logger *zap.Logger,
) (*PageClient, error) {
options := make([]gitea.ClientOption, 0)
if config.Token != "" {
options = append(options, gitea.SetToken(config.Token))
}
options = append(options, gitea.SetGiteaVersion(""))
client, err := gitea.NewClient(config.Server, options...)
if err != nil {
return nil, err
}
alias, err := NewCustomDomains(config.Alias, config.SharedAlias)
if err != nil {
return nil, err
}
pages, err := NewErrorPages(config.ErrorPages)
if err != nil {
return nil, err
}
ownerCache := NewOwnerCache(config.CacheRefresh, config.CacheTimeout)
giteaConfig := &GiteaConfig{
Server: config.Server,
Token: config.Token,
Client: client,
Logger: logger,
CacheMaxSize: config.CacheMaxSize,
CustomHeaders: config.CustomHeaders,
}
domainCache := NewDomainCache(config.CacheRefresh, config.CacheTimeout)
logger.Info("gitea cache ttl " + strconv.FormatInt(config.CacheTimeout.Milliseconds(), 10) + " ms .")
return &PageClient{
GiteaConfig: giteaConfig,
BaseDomain: "." + strings.Trim(config.Domain, "."),
DomainAlias: alias,
ErrorPages: pages,
logger: logger,
AutoRedirect: config.AutoRedirect,
DomainCache: &domainCache,
OwnerCache: &ownerCache,
}, nil
}
func (p *PageClient) Validate() error {
ver, _, err := p.GiteaConfig.Client.ServerVersion()
p.logger.Info("Gitea Version ", zap.String("version", ver))
if err != nil {
p.logger.Warn("Failed to get Gitea version", zap.Error(err))
}
return nil
}

98
pages/cname.go Normal file
View file

@ -0,0 +1,98 @@
package pages
import (
"encoding/json"
"fmt"
cmap "github.com/orcaman/concurrent-map/v2"
"os"
"strings"
"sync"
)
var shared = cmap.New[PageDomain]()
type CustomDomains struct {
/// 映射关系
Alias *cmap.ConcurrentMap[string, PageDomain] `json:"alias,omitempty"`
/// 反向链接
Reverse *cmap.ConcurrentMap[string, string] `json:"reverse,omitempty"`
/// 写锁
Mutex sync.Mutex `json:"-"`
/// 文件落盘
Local string `json:"-"`
// 是否全局共享
Share bool `json:"-"`
}
func (d *CustomDomains) Get(host string) (PageDomain, bool) {
get, b := d.Alias.Get(strings.ToLower(host))
if !b && d.Share {
return shared.Get(strings.ToLower(host))
}
return get, b
}
func (d *CustomDomains) add(domain *PageDomain, aliases ...string) {
d.Mutex.Lock()
defer d.Mutex.Unlock()
for _, alias := range aliases {
key := strings.ToLower(domain.Key())
alias = strings.ToLower(alias)
old, b := d.Reverse.Get(key)
if b {
// 移除旧的映射关系
if d.Share {
shared.Remove(old)
}
d.Alias.Remove(old)
d.Reverse.Remove(key)
}
if d.Share {
shared.Set(alias, *domain)
}
d.Alias.Set(alias, *domain)
d.Reverse.Set(key, alias)
}
if d.Local != "" {
marshal, err := json.Marshal(d)
err = os.WriteFile(d.Local, marshal, 0644)
if err != nil {
fmt.Printf("%v\n", err)
}
}
}
func NewCustomDomains(local string, share bool) (*CustomDomains, error) {
if share {
fmt.Printf("Global Alias Enabled.\n")
}
stat, err := os.Stat(local)
alias := cmap.New[PageDomain]()
reverse := cmap.New[string]()
result := &CustomDomains{
Alias: &alias,
Reverse: &reverse,
Mutex: sync.Mutex{},
Local: local,
Share: share,
}
fmt.Printf("Discover alias file :%s.\n", local)
if local != "" && err == nil && !stat.IsDir() {
bytes, err := os.ReadFile(local)
if err != nil {
return nil, err
}
err = json.Unmarshal(bytes, result)
fmt.Printf("Found %d Alias records.\n", result.Alias.Count())
if err != nil {
return nil, err
}
if share {
for k, v := range result.Alias.Items() {
shared.Set(k, v)
}
}
}
return result, nil
}

10
pages/error.go Normal file
View file

@ -0,0 +1,10 @@
package pages
import "github.com/pkg/errors"
var (
// ErrorNotMatches 确认这不是 Gitea Pages 相关的域名
ErrorNotMatches = errors.New("not matching")
ErrorNotFound = errors.New("not found")
ErrorInternal = errors.New("internal error")
)

94
pages/failback.go Normal file
View file

@ -0,0 +1,94 @@
package pages
import (
"embed"
"github.com/Masterminds/sprig/v3"
"github.com/pkg/errors"
"net/http"
"strconv"
"text/template"
)
type ErrorMetadata struct {
StatusCode int
Request *http.Request
Error string
}
var (
//go:embed 40x.gohtml 50x.gohtml
embedPages embed.FS
)
type ErrorPages struct {
errorPages map[string]*template.Template
}
func newTemplate(key, text string) (*template.Template, error) {
return template.New(key).Funcs(sprig.TxtFuncMap()).Parse(text)
}
func NewErrorPages(pagesTmpl map[string]string) (*ErrorPages, error) {
pages := make(map[string]*template.Template)
for key, value := range pagesTmpl {
tmpl, err := newTemplate(key, value)
if err != nil {
return nil, err
}
pages[key] = tmpl
}
if pages["40x"] == nil {
data, err := embedPages.ReadFile("40x.gohtml")
if err != nil {
return nil, err
}
pages["40x"], err = newTemplate("40x", string(data))
if err != nil {
return nil, err
}
}
if pages["50x"] == nil {
data, err := embedPages.ReadFile("50x.gohtml")
if err != nil {
return nil, err
}
pages["50x"], err = newTemplate("50x", string(data))
if err != nil {
return nil, err
}
}
return &ErrorPages{
errorPages: pages,
}, nil
}
func (p *ErrorPages) flushError(err error, request *http.Request, writer http.ResponseWriter) error {
var code = http.StatusInternalServerError
if errors.Is(err, ErrorNotMatches) {
// 跳过不匹配
return err
} else if errors.Is(err, ErrorNotFound) {
code = http.StatusNotFound
} else {
code = http.StatusInternalServerError
}
var metadata = &ErrorMetadata{
StatusCode: code,
Request: request,
Error: err.Error(),
}
codeStr := strconv.Itoa(code)
writer.Header().Add("Content-Type", "text/html;charset=utf-8")
writer.WriteHeader(code)
if result := p.errorPages[codeStr]; result != nil {
return result.Execute(writer, metadata)
}
switch {
case code >= 400 && code < 500:
return p.errorPages["40x"].Execute(writer, metadata)
case code >= 500:
return p.errorPages["50x"].Execute(writer, metadata)
default:
return p.errorPages["50x"].Execute(writer, metadata)
}
}

53
pages/fake_resp.go Normal file
View file

@ -0,0 +1,53 @@
package pages
import (
"fmt"
"mime"
"net/http"
"path/filepath"
"strconv"
)
type FakeResponse struct {
*http.Response
}
func (r *FakeResponse) Length(len int) {
r.ContentLength = int64(len)
r.Header.Set("Content-Length", strconv.Itoa(len))
}
func (r *FakeResponse) CacheModeIgnore() {
r.CacheMode("SKIP")
}
func (r *FakeResponse) CacheModeMiss() {
r.CacheMode("MISS")
}
func (r *FakeResponse) CacheModeHit() {
r.CacheMode("HIT")
}
func (r *FakeResponse) CacheMode(mode string) {
r.SetHeader("Pages-Server-Cache", mode)
}
func (r *FakeResponse) ContentTypeExt(path string) {
r.ContentType(mime.TypeByExtension(filepath.Ext(path)))
}
func (r *FakeResponse) ContentType(types string) {
r.Header.Set("Content-Type", types)
}
func (r *FakeResponse) ETag(tag string) {
r.Header.Set("ETag", fmt.Sprintf("\"%s\"", tag))
}
func NewFakeResponse() *FakeResponse {
return &FakeResponse{
&http.Response{
StatusCode: http.StatusOK,
Header: make(http.Header),
},
}
}
func (r *FakeResponse) SetHeader(key string, value string) {
r.Header.Set(key, value)
}

85
pages/gitea.go Normal file
View file

@ -0,0 +1,85 @@
package pages
import (
"code.gitea.io/sdk/gitea"
"fmt"
"github.com/pkg/errors"
"go.uber.org/zap"
"io"
"net/http"
"net/url"
)
type GiteaConfig struct {
Server string `json:"server"`
Token string `json:"token"`
Client *gitea.Client `json:"-"`
Logger *zap.Logger `json:"-"`
CustomHeaders map[string]string `json:"custom_headers"`
CacheMaxSize int `json:"max_cache_size"`
}
func (c *GiteaConfig) FileExists(domain *PageDomain, path string) (bool, error) {
context, err := c.OpenFileContext(domain, path)
if context != nil {
defer context.Body.Close()
}
if errors.Is(err, ErrorNotFound) {
return false, nil
} else if err != nil {
return false, err
}
return true, nil
}
func (c *GiteaConfig) ReadStringRepoFile(domain *PageDomain, path string) (string, error) {
data, err := c.ReadRepoFile(domain, path)
if err != nil {
return "", err
}
return string(data), nil
}
func (c *GiteaConfig) ReadRepoFile(domain *PageDomain, path string) ([]byte, error) {
context, err := c.OpenFileContext(domain, path)
if err != nil {
return nil, err
}
defer context.Body.Close()
all, err := io.ReadAll(context.Body)
if err != nil {
return nil, err
}
return all, nil
}
func (c *GiteaConfig) OpenFileContext(domain *PageDomain, path string) (*http.Response, error) {
var (
giteaURL string
err error
)
giteaURL, err = url.JoinPath(c.Server+"/api/v1/repos/", domain.Owner, domain.Repo, "media", path)
if err != nil {
return nil, err
}
giteaURL += "?ref=" + url.QueryEscape(domain.Branch)
req, err := http.NewRequest(http.MethodGet, giteaURL, nil)
if err != nil {
return nil, err
}
req.Header.Add("Authorization", "token "+c.Token)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, errors.Wrap(err, "")
}
switch resp.StatusCode {
case http.StatusForbidden:
return nil, errors.Wrap(ErrorNotFound, "domain file not forbidden")
case http.StatusNotFound:
return nil, errors.Wrap(ErrorNotFound, fmt.Sprintf("domain file not found: %s", path))
case http.StatusOK:
default:
return nil, errors.Wrap(ErrorInternal, fmt.Sprintf("unexpected status code '%d'", resp.StatusCode))
}
return resp, nil
}

17
pages/middle.go Normal file
View file

@ -0,0 +1,17 @@
package pages
import "time"
type MiddlewareConfig struct {
Server string `json:"server"`
Token string `json:"token"`
Domain string `json:"domain"`
Alias string `json:"alias"`
CacheRefresh time.Duration `json:"cache_refresh"`
CacheTimeout time.Duration `json:"cache_timeout"`
ErrorPages map[string]string `json:"errors"`
CustomHeaders map[string]string `json:"custom_headers"`
AutoRedirect *AutoRedirect `json:"redirect"`
SharedAlias bool `json:"shared_alias"`
CacheMaxSize int `json:"cache_max_size"`
}

21
pages/page.go Normal file
View file

@ -0,0 +1,21 @@
package pages
import "fmt"
type PageDomain struct {
Owner string `json:"owner"`
Repo string `json:"repo"`
Branch string `json:"branch"`
}
func NewPageDomain(owner string, repo string, branch string) *PageDomain {
return &PageDomain{
owner,
repo,
branch,
}
}
func (p *PageDomain) Key() string {
return fmt.Sprintf("%s|%s|%s", p.Owner, p.Repo, p.Branch)
}

70
pages/parser.go Normal file
View file

@ -0,0 +1,70 @@
package pages
import (
"fmt"
"github.com/pkg/errors"
"net/http"
"strings"
)
func (p *PageClient) parseDomain(request *http.Request) (*PageDomain, string, error) {
if strings.Contains(request.Host, "]") {
//跳过 ipv6 address 直接访问, 因为仅支持域名的方式
return nil, "", ErrorNotMatches
}
host := strings.Split(request.Host, ":")[0]
filePath := request.URL.Path
pathTrim := strings.Split(strings.Trim(filePath, "/"), "/")
repo := pathTrim[0]
// 处理 scheme://domain/path 的情况
if !strings.HasPrefix(filePath, fmt.Sprintf("/%s/", repo)) {
repo = ""
}
if strings.HasSuffix(host, p.BaseDomain) {
child := strings.Split(strings.TrimSuffix(host, p.BaseDomain), ".")
result := NewPageDomain(
child[len(child)-1],
repo,
"gh-pages",
)
// 处于使用默认 Domain 下
config, err := p.OwnerCache.GetOwnerConfig(p.GiteaConfig, result.Owner)
if err != nil {
return nil, "", err
}
ownerRepoName := result.Owner + p.BaseDomain
if result.Repo == "" && config.Exists(ownerRepoName) {
// 推导为默认仓库
result.Repo = ownerRepoName
return result, filePath, nil
} else if result.Repo == "" || !config.Exists(result.Repo) {
if config.Exists(ownerRepoName) {
result.Repo = ownerRepoName
return result, filePath, nil
}
// 未指定 repo 或者 repo 不存在,跳过
return nil, "", errors.Wrap(ErrorNotFound, result.Repo+" not found")
}
// 存在子目录且仓库存在
pathTrim = pathTrim[1:]
path := ""
if strings.HasSuffix(filePath, "/") {
path = "/" + strings.Join(pathTrim, "/") + "/"
} else {
path = "/" + strings.Join(pathTrim, "/")
}
path = strings.ReplaceAll(path, "//", "/")
path = strings.ReplaceAll(path, "//", "/")
if path == "" {
path = "/"
}
return result, path, nil
} else {
get, exists := p.DomainAlias.Get(host)
if exists {
return &get, filePath, nil
} else {
return nil, "", errors.Wrap(ErrorNotFound, "")
}
}
}

59
pages/route.go Normal file
View file

@ -0,0 +1,59 @@
package pages
import (
"fmt"
"github.com/pkg/errors"
"go.uber.org/zap"
"net/http"
"runtime"
"strings"
)
func (p *PageClient) Route(writer http.ResponseWriter, request *http.Request) error {
defer func() error {
//放在匿名函数里,err捕获到错误信息并且输出
err := recover()
if err != nil {
p.logger.Error("recovered from panic", zap.Any("err", err))
var buf [4096]byte
n := runtime.Stack(buf[:], false)
println(string(buf[:n]))
return p.ErrorPages.flushError(errors.New(fmt.Sprintf("%v", err)), request, writer)
}
return nil
}()
err := p.RouteExists(writer, request)
if err != nil {
p.logger.Debug("route exists error", zap.String("host", request.Host),
zap.String("path", request.RequestURI), zap.Error(err))
return p.ErrorPages.flushError(err, request, writer)
}
return err
}
func (p *PageClient) RouteExists(writer http.ResponseWriter, request *http.Request) error {
domain, filePath, err := p.parseDomain(request)
if err != nil {
return err
}
config, cache, err := p.DomainCache.FetchRepo(p.GiteaConfig, domain)
if err != nil {
return err
}
if !config.Exists {
return ErrorNotFound
}
if !cache && len(config.CNAME) > 0 {
p.logger.Info("Add CNAME link.", zap.Any("CNAME", config.CNAME))
p.DomainAlias.add(domain, config.CNAME...)
}
// 跳过 30x 重定向
if p.AutoRedirect.Enabled &&
len(config.CNAME) > 0 &&
strings.HasPrefix(request.Host, domain.Owner+p.BaseDomain) {
http.Redirect(writer, request, p.AutoRedirect.Scheme+"://"+config.CNAME[0], p.AutoRedirect.Code)
return nil
}
_, err = config.Copy(p.GiteaConfig, filePath, writer, request)
return err
}