1   
  2   
  3   
  4  """ 
  5  This file is part of the web2py Web Framework 
  6  Copyrighted by Massimo Di Pierro <mdipierro@cs.depaul.edu> 
  7  License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html) 
  8   
  9  Contains: 
 10   
 11  - wsgibase: the gluon wsgi application 
 12   
 13  """ 
 14   
 15  if False: import import_all  
 16  import gc 
 17  import Cookie 
 18  import os 
 19  import re 
 20  import copy 
 21  import sys 
 22  import time 
 23  import datetime 
 24  import signal 
 25  import socket 
 26  import random 
 27  import urllib2 
 28  import string 
 29   
 30   
 31  try: 
 32      import simplejson as sj  
 33  except: 
 34      try: 
 35          import json as sj  
 36      except: 
 37          import gluon.contrib.simplejson as sj  
 38   
 39  from thread import allocate_lock 
 40   
 41  from gluon.fileutils import abspath, write_file 
 42  from gluon.settings import global_settings 
 43  from gluon.utils import web2py_uuid 
 44  from gluon.admin import add_path_first, create_missing_folders, create_missing_app_folders 
 45  from gluon.globals import current 
 46   
 47   
 48   
 49   
 50   
 51   
 52   
 53   
 54   
 55   
 56   
 57   
 58   
 59   
 60   
 61   
 62   
 63  web2py_path = global_settings.applications_parent   
 64   
 65  create_missing_folders() 
 66   
 67   
 68  import logging 
 69  import logging.config 
 70   
 71   
 72   
 73   
 74   
 75   
 76  import gluon.messageboxhandler 
 77  logging.gluon = gluon 
 78   
 79  import locale 
 80  locale.setlocale(locale.LC_CTYPE, "C")  
 81   
 82  exists = os.path.exists 
 83  pjoin = os.path.join 
 84   
 85  logpath = abspath("logging.conf") 
 86  if exists(logpath): 
 87      logging.config.fileConfig(abspath("logging.conf")) 
 88  else: 
 89      logging.basicConfig() 
 90  logger = logging.getLogger("web2py") 
 91   
 92  from gluon.restricted import RestrictedError 
 93  from gluon.http import HTTP, redirect 
 94  from gluon.globals import Request, Response, Session 
 95  from gluon.compileapp import build_environment, run_models_in, \ 
 96      run_controller_in, run_view_in 
 97  from gluon.contenttype import contenttype 
 98  from gluon.dal import BaseAdapter 
 99  from gluon.validators import CRYPT 
100  from gluon.html import URL, xmlescape 
101  from gluon.utils import is_valid_ip_address, getipaddrinfo 
102  from gluon.rewrite import load, url_in, THREAD_LOCAL as rwthread, \ 
103      try_rewrite_on_error, fixup_missing_path_info 
104  from gluon import newcron 
105   
106  __all__ = ['wsgibase', 'save_password', 'appfactory', 'HttpServer'] 
107   
108  requests = 0     
109   
110   
111   
112   
113   
114  regex_client = re.compile('[\w\-:]+(\.[\w\-]+)*\.?')   
115   
116  try: 
117      version_info = open(pjoin(global_settings.gluon_parent, 'VERSION'), 'r') 
118      raw_version_string = version_info.read().split()[-1].strip() 
119      version_info.close() 
120      global_settings.web2py_version = raw_version_string 
121      web2py_version = global_settings.web2py_version 
122  except: 
123      raise RuntimeError("Cannot determine web2py version") 
124   
125  try: 
126      from gluon import rocket 
127  except: 
128      if not global_settings.web2py_runtime_gae: 
129          logger.warn('unable to import Rocket') 
130   
131  load() 
132   
133  HTTPS_SCHEMES = set(('https', 'HTTPS')) 
137      """ 
138      guess the client address from the environment variables 
139   
140      first tries 'http_x_forwarded_for', secondly 'remote_addr' 
141      if all fails, assume '127.0.0.1' or '::1' (running locally) 
142      """ 
143      eget = env.get 
144      g = regex_client.search(eget('http_x_forwarded_for', '')) 
145      client = (g.group() or '').split(',')[0] if g else None 
146      if client in (None, '', 'unknown'): 
147          g = regex_client.search(eget('remote_addr', '')) 
148          if g: 
149              client = g.group() 
150          elif env.http_host.startswith('['):   
151              client = '::1' 
152          else: 
153              client = '127.0.0.1'   
154      if not is_valid_ip_address(client): 
155          raise HTTP(400, "Bad Request (request.client=%s)" % client) 
156      return client 
 157   
