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