All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.eclipse.jetty.ee8.nested.Response Maven / Gradle / Ivy

The newest version!
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.ee8.nested;

import java.io.IOException;
import java.io.PrintWriter;
import java.nio.channels.IllegalSelectorException;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Iterator;
import java.util.ListIterator;
import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;
import java.util.function.Supplier;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletResponse;
import javax.servlet.ServletResponseWrapper;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import javax.servlet.http.HttpSession;
import org.eclipse.jetty.http.CookieCompliance;
import org.eclipse.jetty.http.DateGenerator;
import org.eclipse.jetty.http.HttpCookie;
import org.eclipse.jetty.http.HttpCookie.SameSite;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpGenerator;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpHeaderValue;
import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.http.PreEncodedHttpField;
import org.eclipse.jetty.http.content.HttpContent;
import org.eclipse.jetty.io.RuntimeIOException;
import org.eclipse.jetty.io.WriteThroughWriter;
import org.eclipse.jetty.server.Context;
import org.eclipse.jetty.server.HttpCookieUtils;
import org.eclipse.jetty.server.HttpCookieUtils.SetCookieHttpField;
import org.eclipse.jetty.server.Session;
import org.eclipse.jetty.session.SessionManager;
import org.eclipse.jetty.util.AtomicBiInteger;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.URIUtil;

