Coverage for gws-app/gws/base/web/site.py: 88%
139 statements
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-23 20:06 +0200
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-23 20:06 +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 reversed: bool = False
34 """Rewrite URLs generated by the server instead of incoming URLs."""
37class SSLConfig(gws.Config):
38 """SSL certificate and HSTS settings."""
40 crt: gws.FilePath
41 """Certificate bundle location."""
42 key: gws.FilePath
43 """Private key file location."""
44 hsts: gws.Duration = '365d'
45 """Max. age for the Strict-Transport-Security header."""
48class WebDirConfig(gws.Config):
49 """Directory served over the web."""
51 dir: gws.DirPath
52 """Directory path."""
53 allowMime: Optional[list[str]]
54 """Serve only files of these MIME types."""
55 denyMime: Optional[list[str]]
56 """MIME types not to serve."""
59class Config(gws.Config):
60 """Web site the server responds to."""
62 assets: Optional[WebDirConfig]
63 """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 the site."""
68 permissionsPolicy: str = 'geolocation=(self), camera=(), microphone=()'
69 """Permissions policy for the site."""
70 xFrameOptions: str = 'SAMEORIGIN'
71 """Whether the site may be embedded in frames."""
72 errorPage: Optional[gws.ext.config.template]
73 """Template for HTTP error pages. (deprecated in 8.4)"""
74 hostnames: Optional[list[str]]
75 """Host names this site responds to, lowercase and without a port."""
76 host: str = ''
77 """Host name. (deprecated in 8.4)"""
78 rewrite: Optional[list[RewriteRuleConfig]]
79 """Rewrite rules. (deprecated in 8.4)"""
80 rewriteRules: Optional[list[RewriteRuleConfig]]
81 """Rules to rewrite incoming URLs and URLs generated by the server."""
82 withDefaultRewriteRules: bool = True
83 """Add the default rewrite rules for the application."""
84 canonicalHost: str = ''
85 """Host name for canonical URLs."""
86 proxyCount: int = 0
87 """Number of proxies in front of the server."""
88 root: Optional[WebDirConfig]
89 """Directory for static documents."""
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 gws.WebRewriteRule(pattern=r'^/_/webPage/name/home$', target='/', reversed=True),
98 gws.WebRewriteRule(pattern=r'^/_/webPage/name/project/projectUid/([a-z0-9_-]+)', target='/project/$1', reversed=True),
99]
102class Object(gws.WebSite):
103 """Web site."""
105 ssl: bool
106 """The site is served over https."""
107 contentSecurityPolicy: str
108 """Content-Security-Policy header value."""
109 permissionsPolicy: str
110 """Permissions-Policy header value."""
111 xFrameOptions: str
112 """X-Frame-Options header value."""
114 def configure(self):
115 self.hostnames = self.cfg('hostnames') or []
116 p = self.cfg('host')
117 if p:
118 self.root.config_warning('"web.site.host" is deprecated, use "web.site.hostnames"')
119 if p and p != '*':
120 self.hostnames = [p]
122 self.canonicalHost = self.cfg('canonicalHost') or ''
123 if not self.canonicalHost and self.hostnames:
124 self.canonicalHost = self.hostnames[0]
126 self.proxyCount = self.cfg('proxyCount') or 0
127 self.ssl = self.cfg('ssl')
128 self.corsOptions = self.cfg('cors')
129 self.contentSecurityPolicy = self.cfg('contentSecurityPolicy')
130 self.permissionsPolicy = self.cfg('permissionsPolicy')
131 self.xFrameOptions = self.cfg('xFrameOptions')
132 # deprecated
133 if self.cfg('errorPage'):
134 self.root.config_warning('"web.site.errorPage" is deprecated')
135 self.errorPage = self.create_child_if_configured(gws.ext.object.template, self.cfg('errorPage'))
137 p = self.cfg('root')
138 if p:
139 self.staticRoot = gws.WebDocumentRoot(p)
140 elif gws.u.is_dir(DEFAULT_WEB_DIR):
141 self.staticRoot = gws.WebDocumentRoot(dir=DEFAULT_WEB_DIR)
142 else:
143 # note: web root must exist
144 self.root.config_warning(f'web root {DEFAULT_WEB_DIR!r} does not exist, using temporary directory')
145 self.staticRoot = gws.WebDocumentRoot(dir=gws.u.ensure_dir(gws.c.TMP_DIR + '/web'))
147 p = self.cfg('assets')
148 if p:
149 self.assetsRoot = gws.WebDocumentRoot(p)
150 elif gws.u.is_dir(DEFAULT_ASSETS_DIR):
151 self.assetsRoot = gws.WebDocumentRoot(dir=DEFAULT_ASSETS_DIR)
152 else:
153 # note: assets root is optional
154 self.assetsRoot = None
156 self.rewriteRules = []
157 p = self.cfg('rewriteRules')
158 if not p:
159 # deprecated
160 p = self.cfg('rewrite')
161 if p:
162 self.root.config_warning('"web.site.rewrite" is deprecated, use "web.site.rewriteRules"')
163 if not p:
164 p = []
165 for c in p:
166 r = gws.WebRewriteRule(c)
167 if not gws.lib.net.is_abs_url(r.target):
168 # ensure rewriting from root
169 r.target = '/' + r.target.lstrip('/')
170 self.rewriteRules.append(r)
172 if self.cfg('withDefaultRewriteRules', default=True):
173 patterns = set(r.pattern for r in self.rewriteRules)
174 for c in DEFAULT_REWRITE_RULES:
175 if c.pattern not in patterns:
176 self.rewriteRules.append(c)
178 def url_for(self, req, path, mode, **params):
179 if gws.lib.net.is_abs_url(path):
180 return gws.lib.net.add_params(path, params)
182 path = self._apply_reverse_rewrite_rules(path)
183 if gws.lib.net.is_abs_url(path):
184 return gws.lib.net.add_params(path, params)
186 path = '/' + path.lstrip('/')
187 u = gws.lib.net.parse_url(path)
188 u.params.update(params)
190 if mode == 'relative':
191 return gws.lib.net.make_relative_url(u.path, u.params)
193 u.scheme = req.scheme
195 if mode == 'canonical':
196 u.hostname = self.canonicalHost
197 if not u.hostname:
198 u.hostname = req.host
199 u.port = req.port
200 if not u.hostname:
201 raise gws.BadRequestError('no host for an absolute url')
203 return gws.lib.net.make_url(u)
205 def _apply_reverse_rewrite_rules(self, path):
206 """Rewrite a path with the first matching reversed rule."""
207 for r in self.rewriteRules:
208 if not r.reversed:
209 continue
210 m = re.search(r.pattern, path)
211 if not m:
212 continue
213 # we use nginx syntax $1, need python's \1
214 t = r.target.replace('$', '\\')
215 return re.sub(r.pattern, t, path)
217 return path