Package brisa :: Package core :: Module webserver
[hide private]
[frames] | no frames]

Source Code for Module brisa.core.webserver

  1  # Licensed under the MIT license 
  2  # http://opensource.org/licenses/mit-license.php or see LICENSE file. 
  3  # Copyright 2007-2008 Brisa Team <brisa-develop@garage.maemo.org> 
  4   
  5  """ WSGI-based webserver module. Includes a few built-in adapters for working 
  6  with different WSGI servers. 
  7   
  8  If possible, an adapter will be automatically assigned to a WebServer instance - 
  9  if not passed by the user. 
 10   
 11  For retrieving the available adapters, use get_available_adapters(). The results 
 12  can be passed to the WebServer, e.g. WebServer(adapter=some_adapter) where 
 13  some_adapter was retrieved with get_available_adapters(). 
 14  """ 
 15   
 16  __all__ = ('Resource', 'CustomResource', 'WebServer', 'StaticFile', 'adapters', 
 17             'get_available_adapters', 'AdapterInterface', 'CherrypyAdapter', 
 18             'PasteAdapter', 'CircuitsWebAdapter') 
 19   
 20  import os 
 21  import random 
 22  import os.path 
 23  import sys 
 24  import rfc822 
 25  import warnings 
 26  import mimetypes 
 27  import mimetools 
 28  import wsgiref.util 
 29  import wsgiref.headers 
 30   
 31   
 32  from brisa import __enable_webserver_logging__, __enable_offline_mode__ 
 33  from brisa.core import log, config, threaded_call 
 34  from brisa.core.network import parse_url, get_active_ifaces, get_ip_address 
 35   
 36   
 37  if not __enable_offline_mode__: 
 38      if not get_active_ifaces(): 
 39          raise RuntimeError('Network is down.') 
 40   
 41  mimetypes.init() 
 42  log = log.getLogger('core.webserver') 
 43   
 44  invalid_path_exists = 'path does not exist' 
 45  invalid_path_abs = 'path must be absolute' 
 46  invalid_path_dir = 'path must be a file' 
 47   
 48  http_codes = {100: 'Continue', 
 49                101: 'Switching Protocols', 
 50                200: 'OK', 
 51                201: 'Created', 
 52                202: 'Accepted', 
 53                203: 'Non-Authoritative Information', 
 54                204: 'No Content', 
 55                205: 'Reset Content', 
 56                206: 'Partial Content', 
 57                300: 'Multiple Choices', 
 58                301: 'Moved Permanently', 
 59                302: 'Found', 
 60                303: 'See Other', 
 61                304: 'Not Modified', 
 62                305: 'Use Proxy', 
 63                307: 'Temporary Redirect', 
 64                400: 'Bad Request', 
 65                401: 'Unauthorized', 
 66                403: 'Forbidden', 
 67                404: 'Not Found', 
 68                405: 'Method Not Allowed', 
 69                416: 'Requested range not satisfiable', 
 70                417: 'Expectation Failed', 
 71                500: 'Internal Server Error', 
 72                501: 'Not Implemented', 
 73                502: 'Bad Gateway', 
 74                503: 'Service Unavailable', 
 75                504: 'Gateway Time-out', 
 76                505: 'HTTP Version not supported'} 
 77   
 78  chunks_size = 2**16 
 79   
 80  simple_template = '<html><head><title>%s</title><body>%s</body></html>' 
