Skip to content

Commit

Permalink
decorators functionality. Bump to v1.5.0
Browse files Browse the repository at this point in the history
  • Loading branch information
eshaan7 committed Sep 21, 2020
1 parent fb86d39 commit 369cb6a
Show file tree
Hide file tree
Showing 9 changed files with 188 additions and 16 deletions.
94 changes: 93 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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
```

<details><summary>or using python's requests module,</summary>

```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())
```

</details>

> 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.
Expand Down
3 changes: 2 additions & 1 deletion docs/source/Examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
- [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.
4 changes: 2 additions & 2 deletions docs/source/Quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
2 changes: 1 addition & 1 deletion docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 ---------------------------------------------------
Expand Down
1 change: 1 addition & 0 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
69 changes: 69 additions & 0 deletions examples/with_decorators.py
Original file line number Diff line number Diff line change
@@ -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)
2 changes: 1 addition & 1 deletion flask_shell2http/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
"""

# system imports
from http import HTTPStatus
import functools
from http import HTTPStatus
from typing import Callable, Dict, Any

# web imports
Expand Down
27 changes: 18 additions & 9 deletions flask_shell2http/base_entrypoint.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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::
Expand All @@ -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)
Expand All @@ -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}'.")
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

setup(
name="Flask-Shell2HTTP",
version="1.4.3",
version="1.5.0",
url=GITHUB_URL,
license="BSD",
author="Eshaan Bansal",
Expand Down

0 comments on commit 369cb6a

Please sign in to comment.