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