1
0
Fork 0

Initial commit

master
q3k 2018-08-29 19:20:46 +01:00
commit ff5af6936d
6 changed files with 407 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*swp
proto/*pb.go

13
COPYING Normal file
View File

@ -0,0 +1,13 @@
Copyright (C) 2018 Serge Bazanski <q3k@hackerspace.pl>
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.

243
cli.go Normal file
View File

@ -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
}

131
main.go Normal file
View File

@ -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 {}
}

3
proto/generate.go Normal file
View File

@ -0,0 +1,3 @@
//go:generate protoc -I.. ../proxy.proto --go_out=plugins=grpc:.
package proto

15
proxy.proto Normal file
View File

@ -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);
};