fork from d7z-project/caddy-gitea-pages
This commit is contained in:
parent
50a258ea59
commit
9d86fd33c6
86 changed files with 2452 additions and 4500 deletions
15
pages/40x.gohtml
Normal file
15
pages/40x.gohtml
Normal 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
15
pages/50x.gohtml
Normal 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
19
pages/buf.go
Normal 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
370
pages/cache_domain.go
Normal 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
111
pages/cache_owner.go
Normal 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
88
pages/client.go
Normal 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
98
pages/cname.go
Normal 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
10
pages/error.go
Normal 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
94
pages/failback.go
Normal 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
53
pages/fake_resp.go
Normal 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
85
pages/gitea.go
Normal 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
17
pages/middle.go
Normal 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
21
pages/page.go
Normal 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
70
pages/parser.go
Normal 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
59
pages/route.go
Normal 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
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue