extends Node # Stream XMPP # Author : AleaJactaEst # # https://xmpp.org/extensions/xep-0178.html # https://www.iana.org/assignments/sasl-mechanisms/sasl-mechanisms.xhtml # Boucle # - non connecte # - connecte en claire # - connecte en TLS # - lance l'authentification # - step authentification # - identifie signal send_msg_debug(message:String) signal send_msg_error(message:String) signal new_stanza(stanza) enum StreamState { END, # Stream is over. Starting with this. START, # Stream hass started. TLS, # Connection has been made and the first header is send. Let's get ssl! AUTHENTICATE, # We have negotiated whether to use TLS, let's authenticate. STANZA # We have authenticated, account is now allowed to send stanzas } var stream_status = StreamState.END enum XMPPState { NONE, # Nothing is configured TRY_CONNECT_SERVER, # Try to connect to server CONNECTED_SERVER, # Confirmed we are connected to server INTIALIZE, # Initialize first connection with server INTIALIZED, # when initialize is finished FEATURE_SERVER, # We received Feature (TLS ?) START_TLS, # We start TLS WAIT_TLS, # Wait TLS Done MISSING_TLS, # Missing feature TLS -> wait 30min and retry STARTED_TLS, # TLS connection is done SELECT_MECHANISM_AUTHENTICATE, AUTHENTICATE_STEP_PLAIN, # Select mechanism authenticate PLAIN AUTHENTICATE_STEP_1, # Launch authenticate AUTHENTICATE_STEP_2, # Launch authenticate AUTHENTICATE_STEP_3, # Launch authenticate AUTHENTICATE_CHECK_RESULT, # Last time check result AUTHENTICATE_FAILURE, AUTHENTICATE_SUCCESS, AUTHENTICATED, # We finished authenticate NOT_AUTHORIZED, # Not Authorize ACCOUNT_DISABLED, # Account Disable } enum ResultAuthenticate { NONE, # Authenticate not started DONE, # Authenticate finished and done ABORT, # Authenticate finished with error (try other method) NEXT, # Authenticate need execute other step } enum SASLReturn { SUCCESS, ABORTED, ACCOUNT_DISABLED, CREDENTIALS_EXPIRED, ENCRYPTION_REQUIRED, INCORRECT_ENCODING, INVALID_AUTHZID, INVALID_MECHANISM, MALFORMED_REQUEST, MECHANISM_TOO_WEAK, NOT_AUTHORIZED, TEMPORARY_AUTH_FAILURE, UNKNOWN } @export var server_xmpp_name:String = "localhost": set = set_server_xmpp_name, get = get_server_xmpp_name func set_server_xmpp_name(value:String): server_xmpp_name = value func get_server_xmpp_name() -> String: return server_xmpp_name @export var port_number:int = 5222: set = set_port_number, get = get_port_number func set_port_number(value:int): port_number = value func get_port_number() -> int: return port_number @export var account_name:String = "undefined@localhost": set = set_account_name, get = get_account_name func set_account_name(value:String): account_name = value func get_account_name() -> String: return account_name @export var password:String = "undefined": set = set_password, get = get_password func set_password(value:String): password = value func get_password() -> String: return password @export var locale:String = "en": set = set_locale, get = get_locale func set_locale(value:String): locale = value func get_locale() -> String: return locale @export var xmppclient:bool = false: get: return xmppclient set(value): xmppclient = value const MAX_WAIT_CONNECTING:float = 60.0 const MAX_WAIT_MISSING_TLS:float = 1800.0 const MAX_WAIT_ACCOUNT_DISABLED:float = 900.0 const MAX_WAIT_NOT_AUTHORIZED:float = 18000.0 var try_connect:int = 0 var count_connecting_time:float = MAX_WAIT_CONNECTING var partial_stanza:String = "" var tcp_peer:StreamPeerTCP = StreamPeerTCP.new() var ssl_peer:StreamPeerTLS = StreamPeerTLS.new() var tgt_peer = null var status:StreamState = StreamState.END var xmpp_state = XMPPState.NONE var authentication_methods = [] var selected_mechanism_authenticate:String = "" var order_preference_mechanism_authenticate: Array = ['PLAIN'] var banned_mechanism_authenticate:Array = [] func reinit_stream(): if ssl_peer != null: ssl_peer.disconnect_from_stream() else: ssl_peer = StreamPeerTLS.new() if tcp_peer != null: tcp_peer.disconnect_from_host() else: tcp_peer = StreamPeerTCP.new() tgt_peer = tcp_peer start_stream() stream_status = self.StreamState.START func _init() -> void: var language = OS.get_locale() set_locale(language) tgt_peer = tcp_peer # reinit_stream() func _process(delta) -> void: print(server_xmpp_name, ":", port_number, " / get_status:" , tcp_peer.get_status(), " / count_connecting_time:", count_connecting_time, " / stream_status:", stream_status, " / xmpp_state:", XMPPState.keys()[xmpp_state]) if xmppclient == false: return # Wait MAX_WAIT_MISSING_TLS (s) if server can't negociate with TLS if xmpp_state == XMPPState.MISSING_TLS: if count_connecting_time >= MAX_WAIT_MISSING_TLS: xmpp_state = XMPPState.NONE else: count_connecting_time += delta elif xmpp_state == XMPPState.NOT_AUTHORIZED: if count_connecting_time >= MAX_WAIT_NOT_AUTHORIZED: xmpp_state = XMPPState.NONE else: count_connecting_time += delta elif xmpp_state == XMPPState.ACCOUNT_DISABLED: if count_connecting_time >= MAX_WAIT_ACCOUNT_DISABLED: xmpp_state = XMPPState.NONE else: count_connecting_time += delta elif xmpp_state == XMPPState.NONE: if count_connecting_time >= MAX_WAIT_CONNECTING: xmpp_state = XMPPState.TRY_CONNECT_SERVER if ssl_peer.get_status() != StreamPeerTLS.STATUS_DISCONNECTED: ssl_peer.disconnect_from_stream() if tcp_peer.get_status() != StreamPeerTCP.STATUS_NONE: tcp_peer.disconnect_from_host() else: count_connecting_time += delta elif xmpp_state == XMPPState.TRY_CONNECT_SERVER: if tcp_peer.get_status() == StreamPeerTCP.STATUS_CONNECTED: xmpp_state = XMPPState.CONNECTED_SERVER count_connecting_time = MAX_WAIT_CONNECTING elif count_connecting_time >= MAX_WAIT_CONNECTING: var res = tcp_peer.connect_to_host(server_xmpp_name, port_number) if res == OK: send_msg_debug.emit("Send connect_to_host : ok") #stream_status = self.StreamState.END #xmpp_state = XMPPState.CONNECTED_SERVER count_connecting_time = 0 else: send_msg_error.emit("Error to connect to XMPP server (return:%d)" % res) xmpp_state = XMPPState.NONE count_connecting_time = 0 else: if (tcp_peer.has_method("poll")): tcp_peer.poll() count_connecting_time += delta elif xmpp_state == XMPPState.CONNECTED_SERVER: if tcp_peer.get_status() != StreamPeerTCP.STATUS_CONNECTED: xmpp_state = XMPPState.NONE count_connecting_time = 0 return if count_connecting_time >= MAX_WAIT_CONNECTING: count_connecting_time = 0 send_msg_debug.emit("send_tcp_initialize_xmpp") send_tcp_initialize_xmpp() else: count_connecting_time += delta if (tcp_peer.has_method("poll")): tcp_peer.poll() if tcp_peer.get_available_bytes()>0: var response = tcp_peer.get_string(tcp_peer.get_available_bytes()) send_msg_debug.emit("Stream: response: " + response) response = remove_stream_header(response) if not analyze_error(response): if analyze_feature_starttls(response): xmpp_state = XMPPState.START_TLS else: count_connecting_time = 0 xmpp_state = XMPPState.MISSING_TLS elif xmpp_state == XMPPState.START_TLS: if tcp_peer.get_status() != StreamPeerTCP.STATUS_CONNECTED: xmpp_state = XMPPState.NONE count_connecting_time = 0 return if count_connecting_time >= MAX_WAIT_CONNECTING: count_connecting_time = 0 xmpp_state = XMPPState.NONE else: count_connecting_time += delta if (tcp_peer.has_method("poll")): tcp_peer.poll() if tcp_peer.get_available_bytes()>0: var response = tcp_peer.get_string(tcp_peer.get_available_bytes()) send_msg_debug.emit("Stream: response: " + response) response = remove_stream_header(response) if response.begins_with("= MAX_WAIT_CONNECTING: count_connecting_time = 0 xmpp_state = XMPPState.MISSING_TLS else: count_connecting_time += delta else: count_connecting_time = 0 xmpp_state = XMPPState.MISSING_TLS elif xmpp_state == XMPPState.STARTED_TLS: if tcp_peer.get_status() != StreamPeerTCP.STATUS_CONNECTED: xmpp_state = XMPPState.NONE count_connecting_time = 0 return if ssl_peer.get_status() != StreamPeerTLS.STATUS_CONNECTED: xmpp_state = XMPPState.NONE count_connecting_time = 0 return if count_connecting_time >= MAX_WAIT_CONNECTING: count_connecting_time = 0 send_msg_debug.emit("send_ssl_initialize_xmpp") send_ssl_initialize_xmpp() else: count_connecting_time += delta if (ssl_peer.has_method("poll")): ssl_peer.poll() if ssl_peer.get_available_bytes()>0: var response = ssl_peer.get_string(ssl_peer.get_available_bytes()) send_msg_debug.emit("Stream: response: " + response) response = remove_stream_header(response) if not analyze_error(response): if analyze_feature_mechanisms(response): if authentication_methods.size() > 0: count_connecting_time = MAX_WAIT_CONNECTING xmpp_state = XMPPState.SELECT_MECHANISM_AUTHENTICATE #send_msg_debug.emit("AUTHENTICATE_STEP_1") # elif response.begins_with("= MAX_WAIT_CONNECTING: count_connecting_time = 0 negotiate_ssl_authenticate_plain() else: count_connecting_time += delta if (ssl_peer.has_method("poll")): ssl_peer.poll() if ssl_peer.get_available_bytes()>0: var response = ssl_peer.get_string(ssl_peer.get_available_bytes()) send_msg_debug.emit("Stream: response: " + response) response = remove_stream_header(response) var res:SASLReturn = get_result_authenticate(response) if res == SASLReturn.SUCCESS: count_connecting_time = 0 xmpp_state = XMPPState.AUTHENTICATED elif res == SASLReturn.NOT_AUTHORIZED: count_connecting_time = 0 xmpp_state = XMPPState.NOT_AUTHORIZED elif res == SASLReturn.ACCOUNT_DISABLED: count_connecting_time = 0 xmpp_state = XMPPState.ACCOUNT_DISABLED elif res == SASLReturn.NOT_AUTHORIZED: count_connecting_time = 0 xmpp_state = XMPPState.SELECT_MECHANISM_AUTHENTICATE banned_mechanism_authenticate.append(selected_mechanism_authenticate) elif res == SASLReturn.INVALID_MECHANISM: count_connecting_time = 0 xmpp_state = XMPPState.SELECT_MECHANISM_AUTHENTICATE banned_mechanism_authenticate.append(selected_mechanism_authenticate) else: count_connecting_time = 0 xmpp_state = XMPPState.SELECT_MECHANISM_AUTHENTICATE banned_mechanism_authenticate.append(selected_mechanism_authenticate) func get_result_authenticate(response:String) -> SASLReturn: response = remove_stream_header(response) if response.begins_with(""): return SASLReturn.ABORTED elif response.contains(""): return SASLReturn.ACCOUNT_DISABLED if response.contains(""): return SASLReturn.CREDENTIALS_EXPIRED if response.contains(""): return SASLReturn.ENCRYPTION_REQUIRED if response.contains(""): return SASLReturn.INCORRECT_ENCODING if response.contains(""): return SASLReturn.INVALID_AUTHZID if response.contains(""): return SASLReturn.INVALID_MECHANISM if response.contains(""): return SASLReturn.MALFORMED_REQUEST if response.contains(""): return SASLReturn.MECHANISM_TOO_WEAK if response.contains(""): return SASLReturn.NOT_AUTHORIZED if response.contains(""): return SASLReturn.TEMPORARY_AUTH_FAILURE return SASLReturn.UNKNOWN func analyze_error(response:String) -> bool: if response.begins_with(" bool: if response.begins_with(" bool: if response.begins_with(" 0): var tmp:String = "" var sep:String = "" for item in authentication_methods: tmp += sep + item sep = ", " send_msg_debug.emit("Stream: authentication methods: " + tmp) #negotiate_ssl_sasl(authentication_methods) #stream_status = self.StreamState.AUTHENTICATE return true #negotiate_ssl_sasl(authentication_methods) return false func send_tcp_initialize_xmpp() -> void: """ Send the stream header. Needs to be done several times over stream negotiation. """ var message:String = "" + \ " " # " from=\"" + account_name + "\" " + \ # + locale + "'" + print(message) send_tcp_string(message) func send_ssl_initialize_xmpp() -> void: """ Send the stream header. Needs to be done several times over stream negotiation. """ var server_name = account_name.split("@")[-1] var message:String = "" + \ " " # " from=\"" + account_name + "\" " + \ # + locale + "'" + print(message) send_ssl_string(message) func select_mechanism_authenticate(authentication_methods : Array) -> void: selected_mechanism_authenticate = "NONE" for item in order_preference_mechanism_authenticate: if banned_mechanism_authenticate.has(item): continue elif authentication_methods.has(item): selected_mechanism_authenticate = item break func negotiate_ssl_authenticate_plain() -> void: send_msg_debug.emit("Stream: sending request for plain") var msg:PackedByteArray = PackedByteArray() msg.push_back(0) var t = account_name.split("@")[0] msg += t.to_ascii_buffer() #msg += conv_string_to_PackedByteArray(account_name.split("@")[0]) msg.push_back(0) msg += password.to_ascii_buffer() var auth_account:String = Marshalls.raw_to_base64(msg) var request_sasl:String = "" + auth_account + "" send_ssl_string(request_sasl) func negotiate_ssl_sasl(authentication_methods : Array) -> bool: if ( authentication_methods.has("PLAIN")): send_msg_debug.emit("Stream: sending request for plain") var msg:PackedByteArray = PackedByteArray() msg.push_back(0) var t = account_name.split("@")[0] msg += t.to_ascii_buffer() #msg += conv_string_to_PackedByteArray(account_name.split("@")[0]) msg.push_back(0) msg += password.to_ascii_buffer() var auth_account:String = Marshalls.raw_to_base64(msg) var request_sasl:String = "" + auth_account + "" send_ssl_string(request_sasl) return true else: send_msg_error.emit("Impossible to authenticate (unknown protocol)") return false # # # func connect_to_server_xmpp() -> void: var res = tcp_peer.connect_to_host(server_xmpp_name, port_number) if res == OK: count_connecting_time = 0 stream_status = self.StreamState.END set_process(true) tgt_peer = tcp_peer func _process_old(delta) -> void: print(server_xmpp_name, ":", port_number, " / get_status:" , tcp_peer.get_status(), " / count_connecting_time:", count_connecting_time, " / stream_status:", stream_status) if tgt_peer.get_status() == StreamPeerTCP.STATUS_CONNECTED: print("STATUS_CONNECTED:", delta) if (tgt_peer.has_method("poll")): tgt_peer.poll() if tgt_peer.get_available_bytes()>0: var response = tgt_peer.get_string(tgt_peer.get_available_bytes()) send_msg_debug.emit("Stream: response: " + response) if stream_status == self.StreamState.STANZA: #collect_stanza(remove_stream_header(response)) print("collect_stanza") else: print("stream_process") stream_process(remove_stream_header(response)) # print(remove_stream_header(response)) elif stream_status == self.StreamState.END: reinit_stream() #start_stream() #stream_status = self.StreamState.START count_connecting_time = 0 if tgt_peer.get_status() == StreamPeerTCP.STATUS_CONNECTING: print("STATUS_CONNECTING:", delta) count_connecting_time += delta if (tgt_peer.has_method("poll")): tgt_peer.poll() if count_connecting_time > 60: # (1 -> 1s) if it took more than 1s to connect, error print("**** Stream: Stuck connecting, will now disconnect") Global.msg_error("Stream: Stuck connecting, will now disconnect", []) send_msg_debug.emit("Stream: Stuck connecting, will now disconnect") tgt_peer.disconnect_from_host() #interrupts connection to nothing set_process(false) # stop listening for packets try_connect = 20 stream_status = self.StreamState.END reinit_stream() if tgt_peer.get_status() == StreamPeerTCP.STATUS_NONE and xmppclient: print("connect_to_server:", xmppclient) connect_to_server() xmppclient = false func connect_to_server(): """ Connect to the server ip and port, and start checking whether there's stream info yet. """ if tcp_peer.get_status() == StreamPeerTCP.STATUS_CONNECTED: pass if try_connect == 0 and tcp_peer.get_status() == StreamPeerTCP.STATUS_NONE: #print("-> ", server_ip, ":", port_number, "/" , tcp_peer.get_status()) var res = tcp_peer.connect_to_host(server_xmpp_name, port_number) if res == OK: count_connecting_time = 0 stream_status = self.StreamState.END set_process(true) tgt_peer = tcp_peer else: try_connect = 20 else: try_connect -= 1 func send_string(stanza:String) ->void: """ Send a string in the appropriate encoding. """ send_msg_debug.emit("Sending data: '%'".format([stanza], "%")) tgt_peer.put_data(stanza.to_utf8_buffer()) func send_tcp_string(stanza:String) ->void: """ Send a string in the appropriate encoding. """ send_msg_debug.emit("Sending data: '%'".format([stanza], "%")) tcp_peer.put_data(stanza.to_utf8_buffer()) func send_ssl_string(stanza:String) ->void: """ Send a string in the appropriate encoding. """ send_msg_debug.emit("Sending data: '%'".format([stanza], "%")) ssl_peer.put_data(stanza.to_utf8_buffer()) func conv_string_to_PackedByteArray(message:String) -> PackedByteArray: return message.to_ascii_buffer() #func send_PackedByteArray(message:PackedByteArray) -> void: # """ Send a PackedByteArray """ # send_msg_debug.emit("Sending data: '" + message.get_string_from_utf8() + "'") # tgt_peer.put_data(message) func start_stream() -> void: """ Send the stream header. Needs to be done several times over stream negotiation. """ var server_name = account_name.split("@")[-1] var message:String = "" + \ " " # " from=\"" + account_name + "\" " + \ # + locale + "'" + print(message) send_string(message) func remove_stream_header(text :String) -> String: var index = 0 if text.begins_with("") text = text.substr(index+2).strip_edges() # strip stream header var rg = RegEx.new() rg.compile("<\\s?(stream|stream:stream)\\s") var result = rg.search(text) if result: send_msg_debug.emit("Stream: Response header received") index = text.find(">", result.get_end()) text = text.substr(index+1) return text func stream_process(response :String = "") -> void: """ Try to authenticate using SASL. We can currently only do plain. For SCRAM based methods, we need HMAC at the very least. """ if (!response.length() == 0 and stream_status != self.StreamState.STANZA): if response.begins_with(" 0): var tmp:String = "" var sep:String = "" for item in authentication_methods: tmp += sep + item sep = ", " send_msg_debug.emit("Stream: authentication methods: " + tmp) negotiate_sasl(authentication_methods) stream_status = self.StreamState.AUTHENTICATE else: if stream_status == StreamState.TLS: if response.begins_with(" bool: #var ssl_peer = StreamPeerTLS.new() # I am unsure how to validate the ssl certificate for an unknown host? #var ssl_succes = FAILED var options:TLSOptions = TLSOptions.client_unsafe() print("accept_stream") #var ssl_succes = ssl_peer.accept_stream(tcp_peer, options) var ssl_succes = ssl_peer.connect_to_stream(tcp_peer, "localhost", options) print("resultat:", ssl_succes) if ssl_succes == OK: send_msg_debug.emit("Stream: switched to SSL!") print("get_status:", ssl_peer.get_status()) # Wait connection done while ssl_peer.get_status() == StreamPeerTLS.STATUS_HANDSHAKING: ssl_peer.poll() print("get_status:", ssl_peer.get_status()) tgt_peer = ssl_peer start_stream() print("get_status:", ssl_peer.get_status()) print("-----") #stream_status == StreamState.START return true else: send_msg_error.emit("SSL failed, error %".format([ssl_succes], "%")) return false func negotiate_sasl(authentication_methods : Array) -> void: if ( authentication_methods.has("PLAIN")): send_msg_debug.emit("Stream: sending request for plain") var msg:PackedByteArray = PackedByteArray() msg.push_back(0) var t = account_name.split("@")[0] msg += t.to_ascii_buffer() #msg += conv_string_to_PackedByteArray(account_name.split("@")[0]) msg.push_back(0) msg += password.to_ascii_buffer() var auth_account:String = Marshalls.raw_to_base64(msg) var request_sasl:String = "" + auth_account + "" send_string(request_sasl) else: send_msg_error.emit("Impossible to authenticate (unknown protocol)") end_stream() func end_stream() -> void: """ End the stream """ send_msg_debug.emit("Stream: Ending stream") send_string("") if tcp_peer.has_method("disconnect_from_stream"): tcp_peer.disconnect_from_stream() else: tcp_peer.disconnect_from_host() set_process(false) stream_status = StreamState.END