From 859b5d7ea3b8f660ac68d9aea5a53d25a9a7422c Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Tue, 26 Mar 2019 12:00:54 -0600 Subject: [PATCH] Initial commit --- .gitignore | 1 + admin.go | 130 +++++++++++++++++++++++++++++++++ cmd/caddy2/main.go | 21 ++++++ modules.go | 110 ++++++++++++++++++++++++++++ modules/caddyhttp/caddyhttp.go | 27 +++++++ modules_test.go | 71 ++++++++++++++++++ 6 files changed, 360 insertions(+) create mode 100644 .gitignore create mode 100644 admin.go create mode 100644 cmd/caddy2/main.go create mode 100644 modules.go create mode 100644 modules/caddyhttp/caddyhttp.go create mode 100644 modules_test.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..e755a6fec --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +_gitignore/ \ No newline at end of file diff --git a/admin.go b/admin.go new file mode 100644 index 000000000..06727458f --- /dev/null +++ b/admin.go @@ -0,0 +1,130 @@ +package caddy2 + +import ( + "context" + "encoding/json" + "fmt" + "io" + "log" + "net" + "net/http" + "strings" + "sync" +) + +var ( + cfgEndptSrv *http.Server + cfgEndptSrvMu sync.Mutex +) + +// Start starts Caddy's administration endpoint. +func Start(addr string) error { + cfgEndptSrvMu.Lock() + defer cfgEndptSrvMu.Unlock() + + ln, err := net.Listen("tcp", addr) + if err != nil { + return err + } + + mux := http.NewServeMux() + mux.HandleFunc("/load", handleLoadConfig) + + for _, m := range GetModules("admin") { + moduleValue, err := m.New() + if err != nil { + return fmt.Errorf("initializing module '%s': %v", m.Name, err) + } + route := moduleValue.(AdminRoute) + mux.Handle(route.Pattern, route) + } + + cfgEndptSrv = &http.Server{ + Handler: mux, + } + + go cfgEndptSrv.Serve(ln) + + return nil +} + +// AdminRoute represents a route for the admin endpoint. +type AdminRoute struct { + http.Handler + Pattern string +} + +// Stop stops the API endpoint. +func Stop() error { + cfgEndptSrvMu.Lock() + defer cfgEndptSrvMu.Unlock() + + if cfgEndptSrv == nil { + return fmt.Errorf("no server") + } + + err := cfgEndptSrv.Shutdown(context.Background()) // TODO + if err != nil { + return fmt.Errorf("shutting down server: %v", err) + } + + cfgEndptSrv = nil + + return nil +} + +func handleLoadConfig(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return + } + + if !strings.Contains(r.Header.Get("Content-Type"), "/json") { + http.Error(w, "unacceptable Content-Type", http.StatusBadRequest) + return + } + + err := Load(r.Body) + if err != nil { + log.Printf("[ADMIN][ERROR] loading config: %v", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } +} + +// Load loads a configuration. +func Load(r io.Reader) error { + gc := globalConfig{modules: make(map[string]interface{})} + err := json.NewDecoder(r).Decode(&gc) + if err != nil { + return fmt.Errorf("decoding config: %v", err) + } + + for modName, rawMsg := range gc.Modules { + mod, ok := modules[modName] + if !ok { + return fmt.Errorf("unrecognized module: %s", modName) + } + + if mod.New != nil { + val, err := mod.New() + if err != nil { + return fmt.Errorf("initializing module '%s': %v", modName, err) + } + err = json.Unmarshal(rawMsg, &val) + if err != nil { + return fmt.Errorf("decoding module config: %s: %v", modName, err) + } + gc.modules[modName] = val + } + } + + return nil +} + +type globalConfig struct { + TestVal string `json:"testval"` + Modules map[string]json.RawMessage `json:"modules"` + TestArr []string `json:"test_arr"` + modules map[string]interface{} +} diff --git a/cmd/caddy2/main.go b/cmd/caddy2/main.go new file mode 100644 index 000000000..9e482c149 --- /dev/null +++ b/cmd/caddy2/main.go @@ -0,0 +1,21 @@ +package main + +import ( + "log" + + "bitbucket.org/lightcodelabs/caddy2" + + // this is where modules get plugged in + _ "bitbucket.org/lightcodelabs/caddy2/modules/caddyhttp" + _ "bitbucket.org/lightcodelabs/dynamicconfig" +) + +func main() { + err := caddy2.Start("127.0.0.1:1234") + if err != nil { + log.Fatal(err) + } + defer caddy2.Stop() + + select {} +} diff --git a/modules.go b/modules.go new file mode 100644 index 000000000..1c3e231c1 --- /dev/null +++ b/modules.go @@ -0,0 +1,110 @@ +package caddy2 + +import ( + "fmt" + "sort" + "strings" + "sync" +) + +// Module is a module. +type Module struct { + Name string + New func() (interface{}, error) +} + +func (m Module) String() string { return m.Name } + +// RegisterModule registers a module. +func RegisterModule(mod Module) error { + modulesMu.Lock() + defer modulesMu.Unlock() + + if _, ok := modules[mod.Name]; ok { + return fmt.Errorf("module already registered: %s", mod.Name) + } + modules[mod.Name] = mod + return nil +} + +// GetModule returns a module by name. +func GetModule(name string) (Module, error) { + modulesMu.Lock() + defer modulesMu.Unlock() + + m, ok := modules[name] + if !ok { + return Module{}, fmt.Errorf("module not registered: %s", name) + } + return m, nil +} + +// GetModules returns all modules in the given scope/namespace. +// For example, a scope of "foo" returns modules named "foo.bar", +// "foo.lor", but not "bar", "foo.bar.lor", etc. An empty scope +// returns top-level modules, for example "foo" or "bar". Partial +// scopes are not matched (i.e. scope "foo.ba" does not match +// name "foo.bar"). +// +// Because modules are registered to a map, the returned slice +// will be sorted to keep it deterministic. +func GetModules(scope string) []Module { + modulesMu.Lock() + defer modulesMu.Unlock() + + scopeParts := strings.Split(scope, ".") + + // handle the special case of an empty scope, which + // should match only the top-level modules + if len(scopeParts) == 1 && scopeParts[0] == "" { + scopeParts = []string{} + } + + var mods []Module +iterateModules: + for name, m := range modules { + modParts := strings.Split(name, ".") + + // match only the next level of nesting + if len(modParts) != len(scopeParts)+1 { + continue + } + + // specified parts must be exact matches + for i := range scopeParts { + if modParts[i] != scopeParts[i] { + continue iterateModules + } + } + + mods = append(mods, m) + } + + // make return value deterministic + sort.Slice(mods, func(i, j int) bool { + return mods[i].Name < mods[j].Name + }) + + return mods +} + +// Modules returns the names of all registered modules +// in ascending lexicographical order. +func Modules() []string { + modulesMu.Lock() + defer modulesMu.Unlock() + + var names []string + for name := range modules { + names = append(names, name) + } + + sort.Strings(names) + + return names +} + +var ( + modules = make(map[string]Module) + modulesMu sync.Mutex +) diff --git a/modules/caddyhttp/caddyhttp.go b/modules/caddyhttp/caddyhttp.go new file mode 100644 index 000000000..296e28fdc --- /dev/null +++ b/modules/caddyhttp/caddyhttp.go @@ -0,0 +1,27 @@ +package caddyhttp + +import ( + "log" + + "bitbucket.org/lightcodelabs/caddy2" +) + +func init() { + err := caddy2.RegisterModule(caddy2.Module{ + Name: "http", + New: func() (interface{}, error) { return httpModuleConfig{}, nil }, + }) + if err != nil { + log.Fatal(err) + } +} + +type httpModuleConfig struct { + Servers map[string]httpServerConfig `json:"servers"` +} + +type httpServerConfig struct { + Listen []string `json:"listen"` + ReadTimeout string `json:"read_timeout"` + ReadHeaderTimeout string `json:"read_header_timeout"` +} diff --git a/modules_test.go b/modules_test.go new file mode 100644 index 000000000..ff2ff4eb2 --- /dev/null +++ b/modules_test.go @@ -0,0 +1,71 @@ +package caddy2 + +import ( + "reflect" + "testing" +) + +func TestGetModules(t *testing.T) { + modulesMu.Lock() + modules = map[string]Module{ + "a": {Name: "a"}, + "a.b": {Name: "a.b"}, + "a.b.c": {Name: "a.b.c"}, + "a.b.cd": {Name: "a.b.cd"}, + "a.c": {Name: "a.c"}, + "a.d": {Name: "a.d"}, + "b": {Name: "b"}, + "b.a": {Name: "b.a"}, + "b.b": {Name: "b.b"}, + "b.a.c": {Name: "b.a.c"}, + "c": {Name: "c"}, + } + modulesMu.Unlock() + + for i, tc := range []struct { + input string + expect []Module + }{ + { + input: "", + expect: []Module{ + {Name: "a"}, + {Name: "b"}, + {Name: "c"}, + }, + }, + { + input: "a", + expect: []Module{ + {Name: "a.b"}, + {Name: "a.c"}, + {Name: "a.d"}, + }, + }, + { + input: "a.b", + expect: []Module{ + {Name: "a.b.c"}, + {Name: "a.b.cd"}, + }, + }, + { + input: "a.b.c", + }, + { + input: "b", + expect: []Module{ + {Name: "b.a"}, + {Name: "b.b"}, + }, + }, + { + input: "asdf", + }, + } { + actual := GetModules(tc.input) + if !reflect.DeepEqual(actual, tc.expect) { + t.Errorf("Test %d: Expected %v but got %v", i, tc.expect, actual) + } + } +}