Browse Source

adding the ls and rm commands, basic functionality is done!

main
forest 3 months ago
parent
commit
2aa782f8d3
6 changed files with 313 additions and 78 deletions
  1. +1
    -0
      .gitignore
  2. +64
    -0
      ls.go
  3. +158
    -9
      main.go
  4. +81
    -0
      rm.go
  5. +8
    -5
      status.go
  6. +1
    -64
      tunnel.go

+ 1
- 0
.gitignore View File

@ -0,0 +1 @@
greenhouse

+ 64
- 0
ls.go View File

@ -0,0 +1,64 @@
package main
import (
"encoding/json"
"fmt"
"os"
"strings"
)
func ls(args []string) {
if len(args) > 1 {
fmt.Printf("\nError: unknown extra arguments '%s' provided to the ls command\n", strings.Join(args[1:], " "))
displayHelpAndExit("ls")
return
}
status, err := getStatus(false)
if err != nil {
fmt.Printf("\nError: %s\n\n", err)
os.Exit(1)
return
}
if status.NeedsAPIToken {
fmt.Print("\nGreenhouse Daemon is not registered to a Greenhouse account yet. Run the following command to register your account:\n\ngreenhouse register GREENHOUSE_API_TOKEN [SERVER_NAME]\n\n")
os.Exit(0)
return
} else {
if len(args) == 1 {
if args[0] == "--json" {
bytez, err := json.MarshalIndent(status.GUITunnels, "", " ")
if err != nil {
fmt.Print("\nJSON serialization error: %s\n", err)
os.Exit(1)
return
}
fmt.Printf("%s\n", string(bytez))
} else {
fmt.Print("\nError: unknown argument '%s' provided to the ls command. expected --json\n", args[0])
os.Exit(1)
return
}
} else {
fmt.Print("\nTunnels:\n")
for _, tunnel := range status.GUITunnels {
destHost := "localhost"
if tunnel.DestinationType == "host_port" {
destHost = tunnel.DestinationHostname
}
dest := fmt.Sprintf("tcp://%s:%d", destHost, tunnel.DestinationPort)
if tunnel.DestinationType == "folder" {
dest = fmt.Sprintf("file://%s", tunnel.DestinationFolderPath)
}
fmt.Printf(" %s to %s\n", tunnelListenURL(&tunnel), dest)
}
fmt.Print("\n")
}
}
}

+ 158
- 9
main.go View File

