88"""
99
1010try :
11- from typing import Optional , Dict , Union , Tuple
11+ from typing import Optional , Dict , Union , Tuple , Callable
1212 from socket import socket
1313 from socketpool import SocketPool
1414except ImportError :
1717import os
1818from errno import EAGAIN , ECONNRESET
1919
20+ from .exceptions import (
21+ BackslashInPathError ,
22+ FileNotExistsError ,
23+ ParentDirectoryReferenceError ,
24+ ResponseAlreadySentError ,
25+ )
2026from .mime_type import MIMEType
2127from .request import HTTPRequest
2228from .status import HTTPStatus , CommonHTTPStatus
2329from .headers import HTTPHeaders
2430
2531
32+ def _prevent_multiple_send_calls (function : Callable ):
33+ """
34+ Decorator that prevents calling ``send`` or ``send_file`` more than once.
35+ """
36+
37+ def wrapper (self : "HTTPResponse" , * args , ** kwargs ):
38+ if self ._response_already_sent : # pylint: disable=protected-access
39+ raise ResponseAlreadySentError
40+
41+ result = function (self , * args , ** kwargs )
42+ return result
43+
44+ return wrapper
45+
46+
2647class HTTPResponse :
2748 """
2849 Response to a given `HTTPRequest`. Use in `HTTPServer.route` decorator functions.
@@ -73,8 +94,8 @@ def route_func(request):
7394 """
7495 Defaults to ``text/plain`` if not set.
7596
76- Can be explicitly provided in the constructor, in `send()` or
77- implicitly determined from filename in `send_file()`.
97+ Can be explicitly provided in the constructor, in `` send()` ` or
98+ implicitly determined from filename in `` send_file()` `.
7899
79100 Common MIME types are defined in `adafruit_httpserver.mime_type.MIMEType`.
80101 """
@@ -94,7 +115,7 @@ def __init__( # pylint: disable=too-many-arguments
94115 Sets `status`, ``headers`` and `http_version`
95116 and optionally default ``content_type``.
96117
97- To send the response, call `send` or `send_file`.
118+ To send the response, call `` send`` or `` send_file` `.
98119 For chunked response use
99120 ``with HTTPRequest(request, content_type=..., chunked=True) as r:`` and `send_chunk`.
100121 """
@@ -115,7 +136,7 @@ def _send_headers(
115136 ) -> None :
116137 """
117138 Sends headers.
118- Implicitly called by `send` and `send_file` and in
139+ Implicitly called by `` send`` and `` send_file` ` and in
119140 ``with HTTPResponse(request, chunked=True) as response:`` context manager.
120141 """
121142 headers = self .headers .copy ()
@@ -141,6 +162,7 @@ def _send_headers(
141162 self .request .connection , response_message_header .encode ("utf-8" )
142163 )
143164
165+ @_prevent_multiple_send_calls
144166 def send (
145167 self ,
146168 body : str = "" ,
@@ -152,8 +174,6 @@ def send(
152174
153175 Should be called **only once** per response.
154176 """
155- if self ._response_already_sent :
156- raise RuntimeError ("Response was already sent" )
157177
158178 if getattr (body , "encode" , None ):
159179 encoded_response_message_body = body .encode ("utf-8" )
@@ -167,12 +187,41 @@ def send(
167187 self ._send_bytes (self .request .connection , encoded_response_message_body )
168188 self ._response_already_sent = True
169189
170- def send_file (
190+ @staticmethod
191+ def _check_file_path_is_valid (file_path : str ) -> bool :
192+ """
193+ Checks if ``file_path`` is valid.
194+ If not raises error corresponding to the problem.
195+ """
196+
197+ # Check for backslashes
198+ if "\\ " in file_path : # pylint: disable=anomalous-backslash-in-string
199+ raise BackslashInPathError (file_path )
200+
201+ # Check each component of the path for parent directory references
202+ for part in file_path .split ("/" ):
203+ if part == ".." :
204+ raise ParentDirectoryReferenceError (file_path )
205+
206+ @staticmethod
207+ def _get_file_length (file_path : str ) -> int :
208+ """
209+ Tries to get the length of the file at ``file_path``.
210+ Raises ``FileNotExistsError`` if file does not exist.
211+ """
212+ try :
213+ return os .stat (file_path )[6 ]
214+ except OSError :
215+ raise FileNotExistsError (file_path ) # pylint: disable=raise-missing-from
216+
217+ @_prevent_multiple_send_calls
218+ def send_file ( # pylint: disable=too-many-arguments
171219 self ,
172220 filename : str = "index.html" ,
173221 root_path : str = "./" ,
174222 buffer_size : int = 1024 ,
175223 head_only : bool = False ,
224+ safe : bool = True ,
176225 ) -> None :
177226 """
178227 Send response with content of ``filename`` located in ``root_path``.
@@ -181,25 +230,26 @@ def send_file(
181230
182231 Should be called **only once** per response.
183232 """
184- if self ._response_already_sent :
185- raise RuntimeError ("Response was already sent" )
233+
234+ if safe :
235+ self ._check_file_path_is_valid (filename )
186236
187237 if not root_path .endswith ("/" ):
188238 root_path += "/"
189- try :
190- file_length = os . stat ( root_path + filename )[ 6 ]
191- except OSError :
192- # If the file doesn't exist, return 404.
193- HTTPResponse ( self . request , status = CommonHTTPStatus . NOT_FOUND_404 ). send ()
194- return
239+ if filename . startswith ( "/" ) :
240+ filename = filename [ 1 : ]
241+
242+ full_file_path = root_path + filename
243+
244+ file_length = self . _get_file_length ( full_file_path )
195245
196246 self ._send_headers (
197247 content_type = MIMEType .from_file_name (filename ),
198248 content_length = file_length ,
199249 )
200250
201251 if not head_only :
202- with open (root_path + filename , "rb" ) as file :
252+ with open (full_file_path , "rb" ) as file :
203253 while bytes_read := file .read (buffer_size ):
204254 self ._send_bytes (self .request .connection , bytes_read )
205255 self ._response_already_sent = True
0 commit comments