fix(tls): Improve TLS alert handling in connection handler: use the new TlsAlert class to send proper unrecognized_name alerts when a ClientHello is missing SNI and wait for a retry on the same connection before closing. Also, add alertFallbackTimeout tracking to connection records for better timeout management.
This commit is contained in:
		| @@ -3,6 +3,6 @@ | ||||
|  */ | ||||
| export const commitinfo = { | ||||
|   name: '@push.rocks/smartproxy', | ||||
|   version: '4.1.15', | ||||
|   version: '4.1.16', | ||||
|   description: 'A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, dynamic routing with authentication options, and automatic ACME certificate management.' | ||||
| } | ||||
|   | ||||
| @@ -11,6 +11,7 @@ import { TlsManager } from './classes.pp.tlsmanager.js'; | ||||
| import { NetworkProxyBridge } from './classes.pp.networkproxybridge.js'; | ||||
| import { TimeoutManager } from './classes.pp.timeoutmanager.js'; | ||||
| import { PortRangeManager } from './classes.pp.portrangemanager.js'; | ||||
| import { TlsAlert } from './classes.pp.tlsalert.js'; | ||||
|  | ||||
| /** | ||||
|  * Handles new connection processing and setup logic | ||||
| @@ -560,95 +561,125 @@ export class ConnectionHandler { | ||||
|             // Block ClientHello without SNI when allowSessionTicket is false | ||||
|             console.log( | ||||
|               `[${connectionId}] No SNI detected in ClientHello and allowSessionTicket=false. ` + | ||||
|                 `Sending warning unrecognized_name alert to encourage immediate retry with SNI.` | ||||
|                 `Sending unrecognized_name alert to encourage immediate retry with SNI on same connection.` | ||||
|             ); | ||||
|  | ||||
|             // Set the termination reason first | ||||
|             if (record.incomingTerminationReason === null) { | ||||
|               record.incomingTerminationReason = 'session_ticket_blocked_no_sni'; | ||||
|               this.connectionManager.incrementTerminationStat( | ||||
|                 'incoming', | ||||
|                 'session_ticket_blocked_no_sni' | ||||
|               ); | ||||
|             } | ||||
|  | ||||
|             // Create a warning-level alert for unrecognized_name | ||||
|             // This encourages Chrome to retry immediately with SNI | ||||
|             const serverNameUnknownAlertData = Buffer.from([ | ||||
|               0x15, // Alert record type | ||||
|               0x03, | ||||
|               0x03, // TLS 1.2 version | ||||
|               0x00, | ||||
|               0x02, // Length | ||||
|               0x01, // Warning alert level (not fatal) | ||||
|               0x70, // unrecognized_name alert (code 112) | ||||
|             ]); | ||||
|  | ||||
|             // Send a handshake_failure alert instead of unrecognized_name | ||||
|             const sslHandshakeFailureAlertData = Buffer.from([ | ||||
|               0x15, // Alert record type | ||||
|               0x03, | ||||
|               0x03, // TLS 1.2 version | ||||
|               0x00, | ||||
|               0x02, // Length | ||||
|               0x01, // Warning alert level (not fatal) | ||||
|               0x28, // handshake_failure alert (40) instead of unrecognized_name (112) | ||||
|             ]); | ||||
|  | ||||
|             const closeNotifyAlert = Buffer.from([ | ||||
|               0x15, // Alert record type | ||||
|               0x03, | ||||
|               0x03, // TLS 1.2 version | ||||
|               0x00, | ||||
|               0x02, // Length | ||||
|               0x01, // Warning alert level (1) | ||||
|               0x00, // close_notify alert (0) | ||||
|             ]); | ||||
|  | ||||
|             const certificateExpiredAlert = Buffer.from([ | ||||
|               0x15, // Alert record type | ||||
|               0x03, | ||||
|               0x03, // TLS 1.2 version | ||||
|               0x00, | ||||
|               0x02, // Length | ||||
|               0x01, // Warning alert level (1) | ||||
|               0x2F, // certificate_expired alert (47) | ||||
|             ]); | ||||
|  | ||||
|             try { | ||||
|               // Use cork/uncork to ensure the alert is sent as a single packet | ||||
|               // Send the alert but do NOT end the connection | ||||
|               // Using our new TlsAlert class for better alert management | ||||
|               socket.cork(); | ||||
|               const writeSuccessful = socket.write(serverNameUnknownAlertData); | ||||
|               socket.write(TlsAlert.alerts.unrecognizedName); | ||||
|               socket.uncork(); | ||||
|                | ||||
|               // Function to handle the clean socket termination - but more gradually | ||||
|               const finishConnection = () => { | ||||
|                 // Give Chrome more time to process the alert before closing | ||||
|                 // We won't call destroy() at all - just end() and let the socket close naturally | ||||
|                  | ||||
|                 // Log the cleanup but wait for natural closure | ||||
|                 setTimeout(() => { | ||||
|                   socket.end(); | ||||
|                   this.connectionManager.cleanupConnection(record, 'session_ticket_blocked_no_sni'); | ||||
|                 }, 5000); // Longer delay to let socket cleanup happen naturally | ||||
|               }; | ||||
|                | ||||
|               if (writeSuccessful) { | ||||
|                 // Wait longer before ending connection to ensure alert is processed by client | ||||
|                 setTimeout(finishConnection, 200); // Increased from 50ms to 200ms | ||||
|               } else { | ||||
|                 // If the kernel buffer was full, wait for the drain event | ||||
|                 socket.once('drain', () => { | ||||
|                   // Wait longer after drain as well | ||||
|                   setTimeout(finishConnection, 200); | ||||
|                 }); | ||||
|                  | ||||
|                 // Safety timeout is increased too | ||||
|                 setTimeout(() => { | ||||
|                   socket.removeAllListeners('drain'); | ||||
|                   finishConnection(); | ||||
|                 }, 400); // Increased from 250ms to 400ms | ||||
|  | ||||
|               console.log( | ||||
|                 `[${connectionId}] Alert sent, waiting for new ClientHello on same connection...` | ||||
|               ); | ||||
|  | ||||
|               // Remove existing data listener and wait for a new ClientHello | ||||
|               socket.removeAllListeners('data'); | ||||
|  | ||||
|               // Set up a new data handler to capture the next message | ||||
|               socket.once('data', (retryChunk) => { | ||||
|                 // Cancel the fallback timeout as we received data | ||||
|                 if (record.alertFallbackTimeout) { | ||||
|                   clearTimeout(record.alertFallbackTimeout); | ||||
|                   record.alertFallbackTimeout = null; | ||||
|                 } | ||||
|  | ||||
|                 // Check if this is a new ClientHello | ||||
|                 if (this.tlsManager.isClientHello(retryChunk)) { | ||||
|                   console.log(`[${connectionId}] Received new ClientHello after alert`); | ||||
|  | ||||
|                   // Extract SNI from the new ClientHello | ||||
|                   const newServerName = this.tlsManager.extractSNI(retryChunk, connInfo) || ''; | ||||
|  | ||||
|                   if (newServerName) { | ||||
|                     console.log(`[${connectionId}] New ClientHello contains SNI: ${newServerName}`); | ||||
|  | ||||
|                     // Update the record with the new SNI | ||||
|                     record.lockedDomain = newServerName; | ||||
|  | ||||
|                     // Continue with normal connection setup using the new chunk with SNI | ||||
|                     setupConnection(newServerName, retryChunk); | ||||
|                   } else { | ||||
|                     console.log( | ||||
|                       `[${connectionId}] New ClientHello still missing SNI, closing connection` | ||||
|                     ); | ||||
|  | ||||
|                     // If still no SNI after retry, now we can close the connection | ||||
|                     if (record.incomingTerminationReason === null) { | ||||
|                       record.incomingTerminationReason = 'session_ticket_blocked_no_sni'; | ||||
|                       this.connectionManager.incrementTerminationStat( | ||||
|                         'incoming', | ||||
|                         'session_ticket_blocked_no_sni' | ||||
|                       ); | ||||
|                     } | ||||
|  | ||||
|                     // Send a close_notify alert before ending the connection | ||||
|                     TlsAlert.sendCloseNotify(socket) | ||||
|                       .catch((err) => { | ||||
|                         console.log(`[${connectionId}] Error sending close_notify: ${err.message}`); | ||||
|                       }) | ||||
|                       .finally(() => { | ||||
|                         // Clean up even if sending the alert fails | ||||
|                         this.connectionManager.cleanupConnection( | ||||
|                           record, | ||||
|                           'session_ticket_blocked_no_sni' | ||||
|                         ); | ||||
|                       }); | ||||
|                   } | ||||
|                 } else { | ||||
|                   console.log( | ||||
|                     `[${connectionId}] Received non-ClientHello data after alert, closing connection` | ||||
|                   ); | ||||
|  | ||||
|                   // If we got something other than a ClientHello, close the connection | ||||
|                   if (record.incomingTerminationReason === null) { | ||||
|                     record.incomingTerminationReason = 'invalid_protocol'; | ||||
|                     this.connectionManager.incrementTerminationStat('incoming', 'invalid_protocol'); | ||||
|                   } | ||||
|  | ||||
|                   // Send a protocol_version alert before ending the connection | ||||
|                   TlsAlert.send(socket, TlsAlert.LEVEL_FATAL, TlsAlert.PROTOCOL_VERSION, true) | ||||
|                     .catch((err) => { | ||||
|                       console.log( | ||||
|                         `[${connectionId}] Error sending protocol_version alert: ${err.message}` | ||||
|                       ); | ||||
|                     }) | ||||
|                     .finally(() => { | ||||
|                       // Clean up even if sending the alert fails | ||||
|                       this.connectionManager.cleanupConnection(record, 'invalid_protocol'); | ||||
|                     }); | ||||
|                 } | ||||
|               }); | ||||
|  | ||||
|               // Set a fallback timeout in case the client doesn't respond | ||||
|               const fallbackTimeout = setTimeout(() => { | ||||
|                 console.log(`[${connectionId}] No response after alert, closing connection`); | ||||
|  | ||||
|                 if (record.incomingTerminationReason === null) { | ||||
|                   record.incomingTerminationReason = 'alert_timeout'; | ||||
|                   this.connectionManager.incrementTerminationStat('incoming', 'alert_timeout'); | ||||
|                 } | ||||
|  | ||||
|                 // Send a close_notify alert before ending the connection | ||||
|                 TlsAlert.sendCloseNotify(socket) | ||||
|                   .catch((err) => { | ||||
|                     console.log(`[${connectionId}] Error sending close_notify: ${err.message}`); | ||||
|                   }) | ||||
|                   .finally(() => { | ||||
|                     // Clean up even if sending the alert fails | ||||
|                     this.connectionManager.cleanupConnection(record, 'alert_timeout'); | ||||
|                   }); | ||||
|               }, 10000); // 10 second timeout | ||||
|  | ||||
|               // Make sure the timeout doesn't keep the process alive | ||||
|               if (fallbackTimeout.unref) { | ||||
|                 fallbackTimeout.unref(); | ||||
|               } | ||||
|  | ||||
|               // Store the timeout in the record so it can be cleared during cleanup | ||||
|               record.alertFallbackTimeout = fallbackTimeout; | ||||
|             } catch (err) { | ||||
|               // If we can't send the alert, fall back to immediate termination | ||||
|               console.log(`[${connectionId}] Error sending TLS alert: ${err.message}`); | ||||
| @@ -656,6 +687,7 @@ export class ConnectionHandler { | ||||
|               this.connectionManager.cleanupConnection(record, 'session_ticket_blocked_no_sni'); | ||||
|             } | ||||
|  | ||||
|             // Return early to prevent the normal flow | ||||
|             return; | ||||
|           } | ||||
|         } | ||||
|   | ||||
| @@ -104,6 +104,7 @@ export interface IConnectionRecord { | ||||
|   lockedDomain?: string; // Used to lock this connection to the initial SNI | ||||
|   connectionClosed: boolean; // Flag to prevent multiple cleanup attempts | ||||
|   cleanupTimer?: NodeJS.Timeout; // Timer for max lifetime/inactivity | ||||
|   alertFallbackTimeout?: NodeJS.Timeout; // Timer for fallback after alert | ||||
|   lastActivity: number; // Last activity timestamp for inactivity detection | ||||
|   pendingData: Buffer[]; // Buffer to hold data during connection setup | ||||
|   pendingDataSize: number; // Track total size of pending data | ||||
|   | ||||
							
								
								
									
										218
									
								
								ts/classes.pp.tlsalert.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										218
									
								
								ts/classes.pp.tlsalert.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,218 @@ | ||||
| import * as net from 'net'; | ||||
|  | ||||
| /** | ||||
|  * TlsAlert class for managing TLS alert messages | ||||
|  */ | ||||
| export class TlsAlert { | ||||
|   // TLS Alert Levels | ||||
|   static readonly LEVEL_WARNING = 0x01; | ||||
|   static readonly LEVEL_FATAL = 0x02; | ||||
|    | ||||
|   // TLS Alert Description Codes - RFC 8446 (TLS 1.3) / RFC 5246 (TLS 1.2) | ||||
|   static readonly CLOSE_NOTIFY = 0x00; | ||||
|   static readonly UNEXPECTED_MESSAGE = 0x0A; | ||||
|   static readonly BAD_RECORD_MAC = 0x14; | ||||
|   static readonly DECRYPTION_FAILED = 0x15; // TLS 1.0 only | ||||
|   static readonly RECORD_OVERFLOW = 0x16; | ||||
|   static readonly DECOMPRESSION_FAILURE = 0x1E; // TLS 1.2 and below | ||||
|   static readonly HANDSHAKE_FAILURE = 0x28; | ||||
|   static readonly NO_CERTIFICATE = 0x29; // SSLv3 only | ||||
|   static readonly BAD_CERTIFICATE = 0x2A; | ||||
|   static readonly UNSUPPORTED_CERTIFICATE = 0x2B; | ||||
|   static readonly CERTIFICATE_REVOKED = 0x2C; | ||||
|   static readonly CERTIFICATE_EXPIRED = 0x2F; | ||||
|   static readonly CERTIFICATE_UNKNOWN = 0x30; | ||||
|   static readonly ILLEGAL_PARAMETER = 0x2F; | ||||
|   static readonly UNKNOWN_CA = 0x30; | ||||
|   static readonly ACCESS_DENIED = 0x31; | ||||
|   static readonly DECODE_ERROR = 0x32; | ||||
|   static readonly DECRYPT_ERROR = 0x33; | ||||
|   static readonly EXPORT_RESTRICTION = 0x3C; // TLS 1.0 only | ||||
|   static readonly PROTOCOL_VERSION = 0x46; | ||||
|   static readonly INSUFFICIENT_SECURITY = 0x47; | ||||
|   static readonly INTERNAL_ERROR = 0x50; | ||||
|   static readonly INAPPROPRIATE_FALLBACK = 0x56; | ||||
|   static readonly USER_CANCELED = 0x5A; | ||||
|   static readonly NO_RENEGOTIATION = 0x64; // TLS 1.2 and below | ||||
|   static readonly MISSING_EXTENSION = 0x6D; // TLS 1.3 | ||||
|   static readonly UNSUPPORTED_EXTENSION = 0x6E; // TLS 1.3 | ||||
|   static readonly CERTIFICATE_REQUIRED = 0x6F; // TLS 1.3 | ||||
|   static readonly UNRECOGNIZED_NAME = 0x70; | ||||
|   static readonly BAD_CERTIFICATE_STATUS_RESPONSE = 0x71; | ||||
|   static readonly BAD_CERTIFICATE_HASH_VALUE = 0x72; // TLS 1.2 and below | ||||
|   static readonly UNKNOWN_PSK_IDENTITY = 0x73; | ||||
|   static readonly CERTIFICATE_REQUIRED_1_3 = 0x74; // TLS 1.3 | ||||
|   static readonly NO_APPLICATION_PROTOCOL = 0x78; | ||||
|    | ||||
|   /** | ||||
|    * Create a TLS alert buffer with the specified level and description code | ||||
|    *  | ||||
|    * @param level Alert level (warning or fatal) | ||||
|    * @param description Alert description code | ||||
|    * @param tlsVersion TLS version bytes (default is TLS 1.2: 0x0303) | ||||
|    * @returns Buffer containing the TLS alert message | ||||
|    */ | ||||
|   static create( | ||||
|     level: number, | ||||
|     description: number, | ||||
|     tlsVersion: [number, number] = [0x03, 0x03] | ||||
|   ): Buffer { | ||||
|     return Buffer.from([ | ||||
|       0x15, // Alert record type | ||||
|       tlsVersion[0], | ||||
|       tlsVersion[1], // TLS version (default to TLS 1.2: 0x0303) | ||||
|       0x00, | ||||
|       0x02, // Length | ||||
|       level, // Alert level | ||||
|       description, // Alert description | ||||
|     ]); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Create a warning-level TLS alert | ||||
|    *  | ||||
|    * @param description Alert description code | ||||
|    * @returns Buffer containing the warning-level TLS alert message | ||||
|    */ | ||||
|   static createWarning(description: number): Buffer { | ||||
|     return this.create(this.LEVEL_WARNING, description); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Create a fatal-level TLS alert | ||||
|    *  | ||||
|    * @param description Alert description code | ||||
|    * @returns Buffer containing the fatal-level TLS alert message | ||||
|    */ | ||||
|   static createFatal(description: number): Buffer { | ||||
|     return this.create(this.LEVEL_FATAL, description); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Send a TLS alert to a socket and optionally close the connection | ||||
|    *  | ||||
|    * @param socket The socket to send the alert to | ||||
|    * @param level Alert level (warning or fatal) | ||||
|    * @param description Alert description code | ||||
|    * @param closeAfterSend Whether to close the connection after sending the alert | ||||
|    * @param closeDelay Milliseconds to wait before closing the connection (default: 200ms) | ||||
|    * @returns Promise that resolves when the alert has been sent | ||||
|    */ | ||||
|   static async send( | ||||
|     socket: net.Socket, | ||||
|     level: number, | ||||
|     description: number, | ||||
|     closeAfterSend: boolean = false, | ||||
|     closeDelay: number = 200 | ||||
|   ): Promise<void> { | ||||
|     const alert = this.create(level, description); | ||||
|      | ||||
|     return new Promise<void>((resolve, reject) => { | ||||
|       try { | ||||
|         // Ensure the alert is written as a single packet | ||||
|         socket.cork(); | ||||
|         const writeSuccessful = socket.write(alert, (err) => { | ||||
|           if (err) { | ||||
|             reject(err); | ||||
|             return; | ||||
|           } | ||||
|            | ||||
|           if (closeAfterSend) { | ||||
|             setTimeout(() => { | ||||
|               socket.end(); | ||||
|               resolve(); | ||||
|             }, closeDelay); | ||||
|           } else { | ||||
|             resolve(); | ||||
|           } | ||||
|         }); | ||||
|         socket.uncork(); | ||||
|          | ||||
|         // If write wasn't successful immediately, wait for drain | ||||
|         if (!writeSuccessful && !closeAfterSend) { | ||||
|           socket.once('drain', () => { | ||||
|             resolve(); | ||||
|           }); | ||||
|         } | ||||
|       } catch (err) { | ||||
|         reject(err); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Pre-defined TLS alert messages | ||||
|    */ | ||||
|   static readonly alerts = { | ||||
|     // Warning level alerts | ||||
|     closeNotify: TlsAlert.createWarning(TlsAlert.CLOSE_NOTIFY), | ||||
|     unsupportedExtension: TlsAlert.createWarning(TlsAlert.UNSUPPORTED_EXTENSION), | ||||
|     certificateRequired: TlsAlert.createWarning(TlsAlert.CERTIFICATE_REQUIRED), | ||||
|     unrecognizedName: TlsAlert.createWarning(TlsAlert.UNRECOGNIZED_NAME), | ||||
|     noRenegotiation: TlsAlert.createWarning(TlsAlert.NO_RENEGOTIATION), | ||||
|     userCanceled: TlsAlert.createWarning(TlsAlert.USER_CANCELED), | ||||
|      | ||||
|     // Warning level alerts for session resumption | ||||
|     certificateExpiredWarning: TlsAlert.createWarning(TlsAlert.CERTIFICATE_EXPIRED), | ||||
|     handshakeFailureWarning: TlsAlert.createWarning(TlsAlert.HANDSHAKE_FAILURE), | ||||
|     insufficientSecurityWarning: TlsAlert.createWarning(TlsAlert.INSUFFICIENT_SECURITY), | ||||
|      | ||||
|     // Fatal level alerts | ||||
|     unexpectedMessage: TlsAlert.createFatal(TlsAlert.UNEXPECTED_MESSAGE), | ||||
|     badRecordMac: TlsAlert.createFatal(TlsAlert.BAD_RECORD_MAC), | ||||
|     recordOverflow: TlsAlert.createFatal(TlsAlert.RECORD_OVERFLOW), | ||||
|     handshakeFailure: TlsAlert.createFatal(TlsAlert.HANDSHAKE_FAILURE), | ||||
|     badCertificate: TlsAlert.createFatal(TlsAlert.BAD_CERTIFICATE), | ||||
|     certificateExpired: TlsAlert.createFatal(TlsAlert.CERTIFICATE_EXPIRED), | ||||
|     certificateUnknown: TlsAlert.createFatal(TlsAlert.CERTIFICATE_UNKNOWN), | ||||
|     illegalParameter: TlsAlert.createFatal(TlsAlert.ILLEGAL_PARAMETER), | ||||
|     unknownCA: TlsAlert.createFatal(TlsAlert.UNKNOWN_CA), | ||||
|     accessDenied: TlsAlert.createFatal(TlsAlert.ACCESS_DENIED), | ||||
|     decodeError: TlsAlert.createFatal(TlsAlert.DECODE_ERROR), | ||||
|     decryptError: TlsAlert.createFatal(TlsAlert.DECRYPT_ERROR), | ||||
|     protocolVersion: TlsAlert.createFatal(TlsAlert.PROTOCOL_VERSION), | ||||
|     insufficientSecurity: TlsAlert.createFatal(TlsAlert.INSUFFICIENT_SECURITY), | ||||
|     internalError: TlsAlert.createFatal(TlsAlert.INTERNAL_ERROR), | ||||
|   }; | ||||
|    | ||||
|   /** | ||||
|    * Utility method to send a warning-level unrecognized_name alert | ||||
|    * Specifically designed for SNI issues to encourage the client to retry with SNI | ||||
|    *  | ||||
|    * @param socket The socket to send the alert to | ||||
|    * @returns Promise that resolves when the alert has been sent | ||||
|    */ | ||||
|   static async sendSniRequired(socket: net.Socket): Promise<void> { | ||||
|     return this.send(socket, this.LEVEL_WARNING, this.UNRECOGNIZED_NAME); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Utility method to send a close_notify alert and close the connection | ||||
|    *  | ||||
|    * @param socket The socket to send the alert to | ||||
|    * @param closeDelay Milliseconds to wait before closing the connection (default: 200ms) | ||||
|    * @returns Promise that resolves when the alert has been sent and the connection closed | ||||
|    */ | ||||
|   static async sendCloseNotify(socket: net.Socket, closeDelay: number = 200): Promise<void> { | ||||
|     return this.send(socket, this.LEVEL_WARNING, this.CLOSE_NOTIFY, true, closeDelay); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Utility method to send a certificate_expired alert to force new TLS session | ||||
|    *  | ||||
|    * @param socket The socket to send the alert to | ||||
|    * @param fatal Whether to send as a fatal alert (default: false) | ||||
|    * @param closeAfterSend Whether to close the connection after sending the alert (default: true) | ||||
|    * @param closeDelay Milliseconds to wait before closing the connection (default: 200ms) | ||||
|    * @returns Promise that resolves when the alert has been sent | ||||
|    */ | ||||
|   static async sendCertificateExpired( | ||||
|     socket: net.Socket, | ||||
|     fatal: boolean = false, | ||||
|     closeAfterSend: boolean = true, | ||||
|     closeDelay: number = 200 | ||||
|   ): Promise<void> { | ||||
|     const level = fatal ? this.LEVEL_FATAL : this.LEVEL_WARNING; | ||||
|     return this.send(socket, level, this.CERTIFICATE_EXPIRED, closeAfterSend, closeDelay); | ||||
|   } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user