Browse Source

started working on CLI, writing help pages.

main
forest 3 months ago
commit
56fd7fdd47
4 changed files with 174 additions and 0 deletions
  1. +9
    -0
      README.md
  2. +5
    -0
      go.mod
  3. +2
    -0
      go.sum
  4. +158
    -0
      main.go

+ 9
- 0
README.md View File

@ -0,0 +1,9 @@
# 🌱🏠🧑‍💻 greenhouse-cli
CLI stands for Command Line Interface.
This is a simple Greenhouse client application designed for technical users.
Technically the CLI only interacts with the [greenhouse-daemon](https://git.sequentialread.com/forest/greenhouse-daemon/), the daemon is the one who does all of the heavy lifting. This is similar to the separation between the docker CLI and the docker daemon, it is built this way for similar reasons.

+ 5
- 0
go.mod View File

@ -0,0 +1,5 @@
module git.sequentialread.com/forest/greenhouse-cli
go 1.16
require git.sequentialread.com/forest/pkg-errors v0.9.2

+ 2
- 0
go.sum View File

@ -0,0 +1,2 @@
git.sequentialread.com/forest/pkg-errors v0.9.2 h1:j6pwbL6E+TmE7TD0tqRtGwuoCbCfO6ZR26Nv5nest9g=
git.sequentialread.com/forest/pkg-errors v0.9.2/go.mod h1:8TkJ/f8xLWFIAid20aoqgDZcCj9QQt+FU+rk415XO1w=

+ 158
- 0
main.go View File

@ -0,0 +1,158 @@
package main
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"flag"
"fmt"
"io/ioutil"
"log"
"net"
"net/http"
"strings"
"time"
errors "git.sequentialread.com/forest/pkg-errors"
)
var httpClient *http.Client
var baseURL string
func main() {
hostname := flag.String("hostname", "localhost", "default: localhost. the IP address or hostname of the machine that the Greenhouse Daemon is running on.")
port := flag.Int("port", 9572, "default: 9572. the port number that the greenhouse daemon is listening on ")
caFile := flag.String("ca-file", "", "file path of a pem-encoded CA certificate to authenticate the greenhouse-daemon's TLS certificate. (default null)")
unixSocket := flag.String("unix", "", "file path of the unix socket that the Greenhouse Daemon is listening on (an alternative to hostname and port. default null)")
flag.Usage = func() {
fmt.Fprint(flag.CommandLine.Output(),
`Usage: greenhouse [OPTION]... COMMAND [COMMAND SPECIFIC OPTIONS]
Commands:
register GREENHOUSE_API_TOKEN [SERVER_NAME]
Registers this computer as a server.
You must register first before you can run any other commands, but you only have to do it once.
If a SERVER_NAME is not provided, it will use your computer's hostname as a default.
e.g:
greenhouse register 09hBjaaEXAMPLETOKENaa8f
greenhouse register 09hBjaaEXAMPLETOKENaa8f my_cool_server
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
rm SERVICE_NAME [SERVER_NAME]
Remove a tunnel by SERVICE_NAME. Only removes tunnels directed at *this* server by default,
but you may specify a server name to remove a tunnel from another server.
Specifying "*" as SERVICE_NAME will remove all services.
Specifying "*" as SERVER_NAME will remove the specified SERVICE_NAME from all servers.
e.g:
greenhouse rm old_http
greenhouse rm *
greenhouse rm old_http my_other_server
greenhouse rm * *
open LISTEN_URL --[SERVER_NAME:]SERVICE_NAME--> LOCAL_URL
open --json '{...}'
Open a new tunnel.
LISTEN_URL supports the protocol schemes https://, http://, tls://, and tcp://.
LOCAL_URL supports the protocol schemes http://, tcp://, and file://
e.g:
greenhouse open https://www.cli-user.greenhouseusers.com --www--> http://localhost:80
greenhouse open https://files.cli-user.greenhouseusers.com --files--> file:///home/cli-user/Public
greenhouse rm *
greenhouse rm old_http my_other_server
greenhouse rm * *
Options:`)
flag.PrintDefaults()
}
//command := flag.Arg(0)
flag.Parse()
if unixSocket != nil && *unixSocket != "" {
baseURL = "http://unix"
httpClient = &http.Client{
Transport: CreateUnixTransport(*unixSocket),
Timeout: 10 * time.Second,
}
} else {
baseURL = fmt.Sprintf("https://%s:%d", *hostname, *port)
httpClient = &http.Client{
Timeout: 10 * time.Second,
}
if caFile != nil && *caFile != "" {
caCertPool := x509.NewCertPool()
caCert, err := ioutil.ReadFile(*caFile)
if err != nil {
log.Fatal(err)
}
ok, errors := AppendCertsFromPEM(caCertPool, caCert)
if !ok {
errorStrings := make([]string, len(errors))
for i := range errors {
errorStrings[i] = fmt.Sprintf(" - %s", errors[i])
}
log.Fatalf("Failed to add CA certificate '%s' to cert pool because:\n%s\n", *caFile, strings.Join(errorStrings, "\n"))
}
tlsClientConfig := &tls.Config{
RootCAs: caCertPool,
}
tlsClientConfig.BuildNameToCertificate()
httpClient.Transport = &http.Transport{
TLSClientConfig: tlsClientConfig,
}
}
}
}
func CreateUnixTransport(socketFile string) *http.Transport {
return &http.Transport{
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
return net.Dial("unix", socketFile)
},
}
}
// This code was lifted from the golang standard library so I could add proper error handling / reporting to it.
func AppendCertsFromPEM(s *x509.CertPool, pemCerts []byte) (ok bool, errorz []error) {
errorz = []error{}
for len(pemCerts) > 0 {
var block *pem.Block
block, pemCerts = pem.Decode(pemCerts)
if block == nil {
errorz = append(errorz, errors.New("pem.Decode() was not able to find any valid PEM blocks in this file."))
break
}
if block.Type != "CERTIFICATE" || len(block.Headers) != 0 {
errorz = append(errorz, errors.New("a PEM block was skipped because it was not a certificate or did not contain any headers."))
continue
}
certBytes := block.Bytes
cert, err := x509.ParseCertificate(certBytes)
if err != nil {
errorz = append(errorz, errors.Wrap(err, "parsing a PEM block as a Certificate failed because: "))
continue
}
s.AddCert(cert)
ok = true
}
return ok, errorz
}

Loading…
Cancel
Save