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 175 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
209 if no_cache: | 212 if no_cache: |
210 response = cherrypy.response | 213 response = cherrypy.response |
211 response.headers['Pragma'] = 'no-cache' | 214 response.headers['Pragma'] = 'no-cache' |
212 response.headers['Cache-Control'] = 'no-cache' | 215 response.headers['Cache-Control'] = 'no-cache' |
213 | 216 |
214 # CherryPy is a bit wonky at detecting parameters, so just make them all | 217 # CherryPy is a bit wonky at detecting parameters, so just make them all |
215 # optional and validate them ourselves. | 218 # optional and validate them ourselves. |
216 if not f: | 219 if not f: |
217 raise cherrypy.HTTPError(400, 'Invalid request. File must be specified.') | 220 raise cherrypy.HTTPError(400, 'Invalid request. File must be specified.') |
218 | 221 |
| 222 # Check existence early to prevent wasted constraint setup. |
| 223 self._CheckRequestedFileExist(f) |
| 224 |
| 225 # If there are no constraints, just serve the file. |
| 226 if bandwidth is None and latency is None and loss is None: |
| 227 return self._ServeFile(f) |
| 228 |
| 229 constrained_port = self._GetConstrainedPort( |
| 230 f, bandwidth=bandwidth, latency=latency, loss=loss, new_port=new_port, |
| 231 **kwargs) |
| 232 |
| 233 # Build constrained URL using the constrained port and original URL |
| 234 # parameters except the network constraints (bandwidth, latency, and loss). |
| 235 constrained_url = self._GetServerURL(f, constrained_port, |
| 236 no_cache=no_cache, **kwargs) |
| 237 |
| 238 # Redirect request to the constrained port. |
| 239 cherrypy.log('Redirect to %s' % constrained_url) |
| 240 cherrypy.lib.cptools.redirect(constrained_url, internal=False) |
| 241 |
| 242 def _CheckRequestedFileExist(self, f): |
| 243 """Checks if the requested file exists, raises HTTPError otherwise.""" |
| 244 if self._options.local_server_port: |
| 245 self._CheckFileExistOnLocalServer(f) |
| 246 else: |
| 247 self._CheckFileExistOnServer(f) |
| 248 |
| 249 def _CheckFileExistOnServer(self, f): |
| 250 """Checks if requested file f exists to be served by this server.""" |
219 # Sanitize and check the path to prevent www-root escapes. | 251 # Sanitize and check the path to prevent www-root escapes. |
220 sanitized_path = os.path.abspath(os.path.join(self._options.www_root, f)) | 252 sanitized_path = os.path.abspath(os.path.join(self._options.www_root, f)) |
221 if not sanitized_path.startswith(self._options.www_root): | 253 if not sanitized_path.startswith(self._options.www_root): |
222 raise cherrypy.HTTPError(403, 'Invalid file requested.') | 254 raise cherrypy.HTTPError(403, 'Invalid file requested.') |
223 | |
224 # Check existence early to prevent wasted constraint setup. | |
225 if not os.path.exists(sanitized_path): | 255 if not os.path.exists(sanitized_path): |
226 raise cherrypy.HTTPError(404, 'File not found.') | 256 raise cherrypy.HTTPError(404, 'File not found.') |
227 | 257 |
228 # If there are no constraints, just serve the file. | 258 def _CheckFileExistOnLocalServer(self, f): |
229 if bandwidth is None and latency is None and loss is None: | 259 """Checks if requested file exists on local server hosting files.""" |
| 260 test_url = self._GetServerURL(f, self._options.local_server_port) |
| 261 try: |
| 262 cherrypy.log('Check file exist using URL: %s' % test_url) |
| 263 return urllib2.urlopen(test_url) is not None |
| 264 except Exception: |
| 265 raise cherrypy.HTTPError(404, 'File not found on local server.') |
| 266 |
| 267 def _ServeFile(self, f): |
| 268 """Serves the file as an http response.""" |
| 269 if self._options.local_server_port: |
| 270 redirect_url = self._GetServerURL(f, self._options.local_server_port) |
| 271 cherrypy.log('Redirect to %s' % redirect_url) |
| 272 cherrypy.lib.cptools.redirect(redirect_url, internal=False) |
| 273 else: |
| 274 sanitized_path = os.path.abspath(os.path.join(self._options.www_root, f)) |
230 return cherrypy.lib.static.serve_file(sanitized_path) | 275 return cherrypy.lib.static.serve_file(sanitized_path) |
231 | 276 |
| 277 def _GetServerURL(self, f, port, **kwargs): |
| 278 """Returns a URL for local server to serve the file on given port. |
| 279 |
| 280 Args: |
| 281 f: file name to serve on local server. Relative to www_root. |
| 282 port: Local server port (it can be a configured constrained port). |
| 283 kwargs: extra parameteres passed in the URL. |
| 284 """ |
| 285 url = '%s?f=%s&' % (cherrypy.url(), f) |
| 286 if self._options.local_server_port: |
| 287 url = '%s/%s?' % ( |
| 288 cherrypy.url().replace('ServeConstrained', self._options.www_root), f) |
| 289 |
| 290 url = url.replace(':%d' % self._options.port, ':%d' % port) |
| 291 extra_args = urllib.urlencode(kwargs) |
| 292 if extra_args: |
| 293 url += extra_args |
| 294 return url |
| 295 |
| 296 def _GetConstrainedPort(self, f=None, bandwidth=None, latency=None, loss=None, |
| 297 new_port=False, **kwargs): |
| 298 """Creates or gets a port with specified network constraints. |
| 299 |
| 300 See ServeConstrained() for more details. |
| 301 """ |
232 # Validate inputs. isdigit() guarantees a natural number. | 302 # Validate inputs. isdigit() guarantees a natural number. |
233 bandwidth = self._ParseIntParameter( | 303 bandwidth = self._ParseIntParameter( |
234 bandwidth, 'Invalid bandwidth constraint.', lambda x: x > 0) | 304 bandwidth, 'Invalid bandwidth constraint.', lambda x: x > 0) |
235 latency = self._ParseIntParameter( | 305 latency = self._ParseIntParameter( |
236 latency, 'Invalid latency constraint.', lambda x: x >= 0) | 306 latency, 'Invalid latency constraint.', lambda x: x >= 0) |
237 loss = self._ParseIntParameter( | 307 loss = self._ParseIntParameter( |
238 loss, 'Invalid loss constraint.', lambda x: x <= 100 and x >= 0) | 308 loss, 'Invalid loss constraint.', lambda x: x <= 100 and x >= 0) |
239 | 309 |
| 310 redirect_port = self._options.port |
| 311 if self._options.local_server_port: |
| 312 redirect_port = self._options.local_server_port |
| 313 |
| 314 start_time = time.time() |
240 # Allocate a port using the given constraints. If a port with the requested | 315 # Allocate a port using the given constraints. If a port with the requested |
241 # key is already allocated, it will be reused. | 316 # key and kwargs already exist then reuse that port. |
242 # | |
243 # TODO(dalecurtis): The key cherrypy.request.remote.ip might not be unique | |
244 # if build slaves are sharing the same VM. | |
245 start_time = time.time() | |
246 constrained_port = self._port_allocator.Get( | 317 constrained_port = self._port_allocator.Get( |
247 cherrypy.request.remote.ip, server_port=self._options.port, | 318 cherrypy.request.remote.ip, server_port=redirect_port, |
248 interface=self._options.interface, bandwidth=bandwidth, latency=latency, | 319 interface=self._options.interface, bandwidth=bandwidth, latency=latency, |
249 loss=loss, new_port=new_port, file=f, **kwargs) | 320 loss=loss, new_port=new_port, file=f, **kwargs) |
250 end_time = time.time() | 321 |
| 322 cherrypy.log('Time to set up port %d = %.3fsec.' % |
| 323 (constrained_port, time.time() - start_time)) |
251 | 324 |
252 if not constrained_port: | 325 if not constrained_port: |
253 raise cherrypy.HTTPError(503, 'Service unavailable. Out of ports.') | 326 raise cherrypy.HTTPError(503, 'Service unavailable. Out of ports.') |
254 | 327 return constrained_port |
255 cherrypy.log('Time to set up port %d = %ssec.' % | |
256 (constrained_port, end_time - start_time)) | |
257 | |
258 # Build constrained URL using the constrained port and original URL | |
259 # parameters except the network constraints (bandwidth, latency, and loss). | |
260 constrained_url = '%s?f=%s&no_cache=%s&%s' % ( | |
261 cherrypy.url().replace( | |
262 ':%d' % self._options.port, ':%d' % constrained_port), | |
263 f, | |
264 no_cache, | |
265 '&'.join(['%s=%s' % (key, kwargs[key]) for key in kwargs])) | |
266 | |
267 # Redirect request to the constrained port. | |
268 cherrypy.lib.cptools.redirect(constrained_url, internal=False) | |
269 | 328 |
270 def _ParseIntParameter(self, param, msg, check): | 329 def _ParseIntParameter(self, param, msg, check): |
271 """Returns integer value of param and verifies it satisfies the check. | 330 """Returns integer value of param and verifies it satisfies the check. |
272 | 331 |
273 Args: | 332 Args: |
274 param: Parameter name to check. | 333 param: Parameter name to check. |
275 msg: Message in error if raised. | 334 msg: Message in error if raised. |
276 check: Check to verify the parameter value. | 335 check: Check to verify the parameter value. |
277 | 336 |
278 Returns: | 337 Returns: |
(...skipping 30 matching lines...) Expand all Loading... |
309 help=('Interface to setup constraints on. Use lo for a ' | 368 help=('Interface to setup constraints on. Use lo for a ' |
310 'local client. Default: %default')) | 369 'local client. Default: %default')) |
311 parser.add_option('--socket-timeout', type='int', | 370 parser.add_option('--socket-timeout', type='int', |
312 default=cherrypy.server.socket_timeout, | 371 default=cherrypy.server.socket_timeout, |
313 help=('Number of seconds before a socket connection times ' | 372 help=('Number of seconds before a socket connection times ' |
314 'out. Default: %default')) | 373 'out. Default: %default')) |
315 parser.add_option('--threads', type='int', | 374 parser.add_option('--threads', type='int', |
316 default=cherrypy._cpserver.Server.thread_pool, | 375 default=cherrypy._cpserver.Server.thread_pool, |
317 help=('Number of threads in the thread pool. Default: ' | 376 help=('Number of threads in the thread pool. Default: ' |
318 '%default')) | 377 '%default')) |
319 parser.add_option('--www-root', default=os.getcwd(), | 378 parser.add_option('--www-root', default='', |
320 help=('Directory root to serve files from. Defaults to the ' | 379 help=('Directory root to serve files from. If --local-' |
321 'current directory: %default')) | 380 'server-port is used, the path is appended to the ' |
| 381 'redirected URL of local server. Defaults to the ' |
| 382 'current directory (if --local-server-port is not ' |
| 383 'used): %s' % os.getcwd())) |
| 384 parser.add_option('--local-server-port', type='int', |
| 385 help=('Optional local server port to host files.')) |
322 parser.add_option('-v', '--verbose', action='store_true', default=False, | 386 parser.add_option('-v', '--verbose', action='store_true', default=False, |
323 help='Turn on verbose output.') | 387 help='Turn on verbose output.') |
324 | 388 |
325 options = parser.parse_args()[0] | 389 options = parser.parse_args()[0] |
326 | 390 |
327 # Convert port range into the desired tuple format. | 391 # Convert port range into the desired tuple format. |
328 try: | 392 try: |
329 if isinstance(options.port_range, str): | 393 if isinstance(options.port_range, str): |
330 options.port_range = [int(port) for port in options.port_range.split(',')] | 394 options.port_range = [int(port) for port in options.port_range.split(',')] |
331 except ValueError: | 395 except ValueError: |
332 parser.error('Invalid port range specified.') | 396 parser.error('Invalid port range specified.') |
333 | 397 |
334 if options.expiry_time < 0: | 398 if options.expiry_time < 0: |
335 parser.error('Invalid expiry time specified.') | 399 parser.error('Invalid expiry time specified.') |
336 | 400 |
337 # Convert the path to an absolute to remove any . or .. | 401 # Convert the path to an absolute to remove any . or .. |
338 options.www_root = os.path.abspath(options.www_root) | 402 if not options.local_server_port: |
| 403 if not options.www_root: |
| 404 options.www_root = os.getcwd() |
| 405 options.www_root = os.path.abspath(options.www_root) |
339 | 406 |
340 _SetLogger(options.verbose) | 407 _SetLogger(options.verbose) |
341 | 408 |
342 return options | 409 return options |
343 | 410 |
344 | 411 |
345 def _SetLogger(verbose): | 412 def _SetLogger(verbose): |
346 file_handler = handlers.RotatingFileHandler('cns.log', 'a', 10000000, 10) | 413 file_handler = handlers.RotatingFileHandler('cns.log', 'a', 10000000, 10) |
347 file_handler.setFormatter(logging.Formatter('[%(threadName)s] %(message)s')) | 414 file_handler.setFormatter(logging.Formatter('[%(threadName)s] %(message)s')) |
348 | 415 |
(...skipping 31 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
380 try: | 447 try: |
381 cherrypy.quickstart(ConstrainedNetworkServer(options, pa)) | 448 cherrypy.quickstart(ConstrainedNetworkServer(options, pa)) |
382 finally: | 449 finally: |
383 # Disable Ctrl-C handler to prevent interruption of cleanup. | 450 # Disable Ctrl-C handler to prevent interruption of cleanup. |
384 signal.signal(signal.SIGINT, lambda signal, frame: None) | 451 signal.signal(signal.SIGINT, lambda signal, frame: None) |
385 pa.Cleanup(all_ports=True) | 452 pa.Cleanup(all_ports=True) |
386 | 453 |
387 | 454 |
388 if __name__ == '__main__': | 455 if __name__ == '__main__': |
389 Main() | 456 Main() |
OLD | NEW |