Finally functional

This commit is contained in:
Ben 2025-07-28 22:35:06 -07:00
parent be8936a665
commit b673c640e4
Signed by: webmaster
GPG key ID: A5FCBAF34E6E8B50
12 changed files with 261 additions and 113 deletions

View file

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

View file

@ -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")
```
-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.

View file

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

View file

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

14
dummy.py Executable file
View file

@ -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")

9
go.mod
View file

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

15
go.sum Normal file
View file

@ -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=

29
main.go
View file

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

View file

@ -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.
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.

View file

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

View file

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

View file

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