diff --git a/app/src/main/java/com/idormy/sms/forwarder/entity/setting/WebhookSetting.kt b/app/src/main/java/com/idormy/sms/forwarder/entity/setting/WebhookSetting.kt index 37a4ef29..cb47476e 100644 --- a/app/src/main/java/com/idormy/sms/forwarder/entity/setting/WebhookSetting.kt +++ b/app/src/main/java/com/idormy/sms/forwarder/entity/setting/WebhookSetting.kt @@ -2,21 +2,36 @@ package com.idormy.sms.forwarder.entity.setting import com.idormy.sms.forwarder.R import java.io.Serializable +import java.net.Proxy data class WebhookSetting( - val method: String? = "POST", + val method: String = "POST", var webServer: String = "", - val secret: String? = "", - val response: String? = "", - val webParams: String? = "", - val headers: Map?, + val secret: String = "", + val response: String = "", + val webParams: String = "", + val headers: Map = mapOf(), + val proxyType: Proxy.Type = Proxy.Type.DIRECT, + val proxyHost: String = "", + val proxyPort: String = "", + val proxyAuthenticator: Boolean = false, + val proxyUsername: String = "", + val proxyPassword: String = "", ) : Serializable { fun getMethodCheckId(): Int { return when (method) { - null, "POST" -> R.id.rb_method_post + "POST" -> R.id.rb_method_post "PUT" -> R.id.rb_method_put "PATCH" -> R.id.rb_method_patch else -> R.id.rb_method_get } } + + fun getProxyTypeCheckId(): Int { + return when (proxyType) { + Proxy.Type.HTTP -> R.id.rb_proxyHttp + Proxy.Type.SOCKS -> R.id.rb_proxySocks + else -> R.id.rb_proxyNone + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/idormy/sms/forwarder/fragment/senders/WebhookFragment.kt b/app/src/main/java/com/idormy/sms/forwarder/fragment/senders/WebhookFragment.kt index bf7da0ab..d9cea769 100644 --- a/app/src/main/java/com/idormy/sms/forwarder/fragment/senders/WebhookFragment.kt +++ b/app/src/main/java/com/idormy/sms/forwarder/fragment/senders/WebhookFragment.kt @@ -4,9 +4,11 @@ import android.text.TextUtils import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.CompoundButton import android.widget.EditText import android.widget.ImageView import android.widget.LinearLayout +import android.widget.RadioGroup import androidx.fragment.app.viewModels import com.google.gson.Gson import com.idormy.sms.forwarder.R @@ -41,11 +43,12 @@ import io.reactivex.SingleObserver import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers +import java.net.Proxy import java.util.Date @Page(name = "Webhook") @Suppress("PrivatePropertyName") -class WebhookFragment : BaseFragment(), View.OnClickListener { +class WebhookFragment : BaseFragment(), View.OnClickListener, CompoundButton.OnCheckedChangeListener { private val TAG: String = WebhookFragment::class.java.simpleName private var titleBar: TitleBar? = null @@ -132,13 +135,17 @@ class WebhookFragment : BaseFragment(), View.OnC binding!!.etResponse.setText(settingVo.response) binding!!.etWebParams.setText(settingVo.webParams) //set header - if (settingVo.headers != null) { - for ((key, value) in settingVo.headers) { - addHeaderItemLinearLayout( - headerItemMap, binding!!.layoutHeaders, key, value - ) - } + for ((key, value) in settingVo.headers) { + addHeaderItemLinearLayout( + headerItemMap, binding!!.layoutHeaders, key, value + ) } + binding!!.rgProxyType.check(settingVo.getProxyTypeCheckId()) + binding!!.etProxyHost.setText(settingVo.proxyHost) + binding!!.etProxyPort.setText(settingVo.proxyPort) + binding!!.sbProxyAuthenticator.isChecked = settingVo.proxyAuthenticator == true + binding!!.etProxyUsername.setText(settingVo.proxyUsername) + binding!!.etProxyPassword.setText(settingVo.proxyPassword) } } }) @@ -151,9 +158,26 @@ class WebhookFragment : BaseFragment(), View.OnC binding!!.btnAddHeader.setOnClickListener { addHeaderItemLinearLayout(headerItemMap, binding!!.layoutHeaders, null, null) } + binding!!.sbProxyAuthenticator.setOnCheckedChangeListener(this) + binding!!.rgProxyType.setOnCheckedChangeListener { _: RadioGroup?, checkedId: Int -> + if (checkedId == R.id.rb_proxyHttp || checkedId == R.id.rb_proxySocks) { + binding!!.layoutProxyHost.visibility = View.VISIBLE + binding!!.layoutProxyPort.visibility = View.VISIBLE + binding!!.layoutProxyAuthenticator.visibility = if (binding!!.sbProxyAuthenticator.isChecked) View.VISIBLE else View.GONE + } else { + binding!!.layoutProxyHost.visibility = View.GONE + binding!!.layoutProxyPort.visibility = View.GONE + binding!!.layoutProxyAuthenticator.visibility = View.GONE + } + } LiveEventBus.get(KEY_SENDER_TEST, String::class.java).observe(this) { mCountDownHelper?.finish() } } + override fun onCheckedChanged(buttonView: CompoundButton?, isChecked: Boolean) { + //注意:因为只有一个监听,暂不需要判断id + binding!!.layoutProxyAuthenticator.visibility = if (isChecked) View.VISIBLE else View.GONE + } + @SingleClick override fun onClick(v: View) { try { @@ -233,7 +257,26 @@ class WebhookFragment : BaseFragment(), View.OnC val webParams = binding!!.etWebParams.text.toString().trim() val headers = getHeadersFromHeaderItemMap(headerItemMap) - return WebhookSetting(method, webServer, secret, response, webParams, headers) + val proxyType: Proxy.Type = when (binding!!.rgProxyType.checkedRadioButtonId) { + R.id.rb_proxyHttp -> Proxy.Type.HTTP + R.id.rb_proxySocks -> Proxy.Type.SOCKS + else -> Proxy.Type.DIRECT + } + val proxyHost = binding!!.etProxyHost.text.toString().trim() + val proxyPort = binding!!.etProxyPort.text.toString().trim() + + if (proxyType != Proxy.Type.DIRECT && (TextUtils.isEmpty(proxyHost) || TextUtils.isEmpty(proxyPort))) { + throw Exception(getString(R.string.invalid_host_or_port)) + } + + val proxyAuthenticator = binding!!.sbProxyAuthenticator.isChecked + val proxyUsername = binding!!.etProxyUsername.text.toString().trim() + val proxyPassword = binding!!.etProxyPassword.text.toString().trim() + if (proxyAuthenticator && TextUtils.isEmpty(proxyUsername) && TextUtils.isEmpty(proxyPassword)) { + throw Exception(getString(R.string.invalid_username_or_password)) + } + + return WebhookSetting(method, webServer, secret, response, webParams, headers, proxyType, proxyHost, proxyPort, proxyAuthenticator, proxyUsername, proxyPassword) } diff --git a/app/src/main/java/com/idormy/sms/forwarder/utils/interceptor/NoContentInterceptor.kt b/app/src/main/java/com/idormy/sms/forwarder/utils/interceptor/NoContentInterceptor.kt new file mode 100644 index 00000000..54577276 --- /dev/null +++ b/app/src/main/java/com/idormy/sms/forwarder/utils/interceptor/NoContentInterceptor.kt @@ -0,0 +1,51 @@ +package com.idormy.sms.forwarder.utils.interceptor + +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.WorkManager +import androidx.work.workDataOf +import com.google.gson.Gson +import com.idormy.sms.forwarder.entity.result.SendResponse +import com.idormy.sms.forwarder.utils.Log +import com.idormy.sms.forwarder.utils.Worker +import com.idormy.sms.forwarder.workers.UpdateLogsWorker +import com.xuexiang.xutil.XUtil +import okhttp3.Interceptor +import okhttp3.Response +import java.util.concurrent.TimeUnit + +class NoContentInterceptor(private val logId: Long) : Interceptor { + + private val TAG: String = NoContentInterceptor::class.java.simpleName + + override fun intercept(chain: Interceptor.Chain): Response { + val originalResponse = chain.proceed(chain.request()) + + if (originalResponse.code() == 204) { + val response = "HTTP 204 No Content" + Log.d(TAG, response) + /* + // 创建一个空的响应体 + val message = "{\"Code\":0, \"Msg\":\"\", \"Data\":{}}" + val emptyJsonBody = ResponseBody.create(MediaType.parse("application/json"), message) + // 使用新的响应体替换原始响应中的响应体 + return originalResponse.newBuilder() + .body(emptyJsonBody) + .header("Content-Length", message.length.toString()) + .build() + */ + //TODO: 暂时特殊处理,更新日志状态为成功 + val sendResponse = SendResponse(logId, 2, response) + val request = OneTimeWorkRequestBuilder() + .setInitialDelay(200, TimeUnit.MILLISECONDS) + .setInputData( + workDataOf( + Worker.UPDATE_LOGS to Gson().toJson(sendResponse) + ) + ).build() + WorkManager.getInstance(XUtil.getContext()).enqueue(request) + } + + return originalResponse + } +} + diff --git a/app/src/main/java/com/idormy/sms/forwarder/utils/sender/WebhookUtils.kt b/app/src/main/java/com/idormy/sms/forwarder/utils/sender/WebhookUtils.kt index bd260b4f..e819c840 100644 --- a/app/src/main/java/com/idormy/sms/forwarder/utils/sender/WebhookUtils.kt +++ b/app/src/main/java/com/idormy/sms/forwarder/utils/sender/WebhookUtils.kt @@ -4,6 +4,7 @@ import android.annotation.SuppressLint import android.text.TextUtils import android.util.Base64 import com.google.gson.Gson +import com.idormy.sms.forwarder.R import com.idormy.sms.forwarder.database.entity.Rule import com.idormy.sms.forwarder.entity.MsgInfo import com.idormy.sms.forwarder.entity.setting.WebhookSetting @@ -11,16 +12,29 @@ import com.idormy.sms.forwarder.utils.AppUtils import com.idormy.sms.forwarder.utils.Log import com.idormy.sms.forwarder.utils.SendUtils import com.idormy.sms.forwarder.utils.SettingUtils +import com.idormy.sms.forwarder.utils.interceptor.BasicAuthInterceptor +import com.idormy.sms.forwarder.utils.interceptor.LoggingInterceptor +import com.idormy.sms.forwarder.utils.interceptor.NoContentInterceptor import com.xuexiang.xhttp2.XHttp import com.xuexiang.xhttp2.callback.SimpleCallBack import com.xuexiang.xhttp2.exception.ApiException +import com.xuexiang.xutil.net.NetworkUtils +import com.xuexiang.xutil.resource.ResUtils +import okhttp3.Credentials +import okhttp3.Response +import okhttp3.Route +import java.net.Authenticator +import java.net.InetSocketAddress +import java.net.PasswordAuthentication +import java.net.Proxy import java.net.URLEncoder import java.nio.charset.StandardCharsets import java.text.SimpleDateFormat -import java.util.* +import java.util.Date import javax.crypto.Mac import javax.crypto.spec.SecretKeySpec + class WebhookUtils { companion object { @@ -57,7 +71,7 @@ class WebhookUtils { val mac = Mac.getInstance("HmacSHA256") mac.init( SecretKeySpec( - setting.secret?.toByteArray(StandardCharsets.UTF_8), + setting.secret.toByteArray(StandardCharsets.UTF_8), "HmacSHA256" ) ) @@ -65,7 +79,7 @@ class WebhookUtils { sign = URLEncoder.encode(String(Base64.encode(signData, Base64.NO_WRAP)), "UTF-8") } - var webParams = setting.webParams?.trim() + var webParams = setting.webParams.trim() //支持HTTP基本认证(Basic Authentication) val regex = "^(https?://)([^:]+):([^@]+)@(.+)" @@ -90,7 +104,7 @@ class WebhookUtils { Log.d(TAG, "method = GET, Url = $requestUrl") XHttp.get(requestUrl).keepJson(true) } else if (setting.method == "GET" && !TextUtils.isEmpty(webParams)) { - webParams = webParams.toString().replace("[from]", URLEncoder.encode(from, "UTF-8")) + webParams = webParams.replace("[from]", URLEncoder.encode(from, "UTF-8")) .replace("[content]", URLEncoder.encode(content, "UTF-8")) .replace("[msg]", URLEncoder.encode(content, "UTF-8")) .replace("[org_content]", URLEncoder.encode(orgContent, "UTF-8")) @@ -114,7 +128,7 @@ class WebhookUtils { } Log.d(TAG, "method = GET, Url = $requestUrl") XHttp.get(requestUrl).keepJson(true) - } else if (!webParams.isNullOrEmpty() && webParams.startsWith("{")) { + } else if (webParams.isNotEmpty() && webParams.startsWith("{")) { val bodyMsg = webParams.replace("[from]", from) .replace("[content]", escapeJson(content)) .replace("[msg]", escapeJson(content)) @@ -136,7 +150,7 @@ class WebhookUtils { else -> XHttp.post(requestUrl).keepJson(true).upJson(bodyMsg) } } else { - if (webParams.isNullOrEmpty()) { + if (webParams.isEmpty()) { webParams = "from=[from]&content=[content]×tamp=[timestamp]" if (!TextUtils.isEmpty(sign)) webParams += "&sign=[sign]" } @@ -171,7 +185,7 @@ class WebhookUtils { } //添加headers - for ((key, value) in setting.headers?.entries!!) { + for ((key, value) in setting.headers.entries) { request.headers(key, value) } @@ -180,24 +194,65 @@ class WebhookUtils { request.addInterceptor(BasicAuthInterceptor(matches[2], matches[3])) } + //设置代理 + if ((setting.proxyType == Proxy.Type.HTTP || setting.proxyType == Proxy.Type.SOCKS) + && !TextUtils.isEmpty(setting.proxyHost) && !TextUtils.isEmpty(setting.proxyPort) + ) { + //代理服务器的IP和端口号 + Log.d(TAG, "proxyHost = ${setting.proxyHost}, proxyPort = ${setting.proxyPort}") + val proxyHost = if (NetworkUtils.isIP(setting.proxyHost)) setting.proxyHost else NetworkUtils.getDomainAddress(setting.proxyHost) + if (!NetworkUtils.isIP(proxyHost)) { + throw Exception(String.format(ResUtils.getString(R.string.invalid_proxy_host), proxyHost)) + } + val proxyPort: Int = setting.proxyPort.toInt() + + Log.d(TAG, "proxyHost = $proxyHost, proxyPort = $proxyPort") + request.okproxy(Proxy(setting.proxyType, InetSocketAddress(proxyHost, proxyPort))) + + //代理的鉴权账号密码 + if (setting.proxyAuthenticator && (!TextUtils.isEmpty(setting.proxyUsername) || !TextUtils.isEmpty(setting.proxyPassword)) + ) { + Log.i(TAG, "proxyUsername = ${setting.proxyUsername}, proxyPassword = ${setting.proxyPassword}") + + if (setting.proxyType == Proxy.Type.HTTP) { + request.okproxyAuthenticator { _: Route?, response: Response -> + //设置代理服务器账号密码 + val credential = Credentials.basic(setting.proxyUsername, setting.proxyPassword) + response.request().newBuilder() + .header("Proxy-Authorization", credential) + .build() + } + } else { + Authenticator.setDefault(object : Authenticator() { + override fun getPasswordAuthentication(): PasswordAuthentication { + return PasswordAuthentication(setting.proxyUsername, setting.proxyPassword.toCharArray()) + } + }) + } + } + } + request.ignoreHttpsCert() //忽略https证书 .retryCount(SettingUtils.requestRetryTimes) //超时重试的次数 .retryDelay(SettingUtils.requestDelayTime * 1000) //超时重试的延迟时间 .retryIncreaseDelay(SettingUtils.requestDelayTime * 1000) //超时重试叠加延时 .timeStamp(true) //url自动追加时间戳,避免缓存 .addInterceptor(LoggingInterceptor(logId)) //增加一个log拦截器, 记录请求日志 - .execute(object : SimpleCallBack() { + .addInterceptor(NoContentInterceptor(logId)) //拦截 HTTP 204 响应 + .execute(object : SimpleCallBack() { override fun onError(e: ApiException) { + e.printStackTrace() Log.e(TAG, e.detailMessage) val status = 0 SendUtils.updateLogs(logId, status, e.displayMessage) SendUtils.senderLogic(status, msgInfo, rule, senderIndex, msgId) } - override fun onSuccess(response: String) { + override fun onSuccess(resp: Any) { + val response = resp.toString() Log.i(TAG, response) - val status = if (!setting.response.isNullOrEmpty() && !response.contains(setting.response)) 0 else 2 + val status = if (setting.response.isNotEmpty() && !response.contains(setting.response)) 0 else 2 SendUtils.updateLogs(logId, status, response) SendUtils.senderLogic(status, msgInfo, rule, senderIndex, msgId) } diff --git a/app/src/main/res/layout/fragment_senders_webhook.xml b/app/src/main/res/layout/fragment_senders_webhook.xml index 392a45ce..307be313 100644 --- a/app/src/main/res/layout/fragment_senders_webhook.xml +++ b/app/src/main/res/layout/fragment_senders_webhook.xml @@ -248,6 +248,153 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +