1 /*
2  * JBoss, Home of Professional Open Source.
3  * Copyright 2014 Red Hat, Inc., and individual contributors
4  * as indicated by the @author tags.
5  *
6  * Licensed under the Apache License, Version 2.0 (the "License");
7  * you may not use this file except in compliance with the License.
8  * You may obtain a copy of the License at
9  *
10  *     http://www.apache.org/licenses/LICENSE-2.0
11  *
12  *  Unless required by applicable law or agreed to in writing, software
13  *  distributed under the License is distributed on an "AS IS" BASIS,
14  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15  *  See the License for the specific language governing permissions and
16  *  limitations under the License.
17  */

18
19 package io.undertow.servlet.spec;
20
21 import java.io.IOException;
22 import java.io.PrintWriter;
23 import java.io.UnsupportedEncodingException;
24 import java.net.MalformedURLException;
25 import java.net.URL;
26 import java.nio.charset.StandardCharsets;
27 import java.util.ArrayList;
28 import java.util.Collection;
29 import java.util.Collections;
30 import java.util.Date;
31 import java.util.HashSet;
32 import java.util.Locale;
33 import java.util.Map;
34 import java.util.Set;
35 import java.util.TreeMap;
36 import java.util.function.Supplier;
37
38 import javax.servlet.ServletException;
39 import javax.servlet.ServletOutputStream;
40 import javax.servlet.SessionTrackingMode;
41 import javax.servlet.http.HttpServletResponse;
42 import javax.servlet.http.HttpSession;
43
44 import io.undertow.UndertowLogger;
45 import io.undertow.server.Connectors;
46 import io.undertow.server.HttpServerExchange;
47 import io.undertow.server.ResponseCommitListener;
48 import io.undertow.server.handlers.Cookie;
49 import io.undertow.server.protocol.http.HttpAttachments;
50 import io.undertow.servlet.UndertowServletMessages;
51 import io.undertow.servlet.handlers.ServletRequestContext;
52 import io.undertow.util.CanonicalPathUtils;
53 import io.undertow.util.DateUtils;
54 import io.undertow.util.HeaderMap;
55 import io.undertow.util.HeaderValues;
56 import io.undertow.util.Headers;
57 import io.undertow.util.HttpString;
58 import io.undertow.util.Protocols;
59 import io.undertow.util.RedirectBuilder;
60 import io.undertow.util.StatusCodes;
61
62 import static io.undertow.util.URLUtils.isAbsoluteUrl;
63
64
65 /**
66  * @author Stuart Douglas
67  */

