🌱🏠 instant least-authority port-forwarding (with automatic HTTPS) for anyone, anywhere! We **really** don't want your TLS private keys, you can keep them 😃 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.

242 lines
6.4 KiB

package main
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"os/exec"
"path"
"time"
errors "git.sequentialread.com/forest/pkg-errors"
"golang.org/x/sys/unix"
)
type IngressService struct {
BaseHTTPService
Backend *BackendApp
Model *DBModel
DaemonCommand *exec.Cmd
DaemonIsRunning bool
AdminTenantId int
FrontendPort int
}
type GUITunnel struct {
Protocol string `json:"protocol"`
HasSubdomain bool `json:"has_subdomain"`
Subdomain string `json:"subdomain"`
Domain string `json:"domain"`
DestinationType string `json:"destination_type"`
DestinationPort int `json:"destination_port"`
}
type GreenhouseDaemonStatus struct {
NeedsAPIToken bool `json:"needs_api_token"`
HashedToken string `json:"hashed_api_token"`
ApplyConfigStatuses []string `json:"apply_config_statuses"`
ApplyConfigStatusIndex int `json:"apply_config_status_index"`
ApplyConfigStatusError string `json:"apply_config_status_error"`
}
const adminThresholdNodeId = "greenhouse-internal-node"
const greenhouseExternalDomain = "greenhouse-alpha.server.garden"
func NewIngressService(config *Config, backend *BackendApp, model *DBModel) *IngressService {
toReturn := &IngressService{
Model: model,
Backend: backend,
AdminTenantId: config.AdminTenantId,
FrontendPort: config.FrontendPort,
}
toReturn.ClientFactory = func() (*http.Client, *time.Time, error) {
// 99 years aka forever, never expire
expiryTime := time.Now().Add(time.Hour * 24 * 30 * 12 * 99)
return &http.Client{
Transport: CreateUnixTransport("/var/run/greenhouse-daemon.sock"),
Timeout: 10 * time.Second,
}, &expiryTime, nil
}
return toReturn
}
func (service *IngressService) StartGreenhouseDaemon() error {
if service.DaemonIsRunning {
return errors.New("Daemon is already Running ")
}
cwd, err := os.Getwd()
if err != nil {
return err
}
greenhouseDaemonPath := path.Join(cwd, "greenhouse-daemon")
command := exec.Command(path.Join(greenhouseDaemonPath, "greenhouse-daemon"))
env := os.Environ()
env = append(env, fmt.Sprintf("GREENHOUSE_DAEMON_PATH=%s", greenhouseDaemonPath))
env = append(env, "GREENHOUSE_DAEMON_USE_UNIX_SOCKETS=true")
env = append(env, fmt.Sprintf("GREENHOUSE_DAEMON_CLOUD_URL=http://localhost:%d", service.FrontendPort))
command.Env = env
command.Dir = greenhouseDaemonPath
stdoutReader, err := command.StdoutPipe()
if err != nil {
log.Printf("can't Start greenhouse-daemon, command.StdoutPipe() returned %s", err)
return err
}
stdoutScanner := bufio.NewScanner(stdoutReader)
stdoutScanner.Split(bufio.ScanLines)
go (func() {
for stdoutScanner.Scan() {
log.Printf("[greenhouse-daemon stdout] %s \n", stdoutScanner.Text())
}
})()
stderrReader, err := command.StderrPipe()
if err != nil {
log.Printf("can't Start(greenhouse-daemon), command.StdoutPipe() returned %s", err)
return err
}
stderrScanner := bufio.NewScanner(stderrReader)
stderrScanner.Split(bufio.ScanLines)
go (func() {
for stderrScanner.Scan() {
log.Printf("[greenhouse-daemon stderr] %s \n", stderrScanner.Text())
}
})()
err = command.Start()
if err != nil {
log.Printf("can't Start greenhouse-daemon, command.Start() returned %s", err)
return err
}
log.Printf("started greenhouse-daemon with PID %d\n", command.Process.Pid)
service.DaemonCommand = command
service.DaemonIsRunning = true
go (func() {
err := command.Wait()
service.DaemonIsRunning = false
if err != nil {
log.Printf("command.Wait() returned '%s' for greenhouse-daemon child process", err)
}
log.Printf("greenhouse-daemon child process ended with exit code %d", command.ProcessState.ExitCode())
})()
return nil
}
func (service *IngressService) StopGreenhouseDaemon() error {
if service.DaemonCommand.Process != nil {
service.DaemonCommand.Process.Signal(unix.SIGTERM)
service.DaemonIsRunning = false
return nil
} else {
return errors.New("daemon Process is nil")
}
}
func (service *IngressService) GetGreenhouseDaemonProcessRunning() bool {
hasProcessState := service.DaemonCommand != nil && service.DaemonCommand.ProcessState != nil
hasProcess := service.DaemonCommand != nil && service.DaemonCommand.Process != nil
appearsToHaveExited := hasProcessState && (service.DaemonCommand.ProcessState.Exited() || service.DaemonCommand.ProcessState.ExitCode() == -1)
appearsToHavePid := hasProcess && service.DaemonCommand.Process.Pid != 0
return appearsToHavePid && !appearsToHaveExited
}
func (service *IngressService) GetGreenhouseDaemonStatus() (*GreenhouseDaemonStatus, error) {
responseBytes, err := service.MyHTTP200("GET", "http://unix/status", nil, nil)
if err != nil {
return nil, err
}
var responseStatus GreenhouseDaemonStatus
err = json.Unmarshal(responseBytes, &responseStatus)
if err != nil {
return nil, err
}
return &responseStatus, nil
}
func (service *IngressService) ConfigureGreenhouseDaemon() error {
responseStatus, err := service.GetGreenhouseDaemonStatus()
if err != nil {
return err
}
tenant, err := service.Model.GetTenant(service.AdminTenantId)
if err != nil {
return err
}
hasMatchingAPIToken := false
for _, token := range tenant.APITokens {
if responseStatus.HashedToken == token.HashedToken {
hasMatchingAPIToken = true
}
}
if responseStatus.NeedsAPIToken || !hasMatchingAPIToken {
log.Printf(
"responseStatus.NeedsAPIToken (%t) || !hasMatchingAPIToken (%t): now reconfiguring the greenhouse daemon's API token...\n",
responseStatus.NeedsAPIToken, !hasMatchingAPIToken,
)
newTokenName := "greenhouse_builtin_ingress"
apiToken, _, err := service.Backend.CreateAPIToken(service.AdminTenantId, newTokenName)
if err != nil {
return err
}
_, err = service.MyHTTP200(
"POST",
fmt.Sprintf("http://unix/register?serverName=%s", adminThresholdNodeId),
nil,
func(request *http.Request) {
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiToken))
},
)
if err != nil {
return err
}
}
greenhouseGUITunnels := []GUITunnel{
{
Protocol: "https",
HasSubdomain: false,
Domain: "greenhouse-alpha.server.garden",
DestinationType: "local_port",
DestinationPort: service.FrontendPort,
},
}
tunnelsBytes, err := json.Marshal(greenhouseGUITunnels)
if err != nil {
return err
}
_, err = service.MyHTTP200(
"POST",
"http://unix/apply_config",
bytes.NewBuffer(tunnelsBytes),
nil,
)
if err != nil {
return err
}
return nil
}