Transformers

This commit is contained in:
LittleSheep 2023-11-18 00:23:40 +08:00
parent 2fc1ef89db
commit ff1f1dbc9d
13 changed files with 257 additions and 98 deletions

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

15
README.md Normal file
View File

@ -0,0 +1,15 @@
# 🚦 RoadSign
A blazing fast reverse proxy with a lot of shining features.
## Features
1. Reverse proxy
2. Static file hosting
3. Analytics and Metrics
4. Integrate with CI/CD
5. Webhook integration
6. Web management panel
7. **Blazing fast ⚡**
### How fast is it?

View File

@ -2,14 +2,27 @@
"name": "Example Site", "name": "Example Site",
"rules": [ "rules": [
{ {
"host": ["localhost"], "host": [
"path": ["/"] "localhost"
],
"path": [
"/"
]
} }
], ],
"upstreams": [ "upstreams": [
{ {
"name": "Example Upstream", "name": "Example Upstream",
"uri": "https://example.com" "uri": "https://disk.smartsheep.studio"
}
],
"transformers": [
{
"type": "replacePath",
"options": {
"pattern": "/p/123",
"value": "/p/%E5%B7%A5%E4%BD%9C%E5%AE%A4/icon.png"
}
} }
] ]
} }

View File

@ -0,0 +1,20 @@
package administration
import (
roadsign "code.smartsheep.studio/goatworks/roadsign/pkg"
"fmt"
"github.com/gofiber/fiber/v2"
"github.com/spf13/viper"
)
func InitAdministration() *fiber.App {
app := fiber.New(fiber.Config{
AppName: "RoadSign Administration",
ServerHeader: fmt.Sprintf("RoadSign Administration v%s", roadsign.AppVersion),
DisableStartupMessage: true,
EnableIPValidation: true,
TrustedProxies: viper.GetStringSlice("security.administration_trusted_proxies"),
})
return app
}

View File

