browserfetch 0.6.2__tar.gz → 0.10.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {browserfetch-0.6.2 → browserfetch-0.10.0}/PKG-INFO +72 -71
- {browserfetch-0.6.2 → browserfetch-0.10.0}/README.rst +2 -1
- {browserfetch-0.6.2 → browserfetch-0.10.0}/browserfetch/__init__.py +117 -48
- browserfetch-0.10.0/browserfetch/__main__.py +36 -0
- browserfetch-0.10.0/browserfetch/browserfetch.js +147 -0
- browserfetch-0.10.0/pyproject.toml +62 -0
- browserfetch-0.6.2/browserfetch/__main__.py +0 -24
- browserfetch-0.6.2/browserfetch/browserfetch.js +0 -75
- browserfetch-0.6.2/browserfetch.egg-info/PKG-INFO +0 -71
- browserfetch-0.6.2/browserfetch.egg-info/SOURCES.txt +0 -12
- browserfetch-0.6.2/browserfetch.egg-info/dependency_links.txt +0 -1
- browserfetch-0.6.2/browserfetch.egg-info/not-zip-safe +0 -1
- browserfetch-0.6.2/browserfetch.egg-info/requires.txt +0 -2
- browserfetch-0.6.2/browserfetch.egg-info/top_level.txt +0 -1
- browserfetch-0.6.2/pyproject.toml +0 -50
- browserfetch-0.6.2/setup.cfg +0 -4
- {browserfetch-0.6.2 → browserfetch-0.10.0}/LICENSE +0 -0
|
@@ -1,71 +1,72 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
2
|
-
Name: browserfetch
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: fetch in Python using your browser!
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
9
|
-
Requires-
|
|
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
|
-
loop
|
|
42
|
-
loop.
|
|
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
|
-
.. _`browser_cookie3 stopped working on Chrome-based browsers`: https://github.com/borisbabic/browser_cookie3/issues/180
|
|
69
|
-
.. _tampermonkey: https://github.com/Tampermonkey/tampermonkey
|
|
70
|
-
.. _violentmonkey: https://github.com/violentmonkey/violentmonkey
|
|
71
|
-
.. _browserfetch.js: https://github.com/5j9/browserfetch/blob/master/browserfetch/browserfetch.js
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: browserfetch
|
|
3
|
+
Version: 0.10.0
|
|
4
|
+
Summary: fetch in Python using your browser!
|
|
5
|
+
Keywords: browser,fetch,python,cookies
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Description-Content-Type: text/x-rst
|
|
8
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
9
|
+
Requires-Dist: aiohttp
|
|
10
|
+
Requires-Dist: pyperclip
|
|
11
|
+
Requires-Dist: cyclopts
|
|
12
|
+
Project-URL: Homepage, https://github.com/5j9/browserfetch
|
|
13
|
+
|
|
14
|
+
Fetch using your browser.
|
|
15
|
+
|
|
16
|
+
Let the browser manage cookies for you.
|
|
17
|
+
|
|
18
|
+
⚠️ Incomplete. Not tested thoroughly. Consider using `Playwright`_, especially for more complex scenarios.
|
|
19
|
+
|
|
20
|
+
Usage
|
|
21
|
+
-----
|
|
22
|
+
1. You'll run a Python script containing some code like this:
|
|
23
|
+
|
|
24
|
+
.. code-block:: python
|
|
25
|
+
|
|
26
|
+
from asyncio import gather, new_event_loop
|
|
27
|
+
|
|
28
|
+
from browserfetch import fetch, get, post, run_server
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
async def main():
|
|
32
|
+
response1, response2, reponse3 = await gather(
|
|
33
|
+
get('https://example.com/path1', params={'a': 1}),
|
|
34
|
+
fetch('https://example.com/image.png'),
|
|
35
|
+
post('https://example.com/path2', data={'a': 1}),
|
|
36
|
+
)
|
|
37
|
+
# do stuff with retrieved responses
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
loop = new_event_loop()
|
|
41
|
+
loop.create_task(start_server())
|
|
42
|
+
loop.run_until_complete(main())
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
2. Open your browser, goto http://example.com (perhaps solve a captcha and log in).
|
|
46
|
+
3. Copy the contents of `browserfetch.js`_ file and paste it in browser's console. (You can use a browser extensions like violentmonkey_/tampermonkey_ to do this step for you.)
|
|
47
|
+
|
|
48
|
+
That's it! Your Python script starts handling requests.
|
|
49
|
+
The browser tab should remain open of-coarse.
|
|
50
|
+
|
|
51
|
+
The server can handle multiple websocket connections from different websites simultaneously.
|
|
52
|
+
|
|
53
|
+
How it works
|
|
54
|
+
------------
|
|
55
|
+
``browserfetch`` communicates with your browser using a websocket. The ``fetch`` function just passes the request to browser and it is the browser that handles the actual request. Response data is sent back to Python using the same WebSocket connection.
|
|
56
|
+
|
|
57
|
+
Motivations
|
|
58
|
+
-----------
|
|
59
|
+
* `browser_cookie3 stopped working on Chrome-based browsers`_. There is a workaround: ShadowCopy, but it requires admin privilege.
|
|
60
|
+
* Another issue with browser_cookie's approach is that it retrieves cookies from cookie files, but these files are not updated instantly. Thus, you might have to wait or retry a few times before you can successfully access newly set cookies.
|
|
61
|
+
* ShadowCopying and File access are slow and inefficient operations.
|
|
62
|
+
|
|
63
|
+
Downsides
|
|
64
|
+
---------
|
|
65
|
+
* Setting up ``browserfetch`` is more cumbersome since it requires running a Python server and also injecting a small script into the webpage. Using ``browser_cookie3`` might be a better choice if there are many websites that you need to communicate with.
|
|
66
|
+
|
|
67
|
+
.. _playwright: https://playwright.dev/python/docs/intro
|
|
68
|
+
.. _`browser_cookie3 stopped working on Chrome-based browsers`: https://github.com/borisbabic/browser_cookie3/issues/180
|
|
69
|
+
.. _tampermonkey: https://github.com/Tampermonkey/tampermonkey
|
|
70
|
+
.. _violentmonkey: https://github.com/violentmonkey/violentmonkey
|
|
71
|
+
.. _browserfetch.js: https://github.com/5j9/browserfetch/blob/master/browserfetch/browserfetch.js
|
|
72
|
+
|
|
@@ -2,7 +2,7 @@ Fetch using your browser.
|
|
|
2
2
|
|
|
3
3
|
Let the browser manage cookies for you.
|
|
4
4
|
|
|
5
|
-
⚠️
|
|
5
|
+
⚠️ Incomplete. Not tested thoroughly. Consider using `Playwright`_, especially for more complex scenarios.
|
|
6
6
|
|
|
7
7
|
Usage
|
|
8
8
|
-----
|
|
@@ -51,6 +51,7 @@ Downsides
|
|
|
51
51
|
---------
|
|
52
52
|
* Setting up ``browserfetch`` is more cumbersome since it requires running a Python server and also injecting a small script into the webpage. Using ``browser_cookie3`` might be a better choice if there are many websites that you need to communicate with.
|
|
53
53
|
|
|
54
|
+
.. _playwright: https://playwright.dev/python/docs/intro
|
|
54
55
|
.. _`browser_cookie3 stopped working on Chrome-based browsers`: https://github.com/borisbabic/browser_cookie3/issues/180
|
|
55
56
|
.. _tampermonkey: https://github.com/Tampermonkey/tampermonkey
|
|
56
57
|
.. _violentmonkey: https://github.com/violentmonkey/violentmonkey
|
|
@@ -1,23 +1,36 @@
|
|
|
1
|
-
__version__ = '0.
|
|
2
|
-
|
|
1
|
+
__version__ = '0.10.0'
|
|
3
2
|
import atexit
|
|
4
|
-
from asyncio import
|
|
3
|
+
from asyncio import (
|
|
4
|
+
AbstractEventLoop,
|
|
5
|
+
CancelledError,
|
|
6
|
+
Event,
|
|
7
|
+
Task,
|
|
8
|
+
get_running_loop,
|
|
9
|
+
wait_for,
|
|
10
|
+
)
|
|
5
11
|
from collections import defaultdict
|
|
6
12
|
from dataclasses import dataclass
|
|
13
|
+
from html import escape
|
|
7
14
|
from json import dumps, loads
|
|
8
15
|
from logging import getLogger
|
|
9
|
-
from typing import Any
|
|
10
16
|
from urllib.parse import urlencode
|
|
11
17
|
|
|
12
18
|
from aiohttp import ClientSession, ClientWebSocketResponse
|
|
13
|
-
from aiohttp.web import
|
|
19
|
+
from aiohttp.web import (
|
|
20
|
+
Application,
|
|
21
|
+
Request,
|
|
22
|
+
Response as _Response,
|
|
23
|
+
RouteTableDef,
|
|
24
|
+
WebSocketResponse,
|
|
25
|
+
)
|
|
14
26
|
from aiohttp.web_runner import AppRunner, TCPSite
|
|
15
27
|
|
|
16
28
|
logger = getLogger(__name__)
|
|
17
29
|
# maps host to its host_ready event or its websocket
|
|
18
|
-
hosts:
|
|
30
|
+
hosts: defaultdict[
|
|
19
31
|
str, Event | WebSocketResponse | ClientWebSocketResponse
|
|
20
32
|
] = defaultdict(Event)
|
|
33
|
+
|
|
21
34
|
# maps response event id to its response event or response dict
|
|
22
35
|
responses: dict[int, Event | dict] = {}
|
|
23
36
|
|
|
@@ -74,12 +87,15 @@ async def _request(
|
|
|
74
87
|
if body is not None:
|
|
75
88
|
bytes_ += b'\0' + body
|
|
76
89
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
90
|
+
value = hosts[host]
|
|
91
|
+
match value:
|
|
92
|
+
case Event():
|
|
93
|
+
# wait for the Event to be turned into a websocket response
|
|
94
|
+
await value.wait()
|
|
95
|
+
ws = hosts[host]
|
|
96
|
+
await ws.send_bytes(bytes_) # type: ignore
|
|
97
|
+
case WebSocketResponse() | ClientWebSocketResponse():
|
|
98
|
+
await value.send_bytes(bytes_)
|
|
83
99
|
|
|
84
100
|
try:
|
|
85
101
|
await wait_for(response_ready.wait(), data['timeout'])
|
|
@@ -87,7 +103,8 @@ async def _request(
|
|
|
87
103
|
responses.pop(event_id, None)
|
|
88
104
|
raise
|
|
89
105
|
|
|
90
|
-
return
|
|
106
|
+
# this must return a dict at this point, not an Event
|
|
107
|
+
return responses.pop(event_id) # type: ignore
|
|
91
108
|
|
|
92
109
|
|
|
93
110
|
async def receive_responses(ws: WebSocketResponse | ClientWebSocketResponse):
|
|
@@ -98,14 +115,17 @@ async def receive_responses(ws: WebSocketResponse | ClientWebSocketResponse):
|
|
|
98
115
|
j['body'] = body
|
|
99
116
|
event_id = j.pop('event_id')
|
|
100
117
|
try:
|
|
101
|
-
|
|
118
|
+
# We expect only one response to be recieved for each event,
|
|
119
|
+
# therefore this must be an Event, not a dict.
|
|
120
|
+
response_ready: Event = responses[event_id] # type: ignore
|
|
102
121
|
except KeyError: # lock has reached timeout already
|
|
103
122
|
continue
|
|
104
123
|
responses[event_id] = j
|
|
105
|
-
|
|
124
|
+
response_ready.set()
|
|
106
125
|
|
|
107
126
|
|
|
108
127
|
routes = RouteTableDef()
|
|
128
|
+
PROTOCOL = '3'
|
|
109
129
|
|
|
110
130
|
|
|
111
131
|
@routes.get('/ws')
|
|
@@ -113,7 +133,10 @@ async def _(request):
|
|
|
113
133
|
ws = WebSocketResponse()
|
|
114
134
|
await ws.prepare(request)
|
|
115
135
|
|
|
116
|
-
host = await ws.receive_str()
|
|
136
|
+
version, _, host = (await ws.receive_str()).partition(' ')
|
|
137
|
+
assert version == PROTOCOL, (
|
|
138
|
+
f'JavaScript protocol version: {version}, expected: {PROTOCOL}'
|
|
139
|
+
)
|
|
117
140
|
logger.info('registering host %s', host)
|
|
118
141
|
|
|
119
142
|
ws_or_e = hosts[host]
|
|
@@ -126,10 +149,11 @@ async def _(request):
|
|
|
126
149
|
except TypeError:
|
|
127
150
|
logger.info('host WebSocket was closed')
|
|
128
151
|
hosts[host] = Event()
|
|
152
|
+
return ws
|
|
129
153
|
|
|
130
154
|
|
|
131
155
|
@routes.get('/relay')
|
|
132
|
-
async def _(request):
|
|
156
|
+
async def _(request: Request) -> WebSocketResponse:
|
|
133
157
|
ws = WebSocketResponse()
|
|
134
158
|
await ws.prepare(request)
|
|
135
159
|
|
|
@@ -137,13 +161,15 @@ async def _(request):
|
|
|
137
161
|
try:
|
|
138
162
|
bytes_ = await ws.receive_bytes()
|
|
139
163
|
except TypeError: # ws closed
|
|
140
|
-
return
|
|
164
|
+
return ws
|
|
141
165
|
data, null, body = bytes_.partition(b'\0')
|
|
142
166
|
data = loads(data)
|
|
143
167
|
relay_event_id = data['event_id']
|
|
144
168
|
|
|
145
169
|
try:
|
|
146
|
-
r = await _request(
|
|
170
|
+
r: dict = await _request(
|
|
171
|
+
data.pop('host'), data, body if null else None
|
|
172
|
+
)
|
|
147
173
|
except TimeoutError:
|
|
148
174
|
r = {'error': 'TimeoutError in relay'}
|
|
149
175
|
|
|
@@ -152,6 +178,22 @@ async def _(request):
|
|
|
152
178
|
await ws.send_bytes(dumps(r).encode() + b'\0' + body)
|
|
153
179
|
|
|
154
180
|
|
|
181
|
+
@routes.get('/')
|
|
182
|
+
async def _(_) -> _Response:
|
|
183
|
+
hosts_html = '\n'.join(
|
|
184
|
+
[f'<li>{k}: {escape(str(v))}</li>' for k, v in hosts.items()]
|
|
185
|
+
)
|
|
186
|
+
responses_html = '\n'.join(
|
|
187
|
+
[f'<li>{k}: {escape(str(v))}</li>' for k, v in responses.items()]
|
|
188
|
+
)
|
|
189
|
+
return _Response(
|
|
190
|
+
body='<meta charset="utf-8">\n<title>browserfetch</title>\n'
|
|
191
|
+
f'Hosts:\n{hosts_html}\n'
|
|
192
|
+
f'Responses:\n{responses_html}',
|
|
193
|
+
content_type='text/html',
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
|
|
155
197
|
async def relay_client(server_host, server_port):
|
|
156
198
|
async with ClientSession() as session:
|
|
157
199
|
relay_url = f'ws://{server_host}:{server_port}/relay'
|
|
@@ -165,18 +207,37 @@ async def relay_client(server_host, server_port):
|
|
|
165
207
|
try:
|
|
166
208
|
await receive_responses(ws)
|
|
167
209
|
except TypeError:
|
|
168
|
-
logger.info('relay
|
|
210
|
+
logger.info('relay WebSocket was closed')
|
|
211
|
+
hosts.default_factory = Event
|
|
212
|
+
for host, ws_or_e in hosts.items():
|
|
213
|
+
if isinstance(ws_or_e, Event):
|
|
214
|
+
ws_or_e.clear()
|
|
215
|
+
else:
|
|
216
|
+
hosts[host] = Event()
|
|
217
|
+
await start_server(host=_host, port=_port)
|
|
169
218
|
return
|
|
170
219
|
|
|
171
220
|
|
|
221
|
+
async def evaluate(
|
|
222
|
+
string: str,
|
|
223
|
+
host: str,
|
|
224
|
+
timeout: int | float = 95,
|
|
225
|
+
):
|
|
226
|
+
"""Evaluate string in browser context and return JSON.stringify(result)."""
|
|
227
|
+
d = await _request(
|
|
228
|
+
host, {'action': 'eval', 'string': string, 'timeout': timeout}, None
|
|
229
|
+
)
|
|
230
|
+
return d['result']
|
|
231
|
+
|
|
232
|
+
|
|
172
233
|
async def fetch(
|
|
173
234
|
url: str,
|
|
174
235
|
*,
|
|
175
|
-
params: dict = None,
|
|
176
|
-
body: bytes = None,
|
|
236
|
+
params: dict | None = None,
|
|
237
|
+
body: bytes | None = None,
|
|
177
238
|
timeout: int | float = 95,
|
|
178
|
-
options: dict = None,
|
|
179
|
-
host=None,
|
|
239
|
+
options: dict | None = None,
|
|
240
|
+
host: str | None = None,
|
|
180
241
|
) -> Response:
|
|
181
242
|
"""Fetch using browser fetch API available on host.
|
|
182
243
|
|
|
@@ -195,6 +256,7 @@ async def fetch(
|
|
|
195
256
|
d = await _request(
|
|
196
257
|
host,
|
|
197
258
|
{
|
|
259
|
+
'action': 'fetch',
|
|
198
260
|
'url': url,
|
|
199
261
|
'options': options,
|
|
200
262
|
'timeout': timeout,
|
|
@@ -211,9 +273,9 @@ async def fetch(
|
|
|
211
273
|
async def get(
|
|
212
274
|
url: str,
|
|
213
275
|
*,
|
|
214
|
-
params: dict = None,
|
|
215
|
-
options: dict = None,
|
|
216
|
-
host: str = None,
|
|
276
|
+
params: dict | None = None,
|
|
277
|
+
options: dict | None = None,
|
|
278
|
+
host: str | None = None,
|
|
217
279
|
timeout: int | float = 95,
|
|
218
280
|
) -> Response:
|
|
219
281
|
if options is None:
|
|
@@ -228,30 +290,33 @@ async def get(
|
|
|
228
290
|
async def post(
|
|
229
291
|
url: str,
|
|
230
292
|
*,
|
|
231
|
-
params: dict = None,
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
json=None,
|
|
293
|
+
params: dict | None = None,
|
|
294
|
+
data: bytes | dict | str | None = None,
|
|
295
|
+
form: dict | None = None,
|
|
235
296
|
timeout: int | float = 95,
|
|
236
|
-
options: dict = None,
|
|
237
|
-
host: str = None,
|
|
297
|
+
options: dict | None = None,
|
|
298
|
+
host: str | None = None,
|
|
238
299
|
) -> Response:
|
|
239
300
|
if options is None:
|
|
240
|
-
options
|
|
301
|
+
options = {'method': 'POST'}
|
|
241
302
|
else:
|
|
242
303
|
options['method'] = 'POST'
|
|
243
304
|
|
|
244
|
-
if json is not None:
|
|
245
|
-
assert body is None
|
|
246
|
-
body = dumps(json).encode()
|
|
247
|
-
headers = options.setdefault('headers', {})
|
|
248
|
-
headers['Content-Type'] = 'application/json'
|
|
249
|
-
|
|
250
305
|
if data is not None:
|
|
251
|
-
|
|
252
|
-
|
|
306
|
+
if isinstance(data, str):
|
|
307
|
+
body = data.encode()
|
|
308
|
+
elif isinstance(data, bytes):
|
|
309
|
+
body = data
|
|
310
|
+
else:
|
|
311
|
+
body = dumps(data).encode()
|
|
312
|
+
headers = options.setdefault('headers', {})
|
|
313
|
+
headers['Content-Type'] = 'application/json'
|
|
314
|
+
elif form is not None:
|
|
315
|
+
body = urlencode(form).encode()
|
|
253
316
|
headers = options.setdefault('headers', {})
|
|
254
317
|
headers['Content-Type'] = 'application/x-www-form-urlencoded'
|
|
318
|
+
else:
|
|
319
|
+
body = None
|
|
255
320
|
|
|
256
321
|
return await fetch(
|
|
257
322
|
url,
|
|
@@ -268,12 +333,13 @@ app.add_routes(routes)
|
|
|
268
333
|
app_runner = AppRunner(app)
|
|
269
334
|
|
|
270
335
|
|
|
271
|
-
def
|
|
336
|
+
def _shutdown_server(loop: AbstractEventLoop):
|
|
272
337
|
logger.info('waiting for app_runner.cleanup()')
|
|
273
338
|
loop.run_until_complete(app_runner.cleanup())
|
|
274
339
|
|
|
275
340
|
|
|
276
|
-
def
|
|
341
|
+
def _cancel_relay_task(loop: AbstractEventLoop, task: Task):
|
|
342
|
+
logger.info('cancelling relay task')
|
|
277
343
|
task.cancel()
|
|
278
344
|
try:
|
|
279
345
|
loop.run_until_complete(task)
|
|
@@ -282,10 +348,13 @@ def shutdown_relay_client(loop, task: Task):
|
|
|
282
348
|
|
|
283
349
|
|
|
284
350
|
_server = False
|
|
351
|
+
_host = '127.0.0.1'
|
|
352
|
+
_port = 9404
|
|
285
353
|
|
|
286
354
|
|
|
287
|
-
async def start_server(*, host=
|
|
288
|
-
global _server
|
|
355
|
+
async def start_server(*, host=_host, port=_port):
|
|
356
|
+
global _server, _host, _port
|
|
357
|
+
_host, _port = host, port
|
|
289
358
|
loop = get_running_loop()
|
|
290
359
|
await app_runner.setup()
|
|
291
360
|
site = TCPSite(app_runner, host, port)
|
|
@@ -298,8 +367,8 @@ async def start_server(*, host='127.0.0.1', port=9404):
|
|
|
298
367
|
e,
|
|
299
368
|
)
|
|
300
369
|
relay_task = loop.create_task(relay_client(host, port))
|
|
301
|
-
atexit.register(
|
|
370
|
+
atexit.register(_cancel_relay_task, loop, relay_task)
|
|
302
371
|
else:
|
|
303
372
|
_server = True
|
|
304
|
-
atexit.register(
|
|
373
|
+
atexit.register(_shutdown_server, loop)
|
|
305
374
|
logger.info('server started at http://%s:%s', host, port)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def read_js(host_name_generator: str | None):
|
|
5
|
+
js = (Path(__file__).parent / 'browserfetch.js').read_bytes().decode()
|
|
6
|
+
if host_name_generator is not None:
|
|
7
|
+
return js.replace(
|
|
8
|
+
'async function generateHostName() { return location.host };',
|
|
9
|
+
host_name_generator,
|
|
10
|
+
1,
|
|
11
|
+
)
|
|
12
|
+
return js
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def copyjs(*, host_name_generator: str | None = None):
|
|
16
|
+
"""Copy contents of browserfetch.js to clipboard.
|
|
17
|
+
|
|
18
|
+
`host_name_generator` should be a string containing
|
|
19
|
+
an async JavaScript function named `generateHostName()`. This file's
|
|
20
|
+
contents will be copied into the generated js script and will be used
|
|
21
|
+
to generate a host name for connections. The default
|
|
22
|
+
`generateHostName` function returns `location.host`.
|
|
23
|
+
"""
|
|
24
|
+
from pyperclip import copy
|
|
25
|
+
|
|
26
|
+
copy(read_js(host_name_generator))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
if __name__ == '__main__':
|
|
30
|
+
from cyclopts import App
|
|
31
|
+
|
|
32
|
+
from browserfetch import __version__
|
|
33
|
+
|
|
34
|
+
app = App(version=__version__)
|
|
35
|
+
app.command(copyjs)
|
|
36
|
+
app()
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// ==UserScript==
|
|
2
|
+
// @name browserfetch
|
|
3
|
+
// @namespace https://github.com/5j9/browserfetch
|
|
4
|
+
// @match https://example.com/
|
|
5
|
+
// @grant GM_registerMenuCommand
|
|
6
|
+
// ==/UserScript==
|
|
7
|
+
// @ts-check
|
|
8
|
+
(async () => {
|
|
9
|
+
/**
|
|
10
|
+
* @param {Uint8Array | null} body
|
|
11
|
+
* @param {Object} req
|
|
12
|
+
* @returns {Promise<Blob>}
|
|
13
|
+
*/
|
|
14
|
+
async function doFetch(req, body) {
|
|
15
|
+
var returnData, response;
|
|
16
|
+
var options = req['options'] || {};
|
|
17
|
+
|
|
18
|
+
if (req['timeout']) {
|
|
19
|
+
options.signal = AbortSignal.timeout(req['timeout'] * 1000);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (body !== null) {
|
|
23
|
+
options.body = body;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
var r = await fetch(req['url'], options);
|
|
28
|
+
returnData = {
|
|
29
|
+
'event_id': req['event_id'],
|
|
30
|
+
'headers': Object.fromEntries([...r.headers]),
|
|
31
|
+
'ok': r.ok,
|
|
32
|
+
'redirected': r.redirected,
|
|
33
|
+
'status': r.status,
|
|
34
|
+
'status_text': r.statusText,
|
|
35
|
+
'type': r.type,
|
|
36
|
+
'url': r.url
|
|
37
|
+
};
|
|
38
|
+
response = await r.blob();
|
|
39
|
+
} catch (err) {
|
|
40
|
+
returnData = {
|
|
41
|
+
'event_id': req['event_id'],
|
|
42
|
+
'error': err.toString()
|
|
43
|
+
};
|
|
44
|
+
response = "";
|
|
45
|
+
};
|
|
46
|
+
return new Blob([new TextEncoder().encode(JSON.stringify(returnData)), "\0", response]);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
*
|
|
51
|
+
* @param {Object} req
|
|
52
|
+
* @returns {Promise<Uint8Array>}
|
|
53
|
+
*/
|
|
54
|
+
async function doEval(req) {
|
|
55
|
+
var evalled, resp;
|
|
56
|
+
try {
|
|
57
|
+
evalled = eval(req['string']);
|
|
58
|
+
switch (evalled.constructor.name) {
|
|
59
|
+
case 'AsyncFunction':
|
|
60
|
+
evalled = await evalled;
|
|
61
|
+
break;
|
|
62
|
+
case 'Promise':
|
|
63
|
+
evalled = await evalled;
|
|
64
|
+
break;
|
|
65
|
+
case 'Function':
|
|
66
|
+
evalled = evalled();
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
resp = { 'result': evalled, 'event_id': req['event_id'] };
|
|
70
|
+
} catch (err) {
|
|
71
|
+
resp = { 'result': err.toString(), 'event_id': req['event_id'] };
|
|
72
|
+
}
|
|
73
|
+
return new TextEncoder().encode(JSON.stringify(resp));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
*
|
|
78
|
+
* @param {ArrayBuffer} d
|
|
79
|
+
* @returns {[Uint8Array | null, Object]}
|
|
80
|
+
*/
|
|
81
|
+
function parseData(d) {
|
|
82
|
+
var blob, jArray;
|
|
83
|
+
var dArray = new Uint8Array(d);
|
|
84
|
+
var nullIndex = dArray.indexOf(0);
|
|
85
|
+
if (nullIndex === -1) {
|
|
86
|
+
blob = null;
|
|
87
|
+
jArray = dArray;
|
|
88
|
+
} else {
|
|
89
|
+
blob = dArray.slice(nullIndex + 1);
|
|
90
|
+
jArray = dArray.slice(0, nullIndex)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return [blob, JSON.parse(new TextDecoder().decode(jArray))]
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function generateHostName() { return location.host };
|
|
97
|
+
var hostName;
|
|
98
|
+
|
|
99
|
+
function connect() {
|
|
100
|
+
var protocol = '3'
|
|
101
|
+
var ws = new WebSocket("ws://127.0.0.1:9404/ws");
|
|
102
|
+
ws.binaryType = "arraybuffer";
|
|
103
|
+
|
|
104
|
+
ws.onopen = async () => {
|
|
105
|
+
if (!hostName) {
|
|
106
|
+
hostName = await generateHostName();
|
|
107
|
+
}
|
|
108
|
+
ws.send(protocol + ' ' + hostName);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
ws.onclose = () => {
|
|
112
|
+
console.error('WebSocket was closed; will retry in 5 seconds');
|
|
113
|
+
setTimeout(connect, 5000);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
ws.onmessage = async (evt) => {
|
|
117
|
+
var /**@type {Uint8Array | Blob} */ result, j, b;
|
|
118
|
+
[b, j] = parseData(evt.data);
|
|
119
|
+
switch (j['action']) {
|
|
120
|
+
case 'fetch':
|
|
121
|
+
result = await doFetch(j, b);
|
|
122
|
+
break;
|
|
123
|
+
case 'eval':
|
|
124
|
+
result = await doEval(j);
|
|
125
|
+
break;
|
|
126
|
+
default:
|
|
127
|
+
result = new TextEncoder().encode(JSON.stringify({
|
|
128
|
+
'event_id': j['event_id'],
|
|
129
|
+
'error': `Action ${j['action']} is not defined.`
|
|
130
|
+
}));
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
ws.send(result);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// @ts-ignore
|
|
138
|
+
if (window.GM_registerMenuCommand) {
|
|
139
|
+
// @ts-ignore
|
|
140
|
+
GM_registerMenuCommand(
|
|
141
|
+
'connect to browserfetch',
|
|
142
|
+
connect
|
|
143
|
+
);
|
|
144
|
+
} else {
|
|
145
|
+
connect();
|
|
146
|
+
}
|
|
147
|
+
})();
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ['flit_core >=3.2,<4']
|
|
3
|
+
build-backend = 'flit_core.buildapi'
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "browserfetch"
|
|
7
|
+
description = "fetch in Python using your browser! "
|
|
8
|
+
keywords = ["browser", "fetch", "python", "cookies"]
|
|
9
|
+
classifiers = [
|
|
10
|
+
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
|
11
|
+
]
|
|
12
|
+
requires-python = ">=3.11"
|
|
13
|
+
dynamic = ["version"]
|
|
14
|
+
dependencies = [
|
|
15
|
+
"aiohttp",
|
|
16
|
+
"pyperclip",
|
|
17
|
+
"cyclopts",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[[authors]]
|
|
21
|
+
name = "5j9"
|
|
22
|
+
email = "5j9@users.noreply.github.com"
|
|
23
|
+
|
|
24
|
+
[project.license]
|
|
25
|
+
text = "GNU General Public License v3 (GPLv3)"
|
|
26
|
+
|
|
27
|
+
[project.readme]
|
|
28
|
+
file = "README.rst"
|
|
29
|
+
content-type = "text/x-rst"
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://github.com/5j9/browserfetch"
|
|
33
|
+
|
|
34
|
+
[tool.ruff]
|
|
35
|
+
line-length = 79
|
|
36
|
+
format.quote-style = 'single'
|
|
37
|
+
lint.isort.combine-as-imports = true
|
|
38
|
+
lint.extend-select = [
|
|
39
|
+
'W605', # invalid-escape-sequence
|
|
40
|
+
'FA', # flake8-future-annotations
|
|
41
|
+
'I', # isort
|
|
42
|
+
'UP', # pyupgrade
|
|
43
|
+
]
|
|
44
|
+
lint.ignore = [
|
|
45
|
+
'E721', # Do not compare types, use `isinstance()`
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
[tool.pytest.ini_options]
|
|
49
|
+
addopts = '--quiet --tb=short'
|
|
50
|
+
|
|
51
|
+
[tool.pyright]
|
|
52
|
+
typeCheckingMode = 'standard'
|
|
53
|
+
reportDeprecated = "warning"
|
|
54
|
+
reportPropertyTypeMismatch = "warning"
|
|
55
|
+
reportUnnecessaryCast = "warning"
|
|
56
|
+
reportUnnecessaryContains = "warning"
|
|
57
|
+
reportUnnecessaryIsInstance = "warning"
|
|
58
|
+
reportUnnecessaryTypeIgnoreComment = true
|
|
59
|
+
reportInvalidStringEscapeSequence = false
|
|
60
|
+
reportConstantRedefinition = 'error'
|
|
61
|
+
reportTypeCommentUsage = 'warning'
|
|
62
|
+
reportUnnecessaryComparison = 'warning'
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
|
|
4
|
-
from pyperclip import copy
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def main():
|
|
8
|
-
parser = ArgumentParser(
|
|
9
|
-
description='The command-line entry for browserfetch.',
|
|
10
|
-
formatter_class=ArgumentDefaultsHelpFormatter,
|
|
11
|
-
)
|
|
12
|
-
|
|
13
|
-
parser.add_argument(
|
|
14
|
-
'copyjs', help='copy contents of browserfetch.js to clipboard'
|
|
15
|
-
)
|
|
16
|
-
args = parser.parse_args()
|
|
17
|
-
|
|
18
|
-
if args.copyjs:
|
|
19
|
-
with (Path(__file__).parent / 'browserfetch.js').open() as f:
|
|
20
|
-
copy(f.read())
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if __name__ == "__main__":
|
|
24
|
-
main()
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
// ==UserScript==
|
|
2
|
-
// @name browserfetch
|
|
3
|
-
// @namespace https://github.com/5j9/browserfetch
|
|
4
|
-
// @match https://example.com/
|
|
5
|
-
// @grant GM_registerMenuCommand
|
|
6
|
-
// ==/UserScript==
|
|
7
|
-
(() => {
|
|
8
|
-
function connect() {
|
|
9
|
-
var ws = new WebSocket("ws://127.0.0.1:9404/ws");
|
|
10
|
-
ws.binaryType = "arraybuffer";
|
|
11
|
-
|
|
12
|
-
ws.onopen = () => {
|
|
13
|
-
ws.send(location.host);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
ws.onclose = function () {
|
|
17
|
-
console.error('browserfetch: WebSocket was closed; will retry in 5 seconds');
|
|
18
|
-
setTimeout(connect, 5000);
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
ws.onmessage = async (evt) => {
|
|
22
|
-
var returnData, responseBlob, body, jArray;
|
|
23
|
-
var requestArray = new Uint8Array(evt.data);
|
|
24
|
-
var nullIndex = requestArray.indexOf(0);
|
|
25
|
-
if (nullIndex === -1) {
|
|
26
|
-
body = null;
|
|
27
|
-
jArray = requestArray;
|
|
28
|
-
} else {
|
|
29
|
-
body = requestArray.slice(nullIndex + 1);
|
|
30
|
-
jArray = requestArray.slice(0, nullIndex)
|
|
31
|
-
}
|
|
32
|
-
var j = JSON.parse(new TextDecoder().decode(jArray));
|
|
33
|
-
var options = j['options'] || {};
|
|
34
|
-
|
|
35
|
-
if (j['timeout']) {
|
|
36
|
-
options.signal = AbortSignal.timeout(j['timeout'] * 1000);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
if (body !== null) {
|
|
40
|
-
options.body = body;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
try {
|
|
44
|
-
var r = await fetch(j['url'], options);
|
|
45
|
-
returnData = {
|
|
46
|
-
'event_id': j['event_id'],
|
|
47
|
-
'headers': Object.fromEntries([...r.headers]),
|
|
48
|
-
'ok': r.ok,
|
|
49
|
-
'redirected': r.redirected,
|
|
50
|
-
'status': r.status,
|
|
51
|
-
'status_text': r.statusText,
|
|
52
|
-
'type': r.type,
|
|
53
|
-
'url': r.url
|
|
54
|
-
};
|
|
55
|
-
responseBlob = await r.blob();
|
|
56
|
-
} catch (err) {
|
|
57
|
-
returnData = {
|
|
58
|
-
'event_id': j['event_id'],
|
|
59
|
-
'error': err.toString()
|
|
60
|
-
};
|
|
61
|
-
responseBlob = "";
|
|
62
|
-
};
|
|
63
|
-
ws.send(new Blob([new TextEncoder().encode(JSON.stringify(returnData)), "\0", responseBlob]));
|
|
64
|
-
}
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
if (window.GM_registerMenuCommand) {
|
|
68
|
-
GM_registerMenuCommand(
|
|
69
|
-
'connect to browserfetch',
|
|
70
|
-
connect
|
|
71
|
-
);
|
|
72
|
-
} else {
|
|
73
|
-
connect();
|
|
74
|
-
}
|
|
75
|
-
})();
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: browserfetch
|
|
3
|
-
Version: 0.6.2
|
|
4
|
-
Summary: fetch in Python using your browser!
|
|
5
|
-
License: GNU General Public License v3 (GPLv3)
|
|
6
|
-
Project-URL: Homepage, https://github.com/5j9/browserfetch
|
|
7
|
-
Keywords: browser,fetch,python,cookies
|
|
8
|
-
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
9
|
-
Requires-Python: >=3.11
|
|
10
|
-
Description-Content-Type: text/x-rst
|
|
11
|
-
License-File: LICENSE
|
|
12
|
-
Requires-Dist: aiohttp
|
|
13
|
-
Requires-Dist: pyperclip
|
|
14
|
-
|
|
15
|
-
Fetch using your browser.
|
|
16
|
-
|
|
17
|
-
Let the browser manage cookies for you.
|
|
18
|
-
|
|
19
|
-
⚠️ This project is a very simple implementation. Not tested thoroughly. Consider it a proof of concept.
|
|
20
|
-
|
|
21
|
-
Usage
|
|
22
|
-
-----
|
|
23
|
-
1. You'll run a Python script containing some code like this:
|
|
24
|
-
|
|
25
|
-
.. code-block:: python
|
|
26
|
-
|
|
27
|
-
from asyncio import gather, new_event_loop
|
|
28
|
-
|
|
29
|
-
from browserfetch import fetch, get, post, run_server
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
async def main():
|
|
33
|
-
response1, response2, reponse3 = await gather(
|
|
34
|
-
get('https://example.com/path1', params={'a': 1}),
|
|
35
|
-
fetch('https://example.com/image.png'),
|
|
36
|
-
post('https://example.com/path2', data={'a': 1}),
|
|
37
|
-
)
|
|
38
|
-
# do stuff with retrieved responses
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
loop = new_event_loop()
|
|
42
|
-
loop.create_task(start_server())
|
|
43
|
-
loop.run_until_complete(main())
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
2. Open your browser, goto http://example.com (perhaps solve a captcha and log in).
|
|
47
|
-
3. Copy the contents of `browserfetch.js`_ file and paste it in browser's console. (You can use a browser extensions like violentmonkey_/tampermonkey_ to do this step for you.)
|
|
48
|
-
|
|
49
|
-
That's it! Your Python script starts handling requests.
|
|
50
|
-
The browser tab should remain open of-coarse.
|
|
51
|
-
|
|
52
|
-
The server can handle multiple websocket connections from different websites simultaneously.
|
|
53
|
-
|
|
54
|
-
How it works
|
|
55
|
-
------------
|
|
56
|
-
``browserfetch`` communicates with your browser using a websocket. The ``fetch`` function just passes the request to browser and it is the browser that handles the actual request. Response data is sent back to Python using the same WebSocket connection.
|
|
57
|
-
|
|
58
|
-
Motivations
|
|
59
|
-
-----------
|
|
60
|
-
* `browser_cookie3 stopped working on Chrome-based browsers`_. There is a workaround: ShadowCopy, but it requires admin privilege.
|
|
61
|
-
* Another issue with browser_cookie's approach is that it retrieves cookies from cookie files, but these files are not updated instantly. Thus, you might have to wait or retry a few times before you can successfully access newly set cookies.
|
|
62
|
-
* ShadowCopying and File access are slow and inefficient operations.
|
|
63
|
-
|
|
64
|
-
Downsides
|
|
65
|
-
---------
|
|
66
|
-
* Setting up ``browserfetch`` is more cumbersome since it requires running a Python server and also injecting a small script into the webpage. Using ``browser_cookie3`` might be a better choice if there are many websites that you need to communicate with.
|
|
67
|
-
|
|
68
|
-
.. _`browser_cookie3 stopped working on Chrome-based browsers`: https://github.com/borisbabic/browser_cookie3/issues/180
|
|
69
|
-
.. _tampermonkey: https://github.com/Tampermonkey/tampermonkey
|
|
70
|
-
.. _violentmonkey: https://github.com/violentmonkey/violentmonkey
|
|
71
|
-
.. _browserfetch.js: https://github.com/5j9/browserfetch/blob/master/browserfetch/browserfetch.js
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
LICENSE
|
|
2
|
-
README.rst
|
|
3
|
-
pyproject.toml
|
|
4
|
-
browserfetch/__init__.py
|
|
5
|
-
browserfetch/__main__.py
|
|
6
|
-
browserfetch/browserfetch.js
|
|
7
|
-
browserfetch.egg-info/PKG-INFO
|
|
8
|
-
browserfetch.egg-info/SOURCES.txt
|
|
9
|
-
browserfetch.egg-info/dependency_links.txt
|
|
10
|
-
browserfetch.egg-info/not-zip-safe
|
|
11
|
-
browserfetch.egg-info/requires.txt
|
|
12
|
-
browserfetch.egg-info/top_level.txt
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
browserfetch
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
[build-system]
|
|
2
|
-
requires = [
|
|
3
|
-
"setuptools>=66.1.0",
|
|
4
|
-
"wheel",
|
|
5
|
-
]
|
|
6
|
-
build-backend = "setuptools.build_meta"
|
|
7
|
-
|
|
8
|
-
[tool.isort]
|
|
9
|
-
profile = "black"
|
|
10
|
-
line_length = 79
|
|
11
|
-
combine_as_imports = true
|
|
12
|
-
|
|
13
|
-
[project]
|
|
14
|
-
name = "browserfetch"
|
|
15
|
-
description = "fetch in Python using your browser! "
|
|
16
|
-
keywords = ["browser", "fetch", "python", "cookies"]
|
|
17
|
-
classifiers = [
|
|
18
|
-
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
|
19
|
-
]
|
|
20
|
-
requires-python = ">=3.11"
|
|
21
|
-
dynamic = ["version"]
|
|
22
|
-
dependencies = [
|
|
23
|
-
"aiohttp",
|
|
24
|
-
"pyperclip",
|
|
25
|
-
]
|
|
26
|
-
|
|
27
|
-
[[authors]]
|
|
28
|
-
name = "5j9"
|
|
29
|
-
email = "5j9@users.noreply.github.com"
|
|
30
|
-
|
|
31
|
-
[project.license]
|
|
32
|
-
text = "GNU General Public License v3 (GPLv3)"
|
|
33
|
-
|
|
34
|
-
[project.readme]
|
|
35
|
-
file = "README.rst"
|
|
36
|
-
content-type = "text/x-rst"
|
|
37
|
-
|
|
38
|
-
[project.urls]
|
|
39
|
-
Homepage = "https://github.com/5j9/browserfetch"
|
|
40
|
-
|
|
41
|
-
[tool.setuptools]
|
|
42
|
-
packages = ["browserfetch"]
|
|
43
|
-
zip-safe = false
|
|
44
|
-
include-package-data = false
|
|
45
|
-
|
|
46
|
-
[tool.setuptools.dynamic.version]
|
|
47
|
-
attr = "browserfetch.__version__"
|
|
48
|
-
|
|
49
|
-
[tool.setuptools.package-data]
|
|
50
|
-
browserfetch = ["*.js"]
|
browserfetch-0.6.2/setup.cfg
DELETED
|
File without changes
|