Coverage for gws-app/gws/base/web/site.py: 88%

140 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-05 13:35 +0200

1"""Web site.""" 

2 

3from typing import Optional 

4 

5import re 

6 

7import gws 

8import gws.lib.net 

9 

10 

11class CorsConfig(gws.Config): 

12 """CORS headers added to responses, to allow cross-origin requests.""" 

13 

14 allowCredentials: bool = False 

15 """Allow credentials in cross-origin requests.""" 

16 allowHeaders: str = '' 

17 """Headers allowed in cross-origin requests.""" 

18 allowMethods: str = '' 

19 """Methods allowed in cross-origin requests.""" 

20 allowOrigin: str = '' 

21 """Origins allowed to make cross-origin requests.""" 

22 maxAge: int = 5 

23 """How long the browser caches the CORS preflight response.""" 

24 

25 

26class RewriteRuleConfig(gws.Config): 

27 """URL rewrite rule.""" 

28 

29 pattern: gws.Regex 

30 """Regular expression to match the URL path against.""" 

31 target: str 

32 """Target URL.""" 

33 options: Optional[dict] 

34 """Additional options.""" 

35 reversed: bool = False 

36 """Rewrite URLs generated by the server instead of incoming URLs.""" 

37 

38 

39class SSLConfig(gws.Config): 

40 """SSL certificate and HSTS settings.""" 

41 

42 crt: gws.FilePath 

43 """Certificate bundle location.""" 

44 key: gws.FilePath 

45 """Private key file location.""" 

46 hsts: gws.Duration = '365d' 

47 """Max. age for the Strict-Transport-Security header.""" 

48 

49 

50class WebDirConfig(gws.Config): 

51 """Directory served over the web.""" 

52 

53 dir: gws.DirPath 

54 """Directory path.""" 

55 allowMime: Optional[list[str]] 

56 """Serve only files of these MIME types.""" 

57 denyMime: Optional[list[str]] 

58 """MIME types not to serve.""" 

59 

60 

61class Config(gws.Config): 

62 """Web site the server responds to.""" 

63 

64 assets: Optional[WebDirConfig] 

65 """Directory for assets.""" 

66 cors: Optional[CorsConfig] 

67 """CORS configuration.""" 

68 contentSecurityPolicy: str = "default-src 'self'; img-src * data: blob:" 

69 """Content security policy for the site.""" 

70 permissionsPolicy: str = 'geolocation=(self), camera=(), microphone=()' 

71 """Permissions policy for the site.""" 

72 xFrameOptions: str = 'SAMEORIGIN' 

73 """Whether the site may be embedded in frames.""" 

74 errorPage: Optional[gws.ext.config.template] 

75 """Template for HTTP error pages. (deprecated in 8.4)""" 

76 hostnames: Optional[list[str]] 

77 """Host names this site responds to, lowercase and without a port.""" 

78 host: str = '' 

79 """Host name. (deprecated in 8.4)""" 

80 rewrite: Optional[list[RewriteRuleConfig]] 

81 """Rewrite rules. (deprecated in 8.4)""" 

82 rewriteRules: Optional[list[RewriteRuleConfig]] 

83 """Rules to rewrite incoming URLs and URLs generated by the server.""" 

84 withDefaultRewriteRules: bool = True 

85 """Add the default rewrite rules for the application.""" 

86 canonicalHost: str = '' 

87 """Host name for canonical URLs.""" 

88 proxyCount: int = 0 

89 """Number of proxies in front of the server.""" 

90 root: Optional[WebDirConfig] 

91 """Directory for static documents.""" 

92 

93 

94DEFAULT_ASSETS_DIR = '/data/assets' 

95DEFAULT_WEB_DIR = '/data/web' 

96DEFAULT_REWRITE_RULES = [ 

97 gws.WebRewriteRule(pattern=r'^/$', target='/_/webPage/name/home'), 

98 gws.WebRewriteRule(pattern=r'^/project/([a-z0-9_-]+)(?=/@|$)', target='/_/webPage/name/project/projectUid/$1'), 

99 gws.WebRewriteRule(pattern=r'^/_/webPage/name/home$', target='/', reversed=True), 

100 gws.WebRewriteRule(pattern=r'^/_/webPage/name/project/projectUid/([a-z0-9_-]+)', target='/project/$1', reversed=True), 

101] 

102 

103 

104class Object(gws.WebSite): 

105 """Web site.""" 

106 

107 ssl: bool 

108 """The site is served over https.""" 

109 contentSecurityPolicy: str 

110 """Content-Security-Policy header value.""" 

111 permissionsPolicy: str 

112 """Permissions-Policy header value.""" 

