♻️ Refactored process manager
This commit is contained in:
45
pkg/navi/configurator.go
Normal file
45
pkg/navi/configurator.go
Normal file
@ -0,0 +1,45 @@
|
||||
package navi
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
var App *RoadApp
|
||||
|
||||
func ReadInConfig(root string) error {
|
||||
instance := &RoadApp{
|
||||
Sites: []*SiteConfig{},
|
||||
}
|
||||
|
||||
if err := filepath.Walk(root, func(fp string, info os.FileInfo, err error) error {
|
||||
var site SiteConfig
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
} else if file, err := os.OpenFile(fp, os.O_RDONLY, 0755); err != nil {
|
||||
return err
|
||||
} else if data, err := io.ReadAll(file); err != nil {
|
||||
return err
|
||||
} else if err := yaml.Unmarshal(data, &site); err != nil {
|
||||
return err
|
||||
} else {
|
||||
defer file.Close()
|
||||
|
||||
// Extract file name as site id
|
||||
site.ID = strings.SplitN(filepath.Base(fp), ".", 2)[0]
|
||||
instance.Sites = append(instance.Sites, &site)
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
App = instance
|
||||
|
||||
return nil
|
||||
}
|
127
pkg/navi/responder.go
Normal file
127
pkg/navi/responder.go
Normal file
@ -0,0 +1,127 @@
|
||||
package navi
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/proxy"
|
||||
"github.com/gofiber/fiber/v2/utils"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
func makeHypertextResponse(c *fiber.Ctx, upstream *UpstreamInstance) error {
|
||||
timeout := time.Duration(viper.GetInt64("performance.network_timeout")) * time.Millisecond
|
||||
return proxy.Do(c, upstream.MakeURI(c), &fasthttp.Client{
|
||||
ReadTimeout: timeout,
|
||||
WriteTimeout: timeout,
|
||||
})
|
||||
}
|
||||
|
||||
func makeFileResponse(c *fiber.Ctx, upstream *UpstreamInstance) error {
|
||||
uri, queries := upstream.GetRawURI()
|
||||
root := http.Dir(uri)
|
||||
|
||||
method := c.Method()
|
||||
|
||||
// We only serve static assets for GET and HEAD methods
|
||||
if method != fiber.MethodGet && method != fiber.MethodHead {
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
// Strip prefix
|
||||
prefix := c.Route().Path
|
||||
path := strings.TrimPrefix(c.Path(), prefix)
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
path = "/" + path
|
||||
}
|
||||
|
||||
// Add prefix
|
||||
if queries.Get("prefix") != "" {
|
||||
path = queries.Get("prefix") + path
|
||||
}
|
||||
|
||||
if len(path) > 1 {
|
||||
path = utils.TrimRight(path, '/')
|
||||
}
|
||||
|
||||
file, err := root.Open(path)
|
||||
if err != nil && errors.Is(err, fs.ErrNotExist) {
|
||||
if queries.Get("suffix") != "" {
|
||||
file, err = root.Open(path + queries.Get("suffix"))
|
||||
}
|
||||
if err != nil && queries.Get("fallback") != "" {
|
||||
file, err = root.Open(queries.Get("fallback"))
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return fiber.ErrNotFound
|
||||
}
|
||||
return fmt.Errorf("failed to open: %w", err)
|
||||
}
|
||||
|
||||
stat, err := file.Stat()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to stat: %w", err)
|
||||
}
|
||||
|
||||
// Serve index if path is directory
|
||||
if stat.IsDir() {
|
||||
indexFile := lo.Ternary(len(queries.Get("index")) > 0, queries.Get("index"), "index.html")
|
||||
indexPath := utils.TrimRight(path, '/') + indexFile
|
||||
index, err := root.Open(indexPath)
|
||||
if err == nil {
|
||||
indexStat, err := index.Stat()
|
||||
if err == nil {
|
||||
file = index
|
||||
stat = indexStat
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.Status(fiber.StatusOK)
|
||||
|
||||
modTime := stat.ModTime()
|
||||
contentLength := int(stat.Size())
|
||||
|
||||
// Set Content-Type header
|
||||
if queries.Get("charset") == "" {
|
||||
c.Type(filepath.Ext(stat.Name()))
|
||||
} else {
|
||||
c.Type(filepath.Ext(stat.Name()), queries.Get("charset"))
|
||||
}
|
||||
|
||||
// Set Last-Modified header
|
||||
if !modTime.IsZero() {
|
||||
c.Set(fiber.HeaderLastModified, modTime.UTC().Format(http.TimeFormat))
|
||||
}
|
||||
|
||||
if method == fiber.MethodGet {
|
||||
maxAge, err := strconv.Atoi(queries.Get("maxAge"))
|
||||
if lo.Ternary(err != nil, maxAge, 0) > 0 {
|
||||
c.Set(fiber.HeaderCacheControl, "public, max-age="+queries.Get("maxAge"))
|
||||
}
|
||||
c.Response().SetBodyStream(file, contentLength)
|
||||
return nil
|
||||
}
|
||||
if method == fiber.MethodHead {
|
||||
c.Request().ResetBody()
|
||||
c.Response().SkipBody = true
|
||||
c.Response().Header.SetContentLength(contentLength)
|
||||
if err := file.Close(); err != nil {
|
||||
return fmt.Errorf("failed to close: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return fiber.ErrNotFound
|
||||
}
|
49
pkg/navi/route.go
Normal file
49
pkg/navi/route.go
Normal file
@ -0,0 +1,49 @@
|
||||
package navi
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math/rand"
|
||||
|
||||
"code.smartsheep.studio/goatworks/roadsign/pkg/navi/transformers"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type RoadApp struct {
|
||||
Sites []*SiteConfig `json:"sites"`
|
||||
}
|
||||
|
||||
func (v *RoadApp) Forward(ctx *fiber.Ctx, site *SiteConfig) error {
|
||||
if len(site.Upstreams) == 0 {
|
||||
return errors.New("invalid configuration")
|
||||
}
|
||||
|
||||
// Do forward
|
||||
idx := rand.Intn(len(site.Upstreams))
|
||||
upstream := site.Upstreams[idx]
|
||||
|
||||
switch upstream.GetType() {
|
||||
case UpstreamTypeHypertext:
|
||||
return makeHypertextResponse(ctx, upstream)
|
||||
case UpstreamTypeFile:
|
||||
return makeFileResponse(ctx, upstream)
|
||||
default:
|
||||
return fiber.ErrBadGateway
|
||||
}
|
||||
}
|
||||
|
||||
type RequestTransformerConfig = transformers.RequestTransformerConfig
|
||||
|
||||
type SiteConfig struct {
|
||||
ID string `json:"id"`
|
||||
Rules []*RouterRule `json:"rules" yaml:"rules"`
|
||||
Transformers []*RequestTransformerConfig `json:"transformers" yaml:"transformers"`
|
||||
Upstreams []*UpstreamInstance `json:"upstreams" yaml:"upstreams"`
|
||||
}
|
||||
|
||||
type RouterRule struct {
|
||||
Host []string `json:"host" yaml:"host"`
|
||||
Path []string `json:"path" yaml:"path"`
|
||||
Queries map[string]string `json:"queries" yaml:"queries"`
|
||||
Headers map[string][]string `json:"headers" yaml:"headers"`
|
||||
}
|
41
pkg/navi/transformers/compress.go
Normal file
41
pkg/navi/transformers/compress.go
Normal file
@ -0,0 +1,41 @@
|
||||
package transformers
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
var CompressResponse = RequestTransformer{
|
||||
ModifyResponse: func(options any, ctx *fiber.Ctx) error {
|
||||
opts := DeserializeOptions[struct {
|
||||
Level int `json:"level" yaml:"level"`
|
||||
}](options)
|
||||
|
||||
var fctx = func(c *fasthttp.RequestCtx) {}
|
||||
var compressor fasthttp.RequestHandler
|
||||
switch opts.Level {
|
||||
// Best Speed Mode
|
||||
case 1:
|
||||
compressor = fasthttp.CompressHandlerBrotliLevel(fctx,
|
||||
fasthttp.CompressBrotliBestSpeed,
|
||||
fasthttp.CompressBestSpeed,
|
||||
)
|
||||
// Best Compression Mode
|
||||
case 2:
|
||||
compressor = fasthttp.CompressHandlerBrotliLevel(fctx,
|
||||
fasthttp.CompressBrotliBestCompression,
|
||||
fasthttp.CompressBestCompression,
|
||||
)
|
||||
// Default Mode
|
||||
default:
|
||||
compressor = fasthttp.CompressHandlerBrotliLevel(fctx,
|
||||
fasthttp.CompressBrotliDefaultCompression,
|
||||
fasthttp.CompressDefaultCompression,
|
||||
)
|
||||
}
|
||||
|
||||
compressor(ctx.Context())
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
61
pkg/navi/transformers/module.go
Normal file
61
pkg/navi/transformers/module.go
Normal file
@ -0,0 +1,61 @@
|
||||
package transformers
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
// Definitions
|
||||
|
||||
var json = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
|
||||
type RequestTransformer struct {
|
||||
ModifyRequest func(options any, ctx *fiber.Ctx) error
|
||||
ModifyResponse func(options any, ctx *fiber.Ctx) error
|
||||
}
|
||||
|
||||
type RequestTransformerConfig struct {
|
||||
Type string `json:"type" yaml:"type"`
|
||||
Options any `json:"options" yaml:"options"`
|
||||
}
|
||||
|
||||
func (v *RequestTransformerConfig) TransformRequest(ctx *fiber.Ctx) error {
|
||||
for k, f := range Transformers {
|
||||
if k == v.Type {
|
||||
if f.ModifyRequest != nil {
|
||||
return f.ModifyRequest(v.Options, ctx)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *RequestTransformerConfig) TransformResponse(ctx *fiber.Ctx) error {
|
||||
for k, f := range Transformers {
|
||||
if k == v.Type {
|
||||
if f.ModifyResponse != nil {
|
||||
return f.ModifyResponse(v.Options, ctx)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Helpers
|
||||
|
||||
func DeserializeOptions[T any](data any) T {
|
||||
var out T
|
||||
raw, _ := json.Marshal(data)
|
||||
_ = json.Unmarshal(raw, &out)
|
||||
return out
|
||||
}
|
||||
|
||||
// Map of Transformers
|
||||
// Every transformer need to be mapped here so that they can get work.
|
||||
|
||||
var Transformers = map[string]RequestTransformer{
|
||||
"replacePath": ReplacePath,
|
||||
"compressResponse": CompressResponse,
|
||||
}
|
25
pkg/navi/transformers/replace_path.go
Normal file
25
pkg/navi/transformers/replace_path.go
Normal file
@ -0,0 +1,25 @@
|
||||
package transformers
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var ReplacePath = RequestTransformer{
|
||||
ModifyRequest: func(options any, ctx *fiber.Ctx) error {
|
||||
opts := DeserializeOptions[struct {
|
||||
Pattern string `json:"pattern" yaml:"pattern"`
|
||||
Value string `json:"value" yaml:"value"`
|
||||
Repl string `json:"repl" yaml:"repl"` // Use when complex mode(regexp) enabled
|
||||
Complex bool `json:"complex" yaml:"complex"`
|
||||
}](options)
|
||||
path := string(ctx.Request().URI().Path())
|
||||
if !opts.Complex {
|
||||
ctx.Path(strings.ReplaceAll(path, opts.Pattern, opts.Value))
|
||||
} else if ex := regexp.MustCompile(opts.Pattern); ex != nil {
|
||||
ctx.Path(ex.ReplaceAllString(path, opts.Repl))
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
58
pkg/navi/upstream.go
Normal file
58
pkg/navi/upstream.go
Normal file
@ -0,0 +1,58 @@
|
||||
package navi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
const (
|
||||
UpstreamTypeFile = "file"
|
||||
UpstreamTypeHypertext = "hypertext"
|
||||
UpstreamTypeUnknown = "unknown"
|
||||
)
|
||||
|
||||
type UpstreamInstance struct {
|
||||
ID string `json:"id" yaml:"id"`
|
||||
URI string `json:"uri" yaml:"uri"`
|
||||
}
|
||||
|
||||
func (v *UpstreamInstance) GetType() string {
|
||||
protocol := strings.SplitN(v.URI, "://", 2)[0]
|
||||
switch protocol {
|
||||
case "file", "files":
|
||||
return UpstreamTypeFile
|
||||
case "http", "https":
|
||||
return UpstreamTypeHypertext
|
||||
}
|
||||
|
||||
return UpstreamTypeUnknown
|
||||
}
|
||||
|
||||
func (v *UpstreamInstance) GetRawURI() (string, url.Values) {
|
||||
uri := strings.SplitN(v.URI, "://", 2)[1]
|
||||
data := strings.SplitN(uri, "?", 2)
|
||||
data = append(data, " ") // Make data array least have two element
|
||||
qs, _ := url.ParseQuery(data[0])
|
||||
|
||||
return data[0], qs
|
||||
}
|
||||
|
||||
func (v *UpstreamInstance) MakeURI(ctx *fiber.Ctx) string {
|
||||
var queries []string
|
||||
for k, v := range ctx.Queries() {
|
||||
parsed, _ := url.QueryUnescape(v)
|
||||
value := url.QueryEscape(parsed)
|
||||
queries = append(queries, fmt.Sprintf("%s=%s", k, value))
|
||||
}
|
||||
|
||||
path := string(ctx.Request().URI().Path())
|
||||
hash := string(ctx.Request().URI().Hash())
|
||||
|
||||
return v.URI + path +
|
||||
lo.Ternary(len(queries) > 0, "?"+strings.Join(queries, "&"), "") +
|
||||
lo.Ternary(len(hash) > 0, "#"+hash, "")
|
||||
}
|
Reference in New Issue
Block a user