diff --git a/app/build.gradle b/app/build.gradle index 38edc1b4..1487d3e0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -192,7 +192,7 @@ dependencies { //kmnkt基于Kotlin Multiplatform的跨平台socket通信统一接口,支持UDP/TCP/MQTT协议 //https://github.com/xuankaicat/kmnkt implementation("org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5") - implementation files('libs/socket-2.0.0-alpha06-2.aar') + //implementation files('libs/socket-2.0.0-alpha06-2.aar') testImplementation deps.junit androidTestImplementation 'androidx.test.ext:junit:1.1.5' diff --git a/app/libs/socket-2.0.0-alpha06-2.aar b/app/libs/socket-2.0.0-alpha06-2.aar deleted file mode 100644 index dd3a350e..00000000 Binary files a/app/libs/socket-2.0.0-alpha06-2.aar and /dev/null differ diff --git a/app/src/main/java/com/idormy/sms/forwarder/entity/setting/SocketSetting.kt b/app/src/main/java/com/idormy/sms/forwarder/entity/setting/SocketSetting.kt index 2a719498..548d286c 100644 --- a/app/src/main/java/com/idormy/sms/forwarder/entity/setting/SocketSetting.kt +++ b/app/src/main/java/com/idormy/sms/forwarder/entity/setting/SocketSetting.kt @@ -9,6 +9,7 @@ data class SocketSetting( val port: Int = 0, //端口号 val msgTemplate: String = "", //消息模板 val secret: String? = "", //签名密钥 + val response: String? = "", //成功应答关键字 val username: String = "", //用户名 val password: String = "", //密码 val inCharset: String = "", //输入编码 @@ -29,4 +30,11 @@ data class SocketSetting( } } + fun getUriTypeCheckId(): Int { + return when (method) { + "ssl" -> R.id.rb_uriType_ssl + else -> R.id.rb_uriType_tcp + } + } + } \ No newline at end of file diff --git a/app/src/main/java/com/idormy/sms/forwarder/fragment/senders/SocketFragment.kt b/app/src/main/java/com/idormy/sms/forwarder/fragment/senders/SocketFragment.kt index 43266d92..43f17d57 100644 --- a/app/src/main/java/com/idormy/sms/forwarder/fragment/senders/SocketFragment.kt +++ b/app/src/main/java/com/idormy/sms/forwarder/fragment/senders/SocketFragment.kt @@ -122,13 +122,14 @@ class SocketFragment : BaseFragment(), View.OnCli binding!!.etPort.setText(settingVo.port.toString()) binding!!.etMsgTemplate.setText(settingVo.msgTemplate) binding!!.etSecret.setText(settingVo.secret) + binding!!.etResponse.setText(settingVo.response) binding!!.etUsername.setText(settingVo.username) binding!!.etPassword.setText(settingVo.password) binding!!.etInCharset.setSelectedItem(settingVo.inCharset) binding!!.etOutCharset.setSelectedItem(settingVo.outCharset) binding!!.etInMessageTopic.setText(settingVo.inMessageTopic) binding!!.etOutMessageTopic.setText(settingVo.outMessageTopic) - binding!!.etUriType.setText(settingVo.uriType) + binding!!.rgUriType.check(settingVo.getUriTypeCheckId()) binding!!.etPath.setText(settingVo.path) binding!!.etClientId.setText(settingVo.clientId) binding!!.layoutMqtt.visibility = if (checkedId == R.id.rb_method_mqtt) View.VISIBLE else View.GONE @@ -170,6 +171,7 @@ class SocketFragment : BaseFragment(), View.OnCli }.start() return } + R.id.btn_del -> { if (senderId <= 0 || isClone) { popToBack() @@ -183,6 +185,7 @@ class SocketFragment : BaseFragment(), View.OnCli }.show() return } + R.id.btn_save -> { val name = binding!!.etName.text.toString().trim() if (TextUtils.isEmpty(name)) { @@ -226,13 +229,17 @@ class SocketFragment : BaseFragment(), View.OnCli val msgTemplate = binding!!.etMsgTemplate.text.toString().trim() val secret = binding!!.etSecret.text.toString().trim() + val response = binding!!.etResponse.text.toString().trim() val username = binding!!.etUsername.text.toString().trim() val password = binding!!.etPassword.text.toString().trim() val inCharset = binding!!.etInCharset.text.toString().trim() val outCharset = binding!!.etOutCharset.text.toString().trim() val inMessageTopic = binding!!.etInMessageTopic.text.toString().trim() val outMessageTopic = binding!!.etOutMessageTopic.text.toString().trim() - val uriType = binding!!.etUriType.text.toString().trim() + val uriType = when (binding!!.rgUriType.checkedRadioButtonId) { + R.id.rb_uriType_ssl -> "ssl" + else -> "tcp" + } val path = binding!!.etPath.text.toString().trim() val clientId = binding!!.etClientId.text.toString().trim() @@ -240,7 +247,7 @@ class SocketFragment : BaseFragment(), View.OnCli throw Exception(getString(R.string.invalid_mqtt_message_topic)) } - return SocketSetting(method, address, port.toInt(), msgTemplate, secret, username, password, inCharset, outCharset, inMessageTopic, outMessageTopic, uriType, path, clientId) + return SocketSetting(method, address, port.toInt(), msgTemplate, secret, response, username, password, inCharset, outCharset, inMessageTopic, outMessageTopic, uriType, path, clientId) } override fun onDestroyView() { diff --git a/app/src/main/java/com/idormy/sms/forwarder/utils/sender/SocketUtils.kt b/app/src/main/java/com/idormy/sms/forwarder/utils/sender/SocketUtils.kt index b79cd251..4fc3e553 100644 --- a/app/src/main/java/com/idormy/sms/forwarder/utils/sender/SocketUtils.kt +++ b/app/src/main/java/com/idormy/sms/forwarder/utils/sender/SocketUtils.kt @@ -4,12 +4,6 @@ import android.annotation.SuppressLint import android.text.TextUtils import android.util.Base64 import android.util.Log -import com.gitee.xuankaicat.kmnkt.socket.MqttQuality -import com.gitee.xuankaicat.kmnkt.socket.dsl.mqtt -import com.gitee.xuankaicat.kmnkt.socket.dsl.tcp -import com.gitee.xuankaicat.kmnkt.socket.dsl.udp -import com.gitee.xuankaicat.kmnkt.socket.open -import com.gitee.xuankaicat.kmnkt.socket.utils.Charset import com.google.gson.Gson import com.idormy.sms.forwarder.database.entity.Rule import com.idormy.sms.forwarder.entity.MsgInfo @@ -17,7 +11,20 @@ import com.idormy.sms.forwarder.entity.setting.SocketSetting import com.idormy.sms.forwarder.utils.SendUtils import com.idormy.sms.forwarder.utils.SettingUtils import com.xuexiang.xutil.app.AppUtils +import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken +import org.eclipse.paho.client.mqttv3.MqttCallbackExtended +import org.eclipse.paho.client.mqttv3.MqttClient +import org.eclipse.paho.client.mqttv3.MqttConnectOptions +import org.eclipse.paho.client.mqttv3.MqttException +import org.eclipse.paho.client.mqttv3.MqttMessage +import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence +import java.io.BufferedReader +import java.io.BufferedWriter +import java.io.InputStreamReader +import java.io.OutputStreamWriter +import java.net.Socket import java.net.URLEncoder +import java.nio.charset.Charset import java.nio.charset.StandardCharsets import java.text.SimpleDateFormat import java.util.* @@ -62,113 +69,100 @@ class SocketUtils { } if (setting.method == "TCP" || setting.method == "UDP") { - var isReceived = false - var isConnected = false - val socket = if (setting.method == "TCP") { - tcp { - address = setting.address//设置ip地址 - port = setting.port//设置端口号 - if (!TextUtils.isEmpty(setting.inCharset)) inCharset = Charset.forName(setting.inCharset)//设置输入编码 - if (!TextUtils.isEmpty(setting.outCharset)) outCharset = Charset.forName(setting.outCharset)//设置输出编码 - } - } else { - udp { - address = setting.address//设置ip地址 - port = setting.port//设置端口号 - if (!TextUtils.isEmpty(setting.inCharset)) inCharset = Charset.forName(setting.inCharset)//设置输入编码 - if (!TextUtils.isEmpty(setting.outCharset)) outCharset = Charset.forName(setting.outCharset)//设置输出编码 - } - } - - socket.open { - success { - Log.d(TAG, "${setting.method}连接成功") - isConnected = true - //SendUtils.updateLogs(logId, 1, "TCP连接成功") - socket.send(message) - socket.startReceive { str, data -> - isReceived = true - Log.d(TAG, "str=$str,data=$data") - SendUtils.updateLogs(logId, 2, "收到订阅消息:str=$str,data=$data") - SendUtils.senderLogic(2, msgInfo, rule, senderIndex, msgId) - return@startReceive false - } - } - failure { - Log.d(TAG, "${setting.method}连接失败") - val status = 0 - SendUtils.updateLogs(logId, status, "TCP连接失败") - SendUtils.senderLogic(status, msgInfo, rule, senderIndex, msgId) - return@failure false//是否继续尝试连接 - } - loss { - Log.d(TAG, "${setting.method}连接断开") - return@loss true//是否尝试重连 - } - } - - //延时5秒关闭连接 - if (isConnected) { - Thread.sleep(10000) - socket.stopReceive() + // 创建套接字并连接到服务器 + val socket = Socket(setting.address, setting.port) + Log.d(TAG, "连接到服务器: ${setting.address}:${setting.port}") + try { + // 获取输入流和输出流,设置字符集为UTF-8 + val input = BufferedReader(InputStreamReader(socket.getInputStream(), Charset.forName(setting.inCharset))) + val output = BufferedWriter(OutputStreamWriter(socket.getOutputStream(), Charset.forName(setting.outCharset))) + + // 向服务器发送数据 + output.write(message) + output.newLine() // 添加换行符以便服务器使用readLine()来读取 + output.flush() + Log.d(TAG, "发送到服务器的消息: $message") + + // 从服务器接收响应 + val response = input.readLine() + Log.d(TAG, "从服务器接收的响应: $response") + val status = if (!setting.response.isNullOrEmpty() && !response.contains(setting.response)) 0 else 2 + SendUtils.updateLogs(logId, status, response) + SendUtils.senderLogic(status, msgInfo, rule, senderIndex, msgId) + } catch (e: Exception) { + e.printStackTrace() + val status = 0 + SendUtils.updateLogs(logId, status, e.message.toString()) + SendUtils.senderLogic(status, msgInfo, rule, senderIndex, msgId) + } finally { + // 关闭套接字 socket.close() - if (!isReceived) { - SendUtils.updateLogs(logId, 0, "未收到订阅消息") - SendUtils.senderLogic(0, msgInfo, rule, senderIndex, msgId) - } + Log.d(TAG, "Disconnected from MQTT broker") } - return - } else if (setting.method == "MQTT") { - val mqtt = mqtt { - address = setting.address//设置ip地址 - port = setting.port//设置端口号 - if (!TextUtils.isEmpty(setting.inCharset)) inCharset = Charset.forName(setting.inCharset)//设置输入编码 - if (!TextUtils.isEmpty(setting.outCharset)) outCharset = Charset.forName(setting.outCharset)//设置输出编码 - if (!TextUtils.isEmpty(setting.username)) username = setting.username - if (!TextUtils.isEmpty(setting.password)) password = setting.password - if (!TextUtils.isEmpty(setting.inMessageTopic)) inMessageTopic = setting.inMessageTopic - if (!TextUtils.isEmpty(setting.outMessageTopic)) outMessageTopic = setting.outMessageTopic - //自定义配置 - qos = MqttQuality.ExactlyOnce // 服务质量 详见MqttQuality - if (!TextUtils.isEmpty(setting.uriType)) uriType = setting.uriType //通信方式 默认为tcp - if (!TextUtils.isEmpty(setting.clientId)) clientId = setting.clientId //客户端ID,如果为空则为随机值 - timeOut = 10 //设置超时时间 - cleanSession = true //断开连接后是否清楚缓存,如果清除缓存则在重连后需要手动恢复订阅。 - keepAliveInterval = 20 //检测连接是否中断的间隔 - //行为配置 - threadLock = false //是否启用线程同步锁 默认false + // MQTT 连接参数 + val uriType = if (TextUtils.isEmpty(setting.uriType)) "tcp" else setting.uriType + val brokerUrl = "${uriType}://${setting.address}:${setting.port}" + if (!TextUtils.isEmpty(setting.path)) { + brokerUrl.plus(setting.path) } - - mqtt.open { - success { - Log.d(TAG, "MQTT连接成功") - //SendUtils.updateLogs(logId, 1, "MQTT连接成功") - // 订阅并发布后等待至拿到响应消息并赋值给result - // 如果超过10秒没有收到消息则将result设为"消息响应超时",并取消订阅topic - val response = mqtt.sendAndReceiveSync(setting.outMessageTopic, setting.inMessageTopic, message, 10000L) ?: "消息响应超时" - mqtt.close() - - val status = if (response == "消息响应超时") 0 else 2 - SendUtils.updateLogs(logId, status, "收到订阅消息:$response") - SendUtils.senderLogic(status, msgInfo, rule, senderIndex, msgId) - return@success - } - failure { - Log.d(TAG, "MQTT连接失败") - val status = 0 - SendUtils.updateLogs(logId, status, "MQTT连接失败") - SendUtils.senderLogic(status, msgInfo, rule, senderIndex, msgId) - return@failure false//是否继续尝试连接 + Log.d(TAG, "MQTT brokerUrl: $brokerUrl") + val clientId = if (TextUtils.isEmpty(setting.clientId)) UUID.randomUUID().toString() else setting.clientId + val mqttClient = MqttClient(brokerUrl, clientId, MemoryPersistence()) + try { + val options = MqttConnectOptions() + if (!TextUtils.isEmpty(setting.username)) { + options.userName = setting.username } - loss { - Log.d(TAG, "MQTT失去连接") - return@loss true//是否尝试重连 + if (!TextUtils.isEmpty(setting.password)) { + options.password = setting.password.toCharArray() } - } + options.isCleanSession = true + mqttClient.connect(options) + Log.d(TAG, "Connected to MQTT broker: ${mqttClient.serverURI}") + + mqttClient.subscribe(setting.inMessageTopic) + Log.d(TAG, "Subscribed to topic: $setting.inMessageTopic") + + val outMessage = message.toByteArray(Charset.forName(setting.outCharset)) + val mqttMessage = MqttMessage(outMessage) + mqttMessage.qos = 0 // 设置消息质量服务等级 + //异步发布消息 + mqttClient.publish(setting.outMessageTopic, mqttMessage) + Log.d(TAG, "Published message to topic: $setting.outMessageTopic") + mqttClient.setCallback(object : MqttCallbackExtended { + override fun connectionLost(cause: Throwable?) { + val response = "Connection to MQTT broker lost: ${cause?.message}" + Log.d(TAG, response) + SendUtils.updateLogs(logId, 0, response) + SendUtils.senderLogic(0, msgInfo, rule, senderIndex, msgId) + } - } + override fun messageArrived(topic: String?, inMessage: MqttMessage?) { + val payload = inMessage?.payload?.toString(Charset.forName(setting.inCharset)) + Log.d(TAG, "Received message on topic $topic: $payload") + val status = if (!setting.response.isNullOrEmpty() && !payload?.contains(setting.response)!!) 0 else 2 + SendUtils.updateLogs(logId, status, payload.toString()) + SendUtils.senderLogic(status, msgInfo, rule, senderIndex, msgId) + } + override fun deliveryComplete(token: IMqttDeliveryToken?) { + Log.d(TAG, "deliveryComplete") + SendUtils.updateLogs(logId, 2, "deliveryComplete") + SendUtils.senderLogic(2, msgInfo, rule, senderIndex, msgId) + } + + override fun connectComplete(reconnect: Boolean, serverURI: String?) { + Log.d(TAG, "connectComplete") + } + }) + } catch (e: MqttException) { + Log.d(TAG, "An error occurred: ${e.message}") + } finally { + mqttClient.disconnect() + Log.d(TAG, "Disconnected from MQTT broker") + } + } } //JSON需要转义的字符 diff --git a/app/src/main/res/layout/fragment_senders_socket.xml b/app/src/main/res/layout/fragment_senders_socket.xml index 9408230f..cab418b6 100644 --- a/app/src/main/res/layout/fragment_senders_socket.xml +++ b/app/src/main/res/layout/fragment_senders_socket.xml @@ -267,6 +267,28 @@ + + + + + + + + - + android:orientation="horizontal"> + + + + + + diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index b4112661..0f9236d6 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -411,6 +411,7 @@ UDP TCP MQTT + SSL Local IP: Instructions: \n[Note] The APP version of the sender and receiver must be the same!\n1. Please keep the SOURCE and DESTINATION phones in the same Wi-Fi network, and do not turn on isolation. \n2. Tap "Send" on SOURCE mobile phone, and get "server IP" \n3. After filling in "Server IP" on DESTINATION phone, tap "Receive". \n [NOTE:] sender(s), forwarding rule(s) and log(s) will be overwritten after cloning! @@ -780,6 +781,7 @@ For example: payload=%7B%22text%22%3A%22[msg]%22%7D [msg] will be replaced with SMS content.\nJson format is supported, e.g. {\"text\":\"[msg]\"}.\nNote: msg is automatically URLEncoder except in JSON format Secret: If left empty, the sign will not be calculated Successful Response Keyword:If left empty, HTTP status 200 represents success + Successful Response Keyword:Leaving it blank means sending is considered as successful. Headers Key Value diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2f63239f..bcba3697 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -412,6 +412,7 @@ UDP TCP MQTT + SSL 本机IP: 严正声明:\n该功能仅限个人新旧手机切换使用,用于非法用途后果自负!\n\n操作说明:\n1.新旧手机连接同一个WiFi网络(禁用AP隔离),如需穿透内网请先配置Frpc\n2.【二选一】旧手机点【推送】按钮,将本机的配置推送到服务端\n3.【二选一】新手机点【拉取】按钮,将拉取服务端的配置到本机\n\n注意事项:\n1.客户端与服务端的APP版本必须一致,才能克隆!\n2.导入成功后,发送通道、转发规则将完全被覆盖,清空历史记录!\n3.主动请求、保活措施、个性设置不在克隆范围 @@ -781,6 +782,7 @@ 例如:payload=%7B%22text%22%3A%22[msg]%22%7D [msg]将被替换成短信内容。\n支持Json格式,例如:{\"text\":\"[msg]\"}。\n注意:除JSON格式外,msg会自动进行URLEncoder Secret:置空则不计算sign 成功应答关键字:置空则http状态200即为成功 + 成功应答关键字:置空则发出即成功 Headers Key Value