Skip to content

Commit 094e30d

Browse files
Allow access_log format override by web plugins (#733)
* Return DEFAULT_404_RESPONSE by default from static server when file doesnt exist * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix web server with proxy test Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 684c0d4 commit 094e30d

File tree

4 files changed

+72
-57
lines changed

4 files changed

+72
-57
lines changed

proxy/common/constants.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,13 +81,14 @@ def _env_threadless_compliant() -> bool:
8181
DEFAULT_LOG_FILE = None
8282
DEFAULT_LOG_FORMAT = '%(asctime)s - pid:%(process)d [%(levelname)-.1s] %(module)s.%(funcName)s:%(lineno)d - %(message)s'
8383
DEFAULT_LOG_LEVEL = 'INFO'
84+
DEFAULT_WEB_ACCESS_LOG_FORMAT = '{client_addr} - {request_method} {request_path} - {connection_time_ms}ms'
8485
DEFAULT_HTTP_ACCESS_LOG_FORMAT = '{client_ip}:{client_port} - ' + \
8586
'{request_method} {server_host}:{server_port}{request_path} - ' + \
8687
'{response_code} {response_reason} - {response_bytes} bytes - ' + \
87-
'{connection_time_ms} ms'
88+
'{connection_time_ms}ms'
8889
DEFAULT_HTTPS_ACCESS_LOG_FORMAT = '{client_ip}:{client_port} - ' + \
8990
'{request_method} {server_host}:{server_port} - ' + \
90-
'{response_bytes} bytes - {connection_time_ms} ms'
91+
'{response_bytes} bytes - {connection_time_ms}ms'
9192
DEFAULT_NUM_ACCEPTORS = 0
9293
DEFAULT_NUM_WORKERS = 0
9394
DEFAULT_OPEN_FILE_LIMIT = 1024

proxy/http/server/plugin.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import argparse
1313

1414
from abc import ABC, abstractmethod
15-
from typing import List, Tuple
15+
from typing import Any, Dict, List, Optional, Tuple
1616
from uuid import UUID
1717

1818
from ..websocket import WebsocketFrame
@@ -111,3 +111,13 @@ def on_websocket_message(self, frame: WebsocketFrame) -> None:
111111
# def on_websocket_close(self) -> None:
112112
# """Called when websocket connection has been closed."""
113113
# raise NotImplementedError() # pragma: no cover
114+
115+
def on_access_log(self, context: Dict[str, Any]) -> Optional[Dict[str, Any]]:
116+
"""Use this method to override default access log format (see
117+
DEFAULT_WEB_ACCESS_LOG_FORMAT) or to add/update/modify passed context
118+
for usage by default access logger.
119+
120+
Return updated log context to use for default logging format, OR
121+
Return None if plugin has logged the request.
122+
"""
123+
return context

proxy/http/server/web.py

Lines changed: 54 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
from ...common.constants import DEFAULT_STATIC_SERVER_DIR, PROXY_AGENT_HEADER_VALUE
2121
from ...common.constants import DEFAULT_ENABLE_STATIC_SERVER, DEFAULT_ENABLE_WEB_SERVER
22-
from ...common.constants import DEFAULT_MIN_COMPRESSION_LIMIT
22+
from ...common.constants import DEFAULT_MIN_COMPRESSION_LIMIT, DEFAULT_WEB_ACCESS_LOG_FORMAT
2323
from ...common.utils import bytes_, text_, build_http_response, build_websocket_handshake_response
2424
from ...common.types import Readables, Writables
2525
from ...common.flag import flags
@@ -79,6 +79,7 @@ class HttpWebServerPlugin(HttpProtocolHandlerPlugin):
7979
reason=b'NOT FOUND',
8080
headers={
8181
b'Server': PROXY_AGENT_HEADER_VALUE,
82+
b'Content-Length': b'0',
8283
b'Connection': b'close',
8384
},
8485
),
@@ -90,6 +91,7 @@ class HttpWebServerPlugin(HttpProtocolHandlerPlugin):
9091
reason=b'NOT IMPLEMENTED',
9192
headers={
9293
b'Server': PROXY_AGENT_HEADER_VALUE,
94+
b'Content-Length': b'0',
9395
b'Connection': b'close',
9496
},
9597
),
@@ -129,29 +131,32 @@ def encryption_enabled(self) -> bool:
129131

