From 4aa6f799b22252c0592b05314132f9e5da945604 Mon Sep 17 00:00:00 2001 From: Looly Date: Tue, 3 Dec 2024 00:35:23 +0800 Subject: [PATCH] fix redirect bug --- .../org/dromara/hutool/http/HttpUrlUtil.java | 62 +++++++++++++++++ .../dromara/hutool/http/client/Request.java | 11 +++ .../hutool/http/client/RequestContext.java | 9 +++ .../client/engine/jdk/JdkClientEngine.java | 69 ++++--------------- .../client/engine/okhttp/OkHttpEngine.java | 51 +++++++++++--- .../dromara/hutool/http/meta/HttpStatus.java | 3 + .../hutool/http/client/DownloadTest.java | 27 +++++++- 7 files changed, 164 insertions(+), 68 deletions(-) create mode 100644 hutool-http/src/main/java/org/dromara/hutool/http/HttpUrlUtil.java diff --git a/hutool-http/src/main/java/org/dromara/hutool/http/HttpUrlUtil.java b/hutool-http/src/main/java/org/dromara/hutool/http/HttpUrlUtil.java new file mode 100644 index 000000000..53f149e51 --- /dev/null +++ b/hutool-http/src/main/java/org/dromara/hutool/http/HttpUrlUtil.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2024 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.hutool.http; + +import org.dromara.hutool.core.net.url.UrlBuilder; +import org.dromara.hutool.core.text.StrUtil; +import org.dromara.hutool.core.text.split.SplitUtil; + +import java.util.List; + +public class HttpUrlUtil { + /** + * 获取转发的新的URL + * + * @param parentUrl 上级请求的URL + * @param location 获取的Location + * @return 新的URL + */ + public static UrlBuilder getLocationUrl(final UrlBuilder parentUrl, String location) { + final UrlBuilder redirectUrl; + if (!HttpUtil.isHttp(location) && !HttpUtil.isHttps(location)) { + // issue#I5TPSY + // location可能为相对路径 + if (!location.startsWith("/")) { + location = StrUtil.addSuffixIfNot(parentUrl.getPathStr(), "/") + location; + } + + // issue#3265, 相对路径中可能存在参数,单独处理参数 + final String query; + final List split = SplitUtil.split(location, "?", 2, true, true); + if (split.size() == 2) { + // 存在参数 + location = split.get(0); + query = split.get(1); + } else { + query = null; + } + + redirectUrl = UrlBuilder.of(parentUrl.getScheme(), parentUrl.getHost(), parentUrl.getPort(), + location, query, null, parentUrl.getCharset()); + } else { + // location已经是编码过的URL + redirectUrl = UrlBuilder.ofHttpWithoutEncode(location); + } + + return redirectUrl; + } +} diff --git a/hutool-http/src/main/java/org/dromara/hutool/http/client/Request.java b/hutool-http/src/main/java/org/dromara/hutool/http/client/Request.java index 30aa405c4..14a222bd2 100644 --- a/hutool-http/src/main/java/org/dromara/hutool/http/client/Request.java +++ b/hutool-http/src/main/java/org/dromara/hutool/http/client/Request.java @@ -27,6 +27,7 @@ import org.dromara.hutool.core.text.StrUtil; import org.dromara.hutool.core.util.CharsetUtil; import org.dromara.hutool.http.GlobalHeaders; import org.dromara.hutool.http.HttpGlobalConfig; +import org.dromara.hutool.http.HttpUrlUtil; import org.dromara.hutool.http.HttpUtil; import org.dromara.hutool.http.client.body.*; import org.dromara.hutool.http.client.engine.ClientEngine; @@ -206,6 +207,16 @@ public class Request implements HeaderOperation { return this; } + /** + * 设置重定向后的URL,用于处理相对路径 + * + * @param location 重定向后的URL + * @return this + */ + public Request locationTo(final String location){ + return url(HttpUrlUtil.getLocationUrl(handledUrl(), location)); + } + /** * 设置自定义编码,一般用于: *
    diff --git a/hutool-http/src/main/java/org/dromara/hutool/http/client/RequestContext.java b/hutool-http/src/main/java/org/dromara/hutool/http/client/RequestContext.java index ad7b8bc8f..0f23433cb 100644 --- a/hutool-http/src/main/java/org/dromara/hutool/http/client/RequestContext.java +++ b/hutool-http/src/main/java/org/dromara/hutool/http/client/RequestContext.java @@ -27,6 +27,15 @@ public class RequestContext { private Request request; private int redirectCount; + /** + * 构造 + * + * @param request 请求 + */ + public RequestContext(final Request request) { + this.request = request; + } + /** * 获取请求 * diff --git a/hutool-http/src/main/java/org/dromara/hutool/http/client/engine/jdk/JdkClientEngine.java b/hutool-http/src/main/java/org/dromara/hutool/http/client/engine/jdk/JdkClientEngine.java index 95eb24277..49e33c872 100644 --- a/hutool-http/src/main/java/org/dromara/hutool/http/client/engine/jdk/JdkClientEngine.java +++ b/hutool-http/src/main/java/org/dromara/hutool/http/client/engine/jdk/JdkClientEngine.java @@ -18,13 +18,9 @@ package org.dromara.hutool.http.client.engine.jdk; import org.dromara.hutool.core.io.IORuntimeException; import org.dromara.hutool.core.io.IoUtil; -import org.dromara.hutool.core.net.url.UrlBuilder; import org.dromara.hutool.core.net.url.UrlUtil; -import org.dromara.hutool.core.text.StrUtil; -import org.dromara.hutool.core.text.split.SplitUtil; import org.dromara.hutool.core.util.ObjUtil; import org.dromara.hutool.http.HttpException; -import org.dromara.hutool.http.HttpUtil; import org.dromara.hutool.http.client.ClientConfig; import org.dromara.hutool.http.client.Request; import org.dromara.hutool.http.client.RequestContext; @@ -36,10 +32,8 @@ import org.dromara.hutool.http.meta.Method; import org.dromara.hutool.http.proxy.ProxyInfo; import java.io.IOException; -import java.net.HttpURLConnection; import java.net.Proxy; import java.net.URL; -import java.util.List; /** * 基于JDK的UrlConnection的Http客户端引擎实现 @@ -70,7 +64,7 @@ public class JdkClientEngine extends AbstractClientEngine { @Override public JdkHttpResponse send(final Request message) { - return doSend(new RequestContext().setRequest(message)); + return doSend(new RequestContext(message)); } @Override @@ -194,21 +188,19 @@ public class JdkClientEngine extends AbstractClientEngine { throw new HttpException(e); } - if (code != HttpURLConnection.HTTP_OK) { - if (HttpStatus.isRedirected(code)) { - message.url(getLocationUrl(message.handledUrl(), conn.header(HeaderName.LOCATION))); + if (HttpStatus.isRedirected(code)) { + message.locationTo(conn.header(HeaderName.LOCATION)); - // https://www.rfc-editor.org/rfc/rfc7231#section-6.4.7 - // https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Redirections - // 307方法和消息主体都不发生变化。 - if (HttpStatus.HTTP_TEMP_REDIRECT != code) { - // 重定向默认使用GET - message.method(Method.GET); - } + // https://www.rfc-editor.org/rfc/rfc7231#section-6.4.7 + // https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Redirections + // 307方法和消息主体都不发生变化。 + if (HttpStatus.HTTP_TEMP_REDIRECT != code) { + // 重定向默认使用GET + message.method(Method.GET); + } - if (context.canRedirect()) { - return doSend(context); - } + if (context.canRedirect()) { + return doSend(context); } } } @@ -216,41 +208,4 @@ public class JdkClientEngine extends AbstractClientEngine { // 最终页面 return new JdkHttpResponse(conn, this.cookieManager, context.getRequest()); } - - /** - * 获取转发的新的URL - * - * @param parentUrl 上级请求的URL - * @param location 获取的Location - * @return 新的URL - */ - private static UrlBuilder getLocationUrl(final UrlBuilder parentUrl, String location) { - final UrlBuilder redirectUrl; - if (!HttpUtil.isHttp(location) && !HttpUtil.isHttps(location)) { - // issue#I5TPSY - // location可能为相对路径 - if (!location.startsWith("/")) { - location = StrUtil.addSuffixIfNot(parentUrl.getPathStr(), "/") + location; - } - - // issue#3265, 相对路径中可能存在参数,单独处理参数 - final String query; - final List split = SplitUtil.split(location, "?", 2, true, true); - if (split.size() == 2) { - // 存在参数 - location = split.get(0); - query = split.get(1); - } else { - query = null; - } - - redirectUrl = UrlBuilder.of(parentUrl.getScheme(), parentUrl.getHost(), parentUrl.getPort(), - location, query, null, parentUrl.getCharset()); - } else { - // location已经是编码过的URL - redirectUrl = UrlBuilder.ofHttpWithoutEncode(location); - } - - return redirectUrl; - } } diff --git a/hutool-http/src/main/java/org/dromara/hutool/http/client/engine/okhttp/OkHttpEngine.java b/hutool-http/src/main/java/org/dromara/hutool/http/client/engine/okhttp/OkHttpEngine.java index baca6a38f..df2db2843 100644 --- a/hutool-http/src/main/java/org/dromara/hutool/http/client/engine/okhttp/OkHttpEngine.java +++ b/hutool-http/src/main/java/org/dromara/hutool/http/client/engine/okhttp/OkHttpEngine.java @@ -23,10 +23,14 @@ import org.dromara.hutool.core.lang.Assert; import org.dromara.hutool.core.util.ObjUtil; import org.dromara.hutool.http.client.ClientConfig; import org.dromara.hutool.http.client.Request; +import org.dromara.hutool.http.client.RequestContext; import org.dromara.hutool.http.client.Response; import org.dromara.hutool.http.client.body.HttpBody; import org.dromara.hutool.http.client.cookie.InMemoryCookieStore; import org.dromara.hutool.http.client.engine.AbstractClientEngine; +import org.dromara.hutool.http.meta.HeaderName; +import org.dromara.hutool.http.meta.HttpStatus; +import org.dromara.hutool.http.meta.Method; import org.dromara.hutool.http.proxy.ProxyInfo; import org.dromara.hutool.http.ssl.SSLInfo; @@ -65,15 +69,7 @@ public class OkHttpEngine extends AbstractClientEngine { @Override public Response send(final Request message) { initEngine(); - - final okhttp3.Response response; - try { - response = client.newCall(buildRequest(message)).execute(); - } catch (final IOException e) { - throw new IORuntimeException(e); - } - - return new OkHttpResponse(response, message); + return doSend(new RequestContext(message)); } @Override @@ -145,6 +141,43 @@ public class OkHttpEngine extends AbstractClientEngine { this.client = builder.build(); } + /** + * 发送请求 + * + * @param context 请求上下文 + * @return {@link Response} + */ + private Response doSend(final RequestContext context) { + final Request message = context.getRequest(); + final okhttp3.Response response; + try { + response = client.newCall(buildRequest(message)).execute(); + } catch (final IOException e) { + throw new IORuntimeException(e); + } + + // 自定义重定向 + if(message.maxRedirects() > 0){ + final int code = response.code(); + if (HttpStatus.isRedirected(code)) { + message.locationTo(response.header(HeaderName.LOCATION.getValue())); + } + // https://www.rfc-editor.org/rfc/rfc7231#section-6.4.7 + // https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Redirections + // 307方法和消息主体都不发生变化。 + if (HttpStatus.HTTP_TEMP_REDIRECT != code) { + // 重定向默认使用GET + message.method(Method.GET); + } + + if (context.canRedirect()) { + return doSend(context); + } + } + + return new OkHttpResponse(response, message); + } + /** * 构建请求体 * diff --git a/hutool-http/src/main/java/org/dromara/hutool/http/meta/HttpStatus.java b/hutool-http/src/main/java/org/dromara/hutool/http/meta/HttpStatus.java index 9171b5398..0b19b6042 100644 --- a/hutool-http/src/main/java/org/dromara/hutool/http/meta/HttpStatus.java +++ b/hutool-http/src/main/java/org/dromara/hutool/http/meta/HttpStatus.java @@ -360,6 +360,9 @@ public interface HttpStatus { * @since 5.6.3 */ static boolean isRedirected(final int responseCode) { + if(responseCode < 300){ + return false; + } return responseCode == HTTP_MOVED_PERM || responseCode == HTTP_MOVED_TEMP || responseCode == HTTP_SEE_OTHER diff --git a/hutool-http/src/test/java/org/dromara/hutool/http/client/DownloadTest.java b/hutool-http/src/test/java/org/dromara/hutool/http/client/DownloadTest.java index 85268d6e3..af0f5fc3e 100644 --- a/hutool-http/src/test/java/org/dromara/hutool/http/client/DownloadTest.java +++ b/hutool-http/src/test/java/org/dromara/hutool/http/client/DownloadTest.java @@ -26,7 +26,6 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import java.io.File; -import java.io.IOException; import java.util.UUID; /** @@ -203,7 +202,19 @@ public class DownloadTest { @Test @Disabled - public void downloadTeamViewerTest() throws IOException { + public void downloadTeamViewerByOkHttpTest() { + // 此URL有3次重定向, 需要请求4次 + final String url = "https://download.teamviewer.com/download/TeamViewer_Setup_x64.exe"; + HttpGlobalConfig.setMaxRedirects(2); + + final Response send = Request.of(url).send(ClientEngineFactory.createEngine("okhttp")); + Console.log(send.getStatus()); + Console.log(send.headers()); + } + + @Test + @Disabled + public void downloadTeamViewerByJdkTest() { // 此URL有3次重定向, 需要请求4次 final String url = "https://download.teamviewer.com/download/TeamViewer_Setup_x64.exe"; HttpGlobalConfig.setMaxRedirects(2); @@ -212,4 +223,16 @@ public class DownloadTest { Console.log(send.getStatus()); Console.log(send.headers()); } + + @Test + @Disabled + public void downloadTeamViewerByHttpClient5Test() { + // 此URL有3次重定向, 需要请求4次 + final String url = "https://download.teamviewer.com/download/TeamViewer_Setup_x64.exe"; + HttpGlobalConfig.setMaxRedirects(2); + + final Response send = Request.of(url).send(ClientEngineFactory.createEngine("HttpClient5")); + Console.log(send.getStatus()); + Console.log(send.headers()); + } }