342 lines
12 KiB
Swift
342 lines
12 KiB
Swift
// Copyright 2024 The NATS Authors
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
import Foundation
|
|
import NIO
|
|
|
|
internal struct NatsOperation: RawRepresentable, Hashable {
|
|
let rawValue: String
|
|
|
|
static let connect = NatsOperation(rawValue: "CONNECT")
|
|
static let subscribe = NatsOperation(rawValue: "SUB")
|
|
static let unsubscribe = NatsOperation(rawValue: "UNSUB")
|
|
static let publish = NatsOperation(rawValue: "PUB")
|
|
static let hpublish = NatsOperation(rawValue: "HPUB")
|
|
static let message = NatsOperation(rawValue: "MSG")
|
|
static let hmessage = NatsOperation(rawValue: "HMSG")
|
|
static let info = NatsOperation(rawValue: "INFO")
|
|
static let ok = NatsOperation(rawValue: "+OK")
|
|
static let error = NatsOperation(rawValue: "-ERR")
|
|
static let ping = NatsOperation(rawValue: "PING")
|
|
static let pong = NatsOperation(rawValue: "PONG")
|
|
|
|
var rawBytes: String.UTF8View {
|
|
self.rawValue.utf8
|
|
}
|
|
|
|
static func allOperations() -> [NatsOperation] {
|
|
return [
|
|
.connect, .subscribe, .unsubscribe, .publish, .message, .hmessage, .info, .ok, .error,
|
|
.ping, .pong,
|
|
]
|
|
}
|
|
}
|
|
|
|
enum ServerOp {
|
|
case ok
|
|
case info(ServerInfo)
|
|
case ping
|
|
case pong
|
|
case error(NatsError.ServerError)
|
|
case message(MessageInbound)
|
|
case hMessage(HMessageInbound)
|
|
|
|
static func parse(from msg: Data) throws -> ServerOp {
|
|
guard msg.count > 2 else {
|
|
throw NatsError.ProtocolError.parserFailure(
|
|
"unable to parse inbound message: \(String(data: msg, encoding: .utf8)!)")
|
|
}
|
|
guard let msgType = msg.getMessageType() else {
|
|
throw NatsError.ProtocolError.invalidOperation(String(data: msg, encoding: .utf8)!)
|
|
}
|
|
switch msgType {
|
|
case .message:
|
|
return try message(MessageInbound.parse(data: msg))
|
|
case .hmessage:
|
|
return try hMessage(HMessageInbound.parse(data: msg))
|
|
case .info:
|
|
return try info(ServerInfo.parse(data: msg))
|
|
case .ok:
|
|
return ok
|
|
case .error:
|
|
if let errMsg = msg.removePrefix(Data(NatsOperation.error.rawBytes)).toString() {
|
|
return error(NatsError.ServerError(errMsg))
|
|
}
|
|
return error(NatsError.ServerError("unexpected error"))
|
|
case .ping:
|
|
return ping
|
|
case .pong:
|
|
return pong
|
|
default:
|
|
throw NatsError.ProtocolError.invalidOperation(
|
|
"unknown server op: \(String(data: msg, encoding: .utf8)!)")
|
|
}
|
|
}
|
|
}
|
|
|
|
internal struct HMessageInbound: Equatable {
|
|
private static let newline = UInt8(ascii: "\n")
|
|
private static let space = UInt8(ascii: " ")
|
|
var subject: String
|
|
var sid: UInt64
|
|
var reply: String?
|
|
var payload: Data?
|
|
var headers: NatsHeaderMap
|
|
var headersLength: Int
|
|
var length: Int
|
|
var status: StatusCode?
|
|
var description: String?
|
|
|
|
// Parse the operation syntax: HMSG <subject> <sid> [reply-to]
|
|
internal static func parse(data: Data) throws -> HMessageInbound {
|
|
let protoComponents =
|
|
data
|
|
.dropFirst(NatsOperation.hmessage.rawValue.count) // Assuming msg starts with "HMSG "
|
|
.split(separator: space)
|
|
.filter { !$0.isEmpty }
|
|
|
|
let parseArgs: ((Data, Data, Data?, Data, Data) throws -> HMessageInbound) = {
|
|
subjectData, sidData, replyData, lengthHeaders, lengthData in
|
|
let subject = String(decoding: subjectData, as: UTF8.self)
|
|
guard let sid = UInt64(String(decoding: sidData, as: UTF8.self)) else {
|
|
throw NatsError.ProtocolError.parserFailure(
|
|
"unable to parse subscription ID as number")
|
|
}
|
|
var replySubject: String? = nil
|
|
if let replyData = replyData {
|
|
replySubject = String(decoding: replyData, as: UTF8.self)
|
|
}
|
|
let headersLength = Int(String(decoding: lengthHeaders, as: UTF8.self)) ?? 0
|
|
let length = Int(String(decoding: lengthData, as: UTF8.self)) ?? 0
|
|
return HMessageInbound(
|
|
subject: subject, sid: sid, reply: replySubject, payload: nil,
|
|
headers: NatsHeaderMap(),
|
|
headersLength: headersLength, length: length)
|
|
}
|
|
|
|
var msg: HMessageInbound
|
|
switch protoComponents.count {
|
|
case 4:
|
|
msg = try parseArgs(
|
|
protoComponents[0], protoComponents[1], nil, protoComponents[2],
|
|
protoComponents[3])
|
|
case 5:
|
|
msg = try parseArgs(
|
|
protoComponents[0], protoComponents[1], protoComponents[2], protoComponents[3],
|
|
protoComponents[4])
|
|
default:
|
|
throw NatsError.ProtocolError.parserFailure("unable to parse inbound message header")
|
|
}
|
|
return msg
|
|
}
|
|
}
|
|
|
|
// TODO(pp): add headers and HMSG parsing
|
|
internal struct MessageInbound: Equatable {
|
|
private static let newline = UInt8(ascii: "\n")
|
|
private static let space = UInt8(ascii: " ")
|
|
var subject: String
|
|
var sid: UInt64
|
|
var reply: String?
|
|
var payload: Data?
|
|
var length: Int
|
|
|
|
// Parse the operation syntax: MSG <subject> <sid> [reply-to]
|
|
internal static func parse(data: Data) throws -> MessageInbound {
|
|
let protoComponents =
|
|
data
|
|
.dropFirst(NatsOperation.message.rawValue.count) // Assuming msg starts with "MSG "
|
|
.split(separator: space)
|
|
.filter { !$0.isEmpty }
|
|
|
|
let parseArgs: ((Data, Data, Data?, Data) throws -> MessageInbound) = {
|
|
subjectData, sidData, replyData, lengthData in
|
|
let subject = String(decoding: subjectData, as: UTF8.self)
|
|
guard let sid = UInt64(String(decoding: sidData, as: UTF8.self)) else {
|
|
throw NatsError.ProtocolError.parserFailure(
|
|
"unable to parse subscription ID as number")
|
|
}
|
|
var replySubject: String? = nil
|
|
if let replyData = replyData {
|
|
replySubject = String(decoding: replyData, as: UTF8.self)
|
|
}
|
|
let length = Int(String(decoding: lengthData, as: UTF8.self)) ?? 0
|
|
return MessageInbound(
|
|
subject: subject, sid: sid, reply: replySubject, payload: nil, length: length)
|
|
}
|
|
|
|
var msg: MessageInbound
|
|
switch protoComponents.count {
|
|
case 3:
|
|
msg = try parseArgs(protoComponents[0], protoComponents[1], nil, protoComponents[2])
|
|
case 4:
|
|
msg = try parseArgs(
|
|
protoComponents[0], protoComponents[1], protoComponents[2], protoComponents[3])
|
|
default:
|
|
throw NatsError.ProtocolError.parserFailure("unable to parse inbound message header")
|
|
}
|
|
return msg
|
|
}
|
|
}
|
|
|
|
/// Struct representing server information in NATS.
|
|
struct ServerInfo: Codable, Equatable {
|
|
/// The unique identifier of the NATS server.
|
|
let serverId: String
|
|
/// Generated Server Name.
|
|
let serverName: String
|
|
/// The host specified in the cluster parameter/options.
|
|
let host: String
|
|
/// The port number specified in the cluster parameter/options.
|
|
let port: UInt16
|
|
/// The version of the NATS server.
|
|
let version: String
|
|
/// If this is set, then the server should try to authenticate upon connect.
|
|
let authRequired: Bool?
|
|
/// If this is set, then the server must authenticate using TLS.
|
|
let tlsRequired: Bool?
|
|
/// Maximum payload size that the server will accept.
|
|
let maxPayload: UInt
|
|
/// The protocol version in use.
|
|
let proto: Int8
|
|
/// The server-assigned client ID. This may change during reconnection.
|
|
let clientId: UInt64?
|
|
/// The version of golang the NATS server was built with.
|
|
let go: String
|
|
/// The nonce used for nkeys.
|
|
let nonce: String?
|
|
/// A list of server urls that a client can connect to.
|
|
let connectUrls: [String]?
|
|
/// The client IP as known by the server.
|
|
let clientIp: String
|
|
/// Whether the server supports headers.
|
|
let headers: Bool
|
|
/// Whether server goes into lame duck
|
|
private let _lameDuckMode: Bool?
|
|
var lameDuckMode: Bool {
|
|
return _lameDuckMode ?? false
|
|
}
|
|
|
|
private static let prefix = NatsOperation.info.rawValue.data(using: .utf8)!
|
|
|
|
private enum CodingKeys: String, CodingKey {
|
|
case serverId = "server_id"
|
|
case serverName = "server_name"
|
|
case host
|
|
case port
|
|
case version
|
|
case authRequired = "auth_required"
|
|
case tlsRequired = "tls_required"
|
|
case maxPayload = "max_payload"
|
|
case proto
|
|
case clientId = "client_id"
|
|
case go
|
|
case nonce
|
|
case connectUrls = "connect_urls"
|
|
case clientIp = "client_ip"
|
|
case headers
|
|
case _lameDuckMode = "ldm"
|
|
}
|
|
|
|
internal static func parse(data: Data) throws -> ServerInfo {
|
|
let info = data.removePrefix(prefix)
|
|
return try JSONDecoder().decode(self, from: info)
|
|
}
|
|
}
|
|
|
|
enum ClientOp {
|
|
case publish((subject: String, reply: String?, payload: Data?, headers: NatsHeaderMap?))
|
|
case subscribe((sid: UInt64, subject: String, queue: String?))
|
|
case unsubscribe((sid: UInt64, max: UInt64?))
|
|
case connect(ConnectInfo)
|
|
case ping
|
|
case pong
|
|
}
|
|
|
|
/// Info to construct a CONNECT message.
|
|
struct ConnectInfo: Encodable {
|
|
/// Turns on +OK protocol acknowledgments.
|
|
var verbose: Bool
|
|
/// Turns on additional strict format checking, e.g. for properly formed
|
|
/// subjects.
|
|
var pedantic: Bool
|
|
/// User's JWT.
|
|
var userJwt: String?
|
|
/// Public nkey.
|
|
var nkey: String
|
|
/// Signed nonce, encoded to Base64URL.
|
|
var signature: String?
|
|
/// Optional client name.
|
|
var name: String
|
|
/// If set to `true`, the server (version 1.2.0+) will not send originating
|
|
/// messages from this connection to its own subscriptions. Clients should
|
|
/// set this to `true` only for server supporting this feature, which is
|
|
/// when proto in the INFO protocol is set to at least 1.
|
|
var echo: Bool
|
|
/// The implementation language of the client.
|
|
var lang: String
|
|
/// The version of the client.
|
|
var version: String
|
|
/// Sending 0 (or absent) indicates client supports original protocol.
|
|
/// Sending 1 indicates that the client supports dynamic reconfiguration
|
|
/// of cluster topology changes by asynchronously receiving INFO messages
|
|
/// with known servers it can reconnect to.
|
|
var natsProtocol: NatsProtocol
|
|
/// Indicates whether the client requires an SSL connection.
|
|
var tlsRequired: Bool
|
|
/// Connection username (if `auth_required` is set)
|
|
var user: String
|
|
/// Connection password (if auth_required is set)
|
|
var pass: String
|
|
/// Client authorization token (if auth_required is set)
|
|
var authToken: String
|
|
/// Whether the client supports the usage of headers.
|
|
var headers: Bool
|
|
/// Whether the client supports no_responders.
|
|
var noResponders: Bool
|
|
enum CodingKeys: String, CodingKey {
|
|
case verbose
|
|
case pedantic
|
|
case userJwt = "jwt"
|
|
case nkey
|
|
case signature = "sig" // Custom key name for JSON
|
|
case name
|
|
case echo
|
|
case lang
|
|
case version
|
|
case natsProtocol = "protocol"
|
|
case tlsRequired = "tls_required"
|
|
case user
|
|
case pass
|
|
case authToken = "auth_token"
|
|
case headers
|
|
case noResponders = "no_responders"
|
|
}
|
|
}
|
|
|
|
enum NatsProtocol: Encodable {
|
|
case original
|
|
case dynamic
|
|
|
|
func encode(to encoder: Encoder) throws {
|
|
var container = encoder.singleValueContainer()
|
|
|
|
switch self {
|
|
case .original:
|
|
try container.encode(0)
|
|
case .dynamic:
|
|
try container.encode(1)
|
|
}
|
|
}
|
|
}
|