blob: 76b0ee682284dfa5205d92e773611abd54266588 [file] [log] [blame]
/*
* 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.catalina.core;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.servlet.ServletRequest;
import javax.servlet.ServletRequestWrapper;
import javax.servlet.SessionTrackingMode;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import javax.servlet.http.PushBuilder;
import org.apache.catalina.Context;
import org.apache.catalina.connector.Request;
import org.apache.catalina.util.SessionConfig;
import org.apache.coyote.ActionCode;
import org.apache.coyote.PushToken;
import org.apache.tomcat.util.buf.B2CConverter;
import org.apache.tomcat.util.buf.HexUtils;
import org.apache.tomcat.util.collections.CaseInsensitiveKeyMap;
import org.apache.tomcat.util.http.CookieProcessor;
import org.apache.tomcat.util.res.StringManager;
public class ApplicationPushBuilder implements PushBuilder {
private static final StringManager sm = StringManager.getManager(ApplicationPushBuilder.class);
private final HttpServletRequest baseRequest;
private final Request catalinaRequest;
private final org.apache.coyote.Request coyoteRequest;
private final String sessionCookieName;
private final String sessionPathParameterName;
private final boolean addSessionCookie;
private final boolean addSessionPathParameter;
private final Map<String,List<String>> headers = new CaseInsensitiveKeyMap<>();
private final List<Cookie> cookies = new ArrayList<>();
private String method = "GET";
private String path;
private String etag;
private String lastModified;
private String queryString;
private String sessionId;
private boolean conditional;
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 {
throw new UnsupportedOperationException(sm.getString(
"applicationPushBuilder.noCoyoteRequest", current.getClass().getName()));
}
// Populate the initial list of HTTP headers
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
List<String> values = new ArrayList<>();
headers.put(headerName, values);
Enumeration<String> headerValues = request.getHeaders(headerName);
while (headerValues.hasMoreElements()) {
values.add(headerValues.nextElement());
}
}
// Remove the headers
headers.remove("if-match");
if (headers.remove("if-none-match") != null) {
conditional = true;
}
if (headers.remove("if-modified-since") != null) {
conditional = true;
}
headers.remove("if-unmodified-since");
headers.remove("if-range");
headers.remove("range");
headers.remove("expect");
headers.remove("authorization");
headers.remove("referer");
// Also remove the cookie header since it will be regenerated
headers.remove("cookie");
// set the referer header
StringBuffer referer = request.getRequestURL();
if (request.getQueryString() != null) {
referer.append('?');
referer.append(request.getQueryString());
}
addHeader("referer", referer.toString());
// Session
Context context = catalinaRequest.getContext();
sessionCookieName = SessionConfig.getSessionCookieName(context);
sessionPathParameterName = SessionConfig.getSessionUriParamName(context);
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 (Cookie requestCookie : request.getCookies()) {
cookies.add(requestCookie);
}
}
for (Cookie responseCookie : catalinaRequest.getResponse().getCookies()) {
if (responseCookie.getMaxAge() < 0) {
// 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();
}
}
} else {
cookies.add(new Cookie(responseCookie.getName(), responseCookie.getValue()));
}
}
}
@Override
public PushBuilder path(String path) {
if (path.startsWith("/")) {
this.path = path;
} else {
String contextPath = baseRequest.getContextPath();
int len = contextPath.length() + path.length() + 1;
StringBuilder sb = new StringBuilder(len);
sb.append(contextPath);
sb.append('/');
sb.append(path);
this.path = sb.toString();
}
return this;
}
@Override
public String getPath() {
return path;
}
@Override
public PushBuilder method(String method) {
this.method = method;
return this;
}
@Override
public String getMethod() {
return method;
}
@Override
public PushBuilder etag(String etag) {
this.etag = etag;
return this;
}
@Override
public String getEtag() {
return etag;
}
@Override
public PushBuilder lastModified(String lastModified) {
this.lastModified = lastModified;
return this;
}
@Override
public String getLastModified() {
return lastModified;
}
@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 conditional(boolean conditional) {
this.conditional = conditional;
return this;
}
@Override
public boolean isConditional() {
return conditional;
}
@Override
public PushBuilder addHeader(String name, String value) {
List<String> values = headers.get(name);
if (values == null) {
values = new ArrayList<>();
headers.put(name, values);
}
values.add(value);
return this;
}
@Override
public PushBuilder setHeader(String name, String value) {
List<String> values = headers.get(name);
if (values == null) {
values = new ArrayList<>();
headers.put(name, values);
} else {
values.clear();
}
values.add(value);
return this;
}
@Override
public PushBuilder removeHeader(String name) {
headers.remove(name);
return this;
}
@Override
public Set<String> getHeaderNames() {
return Collections.unmodifiableSet(headers.keySet());
}
@Override
public String getHeader(String name) {
List<String> values = headers.get(name);
if (values == null) {
return null;
} else {
return values.get(0);
}
}
@Override
public boolean push() {
if (path == null) {
throw new IllegalStateException(sm.getString("pushBuilder.noPath"));
}
org.apache.coyote.Request pushTarget = new org.apache.coyote.Request();
pushTarget.method().setString(method);
// The next three are implied by the Javadoc getPath()
pushTarget.serverName().setString(baseRequest.getServerName());
pushTarget.setServerPort(baseRequest.getServerPort());
pushTarget.scheme().setString(baseRequest.getScheme());
// Copy headers
for (Map.Entry<String,List<String>> header : headers.entrySet()) {
for (String value : header.getValue()) {
pushTarget.getMimeHeaders().addValue(header.getKey()).setString(value);
}
}
// Path and query string
int queryIndex = path.indexOf('?');
String pushPath;
String pushQueryString = null;
if (queryIndex > -1) {
pushPath = path.substring(0, queryIndex);
if (queryIndex + 1 < path.length()) {
pushQueryString = path.substring(queryIndex + 1);
}
} else {
pushPath = path;
}
// Session ID (do this before setting the path since it may change it)
if (sessionId != null) {
if (addSessionPathParameter) {
pushPath = pushPath + ";" + sessionPathParameterName + "=" + sessionId;
pushTarget.addPathParameter(sessionPathParameterName, sessionId);
}
if (addSessionCookie) {
cookies.add(new Cookie(sessionCookieName, sessionId));
}
}
// Undecoded path - just %nn encoded
pushTarget.requestURI().setString(pushPath);
pushTarget.decodedURI().setString(decode(pushPath,
catalinaRequest.getConnector().getURIEncodingLower()));
// Query string
if (pushQueryString == null && queryString != null) {
pushTarget.queryString().setString(queryString);
} else if (pushQueryString != null && queryString == null) {
pushTarget.queryString().setString(pushQueryString);
} else if (pushQueryString != null && queryString != null) {
pushTarget.queryString().setString(pushQueryString + "&" +queryString);
}
if (conditional) {
if (etag != null) {
setHeader("if-none-match", etag);
} else if (lastModified != null) {
setHeader("if-modified-since", lastModified);
}
}
// Cookies
setHeader("cookie", generateCookieHeader(cookies,
catalinaRequest.getContext().getCookieProcessor()));
PushToken pushToken = new PushToken(pushTarget);
coyoteRequest.action(ActionCode.PUSH_REQUEST, pushToken);
// Reset for next call to this method
pushTarget = null;
path = null;
etag = null;
lastModified = null;
headers.remove("if-none-match");
headers.remove("if-modified-since");
return pushToken.getResult();
}
// Package private so it can be tested. charsetName must be in lower case.
static String decode(String input, String charsetName) {
int start = input.indexOf('%');
int end = 0;
// Shortcut
if (start == -1) {
return input;
}
Charset charset;
try {
charset = B2CConverter.getCharsetLower(charsetName);
} catch (UnsupportedEncodingException uee) {
// Impossible since original request would have triggered an error
// before reaching here
throw new IllegalStateException(uee);
}
StringBuilder result = new StringBuilder(input.length());
while (start != -1) {
// Found the start of a %nn sequence. Copy everything form the last
// end to this start to the output.
result.append(input.substring(end, start));
// Advance the end 3 characters: %nn
end = start + 3;
while (end <input.length() && input.charAt(end) == '%') {
end += 3;
}
result.append(decode(input.substring(start, end), charset));
start = input.indexOf('%', end);
}
// Append the remaining text
result.append(input.substring(end));
return result.toString();
}
private static String decode(String percentSequence, Charset charset) {
byte[] bytes = new byte[percentSequence.length()/3];
for (int i = 0; i < bytes.length; i += 3) {
bytes[i] = (byte) (HexUtils.getDec(percentSequence.charAt(1 + 3 * i)) << 4 +
HexUtils.getDec(percentSequence.charAt(2 + 3 * i)));
}
return new String(bytes, charset);
}
private static String generateCookieHeader(List<Cookie> cookies, CookieProcessor cookieProcessor) {
StringBuilder result = new StringBuilder();
boolean first = true;
for (Cookie cookie : cookies) {
if (first) {
first = false;
} else {
result.append(';');
}
// The cookie header value generated by the CookieProcessor was
// originally intended for the Set-Cookie header on the response.
// However, if passed a Cookie with just a name and value set it
// will generate an appropriate header for the Cookie header on the
// pushed request.
result.append(cookieProcessor.generateHeader(cookie));
}
return result.toString();
}
}