@ -1,6 +1,7 @@
package main
import (
"bytes"
"context"
"crypto/tls"
"crypto/x509"
@ -9,6 +10,7 @@ import (
"fmt"
"io/ioutil"
"log"
"math"
"net"
"net/http"
"os"
@ -112,9 +114,9 @@ func main() {
} else if command == "tunnel" {
tunnel(args)
} else if command == "ls" {
ls(args)
} else if command == "rm" {
rm(args)
} else {
fmt.Printf(`
Unknown command '%s'.
@ -161,21 +163,21 @@ Usage: greenhouse ls [LS_OPTIONS]...
List tunnels configured for your account. Only displays tunnels directed at *this* server by default.
Valid options are:
--all to list tunnels for *all* servers.
--json to output tunnels information in json format.
e.g:
greenhouse ls
greenhouse ls --all --json
greenhouse ls --json
`,
"rm": `
Usage: greenhouse rm LISTEN_URL
Usage: greenhouse rm LISTEN_URL [...]
Remove a tunnel by LISTEN_URL.
Specifying "*" will remove all services.
Remove one or more tunnels by LISTEN_URL.
Specifying "*" will remove all tunnels.
e.g:
greenhouse rm https://www.cli-user.greenhouseusers.com
greenhouse rm https://www.cli-user.greenhouseusers.com https://files.cli-user.greenhouseusers.com
greenhouse rm *
`,
@ -220,9 +222,37 @@ GREENHOUSE_CLI_DAEMON_UNIX_SOCKET=/var/run/greenhouse-daemon.sock greenhouse reg
`,
"json": `
TODO
Here is an example of the json object used to represent a tunnel:
{
"protocol": "https",
"has_subdomain": true,
"subdomain": "test",
"domain": "cli-user.greenhouseusers.com",
"public_port": 443,
"destination_type": "local_port",
"destination_hostname": "127.0.0.1",
"destination_port": 80,
"destination_folder_path": ""
}
`,
The allowed values for protocol are https, tls, and tcp
The tcp protocol does not require a domain, however it does require
a public_port within the port range allocated to your greenhouse account.
The tls and https protocols can be used with any port number as long as
the specified domain is authorized on your greenhouse account.
The only difference between tls and https: https will also open up a
secondary tunnel on port 80, to be used for http -> https redirects.
The allowed values for destination_type are local_port, host_port, and folder
The folder destination type requires a destination_folder_path & only works
with the https protocol.
`,
}
topicHelp, hasTopicHelp := helpTopics[strings.ToLower(topic)]
@ -242,6 +272,83 @@ No help information was found for the topic '%s'.
os.Exit(0)
}
func saveConfiguration(tunnels []GUITunnel) {
requestBodyBytes, err := json.Marshal(tunnels)
if err != nil {
fmt.Printf(`
Internal JSON Serialization Error: %s
`, err)
os.Exit(1)
return
}
applyURL := fmt.Sprintf("%s/apply_config", baseURL)
response, err := httpClient.Post(applyURL, "application/json", bytes.NewBuffer(requestBodyBytes))
if err != nil {
fmt.Printf("\nCouldn't reach greenhouse daemon at %s: %s\n\n", baseURL, err)
os.Exit(1)
return
}
responseBytes, err := ioutil.ReadAll(response.Body)
responseString := "http read error, failed to read response from greenhouse"
if err == nil {
responseString = string(responseBytes)
}
if response.StatusCode != 200 {
fmt.Printf("\nPOST %s returned HTTP %d:\n%s\n\n", applyURL, response.StatusCode, responseString)
os.Exit(1)
return
}
fmt.Println("\nNow applying new tunnel configuration...\n")
doneApplying := false
pollingDuration := time.Millisecond * 300
previousApplyConfigStatusIndex := 0
for !doneApplying {
status, err := getStatus(false)
if err != nil {
fmt.Printf("\nError polling the daemon: %s\n\n", err)
time.Sleep(pollingDuration)
continue
}
if status.ApplyConfigStatusError != "" {
fmt.Printf("\nError applying configuration: %s\n\n", status.ApplyConfigStatusError)
os.Exit(1)
return
}
if previousApplyConfigStatusIndex != status.ApplyConfigStatusIndex {
for i := previousApplyConfigStatusIndex; i < status.ApplyConfigStatusIndex && i < len(status.ApplyConfigStatuses); i++ {
fmt.Printf(" - %s\n", status.ApplyConfigStatuses[i])
}
previousApplyConfigStatusIndex = status.ApplyConfigStatusIndex
}
if previousApplyConfigStatusIndex >= len(status.ApplyConfigStatuses) {
doneApplying = true
}
time.Sleep(pollingDuration)
}
}
func tunnelListenURL(tunnel *GUITunnel) string {
domain := tunnel.Domain
if tunnel.HasSubdomain {
domain = fmt.Sprintf("%s.%s", tunnel.Subdomain, tunnel.Domain)
}
if tunnel.Protocol != "https" || tunnel.PublicPort != 443 {
domain = fmt.Sprintf("%s:%d", tunnel.PublicPort)
}
return fmt.Sprintf("%s://%s", tunnel.Protocol, domain)
}
func getStatus(updateTenantInfo bool) (*DaemonStatus, error) {
updateTenantInfoString := ""
if updateTenantInfo {
@ -327,3 +434,45 @@ func AppendCertsFromPEM(s *x509.CertPool, pemCerts []byte) (ok bool, errorz []er
return ok, errorz
}
// https://gist.github.com/laurent22/8025413
func levenshteinDistance(s string, t string) int {
s = strings.ToLower(s)
t = strings.ToLower(t)
if s == t {
return 0
}
if len(s) == 0 {
return len(t)
}
if len(t) == 0 {
return len(s)
}
v0 := make([]int, len(t)+1)
v1 := make([]int, len(t)+1)
for i := 0; i < len(v0); i++ {
v0[i] = i
}
for i := 0; i < len(s); i++ {
v1[0] = i + 1
for j := 0; j < len(t); j++ {
var cost int
if s[i] == t[j] {
cost = 0
} else {
cost = 1
}
v1[j+1] = int(math.Min(float64(v1[j]+1), math.Min(float64(v0[j+1]+1), float64(v0[j]+cost))))
}
for j := 0; j < len(v0); j++ {
v0[j] = v1[j]
}
}
return v1[len(t)]
}

+ 81
- 0
rm.go View File

@ -0,0 +1,81 @@
package main
import (
"fmt"
"os"
)
func rm(args []string) {
status, err := getStatus(false)
if err != nil {
fmt.Printf("\nError: %s\n\n", err)
os.Exit(1)
return
}
if status.NeedsAPIToken {
fmt.Print("\nGreenhouse Daemon is not registered to a Greenhouse account yet. Run the following command to register your account:\n\ngreenhouse register GREENHOUSE_API_TOKEN [SERVER_NAME]\n\n")
os.Exit(0)
return
} else {
matchedTunnelIndexes := map[int]bool{}
matchedArgumentIndexes := map[int]bool{}
suggestMatch := map[int]int{}
for i, tunnel := range status.GUITunnels {
listenURL := tunnelListenURL(&tunnel)
for j, arg := range args {
if arg == "*" || listenURL == arg {
matchedTunnelIndexes[i] = true
matchedArgumentIndexes[j] = true
}
if levenshteinDistance(listenURL, arg) < 5 {
suggestMatch[j] = i
}
}
}
fmt.Print("\n")
anyUnmatchedArgs := false
for j, arg := range args {
if !matchedArgumentIndexes[j] {
fmt.Printf("'%s' did not match the listen url of any configured tunnel.", arg)
suggestedMatch, hasSuggestedMatch := suggestMatch[j]
if hasSuggestedMatch {
suggestedMatchTunnel := status.GUITunnels[suggestedMatch]
fmt.Printf("\nDid you mean '%s'?", tunnelListenURL(&suggestedMatchTunnel))
}
fmt.Print("\n")
anyUnmatchedArgs = true
}
}
if anyUnmatchedArgs {
fmt.Print("\nTo display the currently configured tunnels, run greenhouse ls\n\n")
os.Exit(1)
return
}
toSendToDaemon := []GUITunnel{}
for i, tunnel := range status.GUITunnels {
if !matchedTunnelIndexes[i] {
toSendToDaemon = append(toSendToDaemon, tunnel)
} else {
fmt.Printf("Removing %s\n", tunnelListenURL(&tunnel))
}
}
saveConfiguration(toSendToDaemon)
count := len(matchedTunnelIndexes)
plural := ""
if count > 1 {
plural = "s"
}
fmt.Printf("\n%d tunnel%s removed successfully!\n\n", count, plural)
}
}

+ 8
- 5
status.go View File

@ -10,7 +10,7 @@ import (
func status(args []string) {
if len(args) > 0 {
fmt.Printf("\nError: unknown extra arguments '%s' provided status command\n", strings.Join(args, " "))
fmt.Printf("\nError: unknown extra arguments '%s' provided to the status command\n", strings.Join(args, " "))
displayHelpAndExit("status")
return
@ -23,7 +23,7 @@ func status(args []string) {
}
if status.NeedsAPIToken {
fmt.Print("\nGreenhouse Daemon is not registered to a Greenhouse account yet. Run the following command to register your account:\n\ngreenhouse register GREENHOUSE_API_TOKEN [SERVER_NAME]\n")
fmt.Print("\nGreenhouse Daemon is not registered to a Greenhouse account yet. Run the following command to register your account:\n\ngreenhouse register GREENHOUSE_API_TOKEN [SERVER_NAME]\n\n")
os.Exit(0)
return
} else {
@ -40,8 +40,9 @@ func status(args []string) {
}
uptimeDuration := time.Since(status.Started)
uptimeDurationSeconds := time.Duration((int64(uptimeDuration) / int64(time.Second)) * int64(time.Second))
suffix = fmt.Sprintf(` Up for %s
Health Check: %s`, uptimeDurationSeconds, healthCheckResult)
suffix = fmt.Sprintf(` PID: %d
Up for %s
Health Check: %s`, status.PID, uptimeDurationSeconds, healthCheckResult)
}
return fmt.Sprintf(` Enabled: %t
Running: %t
@ -52,7 +53,7 @@ func status(args []string) {
for tenantIdNodeId, v := range status.TenantInfo.ClientStates {
split := strings.Split(tenantIdNodeId, ".")
clientStates = append(clientStates, fmt.Sprintf(
"%s:\n CurrentState: %s\n LastState: %s",
"%s:\n Current: %s\n Previous: %s",
split[1], v.CurrentState, v.LastState,
))
}
@ -60,6 +61,7 @@ func status(args []string) {
fmt.Printf(`
Greenhouse Daemon:
Server Name: %s
Configured Tunnels: %d
Caddy Server Status:
%s
@ -79,6 +81,7 @@ Greenhouse Account:
`,
status.ServerName,
len(status.GUITunnels),
serviceStatusString(status.Caddy),
serviceStatusString(status.Threshold),
status.TenantInfo.EmailAddress,


+ 1
- 64
tunnel.go View File

@ -1,15 +1,12 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/url"
"os"
"strconv"
"strings"
"time"
)
func tunnel(args []string) {
@ -426,67 +423,7 @@ Conflict: Your greenhouse account already has a tunnel listening at '%s:%d'.
toSendToDaemon = append(status.GUITunnels, tunnelToCreate)
}
requestBodyBytes, err := json.Marshal(toSendToDaemon)
if err != nil {
fmt.Printf(`
Internal JSON Serialization Error: %s
`, err)
os.Exit(1)
return
}
applyURL := fmt.Sprintf("%s/apply_config", baseURL)
response, err := httpClient.Post(applyURL, "application/json", bytes.NewBuffer(requestBodyBytes))
if err != nil {
fmt.Printf("\nCouldn't reach greenhouse daemon at %s: %s\n\n", baseURL, err)
os.Exit(1)
return
}
responseBytes, err := ioutil.ReadAll(response.Body)
responseString := "http read error, failed to read response from greenhouse"
if err == nil {
responseString = string(responseBytes)
}
if response.StatusCode != 200 {
fmt.Printf("\nPOST %s returned HTTP %d:\n%s\n\n", applyURL, response.StatusCode, responseString)
os.Exit(1)
return
}
fmt.Println("\nNow applying new tunnel configuration...\n")
doneApplying := false
pollingDuration := time.Millisecond * 300
previousApplyConfigStatusIndex := 0
for !doneApplying {
status, err = getStatus(false)
if err != nil {
fmt.Printf("\nError polling the daemon: %s\n\n", err)
time.Sleep(pollingDuration)
continue
}
if status.ApplyConfigStatusError != "" {
fmt.Printf("\nError applying configuration: %s\n\n", status.ApplyConfigStatusError)
os.Exit(1)
return
}
if previousApplyConfigStatusIndex != status.ApplyConfigStatusIndex {
for i := previousApplyConfigStatusIndex; i < status.ApplyConfigStatusIndex && i < len(status.ApplyConfigStatuses); i++ {
fmt.Printf(" - %s\n", status.ApplyConfigStatuses[i])
}
previousApplyConfigStatusIndex = status.ApplyConfigStatusIndex
}
if previousApplyConfigStatusIndex >= len(status.ApplyConfigStatuses) {
doneApplying = true
}
time.Sleep(pollingDuration)
}
saveConfiguration(toSendToDaemon)
fmt.Print("\nYour tunnel was configured successfully!\n\n")
}

Loading…
Cancel
Save