add DNS-01 API relay
This commit is contained in:
parent
1f22f5f34f
commit
710771c228
|
@ -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
|
||||
|
|
1
go.mod
1
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
|
||||
)
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
endpoint := *acmeRelay
|
||||
if strings.HasSuffix(endpoint, "/") {
|
||||
endpoint = endpoint[:len(endpoint)-1]
|
||||
}
|
||||
|
||||
if "" == *relay {
|
||||
*relay = os.Getenv("RELAY") // "wss://example.com:443"
|
||||
}
|
||||
if "" == *token {
|
||||
if "" == *secret {
|
||||
*secret = os.Getenv("SECRET")
|
||||
}
|
||||
*token, err = getToken(*secret, domains)
|
||||
}
|
||||
if nil != err {
|
||||
endpoint += "/api/dns/"
|
||||
if provider, err = newAPIDNSProvider(endpoint, *token); 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)
|
||||
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 {
|
||||
fmt.Println("relay:", relay)
|
||||
log.Fatal(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 {
|
||||
|
|
|
@ -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.
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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")
|
||||
}
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue