2017-02-12 04:13:29 +00:00
|
|
|
package main
|
|
|
|
|
2017-03-04 18:18:08 +00:00
|
|
|
import (
|
2017-03-22 21:43:36 +00:00
|
|
|
"context"
|
2017-03-04 18:18:08 +00:00
|
|
|
"crypto/tls"
|
2020-06-02 09:32:14 +00:00
|
|
|
"errors"
|
2017-03-04 18:18:08 +00:00
|
|
|
"flag"
|
|
|
|
"fmt"
|
2017-03-30 00:28:28 +00:00
|
|
|
"io"
|
2017-03-26 22:33:40 +00:00
|
|
|
"io/ioutil"
|
2020-05-01 06:12:16 +00:00
|
|
|
golog "log"
|
2020-04-30 10:43:36 +00:00
|
|
|
"net/http"
|
2020-06-02 09:32:14 +00:00
|
|
|
"net/url"
|
2017-03-04 18:18:08 +00:00
|
|
|
"os"
|
2020-05-05 04:12:11 +00:00
|
|
|
"strconv"
|
2020-05-01 05:47:46 +00:00
|
|
|
"strings"
|
2017-02-12 04:13:29 +00:00
|
|
|
|
2020-05-01 06:12:16 +00:00
|
|
|
"git.coolaj86.com/coolaj86/go-telebitd/log"
|
2020-06-03 07:47:06 +00:00
|
|
|
telebit "git.coolaj86.com/coolaj86/go-telebitd/mplexer"
|
2020-06-02 09:32:14 +00:00
|
|
|
"git.coolaj86.com/coolaj86/go-telebitd/mplexer/dns01"
|
2020-04-30 10:43:36 +00:00
|
|
|
"git.coolaj86.com/coolaj86/go-telebitd/relay"
|
2020-05-01 05:47:46 +00:00
|
|
|
"git.coolaj86.com/coolaj86/go-telebitd/relay/api"
|
2020-04-30 10:43:36 +00:00
|
|
|
|
2020-06-01 10:16:25 +00:00
|
|
|
//jwt "github.com/dgrijalva/jwt-go"
|
2020-06-02 09:32:14 +00:00
|
|
|
"github.com/caddyserver/certmagic"
|
|
|
|
"github.com/go-acme/lego/v3/challenge"
|
2020-05-05 04:12:11 +00:00
|
|
|
"github.com/go-acme/lego/v3/providers/dns/duckdns"
|
2020-06-02 09:32:14 +00:00
|
|
|
"github.com/go-acme/lego/v3/providers/dns/godaddy"
|
2017-03-30 00:28:28 +00:00
|
|
|
lumberjack "gopkg.in/natefinch/lumberjack.v2"
|
2017-03-26 22:33:40 +00:00
|
|
|
|
2020-04-30 10:43:36 +00:00
|
|
|
_ "github.com/joho/godotenv/autoload"
|
2017-03-04 18:18:08 +00:00
|
|
|
)
|
|
|
|
|
2020-05-05 04:49:38 +00:00
|
|
|
// Loginfo TODO remove
|
2020-05-01 06:12:16 +00:00
|
|
|
var Loginfo = log.Loginfo
|
2020-05-05 04:49:38 +00:00
|
|
|
|
|
|
|
// Logdebug TODO remove
|
2020-05-01 06:12:16 +00:00
|
|
|
var Logdebug = log.Logdebug
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
log.LogFlags = golog.Ldate | golog.Lmicroseconds | golog.Lshortfile
|
|
|
|
}
|
|
|
|
|
2017-03-04 18:18:08 +00:00
|
|
|
var (
|
2017-03-27 21:09:25 +00:00
|
|
|
logfile = "stdout"
|
|
|
|
configPath = "./"
|
2020-04-30 10:43:36 +00:00
|
|
|
configFile = "telebit-relay"
|
2017-03-27 21:09:25 +00:00
|
|
|
|
2020-05-05 04:49:38 +00:00
|
|
|
tcpPort int
|
|
|
|
argDeadTime int
|
|
|
|
connectionTable *api.Table
|
|
|
|
secretKey string
|
|
|
|
wssHostName string
|
|
|
|
adminHostName string
|
|
|
|
idle int
|
|
|
|
dwell int
|
|
|
|
cancelcheck int
|
|
|
|
loadBalanceMethod api.LoadBalanceStrategy
|
|
|
|
nickname string
|
|
|
|
acmeEmail string
|
|
|
|
acmeStorage string
|
|
|
|
acmeAgree bool
|
|
|
|
acmeStaging bool
|
|
|
|
allclients string
|
2020-06-01 10:16:25 +00:00
|
|
|
authURL string
|
|
|
|
acmeRelay string
|
2017-03-04 18:18:08 +00:00
|
|
|
)
|
|
|
|
|
2017-03-26 22:33:40 +00:00
|
|
|
func init() {
|
2020-05-05 04:12:11 +00:00
|
|
|
flag.StringVar(&allclients, "clients", "", "list of client:secret pairings such as example.com:secret123,foo.com:secret321")
|
|
|
|
flag.StringVar(&acmeEmail, "acme-email", "", "email to use for Let's Encrypt / ACME registration")
|
|
|
|
flag.StringVar(&acmeStorage, "acme-storage", "./acme.d/", "path to ACME storage directory")
|
|
|
|
flag.BoolVar(&acmeAgree, "acme-agree", false, "agree to the terms of the ACME service provider (required)")
|
|
|
|
flag.BoolVar(&acmeStaging, "staging", false, "get fake certificates for testing")
|
2020-06-01 10:16:25 +00:00
|
|
|
flag.StringVar(&authURL, "auth-url", "http://localhost:3010/api", "the auth server url")
|
2020-07-17 05:41:04 +00:00
|
|
|
flag.StringVar(&acmeRelay, "acme-relay-url", "", "the ACME DNS-01 relay, if any")
|
2020-06-02 09:32:14 +00:00
|
|
|
flag.StringVar(&adminHostName, "admin-hostname", "", "the management domain")
|
2020-05-05 04:49:38 +00:00
|
|
|
flag.StringVar(&wssHostName, "wss-hostname", "", "the wss domain for connecting devices, if different from admin")
|
2017-03-27 21:09:25 +00:00
|
|
|
flag.StringVar(&configPath, "config-path", configPath, "Configuration File Path")
|
2020-05-05 04:12:11 +00:00
|
|
|
flag.StringVar(&secretKey, "secret", "", "a >= 16-character random string for JWT key signing") // SECRET
|
|
|
|
flag.StringVar(&logfile, "log", logfile, "Log file (or stdout/stderr; empty for none)")
|
|
|
|
flag.IntVar(&tcpPort, "port", 0, "tcp port on which to listen") // PORT
|
|
|
|
flag.StringVar(&nickname, "nickname", "", "a nickname for this server, as an identifier") // NICKNAME
|
2017-03-26 22:33:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
var logoutput io.Writer
|
|
|
|
|
2020-05-05 04:12:11 +00:00
|
|
|
// Client is a domain and secret pair
|
|
|
|
type Client struct {
|
|
|
|
domain string
|
|
|
|
secret string
|
|
|
|
}
|
|
|
|
|
2017-03-04 18:18:08 +00:00
|
|
|
//Main -- main entry point
|
2017-02-12 04:13:29 +00:00
|
|
|
func main() {
|
2020-06-02 09:32:14 +00:00
|
|
|
var err error
|
|
|
|
var provider challenge.Provider = nil
|
|
|
|
|
2017-03-04 18:18:08 +00:00
|
|
|
flag.Parse()
|
2020-04-30 10:43:36 +00:00
|
|
|
|
2020-05-05 04:12:11 +00:00
|
|
|
if !acmeAgree {
|
|
|
|
fmt.Fprintf(os.Stderr, "set --acme-agree=true to accept the terms of the ACME service provider.\n")
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
|
|
|
|
clients := []Client{}
|
|
|
|
for _, pair := range strings.Split(allclients, ", ") {
|
2020-05-05 04:21:01 +00:00
|
|
|
if 0 == len(pair) {
|
2020-05-05 04:12:11 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
keyval := strings.Split(pair, ":")
|
|
|
|
clients = append(clients, Client{
|
|
|
|
domain: keyval[0],
|
|
|
|
secret: keyval[1],
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2020-05-05 04:21:01 +00:00
|
|
|
if 0 == len(secretKey) {
|
|
|
|
secretKey = os.Getenv("SECRET")
|
2020-04-30 10:43:36 +00:00
|
|
|
}
|
|
|
|
if len(secretKey) < 16 {
|
|
|
|
fmt.Fprintf(os.Stderr, "Invalid secret: %q. See --help for details.\n", secretKey)
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
|
2017-03-26 22:33:40 +00:00
|
|
|
switch logfile {
|
|
|
|
case "stdout":
|
|
|
|
logoutput = os.Stdout
|
|
|
|
case "stderr":
|
|
|
|
logoutput = os.Stderr
|
|
|
|
case "":
|
|
|
|
logoutput = ioutil.Discard
|
|
|
|
default:
|
|
|
|
logoutput = &lumberjack.Logger{
|
|
|
|
Filename: logfile,
|
|
|
|
MaxSize: 100,
|
|
|
|
MaxAge: 120,
|
|
|
|
MaxBackups: 10,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// send the output io.Writing to the other packages
|
2020-05-01 06:12:16 +00:00
|
|
|
log.InitLogging(logoutput)
|
2017-03-26 22:33:40 +00:00
|
|
|
|
2020-05-05 04:12:11 +00:00
|
|
|
flag.IntVar(&argDeadTime, "dead-time-counter", 5, "deadtime counter in seconds")
|
|
|
|
|
|
|
|
if 0 == tcpPort {
|
2020-05-05 04:21:01 +00:00
|
|
|
tcpPort, _ = strconv.Atoi(os.Getenv("PORT"))
|
|
|
|
if 0 == tcpPort {
|
2020-05-05 04:12:11 +00:00
|
|
|
fmt.Fprintf(os.Stderr, "must specify --port or PORT\n")
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
2017-03-11 20:28:49 +00:00
|
|
|
}
|
|
|
|
|
2020-05-05 04:12:11 +00:00
|
|
|
if 0 == len(adminHostName) {
|
2020-05-05 04:49:38 +00:00
|
|
|
adminHostName = os.Getenv("ADMIN_HOSTNAME")
|
2020-05-05 04:12:11 +00:00
|
|
|
}
|
|
|
|
if 0 == len(wssHostName) {
|
2020-05-05 04:49:38 +00:00
|
|
|
wssHostName = os.Getenv("WSS_HOSTNAME")
|
2020-05-05 04:12:11 +00:00
|
|
|
}
|
|
|
|
if 0 == len(wssHostName) {
|
|
|
|
wssHostName = adminHostName
|
|
|
|
}
|
|
|
|
|
|
|
|
// load balancer method
|
2020-05-05 04:49:38 +00:00
|
|
|
loadBalanceMethod = api.RoundRobin
|
2020-05-05 04:12:11 +00:00
|
|
|
if 0 == len(nickname) {
|
|
|
|
nickname = os.Getenv("NICKNAME")
|
|
|
|
}
|
2017-03-04 18:18:08 +00:00
|
|
|
|
2020-05-05 04:12:11 +00:00
|
|
|
// TODO what do these "deadtimes" do exactly?
|
|
|
|
dwell := 120
|
|
|
|
idle := 60
|
|
|
|
cancelcheck := 10
|
2017-03-04 18:18:08 +00:00
|
|
|
|
2020-05-01 06:12:16 +00:00
|
|
|
Loginfo.Println("startup")
|
2017-03-04 18:18:08 +00:00
|
|
|
|
2020-06-02 09:32:14 +00:00
|
|
|
if "" != os.Getenv("GODADDY_API_KEY") {
|
|
|
|
id := os.Getenv("GODADDY_API_KEY")
|
|
|
|
secret := os.Getenv("GODADDY_API_SECRET")
|
|
|
|
if provider, err = newGoDaddyDNSProvider(id, secret); nil != err {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
} else if "" != os.Getenv("DUCKDNS_TOKEN") {
|
|
|
|
if provider, err = newDuckDNSProvider(os.Getenv("DUCKDNS_TOKEN")); nil != err {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
endpoint := acmeRelay
|
|
|
|
if strings.HasSuffix(endpoint, "/") {
|
|
|
|
endpoint = endpoint[:len(endpoint)-1]
|
|
|
|
}
|
|
|
|
endpoint += "/api/dns/"
|
|
|
|
/*
|
|
|
|
if provider, err = newAPIDNSProvider(endpoint, *token); nil != err {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
*/
|
|
|
|
panic(errors.New("allow for fetching / creating token"))
|
|
|
|
}
|
|
|
|
|
2017-03-04 18:18:08 +00:00
|
|
|
ctx, cancelContext := context.WithCancel(context.Background())
|
|
|
|
defer cancelContext()
|
|
|
|
|
2020-05-05 04:12:11 +00:00
|
|
|
// CertMagic is Greenlock for Go
|
|
|
|
directory := certmagic.LetsEncryptProductionCA
|
|
|
|
if acmeStaging {
|
|
|
|
directory = certmagic.LetsEncryptStagingCA
|
|
|
|
}
|
2020-06-02 09:32:14 +00:00
|
|
|
magic, err := newCertMagic(directory, acmeEmail, provider, &certmagic.FileStorage{Path: acmeStorage})
|
2020-05-05 04:21:01 +00:00
|
|
|
if nil != err {
|
|
|
|
fmt.Fprintf(os.Stderr, "failed to initialize certificate management (discovery url? local folder perms?): %s\n", err)
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
2020-05-05 04:12:11 +00:00
|
|
|
|
2020-05-01 05:47:46 +00:00
|
|
|
serverStatus := api.NewStatus(ctx)
|
2017-03-20 00:04:47 +00:00
|
|
|
serverStatus.AdminDomain = adminHostName
|
|
|
|
serverStatus.WssDomain = wssHostName
|
2020-04-30 10:43:36 +00:00
|
|
|
serverStatus.Name = nickname
|
2020-05-01 05:47:46 +00:00
|
|
|
serverStatus.DeadTime = api.NewStatusDeadTime(dwell, idle, cancelcheck)
|
2020-05-05 04:49:38 +00:00
|
|
|
serverStatus.LoadbalanceDefaultMethod = string(loadBalanceMethod)
|
2017-03-20 00:04:47 +00:00
|
|
|
|
2020-05-05 04:49:38 +00:00
|
|
|
connectionTable := api.NewTable(dwell, idle, loadBalanceMethod)
|
2020-04-30 10:43:36 +00:00
|
|
|
|
|
|
|
tlsConfig := &tls.Config{
|
2020-05-05 04:12:11 +00:00
|
|
|
GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
2020-05-05 04:49:38 +00:00
|
|
|
return magic.GetCertificate(hello)
|
|
|
|
/*
|
|
|
|
if false {
|
|
|
|
_, _ = magic.GetCertificate(hello)
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO
|
|
|
|
// 1. call out to greenlock for validation
|
|
|
|
// 2. push challenges through http channel
|
|
|
|
// 3. receive certificates (or don't)
|
|
|
|
certbundleT, err := tls.LoadX509KeyPair("certs/fullchain.pem", "certs/privkey.pem")
|
|
|
|
certbundle := &certbundleT
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return certbundle, nil
|
|
|
|
*/
|
2020-04-30 10:43:36 +00:00
|
|
|
},
|
|
|
|
}
|
2017-03-19 14:56:33 +00:00
|
|
|
|
2020-06-03 08:00:38 +00:00
|
|
|
authorizer := func(r *http.Request) (*telebit.Grants, error) {
|
2020-04-30 10:43:36 +00:00
|
|
|
// do we have a valid wss_client?
|
2020-05-01 05:47:46 +00:00
|
|
|
|
|
|
|
var tokenString string
|
|
|
|
if auth := strings.Split(r.Header.Get("Authorization"), " "); len(auth) > 1 {
|
|
|
|
// TODO handle Basic auth tokens as well
|
|
|
|
tokenString = auth[1]
|
|
|
|
}
|
|
|
|
if "" == tokenString {
|
2020-06-22 06:34:42 +00:00
|
|
|
// Browsers do not allow Authorization Headers and must use access_token query string
|
2020-05-01 05:47:46 +00:00
|
|
|
tokenString = r.URL.Query().Get("access_token")
|
|
|
|
}
|
2020-06-22 06:34:42 +00:00
|
|
|
if "" != r.URL.Query().Get("access_token") {
|
|
|
|
r.URL.Query().Set("access_token", "[redacted]")
|
|
|
|
}
|
2020-05-01 05:47:46 +00:00
|
|
|
|
2020-06-03 07:47:06 +00:00
|
|
|
grants, err := telebit.Inspect(authURL, tokenString)
|
2020-06-01 10:16:25 +00:00
|
|
|
/*
|
|
|
|
tok, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
|
|
|
return []byte(secretKey), nil
|
|
|
|
})
|
|
|
|
*/
|
2020-05-01 05:47:46 +00:00
|
|
|
if nil != err {
|
2020-05-05 06:44:21 +00:00
|
|
|
fmt.Println("return an error, do not go on")
|
2020-05-01 05:47:46 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
2020-06-22 06:34:42 +00:00
|
|
|
if "" != r.URL.Query().Get("access_token") {
|
|
|
|
r.URL.Query().Set("access_token", "[redacted:"+grants.Subject+"]")
|
|
|
|
}
|
|
|
|
|
2020-06-02 09:32:14 +00:00
|
|
|
/*
|
|
|
|
fmt.Printf("client claims:\n%+v\n", tok.Claims)
|
|
|
|
*/
|
2020-05-01 05:47:46 +00:00
|
|
|
|
2020-06-01 10:16:25 +00:00
|
|
|
/*
|
2020-06-03 08:00:38 +00:00
|
|
|
domains := []string{}
|
|
|
|
for _, name := range tok.Claims.(jwt.MapClaims)["domains"].([]interface{}) {
|
|
|
|
domains = append(domains, name.(string))
|
|
|
|
}
|
|
|
|
authz := &mplexy.Authz{
|
|
|
|
Domains: grants.Domains,
|
2020-06-01 10:16:25 +00:00
|
|
|
}
|
|
|
|
*/
|
2020-06-03 08:00:38 +00:00
|
|
|
return grants, err
|
2020-04-30 11:11:03 +00:00
|
|
|
|
|
|
|
/*
|
|
|
|
tokenString := r.URL.Query().Get("access_token")
|
|
|
|
result, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
|
|
|
return []byte(secretKey), nil
|
|
|
|
})
|
|
|
|
|
|
|
|
if err != nil || !result.Valid {
|
|
|
|
w.WriteHeader(http.StatusForbidden)
|
|
|
|
w.Write([]byte("Not Authorized"))
|
2020-05-01 06:12:16 +00:00
|
|
|
Loginfo.Println("access_token invalid...closing connection")
|
2020-04-30 11:11:03 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO
|
|
|
|
claims := result.Claims.(jwt.MapClaims)
|
|
|
|
domains, ok := claims["domains"].([]interface{})
|
|
|
|
*/
|
2020-04-30 10:43:36 +00:00
|
|
|
}
|
2017-03-04 18:18:08 +00:00
|
|
|
|
2020-04-30 10:43:36 +00:00
|
|
|
r := relay.New(ctx, tlsConfig, authorizer, serverStatus, connectionTable)
|
|
|
|
r.ListenAndServe(tcpPort)
|
2017-02-12 04:13:29 +00:00
|
|
|
}
|
2020-05-05 04:12:11 +00:00
|
|
|
|
2020-06-02 09:32:14 +00:00
|
|
|
func newCertMagic(
|
|
|
|
directory string,
|
|
|
|
email string,
|
|
|
|
provider challenge.Provider,
|
|
|
|
storage certmagic.Storage,
|
|
|
|
) (*certmagic.Config, error) {
|
2020-05-05 04:12:11 +00:00
|
|
|
cache := certmagic.NewCache(certmagic.CacheOptions{
|
|
|
|
GetConfigForCert: func(cert certmagic.Certificate) (*certmagic.Config, error) {
|
|
|
|
// do whatever you need to do to get the right
|
|
|
|
// configuration for this certificate; keep in
|
|
|
|
// mind that this config value is used as a
|
|
|
|
// template, and will be completed with any
|
|
|
|
// defaults that are set in the Default config
|
|
|
|
return &certmagic.Config{}, nil
|
|
|
|
},
|
|
|
|
})
|
|
|
|
magic := certmagic.New(cache, certmagic.Config{
|
|
|
|
Storage: storage,
|
|
|
|
OnDemand: &certmagic.OnDemandConfig{
|
|
|
|
DecisionFunc: func(name string) error {
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
// Ummm... just a little confusing
|
|
|
|
magic.Issuer = certmagic.NewACMEManager(magic, certmagic.ACMEManager{
|
|
|
|
DNSProvider: provider,
|
|
|
|
CA: directory,
|
|
|
|
Email: email,
|
|
|
|
Agreed: true,
|
|
|
|
DisableHTTPChallenge: true,
|
|
|
|
DisableTLSALPNChallenge: true,
|
|
|
|
// plus any other customizations you need
|
|
|
|
})
|
|
|
|
return magic, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// newDuckDNSProvider is for the sake of demoing the tunnel
|
|
|
|
func newDuckDNSProvider(token string) (*duckdns.DNSProvider, error) {
|
|
|
|
config := duckdns.NewDefaultConfig()
|
|
|
|
config.Token = token
|
|
|
|
return duckdns.NewDNSProviderConfig(config)
|
|
|
|
}
|
2020-06-02 09:32:14 +00:00
|
|
|
|
|
|
|
// newGoDaddyDNSProvider is for the sake of demoing the tunnel
|
|
|
|
func newGoDaddyDNSProvider(id, secret string) (*godaddy.DNSProvider, error) {
|
|
|
|
config := godaddy.NewDefaultConfig()
|
|
|
|
config.APIKey = id
|
|
|
|
config.APISecret = secret
|
|
|
|
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)
|
|
|
|
}
|