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