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