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": idr, err := rc.Put(ctx, &proto.Route{Host: *routesCreateDomain}) if err != nil { ln.FatalErr(ctx, err, ln.Action("create new route")) } fmt.Println("created route with id " + idr.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}) }