blob: ce8429fbe6309f1f2aca773300dcaf65c5d0917d [file] [log] [blame]
/*
* Copyright (c) 2017-2018 Oracle and/or its affiliates. All rights reserved.
* Copyright 2004 The Apache Software Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.catalina.core;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.ResourceBundle;
import java.util.Set;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletRequestWrapper;
import jakarta.servlet.SessionTrackingMode;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import jakarta.servlet.http.PushBuilder;
import org.apache.catalina.Context;
import org.apache.catalina.LogFacade;
import org.apache.catalina.connector.Request;
import org.glassfish.grizzly.http.Cookie;
import org.glassfish.grizzly.http.Method;
import org.glassfish.grizzly.http.server.http2.PushEvent;
import org.glassfish.grizzly.http2.Http2Stream;
import org.glassfish.grizzly.http.util.Header;
import org.glassfish.grizzly.http.util.MimeHeaders;
/**
* Implementation of jakarta.servlet.http.PushBuilder.
*
* @author Shing Wai Chan
*/
public class ApplicationPushBuilder implements PushBuilder {
private static final ResourceBundle rb = LogFacade.getLogger().getResourceBundle();
private static final Header[] REMOVE_HEADERS = {
Header.ETag,
Header.IfModifiedSince,
Header.IfNoneMatch,
Header.IfRange,
Header.IfUnmodifiedSince,
Header.IfMatch,
Header.LastModified,
Header.Referer,
Header.AcceptRanges,
Header.Range,
Header.AcceptRanges,
Header.ContentRange,
Header.Authorization,
Header.ProxyAuthenticate,
Header.ProxyAuthorization,
};
// RFC 7232
private static final Header[] CONDITIONAL_HEADERS = {
Header.IfModifiedSince,
Header.IfNoneMatch,
Header.IfRange,
Header.IfUnmodifiedSince,
Header.IfMatch,
};
private final HttpServletRequest baseRequest;
private final Request catalinaRequest;
private final org.glassfish.grizzly.http.server.Request coyoteRequest;
private final String sessionCookieName;
private final boolean addSessionCookie;
private final boolean addSessionPathParameter;
private final MimeHeaders headers = new MimeHeaders();
private final List<Cookie> cookies = new ArrayList<>();
private String method = "GET";
private String path;
private String queryString;
private String sessionId;
public ApplicationPushBuilder(HttpServletRequest request) {
baseRequest = request;
// Need a reference to the CoyoteRequest in order to process the push
ServletRequest current = request;
while (current instanceof ServletRequestWrapper) {
current = ((ServletRequestWrapper) current).getRequest();
}
if (current instanceof Request) {
catalinaRequest = ((Request) current);
coyoteRequest = catalinaRequest.getCoyoteRequest();
} else {
String msg = MessageFormat.format(
rb.getString(LogFacade.NO_PUSH_COYOTE_REQUEST_EXCEPTION), current.getClass().getName());
throw new UnsupportedOperationException(msg);
}
headers.copyFrom(coyoteRequest.getRequest().getHeaders());
String authorization = headers.getHeader(Header.Authorization);
for (Header removeHeader : REMOVE_HEADERS) {
headers.removeHeader(removeHeader);
}
if (request.getRemoteUser() != null) {
headers.addValue(Header.Authorization).setString(authorization);
}
// set the referer header
StringBuffer referer = request.getRequestURL();
if (request.getQueryString() != null) {
referer.append('?');
referer.append(request.getQueryString());
}
headers.addValue(Header.Referer).setString(referer.toString());
// Session
Context context = catalinaRequest.getContext();
sessionCookieName = context.getSessionCookieName();
HttpSession session = request.getSession(false);
if (session != null) {
sessionId = session.getId();
}
if (sessionId == null) {
sessionId = request.getRequestedSessionId();
}
if (!request.isRequestedSessionIdFromCookie() && !request.isRequestedSessionIdFromURL() &&
sessionId != null) {
Set<SessionTrackingMode> sessionTrackingModes =
request.getServletContext().getEffectiveSessionTrackingModes();
addSessionCookie = sessionTrackingModes.contains(SessionTrackingMode.COOKIE);
addSessionPathParameter = sessionTrackingModes.contains(SessionTrackingMode.URL);
} else {
addSessionCookie = request.isRequestedSessionIdFromCookie();
addSessionPathParameter = request.isRequestedSessionIdFromURL();
}
// Cookies
if (request.getCookies() != null) {
for (jakarta.servlet.http.Cookie c : request.getCookies()) {
cookies.add(new Cookie(c.getName(), c.getValue()));
}
}
org.glassfish.grizzly.http.server.Response coyoteResponse = coyoteRequest.getResponse();
for (Cookie responseCookie : coyoteResponse.getCookies()) {
if (responseCookie.getMaxAge() > 0) {
cookies.add(new Cookie(responseCookie.getName(), responseCookie.getValue()));
} else {
// Path information not available so can only remove based on name.
Iterator<Cookie> cookieIterator = cookies.iterator();
while (cookieIterator.hasNext()) {
Cookie cookie = cookieIterator.next();
if (cookie.getName().equals(responseCookie.getName())) {
cookieIterator.remove();
}
}
}
}
if (cookies != null && !cookies.isEmpty()) {
for (Cookie c : cookies) {
headers.addValue(Header.Cookie).setString(c.asClientCookieString());
}
}
}
@Override
public PushBuilder method(String method) {
if (method == null) {
throw new NullPointerException(rb.getString(LogFacade.NULL_PUSH_METHOD_EXCEPTION));
}
if (method.length() == 0) {
throw new IllegalArgumentException(rb.getString(LogFacade.EMPTY_PUSH_METHOD_EXCEPTION));
}
String upperMethod = method.toUpperCase(Locale.ENGLISH);
if (upperMethod.equals("POST") || upperMethod.equals("PUT") ||
upperMethod.equals("DELETE") || upperMethod.equals("CONNECT") ||
upperMethod.equals("OPTIONS") || upperMethod.equals("TRACE")) {
String msg = MessageFormat.format(
rb.getString(LogFacade.NONCACHEABLE_UNSAFE_PUSH_METHOD_EXCEPTION), method);
throw new IllegalArgumentException(msg);
}
this.method = method;
return this;
}
@Override
public String getMethod() {
return method;
}
@Override
public PushBuilder queryString(String queryString) {
this.queryString = queryString;
return this;
}
@Override
public String getQueryString() {
return queryString;
}
@Override
public PushBuilder sessionId(String sessionId) {
this.sessionId = sessionId;
return this;
}
@Override
public String getSessionId() {
return sessionId;
}
@Override
public PushBuilder addHeader(String name, String value) {
if (isValidNameValue(name, value)) {
headers.addValue(name).setString(value);
}
return this;
}
@Override
public PushBuilder setHeader(String name, String value) {
if (isValidNameValue(name, value)) {
headers.setValue(name).setString(value);
}
return this;
}
@Override
public PushBuilder removeHeader(String name) {
if (isValidName(name)) {
headers.removeHeader(name);
}
return this;
}
@Override
public Set<String> getHeaderNames() {
HashSet<String> names = new HashSet<>();
Iterator<String> nameIter = headers.names().iterator();
while (nameIter.hasNext()) {
names.add(nameIter.next());
}
return names;
}
@Override
public String getHeader(String name) {
return headers.getHeader(name);
}
@Override
public PushBuilder path(String path) {
if (path != null && !path.startsWith("/")) {
this.path = baseRequest.getContextPath() + "/" + path;
} else {
this.path = path;
}
return this;
}
@Override
public String getPath() {
return path;
}
@Override
public void push() {
if (path == null) {
throw new IllegalStateException(rb.getString(LogFacade.NO_PUSH_PATH_EXCEPTION));
}
Http2Stream http2Stream = (Http2Stream) coyoteRequest.getAttribute(Http2Stream.HTTP2_STREAM_ATTRIBUTE);
if (http2Stream == null || !http2Stream.isPushEnabled()) {
return;
}
// modify pathLocal rather than path
String pathLocal = ((path.charAt(0) == '/') ? path : coyoteRequest.getContextPath() + '/' + path);
if (queryString != null) {
pathLocal += ((pathLocal.indexOf('?') != -1)
? '&' + queryString
: '?' + queryString);
}
// Session ID (do this before setting the path since it may change it)
if (sessionId != null) {
if (addSessionPathParameter) {
pathLocal = pathLocal + ";" + sessionCookieName + "=" + sessionId;
}
if (addSessionCookie) {
cookies.add(new Cookie(sessionCookieName, sessionId));
}
}
PushEvent.PushEventBuilder pushEventBuilder = PushEvent.builder();
pushEventBuilder.method(method);
pushEventBuilder.headers(headers);
pushEventBuilder.path(pathLocal);
pushEventBuilder.httpRequest(coyoteRequest.getRequest());
coyoteRequest.getContext().notifyDownstream(pushEventBuilder.build());
// Reset for next call
path = null;
for (Header conditionalHeader : CONDITIONAL_HEADERS) {
headers.removeHeader(conditionalHeader);
}
}
private static boolean isValidNameValue(final String name, final String value) {
return isValidName(name) && value != null && !value.isEmpty();
}
private static boolean isValidName(final String name) {
return (name != null && !name.isEmpty());
}
}