Skip to content

Quick Server Panels display buttons that make HTTP(S) requests or run CLI commands.

Notifications You must be signed in to change notification settings

Recognition101/qsp

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

14 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Quick Server Panels (QSP)

This repository consists of two parts:

  1. A "Panel UI" that, given some JSON configuration, displays buttons that can make HTTP Requests or run CLI commands on a server implementing the QSP Command Protocol. It can be viewed here.
  2. The qsp.js server. It is a standalone reference implementation of:
    1. The QSP Command Protocol
    2. The QSP Proxy Protocol
    3. A simple file server

Panel UI: Creating Buttons

The Panel UI always shows a Configuration section with Import and Export buttons. These buttons allow you to import and export a configuration file. Note that configuration files are stored in the browser's local storage, so your configuration should remain even on page refreshes.

The JSON configuration consists of a PanelConfig object. It uses a root key for the main Panel. Each Panel has a title string, an optional list of buttons, and an optional list of sub-panel children.

For example, this configuration will display a Main Panel, with Sub Panel A containing A-1 and A-2 buttons followed by Sub Panel B containing B-1 and B-2 buttons.

{
    "root": {
        "title": "Main Panel",
        "children": [
            {
                "title": "Sub Panel A",
                "buttons": [
                    { "text": "A-1" },
                    { "text": "A-2" }
                ]
            },
            {
                "title": "Sub Panel B",
                "buttons": [
                    { "text": "B-1" },
                    { "text": "B-2" }
                ]
            }
        ]
    }
}

Note how children and buttons are optional - the Main Panel has no buttons, and both sub-panels have no children panels.

Buttons, by default, do nothing when pressed - so these buttons will simply display their text.

HTTP(S) Requests

Buttons can make HTTP(S) requests when pressed - simply add a request property with an HttpCallRequest object.

Note that only method and url are required - other properties are optional.

For example:

{
    "root": {
        "title": "Main Panel",
        "buttons": [
            {
                "text": "GET Readme",
                "request": {
                    "method": "GET",
                    "url": "http://192.168.1.64:1234/README.md"
                }
            },
            {
                "text": "POST To Endpoint",
                "request": {
                    "method": "POST",
                    "url": "http://192.168.1.64:1234/endpoint",
                    "headers": { "key": "value" },
                    "body": "POST bodies can go here (optionally)."
                }
            }
        ]
    }
}

When held, buttons will repeatedly request. Initially, they wait repeatInitial milliseconds before repeating, and every subsequent repetition occurs repeat milliseconds apart.

For example: this button, when held, waits 2 seconds initially, then begins making repeated calls every 500 milliseconds.

{
    "root": {
        "title": "Main Panel",
        "buttons": [{
            "text": "GET Readme",
            "repeatInitial": 2000,
            "repeat": 500,
            "request": {
                "method": "GET",
                "url": "http://192.168.1.64:1234/README.md"
            }
        }]
    }
}

Buttons can also display the received response text with the showOutput property. When laying buttons out, you can control their column with the optional column property.

In the following example, B is displayed in the same column as A (underneath it) by using column, and when pressed will display the contents of the README.md when pressed (presuming a static file server at 192.168.1.64:1234).

{
    "root": {
        "title": "Main Panel",
        "buttons": [
            {
                "text": "A"
            },
            {
                "text": "B",
                "showOutput": true,
                "column": 1,
                "request": {
                    "method": "GET",
                    "url": "http://192.168.1.64:1234/README.md"
                }
            }
        ]
    }
}

Button Property Expansion

Buttons also have a set property that contains an arbitrary map of key-strings to value-strings. When reading any string-based property, the application replaces all instances of ${key} with the value of set[key].

In the following example, the button will make a GET to http://192.168.1.64:1234/README.md when pressed.

{
    "root": {
        "title": "Main Panel",
        "buttons": [{
            "text": "README",
            "showOutput": true,
            "set": {
                "ip": "192.168.1.64",
                "file": "README"
            },
            "request": {
                "method": "GET",
                "url": "http://${ip}:1234/${file}.md"
            }
        }]
    }
}