162      """ 
163      this function is used to generate a dynamic page. 
164      It first runs all models, then runs the function in the controller, 
165      and then tries to render the output using a view/template. 
166      this function must run from the [application] folder. 
167      A typical example would be the call to the url 
168      /[application]/[controller]/[function] that would result in a call 
169      to [function]() in applications/[application]/[controller].py 
170      rendered by applications/[application]/views/[controller]/[function].html 
171      """ 
172   
173       
174       
175       
176   
177      environment = build_environment(request, response, session) 
178   
179       
180   
181      response.view = '%s/%s.%s' % (request.controller, 
182                                    request.function, 
183                                    request.extension) 
184   
185       
186       
187       
188       
189   
190      run_models_in(environment) 
191      response._view_environment = copy.copy(environment) 
192      page = run_controller_in(request.controller, request.function, environment) 
193      if isinstance(page, dict): 
194          response._vars = page 
195          response._view_environment.update(page) 
196          run_view_in(response._view_environment) 
197          page = response.body.getvalue() 
198       
199      global requests 
200      requests = ('requests' in globals()) and (requests + 1) % 100 or 0 
201      if not requests: 
202          gc.collect() 
203       
204   
205       
206       
207       
208   
209      default_headers = [ 
210          ('Content-Type', contenttype('.' + request.extension)), 
211          ('Cache-Control', 
212           'no-store, no-cache, must-revalidate, post-check=0, pre-check=0'), 
213          ('Expires', time.strftime('%a, %d %b %Y %H:%M:%S GMT', 
214                                    time.gmtime())), 
215          ('Pragma', 'no-cache')] 
216      for key, value in default_headers: 
217          response.headers.setdefault(key, value) 
218   
219      raise HTTP(response.status, page, **response.headers) 
 220   
223 -    def __init__(self, environ, request, response): 
 227      @property 
229          if not hasattr(self,'_environ'): 
230              new_environ = self.wsgi_environ 
231              new_environ['wsgi.input'] = self.request.body 
232              new_environ['wsgi.version'] = 1 
233              self._environ = new_environ 
234          return self._environ 
 236          """ 
237          in controller you can use:: 
238   
239          - request.wsgi.environ 
240          - request.wsgi.start_response 
241   
242          to call third party WSGI applications 
243          """ 
244          self.response.status = str(status).split(' ', 1)[0] 
245          self.response.headers = dict(headers) 
246          return lambda *args, **kargs: \ 
247              self.response.write(escape=False, *args, **kargs) 
 249          """ 
250          In you controller use:: 
251   
252          @request.wsgi.middleware(middleware1, middleware2, ...) 
253   
254          to decorate actions with WSGI middleware. actions must return strings. 
255          uses a simulated environment so it may have weird behavior in some cases 
256          """ 
257          def middleware(f): 
258              def app(environ, start_response): 
259                  data = f() 
260                  start_response(self.response.status, 
261                                 self.response.headers.items()) 
262                  if isinstance(data, list): 
263                      return data 
264                  return [data] 
 265              for item in middleware_apps: 
