Skip to content

Commit 369cb6a

Browse files
committed
decorators functionality. Bump to v1.5.0
1 parent fb86d39 commit 369cb6a

File tree

9 files changed

+188
-16
lines changed

9 files changed

+188
-16
lines changed

README.md

+93-1
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,13 @@ A minimalist [Flask](https://github.com/pallets/flask) extension that serves as
2222
- You can define a callback function/ use signals to listen for process completion. See [Example code](examples/with_callback.py).
2323
* Maybe want to pass some additional context to the callback function ?
2424
* Maybe intercept on completion and update the result ? See [Example code](examples/custom_save_fn.py)
25+
- 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)
2526
- 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.
2627

2728
> Note: This extension is primarily meant for executing long-running
2829
> shell commands/scripts (like nmap, code-analysis' tools) in background from an HTTP request and getting the result at a later time.
2930
30-
## Documentation / Quick Start
31+
## Documentation
3132

3233
[![Documentation Status](https://readthedocs.org/projects/flask-shell2http/badge/?version=latest)](https://flask-shell2http.readthedocs.io/en/latest/?badge=latest)
3334

@@ -36,6 +37,97 @@ from the [documentation](https://flask-shell2http.readthedocs.io/) to get starte
3637

3738
I highly recommend the [Examples](https://flask-shell2http.readthedocs.io/en/stable/Examples.html) section.
3839

40+
## Quick Start
41+
42+
##### Dependencies
43+
44+
* Python: `>=v3.6`
45+
* [Flask](https://pypi.org/project/Flask/)
46+
* [Flask-Executor](https://pypi.org/project/Flask-Executor)
47+
48+
##### Installation
49+
50+
```bash
51+
$ pip install flask flask_shell2http
52+
```
53+
54+
##### Example Program
55+
56+
Create a file called `app.py`.
57+
58+
```python
59+
from flask import Flask
60+
from flask_executor import Executor
61+
from flask_shell2http import Shell2HTTP
62+
63+
# Flask application instance
64+
app = Flask(__name__)
65+
66+
executor = Executor(app)
67+
shell2http = Shell2HTTP(app=app, executor=executor, base_url_prefix="/commands/")
68+
69+
def my_callback_fn(context, future):
70+
# optional user-defined callback function
71+
print(context, future.result())
72+
73+
shell2http.register_command(endpoint="saythis", command_name="echo", callback_fn=my_callback_fn, decorators=[])
74+
```
75+
76+
Run the application server with, `$ flask run -p 4000`.
77+
78+
With <10 lines of code, we succesfully mapped the shell command `echo` to the endpoint `/commands/saythis`.
79+
80+
##### Making HTTP calls
81+
82+
This section demonstrates how we can now call/ execute commands over HTTP that we just mapped in the [example](#example-program) above.
83+
84+
```bash
85+
$ curl -X POST -H 'Content-Type: application/json' -d '{"args": ["Hello", "World!"]}' http://localhost:4000/commands/saythis
86+
```
87+
88+
<details><summary>or using python's requests module,</summary>
89+
90+
```python
91+
# You can also add a timeout if you want, default value is 3600 seconds
92+
data = {"args": ["Hello", "World!"], "timeout": 60}
93+
resp = requests.post("http://localhost:4000/commands/saythis", json=data)
94+
print("Result:", resp.json())
95+
```
96+
97+
</details>
98+
99+
> Note: You can see the JSON schema for the POST request [here](https://github.com/Eshaan7/Flask-Shell2HTTP/blob/master/post-request-schema.json).
100+
101+
returns JSON,
102+
103+
```json
104+
{
105+
"key": "ddbe0a94",
106+
"result_url": "http://localhost:4000/commands/saythis?key=ddbe0a94",
107+
"status": "running"
108+
}
109+
```
110+
111+
Then using this `key` you can query for the result or just by going to the `result_url`,
112+
113+
```bash
114+
$ curl http://localhost:4000/commands/saythis?key=ddbe0a94
115+
```
116+
117+
Returns result in JSON,
118+
119+
```json
120+
{
121+
"report": "Hello World!\n",
122+
"key": "ddbe0a94",
123+
"start_time": 1593019807.7754705,
124+
"end_time": 1593019807.782958,
125+
"process_time": 0.00748753547668457,
126+
"returncode": 0,
127+
"error": null,
128+
}
129+
```
130+
39131
## Inspiration
40132

41133
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.

docs/source/Examples.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ I have created some example python scripts to demonstrate various use-cases. The
77
- [multiple_files.py](https://github.com/Eshaan7/Flask-Shell2HTTP/blob/master/examples/multiple_files.py): Upload multiple files for a single command.
88
- [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.
99
- [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.
10-
- [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.
10+
- [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.
11+
- [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.

docs/source/Quickstart.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,10 @@ executor = Executor(app)
2828
shell2http = Shell2HTTP(app=app, executor=executor, base_url_prefix="/commands/")
2929

3030
def my_callback_fn(context, future):
31-
# additional user-defined callback function
31+
# optional user-defined callback function
3232
print(context, future.result())
3333

34-
shell2http.register_command(endpoint="saythis", command_name="echo", callback_fn=my_callback_fn)
34+
shell2http.register_command(endpoint="saythis", command_name="echo", callback_fn=my_callback_fn, decorators=[])
3535
```
3636

3737
Run the application server with, `$ flask run -p 4000`.

docs/source/conf.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
author = "Eshaan Bansal"
2727

2828
# The full version, including alpha/beta/rc tags
29-
release = "1.4.3"
29+
release = "1.5.0"
3030

3131

3232
# -- General configuration ---------------------------------------------------

docs/source/index.rst

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ A minimalist Flask_ extension that serves as a RESTful/HTTP wrapper for python's
2020
- Can also process multiple uploaded files in one command.
2121
- This is useful for internal docker-to-docker communications if you have different binaries distributed in micro-containers.
2222
- You can define a callback function/ use signals to listen for process completion.
23+
- You can also apply View Decorators to the exposed endpoint.
2324
- 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.
2425

2526
`Note: This extension is primarily meant for executing long-running

examples/with_decorators.py

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# generic imports
2+
import functools
3+
4+
# web imports
5+
from flask import Flask, request, g, abort, Response
6+
from flask_executor import Executor
7+
from flask_shell2http import Shell2HTTP
8+
9+
# Flask application instance
10+
app = Flask(__name__)
11+
12+
# application factory
13+
executor = Executor(app)
14+
shell2http = Shell2HTTP(app, executor, base_url_prefix="/cmd/")
15+
16+
17+
# few decorators [1]
18+
def logging_decorator(f):
19+
@functools.wraps(f)
20+
def decorator(*args, **kwargs):
21+
print("*" * 64)
22+
print(
23+
"from logging_decorator: " + request.url + " : " + str(request.remote_addr)
24+
)
25+
print("*" * 64)
26+
return f(*args, **kwargs)
27+
28+
return decorator
29+
30+
31+
def login_required(f):
32+
@functools.wraps(f)
33+
def decorator(*args, **kwargs):
34+
if not hasattr(g, "user") or g.user is None:
35+
abort(Response("You are not logged in.", 401))
36+
return f(*args, **kwargs)
37+
38+
return decorator
39+
40+
41+
shell2http.register_command(
42+
endpoint="public/echo", command_name="echo", decorators=[logging_decorator]
43+
)
44+
45+
shell2http.register_command(
46+
endpoint="protected/echo",
47+
command_name="echo",
48+
decorators=[login_required, logging_decorator], # [2]
49+
)
50+
51+
# [1] View Decorators:
52+
# https://flask.palletsprojects.com/en/1.1.x/patterns/viewdecorators/
53+
# [2] remember that decorators are applied from left to right in a stack manner.
54+
# But are executed in right to left manner.
55+
# Put logging_decorator first and you will see what happens.
56+
57+
58+
# Test Runner
59+
if __name__ == "__main__":
60+
app.testing = True
61+
c = app.test_client()
62+
# request 1
63+
data = {"args": ["hello", "world"]}
64+
r1 = c.post("cmd/public/echo", json=data)
65+
print(r1.json, r1.status_code)
66+
# request 2
67+
data = {"args": ["Hello", "Friend!"]}
68+
r2 = c.post("cmd/protected/echo", json=data)
69+
print(r2.data, r2.status_code)

flask_shell2http/api.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
"""
88

99
# system imports
10-
from http import HTTPStatus
1110
import functools
11+
from http import HTTPStatus
1212
from typing import Callable, Dict, Any
1313

1414
# web imports

flask_shell2http/base_entrypoint.py

+18-9
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# system imports
22
from collections import OrderedDict
3-
from typing import Callable, Dict, Any
3+
from typing import Callable, Dict, List, Any
44

55
# web imports
66
from flask_executor import Executor
@@ -75,6 +75,7 @@ def register_command(
7575
endpoint: str,
7676
command_name: str,
7777
callback_fn: Callable[[Dict, Future], Any] = None,
78+
decorators: List = [],
7879
) -> None:
7980
"""
8081
Function to map a shell command to an endpoint.
@@ -97,6 +98,9 @@ def register_command(
9798
- The same callback function may be used for multiple commands.
9899
- if request JSON contains a `callback_context` attr, it will be passed
99100
as the first argument to this function.
101+
decorators (List[Callable]):
102+
- A List of view decorators to apply to the endpoint.
103+
- *New in version v1.5.0*
100104
101105
Examples::
102106
@@ -107,7 +111,8 @@ def my_callback_fn(context: dict, future: Future) -> None:
107111
shell2http.register_command(
108112
endpoint="myawesomescript",
109113
command_name="./fuxsocy.py",
110-
callback_fn=my_callback_fn
114+
callback_fn=my_callback_fn,
115+
decorators=[],
111116
)
112117
"""
113118
uri: str = self.__construct_route(endpoint)
@@ -121,14 +126,18 @@ def my_callback_fn(context: dict, future: Future) -> None:
121126
return None
122127

123128
# else, add new URL rule
129+
view_func = shell2httpAPI.as_view(
130+
endpoint,
131+
command_name=command_name,
132+
user_callback_fn=callback_fn,
133+
executor=self.__executor,
134+
)
135+
# apply decorators, if any
136+
for dec in decorators:
137+
view_func = dec(view_func)
138+
# register URL rule
124139
self.app.add_url_rule(
125-
uri,
126-
view_func=shell2httpAPI.as_view(
127-
endpoint,
128-
command_name=command_name,
129-
user_callback_fn=callback_fn,
130-
executor=self.__executor,
131-
),
140+
uri, view_func=view_func,
132141
)
133142
self.__commands.update({uri: command_name})
134143
logger.info(f"New endpoint: '{uri}' registered for command: '{command_name}'.")

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
setup(
1919
name="Flask-Shell2HTTP",
20-
version="1.4.3",
20+
version="1.5.0",
2121
url=GITHUB_URL,
2222
license="BSD",
2323
author="Eshaan Bansal",

0 commit comments

Comments
 (0)