Coverage for src/mathenjeu/apps/staticapp/staticsite.py: 84%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# -*- coding: utf-8 -*-
2"""
3@file
4@brief Starts an application.
5"""
6import os
7from starlette.applications import Starlette
8from starlette.staticfiles import StaticFiles
9from starlette.responses import PlainTextResponse
10# from starlette.middleware.httpsredirect import HTTPSRedirectMiddleware
11from starlette.middleware.trustedhost import TrustedHostMiddleware
12from starlette.routing import Mount
13from starlette.templating import Jinja2Templates
14from ..common import LogApp, AuthentificationAnswers
15from .authmount import AuthMount, AuthStaticFiles
18class StaticApp(LogApp, AuthentificationAnswers):
19 """
20 Implements routes for a web application which serves static files
21 protected with a password.
22 See :ref:`Which server to server starlette application? <faq-server-app-starlette>`.
23 The application allows anybody to connect to the website
24 assuming the know the password.
25 """
27 def __init__(self,
28 # log parameters
29 secret_log=None, folder='.',
30 # authentification parameters
31 max_age=14 * 24 * 60 * 60, cookie_key=None,
32 cookie_name="mathenjeu_static",
33 cookie_domain="127.0.0.1",
34 cookie_path="/",
35 # application parameters
36 content=None,
37 title="MathEnJeu - Static Files", short_title="MEJ",
38 page_doc="http://www.xavierdupre.fr/app/mathenjeu/helpsphinx/",
39 secure=False, middles=None, debug=False, userpwd=None):
40 """
41 @param secret_log to encrypt log (None to ignore)
42 @param folder folder where to write the logs (None to disable the logging)
44 @param max_age cookie's duration in seconds
45 @param cookie_key to encrypt information in the cookie (cannot be None)
46 @param cookie_name name of the session cookie
47 @param cookie_domain cookie is valid for this path only, also defines the
48 domain of the web app (its url)
49 @param cookie_path path of the cookie once storeds
50 @param secure use secured connection for cookies
51 @param content list tuple ``route, folder`` to server
53 @param title title
54 @param short_title short application title
55 @param middles middles ware, list of couple ``[(class, **kwargs)]``
56 where *kwargs* are the parameter constructor
57 @param userpwd users are authentified with any alias but a common password
58 @param debug display debug information (:epkg:`starlette` option)
59 """
60 if title is None:
61 raise ValueError("title cannot be None.")
62 if short_title is None:
63 raise ValueError("short_title cannot be None.")
65 this = os.path.abspath(os.path.dirname(__file__))
66 templates = os.path.join(this, "templates")
67 statics = os.path.join(this, "statics")
68 if not os.path.exists(statics):
69 raise FileNotFoundError("Unable to find '{0}'".format(statics))
70 if not os.path.exists(templates):
71 raise FileNotFoundError("Unable to find '{0}'".format(templates))
73 login_page = "login.html"
74 notauth_page = "notauthorized.html"
75 auth_page = "authorized.html"
76 redirect_logout = "/"
77 app = Starlette(debug=debug)
79 AuthentificationAnswers.__init__(self, app, login_page=login_page, auth_page=auth_page,
80 notauth_page=notauth_page, redirect_logout=redirect_logout,
81 max_age=max_age, cookie_name=cookie_name, cookie_key=cookie_key,
82 cookie_domain=cookie_domain, cookie_path=cookie_path,
83 page_context=self.page_context, userpwd=userpwd)
84 LogApp.__init__(self, folder=folder, secret_log=secret_log,
85 fct_session=self.get_session)
87 self.title = title
88 self.short_title = short_title
89 self.page_doc = page_doc
90 self.approutes = []
91 self.templates = Jinja2Templates(directory=templates)
93 if middles is not None:
94 for middle, kwargs in middles:
95 app.add_middleware(middle, **kwargs)
96 app.add_middleware(TrustedHostMiddleware,
97 allowed_hosts=[cookie_domain])
98 # app.add_middleware(HTTPSRedirectMiddleware)
100 app.mount('/static', StaticFiles(directory=statics), name='static')
101 app.add_route('/login', self.login)
102 app.add_route('/logout', self.logout)
103 app.add_route('/error', self.on_error)
104 app.add_route('/authenticate', self.authenticate, methods=['POST'])
105 app.add_exception_handler(404, self.not_found)
106 app.add_exception_handler(500, self.server_error)
107 app.add_route('/', self.main)
108 app.add_route('/event', self.event)
109 app.add_event_handler("startup", self.startup)
110 app.add_event_handler("shutdown", self.cleanup)
111 self.info("[StaticApp.create_app] create application", None)
113 impossible = {'static', 'login', 'error', 'logout',
114 'authenticate', 'startup', 'shutdown'}
116 if content is not None:
117 for route, local_folder in content:
118 self.info("[StaticApp] add route '{}' for '{}'.".format(
119 route, local_folder), None)
120 route = route.strip()
121 if not os.path.exists(local_folder):
122 raise FileNotFoundError(
123 "Unable to find folder '{0}' mapped to '{1}'".format(local_folder, route))
124 if route in impossible:
125 raise ValueError(
126 "Route '{0}' is forbidden (cannot be in {1})".format(route, impossible))
128 if userpwd:
129 st = AuthStaticFiles(directory=local_folder, html=True)
130 self.info("[StaticApp]1 add route '{}' for '{}'.".format(
131 route, local_folder), None)
132 rt = AuthMount('/' + route, app=st, name=route)
133 else:
134 st = StaticFiles(directory=local_folder, html=True)
135 self.info("[StaticApp]2 add route '{}' for '{}'.".format(
136 route, local_folder), None)
137 rt = Mount('/' + route, app=st, name=route)
138 app.router.routes.append(rt)
140 index = os.path.join(local_folder, 'index.html')
141 if os.path.exists(index):
142 self.info("[StaticApp] add route '/{}/index.html'.", None)
143 self.approutes.append(
144 (route, '/{}/index.html'.format(route)))
145 else:
146 res = os.listdir(local_folder)
147 found = False
148 for r in res:
149 full = os.path.join(local_folder, r)
150 if os.path.isfile(full):
151 self.info("[StaticApp] add route '{}'.".format(
152 '/{}/{}'.format(route, r)), None)
153 self.approutes.append(
154 (route, '/{}/{}'.format(route, r)))
155 found = True
156 break
157 if not found:
158 self.info(
159 "[StaticApp] add route '/{}'.".format(route), None)
160 self.approutes.append((route, '/' + route))
162 #########
163 # common
164 #########
166 def page_context(self, **kwargs):
167 """
168 Returns the page context before applying any template.
170 @param kwargs arguments
171 @return parameters
172 """
173 res = dict(title=self.title, short_title=self.short_title,
174 page_doc=self.page_doc, approutes=self.approutes)
175 res.update(kwargs)
176 return res
178 def startup(self):
179 """
180 Startups.
181 """
182 self.info('[StaticApp] startup', None)
184 def cleanup(self):
185 """
186 Cleans up.
187 """
188 self.info('[StaticApp] cleanup', None)
190 def unlogged_response(self, request, session):
191 """
192 Returns an answer for somebody looking to access
193 the questions without being authentified.
194 """
195 self.log_event("home-unlogged", request, session=session)
196 context = {'request': request}
197 context.update(self.page_context(**session))
198 return self.templates.TemplateResponse('notlogged.html', context)
200 ########
201 # route
202 ########
204 async def main(self, request):
205 """
206 Defines the main page.
207 """
208 session = self.get_session(request, notnone=True)
209 if 'alias' in session:
210 self.log_event("home-logged", request, session=session)
211 context = {'request': request}
212 context.update(self.page_context(**session))
213 return self.templates.TemplateResponse('index.html', context)
214 else:
215 return self.unlogged_response(request, session)
217 async def on_error(self, request):
218 """
219 An example error.
220 """
221 self.log_any('[error]', "?", request)
222 raise RuntimeError("Oh no")
224 async def not_found(self, request, exc):
225 """
226 Returns an :epkg:`HTTP 404` page.
227 """
228 context = {'request': request}
229 context.update(self.page_context())
230 return self.templates.TemplateResponse('404.html', context, status_code=404)
232 async def server_error(self, request, exc):
233 """
234 Returns an :epkg:`HTTP 500` page.
235 """
236 context = {'request': request}
237 context.update(self.page_context())
238 return self.templates.TemplateResponse('500.html', context, status_code=500)
240 #########
241 # event route
242 #########
244 async def event(self, request):
245 """
246 This route does not return anything interesting except
247 a blank page, but it logs
248 """
249 session = self.get_session(request, notnone=True)
250 ps = request.query_params
251 tostr = ','.join('{0}:{1}'.format(k, v) for k, v in sorted(ps.items()))
252 self.log_event("event", request, session=session, events=[tostr])
253 return PlainTextResponse("")