diff --git a/.gitignore b/.gitignore index a1338d6..b68a628 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,6 @@ # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 .glide/ + +/apps/newuser/newuser +/apps/helloworld/helloworld \ No newline at end of file diff --git a/agent/handlers/executable.go b/agent/handlers/executable.go index 76e61f2..4bf94f9 100644 --- a/agent/handlers/executable.go +++ b/agent/handlers/executable.go @@ -9,7 +9,8 @@ import ( "net" "os" "os/exec" - "path/filepath" + + "github.com/BKellogg/DistributedLoadTester/shared/dir" ) // ExecutableHandler handles executables @@ -29,7 +30,7 @@ func ExecutableHandler(conn net.Conn) error { log.Printf("received connection from %s\n", conn.RemoteAddr().String()) write(fmt.Sprintf("Connection recieved. Processing request...\n"), conn) - fp := currentDir() + "/command" + fp := dir.CurrentDir() + "/command" // Open or create the file that the bytes will be written to. // Assign the executable permission to the file to the current user. @@ -115,10 +116,3 @@ func int64FromConn(conn net.Conn) (int64, error) { err := binary.Read(conn, binary.LittleEndian, &size) return size, err } - -// currentDir returns the directory that the application -// is being exucuted in -func currentDir() string { - dir, _ := filepath.Abs(filepath.Dir(os.Args[0])) - return dir -} diff --git a/btp/client.go b/btp/client.go new file mode 100644 index 0000000..11fc081 --- /dev/null +++ b/btp/client.go @@ -0,0 +1,96 @@ +package btp + +import ( + "encoding/binary" + "fmt" + "io" + "net" + "os" +) + +// RequestBuilder represents a builder +// for BTP requests +type RequestBuilder struct { + hasPayload bool + beenSent bool + address string + file string + payloadSize int64 + response io.ReadCloser +} + +// NewRequestBuilder returns an empty RequestBuilder +// pointed at the given address. +func NewRequestBuilder(address string) *RequestBuilder { + return &RequestBuilder{hasPayload: false, address: address} +} + +// SetFile sets the given file path as the file +// to send when the request is sent. +func (rb *RequestBuilder) SetFile(filepath string) { + rb.hasPayload = true + rb.file = filepath +} + +// Send sends the RequestBuilder's request. +// Returns an error if one occurred. +func (rb *RequestBuilder) Send() error { + if !rb.hasPayload { + return fmt.Errorf("btp: cannot send a request with no body") + } + if rb.beenSent { + return fmt.Errorf("btp: cannot sent a request that has already been sent") + } + + // open the file of this request builder + // report any errors that occur. + f, err := os.Open(rb.file) + if err != nil { + return fmt.Errorf("btp: error opening file: %v", err) + } + + // obtain the file statistic so we can get the size of the file + // report any errors that occur. + fstat, err := f.Stat() + if err != nil { + return fmt.Errorf("btp: error obtaining file statistics: %v", err) + } + + // open the connection to this request builder's + // address. Report any errors that occur. + conn, err := net.Dial("tcp", rb.address) + if err != nil { + return fmt.Errorf("btp: error dialing address: %v", rb.address) + } + + // write the size of the file to the connection + // report any errors that occur. + if err = binary.Write(conn, binary.LittleEndian, fstat.Size()); err != nil { + conn.Close() + return fmt.Errorf("btp: error writing file size: %v", err) + } + + // copy the contents of the file into the connection + // report any errors that occur. + if _, err = io.Copy(conn, f); err != nil { + conn.Close() + return fmt.Errorf("btp: error copying file into connection: %v", err) + } + + rb.response = conn + rb.beenSent = true + + return nil +} + +// Response gets the response readCloser from the connection. +// Returns an error if one occurred. +func (rb *RequestBuilder) Response() (io.ReadCloser, error) { + if !rb.beenSent { + return nil, fmt.Errorf("btp: cannot get the response of a request that has not been sent") + } + if rb.response == nil { + return nil, fmt.Errorf("btp: response is nil") + } + return rb.response, nil +} diff --git a/btp/handler.go b/btp/handler.go new file mode 100644 index 0000000..cb97e2d --- /dev/null +++ b/btp/handler.go @@ -0,0 +1,5 @@ +package btp + +// Handler defines the type of function that can be +// used as a BTP Handler +type Handler func(ResponseWriter, *Request) diff --git a/btp/reader.go b/btp/reader.go new file mode 100644 index 0000000..5de4c82 --- /dev/null +++ b/btp/reader.go @@ -0,0 +1,50 @@ +package btp + +import ( + "io" +) + +// Reader returns an io.Reader that +// returns io.EOF when it reads a +// specific number of bytes, regardless +// of if there are more bytes that could be +// read. The client of this reader will be +// unable to change the size of the reader +// so setting it to the size of the body is +// fine. +type Reader struct { + size int64 + reader io.Reader +} + +// Read reads from the current reader into the destination +// byte slice until either the destination is full, or +// the number of bytes specified when the reader was +// created has been hit. +func (r *Reader) Read(dst []byte) (int, error) { + if r.size == 0 { + return 0, io.EOF + } + numBytes, err := r.reader.Read(dst) + r.size -= int64(numBytes) + return numBytes, err +} + +// newReader creates a new Reader that wraps the given +// reader. The returned reader will return io.EOF when +// the source reader is exhausted or the number of bytes +// read is equal to the given size. +func newReader(size int64, r io.Reader) *Reader { + return &Reader{size: size, reader: r} +} + +// min returns the min value of the given +// integers. Needed since the math package +// doesn't implement this for ints, only +// floats. +func min(x, y int) int { + if x < y { + return x + } + return y +} diff --git a/btp/reader_test.go b/btp/reader_test.go new file mode 100644 index 0000000..733d07d --- /dev/null +++ b/btp/reader_test.go @@ -0,0 +1,83 @@ +package btp + +import ( + "bytes" + "io" + "io/ioutil" + "testing" +) + +func TestNewReader(t *testing.T) { + reader1 := bytes.NewReader([]byte("some string")) + reader2 := ioutil.NopCloser(reader1) + + cases := []struct { + size int64 + reader io.Reader + }{ + { + 1, + reader1, + }, + { + 18329320, + reader2, + }, + } + for _, c := range cases { + r := newReader(c.size, c.reader) + if r.size != c.size { + t.Fatalf("error in NewReader: sizes %d and %d did not match", r.size, c.size) + } + } +} + +func TestRead(t *testing.T) { + cases := []struct { + reader *Reader + expectedBytes []byte + }{ + { + newReader(10, bytes.NewReader([]byte("hi there!!"))), + []byte("hi there!!"), + }, + { + newReader(100, bytes.NewReader([]byte("hi there!!"))), + []byte("hi there!!"), + }, + { + newReader(10, bytes.NewReader([]byte("hi there!! how are you doing today?"))), + []byte("hi there!!"), + }, + { + newReader(1000, bytes.NewReader([]byte("hi there!! how are you doing today?"))), + []byte("hi there!! how are you doing today?"), + }, + { + newReader(0, bytes.NewReader([]byte("hi there!! how are you doing today?"))), + []byte(""), + }, + { + newReader(4, bytes.NewReader([]byte("pink fluffy ponies make me happy"))), + []byte("pink"), + }, + } + for _, c := range cases { + dst := make([]byte, len(c.expectedBytes)) + numRead, err := c.reader.Read(dst) + if err != nil { + if err == io.EOF { + continue + } + t.Fatalf("error reading from Reader: %v", err) + } + if numRead != len(c.expectedBytes) { + t.Fatalf("%d bytes read did not match expected number %d", numRead, len(c.expectedBytes)) + } + stringIn, stringOut := string(c.expectedBytes), string(dst) + if stringIn != stringOut { + t.Fatalf("input %s did not match %s output", stringIn, stringOut) + } + + } +} diff --git a/btp/server.go b/btp/server.go new file mode 100644 index 0000000..0afba6c --- /dev/null +++ b/btp/server.go @@ -0,0 +1,45 @@ +package btp + +import ( + "fmt" + "log" + "net" +) + +// Listen starts a btp server listening on the given +// address and handles all requests with the given +// handler. Returns an error if one occurred. +// +// Only errors encountered during startup will be returned. +// Errors encountered during while processing a specific +// connection during any point in it's lifecycle will not +// be returned here. +// +// This function is a blocking function and will never exit +// once properly started. +func Listen(addr string, handler Handler) error { + l, err := net.Listen("tcp", addr) + if err != nil { + return fmt.Errorf("error listening in %s: %v", addr, err) + } + for { + conn, err := l.Accept() + if err != nil { + log.Printf("error accepting connection: %v\n", err) + } + go serveRequest(conn, handler) + } +} + +// serveRequest serves the given connection with the +// given handler. Closes the connection when the serving +// is complete. +func serveRequest(conn net.Conn, handler Handler) { + w, r, err := fullCycleFromConn(conn) + if err != nil { + log.Printf("error getting conn lifecycle: %v", err) + conn.Close() + } + handler(w, r) + conn.Close() +} diff --git a/btp/serverconn.go b/btp/serverconn.go new file mode 100644 index 0000000..144cb71 --- /dev/null +++ b/btp/serverconn.go @@ -0,0 +1,99 @@ +package btp + +import ( + "encoding/binary" + "errors" + "fmt" + "io" + "net" + "os" +) + +// Request represents the structure of a +// request sent to a btp server. +type Request struct { + PayloadSize int64 + Payload io.Reader +} + +// WritePayloadToFile writes the payload in the current request +// to a file on the host machine at the given directory with the given name. +// Returns the filepath of the new file, the number of bytes written to that +// file, and an error if one occured. +// If the directory is not given, the current directory will be used. +func (r *Request) WritePayloadToFile(fileName, dir string) (string, int64, error) { + if len(fileName) == 0 { + return "", -1, errors.New("btp: cannot write to a file with no name") + } + if len(dir) == 0 { + dir = currentDir() + } + + f, err := os.OpenFile(dir+"/"+fileName, os.O_WRONLY|os.O_CREATE, 0744) + if err != nil { + return "", -1, fmt.Errorf("btp: error creating file: %v", err) + } + defer f.Close() + + // Copy the bytes from the request payload into + // the file that was just created. + numBytes, err := io.Copy(f, r.Payload) + if err != nil { + return "", 0, fmt.Errorf("btp: error copying bytes from payload to file: %v", err) + } + + return dir + "/" + fileName, int64(numBytes), err +} + +// requestFromConn returns a pointer to a Request that +// is associated with the given net.Conn. +func requestFromConn(conn net.Conn) (*Request, error) { + payloadSize, err := nextInt64FromConn(conn) + if err != nil { + return nil, err + } + return &Request{ + PayloadSize: payloadSize, + Payload: newReader(payloadSize, conn), + }, nil +} + +// ResponseWriter represents the structure and +// functionality that a btp server has to respond +// to a client. +type ResponseWriter struct { + io.Writer +} + +// WriteString writes the string to the given response +// writer. Returns the number bytes written or an error +// if one occurred. +func (rw ResponseWriter) WriteString(p string) (int, error) { + return rw.Write([]byte(p)) +} + +// resWriterFromConn returns a ResponseWriter that +// is associated with the given net.Conn. +func resWriterFromConn(conn net.Conn) ResponseWriter { + return ResponseWriter{Writer: conn} +} + +// fullCycleFromConn returns a ResponseWriter and a Request that +// are associated with the given conn. Returns an error if one +// occurred. +func fullCycleFromConn(conn net.Conn) (ResponseWriter, *Request, error) { + req, err := requestFromConn(conn) + if err != nil { + return ResponseWriter{}, nil, fmt.Errorf("error creating request with the given conn: %v", err) + } + return resWriterFromConn(conn), req, nil +} + +// nextInt64FromConn reads the first 8 bytes of the connection +// into an int64 and returns it. Assumes that the first 8 bytes represent a +// valid int64. Returns an error if one occurred. +func nextInt64FromConn(conn net.Conn) (int64, error) { + var size int64 + err := binary.Read(conn, binary.LittleEndian, &size) + return size, err +} diff --git a/btp/serverconn_test.go b/btp/serverconn_test.go new file mode 100644 index 0000000..7756299 --- /dev/null +++ b/btp/serverconn_test.go @@ -0,0 +1,53 @@ +package btp + +import ( + "bytes" + "testing" +) + +func TestRWWrite(t *testing.T) { + cases := []struct { + payload []byte + }{ + { + []byte("hello there"), + }, + { + []byte("i love playing with ponies"), + }, + { + []byte("stable genius"), + }, + { + []byte("i am like, very smart"), + }, + { + []byte("*@&!)!@*(@#(dkjfhs7aksdj??sd'$%"), + }, + { + []byte(""), + }, + } + for _, c := range cases { + var b bytes.Buffer + rw := ResponseWriter{Writer: &b} + payloadLength := len(c.payload) + + numWritten, err := rw.Write(c.payload) + if err != nil { + t.Fatalf("error writing payload to ResponseWriter: %v", err) + } + if numWritten != payloadLength { + t.Fatalf("number bytes written %d did not match expected number of bytes written %d", + numWritten, payloadLength) + } + output := make([]byte, payloadLength) + _, err = b.Read(output) + if err != nil { + t.Fatalf("error reading from buffer: %v", err) + } + if !bytes.Equal(c.payload, output) { + t.Fatal("payload bytes did not match the returned output bytes") + } + } +} diff --git a/btp/utils.go b/btp/utils.go new file mode 100644 index 0000000..2952bbb --- /dev/null +++ b/btp/utils.go @@ -0,0 +1,13 @@ +package btp + +import ( + "os" + "path/filepath" +) + +// currentDir returns the directory that the application +// is being exucuted in +func currentDir() string { + dir, _ := filepath.Abs(filepath.Dir(os.Args[0])) + return dir +} diff --git a/btpclient/main.go b/btpclient/main.go new file mode 100644 index 0000000..086a26d --- /dev/null +++ b/btpclient/main.go @@ -0,0 +1,57 @@ +package main + +import ( + "fmt" + "io" + "log" + "os" + "path/filepath" + + "github.com/BKellogg/DistributedLoadTester/btp" +) + +const poorSucker = "localhost:8080" + +func main() { + + if len(os.Args) < 2 { + log.Fatal(`usage: + btpclient `) + } + appPath := os.Args[1] + fullPath, err := filepath.Abs(appPath) + if err != nil { + log.Fatalf("error getting absolute path: %v", err) + } + + reqBuilder := btp.NewRequestBuilder(poorSucker) + reqBuilder.SetFile(fullPath) + if err := reqBuilder.Send(); err != nil { + log.Fatalf("error sending request: %v", err) + } + + response, err := reqBuilder.Response() + if err != nil { + log.Fatalf("error getting response: %v", err) + } + defer response.Close() + + // Read from the conneciton until the connection closes. + if err = copyFromRCIntoStdOut(response); err != nil { + log.Fatalf("error printing from readcloser: %v", err) + } +} + +// copyRCConnIntoStdOut reads and prints all messages from the readcloser +// until the process is terminated, an error occurs, or the connection is +// closed. +func copyFromRCIntoStdOut(rc io.ReadCloser) error { + fmt.Printf("==== Start of connection read ====\n\n") + numBytes, err := io.Copy(os.Stdout, rc) + fmt.Printf("\n\n==== End of connection read ====\n") + fmt.Printf("total bytes read: %d", numBytes) + if err != nil && err != io.EOF { + return fmt.Errorf("error copying into connection into standard out: %v", err) + } + return nil +} diff --git a/btpserver/binaryhandler.go b/btpserver/binaryhandler.go new file mode 100644 index 0000000..1de38cc --- /dev/null +++ b/btpserver/binaryhandler.go @@ -0,0 +1,48 @@ +package main + +import ( + "fmt" + "io" + "os" + "os/exec" + + "github.com/BKellogg/DistributedLoadTester/btp" +) + +// BinaryHandler handles requests that contain binaries as the +// payload. +func BinaryHandler(w btp.ResponseWriter, r *btp.Request) { + + fp, numBytes, err := r.WritePayloadToFile("command", "") + if err != nil { + w.WriteString(fmt.Sprintf("error writing payload to file: %v", err)) + return + } + + fmt.Printf("paylaod size: %d\n", r.PayloadSize) + fmt.Printf("read %d bytes into a file\n", numBytes) + + // Executre the file at the "fp" path + // and write its output back to the client. + cmd := exec.Command(fp) + cmd.Stdout = io.MultiWriter(w, os.Stdout) + if err != nil { + w.WriteString(fmt.Sprintf("error getting command stdout pipe: %v", err)) + return + } + err = cmd.Start() + if err != nil { + w.WriteString(fmt.Sprintf("error starting command: %v\n", err)) + return + } + fmt.Printf("==== Begin program output ====\n\n") + err = cmd.Wait() + if err != nil { + w.WriteString(fmt.Sprintf("error waiting for command to finish: %v\n", err)) + return + } + fmt.Printf("\n\n==== End program output ====\n") + + // one occurred + w.WriteString("success!") +} diff --git a/btpserver/command b/btpserver/command new file mode 100755 index 0000000..05d9042 Binary files /dev/null and b/btpserver/command differ diff --git a/btpserver/main.go b/btpserver/main.go new file mode 100644 index 0000000..2a1645d --- /dev/null +++ b/btpserver/main.go @@ -0,0 +1,13 @@ +package main + +import ( + "log" + + "github.com/BKellogg/DistributedLoadTester/btp" +) + +func main() { + addr := "localhost:8080" + log.Printf("btp listening on %s...\n", addr) + log.Fatal(btp.Listen(addr, BinaryHandler)) +} diff --git a/shared/dir/dir.go b/shared/dir/dir.go new file mode 100644 index 0000000..fd78dc9 --- /dev/null +++ b/shared/dir/dir.go @@ -0,0 +1,13 @@ +package dir + +import ( + "os" + "path/filepath" +) + +// CurrentDir returns the directory that the application +// is being exucuted in +func CurrentDir() string { + dir, _ := filepath.Abs(filepath.Dir(os.Args[0])) + return dir +} diff --git a/shared/sig/sign.go b/shared/sig/sign.go new file mode 100644 index 0000000..7c48b1d --- /dev/null +++ b/shared/sig/sign.go @@ -0,0 +1,13 @@ +package sig + +import ( + "crypto/rsa" + "io" +) + +// SignFromReaderToWriter signs the bytes from src io.Reader and writes them +// to the dst io.Writer using the given PrivateKey privkey. Returns an error +// if one occurred. +func SignFromReaderToWriter(src io.Reader, dst io.Writer, privkey *rsa.PrivateKey) error { + panic("TODO") +} diff --git a/tools/rsapair/main.go b/tools/rsapair/main.go new file mode 100644 index 0000000..d7860a6 --- /dev/null +++ b/tools/rsapair/main.go @@ -0,0 +1,79 @@ +package main + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "errors" + "fmt" + "log" + "os" + + "github.com/BKellogg/DistributedLoadTester/shared/dir" +) + +// KeyBits defines how many bits should be in the generated +// rsa public/private key pair. +const KeyBits = 2048 + +func main() { + + fmt.Printf("creating a %d bit rsa private/public key pair...", KeyBits) + + dir := dir.CurrentDir() + rng := rand.Reader + + privkey, err := rsa.GenerateKey(rng, KeyBits) + if err != nil { + log.Fatalf("error generating rsa key pair: %v", err) + } + pubkey := &privkey.PublicKey + + if err = writePrivKey(privkey, dir); err != nil { + log.Fatalf("error writing privkey to disk: %v", err) + } + if err = writePubKey(pubkey, dir); err != nil { + log.Fatalf("error writing pubkey to disk %v", err) + } + fmt.Printf("done\n") + fmt.Printf("private key has been written to %s\n", dir+"/privkey") + fmt.Printf("public key has been written to %s", dir+"/pubkey") +} + +// writePubKey writes the public key to a file name "pubkey" +// in the given path. Returns an error if one occurred. +func writePubKey(pubkey *rsa.PublicKey, path string) error { + keyBytes := x509.MarshalPKCS1PublicKey(pubkey) + f, err := os.OpenFile(path+"/pubkey", os.O_WRONLY|os.O_CREATE, 0744) + if err != nil { + return errors.New("error creating pubkey file: " + err.Error()) + } + defer f.Close() + bytesWritten, err := f.Write(keyBytes) + if err != nil { + return errors.New("error writing privkey bytes to file: " + err.Error()) + } + if bytesWritten != len(keyBytes) { + return errors.New("number of bytes written to file does not match length of key") + } + return nil +} + +// writePrivKey writes the private key to a file name "privkey" +// in the given path. Returns an error if one occurred. +func writePrivKey(privkey *rsa.PrivateKey, path string) error { + keyBytes := x509.MarshalPKCS1PrivateKey(privkey) + f, err := os.OpenFile(path+"/privkey", os.O_WRONLY|os.O_CREATE, 0744) + if err != nil { + return errors.New("error creating privkey file: " + err.Error()) + } + defer f.Close() + bytesWritten, err := f.Write(keyBytes) + if err != nil { + return errors.New("error writing privkey bytes to file: " + err.Error()) + } + if bytesWritten != len(keyBytes) { + return errors.New("number of bytes written to file does not match length of key") + } + return nil +}