add DNS-01 API relay

This commit is contained in:
AJ ONeal 2020-05-27 02:53:26 -06:00
parent 1f22f5f34f
commit 710771c228
11 changed files with 690 additions and 101 deletions

1
.gitignore vendored
View File

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

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

View File

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

View File

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

22
mplexer/dns01/LICENSE Normal file
View File

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

182
mplexer/dns01/dns01.go Normal file
View File

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

296
mplexer/dns01/dns01_test.go Normal file
View File

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

View File

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

View File

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

View File

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

11
vendor/modules.txt vendored
View File

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