diff --git a/sentinel-adapter/pom.xml b/sentinel-adapter/pom.xml index e29d926506..ca46e097e6 100755 --- a/sentinel-adapter/pom.xml +++ b/sentinel-adapter/pom.xml @@ -16,6 +16,7 @@ sentinel-web-servlet + sentinel-web-servlet-jakarta sentinel-dubbo-adapter sentinel-apache-dubbo-adapter sentinel-apache-dubbo3-adapter diff --git a/sentinel-adapter/sentinel-web-servlet-jakarta/pom.xml b/sentinel-adapter/sentinel-web-servlet-jakarta/pom.xml new file mode 100644 index 0000000000..2e02d164d5 --- /dev/null +++ b/sentinel-adapter/sentinel-web-servlet-jakarta/pom.xml @@ -0,0 +1,51 @@ + + + 4.0.0 + + + com.alibaba.csp + sentinel-adapter + 2.0.0-alpha2-SNAPSHOT + + + sentinel-web-servlet-jakarta + jar + Sentinel adapter for Jakarta EE Servlet 6.0 (Spring Boot 3.x) + + + 6.0.0 + + + + + com.alibaba.csp + sentinel-core + + + + jakarta.servlet + jakarta.servlet-api + ${jakarta.servlet-api.version} + provided + + + + junit + junit + test + + + org.springframework.boot + spring-boot-starter-web + 3.2.0 + test + + + org.springframework.boot + spring-boot-starter-test + 3.2.0 + test + + + diff --git a/sentinel-adapter/sentinel-web-servlet-jakarta/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/CommonFilter.java b/sentinel-adapter/sentinel-web-servlet-jakarta/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/CommonFilter.java new file mode 100644 index 0000000000..62e148bd25 --- /dev/null +++ b/sentinel-adapter/sentinel-web-servlet-jakarta/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/CommonFilter.java @@ -0,0 +1,144 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * 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 com.alibaba.csp.sentinel.adapter.servlet; + +import java.io.IOException; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import com.alibaba.csp.sentinel.Entry; +import com.alibaba.csp.sentinel.EntryType; +import com.alibaba.csp.sentinel.ResourceTypeConstants; +import com.alibaba.csp.sentinel.SphU; +import com.alibaba.csp.sentinel.Tracer; +import com.alibaba.csp.sentinel.adapter.servlet.callback.RequestOriginParser; +import com.alibaba.csp.sentinel.adapter.servlet.callback.UrlCleaner; +import com.alibaba.csp.sentinel.adapter.servlet.callback.WebCallbackManager; +import com.alibaba.csp.sentinel.adapter.servlet.config.WebServletConfig; +import com.alibaba.csp.sentinel.adapter.servlet.util.FilterUtil; +import com.alibaba.csp.sentinel.context.ContextUtil; +import com.alibaba.csp.sentinel.slots.block.BlockException; +import com.alibaba.csp.sentinel.util.StringUtil; + +/** + * Servlet filter that integrates with Sentinel. + * + * @author youji.zj + * @author Eric Zhao + * @author zhaoyuguang + */ +public class CommonFilter implements Filter { + + /** + * Specify whether the URL resource name should contain the HTTP method prefix (e.g. {@code POST:}). + */ + public static final String HTTP_METHOD_SPECIFY = "HTTP_METHOD_SPECIFY"; + /** + * If enabled, use the default context name, or else use the URL path as the context name, + * {@link WebServletConfig#WEB_SERVLET_CONTEXT_NAME}. Please pay attention to the number of context (EntranceNode), + * which may affect the memory footprint. + * + * @since 1.7.0 + */ + public static final String WEB_CONTEXT_UNIFY = "WEB_CONTEXT_UNIFY"; + + private final static String COLON = ":"; + + private boolean httpMethodSpecify = false; + private boolean webContextUnify = true; + + @Override + public void init(FilterConfig filterConfig) { + httpMethodSpecify = Boolean.parseBoolean(filterConfig.getInitParameter(HTTP_METHOD_SPECIFY)); + if (filterConfig.getInitParameter(WEB_CONTEXT_UNIFY) != null) { + webContextUnify = Boolean.parseBoolean(filterConfig.getInitParameter(WEB_CONTEXT_UNIFY)); + } + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + HttpServletRequest sRequest = (HttpServletRequest) request; + Entry urlEntry = null; + + try { + String target = FilterUtil.filterTarget(sRequest); + // Clean and unify the URL. + // For REST APIs, you have to clean the URL (e.g. `/foo/1` and `/foo/2` -> `/foo/:id`), or + // the amount of context and resources will exceed the threshold. + UrlCleaner urlCleaner = WebCallbackManager.getUrlCleaner(); + if (urlCleaner != null) { + target = urlCleaner.clean(target); + } + + // If you intend to exclude some URLs, you can convert the URLs to the empty string "" + // in the UrlCleaner implementation. + if (!StringUtil.isEmpty(target)) { + // Parse the request origin using registered origin parser. + String origin = parseOrigin(sRequest); + String contextName = webContextUnify ? WebServletConfig.WEB_SERVLET_CONTEXT_NAME : target; + ContextUtil.enter(contextName, origin); + + if (httpMethodSpecify) { + // Add HTTP method prefix if necessary. + String pathWithHttpMethod = sRequest.getMethod().toUpperCase() + COLON + target; + urlEntry = SphU.entry(pathWithHttpMethod, ResourceTypeConstants.COMMON_WEB, EntryType.IN); + } else { + urlEntry = SphU.entry(target, ResourceTypeConstants.COMMON_WEB, EntryType.IN); + } + } + chain.doFilter(request, response); + } catch (BlockException e) { + HttpServletResponse sResponse = (HttpServletResponse) response; + // Return the block page, or redirect to another URL. + WebCallbackManager.getUrlBlockHandler().blocked(sRequest, sResponse, e); + } catch (IOException | ServletException | RuntimeException e2) { + Tracer.traceEntry(e2, urlEntry); + throw e2; + } finally { + if (urlEntry != null) { + urlEntry.exit(); + } + ContextUtil.exit(); + } + } + + private String parseOrigin(HttpServletRequest request) { + RequestOriginParser originParser = WebCallbackManager.getRequestOriginParser(); + String origin = EMPTY_ORIGIN; + if (originParser != null) { + origin = originParser.parseOrigin(request); + if (StringUtil.isEmpty(origin)) { + return EMPTY_ORIGIN; + } + } + return origin; + } + + @Override + public void destroy() { + + } + + private static final String EMPTY_ORIGIN = ""; +} diff --git a/sentinel-adapter/sentinel-web-servlet-jakarta/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/CommonTotalFilter.java b/sentinel-adapter/sentinel-web-servlet-jakarta/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/CommonTotalFilter.java new file mode 100644 index 0000000000..a58e431dbf --- /dev/null +++ b/sentinel-adapter/sentinel-web-servlet-jakarta/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/CommonTotalFilter.java @@ -0,0 +1,81 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * 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 com.alibaba.csp.sentinel.adapter.servlet; + +import java.io.IOException; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import com.alibaba.csp.sentinel.Entry; +import com.alibaba.csp.sentinel.ResourceTypeConstants; +import com.alibaba.csp.sentinel.SphU; +import com.alibaba.csp.sentinel.Tracer; +import com.alibaba.csp.sentinel.adapter.servlet.callback.WebCallbackManager; +import com.alibaba.csp.sentinel.adapter.servlet.config.WebServletConfig; +import com.alibaba.csp.sentinel.context.ContextUtil; +import com.alibaba.csp.sentinel.slots.block.BlockException; + +/*** + * Servlet filter for all requests. + * + * @author youji.zj + */ +public class CommonTotalFilter implements Filter { + + public static final String TOTAL_URL_REQUEST = "total-url-request"; + + @Override + public void init(FilterConfig filterConfig) { + + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, + FilterChain chain) throws IOException, ServletException { + HttpServletRequest sRequest = (HttpServletRequest)request; + + Entry entry = null; + try { + ContextUtil.enter(WebServletConfig.WEB_SERVLET_CONTEXT_NAME); + entry = SphU.entry(TOTAL_URL_REQUEST, ResourceTypeConstants.COMMON_WEB); + chain.doFilter(request, response); + } catch (BlockException e) { + HttpServletResponse sResponse = (HttpServletResponse)response; + WebCallbackManager.getUrlBlockHandler().blocked(sRequest, sResponse, e); + } catch (IOException | ServletException | RuntimeException e2) { + Tracer.trace(e2); + throw e2; + } finally { + if (entry != null) { + entry.exit(); + } + ContextUtil.exit(); + } + } + + @Override + public void destroy() { + + } + +} diff --git a/sentinel-adapter/sentinel-web-servlet-jakarta/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/callback/DefaultUrlBlockHandler.java b/sentinel-adapter/sentinel-web-servlet-jakarta/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/callback/DefaultUrlBlockHandler.java new file mode 100644 index 0000000000..7f39a06c4d --- /dev/null +++ b/sentinel-adapter/sentinel-web-servlet-jakarta/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/callback/DefaultUrlBlockHandler.java @@ -0,0 +1,39 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * 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 com.alibaba.csp.sentinel.adapter.servlet.callback; + +import java.io.IOException; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import com.alibaba.csp.sentinel.adapter.servlet.util.FilterUtil; +import com.alibaba.csp.sentinel.slots.block.BlockException; + +/*** + * The default {@link UrlBlockHandler}. + * + * @author youji.zj + */ +public class DefaultUrlBlockHandler implements UrlBlockHandler { + + @Override + public void blocked(HttpServletRequest request, HttpServletResponse response, BlockException ex) + throws IOException { + // Directly redirect to the default flow control (blocked) page or customized block page. + FilterUtil.blockRequest(request, response); + } +} diff --git a/sentinel-adapter/sentinel-web-servlet-jakarta/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/callback/DefaultUrlCleaner.java b/sentinel-adapter/sentinel-web-servlet-jakarta/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/callback/DefaultUrlCleaner.java new file mode 100644 index 0000000000..2d80abd463 --- /dev/null +++ b/sentinel-adapter/sentinel-web-servlet-jakarta/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/callback/DefaultUrlCleaner.java @@ -0,0 +1,27 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * 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 com.alibaba.csp.sentinel.adapter.servlet.callback; + +/*** + * @author youji.zj + */ +public class DefaultUrlCleaner implements UrlCleaner { + + @Override + public String clean(String originUrl) { + return originUrl; + } +} diff --git a/sentinel-adapter/sentinel-web-servlet-jakarta/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/callback/RequestOriginParser.java b/sentinel-adapter/sentinel-web-servlet-jakarta/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/callback/RequestOriginParser.java new file mode 100644 index 0000000000..da127a26ed --- /dev/null +++ b/sentinel-adapter/sentinel-web-servlet-jakarta/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/callback/RequestOriginParser.java @@ -0,0 +1,35 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * 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 com.alibaba.csp.sentinel.adapter.servlet.callback; + +import jakarta.servlet.http.HttpServletRequest; + +/** + * The origin parser parses request origin (e.g. IP, user, appName) from HTTP request. + * + * @author Eric Zhao + * @since 0.2.0 + */ +public interface RequestOriginParser { + + /** + * Parse the origin from given HTTP request. + * + * @param request HTTP request + * @return parsed origin + */ + String parseOrigin(HttpServletRequest request); +} diff --git a/sentinel-adapter/sentinel-web-servlet-jakarta/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/callback/UrlBlockHandler.java b/sentinel-adapter/sentinel-web-servlet-jakarta/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/callback/UrlBlockHandler.java new file mode 100644 index 0000000000..613cb3204c --- /dev/null +++ b/sentinel-adapter/sentinel-web-servlet-jakarta/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/callback/UrlBlockHandler.java @@ -0,0 +1,41 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * 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 com.alibaba.csp.sentinel.adapter.servlet.callback; + +import java.io.IOException; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import com.alibaba.csp.sentinel.slots.block.BlockException; + +/*** + * The URL block handler handles requests when blocked. + * + * @author youji.zj + */ +public interface UrlBlockHandler { + + /** + * Handle the request when blocked. + * + * @param request Servlet request + * @param response Servlet response + * @param ex the block exception. + * @throws IOException some error occurs + */ + void blocked(HttpServletRequest request, HttpServletResponse response, BlockException ex) throws IOException; +} diff --git a/sentinel-adapter/sentinel-web-servlet-jakarta/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/callback/UrlCleaner.java b/sentinel-adapter/sentinel-web-servlet-jakarta/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/callback/UrlCleaner.java new file mode 100644 index 0000000000..c5883396bf --- /dev/null +++ b/sentinel-adapter/sentinel-web-servlet-jakarta/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/callback/UrlCleaner.java @@ -0,0 +1,31 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * 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 com.alibaba.csp.sentinel.adapter.servlet.callback; + +/*** + * @author youji.zj + */ +public interface UrlCleaner { + + /*** + *

Process the url. Some path variables should be handled and unified.

+ *

e.g. collect_item_relation--10200012121-.html will be converted to collect_item_relation.html

+ * + * @param originUrl original url + * @return processed url + */ + String clean(String originUrl); +} diff --git a/sentinel-adapter/sentinel-web-servlet-jakarta/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/callback/WebCallbackManager.java b/sentinel-adapter/sentinel-web-servlet-jakarta/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/callback/WebCallbackManager.java new file mode 100644 index 0000000000..73231bcf8e --- /dev/null +++ b/sentinel-adapter/sentinel-web-servlet-jakarta/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/callback/WebCallbackManager.java @@ -0,0 +1,63 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * 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 com.alibaba.csp.sentinel.adapter.servlet.callback; + +import com.alibaba.csp.sentinel.util.AssertUtil; + +/** + * Registry for URL cleaner and URL block handler. + * + * @author youji.zj + */ +public class WebCallbackManager { + + /** + * URL cleaner. + */ + private static volatile UrlCleaner urlCleaner = new DefaultUrlCleaner(); + + /** + * URL block handler. + */ + private static volatile UrlBlockHandler urlBlockHandler = new DefaultUrlBlockHandler(); + + private static volatile RequestOriginParser requestOriginParser = null; + + public static UrlCleaner getUrlCleaner() { + return urlCleaner; + } + + public static void setUrlCleaner(UrlCleaner urlCleaner) { + WebCallbackManager.urlCleaner = urlCleaner; + } + + public static UrlBlockHandler getUrlBlockHandler() { + return urlBlockHandler; + } + + public static void setUrlBlockHandler(UrlBlockHandler urlBlockHandler) { + AssertUtil.isTrue(urlBlockHandler != null, "URL block handler should not be null"); + WebCallbackManager.urlBlockHandler = urlBlockHandler; + } + + public static RequestOriginParser getRequestOriginParser() { + return requestOriginParser; + } + + public static void setRequestOriginParser(RequestOriginParser requestOriginParser) { + WebCallbackManager.requestOriginParser = requestOriginParser; + } +} diff --git a/sentinel-adapter/sentinel-web-servlet-jakarta/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/config/WebServletConfig.java b/sentinel-adapter/sentinel-web-servlet-jakarta/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/config/WebServletConfig.java new file mode 100644 index 0000000000..dc6f66c42a --- /dev/null +++ b/sentinel-adapter/sentinel-web-servlet-jakarta/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/config/WebServletConfig.java @@ -0,0 +1,94 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * 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 com.alibaba.csp.sentinel.adapter.servlet.config; + +import com.alibaba.csp.sentinel.adapter.servlet.CommonFilter; +import com.alibaba.csp.sentinel.adapter.servlet.CommonTotalFilter; +import com.alibaba.csp.sentinel.config.SentinelConfig; +import com.alibaba.csp.sentinel.log.RecordLog; +import com.alibaba.csp.sentinel.util.StringUtil; + +/** + * The configuration center for Web Servlet adapter. + * + * @author leyou + * @author zhaoyuguang + */ +public final class WebServletConfig { + + public static final String WEB_SERVLET_CONTEXT_NAME = "sentinel_web_servlet_context"; + + public static final String BLOCK_PAGE_URL_CONF_KEY = "csp.sentinel.web.servlet.block.page"; + public static final String BLOCK_PAGE_HTTP_STATUS_CONF_KEY = "csp.sentinel.web.servlet.block.status"; + + private static final int HTTP_STATUS_TOO_MANY_REQUESTS = 429; + + /** + * Get redirecting page when Sentinel blocking for {@link CommonFilter} or + * {@link CommonTotalFilter} occurs. + * + * @return the block page URL, maybe null if not configured. + */ + public static String getBlockPage() { + return SentinelConfig.getConfig(BLOCK_PAGE_URL_CONF_KEY); + } + + public static void setBlockPage(String blockPage) { + SentinelConfig.setConfig(BLOCK_PAGE_URL_CONF_KEY, blockPage); + } + + /** + *

Get the HTTP status when using the default block page.

+ *

You can set the status code with the {@code -Dcsp.sentinel.web.servlet.block.status} + * property. When the property is empty or invalid, Sentinel will use 429 (Too Many Requests) + * as the default status code.

+ * + * @return the HTTP status of the default block page + * @since 1.7.0 + */ + public static int getBlockPageHttpStatus() { + String value = SentinelConfig.getConfig(BLOCK_PAGE_HTTP_STATUS_CONF_KEY); + if (StringUtil.isEmpty(value)) { + return HTTP_STATUS_TOO_MANY_REQUESTS; + } + try { + int s = Integer.parseInt(value); + if (s <= 0) { + throw new IllegalArgumentException("Invalid status code: " + s); + } + return s; + } catch (Exception e) { + RecordLog.warn("[WebServletConfig] Invalid block HTTP status (" + value + "), using default 429"); + setBlockPageHttpStatus(HTTP_STATUS_TOO_MANY_REQUESTS); + } + return HTTP_STATUS_TOO_MANY_REQUESTS; + } + + /** + * Set the HTTP status of the default block page. + * + * @param httpStatus the HTTP status of the default block page + * @since 1.7.0 + */ + public static void setBlockPageHttpStatus(int httpStatus) { + if (httpStatus <= 0) { + throw new IllegalArgumentException("Invalid HTTP status code: " + httpStatus); + } + SentinelConfig.setConfig(BLOCK_PAGE_HTTP_STATUS_CONF_KEY, String.valueOf(httpStatus)); + } + + private WebServletConfig() {} +} diff --git a/sentinel-adapter/sentinel-web-servlet-jakarta/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/util/FilterUtil.java b/sentinel-adapter/sentinel-web-servlet-jakarta/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/util/FilterUtil.java new file mode 100644 index 0000000000..b9498ae84d --- /dev/null +++ b/sentinel-adapter/sentinel-web-servlet-jakarta/src/main/java/com/alibaba/csp/sentinel/adapter/servlet/util/FilterUtil.java @@ -0,0 +1,191 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * 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 com.alibaba.csp.sentinel.adapter.servlet.util; + +import java.io.IOException; +import java.io.PrintWriter; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import com.alibaba.csp.sentinel.adapter.servlet.config.WebServletConfig; +import com.alibaba.csp.sentinel.util.StringUtil; + +/** + * Util class for web servlet filter. + * + * @author zhaoyuguang + * @author youji.zj + * @author Eric Zhao + */ +public final class FilterUtil { + + private static final String PATH_SPLIT = "/"; + + public static String filterTarget(HttpServletRequest request) { + String pathInfo = getResourcePath(request); + if (!pathInfo.startsWith(PATH_SPLIT)) { + pathInfo = PATH_SPLIT + pathInfo; + } + + if (PATH_SPLIT.equals(pathInfo)) { + return pathInfo; + } + + // Note: pathInfo should be converted to camelCase style. + int lastSlashIndex = pathInfo.lastIndexOf("/"); + + if (lastSlashIndex >= 0) { + pathInfo = pathInfo.substring(0, lastSlashIndex) + "/" + + StringUtil.trim(pathInfo.substring(lastSlashIndex + 1)); + } else { + pathInfo = PATH_SPLIT + StringUtil.trim(pathInfo); + } + + return pathInfo; + } + + public static void blockRequest(HttpServletRequest request, HttpServletResponse response) throws IOException { + StringBuffer url = request.getRequestURL(); + + if ("GET".equals(request.getMethod()) && StringUtil.isNotBlank(request.getQueryString())) { + url.append("?").append(request.getQueryString()); + } + + if (StringUtil.isBlank(WebServletConfig.getBlockPage())) { + writeDefaultBlockedPage(response, WebServletConfig.getBlockPageHttpStatus()); + } else { + String redirectUrl = WebServletConfig.getBlockPage() + "?http_referer=" + url.toString(); + // Redirect to the customized block page. + response.sendRedirect(redirectUrl); + } + } + + private static void writeDefaultBlockedPage(HttpServletResponse response, int httpStatus) throws IOException { + response.setStatus(httpStatus); + PrintWriter out = response.getWriter(); + out.print(DEFAULT_BLOCK_MSG); + out.flush(); + out.close(); + } + + private static String getResourcePath(HttpServletRequest request) { + String pathInfo = normalizeAbsolutePath(request.getPathInfo(), false); + String servletPath = normalizeAbsolutePath(request.getServletPath(), pathInfo.length() != 0); + + return servletPath + pathInfo; + } + + private static String normalizeAbsolutePath(String path, boolean removeTrailingSlash) throws IllegalStateException { + return normalizePath(path, true, false, removeTrailingSlash); + } + + private static String normalizePath(String path, boolean forceAbsolute, boolean forceRelative, + boolean removeTrailingSlash) throws IllegalStateException { + char[] pathChars = StringUtil.trimToEmpty(path).toCharArray(); + int length = pathChars.length; + + // Check path and slash. + boolean startsWithSlash = false; + boolean endsWithSlash = false; + + if (length > 0) { + char firstChar = pathChars[0]; + char lastChar = pathChars[length - 1]; + + startsWithSlash = firstChar == PATH_SPLIT.charAt(0) || firstChar == '\\'; + endsWithSlash = lastChar == PATH_SPLIT.charAt(0) || lastChar == '\\'; + } + + StringBuilder buf = new StringBuilder(length); + boolean isAbsolutePath = forceAbsolute || !forceRelative && startsWithSlash; + int index = startsWithSlash ? 0 : -1; + int level = 0; + + if (isAbsolutePath) { + buf.append(PATH_SPLIT); + } + + while (index < length) { + index = indexOfSlash(pathChars, index + 1, false); + + if (index == length) { + break; + } + + int nextSlashIndex = indexOfSlash(pathChars, index, true); + + String element = new String(pathChars, index, nextSlashIndex - index); + index = nextSlashIndex; + + // Ignore "." + if (".".equals(element)) { + continue; + } + + // Backtrack ".." + if ("..".equals(element)) { + if (level == 0) { + if (isAbsolutePath) { + throw new IllegalStateException(path); + } else { + buf.append("..").append(PATH_SPLIT); + } + } else { + buf.setLength(pathChars[--level]); + } + + continue; + } + + pathChars[level++] = (char)buf.length(); + buf.append(element).append(PATH_SPLIT); + } + + // remove the last "/" + if (buf.length() > 0) { + if (!endsWithSlash || removeTrailingSlash) { + buf.setLength(buf.length() - 1); + } + } + + return buf.toString(); + } + + private static int indexOfSlash(char[] chars, int beginIndex, boolean slash) { + int i = beginIndex; + + for (; i < chars.length; i++) { + char ch = chars[i]; + + if (slash) { + if (ch == PATH_SPLIT.charAt(0) || ch == '\\') { + break; // if a slash + } + } else { + if (ch != PATH_SPLIT.charAt(0) && ch != '\\') { + break; // if not a slash + } + } + } + + return i; + } + + public static final String DEFAULT_BLOCK_MSG = "Blocked by Sentinel (flow limiting)"; + + private FilterUtil() {} +}