FazBrowse GitHub Viewer
|
Trending
|
URL:
|
Home
Tools:
[Download Repo ZIP]
[View Raw Code]
[Original HTTPS Page]
pythonnative/src/pythonnative/devserver/auth.py at main · pythonnative/pythonnative · GitHub
pythonnative
/
pythonnative
Public
Notifications
You must be signed in to change notification settings
Fork
23
Star
131
Code
Issues
11
Pull requests
4
Actions
Projects
Security and quality
0
Insights
Additional navigation options
Code
Issues
Pull requests
Actions
Projects
Security and quality
Insights
Expand file tree
Breadcrumbs
pythonnative
/
src
/
pythonnative
/
devserver
/
auth.py
Copy path
More file actions
More file actions
Latest commit
History
History
History
158 lines (126 loc) · 5.16 KB
Breadcrumbs
pythonnative
/
src
/
pythonnative
/
devserver
/
auth.py
Copy path
File metadata and controls
158 lines (126 loc) · 5.16 KB
Raw
Copy raw file
Download raw file
Open symbols panel
Edit and raw actions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
"""The per-user development token that guards the dev server.
``pn start`` serves the application's source code, so everything that
reads it has to prove it belongs to the developer running the server.
The proof is a random token generated once per user and stored with mode
``0600`` in ``~/.pythonnative/dev-token``. It's reused across restarts, so
debug builds that ``pn run`` launched keep connecting after the server
restarts. Delete the file to issue a new token; every client then needs
the new URL.
Clients present the token in one of three ways (see
[`request_token`][pythonnative.devserver.auth.request_token]):
- the ``token`` query parameter (``pn run`` bakes it into the dev-client
URL, and ``pn start`` prints it in the preview URL),
- the ``X-PN-Token`` header, or
- the ``pn_token`` cookie, which the browser preview receives once in
exchange for the query parameter.
Two environment variables override the stored token:
``PN_DEV_TOKEN`` supplies the token itself, and ``PN_DEV_TOKEN_FILE``
points at a different token file.
"""
from
__future__
import
annotations
import
hmac
import
os
import
secrets
from
pathlib
import
Path
from
typing
import
Dict
,
Optional
from
urllib
.
parse
import
parse_qsl
,
urlencode
,
urlsplit
,
urlunsplit
__all__
=
[
"COOKIE_NAME"
,
"HEADER_NAME"
,
"QUERY_PARAM"
,
"TOKEN_ENV"
,
"TOKEN_FILE_ENV"
,
"load_token"
,
"request_token"
,
"token_matches"
,
"token_path"
,
"token_source"
,
"with_token"
,
]
TOKEN_ENV
=
"PN_DEV_TOKEN"
"""Environment variable that supplies the token directly (skips the file)."""
TOKEN_FILE_ENV
=
"PN_DEV_TOKEN_FILE"
"""Environment variable naming the token file (default ``~/.pythonnative/dev-token``)."""
QUERY_PARAM
=
"token"
"""Query parameter that carries the token in URLs."""
HEADER_NAME
=
"X-PN-Token"
"""Request header that carries the token."""
COOKIE_NAME
=
"pn_token"
"""Cookie the browser preview holds after exchanging the query parameter."""
def
token_path
()
->
Path
:
"""Where the token lives: ``$PN_DEV_TOKEN_FILE`` or ``~/.pythonnative/dev-token``."""
override
=
os
.
environ
.
get
(
TOKEN_FILE_ENV
)
if
override
:
return
Path
(
override
).
expanduser
()
return
Path
.
home
()
/
".pythonnative"
/
"dev-token"
def
token_source
()
->
str
:
"""Where [`load_token`][pythonnative.devserver.auth.load_token] gets the token, for messages."""
return
f"$
{
TOKEN_ENV
}
"
if
os
.
environ
.
get
(
TOKEN_ENV
,
""
).
strip
()
else
str
(
token_path
())
def
load_token
()
->
str
:
"""Return this user's dev token, creating it on first use.
``PN_DEV_TOKEN`` wins when set. Otherwise the token file is read, or
created with mode ``0600`` (its directory with ``0700``) holding
``secrets.token_urlsafe(24)``. A file that other users can read is
tightened to ``0600``; an empty one is replaced.
"""
explicit
=
os
.
environ
.
get
(
TOKEN_ENV
,
""
).
strip
()
if
explicit
:
return
explicit
path
=
token_path
()
existing
=
_read
(
path
)
if
existing
:
return
existing
path
.
parent
.
mkdir
(
mode
=
0o700
,
parents
=
True
,
exist_ok
=
True
)
token
=
secrets
.
token_urlsafe
(
24
)
try
:
fd
=
os
.
open
(
path
,
os
.
O_WRONLY
|
os
.
O_CREAT
|
os
.
O_EXCL
,
0o600
)
except
FileExistsError
:
# Another process created it first (or it exists but is empty).
existing
=
_read
(
path
)
if
existing
:
return
existing
fd
=
os
.
open
(
path
,
os
.
O_WRONLY
|
os
.
O_TRUNC
,
0o600
)
with
os
.
fdopen
(
fd
,
"w"
,
encoding
=
"utf-8"
)
as
handle
:
handle
.
write
(
token
+
"
\n
"
)
_restrict
(
path
)
return
token
def
_read
(
path
:
Path
)
->
Optional
[
str
]:
try
:
text
=
path
.
read_text
(
encoding
=
"utf-8"
).
strip
()
except
OSError
:
return
None
if
text
:
_restrict
(
path
)
return
text
or
None
def
_restrict
(
path
:
Path
)
->
None
:
try
:
if
path
.
stat
().
st_mode
&
0o077
:
os
.
chmod
(
path
,
0o600
)
except
OSError
:
pass
def
token_matches
(
candidate
:
Optional
[
str
],
token
:
str
)
->
bool
:
"""Compare ``candidate`` against ``token`` in constant time."""
if
not
candidate
:
return
False
return
hmac
.
compare_digest
(
candidate
.
encode
(
"utf-8"
),
token
.
encode
(
"utf-8"
))
def
request_token
(
headers
:
Dict
[
str
,
str
],
query
:
Dict
[
str
,
str
])
->
Optional
[
str
]:
"""The token a request presents, if any.
Checks the ``token`` query parameter, then the ``X-PN-Token`` header,
then the ``pn_token`` cookie. ``headers`` must have lower-cased names.
"""
for
value
in
(
query
.
get
(
QUERY_PARAM
),
headers
.
get
(
HEADER_NAME
.
lower
()),
_cookie
(
headers
.
get
(
"cookie"
,
""
))):
if
value
:
return
value
return
None
def
_cookie
(
header
:
str
)
->
Optional
[
str
]:
for
part
in
header
.
split
(
";"
):
name
,
sep
,
value
=
part
.
strip
().
partition
(
"="
)
if
sep
and
name
==
COOKIE_NAME
:
return
value
.
strip
().
strip
(
'"'
)
or
None
return
None
def
with_token
(
url
:
str
,
token
:
str
)
->
str
:
"""Return ``url`` with its ``token`` query parameter set to ``token``."""
parts
=
urlsplit
(
url
)
query
=
[(
k
,
v
)
for
k
,
v
in
parse_qsl
(
parts
.
query
,
keep_blank_values
=
True
)
if
k
!=
QUERY_PARAM
]
query
.
append
((
QUERY_PARAM
,
token
))
return
urlunsplit
((
parts
.
scheme
,
parts
.
netloc
,
parts
.
path
,
urlencode
(
query
),
parts
.
fragment
))
Back
|
FazBrowse Home
|
New Git URL