Coverage for gws-app/gws/base/web/site.py: 84%
115 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-16 23:09 +0200
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-16 23:09 +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 errorPage: Optional[gws.ext.config.template]
71 """Error page template."""
72 host: str = '*'
73 """Host name this site responds to (asterisk for any)."""
74 rewrite: Optional[list[RewriteRuleConfig]]
75 """Rewrite rules."""
76 canonicalHost: str = ''
77 """Hostname for reversed URL rewriting."""
78 useForwardedHost: bool = False
79 """Use X-Forwarded-Host for host matching. (added in 8.2)"""
80 root: WebDirConfig
81 """Root directory for static documents."""
84class Object(gws.WebSite):
85 canonicalHost: str
86 ssl: bool
87 contentSecurityPolicy: str
88 permissionsPolicy: str
89 useForwardedHost: bool
91 def configure(self):
92 self.host = self.cfg('host', default='*')
93 self.canonicalHost = self.cfg('canonicalHost')
94 self.useForwardedHost = self.cfg('useForwardedHost')
96 self.staticRoot = gws.WebDocumentRoot(self.cfg('root'))
98 p = self.cfg('assets')
99 self.assetsRoot = gws.WebDocumentRoot(p) if p else None
101 self.ssl = self.cfg('ssl')
103 self.rewriteRules = self.cfg('rewrite', default=[])
104 for r in self.rewriteRules:
105 if not gws.lib.net.is_abs_url(r.target):
106 # ensure rewriting from root
107 r.target = '/' + r.target.lstrip('/')
109 self.errorPage = self.create_child_if_configured(gws.ext.object.template, self.cfg('errorPage'))
110 self.corsOptions = self.cfg('cors')
112 self.contentSecurityPolicy = self.cfg('contentSecurityPolicy')
113 self.permissionsPolicy = self.cfg('permissionsPolicy')
115 def url_for(self, req, path, **params):
116 if gws.lib.net.is_abs_url(path):
117 return gws.lib.net.add_params(path, params)
119 proto = 'https' if self.ssl else 'http'
120 if self.canonicalHost:
121 host = self.canonicalHost
122 elif self.host != '*':
123 host = self.host
124 else:
125 host, proto = self._host_proto_from_env(req.environ)
127 base = proto + '://' + host
129 for rule in self.rewriteRules:
130 if rule.reversed:
131 m = re.match(rule.pattern, path)
132 if m:
133 # we use nginx syntax $1, need python's \1
134 t = rule.target.replace('$', '\\')
135 s = re.sub(rule.pattern, t, path)
136 url = s if gws.lib.net.is_abs_url(s) else base + '/' + s.lstrip('/')
137 return gws.lib.net.add_params(url, params)
139 url = base + '/' + path.lstrip('/')
140 return gws.lib.net.add_params(url, params)
142 def match_host(self, environ):
143 if self.host == '*':
144 return True
145 host, _ = self._host_proto_from_env(environ)
146 lh = host.lower().split(':')[0].strip()
147 return lh == self.host.lower()
149 def _host_proto_from_env(self, environ):
150 host = environ.get('HTTP_HOST', '')
151 proto = 'https' if self.ssl else 'http'
152 if not self.useForwardedHost:
153 return host, proto
154 fh = environ.get('HTTP_X_FORWARDED_HOST', '').split(',')[0].strip()
155 fp = environ.get('HTTP_X_FORWARDED_PROTO', '').split(',')[0].strip().lower()
156 return fh or host, fp or proto