main
py 152 lines 5.09 KB
Raw
1 import secrets
2 from helpers.api import (
3 ApiHandler,
4 Input,
5 Output,
6 Request,
7 Response,
8 session,
9 )
10 from helpers import runtime, dotenv, login
11 from helpers.tunnel_origins import origin_from_url
12 import fnmatch
13
14 ALLOWED_ORIGINS_KEY = "ALLOWED_ORIGINS"
15
16
17 class GetCsrfToken(ApiHandler):
18
19 @classmethod
20 def get_methods(cls) -> list[str]:
21 return ["GET"]
22
23 @classmethod
24 def requires_csrf(cls) -> bool:
25 return False
26
27 async def process(self, input: Input, request: Request) -> Output:
28
29 # check for allowed origin to prevent dns rebinding attacks
30 origin_check = await self.check_allowed_origin(request)
31 if not origin_check["ok"]:
32 return {
33 "ok": False,
34 "error": f"Origin {self.get_origin_from_request(request)} not allowed when login is disabled. Set login and password or add your URL to ALLOWED_ORIGINS env variable. Currently allowed origins: {','.join(origin_check['allowed_origins'])}",
35 }
36
37 # generate a csrf token if it doesn't exist
38 if "csrf_token" not in session:
39 session["csrf_token"] = secrets.token_urlsafe(32)
40
41 # return the csrf token and runtime id
42 return {
43 "ok": True,
44 "token": session["csrf_token"],
45 "runtime_id": runtime.get_runtime_id(),
46 }
47
48 async def check_allowed_origin(self, request: Request):
49 # if login is required, this check is unnecessary
50 if login.is_login_required():
51 return {"ok": True, "origin": "", "allowed_origins": ""}
52 # initialize allowed origins if not yet set
53 self.initialize_allowed_origins(request)
54 # otherwise, check if the origin is allowed
55 return await self.is_allowed_origin(request)
56
57 async def is_allowed_origin(self, request: Request):
58 # get the origin from the request
59 origin = self.get_origin_from_request(request)
60 if not origin:
61 return {"ok": False, "origin": "", "allowed_origins": ""}
62
63 # list of allowed origins
64 allowed_origins = await self.get_allowed_origins()
65
66 # check if the origin is allowed
67 match = any(
68 fnmatch.fnmatch(origin, allowed_origin)
69 for allowed_origin in allowed_origins
70 )
71 return {"ok": match, "origin": origin, "allowed_origins": allowed_origins}
72
73 def get_origin_from_request(self, request: Request):
74 # get from origin
75 r = request.headers.get("Origin") or request.environ.get("HTTP_ORIGIN")
76 if not r:
77 # try referer if origin not present
78 r = (
79 request.headers.get("Referer")
80 or request.referrer
81 or request.environ.get("HTTP_REFERER")
82 )
83 if not r:
84 return None
85 return origin_from_url(r)
86
87 async def get_allowed_origins(self) -> list[str]:
88 # get the allowed origins from the environment
89 allowed_origins = [
90 origin.strip()
91 for origin in (dotenv.get_dotenv_value(ALLOWED_ORIGINS_KEY) or "").split(
92 ","
93 )
94 if origin.strip()
95 ]
96
97 # if there are no allowed origins, allow default localhosts
98 if not allowed_origins:
99 allowed_origins = self.get_default_allowed_origins()
100
101 # always allow tunnel url if running
102 try:
103 from api.tunnel_proxy import process as tunnel_api_process
104
105 tunnel = await tunnel_api_process({"action": "get"})
106 if tunnel and isinstance(tunnel, dict) and tunnel.get("success"):
107 tunnel_origin = origin_from_url(tunnel.get("tunnel_url"))
108 if tunnel_origin:
109 allowed_origins.append(tunnel_origin)
110 except Exception:
111 pass
112
113 return allowed_origins
114
115 def get_default_allowed_origins(self) -> list[str]:
116 return [
117 "*://localhost",
118 "*://localhost:*",
119 "*://127.0.0.1",
120 "*://127.0.0.1:*",
121 "*://0.0.0.0",
122 "*://0.0.0.0:*",
123 ]
124
125 def initialize_allowed_origins(self, request: Request):
126 """
127 If A0 is hosted on a server, add the first visit origin to ALLOWED_ORIGINS.
128 This simplifies deployment process as users can access their new instance without
129 additional setup while keeping it secure.
130 """
131 # dotenv value is already set, do nothing
132 denv = dotenv.get_dotenv_value(ALLOWED_ORIGINS_KEY)
133 if denv:
134 return
135
136 # get the origin from the request
137 req_origin = self.get_origin_from_request(request)
138 if not req_origin:
139 return
140
141 # check if the origin is allowed by default
142 allowed_origins = self.get_default_allowed_origins()
143 match = any(
144 fnmatch.fnmatch(req_origin, allowed_origin)
145 for allowed_origin in allowed_origins
146 )
147 if match:
148 return
149
150 # if not, add it to the allowed origins
151 allowed_origins.append(req_origin)
152 dotenv.save_dotenv_value(ALLOWED_ORIGINS_KEY, ",".join(allowed_origins))