[Enhancement](HttpServer) Support https interface (#16834)

1. Organize http documents
2. Add http interface authentication for FE
3. **Support https interface for FE**
4. Provide authentication interface
5. Add http interface authentication for BE
6. Support https interface for BE
This commit is contained in:
yongjinhou
2023-04-03 14:18:17 +08:00
committed by GitHub
parent ecd3fd07f6
commit aff260c06f
26 changed files with 375 additions and 14 deletions

View File

@ -528,6 +528,7 @@ if [[ "${BUILD_FE}" -eq 1 ]]; then
copy_common_files "${DORIS_OUTPUT}/fe/"
mkdir -p "${DORIS_OUTPUT}/fe/log"
mkdir -p "${DORIS_OUTPUT}/fe/doris-meta"
mkdir -p "${DORIS_OUTPUT}/fe/conf/ssl"
fi
if [[ "${BUILD_SPARK_DPP}" -eq 1 ]]; then

View File

@ -392,13 +392,26 @@ Default value: 0.0.0.0
Default:none
Declare a selection strategy for those servers have many ips. Note that there should at most one ip match this list. this is a list in semicolon-delimited format, in CIDR notation, e.g. 10.10.10.0/24 , If no ip match this rule, will choose one randomly..
Declare a selection strategy for those servers have many ips. Note that there should at most one ip match this list. this is a list in semicolon-delimited format, in CIDR notation, e.g. 10.10.10.0/24 , If no ip match this rule, will choose one randomly.
#### `http_port`
Default:8030
HTTP bind port. Defaults to 8030
HTTP bind port. All FE http ports must be same currently.
#### `https_port`
Default:8050
HTTPS bind port. All FE https ports must be same currently.
#### `enable_https`
Default:false
Https enable flag. If the value is false, http is supported. Otherwise, both http and https are supported, and http requests are automatically redirected to https.
If enable_https is true, you need to configure ssl certificate information in fe.conf.
#### `qe_max_connection`

View File

@ -38,6 +38,18 @@ under the License.
Used to get cluster http, mysql connection information.
## Path parameters
None
## Query parameters
None
## Request body
None
### Response
```

View File

@ -42,7 +42,13 @@ None
## Query parameters
None
* `db_id`
Specify database id
* `group_id`
Specify group id
## Request body

View File

@ -46,3 +46,6 @@ None
None
## Response
TO DO

View File

@ -400,6 +400,19 @@ Doris FE 通过 mysql 协议查询连接端口
FE http 端口,当前所有 FE http 端口都必须相同
#### `https_port`
默认值:8050
FE https 端口,当前所有 FE https 端口都必须相同
#### `enable_https`
默认值:false
FE https 使能标志位,false 表示支持 http,true 表示同时支持 http 与 https,并且会自动将 http 请求重定向到 https
如果 enable_https 为 true,需要在 fe.conf 中配置 ssl 证书信息
#### `qe_max_connection`
默认值:1024

View File

@ -38,6 +38,18 @@ under the License.
用于获取集群http、mysql连接信息。
## Path parameters
## Query parameters
## Request body
### Response
```

View File

@ -42,7 +42,13 @@ under the License.
## Query parameters
* `db_id`
指定数据库id
* `group_id`
指定组id
## Request body

View File

@ -46,3 +46,6 @@ under the License.
## Response
TO DO

View File

@ -126,7 +126,7 @@ Query:
## Query parameters
* query_id
* `query_id`
指定的 query id

View File

@ -338,6 +338,11 @@ public class Config extends ConfigBase {
*/
@ConfField public static long max_bdbje_clock_delta_ms = 5000; // 5s
/**
* Whether to enable all http interface authentication
*/
@ConfField public static boolean enable_all_http_auth = false;
/**
* Fe http port
* Currently, all FEs' http port must be same.
@ -345,9 +350,39 @@ public class Config extends ConfigBase {
@ConfField public static int http_port = 8030;
/**
* Whether to enable all http interface authentication
* Fe https port
* Currently, all FEs' https port must be same.
*/
@ConfField public static boolean enable_all_http_auth = false;
@ConfField public static int https_port = 8050;
/**
* ssl key store path.
* If you want to change the path, you need to create corresponding directory.
*/
@ConfField public static String key_store_path = System.getenv("DORIS_HOME")
+ "/conf/ssl/doris_ssl_certificate.keystore";
/**
* ssl key store password. Null by default.
*/
@ConfField public static String key_store_password = "";
/**
* ssl key store type. "JKS" by default.
*/
@ConfField public static String key_store_type = "JKS";
/**
* ssl key store alias. "doris_ssl_certificate" by default.
*/
@ConfField public static String key_store_alias = "doris_ssl_certificate";
/**
* https enable flag. false by default.
* If the value is false, http is supported. Otherwise, https is supported.
* Currently doris uses many ports, so http and https are not supported at the same time.
*/
@ConfField public static boolean enable_https = false;
/**
* Jetty container default configuration

View File

@ -158,10 +158,16 @@ public class DorisFE {
if (options.enableHttpServer) {
HttpServer httpServer = new HttpServer();
httpServer.setPort(Config.http_port);
httpServer.setHttpsPort(Config.https_port);
httpServer.setMaxHttpPostSize(Config.jetty_server_max_http_post_size);
httpServer.setAcceptors(Config.jetty_server_acceptors);
httpServer.setSelectors(Config.jetty_server_selectors);
httpServer.setWorkers(Config.jetty_server_workers);
httpServer.setKeyStorePath(Config.key_store_path);
httpServer.setKeyStorePassword(Config.key_store_password);
httpServer.setKeyStoreType(Config.key_store_type);
httpServer.setKeyStoreAlias(Config.key_store_alias);
httpServer.setEnableHttps(Config.enable_https);
httpServer.setMaxThreads(Config.jetty_threadPool_maxThreads);
httpServer.setMinThreads(Config.jetty_threadPool_minThreads);
httpServer.setMaxHttpHeaderSize(Config.jetty_server_max_http_header_size);
@ -192,6 +198,10 @@ public class DorisFE {
"Http port", NetUtils.HTTP_PORT_SUGGESTION)) {
throw new IOException("port " + Config.http_port + " already in use");
}
if (Config.enable_https && !NetUtils.isPortAvailable(FrontendOptions.getLocalHostAddress(),
Config.https_port, "Https port", NetUtils.HTTPS_PORT_SUGGESTION)) {
throw new IOException("port " + Config.https_port + " already in use");
}
if (!NetUtils.isPortAvailable(FrontendOptions.getLocalHostAddress(), Config.query_port,
"Query port", NetUtils.QUERY_PORT_SUGGESTION)) {
throw new IOException("port " + Config.query_port + " already in use");

View File

@ -45,6 +45,8 @@ public class NetUtils {
public static final String QUERY_PORT_SUGGESTION = "Please change the 'query_port' in fe.conf and try again.";
public static final String HTTP_PORT_SUGGESTION = "Please change the 'http_port' in fe.conf and try again. "
+ "But you need to make sure that ALL FEs http_port are same.";
public static final String HTTPS_PORT_SUGGESTION = "Please change the 'https_port' in fe.conf and try again. "
+ "But you need to make sure that ALL FEs https_port are same.";
public static final String RPC_PORT_SUGGESTION = "Please change the 'rpc_port' in fe.conf and try again.";
// Target format is "host:port"

View File

@ -36,13 +36,19 @@ import java.util.Map;
@EnableConfigurationProperties
@ServletComponentScan
public class HttpServer extends SpringBootServletInitializer {
private int port;
private int httpsPort;
private int acceptors;
private int selectors;
private int maxHttpPostSize;
private int workers;
private String keyStorePath;
private String keyStorePassword;
private String keyStoreType;
private String keyStoreAlias;
private boolean enableHttps;
private int minThreads;
private int maxThreads;
private int maxHttpHeaderSize;
@ -91,6 +97,30 @@ public class HttpServer extends SpringBootServletInitializer {
this.port = port;
}
public void setHttpsPort(int httpsPort) {
this.httpsPort = httpsPort;
}
public void setKeyStorePath(String keyStorePath) {
this.keyStorePath = keyStorePath;
}
public void setKeyStorePassword(String keyStorePassword) {
this.keyStorePassword = keyStorePassword;
}
public void setKeyStoreType(String keyStoreType) {
this.keyStoreType = keyStoreType;
}
public void setKeyStoreAlias(String keyStoreAlias) {
this.keyStoreAlias = keyStoreAlias;
}
public void setEnableHttps(boolean enableHttps) {
this.enableHttps = enableHttps;
}
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(HttpServer.class);
@ -98,7 +128,19 @@ public class HttpServer extends SpringBootServletInitializer {
public void start() {
Map<String, Object> properties = new HashMap<>();
properties.put("server.port", port);
if (enableHttps) {
properties.put("server.http.port", port);
properties.put("server.port", httpsPort);
// ssl config
properties.put("server.ssl.key-store", keyStorePath);
properties.put("server.ssl.key-store-password", keyStorePassword);
properties.put("server.ssl.key-store-type", keyStoreType);
properties.put("server.ssl.keyalias", keyStoreAlias);
properties.put("server.ssl.enabled", enableHttps);
} else {
properties.put("server.port", port);
properties.put("server.ssl.enabled", enableHttps);
}
if (FrontendOptions.isBindIPV6()) {
properties.put("server.address", "::0");
} else {
@ -109,14 +151,14 @@ public class HttpServer extends SpringBootServletInitializer {
properties.put("spring.http.encoding.charset", "UTF-8");
properties.put("spring.http.encoding.enabled", true);
properties.put("spring.http.encoding.force", true);
//enable jetty config
// enable jetty config
properties.put("server.jetty.acceptors", this.acceptors);
properties.put("server.jetty.max-http-post-size", this.maxHttpPostSize);
properties.put("server.jetty.selectors", this.selectors);
properties.put("server.jetty.threadPool.maxThreads", this.maxThreads);
properties.put("server.jetty.threadPool.minThreads", this.minThreads);
properties.put("server.max-http-header-size", this.maxHttpHeaderSize);
//Worker thread pool is not set by default, set according to your needs
// Worker thread pool is not set by default, set according to your needs
if (this.workers > 0) {
properties.put("server.jetty.workers", this.workers);
}

View File

@ -0,0 +1,54 @@
// 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.config;
import org.eclipse.jetty.security.ConstraintMapping;
import org.eclipse.jetty.security.ConstraintSecurityHandler;
import org.eclipse.jetty.util.security.Constraint;
import org.eclipse.jetty.webapp.AbstractConfiguration;
import org.eclipse.jetty.webapp.WebAppContext;
public class HttpToHttpsJettyConfig extends AbstractConfiguration {
@Override
public void configure(WebAppContext context) throws Exception {
Constraint constraint = new Constraint();
constraint.setDataConstraint(Constraint.DC_CONFIDENTIAL);
ConstraintSecurityHandler handler = new ConstraintSecurityHandler();
ConstraintMapping mappingGet = new ConstraintMapping();
mappingGet.setConstraint(constraint);
mappingGet.setPathSpec("/*");
mappingGet.setMethod("GET");
handler.addConstraintMapping(mappingGet);
ConstraintMapping mappingDel = new ConstraintMapping();
mappingDel.setConstraint(constraint);
mappingDel.setPathSpec("/*");
mappingDel.setMethod("DELETE");
handler.addConstraintMapping(mappingDel);
ConstraintMapping mappingRest = new ConstraintMapping();
mappingRest.setConstraint(constraint);
mappingRest.setPathSpec("/rest/*");
mappingRest.setMethod("POST");
handler.addConstraintMapping(mappingRest);
context.setSecurityHandler(handler);
}
}

View File

@ -0,0 +1,56 @@
// 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.config;
import org.apache.doris.common.Config;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.ServerConnector;
import org.springframework.boot.web.embedded.jetty.ConfigurableJettyWebServerFactory;
import org.springframework.boot.web.embedded.jetty.JettyServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.context.annotation.Configuration;
import java.util.Collections;
@Configuration
public class WebServerFactoryCustomizerConfig implements WebServerFactoryCustomizer<ConfigurableJettyWebServerFactory> {
@Override
public void customize(ConfigurableJettyWebServerFactory factory) {
if (Config.enable_https) {
((JettyServletWebServerFactory) factory).setConfigurations(
Collections.singleton(new HttpToHttpsJettyConfig())
);
factory.addServerCustomizers(
server -> {
HttpConfiguration httpConfiguration = new HttpConfiguration();
httpConfiguration.setSecurePort(Config.https_port);
httpConfiguration.setSecureScheme("https");
ServerConnector connector = new ServerConnector(server);
connector.addConnectionFactory(new HttpConnectionFactory(httpConfiguration));
connector.setPort(Config.http_port);
server.addConnector(connector);
}
);
}
}
}

View File

@ -296,6 +296,11 @@ public class BaseController {
}
protected String getCurrentFrontendURL() {
return "http://" + FrontendOptions.getLocalHostAddress() + ":" + Config.http_port;
if (Config.enable_https) {
// this could be the result of redirection.
return "https://" + FrontendOptions.getLocalHostAddress() + ":" + Config.https_port;
} else {
return "http://" + FrontendOptions.getLocalHostAddress() + ":" + Config.http_port;
}
}
}

View File

@ -103,6 +103,10 @@ public class ColocateMetaService extends RestBaseController {
@RequestMapping(path = "/api/colocate/group_stable", method = {RequestMethod.POST, RequestMethod.DELETE})
public Object group_stable(HttpServletRequest request, HttpServletResponse response)
throws DdlException {
if (needRedirect(request.getScheme())) {
return redirectToHttps(request);
}
executeWithoutPassword(request, response);
GroupId groupId = checkAndGetGroupId(request);
@ -118,6 +122,10 @@ public class ColocateMetaService extends RestBaseController {
@RequestMapping(path = "/api/colocate/bucketseq", method = RequestMethod.POST)
public Object bucketseq(HttpServletRequest request, HttpServletResponse response, @RequestBody String meta)
throws DdlException {
if (needRedirect(request.getScheme())) {
return redirectToHttps(request);
}
executeWithoutPassword(request, response);
final String clusterName = ConnectContext.get().getClusterName();
GroupId groupId = checkAndGetGroupId(request);

View File

@ -45,8 +45,11 @@ public class CancelLoadAction extends RestBaseController {
@RequestMapping(path = "/api/{" + DB_KEY + "}/_cancel", method = RequestMethod.POST)
public Object execute(@PathVariable(value = DB_KEY) final String dbName,
HttpServletRequest request, HttpServletResponse response) {
executeCheckPassword(request, response);
if (needRedirect(request.getScheme())) {
return redirectToHttps(request);
}
executeCheckPassword(request, response);
RedirectView redirectView = redirectToMaster(request, response);
if (redirectView != null) {
return redirectView;

View File

@ -59,6 +59,10 @@ public class LoadAction extends RestBaseController {
@RequestMapping(path = "/api/{" + DB_KEY + "}/{" + TABLE_KEY + "}/_load", method = RequestMethod.PUT)
public Object load(HttpServletRequest request, HttpServletResponse response,
@PathVariable(value = DB_KEY) String db, @PathVariable(value = TABLE_KEY) String table) {
if (needRedirect(request.getScheme())) {
return redirectToHttps(request);
}
if (Config.disable_mini_load) {
ResponseEntity entity = ResponseEntityBuilder.notFound("The mini load operation has been"
+ " disabled by default, if you need to add disable_mini_load=false in fe.conf.");
@ -73,6 +77,10 @@ public class LoadAction extends RestBaseController {
public Object streamLoad(HttpServletRequest request,
HttpServletResponse response,
@PathVariable(value = DB_KEY) String db, @PathVariable(value = TABLE_KEY) String table) {
if (needRedirect(request.getScheme())) {
return redirectToHttps(request);
}
executeCheckPassword(request, response);
return executeWithoutPassword(request, response, db, table, true);
}
@ -81,6 +89,10 @@ public class LoadAction extends RestBaseController {
public Object streamLoad2PC(HttpServletRequest request,
HttpServletResponse response,
@PathVariable(value = DB_KEY) String db) {
if (needRedirect(request.getScheme())) {
return redirectToHttps(request);
}
executeCheckPassword(request, response);
return executeStreamLoad2PC(request, db);
}
@ -90,6 +102,10 @@ public class LoadAction extends RestBaseController {
HttpServletResponse response,
@PathVariable(value = DB_KEY) String db,
@PathVariable(value = TABLE_KEY) String table) {
if (needRedirect(request.getScheme())) {
return redirectToHttps(request);
}
executeCheckPassword(request, response);
return executeStreamLoad2PC(request, db);
}

View File

@ -52,6 +52,10 @@ public class MultiAction extends RestBaseController {
public Object multi_desc(
@PathVariable(value = DB_KEY) final String dbName,
HttpServletRequest request, HttpServletResponse response) {
if (needRedirect(request.getScheme())) {
return redirectToHttps(request);
}
try {
executeCheckPassword(request, response);
@ -84,6 +88,10 @@ public class MultiAction extends RestBaseController {
@PathVariable(value = DB_KEY) final String dbName,
HttpServletRequest request, HttpServletResponse response)
throws DdlException {
if (needRedirect(request.getScheme())) {
return redirectToHttps(request);
}
try {
executeCheckPassword(request, response);
execEnv = ExecuteEnv.getInstance();
@ -110,6 +118,10 @@ public class MultiAction extends RestBaseController {
@PathVariable(value = DB_KEY) final String dbName,
HttpServletRequest request, HttpServletResponse response)
throws DdlException {
if (needRedirect(request.getScheme())) {
return redirectToHttps(request);
}
try {
executeCheckPassword(request, response);
execEnv = ExecuteEnv.getInstance();
@ -154,6 +166,10 @@ public class MultiAction extends RestBaseController {
public Object multi_unload(
@PathVariable(value = DB_KEY) final String dbName,
HttpServletRequest request, HttpServletResponse response) {
if (needRedirect(request.getScheme())) {
return redirectToHttps(request);
}
try {
executeCheckPassword(request, response);
execEnv = ExecuteEnv.getInstance();
@ -188,6 +204,10 @@ public class MultiAction extends RestBaseController {
@PathVariable(value = DB_KEY) final String dbName,
HttpServletRequest request, HttpServletResponse response)
throws DdlException {
if (needRedirect(request.getScheme())) {
return redirectToHttps(request);
}
try {
executeCheckPassword(request, response);
execEnv = ExecuteEnv.getInstance();
@ -222,6 +242,10 @@ public class MultiAction extends RestBaseController {
@PathVariable(value = DB_KEY) final String dbName,
HttpServletRequest request, HttpServletResponse response)
throws DdlException {
if (needRedirect(request.getScheme())) {
return redirectToHttps(request);
}
try {
executeCheckPassword(request, response);
execEnv = ExecuteEnv.getInstance();

View File

@ -20,6 +20,7 @@ package org.apache.doris.httpv2.rest;
import org.apache.doris.analysis.UserIdentity;
import org.apache.doris.catalog.Env;
import org.apache.doris.cluster.ClusterNamespace;
import org.apache.doris.common.Config;
import org.apache.doris.httpv2.controller.BaseController;
import org.apache.doris.httpv2.exception.UnauthorizedException;
import org.apache.doris.qe.ConnectContext;
@ -30,6 +31,7 @@ import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.http.HttpStatus;
import org.springframework.web.servlet.view.RedirectView;
import java.io.BufferedInputStream;
@ -172,4 +174,20 @@ public class RestBaseController extends BaseController {
}
return fullDbName;
}
public boolean needRedirect(String scheme) {
return Config.enable_https && "http".equalsIgnoreCase(scheme);
}
public Object redirectToHttps(HttpServletRequest request) {
String serverName = request.getServerName();
String uri = request.getRequestURI();
String query = request.getQueryString();
query = query == null ? "" : query;
String newUrl = "https://" + serverName + ":" + Config.https_port + uri + "?" + query;
LOG.info("redirect to new url: {}", newUrl);
RedirectView redirectView = new RedirectView(newUrl);
redirectView.setStatusCode(HttpStatus.TEMPORARY_REDIRECT);
return redirectView;
}
}

View File

@ -84,6 +84,10 @@ public class StmtExecutionAction extends RestBaseController {
@RequestMapping(path = "/api/query/{" + NS_KEY + "}/{" + DB_KEY + "}", method = {RequestMethod.POST})
public Object executeSQL(@PathVariable(value = NS_KEY) String ns, @PathVariable(value = DB_KEY) String dbName,
HttpServletRequest request, HttpServletResponse response, @RequestBody String body) {
if (needRedirect(request.getScheme())) {
return redirectToHttps(request);
}
ActionAuthorizationInfo authInfo = checkWithCookie(request, response, false);
String fullDbName = getFullDbName(dbName);
if (Config.enable_all_http_auth) {
@ -125,8 +129,12 @@ public class StmtExecutionAction extends RestBaseController {
* @return plain text of create table stmts
*/
@RequestMapping(path = "/api/query_schema/{" + NS_KEY + "}/{" + DB_KEY + "}", method = {RequestMethod.POST})
public String querySchema(@PathVariable(value = NS_KEY) String ns, @PathVariable(value = DB_KEY) String dbName,
public Object querySchema(@PathVariable(value = NS_KEY) String ns, @PathVariable(value = DB_KEY) String dbName,
HttpServletRequest request, HttpServletResponse response, @RequestBody String sql) {
if (needRedirect(request.getScheme())) {
return redirectToHttps(request);
}
checkWithCookie(request, response, false);
if (ns.equalsIgnoreCase(SystemInfoService.DEFAULT_CLUSTER)) {

View File

@ -84,6 +84,10 @@ public class TableQueryPlanAction extends RestBaseController {
@PathVariable(value = DB_KEY) final String dbName,
@PathVariable(value = TABLE_KEY) final String tblName,
HttpServletRequest request, HttpServletResponse response) {
if (needRedirect(request.getScheme())) {
return redirectToHttps(request);
}
executeCheckPassword(request, response);
// just allocate 2 slot for top holder map
Map<String, Object> resultMap = new HashMap<>(4);

View File

@ -75,6 +75,9 @@ public class UploadAction extends RestBaseController {
@PathVariable(value = TABLE_KEY) String tblName,
@RequestParam("file") MultipartFile file,
HttpServletRequest request, HttpServletResponse response) {
if (needRedirect(request.getScheme())) {
return redirectToHttps(request);
}
checkWithCookie(request, response, false);

View File

@ -76,6 +76,10 @@ public class ImportAction extends RestBaseController {
@RequestMapping(path = "/api/import/file_review", method = RequestMethod.POST)
public Object fileReview(@RequestBody FileReviewRequestVo body,
HttpServletRequest request, HttpServletResponse response) {
if (needRedirect(request.getScheme())) {
return redirectToHttps(request);
}
if (Config.enable_all_http_auth) {
executeCheckPassword(request, response);
}