Skip to content

Commit a7bb6c8

Browse files
committed
user can now define a callback fn, use signals, custom save fn. See examples/ for code
1 parent 006b532 commit a7bb6c8

File tree

13 files changed

+326
-183
lines changed

13 files changed

+326
-183
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ Inspired by the work of awesome folks over at [msoap/shell2http](https://github.
1919
- Set a script that runs on a succesful POST request to an endpoint of your choice. See [Example code](examples/run_script.py).
2020
- Map a base command to an endpoint and pass dynamic arguments to it. See [Example code](examples/basic.py).
2121
- Can also process multiple uploaded files in one command. See [Example code](examples/multiple_files.py).
22-
- This is useful for internal docker-to-docker communications if you have lots of different binaries. See [real-life example](https://github.com/intelowlproject/IntelOwl/blob/develop/integrations/peframe/app.py).
23-
- Currently, all commands are run asynchronously, so result is not available directly. An option would be provided for this in future release.
22+
- This is useful for internal docker-to-docker communications if you have different binaries distributed in micro-containers. See [real-life example](https://github.com/intelowlproject/IntelOwl/blob/develop/integrations/peframe/app.py).
23+
- Currently, all commands are run asynchronously (default timeout is 3600 seconds), so result is not available directly. An option _may_ be provided for this in future release.
2424

2525
> Note: This extension is primarily meant for executing long-running
2626
> shell commands/scripts (like nmap, code-analysis' tools) in background from an HTTP request and getting the result at a later time.
@@ -29,7 +29,7 @@ Inspired by the work of awesome folks over at [msoap/shell2http](https://github.
2929

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

32-
Read the [Quickstart](https://flask-shell2http.readthedocs.io/en/latest/Quickstart.html)
32+
Read the [Quickstart](https://flask-shell2http.readthedocs.io/en/stable/Quickstart.html)
3333
from the [documentation](https://flask-shell2http.readthedocs.io/) to get started!
3434

3535
## Why?

docs/source/Examples.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
## Examples
22

3-
We have created some example python scripts to demonstrate various use-cases. These include extension setup as well as making HTTP calls with python's [requests](https://requests.readthedocs.io/en/master/) module.
3+
I have created some example python scripts to demonstrate various use-cases. These include extension setup as well as making test HTTP calls with python's [requests](https://requests.readthedocs.io/en/master/) module.
44

55
- [run_script.py](https://github.com/Eshaan7/Flask-Shell2HTTP/blob/master/examples/run_script.py): Execute a script on a succesful POST request to an endpoint.
6-
- [basic.py](https://github.com/Eshaan7/Flask-Shell2HTTP/blob/master/examples/basic.py): Map a base command to an endpoint and pass dynamic arguments to it.
7-
- [multiple_files.py](https://github.com/Eshaan7/Flask-Shell2HTTP/blob/master/examples/multiple_files.py): Upload multiple files for a single command.
6+
- [basic.py](https://github.com/Eshaan7/Flask-Shell2HTTP/blob/master/examples/basic.py): Map a base command to an endpoint and pass dynamic arguments to it. Can also pass in a timeout.
7+
- [multiple_files.py](https://github.com/Eshaan7/Flask-Shell2HTTP/blob/master/examples/multiple_files.py): Upload multiple files for a single command.
8+
- [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.
9+
- [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 intercept the future object after completion and update it's result attribute.

docs/source/Quickstart.md

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ app = Flask(__name__)
2727
executor = Executor(app)
2828
shell2http = Shell2HTTP(app=app, executor=executor, base_url_prefix="/commands/")
2929

30-
shell2http.register_command(endpoint="saythis", command_name="echo")
30+
shell2http.register_command(endpoint="saythis", command_name="echo", callback_fn=None)
3131
```
3232

3333
Run the application server with, `$ flask run -p 4000`.
@@ -45,7 +45,8 @@ $ curl -X POST -H 'Content-Type: application/json' -d '{"args": ["Hello", "World
4545
<details><summary>or using python's requests module,</summary>
4646

4747
```python
48-
data = {"args": ["Hello", "World!"]}
48+
# You can also add a timeout if you want, default value is 3600 seconds
49+
data = {"args": ["Hello", "World!"], "timeout": 60}
4950
resp = requests.post("http://localhost:4000/commands/saythis", json=data)
5051
print("Result:", resp.json())
5152
```
@@ -57,29 +58,33 @@ returns JSON,
5758
```json
5859
{
5960
"key": "ddbe0a94847c",
60-
"result_url": "http://localhost:4000/commands/saythis?key=ddbe0a94847c",
61+
"result_url": "http://localhost:4000/commands/saythis?key=ddbe0a94",
6162
"status": "running"
6263
}
6364
```
6465

6566
Then using this `key` you can query for the result or just by going to the `result_url`,
6667

6768
```bash
68-
$ curl http://localhost:4000/commands/saythis?key=ddbe0a94847c
69+
$ curl http://localhost:4000/commands/saythis?key=ddbe0a94
6970
```
7071

7172
Returns result in JSON,
7273

7374
```json
7475
{
75-
"end_time": 1593019807.782958,
76-
"error": "",
77-
"md5": "ddbe0a94847c",
78-
"process_time": 0.00748753547668457,
79-
"report": {
80-
"result": "Hello World!\n"
81-
},
82-
"start_time": 1593019807.7754705,
83-
"status": "success"
76+
"report": "Hello World!\n",
77+
"key": "ddbe0a94",
78+
"start_time": 1593019807.7754705,
79+
"end_time": 1593019807.782958,
80+
"process_time": 0.00748753547668457,
81+
"status": "success",
82+
"returncode": 0,
83+
"error": "",
8484
}
8585
```
86+
87+
##### Bonus
88+
89+
You can also define callback functions or use signals for reactive programming. There may be cases where the process doesn't print result to standard output but to a file/database. In such cases, you may want to intercept the future object and update it's result attribute.
90+
I request you to take a look at [Examples.md](Examples.md) for such use-cases.

docs/source/conf.py

Lines changed: 1 addition & 1 deletion
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.2.0"
29+
release = "1.3.0"
3030

3131

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

examples/basic.py

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
# system imports
2-
import requests
32
import logging
43
import sys
54

@@ -24,31 +23,28 @@
2423
executor = Executor(app)
2524
shell2http = Shell2HTTP(app, executor, base_url_prefix="/cmd/")
2625

27-
ENDPOINT = "echo"
26+
ENDPOINT_AND_CMD = "echo"
2827

29-
shell2http.register_command(endpoint=ENDPOINT, command_name="echo")
28+
shell2http.register_command(endpoint=ENDPOINT_AND_CMD, command_name=ENDPOINT_AND_CMD)
3029

3130

32-
@app.route("/")
33-
def test():
31+
# Test Runner
32+
if __name__ == "__main__":
33+
app.testing = True
34+
c = app.test_client()
3435
"""
3536
The final executed command becomes:
3637
```bash
3738
$ echo hello world
3839
```
3940
"""
40-
url = f"http://localhost:4000/cmd/{ENDPOINT}"
41-
data = {"args": ["hello", "world"]}
42-
resp = requests.post(url, json=data)
43-
resp_data = resp.json()
44-
print(resp_data)
45-
key = resp_data["key"]
46-
if key:
47-
report = requests.get(f"{url}?key={key}")
48-
return report.json()
49-
return resp_data
50-
51-
52-
# Application Runner
53-
if __name__ == "__main__":
54-
app.run(port=4000)
41+
# make new request for a command with arguments
42+
uri = f"/cmd/{ENDPOINT_AND_CMD}"
43+
# timeout in seconds, default value is 3600
44+
data = {"args": ["hello", "world"], "timeout": 60}
45+
resp1 = c.post(uri, json=data).get_json()
46+
print(resp1)
47+
# fetch result
48+
result_url = resp1["result_url"]
49+
resp2 = c.get(result_url).get_json()
50+
print(resp2)

examples/custom_save_fn.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# web imports
2+
from flask import Flask
3+
from flask_executor import Executor
4+
from flask_executor.futures import Future
5+
from flask_shell2http import Shell2HTTP
6+
7+
# Flask application instance
8+
app = Flask(__name__)
9+
10+
# application factory
11+
executor = Executor(app)
12+
shell2http = Shell2HTTP(app, executor)
13+
14+
ENDPOINT = "echo"
15+
16+
17+
def intercept_result(future: Future):
18+
"""
19+
Will be invoked on every process completion
20+
"""
21+
data = None
22+
if future.done():
23+
with open("/path/to/saved/file") as f:
24+
data = f.read()
25+
# 1. get current result object
26+
res = future.result()
27+
# 2. update the report variable,
28+
# you may update only these: report,error,status
29+
res.report = data
30+
res.status = "success"
31+
# 3. set new result
32+
future._result = res
33+
34+
35+
shell2http.register_command(
36+
endpoint=ENDPOINT, command_name=ENDPOINT, callback_fn=intercept_result
37+
)
38+
39+
40+
# Test Runner
41+
if __name__ == "__main__":
42+
app.testing = True
43+
c = app.test_client()
44+
# request new process
45+
data = {"args": ["hello", "world"]}
46+
r = c.post(f"/{ENDPOINT}", json=data)
47+
# get result
48+
result_url = r.get_json()["result_url"]
49+
resp = c.get(result_url)
50+
print(resp.get_json())

examples/with_callback.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# web imports
2+
from flask import Flask
3+
from flask_executor import Executor
4+
from flask_executor.futures import Future
5+
from flask_shell2http import Shell2HTTP
6+
7+
# Flask application instance
8+
app = Flask(__name__)
9+
10+
# application factory
11+
executor = Executor(app)
12+
shell2http = Shell2HTTP(app, executor, base_url_prefix="/cmd/")
13+
14+
ENDPOINT = "echo"
15+
16+
17+
def my_callback_fn(future: Future):
18+
"""
19+
Will be invoked on every process completion
20+
"""
21+
print("[i] Process running ?:", future.running())
22+
print("[i] Process completed ?:", future.done())
23+
print("[+] Result: ", future.result())
24+
25+
26+
shell2http.register_command(
27+
endpoint=ENDPOINT, command_name=ENDPOINT, callback_fn=my_callback_fn
28+
)
29+
30+
31+
# Test Runner
32+
if __name__ == "__main__":
33+
app.testing = True
34+
c = app.test_client()
35+
# request new process
36+
data = {"args": ["hello", "world"]}
37+
c.post(f"cmd/{ENDPOINT}", json=data)
38+
# request another new process
39+
data = {"args": ["Hello", "Friend!"]}
40+
c.post(f"cmd/{ENDPOINT}", json=data)

examples/with_signals.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# web imports
2+
from flask import Flask
3+
from blinker import Namespace # or from flask.signals import Namespace
4+
from flask_executor import Executor
5+
from flask_executor.futures import Future
6+
from flask_shell2http import Shell2HTTP
7+
8+
# Flask application instance
9+
app = Flask(__name__)
10+
11+
# application factory
12+
executor = Executor(app)
13+
shell2http = Shell2HTTP(app, executor, base_url_prefix="/cmd/")
14+
15+
ENDPOINT = "echo"
16+
CMD = "echo"
17+
18+
# Signal Handling
19+
signal_handler = Namespace()
20+
my_signal = signal_handler.signal(f"on_{CMD}_complete")
21+
# ..or any other name of your choice,
22+
23+
24+
@my_signal.connect
25+
def my_callback_fn(future: Future):
26+
"""
27+
Will be invoked on every process completion
28+
"""
29+
print("Process completed ?:", future.done())
30+
print("Result: ", future.result())
31+
32+
33+
shell2http.register_command(
34+
endpoint=ENDPOINT, command_name=CMD, callback_fn=my_signal.send
35+
)
36+
37+
38+
# Test Runner
39+
if __name__ == "__main__":
40+
app.testing = True
41+
c = app.test_client()
42+
# request new process
43+
data = {"args": ["Hello", "Friend!"]}
44+
c.post(f"cmd/{ENDPOINT}", json=data)
45+
# request new process
46+
data = {"args": ["Bye", "Friend!"]}
47+
c.post(f"cmd/{ENDPOINT}", json=data)

0 commit comments

Comments
 (0)