ps5-updates 0.9.0__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.
@@ -0,0 +1,23 @@
1
+ https://github.com/andshrew/PS4-Updates-Python/blob/v1.1.0/LICENSE
2
+
3
+ MIT License
4
+
5
+ Copyright (c) 2023, 2025 andshrew
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in all
15
+ copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ SOFTWARE.
@@ -0,0 +1,22 @@
1
+ # MIT License
2
+
3
+ # Copyright (c) 2025 andshrew
4
+ # https://github.com/andshrew/PS5-Updates-Python
5
+
6
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ # of this software and associated documentation files (the "Software"), to deal
8
+ # in the Software without restriction, including without limitation the rights
9
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ # copies of the Software, and to permit persons to whom the Software is
11
+ # furnished to do so, subject to the following conditions:
12
+
13
+ # The above copyright notice and this permission notice shall be included in all
14
+ # copies or substantial portions of the Software.
15
+
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ # SOFTWARE.
ps5_updates/data.py ADDED
@@ -0,0 +1,240 @@
1
+ # MIT License
2
+
3
+ # Copyright (c) 2026 andshrew
4
+ # https://github.com/andshrew/PS5-Updates-Python
5
+
6
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ # of this software and associated documentation files (the "Software"), to deal
8
+ # in the Software without restriction, including without limitation the rights
9
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ # copies of the Software, and to permit persons to whom the Software is
11
+ # furnished to do so, subject to the following conditions:
12
+
13
+ # The above copyright notice and this permission notice shall be included in all
14
+ # copies or substantial portions of the Software.
15
+
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ # SOFTWARE.
23
+
24
+ import logging
25
+ import socket
26
+ import ssl
27
+ from urllib.parse import urlparse, ParseResult
28
+ from dataclasses import dataclass
29
+ from typing import Optional, Union
30
+
31
+ @dataclass
32
+ class HTTPSocket:
33
+ """A socket based HTTP downloader
34
+
35
+ Intended for partially downloading a file from a web server using a
36
+ manually created HTTP/1.1 request.
37
+
38
+ Warning: HTTPS server validation is disabled to enable downloading
39
+ from servers using certificates issued from non-public CAs.
40
+
41
+ Attributes:
42
+ port: Remote server port
43
+ url: URL as a ParseResult of the file to download
44
+ timeout: Socket timeout in seconds
45
+ """
46
+ port: int
47
+ url: ParseResult
48
+ timeout: Optional[int] = 60
49
+ connection: Optional[socket.socket] = None
50
+ no_more_data: Optional[bool] = False
51
+ _is_connected: Optional[bool] = False
52
+
53
+ def __post_init__(self):
54
+ self.connection = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
55
+ self.connection.settimeout(self.timeout)
56
+
57
+ def __enter__(self):
58
+ self.connect()
59
+ return self
60
+
61
+ def __exit__(self, exc_type, exc_val, exc_tb):
62
+ self.close()
63
+ return False
64
+
65
+ @classmethod
66
+ def from_url(cls, url: Union[str, ParseResult], port: int=0, timeout: int=30) -> 'HTTPSocket':
67
+ """Creates an instance from a URL
68
+
69
+ Attributes:
70
+ url: URL as a String or ParseResult of the file to download
71
+ port: Specific remote server port
72
+ timeout: Socket timeout in seconds
73
+ """
74
+ if isinstance(url, str):
75
+ url = urlparse(url)
76
+ if not isinstance(url, ParseResult):
77
+ raise TypeError('Url should be of type ParseResult')
78
+ if url.scheme != 'http' and url.scheme != 'https':
79
+ raise ValueError('Url should be HTTP or HTTPS')
80
+ if url.port is not None:
81
+ port = url.port
82
+ if url.port is None and port == 0:
83
+ if url.scheme == 'http':
84
+ port = 80
85
+ if url.scheme == 'https':
86
+ port = 443
87
+
88
+ return cls(url=url, port=port, timeout=timeout)
89
+
90
+ def connect(self):
91
+ """Initiates connection to the remote server
92
+ """
93
+ if self._is_connected:
94
+ logger.debug(f'This socket is already connected. Close the socket and create a new one ' +
95
+ f' before calling connect again')
96
+ return
97
+ self._is_connected = False
98
+
99
+ try:
100
+ self.connection.connect((self.url.hostname, self.port))
101
+ except socket.timeout as ex:
102
+ logger.error(f'Socket timeout connecting to: {self.url.hostname} port {self.port}')
103
+ return
104
+
105
+ if self.url.scheme == "https":
106
+ # ssl.CERT_NONE will disable validating server cert (ie. no requirement for issuing CA to be in our trusted CAs)
107
+ # When HTTPS is used by the update servers they are typically using certificates issued by an internal CA, so
108
+ # this validation would fail.
109
+ # Ref: https://docs.python.org/3/library/ssl.html
110
+ context = ssl.create_default_context()
111
+ context.check_hostname = False
112
+ context.verify_mode = ssl.CERT_NONE
113
+ try:
114
+ self.connection = context.wrap_socket(self.connection, server_hostname=self.url.hostname)
115
+ except Exception as ex:
116
+ logger.error(f'Socket error {self.url.hostname} port {self.port}: {ex.args}')
117
+ self._is_connected = False
118
+ return
119
+ self._is_connected = True
120
+
121
+ def close(self):
122
+ """Closes the socket
123
+
124
+ If a new socket is to subsequently be opened using this object then first create a
125
+ new socket by calling _recreate_socket
126
+ """
127
+ if self._is_connected:
128
+ try:
129
+ self.connection.close()
130
+ except Exception as ex:
131
+ logger.error(f'Error when closing socket: {self.url.hostname} port {self.port}: {ex.args}')
132
+ self._is_connected = False
133
+
134
+ def initial_receive(self, magic: bytes, byte_limit: int=16384, redirect_count: int=0,
135
+ redirect_limit: int=5) -> 'bytes':
136
+ """Starts downloading a file
137
+
138
+ Creates the initial HTTP request and handles CDN-type redirects
139
+ If magic bytes are found in the response the response bytes are returned aligned to the
140
+ location of the magic (ie. the initial response is discarded)
141
+
142
+ Attributes:
143
+ magic: Bytes of the files 'magic' signature
144
+ byte_limit: Maximum number of bytes that will be downloaded for finding the files signature
145
+ redirect_limit: If the connection is redirected to another server this is the maximum number of times it will be followed
146
+ """
147
+ url = self.url
148
+ request = f'GET {url.path} HTTP/1.1\r\nHost:{url.hostname}\r\nConnection: close\r\n\r\n'
149
+ self.connection.send(request.encode())
150
+
151
+ received_data = b''
152
+ while len(received_data) < byte_limit:
153
+ response = self.receive(4096)
154
+ if magic in response:
155
+ break
156
+ if magic not in response:
157
+ # Sometimes downloads are redirected to a specific CDN URL
158
+ if any(s in response[0:100].decode() for s in ('302 Moved Temporarily', '302 Found') ):
159
+ logger.debug(f'302 redirect response received, current redirect count: {redirect_count}')
160
+ if redirect_count > redirect_limit:
161
+ self.close()
162
+ logger.error(f'Too many 302 redirect responses have been received, aborting')
163
+ break
164
+ # Parse the responses HTTP headers to find the redirect location
165
+ response_headers = response.decode().splitlines()
166
+ for i, header in enumerate(response_headers):
167
+ if "Location: " in header:
168
+ redirect_url = urlparse(header.replace("Location: ", ""))
169
+ logger.debug(f'Trying again with URL: {redirect_url.geturl()}')
170
+ # Close and recreate the socket, and then call
171
+ # this method agian using the redirected URL
172
+ self.close()
173
+ redirect_count = redirect_count + 1
174
+ self.url = redirect_url
175
+ # Validate if the port has changed
176
+ if redirect_url.port is None:
177
+ if url.scheme == 'http':
178
+ self.port = 80
179
+ if url.scheme == 'https':
180
+ self.port = 443
181
+ else:
182
+ # New URL is using a non-standard port
183
+ self.port = url.port
184
+ self._recreate_socket()
185
+ self.connect()
186
+ return self.initial_receive(magic=magic, byte_limit=byte_limit,
187
+ redirect_count=redirect_count, redirect_limit=redirect_limit)
188
+ if self.no_more_data:
189
+ break
190
+
191
+ if magic not in response:
192
+ # File signaure not found within receive byte limit
193
+ logger.error(f'File magic NOT found and the byte limit {byte_limit} has been reached - actual bytes downloaded {len(response)}')
194
+ response = b''
195
+ self.connection.close()
196
+
197
+ if magic in response:
198
+ # File signaure has been found
199
+ # Discard bytes prior to the location of the magic
200
+ magic_offset = response.find(magic)
201
+ response = response[magic_offset:]
202
+
203
+ return response
204
+
205
+ def receive(self, buffer: int=4096, length: int=0) -> 'bytes':
206
+ """Requests bytes from the remote server
207
+
208
+ Attributes:
209
+ buffer: The number of bytes to receive in each request
210
+ length: The maximum number of bytes to receive
211
+ """
212
+ received_data = b''
213
+ if length == 0:
214
+ length = buffer
215
+ if length < buffer:
216
+ buffer = length
217
+ if self.no_more_data is False:
218
+ while len(received_data) <= length:
219
+ response = self.connection.recv(buffer)
220
+ if len(response) == 0:
221
+ # No more data received in the request
222
+ self.no_more_data = True
223
+ break
224
+ received_data = received_data + response
225
+ return received_data
226
+
227
+ def _recreate_socket(self):
228
+ """
229
+ Internal function for recreating the underlying socket
230
+ """
231
+ self.close()
232
+ self.no_more_data = False
233
+ self._is_connected = False
234
+ self.connection = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
235
+ self.connection.settimeout(self.timeout)
236
+
237
+ logger = logging.getLogger(__name__)
238
+
239
+ if __name__ == "__main__":
240
+ print('https://github.com/andshrew/PS5-Updates-Python')