diff --git a/auth/auth.go b/auth/auth.go index b26002c..407e647 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -4,8 +4,10 @@ package auth reading from a config file and matching requests to the whitelist. */ import ( + "encoding/json" "errors" "os" + "reflect" ) /* Stores a map between a string (bearer token) and a list of unmarshaled JSONS */ @@ -21,8 +23,10 @@ func SetupAuthConfig(filePath string) (err error) { if err != nil {return} // Unmarshal - unmarshaled := UnmarshalJSON(fileContents); - if unmarshaled == nil {return errors.New("Invalid JSON object in config file!");} + var unmarshaled any; + if err := json.Unmarshal(fileContents, &unmarshaled); err != nil { + return errors.New("Invalid JSON object in config file!"); + } // Check type assertion for base JSON object if _, ok := unmarshaled.(map[string]any); !ok { @@ -56,12 +60,74 @@ func Authenticate(bearer string, requestJSON []byte) bool { if _, ok := authConfig[bearer]; !ok {return false;} // Unmarshal JSON - unmarshaledRequest := UnmarshalJSON(requestJSON); + var unmarshaledRequest any; + if err := json.Unmarshal(requestJSON, &unmarshaledRequest); err != nil { + return false; + } // Check for any object for _, jsonObject := range authConfig[bearer] { - if match(unmarshaledRequest, jsonObject) {return true} + if Match(unmarshaledRequest, jsonObject) {return true} } return false; +} + +/* Meat and bones of determining if a request is allowed by a filter */ +func Match(request any, filter any) bool { + // Check that the types are the same + if reflect.TypeOf(request) != reflect.TypeOf(filter) {return false} + + // Can safely switch on type of one object at this point since they're equal + switch filter.(type) { + + // Key-value pairs + case map[string]any: + // Check for every key that's in the filter + for key := range filter.(map[string]any) { + // that it's in the request + if _, ok := request.(map[string]any)[key]; !ok {return false} + + // And recursively check that the value is equal + if !Match(request.(map[string]any)[key], filter.(map[string]any)[key]) { + return false; + } + } + return true; + + /* Arrays attempt to match every item in the filter to ANY item in the + request. Duplicates in the filter are treated as one */ + case []any: + // Check that for every item in the filter + for i := 0; i < len(filter.([]any)); i ++ { + foundMatch := false; + // That something matches in the request + for j := 0; j < len(request.([]any)); j ++ { + if Match(filter.([]any)[i], request.([]any)[j]) { + foundMatch = true; + break + } + } + // Cannot find a match for something in the filter + if !foundMatch {return false} + } + // And the other way around + for i := 0; i < len(filter.([]any)); i ++ { + foundMatch := false; + // That something matches in the request + for j := 0; j < len(request.([]any)); j ++ { + if Match(filter.([]any)[i], request.([]any)[j]) { + foundMatch = true; + break + } + } + // Cannot find a match for something in the filter + if !foundMatch {return false} + } + + return true; + + // Otherwise compare the objects directly using reflect + default: return reflect.DeepEqual(request, filter); + } } \ No newline at end of file diff --git a/auth/json.go b/auth/json.go deleted file mode 100644 index e42d431..0000000 --- a/auth/json.go +++ /dev/null @@ -1,73 +0,0 @@ -package auth - -/* This file contains some JSON helper functions */ - -import ( - "encoding/json" - "reflect" -) - -/* Unmarshals a JSON into a recursive map. Returns nil on error */ -func UnmarshalJSON(marshaledJSON []byte) (unmarshaled any) { - json.Unmarshal(marshaledJSON, &unmarshaled); - return; -} - -/* Meat and bones of determining if a request is allowed by a filter */ -func match(request any, filter any) bool { - // Check that the types are the same - if reflect.TypeOf(request) != reflect.TypeOf(filter) {return false} - - // Can safely switch on type of one object at this point since they're equal - switch filter.(type) { - - // Key-value pairs - case map[string]any: - // Check for every key that's in the filter - for key := range filter.(map[string]any) { - // that it's in the request - if _, ok := request.(map[string]any)[key]; !ok {return false} - - // And recursively check that the value is equal - if !match(request.(map[string]any)[key], filter.(map[string]any)[key]) { - return false; - } - } - return true; - - /* Arrays attempt to match every item in the filter to ANY item in the - request. Duplicates in the filter are treated as one */ - case []any: - // Check that for every item in the filter - for i := 0; i < len(filter.([]any)); i ++ { - foundMatch := false; - // That something matches in the request - for j := 0; j < len(request.([]any)); j ++ { - if match(filter.([]any)[i], request.([]any)[j]) { - foundMatch = true; - break - } - } - // Cannot find a match for something in the filter - if !foundMatch {return false} - } - // And the other way around - for i := 0; i < len(filter.([]any)); i ++ { - foundMatch := false; - // That something matches in the request - for j := 0; j < len(request.([]any)); j ++ { - if match(filter.([]any)[i], request.([]any)[j]) { - foundMatch = true; - break - } - } - // Cannot find a match for something in the filter - if !foundMatch {return false} - } - - return true; - - // Otherwise compare the objects directly using reflect - default: return reflect.DeepEqual(request, filter); - } -} \ No newline at end of file diff --git a/auth/readme.md b/auth/readme.md index c387cf5..16c96e9 100644 --- a/auth/readme.md +++ b/auth/readme.md @@ -1,6 +1,6 @@ # Auth - Signal-CLI HTTP -This module handles the reading and parsing of the auth JSON file. It also acts as a verifier in relation to that information. The file is a JSON object. It acts as a whitelist for which bearer token can do what action. It is passed to the HTTP endpoint via the `Authorization: Bearer ` header. +This module handles the reading and parsing of the auth JSON file. It also acts as a verifier in relation to that information. The file is a JSON object. It acts as a whitelist for which bearer token can do what action. It is passed to the HTTP endpoint via the `Authorization: ` header. Nore that this is not `Authorization: Bearer ` Here's a sample auth JSON: @@ -16,20 +16,26 @@ Here's a sample auth JSON: } ``` -When an HTTP request comes in, this software will do the following: +When an HTTP request comes in, this software will do the following (sending error responses when appropriate): 1. Check that there's an `Authorization` header 2. Get the authorization header's value (bearer token) 3. Read the JSON array corresponding to the bearer token. -4. See if any JSON object in that array (called a filter) does not have any data the request JSON doesn't, except for arrays which must match excactly. -5. If the statement in step 4 is true, forward the request into the signal-cli process and return the response. +4. See if any JSON object in that array "matches" the request JSON +5. Forward the request to the subprocess, and return the result. -So for example, the reqest `{"method":"send","params":{"recipient":["+16028675309"],"message":"message"},"id":"SomeID"},` would be allowed by the filter `{"method":"send","params":{"recipient":["+16028675309"]}}` because the filter does not have any data the request does not. But `{"method":"send","params":{"recipient":["+5555555555"],"message":"message"},"id":"SomeID"},` would not because the phone number differs. +The rules for matching a request JSON to a filter JSON is a recursive process. At each step it goes through the following checks: +1. The types (map JSON, array JSON, value literal, etc) must be the same +2. For the map JSON type, each key inside the filter json must be present inside the request JSON. Each key-value pair in the filter JSON must also match (recursively). +3. For the array JSON type, each key inside the filter json must be present inside the request JSON **and vice-versa**. Each key-value pair must also match (recursively). +4. For anything else, it matches the object directly. This is invoked when checking equality of a value literal. -Note: items in arrays must "match" exactly, but items in items in arrays follow normal rules. So the request `{"method":"send","params":{"recipient":["+16028675309","someBadNumber"]}}` would NOT match the filter `{"method":"send","params":{"recipient":["+16028675309",]}}` - -These filters can be as granular as you want. +Here's some examples for each case: +1. the request `{"method":"send","params":{"recipient":["+16028675309"],"message":"message"},"id":"SomeID"},` would not match the filter `["+5555555555"]` because one is a JSON map and the other a JSON array. +2. the request `{"method":"something","params":{"recipient":["+16028675309"],"message":"message"},"id":"SomeID"},` would not match the filter `{"method":"send","params":{"recipient":["+16028675309"],"message":"message"}}` because the "method" differs. This would also fail to match if the `method` key was missing in the request JSON. +3. `{"method":"send","params":{"recipient":["+16028675309","someBadNumber"]}}` would not match the filter `{"method":"send","params":{"recipient":["+16028675309",]}}` because of the `someBadNumber` number in the request. This rule exists so that a malicious request cant send a message to both a room/concact that it's whitelisted for, and one that it isn't. +4. `"+16028675309"` would not match the filter `"+15555555555"` because their values differ. Here's what each filter JSON object in the above sample JSON does: `{"method":"send","params":{"recipient":["+16028675309"]}}` allows sending to `+16028675309` (any message, timestamp, etc.) diff --git a/dummy.py b/dummy.py index 0426c45..76d6a2e 100755 --- a/dummy.py +++ b/dummy.py @@ -6,6 +6,11 @@ import sys import json +# Stress-testing incoming messages +for i in range(2000): + print(json.dumps({"method":"receive","params":{"envelope":{"source":"67a13c3e-8d29-2539-ce8e-41129c349d6d"},"data":i}})) + +# Reply to incoming messages for line in sys.stdin: try: data = json.loads(line.strip()) diff --git a/main.go b/main.go index fd91d07..426d7d8 100644 --- a/main.go +++ b/main.go @@ -7,7 +7,8 @@ import ( "signal-cli-http/auth" "signal-cli-http/subprocess" "signal-cli-http/web" - + "time" + "log" "sync" ) @@ -35,6 +36,7 @@ func main() { err = subprocess.SetupCMD(binary); if err != nil {log.Default().Print("Error running subprocess at ", binary, ": ", err); return;}; log.Default().Print("Started subprocess at ", binary); + subprocess.StartCacheClear(); // HTTP Listen port, portSet := args.GetHTTPPort(); @@ -46,5 +48,9 @@ func main() { log.Default().Print("Listening on port ", port); log.Default().Print("Startup tasks complete!"); + + time.Sleep(time.Millisecond * 500); + + // Needed to not kill program wg.Wait(); } \ No newline at end of file diff --git a/readme.md b/readme.md index cb0c61b..432dd95 100644 --- a/readme.md +++ b/readme.md @@ -8,7 +8,7 @@ Please see the JSONRPC documentation for `signal-cli`: [https://github.com/AsamK Please also read the following README files for the individual modules to understand how to configure and interact with this program: -* [args](args/readme.md) handles command line arguments. -* [auth](auth/readme.md) handles the authentication JSON and checking requests. -* [subprocess](subprocess/readme.md) manages the underlying `signal-cli` JSONRPC process, along with caching incoming messages. -* [web](web/readme.md) - handles the HTTP requests to this program, including the necessary edge cases. \ No newline at end of file +* [args](args/readme.md) handles command line arguments. Go here to learn how to run the program. +* [auth](auth/readme.md) handles the authentication JSON and checking requests. Go here to learn how to secure the program, and whitelist authentication keys. +* [subprocess](subprocess/readme.md) manages the underlying `signal-cli` JSONRPC process, along with caching incoming messages. Go here to understand how this program relays and returns the JSON objects. +* [web](web/readme.md) - handles the HTTP requests to this program, including the necessary edge cases. Go here to understand how to send and understand the responses to the program's HTTP endpoint. \ No newline at end of file diff --git a/subprocess/incoming.go b/subprocess/incoming.go new file mode 100644 index 0000000..bedc05a --- /dev/null +++ b/subprocess/incoming.go @@ -0,0 +1,103 @@ +package subprocess + +/* This file manages incoming messages, marked with "method":"receive" in the JSON */ + +import ( + "signal-cli-http/auth" + "sort" + "sync" + "time" +) + +/* Stores an incoming message */ +type IncomingMessage struct { + /* Stores the time the message came in */ + receivedTime time.Time; + /* Stores the message body */ + body string; + /* Stores the unmarshaledJSON */ + unmarshaledJSONMap map[string]any; +} + +func newIncomingMessage() *IncomingMessage {return &IncomingMessage{}} + +/* Stores unlimited incoming messages up to 5 minutes old. + This is intentionally an array of pointers so cache-clearing and appending + don't require a copy of unmarshaledJSONMap. */ +var incomingMessageCache []*IncomingMessage; +/* Locks incomingMessageCache */ +var incomingMessageCacheLock sync.RWMutex; + +/* Here exclusively for testing purposes */ +func GetIMC() []*IncomingMessage {return incomingMessageCache} + +/* Handler for incoming JSON objects which have "method":"receive" */ +func handleIncoming(body string, unmarshaledJSONMap map[string]any) (ok bool) { + // Check that the message's method is "receive" + if val, ok := unmarshaledJSONMap["method"]; !ok || val != "receive" {return false} + + // Create new message structure + var newMessage *IncomingMessage = newIncomingMessage(); + // Using time.Now() to ensure that pre/post-dated messages don't have issue + newMessage.receivedTime = time.Now(); + newMessage.body = body; + newMessage.unmarshaledJSONMap = unmarshaledJSONMap; + + // Add message into cache + incomingMessageCacheLock.Lock(); + incomingMessageCache = append(incomingMessageCache, newMessage); + incomingMessageCacheLock.Unlock(); + return true; +} + +/* Handles clearing space in incomingMessageCache */ +func StartCacheClear() {go cacheClear()} +/* Runs in an infinite loop to try and clear the cache when needed */ +func cacheClear() { + for { + // More than reasonable delay + time.Sleep(time.Millisecond * 25); + // Only attempt to clear when it's 1000 items or more + if len(incomingMessageCache) < 1000 {continue} + + incomingMessageCacheLock.Lock(); + + fiveMinutesAgo := time.Now().Add(time.Minute*(-5)); + + // Find first index in incomingMessageCache that is at most 15 minutes old + i := sort.Search(len(incomingMessageCache), func(i int) bool { + return incomingMessageCache[i].receivedTime.After(fiveMinutesAgo) + }) + incomingMessageCache = incomingMessageCache[i:] + + incomingMessageCacheLock.Unlock(); + } +} + +/* Returns a list of encoded JSON strings from incomingMessageCache that match + the filter from + @return always valid JSON array object. Can be empty. */ +func GetIncoming(filter map[string]any) string { + // Create copy of incomingMessageCache as the following loop can be slow + incomingMessageCacheLock.RLock(); + incomingMessageCacheCopy := incomingMessageCache; + incomingMessageCacheLock.RUnlock(); + + // Create list of messages that match the filter + var list []string; + for _, message := range incomingMessageCacheCopy { + if !auth.Match(message.unmarshaledJSONMap, filter) {continue} + list = append(list, message.body) + } + + // Constructs the JSON string without considering the JSON object + var encoded string = "[" + for index, object := range list { + encoded += object + if index == len(list) - 1 {continue} + encoded += "," + } + encoded += "]\n" + + return encoded; +} \ No newline at end of file diff --git a/subprocess/readme.md b/subprocess/readme.md index 8728230..20d8ad6 100644 --- a/subprocess/readme.md +++ b/subprocess/readme.md @@ -4,4 +4,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. -This system works with multiple requests at the same time safely. \ No newline at end of file +This system works with multiple requests at the same time safely. + +This system also caches incoming messages up to at least 5 minutes old for later querying. This process takes in a filter JSON and goes through this list and finds any incoming message JSON objects that match to the filter JSON as outlined in the [auth module](../auth/readme.md). \ No newline at end of file diff --git a/subprocess/request.go b/subprocess/request.go index 75b28ab..4fd935a 100644 --- a/subprocess/request.go +++ b/subprocess/request.go @@ -12,7 +12,8 @@ import ( ) /* 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 waitingJobs map[string]*string = make(map[string]*string); +/* Mutex for waitingJobs list */ var waitingJobsMutex sync.RWMutex; /* Method which module http calls to forward the request to the subprocess*/ @@ -23,6 +24,7 @@ func Request(body map[string]any) (responseJSON string, err error) { } // Generate ID and store it + // 32 chars of base 64 is secure enough to assume no accidental collision var id string = genID(); if len(id) == 0 { err = errors.New("Error generating ID!"); @@ -42,42 +44,65 @@ func Request(body map[string]any) (responseJSON string, err error) { waitingJobs[id] = nil; waitingJobsMutex.Unlock(); - // Wait for request to finish - for waitingJobs[id] == nil { + // Timeout waiting for response after 30 seconds + timeoutTime := time.Now().Add(time.Second * 30); + + // Wait for request to return + for { + // Check every milisecond time.Sleep(time.Millisecond) + + // check if job is fufiled + waitingJobsMutex.RLock(); + if waitingJobs[id] != nil { + waitingJobsMutex.RUnlock(); + break; + } + waitingJobsMutex.RUnlock(); + + // If request takes more than 30 seconds + /* This is technically a race condition, as the job could be fufiled + between here and the job fufiled check, but it's the difference of + a milisecond or two out of 30 seconds */ + if time.Now().After(timeoutTime) { + // Delete job from queue + waitingJobsMutex.Lock(); + delete(waitingJobs, id) + waitingJobsMutex.Unlock(); + return "", errors.New("Request timed out!"); + } } // Lock job when dequeueing waitingJobsMutex.Lock(); responseJSON = *waitingJobs[id]; - delete(waitingJobs, id) + delete(waitingJobs, id) // Remove job from "queue" waitingJobsMutex.Unlock(); - err = nil; + err = nil; // No problems return; } /* 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} - +func handleResponse(body string, unmarshaledJSONMap map[string]any) { + waitingJobsMutex.RLock(); // Read lock val, ok := unmarshaledJSONMap["id"]; + waitingJobsMutex.RUnlock(); if !ok {return} id, ok := val.(string) if !ok {return} - - // Write response into + // Read-Write lock the mutex when writing job result waitingJobsMutex.Lock(); + defer waitingJobsMutex.Unlock(); + + // Skip storage if there isn't a request for this ID + if _, ok := waitingJobs[id]; !ok {return} + // Store response in waiting Jobs waitingJobs[id] = &body; - waitingJobsMutex.Unlock(); - - } +/* Helper function to generate a random ID */ func genID() string { b := make([]byte, 32) if _, err := rand.Read(b); err != nil {return ""} diff --git a/subprocess/subprocess.go b/subprocess/subprocess.go index 16d0a65..d8167ae 100644 --- a/subprocess/subprocess.go +++ b/subprocess/subprocess.go @@ -4,11 +4,14 @@ package subprocess import ( "bufio" + "encoding/json" "errors" "os" "os/exec" + "os/signal" "sync" - + "syscall" + "github.com/creack/pty" ) @@ -35,18 +38,54 @@ func SetupCMD(binaryLocation string) error { reader = bufio.NewScanner(f); go readCMD(); + // Make sure cmd is killed when this program is + go catchKillSignal(); + // No problem return nil; } +/* Catch signals from this program to kill child */ +func catchKillSignal() { + signalCatcher := make(chan os.Signal, 1) + signal.Notify(signalCatcher, syscall.SIGINT, syscall.SIGTERM, syscall.SIGILL) + <- signalCatcher; + cmd.Process.Kill(); + os.Exit(0); // Without this the process never exits +} + +/* Continuously reads the next line up to 40960 bytes and forwards it to response */ func readCMD() { - var maxCapacity int = 4096; + var maxCapacity int = 40960; buf := make([]byte, maxCapacity); reader.Buffer(buf, maxCapacity); - for reader.Scan() {Response(reader.Text())} + for reader.Scan() { + // Read the line + line := reader.Text(); + + // Unmarshal the JSON + var unmarshaledJSON any; + if err := json.Unmarshal([]byte(line), &unmarshaledJSON); err != nil {continue} + + // Make sure it's a JSON map + unmarshaledJSONMap, ok := unmarshaledJSON.(map[string]any) + if !ok {continue} + + // Get method + method, ok := unmarshaledJSONMap["method"]; + if !ok {continue} + + // Redirect to handlers based off method + if method == "receive" { + handleIncoming(line, unmarshaledJSONMap); + } else { + handleResponse(line, unmarshaledJSONMap); + } + } } +/* Write a line into the subprocess */ func writeCMD(line string) (ok bool) { fLock.Lock(); if line[len(line)-1] != '\n' {line += "\n"} diff --git a/web/listen.go b/web/listen.go index b439b03..fef3f8d 100644 --- a/web/listen.go +++ b/web/listen.go @@ -3,26 +3,39 @@ package web /* This file handles listening to HTTP requests */ import ( + "encoding/json" "signal-cli-http/auth" "signal-cli-http/subprocess" - + "fmt" "io" "log" "net/http" + "time" ) +/* Starts the listener */ func StartWebserver(port int) error { http.HandleFunc("/", getRoot); return http.ListenAndServe(":"+fmt.Sprint(port), nil); } +/* Writes data to the log */ +func writeLog(method string, status int, start time.Time) { + duration := time.Now().Sub(start); + log.Default().Printf("%s %d %s", method, status, duration.String()) +} + +/* Handler for all requests */ func getRoot(w http.ResponseWriter, r *http.Request) { + startTime := time.Now() + // Check that Authentication header exists authArr, ok := r.Header["Authentication"] if (!ok) || (len(authArr) == 0) { w.WriteHeader(400); - w.Write([]byte("Authentication header missing\n")) + w.Write([]byte("Authentication header missing\n")); + writeLog(r.Method, 400, startTime); return; } bearer := authArr[0]; @@ -32,39 +45,58 @@ func getRoot(w http.ResponseWriter, r *http.Request) { if err != nil { w.WriteHeader(500); w.Write([]byte("Error reading body\n")); + writeLog(r.Method, 500, startTime); return; } + // Check Authentication header if !auth.Authenticate(bearer, body) { w.WriteHeader(403); w.Write([]byte("Bearer key not whitelisted for this request type\n")); + writeLog(r.Method, 403, startTime); return; } + // Attempt to unmarshal JSON - bodyUnmarshaled := auth.UnmarshalJSON(body); - if bodyUnmarshaled == nil { + var bodyUnmarshaled any; + if err := json.Unmarshal(body, &bodyUnmarshaled); err != nil { w.WriteHeader(400); w.Write([]byte("Body content is not a valid JSON")); + writeLog(r.Method, 400, startTime); return; } + // Type assertion - b, ok := bodyUnmarshaled.(map[string]any) + b, ok := bodyUnmarshaled.(map[string]any); if !ok { w.WriteHeader(400); w.Write([]byte("Body content is not of the write format")); + writeLog(r.Method, 400, startTime); return; } + + // Handle incoming + method, ok := b["method"]; + + if method == "receive" { + incoming := subprocess.GetIncoming(b) + w.WriteHeader(200); + w.Write([]byte(incoming)); + writeLog(r.Method, 200, startTime); + return; + } + // Run request - bodyContent, err := subprocess.Request(b) + bodyContent, err := subprocess.Request(b); if err != nil { w.WriteHeader(500); w.Write([]byte("Internal server error: " + err.Error() + "\n")); - return + writeLog(r.Method, 500, startTime); + return; } + // Request returned something w.WriteHeader(200); w.Write([]byte(bodyContent)); - - // Log the request - log.Default().Print("HTTP Request: ", bearer, " " , 200, " ", string(body)) + log.Default().Print("HTTP Request: ", bearer, " " , 200, " ", string(body)); } \ No newline at end of file diff --git a/web/readme.md b/web/readme.md index 590a962..63c1e87 100644 --- a/web/readme.md +++ b/web/readme.md @@ -1,17 +1,19 @@ # Web - Signal-CLI HTTP -This module handles the HTTP requests. The path used for requests genuinely does not matter. However, every request must have a JSON object body, and an `Authorization: bearer ` header. The response will also always be a JSON. +This module handles the HTTP requests. The path used for requests genuinely does not matter. However, every request must have a JSON object body, and an `Authorization: bearer ` header. Possible response codes: -* 400 Bad Request: Bad JSON, missing Authorization header, etc. +* 400 Bad Request: Bad JSON, missing Authorization header, **sent a request with a defined id**, etc. * 401 Unauthorized: bearer token not allowed for presented request. * 500 Internal Server Error: Self explanitory. * 501 Not Implemented: Will be returned for message receiving. * 200 OK: Request was forwarded to JSONRPC subprocess and that process returned something, including if the "error" key is present in the returned JSON. -Any non-200 response code will come with a JSON with the following format: +There is no body content with non-200 response codes. With 200 the response is a valid JSON map or array. -```json -{"error":"Error message string"} -``` \ No newline at end of file +This program simply relays requests to the signal-cli program. **It will not prevent you from breaking anything, outside of not whitelisting certain requests. This program does not understand what requests mean.** Each request comes formatted as a JSON object outlined in the [JSON-RPC documentation](https://github.com/AsamK/signal-cli/blob/master/man/signal-cli-jsonrpc.5.adoc](https://github.com/AsamK/signal-cli/blob/master/man/signal-cli-jsonrpc.5.adoc). + +The program will ensure that the request object is a JSON map, and that the `request` key is present. For any request type that is not `receive`, the program will generate an ID for your request (do not put on in the request, it will return an error) and return the program's response. + +For `receive` it will return a JSON list of JSON maps in the incoming message cache that match to your request. There is no limit to how many messages it returns so be careful. \ No newline at end of file