carconnectivity-connector-seatcupra 0.1a1__py3-none-any.whl
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.
- 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)
|