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
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-05 13:35 +0200
1"""Web site."""
3from typing import Optional
5import re
7import gws
8import gws.lib.net
11class CorsConfig(gws.Config):
12 """CORS headers added to responses, to allow cross-origin requests."""
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."""
26class RewriteRuleConfig(gws.Config):
27 """URL rewrite rule."""
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."""
39class SSLConfig(gws.Config):
40 """SSL certificate and HSTS settings."""
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."""
50class WebDirConfig(gws.Config):
51 """Directory served over the web."""
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."""
61class Config(gws.Config):
62 """Web site the server responds to."""
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."""
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]
104class Object(gws.WebSite):
105 """Web site."""
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."""
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]
124 self.canonicalHost = self.cfg('canonicalHost') or ''
125 if not self.canonicalHost and self.hostnames:
126 self.canonicalHost = self.hostnames[0]
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'))
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'))
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
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)
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)
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)
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)
188 path = '/' + path.lstrip('/')
189 u = gws.lib.net.parse_url(path)
190 u.params.update(params)
192 if mode == 'relative':
193 return gws.lib.net.make_relative_url(u.path, u.params)
195 u.scheme = req.scheme
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')
205 return gws.lib.net.make_url(u)
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)
219 return path