static-http 0.1.1__tar.gz → 0.1.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.
- {static_http-0.1.1 → static_http-0.1.3}/LICENSE +1 -1
- {static_http-0.1.1 → static_http-0.1.3}/PKG-INFO +4 -2
- {static_http-0.1.1 → static_http-0.1.3}/README.md +2 -0
- {static_http-0.1.1 → static_http-0.1.3}/pyproject.toml +2 -2
- {static_http-0.1.1 → static_http-0.1.3}/src/http_here/__init__.py +1 -1
- {static_http-0.1.1 → static_http-0.1.3}/src/http_here/cli.py +10 -1
- {static_http-0.1.1 → static_http-0.1.3}/src/http_here/server.py +86 -1
- {static_http-0.1.1 → static_http-0.1.3}/src/static_http.egg-info/PKG-INFO +4 -2
- {static_http-0.1.1 → static_http-0.1.3}/tests/test_server.py +26 -0
- {static_http-0.1.1 → static_http-0.1.3}/setup.cfg +0 -0
- {static_http-0.1.1 → static_http-0.1.3}/src/http_here/__main__.py +0 -0
- {static_http-0.1.1 → static_http-0.1.3}/src/http_here/keyboard.py +0 -0
- {static_http-0.1.1 → static_http-0.1.3}/src/http_here/qrcode.py +0 -0
- {static_http-0.1.1 → static_http-0.1.3}/src/http_here/ranges.py +0 -0
- {static_http-0.1.1 → static_http-0.1.3}/src/http_here/urls.py +0 -0
- {static_http-0.1.1 → static_http-0.1.3}/src/static_http.egg-info/SOURCES.txt +0 -0
- {static_http-0.1.1 → static_http-0.1.3}/src/static_http.egg-info/dependency_links.txt +0 -0
- {static_http-0.1.1 → static_http-0.1.3}/src/static_http.egg-info/entry_points.txt +0 -0
- {static_http-0.1.1 → static_http-0.1.3}/src/static_http.egg-info/requires.txt +0 -0
- {static_http-0.1.1 → static_http-0.1.3}/src/static_http.egg-info/top_level.txt +0 -0
- {static_http-0.1.1 → static_http-0.1.3}/tests/test_cli.py +0 -0
- {static_http-0.1.1 → static_http-0.1.3}/tests/test_qrcode.py +0 -0
- {static_http-0.1.1 → static_http-0.1.3}/tests/test_ranges.py +0 -0
- {static_http-0.1.1 → static_http-0.1.3}/tests/test_urls.py +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: static-http
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: A temporary dependency-free static HTTP server with byte-range support.
|
|
5
|
-
Author:
|
|
5
|
+
Author: John Paul Ellis
|
|
6
6
|
License: MIT
|
|
7
7
|
Keywords: http,server,static-http,static,range,cli
|
|
8
8
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -72,6 +72,7 @@ By default:
|
|
|
72
72
|
- `--no-cache` — send `Cache-Control: no-store`.
|
|
73
73
|
- `--quiet` — suppress per-request logs.
|
|
74
74
|
- `--verbose` — print detailed startup/binding information.
|
|
75
|
+
- `--include-hidden` — include dot-prefixed files and directories in normal serving and directory listings.
|
|
75
76
|
- `--version` — print package version and exit.
|
|
76
77
|
|
|
77
78
|
## Examples
|
|
@@ -82,6 +83,7 @@ static-http --qr
|
|
|
82
83
|
static-http --no-cache
|
|
83
84
|
static-http --quiet
|
|
84
85
|
static-http --verbose
|
|
86
|
+
static-http --include-hidden
|
|
85
87
|
static-http --port 9000 --cors
|
|
86
88
|
static-http --no-dir-list
|
|
87
89
|
```
|
|
@@ -50,6 +50,7 @@ By default:
|
|
|
50
50
|
- `--no-cache` — send `Cache-Control: no-store`.
|
|
51
51
|
- `--quiet` — suppress per-request logs.
|
|
52
52
|
- `--verbose` — print detailed startup/binding information.
|
|
53
|
+
- `--include-hidden` — include dot-prefixed files and directories in normal serving and directory listings.
|
|
53
54
|
- `--version` — print package version and exit.
|
|
54
55
|
|
|
55
56
|
## Examples
|
|
@@ -60,6 +61,7 @@ static-http --qr
|
|
|
60
61
|
static-http --no-cache
|
|
61
62
|
static-http --quiet
|
|
62
63
|
static-http --verbose
|
|
64
|
+
static-http --include-hidden
|
|
63
65
|
static-http --port 9000 --cors
|
|
64
66
|
static-http --no-dir-list
|
|
65
67
|
```
|
|
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "static-http"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.3"
|
|
8
8
|
description = "A temporary dependency-free static HTTP server with byte-range support."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
11
11
|
license = { text = "MIT" }
|
|
12
|
-
authors = [{ name = "
|
|
12
|
+
authors = [{ name = "John Paul Ellis" }]
|
|
13
13
|
dependencies = []
|
|
14
14
|
keywords = ["http", "server", "static-http", "static", "range", "cli"]
|
|
15
15
|
classifiers = [
|
|
@@ -125,6 +125,11 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
125
125
|
action="store_true",
|
|
126
126
|
help="Print extra startup and binding information.",
|
|
127
127
|
)
|
|
128
|
+
parser.add_argument(
|
|
129
|
+
"--include-hidden",
|
|
130
|
+
action="store_true",
|
|
131
|
+
help="Include dot-prefixed files/directories when serving and listing directories.",
|
|
132
|
+
)
|
|
128
133
|
parser.add_argument(
|
|
129
134
|
"--version",
|
|
130
135
|
action="version",
|
|
@@ -166,12 +171,14 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
166
171
|
bind_port = args.port
|
|
167
172
|
|
|
168
173
|
response_headers = build_response_headers(cors=args.cors, no_cache=args.no_cache, headers=args.header)
|
|
174
|
+
effective_quiet = args.quiet or not args.verbose
|
|
169
175
|
|
|
170
176
|
handler = make_handler(
|
|
171
177
|
directory=root,
|
|
172
178
|
extra_headers=response_headers,
|
|
173
179
|
disable_dir_list=args.no_dir_list,
|
|
174
|
-
quiet=
|
|
180
|
+
quiet=effective_quiet,
|
|
181
|
+
show_hidden=args.include_hidden,
|
|
175
182
|
)
|
|
176
183
|
|
|
177
184
|
try:
|
|
@@ -180,6 +187,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
180
187
|
print(f"Could not bind {bind_host}:{bind_port}: {exc}", file=sys.stderr)
|
|
181
188
|
return 1
|
|
182
189
|
|
|
190
|
+
server.verbose = bool(args.verbose)
|
|
191
|
+
|
|
183
192
|
actual_bind = server.server_address[0]
|
|
184
193
|
actual_port = server.server_address[1]
|
|
185
194
|
discovered_lan = urls.discover_lan_urls() if urls.is_all_interfaces_bind(actual_bind) else []
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import io
|
|
6
|
+
import html
|
|
6
7
|
import os
|
|
8
|
+
import sys
|
|
7
9
|
import posixpath
|
|
8
10
|
import stat
|
|
9
11
|
import urllib.parse
|
|
@@ -26,11 +28,13 @@ class RangeAwareHTTPRequestHandler(SimpleHTTPRequestHandler):
|
|
|
26
28
|
extra_headers: Mapping[str, str],
|
|
27
29
|
disable_dir_list: bool,
|
|
28
30
|
quiet: bool,
|
|
31
|
+
show_hidden: bool,
|
|
29
32
|
**kwargs,
|
|
30
33
|
) -> None:
|
|
31
34
|
self._extra_headers = dict(extra_headers)
|
|
32
35
|
self._disable_dir_list = disable_dir_list
|
|
33
36
|
self._quiet = quiet
|
|
37
|
+
self._show_hidden = show_hidden
|
|
34
38
|
super().__init__(*args, directory=directory, **kwargs)
|
|
35
39
|
|
|
36
40
|
def _add_custom_headers(self) -> None:
|
|
@@ -42,6 +46,16 @@ class RangeAwareHTTPRequestHandler(SimpleHTTPRequestHandler):
|
|
|
42
46
|
return
|
|
43
47
|
super().log_message(format, *args)
|
|
44
48
|
|
|
49
|
+
def log_error(self, format: str, *args) -> None:
|
|
50
|
+
if self._quiet:
|
|
51
|
+
return
|
|
52
|
+
super().log_error(format, *args)
|
|
53
|
+
|
|
54
|
+
def _is_hidden(self, segment: str) -> bool:
|
|
55
|
+
if self._show_hidden:
|
|
56
|
+
return False
|
|
57
|
+
return segment.startswith(".")
|
|
58
|
+
|
|
45
59
|
def translate_path(self, path: str) -> str | None:
|
|
46
60
|
# Safe path mapping inspired by SimpleHTTPRequestHandler, with strict traversal defense.
|
|
47
61
|
path = path.split("?", 1)[0]
|
|
@@ -61,6 +75,8 @@ class RangeAwareHTTPRequestHandler(SimpleHTTPRequestHandler):
|
|
|
61
75
|
continue
|
|
62
76
|
if segment == "..":
|
|
63
77
|
continue
|
|
78
|
+
if self._is_hidden(segment):
|
|
79
|
+
return None
|
|
64
80
|
if ":" in segment:
|
|
65
81
|
return None
|
|
66
82
|
parts.append(segment)
|
|
@@ -183,6 +199,65 @@ class RangeAwareHTTPRequestHandler(SimpleHTTPRequestHandler):
|
|
|
183
199
|
f.close()
|
|
184
200
|
raise
|
|
185
201
|
|
|
202
|
+
def list_directory(self, path: str) -> io.BytesIO | None:
|
|
203
|
+
if self._show_hidden:
|
|
204
|
+
return super().list_directory(path)
|
|
205
|
+
|
|
206
|
+
try:
|
|
207
|
+
listdir = os.listdir(path)
|
|
208
|
+
except OSError:
|
|
209
|
+
self.send_error(HTTPStatus.NOT_FOUND, "No permission to list directory")
|
|
210
|
+
return None
|
|
211
|
+
|
|
212
|
+
listdir = [entry for entry in listdir if not self._is_hidden(entry)]
|
|
213
|
+
if not listdir:
|
|
214
|
+
self.send_error(HTTPStatus.NOT_FOUND, "No visible files")
|
|
215
|
+
return None
|
|
216
|
+
|
|
217
|
+
listdir.sort(key=lambda a: a.lower())
|
|
218
|
+
r = []
|
|
219
|
+
|
|
220
|
+
try:
|
|
221
|
+
displaypath = urllib.parse.unquote(self.path, errors="surrogatepass")
|
|
222
|
+
except UnicodeDecodeError:
|
|
223
|
+
displaypath = urllib.parse.unquote(self.path)
|
|
224
|
+
displaypath = html.escape(displaypath)
|
|
225
|
+
|
|
226
|
+
enc = "UTF-8"
|
|
227
|
+
title = f"Directory listing for {displaypath}"
|
|
228
|
+
r.append("<!DOCTYPE html>\n")
|
|
229
|
+
r.append("<html>\n<head>\n")
|
|
230
|
+
r.append(f'<meta charset="{enc}">\n')
|
|
231
|
+
r.append(f"<title>{title}</title>\n")
|
|
232
|
+
r.append("</head>\n<body>\n")
|
|
233
|
+
r.append(f"<h1>{title}</h1>\n<hr>\n<ul>\n")
|
|
234
|
+
|
|
235
|
+
for name in listdir:
|
|
236
|
+
full_name = os.path.join(path, name)
|
|
237
|
+
displayname = linkname = name
|
|
238
|
+
if os.path.isdir(full_name):
|
|
239
|
+
displayname = name + "/"
|
|
240
|
+
linkname = name + "/"
|
|
241
|
+
if os.path.islink(full_name):
|
|
242
|
+
displayname = name + "@"
|
|
243
|
+
|
|
244
|
+
r.append(
|
|
245
|
+
f'<li><a href="{urllib.parse.quote(linkname, errors="surrogatepass")}">'
|
|
246
|
+
f"{html.escape(displayname)}</a></li>\n"
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
r.append("</ul>\n<hr>\n</body>\n</html>\n")
|
|
250
|
+
encoded = "".join(r).encode(enc, "surrogateescape")
|
|
251
|
+
|
|
252
|
+
f = io.BytesIO()
|
|
253
|
+
f.write(encoded)
|
|
254
|
+
f.seek(0)
|
|
255
|
+
self.send_response(HTTPStatus.OK)
|
|
256
|
+
self.send_header("Content-Type", f"text/html; charset={enc}")
|
|
257
|
+
self.send_header("Content-Length", str(len(encoded)))
|
|
258
|
+
self.end_headers()
|
|
259
|
+
return f
|
|
260
|
+
|
|
186
261
|
|
|
187
262
|
class _RangeFile:
|
|
188
263
|
def __init__(self, wrapped: io.BufferedReader, remaining: int) -> None:
|
|
@@ -206,8 +281,17 @@ class ThreadedHTTPServer(ThreadingHTTPServer):
|
|
|
206
281
|
daemon_threads = True
|
|
207
282
|
allow_reuse_address = True
|
|
208
283
|
|
|
284
|
+
def handle_error(self, request, client_address) -> None: # type: ignore[override]
|
|
285
|
+
exc = sys.exc_info()[1]
|
|
286
|
+
if (
|
|
287
|
+
not bool(getattr(self, "verbose", False))
|
|
288
|
+
and isinstance(exc, (BrokenPipeError, ConnectionAbortedError, ConnectionResetError))
|
|
289
|
+
):
|
|
290
|
+
return
|
|
291
|
+
super().handle_error(request, client_address)
|
|
292
|
+
|
|
209
293
|
|
|
210
|
-
def make_handler(*, directory: str, extra_headers: Mapping[str, str], disable_dir_list: bool, quiet: bool):
|
|
294
|
+
def make_handler(*, directory: str, extra_headers: Mapping[str, str], disable_dir_list: bool, quiet: bool, show_hidden: bool = False):
|
|
211
295
|
def _factory(*args, **kwargs):
|
|
212
296
|
return RangeAwareHTTPRequestHandler(
|
|
213
297
|
*args,
|
|
@@ -215,6 +299,7 @@ def make_handler(*, directory: str, extra_headers: Mapping[str, str], disable_di
|
|
|
215
299
|
extra_headers=extra_headers,
|
|
216
300
|
disable_dir_list=disable_dir_list,
|
|
217
301
|
quiet=quiet,
|
|
302
|
+
show_hidden=show_hidden,
|
|
218
303
|
**kwargs,
|
|
219
304
|
)
|
|
220
305
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: static-http
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: A temporary dependency-free static HTTP server with byte-range support.
|
|
5
|
-
Author:
|
|
5
|
+
Author: John Paul Ellis
|
|
6
6
|
License: MIT
|
|
7
7
|
Keywords: http,server,static-http,static,range,cli
|
|
8
8
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -72,6 +72,7 @@ By default:
|
|
|
72
72
|
- `--no-cache` — send `Cache-Control: no-store`.
|
|
73
73
|
- `--quiet` — suppress per-request logs.
|
|
74
74
|
- `--verbose` — print detailed startup/binding information.
|
|
75
|
+
- `--include-hidden` — include dot-prefixed files and directories in normal serving and directory listings.
|
|
75
76
|
- `--version` — print package version and exit.
|
|
76
77
|
|
|
77
78
|
## Examples
|
|
@@ -82,6 +83,7 @@ static-http --qr
|
|
|
82
83
|
static-http --no-cache
|
|
83
84
|
static-http --quiet
|
|
84
85
|
static-http --verbose
|
|
86
|
+
static-http --include-hidden
|
|
85
87
|
static-http --port 9000 --cors
|
|
86
88
|
static-http --no-dir-list
|
|
87
89
|
```
|
|
@@ -89,6 +89,7 @@ def test_headers_and_directory_listing_controls(tmp_path: Path) -> None:
|
|
|
89
89
|
root.mkdir()
|
|
90
90
|
(root / "index.txt").write_bytes(b"index")
|
|
91
91
|
(tmp_path / "a b.txt").write_bytes(b"space")
|
|
92
|
+
(tmp_path / ".hidden.txt").write_bytes(b"secret")
|
|
92
93
|
(tmp_path / "rootdir").mkdir()
|
|
93
94
|
|
|
94
95
|
with _running_server(
|
|
@@ -110,6 +111,10 @@ def test_headers_and_directory_listing_controls(tmp_path: Path) -> None:
|
|
|
110
111
|
assert resp.headers["Access-Control-Allow-Origin"] == "*"
|
|
111
112
|
assert resp.headers["Cache-Control"] == "no-store"
|
|
112
113
|
|
|
114
|
+
with pytest.raises(urllib.error.HTTPError) as exc:
|
|
115
|
+
urllib.request.urlopen(_http_url(srv, "/.hidden.txt"))
|
|
116
|
+
assert exc.value.code in {400, 404}
|
|
117
|
+
|
|
113
118
|
with _running_server(
|
|
114
119
|
tmp_path,
|
|
115
120
|
extra_headers={},
|
|
@@ -121,6 +126,27 @@ def test_headers_and_directory_listing_controls(tmp_path: Path) -> None:
|
|
|
121
126
|
assert exc.value.code == 403
|
|
122
127
|
|
|
123
128
|
|
|
129
|
+
def test_show_hidden_flag_exposes_hidden_entries(tmp_path: Path) -> None:
|
|
130
|
+
(tmp_path / ".hidden.txt").write_bytes(b"secret")
|
|
131
|
+
|
|
132
|
+
with _running_server(
|
|
133
|
+
tmp_path,
|
|
134
|
+
extra_headers={},
|
|
135
|
+
disable_dir_list=False,
|
|
136
|
+
quiet=True,
|
|
137
|
+
show_hidden=True,
|
|
138
|
+
) as srv:
|
|
139
|
+
list_url = _http_url(srv, "/")
|
|
140
|
+
with urllib.request.urlopen(list_url) as resp:
|
|
141
|
+
assert resp.status == 200
|
|
142
|
+
body = resp.read()
|
|
143
|
+
assert b".hidden.txt" in body
|
|
144
|
+
|
|
145
|
+
with urllib.request.urlopen(_http_url(srv, "/.hidden.txt")) as resp:
|
|
146
|
+
assert resp.status == 200
|
|
147
|
+
assert resp.read() == b"secret"
|
|
148
|
+
|
|
149
|
+
|
|
124
150
|
def test_path_traversal_cannot_escape_root(tmp_path: Path) -> None:
|
|
125
151
|
(tmp_path / "inside.txt").write_bytes(b"inside")
|
|
126
152
|
(tmp_path.parent / "outside.txt").write_bytes(b"outside")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|