更新
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Read-only production WebSocket handshake diagnostic.
|
||||
|
||||
Runs on the BaoTa host so production credentials never leave the server. The
|
||||
script prints only HTTP status lines and the first command received.
|
||||
"""
|
||||
import base64
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
import ssl
|
||||
import sys
|
||||
import time
|
||||
|
||||
sys.path.insert(0, "/www/server/xingyu-im/ops")
|
||||
from baota_publish_test_users import database, env_values, query
|
||||
|
||||
|
||||
def encode_json(value):
|
||||
raw = json.dumps(value, separators=(",", ":"), ensure_ascii=False).encode()
|
||||
return base64.urlsafe_b64encode(raw).rstrip(b"=").decode()
|
||||
|
||||
|
||||
def user_token(secret, user_id, nickname, version):
|
||||
now = int(time.time())
|
||||
header = encode_json({"alg": "HS256", "typ": "JWT"})
|
||||
payload = encode_json({
|
||||
"role": "user", "name": nickname, "ver": version,
|
||||
"sub": str(user_id), "iat": now, "exp": now + 180,
|
||||
})
|
||||
signing_input = (header + "." + payload).encode()
|
||||
signature = base64.urlsafe_b64encode(
|
||||
hmac.new(secret.encode(), signing_input, hashlib.sha256).digest()
|
||||
).rstrip(b"=").decode()
|
||||
return header + "." + payload + "." + signature
|
||||
|
||||
|
||||
def receive_exact(stream, count):
|
||||
output = b""
|
||||
while len(output) < count:
|
||||
chunk = stream.recv(count - len(output))
|
||||
if not chunk:
|
||||
raise RuntimeError("connection closed")
|
||||
output += chunk
|
||||
return output
|
||||
|
||||
|
||||
def first_command(stream):
|
||||
first, second = receive_exact(stream, 2)
|
||||
size = second & 0x7F
|
||||
if size == 126:
|
||||
size = int.from_bytes(receive_exact(stream, 2), "big")
|
||||
elif size == 127:
|
||||
size = int.from_bytes(receive_exact(stream, 8), "big")
|
||||
payload = receive_exact(stream, size)
|
||||
return json.loads(payload).get("command")
|
||||
|
||||
|
||||
def handshake(token, origin):
|
||||
key = base64.b64encode(os.urandom(16)).decode()
|
||||
headers = [
|
||||
"GET /ws HTTP/1.1",
|
||||
"Host: im.bchongw.com",
|
||||
"Upgrade: websocket",
|
||||
"Connection: Upgrade",
|
||||
"Sec-WebSocket-Version: 13",
|
||||
"Sec-WebSocket-Key: " + key,
|
||||
"Sec-WebSocket-Protocol: xingyu.jwt." + token,
|
||||
]
|
||||
if origin is not None:
|
||||
headers.append("Origin: " + origin)
|
||||
request = ("\r\n".join(headers) + "\r\n\r\n").encode()
|
||||
context = ssl.create_default_context()
|
||||
with socket.create_connection(("im.bchongw.com", 443), timeout=10) as raw:
|
||||
with context.wrap_socket(raw, server_hostname="im.bchongw.com") as stream:
|
||||
stream.settimeout(10)
|
||||
stream.sendall(request)
|
||||
response = b""
|
||||
while b"\r\n\r\n" not in response:
|
||||
response += stream.recv(4096)
|
||||
head, remainder = response.split(b"\r\n\r\n", 1)
|
||||
status = head.split(b"\r\n", 1)[0].decode("ascii", errors="replace")
|
||||
command = None
|
||||
if " 101 " in status:
|
||||
if remainder:
|
||||
# Keep this branch simple; the server's AUTH_ACK normally
|
||||
# follows in a second TLS record and is read below.
|
||||
class Buffered:
|
||||
def __init__(self, initial, source): self.initial, self.source = initial, source
|
||||
def recv(self, count):
|
||||
if self.initial:
|
||||
chunk, self.initial = self.initial[:count], self.initial[count:]
|
||||
return chunk
|
||||
return self.source.recv(count)
|
||||
command = first_command(Buffered(remainder, stream))
|
||||
else:
|
||||
command = first_command(stream)
|
||||
return status, command
|
||||
|
||||
|
||||
values = env_values()
|
||||
conn, _ = database(values)
|
||||
try:
|
||||
user = query(conn, """SELECT user.id,profile.nickname,security.token_version
|
||||
FROM users user JOIN user_profiles profile ON profile.user_id=user.id
|
||||
JOIN user_security_controls security ON security.user_id=user.id
|
||||
WHERE user.status=1 AND user.deleted_at IS NULL ORDER BY user.id LIMIT 1""")[0]
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
token = user_token(values["IM_JWT_SECRET"], user[0], user[1], user[2])
|
||||
cases = [
|
||||
("no-origin", None),
|
||||
("web-origin", "https://im.bchongw.com"),
|
||||
("native-localhost", "http://localhost"),
|
||||
("native-file", "file://"),
|
||||
]
|
||||
for label, origin in cases:
|
||||
try:
|
||||
status, command = handshake(token, origin)
|
||||
print(json.dumps({"case": label, "status": status, "command": command}, ensure_ascii=False))
|
||||
except Exception as error:
|
||||
print(json.dumps({"case": label, "error": type(error).__name__}, ensure_ascii=False))
|
||||
Reference in New Issue
Block a user