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
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-24 12:46 +0200
1from typing import Optional
3import re
5import gws
6import gws.lib.net
9class CorsConfig(gws.Config):
10 """CORS configuration."""
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."""
24class RewriteRuleConfig(gws.Config):
25 """Rewrite rule configuration."""
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."""
37class SSLConfig(gws.Config):
38 """SSL configuration."""
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."""
48class WebDirConfig(gws.Config):
49 """Web-accessible directory."""
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)."""
59class Config(gws.Config):
60 """Site (virtual host) configuration"""
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."""
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]
100class Object(gws.WebSite):
101 ssl: bool
102 contentSecurityPolicy: str
103 permissionsPolicy: str
104 xFrameOptions: str
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]
112 self.canonicalHost = self.cfg('canonicalHost') or ''
113 if not self.canonicalHost and self.hostnames:
114 self.canonicalHost = self.hostnames[0]
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'))
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'))
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
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)
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)
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)
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)
172 path = '/' + path.lstrip('/')
173 u = gws.lib.net.parse_url(path)
174 u.params.update(params)
176 if mode == 'relative':
177 return gws.lib.net.make_relative_url(u.path, u.params)
179 u.scheme = req.scheme
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')
189 return gws.lib.net.make_url(u)
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)
202 return path