✨ Static Files Hosting
This commit is contained in:
		| @@ -6,10 +6,12 @@ A blazing fast reverse proxy with a lot of shining features. | ||||
|  | ||||
| 1. Reverse proxy | ||||
| 2. Static file hosting | ||||
| 3. Analytics and Metrics | ||||
| 3. ~~Analytics and Metrics~~ | ||||
| 4. Integrate with CI/CD | ||||
| 5. Webhook integration | ||||
| 6. Web management panel | ||||
| 5. ~~Webhook integration~~ | ||||
| 6. ~~Web management panel~~ | ||||
| 7. **Blazing fast ⚡** | ||||
|  | ||||
| > Deleted item means under construction, check out our roadmap! | ||||
|  | ||||
| ### How fast is it? | ||||
| @@ -6,22 +6,22 @@ | ||||
|         "localhost" | ||||
|       ], | ||||
|       "path": [ | ||||
|         "/" | ||||
|         "/site1" | ||||
|       ] | ||||
|     } | ||||
|   ], | ||||
|   "upstreams": [ | ||||
|     { | ||||
|       "name": "Example Upstream", | ||||
|       "uri": "https://disk.smartsheep.studio" | ||||
|       "uri": "file:///site" | ||||
|     } | ||||
|   ], | ||||
|   "transformers": [ | ||||
|     { | ||||
|       "type": "replacePath", | ||||
|       "options": { | ||||
|         "pattern": "/p/123", | ||||
|         "value": "/p/%E5%B7%A5%E4%BD%9C%E5%AE%A4/icon.png" | ||||
|         "pattern": "/site1", | ||||
|         "value": "/" | ||||
|       } | ||||
|     } | ||||
|   ] | ||||
|   | ||||
| @@ -3,8 +3,8 @@ 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" | ||||
| 	"code.smartsheep.studio/goatworks/roadsign/pkg/sign" | ||||
| 	"github.com/rs/zerolog" | ||||
| 	"github.com/rs/zerolog/log" | ||||
| 	"github.com/spf13/viper" | ||||
| @@ -30,11 +30,11 @@ func main() { | ||||
| 		log.Panic().Err(err).Msg("An error occurred when loading settings.") | ||||
| 	} | ||||
|  | ||||
| 	// Load configurations | ||||
| 	if err := configurator.ReadInConfig(viper.GetString("paths.configs")); err != nil { | ||||
| 	// Load & init sign | ||||
| 	if err := sign.ReadInConfig(viper.GetString("paths.configs")); err != nil { | ||||
| 		log.Panic().Err(err).Msg("An error occurred when loading configurations.") | ||||
| 	} else { | ||||
| 		log.Debug().Any("sites", configurator.C).Msg("All configuration has been loaded.") | ||||
| 		log.Debug().Any("sites", sign.C).Msg("All configuration has been loaded.") | ||||
| 	} | ||||
|  | ||||
| 	// Init hypertext server | ||||
|   | ||||
| @@ -1,41 +0,0 @@ | ||||
| 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,15 +1,10 @@ | ||||
| package hypertext | ||||
|  | ||||
| import ( | ||||
| 	"code.smartsheep.studio/goatworks/roadsign/pkg/configurator" | ||||
| 	"code.smartsheep.studio/goatworks/roadsign/pkg/sign" | ||||
| 	"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" | ||||
| 	"regexp" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| func UseProxies(app *fiber.App) { | ||||
| @@ -20,21 +15,28 @@ func UseProxies(app *fiber.App) { | ||||
| 		headers := ctx.GetReqHeaders() | ||||
|  | ||||
| 		// Filtering sites | ||||
| 		for _, site := range configurator.C.Sites { | ||||
| 		for _, site := range sign.C.Sites { | ||||
| 			// Matching rules | ||||
| 			for _, rule := range site.Rules { | ||||
| 				if !lo.Contains(rule.Host, host) { | ||||
| 					continue | ||||
| 				} else if !lo.ContainsBy(rule.Path, func(item string) bool { | ||||
| 					matched, err := regexp.MatchString(item, path) | ||||
| 					return matched && err == nil | ||||
| 				}) { | ||||
| 				} | ||||
|  | ||||
| 				if !func() bool { | ||||
| 					flag := false | ||||
| 					for _, pattern := range rule.Path { | ||||
| 						if ok, _ := regexp.MatchString(pattern, path); ok { | ||||
| 							flag = true | ||||
| 							break | ||||
| 						} | ||||
| 					} | ||||
| 					return flag | ||||
| 				}() { | ||||
| 					continue | ||||
| 				} | ||||
|  | ||||
| 				flag := true | ||||
|  | ||||
| 				// Filter query strings | ||||
| 				flag := true | ||||
| 				for rk, rv := range rule.Queries { | ||||
| 					for ik, iv := range queries { | ||||
| 						if rk != ik && rv != iv { | ||||
| @@ -72,11 +74,11 @@ func UseProxies(app *fiber.App) { | ||||
| 				if !flag { | ||||
| 					continue | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			// Passing all the rules means the site is what we are looking for. | ||||
| 			// Let us respond to our client! | ||||
| 			return makeResponse(ctx, site) | ||||
| 				// Passing all the rules means the site is what we are looking for. | ||||
| 				// Let us respond to our client! | ||||
| 				return makeResponse(ctx, site) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		// There is no site available for this request. | ||||
| @@ -86,25 +88,14 @@ func UseProxies(app *fiber.App) { | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func makeResponse(ctx *fiber.Ctx, site configurator.SiteConfig) error { | ||||
| 	// 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 | ||||
| 	} | ||||
|  | ||||
| func makeResponse(ctx *fiber.Ctx, site sign.SiteConfig) error { | ||||
| 	// Modify request | ||||
| 	for _, transformer := range site.Transformers { | ||||
| 		transformer.TransformRequest(ctx) | ||||
| 	} | ||||
|  | ||||
| 	// Perform forward | ||||
| 	timeout := time.Duration(viper.GetInt64("performance.network_timeout")) * time.Millisecond | ||||
| 	err := proxy.Do(ctx, upstream.MakeURI(ctx), &fasthttp.Client{ | ||||
| 		ReadTimeout:  timeout, | ||||
| 		WriteTimeout: timeout, | ||||
| 	}) | ||||
| 	// Forward | ||||
| 	err := sign.C.Forward(ctx, site) | ||||
|  | ||||
| 	// Modify response | ||||
| 	for _, transformer := range site.Transformers { | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| package configurator | ||||
| package sign | ||||
| 
 | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| @@ -1,4 +1,4 @@ | ||||
| package configurator | ||||
| package sign | ||||
| 
 | ||||
| import "encoding/json" | ||||
| 
 | ||||
							
								
								
									
										78
									
								
								pkg/sign/router.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								pkg/sign/router.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,78 @@ | ||||
| package sign | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"github.com/gofiber/fiber/v2" | ||||
| 	"github.com/gofiber/fiber/v2/middleware/filesystem" | ||||
| 	"github.com/gofiber/fiber/v2/middleware/proxy" | ||||
| 	"github.com/samber/lo" | ||||
| 	"github.com/spf13/viper" | ||||
| 	"github.com/valyala/fasthttp" | ||||
| 	"math/rand" | ||||
| 	"net/http" | ||||
| 	"strconv" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| type AppConfig struct { | ||||
| 	Sites []SiteConfig `json:"sites"` | ||||
| } | ||||
|  | ||||
| func (v *AppConfig) Forward(ctx *fiber.Ctx, site SiteConfig) error { | ||||
| 	if len(site.Upstreams) == 0 { | ||||
| 		return errors.New("invalid configuration") | ||||
| 	} | ||||
|  | ||||
| 	idx := rand.Intn(len(site.Upstreams)) | ||||
| 	upstream := &site.Upstreams[idx] | ||||
|  | ||||
| 	switch upstream.GetType() { | ||||
| 	case UpstreamTypeHypertext: | ||||
| 		timeout := time.Duration(viper.GetInt64("performance.network_timeout")) * time.Millisecond | ||||
| 		return proxy.Do(ctx, upstream.MakeURI(ctx), &fasthttp.Client{ | ||||
| 			ReadTimeout:  timeout, | ||||
| 			WriteTimeout: timeout, | ||||
| 		}) | ||||
| 	case UpstreamTypeFile: | ||||
| 		uri, queries := upstream.GetRawURI() | ||||
| 		fs := filesystem.New(filesystem.Config{ | ||||
| 			Root:               http.Dir(uri), | ||||
| 			ContentTypeCharset: queries.Get("charset"), | ||||
| 			Index: func() string { | ||||
| 				val := queries.Get("index") | ||||
| 				return lo.Ternary(len(val) > 0, val, "index.html") | ||||
| 			}(), | ||||
| 			NotFoundFile: func() string { | ||||
| 				val := queries.Get("fallback") | ||||
| 				return lo.Ternary(len(val) > 0, val, "404.html") | ||||
| 			}(), | ||||
| 			Browse: func() bool { | ||||
| 				browse, err := strconv.ParseBool(queries.Get("browse")) | ||||
| 				return lo.Ternary(err == nil, browse, false) | ||||
| 			}(), | ||||
| 			MaxAge: func() int { | ||||
| 				age, err := strconv.Atoi(queries.Get("maxAge")) | ||||
| 				return lo.Ternary(err == nil, age, 3600) | ||||
| 			}(), | ||||
| 		}) | ||||
| 		return fs(ctx) | ||||
| 	default: | ||||
| 		return fiber.ErrBadGateway | ||||
| 	} | ||||
| } | ||||
|  | ||||
| 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,4 +1,4 @@ | ||||
| package configurator | ||||
| package sign | ||||
| 
 | ||||
| import ( | ||||
| 	"github.com/gofiber/fiber/v2" | ||||
| @@ -1,9 +1,10 @@ | ||||
| package configurator | ||||
| package sign | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"github.com/gofiber/fiber/v2" | ||||
| 	"github.com/samber/lo" | ||||
| 	"net/url" | ||||
| 	"strings" | ||||
| ) | ||||
| 
 | ||||
| @@ -31,6 +32,14 @@ func (v *UpstreamConfig) GetType() string { | ||||
| 	return UpstreamTypeUnknown | ||||
| } | ||||
| 
 | ||||
| func (v *UpstreamConfig) GetRawURI() (string, url.Values) { | ||||
| 	uri := strings.SplitN(v.URI, "://", 2)[1] | ||||
| 	data := strings.SplitN(uri, "?", 2) | ||||
| 	qs, _ := url.ParseQuery(uri) | ||||
| 
 | ||||
| 	return data[0], qs | ||||
| } | ||||
| 
 | ||||
| func (v *UpstreamConfig) MakeURI(ctx *fiber.Ctx) string { | ||||
| 	var queries []string | ||||
| 	for k, v := range ctx.Queries() { | ||||
		Reference in New Issue
	
	Block a user