新增:`HttpServer`请求/应答报文进行`RSA`非对称加密传输

优化:允许自定义客户端与服务端时间容差,避免请求重放攻击 #227
This commit is contained in:
pppscn 2022-10-15 14:08:50 +08:00
parent 1b93aeb857
commit 581c0c2ef2
18 changed files with 1336 additions and 392 deletions

View File

@ -6,6 +6,7 @@ import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.RadioGroup
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import com.idormy.sms.forwarder.App import com.idormy.sms.forwarder.App
@ -38,9 +39,7 @@ import com.xuexiang.xutil.XUtil
@Suppress("PrivatePropertyName", "PropertyName") @Suppress("PrivatePropertyName", "PropertyName")
@Page(name = "主动控制·客户端") @Page(name = "主动控制·客户端")
class ClientFragment : BaseFragment<FragmentClientBinding?>(), class ClientFragment : BaseFragment<FragmentClientBinding?>(), View.OnClickListener, RecyclerViewHolder.OnItemClickListener<PageInfo> {
View.OnClickListener,
RecyclerViewHolder.OnItemClickListener<PageInfo> {
val TAG: String = ClientFragment::class.java.simpleName val TAG: String = ClientFragment::class.java.simpleName
private var appContext: App? = null private var appContext: App? = null
@ -113,6 +112,41 @@ class ClientFragment : BaseFragment<FragmentClientBinding?>(),
} }
}) })
//安全措施
var safetyMeasuresId = R.id.rb_safety_measures_none
when (HttpServerUtils.clientSafetyMeasures) {
1 -> {
safetyMeasuresId = R.id.rb_safety_measures_sign
binding!!.tvSignKey.text = getString(R.string.sign_key)
}
2 -> {
safetyMeasuresId = R.id.rb_safety_measures_rsa
binding!!.tvSignKey.text = getString(R.string.public_key)
}
else -> {
binding!!.layoutSignKey.visibility = View.GONE
}
}
binding!!.rgSafetyMeasures.check(safetyMeasuresId)
binding!!.rgSafetyMeasures.setOnCheckedChangeListener { _: RadioGroup?, checkedId: Int ->
var safetyMeasures = 0
binding!!.layoutSignKey.visibility = View.GONE
when (checkedId) {
R.id.rb_safety_measures_sign -> {
safetyMeasures = 1
binding!!.tvSignKey.text = getString(R.string.sign_key)
binding!!.layoutSignKey.visibility = View.VISIBLE
}
R.id.rb_safety_measures_rsa -> {
safetyMeasures = 2
binding!!.tvSignKey.text = getString(R.string.public_key)
binding!!.layoutSignKey.visibility = View.VISIBLE
}
else -> {}
}
HttpServerUtils.clientSafetyMeasures = safetyMeasures
}
binding!!.etSignKey.setText(HttpServerUtils.clientSignKey) binding!!.etSignKey.setText(HttpServerUtils.clientSignKey)
binding!!.etSignKey.addTextChangedListener(object : TextWatcher { binding!!.etSignKey.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
@ -136,30 +170,46 @@ class ClientFragment : BaseFragment<FragmentClientBinding?>(),
} }
Log.d(TAG, "serverHistory = $serverHistory") Log.d(TAG, "serverHistory = $serverHistory")
MaterialDialog.Builder(requireContext()) MaterialDialog.Builder(requireContext()).title(R.string.server_history).items(serverHistory.keys).itemsCallbackSingleChoice(0) { _: MaterialDialog?, _: View?, _: Int, text: CharSequence ->
.title(R.string.server_history) //XToastUtils.info("$which: $text")
.items(serverHistory.keys) val matches = Regex("【(.*)】(.*)", RegexOption.IGNORE_CASE).findAll(text).toList().flatMap(MatchResult::groupValues)
.itemsCallbackSingleChoice(0) { _: MaterialDialog?, _: View?, _: Int, text: CharSequence -> Log.i(TAG, "matches = $matches")
//XToastUtils.info("$which: $text") if (matches.isNotEmpty()) {
val matches = Regex("【(.*)】(.*)", RegexOption.IGNORE_CASE).findAll(text).toList().flatMap(MatchResult::groupValues) binding!!.etServerAddress.setText(matches[2])
Log.i(TAG, "matches = $matches") } else {
if (matches.isNotEmpty()) { binding!!.etServerAddress.setText(text)
binding!!.etServerAddress.setText(matches[2]) }
val signKey = serverHistory[text].toString()
if (!TextUtils.isEmpty(signKey)) {
val keyMatches = Regex("(.*)##(.*)", RegexOption.IGNORE_CASE).findAll(signKey).toList().flatMap(MatchResult::groupValues)
Log.i(TAG, "keyMatches = $keyMatches")
if (keyMatches.isNotEmpty()) {
binding!!.etSignKey.setText(keyMatches[1])
var safetyMeasuresId = R.id.rb_safety_measures_none
when (keyMatches[2]) {
"1" -> {
safetyMeasuresId = R.id.rb_safety_measures_sign
binding!!.tvSignKey.text = getString(R.string.sign_key)
}
"2" -> {
safetyMeasuresId = R.id.rb_safety_measures_rsa
binding!!.tvSignKey.text = getString(R.string.public_key)
}
else -> {
binding!!.tvSignKey.visibility = View.GONE
binding!!.etSignKey.visibility = View.GONE
}
}
binding!!.rgSafetyMeasures.check(safetyMeasuresId)
} else { } else {
binding!!.etServerAddress.setText(text) binding!!.etSignKey.setText(serverHistory[text])
} }
binding!!.etSignKey.setText(serverHistory[text])
true // allow selection
} }
.positiveText(R.string.select) true // allow selection
.negativeText(R.string.cancel) }.positiveText(R.string.select).negativeText(R.string.cancel).neutralText(R.string.clear_history).neutralColor(ResUtils.getColors(R.color.red)).onNeutral { _: MaterialDialog?, _: DialogAction? ->
.neutralText(R.string.clear_history) serverHistory.clear()
.neutralColor(ResUtils.getColors(R.color.red)) HttpServerUtils.serverHistory = ""
.onNeutral { _: MaterialDialog?, _: DialogAction? -> }.show()
serverHistory.clear()
HttpServerUtils.serverHistory = ""
}
.show()
} }
R.id.btn_server_test -> { R.id.btn_server_test -> {
if (!CommonUtils.checkUrl(HttpServerUtils.serverAddress)) { if (!CommonUtils.checkUrl(HttpServerUtils.serverAddress)) {
@ -183,22 +233,12 @@ class ClientFragment : BaseFragment<FragmentClientBinding?>(),
XToastUtils.error(getString(R.string.click_test_button_first)) XToastUtils.error(getString(R.string.click_test_button_first))
return return
} }
if (serverConfig != null && ( if (serverConfig != null && ((item.name == ResUtils.getString(R.string.api_sms_send) && !serverConfig!!.enableApiSmsSend) || (item.name == ResUtils.getString(R.string.api_sms_query) && !serverConfig!!.enableApiSmsQuery) || (item.name == ResUtils.getString(R.string.api_call_query) && !serverConfig!!.enableApiCallQuery) || (item.name == ResUtils.getString(R.string.api_contact_query) && !serverConfig!!.enableApiContactQuery) || (item.name == ResUtils.getString(R.string.api_battery_query) && !serverConfig!!.enableApiBatteryQuery) || (item.name == ResUtils.getString(R.string.api_wol) && !serverConfig!!.enableApiWol))) {
(item.name == ResUtils.getString(R.string.api_sms_send) && !serverConfig!!.enableApiSmsSend)
|| (item.name == ResUtils.getString(R.string.api_sms_query) && !serverConfig!!.enableApiSmsQuery)
|| (item.name == ResUtils.getString(R.string.api_call_query) && !serverConfig!!.enableApiCallQuery)
|| (item.name == ResUtils.getString(R.string.api_contact_query) && !serverConfig!!.enableApiContactQuery)
|| (item.name == ResUtils.getString(R.string.api_battery_query) && !serverConfig!!.enableApiBatteryQuery)
|| (item.name == ResUtils.getString(R.string.api_wol) && !serverConfig!!.enableApiWol)
)
) {
XToastUtils.error(getString(R.string.disabled_on_the_server)) XToastUtils.error(getString(R.string.disabled_on_the_server))
return return
} }
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST") PageOption.to(Class.forName(item.classPath) as Class<XPageFragment>) //跳转的fragment
PageOption.to(Class.forName(item.classPath) as Class<XPageFragment>) //跳转的fragment .setNewActivity(true).open(this)
.setNewActivity(true)
.open(this)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
XToastUtils.error(e.message.toString()) XToastUtils.error(e.message.toString())
@ -212,58 +252,87 @@ class ClientFragment : BaseFragment<FragmentClientBinding?>(),
val msgMap: MutableMap<String, Any> = mutableMapOf() val msgMap: MutableMap<String, Any> = mutableMapOf()
val timestamp = System.currentTimeMillis() val timestamp = System.currentTimeMillis()
msgMap["timestamp"] = timestamp msgMap["timestamp"] = timestamp
val clientSignKey = HttpServerUtils.clientSignKey
if (!TextUtils.isEmpty(clientSignKey)) { val clientSignKey = HttpServerUtils.clientSignKey.toString()
msgMap["sign"] = HttpServerUtils.calcSign(timestamp.toString(), clientSignKey.toString()) if ((HttpServerUtils.clientSafetyMeasures == 1 || HttpServerUtils.clientSafetyMeasures == 2) && TextUtils.isEmpty(clientSignKey)) {
if (needToast) XToastUtils.error("请输入签名密钥或RSA公钥")
return
}
if (HttpServerUtils.clientSafetyMeasures == 1 && !TextUtils.isEmpty(clientSignKey)) {
msgMap["sign"] = HttpServerUtils.calcSign(timestamp.toString(), clientSignKey)
} }
val dataMap: MutableMap<String, Any> = mutableMapOf() val dataMap: MutableMap<String, Any> = mutableMapOf()
msgMap["data"] = dataMap msgMap["data"] = dataMap
val requestMsg: String = Gson().toJson(msgMap) var requestMsg: String = Gson().toJson(msgMap)
Log.i(TAG, "requestMsg:$requestMsg") Log.i(TAG, "requestMsg:$requestMsg")
if (needToast) mCountDownHelper?.start() val postRequest = XHttp.post(requestUrl)
XHttp.post(requestUrl)
.upJson(requestMsg)
.keepJson(true) .keepJson(true)
.timeOut((SettingUtils.requestTimeout * 1000).toLong()) //超时时间10s .timeOut((SettingUtils.requestTimeout * 1000).toLong()) //超时时间10s
.cacheMode(CacheMode.NO_CACHE) .cacheMode(CacheMode.NO_CACHE).timeStamp(true)
//.retryCount(SettingUtils.requestRetryTimes) //超时重试的次数
//.retryDelay(SettingUtils.requestDelayTime) //超时重试的延迟时间
//.retryIncreaseDelay(SettingUtils.requestDelayTime) //超时重试叠加延时
.timeStamp(true)
.execute(object : SimpleCallBack<String>() {
override fun onError(e: ApiException) { if (HttpServerUtils.clientSafetyMeasures == 2) {
XToastUtils.error(e.displayMessage) val publicKey = RSACrypt.getPublicKey(HttpServerUtils.clientSignKey.toString())
if (needToast) mCountDownHelper?.finish() try {
} requestMsg = Base64.encode(requestMsg.toByteArray())
requestMsg = RSACrypt.encryptByPublicKey(requestMsg, publicKey)
Log.i(TAG, "requestMsg: $requestMsg")
} catch (e: Exception) {
if (needToast) XToastUtils.error(ResUtils.getString(R.string.request_failed) + e.message)
e.printStackTrace()
return
}
postRequest.upString(requestMsg)
} else {
postRequest.upJson(requestMsg)
}
override fun onSuccess(response: String) { if (needToast) mCountDownHelper?.start()
Log.i(TAG, response) postRequest.execute(object : SimpleCallBack<String>() {
try { override fun onError(e: ApiException) {
val resp: BaseResponse<ConfigData> = Gson().fromJson(response, object : TypeToken<BaseResponse<ConfigData>>() {}.type) XToastUtils.error(e.displayMessage)
if (resp.code == 200) { if (needToast) mCountDownHelper?.finish()
serverConfig = resp.data!! }
if (needToast) XToastUtils.success(ResUtils.getString(R.string.request_succeeded))
//删除3.0.8之前保存的记录 override fun onSuccess(response: String) {
serverHistory.remove(HttpServerUtils.serverAddress.toString()) Log.i(TAG, response)
//添加到历史记录 try {
val key = "${serverConfig?.extraDeviceMark}${HttpServerUtils.serverAddress.toString()}" var json = response
serverHistory[key] = HttpServerUtils.clientSignKey ?: "" if (HttpServerUtils.clientSafetyMeasures == 2) {
HttpServerUtils.serverHistory = Gson().toJson(serverHistory) val publicKey = RSACrypt.getPublicKey(HttpServerUtils.clientSignKey.toString())
HttpServerUtils.serverConfig = Gson().toJson(serverConfig) json = RSACrypt.decryptByPublicKey(json, publicKey)
json = String(Base64.decode(json))
}
val resp: BaseResponse<ConfigData> = Gson().fromJson(json, object : TypeToken<BaseResponse<ConfigData>>() {}.type)
if (resp.code == 200) {
serverConfig = resp.data!!
if (needToast) XToastUtils.success(ResUtils.getString(R.string.request_succeeded))
//删除3.0.8之前保存的记录
serverHistory.remove(HttpServerUtils.serverAddress.toString())
//添加到历史记录
val key = "${serverConfig?.extraDeviceMark}${HttpServerUtils.serverAddress.toString()}"
if (TextUtils.isEmpty(HttpServerUtils.clientSignKey)) {
serverHistory[key] = "SMSFORWARDER##" + HttpServerUtils.clientSafetyMeasures.toString()
} else { } else {
if (needToast) XToastUtils.error(ResUtils.getString(R.string.request_failed) + resp.msg) serverHistory[key] = HttpServerUtils.clientSignKey + "##" + HttpServerUtils.clientSafetyMeasures.toString()
} }
} catch (e: Exception) { HttpServerUtils.serverHistory = Gson().toJson(serverHistory)
e.printStackTrace() HttpServerUtils.serverConfig = Gson().toJson(serverConfig)
if (needToast) XToastUtils.error(ResUtils.getString(R.string.request_failed) + response) } else {
if (needToast) XToastUtils.error(ResUtils.getString(R.string.request_failed) + resp.msg)
} }
if (needToast) mCountDownHelper?.finish() if (needToast) mCountDownHelper?.finish()
} catch (e: Exception) {
e.printStackTrace()
if (needToast) {
XToastUtils.error(ResUtils.getString(R.string.request_failed) + response)
mCountDownHelper?.finish()
}
} }
}
}) })
} }
override fun onDestroyView() { override fun onDestroyView() {

View File

@ -10,6 +10,7 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.CompoundButton import android.widget.CompoundButton
import android.widget.RadioGroup
import com.hjq.permissions.OnPermissionCallback import com.hjq.permissions.OnPermissionCallback
import com.hjq.permissions.Permission import com.hjq.permissions.Permission
import com.hjq.permissions.XXPermissions import com.hjq.permissions.XXPermissions
@ -18,20 +19,19 @@ import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.core.BaseFragment import com.idormy.sms.forwarder.core.BaseFragment
import com.idormy.sms.forwarder.databinding.FragmentServerBinding import com.idormy.sms.forwarder.databinding.FragmentServerBinding
import com.idormy.sms.forwarder.service.HttpService import com.idormy.sms.forwarder.service.HttpService
import com.idormy.sms.forwarder.utils.HTTP_SERVER_PORT import com.idormy.sms.forwarder.utils.*
import com.idormy.sms.forwarder.utils.HttpServerUtils
import com.idormy.sms.forwarder.utils.RandomUtils
import com.idormy.sms.forwarder.utils.XToastUtils
import com.xuexiang.xaop.annotation.SingleClick import com.xuexiang.xaop.annotation.SingleClick
import com.xuexiang.xpage.annotation.Page import com.xuexiang.xpage.annotation.Page
import com.xuexiang.xui.widget.actionbar.TitleBar import com.xuexiang.xui.widget.actionbar.TitleBar
import com.xuexiang.xui.widget.button.SmoothCheckBox import com.xuexiang.xui.widget.button.SmoothCheckBox
import com.xuexiang.xui.widget.dialog.materialdialog.MaterialDialog import com.xuexiang.xui.widget.dialog.materialdialog.MaterialDialog
import com.xuexiang.xui.widget.picker.XSeekBar
import com.xuexiang.xutil.app.ServiceUtils import com.xuexiang.xutil.app.ServiceUtils
import com.xuexiang.xutil.net.NetworkUtils import com.xuexiang.xutil.net.NetworkUtils
import com.xuexiang.xutil.system.ClipboardUtils import com.xuexiang.xutil.system.ClipboardUtils
import java.io.File import java.io.File
import java.net.InetAddress import java.net.InetAddress
import java.security.KeyPairGenerator
@Suppress("PrivatePropertyName") @Suppress("PrivatePropertyName")
@ -79,6 +79,65 @@ class ServerFragment : BaseFragment<FragmentServerBinding?>(), View.OnClickListe
//启动更新UI定时器 //启动更新UI定时器
handler.post(runnable) handler.post(runnable)
//安全措施
var safetyMeasuresId = R.id.rb_safety_measures_none
when (HttpServerUtils.safetyMeasures) {
1 -> {
safetyMeasuresId = R.id.rb_safety_measures_sign
binding!!.layoutSignKey.visibility = View.VISIBLE
binding!!.layoutTimeTolerance.visibility = View.VISIBLE
}
2 -> {
safetyMeasuresId = R.id.rb_safety_measures_rsa
binding!!.layoutPrivateKey.visibility = View.VISIBLE
binding!!.layoutPublicKey.visibility = View.VISIBLE
}
else -> {}
}
binding!!.rgSafetyMeasures.check(safetyMeasuresId)
binding!!.rgSafetyMeasures.setOnCheckedChangeListener { _: RadioGroup?, checkedId: Int ->
var safetyMeasures = 0
binding!!.layoutSignKey.visibility = View.GONE
binding!!.layoutTimeTolerance.visibility = View.GONE
binding!!.layoutPrivateKey.visibility = View.GONE
binding!!.layoutPublicKey.visibility = View.GONE
when (checkedId) {
R.id.rb_safety_measures_sign -> {
safetyMeasures = 1
binding!!.layoutSignKey.visibility = View.VISIBLE
binding!!.layoutTimeTolerance.visibility = View.VISIBLE
}
R.id.rb_safety_measures_rsa -> {
safetyMeasures = 2
binding!!.layoutPrivateKey.visibility = View.VISIBLE
binding!!.layoutPublicKey.visibility = View.VISIBLE
}
else -> {}
}
HttpServerUtils.safetyMeasures = safetyMeasures
}
//RSA公私钥
binding!!.btnCopyPublicKey.setOnClickListener(this)
binding!!.btnGenerateKey.setOnClickListener(this)
binding!!.etPublicKey.setText(HttpServerUtils.serverPublicKey)
binding!!.etPublicKey.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable) {
HttpServerUtils.serverPublicKey = binding!!.etPublicKey.text.toString().trim()
}
})
binding!!.etPrivateKey.setText(HttpServerUtils.serverPrivateKey)
binding!!.etPrivateKey.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable) {
HttpServerUtils.serverPrivateKey = binding!!.etPrivateKey.text.toString().trim()
}
})
//签名密钥
binding!!.btnSignKey.setOnClickListener(this) binding!!.btnSignKey.setOnClickListener(this)
binding!!.btnPathPicker.setOnClickListener(this) binding!!.btnPathPicker.setOnClickListener(this)
binding!!.etSignKey.setText(HttpServerUtils.serverSignKey) binding!!.etSignKey.setText(HttpServerUtils.serverSignKey)
@ -89,6 +148,13 @@ class ServerFragment : BaseFragment<FragmentServerBinding?>(), View.OnClickListe
HttpServerUtils.serverSignKey = binding!!.etSignKey.text.toString().trim() HttpServerUtils.serverSignKey = binding!!.etSignKey.text.toString().trim()
} }
}) })
//时间容差
binding!!.xsbTimeTolerance.setDefaultValue(HttpServerUtils.timeTolerance)
binding!!.xsbTimeTolerance.setOnSeekBarListener { _: XSeekBar?, newValue: Int ->
HttpServerUtils.timeTolerance = newValue
}
//web客户端
binding!!.etWebPath.setText(HttpServerUtils.serverWebPath) binding!!.etWebPath.setText(HttpServerUtils.serverWebPath)
binding!!.etWebPath.addTextChangedListener(object : TextWatcher { binding!!.etWebPath.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
@ -155,6 +221,29 @@ class ServerFragment : BaseFragment<FragmentServerBinding?>(), View.OnClickListe
} }
refreshButtonText() refreshButtonText()
} }
R.id.btn_generate_key -> {
val generator = KeyPairGenerator.getInstance("RSA") //密钥生成器
generator.initialize(2048)
val keyPair = generator.genKeyPair()
val publicKey = keyPair.public
val privateKey = keyPair.private
val publicKeyEncoded = Base64.encode(publicKey.encoded)
val privateKeyEncoded = Base64.encode(privateKey.encoded)
println("publicKey=$publicKeyEncoded")
println("privateKey=$privateKeyEncoded")
binding!!.etPublicKey.setText(publicKeyEncoded)
binding!!.etPrivateKey.setText(privateKeyEncoded)
ClipboardUtils.copyText(publicKeyEncoded)
XToastUtils.info(getString(R.string.rsa_key_tips))
}
R.id.btn_copy_public_key -> {
ClipboardUtils.copyText(binding!!.etPublicKey.text)
XToastUtils.info(getString(R.string.rsa_key_tips2))
}
R.id.btn_sign_key -> { R.id.btn_sign_key -> {
val sign = RandomUtils.getRandomNumbersAndLetters(16) val sign = RandomUtils.getRandomNumbersAndLetters(16)
ClipboardUtils.copyText(sign) ClipboardUtils.copyText(sign)

View File

@ -10,9 +10,7 @@ import com.idormy.sms.forwarder.core.BaseFragment
import com.idormy.sms.forwarder.databinding.FragmentClientBatteryQueryBinding import com.idormy.sms.forwarder.databinding.FragmentClientBatteryQueryBinding
import com.idormy.sms.forwarder.entity.BatteryInfo import com.idormy.sms.forwarder.entity.BatteryInfo
import com.idormy.sms.forwarder.server.model.BaseResponse import com.idormy.sms.forwarder.server.model.BaseResponse
import com.idormy.sms.forwarder.utils.HttpServerUtils import com.idormy.sms.forwarder.utils.*
import com.idormy.sms.forwarder.utils.SettingUtils
import com.idormy.sms.forwarder.utils.XToastUtils
import com.xuexiang.xhttp2.XHttp import com.xuexiang.xhttp2.XHttp
import com.xuexiang.xhttp2.cache.model.CacheMode import com.xuexiang.xhttp2.cache.model.CacheMode
import com.xuexiang.xhttp2.callback.SimpleCallBack import com.xuexiang.xhttp2.callback.SimpleCallBack
@ -60,53 +58,71 @@ class BatteryQueryFragment : BaseFragment<FragmentClientBatteryQueryBinding?>()
val dataMap: MutableMap<String, Any> = mutableMapOf() val dataMap: MutableMap<String, Any> = mutableMapOf()
msgMap["data"] = dataMap msgMap["data"] = dataMap
val requestMsg: String = Gson().toJson(msgMap) var requestMsg: String = Gson().toJson(msgMap)
Log.i(TAG, "requestMsg:$requestMsg") Log.i(TAG, "requestMsg:$requestMsg")
XHttp.post(requestUrl) val postRequest = XHttp.post(requestUrl)
.upJson(requestMsg)
.keepJson(true) .keepJson(true)
.timeOut((SettingUtils.requestTimeout * 1000).toLong()) //超时时间10s .timeOut((SettingUtils.requestTimeout * 1000).toLong()) //超时时间10s
.cacheMode(CacheMode.NO_CACHE) .cacheMode(CacheMode.NO_CACHE)
//.retryCount(SettingUtils.requestRetryTimes) //超时重试的次数
//.retryDelay(SettingUtils.requestDelayTime) //超时重试的延迟时间
//.retryIncreaseDelay(SettingUtils.requestDelayTime) //超时重试叠加延时
.timeStamp(true) .timeStamp(true)
.execute(object : SimpleCallBack<String>() {
override fun onError(e: ApiException) { if (HttpServerUtils.clientSafetyMeasures == 2) {
XToastUtils.error(e.displayMessage) val publicKey = RSACrypt.getPublicKey(HttpServerUtils.clientSignKey.toString())
} try {
requestMsg = Base64.encode(requestMsg.toByteArray())
requestMsg = RSACrypt.encryptByPublicKey(requestMsg, publicKey)
Log.i(TAG, "requestMsg: $requestMsg")
} catch (e: Exception) {
XToastUtils.error(ResUtils.getString(R.string.request_failed) + e.message)
e.printStackTrace()
return
}
postRequest.upString(requestMsg)
} else {
postRequest.upJson(requestMsg)
}
override fun onSuccess(response: String) { postRequest.execute(object : SimpleCallBack<String>() {
Log.i(TAG, response) override fun onError(e: ApiException) {
try { XToastUtils.error(e.displayMessage)
val resp: BaseResponse<BatteryInfo> = Gson().fromJson(response, object : TypeToken<BaseResponse<BatteryInfo>>() {}.type) }
if (resp.code == 200) {
XToastUtils.success(ResUtils.getString(R.string.request_succeeded))
val batteryInfo = resp.data ?: return
val groupListView = binding!!.infoList override fun onSuccess(response: String) {
val section = XUIGroupListView.newSection(context) Log.i(TAG, response)
section.addItemView(groupListView.createItemView(String.format(ResUtils.getString(R.string.battery_level), batteryInfo.level))) {} try {
if (batteryInfo.scale != "") section.addItemView(groupListView.createItemView(String.format(ResUtils.getString(R.string.battery_scale), batteryInfo.scale))) {} var json = response
if (batteryInfo.voltage != "") section.addItemView(groupListView.createItemView(String.format(ResUtils.getString(R.string.battery_voltage), batteryInfo.voltage))) {} if (HttpServerUtils.clientSafetyMeasures == 2) {
if (batteryInfo.temperature != "") section.addItemView(groupListView.createItemView(String.format(ResUtils.getString(R.string.battery_temperature), batteryInfo.temperature))) {} val publicKey = RSACrypt.getPublicKey(HttpServerUtils.clientSignKey.toString())
section.addItemView(groupListView.createItemView(String.format(ResUtils.getString(R.string.battery_status), batteryInfo.status))) {} json = RSACrypt.decryptByPublicKey(json, publicKey)
section.addItemView(groupListView.createItemView(String.format(ResUtils.getString(R.string.battery_health), batteryInfo.health))) {} json = String(Base64.decode(json))
section.addItemView(groupListView.createItemView(String.format(ResUtils.getString(R.string.battery_plugged), batteryInfo.plugged))) {}
section.addTo(groupListView)
} else {
XToastUtils.error(ResUtils.getString(R.string.request_failed) + resp.msg)
}
} catch (e: Exception) {
e.printStackTrace()
XToastUtils.error(ResUtils.getString(R.string.request_failed) + response)
} }
} val resp: BaseResponse<BatteryInfo> = Gson().fromJson(json, object : TypeToken<BaseResponse<BatteryInfo>>() {}.type)
if (resp.code == 200) {
XToastUtils.success(ResUtils.getString(R.string.request_succeeded))
val batteryInfo = resp.data ?: return
val groupListView = binding!!.infoList
val section = XUIGroupListView.newSection(context)
section.addItemView(groupListView.createItemView(String.format(ResUtils.getString(R.string.battery_level), batteryInfo.level))) {}
if (batteryInfo.scale != "") section.addItemView(groupListView.createItemView(String.format(ResUtils.getString(R.string.battery_scale), batteryInfo.scale))) {}
if (batteryInfo.voltage != "") section.addItemView(groupListView.createItemView(String.format(ResUtils.getString(R.string.battery_voltage), batteryInfo.voltage))) {}
if (batteryInfo.temperature != "") section.addItemView(groupListView.createItemView(String.format(ResUtils.getString(R.string.battery_temperature), batteryInfo.temperature))) {}
section.addItemView(groupListView.createItemView(String.format(ResUtils.getString(R.string.battery_status), batteryInfo.status))) {}
section.addItemView(groupListView.createItemView(String.format(ResUtils.getString(R.string.battery_health), batteryInfo.health))) {}
section.addItemView(groupListView.createItemView(String.format(ResUtils.getString(R.string.battery_plugged), batteryInfo.plugged))) {}
section.addTo(groupListView)
} else {
XToastUtils.error(ResUtils.getString(R.string.request_failed) + resp.msg)
}
} catch (e: Exception) {
e.printStackTrace()
XToastUtils.error(ResUtils.getString(R.string.request_failed) + response)
}
}
})
})
} }
} }

View File

@ -207,46 +207,65 @@ class CallQueryFragment : BaseFragment<FragmentClientCallQueryBinding?>() {
if (refresh) pageNum = 1 if (refresh) pageNum = 1
msgMap["data"] = CallQueryData(callType, pageNum, pageSize, keyword) msgMap["data"] = CallQueryData(callType, pageNum, pageSize, keyword)
val requestMsg: String = Gson().toJson(msgMap) var requestMsg: String = Gson().toJson(msgMap)
Log.i(TAG, "requestMsg:$requestMsg") Log.i(TAG, "requestMsg:$requestMsg")
XHttp.post(requestUrl) val postRequest = XHttp.post(requestUrl)
.upJson(requestMsg)
.keepJson(true) .keepJson(true)
.timeOut((SettingUtils.requestTimeout * 1000).toLong()) //超时时间10s .timeOut((SettingUtils.requestTimeout * 1000).toLong()) //超时时间10s
.cacheMode(CacheMode.NO_CACHE) .cacheMode(CacheMode.NO_CACHE)
.timeStamp(true) .timeStamp(true)
.execute(object : SimpleCallBack<String>() {
override fun onError(e: ApiException) { if (HttpServerUtils.clientSafetyMeasures == 2) {
XToastUtils.error(e.displayMessage) val publicKey = RSACrypt.getPublicKey(HttpServerUtils.clientSignKey.toString())
} try {
requestMsg = Base64.encode(requestMsg.toByteArray())
requestMsg = RSACrypt.encryptByPublicKey(requestMsg, publicKey)
Log.i(TAG, "requestMsg: $requestMsg")
} catch (e: Exception) {
XToastUtils.error(ResUtils.getString(R.string.request_failed) + e.message)
e.printStackTrace()
return
}
postRequest.upString(requestMsg)
} else {
postRequest.upJson(requestMsg)
}
override fun onSuccess(response: String) { postRequest.execute(object : SimpleCallBack<String>() {
Log.i(TAG, response) override fun onError(e: ApiException) {
try { XToastUtils.error(e.displayMessage)
val resp: BaseResponse<List<CallInfo>?> = Gson().fromJson(response, object : TypeToken<BaseResponse<List<CallInfo>?>>() {}.type) }
if (resp.code == 200) {
//XToastUtils.success(ResUtils.getString(R.string.request_succeeded)) override fun onSuccess(response: String) {
pageNum++ Log.i(TAG, response)
if (refresh) { try {
mAdapter!!.refresh(resp.data) var json = response
binding!!.refreshLayout.finishRefresh() if (HttpServerUtils.clientSafetyMeasures == 2) {
binding!!.recyclerView.scrollToPosition(0) val publicKey = RSACrypt.getPublicKey(HttpServerUtils.clientSignKey.toString())
} else { json = RSACrypt.decryptByPublicKey(json, publicKey)
mAdapter!!.loadMore(resp.data) json = String(Base64.decode(json))
binding!!.refreshLayout.finishLoadMore()
}
} else {
XToastUtils.error(ResUtils.getString(R.string.request_failed) + resp.msg)
}
} catch (e: Exception) {
e.printStackTrace()
XToastUtils.error(ResUtils.getString(R.string.request_failed) + response)
} }
val resp: BaseResponse<List<CallInfo>?> = Gson().fromJson(json, object : TypeToken<BaseResponse<List<CallInfo>?>>() {}.type)
if (resp.code == 200) {
pageNum++
if (refresh) {
mAdapter!!.refresh(resp.data)
binding!!.refreshLayout.finishRefresh()
binding!!.recyclerView.scrollToPosition(0)
} else {
mAdapter!!.loadMore(resp.data)
binding!!.refreshLayout.finishLoadMore()
}
} else {
XToastUtils.error(ResUtils.getString(R.string.request_failed) + resp.msg)
}
} catch (e: Exception) {
e.printStackTrace()
XToastUtils.error(ResUtils.getString(R.string.request_failed) + response)
} }
}
}) })
} }

