Skip to content

Modules

Maurice Lambert edited this page Jun 13, 2024 · 8 revisions

Modules

Module is useful to send custom response:

  • Custom headers
  • Custom dynamic page
  • Custom URL
  • Custom request (POST form, custom JSON content...)
  • Custom authentication and permissions
  • ...

Module is a python module or a package imported in WebScripts Server.

Custom functions

Signatures:

from typing import TypeVar, List, Dict, Tuple, Union
from collections.abc import Iterator
from os import _Environ

Json = TypeVar("Json", dict, list, str, int, None)
Server = TypeVar("Server")
User = TypeVar("User")

def example1(
        environ: _Environ,
        user: User,
        server: Server,
        filename: str,
        arguments: List[str],       # Arguments is a list of str, if you send a "WebScripts request" (a JSON object with "arguments" as attribute)
        inputs: List[str],          # Value of inputs
        csrf_token: str = None,
    ) -> Tuple[str, Dict[str, str], Union[str, bytes, Iterator[bytes]]]:

    return (
        "200 OK",
        {"Content-Security-Policy": "default-src 'self'"},
        "Response text."
    )

def example2(
        environ: _Environ,
        user: User,
        server: Server,
        filename: str,
        arguments: Json,            # Arguments is a loaded JSON, if you send a JSON content without attribute named "arguments" 
        inputs: List[str],          # Inputs will be a empty list
        csrf_token: str = None,
    ) -> Tuple[str, Dict[str, str], Union[str, bytes, Iterator[bytes]]]:

        return (
                "200 OK",
                {"Content-Security-Policy": "default-src 'self'"},
                [b"Response text."]
        )

def example3(
        environ: _Environ,
        user: User,
        server: Server,
        filename: str,
        arguments: bytes,           # Arguments is bytes, if you send a non JSON request
        inputs: List[str],          # Inputs will be a empty list
        csrf_token: str = None,
    ) -> Tuple[str, Dict[str, str], Union[str, bytes, Iterator[bytes]]]:

        return (
                "200 OK",
                {"Content-Security-Policy": "default-src 'self'"},
                (x for x in [b"Response text."])
        )

Arguments

  1. environ (no default value): WSGI environment variables for this request
  2. user (no default value): User object (attributes: ["id", "name", "groups", "csrf", "ip", "check_csrf"] , optional: your custom user configurations)
  3. server (no default value): it's the WebScripts Server object and contains configurations, useful functions, you can change the WebScripts behaviour from this object. Be careful, it's an advanced usages and you can break the server or security features.
  4. filename (no default value): element after the last /
  5. arguments (no default value): list of command line arguments (to launch a script) or a loaded JSON (for JSON content without "arguments" attribute) or bytes (for non-JSON content)
  6. inputs (no default value): list of inputs (for stdin of the script) or empty list (if the content is a non WebScripts request: non-JSON content or JSON without "arguments" attribute)
  7. csrf_token (optional: default value is None)

The arguments and inputs lists are built if you respect the default JSON body of WebScripts Server. If the body of the request is JSON without arguments key/attribute, arguments will be a dict or list. If the body of the request is not JSON, arguments will be bytes.

Examples

  1. WebScripts request:
POST /ModuleName/ClassName/method_name/little_string_argument HTTP/1.1
Host: webscripts.local
User-Agent: WebScripts client
Origin: http://127.0.0.1:8000
Content-Type: application/json

