🌱🏠😈 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.
1245 lines
45 KiB
1245 lines
45 KiB
package main |
|
|
|
import ( |
|
"context" |
|
"crypto/rand" |
|
"crypto/sha256" |
|
"crypto/tls" |
|
"crypto/x509" |
|
"encoding/json" |
|
"encoding/pem" |
|
"fmt" |
|
"io/ioutil" |
|
"net" |
|
"net/http" |
|
"os" |
|
"path" |
|
"path/filepath" |
|
"regexp" |
|
"runtime" |
|
"strconv" |
|
"strings" |
|
"sync" |
|
"time" |
|
|
|
"git.sequentialread.com/forest/easypki.git/pkg/easypki" |
|
easypkiStore "git.sequentialread.com/forest/easypki.git/pkg/store" |
|
netstat "git.sequentialread.com/forest/go-netstat" |
|
child "git.sequentialread.com/forest/greenhouse-daemon/child-process-service" |
|
greenhousePKI "git.sequentialread.com/forest/greenhouse/pki" |
|
errors "git.sequentialread.com/forest/pkg-errors" |
|
) |
|
|
|
type DaemonConfig struct { |
|
APIToken string |
|
ServerName string |
|
TunnelsEnabled bool |
|
GUITunnels []GUITunnel |
|
} |
|
|
|
type DaemonAPI struct { |
|
Config *DaemonConfig |
|
ConfigurationMutex sync.Mutex |
|
ConfigService *ConfigService |
|
TenantInfo *TenantInfo |
|
HTTPClient *http.Client |
|
DaemonPath string |
|
CloudURL string |
|
UseUnixSockets bool |
|
CACertFile string |
|
TLSCertFile string |
|
TLSKeyFile string |
|
|
|
ThresholdService *child.ChildProcessService |
|
CaddyService *child.ChildProcessService |
|
LogManager child.LogManager |
|
ApplyConfigStatuses []string |
|
ApplyConfigStatusIndex int |
|
ApplyConfigStatusError string |
|
|
|
TelemetryID string |
|
} |
|
|
|
type ThresholdConfig struct { |
|
DebugLog bool |
|
ClientId string |
|
GreenhouseDomain string |
|
GreenhouseAPIToken string |
|
GreenhouseThresholdPort int |
|
CaCertificate string |
|
// TODO rename Tls to TLS |
|
ClientTlsKey string |
|
ClientTlsCertificate string |
|
AdminUnixSocket string |
|
AdminAPIPort int |
|
AdminAPICACertificateFile string |
|
AdminAPITlsKeyFile string |
|
AdminAPITlsCertificateFile string |
|
// TODO Metrics MetricsConfig |
|
} |
|
|
|
type Status struct { |
|
NeedsAPIToken bool `json:"needs_api_token"` |
|
HashedToken string `json:"hashed_api_token"` |
|
Threshold *child.ChildProcessStatus `json:"threshold"` |
|
Caddy *child.ChildProcessStatus `json:"caddy"` |
|
ApplyConfigStatuses []string `json:"apply_config_statuses"` |
|
ApplyConfigStatusIndex int `json:"apply_config_status_index"` |
|
ApplyConfigStatusError string `json:"apply_config_status_error"` |
|
TenantInfo *TenantInfo `json:"tenant_info"` |
|
ServerName string `json:"server_name"` |
|
GUITunnels []GUITunnel `json:"tunnels"` |
|
UpdateTenantInfoMessage string `json:"update_tenant_info_message"` |
|
DaemonTelemetryID string `json:"daemon_telemetry_id"` |
|
} |
|
|
|
type TenantInfo struct { |
|
ThresholdServers []string |
|
ClientStates map[string]ThresholdClientState |
|
Listeners []ThresholdTunnel |
|
AuthorizedDomains []string |
|
PortStart int |
|
PortEnd int |
|
EmailAddress string |
|
} |
|
|
|
type GUITunnel struct { |
|
Protocol string `json:"protocol"` |
|
HasSubdomain bool `json:"has_subdomain"` |
|
Subdomain string `json:"subdomain"` |
|
Domain string `json:"domain"` |
|
PublicPort int `json:"public_port"` |
|
DestinationType string `json:"destination_type"` |
|
DestinationHostname string `json:"destination_hostname"` |
|
DestinationPort int `json:"destination_port"` |
|
DestinationFolderPath string `json:"destination_folder_path"` |
|
} |
|
|
|
type ListeningSocket struct { |
|
LocalAddr string |
|
RemoteAddr string |
|
State string |
|
PID int |
|
ProcessCommand string |
|
} |
|
|
|
var nonAlphanumericRegexp *regexp.Regexp |
|
|
|
func (tunnel *GUITunnel) GetServiceId() string { |
|
if nonAlphanumericRegexp == nil { |
|
nonAlphanumericRegexp = regexp.MustCompile("[^a-zA-Z0-9_-]+") |
|
} |
|
destination := fmt.Sprintf("localhost_%d", tunnel.DestinationPort) |
|
if tunnel.DestinationType == "host_port" { |
|
hostname := strings.ToLower(tunnel.DestinationHostname) |
|
hostname = nonAlphanumericRegexp.ReplaceAllString(hostname, "_") |
|
destination = fmt.Sprintf("%s_%d", hostname, tunnel.DestinationPort) |
|
} |
|
if tunnel.DestinationType == "folder" { |
|
hashBytes := sha256.Sum256([]byte(fmt.Sprintf("094wbnmpj905=-0m24jh65e0-j,bfdio5490h6%s", tunnel.DestinationFolderPath))) |
|
destination = fmt.Sprintf("%x", hashBytes[10:20]) |
|
} |
|
return fmt.Sprintf("autogenerated_%s_%s", tunnel.DestinationType, destination) |
|
} |
|
|
|
func (tunnel *GUITunnel) GetStringDisplay(omitDestination bool) string { |
|
domain := tunnel.Domain |
|
if tunnel.HasSubdomain { |
|
if tunnel.Subdomain == "" { |
|
domain = fmt.Sprintf("<blank>.%s", domain) |
|
} else { |
|
domain = fmt.Sprintf("%s.%s", tunnel.Subdomain, domain) |
|
} |
|
} |
|
portPart := "" |
|
if tunnel.Protocol != "https" { |
|
portPart = fmt.Sprintf(":%d", tunnel.PublicPort) |
|
} |
|
sourcePart := fmt.Sprintf("%s://%s%s", tunnel.Protocol, domain, portPart) |
|
if omitDestination { |
|
return sourcePart |
|
} |
|
|
|
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.DestinationType == "folder" { |
|
destination = tunnel.DestinationFolderPath |
|
} |
|
|
|
return fmt.Sprintf("%s -> %s", sourcePart, destination) |
|
} |
|
|
|
type ThresholdTunnel struct { |
|
ClientId string |
|
ListenPort int |
|
ListenAddress string |
|
ListenHostnameGlob string |
|
BackEndService string |
|
HaProxyProxyProtocol bool |
|
} |
|
|
|
type ThresholdClientState struct { |
|
CurrentState string |
|
LastState string |
|
} |
|
|
|
const greenhouseThresholdPort = 9056 |
|
const daemonAdminPort = 9572 |
|
const daemonSocketFile = "/var/run/greenhouse-daemon.sock" |
|
|
|
const thresholdAdminPort = 9573 |
|
const thresholdAdminSocketFile = "/var/run/greenhouse-daemon-threshold.sock" |
|
|
|
// TODO this is currently hardcoded in caddy-config.json |
|
const caddyAdminPort = 9574 |
|
const caddyAdminSocketFile = "/var/run/greenhouse-daemon-caddy-admin.sock" |
|
|
|
const mainCAName = "greenhouse_daemon_localhost_ca" |
|
|
|
var log child.LogManager |
|
var daemonTelemetryId string |
|
var daemonTelemetryAccount string |
|
|
|
func main() { |
|
|
|
var err error |
|
daemonPath := os.Getenv("GREENHOUSE_DAEMON_PATH") |
|
daemonExecutablePath := daemonPath |
|
if daemonPath == "" { |
|
if runtime.GOOS == "linux" || runtime.GOOS == "bsd" { |
|
daemonPath = "/opt/greenhouse-daemon" |
|
} else if runtime.GOOS == "darwin" { |
|
daemonPath = "/Library/Application Support/greenhouse-daemon" |
|
} else if runtime.GOOS == "windows" { |
|
daemonPath = fmt.Sprintf(`%s\greenhouse-background-service`, os.Getenv("ProgramData")) |
|
daemonExecutablePath = "C:\\Program Files (x86)\\greenhouse\\background-service" |
|
executableLocation, err := os.Executable() |
|
if err == nil { |
|
daemonExecutablePath = filepath.Dir(executableLocation) |
|
} |
|
} else { |
|
fatalfWithTelemetry("can't start the greenhouse-daemon because operating system '%s' is not supported yet\n\n", runtime.GOOS) |
|
} |
|
} |
|
|
|
log = child.NewDaemonLogManager(daemonPath, filepath.Join(daemonPath, "daemon")) |
|
go log.ConsumeFromChannel() |
|
|
|
cloudURL := os.Getenv("GREENHOUSE_DAEMON_CLOUD_URL") |
|
if cloudURL == "" { |
|
cloudURL = "https://greenhouse-alpha.server.garden" |
|
} |
|
|
|
useUnixSockets := false |
|
useUnixSocketsString := strings.ToLower(os.Getenv("GREENHOUSE_DAEMON_USE_UNIX_SOCKETS")) |
|
if useUnixSocketsString == "yes" || useUnixSocketsString == "true" || useUnixSocketsString == "1" || useUnixSocketsString == "t" || useUnixSocketsString == "y" { |
|
useUnixSockets = true |
|
} |
|
|
|
thresholdExecutable := "greenhouse-threshold" |
|
if runtime.GOOS == "windows" { |
|
thresholdExecutable = "greenhouse-threshold.exe" |
|
} |
|
thresholdExecutable = filepath.Join(daemonExecutablePath, thresholdExecutable) |
|
thresholdArguments := []string{"-mode", "client", "-configFile", "threshold-config.json"} |
|
|
|
caddyExecutable := "greenhouse-caddy" |
|
if runtime.GOOS == "windows" { |
|
caddyExecutable = "greenhouse-caddy.exe" |
|
} |
|
caddyExecutable = filepath.Join(daemonExecutablePath, caddyExecutable) |
|
caddyConfigAbsPath, err := filepath.Abs("caddy-config.json") |
|
if err != nil { |
|
fatalfWithTelemetry("can't start the greenhouse-daemon because can't resolve absolute path of \"caddy-config.json\": %+v\n\n", err) |
|
return |
|
} |
|
caddyArguments := []string{"run", "-config", caddyConfigAbsPath} |
|
|
|
// TODO this might need to be changed for windows/mac |
|
// https://caddyserver.com/docs/conventions#data-directory |
|
caddyEnvironment := []string{ |
|
fmt.Sprintf("XDG_DATA_HOME=%s", filepath.Join(daemonPath, "caddyData")), |
|
fmt.Sprintf("XDG_CONFIG_HOME=%s", filepath.Join(daemonPath, "caddyConfig")), |
|
} |
|
|
|
daemonTelemetryIdBytes, err := ioutil.ReadFile(filepath.Join(daemonPath, "daemon-telemetry-id.txt")) |
|
if err == nil { |
|
daemonTelemetryId = string(daemonTelemetryIdBytes) |
|
} else { |
|
buffer := make([]byte, 4) |
|
n, err := rand.Read(buffer) |
|
if n != 4 || err != nil { |
|
fatalfWithTelemetry("can't start the greenhouse-daemon, can't read 4 random bytes: read: %d err: %+v", n, err) |
|
} |
|
daemonTelemetryId = fmt.Sprintf("%x", buffer) |
|
ioutil.WriteFile(filepath.Join(daemonPath, "daemon-telemetry-id.txt"), []byte(daemonTelemetryId), 0755) |
|
} |
|
|
|
var daemonConfig DaemonConfig |
|
daemonConfigBytes, err := ioutil.ReadFile(filepath.Join(daemonPath, "daemon-config.json")) |
|
// if the file doesn't exist, just skip it. |
|
if err == nil { |
|
err = json.Unmarshal(daemonConfigBytes, &daemonConfig) |
|
if err != nil { |
|
fatalfWithTelemetry("can't start the greenhouse-daemon, can't parse daemon-config.json %+v", err) |
|
} |
|
} |
|
|
|
var tenantInfo TenantInfo |
|
tenantInfoBytes, err := ioutil.ReadFile(filepath.Join(daemonPath, "daemon-tenant-info.json")) |
|
// if the file doesn't exist, just skip it. |
|
if err == nil { |
|
err = json.Unmarshal(tenantInfoBytes, &tenantInfo) |
|
if err != nil { |
|
fatalfWithTelemetry("can't start the greenhouse-daemon, can't parse daemon-tenant-info.json %+v", err) |
|
} |
|
} |
|
daemonTelemetryAccount = tenantInfo.EmailAddress |
|
|
|
var thresholdConfig ThresholdConfig |
|
thresholdConfigBytes, err := ioutil.ReadFile(filepath.Join(daemonPath, "threshold-config.json")) |
|
// if the file doesn't exist, just skip it. |
|
if err == nil { |
|
err = json.Unmarshal(thresholdConfigBytes, &thresholdConfig) |
|
if err != nil { |
|
fatalfWithTelemetry("can't start the greenhouse-daemon, can't parse threshold-config.json %+v", err) |
|
} |
|
} |
|
|
|
caddyOnPreStart := func() error { |
|
// caddy does not know how to clean up after itself when (its admin API specifically) is listening on a unix socket, |
|
// so we have to do that ourselves. |
|
if useUnixSockets { |
|
os.Remove(caddyAdminSocketFile) |
|
|
|
} |
|
return nil |
|
} |
|
|
|
log.Printf("thresholdExecutable: %s, caddyExecutable: %s\n", thresholdExecutable, caddyExecutable) |
|
|
|
daemonAPIInstance := DaemonAPI{ |
|
Config: &daemonConfig, |
|
ConfigurationMutex: sync.Mutex{}, |
|
TenantInfo: &tenantInfo, |
|
HTTPClient: &http.Client{ |
|
Timeout: time.Second * 10, |
|
}, |
|
DaemonPath: daemonPath, |
|
CloudURL: cloudURL, |
|
UseUnixSockets: useUnixSockets, |
|
CACertFile: filepath.Join(daemonPath, fmt.Sprintf("%s.crt", mainCAName)), |
|
TLSCertFile: filepath.Join(daemonPath, "greenhouse-daemon.crt"), |
|
TLSKeyFile: filepath.Join(daemonPath, "greenhouse-daemon.key"), |
|
ThresholdService: child.NewChildProcessService( |
|
daemonPath, |
|
thresholdExecutable, |
|
thresholdArguments, |
|
[]string{}, |
|
nil, |
|
daemonConfig.TunnelsEnabled, |
|
child.NewDaemonLogManager(daemonPath, thresholdExecutable), |
|
), |
|
CaddyService: child.NewChildProcessService( |
|
daemonPath, |
|
caddyExecutable, |
|
caddyArguments, |
|
caddyEnvironment, |
|
caddyOnPreStart, |
|
daemonConfig.TunnelsEnabled, |
|
child.NewDaemonLogManager(daemonPath, thresholdExecutable), |
|
), |
|
LogManager: log, |
|
TelemetryID: daemonTelemetryId, |
|
} |
|
|
|
go daemonAPIInstance.ThresholdService.MainLoop() |
|
go daemonAPIInstance.CaddyService.MainLoop() |
|
|
|
missingAnyCertsOrKeys := false |
|
certsAndKeysToCheck := []string{ |
|
daemonAPIInstance.CACertFile, |
|
daemonAPIInstance.TLSCertFile, |
|
daemonAPIInstance.TLSKeyFile, |
|
} |
|
for _, filepath := range certsAndKeysToCheck { |
|
_, err := os.Stat(filepath) |
|
if os.IsNotExist(err) { |
|
missingAnyCertsOrKeys = true |
|
break |
|
} |
|
} |
|
|
|
if missingAnyCertsOrKeys && !useUnixSockets { |
|
err = GenerateTLSCertificatesAndKeys(daemonPath) |
|
if err != nil { |
|
fatalfWithTelemetry("can't start the greenhouse-daemon because GenerateTLSCertificatesAndKeys returned %+v", err) |
|
} |
|
} |
|
|
|
// authenticatedHTTPClient, err := daemonAPIInstance.MakeServiceClient() |
|
// if err != nil { |
|
// log.Fatalf("can't start the greenhouse-daemon because: %+v", err) |
|
// } |
|
// daemonAPIInstance.CaddyConfigService = NewCaddyConfigService(authenticatedHTTPClient) |
|
|
|
// the unix socket will only be used if useUnixSockets == true |
|
thresholdClient, err := daemonAPIInstance.MakeServiceClient(thresholdAdminSocketFile) |
|
if err != nil { |
|
fatalfWithTelemetry("can't start the greenhouse-daemon because can't make threshold client: %+v", err) |
|
} |
|
caddyAdminClient, err := daemonAPIInstance.MakeServiceClient(caddyAdminSocketFile) |
|
if err != nil { |
|
fatalfWithTelemetry("can't start the greenhouse-daemon because can't make caddy admin client: %+v", err) |
|
} |
|
thresholdAdminURL := fmt.Sprintf("https://127.0.0.1:%d", thresholdAdminPort) |
|
caddyAdminURL := fmt.Sprintf("https://127.0.0.1:%d", caddyAdminPort) |
|
if useUnixSockets { |
|
thresholdAdminURL = "http://unix" |
|
caddyAdminURL = "http://unix" |
|
} |
|
|
|
daemonAPIInstance.ConfigService = &ConfigService{ |
|
ClientId: thresholdConfig.ClientId, |
|
EmailAddress: tenantInfo.EmailAddress, |
|
UseUnixSockets: useUnixSockets, |
|
ThresholdAdminBaseURL: thresholdAdminURL, |
|
CaddyAdminBaseURL: caddyAdminURL, |
|
ThresholdClient: thresholdClient, |
|
CaddyAdminClient: caddyAdminClient, |
|
TelemetryID: daemonTelemetryId, |
|
} |
|
|
|
if daemonAPIInstance.Config.TunnelsEnabled && len(daemonAPIInstance.Config.GUITunnels) > 0 { |
|
log.Printf("greenhouse-daemon enabling configured tunnels:\n") |
|
for _, tunnel := range daemonAPIInstance.Config.GUITunnels { |
|
log.Printf(" %s", tunnel.GetStringDisplay(false)) |
|
} |
|
|
|
log.Printf("startup aquiring ConfigurationMutex lock...\n") |
|
daemonAPIInstance.ConfigurationMutex.Lock() |
|
|
|
thresholdConfig, caddyConfig, err := daemonAPIInstance.ConfigService.PrepareConfigs(daemonAPIInstance.Config.GUITunnels) |
|
if err != nil { |
|
fatalfWithTelemetry("can't start the greenhouse-daemon because daemon-config.json is invalid: %+v", err) |
|
} |
|
|
|
completionChannel := make(chan bool) |
|
go daemonAPIInstance.applyConfigAsync(daemonAPIInstance.Config.GUITunnels, thresholdConfig, caddyConfig, completionChannel) |
|
|
|
go (func() { |
|
<-completionChannel |
|
log.Printf("startup releasing ConfigurationMutex lock\n") |
|
daemonAPIInstance.ConfigurationMutex.Unlock() |
|
})() |
|
} |
|
|
|
var listener net.Listener |
|
if useUnixSockets { |
|
os.Remove(daemonSocketFile) |
|
|
|
listenAddress, err := net.ResolveUnixAddr("unix", daemonSocketFile) |
|
if err != nil { |
|
fatalfWithTelemetry("can't start the greenhouse-daemon because net.ResolveUnixAddr() returned %+v", err) |
|
} |
|
|
|
listener, err = net.ListenUnix("unix", listenAddress) |
|
if err != nil { |
|
fatalfWithTelemetry("can't start the greenhouse-daemon because net.ListenUnix(\"unix\", \"%s\") returned %+v", listenAddress, err) |
|
} |
|
|
|
log.Printf("greenhouse-daemon about to start listening at http://%s 😈\n", daemonSocketFile) |
|
} else { |
|
|
|
addrString := fmt.Sprintf("127.0.0.1:%d", daemonAdminPort) |
|
addr, err := net.ResolveTCPAddr("tcp", addrString) |
|
if err != nil { |
|
fatalfWithTelemetry("can't start the greenhouse-daemon because net.ResolveTCPAddr(%s) returned %+v", addrString, err) |
|
} |
|
tcpListener, err := net.ListenTCP("tcp", addr) |
|
if err != nil { |
|
fatalfWithTelemetry("can't start the greenhouse-daemon because net.ListenTCP(%s) returned %+v", addrString, err) |
|
} |
|
tlsCert, err := tls.LoadX509KeyPair(daemonAPIInstance.TLSCertFile, daemonAPIInstance.TLSKeyFile) |
|
if err != nil { |
|
fatalfWithTelemetry( |
|
"can't start the greenhouse-daemon because tls.LoadX509KeyPair(%s,%s) returned %+v", |
|
daemonAPIInstance.TLSCertFile, daemonAPIInstance.TLSKeyFile, err, |
|
) |
|
} |
|
|
|
tlsConfig := &tls.Config{ |
|
Certificates: []tls.Certificate{tlsCert}, |
|
} |
|
tlsConfig.BuildNameToCertificate() |
|
|
|
listener = tls.NewListener(tcpListener, tlsConfig) |
|
|
|
go postTelemetry("daemon-started", daemonTelemetryAccount, daemonTelemetryId, "success!!") |
|
log.Printf("greenhouse-daemon about to start listening at https://%s 😈\n", addrString) |
|
} |
|
|
|
server := http.Server{ |
|
Handler: &daemonAPIInstance, |
|
ReadTimeout: 10 * time.Second, |
|
WriteTimeout: 10 * time.Second, |
|
} |
|
|
|
go (func() { |
|
err = server.Serve(listener) |
|
fatalfWithTelemetry("server.Serve returned %+v", err) |
|
})() |
|
|
|
sigs := child.GetSignalChannelOSIndependent() |
|
done := make(chan bool, 1) |
|
go func() { |
|
sig := <-sigs |
|
log.Printf("Greenhouse daemon recieved signal: %s\n", sig) |
|
go postTelemetry("daemon-signal", daemonTelemetryAccount, daemonTelemetryId, sig.String()) |
|
|
|
daemonAPIInstance.CaddyService.Enabled = false |
|
daemonAPIInstance.ThresholdService.Enabled = false |
|
attempts := 0 |
|
for attempts < 15 { |
|
attempts++ |
|
time.Sleep(500 * time.Millisecond) |
|
caddyStatus := daemonAPIInstance.CaddyService.Status() |
|
thresholdStatus := daemonAPIInstance.ThresholdService.Status() |
|
log.Printf( |
|
"Greenhouse daemon is waiting for caddy (enabled: %t, running: %t) and threshold (enabled: %t, running: %t) to stop... \n", |
|
caddyStatus.Enabled, caddyStatus.PID > 0, thresholdStatus.Enabled, thresholdStatus.PID > 0, |
|
) |
|
if caddyStatus.PID < 1 && thresholdStatus.PID < 1 { |
|
log.Printf("Caddy and threshold have stopped\n") |
|
done <- true |
|
return |
|
} |
|
} |
|
postTelemetry("daemon-timed-out-waiting-threshold-and-caddy", daemonTelemetryAccount, daemonTelemetryId, "oof") |
|
done <- true |
|
}() |
|
|
|
fmt.Println("greenhouse-daemon is running") |
|
<-done |
|
fmt.Println("exiting") |
|
} |
|
|
|
func (daemon *DaemonAPI) ServeHTTP(responseWriter http.ResponseWriter, request *http.Request) { |
|
switch path.Clean(request.URL.Path) { |
|
case "/ping": |
|
responseWriter.Header().Set("Content-Type", "text/plain") |
|
responseWriter.Write([]byte("pong\n")) |
|
|
|
case "/logs": |
|
service := request.URL.Query().Get("service") |
|
countString := request.URL.Query().Get("count") |
|
if countString == "" { |
|
countString = "3000" |
|
} |
|
count, err := strconv.Atoi(countString) |
|
if err != nil { |
|
errorMessage := fmt.Sprintf("greenhouse-daemon: 400 bad request: invalid count: %s", countString) |
|
http.Error(responseWriter, errorMessage, http.StatusInternalServerError) |
|
return |
|
} |
|
|
|
logManagerMap := map[string]child.LogManager{ |
|
"caddy": daemon.CaddyService.LogManager, |
|
"threshold": daemon.ThresholdService.LogManager, |
|
"daemon": daemon.LogManager, |
|
} |
|
var logManagers []child.LogManager |
|
if service == "all" || service == "" { |
|
logManagers = []child.LogManager{ |
|
logManagerMap["caddy"], |
|
logManagerMap["threshold"], |
|
logManagerMap["daemon"], |
|
} |
|
} else { |
|
logManager, hasLogManager := logManagerMap[service] |
|
if !hasLogManager { |
|
http.Error(responseWriter, "404 not found, try /log?service=daemon, /log?service=caddy, /log?service=threshold or /log?service=all", http.StatusNotFound) |
|
return |
|
} |
|
logManagers = []child.LogManager{logManager} |
|
} |
|
|
|
logIterators := make([]*child.LogIterator, len(logManagers)) |
|
for i := 0; i < len(logManagers); i++ { |
|
iterator, err := logManagers[i].Iterator() |
|
|
|
if err != nil { |
|
errorMessage := fmt.Sprintf("greenhouse-daemon: 500 internal server error: failed to open log iterator") |
|
go postTelemetry("daemon-logs", daemonTelemetryAccount, daemonTelemetryId, fmt.Sprintf("%s: %s\n", errorMessage, err)) |
|
log.Printf("%s: %+v\n", errorMessage, err) |
|
http.Error(responseWriter, errorMessage, http.StatusInternalServerError) |
|
return |
|
} |
|
logIterators[i] = iterator |
|
} |
|
|
|
logs, err := child.Joinerate(logIterators, count) |
|
if err != nil { |
|
errorMessage := "greenhouse-daemon: 500 internal server error: failed to read log file(s)" |
|
go postTelemetry("daemon-logs", daemonTelemetryAccount, daemonTelemetryId, fmt.Sprintf("%s: %s\n", errorMessage, err)) |
|
log.Printf("%s: %+v\n", errorMessage, err) |
|
http.Error(responseWriter, errorMessage, http.StatusInternalServerError) |
|
return |
|
} |
|
|
|
logBytes, err := json.Marshal(logs) |
|
if err != nil { |
|
errorMessage := "greenhouse-daemon: 500 internal server error: json serialization failed" |
|
go postTelemetry("daemon-logs", daemonTelemetryAccount, daemonTelemetryId, fmt.Sprintf("%s: %s\n", errorMessage, err)) |
|
log.Printf("%s: %+v\n", errorMessage, err) |
|
http.Error(responseWriter, errorMessage, http.StatusInternalServerError) |
|
return |
|
} |
|
responseWriter.Header().Set("Content-Type", "application/json") |
|
responseWriter.Write(logBytes) |
|
case "/status": |
|
updateTenantInfo := request.URL.Query().Get("updateTenantInfo") |
|
updateTenantInfoMessage := "n/a" |
|
if updateTenantInfo != "" { |
|
if daemon.Config == nil || daemon.Config.APIToken == "" { |
|
http.Error(responseWriter, "greenhouse-daemon: 400 bad request. please register before sending this request", http.StatusBadRequest) |
|
return |
|
} |
|
|
|
err, statusCode, errorMessage, tenantInfo := daemon.UpdateTenantInfo(nil) |
|
if err != nil { |
|
updateTenantInfoMessage = fmt.Sprintf("greenhouse-daemon can't get your account info: %d %s: %s", statusCode, errorMessage, err) |
|
log.Printf("%s:\n%+v\n", updateTenantInfoMessage, err) |
|
} else if statusCode != 200 { |
|
updateTenantInfoMessage = fmt.Sprintf("greenhouse-daemon can't get your account info: greenhouse returned HTTP %d: %s", statusCode, errorMessage) |
|
fmt.Printf("greenhouse-daemon can't get your account info: greenhouse returned HTTP %d: %s\n", statusCode, errorMessage) |
|
} else { |
|
daemon.TenantInfo = tenantInfo |
|
daemonTelemetryAccount = tenantInfo.EmailAddress |
|
daemon.ConfigService.EmailAddress = tenantInfo.EmailAddress |
|
updateTenantInfoMessage = "success" |
|
} |
|
} |
|
|
|
hashedTokenArray := sha256.Sum256([]byte(daemon.Config.APIToken)) |
|
status := &Status{ |
|
NeedsAPIToken: daemon.Config.APIToken == "", |
|
HashedToken: fmt.Sprintf("%x", hashedTokenArray[:]), |
|
Threshold: daemon.ThresholdService.Status(), |
|
Caddy: daemon.CaddyService.Status(), |
|
TenantInfo: daemon.TenantInfo, |
|
ServerName: daemon.Config.ServerName, |
|
GUITunnels: daemon.Config.GUITunnels, |
|
UpdateTenantInfoMessage: updateTenantInfoMessage, |
|
ApplyConfigStatuses: daemon.ApplyConfigStatuses, |
|
ApplyConfigStatusIndex: daemon.ApplyConfigStatusIndex, |
|
ApplyConfigStatusError: daemon.ApplyConfigStatusError, |
|
DaemonTelemetryID: daemon.TelemetryID, |
|
} |
|
statusBytes, err := json.MarshalIndent(status, "", " ") |
|
|
|
//log.Printf("statusBytes: %s\n\n", statusBytes) |
|
if err != nil { |
|
errorMessage := "greenhouse-daemon: 500 internal server error: json serialization failed" |
|
go postTelemetry("daemon-get-status", daemonTelemetryAccount, daemonTelemetryId, fmt.Sprintf("%s: %s\n", errorMessage, err)) |
|
http.Error(responseWriter, errorMessage, http.StatusInternalServerError) |
|
return |
|
} |
|
|
|
responseWriter.Header().Set("Content-Type", "application/json") |
|
responseWriter.Write(statusBytes) |
|
case "/register": |
|
serverName := strings.ToLower(request.URL.Query().Get("serverName")) |
|
|
|
subdomainRegex := regexp.MustCompile("^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$") |
|
if !subdomainRegex.MatchString(serverName) { |
|
http.Error(responseWriter, "400 Bad Request, the serverName must be a valid subdomain. It should only contain letters, numbers, and dashes", http.StatusBadRequest) |
|
return |
|
} |
|
|
|
authorizationHeader := request.Header.Get("Authorization") |
|
apiToken := strings.TrimPrefix(authorizationHeader, "Bearer ") |
|
|
|
// First order of business: get info for this tenant based on the apiToken to ensure they don't already |
|
// have a connected server named `serverName`. |
|
err, statusCode, statusString, tenantInfo := daemon.GetTenantInfo(apiToken) |
|
if err != nil || statusCode != 200 { |
|
errorMessage := fmt.Sprintf("greenhouse-daemon: %d %s", statusCode, statusString) |
|
http.Error(responseWriter, errorMessage, statusCode) |
|
return |
|
} |
|
|
|
if tenantInfo.ClientStates[serverName].CurrentState == "ClientConnected" { |
|
errorMessage := fmt.Sprintf("greenhouse-daemon: 409 conflict, you already have a connected server named '%s'", serverName) |
|
go postTelemetry("daemon-register", daemonTelemetryAccount, daemonTelemetryId, errorMessage) |
|
http.Error(responseWriter, errorMessage, http.StatusConflict) |
|
return |
|
} |
|
|
|
// set the api token in the daemon config first because it's required by UpdateTenantInfo |
|
daemon.Config.APIToken = apiToken |
|
daemon.Config.ServerName = serverName |
|
|
|
// make sure we can get the tenant info before we configure threshold. |
|
err, statusCode, errorMessage, tenantInfo := daemon.UpdateTenantInfo(tenantInfo) |
|
|
|
if err != nil || statusCode != 200 { |
|
errorMessage := fmt.Sprintf("greenhouse-daemon failed to get tenant info: %d %s", statusCode, errorMessage) |
|
go postTelemetry("daemon-register", daemonTelemetryAccount, daemonTelemetryId, fmt.Sprintf("%s: HTTP %d err: %s\n", errorMessage, statusCode, err)) |
|
log.Printf("%s:\n%+v\n", errorMessage, err) |
|
http.Error(responseWriter, errorMessage, statusCode) |
|
return |
|
} |
|
|
|
daemon.TenantInfo = tenantInfo |
|
daemonTelemetryAccount = tenantInfo.EmailAddress |
|
daemon.ConfigService.EmailAddress = tenantInfo.EmailAddress |
|
go postTelemetry("daemon-register", daemonTelemetryAccount, daemonTelemetryId, daemonTelemetryAccount) |
|
|
|
// TODO real greenhouse API url |
|
registerURL := fmt.Sprintf("%s/api/client_config?serverName=%s", daemon.CloudURL, serverName) |
|
configRequest, err := http.NewRequest("POST", registerURL, nil) |
|
if err != nil { |
|
errorMessage := "greenhouse-daemon: 500 internal server error" |
|
go postTelemetry("daemon-register", daemonTelemetryAccount, daemonTelemetryId, fmt.Sprintf("%s: %s\n", errorMessage, err)) |
|
log.Printf("%s:\n%+v\n", errorMessage, err) |
|
http.Error(responseWriter, errorMessage, http.StatusInternalServerError) |
|
return |
|
} |
|
configRequest.Header.Add("Authorization", fmt.Sprintf("Bearer %s", apiToken)) |
|
|
|
configResponse, err := daemon.HTTPClient.Do(configRequest) |
|
if err != nil { |
|
errorMessage := fmt.Sprintf("greenhouse-daemon: 503 bad gateway, can't reach greenhouse cloud at %s", registerURL) |
|
go postTelemetry("daemon-register", daemonTelemetryAccount, daemonTelemetryId, fmt.Sprintf("%s: %s\n", errorMessage, err)) |
|
log.Printf("%s:\n%+v\n", errorMessage, err) |
|
http.Error(responseWriter, errorMessage, http.StatusBadGateway) |
|
return |
|
} |
|
if configResponse.StatusCode != http.StatusOK { |
|
errorMessage := fmt.Sprintf( |
|
"greenhouse-daemon: %d %s, the server at %s returned HTTP %d", |
|
configResponse.StatusCode, configResponse.Status, registerURL, configResponse.StatusCode, |
|
) |
|
go postTelemetry("daemon-register", daemonTelemetryAccount, daemonTelemetryId, fmt.Sprintf("%s: %s\n", errorMessage, err)) |
|
log.Printf("%s:\n%+v\n", errorMessage, err) |
|
http.Error(responseWriter, errorMessage, configResponse.StatusCode) |
|
return |
|
} |
|
|
|
configBytes, err := ioutil.ReadAll(configResponse.Body) |
|
if err != nil { |
|
errorMessage := fmt.Sprintf("greenhouse-daemon: 503 bad gateway, read error on %s. please try again", registerURL) |
|
go postTelemetry("daemon-register", daemonTelemetryAccount, daemonTelemetryId, fmt.Sprintf("%s: %s\n", errorMessage, err)) |
|
log.Printf("%s:\n%+v\n", errorMessage, err) |
|
http.Error(responseWriter, errorMessage, http.StatusBadGateway) |
|
return |
|
} |
|
|
|
var thresholdConfig ThresholdConfig |
|
err = json.Unmarshal(configBytes, &thresholdConfig) |
|
if err != nil { |
|
errorMessage := fmt.Sprintf("greenhouse-daemon: 500 internal server error, %s did not return json", registerURL) |
|
go postTelemetry("daemon-register", daemonTelemetryAccount, daemonTelemetryId, fmt.Sprintf("%s: %s\n", errorMessage, err)) |
|
log.Printf("%s:\n%+v\n", errorMessage, err) |
|
http.Error(responseWriter, errorMessage, http.StatusInternalServerError) |
|
return |
|
} |
|
|
|
thresholdConfig.GreenhouseThresholdPort = greenhouseThresholdPort |
|
thresholdConfig.GreenhouseAPIToken = apiToken |
|
if !daemon.UseUnixSockets { |
|
thresholdConfig.AdminAPIPort = thresholdAdminPort |
|
thresholdConfig.AdminAPICACertificateFile = daemon.CACertFile |
|
thresholdConfig.AdminAPITlsCertificateFile = daemon.TLSCertFile |
|
thresholdConfig.AdminAPITlsKeyFile = daemon.TLSKeyFile |
|
} else { |
|
thresholdConfig.AdminUnixSocket = thresholdAdminSocketFile |
|
} |
|
daemon.ConfigService.ClientId = thresholdConfig.ClientId |
|
|
|
configBytesToWrite, err := json.MarshalIndent(thresholdConfig, "", " ") |
|
if err != nil { |
|
errorMessage := "greenhouse-daemon: 500 internal server error: json serialization failed" |
|
go postTelemetry("daemon-register", daemonTelemetryAccount, daemonTelemetryId, fmt.Sprintf("%s: %s\n", errorMessage, err)) |
|
log.Printf("%s:\n%+v\n", errorMessage, err) |
|
http.Error(responseWriter, errorMessage, http.StatusInternalServerError) |
|
return |
|
} |
|
err = ioutil.WriteFile(filepath.Join(daemon.DaemonPath, "threshold-config.json"), configBytesToWrite, 0600) |
|
if err != nil { |
|
errorMessage := "greenhouse-daemon: 500 internal server error: write threshold config file failed" |
|
go postTelemetry("daemon-register", daemonTelemetryAccount, daemonTelemetryId, fmt.Sprintf("%s: %s\n", errorMessage, err)) |
|
log.Printf("%s:\n%+v\n", errorMessage, err) |
|
http.Error(responseWriter, errorMessage, http.StatusInternalServerError) |
|
return |
|
} |
|
|
|
err = daemon.writeDaemonConfigJSON() |
|
if err != nil { |
|
errorMessage := "greenhouse-daemon: 500 internal server error: writeDaemonConfigJSON failed" |
|
go postTelemetry("daemon-register", daemonTelemetryAccount, daemonTelemetryId, fmt.Sprintf("%s: %s\n", errorMessage, err)) |
|
log.Printf("%s:\n%+v\n", errorMessage, err) |
|
http.Error(responseWriter, errorMessage, http.StatusInternalServerError) |
|
return |
|
} |
|
|
|
go postTelemetry("daemon-register", daemonTelemetryAccount, daemonTelemetryId, "success!!") |
|
responseWriter.Write([]byte("OK")) |
|
|
|
case "/unregister": |
|
os.Remove(filepath.Join(daemon.DaemonPath, "threshold-config.json")) |
|
os.Remove(filepath.Join(daemon.DaemonPath, "daemon-config.json")) |
|
os.Remove(filepath.Join(daemon.DaemonPath, "daemon-tenant-info.json")) |
|
|
|
daemon.ConfigService.ClientId = "" |
|
daemon.TenantInfo = nil |
|
daemon.ConfigService.EmailAddress = "" |
|
daemon.Config.APIToken = "" |
|
daemon.Config.ServerName = "" |
|
|
|
go postTelemetry("daemon-unregister", daemonTelemetryAccount, daemonTelemetryId, "success!!") |
|
|
|
responseWriter.Write([]byte("OK")) |
|
|
|
case "/apply_config": |
|
requestBytes, err := ioutil.ReadAll(request.Body) |
|
if err != nil { |
|
errorMessage := "greenhouse-daemon: 500 apply_config failed: http read error" |
|
go postTelemetry("daemon-apply-config", daemonTelemetryAccount, daemonTelemetryId, fmt.Sprintf("%s: %s\n", errorMessage, err)) |
|
log.Printf("%s:\n%+v\n", errorMessage, err) |
|
http.Error(responseWriter, errorMessage, http.StatusInternalServerError) |
|
return |
|
} |
|
|
|
log.Printf("apply_config: %s\n\n", string(requestBytes)) |
|
|
|
var tunnels []GUITunnel |
|
err = json.Unmarshal(requestBytes, &tunnels) |
|
if err != nil { |
|
errorMessage := "greenhouse-daemon: 400 bad request: invalid json:" |
|
go postTelemetry("daemon-apply-config", daemonTelemetryAccount, daemonTelemetryId, fmt.Sprintf("%s: %s\n", errorMessage, err)) |
|
log.Printf("%s:\n%+v\n", errorMessage, err) |
|
http.Error(responseWriter, errorMessage, http.StatusBadRequest) |
|
return |
|
} |
|
|
|
log.Printf("/apply_config aquiring ConfigurationMutex lock...\n") |
|
//timeBeforeLockAquired := time.Now() |
|
daemon.ConfigurationMutex.Lock() |
|
// if time.Since(timeBeforeLockAquired) > time.Second * 9 { |
|
// log.Println("blahblah TODO do I need this?") |
|
// } |
|
|
|
thresholdConfig, caddyConfig, err := daemon.ConfigService.PrepareConfigs(tunnels) |
|
if err != nil { |
|
errorMessage := fmt.Sprintf("greenhouse-daemon: 400 bad request: %s", err) |
|
go postTelemetry("daemon-apply-config", daemonTelemetryAccount, daemonTelemetryId, fmt.Sprintf("%s: %s\n", errorMessage, err)) |
|
log.Printf("%s:\n%+v\n", errorMessage, err) |
|
http.Error(responseWriter, errorMessage, http.StatusBadRequest) |
|
return |
|
} |
|
|
|
// bytez1, _ := json.MarshalIndent(thresholdConfig, "", " ") |
|
// log.Printf("thresholdConfig: %s\n\n", string(bytez1)) |
|
// bytez2, _ := json.MarshalIndent(caddyConfig, "", " ") |
|
// log.Printf("caddyConfig: %s\n\n", string(bytez2)) |
|
|
|
daemon.ThresholdService.Enabled = true |
|
daemon.CaddyService.Enabled = true |
|
daemon.Config.TunnelsEnabled = true |
|
daemon.Config.GUITunnels = tunnels |
|
|
|
err = daemon.writeDaemonConfigJSON() |
|
if err != nil { |
|
errorMessage := "greenhouse-daemon: 500 internal server error: writeDaemonConfigJSON failed" |
|
go postTelemetry("daemon-apply-config", daemonTelemetryAccount, daemonTelemetryId, fmt.Sprintf("%s: %s\n", errorMessage, err)) |
|
log.Printf("%s:\n%+v\n", errorMessage, err) |
|
http.Error(responseWriter, errorMessage, http.StatusInternalServerError) |
|
return |
|
} |
|
|
|
go postTelemetry("daemon-apply-config", daemonTelemetryAccount, daemonTelemetryId, "started...") |
|
|
|
completionChannel := make(chan bool) |
|
go daemon.applyConfigAsync(tunnels, thresholdConfig, caddyConfig, completionChannel) |
|
|
|
go (func() { |
|
<-completionChannel |
|
go postTelemetry("daemon-apply-config", daemonTelemetryAccount, daemonTelemetryId, fmt.Sprintf( |
|
"completed: ApplyConfigStatusIndex: %d, error: '%s'", daemon.ApplyConfigStatusIndex, daemon.ApplyConfigStatusError, |
|
)) |
|
log.Printf("/apply_config releasing ConfigurationMutex lock\n") |
|
daemon.ConfigurationMutex.Unlock() |
|
})() |
|
|
|
case "/netstat": |
|
entries, err := netstat.TCPSocks(func(s *netstat.SockTabEntry) bool { |
|
return s.State == netstat.Listen |
|
}) |
|
if err != nil { |
|
errorMessage := "greenhouse-daemon: 500 internal server error: netstat.TCPSocks failed" |
|
go postTelemetry("daemon-netstat", daemonTelemetryAccount, daemonTelemetryId, fmt.Sprintf("%s: %s\n", errorMessage, err)) |
|
log.Printf("%s:\n%+v\n", errorMessage, err) |
|
http.Error(responseWriter, errorMessage, http.StatusInternalServerError) |
|
return |
|
} |
|
|
|
toReturn := []ListeningSocket{} |
|
everyPidWasZero := true |
|
for _, entry := range entries { |
|
toAppend := ListeningSocket{ |
|
LocalAddr: entry.LocalAddr.String(), |
|
RemoteAddr: entry.RemoteAddr.String(), |
|
State: entry.State.String(), |
|
} |
|
if entry.Process != nil { |
|
if entry.Process.Pid != 0 { |
|
everyPidWasZero = false |
|
} |
|
toAppend.PID = entry.Process.Pid |
|
toAppend.ProcessCommand = entry.Process.Name |
|
} |
|
toReturn = append(toReturn, toAppend) |
|
} |
|
if everyPidWasZero { |
|
for _, entry := range toReturn { |
|
entry.ProcessCommand = "<unknown>" |
|
} |
|
} |
|
|
|
netstatBytes, err := json.MarshalIndent(toReturn, "", " ") |
|
|
|
if err != nil { |
|
errorMessage := "greenhouse-daemon: 500 internal server error: json serialization failed" |
|
go postTelemetry("daemon-netstat", daemonTelemetryAccount, daemonTelemetryId, fmt.Sprintf("%s: %s\n", errorMessage, err)) |
|
log.Printf("%s:\n%+v\n", errorMessage, err) |
|
http.Error(responseWriter, errorMessage, http.StatusInternalServerError) |
|
return |
|
} |
|
|
|
responseWriter.Header().Set("Content-Type", "application/json") |
|
responseWriter.Write(netstatBytes) |
|
|
|
default: |
|
http.Error(responseWriter, "greenhouse-daemon: 404 not found, try GET /status, GET /logs, or POST /register", http.StatusNotFound) |
|
} |
|
} |
|
|
|
func (daemon *DaemonAPI) applyConfigAsync(tunnels []GUITunnel, thresholdConfig *ThresholdTunnelsConfig, caddyConfig *map[string]*CaddyApp, completionChannel chan bool) { |
|
|
|
daemon.ApplyConfigStatusIndex = 0 |
|
daemon.ApplyConfigStatusError = "" |
|
daemon.ApplyConfigStatuses = []string{ |
|
"waiting for underlying services to start", |
|
"creating threshold tunnels", |
|
"testing threshold tunnels", |
|
"configuring caddy", |
|
"waiting for caddy to obtain https certificates from Let's Encrypt", |
|
"final testing", |
|
} |
|
|
|
log.Printf("waiting for threshold to start...\n") |
|
thresholdStarted := false |
|
startedWaiting := time.Now() |
|
for !thresholdStarted && time.Since(startedWaiting) < time.Second*10 { |
|
thresholdStatus := daemon.ThresholdService.Status() |
|
//jsonBytes, _ := json.MarshalIndent(thresholdStatus, "", " ") |
|
//log.Printf("thresholdStatus: %s\n\n", string(jsonBytes)) |
|
// TODO use the health status instead of uptime |
|
if thresholdStatus.PID != 0 && time.Since(thresholdStatus.Started) > time.Second { |
|
thresholdStarted = true |
|
} |
|
time.Sleep(time.Millisecond * 700) |
|
} |
|
if !thresholdStarted { |
|
daemon.ApplyConfigStatusError = "timed out after 10 seconds waiting for threshold to start" |
|
log.Printf("timed out after 10 seconds waiting for threshold to start\n") |
|
completionChannel <- false |
|
return |
|
} |
|
|
|
log.Printf("waiting for caddy to start...\n") |
|
caddyStarted := false |
|
startedWaiting = time.Now() |
|
for !caddyStarted && time.Since(startedWaiting) < time.Second*10 { |
|
caddyStatus := daemon.CaddyService.Status() |
|
//jsonBytes, _ := json.MarshalIndent(caddyStatus, "", " ") |
|
//log.Printf("caddyStatus: %s\n\n", string(jsonBytes)) |
|
// TODO use the health status instead of uptime |
|
if caddyStatus.PID != 0 && time.Since(caddyStatus.Started) > time.Second { |
|
caddyStarted = true |
|
} |
|
time.Sleep(time.Millisecond * 700) |
|
} |
|
if !caddyStarted { |
|
daemon.ApplyConfigStatusError = "timed out after 10 seconds waiting for caddy to start" |
|
log.Printf("timed out after 10 seconds waiting for caddy to start\n") |
|
completionChannel <- false |
|
return |
|
} |
|
|
|
log.Printf("services are started!\n") |
|
|
|
// TODO make this an option in the UI: allow the user to clear all other clients tunnels..? |
|
newTunnelListeners := map[string]bool{} |
|
for _, listener := range thresholdConfig.Listeners { |
|
newTunnelListeners[fmt.Sprintf("%s:%d", listener.ListenHostnameGlob, listener.ListenPort)] = true |
|
} |
|
|
|
//jsonBytes, _ := json.MarshalIndent(daemon.TenantInfo.Listeners, "", " ") |
|
//log.Printf("existing listeners: %s\n\n", jsonBytes) |
|
|
|
for _, listener := range daemon.TenantInfo.Listeners { |
|
conflictingListener := newTunnelListeners[fmt.Sprintf("%s:%d", listener.ListenHostnameGlob, listener.ListenPort)] |
|
if listener.ClientId != daemon.ConfigService.ClientId && !conflictingListener { |
|
thresholdConfig.Listeners = append(thresholdConfig.Listeners, listener) |
|
} |
|
} |
|
|
|
//jsonBytes, _ = json.MarshalIndent(thresholdConfig, "", " ") |
|
//log.Printf("new listeners: %s\n\n", jsonBytes) |
|
|
|
daemon.ApplyConfigStatusIndex++ |
|
err := daemon.ConfigService.ConfigureThreshold(thresholdConfig) |
|
if err != nil { |
|
daemon.ApplyConfigStatusError = fmt.Sprintf("error creating threshold tunnels: %s", err) |
|
log.Printf("error creating threshold tunnels: %+v\n", err) |
|
completionChannel <- false |
|
return |
|
} |
|
|
|
daemon.ApplyConfigStatusIndex++ |
|
err = daemon.ConfigService.TestThreshold(thresholdConfig, daemon.TenantInfo.AuthorizedDomains[0]) |
|
if err != nil { |
|
daemon.ApplyConfigStatusError = fmt.Sprintf("testing threshold tunnels: %s", err) |
|
log.Printf("testing threshold tunnels failed: %+v\n", err) |
|
completionChannel <- false |
|
return |
|
} |
|
|
|
daemon.ApplyConfigStatusIndex++ |
|
err = daemon.ConfigService.ConfigureCaddy(caddyConfig) |
|
if err != nil { |
|
daemon.ApplyConfigStatusError = fmt.Sprintf("error configuring caddy: %s", err) |
|
log.Printf("error configuring caddy: %+v\n", err) |
|
completionChannel <- false |
|
return |
|
} |
|
|
|
daemon.ApplyConfigStatusIndex++ |
|
err = daemon.ConfigService.EnsureCaddyACMECompletes(thresholdConfig) |
|
if err != nil { |
|
daemon.ApplyConfigStatusError = fmt.Sprintf("caddy failed to obtain https certificates from Let's Encrypt: %s", err) |
|
log.Printf("caddy failed to obtain https certificates from Let's Encrypt: %+v\n", err) |
|
completionChannel <- false |
|
return |
|
} |
|
|
|
daemon.ApplyConfigStatusIndex++ |
|
err = daemon.ConfigService.TestFinalTunnels(tunnels) |
|
if err != nil { |
|
daemon.ApplyConfigStatusError = fmt.Sprintf("testing threshold tunnels: %s", err) |
|
//log.Printf("testing threshold tunnels: %+v\n") |
|
completionChannel <- false |
|
return |
|
} |
|
|
|
daemon.ApplyConfigStatusIndex++ |
|
|
|
completionChannel <- true |
|
} |
|
|
|
func (daemon *DaemonAPI) GetTenantInfo(apiToken string) (error, int, string, *TenantInfo) { |
|
|
|
tenantInfoUrl := fmt.Sprintf("%s/api/tenant_info", daemon.CloudURL) |
|
//log.Printf("GET %s \n", tenantInfoUrl) |
|
tenantInfoRequest, err := http.NewRequest("POST", tenantInfoUrl, nil) |
|
if err != nil { |
|
return err, 500, "internal server error", nil |
|
} |
|
|
|
tenantInfoRequest.Header.Add("Authorization", fmt.Sprintf("Bearer %s", apiToken)) |
|
|
|
tenantInfoResponse, err := daemon.HTTPClient.Do(tenantInfoRequest) |
|
if err != nil { |
|
return err, 503, fmt.Sprintf("bad gateway, can't reach the server at %s", tenantInfoUrl), nil |
|
} |
|
|
|
if tenantInfoResponse.StatusCode != http.StatusOK { |
|
errorMessage := fmt.Sprintf( |
|
"%s, the server at %s returned HTTP %d", |
|
tenantInfoResponse.Status, tenantInfoUrl, tenantInfoResponse.StatusCode, |
|
) |
|
return err, tenantInfoResponse.StatusCode, errorMessage, nil |
|
} |
|
|
|
tenantInfoBytes, err := ioutil.ReadAll(tenantInfoResponse.Body) |
|
if err != nil { |
|
return err, 503, fmt.Sprintf("bad gateway, read error on %s. please try again", tenantInfoUrl), nil |
|
} |
|
|
|
//log.Printf("tenantInfoBytes: %s\n\n", tenantInfoBytes) |
|
|
|
var tenantInfo TenantInfo |
|
err = json.Unmarshal(tenantInfoBytes, &tenantInfo) |
|
if err != nil { |
|
return err, 500, fmt.Sprintf("internal server error, %s did not return json", tenantInfoUrl), nil |
|
} |
|
|
|
return nil, 200, "ok", &tenantInfo |
|
} |
|
|
|
func (daemon *DaemonAPI) UpdateTenantInfo(tenantInfo *TenantInfo) (error, int, string, *TenantInfo) { |
|
|
|
if tenantInfo == nil { |
|
var err error |
|
var statusCode int |
|
var statusString string |
|
err, statusCode, statusString, tenantInfo = daemon.GetTenantInfo(daemon.Config.APIToken) |
|
if err != nil || statusCode != 200 { |
|
return err, statusCode, statusString, nil |
|
} |
|
daemonTelemetryAccount = tenantInfo.EmailAddress |
|
} |
|
|
|
tenantInfoBytesToWrite, err := json.MarshalIndent(daemon.TenantInfo, "", " ") |
|
if err != nil { |
|
return err, 500, "internal server error: json serialization failed", nil |
|
} |
|
err = ioutil.WriteFile(filepath.Join(daemon.DaemonPath, "daemon-tenant-info.json"), tenantInfoBytesToWrite, 0600) |
|
if err != nil { |
|
return err, 500, "internal server error: write threshold config file failed", nil |
|
} |
|
|
|
//log.Printf("tenantInfoBytesToWrite: %s\n\n", tenantInfoBytesToWrite) |
|
|
|
return nil, 200, "ok", tenantInfo |
|
} |
|
|
|
func (daemon *DaemonAPI) writeDaemonConfigJSON() error { |
|
configBytesToWrite, err := json.MarshalIndent(daemon.Config, "", " ") |
|
if err != nil { |
|
return err |
|
} |
|
err = ioutil.WriteFile(filepath.Join(daemon.DaemonPath, "daemon-config.json"), configBytesToWrite, 0600) |
|
if err != nil { |
|
return err |
|
} |
|
return nil |
|
} |
|
|
|
func (daemon *DaemonAPI) MakeServiceClient(socketFile string) (*http.Client, error) { |
|
|
|
if daemon.UseUnixSockets { |
|
return &http.Client{ |
|
Transport: &http.Transport{ |
|
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { |
|
return net.Dial("unix", socketFile) |
|
}, |
|
}, |
|
Timeout: 15 * time.Second, |
|
}, nil |
|
} else { |
|
cert, err := tls.LoadX509KeyPair(daemon.TLSCertFile, daemon.TLSKeyFile) |
|
if err != nil { |
|
log.Printf(fmt.Sprintf("can't MakeServiceClient() because tls.LoadX509KeyPair returned: \n%+v\n", err)) |
|
return nil, errors.Wrap(err, "can't MakeServiceClient() because tls.LoadX509KeyPair returned") |
|
} |
|
|
|
caCertPool := x509.NewCertPool() |
|
caCert, err := ioutil.ReadFile(daemon.CACertFile) |
|
if err != nil { |
|
return nil, errors.Wrap(err, "can't MakeServiceClient() because can't read the CA cert file") |
|
} |
|
ok := caCertPool.AppendCertsFromPEM(caCert) |
|
if !ok { |
|
return nil, errors.Errorf("Failed to add CA certificate '%s' to cert pool\n", daemon.CACertFile) |
|
} |
|
|
|
tlsClientConfig := &tls.Config{ |
|
Certificates: []tls.Certificate{cert}, |
|
RootCAs: caCertPool, |
|
} |
|
tlsClientConfig.BuildNameToCertificate() |
|
|
|
return &http.Client{ |
|
Transport: &http.Transport{ |
|
TLSClientConfig: tlsClientConfig, |
|
}, |
|
Timeout: 15 * time.Second, |
|
}, nil |
|
} |
|
|
|
} |
|
|
|
func GenerateTLSCertificatesAndKeys(daemonPath string) error { |
|
pkiService := greenhousePKI.NewPKIService(&easypki.EasyPKI{Store: &easypkiStore.InMemory{}}) |
|
|
|
mainCA, err := pkiService.GetCACertificate(mainCAName) |
|
if err != nil { |
|
return errors.Wrap(err, "GetCACertificate") |
|
} |
|
mainCABytes := pem.EncodeToMemory(&pem.Block{ |
|
Bytes: mainCA.Raw, |
|
Type: "CERTIFICATE", |
|
}) |
|
err = ioutil.WriteFile(filepath.Join(daemonPath, fmt.Sprintf("%s.crt", mainCAName)), mainCABytes, 0600) |
|
if err != nil { |
|
return errors.Wrap(err, "Write daemon CA certificate") |
|
} |
|
expiry := time.Now().Add(time.Hour * time.Duration(24*31*12*99)) |
|
daemonKey, daemonCert, err := pkiService.GetServerKeyPair(mainCAName, "greenhouse-daemon", []net.IP{net.ParseIP("127.0.0.1")}, expiry) |
|
if err != nil { |
|
return errors.Wrap(err, "GetServerKeyPair") |
|
} |
|
|
|
daemonKeyBytes := pem.EncodeToMemory(&pem.Block{ |
|
Bytes: x509.MarshalPKCS1PrivateKey(daemonKey), |
|
Type: "RSA PRIVATE KEY", |
|
}) |
|
daemonCertBytes := pem.EncodeToMemory(&pem.Block{ |
|
Bytes: daemonCert.Raw, |
|
Type: "CERTIFICATE", |
|
}) |
|
|
|
err = ioutil.WriteFile(filepath.Join(daemonPath, "greenhouse-daemon.crt"), daemonCertBytes, 0600) |
|
if err != nil { |
|
return errors.Wrap(err, "Write daemon TLS certificate") |
|
} |
|
|
|
err = ioutil.WriteFile(filepath.Join(daemonPath, "greenhouse-daemon.key"), daemonKeyBytes, 0600) |
|
if err != nil { |
|
return errors.Wrap(err, "Write daemon TLS key") |
|
} |
|
|
|
return nil |
|
} |
|
|
|
// func brotliCompress(utf8String []byte) ([]byte, error) { |
|
// out := bytes.Buffer{} |
|
// // Quality controls the compression-speed vs compression-density trade-offs. |
|
// // The higher the quality, the slower the compression. Range is 0 to 11. |
|
// writer := brotli.NewWriterOptions(&out, brotli.WriterOptions{Quality: 1}) |
|
// in := bytes.NewReader(utf8String) |
|
// n, err := io.Copy(writer, in) |
|
// if err != nil { |
|
// return nil, err |
|
// } |
|
|
|
// if int(n) != len(utf8String) { |
|
// return nil, errors.Errorf("brotli compress failed, unable to compress all bytes") |
|
// } |
|
|
|
// if err := writer.Close(); err != nil { |
|
// return nil, err |
|
// } |
|
|
|
// return out.Bytes(), nil |
|
// } |
|
|
|
// func brotliDecompress(brotliCompressedBytes []byte) ([]byte, error) { |
|
// return ioutil.ReadAll(brotli.NewReader(bytes.NewReader(brotliCompressedBytes))) |
|
// }
|
|
|