mirror of
https://github.com/apache/struts.git
synced 2026-08-31 11:24:28 +00:00
Merge branch 'master' into coop-coep-post
This commit is contained in:
@@ -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.struts2.interceptor;
|
||||
|
||||
import static org.apache.struts2.interceptor.ResourceIsolationPolicy.SEC_FETCH_DEST_HEADER;
|
||||
import static org.apache.struts2.interceptor.ResourceIsolationPolicy.SEC_FETCH_MODE_HEADER;
|
||||
import static org.apache.struts2.interceptor.ResourceIsolationPolicy.SEC_FETCH_SITE_HEADER;
|
||||
import static org.apache.struts2.interceptor.ResourceIsolationPolicy.VARY_HEADER;
|
||||
|
||||
import com.opensymphony.xwork2.ActionContext;
|
||||
import com.opensymphony.xwork2.ActionInvocation;
|
||||
import com.opensymphony.xwork2.interceptor.AbstractInterceptor;
|
||||
import com.opensymphony.xwork2.interceptor.PreResultListener;
|
||||
import com.opensymphony.xwork2.util.TextParseUtil;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
/**
|
||||
* Interceptor that implements Fetch Metadata policy on incoming requests used to protect against
|
||||
* CSRF, XSSI, and cross-origin information leaks. Uses {@link StrutsResourceIsolationPolicy} to
|
||||
* filter the requests allowed to be processed.
|
||||
*
|
||||
* @see <a href="https://web.dev/fetch-metadata/">https://web.dev/fetch-metadata/</a>
|
||||
**/
|
||||
|
||||
public class FetchMetadataInterceptor extends AbstractInterceptor {
|
||||
private static final Logger logger = LogManager.getLogger(FetchMetadataInterceptor.class);
|
||||
private static final String VARY_HEADER_VALUE = String.format("%s,%s,%s", SEC_FETCH_DEST_HEADER, SEC_FETCH_SITE_HEADER, SEC_FETCH_MODE_HEADER);
|
||||
private static final String SC_FORBIDDEN = String.valueOf(HttpServletResponse.SC_FORBIDDEN);
|
||||
|
||||
private final Set<String> exemptedPaths = new HashSet<>();
|
||||
private final ResourceIsolationPolicy resourceIsolationPolicy = new StrutsResourceIsolationPolicy();
|
||||
|
||||
public void setExemptedPaths(String paths){
|
||||
this.exemptedPaths.addAll(TextParseUtil.commaDelimitedStringToSet(paths));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String intercept(ActionInvocation invocation) throws Exception {
|
||||
ActionContext context = invocation.getInvocationContext();
|
||||
HttpServletRequest request = context.getServletRequest();
|
||||
|
||||
addVaryHeaders(invocation);
|
||||
|
||||
String contextPath = request.getContextPath();
|
||||
// Apply exemptions: paths/endpoints meant to be served cross-origin
|
||||
if (exemptedPaths.contains(contextPath)) {
|
||||
return invocation.invoke();
|
||||
}
|
||||
|
||||
// Check if request is allowed
|
||||
if (resourceIsolationPolicy.isRequestAllowed(request)) {
|
||||
return invocation.invoke();
|
||||
}
|
||||
|
||||
logger.atDebug().log(
|
||||
"Fetch metadata rejected cross-origin request to %s",
|
||||
contextPath
|
||||
);
|
||||
return SC_FORBIDDEN;
|
||||
}
|
||||
|
||||
private void addVaryHeaders(ActionInvocation invocation) {
|
||||
HttpServletResponse response = invocation.getInvocationContext().getServletResponse();
|
||||
response.setHeader(VARY_HEADER, VARY_HEADER_VALUE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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.struts2.interceptor;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
* Interface for the resource isolation policies to be used for fetch metadata checks.
|
||||
*
|
||||
* Resource isolation policies are designed to protect against cross origin attacks and use the
|
||||
* {@code sec-fetch-*} request headers to decide whether to accept or reject a request. Read more
|
||||
* about <a href="https://web.dev/fetch-metadata/">Fetch Metadata.</a>
|
||||
*
|
||||
* See {@link StrutsResourceIsolationPolicy} for the default implementation used.
|
||||
*
|
||||
* @see <a href="https://web.dev/fetch-metadata/">https://web.dev/fetch-metadata/</a>
|
||||
**/
|
||||
|
||||
@FunctionalInterface
|
||||
public interface ResourceIsolationPolicy {
|
||||
String SEC_FETCH_SITE_HEADER = "sec-fetch-site";
|
||||
String SEC_FETCH_MODE_HEADER = "sec-fetch-mode";
|
||||
String SEC_FETCH_DEST_HEADER = "sec-fetch-dest";
|
||||
String VARY_HEADER = "Vary";
|
||||
String SAME_ORIGIN = "same-origin";
|
||||
String SAME_SITE = "same-site";
|
||||
String NONE = "none";
|
||||
String MODE_NAVIGATE = "navigate";
|
||||
String DEST_OBJECT = "object";
|
||||
String DEST_EMBED = "embed";
|
||||
String CROSS_SITE = "cross-site";
|
||||
String CORS = "cors";
|
||||
String DEST_SCRIPT = "script";
|
||||
String DEST_IMAGE = "image";
|
||||
|
||||
boolean isRequestAllowed(HttpServletRequest request);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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.struts2.interceptor;
|
||||
|
||||
import org.apache.logging.log4j.util.Strings;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
*
|
||||
* Default resource isolation policy used in {@link FetchMetadataInterceptor} that
|
||||
* implements the {@link ResourceIsolationPolicy} interface. This default policy is based on
|
||||
* <a href="https://web.dev/fetch-metadata/">https://web.dev/fetch-metadata/</a>.
|
||||
*
|
||||
* @see <a href="https://web.dev/fetch-metadata/">https://web.dev/fetch-metadata/</a>
|
||||
**/
|
||||
|
||||
public final class StrutsResourceIsolationPolicy implements ResourceIsolationPolicy {
|
||||
|
||||
@Override
|
||||
public boolean isRequestAllowed(HttpServletRequest request) {
|
||||
String site = request.getHeader(SEC_FETCH_SITE_HEADER);
|
||||
|
||||
// Allow requests from browsers which don't send Fetch Metadata
|
||||
if (Strings.isEmpty(site)){
|
||||
return true;
|
||||
}
|
||||
|
||||
// Allow same-site and browser-initiated requests
|
||||
if (SAME_ORIGIN.equals(site) || SAME_SITE.equals(site) || NONE.equals(site)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Allow simple top-level navigations except <object> and <embed>
|
||||
return isAllowedTopLevelNavigation(request);
|
||||
}
|
||||
|
||||
private boolean isAllowedTopLevelNavigation(HttpServletRequest request) {
|
||||
String mode = request.getHeader(SEC_FETCH_MODE_HEADER);
|
||||
String dest = request.getHeader(SEC_FETCH_DEST_HEADER);
|
||||
|
||||
boolean isSimpleTopLevelNavigation = MODE_NAVIGATE.equals(mode) || "GET".equals(request.getMethod());
|
||||
boolean isNotObjectOrEmbedRequest = !DEST_EMBED.equals(dest) && !DEST_OBJECT.equals(dest);
|
||||
|
||||
return isSimpleTopLevelNavigation && isNotObjectOrEmbedRequest;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* 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.struts2.result;
|
||||
|
||||
import com.opensymphony.xwork2.ActionInvocation;
|
||||
import com.opensymphony.xwork2.Result;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.apache.logging.log4j.message.ParameterizedMessage;
|
||||
import org.apache.struts2.StrutsException;
|
||||
import org.apache.struts2.result.plain.HttpHeader;
|
||||
import org.apache.struts2.result.plain.ResponseBuilder;
|
||||
|
||||
import javax.servlet.http.Cookie;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* This result can only be used in code, as a result of action's method, eg.:
|
||||
* <p>
|
||||
* public PlainResult execute() {
|
||||
* return response -> response.write("");
|
||||
* }
|
||||
* <p>
|
||||
* Please notice the result type of the method is a PlainResult not a String.
|
||||
*/
|
||||
public interface PlainResult extends Result {
|
||||
|
||||
Logger LOG = LogManager.getLogger(PlainResult.class);
|
||||
|
||||
@Override
|
||||
default void execute(ActionInvocation invocation) throws Exception {
|
||||
LOG.debug("Executing plain result");
|
||||
ResponseBuilder builder = new ResponseBuilder();
|
||||
write(builder);
|
||||
|
||||
HttpServletResponse response = invocation.getInvocationContext().getServletResponse();
|
||||
|
||||
if (response.isCommitted()) {
|
||||
if (ignoreCommitted()) {
|
||||
LOG.warn("Http response already committed, ignoring & skipping!");
|
||||
return;
|
||||
} else {
|
||||
throw new StrutsException("Http response already committed, cannot modify it!");
|
||||
}
|
||||
}
|
||||
|
||||
for (HttpHeader<String> header : builder.getStringHeaders()) {
|
||||
LOG.debug(new ParameterizedMessage("A string header: {} = {}", header.getName(), header.getValue()));
|
||||
response.addHeader(header.getName(), header.getValue());
|
||||
}
|
||||
for (HttpHeader<Long> header : builder.getDateHeaders()) {
|
||||
LOG.debug(new ParameterizedMessage("A date header: {} = {}", header.getName(), header.getValue()));
|
||||
response.addDateHeader(header.getName(), header.getValue());
|
||||
}
|
||||
for (HttpHeader<Integer> header : builder.getIntHeaders()) {
|
||||
LOG.debug(new ParameterizedMessage("An int header: {} = {}", header.getName(), header.getValue()));
|
||||
response.addIntHeader(header.getName(), header.getValue());
|
||||
}
|
||||
|
||||
for (Cookie cookie : builder.getCookies()) {
|
||||
LOG.debug(new ParameterizedMessage("A cookie: {} = {}", cookie.getName(), cookie.getValue()));
|
||||
response.addCookie(cookie);
|
||||
}
|
||||
|
||||
response.getWriter().write(builder.getBody());
|
||||
response.flushBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implement this method in action using lambdas
|
||||
*
|
||||
* @param response a response builder used to build a Http response
|
||||
*/
|
||||
void write(ResponseBuilder response);
|
||||
|
||||
/**
|
||||
* Controls if result should ignore already committed Http response
|
||||
* If set to true only a warning will be issued and the rest of the result
|
||||
* will be skipped
|
||||
*
|
||||
* @return boolean false by default which means an exception will be thrown
|
||||
*/
|
||||
default boolean ignoreCommitted() {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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.struts2.result.plain;
|
||||
|
||||
import java.io.StringWriter;
|
||||
|
||||
class BodyWriter {
|
||||
|
||||
private final StringWriter body = new StringWriter();
|
||||
|
||||
public BodyWriter write(String out) {
|
||||
body.write(out);
|
||||
return this;
|
||||
}
|
||||
|
||||
public BodyWriter writeLine(String out) {
|
||||
body.write(out);
|
||||
body.write("\n");
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getBody() {
|
||||
return body.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* 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.struts2.result.plain;
|
||||
|
||||
class DateHttpHeader implements HttpHeader<Long> {
|
||||
|
||||
private final String name;
|
||||
private final Long value;
|
||||
|
||||
public DateHttpHeader(String name, Long value) {
|
||||
this.name = name;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public Long getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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.struts2.result.plain;
|
||||
|
||||
import javax.servlet.http.Cookie;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
class HttpCookies {
|
||||
|
||||
private final List<Cookie> cookies = new ArrayList<>();
|
||||
|
||||
public HttpCookies add(String name, String value) {
|
||||
cookies.add(new Cookie(name, value));
|
||||
return this;
|
||||
}
|
||||
|
||||
public List<Cookie> getCookies() {
|
||||
return Collections.unmodifiableList(cookies);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* 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.struts2.result.plain;
|
||||
|
||||
public interface HttpHeader<T> {
|
||||
|
||||
String getName();
|
||||
|
||||
T getValue();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* 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.struts2.result.plain;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
class HttpHeaders {
|
||||
|
||||
private final List<HttpHeader<String>> stringHeaders = new ArrayList<>();
|
||||
private final List<HttpHeader<Long>> dateHeaders = new ArrayList<>();
|
||||
private final List<HttpHeader<Integer>> intHeaders = new ArrayList<>();
|
||||
|
||||
public HttpHeaders add(String name, String value) {
|
||||
stringHeaders.add(new StringHttpHeader(name, value));
|
||||
return this;
|
||||
}
|
||||
|
||||
public HttpHeaders add(String name, Long value) {
|
||||
dateHeaders.add(new DateHttpHeader(name, value));
|
||||
return this;
|
||||
}
|
||||
|
||||
public HttpHeaders add(String name, Integer value) {
|
||||
intHeaders.add(new IntHttpHeader(name, value));
|
||||
return this;
|
||||
}
|
||||
|
||||
public List<HttpHeader<String>> getStringHeaders() {
|
||||
return Collections.unmodifiableList(stringHeaders);
|
||||
}
|
||||
|
||||
public List<HttpHeader<Long>> getDateHeaders() {
|
||||
return Collections.unmodifiableList(dateHeaders);
|
||||
}
|
||||
|
||||
public List<HttpHeader<Integer>> getIntHeaders() {
|
||||
return Collections.unmodifiableList(intHeaders);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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.struts2.result.plain;
|
||||
|
||||
class IntHttpHeader implements HttpHeader<Integer> {
|
||||
|
||||
private final String name;
|
||||
private final Integer value;
|
||||
|
||||
public IntHttpHeader(String name, Integer value) {
|
||||
this.name = name;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public Integer getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* 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.struts2.result.plain;
|
||||
|
||||
import javax.servlet.http.Cookie;
|
||||
|
||||
public class ResponseBuilder {
|
||||
|
||||
public static final String CONTENT_TYPE = "Content-Type";
|
||||
|
||||
public static final String TEXT_PLAIN = "text/plain";
|
||||
public static final String TEXT_HTML = "text/html";
|
||||
public static final String APPLICATION_JSON = "application/json";
|
||||
|
||||
private final BodyWriter body;
|
||||
private final HttpHeaders headers;
|
||||
private final HttpCookies cookies;
|
||||
|
||||
public ResponseBuilder() {
|
||||
this.body = new BodyWriter();
|
||||
this.headers = new HttpHeaders().add(CONTENT_TYPE, TEXT_PLAIN + "; charset=UTF-8");
|
||||
this.cookies = new HttpCookies();
|
||||
}
|
||||
|
||||
public ResponseBuilder write(String out) {
|
||||
body.write(out);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ResponseBuilder writeLine(String out) {
|
||||
body.writeLine(out);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ResponseBuilder withHeader(String name, String value) {
|
||||
headers.add(name, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ResponseBuilder withHeader(String name, Long value) {
|
||||
headers.add(name, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ResponseBuilder withHeader(String name, Integer value) {
|
||||
headers.add(name, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ResponseBuilder withContentTypeTextPlain() {
|
||||
headers.add(CONTENT_TYPE, TEXT_PLAIN + "; charset=UTF-8");
|
||||
return this;
|
||||
}
|
||||
|
||||
public ResponseBuilder withContentTypeTextHtml() {
|
||||
headers.add(CONTENT_TYPE, TEXT_HTML + "; charset=UTF-8");
|
||||
return this;
|
||||
}
|
||||
|
||||
public ResponseBuilder withContentTypeJson() {
|
||||
headers.add(CONTENT_TYPE, APPLICATION_JSON);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ResponseBuilder withContentType(String contentType) {
|
||||
headers.add(CONTENT_TYPE, contentType);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ResponseBuilder withCookie(String name, String value) {
|
||||
cookies.add(name, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Iterable<HttpHeader<String>> getStringHeaders() {
|
||||
return headers.getStringHeaders();
|
||||
}
|
||||
|
||||
public Iterable<HttpHeader<Long>> getDateHeaders() {
|
||||
return headers.getDateHeaders();
|
||||
}
|
||||
|
||||
public Iterable<HttpHeader<Integer>> getIntHeaders() {
|
||||
return headers.getIntHeaders();
|
||||
}
|
||||
|
||||
public Iterable<Cookie> getCookies() {
|
||||
return cookies.getCookies();
|
||||
}
|
||||
|
||||
public String getBody() {
|
||||
return body.getBody();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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.struts2.result.plain;
|
||||
|
||||
class StringHttpHeader implements HttpHeader<String> {
|
||||
|
||||
private final String name;
|
||||
private final String value;
|
||||
|
||||
public StringHttpHeader(String name, String value) {
|
||||
this.name = name;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -275,6 +275,7 @@
|
||||
<interceptor name="annotationParameterFilter" class="com.opensymphony.xwork2.interceptor.annotations.AnnotationParameterFilterInterceptor" />
|
||||
<interceptor name="multiselect" class="org.apache.struts2.interceptor.MultiselectInterceptor" />
|
||||
<interceptor name="noop" class="org.apache.struts2.interceptor.NoOpInterceptor" />
|
||||
<interceptor name="fetchMetadata" class="org.apache.struts2.interceptor.FetchMetadataInterceptor" />
|
||||
|
||||
<!-- Empty stack - performs no operations -->
|
||||
<interceptor-stack name="emptyStack">
|
||||
@@ -399,6 +400,7 @@
|
||||
<param name="exemptedPaths"></param>
|
||||
<param name="mode">same-origin</param>
|
||||
</interceptor-ref>
|
||||
<interceptor-ref name="fetchMetadata"/>
|
||||
<interceptor-ref name="validation">
|
||||
<param name="excludeMethods">input,back,cancel,browse</param>
|
||||
</interceptor-ref>
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* 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.struts2.interceptor;
|
||||
|
||||
|
||||
import static org.apache.struts2.interceptor.ResourceIsolationPolicy.SEC_FETCH_DEST_HEADER;
|
||||
import static org.apache.struts2.interceptor.ResourceIsolationPolicy.SEC_FETCH_MODE_HEADER;
|
||||
import static org.apache.struts2.interceptor.ResourceIsolationPolicy.SEC_FETCH_SITE_HEADER;
|
||||
import static org.apache.struts2.interceptor.ResourceIsolationPolicy.VARY_HEADER;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
|
||||
import com.opensymphony.xwork2.ActionContext;
|
||||
import com.opensymphony.xwork2.XWorkTestCase;
|
||||
import com.opensymphony.xwork2.mock.MockActionInvocation;
|
||||
import org.apache.struts2.ServletActionContext;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public class FetchMetadataInterceptorTest extends XWorkTestCase {
|
||||
|
||||
private final FetchMetadataInterceptor interceptor = new FetchMetadataInterceptor();
|
||||
private final MockActionInvocation mai = new MockActionInvocation();
|
||||
private final MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
private final MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
private static final String VARY_HEADER_VALUE = String.format(
|
||||
"%s,%s,%s",
|
||||
SEC_FETCH_DEST_HEADER,
|
||||
SEC_FETCH_SITE_HEADER,
|
||||
SEC_FETCH_MODE_HEADER
|
||||
);
|
||||
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
super.setUp();
|
||||
container.inject(interceptor);
|
||||
interceptor.setExemptedPaths("/foo,/bar");
|
||||
ServletActionContext.setRequest(request);
|
||||
ServletActionContext.setResponse(response);
|
||||
ActionContext context = ServletActionContext.getActionContext();
|
||||
mai.setInvocationContext(context);
|
||||
}
|
||||
|
||||
public void testNoSite() throws Exception {
|
||||
request.removeHeader("sec-fetch-site");
|
||||
|
||||
assertNotEquals("Expected interceptor to accept this request", "403",
|
||||
interceptor.intercept(mai));
|
||||
}
|
||||
|
||||
public void testValidSite() throws Exception {
|
||||
for (String header : Arrays.asList("same-origin", "same-site", "none")){
|
||||
request.addHeader("sec-fetch-site", header);
|
||||
|
||||
assertNotEquals("Expected interceptor to accept this request", "403",
|
||||
interceptor.intercept(mai));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void testValidTopLevelNavigation() throws Exception {
|
||||
request.addHeader("sec-fetch-mode", "navigate");
|
||||
request.addHeader("sec-fetch-dest", "script");
|
||||
request.setMethod("GET");
|
||||
|
||||
assertNotEquals("Expected interceptor to accept this request", "403",
|
||||
interceptor.intercept(mai));
|
||||
}
|
||||
|
||||
public void testInvalidTopLevelNavigation() throws Exception {
|
||||
for (String header : Arrays.asList("object", "embed")) {
|
||||
request.addHeader("sec-fetch-site", "foo");
|
||||
request.addHeader("sec-fetch-mode", "navigate");
|
||||
request.addHeader("sec-fetch-dest", header);
|
||||
request.setMethod("GET");
|
||||
|
||||
assertEquals("Expected interceptor to NOT accept this request", "403", interceptor.intercept(mai));
|
||||
}
|
||||
}
|
||||
|
||||
public void testPathInExemptedPaths() throws Exception {
|
||||
request.addHeader("sec-fetch-site", "foo");
|
||||
request.setContextPath("/foo");
|
||||
|
||||
assertNotEquals("Expected interceptor to accept this request", "403",
|
||||
interceptor.intercept(mai));
|
||||
}
|
||||
|
||||
public void testPathNotInExemptedPaths() throws Exception {
|
||||
request.addHeader("sec-fetch-site", "foo");
|
||||
request.setContextPath("/foobar");
|
||||
|
||||
assertEquals("Expected interceptor to NOT accept this request", "403", interceptor.intercept(mai));
|
||||
}
|
||||
|
||||
public void testVaryHeaderAcceptedReq() throws Exception {
|
||||
request.addHeader("sec-fetch-site", "foo");
|
||||
request.setContextPath("/foo");
|
||||
|
||||
interceptor.intercept(mai);
|
||||
|
||||
assertTrue("Expected vary header to be included", response.containsHeader(VARY_HEADER));
|
||||
assertEquals("Expected different vary header value", response.getHeader(VARY_HEADER), VARY_HEADER_VALUE);
|
||||
}
|
||||
|
||||
public void testVaryHeaderRejectedReq() throws Exception {
|
||||
request.addHeader("sec-fetch-site", "foo");
|
||||
|
||||
interceptor.intercept(mai);
|
||||
|
||||
assertTrue("Expected vary header to be included", response.containsHeader(VARY_HEADER));
|
||||
assertEquals("Expected different vary header value", response.getHeader(VARY_HEADER), VARY_HEADER_VALUE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* 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.struts2.result;
|
||||
|
||||
import com.opensymphony.xwork2.ActionContext;
|
||||
import com.opensymphony.xwork2.mock.MockActionInvocation;
|
||||
import org.apache.struts2.StrutsException;
|
||||
import org.apache.struts2.StrutsInternalTestCase;
|
||||
import org.apache.struts2.result.plain.ResponseBuilder;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
|
||||
public class PlainResultTest extends StrutsInternalTestCase {
|
||||
|
||||
private MockHttpServletResponse response;
|
||||
private MockActionInvocation invocation;
|
||||
|
||||
public void testWritePlainText() throws Exception {
|
||||
PlainResult result = (PlainResult) response ->
|
||||
response.write("test").withContentTypeTextPlain();
|
||||
|
||||
result.execute(invocation);
|
||||
|
||||
assertEquals("test", response.getContentAsString());
|
||||
assertEquals("text/plain; charset=UTF-8", response.getContentType());
|
||||
}
|
||||
|
||||
public void testWritePlainHtml() throws Exception {
|
||||
PlainResult result = (PlainResult) response ->
|
||||
response.write("<b>test</b>").withContentTypeTextHtml();
|
||||
|
||||
result.execute(invocation);
|
||||
|
||||
assertEquals("<b>test</b>", response.getContentAsString());
|
||||
assertEquals("text/html; charset=UTF-8", response.getContentType());
|
||||
}
|
||||
|
||||
public void testWriteJson() throws Exception {
|
||||
PlainResult result = (PlainResult) response ->
|
||||
response.write("{ 'value': 'test' }").withContentTypeJson();
|
||||
|
||||
result.execute(invocation);
|
||||
|
||||
assertEquals("{ 'value': 'test' }", response.getContentAsString());
|
||||
assertEquals("application/json", response.getContentType());
|
||||
}
|
||||
|
||||
public void testWriteContentTypeCsvWithCookie() throws Exception {
|
||||
PlainResult result = (PlainResult) response ->
|
||||
response.writeLine("name;value")
|
||||
.withContentType("text/csv")
|
||||
.withCookie("X-Test", "test")
|
||||
.writeLine("line;1")
|
||||
.write("line;2");
|
||||
|
||||
result.execute(invocation);
|
||||
|
||||
assertEquals("name;value\nline;1\nline;2", response.getContentAsString());
|
||||
assertEquals("text/csv", response.getContentType());
|
||||
}
|
||||
|
||||
public void testHeaders() throws Exception {
|
||||
PlainResult result = (PlainResult) response ->
|
||||
response.withHeader("X-String", "test")
|
||||
.withHeader("X-Date", 0L)
|
||||
.withHeader("X-Number", 100)
|
||||
.write("");
|
||||
|
||||
result.execute(invocation);
|
||||
|
||||
assertEquals("", response.getContentAsString());
|
||||
assertEquals("text/plain; charset=UTF-8", response.getContentType());
|
||||
assertEquals("test", response.getHeader("X-String"));
|
||||
assertEquals("Thu, 01 Jan 1970 00:00:00 GMT", response.getHeader("X-Date"));
|
||||
assertEquals("100", response.getHeader("X-NUmber"));
|
||||
}
|
||||
|
||||
public void testExceptionOnCommitted() throws Exception {
|
||||
response.setCommitted(true);
|
||||
|
||||
PlainResult result = (PlainResult) response ->
|
||||
response.write("");
|
||||
|
||||
try {
|
||||
result.execute(invocation);
|
||||
fail("Exception was expected!");
|
||||
} catch (StrutsException e) {
|
||||
assertEquals("Http response already committed, cannot modify it!", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public void testNoExceptionOnCommitted() throws Exception {
|
||||
response.setCommitted(true);
|
||||
|
||||
PlainResult result = new PlainResult() {
|
||||
@Override
|
||||
public void write(ResponseBuilder response) {
|
||||
response.write("");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean ignoreCommitted() {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
result.execute(invocation);
|
||||
assertTrue(true);
|
||||
} catch (StrutsException e) {
|
||||
fail(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public void setUp() throws Exception {
|
||||
super.setUp();
|
||||
invocation = new MockActionInvocation();
|
||||
response = new MockHttpServletResponse();
|
||||
invocation.setInvocationContext(ActionContext.getContext());
|
||||
|
||||
ActionContext.getContext().withServletResponse(response).withActionInvocation(invocation);
|
||||
}
|
||||
}
|
||||
@@ -104,6 +104,7 @@
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.build.outputTimestamp>2020-07-11T19:40:00Z</project.build.outputTimestamp>
|
||||
<java.version>1.8</java.version>
|
||||
|
||||
<!-- dependency versions in alphanumeric order -->
|
||||
@@ -404,6 +405,8 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-release-plugin</artifactId>
|
||||
<!-- See https://issues.apache.org/jira/browse/MRELEASE-1029 -->
|
||||
<version>3.0.0-M1</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
|
||||
Reference in New Issue
Block a user