carconnectivity-connector-seatcupra 0.1a1__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- carconnectivity_connector_seatcupra-0.1a1.dist-info/LICENSE +21 -0
- carconnectivity_connector_seatcupra-0.1a1.dist-info/METADATA +124 -0
- carconnectivity_connector_seatcupra-0.1a1.dist-info/RECORD +17 -0
- carconnectivity_connector_seatcupra-0.1a1.dist-info/WHEEL +5 -0
- carconnectivity_connector_seatcupra-0.1a1.dist-info/top_level.txt +1 -0
- carconnectivity_connectors/seatcupra/__init__.py +0 -0
- carconnectivity_connectors/seatcupra/_version.py +21 -0
- carconnectivity_connectors/seatcupra/auth/__init__.py +0 -0
- carconnectivity_connectors/seatcupra/auth/auth_util.py +141 -0
- carconnectivity_connectors/seatcupra/auth/helpers/blacklist_retry.py +29 -0
- carconnectivity_connectors/seatcupra/auth/my_cupra_session.py +244 -0
- carconnectivity_connectors/seatcupra/auth/openid_session.py +440 -0
- carconnectivity_connectors/seatcupra/auth/session_manager.py +150 -0
- carconnectivity_connectors/seatcupra/auth/vw_web_session.py +239 -0
- carconnectivity_connectors/seatcupra/charging.py +74 -0
- carconnectivity_connectors/seatcupra/connector.py +686 -0
- carconnectivity_connectors/seatcupra/ui/connector_ui.py +39 -0
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2021 Till Steinbach
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
@@ -0,0 +1,124 @@
|
|
1
|
+
Metadata-Version: 2.2
|
2
|
+
Name: carconnectivity-connector-seatcupra
|
3
|
+
Version: 0.1a1
|
4
|
+
Summary: CarConnectivity connector for Seat and Cupra services
|
5
|
+
Author: Till Steinbach
|
6
|
+
License: MIT License
|
7
|
+
|
8
|
+
Copyright (c) 2021 Till Steinbach
|
9
|
+
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
12
|
+
in the Software without restriction, including without limitation the rights
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
15
|
+
furnished to do so, subject to the following conditions:
|
16
|
+
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
18
|
+
copies or substantial portions of the Software.
|
19
|
+
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
26
|
+
SOFTWARE.
|
27
|
+
|
28
|
+
Classifier: Development Status :: 3 - Alpha
|
29
|
+
Classifier: License :: OSI Approved :: MIT License
|
30
|
+
Classifier: Intended Audience :: Developers
|
31
|
+
Classifier: Programming Language :: Python :: 3.9
|
32
|
+
Classifier: Programming Language :: Python :: 3.10
|
33
|
+
Classifier: Programming Language :: Python :: 3.11
|
34
|
+
Classifier: Programming Language :: Python :: 3.12
|
35
|
+
Classifier: Programming Language :: Python :: 3.13
|
36
|
+
Classifier: Topic :: Software Development :: Libraries
|
37
|
+
Requires-Python: >=3.9
|
38
|
+
Description-Content-Type: text/markdown
|
39
|
+
License-File: LICENSE
|
40
|
+
Requires-Dist: carconnectivity>=0.3
|
41
|
+
Requires-Dist: oauthlib~=3.2.2
|
42
|
+
Requires-Dist: requests~=2.32.3
|
43
|
+
Requires-Dist: jwt~=1.3.1
|
44
|
+
|
45
|
+
|
46
|
+
|
47
|
+
# CarConnectivity Connector for Seat and Cupra Vehicles
|
48
|
+
[](https://github.com/tillsteinbach/CarConnectivity-connector-seatcupra/)
|
49
|
+
[](https://github.com/tillsteinbach/CarConnectivity-connector-seatcupra/releases/latest)
|
50
|
+
[](https://github.com/tillsteinbach/CarConnectivity-connector-seatcupra/blob/master/LICENSE)
|
51
|
+
[](https://github.com/tillsteinbach/CarConnectivity-connector-seatcupra/issues)
|
52
|
+
[](https://pypi.org/project/carconnectivity-connector-seatcupra/)
|
53
|
+
[](https://pypi.org/project/carconnectivity-connector-seatcupra/)
|
54
|
+
[](https://www.paypal.com/donate?hosted_button_id=2BVFF5GJ9SXAJ)
|
55
|
+
[](https://github.com/sponsors/tillsteinbach)
|
56
|
+
|
57
|
+
|
58
|
+
## Due to lack of access to a Cupra car the development of this conenctor is currently stuck. If you want to help me with access to your account, please contact me!
|
59
|
+
|
60
|
+
[CarConnectivity](https://github.com/tillsteinbach/CarConnectivity) is a python API to connect to various car services. This connector enables the integration of seat and cupra vehicles through the MyCupra API. Look at [CarConnectivity](https://github.com/tillsteinbach/CarConnectivity) for other supported brands.
|
61
|
+
|
62
|
+
## Configuration
|
63
|
+
In your carconnectivity.json configuration add a section for the seatcupra connector like this:
|
64
|
+
```
|
65
|
+
{
|
66
|
+
"carConnectivity": {
|
67
|
+
"connectors": [
|
68
|
+
{
|
69
|
+
"type": "seatcupra",
|
70
|
+
"config": {
|
71
|
+
"username": "test@test.de",
|
72
|
+
"password": "testpassword123"
|
73
|
+
}
|
74
|
+
}
|
75
|
+
]
|
76
|
+
}
|
77
|
+
}
|
78
|
+
```
|
79
|
+
### Credentials
|
80
|
+
If you do not want to provide your username or password inside the configuration you have to create a ".netrc" file at the appropriate location (usually this is your home folder):
|
81
|
+
```
|
82
|
+
# For MyCupra
|
83
|
+
machine seatcupra
|
84
|
+
login test@test.de
|
85
|
+
password testpassword123
|
86
|
+
```
|
87
|
+
In this case the configuration needs to look like this:
|
88
|
+
```
|
89
|
+
{
|
90
|
+
"carConnectivity": {
|
91
|
+
"connectors": [
|
92
|
+
{
|
93
|
+
"type": "seatcupra",
|
94
|
+
"config": {
|
95
|
+
}
|
96
|
+
}
|
97
|
+
]
|
98
|
+
}
|
99
|
+
}
|
100
|
+
```
|
101
|
+
|
102
|
+
You can also provide the location of the netrc file in the configuration.
|
103
|
+
```
|
104
|
+
{
|
105
|
+
"carConnectivity": {
|
106
|
+
"connectors": [
|
107
|
+
{
|
108
|
+
"type": "seatcupra",
|
109
|
+
"config": {
|
110
|
+
"netrc": "/some/path/on/your/filesystem"
|
111
|
+
}
|
112
|
+
}
|
113
|
+
]
|
114
|
+
}
|
115
|
+
}
|
116
|
+
```
|
117
|
+
The optional S-PIN needed for some commands can be provided in the account section of the netrc:
|
118
|
+
```
|
119
|
+
# For MyCupra
|
120
|
+
machine seatcupra
|
121
|
+
login test@test.de
|
122
|
+
password testpassword123
|
123
|
+
account 1234
|
124
|
+
```
|
@@ -0,0 +1,17 @@
|
|
1
|
+
carconnectivity_connectors/seatcupra/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
+
carconnectivity_connectors/seatcupra/_version.py,sha256=_BIT6nGqXqfHgeHw9NwtWXgZnV8T_7fSo6lEqHbnf88,508
|
3
|
+
carconnectivity_connectors/seatcupra/charging.py,sha256=kcCJJddZxUXFoayYMpq3lzdnrPp5yexnGBfDB-zQrmE,3336
|
4
|
+
carconnectivity_connectors/seatcupra/connector.py,sha256=nyfIjft7pXPfQojU93BfNdXQ-fSmU7DFh9Xqw4ujfd4,44278
|
5
|
+
carconnectivity_connectors/seatcupra/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
6
|
+
carconnectivity_connectors/seatcupra/auth/auth_util.py,sha256=Y81h8fGOMSMgPtE4wI_TI9WgE_s43uaPjRLBBINhj4g,4433
|
7
|
+
carconnectivity_connectors/seatcupra/auth/my_cupra_session.py,sha256=iK5SlankZqaeneC3SNad8nHHcrP0tTmYxToI_9cqwlo,10744
|
8
|
+
carconnectivity_connectors/seatcupra/auth/openid_session.py,sha256=dA0vE2YuckkMPeqJo2dEI0h8_XfohdCgdGkTyshPF7Q,16858
|
9
|
+
carconnectivity_connectors/seatcupra/auth/session_manager.py,sha256=NizIuY-pvkVBSwqYwFHKtjTU_02Nj4vMgjD_FjqCY6c,5377
|
10
|
+
carconnectivity_connectors/seatcupra/auth/vw_web_session.py,sha256=hgsCdXugVnSgvLta4hBNtoNgMhAA83paAYO2fUOOFyM,10657
|
11
|
+
carconnectivity_connectors/seatcupra/auth/helpers/blacklist_retry.py,sha256=f3wsiY5bpHDBxp7Va1Mv9nKJ4u3qnCHZZmDu78_AhMk,1251
|
12
|
+
carconnectivity_connectors/seatcupra/ui/connector_ui.py,sha256=SNYnlcGJpbWhuLiIHD2l6H9IfSiMz3IgmvXsdossDnE,1412
|
13
|
+
carconnectivity_connector_seatcupra-0.1a1.dist-info/LICENSE,sha256=PIwI1alwDyOfvEQHdGCm2u9uf_mGE8030xZDfun0xTo,1071
|
14
|
+
carconnectivity_connector_seatcupra-0.1a1.dist-info/METADATA,sha256=eG-9qfRLgK8bseYbWLxo_brIPtu02IrgXjW5ZiimCc0,5384
|
15
|
+
carconnectivity_connector_seatcupra-0.1a1.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
16
|
+
carconnectivity_connector_seatcupra-0.1a1.dist-info/top_level.txt,sha256=KqA8GviZsDH4PtmnwSQsz0HB_w-TWkeEHLIRNo5dTaI,27
|
17
|
+
carconnectivity_connector_seatcupra-0.1a1.dist-info/RECORD,,
|
@@ -0,0 +1 @@
|
|
1
|
+
carconnectivity_connectors
|
File without changes
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# file generated by setuptools-scm
|
2
|
+
# don't change, don't track in version control
|
3
|
+
|
4
|
+
__all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
|
5
|
+
|
6
|
+
TYPE_CHECKING = False
|
7
|
+
if TYPE_CHECKING:
|
8
|
+
from typing import Tuple
|
9
|
+
from typing import Union
|
10
|
+
|
11
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
12
|
+
else:
|
13
|
+
VERSION_TUPLE = object
|
14
|
+
|
15
|
+
version: str
|
16
|
+
__version__: str
|
17
|
+
__version_tuple__: VERSION_TUPLE
|
18
|
+
version_tuple: VERSION_TUPLE
|
19
|
+
|
20
|
+
__version__ = version = '0.1a1'
|
21
|
+
__version_tuple__ = version_tuple = (0, 1)
|
File without changes
|
@@ -0,0 +1,141 @@
|
|
1
|
+
|
2
|
+
"""
|
3
|
+
This module provides utility functions and classes for handling authentication and parsing HTML forms
|
4
|
+
and scripts for the seatcupra car connectivity connector.
|
5
|
+
"""
|
6
|
+
from __future__ import annotations
|
7
|
+
from typing import TYPE_CHECKING
|
8
|
+
|
9
|
+
import json
|
10
|
+
import re
|
11
|
+
from html.parser import HTMLParser
|
12
|
+
|
13
|
+
if TYPE_CHECKING:
|
14
|
+
from typing import Optional, Dict
|
15
|
+
|
16
|
+
|
17
|
+
def add_bearer_auth_header(token, headers: Optional[Dict[str, str]] = None) -> Dict[str, str]:
|
18
|
+
"""
|
19
|
+
Adds a Bearer token to the Authorization header.
|
20
|
+
|
21
|
+
Args:
|
22
|
+
token (str): The Bearer token to be added to the headers.
|
23
|
+
headers (Optional[Dict[str, str]]): An optional dictionary of headers to which the Authorization header will be added.
|
24
|
+
If not provided, a new dictionary will be created.
|
25
|
+
|
26
|
+
Returns:
|
27
|
+
Dict[str, str]: The headers dictionary with the added Authorization header.
|
28
|
+
"""
|
29
|
+
headers = headers or {}
|
30
|
+
headers['Authorization'] = f'Bearer {token}'
|
31
|
+
return headers
|
32
|
+
|
33
|
+
|
34
|
+
class HTMLFormParser(HTMLParser):
|
35
|
+
"""
|
36
|
+
A custom HTML parser to extract form data from HTML content.
|
37
|
+
"""
|
38
|
+
def __init__(self, form_id) -> None:
|
39
|
+
super().__init__()
|
40
|
+
self._form_id = form_id
|
41
|
+
self._inside_form: bool = False
|
42
|
+
self.target = None
|
43
|
+
self.data = {}
|
44
|
+
|
45
|
+
def _get_attr(self, attrs, name):
|
46
|
+
for attr in attrs:
|
47
|
+
if attr[0] == name:
|
48
|
+
return attr[1]
|
49
|
+
return None
|
50
|
+
|
51
|
+
def handle_starttag(self, tag, attrs) -> None:
|
52
|
+
if self._inside_form and tag == 'input':
|
53
|
+
self.handle_input(attrs)
|
54
|
+
return
|
55
|
+
|
56
|
+
if tag == 'form' and self._get_attr(attrs, 'id') == self._form_id:
|
57
|
+
self._inside_form = True
|
58
|
+
self.target = self._get_attr(attrs, 'action')
|
59
|
+
|
60
|
+
def handle_endtag(self, tag) -> None:
|
61
|
+
if tag == 'form' and self._inside_form:
|
62
|
+
self._inside_form = False
|
63
|
+
|
64
|
+
def handle_input(self, attrs) -> None:
|
65
|
+
if not self._inside_form:
|
66
|
+
return
|
67
|
+
|
68
|
+
name = self._get_attr(attrs, 'name')
|
69
|
+
value = self._get_attr(attrs, 'value')
|
70
|
+
|
71
|
+
if name:
|
72
|
+
self.data[name] = value
|
73
|
+
|
74
|
+
|
75
|
+
class ScriptFormParser(HTMLParser):
|
76
|
+
fields: list[str] = []
|
77
|
+
targetField: str = ''
|
78
|
+
|
79
|
+
def __init__(self):
|
80
|
+
super().__init__()
|
81
|
+
self._inside_script = False
|
82
|
+
self.data = {}
|
83
|
+
self.target = None
|
84
|
+
|
85
|
+
def handle_starttag(self, tag, attrs) -> None:
|
86
|
+
if not self._inside_script and tag == 'script':
|
87
|
+
self._inside_script = True
|
88
|
+
|
89
|
+
def handle_endtag(self, tag) -> None:
|
90
|
+
if self._inside_script and tag == 'script':
|
91
|
+
self._inside_script = False
|
92
|
+
|
93
|
+
def handle_data(self, data) -> None:
|
94
|
+
if not self._inside_script:
|
95
|
+
return
|
96
|
+
|
97
|
+
match: re.Match[str] | None = re.search(r'templateModel: (.*?),\n', data)
|
98
|
+
if not match:
|
99
|
+
return
|
100
|
+
|
101
|
+
result = json.loads(match.group(1))
|
102
|
+
self.target = result.get(self.targetField, None)
|
103
|
+
self.data = {k: v for k, v in result.items() if k in self.fields}
|
104
|
+
|
105
|
+
match2 = re.search(r'csrf_token: \'(.*?)\'', data)
|
106
|
+
if match2:
|
107
|
+
self.data['_csrf'] = match2.group(1)
|
108
|
+
|
109
|
+
|
110
|
+
class CredentialsFormParser(ScriptFormParser):
|
111
|
+
fields: list[str] = ['relayState', 'hmac', 'registerCredentialsPath', 'error', 'errorCode']
|
112
|
+
targetField: str = 'postAction'
|
113
|
+
|
114
|
+
|
115
|
+
class TermsAndConditionsFormParser(ScriptFormParser):
|
116
|
+
fields: list[str] = ['relayState', 'hmac', 'countryOfResidence', 'legalDocuments']
|
117
|
+
targetField: str = 'loginUrl'
|
118
|
+
|
119
|
+
def handle_data(self, data) -> None:
|
120
|
+
if not self._inside_script:
|
121
|
+
return
|
122
|
+
|
123
|
+
super().handle_data(data)
|
124
|
+
|
125
|
+
if 'countryOfResidence' in self.data:
|
126
|
+
self.data['countryOfResidence'] = self.data['countryOfResidence'].upper()
|
127
|
+
|
128
|
+
if 'legalDocuments' not in self.data:
|
129
|
+
return
|
130
|
+
|
131
|
+
for key in self.data['legalDocuments'][0]:
|
132
|
+
# Skip unnecessary keys
|
133
|
+
if key in ('skipLink', 'declineLink', 'majorVersion', 'minorVersion', 'changeSummary'):
|
134
|
+
continue
|
135
|
+
|
136
|
+
# Move values under a new key while converting boolean values to 'yes' or 'no'
|
137
|
+
v = self.data['legalDocuments'][0][key]
|
138
|
+
self.data[f'legalDocuments[0].{key}'] = ('yes' if v else 'no') if isinstance(v, bool) else v
|
139
|
+
|
140
|
+
# Remove the original object
|
141
|
+
del self.data['legalDocuments']
|
@@ -0,0 +1,29 @@
|
|
1
|
+
"""Implements a custom Retry class that allows for blacklisting certain status codes that will not retry."""
|
2
|
+
from urllib3.util.retry import Retry
|
3
|
+
|
4
|
+
|
5
|
+
class BlacklistRetry(Retry):
|
6
|
+
"""
|
7
|
+
BlacklistRetry class extends the Retry class to include a blacklist of status codes that should not be retried.
|
8
|
+
"""
|
9
|
+
def __init__(self, status_blacklist=None, **kwargs) -> None:
|
10
|
+
self.status_blacklist = status_blacklist
|
11
|
+
super().__init__(**kwargs)
|
12
|
+
|
13
|
+
def is_retry(self, method, status_code, has_retry_after=False) -> bool:
|
14
|
+
"""
|
15
|
+
Determines if a request should be retried based on the HTTP method, status code,
|
16
|
+
and the presence of a 'Retry-After' header.
|
17
|
+
|
18
|
+
Args:
|
19
|
+
method (str): The HTTP method of the request (e.g., 'GET', 'POST').
|
20
|
+
status_code (int): The HTTP status code of the response.
|
21
|
+
has_retry_after (bool): Indicates if the response contains a 'Retry-After' header.
|
22
|
+
|
23
|
+
Returns:
|
24
|
+
bool: True if the request should be retried, False otherwise.
|
25
|
+
"""
|
26
|
+
if self.status_blacklist is not None and status_code in self.status_blacklist:
|
27
|
+
return False
|
28
|
+
else:
|
29
|
+
return super().is_retry(method, status_code, has_retry_after)
|
@@ -0,0 +1,244 @@
|
|
1
|
+
"""
|
2
|
+
Module implements the MyCupra Session handling.
|
3
|
+
"""
|
4
|
+
from __future__ import annotations
|
5
|
+
from typing import TYPE_CHECKING
|
6
|
+
|
7
|
+
import json
|
8
|
+
import logging
|
9
|
+
import secrets
|
10
|
+
|
11
|
+
from urllib.parse import parse_qsl, urlparse
|
12
|
+
|
13
|
+
import requests
|
14
|
+
from requests.models import CaseInsensitiveDict
|
15
|
+
|
16
|
+
from oauthlib.common import add_params_to_uri, generate_nonce, to_unicode
|
17
|
+
from oauthlib.oauth2 import InsecureTransportError
|
18
|
+
from oauthlib.oauth2 import is_secure_transport
|
19
|
+
|
20
|
+
from carconnectivity.errors import AuthenticationError, RetrievalError, TemporaryAuthenticationError
|
21
|
+
|
22
|
+
from carconnectivity_connectors.seatcupra.auth.openid_session import AccessType
|
23
|
+
from carconnectivity_connectors.seatcupra.auth.vw_web_session import VWWebSession
|
24
|
+
|
25
|
+
if TYPE_CHECKING:
|
26
|
+
from typing import Tuple, Dict
|
27
|
+
|
28
|
+
|
29
|
+
LOG: logging.Logger = logging.getLogger("carconnectivity.connectors.seatcupra.auth")
|
30
|
+
|
31
|
+
|
32
|
+
class MyCupraSession(VWWebSession):
|
33
|
+
"""
|
34
|
+
MyCupraSession class handles the authentication and session management for Cupras's MyCupra service.
|
35
|
+
"""
|
36
|
+
def __init__(self, session_user, **kwargs) -> None:
|
37
|
+
super(MyCupraSession, self).__init__(client_id='3c756d46-f1ba-4d78-9f9a-cff0d5292d51@apps_vw-dilab_com',
|
38
|
+
refresh_url='https://identity.vwgroup.io/oidc/v1/token',
|
39
|
+
scope='openid profile nickname birthdate phone',
|
40
|
+
redirect_uri='cupra://oauth-callback',
|
41
|
+
state=None,
|
42
|
+
session_user=session_user,
|
43
|
+
**kwargs)
|
44
|
+
|
45
|
+
self.headers = CaseInsensitiveDict({
|
46
|
+
'accept': '*/*',
|
47
|
+
'content-type': 'application/json',
|
48
|
+
'user-agent': 'CUPRAApp%20-%20Store/20220503 CFNetwork/1333.0.4 Darwin/21.5.0',
|
49
|
+
'accept-language': 'de-de',
|
50
|
+
'accept-encoding': 'gzip, deflate, br'
|
51
|
+
})
|
52
|
+
|
53
|
+
def login(self):
|
54
|
+
super(MyCupraSession, self).login()
|
55
|
+
# retrieve authorization URL
|
56
|
+
authorization_url_str: str = self.authorization_url(url='https://identity.vwgroup.io/oidc/v1/authorize')
|
57
|
+
# perform web authentication
|
58
|
+
response = self.do_web_auth(authorization_url_str)
|
59
|
+
# fetch tokens from web authentication response
|
60
|
+
self.fetch_tokens('https://identity.vwgroup.io/oidc/v1/token',
|
61
|
+
authorization_response=response)
|
62
|
+
|
63
|
+
def refresh(self) -> None:
|
64
|
+
# refresh tokens from refresh endpoint
|
65
|
+
self.refresh_tokens(
|
66
|
+
'https://identity.vwgroup.io/oidc/v1/token',
|
67
|
+
)
|
68
|
+
|
69
|
+
def fetch_tokens(
|
70
|
+
self,
|
71
|
+
token_url,
|
72
|
+
authorization_response=None,
|
73
|
+
**_
|
74
|
+
):
|
75
|
+
"""
|
76
|
+
Fetches tokens using the given token URL using the tokens from authorization response.
|
77
|
+
|
78
|
+
Args:
|
79
|
+
token_url (str): The URL to request the tokens from.
|
80
|
+
authorization_response (str, optional): The authorization response containing the tokens. Defaults to None.
|
81
|
+
**_ : Additional keyword arguments.
|
82
|
+
|
83
|
+
Returns:
|
84
|
+
dict: A dictionary containing the fetched tokens if successful.
|
85
|
+
None: If the tokens could not be fetched.
|
86
|
+
|
87
|
+
Raises:
|
88
|
+
TemporaryAuthenticationError: If the token request fails due to a temporary MyCupra failure.
|
89
|
+
"""
|
90
|
+
# take token from authorization response (those are stored in self.token now!)
|
91
|
+
self.parse_from_fragment(authorization_response)
|
92
|
+
|
93
|
+
if self.token is not None and all(key in self.token for key in ('state', 'id_token', 'access_token', 'code')):
|
94
|
+
# Generate json body for token request
|
95
|
+
body: Dict[str, str] = {
|
96
|
+
'state': self.token['state'],
|
97
|
+
'id_token': self.token['id_token'],
|
98
|
+
'redirect_uri': self.redirect_uri,
|
99
|
+
'client_id': self.client_id,
|
100
|
+
'client_secret': 'eb8814e641c81a2640ad62eeccec11c98effc9bccd4269ab7af338b50a94b3a2',
|
101
|
+
'code': self.token['code'],
|
102
|
+
'grant_type': 'authorization_code'
|
103
|
+
}
|
104
|
+
|
105
|
+
request_headers: CaseInsensitiveDict = dict(self.headers) # pyright: ignore reportAssignmentType
|
106
|
+
request_headers['content-type'] = 'application/x-www-form-urlencoded; charset=utf-8'
|
107
|
+
|
108
|
+
# request tokens from token_url
|
109
|
+
token_response = self.post(token_url, headers=request_headers, data=body, allow_redirects=False,
|
110
|
+
access_type=AccessType.NONE) # pyright: ignore reportCallIssue
|
111
|
+
if token_response.status_code != requests.codes['ok']:
|
112
|
+
raise TemporaryAuthenticationError(f'Token could not be fetched due to temporary MyCupra failure: {token_response.status_code}')
|
113
|
+
# parse token from response body
|
114
|
+
token = self.parse_from_body(token_response.text)
|
115
|
+
|
116
|
+
return token
|
117
|
+
return None
|
118
|
+
|
119
|
+
def parse_from_body(self, token_response, state=None):
|
120
|
+
"""
|
121
|
+
Fix strange token naming before parsing it with OAuthlib.
|
122
|
+
"""
|
123
|
+
try:
|
124
|
+
# Tokens are in body of response in json format
|
125
|
+
token = json.loads(token_response)
|
126
|
+
except json.decoder.JSONDecodeError as err:
|
127
|
+
raise TemporaryAuthenticationError('Token could not be refreshed due to temporary MyCupra failure: json could not be decoded') from err
|
128
|
+
# Fix token keys, we want access_token instead of accessToken
|
129
|
+
if 'accessToken' in token:
|
130
|
+
token['access_token'] = token.pop('accessToken')
|
131
|
+
# Fix token keys, we want id_token instead of idToken
|
132
|
+
if 'idToken' in token:
|
133
|
+
token['id_token'] = token.pop('idToken')
|
134
|
+
# Fix token keys, we want refresh_token instead of refreshToken
|
135
|
+
if 'refreshToken' in token:
|
136
|
+
token['refresh_token'] = token.pop('refreshToken')
|
137
|
+
# generate json from fixed dict
|
138
|
+
fixed_token_response = to_unicode(json.dumps(token)).encode("utf-8")
|
139
|
+
# Let OAuthlib parse the token
|
140
|
+
return super(MyCupraSession, self).parse_from_body(token_response=fixed_token_response, state=state)
|
141
|
+
|
142
|
+
def refresh_tokens(
|
143
|
+
self,
|
144
|
+
token_url,
|
145
|
+
refresh_token=None,
|
146
|
+
auth=None,
|
147
|
+
timeout=None,
|
148
|
+
headers=None,
|
149
|
+
verify=True,
|
150
|
+
proxies=None,
|
151
|
+
**_
|
152
|
+
):
|
153
|
+
"""
|
154
|
+
Refreshes the authentication tokens using the provided refresh token.
|
155
|
+
Args:
|
156
|
+
token_url (str): The URL to request new tokens from.
|
157
|
+
refresh_token (str, optional): The refresh token to use. Defaults to None.
|
158
|
+
auth (tuple, optional): Authentication credentials. Defaults to None.
|
159
|
+
timeout (float or tuple, optional): How long to wait for the server to send data before giving up. Defaults to None.
|
160
|
+
headers (dict, optional): Headers to include in the request. Defaults to None.
|
161
|
+
verify (bool, optional): Whether to verify the server's TLS certificate. Defaults to True.
|
162
|
+
proxies (dict, optional): Proxies to use for the request. Defaults to None.
|
163
|
+
**_ (dict): Additional arguments.
|
164
|
+
Raises:
|
165
|
+
ValueError: If no token endpoint is set for auto_refresh.
|
166
|
+
InsecureTransportError: If the token URL is not secure.
|
167
|
+
AuthenticationError: If the server requests new authorization.
|
168
|
+
TemporaryAuthenticationError: If the token could not be refreshed due to a temporary server failure.
|
169
|
+
RetrievalError: If the status code from the server is not recognized.
|
170
|
+
Returns:
|
171
|
+
dict: The new tokens.
|
172
|
+
"""
|
173
|
+
LOG.info('Refreshing tokens')
|
174
|
+
if not token_url:
|
175
|
+
raise ValueError("No token endpoint set for auto_refresh.")
|
176
|
+
|
177
|
+
if not is_secure_transport(token_url):
|
178
|
+
raise InsecureTransportError()
|
179
|
+
|
180
|
+
# Store old refresh token in case no new one is given
|
181
|
+
refresh_token = refresh_token or self.refresh_token
|
182
|
+
if refresh_token is None:
|
183
|
+
self.login()
|
184
|
+
return self.token
|
185
|
+
|
186
|
+
if headers is None:
|
187
|
+
headers = dict(self.headers)
|
188
|
+
|
189
|
+
body: Dict[str, str] = {
|
190
|
+
'client_id': self.client_id,
|
191
|
+
'client_secret': 'eb8814e641c81a2640ad62eeccec11c98effc9bccd4269ab7af338b50a94b3a2',
|
192
|
+
'grant_type': 'refresh_token',
|
193
|
+
'refresh_token': self.refresh_token
|
194
|
+
}
|
195
|
+
|
196
|
+
headers['content-type'] = 'application/x-www-form-urlencoded; charset=utf-8'
|
197
|
+
|
198
|
+
# Request new tokens using the refresh token
|
199
|
+
token_response = self.post(
|
200
|
+
token_url,
|
201
|
+
data=body,
|
202
|
+
auth=auth,
|
203
|
+
timeout=timeout,
|
204
|
+
headers=headers,
|
205
|
+
verify=verify,
|
206
|
+
withhold_token=False, # pyright: ignore reportCallIssue
|
207
|
+
proxies=proxies,
|
208
|
+
access_type=AccessType.NONE # pyright: ignore reportCallIssue
|
209
|
+
)
|
210
|
+
if token_response.status_code == requests.codes['unauthorized']:
|
211
|
+
raise AuthenticationError('Refreshing tokens failed: Server requests new authorization')
|
212
|
+
elif token_response.status_code in (requests.codes['internal_server_error'], requests.codes['service_unavailable'], requests.codes['gateway_timeout']):
|
213
|
+
raise TemporaryAuthenticationError('Token could not be refreshed due to temporary MyCupra failure: {tokenResponse.status_code}')
|
214
|
+
elif token_response.status_code == requests.codes['ok']:
|
215
|
+
# parse new tokens from response
|
216
|
+
self.parse_from_body(token_response.text)
|
217
|
+
if self.token is not None and "refresh_token" not in self.token:
|
218
|
+
LOG.debug("No new refresh token given. Re-using old.")
|
219
|
+
self.token["refresh_token"] = refresh_token
|
220
|
+
return self.token
|
221
|
+
else:
|
222
|
+
raise RetrievalError(f'Status Code from MyCupra while refreshing tokens was: {token_response.status_code}')
|
223
|
+
|
224
|
+
def request(
|
225
|
+
self,
|
226
|
+
method,
|
227
|
+
url,
|
228
|
+
data=None,
|
229
|
+
headers=None,
|
230
|
+
withhold_token=False,
|
231
|
+
access_type=AccessType.ACCESS,
|
232
|
+
token=None,
|
233
|
+
timeout=None,
|
234
|
+
**kwargs
|
235
|
+
):
|
236
|
+
"""Intercept all requests and add userId if present."""
|
237
|
+
if not is_secure_transport(url):
|
238
|
+
raise InsecureTransportError()
|
239
|
+
if self.user_id is not None:
|
240
|
+
headers = headers or {}
|
241
|
+
headers['user-id'] = self.user_id
|
242
|
+
|
243
|
+
return super(MyCupraSession, self).request(method, url, headers=headers, data=data, withhold_token=withhold_token, access_type=access_type, token=token,
|
244
|
+
timeout=timeout, **kwargs)
|