1 /*
2  * Copyright 2008-2019 by Emeric Vernat
3  *
4  *     This file is part of Java Melody.
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 package net.bull.javamelody;
19
20 import java.io.BufferedInputStream;
21 import java.io.BufferedReader;
22 import java.io.IOException;
23 import java.io.InputStream;
24 import java.io.InputStreamReader;
25 import java.util.NoSuchElementException;
26 import java.util.Scanner;
27 import java.util.regex.Pattern;
28
29 import javax.servlet.ReadListener;
30 import javax.servlet.ServletInputStream;
31 import javax.servlet.ServletRequest;
32 import javax.servlet.http.HttpServletRequest;
33 import javax.servlet.http.HttpServletRequestWrapper;
34 import javax.xml.stream.XMLInputFactory;
35 import javax.xml.stream.XMLStreamException;
36 import javax.xml.stream.XMLStreamReader;
37
38 import net.bull.javamelody.internal.common.LOG;
39
40 //20091201 dhartford GWTRequestWrapper
41 //20100519 dhartford adjustments for UTF-8, however did not have an impact so removed.
42 //20100520 dhartford adjustments for reader/inputstream.
43 //20110206 evernat   refactoring
44 //20131111 roy.paterson   SOAP request wrapper
45 //20131111 evernat   refactoring
46
47 /**
48  * Simple Wrapper class to inspect payload for name.
49  * @author dhartford, roy.paterson, evernat
50  */