68 public final class HttpServletResponseImpl implements HttpServletResponse {
69
70     private final HttpServerExchange exchange;
71     private final ServletContextImpl originalServletContext;
72     private volatile ServletContextImpl servletContext;
73
74     private ServletOutputStreamImpl servletOutputStream;
75     private ResponseState responseState = ResponseState.NONE;
76     private PrintWriter writer;
77     private Integer bufferSize;
78     private long contentLength = -1;
79     private boolean insideInclude = false;
80     private Locale locale;
81     private boolean responseDone = false;
82
83     private boolean ignoredFlushPerformed = false;
84
85     private boolean treatAsCommitted = false;
86
87     private boolean charsetSet = false//if a content type has been set either implicitly or implicitly
88     private String contentType;
89     private String charset;
90     private Supplier<Map<String, String>> trailerSupplier;
91     // a map of cookie name -> a map of (cookie path + cookie domain) -> cookie
92     private Map<String, Map<String, Cookie>> duplicateCookies;
93
94     public HttpServletResponseImpl(final HttpServerExchange exchange, final ServletContextImpl servletContext) {
95         this.exchange = exchange;
96         this.servletContext = servletContext;
97         this.originalServletContext = servletContext;
98     }
99
100     public HttpServerExchange getExchange() {
101         return exchange;
102     }
103
104     @Override
105     public void addCookie(final javax.servlet.http.Cookie cookie) {
106         if (insideInclude) {
107             return;
108         }
109         final ServletCookieAdaptor servletCookieAdaptor = new ServletCookieAdaptor(cookie);
110         if (cookie.getVersion() == 0) {
111             servletCookieAdaptor.setVersion(servletContext.getDeployment().getDeploymentInfo().getDefaultCookieVersion());
112         }
113         // test for duplicate entry
114         if (exchange.getResponseCookies().containsKey(servletCookieAdaptor.getName())) {
115             final String cookieName = servletCookieAdaptor.getName();
116             final String path = servletCookieAdaptor.getPath();
117             final String domain = servletCookieAdaptor.getDomain();
118             final Cookie otherCookie = exchange.getResponseCookies().get(cookieName);
119             final String otherCookiePath = otherCookie.getPath();
120             final String otherCookieDomain = otherCookie.getDomain();
121             // if both cookies have same path, name, and domain, overwrite previous cookie
122             if ((path == otherCookiePath || (path != null && path.equals(otherCookiePath))) &&
123                     (domain == otherCookieDomain || (domain != null && domain.equals(otherCookieDomain)))) {
124                 exchange.setResponseCookie(servletCookieAdaptor);
125             }
126             // else, create a duplicate cookie entry
127             else {
128                 final Map<String, Cookie> cookiesByPathPlusDomain;
129                 if (duplicateCookies == null) {
130                     duplicateCookies = new TreeMap<>();
131                     exchange.addResponseCommitListener(
132                             new DuplicateCookieCommitListener());
133                 }
134                 if (duplicateCookies.containsKey(cookieName)) {
135                     cookiesByPathPlusDomain = duplicateCookies.get(cookieName);
136                 } else {
137                     cookiesByPathPlusDomain = new TreeMap<>();
138                     duplicateCookies.put(cookieName, cookiesByPathPlusDomain);
139                 }
140                 String pathPlusDomain = (otherCookiePath == null ? "null" : otherCookiePath) +
141                         (otherCookieDomain == null"null" : otherCookieDomain);
142                 cookiesByPathPlusDomain.put(pathPlusDomain, otherCookie);
143             }
144         }
145         exchange.setResponseCookie(servletCookieAdaptor);
146     }
147
148     @Override
149     public boolean containsHeader(final String name) {
150         return exchange.getResponseHeaders().contains(name);
151     }
152
153     @Override
154     public String encodeUrl(final String url) {
155         return encodeURL(url);
156     }
157
158     @Override
159     public String encodeRedirectUrl(final String url) {
160         return encodeRedirectURL(url);
161     }
162
163     @Override
164     public void sendError(final int sc, final String msg) throws IOException {
165         if(insideInclude) {
166             //not 100% sure this is the correct action
167             return;
168         }
169         ServletRequestContext src = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY);
170         if (responseStarted()) {
171             if(src.getErrorCode() > 0) {
172                 return//error already set
173             }
174             throw UndertowServletMessages.MESSAGES.responseAlreadyCommited();
175         }
176         if(servletContext.getDeployment().getDeploymentInfo().isSendCustomReasonPhraseOnError()) {
177             exchange.setReasonPhrase(msg);
178         }
179         writer = null;
180         responseState = ResponseState.NONE;
181         exchange.setStatusCode(sc);
182         if(src.isRunningInsideHandler()) {
183             //all we do is set the error on the context, we handle it when the request is returned
184             treatAsCommitted = true;
185             src.setError(sc, msg);
186         } else {
187             //if the src is null there is no outer handler, as we are in an asnc request
188             doErrorDispatch(sc, msg);
189         }
190     }
191
192     public void doErrorDispatch(int sc, String error) throws IOException {
193         writer = null;
194         responseState = ResponseState.NONE;
195         resetBuffer();
196         treatAsCommitted = false;
197         final String location = servletContext.getDeployment().getErrorPages().getErrorLocation(sc);
198         if (location != null) {
199             RequestDispatcherImpl requestDispatcher = new RequestDispatcherImpl(location, servletContext);
200             final ServletRequestContext servletRequestContext = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY);
201             try {
202                 requestDispatcher.error(servletRequestContext, servletRequestContext.getServletRequest(), servletRequestContext.getServletResponse(), exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY).getCurrentServlet().getManagedServlet().getServletInfo().getName(), error);
203             } catch (ServletException e) {
204                 throw new RuntimeException(e);
205             }
206         } else if (error != null) {
207             setContentType("text/html");
208             setCharacterEncoding("UTF-8");
209             if(servletContext.getDeployment().getDeploymentInfo().isEscapeErrorMessage()) {
210                 getWriter().write("<html><head><title>Error</title></head><body>" + escapeHtml(error) + "</body></html>");
211             } else {
212                 getWriter().write("<html><head><title>Error</title></head><body>" + error + "</body></html>");
213             }
214             getWriter().close();
215         }
216         responseDone();
217     }
218
219     @Override
220     public void sendError(final int sc) throws IOException {
221         sendError(sc, StatusCodes.getReason(sc));
222     }
223
224     @Override
225     public void sendRedirect(final String location) throws IOException {
226         if (responseStarted()) {
227             throw UndertowServletMessages.MESSAGES.responseAlreadyCommited();
228         }
229         resetBuffer();
230         setStatus(StatusCodes.FOUND);
231         String realPath;
232         if (isAbsoluteUrl(location)) {//absolute url
233             exchange.getResponseHeaders().put(Headers.LOCATION, location);
234         } else {
235             if (location.startsWith("/")) {
236                 realPath = location;
237             } else {
238                 String current = exchange.getRelativePath();
239                 int lastSlash = current.lastIndexOf("/");
240                 if (lastSlash != -1) {
241                     current = current.substring(0, lastSlash + 1);
242                 }
243                 realPath = CanonicalPathUtils.canonicalize(servletContext.getContextPath() + current + location);
244             }
245             String loc = exchange.getRequestScheme() + "://" + exchange.getHostAndPort() + realPath;
246             exchange.getResponseHeaders().put(Headers.LOCATION, loc);
247         }
248         responseDone();
249     }
250
251     @Override
252     public void setDateHeader(final String name, final long date) {
253         setHeader(name, DateUtils.toDateString(new Date(date)));
254     }
255
256     @Override
257     public void addDateHeader(final String name, final long date) {
258         addHeader(name, DateUtils.toDateString(new Date(date)));
259     }
260
261     @Override
262     public void setHeader(final String name, final String value) {
263         if(name == null) {
264             throw UndertowServletMessages.MESSAGES.headerNameWasNull();
265         }
266         setHeader(HttpString.tryFromString(name), value);
267     }
268
269
270     public void setHeader(final HttpString name, final String value) {
271         if(name == null) {
272             throw UndertowServletMessages.MESSAGES.headerNameWasNull();
273         }
274         if (insideInclude || ignoredFlushPerformed) {
275             return;
276         }
277         if(name.equals(Headers.CONTENT_TYPE)) {
278             setContentType(value);
279         } else {
280             exchange.getResponseHeaders().put(name, value);
281         }
282     }
283
284     @Override
285     public void addHeader(final String name, final String value) {
286         if(name == null) {
287             throw UndertowServletMessages.MESSAGES.headerNameWasNull();
288         }
289         addHeader(HttpString.tryFromString(name), value);
290     }
291
292     public void addHeader(final HttpString name, final String value) {
293         if(name == null) {
294             throw UndertowServletMessages.MESSAGES.headerNameWasNull();
295         }
296         if (insideInclude || ignoredFlushPerformed || treatAsCommitted) {
297             return;
298         }
299         if(name.equals(Headers.CONTENT_TYPE) && !exchange.getResponseHeaders().contains(Headers.CONTENT_TYPE)) {
300             setContentType(value);
301         } else {
302             exchange.getResponseHeaders().add(name, value);
303         }
304     }
305
306     @Override
307     public void setIntHeader(final String name, final int value) {
308         setHeader(name, Integer.toString(value));
309     }
310
311     @Override
312     public void addIntHeader(final String name, final int value) {
313         addHeader(name, Integer.toString(value));
314     }
315
316     @Override
317     public void setStatus(final int sc) {
318         if (insideInclude || treatAsCommitted) {
319             return;
320         }
321         if (responseStarted()) {
322             return;
323         }
324         exchange.setStatusCode(sc);
325     }
326
327     @Override
328     public void setStatus(final int sc, final String sm) {
329         setStatus(sc);
330         if(!insideInclude && servletContext.getDeployment().getDeploymentInfo().isSendCustomReasonPhraseOnError()) {
331             exchange.setReasonPhrase(sm);
332         }
333     }
334
335     @Override
336     public int getStatus() {
337         return exchange.getStatusCode();
338     }
339
340     @Override
341     public String getHeader(final String name) {
342         return exchange.getResponseHeaders().getFirst(name);
343     }
344
345     @Override
346     public Collection<String> getHeaders(final String name) {
347         HeaderValues headers = exchange.getResponseHeaders().get(name);
348         if(headers == null) {
349             return Collections.emptySet();
350         }
351         return new ArrayList<>(headers);
352     }
353
354     @Override
355     public Collection<String> getHeaderNames() {
356         final Set<String> headers = new HashSet<>();
357         for (final HttpString i : exchange.getResponseHeaders().getHeaderNames()) {
358             headers.add(i.toString());
359         }
360         return headers;
361     }
362
363     @Override
364     public String getCharacterEncoding() {
365         if (charset != null) {
366             return charset;
367         }
368         // first check, web-app context level default response encoding
369         if (servletContext.getDeployment().getDeploymentInfo().getDefaultResponseEncoding() != null) {
370             return servletContext.getDeployment().getDeploymentInfo().getDefaultResponseEncoding();
371         }
372         // now check the container level default encoding
373         if (servletContext.getDeployment().getDeploymentInfo().getDefaultEncoding() != null) {
374             return servletContext.getDeployment().getDeploymentInfo().getDefaultEncoding();
375         }
376         // if no explicit encoding is specified, this method is supposed to return ISO-8859-1 as per the
377         // expectation of this API
378         return StandardCharsets.ISO_8859_1.name();
379     }
380
381     @Override
382     public String getContentType() {
383         if (contentType != null) {
384             if (charsetSet) {
385                 return contentType + ";charset=" + getCharacterEncoding();
386             } else {
387                 return contentType;
388             }
389         }
390         return null;
391     }
392
393     @Override
394     public ServletOutputStream getOutputStream() {
395         if (responseState == ResponseState.WRITER) {
396             throw UndertowServletMessages.MESSAGES.getWriterAlreadyCalled();
397         }
398         responseState = ResponseState.STREAM;
399         createOutputStream();
400         return servletOutputStream;
401     }
402
403     @Override
404     public PrintWriter getWriter() throws IOException {
405         if (writer == null) {
406             if (!charsetSet) {
407                 //servet 5.5
408                 setCharacterEncoding(getCharacterEncoding());
409             }
410             if (responseState == ResponseState.STREAM) {
411                 throw UndertowServletMessages.MESSAGES.getOutputStreamAlreadyCalled();
412             }
413             responseState = ResponseState.WRITER;
414             createOutputStream();
415             final ServletPrintWriter servletPrintWriter = new ServletPrintWriter(servletOutputStream, getCharacterEncoding());
416             writer = ServletPrintWriterDelegate.newInstance(servletPrintWriter);
417         }
418         return writer;
419     }
420
421     private void createOutputStream() {
422         if (servletOutputStream == null) {
423             if (bufferSize == null) {
424                 servletOutputStream = new ServletOutputStreamImpl(exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY));
425             } else {
426                 servletOutputStream = new ServletOutputStreamImpl(exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY), bufferSize);
427             }
428         }
429     }
430
431     @Override
432     public void setCharacterEncoding(final String charset) {
433         if (insideInclude || responseStarted() || writer != null || isCommitted()) {
434             return;
435         }
436         charsetSet = charset != null;
437         this.charset = charset;
438         if (contentType != null) {
439             exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, getContentType());
440         }
441     }
442
443     @Override
444     public void setContentLength(final int len) {
445         setContentLengthLong((long) len);
446     }
447
448     @Override
449     public void setContentLengthLong(final long len) {
450         if (insideInclude || responseStarted()) {
451             return;
452         }
453         if(len >= 0) {
454             exchange.getResponseHeaders().put(Headers.CONTENT_LENGTH, Long.toString(len));
455         } else {
456             exchange.getResponseHeaders().remove(Headers.CONTENT_LENGTH);
457         }
458         this.contentLength = len;
459     }
460
461     boolean isIgnoredFlushPerformed() {
462         return ignoredFlushPerformed;
463     }
464
465     void setIgnoredFlushPerformed(boolean ignoredFlushPerformed) {
466         this.ignoredFlushPerformed = ignoredFlushPerformed;
467     }
468
469     private boolean responseStarted() {
470         return exchange.isResponseStarted() || ignoredFlushPerformed || treatAsCommitted;
471     }
472
473     @Override
474     public void setContentType(final String type) {
475         if (type == null || insideInclude || responseStarted()) {
476             return;
477         }
478         ContentTypeInfo ct = servletContext.parseContentType(type);
479         contentType = ct.getContentType();
480         boolean useCharset = false;
481         if(ct.getCharset() != null && writer == null && !isCommitted()) {
482             charset = ct.getCharset();
483             charsetSet = true;
484             useCharset = true;
485         }
486         if(useCharset || !charsetSet) {
487             exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, ct.getHeader());
488         } else if(ct.getCharset() == null) {
489             exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, ct.getHeader() + "; charset=" + charset);
490         }else {
491             exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, ct.getContentType() + "; charset=" + charset);
492         }
493     }
494
495     @Override
496     public void setBufferSize(final int size) {
497         if (servletOutputStream != null) {
498             servletOutputStream.setBufferSize(size);
499         }
500         this.bufferSize = size;
501     }
502
503     @Override
504     public int getBufferSize() {
505         if (bufferSize == null) {
506             return exchange.getConnection().getBufferSize();
507         }
508         return bufferSize;
509     }
510
511     @Override
512     public void flushBuffer() throws IOException {
513         if (writer != null) {
514             writer.flush();
515         } else if (servletOutputStream != null) {
516             servletOutputStream.flush();
517         } else {
518             createOutputStream();
519             servletOutputStream.flush();
520         }
521     }
522
523     public void closeStreamAndWriter() throws IOException {
524         if(treatAsCommitted) {
525             return;
526         }
527         if (writer != null) {
528             writer.close();
529         } else {
530             if (servletOutputStream == null) {
531                 createOutputStream();
532             }
533             //close also flushes
534             servletOutputStream.close();
535         }
536     }
537
538     public void freeResources() throws IOException {
539         if(writer != null) {
540             writer.close();
541         }
542         if(servletOutputStream != null) {
543             servletOutputStream.close();
544         }
545     }
546
547     @Override
548     public void resetBuffer() {
549         if (servletOutputStream != null) {
550             servletOutputStream.resetBuffer();
551         }
552         if (writer != null) {
553             final ServletPrintWriter servletPrintWriter;
554             try {
555                 servletPrintWriter = new ServletPrintWriter(servletOutputStream, getCharacterEncoding());
556             writer = ServletPrintWriterDelegate.newInstance(servletPrintWriter);
557             } catch (UnsupportedEncodingException e) {
558                 throw new RuntimeException(e); //should never happen
559             }
560         }
561     }
562
563     @Override
564     public boolean isCommitted() {
565         return responseStarted();
566     }
567
568     @Override
569     public void reset() {
570         if (servletOutputStream != null) {
571             servletOutputStream.resetBuffer();
572         }
573         writer = null;
574         responseState = ResponseState.NONE;
575         exchange.getResponseHeaders().clear();
576         exchange.setStatusCode(StatusCodes.OK);
577         treatAsCommitted = false;
578     }
579
580     @Override
581     public void setLocale(final Locale loc) {
582         if (insideInclude || responseStarted()) {
583             return;
584         }
585         this.locale = loc;
586         exchange.getResponseHeaders().put(Headers.CONTENT_LANGUAGE, loc.getLanguage() + "-" + loc.getCountry());
587         if (!charsetSet && writer == null) {
588             final Map<String, String> localeCharsetMapping = servletContext.getDeployment().getDeploymentInfo().getLocaleCharsetMapping();
589             // Match full language_country_variant first, then language_country,
590             // then language only
591             String charset = localeCharsetMapping.get(locale.toString());
592             if (charset == null) {
593                 charset = localeCharsetMapping.get(locale.getLanguage() + "_"
594                         + locale.getCountry());
595                 if (charset == null) {
596                     charset = localeCharsetMapping.get(locale.getLanguage());
597                 }
598             }
599             if (charset != null) {
600                 this.charset = charset;
601                 if (contentType != null) {
602                     exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, getContentType());
603                 }
604             }
605         }
606
607     }
608
609     @Override
610     public Locale getLocale() {
611         if (locale != null) {
612             return locale;
613         }
614         return Locale.getDefault();
615     }
616
617     public void responseDone() {
618         if (responseDone || treatAsCommitted) {
619             return;
620         }
621         responseDone = true;
622         try {
623             closeStreamAndWriter();
624         } catch (IOException e) {
625             UndertowLogger.REQUEST_IO_LOGGER.ioException(e);
626         } finally {
627             servletContext.updateSessionAccessTime(exchange);
628         }
629     }
630
631     public boolean isInsideInclude() {
632         return insideInclude;
633     }
634
635     public void setInsideInclude(final boolean insideInclude) {
636         this.insideInclude = insideInclude;
637     }
638
639     public void setServletContext(final ServletContextImpl servletContext) {
640         this.servletContext = servletContext;
641     }
642
643     public ServletContextImpl getServletContext() {
644         return servletContext;
645     }
646
647     public String encodeURL(String url) {
648         String absolute = toAbsolute(url);
649         if (isEncodeable(absolute)) {
650             // W3c spec clearly said
651             if (url.equalsIgnoreCase("")) {
652                 url = absolute;
653             }
654             return originalServletContext.getSessionConfig().rewriteUrl(url, servletContext.getSession(originalServletContext, exchange, true).getId());
655         } else {
656             return (url);
657         }
658
659     }
660
661     /**
662      * Encode the session identifier associated with this response
663      * into the specified redirect URL, if necessary.
664      *
665      * @param url URL to be encoded
666      */

