diff --git a/README.md b/README.md index af39bba..f9aeb73 100644 --- a/README.md +++ b/README.md @@ -22,12 +22,13 @@ A minimalist [Flask](https://github.com/pallets/flask) extension that serves as - You can define a callback function/ use signals to listen for process completion. See [Example code](examples/with_callback.py). * Maybe want to pass some additional context to the callback function ? * Maybe intercept on completion and update the result ? See [Example code](examples/custom_save_fn.py) +- You can also apply [View Decorators](https://flask.palletsprojects.com/en/1.1.x/patterns/viewdecorators/) to the exposed endpoint. See [Example code](examples/with_decorators.py) - Currently, all commands run asynchronously (default timeout is 3600 seconds), so result is not available directly. An option _may_ be provided for this in future releases for commands that return immediately. > Note: This extension is primarily meant for executing long-running > shell commands/scripts (like nmap, code-analysis' tools) in background from an HTTP request and getting the result at a later time. -## Documentation / Quick Start +## Documentation [![Documentation Status](https://readthedocs.org/projects/flask-shell2http/badge/?version=latest)](https://flask-shell2http.readthedocs.io/en/latest/?badge=latest) @@ -36,6 +37,97 @@ from the [documentation](https://flask-shell2http.readthedocs.io/) to get starte I highly recommend the [Examples](https://flask-shell2http.readthedocs.io/en/stable/Examples.html) section. +## Quick Start + +##### Dependencies + +* Python: `>=v3.6` +* [Flask](https://pypi.org/project/Flask/) +* [Flask-Executor](https://pypi.org/project/Flask-Executor) + +##### Installation + +```bash +$ pip install flask flask_shell2http +``` + +##### Example Program + +Create a file called `app.py`. + +```python +from flask import Flask +from flask_executor import Executor +from flask_shell2http import Shell2HTTP + +# Flask application instance +app = Flask(__name__) + +executor = Executor(app) +shell2http = Shell2HTTP(app=app, executor=executor, base_url_prefix="/commands/") + +def my_callback_fn(context, future): + # optional user-defined callback function + print(context, future.result()) + +shell2http.register_command(endpoint="saythis", command_name="echo", callback_fn=my_callback_fn, decorators=[]) +``` + +Run the application server with, `$ flask run -p 4000`. + +With <10 lines of code, we succesfully mapped the shell command `echo` to the endpoint `/commands/saythis`. + +##### Making HTTP calls + +This section demonstrates how we can now call/ execute commands over HTTP that we just mapped in the [example](#example-program) above. + +```bash +$ curl -X POST -H 'Content-Type: application/json' -d '{"args": ["Hello", "World!"]}' http://localhost:4000/commands/saythis +``` + +
or using python's requests module, + +```python +# You can also add a timeout if you want, default value is 3600 seconds +data = {"args": ["Hello", "World!"], "timeout": 60} +resp = requests.post("http://localhost:4000/commands/saythis", json=data) +print("Result:", resp.json()) +``` + +
+ +> Note: You can see the JSON schema for the POST request [here](https://github.com/Eshaan7/Flask-Shell2HTTP/blob/master/post-request-schema.json). + +returns JSON, + +```json +{ + "key": "ddbe0a94", + "result_url": "http://localhost:4000/commands/saythis?key=ddbe0a94", + "status": "running" +} +``` + +Then using this `key` you can query for the result or just by going to the `result_url`, + +```bash +$ curl http://localhost:4000/commands/saythis?key=ddbe0a94 +``` + +Returns result in JSON, + +```json +{ + "report": "Hello World!\n", + "key": "ddbe0a94", + "start_time": 1593019807.7754705, + "end_time": 1593019807.782958, + "process_time": 0.00748753547668457, + "returncode": 0, + "error": null, +} +``` + ## Inspiration This was initially made to integrate various command-line tools easily with [Intel Owl](https://github.com/intelowlproject/IntelOwl), which I am working on as part of Google Summer of Code. diff --git a/docs/source/Examples.md b/docs/source/Examples.md index e9beaea..d16e169 100644 --- a/docs/source/Examples.md +++ b/docs/source/Examples.md @@ -7,4 +7,5 @@ I have created some example python scripts to demonstrate various use-cases. The - [multiple_files.py](https://github.com/Eshaan7/Flask-Shell2HTTP/blob/master/examples/multiple_files.py): Upload multiple files for a single command. - [with_callback.py](https://github.com/Eshaan7/Flask-Shell2HTTP/blob/master/examples/with_callback.py): Define a callback function that executes on command/process completion. - [with_signals.py](https://github.com/Eshaan7/Flask-Shell2HTTP/blob/master/examples/with_signals.py): Using [Flask Signals](https://flask.palletsprojects.com/en/1.1.x/signals/) as callback function. -- [custom_save_fn.py](https://github.com/Eshaan7/Flask-Shell2HTTP/blob/master/examples/custom_save_fn.py): There may be cases where the process doesn't print result to standard output but to a file/database. This example shows how to pass additional context to the callback function, intercept the future object after completion and update it's result attribute before it's ready to be consumed. \ No newline at end of file +- [with_decorators.py](https://github.com/Eshaan7/Flask-Shell2HTTP/blob/master/examples/with_decorators.py): Shows how to apply [View Decorators](https://flask.palletsprojects.com/en/1.1.x/patterns/viewdecorators/) to the exposed endpoint. Useful in case you wish to apply authentication, caching, etc. to the endpoint. +- [custom_save_fn.py](https://github.com/Eshaan7/Flask-Shell2HTTP/blob/master/examples/custom_save_fn.py): There may be cases where the process doesn't print result to standard output but to a file/database. This example shows how to pass additional context to the callback function, intercept the future object after completion and update it's result attribute before it's ready to be consumed. diff --git a/docs/source/Quickstart.md b/docs/source/Quickstart.md index 07ca903..dd8c514 100644 --- a/docs/source/Quickstart.md +++ b/docs/source/Quickstart.md @@ -28,10 +28,10 @@ executor = Executor(app) shell2http = Shell2HTTP(app=app, executor=executor, base_url_prefix="/commands/") def my_callback_fn(context, future): - # additional user-defined callback function + # optional user-defined callback function print(context, future.result()) -shell2http.register_command(endpoint="saythis", command_name="echo", callback_fn=my_callback_fn) +shell2http.register_command(endpoint="saythis", command_name="echo", callback_fn=my_callback_fn, decorators=[]) ``` Run the application server with, `$ flask run -p 4000`. diff --git a/docs/source/conf.py b/docs/source/conf.py index 972fc5d..e29c7d8 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -26,7 +26,7 @@ author = "Eshaan Bansal" # The full version, including alpha/beta/rc tags -release = "1.4.3" +release = "1.5.0" # -- General configuration --------------------------------------------------- diff --git a/docs/source/index.rst b/docs/source/index.rst index ff29dab..b440bf4 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -20,6 +20,7 @@ A minimalist Flask_ extension that serves as a RESTful/HTTP wrapper for python's - Can also process multiple uploaded files in one command. - This is useful for internal docker-to-docker communications if you have different binaries distributed in micro-containers. - You can define a callback function/ use signals to listen for process completion. +- You can also apply View Decorators to the exposed endpoint. - Currently, all commands run asynchronously (default timeout is 3600 seconds), so result is not available directly. An option _may_ be provided for this in future release. `Note: This extension is primarily meant for executing long-running diff --git a/examples/with_decorators.py b/examples/with_decorators.py new file mode 100644 index 0000000..5b93276 --- /dev/null +++ b/examples/with_decorators.py @@ -0,0 +1,69 @@ +# generic imports +import functools + +# web imports +from flask import Flask, request, g, abort, Response +from flask_executor import Executor +from flask_shell2http import Shell2HTTP + +# Flask application instance +app = Flask(__name__) + +# application factory +executor = Executor(app) +shell2http = Shell2HTTP(app, executor, base_url_prefix="/cmd/") + + +# few decorators [1] +def logging_decorator(f): + @functools.wraps(f) + def decorator(*args, **kwargs): + print("*" * 64) + print( + "from logging_decorator: " + request.url + " : " + str(request.remote_addr) + ) + print("*" * 64) + return f(*args, **kwargs) + + return decorator + + +def login_required(f): + @functools.wraps(f) + def decorator(*args, **kwargs): + if not hasattr(g, "user") or g.user is None: + abort(Response("You are not logged in.", 401)) + return f(*args, **kwargs) + + return decorator + + +shell2http.register_command( + endpoint="public/echo", command_name="echo", decorators=[logging_decorator] +) + +shell2http.register_command( + endpoint="protected/echo", + command_name="echo", + decorators=[login_required, logging_decorator], # [2] +) + +# [1] View Decorators: +# https://flask.palletsprojects.com/en/1.1.x/patterns/viewdecorators/ +# [2] remember that decorators are applied from left to right in a stack manner. +# But are executed in right to left manner. +# Put logging_decorator first and you will see what happens. + + +# Test Runner +if __name__ == "__main__": + app.testing = True + c = app.test_client() + # request 1 + data = {"args": ["hello", "world"]} + r1 = c.post("cmd/public/echo", json=data) + print(r1.json, r1.status_code) + # request 2 + data = {"args": ["Hello", "Friend!"]} + r2 = c.post("cmd/protected/echo", json=data) + print(r2.data, r2.status_code) diff --git a/flask_shell2http/api.py b/flask_shell2http/api.py index e7cde27..13188b8 100644 --- a/flask_shell2http/api.py +++ b/flask_shell2http/api.py @@ -7,8 +7,8 @@ """ # system imports -from http import HTTPStatus import functools +from http import HTTPStatus from typing import Callable, Dict, Any # web imports diff --git a/flask_shell2http/base_entrypoint.py b/flask_shell2http/base_entrypoint.py index 6d6b557..cb4adc2 100644 --- a/flask_shell2http/base_entrypoint.py +++ b/flask_shell2http/base_entrypoint.py @@ -1,6 +1,6 @@ # system imports from collections import OrderedDict -from typing import Callable, Dict, Any +from typing import Callable, Dict, List, Any # web imports from flask_executor import Executor @@ -75,6 +75,7 @@ def register_command( endpoint: str, command_name: str, callback_fn: Callable[[Dict, Future], Any] = None, + decorators: List = [], ) -> None: """ Function to map a shell command to an endpoint. @@ -97,6 +98,9 @@ def register_command( - The same callback function may be used for multiple commands. - if request JSON contains a `callback_context` attr, it will be passed as the first argument to this function. + decorators (List[Callable]): + - A List of view decorators to apply to the endpoint. + - *New in version v1.5.0* Examples:: @@ -107,7 +111,8 @@ def my_callback_fn(context: dict, future: Future) -> None: shell2http.register_command( endpoint="myawesomescript", command_name="./fuxsocy.py", - callback_fn=my_callback_fn + callback_fn=my_callback_fn, + decorators=[], ) """ uri: str = self.__construct_route(endpoint) @@ -121,14 +126,18 @@ def my_callback_fn(context: dict, future: Future) -> None: return None # else, add new URL rule + view_func = shell2httpAPI.as_view( + endpoint, + command_name=command_name, + user_callback_fn=callback_fn, + executor=self.__executor, + ) + # apply decorators, if any + for dec in decorators: + view_func = dec(view_func) + # register URL rule self.app.add_url_rule( - uri, - view_func=shell2httpAPI.as_view( - endpoint, - command_name=command_name, - user_callback_fn=callback_fn, - executor=self.__executor, - ), + uri, view_func=view_func, ) self.__commands.update({uri: command_name}) logger.info(f"New endpoint: '{uri}' registered for command: '{command_name}'.") diff --git a/setup.py b/setup.py index 3d79ff9..bdb4d22 100644 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ setup( name="Flask-Shell2HTTP", - version="1.4.3", + version="1.5.0", url=GITHUB_URL, license="BSD", author="Eshaan Bansal",