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