diff --git a/args/args.go b/args/args.go index 4d1bda8..e77d4af 100644 --- a/args/args.go +++ b/args/args.go @@ -5,7 +5,6 @@ package args import ( "flag" - "os" ) /* Method to trigger flag parsing. Can be called multiple times safely. */ @@ -13,11 +12,6 @@ func Parse() { if (flagsParsed) {return} flag.Parse(); flagsParsed = true; - - // Process DBUS socket location - if socketLocation == nil || *socketLocation == "" { - *socketLocation = os.Getenv("DBUS_SYSTEM_BUS_ADDRESS"); - } } /* module-specific variable to avoid re-parsing flags */ var flagsParsed bool = false; @@ -38,18 +32,10 @@ func GetHTTPPort() (port int, set bool) { return *httpPort, true; } -/* Listen on a UNIX socket */ -var socketLocation = flag.String("socket", "", "Location of UNIX socket to listen on. Setting will disable TCP.") -/* @return set boolean will be true if argument is not nil */ -func GetSocketLocation() (port string, set bool) { - if socketLocation == nil {return "", false} - return *socketLocation, true; -} - /* Where the signal-cli binary is */ var binaryLocation = flag.String("binary", "/usr/local/bin/signal-cli", "Location of the signal-cli binary.") /* @return set boolean will be true if argument is not nil */ -func GetBinaryLocation() (port string, set bool) { +func GetBinaryLocation() (binary string, set bool) { if binaryLocation == nil {return "", false} return *binaryLocation, true; } \ No newline at end of file diff --git a/args/readme.md b/args/readme.md index f312cf6..bb2a420 100644 --- a/args/readme.md +++ b/args/readme.md @@ -3,6 +3,12 @@ This module handles command line arguments. A list follows. You can also pass through -h for this list. ``` --conf string - Config file to read from (default "./config.txt") -``` \ No newline at end of file +-auth string + Authorization file to read from (default "./auth.json") +-binary string + Location of the signal-cli binary. (default "/usr/local/bin/signal-cli") +-port int + Port number to bind to (default 11938) +``` + +Note: the `dummy,py` python file echoes back a valid JSON with just the "id" key in it, for every JSON sent to it with the "id" key. It's useful for testing things that don't need signal-cli specifically. \ No newline at end of file diff --git a/auth/auth.go b/auth/auth.go index 2498dc6..b26002c 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -1,7 +1,7 @@ package auth -/* This file contains the AuthAuthConfig object and its methods, which handle reading - from a config file and matching requests to the whitelist. */ +/* This file contains the AuthAuthConfig object and its methods, which handle + reading from a config file and matching requests to the whitelist. */ import ( "errors" @@ -9,10 +9,10 @@ import ( ) /* Stores a map between a string (bearer token) and a list of unmarshaled JSONS */ -var authConfig any; +var authConfig map[string][]any = make(map[string][]any); var authConfigSetup bool = false; -/* Opens and reads a file at the path */ +/* Opens, reads, and parses a file at the path */ func SetupAuthConfig(filePath string) (err error) { if authConfigSetup {return errors.New("Auth configuration already set up!")} @@ -21,17 +21,47 @@ func SetupAuthConfig(filePath string) (err error) { if err != nil {return} // Unmarshal - authConfig = unmarshalJSON(fileContents); - if authConfig == nil {return errors.New("Invalid JSON config!");} + unmarshaled := UnmarshalJSON(fileContents); + if unmarshaled == nil {return errors.New("Invalid JSON object in config file!");} - print(match(authConfig, authConfig), "\n") + // Check type assertion for base JSON object + if _, ok := unmarshaled.(map[string]any); !ok { + return errors.New("JSON is incorrect format"); + } + + // Loop through each bearer key + for key, val := range unmarshaled.(map[string]any) { + // Check type assertion + if _, ok := val.([]any); !ok { + return errors.New("JSON is incorrect format for key " + key); + } + + // Copy over array + authConfig[key] = val.([]any); + } // Finish setup authConfigSetup = true; return nil; } -/* Gets a reference copy to the config data */ -func GetAuthConfigData() (any, bool) { +/* Gets a copy to the config data */ +func GetAuthConfigData() (map[string][]any, bool) { return authConfig, authConfigSetup; +} + +/* Returns true iff bearer is authorized for this request JSON */ +func Authenticate(bearer string, requestJSON []byte) bool { + // Check if bearer token exists at all + if _, ok := authConfig[bearer]; !ok {return false;} + + // Unmarshal JSON + unmarshaledRequest := UnmarshalJSON(requestJSON); + + // Check for any object + for _, jsonObject := range authConfig[bearer] { + if match(unmarshaledRequest, jsonObject) {return true} + } + + return false; } \ No newline at end of file diff --git a/auth/json.go b/auth/json.go index de2e32b..138a353 100644 --- a/auth/json.go +++ b/auth/json.go @@ -8,7 +8,7 @@ import ( ) /* Unmarshals a JSON into a recursive map. Returns nil on error */ -func unmarshalJSON(marshaledJSON []byte) (unmarshaled any) { +func UnmarshalJSON(marshaledJSON []byte) (unmarshaled any) { json.Unmarshal(marshaledJSON, &unmarshaled); return; } @@ -55,6 +55,6 @@ func match(request any, filter any) bool { return true; // Otherwise compare the objects directly using reflect - default: return reflect.DeepEqual(request, filter) + default: return reflect.DeepEqual(request, filter); } } \ No newline at end of file diff --git a/dummy.py b/dummy.py new file mode 100755 index 0000000..0426c45 --- /dev/null +++ b/dummy.py @@ -0,0 +1,14 @@ +#!/usr/bin/python + +# Dummy script used as a stand-in for singal-cli which replies to any JSON +# request with a JSON that has the same ID. + +import sys +import json + +for line in sys.stdin: + try: + data = json.loads(line.strip()) + if 'id' in data: print(json.dumps({'id': data['id']})) + except: + print("ERROR") \ No newline at end of file diff --git a/go.mod b/go.mod index 5ab0d05..8d3aaf4 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,12 @@ module signal-cli-http go 1.19 + +require ( + github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 // indirect + github.com/ThomasRooney/gexpect v0.0.0-20161231170123-5482f0350944 // indirect + github.com/creack/pty v1.1.24 // indirect + github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/kr/pty v1.1.8 // indirect + golang.org/x/sys v0.34.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..196fc6d --- /dev/null +++ b/go.sum @@ -0,0 +1,15 @@ +github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= +github.com/ThomasRooney/gexpect v0.0.0-20161231170123-5482f0350944/go.mod h1:sPML5WwI6oxLRLPuuqbtoOKhtmpVDCYtwsps+I+vjIY= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index 34cbb19..fd91d07 100644 --- a/main.go +++ b/main.go @@ -5,6 +5,7 @@ package main import ( "signal-cli-http/args" "signal-cli-http/auth" + "signal-cli-http/subprocess" "signal-cli-http/web" "log" @@ -16,23 +17,33 @@ func main() { // Read arguments args.Parse(); - configLocation, confLocationSet := args.GetAuthJson(); - if !confLocationSet {log.Default().Print("No auth config value!"); return;} - log.Default().Print("Reading auth config value from ", configLocation); + authLocation, ok := args.GetAuthJson(); + if !ok { + log.Default().Print("No auth config value!"); + return; + } + log.Default().Print("Reading auth config value from ", authLocation); - // Set up config - err := auth.SetupAuthConfig(configLocation); + // Set up authentication + err := auth.SetupAuthConfig(authLocation); if err != nil {log.Default().Print("Error reading config: ", err); return;} - log.Default().Print(auth.GetAuthConfigData()); + log.Default().Print("Read auth config data"); + // Setup Subprocess + binary, binarySet := args.GetBinaryLocation(); + if !binarySet {log.Default().Print("Read auth config data"); return;}; + err = subprocess.SetupCMD(binary); + if err != nil {log.Default().Print("Error running subprocess at ", binary, ": ", err); return;}; + log.Default().Print("Started subprocess at ", binary); + + // HTTP Listen port, portSet := args.GetHTTPPort(); if !portSet {log.Default().Print("No port value!"); return;} - log.Default().Print("Listening on port ", port); - go func() { defer wg.Done(); - web.StartWebserver(port); + log.Default().Print("Error with web server: ", web.StartWebserver(port)); }() + log.Default().Print("Listening on port ", port); log.Default().Print("Startup tasks complete!"); wg.Wait(); diff --git a/subprocess/readme.md b/subprocess/readme.md index 829dc96..8728230 100644 --- a/subprocess/readme.md +++ b/subprocess/readme.md @@ -2,4 +2,6 @@ This module spawns and handles IO for the signal-cli process. -Do not pass an object with the "id" key into this module's methods. It will reject the request for that reason. \ No newline at end of file +Do not pass an object with the "id" key into this module's methods. It will reject the request for that reason. + +This system works with multiple requests at the same time safely. \ No newline at end of file diff --git a/subprocess/request.go b/subprocess/request.go index bd2a50b..717c812 100644 --- a/subprocess/request.go +++ b/subprocess/request.go @@ -2,20 +2,82 @@ package subprocess /* This file manages verifying/sanatizing the request and response and forwarding it to the subprocess */ -/* Method which module http calls to create the subprocess */ -func Run(path string, body []byte) (status int, bodyContents []byte, err error) { - arguments := getArguments(path, body); +import ( + "crypto/rand" + "encoding/base64" + "encoding/json" + "errors" + "sync" + "time" +) + +/* Stores a map between job ID and a string pointer. Nil for job not done yet */ +var waitingJobs map[string]*string = make (map[string]*string); +var waitingJobsMutex sync.RWMutex; + +/* Method which module http calls to forward the request to the subprocess*/ +func Request(body map[string]any) (responseJSON string, err error) { + if _, ok := body["id"]; ok { + err = errors.New("Request cannot contain id!"); + return + } - // Don't know what to do with this request - if arguments == nil {return 404, []byte("Unknown request\n"), nil;} + // Generate ID and store it + var id string = genID(); + if len(id) == 0 { + err = errors.New("Error generating ID!"); + return + } + body["id"] = id; - // Call subprocess - status, bodyContents, err = runCommand(arguments); + // Marshal JSON to bytes + contents, err := json.Marshal(body) + if err != nil {return "", err} - return + // Lock job when enqueueing + waitingJobsMutex.Lock(); + writeCMD(string(contents)); + waitingJobs[id] = nil; + waitingJobsMutex.Unlock(); + + // Wait for request to finish + for waitingJobs[id] == nil { + time.Sleep(time.Millisecond) + } + + // Lock job when dequeueing + waitingJobsMutex.Lock(); + responseJSON = *waitingJobs[id]; + delete(waitingJobs, id) + waitingJobsMutex.Unlock(); + + err = nil; + return; } -/* Converts a request into the correct binary arguments */ -func getArguments(path string, body []byte) []string { - return nil // For now +/* Handles putting a response into waitingJobs. Returns false on error */ +func Response(body string) { + var unmarshaledJSON any; + json.Unmarshal([]byte(body), &unmarshaledJSON); + unmarshaledJSONMap, ok := unmarshaledJSON.(map[string]any) + if !ok {return} + + val, ok := unmarshaledJSONMap["id"]; + if !ok {return} + id, ok := val.(string) + if !ok {return} + + + // Write response into + waitingJobsMutex.Lock(); + waitingJobs[id] = &body; + waitingJobsMutex.Unlock(); + + +} + +func genID() string { + b := make([]byte, 32) + if _, err := rand.Read(b); err != nil {return ""} + return base64.RawURLEncoding.EncodeToString(b) } \ No newline at end of file diff --git a/subprocess/subprocess.go b/subprocess/subprocess.go index a1ea05f..16d0a65 100644 --- a/subprocess/subprocess.go +++ b/subprocess/subprocess.go @@ -1,44 +1,56 @@ package subprocess -import ( - "signal-cli-http/args" - - "errors" - "os/exec" - "strings" -) - /* This file manages calling signal-cli */ -/* Runs the command */ -func runCommand(arguments []string) (returnStatus int, bodyContent []byte, err error) { - // Get binary location - binary, ok := args.GetBinaryLocation(); - if !ok { - err = errors.New("Binary cannot be found!"); - return; - } +import ( + "bufio" + "errors" + "os" + "os/exec" + "sync" - // Create command using binary location and arguments - command := exec.Command(binary, strings.Join(arguments, " ")); - - // Duplicate pointer into command.Stdout - //command.Stdout = &bodyContent; + "github.com/creack/pty" +) - // Run the command - err = command.Run(); - if err != nil {return} +var cmd *exec.Cmd; +var cmdStarted = false; +var f *os.File; +var fLock sync.RWMutex; +var reader *bufio.Scanner; + +func SetupCMD(binaryLocation string) error { + // Avoid double set-up + if cmdStarted {return errors.New("cmd already started")}; - // Extract exit code if possible - if exitError, ok := err.(*exec.ExitError); ok { - returnStatus = exitError.ExitCode(); - } else { - err = errors.New("Cannot get exit code!"); - } + // Create cmd object + cmd = exec.Command(binaryLocation, "jsonRpc"); + if cmd == nil {return errors.New("Creating process failed!")} - // Get output - bodyContent, err = command.Output(); + // Start it + file, err := pty.Start(cmd); + f = file; + if err != nil {return err} - // Named return values allow this - return; + // Set up reader object and loop + reader = bufio.NewScanner(f); + go readCMD(); + + // No problem + return nil; +} + +func readCMD() { + var maxCapacity int = 4096; + buf := make([]byte, maxCapacity); + reader.Buffer(buf, maxCapacity); + + for reader.Scan() {Response(reader.Text())} +} + +func writeCMD(line string) (ok bool) { + fLock.Lock(); + if line[len(line)-1] != '\n' {line += "\n"} + f.WriteString(line); + fLock.Unlock(); + return true; } \ No newline at end of file diff --git a/web/listen.go b/web/listen.go index 04c7434..b439b03 100644 --- a/web/listen.go +++ b/web/listen.go @@ -3,7 +3,7 @@ package web /* This file handles listening to HTTP requests */ import ( - //"signal-cli-http/auth" + "signal-cli-http/auth" "signal-cli-http/subprocess" "fmt" @@ -12,11 +12,9 @@ import ( "net/http" ) -func StartWebserver(port int) { - http.HandleFunc("/", getRoot) - - err := http.ListenAndServe(":"+fmt.Sprint(port), nil) - fmt.Println(err) +func StartWebserver(port int) error { + http.HandleFunc("/", getRoot); + return http.ListenAndServe(":"+fmt.Sprint(port), nil); } func getRoot(w http.ResponseWriter, r *http.Request) { @@ -29,41 +27,44 @@ func getRoot(w http.ResponseWriter, r *http.Request) { } bearer := authArr[0]; - // Check that the request is allowed for the path - /*if !conf.GlobalConfig.ValidateBearerKey(bearer, r.URL.Path) { - w.WriteHeader(403); - w.Write([]byte("Bearer key not whitelisted for this path\n")) - return; - }*/ - // Read request body body, err := io.ReadAll(r.Body) if err != nil { w.WriteHeader(500); - w.Write([]byte("Error reading body\n")) + w.Write([]byte("Error reading body\n")); return; } - - // Call subprocess - status, bodyContent, err := subprocess.Run(r.URL.Path, body) - - // Error + // Check Authentication header + if !auth.Authenticate(bearer, body) { + w.WriteHeader(403); + w.Write([]byte("Bearer key not whitelisted for this request type\n")); + return; + } + // Attempt to unmarshal JSON + bodyUnmarshaled := auth.UnmarshalJSON(body); + if bodyUnmarshaled == nil { + w.WriteHeader(400); + w.Write([]byte("Body content is not a valid JSON")); + return; + } + // Type assertion + b, ok := bodyUnmarshaled.(map[string]any) + if !ok { + w.WriteHeader(400); + w.Write([]byte("Body content is not of the write format")); + return; + } + // Run request + bodyContent, err := subprocess.Request(b) if err != nil { w.WriteHeader(500); w.Write([]byte("Internal server error: " + err.Error() + "\n")); return } - // Respond to client with status - if status == 0 { - w.WriteHeader(200); - w.Write(bodyContent); - } else { - w.WriteHeader(400); - w.Write([]byte("Program exited with status " + fmt.Sprint(status))); - - } + w.WriteHeader(200); + w.Write([]byte(bodyContent)); // Log the request - log.Default().Print("HTTP Request: ", bearer, " " , r.URL.Path, " ", status) + log.Default().Print("HTTP Request: ", bearer, " " , 200, " ", string(body)) } \ No newline at end of file