diff --git a/main.go b/main.go index 1b97016..caff76e 100644 --- a/main.go +++ b/main.go @@ -10,7 +10,6 @@ import ( "syscall" "go.uber.org/zap" - "gopkg.d7z.net/gitea-pages/pkg/core" "gopkg.in/yaml.v3" "gopkg.d7z.net/gitea-pages/pkg" @@ -65,7 +64,7 @@ func main() { if err != nil { log.Fatalln(err) } - backend := core.NewCacheBackend(gitea, options.CacheMeta, options.CacheMetaTTL, + backend := providers.NewProviderCache(gitea, options.CacheMeta, options.CacheMetaTTL, options.CacheBlob, options.CacheBlobLimit, ) giteaServer := pkg.NewPageServer(backend, *options) diff --git a/pkg/core/domain.go b/pkg/core/domain.go index 5ddd8ed..66aa1bb 100644 --- a/pkg/core/domain.go +++ b/pkg/core/domain.go @@ -42,10 +42,10 @@ func (p *PageDomain) ParseDomainMeta(ctx context.Context, domain, path, branch s if !strings.HasSuffix(domain, "."+p.baseDomain) { alias, err := p.alias.Query(ctx, domain) // 确定 alias 是否存在内容 if err != nil { - zap.L().Warn("未知域名", zap.String("base", p.baseDomain), zap.String("domain", domain), zap.Error(err)) + zap.L().Warn("unknown domain", zap.String("base", p.baseDomain), zap.String("domain", domain), zap.Error(err)) return nil, os.ErrNotExist } - zap.L().Debug("命中别名", zap.String("domain", domain), zap.Any("alias", alias)) + zap.L().Debug("alias hit", zap.String("domain", domain), zap.Any("alias", alias)) return p.returnMeta(ctx, alias.Owner, alias.Repo, alias.Branch, pathArr) } owner := strings.TrimSuffix(domain, "."+p.baseDomain) @@ -70,7 +70,7 @@ func (p *PageDomain) ParseDomainMeta(ctx context.Context, domain, path, branch s func (p *PageDomain) returnMeta(ctx context.Context, owner, repo, branch string, path []string) (*PageDomainContent, error) { result := &PageDomainContent{} - meta, vfs, err := p.GetMeta(ctx, owner, repo, branch) + meta, err := p.GetMeta(ctx, owner, repo, branch) if err != nil { zap.L().Debug("查询错误", zap.Error(err)) if meta != nil { @@ -82,7 +82,7 @@ func (p *PageDomain) returnMeta(ctx context.Context, owner, repo, branch string, result.PageMetaContent = meta result.Owner = owner result.Repo = repo - result.PageVFS = vfs + result.PageVFS = NewPageVFS(p.client, p.Backend, owner, repo, result.CommitID) result.Path = strings.Join(path, "/") if err = p.alias.Bind(ctx, meta.Alias, result.Owner, result.Repo, branch); err != nil { diff --git a/pkg/core/filter.go b/pkg/core/filter.go new file mode 100644 index 0000000..87f416e --- /dev/null +++ b/pkg/core/filter.go @@ -0,0 +1,46 @@ +package core + +import ( + "context" + "encoding/json" + "net/http" +) + +type FilterParams map[string]any + +func (f FilterParams) Unmarshal(target any) error { + marshal, err := json.Marshal(f) + if err != nil { + return err + } + return json.Unmarshal(marshal, target) +} + +type Filter struct { + Path string `json:"path"` + Type string `json:"type"` + Params FilterParams `json:"params"` +} + +func NextCallWrapper(call FilterCall, parentCall NextCall) NextCall { + return func(ctx context.Context, writer http.ResponseWriter, request *http.Request, metadata *PageDomainContent) error { + return call(ctx, writer, request, metadata, parentCall) + } +} + +type NextCall func( + ctx context.Context, + writer http.ResponseWriter, + request *http.Request, + metadata *PageDomainContent, +) error + +type FilterCall func( + ctx context.Context, + writer http.ResponseWriter, + request *http.Request, + metadata *PageDomainContent, + next NextCall, +) error + +type FilterInstance func(config FilterParams) (FilterCall, error) diff --git a/pkg/core/meta.go b/pkg/core/meta.go index 26c65ff..9ca8456 100644 --- a/pkg/core/meta.go +++ b/pkg/core/meta.go @@ -2,6 +2,7 @@ package core import ( "context" + "encoding/json" "fmt" "net/http" "net/url" @@ -16,15 +17,11 @@ import ( "gopkg.d7z.net/middleware/tools" "gopkg.in/yaml.v3" - "github.com/gobwas/glob" - "github.com/pkg/errors" "gopkg.d7z.net/gitea-pages/pkg/utils" ) -var regexpHostname = regexp.MustCompile(`^(?:([a-z0-9-]+|\*)\.)?([a-z0-9-]{1,61})\.([a-z0-9]{2,7})$`) - type ServerMeta struct { Backend Domain string @@ -34,6 +31,52 @@ type ServerMeta struct { locker *utils.Locker } +// PageConfig 配置 + +type PageMetaContent struct { + CommitID string `json:"commit_id"` // 提交 COMMIT ID + LastModified time.Time `json:"last_modified"` // 上次更新时间 + IsPage bool `json:"is_page"` // 是否为 Page + ErrorMsg string `json:"error"` // 错误消息 (作为 500 错误日志暴露至前端) + + Alias []string `json:"alias"` // alias + + Filters []Filter `json:"filters"` // 路由消息 +} + +func NewEmptyPageMetaContent() *PageMetaContent { + return &PageMetaContent{ + IsPage: false, + Filters: []Filter{ + { + Path: "**", + Type: "default_not_found", + Params: map[string]any{}, + }, + { // 默认阻塞 + Path: ".git/**", + Type: "block", + Params: map[string]any{ + "code": "404", + "message": "Not found", + }, + }, { // 默认阻塞 + Path: ".pages.yaml", + Type: "block", + Params: map[string]any{ + "code": "404", + "message": "Not found", + }, + }, + }, + } +} + +func (m *PageMetaContent) String() string { + marshal, _ := json.Marshal(m) + return string(marshal) +} + func NewServerMeta(client *http.Client, backend Backend, kv kv.KV, domain string, ttl time.Duration) *ServerMeta { return &ServerMeta{ Backend: backend, @@ -44,15 +87,15 @@ func NewServerMeta(client *http.Client, backend Backend, kv kv.KV, domain string } } -func (s *ServerMeta) GetMeta(ctx context.Context, owner, repo, branch string) (*PageMetaContent, *PageVFS, error) { +func (s *ServerMeta) GetMeta(ctx context.Context, owner, repo, branch string) (*PageMetaContent, error) { repos, err := s.Repos(ctx, owner) if err != nil { - return nil, nil, err + return nil, err } defBranch := repos[repo] if defBranch == "" { - return nil, nil, os.ErrNotExist + return nil, os.ErrNotExist } if branch == "" { @@ -61,21 +104,21 @@ func (s *ServerMeta) GetMeta(ctx context.Context, owner, repo, branch string) (* branches, err := s.Branches(ctx, owner, repo) if err != nil { - return nil, nil, err + return nil, err } info := branches[branch] if info == nil { - return nil, nil, os.ErrNotExist + return nil, os.ErrNotExist } key := fmt.Sprintf("%s/%s/%s", owner, repo, branch) if cache, found := s.cache.Load(ctx, key); found { if cache.IsPage { - return &cache, NewPageVFS(s.client, s.Backend, owner, repo, cache.CommitID), nil + return &cache, nil } - return nil, nil, os.ErrNotExist + return nil, os.ErrNotExist } mux := s.locker.Open(key) @@ -84,9 +127,9 @@ func (s *ServerMeta) GetMeta(ctx context.Context, owner, repo, branch string) (* if cache, found := s.cache.Load(ctx, key); found { if cache.IsPage { - return &cache, NewPageVFS(s.client, s.Backend, owner, repo, cache.CommitID), nil + return &cache, nil } - return nil, nil, os.ErrNotExist + return nil, os.ErrNotExist } rel := NewEmptyPageMetaContent() @@ -98,39 +141,42 @@ func (s *ServerMeta) GetMeta(ctx context.Context, owner, repo, branch string) (* if exists, _ := vfs.Exists(ctx, "index.html"); !exists { rel.IsPage = false _ = s.cache.Store(ctx, key, *rel) - return nil, nil, os.ErrNotExist + return nil, os.ErrNotExist } - rel.IsPage = true - - // 添加默认跳过的内容 - for _, defIgnore := range rel.Ignore { - rel.ignoreL = append(rel.ignoreL, glob.MustCompile(defIgnore)) - } - // 解析配置 if err := s.parsePageConfig(ctx, rel, vfs); err != nil { rel.IsPage = false rel.ErrorMsg = err.Error() _ = s.cache.Store(ctx, key, *rel) - return nil, nil, err + return nil, err } - // 处理 CNAME 文件 - if err := s.parseCNAME(ctx, rel, vfs); err != nil { - rel.IsPage = false - rel.ErrorMsg = err.Error() - _ = s.cache.Store(ctx, key, *rel) - return nil, nil, err - } - - rel.Alias = utils.ClearDuplicates(rel.Alias) - rel.Ignore = utils.ClearDuplicates(rel.Ignore) _ = s.cache.Store(ctx, key, *rel) - return rel, vfs, nil + return rel, nil } -func (s *ServerMeta) parsePageConfig(ctx context.Context, rel *PageMetaContent, vfs *PageVFS) error { +func (s *ServerMeta) parsePageConfig(ctx context.Context, meta *PageMetaContent, vfs *PageVFS) error { + alias := make([]string, 0) + defer func(alias *[]string) { + meta.Alias = *alias + direct := *alias + meta.Filters = append(meta.Filters, Filter{ + Path: "**", + Type: "redirect", + Params: map[string]any{ + "targets": direct, + }, + }) + }(&alias) + cname, err := vfs.ReadString(ctx, "CNAME") + if cname != "" && err == nil { + if al, ok := s.aliasCheck(cname); ok { + alias = append(alias, al) + } else { + return fmt.Errorf("invalid alias %s", cname) + } + } data, err := vfs.ReadString(ctx, ".pages.yaml") if err != nil { zap.L().Debug("failed to read meta data", zap.String("error", err.Error())) @@ -141,43 +187,48 @@ func (s *ServerMeta) parsePageConfig(ctx context.Context, rel *PageMetaContent, if err = yaml.Unmarshal([]byte(data), cfg); err != nil { return errors.Wrap(err, "parse .pages.yaml failed") } - - rel.VRoute = cfg.VirtualRoute + if cfg.VirtualRoute { + meta.Filters = append(meta.Filters, Filter{ + Path: "**", + Type: "forward", + Params: map[string]any{ + "path": "index.html", + }, + }) + } // 处理别名 for _, cname := range cfg.Alias { - if err := s.addAlias(rel, cname); err != nil { - return err + if cname == "" { + continue + } + if al, ok := s.aliasCheck(cname); ok { + alias = append(alias, al) + } else { + return fmt.Errorf("invalid alias %s", cname) } } // 处理渲染器 for sType, pattern := range cfg.Renders() { - r := GetRender(sType) - if r == nil { - return errors.Errorf("render not found %s", sType) - } - - g, err := glob.Compile(strings.TrimSpace(pattern)) - if err != nil { - return errors.Wrapf(err, "compile render pattern failed: %s", pattern) - } - - rel.rendersL = append(rel.rendersL, &renderCompiler{ - regex: g, - Render: r, + meta.Filters = append(meta.Filters, Filter{ + Path: pattern, + Type: sType, + Params: map[string]any{}, }) - rel.Renders[sType] = append(rel.Renders[sType], pattern) } // 处理跳过内容 for _, pattern := range cfg.Ignores() { - g, err := glob.Compile(pattern) - if err != nil { - return errors.Wrapf(err, "compile ignore pattern failed: %s", pattern) - } - rel.ignoreL = append(rel.ignoreL, g) - rel.Ignore = append(rel.Ignore, pattern) + meta.Filters = append(meta.Filters, Filter{ // 默认直连 + Path: pattern, + Type: "block", + Params: map[string]any{ + "code": "404", + "message": "Not found", + }, + }, + ) } // 处理反向代理 @@ -196,35 +247,29 @@ func (s *ServerMeta) parsePageConfig(ctx context.Context, rel *PageMetaContent, if rURL.Scheme != "http" && rURL.Scheme != "https" { return errors.Errorf("invalid backend url scheme: %s", backend) } - - rel.Proxy[path] = rURL.String() + meta.Filters = append(meta.Filters, Filter{ + Path: path, + Type: "reverse_proxy", + Params: map[string]any{ + "prefix": path, + "target": rURL.String(), + }, + }) } return nil } -func (s *ServerMeta) parseCNAME(ctx context.Context, rel *PageMetaContent, vfs *PageVFS) error { - cname, err := vfs.ReadString(ctx, "CNAME") - if err != nil { - return nil // CNAME 文件不存在是正常情况 - } - if err := s.addAlias(rel, cname); err != nil { - zap.L().Debug("指定的 CNAME 不合法", zap.String("cname", cname), zap.Error(err)) - return err - } - return nil -} +var regexpHostname = regexp.MustCompile(`^(?:([a-z0-9-]+|\*)\.)?([a-z0-9-]{1,61})\.([a-z0-9]{2,7})$`) -func (s *ServerMeta) addAlias(rel *PageMetaContent, cname string) error { +func (s *ServerMeta) aliasCheck(cname string) (string, bool) { cname = strings.TrimSpace(cname) if !regexpHostname.MatchString(cname) { - return errors.New("invalid domain name format") + return "", false } if strings.HasSuffix(strings.ToLower(cname), strings.ToLower(s.Domain)) { - return errors.New("alias cannot be subdomain of main domain") + return "", false } - - rel.Alias = append(rel.Alias, cname) - return nil + return cname, true } diff --git a/pkg/core/meta_content.go b/pkg/core/meta_content.go deleted file mode 100644 index 60b728e..0000000 --- a/pkg/core/meta_content.go +++ /dev/null @@ -1,105 +0,0 @@ -package core - -import ( - "encoding/json" - "time" - - "github.com/gobwas/glob" - "gopkg.in/yaml.v3" -) - -type renderCompiler struct { - regex glob.Glob - Render -} - -// PageConfig 配置 - -type PageMetaContent struct { - CommitID string `json:"commit-id"` // 提交 COMMIT ID - LastModified time.Time `json:"last-modified"` // 上次更新时间 - IsPage bool `json:"is-page"` // 是否为 Page - ErrorMsg string `json:"error"` // 错误消息 - - VRoute bool `yaml:"v-route"` // 虚拟路由 - Proxy map[string]string `yaml:"proxy"` // 反向代理 - Renders map[string][]string `json:"renders"` // 配置的渲染器 - - Alias []string `yaml:"aliases"` // 重定向 - Ignore []string `yaml:"ignore"` // 跳过的内容 - - rendersL []*renderCompiler - ignoreL []glob.Glob -} - -func NewEmptyPageMetaContent() *PageMetaContent { - return &PageMetaContent{ - IsPage: false, - Proxy: make(map[string]string), - Alias: make([]string, 0), - Renders: make(map[string][]string), - Ignore: []string{".*", "**/.*"}, - } -} - -func (m *PageMetaContent) UnmarshalJSON(bytes []byte) error { - type alias PageMetaContent - var c alias - if err := json.Unmarshal(bytes, &c); err != nil { - return err - } - *m = PageMetaContent(c) - return m.init() -} - -func (m *PageMetaContent) UnmarshalYAML(value *yaml.Node) error { - type alias PageMetaContent - var c alias - if err := value.Decode(&c); err != nil { - return err - } - *m = PageMetaContent(c) - return m.init() -} - -func (m *PageMetaContent) init() error { - clear(m.rendersL) - for key, gs := range m.Renders { - for _, g := range gs { - m.rendersL = append(m.rendersL, &renderCompiler{ - regex: glob.MustCompile(g), - Render: GetRender(key), - }) - } - } - clear(m.ignoreL) - for _, g := range m.Ignore { - m.ignoreL = append(m.ignoreL, glob.MustCompile(g)) - } - return nil -} - -func (m *PageMetaContent) IgnorePath(path string) bool { - for _, g := range m.ignoreL { - if g.Match(path) { - return true - } - } - return false -} - -func (m *PageMetaContent) TryRender(path ...string) Render { - for _, s := range path { - for _, compiler := range m.rendersL { - if compiler.regex.Match(s) { - return compiler.Render - } - } - } - return nil -} - -func (m *PageMetaContent) String() string { - marshal, _ := json.Marshal(m) - return string(marshal) -} diff --git a/pkg/core/proxy.go b/pkg/core/proxy.go deleted file mode 100644 index 9a8bc95..0000000 --- a/pkg/core/proxy.go +++ /dev/null @@ -1 +0,0 @@ -package core diff --git a/pkg/core/render.go b/pkg/core/render.go deleted file mode 100644 index 5bb8f23..0000000 --- a/pkg/core/render.go +++ /dev/null @@ -1,30 +0,0 @@ -package core - -import ( - "context" - "io" - "net/http" - "sync" -) - -var ( - renders = make(map[string]Render) - lock = &sync.Mutex{} -) - -type Render interface { - Render(ctx context.Context, w http.ResponseWriter, r *http.Request, input io.Reader, meta *PageDomainContent) error -} - -func RegisterRender(fType string, r Render) { - lock.Lock() - defer lock.Unlock() - if renders[fType] != nil { - panic("duplicate render type: " + fType) - } - renders[fType] = r -} - -func GetRender(key string) Render { - return renders[key] -} diff --git a/pkg/core/page.go b/pkg/core/vfs.go similarity index 100% rename from pkg/core/page.go rename to pkg/core/vfs.go diff --git a/pkg/filters/common.go b/pkg/filters/common.go new file mode 100644 index 0000000..7da47bd --- /dev/null +++ b/pkg/filters/common.go @@ -0,0 +1,12 @@ +package filters + +import "gopkg.d7z.net/gitea-pages/pkg/core" + +func DefaultFilters() map[string]core.FilterInstance { + return map[string]core.FilterInstance{ + "redirect": FilterInstRedirect, + "direct": FilterInstDirect, + "reverse_proxy": FilterInstProxy, + "default_not_found": FilterInstDefaultNotFound, + } +} diff --git a/pkg/filters/default.go b/pkg/filters/default.go new file mode 100644 index 0000000..c712e55 --- /dev/null +++ b/pkg/filters/default.go @@ -0,0 +1,30 @@ +package filters + +import ( + "context" + "io" + "net/http" + "os" + + "github.com/pkg/errors" + "gopkg.d7z.net/gitea-pages/pkg/core" +) + +var FilterInstDefaultNotFound core.FilterInstance = func(config core.FilterParams) (core.FilterCall, error) { + return func(ctx context.Context, writer http.ResponseWriter, request *http.Request, metadata *core.PageDomainContent, next core.NextCall) error { + err := next(ctx, writer, request, metadata) + if err != nil && errors.Is(err, os.ErrNotExist) { + open, err := metadata.NativeOpen(ctx, "/404.html", nil) + if open != nil { + defer open.Body.Close() + } + if err != nil { + return err + } + writer.Header().Set("Content-Type", "text/html; charset=utf-8") + writer.WriteHeader(http.StatusNotFound) + _, _ = io.Copy(writer, open.Body) + } + return nil + }, nil +} diff --git a/pkg/filters/direct.go b/pkg/filters/direct.go new file mode 100644 index 0000000..a3d1ed4 --- /dev/null +++ b/pkg/filters/direct.go @@ -0,0 +1,60 @@ +package filters + +import ( + "context" + "io" + "net/http" + "os" + "path/filepath" + "time" + + "github.com/pkg/errors" + "go.uber.org/zap" + "gopkg.d7z.net/gitea-pages/pkg/core" +) + +var FilterInstDirect core.FilterInstance = func(config core.FilterParams) (core.FilterCall, error) { + var param struct { + Prefix string `json:"prefix"` + } + if err := config.Unmarshal(¶m); err != nil { + return nil, err + } + return func(ctx context.Context, writer http.ResponseWriter, request *http.Request, metadata *core.PageDomainContent, next core.NextCall) error { + var resp *http.Response + var path string + var err error + failback := []string{param.Prefix + metadata.Path, param.Prefix + metadata.Path + "/index.html"} + for _, p := range failback { + resp, err = metadata.NativeOpen(request.Context(), p, nil) + if err != nil { + if resp != nil { + resp.Body.Close() + } + if !errors.Is(err, os.ErrNotExist) { + zap.L().Debug("error", zap.Any("error", err)) + } + continue + } + path = p + break + } + if resp == nil { + return os.ErrNotExist + } + defer resp.Body.Close() + if err != nil { + return err + } + writer.Header().Set("Content-Type", resp.Header.Get("Content-Type")) + lastMod, err := time.Parse(http.TimeFormat, resp.Header.Get("Last-Modified")) + if err == nil { + if seeker, ok := resp.Body.(io.ReadSeeker); ok { + http.ServeContent(writer, request, filepath.Base(path), lastMod, seeker) + return nil + } + } + _, err = io.Copy(writer, resp.Body) + return err + }, nil +} diff --git a/pkg/filters/proxy.go b/pkg/filters/proxy.go new file mode 100644 index 0000000..5b4e2b6 --- /dev/null +++ b/pkg/filters/proxy.go @@ -0,0 +1,49 @@ +package filters + +import ( + "context" + "fmt" + "net" + "net/http" + "net/http/httputil" + "net/url" + "strings" + + "go.uber.org/zap" + "gopkg.d7z.net/gitea-pages/pkg/core" + "gopkg.d7z.net/gitea-pages/pkg/utils" +) + +var FilterInstProxy core.FilterInstance = func(config core.FilterParams) (core.FilterCall, error) { + var param struct { + Prefix string `json:"prefix"` + Target string `json:"target"` + } + if err := config.Unmarshal(¶m); err != nil { + return nil, err + } + return func(ctx context.Context, writer http.ResponseWriter, request *http.Request, metadata *core.PageDomainContent, next core.NextCall) error { + proxyPath := "/" + metadata.Path + targetPath := strings.TrimPrefix(proxyPath, param.Prefix) + if !strings.HasPrefix(targetPath, "/") { + targetPath = "/" + targetPath + } + u, _ := url.Parse(param.Target) + request.URL.Path = targetPath + request.RequestURI = request.URL.RequestURI() + proxy := httputil.NewSingleHostReverseProxy(u) + // todo: 处理透传 + // proxy.Transport = s.options.HTTPClient.Transport + if host, _, err := net.SplitHostPort(request.RemoteAddr); err == nil { + request.Header.Set("X-Real-IP", host) + } + request.Header.Set("X-Page-IP", utils.GetRemoteIP(request)) + request.Header.Set("X-Page-Refer", fmt.Sprintf("%s/%s/%s", metadata.Owner, metadata.Repo, metadata.Path)) + request.Header.Set("X-Page-Host", request.Host) + zap.L().Debug("命中反向代理", zap.Any("prefix", param.Prefix), zap.Any("target", param.Target), + zap.Any("path", proxyPath), zap.Any("target", fmt.Sprintf("%s%s", u, targetPath))) + // todo(security): 处理 websocket + proxy.ServeHTTP(writer, request) + return nil + }, nil +} diff --git a/pkg/filters/redirect.go b/pkg/filters/redirect.go new file mode 100644 index 0000000..47ef73a --- /dev/null +++ b/pkg/filters/redirect.go @@ -0,0 +1,46 @@ +package filters + +import ( + "context" + "fmt" + "net/http" + "net/url" + "regexp" + "slices" + "strings" + + "go.uber.org/zap" + "gopkg.d7z.net/gitea-pages/pkg/core" +) + +var portExp = regexp.MustCompile(`:\d+$`) + +var FilterInstRedirect core.FilterInstance = func(config core.FilterParams) (core.FilterCall, error) { + var param struct { + Targets []string `json:"targets"` + } + if err := config.Unmarshal(¶m); err != nil { + return nil, err + } + return func(ctx context.Context, writer http.ResponseWriter, request *http.Request, metadata *core.PageDomainContent, next core.NextCall) error { + domain := portExp.ReplaceAllString(strings.ToLower(request.Host), "") + if len(param.Targets) > 0 && !slices.Contains(metadata.Alias, domain) { + // 重定向到配置的地址 + zap.L().Debug("redirect", zap.Any("src", request.Host), zap.Any("dst", param.Targets[0])) + path := metadata.Path + if strings.HasSuffix(path, "/index.html") || path == "index.html" { + path = strings.TrimSuffix(path, "index.html") + } + target, err := url.Parse(fmt.Sprintf("https://%s/%s", param.Targets[0], path)) + if err != nil { + return err + } + target.RawQuery = request.URL.RawQuery + + http.Redirect(writer, request, target.String(), http.StatusFound) + return nil + } else { + return next(ctx, writer, request, metadata) + } + }, nil +} diff --git a/pkg/core/backend_cache.go b/pkg/providers/cache.go similarity index 76% rename from pkg/core/backend_cache.go rename to pkg/providers/cache.go index 0a710bb..ee62c97 100644 --- a/pkg/core/backend_cache.go +++ b/pkg/providers/cache.go @@ -1,4 +1,4 @@ -package core +package providers import ( "bytes" @@ -12,37 +12,38 @@ import ( "github.com/pkg/errors" "go.uber.org/zap" + "gopkg.d7z.net/gitea-pages/pkg/core" "gopkg.d7z.net/gitea-pages/pkg/utils" "gopkg.d7z.net/middleware/cache" "gopkg.d7z.net/middleware/kv" "gopkg.d7z.net/middleware/tools" ) -type CacheBackend struct { - backend Backend +type ProviderCache struct { + parent core.Backend cacheRepo *tools.Cache[map[string]string] - cacheBranch *tools.Cache[map[string]*BranchInfo] + cacheBranch *tools.Cache[map[string]*core.BranchInfo] cacheBlob cache.Cache cacheBlobLimit uint64 } -func (c *CacheBackend) Close() error { - return c.backend.Close() +func (c *ProviderCache) Close() error { + return c.parent.Close() } -func NewCacheBackend( - backend Backend, +func NewProviderCache( + backend core.Backend, cacheMeta kv.KV, cacheMetaTTL time.Duration, cacheBlob cache.Cache, cacheBlobLimit uint64, -) *CacheBackend { +) *ProviderCache { repoCache := tools.NewCache[map[string]string](cacheMeta, "repos", cacheMetaTTL) - branchCache := tools.NewCache[map[string]*BranchInfo](cacheMeta, "branches", cacheMetaTTL) - return &CacheBackend{ - backend: backend, + branchCache := tools.NewCache[map[string]*core.BranchInfo](cacheMeta, "branches", cacheMetaTTL) + return &ProviderCache{ + parent: backend, cacheRepo: repoCache, cacheBranch: branchCache, @@ -51,11 +52,11 @@ func NewCacheBackend( } } -func (c *CacheBackend) Repos(ctx context.Context, owner string) (map[string]string, error) { +func (c *ProviderCache) Repos(ctx context.Context, owner string) (map[string]string, error) { if load, b := c.cacheRepo.Load(ctx, owner); b { return load, nil } - ret, err := c.backend.Repos(ctx, owner) + ret, err := c.parent.Repos(ctx, owner) if err != nil { if errors.Is(err, os.ErrNotExist) { _ = c.cacheRepo.Store(ctx, owner, map[string]string{}) @@ -69,15 +70,15 @@ func (c *CacheBackend) Repos(ctx context.Context, owner string) (map[string]stri return ret, err } -func (c *CacheBackend) Branches(ctx context.Context, owner, repo string) (map[string]*BranchInfo, error) { +func (c *ProviderCache) Branches(ctx context.Context, owner, repo string) (map[string]*core.BranchInfo, error) { key := fmt.Sprintf("%s/%s", owner, repo) if load, b := c.cacheBranch.Load(ctx, key); b { return load, nil } - ret, err := c.backend.Branches(ctx, owner, repo) + ret, err := c.parent.Branches(ctx, owner, repo) if err != nil { if errors.Is(err, os.ErrNotExist) { - _ = c.cacheBranch.Store(ctx, key, map[string]*BranchInfo{}) + _ = c.cacheBranch.Store(ctx, key, map[string]*core.BranchInfo{}) } return nil, err } @@ -88,10 +89,10 @@ func (c *CacheBackend) Branches(ctx context.Context, owner, repo string) (map[st return ret, err } -func (c *CacheBackend) Open(ctx context.Context, client *http.Client, owner, repo, commit, path string, headers http.Header) (*http.Response, error) { +func (c *ProviderCache) Open(ctx context.Context, client *http.Client, owner, repo, commit, path string, headers http.Header) (*http.Response, error) { if headers != nil && headers.Get("Range") != "" { // ignore custom header - return c.backend.Open(ctx, client, owner, repo, commit, path, headers) + return c.parent.Open(ctx, client, owner, repo, commit, path, headers) } key := fmt.Sprintf("%s/%s/%s/%s", owner, repo, commit, path) lastCache, err := c.cacheBlob.Get(ctx, key) @@ -125,7 +126,7 @@ func (c *CacheBackend) Open(ctx context.Context, client *http.Client, owner, rep Header: respHeader, }, nil } - open, err := c.backend.Open(ctx, client, owner, repo, commit, path, http.Header{}) + open, err := c.parent.Open(ctx, client, owner, repo, commit, path, http.Header{}) if err != nil || open == nil { if open != nil { _ = open.Body.Close() diff --git a/pkg/renders/gotemplate.go b/pkg/renders/gotemplate.go index a0b3f84..0c4170e 100644 --- a/pkg/renders/gotemplate.go +++ b/pkg/renders/gotemplate.go @@ -14,7 +14,6 @@ import ( type GoTemplate struct{} func init() { - core.RegisterRender("gotemplate", &GoTemplate{}) } func (g GoTemplate) Render(ctx context.Context, w http.ResponseWriter, r *http.Request, input io.Reader, meta *core.PageDomainContent) error { diff --git a/pkg/server.go b/pkg/server.go index ce54ea6..0854ed9 100644 --- a/pkg/server.go +++ b/pkg/server.go @@ -1,25 +1,20 @@ package pkg import ( - "context" "errors" - "fmt" - "io" - "net" "net/http" - "net/http/httputil" - "net/url" "os" - "path/filepath" "regexp" "slices" "strings" "time" + "github.com/gobwas/glob" "github.com/google/uuid" + lru "github.com/hashicorp/golang-lru/v2" "go.uber.org/zap" "gopkg.d7z.net/gitea-pages/pkg/core" - "gopkg.d7z.net/gitea-pages/pkg/utils" + "gopkg.d7z.net/gitea-pages/pkg/filters" "gopkg.d7z.net/middleware/cache" "gopkg.d7z.net/middleware/kv" ) @@ -82,10 +77,13 @@ func DefaultOptions(domain string) ServerOptions { } type Server struct { - options *ServerOptions - meta *core.PageDomain - backend core.Backend - fs http.Handler + options *ServerOptions + meta *core.PageDomain + backend core.Backend + fs http.Handler + filterMgr map[string]core.FilterInstance + + filtersCache *lru.Cache[string, glob.Glob] } var staticPrefix = "/.well-known/page-server/" @@ -97,11 +95,17 @@ func NewPageServer(backend core.Backend, options ServerOptions) *Server { if options.StaticDir != "" { fs = http.StripPrefix(staticPrefix, http.FileServer(http.Dir(options.StaticDir))) } + c, err := lru.New[string, glob.Glob](256) + if err != nil { + panic(err) + } return &Server{ - backend: backend, - options: &options, - meta: pageMeta, - fs: fs, + backend: backend, + options: &options, + meta: pageMeta, + fs: fs, + filtersCache: c, + filterMgr: filters.DefaultFilters(), } } @@ -135,106 +139,48 @@ func (s *Server) Serve(writer http.ResponseWriter, request *http.Request) error return err } zap.L().Debug("new request", zap.Any("request path", meta.Path)) - if len(meta.Alias) > 0 && !slices.Contains(meta.Alias, domain) { - // 重定向到配置的地址 - zap.L().Debug("redirect", zap.Any("src", request.Host), zap.Any("dst", meta.Alias[0])) - http.Redirect(writer, request, fmt.Sprintf("https://%s/%s", meta.Alias[0], meta.Path), http.StatusFound) - return nil - } - if s.options.EnableProxy && s.Proxy(writer, request, meta) { - return nil - } if strings.HasSuffix(meta.Path, "/") || meta.Path == "" { meta.Path += "index.html" } - // 如果不是反向代理路由则跳过任何配置 - if request.Method != http.MethodGet { - return os.ErrNotExist - } - if meta.IgnorePath(meta.Path) { - zap.L().Debug("ignore path", zap.Any("request", request.RequestURI), zap.Any("meta.path", meta.Path)) - return os.ErrNotExist - } + activeFiltersCall := make([]core.FilterCall, 0) + activeFilters := make([]core.Filter, 0) - var resp *http.Response - var path string - failback := []string{meta.Path, meta.Path + "/index.html"} - if meta.VRoute { - failback = append(failback, "index.html") - } - failback = append(failback, "404.html") - for _, p := range failback { - resp, err = meta.NativeOpen(request.Context(), p, nil) - if err != nil { - if resp != nil { - resp.Body.Close() + for _, filter := range meta.Filters { + value, ok := s.filtersCache.Get(filter.Path) + if !ok { + value, err = glob.Compile(filter.Path) + if err != nil { + continue } - if !errors.Is(err, os.ErrNotExist) { - zap.L().Debug("error", zap.Any("error", err)) - } - continue + s.filtersCache.Add(filter.Path, value) } - path = p - break - } - - if resp == nil { - return os.ErrNotExist - } - defer resp.Body.Close() - - if err != nil { - return err - } - if path == "404.html" && request.URL.Path != "/404.html" { - writer.WriteHeader(http.StatusNotFound) - } - ctx, cancel := context.WithTimeout(request.Context(), 3*time.Second) - defer cancel() - if render := meta.TryRender(path); render != nil { - return render.Render(ctx, writer, request, resp.Body, meta) - } - writer.Header().Set("Content-Type", resp.Header.Get("Content-Type")) - lastMod, err := time.Parse(http.TimeFormat, resp.Header.Get("Last-Modified")) - if err == nil { - if seeker, ok := resp.Body.(io.ReadSeeker); ok && !(path == "404.html" && request.URL.Path != "/404.html") { - http.ServeContent(writer, request, filepath.Base(path), lastMod, seeker) - return nil + if value.Match(meta.Path) { + instance := s.filterMgr[filter.Type] + if instance == nil { + return errors.New("filter not found : " + filter.Type) + } + activeFilters = append(activeFilters, filter) + call, err := instance(filter.Params) + if err != nil { + return err + } + activeFiltersCall = append(activeFiltersCall, call) } } - _, err = io.Copy(writer, resp.Body) - return err -} + slices.Reverse(activeFiltersCall) + slices.Reverse(activeFilters) -func (s *Server) Proxy(writer http.ResponseWriter, request *http.Request, meta *core.PageDomainContent) bool { - proxyPath := "/" + meta.Path - for prefix, backend := range meta.Proxy { - if strings.HasPrefix(proxyPath, prefix) { - targetPath := strings.TrimPrefix(proxyPath, prefix) - if !strings.HasPrefix(targetPath, "/") { - targetPath = "/" + targetPath - } - u, _ := url.Parse(backend) - request.URL.Path = targetPath - request.RequestURI = request.URL.RequestURI() - proxy := httputil.NewSingleHostReverseProxy(u) - proxy.Transport = s.options.HTTPClient.Transport + zap.L().Debug("active filters", zap.Any("filters", activeFilters)) - if host, _, err := net.SplitHostPort(request.RemoteAddr); err == nil { - request.Header.Set("X-Real-IP", host) - } - request.Header.Set("X-Page-IP", utils.GetRemoteIP(request)) - request.Header.Set("X-Page-Refer", fmt.Sprintf("%s/%s/%s", meta.Owner, meta.Repo, meta.Path)) - request.Header.Set("X-Page-Host", request.Host) - zap.L().Debug("命中反向代理", zap.Any("prefix", prefix), zap.Any("backend", backend), - zap.Any("path", proxyPath), zap.Any("target", fmt.Sprintf("%s%s", u, targetPath))) - // todo(security): 处理 websocket - proxy.ServeHTTP(writer, request) - return true - } + direct, _ := filters.FilterInstDirect(map[string]any{ + "prefix": "", + }) + stack := core.NextCallWrapper(direct, nil) + for _, filter := range activeFiltersCall { + stack = core.NextCallWrapper(filter, stack) } - return false + return stack(ctx, writer, request, meta) } func (s *Server) Close() error {