Chromium Code Reviews| 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 import mimetypes | 16 import mimetypes |
| 17 import optparse | 17 import optparse |
| 18 import os | 18 import os |
| 19 import signal | 19 import signal |
| 20 import sys | 20 import sys |
| 21 import threading | 21 import threading |
| 22 import time | 22 import time |
| 23 import urllib2 | |
| 24 | |
| 23 import traffic_control | 25 import traffic_control |
| 24 | 26 |
| 25 try: | 27 try: |
| 26 import cherrypy | 28 import cherrypy |
| 27 except ImportError: | 29 except ImportError: |
| 28 print ('CNS requires CherryPy v3 or higher to be installed. Please install\n' | 30 print ('CNS requires CherryPy v3 or higher to be installed. Please install\n' |
| 29 'and try again. On Linux: sudo apt-get install python-cherrypy3\n') | 31 'and try again. On Linux: sudo apt-get install python-cherrypy3\n') |
| 30 sys.exit(1) | 32 sys.exit(1) |
| 31 | 33 |
| 32 # Add webm file types to mimetypes map since cherrypy's default type is text. | 34 # 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... | |
| 193 if no_cache: | 195 if no_cache: |
| 194 response = cherrypy.response | 196 response = cherrypy.response |
| 195 response.headers['Pragma'] = 'no-cache' | 197 response.headers['Pragma'] = 'no-cache' |
| 196 response.headers['Cache-Control'] = 'no-cache' | 198 response.headers['Cache-Control'] = 'no-cache' |
| 197 | 199 |
| 198 # CherryPy is a bit wonky at detecting parameters, so just make them all | 200 # CherryPy is a bit wonky at detecting parameters, so just make them all |
| 199 # optional and validate them ourselves. | 201 # optional and validate them ourselves. |
| 200 if not f: | 202 if not f: |
| 201 raise cherrypy.HTTPError(400, 'Invalid request. File must be specified.') | 203 raise cherrypy.HTTPError(400, 'Invalid request. File must be specified.') |
| 202 | 204 |
| 205 # Check existence early to prevent wasted constraint setup. | |
| 206 self._CheckRequestedFileExist(f) | |
| 207 | |
| 208 # If there are no constraints, just serve the file. | |
| 209 if bandwidth is None and latency is None and loss is None: | |
| 210 return self._ServeFile(f) | |
| 211 | |
| 212 constrained_port = self._GetConstrainedPort( | |
| 213 f, bandwidth=bandwidth, latency=latency, loss=loss, new_port=new_port, | |
| 214 **kwargs) | |
| 215 | |
| 216 # Build constrained URL using the constrained port and original URL | |
| 217 # parameters except the network constraints (bandwidth, latency, and loss). | |
| 218 if self._options.local_server_port: | |
| 219 constrained_url = self._GetLocalServerURL(f, constrained_port, **kwargs) | |
| 220 else: | |
| 221 constrained_url = '%s?f=%s&no_cache=%s&%s' % ( | |
|
DaleCurtis
2012/08/07 23:53:57
This looks similar to the self._GetLocalServerURL(
shadi
2012/08/08 22:55:49
They are not actually that similar, but Done. :-)
| |
| 222 cherrypy.url().replace( | |
| 223 ':%d' % self._options.port, ':%d' % constrained_port), | |
| 224 '%s' % f, | |
| 225 no_cache, | |
| 226 '&'.join(['%s=%s' % (key, kwargs[key]) for key in kwargs])) | |
| 227 | |
| 228 # Redirect request to the constrained port. | |
| 229 cherrypy.log('Redirect to %s' % constrained_url) | |
| 230 cherrypy.lib.cptools.redirect(constrained_url, internal=False) | |
| 231 | |
| 232 def _CheckRequestedFileExist(self, f): | |
| 233 """Checks if the requested file exists, raises HTTPError otherwise.""" | |
| 234 if self._options.local_server_port: | |
| 235 self._CheckFileExistOnLocalServer(f) | |
| 236 else: | |
| 237 self._CheckFileExistOnServer(f) | |
| 238 | |
| 239 def _CheckFileExistOnServer(self, f): | |
| 240 """Checks if requested f exists to be served by this server.""" | |
|
DaleCurtis
2012/08/07 23:53:57
s/f/file f/
shadi
2012/08/08 22:55:49
Done.
| |
| 203 # Sanitize and check the path to prevent www-root escapes. | 241 # Sanitize and check the path to prevent www-root escapes. |
| 204 sanitized_path = os.path.abspath(os.path.join(self._options.www_root, f)) | 242 sanitized_path = os.path.abspath(os.path.join(self._options.www_root, f)) |
| 205 if not sanitized_path.startswith(self._options.www_root): | 243 if not sanitized_path.startswith(self._options.www_root): |
| 206 raise cherrypy.HTTPError(403, 'Invalid file requested.') | 244 raise cherrypy.HTTPError(403, 'Invalid file requested.') |
| 207 | |
| 208 # Check existence early to prevent wasted constraint setup. | |
| 209 if not os.path.exists(sanitized_path): | 245 if not os.path.exists(sanitized_path): |
| 210 raise cherrypy.HTTPError(404, 'File not found.') | 246 raise cherrypy.HTTPError(404, 'File not found.') |
| 211 | 247 |
| 212 # If there are no constraints, just serve the file. | 248 def _CheckFileExistOnLocalServer(self, f): |
|
DaleCurtis
2012/08/07 23:53:57
Hmmm, what is urlopen doing behind the scenes here
shadi
2012/08/08 22:55:49
No it is not pulling down the remote file. It retu
| |
| 213 if bandwidth is None and latency is None and loss is None: | 249 """Checks if requested file exists on local server hosting files.""" |
| 250 test_url = self._GetLocalServerURL(f, self._options.local_server_port) | |
| 251 try: | |
| 252 cherrypy.log('Check file exist using URL: %s' % test_url) | |
| 253 return urllib2.urlopen(test_url) is not None | |
| 254 except Exception: | |
| 255 raise cherrypy.HTTPError(404, 'File not found on local server.') | |
| 256 | |
| 257 def _ServeFile(self, f): | |
| 258 """Serves the file as an http response.""" | |
| 259 if self._options.local_server_port: | |
| 260 redirect_url = self._GetLocalServerURL(f, self._options.local_server_port) | |
| 261 cherrypy.log('Redirect to %s' % redirect_url) | |
| 262 cherrypy.lib.cptools.redirect(redirect_url, internal=False) | |
|
DaleCurtis
2012/08/07 23:53:57
Do you need to return anything here to keep Cherry
shadi
2012/08/08 22:55:49
CherryPy has not been complaining :-)
| |
| 263 else: | |
| 264 sanitized_path = os.path.abspath(os.path.join(self._options.www_root, f)) | |
| 214 return cherrypy.lib.static.serve_file(sanitized_path) | 265 return cherrypy.lib.static.serve_file(sanitized_path) |
| 215 | 266 |
| 267 def _GetLocalServerURL(self, f, port, **kwargs): | |
| 268 """Returns a URL for local server to serve the file on given port. | |
| 269 | |
| 270 Args: | |
| 271 f: file name to serve on local server. Relative to www_root. | |
| 272 port: Local server port (it can be a configured constrained port). | |
| 273 kwargs: extra parameteres passed in the URL. | |
| 274 """ | |
| 275 base_url = cherrypy.url().replace('ServeConstrained', | |
| 276 self._options.www_root) | |
| 277 base_url = base_url.replace(':%d' % self._options.port, ':%d' % port) | |
| 278 url = '%s/%s' % (base_url, f) | |
| 279 extra_args = '&'.join(['%s=%s' % (key, kwargs[key]) for key in kwargs]) | |
| 280 if extra_args: | |
| 281 url += '?%s' % extra_args | |
| 282 return 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 """ | |
| 216 # Validate inputs. isdigit() guarantees a natural number. | 290 # Validate inputs. isdigit() guarantees a natural number. |
| 217 bandwidth = self._ParseIntParameter( | 291 bandwidth = self._ParseIntParameter( |
| 218 bandwidth, 'Invalid bandwidth constraint.', lambda x: x > 0) | 292 bandwidth, 'Invalid bandwidth constraint.', lambda x: x > 0) |
| 219 latency = self._ParseIntParameter( | 293 latency = self._ParseIntParameter( |
| 220 latency, 'Invalid latency constraint.', lambda x: x >= 0) | 294 latency, 'Invalid latency constraint.', lambda x: x >= 0) |
| 221 loss = self._ParseIntParameter( | 295 loss = self._ParseIntParameter( |
| 222 loss, 'Invalid loss constraint.', lambda x: x <= 100 and x >= 0) | 296 loss, 'Invalid loss constraint.', lambda x: x <= 100 and x >= 0) |
| 223 | 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() | |
| 224 # 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 |
| 225 # key is already allocated, it will be reused. | 304 # key and kwargs already exist then reuse that port. |
| 226 # | |
| 227 # TODO(dalecurtis): The key cherrypy.request.remote.ip might not be unique | |
| 228 # if build slaves are sharing the same VM. | |
| 229 start_time = time.time() | |
| 230 constrained_port = self._port_allocator.Get( | 305 constrained_port = self._port_allocator.Get( |
| 231 cherrypy.request.remote.ip, server_port=self._options.port, | 306 cherrypy.request.remote.ip, server_port=redirect_port, |
| 232 interface=self._options.interface, bandwidth=bandwidth, latency=latency, | 307 interface=self._options.interface, bandwidth=bandwidth, latency=latency, |
| 233 loss=loss, new_port=new_port, file=f, **kwargs) | 308 loss=loss, new_port=new_port, file=f, **kwargs) |
| 234 end_time = time.time() | 309 |
| 310 cherrypy.log('Time to set up port %d = %ssec.' % | |
| 311 (constrained_port, time.time() - start_time)) | |
| 235 | 312 |
| 236 if not constrained_port: | 313 if not constrained_port: |
| 237 raise cherrypy.HTTPError(503, 'Service unavailable. Out of ports.') | 314 raise cherrypy.HTTPError(503, 'Service unavailable. Out of ports.') |
| 238 | 315 return constrained_port |
| 239 cherrypy.log('Time to set up port %d = %ssec.' % | |
| 240 (constrained_port, end_time - start_time)) | |
| 241 | |
| 242 # Build constrained URL using the constrained port and original URL | |
| 243 # parameters except the network constraints (bandwidth, latency, and loss). | |
| 244 constrained_url = '%s?f=%s&no_cache=%s&%s' % ( | |
| 245 cherrypy.url().replace( | |
| 246 ':%d' % self._options.port, ':%d' % constrained_port), | |
| 247 f, | |
| 248 no_cache, | |
| 249 '&'.join(['%s=%s' % (key, kwargs[key]) for key in kwargs])) | |
| 250 | |
| 251 # Redirect request to the constrained port. | |
| 252 cherrypy.lib.cptools.redirect(constrained_url, internal=False) | |
| 253 | 316 |
| 254 def _ParseIntParameter(self, param, msg, check): | 317 def _ParseIntParameter(self, param, msg, check): |
| 255 """Returns integer value of param and verifies it satisfies the check. | 318 """Returns integer value of param and verifies it satisfies the check. |
| 256 | 319 |
| 257 Args: | 320 Args: |
| 258 param: Parameter name to check. | 321 param: Parameter name to check. |
| 259 msg: Message in error if raised. | 322 msg: Message in error if raised. |
| 260 check: Check to verify the parameter value. | 323 check: Check to verify the parameter value. |
| 261 | 324 |
| 262 Returns: | 325 Returns: |
| (...skipping 30 matching lines...) Expand all Loading... | |
| 293 help=('Interface to setup constraints on. Use lo for a ' | 356 help=('Interface to setup constraints on. Use lo for a ' |
| 294 'local client. Default: %default')) | 357 'local client. Default: %default')) |
| 295 parser.add_option('--socket-timeout', type='int', | 358 parser.add_option('--socket-timeout', type='int', |
| 296 default=cherrypy.server.socket_timeout, | 359 default=cherrypy.server.socket_timeout, |
| 297 help=('Number of seconds before a socket connection times ' | 360 help=('Number of seconds before a socket connection times ' |
| 298 'out. Default: %default')) | 361 'out. Default: %default')) |
| 299 parser.add_option('--threads', type='int', | 362 parser.add_option('--threads', type='int', |
| 300 default=cherrypy._cpserver.Server.thread_pool, | 363 default=cherrypy._cpserver.Server.thread_pool, |
| 301 help=('Number of threads in the thread pool. Default: ' | 364 help=('Number of threads in the thread pool. Default: ' |
| 302 '%default')) | 365 '%default')) |
| 303 parser.add_option('--www-root', default=os.getcwd(), | 366 parser.add_option('--www-root', default='', |
|
DaleCurtis
2012/08/07 23:53:57
just remove the default='' ?
shadi
2012/08/08 22:55:49
I actually put this so that www_root == '' when --
| |
| 304 help=('Directory root to serve files from. Defaults to the ' | 367 help=('Directory root to serve files from. Defaults to the ' |
| 305 '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.')) | |
| 306 parser.add_option('-v', '--verbose', action='store_true', default=False, | 372 parser.add_option('-v', '--verbose', action='store_true', default=False, |
| 307 help='Turn on verbose output.') | 373 help='Turn on verbose output.') |
| 308 | 374 |
| 309 options = parser.parse_args()[0] | 375 options = parser.parse_args()[0] |
| 310 | 376 |
| 311 # Convert port range into the desired tuple format. | 377 # Convert port range into the desired tuple format. |
| 312 try: | 378 try: |
| 313 if isinstance(options.port_range, str): | 379 if isinstance(options.port_range, str): |
| 314 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(',')] |
| 315 except ValueError: | 381 except ValueError: |
| 316 parser.error('Invalid port range specified.') | 382 parser.error('Invalid port range specified.') |
| 317 | 383 |
| 318 if options.expiry_time < 0: | 384 if options.expiry_time < 0: |
| 319 parser.error('Invalid expiry time specified.') | 385 parser.error('Invalid expiry time specified.') |
| 320 | 386 |
| 321 # Convert the path to an absolute to remove any . or .. | 387 # Convert the path to an absolute to remove any . or .. |
| 322 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) | |
| 323 | 392 |
| 324 # Required so that cherrypy logs do not get propagated to root logger causing | 393 # Required so that cherrypy logs do not get propagated to root logger causing |
| 325 # the logs to be printed twice. | 394 # the logs to be printed twice. |
| 326 cherrypy.log.error_log.propagate = False | 395 cherrypy.log.error_log.propagate = False |
| 327 cherrypy.log.access_log.propagate = False | 396 cherrypy.log.access_log.propagate = False |
| 328 | 397 |
| 329 _SetLogger(options.verbose) | 398 _SetLogger(options.verbose) |
| 330 | 399 |
| 331 return options | 400 return options |
| 332 | 401 |
| (...skipping 31 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 364 try: | 433 try: |
| 365 cherrypy.quickstart(ConstrainedNetworkServer(options, pa)) | 434 cherrypy.quickstart(ConstrainedNetworkServer(options, pa)) |
| 366 finally: | 435 finally: |
| 367 # Disable Ctrl-C handler to prevent interruption of cleanup. | 436 # Disable Ctrl-C handler to prevent interruption of cleanup. |
| 368 signal.signal(signal.SIGINT, lambda signal, frame: None) | 437 signal.signal(signal.SIGINT, lambda signal, frame: None) |
| 369 pa.Cleanup(all_ports=True) | 438 pa.Cleanup(all_ports=True) |
| 370 | 439 |
| 371 | 440 |
| 372 if __name__ == '__main__': | 441 if __name__ == '__main__': |
| 373 Main() | 442 Main() |
| OLD | NEW |