{"arguments": {"arg1": {"value": "my argument", "input": false}, "--optional-arg1": {"value": "WebScripts is good !", "input": false}, "arg1": {"value": "my input", "input": true}}}
  • filename will be little_string_argument
  • arguments will be a list with my argument, --optional-arg1 and WebScripts is good ! as values (arg1 don't start with -: is not in the list, but --optional-arg1 start with -: is in the arguments list)
  • inputs will be a list with my input as values
  1. JSON request:
POST /ModuleName/ClassName/method_name/ HTTP/1.1
Host: webscripts.local
User-Agent: WebScripts client
Origin: http://127.0.0.1:8000
Content-Type: application/json

{"my dict": {"my list": [1, 2.5], "my string": "WebScripts is good !", "my int": 678, "my float": 45.6, "null": null, "true": true, "false": false}}
  • filename will be empty string
  • arguments will be a dict: {"my dict": {"my list": [1, 2.5], "my string": "WebScripts is good !", "my int": 678, "my float": 45.6, "null": None, "true": True, "false": False}}
  • inputs will be a empty list
  1. Others request:
POST /ModuleName/ClassName/method_name/xml_is_bad HTTP/1.1
Host: webscripts.local
User-Agent: WebScripts client
Origin: http://127.0.0.1:8000
Content-Type: application/xml

<ilove>JSON</ilove>
  • filename will be a string with xml_is_bad as value
  • arguments will be bytes with <ilove>JSON</ilove> as value (you can send binary data, like archives, images, office documents, executables, ect...)
  • inputs will be a empty list

Return

  1. Response HTTP code (str): the HTTP status of the response, the first three digits are required (example: 200 OK)
  2. Headers (dict): dictionary of HTTP headers (pairs of names and header values)
  3. Response body (bytes, str, Iterator[bytes]): the HTTP body of the response

URLs

In the PATH_INFO the character / is like . (object attribute) in python code, the last / is a call (function()).

Examples

URLs to call a function named hello in a hello module:

/hello/hello/                   # python code equivalent: hello.hello(..., filename='', ...)
/hello/hello/abc                # python code equivalent: hello.hello(..., filename='abc', ...)

URLs to call a function named test in a class named Test in a module named Tests in a package named Example:

/Example/Tests/Test/test/       # python code equivalent: Example.Tests.Test.test(..., filename='', ...)
/Example/Tests/Test/test/abc    # python code equivalent: Example.Tests.Test.test(..., filename='abc', ...)

Headers

Some default security headers are sended for all response, you can override the value but you can't delete these headers.

Custom error pages

To build your custom error pages (HTTP errors: 500, 403, 404...) create a module (the name does not matter) with functions named: page_<error>, for example on error 500 the function used will be page_500.

Look at /path/of/WebScripts/scripts/py/hello.py this is a demonstration.

Try

To try a module you can comment/uncomment lines (16-19) in server.ini, to get the following configuration:

# modules                                                                                        # Add custom modules (names) to the server
# modules_path                                                                                   # Add directory to import custom modules
modules=hello
modules_path=./scripts/py

Start the WebScripts Server and open these URL in your web broswer:

  1. Hello function.
  2. Custom error 500 page (only if the debug configuration is false).
  3. Custom error 404 page (only if the debug configuration is false).
  4. Custom error 403 page

Get the code in /path/of/WebScripts/project/scripts/py/hello.py.

Default modules

  • cgi, make your own web pages and responses with any executable files and scripts (it's like modules for non python syntax, but you can't access to WebScripts server and configurations)
  • Configuration, activated with the debug mode, read and change your configurations in the web page without stop and restart the WebScripts server.
  • csp, activated with the debug mode, debug the CSP errors and get the CSP report.
  • error_pages, default error pages with requests to WebScripts administrators.
  • JsonRpc, a simple json rpc module to add simple API for some of your automatised tasks
  • notification, add a notifcation on the WebScripts Web Page
  • rss, a RSS to notify, read and add news for teams
  • share, uploads files and generates links to download shared files

Examples

CGI

Simple

URL: http://127.0.0.1:8000/cgi/bin/test.py, http://127.0.0.1:8000/cgi/test.py, http://127.0.0.1:8000/bin/test.py, http://127.0.0.1:8000/cgi-bin/test.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

print("Content-Type: text/plain")
print()
print("Hello world !")
Advanced

URL: http://127.0.0.1:8000/cgi/bin/hello.py, http://127.0.0.1:8000/cgi/hello.py, http://127.0.0.1:8000/bin/hello.py, http://127.0.0.1:8000/cgi-bin/hello.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from cgi import FieldStorage, parse, MiniFieldStorage
from urllib.parse import unquote, parse_qs

# from cgitb import enable
from os import environ
from sys import argv

# enable() # debug mode


def parse_args(*args, **kwargs) -> None:

    """
    This function parses arguments/body with
    differents functions/tools.
    """

    print("\t\t - Simple parsing:")
    if len(argv) == 2:
        arguments = unquote(argv[1])
        print(f"\t\t\t - Arguments: {argv[0]!r} {argv[1]!r}")
    else:
        arguments = parse_qs(
            environ["QUERY_STRING"], *args, **kwargs
        ) or parse(*args, **kwargs)
        for key, values in arguments.items():
            print(
                "\t\t\t - ",
                repr(key),
                "=",
                *[(repr(v) + ", ") for v in values],
            )

    print("\t\t - Complex parsing:")
    arguments = FieldStorage(*args, **kwargs)
    for argument_name in arguments.keys():
        value = arguments[argument_name]
        if isinstance(value, MiniFieldStorage):
            print(
                "\t\t\t - ",
                repr(argument_name),
                "=",
                repr(value.value) + ",",
                value,
            )
        elif isinstance(value, list):
            print(
                "\t\t\t - ",
                repr(argument_name),
                "=",
                [(repr(v.value) + ",") for v in value],
                *value,
            )


print("Content-Type: text/plain")
print()
print("Hello world !")

print("\t 1. Don't keep blank values: ")
parse_args()
print("\t 2. Keep blank values: ")
parse_args(keep_blank_values=True)

print("- WebScripts -")

CSP

Only with insecure mode activated, don't use it in production.

URL: http://127.0.0.1:8000/csp/debug/

Configurations

Only with debug mode activated, don't use it in production.

URL: http://127.0.0.1:8000/Configurations/Reload/server/, http://127.0.0.1:8000/Configurations/Reload/scripts/[script_name] (http://127.0.0.1:8000/Configurations/Reload/scripts/test_config.py), http://127.0.0.1:8000/Configurations/Reload/arguments/[script_name]|[argument_name] (http://127.0.0.1:8000/Configurations/Reload/arguments/test_config.py|test_input), http://127.0.0.1:8000/Configurations/Reload/modules/, http://127.0.0.1:8000/Configurations/Reload/web/, http://127.0.0.1:8000/Configurations/Reload/module/[module_name] (http://127.0.0.1:8000/Configurations/Reload/module/cgi)

Error pages

URL: http://127.0.0.1:8000/error_pages/Report/new/[HTTP_error_code], http://127.0.0.1:8000/error_pages/Request/send/[HTTP_error_code]

Error pages are used when the body is empty or None:

# create your default error page for error 404
def page_404(error: str) -> Tuple[str, Dict[str, str], List[bytes]]:
    """
    This function returns the default HTTP response for error 404.
    """
    return "404 Not Found" or error, {"Header1": "Value1"}, [b'<html><body>Error 404, Page Not Found, are you lost ?</body></html>']

def not_found_default(environ, user, server_configuration, filename, arguments, inputs, csrf_token = None):
    return "404", {}, None  # call page_404 because data is None

def not_found_default(environ, user, server_configuration, filename, arguments, inputs, csrf_token = None):
    return "404", {}, b''   # call page_404 because data is empty

def not_found_custom(environ, user, server_configuration, filename, arguments, inputs, csrf_token = None):
    return "404", {"Header1": "Value1"}, [b'html><body>This is not my default 404 error page !</body></html>'] # don't call page_404 because there are data

JsonRpc

Server
from WebScripts.modules.JsonRpc import JsonRpc

def test_call() -> int:
    return 0

def test_argument_list(*args) -> str:
    return str([repr(a) for a in args])

def test_argument_dict(a:int = 1, b:int = 2) -> int:
    return a + b

JsonRpc.register_function(test_call, "call")
JsonRpc.register_function(test_argument_list)
JsonRpc.register_function(test_argument_dict, "test_args_dict")

# start your WebScripts server here
Client

URL: http://127.0.0.1:8000/JsonRpc/JsonRpc/call, http://127.0.0.1:8000/JsonRpc/JsonRpc/test_argument_list, http://127.0.0.1:8000/JsonRpc/JsonRpc/test_args_dict

from urllib.request import urlopen, Request, HTTPError, URLError
from pprint import pprint
from json import load

try:
    response = urlopen(
        Request(
            "http://127.0.0.1:8000/JsonRpc/JsonRpc/call",
            method="POST",
            headers={"Origin": "http://127.0.0.1:8000", "Authorization": "Basic QWRtaW46QWRtaW4=", "Content-Type": "application/json"},
            data=b'{"jsonrpc": "2.0", "id": 1, "method": "call"}',
        )
    )
except (HTTPError, URLError) as e:
    response = e

print("Status", response.code, response.reason)
pprint(load(response))

response = urlopen(
    Request(
        "http://127.0.0.1:8000/JsonRpc/JsonRpc/test_argument_list",
        method="POST",
        headers={"Origin": "http://127.0.0.1:8000", "Authorization": "Basic QWRtaW46QWRtaW4=", "Content-Type": "application/json"},
        data=b'{"jsonrpc": "2.0", "id": 2, "method": "test_argument_list", "params": ["abc", 1, null, true]}',
    )
)
pprint(load(response))

response = urlopen(
    Request(
        "http://127.0.0.1:8000/JsonRpc/JsonRpc/test_args_dict",
        method="POST",
        headers={"Origin": "http://127.0.0.1:8000", "Authorization": "Basic QWRtaW46QWRtaW4=", "Content-Type": "application/json"},
        data=b'{"jsonrpc": "2.0", "id": 3, "method": "test_args_dict", "params": {"a": 2, "b": 3}}',
    )
)
pprint(load(response))

Notification

URL: http://127.0.0.1:8000/notification/add/

RSS

URL: http://127.0.0.1:8000/rss/Feed/csv/[news_category], http://127.0.0.1:8000/rss/Feed/json/[news_category], http://127.0.0.1:8000/rss/Feed/[news_category]

Share

URL: http://127.0.0.1:8000/share/Download/filename/LICENSE.txt, http://127.0.0.1:8000/share/Download/id/0, http://127.0.0.1:8000/share/upload/