667     public String encodeRedirectURL(String url) {
668         if (isEncodeable(toAbsolute(url))) {
669             return originalServletContext.getSessionConfig().rewriteUrl(url, servletContext.getSession(originalServletContext, exchange, true).getId());
670         } else {
671             return url;
672         }
673     }
674
675     /**
676      * Convert (if necessary) and return the absolute URL that represents the
677      * resource referenced by this possibly relative URL.  If this URL is
678      * already absolute, return it unchanged.
679      *
680      * @param location URL to be (possibly) converted and then returned
681      * @throws IllegalArgumentException if a MalformedURLException is
682      *                                  thrown when converting the relative URL to an absolute one
683      */

684     private String toAbsolute(String location) {
685
686         if (location == null) {
687             return location;
688         }
689
690         boolean leadingSlash = location.startsWith("/");
691
692         if (leadingSlash || !hasScheme(location)) {
693             return RedirectBuilder.redirect(exchange, location, false);
694         } else {
695             return location;
696         }
697
698     }
699
700     /**
701      * Determine if a URI string has a <code>scheme</code> component.
702      */

703     private boolean hasScheme(String uri) {
704         int len = uri.length();
705         for (int i = 0; i < len; i++) {
706             char c = uri.charAt(i);
707             if (c == ':') {
708                 return i > 0;
709             } else if (!Character.isLetterOrDigit(c) &&
710                     (c != '+' && c != '-' && c != '.')) {
711                 return false;
712             }
713         }
714         return false;
715     }
716
717     /**
718      * Return <code>true</code> if the specified URL should be encoded with
719      * a session identifier.  This will be true if all of the following
720      * conditions are met:
721      * <ul>
722      * <li>The request we are responding to asked for a valid session
723      * <li>The requested session ID was not received via a cookie
724      * <li>The specified URL points back to somewhere within the web
725      * application that is responding to this request
726      * </ul>
727      *
728      * @param location Absolute URL to be validated
729      */

