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