266                  app = item(app) 
267              def caller(app): 
268                  return app(self.environ, self.start_response) 
273      """ 
274      this is the gluon wsgi application. the first function called when a page 
275      is requested (static or dynamic). it can be called by paste.httpserver 
276      or by apache mod_wsgi. 
277   
278        - fills request with info 
279        - the environment variables, replacing '.' with '_' 
280        - adds web2py path and version info 
281        - compensates for fcgi missing path_info and query_string 
282        - validates the path in url 
283   
284      The url path must be either: 
285   
286      1. for static pages: 
287   
288        - /<application>/static/<file> 
289   
290      2. for dynamic pages: 
291   
292        - /<application>[/<controller>[/<function>[/<sub>]]][.<extension>] 
293        - (sub may go several levels deep, currently 3 levels are supported: 
294           sub1/sub2/sub3) 
295   
296      The naming conventions are: 
297   
298        - application, controller, function and extension may only contain 
299          [a-zA-Z0-9_] 
300        - file and sub may also contain '-', '=', '.' and '/' 
301      """ 
302      eget = environ.get 
303      current.__dict__.clear() 
304      request = Request(environ) 
305      response = Response() 
306      session = Session() 
307      env = request.env 
308       
309      env.web2py_version = web2py_version 
310       
311      static_file = False 
312      try: 
313          try: 
314              try: 
315                   
316                   
317                   
318                   
319                   
320                   
321                   
322                   
323   
324                  fixup_missing_path_info(environ) 
325                  (static_file, version, environ) = url_in(request, environ) 
326                  response.status = env.web2py_status_code or response.status 
327   
328                  if static_file: 
329                      if eget('QUERY_STRING', '').startswith('attachment'): 
330                          response.headers['Content-Disposition'] \ 
331                              = 'attachment' 
332                      if version: 
333                          response.headers['Cache-Control'] = 'max-age=315360000' 
334                          response.headers[ 
335                              'Expires'] = 'Thu, 31 Dec 2037 23:59:59 GMT' 
336                      response.stream(static_file, request=request) 
337   
338   
339                   
340                   
341                   
342                  app = request.application   
343   
344                  if not global_settings.local_hosts: 
345                      local_hosts = set(['127.0.0.1', '::ffff:127.0.0.1', '::1']) 
346                      if not global_settings.web2py_runtime_gae: 
347                          try: 
348                              fqdn = socket.getfqdn() 
349                              local_hosts.add(socket.gethostname()) 
350                              local_hosts.add(fqdn) 
351                              local_hosts.update([ 
352                                  addrinfo[4][0] for addrinfo 
353                                  in getipaddrinfo(fqdn)]) 
354                              if env.server_name: 
355                                  local_hosts.add(env.server_name) 
356                                  local_hosts.update([ 
357                                          addrinfo[4][0] for addrinfo 
358                                          in getipaddrinfo(env.server_name)]) 
359                          except (socket.gaierror, TypeError): 
360                              pass 
361                      global_settings.local_hosts = list(local_hosts) 
362                  else: 
363                      local_hosts = global_settings.local_hosts 
364                  client = get_client(env) 
365                  x_req_with = str(env.http_x_requested_with).lower() 
366   
367                  request.update( 
368                      client = client, 
369                      folder = abspath('applications', app) + os.sep, 
370                      ajax = x_req_with == 'xmlhttprequest', 
371                      cid = env.http_web2py_component_element, 
372                      is_local = env.remote_addr in local_hosts, 
373                      is_https = env.wsgi_url_scheme in HTTPS_SCHEMES or \ 
374                          request.env.http_x_forwarded_proto in HTTPS_SCHEMES \ 
375                          or env.https == 'on' 
376                      ) 
377                  request.compute_uuid()   
378                  request.url = environ['PATH_INFO'] 
379   
380                   
381                   
382                   
383   
384                  disabled = pjoin(request.folder, 'DISABLED') 
385                  if not exists(request.folder): 
386                      if app == rwthread.routes.default_application \ 
387                              and app != 'welcome': 
388                          redirect(URL('welcome', 'default', 'index')) 
389                      elif rwthread.routes.error_handler: 
390                          _handler = rwthread.routes.error_handler 
391                          redirect(URL(_handler['application'], 
392                                       _handler['controller'], 
393                                       _handler['function'], 
394                                       args=app)) 
395                      else: 
396                          raise HTTP(404, rwthread.routes.error_message 
397                                     % 'invalid request', 
398                                     web2py_error='invalid application') 
399                  elif not request.is_local and exists(disabled): 
400                      raise HTTP(503, "<html><body><h1>Temporarily down for maintenance</h1></body></html>") 
401   
402                   
403                   
404                   
405   
406                  create_missing_app_folders(request) 
407   
408                   
409                   
410                   
411   
412                   
413   
414                   
415                   
416                   
417   
418                  request.wsgi = LazyWSGI(environ, request, response) 
419   
420                   
421                   
422                   
423   
424                  if env.http_cookie: 
425                      try: 
426                          request.cookies.load(env.http_cookie) 
427                      except Cookie.CookieError, e: 
428                          pass   
429   
430                   
431                   
432                   
433   
434                  if not env.web2py_disable_session: 
435                      session.connect(request, response) 
436   
437                   
438                   
439                   
440   
441                  if global_settings.debugging and app != "admin": 
442                      import gluon.debug 
443                       
444                      gluon.debug.dbg.do_debug(mainpyfile=request.folder) 
445   
446                  serve_controller(request, response, session) 
447   
448              except HTTP, http_response: 
449   
450                  if static_file: 
451                      return http_response.to(responder, env=env) 
452   
453                  if request.body: 
454                      request.body.close() 
455   
456                  if hasattr(current,'request'): 
457   
458                       
459                       
460                       
461                      session._try_store_in_db(request, response) 
462   
463                       
464                       
465                       
466   
467                      if response.do_not_commit is True: 
468                          BaseAdapter.close_all_instances(None) 
469                      elif response.custom_commit: 
470                          BaseAdapter.close_all_instances(response.custom_commit) 
471                      else: 
472                          BaseAdapter.close_all_instances('commit') 
473   
474                       
475                       
476                       
477                       
478   
479                      session._try_store_in_cookie_or_file(request, response) 
480   
481                       
482                      if request.cid: 
483                          http_response.headers.setdefault( 
484                              'web2py-component-content', 'replace') 
485   
486                      if request.ajax: 
487                          if response.flash: 
488                              http_response.headers['web2py-component-flash'] = \ 
489                                  urllib2.quote(xmlescape(response.flash)\ 
490                                                    .replace('\n','')) 
491                          if response.js: 
492                              http_response.headers['web2py-component-command'] = \ 
493                                  urllib2.quote(response.js.replace('\n','')) 
494   
495                       
496                       
497                       
498   
499                      session._fixup_before_save() 
500                      http_response.cookies2headers(response.cookies) 
501   
502                  ticket = None 
503   
504              except RestrictedError, e: 
505   
506                  if request.body: 
507                      request.body.close() 
508   
509                   
510                   
511                   
512   
513                   
514                  if not request.tickets_db: 
515                      ticket = e.log(request) or 'unknown' 
516                   
517                  if response._custom_rollback: 
518                      response._custom_rollback() 
519                  else: 
520                      BaseAdapter.close_all_instances('rollback') 
521                   
522                  if request.tickets_db: 
523                      ticket = e.log(request) or 'unknown' 
524   
525                  http_response = \ 
526                      HTTP(500, rwthread.routes.error_message_ticket % 
527                           dict(ticket=ticket), 
528                           web2py_error='ticket %s' % ticket) 
529   
530          except: 
531   
532              if request.body: 
533                  request.body.close() 
534   
535               
536               
537               
538   
539              try: 
540                  if response._custom_rollback: 
541                      response._custom_rollback() 
542                  else: 
543                      BaseAdapter.close_all_instances('rollback') 
544              except: 
545                  pass 
546              e = RestrictedError('Framework', '', '', locals()) 
547              ticket = e.log(request) or 'unrecoverable' 
548              http_response = \ 
549                  HTTP(500, rwthread.routes.error_message_ticket 
550                       % dict(ticket=ticket), 
551                       web2py_error='ticket %s' % ticket) 
552   
553      finally: 
554          if response and hasattr(response, 'session_file') \ 
555                  and response.session_file: 
556              response.session_file.close() 
557   
558      session._unlock(response) 
559      http_response, new_environ = try_rewrite_on_error( 
560          http_response, request, environ, ticket) 
561      if not http_response: 
562          return wsgibase(new_environ, responder) 
563      if global_settings.web2py_crontype == 'soft': 
564          newcron.softcron(global_settings.applications_parent).start() 
565      return http_response.to(responder, env=env) 
 566   