730     private boolean isEncodeable(final String location) {
731
732         if (location == null)
733             return (false);
734
735         // Is this an intra-document reference?
736         if (location.startsWith("#"))
737             return (false);
738
739         // Are we in a valid session that is not using cookies?
740         final HttpServletRequestImpl hreq = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY).getOriginalRequest();
741
742         // Is URL encoding permitted
743         if (!originalServletContext.getEffectiveSessionTrackingModes().contains(SessionTrackingMode.URL)) {
744             return false;
745         }
746
747         final HttpSession session = hreq.getSession(false);
748         if (session == null) {
749             return false;
750         } else if(hreq.isRequestedSessionIdFromCookie()) {
751             return false;
752         } else if (!hreq.isRequestedSessionIdFromURL() && !session.isNew()) {
753             return false;
754         }
755
756         return doIsEncodeable(hreq, session, location);
757     }
758
759     private boolean doIsEncodeable(HttpServletRequestImpl hreq, HttpSession session,
760                                    String location) {
761         // Is this a valid absolute URL?
762         URL url = null;
763         try {
764             url = new URL(location);
765         } catch (MalformedURLException e) {
766             return false;
767         }
768
769         // Does this URL match down to (and including) the context path?
770         if (!hreq.getScheme().equalsIgnoreCase(url.getProtocol())) {
771             return false;
772         }
773         if (!hreq.getServerName().equalsIgnoreCase(url.getHost())) {
774             return false;
775         }
776         int serverPort = hreq.getServerPort();
777         if (serverPort == -1) {
778             if ("https".equals(hreq.getScheme())) {
779                 serverPort = 443;
780             } else {
781                 serverPort = 80;
782             }
783         }
784         int urlPort = url.getPort();
785         if (urlPort == -1) {
786             if ("https".equals(url.getProtocol())) {
787                 urlPort = 443;
788             } else {
789                 urlPort = 80;
790             }
791         }
792         if (serverPort != urlPort) {
793             return false;
794         }
795
796         String file = url.getFile();
797         if (file == null) {
798             return false;
799         }
800         String tok = originalServletContext.getSessionCookieConfig().getName().toLowerCase() + "=" + session.getId();
801         if (file.contains(tok)) {
802             return false;
803         }
804
805         // This URL belongs to our web application, so it is encodeable
806         return true;
807
808     }
809
810     public long getContentLength() {
811         return contentLength;
812     }
813
814     public enum ResponseState {
815         NONE,
816         STREAM,
817         WRITER
818     }
819
820     private static String escapeHtml(String msg) {
821         return msg.replace("<""&lt;").replace(">""&gt;").replace("&""&amp;");
822     }
823
824     public boolean isTreatAsCommitted() {
825         return treatAsCommitted;
826     }
827
828     @Override
829     public void setTrailerFields(Supplier<Map<String, String>> supplier) {
830         if(exchange.isResponseStarted()) {
831             throw UndertowServletMessages.MESSAGES.responseAlreadyCommited();
832         }
833         if(exchange.getProtocol() == Protocols.HTTP_1_0) {
834             throw UndertowServletMessages.MESSAGES.trailersNotSupported("HTTP/1.0 request");
835         } else if(exchange.getProtocol() == Protocols.HTTP_1_1) {
836             if(exchange.getResponseHeaders().contains(Headers.CONTENT_LENGTH)) {
837                 throw UndertowServletMessages.MESSAGES.trailersNotSupported("not chunked");
838             }
839         }
840         this.trailerSupplier = supplier;
841         exchange.putAttachment(HttpAttachments.RESPONSE_TRAILER_SUPPLIER, () -> {
842             HeaderMap trailers = new HeaderMap();
843             Map<String, String> map = supplier.get();
844             for(Map.Entry<String, String> e : map.entrySet()) {
845                 trailers.put(new HttpString(e.getKey()), e.getValue());
846             }
847             return trailers;
848         });
849     }
850
851     @Override
852     public Supplier<Map<String, String>> getTrailerFields() {
853         return trailerSupplier;
854     }
855
856     private class DuplicateCookieCommitListener implements
857             ResponseCommitListener {
858
859         @Override
860         public void beforeCommit(HttpServerExchange exchange) {
861             for (Map.Entry<String, Map<String, Cookie>> duplicateCookiesEntry : duplicateCookies.entrySet()) {
862                 for (Map.Entry<String, Cookie> cookiesByPathEntry : duplicateCookiesEntry.getValue().entrySet()) {
863                     Connectors.addCookie(exchange, cookiesByPathEntry.getValue());
864                 }
865             }
866         }
867     }
868 }
869