pywebexec 0.0.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pywebexec-0.0.3/.github/workflows/python-publish.yml +39 -0
- pywebexec-0.0.3/.gitignore +172 -0
- pywebexec-0.0.3/LICENSE +21 -0
- pywebexec-0.0.3/PKG-INFO +130 -0
- pywebexec-0.0.3/README.md +71 -0
- pywebexec-0.0.3/pyproject.toml +56 -0
- pywebexec-0.0.3/pywebexec/__init__.py +5 -0
- pywebexec-0.0.3/pywebexec/pywebexec.py +284 -0
- pywebexec-0.0.3/pywebexec/static/images/aborted.svg +1 -0
- pywebexec-0.0.3/pywebexec/static/images/copy.svg +1 -0
- pywebexec-0.0.3/pywebexec/static/images/copy_ok.svg +1 -0
- pywebexec-0.0.3/pywebexec/static/images/failed.svg +1 -0
- pywebexec-0.0.3/pywebexec/static/images/running.svg +1 -0
- pywebexec-0.0.3/pywebexec/static/images/success.svg +1 -0
- pywebexec-0.0.3/pywebexec/templates/__init__.py +0 -0
- pywebexec-0.0.3/pywebexec/templates/index.html +265 -0
- pywebexec-0.0.3/pywebexec/version.py +16 -0
- pywebexec-0.0.3/pywebexec.egg-info/PKG-INFO +130 -0
- pywebexec-0.0.3/pywebexec.egg-info/SOURCES.txt +23 -0
- pywebexec-0.0.3/pywebexec.egg-info/dependency_links.txt +1 -0
- pywebexec-0.0.3/pywebexec.egg-info/entry_points.txt +2 -0
- pywebexec-0.0.3/pywebexec.egg-info/requires.txt +3 -0
- pywebexec-0.0.3/pywebexec.egg-info/top_level.txt +1 -0
- pywebexec-0.0.3/setup.cfg +7 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# This workflow will upload a Python Package using Twine when a release is created
|
|
2
|
+
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
|
|
3
|
+
|
|
4
|
+
# This workflow uses actions that are not certified by GitHub.
|
|
5
|
+
# They are provided by a third-party and are governed by
|
|
6
|
+
# separate terms of service, privacy policy, and support
|
|
7
|
+
# documentation.
|
|
8
|
+
|
|
9
|
+
name: Upload Python Package
|
|
10
|
+
|
|
11
|
+
on:
|
|
12
|
+
release:
|
|
13
|
+
types: [published]
|
|
14
|
+
|
|
15
|
+
permissions:
|
|
16
|
+
contents: read
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
deploy:
|
|
20
|
+
|
|
21
|
+
runs-on: ubuntu-latest
|
|
22
|
+
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/checkout@v4
|
|
25
|
+
- name: Set up Python
|
|
26
|
+
uses: actions/setup-python@v3
|
|
27
|
+
with:
|
|
28
|
+
python-version: '3.x'
|
|
29
|
+
- name: Install dependencies
|
|
30
|
+
run: |
|
|
31
|
+
python -m pip install --upgrade pip
|
|
32
|
+
pip install build
|
|
33
|
+
- name: Build package
|
|
34
|
+
run: python -m build
|
|
35
|
+
- name: Publish package
|
|
36
|
+
uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
|
|
37
|
+
with:
|
|
38
|
+
user: __token__
|
|
39
|
+
password: ${{ secrets.PYPI_API_TOKEN }}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
# Usually these files are written by a python script from a template
|
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py,cover
|
|
50
|
+
.hypothesis/
|
|
51
|
+
.pytest_cache/
|
|
52
|
+
cover/
|
|
53
|
+
|
|
54
|
+
# Translations
|
|
55
|
+
*.mo
|
|
56
|
+
*.pot
|
|
57
|
+
|
|
58
|
+
# Django stuff:
|
|
59
|
+
*.log
|
|
60
|
+
local_settings.py
|
|
61
|
+
db.sqlite3
|
|
62
|
+
db.sqlite3-journal
|
|
63
|
+
|
|
64
|
+
# Flask stuff:
|
|
65
|
+
instance/
|
|
66
|
+
.webassets-cache
|
|
67
|
+
|
|
68
|
+
# Scrapy stuff:
|
|
69
|
+
.scrapy
|
|
70
|
+
|
|
71
|
+
# Sphinx documentation
|
|
72
|
+
docs/_build/
|
|
73
|
+
|
|
74
|
+
# PyBuilder
|
|
75
|
+
.pybuilder/
|
|
76
|
+
target/
|
|
77
|
+
|
|
78
|
+
# Jupyter Notebook
|
|
79
|
+
.ipynb_checkpoints
|
|
80
|
+
|
|
81
|
+
# IPython
|
|
82
|
+
profile_default/
|
|
83
|
+
ipython_config.py
|
|
84
|
+
|
|
85
|
+
# pyenv
|
|
86
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
87
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
88
|
+
# .python-version
|
|
89
|
+
|
|
90
|
+
# pipenv
|
|
91
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
92
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
93
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
94
|
+
# install all needed dependencies.
|
|
95
|
+
#Pipfile.lock
|
|
96
|
+
|
|
97
|
+
# UV
|
|
98
|
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
99
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
100
|
+
# commonly ignored for libraries.
|
|
101
|
+
#uv.lock
|
|
102
|
+
|
|
103
|
+
# poetry
|
|
104
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
105
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
106
|
+
# commonly ignored for libraries.
|
|
107
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
108
|
+
#poetry.lock
|
|
109
|
+
|
|
110
|
+
# pdm
|
|
111
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
112
|
+
#pdm.lock
|
|
113
|
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
114
|
+
# in version control.
|
|
115
|
+
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
|
116
|
+
.pdm.toml
|
|
117
|
+
.pdm-python
|
|
118
|
+
.pdm-build/
|
|
119
|
+
|
|
120
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
121
|
+
__pypackages__/
|
|
122
|
+
|
|
123
|
+
# Celery stuff
|
|
124
|
+
celerybeat-schedule
|
|
125
|
+
celerybeat.pid
|
|
126
|
+
|
|
127
|
+
# SageMath parsed files
|
|
128
|
+
*.sage.py
|
|
129
|
+
|
|
130
|
+
# Environments
|
|
131
|
+
.env
|
|
132
|
+
.venv
|
|
133
|
+
env/
|
|
134
|
+
venv/
|
|
135
|
+
ENV/
|
|
136
|
+
env.bak/
|
|
137
|
+
venv.bak/
|
|
138
|
+
|
|
139
|
+
# Spyder project settings
|
|
140
|
+
.spyderproject
|
|
141
|
+
.spyproject
|
|
142
|
+
|
|
143
|
+
# Rope project settings
|
|
144
|
+
.ropeproject
|
|
145
|
+
|
|
146
|
+
# mkdocs documentation
|
|
147
|
+
/site
|
|
148
|
+
|
|
149
|
+
# mypy
|
|
150
|
+
.mypy_cache/
|
|
151
|
+
.dmypy.json
|
|
152
|
+
dmypy.json
|
|
153
|
+
|
|
154
|
+
# Pyre type checker
|
|
155
|
+
.pyre/
|
|
156
|
+
|
|
157
|
+
# pytype static type analyzer
|
|
158
|
+
.pytype/
|
|
159
|
+
|
|
160
|
+
# Cython debug symbols
|
|
161
|
+
cython_debug/
|
|
162
|
+
|
|
163
|
+
# PyCharm
|
|
164
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
165
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
166
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
167
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
168
|
+
#.idea/
|
|
169
|
+
|
|
170
|
+
# PyPI configuration file
|
|
171
|
+
.pypirc
|
|
172
|
+
pywebexec/version.py
|
pywebexec-0.0.3/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 joknarf
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
pywebexec-0.0.3/PKG-INFO
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: pywebexec
|
|
3
|
+
Version: 0.0.3
|
|
4
|
+
Summary: Simple Python HTTP Exec Server
|
|
5
|
+
Home-page: https://github.com/joknarf/pywebexec
|
|
6
|
+
Author: Franck Jouvanceau
|
|
7
|
+
Maintainer: Franck Jouvanceau
|
|
8
|
+
License: MIT License
|
|
9
|
+
|
|
10
|
+
Copyright (c) 2025 joknarf
|
|
11
|
+
|
|
12
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
in the Software without restriction, including without limitation the rights
|
|
15
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
furnished to do so, subject to the following conditions:
|
|
18
|
+
|
|
19
|
+
The above copyright notice and this permission notice shall be included in all
|
|
20
|
+
copies or substantial portions of the Software.
|
|
21
|
+
|
|
22
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
28
|
+
SOFTWARE.
|
|
29
|
+
|
|
30
|
+
Project-URL: Homepage, https://github.com/joknarf/pywebexec
|
|
31
|
+
Project-URL: Documentation, https://github.com/joknarf/pywebexec/blob/main/README.md
|
|
32
|
+
Project-URL: Repository, https://github.com/joknarf/pywebexec.git
|
|
33
|
+
Keywords: http,fileserver,browser,explorer
|
|
34
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
35
|
+
Classifier: Intended Audience :: System Administrators
|
|
36
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
37
|
+
Classifier: Operating System :: POSIX
|
|
38
|
+
Classifier: Operating System :: Unix
|
|
39
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
40
|
+
Classifier: Operating System :: MacOS
|
|
41
|
+
Classifier: Programming Language :: Python
|
|
42
|
+
Classifier: Programming Language :: Python :: 3
|
|
43
|
+
Classifier: Programming Language :: Python :: 3.6
|
|
44
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
45
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
46
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
47
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
48
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
49
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
50
|
+
Classifier: Topic :: System :: Clustering
|
|
51
|
+
Classifier: Topic :: System :: Networking
|
|
52
|
+
Classifier: Topic :: System :: Systems Administration
|
|
53
|
+
Requires-Python: >=3.6
|
|
54
|
+
Description-Content-Type: text/markdown
|
|
55
|
+
License-File: LICENSE
|
|
56
|
+
Requires-Dist: cryptography>=40.0.2
|
|
57
|
+
Requires-Dist: Flask>=3.0.3
|
|
58
|
+
Requires-Dist: Flask-HTTPAuth>=4.8.0
|
|
59
|
+
|
|
60
|
+
[](https://pypi.org/project/pywebexec/)
|
|
61
|
+

|
|
62
|
+
[](https://shields.io/)
|
|
63
|
+
[](https://pepy.tech/project/pywebexec)
|
|
64
|
+
[](https://shields.io/)
|
|
65
|
+
|
|
66
|
+
# pywebexec
|
|
67
|
+
Simple Python HTTP(S) API/Web Command Launcher
|
|
68
|
+
|
|
69
|
+
## Install
|
|
70
|
+
```
|
|
71
|
+
$ pip install pywebexec
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Quick start
|
|
75
|
+
|
|
76
|
+
* start http server serving current directory executables listening on 0.0.0.0 port 8080
|
|
77
|
+
```
|
|
78
|
+
$ pywebexec
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
* Launch commands with params/view live output/Status using browser `http://<yourserver>:8080`
|
|
82
|
+
|
|
83
|
+
## features
|
|
84
|
+
|
|
85
|
+
* Serve executables in current directory
|
|
86
|
+
* Launch commands with params from web browser
|
|
87
|
+
* Follow live output
|
|
88
|
+
* Stop command
|
|
89
|
+
* Relaunch command
|
|
90
|
+
* HTTPS support
|
|
91
|
+
* HTTPS self-signed certificate generator
|
|
92
|
+
* Can be started as a daemon (POSIX)
|
|
93
|
+
* uses gunicorn to serve http/https
|
|
94
|
+
|
|
95
|
+
## Customize server
|
|
96
|
+
```
|
|
97
|
+
$ pywebexec --listen 0.0.0.0 --port 8080
|
|
98
|
+
$ pywebexec -l 0.0.0.0 -p 8080
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Basic auth user/password
|
|
102
|
+
```
|
|
103
|
+
$ pywebexec --user myuser [--password mypass]
|
|
104
|
+
$ pywebfs -u myuser [-P mypass]
|
|
105
|
+
```
|
|
106
|
+
Generated password is given if no `--pasword` option
|
|
107
|
+
|
|
108
|
+
## HTTPS server
|
|
109
|
+
|
|
110
|
+
* Generate auto-signed certificate and start https server
|
|
111
|
+
```
|
|
112
|
+
$ pywebfs --gencert
|
|
113
|
+
$ pywebfs --g
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
* Start https server using existing certificate
|
|
117
|
+
```
|
|
118
|
+
$ pywebfs --cert /pathto/host.cert --key /pathto/host.key
|
|
119
|
+
$ pywebfs -c /pathto/host.cert -k /pathto/host.key
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Launch server as a daemon (Linux)
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
$ pywebexec start
|
|
126
|
+
$ pywebexec status
|
|
127
|
+
$ pywebexec stop
|
|
128
|
+
```
|
|
129
|
+
* log of server are stored in current directory `.web_status/pwexec_<listen>:<port>.log`
|
|
130
|
+
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
[](https://pypi.org/project/pywebexec/)
|
|
2
|
+

|
|
3
|
+
[](https://shields.io/)
|
|
4
|
+
[](https://pepy.tech/project/pywebexec)
|
|
5
|
+
[](https://shields.io/)
|
|
6
|
+
|
|
7
|
+
# pywebexec
|
|
8
|
+
Simple Python HTTP(S) API/Web Command Launcher
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
```
|
|
12
|
+
$ pip install pywebexec
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
* start http server serving current directory executables listening on 0.0.0.0 port 8080
|
|
18
|
+
```
|
|
19
|
+
$ pywebexec
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
* Launch commands with params/view live output/Status using browser `http://<yourserver>:8080`
|
|
23
|
+
|
|
24
|
+
## features
|
|
25
|
+
|
|
26
|
+
* Serve executables in current directory
|
|
27
|
+
* Launch commands with params from web browser
|
|
28
|
+
* Follow live output
|
|
29
|
+
* Stop command
|
|
30
|
+
* Relaunch command
|
|
31
|
+
* HTTPS support
|
|
32
|
+
* HTTPS self-signed certificate generator
|
|
33
|
+
* Can be started as a daemon (POSIX)
|
|
34
|
+
* uses gunicorn to serve http/https
|
|
35
|
+
|
|
36
|
+
## Customize server
|
|
37
|
+
```
|
|
38
|
+
$ pywebexec --listen 0.0.0.0 --port 8080
|
|
39
|
+
$ pywebexec -l 0.0.0.0 -p 8080
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Basic auth user/password
|
|
43
|
+
```
|
|
44
|
+
$ pywebexec --user myuser [--password mypass]
|
|
45
|
+
$ pywebfs -u myuser [-P mypass]
|
|
46
|
+
```
|
|
47
|
+
Generated password is given if no `--pasword` option
|
|
48
|
+
|
|
49
|
+
## HTTPS server
|
|
50
|
+
|
|
51
|
+
* Generate auto-signed certificate and start https server
|
|
52
|
+
```
|
|
53
|
+
$ pywebfs --gencert
|
|
54
|
+
$ pywebfs --g
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
* Start https server using existing certificate
|
|
58
|
+
```
|
|
59
|
+
$ pywebfs --cert /pathto/host.cert --key /pathto/host.key
|
|
60
|
+
$ pywebfs -c /pathto/host.cert -k /pathto/host.key
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Launch server as a daemon (Linux)
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
$ pywebexec start
|
|
67
|
+
$ pywebexec status
|
|
68
|
+
$ pywebexec stop
|
|
69
|
+
```
|
|
70
|
+
* log of server are stored in current directory `.web_status/pwexec_<listen>:<port>.log`
|
|
71
|
+
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "pywebexec"
|
|
3
|
+
authors = [
|
|
4
|
+
{ name = "Franck Jouvanceau" },
|
|
5
|
+
]
|
|
6
|
+
maintainers = [
|
|
7
|
+
{ name = "Franck Jouvanceau" },
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
description = "Simple Python HTTP Exec Server"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"cryptography>=40.0.2",
|
|
13
|
+
"Flask>=3.0.3",
|
|
14
|
+
"Flask-HTTPAuth>=4.8.0",
|
|
15
|
+
]
|
|
16
|
+
dynamic=["version"]
|
|
17
|
+
readme = "README.md"
|
|
18
|
+
license = {file = "LICENSE"}
|
|
19
|
+
requires-python = ">= 3.6"
|
|
20
|
+
keywords = ["http", "fileserver", "browser", "explorer"]
|
|
21
|
+
classifiers = [
|
|
22
|
+
"Development Status :: 5 - Production/Stable",
|
|
23
|
+
"Intended Audience :: System Administrators",
|
|
24
|
+
"License :: OSI Approved :: MIT License",
|
|
25
|
+
"Operating System :: POSIX",
|
|
26
|
+
"Operating System :: Unix",
|
|
27
|
+
"Operating System :: Microsoft :: Windows ",
|
|
28
|
+
"Operating System :: MacOS",
|
|
29
|
+
"Programming Language :: Python",
|
|
30
|
+
"Programming Language :: Python :: 3",
|
|
31
|
+
"Programming Language :: Python :: 3.6",
|
|
32
|
+
"Programming Language :: Python :: 3.7",
|
|
33
|
+
"Programming Language :: Python :: 3.8",
|
|
34
|
+
"Programming Language :: Python :: 3.9",
|
|
35
|
+
"Programming Language :: Python :: 3.10",
|
|
36
|
+
"Programming Language :: Python :: 3.11",
|
|
37
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
38
|
+
"Topic :: System :: Clustering",
|
|
39
|
+
"Topic :: System :: Networking",
|
|
40
|
+
"Topic :: System :: Systems Administration",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[project.urls]
|
|
44
|
+
Homepage = "https://github.com/joknarf/pywebexec"
|
|
45
|
+
Documentation = "https://github.com/joknarf/pywebexec/blob/main/README.md"
|
|
46
|
+
Repository = "https://github.com/joknarf/pywebexec.git"
|
|
47
|
+
|
|
48
|
+
[build-system]
|
|
49
|
+
requires = ["setuptools >= 61.0", "setuptools_scm[toml]>=6.2"]
|
|
50
|
+
build-backend = "setuptools.build_meta"
|
|
51
|
+
|
|
52
|
+
[tool.setuptools_scm]
|
|
53
|
+
version_file = "pywebexec/version.py"
|
|
54
|
+
|
|
55
|
+
[project.scripts]
|
|
56
|
+
pywebexec = "pywebexec.pywebexec:start_gunicorn"
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
from flask import Flask, request, jsonify, render_template
|
|
2
|
+
from flask_httpauth import HTTPBasicAuth
|
|
3
|
+
import subprocess
|
|
4
|
+
import threading
|
|
5
|
+
import os
|
|
6
|
+
import json
|
|
7
|
+
import uuid
|
|
8
|
+
import argparse
|
|
9
|
+
import random
|
|
10
|
+
import string
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
import shlex
|
|
13
|
+
from gunicorn.app.base import BaseApplication
|
|
14
|
+
|
|
15
|
+
app = Flask(__name__)
|
|
16
|
+
auth = HTTPBasicAuth()
|
|
17
|
+
|
|
18
|
+
# Directory to store the script status and output
|
|
19
|
+
SCRIPT_STATUS_DIR = 'script_status'
|
|
20
|
+
|
|
21
|
+
if not os.path.exists(SCRIPT_STATUS_DIR):
|
|
22
|
+
os.makedirs(SCRIPT_STATUS_DIR)
|
|
23
|
+
|
|
24
|
+
def generate_random_password(length=12):
|
|
25
|
+
characters = string.ascii_letters + string.digits + string.punctuation
|
|
26
|
+
return ''.join(random.choice(characters) for i in range(length))
|
|
27
|
+
|
|
28
|
+
class StandaloneApplication(BaseApplication):
|
|
29
|
+
|
|
30
|
+
def __init__(self, app, options=None):
|
|
31
|
+
self.options = options or {}
|
|
32
|
+
self.application = app
|
|
33
|
+
super().__init__()
|
|
34
|
+
|
|
35
|
+
def load_config(self):
|
|
36
|
+
config = {
|
|
37
|
+
key: value for key, value in self.options.items()
|
|
38
|
+
if key in self.cfg.settings and value is not None
|
|
39
|
+
}
|
|
40
|
+
for key, value in config.items():
|
|
41
|
+
self.cfg.set(key.lower(), value)
|
|
42
|
+
|
|
43
|
+
def load(self):
|
|
44
|
+
return self.application
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def start_gunicorn():
|
|
48
|
+
options = {
|
|
49
|
+
'bind': '%s:%s' % (args.listen, args.port),
|
|
50
|
+
'workers': 4,
|
|
51
|
+
'certfile': args.cert,
|
|
52
|
+
'keyfile': args.key,
|
|
53
|
+
}
|
|
54
|
+
StandaloneApplication(app, options=options).run()
|
|
55
|
+
|
|
56
|
+
def parseargs():
|
|
57
|
+
global app, args
|
|
58
|
+
parser = argparse.ArgumentParser(description='Run the script execution server.')
|
|
59
|
+
parser.add_argument('--user', help='Username for basic auth')
|
|
60
|
+
parser.add_argument('--password', help='Password for basic auth')
|
|
61
|
+
parser.add_argument(
|
|
62
|
+
"-l", "--listen", type=str, default="0.0.0.0", help="HTTP server listen address"
|
|
63
|
+
)
|
|
64
|
+
parser.add_argument(
|
|
65
|
+
"-p", "--port", type=int, default=8080, help="HTTP server listen port"
|
|
66
|
+
)
|
|
67
|
+
parser.add_argument(
|
|
68
|
+
"-d", "--dir", type=str, default=os.getcwd(), help="Serve target directory"
|
|
69
|
+
)
|
|
70
|
+
parser.add_argument(
|
|
71
|
+
"-t",
|
|
72
|
+
"--title",
|
|
73
|
+
type=str,
|
|
74
|
+
default="FileBrowser",
|
|
75
|
+
help="Web html title",
|
|
76
|
+
)
|
|
77
|
+
parser.add_argument("-c", "--cert", type=str, help="Path to https certificate")
|
|
78
|
+
parser.add_argument("-k", "--key", type=str, help="Path to https certificate key")
|
|
79
|
+
|
|
80
|
+
args = parser.parse_args()
|
|
81
|
+
|
|
82
|
+
if args.user:
|
|
83
|
+
app.config['USER'] = args.user
|
|
84
|
+
if args.password:
|
|
85
|
+
app.config['PASSWORD'] = args.password
|
|
86
|
+
else:
|
|
87
|
+
app.config['PASSWORD'] = generate_random_password()
|
|
88
|
+
print(f'Generated password for user {args.user}: {app.config["PASSWORD"]}')
|
|
89
|
+
else:
|
|
90
|
+
app.config['USER'] = None
|
|
91
|
+
app.config['PASSWORD'] = None
|
|
92
|
+
return args
|
|
93
|
+
|
|
94
|
+
parseargs()
|
|
95
|
+
|
|
96
|
+
def get_status_file_path(script_id):
|
|
97
|
+
return os.path.join(SCRIPT_STATUS_DIR, f'{script_id}.json')
|
|
98
|
+
|
|
99
|
+
def get_output_file_path(script_id):
|
|
100
|
+
return os.path.join(SCRIPT_STATUS_DIR, f'{script_id}_output.txt')
|
|
101
|
+
|
|
102
|
+
def update_script_status(script_id, status, script_name=None, params=None, start_time=None, end_time=None, exit_code=None):
|
|
103
|
+
status_file_path = get_status_file_path(script_id)
|
|
104
|
+
status_data = read_script_status(script_id) or {}
|
|
105
|
+
status_data['status'] = status
|
|
106
|
+
if script_name is not None:
|
|
107
|
+
status_data['script_name'] = script_name
|
|
108
|
+
if params is not None:
|
|
109
|
+
status_data['params'] = params
|
|
110
|
+
if start_time is not None:
|
|
111
|
+
status_data['start_time'] = start_time
|
|
112
|
+
if end_time is not None:
|
|
113
|
+
status_data['end_time'] = end_time
|
|
114
|
+
if exit_code is not None:
|
|
115
|
+
status_data['exit_code'] = exit_code
|
|
116
|
+
with open(status_file_path, 'w') as f:
|
|
117
|
+
json.dump(status_data, f)
|
|
118
|
+
|
|
119
|
+
def read_script_status(script_id):
|
|
120
|
+
status_file_path = get_status_file_path(script_id)
|
|
121
|
+
if not os.path.exists(status_file_path):
|
|
122
|
+
return None
|
|
123
|
+
with open(status_file_path, 'r') as f:
|
|
124
|
+
return json.load(f)
|
|
125
|
+
|
|
126
|
+
# Dictionary to store the process objects
|
|
127
|
+
processes = {}
|
|
128
|
+
|
|
129
|
+
def run_script(script_name, params, script_id):
|
|
130
|
+
start_time = datetime.now().isoformat()
|
|
131
|
+
update_script_status(script_id, 'running', script_name=script_name, params=params, start_time=start_time)
|
|
132
|
+
try:
|
|
133
|
+
output_file_path = get_output_file_path(script_id)
|
|
134
|
+
with open(output_file_path, 'w') as output_file:
|
|
135
|
+
# Run the script with parameters and redirect stdout and stderr to the file
|
|
136
|
+
process = subprocess.Popen([script_name] + params, stdout=output_file, stderr=output_file, text=True)
|
|
137
|
+
processes[script_id] = process
|
|
138
|
+
process.wait()
|
|
139
|
+
processes.pop(script_id, None)
|
|
140
|
+
|
|
141
|
+
end_time = datetime.now().isoformat()
|
|
142
|
+
# Update the status based on the result
|
|
143
|
+
if process.returncode == 0:
|
|
144
|
+
update_script_status(script_id, 'success', end_time=end_time, exit_code=process.returncode)
|
|
145
|
+
elif process.returncode == -15:
|
|
146
|
+
update_script_status(script_id, 'aborted', end_time=end_time, exit_code=process.returncode)
|
|
147
|
+
else:
|
|
148
|
+
update_script_status(script_id, 'failed', end_time=end_time, exit_code=process.returncode)
|
|
149
|
+
except Exception as e:
|
|
150
|
+
end_time = datetime.now().isoformat()
|
|
151
|
+
update_script_status(script_id, 'failed', end_time=end_time, exit_code=1)
|
|
152
|
+
with open(get_output_file_path(script_id), 'a') as output_file:
|
|
153
|
+
output_file.write(str(e))
|
|
154
|
+
|
|
155
|
+
def auth_required(f):
|
|
156
|
+
if app.config.get('USER'):
|
|
157
|
+
return auth.login_required(f)
|
|
158
|
+
return f
|
|
159
|
+
|
|
160
|
+
@app.route('/run_script', methods=['POST'])
|
|
161
|
+
@auth_required
|
|
162
|
+
def run_script_endpoint():
|
|
163
|
+
data = request.json
|
|
164
|
+
script_name = data.get('script_name')
|
|
165
|
+
params = data.get('params', [])
|
|
166
|
+
|
|
167
|
+
if not script_name:
|
|
168
|
+
return jsonify({'error': 'script_name is required'}), 400
|
|
169
|
+
|
|
170
|
+
# Ensure the script is an executable in the current directory
|
|
171
|
+
script_path = os.path.join(".", os.path.basename(script_name))
|
|
172
|
+
if not os.path.isfile(script_path) or not os.access(script_path, os.X_OK):
|
|
173
|
+
return jsonify({'error': 'script_name must be an executable in the current directory'}), 400
|
|
174
|
+
|
|
175
|
+
# Split params using shell-like syntax
|
|
176
|
+
try:
|
|
177
|
+
params = shlex.split(' '.join(params))
|
|
178
|
+
except ValueError as e:
|
|
179
|
+
return jsonify({'error': str(e)}), 400
|
|
180
|
+
|
|
181
|
+
# Generate a unique script_id
|
|
182
|
+
script_id = str(uuid.uuid4())
|
|
183
|
+
|
|
184
|
+
# Set the initial status to running and save script details
|
|
185
|
+
update_script_status(script_id, 'running', script_name, params)
|
|
186
|
+
|
|
187
|
+
# Run the script in a separate thread
|
|
188
|
+
thread = threading.Thread(target=run_script, args=(script_path, params, script_id))
|
|
189
|
+
thread.start()
|
|
190
|
+
|
|
191
|
+
return jsonify({'message': 'Script is running', 'script_id': script_id})
|
|
192
|
+
|
|
193
|
+
@app.route('/stop_script/<script_id>', methods=['POST'])
|
|
194
|
+
@auth_required
|
|
195
|
+
def stop_script(script_id):
|
|
196
|
+
process = processes.get(script_id)
|
|
197
|
+
end_time = datetime.now().isoformat()
|
|
198
|
+
if process and process.poll() is None:
|
|
199
|
+
try:
|
|
200
|
+
process.terminate()
|
|
201
|
+
process.wait() # Ensure the process has terminated
|
|
202
|
+
return jsonify({'message': 'Script aborted'})
|
|
203
|
+
except Exception as e:
|
|
204
|
+
status_data = read_script_status(script_id) or {}
|
|
205
|
+
status_data['status'] = 'failed'
|
|
206
|
+
status_data['end_time'] = end_time
|
|
207
|
+
status_data['exit_code'] = 1
|
|
208
|
+
with open(get_status_file_path(script_id), 'w') as f:
|
|
209
|
+
json.dump(status_data, f)
|
|
210
|
+
with open(get_output_file_path(script_id), 'a') as output_file:
|
|
211
|
+
output_file.write(str(e))
|
|
212
|
+
return jsonify({'error': 'Failed to terminate script'}), 500
|
|
213
|
+
update_script_status(script_id, 'failed', end_time=end_time, exit_code=1)
|
|
214
|
+
return jsonify({'error': 'Invalid script_id or script not running'}), 400
|
|
215
|
+
|
|
216
|
+
@app.route('/script_status/<script_id>', methods=['GET'])
|
|
217
|
+
@auth_required
|
|
218
|
+
def get_script_status(script_id):
|
|
219
|
+
status = read_script_status(script_id)
|
|
220
|
+
if not status:
|
|
221
|
+
return jsonify({'error': 'Invalid script_id'}), 404
|
|
222
|
+
|
|
223
|
+
output_file_path = get_output_file_path(script_id)
|
|
224
|
+
if os.path.exists(output_file_path):
|
|
225
|
+
with open(output_file_path, 'r') as output_file:
|
|
226
|
+
output = output_file.read()
|
|
227
|
+
status['output'] = output
|
|
228
|
+
|
|
229
|
+
return jsonify(status)
|
|
230
|
+
|
|
231
|
+
@app.route('/')
|
|
232
|
+
@auth_required
|
|
233
|
+
def index():
|
|
234
|
+
return render_template('index.html')
|
|
235
|
+
|
|
236
|
+
@app.route('/scripts', methods=['GET'])
|
|
237
|
+
@auth_required
|
|
238
|
+
def list_scripts():
|
|
239
|
+
scripts = []
|
|
240
|
+
for filename in os.listdir(SCRIPT_STATUS_DIR):
|
|
241
|
+
if filename.endswith('.json'):
|
|
242
|
+
script_id = filename[:-5]
|
|
243
|
+
status = read_script_status(script_id)
|
|
244
|
+
if status:
|
|
245
|
+
command = status['script_name'] + ' ' + shlex.join(status['params'])
|
|
246
|
+
scripts.append({
|
|
247
|
+
'script_id': script_id,
|
|
248
|
+
'status': status['status'],
|
|
249
|
+
'start_time': status.get('start_time', 'N/A'),
|
|
250
|
+
'end_time': status.get('end_time', 'N/A'),
|
|
251
|
+
'command': command,
|
|
252
|
+
'exit_code': status.get('exit_code', 'N/A')
|
|
253
|
+
})
|
|
254
|
+
# Sort scripts by start_time in descending order
|
|
255
|
+
scripts.sort(key=lambda x: x['start_time'], reverse=True)
|
|
256
|
+
return jsonify(scripts)
|
|
257
|
+
|
|
258
|
+
@app.route('/script_output/<script_id>', methods=['GET'])
|
|
259
|
+
@auth_required
|
|
260
|
+
def get_script_output(script_id):
|
|
261
|
+
output_file_path = get_output_file_path(script_id)
|
|
262
|
+
if os.path.exists(output_file_path):
|
|
263
|
+
with open(output_file_path, 'r') as output_file:
|
|
264
|
+
output = output_file.read()
|
|
265
|
+
status_data = read_script_status(script_id) or {}
|
|
266
|
+
return jsonify({'output': output, 'status': status_data.get("status")})
|
|
267
|
+
return jsonify({'error': 'Invalid script_id'}), 404
|
|
268
|
+
|
|
269
|
+
@app.route('/executables', methods=['GET'])
|
|
270
|
+
@auth_required
|
|
271
|
+
def list_executables():
|
|
272
|
+
executables = [f for f in os.listdir('.') if os.path.isfile(f) and os.access(f, os.X_OK)]
|
|
273
|
+
return jsonify(executables)
|
|
274
|
+
|
|
275
|
+
@auth.verify_password
|
|
276
|
+
def verify_password(username, password):
|
|
277
|
+
return username == app.config['USER'] and password == app.config['PASSWORD']
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
if __name__ == '__main__':
|
|
283
|
+
start_gunicorn()
|
|
284
|
+
#app.run(host='0.0.0.0', port=5000)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg viewBox="-1 -1 13 13" xmlns="http://www.w3.org/2000/svg" fill="#000000"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"><path fill-rule="evenodd" clip-rule="evenodd" d="M6 12A6 6 0 106 0a6 6 0 000 12zM3 5a1 1 0 000 2h6a1 1 0 100-2H3z" fill="#ff641a"></path></g></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" role="img" viewBox="0 0 16 16" width="25" height="16" fill="currentColor"><path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"></path><path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"></path></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" role="img" viewBox="0 0 16 16" width="25" height="16" fill="currentColor" style="display: inline-block; user-select: none; vertical-align: text-bottom; overflow: visible;"><path fill="#118811" d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z"></path></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg viewBox="-2 -2 34 34" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns" fill="#000000"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"><title>cross-circle</title><desc>Created with Sketch Beta.</desc><defs></defs><g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage"><g id="Icon-Set-Filled" sketch:type="MSLayerGroup" transform="translate(-570.000000, -1089.000000)" fill="#ca0000"><path d="M591.657,1109.24 C592.048,1109.63 592.048,1110.27 591.657,1110.66 C591.267,1111.05 590.633,1111.05 590.242,1110.66 L586.006,1106.42 L581.74,1110.69 C581.346,1111.08 580.708,1111.08 580.314,1110.69 C579.921,1110.29 579.921,1109.65 580.314,1109.26 L584.58,1104.99 L580.344,1100.76 C579.953,1100.37 579.953,1099.73 580.344,1099.34 C580.733,1098.95 581.367,1098.95 581.758,1099.34 L585.994,1103.58 L590.292,1099.28 C590.686,1098.89 591.323,1098.89 591.717,1099.28 C592.11,1099.68 592.11,1100.31 591.717,1100.71 L587.42,1105.01 L591.657,1109.24 L591.657,1109.24 Z M586,1089 C577.163,1089 570,1096.16 570,1105 C570,1113.84 577.163,1121 586,1121 C594.837,1121 602,1113.84 602,1105 C602,1096.16 594.837,1089 586,1089 L586,1089 Z" id="cross-circle" sketch:type="MSShapeGroup"></path></g></g></g></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg viewBox="0 0 1024 1024" class="icon" version="1.1" xmlns="http://www.w3.org/2000/svg" fill="#000000"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"><path d="M661.333333 170.666667l253.866667 34.133333-209.066667 209.066667zM362.666667 853.333333L108.8 819.2l209.066667-209.066667zM170.666667 362.666667L204.8 108.8l209.066667 209.066667z" fill="#4949d1"></path><path d="M198.4 452.266667l-89.6 17.066666c-2.133333 14.933333-2.133333 27.733333-2.133333 42.666667 0 98.133333 34.133333 192 98.133333 264.533333l64-55.466666C219.733333 663.466667 192 588.8 192 512c0-19.2 2.133333-40.533333 6.4-59.733333zM512 106.666667c-115.2 0-217.6 49.066667-292.266667 125.866666l59.733334 59.733334C339.2 230.4 420.266667 192 512 192c19.2 0 40.533333 2.133333 59.733333 6.4l14.933334-83.2C563.2 108.8 537.6 106.666667 512 106.666667zM825.6 571.733333l89.6-17.066666c2.133333-14.933333 2.133333-27.733333 2.133333-42.666667 0-93.866667-32-185.6-91.733333-258.133333l-66.133333 53.333333c46.933333 57.6 72.533333 130.133333 72.533333 202.666667 0 21.333333-2.133333 42.666667-6.4 61.866666zM744.533333 731.733333C684.8 793.6 603.733333 832 512 832c-19.2 0-40.533333-2.133333-59.733333-6.4l-14.933334 83.2c25.6 4.266667 51.2 6.4 74.666667 6.4 115.2 0 217.6-49.066667 292.266667-125.866667l-59.733334-57.6z" fill="#4949d1"></path><path d="M853.333333 661.333333l-34.133333 253.866667-209.066667-209.066667z" fill="#4949d1"></path></g></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg viewBox="0 75 949 949" xmlns="http://www.w3.org/2000/svg" fill="#000000"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"><path fill="#00a600" d="M512 64a448 448 0 1 1 0 896 448 448 0 0 1 0-896zm-55.808 536.384-99.52-99.584a38.4 38.4 0 1 0-54.336 54.336l126.72 126.72a38.272 38.272 0 0 0 54.336 0l262.4-262.464a38.4 38.4 0 1 0-54.272-54.336L456.192 600.384z"></path></g></svg>
|
|
File without changes
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<title>pywebexec</title>
|
|
6
|
+
<style>
|
|
7
|
+
body { font-family: Arial, sans-serif; }
|
|
8
|
+
.table-container { max-height: 385px; overflow-y: auto; }
|
|
9
|
+
table { width: 100%; border-collapse: collapse; }
|
|
10
|
+
th, td { padding: 8px; text-align: left; border-bottom: 1px solid #ddd; }
|
|
11
|
+
th { background-color: #f2f2f2; position: sticky; top: 0; z-index: 1; }
|
|
12
|
+
.output {
|
|
13
|
+
white-space: pre-wrap;
|
|
14
|
+
background: #f0f0f0;
|
|
15
|
+
padding: 10px;
|
|
16
|
+
border: 1px solid #ccc;
|
|
17
|
+
font-family: monospace;
|
|
18
|
+
border-radius: 15px;
|
|
19
|
+
}
|
|
20
|
+
.copy-icon { cursor: pointer; }
|
|
21
|
+
.monospace { font-family: monospace; }
|
|
22
|
+
.copied { color: green; margin-left: 5px; }
|
|
23
|
+
button {
|
|
24
|
+
-webkit-appearance: none;
|
|
25
|
+
-webkit-border-radius: none;
|
|
26
|
+
appearance: none;
|
|
27
|
+
border-radius: 15px;
|
|
28
|
+
padding: 3px;
|
|
29
|
+
padding-right: 13px;
|
|
30
|
+
border: 1px #555 solid;
|
|
31
|
+
height: 22px;
|
|
32
|
+
font-size: 13px;
|
|
33
|
+
outline: none;
|
|
34
|
+
text-indent: 10px;
|
|
35
|
+
background-color: #eee;
|
|
36
|
+
display: inline-block;
|
|
37
|
+
vertical-align: middle;
|
|
38
|
+
}
|
|
39
|
+
form {
|
|
40
|
+
padding-bottom: 15px;
|
|
41
|
+
}
|
|
42
|
+
.status-icon {
|
|
43
|
+
display: inline-block;
|
|
44
|
+
width: 16px;
|
|
45
|
+
height: 16px;
|
|
46
|
+
margin-right: 5px;
|
|
47
|
+
background-size: contain;
|
|
48
|
+
background-repeat: no-repeat;
|
|
49
|
+
vertical-align: middle;
|
|
50
|
+
}
|
|
51
|
+
.status-running {
|
|
52
|
+
background-image: url("/static/images/running.svg")
|
|
53
|
+
}
|
|
54
|
+
.status-success {
|
|
55
|
+
background-image: url("/static/images/success.svg")
|
|
56
|
+
}
|
|
57
|
+
.status-failed {
|
|
58
|
+
background-image: url("/static/images/failed.svg")
|
|
59
|
+
}
|
|
60
|
+
.status-aborted {
|
|
61
|
+
background-image: url("/static/images/aborted.svg")
|
|
62
|
+
}
|
|
63
|
+
.copy_clip {
|
|
64
|
+
padding-right: 25px;
|
|
65
|
+
background-repeat: no-repeat;
|
|
66
|
+
background-position: right top;
|
|
67
|
+
background-size: 25px 16px;
|
|
68
|
+
white-space: nowrap;
|
|
69
|
+
}
|
|
70
|
+
.copy_clip_left {
|
|
71
|
+
padding-left: 25px;
|
|
72
|
+
padding-right: 0px;
|
|
73
|
+
background-position: left top;
|
|
74
|
+
}
|
|
75
|
+
.copy_clip:hover {
|
|
76
|
+
cursor: pointer;
|
|
77
|
+
background-image: url("/static/images/copy.svg");
|
|
78
|
+
}
|
|
79
|
+
.copy_clip_ok, .copy_clip_ok:hover {
|
|
80
|
+
background-image: url("/static/images/copy_ok.svg");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
</style>
|
|
84
|
+
</head>
|
|
85
|
+
<body>
|
|
86
|
+
<h1>pywebexec</h1>
|
|
87
|
+
<form id="launchForm">
|
|
88
|
+
<label for="scriptName">Command:</label>
|
|
89
|
+
<select id="scriptName" name="scriptName"></select>
|
|
90
|
+
<label for="params">Params:</label>
|
|
91
|
+
<input type="text" id="params" name="params">
|
|
92
|
+
<button type="submit">Launch</button>
|
|
93
|
+
</form>
|
|
94
|
+
<div class="table-container">
|
|
95
|
+
<table>
|
|
96
|
+
<thead>
|
|
97
|
+
<tr>
|
|
98
|
+
<th>Script ID</th>
|
|
99
|
+
<th>Status</th>
|
|
100
|
+
<th>Start Time</th>
|
|
101
|
+
<th>Duration</th>
|
|
102
|
+
<th>Exit</th>
|
|
103
|
+
<th>Command</th>
|
|
104
|
+
<th>Actions</th>
|
|
105
|
+
</tr>
|
|
106
|
+
</thead>
|
|
107
|
+
<tbody id="scripts"></tbody>
|
|
108
|
+
</table>
|
|
109
|
+
</div>
|
|
110
|
+
<div id="output" class="output"></div>
|
|
111
|
+
|
|
112
|
+
<script>
|
|
113
|
+
let currentScriptId = null;
|
|
114
|
+
let outputInterval = null;
|
|
115
|
+
|
|
116
|
+
document.getElementById('launchForm').addEventListener('submit', async (event) => {
|
|
117
|
+
event.preventDefault();
|
|
118
|
+
const scriptName = document.getElementById('scriptName').value;
|
|
119
|
+
const params = document.getElementById('params').value.split(' ');
|
|
120
|
+
const response = await fetch('/run_script', {
|
|
121
|
+
method: 'POST',
|
|
122
|
+
headers: {
|
|
123
|
+
'Content-Type': 'application/json'
|
|
124
|
+
},
|
|
125
|
+
body: JSON.stringify({ script_name: scriptName, params: params })
|
|
126
|
+
});
|
|
127
|
+
const data = await response.json();
|
|
128
|
+
fetchScripts();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
async function fetchScripts() {
|
|
132
|
+
const response = await fetch('/scripts');
|
|
133
|
+
const scripts = await response.json();
|
|
134
|
+
scripts.sort((a, b) => new Date(b.start_time) - new Date(a.start_time));
|
|
135
|
+
const scriptsTbody = document.getElementById('scripts');
|
|
136
|
+
scriptsTbody.innerHTML = '';
|
|
137
|
+
scripts.forEach(script => {
|
|
138
|
+
const scriptRow = document.createElement('tr');
|
|
139
|
+
scriptRow.innerHTML = `
|
|
140
|
+
<td class="monospace">
|
|
141
|
+
<span class="copy_clip" onclick="copyToClipboard('${script.script_id}', this)">${script.script_id.slice(0, 8)}</span>
|
|
142
|
+
</td>
|
|
143
|
+
<td><span class="status-icon status-${script.status}"></span>${script.status}</td>
|
|
144
|
+
<td>${formatTime(script.start_time)}</td>
|
|
145
|
+
<td>${script.status === 'running' ? formatDuration(script.start_time, new Date().toISOString()) : formatDuration(script.start_time, script.end_time)}</td>
|
|
146
|
+
<td>${script.exit_code}</td>
|
|
147
|
+
<td>${script.command.replace(/^\.\//, '')}</td>
|
|
148
|
+
<td>
|
|
149
|
+
<button onclick="viewOutput('${script.script_id}')">Log</button>
|
|
150
|
+
<button onclick="relaunchScript('${script.script_id}')">Relaunch</button>
|
|
151
|
+
${script.status === 'running' ? `<button onclick="stopScript('${script.script_id}')">Stop</button>` : ''}
|
|
152
|
+
</td>
|
|
153
|
+
`;
|
|
154
|
+
scriptsTbody.appendChild(scriptRow);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function fetchExecutables() {
|
|
159
|
+
const response = await fetch('/executables');
|
|
160
|
+
const executables = await response.json();
|
|
161
|
+
const scriptNameSelect = document.getElementById('scriptName');
|
|
162
|
+
scriptNameSelect.innerHTML = '';
|
|
163
|
+
executables.forEach(executable => {
|
|
164
|
+
const option = document.createElement('option');
|
|
165
|
+
option.value = executable;
|
|
166
|
+
option.textContent = executable;
|
|
167
|
+
scriptNameSelect.appendChild(option);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function fetchOutput(script_id) {
|
|
172
|
+
const outputDiv = document.getElementById('output');
|
|
173
|
+
const response = await fetch(`/script_output/${script_id}`);
|
|
174
|
+
const data = await response.json();
|
|
175
|
+
if (data.error) {
|
|
176
|
+
outputDiv.innerHTML = data.error;
|
|
177
|
+
clearInterval(outputInterval);
|
|
178
|
+
} else {
|
|
179
|
+
outputDiv.innerHTML = data.output;
|
|
180
|
+
if (data.status != 'running') {
|
|
181
|
+
clearInterval(outputInterval)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function viewOutput(script_id) {
|
|
187
|
+
currentScriptId = script_id;
|
|
188
|
+
clearInterval(outputInterval);
|
|
189
|
+
const response = await fetch(`/script_status/${script_id}`);
|
|
190
|
+
const data = await response.json();
|
|
191
|
+
if (data.status === 'running') {
|
|
192
|
+
fetchOutput(script_id);
|
|
193
|
+
outputInterval = setInterval(() => fetchOutput(script_id), 1000);
|
|
194
|
+
} else {
|
|
195
|
+
fetchOutput(script_id);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function relaunchScript(script_id) {
|
|
200
|
+
const response = await fetch(`/script_status/${script_id}`);
|
|
201
|
+
const data = await response.json();
|
|
202
|
+
if (data.error) {
|
|
203
|
+
alert(data.error);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const relaunchResponse = await fetch('/run_script', {
|
|
207
|
+
method: 'POST',
|
|
208
|
+
headers: {
|
|
209
|
+
'Content-Type': 'application/json'
|
|
210
|
+
},
|
|
211
|
+
body: JSON.stringify({
|
|
212
|
+
script_name: data.script_name,
|
|
213
|
+
params: data.params
|
|
214
|
+
})
|
|
215
|
+
});
|
|
216
|
+
const relaunchData = await relaunchResponse.json();
|
|
217
|
+
alert(relaunchData.message);
|
|
218
|
+
fetchScripts();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function stopScript(script_id) {
|
|
222
|
+
const response = await fetch(`/stop_script/${script_id}`, {
|
|
223
|
+
method: 'POST'
|
|
224
|
+
});
|
|
225
|
+
const data = await response.json();
|
|
226
|
+
if (data.error) {
|
|
227
|
+
alert(data.error);
|
|
228
|
+
} else {
|
|
229
|
+
alert(data.message);
|
|
230
|
+
fetchScripts();
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function formatTime(time) {
|
|
235
|
+
if (!time || time === 'N/A') return 'N/A';
|
|
236
|
+
const date = new Date(time);
|
|
237
|
+
return date.toISOString().slice(0, 16).replace('T', ' ');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function formatDuration(startTime, endTime) {
|
|
241
|
+
if (!startTime || !endTime) return 'N/A';
|
|
242
|
+
const start = new Date(startTime);
|
|
243
|
+
const end = new Date(endTime);
|
|
244
|
+
const duration = (end - start) / 1000;
|
|
245
|
+
const hours = Math.floor(duration / 3600);
|
|
246
|
+
const minutes = Math.floor((duration % 3600) / 60);
|
|
247
|
+
const seconds = Math.floor(duration % 60);
|
|
248
|
+
return `${hours}h ${minutes}m ${seconds}s`;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function copyToClipboard(text, element) {
|
|
252
|
+
navigator.clipboard.writeText(text).then(() => {
|
|
253
|
+
element.classList.add('copy_clip_ok')
|
|
254
|
+
setTimeout(() => {
|
|
255
|
+
element.classList.remove('copy_clip_ok');
|
|
256
|
+
}, 2000);
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
fetchScripts();
|
|
261
|
+
fetchExecutables();
|
|
262
|
+
setInterval(fetchScripts, 5000);
|
|
263
|
+
</script>
|
|
264
|
+
</body>
|
|
265
|
+
</html>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# file generated by setuptools_scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
TYPE_CHECKING = False
|
|
4
|
+
if TYPE_CHECKING:
|
|
5
|
+
from typing import Tuple, Union
|
|
6
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
7
|
+
else:
|
|
8
|
+
VERSION_TUPLE = object
|
|
9
|
+
|
|
10
|
+
version: str
|
|
11
|
+
__version__: str
|
|
12
|
+
__version_tuple__: VERSION_TUPLE
|
|
13
|
+
version_tuple: VERSION_TUPLE
|
|
14
|
+
|
|
15
|
+
__version__ = version = '0.0.3'
|
|
16
|
+
__version_tuple__ = version_tuple = (0, 0, 3)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: pywebexec
|
|
3
|
+
Version: 0.0.3
|
|
4
|
+
Summary: Simple Python HTTP Exec Server
|
|
5
|
+
Home-page: https://github.com/joknarf/pywebexec
|
|
6
|
+
Author: Franck Jouvanceau
|
|
7
|
+
Maintainer: Franck Jouvanceau
|
|
8
|
+
License: MIT License
|
|
9
|
+
|
|
10
|
+
Copyright (c) 2025 joknarf
|
|
11
|
+
|
|
12
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
in the Software without restriction, including without limitation the rights
|
|
15
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
furnished to do so, subject to the following conditions:
|
|
18
|
+
|
|
19
|
+
The above copyright notice and this permission notice shall be included in all
|
|
20
|
+
copies or substantial portions of the Software.
|
|
21
|
+
|
|
22
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
28
|
+
SOFTWARE.
|
|
29
|
+
|
|
30
|
+
Project-URL: Homepage, https://github.com/joknarf/pywebexec
|
|
31
|
+
Project-URL: Documentation, https://github.com/joknarf/pywebexec/blob/main/README.md
|
|
32
|
+
Project-URL: Repository, https://github.com/joknarf/pywebexec.git
|
|
33
|
+
Keywords: http,fileserver,browser,explorer
|
|
34
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
35
|
+
Classifier: Intended Audience :: System Administrators
|
|
36
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
37
|
+
Classifier: Operating System :: POSIX
|
|
38
|
+
Classifier: Operating System :: Unix
|
|
39
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
40
|
+
Classifier: Operating System :: MacOS
|
|
41
|
+
Classifier: Programming Language :: Python
|
|
42
|
+
Classifier: Programming Language :: Python :: 3
|
|
43
|
+
Classifier: Programming Language :: Python :: 3.6
|
|
44
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
45
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
46
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
47
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
48
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
49
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
50
|
+
Classifier: Topic :: System :: Clustering
|
|
51
|
+
Classifier: Topic :: System :: Networking
|
|
52
|
+
Classifier: Topic :: System :: Systems Administration
|
|
53
|
+
Requires-Python: >=3.6
|
|
54
|
+
Description-Content-Type: text/markdown
|
|
55
|
+
License-File: LICENSE
|
|
56
|
+
Requires-Dist: cryptography>=40.0.2
|
|
57
|
+
Requires-Dist: Flask>=3.0.3
|
|
58
|
+
Requires-Dist: Flask-HTTPAuth>=4.8.0
|
|
59
|
+
|
|
60
|
+
[](https://pypi.org/project/pywebexec/)
|
|
61
|
+

|
|
62
|
+
[](https://shields.io/)
|
|
63
|
+
[](https://pepy.tech/project/pywebexec)
|
|
64
|
+
[](https://shields.io/)
|
|
65
|
+
|
|
66
|
+
# pywebexec
|
|
67
|
+
Simple Python HTTP(S) API/Web Command Launcher
|
|
68
|
+
|
|
69
|
+
## Install
|
|
70
|
+
```
|
|
71
|
+
$ pip install pywebexec
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Quick start
|
|
75
|
+
|
|
76
|
+
* start http server serving current directory executables listening on 0.0.0.0 port 8080
|
|
77
|
+
```
|
|
78
|
+
$ pywebexec
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
* Launch commands with params/view live output/Status using browser `http://<yourserver>:8080`
|
|
82
|
+
|
|
83
|
+
## features
|
|
84
|
+
|
|
85
|
+
* Serve executables in current directory
|
|
86
|
+
* Launch commands with params from web browser
|
|
87
|
+
* Follow live output
|
|
88
|
+
* Stop command
|
|
89
|
+
* Relaunch command
|
|
90
|
+
* HTTPS support
|
|
91
|
+
* HTTPS self-signed certificate generator
|
|
92
|
+
* Can be started as a daemon (POSIX)
|
|
93
|
+
* uses gunicorn to serve http/https
|
|
94
|
+
|
|
95
|
+
## Customize server
|
|
96
|
+
```
|
|
97
|
+
$ pywebexec --listen 0.0.0.0 --port 8080
|
|
98
|
+
$ pywebexec -l 0.0.0.0 -p 8080
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Basic auth user/password
|
|
102
|
+
```
|
|
103
|
+
$ pywebexec --user myuser [--password mypass]
|
|
104
|
+
$ pywebfs -u myuser [-P mypass]
|
|
105
|
+
```
|
|
106
|
+
Generated password is given if no `--pasword` option
|
|
107
|
+
|
|
108
|
+
## HTTPS server
|
|
109
|
+
|
|
110
|
+
* Generate auto-signed certificate and start https server
|
|
111
|
+
```
|
|
112
|
+
$ pywebfs --gencert
|
|
113
|
+
$ pywebfs --g
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
* Start https server using existing certificate
|
|
117
|
+
```
|
|
118
|
+
$ pywebfs --cert /pathto/host.cert --key /pathto/host.key
|
|
119
|
+
$ pywebfs -c /pathto/host.cert -k /pathto/host.key
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Launch server as a daemon (Linux)
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
$ pywebexec start
|
|
126
|
+
$ pywebexec status
|
|
127
|
+
$ pywebexec stop
|
|
128
|
+
```
|
|
129
|
+
* log of server are stored in current directory `.web_status/pwexec_<listen>:<port>.log`
|
|
130
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
.gitignore
|
|
2
|
+
LICENSE
|
|
3
|
+
README.md
|
|
4
|
+
pyproject.toml
|
|
5
|
+
setup.cfg
|
|
6
|
+
.github/workflows/python-publish.yml
|
|
7
|
+
pywebexec/__init__.py
|
|
8
|
+
pywebexec/pywebexec.py
|
|
9
|
+
pywebexec/version.py
|
|
10
|
+
pywebexec.egg-info/PKG-INFO
|
|
11
|
+
pywebexec.egg-info/SOURCES.txt
|
|
12
|
+
pywebexec.egg-info/dependency_links.txt
|
|
13
|
+
pywebexec.egg-info/entry_points.txt
|
|
14
|
+
pywebexec.egg-info/requires.txt
|
|
15
|
+
pywebexec.egg-info/top_level.txt
|
|
16
|
+
pywebexec/static/images/aborted.svg
|
|
17
|
+
pywebexec/static/images/copy.svg
|
|
18
|
+
pywebexec/static/images/copy_ok.svg
|
|
19
|
+
pywebexec/static/images/failed.svg
|
|
20
|
+
pywebexec/static/images/running.svg
|
|
21
|
+
pywebexec/static/images/success.svg
|
|
22
|
+
pywebexec/templates/__init__.py
|
|
23
|
+
pywebexec/templates/index.html
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pywebexec
|