113 xFrameOptions: str 

114 """X-Frame-Options header value.""" 

115 

116 def configure(self): 

117 self.hostnames = self.cfg('hostnames') or [] 

118 p = self.cfg('host') 

119 if p: 

120 self.root.config_warning('"web.site.host" is deprecated, use "web.site.hostnames"') 

121 if p and p != '*': 

122 self.hostnames = [p] 

123 

124 self.canonicalHost = self.cfg('canonicalHost') or '' 

125 if not self.canonicalHost and self.hostnames: 

126 self.canonicalHost = self.hostnames[0] 

127 

128 self.proxyCount = self.cfg('proxyCount') or 0 

129 self.ssl = self.cfg('ssl') 

130 self.corsOptions = self.cfg('cors') 

131 self.contentSecurityPolicy = self.cfg('contentSecurityPolicy') 

132 self.permissionsPolicy = self.cfg('permissionsPolicy') 

133 self.xFrameOptions = self.cfg('xFrameOptions') 

134 # deprecated 

135 if self.cfg('errorPage'): 

136 self.root.config_warning('"web.site.errorPage" is deprecated') 

137 self.errorPage = self.create_child_if_configured(gws.ext.object.template, self.cfg('errorPage')) 

138 

139 p = self.cfg('root') 

140 if p: 

141 self.staticRoot = gws.WebDocumentRoot(p) 

142 elif gws.u.is_dir(DEFAULT_WEB_DIR): 

143 self.staticRoot = gws.WebDocumentRoot(dir=DEFAULT_WEB_DIR) 

144 else: 

145 # note: web root must exist 

146 self.root.config_warning(f'web root {DEFAULT_WEB_DIR!r} does not exist, using temporary directory') 

147 self.staticRoot = gws.WebDocumentRoot(dir=gws.u.ensure_dir(gws.c.TMP_DIR + '/web')) 

148 

149 p = self.cfg('assets') 

150 if p: 

151 self.assetsRoot = gws.WebDocumentRoot(p) 

152 elif gws.u.is_dir(DEFAULT_ASSETS_DIR): 

153 self.assetsRoot = gws.WebDocumentRoot(dir=DEFAULT_ASSETS_DIR) 

154 else: 

155 # note: assets root is optional 

156 self.assetsRoot = None 

157 

158 self.rewriteRules = [] 

159 p = self.cfg('rewriteRules') 

160 if not p: 

161 # deprecated 

162 p = self.cfg('rewrite') 

163 if p: 

164 self.root.config_warning('"web.site.rewrite" is deprecated, use "web.site.rewriteRules"') 

165 if not p: 

166 p = [] 

167 for c in p: 

168 r = gws.WebRewriteRule(c) 

169 if not gws.lib.net.is_abs_url(r.target): 

170 # ensure rewriting from root 

171 r.target = '/' + r.target.lstrip('/') 

172 self.rewriteRules.append(r) 

173 

174 if self.cfg('withDefaultRewriteRules', default=True): 

175 patterns = set(r.pattern for r in self.rewriteRules) 

176 for c in DEFAULT_REWRITE_RULES: 

177 if c.pattern not in patterns: 

178 self.rewriteRules.insert(0, c) 

179 

180 def url_for(self, req, path, mode, **params): 

181 if gws.lib.net.is_abs_url(path): 

182 return gws.lib.net.add_params(path, params) 

183 

184 path = self._apply_reverse_rewrite_rules(path) 

185 if gws.lib.net.is_abs_url(path): 

186 return gws.lib.net.add_params(path, params) 

187 

188 path = '/' + path.lstrip('/') 

189 u = gws.lib.net.parse_url(path) 

190 u.params.update(params) 

191 

192 if mode == 'relative': 

193 return gws.lib.net.make_relative_url(u.path, u.params) 

194 

195 u.scheme = req.scheme 

196 

197 if mode == 'canonical': 

198 u.hostname = self.canonicalHost 

199 if not u.hostname: 

200 u.hostname = req.host 

201 u.port = req.port 

202 if not u.hostname: 

203 raise gws.BadRequestError('no host for an absolute url') 

204 

205 return gws.lib.net.make_url(u) 

206 

207 def _apply_reverse_rewrite_rules(self, path): 

208 """Rewrite a path with the first matching reversed rule.""" 

209 for r in self.rewriteRules: 

210 if not r.reversed: 

211 continue 

212 m = re.search(r.pattern, path) 

213 if not m: 

214 continue 

215 # we use nginx syntax $1, need python's \1 

216 t = r.target.replace('$', '\\') 

217 return re.sub(r.pattern, t, path) 

218 

219 return path