Rudimentary start of HTTP servers

This commit is contained in:
Matthew Holt 2019-03-26 15:45:51 -06:00
parent 859b5d7ea3
commit 86e2d1b0a4
6 changed files with 311 additions and 44 deletions

View File

@ -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{}
}

62
caddy.go Normal file
View File

@ -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
}

51
listeners.go Normal file
View File

@ -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' 😉")

View File

@ -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

View File

@ -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"`
}

View File

@ -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)
}
}
}