├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── client └── client.go ├── config └── config.go ├── flag.go ├── go.mod ├── go.sum ├── hookup.go ├── log.go ├── main.go ├── operator ├── mdns.go ├── router.go └── router_test.go ├── route.go └── server.go /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | coverage.out 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: *.go **/*.go 2 | env GOOS=linux GOARCH=arm GOARM=5 go build -o build/switchboard 3 | 4 | coverage.out: *.go **/*.go 5 | go test -coverprofile=coverage.out ./... 6 | 7 | cover: coverage.out 8 | go tool cover -html=coverage.out 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Switchboard 2 | ==== 3 | 4 | Simple mDNS-based reverse proxy for personal infrastructure. 5 | 6 | The server will check for mDNS broadcasts regularly and update its configuration. 7 | TLS is supported through Let's Encrypt. 8 | ``` 9 | switchboard route -port 80 -domain first.domain -domain second.domain 10 | ``` 11 | 12 | A node in the network can tell the switchboard to send requests that match a pattern its way. 13 | ``` 14 | switchboard hookup -port 8000 -pattern first.domain/ 15 | // requests like https://first.domain/hello will be forwarded to this box on port 8000 16 | ``` 17 | 18 | ``` 19 | switchboard hookup -port 8000 -pattern /test 20 | // requests like https:///test will be forwarded to this box on port 8000 21 | ``` 22 | -------------------------------------------------------------------------------- /client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/hashicorp/mdns" 8 | "github.com/whytheplatypus/switchboard/config" 9 | ) 10 | 11 | func Hookup(pattern string, port int) *mdns.Server { 12 | // Setup our service export 13 | host, _ := os.Hostname() 14 | info := []string{pattern} 15 | service, _ := mdns.NewMDNSService( 16 | host, 17 | fmt.Sprintf("%s", config.ServiceName), 18 | "", 19 | "", 20 | port, 21 | nil, 22 | info, 23 | ) 24 | 25 | // Create the mDNS server, defer shutdown 26 | server, _ := mdns.NewServer(&mdns.Config{Zone: service}) 27 | return server 28 | } 29 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | const ServiceName = "_switchboard" 4 | -------------------------------------------------------------------------------- /flag.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type StringArray []string 4 | 5 | func (av *StringArray) String() string { 6 | return "" 7 | } 8 | 9 | func (av *StringArray) Set(s string) error { 10 | *av = append(*av, s) 11 | return nil 12 | } 13 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/whytheplatypus/switchboard 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/gorilla/handlers v1.4.2 7 | github.com/hashicorp/mdns v1.0.3 8 | github.com/whytheplatypus/flushable v0.0.0-20200704222724-98b04836c8a0 9 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 10 | golang.org/x/net v0.0.0-20200625001655-4c5254603344 // indirect 11 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 // indirect 12 | ) 13 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/gorilla/handlers v1.4.2 h1:0QniY0USkHQ1RGCLfKxeNHK9bkDHGRYGNDFBCS+YARg= 2 | github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= 3 | github.com/hashicorp/mdns v1.0.3 h1:hPneYJlzSjxFBmUlnDGXRykxBZ++dQAJhU57gCO7TzI= 4 | github.com/hashicorp/mdns v1.0.3/go.mod h1:P9sIDVQGUBr2GtS4qS2QCBdtgqP7TBt6d8looU5l5r4= 5 | github.com/miekg/dns v1.1.27 h1:aEH/kqUzUxGJ/UHcEKdJY+ugH6WEzsEBBSPa8zuy1aM= 6 | github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= 7 | github.com/whytheplatypus/flushable v0.0.0-20200704222724-98b04836c8a0 h1:VbWGd3s2P4vBgfdO6PdaLlk2fzdRGfQ9B2yKO551AOk= 8 | github.com/whytheplatypus/flushable v0.0.0-20200704222724-98b04836c8a0/go.mod h1:FB3YJ88UFtdKuuzDLBwTJc79k+zmeMvR3FX1DuOv2qY= 9 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 10 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8= 11 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 12 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= 13 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 14 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 15 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 16 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 17 | golang.org/x/net v0.0.0-20190923162816-aa69164e4478 h1:l5EDrHhldLYb3ZRHDUhXF7Om7MvYXnkV9/iQNo1lX6g= 18 | golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 19 | golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4= 20 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 21 | golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= 22 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 23 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA= 24 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 25 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 26 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 27 | golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe h1:6fAMxZRR6sl1Uq8U61gxU+kPTs2tR8uOySCbBP7BN/M= 28 | golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 29 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= 30 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 31 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 32 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 33 | golang.org/x/tools v0.0.0-20191216052735-49a3e744a425 h1:VvQyQJN0tSuecqgcIxMWnnfG5kSmgy9KZR9sW3W5QeA= 34 | golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 35 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 36 | -------------------------------------------------------------------------------- /hookup.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | 7 | "github.com/whytheplatypus/switchboard/client" 8 | ) 9 | 10 | func hookup(args []string, ctx context.Context) { 11 | flags := flag.NewFlagSet("hookup", flag.ExitOnError) 12 | pattern := flags.String("pattern", "", "the url pattern that should forward to this service") 13 | port := flags.Int("port", 80, "the port the service runs on") 14 | flags.Parse(args) 15 | 16 | server := client.Hookup(*pattern, *port) 17 | defer server.Shutdown() 18 | <-ctx.Done() 19 | } 20 | -------------------------------------------------------------------------------- /log.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "net/http" 7 | "os" 8 | 9 | "github.com/whytheplatypus/flushable" 10 | ) 11 | 12 | var ( 13 | accessLog = log.New(os.Stderr, "[access] ", log.LstdFlags) 14 | registrationLog = log.New(os.Stderr, "[registration] ", log.LstdFlags) 15 | routingLog = log.New(os.Stderr, "[routing] ", log.LstdFlags) 16 | ) 17 | 18 | func configureLog(addr string) { 19 | go func() { 20 | r := http.NewServeMux() 21 | r.Handle("/debug/access", logHandler(accessLog)) 22 | r.Handle("/debug/registration", logHandler(registrationLog)) 23 | r.Handle("/debug/routing", logHandler(routingLog)) 24 | if err := http.ListenAndServe(addr, r); err != nil { 25 | log.Println(err) 26 | } 27 | }() 28 | } 29 | 30 | func logHandler(l *log.Logger) http.Handler { 31 | m := &flushable.MultiFlusher{} 32 | l.SetOutput(io.MultiWriter(m, l.Writer())) 33 | return m 34 | } 35 | 36 | type lWriter struct { 37 | *log.Logger 38 | } 39 | 40 | func (lw *lWriter) Write(p []byte) (n int, err error) { 41 | err = lw.Output(2, string(p)) 42 | return len(p), err 43 | } 44 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "os" 8 | "os/signal" 9 | "syscall" 10 | ) 11 | 12 | var cmds = map[string]func(args []string, ctx context.Context){ 13 | "hookup": hookup, 14 | "route": route, 15 | } 16 | 17 | func main() { 18 | flag.Parse() 19 | args := flag.Args() 20 | cmd, ok := cmds[args[0]] 21 | if !ok { 22 | flag.Usage() 23 | for key := range cmds { 24 | fmt.Fprintln(flag.CommandLine.Output(), key) 25 | } 26 | os.Exit(2) 27 | } 28 | ctx, cancel := context.WithCancel(context.Background()) 29 | 30 | go func() { 31 | waitFor(syscall.SIGINT, syscall.SIGTERM) 32 | cancel() 33 | }() 34 | 35 | cmd(args[1:], ctx) 36 | } 37 | 38 | func waitFor(calls ...os.Signal) { 39 | sigs := make(chan os.Signal, 1) 40 | signal.Notify(sigs, calls...) 41 | <-sigs 42 | } 43 | -------------------------------------------------------------------------------- /operator/mdns.go: -------------------------------------------------------------------------------- 1 | package operator 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/url" 7 | "strings" 8 | "time" 9 | 10 | "github.com/hashicorp/mdns" 11 | "github.com/whytheplatypus/switchboard/config" 12 | ) 13 | 14 | func Listen(ctx context.Context) <-chan *mdns.ServiceEntry { 15 | // Make a channel for results and start listening 16 | entries := make(chan *mdns.ServiceEntry, 5) 17 | 18 | // Start the lookup 19 | go func() { 20 | ticker := time.NewTicker(5 * time.Second) 21 | defer ticker.Stop() 22 | defer close(entries) 23 | for { 24 | select { 25 | case <-ctx.Done(): 26 | return 27 | case <-ticker.C: 28 | mdns.Lookup(fmt.Sprintf("%s", config.ServiceName), entries) 29 | } 30 | } 31 | }() 32 | 33 | return entries 34 | } 35 | 36 | func Connect(entry *mdns.ServiceEntry) error { 37 | if !strings.Contains(entry.Name, config.ServiceName) { 38 | return ErrUnknownEntry 39 | } 40 | return register(entry) 41 | } 42 | 43 | func register(entry *mdns.ServiceEntry) error { 44 | 45 | u, err := url.Parse(fmt.Sprintf("http://%s:%d", entry.AddrV4, entry.Port)) 46 | if err != nil { 47 | return err 48 | } 49 | defaultRouter.register(entry.InfoFields[0], u) 50 | return nil 51 | } 52 | -------------------------------------------------------------------------------- /operator/router.go: -------------------------------------------------------------------------------- 1 | package operator 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/http" 7 | "net/http/httputil" 8 | "net/url" 9 | "sort" 10 | "strings" 11 | "sync" 12 | ) 13 | 14 | var ( 15 | ErrUnknownEntry = errors.New("mdns: unkown entry type recieved") 16 | ErrDuplicateEntry = errors.New("mdns: duplicate entry recieved") 17 | ) 18 | 19 | var defaultRouter = &Router{} 20 | 21 | type phonebookIndex []string 22 | 23 | func (i phonebookIndex) Len() int { return len(i) } 24 | func (i phonebookIndex) Swap(j, k int) { i[j], i[k] = i[k], i[j] } 25 | func (i phonebookIndex) Less(j, k int) bool { return len(i[j]) > len(i[k]) } 26 | 27 | type Router struct { 28 | phonebook map[string]*url.URL 29 | index phonebookIndex 30 | mu sync.Mutex 31 | matcher func(pattern *url.URL, requested *url.URL) bool 32 | } 33 | 34 | func (r *Router) register(pattern string, target *url.URL) { 35 | r.mu.Lock() 36 | if r.phonebook == nil { 37 | r.phonebook = map[string]*url.URL{} 38 | } 39 | defer r.mu.Unlock() 40 | r.phonebook[pattern] = target 41 | r.updateIndex() 42 | } 43 | 44 | func (r *Router) updateIndex() { 45 | r.index = phonebookIndex(make([]string, len(r.phonebook))) 46 | i := 0 47 | for k := range r.phonebook { 48 | r.index[i] = k 49 | i++ 50 | } 51 | sort.Sort(r.index) 52 | } 53 | 54 | func (r *Router) direct(req *http.Request) { 55 | target, pattern := r.lookup(req) 56 | if target == nil { 57 | panic("No Target URL found") 58 | } 59 | targetQuery := target.RawQuery 60 | req.URL.Scheme = target.Scheme 61 | req.URL.Host = target.Host 62 | req.URL.Path = strings.TrimPrefix(req.URL.Path, pattern.Path) 63 | if !strings.HasPrefix(req.URL.Path, "/") { 64 | req.URL.Path = fmt.Sprintf("/%s", req.URL.Path) 65 | } 66 | if targetQuery == "" || req.URL.RawQuery == "" { 67 | req.URL.RawQuery = targetQuery + req.URL.RawQuery 68 | } else { 69 | req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery 70 | } 71 | if _, ok := req.Header["User-Agent"]; !ok { 72 | // explicitly disable User-Agent so it's not set to default value 73 | req.Header.Set("User-Agent", "") 74 | } 75 | 76 | } 77 | 78 | func (r *Router) lookup(req *http.Request) (*url.URL, *url.URL) { 79 | r.mu.Lock() 80 | defer r.mu.Unlock() 81 | req.URL.Host = req.Host 82 | for _, v := range r.index { 83 | u, err := url.Parse(v) 84 | if err != nil { 85 | panic(err) 86 | } 87 | if r.match(u, req.URL) { 88 | return r.phonebook[v], u 89 | } 90 | } 91 | return nil, nil 92 | } 93 | 94 | func (r *Router) match(pattern *url.URL, requested *url.URL) bool { 95 | if r.matcher != nil { 96 | return r.matcher(pattern, requested) 97 | } 98 | return defaultMatch(pattern, requested) 99 | } 100 | 101 | func (r *Router) Handler() *httputil.ReverseProxy { 102 | proxy := &httputil.ReverseProxy{ 103 | Director: r.direct, 104 | } 105 | return proxy 106 | } 107 | 108 | func Handler() *httputil.ReverseProxy { 109 | return defaultRouter.Handler() 110 | } 111 | 112 | func defaultMatch(p *url.URL, r *url.URL) bool { 113 | return (p.Host == "" || p.Host == r.Host) && strings.HasPrefix(r.Path, p.Path) && strings.Contains(r.RawQuery, p.RawQuery) 114 | } 115 | -------------------------------------------------------------------------------- /operator/router_test.go: -------------------------------------------------------------------------------- 1 | package operator 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "log" 7 | "net/http" 8 | "net/http/httptest" 9 | "net/url" 10 | "testing" 11 | ) 12 | 13 | func init() { 14 | log.SetFlags(log.Llongfile) 15 | } 16 | 17 | func parseURL(s string) *url.URL { 18 | u, err := url.Parse(s) 19 | if err != nil { 20 | panic(err) 21 | } 22 | return u 23 | } 24 | 25 | func TestHandler(t *testing.T) { 26 | router := defaultRouter.Handler() 27 | h := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { 28 | defer func() { 29 | if err := recover(); err != nil { 30 | http.NotFound(rw, r) 31 | } 32 | }() 33 | router.ServeHTTP(rw, r) 34 | }) 35 | srv := httptest.NewServer(h) 36 | defer srv.Close() 37 | pathEchoSrv := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { 38 | rw.Write([]byte(r.URL.String())) 39 | })) 40 | defer pathEchoSrv.Close() 41 | c := srv.Client() 42 | 43 | tests := []struct { 44 | pattern string 45 | target *url.URL 46 | url string 47 | result string 48 | }{ 49 | { 50 | "", 51 | nil, 52 | fmt.Sprintf("%s/%s", srv.URL, "not-found"), 53 | "404 page not found\n", 54 | }, 55 | { 56 | "/test", 57 | parseURL(pathEchoSrv.URL), 58 | fmt.Sprintf("%s/%s", srv.URL, "test"), 59 | "/", 60 | }, 61 | { 62 | srv.URL, 63 | parseURL(pathEchoSrv.URL), 64 | fmt.Sprintf("%s/%s", srv.URL, "test"), 65 | "/test", 66 | }, 67 | { 68 | fmt.Sprintf("%s/%s", srv.URL, "other/"), 69 | parseURL(pathEchoSrv.URL), 70 | fmt.Sprintf("%s/%s", srv.URL, "other/test"), 71 | "/test", 72 | }, 73 | { 74 | srv.URL, 75 | parseURL(pathEchoSrv.URL), 76 | fmt.Sprintf("%s/%s", srv.URL, "test/"), 77 | "/test/", 78 | }, 79 | } 80 | for _, tt := range tests { 81 | if tt.pattern != "" { 82 | defaultRouter.register(tt.pattern, tt.target) 83 | } 84 | resp, err := c.Get(tt.url) 85 | if err != nil { 86 | t.Fatal(err) 87 | } 88 | b, err := ioutil.ReadAll(resp.Body) 89 | if err != nil { 90 | t.Fatal(err) 91 | } 92 | if string(b) != tt.result { 93 | t.Fatal("wrong route, got", string(b)) 94 | } 95 | } 96 | if len(defaultRouter.index) > 3 { 97 | t.Fatal("duplicate entries were registered", len(defaultRouter.index)) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /route.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "flag" 7 | "fmt" 8 | "net/http" 9 | 10 | "github.com/gorilla/handlers" 11 | "github.com/whytheplatypus/switchboard/operator" 12 | ) 13 | 14 | func route(args []string, ctx context.Context) { 15 | flags := flag.NewFlagSet("route", flag.ExitOnError) 16 | port := flags.Int("port", 80, "the port this should run on") 17 | cdir := flags.String("cert-directory", "/var/cache/switchboard/autocert", "the directory to store the acme cert") 18 | var domains StringArray 19 | flags.Var(&domains, "domain", "a domain to register a tls cert for") 20 | httpLog := flags.String("log-http", "", "The address to serve logs over, no logs are served if empty") 21 | flags.Parse(args) 22 | 23 | if *httpLog != "" { 24 | configureLog(*httpLog) 25 | } 26 | 27 | go func() { 28 | entries := operator.Listen(ctx) 29 | for entry := range entries { 30 | if err := operator.Connect(entry); err != nil { 31 | registrationLog.Println(err) 32 | continue 33 | } 34 | // register 35 | registrationLog.Printf(`{"send":"%s","to":"http://%s:%d"}`, 36 | entry.InfoFields[0], 37 | entry.AddrV4, 38 | entry.Port, 39 | ) 40 | } 41 | }() 42 | 43 | router := operator.Handler() 44 | 45 | router.ModifyResponse = func(r *http.Response) error { 46 | info := struct { 47 | Host string `json:"host"` 48 | Target string `json:"target"` 49 | Path string `json:"path"` 50 | Query string `json:"query"` 51 | }{ 52 | r.Request.Host, 53 | r.Request.URL.Host, 54 | r.Request.URL.Path, 55 | r.Request.URL.RawQuery, 56 | } 57 | 58 | b, _ := json.Marshal(info) 59 | routingLog.Println(string(b)) 60 | return nil 61 | } 62 | 63 | h := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { 64 | defer func() { 65 | if err := recover(); err != nil { 66 | http.NotFound(rw, r) 67 | } 68 | }() 69 | router.ServeHTTP(rw, r) 70 | }) 71 | 72 | srv := &server{ 73 | Addr: fmt.Sprintf(":%d", *port), 74 | Handler: handlers.LoggingHandler(&lWriter{accessLog}, h), 75 | CertDir: *cdir, 76 | Domains: domains, 77 | } 78 | 79 | if err := srv.serve(ctx); err != nil { 80 | routingLog.Fatal(err) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "os" 7 | 8 | "golang.org/x/crypto/acme/autocert" 9 | ) 10 | 11 | type server struct { 12 | Addr string 13 | Handler http.Handler 14 | CertDir string 15 | Domains []string 16 | } 17 | 18 | func (s *server) serve(ctx context.Context) error { 19 | 20 | srv := &http.Server{ 21 | Addr: s.Addr, 22 | Handler: s.Handler, 23 | } 24 | 25 | if s.CertDir != "" && len(s.Domains) > 0 { 26 | m := &autocert.Manager{ 27 | Prompt: autocert.AcceptTOS, 28 | } 29 | 30 | m.HostPolicy = autocert.HostWhitelist(s.Domains...) 31 | 32 | if err := os.MkdirAll(s.CertDir, os.ModePerm); err != nil { 33 | return err 34 | } 35 | m.Cache = autocert.DirCache(s.CertDir) 36 | srv.Handler = m.HTTPHandler(nil) 37 | 38 | crtSrv := &http.Server{ 39 | Handler: s.Handler, 40 | } 41 | //TODO return errors 42 | go crtSrv.Serve(m.Listener()) 43 | defer crtSrv.Shutdown(context.Background()) 44 | } 45 | 46 | //TODO return errors 47 | go srv.ListenAndServe() 48 | <-ctx.Done() 49 | //TODO return errors 50 | srv.Shutdown(context.Background()) 51 | return nil 52 | } 53 | --------------------------------------------------------------------------------