summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMatthew Holt <mholt@users.noreply.github.com>2019-03-26 12:00:54 -0600
committerMatthew Holt <mholt@users.noreply.github.com>2019-03-26 12:00:54 -0600
commit859b5d7ea3b8f660ac68d9aea5a53d25a9a7422c (patch)
treec5baf11ff459d8811104b00c07b0a3c24bd16d9c
Initial commit
-rw-r--r--.gitignore1
-rw-r--r--admin.go130
-rw-r--r--cmd/caddy2/main.go21
-rw-r--r--modules.go110
-rw-r--r--modules/caddyhttp/caddyhttp.go27
-rw-r--r--modules_test.go71
6 files changed, 360 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e755a6f
--- /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 0000000..0672745
--- /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 0000000..9e482c1
--- /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 0000000..1c3e231
--- /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 0000000..296e28f
--- /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 0000000..ff2ff4e
--- /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)
+ }
+ }
+}