1import os 2import socket 3 4import pytest 5from unit.applications.proto import TestApplicationProto 6from unit.utils import waitforfiles 7 8 9class TestStatic(TestApplicationProto): 10 prerequisites = {} 11 12 @pytest.fixture(autouse=True) 13 def setup_method_fixture(self, temp_dir): 14 os.makedirs(f'{temp_dir}/assets/dir') 15 assets_dir = f'{temp_dir}/assets' 16 17 with open(f'{assets_dir}/index.html', 'w') as index, open( 18 f'{assets_dir}/README', 'w' 19 ) as readme, open(f'{assets_dir}/log.log', 'w') as log, open( 20 f'{assets_dir}/dir/file', 'w' 21 ) as file: 22 index.write('0123456789') 23 readme.write('readme') 24 log.write('[debug]') 25 file.write('blah') 26 27 self._load_conf( 28 { 29 "listeners": {"*:7080": {"pass": "routes"}}, 30 "routes": [{"action": {"share": f'{assets_dir}$uri'}}], 31 "settings": { 32 "http": { 33 "static": { 34 "mime_types": {"text/plain": [".log", "README"]} 35 } 36 } 37 }, 38 } 39 ) 40 41 def test_static_index(self, temp_dir): 42 def set_index(index): 43 assert 'success' in self.conf( 44 {"share": f'{temp_dir}/assets$uri', "index": index}, 45 'routes/0/action', 46 ), 'configure index' 47 48 set_index('README') 49 assert self.get()['body'] == 'readme', 'index' 50 51 self.conf_delete('routes/0/action/index') 52 assert self.get()['body'] == '0123456789', 'delete index' 53 54 set_index('') 55 assert self.get()['status'] == 404, 'index empty' 56 57 def test_static_index_default(self): 58 assert self.get(url='/index.html')['body'] == '0123456789', 'index' 59 assert self.get(url='/')['body'] == '0123456789', 'index 2' 60 assert self.get(url='//')['body'] == '0123456789', 'index 3' 61 assert self.get(url='/.')['body'] == '0123456789', 'index 4' 62 assert self.get(url='/./')['body'] == '0123456789', 'index 5' 63 assert self.get(url='/?blah')['body'] == '0123456789', 'index vars' 64 assert self.get(url='/#blah')['body'] == '0123456789', 'index anchor' 65 assert self.get(url='/dir/')['status'] == 404, 'index not found' 66 67 resp = self.get(url='/index.html/') 68 assert resp['status'] == 404, 'index not found 2 status' 69 assert ( 70 resp['headers']['Content-Type'] == 'text/html' 71 ), 'index not found 2 Content-Type' 72 73 def test_static_index_invalid(self, skip_alert, temp_dir): 74 skip_alert(r'failed to apply new conf') 75 76 def check_index(index): 77 assert 'error' in self.conf( 78 {"share": f'{temp_dir}/assets$uri', "index": index}, 79 'routes/0/action', 80 ) 81 82 check_index({}) 83 check_index(['index.html', '$blah']) 84 85 def test_static_large_file(self, temp_dir): 86 file_size = 32 * 1024 * 1024 87 with open(f'{temp_dir}/assets/large', 'wb') as f: 88 f.seek(file_size - 1) 89 f.write(b'\0') 90 91 assert ( 92 len(self.get(url='/large', read_buffer_size=1024 * 1024)['body']) 93 == file_size 94 ), 'large file' 95 96 def test_static_etag(self, temp_dir): 97 etag = self.get(url='/')['headers']['ETag'] 98 etag_2 = self.get(url='/README')['headers']['ETag'] 99 100 assert etag != etag_2, 'different ETag' 101 assert etag == self.get(url='/')['headers']['ETag'], 'same ETag' 102 103 with open(f'{temp_dir}/assets/index.html', 'w') as f: 104 f.write('blah') 105 106 assert etag != self.get(url='/')['headers']['ETag'], 'new ETag' 107 108 def test_static_redirect(self): 109 resp = self.get(url='/dir') 110 assert resp['status'] == 301, 'redirect status' 111 assert resp['headers']['Location'] == '/dir/', 'redirect Location' 112 assert 'Content-Type' not in resp['headers'], 'redirect Content-Type' 113 114 def test_static_space_in_name(self, temp_dir): 115 assets_dir = f'{temp_dir}/assets' 116 117 os.rename( 118 f'{assets_dir}/dir/file', 119 f'{assets_dir}/dir/fi le', 120 ) 121 assert waitforfiles(f'{assets_dir}/dir/fi le') 122 assert self.get(url='/dir/fi le')['body'] == 'blah', 'file name' 123 124 os.rename(f'{assets_dir}/dir', f'{assets_dir}/di r') 125 assert waitforfiles(f'{assets_dir}/di r/fi le') 126 assert self.get(url='/di r/fi le')['body'] == 'blah', 'dir name' 127 128 os.rename(f'{assets_dir}/di r', f'{assets_dir}/ di r ') 129 assert waitforfiles(f'{assets_dir}/ di r /fi le') 130 assert ( 131 self.get(url='/ di r /fi le')['body'] == 'blah' 132 ), 'dir name enclosing' 133 134 assert ( 135 self.get(url='/%20di%20r%20/fi le')['body'] == 'blah' 136 ), 'dir encoded' 137 assert ( 138 self.get(url='/ di r %2Ffi le')['body'] == 'blah' 139 ), 'slash encoded' 140 assert self.get(url='/ di r /fi%20le')['body'] == 'blah', 'file encoded' 141 assert ( 142 self.get(url='/%20di%20r%20%2Ffi%20le')['body'] == 'blah' 143 ), 'encoded' 144 assert ( 145 self.get(url='/%20%64%69%20%72%20%2F%66%69%20%6C%65')['body'] 146 == 'blah' 147 ), 'encoded 2' 148 149 os.rename( 150 f'{assets_dir}/ di r /fi le', 151 f'{assets_dir}/ di r / fi le ', 152 ) 153 assert waitforfiles(f'{assets_dir}/ di r / fi le ') 154 assert ( 155 self.get(url='/%20di%20r%20/%20fi%20le%20')['body'] == 'blah' 156 ), 'file name enclosing' 157 158 try: 159 open(f'{temp_dir}/ф а', 'a').close() 160 utf8 = True 161 162 except KeyboardInterrupt: 163 raise 164 165 except: 166 utf8 = False 167 168 if utf8: 169 os.rename( 170 f'{assets_dir}/ di r / fi le ', 171 f'{assets_dir}/ di r /фа йл', 172 ) 173 assert waitforfiles(f'{assets_dir}/ di r /фа йл') 174 assert ( 175 self.get(url='/ di r /фа йл')['body'] == 'blah' 176 ), 'file name 2' 177 178 os.rename( 179 f'{assets_dir}/ di r ', 180 f'{assets_dir}/ди ректория', 181 ) 182 assert waitforfiles(f'{assets_dir}/ди ректория/фа йл') 183 assert ( 184 self.get(url='/ди ректория/фа йл')['body'] == 'blah' 185 ), 'dir name 2' 186 187 def test_static_unix_socket(self, temp_dir): 188 sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 189 sock.bind(f'{temp_dir}/assets/unix_socket') 190 191 assert self.get(url='/unix_socket')['status'] == 404, 'socket' 192 193 sock.close() 194 195 def test_static_unix_fifo(self, temp_dir): 196 os.mkfifo(f'{temp_dir}/assets/fifo') 197 198 assert self.get(url='/fifo')['status'] == 404, 'fifo' 199 200 def test_static_method(self): 201 resp = self.head() 202 assert resp['status'] == 200, 'HEAD status' 203 assert resp['body'] == '', 'HEAD empty body' 204 205 assert self.delete()['status'] == 405, 'DELETE' 206 assert self.post()['status'] == 405, 'POST' 207 assert self.put()['status'] == 405, 'PUT' 208 209 def test_static_path(self): 210 assert self.get(url='/dir/../dir/file')['status'] == 200, 'relative' 211 212 assert self.get(url='./')['status'] == 400, 'path invalid' 213 assert self.get(url='../')['status'] == 400, 'path invalid 2' 214 assert self.get(url='/..')['status'] == 400, 'path invalid 3' 215 assert self.get(url='../assets/')['status'] == 400, 'path invalid 4' 216 assert self.get(url='/../assets/')['status'] == 400, 'path invalid 5' 217 218 def test_static_two_clients(self): 219 sock = self.get(no_recv=True) 220 sock2 = self.get(no_recv=True) 221 222 assert sock.recv(1) == b'H', 'client 1' 223 assert sock2.recv(1) == b'H', 'client 2' 224 assert sock.recv(1) == b'T', 'client 1 again' 225 assert sock2.recv(1) == b'T', 'client 2 again' 226 227 sock.close() 228 sock2.close() 229 230 def test_static_mime_types(self): 231 assert 'success' in self.conf( 232 { 233 "text/x-code/x-blah/x-blah": "readme", 234 "text/plain": [".html", ".log", "file"], 235 }, 236 'settings/http/static/mime_types', 237 ), 'configure mime_types' 238 239 assert ( 240 self.get(url='/README')['headers']['Content-Type'] 241 == 'text/x-code/x-blah/x-blah' 242 ), 'mime_types string case insensitive' 243 assert ( 244 self.get(url='/index.html')['headers']['Content-Type'] 245 == 'text/plain' 246 ), 'mime_types html' 247 assert ( 248 self.get(url='/')['headers']['Content-Type'] == 'text/plain' 249 ), 'mime_types index default' 250 assert ( 251 self.get(url='/dir/file')['headers']['Content-Type'] == 'text/plain' 252 ), 'mime_types file in dir' 253 254 def test_static_mime_types_partial_match(self): 255 assert 'success' in self.conf( 256 { 257 "text/x-blah": ["ile", "fil", "f", "e", ".file"], 258 }, 259 'settings/http/static/mime_types', 260 ), 'configure mime_types' 261 assert 'Content-Type' not in self.get(url='/dir/file'), 'partial match' 262 263 def test_static_mime_types_reconfigure(self): 264 assert 'success' in self.conf( 265 { 266 "text/x-code": "readme", 267 "text/plain": [".html", ".log", "file"], 268 }, 269 'settings/http/static/mime_types', 270 ), 'configure mime_types' 271 272 assert self.conf_get('settings/http/static/mime_types') == { 273 'text/x-code': 'readme', 274 'text/plain': ['.html', '.log', 'file'], 275 }, 'mime_types get' 276 assert ( 277 self.conf_get('settings/http/static/mime_types/text%2Fx-code') 278 == 'readme' 279 ), 'mime_types get string' 280 assert self.conf_get( 281 'settings/http/static/mime_types/text%2Fplain' 282 ) == ['.html', '.log', 'file'], 'mime_types get array' 283 assert ( 284 self.conf_get('settings/http/static/mime_types/text%2Fplain/1') 285 == '.log' 286 ), 'mime_types get array element' 287 288 assert 'success' in self.conf_delete( 289 'settings/http/static/mime_types/text%2Fplain/2' 290 ), 'mime_types remove array element' 291 assert ( 292 'Content-Type' not in self.get(url='/dir/file')['headers'] 293 ), 'mime_types removed' 294 295 assert 'success' in self.conf_post( 296 '"file"', 'settings/http/static/mime_types/text%2Fplain' 297 ), 'mime_types add array element' 298 assert ( 299 self.get(url='/dir/file')['headers']['Content-Type'] == 'text/plain' 300 ), 'mime_types reverted' 301 302 assert 'success' in self.conf( 303 '"file"', 'settings/http/static/mime_types/text%2Fplain' 304 ), 'configure mime_types update' 305 assert ( 306 self.get(url='/dir/file')['headers']['Content-Type'] == 'text/plain' 307 ), 'mime_types updated' 308 assert ( 309 'Content-Type' not in self.get(url='/log.log')['headers'] 310 ), 'mime_types updated 2' 311 312 assert 'success' in self.conf( 313 '".log"', 'settings/http/static/mime_types/text%2Fblahblahblah' 314 ), 'configure mime_types create' 315 assert ( 316 self.get(url='/log.log')['headers']['Content-Type'] 317 == 'text/blahblahblah' 318 ), 'mime_types create' 319 320 def test_static_mime_types_correct(self): 321 assert 'error' in self.conf( 322 {"text/x-code": "readme", "text/plain": "readme"}, 323 'settings/http/static/mime_types', 324 ), 'mime_types same extensions' 325 assert 'error' in self.conf( 326 {"text/x-code": [".h", ".c"], "text/plain": ".c"}, 327 'settings/http/static/mime_types', 328 ), 'mime_types same extensions array' 329 assert 'error' in self.conf( 330 { 331 "text/x-code": [".h", ".c", "readme"], 332 "text/plain": "README", 333 }, 334 'settings/http/static/mime_types', 335 ), 'mime_types same extensions case insensitive' 336 337 @pytest.mark.skip('not yet') 338 def test_static_mime_types_invalid(self, temp_dir): 339 assert 'error' in self.http( 340 b"""PUT /config/settings/http/static/mime_types/%0%00% HTTP/1.1\r 341Host: localhost\r 342Connection: close\r 343Content-Length: 6\r 344\r 345\"blah\"""", 346 raw_resp=True, 347 raw=True, 348 sock_type='unix', 349 addr=f'{temp_dir}/control.unit.sock', 350 ), 'mime_types invalid' 351