Skip to content

Commit f75f816

Browse files
committed
Initial commit
1 parent ee56a6e commit f75f816

File tree

14 files changed

+720
-0
lines changed

14 files changed

+720
-0
lines changed

.flake8

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[flake8]
2+
max-line-length = 88
3+
ignore =
4+
W503, # line break before binary operator
5+
E231, # missing whitespace after ','
6+
exclude =
7+
Dockerfile,
8+
venv,
9+
virtualenv
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# This workflows will upload a Python Package using Twine when a release is created
2+
# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries
3+
4+
name: Upload Python Package
5+
6+
on:
7+
release:
8+
types: [created]
9+
10+
jobs:
11+
deploy:
12+
13+
runs-on: ubuntu-latest
14+
15+
steps:
16+
- uses: actions/checkout@v2
17+
- name: Set up Python
18+
uses: actions/setup-python@v2
19+
with:
20+
python-version: '3.x'
21+
- name: Install dependencies
22+
run: |
23+
python -m pip install --upgrade pip
24+
pip install setuptools wheel twine
25+
- name: Build and publish
26+
env:
27+
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
28+
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
29+
run: |
30+
python setup.py sdist bdist_wheel
31+
twine upload dist/*

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.vscode
2+
venv/
3+
virtualenv/
4+
__pycache__/
5+
app.py
6+
build/
7+
dist/
8+
*.egg-info

README.md

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# Flask-Shell2HTTP
2+
3+
A minimalist REST API wrapper for python's subprocess API.<br/>
4+
Execute shell commands asynchronously and safely from flask's endpoints.
5+
6+
Inspired by the work of awesome folks over at [msoap/shell2http](https://github.com/msoap/shell2http).
7+
8+
## You can use this for
9+
10+
- Set a script that runs on a succesful POST request to an endpoint of your choice. See [Example code](examples/run_script.py)
11+
- Map a base command to an endpoint and passing dynamic arguments to it. See [Example code](examples/basic.py)
12+
- Can also process uploaded files. See [Example code](examples/multiple_files.py)
13+
- Choose to run a command asynchronously or not. (upcoming feature)
14+
15+
## Quick Start
16+
17+
#### Dependencies
18+
19+
- Python: `>=v3.6`
20+
- [Flask](https://pypi.org/project/Flask/)
21+
- [Flask-Executor](https://pypi.org/project/Flask-Executor)
22+
23+
#### Install
24+
25+
```bash
26+
$ pip install flask_shell2http flask_executor
27+
```
28+
29+
#### Example
30+
31+
```python
32+
from flask import Flask
33+
from flask_executor import Executor
34+
from flask_shell2http import Shell2HTTP
35+
36+
# Flask application instance
37+
app = Flask(__name__)
38+
39+
executor = Executor(app)
40+
shell2http = Shell2HTTP(app=app, executor=executor, base_url_prefix="/commands/")
41+
42+
shell2http.register_command(endpoint="saythis", command_name="echo")
43+
```
44+
45+
Run the application server with, `$ flask run -p 4000`.
46+
47+
#### Make HTTP calls
48+
49+
```bash
50+
$ curl -X POST -d '{"args": ["Hello", "World!"]}' http://localhost:4000/commands/saythis
51+
```
52+
53+
<details><summary>or using python's requests module,</summary>
54+
55+
```python
56+
data = {"args": ["Hello", "World!"]}
57+
resp = requests.post("http://localhost:4000/commands/saythis", json=data)
58+
print("Result:", resp.json())
59+
```
60+
61+
</details>
62+
63+
returns JSON,
64+
65+
```json
66+
{
67+
"key": "ddbe0a94847c65f9b8198424ffd07c50",
68+
"status": "running"
69+
}
70+
```
71+
72+
Then using this `key` you can query for the result,
73+
74+
```bash
75+
$ curl http://localhost:4000/commands/saythis?key=ddbe0a94847c65f9b8198424ffd07c50
76+
```
77+
78+
Returns result in JSON,
79+
80+
```json
81+
{
82+
"end_time": 1593019807.782958,
83+
"error": "",
84+
"md5": "ddbe0a94847c65f9b8198424ffd07c50",
85+
"process_time": 0.00748753547668457,
86+
"report": {
87+
"result": "Hello World!\n"
88+
},
89+
"start_time": 1593019807.7754705,
90+
"status": "success"
91+
}
92+
```
93+
94+
## Why?
95+
96+
This was made to integrate various command-line tools easily with [IntelOwl](https://github.com/intelowlproject/IntelOwl).
97+
98+
## Various examples
99+
100+
You can find various examples under [examples](examples/)

examples/basic.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# system imports
2+
import requests
3+
4+
# web imports
5+
from flask import Flask
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+
ENDPOINT = "echo"
17+
18+
shell2http.register_command(endpoint=ENDPOINT, command_name="echo")
19+
20+
21+
@app.route("/")
22+
def test():
23+
"""
24+
The final executed command becomes:
25+
```bash
26+
$ echo hello world
27+
```
28+
"""
29+
url = f"http://localhost:4000/cmd/{ENDPOINT}"
30+
data = {"args": ["hello", "world"]}
31+
resp = requests.post(url, json=data)
32+
resp_data = resp.json()
33+
print(resp_data)
34+
key = resp_data["key"]
35+
if key:
36+
resp2 = requests.get(f"{url}?key={key}")
37+
return resp2.json()
38+
else:
39+
return resp_data
40+
41+
42+
# Application Runner
43+
if __name__ == "__main__":
44+
app.run(port=4000)

examples/multiple_files.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# system imports
2+
import requests
3+
import tempfile
4+
5+
# web imports
6+
from flask import Flask
7+
from flask_executor import Executor
8+
from flask_shell2http import Shell2HTTP
9+
10+
# Flask application instance
11+
app = Flask(__name__)
12+
13+
# application factory
14+
executor = Executor()
15+
executor.init_app(app)
16+
shell2http = Shell2HTTP(base_url_prefix="/cmd/")
17+
shell2http.init_app(app, executor)
18+
19+
ENDPOINT = "catthisformeplease"
20+
21+
shell2http.register_command(endpoint=ENDPOINT, command_name="strings")
22+
23+
24+
@app.route("/")
25+
def test():
26+
"""
27+
Prefix each filename with @ in arguments.\n
28+
Files are stored in temporary directories which are flushed on command completion.\n
29+
The final executed command becomes:
30+
```bash
31+
$ strings /tmp/inputfile /tmp/someotherfile
32+
```
33+
"""
34+
url = f"http://localhost:4000/cmd/{ENDPOINT}"
35+
data = {"args": ["@inputfile", "@someotherfile"]}
36+
with tempfile.TemporaryFile() as fp:
37+
fp.write(b"Hello world!")
38+
fp.seek(0)
39+
f = fp.read()
40+
files = {"inputfile": f, "someotherfile": f}
41+
resp = requests.post(url=url, files=files, data=data)
42+
resp_data = resp.json()
43+
print(resp_data)
44+
key = resp_data["key"]
45+
if key:
46+
resp2 = requests.get(f"{url}?key={key}")
47+
return resp2.json()
48+
else:
49+
return resp_data
50+
51+
52+
# Application Runner
53+
if __name__ == "__main__":
54+
app.run(port=4000)

examples/run_script.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# system imports
2+
import requests
3+
4+
# web imports
5+
from flask import Flask
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="/scripts/")
15+
16+
shell2http.register_command(endpoint="hacktheplanet", command_name="./fuxsocy.py")
17+
18+
19+
@app.route("/")
20+
def test():
21+
"""
22+
The final executed command becomes:
23+
```bash
24+
$ ./fuxsocy.py
25+
```
26+
"""
27+
url = "http://localhost:4000/scripts/hacktheplanet"
28+
resp = requests.post(url)
29+
resp_data = resp.json()
30+
print(resp_data)
31+
key = resp_data["key"]
32+
if key:
33+
resp2 = requests.get(f"{url}?key={key}")
34+
return resp2.json()
35+
else:
36+
return resp_data
37+
38+
39+
# Application Runner
40+
if __name__ == "__main__":
41+
app.run(port=4000)

flask_shell2http/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# flake8: noqa
2+
from .base_entrypoint import Shell2HTTP

flask_shell2http/api.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# system imports
2+
from http import HTTPStatus
3+
4+
# web imports
5+
from flask import request, jsonify, make_response
6+
from flask.views import MethodView
7+
8+
# lib imports
9+
from .classes import JobExecutor, ReportStore, RequestParser
10+
11+
12+
class shell2httpAPI(MethodView):
13+
command_name: str
14+
executor: JobExecutor
15+
store: ReportStore
16+
request_parser: RequestParser
17+
18+
def get(self):
19+
try:
20+
md5: str = request.args.get("key")
21+
if not md5:
22+
raise Exception("No key provided in arguments.")
23+
# check if job has been finished
24+
future = self.executor.get_job(md5)
25+
if future:
26+
if not future.done:
27+
return make_response(jsonify(status="running", md5=md5), 200)
28+
29+
# pop future object since it has been finished
30+
self.executor.pop_job(md5)
31+
32+
# if yes, get result from store
33+
report = self.store.get_one(md5)
34+
if not report:
35+
raise Exception(f"Report does not exist for key:{md5}")
36+
37+
return make_response(report.to_json(), HTTPStatus.OK)
38+
39+
except Exception as e:
40+
return make_response(jsonify(error=str(e)), HTTPStatus.NOT_FOUND)
41+
42+
def post(self):
43+
try:
44+
# Check if command is correct and parse it
45+
cmd, md5 = self.request_parser.parse_req(request)
46+
47+
# run executor job in background
48+
job_key = JobExecutor.make_key(md5)
49+
future = self.executor.new_job(
50+
future_key=job_key, fn=self.executor.run_command, cmd=cmd, md5=md5
51+
)
52+
# callback that adds result to store
53+
future.add_done_callback(self.store.save_result)
54+
# callback that removes the temporary directory
55+
future.add_done_callback(self.request_parser.cleanup_temp_dir)
56+
57+
return make_response(
58+
jsonify(status="running", key=md5), HTTPStatus.ACCEPTED,
59+
)
60+
61+
except Exception as e:
62+
return make_response(jsonify(error=str(e)), HTTPStatus.BAD_REQUEST)
63+
64+
def __init__(self, command_name, executor):
65+
self.command_name = command_name
66+
self.executor = JobExecutor(executor)
67+
self.store = ReportStore()
68+
self.request_parser = RequestParser(command_name)

0 commit comments

Comments
 (0)