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