From 24d80fe7d6492d393d30d306bbbdb861b1d6acde Mon Sep 17 00:00:00 2001 From: Kleissner Date: Tue, 10 Aug 2021 04:58:53 +0200 Subject: [PATCH] Finish /console API endpoint. The code handling input/output from the user/command line needed to be adjusted. --- API.go | 54 +++++++++---- API.md | 17 ++-- Command Line.go | 206 ++++++++++++++++++++++++++++-------------------- 3 files changed, 171 insertions(+), 106 deletions(-) diff --git a/API.go b/API.go index 59f76c2..ddfc2a9 100644 --- a/API.go +++ b/API.go @@ -7,10 +7,10 @@ Author: Peter Kleissner package main import ( + "bytes" "crypto/tls" "encoding/hex" "encoding/json" - "fmt" "log" "net/http" "time" @@ -142,25 +142,49 @@ func apiConsole(w http.ResponseWriter, r *http.Request) { // May happen if request is simple HTTP request. return } - - // start reader/writer to forward - defer c.Close() + + bufferR := bytes.NewBuffer(make([]byte, 0, 4096)) + bufferW := bytes.NewBuffer(make([]byte, 0, 4096)) + + terminateSignal := make(chan struct{}) + defer close(terminateSignal) + + // start userCommands which handles the actual commands + go userCommands(bufferR, bufferW, terminateSignal) + + // go routine to receive output from userCommands and forward to websocket + go func() { + bufferW2 := make([]byte, 4096) + for { + select { + case <-terminateSignal: + return + default: + } + + countRead, err := bufferW.Read(bufferW2) + if err != nil || countRead == 0 { + time.Sleep(250 * time.Millisecond) + continue + } + + c.WriteMessage(websocket.TextMessage, bufferW2[:countRead]) + } + }() + + // read from websocket loop and forward to the userCommands routine for { _, message, err := c.ReadMessage() - if err != nil { - //fmt.Println("read:", err) + if err != nil { // when channel is closed, an error is returned here break } - //c.NextWriter(websocket.BinaryMessage) - //c.NextReader() - //userCommands() - fmt.Printf("recv: %s", message) - //err = c.WriteMessage(mt, message) - //if err != nil { - // //fmt.Println("write:", err) - // break - //} + // make sure the message has the \n delimiter which is used to detect a line + if !bytes.HasSuffix(message, []byte{'\n'}) { + message = append(message, '\n') + } + + bufferR.Write(message) } } diff --git a/API.md b/API.md index f9903d8..29c6c38 100644 --- a/API.md +++ b/API.md @@ -16,7 +16,11 @@ The API is still in development and endpoints are subject to change. The API sho ## Configuartion -The configuration file (default `Config.yaml`) contains settings for the API. +The configuration file (default `Config.yaml`) contains settings for the API. To listen on `http://127.0.0.1:112/` add this line: + +```yaml +APIListen: ["127.0.0.1:112"] +``` ## Overview @@ -25,9 +29,6 @@ These are the functions provided by the API: ``` /status Provides current connectivity status to the network /peer/self Provides information about the self peer details -/blockchain/self/header Header of the self peers blockchain -/blockchain/self/read Read the self peers blockchain -/blockchain/self/append Add a record to the blockchain /share/list List all files and directories that are shared /share/file Share a file via the peers blockchain @@ -37,9 +38,13 @@ These are the functions provided by the API: /status/ws Starts a websocket to receive updates on operations immediately (push instead of pull) /console Console provides a websocket to send/receive internal commands + +/blockchain/self/header Header of the self peers blockchain +/blockchain/self/read Read the self peers blockchain +/blockchain/self/append Add a record to the blockchain ``` -The `/share` functions are providing high-level functionality to work with files; the `/blockchain` functions provide low-level functionality. +The `/share` functions are providing high-level functionality to work with files. The `/blockchain` functions provide low-level functionality which is typically not needed. ## Status @@ -84,7 +89,7 @@ Example response data: Todo ## Console -The `/console` web-socket allows to execute internal commands. This should be only used for debugging purposes by the end-user. The same input and output as raw text as via the command-line is provided through this endpoint. +The `/console` websocket allows to execute internal commands. This should be only used for debugging purposes by the end-user. The same input and output as raw text as via the command-line is provided through this endpoint. ``` Request: ws://127.0.0.1:112/console diff --git a/Command Line.go b/Command Line.go index 01a52ec..a0c664e 100644 --- a/Command Line.go +++ b/Command Line.go @@ -23,65 +23,6 @@ import ( "github.com/btcsuite/btcd/btcec" ) -func getUserOptionString(reader *bufio.Reader) (response string, valid bool) { - responseA, err := reader.ReadString('\n') - if err != nil { - return "", false - } - - responseA = strings.TrimSpace(responseA) - - return responseA, true -} - -func getUserOptionBool(reader *bufio.Reader) (response bool, valid bool) { - responseA, err := reader.ReadString('\n') - if err != nil { - return false, false - } - - responseA = strings.TrimSpace(responseA) // also removes the delimiter - - responseI, err := strconv.Atoi(responseA) - if err != nil || (responseI != 0 && responseI != 1) { - return false, false - } - - return responseI == 1, true -} - -func getUserOptionInt(reader *bufio.Reader) (response int, valid bool) { - responseA, err := reader.ReadString('\n') - if err != nil { - return 0, false - } - - responseA = strings.TrimSpace(responseA) // also removes the delimiter - - responseI, err := strconv.Atoi(responseA) - if err != nil { - return 0, false - } - - return responseI, true -} - -func getUserOptionHash(reader *bufio.Reader) (hash []byte, valid bool) { - responseA, err := reader.ReadString('\n') - if err != nil { - return nil, false - } - - responseA = strings.TrimSpace(responseA) - - hash, err = hex.DecodeString(responseA) - if err != nil || len(hash) != 256/8 { - return nil, false - } - - return hash, true -} - func showHelp(output io.Writer) { fmt.Fprint(output, "Please enter a command:\n"+ "help Show this help\n"+ @@ -104,18 +45,18 @@ func showHelp(output io.Writer) { "\n") } -func userCommands(input io.Reader, output io.Writer) { +func userCommands(input io.Reader, output io.Writer, terminateSignal chan struct{}) { reader := bufio.NewReader(input) fmt.Fprint(output, appName+" "+core.Version+"\n------------------------------\n") showHelp(output) for { - command, valid := getUserOptionString(reader) - if !valid { - time.Sleep(time.Second) - continue + command, _, terminate := getUserOptionString(reader, terminateSignal) + if terminate { + return } + command = strings.ToLower(command) switch command { @@ -155,8 +96,10 @@ func userCommands(input io.Reader, output io.Writer) { } case "chat all", "chat": - if text, valid := getUserOptionString(reader); valid { + if text, valid, terminate := getUserOptionString(reader, terminateSignal); valid { core.SendChatAll(text) + } else if terminate { + return } case "status": @@ -242,13 +185,15 @@ func userCommands(input io.Reader, output io.Writer) { fmt.Fprintf(output, "\n") case "hash": - if text, valid := getUserOptionString(reader); valid { + if text, valid, terminate := getUserOptionString(reader, terminateSignal); valid { hash := core.Data2Hash([]byte(text)) fmt.Fprintf(output, "blake3 hash: %s\n", hex.EncodeToString(hash)) + } else if terminate { + return } case "warehouse get": - if hash, valid := getUserOptionHash(reader); valid { + if hash, valid, terminate := getUserOptionHash(reader, terminateSignal); valid { data, found := core.GetDataLocal(hash) if !found { fmt.Fprintf(output, "Not found.\n") @@ -256,30 +201,36 @@ func userCommands(input io.Reader, output io.Writer) { fmt.Fprintf(output, "Data hex: %s\n", hex.EncodeToString(data)) fmt.Fprintf(output, "Data string: %s\n", string(data)) } + } else if terminate { + return } else { fmt.Fprintf(output, "Invalid hash. Hex-encoded blake3 hash as input is required.\n") } case "warehouse store": - if text, valid := getUserOptionString(reader); valid { + if text, valid, terminate := getUserOptionString(reader, terminateSignal); valid { if err := core.StoreDataLocal([]byte(text)); err != nil { fmt.Fprintf(output, "Error storing data: %s\n", err.Error()) break } fmt.Fprintf(output, "Stored via hash: %s\n", hex.EncodeToString(core.Data2Hash([]byte(text)))) + } else if terminate { + return } case "dht store": - if text, valid := getUserOptionString(reader); valid { + if text, valid, terminate := getUserOptionString(reader, terminateSignal); valid { if err := core.StoreDataDHT([]byte(text), 5); err != nil { fmt.Fprintf(output, "Error storing data: %s\n", err.Error()) break } fmt.Fprintf(output, "Stored via hash: %s\n", hex.EncodeToString(core.Data2Hash([]byte(text)))) + } else if terminate { + return } case "dht get": - if hash, valid := getUserOptionHash(reader); valid { + if hash, valid, terminate := getUserOptionHash(reader, terminateSignal); valid { data, sender, found := core.GetDataDHT(hash) if !found { fmt.Fprintf(output, "Not found.\n") @@ -288,22 +239,28 @@ func userCommands(input io.Reader, output io.Writer) { fmt.Fprintf(output, "Data hex: %s\n", hex.EncodeToString(data)) fmt.Fprintf(output, "Data string: %s\n", string(data)) } + } else if terminate { + return } else { fmt.Fprintf(output, "Invalid hash. Hex-encoded blake3 hash as input is required.\n") } case "log error": fmt.Fprintf(output, "Please choose the target output of error messages:\n0 = Log file (default)\n1 = Command line\n2 = Log file + command line\n3 = None\n") - if number, valid := getUserOptionInt(reader); !valid || number < 0 || number > 3 { - fmt.Fprintf(output, "Invalid option.\n") - } else { + if number, valid, terminate := getUserOptionInt(reader, terminateSignal); valid && number >= 0 && number <= 3 { config.ErrorOutput = number + } else if terminate { + return + } else { + fmt.Fprintf(output, "Invalid option.\n") } case "debug connect": fmt.Fprintf(output, "Please specify the target peer to connect to via DHT lookup, either by peer ID or node ID:\n") - text, valid := getUserOptionString(reader) - if !valid || (len(text) != 66 && len(text) != 64) { + text, valid, terminate := getUserOptionString(reader, terminateSignal) + if terminate { + return + } else if !valid || (len(text) != 66 && len(text) != 64) { fmt.Fprintf(output, "Invalid peer ID or node ID. It must be hex-encoded and 66 (peer ID) or 64 characters (node ID) long.\n") break } @@ -345,31 +302,40 @@ func userCommands(input io.Reader, output io.Writer) { case "debug watch searches": fmt.Fprintf(output, "Enable (1) or disable (0) watching of all outgoing DHT searches? (current setting: %t)\n", enableMonitorAll) - if number, valid := getUserOptionInt(reader); !valid || number < 0 || number > 1 { - fmt.Fprintf(output, "Invalid option.\n") - } else { + if number, valid, terminate := getUserOptionInt(reader, terminateSignal); valid && number >= 0 && number <= 1 { enableMonitorAll = number == 1 + } else if terminate { + return + } else { + fmt.Fprintf(output, "Invalid option.\n") } case "debug watch incoming": fmt.Fprintf(output, "Enable (1) or disable (0) watching of all incoming information requests? (current setting: %t)\n", enableWatchIncomingAll) - if number, valid := getUserOptionInt(reader); !valid || number < 0 || number > 1 { - fmt.Fprintf(output, "Invalid option.\n") - } else { + if number, valid, terminate := getUserOptionInt(reader, terminateSignal); valid && number >= 0 && number <= 1 { enableWatchIncomingAll = number == 1 + } else if terminate { + return + } else { + fmt.Fprintf(output, "Invalid option.\n") } case "debug bucket refresh": fmt.Fprintf(output, "Disable (1) or enable (0) bucket refresh. This can be useful to disable bucket refresh when debugging outgoing DHT searches. (current setting: %t)\n", dht.DisableBucketRefresh) - if number, valid := getUserOptionInt(reader); !valid || number < 0 || number > 1 { - fmt.Fprintf(output, "Invalid option.\n") - } else { + if number, valid, terminate := getUserOptionInt(reader, terminateSignal); valid && number >= 0 && number <= 1 { dht.DisableBucketRefresh = number == 1 + } else if terminate { + return + } else { + fmt.Fprintf(output, "Invalid option.\n") } case "debug watch": fmt.Fprintf(output, "Enter hash of data or node ID to watch. This monitors info requests and packets. Enter same hash again to remove from list.\n") - text, _ := getUserOptionString(reader) + text, _, terminate := getUserOptionString(reader, terminateSignal) + if terminate { + return + } var hash []byte var err error if hash, err = hex.DecodeString(text); err != nil || len(hash) != 256/8 { @@ -564,3 +530,73 @@ func logError(function, format string, v ...interface{}) { fmt.Printf("["+function+"] "+format, v...) } } + +// ---- command-line helper functions ---- + +// timeRetryUserInput defines how long the code waits for user input from reader before trying again +// The termination signal takes effect once the reader is drained and returns io.EOF. +const timeRetryUserInput = 500 * time.Millisecond + +// readUserText reads user text from the buffer. Blocking, unless termination signal is raised! +func readUserText(reader *bufio.Reader, terminateSignal <-chan struct{}) (text string, valid, terminate bool) { + for { + if text, err := reader.ReadString('\n'); err == nil { + return strings.TrimSpace(text), true, false + } + + // check for termination signal + select { + case <-terminateSignal: + return "", false, true + default: + } + + time.Sleep(timeRetryUserInput) + } +} + +func getUserOptionString(reader *bufio.Reader, terminateSignal <-chan struct{}) (response string, valid, terminate bool) { + return readUserText(reader, terminateSignal) +} + +func getUserOptionBool(reader *bufio.Reader, terminateSignal <-chan struct{}) (response bool, valid, terminate bool) { + responseA, valid, terminate := readUserText(reader, terminateSignal) + if !valid || terminate { + return false, valid, terminate + } + + responseI, err := strconv.Atoi(responseA) + if err != nil || (responseI != 0 && responseI != 1) { + return false, false, false + } + + return responseI == 1, true, false +} + +func getUserOptionInt(reader *bufio.Reader, terminateSignal <-chan struct{}) (response int, valid, terminate bool) { + responseA, valid, terminate := readUserText(reader, terminateSignal) + if !valid || terminate { + return 0, valid, terminate + } + + responseI, err := strconv.Atoi(responseA) + if err != nil { + return 0, false, false + } + + return responseI, true, false +} + +func getUserOptionHash(reader *bufio.Reader, terminateSignal <-chan struct{}) (hash []byte, valid, terminate bool) { + responseA, valid, terminate := readUserText(reader, terminateSignal) + if !valid || terminate { + return nil, valid, terminate + } + + hash, err := hex.DecodeString(responseA) + if err != nil || len(hash) != 256/8 { + return nil, false, false + } + + return hash, true, false +}