Browse Source

splitting each command out into its own file

main
forest 3 months ago
parent
commit
1728f7c478
3 changed files with 573 additions and 553 deletions
  1. +0
    -553
      main.go
  2. +81
    -0
      register.go
  3. +492
    -0
      tunnel.go

+ 0
- 553
main.go View File

@ -1,7 +1,6 @@
package main
import (
"bytes"
"context"
"crypto/tls"
"crypto/x509"
@ -12,9 +11,7 @@ import (
"log"
"net"
"net/http"
"net/url"
"os"
"regexp"
"strconv"
"strings"
"time"
@ -128,556 +125,6 @@ Unknown command '%s'.
}
}
func tunnel(args []string) {
var tunnelToCreate GUITunnel
var status *DaemonStatus
getTCPPortRangeWarning := func(status *DaemonStatus) string {
return fmt.Sprintf(
"Your Greenhouse account was allocated TCP ports %d-%d. You must either use a port within that range, or use a protocol that can be routed by hostname (tls, http, or https)\n",
status.TenantInfo.PortStart, status.TenantInfo.PortEnd,
)
}
printUnauthorizedDomainError := func(domain string, authorizedDomains []string) {
fmt.Printf(`
Error: domain '%s' hasn't been authorized for use with your Greenhouse account yet.
Your authorized domains are:
- %s
If you wish to authorize '%s', you may blahblahblah <TODO how does a user authorize a domain?>
`, domain, strings.Join(authorizedDomains, "\n - "), domain,
)
}
if len(args) > 0 && args[0] == "--json" {
if len(args) > 2 {
fmt.Printf("\nError: unknown extra arguments provided for --json mode: %s\nDo you have shell quoting issues in your json object?\n", strings.Join(args[2:], " "))
displayHelpAndExit("tunnel")
return
}
if len(args) == 1 {
fmt.Print("\nError: json object argument is required for `greenhouse tunnel --json` command\n\n")
displayHelpAndExit("tunnel")
return
}
err := json.Unmarshal([]byte(args[1]), &tunnelToCreate)
if err != nil {
fmt.Printf("\nError: second argument could not be parsed as a JSON object: %s\n\n", err)
os.Exit(1)
return
}
if tunnelToCreate.DestinationType != "local_port" && tunnelToCreate.DestinationType != "host_port" && tunnelToCreate.DestinationType != "folder" {
fmt.Print("\nInvalid: destination_type property must be set to local_port, host_port, or folder\n\n")
os.Exit(1)
return
}
if tunnelToCreate.Protocol != "https" && tunnelToCreate.Protocol != "tcp" && tunnelToCreate.Protocol != "tls" {
fmt.Print("\nInvalid: protocol property must be set to https, tcp, or tls\n\n")
os.Exit(1)
return
}
if tunnelToCreate.DestinationType != "folder" && tunnelToCreate.DestinationPort == 0 {
fmt.Printf("\nInvalid: destination_port property is required for destination_type %s\n\n", tunnelToCreate.DestinationType)
os.Exit(1)
return
}
if tunnelToCreate.DestinationType == "host_port" && tunnelToCreate.DestinationHostname == "" {
fmt.Print("\nInvalid: destination_hostname property is required for destination_type host_port\n\n")
os.Exit(1)
return
}
if tunnelToCreate.DestinationType == "folder" && tunnelToCreate.DestinationFolderPath == "" {
fmt.Print("\nInvalid: destination_folder_path property is required for destination_type folder\n\n")
os.Exit(1)
return
}
if tunnelToCreate.Protocol == "tcp" {
if tunnelToCreate.Domain != "" || tunnelToCreate.Subdomain != "" || tunnelToCreate.HasSubdomain {
fmt.Print(`
Warning: the "domain" property has no effect for {"protocol": "tcp", ...}, it should be omitted because the TCP protocol has no concept of a domain.
You may reach your TCP tunnels via *any* of your Greenhouse domains.
`)
}
tunnelToCreate.Domain = status.TenantInfo.AuthorizedDomains[0]
tunnelToCreate.HasSubdomain = false
tunnelToCreate.Subdomain = ""
} else {
if tunnelToCreate.Domain == "" {
fmt.Printf("\nInvalid: domain property is required for %s protocol\n\n", tunnelToCreate.Protocol)
os.Exit(1)
return
}
}
if tunnelToCreate.DestinationType == "folder" && tunnelToCreate.Protocol != "https" {
fmt.Printf("\nError: the folder destination_type is incompatible with the %s protocol. \nUse the https protocol instead.\n\n", tunnelToCreate.Protocol)
os.Exit(1)
return
}
status, err = getStatus(true)
if err != nil {
fmt.Printf("\nError: %s\n\n", err)
os.Exit(1)
return
}
if tunnelToCreate.PublicPort == 0 {
warning := ""
if tunnelToCreate.Protocol == "tcp" {
warning = getTCPPortRangeWarning(status)
}
fmt.Printf("\nInvalid: public_port property is required\n%s\n", warning)
os.Exit(1)
return
}
if tunnelToCreate.Protocol == "tcp" && (tunnelToCreate.PublicPort < status.TenantInfo.PortStart || tunnelToCreate.PublicPort > status.TenantInfo.PortEnd) {
fmt.Printf(`
Error: public_port '%d' is out-of-range for the tcp protocol
%s`, tunnelToCreate.PublicPort, getTCPPortRangeWarning(status))
os.Exit(1)
return
}
if tunnelToCreate.Protocol != "tcp" {
listenHostnameIsAuthorized := false
for _, hostname := range status.TenantInfo.AuthorizedDomains {
if hostname == tunnelToCreate.Domain {
listenHostnameIsAuthorized = true
}
}
if !listenHostnameIsAuthorized {
printUnauthorizedDomainError(tunnelToCreate.Domain, status.TenantInfo.AuthorizedDomains)
os.Exit(1)
return
}
}
// end of json parsing and validation logic -- skip over the CLI parsing/validation logic below
// until we get to the common logic shared between both.
} else {
if len(args) < 3 {
fmt.Printf("\nError: expected at least 3 arguments for tunnel command, but only saw %d:\n", len(args))
displayHelpAndExit("tunnel")
return
}
if len(args) == 4 {
fmt.Printf("\nError: unknown extra argument provided: %s\n", args[3])
displayHelpAndExit("tunnel")
return
}
if len(args) > 5 {
fmt.Printf("\nError: unknown extra argument(s) provided: %s\n", strings.Join(args[5:], " "))
displayHelpAndExit("tunnel")
return
}
listenURLString := args[0]
to := args[1]
localURLString := args[2]
var serverName string
//var serviceName string
if len(args) == 5 {
if strings.ToLower(args[3]) == "on" {
serverName = args[4]
} else {
fmt.Printf("\nError: expected the fourth tunnel argument '%s' to match the word 'on'\n", args[3])
displayHelpAndExit("tunnel")
return
}
}
listenURL, err := url.Parse(listenURLString)
if err != nil {
fmt.Printf("\nError: the given listen url '%s' was not a valid url: %s\n", listenURLString, err)
displayHelpAndExit("tunnel")
return
}
if listenURL.Scheme != "https" && listenURL.Scheme != "tls" && listenURL.Scheme != "tcp" {
fmt.Printf("\nError: the given listen url '%s' has an unsupported scheme '%s'\n", listenURL, listenURL.Scheme)
displayHelpAndExit("tunnel")
return
}
if listenURL.RawQuery != "" {
fmt.Printf("\nError: the given listen url '%s' should not have a query '?%s'\n", listenURL, listenURL.RawQuery)
displayHelpAndExit("tunnel")
return
}
if strings.ToLower(to) != "to" {
fmt.Printf("\nError: expected the argument '%s' to match the word 'to'\n", to)
displayHelpAndExit("tunnel")
return
}
localURL, err := url.Parse(localURLString)
if err != nil {
fmt.Printf("\nError: the given local url '%s' was not a valid url: %s\n", localURLString, err)
displayHelpAndExit("tunnel")
return
}
if localURL.Scheme != "http" && localURL.Scheme != "tcp" && localURL.Scheme != "file" {
fmt.Printf("\nError: the given local url '%s' has an unsupported scheme '%s'\n", localURL, localURL.Scheme)
os.Exit(1)
return
}
if localURL.Scheme == "file" && listenURL.Scheme != "https" {
fmt.Printf("\nError: Local URL '%s' is incompatible with listen URL '%s'.\nThe local file:// scheme requires the listen URL to use the https:// scheme.\n", localURL, listenURL)
os.Exit(1)
return
}
localPort := 0
if localURL.Scheme == "http" {
localPort = 80
}
if localURL.Port() != "" {
localPort, _ = strconv.Atoi(localURL.Port())
}
if localURL.Scheme != "file" && localPort == 0 {
fmt.Printf("\nError: Local URL '%s' is missing a port number. Port number is required for the %s:// scheme\n", localURL, localURL.Scheme)
os.Exit(1)
return
}
if localURL.RawQuery != "" {
fmt.Printf("\nError: the given local url '%s' should not have a query '?%s'\n", localURL, localURL.RawQuery)
os.Exit(1)
return
}
// end of client-side validation, need to grab the daemon status for the rest of the validation.
status, err = getStatus(true)
if err != nil {
fmt.Printf("\nError: %s\n\n", err)
os.Exit(1)
return
}
listenPort := 0
if listenURL.Scheme == "https" {
listenPort = 443
}
if listenURL.Scheme == "http" {
listenPort = 80
}
listenPortString := listenURL.Port()
if listenPortString != "" {
var err error
listenPort, err = strconv.Atoi(listenPortString)
if err != nil {
fmt.Printf("\nError: the given listen url '%s' has an invalid port '%s'\n", listenURL, listenPortString)
displayHelpAndExit("tunnel")
return
}
if listenURL.Scheme == "tcp" && (listenPort < status.TenantInfo.PortStart || listenPort > status.TenantInfo.PortEnd) {
fmt.Printf(`
Error: The given listen url '%s' has an out-of-range port '%d'
%s`, listenURL, listenPort, getTCPPortRangeWarning(status))
os.Exit(1)
return
}
}
if listenPort == 0 {
warning := ""
if listenURL.Scheme == "tcp" {
warning = getTCPPortRangeWarning(status)
}
fmt.Printf(`
Error: Unable to determine a listening port for the listen url '%s'. Please specify a listening port.
%s`, listenURL, warning)
displayHelpAndExit("tunnel")
return
}
listenHostname := strings.ToLower(listenURL.Hostname())
subdomain := ""
if listenURL.Scheme != "tcp" {
listenHostnameIsAuthorized := false
for _, hostname := range status.TenantInfo.AuthorizedDomains {
if hostname == listenHostname {
listenHostnameIsAuthorized = true
}
if strings.HasSuffix(listenHostname, hostname) {
listenHostnameIsAuthorized = true
subdomain = strings.Trim(strings.TrimSuffix(listenHostname, hostname), ".")
}
}
if !listenHostnameIsAuthorized {
printUnauthorizedDomainError(listenHostname, status.TenantInfo.AuthorizedDomains)
os.Exit(1)
return
}
} else {
fmt.Printf(`
Warning: the specified listen URL '%s' has a hostname '%s'. Hostname has no effect for tcp:// URLs, it should be omitted because the TCP protocol has no concept of a domain.
You may reach your TCP tunnels via *any* of your Greenhouse domains.
`, listenURL, listenHostname)
}
if serverName == "" || serverName == status.ServerName {
serverName = status.ServerName
} else {
foundServerName := false
foundServerNameCurrentState := "Unknown"
serverNamesSlice := []string{fmt.Sprintf("%s (self)", status.ServerName)}
for otherServersName, otherServersState := range status.TenantInfo.ClientStates {
if otherServersName != status.ServerName {
serverNamesSlice = append(serverNamesSlice, fmt.Sprintf("%s (%s)", otherServersName, otherServersState.CurrentState))
}
if serverName == otherServersName {
foundServerName = true
foundServerNameCurrentState = otherServersState.CurrentState
}
}
if !foundServerName {
fmt.Printf(`
Error: There is no server named '%s' currently associated to your greenhouse account.
Currently registered servers:
- %s
`, serverName, strings.Join(serverNamesSlice, "\n - "),
)
os.Exit(1)
return
} else if foundServerNameCurrentState != "ClientConnected" {
fmt.Printf(`
Error: The server '%s' has the state '%s'. Can't create a tunnel to it until it is connected.
`, serverName, foundServerNameCurrentState,
)
os.Exit(1)
return
}
}
destinationType := "local_port"
destinationHostname := "127.0.0.1"
destinationFolderPath := ""
if localURL.Scheme == "file" {
destinationType = "folder"
destinationFolderPath = localURL.Path
} else if localURL.Hostname() != "127.0.0.1" && localURL.Hostname() != "localhost" {
destinationType = "host_port"
destinationHostname = localURL.Hostname()
}
domain := strings.Trim(strings.TrimPrefix(listenHostname, subdomain), ".")
if listenURL.Scheme == "tcp" {
domain = status.TenantInfo.AuthorizedDomains[0]
}
tunnelToCreate = GUITunnel{
Protocol: listenURL.Scheme,
HasSubdomain: subdomain != "",
Subdomain: subdomain,
Domain: domain,
PublicPort: listenPort,
DestinationType: destinationType,
DestinationHostname: destinationHostname,
DestinationPort: localPort,
DestinationFolderPath: destinationFolderPath,
}
// End of command line parsing and validation logic, on to the validation/action logic
// that both --json and normal command line share.
}
listenHostname := tunnelToCreate.Domain
if tunnelToCreate.HasSubdomain {
listenHostname = fmt.Sprintf("%s.%s", tunnelToCreate.Subdomain, tunnelToCreate.Domain)
}
conflictIndex := -1
for i, otherTunnel := range status.GUITunnels {
otherTunnelListenHostname := otherTunnel.Domain
if otherTunnel.HasSubdomain {
otherTunnelListenHostname = fmt.Sprintf("%s.%s", otherTunnel.Subdomain, otherTunnel.Domain)
}
bothAreTCP := tunnelToCreate.Protocol == "tcp" && otherTunnel.Protocol == "tcp"
if otherTunnel.PublicPort == tunnelToCreate.PublicPort && (bothAreTCP || otherTunnelListenHostname == listenHostname) {
conflictIndex = i
}
}
toSendToDaemon := make([]GUITunnel, len(status.GUITunnels))
if conflictIndex != -1 {
displayHostname := listenHostname
if tunnelToCreate.Protocol == "tcp" {
displayHostname = ""
}
fmt.Printf(`
Conflict: Your greenhouse account already has a tunnel listening at '%s:%d'.
`, displayHostname, tunnelToCreate.PublicPort,
)
if !yesOrNoConfirmation("Do you want to overwrite it?") {
os.Exit(0)
return
}
for i, tunnel := range status.GUITunnels {
if i == conflictIndex {
toSendToDaemon[i] = tunnelToCreate
} else {
toSendToDaemon[i] = tunnel
}
}
} else {
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)
}
fmt.Print("\nYour tunnel was configured successfully!\n\n")
}
func register(args []string) {
if len(args) == 0 {
fmt.Print(`
Error: an api token is required.
`)
displayHelpAndExit("register")
return
}
apiToken := args[0]
serverName := strings.Join(args[1:], " ")
if serverName == "" {
hostname, err := os.Hostname()
if err != nil {
fmt.Printf(`
Error: server name was not provided, and greenhouse wasn't able to figure out what your hostname is: %s
`, err)
os.Exit(1)
return
}
serverName = hostname
} else if !regexp.MustCompile("[a-zA-Z0-9_-]+").MatchString(serverName) {
fmt.Printf(`
Error: server name '%s' must only contain letters, numbers, dashes, and underscores. (matching [a-zA-Z0-9_-]+)
`, serverName)
displayHelpAndExit("register")
return
}
if !regexp.MustCompile("[a-zA-Z0-9]+").MatchString(apiToken) {
fmt.Printf(`
Error: invalid API token '%s'. Valid api tokens will only contain letters and numbers. (Base58-encoded, matching [a-zA-Z0-9]+)
`, apiToken)
displayHelpAndExit("register")
return
}
registerURL := fmt.Sprintf("%s/register/?serverName=%s", baseURL, serverName)
registerRequest, err := http.NewRequest("POST", registerURL, nil)
if err != nil {
fmt.Printf("\nUnexpected error creating POST request for %s: %s\n\n", registerURL, err)
os.Exit(1)
return
}
registerRequest.Header.Add("Authorization", fmt.Sprintf("Bearer %s", apiToken))
response, err := httpClient.Do(registerRequest)
if err != nil {
fmt.Printf("\nCouldn't reach greenhouse daemon at %s: %s\n\n", baseURL, err)
os.Exit(1)
return
}
if response.StatusCode != 200 {
responseBytes, err := ioutil.ReadAll(response.Body)
responseString := "http read error, failed to read response from greenhouse"
if err == nil {
responseString = string(responseBytes)
}
fmt.Printf("\nPOST %s returned HTTP %d:\n%s\n\n", registerURL, response.StatusCode, responseString)
os.Exit(1)
return
}
fmt.Printf("\nSuccess! This computer has been registered as a server. Server name: %s\n\n", serverName)
}
func displayHelpAndExit(topic string) {
defaultHelp := `


+ 81
- 0
register.go View File

@ -0,0 +1,81 @@
package main
import (
"fmt"
"io/ioutil"
"net/http"
"os"
"regexp"
"strings"
)
func register(args []string) {
if len(args) == 0 {
fmt.Print(`
Error: an api token is required.
`)
displayHelpAndExit("register")
return
}
apiToken := args[0]
serverName := strings.Join(args[1:], " ")
if serverName == "" {
hostname, err := os.Hostname()
if err != nil {
fmt.Printf(`
Error: server name was not provided, and greenhouse wasn't able to figure out what your hostname is: %s
`, err)
os.Exit(1)
return
}
serverName = hostname
} else if !regexp.MustCompile("[a-zA-Z0-9_-]+").MatchString(serverName) {
fmt.Printf(`
Error: server name '%s' must only contain letters, numbers, dashes, and underscores. (matching [a-zA-Z0-9_-]+)
`, serverName)
displayHelpAndExit("register")
return
}
if !regexp.MustCompile("[a-zA-Z0-9]+").MatchString(apiToken) {
fmt.Printf(`
Error: invalid API token '%s'. Valid api tokens will only contain letters and numbers. (Base58-encoded, matching [a-zA-Z0-9]+)
`, apiToken)
displayHelpAndExit("register")
return
}
registerURL := fmt.Sprintf("%s/register/?serverName=%s", baseURL, serverName)
registerRequest, err := http.NewRequest("POST", registerURL, nil)
if err != nil {
fmt.Printf("\nUnexpected error creating POST request for %s: %s\n\n", registerURL, err)
os.Exit(1)
return
}
registerRequest.Header.Add("Authorization", fmt.Sprintf("Bearer %s", apiToken))
response, err := httpClient.Do(registerRequest)
if err != nil {
fmt.Printf("\nCouldn't reach greenhouse daemon at %s: %s\n\n", baseURL, err)
os.Exit(1)
return
}
if response.StatusCode != 200 {
responseBytes, err := ioutil.ReadAll(response.Body)
responseString := "http read error, failed to read response from greenhouse"
if err == nil {
responseString = string(responseBytes)
}
fmt.Printf("\nPOST %s returned HTTP %d:\n%s\n\n", registerURL, response.StatusCode, responseString)
os.Exit(1)
return
}
fmt.Printf("\nSuccess! This computer has been registered as a server. Server name: %s\n\n", serverName)
}

+ 492
- 0
tunnel.go View File

@ -0,0 +1,492 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/url"
"os"
"strconv"
"strings"
"time"
)
func tunnel(args []string) {
var tunnelToCreate GUITunnel
var status *DaemonStatus
getTCPPortRangeWarning := func(status *DaemonStatus) string {
return fmt.Sprintf(
"Your Greenhouse account was allocated TCP ports %d-%d. You must either use a port within that range, or use a protocol that can be routed by hostname (tls, http, or https)\n",
status.TenantInfo.PortStart, status.TenantInfo.PortEnd,
)
}
printUnauthorizedDomainError := func(domain string, authorizedDomains []string) {
fmt.Printf(`
Error: domain '%s' hasn't been authorized for use with your Greenhouse account yet.
Your authorized domains are:
- %s
If you wish to authorize '%s', you may blahblahblah <TODO how does a user authorize a domain?>
`, domain, strings.Join(authorizedDomains, "\n - "), domain,
)
}
if len(args) > 0 && args[0] == "--json" {
if len(args) > 2 {
fmt.Printf("\nError: unknown extra arguments provided for --json mode: %s\nDo you have shell quoting issues in your json object?\n", strings.Join(args[2:], " "))
displayHelpAndExit("tunnel")
return
}
if len(args) == 1 {
fmt.Print("\nError: json object argument is required for `greenhouse tunnel --json` command\n\n")
displayHelpAndExit("tunnel")
return
}
err := json.Unmarshal([]byte(args[1]), &tunnelToCreate)
if err != nil {
fmt.Printf("\nError: second argument could not be parsed as a JSON object: %s\n\n", err)
os.Exit(1)
return
}
if tunnelToCreate.DestinationType != "local_port" && tunnelToCreate.DestinationType != "host_port" && tunnelToCreate.DestinationType != "folder" {
fmt.Print("\nInvalid: destination_type property must be set to local_port, host_port, or folder\n\n")
os.Exit(1)
return
}
if tunnelToCreate.Protocol != "https" && tunnelToCreate.Protocol != "tcp" && tunnelToCreate.Protocol != "tls" {
fmt.Print("\nInvalid: protocol property must be set to https, tcp, or tls\n\n")
os.Exit(1)
return
}
if tunnelToCreate.DestinationType != "folder" && tunnelToCreate.DestinationPort == 0 {
fmt.Printf("\nInvalid: destination_port property is required for destination_type %s\n\n", tunnelToCreate.DestinationType)
os.Exit(1)
return
}
if tunnelToCreate.DestinationType == "host_port" && tunnelToCreate.DestinationHostname == "" {
fmt.Print("\nInvalid: destination_hostname property is required for destination_type host_port\n\n")
os.Exit(1)
return
}
if tunnelToCreate.DestinationType == "folder" && tunnelToCreate.DestinationFolderPath == "" {
fmt.Print("\nInvalid: destination_folder_path property is required for destination_type folder\n\n")
os.Exit(1)
return
}
if tunnelToCreate.Protocol == "tcp" {
if tunnelToCreate.Domain != "" || tunnelToCreate.Subdomain != "" || tunnelToCreate.HasSubdomain {
fmt.Print(`
Warning: the "domain" property has no effect for {"protocol": "tcp", ...}, it should be omitted because the TCP protocol has no concept of a domain.
You may reach your TCP tunnels via *any* of your Greenhouse domains.
`)
}
tunnelToCreate.Domain = status.TenantInfo.AuthorizedDomains[0]
tunnelToCreate.HasSubdomain = false
tunnelToCreate.Subdomain = ""
} else {
if tunnelToCreate.Domain == "" {
fmt.Printf("\nInvalid: domain property is required for %s protocol\n\n", tunnelToCreate.Protocol)
os.Exit(1)
return
}
}
if tunnelToCreate.DestinationType == "folder" && tunnelToCreate.Protocol != "https" {
fmt.Printf("\nError: the folder destination_type is incompatible with the %s protocol. \nUse the https protocol instead.\n\n", tunnelToCreate.Protocol)
os.Exit(1)
return
}
status, err = getStatus(true)
if err != nil {
fmt.Printf("\nError: %s\n\n", err)
os.Exit(1)
return
}
if tunnelToCreate.PublicPort == 0 {
warning := ""
if tunnelToCreate.Protocol == "tcp" {
warning = getTCPPortRangeWarning(status)
}
fmt.Printf("\nInvalid: public_port property is required\n%s\n", warning)
os.Exit(1)
return
}
if tunnelToCreate.Protocol == "tcp" && (tunnelToCreate.PublicPort < status.TenantInfo.PortStart || tunnelToCreate.PublicPort > status.TenantInfo.PortEnd) {
fmt.Printf(`
Error: public_port '%d' is out-of-range for the tcp protocol
%s`, tunnelToCreate.PublicPort, getTCPPortRangeWarning(status))
os.Exit(1)
return
}
if tunnelToCreate.Protocol != "tcp" {
listenHostnameIsAuthorized := false
for _, hostname := range status.TenantInfo.AuthorizedDomains {
if hostname == tunnelToCreate.Domain {
listenHostnameIsAuthorized = true
}
}
if !listenHostnameIsAuthorized {
printUnauthorizedDomainError(tunnelToCreate.Domain, status.TenantInfo.AuthorizedDomains)
os.Exit(1)
return
}
}
// end of json parsing and validation logic -- skip over the CLI parsing/validation logic below
// until we get to the common logic shared between both.
} else {
if len(args) < 3 {
fmt.Printf("\nError: expected at least 3 arguments for tunnel command, but only saw %d:\n", len(args))
displayHelpAndExit("tunnel")
return
}
if len(args) == 4 {
fmt.Printf("\nError: unknown extra argument provided: %s\n", args[3])
displayHelpAndExit("tunnel")
return
}
if len(args) > 5 {
fmt.Printf("\nError: unknown extra argument(s) provided: %s\n", strings.Join(args[5:], " "))
displayHelpAndExit("tunnel")
return
}
listenURLString := args[0]
to := args[1]
localURLString := args[2]
var serverName string
//var serviceName string
if len(args) == 5 {
if strings.ToLower(args[3]) == "on" {
serverName = args[4]
} else {
fmt.Printf("\nError: expected the fourth tunnel argument '%s' to match the word 'on'\n", args[3])
displayHelpAndExit("tunnel")
return
}
}
listenURL, err := url.Parse(listenURLString)
if err != nil {
fmt.Printf("\nError: the given listen url '%s' was not a valid url: %s\n", listenURLString, err)
displayHelpAndExit("tunnel")
return
}
if listenURL.Scheme != "https" && listenURL.Scheme != "tls" && listenURL.Scheme != "tcp" {
fmt.Printf("\nError: the given listen url '%s' has an unsupported scheme '%s'\n", listenURL, listenURL.Scheme)
displayHelpAndExit("tunnel")
return
}
if listenURL.RawQuery != "" {
fmt.Printf("\nError: the given listen url '%s' should not have a query '?%s'\n", listenURL, listenURL.RawQuery)
displayHelpAndExit("tunnel")
return
}
if strings.ToLower(to) != "to" {
fmt.Printf("\nError: expected the argument '%s' to match the word 'to'\n", to)
displayHelpAndExit("tunnel")
return
}
localURL, err := url.Parse(localURLString)
if err != nil {
fmt.Printf("\nError: the given local url '%s' was not a valid url: %s\n", localURLString, err)
displayHelpAndExit("tunnel")
return
}
if localURL.Scheme != "http" && localURL.Scheme != "tcp" && localURL.Scheme != "file" {
fmt.Printf("\nError: the given local url '%s' has an unsupported scheme '%s'\n", localURL, localURL.Scheme)
os.Exit(1)
return
}
if localURL.Scheme == "file" && listenURL.Scheme != "https" {
fmt.Printf("\nError: Local URL '%s' is incompatible with listen URL '%s'.\nThe local file:// scheme requires the listen URL to use the https:// scheme.\n", localURL, listenURL)
os.Exit(1)
return
}
localPort := 0
if localURL.Scheme == "http" {
localPort = 80
}
if localURL.Port() != "" {
localPort, _ = strconv.Atoi(localURL.Port())
}
if localURL.Scheme != "file" && localPort == 0 {
fmt.Printf("\nError: Local URL '%s' is missing a port number. Port number is required for the %s:// scheme\n", localURL, localURL.Scheme)
os.Exit(1)
return
}
if localURL.RawQuery != "" {
fmt.Printf("\nError: the given local url '%s' should not have a query '?%s'\n", localURL, localURL.RawQuery)
os.Exit(1)
return
}
// end of client-side validation, need to grab the daemon status for the rest of the validation.
status, err = getStatus(true)
if err != nil {
fmt.Printf("\nError: %s\n\n", err)
os.Exit(1)
return
}
listenPort := 0
if listenURL.Scheme == "https" {
listenPort = 443
}
if listenURL.Scheme == "http" {
listenPort = 80
}
listenPortString := listenURL.Port()
if listenPortString != "" {
var err error
listenPort, err = strconv.Atoi(listenPortString)
if err != nil {
fmt.Printf("\nError: the given listen url '%s' has an invalid port '%s'\n", listenURL, listenPortString)
displayHelpAndExit("tunnel")
return
}
if listenURL.Scheme == "tcp" && (listenPort < status.TenantInfo.PortStart || listenPort > status.TenantInfo.PortEnd) {
fmt.Printf(`
Error: The given listen url '%s' has an out-of-range port '%d'
%s`, listenURL, listenPort, getTCPPortRangeWarning(status))
os.Exit(1)
return
}
}
if listenPort == 0 {
warning := ""
if listenURL.Scheme == "tcp" {
warning = getTCPPortRangeWarning(status)
}
fmt.Printf(`
Error: Unable to determine a listening port for the listen url '%s'. Please specify a listening port.
%s`, listenURL, warning)
displayHelpAndExit("tunnel")
return
}
listenHostname := strings.ToLower(listenURL.Hostname())
subdomain := ""
if listenURL.Scheme != "tcp" {
listenHostnameIsAuthorized := false
for _, hostname := range status.TenantInfo.AuthorizedDomains {
if hostname == listenHostname {
listenHostnameIsAuthorized = true
}
if strings.HasSuffix(listenHostname, hostname) {
listenHostnameIsAuthorized = true
subdomain = strings.Trim(strings.TrimSuffix(listenHostname, hostname), ".")
}
}
if !listenHostnameIsAuthorized {
printUnauthorizedDomainError(listenHostname, status.TenantInfo.AuthorizedDomains)
os.Exit(1)
return
}
} else {
fmt.Printf(`
Warning: the specified listen URL '%s' has a hostname '%s'. Hostname has no effect for tcp:// URLs, it should be omitted because the TCP protocol has no concept of a domain.
You may reach your TCP tunnels via *any* of your Greenhouse domains.
`, listenURL, listenHostname)
}
if serverName == "" || serverName == status.ServerName {
serverName = status.ServerName
} else {
foundServerName := false
foundServerNameCurrentState := "Unknown"
serverNamesSlice := []string{fmt.Sprintf("%s (self)", status.ServerName)}
for otherServersName, otherServersState := range status.TenantInfo.ClientStates {
if otherServersName != status.ServerName {
serverNamesSlice = append(serverNamesSlice, fmt.Sprintf("%s (%s)", otherServersName, otherServersState.CurrentState))
}
if serverName == otherServersName {
foundServerName = true
foundServerNameCurrentState = otherServersState.CurrentState
}
}
if !foundServerName {
fmt.Printf(`
Error: There is no server named '%s' currently associated to your greenhouse account.
Currently registered servers:
- %s
`, serverName, strings.Join(serverNamesSlice, "\n - "),
)
os.Exit(1)
return
} else if foundServerNameCurrentState != "ClientConnected" {
fmt.Printf(`
Error: The server '%s' has the state '%s'. Can't create a tunnel to it until it is connected.
`, serverName, foundServerNameCurrentState,
)
os.Exit(1)
return
}
}
destinationType := "local_port"
destinationHostname := "127.0.0.1"
destinationFolderPath := ""
if localURL.Scheme == "file" {
destinationType = "folder"
destinationFolderPath = localURL.Path
} else if localURL.Hostname() != "127.0.0.1" && localURL.Hostname() != "localhost" {
destinationType = "host_port"
destinationHostname = localURL.Hostname()
}
domain := strings.Trim(strings.TrimPrefix(listenHostname, subdomain), ".")
if listenURL.Scheme == "tcp" {
domain = status.TenantInfo.AuthorizedDomains[0]
}
tunnelToCreate = GUITunnel{
Protocol: listenURL.Scheme,
HasSubdomain: subdomain != "",
Subdomain: subdomain,
Domain: domain,
PublicPort: listenPort,
DestinationType: destinationType,
DestinationHostname: destinationHostname,
DestinationPort: localPort,
DestinationFolderPath: destinationFolderPath,
}
// End of command line parsing and validation logic, on to the validation/action logic
// that both --json and normal command line share.
}
listenHostname := tunnelToCreate.Domain
if tunnelToCreate.HasSubdomain {
listenHostname = fmt.Sprintf("%s.%s", tunnelToCreate.Subdomain, tunnelToCreate.Domain)
}
conflictIndex := -1
for i, otherTunnel := range status.GUITunnels {
otherTunnelListenHostname := otherTunnel.Domain
if otherTunnel.HasSubdomain {
otherTunnelListenHostname = fmt.Sprintf("%s.%s", otherTunnel.Subdomain, otherTunnel.Domain)
}
bothAreTCP := tunnelToCreate.Protocol == "tcp" && otherTunnel.Protocol == "tcp"
if otherTunnel.PublicPort == tunnelToCreate.PublicPort && (bothAreTCP || otherTunnelListenHostname == listenHostname) {
conflictIndex = i
}
}
toSendToDaemon := make([]GUITunnel, len(status.GUITunnels))
if conflictIndex != -1 {
displayHostname := listenHostname
if tunnelToCreate.Protocol == "tcp" {
displayHostname = ""
}
fmt.Printf(`
Conflict: Your greenhouse account already has a tunnel listening at '%s:%d'.
`, displayHostname, tunnelToCreate.PublicPort,
)
if !yesOrNoConfirmation("Do you want to overwrite it?") {
os.Exit(0)
return
}
for i, tunnel := range status.GUITunnels {
if i == conflictIndex {
toSendToDaemon[i] = tunnelToCreate
} else {
toSendToDaemon[i] = tunnel
}
}
} else {
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)
}
fmt.Print("\nYour tunnel was configured successfully!\n\n")
}

Loading…
Cancel
Save