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 22:59 +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 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.""" 

82 

83 

84class Object(gws.WebSite): 

85 canonicalHost: str 

86 ssl: bool 

87 contentSecurityPolicy: str 

88 permissionsPolicy: str 

89 useForwardedHost: bool 

90 

91 def configure(self): 

92 self.host = self.cfg('host', default='*') 

93 self.canonicalHost = self.cfg('canonicalHost') 

94 self.useForwardedHost = self.cfg('useForwardedHost') 

95 

96 self.staticRoot = gws.WebDocumentRoot(self.cfg('root')) 

97 

98 p = self.cfg('assets') 

99 self.assetsRoot = gws.WebDocumentRoot(p) if p else None 

100 

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

102 

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('/') 

108 

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

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

111 

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

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

114 

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) 

118 

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) 

126 

127 base = proto + '://' + host 

128 

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) 

138 

139 url = base + '/' + path.lstrip('/') 

140 return gws.lib.net.add_params(url, params) 

141 

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() 

148 

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