✨ Transformers
This commit is contained in:
		
							
								
								
									
										6
									
								
								.idea/vcs.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.idea/vcs.xml
									
									
									
										generated
									
									
									
										Normal 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
									
								
							
							
						
						
									
										15
									
								
								README.md
									
									
									
									
									
										Normal 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? | ||||
| @@ -2,14 +2,27 @@ | ||||
|   "name": "Example Site", | ||||
|   "rules": [ | ||||
|     { | ||||
|       "host": ["localhost"], | ||||
|       "path": ["/"] | ||||
|       "host": [ | ||||
|         "localhost" | ||||
|       ], | ||||
|       "path": [ | ||||
|         "/" | ||||
|       ] | ||||
|     } | ||||
|   ], | ||||
|   "upstreams": [ | ||||
|     { | ||||
|       "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" | ||||
|       } | ||||
|     } | ||||
|   ] | ||||
| } | ||||
							
								
								
									
										20
									
								
								pkg/administration/init.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								pkg/administration/init.go
									
									
									
									
									
										Normal 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 | ||||
| } | ||||
| @@ -2,6 +2,7 @@ package main | ||||
|  | ||||
| import ( | ||||
| 	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/hypertext" | ||||
| 	"github.com/rs/zerolog" | ||||
| @@ -37,7 +38,22 @@ func main() { | ||||
| 	} | ||||
|  | ||||
| 	// 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) | ||||
|  | ||||
|   | ||||
							
								
								
									
										10
									
								
								pkg/configurator/deserializer.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								pkg/configurator/deserializer.go
									
									
									
									
									
										Normal 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 | ||||
| } | ||||
							
								
								
									
										41
									
								
								pkg/configurator/router.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								pkg/configurator/router.go
									
									
									
									
									
										Normal 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"` | ||||
| } | ||||
| @@ -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 | ||||
| } | ||||
							
								
								
									
										58
									
								
								pkg/configurator/transformer.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								pkg/configurator/transformer.go
									
									
									
									
									
										Normal 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)) | ||||
| 			} | ||||
| 		}, | ||||
| 	}, | ||||
| } | ||||
							
								
								
									
										46
									
								
								pkg/configurator/upstream.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								pkg/configurator/upstream.go
									
									
									
									
									
										Normal 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, "") | ||||
| } | ||||
| @@ -33,8 +33,8 @@ func InitServer() *fiber.App { | ||||
| 	return app | ||||
| } | ||||
|  | ||||
| func RunServer(app *fiber.App) { | ||||
| 	for _, port := range viper.GetStringSlice("hypertext.ports") { | ||||
| func RunServer(app *fiber.App, ports []string, securedPorts []string, pem string, key string) { | ||||
| 	for _, port := range ports { | ||||
| 		port := port | ||||
| 		go func() { | ||||
| 			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 | ||||
| 		pem := viper.GetString("hypertext.certificate.pem") | ||||
| 		key := viper.GetString("hypertext.certificate.key") | ||||
| 		go func() { | ||||
| 			if err := app.ListenTLS(port, pem, key); err != nil { | ||||
| 				log.Panic().Err(err).Msg("An error occurred when listening hypertext tls ports.") | ||||
|   | ||||
| @@ -2,33 +2,23 @@ package hypertext | ||||
|  | ||||
| import ( | ||||
| 	"code.smartsheep.studio/goatworks/roadsign/pkg/configurator" | ||||
| 	"fmt" | ||||
| 	"github.com/gofiber/fiber/v2" | ||||
| 	"github.com/gofiber/fiber/v2/middleware/proxy" | ||||
| 	"github.com/rs/zerolog/log" | ||||
| 	"github.com/samber/lo" | ||||
| 	"github.com/spf13/viper" | ||||
| 	"github.com/valyala/fasthttp" | ||||
| 	"math/rand" | ||||
| 	"regexp" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| func UseProxies(app *fiber.App) { | ||||
| 	app.All("/", func(ctx *fiber.Ctx) error { | ||||
| 	app.All("/*", func(ctx *fiber.Ctx) error { | ||||
| 		host := ctx.Hostname() | ||||
| 		path := ctx.Path() | ||||
| 		queries := ctx.Queries() | ||||
| 		headers := ctx.GetReqHeaders() | ||||
|  | ||||
| 		log.Debug(). | ||||
| 			Any("host", host). | ||||
| 			Any("path", path). | ||||
| 			Any("queries", queries). | ||||
| 			Any("headers", headers). | ||||
| 			Msg("A new request received") | ||||
|  | ||||
| 		// Filtering sites | ||||
| 		for _, site := range configurator.C.Sites { | ||||
| 			// 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 { | ||||
| 	upstream := doLoadBalance(site) | ||||
| 	// Load balance | ||||
| 	upstream := configurator.C.LoadBalance(site) | ||||
| 	if upstream == nil { | ||||
| 		log.Warn().Str("id", site.ID).Msg("There is no available upstream for this request.") | ||||
| 		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 | ||||
| 	return proxy.Do(ctx, makeRequestUri(ctx, *upstream), &fasthttp.Client{ | ||||
| 	err := proxy.Do(ctx, upstream.MakeURI(ctx), &fasthttp.Client{ | ||||
| 		ReadTimeout:  timeout, | ||||
| 		WriteTimeout: timeout, | ||||
| 	}) | ||||
|  | ||||
| 	// Modify response | ||||
| 	for _, transformer := range site.Transformers { | ||||
| 		transformer.TransformResponse(ctx) | ||||
| 	} | ||||
|  | ||||
| 	return err | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,9 @@ | ||||
| debug: | ||||
|   print_routes: true | ||||
|  | ||||
| security: | ||||
|   administration_trusted_proxies: [ "localhost" ] | ||||
|  | ||||
| performance: | ||||
|   prefork: false | ||||
|   network_timeout: 3000 | ||||
| @@ -8,9 +11,13 @@ performance: | ||||
| hypertext: | ||||
|   ports: [ ":80" ] | ||||
|   secured_ports: [ ] | ||||
|   administration_ports: [ ":81" ] | ||||
|   administration_secured_ports: [ ] | ||||
|   certificate: | ||||
|     pem: "./cert.pem" | ||||
|     key: "./cert.key" | ||||
|     administration_pem: "./cert.pem" | ||||
|     administration_key: "./cert.key" | ||||
|   limitation: | ||||
|     max_body_size: -1 | ||||
|     max_qps: 30 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user