summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--admin.go55
-rw-r--r--caddy.go62
-rw-r--r--listeners.go51
-rw-r--r--modules.go30
-rw-r--r--modules/caddyhttp/caddyhttp.go75
-rw-r--r--modules/caddyhttp/caddyhttp_test.go80
6 files changed, 310 insertions, 43 deletions
diff --git a/admin.go b/admin.go
index 0672745..2841ed8 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 0000000..2f12776
--- /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 0000000..962cb1d
--- /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 1c3e231..4a396d4 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 296e28f..99efef0 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"`
}
+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 string `json:"read_timeout"`
- ReadHeaderTimeout string `json:"read_header_timeout"`
+ 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 0000000..c65a9a2
--- /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)
+ }
+ }
+}