summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorSimeon Simeonov2023-01-12 22:36:04 +0100
committerSimeon Simeonov2023-01-12 22:36:04 +0100
commite62d7c5ea3bc014123cf6f78e5345c588cd4f068 (patch)
tree17ea5bbbe613a320f4ce91dd019e497a8bd90565 /src
parentdc1baaa1b1079516a7e2aead21f9af847e64494d (diff)
Make the project PEP517 compatible
Diffstat (limited to 'src')
-rw-r--r--src/ngus/__init__.py128
-rw-r--r--src/ngus/__main__.py114
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
17import cgi
18import http.server
19import pathlib
20
21__author__ = 'Simeon Simeonov'
22__version__ = '2.0'
23__license__ = 'GPL3'
24
25DEFAULT_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
51class 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
99class 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
17import argparse
18import base64
19import logging
20import pathlib
21import sys
22
23import ngus
24
25
26def eprint(*arg, **kwargs):
27 """stdderr print wrapper"""
28 print(*arg, file=sys.stderr, flush=True, **kwargs)
29
30
31def 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
113if __name__ == '__main__':
114 main()