569      """ 
570      used by main() to save the password in the parameters_port.py file. 
571      """ 
572   
573      password_file = abspath('parameters_%i.py' % port) 
574      if password == '<random>': 
575           
576          chars = string.letters + string.digits 
577          password = ''.join([random.choice(chars) for i in range(8)]) 
578          cpassword = CRYPT()(password)[0] 
579          print '******************* IMPORTANT!!! ************************' 
580          print 'your admin password is "%s"' % password 
581          print '*********************************************************' 
582      elif password == '<recycle>': 
583           
584          if exists(password_file): 
585              return 
586          else: 
587              password = '' 
588      elif password.startswith('<pam_user:'): 
589           
590          cpassword = password[1:-1] 
591      else: 
592           
593          cpassword = CRYPT()(password)[0] 
594      fp = open(password_file, 'w') 
595      if password: 
596          fp.write('password="%s"\n' % cpassword) 
597      else: 
598          fp.write('password=None\n') 
599      fp.close() 
 600   
601   
602 -def appfactory(wsgiapp=wsgibase, 
603                 logfilename='httpserver.log', 
604                 profiler_dir=None, 
605                 profilerfilename=None): 
 606      """ 
607      generates a wsgi application that does logging and profiling and calls 
608      wsgibase 
609   
610      .. function:: gluon.main.appfactory( 
611              [wsgiapp=wsgibase 
612              [, logfilename='httpserver.log' 
613              [, profilerfilename='profiler.log']]]) 
614   
615      """ 
616      if profilerfilename is not None: 
617          raise BaseException("Deprecated API") 
618      if profiler_dir: 
619          profiler_dir = abspath(profiler_dir) 
620          logger.warn('profiler is on. will use dir %s', profiler_dir) 
621          if not os.path.isdir(profiler_dir): 
622              try: 
623                  os.makedirs(profiler_dir) 
624              except: 
625                  raise BaseException("Can't create dir %s" % profiler_dir) 
626          filepath = pjoin(profiler_dir, 'wtest') 
627          try: 
628              filehandle = open( filepath, 'w' ) 
629              filehandle.close() 
630              os.unlink(filepath) 
631          except IOError: 
632              raise BaseException("Unable to write to dir %s" % profiler_dir) 
633   
634      def app_with_logging(environ, responder): 
635          """ 
636          a wsgi app that does logging and profiling and calls wsgibase 
637          """ 
638          status_headers = [] 
639   
640          def responder2(s, h): 
641              """ 
642              wsgi responder app 
643              """ 
644              status_headers.append(s) 
645              status_headers.append(h) 
646              return responder(s, h) 
 647   
