🌱🏠 a cloud service to enable your own server (owned by you and running on your computer) to be accessible on the internet in seconds, no credit card required 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.
 
 
 
 

240 lines
5.9 KiB

package main
import (
"bufio"
"bytes"
"crypto/rand"
"crypto/sha256"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"os/exec"
"path"
"time"
errors "git.sequentialread.com/forest/pkg-errors"
base58 "github.com/shengdoushi/base58"
)
type IngressService struct {
BaseHTTPService
Model *DBModel
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"`
}
const adminThresholdNodeId = "greenhouse_internal_node"
func NewIngressService(config *Config, model *DBModel) *IngressService {
toReturn := &IngressService{
Model: model,
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.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) GetGreenhouseDaemonStatus() (string, error) {
responseBytes, err := service.MyHTTP200("GET", "http://unix/status", nil, nil)
if err != nil {
return "", err
}
return string(responseBytes), nil
}
func (service *IngressService) ConfigureGreenhouseDaemon() error {
responseBytes, err := service.MyHTTP200("GET", "http://unix/status", nil, nil)
if err != nil {
return err
}
var responseStatus GreenhouseDaemonStatus
err = json.Unmarshal(responseBytes, &responseStatus)
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,
)
i := 0
newTokenName := "greenhouse_builtin_ingress"
for i < 100 {
conflict := false
for _, token := range tenant.APITokens {
if token.Name == newTokenName {
conflict = true
}
}
if conflict == false {
break
}
i++
newTokenName = fmt.Sprintf("greenhouse_builtin_ingress_%d", i)
}
if i >= 100 {
return errors.New("too many greenhouse_builtin_ingress tokens")
}
apiTokenBuffer := make([]byte, 16)
rand.Read(apiTokenBuffer)
apiToken := base58.Encode(apiTokenBuffer, base58.BitcoinAlphabet)
rawHash := sha256.Sum256([]byte(apiToken))
hashedAPIToken := fmt.Sprintf("%x", rawHash)
err = service.Model.CreateAPIToken(service.AdminTenantId, newTokenName, hashedAPIToken)
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: true,
Subdomain: "greenhouse",
Domain: fmt.Sprintf("%s.%s", tenant.Subdomain, freeSubdomainDomain),
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
}