From 4460d004b4507484176fe7eef54de203489454df Mon Sep 17 00:00:00 2001 From: tangcent Date: Sun, 28 Apr 2024 09:24:10 +0800 Subject: [PATCH] feat: added functionality to choose between apache and okHttp for sending HTTP requests via settings --- .../com/itangcent/http/ApacheHttpClient.kt | 38 +-- idea-plugin/build.gradle.kts | 4 + .../plugin/actions/YapiDashBoardAction.kt | 2 +- .../idea/plugin/actions/YapiExportAction.kt | 2 +- .../plugin/api/export/suv/SuvApiExporter.kt | 2 +- ...hedApiHelper.kt => CachedYapiApiHelper.kt} | 2 +- .../idea/plugin/dialog/EasyApiSettingGUI.form | 72 ++++- .../idea/plugin/dialog/EasyApiSettingGUI.kt | 16 +- .../idea/plugin/settings/HttpClientType.kt | 10 + .../idea/plugin/settings/Settings.kt | 22 +- .../settings/helper/HttpSettingsHelper.kt | 35 ++- .../settings/xml/ApplicationSettings.kt | 11 +- .../com/itangcent/idea/utils/OkHttpClient.kt | 288 ++++++++++++++++++ .../http/ConfigurableHttpClientProvider.kt | 95 +++--- .../build/kotlin/pluginadapterjar-classes.txt | 0 plugin-adapter/build/libs/plugin-adapter.jar | Bin 261 -> 0 bytes plugin-adapter/build/tmp/jar/MANIFEST.MF | 2 - 17 files changed, 508 insertions(+), 93 deletions(-) rename idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/yapi/{YapiCachedApiHelper.kt => CachedYapiApiHelper.kt} (97%) create mode 100644 idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/settings/HttpClientType.kt create mode 100644 idea-plugin/src/main/kotlin/com/itangcent/idea/utils/OkHttpClient.kt delete mode 100644 plugin-adapter/build/kotlin/pluginadapterjar-classes.txt delete mode 100644 plugin-adapter/build/libs/plugin-adapter.jar delete mode 100644 plugin-adapter/build/tmp/jar/MANIFEST.MF diff --git a/common-api/src/main/kotlin/com/itangcent/http/ApacheHttpClient.kt b/common-api/src/main/kotlin/com/itangcent/http/ApacheHttpClient.kt index 0788f7708..52e9ef339 100644 --- a/common-api/src/main/kotlin/com/itangcent/http/ApacheHttpClient.kt +++ b/common-api/src/main/kotlin/com/itangcent/http/ApacheHttpClient.kt @@ -52,11 +52,8 @@ open class ApacheHttpClient : HttpClient { private val httpClient: org.apache.http.client.HttpClient - constructor() { - val basicCookieStore = BasicCookieStore() - this.apacheCookieStore = ApacheCookieStore(basicCookieStore) - this.httpClientContext!!.cookieStore = basicCookieStore - this.httpClient = HttpClients.custom() + constructor() : this( + HttpClients.custom() .setConnectionManager(PoolingHttpClientConnectionManager().also { it.maxTotal = 50 it.defaultMaxPerRoute = 20 @@ -73,10 +70,8 @@ open class ApacheHttpClient : HttpClient { .setSocketTimeout(30 * 1000) .setCookieSpec(CookieSpecs.STANDARD).build() ) - .setSSLHostnameVerifier(NOOP_HOST_NAME_VERIFIER) - .setSSLSocketFactory(SSLSF) .build() - } + ) constructor(httpClient: org.apache.http.client.HttpClient) { val basicCookieStore = BasicCookieStore() @@ -187,13 +182,7 @@ open class ApacheHttpClient : HttpClient { } @ScriptTypeName("request") -class ApacheHttpRequest : AbstractHttpRequest { - - private val apacheHttpClient: ApacheHttpClient - - constructor(apacheHttpClient: ApacheHttpClient) : super() { - this.apacheHttpClient = apacheHttpClient - } +class ApacheHttpRequest(private val apacheHttpClient: ApacheHttpClient) : AbstractHttpRequest() { /** * Executes HTTP request using the [apacheHttpClient]. @@ -214,13 +203,7 @@ fun HttpRequest.contentType(contentType: ContentType): HttpRequest { * The implement of [CookieStore] by [org.apache.http.client.CookieStore]. */ @ScriptTypeName("cookieStore") -class ApacheCookieStore : CookieStore { - - private var cookieStore: org.apache.http.client.CookieStore - - constructor(cookieStore: org.apache.http.client.CookieStore) { - this.cookieStore = cookieStore - } +class ApacheCookieStore(private var cookieStore: org.apache.http.client.CookieStore) : CookieStore { /** * Adds an [Cookie], replacing any existing equivalent cookies. @@ -281,7 +264,7 @@ class ApacheHttpResponse( * * @return the status of the response, or {@code null} if not yet set */ - override fun code(): Int? { + override fun code(): Int { val statusLine = response.statusLine return statusLine.statusCode } @@ -353,17 +336,12 @@ class ApacheHttpResponse( * The implement of [Cookie] by [org.apache.http.cookie.Cookie]. */ @ScriptTypeName("cookie") -class ApacheCookie : Cookie { - private val cookie: org.apache.http.cookie.Cookie +class ApacheCookie(private val cookie: org.apache.http.cookie.Cookie) : Cookie { fun getWrapper(): org.apache.http.cookie.Cookie { return cookie } - constructor(cookie: org.apache.http.cookie.Cookie) { - this.cookie = cookie - } - override fun getName(): String? { return cookie.name } @@ -404,7 +382,7 @@ class ApacheCookie : Cookie { return cookie.isSecure } - override fun getVersion(): Int? { + override fun getVersion(): Int { return cookie.version } diff --git a/idea-plugin/build.gradle.kts b/idea-plugin/build.gradle.kts index b99dfe1f9..21bc1d88b 100644 --- a/idea-plugin/build.gradle.kts +++ b/idea-plugin/build.gradle.kts @@ -78,6 +78,10 @@ dependencies { // https://mvnrepository.com/artifact/org.xerial/sqlite-jdbc implementation("org.xerial:sqlite-jdbc:3.34.0") + // https://mvnrepository.com/artifact/com.squareup.okhttp3/okhttp + implementation("com.squareup.okhttp3:okhttp:4.12.0") + + // https://search.maven.org/artifact/org.mockito.kotlin/mockito-kotlin/3.2.0/jar testImplementation("org.mockito.kotlin:mockito-kotlin:3.2.0") diff --git a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/actions/YapiDashBoardAction.kt b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/actions/YapiDashBoardAction.kt index da7e6a7da..d37b501b9 100644 --- a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/actions/YapiDashBoardAction.kt +++ b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/actions/YapiDashBoardAction.kt @@ -28,7 +28,7 @@ class YapiDashBoardAction : ApiExportAction("YapiDashBoard") { builder.bind(YapiDashBoard::class) { it.singleton() } builder.bind(YapiApiDashBoardExporter::class) { it.singleton() } - builder.bind(YapiApiHelper::class) { it.with(YapiCachedApiHelper::class).singleton() } + builder.bind(YapiApiHelper::class) { it.with(CachedYapiApiHelper::class).singleton() } builder.bind(HttpClientProvider::class) { it.with(ConfigurableHttpClientProvider::class).singleton() } //allow cache api diff --git a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/actions/YapiExportAction.kt b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/actions/YapiExportAction.kt index 266a2e318..19d5dca6a 100644 --- a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/actions/YapiExportAction.kt +++ b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/actions/YapiExportAction.kt @@ -26,7 +26,7 @@ class YapiExportAction : ApiExportAction("Export Yapi") { builder.bind(HttpClientProvider::class) { it.with(ConfigurableHttpClientProvider::class).singleton() } builder.bind(LinkResolver::class) { it.with(YapiLinkResolver::class).singleton() } - builder.bind(YapiApiHelper::class) { it.with(YapiCachedApiHelper::class).singleton() } + builder.bind(YapiApiHelper::class) { it.with(CachedYapiApiHelper::class).singleton() } builder.bind(ClassExporter::class) { it.with(CompositeClassExporter::class).singleton() } diff --git a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/suv/SuvApiExporter.kt b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/suv/SuvApiExporter.kt index 3058f8891..3463543dc 100644 --- a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/suv/SuvApiExporter.kt +++ b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/suv/SuvApiExporter.kt @@ -407,7 +407,7 @@ open class SuvApiExporter { builder.bind(LocalFileRepository::class) { it.with(DefaultLocalFileRepository::class).singleton() } - builder.bind(YapiApiHelper::class) { it.with(YapiCachedApiHelper::class).singleton() } + builder.bind(YapiApiHelper::class) { it.with(CachedYapiApiHelper::class).singleton() } builder.bind(HttpClientProvider::class) { it.with(ConfigurableHttpClientProvider::class).singleton() } builder.bind(LinkResolver::class) { it.with(YapiLinkResolver::class).singleton() } diff --git a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/yapi/YapiCachedApiHelper.kt b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/yapi/CachedYapiApiHelper.kt similarity index 97% rename from idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/yapi/YapiCachedApiHelper.kt rename to idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/yapi/CachedYapiApiHelper.kt index 1a9647c7d..f5e57f95c 100644 --- a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/yapi/YapiCachedApiHelper.kt +++ b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/yapi/CachedYapiApiHelper.kt @@ -12,7 +12,7 @@ import java.util.concurrent.ConcurrentHashMap * cache: * projectToken -> projectId */ -open class YapiCachedApiHelper : DefaultYapiApiHelper() { +open class CachedYapiApiHelper : DefaultYapiApiHelper() { @Inject private val localFileRepository: LocalFileRepository? = null diff --git a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/dialog/EasyApiSettingGUI.form b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/dialog/EasyApiSettingGUI.form index 65ceacfb6..cdab274e4 100644 --- a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/dialog/EasyApiSettingGUI.form +++ b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/dialog/EasyApiSettingGUI.form @@ -3,7 +3,7 @@ - + @@ -1407,7 +1407,7 @@ - + @@ -1418,7 +1418,7 @@ - + @@ -1452,7 +1452,7 @@ - + @@ -1470,9 +1470,9 @@ - + - + @@ -1496,6 +1496,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/dialog/EasyApiSettingGUI.kt b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/dialog/EasyApiSettingGUI.kt index 91344e7ce..962d00c8c 100644 --- a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/dialog/EasyApiSettingGUI.kt +++ b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/dialog/EasyApiSettingGUI.kt @@ -29,8 +29,6 @@ import com.itangcent.idea.utils.isDoubleClick import com.itangcent.intellij.context.ActionContext import com.itangcent.intellij.extend.rx.ThrottleHelper import com.itangcent.intellij.logger.Logger -import com.itangcent.suv.http.ConfigurableHttpClientProvider -import com.itangcent.common.utils.ResourceUtils import java.awt.event.MouseAdapter import java.awt.event.MouseEvent import java.awt.event.MouseListener @@ -165,6 +163,10 @@ class EasyApiSettingGUI : AbstractEasyApiSettingGUI() { private var recommendedCheckBox: JCheckBox? = null + private var unsafeSslCheckBox: JCheckBox? = null + + private var httpClientComboBox: JComboBox? = null + private var httpTimeOutTextField: JTextField? = null private var trustHostsTextArea: JTextArea? = null @@ -253,6 +255,9 @@ class EasyApiSettingGUI : AbstractEasyApiSettingGUI() { markdownFormatTypeComboBox!!.model = DefaultComboBoxModel(MarkdownFormatType.values().mapToTypedArray { it.name }) + httpClientComboBox!!.model = + DefaultComboBoxModel(HttpClientType.values().mapToTypedArray { it.name.lowercase().capitalize() }) + //endregion general----------------------------------------------------- } @@ -300,6 +305,8 @@ class EasyApiSettingGUI : AbstractEasyApiSettingGUI() { this.trustHostsTextArea!!.text = settings.trustHosts.joinToString(separator = "\n") this.maxDeepTextField!!.text = settings.inferMaxDeep.toString() + this.unsafeSslCheckBox!!.isSelected = settings.unsafeSsl + this.httpClientComboBox!!.selectedItem = settings.httpClient this.httpTimeOutTextField!!.text = settings.httpTimeOut.toString() refresh() @@ -606,8 +613,9 @@ class EasyApiSettingGUI : AbstractEasyApiSettingGUI() { settings.yapiExportMode = yapiExportModeComboBox!!.selectedItem as? String ?: YapiExportMode.ALWAYS_UPDATE.name settings.yapiReqBodyJson5 = yapiReqBodyJson5CheckBox!!.isSelected settings.yapiResBodyJson5 = yapiResBodyJson5CheckBox!!.isSelected - settings.httpTimeOut = - httpTimeOutTextField!!.text.toIntOrNull() ?: ConfigurableHttpClientProvider.defaultHttpTimeOut + settings.unsafeSsl = unsafeSslCheckBox!!.isSelected + settings.httpClient = httpClientComboBox!!.selectedItem as? String ?: HttpClientType.APACHE.name + settings.httpTimeOut = httpTimeOutTextField!!.text.toIntOrNull() ?: 10 settings.useRecommendConfig = recommendedCheckBox!!.isSelected settings.logLevel = (logLevelComboBox!!.selected() ?: CommonSettingsHelper.CoarseLogLevel.LOW).getLevel() diff --git a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/settings/HttpClientType.kt b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/settings/HttpClientType.kt new file mode 100644 index 000000000..04f32f32c --- /dev/null +++ b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/settings/HttpClientType.kt @@ -0,0 +1,10 @@ +package com.itangcent.idea.plugin.settings + +/** + * @author joe.wu + * @date 2024/04/28 + */ +enum class HttpClientType { + APACHE, + OKHTTP, +} \ No newline at end of file diff --git a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/settings/Settings.kt b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/settings/Settings.kt index ae00b7f4e..d13ff75ed 100644 --- a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/settings/Settings.kt +++ b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/settings/Settings.kt @@ -82,6 +82,10 @@ class Settings : ProjectSettingsSupport, ApplicationSettingsSupport { override var trustHosts: Array = DEFAULT_TRUST_HOSTS + override var unsafeSsl: Boolean = false + + override var httpClient: String = "Apache" + //endregion /** @@ -156,6 +160,8 @@ class Settings : ProjectSettingsSupport, ApplicationSettingsSupport { if (yapiReqBodyJson5 != other.yapiReqBodyJson5) return false if (yapiResBodyJson5 != other.yapiResBodyJson5) return false if (httpTimeOut != other.httpTimeOut) return false + if (unsafeSsl != other.unsafeSsl) return false + if (httpClient != other.httpClient) return false if (!trustHosts.contentEquals(other.trustHosts)) return false if (useRecommendConfig != other.useRecommendConfig) return false if (recommendConfigs != other.recommendConfigs) return false @@ -201,6 +207,8 @@ class Settings : ProjectSettingsSupport, ApplicationSettingsSupport { result = 31 * result + yapiReqBodyJson5.hashCode() result = 31 * result + yapiResBodyJson5.hashCode() result = 31 * result + httpTimeOut + result = 31 * result + unsafeSsl.hashCode() + result = 31 * result + httpClient.hashCode() result = 31 * result + trustHosts.contentHashCode() result = 31 * result + useRecommendConfig.hashCode() result = 31 * result + recommendConfigs.hashCode() @@ -215,7 +223,19 @@ class Settings : ProjectSettingsSupport, ApplicationSettingsSupport { } override fun toString(): String { - return "Settings(methodDocEnable=$methodDocEnable, genericEnable=$genericEnable, feignEnable=$feignEnable, jaxrsEnable=$jaxrsEnable, actuatorEnable=$actuatorEnable, pullNewestDataBefore=$pullNewestDataBefore, postmanToken=$postmanToken, postmanWorkspace=$postmanWorkspace, postmanExportMode=$postmanExportMode, postmanCollections=$postmanCollections, wrapCollection=$wrapCollection, autoMergeScript=$autoMergeScript, postmanJson5FormatType='$postmanJson5FormatType', queryExpanded=$queryExpanded, formExpanded=$formExpanded, readGetter=$readGetter, readSetter=$readSetter, inferEnable=$inferEnable, inferMaxDeep=$inferMaxDeep, selectedOnly=$selectedOnly, yapiServer=$yapiServer, yapiTokens=$yapiTokens, enableUrlTemplating=$enableUrlTemplating, switchNotice=$switchNotice, loginMode=$loginMode, yapiExportMode=$yapiExportMode, yapiReqBodyJson5=$yapiReqBodyJson5, yapiResBodyJson5=$yapiResBodyJson5, httpTimeOut=$httpTimeOut, trustHosts=${trustHosts.contentToString()}, useRecommendConfig=$useRecommendConfig, recommendConfigs='$recommendConfigs', logLevel=$logLevel, logCharset='$logCharset', outputDemo=$outputDemo, outputCharset='$outputCharset', markdownFormatType='$markdownFormatType', builtInConfig=$builtInConfig), remoteConfig=$remoteConfig)" + return "Settings(methodDocEnable=$methodDocEnable, genericEnable=$genericEnable, feignEnable=$feignEnable," + + " jaxrsEnable=$jaxrsEnable, actuatorEnable=$actuatorEnable, pullNewestDataBefore=$pullNewestDataBefore," + + " postmanToken=$postmanToken, postmanWorkspace=$postmanWorkspace, postmanExportMode=$postmanExportMode," + + " postmanCollections=$postmanCollections, wrapCollection=$wrapCollection, autoMergeScript=$autoMergeScript," + + " postmanJson5FormatType='$postmanJson5FormatType', queryExpanded=$queryExpanded, formExpanded=$formExpanded," + + " readGetter=$readGetter, readSetter=$readSetter, inferEnable=$inferEnable, inferMaxDeep=$inferMaxDeep," + + " selectedOnly=$selectedOnly, yapiServer=$yapiServer, yapiTokens=$yapiTokens, enableUrlTemplating=$enableUrlTemplating," + + " switchNotice=$switchNotice, loginMode=$loginMode, yapiExportMode=$yapiExportMode, yapiReqBodyJson5=$yapiReqBodyJson5," + + " yapiResBodyJson5=$yapiResBodyJson5, httpTimeOut=$httpTimeOut, unsafeSsl=$unsafeSsl, " + + " httpClient='$httpClient', trustHosts=${trustHosts.contentToString()}," + + " useRecommendConfig=$useRecommendConfig, recommendConfigs='$recommendConfigs', logLevel=$logLevel, logCharset='$logCharset'," + + " outputDemo=$outputDemo, outputCharset='$outputCharset', markdownFormatType='$markdownFormatType'," + + " builtInConfig=$builtInConfig), remoteConfig=$remoteConfig)" } companion object { diff --git a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/settings/helper/HttpSettingsHelper.kt b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/settings/helper/HttpSettingsHelper.kt index bfa17711c..a54570e1c 100644 --- a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/settings/helper/HttpSettingsHelper.kt +++ b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/settings/helper/HttpSettingsHelper.kt @@ -3,10 +3,12 @@ package com.itangcent.idea.plugin.settings.helper import com.google.inject.Inject import com.google.inject.Singleton import com.intellij.openapi.ui.Messages +import com.itangcent.idea.plugin.settings.HttpClientType import com.itangcent.idea.plugin.settings.SettingBinder import com.itangcent.idea.plugin.settings.update import com.itangcent.idea.plugin.utils.RegexUtils import com.itangcent.idea.swing.MessagesHelper +import com.itangcent.intellij.logger.Logger import java.net.URL import java.util.concurrent.TimeUnit @@ -19,6 +21,9 @@ class HttpSettingsHelper { @Inject private lateinit var messagesHelper: MessagesHelper + @Inject + private lateinit var logger: Logger + //region trustHosts---------------------------------------------------- fun checkTrustUrl(url: String, dumb: Boolean = true): Boolean { @@ -48,12 +53,15 @@ class HttpSettingsHelper { return false } if (settings.yapiServer == host - || trustHosts.contains(host)) { + || trustHosts.contains(host) + ) { return true } if (!dumb) { - val trustRet = messagesHelper.showYesNoDialog("Do you trust [$host]?", - "Trust Host", Messages.getQuestionIcon()) + val trustRet = messagesHelper.showYesNoDialog( + "Do you trust [$host]?", + "Trust Host", Messages.getQuestionIcon() + ) return if (trustRet == Messages.YES) { addTrustHost(host) @@ -104,12 +112,29 @@ class HttpSettingsHelper { return timeUnit.convert(settingBinder.read().httpTimeOut.toLong(), TimeUnit.SECONDS).toInt() } + fun unsafeSsl(): Boolean { + return settingBinder.read().unsafeSsl + } + + fun httpClientType(): HttpClientType { + return settingBinder.read().httpClient.uppercase().let { + try { + HttpClientType.valueOf(it) + } catch (e: Exception) { + logger.error("invalid httpClient type:$it") + HttpClientType.APACHE + } + } + } + companion object { val HOST_RESOLVERS: Array<(String) -> String?> = arrayOf({ if (it.startsWith("https://raw.githubusercontent.com")) { val url = if (it.endsWith("/")) it else "$it/" - return@arrayOf RegexUtils.extract("https://raw.githubusercontent.com/(.*?)/.*", - url, "https://raw.githubusercontent.com/$1") + return@arrayOf RegexUtils.extract( + "https://raw.githubusercontent.com/(.*?)/.*", + url, "https://raw.githubusercontent.com/$1" + ) } return@arrayOf null }) diff --git a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/settings/xml/ApplicationSettings.kt b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/settings/xml/ApplicationSettings.kt index fc572d4c3..e2b4b9f59 100644 --- a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/settings/xml/ApplicationSettings.kt +++ b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/settings/xml/ApplicationSettings.kt @@ -38,6 +38,9 @@ interface ApplicationSettingsSupport { //unit:s var httpTimeOut: Int var trustHosts: Array + var unsafeSsl: Boolean + var httpClient: String + //enable to use recommend config var useRecommendConfig: Boolean @@ -77,7 +80,6 @@ interface ApplicationSettingsSupport { newSetting.yapiExportMode = this.yapiExportMode newSetting.yapiReqBodyJson5 = this.yapiReqBodyJson5 newSetting.yapiResBodyJson5 = this.yapiResBodyJson5 - newSetting.httpTimeOut = this.httpTimeOut newSetting.useRecommendConfig = this.useRecommendConfig newSetting.recommendConfigs = this.recommendConfigs newSetting.logLevel = this.logLevel @@ -86,6 +88,9 @@ interface ApplicationSettingsSupport { newSetting.outputCharset = this.outputCharset newSetting.markdownFormatType = this.markdownFormatType newSetting.builtInConfig = this.builtInConfig + newSetting.httpTimeOut = this.httpTimeOut + newSetting.unsafeSsl = this.unsafeSsl + newSetting.httpClient = this.httpClient newSetting.trustHosts = this.trustHosts newSetting.remoteConfig = this.remoteConfig } @@ -158,6 +163,10 @@ class ApplicationSettings : ApplicationSettingsSupport { override var trustHosts: Array = Settings.DEFAULT_TRUST_HOSTS + override var unsafeSsl: Boolean = false + + override var httpClient: String = "Apache" + //endregion //enable to use recommend config diff --git a/idea-plugin/src/main/kotlin/com/itangcent/idea/utils/OkHttpClient.kt b/idea-plugin/src/main/kotlin/com/itangcent/idea/utils/OkHttpClient.kt new file mode 100644 index 000000000..7bf51600a --- /dev/null +++ b/idea-plugin/src/main/kotlin/com/itangcent/idea/utils/OkHttpClient.kt @@ -0,0 +1,288 @@ +package com.itangcent.idea.utils + +import com.itangcent.annotation.script.ScriptTypeName +import com.itangcent.common.utils.GsonUtils +import com.itangcent.http.* +import com.itangcent.http.Cookie +import okhttp3.* +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.RequestBody.Companion.asRequestBody +import okhttp3.RequestBody.Companion.toRequestBody +import java.io.File +import java.util.concurrent.TimeUnit + +/** + * @author joe.wu + * @date 2024/04/27 + */ +@ScriptTypeName("httpClient") +class OkHttpClient : HttpClient { + + private val cookieStore: OkHttpCookieStore = OkHttpCookieStore() + + private val client: okhttp3.OkHttpClient + + constructor(clientBuilder: okhttp3.OkHttpClient.Builder) { + this.client = clientBuilder.cookieJar(cookieStore).build() + } + + constructor() : this( + okhttp3.OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + ) + + override fun cookieStore(): CookieStore { + return cookieStore + } + + override fun request(): HttpRequest { + return OkHttpRequest(this) + } + + fun call(request: OkHttpRequest): HttpResponse { + val builder = Request.Builder() + + // Handle URL and query parameters + val httpUrlBuilder = + request.url()?.toHttpUrlOrNull()?.newBuilder() ?: throw IllegalArgumentException("Invalid URL") + request.querys()?.forEach { query -> + httpUrlBuilder.addQueryParameter(query.name() ?: "", query.value()) + } + builder.url(httpUrlBuilder.build()) + + // Handle headers + request.headers()?.forEach { header -> + builder.addHeader(header.name() ?: "", header.value() ?: "") + } + + // Handle request body + val requestBody = buildRequestBody(request) + + // Set request method and body + builder.method(request.method(), requestBody) + + val call = client.newCall(builder.build()) + val response = call.execute() + + return OkHttpResponse(request, response) + } + + private fun buildRequestBody(request: OkHttpRequest): RequestBody? { + if (request.method().equals("GET", ignoreCase = true)) return null + + if (request.contentType()?.startsWith("application/x-www-form-urlencoded") == true) { + val formBodyBuilder = FormBody.Builder() + request.params()?.forEach { param -> + formBodyBuilder.add(param.name() ?: "", param.value() ?: "") + } + formBodyBuilder.build() + } + + if (request.contentType()?.startsWith("multipart/form-data") == true) { + val builder = MultipartBody.Builder().setType(MultipartBody.FORM) + request.params()?.forEach { param -> + if (param.type() == "file") { + val file = File(param.value()!!) + if (!file.exists()) { + throw IllegalArgumentException("file ${file.absolutePath} not exists") + } + builder.addFormDataPart( + param.name() ?: "", + file.name, + file.asRequestBody(contentType = "application/octet-stream".toMediaType()) + ) + } else { + builder.addFormDataPart(param.name() ?: "", param.value() ?: "") + } + } + return builder.build() + } + + val body = request.body() + return (if (body is String) body else GsonUtils.toJson(body)) + .toRequestBody("application/json; charset=utf-8".toMediaType()) + } +} + +class OkHttpCookieStore : CookieJar, CookieStore { + private val cookieStore = mutableMapOf>() + + override fun saveFromResponse(url: HttpUrl, cookies: List) { + cookieStore[url.host] = cookies.map { OkHttpCookie(it) } + } + + override fun loadForRequest(url: HttpUrl): List { + return cookieStore[url.host]?.map { it.asOkHttpCookie() } ?: emptyList() + } + + override fun addCookie(cookie: Cookie?) { + val domain = cookie?.getDomain() ?: return + val existingCookies = cookieStore.getOrDefault(domain, emptyList()) + cookieStore[domain] = existingCookies.toMutableList() + cookie + + } + + override fun addCookies(cookies: Array?) { + cookies?.forEach { addCookie(it) } + } + + override fun cookies(): List { + return cookieStore.values.flatten() + } + + override fun clear() { + cookieStore.clear() + } + + override fun newCookie(): MutableCookie { + return BasicCookie() + } +} + +@ScriptTypeName("cookie") +class OkHttpCookie(private val cookie: okhttp3.Cookie) : Cookie { + + fun getWrapper(): okhttp3.Cookie { + return cookie + } + + override fun getName(): String { + return cookie.name + } + + override fun getValue(): String { + return cookie.value + } + + override fun getDomain(): String { + return cookie.domain + } + + override fun getPath(): String { + return cookie.path + } + + override fun getExpiryDate(): Long { + return cookie.expiresAt + } + + override fun isPersistent(): Boolean { + return cookie.persistent + } + + override fun isSecure(): Boolean { + return cookie.secure + } + + override fun getComment(): String? { + // OkHttp's Cookie class does not support comments; return null or an empty string if needed. + return null + } + + override fun getCommentURL(): String? { + // OkHttp's Cookie class does not support comment URLs; return null. + return null + } + + override fun getPorts(): IntArray? { + // OkHttp's Cookie class does not support ports; return null. + return null + } + + override fun getVersion(): Int { + // OkHttp's Cookie class does not explicitly handle version; typically version 1 (Netscape spec) is assumed. + return 1 + } + + override fun toString(): String { + return cookie.toString() + } + + companion object { + fun fromSetCookieHeader(url: HttpUrl, setCookieHeader: String): OkHttpCookie { + val cookie = okhttp3.Cookie.parse(url, setCookieHeader) + ?: throw IllegalArgumentException("Invalid Set-Cookie header") + return OkHttpCookie(cookie) + } + } +} + +fun Cookie.asOkHttpCookie(): okhttp3.Cookie { + if (this is OkHttpCookie) { + return this.getWrapper() + } + + // Build a new OkHttp Cookie from generic Cookie interface + return okhttp3.Cookie.Builder().apply { + name(this@asOkHttpCookie.getName() ?: throw IllegalArgumentException("Cookie name cannot be null")) + value(this@asOkHttpCookie.getValue() ?: throw IllegalArgumentException("Cookie value cannot be null")) + domain(this@asOkHttpCookie.getDomain() ?: throw IllegalArgumentException("Cookie domain cannot be null")) + path(this@asOkHttpCookie.getPath() ?: "/") // Default to root if path is not specified + + if (this@asOkHttpCookie.getExpiryDate() != null) { + expiresAt(this@asOkHttpCookie.getExpiryDate()!!) + } else { + // If no expiry is set, use a far future date to mimic a non-expiring cookie + expiresAt(System.currentTimeMillis() + TimeUnit.DAYS.toMillis(365)) // Plus one year + } + + if (this@asOkHttpCookie.isSecure()) { + secure() + } + }.build() +} + +@ScriptTypeName("request") +class OkHttpRequest(private val client: OkHttpClient) : AbstractHttpRequest() { + override fun call(): HttpResponse { + return client.call(this) + } +} + +@ScriptTypeName("response") +class OkHttpResponse( + private val request: HttpRequest, + private val response: Response +) : AbstractHttpResponse(), AutoCloseable { + + /** + * Obtains the status code of the HTTP response. + * + * @return the HTTP status code + */ + override fun code(): Int { + return response.code + } + + /** + * Obtains all headers of the HTTP response. + * + * @return a list of headers (name-value pairs) + */ + override fun headers(): List { + return response.headers.names().map { name -> BasicHttpHeader(name, response.header(name)) } + } + + /** + * Obtains the byte array of the response body if available. + * + * @return the byte array of the response body, or null if no body is available + */ + override fun bytes(): ByteArray? { + return response.body?.bytes() + } + + override fun request(): HttpRequest { + return request + } + + /** + * Closes the response to free resources. + */ + override fun close() { + response.close() + } +} \ No newline at end of file diff --git a/idea-plugin/src/main/kotlin/com/itangcent/suv/http/ConfigurableHttpClientProvider.kt b/idea-plugin/src/main/kotlin/com/itangcent/suv/http/ConfigurableHttpClientProvider.kt index 94eb2e1f5..0c1a9375e 100644 --- a/idea-plugin/src/main/kotlin/com/itangcent/suv/http/ConfigurableHttpClientProvider.kt +++ b/idea-plugin/src/main/kotlin/com/itangcent/suv/http/ConfigurableHttpClientProvider.kt @@ -7,7 +7,9 @@ import com.itangcent.annotation.script.ScriptTypeName import com.itangcent.http.* import com.itangcent.idea.plugin.api.export.core.ClassExportRuleKeys import com.itangcent.idea.plugin.rule.SuvRuleContext +import com.itangcent.idea.plugin.settings.HttpClientType import com.itangcent.idea.plugin.settings.helper.HttpSettingsHelper +import com.itangcent.idea.utils.OkHttpClient import com.itangcent.intellij.config.ConfigReader import com.itangcent.intellij.config.rule.RuleComputer import com.itangcent.intellij.logger.Logger @@ -16,6 +18,7 @@ import org.apache.http.client.config.RequestConfig import org.apache.http.config.SocketConfig import org.apache.http.impl.client.HttpClients import org.apache.http.impl.conn.PoolingHttpClientConnectionManager +import org.apache.http.ssl.SSLContexts import java.io.ByteArrayInputStream import java.io.InputStream import java.nio.charset.Charset @@ -28,8 +31,8 @@ import java.util.concurrent.TimeUnit @Singleton class ConfigurableHttpClientProvider : AbstractHttpClientProvider() { - @Inject(optional = true) - protected val httpSettingsHelper: HttpSettingsHelper? = null + @Inject + protected lateinit var httpSettingsHelper: HttpSettingsHelper @Inject(optional = true) protected val configReader: ConfigReader? = null @@ -46,50 +49,77 @@ class ConfigurableHttpClientProvider : AbstractHttpClientProvider() { * @return An instance of HttpClient. */ override fun buildHttpClient(): HttpClient { - val httpClientBuilder = HttpClients.custom() + return when (httpSettingsHelper.httpClientType()) { + HttpClientType.APACHE -> { + buildApacheHttpClient() + } + + HttpClientType.OKHTTP -> { + buildOkHttpClient() + } + } + } - val config = readHttpConfig() + private fun buildApacheHttpClient(): ApacheHttpClient { + var httpClientBuilder = HttpClients.custom() - httpClientBuilder + val timeOutInMills = httpSettingsHelper.httpTimeOut(TimeUnit.MILLISECONDS) + httpClientBuilder = httpClientBuilder .setConnectionManager(PoolingHttpClientConnectionManager().also { it.maxTotal = 50 it.defaultMaxPerRoute = 20 }) .setDefaultSocketConfig( SocketConfig.custom() - .setSoTimeout(config.timeOut) + .setSoTimeout(timeOutInMills) .build() ) .setDefaultRequestConfig( RequestConfig.custom() - .setConnectTimeout(config.timeOut) - .setConnectionRequestTimeout(config.timeOut) - .setSocketTimeout(config.timeOut) + .setConnectTimeout(timeOutInMills) + .setConnectionRequestTimeout(timeOutInMills) + .setSocketTimeout(timeOutInMills) .setCookieSpec(CookieSpecs.STANDARD).build() ) - .setSSLHostnameVerifier(NOOP_HOST_NAME_VERIFIER) - .setSSLSocketFactory(SSLSF) - return HttpClientWrapper(ApacheHttpClient(httpClientBuilder.build())) + if (httpSettingsHelper.unsafeSsl()) { + httpClientBuilder = httpClientBuilder.setSSLHostnameVerifier(NOOP_HOST_NAME_VERIFIER) + .setSSLSocketFactory(SSLSF) + } + + return ApacheHttpClient(httpClientBuilder.build()) } - private fun readHttpConfig(): HttpConfig { - val httpConfig = HttpConfig() + private fun buildOkHttpClient(): OkHttpClient { + val timeOutInMills = httpSettingsHelper.httpTimeOut(TimeUnit.MILLISECONDS).toLong() + val builder = okhttp3.OkHttpClient.Builder() + .connectTimeout(timeOutInMills, TimeUnit.MILLISECONDS) + .readTimeout(timeOutInMills, TimeUnit.MILLISECONDS) + .writeTimeout(timeOutInMills, TimeUnit.MILLISECONDS) + + if (httpSettingsHelper.unsafeSsl()) { + builder.hostnameVerifier { _, _ -> true } + val trustAllCert = object : javax.net.ssl.X509TrustManager { + override fun checkClientTrusted( + chain: Array?, + authType: String? + ) { + } - httpSettingsHelper?.let { - httpConfig.timeOut = it.httpTimeOut(TimeUnit.MILLISECONDS) - } + override fun checkServerTrusted( + chain: Array?, + authType: String? + ) { + } - if (configReader != null) { - try { - configReader.first("http.timeOut")?.toLong() - ?.let { httpConfig.timeOut = TimeUnit.SECONDS.toMillis(it).toInt() } - } catch (e: NumberFormatException) { - logger.warn("http.timeOut must be a number") + override fun getAcceptedIssuers(): Array { + return arrayOf() + } } + builder.sslSocketFactory(SSLContexts.createSystemDefault().socketFactory, trustAllCert) } - return httpConfig + return OkHttpClient(builder) } /** @@ -235,9 +265,7 @@ class ConfigurableHttpClientProvider : AbstractHttpClientProvider() { */ override fun call(): HttpResponse { val url = url() ?: throw IllegalArgumentException("url not be set") - if (httpSettingsHelper != null - && !httpSettingsHelper.checkTrustUrl(url, false) - ) { + if (!httpSettingsHelper.checkTrustUrl(url, false)) { logger.warn("[access forbidden] call:$url") return EmptyHttpResponse(this) } @@ -338,17 +366,4 @@ class ConfigurableHttpClientProvider : AbstractHttpClientProvider() { return this.discarded } } - - /** - * A data class that holds configuration settings for the HTTP client. - */ - class HttpConfig { - - //default 10s - var timeOut: Int = defaultHttpTimeOut - } - - companion object { - const val defaultHttpTimeOut: Int = 10 - } } \ No newline at end of file diff --git a/plugin-adapter/build/kotlin/pluginadapterjar-classes.txt b/plugin-adapter/build/kotlin/pluginadapterjar-classes.txt deleted file mode 100644 index e69de29bb..000000000 diff --git a/plugin-adapter/build/libs/plugin-adapter.jar b/plugin-adapter/build/libs/plugin-adapter.jar deleted file mode 100644 index 6ffdb4af399b2bea2cc6773f1b3640f8c6817c3a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 261 zcmWIWW@Zs#VBp|jST{Q*oB;@!Km-tQ0~t1dKY=Ub+yi&Ilno`;EM5sr;na80S#kh5@E(|FVI9F t5MX%g2%_PxK-Y%u3XozDSkkBoB;neS+!Wx=$_7%w1ca49x(vi&006F~F^d2I diff --git a/plugin-adapter/build/tmp/jar/MANIFEST.MF b/plugin-adapter/build/tmp/jar/MANIFEST.MF deleted file mode 100644 index 59499bce4..000000000 --- a/plugin-adapter/build/tmp/jar/MANIFEST.MF +++ /dev/null @@ -1,2 +0,0 @@ -Manifest-Version: 1.0 -