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