648          time_in = time.time() 
649          ret = [0] 
650          if not profiler_dir: 
651              ret[0] = wsgiapp(environ, responder2) 
652          else: 
653              import cProfile 
654              prof = cProfile.Profile() 
655              prof.enable() 
656              ret[0] = wsgiapp(environ, responder2) 
657              prof.disable() 
658              destfile = pjoin(profiler_dir, "req_%s.prof" % web2py_uuid()) 
659              prof.dump_stats(destfile) 
660   
661          try: 
662              line = '%s, %s, %s, %s, %s, %s, %f\n' % ( 
663                  environ['REMOTE_ADDR'], 
664                  datetime.datetime.today().strftime('%Y-%m-%d %H:%M:%S'), 
665                  environ['REQUEST_METHOD'], 
666                  environ['PATH_INFO'].replace(',', '%2C'), 
667                  environ['SERVER_PROTOCOL'], 
668                  (status_headers[0])[:3], 
669                  time.time() - time_in, 
670              ) 
671              if not logfilename: 
672                  sys.stdout.write(line) 
673              elif isinstance(logfilename, str): 
674                  write_file(logfilename, line, 'a') 
675              else: 
676                  logfilename.write(line) 
677          except: 
678              pass 
679          return ret[0] 
680   
681      return app_with_logging 
682   
684      """ 
685      the web2py web server (Rocket) 
686      """ 
687   
688 -    def __init__( 
689          self, 
690          ip='127.0.0.1', 
691          port=8000, 
692          password='', 
693          pid_filename='httpserver.pid', 
694          log_filename='httpserver.log', 
695          profiler_dir=None, 
696          ssl_certificate=None, 
697          ssl_private_key=None, 
698          ssl_ca_certificate=None, 
699          min_threads=None, 
700          max_threads=None, 
701          server_name=None, 
702          request_queue_size=5, 
703          timeout=10, 
704          socket_timeout=1, 
705          shutdown_timeout=None,   
706          path=None, 
707          interfaces=None   
708      ): 
 709          """ 
710          starts the web server. 
711          """ 
712   
713          if interfaces: 
714               
715               
716              import types 
717              if isinstance(interfaces, types.ListType): 
718                  for i in interfaces: 
719                      if not isinstance(i, types.TupleType): 
720                          raise "Wrong format for rocket interfaces parameter - see http://packages.python.org/rocket/" 
721              else: 
722                  raise "Wrong format for rocket interfaces parameter - see http://packages.python.org/rocket/" 
723   
724          if path: 
725               
726               
727              global web2py_path 
728              path = os.path.normpath(path) 
729              web2py_path = path 
730              global_settings.applications_parent = path 
731              os.chdir(path) 
732              [add_path_first(p) for p in (path, abspath('site-packages'), "")] 
733              if exists("logging.conf"): 
734                  logging.config.fileConfig("logging.conf") 
735   
736          save_password(password, port) 
737          self.pid_filename = pid_filename 
738          if not server_name: 
739              server_name = socket.gethostname() 
740          logger.info('starting web server...') 
741          rocket.SERVER_NAME = server_name 
742          rocket.SOCKET_TIMEOUT = socket_timeout 
743          sock_list = [ip, port] 
744          if not ssl_certificate or not ssl_private_key: 
745              logger.info('SSL is off') 
746          elif not rocket.ssl: 
747              logger.warning('Python "ssl" module unavailable. SSL is OFF') 
748          elif not exists(ssl_certificate): 
749              logger.warning('unable to open SSL certificate. SSL is OFF') 
750          elif not exists(ssl_private_key): 
751              logger.warning('unable to open SSL private key. SSL is OFF') 
752          else: 
753              sock_list.extend([ssl_private_key, ssl_certificate]) 
754              if ssl_ca_certificate: 
755                  sock_list.append(ssl_ca_certificate) 
756   
757              logger.info('SSL is ON') 
758          app_info = {'wsgi_app': appfactory(wsgibase, 
759                                             log_filename, 
760                                             profiler_dir)} 
761   
762          self.server = rocket.Rocket(interfaces or tuple(sock_list), 
763                                      method='wsgi', 
764                                      app_info=app_info, 
765                                      min_threads=min_threads, 
766                                      max_threads=max_threads, 
767                                      queue_size=int(request_queue_size), 
768                                      timeout=int(timeout), 
769                                      handle_signals=False, 
770                                      ) 
 771   
773          """ 
774          start the web server 
775          """ 
776          try: 
777              signal.signal(signal.SIGTERM, lambda a, b, s=self: s.stop()) 
778              signal.signal(signal.SIGINT, lambda a, b, s=self: s.stop()) 
779          except: 
780              pass 
781          write_file(self.pid_filename, str(os.getpid())) 
782          self.server.start() 
 783   
784 -    def stop(self, stoplogging=False): 
 785          """ 
786          stop cron and the web server 
787          """ 
788          newcron.stopcron() 
789          self.server.stop(stoplogging) 
790          try: 
791              os.unlink(self.pid_filename) 
792          except: 
793              pass 
  794