Finally functional
This commit is contained in:
parent
be8936a665
commit
b673c640e4
12 changed files with 261 additions and 113 deletions
16
args/args.go
16
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;
|
||||
}
|
|
@ -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.
|
48
auth/auth.go
48
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;
|
||||
}
|
|
@ -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
14
dummy.py
Executable 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
9
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
|
||||
)
|
||||
|
|
15
go.sum
Normal file
15
go.sum
Normal 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
29
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();
|
||||
|
|
|
@ -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.
|
|
@ -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)
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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))
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue