重构路由
This commit is contained in:
@@ -1,46 +1,48 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
import "strings"
|
import (
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
type PageConfig struct {
|
type PageConfig struct {
|
||||||
Alias []string `yaml:"alias"` // 重定向地址
|
Alias []string `yaml:"alias"` // 重定向地址
|
||||||
Render map[string]string `yaml:"templates"` // 渲染器地址
|
Routes []PageConfigRoute `yaml:"routes"` // 路由配置
|
||||||
|
|
||||||
VirtualRoute bool `yaml:"v-route"` // 是否使用虚拟路由(任何路径均使用 /index.html 返回 200 响应)
|
|
||||||
ReverseProxy map[string]string `yaml:"proxy"` // 反向代理路由
|
|
||||||
|
|
||||||
Ignore string `yaml:"ignore"` // 跳过展示的内容
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *PageConfig) Ignores() []string {
|
type PageConfigRoute struct {
|
||||||
i := make([]string, 0)
|
Path string `yaml:"path"`
|
||||||
if p.Ignore == "" {
|
Type string `yaml:"type"`
|
||||||
return i
|
Params map[string]any `yaml:"params"`
|
||||||
}
|
|
||||||
for _, line := range strings.Split(p.Ignore, "\n") {
|
|
||||||
for _, item := range strings.Split(line, ",") {
|
|
||||||
item = strings.TrimSpace(item)
|
|
||||||
if item == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
i = append(i, item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return i
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *PageConfig) Renders() map[string]string {
|
func (p *PageConfigRoute) UnmarshalYAML(value *yaml.Node) error {
|
||||||
result := make(map[string]string)
|
var data map[string]any
|
||||||
for sType, patterns := range p.Render {
|
if err := value.Decode(&data); err != nil {
|
||||||
for _, line := range strings.Split(patterns, "\n") {
|
return err
|
||||||
for _, item := range strings.Split(line, ",") {
|
|
||||||
item = strings.TrimSpace(item)
|
|
||||||
if item == "" {
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
result[sType] = item
|
if item, ok := data["path"]; ok {
|
||||||
|
p.Path = item.(string)
|
||||||
|
} else {
|
||||||
|
return errors.New("missing path field")
|
||||||
}
|
}
|
||||||
|
delete(data, "path")
|
||||||
|
keys := make([]string, 0)
|
||||||
|
for k := range data {
|
||||||
|
keys = append(keys, k)
|
||||||
}
|
}
|
||||||
|
if len(keys) != 1 {
|
||||||
|
return errors.New("invalid param")
|
||||||
}
|
}
|
||||||
return result
|
p.Type = keys[0]
|
||||||
|
params := data[p.Type]
|
||||||
|
// 跳过空参数
|
||||||
|
if _, ok := params.(string); ok || params == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out, err := yaml.Marshal(params)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return yaml.Unmarshal(out, &p.Params)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,21 @@ package core
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
type FilterParams map[string]any
|
type FilterParams map[string]any
|
||||||
|
|
||||||
|
func (f FilterParams) String() string {
|
||||||
|
marshal, _ := json.Marshal(f)
|
||||||
|
return strings.ReplaceAll(string(marshal), "\"", "'")
|
||||||
|
}
|
||||||
|
|
||||||
func (f FilterParams) Unmarshal(target any) error {
|
func (f FilterParams) Unmarshal(target any) error {
|
||||||
marshal, err := json.Marshal(f)
|
marshal, err := json.Marshal(f)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -22,9 +32,12 @@ type Filter struct {
|
|||||||
Params FilterParams `json:"params"`
|
Params FilterParams `json:"params"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NextCallWrapper(call FilterCall, parentCall NextCall) NextCall {
|
func NextCallWrapper(call FilterCall, parentCall NextCall, stack Filter) NextCall {
|
||||||
return func(ctx context.Context, writer http.ResponseWriter, request *http.Request, metadata *PageDomainContent) error {
|
return func(ctx context.Context, writer http.ResponseWriter, request *http.Request, metadata *PageDomainContent) error {
|
||||||
return call(ctx, writer, request, metadata, parentCall)
|
zap.L().Debug(fmt.Sprintf("call filter(%s) before", stack.Type), zap.Any("filter", stack))
|
||||||
|
err := call(ctx, writer, request, metadata, parentCall)
|
||||||
|
zap.L().Debug(fmt.Sprintf("call filter(%s) after", stack.Type), zap.Any("filter", stack), zap.Error(err))
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,6 +48,10 @@ type NextCall func(
|
|||||||
metadata *PageDomainContent,
|
metadata *PageDomainContent,
|
||||||
) error
|
) error
|
||||||
|
|
||||||
|
var NotFountNextCall = func(ctx context.Context, writer http.ResponseWriter, request *http.Request, metadata *PageDomainContent) error {
|
||||||
|
return os.ErrNotExist
|
||||||
|
}
|
||||||
|
|
||||||
type FilterCall func(
|
type FilterCall func(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
writer http.ResponseWriter,
|
writer http.ResponseWriter,
|
||||||
|
|||||||
@@ -5,13 +5,12 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/gobwas/glob"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"gopkg.d7z.net/middleware/kv"
|
"gopkg.d7z.net/middleware/kv"
|
||||||
"gopkg.d7z.net/middleware/tools"
|
"gopkg.d7z.net/middleware/tools"
|
||||||
@@ -50,23 +49,18 @@ func NewEmptyPageMetaContent() *PageMetaContent {
|
|||||||
Filters: []Filter{
|
Filters: []Filter{
|
||||||
{
|
{
|
||||||
Path: "**",
|
Path: "**",
|
||||||
Type: "default_not_found",
|
Type: "_404_",
|
||||||
Params: map[string]any{},
|
Params: map[string]any{},
|
||||||
},
|
},
|
||||||
{ // 默认阻塞
|
{ // 默认阻塞
|
||||||
Path: ".git/**",
|
Path: ".git/**",
|
||||||
Type: "block",
|
Type: "block",
|
||||||
Params: map[string]any{
|
Params: map[string]any{},
|
||||||
"code": "404",
|
|
||||||
"message": "Not found",
|
|
||||||
},
|
},
|
||||||
}, { // 默认阻塞
|
{ // 默认阻塞
|
||||||
Path: ".pages.yaml",
|
Path: ".pages.yaml",
|
||||||
Type: "block",
|
Type: "block",
|
||||||
Params: map[string]any{
|
Params: map[string]any{},
|
||||||
"code": "404",
|
|
||||||
"message": "Not found",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -161,6 +155,7 @@ func (s *ServerMeta) parsePageConfig(ctx context.Context, meta *PageMetaContent,
|
|||||||
defer func(alias *[]string) {
|
defer func(alias *[]string) {
|
||||||
meta.Alias = *alias
|
meta.Alias = *alias
|
||||||
direct := *alias
|
direct := *alias
|
||||||
|
if len(direct) > 0 {
|
||||||
meta.Filters = append(meta.Filters, Filter{
|
meta.Filters = append(meta.Filters, Filter{
|
||||||
Path: "**",
|
Path: "**",
|
||||||
Type: "redirect",
|
Type: "redirect",
|
||||||
@@ -168,6 +163,14 @@ func (s *ServerMeta) parsePageConfig(ctx context.Context, meta *PageMetaContent,
|
|||||||
"targets": direct,
|
"targets": direct,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
meta.Filters = append(meta.Filters, Filter{
|
||||||
|
Path: "**",
|
||||||
|
Type: "direct",
|
||||||
|
Params: map[string]any{
|
||||||
|
"prefix": "",
|
||||||
|
},
|
||||||
|
})
|
||||||
}(&alias)
|
}(&alias)
|
||||||
cname, err := vfs.ReadString(ctx, "CNAME")
|
cname, err := vfs.ReadString(ctx, "CNAME")
|
||||||
if cname != "" && err == nil {
|
if cname != "" && err == nil {
|
||||||
@@ -187,76 +190,36 @@ func (s *ServerMeta) parsePageConfig(ctx context.Context, meta *PageMetaContent,
|
|||||||
if err = yaml.Unmarshal([]byte(data), cfg); err != nil {
|
if err = yaml.Unmarshal([]byte(data), cfg); err != nil {
|
||||||
return errors.Wrap(err, "parse .pages.yaml failed")
|
return errors.Wrap(err, "parse .pages.yaml failed")
|
||||||
}
|
}
|
||||||
if cfg.VirtualRoute {
|
|
||||||
meta.Filters = append(meta.Filters, Filter{
|
|
||||||
Path: "**",
|
|
||||||
Type: "forward",
|
|
||||||
Params: map[string]any{
|
|
||||||
"path": "index.html",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理别名
|
// 处理别名
|
||||||
for _, cname := range cfg.Alias {
|
for _, item := range cfg.Alias {
|
||||||
if cname == "" {
|
if item == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if al, ok := s.aliasCheck(cname); ok {
|
if al, ok := s.aliasCheck(item); ok {
|
||||||
alias = append(alias, al)
|
alias = append(alias, al)
|
||||||
} else {
|
} else {
|
||||||
return fmt.Errorf("invalid alias %s", cname)
|
return fmt.Errorf("invalid alias %s", item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理渲染器
|
// 处理自定义路由
|
||||||
for sType, pattern := range cfg.Renders() {
|
for _, r := range cfg.Routes {
|
||||||
meta.Filters = append(meta.Filters, Filter{
|
for _, item := range strings.Split(r.Path, ",") {
|
||||||
Path: pattern,
|
item = strings.TrimSpace(item)
|
||||||
Type: sType,
|
if item == "" {
|
||||||
Params: map[string]any{},
|
continue
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
if _, err := glob.Compile(item); err != nil {
|
||||||
// 处理跳过内容
|
return errors.Wrapf(err, "invalid route glob pattern: %s", item)
|
||||||
for _, pattern := range cfg.Ignores() {
|
|
||||||
meta.Filters = append(meta.Filters, Filter{ // 默认直连
|
|
||||||
Path: pattern,
|
|
||||||
Type: "block",
|
|
||||||
Params: map[string]any{
|
|
||||||
"code": "404",
|
|
||||||
"message": "Not found",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理反向代理
|
|
||||||
for path, backend := range cfg.ReverseProxy {
|
|
||||||
path = filepath.ToSlash(filepath.Clean(path))
|
|
||||||
if !strings.HasPrefix(path, "/") {
|
|
||||||
path = "/" + path
|
|
||||||
}
|
|
||||||
path = strings.TrimSuffix(path, "/")
|
|
||||||
|
|
||||||
rURL, err := url.Parse(backend)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "parse backend url failed: %s", backend)
|
|
||||||
}
|
|
||||||
|
|
||||||
if rURL.Scheme != "http" && rURL.Scheme != "https" {
|
|
||||||
return errors.Errorf("invalid backend url scheme: %s", backend)
|
|
||||||
}
|
}
|
||||||
meta.Filters = append(meta.Filters, Filter{
|
meta.Filters = append(meta.Filters, Filter{
|
||||||
Path: path,
|
Path: item,
|
||||||
Type: "reverse_proxy",
|
Type: r.Type,
|
||||||
Params: map[string]any{
|
Params: r.Params,
|
||||||
"prefix": path,
|
|
||||||
"target": rURL.String(),
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
6
pkg/filters/README.md
Normal file
6
pkg/filters/README.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# 拦截器路径
|
||||||
|
|
||||||
|
1. 404
|
||||||
|
2. redirect
|
||||||
|
3. failback
|
||||||
|
4. direct
|
||||||
31
pkg/filters/block.go
Normal file
31
pkg/filters/block.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package filters
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"gopkg.d7z.net/gitea-pages/pkg/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
var FilterInstBlock core.FilterInstance = func(config core.FilterParams) (core.FilterCall, error) {
|
||||||
|
var param struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
if err := config.Unmarshal(¶m); nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if param.Code == 0 {
|
||||||
|
param.Code = http.StatusForbidden
|
||||||
|
}
|
||||||
|
if param.Message == "" {
|
||||||
|
param.Message = http.StatusText(param.Code)
|
||||||
|
}
|
||||||
|
return func(ctx context.Context, writer http.ResponseWriter, request *http.Request, metadata *core.PageDomainContent, next core.NextCall) error {
|
||||||
|
writer.WriteHeader(param.Code)
|
||||||
|
if param.Message != "" {
|
||||||
|
_, _ = writer.Write([]byte(param.Message))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
@@ -4,9 +4,12 @@ import "gopkg.d7z.net/gitea-pages/pkg/core"
|
|||||||
|
|
||||||
func DefaultFilters() map[string]core.FilterInstance {
|
func DefaultFilters() map[string]core.FilterInstance {
|
||||||
return map[string]core.FilterInstance{
|
return map[string]core.FilterInstance{
|
||||||
|
"block": FilterInstBlock,
|
||||||
"redirect": FilterInstRedirect,
|
"redirect": FilterInstRedirect,
|
||||||
"direct": FilterInstDirect,
|
"direct": FilterInstDirect,
|
||||||
"reverse_proxy": FilterInstProxy,
|
"reverse_proxy": FilterInstProxy,
|
||||||
"default_not_found": FilterInstDefaultNotFound,
|
"_404_": FilterInstDefaultNotFound,
|
||||||
|
"failback": FilterInstFailback,
|
||||||
|
"template": FilterInstTemplate,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ var FilterInstDefaultNotFound core.FilterInstance = func(config core.FilterParam
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
writer.Header().Set("Content-Type", "text/html; charset=utf-8")
|
writer.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
if l := open.Header.Get("Content-Length"); l != "" {
|
||||||
|
writer.Header().Set("Content-Length", l)
|
||||||
|
}
|
||||||
writer.WriteHeader(http.StatusNotFound)
|
writer.WriteHeader(http.StatusNotFound)
|
||||||
_, _ = io.Copy(writer, open.Body)
|
_, _ = io.Copy(writer, open.Body)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ package filters
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
|
"mime"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
@@ -20,12 +22,17 @@ var FilterInstDirect core.FilterInstance = func(config core.FilterParams) (core.
|
|||||||
if err := config.Unmarshal(¶m); err != nil {
|
if err := config.Unmarshal(¶m); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
param.Prefix = strings.Trim(param.Prefix, "/") + "/"
|
||||||
return func(ctx context.Context, writer http.ResponseWriter, request *http.Request, metadata *core.PageDomainContent, next core.NextCall) 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)) || err == nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
var path string
|
var path string
|
||||||
var err error
|
defaultPath := param.Prefix + strings.TrimSuffix(metadata.Path, "/")
|
||||||
failback := []string{param.Prefix + metadata.Path, param.Prefix + metadata.Path + "/index.html"}
|
for _, p := range []string{defaultPath, defaultPath + "/index.html"} {
|
||||||
for _, p := range failback {
|
zap.L().Debug("direct fetch", zap.String("path", p))
|
||||||
resp, err = metadata.NativeOpen(request.Context(), p, nil)
|
resp, err = metadata.NativeOpen(request.Context(), p, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if resp != nil {
|
if resp != nil {
|
||||||
@@ -33,6 +40,7 @@ var FilterInstDirect core.FilterInstance = func(config core.FilterParams) (core.
|
|||||||
}
|
}
|
||||||
if !errors.Is(err, os.ErrNotExist) {
|
if !errors.Is(err, os.ErrNotExist) {
|
||||||
zap.L().Debug("error", zap.Any("error", err))
|
zap.L().Debug("error", zap.Any("error", err))
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -46,7 +54,8 @@ var FilterInstDirect core.FilterInstance = func(config core.FilterParams) (core.
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
writer.Header().Set("Content-Type", resp.Header.Get("Content-Type"))
|
|
||||||
|
writer.Header().Set("Content-Type", mime.TypeByExtension(filepath.Ext(path)))
|
||||||
lastMod, err := time.Parse(http.TimeFormat, resp.Header.Get("Last-Modified"))
|
lastMod, err := time.Parse(http.TimeFormat, resp.Header.Get("Last-Modified"))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if seeker, ok := resp.Body.(io.ReadSeeker); ok {
|
if seeker, ok := resp.Body.(io.ReadSeeker); ok {
|
||||||
|
|||||||
49
pkg/filters/failback.go
Normal file
49
pkg/filters/failback.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package filters
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"mime"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gopkg.d7z.net/gitea-pages/pkg/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
var FilterInstFailback core.FilterInstance = func(config core.FilterParams) (core.FilterCall, error) {
|
||||||
|
var param struct {
|
||||||
|
Path string `json:"path"`
|
||||||
|
}
|
||||||
|
if err := config.Unmarshal(¶m); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if param.Path == "" {
|
||||||
|
return nil, errors.Errorf("filter failback: path is empty")
|
||||||
|
}
|
||||||
|
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)) || err == nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
resp, err := metadata.NativeOpen(ctx, param.Path, nil)
|
||||||
|
if resp != nil {
|
||||||
|
defer resp.Body.Close()
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
writer.Header().Set("Content-Type", mime.TypeByExtension(filepath.Ext(param.Path)))
|
||||||
|
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(param.Path), lastMod, seeker)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, err = io.Copy(writer, resp.Body)
|
||||||
|
return err
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
@@ -18,10 +18,20 @@ var portExp = regexp.MustCompile(`:\d+$`)
|
|||||||
var FilterInstRedirect core.FilterInstance = func(config core.FilterParams) (core.FilterCall, error) {
|
var FilterInstRedirect core.FilterInstance = func(config core.FilterParams) (core.FilterCall, error) {
|
||||||
var param struct {
|
var param struct {
|
||||||
Targets []string `json:"targets"`
|
Targets []string `json:"targets"`
|
||||||
|
Code int `json:"code"`
|
||||||
}
|
}
|
||||||
if err := config.Unmarshal(¶m); err != nil {
|
if err := config.Unmarshal(¶m); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if len(param.Targets) == 0 {
|
||||||
|
return nil, fmt.Errorf("no targets")
|
||||||
|
}
|
||||||
|
if param.Code == 0 {
|
||||||
|
param.Code = http.StatusFound
|
||||||
|
}
|
||||||
|
if param.Code < 300 || param.Code > 399 {
|
||||||
|
return nil, fmt.Errorf("invalid code: %d", param.Code)
|
||||||
|
}
|
||||||
return func(ctx context.Context, writer http.ResponseWriter, request *http.Request, metadata *core.PageDomainContent, next core.NextCall) error {
|
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), "")
|
domain := portExp.ReplaceAllString(strings.ToLower(request.Host), "")
|
||||||
if len(param.Targets) > 0 && !slices.Contains(metadata.Alias, domain) {
|
if len(param.Targets) > 0 && !slices.Contains(metadata.Alias, domain) {
|
||||||
@@ -37,7 +47,7 @@ var FilterInstRedirect core.FilterInstance = func(config core.FilterParams) (cor
|
|||||||
}
|
}
|
||||||
target.RawQuery = request.URL.RawQuery
|
target.RawQuery = request.URL.RawQuery
|
||||||
|
|
||||||
http.Redirect(writer, request, target.String(), http.StatusFound)
|
http.Redirect(writer, request, target.String(), param.Code)
|
||||||
return nil
|
return nil
|
||||||
} else {
|
} else {
|
||||||
return next(ctx, writer, request, metadata)
|
return next(ctx, writer, request, metadata)
|
||||||
|
|||||||
45
pkg/filters/template.go
Normal file
45
pkg/filters/template.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package filters
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gopkg.d7z.net/gitea-pages/pkg/core"
|
||||||
|
"gopkg.d7z.net/gitea-pages/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
var FilterInstTemplate 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
|
||||||
|
}
|
||||||
|
param.Prefix = strings.Trim(param.Prefix, "/") + "/"
|
||||||
|
return func(ctx context.Context, writer http.ResponseWriter, request *http.Request, metadata *core.PageDomainContent, next core.NextCall) error {
|
||||||
|
data, err := metadata.ReadString(ctx, param.Prefix+metadata.Path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
out := &bytes.Buffer{}
|
||||||
|
parse, err := utils.NewTemplate().Funcs(map[string]any{
|
||||||
|
"template": func(path string) (any, error) {
|
||||||
|
return metadata.ReadString(ctx, param.Prefix+path)
|
||||||
|
},
|
||||||
|
}).Parse(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = parse.Execute(out, utils.NewTemplateInject(request, nil))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, _ = out.WriteTo(writer)
|
||||||
|
return nil
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package pkg
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
@@ -77,13 +78,13 @@ func DefaultOptions(domain string) ServerOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
|
backend core.Backend
|
||||||
options *ServerOptions
|
options *ServerOptions
|
||||||
meta *core.PageDomain
|
meta *core.PageDomain
|
||||||
backend core.Backend
|
staticFS http.Handler
|
||||||
fs http.Handler
|
|
||||||
filterMgr map[string]core.FilterInstance
|
|
||||||
|
|
||||||
filtersCache *lru.Cache[string, glob.Glob]
|
filterMgr map[string]core.FilterInstance
|
||||||
|
globCache *lru.Cache[string, glob.Glob]
|
||||||
}
|
}
|
||||||
|
|
||||||
var staticPrefix = "/.well-known/page-server/"
|
var staticPrefix = "/.well-known/page-server/"
|
||||||
@@ -103,8 +104,8 @@ func NewPageServer(backend core.Backend, options ServerOptions) *Server {
|
|||||||
backend: backend,
|
backend: backend,
|
||||||
options: &options,
|
options: &options,
|
||||||
meta: pageMeta,
|
meta: pageMeta,
|
||||||
fs: fs,
|
staticFS: fs,
|
||||||
filtersCache: c,
|
globCache: c,
|
||||||
filterMgr: filters.DefaultFilters(),
|
filterMgr: filters.DefaultFilters(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -112,8 +113,8 @@ func NewPageServer(backend core.Backend, options ServerOptions) *Server {
|
|||||||
func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
||||||
sessionID, _ := uuid.NewRandom()
|
sessionID, _ := uuid.NewRandom()
|
||||||
request.Header.Set("Session-ID", sessionID.String())
|
request.Header.Set("Session-ID", sessionID.String())
|
||||||
if s.fs != nil && strings.HasPrefix(request.URL.Path, staticPrefix) {
|
if s.staticFS != nil && strings.HasPrefix(request.URL.Path, staticPrefix) {
|
||||||
s.fs.ServeHTTP(writer, request)
|
s.staticFS.ServeHTTP(writer, request)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
@@ -145,15 +146,16 @@ func (s *Server) Serve(writer http.ResponseWriter, request *http.Request) error
|
|||||||
}
|
}
|
||||||
activeFiltersCall := make([]core.FilterCall, 0)
|
activeFiltersCall := make([]core.FilterCall, 0)
|
||||||
activeFilters := make([]core.Filter, 0)
|
activeFilters := make([]core.Filter, 0)
|
||||||
|
filtersRoute := make([]string, 0)
|
||||||
|
|
||||||
for _, filter := range meta.Filters {
|
for _, filter := range meta.Filters {
|
||||||
value, ok := s.filtersCache.Get(filter.Path)
|
value, ok := s.globCache.Get(filter.Path)
|
||||||
if !ok {
|
if !ok {
|
||||||
value, err = glob.Compile(filter.Path)
|
value, err = glob.Compile(filter.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
s.filtersCache.Add(filter.Path, value)
|
s.globCache.Add(filter.Path, value)
|
||||||
}
|
}
|
||||||
if value.Match(meta.Path) {
|
if value.Match(meta.Path) {
|
||||||
instance := s.filterMgr[filter.Type]
|
instance := s.filterMgr[filter.Type]
|
||||||
@@ -161,6 +163,7 @@ func (s *Server) Serve(writer http.ResponseWriter, request *http.Request) error
|
|||||||
return errors.New("filter not found : " + filter.Type)
|
return errors.New("filter not found : " + filter.Type)
|
||||||
}
|
}
|
||||||
activeFilters = append(activeFilters, filter)
|
activeFilters = append(activeFilters, filter)
|
||||||
|
filtersRoute = append(filtersRoute, fmt.Sprintf("%s[%s]%s", filter.Type, filter.Path, filter.Params))
|
||||||
call, err := instance(filter.Params)
|
call, err := instance(filter.Params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -171,16 +174,18 @@ func (s *Server) Serve(writer http.ResponseWriter, request *http.Request) error
|
|||||||
slices.Reverse(activeFiltersCall)
|
slices.Reverse(activeFiltersCall)
|
||||||
slices.Reverse(activeFilters)
|
slices.Reverse(activeFilters)
|
||||||
|
|
||||||
zap.L().Debug("active filters", zap.Any("filters", activeFilters))
|
l := len(filtersRoute)
|
||||||
|
for i := l - 2; i >= 0; i-- {
|
||||||
direct, _ := filters.FilterInstDirect(map[string]any{
|
filtersRoute = append(filtersRoute, filtersRoute[i])
|
||||||
"prefix": "",
|
|
||||||
})
|
|
||||||
stack := core.NextCallWrapper(direct, nil)
|
|
||||||
for _, filter := range activeFiltersCall {
|
|
||||||
stack = core.NextCallWrapper(filter, stack)
|
|
||||||
}
|
}
|
||||||
return stack(ctx, writer, request, meta)
|
zap.L().Debug("active filters", zap.String("filters", strings.Join(filtersRoute, " -> ")))
|
||||||
|
|
||||||
|
var stack core.NextCall = core.NotFountNextCall
|
||||||
|
for i, filter := range activeFiltersCall {
|
||||||
|
stack = core.NextCallWrapper(filter, stack, activeFilters[i])
|
||||||
|
}
|
||||||
|
err = stack(ctx, writer, request, meta)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) Close() error {
|
func (s *Server) Close() error {
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ func NewDummy() (*ProviderDummy, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *ProviderDummy) Repos(ctx context.Context, owner string) (map[string]string, error) {
|
func (p *ProviderDummy) Repos(_ context.Context, owner string) (map[string]string, error) {
|
||||||
dir, err := os.ReadDir(filepath.Join(p.BaseDir, owner))
|
dir, err := os.ReadDir(filepath.Join(p.BaseDir, owner))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -44,7 +44,7 @@ func (p *ProviderDummy) Repos(ctx context.Context, owner string) (map[string]str
|
|||||||
return repos, nil
|
return repos, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *ProviderDummy) Branches(ctx context.Context, owner, repo string) (map[string]*core.BranchInfo, error) {
|
func (p *ProviderDummy) Branches(_ context.Context, owner, repo string) (map[string]*core.BranchInfo, error) {
|
||||||
dir, err := os.ReadDir(filepath.Join(p.BaseDir, owner, repo))
|
dir, err := os.ReadDir(filepath.Join(p.BaseDir, owner, repo))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -99,47 +99,3 @@ func Test_fail_back(t *testing.T) {
|
|||||||
assert.Equal(t, "hello world 2", string(data))
|
assert.Equal(t, "hello world 2", string(data))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_get_v_route(t *testing.T) {
|
|
||||||
server := core.NewDefaultTestServer()
|
|
||||||
defer server.Close()
|
|
||||||
server.AddFile("org1/repo1/gh-pages/index.html", "hello world")
|
|
||||||
server.AddFile("org1/repo1/gh-pages/.pages.yaml", `
|
|
||||||
v-route: true
|
|
||||||
`)
|
|
||||||
data, _, err := server.OpenFile("https://org1.example.com/repo1/")
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, "hello world", string(data))
|
|
||||||
|
|
||||||
data, _, err = server.OpenFile("https://org1.example.com/repo1/404")
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, "hello world", string(data))
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_get_v_ignore(t *testing.T) {
|
|
||||||
server := core.NewDefaultTestServer()
|
|
||||||
defer server.Close()
|
|
||||||
server.AddFile("org1/repo1/gh-pages/index.html", "hello world")
|
|
||||||
server.AddFile("org1/repo1/gh-pages/bad.html", "hello world")
|
|
||||||
server.AddFile("org1/repo1/gh-pages/.pages.yaml", `
|
|
||||||
ignore: .pages.yaml
|
|
||||||
`)
|
|
||||||
data, _, err := server.OpenFile("https://org1.example.com/repo1/")
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, "hello world", string(data))
|
|
||||||
|
|
||||||
data, _, err = server.OpenFile("https://org1.example.com/repo1/bad.html")
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, "hello world", string(data))
|
|
||||||
server.AddFile("org1/repo1/gh-pages/.pages.yaml", `
|
|
||||||
ignore: bad.*
|
|
||||||
`)
|
|
||||||
data, _, err = server.OpenFile("https://org1.example.com/repo1/")
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, "hello world", string(data))
|
|
||||||
_, resp, _ := server.OpenFile("https://org1.example.com/repo1/bad.html")
|
|
||||||
assert.Equal(t, 404, resp.StatusCode)
|
|
||||||
// 默认排除的内容
|
|
||||||
_, resp, _ = server.OpenFile("https://org1.example.com/repo1/.pages.yaml")
|
|
||||||
assert.Equal(t, 404, resp.StatusCode)
|
|
||||||
}
|
|
||||||
36
tests/filter_block_test.go
Normal file
36
tests/filter_block_test.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package tests
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"gopkg.d7z.net/gitea-pages/tests/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_filter_block(t *testing.T) {
|
||||||
|
server := core.NewDefaultTestServer()
|
||||||
|
defer server.Close()
|
||||||
|
server.AddFile("org1/repo1/gh-pages/index.html", "hello world")
|
||||||
|
server.AddFile("org1/repo1/gh-pages/bad.html", "hello world")
|
||||||
|
data, _, err := server.OpenFile("https://org1.example.com/repo1/")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "hello world", string(data))
|
||||||
|
|
||||||
|
data, _, err = server.OpenFile("https://org1.example.com/repo1/bad.html")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "hello world", string(data))
|
||||||
|
server.AddFile("org1/repo1/gh-pages/.pages.yaml", `
|
||||||
|
routes:
|
||||||
|
- path: "bad.html"
|
||||||
|
block:
|
||||||
|
code:
|
||||||
|
`)
|
||||||
|
data, _, err = server.OpenFile("https://org1.example.com/repo1/")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "hello world", string(data))
|
||||||
|
_, resp, _ := server.OpenFile("https://org1.example.com/repo1/bad.html")
|
||||||
|
assert.Equal(t, 403, resp.StatusCode)
|
||||||
|
// 默认排除的内容
|
||||||
|
_, resp, _ = server.OpenFile("https://org1.example.com/repo1/.pages.yaml")
|
||||||
|
assert.Equal(t, 403, resp.StatusCode)
|
||||||
|
}
|
||||||
35
tests/filter_failback_test.go
Normal file
35
tests/filter_failback_test.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package tests
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"gopkg.d7z.net/gitea-pages/tests/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_filter_failback(t *testing.T) {
|
||||||
|
server := core.NewDefaultTestServer()
|
||||||
|
defer server.Close()
|
||||||
|
server.AddFile("org1/repo1/gh-pages/index.html", "hello world")
|
||||||
|
server.AddFile("org1/repo1/gh-pages/404.html", "404 page")
|
||||||
|
server.AddFile("org1/repo1/gh-pages/.pages.yaml", `
|
||||||
|
routes:
|
||||||
|
- path: "**"
|
||||||
|
failback:
|
||||||
|
path: index.html
|
||||||
|
|
||||||
|
`)
|
||||||
|
//data, _, err := server.OpenFile("https://org1.example.com/repo1/")
|
||||||
|
//assert.NoError(t, err)
|
||||||
|
//assert.Equal(t, "hello world", string(data))
|
||||||
|
//
|
||||||
|
//// 测试默认回退
|
||||||
|
//data, _, err = server.OpenFile("https://org1.example.com/repo1/404")
|
||||||
|
//assert.NoError(t, err)
|
||||||
|
//assert.Equal(t, "hello world", string(data))
|
||||||
|
|
||||||
|
// 测试存在的页面
|
||||||
|
data, _, err := server.OpenFile("https://org1.example.com/repo1/404.html")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "404 page", string(data))
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"gopkg.d7z.net/gitea-pages/tests/core"
|
"gopkg.d7z.net/gitea-pages/tests/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_proxy(t *testing.T) {
|
func test_proxy(t *testing.T) {
|
||||||
server := core.NewDefaultTestServer()
|
server := core.NewDefaultTestServer()
|
||||||
hs := core.NewServer()
|
hs := core.NewServer()
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
@@ -17,8 +17,12 @@ func Test_proxy(t *testing.T) {
|
|||||||
|
|
||||||
server.AddFile("org1/repo1/gh-pages/index.html", "hello world")
|
server.AddFile("org1/repo1/gh-pages/index.html", "hello world")
|
||||||
server.AddFile("org1/repo1/gh-pages/.pages.yaml", `
|
server.AddFile("org1/repo1/gh-pages/.pages.yaml", `
|
||||||
|
routes:
|
||||||
|
- path: /api/**
|
||||||
|
reverse_proxy:
|
||||||
|
prefix: /api
|
||||||
|
target: %s
|
||||||
proxy:
|
proxy:
|
||||||
/api: %s/test
|
|
||||||
/abi: %s/
|
/abi: %s/
|
||||||
`, hs.URL, hs.URL)
|
`, hs.URL, hs.URL)
|
||||||
data, _, err := server.OpenFile("https://org1.example.com/repo1/")
|
data, _, err := server.OpenFile("https://org1.example.com/repo1/")
|
||||||
@@ -36,7 +40,7 @@ proxy:
|
|||||||
assert.Equal(t, 404, resp.StatusCode)
|
assert.Equal(t, 404, resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_cname_proxy(t *testing.T) {
|
func test_cname_proxy(t *testing.T) {
|
||||||
server := core.NewDefaultTestServer()
|
server := core.NewDefaultTestServer()
|
||||||
hs := core.NewServer()
|
hs := core.NewServer()
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
@@ -9,14 +9,16 @@ import (
|
|||||||
_ "gopkg.d7z.net/gitea-pages/pkg/renders"
|
_ "gopkg.d7z.net/gitea-pages/pkg/renders"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_get_render(t *testing.T) {
|
func Test_Filter_Template(t *testing.T) {
|
||||||
server := core.NewDefaultTestServer()
|
server := core.NewDefaultTestServer()
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
server.AddFile("org1/repo1/gh-pages/index.html", "hello world")
|
server.AddFile("org1/repo1/gh-pages/index.html", "hello world")
|
||||||
server.AddFile("org1/repo1/gh-pages/tmpl/index.html", "hello world,{{ .Request.Host }}")
|
server.AddFile("org1/repo1/gh-pages/tmpl/index.html", "hello world,{{ .Request.Host }}")
|
||||||
|
server.AddFile("org1/repo1/gh-pages/tmpl/ignore.html", "hello world, No Template")
|
||||||
server.AddFile("org1/repo1/gh-pages/.pages.yaml", `
|
server.AddFile("org1/repo1/gh-pages/.pages.yaml", `
|
||||||
templates:
|
routes:
|
||||||
gotemplate: tmpl/*.html
|
- path: tmpl/index.html
|
||||||
|
template:
|
||||||
`)
|
`)
|
||||||
data, _, err := server.OpenFile("https://org1.example.com/repo1/")
|
data, _, err := server.OpenFile("https://org1.example.com/repo1/")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
@@ -25,4 +27,8 @@ templates:
|
|||||||
data, _, err = server.OpenFile("https://org1.example.com/repo1/tmpl/index.html")
|
data, _, err = server.OpenFile("https://org1.example.com/repo1/tmpl/index.html")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, "hello world,org1.example.com", string(data))
|
assert.Equal(t, "hello world,org1.example.com", string(data))
|
||||||
|
|
||||||
|
data, _, err = server.OpenFile("https://org1.example.com/repo1/tmpl/ignore.html")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "hello world, No Template", string(data))
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user