/**
 * 

{@link Response} provides the implementation for {@link HttpServletResponse}.

*/ public class Response implements HttpServletResponse { private static final int __MIN_BUFFER_SIZE = 1; private static final HttpField __EXPIRES_01JAN1970 = new PreEncodedHttpField(HttpHeader.EXPIRES, DateGenerator.__01Jan1970); public static final int NO_CONTENT_LENGTH = -1; public static final int USE_KNOWN_CONTENT_LENGTH = -2; /** * String used in the {@code Comment} attribute of {@link Cookie} * to support the {@code HttpOnly} attribute. */ protected static final String HTTP_ONLY_COMMENT = "__HTTP_ONLY__"; /** * String used in the {@code Comment} attribute of {@link Cookie} * to support the {@code Partitioned} attribute. */ protected static final String PARTITIONED_COMMENT = "__PARTITIONED__"; /** * The strings used in the {@code Comment} attribute of {@link Cookie} * to support the {@code SameSite} attribute. */ protected static final String SAME_SITE_COMMENT = "__SAME_SITE_"; protected static final String SAME_SITE_NONE_COMMENT = SAME_SITE_COMMENT + "NONE__"; protected static final String SAME_SITE_LAX_COMMENT = SAME_SITE_COMMENT + "LAX__"; protected static final String SAME_SITE_STRICT_COMMENT = SAME_SITE_COMMENT + "STRICT__"; public enum OutputType { NONE, STREAM, WRITER } /** * If a header name starts with this string, the header (stripped of the prefix) * can be set during include using only {@link #setHeader(String, String)} or * {@link #addHeader(String, String)}. */ public static final String SET_INCLUDE_HEADER_PREFIX = "org.eclipse.jetty.server.include."; private final HttpChannel _channel; // hi is errorSent flag, lo is include count private final AtomicBiInteger _errorSentAndIncludes = new AtomicBiInteger(); private final HttpOutput _out; private int _status = HttpStatus.OK_200; private HttpFields.Mutable _fields; private String _reason; private Locale _locale; private MimeTypes.Type _mimeType; private String _characterEncoding; private EncodingFrom _encodingFrom = EncodingFrom.NOT_SET; private String _contentType; private OutputType _outputType = OutputType.NONE; private ResponseWriter _writer; private long _contentLength = -1; private Supplier _trailers; private enum EncodingFrom { /** * Character encoding was not set, or the encoding was cleared with {@code setCharacterEncoding(null)}. */ NOT_SET, /** * Using the default character encoding from the context otherwise iso-8859-1. */ DEFAULT, /** * Character encoding was inferred from the Content-Type and will be added as a parameter to the Content-Type. */ INFERRED, /** * The default character encoding of the locale was used after a call to {@link #setLocale(Locale)}. */ SET_LOCALE, /** * The character encoding has been explicitly set using the Content-Type charset parameter with {@link #setContentType(String)}. */ SET_CONTENT_TYPE, /** * The character encoding has been explicitly set using {@link #setCharacterEncoding(String)}. */ SET_CHARACTER_ENCODING } private static final EnumSet __localeOverride = EnumSet.of(EncodingFrom.NOT_SET, EncodingFrom.DEFAULT, EncodingFrom.INFERRED, EncodingFrom.SET_LOCALE); private static final EnumSet __explicitCharset = EnumSet.of(EncodingFrom.SET_LOCALE, EncodingFrom.SET_CHARACTER_ENCODING, EncodingFrom.SET_CONTENT_TYPE); public static Response getBaseResponse(ServletResponse servletResponse) { while (true) { if (servletResponse instanceof Response response) return response; if (servletResponse instanceof ServletResponseWrapper wrapper) servletResponse = wrapper.getResponse(); else return null; } } public Response(HttpChannel channel, HttpOutput out) { _channel = channel; _out = out; } public HttpChannel getHttpChannel() { return _channel; } protected void onResponse(HttpFields.Mutable headers) { _fields = headers; } protected void recycle() { // _channel need not be recycled _fields = null; _errorSentAndIncludes.set(0); _out.recycle(); _status = HttpStatus.OK_200; _reason = null; _locale = null; _mimeType = null; _characterEncoding = null; _encodingFrom = EncodingFrom.NOT_SET; _contentType = null; _outputType = OutputType.NONE; // _writer does not need to be recycled _contentLength = -1; _trailers = null; } public HttpOutput getHttpOutput() { return _out; } public void reopen() { // Make the response mutable and reopen output. setErrorSent(false); _out.reopen(); } public void errorClose() { // Make the response immutable and soft close the output. setErrorSent(true); _out.softClose(); } /** * @return true if the response is mutable, ie not errorSent and not included */ private boolean isMutable() { return _errorSentAndIncludes.get() == 0; } private void setErrorSent(boolean errorSent) { _errorSentAndIncludes.getAndSetHi(errorSent ? 1 : 0); } public boolean isIncluding() { return _errorSentAndIncludes.getLo() > 0; } public void include() { _errorSentAndIncludes.add(0, 1); } public void included() { _errorSentAndIncludes.add(0, -1); if (_outputType == OutputType.WRITER) { _writer.reopen(); } _out.reopen(); } public void addCookie(HttpCookie cookie) { if (StringUtil.isBlank(cookie.getName())) throw new IllegalArgumentException("Cookie.name cannot be blank/null"); // add the set cookie _fields.add(new SetCookieHttpField(checkSameSite(cookie), getHttpChannel().getHttpConfiguration().getResponseCookieCompliance())); // Expire responses with set-cookie headers so they do not get cached. _fields.put(__EXPIRES_01JAN1970); } /** * Check that samesite is set on the cookie. If not, use a * context default value, if one has been set. * * @param cookie the cookie to check * @return either the original cookie, or a new one that has the samesit default set */ private HttpCookie checkSameSite(HttpCookie cookie) { if (cookie == null || cookie.getSameSite() != null) return cookie; //sameSite is not set, use the default configured for the context, if one exists ContextHandler.APIContext apiContext = _channel.getRequest().getContext(); Context context = (apiContext == null) ? null : apiContext.getCoreContext(); SameSite contextDefault = HttpCookieUtils.getSameSiteDefault(context); if (contextDefault == null) //no default set return cookie; return HttpCookie.from(cookie, HttpCookie.SAME_SITE_ATTRIBUTE, contextDefault.getAttributeValue()); } @Override public void addCookie(Cookie cookie) { //Servlet Spec 9.3 Include method: cannot set a cookie if handling an include if (isMutable()) { if (StringUtil.isBlank(cookie.getName())) throw new IllegalArgumentException("Cookie.name cannot be blank/null"); addCookie(new HttpCookieFacade(cookie)); } } /** * Replace (or add) a cookie. * Using name, path and domain, look for a matching set-cookie header and replace it. * * @param cookie The cookie to add/replace */ public void replaceCookie(HttpCookie cookie) { for (ListIterator i = _fields.listIterator(); i.hasNext(); ) { HttpField field = i.next(); if (field.getHeader() == HttpHeader.SET_COOKIE) { CookieCompliance compliance = getHttpChannel().getHttpConfiguration().getResponseCookieCompliance(); if (field instanceof HttpCookieUtils.SetCookieHttpField) { if (!HttpCookieUtils.match(((HttpCookieUtils.SetCookieHttpField) field).getHttpCookie(), cookie.getName(), cookie.getDomain(), cookie.getPath())) continue; } else { if (!HttpCookieUtils.match(field.getValue(), cookie.getName(), cookie.getDomain(), cookie.getPath())) continue; } i.set(new SetCookieHttpField(checkSameSite(cookie), compliance)); return; } } // Not replaced, so add normally addCookie(cookie); } public boolean containsHeader(String name) { return _fields.contains(name); } @Override public String encodeURL(String url) { final Request request = _channel.getRequest(); SessionManager sessionManager = _channel.getCoreRequest().getSessionManager(); if (sessionManager == null) return url; HttpURI uri = null; if (sessionManager.isCheckingRemoteSessionIdEncoding() && URIUtil.hasScheme(url)) { uri = HttpURI.from(url); String path = uri.getPath(); path = (path == null ? "" : path); int port = uri.getPort(); if (port < 0) port = URIUtil.getDefaultPortForScheme(uri.getScheme()); // Is it the same server? if (!request.getServerName().equalsIgnoreCase(uri.getHost())) return url; if (request.getServerPort() != port) return url; if (request.getContext() != null && !path.startsWith(request.getContextPath())) return url; } String sessionURLPrefix = sessionManager.getSessionIdPathParameterNamePrefix(); if (sessionURLPrefix == null) return url; if (url == null) return null; // should not encode if cookies in evidence if ((sessionManager.isUsingCookies() && request.isRequestedSessionIdFromCookie()) || !sessionManager.isUsingUriParameters()) { int prefix = url.indexOf(sessionURLPrefix); if (prefix != -1) { int suffix = url.indexOf("?", prefix); if (suffix < 0) suffix = url.indexOf("#", prefix); if (suffix <= prefix) return url.substring(0, prefix); return url.substring(0, prefix) + url.substring(suffix); } return url; } // get session; HttpSession httpSession = request.getSession(false); // no session if (httpSession == null) return url; // invalid session Session coreSession = Session.getSession(httpSession); if (!coreSession.isValid()) return url; String id = coreSession.getExtendedId(); if (uri == null) uri = HttpURI.from(url); // Already encoded int prefix = url.indexOf(sessionURLPrefix); if (prefix != -1) { int suffix = url.indexOf("?", prefix); if (suffix < 0) suffix = url.indexOf("#", prefix); if (suffix <= prefix) return url.substring(0, prefix + sessionURLPrefix.length()) + id; return url.substring(0, prefix + sessionURLPrefix.length()) + id + url.substring(suffix); } // edit the session int suffix = url.indexOf('?'); if (suffix < 0) suffix = url.indexOf('#'); if (suffix < 0) { return url + //if no path, insert the root path ((HttpScheme.HTTPS.is(uri.getScheme()) || HttpScheme.HTTP.is(uri.getScheme())) && StringUtil.isEmpty(uri.getPath()) ? "/" : "") + sessionURLPrefix + id; } return url.substring(0, suffix) + //if no path so insert the root path ((HttpScheme.HTTPS.is(uri.getScheme()) || HttpScheme.HTTP.is(uri.getScheme())) && StringUtil.isEmpty(uri.getPath()) ? "/" : "") + sessionURLPrefix + id + url.substring(suffix); } @Override public String encodeRedirectURL(String url) { return encodeURL(url); } @Override @Deprecated(since = "Servlet API 2.1") public String encodeUrl(String url) { return encodeURL(url); } @Override @Deprecated(since = "Servlet API 2.1") public String encodeRedirectUrl(String url) { return encodeRedirectURL(url); } @Override public void sendError(int sc) throws IOException { sendError(sc, null); } /** * Send an error response. *

In addition to the servlet standard handling, this method supports some additional codes:

*
*
102
Send a partial PROCESSING response and allow additional responses
*
103
Send a partial EARLY_HINT response as per RFC8297
*
-1
Abort the HttpChannel and close the connection/stream
*
* @param code The error code * @param message The message * @throws IOException If an IO problem occurred sending the error response. */ @Override public void sendError(int code, String message) throws IOException { if (isIncluding()) return; switch(code) { case -1 -> _channel.abort(new org.eclipse.jetty.server.Request.Handler.AbortException(message)); case HttpStatus.PROCESSING_102 -> sendProcessing(); case HttpStatus.EARLY_HINTS_103 -> sendEarlyHint(); default -> _channel.getState().sendError(code, message); } } /** * Sends a 102 Processing interim response. * This method is called by {@link #sendError(int)} if it is passed status code 102. * * @throws IOException if unable to send the 102 response * @see HttpServletResponse#sendError(int) */ public void sendProcessing() throws IOException { if (!isCommitted()) _channel.send102Processing(_fields.asImmutable()); } /** * Sends a 103 Early Hints interim response, as per * RFC8297. * This method is called by {@link #sendError(int)} if it is passed status code 103. * * @throws IOException if unable to send the 103 response * @see javax.servlet.http.HttpServletResponse#sendError(int) */ public void sendEarlyHint() throws IOException { if (!isCommitted()) _channel.send103EarlyHints(_fields.asImmutable()); } /** * Sends a response with one of the 300 series redirection codes. * * @param code the redirect status code * @param location the location to send in {@code Location} headers * @throws IOException if unable to send the redirect */ public void sendRedirect(int code, String location) throws IOException { sendRedirect(code, location, false); } /** * Sends a response with a HTTP version appropriate 30x redirection. * * @param location the location to send in {@code Location} headers * @param consumeAll if True, consume any HTTP/1 request input before doing the redirection. If the input cannot * be consumed without blocking, then add a `Connection: close` header to the response. * @throws IOException if unable to send the redirect */ public void sendRedirect(String location, boolean consumeAll) throws IOException { sendRedirect(getHttpChannel().getRequest().getHttpVersion().getVersion() < HttpVersion.HTTP_1_1.getVersion() ? HttpServletResponse.SC_MOVED_TEMPORARILY : HttpServletResponse.SC_SEE_OTHER, location, consumeAll); } /** * Sends a response with a given redirection code. * * @param code the redirect status code * @param location the location to send in {@code Location} headers * @param consumeAll if True, consume any HTTP/1 request input before doing the redirection. If the input cannot * be consumed without blocking, then add a `Connection: close` header to the response. * @throws IOException if unable to send the redirect */ public void sendRedirect(int code, String location, boolean consumeAll) throws IOException { if (consumeAll) getHttpChannel().ensureConsumeAllOrNotPersistent(); if (!HttpStatus.isRedirectionWithLocation(code)) throw new IllegalArgumentException("Not a 3xx redirect code"); if (!isMutable()) return; if (location == null) throw new IllegalArgumentException(); if (!URIUtil.hasScheme(location)) { StringBuilder buf = _channel.getHttpConfiguration().isRelativeRedirectAllowed() ? new StringBuilder() : _channel.getRequest().getRootURL(); if (location.startsWith("/")) { // absolute in context location = URIUtil.normalizePathQuery(location); } else { // relative to request String path = _channel.getRequest().getRequestURI(); String parent = (path.endsWith("/")) ? path : URIUtil.parentPath(path); location = URIUtil.normalizePathQuery(URIUtil.addEncodedPaths(parent, location)); if (location != null && !location.startsWith("/")) buf.append('/'); } if (location == null) throw new IllegalStateException("path cannot be above root"); buf.append(location); location = buf.toString(); } resetBuffer(); setHeader(HttpHeader.LOCATION, location); setStatus(code); closeOutput(); } @Override public void sendRedirect(String location) throws IOException { sendRedirect(HttpServletResponse.SC_MOVED_TEMPORARILY, location); } @Override public void setDateHeader(String name, long date) { if (isMutable()) { HttpHeader header = HttpHeader.CACHE.get(name); if (header == null) _fields.putDate(name, date); else _fields.putDate(header, date); } } @Override public void addDateHeader(String name, long date) { if (isMutable()) _fields.addDateField(name, date); } public void setHeader(HttpHeader name, String value) { if (isMutable()) { if (HttpHeader.CONTENT_TYPE == name) setContentType(value); else { _fields.put(name, value); if (HttpHeader.CONTENT_LENGTH == name) { if (value == null) _contentLength = -1L; else _contentLength = Long.parseLong(value); } } } } @Override public void setHeader(String name, String value) { long biInt = _errorSentAndIncludes.get(); if (biInt != 0) { boolean errorSent = AtomicBiInteger.getHi(biInt) != 0; boolean including = AtomicBiInteger.getLo(biInt) > 0; if (!errorSent && including && name.startsWith(SET_INCLUDE_HEADER_PREFIX)) name = name.substring(SET_INCLUDE_HEADER_PREFIX.length()); else return; } if (HttpHeader.CONTENT_TYPE.is(name)) setContentType(value); else { _fields.put(name, value); if (HttpHeader.CONTENT_LENGTH.is(name)) { if (value == null) _contentLength = -1L; else _contentLength = Long.parseLong(value); } } } @Override public Collection getHeaderNames() { return _fields.getFieldNamesCollection(); } @Override public String getHeader(String name) { return _fields.get(name); } @Override public Collection getHeaders(String name) { Collection i = _fields.getValuesList(name); if (i == null) return Collections.emptyList(); return i; } @Override public void addHeader(String name, String value) { long biInt = _errorSentAndIncludes.get(); if (biInt != 0) { boolean errorSent = AtomicBiInteger.getHi(biInt) != 0; boolean including = AtomicBiInteger.getLo(biInt) > 0; if (!errorSent && including && name.startsWith(SET_INCLUDE_HEADER_PREFIX)) name = name.substring(SET_INCLUDE_HEADER_PREFIX.length()); else return; } if (HttpHeader.CONTENT_TYPE.is(name)) { setContentType(value); return; } if (HttpHeader.CONTENT_LENGTH.is(name)) { setHeader(name, value); return; } _fields.add(name, value); } @Override public void setIntHeader(String name, int value) { if (isMutable()) { _fields.put(name, value); if (HttpHeader.CONTENT_LENGTH.is(name)) _contentLength = value; } } @Override public void addIntHeader(String name, int value) { if (isMutable()) { _fields.add(name, Integer.toString(value)); if (HttpHeader.CONTENT_LENGTH.is(name)) _contentLength = value; } } @Override public void setStatus(int sc) { if (sc <= 0) throw new IllegalArgumentException(); if (isMutable()) { // Null the reason only if the status is different. This allows // a specific reason to be sent with setStatusWithReason followed by sendError. if (_status != sc) _reason = null; _status = sc; } } @Override @Deprecated(since = "Servlet API 2.1") public void setStatus(int sc, String message) { setStatusWithReason(sc, null); } public void setStatusWithReason(int sc, String message) { if (sc <= 0) throw new IllegalArgumentException(); if (isMutable()) { _status = sc; _reason = message; } } @Override public String getCharacterEncoding() { return getCharacterEncoding(false); } /** * Private utility method to get the character encoding. * A standard call to {@link #getCharacterEncoding()} should not change the Content-Type header. * But when {@link #getWriter()} is called we must decide what Content-Type to use, so this will allow an inferred * charset to be set in in the Content-Type. * @param setContentType if true allow the Content-Type header to be changed if character encoding was inferred or the default encoding was used. * @return the character encoding for this response. */ private String getCharacterEncoding(boolean setContentType) { // First try explicit char encoding. if (_characterEncoding != null) return _characterEncoding; String encoding; // Try charset from mime type. if (_mimeType != null && _mimeType.isCharsetAssumed()) return _mimeType.getCharsetString(); // Get MimeTypes from context MimeTypes mimeTypes = _channel.getRequest().getCoreRequest().getContext().getMimeTypes(); // Try charset assumed from content type (assumed charsets are not added to content type header). encoding = mimeTypes.getCharsetAssumedFromContentType(_contentType); if (encoding != null) return encoding; // Try char set inferred from content type. encoding = mimeTypes.getCharsetInferredFromContentType(_contentType); if (encoding != null) { if (setContentType) setCharacterEncoding(encoding, EncodingFrom.INFERRED); return encoding; } // Try any default char encoding for the context. ContextHandler.APIContext context = _channel.getRequest().getContext(); if (context != null) { encoding = context.getResponseCharacterEncoding(); if (encoding != null) { if (setContentType) setCharacterEncoding(encoding, EncodingFrom.DEFAULT); return encoding; } } // Fallback to last resort iso-8859-1. encoding = MimeTypes.ISO_8859_1; if (setContentType) setCharacterEncoding(encoding, EncodingFrom.DEFAULT); return encoding; } @Override public String getContentType() { return _contentType; } @Override public ServletOutputStream getOutputStream() throws IOException { if (_outputType == OutputType.WRITER) throw new IllegalStateException("WRITER"); _outputType = OutputType.STREAM; return _out; } public boolean isWriting() { return _outputType == OutputType.WRITER; } public boolean isStreaming() { return _outputType == OutputType.STREAM; } public boolean isWritingOrStreaming() { return isWriting() || isStreaming(); } @Override public PrintWriter getWriter() throws IOException { if (_outputType == OutputType.STREAM) throw new IllegalStateException("STREAM"); if (_outputType == OutputType.NONE) { String encoding = getCharacterEncoding(true); Locale locale = getLocale(); if (_writer != null && _writer.isFor(locale, encoding)) { _writer.reopen(); } else { // We must use an specialized Writer here as we rely on the non cached characters // in the writer implementation for flush and completion operations. WriteThroughWriter outputStreamWriter = WriteThroughWriter.newWriter(_out, encoding); _writer = new ResponseWriter(outputStreamWriter, locale, encoding); } // Set the output type at the end, because setCharacterEncoding() checks for it. _outputType = OutputType.WRITER; } return _writer; } @Override public void setContentLength(int len) { // Protect from setting after committed as default handling // of a servlet HEAD request ALWAYS sets _content length, even // if the getHandling committed the response! if (isCommitted() || !isMutable()) return; if (len > 0) { long written = _out.getWritten(); if (written > len) throw new IllegalArgumentException("setContentLength(" + len + ") when already written " + written); _contentLength = len; _fields.put(HttpHeader.CONTENT_LENGTH, len); if (isAllContentWritten(written)) { try { closeOutput(); } catch (IOException e) { throw new RuntimeIOException(e); } } } else if (len == 0) { long written = _out.getWritten(); if (written > 0) throw new IllegalArgumentException("setContentLength(0) when already written " + written); _contentLength = len; _fields.put(HttpFields.CONTENT_LENGTH_0); } else { _contentLength = len; _fields.remove(HttpHeader.CONTENT_LENGTH); } } public long getContentLength() { return _contentLength; } public boolean isAllContentWritten(long written) { return (_contentLength >= 0 && written >= _contentLength); } public boolean isContentComplete(long written) { return (_contentLength < 0 || written >= _contentLength); } public void closeOutput() throws IOException { if (_outputType == OutputType.WRITER) _writer.close(); else _out.close(); } /** * close the output * * @deprecated Use {@link #closeOutput()} */ @Deprecated public void completeOutput() throws IOException { closeOutput(); } public void completeOutput(Callback callback) { if (_outputType == OutputType.WRITER) _writer.markAsClosed(); _out.complete(callback); } public long getLongContentLength() { return _contentLength; } public void setLongContentLength(long len) { // Protect from setting after committed as default handling // of a servlet HEAD request ALWAYS sets _content length, even // if the getHandling committed the response! if (isCommitted() || !isMutable()) return; _contentLength = len; _fields.put(HttpHeader.CONTENT_LENGTH, len); } @Override public void setContentLengthLong(long length) { setLongContentLength(length); } @Override public void setCharacterEncoding(String encoding) { setCharacterEncoding(encoding, EncodingFrom.SET_CHARACTER_ENCODING); } private void setCharacterEncoding(String encoding, EncodingFrom from) { if (!isMutable() || isWriting() || isCommitted()) return; if (encoding == null) { _encodingFrom = EncodingFrom.NOT_SET; if (_characterEncoding != null) { _characterEncoding = null; if (_mimeType != null) { _mimeType = _mimeType.getBaseType(); _contentType = _mimeType.asString(); _fields.put(_mimeType.getContentTypeField()); } else if (_contentType != null) { _contentType = MimeTypes.getContentTypeWithoutCharset(_contentType); _fields.put(HttpHeader.CONTENT_TYPE, _contentType); } } } else { _encodingFrom = from; _characterEncoding = HttpGenerator.__STRICT ? encoding : MimeTypes.normalizeCharset(encoding); if (_mimeType != null) { _contentType = _mimeType.getBaseType().asString() + ";charset=" + _characterEncoding; _mimeType = MimeTypes.CACHE.get(_contentType); if (_mimeType == null || HttpGenerator.__STRICT) _fields.put(HttpHeader.CONTENT_TYPE, _contentType); else _fields.put(_mimeType.getContentTypeField()); } else if (_contentType != null) { _contentType = MimeTypes.getContentTypeWithoutCharset(_contentType) + ";charset=" + _characterEncoding; _fields.put(HttpHeader.CONTENT_TYPE, _contentType); } } } @Override public void setContentType(String contentType) { if (isCommitted() || !isMutable()) return; if (contentType == null) { if (isWriting() && _characterEncoding != null) throw new IllegalSelectorException(); if (_locale == null) _characterEncoding = null; _mimeType = null; _contentType = null; _fields.remove(HttpHeader.CONTENT_TYPE); } else { _contentType = contentType; _mimeType = MimeTypes.CACHE.get(contentType); String charset = MimeTypes.getCharsetFromContentType(contentType); if (charset == null && _mimeType != null && _mimeType.isCharsetAssumed()) charset = _mimeType.getCharsetString(); if (charset == null) { switch(_encodingFrom) { case NOT_SET: break; case DEFAULT: case INFERRED: case SET_CONTENT_TYPE: case SET_LOCALE: case SET_CHARACTER_ENCODING: { _contentType = contentType + ";charset=" + _characterEncoding; _mimeType = MimeTypes.CACHE.get(_contentType); break; } default: throw new IllegalStateException(_encodingFrom.toString()); } } else if (isWriting() && !charset.equalsIgnoreCase(_characterEncoding)) { // too late to change the character encoding; _contentType = MimeTypes.getContentTypeWithoutCharset(_contentType); if (_characterEncoding != null && (_mimeType == null || !_mimeType.isCharsetAssumed())) _contentType = _contentType + ";charset=" + _characterEncoding; _mimeType = MimeTypes.CACHE.get(_contentType); } else { _characterEncoding = charset; _encodingFrom = EncodingFrom.SET_CONTENT_TYPE; } if (HttpGenerator.__STRICT || _mimeType == null) _fields.put(HttpHeader.CONTENT_TYPE, _contentType); else { _contentType = _mimeType.asString(); _fields.put(_mimeType.getContentTypeField()); } } } @Override public void setBufferSize(int size) { if (isCommitted()) throw new IllegalStateException("cannot set buffer size after response is in committed state"); if (getContentCount() > 0) throw new IllegalStateException("cannot set buffer size after response has " + getContentCount() + " bytes already written"); if (size < __MIN_BUFFER_SIZE) size = __MIN_BUFFER_SIZE; _out.setBufferSize(size); } @Override public int getBufferSize() { return _out.getBufferSize(); } @Override public void flushBuffer() throws IOException { if (!_out.isClosed()) _out.flush(); } @Override public void reset() { _status = 200; _reason = null; _out.resetBuffer(); _outputType = OutputType.NONE; _contentLength = -1; _contentType = null; _mimeType = null; _characterEncoding = null; _encodingFrom = EncodingFrom.NOT_SET; _trailers = null; // Clear all response headers _fields.clear(); // recreate necessary connection related fields for (String value : _channel.getRequest().getHttpFields().getCSV(HttpHeader.CONNECTION, false)) { HttpHeaderValue cb = HttpHeaderValue.CACHE.get(value); if (cb != null) { switch(cb) { case CLOSE: _fields.put(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE.toString()); break; case KEEP_ALIVE: if (HttpVersion.HTTP_1_0.is(_channel.getRequest().getProtocol())) _fields.put(HttpHeader.CONNECTION, HttpHeaderValue.KEEP_ALIVE.toString()); break; case TE: _fields.put(HttpHeader.CONNECTION, HttpHeaderValue.TE.toString()); break; default: } } } // recreate session cookies ContextHandler.CoreContextRequest coreRequest = getHttpChannel().getCoreRequest(); Request request = getHttpChannel().getRequest(); HttpSession httpSession = request.getSession(false); if (httpSession != null && httpSession.isNew()) { SessionManager sessionManager = coreRequest.getSessionManager(); if (sessionManager != null) { HttpCookie cookie = sessionManager.getSessionCookie(coreRequest.getManagedSession(), request.isSecure()); if (cookie != null) addCookie(cookie); } } } public void resetContent() { _out.resetBuffer(); _outputType = OutputType.NONE; _contentLength = -1; _contentType = null; _mimeType = null; _characterEncoding = null; _encodingFrom = EncodingFrom.NOT_SET; // remove the content related response headers and keep all others for (Iterator i = getHttpFields().iterator(); i.hasNext(); ) { HttpField field = i.next(); if (field.getHeader() == null) continue; switch(field.getHeader()) { case CONTENT_TYPE: case CONTENT_LENGTH: case CONTENT_ENCODING: case CONTENT_LANGUAGE: case CONTENT_RANGE: case CONTENT_MD5: case CONTENT_LOCATION: case TRANSFER_ENCODING: case CACHE_CONTROL: case LAST_MODIFIED: case EXPIRES: case ETAG: case VARY: i.remove(); continue; default: } } } public void resetForForward() { resetBuffer(); _outputType = OutputType.NONE; } @Override public void resetBuffer() { _out.resetBuffer(); _out.reopen(); } public Supplier getTrailers() { return _trailers; } public void setTrailers(Supplier trailers) { _trailers = trailers; } @Override public Supplier> getTrailerFields() { if (_trailers instanceof HttpFieldsSupplier) return ((HttpFieldsSupplier) _trailers).getSupplier(); return null; } @Override public void setTrailerFields(Supplier> trailers) { if (isCommitted()) throw new IllegalStateException("Committed"); HttpVersion version = getHttpChannel().getRequest().getHttpVersion(); if (version == null || version.compareTo(HttpVersion.HTTP_1_1) < 0) throw new IllegalStateException("Trailers not supported in " + version); setTrailers(new HttpFieldsSupplier(trailers)); } protected MetaData.Response newResponseMetaData() { return new MetaData.Response(getStatus(), getReason(), _channel.getRequest().getHttpVersion(), _channel.getCoreResponse().getHeaders(), getLongContentLength(), getTrailers()); } @Override public boolean isCommitted() { // If we are in sendError state, we pretend to be committed if (_channel.isSendError()) return true; return _channel.isCommitted(); } @Override public void setLocale(Locale locale) { if (isCommitted() || !isMutable()) return; if (locale == null) { _locale = null; _fields.remove(HttpHeader.CONTENT_LANGUAGE); if (_encodingFrom == EncodingFrom.SET_LOCALE) setCharacterEncoding(null, EncodingFrom.NOT_SET); } else { _locale = locale; _fields.put(HttpHeader.CONTENT_LANGUAGE, StringUtil.replace(locale.toString(), '_', '-')); if (_outputType != OutputType.NONE) return; ContextHandler.APIContext context = _channel.getRequest().getContext(); if (context == null) return; String charset = context.getContextHandler().getLocaleEncoding(locale); if (!StringUtil.isEmpty(charset) && __localeOverride.contains(_encodingFrom)) setCharacterEncoding(charset, EncodingFrom.SET_LOCALE); } } @Override public Locale getLocale() { if (_locale == null) return Locale.getDefault(); return _locale; } @Override public int getStatus() { return _status; } public String getReason() { return _reason; } public HttpFields.Mutable getHttpFields() { return _fields; } public long getContentCount() { return _out.getWritten(); } @Override public String toString() { return String.format("%s %d %s%n%s", _channel.getRequest().getHttpVersion(), _status, _reason == null ? "" : _reason, _fields); } public void putHeaders(HttpContent content, long contentLength, boolean etag) { HttpField lm = content.getLastModified(); if (lm != null) _fields.put(lm); if (contentLength == USE_KNOWN_CONTENT_LENGTH) { _fields.put(content.getContentLength()); _contentLength = content.getContentLengthValue(); } else if (contentLength > NO_CONTENT_LENGTH) { _fields.put(HttpHeader.CONTENT_LENGTH, contentLength); _contentLength = contentLength; } HttpField ct = content.getContentType(); if (ct != null) { if (!__explicitCharset.contains(_encodingFrom)) { _fields.put(ct); _contentType = ct.getValue(); _characterEncoding = content.getCharacterEncoding(); _mimeType = content.getMimeType(); } else { setContentType(content.getContentTypeValue()); } } HttpField ce = content.getContentEncoding(); if (ce != null) _fields.put(ce); if (etag) { HttpField et = content.getETag(); if (et != null) _fields.put(et); } } public static void putHeaders(HttpServletResponse response, HttpContent content, long contentLength, boolean etag) { long lml = content.getResource().lastModified().toEpochMilli(); if (lml >= 0) response.setDateHeader(HttpHeader.LAST_MODIFIED.asString(), lml); if (contentLength == USE_KNOWN_CONTENT_LENGTH) contentLength = content.getContentLengthValue(); if (contentLength > NO_CONTENT_LENGTH) { if (contentLength < Integer.MAX_VALUE) response.setContentLength((int) contentLength); else response.setHeader(HttpHeader.CONTENT_LENGTH.asString(), Long.toString(contentLength)); } String ct = content.getContentTypeValue(); if (ct != null && response.getContentType() == null) response.setContentType(ct); String ce = content.getContentEncodingValue(); if (ce != null) response.setHeader(HttpHeader.CONTENT_ENCODING.asString(), ce); if (etag) { String et = content.getETagValue(); if (et != null) response.setHeader(HttpHeader.ETAG.asString(), et); } } public static HttpServletResponse unwrap(ServletResponse servletResponse) { if (servletResponse instanceof HttpServletResponseWrapper) { return (HttpServletResponseWrapper) servletResponse; } if (servletResponse instanceof ServletResponseWrapper) { return unwrap(((ServletResponseWrapper) servletResponse).getResponse()); } return (HttpServletResponse) servletResponse; } protected static class HttpFieldsSupplier implements Supplier { private final Supplier> _supplier; public HttpFieldsSupplier(Supplier> trailers) { _supplier = trailers; } @Override public HttpFields get() { Map t = _supplier.get(); if (t == null) return null; HttpFields.Mutable fields = HttpFields.build(); for (Map.Entry e : t.entrySet()) { fields.add(e.getKey(), e.getValue()); } return fields.asImmutable(); } public Supplier> getSupplier() { return _supplier; } } protected static class HttpCookieFacade implements HttpCookie { private final Cookie _cookie; private final String _comment; private final boolean _httpOnly; private final SameSite _sameSite; private final boolean _partitioned; public HttpCookieFacade(Cookie cookie) { _cookie = cookie; String comment = cookie.getComment(); // HttpOnly was supported as a comment in cookie flags before the Cookie implementation so need to check that. _httpOnly = cookie.isHttpOnly() || isHttpOnlyInComment(comment); _sameSite = getSameSiteFromComment(comment); _partitioned = isPartitionedInComment(comment); _comment = getCommentWithoutAttributes(comment); } @Override public String getComment() { return _comment; } @Override public String getDomain() { return _cookie.getDomain(); } @Override public long getMaxAge() { return _cookie.getMaxAge(); } @Override public String getPath() { return _cookie.getPath(); } @Override public boolean isSecure() { return _cookie.getSecure(); } @Override public String getName() { return _cookie.getName(); } @Override public String getValue() { return _cookie.getValue(); } @Override public int getVersion() { return _cookie.getVersion(); } @Override public SameSite getSameSite() { return _sameSite; } @Override public boolean isHttpOnly() { return _httpOnly; } @Override public boolean isPartitioned() { return _partitioned; } @Override public Map getAttributes() { Map attributes = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); attributes.put(COMMENT_ATTRIBUTE, _comment); attributes.put(DOMAIN_ATTRIBUTE, getDomain()); if (_httpOnly) attributes.put(HTTP_ONLY_ATTRIBUTE, Boolean.TRUE.toString()); if (_cookie.getMaxAge() >= 0) attributes.put(MAX_AGE_ATTRIBUTE, Long.toString(getMaxAge())); attributes.put(PATH_ATTRIBUTE, getPath()); if (_sameSite != null) attributes.put(SAME_SITE_ATTRIBUTE, _sameSite.getAttributeValue()); if (isSecure()) attributes.put(SECURE_ATTRIBUTE, Boolean.TRUE.toString()); if (isPartitioned()) attributes.put(PARTITIONED_ATTRIBUTE, Boolean.TRUE.toString()); return attributes; } @Override public int hashCode() { return HttpCookie.hashCode(this); } @Override public boolean equals(Object obj) { return HttpCookie.equals(this, obj); } @Override public String toString() { return HttpCookie.toString(this); } private static boolean isHttpOnlyInComment(String comment) { return comment != null && comment.contains(HTTP_ONLY_COMMENT); } protected static boolean isPartitionedInComment(String comment) { return comment != null && comment.contains(PARTITIONED_COMMENT); } protected static SameSite getSameSiteFromComment(String comment) { if (comment == null) return null; if (comment.contains(SAME_SITE_STRICT_COMMENT)) return SameSite.STRICT; if (comment.contains(SAME_SITE_LAX_COMMENT)) return SameSite.LAX; if (comment.contains(SAME_SITE_NONE_COMMENT)) return SameSite.NONE; return null; } protected static String getCommentWithoutAttributes(String comment) { if (comment == null) return null; String strippedComment = comment.trim(); strippedComment = StringUtil.strip(strippedComment, HTTP_ONLY_COMMENT); strippedComment = StringUtil.strip(strippedComment, PARTITIONED_COMMENT); strippedComment = StringUtil.strip(strippedComment, SAME_SITE_NONE_COMMENT); strippedComment = StringUtil.strip(strippedComment, SAME_SITE_LAX_COMMENT); strippedComment = StringUtil.strip(strippedComment, SAME_SITE_STRICT_COMMENT); return strippedComment.isEmpty() ? null : strippedComment; } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy