Compare commits
No commits in common. "5b213eb72f1549fe2b2aed6f677ac93385149582" and "d302f39719ecdc76c4a2f5fcdba30475535453b3" have entirely different histories.
5b213eb72f
...
d302f39719
12 changed files with 126 additions and 339 deletions
74
auth/auth.go
74
auth/auth.go
|
@ -4,10 +4,8 @@ package auth
|
||||||
reading from a config file and matching requests to the whitelist. */
|
reading from a config file and matching requests to the whitelist. */
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
/* Stores a map between a string (bearer token) and a list of unmarshaled JSONS */
|
/* Stores a map between a string (bearer token) and a list of unmarshaled JSONS */
|
||||||
|
@ -23,10 +21,8 @@ func SetupAuthConfig(filePath string) (err error) {
|
||||||
if err != nil {return}
|
if err != nil {return}
|
||||||
|
|
||||||
// Unmarshal
|
// Unmarshal
|
||||||
var unmarshaled any;
|
unmarshaled := UnmarshalJSON(fileContents);
|
||||||
if err := json.Unmarshal(fileContents, &unmarshaled); err != nil {
|
if unmarshaled == nil {return errors.New("Invalid JSON object in config file!");}
|
||||||
return errors.New("Invalid JSON object in config file!");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check type assertion for base JSON object
|
// Check type assertion for base JSON object
|
||||||
if _, ok := unmarshaled.(map[string]any); !ok {
|
if _, ok := unmarshaled.(map[string]any); !ok {
|
||||||
|
@ -60,74 +56,12 @@ func Authenticate(bearer string, requestJSON []byte) bool {
|
||||||
if _, ok := authConfig[bearer]; !ok {return false;}
|
if _, ok := authConfig[bearer]; !ok {return false;}
|
||||||
|
|
||||||
// Unmarshal JSON
|
// Unmarshal JSON
|
||||||
var unmarshaledRequest any;
|
unmarshaledRequest := UnmarshalJSON(requestJSON);
|
||||||
if err := json.Unmarshal(requestJSON, &unmarshaledRequest); err != nil {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for any object
|
// Check for any object
|
||||||
for _, jsonObject := range authConfig[bearer] {
|
for _, jsonObject := range authConfig[bearer] {
|
||||||
if Match(unmarshaledRequest, jsonObject) {return true}
|
if match(unmarshaledRequest, jsonObject) {return true}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
73
auth/json.go
Normal file
73
auth/json.go
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
# Auth - Signal-CLI HTTP
|
# 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: <bearerToken>` header. Nore that this is not `Authorization: Bearer <token>`
|
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 <bearerToken>` header.
|
||||||
|
|
||||||
Here's a sample auth JSON:
|
Here's a sample auth JSON:
|
||||||
|
|
||||||
|
@ -16,26 +16,20 @@ Here's a sample auth JSON:
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
When an HTTP request comes in, this software will do the following (sending error responses when appropriate):
|
When an HTTP request comes in, this software will do the following:
|
||||||
|
|
||||||
1. Check that there's an `Authorization` header
|
1. Check that there's an `Authorization` header
|
||||||
2. Get the authorization header's value (bearer token)
|
2. Get the authorization header's value (bearer token)
|
||||||
3. Read the JSON array corresponding to the bearer token.
|
3. Read the JSON array corresponding to the bearer token.
|
||||||
4. See if any JSON object in that array "matches" the request JSON
|
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. Forward the request to the subprocess, and return the result.
|
5. If the statement in step 4 is true, forward the request into the signal-cli process and return the response.
|
||||||
|
|
||||||
The rules for matching a request JSON to a filter JSON is a recursive process. At each step it goes through the following checks:
|
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.
|
||||||
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.
|
|
||||||
|
|
||||||
Here's some examples for each case:
|
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.
|
||||||
|
|
||||||
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:
|
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.)
|
`{"method":"send","params":{"recipient":["+16028675309"]}}` allows sending to `+16028675309` (any message, timestamp, etc.)
|
||||||
|
|
5
dummy.py
5
dummy.py
|
@ -6,11 +6,6 @@
|
||||||
import sys
|
import sys
|
||||||
import json
|
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:
|
for line in sys.stdin:
|
||||||
try:
|
try:
|
||||||
data = json.loads(line.strip())
|
data = json.loads(line.strip())
|
||||||
|
|
8
main.go
8
main.go
|
@ -7,8 +7,7 @@ import (
|
||||||
"signal-cli-http/auth"
|
"signal-cli-http/auth"
|
||||||
"signal-cli-http/subprocess"
|
"signal-cli-http/subprocess"
|
||||||
"signal-cli-http/web"
|
"signal-cli-http/web"
|
||||||
"time"
|
|
||||||
|
|
||||||
"log"
|
"log"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
@ -36,7 +35,6 @@ func main() {
|
||||||
err = subprocess.SetupCMD(binary);
|
err = subprocess.SetupCMD(binary);
|
||||||
if err != nil {log.Default().Print("Error running subprocess at ", binary, ": ", err); return;};
|
if err != nil {log.Default().Print("Error running subprocess at ", binary, ": ", err); return;};
|
||||||
log.Default().Print("Started subprocess at ", binary);
|
log.Default().Print("Started subprocess at ", binary);
|
||||||
subprocess.StartCacheClear();
|
|
||||||
|
|
||||||
// HTTP Listen
|
// HTTP Listen
|
||||||
port, portSet := args.GetHTTPPort();
|
port, portSet := args.GetHTTPPort();
|
||||||
|
@ -48,9 +46,5 @@ func main() {
|
||||||
log.Default().Print("Listening on port ", port);
|
log.Default().Print("Listening on port ", port);
|
||||||
|
|
||||||
log.Default().Print("Startup tasks complete!");
|
log.Default().Print("Startup tasks complete!");
|
||||||
|
|
||||||
time.Sleep(time.Millisecond * 500);
|
|
||||||
|
|
||||||
// Needed to not kill program
|
|
||||||
wg.Wait();
|
wg.Wait();
|
||||||
}
|
}
|
|
@ -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:
|
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. Go here to learn how to run the program.
|
* [args](args/readme.md) handles command line arguments.
|
||||||
* [auth](auth/readme.md) handles the authentication JSON and checking requests. Go here to learn how to secure the program, and whitelist authentication keys.
|
* [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. Go here to understand how this program relays and returns the JSON objects.
|
* [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. Go here to understand how to send and understand the responses to the program's HTTP endpoint.
|
* [web](web/readme.md) - handles the HTTP requests to this program, including the necessary edge cases.
|
|
@ -1,103 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
|
@ -4,6 +4,4 @@ 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.
|
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.
|
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).
|
|
|
@ -12,8 +12,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
/* Stores a map between job ID and a string pointer. Nil for job not done yet */
|
/* 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;
|
var waitingJobsMutex sync.RWMutex;
|
||||||
|
|
||||||
/* Method which module http calls to forward the request to the subprocess*/
|
/* Method which module http calls to forward the request to the subprocess*/
|
||||||
|
@ -24,7 +23,6 @@ func Request(body map[string]any) (responseJSON string, err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate ID and store it
|
// Generate ID and store it
|
||||||
// 32 chars of base 64 is secure enough to assume no accidental collision
|
|
||||||
var id string = genID();
|
var id string = genID();
|
||||||
if len(id) == 0 {
|
if len(id) == 0 {
|
||||||
err = errors.New("Error generating ID!");
|
err = errors.New("Error generating ID!");
|
||||||
|
@ -44,65 +42,42 @@ func Request(body map[string]any) (responseJSON string, err error) {
|
||||||
waitingJobs[id] = nil;
|
waitingJobs[id] = nil;
|
||||||
waitingJobsMutex.Unlock();
|
waitingJobsMutex.Unlock();
|
||||||
|
|
||||||
// Timeout waiting for response after 30 seconds
|
// Wait for request to finish
|
||||||
timeoutTime := time.Now().Add(time.Second * 30);
|
for waitingJobs[id] == nil {
|
||||||
|
|
||||||
// Wait for request to return
|
|
||||||
for {
|
|
||||||
// Check every milisecond
|
|
||||||
time.Sleep(time.Millisecond)
|
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
|
// Lock job when dequeueing
|
||||||
waitingJobsMutex.Lock();
|
waitingJobsMutex.Lock();
|
||||||
responseJSON = *waitingJobs[id];
|
responseJSON = *waitingJobs[id];
|
||||||
delete(waitingJobs, id) // Remove job from "queue"
|
delete(waitingJobs, id)
|
||||||
waitingJobsMutex.Unlock();
|
waitingJobsMutex.Unlock();
|
||||||
|
|
||||||
err = nil; // No problems
|
err = nil;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Handles putting a response into waitingJobs. Returns false on error */
|
/* Handles putting a response into waitingJobs. Returns false on error */
|
||||||
func handleResponse(body string, unmarshaledJSONMap map[string]any) {
|
func Response(body string) {
|
||||||
waitingJobsMutex.RLock(); // Read lock
|
var unmarshaledJSON any;
|
||||||
|
json.Unmarshal([]byte(body), &unmarshaledJSON);
|
||||||
|
unmarshaledJSONMap, ok := unmarshaledJSON.(map[string]any)
|
||||||
|
if !ok {return}
|
||||||
|
|
||||||
val, ok := unmarshaledJSONMap["id"];
|
val, ok := unmarshaledJSONMap["id"];
|
||||||
waitingJobsMutex.RUnlock();
|
|
||||||
if !ok {return}
|
if !ok {return}
|
||||||
id, ok := val.(string)
|
id, ok := val.(string)
|
||||||
if !ok {return}
|
if !ok {return}
|
||||||
|
|
||||||
// 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
|
// Write response into
|
||||||
if _, ok := waitingJobs[id]; !ok {return}
|
waitingJobsMutex.Lock();
|
||||||
// Store response in waiting Jobs
|
|
||||||
waitingJobs[id] = &body;
|
waitingJobs[id] = &body;
|
||||||
|
waitingJobsMutex.Unlock();
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Helper function to generate a random ID */
|
|
||||||
func genID() string {
|
func genID() string {
|
||||||
b := make([]byte, 32)
|
b := make([]byte, 32)
|
||||||
if _, err := rand.Read(b); err != nil {return ""}
|
if _, err := rand.Read(b); err != nil {return ""}
|
||||||
|
|
|
@ -4,14 +4,11 @@ package subprocess
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"os/signal"
|
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"github.com/creack/pty"
|
"github.com/creack/pty"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -38,54 +35,18 @@ func SetupCMD(binaryLocation string) error {
|
||||||
reader = bufio.NewScanner(f);
|
reader = bufio.NewScanner(f);
|
||||||
go readCMD();
|
go readCMD();
|
||||||
|
|
||||||
// Make sure cmd is killed when this program is
|
|
||||||
go catchKillSignal();
|
|
||||||
|
|
||||||
// No problem
|
// No problem
|
||||||
return nil;
|
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() {
|
func readCMD() {
|
||||||
var maxCapacity int = 40960;
|
var maxCapacity int = 4096;
|
||||||
buf := make([]byte, maxCapacity);
|
buf := make([]byte, maxCapacity);
|
||||||
reader.Buffer(buf, maxCapacity);
|
reader.Buffer(buf, maxCapacity);
|
||||||
|
|
||||||
for reader.Scan() {
|
for reader.Scan() {Response(reader.Text())}
|
||||||
// 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) {
|
func writeCMD(line string) (ok bool) {
|
||||||
fLock.Lock();
|
fLock.Lock();
|
||||||
if line[len(line)-1] != '\n' {line += "\n"}
|
if line[len(line)-1] != '\n' {line += "\n"}
|
||||||
|
|
|
@ -3,39 +3,26 @@ package web
|
||||||
/* This file handles listening to HTTP requests */
|
/* This file handles listening to HTTP requests */
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"signal-cli-http/auth"
|
"signal-cli-http/auth"
|
||||||
"signal-cli-http/subprocess"
|
"signal-cli-http/subprocess"
|
||||||
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
/* Starts the listener */
|
|
||||||
func StartWebserver(port int) error {
|
func StartWebserver(port int) error {
|
||||||
http.HandleFunc("/", getRoot);
|
http.HandleFunc("/", getRoot);
|
||||||
return http.ListenAndServe(":"+fmt.Sprint(port), nil);
|
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) {
|
func getRoot(w http.ResponseWriter, r *http.Request) {
|
||||||
startTime := time.Now()
|
|
||||||
|
|
||||||
// Check that Authentication header exists
|
// Check that Authentication header exists
|
||||||
authArr, ok := r.Header["Authentication"]
|
authArr, ok := r.Header["Authentication"]
|
||||||
if (!ok) || (len(authArr) == 0) {
|
if (!ok) || (len(authArr) == 0) {
|
||||||
w.WriteHeader(400);
|
w.WriteHeader(400);
|
||||||
w.Write([]byte("Authentication header missing\n"));
|
w.Write([]byte("Authentication header missing\n"))
|
||||||
writeLog(r.Method, 400, startTime);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
bearer := authArr[0];
|
bearer := authArr[0];
|
||||||
|
@ -45,58 +32,39 @@ func getRoot(w http.ResponseWriter, r *http.Request) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WriteHeader(500);
|
w.WriteHeader(500);
|
||||||
w.Write([]byte("Error reading body\n"));
|
w.Write([]byte("Error reading body\n"));
|
||||||
writeLog(r.Method, 500, startTime);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check Authentication header
|
// Check Authentication header
|
||||||
if !auth.Authenticate(bearer, body) {
|
if !auth.Authenticate(bearer, body) {
|
||||||
w.WriteHeader(403);
|
w.WriteHeader(403);
|
||||||
w.Write([]byte("Bearer key not whitelisted for this request type\n"));
|
w.Write([]byte("Bearer key not whitelisted for this request type\n"));
|
||||||
writeLog(r.Method, 403, startTime);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attempt to unmarshal JSON
|
// Attempt to unmarshal JSON
|
||||||
var bodyUnmarshaled any;
|
bodyUnmarshaled := auth.UnmarshalJSON(body);
|
||||||
if err := json.Unmarshal(body, &bodyUnmarshaled); err != nil {
|
if bodyUnmarshaled == nil {
|
||||||
w.WriteHeader(400);
|
w.WriteHeader(400);
|
||||||
w.Write([]byte("Body content is not a valid JSON"));
|
w.Write([]byte("Body content is not a valid JSON"));
|
||||||
writeLog(r.Method, 400, startTime);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Type assertion
|
// Type assertion
|
||||||
b, ok := bodyUnmarshaled.(map[string]any);
|
b, ok := bodyUnmarshaled.(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
w.WriteHeader(400);
|
w.WriteHeader(400);
|
||||||
w.Write([]byte("Body content is not of the write format"));
|
w.Write([]byte("Body content is not of the write format"));
|
||||||
writeLog(r.Method, 400, startTime);
|
|
||||||
return;
|
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
|
// Run request
|
||||||
bodyContent, err := subprocess.Request(b);
|
bodyContent, err := subprocess.Request(b)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WriteHeader(500);
|
w.WriteHeader(500);
|
||||||
w.Write([]byte("Internal server error: " + err.Error() + "\n"));
|
w.Write([]byte("Internal server error: " + err.Error() + "\n"));
|
||||||
writeLog(r.Method, 500, startTime);
|
return
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Request returned something
|
|
||||||
w.WriteHeader(200);
|
w.WriteHeader(200);
|
||||||
w.Write([]byte(bodyContent));
|
w.Write([]byte(bodyContent));
|
||||||
log.Default().Print("HTTP Request: ", bearer, " " , 200, " ", string(body));
|
|
||||||
|
// Log the request
|
||||||
|
log.Default().Print("HTTP Request: ", bearer, " " , 200, " ", string(body))
|
||||||
}
|
}
|
|
@ -1,19 +1,17 @@
|
||||||
# Web - Signal-CLI HTTP
|
# 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 <token>` header.
|
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 <token>` header. The response will also always be a JSON.
|
||||||
|
|
||||||
Possible response codes:
|
Possible response codes:
|
||||||
|
|
||||||
* 400 Bad Request: Bad JSON, missing Authorization header, **sent a request with a defined id**, etc.
|
* 400 Bad Request: Bad JSON, missing Authorization header, etc.
|
||||||
* 401 Unauthorized: bearer token not allowed for presented request.
|
* 401 Unauthorized: bearer token not allowed for presented request.
|
||||||
* 500 Internal Server Error: Self explanitory.
|
* 500 Internal Server Error: Self explanitory.
|
||||||
* 501 Not Implemented: Will be returned for message receiving.
|
* 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.
|
* 200 OK: Request was forwarded to JSONRPC subprocess and that process returned something, including if the "error" key is present in the returned JSON.
|
||||||
|
|
||||||
There is no body content with non-200 response codes. With 200 the response is a valid JSON map or array.
|
Any non-200 response code will come with a JSON with the following format:
|
||||||
|
|
||||||
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).
|
```json
|
||||||
|
{"error":"Error message string"}
|
||||||
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.
|
|
Loading…
Add table
Add a link
Reference in a new issue