View File

@ -18,10 +18,8 @@ import com.idormy.sms.forwarder.core.BaseFragment
import com.idormy.sms.forwarder.databinding.FragmentClientCloneBinding import com.idormy.sms.forwarder.databinding.FragmentClientCloneBinding
import com.idormy.sms.forwarder.entity.CloneInfo import com.idormy.sms.forwarder.entity.CloneInfo
import com.idormy.sms.forwarder.server.model.BaseResponse import com.idormy.sms.forwarder.server.model.BaseResponse
import com.idormy.sms.forwarder.utils.CommonUtils import com.idormy.sms.forwarder.utils.*
import com.idormy.sms.forwarder.utils.HttpServerUtils import com.idormy.sms.forwarder.utils.Base64
import com.idormy.sms.forwarder.utils.SettingUtils
import com.idormy.sms.forwarder.utils.XToastUtils
import com.xuexiang.xaop.annotation.SingleClick import com.xuexiang.xaop.annotation.SingleClick
import com.xuexiang.xhttp2.XHttp import com.xuexiang.xhttp2.XHttp
import com.xuexiang.xhttp2.cache.model.CacheMode import com.xuexiang.xhttp2.cache.model.CacheMode
@ -181,12 +179,7 @@ class CloneFragment : BaseFragment<FragmentClientCloneBinding?>(), View.OnClickL
XToastUtils.error(getString(R.string.export_failed)) XToastUtils.error(getString(R.string.export_failed))
} }
} catch (e: Exception) { } catch (e: Exception) {
XToastUtils.error( XToastUtils.error(String.format(getString(R.string.export_failed_tips), e.message))
String.format(
getString(R.string.export_failed_tips),
e.message
)
)
} }
} }
//导入配置 //导入配置
@ -225,12 +218,7 @@ class CloneFragment : BaseFragment<FragmentClientCloneBinding?>(), View.OnClickL
XToastUtils.error(getString(R.string.import_failed)) XToastUtils.error(getString(R.string.import_failed))
} }
} catch (e: Exception) { } catch (e: Exception) {
XToastUtils.error( XToastUtils.error(String.format(getString(R.string.import_failed_tips), e.message))
String.format(
getString(R.string.import_failed_tips),
e.message
)
)
} }
} }
} }
@ -258,43 +246,59 @@ class CloneFragment : BaseFragment<FragmentClientCloneBinding?>(), View.OnClickL
} }
msgMap["data"] = HttpServerUtils.exportSettings() msgMap["data"] = HttpServerUtils.exportSettings()
val requestMsg: String = Gson().toJson(msgMap) var requestMsg: String = Gson().toJson(msgMap)
Log.i(TAG, "requestMsg:$requestMsg") Log.i(TAG, "requestMsg:$requestMsg")
XHttp.post(requestUrl) val postRequest = XHttp.post(requestUrl)
.upJson(requestMsg)
.keepJson(true) .keepJson(true)
.timeOut((SettingUtils.requestTimeout * 1000).toLong()) //超时时间10s .timeOut((SettingUtils.requestTimeout * 1000).toLong()) //超时时间10s
.cacheMode(CacheMode.NO_CACHE) .cacheMode(CacheMode.NO_CACHE)
//.retryCount(SettingUtils.requestRetryTimes) //超时重试的次数
//.retryDelay(SettingUtils.requestDelayTime) //超时重试的延迟时间
//.retryIncreaseDelay(SettingUtils.requestDelayTime) //超时重试叠加延时
.timeStamp(true) .timeStamp(true)
.execute(object : SimpleCallBack<String>() {
override fun onError(e: ApiException) { if (HttpServerUtils.clientSafetyMeasures == 2) {
XToastUtils.error(e.displayMessage) val publicKey = RSACrypt.getPublicKey(HttpServerUtils.clientSignKey.toString())
} try {
requestMsg = Base64.encode(requestMsg.toByteArray())
requestMsg = RSACrypt.encryptByPublicKey(requestMsg, publicKey)
Log.i(TAG, "requestMsg: $requestMsg")
} catch (e: Exception) {
XToastUtils.error(ResUtils.getString(R.string.request_failed) + e.message)
e.printStackTrace()
return
}
postRequest.upString(requestMsg)
} else {
postRequest.upJson(requestMsg)
}
override fun onSuccess(response: String) { postRequest.execute(object : SimpleCallBack<String>() {
Log.i(TAG, response) override fun onError(e: ApiException) {
try { XToastUtils.error(e.displayMessage)
val resp: BaseResponse<String> = Gson().fromJson( pushCountDownHelper?.finish()
response, }
object : TypeToken<BaseResponse<String>>() {}.type
) override fun onSuccess(response: String) {
if (resp.code == 200) { Log.i(TAG, response)
XToastUtils.success(ResUtils.getString(R.string.request_succeeded)) try {
} else { var json = response
XToastUtils.error(ResUtils.getString(R.string.request_failed) + resp.msg) if (HttpServerUtils.clientSafetyMeasures == 2) {
} val publicKey = RSACrypt.getPublicKey(HttpServerUtils.clientSignKey.toString())
} catch (e: Exception) { json = RSACrypt.decryptByPublicKey(json, publicKey)
e.printStackTrace() json = String(Base64.decode(json))
XToastUtils.error(ResUtils.getString(R.string.request_failed) + response)
} }
val resp: BaseResponse<String> = Gson().fromJson(json, object : TypeToken<BaseResponse<String>>() {}.type)
if (resp.code == 200) {
XToastUtils.success(ResUtils.getString(R.string.request_succeeded))
} else {
XToastUtils.error(ResUtils.getString(R.string.request_failed) + resp.msg)
}
} catch (e: Exception) {
e.printStackTrace()
XToastUtils.error(ResUtils.getString(R.string.request_failed) + response)
} }
pushCountDownHelper?.finish()
}) }
})
} }
@ -323,62 +327,80 @@ class CloneFragment : BaseFragment<FragmentClientCloneBinding?>(), View.OnClickL
dataMap["version_code"] = AppUtils.getAppVersionCode() dataMap["version_code"] = AppUtils.getAppVersionCode()
msgMap["data"] = dataMap msgMap["data"] = dataMap
val requestMsg: String = Gson().toJson(msgMap) var requestMsg: String = Gson().toJson(msgMap)
Log.i(TAG, "requestMsg:$requestMsg") Log.i(TAG, "requestMsg:$requestMsg")
XHttp.post(requestUrl) val postRequest = XHttp.post(requestUrl)
.upJson(requestMsg)
.keepJson(true) .keepJson(true)
.timeOut((SettingUtils.requestTimeout * 1000).toLong()) //超时时间10s .timeOut((SettingUtils.requestTimeout * 1000).toLong()) //超时时间10s
.cacheMode(CacheMode.NO_CACHE) .cacheMode(CacheMode.NO_CACHE)
//.retryCount(SettingUtils.requestRetryTimes) //超时重试的次数
//.retryDelay(SettingUtils.requestDelayTime) //超时重试的延迟时间
//.retryIncreaseDelay(SettingUtils.requestDelayTime) //超时重试叠加延时
.timeStamp(true) .timeStamp(true)
.execute(object : SimpleCallBack<String>() {
override fun onError(e: ApiException) { if (HttpServerUtils.clientSafetyMeasures == 2) {
XToastUtils.error(e.displayMessage) val publicKey = RSACrypt.getPublicKey(HttpServerUtils.clientSignKey.toString())
} try {
requestMsg = Base64.encode(requestMsg.toByteArray())
requestMsg = RSACrypt.encryptByPublicKey(requestMsg, publicKey)
Log.i(TAG, "requestMsg: $requestMsg")
} catch (e: Exception) {
XToastUtils.error(ResUtils.getString(R.string.request_failed) + e.message)
e.printStackTrace()
return
}
postRequest.upString(requestMsg)
} else {
postRequest.upJson(requestMsg)
}
override fun onSuccess(response: String) { postRequest.execute(object : SimpleCallBack<String>() {
Log.i(TAG, response) override fun onError(e: ApiException) {
try { XToastUtils.error(e.displayMessage)
//替换Date字段为当前时间 exportCountDownHelper?.finish()
val builder = GsonBuilder() }
builder.registerTypeAdapter(
Date::class.java,
JsonDeserializer<Any?> { _, _, _ -> Date() })
val gson = builder.create()
val resp: BaseResponse<CloneInfo> = gson.fromJson(
response,
object : TypeToken<BaseResponse<CloneInfo>>() {}.type
)
if (resp.code == 200) {
val cloneInfo = resp.data
Log.d(TAG, "cloneInfo = $cloneInfo")
if (cloneInfo == null) { override fun onSuccess(response: String) {
XToastUtils.error(ResUtils.getString(R.string.request_failed)) Log.i(TAG, response)
return try {
} var json = response
if (HttpServerUtils.clientSafetyMeasures == 2) {
//判断版本是否一致 val publicKey = RSACrypt.getPublicKey(HttpServerUtils.clientSignKey.toString())
HttpServerUtils.compareVersion(cloneInfo) json = RSACrypt.decryptByPublicKey(json, publicKey)
json = String(Base64.decode(json))
if (HttpServerUtils.restoreSettings(cloneInfo)) {
XToastUtils.success(getString(R.string.import_succeeded))
}
} else {
XToastUtils.error(ResUtils.getString(R.string.request_failed) + resp.msg)
}
} catch (e: Exception) {
e.printStackTrace()
XToastUtils.error(ResUtils.getString(R.string.request_failed) + response)
} }
}
}) //替换Date字段为当前时间
val builder = GsonBuilder()
builder.registerTypeAdapter(
Date::class.java,
JsonDeserializer<Any?> { _, _, _ -> Date() })
val gson = builder.create()
val resp: BaseResponse<CloneInfo> = gson.fromJson(json, object : TypeToken<BaseResponse<CloneInfo>>() {}.type)
if (resp.code == 200) {
val cloneInfo = resp.data
Log.d(TAG, "cloneInfo = $cloneInfo")
if (cloneInfo == null) {
XToastUtils.error(ResUtils.getString(R.string.request_failed))
return
}
//判断版本是否一致
HttpServerUtils.compareVersion(cloneInfo)
if (HttpServerUtils.restoreSettings(cloneInfo)) {
XToastUtils.success(getString(R.string.import_succeeded))
}
} else {
XToastUtils.error(ResUtils.getString(R.string.request_failed) + resp.msg)
}
} catch (e: Exception) {
e.printStackTrace()
XToastUtils.error(ResUtils.getString(R.string.request_failed) + response)
}
exportCountDownHelper?.finish()
}
})
} }

