srcviewserver 0.1.0a0__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.
- srcviewserver-0.1.0a0/LICENSE +21 -0
- srcviewserver-0.1.0a0/PKG-INFO +56 -0
- srcviewserver-0.1.0a0/README.md +33 -0
- srcviewserver-0.1.0a0/pyproject.toml +38 -0
- srcviewserver-0.1.0a0/setup.cfg +7 -0
- srcviewserver-0.1.0a0/srcviewserver.egg-info/PKG-INFO +56 -0
- srcviewserver-0.1.0a0/srcviewserver.egg-info/SOURCES.txt +11 -0
- srcviewserver-0.1.0a0/srcviewserver.egg-info/dependency_links.txt +1 -0
- srcviewserver-0.1.0a0/srcviewserver.egg-info/entry_points.txt +2 -0
- srcviewserver-0.1.0a0/srcviewserver.egg-info/requires.txt +9 -0
- srcviewserver-0.1.0a0/srcviewserver.egg-info/top_level.txt +1 -0
- srcviewserver-0.1.0a0/srcviewserver.py +402 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jifeng Wu
|
|
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.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: srcviewserver
|
|
3
|
+
Version: 0.1.0a0
|
|
4
|
+
Summary: A simple HTTP server that serves source files with syntax highlighting.
|
|
5
|
+
Author-email: Jifeng Wu <jifengwu2k@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/jifengwu2k/srcviewserver
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/jifengwu2k/srcviewserver/issues
|
|
9
|
+
Classifier: Programming Language :: Python :: 2
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=2
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: fspathverbs
|
|
16
|
+
Requires-Dist: guess-file-mime-type
|
|
17
|
+
Requires-Dist: httppackets
|
|
18
|
+
Requires-Dist: pygments
|
|
19
|
+
Requires-Dist: six
|
|
20
|
+
Requires-Dist: textcompat
|
|
21
|
+
Requires-Dist: typing; python_version < "3.5"
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# `srcviewserver`
|
|
25
|
+
|
|
26
|
+
A simple HTTP server that serves source files with syntax highlighting.
|
|
27
|
+
|
|
28
|
+
Source files with a recognized language extension are highlighted and wrapped in an HTML page.
|
|
29
|
+
All other files are served as-is (binary streaming). Directories get an HTML listing.
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
pip install srcviewserver
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Usage
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
srcviewserver [--port PORT] [--bind ADDRESS]
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
| Option | Default | Description |
|
|
44
|
+
|------------- |-------------- |-------------------------- |
|
|
45
|
+
| `--bind -b` | `127.0.0.1` | Address to bind to. |
|
|
46
|
+
| `--port -p` | `8000` | Port to listen on. |
|
|
47
|
+
|
|
48
|
+
Navigate to `http://127.0.0.1:8000/` and browse the directory tree.
|
|
49
|
+
|
|
50
|
+
## Contributing
|
|
51
|
+
|
|
52
|
+
Contributions are welcome! Please submit pull requests or open issues on the GitHub repository.
|
|
53
|
+
|
|
54
|
+
## License
|
|
55
|
+
|
|
56
|
+
This project is licensed under the [MIT License](LICENSE).
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# `srcviewserver`
|
|
2
|
+
|
|
3
|
+
A simple HTTP server that serves source files with syntax highlighting.
|
|
4
|
+
|
|
5
|
+
Source files with a recognized language extension are highlighted and wrapped in an HTML page.
|
|
6
|
+
All other files are served as-is (binary streaming). Directories get an HTML listing.
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
pip install srcviewserver
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
srcviewserver [--port PORT] [--bind ADDRESS]
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
| Option | Default | Description |
|
|
21
|
+
|------------- |-------------- |-------------------------- |
|
|
22
|
+
| `--bind -b` | `127.0.0.1` | Address to bind to. |
|
|
23
|
+
| `--port -p` | `8000` | Port to listen on. |
|
|
24
|
+
|
|
25
|
+
Navigate to `http://127.0.0.1:8000/` and browse the directory tree.
|
|
26
|
+
|
|
27
|
+
## Contributing
|
|
28
|
+
|
|
29
|
+
Contributions are welcome! Please submit pull requests or open issues on the GitHub repository.
|
|
30
|
+
|
|
31
|
+
## License
|
|
32
|
+
|
|
33
|
+
This project is licensed under the [MIT License](LICENSE).
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "srcviewserver"
|
|
7
|
+
version = "0.1.0a0"
|
|
8
|
+
description = "A simple HTTP server that serves source files with syntax highlighting."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=2"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name="Jifeng Wu", email="jifengwu2k@gmail.com" }
|
|
14
|
+
]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Programming Language :: Python :: 2",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
]
|
|
20
|
+
dependencies = [
|
|
21
|
+
"fspathverbs",
|
|
22
|
+
"guess-file-mime-type",
|
|
23
|
+
"httppackets",
|
|
24
|
+
"pygments",
|
|
25
|
+
"six",
|
|
26
|
+
"textcompat",
|
|
27
|
+
"typing; python_version < '3.5'",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.scripts]
|
|
31
|
+
srcviewserver = "srcviewserver:main"
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
"Homepage" = "https://github.com/jifengwu2k/srcviewserver"
|
|
35
|
+
"Bug Tracker" = "https://github.com/jifengwu2k/srcviewserver/issues"
|
|
36
|
+
|
|
37
|
+
[tool.setuptools]
|
|
38
|
+
py-modules = ["srcviewserver"]
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: srcviewserver
|
|
3
|
+
Version: 0.1.0a0
|
|
4
|
+
Summary: A simple HTTP server that serves source files with syntax highlighting.
|
|
5
|
+
Author-email: Jifeng Wu <jifengwu2k@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/jifengwu2k/srcviewserver
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/jifengwu2k/srcviewserver/issues
|
|
9
|
+
Classifier: Programming Language :: Python :: 2
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=2
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: fspathverbs
|
|
16
|
+
Requires-Dist: guess-file-mime-type
|
|
17
|
+
Requires-Dist: httppackets
|
|
18
|
+
Requires-Dist: pygments
|
|
19
|
+
Requires-Dist: six
|
|
20
|
+
Requires-Dist: textcompat
|
|
21
|
+
Requires-Dist: typing; python_version < "3.5"
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# `srcviewserver`
|
|
25
|
+
|
|
26
|
+
A simple HTTP server that serves source files with syntax highlighting.
|
|
27
|
+
|
|
28
|
+
Source files with a recognized language extension are highlighted and wrapped in an HTML page.
|
|
29
|
+
All other files are served as-is (binary streaming). Directories get an HTML listing.
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
pip install srcviewserver
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Usage
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
srcviewserver [--port PORT] [--bind ADDRESS]
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
| Option | Default | Description |
|
|
44
|
+
|------------- |-------------- |-------------------------- |
|
|
45
|
+
| `--bind -b` | `127.0.0.1` | Address to bind to. |
|
|
46
|
+
| `--port -p` | `8000` | Port to listen on. |
|
|
47
|
+
|
|
48
|
+
Navigate to `http://127.0.0.1:8000/` and browse the directory tree.
|
|
49
|
+
|
|
50
|
+
## Contributing
|
|
51
|
+
|
|
52
|
+
Contributions are welcome! Please submit pull requests or open issues on the GitHub repository.
|
|
53
|
+
|
|
54
|
+
## License
|
|
55
|
+
|
|
56
|
+
This project is licensed under the [MIT License](LICENSE).
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
setup.cfg
|
|
5
|
+
srcviewserver.py
|
|
6
|
+
srcviewserver.egg-info/PKG-INFO
|
|
7
|
+
srcviewserver.egg-info/SOURCES.txt
|
|
8
|
+
srcviewserver.egg-info/dependency_links.txt
|
|
9
|
+
srcviewserver.egg-info/entry_points.txt
|
|
10
|
+
srcviewserver.egg-info/requires.txt
|
|
11
|
+
srcviewserver.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
srcviewserver
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
# Copyright (c) 2026 Jifeng Wu
|
|
2
|
+
# Licensed under the MIT License. See LICENSE file in the project root
|
|
3
|
+
# for full license information.
|
|
4
|
+
from __future__ import print_function, unicode_literals
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import codecs
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
import posixpath
|
|
11
|
+
import socket
|
|
12
|
+
import time
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
from typing import (
|
|
17
|
+
BinaryIO,
|
|
18
|
+
Dict,
|
|
19
|
+
List,
|
|
20
|
+
Optional,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
from fspathverbs import (
|
|
25
|
+
Child,
|
|
26
|
+
Current,
|
|
27
|
+
FSPathVerb,
|
|
28
|
+
Parent,
|
|
29
|
+
Root,
|
|
30
|
+
compile_to_fspathverbs,
|
|
31
|
+
)
|
|
32
|
+
from guess_file_mime_type import guess_file_mime_type
|
|
33
|
+
from httppackets.http_1_1_parser import (
|
|
34
|
+
Decision,
|
|
35
|
+
ParserError,
|
|
36
|
+
parse_http_1_1_requests,
|
|
37
|
+
)
|
|
38
|
+
from httppackets.http_1_1_serializer import (
|
|
39
|
+
SupportsRead,
|
|
40
|
+
serialize_http_1_1_response,
|
|
41
|
+
)
|
|
42
|
+
from pygments import highlight
|
|
43
|
+
from pygments.formatters import HtmlFormatter
|
|
44
|
+
from pygments.lexers import get_lexer_for_filename
|
|
45
|
+
from pygments.util import ClassNotFound
|
|
46
|
+
from six.moves.urllib.parse import quote, unquote, urlparse
|
|
47
|
+
from textcompat import filesystem_str_to_text
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
DEFAULT_BIND = "127.0.0.1"
|
|
51
|
+
DEFAULT_PORT = 8000
|
|
52
|
+
SERVER = "sourceview"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
ERROR_MESSAGES = {
|
|
56
|
+
400: "Bad request",
|
|
57
|
+
403: "Forbidden",
|
|
58
|
+
404: "Not found",
|
|
59
|
+
405: "Method not allowed",
|
|
60
|
+
406: "Not acceptable",
|
|
61
|
+
408: "Request timeout",
|
|
62
|
+
414: "URI too long",
|
|
63
|
+
500: "Internal server error",
|
|
64
|
+
501: "Not implemented",
|
|
65
|
+
503: "Service unavailable",
|
|
66
|
+
} # type: Dict[int, str]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def format_current_date_time():
|
|
70
|
+
# type: () -> str
|
|
71
|
+
"""Return the current time as an HTTP-date string."""
|
|
72
|
+
return time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(time.time()))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def write_error(stream, code):
|
|
76
|
+
# type: (BinaryIO, int) -> None
|
|
77
|
+
"""Write an HTTP error response to *stream*."""
|
|
78
|
+
message = ERROR_MESSAGES.get(code, "Unknown error")
|
|
79
|
+
|
|
80
|
+
body = '\n'.join(
|
|
81
|
+
(
|
|
82
|
+
'<!DOCTYPE HTML>',
|
|
83
|
+
'<html>',
|
|
84
|
+
'<head>',
|
|
85
|
+
'<meta charset="utf-8">',
|
|
86
|
+
'<title>%d %s</title>' % (code, message),
|
|
87
|
+
'</head>',
|
|
88
|
+
'<body>',
|
|
89
|
+
'<h1>%d %s</h1>' % (code, message),
|
|
90
|
+
'</body>',
|
|
91
|
+
'</html>',
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
body_bytes = body.encode("utf-8")
|
|
96
|
+
|
|
97
|
+
headers = {
|
|
98
|
+
"content-type": ["text/html; charset=utf-8"],
|
|
99
|
+
"content-length": [str(len(body_bytes))],
|
|
100
|
+
"connection": ["close"],
|
|
101
|
+
"date": [format_current_date_time()],
|
|
102
|
+
"server": [SERVER],
|
|
103
|
+
} # type: Dict[str, List[str]]
|
|
104
|
+
|
|
105
|
+
serialize_http_1_1_response(
|
|
106
|
+
stream,
|
|
107
|
+
status_code=code,
|
|
108
|
+
reason=message,
|
|
109
|
+
headers=headers,
|
|
110
|
+
body=body_bytes,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class FileBodyReader(SupportsRead):
|
|
115
|
+
__slots__ = ("_file",)
|
|
116
|
+
|
|
117
|
+
def __init__(self, file_obj):
|
|
118
|
+
# type: (BinaryIO) -> None
|
|
119
|
+
self._file = file_obj
|
|
120
|
+
|
|
121
|
+
def read(self, n=-1):
|
|
122
|
+
# type: (int) -> bytes
|
|
123
|
+
return self._file.read(n)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def serve_path(wf, method, path, target):
|
|
127
|
+
# type: (BinaryIO, str, str, str) -> None
|
|
128
|
+
"""Serve *path* as a response on *wf*."""
|
|
129
|
+
if os.path.isdir(path):
|
|
130
|
+
if method == "GET":
|
|
131
|
+
# Directory listing
|
|
132
|
+
names = os.listdir(path)
|
|
133
|
+
names.sort()
|
|
134
|
+
|
|
135
|
+
listing_html_lines = [
|
|
136
|
+
'<!DOCTYPE HTML>',
|
|
137
|
+
'<html>',
|
|
138
|
+
'<head>',
|
|
139
|
+
'<meta charset="utf-8">',
|
|
140
|
+
'<title>Directory listing for %s</title>' % (target,),
|
|
141
|
+
'</head>',
|
|
142
|
+
'<body>',
|
|
143
|
+
'<h1>Directory listing for %s</h1>' % (target,),
|
|
144
|
+
'<hr/>',
|
|
145
|
+
'<ul>',
|
|
146
|
+
] # type: List[str]
|
|
147
|
+
|
|
148
|
+
for name in names:
|
|
149
|
+
quoted = quote(name)
|
|
150
|
+
display = filesystem_str_to_text(name)
|
|
151
|
+
full = os.path.join(path, name)
|
|
152
|
+
|
|
153
|
+
if os.path.isdir(full):
|
|
154
|
+
listing_html_lines.append('<li><a href="%s/">%s/</a></li>' % (quoted, display))
|
|
155
|
+
else:
|
|
156
|
+
listing_html_lines.append('<li><a href="%s">%s</a></li>' % (quoted, display))
|
|
157
|
+
|
|
158
|
+
listing_html_lines += [
|
|
159
|
+
'</ul>',
|
|
160
|
+
'<hr/>',
|
|
161
|
+
'</body>',
|
|
162
|
+
'</html>'
|
|
163
|
+
]
|
|
164
|
+
|
|
165
|
+
listing_html = '\n'.join(listing_html_lines)
|
|
166
|
+
|
|
167
|
+
listing_html_bytes = listing_html.encode("utf-8")
|
|
168
|
+
|
|
169
|
+
resp_headers = {
|
|
170
|
+
"content-type": ["text/html; charset=utf-8"],
|
|
171
|
+
"content-length": [str(len(listing_html_bytes))],
|
|
172
|
+
"connection": ["close"],
|
|
173
|
+
"date": [format_current_date_time()],
|
|
174
|
+
"server": [SERVER],
|
|
175
|
+
} # type: Dict[str, List[str]]
|
|
176
|
+
|
|
177
|
+
serialize_http_1_1_response(
|
|
178
|
+
wf,
|
|
179
|
+
status_code=200,
|
|
180
|
+
reason="OK",
|
|
181
|
+
headers=resp_headers,
|
|
182
|
+
body=listing_html_bytes,
|
|
183
|
+
)
|
|
184
|
+
else:
|
|
185
|
+
resp_headers = {
|
|
186
|
+
"content-type": ["text/html; charset=utf-8"],
|
|
187
|
+
"content-length": ["0"],
|
|
188
|
+
"connection": ["close"],
|
|
189
|
+
"date": [format_current_date_time()],
|
|
190
|
+
"server": [SERVER],
|
|
191
|
+
} # type: Dict[str, List[str]]
|
|
192
|
+
|
|
193
|
+
serialize_http_1_1_response(
|
|
194
|
+
wf,
|
|
195
|
+
status_code=200,
|
|
196
|
+
reason="OK",
|
|
197
|
+
headers=resp_headers,
|
|
198
|
+
body=None,
|
|
199
|
+
)
|
|
200
|
+
else:
|
|
201
|
+
# File
|
|
202
|
+
content_type = guess_file_mime_type(path)
|
|
203
|
+
if method == "GET":
|
|
204
|
+
try:
|
|
205
|
+
lexer = get_lexer_for_filename(path)
|
|
206
|
+
except ClassNotFound:
|
|
207
|
+
lexer = None
|
|
208
|
+
|
|
209
|
+
if lexer is not None:
|
|
210
|
+
with codecs.open(path, 'r', encoding='utf-8') as inf:
|
|
211
|
+
code = inf.read()
|
|
212
|
+
|
|
213
|
+
formatter = HtmlFormatter(nowrap=True, noclasses=True, style='default')
|
|
214
|
+
highlighted = highlight(code, lexer, formatter)
|
|
215
|
+
|
|
216
|
+
input_file_name_with_ext = os.path.basename(path)
|
|
217
|
+
|
|
218
|
+
html_content = '\n'.join(
|
|
219
|
+
(
|
|
220
|
+
'<!DOCTYPE html>',
|
|
221
|
+
'<html>',
|
|
222
|
+
'<head>',
|
|
223
|
+
'<meta charset="utf-8">',
|
|
224
|
+
'<title>%s</title>' % filesystem_str_to_text(input_file_name_with_ext),
|
|
225
|
+
'<style> .code { white-space: pre-wrap; word-break: break-word; font-family: monospace } </style>',
|
|
226
|
+
'</head>',
|
|
227
|
+
'<body>',
|
|
228
|
+
'<div class="code">',
|
|
229
|
+
highlighted,
|
|
230
|
+
'</div>',
|
|
231
|
+
'</body>',
|
|
232
|
+
'</html>',
|
|
233
|
+
)
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
body_bytes = html_content.encode("utf-8")
|
|
237
|
+
|
|
238
|
+
resp_headers = {
|
|
239
|
+
"content-type": ["text/html; charset=utf-8"],
|
|
240
|
+
"content-length": [str(len(body_bytes))],
|
|
241
|
+
"connection": ["close"],
|
|
242
|
+
"date": [format_current_date_time()],
|
|
243
|
+
"server": [SERVER],
|
|
244
|
+
} # type: Dict[str, List[str]]
|
|
245
|
+
|
|
246
|
+
serialize_http_1_1_response(
|
|
247
|
+
wf,
|
|
248
|
+
status_code=200,
|
|
249
|
+
reason="OK",
|
|
250
|
+
headers=resp_headers,
|
|
251
|
+
body=body_bytes,
|
|
252
|
+
)
|
|
253
|
+
else:
|
|
254
|
+
with open(path, "rb") as inf:
|
|
255
|
+
resp_headers = {
|
|
256
|
+
"content-type": [content_type],
|
|
257
|
+
"connection": ["close"],
|
|
258
|
+
"date": [format_current_date_time()],
|
|
259
|
+
"server": [SERVER],
|
|
260
|
+
} # type: Dict[str, List[str]]
|
|
261
|
+
|
|
262
|
+
serialize_http_1_1_response(
|
|
263
|
+
wf,
|
|
264
|
+
status_code=200,
|
|
265
|
+
reason="OK",
|
|
266
|
+
headers=resp_headers,
|
|
267
|
+
body=FileBodyReader(inf),
|
|
268
|
+
)
|
|
269
|
+
else:
|
|
270
|
+
file_size = os.path.getsize(path)
|
|
271
|
+
|
|
272
|
+
resp_headers = {
|
|
273
|
+
"content-type": [content_type],
|
|
274
|
+
"content-length": [str(file_size)],
|
|
275
|
+
"connection": ["close"],
|
|
276
|
+
"date": [format_current_date_time()],
|
|
277
|
+
"server": [SERVER],
|
|
278
|
+
} # type: Dict[str, List[str]]
|
|
279
|
+
|
|
280
|
+
serialize_http_1_1_response(
|
|
281
|
+
wf,
|
|
282
|
+
status_code=200,
|
|
283
|
+
reason="OK",
|
|
284
|
+
headers=resp_headers,
|
|
285
|
+
body=None,
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def handle_connection(sock):
|
|
290
|
+
# type: (socket.socket) -> None
|
|
291
|
+
"""Handle a single TCP connection: parse request, serve response."""
|
|
292
|
+
|
|
293
|
+
rf = sock.makefile("rb")
|
|
294
|
+
wf = sock.makefile("wb")
|
|
295
|
+
|
|
296
|
+
cwd = os.getcwd()
|
|
297
|
+
|
|
298
|
+
def on_headers(method, target, headers):
|
|
299
|
+
# type: (str, str, Dict[str, List[str]]) -> Decision
|
|
300
|
+
if method not in ("GET", "HEAD"):
|
|
301
|
+
write_error(wf, 501)
|
|
302
|
+
else:
|
|
303
|
+
parsed = urlparse(target)
|
|
304
|
+
decoded = unquote(parsed.path)
|
|
305
|
+
|
|
306
|
+
# Compile the URL path into verbs using POSIX semantics.
|
|
307
|
+
url_verbs = compile_to_fspathverbs(decoded, posixpath.split)
|
|
308
|
+
|
|
309
|
+
# Build a relative component list from the verbs.
|
|
310
|
+
rel_components = [] # type: List[str]
|
|
311
|
+
for verb in url_verbs:
|
|
312
|
+
if isinstance(verb, Root):
|
|
313
|
+
rel_components = []
|
|
314
|
+
elif isinstance(verb, Parent):
|
|
315
|
+
if rel_components:
|
|
316
|
+
rel_components.pop()
|
|
317
|
+
elif isinstance(verb, Child):
|
|
318
|
+
rel_components.append(verb.child)
|
|
319
|
+
# Current is a no-op
|
|
320
|
+
|
|
321
|
+
# Join with cwd to get the filesystem path.
|
|
322
|
+
if rel_components:
|
|
323
|
+
path = os.path.join(cwd, *rel_components)
|
|
324
|
+
else:
|
|
325
|
+
path = cwd
|
|
326
|
+
|
|
327
|
+
if not os.path.exists(path):
|
|
328
|
+
write_error(wf, 404)
|
|
329
|
+
else:
|
|
330
|
+
serve_path(wf, method, path, target)
|
|
331
|
+
|
|
332
|
+
return Decision.ABORT
|
|
333
|
+
|
|
334
|
+
def on_body(reader):
|
|
335
|
+
pass
|
|
336
|
+
|
|
337
|
+
try:
|
|
338
|
+
parse_http_1_1_requests(rf, on_headers=on_headers, on_body=on_body)
|
|
339
|
+
except ParserError as e:
|
|
340
|
+
write_error(wf, 400)
|
|
341
|
+
except Exception as e:
|
|
342
|
+
logging.exception(e)
|
|
343
|
+
write_error(wf, 500)
|
|
344
|
+
finally:
|
|
345
|
+
rf.close()
|
|
346
|
+
wf.close()
|
|
347
|
+
sock.close()
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def main():
|
|
351
|
+
# type: () -> None
|
|
352
|
+
"""Entry point for ``python -m httpserver``."""
|
|
353
|
+
parser = argparse.ArgumentParser(
|
|
354
|
+
description="Simple HTTP server using httppackets (http.server lookalike).",
|
|
355
|
+
)
|
|
356
|
+
parser.add_argument(
|
|
357
|
+
"--port",
|
|
358
|
+
"-p",
|
|
359
|
+
type=int,
|
|
360
|
+
default=DEFAULT_PORT,
|
|
361
|
+
help="port to listen on",
|
|
362
|
+
)
|
|
363
|
+
parser.add_argument(
|
|
364
|
+
"--bind",
|
|
365
|
+
"-b",
|
|
366
|
+
type=str,
|
|
367
|
+
default=DEFAULT_BIND,
|
|
368
|
+
help="address to bind to",
|
|
369
|
+
)
|
|
370
|
+
args = parser.parse_args()
|
|
371
|
+
|
|
372
|
+
logging.basicConfig(
|
|
373
|
+
format="%(message)s",
|
|
374
|
+
level=logging.INFO,
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
bind = args.bind
|
|
378
|
+
port = args.port
|
|
379
|
+
|
|
380
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
381
|
+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
382
|
+
sock.bind((bind, port))
|
|
383
|
+
sock.listen(128)
|
|
384
|
+
|
|
385
|
+
logging.info(
|
|
386
|
+
"Serving HTTP on http://%s:%d/ ...",
|
|
387
|
+
bind,
|
|
388
|
+
port,
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
try:
|
|
392
|
+
while True:
|
|
393
|
+
client_sock, client_addr = sock.accept()
|
|
394
|
+
handle_connection(client_sock)
|
|
395
|
+
except KeyboardInterrupt:
|
|
396
|
+
logging.info("Keyboard interrupt received, shutting down.")
|
|
397
|
+
finally:
|
|
398
|
+
sock.close()
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
if __name__ == "__main__":
|
|
402
|
+
main()
|