Hide keyboard shortcuts

Hot-keys on this page

r m x p   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 

16 

17 

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 """ 

26 

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) 

43 

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 

52 

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.") 

64 

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)) 

72 

73 login_page = "login.html" 

74 notauth_page = "notauthorized.html" 

75 auth_page = "authorized.html" 

76 redirect_logout = "/" 

77 app = Starlette(debug=debug) 

78 

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) 

86 

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) 

92 

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) 

99 

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) 

112 

113 impossible = {'static', 'login', 'error', 'logout', 

114 'authenticate', 'startup', 'shutdown'} 

115 

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)) 

127 

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) 

139 

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)) 

161 

162 ######### 

163 # common 

164 ######### 

165 

166 def page_context(self, **kwargs): 

167 """ 

168 Returns the page context before applying any template. 

169 

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 

177 

178 def startup(self): 

179 """ 

180 Startups. 

181 """ 

182 self.info('[StaticApp] startup', None) 

183 

184 def cleanup(self): 

185 """ 

186 Cleans up. 

187 """ 

188 self.info('[StaticApp] cleanup', None) 

189 

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) 

199 

200 ######## 

201 # route 

202 ######## 

203 

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) 

216 

217 async def on_error(self, request): 

218 """ 

219 An example error. 

220 """ 

221 self.log_any('[error]', "?", request) 

222 raise RuntimeError("Oh no") 

223 

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) 

231 

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) 

239 

240 ######### 

241 # event route 

242 ######### 

243 

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("")