130132
@staticmethod
131133
def read_and_build_static_file_response(path: str, min_compression_limit: int) -> memoryview:
132-
with open(path, 'rb') as f:
133-
content = f.read()
134-
content_type = mimetypes.guess_type(path)[0]
135-
if content_type is None:
136-
content_type = 'text/plain'
137-
headers = {
138-
b'Content-Type': bytes_(content_type),
139-
b'Cache-Control': b'max-age=86400',
140-
b'Connection': b'close',
141-
}
142-
do_compress = len(content) > min_compression_limit
143-
if do_compress:
144-
headers.update({
145-
b'Content-Encoding': b'gzip',
146-
})
147-
return memoryview(
148-
build_http_response(
149-
httpStatusCodes.OK,
150-
reason=b'OK',
151-
headers=headers,
152-
body=gzip.compress(content) if do_compress else content,
153-
),
154-
)
134+
try:
135+
with open(path, 'rb') as f:
136+
content = f.read()
137+
content_type = mimetypes.guess_type(path)[0]
138+
if content_type is None:
139+
content_type = 'text/plain'
140+
headers = {
141+
b'Content-Type': bytes_(content_type),
142+
b'Cache-Control': b'max-age=86400',
143+
b'Connection': b'close',
144+
}
145+
do_compress = len(content) > min_compression_limit
146+
if do_compress:
147+
headers.update({
148+
b'Content-Encoding': b'gzip',
149+
})
150+
return memoryview(
151+
build_http_response(
152+
httpStatusCodes.OK,
153+
reason=b'OK',
154+
headers=headers,
155+
body=gzip.compress(content) if do_compress else content,
156+
),
157+
)
158+
except FileNotFoundError:
159+
return HttpWebServerPlugin.DEFAULT_404_RESPONSE
155160

156161
def try_upgrade(self) -> bool:
157162
if self.request.has_header(b'connection') and \
@@ -215,16 +220,13 @@ def on_request_complete(self) -> Union[socket.socket, bool]:
215220
# No-route found, try static serving if enabled
216221
if self.flags.enable_static_server:
217222
path = text_(path).split('?')[0]
218-
try:
219-
self.client.queue(
220-
self.read_and_build_static_file_response(
221-
self.flags.static_server_dir + path,
222-
self.flags.min_compression_limit,
223-
),
224-
)
225-
return True
226-
except FileNotFoundError:
227-
pass
223+
self.client.queue(
224+
self.read_and_build_static_file_response(
225+
self.flags.static_server_dir + path,
226+
self.flags.min_compression_limit,
227+
),
228+
)
229+
return True
228230

229231
# Catch all unhandled web server requests, return 404
230232
self.client.queue(self.DEFAULT_404_RESPONSE)
@@ -301,19 +303,26 @@ def on_response_chunk(self, chunk: List[memoryview]) -> List[memoryview]:
301303
def on_client_connection_close(self) -> None:
302304
if self.request.has_host():
303305
return
306+
context = {
307+
'client_addr': self.client.address,
308+
'request_method': text_(self.request.method),
309+
'request_path': text_(self.request.path),
310+
'connection_time_ms': '%.2f' % ((time.time() - self.start_time) * 1000),
311+
}
312+
log_handled = False
304313
if self.route:
314+
# May be merge on_client_connection_close and on_access_log???
315+
# probably by simply deprecating on_client_connection_close in future.
305316
self.route.on_client_connection_close()
306-
self.access_log()
317+
ctx = self.route.on_access_log(context)
318+
if ctx is None:
319+
log_handled = True
320+
else:
321+
context = ctx
322+
if not log_handled:
323+
self.access_log(context)
307324

308325
# TODO: Allow plugins to customize access_log, similar
309326
# to how proxy server plugins are able to do it.
310-
def access_log(self) -> None:
311-
logger.info(
312-
'%s - %s %s - %.2f ms' %
313-
(
314-
self.client.address,
315-
text_(self.request.method),
316-
text_(self.request.path),
317-
(time.time() - self.start_time) * 1000,
318-
),
319-
)
327+
def access_log(self, context: Dict[str, Any]) -> None:
328+
logger.info(DEFAULT_WEB_ACCESS_LOG_FORMAT.format_map(context))

tests/testing/test_embed.py

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@
1616

1717
from proxy import TestCase
1818
from proxy.common.constants import DEFAULT_CLIENT_RECVBUF_SIZE, PROXY_AGENT_HEADER_VALUE
19-
from proxy.common.utils import socket_connection, build_http_request, build_http_response
20-
from proxy.http.parser import httpStatusCodes, httpMethods
19+
from proxy.common.utils import socket_connection, build_http_request
20+
from proxy.http.server import HttpWebServerPlugin
21+
from proxy.http.parser import httpMethods
2122

2223

2324
@unittest.skipIf(os.name == 'nt', 'Disabled for Windows due to weird permission issues.')
@@ -44,13 +45,7 @@ def test_with_proxy(self) -> None:
4445
response = conn.recv(DEFAULT_CLIENT_RECVBUF_SIZE)
4546
self.assertEqual(
4647
response,
47-
build_http_response(
48-
httpStatusCodes.NOT_FOUND, reason=b'NOT FOUND',
49-
headers={
50-
b'Server': PROXY_AGENT_HEADER_VALUE,
51-
b'Connection': b'close',
52-
},
53-
),
48+
HttpWebServerPlugin.DEFAULT_404_RESPONSE.tobytes(),
5449
)
5550

5651
def test_proxy_vcr(self) -> None:

0 commit comments

Comments
 (0)