001/**
002 *
003 * Copyright (c) 2014, the Railo Company Ltd. All rights reserved.
004 *
005 * This library is free software; you can redistribute it and/or
006 * modify it under the terms of the GNU Lesser General Public
007 * License as published by the Free Software Foundation; either 
008 * version 2.1 of the License, or (at your option) any later version.
009 * 
010 * This library is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013 * Lesser General Public License for more details.
014 * 
015 * You should have received a copy of the GNU Lesser General Public 
016 * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
017 * 
018 **/
019package lucee.runtime.net.http;
020
021import java.io.BufferedReader;
022import java.io.IOException;
023import java.io.InputStream;
024import java.io.Serializable;
025import java.io.UnsupportedEncodingException;
026import java.security.Principal;
027import java.util.Enumeration;
028import java.util.HashMap;
029import java.util.Iterator;
030import java.util.LinkedList;
031import java.util.Locale;
032import java.util.Map;
033
034import javax.servlet.RequestDispatcher;
035import javax.servlet.ServletInputStream;
036import javax.servlet.http.Cookie;
037import javax.servlet.http.HttpServletRequest;
038import javax.servlet.http.HttpSession;
039
040import lucee.commons.io.IOUtil;
041import lucee.commons.lang.ExceptionUtil;
042import lucee.commons.lang.StringUtil;
043import lucee.commons.net.URLItem;
044import lucee.runtime.PageContext;
045import lucee.runtime.PageContextImpl;
046import lucee.runtime.config.Config;
047import lucee.runtime.engine.ThreadLocalPageContext;
048import lucee.runtime.op.Caster;
049import lucee.runtime.op.date.DateCaster;
050import lucee.runtime.type.Collection;
051import lucee.runtime.type.KeyImpl;
052import lucee.runtime.type.dt.DateTime;
053import lucee.runtime.type.scope.Form;
054import lucee.runtime.type.scope.FormImpl;
055import lucee.runtime.type.scope.URL;
056import lucee.runtime.type.scope.URLImpl;
057import lucee.runtime.type.scope.UrlFormImpl;
058import lucee.runtime.type.scope.util.ScopeUtil;
059import lucee.runtime.type.util.ArrayUtil;
060import lucee.runtime.util.EnumerationWrapper;
061
062/**
063 * extends a existing {@link HttpServletRequest} with the possibility to reread the input as many you want.
064 */
065public final class HTTPServletRequestWrap implements HttpServletRequest,Serializable {
066
067
068        private boolean firstRead=true;
069        private byte[] barr;
070        private static final int MIN_STORAGE_SIZE=1*1024*1024;
071        private static final int MAX_STORAGE_SIZE=50*1024*1024;
072        private static final int SPACIG=1024*1024;
073        
074        private String servlet_path;
075        private String request_uri;
076        private String context_path;
077        private String path_info;
078        private String query_string;
079        private boolean disconnected;
080        private final HttpServletRequest req;
081        
082        private static class DisconnectData {
083                private Map<String, Object> attributes;
084                private String authType;
085                private Cookie[] cookies;
086                private Map<Collection.Key,LinkedList<String>> headers;// this is a Pait List because there could by multiple entries with the same name
087                private String method;
088                private String pathTranslated;
089                private String remoteUser;
090                private String requestedSessionId;
091                private boolean requestedSessionIdFromCookie;
092                //private Request _request;
093                private boolean requestedSessionIdFromURL;
094                private boolean secure;
095                private boolean requestedSessionIdValid;
096                private String characterEncoding;
097                private int contentLength;
098                private String contentType;
099                private int serverPort;
100                private String serverName;
101                private String scheme;
102                private String remoteHost;
103                private String remoteAddr;
104                private String protocol;
105                private Locale locale;
106                private HttpSession session;
107                private Principal userPrincipal;
108        }
109        DisconnectData disconnectData;
110        
111        /**
112         * Constructor of the class
113         * @param req 
114         * @param max how many is possible to re read
115         */
116        public HTTPServletRequestWrap(HttpServletRequest req) {
117                this.req=pure(req);
118                if((servlet_path=attrAsString("javax.servlet.include.servlet_path"))!=null){
119                        request_uri=attrAsString("javax.servlet.include.request_uri");
120                        context_path=attrAsString("javax.servlet.include.context_path");
121                        path_info=attrAsString("javax.servlet.include.path_info");
122                        query_string = attrAsString("javax.servlet.include.query_string");
123                }
124                else {
125                        servlet_path=req.getServletPath();
126                        request_uri=req.getRequestURI();
127                        context_path=req.getContextPath();
128                        path_info=req.getPathInfo();
129                        query_string = req.getQueryString();
130                }
131        }
132        
133        private String attrAsString(String key) {
134                Object res = getAttribute(key);
135                if(res==null) return null;
136                return res.toString();
137        }
138        
139        public static HttpServletRequest pure(HttpServletRequest req) {
140                HttpServletRequest req2;
141                while(req instanceof HTTPServletRequestWrap){
142                        req2 =  ((HTTPServletRequestWrap)req).getOriginalRequest();
143                        if(req2==req) break;
144                        req=req2;
145                }
146                return req;
147        }
148
149        @Override
150        public String getContextPath() {
151                return context_path;
152        }
153        
154        @Override
155        public String getPathInfo() {
156                return path_info;
157        }
158        
159        @Override
160        public StringBuffer getRequestURL() {
161                return new StringBuffer(isSecure()?"https":"http").
162                        append("://").
163                        append(getServerName()).
164                        append(':').
165                        append(getServerPort()).
166                        append(request_uri.startsWith("/")?request_uri:"/"+request_uri);
167        }
168        
169        @Override
170        public String getQueryString() {
171                return query_string;
172        }
173        @Override
174        public String getRequestURI() {
175                return request_uri;
176        }
177        
178        @Override
179        public String getServletPath() {
180                return servlet_path;
181        }
182        
183        @Override
184        public RequestDispatcher getRequestDispatcher(String relpath) {
185                return new RequestDispatcherWrap(this,relpath);
186        }
187        
188        public RequestDispatcher getOriginalRequestDispatcher(String relpath) {
189                if(disconnected) return null;
190                return req.getRequestDispatcher(relpath);
191        }
192
193        @Override
194        public synchronized void removeAttribute(String name) {
195                if(disconnected) disconnectData.attributes.remove(name); 
196                else req.removeAttribute(name);
197        }
198
199        @Override
200        public synchronized void setAttribute(String name, Object value) {
201                if(disconnected) disconnectData.attributes.put(name, value);
202                else req.setAttribute(name, value);
203        }
204        
205        /*public void setAttributes(Request request) {
206                this._request=request;
207        }*/
208
209
210        @Override
211        public synchronized Object getAttribute(String name) {
212                if(disconnected) return disconnectData.attributes.get(name);
213                return req.getAttribute(name);
214        }
215
216        public synchronized Enumeration getAttributeNames() {
217                if(disconnected) {
218                        return new EnumerationWrapper(disconnectData.attributes);
219                }
220                return req.getAttributeNames();
221                
222        }
223
224        @Override
225        public ServletInputStream getInputStream() throws IOException {
226                //if(ba rr!=null) throw new IllegalStateException();
227                if(barr==null) {
228                        if(!firstRead) {
229                                PageContext pc = ThreadLocalPageContext.get();
230                                if(pc!=null) {
231                                        return pc.formScope().getInputStream();
232                                }
233                                return new ServletInputStreamDummy(new byte[]{});       //throw new IllegalStateException();
234                        }
235                        
236                        firstRead=false;
237                        
238                        if(isToBig(getContentLength())) {
239                                return req.getInputStream();
240                        }
241                        InputStream is=null;
242                        try {
243                                barr=IOUtil.toBytes(is=req.getInputStream());
244                                
245                                //Resource res = ResourcesImpl.getFileResourceProvider().getResource("/Users/mic/Temp/multipart.txt");
246                                //IOUtil.copy(new ByteArrayInputStream(barr), res, true);
247                                
248                        }
249                        catch(Throwable t) {
250                                ExceptionUtil.rethrowIfNecessary(t);
251                                barr=null;
252                                return new ServletInputStreamDummy(new byte[]{});        
253                        }
254                        finally {
255                                IOUtil.closeEL(is);
256                        }
257                }
258                
259                return new ServletInputStreamDummy(barr);       
260        }
261        
262        @Override
263        public Map<String,String[]> getParameterMap() {
264                PageContext pc = ThreadLocalPageContext.get();
265                FormImpl form=_form(pc);
266                URLImpl url=_url(pc);
267                
268                return ScopeUtil.getParameterMap(
269                                new URLItem[][]{form.getRaw(),url.getRaw()}, 
270                                new String[]{form.getEncoding(),url.getEncoding()});
271        }
272
273        private static URLImpl _url(PageContext pc) {
274                URL u = pc.urlScope();
275                if(u instanceof UrlFormImpl) {
276                        return ((UrlFormImpl) u).getURL();
277                }
278                return (URLImpl) u;
279        }
280
281        private static FormImpl _form(PageContext pc) {
282                Form f = pc.formScope();
283                if(f instanceof UrlFormImpl) {
284                        return ((UrlFormImpl) f).getForm();
285                }
286                return (FormImpl) f;
287        }
288
289        @Override
290        public Enumeration<String> getParameterNames() {
291                return new ItasEnum<String>(getParameterMap().keySet().iterator());
292        }
293
294        @Override
295        public String[] getParameterValues(String name) {
296                return getParameterValues(ThreadLocalPageContext.get(), name); 
297        }
298        
299        public static String[] getParameterValues(PageContext pc, String name) {
300                pc = ThreadLocalPageContext.get(pc);
301                FormImpl form = _form(pc);
302                URLImpl url= _url(pc);
303                
304                return ScopeUtil.getParameterValues(
305                                new URLItem[][]{form.getRaw(),url.getRaw()}, 
306                                new String[]{form.getEncoding(),url.getEncoding()},name);
307        }
308
309        private boolean isToBig(int contentLength) {
310                if(contentLength<MIN_STORAGE_SIZE) return false;
311                if(contentLength>MAX_STORAGE_SIZE) return true;
312                Runtime rt = Runtime.getRuntime();
313                long av = rt.maxMemory()-rt.totalMemory()+rt.freeMemory();
314                return (av-SPACIG)<contentLength;
315        }
316
317        /* *
318         * with this method it is possibiliy to rewrite the input as many you want
319         * @return input stream from request
320         * @throws IOException
321         * /
322        public ServletInputStream getStoredInputStream() throws IOException {
323                if(firstRead || barr!=null) return getInputStream();
324                return new ServletInputStreamDummy(new byte[]{});        
325        }*/
326
327        @Override
328        public BufferedReader getReader() throws IOException {
329                String enc = getCharacterEncoding();
330                if(StringUtil.isEmpty(enc))enc="iso-8859-1";
331                return IOUtil.toBufferedReader(IOUtil.getReader(getInputStream(), enc));
332        }
333        
334        public void clear() {
335                barr=null;
336        }
337
338        
339
340
341        public HttpServletRequest getOriginalRequest() {
342                if(disconnected) return null;
343                return req;
344        }
345
346        public synchronized void disconnect(PageContextImpl pc) {
347                if(disconnected) return;
348                disconnectData=new DisconnectData();
349                
350                // attributes
351                {
352                        Enumeration<String> attrNames = req.getAttributeNames();
353                        disconnectData.attributes=new HashMap<String, Object>();
354                        String k;
355                        while(attrNames.hasMoreElements()){
356                                k=attrNames.nextElement();
357                                disconnectData.attributes.put(k, req.getAttribute(k));
358                        }
359                }
360                
361                // headers
362                {
363                        Enumeration headerNames = req.getHeaderNames();
364                        disconnectData.headers=new HashMap<Collection.Key, LinkedList<String>>();
365                        
366                        String k;
367                        Enumeration e;
368                        while(headerNames.hasMoreElements()){
369                                k=headerNames.nextElement().toString();
370                                e = req.getHeaders(k);
371                                LinkedList<String> list=new LinkedList<String>();
372                                while(e.hasMoreElements()){
373                                        list.add(e.nextElement().toString());
374                                }
375                                disconnectData.headers.put(KeyImpl.init(k),list);
376                        }
377                }
378                
379                // cookies
380                {
381                        Cookie[] _cookies = req.getCookies();
382                        if(!ArrayUtil.isEmpty(_cookies)) {
383                                disconnectData.cookies=new Cookie[_cookies.length];
384                                for(int i=0;i<_cookies.length;i++) 
385                                        disconnectData.cookies[i]=_cookies[i];
386                        }
387                        else
388                                disconnectData.cookies=new Cookie[0];
389                }
390                
391                disconnectData.authType = req.getAuthType();
392                disconnectData.method=req.getMethod();
393                disconnectData.pathTranslated=req.getPathTranslated();
394                disconnectData.remoteUser=req.getRemoteUser();
395                disconnectData.requestedSessionId=req.getRequestedSessionId();
396                disconnectData.requestedSessionIdFromCookie=req.isRequestedSessionIdFromCookie();
397                disconnectData.requestedSessionIdFromURL=req.isRequestedSessionIdFromURL();
398                disconnectData.secure = req.isSecure();
399                disconnectData.requestedSessionIdValid=req.isRequestedSessionIdValid();
400                disconnectData.characterEncoding = req.getCharacterEncoding();
401                disconnectData.contentLength = req.getContentLength();
402                disconnectData.contentType=req.getContentType();
403                disconnectData.serverPort=req.getServerPort();
404                disconnectData.serverName=req.getServerName();
405                disconnectData.scheme=req.getScheme();
406                disconnectData.remoteHost=req.getRemoteHost();
407                disconnectData.remoteAddr=req.getRemoteAddr();
408                disconnectData.protocol=req.getProtocol();
409                disconnectData.locale=req.getLocale();
410                // only store it when j2ee sessions are enabled
411                if(pc.getSessionType()==Config.SESSION_TYPE_J2EE)
412                        disconnectData.session=req.getSession(true); // create if necessary
413                
414                disconnectData.userPrincipal=req.getUserPrincipal();
415                
416                if(barr==null) {
417                        try {
418                                barr=IOUtil.toBytes(req.getInputStream(),true);
419                        }
420                        catch (IOException e) {
421                                // e.printStackTrace();
422                        }
423                }
424                disconnected=true;
425                //req=null;
426        }
427        
428        static class ArrayEnum<E> implements Enumeration<E> {
429
430                @Override
431                public boolean hasMoreElements() {
432                        return false;
433                }
434
435                @Override
436                public E nextElement() {
437                        return null;
438                }
439                
440        }
441        
442        static class ItasEnum<E> implements Enumeration<E> {
443
444                private Iterator<E> it;
445
446                public ItasEnum(Iterator<E> it){
447                        this.it=it;
448                }
449                @Override
450                public boolean hasMoreElements() {
451                        return it.hasNext();
452                }
453
454                @Override
455                public E nextElement() {
456                        return it.next();
457                }
458        }
459        
460        static class EmptyEnum<E> implements Enumeration<E> {
461
462                @Override
463                public boolean hasMoreElements() {
464                        return false;
465                }
466
467                @Override
468                public E nextElement() {
469                        return null;
470                }
471        }
472        
473        static class StringItasEnum implements Enumeration<String> {
474
475                private Iterator<?> it;
476
477                public StringItasEnum(Iterator<?> it){
478                        this.it=it;
479                }
480                @Override
481                public boolean hasMoreElements() {
482                        return it.hasNext();
483                }
484
485                @Override
486                public String nextElement() {
487                        return StringUtil.toStringNative(it.next(),"");
488                }
489                
490        }
491
492        @Override
493        public String getAuthType() {
494                if(disconnected) return disconnectData.authType;
495                return req.getAuthType();
496        }
497
498        @Override
499        public Cookie[] getCookies() {
500                if(disconnected) return disconnectData.cookies;
501                return req.getCookies();
502                
503        }
504
505        @Override
506        public long getDateHeader(String name) {
507                if(!disconnected) return req.getDateHeader(name);
508                
509                String h = getHeader(name);
510                if(h==null) return -1;
511                DateTime dt = DateCaster.toDateAdvanced(h, null,null);
512                if(dt==null) throw new IllegalArgumentException("cannot convert ["+getHeader(name)+"] to date time value");
513                return dt.getTime();
514        }
515
516        @Override
517        public int getIntHeader(String name) {
518                if(!disconnected) return req.getIntHeader(name);
519                
520                String h = getHeader(name);
521                if(h==null) return -1;
522                Integer i = Caster.toInteger(h, null);
523                if(i==null) throw new NumberFormatException("cannot convert ["+getHeader(name)+"] to int value");
524                return i.intValue();
525        }
526
527        @Override
528        public String getHeader(String name) {
529                if(!disconnected) return req.getHeader(name);
530                
531                LinkedList<String> value = disconnectData.headers.get(KeyImpl.init(name));
532                if(value==null) return null;
533                return value.getFirst();
534        }
535
536        @Override
537        public Enumeration getHeaderNames() {
538                if(!disconnected) return req.getHeaderNames();
539                return new StringItasEnum(disconnectData.headers.keySet().iterator());
540        }
541
542        @Override
543        public Enumeration getHeaders(String name) {
544                if(!disconnected) return req.getHeaders(name);
545                
546                LinkedList<String> value = disconnectData.headers.get(KeyImpl.init(name));
547                if(value!=null)return new ItasEnum<String>(value.iterator());
548                return new EmptyEnum<String>();
549        }
550
551        @Override
552        public String getMethod() {
553                if(!disconnected) return req.getMethod();
554                return disconnectData.method;
555        }
556
557        @Override
558        public String getPathTranslated() {
559                if(!disconnected) return req.getPathTranslated();
560                return disconnectData.pathTranslated;
561        }
562
563        @Override
564        public String getRemoteUser() {
565                if(!disconnected) return req.getRemoteUser();
566                return disconnectData.remoteUser;
567        }
568
569        @Override
570        public String getRequestedSessionId() {
571                if(!disconnected) return req.getRequestedSessionId();
572                return disconnectData.requestedSessionId;
573        }
574
575        @Override
576        public HttpSession getSession() {
577                return getSession(true);
578        }
579
580        @Override
581        public HttpSession getSession(boolean create) {
582                if(!disconnected) return req.getSession(create);
583                return this.disconnectData.session;
584        }
585
586        @Override
587        public Principal getUserPrincipal() {
588                if(!disconnected) return req.getUserPrincipal();
589                return this.disconnectData.userPrincipal;
590        }
591
592        @Override
593        public boolean isRequestedSessionIdFromCookie() {
594                if(!disconnected) return req.isRequestedSessionIdFromCookie();
595                return disconnectData.requestedSessionIdFromCookie;
596        }
597
598        @Override
599        public boolean isRequestedSessionIdFromURL() {
600                if(!disconnected) return req.isRequestedSessionIdFromURL();
601                return disconnectData.requestedSessionIdFromURL;
602        }
603
604        @Override
605        public boolean isRequestedSessionIdFromUrl() {
606                return isRequestedSessionIdFromURL();
607        }
608
609        @Override
610        public boolean isRequestedSessionIdValid() {
611                if(!disconnected) return req.isRequestedSessionIdValid();
612                return disconnectData.requestedSessionIdValid;
613        }
614
615        @Override
616        public String getCharacterEncoding() {
617                if(!disconnected) return req.getCharacterEncoding();
618                return disconnectData.characterEncoding;
619        }
620
621        @Override
622        public int getContentLength() {
623                if(!disconnected) return req.getContentLength();
624                return disconnectData.contentLength;
625        }
626
627        @Override
628        public String getContentType() {
629                if(!disconnected) return req.getContentType();
630                return disconnectData.contentType;
631        }
632
633        @Override
634        public Locale getLocale() {
635                if(!disconnected) return req.getLocale();
636                return disconnectData.locale;
637        }
638
639        @Override
640        public boolean isUserInRole(String role) {
641                if(!disconnected) return req.isUserInRole(role);
642                // try it anyway, in some servlet engine it is still working
643                try{
644                        return req.isUserInRole(role);
645                }
646                catch(Throwable t){
647                        ExceptionUtil.rethrowIfNecessary(t);
648                }
649                // TODO add support for this
650                throw new RuntimeException("this method is not supported when root request is gone");
651        }
652
653        @Override
654        public Enumeration getLocales() {
655                if(!disconnected) return req.getLocales();
656                // try it anyway, in some servlet engine it is still working
657                try{
658                        return req.getLocales();
659                }
660                catch(Throwable t){
661                        ExceptionUtil.rethrowIfNecessary(t);
662                }
663                // TODO add support for this
664                throw new RuntimeException("this method is not supported when root request is gone");
665        }
666
667        @Override
668        public String getRealPath(String path) {
669                if(!disconnected) return req.getRealPath(path);
670                // try it anyway, in some servlet engine it is still working
671                try{
672                        return req.getRealPath(path);
673                }
674                catch(Throwable t){
675                        ExceptionUtil.rethrowIfNecessary(t);
676                }
677                // TODO add support for this
678                throw new RuntimeException("this method is not supported when root request is gone");
679        }
680
681        @Override
682        public String getParameter(String name) {
683                if(!disconnected) return req.getParameter(name);
684                String[] values = getParameterValues(name);
685                if(ArrayUtil.isEmpty(values)) return null;
686                return values[0];
687        }
688
689        @Override
690        public String getProtocol() {
691                if(!disconnected) return req.getProtocol();
692                return disconnectData.protocol;
693        }
694
695        @Override
696        public String getRemoteAddr() {
697                if(!disconnected) return req.getRemoteAddr();
698                return disconnectData.remoteAddr;
699        }
700
701        @Override
702        public String getRemoteHost() {
703                if(!disconnected) return req.getRemoteHost();
704                return disconnectData.remoteHost;
705        }
706
707        @Override
708        public String getScheme() {
709                if(!disconnected) return req.getScheme();
710                return disconnectData.scheme;
711        }
712
713        @Override
714        public String getServerName() {
715                if(!disconnected) return req.getServerName();
716                return disconnectData.serverName;
717        }
718
719        @Override
720        public int getServerPort() {
721                if(!disconnected) return req.getServerPort();
722                return disconnectData.serverPort;
723        }
724
725        @Override
726        public boolean isSecure() {
727                if(!disconnected) return req.isSecure();
728                return disconnectData.secure;
729        }
730
731        @Override
732        public void setCharacterEncoding(String enc) throws UnsupportedEncodingException {
733                if(!disconnected) req.setCharacterEncoding(enc);
734                else disconnectData.characterEncoding=enc;
735        }
736}