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

149 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-24 12:46 +0200

1from typing import Optional 

2 

3import re 

4 

5import gws 

6import gws.lib.net 

7 

8 

9class CorsConfig(gws.Config): 

10 """CORS configuration.""" 

11 

12 allowCredentials: bool = False 

13 """Access-Control-Allow-Credentials header.""" 

14 allowHeaders: str = '' 

15 """Access-Control-Allow-Headers header.""" 

16 allowMethods: str = '' 

17 """Access-Control-Allow-Methods header.""" 

18 allowOrigin: str = '' 

19 """Access-Control-Allow-Origin header.""" 

20 maxAge: int = 5 

21 """Access-Control-Max-Age header.""" 

22 

23 

24class RewriteRuleConfig(gws.Config): 

25 """Rewrite rule configuration.""" 

26 

27 pattern: gws.Regex 

28 """Expression to match the url against.""" 

29 target: str 

30 """Target url with placeholders.""" 

31 options: Optional[dict] 

32 """Additional options.""" 

33 reversed: bool = False 

34 """Reversed rewrite rule.""" 

35 

36 

37class SSLConfig(gws.Config): 

38 """SSL configuration.""" 

39 

40 crt: gws.FilePath 

41 """Crt bundle location.""" 

42 key: gws.FilePath 

43 """Key file location.""" 

44 hsts: gws.Duration = '365d' 

45 """HSTS max age.""" 

46 

47 

48class WebDirConfig(gws.Config): 

49 """Web-accessible directory.""" 

50 

51 dir: gws.DirPath 

52 """Directory path.""" 

53 allowMime: Optional[list[str]] 

54 """Allowed mime types.""" 

55 denyMime: Optional[list[str]] 

56 """Disallowed mime types (from the standard list).""" 

57 

58 

59class Config(gws.Config): 

60 """Site (virtual host) configuration""" 

61 

62 assets: Optional[WebDirConfig] 

63 """Root directory for assets.""" 

64 cors: Optional[CorsConfig] 

65 """Cors configuration.""" 

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

67 """Content Security Policy for this site.""" 

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

69 """Permissions Policy for this site.""" 

70 xFrameOptions: str = 'SAMEORIGIN' 

71 """X-Frame-Options header value. (added in 8.4)""" 

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

73 """Error page template. (deprecated in 8.4)""" 

74 hostnames: Optional[list[str]] 

75 """Host names this site responds to, lowercase and without a port. (added in 8.4)""" 

76 host: str = '' 

77 """Host name this site responds to. (deprecated in 8.4)""" 

78 rewrite: Optional[list[RewriteRuleConfig]] 

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

80 rewriteRules: Optional[list[RewriteRuleConfig]] 

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

82 withDefaultRewriteRules: bool = True 

83 """Whether to add default rewrite rules. (added in 8.4)""" 

84 canonicalHost: str = '' 

85 """Hostname for reversed URL rewriting.""" 

86 proxyCount: int = 0 

87 """Number of proxies between the client and the server which append to X-Forwarded-For. Only set this if the server is not reachable except via these proxies. (added in 8.4)""" 

88 root: Optional[WebDirConfig] 

89 """Root directory for static documents.""" 

90 

91 

92DEFAULT_ASSETS_DIR = '/data/assets' 

93DEFAULT_WEB_DIR = '/data/web' 

94DEFAULT_REWRITE_RULES = [ 

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

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

97] 

98 

99 

100class Object(gws.WebSite): 

101 ssl: bool 

102 contentSecurityPolicy: str 

103 permissionsPolicy: str 

104 xFrameOptions: str 

105 

106 def configure(self): 

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

108 p = self.cfg('host') 

109 if p and p != '*': 

110 self.hostnames = [p] 

111 

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

113 if not self.canonicalHost and self.hostnames: 

114 self.canonicalHost = self.hostnames[0] 

115 

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

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

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

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

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

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

122 # deprecated 

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

124 

125 p = self.cfg('root') 

126 if p: 

127 self.staticRoot = gws.WebDocumentRoot(p) 

128 elif gws.u.is_dir(DEFAULT_WEB_DIR): 

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

130 else: 

131 # note: web root must exist 

132 gws.log.warning(f'web root {DEFAULT_WEB_DIR!r} does not exist, using temporary directory') 

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

134 

135 p = self.cfg('assets') 

136 if p: 

137 self.assetsRoot = gws.WebDocumentRoot(p) 

138 elif gws.u.is_dir(DEFAULT_ASSETS_DIR): 

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

140 else: 

141 # note: assets root is optional 

142 self.assetsRoot = None 

143 

144 self.rewriteRules = [] 

145 p = self.cfg('rewriteRules') 

146 if not p: 

147 # deprecated 

148 p = self.cfg('rewrite') 

149 if not p: 

150 p = [] 

151 for c in p: 

152 r = gws.WebRewriteRule(c) 

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

154 # ensure rewriting from root 

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

156 self.rewriteRules.append(r) 

157 

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

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

160 for c in DEFAULT_REWRITE_RULES: 

161 if c.pattern not in patterns: 

162 self.rewriteRules.insert(0, c) 

163 

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

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

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

167 

168 path = self._apply_reverse_rewrite_rules(path) 

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

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

171 

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

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

174 u.params.update(params) 

175 

176 if mode == 'relative': 

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

178 

179 u.scheme = req.scheme 

180 

181 if mode == 'canonical': 

182 u.hostname = self.canonicalHost 

183 if not u.hostname: 

184 u.hostname = req.host 

185 u.port = req.port 

186 if not u.hostname: 

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

188 

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

190 

191 def _apply_reverse_rewrite_rules(self, path): 

192 for r in self.rewriteRules: 

193 if not r.reversed: 

194 continue 

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

196 if not m: 

197 continue 

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

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

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

201 

202 return path