🌱🏠😈 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.

1241 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
}
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)
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)))
// }