Incoming should be working, but I haven't tested it too much.

This commit is contained in:
Ben 2025-08-01 00:44:37 -07:00
parent d302f39719
commit 309b836931
Signed by: webmaster
GPG key ID: A5FCBAF34E6E8B50
8 changed files with 259 additions and 98 deletions

View file

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

View file

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

View file

@ -6,6 +6,8 @@
import sys
import json
print(json.dumps({"method":"receive","params":{"envelope":{"source":"67a13c3e-8d29-2539-ce8e-41129c349d6d"},"data":"stuff"}}))
for line in sys.stdin:
try:
data = json.loads(line.strip())

View file

@ -3,11 +3,13 @@ package main
/* This is the main function of this program. It handles setting everything up */
import (
"fmt"
"signal-cli-http/args"
"signal-cli-http/auth"
"signal-cli-http/subprocess"
"signal-cli-http/web"
"time"
"log"
"sync"
)
@ -46,5 +48,9 @@ func main() {
log.Default().Print("Listening on port ", port);
log.Default().Print("Startup tasks complete!");
time.Sleep(time.Millisecond * 500);
fmt.Println(subprocess.GetIMC())
wg.Wait();
}

102
subprocess/incoming.go Normal file
View file

@ -0,0 +1,102 @@
package subprocess
/* This file manages incoming messages*/
import (
"fmt"
"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 incoming messages in a room up to at least 15 minutes old 10,000
This is intentionally an array of pointers so cache-clearing is faster */
var incomingMessageCache []*IncomingMessage;
var incomingMessageCacheLock sync.RWMutex;
func GetIMC() []*IncomingMessage {return incomingMessageCache}
/* Handler for incoming JSON objects which have "method":"receive" */
func handleIncoming(body string, unmarshaledJSONMap map[string]any) (ok bool) {
if val, ok := unmarshaledJSONMap["method"]; !ok || val != "receive" {return false}
fmt.Println(body)
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;
// Obtain read-write lock
incomingMessageCacheLock.Lock();
incomingMessageCache = append(incomingMessageCache, newMessage);
incomingMessageCacheLock.Unlock();
return true;
}
/* Handles clearing space in incomingMessageCache */
func main() {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);
// Only clear when it's 1000 items over
if len(incomingMessageCache) < 1000 {continue}
incomingMessageCacheLock.Lock();
// Don't clear anything after this time
fifteenMinutesAgo := time.Now().Add(-15 * time.Minute);
// Find index in incomingMessageCache that is closest above 15 minutes ago
i := sort.Search(len(incomingMessageCache), func(i int) bool {
return incomingMessageCache[i].receivedTime.After(fifteenMinutesAgo)
})
incomingMessageCache = incomingMessageCache[i:]
incomingMessageCacheLock.Unlock();
}
}
/* Returns a list of encoded JSON strings from incomingMessageCache that match
the filter from */
func GetIncoming(filter map[string]any) string {
var list []string;
// Create copy of incomingMessageCache for efficency
incomingMessageCacheLock.RLock();
incomingMessageCacheCopy := incomingMessageCache;
incomingMessageCacheLock.RUnlock();
// Create list of messages that match the filter
for _, message := range incomingMessageCacheCopy {
if !auth.Match(message.unmarshaledJSONMap, filter) {continue}
fmt.Println(message.body)
list = append(list, message.body)
}
var encoded string = "["
for index, object := range list {
encoded += object
if index == len(list) - 1 {continue}
encoded += ","
}
encoded += "]"
return encoded;
}

View file

@ -42,9 +42,15 @@ func Request(body map[string]any) (responseJSON string, err error) {
waitingJobs[id] = nil;
waitingJobsMutex.Unlock();
// Wait for request to finish
for waitingJobs[id] == nil {
// Wait for request to return
for {
time.Sleep(time.Millisecond)
waitingJobsMutex.RLock();
if waitingJobs[id] != nil {
waitingJobsMutex.RUnlock();
break;
}
waitingJobsMutex.RUnlock();
}
// Lock job when dequeueing
@ -58,26 +64,23 @@ func Request(body map[string]any) (responseJSON string, err error) {
}
/* 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) {
val, ok := unmarshaledJSONMap["id"];
if !ok {return}
id, ok := val.(string)
if !ok {return}
// Write response into
// Read-Write lock the mutex
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 ""}

View file

@ -4,11 +4,12 @@ package subprocess
import (
"bufio"
"encoding/json"
"errors"
"os"
"os/exec"
"sync"
"github.com/creack/pty"
)
@ -39,14 +40,38 @@ func SetupCMD(binaryLocation string) error {
return nil;
}
/* 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"}

View file

@ -3,13 +3,15 @@ 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"
)
func StartWebserver(port int) error {
@ -17,12 +19,20 @@ func StartWebserver(port int) error {
return http.ListenAndServe(":"+fmt.Sprint(port), nil);
}
func writeLog(method string, status int, start time.Time) {
duration := time.Now().Sub(start);
log.Default().Printf("%s %d %s", method, status, duration.String())
}
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"))
writeLog(r.Method, 400, startTime)
return;
}
bearer := authArr[0];
@ -32,28 +42,47 @@ 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)
if err != nil {
@ -62,6 +91,7 @@ func getRoot(w http.ResponseWriter, r *http.Request) {
return
}
// Request returned something
w.WriteHeader(200);
w.Write([]byte(bodyContent));