@ -2,6 +2,7 @@ package main
import ( import (
roadsign "code.smartsheep.studio/goatworks/roadsign/pkg" roadsign "code.smartsheep.studio/goatworks/roadsign/pkg"
"code.smartsheep.studio/goatworks/roadsign/pkg/administration"
"code.smartsheep.studio/goatworks/roadsign/pkg/configurator" "code.smartsheep.studio/goatworks/roadsign/pkg/configurator"
"code.smartsheep.studio/goatworks/roadsign/pkg/hypertext" "code.smartsheep.studio/goatworks/roadsign/pkg/hypertext"
"github.com/rs/zerolog" "github.com/rs/zerolog"
@ -37,7 +38,22 @@ func main() {
} }
// Init hypertext server // Init hypertext server
hypertext.RunServer(hypertext.InitServer()) hypertext.RunServer(
hypertext.InitServer(),
viper.GetStringSlice("hypertext.ports"),
viper.GetStringSlice("hypertext.secured_ports"),
viper.GetString("hypertext.certificate.pem"),
viper.GetString("hypertext.certificate.key"),
)
// Init administration server
hypertext.RunServer(
administration.InitAdministration(),
viper.GetStringSlice("hypertext.administration_ports"),
viper.GetStringSlice("hypertext.administration_secured_ports"),
viper.GetString("hypertext.certificate.administration_pem"),
viper.GetString("hypertext.certificate.administration_key"),
)
log.Info().Msgf("RoadSign v%s is started...", roadsign.AppVersion) log.Info().Msgf("RoadSign v%s is started...", roadsign.AppVersion)

View File

@ -0,0 +1,10 @@
package configurator
import "encoding/json"
func DeserializeOptions[T any](data any) T {
var out T
raw, _ := json.Marshal(data)
_ = json.Unmarshal(raw, &out)
return out
}

View File

@ -0,0 +1,41 @@
package configurator
import (
"math/rand"
)
type AppConfig struct {
Sites []SiteConfig `json:"sites"`
}
func (v *AppConfig) LoadBalance(site SiteConfig) *UpstreamConfig {
idx := rand.Intn(len(site.Upstreams))
switch site.Upstreams[idx].GetType() {
case UpstreamTypeHypertext:
return &site.Upstreams[idx]
case UpstreamTypeFile:
// TODO Make this into hypertext configuration
return &site.Upstreams[idx]
default:
// Give him the null value when this configuration is invalid.
// Then we can print a log in the console to warm him.
return nil
}
}
type SiteConfig struct {
ID string `json:"id"`
Name string `json:"name"`
Rules []RouterRuleConfig `json:"rules"`
Transformers []RequestTransformerConfig `json:"transformers"`
Upstreams []UpstreamConfig `json:"upstreams"`
}
type RouterRuleConfig struct {
Host []string `json:"host"`
Path []string `json:"path"`
Queries map[string]string `json:"query"`
Headers map[string][]string `json:"headers"`
}

View File

@ -1,46 +0,0 @@
package configurator
import "strings"
type AppConfig struct {
Sites []SiteConfig `json:"sites"`
}
type SiteConfig struct {
ID string `json:"id"`
Name string `json:"name"`
Rules []RouterRuleConfig `json:"rules"`
Upstreams []UpstreamConfig `json:"upstreams"`
}
type RouterRuleConfig struct {
Host []string `json:"host"`
Path []string `json:"path"`
Queries map[string]string `json:"query"`
Headers map[string][]string `json:"headers"`
}
const (
UpstreamTypeFile = "file"
UpstreamTypeHypertext = "hypertext"
UpstreamTypeUnknown = "unknown"
)
type UpstreamConfig struct {
Name string `json:"name"`
URI string `json:"uri"`
}
func (v *UpstreamConfig) GetType() string {
protocol := strings.SplitN(v.URI, "://", 2)[0]
switch protocol {
case "file":
return UpstreamTypeFile
case "http":
case "https":
return UpstreamTypeHypertext
}
return UpstreamTypeUnknown
}

View File

@ -0,0 +1,58 @@
package configurator
import (
"github.com/gofiber/fiber/v2"
"regexp"
"strings"
)
type RequestTransformer struct {
ModifyRequest func(options any, ctx *fiber.Ctx)
ModifyResponse func(options any, ctx *fiber.Ctx)
}
type RequestTransformerConfig struct {
Type string `json:"type"`
Options any `json:"options"`
}
func (v *RequestTransformerConfig) TransformRequest(ctx *fiber.Ctx) {
for k, f := range Transformers {
if k == v.Type {
if f.ModifyRequest != nil {
f.ModifyRequest(v.Options, ctx)
}
break
}
}
}
func (v *RequestTransformerConfig) TransformResponse(ctx *fiber.Ctx) {
for k, f := range Transformers {
if k == v.Type {
if f.ModifyResponse != nil {
f.ModifyResponse(v.Options, ctx)
}
break
}
}
}
var Transformers = map[string]RequestTransformer{
"replacePath": {
ModifyRequest: func(options any, ctx *fiber.Ctx) {
opts := DeserializeOptions[struct {
Pattern string `json:"pattern"`
Value string `json:"value"`
Repl string `json:"repl"` // Use when complex mode(regexp) enabled
Complex bool `json:"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))
}
},
},
}

View File

@ -0,0 +1,46 @@
package configurator
import (
"fmt"
"github.com/gofiber/fiber/v2"
"github.com/samber/lo"
"strings"
)
const (
UpstreamTypeFile = "file"
UpstreamTypeHypertext = "hypertext"
UpstreamTypeUnknown = "unknown"
)
type UpstreamConfig struct {
Name string `json:"name"`
URI string `json:"uri"`
}
func (v *UpstreamConfig) GetType() string {
protocol := strings.SplitN(v.URI, "://", 2)[0]
switch protocol {
case "file":
return UpstreamTypeFile
case "http":
case "https":
return UpstreamTypeHypertext
}
return UpstreamTypeUnknown
}
func (v *UpstreamConfig) MakeURI(ctx *fiber.Ctx) string {
var queries []string
for k, v := range ctx.Queries() {
queries = append(queries, fmt.Sprintf("%s=%s", k, v))
}
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, "")
}

View File

@ -33,8 +33,8 @@ func InitServer() *fiber.App {
return app return app
} }
func RunServer(app *fiber.App) { func RunServer(app *fiber.App, ports []string, securedPorts []string, pem string, key string) {
for _, port := range viper.GetStringSlice("hypertext.ports") { for _, port := range ports {
port := port port := port
go func() { go func() {
if err := app.Listen(port); err != nil { if err := app.Listen(port); err != nil {
@ -43,10 +43,8 @@ func RunServer(app *fiber.App) {
}() }()
} }
for _, port := range viper.GetStringSlice("hypertext.secured_ports") { for _, port := range securedPorts {
port := port port := port
pem := viper.GetString("hypertext.certificate.pem")
key := viper.GetString("hypertext.certificate.key")
go func() { go func() {
if err := app.ListenTLS(port, pem, key); err != nil { if err := app.ListenTLS(port, pem, key); err != nil {
log.Panic().Err(err).Msg("An error occurred when listening hypertext tls ports.") log.Panic().Err(err).Msg("An error occurred when listening hypertext tls ports.")

View File

@ -2,33 +2,23 @@ package hypertext
import ( import (
"code.smartsheep.studio/goatworks/roadsign/pkg/configurator" "code.smartsheep.studio/goatworks/roadsign/pkg/configurator"
"fmt"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/proxy" "github.com/gofiber/fiber/v2/middleware/proxy"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/samber/lo" "github.com/samber/lo"
"github.com/spf13/viper" "github.com/spf13/viper"
"github.com/valyala/fasthttp" "github.com/valyala/fasthttp"
"math/rand"
"regexp" "regexp"
"strings"
"time" "time"
) )
func UseProxies(app *fiber.App) { func UseProxies(app *fiber.App) {
app.All("/", func(ctx *fiber.Ctx) error { app.All("/*", func(ctx *fiber.Ctx) error {
host := ctx.Hostname() host := ctx.Hostname()
path := ctx.Path() path := ctx.Path()
queries := ctx.Queries() queries := ctx.Queries()
headers := ctx.GetReqHeaders() headers := ctx.GetReqHeaders()
log.Debug().
Any("host", host).
Any("path", path).
Any("queries", queries).
Any("headers", headers).
Msg("A new request received")
// Filtering sites // Filtering sites
for _, site := range configurator.C.Sites { for _, site := range configurator.C.Sites {
// Matching rules // Matching rules
@ -96,45 +86,30 @@ func UseProxies(app *fiber.App) {
}) })
} }
func doLoadBalance(site configurator.SiteConfig) *configurator.UpstreamConfig {
idx := rand.Intn(len(site.Upstreams))
switch site.Upstreams[idx].GetType() {
case configurator.UpstreamTypeHypertext:
return &site.Upstreams[idx]
case configurator.UpstreamTypeFile:
// TODO Make this into hypertext configuration
return &site.Upstreams[idx]
default:
// Give him the null value when this configuration is invalid.
// Then we can print a log in the console to warm him.
return nil
}
}
func makeRequestUri(ctx *fiber.Ctx, upstream configurator.UpstreamConfig) string {
var queries []string
for k, v := range ctx.Queries() {
queries = append(queries, fmt.Sprintf("%s=%s", k, v))
}
hash := string(ctx.Request().URI().Hash())
return upstream.URI +
lo.Ternary(len(queries) > 0, "?"+strings.Join(queries, "&"), "") +
lo.Ternary(len(hash) > 0, "#"+hash, "")
}
func makeResponse(ctx *fiber.Ctx, site configurator.SiteConfig) error { func makeResponse(ctx *fiber.Ctx, site configurator.SiteConfig) error {
upstream := doLoadBalance(site) // Load balance
upstream := configurator.C.LoadBalance(site)
if upstream == nil { if upstream == nil {
log.Warn().Str("id", site.ID).Msg("There is no available upstream for this request.") log.Warn().Str("id", site.ID).Msg("There is no available upstream for this request.")
return fiber.ErrBadGateway return fiber.ErrBadGateway
} }
// Modify request
for _, transformer := range site.Transformers {
transformer.TransformRequest(ctx)
}
// Perform forward
timeout := time.Duration(viper.GetInt64("performance.network_timeout")) * time.Millisecond timeout := time.Duration(viper.GetInt64("performance.network_timeout")) * time.Millisecond
return proxy.Do(ctx, makeRequestUri(ctx, *upstream), &fasthttp.Client{ err := proxy.Do(ctx, upstream.MakeURI(ctx), &fasthttp.Client{
ReadTimeout: timeout, ReadTimeout: timeout,
WriteTimeout: timeout, WriteTimeout: timeout,
}) })
// Modify response
for _, transformer := range site.Transformers {
transformer.TransformResponse(ctx)
}
return err
} }

View File

@ -1,16 +1,23 @@
debug: debug:
print_routes: true print_routes: true
security:
administration_trusted_proxies: [ "localhost" ]
performance: performance:
prefork: false prefork: false
network_timeout: 3000 network_timeout: 3000
hypertext: hypertext:
ports: [ ":80" ] ports: [ ":80" ]
secured_ports: [] secured_ports: [ ]
administration_ports: [ ":81" ]
administration_secured_ports: [ ]
certificate: certificate:
pem: "./cert.pem" pem: "./cert.pem"
key: "./cert.key" key: "./cert.key"
administration_pem: "./cert.pem"
administration_key: "./cert.key"
limitation: limitation:
max_body_size: -1 max_body_size: -1
max_qps: 30 max_qps: 30