Cross-platform desktop application for greenhouse
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

836 lines
26 KiB

package main
import (
"bytes"
"crypto/rand"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"net/http"
"strings"
errors "git.sequentialread.com/forest/pkg-errors"
)
type ThresholdTunnelsConfig struct {
Listeners []ThresholdTunnel
ServiceToLocalAddrMap map[string]string
}
type CaddyApp struct {
//http app
HttpPort int `json:"http_port,omitempty"`
HttpsPort int `json:"https_port,omitempty"`
Servers map[string]*CaddyServer `json:"servers,omitempty"`
//tls app
Automation *CaddyTLSAutomation `json:"automation,omitempty"`
}
type CaddyAutomaticHTTPS struct {
Disable bool `json:"disable"`
}
type CaddyTLSAutomation struct {
Policies []CaddyTLSPolicy `json:"policies,omitempty"`
}
type CaddyTLSPolicy struct {
Subjects []string `json:"subjects,omitempty"`
Issuers []CaddyACMEIssuer `json:"issuers,omitempty"`
}
type CaddyACMEIssuer struct {
CA string `json:"ca"`
Email string `json:"email"`
Module string `json:"module"`
}
type CaddyServer struct {
AutomaticHTTPS *CaddyAutomaticHTTPS `json:"automatic_https,omitempty"`
Listen []string `json:"listen"`
ListenerWrappers []CaddyListenerWrapper `json:"listener_wrappers,omitempty"`
Routes []CaddyRoute `json:"routes"`
Logs *CaddyServerLogs `json:"logs,omitempty"`
}
type CaddyListenerWrapper struct {
Wrapper string `json:"wrapper"`
Timeout string `json:"timeout,omitempty"`
}
type CaddyServerLogs struct {
LoggerNames map[string]string `json:"logger_names"`
}
type CaddyRoute struct {
Handle []CaddyHandler `json:"handle,omitempty"`
Match []CaddyMatch `json:"match,omitempty"`
Terminal bool `json:"terminal,omitempty"`
}
// https://caddyserver.com/docs/json/apps/http/servers/routes/handle/
type CaddyHandler struct {
Handler string `json:"handler"`
//CaddySubrouteHandler
Routes []CaddyRoute `json:"routes,omitempty"`
//CaddyReverseProxyHandler
Upstreams []CaddyUpstream `json:"upstreams,omitempty"`
//CaddyStaticResponseHandler
Headers map[string][]string `json:"headers,omitempty"`
StatusCode int `json:"status_code,omitempty"`
//CaddyFileServerHandler
Root string `json:"root,omitempty"`
Passthrough bool `json:"pass_thru,omitempty"`
Browse *CaddyFileServerBrowse `json:"browse,omitempty"`
}
type CaddyFileServerBrowse struct {
TemplateFile string `json:"template_file,omitempty"`
}
// https://caddyserver.com/docs/json/apps/http/servers/routes/handle/reverse_proxy/
type CaddyUpstream struct {
Dial string `json:"dial"`
}
// https://caddyserver.com/docs/json/apps/http/servers/routes/match/
type CaddyMatch struct {
// match host
Host []string `json:"host,omitempty"`
// match vars regexp
VarsRegexp map[string]CaddyVarsRegexp `json:"vars_regexp,omitempty"`
// match host (sni) for layer4 plugin
TLS *CaddyMatchTLS `json:"tls,omitempty"`
}
type CaddyMatchTLS struct {
SNI []string `json:"sni,omitempty"`
}
type CaddyVarsRegexp struct {
Name string `json:"name,omitempty"`
Pattern string `json:"pattern,omitempty"`
}
type ConfigService struct {
ClientId string
ThresholdAdminBaseURL string
CaddyAdminBaseURL string
ThresholdClient *http.Client
CaddyAdminClient *http.Client
EmailAddress string
UseUnixSockets bool
}
// TODO make these configurable?
const caddyHTTPPort = 9575
const caddyHTTPSPort = 9576
const caddyTLSPort = 9577
const caddyHTTPSocketFile = "/var/run/greenhouse-daemon-caddy-http.sock"
const caddyHTTPSSocketFile = "/var/run/greenhouse-daemon-caddy-https.sock"
const caddyTLSSocketFile = "/var/run/greenhouse-daemon-caddy-tls.sock"
const caddyACMEIssuerURL = "https://acme-v02.api.letsencrypt.org/directory"
func (service *ConfigService) PrepareConfigs(tunnels []GUITunnel) (*ThresholdTunnelsConfig, *map[string]*CaddyApp, error) {
thresholdConfig := ThresholdTunnelsConfig{
Listeners: []ThresholdTunnel{},
ServiceToLocalAddrMap: map[string]string{},
}
allHostnamesMap := map[string]bool{}
for _, tunnel := range tunnels {
allHostnamesMap[service.getPrototypeListenerConfig(tunnel).ListenHostnameGlob] = true
}
allHostnames := make([]string, len(allHostnamesMap))
i := 0
for k := range allHostnamesMap {
allHostnames[i] = k
i++
}
// TODO advanced option to allow listening on 0.0.0.0?
caddyTLSListen := fmt.Sprintf("127.0.0.1:%d", caddyTLSPort)
caddyHTTPSListen := fmt.Sprintf("127.0.0.1:%d", caddyHTTPSPort)
caddyHTTPListen := fmt.Sprintf("127.0.0.1:%d", caddyHTTPPort)
if service.UseUnixSockets {
caddyTLSListen = fmt.Sprintf("unix//%s", caddyTLSSocketFile)
caddyHTTPSListen = fmt.Sprintf("unix//%s", caddyHTTPSSocketFile)
caddyHTTPListen = fmt.Sprintf("unix//%s", caddyHTTPSocketFile)
}
caddyConfig := map[string]*CaddyApp{
"layer4": &CaddyApp{
Servers: map[string]*CaddyServer{
"layer4_server_0": {
Listen: []string{caddyTLSListen},
Routes: []CaddyRoute{},
},
},
},
"http": &CaddyApp{
Servers: map[string]*CaddyServer{
"https_server": {
Listen: []string{caddyHTTPSListen},
ListenerWrappers: []CaddyListenerWrapper{
CaddyListenerWrapper{
Wrapper: "proxy_protocol",
Timeout: "5s",
},
CaddyListenerWrapper{
Wrapper: "tls",
},
},
// Logs: &CaddyServerLogs{
// LoggerNames: map[string]string{
// "*": "greenhouse-daemon-http",
// },
// },
Routes: []CaddyRoute{service.getFbclidRemovalRoute()},
},
"http_redirector": {
AutomaticHTTPS: &CaddyAutomaticHTTPS{
Disable: true,
},
Listen: []string{caddyHTTPListen},
// ListenerWrappers: []CaddyListenerWrapper{
// CaddyListenerWrapper{
// Wrapper: "proxy_protocol",
// Timeout: "5s",
// },
// CaddyListenerWrapper{
// Wrapper: "tls",
// },
// },
// // TODO disable logs for the redirector
// Logs: &CaddyServerLogs{
// LoggerNames: map[string]string{
// "*": "greenhouse-daemon-http",
// },
// },
Routes: []CaddyRoute{service.getHTTPSRedirectRoute()},
},
},
},
"tls": &CaddyApp{
Automation: &CaddyTLSAutomation{
Policies: []CaddyTLSPolicy{
CaddyTLSPolicy{
Subjects: allHostnames,
Issuers: []CaddyACMEIssuer{
CaddyACMEIssuer{
CA: caddyACMEIssuerURL,
Email: service.EmailAddress,
Module: "acme",
},
},
},
},
},
},
}
if !service.UseUnixSockets {
caddyConfig["http"].HttpPort = caddyHTTPPort
caddyConfig["http"].HttpsPort = caddyHTTPSPort
}
jsonBytes, _ := json.MarshalIndent(tunnels, "", " ")
log.Printf("tunnels: %s\n\n", string(jsonBytes))
for _, tunnel := range tunnels {
if tunnel.DestinationType == "folder" && tunnel.Protocol != "https" {
return nil, nil, errors.Errorf("the 'folder' destination type is limted to https tunnels")
}
destination := fmt.Sprintf("127.0.0.1:%d", tunnel.DestinationPort)
if tunnel.DestinationType == "host_port" {
destination = fmt.Sprintf("%s:%d", tunnel.DestinationHostname, tunnel.DestinationPort)
}
if tunnel.Protocol == "https" {
tunnelListener := service.getPrototypeListenerConfig(tunnel)
tunnelListener.ListenPort = 80
tunnelListener.BackEndService = fmt.Sprintf("%s_80", tunnel.GetServiceId())
//It's not required because all http will be redirected before it is used.
//We probably won't log http requests, just https ones
//tunnelListener.HaProxyProxyProtocol = true
thresholdConfig.Listeners = append(thresholdConfig.Listeners, tunnelListener)
thresholdConfig.ServiceToLocalAddrMap[tunnelListener.BackEndService] = caddyHTTPListen
tunnelListener = service.getPrototypeListenerConfig(tunnel)
tunnelListener.ListenPort = 443
tunnelListener.BackEndService = fmt.Sprintf("%s_443", tunnel.GetServiceId())
tunnelListener.HaProxyProxyProtocol = true
thresholdConfig.Listeners = append(thresholdConfig.Listeners, tunnelListener)
thresholdConfig.ServiceToLocalAddrMap[tunnelListener.BackEndService] = caddyHTTPSListen
if tunnel.DestinationType == "folder" {
caddyConfig["http"].Servers["https_server"].Routes = append(
caddyConfig["http"].Servers["https_server"].Routes,
CaddyRoute{
Handle: []CaddyHandler{
CaddyHandler{
Handler: "file_server",
Root: tunnel.FolderPath,
Passthrough: false,
Browse: &CaddyFileServerBrowse{},
},
},
Match: []CaddyMatch{
CaddyMatch{
Host: []string{tunnelListener.ListenHostnameGlob},
},
},
Terminal: true,
},
)
} else {
caddyConfig["http"].Servers["https_server"].Routes = append(
caddyConfig["http"].Servers["https_server"].Routes,
CaddyRoute{
Handle: []CaddyHandler{
CaddyHandler{
// I believe this handler sets X-Real-IP and X-Forwarded-For automatically :)
Handler: "reverse_proxy",
Upstreams: []CaddyUpstream{
CaddyUpstream{
Dial: destination,
},
},
},
},
Match: []CaddyMatch{
CaddyMatch{
Host: []string{tunnelListener.ListenHostnameGlob},
},
},
Terminal: true,
},
)
}
}
if tunnel.Protocol == "tls" {
tunnelListener := service.getPrototypeListenerConfig(tunnel)
thresholdConfig.Listeners = append(thresholdConfig.Listeners, tunnelListener)
thresholdConfig.ServiceToLocalAddrMap[tunnelListener.BackEndService] = caddyTLSListen
// TODO it should be possible to both read and write proxy protocol here...
// 1. add an option to caddy-layer4 to read proxy protocol by wrapping the TCP listener
// 2. caddy-layer4 terminate TLS by wrapping the TCP listener (caddy-layer4 already does this)
// 3. forward proxy protocol to the backend (caddy-layer4 already supports this)
// However, this is more of an edge case -- probably not needed by 99% of users.
// Biggest usecase would probably be email, but not really cuz email has to go via TCP anyways due to STARTTLS
caddyConfig["layer4"].Servers["layer4_server_0"].Routes = append(
caddyConfig["layer4"].Servers["layer4_server_0"].Routes,
CaddyRoute{
Handle: []CaddyHandler{
CaddyHandler{
Handler: "tls",
},
CaddyHandler{
Handler: "proxy",
Upstreams: []CaddyUpstream{
CaddyUpstream{
Dial: destination,
},
},
},
},
Match: []CaddyMatch{
CaddyMatch{
TLS: &CaddyMatchTLS{
SNI: []string{tunnelListener.ListenHostnameGlob},
},
},
},
},
)
}
if tunnel.Protocol == "tcp" {
tunnelListener := service.getPrototypeListenerConfig(tunnel)
tunnelListener.ListenHostnameGlob = ""
thresholdConfig.Listeners = append(thresholdConfig.Listeners, tunnelListener)
// TODO user specifying PROXY protocol should be supported here.
thresholdConfig.ServiceToLocalAddrMap[tunnelListener.BackEndService] = destination
// no caddy config, TCP goes direct from threshold to the destination service
}
}
return &thresholdConfig, &caddyConfig, nil
}
func (service *ConfigService) ConfigureThreshold(thresholdConfig *ThresholdTunnelsConfig) error {
url := fmt.Sprintf("%s/liveconfig", service.ThresholdAdminBaseURL)
jsonBytes, err := json.Marshal(thresholdConfig)
if err != nil {
return errors.Wrapf(err, "error serializing threshold json for ConfigService.Configure", url)
}
request, err := http.NewRequest("PUT", url, bytes.NewReader(jsonBytes))
if err != nil {
return errors.Wrapf(err, "error constructing http request to threshold (%s) for ConfigService.Configure", url)
}
response, err := service.ThresholdClient.Do(request)
if err != nil {
return errors.Wrapf(err, "error sending http request to threshold (%s) for ConfigService.Configure", url)
}
bodyBytes, err := ioutil.ReadAll(response.Body)
if err != nil {
return errors.Wrapf(err, "http read error on request to threshold (%s) for ConfigService.Configure", url)
}
if response.StatusCode != 200 {
return errors.Errorf(
"ConfigService.Configure: got HTTP %d %s from threshold (%s): %s",
response.StatusCode, response.Status, url, string(bodyBytes),
)
}
return nil
}
func (service *ConfigService) TestThreshold(thresholdConfig *ThresholdTunnelsConfig) error {
url := fmt.Sprintf("%s/start_test", service.ThresholdAdminBaseURL)
response, err := service.ThresholdClient.Get(url)
if err != nil {
return err
}
if response.StatusCode != http.StatusOK {
responseContent := "http read error when trying to read error message"
responseBytes, err := ioutil.ReadAll(response.Body)
if err == nil {
responseContent = string(responseBytes)
}
return errors.Errorf("calling /start_test on the threshold client returned HTTP %d %s: %s", response.StatusCode, response.Status, responseContent)
}
// TODO get thresholdServerAddress from the threshold admin api
thresholdServerAddress := ""
sentTokens := map[string]string{}
listenerConfigsByToken := map[string]ThresholdTunnel{}
for _, listenerConfig := range thresholdConfig.Listeners {
// only test the tunnels belonging to this threshold client.
if listenerConfig.ClientId != service.ClientId {
continue
}
tokenBuffer := make([]byte, 8)
rand.Read(tokenBuffer)
token := fmt.Sprintf("%x", tokenBuffer)
sentTokens[token] = "sent"
listenerConfigsByToken[token] = listenerConfig
if listenerConfig.ListenHostnameGlob != "" && listenerConfig.ListenHostnameGlob != "*" {
hostnames := strings.Split(listenerConfig.ListenHostnameGlob, ",")
for _, hostname := range hostnames {
specificHostname := strings.ReplaceAll(hostname, "*", "a")
if listenerConfig.ListenPort != 80 {
remoteAddress := fmt.Sprintf("%s:%d", specificHostname, listenerConfig.ListenPort)
connection, err := tls.Dial("tcp", remoteAddress, &tls.Config{InsecureSkipVerify: true})
if err != nil {
log.Printf("TestThreshold(): connecting to threshold server '%s' returned %s", remoteAddress, err)
return err
}
connection.Write([]byte(token))
responseBuffer := make([]byte, 16)
_, err = connection.Read(responseBuffer)
if err != nil && err != io.EOF {
log.Printf("TestThreshold(): threshold server '%s' read error %s", remoteAddress, err)
return err
}
if string(responseBuffer) != token {
return errors.Errorf("sent test token '%s' to tls://%s, recieved '%s' in response", token, remoteAddress, string(responseBuffer))
}
} else {
url := fmt.Sprintf(fmt.Sprintf("http://%s/%s", specificHostname, token))
response, err := http.Get(url)
if err != nil {
return err
}
if response.StatusCode != http.StatusOK {
return errors.Errorf("sent test token '%s' to %s, recieved HTTP %d %s", token, url, response.StatusCode, response.Status)
}
responseBytes, err := ioutil.ReadAll(response.Body)
if err != nil {
return err
}
if string(responseBytes) != token {
return errors.Errorf("sent test token '%s' to %s, recieved '%s' in response", token, url, string(responseBytes))
}
}
}
} else {
tcpAddressString := fmt.Sprintf("%s:%d", thresholdServerAddress, listenerConfig.ListenPort)
tcpAddress, err := net.ResolveTCPAddr("tcp", tcpAddressString)
if err != nil {
return err
}
connection, err := net.DialTCP("tcp", nil, tcpAddress)
if err != nil {
return err
}
connection.Write([]byte(token))
responseBuffer := make([]byte, 16)
_, err = connection.Read(responseBuffer)
if err != nil && err != io.EOF {
return err
}
if string(responseBuffer) != token {
return errors.Errorf("sent test token '%s' to tcp://%s, recieved '%s' in response", token, tcpAddressString, string(responseBuffer))
}
}
}
url = fmt.Sprintf("%s/end_test", service.ThresholdAdminBaseURL)
response, err = service.ThresholdClient.Get(url)
if err != nil {
return err
}
responseBytes, err := ioutil.ReadAll(response.Body)
if err != nil {
return errors.Wrap(err, "trying to read /end_test response")
}
if response.StatusCode != http.StatusOK {
return errors.Errorf("calling /start_test on the threshold client returned HTTP %d %s: %s", response.StatusCode, response.Status, string(responseBytes))
}
lines := strings.Split(string(responseBytes), "\n")
for _, line := range lines {
if len(strings.TrimSpace(line)) > 0 {
state, hasState := sentTokens[line]
if !hasState {
jsonBytes, _ := json.MarshalIndent(listenerConfigsByToken, "", " ")
return errors.Errorf("malformed token '%s' was returned by threshold test. configured test tokens were: %s ", line, string(jsonBytes))
}
if state != "sent" {
return errors.Errorf("token '%s' (%s:%d) was returned by threshold test more than once", line, listenerConfigsByToken[line].ListenHostnameGlob, listenerConfigsByToken[line].ListenPort)
}
sentTokens[line] = "recieved"
}
}
for token, result := range sentTokens {
if result != "recieved" {
return errors.Errorf("token '%s' (%s:%d) was never returned by threshold test", token, listenerConfigsByToken[token].ListenHostnameGlob, listenerConfigsByToken[token].ListenPort)
}
}
return nil
}
func (service *ConfigService) ConfigureCaddy(caddyConfig *map[string]*CaddyApp) error {
url := fmt.Sprintf("%s/config/apps", service.CaddyAdminBaseURL)
jsonBytes, err := json.Marshal(caddyConfig)
if err != nil {
return errors.Wrapf(err, "error serializing caddy json for ConfigService.Configure", url)
}
log.Printf("Caddy Config: %s\n\n", string(jsonBytes))
request, err := http.NewRequest("POST", url, bytes.NewReader(jsonBytes))
if err != nil {
return errors.Wrapf(err, "error constructing http request to caddy (%s) for ConfigService.Configure", url)
}
request.Header.Add("Content-Type", "application/json")
response, err := service.CaddyAdminClient.Do(request)
if err != nil {
return errors.Wrapf(err, "error sending http request to caddy (%s) for ConfigService.Configure", url)
}
bodyBytes, err := ioutil.ReadAll(response.Body)
if err != nil {
return errors.Wrapf(err, "http read error on request to caddy (%s) for ConfigService.Configure", url)
}
if response.StatusCode != 200 {
return errors.Errorf(
"ConfigService.Configure: got HTTP %d %s from caddy (%s): %s",
response.StatusCode, response.Status, url, string(bodyBytes),
)
}
return nil
}
func (service *ConfigService) EnsureCaddyACMECompletes(thresholdConfig *ThresholdTunnelsConfig) error {
// TODO poll for certs present in:
// root@thingpad:/opt/greenhouse-daemon/caddyData/caddy/certificates# ls acme-v02.api.letsencrypt.org-directory
// test1.forest-n-johnson.greenhouseusers.com
// TODO test threshold tunnels before setting up caddy.
return nil
}
func (service *ConfigService) TestFinalTunnels(tunnels []GUITunnel) error {
// TODO test each tunnel
return nil
}
func (service *ConfigService) getPrototypeListenerConfig(tunnel GUITunnel) ThresholdTunnel {
domain := tunnel.Domain
if tunnel.HasSubdomain {
domain = fmt.Sprintf("%s.%s", tunnel.Subdomain, domain)
}
return ThresholdTunnel{
ListenAddress: "0.0.0.0",
ListenHostnameGlob: domain,
ListenPort: tunnel.PublicPort,
BackEndService: tunnel.GetServiceId(),
ClientId: service.ClientId,
}
}
func (service *ConfigService) getHTTPSRedirectRoute() CaddyRoute {
// because we want to be able to listen on unix sockets we have to do the http and https servers separately,
// which means we have to do our own https redirect
return CaddyRoute{
Handle: []CaddyHandler{
CaddyHandler{
Handler: "static_response",
Headers: map[string][]string{
"Location": []string{"https://{http.regexp.https_redirect.1}"},
},
StatusCode: 301,
},
},
Match: []CaddyMatch{
CaddyMatch{
VarsRegexp: map[string]CaddyVarsRegexp{
"{http.request.uri}": CaddyVarsRegexp{
Name: "https_redirect",
Pattern: "^http://(.*)$",
},
},
},
},
}
}
func (service *ConfigService) getFbclidRemovalRoute() CaddyRoute {
// facebook adds this evil ?fbclid=xyz request parameter whenever someone clicks a link
// this handler will match all requests that have this parameter
// and it will redirect to the same URI with the parameter removed
return CaddyRoute{
Handle: []CaddyHandler{
CaddyHandler{
Handler: "static_response",
Headers: map[string][]string{
"Location": []string{"{http.regexp.fbclid_regex.1}"},
},
StatusCode: 302,
},
},
Match: []CaddyMatch{
CaddyMatch{
VarsRegexp: map[string]CaddyVarsRegexp{
"{http.request.uri}": CaddyVarsRegexp{
Name: "fbclid_regex",
Pattern: "^(.*?)([?&]fbclid=[a-zA-Z0-9_-]+)$",
},
},
},
},
}
}
// func (service *ConfigService) getGreenhouseHTTPClient() (*http.Client, error) {
// if service.GreenhouseHTTPClient != nil {
// return service.GreenhouseHTTPClient, nil
// }
// cert, err := tls.X509KeyPair([]byte(service.Config.ClientTlsCertificate), []byte(service.Config.ClientTlsKey))
// if err != nil {
// return nil, err
// }
// caCertPool := x509.NewCertPool()
// ok := caCertPool.AppendCertsFromPEM([]byte(service.Config.CaCertificate))
// if !ok {
// return nil, errors.New("Failed to add config.CaCertificate to cert pool")
// }
// tlsClientConfig := &tls.Config{
// Certificates: []tls.Certificate{cert},
// RootCAs: caCertPool,
// }
// tlsClientConfig.BuildNameToCertificate()
// toReturn := &http.Client{
// Transport: &http.Transport{
// TLSClientConfig: tlsClientConfig,
// },
// Timeout: 10 * time.Second,
// }
// service.GreenhouseHTTPClient = toReturn
// return toReturn, nil
// }
// func (service *ConfigService) Configure(tunnels []GUITunnel) error {
// caddyConfig := map[string]*CaddyApp{}
// publicPorts := map[int][]*GUITunnel{}
// for _, tunnel := range tunnels {
// if tunnel.DestinationType == "host_port" {
// return fmt.Errorf(
// "tunnel %s is missing the required public port field",
// tunnel.ContainerName,
// )
// }
// if _, has := publicPorts[tunnel.PublicPort]; !has {
// publicPorts[tunnel.PublicPort] = []*GUITunnel{}
// }
// publicPorts[tunnel.PublicPort] = append(publicPorts[tunnel.PublicPort], tunnel)
// }
// // TODO sort public ports once we get that far and have more than one
// for port, containerConfigs := range publicPorts {
// if port == 443 {
// allHostnames := []string{}
// for _, container := range containerConfigs {
// allHostnames = append(allHostnames, strings.Split(container.PublicHostnames, ",")...)
// }
// sort.Strings(allHostnames)
// caddyConfig["http"] = &CaddyApp{
// Servers: map[string]*CaddyServer{
// "srv0": {
// Listen: []string{":443"},
// Logs: &CaddyServerLogs{
// LoggerNames: map[string]string{
// "*": "goatcounter",
// },
// },
// Routes: []CaddyRoute{fbclidRoute},
// },
// },
// }
// if CADDY_ACME_ISSUER_URL != "" && CADDY_ACME_CLIENT_EMAIL_ADDRESS != "" {
// caddyConfig["tls"] =
// }
// // sort them so that the json always comes out the same & easier to compare (canonical)
// sort.Slice(containerConfigs, func(i, j int) bool {
// return containerConfigs[i].ContainerName < containerConfigs[j].ContainerName
// })
// for _, container := range containerConfigs {
// if container.PublicHostnames != "" {
// newRoute := CaddyRoute{
// Handle: []CaddyHandler{
// // this handler is just here to standardize the favicon (or any other universal static file)
// // across the sites
// CaddyHandler{
// Handler: "file_server",
// Root: FAVICON_DIRECTORY,
// Passthrough: true,
// },
// CaddyHandler{
// Handler: "reverse_proxy",
// Upstreams: []CaddyUpstream{
// CaddyUpstream{
// Dial: container.ContainerAddress,
// },
// },
// },
// },
// Match: []CaddyMatch{
// CaddyMatch{
// Host: strings.Split(container.PublicHostnames, ","),
// },
// },
// Terminal: true,
// }
// caddyConfig["http"].Servers["srv0"].Routes = append(
// caddyConfig["http"].Servers["srv0"].Routes,
// newRoute,
// )
// }
// }
// } else {
// // TODO support TCP and TLS (udp??)
// return fmt.Errorf(
// "unsupported public-port %d on container '%s'. currently only https is supported",
// port, containerConfigs[0].ContainerName,
// )
// }
// }
// caddyConfigBytes, _ := json.MarshalIndent(caddyConfig, "", " ")
// caddyResponseString := ""
// _, caddyResponseBytes, err := unixHTTP(
// caddyHTTPClient, CADDY_SOCKET, "GET", "/config/apps", caddyConfigBytes,
// )
// if err != nil {
// caddyResponseString = string(caddyResponseBytes)
// }
// caddyConfigIsEmpty := caddyResponseString == "null" || caddyResponseString == "[]"
// if caddyConfigIsEmpty || !byteArraysEqual(caddyConfigBytes, previousCaddyConfigBytes) {
// log.Println("!byteArraysEqual(caddyConfigBytes, previousCaddyConfigBytes)")
// log.Println(".")
// log.Println(".")
// log.Println(".")
// log.Printf("==================================\nOLD\n%s\n\n", string(previousCaddyConfigBytes))
// log.Printf("==================================\nNEW\n%s\n\n", string(caddyConfigBytes))
// log.Println("")
// log.Println("")
// log.Println("")
// caddyResponse, caddyResponseBytes, err := unixHTTP(
// caddyHTTPClient, CADDY_SOCKET, "POST", "/config/apps", caddyConfigBytes,
// )
// if err != nil {
// return errors.Wrap(err, "failed to call caddy admin api")
// }
// if caddyResponse.StatusCode != http.StatusOK {
// return fmt.Errorf("caddy admin api returned HTTP %d: %s", caddyResponse.StatusCode, string(caddyResponseBytes))
// }
// previousCaddyConfigBytes = caddyConfigBytes
// }
// // jsonBytes, err := json.Marshal(configUpdate)
// return nil
// }