diff options
| author | Simeon Simeonov | 2023-01-12 22:36:04 +0100 |
|---|---|---|
| committer | Simeon Simeonov | 2023-01-12 22:36:04 +0100 |
| commit | e62d7c5ea3bc014123cf6f78e5345c588cd4f068 (patch) | |
| tree | 17ea5bbbe613a320f4ce91dd019e497a8bd90565 /src | |
| parent | dc1baaa1b1079516a7e2aead21f9af847e64494d (diff) | |
Make the project PEP517 compatible
Diffstat (limited to 'src')
| -rw-r--r-- | src/ngus/__init__.py | 128 | ||||
| -rw-r--r-- | src/ngus/__main__.py | 114 |
2 files changed, 242 insertions, 0 deletions
diff --git a/src/ngus/__init__.py b/src/ngus/__init__.py new file mode 100644 index 0000000..d085fc1 --- /dev/null +++ b/src/ngus/__init__.py | |||
| @@ -0,0 +1,128 @@ | |||
| 1 | # ngus | ||
| 2 | # Copyright (C) 2021-2023 Simeon Simeonov | ||
| 3 | |||
| 4 | # This program is free software: you can redistribute it and/or modify | ||
| 5 | # it under the terms of the GNU General Public License as published by | ||
| 6 | # the Free Software Foundation, either version 3 of the License, or | ||
| 7 | # (at your option) any later version. | ||
| 8 | |||
| 9 | # This program is distributed in the hope that it will be useful, | ||
| 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 12 | # GNU General Public License for more details. | ||
| 13 | |||
| 14 | # You should have received a copy of the GNU General Public License | ||
| 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
| 16 | |||
| 17 | import cgi | ||
| 18 | import http.server | ||
| 19 | import pathlib | ||
| 20 | |||
| 21 | __author__ = 'Simeon Simeonov' | ||
| 22 | __version__ = '2.0' | ||
| 23 | __license__ = 'GPL3' | ||
| 24 | |||
| 25 | DEFAULT_UPLOAD_PAGE = bytes('''<!DOCTYPE html> | ||
| 26 | <html> | ||
| 27 | <head> | ||
| 28 | <title>File Upload</title> | ||
| 29 | <meta name="viewport" content="width=device-width, user-scalable=no" /> | ||
| 30 | <style type="text/css"> | ||
| 31 | @media (prefers-color-scheme: dark) { | ||
| 32 | body { | ||
| 33 | background-color: #000; | ||
| 34 | color: #fff; | ||
| 35 | } | ||
| 36 | } | ||
| 37 | </style> | ||
| 38 | </head> | ||
| 39 | <body> | ||
| 40 | <h1>File Upload</h1> | ||
| 41 | <form method="POST" enctype="multipart/form-data"> | ||
| 42 | <input name="ufile" type="file" /> | ||
| 43 | <br/> | ||
| 44 | <br/> | ||
| 45 | <input type="submit" /> | ||
| 46 | </form> | ||
| 47 | </body> | ||
| 48 | </html>''', 'utf-8') | ||
| 49 | |||
| 50 | |||
| 51 | class NgusBaseHTTPRequestHandler(http.server.BaseHTTPRequestHandler): | ||
| 52 | """ | ||
| 53 | A custom http.server.SimpleHTTPRequestHandler that handles POST uploads | ||
| 54 | """ | ||
| 55 | def do_GET(self): | ||
| 56 | """GET requests will load the upload page""" | ||
| 57 | self._send_upload_page() | ||
| 58 | |||
| 59 | def do_POST(self): | ||
| 60 | """POST requests will be handled according to the settings""" | ||
| 61 | if self.server.basic_auth is not None: | ||
| 62 | if ( | ||
| 63 | 'Authorization' not in self.headers | ||
| 64 | or len(self.headers['Authorization'].split()) != 2 | ||
| 65 | or self.headers['Authorization'].split()[0].lower() != 'basic' | ||
| 66 | or self.headers[ | ||
| 67 | 'Authorization' | ||
| 68 | ].split()[1] != self.server.basic_auth | ||
| 69 | ): | ||
| 70 | self.send_response(http.HTTPStatus.UNAUTHORIZED) | ||
| 71 | self.send_header('WWW-Authenticate', | ||
| 72 | 'Basic realm="ngus", charset="UTF-8"') | ||
| 73 | self.end_headers() | ||
| 74 | return None | ||
| 75 | form = cgi.FieldStorage(fp=self.rfile, | ||
| 76 | headers=self.headers, | ||
| 77 | environ={'REQUEST_METHOD': 'POST'}) | ||
| 78 | if not (upload_dir := self.server.upload_dir).is_dir(): | ||
| 79 | raise ValueError('upload_dir is not a directory') | ||
| 80 | i_name = self.server.input_name | ||
| 81 | if i_name in form and form[i_name].file and form[i_name].filename: | ||
| 82 | with open( | ||
| 83 | upload_dir / pathlib.Path(form[i_name].filename).name, | ||
| 84 | 'wb' | ||
| 85 | ) as f: | ||
| 86 | f.write(form[i_name].file.read()) | ||
| 87 | self._send_upload_page() | ||
| 88 | return None | ||
| 89 | |||
| 90 | def _send_upload_page(self): | ||
| 91 | """Renders an upload page (form)""" | ||
| 92 | self.send_response(http.HTTPStatus.OK) | ||
| 93 | self.send_header('Content-Type', 'text/html; charset=utf-8') | ||
| 94 | self.send_header('Content-Length', len(self.server.upload_page)) | ||
| 95 | self.end_headers() | ||
| 96 | self.wfile.write(self.server.upload_page) | ||
| 97 | |||
| 98 | |||
| 99 | class NgusHTTPServer(http.server.HTTPServer): | ||
| 100 | """A custom http.server.HTTPServer""" | ||
| 101 | def __init__(self, *arg, **kwargs): | ||
| 102 | """ | ||
| 103 | """ | ||
| 104 | self._basic_auth = kwargs.pop('basic_auth', None) | ||
| 105 | self._input_name = kwargs.pop('input_name', 'ufile') | ||
| 106 | self._upload_dir = kwargs.pop('upload_dir', pathlib.Path.cwd()) | ||
| 107 | self._upload_page = kwargs.pop('upload_page', DEFAULT_UPLOAD_PAGE) | ||
| 108 | super().__init__(*arg, **kwargs) | ||
| 109 | |||
| 110 | @property | ||
| 111 | def basic_auth(self): | ||
| 112 | """basic_auth-property""" | ||
| 113 | return self._basic_auth | ||
| 114 | |||
| 115 | @property | ||
| 116 | def input_name(self): | ||
| 117 | """input_name-property""" | ||
| 118 | return self._input_name | ||
| 119 | |||
| 120 | @property | ||
| 121 | def upload_dir(self): | ||
| 122 | """upload_dir-property""" | ||
| 123 | return self._upload_dir | ||
| 124 | |||
| 125 | @property | ||
| 126 | def upload_page(self): | ||
| 127 | """upload_page-property""" | ||
| 128 | return self._upload_page | ||
diff --git a/src/ngus/__main__.py b/src/ngus/__main__.py new file mode 100644 index 0000000..efa2c4a --- /dev/null +++ b/src/ngus/__main__.py | |||
| @@ -0,0 +1,114 @@ | |||
| 1 | # ngus | ||
| 2 | # Copyright (C) 2021-2023 Simeon Simeonov | ||
| 3 | |||
| 4 | # This program is free software: you can redistribute it and/or modify | ||
| 5 | # it under the terms of the GNU General Public License as published by | ||
| 6 | # the Free Software Foundation, either version 3 of the License, or | ||
| 7 | # (at your option) any later version. | ||
| 8 | |||
| 9 | # This program is distributed in the hope that it will be useful, | ||
| 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 12 | # GNU General Public License for more details. | ||
| 13 | |||
| 14 | # You should have received a copy of the GNU General Public License | ||
| 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
| 16 | |||
| 17 | import argparse | ||
| 18 | import base64 | ||
| 19 | import logging | ||
| 20 | import pathlib | ||
| 21 | import sys | ||
| 22 | |||
| 23 | import ngus | ||
| 24 | |||
| 25 | |||
| 26 | def eprint(*arg, **kwargs): | ||
| 27 | """stdderr print wrapper""" | ||
| 28 | print(*arg, file=sys.stderr, flush=True, **kwargs) | ||
| 29 | |||
| 30 | |||
| 31 | def main(inargs=None): | ||
| 32 | """main entry point""" | ||
| 33 | parser = argparse.ArgumentParser( | ||
| 34 | description='The following options are available') | ||
| 35 | parser.add_argument( | ||
| 36 | '-b', '--basic-auth', | ||
| 37 | metavar='<username:password>', | ||
| 38 | type=str, | ||
| 39 | dest='basic_auth', | ||
| 40 | default='', | ||
| 41 | help=('Require Basic auth from the client in order to POST a file ' | ||
| 42 | '(default: No basic auth required)')) | ||
| 43 | parser.add_argument( | ||
| 44 | '-H', '--hostname', | ||
| 45 | metavar='<hostname>', | ||
| 46 | type=str, | ||
| 47 | dest='hostname', | ||
| 48 | default='127.0.0.1', | ||
| 49 | help='ngus server IP / hostname (default: 127.0.0.1)') | ||
| 50 | parser.add_argument( | ||
| 51 | '-i', '--input-name', | ||
| 52 | metavar='<name>', | ||
| 53 | type=str, | ||
| 54 | dest='input_name', | ||
| 55 | default='ufile', | ||
| 56 | help='The name of the form input field (default: "ufile")') | ||
| 57 | parser.add_argument( | ||
| 58 | '-p', '--port', | ||
| 59 | metavar='<port>', | ||
| 60 | type=int, | ||
| 61 | dest='port', | ||
| 62 | default=8080, | ||
| 63 | help='ngus server port (default: 8080)') | ||
| 64 | parser.add_argument( | ||
| 65 | '-u', '--upload-dir', | ||
| 66 | metavar='<dir>', | ||
| 67 | type=pathlib.Path, | ||
| 68 | dest='upload_dir', | ||
| 69 | default=pathlib.Path.cwd(), | ||
| 70 | help='ngus server upload dir (default: CWD)') | ||
| 71 | parser.add_argument( | ||
| 72 | '-U', '--upload-page', | ||
| 73 | metavar='<filename>', | ||
| 74 | type=argparse.FileType('rb'), | ||
| 75 | dest='upload_page', | ||
| 76 | default=None, | ||
| 77 | help='Alternative upload page (form) to display (default: None)') | ||
| 78 | parser.add_argument( | ||
| 79 | '-v', '--version', | ||
| 80 | action='version', | ||
| 81 | version=f'%(prog)s {ngus.__version__}', | ||
| 82 | help='Display program-version and exit') | ||
| 83 | args = parser.parse_args(inargs) | ||
| 84 | try: | ||
| 85 | logging.basicConfig(format='%(asctime)s - %(levelname)s - %(message)s', | ||
| 86 | level=logging.DEBUG) | ||
| 87 | logger = logging.getLogger('ngus') | ||
| 88 | logger.info('ngus starting') | ||
| 89 | server_params = { | ||
| 90 | 'input_name': args.input_name, | ||
| 91 | 'upload_dir': args.upload_dir | ||
| 92 | } | ||
| 93 | if args.upload_page is not None: | ||
| 94 | server_params['upload_page'] = args.upload_page.read() | ||
| 95 | if args.basic_auth and len(args.basic_auth.split(':')) == 2: | ||
| 96 | server_params['basic_auth'] = base64.b64encode( | ||
| 97 | args.basic_auth.strip().encode('utf-8')).decode() | ||
| 98 | logger.info('Requiring basic-auth') | ||
| 99 | ngus_server = ngus.NgusHTTPServer((args.hostname, args.port), | ||
| 100 | ngus.NgusBaseHTTPRequestHandler, | ||
| 101 | **server_params) | ||
| 102 | logger.info(f'Listening on {args.hostname}:{args.port}') | ||
| 103 | logger.info('Done!') | ||
| 104 | ngus_server.serve_forever() | ||
| 105 | except KeyboardInterrupt: | ||
| 106 | print('\n\nTerminating...') | ||
| 107 | except Exception as e: | ||
| 108 | eprint(f'ngus critical error: {e}') | ||
| 109 | sys.exit(1) | ||
| 110 | sys.exit(0) | ||
| 111 | |||
| 112 | |||
| 113 | if __name__ == '__main__': | ||
| 114 | main() | ||