51 public class PayloadNameRequestWrapper extends HttpServletRequestWrapper {
52     private static final Pattern GWT_RPC_SEPARATOR_CHAR_PATTERN = Pattern
53             .compile(Pattern.quote("|"));
54
55     /**
56      * Name of request, or null if we don't know based on payload @null
57      */

58     private String name;
59
60     /**
61      * Type of request if name != null, or null if we don't know based on the payload @null
62      */

63     private String requestType;
64
65     private BufferedInputStream bufferedInputStream;
66     private ServletInputStream inputStream;
67     private BufferedReader reader;
68
69     /**
70      * Constructor.
71      * @param request the original HttpServletRequest
72      */

73     public PayloadNameRequestWrapper(HttpServletRequest request) {
74         super(request);
75     }
76
77     protected void initialize() throws IOException {
78         //name on a best-effort basis
79         name = null;
80         requestType = null;
81
82         final HttpServletRequest request = (HttpServletRequest) getRequest();
83         final String contentType = request.getContentType();
84         if (contentType == null) {
85             //don't know how to handle this content type
86             return;
87         }
88
89         if (!"POST".equalsIgnoreCase(request.getMethod())) {
90             //no payload
91             return;
92         }
93
94         //Try look for name in payload on a best-effort basis...
95         try {
96             if (contentType.startsWith("text/x-gwt-rpc")) {
97                 //parse GWT-RPC method name
98                 name = parseGwtRpcMethodName(getBufferedInputStream(), getCharacterEncoding());
99                 requestType = "GWT-RPC";
100             } else if (contentType.startsWith("application/soap+xml"//SOAP 1.2
101                     || contentType.startsWith("text/xml"//SOAP 1.1
102                             && request.getHeader("SOAPAction") != null) {
103                 //parse SOAP method name
104                 name = parseSoapMethodName(getBufferedInputStream(), getCharacterEncoding());
105                 requestType = "SOAP";
106             } else {
107                 //don't know how to name this request based on payload
108                 //(don't parse if text/xml for XML-RPC, because it is obsolete)
109                 name = null;
110                 requestType = null;
111             }
112         } catch (final Exception e) {
113             LOG.debug("Error trying to parse payload content for request name", e);
114
115             //best-effort - couldn't figure it out
116             name = null;
117             requestType = null;
118         } finally {
119             //reset stream so application is unaffected
120             resetBufferedInputStream();
121         }
122     }
123
124     protected BufferedInputStream getBufferedInputStream() throws IOException {
125         if (bufferedInputStream == null) {
126             //workaround Tomcat issue with form POSTs
127             //see http://stackoverflow.com/questions/18489399/read-httpservletrequests-post-body-and-then-call-getparameter-in-tomcat
128             final ServletRequest request = getRequest();
129             request.getParameterMap();
130
131             //buffer the payload so we can inspect it
132             bufferedInputStream = new BufferedInputStream(request.getInputStream());
133             // and mark to allow the stream to be reset
134             bufferedInputStream.mark(Integer.MAX_VALUE);
135         }
136         return bufferedInputStream;
137     }
138
139     protected void resetBufferedInputStream() throws IOException {
140         if (bufferedInputStream != null) {
141             bufferedInputStream.reset();
142         }
143     }
144
145     /**
146      * Try to parse GWT-RPC method name from request body stream.  Does not close the stream.
147      *
148      * @param stream GWT-RPC request body stream @nonnull
149      * @param charEncoding character encoding of stream, or null for platform default @null
150      * @return GWT-RPC method name, or null if unable to parse @null
151      */

152     @SuppressWarnings("resource")
153     private static String parseGwtRpcMethodName(InputStream stream, String charEncoding) {
154         //commented out code uses GWT-user library for a more 'proper' approach.
155         //GWT-user library approach is more future-proof, but requires more dependency management.
156         //                RPCRequest decodeRequest = RPC.decodeRequest(readLine);
157         //                gwtmethodname = decodeRequest.getMethod().getName();
158
159         try {
160             final Scanner scanner;
161             if (charEncoding == null) {
162                 scanner = new Scanner(stream);
163             } else {
164                 scanner = new Scanner(stream, charEncoding);
165             }
166             scanner.useDelimiter(GWT_RPC_SEPARATOR_CHAR_PATTERN); //AbstractSerializationStream.RPC_SEPARATOR_CHAR
167
168             //AbstractSerializationStreamReader.prepareToRead(...)
169             scanner.next(); //stream version number
170             scanner.next(); //flags
171
172             //ServerSerializationStreamReader.deserializeStringTable()
173             scanner.next(); //type name count
174
175             //ServerSerializationStreamReader.preapreToRead(...)
176             scanner.next(); //module base URL
177             scanner.next(); //strong name
178
179             //RPC.decodeRequest(...)
180             scanner.next(); //service interface name
181             return "." + scanner.next(); //service method name
182
183             //note we don't close the scanner because we don't want to close the underlying stream
184         } catch (final NoSuchElementException e) {
185             LOG.debug("Unable to parse GWT-RPC request", e);
186
187             //code above is best-effort - we were unable to parse GWT payload so
188             //treat as a normal HTTP request
189             return null;
190         }
191     }
192
193     /**
194      * Scan xml for tag child of the current element
195      *
196      * @param reader reader, must be at "start element" @nonnull
197      * @param tagName name of child tag to find @nonnull
198      * @return if found tag
199      * @throws XMLStreamException on error
200      */

201     static boolean scanForChildTag(XMLStreamReader reader, String tagName)
202             throws XMLStreamException {
203         assert reader.isStartElement();
204
205         int level = -1;
206         while (reader.hasNext()) {
207             //keep track of level so we only search children, not descendants
208             if (reader.isStartElement()) {
209                 level++;
210             } else if (reader.isEndElement()) {
211                 level--;
212             }
213             if (level < 0) {
214                 //end parent tag - no more children
215                 break;
216             }
217
218             reader.next();
219
220             if (level == 0 && reader.isStartElement() && reader.getLocalName().equals(tagName)) {
221                 return true//found
222             }
223         }
224         return false//got to end of parent element and not found
225     }
226
227     /**
228      * Try to parse SOAP method name from request body stream.  Does not close the stream.
229      *
230      * @param stream SOAP request body stream @nonnull
231      * @param charEncoding character encoding of stream, or null for platform default @null
232      * @return SOAP method name, or null if unable to parse @null
233      */

234     private static String parseSoapMethodName(InputStream stream, String charEncoding) {
235         try {
236             // newInstance() et pas newFactory() pour java 1.5 (issue 367)
237             final XMLInputFactory factory = XMLInputFactory.newInstance();
238             factory.setProperty(XMLInputFactory.SUPPORT_DTD, false); // disable DTDs entirely for that factory
239             factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false); // disable external entities
240             final XMLStreamReader xmlReader;
241             if (charEncoding != null) {
242                 xmlReader = factory.createXMLStreamReader(stream, charEncoding);
243             } else {
244                 xmlReader = factory.createXMLStreamReader(stream);
245             }
246
247             //best-effort parsing
248
249             //start document, go to first tag
250             xmlReader.nextTag();
251
252             //expect first tag to be "Envelope"
253             if (!"Envelope".equals(xmlReader.getLocalName())) {
254                 LOG.debug("Unexpected first tag of SOAP request: '" + xmlReader.getLocalName()
255                         + "' (expected 'Envelope')");
256                 return null//failed
257             }
258
259             //scan for body tag
260             if (!scanForChildTag(xmlReader, "Body")) {
261                 LOG.debug("Unable to find SOAP 'Body' tag");
262                 return null//failed
263             }
264
265             xmlReader.nextTag();
266
267             //tag is method name
268             return "." + xmlReader.getLocalName();
269         } catch (final XMLStreamException e) {
270             LOG.debug("Unable to parse SOAP request", e);
271             //failed
272             return null;
273         }
274     }
275
276     /** {@inheritDoc} */
277     @Override
278     public BufferedReader getReader() throws IOException {
279         if (bufferedInputStream == null) {
280             return super.getReader();
281         }
282         if (reader == null) {
283             // use character encoding as said in the API
284             final String characterEncoding = this.getCharacterEncoding();
285             if (characterEncoding == null) {
286                 reader = new BufferedReader(new InputStreamReader(this.getInputStream()));
287             } else {
288                 reader = new BufferedReader(
289                         new InputStreamReader(this.getInputStream(), characterEncoding));
290             }
291         }
292         return reader;
293     }
294
295     /** {@inheritDoc} */
296     @Override
297     public ServletInputStream getInputStream() throws IOException {
298         final ServletInputStream requestInputStream = super.getInputStream();
299         if (bufferedInputStream == null) {
300             return requestInputStream;
301         }
302         if (inputStream == null) {
303             final BufferedInputStream myBufferedInputStream = bufferedInputStream;
304             //CHECKSTYLE:OFF
305             inputStream = new ServletInputStream() {
306                 //CHECKSTYLE:ON
307                 @Override
308                 public int read() throws IOException {
309                     return myBufferedInputStream.read();
310                 }
311
312                 @Override
313                 public boolean isFinished() {
314                     return requestInputStream.isFinished();
315                 }
316
317                 @Override
318                 public boolean isReady() {
319                     return requestInputStream.isReady();
320                 }
321
322                 @Override
323                 public void setReadListener(ReadListener readListener) {
324                     requestInputStream.setReadListener(readListener);
325                 }
326             };
327         }
328         return inputStream;
329     }
330
331     /**
332      * @return name of request, or null if we can't figure out a good name based on
333      *   the request payload @null
334      */

335     public String getPayloadRequestName() {
336         return name;
337     }
338
339     /**
340      * Get type of request.  If {@link #getPayloadRequestName()} returns non-null then
341      * this method also returns non-null.
342      *
343      * @return type of request if or null if don't know @null
344      */

345     public String getPayloadRequestType() {
346         return requestType;
347     }
348 }
349