View File

@ -190,43 +190,60 @@ class ContactQueryFragment : BaseFragment<FragmentClientContactQueryBinding?>()
else else
ContactQueryData(1, 20, null, keyword) ContactQueryData(1, 20, null, keyword)
val requestMsg: String = Gson().toJson(msgMap) var requestMsg: String = Gson().toJson(msgMap)
Log.i(TAG, "requestMsg:$requestMsg") Log.i(TAG, "requestMsg:$requestMsg")
XHttp.post(requestUrl) val postRequest = XHttp.post(requestUrl)
.upJson(requestMsg)
.keepJson(true) .keepJson(true)
.timeOut((SettingUtils.requestTimeout * 1000).toLong()) //超时时间10s .timeOut((SettingUtils.requestTimeout * 1000).toLong()) //超时时间10s
.cacheMode(CacheMode.NO_CACHE) .cacheMode(CacheMode.NO_CACHE)
//.retryCount(SettingUtils.requestRetryTimes) //超时重试的次数
//.retryDelay(SettingUtils.requestDelayTime) //超时重试的延迟时间
//.retryIncreaseDelay(SettingUtils.requestDelayTime) //超时重试叠加延时
.timeStamp(true) .timeStamp(true)
.execute(object : SimpleCallBack<String>() {
override fun onError(e: ApiException) { if (HttpServerUtils.clientSafetyMeasures == 2) {
XToastUtils.error(e.displayMessage) val publicKey = RSACrypt.getPublicKey(HttpServerUtils.clientSignKey.toString())
} try {
requestMsg = Base64.encode(requestMsg.toByteArray())
requestMsg = RSACrypt.encryptByPublicKey(requestMsg, publicKey)
Log.i(TAG, "requestMsg: $requestMsg")
} catch (e: Exception) {
XToastUtils.error(ResUtils.getString(R.string.request_failed) + e.message)
e.printStackTrace()
return
}
postRequest.upString(requestMsg)
} else {
postRequest.upJson(requestMsg)
}
override fun onSuccess(response: String) { postRequest.execute(object : SimpleCallBack<String>() {
Log.i(TAG, response) override fun onError(e: ApiException) {
try { XToastUtils.error(e.displayMessage)
val resp: BaseResponse<List<ContactInfo>?> = Gson().fromJson(response, object : TypeToken<BaseResponse<List<ContactInfo>?>>() {}.type) }
if (resp.code == 200) {
//XToastUtils.success(ResUtils.getString(R.string.request_succeeded)) override fun onSuccess(response: String) {
mAdapter!!.refresh(resp.data) Log.i(TAG, response)
binding!!.refreshLayout.finishRefresh() try {
binding!!.recyclerView.scrollToPosition(0) var json = response
} else { if (HttpServerUtils.clientSafetyMeasures == 2) {
XToastUtils.error(ResUtils.getString(R.string.request_failed) + resp.msg) val publicKey = RSACrypt.getPublicKey(HttpServerUtils.clientSignKey.toString())
} json = RSACrypt.decryptByPublicKey(json, publicKey)
} catch (e: Exception) { json = String(Base64.decode(json))
e.printStackTrace()
XToastUtils.error(ResUtils.getString(R.string.request_failed) + response)
} }
val resp: BaseResponse<List<ContactInfo>?> = Gson().fromJson(json, object : TypeToken<BaseResponse<List<ContactInfo>?>>() {}.type)
if (resp.code == 200) {
//XToastUtils.success(ResUtils.getString(R.string.request_succeeded))
mAdapter!!.refresh(resp.data)
binding!!.refreshLayout.finishRefresh()
binding!!.recyclerView.scrollToPosition(0)
} else {
XToastUtils.error(ResUtils.getString(R.string.request_failed) + resp.msg)
}
} catch (e: Exception) {
e.printStackTrace()
XToastUtils.error(ResUtils.getString(R.string.request_failed) + response)
} }
}
}) })
} }

View File

