From ff5af6936d4e38dafef52fa44a07033b6a53490f Mon Sep 17 00:00:00 2001 From: Sergiusz Bazanski Date: Wed, 29 Aug 2018 19:20:46 +0100 Subject: [PATCH] Initial commit --- .gitignore | 2 + COPYING | 13 +++ cli.go | 243 ++++++++++++++++++++++++++++++++++++++++++++++ main.go | 131 +++++++++++++++++++++++++ proto/generate.go | 3 + proxy.proto | 15 +++ 6 files changed, 407 insertions(+) create mode 100644 .gitignore create mode 100644 COPYING create mode 100644 cli.go create mode 100644 main.go create mode 100644 proto/generate.go create mode 100644 proxy.proto diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..b1a12610 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*swp +proto/*pb.go diff --git a/COPYING b/COPYING new file mode 100644 index 00000000..314a8897 --- /dev/null +++ b/COPYING @@ -0,0 +1,13 @@ +Copyright (C) 2018 Serge Bazanski + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/cli.go b/cli.go new file mode 100644 index 00000000..b9642cf1 --- /dev/null +++ b/cli.go @@ -0,0 +1,243 @@ +package main + +import ( + "context" + "fmt" + "strings" + + "github.com/golang/glog" + "github.com/ziutek/telnet" + "golang.org/x/net/trace" +) + +type cliClient struct { + conn *telnet.Conn + + username string + password string + + loggedIn bool + promptHostname string +} + +func newCliClient(c *telnet.Conn, username, password string) *cliClient { + return &cliClient{ + conn: c, + username: username, + password: password, + } +} + +func (c *cliClient) readUntil(ctx context.Context, delims ...string) (string, error) { + chStr := make(chan string, 1) + chErr := make(chan error, 1) + go func() { + s, err := c.conn.ReadUntil(delims...) + if err != nil { + chErr <- err + return + } + chStr <- string(s) + }() + + select { + case <-ctx.Done(): + return "", fmt.Errorf("context done") + case err := <-chErr: + c.trace(ctx, "readUntil failed: %v", err) + return "", err + case s := <-chStr: + c.trace(ctx, "readUntil <- %q", s) + return s, nil + + } +} + +func (c *cliClient) readString(ctx context.Context, delim byte) (string, error) { + chStr := make(chan string, 1) + chErr := make(chan error, 1) + go func() { + s, err := c.conn.ReadString(delim) + if err != nil { + chErr <- err + return + } + chStr <- s + }() + + select { + case <-ctx.Done(): + return "", fmt.Errorf("context done") + case err := <-chErr: + c.trace(ctx, "readString failed: %v", err) + return "", err + case s := <-chStr: + c.trace(ctx, "readString <- %q", s) + return s, nil + + } +} + +func (c *cliClient) writeLine(ctx context.Context, s string) error { + n, err := c.conn.Write([]byte(s + "\n")) + if got, want := n, len(s)+1; got != want { + err = fmt.Errorf("wrote %d bytes out of %d", got, want) + } + if err != nil { + c.trace(ctx, "writeLine failed: %v", err) + return err + } + c.trace(ctx, "writeLine -> %q", s) + return nil +} + +func (c *cliClient) trace(ctx context.Context, f string, parts ...interface{}) { + tr, ok := trace.FromContext(ctx) + if !ok { + fmted := fmt.Sprintf(f, parts...) + glog.Infof("[no trace] %s", fmted) + return + } + tr.LazyPrintf(f, parts...) +} + +func (c *cliClient) logIn(ctx context.Context) error { + if c.loggedIn { + return nil + } + + // Provide username. + prompt, err := c.readString(ctx, ':') + if err != nil { + return fmt.Errorf("could not read username prompt: %v", err) + } + if !strings.HasSuffix(prompt, "User:") { + return fmt.Errorf("invalid username prompt: %v", err) + } + if err := c.writeLine(ctx, c.username); err != nil { + return fmt.Errorf("could not write username: %v") + } + + // Provide password. + prompt, err = c.readString(ctx, ':') + if err != nil { + return fmt.Errorf("could not read password prompt: %v", err) + } + if !strings.HasSuffix(prompt, "Password:") { + return fmt.Errorf("invalid password prompt: %v", err) + } + if err := c.writeLine(ctx, c.password); err != nil { + return fmt.Errorf("could not write password: %v") + } + + // Get unprivileged prompt. + prompt, err = c.readString(ctx, '>') + if err != nil { + return fmt.Errorf("could not read unprivileged prompt: %v", err) + } + + parts := strings.Split(prompt, "\r\n") + c.promptHostname = strings.TrimSuffix(parts[len(parts)-1], ">") + + // Enable privileged mode. + + if err := c.writeLine(ctx, "enable"); err != nil { + return fmt.Errorf("could not write enable: %v") + } + + // Provide password (again) + prompt, err = c.readString(ctx, ':') + if err != nil { + return fmt.Errorf("could not read password prompt: %v", err) + } + if !strings.HasSuffix(prompt, "Password:") { + return fmt.Errorf("invalid password prompt: %v", err) + } + if err := c.writeLine(ctx, c.password); err != nil { + return fmt.Errorf("could not write password: %v") + } + + // Get privileged prompt. + prompt, err = c.readString(ctx, '#') + if err != nil { + return fmt.Errorf("could not read privileged prompt: %v", err) + } + + if !strings.HasSuffix(prompt, c.promptHostname+"#") { + return fmt.Errorf("unexpected privileged prompt: %v", prompt) + } + + // Disable pager. + if err := c.writeLine(ctx, "terminal length 0"); err != nil { + return fmt.Errorf("could not diable pager: %v", err) + } + prompt, err = c.readString(ctx, '#') + if err != nil { + return fmt.Errorf("could not disable pager: %v", err) + } + if !strings.HasSuffix(prompt, c.promptHostname+"#") { + return fmt.Errorf("unexpected privileged prompt: %v", prompt) + } + + // Success! + c.loggedIn = true + c.trace(ctx, "logged into %v", c.promptHostname) + return nil +} + +func (c *cliClient) runCommand(ctx context.Context, command string) ([]string, string, error) { + if err := c.logIn(ctx); err != nil { + return nil, "", fmt.Errorf("could not log in: %v", err) + } + + // First, synchronize to prompt. + attempts := 3 + for { + c.writeLine(ctx, "") + line, err := c.readString(ctx, '\n') + if err != nil { + return nil, "", fmt.Errorf("while synchronizing to prompt: %v", err) + } + line = strings.Trim(line, "\r\n") + if strings.HasSuffix(line, c.promptHostname+"#") { + break + } + + attempts -= 1 + if attempts == 0 { + return nil, "", fmt.Errorf("could not find prompt, last result %q", line) + } + } + + // Send comand. + c.writeLine(ctx, command) + + // First, read until prompt again. + if _, err := c.readUntil(ctx, c.promptHostname+"#"); err != nil { + return nil, "", fmt.Errorf("could not get command hostname echo: %v", err) + } + + loopback, err := c.readUntil(ctx, "\r\n") + if err != nil { + return nil, "", fmt.Errorf("could not get command loopback: %v", err) + } + loopback = strings.Trim(loopback, "\r\n") + c.trace(ctx, "effective command: %q", loopback) + + // Read until we have a standalone prompt with no newline afterwards. + data, err := c.readUntil(ctx, c.promptHostname+"#") + if err != nil { + return nil, "", fmt.Errorf("could not get command results: %v", err) + } + + lines := []string{} + for _, line := range strings.Split(data, "\r\n") { + if line == c.promptHostname+"#" { + break + } + lines = append(lines, line) + } + c.trace(ctx, "command %q returned lines: %v", command, lines) + + return lines, loopback, nil +} diff --git a/main.go b/main.go new file mode 100644 index 00000000..8f26e0bf --- /dev/null +++ b/main.go @@ -0,0 +1,131 @@ +package main + +import ( + "context" + "flag" + "net" + "net/http" + + "code.hackerspace.pl/q3k/hspki" + "github.com/golang/glog" + "github.com/q3k/statusz" + "github.com/ziutek/telnet" + "golang.org/x/net/trace" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/reflection" + "google.golang.org/grpc/status" + + pb "code.hackerspace.pl/q3k/m6220-proxy/proto" +) + +var ( + flagListenAddress string + flagDebugAddress string + flagSwitchAddress string + flagSwitchUsername string + flagSwitchPassword string +) + +func init() { + flag.Set("logtostderr", "true") +} + +type service struct { + connectionSemaphore chan int +} + +func (s *service) connect() (*cliClient, error) { + s.connectionSemaphore <- 1 + conn, err := telnet.Dial("tcp", flagSwitchAddress) + if err != nil { + <-s.connectionSemaphore + return nil, err + } + + cli := newCliClient(conn, flagSwitchUsername, flagSwitchPassword) + return cli, nil +} + +func (s *service) disconnect() { + <-s.connectionSemaphore +} + +func (s *service) RunCommand(ctx context.Context, req *pb.RunCommandRequest) (*pb.RunCommandResponse, error) { + if req.Command == "" { + return nil, status.Error(codes.InvalidArgument, "command cannot be null") + } + + cli, err := s.connect() + if err != nil { + return nil, status.Error(codes.Unavailable, "could not connect to backend") + } + defer s.disconnect() + + lines, effective, err := cli.runCommand(ctx, req.Command) + if err != nil { + return nil, err + } + res := &pb.RunCommandResponse{ + EffectiveCommand: effective, + Lines: lines, + } + return res, nil +} + +func main() { + flag.StringVar(&flagListenAddress, "listen_address", "127.0.0.1:42000", "Address to listen on for gRPC") + flag.StringVar(&flagDebugAddress, "debug_address", "127.0.0.1:42001", "Address to listen on for Debug HTTP") + flag.StringVar(&flagSwitchAddress, "switch_address", "127.0.0.1:23", "Telnet address of M6220") + flag.StringVar(&flagSwitchUsername, "switch_username", "admin", "Switch login username") + flag.StringVar(&flagSwitchPassword, "switch_password", "admin", "Switch login password") + flag.Parse() + + s := &service{ + connectionSemaphore: make(chan int, 1), + } + + grpc.EnableTracing = true + grpcLis, err := net.Listen("tcp", flagListenAddress) + if err != nil { + glog.Exitf("Could not listen on %v: %v", flagListenAddress, err) + } + grpcSrv := grpc.NewServer(hspki.WithServerHSPKI()...) + pb.RegisterM6220ProxyServer(grpcSrv, s) + reflection.Register(grpcSrv) + + glog.Infof("Starting gRPC on %v", flagListenAddress) + go func() { + if err := grpcSrv.Serve(grpcLis); err != nil { + glog.Exitf("Could not start gRPC: %v", err) + } + }() + + if flagDebugAddress != "" { + glog.Infof("Starting debug on %v", flagDebugAddress) + httpMux := http.NewServeMux() + httpMux.HandleFunc("/debug/status", statusz.StatusHandler) + httpMux.HandleFunc("/debug/requests", trace.Traces) + httpMux.HandleFunc("/", statusz.StatusHandler) + + httpLis, err := net.Listen("tcp", flagDebugAddress) + if err != nil { + glog.Exitf("Could not listen on %v: %v", flagDebugAddress, err) + } + httpSrv := &http.Server{ + Addr: flagDebugAddress, + Handler: httpMux, + } + + go func() { + if err := httpSrv.Serve(httpLis); err != nil { + glog.Exitf("Could not start HTTP server: %v", err) + } + }() + + } + + glog.Infof("Running!") + + select {} +} diff --git a/proto/generate.go b/proto/generate.go new file mode 100644 index 00000000..fc6193de --- /dev/null +++ b/proto/generate.go @@ -0,0 +1,3 @@ +//go:generate protoc -I.. ../proxy.proto --go_out=plugins=grpc:. + +package proto diff --git a/proxy.proto b/proxy.proto new file mode 100644 index 00000000..d35a2c30 --- /dev/null +++ b/proxy.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; +package proto; + +message RunCommandRequest { + string command = 1; +}; + +message RunCommandResponse { + string effective_command = 1; + repeated string lines = 2; +}; + +service M6220Proxy { + rpc RunCommand(RunCommandRequest) returns (RunCommandResponse); +};