362 lines
10 KiB
Go
362 lines
10 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
|
|
"go.uber.org/atomic"
|
|
|
|
"git.xeserv.us/xena/route/internal/database"
|
|
"git.xeserv.us/xena/route/internal/routecrypto"
|
|
proto "git.xeserv.us/xena/route/proto"
|
|
"github.com/Xe/ln"
|
|
"github.com/Xe/uuid"
|
|
jwtcreds "github.com/Xe/x/tools/svc/credentials/jwt"
|
|
"github.com/dickeyxxx/netrc"
|
|
"github.com/kr/pretty"
|
|
"github.com/olekukonko/tablewriter"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/credentials"
|
|
"gopkg.in/alecthomas/kingpin.v2"
|
|
)
|
|
|
|
var allScopes = []string{
|
|
"token:get", "token:getall", "token:put", "token:delete", "token:deactivate",
|
|
"route:get", "route:getall", "route:put", "route:delete",
|
|
"connect",
|
|
"backend:list", "backend:kill",
|
|
"admin",
|
|
}
|
|
|
|
var whoami string
|
|
|
|
func userHomeDir() string {
|
|
if runtime.GOOS == "windows" {
|
|
home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH")
|
|
if home == "" {
|
|
home = os.Getenv("USERPROFILE")
|
|
}
|
|
return home
|
|
}
|
|
return os.Getenv("HOME")
|
|
}
|
|
|
|
var (
|
|
app = kingpin.New("route", "An interface to routed")
|
|
grpcServer = app.Flag("routed-addr", "routed grpc address").Default("127.0.0.1:7268").String()
|
|
netrcPath = app.Flag("netrc", "netrc path").Default(filepath.Join(userHomeDir(), ".netrc")).String()
|
|
|
|
generateKey = app.Command("generate-key", "generate SSL cert crypto key")
|
|
|
|
backends = app.Command("backend", "backend management")
|
|
|
|
backendList = backends.Command("list", "list connected backends")
|
|
backendListDomain = backendList.Flag("domain", "if set, match backends against this domain name").String()
|
|
backendListUser = backendList.Flag("user", "if set, match backends against this username").String()
|
|
|
|
backendKill = backends.Command("kill", "forcibly disconnect a given backend")
|
|
backendKillID = backendKill.Flag("id", "the ID of the backend to remove").Required().String()
|
|
|
|
routes = app.Command("route", "route management")
|
|
|
|
routesCreate = routes.Command("create", "create a new route")
|
|
routesCreateDomain = routesCreate.Flag("domain", "domain for the route (if not given one will be generated for you)").String()
|
|
|
|
routesInspect = routes.Command("inspect", "inspect one route")
|
|
routesInspectDomain = routesInspect.Flag("domain", "domain to inspect").Required().String()
|
|
|
|
routesList = routes.Command("list", "list all routes owned by you")
|
|
|
|
routesRm = routes.Command("rm", "remove a route")
|
|
routesRmID = routesRm.Flag("id", "route ID to remove").Required().String()
|
|
|
|
testServer = app.Command("test-server", "spawn a simple HTTP test server on a TCP address")
|
|
testServerAddr = testServer.Flag("addr", "TCP address").Default(":9090").String()
|
|
|
|
token = app.Command("token", "token management")
|
|
|
|
tokenCreate = token.Command("create", "create a new token")
|
|
tokenCreateScopes = tokenCreate.Flag("scope", "token scopes").Default(allScopes...).Strings()
|
|
|
|
tokenGenerate = token.Command("generate-root", "generate a root token")
|
|
tokenGenerateKey = tokenGenerate.Flag("key", "token crypto key").Required().String()
|
|
tokenGenerateScopes = tokenGenerate.Flag("scopes", "token scopes").Default(allScopes...).Strings()
|
|
tokenGenerateUsername = tokenGenerate.Flag("username", "token username").Required().String()
|
|
tokenGenerateDatabasePath = tokenGenerate.Flag("db", "database file to add the root token to").Required().String()
|
|
|
|
tokenInspect = token.Command("inspect", "inspect a token")
|
|
tokenInspectID = tokenInspect.Flag("token-id", "token id").Required().String()
|
|
|
|
tokenList = token.Command("list", "list all tokens belonging to you")
|
|
tokenListDeactivated = tokenList.Flag("deactivated", "list deactivated tokens?").Default("false").Bool()
|
|
|
|
tokenRm = token.Command("rm", "remove a token")
|
|
tokenRmHard = tokenRm.Flag("hard", "hard-delete the token instead of deactivating it").Default("false").Bool()
|
|
tokenRmID = tokenRm.Flag("id", "token id").Required().String()
|
|
)
|
|
|
|
var (
|
|
hits *atomic.Int64
|
|
)
|
|
|
|
func init() {
|
|
hits = atomic.NewInt64(0)
|
|
}
|
|
|
|
func handle(w http.ResponseWriter, r *http.Request) {
|
|
fmt.Fprintln(w, "Route is go!")
|
|
fmt.Fprintf(w, "%s\n", pretty.Sprintf("%s", r.Header))
|
|
hn, _ := os.Hostname()
|
|
fmt.Fprintf(w, "Served by %s running %s\n", hn, runtime.GOOS)
|
|
fmt.Fprintf(w, "Hit count: %d", hits.Inc())
|
|
|
|
ip := r.Header.Get("X-Remote-Ip")
|
|
if ip != "" {
|
|
log.Printf("Hit from %s: %s", ip, r.RequestURI)
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
cmdline := kingpin.MustParse(app.Parse(os.Args[1:]))
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
retry_netrc:
|
|
n, err := netrc.Parse(*netrcPath)
|
|
if err != nil {
|
|
_, err := os.Stat(*netrcPath)
|
|
if err == os.ErrNotExist {
|
|
fout, err := os.Create(*netrcPath)
|
|
if err != nil {
|
|
ln.FatalErr(ctx, err, ln.Action("creating netrc"), ln.F{"path": *netrcPath})
|
|
}
|
|
fout.Close()
|
|
goto retry_netrc
|
|
}
|
|
}
|
|
_ = n
|
|
|
|
switch cmdline {
|
|
case "test-server":
|
|
http.HandleFunc("/", handle)
|
|
|
|
ln.FatalErr(ctx, http.ListenAndServe(*testServerAddr, nil), ln.Action("test server listenAndServe"))
|
|
|
|
return
|
|
|
|
case "generate-key":
|
|
key, err := routecrypto.GenerateKey()
|
|
if err != nil {
|
|
ln.FatalErr(ctx, err, ln.Action("generating encryption key"))
|
|
}
|
|
|
|
fmt.Println("Your key is:", routecrypto.ShowKey(key))
|
|
|
|
return
|
|
|
|
case "token generate-root":
|
|
key, err := routecrypto.ParseKey(*tokenGenerateKey)
|
|
if err != nil {
|
|
ln.FatalErr(ctx, err, ln.Action("parsing encryption key"))
|
|
}
|
|
db, err := database.NewBoltStorage(*tokenGenerateDatabasePath, key)
|
|
if err != nil {
|
|
ln.FatalErr(ctx, err, ln.Action("opening routed database"))
|
|
}
|
|
|
|
tBody := uuid.New()
|
|
|
|
_, err = db.PutToken(context.Background(), tBody, *tokenGenerateUsername, *tokenGenerateScopes)
|
|
if err != nil {
|
|
ln.FatalErr(ctx, err, ln.Action("add newly created token to database"))
|
|
}
|
|
defer db.Close()
|
|
|
|
fmt.Println("Your token is:", tBody)
|
|
|
|
n.AddMachine(*grpcServer, *tokenGenerateUsername, tBody)
|
|
err = n.Save()
|
|
if err != nil {
|
|
ln.FatalErr(ctx, err, ln.Action("add machine to netrc"))
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
m := n.Machine(*grpcServer)
|
|
|
|
connCreds := credentials.NewTLS(&tls.Config{
|
|
InsecureSkipVerify: true,
|
|
})
|
|
creds := jwtcreds.NewFromToken(m.Get("password"))
|
|
conn, err := grpc.Dial(*grpcServer,
|
|
grpc.WithTransportCredentials(connCreds),
|
|
grpc.WithPerRPCCredentials(creds))
|
|
if err != nil {
|
|
ln.FatalErr(ctx, err, ln.Action("dialing grpc server"), ln.F{"hostname": *grpcServer})
|
|
}
|
|
|
|
rc := proto.NewRoutesClient(conn)
|
|
tc := proto.NewTokensClient(conn)
|
|
bc := proto.NewBackendsClient(conn)
|
|
|
|
_ = rc
|
|
_ = tc
|
|
_ = bc
|
|
|
|
switch cmdline {
|
|
case "route create":
|
|
rt, err := rc.Put(ctx, &proto.Route{Host: *routesCreateDomain})
|
|
if err != nil {
|
|
ln.FatalErr(ctx, err, ln.Action("create new route"))
|
|
}
|
|
|
|
fmt.Printf("created route for domain %s, id: %s\n", rt.Host, rt.Id)
|
|
|
|
return
|
|
|
|
case "route inspect":
|
|
r, err := rc.Get(context.Background(), &proto.GetRouteRequest{
|
|
Host: *routesCreateDomain,
|
|
})
|
|
if err != nil {
|
|
ln.FatalErr(ctx, err, ln.Action("get single route"), ln.F{"domain": *routesCreateDomain})
|
|
}
|
|
|
|
json.NewEncoder(os.Stdout).Encode(r)
|
|
fmt.Println()
|
|
|
|
return
|
|
|
|
case "route list":
|
|
rts, err := rc.GetAll(context.Background(), &proto.Nil{})
|
|
if err != nil {
|
|
ln.FatalErr(ctx, err, ln.Action("get all routes"))
|
|
}
|
|
|
|
table := tablewriter.NewWriter(os.Stdout)
|
|
table.SetHeader([]string{"ID", "Host"})
|
|
|
|
for _, r := range rts.Routes {
|
|
table.Append([]string{r.Id, r.Host})
|
|
}
|
|
|
|
table.Render()
|
|
|
|
return
|
|
|
|
case "route rm":
|
|
_, err := rc.Delete(context.Background(), &proto.Route{Id: *routesRmID})
|
|
if err != nil {
|
|
ln.FatalErr(ctx, err, ln.Action("remove single route"), ln.F{"id": *routesRmID})
|
|
}
|
|
|
|
return
|
|
|
|
case "backend list":
|
|
bkds, err := bc.List(context.Background(), &proto.BackendSelector{
|
|
Domain: *backendListDomain,
|
|
User: *backendListUser,
|
|
})
|
|
if err != nil {
|
|
ln.FatalErr(ctx, err, ln.Action("list backends"))
|
|
}
|
|
|
|
table := tablewriter.NewWriter(os.Stdout)
|
|
table.SetHeader([]string{"ID", "Proto", "User", "Domain", "Failure Chance", "Host"})
|
|
|
|
for _, bknd := range bkds.Backends {
|
|
table.Append([]string{bknd.Id, bknd.Proto, bknd.User, bknd.Domain, fmt.Sprint(bknd.Phi), bknd.Host})
|
|
}
|
|
|
|
table.Render()
|
|
|
|
return
|
|
|
|
case "backend kill":
|
|
_, err := bc.Kill(context.Background(), &proto.BackendID{Id: *backendKillID})
|
|
if err != nil {
|
|
ln.FatalErr(ctx, err, ln.Action("attempt to kill backend"), ln.F{"backend_id": *backendKillID})
|
|
}
|
|
|
|
fmt.Println("killed backend " + *backendKillID)
|
|
|
|
return
|
|
|
|
case "token list":
|
|
lis, err := tc.GetAll(ctx, &proto.Nil{})
|
|
if err != nil {
|
|
ln.FatalErr(ctx, err, ln.Action("get all tokens"))
|
|
}
|
|
|
|
table := tablewriter.NewWriter(os.Stdout)
|
|
table.SetHeader([]string{"ID", "Active", "Scopes"})
|
|
|
|
for _, tkn := range lis.Tokens {
|
|
table.Append([]string{tkn.Id, fmt.Sprint(tkn.Active), fmt.Sprint(tkn.Scopes)})
|
|
}
|
|
|
|
table.Render()
|
|
|
|
return
|
|
|
|
case "token create":
|
|
scps := *tokenCreateScopes
|
|
tkn := &proto.Token{
|
|
Scopes: scps,
|
|
}
|
|
|
|
ftkn, err := tc.Put(ctx, tkn)
|
|
if err != nil {
|
|
ln.FatalErr(ctx, err, ln.Action("put token to server"))
|
|
}
|
|
|
|
fmt.Printf("Your token is: %s\n", ftkn.Body)
|
|
fmt.Printf("It has permission for the following scopes: %v\n", ftkn.Scopes)
|
|
|
|
return
|
|
|
|
case "token inspect":
|
|
tkn, err := tc.Get(ctx, &proto.GetTokenRequest{Id: *tokenInspectID})
|
|
if err != nil {
|
|
ln.FatalErr(ctx, err, ln.Action("fetch token from server"), ln.F{"token_id": *tokenInspectID})
|
|
}
|
|
|
|
json.NewEncoder(os.Stdout).Encode(tkn)
|
|
|
|
return
|
|
|
|
case "token rm":
|
|
tkn, err := tc.Get(ctx, &proto.GetTokenRequest{Id: *tokenRmID})
|
|
if err != nil {
|
|
ln.FatalErr(ctx, err, ln.Action("fetch token from server"), ln.F{"token_id": *tokenRmID})
|
|
}
|
|
|
|
var action ln.Fer
|
|
if *tokenRmHard {
|
|
_, err = tc.Delete(ctx, tkn)
|
|
action = ln.Action("actually delete token")
|
|
} else {
|
|
_, err = tc.Deactivate(ctx, tkn)
|
|
action = ln.Action("deactivate token")
|
|
}
|
|
|
|
if err != nil {
|
|
ln.FatalErr(ctx, err, ln.F{"token_id": *tokenRmID}, action)
|
|
}
|
|
|
|
fmt.Printf("token with id %s and body %s removed.\n", tkn.Id, tkn.Body)
|
|
|
|
return
|
|
}
|
|
|
|
ln.Fatal(ctx, ln.Action("not implemented"), ln.F{"command": cmdline})
|
|
}
|