another.im-ios/AnotherXMPP/modules/session/SessionModule.swift
2024-12-18 04:51:41 +01:00

167 lines
4.9 KiB
Swift

// RFC - 6120
import Foundation
enum SessionState: Codable & Equatable {
case waitingSRVRecords
case tryingConnect
case readyToStreamInit
case streamActive
}
// TODO: add stream errors processing
final class SessionModule: XmppModule {
let id = "Session module"
private var reqId = ""
func reduce(oldState: ClientState, with event: Event) -> ClientState {
var newState = oldState
switch event {
case .startClientLogin(let jid, let credsId):
newState.jid = jid
newState.credentialsId = credsId
case .domainResolved:
newState.sessionState = .tryingConnect
case .tryConnect:
newState.srvRecordIndex += 1
case .allRecordsUnreachable:
newState.srvRecords = []
newState.srvRecordIndex = -1
newState.sessionState = .waitingSRVRecords
case .socketConnected(let type):
newState.socketType = type
newState.sessionState = .readyToStreamInit
if type == .directTls {
newState.isSocketSecured = true
}
case .startTlsFailed:
newState.sessionState = .tryingConnect
newState.isSocketSecured = false
case .startTlsDone:
newState.sessionState = .readyToStreamInit
newState.isSocketSecured = true
case .authDone(let saslType, let args):
switch saslType {
case .sasl1:
newState.sessionState = .readyToStreamInit
case .sasl2:
if let authId = args["authorization-identifier"], let newJid = try? JID(authId) {
newState.jid = newJid
}
}
case .bindStreamDone(let jidStr):
if let jid = try? JID(jidStr) {
newState.jid = jid
newState.isStreamBound = true
}
case .bindStreamError:
newState.isStreamBound = false // TODO: implement good error handling
default:
break
}
return newState
}
func process(state: ClientState, with event: Event) async -> Event? {
switch (event, state.sessionState) {
case (.startClientLogin, .waitingSRVRecords):
return .resolveDomain
case (.domainResolved, .tryingConnect):
return .tryConnect
case (.socketError, .tryingConnect):
return .tryConnect
case (.socketConnected, .readyToStreamInit):
return .startStream
case (.startStream, .readyToStreamInit):
let req = XMLElement(
name: "stream:stream",
xmlns: nil,
attributes: [
"from": state.jid.bare,
"to": state.jid.domainPart,
"xml:lang": "en",
"version": "1.0",
"xmlns": "jabber:client",
"xmlns:stream": "http://etherx.jabber.org/streams"
],
content: nil,
nodes: [],
woClose: true
)
return .xmlOutbound(req)
case (.startTlsFailed, _):
// try reconnect with another srv record if starttls failed
return .tryConnect
case (.startTlsDone, _):
return .startStream
case (.authDone, _):
return .startStream
// Stream binding
case (.xmlInbound(let xml), _):
if !state.isStreamBound, xml.name == "stream:features", xml.nodes.map({ $0.name }).contains("bind") {
let reqXml = XMLElement(
name: "bind",
xmlns: "urn:ietf:params:xml:ns:xmpp-bind",
attributes: [:],
content: nil,
nodes: []
)
if let request = Stanza.iqSet(payload: reqXml), let id = request.id {
reqId = id
return .stanzaOutbound(request)
} else {
return nil
}
} else {
return nil
}
case (.stanzaInbound(let stanza), _):
guard stanza.id == reqId else { return nil }
switch stanza.type {
case .iq(.result):
let jid = stanza.wrapped
.nodes
.first(where: { $0.name == "bind" })?
.nodes
.first(where: { $0.name == "jid" })?
.content
if let jid {
return .bindStreamDone(jid)
} else {
return nil
}
default:
return .bindStreamError // TODO: implement good error handling
}
case (.bindStreamDone, _):
return .streamReady
default:
return nil
}
}
}