diff --git a/admin.go b/admin.go index 06727458..2841ed85 100644 --- a/admin.go +++ b/admin.go @@ -17,8 +17,8 @@ var ( cfgEndptSrvMu sync.Mutex ) -// Start starts Caddy's administration endpoint. -func Start(addr string) error { +// StartAdmin starts Caddy's administration endpoint. +func StartAdmin(addr string) error { cfgEndptSrvMu.Lock() defer cfgEndptSrvMu.Unlock() @@ -48,14 +48,8 @@ func Start(addr string) error { 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 { +// StopAdmin stops the API endpoint. +func StopAdmin() error { cfgEndptSrvMu.Lock() defer cfgEndptSrvMu.Unlock() @@ -73,6 +67,12 @@ func Stop() error { return nil } +// AdminRoute represents a route for the admin endpoint. +type AdminRoute struct { + http.Handler + Pattern string +} + func handleLoadConfig(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) @@ -92,39 +92,16 @@ func handleLoadConfig(w http.ResponseWriter, r *http.Request) { } } -// Load loads a configuration. +// Load loads and starts a configuration. func Load(r io.Reader) error { - gc := globalConfig{modules: make(map[string]interface{})} - err := json.NewDecoder(r).Decode(&gc) + var cfg Config + err := json.NewDecoder(r).Decode(&cfg) 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 - } + err = Start(cfg) + if err != nil { + return fmt.Errorf("starting: %v", err) } - 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/caddy.go b/caddy.go new file mode 100644 index 00000000..2f12776b --- /dev/null +++ b/caddy.go @@ -0,0 +1,62 @@ +package caddy2 + +import ( + "encoding/json" + "fmt" + "strings" + "time" +) + +// Start runs Caddy with the given config. +func Start(cfg Config) error { + cfg.runners = make(map[string]Runner) + + for modName, rawMsg := range cfg.Modules { + mod, ok := modules[modName] + if !ok { + return fmt.Errorf("unrecognized module: %s", modName) + } + val, err := LoadModule(mod, rawMsg) + if err != nil { + return fmt.Errorf("loading module '%s': %v", modName, err) + } + cfg.runners[modName] = val.(Runner) + } + + for name, r := range cfg.runners { + err := r.Run() + if err != nil { + return fmt.Errorf("%s module: %v", name, err) + } + } + + return nil +} + +// Runner is a thing that Caddy runs. +type Runner interface { + Run() error +} + +// Config represents a Caddy configuration. +type Config struct { + TestVal string `json:"testval"` + Modules map[string]json.RawMessage `json:"modules"` + runners map[string]Runner +} + +// Duration is a JSON-string-unmarshable duration type. +type Duration time.Duration + +// UnmarshalJSON satisfies json.Unmarshaler. +func (d *Duration) UnmarshalJSON(b []byte) (err error) { + dd, err := time.ParseDuration(strings.Trim(string(b), `"`)) + cd := Duration(dd) + d = &cd + return +} + +// MarshalJSON satisfies json.Marshaler. +func (d Duration) MarshalJSON() (b []byte, err error) { + return []byte(fmt.Sprintf(`"%s"`, time.Duration(d).String())), nil +} diff --git a/listeners.go b/listeners.go new file mode 100644 index 00000000..962cb1d2 --- /dev/null +++ b/listeners.go @@ -0,0 +1,51 @@ +package caddy2 + +import ( + "fmt" + "net" + "sync/atomic" +) + +// Listen returns a listener suitable for use in a Caddy module. +func Listen(proto, addr string) (net.Listener, error) { + ln, err := net.Listen(proto, addr) + if err != nil { + return nil, err + } + return &fakeCloseListener{Listener: ln}, nil +} + +// fakeCloseListener's Close() method is a no-op. This allows +// stopping servers that are using the listener without giving +// up the socket; thus, servers become hot-swappable while the +// listener remains running. Listeners should be re-wrapped in +// a new fakeCloseListener each time the listener is reused. +type fakeCloseListener struct { + closed int32 + net.Listener +} + +// Accept accepts connections until Close() is called. +func (fcl *fakeCloseListener) Accept() (net.Conn, error) { + if atomic.LoadInt32(&fcl.closed) == 1 { + return nil, ErrSwappingServers + } + return fcl.Listener.Accept() +} + +// Close stops accepting new connections, but does not +// actually close the underlying listener. +func (fcl *fakeCloseListener) Close() error { + atomic.StoreInt32(&fcl.closed, 1) + return nil +} + +// CloseUnderlying actually closes the underlying listener. +func (fcl *fakeCloseListener) CloseUnderlying() error { + return fcl.Listener.Close() +} + +// ErrSwappingServers is returned by fakeCloseListener when +// Close() is called, indicating that it is pretending to +// be closed so that the server using it can terminate. +var ErrSwappingServers = fmt.Errorf("listener 'closed' 😉") diff --git a/modules.go b/modules.go index 1c3e231c..4a396d42 100644 --- a/modules.go +++ b/modules.go @@ -1,7 +1,9 @@ package caddy2 import ( + "encoding/json" "fmt" + "reflect" "sort" "strings" "sync" @@ -104,6 +106,34 @@ func Modules() []string { return names } +// LoadModule decodes rawMsg into a new instance of mod and +// returns the value. If mod.New() does not return a pointer +// value, it is converted to one so that it is unmarshaled +// into the underlying concrete type. If mod.New is nil, an +// error is returned. +func LoadModule(mod Module, rawMsg json.RawMessage) (interface{}, error) { + if mod.New == nil { + return nil, fmt.Errorf("no constructor") + } + + val, err := mod.New() + if err != nil { + return nil, fmt.Errorf("initializing module '%s': %v", mod.Name, err) + } + + // value must be a pointer for unmarshaling into concrete type + if rv := reflect.ValueOf(val); rv.Kind() != reflect.Ptr { + val = reflect.New(rv.Type()).Elem().Addr().Interface() + } + + err = json.Unmarshal(rawMsg, &val) + if err != nil { + return nil, fmt.Errorf("decoding module config: %s: %v", mod.Name, err) + } + + return val, nil +} + var ( modules = make(map[string]Module) modulesMu sync.Mutex diff --git a/modules/caddyhttp/caddyhttp.go b/modules/caddyhttp/caddyhttp.go index 296e28fd..99efef04 100644 --- a/modules/caddyhttp/caddyhttp.go +++ b/modules/caddyhttp/caddyhttp.go @@ -1,7 +1,13 @@ package caddyhttp import ( + "fmt" "log" + "net" + "net/http" + "strconv" + "strings" + "time" "bitbucket.org/lightcodelabs/caddy2" ) @@ -9,7 +15,7 @@ import ( func init() { err := caddy2.RegisterModule(caddy2.Module{ Name: "http", - New: func() (interface{}, error) { return httpModuleConfig{}, nil }, + New: func() (interface{}, error) { return new(httpModuleConfig), nil }, }) if err != nil { log.Fatal(err) @@ -20,8 +26,69 @@ 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"` +func (hc *httpModuleConfig) Run() error { + fmt.Printf("RUNNING: %#v\n", hc) + + for _, srv := range hc.Servers { + s := http.Server{ + ReadTimeout: time.Duration(srv.ReadTimeout), + ReadHeaderTimeout: time.Duration(srv.ReadHeaderTimeout), + } + + for _, lnAddr := range srv.Listen { + proto, addrs, err := parseListenAddr(lnAddr) + if err != nil { + return fmt.Errorf("parsing listen address '%s': %v", lnAddr, err) + } + for _, addr := range addrs { + ln, err := caddy2.Listen(proto, addr) + if err != nil { + return fmt.Errorf("%s: listening on %s: %v", proto, addr, err) + } + go s.Serve(ln) + } + } + } + + return nil +} + +func parseListenAddr(a string) (proto string, addrs []string, err error) { + proto = "tcp" + if idx := strings.Index(a, ":::"); idx >= 0 { + proto = strings.ToLower(strings.TrimSpace(a[:idx])) + a = a[idx+3:] + } + var host, port string + host, port, err = net.SplitHostPort(a) + if err != nil { + return + } + ports := strings.SplitN(port, "-", 2) + if len(ports) == 1 { + ports = append(ports, ports[0]) + } + var start, end int + start, err = strconv.Atoi(ports[0]) + if err != nil { + return + } + end, err = strconv.Atoi(ports[1]) + if err != nil { + return + } + if end < start { + err = fmt.Errorf("end port must be greater than start port") + return + } + for p := start; p <= end; p++ { + addrs = append(addrs, net.JoinHostPort(host, fmt.Sprintf("%d", p))) + } + return +} + +type httpServerConfig struct { + Listen []string `json:"listen"` + ReadTimeout caddy2.Duration `json:"read_timeout"` + ReadHeaderTimeout caddy2.Duration `json:"read_header_timeout"` } diff --git a/modules/caddyhttp/caddyhttp_test.go b/modules/caddyhttp/caddyhttp_test.go new file mode 100644 index 00000000..c65a9a27 --- /dev/null +++ b/modules/caddyhttp/caddyhttp_test.go @@ -0,0 +1,80 @@ +package caddyhttp + +import ( + "reflect" + "testing" +) + +func TestParseListenerAddr(t *testing.T) { + for i, tc := range []struct { + input string + expectProto string + expectAddrs []string + expectErr bool + }{ + { + input: "", + expectProto: "tcp", + expectErr: true, + }, + { + input: ":", + expectProto: "tcp", + expectErr: true, + }, + { + input: ":1234", + expectProto: "tcp", + expectAddrs: []string{":1234"}, + }, + { + input: "tcp::::1234", + expectProto: "tcp", + expectAddrs: []string{":1234"}, + }, + { + input: "tcp6::::1234", + expectProto: "tcp6", + expectAddrs: []string{":1234"}, + }, + { + input: "tcp4:::localhost:1234", + expectProto: "tcp4", + expectAddrs: []string{"localhost:1234"}, + }, + { + input: "unix:::localhost:1234-1236", + expectProto: "unix", + expectAddrs: []string{"localhost:1234", "localhost:1235", "localhost:1236"}, + }, + { + input: "localhost:1234-1234", + expectProto: "tcp", + expectAddrs: []string{"localhost:1234"}, + }, + { + input: "localhost:2-1", + expectProto: "tcp", + expectErr: true, + }, + { + input: "localhost:0", + expectProto: "tcp", + expectAddrs: []string{"localhost:0"}, + }, + } { + actualProto, actualAddrs, err := parseListenAddr(tc.input) + if tc.expectErr && err == nil { + t.Errorf("Test %d: Expected error but got: %v", i, err) + } + if !tc.expectErr && err != nil { + t.Errorf("Test %d: Expected no error but got: %v", i, err) + } + if actualProto != tc.expectProto { + t.Errorf("Test %d: Expeceted protocol '%s' but got '%s'", i, tc.expectProto, actualProto) + } + if !reflect.DeepEqual(tc.expectAddrs, actualAddrs) { + t.Errorf("Test %d: Expected addresses %v but got %v", i, tc.expectAddrs, actualAddrs) + } + } +}