Matthew Holt
6 years ago
commit
859b5d7ea3
6 changed files with 360 additions and 0 deletions
@ -0,0 +1 @@ |
|||||
|
_gitignore/ |
@ -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{} |
||||
|
} |
@ -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 {} |
||||
|
} |
@ -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 |
||||
|
) |
@ -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"` |
||||
|
} |
@ -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) |
||||
|
} |
||||
|
} |
||||
|
} |
Loading…
Reference in new issue