From e62d7c5ea3bc014123cf6f78e5345c588cd4f068 Mon Sep 17 00:00:00 2001 From: Simeon Simeonov Date: Thu, 12 Jan 2023 22:36:04 +0100 Subject: Make the project PEP517 compatible --- src/ngus/__init__.py | 128 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/ngus/__main__.py | 114 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 242 insertions(+) create mode 100644 src/ngus/__init__.py create mode 100644 src/ngus/__main__.py (limited to 'src') 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 @@ +# ngus +# Copyright (C) 2021-2023 Simeon Simeonov + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import cgi +import http.server +import pathlib + +__author__ = 'Simeon Simeonov' +__version__ = '2.0' +__license__ = 'GPL3' + +DEFAULT_UPLOAD_PAGE = bytes(''' + + +File Upload + + + + +

File Upload

+
+ +
+
+ +
+ +''', 'utf-8') + + +class NgusBaseHTTPRequestHandler(http.server.BaseHTTPRequestHandler): + """ + A custom http.server.SimpleHTTPRequestHandler that handles POST uploads + """ + def do_GET(self): + """GET requests will load the upload page""" + self._send_upload_page() + + def do_POST(self): + """POST requests will be handled according to the settings""" + if self.server.basic_auth is not None: + if ( + 'Authorization' not in self.headers + or len(self.headers['Authorization'].split()) != 2 + or self.headers['Authorization'].split()[0].lower() != 'basic' + or self.headers[ + 'Authorization' + ].split()[1] != self.server.basic_auth + ): + self.send_response(http.HTTPStatus.UNAUTHORIZED) + self.send_header('WWW-Authenticate', + 'Basic realm="ngus", charset="UTF-8"') + self.end_headers() + return None + form = cgi.FieldStorage(fp=self.rfile, + headers=self.headers, + environ={'REQUEST_METHOD': 'POST'}) + if not (upload_dir := self.server.upload_dir).is_dir(): + raise ValueError('upload_dir is not a directory') + i_name = self.server.input_name + if i_name in form and form[i_name].file and form[i_name].filename: + with open( + upload_dir / pathlib.Path(form[i_name].filename).name, + 'wb' + ) as f: + f.write(form[i_name].file.read()) + self._send_upload_page() + return None + + def _send_upload_page(self): + """Renders an upload page (form)""" + self.send_response(http.HTTPStatus.OK) + self.send_header('Content-Type', 'text/html; charset=utf-8') + self.send_header('Content-Length', len(self.server.upload_page)) + self.end_headers() + self.wfile.write(self.server.upload_page) + + +class NgusHTTPServer(http.server.HTTPServer): + """A custom http.server.HTTPServer""" + def __init__(self, *arg, **kwargs): + """ + """ + self._basic_auth = kwargs.pop('basic_auth', None) + self._input_name = kwargs.pop('input_name', 'ufile') + self._upload_dir = kwargs.pop('upload_dir', pathlib.Path.cwd()) + self._upload_page = kwargs.pop('upload_page', DEFAULT_UPLOAD_PAGE) + super().__init__(*arg, **kwargs) + + @property + def basic_auth(self): + """basic_auth-property""" + return self._basic_auth + + @property + def input_name(self): + """input_name-property""" + return self._input_name + + @property + def upload_dir(self): + """upload_dir-property""" + return self._upload_dir + + @property + def upload_page(self): + """upload_page-property""" + 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 @@ +# ngus +# Copyright (C) 2021-2023 Simeon Simeonov + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import argparse +import base64 +import logging +import pathlib +import sys + +import ngus + + +def eprint(*arg, **kwargs): + """stdderr print wrapper""" + print(*arg, file=sys.stderr, flush=True, **kwargs) + + +def main(inargs=None): + """main entry point""" + parser = argparse.ArgumentParser( + description='The following options are available') + parser.add_argument( + '-b', '--basic-auth', + metavar='', + type=str, + dest='basic_auth', + default='', + help=('Require Basic auth from the client in order to POST a file ' + '(default: No basic auth required)')) + parser.add_argument( + '-H', '--hostname', + metavar='', + type=str, + dest='hostname', + default='127.0.0.1', + help='ngus server IP / hostname (default: 127.0.0.1)') + parser.add_argument( + '-i', '--input-name', + metavar='', + type=str, + dest='input_name', + default='ufile', + help='The name of the form input field (default: "ufile")') + parser.add_argument( + '-p', '--port', + metavar='', + type=int, + dest='port', + default=8080, + help='ngus server port (default: 8080)') + parser.add_argument( + '-u', '--upload-dir', + metavar='', + type=pathlib.Path, + dest='upload_dir', + default=pathlib.Path.cwd(), + help='ngus server upload dir (default: CWD)') + parser.add_argument( + '-U', '--upload-page', + metavar='', + type=argparse.FileType('rb'), + dest='upload_page', + default=None, + help='Alternative upload page (form) to display (default: None)') + parser.add_argument( + '-v', '--version', + action='version', + version=f'%(prog)s {ngus.__version__}', + help='Display program-version and exit') + args = parser.parse_args(inargs) + try: + logging.basicConfig(format='%(asctime)s - %(levelname)s - %(message)s', + level=logging.DEBUG) + logger = logging.getLogger('ngus') + logger.info('ngus starting') + server_params = { + 'input_name': args.input_name, + 'upload_dir': args.upload_dir + } + if args.upload_page is not None: + server_params['upload_page'] = args.upload_page.read() + if args.basic_auth and len(args.basic_auth.split(':')) == 2: + server_params['basic_auth'] = base64.b64encode( + args.basic_auth.strip().encode('utf-8')).decode() + logger.info('Requiring basic-auth') + ngus_server = ngus.NgusHTTPServer((args.hostname, args.port), + ngus.NgusBaseHTTPRequestHandler, + **server_params) + logger.info(f'Listening on {args.hostname}:{args.port}') + logger.info('Done!') + ngus_server.serve_forever() + except KeyboardInterrupt: + print('\n\nTerminating...') + except Exception as e: + eprint(f'ngus critical error: {e}') + sys.exit(1) + sys.exit(0) + + +if __name__ == '__main__': + main() -- cgit v1.3