OLD | NEW |
---|---|
1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. |
3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
5 | 5 |
6 """Constrained Network Server. Serves files with supplied network constraints. | 6 """Constrained Network Server. Serves files with supplied network constraints. |
7 | 7 |
8 The CNS exposes a web based API allowing network constraints to be imposed on | 8 The CNS exposes a web based API allowing network constraints to be imposed on |
9 file serving. | 9 file serving. |
10 | 10 |
11 TODO(dalecurtis): Add some more docs here. | 11 TODO(dalecurtis): Add some more docs here. |
12 | 12 |
13 """ | 13 """ |
14 | 14 |
15 import logging | 15 import logging |
16 from logging import handlers | 16 from logging import handlers |
17 import mimetypes | 17 import mimetypes |
18 import optparse | 18 import optparse |
19 import os | 19 import os |
20 import signal | 20 import signal |
21 import sys | 21 import sys |
22 import threading | 22 import threading |
23 import time | 23 import time |
24 import urllib | |
25 import urllib2 | |
26 | |
24 import traffic_control | 27 import traffic_control |
25 | 28 |
26 try: | 29 try: |
27 import cherrypy | 30 import cherrypy |
28 except ImportError: | 31 except ImportError: |
29 print ('CNS requires CherryPy v3 or higher to be installed. Please install\n' | 32 print ('CNS requires CherryPy v3 or higher to be installed. Please install\n' |
30 'and try again. On Linux: sudo apt-get install python-cherrypy3\n') | 33 'and try again. On Linux: sudo apt-get install python-cherrypy3\n') |
31 sys.exit(1) | 34 sys.exit(1) |
32 | 35 |
33 # Add webm file types to mimetypes map since cherrypy's default type is text. | 36 # Add webm file types to mimetypes map since cherrypy's default type is text. |
(...skipping 160 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
194 if no_cache: | 197 if no_cache: |
195 response = cherrypy.response | 198 response = cherrypy.response |
196 response.headers['Pragma'] = 'no-cache' | 199 response.headers['Pragma'] = 'no-cache' |
197 response.headers['Cache-Control'] = 'no-cache' | 200 response.headers['Cache-Control'] = 'no-cache' |
198 | 201 |
199 # CherryPy is a bit wonky at detecting parameters, so just make them all | 202 # CherryPy is a bit wonky at detecting parameters, so just make them all |
200 # optional and validate them ourselves. | 203 # optional and validate them ourselves. |
201 if not f: | 204 if not f: |
202 raise cherrypy.HTTPError(400, 'Invalid request. File must be specified.') | 205 raise cherrypy.HTTPError(400, 'Invalid request. File must be specified.') |
203 | 206 |
207 # Check existence early to prevent wasted constraint setup. | |
208 self._CheckRequestedFileExist(f) | |
209 | |
210 # If there are no constraints, just serve the file. | |
211 if bandwidth is None and latency is None and loss is None: | |
212 return self._ServeFile(f) | |
213 | |
214 constrained_port = self._GetConstrainedPort( | |
215 f, bandwidth=bandwidth, latency=latency, loss=loss, new_port=new_port, | |
216 **kwargs) | |
217 | |
218 # Build constrained URL using the constrained port and original URL | |
219 # parameters except the network constraints (bandwidth, latency, and loss). | |
220 constrained_url = self._GetServerURL(f, constrained_port, | |
221 no_cache=no_cache, **kwargs) | |
222 | |
223 # Redirect request to the constrained port. | |
224 cherrypy.log('Redirect to %s' % constrained_url) | |
225 cherrypy.lib.cptools.redirect(constrained_url, internal=False) | |
226 | |
227 def _CheckRequestedFileExist(self, f): | |
228 """Checks if the requested file exists, raises HTTPError otherwise.""" | |
229 if self._options.local_server_port: | |
230 self._CheckFileExistOnLocalServer(f) | |
231 else: | |
232 self._CheckFileExistOnServer(f) | |
233 | |
234 def _CheckFileExistOnServer(self, f): | |
235 """Checks if requested file f exists to be served by this server.""" | |
204 # Sanitize and check the path to prevent www-root escapes. | 236 # Sanitize and check the path to prevent www-root escapes. |
205 sanitized_path = os.path.abspath(os.path.join(self._options.www_root, f)) | 237 sanitized_path = os.path.abspath(os.path.join(self._options.www_root, f)) |
206 if not sanitized_path.startswith(self._options.www_root): | 238 if not sanitized_path.startswith(self._options.www_root): |
207 raise cherrypy.HTTPError(403, 'Invalid file requested.') | 239 raise cherrypy.HTTPError(403, 'Invalid file requested.') |
208 | |
209 # Check existence early to prevent wasted constraint setup. | |
210 if not os.path.exists(sanitized_path): | 240 if not os.path.exists(sanitized_path): |
211 raise cherrypy.HTTPError(404, 'File not found.') | 241 raise cherrypy.HTTPError(404, 'File not found.') |
212 | 242 |
213 # If there are no constraints, just serve the file. | 243 def _CheckFileExistOnLocalServer(self, f): |
214 if bandwidth is None and latency is None and loss is None: | 244 """Checks if requested file exists on local server hosting files.""" |
245 test_url = self._GetServerURL(f, self._options.local_server_port) | |
246 try: | |
247 cherrypy.log('Check file exist using URL: %s' % test_url) | |
248 return urllib2.urlopen(test_url) is not None | |
249 except Exception: | |
250 raise cherrypy.HTTPError(404, 'File not found on local server.') | |
251 | |
252 def _ServeFile(self, f): | |
253 """Serves the file as an http response.""" | |
254 if self._options.local_server_port: | |
255 redirect_url = self._GetServerURL(f, self._options.local_server_port) | |
256 cherrypy.log('Redirect to %s' % redirect_url) | |
257 cherrypy.lib.cptools.redirect(redirect_url, internal=False) | |
258 else: | |
259 sanitized_path = os.path.abspath(os.path.join(self._options.www_root, f)) | |
215 return cherrypy.lib.static.serve_file(sanitized_path) | 260 return cherrypy.lib.static.serve_file(sanitized_path) |
216 | 261 |
262 def _GetServerURL(self, f, port, **kwargs): | |
263 """Returns a URL for local server to serve the file on given port. | |
264 | |
265 Args: | |
266 f: file name to serve on local server. Relative to www_root. | |
267 port: Local server port (it can be a configured constrained port). | |
268 kwargs: extra parameteres passed in the URL. | |
269 """ | |
270 base_url = '%s?f=%s' % (cherrypy.url(), f) | |
271 if self._options.local_server_port: | |
272 base_url = '%s/%s' % (cherrypy.url().replace('ServeConstrained', | |
DaleCurtis
2012/08/09 17:28:02
Cleaner as:
base_url = '%s/%s' % (
cherrypy.u
shadi
2012/08/09 18:26:49
The www_root used with local-server-port is relati
DaleCurtis
2012/08/09 18:46:31
You need to update the --options documentation for
| |
273 self._options.www_root), | |
274 f) | |
275 base_url = base_url.replace(':%d' % self._options.port, ':%d' % port) | |
276 extra_args = urllib.urlencode(kwargs) | |
277 if extra_args: | |
278 delimeter = '&' | |
DaleCurtis
2012/08/09 17:28:02
You can probably just roll the &, ? into the if ab
shadi
2012/08/09 18:26:49
Done.
| |
279 if self._options.local_server_port: | |
280 delimeter = '?' | |
281 base_url += '%s%s' % (delimeter, extra_args) | |
282 return base_url | |
283 | |
284 def _GetConstrainedPort(self, f=None, bandwidth=None, latency=None, loss=None, | |
285 new_port=False, **kwargs): | |
286 """Creates or gets a port with specified network constraints. | |
287 | |
288 See ServeConstrained() for more details. | |
289 """ | |
217 # Validate inputs. isdigit() guarantees a natural number. | 290 # Validate inputs. isdigit() guarantees a natural number. |
218 bandwidth = self._ParseIntParameter( | 291 bandwidth = self._ParseIntParameter( |
219 bandwidth, 'Invalid bandwidth constraint.', lambda x: x > 0) | 292 bandwidth, 'Invalid bandwidth constraint.', lambda x: x > 0) |
220 latency = self._ParseIntParameter( | 293 latency = self._ParseIntParameter( |
221 latency, 'Invalid latency constraint.', lambda x: x >= 0) | 294 latency, 'Invalid latency constraint.', lambda x: x >= 0) |
222 loss = self._ParseIntParameter( | 295 loss = self._ParseIntParameter( |
223 loss, 'Invalid loss constraint.', lambda x: x <= 100 and x >= 0) | 296 loss, 'Invalid loss constraint.', lambda x: x <= 100 and x >= 0) |
224 | 297 |
298 redirect_port = self._options.port | |
299 if self._options.local_server_port: | |
300 redirect_port = self._options.local_server_port | |
301 | |
302 start_time = time.time() | |
225 # Allocate a port using the given constraints. If a port with the requested | 303 # Allocate a port using the given constraints. If a port with the requested |
226 # key is already allocated, it will be reused. | 304 # key and kwargs already exist then reuse that port. |
227 # | |
228 # TODO(dalecurtis): The key cherrypy.request.remote.ip might not be unique | |
229 # if build slaves are sharing the same VM. | |
230 start_time = time.time() | |
231 constrained_port = self._port_allocator.Get( | 305 constrained_port = self._port_allocator.Get( |
232 cherrypy.request.remote.ip, server_port=self._options.port, | 306 cherrypy.request.remote.ip, server_port=redirect_port, |
233 interface=self._options.interface, bandwidth=bandwidth, latency=latency, | 307 interface=self._options.interface, bandwidth=bandwidth, latency=latency, |
234 loss=loss, new_port=new_port, file=f, **kwargs) | 308 loss=loss, new_port=new_port, file=f, **kwargs) |
235 end_time = time.time() | 309 |
310 cherrypy.log('Time to set up port %d = %.3fsec.' % | |
311 (constrained_port, time.time() - start_time)) | |
236 | 312 |
237 if not constrained_port: | 313 if not constrained_port: |
238 raise cherrypy.HTTPError(503, 'Service unavailable. Out of ports.') | 314 raise cherrypy.HTTPError(503, 'Service unavailable. Out of ports.') |
239 | 315 return constrained_port |
240 cherrypy.log('Time to set up port %d = %ssec.' % | |
241 (constrained_port, end_time - start_time)) | |
242 | |
243 # Build constrained URL using the constrained port and original URL | |
244 # parameters except the network constraints (bandwidth, latency, and loss). | |
245 constrained_url = '%s?f=%s&no_cache=%s&%s' % ( | |
246 cherrypy.url().replace( | |
247 ':%d' % self._options.port, ':%d' % constrained_port), | |
248 f, | |
249 no_cache, | |
250 '&'.join(['%s=%s' % (key, kwargs[key]) for key in kwargs])) | |
251 | |
252 # Redirect request to the constrained port. | |
253 cherrypy.lib.cptools.redirect(constrained_url, internal=False) | |
254 | 316 |
255 def _ParseIntParameter(self, param, msg, check): | 317 def _ParseIntParameter(self, param, msg, check): |
256 """Returns integer value of param and verifies it satisfies the check. | 318 """Returns integer value of param and verifies it satisfies the check. |
257 | 319 |
258 Args: | 320 Args: |
259 param: Parameter name to check. | 321 param: Parameter name to check. |
260 msg: Message in error if raised. | 322 msg: Message in error if raised. |
261 check: Check to verify the parameter value. | 323 check: Check to verify the parameter value. |
262 | 324 |
263 Returns: | 325 Returns: |
(...skipping 30 matching lines...) Expand all Loading... | |
294 help=('Interface to setup constraints on. Use lo for a ' | 356 help=('Interface to setup constraints on. Use lo for a ' |
295 'local client. Default: %default')) | 357 'local client. Default: %default')) |
296 parser.add_option('--socket-timeout', type='int', | 358 parser.add_option('--socket-timeout', type='int', |
297 default=cherrypy.server.socket_timeout, | 359 default=cherrypy.server.socket_timeout, |
298 help=('Number of seconds before a socket connection times ' | 360 help=('Number of seconds before a socket connection times ' |
299 'out. Default: %default')) | 361 'out. Default: %default')) |
300 parser.add_option('--threads', type='int', | 362 parser.add_option('--threads', type='int', |
301 default=cherrypy._cpserver.Server.thread_pool, | 363 default=cherrypy._cpserver.Server.thread_pool, |
302 help=('Number of threads in the thread pool. Default: ' | 364 help=('Number of threads in the thread pool. Default: ' |
303 '%default')) | 365 '%default')) |
304 parser.add_option('--www-root', default=os.getcwd(), | 366 parser.add_option('--www-root', default='', |
305 help=('Directory root to serve files from. Defaults to the ' | 367 help=('Directory root to serve files from. Defaults to the ' |
306 'current directory: %default')) | 368 'current directory (if --local-server-port is not ' |
369 'used): %s' % os.getcwd())) | |
370 parser.add_option('--local-server-port', type='int', | |
371 help=('Optional local server port to host files.')) | |
307 parser.add_option('-v', '--verbose', action='store_true', default=False, | 372 parser.add_option('-v', '--verbose', action='store_true', default=False, |
308 help='Turn on verbose output.') | 373 help='Turn on verbose output.') |
309 | 374 |
310 options = parser.parse_args()[0] | 375 options = parser.parse_args()[0] |
311 | 376 |
312 # Convert port range into the desired tuple format. | 377 # Convert port range into the desired tuple format. |
313 try: | 378 try: |
314 if isinstance(options.port_range, str): | 379 if isinstance(options.port_range, str): |
315 options.port_range = [int(port) for port in options.port_range.split(',')] | 380 options.port_range = [int(port) for port in options.port_range.split(',')] |
316 except ValueError: | 381 except ValueError: |
317 parser.error('Invalid port range specified.') | 382 parser.error('Invalid port range specified.') |
318 | 383 |
319 if options.expiry_time < 0: | 384 if options.expiry_time < 0: |
320 parser.error('Invalid expiry time specified.') | 385 parser.error('Invalid expiry time specified.') |
321 | 386 |
322 # Convert the path to an absolute to remove any . or .. | 387 # Convert the path to an absolute to remove any . or .. |
323 options.www_root = os.path.abspath(options.www_root) | 388 if not options.local_server_port: |
389 if not options.www_root: | |
390 options.www_root = os.getcwd() | |
391 options.www_root = os.path.abspath(options.www_root) | |
324 | 392 |
325 _SetLogger(options.verbose) | 393 _SetLogger(options.verbose) |
326 | 394 |
327 return options | 395 return options |
328 | 396 |
329 | 397 |
330 def _SetLogger(verbose): | 398 def _SetLogger(verbose): |
331 file_handler = handlers.RotatingFileHandler('cns.log', 'a', 10000000, 10) | 399 file_handler = handlers.RotatingFileHandler('cns.log', 'a', 10000000, 10) |
332 file_handler.setFormatter(logging.Formatter('[%(threadName)s] %(message)s')) | 400 file_handler.setFormatter(logging.Formatter('[%(threadName)s] %(message)s')) |
333 | 401 |
(...skipping 31 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
365 try: | 433 try: |
366 cherrypy.quickstart(ConstrainedNetworkServer(options, pa)) | 434 cherrypy.quickstart(ConstrainedNetworkServer(options, pa)) |
367 finally: | 435 finally: |
368 # Disable Ctrl-C handler to prevent interruption of cleanup. | 436 # Disable Ctrl-C handler to prevent interruption of cleanup. |
369 signal.signal(signal.SIGINT, lambda signal, frame: None) | 437 signal.signal(signal.SIGINT, lambda signal, frame: None) |
370 pa.Cleanup(all_ports=True) | 438 pa.Cleanup(all_ports=True) |
371 | 439 |
372 | 440 |
373 if __name__ == '__main__': | 441 if __name__ == '__main__': |
374 Main() | 442 Main() |
OLD | NEW |