-
Notifications
You must be signed in to change notification settings - Fork 3
/
testserver.py
executable file
·453 lines (361 loc) · 16.3 KB
/
testserver.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
#!/usr/bin/env python
# This file provides unit tests to verify the correctness of the server at
# pdfjs.robwu.nl. To run local tests, Nginx must be installed.
# The local test expects localhost.crt and localhost.key to exist in the
# directory containing this script. If these are unavailable, they are
# generated on the fly using openssl (which should then be installed).
#
# To run the tests:
# ./testserver.py TestHttp TestHttps # Run test against local Nginx server.
# ./testserver.py TestProd # Run test against pdfjs.robwu.nl
import atexit
import os
import re
import signal
import ssl
import shutil
import subprocess
import tempfile
import time
import unittest
try:
# Py3
from http.client import BadStatusLine
from urllib.request import HTTPHandler, HTTPSHandler, UnknownHandler
from urllib.request import OpenerDirector, Request
from urllib.error import URLError
except ImportError:
# Py2
from httplib import BadStatusLine
from urllib2 import HTTPHandler, HTTPSHandler, UnknownHandler
from urllib2 import OpenerDirector, Request
from urllib2 import URLError
def get_http_status(url, **kwargs):
return get_http_response(url, **kwargs).getcode()
def get_http_response(url, **kwargs):
req = Request(url,
data=kwargs.get('data', None),
headers=kwargs.get('headers', {}))
context = None
if url.startswith('https://localhost'):
if get_http_response._shouldWarnAboutCertValidation:
get_http_response._shouldWarnAboutCertValidation = False
print('Warning: Disabling certificate validation for localhost')
context = ssl._create_unverified_context()
# Use OpenerDirector instead of urlopen(req, context=context) to make sure
# that we can send requests without automatically following redirects, and
# that non-2xx status codes won't raise HTTPError.
opener = OpenerDirector()
opener.add_handler(HTTPHandler())
opener.add_handler(HTTPSHandler(context=context))
opener.add_handler(UnknownHandler())
return opener.open(req)
get_http_response._shouldWarnAboutCertValidation = True
def good_headers():
'''A dictionary of expected good headers.'''
return {
'user-agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.94 Safari/537.36 = testserver.py', # NOQA
'deduplication-id': '0123456789',
'extension-version': '0',
}
class LocalServer:
instance = None
# Let's hard-code them for now.
http_port = 8088
https_port = 8443
server_proc = None
nginx_root_path = ''
nginx_prefix_path = ''
nginx_log_path = ''
@staticmethod
def Get():
if not LocalServer.instance:
LocalServer.instance = LocalServer()
LocalServer.instance.start_server()
return LocalServer.instance
def _create_nginx_root_content(self):
'''
Create a temporary directory containing the the Nginx server's config.
'''
def src_path(x): return os.path.join(os.path.dirname(__file__), x)
with open(src_path('nl.robwu.pdfjs.conf')) as f:
server_conf = f.read()
for count, needle, replacement in [
# Listen on localhost only.
[0, '(?= listen )', ' #'],
[1, '(?=# listen .*80;)',
'listen 127.0.0.1:%d; ' % self.http_port],
[1, '(?=# listen .*443 ssl;)',
'listen 127.0.0.1:%d ssl; ' % self.https_port],
# Self-signed test certificates, e.g. via
# openssl req -x509 -newkey rsa:2048 -keyout localhost.key -out localhost.crt -nodes -sha256 -subj '/CN=localhost' # NOQA
[1, '( ssl_certificate) .+;', r'\1 localhost.crt;'],
[1, '( ssl_certificate_key) .+;', r'\1 localhost.key;'],
# Not really needed because the test server doesn't have OCSP,
# but Nginx checks whether the file exists, so rename it.
[1, '( ssl_trusted_certificate) .+;', r'\1 localhost.crt;'],
# Write to a temporary access log instead of a system destination.
[1, '( access_log) /[^ ]+', r'\1 localhost.log'],
# Without this, it takes 5 seconds before the logs are flushed.
# Also, when worker processes are used, this also slows down
# termination by 5s (because workers wait until the sockets are
# closed upon a graceful shutdown).
[1, 'server {', 'server { lingering_close off;'],
]:
rneedle = re.compile(needle)
found = rneedle.findall(server_conf)
if not found:
raise ValueError('Not found in source: %s' % needle)
if count and len(found) < count:
raise ValueError('Expected %d, but got %d for: %s' % (
count, len(found), needle))
server_conf = re.sub(
rneedle, replacement, server_conf, count=count)
# Generation succeeded, create files.
nginx_root = tempfile.mkdtemp(prefix='nginx_test_server')
with open(os.path.join(nginx_root, 'nl.robwu.pdfjs.conf'), 'w') as f:
f.write(server_conf)
if not os.path.isfile(src_path('localhost.key')):
subprocess.call([
'openssl', 'req', '-x509',
'-newkey', 'rsa:2048',
'-keyout', 'localhost.key',
'-out', 'localhost.crt',
'-nodes',
'-sha256',
'-subj', '/CN=localhost'
], cwd=src_path('.'))
for filename in [
'nginx.conf',
# For testing, these are actually self-signed certificates.
'localhost.crt',
'localhost.key',
]:
shutil.copyfile(src_path(filename),
os.path.join(nginx_root, filename))
prefix_path = os.path.join(nginx_root, 'prefix')
os.mkdir(prefix_path)
# Required by nginx, "temp" as specific in nginx.conf
os.mkdir(os.path.join(prefix_path, 'temp'))
self.nginx_root_path = nginx_root
self.nginx_prefix_path = prefix_path
def start_server(self):
'''
Start a Nginx server at the given ports.
'''
if not self.nginx_root_path:
self._create_nginx_root_content()
self.nginx_log_path = '%s/localhost.log' % self.nginx_prefix_path
print('Starting nginx server at %s' % self.nginx_root_path)
self.server_proc = subprocess.Popen([
'nginx',
'-p', self.nginx_prefix_path,
'-c', os.path.join(self.nginx_root_path, 'nginx.conf'),
'-g', 'daemon off; master_process off;',
])
atexit.register(self.stop_server)
# Wait until the server has started (at most a few seconds)
for i in range(0, 10):
try:
get_http_status('http://localhost:%d' % self.http_port)
return # Request succeeded, server started.
except URLError:
time.sleep(0.2)
def stop_server(self):
self.server_proc.send_signal(signal.SIGQUIT)
self.server_proc.wait()
self.server_proc = None
def get_http_base_url(self):
return 'http://localhost:%d' % self.http_port
def get_https_base_url(self):
return 'https://localhost:%d' % self.https_port
def get_log_content(self):
# Assume that the logs have been written.
# We have set lingering_close to "off", and from Python's side (urllib)
# the request has ended, so the log should immediately be flushed.
with open(self.nginx_log_path, 'r') as f:
return f.read()
class TestHttpBase(object):
'''
These tests check whether the response from the logging server is OK.
If the response is 204, it's assumed that an entry is written to the log,
but the tests do NOT check whether the log is actually written.
'''
@classmethod
def setUpClass(cls):
cls.base_url = cls.get_base_url()
# Check whether we can connect before running all other tests.
get_http_status(cls.base_url)
def assertStatus(self, expected_status, path, **kwargs):
http_method = 'POST' if 'data' in kwargs else 'GET'
status = get_http_status(self.base_url + path, **kwargs)
msg = 'Expected %d but got %d for %s %s' % (
expected_status, status, http_method, path)
self.assertEqual(expected_status, status, msg)
def test_non_existing_404(self):
self.assertStatus(404, '/.well-known/')
self.assertStatus(404, '/favicon.ico')
# Actually, robots.txt is supported to avoid getting crawled.
self.assertStatus(200, '/robots.txt')
self.assertStatus(404, '/.well-known/', data=b'')
def test_root_index_response(self):
self.assertStatus(302, '/')
res = get_http_response(self.base_url + '/')
self.assertEqual(
res.headers['location'],
'https://github.com/mozilla/pdf.js/wiki/PDF-Viewer-(Chrome-extension)', # NOQA
'Expected redirect target at /'
)
# Redirect is independent of HTTP method, also for POST:
self.assertStatus(302, '/', data=b'')
def test_logging_invalid_method(self):
self.assertStatus(405, '/logpdfjs')
def test_logging_invalid_body(self):
# Nginx doesn't allow us to disable the body, so the config accepts
# length 1. Requests with bodies of size 2 should be rejected though.
self.assertStatus(413, '/logpdfjs', data=b'12')
def test_logging_valid_headers(self):
self.assertStatus(204, '/logpdfjs', data=b'', headers=good_headers())
def test_logging_invalid_headers(self):
self.assertStatus(400, '/logpdfjs', data=b'', headers={})
headers = good_headers()
del headers['deduplication-id']
self.assertStatus(400, '/logpdfjs', data=b'', headers=headers)
# Can't test a missing User-Agent header because urllib always adds it.
# Let's assume that Nginx doesn't blow up when it is missing...
headers = good_headers()
del headers['extension-version']
self.assertStatus(400, '/logpdfjs', data=b'', headers=headers)
def test_logging_valid_deduplication_id(self):
headers = good_headers()
headers['deduplication-id'] = '0123abcdef'
self.assertStatus(204, '/logpdfjs', data=b'', headers=headers)
def test_logging_invalid_deduplication_id(self):
# Note that the last character in Deduplication-Id is an uppercase 'F'.
headers = good_headers()
headers['deduplication-id'] = '012345678F'
self.assertStatus(400, '/logpdfjs', data=b'', headers=headers)
headers = good_headers()
headers['deduplication-id'] = '012345678g'
self.assertStatus(400, '/logpdfjs', data=b'', headers=headers)
# Too short
headers = good_headers()
headers['deduplication-id'] = '012345678'
self.assertStatus(400, '/logpdfjs', data=b'', headers=headers)
# Too long
headers = good_headers()
headers['deduplication-id'] = '0123456789a'
self.assertStatus(400, '/logpdfjs', data=b'', headers=headers)
def test_logging_valid_user_agent(self):
# Minimal allowed
headers = good_headers()
headers['user-agent'] = 'a'
self.assertStatus(204, '/logpdfjs', data=b'', headers=headers)
# Maximum allowed.
headers = good_headers()
headers['user-agent'] = 'a' * 1000
self.assertStatus(204, '/logpdfjs', data=b'', headers=headers)
def test_logging_invalid_user_agent(self):
# Too short
headers = good_headers()
headers['user-agent'] = ''
self.assertStatus(400, '/logpdfjs', data=b'', headers=headers)
# Too long
headers = good_headers()
headers['user-agent'] = 'a' * 1001
self.assertStatus(400, '/logpdfjs', data=b'', headers=headers)
headers = good_headers()
headers['user-agent'] = 'x\x00'
self.assertStatus(400, '/logpdfjs', data=b'', headers=headers)
def test_logging_valid_extension_version(self):
headers = good_headers()
headers['extension-version'] = '0'
self.assertStatus(204, '/logpdfjs', data=b'', headers=headers)
headers = good_headers()
headers['extension-version'] = '0.0.0.0'
self.assertStatus(204, '/logpdfjs', data=b'', headers=headers)
headers = good_headers()
headers['extension-version'] = '65535.65535.65535.65535'
self.assertStatus(204, '/logpdfjs', data=b'', headers=headers)
def test_logging_invalid_extension_version(self):
headers = good_headers()
headers['extension-version'] = ''
self.assertStatus(400, '/logpdfjs', data=b'', headers=headers)
headers = good_headers()
headers['extension-version'] = '.'
self.assertStatus(400, '/logpdfjs', data=b'', headers=headers)
headers = good_headers()
headers['extension-version'] = '0.'
self.assertStatus(400, '/logpdfjs', data=b'', headers=headers)
headers = good_headers()
headers['extension-version'] = '.0'
self.assertStatus(400, '/logpdfjs', data=b'', headers=headers)
headers = good_headers()
headers['extension-version'] = '65536'
self.assertStatus(400, '/logpdfjs', data=b'', headers=headers)
def test_extension_version_pattern(self):
# Copy-paste of the numeric regex in the nginx config.
regex = '^([0-5]?[0-9]{1,4}|6([0-4][0-9]{3}|5([0-4][0-9]{2}|5([0-2][0-9]|3[0-5]))))$' # NOQA
pattern = re.compile(regex, re.DOTALL)
for i in range(0, 0xFFFF + 1):
self.assertTrue(re.match(pattern, str(i)),
'%d should match the version pattern!' % i)
self.assertFalse(re.match(pattern, str(0xFFFF + 1)),
'0xFFFF+1 should not match the version pattern!')
class TestLocalBase(object):
'''
Tests specific to a local Nginx instance.
'''
def test_did_write_log(self):
old_log = LocalServer.Get().get_log_content()
headers = good_headers()
headers['extension-version'] = '1337'
self.assertStatus(204, '/logpdfjs', data=b'', headers=headers)
new_log = LocalServer.Get().get_log_content()
new_log = new_log[len(old_log):]
self.assertNotEqual(new_log, '', 'Expected a new log entry.')
self.assertEqual(new_log, '0123456789 1337 "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.94 Safari/537.36 = testserver.py"\n') # NOQA
def test_did_not_write_log(self):
old_log = LocalServer.Get().get_log_content()
headers = good_headers()
headers['extension-version'] = ''
self.assertStatus(400, '/logpdfjs', data=b'', headers=headers)
new_log = LocalServer.Get().get_log_content()
new_log = new_log[len(old_log):]
self.assertEqual(new_log, '', 'Expected a new log entry.')
class TestHttp(TestHttpBase, TestLocalBase, unittest.TestCase):
@staticmethod
def get_base_url():
return LocalServer.Get().get_http_base_url()
class TestHttps(TestHttpBase, TestLocalBase, unittest.TestCase):
@staticmethod
def get_base_url():
return LocalServer.Get().get_https_base_url()
class TestProd(TestHttpBase, unittest.TestCase):
@staticmethod
def get_base_url():
return 'https://pdfjs.robwu.nl'
def test_bad_host(self):
headers = good_headers()
headers['host'] = 'not.pdfjs.robwu.nl'
try:
status = get_http_status(self.base_url + '/logpdfjs', data=b'',
headers=headers)
self.assertNotEqual(204, status)
except BadStatusLine:
pass # Connection closed, OK!
def test_http(self):
status = get_http_status('http://pdfjs.robwu.nl/logpdfjs',
data=b'', headers=good_headers())
self.assertEqual(204, status)
def test_http_bad_host(self):
headers = good_headers()
headers['host'] = 'not.pdfjs.robwu.nl'
try:
status = get_http_status('http://pdfjs.robwu.nl/logpdfjs',
data=b'', headers=headers)
self.assertNotEqual(204, status)
except BadStatusLine:
pass # Connection closed, OK!
if __name__ == '__main__':
unittest.main()