|
15 | 15 |
|
16 | 16 | _MAX_PAYLOAD_BYTES = 1 << 20 # 1 MiB |
17 | 17 | _FRAME_HEADER = struct.Struct("!I") |
| 18 | +_RESPONSE_TERMINATOR = b"Return_Data_Over_JE\n" |
| 19 | +_AUTH_FAILED = object() |
18 | 20 |
|
19 | 21 |
|
20 | 22 | class TCPServer: |
@@ -49,6 +51,9 @@ def __init__( |
49 | 51 | self._tls_context: Optional[ssl.SSLContext] = None |
50 | 52 | if certfile and keyfile: |
51 | 53 | self._tls_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) |
| 54 | + # Pin minimum TLS version so older insecure suites cannot be |
| 55 | + # negotiated; PROTOCOL_TLS_SERVER alone permits TLS 1.0/1.1. |
| 56 | + self._tls_context.minimum_version = ssl.TLSVersion.TLSv1_2 |
52 | 57 | self._tls_context.load_cert_chain(certfile=certfile, keyfile=keyfile) |
53 | 58 |
|
54 | 59 | def socket_server(self, host: str, port: int) -> None: |
@@ -119,49 +124,66 @@ def handle(self, connection) -> None: |
119 | 124 | print(f"Command received: {len(command_string)} bytes", flush=True) |
120 | 125 |
|
121 | 126 | if command_string == "quit_server": |
122 | | - if self.token is not None: |
123 | | - self._send_frame(connection, b"Error: token required\n") |
124 | | - return |
125 | | - self.close_flag = True |
126 | | - self._send_frame(connection, b"Server shutting down\n") |
127 | | - print("Now quit server", flush=True) |
128 | | - return |
129 | | - |
130 | | - try: |
131 | | - payload = json.loads(command_string) |
132 | | - except json.JSONDecodeError as error: |
133 | | - self._send_frame(connection, f"Error: {error}\n".encode("utf-8")) |
134 | | - self._send_frame(connection, b"Return_Data_Over_JE\n") |
| 127 | + self._handle_legacy_quit(connection) |
135 | 128 | return |
136 | 129 |
|
137 | | - command = payload |
138 | | - if isinstance(payload, dict) and ("token" in payload or "command" in payload): |
139 | | - if not self._check_token(payload.get("token")): |
140 | | - self._send_frame(connection, b"Error: unauthorised\n") |
141 | | - return |
142 | | - command = payload.get("command") |
143 | | - if payload.get("op") == "quit": |
144 | | - self.close_flag = True |
145 | | - self._send_frame(connection, b"Server shutting down\n") |
146 | | - return |
147 | | - elif self.token is not None: |
148 | | - self._send_frame(connection, b"Error: token required\n") |
| 130 | + command = self._authorise_payload(connection, command_string) |
| 131 | + if command is _AUTH_FAILED: |
149 | 132 | return |
150 | | - |
151 | 133 | if command is None: |
152 | | - self._send_frame(connection, b"Return_Data_Over_JE\n") |
| 134 | + self._send_frame(connection, _RESPONSE_TERMINATOR) |
153 | 135 | return |
154 | 136 |
|
155 | | - try: |
156 | | - for execute_return in execute_action(command).values(): |
157 | | - self._send_frame(connection, f"{execute_return}\n".encode("utf-8")) |
158 | | - self._send_frame(connection, b"Return_Data_Over_JE\n") |
159 | | - except Exception as error: |
160 | | - self._send_frame(connection, f"Error: {error}\n".encode("utf-8")) |
161 | | - self._send_frame(connection, b"Return_Data_Over_JE\n") |
| 137 | + self._dispatch_command(connection, command) |
162 | 138 | finally: |
163 | 139 | connection.close() |
164 | 140 |
|
| 141 | + def _handle_legacy_quit(self, connection) -> None: |
| 142 | + if self.token is not None: |
| 143 | + self._send_frame(connection, b"Error: token required\n") |
| 144 | + return |
| 145 | + self.close_flag = True |
| 146 | + self._send_frame(connection, b"Server shutting down\n") |
| 147 | + print("Now quit server", flush=True) |
| 148 | + |
| 149 | + def _authorise_payload(self, connection, command_string: str): |
| 150 | + """ |
| 151 | + Decode the JSON envelope, enforce the token, and return the |
| 152 | + actual command to execute. Returns ``_AUTH_FAILED`` when the |
| 153 | + client has already been answered (bad JSON, missing/bad token, |
| 154 | + or a quit op was honoured). |
| 155 | + """ |
| 156 | + try: |
| 157 | + payload = json.loads(command_string) |
| 158 | + except json.JSONDecodeError as error: |
| 159 | + self._send_frame(connection, f"Error: {error}\n".encode("utf-8")) |
| 160 | + self._send_frame(connection, _RESPONSE_TERMINATOR) |
| 161 | + return _AUTH_FAILED |
| 162 | + |
| 163 | + if isinstance(payload, dict) and ("token" in payload or "command" in payload): |
| 164 | + if not self._check_token(payload.get("token")): |
| 165 | + self._send_frame(connection, b"Error: unauthorised\n") |
| 166 | + return _AUTH_FAILED |
| 167 | + if payload.get("op") == "quit": |
| 168 | + self.close_flag = True |
| 169 | + self._send_frame(connection, b"Server shutting down\n") |
| 170 | + return _AUTH_FAILED |
| 171 | + return payload.get("command") |
| 172 | + |
| 173 | + if self.token is not None: |
| 174 | + self._send_frame(connection, b"Error: token required\n") |
| 175 | + return _AUTH_FAILED |
| 176 | + |
| 177 | + return payload |
| 178 | + |
| 179 | + def _dispatch_command(self, connection, command) -> None: |
| 180 | + try: |
| 181 | + for execute_return in execute_action(command).values(): |
| 182 | + self._send_frame(connection, f"{execute_return}\n".encode("utf-8")) |
| 183 | + except Exception as error: |
| 184 | + self._send_frame(connection, f"Error: {error}\n".encode("utf-8")) |
| 185 | + self._send_frame(connection, _RESPONSE_TERMINATOR) |
| 186 | + |
165 | 187 |
|
166 | 188 | def start_load_density_socket_server( |
167 | 189 | host: str = "localhost", |
|
0 commit comments