81 82 83 -def simple_response(code, start_response, extra_msg=None):
84 """ Performs a simple response for a request. Usually used for returning 85 errors or empty pages. For example, start_response(404, start_response, 86 'File not available on the server') will return a page to the user with the 87 404 status and this message on the body, wrapped in a simple html template. 88 89 @param code: status code present on http_codes dict 90 @param start_response: start_response wsgi function 91 @param extra_msg: message that goes on the body. If not supplied, the status 92 message will take its place on the body. 93 94 @type code: integer 95 @type start_response: callable 96 @type extra_msg: string 97 98 @note: this function also sends the headers 99 100 @return: final message to return as response body 101 @rtype: string 102 """ 103 status_msg = '%d %s' % (code, http_codes[code]) 104 start_response(status_msg, [('Content-type', 'text/html')]) 105 if extra_msg: 106 return simple_template % (status_msg, '%s<br/>%s' % (status_msg, \ 107 extra_msg)) 108 else: 109 return simple_template % (status_msg, status_msg)
110
111 112 -def response(code, start_response, msg):
113 """ Performs a response for a request without wrapping in a html template. 114 115 @param code: status code present on http_codes dict 116 @param start_response: start_response wsgi function 117 @param msg: message that goes on the body. If not supplied, the status 118 message will take its place on the body. 119 120 @type code: integer 121 @type start_response: callable 122 @type msg: string 123 124 @note: this function also sends the headers 125 126 @return: final message to return as response body 127 @rtype: string 128 """ 129 status_msg = '%d %s' % (code, http_codes[code]) 130 start_response(status_msg, [('Content-type', 'text/html')]) 131 return msg
132
133 134 135 -class Request(object):
136 """ Request wrapper class. 137 """ 138
139 - def __init__(self, env):
140 """ Constructor for the Request wrapper class. Parses the HTTP headers 141 and sets request attributes. 142 143 @param env: wsgi environ dict 144 @type env: dict 145 """ 146 self.env = env.copy() 147 self.body = env.get('wsgi.input', None) 148 self.headers = wsgiref.headers.Headers([]) 149 150 for k,v in env.items(): 151 if 'HTTP' in k: 152 key = k.replace('HTTP_', '').lower().replace('_', '-') 153 self.headers[key] = v 154 155 self.method = env['REQUEST_METHOD'] 156 self.server_protocol = env['SERVER_PROTOCOL'] 157 self.protocol = tuple(map(int, self.server_protocol[5:].split('.'))) 158 self.headers['Content-length'] = env.get('CONTENT_LENGTH', 0) 159 160 if not self.headers['Content-length']: 161 del self.headers['Content-length'] 162 else: 163 self.headers['Content-length'] = int(self.headers['Content-length']) 164 165 self.headers['Content-type'] = env.get('CONTENT_TYPE', '') 166 self.query = env['QUERY_STRING'] 167 self.uri = env['SCRIPT_NAME'] 168 169 if self.query: 170 self.params = dict([(lambda k: k.split('='))(v) for v in \ 171 self.query.split('&')]) 172 else: 173 self.params = {}
174
175 - def read(self):
176 """ Reads the request payload, if available. 177 """ 178 if 'Content-length' in self.headers and self.body: 179 return self.body.read(self.headers['Content-length']) 180 else: 181 raise RuntimeError('Header does not contain a content-'\ 182 'length field. It is possible that this'\ 183 ' request does not contain a payload.')
184
185 186 -class Response(object):
187 """ Response wrapper class. 188 """ 189
190 - def __init__(self, status, start_response):
191 """ Constructor for the Response class. 192 193 @param status: response status code, can be modified later 194 @param start_response: wsgi start_response function 195 196 @type status: int 197 @type start_response: callable 198 """ 199 self.status = status 200 self.start_response = start_response 201 self.headers = wsgiref.headers.Headers([]) 202 self.body = None
203
204 - def _respond(self):
205 """ Sends the headers for this response. After this call, response.body 206 should be returned to the WSGI server as the response payload. 207 """ 208 if not self.status: 209 raise ValueError('Status code not set for a HTTP response.') 210 status_msg = '%d %s' % (self.status, http_codes[self.status]) 211 self.start_response(status_msg, self.headers.items())
212
213 214 -def get_byte_ranges(r, clen):
215 """ Based on a Range header and on the content length of a file, returns a 216 list of byte ranges requested. Returns None if the header was invalid and an 217 empty list in case of invalid range. 218 219 Based on Cherrypy 3.1 implementation. 220 221 @param r: range header 222 @param clen: content length 223 """ 224 # Byte unit supported by us (HTTP1.1) is "bytes". Get the ranges string. 225 log.debug('Range: %s, %d' % (r, clen)) 226 227 if not r: 228 return None 229 230 bunit, branges = r.split('=', 1) 231 232 log.debug('Bunit, branges: %s %s' % (bunit, branges)) 233 234 if bunit != 'bytes': 235 # Ignore any other kind of units 236 log.warning('Received a request with wrong Range header (unit is not'\ 237 'bytes') 238 return None 239 240 ranges = [] 241 242 for br in branges.split(','): 243 start, stop = [b.strip() for b in br.split('-', 1)] 244 log.debug('%s %s' % (start, stop)) 245 246 if not start: 247 # If the first number is missing, should return the last n bytes 248 # and stop must be present, or else return the whole body. 249 if not stop: 250 # Invalid! Return None and indicate the whole body 251 return None 252 # Last n bytes 253 ranges.append((clen - int(stop), clen)) 254 log.debug('Ranges: %s' % ranges) 255 else: 256 # Start is present 257 if not stop: 258 # Whole body 259 stop = clen - 1 260 if int(start) >= clen: 261 # If start is equal to or larger than the size of the document 262 # minus one, return the whole body 263 continue 264 if int(stop) < int(start): 265 return None 266 ranges.append((int(start), int(stop) + 1)) 267 log.debug('Ranges: %s' % ranges) 268 269 270 log.debug('Ranges: %s' % ranges) 271 return ranges
272
273 274 -def chunk_generator(f, chunk_size, max_chunked):
275 """ Generates chunks of a file. Stops when reaches the max_chunked value 276 of generated chunks. 277 """ 278 log.debug('Chunk generator of %s, chunks size %d and max chunked data %d' % 279 (f, chunk_size, max_chunked)) 280 281 while max_chunked > 0: 282 chunk = f.read(min(chunk_size, max_chunked)) 283 clen = len(chunk) 284 if clen == 0: 285 return 286 max_chunked -= clen 287 yield chunk
288
289 290 -def setup_single_part_response(r, rng, clen):
291 """ Setups a response object for a single part response. Based on Cherrypy 292 3.1 implementation. 293 294 @param r: response object 295 @param rng: 2-tuple of the form (start, stop) with the byte 296 range requested 297 @param clen: length of the body file 298 """ 299 start, stop = rng 300 if stop > clen: 301 stop = clen 302 res_len = stop - start 303 304 r.headers['Content-range'] = 'bytes %s-%s/%s' % (start, stop - 1, clen) 305 r.headers['Content-length'] = str(res_len) 306 307 r.body.seek(start) 308 r.body = chunk_generator(r.body, chunks_size, res_len)
309
310 311 -def setup_multi_part_response(r, rngs, clen, content_type):
312 """ Setups a response object for a multi part response, based on the byte 313 ranges requested. Based on Cherrypy 3.1 implementation. 314 315 @param r: response object 316 @param rngs: list of ranges 317 @param clen: length of the body file 318 """ 319 b = mimetools.choose_boundary() 320 r.headers['Content-type'] = 'multipart/byteranges;'\ 321 'boundary=%s' % b 322 323 real_file = r.body 324 325 def mpart_body_generator(): 326 yield '\r\n' 327 328 for start, stop in rngs: 329 yield '--%s\r\n' % b 330 yield 'Content-type: %s\r\n' % content_type 331 yield 'Content-range: bytes\r\n%s-%s/%s\r\n\r\n' % \ 332 (start, stop - 1, clen) 333 real_file.seek(start) 334 for c in chunk_generator(real_file, chunks_size, stop-start): 335 yield c 336 yield '\r\n' 337 yield '--%s--\r\n' % b
338 339 r.body = mpart_body_generator() 340
341 342 -class StaticFile(object):
343 """ Object that matches with a file and makes it available on the server. 344 """ 345
346 - def __init__(self, name, path, content_type=None, disposition=None):
347 """ Constructor for the StaticFile class. 348 349 @param name: file name visible on the webserver 350 @param path: file path on the system 351 @param content_type: force content type, e.g. "application/x-download" 352 @param disposition: file disposition, e.g. "attachment" 353 354 @note: path supplied must exist and point to a file 355 """ 356 if not os.path.exists(path): 357 warnings.warn(invalid_path_exists) 358 if not os.path.isabs(path): 359 warnings.warn(invalid_path_abs) 360 if os.path.isdir(path): 361 raise TypeError(invalid_path_dir) 362 363 self.name = name 364 self.path = path 365 self._content_type = content_type 366 self._disposition = disposition 367 368 if not self._content_type: 369 self._guess_content_type()
370
371 - def _guess_content_type(self):
372 """ Guesses content type for this file based on the filename. 373 374 Copyright (c) 2002-2008, CherryPy Team (team@cherrypy.org) 375 """ 376 self._content_type = mimetypes.types_map.get('.%s' % 377 self.path.split('.')[-1], 378 'text/plain') 379 log.debug('File %s type %s' % (self.path, self._content_type))
380
381 - def application(self, environ, start_response, response=None):
382 """ Application wsgi callback that processes a request. Must not be 383 called by the user. 384 385 @param response: used when the request was redirected to this file. If 386 not present, then this file was accessed directly (no 387 redirections) and in this case environ and 388 start_response must be passed accordingly. 389 """ 390 req = Request(environ) 391 392 if response: 393 # Redirect mode, see method doc and comments at the end 394 r = response 395 else: 396 # Normal request 397 r = Response(200, start_response) 398 399 if not os.path.exists(self.path): 400 log.warning('Received request on missing file: %s' % self.path) 401 return simple_response(500, r.start_response, 'File not available.') 402 403 try: 404 st = os.stat(self.path) 405 except OSError: 406 return simple_response(404, r.start_response) 407 408 r.body = open(self.path, 'rb') 409 content_length = st.st_size 410 411 h = r.headers 412 h['Last-modified'] = rfc822.formatdate(st.st_mtime) 413 h['Content-type'] = self._content_type 414 415 if self._disposition: 416 h['Content-disposition'] = '%s; filename="%s"' % \ 417 (self._disposition, self.name) 418 419 if req.protocol >= (1, 1): 420 h['Accept-ranges'] = 'bytes' 421 422 if 'range' not in req.headers: 423 # Send the whole response body 424 h['Content-length'] = str(content_length) 425 else: 426 ranges = get_byte_ranges(req.headers['Range'], content_length) 427 428 if ranges == []: 429 # Request range not satisfiable 430 r.status = 416 431 r.headers['Content-range'] = 'bytes */%s' % content_length 432 r.headers['Content-length'] = 0 433 r.body = [''] 434 elif ranges: 435 # Partial content status 436 r.status = 206 437 438 if len(ranges) == 1: 439 # Single part 440 setup_single_part_response(r, ranges[0], content_length) 441 442 else: 443 # Multipart 444 setup_multi_part_response(r, ranges, content_length, 445 self._content_type) 446 447 # Recalculate content length 448 s = 0 449 for ra in ranges: 450 s += ra[1] - ra[0] + 1 451 h['Content-length'] = str(s) 452 453 else: 454 # Lower protocols do not support ranges, whole body 455 h['Content-length'] = str(content_length) 456 457 if not response: 458 # Normal request, not redirected. When redirected, who respond()s is 459 # the caller. 460 r._respond() 461 462 return r.body
463
464 - def render(self, uri, request, response):
465 """ Enables the file to receive an URL redirection, that is, a 466 resource can return this file on the get_render() method. 467 """ 468 return self.application(request.env, None, response)
469
470 471 -class Resource(object):
472 """ Represents a resource or a folder on the webserver. 473 474 Inheritance from this class should be used when you need special and 475 accessing request attributes. 476 477 When a request arrives at a resource, it looks first inside his files 478 and resources. If no handler is found, it asks the get_render() method to 479 return who will render(uri, req, resp) the request - default is self. 480 """ 481
482 - def __init__(self, name):
483 """ Constructor for the Resource class. 484 485 @param name: resource name visible on the webserver 486 @type name: string 487 """ 488 self.name = name 489 self._tree = {}
490
491 - def render(self, uri, request, response):
492 """ Renders a request. Default action on a resource is doing nothing. 493 494 @note: should be overriden as needed 495 """ 496 return None
497
498 - def add_static_file(self, file):
499 """ Adds a static file to the resource. 500 501 @param file: file to add 502 @type file: StaticFile 503 504 @note: if the file name is already present on the tree, it will get 505 overriden 506 """ 507 if not isinstance(file, StaticFile): 508 raise ValueError('file must be a StaticFile instance.') 509 if file.name in self._tree: 510 warnings.warn('name override: %s' % file.name) 511 self._tree[file.name] = file
512
513 - def add_resource(self, resource):
514 """ Adds a resource to the resource. 515 516 @param resource: resource to add 517 @type resource: Resource 518 519 @note: if this resource is already present on the tree, it will get 520 overriden 521 """ 522 if not isinstance(resource, Resource): 523 raise ValueError('resource must be a Resource instance.') 524 if resource.name in self._tree: 525 warnings.warn('name override: %s' % resource.name) 526 self._tree[resource.name] = resource
527
528 - def application(self, environ, start_response):
529 """ WSGI application callback. May not be called directly by the 530 user. 531 """ 532 path = wsgiref.util.shift_path_info(environ) 533 534 if path in self._tree: 535 # Path directly available 536 return self._tree[path].application(environ, start_response) 537 else: 538 # End point or needs redirect. In case get_render is overriden, 539 # then a request should be handled by returned get_render() 540 # object. 541 req = Request(environ) 542 resp = Response(200, start_response) 543 544 render = self.get_render(req.uri, req.params) 545 546 if not render: 547 log.error('Could not find resource at %s' % req.uri) 548 return simple_response(404, start_response) 549 550 resp.body = render.render(req.uri, req, resp) 551 552 if not resp.body and not req.headers: 553 log.error('Body and headers were empty.') 554 return simple_response(404, start_response) 555 else: 556 resp._respond() 557 return resp.body 558 559 return simple_response(404, start_response)
560
561 - def get_render(self, uri, params):
562 """ Returns the default render for the given request, uri and params. 563 564 Default render is self. 565 """ 566 return self
567
568 569 -class CustomResource(Resource):
570 """ Same as Resource. 571 """ 572 pass
573
574 575 -class AdapterInterface(object):
576 """ Common interface for WSGI-servers adapters. 577 """ 578
579 - def setup(self, server_name, host, port, app_callback):
580 """ Setups the adapter. 581 """ 582 raise NotImplementedError
583
584 - def start(self):
585 """ Starts the WSGI server. 586 """ 587 raise NotImplementedError
588
589 - def stop(self):
590 """ Stops the WSGI server. 591 """ 592 raise NotImplementedError
593 594 @classmethod
595 - def is_available(cls):
596 """ Returns True if the adapter is available on the system. 597 """ 598 raise NotImplementedError
599
600 601 -class CherrypyAdapter(AdapterInterface):
602 """ Cherrypy WSGI server adapter. 603 """ 604
605 - def setup(self, server_name, host, port, app_callback):
606 from cherrypy import wsgiserver 607 self._server = wsgiserver.CherryPyWSGIServer((host, port), 608 app_callback, 609 server_name=server_name)
610
611 - def start(self):
612 self._server.start()
613
614 - def stop(self):
615 self._server.stop()
616 617 @classmethod
618 - def is_available(cls):
619 try: 620 from cherrypy import wsgiserver 621 return True 622 except ImportError: 623 return False
624
625 626 -class PasteAdapter(AdapterInterface):
627 """ Paste WSGI server adapter. 628 """ 629
630 - def setup(self, server_name, host, port, app_callback):
631 from paste.httpserver import serve 632 self._server = serve(app_callback, host, port, 633 start_loop=False)
634
635 - def start(self):
636 self._server.serve_forever()
637
638 - def stop(self):
639 self._server.server_close()
640 641 @classmethod
642 - def is_available(cls):
643 try: 644 from paste.httpserver import serve 645 return True 646 except ImportError: 647 return False
648
649 650 -class CircuitsWebAdapter(AdapterInterface):
651 """ circuits.web WSGI server adapter. 652 """ 653
654 - def setup(self, server_name, host, port, app_callback):
655 from circuits.web import BaseServer 656 from circuits.lib.web.wsgi import Gateway 657 self._server = BaseServer(port, host) 658 self._server += Gateway(app_callback)
659
660 - def start(self):
661 self._server.start()
662
663 - def stop(self):
664 self._server.stop()
665 666 @classmethod
667 - def is_available(cls):
668 try: 669 from circuits.web import BaseServer 670 from circuits.lib.web.wsgi import Gateway 671 return True 672 except ImportError: 673 return False
674 675 676 adapters = {'cherrypy': CherrypyAdapter, 677 'paste': PasteAdapter, 678 'circuits.web': CircuitsWebAdapter}
679 680 681 -def client_host(server_host):
682 """ Return the host on which a client can connect to the given listener. 683 684 Copyright (c) 2002-2008, CherryPy Team (team@cherrypy.org) 685 """ 686 if server_host == '0.0.0.0': 687 # 0.0.0.0 is INADDR_ANY, which should answer on localhost. 688 return '127.0.0.1' 689 if server_host == '::': 690 # :: is IN6ADDR_ANY, which should answer on localhost. 691 return '::1' 692 693 return server_host
694
695 -def check_port(host, port, timeout=1.0):
696 """ Raises an error if the given port is not free on the given host. 697 698 Copyright (c) 2002-2008, CherryPy Team (team@cherrypy.org) 699 """ 700 if not host: 701 raise ValueError("Host values of '' or None are not allowed.") 702 host = client_host(host) 703 port = int(port) 704 705 import socket 706 707 # AF_INET or AF_INET6 socket 708 # Get the correct address family for our host (allows IPv6 addresses) 709 for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC, 710 socket.SOCK_STREAM): 711 af, socktype, proto, canonname, sa = res 712 s = None 713 try: 714 s = socket.socket(af, socktype, proto) 715 # See http://groups.google.com/group/cherrypy-users/ 716 # browse_frm/thread/bbfe5eb39c904fe0 717 s.settimeout(timeout) 718 s.connect((host, port)) 719 s.close() 720 raise IOError("Port %s is in use on %s; perhaps the previous " 721 "httpserver did not shut down properly." % 722 (repr(port), repr(host))) 723 except socket.error: 724 if s: 725 s.close()
726
727 728 -def get_preferred_adapter():
729 """ Returns the preferred adapter, located at brisa.webserver_adapter 730 config entry. 731 """ 732 pref = config.get_parameter('brisa', 'webserver_adapter') 733 return adapters.get(pref, None)
734
735 736 -def get_available_adapters():
737 """ Returns a list of the available adapters. 738 """ 739 return [a for a in adapters.values() if a.is_available()]
740
741 742 -class WebServer(Resource):
743 """ Webserver class. 744 """ 745 746 msg_already_started = 'tried to start() WebServer when already started' 747 msg_already_stopped = 'tried to stop() WebServer when already stopped' 748 CustomResource = CustomResource 749 Resource = Resource 750 StaticFile = StaticFile 751
752 - def __init__(self, server_name='', host=None, port=None, adapter=None):
753 """ WebServer class constructor. 754 755 @param server_name: server name 756 @param host: host to listen on 757 @param port: port to listen on 758 @param adapter: optional, can receive an adapter retrieved with 759 get_available_adapters() 760 761 @type server_name: string 762 @type host: string 763 @type port: int 764 @type adapter: AdapterInterface 765 766 @note: the only parameter that should be always passed is the server 767 name. In most case scenarios, the others can be guessed/assigned 768 automatically by the webserver. 769 """ 770 Resource.__init__(self, '/') 771 self.server_name = server_name 772 self.host = None 773 self.port = None 774 self.adapter = None 775 self.running = False 776 777 if adapter and not issubclass(adapter, AdapterInterface): 778 # Invalid adapter 779 raise TypeError('Adapter must implement AdapterInterface') 780 elif adapter: 781 # Adapter OK 782 self.adapter = adapter() 783 else: 784 pref = get_preferred_adapter() 785 786 if not pref: 787 # First available adapter 788 available = get_available_adapters() 789 790 if len(available) > 0: 791 self.adapter = available[0]() 792 else: 793 # Preferred adapter 794 self.adapter = pref() 795 796 if not self.adapter: 797 log.critical('Could not select a adapter. Check the supported '\ 798 'adapters and install one of them on your system.') 799 raise SystemExit 800 801 if not self.host and not self.port: 802 self._generate_random_address() 803 else: 804 self.set_bind_address(host, port) 805 806 self.adapter.setup(self.server_name, self.host, self.port, 807 self.application)
808
809 - def set_adapter(self, adapter):
810 """ Sets the adapter (even after construction). 811 812 Do not call this after the webserver is started. 813 """ 814 if adapter and not issubclass(adapter, AdapterInterface): 815 raise TypeError('Adapter must implement AdapterInterface') 816 self.adapter = adapter() 817 self.adapter.setup(self.server_name, self.host, self.port, 818 self._app_callback)
819
820 - def start(self):
821 """ Starts the webserver. 822 """ 823 if not self.is_running(): 824 if not self.adapter: 825 raise RuntimeError('Adapter not set.') 826 threaded_call.run_async_function(self.adapter.start) 827 self.running = True 828 else: 829 log.warning(self.msg_already_started)
830
831 - def stop(self):
832 """ Stops the webserver. 833 """ 834 if self.is_running(): 835 if not self.adapter: 836 raise RuntimeError('Adapter not set') 837 self.adapter.stop() 838 else: 839 log.warning(self.msg_already_stopped)
840
841 - def get_listen_url(self):
842 """ Returns the URL currently set to listen on. 843 844 @rtype: string 845 """ 846 return 'http://%s:%d' % (self.host, self.port)
847
848 - def get_bind_address(self):
849 """ Returns the address currently bind to. 850 851 @return: address (host, port) 852 @rtype: tuple 853 """ 854 return (self.host, self.port)
855
856 - def set_bind_address(self, host, port):
857 """ Sets the listening url, if it is usable. 858 """ 859 if not self.check_url(host, port): 860 raise ValueError('URL cannot be used by the webserver.') 861 else: 862 self.host = host 863 self.port = port
864
865 - def get_host(self):
866 """ Returns the hostname of the URL currently set to listen on. 867 868 @return: host 869 @rtype: string 870 """ 871 return self.host
872
873 - def get_port(self):
874 """ Returns the port of the URL currently set to listen on. 875 876 @return: port 877 @rtype: int 878 """ 879 return self.port
880
881 - def is_running(self):
882 """ Returns True if the webserver is running, False otherwise. 883 """ 884 return self.running
885
886 - def check_url(self, host, port):
887 """ Returns True if the webserver will be able to listen on this URL, 888 False otherwise. 889 890 @param host: host 891 @param port: port 892 893 @type host: string 894 @type port: int 895 896 @return: if the URL is usable 897 @rtype: bool 898 """ 899 try: 900 check_port(host, port) 901 return True 902 except IOError: 903 return False
904
905 - def _generate_random_address(self):
906 # Default listen url: localhost:port where port is on the dynamic 907 # range (non registered ports) 908 ifaces = get_active_ifaces() 909 host = None 910 port = None 911 if ifaces: 912 host = get_ip_address(ifaces[0]) 913 else: 914 host = 'localhost' 915 916 while not port: 917 port = random.randint(49152, 65535) 918 try: 919 check_port(host, port) 920 except IOError: 921 port = 0 922 923 self.host = host 924 self.port = port
925