From e839049f49ad2448b60c9d5e507aa4e3a67dad95 Mon Sep 17 00:00:00 2001 From: liangzhenying <867174124@qq.com> Date: Tue, 27 Jan 2026 09:40:31 +0800 Subject: [PATCH] =?utf8?q?=E6=96=B0=E5=A2=9E=E5=8C=97=E7=A7=91Ai?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit --- ShenYin/ShenYin/Launch/SceneDelegate.swift | 4 +- ShenYin/ShenYin/Sdk/LogManager.swift | 2 +- ShenYin/ShenYin/Tools/YLTools.swift | 97 + ShenYin/ShenYin/UI/Raisound/AesEncrypt.swift | 7 + ShenYin/ShenYin/UI/Raisound/MSVoiceChat.swift | 370 ++++ .../UI/Raisound/MSVoiceChatModel.swift | 64 + ShenYin/ShenYin/UI/Raisound/RSApi.swift | 122 ++ ShenYin/ShenYin/UI/Raisound/RSAuth.swift | 183 ++ ShenYin/ShenYin/UI/Raisound/RSChatVC.swift | 20 + ShenYin/ShenYin/UI/Raisound/RSManager.swift | 51 + .../ShenYin/UI/Raisound/SwiftWebSocket.swift | 1846 +++++++++++++++++ 11 files changed, 2763 insertions(+), 3 deletions(-) create mode 100644 ShenYin/ShenYin/UI/Raisound/AesEncrypt.swift create mode 100644 ShenYin/ShenYin/UI/Raisound/MSVoiceChat.swift create mode 100644 ShenYin/ShenYin/UI/Raisound/MSVoiceChatModel.swift create mode 100644 ShenYin/ShenYin/UI/Raisound/RSApi.swift create mode 100644 ShenYin/ShenYin/UI/Raisound/RSAuth.swift create mode 100644 ShenYin/ShenYin/UI/Raisound/RSChatVC.swift create mode 100644 ShenYin/ShenYin/UI/Raisound/RSManager.swift create mode 100644 ShenYin/ShenYin/UI/Raisound/SwiftWebSocket.swift diff --git a/ShenYin/ShenYin/Launch/SceneDelegate.swift b/ShenYin/ShenYin/Launch/SceneDelegate.swift index 13273e9..b55a14d 100644 --- a/ShenYin/ShenYin/Launch/SceneDelegate.swift +++ b/ShenYin/ShenYin/Launch/SceneDelegate.swift @@ -19,8 +19,8 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { App.shared.window = window // window?.rootViewController = BaseTC() // MapManager.shared.setupSdk() -// window?.rootViewController = UINavigationController(rootViewController: ReadyVC()) - AppLaunch.start() + window?.rootViewController = UINavigationController(rootViewController: RSChatVC()) +// AppLaunch.start() } func sceneDidDisconnect(_ scene: UIScene) { diff --git a/ShenYin/ShenYin/Sdk/LogManager.swift b/ShenYin/ShenYin/Sdk/LogManager.swift index f61e054..8735333 100644 --- a/ShenYin/ShenYin/Sdk/LogManager.swift +++ b/ShenYin/ShenYin/Sdk/LogManager.swift @@ -9,7 +9,7 @@ import Foundation /// 日记管理 class Logger { - static var isDebug: Bool = false + static var isDebug: Bool = true fileprivate static let shared = Logger() private var fileHandle:FileHandle? diff --git a/ShenYin/ShenYin/Tools/YLTools.swift b/ShenYin/ShenYin/Tools/YLTools.swift index 3e7abd7..21e7c7c 100644 --- a/ShenYin/ShenYin/Tools/YLTools.swift +++ b/ShenYin/ShenYin/Tools/YLTools.swift @@ -181,3 +181,100 @@ extension UIImage { return newImage } } +struct JSON { } + +extension Dictionary { + + func jsonString(options: JSONSerialization.WritingOptions = [.fragmentsAllowed]) -> String? { + guard JSONSerialization.isValidJSONObject(self) else { + Logger.debug("无效的 JSONObject") + return nil + } + guard let data = try? JSONSerialization.data(withJSONObject: self, options: options) else { return nil } + return data.string + } +} + +extension Array { + + func jsonString(options: JSONSerialization.WritingOptions = [.fragmentsAllowed]) -> String? { + guard JSONSerialization.isValidJSONObject(self) else { + Logger.debug("无效的 JSONObject") + return nil + } + guard let data = try? JSONSerialization.data(withJSONObject: self, options: options) else { return nil } + return data.string + } +} + +extension String { + + func jsonObject(options: JSONSerialization.ReadingOptions = [.mutableContainers]) -> Any? { + guard let data = self.data else { return nil } + return data.jsonObject(options: options) + } +} + +// MARK: - Extension Codable + +extension Encodable { + + /// JSON encode + var encode: Data? { + do { + return try JSONEncoder().encode(self) + } catch { Logger.debug(error) } + return nil + } + + var jsonString: String? { + encode?.string + } +} + +extension Decodable { + + /// JSON decode + static func decode(_ from: Data) -> Self? { + do { + return try JSONDecoder().decode(Self.self, from: from) + } catch { Logger.debug(error) } + return nil + } + + /// JSON decode + static func decode(_ from: String) -> Self? { + guard let data = from.data else { return nil } + return decode(data) + } +} + +extension JSON { + /// JSON decode + static func decode(_ decodable: T.Type, from data: Data) -> T? { + do { + return try JSONDecoder().decode(decodable.self, from: data) + } catch { Logger.debug(error) } + return nil + } + + /// JSON decode + static func decode(_ decodable: T.Type, from jsonString: String) -> T? { + guard let data = jsonString.data else { return nil } + return decode(decodable, from: data) + } +} + +extension Array where Element: Encodable { + /// JSON encode + var jsonData: Data? { + do { + return try JSONEncoder().encode(self) + } catch { Logger.debug(error) } + return nil + } + + var jsonString: String? { + encode?.string + } +} diff --git a/ShenYin/ShenYin/UI/Raisound/AesEncrypt.swift b/ShenYin/ShenYin/UI/Raisound/AesEncrypt.swift new file mode 100644 index 0000000..eec79ad --- /dev/null +++ b/ShenYin/ShenYin/UI/Raisound/AesEncrypt.swift @@ -0,0 +1,7 @@ +// +// AesEncrypt.swift +// ShenYin +// +// Created by liangshi on 2026/1/26. +// + diff --git a/ShenYin/ShenYin/UI/Raisound/MSVoiceChat.swift b/ShenYin/ShenYin/UI/Raisound/MSVoiceChat.swift new file mode 100644 index 0000000..53793b4 --- /dev/null +++ b/ShenYin/ShenYin/UI/Raisound/MSVoiceChat.swift @@ -0,0 +1,370 @@ +//// +//// MSVoiceChat.swift +//// MixSdk +//// +//// Created by incar on 2024/7/29. +//// +// +import Foundation + + +/// 语音文件识别回调 +//public typealias MSSttCallBack = (_ text: String?,_ error:MSError?) -> Void +///// 事件回调 +//public typealias MSSttEventCallBack = (_ model:MSSttEvent) -> Void + +/// 连接状态回调 +public typealias MSConnectStateCallBack = (_ state:MSConnectState) -> Void +/// 对话回调 +public typealias MSChatCallBack = (_ model: MSChatModel) -> Void +/// 语音回复回调 +public typealias MSVoiceChatTtsCallBack = (_ data: Data) -> Void +/// 事件回调 +public typealias MSVoiceChatEventCallBack = (_ model:MSVoiceChatEvent) -> Void + + +public class MSVoiceChat { + var socket:WebSocket? + var timer:Timer? + // /// 事件回调 + // public var eventCallBack:MSVoiceChatEventCallBack? + /// 对话回调 + public var callBack:MSChatCallBack? + /// 语音数据回调 + public var ttsCallBack:MSVoiceChatTtsCallBack? + + /// 连接状态回调 + public var connectStateCallBack:MSConnectStateCallBack? + + /// 当前处在的事件状态 + public var currentEvent:MSVoiceChatEvent? + /// 九天pid + public var jiutian_pid:String = "" + /// 九天did + public var jiutian_did:String = "" + /// 设备ID/用户ID + var deviceId:String = "" + + /// 任务id + var taskId:String = "" + var dialogId:String = "" + + /// 最后的聊天内容 + var lastChatModel:MSChatModel? + + /// 连接状态 + public var connectState:MSConnectState{ + if let s = socket, s.readyState == .open { + return .connected + } + else{ + return .disConnected + } + } + init(){ + + } + + /// 开始任务 + public func start(){ + openWebSocket() + setupHeartBeat() + } + + func setupHeartBeat(){ + timer?.invalidate() + timer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true) { [weak self] _ in + guard let self = self else {return} + if socket?.readyState == .open{ + self.socket?.send(text: "") + } + else{ + self.openWebSocket() + } + } + RunLoop.current.add(timer!, forMode: .common) + } + func closeWebSocket(){ + if connectState == .connected { + socket?.close(10100,reason: "正常结束") + } + if let model = lastChatModel,model.type != "end"{ + lastChatModel = nil + let endModel = MSChatModel() + endModel.type = "end" + callBack?(endModel) + } + currentEvent = nil; + } + /// 重置聊天对话 + public func reset(){ + closeWebSocket() + } + + + /// 开始说话,在发送语音数据前,一定要先调用该方法 + private func startSpeedch(){ + + let params:[String:Any] = + [ + "event": "start", + "payload": [ + "device_id": RSAuth.shared.deviceID, // 设备ID + "user_agent": "AudioClient/1.0", // 浏览器 UA + "simple_response": true, // true 只返回完整的LLM文本 默认false 返回增量LLM文本 + "outbound_attributes": [ // TTS输出音频配置 + "cfg": [ + "format": "pcm", // 音频输出格式 + "codec":"raw", // raw ogg (当 format 为 opus 默认使用 ogg 封装 codec使用 raw 为原始opus) + "sample_rate": 16000, // 音频输出采样率 + "bit_rate": 16, // 比特率 + "channel": 1, // 声道 1 单声道 + "volume": 100, // 音量 0-100 + "speech_rate": 0, // 语速 [-50,100] 默认 0 + "chunk_interval_ms": 100 // 配置每段音频入队间隔时间 100ms + ] + ], + "inbound_attributes": [ // ASR输入音频配置 + "disable_vad": false, // 开启vad 自动断句 (开启后可以有客户端处理回音消除后持续发送音频由服务器判断是否转写和发送LLM) + "vad_silence": 400, // 设置vad 静音阈值 (毫秒) 连续静音超过 阈值自动切换状态 + "interrupt_options": [ + "concatenate_results": true, // 是否在打断后拼接前后的ASR结果 + "thinking_time_only": true // 是否只在思考时间允许打断 思考时间(ASR结束--->第一帧音频合成) + // "keyword_interrupt": false, // 启用打断词功能 (打断词只有在思考以及之后才能打断) + // "interrupt_keywords": ["停止", "打断","打断一下", "等一下", "别说了", "打住"] // 配置打断词列表 + ], + "cfg": [ + "format": "pcm", // 音频输入格式 + "sample_rate": 16000, // 音频输入采样率 目前只支持 16000 + "bit_rate": 16, // 比特率 目前只支持 16bit + "channel": 1, // 声道 目前只支持单声道 + "enable_punctuation": true, // 开启标点 + "enable_itn": true, // 开启itn + "keywords": [ + "张三" + ] // 热词 + ] + ] + ] + ] + let cmd = params.jsonString() ?? "" + Logger.debug("send" ,cmd) + socket?.send(text: cmd) + } + /// 开始发送语音前发送 + public func beginSpeedch(){ + let params:[String:Any] = + [ + "event": "audio.create", + "payload": {} + ] + let cmd = params.jsonString() ?? "" + Logger.debug("send" ,cmd) + socket?.send(text: cmd) + } + + /// 结束说话 + public func endSpeedch(){ + let params:[String:Any] = + [ + "event": "audio.completed", + "payload": {} + ] + let cmd = params.jsonString() ?? "" + Logger.debug("send" ,cmd) + socket?.send(text: cmd) + } + + public func cancelSpeedch(){ + let params:[String:Any] = + [ + "event": "cancel", + "payload": {} + ] + let cmd = params.jsonString() ?? "" + Logger.debug("send" ,cmd) + socket?.send(text: cmd) + } + /// 发送内容 + /// - Parameter content: 内容 + public func send(content:String){ + requestToSpeak() + + let params:[String:Any] = [ + "header": [ + "trace_id": self.taskId, + "event": "RequestToRespond" + ], + "payload": [ + "dialog_id": dialogId, + "type": "prompt", // prompt 直接将文本提交给大模型 transcript 直接将文本转成音频 + "text": content, + "session_id": self.taskId + ] + ] + + let cmd = params.jsonString() ?? "" + Logger.debug("send" ,cmd) + socket?.send(text: cmd) + } + + /// 打断,请求说话 + public func requestToSpeak(){ + if socket?.readyState == .open{ + sendSpeakCMD() + } + else{ + //还没连接 + Logger.debug("还没连接!!!!!!!!!!!") + self.openWebSocket() + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + self.sendSpeakCMD() + } + } + } + + func sendSpeakCMD(){ + Logger.debug("开始请求说话") + let params:[String:Any] = + [ + "header":[ + "trace_id":self.taskId, + "event":"RequestToSpeak" + ], + "payload":[ + "dialog_id":dialogId, + "session_id": self.taskId, + ] + ] + let cmd = params.jsonString() ?? "" + Logger.debug("send" ,cmd) + socket?.send(text: cmd) + + let tmpModel = MSChatModel() + tmpModel.type = "start" + tmpModel.content = nil + lastChatModel = tmpModel + callBack?(tmpModel) + } + /// 本地开始播放 + public func startPlay(){ + let params:[String:Any] = + [ + "header":[ + "trace_id":self.taskId, + "event":"LocalRespondingStarted" + ], + "payload":[ + "dialog_id":dialogId, + "session_id": self.taskId, + ] + ] + let cmd = params.jsonString() ?? "" + Logger.debug("send" ,cmd) + socket?.send(text: cmd) + } + + /// 本地结束播放 + public func endPlay(){ + let params:[String:Any] = + [ + "event": "audio.played", + "payload": {} + ] + let cmd = params.jsonString() ?? "" + Logger.debug("send" ,cmd) + socket?.send(text: cmd) + + } + /// 发送语音数据,format pcm,采样率 16000 + /// - Parameter data: 语音数据 + func sendVoice(pcmData:Data){ + if !RSAuth.shared.deviceToken.isEmpty { + Logger.debug("还未鉴权") + return + } + Logger.debug("send \(pcmData)") + socket?.send(data: pcmData) + } +} + +extension MSVoiceChat{ + private func openWebSocket(){ + taskId = "\(Int(Date().timeIntervalSince1970)*1000)".md5().lowercased() + currentEvent = nil; + + + let url = RSApi.wsBaseURL + "token=\(RSAuth.shared.deviceToken)&dt=\(RSAuth.shared.deviceID)" + + + Logger.debug(url) + socket = WebSocket(url) + socket?.delegate = self + } +} + + +extension MSVoiceChat:WebSocketDelegate{ + func webSocketOpen() { + Logger.debug("MSVoiceChat webSocketOpen") + connectStateCallBack?(.connected) + startSpeedch() + // startTask() + } + + func webSocketClose(_ code: Int, reason: String, wasClean: Bool) { + Logger.debug("MSVoiceChat webSocketClose",code,reason,wasClean) + // connectStateCallBack?(.disConnected) + } + + func webSocketError(_ error: NSError) { + Logger.debug("MSVoiceChat webSocketError",error) + } + + func webSocketMessageText(_ text: String) { + Logger.debug("MSVoiceChat receive",text) + guard let data = text.data,let model = JSON.decode(MSVoiceChatEvent.self, from: data) else{ + return + } + if model.event == "TaskFailed"{ + closeWebSocket() + return + } + else if model.event == "Started",let dialogId = model.payload?.dialog_id{ + self.dialogId = dialogId + } + else if model.event == "DialogStateChanged"{ + currentEvent = model + } + + if model.event == "RespondingContent",let payload = model.payload,let text = payload.text{ + if let finish = payload.finished,finish == false { + let tmpModel = MSChatModel() + tmpModel.type = "middle" + tmpModel.content = text + lastChatModel = tmpModel + callBack?(tmpModel) + } + else{ + let tmpModel = MSChatModel() + tmpModel.type = "end" + tmpModel.content = nil + lastChatModel = tmpModel + callBack?(tmpModel) + } + } + } + + func webSocketMessageData(_ data: Data){ + // Logger.debug("MSVoiceChat receive data ",data) + ttsCallBack?(data) + } + + func webSocketPong(){ + Logger.debug("MSVoiceChat Pong") + } + + func webSocketEnd(_ code: Int, reason: String, wasClean: Bool, error: NSError?){ + Logger.debug("MSVoiceChat webSocketEnd",code,reason,wasClean,error ?? "") + } +} diff --git a/ShenYin/ShenYin/UI/Raisound/MSVoiceChatModel.swift b/ShenYin/ShenYin/UI/Raisound/MSVoiceChatModel.swift new file mode 100644 index 0000000..f3ff160 --- /dev/null +++ b/ShenYin/ShenYin/UI/Raisound/MSVoiceChatModel.swift @@ -0,0 +1,64 @@ +// +// MSVoiceChatModel.swift +// ShenYin +// +// Created by liangshi on 2026/1/26. +// + +import Foundation +/// 连接类型 +public enum MSConnectState: Int { + /// 已连接 + case connected + /// 未连接 + case disConnected +} +/// +/// voiceChat事件 +public class MSVoiceChatEvent:Codable{ + /// 事件 + /// Started + /// DialogStateChanged + /// SpeechContent + /// RespondingContent + /// RespondingStarted + /// RespondingEnded + /// LocalRespondingStarted + /// LocalRespondingEnded + /// TaskFailed + public var event:String = "" + /// + public var msg:String = "" + /// 状态码 20000000 + public var status:Int = 0 + /// 任务id + public var task_id:String = "" + /// 内容 + public var payload:MSVoiceChatPayLoad? +} + +public class MSVoiceChatPayLoad:Codable{ + /// 会话id + public var dialog_id:String? + /// Listening 可以说话 + /// Thinking 正在思考 + /// Responding 正在回复 + public var state:String? + + /// 是否结束 + public var finished:Bool? + /// 识别的文字 + public var text:String? = "" +} + +/// 对话模型 +public class MSChatModel:Codable{ + /// 类型 start middle end + public var type:String = "" + /// 内容 + public var content:String? + + /// 深度思考,deepseek才有 + public var reasoning_content:String? + +} diff --git a/ShenYin/ShenYin/UI/Raisound/RSApi.swift b/ShenYin/ShenYin/UI/Raisound/RSApi.swift new file mode 100644 index 0000000..dc3b69c --- /dev/null +++ b/ShenYin/ShenYin/UI/Raisound/RSApi.swift @@ -0,0 +1,122 @@ +// +// RSApi.swift +// ShenYin +// +// Created by liangshi on 2026/1/26. +// + +//8FFDCB2185D5、8FFDCB2185D7、8FFDCB2185D8、8FFDCB2185D9 + +import Foundation +import HandyJSON +import SwiftHTTP + +class RSApi { + static let baseUrl = "https://app-api.raisound.com" + static let wsBaseURL = "wss://amp.raisound.com/vc/agent?" + + static func request(api: String,params:[String:Any],method:RequestMethod,completionHandler:@escaping ((T, String) -> Void)){ + let url = baseUrl + api + Logger.debug("request => ",url ) + Logger.debug(YLToos.jsonToString(params)) + + if method == .get { + HTTP.GET(url,parameters: params,requestSerializer: JSONParameterSerializer()){ response in + handleResponse(response, completionHandler: completionHandler) + } + } + else if method == .post{ + HTTP.POST(url,parameters: params,requestSerializer: JSONParameterSerializer()){ response in + handleResponse(response, completionHandler: completionHandler) + } + } + else if method == .put{ + HTTP.PUT(url,parameters: params,requestSerializer: JSONParameterSerializer()){ response in + handleResponse(response, completionHandler: completionHandler) + } + } + else if method == .delete{ + HTTP.DELETE(url,parameters: params,requestSerializer: JSONParameterSerializer()){ response in + handleResponse(response, completionHandler: completionHandler) + } + } + } +} + + +extension RSApi{ + private static func handleResponse(_ response: Response, completionHandler: @escaping ((T, String) -> Void)) { + DispatchQueue.main.sync { + guard let dict = receiveData(response) else{ + completionHandler(T(),"请求失败".l10n) + return + } + let responseStr = response.data.string ?? "" + if let res = T.deserialize(from: dict){ + completionHandler(res,responseStr) + return + } + completionHandler(T(),"请求失败".l10n) + + } + } + private static func handleResponse1(_ data: Data?,error:Error?, completionHandler: @escaping ((T, String) -> Void)) { + DispatchQueue.main.sync { + if let error = error { + Logger.debug(error) + completionHandler(T(),"请求失败".l10n) + return + } + guard let data = data else{ + completionHandler(T(),"请求失败".l10n) + return + } + Logger.debug(data.string ?? "") + guard let dict = data.jsonObject() as? NSDictionary else{ + completionHandler(T(),"请求失败".l10n) + return + } + let responseStr = data.string ?? "" + if let res = T.deserialize(from: dict){ + completionHandler(res,responseStr) + return + } + completionHandler(T(),"请求失败".l10n) + } + } + + static func receiveData(_ response:Response) ->(NSDictionary?){ + if let _ = response.error { + Logger.debug(response.description) + return (nil) + } + Logger.debug(response.data.string ?? "") + guard let dict = response.data.jsonObject() as? NSDictionary else{ + return (nil) + } + return (dict) + } + + static func receiveData(_ data:Data?, _ response:URLResponse?,_ error:Error?) ->(NSDictionary?){ + if let _ = error { + Logger.debug(response?.description ?? "") + return (nil) + } + + guard let data = data,let dict = data.jsonObject() as? NSDictionary else{ + Logger.debug(response?.description ?? "") + return (nil) + } + Logger.debug(data.string ?? "") + return (dict) + } + static func readFile(from path:String) ->Data?{ + let fileURL = URL(fileURLWithPath: path) + do { + let data = try Data(contentsOf:fileURL) + return data + } catch { + return nil + } + } +} diff --git a/ShenYin/ShenYin/UI/Raisound/RSAuth.swift b/ShenYin/ShenYin/UI/Raisound/RSAuth.swift new file mode 100644 index 0000000..0771d30 --- /dev/null +++ b/ShenYin/ShenYin/UI/Raisound/RSAuth.swift @@ -0,0 +1,183 @@ +// +// RSAuth.swift +// ShenYin +// +// Created by liangshi on 2026/1/26. +// + +/// 注册返回模型 +public class MSRegisterModel{ + /// 设备ID + public var deviceId:String = "" + /// 设备密码 + public var password:String = "" + /// 16个字符的随机字符串 + public var nonce:String = "" +} + +import SwiftHTTP + +/// 注册回调 +public typealias MSRegisterCallBack = (_ password: String?,_ error:MSError?) -> Void +/// 登录回调 +public typealias MSLoginCallBack = (_ token: String?,_ error:MSError?) -> Void + +class RSAuth { + static let shared = RSAuth() + static let productId = "hESCubuDmTbYTAxvUugI" + static let productKey = "gSUeLgTFnQZMcySLy2TCrEfyPbQgarxa" + var deviceToken = "" + var deviceID = "" + + public func register(deviceId:String,completeHandler: @escaping MSRegisterCallBack){ + let timeStamp = "\(Int(Date().timeIntervalSince1970)*1000)" + let value = deviceId + RSAuth.productId + timeStamp + RSAuth.productKey + let signValue = value.yc.md5 + let params = [ + "pid":RSAuth.productId, + "deviceId":deviceId, + "signMethod":"MD5", + "sign":signValue, + "timeStamp":timeStamp, + ] + let url = RSApi.baseUrl + "/dev/auth/active" + Logger.debug("text = ",value) + Logger.debug("productKey = ",RSAuth.productKey) + Logger.debug("signValue = ",signValue) + Logger.debug(url,params) + + HTTP.PUT(url,parameters: params,requestSerializer: JSONParameterSerializer()){ response in + DispatchQueue.main.sync { + let (dict,error) = MSHttpManager.receiveData(response) + if let error = error { + completeHandler(nil,error) + return + } + guard let dict = dict else {return completeHandler(nil,error)} + if let result = dict["success"] as? Bool,result == true,let dataDict = dict["data"] as? [String:Any]{ + let model = MSRegisterModel() + model.deviceId = dataDict["deviceId"] as? String ?? "" + model.password = dataDict["password"] as? String ?? "" + model.nonce = dataDict["nonce"] as? String ?? "" + + /// 解密 + let key1 = RSAuth.productKey + let enCode1 = model.password + let iv1 = model.nonce + + let decode1 = AESEncryption.decrypt(enCode1, key: key1, iv: iv1) + completeHandler(decode1 ?? "",nil) + } + else{ + completeHandler(nil,MSError.reponseError(dict["code"] as? Int ?? 0,dict["msg"] as? String ?? "")) + } + } + } + } + + /// 登录 + /// - Parameters: + /// - productKey:产品Key + /// - deviceId: 设备ID + /// - deviceSecrect: 注册返回的密码 + /// - completeHandler: 结果回调 + public func login(deviceId:String,password:String,completeHandler: @escaping MSLoginCallBack){ + /// 加密 + let nonce = AESEncryption.generateRandomString() + let key = RSAuth.productKey + guard let pwd = AESEncryption.encrypt(password, key: key, iv: nonce) else { + completeHandler(nil,MSError.invalidRequest) + return + } + + let params = [ + "deviceId":deviceId, + "password":pwd, + "nonce":nonce, + ] + let url = RSApi.baseUrl + "/dev/auth/sign_in" + Logger.debug("productKey = \(RSAuth.productKey)","deviceId = \(deviceId)","password = \(password)") + + Logger.debug("text = ",password) + Logger.debug("key = ",key) + Logger.debug("nonce = ",nonce) + Logger.debug("sign = ",pwd) + Logger.debug(url,params) + +// MixSdk.shared.voiceChat.deviceId = deviceId + + HTTP.POST(url,parameters: params,requestSerializer: JSONParameterSerializer()){ response in + DispatchQueue.main.sync { + let (dict,error) = MSHttpManager.receiveData(response) + if let error = error { + completeHandler(nil,error) + return + } + guard let dict = dict else {return completeHandler(nil,error)} + + if let result = dict["success"] as? Bool,result == true,let dataDict = dict["data"] as? [String:Any]{ + self.deviceToken = dataDict["token"] as? String ?? "" + self.deviceID = deviceId + completeHandler(self.deviceToken,nil) + } + else{ + completeHandler(nil,MSError.reponseError(dict["code"] as? Int ?? 0,dict["msg"] as? String ?? "")) + } + } + } + } +} + +class MSHttpManager{ + + static func receiveData(_ response:Response) ->(NSDictionary?,MSError?){ + if let _ = response.error { + Logger.debug(response.description) + return (nil,MSError.invalidRequest) + } + Logger.debug(response.data.string ?? "") + guard let dict = response.data.jsonObject() as? NSDictionary else{ + return (nil,MSError.parsingError) + } + return (dict,nil) + } + + static func receiveData(_ data:Data?, _ response:URLResponse?,_ error:Error?) ->(NSDictionary?,MSError?){ + if let _ = error { + Logger.debug(response?.description ?? "") + return (nil,MSError.invalidRequest) + } + + guard let data = data,let dict = data.jsonObject() as? NSDictionary else{ + Logger.debug(response?.description ?? "") + return (nil,MSError.parsingError) + } + Logger.debug(data.string ?? "") + return (dict,nil) + } + +} +/// 错误类型 +public enum MSError: Error { + /// 无效的请求 + case invalidRequest + /// 解析出错 + case parsingError + /// 响应出错 + case reponseError(Int,String) + /// 未鉴权 + case unauthorised + /// 未知错误 + case unknow + + public var msg:String{ + switch self { + case .unauthorised: + return "未鉴权" + case .reponseError(_,let message): + return message + default: + return "请求失败" + } + } +} diff --git a/ShenYin/ShenYin/UI/Raisound/RSChatVC.swift b/ShenYin/ShenYin/UI/Raisound/RSChatVC.swift new file mode 100644 index 0000000..ba6159d --- /dev/null +++ b/ShenYin/ShenYin/UI/Raisound/RSChatVC.swift @@ -0,0 +1,20 @@ +// +// RSChatVC.swift +// ShenYin +// +// Created by liangshi on 2026/1/26. +// + +import UIKit +class RSChatVC:BaseVC{ + let tableView = UITableView(frame: CGRectZero, style: .plain) + var dataList:[Any] = [] + override func viewDidLoad() { + super.viewDidLoad() + } + override func setupViews() { + self.navBar.title = "客服".l10n + RSManager.shared.start() + } + +} diff --git a/ShenYin/ShenYin/UI/Raisound/RSManager.swift b/ShenYin/ShenYin/UI/Raisound/RSManager.swift new file mode 100644 index 0000000..15ac44c --- /dev/null +++ b/ShenYin/ShenYin/UI/Raisound/RSManager.swift @@ -0,0 +1,51 @@ +// +// RSManager.swift +// ShenYin +// +// Created by liangshi on 2026/1/26. +// + +class RSManager{ + static let shared = RSManager() + let deviceId = "8FFDCB2185D5" + var chat:MSVoiceChat = MSVoiceChat() + + func start(){ + if let password = UserDefaults.standard.string(forKey: deviceId){ + startLogin(password: password) + } + else{ + registerAction() + } + } + func registerAction() { + RSAuth.shared.register( deviceId: deviceId){[weak self] password,error in + guard let self = self else{return} + if let password = password{ + print(password) + //deviceId和password APP要确保这个一一对应。 + UserDefaults.standard.setValue(password, forKey: self.deviceId) + startLogin(password: password) + } + else{ + Logger.debug(error!) + } + } + } + func startLogin(password:String){ + RSAuth.shared.login(deviceId: deviceId, password: password){[weak self] token,error in + if let err = error{ + if case .reponseError(let code,let message) = err,code == 50021{ + //50021是账号或密码错误.可以在这里调用注册,重新注册后,之前的密码会被覆盖,APP更新保存的新密码 + self?.registerAction() + } + } + else{ + Logger.debug(token ?? "") + self?.chat.start() + } + } + } + + +} diff --git a/ShenYin/ShenYin/UI/Raisound/SwiftWebSocket.swift b/ShenYin/ShenYin/UI/Raisound/SwiftWebSocket.swift new file mode 100644 index 0000000..68ccbe1 --- /dev/null +++ b/ShenYin/ShenYin/UI/Raisound/SwiftWebSocket.swift @@ -0,0 +1,1846 @@ +/* +* SwiftWebSocket (websocket.swift) +* +* Copyright (C) Josh Baker. All Rights Reserved. +* Contact: @tidwall, joshbaker77@gmail.com +* +* This software may be modified and distributed under the terms +* of the MIT license. See the LICENSE file for details. +* +*/ + +import Foundation + +private let windowBufferSize = 0x2000 + +private class Payload { + var ptr : UnsafeMutableRawPointer + var cap : Int + var len : Int + init(){ + len = 0 + cap = windowBufferSize + ptr = malloc(cap) + } + deinit{ + free(ptr) + } + var count : Int { + get { + return len + } + set { + if newValue > cap { + while cap < newValue { + cap *= 2 + } + ptr = realloc(ptr, cap) + } + len = newValue + } + } + func append(_ bytes: UnsafePointer, length: Int){ + let prevLen = len + count = len+length + memcpy(ptr+prevLen, bytes, length) + } + var array : [UInt8] { + get { + var array = [UInt8](repeating: 0, count: count) + memcpy(&array, ptr, count) + return array + } + set { + count = 0 + append(newValue, length: newValue.count) + } + } + var nsdata : Data { + get { + return Data(bytes: ptr.assumingMemoryBound(to: UInt8.self), count: count) + } + set { + count = 0 + append((newValue as NSData).bytes.bindMemory(to: UInt8.self, capacity: newValue.count), length: newValue.count) + } + } + var buffer : UnsafeBufferPointer { + get { + return UnsafeBufferPointer(start: ptr.assumingMemoryBound(to: UInt8.self), count: count) + } + set { + count = 0 + append(newValue.baseAddress!, length: newValue.count) + } + } +} + +private enum OpCode : UInt8, CustomStringConvertible { + case `continue` = 0x0, text = 0x1, binary = 0x2, close = 0x8, ping = 0x9, pong = 0xA + var isControl : Bool { + switch self { + case .close, .ping, .pong: + return true + default: + return false + } + } + var description : String { + switch self { + case .`continue`: return "Continue" + case .text: return "Text" + case .binary: return "Binary" + case .close: return "Close" + case .ping: return "Ping" + case .pong: return "Pong" + } + } +} + +/// The WebSocketEvents struct is used by the events property and manages the events for the WebSocket connection. +struct WebSocketEvents { + /// An event to be called when the WebSocket connection's readyState changes to .Open; this indicates that the connection is ready to send and receive data. + var open : ()->() = {} + /// An event to be called when the WebSocket connection's readyState changes to .Closed. + var close : (_ code : Int, _ reason : String, _ wasClean : Bool)->() = {(code, reason, wasClean) in} + /// An event to be called when an error occurs. + var error : (_ error : Error)->() = {(error) in} + /// An event to be called when a message is received from the server. + var message : (_ data : Any)->() = {(data) in} + /// An event to be called when a pong is received from the server. + var pong : (_ data : Any)->() = {(data) in} + /// An event to be called when the WebSocket process has ended; this event is guarenteed to be called once and can be used as an alternative to the "close" or "error" events. + var end : (_ code : Int, _ reason : String, _ wasClean : Bool, _ error : Error?)->() = {(code, reason, wasClean, error) in} +} + +/// The WebSocketBinaryType enum is used by the binaryType property and indicates the type of binary data being transmitted by the WebSocket connection. +enum WebSocketBinaryType : CustomStringConvertible { + /// The WebSocket should transmit [UInt8] objects. + case uInt8Array + /// The WebSocket should transmit NSData objects. + case nsData + /// The WebSocket should transmit UnsafeBufferPointer objects. This buffer is only valid during the scope of the message event. Use at your own risk. + case uInt8UnsafeBufferPointer + var description : String { + switch self { + case .uInt8Array: return "UInt8Array" + case .nsData: return "NSData" + case .uInt8UnsafeBufferPointer: return "UInt8UnsafeBufferPointer" + } + } +} + +/// The WebSocketReadyState enum is used by the readyState property to describe the status of the WebSocket connection. +@objc enum WebSocketReadyState : Int, CustomStringConvertible { + /// The connection is not yet open. + case connecting = 0 + /// The connection is open and ready to communicate. + case open = 1 + /// The connection is in the process of closing. + case closing = 2 + /// The connection is closed or couldn't be opened. + case closed = 3 + fileprivate var isClosed : Bool { + switch self { + case .closing, .closed: + return true + default: + return false + } + } + /// Returns a string that represents the ReadyState value. + var description : String { + switch self { + case .connecting: return "Connecting" + case .open: return "Open" + case .closing: return "Closing" + case .closed: return "Closed" + } + } +} + +private let defaultMaxWindowBits = 15 +/// The WebSocketCompression struct is used by the compression property and manages the compression options for the WebSocket connection. + struct WebSocketCompression { + /// Used to accept compressed messages from the server. Default is true. + var on = false + /// request no context takeover. + var noContextTakeover = false + /// request max window bits. + var maxWindowBits = defaultMaxWindowBits +} + +/// The WebSocketService options are used by the services property and manages the underlying socket services. + struct WebSocketService : OptionSet { + typealias RawValue = UInt + var value: UInt = 0 + init(_ value: UInt) { self.value = value } + init(rawValue value: UInt) { self.value = value } + init(nilLiteral: ()) { self.value = 0 } + static var allZeros: WebSocketService { return self.init(0) } + static func fromMask(_ raw: UInt) -> WebSocketService { return self.init(raw) } + var rawValue: UInt { return self.value } + /// No services. + static var None: WebSocketService { return self.init(0) } + /// Allow socket to handle VoIP. + static var VoIP: WebSocketService { return self.init(1 << 0) } + /// Allow socket to handle video. + static var Video: WebSocketService { return self.init(1 << 1) } + /// Allow socket to run in background. + static var Background: WebSocketService { return self.init(1 << 2) } + /// Allow socket to handle voice. + static var Voice: WebSocketService { return self.init(1 << 3) } +} + +private let atEndDetails = "streamStatus.atEnd" +private let timeoutDetails = "The operation couldn’t be completed. Operation timed out" +private let timeoutDuration : CFTimeInterval = 30 + +enum WebSocketError : Error, CustomStringConvertible { + case memory + case needMoreInput + case invalidHeader + case invalidAddress + case network(String) + case libraryError(String) + case payloadError(String) + case protocolError(String) + case invalidResponse(String) + case invalidCompressionOptions(String) + var description : String { + switch self { + case .memory: return "Memory" + case .needMoreInput: return "NeedMoreInput" + case .invalidAddress: return "InvalidAddress" + case .invalidHeader: return "InvalidHeader" + case let .invalidResponse(details): return "InvalidResponse(\(details))" + case let .invalidCompressionOptions(details): return "InvalidCompressionOptions(\(details))" + case let .libraryError(details): return "LibraryError(\(details))" + case let .protocolError(details): return "ProtocolError(\(details))" + case let .payloadError(details): return "PayloadError(\(details))" + case let .network(details): return "Network(\(details))" + } + } + var details : String { + switch self { + case .invalidResponse(let details): return details + case .invalidCompressionOptions(let details): return details + case .libraryError(let details): return details + case .protocolError(let details): return details + case .payloadError(let details): return details + case .network(let details): return details + default: return "" + } + } +} + +private class UTF8 { + var text : String = "" + var count : UInt32 = 0 // number of bytes + var procd : UInt32 = 0 // number of bytes processed + var codepoint : UInt32 = 0 // the actual codepoint + var bcount = 0 + init() { text = "" } + func append(_ byte : UInt8) throws { + if count == 0 { + if byte <= 0x7F { + text.append(String(UnicodeScalar(byte))) + return + } + if byte == 0xC0 || byte == 0xC1 { + throw WebSocketError.payloadError("invalid codepoint: invalid byte") + } + if byte >> 5 & 0x7 == 0x6 { + count = 2 + } else if byte >> 4 & 0xF == 0xE { + count = 3 + } else if byte >> 3 & 0x1F == 0x1E { + count = 4 + } else { + throw WebSocketError.payloadError("invalid codepoint: frames") + } + procd = 1 + codepoint = (UInt32(byte) & (0xFF >> count)) << ((count-1) * 6) + return + } + if byte >> 6 & 0x3 != 0x2 { + throw WebSocketError.payloadError("invalid codepoint: signature") + } + codepoint += UInt32(byte & 0x3F) << ((count-procd-1) * 6) + if codepoint > 0x10FFFF || (codepoint >= 0xD800 && codepoint <= 0xDFFF) { + throw WebSocketError.payloadError("invalid codepoint: out of bounds") + } + procd += 1 + if procd == count { + if codepoint <= 0x7FF && count > 2 { + throw WebSocketError.payloadError("invalid codepoint: overlong") + } + if codepoint <= 0xFFFF && count > 3 { + throw WebSocketError.payloadError("invalid codepoint: overlong") + } + procd = 0 + count = 0 + text.append(String.init(describing: UnicodeScalar(codepoint)!)) + } + return + } + func append(_ bytes : UnsafePointer, length : Int) throws { + if length == 0 { + return + } + if count == 0 { + var ascii = true + for i in 0 ..< length { + if bytes[i] > 0x7F { + ascii = false + break + } + } + if ascii { + text += NSString(bytes: bytes, length: length, encoding: String.Encoding.ascii.rawValue)! as String + bcount += length + return + } + } + for i in 0 ..< length { + try append(bytes[i]) + } + bcount += length + } + var completed : Bool { + return count == 0 + } + static func bytes(_ string : String) -> [UInt8]{ + let data = string.data(using: String.Encoding.utf8)! + return [UInt8](UnsafeBufferPointer(start: (data as NSData).bytes.bindMemory(to: UInt8.self, capacity: data.count), count: data.count)) + } + static func string(_ bytes : [UInt8]) -> String{ + if let str = NSString(bytes: bytes, length: bytes.count, encoding: String.Encoding.utf8.rawValue) { + return str as String + } + return "" + } +} + +private class Frame { + var inflate = false + var code = OpCode.continue + var utf8 = UTF8() + var payload = Payload() + var statusCode = UInt16(0) + var finished = true + static func makeClose(_ statusCode: UInt16, reason: String) -> Frame { + let f = Frame() + f.code = .close + f.statusCode = statusCode + f.utf8.text = reason + return f + } + func copy() -> Frame { + let f = Frame() + f.code = code + f.utf8.text = utf8.text + f.payload.buffer = payload.buffer + f.statusCode = statusCode + f.finished = finished + f.inflate = inflate + return f + } +} + +private class Delegate : NSObject, StreamDelegate { + @objc func stream(_ aStream: Stream, handle eventCode: Stream.Event){ + manager.signal() + } +} + + +@_silgen_name("zlibVersion") private func zlibVersion() -> OpaquePointer +@_silgen_name("deflateInit2_") private func deflateInit2(_ strm : UnsafeMutableRawPointer, level : CInt, method : CInt, windowBits : CInt, memLevel : CInt, strategy : CInt, version : OpaquePointer, stream_size : CInt) -> CInt +@_silgen_name("deflateInit_") private func deflateInit(_ strm : UnsafeMutableRawPointer, level : CInt, version : OpaquePointer, stream_size : CInt) -> CInt +@_silgen_name("deflateEnd") private func deflateEnd(_ strm : UnsafeMutableRawPointer) -> CInt +@_silgen_name("deflate") private func deflate(_ strm : UnsafeMutableRawPointer, flush : CInt) -> CInt +@_silgen_name("inflateInit2_") private func inflateInit2(_ strm : UnsafeMutableRawPointer, windowBits : CInt, version : OpaquePointer, stream_size : CInt) -> CInt +@_silgen_name("inflateInit_") private func inflateInit(_ strm : UnsafeMutableRawPointer, version : OpaquePointer, stream_size : CInt) -> CInt +@_silgen_name("inflate") private func inflateG(_ strm : UnsafeMutableRawPointer, flush : CInt) -> CInt +@_silgen_name("inflateEnd") private func inflateEndG(_ strm : UnsafeMutableRawPointer) -> CInt + +private func zerror(_ res : CInt) -> Error? { + var err = "" + switch res { + case 0: return nil + case 1: err = "stream end" + case 2: err = "need dict" + case -1: err = "errno" + case -2: err = "stream error" + case -3: err = "data error" + case -4: err = "mem error" + case -5: err = "buf error" + case -6: err = "version error" + default: err = "undefined error" + } + return WebSocketError.payloadError("zlib: \(err): \(res)") +} + +private struct z_stream { + var next_in : UnsafePointer? = nil + var avail_in : CUnsignedInt = 0 + var total_in : CUnsignedLong = 0 + + var next_out : UnsafeMutablePointer? = nil + var avail_out : CUnsignedInt = 0 + var total_out : CUnsignedLong = 0 + + var msg : UnsafePointer? = nil + var state : OpaquePointer? = nil + + var zalloc : OpaquePointer? = nil + var zfree : OpaquePointer? = nil + var opaque : OpaquePointer? = nil + + var data_type : CInt = 0 + var adler : CUnsignedLong = 0 + var reserved : CUnsignedLong = 0 +} + +private class Inflater { + var windowBits = 0 + var strm = z_stream() + var tInput = [[UInt8]]() + var inflateEnd : [UInt8] = [0x00, 0x00, 0xFF, 0xFF] + var bufferSize = windowBufferSize + var buffer = malloc(windowBufferSize) + init?(windowBits : Int){ + if buffer == nil { + return nil + } + self.windowBits = windowBits + let ret = inflateInit2(&strm, windowBits: -CInt(windowBits), version: zlibVersion(), stream_size: CInt(MemoryLayout.size)) + if ret != 0 { + return nil + } + } + deinit{ + _ = inflateEndG(&strm) + free(buffer) + } + func inflate(_ bufin : UnsafePointer, length : Int, final : Bool) throws -> (p : UnsafeMutablePointer, n : Int){ + var buf = buffer + var bufsiz = bufferSize + var buflen = 0 + for i in 0 ..< 2{ + if i == 0 { + strm.avail_in = CUnsignedInt(length) + strm.next_in = UnsafePointer(bufin) + } else { + if !final { + break + } + strm.avail_in = CUnsignedInt(inflateEnd.count) + strm.next_in = UnsafePointer(inflateEnd) + } + while true { + strm.avail_out = CUnsignedInt(bufsiz) + strm.next_out = buf?.assumingMemoryBound(to: UInt8.self) + _ = inflateG(&strm, flush: 0) + let have = bufsiz - Int(strm.avail_out) + bufsiz -= have + buflen += have + if strm.avail_out != 0{ + break + } + if bufsiz == 0 { + bufferSize *= 2 + let nbuf = realloc(buffer, bufferSize) + if nbuf == nil { + throw WebSocketError.payloadError("memory") + } + buffer = nbuf + buf = buffer?.advanced(by: Int(buflen)) + bufsiz = bufferSize - buflen + } + } + } + return (buffer!.assumingMemoryBound(to: UInt8.self), buflen) + } +} + +private class Deflater { + var windowBits = 0 + var memLevel = 0 + var strm = z_stream() + var bufferSize = windowBufferSize + var buffer = malloc(windowBufferSize) + init?(windowBits : Int, memLevel : Int){ + if buffer == nil { + return nil + } + self.windowBits = windowBits + self.memLevel = memLevel + let ret = deflateInit2(&strm, level: 6, method: 8, windowBits: -CInt(windowBits), memLevel: CInt(memLevel), strategy: 0, version: zlibVersion(), stream_size: CInt(MemoryLayout.size)) + if ret != 0 { + return nil + } + } + deinit{ + _ = deflateEnd(&strm) + free(buffer) + } + /*func deflate(_ bufin : UnsafePointer, length : Int, final : Bool) -> (p : UnsafeMutablePointer, n : Int, err : NSError?){ + return (nil, 0, nil) + }*/ +} + +/// WebSocketDelegate is an Objective-C alternative to WebSocketEvents and is used to delegate the events for the WebSocket connection. +@objc protocol WebSocketDelegate { + /// A function to be called when the WebSocket connection's readyState changes to .Open; this indicates that the connection is ready to send and receive data. + func webSocketOpen() + /// A function to be called when the WebSocket connection's readyState changes to .Closed. + func webSocketClose(_ code: Int, reason: String, wasClean: Bool) + /// A function to be called when an error occurs. + func webSocketError(_ error: NSError) + /// A function to be called when a message (string) is received from the server. + @objc optional func webSocketMessageText(_ text: String) + /// A function to be called when a message (binary) is received from the server. + @objc optional func webSocketMessageData(_ data: Data) + /// A function to be called when a pong is received from the server. + @objc optional func webSocketPong() + /// A function to be called when the WebSocket process has ended; this event is guarenteed to be called once and can be used as an alternative to the "close" or "error" events. + @objc optional func webSocketEnd(_ code: Int, reason: String, wasClean: Bool, error: NSError?) +} + +/// WebSocket objects are bidirectional network streams that communicate over HTTP. RFC 6455. +private class InnerWebSocket: Hashable { + var id : Int + var mutex = pthread_mutex_t() + let request : URLRequest! + let subProtocols : [String]! + var frames : [Frame] = [] + var delegate : Delegate + var inflater : Inflater! + var deflater : Deflater! + var outputBytes : UnsafeMutablePointer? + var outputBytesSize : Int = 0 + var outputBytesStart : Int = 0 + var outputBytesLength : Int = 0 + var inputBytes : UnsafeMutablePointer? + var inputBytesSize : Int = 0 + var inputBytesStart : Int = 0 + var inputBytesLength : Int = 0 + var createdAt = CFAbsoluteTimeGetCurrent() + var connectionTimeout = false + var eclose : ()->() = {} + var _eventQueue : DispatchQueue? = DispatchQueue.main + var _subProtocol = "" + var _compression = WebSocketCompression() + var _allowSelfSignedSSL = false + var _services = WebSocketService.None + var _event = WebSocketEvents() + var _eventDelegate: WebSocketDelegate? + var _binaryType = WebSocketBinaryType.uInt8Array + var _readyState = WebSocketReadyState.connecting + var _networkTimeout = TimeInterval(-1) + + + var url : String { + return request.url!.description + } + var subProtocol : String { + get { return privateSubProtocol } + } + var privateSubProtocol : String { + get { lock(); defer { unlock() }; return _subProtocol } + set { lock(); defer { unlock() }; _subProtocol = newValue } + } + var compression : WebSocketCompression { + get { lock(); defer { unlock() }; return _compression } + set { lock(); defer { unlock() }; _compression = newValue } + } + var allowSelfSignedSSL : Bool { + get { lock(); defer { unlock() }; return _allowSelfSignedSSL } + set { lock(); defer { unlock() }; _allowSelfSignedSSL = newValue } + } + var services : WebSocketService { + get { lock(); defer { unlock() }; return _services } + set { lock(); defer { unlock() }; _services = newValue } + } + var event : WebSocketEvents { + get { lock(); defer { unlock() }; return _event } + set { lock(); defer { unlock() }; _event = newValue } + } + var eventDelegate : WebSocketDelegate? { + get { lock(); defer { unlock() }; return _eventDelegate } + set { lock(); defer { unlock() }; _eventDelegate = newValue } + } + var eventQueue : DispatchQueue? { + get { lock(); defer { unlock() }; return _eventQueue; } + set { lock(); defer { unlock() }; _eventQueue = newValue } + } + var binaryType : WebSocketBinaryType { + get { lock(); defer { unlock() }; return _binaryType } + set { lock(); defer { unlock() }; _binaryType = newValue } + } + var readyState : WebSocketReadyState { + get { return privateReadyState } + } + var privateReadyState : WebSocketReadyState { + get { lock(); defer { unlock() }; return _readyState } + set { lock(); defer { unlock() }; _readyState = newValue } + } + + func copyOpen(_ request: URLRequest, subProtocols : [String] = []) -> InnerWebSocket{ + let ws = InnerWebSocket(request: request, subProtocols: subProtocols, stub: false) + ws.eclose = eclose + ws.compression = compression + ws.allowSelfSignedSSL = allowSelfSignedSSL + ws.services = services + ws.event = event + ws.eventQueue = eventQueue + ws.binaryType = binaryType + return ws + } + + var hashValue: Int { return id } + + func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + init(request: URLRequest, subProtocols : [String] = [], stub : Bool = false){ + pthread_mutex_init(&mutex, nil) + self.id = manager.nextId() + self.request = request + self.subProtocols = subProtocols + self.outputBytes = UnsafeMutablePointer.allocate(capacity: windowBufferSize) + self.outputBytesSize = windowBufferSize + self.inputBytes = UnsafeMutablePointer.allocate(capacity: windowBufferSize) + self.inputBytesSize = windowBufferSize + self.delegate = Delegate() + if stub{ + manager.queue.asyncAfter(deadline: DispatchTime.now() + Double(0) / Double(NSEC_PER_SEC)){ + _ = self + } + } else { + manager.queue.asyncAfter(deadline: DispatchTime.now() + Double(0) / Double(NSEC_PER_SEC)){ + manager.add(self) + } + } + } + deinit{ + if outputBytes != nil { + free(outputBytes) + } + if inputBytes != nil { + free(inputBytes) + } + pthread_mutex_init(&mutex, nil) + } + @inline(__always) fileprivate func lock(){ + pthread_mutex_lock(&mutex) + } + @inline(__always) fileprivate func unlock(){ + pthread_mutex_unlock(&mutex) + } + + fileprivate var dirty : Bool { + lock() + defer { unlock() } + if exit { + return false + } + if connectionTimeout { + return true + } + if stage != .readResponse && stage != .handleFrames { + return true + } + if rd.streamStatus == .opening && wr.streamStatus == .opening { + return false; + } + if rd.streamStatus != .open || wr.streamStatus != .open { + return true + } + if rd.streamError != nil || wr.streamError != nil { + return true + } + if rd.hasBytesAvailable || frames.count > 0 || inputBytesLength > 0 { + return true + } + if outputBytesLength > 0 && wr.hasSpaceAvailable{ + return true + } + return false + } + enum Stage : Int { + case openConn + case readResponse + case handleFrames + case closeConn + case end + } + var stage = Stage.openConn + var rd : InputStream! + var wr : OutputStream! + var atEnd = false + var closeCode = UInt16(0) + var closeReason = "" + var closeClean = false + var closeFinal = false + var finalError : Error? + var exit = false + var more = true + func step(){ + if exit { + return + } + do { + try stepBuffers(more) + try stepStreamErrors() + more = false + switch stage { + case .openConn: + try openConn() + stage = .readResponse + case .readResponse: + try readResponse() + privateReadyState = .open + fire { + self.event.open() + self.eventDelegate?.webSocketOpen() + } + stage = .handleFrames + case .handleFrames: + try stepOutputFrames() + if closeFinal { + privateReadyState = .closing + stage = .closeConn + return + } + let frame = try readFrame() + switch frame.code { + case .text: + fire { + self.event.message(frame.utf8.text) + self.eventDelegate?.webSocketMessageText?(frame.utf8.text) + } + case .binary: + fire { + switch self.binaryType { + case .uInt8Array: + self.event.message(frame.payload.array) +// let data = Data(bytes: frame.payload.array, count: frame.payload.array.count) +// print(data) + self.eventDelegate?.webSocketMessageData?(frame.payload.nsdata) + case .nsData: + self.event.message(frame.payload.nsdata) + // The WebSocketDelegate is necessary to add Objective-C compability and it is only possible to send binary data with NSData. + self.eventDelegate?.webSocketMessageData?(frame.payload.nsdata) + case .uInt8UnsafeBufferPointer: + self.event.message(frame.payload.buffer) + } + } + case .ping: + let nframe = frame.copy() + nframe.code = .pong + lock() + frames += [nframe] + unlock() + case .pong: + fire { + switch self.binaryType { + case .uInt8Array: + self.event.pong(frame.payload.array) + case .nsData: + self.event.pong(frame.payload.nsdata) + case .uInt8UnsafeBufferPointer: + self.event.pong(frame.payload.buffer) + } + self.eventDelegate?.webSocketPong?() + } + case .close: + lock() + frames += [frame] + unlock() + default: + break + } + case .closeConn: + if let error = finalError { + self.event.error(error) + self.eventDelegate?.webSocketError(error as NSError) + } + privateReadyState = .closed + if rd != nil { + closeConn() + fire { + self.eclose() + self.event.close(Int(self.closeCode), self.closeReason, self.closeFinal) + self.eventDelegate?.webSocketClose(Int(self.closeCode), reason: self.closeReason, wasClean: self.closeFinal) + } + } + stage = .end + case .end: + fire { + self.event.end(Int(self.closeCode), self.closeReason, self.closeClean, self.finalError) + self.eventDelegate?.webSocketEnd?(Int(self.closeCode), reason: self.closeReason, wasClean: self.closeClean, error: self.finalError as NSError?) + } + exit = true + manager.remove(self) + } + } catch WebSocketError.needMoreInput { + more = true + } catch { + if finalError != nil { + return + } + finalError = error + if stage == .openConn || stage == .readResponse { + stage = .closeConn + } else { + var frame : Frame? + if let error = error as? WebSocketError{ + switch error { + case .network(let details): + if details == atEndDetails{ + stage = .closeConn + frame = Frame.makeClose(1006, reason: "Abnormal Closure") + atEnd = true + finalError = nil + } + case .protocolError: + frame = Frame.makeClose(1002, reason: "Protocol error") + case .payloadError: + frame = Frame.makeClose(1007, reason: "Payload error") + default: + break + } + } + if frame == nil { + frame = Frame.makeClose(1006, reason: "Abnormal Closure") + } + if let frame = frame { + if frame.statusCode == 1007 { + self.lock() + self.frames = [frame] + self.unlock() + manager.signal() + } else { + manager.queue.asyncAfter(deadline: DispatchTime.now() + Double(0) / Double(NSEC_PER_SEC)){ + self.lock() + self.frames += [frame] + self.unlock() + manager.signal() + } + } + } + } + } + } + func stepBuffers(_ more: Bool) throws { + if rd != nil { + if stage != .closeConn && rd.streamStatus == Stream.Status.atEnd { + if atEnd { + return; + } + throw WebSocketError.network(atEndDetails) + } + if more { + while rd.hasBytesAvailable { + var size = inputBytesSize + while size-(inputBytesStart+inputBytesLength) < windowBufferSize { + size *= 2 + } + if size > inputBytesSize { + let ptr = realloc(inputBytes, size) + if ptr == nil { + throw WebSocketError.memory + } + inputBytes = ptr?.assumingMemoryBound(to: UInt8.self) + inputBytesSize = size + } + let n = rd.read(inputBytes!+inputBytesStart+inputBytesLength, maxLength: inputBytesSize-inputBytesStart-inputBytesLength) + if n > 0 { + inputBytesLength += n + } + } + } + } + if wr != nil && wr.hasSpaceAvailable && outputBytesLength > 0 { + let n = wr.write(outputBytes!+outputBytesStart, maxLength: outputBytesLength) + if n > 0 { + outputBytesLength -= n + if outputBytesLength == 0 { + outputBytesStart = 0 + } else { + outputBytesStart += n + } + } + } + } + func stepStreamErrors() throws { + if finalError == nil { + if connectionTimeout { + throw WebSocketError.network(timeoutDetails) + } + if let error = rd?.streamError { + throw WebSocketError.network(error.localizedDescription) + } + if let error = wr?.streamError { + throw WebSocketError.network(error.localizedDescription) + } + } + } + func stepOutputFrames() throws { + lock() + defer { + frames = [] + unlock() + } + if !closeFinal { + for frame in frames { + try writeFrame(frame) + if frame.code == .close { + closeCode = frame.statusCode + closeReason = frame.utf8.text + closeFinal = true + return + } + } + } + } + @inline(__always) func fire(_ block: ()->()){ + if let queue = eventQueue { + queue.sync { + block() + } + } else { + block() + } + } + + var readStateSaved = false + var readStateFrame : Frame? + var readStateFinished = false + var leaderFrame : Frame? + func readFrame() throws -> Frame { + var frame : Frame + var finished : Bool + if !readStateSaved { + if leaderFrame != nil { + frame = leaderFrame! + finished = false + leaderFrame = nil + } else { + frame = try readFrameFragment(nil) + finished = frame.finished + } + if frame.code == .continue{ + throw WebSocketError.protocolError("leader frame cannot be a continue frame") + } + if !finished { + readStateSaved = true + readStateFrame = frame + readStateFinished = finished + throw WebSocketError.needMoreInput + } + } else { + frame = readStateFrame! + finished = readStateFinished + if !finished { + let cf = try readFrameFragment(frame) + finished = cf.finished + if cf.code != .continue { + if !cf.code.isControl { + throw WebSocketError.protocolError("only ping frames can be interlaced with fragments") + } + leaderFrame = frame + return cf + } + if !finished { + readStateSaved = true + readStateFrame = frame + readStateFinished = finished + throw WebSocketError.needMoreInput + } + } + } + if !frame.utf8.completed { + throw WebSocketError.payloadError("incomplete utf8") + } + readStateSaved = false + readStateFrame = nil + readStateFinished = false + return frame + } + + func closeConn() { + rd.remove(from: RunLoop.main, forMode: RunLoop.Mode.default) + wr.remove(from: RunLoop.main, forMode: RunLoop.Mode.default) + rd.delegate = nil + wr.delegate = nil + rd.close() + wr.close() + } + + func openConn() throws { + var req = request! + req.setValue("websocket", forHTTPHeaderField: "Upgrade") + req.setValue("Upgrade", forHTTPHeaderField: "Connection") + if req.value(forHTTPHeaderField: "User-Agent") == nil { + req.setValue("SwiftWebSocket", forHTTPHeaderField: "User-Agent") + } + req.setValue("13", forHTTPHeaderField: "Sec-WebSocket-Version") + + if req.url == nil || req.url!.host == nil{ + throw WebSocketError.invalidAddress + } + if req.url!.port == nil || req.url!.port! == 80 || req.url!.port! == 443 { + req.setValue(req.url!.host!, forHTTPHeaderField: "Host") + } else { + req.setValue("\(req.url!.host!):\(req.url!.port!)", forHTTPHeaderField: "Host") + } + let origin = req.value(forHTTPHeaderField: "Origin") + if origin == nil || origin! == ""{ + req.setValue(req.url!.absoluteString, forHTTPHeaderField: "Origin") + } + if subProtocols.count > 0 { + req.setValue(subProtocols.joined(separator: ","), forHTTPHeaderField: "Sec-WebSocket-Protocol") + } + if req.url!.scheme != "wss" && req.url!.scheme != "ws" { + throw WebSocketError.invalidAddress + } + if compression.on { + var val = "permessage-deflate" + if compression.noContextTakeover { + val += "; client_no_context_takeover; server_no_context_takeover" + } + val += "; client_max_window_bits" + if compression.maxWindowBits != 0 { + val += "; server_max_window_bits=\(compression.maxWindowBits)" + } + req.setValue(val, forHTTPHeaderField: "Sec-WebSocket-Extensions") + } + + let security: TCPConnSecurity + let port : Int + if req.url!.scheme == "wss" { + port = req.url!.port ?? 443 + security = .negoticatedSSL + } else { + port = req.url!.port ?? 80 + security = .none + } + + var path = CFURLCopyPath(req.url! as CFURL) as String + if path == "" { + path = "/" + } + if let q = req.url!.query { + if q != "" { + path += "?" + q + } + } + var reqs = "GET \(path) HTTP/1.1\r\n" + for key in req.allHTTPHeaderFields!.keys { + if let val = req.value(forHTTPHeaderField: key) { + reqs += "\(key): \(val)\r\n" + } + } + var keyb = [UInt32](repeating: 0, count: 4) + for i in 0 ..< 4 { + keyb[i] = arc4random() + } + let rkey = Data(bytes: UnsafePointer(keyb), count: 16).base64EncodedString(options: NSData.Base64EncodingOptions(rawValue: 0)) + reqs += "Sec-WebSocket-Key: \(rkey)\r\n" + reqs += "\r\n" + var header = [UInt8]() + for b in reqs.utf8 { + header += [b] + } + let addr = ["\(req.url!.host!)", "\(port)"] + if addr.count != 2 || Int(addr[1]) == nil { + throw WebSocketError.invalidAddress + } + + var (rdo, wro) : (InputStream?, OutputStream?) + var readStream: Unmanaged? + var writeStream: Unmanaged? + CFStreamCreatePairWithSocketToHost(nil, addr[0] as CFString, UInt32(Int(addr[1])!), &readStream, &writeStream); + rdo = readStream!.takeRetainedValue() + wro = writeStream!.takeRetainedValue() + (rd, wr) = (rdo!, wro!) + rd.setProperty(security.level, forKey: Stream.PropertyKey.socketSecurityLevelKey) + wr.setProperty(security.level, forKey: Stream.PropertyKey.socketSecurityLevelKey) + if services.contains(.VoIP) { + rd.setProperty(StreamNetworkServiceTypeValue.voIP.rawValue, forKey: Stream.PropertyKey.networkServiceType) + wr.setProperty(StreamNetworkServiceTypeValue.voIP.rawValue, forKey: Stream.PropertyKey.networkServiceType) + } + if services.contains(.Video) { + rd.setProperty(StreamNetworkServiceTypeValue.video.rawValue, forKey: Stream.PropertyKey.networkServiceType) + wr.setProperty(StreamNetworkServiceTypeValue.video.rawValue, forKey: Stream.PropertyKey.networkServiceType) + } + if services.contains(.Background) { + rd.setProperty(StreamNetworkServiceTypeValue.background.rawValue, forKey: Stream.PropertyKey.networkServiceType) + wr.setProperty(StreamNetworkServiceTypeValue.background.rawValue, forKey: Stream.PropertyKey.networkServiceType) + } + if services.contains(.Voice) { + rd.setProperty(StreamNetworkServiceTypeValue.voice.rawValue, forKey: Stream.PropertyKey.networkServiceType) + wr.setProperty(StreamNetworkServiceTypeValue.voice.rawValue, forKey: Stream.PropertyKey.networkServiceType) + } + if allowSelfSignedSSL { + let prop: Dictionary = [kCFStreamSSLPeerName: kCFNull, kCFStreamSSLValidatesCertificateChain: NSNumber(value: false)] + rd.setProperty(prop, forKey: Stream.PropertyKey(rawValue: kCFStreamPropertySSLSettings as String as String)) + wr.setProperty(prop, forKey: Stream.PropertyKey(rawValue: kCFStreamPropertySSLSettings as String as String)) + } + rd.delegate = delegate + wr.delegate = delegate + rd.schedule(in: RunLoop.main, forMode: RunLoop.Mode.default) + wr.schedule(in: RunLoop.main, forMode: RunLoop.Mode.default) + rd.open() + wr.open() + try write(header, length: header.count) + } + + func write(_ bytes: UnsafePointer, length: Int) throws { + if outputBytesStart+outputBytesLength+length > outputBytesSize { + var size = outputBytesSize + while outputBytesStart+outputBytesLength+length > size { + size *= 2 + } + let ptr = realloc(outputBytes, size) + if ptr == nil { + throw WebSocketError.memory + } + outputBytes = ptr?.assumingMemoryBound(to: UInt8.self) + outputBytesSize = size + } + memcpy(outputBytes!+outputBytesStart+outputBytesLength, bytes, length) + outputBytesLength += length + } + + func readResponse() throws { + let end : [UInt8] = [ 0x0D, 0x0A, 0x0D, 0x0A ] + let ptr = memmem(inputBytes!+inputBytesStart, inputBytesLength, end, 4) + if ptr == nil { + throw WebSocketError.needMoreInput + } + let buffer = inputBytes!+inputBytesStart + let bufferCount = ptr!.assumingMemoryBound(to: UInt8.self)-(inputBytes!+inputBytesStart) + let string = NSString(bytesNoCopy: buffer, length: bufferCount, encoding: String.Encoding.utf8.rawValue, freeWhenDone: false) as String? + if string == nil { + throw WebSocketError.invalidHeader + } + let header = string! + var needsCompression = false + var serverMaxWindowBits = 15 + let clientMaxWindowBits = 15 + var key = "" + let trim : (String)->(String) = { (text) in return text.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)} + let eqval : (String,String)->(String) = { (line, del) in return trim(line.components(separatedBy: del)[1]) } + let lines = header.components(separatedBy: "\r\n") + for i in 0 ..< lines.count { + let line = trim(lines[i]) + if i == 0 { + if !line.hasPrefix("HTTP/1.1 101"){ + throw WebSocketError.invalidResponse(line) + } + } else if line != "" { + var value = "" + if line.hasPrefix("\t") || line.hasPrefix(" ") { + value = trim(line) + } else { + key = "" + if let r = line.range(of: ":") { + key = trim(String(line[.. 15 { + throw WebSocketError.invalidCompressionOptions("server_max_window_bits") + } + if serverMaxWindowBits < 8 || serverMaxWindowBits > 15 { + throw WebSocketError.invalidCompressionOptions("client_max_window_bits") + } + inflater = Inflater(windowBits: serverMaxWindowBits) + if inflater == nil { + throw WebSocketError.invalidCompressionOptions("inflater init") + } + deflater = Deflater(windowBits: clientMaxWindowBits, memLevel: 8) + if deflater == nil { + throw WebSocketError.invalidCompressionOptions("deflater init") + } + } + inputBytesLength -= bufferCount+4 + if inputBytesLength == 0 { + inputBytesStart = 0 + } else { + inputBytesStart += bufferCount+4 + } + } + + class ByteReader { + var start : UnsafePointer + var end : UnsafePointer + var bytes : UnsafePointer + init(bytes: UnsafePointer, length: Int){ + self.bytes = bytes + start = bytes + end = bytes+length + } + func readByte() throws -> UInt8 { + if bytes >= end { + throw WebSocketError.needMoreInput + } + let b = bytes.pointee + bytes += 1 + return b + } + var length : Int { + return end - bytes + } + var position : Int { + get { + return bytes - start + } + set { + bytes = start + newValue + } + } + } + + var fragStateSaved = false + var fragStatePosition = 0 + var fragStateInflate = false + var fragStateLen = 0 + var fragStateFin = false + var fragStateCode = OpCode.continue + var fragStateLeaderCode = OpCode.continue + var fragStateUTF8 = UTF8() + var fragStatePayload = Payload() + var fragStateStatusCode = UInt16(0) + var fragStateHeaderLen = 0 + var buffer = [UInt8](repeating: 0, count: windowBufferSize) + var reusedPayload = Payload() + func readFrameFragment(_ leader : Frame?) throws -> Frame { + var inflate : Bool + var len : Int + var fin = false + var code : OpCode + var leaderCode : OpCode + var utf8 : UTF8 + var payload : Payload + var statusCode : UInt16 + var headerLen : Int + var leader = leader + + let reader = ByteReader(bytes: inputBytes!+inputBytesStart, length: inputBytesLength) + if fragStateSaved { + // load state + reader.position += fragStatePosition + inflate = fragStateInflate + len = fragStateLen + fin = fragStateFin + code = fragStateCode + leaderCode = fragStateLeaderCode + utf8 = fragStateUTF8 + payload = fragStatePayload + statusCode = fragStateStatusCode + headerLen = fragStateHeaderLen + fragStateSaved = false + } else { + var b = try reader.readByte() + fin = b >> 7 & 0x1 == 0x1 + let rsv1 = b >> 6 & 0x1 == 0x1 + let rsv2 = b >> 5 & 0x1 == 0x1 + let rsv3 = b >> 4 & 0x1 == 0x1 + if inflater != nil && (rsv1 || (leader != nil && leader!.inflate)) { + inflate = true + } else if rsv1 || rsv2 || rsv3 { + throw WebSocketError.protocolError("invalid extension") + } else { + inflate = false + } + code = OpCode.binary + if let c = OpCode(rawValue: (b & 0xF)){ + code = c + } else { + throw WebSocketError.protocolError("invalid opcode") + } + if !fin && code.isControl { + throw WebSocketError.protocolError("unfinished control frame") + } + b = try reader.readByte() + if b >> 7 & 0x1 == 0x1 { + throw WebSocketError.protocolError("server sent masked frame") + } + var len64 = Int64(b & 0x7F) + var bcount = 0 + if b & 0x7F == 126 { + bcount = 2 + } else if len64 == 127 { + bcount = 8 + } + if bcount != 0 { + if code.isControl { + throw WebSocketError.protocolError("invalid payload size for control frame") + } + len64 = 0 + var i = bcount-1 + while i >= 0 { + b = try reader.readByte() + len64 += Int64(b) << Int64(i*8) + i -= 1 + } + } + len = Int(len64) + if code == .continue { + if code.isControl { + throw WebSocketError.protocolError("control frame cannot have the 'continue' opcode") + } + if leader == nil { + throw WebSocketError.protocolError("continue frame is missing it's leader") + } + } + if code.isControl { + if leader != nil { + leader = nil + } + if inflate { + throw WebSocketError.protocolError("control frame cannot be compressed") + } + } + statusCode = 0 + if leader != nil { + leaderCode = leader!.code + utf8 = leader!.utf8 + payload = leader!.payload + } else { + leaderCode = code + utf8 = UTF8() + payload = reusedPayload + payload.count = 0 + } + if leaderCode == .close { + if len == 1 { + throw WebSocketError.protocolError("invalid payload size for close frame") + } + if len >= 2 { + let b1 = try reader.readByte() + let b2 = try reader.readByte() + statusCode = (UInt16(b1) << 8) + UInt16(b2) + len -= 2 + if statusCode < 1000 || statusCode > 4999 || (statusCode >= 1004 && statusCode <= 1006) || (statusCode >= 1012 && statusCode <= 2999) { + throw WebSocketError.protocolError("invalid status code for close frame") + } + } + } + headerLen = reader.position + } + + let rlen : Int + let rfin : Bool + let chopped : Bool + if reader.length+reader.position-headerLen < len { + rlen = reader.length + rfin = false + chopped = true + } else { + rlen = len-reader.position+headerLen + rfin = fin + chopped = false + } + let bytes : UnsafeMutablePointer + let bytesLen : Int + if inflate { + (bytes, bytesLen) = try inflater!.inflate(reader.bytes, length: rlen, final: rfin) + } else { + (bytes, bytesLen) = (UnsafeMutablePointer.init(mutating: reader.bytes), rlen) + } + reader.bytes += rlen + + if leaderCode == .text || leaderCode == .close { + try utf8.append(bytes, length: bytesLen) + } else { + payload.append(bytes, length: bytesLen) + } + + if chopped { + // save state + fragStateHeaderLen = headerLen + fragStateStatusCode = statusCode + fragStatePayload = payload + fragStateUTF8 = utf8 + fragStateLeaderCode = leaderCode + fragStateCode = code + fragStateFin = fin + fragStateLen = len + fragStateInflate = inflate + fragStatePosition = reader.position + fragStateSaved = true + throw WebSocketError.needMoreInput + } + + inputBytesLength -= reader.position + if inputBytesLength == 0 { + inputBytesStart = 0 + } else { + inputBytesStart += reader.position + } + + let f = Frame() + (f.code, f.payload, f.utf8, f.statusCode, f.inflate, f.finished) = (code, payload, utf8, statusCode, inflate, fin) + return f + } + + var head = [UInt8](repeating: 0, count: 0xFF) + func writeFrame(_ f : Frame) throws { + if !f.finished{ + throw WebSocketError.libraryError("cannot send unfinished frames") + } + var hlen = 0 + let b : UInt8 = 0x80 + var deflate = false + if deflater != nil { + if f.code == .binary || f.code == .text { + deflate = true + // b |= 0x40 + } + } + head[hlen] = b | f.code.rawValue + hlen += 1 + var payloadBytes : [UInt8] + var payloadLen = 0 + if f.utf8.text != "" { + payloadBytes = UTF8.bytes(f.utf8.text) + } else { + payloadBytes = f.payload.array + } + payloadLen += payloadBytes.count + if deflate { + + } + var usingStatusCode = false + if f.statusCode != 0 && payloadLen != 0 { + payloadLen += 2 + usingStatusCode = true + } + if payloadLen < 126 { + head[hlen] = 0x80 | UInt8(payloadLen) + hlen += 1 + } else if payloadLen <= 0xFFFF { + head[hlen] = 0x80 | 126 + hlen += 1 + var i = 1 + while i >= 0 { + head[hlen] = UInt8((UInt16(payloadLen) >> UInt16(i*8)) & 0xFF) + hlen += 1 + i -= 1 + } + } else { + head[hlen] = UInt8((0x1 << 7) + 127) + hlen += 1 + var i = 7 + while i >= 0 { + head[hlen] = UInt8((UInt64(payloadLen) >> UInt64(i*8)) & 0xFF) + hlen += 1 + i -= 1 + } + } + let r = arc4random() + var maskBytes : [UInt8] = [UInt8(r >> 0 & 0xFF), UInt8(r >> 8 & 0xFF), UInt8(r >> 16 & 0xFF), UInt8(r >> 24 & 0xFF)] + for i in 0 ..< 4 { + head[hlen] = maskBytes[i] + hlen += 1 + } + if payloadLen > 0 { + if usingStatusCode { + var sc = [UInt8(f.statusCode >> 8 & 0xFF), UInt8(f.statusCode >> 0 & 0xFF)] + for i in 0 ..< 2 { + sc[i] ^= maskBytes[i % 4] + } + head[hlen] = sc[0] + hlen += 1 + head[hlen] = sc[1] + hlen += 1 + for i in 2 ..< payloadLen { + payloadBytes[i-2] ^= maskBytes[i % 4] + } + } else { + for i in 0 ..< payloadLen { + payloadBytes[i] ^= maskBytes[i % 4] + } + } + } + try write(head, length: hlen) + try write(payloadBytes, length: payloadBytes.count) + } + func close(_ code : Int = 1000, reason : String = "Normal Closure") { + let f = Frame() + f.code = .close + f.statusCode = UInt16(truncatingIfNeeded: code) + f.utf8.text = reason + sendFrame(f) + } + func sendFrame(_ f : Frame) { + lock() + frames += [f] + unlock() + manager.signal() + } + func send(_ message : Any) { + let f = Frame() + if let message = message as? String { + f.code = .text + f.utf8.text = message + } else if let message = message as? [UInt8] { + f.code = .binary + f.payload.array = message + } else if let message = message as? UnsafeBufferPointer { + f.code = .binary + f.payload.append(message.baseAddress!, length: message.count) + } else if let message = message as? Data { + f.code = .binary + f.payload.nsdata = message + } else { + f.code = .text + f.utf8.text = "\(message)" + } + sendFrame(f) + } + func ping() { + let f = Frame() + f.code = .ping + sendFrame(f) + } + func ping(_ message : Any){ + let f = Frame() + f.code = .ping + if let message = message as? String { + f.payload.array = UTF8.bytes(message) + } else if let message = message as? [UInt8] { + f.payload.array = message + } else if let message = message as? UnsafeBufferPointer { + f.payload.append(message.baseAddress!, length: message.count) + } else if let message = message as? Data { + f.payload.nsdata = message + } else { + f.utf8.text = "\(message)" + } + sendFrame(f) + } +} +private func ==(lhs: InnerWebSocket, rhs: InnerWebSocket) -> Bool { + return lhs.id == rhs.id +} + +private enum TCPConnSecurity { + case none + case negoticatedSSL + + var level: String { + switch self { + case .none: return StreamSocketSecurityLevel.none.rawValue + case .negoticatedSSL: return StreamSocketSecurityLevel.negotiatedSSL.rawValue + } + } +} + +// Manager class is used to minimize the number of dispatches and cycle through network events +// using fewers threads. Helps tremendously with lowing system resources when many conncurrent +// sockets are opened. +private class Manager { + var queue = DispatchQueue(label: "SwiftWebSocketInstance", attributes: []) + var once = Int() + var mutex = pthread_mutex_t() + var cond = pthread_cond_t() + var websockets = Set() + var _nextId = 0 + init(){ + pthread_mutex_init(&mutex, nil) + pthread_cond_init(&cond, nil) + DispatchQueue(label: "SwiftWebSocket", attributes: []).async { + var wss : [InnerWebSocket] = [] + while true { + var wait = true + wss.removeAll() + pthread_mutex_lock(&self.mutex) + for ws in self.websockets { + wss.append(ws) + } + for ws in wss { + self.checkForConnectionTimeout(ws) + if ws.dirty { + pthread_mutex_unlock(&self.mutex) + ws.step() + pthread_mutex_lock(&self.mutex) + wait = false + } + } + if wait { + _ = self.wait(250) + } + pthread_mutex_unlock(&self.mutex) + } + } + } + func checkForConnectionTimeout(_ ws : InnerWebSocket) { + if ws.rd != nil && ws.wr != nil && (ws.rd.streamStatus == .opening || ws.wr.streamStatus == .opening) { + let age = CFAbsoluteTimeGetCurrent() - ws.createdAt + if age >= timeoutDuration { + ws.connectionTimeout = true + } + } + } + func wait(_ timeInMs : Int) -> Int32 { + var ts = timespec() + var tv = timeval() + gettimeofday(&tv, nil) + ts.tv_sec = time(nil) + timeInMs / 1000; + let v1 = Int(tv.tv_usec * 1000) + let v2 = Int(1000 * 1000 * Int(timeInMs % 1000)) + ts.tv_nsec = v1 + v2; + ts.tv_sec += ts.tv_nsec / (1000 * 1000 * 1000); + ts.tv_nsec %= (1000 * 1000 * 1000); + return pthread_cond_timedwait(&self.cond, &self.mutex, &ts) + } + func signal(){ + pthread_mutex_lock(&mutex) + pthread_cond_signal(&cond) + pthread_mutex_unlock(&mutex) + } + func add(_ websocket: InnerWebSocket) { + pthread_mutex_lock(&mutex) + websockets.insert(websocket) + pthread_cond_signal(&cond) + pthread_mutex_unlock(&mutex) + } + func remove(_ websocket: InnerWebSocket) { + pthread_mutex_lock(&mutex) + websockets.remove(websocket) + pthread_cond_signal(&cond) + pthread_mutex_unlock(&mutex) + } + func nextId() -> Int { + pthread_mutex_lock(&mutex) + defer { pthread_mutex_unlock(&mutex) } + _nextId += 1 + return _nextId + } +} + +private let manager = Manager() + +/// WebSocket objects are bidirectional network streams that communicate over HTTP. RFC 6455. +@objcMembers +class WebSocket: NSObject { + fileprivate var ws: InnerWebSocket + fileprivate var id = manager.nextId() + fileprivate var opened: Bool + + override var hash: Int { return id } + override func isEqual(_ other: Any?) -> Bool { + guard let other = other as? WebSocket else { return false } + return self.id == other.id + } + + /// Create a WebSocket connection to a URL; this should be the URL to which the WebSocket server will respond. + convenience init(_ url: String){ + self.init(request: URLRequest(url: URL(string: url)!), subProtocols: []) + } + /// Create a WebSocket connection to a URL; this should be the URL to which the WebSocket server will respond. + convenience init(url: URL){ + self.init(request: URLRequest(url: url), subProtocols: []) + } + /// Create a WebSocket connection to a URL; this should be the URL to which the WebSocket server will respond. Also include a list of protocols. + convenience init(_ url: String, subProtocols : [String]){ + self.init(request: URLRequest(url: URL(string: url)!), subProtocols: subProtocols) + } + /// Create a WebSocket connection to a URL; this should be the URL to which the WebSocket server will respond. Also include a protocol. + convenience init(_ url: String, subProtocol : String){ + self.init(request: URLRequest(url: URL(string: url)!), subProtocols: [subProtocol]) + } + /// Create a WebSocket connection from an NSURLRequest; Also include a list of protocols. + init(request: URLRequest, subProtocols : [String] = []){ + let hasURL = request.url != nil + opened = hasURL + ws = InnerWebSocket(request: request, subProtocols: subProtocols, stub: !hasURL) + super.init() + // weak/strong pattern from: + // http://stackoverflow.com/a/17105368/424124 + // https://dhoerl.wordpress.com/2013/04/23/i-finally-figured-out-weakself-and-strongself/ + ws.eclose = { [weak self] in + if let strongSelf = self { + strongSelf.opened = false + } + } + } + /// Create a WebSocket object with a deferred connection; the connection is not opened until the .open() method is called. + convenience override init(){ + var request = URLRequest(url: URL(string: "http://apple.com")!) + request.url = nil + self.init(request: request, subProtocols: []) + } + /// The URL as resolved by the constructor. This is always an absolute URL. Read only. + var url : String{ return ws.url } + /// A string indicating the name of the sub-protocol the server selected; this will be one of the strings specified in the protocols parameter when creating the WebSocket object. + var subProtocol : String{ return ws.subProtocol } + /// The compression options of the WebSocket. + var compression : WebSocketCompression{ + get { return ws.compression } + set { ws.compression = newValue } + } + /// Allow for Self-Signed SSL Certificates. Default is false. + var allowSelfSignedSSL : Bool{ + get { return ws.allowSelfSignedSSL } + set { ws.allowSelfSignedSSL = newValue } + } + /// The services of the WebSocket. + var services : WebSocketService{ + get { return ws.services } + set { ws.services = newValue } + } + /// The events of the WebSocket. + var event : WebSocketEvents{ + get { return ws.event } + set { ws.event = newValue } + } + /// The queue for firing off events. default is main_queue + var eventQueue : DispatchQueue?{ + get { return ws.eventQueue } + set { ws.eventQueue = newValue } + } + /// A WebSocketBinaryType value indicating the type of binary data being transmitted by the connection. Default is .UInt8Array. + var binaryType : WebSocketBinaryType{ + get { return ws.binaryType } + set { ws.binaryType = newValue } + } + /// The current state of the connection; this is one of the WebSocketReadyState constants. Read only. + var readyState : WebSocketReadyState{ + return ws.readyState + } + /// Opens a deferred or closed WebSocket connection to a URL; this should be the URL to which the WebSocket server will respond. + func open(_ url: String){ + open(request: URLRequest(url: URL(string: url)!), subProtocols: []) + } + /// Opens a deferred or closed WebSocket connection to a URL; this should be the URL to which the WebSocket server will respond. + func open(nsurl url: URL){ + open(request: URLRequest(url: url), subProtocols: []) + } + /// Opens a deferred or closed WebSocket connection to a URL; this should be the URL to which the WebSocket server will respond. Also include a list of protocols. + func open(_ url: String, subProtocols : [String]){ + open(request: URLRequest(url: URL(string: url)!), subProtocols: subProtocols) + } + /// Opens a deferred or closed WebSocket connection to a URL; this should be the URL to which the WebSocket server will respond. Also include a protocol. + func open(_ url: String, subProtocol : String){ + open(request: URLRequest(url: URL(string: url)!), subProtocols: [subProtocol]) + } + /// Opens a deferred or closed WebSocket connection from an NSURLRequest; Also include a list of protocols. + func open(request: URLRequest, subProtocols : [String] = []){ + if opened{ + return + } + opened = true + ws = ws.copyOpen(request, subProtocols: subProtocols) + } + /// Opens a closed WebSocket connection from an NSURLRequest; Uses the same request and protocols as previously closed WebSocket + func open(){ + open(request: ws.request, subProtocols: ws.subProtocols) + } + /** + Closes the WebSocket connection or connection attempt, if any. If the connection is already closed or in the state of closing, this method does nothing. + + :param: code An integer indicating the status code explaining why the connection is being closed. If this parameter is not specified, a default value of 1000 (indicating a normal closure) is assumed. + :param: reason A human-readable string explaining why the connection is closing. This string must be no longer than 123 bytes of UTF-8 text (not characters). + */ + func close(_ code : Int = 1000, reason : String = "Normal Closure"){ + if !opened{ + return + } + opened = false + ws.close(code, reason: reason) + } + /** + Transmits message to the server over the WebSocket connection. + + :param: message The message to be sent to the server. + */ + func send(_ message : Any){ + if !opened{ + return + } + ws.send(message) + } + /** + Transmits a ping to the server over the WebSocket connection. + + :param: optional message The data to be sent to the server. + */ + func ping(_ message : Any){ + if !opened{ + return + } + ws.ping(message) + } + /** + Transmits a ping to the server over the WebSocket connection. + */ + func ping(){ + if !opened{ + return + } + ws.ping() + } +} + + func ==(lhs: WebSocket, rhs: WebSocket) -> Bool { + return lhs.id == rhs.id +} + +extension WebSocket { + /// The events of the WebSocket using a delegate. + @objc + var delegate : WebSocketDelegate? { + get { return ws.eventDelegate } + set { ws.eventDelegate = newValue } + } + /** + Transmits message to the server over the WebSocket connection. + + :param: text The message (string) to be sent to the server. + */ + @objc + func send(text: String){ + send(text) + } + /** + Transmits message to the server over the WebSocket connection. + + :param: data The message (binary) to be sent to the server. + */ + @objc + func send(data: Data){ + send(data) + } +} -- 2.17.1