From a6cd5641810a53acde03e78545954f473b9c5acd Mon Sep 17 00:00:00 2001 From: guoqiang Date: Tue, 26 May 2026 16:52:39 +0800 Subject: [PATCH] [improvement](fe) Enhance FE meta service request validation ### What problem does this PR solve? Issue Number: N/A Related PR: N/A Problem Summary: FE meta service endpoints are used by FE nodes for metadata synchronization and coordination. This change strengthens internal FE caller validation by carrying the cluster token on FE-to-FE meta requests and validating it on the receiver side, while preserving a temporary legacy switch for rolling upgrades. ### Release note FE meta service internal requests now include cluster token validation by default. During rolling upgrades from older versions, set enable_meta_service_legacy_node_ident_auth=true temporarily on upgraded FEs if old FEs still need to call these endpoints without token headers. Disable it after all FEs are upgraded. The /dump endpoint now always checks HTTP user credentials, and /put only accepts the configured FE HTTP port. ### Check List (For Author) - Test: Unit Test, Manual test - mvn -pl fe-core -am -DskipUT=false -Dcheckstyle.skip=true -DfailIfNoTests=false -Dmaven.build.cache.enabled=false -Dtest=org.apache.doris.httpv2.meta.MetaServiceTest test - mvn -pl fe-core -am -DskipUT=false -DfailIfNoTests=false -Dmaven.build.cache.enabled=false -Dtest=org.apache.doris.httpv2.meta.MetaServiceTest,org.apache.doris.common.util.HttpURLUtilTest test - ./run-fe-ut.sh --run org.apache.doris.httpv2.meta.MetaServiceTest,org.apache.doris.common.util.HttpURLUtilTest - Manual: started FE on HTTP 26030 and query port 27030; verified no-token FE meta request returns business code 401, and token-carrying /image?version=155292 returns HTTP 200. - Behavior changed: Yes. FE meta service endpoints require the cluster token by default; legacy header-only node identity fallback is available only when enable_meta_service_legacy_node_ident_auth=true. /dump now always checks HTTP user credentials. /put rejects ports other than Config.http_port. - Does this need documentation: Yes. Document rolling-upgrade use of enable_meta_service_legacy_node_ident_auth and cluster-token validation for FE meta service requests. --- .../java/org/apache/doris/common/Config.java | 6 + .../apache/doris/common/util/HttpURLUtil.java | 24 +- .../apache/doris/httpv2/meta/MetaService.java | 49 +++- .../org/apache/doris/master/MetaHelper.java | 2 +- .../doris/common/util/HttpURLUtilTest.java | 87 +++++++ .../doris/httpv2/meta/MetaServiceTest.java | 225 ++++++++++++++++++ 6 files changed, 379 insertions(+), 14 deletions(-) create mode 100644 fe/fe-core/src/test/java/org/apache/doris/common/util/HttpURLUtilTest.java create mode 100644 fe/fe-core/src/test/java/org/apache/doris/httpv2/meta/MetaServiceTest.java diff --git a/fe/fe-common/src/main/java/org/apache/doris/common/Config.java b/fe/fe-common/src/main/java/org/apache/doris/common/Config.java index 5c242bd25fade8..ed99088e15949a 100644 --- a/fe/fe-common/src/main/java/org/apache/doris/common/Config.java +++ b/fe/fe-common/src/main/java/org/apache/doris/common/Config.java @@ -782,6 +782,12 @@ public class Config extends ConfigBase { // check token when download image file. @ConfField public static boolean enable_token_check = true; + @ConfField(mutable = true, description = { + "Whether FE meta service accepts legacy FE node identity headers without a cluster token. " + + "This is only for rolling upgrade compatibility and should be disabled after all FEs " + + "are upgraded."}) + public static boolean enable_meta_service_legacy_node_ident_auth = false; + /** * Set to true if you deploy Palo using thirdparty deploy manager * Valid options are: diff --git a/fe/fe-core/src/main/java/org/apache/doris/common/util/HttpURLUtil.java b/fe/fe-core/src/main/java/org/apache/doris/common/util/HttpURLUtil.java index 70b3e68450aa45..40de18db35848d 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/common/util/HttpURLUtil.java +++ b/fe/fe-core/src/main/java/org/apache/doris/common/util/HttpURLUtil.java @@ -19,8 +19,11 @@ import org.apache.doris.catalog.Env; import org.apache.doris.cloud.security.SecurityChecker; +import org.apache.doris.common.Config; +import org.apache.doris.httpv2.meta.MetaBaseAction; import org.apache.doris.system.SystemInfoService.HostInfo; +import com.google.common.base.Strings; import com.google.common.collect.Maps; import java.io.IOException; @@ -37,9 +40,11 @@ public static HttpURLConnection getConnectionWithNodeIdent(String request) throw HttpURLConnection conn = (HttpURLConnection) url.openConnection(); // Must use Env.getServingEnv() instead of getCurrentEnv(), // because here we need to obtain selfNode through the official service catalog. - HostInfo selfNode = Env.getServingEnv().getSelfNode(); + Env env = Env.getServingEnv(); + HostInfo selfNode = env.getSelfNode(); conn.setRequestProperty(Env.CLIENT_NODE_HOST_KEY, selfNode.getHost()); conn.setRequestProperty(Env.CLIENT_NODE_PORT_KEY, selfNode.getPort() + ""); + setClusterToken(conn, getClusterToken(env)); return conn; } catch (Exception e) { throw new IOException(e); @@ -52,10 +57,25 @@ public static Map getNodeIdentHeaders() throws IOException { Map headers = Maps.newHashMap(); // Must use Env.getServingEnv() instead of getCurrentEnv(), // because here we need to obtain selfNode through the official service catalog. - HostInfo selfNode = Env.getServingEnv().getSelfNode(); + Env env = Env.getServingEnv(); + HostInfo selfNode = env.getSelfNode(); headers.put(Env.CLIENT_NODE_HOST_KEY, selfNode.getHost()); headers.put(Env.CLIENT_NODE_PORT_KEY, selfNode.getPort() + ""); + String token = getClusterToken(env); + if (!Strings.isNullOrEmpty(token)) { + headers.put(MetaBaseAction.TOKEN, token); + } return headers; } + private static String getClusterToken(Env env) { + String token = env.getToken(); + return Strings.isNullOrEmpty(token) ? Config.auth_token : token; + } + + private static void setClusterToken(HttpURLConnection conn, String token) { + if (!Strings.isNullOrEmpty(token)) { + conn.setRequestProperty(MetaBaseAction.TOKEN, token); + } + } } diff --git a/fe/fe-core/src/main/java/org/apache/doris/httpv2/meta/MetaService.java b/fe/fe-core/src/main/java/org/apache/doris/httpv2/meta/MetaService.java index 6a8bd71facaa79..8fa776c0f50ac9 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/httpv2/meta/MetaService.java +++ b/fe/fe-core/src/main/java/org/apache/doris/httpv2/meta/MetaService.java @@ -23,6 +23,7 @@ import org.apache.doris.common.util.NetUtils; import org.apache.doris.ha.FrontendNodeType; import org.apache.doris.httpv2.entity.ResponseEntityBuilder; +import org.apache.doris.httpv2.exception.UnauthorizedException; import org.apache.doris.httpv2.rest.RestBaseController; import org.apache.doris.master.MetaHelper; import org.apache.doris.persist.MetaCleaner; @@ -55,31 +56,56 @@ public class MetaService extends RestBaseController { private File imageDir = MetaHelper.getMasterImageDir(); - private boolean isFromValidFe(String clientHost, String clientPortStr) { + private Frontend getValidFe(String clientHost, String clientPortStr) { Integer clientPort; try { clientPort = Integer.valueOf(clientPortStr); } catch (Exception e) { LOG.warn("get clientPort error. clientPortStr: {}", clientPortStr, e.getMessage()); - return false; + return null; } Frontend fe = Env.getCurrentEnv().checkFeExist(clientHost, clientPort); if (fe == null) { LOG.warn("request is not from valid FE. client: {}, {}", clientHost, clientPortStr); - return false; } - return true; + return fe; } private void checkFromValidFe(HttpServletRequest request) - throws InvalidClientException { + throws UnauthorizedException { String clientHost = request.getHeader(Env.CLIENT_NODE_HOST_KEY); String clientPort = request.getHeader(Env.CLIENT_NODE_PORT_KEY); - if (!isFromValidFe(clientHost, clientPort)) { - throw new InvalidClientException("invalid client host: " + clientHost + ":" + clientPort - + ", request from " + request.getRemoteHost()); + Frontend fe = getValidFe(clientHost, clientPort); + if (fe == null) { + throw unauthorized(clientHost, clientPort, request); + } + + String requestToken = request.getHeader(MetaBaseAction.TOKEN); + if (!Strings.isNullOrEmpty(requestToken)) { + if (requestToken.equals(getClusterToken())) { + return; + } + LOG.warn("reject meta request with invalid token. client: {}, {}, request from: {}", + clientHost, clientPort, request.getRemoteAddr()); + throw unauthorized(clientHost, clientPort, request); + } + + if (Config.enable_meta_service_legacy_node_ident_auth) { + return; } + + throw unauthorized(clientHost, clientPort, request); + } + + private String getClusterToken() { + String token = Env.getCurrentEnv().getToken(); + return Strings.isNullOrEmpty(token) ? Config.auth_token : token; + } + + private UnauthorizedException unauthorized(String clientHost, String clientPort, HttpServletRequest request) { + return new UnauthorizedException("invalid client host: " + clientHost + ":" + clientPort + + ", request from " + request.getRemoteAddr()); } @RequestMapping(path = "/image", method = RequestMethod.GET) @@ -149,6 +175,9 @@ public Object put(HttpServletRequest request, HttpServletResponse response) thro if (port < 0 || port > 65535) { return ResponseEntityBuilder.badRequest("port is invalid. The port number is between 0-65535"); } + if (port != Config.http_port) { + return ResponseEntityBuilder.badRequest("port must be FE HTTP port: " + Config.http_port); + } String versionStr = request.getParameter(VERSION); if (Strings.isNullOrEmpty(versionStr)) { @@ -242,9 +271,7 @@ public Object check(HttpServletRequest request, HttpServletResponse response) th @RequestMapping(value = "/dump", method = RequestMethod.GET) public Object dump(HttpServletRequest request, HttpServletResponse response) throws DdlException { - if (Config.enable_all_http_auth) { - executeCheckPassword(request, response); - } + executeCheckPassword(request, response); /* * Before dump, we acquired the catalog read lock and all databases' read lock and all diff --git a/fe/fe-core/src/main/java/org/apache/doris/master/MetaHelper.java b/fe/fe-core/src/main/java/org/apache/doris/master/MetaHelper.java index 96e7375679150d..553d29056494a6 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/master/MetaHelper.java +++ b/fe/fe-core/src/main/java/org/apache/doris/master/MetaHelper.java @@ -149,7 +149,7 @@ private static void checkFile(File file) throws IOException { public static ResponseBody doGet(String url, int timeout, Class clazz) throws IOException { Map headers = HttpURLUtil.getNodeIdentHeaders(); - LOG.info("meta helper, url: {}, timeout{}, headers: {}", url, timeout, headers); + LOG.info("meta helper, url: {}, timeout: {}, header names: {}", url, timeout, headers.keySet()); String response = HttpUtils.doGet(url, headers, timeout); return parseResponse(response, clazz); } diff --git a/fe/fe-core/src/test/java/org/apache/doris/common/util/HttpURLUtilTest.java b/fe/fe-core/src/test/java/org/apache/doris/common/util/HttpURLUtilTest.java new file mode 100644 index 00000000000000..4035326e3668c9 --- /dev/null +++ b/fe/fe-core/src/test/java/org/apache/doris/common/util/HttpURLUtilTest.java @@ -0,0 +1,87 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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.apache.doris.common.util; + +import org.apache.doris.catalog.Env; +import org.apache.doris.common.Config; +import org.apache.doris.httpv2.meta.MetaBaseAction; +import org.apache.doris.system.SystemInfoService.HostInfo; + +import org.junit.Assert; +import org.junit.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import java.net.HttpURLConnection; +import java.util.Map; + +public class HttpURLUtilTest { + + @Test + public void testNodeIdentHeadersIncludeClusterToken() throws Exception { + Env env = Mockito.mock(Env.class); + Mockito.when(env.getSelfNode()).thenReturn(new HostInfo("127.0.0.1", 9010)); + Mockito.when(env.getToken()).thenReturn("cluster-token"); + + try (MockedStatic envStatic = Mockito.mockStatic(Env.class)) { + envStatic.when(Env::getServingEnv).thenReturn(env); + + Map headers = HttpURLUtil.getNodeIdentHeaders(); + + Assert.assertEquals("127.0.0.1", headers.get(Env.CLIENT_NODE_HOST_KEY)); + Assert.assertEquals("9010", headers.get(Env.CLIENT_NODE_PORT_KEY)); + Assert.assertEquals("cluster-token", headers.get(MetaBaseAction.TOKEN)); + } + } + + @Test + public void testNodeIdentHeadersUseConfigAuthTokenWhenEnvTokenIsEmpty() throws Exception { + String oldAuthToken = Config.auth_token; + Config.auth_token = "config-token"; + Env env = Mockito.mock(Env.class); + Mockito.when(env.getSelfNode()).thenReturn(new HostInfo("127.0.0.1", 9010)); + Mockito.when(env.getToken()).thenReturn(""); + + try (MockedStatic envStatic = Mockito.mockStatic(Env.class)) { + envStatic.when(Env::getServingEnv).thenReturn(env); + + Map headers = HttpURLUtil.getNodeIdentHeaders(); + + Assert.assertEquals("config-token", headers.get(MetaBaseAction.TOKEN)); + } finally { + Config.auth_token = oldAuthToken; + } + } + + @Test + public void testNodeIdentConnectionIncludesClusterToken() throws Exception { + Env env = Mockito.mock(Env.class); + Mockito.when(env.getSelfNode()).thenReturn(new HostInfo("127.0.0.1", 9010)); + Mockito.when(env.getToken()).thenReturn("cluster-token"); + + try (MockedStatic envStatic = Mockito.mockStatic(Env.class)) { + envStatic.when(Env::getServingEnv).thenReturn(env); + + HttpURLConnection connection = HttpURLUtil.getConnectionWithNodeIdent("http://127.0.0.1:8030/info"); + + Assert.assertEquals("127.0.0.1", connection.getRequestProperty(Env.CLIENT_NODE_HOST_KEY)); + Assert.assertEquals("9010", connection.getRequestProperty(Env.CLIENT_NODE_PORT_KEY)); + Assert.assertEquals("cluster-token", connection.getRequestProperty(MetaBaseAction.TOKEN)); + } + } +} diff --git a/fe/fe-core/src/test/java/org/apache/doris/httpv2/meta/MetaServiceTest.java b/fe/fe-core/src/test/java/org/apache/doris/httpv2/meta/MetaServiceTest.java new file mode 100644 index 00000000000000..a050aa2a8a80da --- /dev/null +++ b/fe/fe-core/src/test/java/org/apache/doris/httpv2/meta/MetaServiceTest.java @@ -0,0 +1,225 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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.apache.doris.httpv2.meta; + +import org.apache.doris.catalog.Env; +import org.apache.doris.common.Config; +import org.apache.doris.ha.FrontendNodeType; +import org.apache.doris.httpv2.entity.ResponseBody; +import org.apache.doris.httpv2.exception.UnauthorizedException; +import org.apache.doris.httpv2.rest.RestApiStatusCode; +import org.apache.doris.master.MetaHelper; +import org.apache.doris.persist.Storage; +import org.apache.doris.system.Frontend; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.springframework.http.ResponseEntity; + +import java.io.File; +import java.lang.reflect.Field; + +public class MetaServiceTest { + private static final String CLUSTER_TOKEN = "cluster-token"; + private static final String CONFIG_TOKEN = "config-token"; + private static final String BAD_TOKEN = "bad-token"; + private static final String FE_HOST = "127.0.0.1"; + private static final int FE_EDIT_LOG_PORT = 9010; + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + private boolean oldLegacyNodeIdentAuth; + private boolean oldEnableAllHttpAuth; + private String oldAuthToken; + private int oldHttpPort; + private Env env; + private MockedStatic envStatic; + + @Before + public void setUp() { + oldLegacyNodeIdentAuth = Config.enable_meta_service_legacy_node_ident_auth; + oldEnableAllHttpAuth = Config.enable_all_http_auth; + oldAuthToken = Config.auth_token; + oldHttpPort = Config.http_port; + Config.enable_meta_service_legacy_node_ident_auth = true; + Config.enable_all_http_auth = false; + Config.auth_token = CONFIG_TOKEN; + Config.http_port = 8030; + + env = Mockito.mock(Env.class); + envStatic = Mockito.mockStatic(Env.class); + envStatic.when(Env::getCurrentEnv).thenReturn(env); + + Frontend frontend = new Frontend(FrontendNodeType.FOLLOWER, "fe1", FE_HOST, FE_EDIT_LOG_PORT); + Mockito.when(env.checkFeExist(FE_HOST, FE_EDIT_LOG_PORT)).thenReturn(frontend); + Mockito.when(env.getToken()).thenReturn(CLUSTER_TOKEN); + Mockito.when(env.getImageDir()).thenReturn(temporaryFolder.getRoot().getAbsolutePath()); + } + + @After + public void tearDown() { + Config.enable_meta_service_legacy_node_ident_auth = oldLegacyNodeIdentAuth; + Config.enable_all_http_auth = oldEnableAllHttpAuth; + Config.auth_token = oldAuthToken; + Config.http_port = oldHttpPort; + if (envStatic != null) { + envStatic.close(); + } + } + + @Test + public void testTokenAllowsMetaRequestFromProxyAddress() throws Exception { + MetaService service = new MetaService(); + HttpServletRequest request = newRequest("192.0.2.10", CLUSTER_TOKEN); + HttpServletResponse response = Mockito.mock(HttpServletResponse.class); + + Object result = service.role(request, response); + + Assert.assertEquals(RestApiStatusCode.OK.code, responseCode(result)); + Mockito.verify(response).setHeader("role", FrontendNodeType.FOLLOWER.name()); + } + + @Test + public void testConfigAuthTokenAllowsMetaRequestWhenEnvTokenIsEmpty() throws Exception { + Mockito.when(env.getToken()).thenReturn(""); + MetaService service = new MetaService(); + HttpServletRequest request = newRequest("192.0.2.10", CONFIG_TOKEN); + HttpServletResponse response = Mockito.mock(HttpServletResponse.class); + + Object result = service.role(request, response); + + Assert.assertEquals(RestApiStatusCode.OK.code, responseCode(result)); + Mockito.verify(response).setHeader("role", FrontendNodeType.FOLLOWER.name()); + } + + @Test + public void testLegacyFallbackAllowsProxyAddress() throws Exception { + MetaService service = new MetaService(); + HttpServletRequest request = newRequest("192.0.2.10", null); + HttpServletResponse response = Mockito.mock(HttpServletResponse.class); + + Object result = service.role(request, response); + + Assert.assertEquals(RestApiStatusCode.OK.code, responseCode(result)); + Mockito.verify(response).setHeader("role", FrontendNodeType.FOLLOWER.name()); + } + + @Test + public void testLegacyFallbackAllowsCheckFromProxyAddress() throws Exception { + MetaService service = serviceWithImageDir(); + HttpServletRequest request = newRequest("192.0.2.10", null); + HttpServletResponse response = Mockito.mock(HttpServletResponse.class); + + Object result = service.check(request, response); + + Assert.assertEquals(RestApiStatusCode.OK.code, responseCode(result)); + Mockito.verify(response).setHeader(MetaBaseAction.TOKEN, CLUSTER_TOKEN); + } + + @Test + public void testWrongTokenDoesNotFallBackToLegacyAddressCheck() { + MetaService service = new MetaService(); + HttpServletRequest request = newRequest(FE_HOST, BAD_TOKEN); + HttpServletResponse response = Mockito.mock(HttpServletResponse.class); + + Assert.assertThrows(UnauthorizedException.class, () -> service.role(request, response)); + } + + @Test + public void testLegacyFallbackCanBeDisabled() { + Config.enable_meta_service_legacy_node_ident_auth = false; + MetaService service = new MetaService(); + HttpServletRequest request = newRequest(FE_HOST, null); + HttpServletResponse response = Mockito.mock(HttpServletResponse.class); + + Assert.assertThrows(UnauthorizedException.class, () -> service.role(request, response)); + } + + @Test + public void testPutRejectsUnexpectedHttpPort() throws Exception { + MetaService service = new MetaService(); + HttpServletRequest request = newRequest(FE_HOST, CLUSTER_TOKEN); + HttpServletResponse response = Mockito.mock(HttpServletResponse.class); + Mockito.when(request.getParameter("version")).thenReturn("100"); + Mockito.when(request.getParameter("port")).thenReturn(Integer.toString(Config.http_port + 1)); + + try (MockedStatic metaHelper = Mockito.mockStatic(MetaHelper.class)) { + File partialFile = temporaryFolder.newFile("image.100.part"); + metaHelper.when(() -> MetaHelper.getFile(Mockito.anyString(), Mockito.any(File.class))) + .thenReturn(partialFile); + + Object result = service.put(request, response); + + Assert.assertEquals(RestApiStatusCode.BAD_REQUEST.code, responseCode(result)); + metaHelper.verify(() -> MetaHelper.getRemoteFile(Mockito.anyString(), Mockito.anyInt(), + Mockito.any(File.class)), Mockito.never()); + } + } + + @Test + public void testDumpAlwaysChecksPassword() throws Exception { + MetaService service = Mockito.spy(new MetaService()); + HttpServletRequest request = newRequest(FE_HOST, CLUSTER_TOKEN); + HttpServletResponse response = Mockito.mock(HttpServletResponse.class); + Mockito.doReturn(null).when(service).executeCheckPassword(request, response); + Mockito.when(env.dumpImage()).thenReturn(null); + + service.dump(request, response); + + Mockito.verify(service).executeCheckPassword(request, response); + } + + private MetaService serviceWithImageDir() throws Exception { + File imageDir = temporaryFolder.newFolder("image"); + Storage storage = new Storage(12345, CLUSTER_TOKEN, imageDir.getAbsolutePath()); + storage.writeClusterIdAndToken(); + + MetaService service = new MetaService(); + Field imageDirField = MetaService.class.getDeclaredField("imageDir"); + imageDirField.setAccessible(true); + imageDirField.set(service, imageDir); + return service; + } + + private HttpServletRequest newRequest(String remoteAddr, String token) { + HttpServletRequest request = Mockito.mock(HttpServletRequest.class); + Mockito.when(request.getHeader(Env.CLIENT_NODE_HOST_KEY)).thenReturn(FE_HOST); + Mockito.when(request.getHeader(Env.CLIENT_NODE_PORT_KEY)).thenReturn(Integer.toString(FE_EDIT_LOG_PORT)); + Mockito.when(request.getHeader(MetaBaseAction.TOKEN)).thenReturn(token); + Mockito.when(request.getRemoteAddr()).thenReturn(remoteAddr); + Mockito.when(request.getRemoteHost()).thenReturn(remoteAddr); + Mockito.when(request.getParameter("host")).thenReturn(FE_HOST); + Mockito.when(request.getParameter("port")).thenReturn(Integer.toString(FE_EDIT_LOG_PORT)); + return request; + } + + private int responseCode(Object result) { + ResponseEntity responseEntity = (ResponseEntity) result; + ResponseBody body = (ResponseBody) responseEntity.getBody(); + return body.getCode(); + } +}