@ -197,46 +197,66 @@ class SmsQueryFragment : BaseFragment<FragmentClientSmsQueryBinding?>() {
if (refresh) pageNum = 1 if (refresh) pageNum = 1
msgMap["data"] = SmsQueryData(smsType, pageNum, pageSize, keyword) msgMap["data"] = SmsQueryData(smsType, pageNum, pageSize, keyword)
val requestMsg: String = Gson().toJson(msgMap) var requestMsg: String = Gson().toJson(msgMap)
Log.i(TAG, "requestMsg:$requestMsg") Log.i(TAG, "requestMsg:$requestMsg")
XHttp.post(requestUrl) val postRequest = XHttp.post(requestUrl)
.upJson(requestMsg)
.keepJson(true) .keepJson(true)
.timeOut((SettingUtils.requestTimeout * 1000).toLong()) //超时时间10s .timeOut((SettingUtils.requestTimeout * 1000).toLong()) //超时时间10s
.cacheMode(CacheMode.NO_CACHE) .cacheMode(CacheMode.NO_CACHE)
.timeStamp(true) .timeStamp(true)
.execute(object : SimpleCallBack<String>() {
override fun onError(e: ApiException) { if (HttpServerUtils.clientSafetyMeasures == 2) {
XToastUtils.error(e.displayMessage) val publicKey = RSACrypt.getPublicKey(HttpServerUtils.clientSignKey.toString())
} try {
requestMsg = Base64.encode(requestMsg.toByteArray())
requestMsg = RSACrypt.encryptByPublicKey(requestMsg, publicKey)
Log.i(TAG, "requestMsg: $requestMsg")
} catch (e: Exception) {
XToastUtils.error(ResUtils.getString(R.string.request_failed) + e.message)
e.printStackTrace()
return
}
postRequest.upString(requestMsg)
} else {
postRequest.upJson(requestMsg)
}
override fun onSuccess(response: String) { postRequest.execute(object : SimpleCallBack<String>() {
Log.i(TAG, response) override fun onError(e: ApiException) {
try { XToastUtils.error(e.displayMessage)
val resp: BaseResponse<List<SmsInfo>?> = Gson().fromJson(response, object : TypeToken<BaseResponse<List<SmsInfo>?>>() {}.type) }
if (resp.code == 200) {
//XToastUtils.success(ResUtils.getString(R.string.request_succeeded)) override fun onSuccess(response: String) {
pageNum++ Log.i(TAG, response)
if (refresh) { try {
mAdapter!!.refresh(resp.data) var json = response
binding!!.refreshLayout.finishRefresh() if (HttpServerUtils.clientSafetyMeasures == 2) {
binding!!.recyclerView.scrollToPosition(0) val publicKey = RSACrypt.getPublicKey(HttpServerUtils.clientSignKey.toString())
} else { json = RSACrypt.decryptByPublicKey(json, publicKey)
mAdapter!!.loadMore(resp.data) json = String(Base64.decode(json))
binding!!.refreshLayout.finishLoadMore()
}
} else {
XToastUtils.error(ResUtils.getString(R.string.request_failed) + resp.msg)
}
} catch (e: Exception) {
e.printStackTrace()
XToastUtils.error(ResUtils.getString(R.string.request_failed) + response)
} }
val resp: BaseResponse<List<SmsInfo>?> = Gson().fromJson(json, object : TypeToken<BaseResponse<List<SmsInfo>?>>() {}.type)
if (resp.code == 200) {
//XToastUtils.success(ResUtils.getString(R.string.request_succeeded))
pageNum++
if (refresh) {
mAdapter!!.refresh(resp.data)
binding!!.refreshLayout.finishRefresh()
binding!!.recyclerView.scrollToPosition(0)
} else {
mAdapter!!.loadMore(resp.data)
binding!!.refreshLayout.finishLoadMore()
}
} else {
XToastUtils.error(ResUtils.getString(R.string.request_failed) + resp.msg)
}
} catch (e: Exception) {
e.printStackTrace()
XToastUtils.error(ResUtils.getString(R.string.request_failed) + response)
} }
}
}) })
} }

View File

@ -112,43 +112,60 @@ class SmsSendFragment : BaseFragment<FragmentClientSmsSendBinding?>(), View.OnCl
dataMap["msg_content"] = msgContent dataMap["msg_content"] = msgContent
msgMap["data"] = dataMap msgMap["data"] = dataMap
val requestMsg: String = Gson().toJson(msgMap) var requestMsg: String = Gson().toJson(msgMap)
Log.i(TAG, "requestMsg:$requestMsg") Log.i(TAG, "requestMsg:$requestMsg")
mCountDownHelper?.start() val postRequest = XHttp.post(requestUrl)
XHttp.post(requestUrl)
.upJson(requestMsg)
.keepJson(true) .keepJson(true)
.timeOut((SettingUtils.requestTimeout * 1000).toLong()) //超时时间10s .timeOut((SettingUtils.requestTimeout * 1000).toLong()) //超时时间10s
.cacheMode(CacheMode.NO_CACHE) .cacheMode(CacheMode.NO_CACHE)
//.retryCount(SettingUtils.requestRetryTimes) //超时重试的次数
//.retryDelay(SettingUtils.requestDelayTime) //超时重试的延迟时间
//.retryIncreaseDelay(SettingUtils.requestDelayTime) //超时重试叠加延时
.timeStamp(true) .timeStamp(true)
.execute(object : SimpleCallBack<String>() {
override fun onError(e: ApiException) { if (HttpServerUtils.clientSafetyMeasures == 2) {
XToastUtils.error(e.displayMessage) val publicKey = RSACrypt.getPublicKey(HttpServerUtils.clientSignKey.toString())
mCountDownHelper?.finish() try {
} requestMsg = Base64.encode(requestMsg.toByteArray())
requestMsg = RSACrypt.encryptByPublicKey(requestMsg, publicKey)
Log.i(TAG, "requestMsg: $requestMsg")
} catch (e: Exception) {
XToastUtils.error(ResUtils.getString(R.string.request_failed) + e.message)
e.printStackTrace()
return
}
postRequest.upString(requestMsg)
} else {
postRequest.upJson(requestMsg)
}
override fun onSuccess(response: String) { mCountDownHelper?.start()
Log.i(TAG, response) postRequest.execute(object : SimpleCallBack<String>() {
try { override fun onError(e: ApiException) {
val resp: BaseResponse<String> = Gson().fromJson(response, object : TypeToken<BaseResponse<String>>() {}.type) XToastUtils.error(e.displayMessage)
if (resp.code == 200) { mCountDownHelper?.finish()
XToastUtils.success(ResUtils.getString(R.string.request_succeeded)) }
} else {
XToastUtils.error(ResUtils.getString(R.string.request_failed) + resp.msg) override fun onSuccess(response: String) {
} Log.i(TAG, response)
} catch (e: Exception) { try {
e.printStackTrace() var json = response
XToastUtils.error(ResUtils.getString(R.string.request_failed) + response) if (HttpServerUtils.clientSafetyMeasures == 2) {
val publicKey = RSACrypt.getPublicKey(HttpServerUtils.clientSignKey.toString())
json = RSACrypt.decryptByPublicKey(json, publicKey)
json = String(Base64.decode(json))
} }
mCountDownHelper?.finish() val resp: BaseResponse<String> = Gson().fromJson(json, object : TypeToken<BaseResponse<String>>() {}.type)
if (resp.code == 200) {
XToastUtils.success(ResUtils.getString(R.string.request_succeeded))
} else {
XToastUtils.error(ResUtils.getString(R.string.request_failed) + resp.msg)
}
} catch (e: Exception) {
e.printStackTrace()
XToastUtils.error(ResUtils.getString(R.string.request_failed) + response)
} }
mCountDownHelper?.finish()
}) }
})
} }
else -> {} else -> {}
} }

View File

@ -10,9 +10,7 @@ import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.core.BaseFragment import com.idormy.sms.forwarder.core.BaseFragment
import com.idormy.sms.forwarder.databinding.FragmentClientWolSendBinding import com.idormy.sms.forwarder.databinding.FragmentClientWolSendBinding
import com.idormy.sms.forwarder.server.model.BaseResponse import com.idormy.sms.forwarder.server.model.BaseResponse
import com.idormy.sms.forwarder.utils.HttpServerUtils import com.idormy.sms.forwarder.utils.*
import com.idormy.sms.forwarder.utils.SettingUtils
import com.idormy.sms.forwarder.utils.XToastUtils
import com.xuexiang.xaop.annotation.SingleClick import com.xuexiang.xaop.annotation.SingleClick
import com.xuexiang.xhttp2.XHttp import com.xuexiang.xhttp2.XHttp
import com.xuexiang.xhttp2.cache.model.CacheMode import com.xuexiang.xhttp2.cache.model.CacheMode
@ -144,46 +142,63 @@ class WolSendFragment : BaseFragment<FragmentClientWolSendBinding?>(), View.OnCl
dataMap["port"] = port dataMap["port"] = port
msgMap["data"] = dataMap msgMap["data"] = dataMap
val requestMsg: String = Gson().toJson(msgMap) var requestMsg: String = Gson().toJson(msgMap)
Log.i(TAG, "requestMsg:$requestMsg") Log.i(TAG, "requestMsg:$requestMsg")
mCountDownHelper?.start() val postRequest = XHttp.post(requestUrl)
XHttp.post(requestUrl)
.upJson(requestMsg)
.keepJson(true) .keepJson(true)
.timeOut((SettingUtils.requestTimeout * 1000).toLong()) //超时时间10s .timeOut((SettingUtils.requestTimeout * 1000).toLong()) //超时时间10s
.cacheMode(CacheMode.NO_CACHE) .cacheMode(CacheMode.NO_CACHE)
.timeStamp(true) .timeStamp(true)
.execute(object : SimpleCallBack<String>() {
override fun onError(e: ApiException) { if (HttpServerUtils.clientSafetyMeasures == 2) {
XToastUtils.error(e.displayMessage) val publicKey = RSACrypt.getPublicKey(HttpServerUtils.clientSignKey.toString())
mCountDownHelper?.finish() try {
} requestMsg = Base64.encode(requestMsg.toByteArray())
requestMsg = RSACrypt.encryptByPublicKey(requestMsg, publicKey)
Log.i(TAG, "requestMsg: $requestMsg")
} catch (e: Exception) {
XToastUtils.error(ResUtils.getString(R.string.request_failed) + e.message)
e.printStackTrace()
return
}
postRequest.upString(requestMsg)
} else {
postRequest.upJson(requestMsg)
}
override fun onSuccess(response: String) { mCountDownHelper?.start()
Log.i(TAG, response) postRequest.execute(object : SimpleCallBack<String>() {
try { override fun onError(e: ApiException) {
val resp: BaseResponse<String> = Gson().fromJson( XToastUtils.error(e.displayMessage)
response, mCountDownHelper?.finish()
object : TypeToken<BaseResponse<String>>() {}.type }
)
if (resp.code == 200) { override fun onSuccess(response: String) {
XToastUtils.success(ResUtils.getString(R.string.request_succeeded)) Log.i(TAG, response)
//添加到历史记录 try {
wolHistory[mac] = ip var json = response
HttpServerUtils.wolHistory = Gson().toJson(wolHistory) if (HttpServerUtils.clientSafetyMeasures == 2) {
} else { val publicKey = RSACrypt.getPublicKey(HttpServerUtils.clientSignKey.toString())
XToastUtils.error(ResUtils.getString(R.string.request_failed) + resp.msg) json = RSACrypt.decryptByPublicKey(json, publicKey)
} json = String(Base64.decode(json))
} catch (e: Exception) {
e.printStackTrace()
XToastUtils.error(ResUtils.getString(R.string.request_failed) + response)
} }
mCountDownHelper?.finish() val resp: BaseResponse<String> = Gson().fromJson(json, object : TypeToken<BaseResponse<String>>() {}.type)
if (resp.code == 200) {
XToastUtils.success(ResUtils.getString(R.string.request_succeeded))
//添加到历史记录
wolHistory[mac] = ip
HttpServerUtils.wolHistory = Gson().toJson(wolHistory)
} else {
XToastUtils.error(ResUtils.getString(R.string.request_failed) + resp.msg)
}
} catch (e: Exception) {
e.printStackTrace()
XToastUtils.error(ResUtils.getString(R.string.request_failed) + response)
} }
mCountDownHelper?.finish()
}) }
})
} }
else -> {} else -> {}
} }

