wsbuilder 0.1.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.
wsbuilder/__init__.py ADDED
@@ -0,0 +1,27 @@
1
+ """wsbuilder: mini framework HTTP + WebSocket extraido de PortHound4."""
2
+
3
+ from .framework import (
4
+ App,
5
+ HTTPServer,
6
+ Request,
7
+ Response,
8
+ Route,
9
+ Router,
10
+ WebSocket,
11
+ parse_close_payload,
12
+ parse_query_string,
13
+ )
14
+
15
+ __version__ = "0.1.0"
16
+
17
+ __all__ = [
18
+ "App",
19
+ "HTTPServer",
20
+ "Request",
21
+ "Response",
22
+ "Route",
23
+ "Router",
24
+ "WebSocket",
25
+ "parse_close_payload",
26
+ "parse_query_string",
27
+ ]
wsbuilder/__main__.py ADDED
@@ -0,0 +1,17 @@
1
+ import argparse
2
+
3
+ from .ws_demo import HOST, PORT, run_server
4
+
5
+
6
+ def main():
7
+ parser = argparse.ArgumentParser(
8
+ description="Levanta el demo HTTP + WebSocket incluido en wsbuilder"
9
+ )
10
+ parser.add_argument("--host", default=HOST, help="Host de escucha")
11
+ parser.add_argument("--port", type=int, default=PORT, help="Puerto de escucha")
12
+ args = parser.parse_args()
13
+ run_server(args.host, args.port)
14
+
15
+
16
+ if __name__ == "__main__":
17
+ main()
wsbuilder/framework.py ADDED
@@ -0,0 +1,662 @@
1
+ import json
2
+ import os
3
+ import socket
4
+ import ssl
5
+ from concurrent.futures import ThreadPoolExecutor
6
+
7
+ MAGIC_WS = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
8
+ DEFAULT_CORS_ALLOW_ORIGIN = str(
9
+ os.getenv("WSBUILDER_CORS_ALLOW_ORIGIN", os.getenv("PORTHOUND_CORS_ALLOW_ORIGIN", ""))
10
+ ).strip()
11
+
12
+ STATUS_MESSAGES = {
13
+ 100: "Continue",
14
+ 101: "Switching Protocols",
15
+ 102: "Processing",
16
+ 103: "Early Hints",
17
+ 200: "OK",
18
+ 201: "Created",
19
+ 202: "Accepted",
20
+ 203: "Non-Authoritative Information",
21
+ 204: "No Content",
22
+ 205: "Reset Content",
23
+ 206: "Partial Content",
24
+ 207: "Multi-Status",
25
+ 208: "Already Reported",
26
+ 226: "IM Used",
27
+ 300: "Multiple Choices",
28
+ 301: "Moved Permanently",
29
+ 302: "Found",
30
+ 303: "See Other",
31
+ 304: "Not Modified",
32
+ 305: "Use Proxy",
33
+ 307: "Temporary Redirect",
34
+ 308: "Permanent Redirect",
35
+ 400: "Bad Request",
36
+ 401: "Unauthorized",
37
+ 402: "Payment Required",
38
+ 403: "Forbidden",
39
+ 404: "Not Found",
40
+ 405: "Method Not Allowed",
41
+ 406: "Not Acceptable",
42
+ 407: "Proxy Authentication Required",
43
+ 408: "Request Timeout",
44
+ 409: "Conflict",
45
+ 410: "Gone",
46
+ 411: "Length Required",
47
+ 412: "Precondition Failed",
48
+ 413: "Payload Too Large",
49
+ 414: "URI Too Long",
50
+ 415: "Unsupported Media Type",
51
+ 416: "Range Not Satisfiable",
52
+ 417: "Expectation Failed",
53
+ 418: "I'm a teapot",
54
+ 421: "Misdirected Request",
55
+ 422: "Unprocessable Entity",
56
+ 423: "Locked",
57
+ 424: "Failed Dependency",
58
+ 425: "Too Early",
59
+ 426: "Upgrade Required",
60
+ 428: "Precondition Required",
61
+ 429: "Too Many Requests",
62
+ 431: "Request Header Fields Too Large",
63
+ 451: "Unavailable For Legal Reasons",
64
+ 500: "Internal Server Error",
65
+ 501: "Not Implemented",
66
+ 502: "Bad Gateway",
67
+ 503: "Service Unavailable",
68
+ 504: "Gateway Timeout",
69
+ 505: "HTTP Version Not Supported",
70
+ 506: "Variant Also Negotiates",
71
+ 507: "Insufficient Storage",
72
+ 508: "Loop Detected",
73
+ 510: "Not Extended",
74
+ 511: "Network Authentication Required",
75
+ }
76
+
77
+
78
+ def parse_query_string(qs):
79
+ params = {}
80
+ if not qs:
81
+ return params
82
+ parts = qs.split("&")
83
+ for part in parts:
84
+ if not part:
85
+ continue
86
+ if "=" in part:
87
+ k, v = part.split("=", 1)
88
+ else:
89
+ k, v = part, ""
90
+ params[k] = v
91
+ return params
92
+
93
+
94
+ class Request:
95
+ def __init__(self, method, path, query_string, headers, body, client, tls=None):
96
+ self.method = (method or "").upper()
97
+ self.path = path or "/"
98
+ self.query_string = query_string or ""
99
+ self.query = parse_query_string(self.query_string)
100
+ self.headers = headers or {}
101
+ self.body = body or b""
102
+ self.client = client
103
+ self.tls = tls or {}
104
+
105
+ def text(self, encoding="utf-8"):
106
+ return self.body.decode(encoding, errors="ignore")
107
+
108
+ def json(self):
109
+ try:
110
+ return json.loads(self.text())
111
+ except Exception:
112
+ return None
113
+
114
+
115
+ class Response:
116
+ def __init__(self, status=200, body=b"", headers=None, reason=None):
117
+ self.status = status
118
+ self.reason = reason
119
+ if isinstance(body, (bytes, bytearray)):
120
+ self.body = bytes(body)
121
+ else:
122
+ self.body = str(body).encode("utf-8")
123
+ self.headers = headers or {}
124
+
125
+ @classmethod
126
+ def json(cls, data, status=200, headers=None):
127
+ body = json.dumps(data).encode("utf-8")
128
+ hdrs = {"Content-Type": "application/json; charset=utf-8"}
129
+ if headers:
130
+ hdrs.update(headers)
131
+ return cls(status=status, body=body, headers=hdrs)
132
+
133
+ @classmethod
134
+ def text(cls, text, status=200, headers=None):
135
+ hdrs = {"Content-Type": "text/plain; charset=utf-8"}
136
+ if headers:
137
+ hdrs.update(headers)
138
+ return cls(status=status, body=text, headers=hdrs)
139
+
140
+ @classmethod
141
+ def html(cls, html, status=200, headers=None):
142
+ hdrs = {"Content-Type": "text/html; charset=utf-8"}
143
+ if headers:
144
+ hdrs.update(headers)
145
+ return cls(status=status, body=html, headers=hdrs)
146
+
147
+
148
+ class Route:
149
+ def __init__(self, path, methods, handler, kind):
150
+ self.path = path
151
+ self.methods = {m.upper() for m in methods}
152
+ self.handler = handler
153
+ self.kind = kind
154
+
155
+
156
+ class Router:
157
+ def __init__(self):
158
+ self.routes = []
159
+
160
+ def add(self, route):
161
+ self.routes.append(route)
162
+
163
+ def resolve(self, path, method=None):
164
+ for route in self.routes:
165
+ if route.path != path:
166
+ continue
167
+ if method is None or method.upper() in route.methods:
168
+ return route
169
+ return None
170
+
171
+
172
+ class App:
173
+ def __init__(self):
174
+ self.router = Router()
175
+ self.ws_routes = {}
176
+ self.startup_hooks = []
177
+
178
+ def route(self, path, methods=("GET",), kind="plain"):
179
+ def decorator(func):
180
+ self.router.add(Route(path, methods, func, kind))
181
+ return func
182
+ return decorator
183
+
184
+ def view(self, path, methods=("GET",)):
185
+ return self.route(path, methods=methods, kind="plain")
186
+
187
+ def api(self, path, methods=("GET",)):
188
+ return self.route(path, methods=methods, kind="api")
189
+
190
+ def ws(self, path):
191
+ def decorator(func):
192
+ self.ws_routes[path] = func
193
+ return func
194
+ return decorator
195
+
196
+ def add_startup(self, func):
197
+ self.startup_hooks.append(func)
198
+
199
+ def dispatch(self, request):
200
+ if request.method == "OPTIONS":
201
+ route = self.router.resolve(request.path, method=None)
202
+ if route:
203
+ allow = sorted(route.methods | {"OPTIONS"})
204
+ headers = {
205
+ "Access-Control-Allow-Methods": ", ".join(allow),
206
+ "Access-Control-Allow-Headers": "Content-Type, Authorization, X-API-Key",
207
+ }
208
+ if DEFAULT_CORS_ALLOW_ORIGIN:
209
+ headers["Access-Control-Allow-Origin"] = DEFAULT_CORS_ALLOW_ORIGIN
210
+ if DEFAULT_CORS_ALLOW_ORIGIN != "*":
211
+ headers["Vary"] = "Origin"
212
+ return Response(status=200, body=b"", headers=headers)
213
+
214
+ route = self.router.resolve(request.path, request.method)
215
+ if not route:
216
+ return Response.text("Not Found", status=404)
217
+
218
+ try:
219
+ result = route.handler(request)
220
+ except Exception as e:
221
+ print(f"[http] handler error {request.method} {request.path}: {e}")
222
+ if route.kind == "api":
223
+ headers = {}
224
+ if DEFAULT_CORS_ALLOW_ORIGIN:
225
+ headers["Access-Control-Allow-Origin"] = DEFAULT_CORS_ALLOW_ORIGIN
226
+ if DEFAULT_CORS_ALLOW_ORIGIN != "*":
227
+ headers["Vary"] = "Origin"
228
+ return Response.json(
229
+ {"status": "error", "message": "Internal Server Error"},
230
+ status=500,
231
+ headers=headers,
232
+ )
233
+ return Response.text("Internal Server Error", status=500)
234
+
235
+ if isinstance(result, Response):
236
+ resp = result
237
+ elif route.kind == "api" and isinstance(result, (dict, list)):
238
+ resp = Response.json(result)
239
+ else:
240
+ resp = Response.text("" if result is None else str(result))
241
+
242
+ if route.kind == "api":
243
+ if DEFAULT_CORS_ALLOW_ORIGIN:
244
+ resp.headers.setdefault("Access-Control-Allow-Origin", DEFAULT_CORS_ALLOW_ORIGIN)
245
+ if DEFAULT_CORS_ALLOW_ORIGIN != "*":
246
+ resp.headers.setdefault("Vary", "Origin")
247
+
248
+ return resp
249
+
250
+ def run(self, host, port, ssl_context=None):
251
+ server = HTTPServer(host, port, self, ssl_context=ssl_context)
252
+ server.serve_forever()
253
+
254
+
255
+ class HTTPServer:
256
+ MAX_CONNECTION_WORKERS = 64
257
+ MAX_REQUEST_HEADER_BYTES = 64 * 1024
258
+ MAX_REQUEST_BODY_BYTES = 2 * 1024 * 1024
259
+
260
+ def __init__(self, host, port, app, ssl_context=None):
261
+ self.host = host
262
+ self.port = port
263
+ self.app = app
264
+ self._sock = None
265
+ self.ssl_context = ssl_context
266
+
267
+ def serve_forever(self):
268
+ for hook in self.app.startup_hooks:
269
+ try:
270
+ hook()
271
+ except Exception as e:
272
+ print(f"[startup] error: {e}")
273
+
274
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
275
+ s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
276
+ s.bind((self.host, self.port))
277
+ s.listen(128)
278
+ self._sock = s
279
+ scheme = "https" if self.ssl_context else "http"
280
+ print(f"Server listening on {scheme}://{self.host}:{self.port}/")
281
+ interrupted = False
282
+ try:
283
+ with ThreadPoolExecutor(
284
+ max_workers=self.MAX_CONNECTION_WORKERS,
285
+ thread_name_prefix="framework-http",
286
+ ) as pool:
287
+ while True:
288
+ conn, addr = s.accept()
289
+ pool.submit(self.handle_conn, conn, addr)
290
+ except KeyboardInterrupt:
291
+ interrupted = True
292
+ print("\n[shutdown] interrupted by user (Ctrl+C). stopping server...")
293
+ finally:
294
+ s.close()
295
+ if interrupted:
296
+ print("[shutdown] server stopped.")
297
+
298
+ def handle_conn(self, conn, addr):
299
+ tls_meta = {
300
+ "enabled": bool(self.ssl_context),
301
+ "peer_cert": None,
302
+ "cipher": None,
303
+ "version": None,
304
+ }
305
+ if self.ssl_context:
306
+ try:
307
+ conn = self.ssl_context.wrap_socket(conn, server_side=True)
308
+ tls_meta["peer_cert"] = conn.getpeercert()
309
+ tls_meta["cipher"] = conn.cipher()
310
+ tls_meta["version"] = conn.version()
311
+ except ssl.SSLError as e:
312
+ print(f"[tls] handshake error from {addr}: {e}")
313
+ try:
314
+ conn.close()
315
+ except Exception:
316
+ pass
317
+ return
318
+ except Exception as e:
319
+ print(f"[tls] wrap error from {addr}: {e}")
320
+ try:
321
+ conn.close()
322
+ except Exception:
323
+ pass
324
+ return
325
+
326
+ with conn:
327
+ try:
328
+ req = parse_http_request(
329
+ conn,
330
+ max_header_bytes=self.MAX_REQUEST_HEADER_BYTES,
331
+ )
332
+ except ValueError as e:
333
+ message = str(e).lower()
334
+ status = 431 if "header" in message else 400
335
+ send_http_response(conn, Response.text(str(e), status=status))
336
+ return
337
+ if not req:
338
+ return
339
+
340
+ headers = req["headers"]
341
+ body = req["remainder"]
342
+
343
+ if "content-length" in headers:
344
+ try:
345
+ cl = int(headers["content-length"])
346
+ except Exception:
347
+ cl = 0
348
+ if cl < 0:
349
+ cl = 0
350
+ if cl > self.MAX_REQUEST_BODY_BYTES:
351
+ send_http_response(
352
+ conn,
353
+ Response.text("Payload Too Large", status=413),
354
+ )
355
+ return
356
+ if len(body) < cl:
357
+ need = cl - len(body)
358
+ if need > 0:
359
+ body += recv_exact(conn, need)
360
+ if len(body) > self.MAX_REQUEST_BODY_BYTES:
361
+ send_http_response(
362
+ conn,
363
+ Response.text("Payload Too Large", status=413),
364
+ )
365
+ return
366
+
367
+ raw_path = req["path"]
368
+ path, _, query = raw_path.partition("?")
369
+
370
+ request = Request(
371
+ method=req["method"],
372
+ path=path,
373
+ query_string=query,
374
+ headers=headers,
375
+ body=body,
376
+ client=addr,
377
+ tls=tls_meta,
378
+ )
379
+
380
+ if is_ws_request(headers):
381
+ handler = self.app.ws_routes.get(path)
382
+ if not handler:
383
+ send_http_response(conn, Response.text("Not Found", status=404))
384
+ return
385
+ ws = handshake_websocket(conn, addr, headers)
386
+ if not ws:
387
+ return
388
+ try:
389
+ handler(ws, request)
390
+ except Exception as e:
391
+ print(f"[ws] error: {e}")
392
+ return
393
+
394
+ response = self.app.dispatch(request)
395
+ send_http_response(conn, response)
396
+
397
+
398
+ def parse_http_request(conn, max_header_bytes=65536):
399
+ data = b""
400
+ while b"\r\n\r\n" not in data:
401
+ chunk = conn.recv(1024)
402
+ if not chunk:
403
+ break
404
+ data += chunk
405
+ if len(data) > int(max_header_bytes):
406
+ raise ValueError("Request headers too large")
407
+ header_text, _, remainder = data.partition(b"\r\n\r\n")
408
+ lines = header_text.decode("utf-8", errors="ignore").split("\r\n")
409
+ if not lines:
410
+ return None
411
+ request_line = lines[0]
412
+ parts = request_line.split()
413
+ if len(parts) != 3:
414
+ return None
415
+ method, path, version = parts
416
+ if not version.startswith("HTTP/"):
417
+ return None
418
+ headers = {}
419
+ for line in lines[1:]:
420
+ if ":" in line:
421
+ k, v = line.split(":", 1)
422
+ headers[k.strip().lower()] = v.strip()
423
+ return {
424
+ "method": method,
425
+ "path": path,
426
+ "version": version,
427
+ "headers": headers,
428
+ "remainder": remainder,
429
+ }
430
+
431
+
432
+ def send_http_response(conn, response):
433
+ status_code = response.status
434
+ reason = response.reason or STATUS_MESSAGES.get(status_code, "OK")
435
+ headers = response.headers or {}
436
+ lowermap = {k.lower(): v for k, v in headers.items()}
437
+ if "content-length" not in lowermap:
438
+ headers["Content-Length"] = str(len(response.body))
439
+ if "connection" not in lowermap:
440
+ headers["Connection"] = "close"
441
+ status_line = f"HTTP/1.1 {status_code} {reason}\r\n"
442
+ hdrs = ""
443
+ for k, v in headers.items():
444
+ hdrs += f"{k}: {v}\r\n"
445
+ resp = status_line + hdrs + "\r\n"
446
+ try:
447
+ conn.sendall(resp.encode("utf-8") + response.body)
448
+ except Exception as e:
449
+ print(f"[http] send error {status_code}: {e}")
450
+
451
+
452
+ def is_ws_request(headers):
453
+ return headers.get("upgrade", "").lower() == "websocket"
454
+
455
+
456
+ def handshake_websocket(conn, addr, headers):
457
+ key = headers.get("sec-websocket-key", "")
458
+ if not key:
459
+ send_http_response(conn, Response.text("Missing Sec-WebSocket-Key", status=400))
460
+ return None
461
+
462
+ subprotocol = ""
463
+ offered = headers.get("sec-websocket-protocol", "")
464
+ if offered:
465
+ parts = [p.strip() for p in offered.split(",") if p.strip()]
466
+ if parts:
467
+ subprotocol = parts[0]
468
+
469
+ accept_src = (key + MAGIC_WS).encode("utf-8")
470
+ digest = sha1(accept_src)
471
+ accept = base64_encode(digest)
472
+
473
+ resp_headers = {
474
+ "Upgrade": "websocket",
475
+ "Connection": "Upgrade",
476
+ "Sec-WebSocket-Accept": accept,
477
+ }
478
+ if subprotocol:
479
+ resp_headers["Sec-WebSocket-Protocol"] = subprotocol
480
+
481
+ send_http_response(conn, Response(status=101, body=b"", headers=resp_headers))
482
+ return WebSocket(conn, addr, subprotocol, headers)
483
+
484
+
485
+ class WebSocket:
486
+ def __init__(self, sock, addr, subprotocol, headers):
487
+ self.sock = sock
488
+ self.addr = addr
489
+ self.subprotocol = subprotocol or ""
490
+ self.headers = headers or {}
491
+
492
+ def recv_frame(self):
493
+ return read_ws_frame_raw(self.sock)
494
+
495
+ def send_frame(self, opcode, payload=b""):
496
+ self.sock.sendall(make_ws_frame_bytes(opcode, payload))
497
+
498
+ def send_text(self, text):
499
+ self.send_frame(0x1, text.encode("utf-8"))
500
+
501
+ def send_binary(self, data):
502
+ self.send_frame(0x2, data)
503
+
504
+ def send_ping(self, payload=b""):
505
+ self.send_frame(0x9, payload)
506
+
507
+ def send_pong(self, payload=b""):
508
+ self.send_frame(0xA, payload)
509
+
510
+ def close(self, code=1000, reason=""):
511
+ payload = code.to_bytes(2, "big")
512
+ if reason:
513
+ payload += reason.encode("utf-8")
514
+ self.send_frame(0x8, payload)
515
+
516
+
517
+ def recv_exact(conn, n):
518
+ data = b""
519
+ while len(data) < n:
520
+ chunk = conn.recv(n - len(data))
521
+ if not chunk:
522
+ raise ConnectionError("connection closed")
523
+ data += chunk
524
+ return data
525
+
526
+
527
+ def read_ws_frame_raw(conn):
528
+ hdr = recv_exact(conn, 2)
529
+ b1, b2 = hdr[0], hdr[1]
530
+ fin = (b1 >> 7) & 1
531
+ opcode = b1 & 0x0F
532
+ masked = (b2 >> 7) & 1
533
+ payload_len = b2 & 0x7F
534
+
535
+ if payload_len == 126:
536
+ ext = recv_exact(conn, 2)
537
+ payload_len = int.from_bytes(ext, "big")
538
+ elif payload_len == 127:
539
+ ext = recv_exact(conn, 8)
540
+ payload_len = int.from_bytes(ext, "big")
541
+
542
+ mask = None
543
+ if masked:
544
+ mask = recv_exact(conn, 4)
545
+
546
+ payload = b""
547
+ if payload_len:
548
+ payload = recv_exact(conn, payload_len)
549
+ if masked and mask:
550
+ payload = bytes(b ^ mask[i % 4] for i, b in enumerate(payload))
551
+ return fin, opcode, payload, bool(masked), mask
552
+
553
+
554
+ def make_ws_frame_bytes(opcode, payload=b""):
555
+ fin = 0x80
556
+ b1 = fin | (opcode & 0x0F)
557
+ payload_len = len(payload)
558
+ header = bytes([b1])
559
+
560
+ if payload_len <= 125:
561
+ header += bytes([payload_len])
562
+ elif payload_len <= 65535:
563
+ header += bytes([126]) + payload_len.to_bytes(2, "big")
564
+ else:
565
+ header += bytes([127]) + payload_len.to_bytes(8, "big")
566
+
567
+ return header + payload
568
+
569
+
570
+ def parse_close_payload(payload):
571
+ if not payload:
572
+ return None, None
573
+ if len(payload) >= 2:
574
+ code = int.from_bytes(payload[:2], "big")
575
+ reason = ""
576
+ if len(payload) > 2:
577
+ try:
578
+ reason = payload[2:].decode("utf-8", errors="ignore")
579
+ except Exception:
580
+ reason = ""
581
+ return code, reason
582
+ return None, None
583
+
584
+
585
+ def _left_rotate(n, b):
586
+ return ((n << b) | (n >> (32 - b))) & 0xFFFFFFFF
587
+
588
+
589
+ def sha1(data_bytes):
590
+ message = bytearray(data_bytes)
591
+ orig_len_bits = (8 * len(message)) & 0xFFFFFFFFFFFFFFFF
592
+ message.append(0x80)
593
+ while (len(message) * 8) % 512 != 448:
594
+ message.append(0)
595
+ message += orig_len_bits.to_bytes(8, "big")
596
+
597
+ h0 = 0x67452301
598
+ h1 = 0xEFCDAB89
599
+ h2 = 0x98BADCFE
600
+ h3 = 0x10325476
601
+ h4 = 0xC3D2E1F0
602
+
603
+ for i in range(0, len(message), 64):
604
+ w = [0] * 80
605
+ chunk = message[i : i + 64]
606
+ for j in range(16):
607
+ w[j] = int.from_bytes(chunk[j * 4 : (j + 1) * 4], "big")
608
+ for j in range(16, 80):
609
+ w[j] = _left_rotate(w[j - 3] ^ w[j - 8] ^ w[j - 14] ^ w[j - 16], 1)
610
+
611
+ a, b, c, d, e = h0, h1, h2, h3, h4
612
+ for t in range(80):
613
+ if 0 <= t <= 19:
614
+ f = (b & c) | ((~b) & d)
615
+ k = 0x5A827999
616
+ elif 20 <= t <= 39:
617
+ f = b ^ c ^ d
618
+ k = 0x6ED9EBA1
619
+ elif 40 <= t <= 59:
620
+ f = (b & c) | (b & d) | (c & d)
621
+ k = 0x8F1BBCDC
622
+ else:
623
+ f = b ^ c ^ d
624
+ k = 0xCA62C1D6
625
+ tmp = (_left_rotate(a, 5) + f + e + k + w[t]) & 0xFFFFFFFF
626
+ e = d
627
+ d = c
628
+ c = _left_rotate(b, 30)
629
+ b = a
630
+ a = tmp
631
+
632
+ h0 = (h0 + a) & 0xFFFFFFFF
633
+ h1 = (h1 + b) & 0xFFFFFFFF
634
+ h2 = (h2 + c) & 0xFFFFFFFF
635
+ h3 = (h3 + d) & 0xFFFFFFFF
636
+ h4 = (h4 + e) & 0xFFFFFFFF
637
+
638
+ digest = b"".join(x.to_bytes(4, "big") for x in [h0, h1, h2, h3, h4])
639
+ return digest
640
+
641
+
642
+ B64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
643
+
644
+
645
+ def base64_encode(data_bytes):
646
+ res = []
647
+ i = 0
648
+ n = len(data_bytes)
649
+ while i < n:
650
+ b = data_bytes[i : i + 3]
651
+ i += 3
652
+ pad = 3 - len(b)
653
+ val = 0
654
+ for x in b:
655
+ val = (val << 8) + x
656
+ val <<= pad * 8
657
+ for j in range(18, -1, -6):
658
+ idx = (val >> j) & 0x3F
659
+ res.append(B64_ALPHABET[idx])
660
+ if pad:
661
+ res[-pad:] = "=" * pad
662
+ return "".join(res)