167 lines
4.9 KiB
Swift
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
|
|
}
|
|
}
|
|
}
|