1 | # -*- coding: utf-8 -*- |
---|
2 | |
---|
3 | """ |
---|
4 | | This file is part of the web2py Web Framework |
---|
5 | | Copyrighted by Massimo Di Pierro <mdipierro@cs.depaul.edu> |
---|
6 | | License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html) |
---|
7 | |
---|
8 | The gluon wsgi application |
---|
9 | --------------------------- |
---|
10 | """ |
---|
11 | |
---|
12 | if False: |
---|
13 | from . import import_all # DO NOT REMOVE PART OF FREEZE PROCESS |
---|
14 | import gc |
---|
15 | |
---|
16 | import os |
---|
17 | import re |
---|
18 | import copy |
---|
19 | import sys |
---|
20 | import time |
---|
21 | import datetime |
---|
22 | import signal |
---|
23 | import socket |
---|
24 | import random |
---|
25 | import string |
---|
26 | |
---|
27 | from gluon._compat import Cookie, urllib_quote |
---|
28 | # from thread import allocate_lock |
---|
29 | |
---|
30 | from gluon.fileutils import abspath, read_file, write_file, create_missing_folders, create_missing_app_folders, \ |
---|
31 | add_path_first |
---|
32 | from gluon.settings import global_settings |
---|
33 | from gluon.utils import web2py_uuid, unlocalised_http_header_date |
---|
34 | from gluon.globals import current |
---|
35 | |
---|
36 | # Remarks: |
---|
37 | # calling script has inserted path to script directory into sys.path |
---|
38 | # applications_parent (path to applications/, site-packages/ etc) |
---|
39 | # defaults to that directory set sys.path to |
---|
40 | # ("", gluon_parent/site-packages, gluon_parent, ...) |
---|
41 | # |
---|
42 | # this is wrong: |
---|
43 | # web2py_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) |
---|
44 | # because we do not want the path to this file which may be Library.zip |
---|
45 | # gluon_parent is the directory containing gluon, web2py.py, logging.conf |
---|
46 | # and the handlers. |
---|
47 | # applications_parent (web2py_path) is the directory containing applications/ |
---|
48 | # and routes.py |
---|
49 | # The two are identical unless web2py_path is changed via the web2py.py -f folder option |
---|
50 | # main.web2py_path is the same as applications_parent (for backward compatibility) |
---|
51 | |
---|
52 | web2py_path = global_settings.applications_parent # backward compatibility |
---|
53 | |
---|
54 | create_missing_folders() |
---|
55 | |
---|
56 | # set up logging for subsequent imports |
---|
57 | import logging.config |
---|
58 | |
---|
59 | # This needed to prevent exception on Python 2.5: |
---|
60 | # NameError: name 'gluon' is not defined |
---|
61 | # See http://bugs.python.org/issue1436 |
---|
62 | |
---|
63 | # attention!, the import Tkinter in messageboxhandler, changes locale ... |
---|
64 | import gluon.messageboxhandler |
---|
65 | logging.gluon = gluon |
---|
66 | # so we must restore it! Thanks ozancag |
---|
67 | import locale |
---|
68 | locale.setlocale(locale.LC_CTYPE, "C") # IMPORTANT, web2py requires locale "C" |
---|
69 | |
---|
70 | exists = os.path.exists |
---|
71 | pjoin = os.path.join |
---|
72 | |
---|
73 | try: |
---|
74 | logging.config.fileConfig(abspath("logging.conf")) |
---|
75 | except: # fails on GAE or when logfile is missing |
---|
76 | logging.basicConfig() |
---|
77 | logger = logging.getLogger("web2py") |
---|
78 | |
---|
79 | from gluon.restricted import RestrictedError |
---|
80 | from gluon.http import HTTP, redirect |
---|
81 | from gluon.globals import Request, Response, Session |
---|
82 | from gluon.compileapp import build_environment, run_models_in, \ |
---|
83 | run_controller_in, run_view_in |
---|
84 | from gluon.contenttype import contenttype |
---|
85 | from pydal.base import BaseAdapter |
---|
86 | from gluon.validators import CRYPT |
---|
87 | from gluon.html import URL, xmlescape |
---|
88 | from gluon.utils import is_valid_ip_address, getipaddrinfo |
---|
89 | from gluon.rewrite import load as load_routes, url_in, THREAD_LOCAL as rwthread, \ |
---|
90 | try_rewrite_on_error, fixup_missing_path_info |
---|
91 | from gluon import newcron |
---|
92 | |
---|
93 | __all__ = ['wsgibase', 'save_password', 'appfactory', 'HttpServer'] |
---|
94 | |
---|
95 | requests = 0 # gc timer |
---|
96 | |
---|
97 | # Security Checks: validate URL and session_id here, |
---|
98 | # accept_language is validated in languages |
---|
99 | |
---|
100 | try: |
---|
101 | version_info = read_file(pjoin(global_settings.gluon_parent, 'VERSION')) |
---|
102 | raw_version_string = version_info.split()[-1].strip() |
---|
103 | global_settings.web2py_version = raw_version_string |
---|
104 | web2py_version = global_settings.web2py_version |
---|
105 | except: |
---|
106 | raise RuntimeError("Cannot determine web2py version") |
---|
107 | |
---|
108 | try: |
---|
109 | from gluon import rocket |
---|
110 | except: |
---|
111 | if not global_settings.web2py_runtime_gae: |
---|
112 | logger.warn('unable to import Rocket') |
---|
113 | |
---|
114 | load_routes() |
---|
115 | |
---|
116 | HTTPS_SCHEMES = set(('https', 'HTTPS')) |
---|
117 | |
---|
118 | |
---|
119 | # pattern used to match client IP address |
---|
120 | REGEX_CLIENT = re.compile(r'[\w:]+(\.\w+)*') |
---|
121 | |
---|
122 | def get_client(env): |
---|
123 | """ |
---|
124 | Guesses the client address from the environment variables |
---|
125 | |
---|
126 | First tries 'http_x_forwarded_for', secondly 'remote_addr' |
---|
127 | if all fails, assume '127.0.0.1' or '::1' (running locally) |
---|
128 | """ |
---|
129 | eget = env.get |
---|
130 | m = REGEX_CLIENT.search(eget('http_x_forwarded_for', '')) |
---|
131 | client = m and m.group() |
---|
132 | if client in (None, '', 'unknown'): |
---|
133 | m = REGEX_CLIENT.search(eget('remote_addr', '')) |
---|
134 | if m: |
---|
135 | client = m.group() |
---|
136 | elif env.http_host.startswith('['): # IPv6 |
---|
137 | client = '::1' |
---|
138 | else: |
---|
139 | client = '127.0.0.1' # IPv4 |
---|
140 | if not is_valid_ip_address(client): |
---|
141 | raise HTTP(400, "Bad Request (request.client=%s)" % client) |
---|
142 | return client |
---|
143 | |
---|
144 | |
---|
145 | def serve_controller(request, response, session): |
---|
146 | """ |
---|
147 | This function is used to generate a dynamic page. |
---|
148 | It first runs all models, then runs the function in the controller, |
---|
149 | and then tries to render the output using a view/template. |
---|
150 | this function must run from the [application] folder. |
---|
151 | A typical example would be the call to the url |
---|
152 | /[application]/[controller]/[function] that would result in a call |
---|
153 | to [function]() in applications/[application]/[controller].py |
---|
154 | rendered by applications/[application]/views/[controller]/[function].html |
---|
155 | """ |
---|
156 | |
---|
157 | # ################################################## |
---|
158 | # build environment for controller and view |
---|
159 | # ################################################## |
---|
160 | |
---|
161 | environment = build_environment(request, response, session) |
---|
162 | |
---|
163 | # set default view, controller can override it |
---|
164 | |
---|
165 | response.view = '%s/%s.%s' % (request.controller, |
---|
166 | request.function, |
---|
167 | request.extension) |
---|
168 | |
---|
169 | # also, make sure the flash is passed through |
---|
170 | # ################################################## |
---|
171 | # process models, controller and view (if required) |
---|
172 | # ################################################## |
---|
173 | |
---|
174 | run_models_in(environment) |
---|
175 | response._view_environment = copy.copy(environment) |
---|
176 | page = run_controller_in(request.controller, request.function, environment) |
---|
177 | if isinstance(page, dict): |
---|
178 | response._vars = page |
---|
179 | response._view_environment.update(page) |
---|
180 | page = run_view_in(response._view_environment) |
---|
181 | |
---|
182 | if not request.env.web2py_disable_garbage_collect: |
---|
183 | # logic to garbage collect after exec, not always, once every 100 requests |
---|
184 | global requests |
---|
185 | requests = ('requests' in globals()) and (requests + 1) % 100 or 0 |
---|
186 | if not requests: |
---|
187 | gc.collect() |
---|
188 | # end garbage collection logic |
---|
189 | |
---|
190 | # ################################################## |
---|
191 | # set default headers it not set |
---|
192 | # ################################################## |
---|
193 | |
---|
194 | default_headers = [ |
---|
195 | ('Content-Type', contenttype('.' + request.extension)), |
---|
196 | ('Cache-Control', |
---|
197 | 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0'), |
---|
198 | ('Expires', unlocalised_http_header_date(time.gmtime())), |
---|
199 | ('Pragma', 'no-cache')] |
---|
200 | for key, value in default_headers: |
---|
201 | response.headers.setdefault(key, value) |
---|
202 | |
---|
203 | raise HTTP(response.status, page, **response.headers) |
---|
204 | |
---|
205 | |
---|
206 | class LazyWSGI(object): |
---|
207 | def __init__(self, environ, request, response): |
---|
208 | self.wsgi_environ = environ |
---|
209 | self.request = request |
---|
210 | self.response = response |
---|
211 | |
---|
212 | @property |
---|
213 | def environ(self): |
---|
214 | if not hasattr(self, '_environ'): |
---|
215 | new_environ = self.wsgi_environ |
---|
216 | new_environ['wsgi.input'] = self.request.body |
---|
217 | new_environ['wsgi.version'] = 1 |
---|
218 | self._environ = new_environ |
---|
219 | return self._environ |
---|
220 | |
---|
221 | def start_response(self, status='200', headers=[], exec_info=None): |
---|
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 | self.response.status = int(str(status).split(' ', 1)[0]) |
---|
231 | self.response.headers = dict(headers) |
---|
232 | return lambda *args, **kargs: \ |
---|
233 | self.response.write(escape=False, *args, **kargs) |
---|
234 | |
---|
235 | def middleware(self, *middleware_apps): |
---|
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(self.response.status, |
---|
248 | list(self.response.headers.items())) |
---|
249 | if isinstance(data, list): |
---|
250 | return data |
---|
251 | return [data] |
---|
252 | for item in middleware_apps: |
---|
253 | app = item(app) |
---|
254 | |
---|
255 | def caller(app): |
---|
256 | return app(self.environ, self.start_response) |
---|
257 | return lambda caller=caller, app=app: caller(app) |
---|
258 | return middleware |
---|
259 | |
---|
260 | |
---|
261 | def wsgibase(environ, responder): |
---|
262 | """ |
---|
263 | The gluon wsgi application. The first function called when a page |
---|
264 | is requested (static or dynamic). It can be called by paste.httpserver |
---|
265 | or by apache mod_wsgi (or any WSGI-compatible server). |
---|
266 | |
---|
267 | - fills request with info |
---|
268 | - the environment variables, replacing '.' with '_' |
---|
269 | - adds web2py path and version info |
---|
270 | - compensates for fcgi missing path_info and query_string |
---|
271 | - validates the path in url |
---|
272 | |
---|
273 | The url path must be either: |
---|
274 | |
---|
275 | 1. for static pages: |
---|
276 | |
---|
277 | - /<application>/static/<file> |
---|
278 | |
---|
279 | 2. for dynamic pages: |
---|
280 | |
---|
281 | - /<application>[/<controller>[/<function>[/<sub>]]][.<extension>] |
---|
282 | |
---|
283 | The naming conventions are: |
---|
284 | |
---|
285 | - application, controller, function and extension may only contain |
---|
286 | `[a-zA-Z0-9_]` |
---|
287 | - file and sub may also contain '-', '=', '.' and '/' |
---|
288 | """ |
---|
289 | eget = environ.get |
---|
290 | current.__dict__.clear() |
---|
291 | request = Request(environ) |
---|
292 | response = Response() |
---|
293 | session = Session() |
---|
294 | env = request.env |
---|
295 | # env.web2py_path = global_settings.applications_parent |
---|
296 | env.web2py_version = web2py_version |
---|
297 | # env.update(global_settings) |
---|
298 | static_file = False |
---|
299 | http_response = None |
---|
300 | try: |
---|
301 | try: |
---|
302 | try: |
---|
303 | # ################################################## |
---|
304 | # handle fcgi missing path_info and query_string |
---|
305 | # select rewrite parameters |
---|
306 | # rewrite incoming URL |
---|
307 | # parse rewritten header variables |
---|
308 | # parse rewritten URL |
---|
309 | # serve file if static |
---|
310 | # ################################################## |
---|
311 | |
---|
312 | fixup_missing_path_info(environ) |
---|
313 | (static_file, version, environ) = url_in(request, environ) |
---|
314 | response.status = env.web2py_status_code or response.status |
---|
315 | |
---|
316 | if static_file: |
---|
317 | if eget('QUERY_STRING', '').startswith('attachment'): |
---|
318 | response.headers['Content-Disposition'] \ |
---|
319 | = 'attachment' |
---|
320 | if version: |
---|
321 | response.headers['Cache-Control'] = 'max-age=315360000' |
---|
322 | response.headers[ |
---|
323 | 'Expires'] = 'Thu, 31 Dec 2037 23:59:59 GMT' |
---|
324 | response.stream(static_file, request=request) |
---|
325 | |
---|
326 | # ################################################## |
---|
327 | # fill in request items |
---|
328 | # ################################################## |
---|
329 | app = request.application # must go after url_in! |
---|
330 | |
---|
331 | if not global_settings.local_hosts: |
---|
332 | local_hosts = set(['127.0.0.1', '::ffff:127.0.0.1', '::1']) |
---|
333 | if not global_settings.web2py_runtime_gae: |
---|
334 | try: |
---|
335 | fqdn = socket.getfqdn() |
---|
336 | local_hosts.add(socket.gethostname()) |
---|
337 | local_hosts.add(fqdn) |
---|
338 | local_hosts.update([ |
---|
339 | addrinfo[4][0] for addrinfo |
---|
340 | in getipaddrinfo(fqdn)]) |
---|
341 | if env.server_name: |
---|
342 | local_hosts.add(env.server_name) |
---|
343 | local_hosts.update([ |
---|
344 | addrinfo[4][0] for addrinfo |
---|
345 | in getipaddrinfo(env.server_name)]) |
---|
346 | except (socket.gaierror, TypeError): |
---|
347 | pass |
---|
348 | global_settings.local_hosts = list(local_hosts) |
---|
349 | else: |
---|
350 | local_hosts = global_settings.local_hosts |
---|
351 | client = get_client(env) |
---|
352 | x_req_with = str(env.http_x_requested_with).lower() |
---|
353 | |
---|
354 | request.update( |
---|
355 | client=client, |
---|
356 | folder=abspath('applications', app), |
---|
357 | ajax=x_req_with == 'xmlhttprequest', |
---|
358 | cid=env.http_web2py_component_element, |
---|
359 | is_local=(env.remote_addr in local_hosts and client == env.remote_addr), |
---|
360 | is_shell=False, |
---|
361 | is_scheduler=False, |
---|
362 | is_https=env.wsgi_url_scheme in HTTPS_SCHEMES or |
---|
363 | request.env.http_x_forwarded_proto in HTTPS_SCHEMES or env.https == 'on' |
---|
364 | ) |
---|
365 | request.url = environ['PATH_INFO'] |
---|
366 | |
---|
367 | # ################################################## |
---|
368 | # access the requested application |
---|
369 | # ################################################## |
---|
370 | |
---|
371 | disabled = pjoin(request.folder, 'DISABLED') |
---|
372 | if not exists(request.folder): |
---|
373 | if app == rwthread.routes.default_application \ |
---|
374 | and app != 'welcome': |
---|
375 | redirect(URL('welcome', 'default', 'index')) |
---|
376 | elif rwthread.routes.error_handler: |
---|
377 | _handler = rwthread.routes.error_handler |
---|
378 | redirect(URL(_handler['application'], |
---|
379 | _handler['controller'], |
---|
380 | _handler['function'], |
---|
381 | args=app)) |
---|
382 | else: |
---|
383 | raise HTTP(404, rwthread.routes.error_message |
---|
384 | % 'invalid request', |
---|
385 | web2py_error='invalid application') |
---|
386 | elif not request.is_local and exists(disabled): |
---|
387 | five0three = os.path.join(request.folder, 'static', '503.html') |
---|
388 | if os.path.exists(five0three): |
---|
389 | raise HTTP(503, open(five0three, 'r').read()) |
---|
390 | else: |
---|
391 | raise HTTP(503, "<html><body><h1>Temporarily down for maintenance</h1></body></html>") |
---|
392 | |
---|
393 | # ################################################## |
---|
394 | # build missing folders |
---|
395 | # ################################################## |
---|
396 | |
---|
397 | create_missing_app_folders(request) |
---|
398 | |
---|
399 | # ################################################## |
---|
400 | # get the GET and POST data |
---|
401 | # ################################################## |
---|
402 | |
---|
403 | # parse_get_post_vars(request, environ) |
---|
404 | |
---|
405 | # ################################################## |
---|
406 | # expose wsgi hooks for convenience |
---|
407 | # ################################################## |
---|
408 | |
---|
409 | request.wsgi = LazyWSGI(environ, request, response) |
---|
410 | |
---|
411 | # ################################################## |
---|
412 | # load cookies |
---|
413 | # ################################################## |
---|
414 | |
---|
415 | if env.http_cookie: |
---|
416 | for single_cookie in env.http_cookie.split(';'): |
---|
417 | single_cookie = single_cookie.strip() |
---|
418 | if single_cookie: |
---|
419 | try: |
---|
420 | request.cookies.load(single_cookie) |
---|
421 | except Cookie.CookieError: |
---|
422 | pass # single invalid cookie ignore |
---|
423 | |
---|
424 | # ################################################## |
---|
425 | # try load session or create new session file |
---|
426 | # ################################################## |
---|
427 | |
---|
428 | if not env.web2py_disable_session: |
---|
429 | session.connect(request, response) |
---|
430 | |
---|
431 | # ################################################## |
---|
432 | # run controller |
---|
433 | # ################################################## |
---|
434 | |
---|
435 | if global_settings.debugging and app != "admin": |
---|
436 | import gluon.debug |
---|
437 | # activate the debugger |
---|
438 | gluon.debug.dbg.do_debug(mainpyfile=request.folder) |
---|
439 | |
---|
440 | serve_controller(request, response, session) |
---|
441 | except HTTP as hr: |
---|
442 | http_response = hr |
---|
443 | |
---|
444 | if static_file: |
---|
445 | return http_response.to(responder, env=env) |
---|
446 | |
---|
447 | if request.body: |
---|
448 | request.body.close() |
---|
449 | |
---|
450 | if hasattr(current, 'request'): |
---|
451 | |
---|
452 | # ################################################## |
---|
453 | # on success, try store session in database |
---|
454 | # ################################################## |
---|
455 | if not env.web2py_disable_session: |
---|
456 | session._try_store_in_db(request, response) |
---|
457 | |
---|
458 | # ################################################## |
---|
459 | # on success, commit database |
---|
460 | # ################################################## |
---|
461 | |
---|
462 | if response.do_not_commit is True: |
---|
463 | BaseAdapter.close_all_instances(None) |
---|
464 | elif response.custom_commit: |
---|
465 | BaseAdapter.close_all_instances(response.custom_commit) |
---|
466 | else: |
---|
467 | BaseAdapter.close_all_instances('commit') |
---|
468 | |
---|
469 | # ################################################## |
---|
470 | # if session not in db try store session on filesystem |
---|
471 | # this must be done after trying to commit database! |
---|
472 | # ################################################## |
---|
473 | if not env.web2py_disable_session: |
---|
474 | session._try_store_in_cookie_or_file(request, response) |
---|
475 | |
---|
476 | # Set header so client can distinguish component requests. |
---|
477 | if request.cid: |
---|
478 | http_response.headers.setdefault( |
---|
479 | 'web2py-component-content', 'replace') |
---|
480 | |
---|
481 | if request.ajax: |
---|
482 | if response.flash: |
---|
483 | http_response.headers['web2py-component-flash'] = \ |
---|
484 | urllib_quote(xmlescape(response.flash).replace(b'\n', b'')) |
---|
485 | if response.js: |
---|
486 | http_response.headers['web2py-component-command'] = \ |
---|
487 | urllib_quote(response.js.replace('\n', '')) |
---|
488 | |
---|
489 | # ################################################## |
---|
490 | # store cookies in headers |
---|
491 | # ################################################## |
---|
492 | |
---|
493 | session._fixup_before_save() |
---|
494 | http_response.cookies2headers(response.cookies) |
---|
495 | |
---|
496 | ticket = None |
---|
497 | |
---|
498 | except RestrictedError as e: |
---|
499 | |
---|
500 | if request.body: |
---|
501 | request.body.close() |
---|
502 | |
---|
503 | # ################################################## |
---|
504 | # on application error, rollback database |
---|
505 | # ################################################## |
---|
506 | |
---|
507 | # log tickets before rollback if not in DB |
---|
508 | if not request.tickets_db: |
---|
509 | ticket = e.log(request) or 'unknown' |
---|
510 | # rollback |
---|
511 | if response._custom_rollback: |
---|
512 | response._custom_rollback() |
---|
513 | else: |
---|
514 | BaseAdapter.close_all_instances('rollback') |
---|
515 | # if tickets in db, reconnect and store it in db |
---|
516 | if request.tickets_db: |
---|
517 | ticket = e.log(request) or 'unknown' |
---|
518 | |
---|
519 | http_response = \ |
---|
520 | HTTP(500, rwthread.routes.error_message_ticket % |
---|
521 | dict(ticket=ticket), |
---|
522 | web2py_error='ticket %s' % ticket) |
---|
523 | |
---|
524 | except: |
---|
525 | |
---|
526 | if request.body: |
---|
527 | request.body.close() |
---|
528 | |
---|
529 | # ################################################## |
---|
530 | # on application error, rollback database |
---|
531 | # ################################################## |
---|
532 | |
---|
533 | try: |
---|
534 | if response._custom_rollback: |
---|
535 | response._custom_rollback() |
---|
536 | else: |
---|
537 | BaseAdapter.close_all_instances('rollback') |
---|
538 | except: |
---|
539 | pass |
---|
540 | e = RestrictedError('Framework', '', '', locals()) |
---|
541 | ticket = e.log(request) or 'unrecoverable' |
---|
542 | http_response = \ |
---|
543 | HTTP(500, rwthread.routes.error_message_ticket |
---|
544 | % dict(ticket=ticket), |
---|
545 | web2py_error='ticket %s' % ticket) |
---|
546 | |
---|
547 | finally: |
---|
548 | if response and hasattr(response, 'session_file') \ |
---|
549 | and response.session_file: |
---|
550 | response.session_file.close() |
---|
551 | |
---|
552 | session._unlock(response) |
---|
553 | http_response, new_environ = try_rewrite_on_error( |
---|
554 | http_response, request, environ, ticket) |
---|
555 | if not http_response: |
---|
556 | return wsgibase(new_environ, responder) |
---|
557 | |
---|
558 | if global_settings.web2py_crontype == 'soft': |
---|
559 | cmd_opts = global_settings.cmd_options |
---|
560 | newcron.softcron(global_settings.applications_parent, |
---|
561 | apps=cmd_opts and cmd_opts.crontabs) |
---|
562 | |
---|
563 | return http_response.to(responder, env=env) |
---|
564 | |
---|
565 | |
---|
566 | def save_password(password, port): |
---|
567 | """ |
---|
568 | Used by main() to save the password in the parameters_port.py file. |
---|
569 | """ |
---|
570 | |
---|
571 | password_file = abspath('parameters_%i.py' % port) |
---|
572 | if password == '<random>': |
---|
573 | # make up a new password |
---|
574 | chars = string.letters + string.digits |
---|
575 | password = ''.join([random.choice(chars) for _ in range(8)]) |
---|
576 | cpassword = CRYPT()(password)[0] |
---|
577 | print('******************* IMPORTANT!!! ************************') |
---|
578 | print('your admin password is "%s"' % password) |
---|
579 | print('*********************************************************') |
---|
580 | elif password == '<recycle>': |
---|
581 | # reuse the current password if any |
---|
582 | if exists(password_file): |
---|
583 | return |
---|
584 | else: |
---|
585 | password = '' |
---|
586 | elif password.startswith('<pam_user:'): |
---|
587 | # use the pam password for specified user |
---|
588 | cpassword = password[1:-1] |
---|
589 | else: |
---|
590 | # use provided password |
---|
591 | cpassword = CRYPT()(password)[0] |
---|
592 | fp = open(password_file, 'w') |
---|
593 | if password: |
---|
594 | fp.write('password="%s"\n' % cpassword) |
---|
595 | else: |
---|
596 | fp.write('password=None\n') |
---|
597 | fp.close() |
---|
598 | |
---|
599 | |
---|
600 | def appfactory(wsgiapp=wsgibase, |
---|
601 | logfilename='httpserver.log', |
---|
602 | profiler_dir=None, |
---|
603 | profilerfilename=None): |
---|
604 | """ |
---|
605 | generates a wsgi application that does logging and profiling and calls |
---|
606 | wsgibase |
---|
607 | |
---|
608 | Args: |
---|
609 | wsgiapp: the base application |
---|
610 | logfilename: where to store apache-compatible requests log |
---|
611 | profiler_dir: where to store profile files |
---|
612 | |
---|
613 | """ |
---|
614 | if profilerfilename is not None: |
---|
615 | raise BaseException("Deprecated API") |
---|
616 | if profiler_dir: |
---|
617 | profiler_dir = abspath(profiler_dir) |
---|
618 | logger.warn('profiler is on. will use dir %s', profiler_dir) |
---|
619 | if not os.path.isdir(profiler_dir): |
---|
620 | try: |
---|
621 | os.makedirs(profiler_dir) |
---|
622 | except: |
---|
623 | raise BaseException("Can't create dir %s" % profiler_dir) |
---|
624 | filepath = pjoin(profiler_dir, 'wtest') |
---|
625 | try: |
---|
626 | filehandle = open(filepath, 'w') |
---|
627 | filehandle.close() |
---|
628 | os.unlink(filepath) |
---|
629 | except IOError: |
---|
630 | raise BaseException("Unable to write to dir %s" % profiler_dir) |
---|
631 | |
---|
632 | def app_with_logging(environ, responder): |
---|
633 | """ |
---|
634 | a wsgi app that does logging and profiling and calls wsgibase |
---|
635 | """ |
---|
636 | status_headers = [] |
---|
637 | |
---|
638 | def responder2(s, h): |
---|
639 | """ |
---|
640 | wsgi responder app |
---|
641 | """ |
---|
642 | status_headers.append(s) |
---|
643 | status_headers.append(h) |
---|
644 | return responder(s, h) |
---|
645 | |
---|
646 | time_in = time.time() |
---|
647 | ret = [0] |
---|
648 | if not profiler_dir: |
---|
649 | ret[0] = wsgiapp(environ, responder2) |
---|
650 | else: |
---|
651 | import cProfile |
---|
652 | prof = cProfile.Profile() |
---|
653 | prof.enable() |
---|
654 | ret[0] = wsgiapp(environ, responder2) |
---|
655 | prof.disable() |
---|
656 | destfile = pjoin(profiler_dir, "req_%s.prof" % web2py_uuid()) |
---|
657 | prof.dump_stats(destfile) |
---|
658 | |
---|
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 | write_file(logfilename, line, 'a') |
---|
673 | else: |
---|
674 | logfilename.write(line) |
---|
675 | except: |
---|
676 | pass |
---|
677 | return ret[0] |
---|
678 | |
---|
679 | return app_with_logging |
---|
680 | |
---|
681 | |
---|
682 | class HttpServer(object): |
---|
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_dir=None, |
---|
695 | ssl_certificate=None, |
---|
696 | ssl_private_key=None, |
---|
697 | ssl_ca_certificate=None, |
---|
698 | min_threads=None, |
---|
699 | max_threads=None, |
---|
700 | server_name=None, |
---|
701 | request_queue_size=5, |
---|
702 | timeout=10, |
---|
703 | socket_timeout=1, |
---|
704 | shutdown_timeout=None, # Rocket does not use a shutdown timeout |
---|
705 | path=None, |
---|
706 | interfaces=None # Rocket is able to use several interfaces - must be list of socket-tuples as string |
---|
707 | ): |
---|
708 | """ |
---|
709 | starts the web server. |
---|
710 | """ |
---|
711 | |
---|
712 | if interfaces: |
---|
713 | # if interfaces is specified, it must be tested for rocket parameter correctness |
---|
714 | # not necessarily completely tested (e.g. content of tuples or ip-format) |
---|
715 | if isinstance(interfaces, list): |
---|
716 | for i in interfaces: |
---|
717 | if not isinstance(i, tuple): |
---|
718 | raise AttributeError("Wrong format for rocket interfaces parameter - see http://packages.python.org/rocket/") |
---|
719 | else: |
---|
720 | raise AttributeError("Wrong format for rocket interfaces parameter - see http://packages.python.org/rocket/") |
---|
721 | |
---|
722 | if path: |
---|
723 | # if a path is specified change the global variables so that web2py |
---|
724 | # runs from there instead of cwd or os.environ['web2py_path'] |
---|
725 | global web2py_path |
---|
726 | path = os.path.normpath(path) |
---|
727 | web2py_path = path |
---|
728 | global_settings.applications_parent = path |
---|
729 | os.chdir(path) |
---|
730 | load_routes() |
---|
731 | for p in (path, abspath('site-packages'), ""): |
---|
732 | add_path_first(p) |
---|
733 | if exists("logging.conf"): |
---|
734 | logging.config.fileConfig("logging.conf") |
---|
735 | |
---|
736 | save_password(password, port) |
---|
737 | self.pid_filename = pid_filename |
---|
738 | if not server_name: |
---|
739 | server_name = socket.gethostname() |
---|
740 | logger.info('starting web server...') |
---|
741 | rocket.SERVER_NAME = server_name |
---|
742 | rocket.SOCKET_TIMEOUT = socket_timeout |
---|
743 | sock_list = [ip, port] |
---|
744 | if not ssl_certificate or not ssl_private_key: |
---|
745 | logger.info('SSL is off') |
---|
746 | elif not rocket.has_ssl: |
---|
747 | logger.warning('Python "ssl" module unavailable. SSL is OFF') |
---|
748 | elif not exists(ssl_certificate): |
---|
749 | logger.warning('unable to open SSL certificate. SSL is OFF') |
---|
750 | elif not exists(ssl_private_key): |
---|
751 | logger.warning('unable to open SSL private key. SSL is OFF') |
---|
752 | else: |
---|
753 | sock_list.extend([ssl_private_key, ssl_certificate]) |
---|
754 | if ssl_ca_certificate: |
---|
755 | sock_list.append(ssl_ca_certificate) |
---|
756 | |
---|
757 | logger.info('SSL is ON') |
---|
758 | app_info = {'wsgi_app': appfactory(wsgibase, |
---|
759 | log_filename, |
---|
760 | profiler_dir)} |
---|
761 | |
---|
762 | self.server = rocket.Rocket(interfaces or tuple(sock_list), |
---|
763 | method='wsgi', |
---|
764 | app_info=app_info, |
---|
765 | min_threads=min_threads, |
---|
766 | max_threads=max_threads, |
---|
767 | queue_size=request_queue_size, |
---|
768 | timeout=timeout, |
---|
769 | handle_signals=False, |
---|
770 | ) |
---|
771 | |
---|
772 | def start(self): |
---|
773 | """ |
---|
774 | start the web server |
---|
775 | """ |
---|
776 | try: |
---|
777 | signal.signal(signal.SIGTERM, lambda a, b, s=self: s.stop()) |
---|
778 | signal.signal(signal.SIGINT, lambda a, b, s=self: s.stop()) |
---|
779 | except: |
---|
780 | pass |
---|
781 | write_file(self.pid_filename, str(os.getpid())) |
---|
782 | self.server.start() |
---|
783 | |
---|
784 | def stop(self, stoplogging=False): |
---|
785 | """ |
---|
786 | stop cron and the web server |
---|
787 | """ |
---|
788 | if global_settings.web2py_crontype == 'soft': |
---|
789 | try: |
---|
790 | newcron.stopcron() |
---|
791 | except: |
---|
792 | pass |
---|
793 | self.server.stop(stoplogging) |
---|
794 | try: |
---|
795 | os.unlink(self.pid_filename) |
---|
796 | except: |
---|
797 | pass |
---|