View File

@ -1,13 +1,17 @@
package com.idormy.sms.forwarder.server.component package com.idormy.sms.forwarder.server.component
import android.text.TextUtils
import android.util.Log import android.util.Log
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
import com.idormy.sms.forwarder.server.model.BaseRequest import com.idormy.sms.forwarder.server.model.BaseRequest
import com.idormy.sms.forwarder.utils.Base64
import com.idormy.sms.forwarder.utils.HttpServerUtils import com.idormy.sms.forwarder.utils.HttpServerUtils
import com.idormy.sms.forwarder.utils.RSACrypt
import com.xuexiang.xrouter.utils.TextUtils
import com.yanzhenjie.andserver.annotation.Converter import com.yanzhenjie.andserver.annotation.Converter
import com.yanzhenjie.andserver.error.HttpException
import com.yanzhenjie.andserver.framework.MessageConverter import com.yanzhenjie.andserver.framework.MessageConverter
import com.yanzhenjie.andserver.framework.body.JsonBody import com.yanzhenjie.andserver.framework.body.JsonBody
import com.yanzhenjie.andserver.framework.body.StringBody
import com.yanzhenjie.andserver.http.ResponseBody import com.yanzhenjie.andserver.http.ResponseBody
import com.yanzhenjie.andserver.util.IOUtils import com.yanzhenjie.andserver.util.IOUtils
import com.yanzhenjie.andserver.util.MediaType import com.yanzhenjie.andserver.util.MediaType
@ -25,7 +29,17 @@ class AppMessageConverter : MessageConverter {
override fun convert(output: Any?, mediaType: MediaType?): ResponseBody { override fun convert(output: Any?, mediaType: MediaType?): ResponseBody {
//返回统一结构报文 //返回统一结构报文
return JsonBody(HttpServerUtils.response(output)) var response = HttpServerUtils.response(output)
Log.d(TAG, "response: $response")
if (HttpServerUtils.safetyMeasures != 2) {
return JsonBody(response)
}
val privateKey = RSACrypt.getPrivateKey(HttpServerUtils.serverPrivateKey.toString())
response = Base64.encode(response.toByteArray())
response = RSACrypt.encryptByPrivateKey(response, privateKey)
return StringBody(response)
} }
@Throws(IOException::class) @Throws(IOException::class)
@ -33,9 +47,21 @@ class AppMessageConverter : MessageConverter {
val charset: Charset? = mediaType?.charset val charset: Charset? = mediaType?.charset
Log.d(TAG, "Charset: $charset") Log.d(TAG, "Charset: $charset")
val json = if (charset == null) IOUtils.toString(stream) else IOUtils.toString(stream, charset) var json = if (charset == null) IOUtils.toString(stream) else IOUtils.toString(stream, charset)
Log.d(TAG, "Json: $json") Log.d(TAG, "Json: $json")
if (HttpServerUtils.safetyMeasures == 2) {
if (TextUtils.isEmpty(HttpServerUtils.serverPrivateKey)) {
Log.e(TAG, "RSA解密失败: 私钥为空")
throw HttpException(500, "服务端未配置私钥")
}
val privateKey = RSACrypt.getPrivateKey(HttpServerUtils.serverPrivateKey.toString())
json = RSACrypt.decryptByPrivateKey(json, privateKey)
json = String(Base64.decode(json))
Log.d(TAG, "Json: $json")
}
//修改接口数据中的null、“”为默认值 //修改接口数据中的null、“”为默认值
val builder = GsonBuilder() val builder = GsonBuilder()
builder.registerTypeAdapter(Int::class.java, IntegerDefaultAdapter()) builder.registerTypeAdapter(Int::class.java, IntegerDefaultAdapter())
@ -45,7 +71,7 @@ class AppMessageConverter : MessageConverter {
Log.d(TAG, "Bean: $t") Log.d(TAG, "Bean: $t")
//校验时间戳时间误差不能超过1小时&& 签名 //校验时间戳时间误差不能超过1小时&& 签名
if (!TextUtils.isEmpty(HttpServerUtils.serverSignKey)) { if (HttpServerUtils.safetyMeasures == 1) {
HttpServerUtils.checkSign(t as BaseRequest<*>) HttpServerUtils.checkSign(t as BaseRequest<*>)
} }

View File

@ -0,0 +1,88 @@
package com.idormy.sms.forwarder.utils
import java.io.UnsupportedEncodingException
/**
* Base64编码解码
*/
object Base64 {
private val base64EncodeChars = charArrayOf('A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/')
private val base64DecodeChars = byteArrayOf(-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1)
fun encode(data: ByteArray): String {
val sb = StringBuffer()
val len = data.size
var i = 0
var b1: Int
var b2: Int
var b3: Int
while (i < len) {
b1 = ((data[i++]).toInt() and 0xff)
if (i == len) {
sb.append(base64EncodeChars[b1.ushr(2)])
sb.append(base64EncodeChars[b1 and 0x3 shl 4])
sb.append("==")
break
}
b2 = (data[i++]).toInt() and 0xff
if (i == len) {
sb.append(base64EncodeChars[b1.ushr(2)])
sb.append(base64EncodeChars[b1 and 0x03 shl 4 or (b2 and 0xf0).ushr(4)])
sb.append(base64EncodeChars[b2 and 0x0f shl 2])
sb.append("=")
break
}
b3 = (data[i++]).toInt() and 0xff
sb.append(base64EncodeChars[b1.ushr(2)])
sb.append(base64EncodeChars[b1 and 0x03 shl 4 or (b2 and 0xf0).ushr(4)])
sb.append(base64EncodeChars[b2 and 0x0f shl 2 or (b3 and 0xc0).ushr(6)])
sb.append(base64EncodeChars[b3 and 0x3f])
}
return sb.toString()
}
@Throws(UnsupportedEncodingException::class)
fun decode(str: String): ByteArray {
val sb = StringBuffer()
val data = str.toByteArray(charset("US-ASCII"))
val len = data.size
var i = 0
var b1: Int
var b2: Int
var b3: Int
var b4: Int
while (i < len) {
/* b1 */
do {
b1 = base64DecodeChars[(data[i++]).toInt()].toInt()
} while (i < len && b1 == -1)
if (b1 == -1) break
/* b2 */
do {
b2 = base64DecodeChars[(data[i++]).toInt()].toInt()
} while (i < len && b2 == -1)
if (b2 == -1) break
sb.append((b1 shl 2 or (b2 and 0x30).ushr(4)).toChar())
/* b3 */
do {
b3 = data[i++].toInt()
if (b3 == 61) return sb.toString().toByteArray(charset("ISO-8859-1"))
b3 = base64DecodeChars[b3].toInt()
} while (i < len && b3 == -1)
if (b3 == -1) break
sb.append((b2 and 0x0f shl 4 or (b3 and 0x3c).ushr(2)).toChar())
/* b4 */
do {
b4 = data[i++].toInt()
if (b4 == 61) return sb.toString().toByteArray(charset("ISO-8859-1"))
b4 = base64DecodeChars[b4].toInt()
} while (i < len && b4 == -1)
if (b4 == -1) break
sb.append((b3 and 0x03 shl 6 or b4).toChar())
}
return sb.toString().toByteArray(charset("ISO-8859-1"))
}
}

View File

@ -322,7 +322,11 @@ const val HTTP_SUCCESS_CODE: Int = 200
const val HTTP_FAILURE_CODE: Int = 500 const val HTTP_FAILURE_CODE: Int = 500
const val SP_ENABLE_SERVER = "enable_server" const val SP_ENABLE_SERVER = "enable_server"
const val SP_ENABLE_SERVER_AUTORUN = "enable_server_autorun" const val SP_ENABLE_SERVER_AUTORUN = "enable_server_autorun"
const val SP_SERVER_SAFETY_MEASURES = "server_safety_measures"
const val SP_SERVER_SIGN_KEY = "server_sign_key" const val SP_SERVER_SIGN_KEY = "server_sign_key"
const val SP_SERVER_TIME_TOLERANCE = "server_time_tolerance"
const val SP_SERVER_PUBLIC_KEY = "server_public_key"
const val SP_SERVER_PRIVATE_KEY = "server_private_key"
const val SP_SERVER_WEB_PATH = "server_web_path" const val SP_SERVER_WEB_PATH = "server_web_path"
const val SP_ENABLE_API_CLONE = "enable_api_clone" const val SP_ENABLE_API_CLONE = "enable_api_clone"
const val SP_ENABLE_API_SMS_SEND = "enable_api_sms_send" const val SP_ENABLE_API_SMS_SEND = "enable_api_sms_send"
@ -335,6 +339,7 @@ const val SP_WOL_HISTORY = "wol_history"
const val SP_SERVER_ADDRESS = "server_address" const val SP_SERVER_ADDRESS = "server_address"
const val SP_SERVER_HISTORY = "server_history" const val SP_SERVER_HISTORY = "server_history"
const val SP_SERVER_CONFIG = "server_config" const val SP_SERVER_CONFIG = "server_config"
const val SP_CLIENT_SAFETY_MEASURES = "client_safety_measures"
const val SP_CLIENT_SIGN_KEY = "client_sign_key" const val SP_CLIENT_SIGN_KEY = "client_sign_key"
var CLIENT_FRAGMENT_LIST = listOf( var CLIENT_FRAGMENT_LIST = listOf(
PageInfo( PageInfo(

View File

@ -32,6 +32,30 @@ class HttpServerUtils private constructor() {
MMKVUtils.put(SP_ENABLE_SERVER_AUTORUN, enableServerAutorun) MMKVUtils.put(SP_ENABLE_SERVER_AUTORUN, enableServerAutorun)
} }
//服务端安全设置
@JvmStatic
var safetyMeasures: Int
get() = MMKVUtils.getInt(SP_SERVER_SAFETY_MEASURES, if (TextUtils.isEmpty(serverSignKey)) 0 else 1)
set(safetyMeasures) {
MMKVUtils.put(SP_SERVER_SAFETY_MEASURES, safetyMeasures)
}
//服务端RSA公钥
@JvmStatic
var serverPublicKey: String?
get() = MMKVUtils.getString(SP_SERVER_PUBLIC_KEY, "")
set(serverPublicKey) {
MMKVUtils.put(SP_SERVER_PUBLIC_KEY, serverPublicKey)
}
//服务端RSA私钥
@JvmStatic
var serverPrivateKey: String?
get() = MMKVUtils.getString(SP_SERVER_PRIVATE_KEY, "")
set(serverPrivateKey) {
MMKVUtils.put(SP_SERVER_PRIVATE_KEY, serverPrivateKey)
}
//服务端签名密钥 //服务端签名密钥
@JvmStatic @JvmStatic
var serverSignKey: String? var serverSignKey: String?
@ -40,6 +64,14 @@ class HttpServerUtils private constructor() {
MMKVUtils.put(SP_SERVER_SIGN_KEY, serverSignKey) MMKVUtils.put(SP_SERVER_SIGN_KEY, serverSignKey)
} }
//时间容差
@JvmStatic
var timeTolerance: Int
get() = MMKVUtils.getInt(SP_SERVER_TIME_TOLERANCE, 600)
set(timeTolerance) {
MMKVUtils.put(SP_SERVER_TIME_TOLERANCE, timeTolerance)
}
//自定义web客户端目录 //自定义web客户端目录
@JvmStatic @JvmStatic
var serverWebPath: String? var serverWebPath: String?
@ -72,7 +104,15 @@ class HttpServerUtils private constructor() {
MMKVUtils.put(SP_SERVER_CONFIG, serverConfig) MMKVUtils.put(SP_SERVER_CONFIG, serverConfig)
} }
//客户端签名密钥 //服务端安全设置
@JvmStatic
var clientSafetyMeasures: Int
get() = MMKVUtils.getInt(SP_CLIENT_SAFETY_MEASURES, if (TextUtils.isEmpty(clientSignKey)) 0 else 1)
set(clientSafetyMeasures) {
MMKVUtils.put(SP_CLIENT_SAFETY_MEASURES, clientSafetyMeasures)
}
//客户端签名密钥/RSA公钥
@JvmStatic @JvmStatic
var clientSignKey: String? var clientSignKey: String?
get() = MMKVUtils.getString(SP_CLIENT_SIGN_KEY, "") get() = MMKVUtils.getString(SP_CLIENT_SIGN_KEY, "")
@ -164,8 +204,9 @@ class HttpServerUtils private constructor() {
val timestamp = System.currentTimeMillis() val timestamp = System.currentTimeMillis()
val diffTime = kotlin.math.abs(timestamp - req.timestamp) val diffTime = kotlin.math.abs(timestamp - req.timestamp)
if (diffTime > 3600000L) { val tolerance = timeTolerance * 1000L
throw HttpException(500, String.format(getString(R.string.timestamp_verify_failed), timestamp, diffTime)) if (diffTime > tolerance) {
throw HttpException(500, String.format(getString(R.string.timestamp_verify_failed), timestamp, timeTolerance, diffTime))
} }
val sign = calcSign(req.timestamp.toString(), signSecret.toString()) val sign = calcSign(req.timestamp.toString(), signSecret.toString())
@ -306,7 +347,7 @@ class HttpServerUtils private constructor() {
if (output != null) { if (output != null) {
resp["data"] = output resp["data"] = output
} }
if (!TextUtils.isEmpty(serverSignKey)) { if (safetyMeasures == 1) {
resp["sign"] = calcSign(timestamp.toString(), serverSignKey.toString()) resp["sign"] = calcSign(timestamp.toString(), serverSignKey.toString())
} }
} }

View File

@ -0,0 +1,210 @@
package com.idormy.sms.forwarder.utils
import java.io.ByteArrayOutputStream
import java.security.KeyFactory
import java.security.PrivateKey
import java.security.PublicKey
import java.security.spec.PKCS8EncodedKeySpec
import java.security.spec.X509EncodedKeySpec
import javax.crypto.Cipher
/**
* 非对称加密RSA加密和解密
*/
@Suppress("unused")
object RSACrypt {
private const val transformation = "RSA"
private const val ENCRYPT_MAX_SIZE = 245
private const val DECRYPT_MAX_SIZE = 256
/**
* 私钥加密
* @param input 原文
* @param privateKey 私钥
*/
fun encryptByPrivateKey(input: String, privateKey: PrivateKey): String {
//创建cipher对象
val cipher = Cipher.getInstance(transformation)
//初始化cipher
cipher.init(Cipher.ENCRYPT_MODE, privateKey)
//****非对称加密****
val byteArray = input.toByteArray()
//分段加密
var temp: ByteArray?
var offset = 0 //当前偏移的位置
val outputStream = ByteArrayOutputStream()
//拆分input
while (byteArray.size - offset > 0) {
//每次最大加密245个字节
if (byteArray.size - offset >= ENCRYPT_MAX_SIZE) {
//剩余部分大于245
//加密完整245
temp = cipher.doFinal(byteArray, offset, ENCRYPT_MAX_SIZE)
//重新计算偏移位置
offset += ENCRYPT_MAX_SIZE
} else {
//加密最后一块
temp = cipher.doFinal(byteArray, offset, byteArray.size - offset)
//重新计算偏移位置
offset = byteArray.size
}
//存储到临时的缓冲区
outputStream.write(temp)
}
outputStream.close()
return Base64.encode(outputStream.toByteArray())
}
/**
* 公钥加密
* @param input 原文
* @param publicKey 公钥
*/
fun encryptByPublicKey(input: String, publicKey: PublicKey): String {
//创建cipher对象
val cipher = Cipher.getInstance(transformation)
//初始化cipher
cipher.init(Cipher.ENCRYPT_MODE, publicKey)
//****非对称加密****
val byteArray = input.toByteArray()
var temp: ByteArray?
var offset = 0 //当前偏移的位置
val outputStream = ByteArrayOutputStream()
//拆分input
while (byteArray.size - offset > 0) {
//每次最大加密117个字节
if (byteArray.size - offset >= ENCRYPT_MAX_SIZE) {
//剩余部分大于117
//加密完整117
temp = cipher.doFinal(byteArray, offset, ENCRYPT_MAX_SIZE)
//重新计算偏移位置
offset += ENCRYPT_MAX_SIZE
} else {
//加密最后一块
temp = cipher.doFinal(byteArray, offset, byteArray.size - offset)
//重新计算偏移位置
offset = byteArray.size
}
//存储到临时的缓冲区
outputStream.write(temp)
}
outputStream.close()
return Base64.encode(outputStream.toByteArray())
}
/**
* 私钥解密
* @param input 秘文
* @param privateKey 私钥
*/
fun decryptByPrivateKey(input: String, privateKey: PrivateKey): String {
//创建cipher对象
val cipher = Cipher.getInstance(transformation)
//初始化cipher
cipher.init(Cipher.DECRYPT_MODE, privateKey)
//****非对称加密****
val byteArray = Base64.decode(input)
//分段解密
var temp: ByteArray?
var offset = 0 //当前偏移的位置
val outputStream = ByteArrayOutputStream()
//拆分input
while (byteArray.size - offset > 0) {
//每次最大解密256个字节
if (byteArray.size - offset >= DECRYPT_MAX_SIZE) {
temp = cipher.doFinal(byteArray, offset, DECRYPT_MAX_SIZE)
//重新计算偏移位置
offset += DECRYPT_MAX_SIZE
} else {
//加密最后一块
temp = cipher.doFinal(byteArray, offset, byteArray.size - offset)
//重新计算偏移位置
offset = byteArray.size
}
//存储到临时的缓冲区
outputStream.write(temp)
}
outputStream.close()
return String(outputStream.toByteArray())
}
/**
* 公钥解密
* @param input 秘文
* @param publicKey 公钥
*/
fun decryptByPublicKey(input: String, publicKey: PublicKey): String {
//创建cipher对象
val cipher = Cipher.getInstance(transformation)
//初始化cipher
cipher.init(Cipher.DECRYPT_MODE, publicKey)
//****非对称加密****
val byteArray = Base64.decode(input)
//分段解密
var temp: ByteArray?
var offset = 0 //当前偏移的位置
val outputStream = ByteArrayOutputStream()
//拆分input
while (byteArray.size - offset > 0) {
//每次最大解密256个字节
if (byteArray.size - offset >= DECRYPT_MAX_SIZE) {
temp = cipher.doFinal(byteArray, offset, DECRYPT_MAX_SIZE)
//重新计算偏移位置
offset += DECRYPT_MAX_SIZE
} else {
//加密最后一块
temp = cipher.doFinal(byteArray, offset, byteArray.size - offset)
//重新计算偏移位置
offset = byteArray.size
}
//存储到临时的缓冲区
outputStream.write(temp)
}
outputStream.close()
return String(outputStream.toByteArray())
}
fun getPrivateKey(privateKeyStr: String): PrivateKey {
//字符串转成秘钥对对象
val generator = KeyFactory.getInstance("RSA")
return generator.generatePrivate(PKCS8EncodedKeySpec(Base64.decode(privateKeyStr)))
}
fun getPublicKey(publicKeyStr: String): PublicKey {
//字符串转成秘钥对对象
val kf = KeyFactory.getInstance("RSA")
return kf.generatePublic(X509EncodedKeySpec(Base64.decode(publicKeyStr)))
}
}

View File

@ -61,20 +61,55 @@
android:layout_weight="1" android:layout_weight="1"
android:hint="@string/service_address_hint" /> android:hint="@string/service_address_hint" />
<com.xuexiang.xui.widget.button.ButtonView </LinearLayout>
android:id="@+id/btn_server_history"
style="@style/ButtonView.Gray" <LinearLayout
style="@style/settingBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="5dp" android:text="@string/safety_measures"
android:gravity="center" android:textStyle="bold" />
android:minWidth="70dp"
android:padding="5dp" <RadioGroup
android:text="@string/server_history" /> android:id="@+id/rg_safety_measures"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="3dp"
android:orientation="horizontal">
<RadioButton
android:id="@+id/rb_safety_measures_none"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true"
android:text="@string/safety_measures_none"
android:textSize="11sp" />
<RadioButton
android:id="@+id/rb_safety_measures_sign"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/safety_measures_sign"
android:textSize="11sp" />
<RadioButton
android:id="@+id/rb_safety_measures_rsa"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/safety_measures_encrypt"
android:textSize="11sp" />
</RadioGroup>
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/layout_sign_key"
style="@style/settingBarStyle" style="@style/settingBarStyle"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -82,24 +117,52 @@
tools:ignore="RtlSymmetry"> tools:ignore="RtlSymmetry">
<TextView <TextView
android:id="@+id/tv_sign_key"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/sign_key" android:text="@string/sign_key"
android:textStyle="bold" /> android:textStyle="bold" />
<com.xuexiang.xui.widget.edittext.ClearEditText <EditText
android:id="@+id/et_sign_key" android:id="@+id/et_sign_key"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="5dp" android:layout_marginStart="5dp"
android:layout_weight="1" /> android:layout_weight="1"
android:gravity="top"
android:importantForAutofill="no"
android:inputType="textMultiLine"
android:maxLines="5"
android:minLines="1"
android:scrollbars="vertical"
android:textSize="10sp"
tools:ignore="LabelFor,SmallSp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_margin="10dp"
android:gravity="center_horizontal">
<com.xuexiang.xui.widget.button.ButtonView
android:id="@+id/btn_server_history"
style="@style/ButtonView.Gray"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:minWidth="70dp"
android:padding="5dp"
android:text="@string/server_history" />
<com.xuexiang.xui.widget.button.ButtonView <com.xuexiang.xui.widget.button.ButtonView
android:id="@+id/btn_server_test" android:id="@+id/btn_server_test"
style="@style/ButtonView.Green" style="@style/ButtonView.Green"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="5dp" android:layout_marginStart="15dp"
android:gravity="center" android:gravity="center"
android:minWidth="70dp" android:minWidth="70dp"
android:padding="5dp" android:padding="5dp"

View File

@ -87,19 +87,11 @@
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="horizontal"> android:orientation="horizontal">
<TextView
android:id="@+id/tv_server_tips"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/http_server_stopped"
android:textSize="10sp"
tools:ignore="SmallSp" />
<com.xuexiang.xui.widget.button.shadowbutton.RippleShadowShadowButton <com.xuexiang.xui.widget.button.shadowbutton.RippleShadowShadowButton
android:id="@+id/iv_copy" android:id="@+id/iv_copy"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="5dp" android:layout_marginEnd="5dp"
android:gravity="center" android:gravity="center"
android:padding="3dp" android:padding="3dp"
android:text="@string/copy" android:text="@string/copy"
@ -111,6 +103,14 @@
app:sb_shape_type="rectangle" app:sb_shape_type="rectangle"
tools:ignore="PrivateResource,SmallSp" /> tools:ignore="PrivateResource,SmallSp" />
<TextView
android:id="@+id/tv_server_tips"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/http_server_stopped"
android:textSize="10sp"
tools:ignore="SmallSp" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
@ -134,10 +134,57 @@
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
style="@style/settingBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/safety_measures"
android:textStyle="bold" />
<RadioGroup
android:id="@+id/rg_safety_measures"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="3dp"
android:orientation="horizontal">
<RadioButton
android:id="@+id/rb_safety_measures_none"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true"
android:text="@string/safety_measures_none"
android:textSize="11sp" />
<RadioButton
android:id="@+id/rb_safety_measures_sign"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/safety_measures_sign"
android:textSize="11sp" />
<RadioButton
android:id="@+id/rb_safety_measures_rsa"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/safety_measures_encrypt"
android:textSize="11sp" />
</RadioGroup>
</LinearLayout>
<LinearLayout
android:id="@+id/layout_sign_key"
style="@style/settingBarStyle" style="@style/settingBarStyle"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingEnd="15dp" android:paddingEnd="15dp"
android:visibility="gone"
tools:ignore="RtlSymmetry"> tools:ignore="RtlSymmetry">
<TextView <TextView
@ -172,33 +219,142 @@
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/layout_time_tolerance"
style="@style/settingBarStyle" style="@style/settingBarStyle"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingEnd="15dp" android:paddingEnd="15dp"
android:visibility="gone"
tools:ignore="RtlSymmetry">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/time_tolerance"
android:textStyle="bold"
tools:ignore="RelativeOverlap" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/time_tolerance_tips"
android:textSize="9sp"
tools:ignore="SmallSp" />
</LinearLayout>
<com.xuexiang.xui.widget.picker.XSeekBar
android:id="@+id/xsb_time_tolerance"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_weight="1"
app:xsb_max="600"
app:xsb_min="1" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:text="@string/seconds"
android:textSize="12sp"
android:textStyle="bold" />
</LinearLayout>
<LinearLayout
android:id="@+id/layout_private_key"
style="@style/settingBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingEnd="15dp"
android:visibility="gone"
tools:ignore="RtlSymmetry"> tools:ignore="RtlSymmetry">
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/web_client" android:text="@string/private_key"
android:textStyle="bold" /> android:textStyle="bold" />
<com.xuexiang.xui.widget.edittext.ClearEditText <EditText
android:id="@+id/et_web_path" android:id="@+id/et_private_key"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="5dp" android:layout_marginStart="5dp"
android:layout_weight="1" /> android:layout_weight="1"
android:gravity="top"
android:hint="@string/private_key_tips"
android:importantForAutofill="no"
android:inputType="textMultiLine"
android:maxLines="5"
android:minLines="2"
android:scrollbars="vertical"
android:textSize="10sp"
tools:ignore="SmallSp" />
<com.xuexiang.xui.widget.button.shadowbutton.RippleShadowShadowButton <com.xuexiang.xui.widget.button.shadowbutton.RippleShadowShadowButton
android:id="@+id/btn_path_picker" android:id="@+id/btn_generate_key"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="5dp" android:layout_marginStart="5dp"
android:gravity="center" android:gravity="center"
android:padding="5dp" android:padding="5dp"
android:text="@string/select_directory" android:text="@string/generate_key"
android:textColor="@color/white"
android:textSize="10sp"
app:sb_color_unpressed="@color/colorPrimary"
app:sb_ripple_color="@color/white"
app:sb_ripple_duration="500"
app:sb_shape_type="rectangle"
tools:ignore="SmallSp" />
</LinearLayout>
<LinearLayout
android:id="@+id/layout_public_key"
style="@style/settingBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingEnd="15dp"
android:visibility="gone"
tools:ignore="RtlSymmetry">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/public_key"
android:textStyle="bold" />
<EditText
android:id="@+id/et_public_key"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_weight="1"
android:gravity="top"
android:hint="@string/public_key_tips"
android:importantForAutofill="no"
android:inputType="textMultiLine"
android:maxLines="5"
android:minLines="2"
android:scrollbars="vertical"
android:textSize="10sp"
tools:ignore="SmallSp" />
<com.xuexiang.xui.widget.button.shadowbutton.RippleShadowShadowButton
android:id="@+id/btn_copy_public_key"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:gravity="center"
android:padding="5dp"
android:text="@string/copy_public_key"
android:textColor="@color/white" android:textColor="@color/white"
android:textSize="10sp" android:textSize="10sp"
app:sb_color_unpressed="@color/colorPrimary" app:sb_color_unpressed="@color/colorPrimary"
@ -233,6 +389,47 @@
tools:ignore="SmallSp" /> tools:ignore="SmallSp" />
</LinearLayout> </LinearLayout>
<LinearLayout
style="@style/settingBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingEnd="15dp"
tools:ignore="RtlSymmetry">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/web_client"
android:textStyle="bold" />
<com.xuexiang.xui.widget.edittext.ClearEditText
android:id="@+id/et_web_path"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_weight="1"
android:hint="@string/web_path_tips"
android:textSize="10sp"
tools:ignore="SmallSp" />
<com.xuexiang.xui.widget.button.shadowbutton.RippleShadowShadowButton
android:id="@+id/btn_path_picker"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:gravity="center"
android:padding="5dp"
android:text="@string/select_directory"
android:textColor="@color/white"
android:textSize="10sp"
app:sb_color_unpressed="@color/colorPrimary"
app:sb_ripple_color="@color/white"
app:sb_ripple_duration="500"
app:sb_shape_type="rectangle"
tools:ignore="SmallSp" />
</LinearLayout>
<LinearLayout <LinearLayout
style="@style/settingBarStyle" style="@style/settingBarStyle"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -459,7 +656,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/api_wol" android:text="@string/api_wol"
android:textStyle="bold" android:textStyle="bold"
tools:ignore="RelativeOverlap" /> tools:ignore="RelativeOverlap,TooManyViews" />
<TextView <TextView
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@ -778,6 +778,8 @@
<string name="server_settings">Server Settings</string> <string name="server_settings">Server Settings</string>
<string name="server_settings_tips">It is recommended to enable signature settings, click "Random" to generate and copy to clipboard</string> <string name="server_settings_tips">It is recommended to enable signature settings, click "Random" to generate and copy to clipboard</string>
<string name="sign_key">Sign Key</string> <string name="sign_key">Sign Key</string>
<string name="rsa_key_tips">Key pair generated and copied to clipboard</string>
<string name="rsa_key_tips2">Copied to clipboard</string>
<string name="sign_key_tips">Key generated and copied to clipboard</string> <string name="sign_key_tips">Key generated and copied to clipboard</string>
<string name="copy" tools:ignore="PrivateResource">Copy</string> <string name="copy" tools:ignore="PrivateResource">Copy</string>
<string name="random">Random</string> <string name="random">Random</string>
@ -867,7 +869,7 @@
<string name="sign_verify_failed">Sign verify failed</string> <string name="sign_verify_failed">Sign verify failed</string>
<string name="version_code_required">version_code required</string> <string name="version_code_required">version_code required</string>
<string name="inconsistent_version">The app versions of the client and server are inconsistent</string> <string name="inconsistent_version">The app versions of the client and server are inconsistent</string>
<string name="timestamp_verify_failed" formatted="false">The timestamp verification failed, and the difference with the server time (%s) cannot exceed 1 hour (diffTime=%s)</string> <string name="timestamp_verify_failed" formatted="false">The timestamp verification failed, and the difference with the server time (%s) cannot exceed %s sec. (diffTime=%s)</string>
<string name="main_title">Main title</string> <string name="main_title">Main title</string>
<string name="subtitle">Subtitle</string> <string name="subtitle">Subtitle</string>
@ -921,5 +923,18 @@
<string name="user_id">User ID</string> <string name="user_id">User ID</string>
<string name="auto_clean_logs">Auto delete logs N days ago</string> <string name="auto_clean_logs">Auto delete logs N days ago</string>
<string name="auto_clean_logs_tips">0=disabled, scan when battery change</string> <string name="auto_clean_logs_tips">0=disabled, scan when battery change</string>
<string name="day"></string> <string name="day">Day</string>
<string name="safety_measures">Safety Measures</string>
<string name="safety_measures_none">None</string>
<string name="safety_measures_sign">Sign</string>
<string name="safety_measures_encrypt">Encrypt</string>
<string name="web_path_tips">See Github Wiki, download to Download directory</string>
<string name="time_tolerance">Time Tolerance</string>
<string name="time_tolerance_tips">Minimize time tolerance to avoid request replay attacks</string>
<string name="private_key">Private Key</string>
<string name="private_key_tips">Private key is used on the server: the private key of the server response message is encrypted, and the client public key is decrypted</string>
<string name="generate_key">Generate</string>
<string name="public_key">Public Key</string>
<string name="public_key_tips">Public key is used on the client: client request message public key encryption, server private key decryption</string>
<string name="copy_public_key">Copy</string>
</resources> </resources>

View File

@ -781,6 +781,8 @@
<string name="copy" tools:ignore="PrivateResource">复制</string> <string name="copy" tools:ignore="PrivateResource">复制</string>
<string name="random">随机生成</string> <string name="random">随机生成</string>
<string name="sign_key">签名密钥</string> <string name="sign_key">签名密钥</string>
<string name="rsa_key_tips">已生成公私钥对,并复制公钥到剪贴板</string>
<string name="rsa_key_tips2">已复制公钥到剪贴板</string>
<string name="sign_key_tips">已生成密钥,并复制到剪贴板</string> <string name="sign_key_tips">已生成密钥,并复制到剪贴板</string>
<string name="enable_function">启用功能</string> <string name="enable_function">启用功能</string>
<string name="enable_function_tips">按需选择您要启用远程控制的功能</string> <string name="enable_function_tips">按需选择您要启用远程控制的功能</string>
@ -832,9 +834,9 @@
<string name="battery_plugged">充电器:%s</string> <string name="battery_plugged">充电器:%s</string>
<string name="server_history">历史记录</string> <string name="server_history">历史记录</string>
<string name="server_test">测试接口</string> <string name="server_test">登录服务</string>
<string name="invalid_service_address">无效的服务地址!\n格式http://127.0.0.1:5000 或 https://smsf.demo.com</string> <string name="invalid_service_address">无效的服务地址!\n格式http://127.0.0.1:5000 或 https://smsf.demo.com</string>
<string name="click_test_button_first">请先点击【测试接口】按钮,获取服务端已启用的功能列表</string> <string name="click_test_button_first">请先点击【登录服务】按钮,获取服务端已启用的功能列表</string>
<string name="disabled_on_the_server">服务端禁用此功能</string> <string name="disabled_on_the_server">服务端禁用此功能</string>
<string name="frpc_failed_to_run">Frpc运行失败</string> <string name="frpc_failed_to_run">Frpc运行失败</string>
<string name="successfully_deleted">删除成功</string> <string name="successfully_deleted">删除成功</string>
@ -868,7 +870,7 @@
<string name="sign_verify_failed">签名校验失败</string> <string name="sign_verify_failed">签名校验失败</string>
<string name="version_code_required">version_code节点必传</string> <string name="version_code_required">version_code节点必传</string>
<string name="inconsistent_version">客户端与服务端的App版本不一致</string> <string name="inconsistent_version">客户端与服务端的App版本不一致</string>
<string name="timestamp_verify_failed" formatted="false">timestamp校验失败与服务器时间(%s)误差不能超过1小时(diffTime=%s)</string> <string name="timestamp_verify_failed" formatted="false">timestamp校验失败与服务器时间(%s)误差不能超过%s秒(diffTime=%s)</string>
<string name="main_title">主标题</string> <string name="main_title">主标题</string>
<string name="subtitle">副标题</string> <string name="subtitle">副标题</string>
@ -923,4 +925,17 @@
<string name="auto_clean_logs">自动删除N天前的转发记录</string> <string name="auto_clean_logs">自动删除N天前的转发记录</string>
<string name="auto_clean_logs_tips">0=禁用,触发机制:每次电量变化时扫描</string> <string name="auto_clean_logs_tips">0=禁用,触发机制:每次电量变化时扫描</string>
<string name="day"></string> <string name="day"></string>
<string name="safety_measures">安全措施</string>
<string name="safety_measures_none">不需要</string>
<string name="safety_measures_sign">校验签名</string>
<string name="safety_measures_encrypt">加密传输</string>
<string name="web_path_tips">参见 Github Wiki下载到 Download 目录</string>
<string name="time_tolerance">客户端与服务端时间容差</string>
<string name="time_tolerance_tips">尽量缩短时间容差,避免请求重放攻击</string>
<string name="private_key">RSA私钥</string>
<string name="private_key_tips">RSA私钥用在服务端服务端应答报文私钥加密客户端公钥解密</string>
<string name="generate_key">生成密钥</string>
<string name="public_key">RSA公钥</string>
<string name="public_key_tips">RSA公钥用在客户端客户端请求报文公钥加密服务端私钥解密</string>
<string name="copy_public_key">复制公钥</string>
</resources> </resources>