Note: If the actual text is desired, simply add a backslash between the $ and the {. For example, hello $\{world} will expand to the text: hello ${world}. More slashes can be added - one slash will always be removed.

Additionally, a setList property allows substitution of numeric keys. The following example behaves the same as the previous example, but uses setList:

{
    "root": {
        "title": "Main Panel",
        "buttons": [{
            "text": "README (List)",
            "showOutput": true,
            "setList": ["192.168.1.64", "README"],
            "request": {
                "method": "GET",
                "url": "http://${0}:1234/${1}.md"
            }
        }]
    }
}

In addition to the key-value pairs in set and setList, there is a list of global variables that are can be referenced (and which can be overridden if a user uses the same key-name):

  1. GLOBAL_PROTOCOL - The protocol of this page (ex: http: or https:)
  2. GLOBAL_HOST - The current page's host (ex: abc.example.com:1234)
  3. GLOBAL_HOSTNAME - The current page's host name (ex: abc.example.com)
  4. GLOBAL_PORT - The current page's port number (ex: 1234)
  5. GLOBAL_PATHNAME - The current page's path (ex: /a/b/index.html)

Finally, transform functions can be used by prefixing the key with a function name. There are two functions:

  1. ${urlencode:x} - Use encodeURIComponent to encode the text in x.
  2. ${jsonString:x} - Use JSON.stringify to encode the text in x.

For example, the following button makes a request to http://192.168.1.64:1234/README.md?a=%7B%22key%22%3A%22a%5C%22b%22%7D:

{
    "root": {
        "title": "Main Panel",
        "buttons": [{
            "text": "README (Query JSON)",
            "showOutput": true,
            "set": {
                "value": "a\"b",
                "data": "{\"key\":${jsonString:value}}"
            },
            "request": {
                "method": "GET",
                "url": "http://192.168.1.64:1234/README.md?a=${urlencode:data}"
            }
        }]
    }
}

Notice how even set values are expanded - allowing set.data to reference set.value. In the above example:

  • ${value} expands to a"b
  • ${jsonString:value} expands to "a\"" (a JSON-safe quote-surrounded string)
  • ${urlencode:value} expands to a%22b (a URL-param-safe string)

By combining these two (as done above), we can embed JSON data as a URL parameter.

Button Inheritance

The root configuration object also accepts a templates object containing buttons that are not to be displayed.

Instead, these buttons can provide "default" values to other buttons who reference them with the is property. For example, both buttons in the following example GET the same README.md file:

{
    "templates": {
        "ParentA": {
            "text": "unused (Parent A)",
            "showOutput": true,
            "set": { "ip": "192.168.1.64" }
        },
        "ParentB": {
            "text": "unused (Parent B)",
            "showOutput": true,
            "request": {
                "method": "GET",
                "url": "http://192.168.1.64:1234/README.md"
            }
        }
    },

    "root": {
        "title": "Main Panel",
        "buttons": [
            {
                "text": "A",
                "is": "ParentA",
                "set": { "file": "README" },
                "request": {
                    "method": "GET",
                    "url": "http://${ip}:1234/${file}.md"
                }
            },
            {
                "text": "B",
                "is": "ParentB"
            }
        ]
    }
}

Note the following:

  • All properties are "inherited" - for example, both buttons act as if showOutput is true, since they both "inherit" that override from each of their parents.
  • Even request is inherited, as shown by B.
  • set keys are inherited per-key. Above, file is found in A, but ip is inherited from ParentA.
  • Buttons in the templates object can also use the is property, creating multi-level inheritance chains.

Inputs

Users of the Panel UI can update the set map from within the UI. To enable this, add an arguments list to a button. Each item in the list is an ArgumentSchema with a key (matching a set key), user-displayed info, and an optional list of accepted values.

In the following example, users can update host with a text field, and choose from two files (README.md or index.js) to download.

{
    "root": {
        "title": "Main Panel",
        "buttons": [{
            "text": "Download",
            "showOutput": true,
            "set": {
                "host": "192.168.1.64:1234",
                "file": "README.md"
            },
            "arguments": [
                {
                    "key": "host",
                    "info": "The address to download from."
                },
                {
                    "key": "file",
                    "info": "The filename to download.",
                    "values": ["README.md", "index.js"]
                }
            ],
            "request": {
                "method": "GET",
                "url": "http://${host}/${file}"
            }
        }]
    }
}

Note: user input is not escaped in any way - all input is directly copied into set. This means users can potentially input values with ${replacement} text.

Proxy Protocol

By adding a proxyUrl property to a button, the entire HttpCallRequest in the button's request will be serialized and POST-ed to ${proxyUrl}?do=proxy. Assuming the server located at proxyUrl implements the QSP Proxy Protocol, the server will place the request itself and return the result.

The qsp.js server is a reference implementation of the QSP Proxy Protocol.

Command Protocol

By adding a command name to the button along with a commandUrl property, the following will occur:

  1. A list of supported commands will be fetched from ${commandUrl}?do=getCommands.
  2. A command whose name matches command will be found from that list.
  3. The matching command will provide arguments for this button.
  4. Upon pressing the button, the corresponding binary runner will be run on the server.

Additionally, the button can set isPersist to true or false (default). If it is true, the command will stream its output into a new popup window (good for long-running commands).

For an example of using command, see the Full CLI Command Example section below.

QSP Server Reference Implementation

The qsp.js file contains a reference implementation of the QSP Command and Proxy protocols, in addition to serving as a simple static file server.

It can be run with the -h to display usage information, including how to run it with a configuration file.

Configuration

The qsp.js file configuration is a QspServerConfig object. It contains a list of commands, each of which are a QspServerConfigCommand object.

Each command consists of a name (for reference by Panel UI buttons' command property), a list of arguments (identical in structure to Panel UI button arguments), and a runner which describes the CLI binary to run (and its arguments).

The runner is a list of strings. The first represents the binary name (ex: "ls"), and all following strings represent arguments passed to that binary. Each string can contain substitutions with a similar replacement syntax as the Panel UI - for example, ${replace} is replaced with the value of the argument whose key is replace. It supports a different set of string transform functions:

  1. ${relativePath:x} - assumes x is a path or URL, and converts it to a path that must be within the current working directory.

Full CLI Command Example

Check out this repository. In the newly checked out folder, create a qsp-config.js file with the following contents:

/** @type {import('./types').QspServerConfig} */
export const config = {
    commands: [
        {
            name: 'list',
            arguments: [{
                key: 'type',
                info: 'The kind of output to provide',
                values: ['l', 'lah']
            }],
            runner: ['ls', '-${type}']
        },
        {
            name: 'ping-command',
            arguments: [{
                key: 'host',
                info: 'The host to ping.'
            }],
            runner: ['ping', '${host}', '-c', '10']
        }
    ]
};

Additionally, create a panel-config.js with the following contents:

/** @type {import('./types').PanelConfig} */
export const config = {
    "root": {
        "title": "Main Panel",
        "buttons": [
            {
                "text": "LS",
                "command": "list",
                "commandUrl": "http://localhost:1234"
            },
            {
                "text": "Ping Btn",
                "command": "ping-command",
                "commandUrl": "http://localhost:1234",
                "isPersisted": true
            }
        ]
    }
}

Then, run the reference server with:

./qsp.js -c ./qsp-config.js -p 1234 -l _LOCAL

Next, open the Panel UI here: http://localhost:1234/_LOCAL/index.html

In that UI, use Import to import the panel-config.js you created above.

This creates two buttons, one of which runs ls and one which runs ping. Notice:

  • The "isPersisted": true flag in the Panel UI configuration means the output will stream into a new window as it is produced. This is ideal for long-running commands like ping.
  • The LS button in the Panel UI does not display an input popup when pressed. Since it has a single input that uses values (and no other inputs), the button itself becomes a dropdown. Choosing an item from the dropdown runs the command without needing an additional popup. This is ideal for a simple command with a few variants.

About

Quick Server Panels display buttons that make HTTP(S) requests or run CLI commands.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published