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 dumps(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:
528 del response.cookies[response.session_id_name]
529 elif session._secure:
530 response.cookies[response.session_id_name]['secure'] = True
531 if len(response.cookies)>0:
532 http_response.headers['Set-Cookie'] = \
533 [str(cookie)[11:] for cookie in response.cookies.values()]
534 ticket=None
535
536 except RestrictedError, e:
537
538 if request.body:
539 request.body.close()
540
541
542
543
544
545 ticket = e.log(request) or 'unknown'
546 if response._custom_rollback:
547 response._custom_rollback()
548 else:
549 BaseAdapter.close_all_instances('rollback')
550
551 http_response = \
552 HTTP(500,
553 rewrite.thread.routes.error_message_ticket % dict(ticket=ticket),
554 web2py_error='ticket %s' % ticket)
555
556 except:
557
558 if request.body:
559 request.body.close()
560
561
562
563
564
565 try:
566 if response._custom_rollback:
567 response._custom_rollback()
568 else:
569 BaseAdapter.close_all_instances('rollback')
570 except:
571 pass
572 e = RestrictedError('Framework', '', '', locals())
573 ticket = e.log(request) or 'unrecoverable'
574 http_response = \
575 HTTP(500,
576 rewrite.thread.routes.error_message_ticket % dict(ticket=ticket),
577 web2py_error='ticket %s' % ticket)
578
579 finally:
580 if response and hasattr(response, 'session_file') and response.session_file:
581 response.session_file.close()
582
583
584
585
586 session._unlock(response)
587 http_response = rewrite.try_redirect_on_error(http_response,request,ticket)
588 if global_settings.web2py_crontype == 'soft':
589 newcron.softcron(global_settings.applications_parent).start()
590 return http_response.to(responder)
591
592
594 """
595 used by main() to save the password in the parameters_port.py file.
596 """
597
598 password_file = abspath('parameters_%i.py' % port)
599 if password == '<random>':
600
601 chars = string.letters + string.digits
602 password = ''.join([random.choice(chars) for i in range(8)])
603 cpassword = CRYPT()(password)[0]
604 print '******************* IMPORTANT!!! ************************'
605 print 'your admin password is "%s"' % password
606 print '*********************************************************'
607 elif password == '<recycle>':
608
609 if os.path.exists(password_file):
610 return
611 else:
612 password = ''
613 elif password.startswith('<pam_user:'):
614
615 cpassword = password[1:-1]
616 else:
617
618 cpassword = CRYPT()(password)[0]
619 fp = open(password_file, 'w')
620 if password:
621 fp.write('password="%s"\n' % cpassword)
622 else:
623 fp.write('password=None\n')
624 fp.close()
625
626
627 -def appfactory(wsgiapp=wsgibase,
628 logfilename='httpserver.log',
629 profilerfilename='profiler.log'):
630 """
631 generates a wsgi application that does logging and profiling and calls
632 wsgibase
633
634 .. function:: gluon.main.appfactory(
635 [wsgiapp=wsgibase
636 [, logfilename='httpserver.log'
637 [, profilerfilename='profiler.log']]])
638
639 """
640 if profilerfilename and os.path.exists(profilerfilename):
641 os.unlink(profilerfilename)
642 locker = thread.allocate_lock()
643
644 def app_with_logging(environ, responder):
645 """
646 a wsgi app that does logging and profiling and calls wsgibase
647 """
648 status_headers = []
649
650 def responder2(s, h):
651 """
652 wsgi responder app
653 """
654 status_headers.append(s)
655 status_headers.append(h)
656 return responder(s, h)
657
658 time_in = time.time()
659 ret = [0]
660 if not profilerfilename:
661 ret[0] = wsgiapp(environ, responder2)
662 else:
663 import cProfile
664 import pstats
665 logger.warn('profiler is on. this makes web2py slower and serial')
666
667 locker.acquire()
668 cProfile.runctx('ret[0] = wsgiapp(environ, responder2)',
669 globals(), locals(), profilerfilename+'.tmp')
670 stat = pstats.Stats(profilerfilename+'.tmp')
671 stat.stream = cStringIO.StringIO()
672 stat.strip_dirs().sort_stats("time").print_stats(80)
673 profile_out = stat.stream.getvalue()
674 profile_file = open(profilerfilename, 'a')
675 profile_file.write('%s\n%s\n%s\n%s\n\n' % \
676 ('='*60, environ['PATH_INFO'], '='*60, profile_out))
677 profile_file.close()
678 locker.release()
679 try:
680 line = '%s, %s, %s, %s, %s, %s, %f\n' % (
681 environ['REMOTE_ADDR'],
682 datetime.datetime.today().strftime('%Y-%m-%d %H:%M:%S'),
683 environ['REQUEST_METHOD'],
684 environ['PATH_INFO'].replace(',', '%2C'),
685 environ['SERVER_PROTOCOL'],
686 (status_headers[0])[:3],
687 time.time() - time_in,
688 )
689 if not logfilename:
690 sys.stdout.write(line)
691 elif isinstance(logfilename, str):
692 write_file(logfilename, line, 'a')
693 else:
694 logfilename.write(line)
695 except:
696 pass
697 return ret[0]
698
699 return app_with_logging
700
701
703 """
704 the web2py web server (Rocket)
705 """
706
707 - def __init__(
708 self,
709 ip='127.0.0.1',
710 port=8000,
711 password='',
712 pid_filename='httpserver.pid',
713 log_filename='httpserver.log',
714 profiler_filename=None,
715 ssl_certificate=None,
716 ssl_private_key=None,
717 min_threads=None,
718 max_threads=None,
719 server_name=None,
720 request_queue_size=5,
721 timeout=10,
722 shutdown_timeout=None,
723 path=None,
724 interfaces=None
725 ):
726 """
727 starts the web server.
728 """
729
730 if interfaces:
731
732
733 import types
734 if isinstance(interfaces,types.ListType):
735 for i in interfaces:
736 if not isinstance(i,types.TupleType):
737 raise "Wrong format for rocket interfaces parameter - see http://packages.python.org/rocket/"
738 else:
739 raise "Wrong format for rocket interfaces parameter - see http://packages.python.org/rocket/"
740
741 if path:
742
743
744 global web2py_path
745 path = os.path.normpath(path)
746 web2py_path = path
747 global_settings.applications_parent = path
748 os.chdir(path)
749 [add_path_first(p) for p in (path, abspath('site-packages'), "")]
750
751 save_password(password, port)
752 self.pid_filename = pid_filename
753 if not server_name:
754 server_name = socket.gethostname()
755 logger.info('starting web server...')
756 rocket.SERVER_NAME = server_name
757 sock_list = [ip, port]
758 if not ssl_certificate or not ssl_private_key:
759 logger.info('SSL is off')
760 elif not rocket.ssl:
761 logger.warning('Python "ssl" module unavailable. SSL is OFF')
762 elif not os.path.exists(ssl_certificate):
763 logger.warning('unable to open SSL certificate. SSL is OFF')
764 elif not os.path.exists(ssl_private_key):
765 logger.warning('unable to open SSL private key. SSL is OFF')
766 else:
767 sock_list.extend([ssl_private_key, ssl_certificate])
768 logger.info('SSL is ON')
769 app_info = {'wsgi_app': appfactory(wsgibase,
770 log_filename,
771 profiler_filename) }
772
773 self.server = rocket.Rocket(interfaces or tuple(sock_list),
774 method='wsgi',
775 app_info=app_info,
776 min_threads=min_threads,
777 max_threads=max_threads,
778 queue_size=int(request_queue_size),
779 timeout=int(timeout),
780 handle_signals=False,
781 )
782
783
785 """
786 start the web server
787 """
788 try:
789 signal.signal(signal.SIGTERM, lambda a, b, s=self: s.stop())
790 signal.signal(signal.SIGINT, lambda a, b, s=self: s.stop())
791 except:
792 pass
793 write_file(self.pid_filename, str(os.getpid()))
794 self.server.start()
795
796 - def stop(self, stoplogging=False):
797 """
798 stop cron and the web server
799 """
800 newcron.stopcron()
801 self.server.stop(stoplogging)
802 try:
803 os.unlink(self.pid_filename)
804 except:
805 pass
806