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)
269 return lambda caller=caller, app=app: caller(app)
270 return middleware
271
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