🌱🏠😈 Common background service doing the heavy lifting for various user-facing greenhouse client applications
https://greenhouse.server.garden/
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.
705 lines
22 KiB
705 lines
22 KiB
package main |
|
|
|
import ( |
|
"bytes" |
|
"crypto/rand" |
|
"crypto/tls" |
|
"encoding/json" |
|
"fmt" |
|
"io" |
|
"io/ioutil" |
|
"net" |
|
"net/http" |
|
"regexp" |
|
"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"` |
|
DisableRedirects bool `json:"disable_redirects"` |
|
} |
|
|
|
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 |
|
TelemetryID string |
|
} |
|
|
|
// 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) { |
|
|
|
sourceURLs := map[string]int{} |
|
tunnelsWithInvalidSubdomains := []string{} |
|
// TODO: support wildcard subdomains "^([A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)|\\*$" |
|
// Right now wildcard subdomains are blocked on Caddy requiring the dns acme challenge for them. |
|
subdomainRegex := regexp.MustCompile("^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$") |
|
for _, tunnel := range tunnels { |
|
sourceURL := tunnel.GetStringDisplay(true) |
|
sourceURLs[sourceURL]++ |
|
if tunnel.HasSubdomain && !subdomainRegex.MatchString(tunnel.Subdomain) { |
|
tunnelsWithInvalidSubdomains = append(tunnelsWithInvalidSubdomains, sourceURL) |
|
} |
|
} |
|
duplicatedSources := []string{} |
|
for k, v := range sourceURLs { |
|
if v > 1 { |
|
duplicatedSources = append(duplicatedSources, k) |
|
} |
|
} |
|
errorStrings := []string{} |
|
if len(duplicatedSources) > 0 { |
|
errorStrings = append(errorStrings, fmt.Sprintf("multiple tunnels exist with the URL(s) %s", strings.Join(duplicatedSources, ", "))) |
|
} |
|
if len(tunnelsWithInvalidSubdomains) > 0 { |
|
errorStrings = append(errorStrings, fmt.Sprintf("the tunnel(s) %s have invalid subdomains", strings.Join(tunnelsWithInvalidSubdomains, ", "))) |
|
|
|
} |
|
if len(errorStrings) > 0 { |
|
return nil, nil, errors.New(strings.Join(errorStrings, ", \n")) |
|
} |
|
|
|
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": { |
|
Servers: map[string]*CaddyServer{ |
|
"layer4_server_0": { |
|
Listen: []string{caddyTLSListen}, |
|
Routes: []CaddyRoute{}, |
|
}, |
|
}, |
|
}, |
|
"http": { |
|
Servers: map[string]*CaddyServer{ |
|
"https_server": { |
|
AutomaticHTTPS: &CaddyAutomaticHTTPS{ |
|
DisableRedirects: true, |
|
}, |
|
Listen: []string{caddyHTTPSListen}, |
|
ListenerWrappers: []CaddyListenerWrapper{ |
|
{ |
|
Wrapper: "proxy_protocol", |
|
Timeout: "5s", |
|
}, |
|
{ |
|
Wrapper: "tls", |
|
}, |
|
}, |
|
// Logs: { |
|
// LoggerNames: map[string]string{ |
|
// "*": "greenhouse-daemon-http", |
|
// }, |
|
// }, |
|
Routes: []CaddyRoute{service.getFbclidRemovalRoute()}, |
|
}, |
|
"http_redirector": { |
|
AutomaticHTTPS: &CaddyAutomaticHTTPS{ |
|
Disable: true, |
|
}, |
|
Listen: []string{caddyHTTPListen}, |
|
// ListenerWrappers: []CaddyListenerWrapper{ |
|
// { |
|
// Wrapper: "proxy_protocol", |
|
// Timeout: "5s", |
|
// }, |
|
// { |
|
// Wrapper: "tls", |
|
// }, |
|
// }, |
|
// // TODO disable logs for the redirector |
|
// Logs: &CaddyServerLogs{ |
|
// LoggerNames: map[string]string{ |
|
// "*": "greenhouse-daemon-http", |
|
// }, |
|
// }, |
|
Routes: []CaddyRoute{service.getHTTPSRedirectRoute()}, |
|
}, |
|
}, |
|
}, |
|
"tls": { |
|
Automation: &CaddyTLSAutomation{ |
|
Policies: []CaddyTLSPolicy{ |
|
{ |
|
Subjects: allHostnames, |
|
Issuers: []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{ |
|
{ |
|
Handler: "file_server", |
|
Root: tunnel.DestinationFolderPath, |
|
Passthrough: false, |
|
Browse: &CaddyFileServerBrowse{}, |
|
}, |
|
}, |
|
Match: []CaddyMatch{ |
|
{ |
|
Host: []string{tunnelListener.ListenHostnameGlob}, |
|
}, |
|
}, |
|
Terminal: true, |
|
}, |
|
) |
|
} else { |
|
caddyConfig["http"].Servers["https_server"].Routes = append( |
|
caddyConfig["http"].Servers["https_server"].Routes, |
|
CaddyRoute{ |
|
Handle: []CaddyHandler{ |
|
{ |
|
// I believe this handler sets X-Real-IP and X-Forwarded-For automatically :) |
|
Handler: "reverse_proxy", |
|
Upstreams: []CaddyUpstream{ |
|
{ |
|
Dial: destination, |
|
}, |
|
}, |
|
}, |
|
}, |
|
Match: []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{ |
|
{ |
|
Handler: "tls", |
|
}, |
|
{ |
|
Handler: "proxy", |
|
Upstreams: []CaddyUpstream{ |
|
{ |
|
Dial: destination, |
|
}, |
|
}, |
|
}, |
|
}, |
|
Match: []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, defaultDomain string) 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) |
|
} |
|
|
|
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 |
|
} |
|
|
|
// bytez, _ := json.MarshalIndent(listenerConfig, "", " ") |
|
// log.Printf("TestThreshold(): listenerConfig = '%s'", string(bytez)) |
|
|
|
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 specificHostname == "" { |
|
specificHostname = defaultDomain |
|
} |
|
|
|
if listenerConfig.ListenPort != 80 { |
|
remoteAddress := fmt.Sprintf("%s:%d", specificHostname, listenerConfig.ListenPort) |
|
//log.Printf("TestThreshold(): remoteAddress = '%s'", remoteAddress) |
|
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", defaultDomain, 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 or incorrect 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") |
|
|
|
// TODO if/when this needs to happen... ??? |
|
// caddy may fail to accept the new config if it cannot create these files. they must not already exist... sometimes?? |
|
// however if we delete them every time, it also doesn't seem to work; they wont always be re-created. |
|
// |
|
// if service.UseUnixSockets { |
|
// os.Remove(caddyHTTPSocketFile) |
|
// os.Remove(caddyHTTPSSocketFile) |
|
// os.Remove(caddyTLSSocketFile) |
|
// } |
|
|
|
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 |
|
|
|
// 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{ |
|
{ |
|
Handler: "static_response", |
|
Headers: map[string][]string{ |
|
"Location": {"https://{http.request.host}{http.request.uri}"}, |
|
}, |
|
StatusCode: 301, |
|
}, |
|
}, |
|
} |
|
} |
|
|
|
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{ |
|
{ |
|
Handler: "static_response", |
|
Headers: map[string][]string{ |
|
"Location": {"{http.regexp.fbclid_regex.1}"}, |
|
}, |
|
StatusCode: 302, |
|
}, |
|
}, |
|
Match: []CaddyMatch{ |
|
{ |
|
VarsRegexp: map[string]CaddyVarsRegexp{ |
|
"{http.request.uri}": { |
|
Name: "fbclid_regex", |
|
Pattern: "^(.*?)([?&]fbclid=[a-zA-Z0-9_-]+)$", |
|
}, |
|
}, |
|
}, |
|
}, |
|
} |
|
}
|
|
|