From 710771c2282477cbcfc27587cac71f25f843fb35 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 27 May 2020 02:53:26 -0600 Subject: [PATCH] add DNS-01 API relay --- .gitignore | 1 + go.mod | 1 + mplexer/cmd/dnsclient/dnsclient.go | 105 ++++++++++ mplexer/cmd/telebit/telebit.go | 89 ++++++--- mplexer/dns01/LICENSE | 22 +++ mplexer/dns01/dns01.go | 182 ++++++++++++++++++ mplexer/dns01/dns01_test.go | 296 +++++++++++++++++++++++++++++ mplexer/listener.go | 2 +- mplexer/routemux.go | 10 +- mplexer/server.go | 72 ------- vendor/modules.txt | 11 ++ 11 files changed, 690 insertions(+), 101 deletions(-) create mode 100644 mplexer/cmd/dnsclient/dnsclient.go create mode 100644 mplexer/dns01/LICENSE create mode 100644 mplexer/dns01/dns01.go create mode 100644 mplexer/dns01/dns01_test.go delete mode 100644 mplexer/server.go diff --git a/.gitignore b/.gitignore index 69dee76..aa80a37 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ certs acme.d xversion.go +/mplexer/cmd/dnsclient/dnsclient /mplexer/cmd/mgmt/mgmt /mplexer/cmd/signjwt/signjwt /mplexer/cmd/telebit/telebit diff --git a/go.mod b/go.mod index df3317f..71ee457 100644 --- a/go.mod +++ b/go.mod @@ -11,5 +11,6 @@ require ( github.com/gorilla/mux v1.7.4 github.com/gorilla/websocket v1.4.2 github.com/joho/godotenv v1.3.0 + github.com/stretchr/testify v1.5.1 gopkg.in/natefinch/lumberjack.v2 v2.0.0 ) diff --git a/mplexer/cmd/dnsclient/dnsclient.go b/mplexer/cmd/dnsclient/dnsclient.go new file mode 100644 index 0000000..bbb9d80 --- /dev/null +++ b/mplexer/cmd/dnsclient/dnsclient.go @@ -0,0 +1,105 @@ +//go:generate go run -mod=vendor git.rootprojects.org/root/go-gitver + +package main + +import ( + "errors" + "flag" + "fmt" + "net/url" + "os" + "strings" + + dns01 "git.coolaj86.com/coolaj86/go-telebitd/mplexer/dns01" + + jwt "github.com/dgrijalva/jwt-go" + "github.com/go-acme/lego/v3/challenge" + _ "github.com/joho/godotenv/autoload" +) + +var ( + // GitRev refers to the abbreviated commit hash + GitRev = "0000000" + // GitVersion refers to the most recent tag, plus any commits made since then + GitVersion = "v0.0.0-pre0+0000000" + // GitTimestamp refers to the timestamp of the most recent commit + GitTimestamp = "0000-00-00T00:00:00+0000" +) + +func main() { + var err error + var provider challenge.Provider = nil + var domains []string + + // TODO replace the websocket connection with a mock server + acmeRelay := flag.String("acme-relay", "", "the base url of the ACME DNS-01 relay, if not the same as the tunnel relay") + secret := flag.String("secret", "", "the same secret used by telebit-relay (used for JWT authentication)") + token := flag.String("token", "", "a pre-generated token to give the server (instead of generating one with --secret)") + flag.Parse() + + if len(os.Args) >= 2 { + if "version" == os.Args[1] { + fmt.Printf("telebit %s %s %s", GitVersion, GitRev, GitTimestamp) + os.Exit(0) + } + } + + if "" == *token { + if "" == *secret { + *secret = os.Getenv("SECRET") + } + *token, err = getToken(*secret, domains) + } + if nil != err { + fmt.Fprintf(os.Stderr, "neither secret nor token provided") + os.Exit(1) + return + } + + if "" == *acmeRelay { + panic(errors.New("ACME relay should be specified")) + } + + endpoint := *acmeRelay + if strings.HasSuffix(endpoint, "/") { + endpoint = endpoint[:len(endpoint)-1] + } + endpoint += "/api/dns/" + if provider, err = newAPIDNSProvider(endpoint, *token); nil != err { + panic(err) + } + + err = provider.Present(os.Getenv("HOSTNAME"), "xxx", "yyy") + if nil != err { + fmt.Fprintf(os.Stderr, err.Error()) + } + err = provider.Present(os.Getenv("HOSTNAME"), "xxx", "yyy") + if nil != err { + fmt.Fprintf(os.Stderr, err.Error()) + os.Exit(1) + } + + fmt.Println("quite possibly successful") +} + +// newAPIDNSProvider is for the sake of demoing the tunnel +func newAPIDNSProvider(baseURL string, token string) (*dns01.DNSProvider, error) { + config := dns01.NewDefaultConfig() + config.Token = token + endpoint, err := url.Parse(baseURL) + if nil != err { + return nil, err + } + config.Endpoint = endpoint + return dns01.NewDNSProviderConfig(config) +} + +func getToken(secret string, domains []string) (token string, err error) { + tokenData := jwt.MapClaims{"domains": domains} + + jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, tokenData) + if token, err = jwtToken.SignedString([]byte(secret)); err != nil { + return "", err + } + return token, nil +} diff --git a/mplexer/cmd/telebit/telebit.go b/mplexer/cmd/telebit/telebit.go index 8ce82c1..72a0b72 100644 --- a/mplexer/cmd/telebit/telebit.go +++ b/mplexer/cmd/telebit/telebit.go @@ -7,11 +7,14 @@ import ( "flag" "fmt" "log" + "net" + "net/url" "os" "strings" "time" telebit "git.coolaj86.com/coolaj86/go-telebitd/mplexer" + dns01 "git.coolaj86.com/coolaj86/go-telebitd/mplexer/dns01" "github.com/caddyserver/certmagic" jwt "github.com/dgrijalva/jwt-go" @@ -39,7 +42,6 @@ type Forward struct { func main() { var err error var provider challenge.Provider = nil - var enableTLSALPN01 bool var domains []string var forwards []Forward @@ -50,6 +52,8 @@ func main() { acmeStaging := flag.Bool("acme-staging", false, "get fake certificates for testing") acmeDirectory := flag.String("acme-directory", "", "ACME Directory URL") enableHTTP01 := flag.Bool("acme-http-01", false, "enable HTTP-01 ACME challenges") + enableTLSALPN01 := flag.Bool("acme-tls-alpn-01", false, "enable TLS-ALPN-01 ACME challenges") + acmeRelay := flag.String("acme-relay", "", "the base url of the ACME DNS-01 relay, if not the same as the tunnel relay") relay := flag.String("relay", "", "the domain (or ip address) at which the relay server is running") secret := flag.String("secret", "", "the same secret used by telebit-relay (used for JWT authentication)") token := flag.String("token", "", "a pre-generated token to give the server (instead of generating one with --secret)") @@ -98,6 +102,30 @@ func main() { domains = append(domains, domain) } + if "" == *token { + if "" == *secret { + *secret = os.Getenv("SECRET") + } + *token, err = getToken(*secret, domains) + } + if nil != err { + fmt.Fprintf(os.Stderr, "neither secret nor token provided") + os.Exit(1) + return + } + + if "" == *relay { + *relay = os.Getenv("RELAY") // "wss://example.com:443" + } + if "" == *relay { + fmt.Fprintf(os.Stderr, "Missing relay url") + os.Exit(1) + return + } + if "" == *acmeRelay { + *acmeRelay = strings.Replace(*relay, "ws", "http", 1) // "https://example.com:443" + } + if "" != os.Getenv("GODADDY_API_KEY") { id := os.Getenv("GODADDY_API_KEY") secret := os.Getenv("GODADDY_API_SECRET") @@ -109,23 +137,15 @@ func main() { panic(err) } } else { - enableTLSALPN01 = true - } - - if "" == *relay { - *relay = os.Getenv("RELAY") // "wss://example.com:443" - } - if "" == *token { - if "" == *secret { - *secret = os.Getenv("SECRET") + endpoint := *acmeRelay + if strings.HasSuffix(endpoint, "/") { + endpoint = endpoint[:len(endpoint)-1] + } + endpoint += "/api/dns/" + if provider, err = newAPIDNSProvider(endpoint, *token); nil != err { + panic(err) } - *token, err = getToken(*secret, domains) } - if nil != err { - panic(err) - } - - ctx := context.Background() acme := &telebit.ACME{ Email: *email, @@ -134,7 +154,7 @@ func main() { Directory: *acmeDirectory, DNSProvider: provider, EnableHTTPChallenge: *enableHTTP01, - EnableTLSALPNChallenge: enableTLSALPN01, + EnableTLSALPNChallenge: *enableTLSALPN01, } mux := telebit.NewRouteMux() @@ -144,13 +164,24 @@ func main() { //mux.ForwardTCP(fwd.pattern, "localhost:"+fwd.port, 120*time.Second) } - tun, err := telebit.DialWebsocketTunnel(ctx, *relay, *token) - if nil != err { - fmt.Println("relay:", relay) - log.Fatal(err) - return - } + connected := make(chan net.Conn) + go func() { + timeoutCtx, cancelTimeout := context.WithDeadline(context.Background(), time.Now().Add(10*time.Second)) + tun, err := telebit.DialWebsocketTunnel(timeoutCtx, *relay, *token) + if nil != err { + msg := "" + if strings.Contains(err.Error(), "bad handshake") { + msg = " (may be auth related)" + } + fmt.Fprintf(os.Stderr, "Error connecting to %s: %s%s\n", *relay, err, msg) + os.Exit(1) + return + } + cancelTimeout() + connected <- tun + }() + tun := <-connected fmt.Printf("Listening at %s\n", *relay) log.Fatal("Closed server: ", telebit.ListenAndServe(tun, mux)) } @@ -183,6 +214,18 @@ func newGoDaddyDNSProvider(id, secret string) (*godaddy.DNSProvider, error) { return godaddy.NewDNSProviderConfig(config) } +// newAPIDNSProvider is for the sake of demoing the tunnel +func newAPIDNSProvider(baseURL string, token string) (*dns01.DNSProvider, error) { + config := dns01.NewDefaultConfig() + config.Token = token + endpoint, err := url.Parse(baseURL) + if nil != err { + return nil, err + } + config.Endpoint = endpoint + return dns01.NewDNSProviderConfig(config) +} + /* // TODO for http proxy return mplexer.TargetOptions { diff --git a/mplexer/dns01/LICENSE b/mplexer/dns01/LICENSE new file mode 100644 index 0000000..e4eac5e --- /dev/null +++ b/mplexer/dns01/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2020 AJ ONeal +Copyright (c) 2015-2017 Sebastian Erhart + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/mplexer/dns01/dns01.go b/mplexer/dns01/dns01.go new file mode 100644 index 0000000..3369818 --- /dev/null +++ b/mplexer/dns01/dns01.go @@ -0,0 +1,182 @@ +// Package dns01 implements a DNS provider for solving the DNS-01 challenge through a HTTP server. +package dns01 + +// Adapted from https://github.com/go-acme/lego/blob/master/providers/dns/httpreq/httpreq.go + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "path" + "time" + + "github.com/go-acme/lego/v3/challenge/dns01" + "github.com/go-acme/lego/v3/platform/config/env" +) + +// Environment variables names. +const ( + envNamespace = "API_" + + EnvEndpoint = envNamespace + "ENDPOINT" + EnvToken = envNamespace + "TOKEN" + + EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" + EnvPollingInterval = envNamespace + "POLLING_INTERVAL" + EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" +) + +type dnsChallenge struct { + Domain string `json:"domain"` + Hostname string `json:"hostname"` + Token string `json:"token"` + KeyAuth string `json:"key_authorization"` + KeyAuthDigest string `json:"key_authorization_digest"` +} + +// Config is used to configure the creation of the DNSProvider. +type Config struct { + Endpoint *url.URL + Token string + PropagationTimeout time.Duration + PollingInterval time.Duration + HTTPClient *http.Client +} + +// NewDefaultConfig returns a default configuration for the DNSProvider. +func NewDefaultConfig() *Config { + return &Config{ + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), + HTTPClient: &http.Client{ + Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 15*time.Second), + }, + } +} + +// DNSProvider implements the challenge.Provider interface. +type DNSProvider struct { + config *Config +} + +// NewDNSProvider returns a DNSProvider instance. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get(EnvEndpoint) + if err != nil { + return nil, fmt.Errorf("api: %w", err) + } + + endpoint, err := url.Parse(values[EnvEndpoint]) + if err != nil { + return nil, fmt.Errorf("api: %w", err) + } + + config := NewDefaultConfig() + config.Token = env.GetOrFile(EnvToken) + config.Endpoint = endpoint + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("api: the configuration of the DNS provider is nil") + } + + if config.Endpoint == nil { + return nil, errors.New("api: the endpoint is missing") + } + + return &DNSProvider{config: config}, nil +} + +// Timeout returns the timeout and interval to use when checking for DNS propagation. +// Adjusting here to cope with spikes in propagation times. +func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { + return d.config.PropagationTimeout, d.config.PollingInterval +} + +// Present creates a TXT record to fulfill the dns-01 challenge. +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + msg := getDNSChallenge(domain, token, keyAuth) + + err := d.doRequest(http.MethodPost, fmt.Sprintf("/%s", msg.Domain), msg) + if err != nil { + return fmt.Errorf("api: %w", err) + } + return nil +} + +// CleanUp removes the TXT record matching the specified parameters. +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + msg := getDNSChallenge(domain, token, keyAuth) + + err := d.doRequest( + http.MethodDelete, + fmt.Sprintf("/%s/%s/%s", msg.Domain, msg.Token, msg.KeyAuth), + nil, + ) + if err != nil { + return fmt.Errorf("api: %w", err) + } + return nil +} + +func getDNSChallenge(domain, token, keyAuth string) *dnsChallenge { + hostname, digest := dns01.GetRecord(domain, keyAuth) + return &dnsChallenge{ + Domain: domain, + Hostname: hostname, + Token: token, + KeyAuth: keyAuth, + KeyAuthDigest: digest, + } +} + +func (d *DNSProvider) doRequest(method, uri string, msg interface{}) error { + reqBody := &bytes.Buffer{} + if nil != msg { + err := json.NewEncoder(reqBody).Encode(msg) + if err != nil { + return err + } + } + + newURI := path.Join(d.config.Endpoint.EscapedPath(), uri) + endpoint, err := d.config.Endpoint.Parse(newURI) + if err != nil { + return err + } + + req, err := http.NewRequest(method, endpoint.String(), reqBody) + if err != nil { + return err + } + + req.Header.Set("Content-Type", "application/json") + + if len(d.config.Token) > 0 { + req.Header.Set("Authorization", "Bearer "+d.config.Token) + } + + resp, err := d.config.HTTPClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode >= http.StatusBadRequest { + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("%d: failed to read response body: %w", resp.StatusCode, err) + } + + return fmt.Errorf("%d: request failed: %v", resp.StatusCode, string(body)) + } + + return nil +} diff --git a/mplexer/dns01/dns01_test.go b/mplexer/dns01/dns01_test.go new file mode 100644 index 0000000..b97940a --- /dev/null +++ b/mplexer/dns01/dns01_test.go @@ -0,0 +1,296 @@ +package dns01 + +// Adapted from https://github.com/go-acme/lego/blob/master/providers/dns/httpreq/httpreq.go + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "path" + "strings" + "testing" + + "github.com/go-acme/lego/v3/platform/tester" + "github.com/stretchr/testify/require" +) + +var envTest = tester.NewEnvTest(EnvEndpoint, EnvToken) + +func TestNewDNSProvider(t *testing.T) { + testCases := []struct { + desc string + envVars map[string]string + expected string + }{ + { + desc: "success", + envVars: map[string]string{ + EnvEndpoint: "http://localhost:8090", + }, + }, + { + desc: "invalid URL", + envVars: map[string]string{ + EnvEndpoint: ":", + }, + expected: `api: parse ":": missing protocol scheme`, + }, + { + desc: "missing endpoint", + envVars: map[string]string{ + EnvEndpoint: "", + }, + expected: "api: some credentials information are missing: API_ENDPOINT", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + defer envTest.RestoreEnv() + envTest.ClearEnv() + + envTest.Apply(test.envVars) + + p, err := NewDNSProvider() + + if len(test.expected) == 0 { + require.NoError(t, err) + require.NotNil(t, p) + require.NotNil(t, p.config) + } else { + require.EqualError(t, err, test.expected) + } + }) + } +} + +func TestNewDNSProviderConfig(t *testing.T) { + testCases := []struct { + desc string + endpoint *url.URL + expected string + }{ + { + desc: "success", + endpoint: mustParse("http://localhost:8090"), + }, + { + desc: "missing endpoint", + expected: "api: the endpoint is missing", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := NewDefaultConfig() + config.Endpoint = test.endpoint + + p, err := NewDNSProviderConfig(config) + + if len(test.expected) == 0 { + require.NoError(t, err) + require.NotNil(t, p) + require.NotNil(t, p.config) + } else { + require.EqualError(t, err, test.expected) + } + }) + } +} + +func TestNewDNSProvider_Present(t *testing.T) { + envTest.RestoreEnv() + + testCases := []struct { + desc string + token string + pathPrefix string + handler http.HandlerFunc + expectedError string + }{ + { + desc: "success", + handler: successHandler, + }, + { + desc: "success with path prefix", + handler: successHandler, + pathPrefix: "/api/acme/", + }, + { + desc: "error", + handler: http.NotFound, + expectedError: "api: 404: request failed: 404 page not found\n", + }, + { + desc: "success raw mode", + handler: successRawModeHandler, + }, + { + desc: "error raw mode", + handler: http.NotFound, + expectedError: "api: 404: request failed: 404 page not found\n", + }, + { + desc: "bearer auth", + token: "foobar", + handler: func(rw http.ResponseWriter, req *http.Request) { + token := strings.Replace(req.Header.Get("Authorization"), "Bearer ", "", 1) + if token != "foobar" { + rw.Header().Set("WWW-Authenticate", fmt.Sprintf(`Basic realm="%s"`, "Please enter your username and password.")) + http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + return + } + + fmt.Fprint(rw, "lego") + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + mux := http.NewServeMux() + hostname := "domain" + mux.HandleFunc(path.Join("/", test.pathPrefix, "/"+hostname), test.handler) + server := httptest.NewServer(mux) + + config := NewDefaultConfig() + config.Endpoint = mustParse(server.URL + test.pathPrefix) + config.Token = test.token + + p, err := NewDNSProviderConfig(config) + require.NoError(t, err) + + err = p.Present("domain", "token", "key") + if test.expectedError == "" { + require.NoError(t, err) + } else { + require.EqualError(t, err, test.expectedError) + } + }) + } +} + +func TestNewDNSProvider_Cleanup(t *testing.T) { + envTest.RestoreEnv() + + testCases := []struct { + desc string + token string + handler http.HandlerFunc + expectedError string + }{ + { + desc: "success", + handler: successHandler, + }, + { + desc: "error", + handler: http.NotFound, + expectedError: "api: 404: request failed: 404 page not found\n", + }, + { + desc: "success raw mode", + handler: successRawModeHandler, + }, + { + desc: "error raw mode", + handler: http.NotFound, + expectedError: "api: 404: request failed: 404 page not found\n", + }, + { + desc: "basic auth", + token: "foobar", + handler: func(rw http.ResponseWriter, req *http.Request) { + token := strings.Replace(req.Header.Get("Authorization"), "Bearer ", "", 1) + if token != "foobar" { + rw.Header().Set("WWW-Authenticate", fmt.Sprintf(`Basic realm="%s"`, "Please enter your username and password.")) + http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + return + } + fmt.Fprint(rw, "lego") + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + hostname := "domain" + dnsToken := "token" + dnsKeyAuth := "key" + mux := http.NewServeMux() + mux.HandleFunc( + fmt.Sprintf("/%s/%s/%s", hostname, dnsToken, dnsKeyAuth), + test.handler, + ) + server := httptest.NewServer(mux) + + config := NewDefaultConfig() + config.Endpoint = mustParse(server.URL) + config.Token = test.token + + p, err := NewDNSProviderConfig(config) + require.NoError(t, err) + + err = p.CleanUp("domain", "token", "key") + if test.expectedError == "" { + require.NoError(t, err) + } else { + require.EqualError(t, err, test.expectedError) + } + }) + } +} + +func successHandler(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodPost && req.Method != http.MethodDelete { + http.Error(rw, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return + } + + msg := &dnsChallenge{} + err := json.NewDecoder(req.Body).Decode(msg) + if err != nil { + if !(req.Method == http.MethodDelete && io.EOF == err) { + http.Error(rw, err.Error(), http.StatusBadRequest) + } + return + } + + fmt.Fprint(rw, "lego") +} + +func successRawModeHandler(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodPost && req.Method != http.MethodDelete { + http.Error(rw, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return + } + + msg := &dnsChallenge{} + err := json.NewDecoder(req.Body).Decode(msg) + if err != nil { + if !(req.Method == http.MethodDelete && io.EOF == err) { + http.Error(rw, err.Error(), http.StatusBadRequest) + } + return + } + + fmt.Fprint(rw, "lego") +} + +func mustParse(rawURL string) *url.URL { + uri, err := url.Parse(rawURL) + if err != nil { + panic(err) + } + return uri +} diff --git a/mplexer/listener.go b/mplexer/listener.go index 02e0d6f..fd645c4 100644 --- a/mplexer/listener.go +++ b/mplexer/listener.go @@ -40,7 +40,7 @@ func Listen(tun net.Conn) *Listener { go func() { err := listener.encoder.Run() fmt.Printf("encoder stopped entirely: %q", err) - tun.Close() + listener.Close() }() // Decode the stream as it comes in diff --git a/mplexer/routemux.go b/mplexer/routemux.go index 930c8fe..7222925 100644 --- a/mplexer/routemux.go +++ b/mplexer/routemux.go @@ -12,7 +12,7 @@ import ( // and selects the matching handler. type RouteMux struct { defaultTimeout time.Duration - list []meta + routes []meta } type meta struct { @@ -34,7 +34,7 @@ func (m *RouteMux) Serve(client net.Conn) error { wconn := &ConnWrap{Conn: client} servername := wconn.Servername() - for _, meta := range m.list { + for _, meta := range m.routes { if servername == meta.addr || "*" == meta.addr { //fmt.Println("[debug] test of route:", meta) if err := meta.handler.Serve(client); nil != err { @@ -52,7 +52,7 @@ func (m *RouteMux) Serve(client net.Conn) error { // ForwardTCP creates and returns a connection to a local handler target. func (m *RouteMux) ForwardTCP(servername string, target string, timeout time.Duration) error { // TODO check servername - m.list = append(m.list, meta{ + m.routes = append(m.routes, meta{ addr: servername, terminate: false, handler: NewForwarder(target, timeout), @@ -63,7 +63,7 @@ func (m *RouteMux) ForwardTCP(servername string, target string, timeout time.Dur // HandleTCP creates and returns a connection to a local handler target. func (m *RouteMux) HandleTCP(servername string, handler Handler) error { // TODO check servername - m.list = append(m.list, meta{ + m.routes = append(m.routes, meta{ addr: servername, terminate: false, handler: handler, @@ -74,7 +74,7 @@ func (m *RouteMux) HandleTCP(servername string, handler Handler) error { // HandleTLS creates and returns a connection to a local handler target. func (m *RouteMux) HandleTLS(servername string, acme *ACME, handler Handler) error { // TODO check servername - m.list = append(m.list, meta{ + m.routes = append(m.routes, meta{ addr: servername, terminate: true, handler: HandlerFunc(func(client net.Conn) error { diff --git a/mplexer/server.go b/mplexer/server.go deleted file mode 100644 index 97fdd04..0000000 --- a/mplexer/server.go +++ /dev/null @@ -1,72 +0,0 @@ -package telebit - -import ( - "context" - "errors" -) - -type Server struct { - ctx context.Context - newConns chan *Conn - data []byte - dataReady chan struct{} -} - -func (s *Server) Accept() (*Conn, error) { - select { - case <-s.ctx.Done(): - return nil, errors.New("TODO: ErrClosed") - case conn := <-s.newConns: - return conn, nil - } -} - -// Read packs transforms local responses into wrapped data for the tunnel -func (s *Server) Read(b []byte) (int, error) { - select { - case <-s.ctx.Done(): - return 0, errors.New("TODO: EOF / ErrClosed") - case <-s.dataReady: - if 0 == len(s.data) { - return s.Read(b) - } - return s.read(b) - } -} - -func (s *Server) read(b []byte) (int, error) { - // TODO mutex data while reading, against writing? - - c := len(b) // capacity - a := len(s.data) // available - n := c - - // see if the available data is smaller than the receiving buffer - if a < c { - n = a - } - - // copy available data up to capacity - for i := 0; i < n; i++ { - b[i] = s.data[i] - } - // shrink the data slice by amount read - s.data = s.data[n:] - - // if there's data left over, flag as ready to read again - // otherwise... flag as ready to write? - if len(b) > 0 { - s.dataReady <- struct{}{} - } else { - //p.writeReady <- struct{}{} - } - - // Note a read error should not be possible here - // as all traffic (including errors) can be wrapped - return n, nil -} - -// Close (TODO) should politely close all connections, if possible (set Read() to io.EOF, or use ErrClosed?) -func (s *Server) Close() error { - return errors.New("not implemented") -} diff --git a/vendor/modules.txt b/vendor/modules.txt index e27119a..c4aae61 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -7,6 +7,8 @@ git.rootprojects.org/root/go-gitver/gitver github.com/caddyserver/certmagic # github.com/cenkalti/backoff/v4 v4.0.0 github.com/cenkalti/backoff/v4 +# github.com/davecgh/go-spew v1.1.1 +github.com/davecgh/go-spew/spew # github.com/dgrijalva/jwt-go v3.2.0+incompatible ## explicit github.com/dgrijalva/jwt-go @@ -27,6 +29,7 @@ github.com/go-acme/lego/v3/challenge/tlsalpn01 github.com/go-acme/lego/v3/lego github.com/go-acme/lego/v3/log github.com/go-acme/lego/v3/platform/config/env +github.com/go-acme/lego/v3/platform/tester github.com/go-acme/lego/v3/platform/wait github.com/go-acme/lego/v3/providers/dns/duckdns github.com/go-acme/lego/v3/providers/dns/godaddy @@ -49,6 +52,12 @@ github.com/joho/godotenv/autoload github.com/klauspost/cpuid # github.com/miekg/dns v1.1.27 github.com/miekg/dns +# github.com/pmezard/go-difflib v1.0.0 +github.com/pmezard/go-difflib/difflib +# github.com/stretchr/testify v1.5.1 +## explicit +github.com/stretchr/testify/assert +github.com/stretchr/testify/require # golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 golang.org/x/crypto/ed25519 golang.org/x/crypto/ed25519/internal/edwards25519 @@ -76,3 +85,5 @@ gopkg.in/natefinch/lumberjack.v2 gopkg.in/square/go-jose.v2 gopkg.in/square/go-jose.v2/cipher gopkg.in/square/go-jose.v2/json +# gopkg.in/yaml.v2 v2.2.8 +gopkg.in/yaml.v2