gpsoauth 2.0.0__tar.gz → 3.0.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.
@@ -1,22 +1,24 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: gpsoauth
3
- Version: 2.0.0
3
+ Version: 3.0.0
4
4
  Summary: A python client library for Google Play Services OAuth.
5
5
  License: MIT
6
+ License-File: LICENSE
6
7
  Author: Simon Weber
7
8
  Author-email: simon@simonmweber.com
8
- Requires-Python: >=3.9,<4.0
9
+ Requires-Python: >=3.10,<4.0
9
10
  Classifier: Development Status :: 5 - Production/Stable
10
11
  Classifier: Intended Audience :: Developers
11
12
  Classifier: License :: OSI Approved :: MIT License
12
13
  Classifier: Operating System :: OS Independent
13
14
  Classifier: Programming Language :: Python
14
15
  Classifier: Programming Language :: Python :: 3
15
- Classifier: Programming Language :: Python :: 3.9
16
16
  Classifier: Programming Language :: Python :: 3.10
17
17
  Classifier: Programming Language :: Python :: 3.11
18
18
  Classifier: Programming Language :: Python :: 3.12
19
19
  Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Programming Language :: Python :: 3.15
20
22
  Classifier: Typing :: Typed
21
23
  Requires-Dist: pycryptodomex (>=3.0)
22
24
  Requires-Dist: requests (>=2.0.0)
