diff --git a/cmd/flatend/config.go b/cmd/flatend/config.go new file mode 100644 index 0000000..95a3e2a --- /dev/null +++ b/cmd/flatend/config.go @@ -0,0 +1,135 @@ +package main + +import ( + "errors" + "fmt" + "net/http" + "net/url" + "strings" + "time" +) + +type Config struct { + HTTP []ConfigHTTP +} + +func (c Config) Validate() error { + for _, srv := range c.HTTP { + err := srv.Validate() + if err != nil { + return err + } + } + + return nil +} + +type Duration struct { + time.Duration +} + +func (d *Duration) UnmarshalText(text []byte) error { + var err error + d.Duration, err = time.ParseDuration(string(text)) + return err +} + +type ConfigHTTP struct { + Addr string + Addrs []string + + RedirectTrailingSlash *bool `toml:"redirect_trailing_slash"` + RedirectFixedPath *bool `toml:"redirect_fixed_path"` + + Timeout struct { + Read Duration + ReadHeader Duration + Idle Duration + Write Duration + Shutdown Duration + } + + Min struct { + BodySize *int `toml:"body_size"` + } + + Max struct { + HeaderSize int `toml:"header_size"` + BodySize *int `toml:"body_size"` + } + + Routes []ConfigRoute +} + +func (h ConfigHTTP) GetAddrs() []string { + if h.Addr != "" { + return []string{h.Addr} + } + return h.Addrs +} + +func (h ConfigHTTP) Validate() error { + if h.Addr != "" && h.Addrs != nil { + return errors.New("'addr' and 'addrs' cannot both be non-nil at the same time") + } + + for _, route := range h.Routes { + err := route.Validate() + if err != nil { + return err + } + } + + return nil +} + +type ConfigRoute struct { + Path string + Dispatch string + Static string // static files + Service string + Services []string + + NoCache bool + + Min struct { + BodySize *int `toml:"body_size"` + } + + Max struct { + BodySize *int `toml:"body_size"` + } +} + +func (r ConfigRoute) Validate() error { + if r.Service != "" && r.Services != nil { + return errors.New("'service' and 'services' cannot both be non-nil at the same time") + } + + fields := strings.Fields(r.Path) + if len(fields) != 2 { + return fmt.Errorf("invalid number of fields in route path '%s' (format: 'HTTP_METHOD /path/here')", + r.Path) + } + + method := strings.ToUpper(fields[0]) + _, exists := Methods[method] + if !exists { + return fmt.Errorf("unknown http method '%s'", method) + } + + if len(fields[1]) < 1 || fields[1][0] != '/' { + return fmt.Errorf("path must begin with '/' in path '%s'", fields[1]) + } + + _, err := url.ParseRequestURI(fields[1]) + if err != nil { + return fmt.Errorf("invalid http path '%s': %w", fields[1], err) + } + + if r.Static != "" && method != http.MethodGet { + return fmt.Errorf("path '%s' method must be 'GET' to serve files", r.Path) + } + + return nil +} diff --git a/cmd/flatend/handlers.go b/cmd/flatend/handlers.go new file mode 100644 index 0000000..379119d --- /dev/null +++ b/cmd/flatend/handlers.go @@ -0,0 +1,39 @@ +package main + +import ( + "github.com/julienschmidt/httprouter" + "github.com/lithdew/flatend" + "io" + "net/http" + "strings" +) + +func HandleService(node *flatend.Node, services []string) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + headers := make(map[string]string) + for key := range r.Header { + headers[strings.ToLower(key)] = r.Header.Get(key) + } + + for key := range r.URL.Query() { + headers["query."+strings.ToLower(key)] = r.URL.Query().Get(key) + } + + params := httprouter.ParamsFromContext(r.Context()) + for _, param := range params { + headers["params."+strings.ToLower(param.Key)] = param.Value + } + + stream, err := node.Push(services, headers, r.Body) + if err != nil { + w.Write([]byte(err.Error())) + return + } + + for name, val := range stream.Header.Headers { + w.Header().Set(name, val) + } + + _, _ = io.Copy(w, stream.Reader) + }) +} diff --git a/cmd/flatend/main.go b/cmd/flatend/main.go index 0435ab6..0a94403 100644 --- a/cmd/flatend/main.go +++ b/cmd/flatend/main.go @@ -8,137 +8,17 @@ import ( "github.com/julienschmidt/httprouter" "github.com/lithdew/flatend" "github.com/spf13/pflag" - "io" "io/ioutil" "net" "net/http" - "net/url" "os" "os/signal" + "path/filepath" "strings" - "time" ) var json = jsoniter.ConfigCompatibleWithStandardLibrary -type Config struct { - HTTP []ConfigHTTP -} - -func (c Config) Validate() error { - for _, srv := range c.HTTP { - err := srv.Validate() - if err != nil { - return err - } - } - - return nil -} - -type Duration struct { - time.Duration -} - -func (d *Duration) UnmarshalText(text []byte) error { - var err error - d.Duration, err = time.ParseDuration(string(text)) - return err -} - -type ConfigHTTP struct { - Addr string - Addrs []string - - RedirectTrailingSlash *bool `toml:"redirect_trailing_slash"` - RedirectFixedPath *bool `toml:"redirect_fixed_path"` - - Timeout struct { - Read Duration - ReadHeader Duration - Idle Duration - Write Duration - Shutdown Duration - } - - Min struct { - BodySize *int `toml:"body_size"` - } - - Max struct { - HeaderSize int `toml:"header_size"` - BodySize *int `toml:"body_size"` - } - - Routes []ConfigRoute -} - -func (h ConfigHTTP) GetAddrs() []string { - if h.Addr != "" { - return []string{h.Addr} - } - return h.Addrs -} - -func (h ConfigHTTP) Validate() error { - if h.Addr != "" && h.Addrs != nil { - return errors.New("'addr' and 'addrs' cannot both be non-nil at the same time") - } - - for _, route := range h.Routes { - err := route.Validate() - if err != nil { - return err - } - } - - return nil -} - -type ConfigRoute struct { - Path string - Dispatch string - Service string - Services []string - - Min struct { - BodySize *int `toml:"body_size"` - } - - Max struct { - BodySize *int `toml:"body_size"` - } -} - -func (r ConfigRoute) Validate() error { - if r.Service != "" && r.Services != nil { - return errors.New("'service' and 'services' cannot both be non-nil at the same time") - } - - fields := strings.Fields(r.Path) - if len(fields) != 2 { - return fmt.Errorf("invalid number of fields in route path '%s' (format: 'HTTP_METHOD /path/here')", - r.Path) - } - - method := strings.ToUpper(fields[0]) - _, exists := Methods[method] - if !exists { - return fmt.Errorf("unknown http method '%s'", method) - } - - if len(fields[1]) < 1 || fields[1][0] != '/' { - return fmt.Errorf("path must begin with '/' in path '%s'", fields[1]) - } - - _, err := url.ParseRequestURI(fields[1]) - if err != nil { - return fmt.Errorf("invalid http path '%s': %w", fields[1], err) - } - - return nil -} - func check(err error) { if err != nil { panic(err) @@ -193,39 +73,44 @@ func main() { for _, route := range cfg.Routes { fields := strings.Fields(route.Path) - services := route.Services - if route.Service != "" { - services = append(services, route.Service) - } - - handler := func(w http.ResponseWriter, r *http.Request, params httprouter.Params) { - headers := make(map[string]string) - for key := range r.Header { - headers[strings.ToLower(key)] = r.Header.Get(key) - } + var handler http.Handler - for key := range r.URL.Query() { - headers["query."+strings.ToLower(key)] = r.URL.Query().Get(key) - } + switch { + case route.Static != "": + static := route.Static - for _, param := range params { - headers["params."+strings.ToLower(param.Key)] = param.Value - } + info, err := os.Lstat(static) + check(err) - stream, err := node.Push(services, headers, r.Body) - if err != nil { - w.Write([]byte(err.Error())) - return + if info.IsDir() { + if fields[1] == "/" { + router.NotFound = http.FileServer(http.Dir(static)) + } else { + if info.IsDir() && !strings.HasPrefix(fields[1], "/*filepath") { + fields[1] = filepath.Join(fields[1], "/*filepath") + } + router.ServeFiles(fields[1], http.Dir(static)) + } + } else { + handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, static) + }) } - - for name, val := range stream.Header.Headers { - w.Header().Set(name, val) + case route.Service != "" || len(route.Services) > 0: + services := route.Services + if route.Service != "" { + services = append(services, route.Service) } - _, _ = io.Copy(w, stream.Reader) + handler = HandleService(node, services) } - router.Handle(fields[0], fields[1], handler) + if handler != nil { + if route.NoCache { + handler = NoCache(handler) + } + router.Handler(fields[0], fields[1], handler) + } } srv := &http.Server{ diff --git a/cmd/flatend/middleware.go b/cmd/flatend/middleware.go new file mode 100644 index 0000000..4905384 --- /dev/null +++ b/cmd/flatend/middleware.go @@ -0,0 +1,42 @@ +package main + +import ( + "net/http" + "time" +) + +var epoch = time.Unix(0, 0).Format(http.TimeFormat) + +var noCacheHeaders = map[string]string{ + "Expires": epoch, + "Cache-Control": "no-cache, private, max-age=0", + "Pragma": "no-cache", + "X-Accel-Expires": "0", +} + +var etagHeaders = []string{ + "ETag", + "If-Modified-Since", + "If-Match", + "If-None-Match", + "If-Range", + "If-Unmodified-Since", +} + +func NoCache(h http.Handler) http.Handler { + fn := func(w http.ResponseWriter, r *http.Request) { + for _, v := range etagHeaders { + if r.Header.Get(v) != "" { + r.Header.Del(v) + } + } + + for k, v := range noCacheHeaders { + w.Header().Set(k, v) + } + + h.ServeHTTP(w, r) + } + + return http.HandlerFunc(fn) +} diff --git a/config.toml b/config.toml index b7c5914..9f81538 100644 --- a/config.toml +++ b/config.toml @@ -13,6 +13,11 @@ write = "10s" header_size = 1048576 body_size = 1048576 +[[http.routes]] +path = "GET /" +static = "." +nocache = true + [[http.routes]] path = "GET /todos" service = "all_todos" diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index 1a81d0b..acadeeb 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -5,7 +5,7 @@ async function main() { const node = new Node(); node.register("get_todos", (ctx: Context) => ctx.json(ctx.headers)); - node.register("all_todos", (ctx: Context) => fs.createReadStream("package.json").pipe(ctx.header('content-type', 'application/json'))); + node.register("all_todos", (ctx: Context) => fs.createReadStream("./nodejs/package.json").pipe(ctx)); node.register('pipe', async (ctx: Context) => { const body = await ctx.body({limit: 65536});