@@ -89,8 +91,40 @@ auth_response = gpsoauth.perform_oauth(
89
91
  token = auth_response['Auth']
90
92
  ```
91
93
 
94
+ ## Using another HTTP client
95
+
96
+ `perform_master_login()`, `exchange_token()` and `perform_oauth()` send their requests with [requests](https://requests.readthedocs.io/). To send them with another HTTP client, such as an async one, build the form fields with `build_master_login_data()`, `build_exchange_token_data()` or `build_oauth_data()`, which take the same arguments except `proxy`. For example, with [aiohttp](https://docs.aiohttp.org/):
97
+
98
+ ```python
99
+ from urllib.parse import urlencode
100
+
101
+ import aiohttp
102
+ import gpsoauth
103
+
104
+ # Loading the certificates blocks: create the context once, outside the event loop.
105
+ ssl_context = gpsoauth.create_ssl_context()
106
+
107
+ async def perform_oauth(email, master_token, android_id):
108
+ data = gpsoauth.build_oauth_data(
109
+ email, master_token, android_id,
110
+ service='sj', app='com.google.android.music',
111
+ client_sig='...')
112
+ async with aiohttp.ClientSession() as session, session.post(
113
+ gpsoauth.AUTH_URL,
114
+ data=urlencode(data).encode('ascii'),
115
+ headers=gpsoauth.build_auth_headers(),
116
+ ssl=ssl_context,
117
+ ) as response:
118
+ return gpsoauth.google.parse_auth_response(await response.text())
119
+ ```
120
+
121
+ These functions give you the form fields, the headers gpsoauth sets and its TLS settings. Your client still adds its own default headers, and handles proxies, timeouts and HTTP errors as it does for its other requests. It must also URL-encode the fields, as in the example: `build_master_login_data()` returns `EncryptedPasswd` as bytes, which aiohttp would otherwise send as a file upload.
122
+
123
+ `create_ssl_context()` checks the server's certificate and hostname, and trusts the system's certificate authorities. gpsoauth's own functions may also trust the CA bundle requests uses, depending on its version and settings: certifi's, or the file named by `REQUESTS_CA_BUNDLE` or `CURL_CA_BUNDLE`. So a certificate that gpsoauth's functions accept can fail with another client. To trust the same certificates, add that bundle with `ssl_context.load_verify_locations()`, for example `ssl_context.load_verify_locations(requests.certs.where())` for certifi's. This also blocks, so do it where you create the context, outside the event loop.
124
+
92
125
  ## Ports
93
126
 
127
+ - Typescript: <https://github.com/billgreenwald/gpsoauth-js>
94
128
  - C\#: <https://github.com/vemacs/GPSOAuthSharp>
95
129
  - Ruby: <https://github.com/bryanmytko/gpsoauth>
96
130
  - Java: <https://github.com/svarzee/gpsoauth-java>
@@ -62,8 +62,40 @@ auth_response = gpsoauth.perform_oauth(
62
62
  token = auth_response['Auth']
63
63
  ```
64
64
 
65
+ ## Using another HTTP client
66
+
67
+ `perform_master_login()`, `exchange_token()` and `perform_oauth()` send their requests with [requests](https://requests.readthedocs.io/). To send them with another HTTP client, such as an async one, build the form fields with `build_master_login_data()`, `build_exchange_token_data()` or `build_oauth_data()`, which take the same arguments except `proxy`. For example, with [aiohttp](https://docs.aiohttp.org/):
68
+
69
+ ```python
70
+ from urllib.parse import urlencode
71
+
72
+ import aiohttp
73
+ import gpsoauth
74
+
75
+ # Loading the certificates blocks: create the context once, outside the event loop.
76
+ ssl_context = gpsoauth.create_ssl_context()
77
+
78
+ async def perform_oauth(email, master_token, android_id):
79
+ data = gpsoauth.build_oauth_data(
80
+ email, master_token, android_id,
81
+ service='sj', app='com.google.android.music',
82
+ client_sig='...')
83
+ async with aiohttp.ClientSession() as session, session.post(
84
+ gpsoauth.AUTH_URL,
85
+ data=urlencode(data).encode('ascii'),
86
+ headers=gpsoauth.build_auth_headers(),
87
+ ssl=ssl_context,
88
+ ) as response:
89
+ return gpsoauth.google.parse_auth_response(await response.text())
90
+ ```
91
+
92
+ These functions give you the form fields, the headers gpsoauth sets and its TLS settings. Your client still adds its own default headers, and handles proxies, timeouts and HTTP errors as it does for its other requests. It must also URL-encode the fields, as in the example: `build_master_login_data()` returns `EncryptedPasswd` as bytes, which aiohttp would otherwise send as a file upload.
93
+
94
+ `create_ssl_context()` checks the server's certificate and hostname, and trusts the system's certificate authorities. gpsoauth's own functions may also trust the CA bundle requests uses, depending on its version and settings: certifi's, or the file named by `REQUESTS_CA_BUNDLE` or `CURL_CA_BUNDLE`. So a certificate that gpsoauth's functions accept can fail with another client. To trust the same certificates, add that bundle with `ssl_context.load_verify_locations()`, for example `ssl_context.load_verify_locations(requests.certs.where())` for certifi's. This also blocks, so do it where you create the context, outside the event loop.
95
+
65
96
  ## Ports
66
97
 
98
+ - Typescript: <https://github.com/billgreenwald/gpsoauth-js>
67
99
  - C\#: <https://github.com/vemacs/GPSOAuthSharp>
68
100
  - Ruby: <https://github.com/bryanmytko/gpsoauth>
69
101
  - Java: <https://github.com/svarzee/gpsoauth-java>
@@ -62,24 +62,61 @@ class SSLContext(ssl.SSLContext):
62
62
  """
63
63
 
64
64
 
65
+ def create_ssl_context() -> ssl.SSLContext:
66
+ """
67
+ Return a new SSL context with the TLS settings gpsoauth uses for AUTH_URL.
68
+
69
+ It is built as the one gpsoauth's own requests use: SSLContext() without a
70
+ protocol argument, with urllib3's ciphers if urllib3 < 2. It requires a
71
+ valid certificate for the server's hostname and trusts the system's
72
+ certificate authorities. Unlike urllib3's default context, it allows TLS
73
+ session tickets, and unlike urllib3's and aiohttp's, it offers no ALPN
74
+ (its set_alpn_protocols() does nothing): Google returns 403 Bad
75
+ Authentication if tickets are disabled or ALPN is offered.
76
+
77
+ gpsoauth's own functions may also trust the CA bundle requests uses,
78
+ depending on its version and settings: certifi's, or the file named by
79
+ REQUESTS_CA_BUNDLE or CURL_CA_BUNDLE. To trust the same certificates, add
80
+ that bundle with the context's load_verify_locations().
81
+
82
+ Loading certificates blocks, so in async code create the context once,
83
+ outside the event loop, and add any bundle there too.
84
+ """
85
+ context = SSLContext()
86
+ if SSL_DEFAULT_CIPHERS:
87
+ context.set_ciphers(SSL_DEFAULT_CIPHERS)
88
+ context.verify_mode = ssl.CERT_REQUIRED
89
+ context.check_hostname = True
90
+ context.options &= ~ssl.OP_NO_TICKET # pylint: disable=E1101
91
+ context.load_default_certs()
92
+ return context
93
+
94
+
65
95
  class AuthHTTPAdapter(requests.adapters.HTTPAdapter):
66
96
  """TLS tweaks."""
67
97
 
68
98
  def init_poolmanager(self, *args: Any, **kwargs: Any) -> None:
69
- """
70
- Secure settings from ssl.create_default_context(), but without
71
- ssl.OP_NO_TICKET which causes Google to return 403 Bad
72
- Authentication.
73
- """
74
- context = SSLContext()
75
- if SSL_DEFAULT_CIPHERS:
76
- context.set_ciphers(SSL_DEFAULT_CIPHERS)
77
- context.verify_mode = ssl.CERT_REQUIRED
78
- context.options &= ~ssl.OP_NO_TICKET # pylint: disable=E1101
79
- context.load_default_certs()
99
+ """Use the TLS settings of create_ssl_context()."""
100
+ context = create_ssl_context()
101
+ # Leave the hostname check to urllib3, as gpsoauth always has: its
102
+ # matching differs from the ssl module's, e.g. for partial wildcards.
103
+ context.check_hostname = False
80
104
  self.poolmanager = PoolManager(*args, ssl_context=context, **kwargs)
81
105
 
82
106
 
107
+ def build_auth_headers() -> dict[str, str]:
108
+ """
109
+ Return the headers gpsoauth sets on its requests to AUTH_URL.
110
+
111
+ HTTP clients add their own default headers to these, such as Accept.
112
+ """
113
+ return {
114
+ "Accept-Encoding": "identity",
115
+ "Content-type": "application/x-www-form-urlencoded",
116
+ "User-Agent": USER_AGENT,
117
+ }
118
+
119
+
83
120
  def _perform_auth_request(
84
121
  data: dict[str, int | str | bytes], proxies: MutableMapping[str, str] | None = None
85
122
  ) -> dict[str, str]:
@@ -87,19 +124,52 @@ def _perform_auth_request(
87
124
  session.mount(AUTH_URL, AuthHTTPAdapter())
88
125
  if proxies:
89
126
  session.proxies = proxies
90
- session.headers.update(
91
- {
92
- "Accept-Encoding": "identity",
93
- "Content-type": "application/x-www-form-urlencoded",
94
- "User-Agent": USER_AGENT,
95
- }
96
- )
127
+ session.headers.update(build_auth_headers())
97
128
 
98
129
  res = session.post(AUTH_URL, data=data, verify=True)
99
130
 
100
131
  return google.parse_auth_response(res.text)
101
132
 
102
133
 
134
+ def build_master_login_data(
135
+ email: str,
136
+ password: str,
137
+ android_id: str,
138
+ service: str = "ac2dm",
139
+ device_country: str = "us",
140
+ operator_country: str = "us",
141
+ lang: str = "en",
142
+ sdk_version: int = 17,
143
+ client_sig: str = "38918a453d07199354f8b19af05ec6562ced5788",
144
+ ) -> dict[str, int | str | bytes]:
145
+ """
146
+ Return the form fields perform_master_login() posts to AUTH_URL, as a new dict.
147
+
148
+ It takes the same arguments, except proxy. EncryptedPasswd differs on each
149
+ call, as the encryption is randomized.
150
+ """
151
+ return {
152
+ "accountType": "HOSTED_OR_GOOGLE",
153
+ "Email": email,
154
+ "has_permission": 1,
155
+ "add_account": 1,
156
+ "EncryptedPasswd": google.construct_signature(
157
+ email, password, ANDROID_KEY_7_3_29
158
+ ),
159
+ "service": service,
160
+ "source": "android",
161
+ "androidId": android_id,
162
+ "device_country": device_country,
163
+ "operatorCountry": operator_country,
164
+ "lang": lang,
165
+ "sdk_version": sdk_version,
166
+ "google_play_services_version": 240913000,
167
+ "client_sig": client_sig,
168
+ "callerSig": client_sig,
169
+ "droidguard_results": "dummy123",
170
+ }
171
+
172
+
103
173
  def perform_master_login(
104
174
  email: str,
105
175
  password: str,
@@ -132,14 +202,44 @@ def perform_master_login(
132
202
  }
133
203
  """
134
204
 
135
- data: dict[str, int | str | bytes] = {
205
+ data = build_master_login_data(
206
+ email,
207
+ password,
208
+ android_id,
209
+ service=service,
210
+ device_country=device_country,
211
+ operator_country=operator_country,
212
+ lang=lang,
213
+ sdk_version=sdk_version,
214
+ client_sig=client_sig,
215
+ )
216
+
217
+ return _perform_auth_request(data, proxy)
218
+
219
+
220
+ def build_exchange_token_data(
221
+ email: str,
222
+ token: str,
223
+ android_id: str,
224
+ service: str = "ac2dm",
225
+ device_country: str = "us",
226
+ operator_country: str = "us",
227
+ lang: str = "en",
228
+ sdk_version: int = 17,
229
+ client_sig: str = "38918a453d07199354f8b19af05ec6562ced5788",
230
+ ) -> dict[str, int | str | bytes]:
231
+ """
232
+ Return the form fields exchange_token() posts to AUTH_URL, as a new dict.
233
+
234
+ It takes the same arguments, except proxy.
235
+ """
236
+ return {
136
237
  "accountType": "HOSTED_OR_GOOGLE",
137
238
  "Email": email,
138
239
  "has_permission": 1,
139
240
  "add_account": 1,
140
- "EncryptedPasswd": google.construct_signature(
141
- email, password, ANDROID_KEY_7_3_29
142
- ),
241
+ "ACCESS_TOKEN": 1,
242
+ "Token": token,
143
243
  "service": service,
144
244
  "source": "android",
145
245
  "androidId": android_id,
@@ -153,8 +253,6 @@ def perform_master_login(
153
253
  "droidguard_results": "dummy123",
154
254
  }
155
255
 
156
- return _perform_auth_request(data, proxy)
157
-
158
256
 
159
257
  def exchange_token(
160
258
  email: str,
@@ -188,28 +286,55 @@ def exchange_token(
188
286
  }
189
287
  """
190
288
 
191
- data: dict[str, int | str | bytes] = {
289
+ data = build_exchange_token_data(
290
+ email,
291
+ token,
292
+ android_id,
293
+ service=service,
294
+ device_country=device_country,
295
+ operator_country=operator_country,
296
+ lang=lang,
297
+ sdk_version=sdk_version,
298
+ client_sig=client_sig,
299
+ )
300
+
301
+ return _perform_auth_request(data, proxy)
302
+
303
+
304
+ def build_oauth_data(
305
+ email: str,
306
+ master_token: str,
307
+ android_id: str,
308
+ service: str,
309
+ app: str,
310
+ client_sig: str,
311
+ device_country: str = "us",
312
+ operator_country: str = "us",
313
+ lang: str = "en",
314
+ sdk_version: int = 17,
315
+ ) -> dict[str, int | str | bytes]:
316
+ """
317
+ Return the form fields perform_oauth() posts to AUTH_URL, as a new dict.
318
+
319
+ It takes the same arguments, except proxy.
320
+ """
321
+ return {
192
322
  "accountType": "HOSTED_OR_GOOGLE",
193
323
  "Email": email,
194
324
  "has_permission": 1,
195
- "add_account": 1,
196
- "ACCESS_TOKEN": 1,
197
- "Token": token,
325
+ "EncryptedPasswd": master_token,
198
326
  "service": service,
199
327
  "source": "android",
200
328
  "androidId": android_id,
329
+ "app": app,
330
+ "client_sig": client_sig,
201
331
  "device_country": device_country,
202
332
  "operatorCountry": operator_country,
203
333
  "lang": lang,
204
334
  "sdk_version": sdk_version,
205
335
  "google_play_services_version": 240913000,
206
- "client_sig": client_sig,
207
- "callerSig": client_sig,
208
- "droidguard_results": "dummy123",
209
336
  }
210
337
 
211
- return _perform_auth_request(data, proxy)
212
-
213
338
 
214
339
  def perform_oauth(
215
340
  email: str,
@@ -241,21 +366,17 @@ def perform_oauth(
241
366
  ``Authorization: GoogleLogin auth=res['Auth']``.
242
367
  """
243
368
 
244
- data: dict[str, int | str | bytes] = {
245
- "accountType": "HOSTED_OR_GOOGLE",
246
- "Email": email,
247
- "has_permission": 1,
248
- "EncryptedPasswd": master_token,
249
- "service": service,
250
- "source": "android",
251
- "androidId": android_id,
252
- "app": app,
253
- "client_sig": client_sig,
254
- "device_country": device_country,
255
- "operatorCountry": operator_country,
256
- "lang": lang,
257
- "sdk_version": sdk_version,
258
- "google_play_services_version": 240913000,
259
- }
369
+ data = build_oauth_data(
370
+ email,
371
+ master_token,
372
+ android_id,
373
+ service=service,
374
+ app=app,
375
+ client_sig=client_sig,
376
+ device_country=device_country,
377
+ operator_country=operator_country,
378
+ lang=lang,
379
+ sdk_version=sdk_version,
380
+ )
260
381
 
261
382
  return _perform_auth_request(data, proxy)
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "gpsoauth"
3
- version = "2.0.0"
3
+ version = "3.0.0"
4
4
  description = "A python client library for Google Play Services OAuth."
5
5
  authors = ["Simon Weber <simon@simonmweber.com>"]
6
6
  license = "MIT"
@@ -11,11 +11,11 @@ classifiers = [
11
11
  "Operating System :: OS Independent",
12
12
  "Programming Language :: Python",
13
13
  "Programming Language :: Python :: 3",
14
- "Programming Language :: Python :: 3.9",
15
14
  "Programming Language :: Python :: 3.10",
16
15
  "Programming Language :: Python :: 3.11",
17
16
  "Programming Language :: Python :: 3.12",
18
17
  "Programming Language :: Python :: 3.13",
18
+ "Programming Language :: Python :: 3.14",
19
19
  "Typing :: Typed",
20
20
  ]
21
21
  readme = "README.md"
@@ -24,7 +24,7 @@ repository = "https://github.com/simon-weber/gpsoauth"
24
24
  include = ["gpsoauth/py.typed"]
25
25
 
26
26
  [tool.poetry.dependencies]
27
- python = "^3.9"
27
+ python = "^3.10"
28
28
  pycryptodomex = ">= 3.0"
29
29
  requests = ">= 2.0.0"
30
30
  urllib3 = ">= 1.26.0"
@@ -33,7 +33,7 @@ urllib3 = ">= 1.26.0"
33
33
  pre-commit = "^2.11.1"
34
34
  black = "^22.3.0"
35
35
  isort = "^5.8.0"
36
- pytest = "^6.2.2"
36
+ pytest = "^8.4"
37
37
  flake8 = "^7.0"
38
38
  flake8-bugbear = ">= 21.4.1"
39
39
  flake8-comprehensions = ">= 3.4.0"
File without changes
File without changes
File without changes
File without changes