001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: *
017: */
018:
019: package org.apache.jmeter.protocol.http.proxy;
020:
021: import java.io.ByteArrayOutputStream;
022: import java.io.IOException;
023: import java.io.InputStream;
024: import java.io.UnsupportedEncodingException;
025: import java.net.MalformedURLException;
026: import java.net.ProtocolException;
027: import java.net.URL;
028: import java.util.HashMap;
029: import java.util.Iterator;
030: import java.util.Map;
031: import java.util.StringTokenizer;
032:
033: import org.apache.commons.lang.CharUtils;
034: import org.apache.jmeter.protocol.http.config.MultipartUrlConfig;
035: import org.apache.jmeter.protocol.http.control.Header;
036: import org.apache.jmeter.protocol.http.control.HeaderManager;
037: import org.apache.jmeter.protocol.http.control.gui.HttpTestSampleGui;
038: import org.apache.jmeter.protocol.http.control.gui.HttpTestSampleGui2;
039: import org.apache.jmeter.protocol.http.gui.HeaderPanel;
040: import org.apache.jmeter.protocol.http.sampler.HTTPSampler2;
041: import org.apache.jmeter.protocol.http.sampler.HTTPSamplerBase;
042: import org.apache.jmeter.protocol.http.sampler.HTTPSamplerFactory;
043: import org.apache.jmeter.protocol.http.util.HTTPConstants;
044: import org.apache.jmeter.testelement.TestElement;
045: import org.apache.jmeter.util.JMeterUtils;
046: import org.apache.jorphan.logging.LoggingManager;
047: import org.apache.jorphan.util.JOrphanUtils;
048: import org.apache.log.Logger;
049:
050: //For unit tests, @see TestHttpRequestHdr
051:
052: /**
053: * The headers of the client HTTP request.
054: *
055: */
056: public class HttpRequestHdr {
057: private static final Logger log = LoggingManager
058: .getLoggerForClass();
059:
060: private static final String HTTP = "http"; // $NON-NLS-1$
061: private static final String HTTPS = "https"; // $NON-NLS-1$
062: private static final String PROXY_CONNECTION = "proxy-connection"; // $NON-NLS-1$
063: private static final String CONTENT_TYPE = "content-type"; // $NON-NLS-1$
064: private static final String CONTENT_LENGTH = "content-length"; // $NON-NLS-1$
065:
066: /**
067: * Http Request method. Such as get or post.
068: */
069: private String method = ""; // $NON-NLS-1$
070:
071: /**
072: * The requested url. The universal resource locator that hopefully uniquely
073: * describes the object or service the client is requesting.
074: */
075: private String url = ""; // $NON-NLS-1$
076:
077: /**
078: * Version of http being used. Such as HTTP/1.0.
079: */
080: private String version = ""; // NOTREAD // $NON-NLS-1$
081:
082: private byte[] rawPostData;
083:
084: private Map headers = new HashMap();
085:
086: private HTTPSamplerBase sampler;
087:
088: private HeaderManager headerManager;
089:
090: /*
091: * Optionally number the requests
092: */
093: private static final boolean numberRequests = JMeterUtils
094: .getPropDefault("proxy.number.requests", false); // $NON-NLS-1$
095:
096: private static int requestNumber = 0;// running number
097:
098: public HttpRequestHdr() {
099: this .sampler = HTTPSamplerFactory.newInstance();
100: }
101:
102: /**
103: * @param sampler the http sampler
104: */
105: public HttpRequestHdr(HTTPSamplerBase sampler) {
106: this .sampler = sampler;
107: }
108:
109: /**
110: * Parses a http header from a stream.
111: *
112: * @param in
113: * the stream to parse.
114: * @return array of bytes from client.
115: */
116: public byte[] parse(InputStream in) throws IOException {
117: boolean inHeaders = true;
118: int readLength = 0;
119: int dataLength = 0;
120: boolean first = true;
121: ByteArrayOutputStream clientRequest = new ByteArrayOutputStream();
122: ByteArrayOutputStream line = new ByteArrayOutputStream();
123: int x;
124: while ((inHeaders || readLength < dataLength)
125: && ((x = in.read()) != -1)) {
126: line.write(x);
127: clientRequest.write(x);
128: if (first && !CharUtils.isAscii((char) x)) {
129: throw new IllegalArgumentException(
130: "Only ASCII supported in headers (perhaps SSL was used?)");
131: }
132: if (inHeaders && (byte) x == (byte) '\n') { // $NON-NLS-1$
133: if (line.size() < 3) {
134: inHeaders = false;
135: first = false; // cannot be first line either
136: }
137: if (first) {
138: parseFirstLine(line.toString());
139: first = false;
140: } else {
141: dataLength = Math.max(parseLine(line.toString()),
142: dataLength);
143: }
144: if (log.isDebugEnabled()) {
145: log
146: .debug("Client Request Line: "
147: + line.toString());
148: }
149: line.reset();
150: } else if (!inHeaders) {
151: readLength++;
152: }
153: }
154: // Keep the raw post data
155: rawPostData = line.toByteArray();
156:
157: if (log.isDebugEnabled()) {
158: log.debug("rawPostData in default JRE encoding: "
159: + new String(rawPostData));
160: log.debug("Request: " + clientRequest.toString());
161: }
162: return clientRequest.toByteArray();
163: }
164:
165: private void parseFirstLine(String firstLine) {
166: if (log.isDebugEnabled()) {
167: log.debug("browser request: " + firstLine);
168: }
169: if (!CharUtils.isAsciiAlphanumeric(firstLine.charAt(0))) {
170: throw new IllegalArgumentException(
171: "Unrecognised header line (probably used HTTPS)");
172: }
173: StringTokenizer tz = new StringTokenizer(firstLine);
174: method = getToken(tz).toUpperCase();
175: url = getToken(tz);
176: if (url.toLowerCase().startsWith(HTTPConstants.PROTOCOL_HTTPS)) {
177: throw new IllegalArgumentException(
178: "Cannot handle https URLS: " + url);
179: }
180: version = getToken(tz);
181: if (log.isDebugEnabled()) {
182: log.debug("parser input: " + firstLine);
183: log.debug("parsed method: " + method);
184: log.debug("parsed url: " + url);
185: log.debug("parsed version:" + version);
186: }
187: if ("CONNECT".equalsIgnoreCase(method)) {
188: throw new IllegalArgumentException(
189: "Cannot handle CONNECT - probably used HTTPS");
190: }
191: }
192:
193: /*
194: * Split line into name/value pairs and store in headers if relevant
195: * If name = "content-length", then return value as int, else return 0
196: */
197: private int parseLine(String nextLine) {
198: StringTokenizer tz;
199: tz = new StringTokenizer(nextLine);
200: String token = getToken(tz);
201: // look for termination of HTTP command
202: if (0 == token.length()) {
203: return 0;
204: } else {
205: String trimmed = token.trim();
206: String name = trimmed.substring(0, trimmed.length() - 1);// drop ':'
207: String value = getRemainder(tz);
208: headers.put(name.toLowerCase(), new Header(name, value));
209: if (name.equalsIgnoreCase(CONTENT_LENGTH)) {
210: return Integer.parseInt(value);
211: }
212: }
213: return 0;
214: }
215:
216: private HeaderManager createHeaderManager() {
217: HeaderManager manager = new HeaderManager();
218: Iterator keys = headers.keySet().iterator();
219: while (keys.hasNext()) {
220: String key = (String) keys.next();
221: if (!key.equals(PROXY_CONNECTION)
222: && !key.equals(CONTENT_LENGTH)) {
223: manager.add((Header) headers.get(key));
224: }
225: }
226: manager.setName("Browser-derived headers");
227: manager.setProperty(TestElement.TEST_CLASS, HeaderManager.class
228: .getName());
229: manager.setProperty(TestElement.GUI_CLASS, HeaderPanel.class
230: .getName());
231: return manager;
232: }
233:
234: public HeaderManager getHeaderManager() {
235: if (headerManager == null) {
236: headerManager = createHeaderManager();
237: }
238: return headerManager;
239: }
240:
241: public HTTPSamplerBase getSampler(Map pageEncodings,
242: Map formEncodings) throws MalformedURLException,
243: IOException, ProtocolException {
244: // Damn! A whole new GUI just to instantiate a test element?
245: // Isn't there a beter way?
246: HttpTestSampleGui tempGui = null;
247: // Create the corresponding gui for the sampler class
248: if (sampler instanceof HTTPSampler2) {
249: tempGui = new HttpTestSampleGui2();
250: } else {
251: tempGui = new HttpTestSampleGui();
252: }
253: sampler.setProperty(TestElement.GUI_CLASS, tempGui.getClass()
254: .getName());
255:
256: // Populate the sampler
257: populateSampler(pageEncodings, formEncodings);
258:
259: tempGui.configure(sampler);
260: tempGui.modifyTestElement(sampler);
261: // Defaults
262: sampler.setFollowRedirects(false);
263: sampler.setUseKeepAlive(true);
264:
265: if (log.isDebugEnabled())
266: log
267: .debug("getSampler: sampler path = "
268: + sampler.getPath());
269: return sampler;
270: }
271:
272: /**
273: *
274: * @return the sampler
275: * @throws MalformedURLException
276: * @throws IOException
277: * @throws ProtocolException
278: * @deprecated use the getSampler(HashMap pageEncodings, HashMap formEncodings) instead, since
279: * that properly handles the encodings of the page
280: */
281: public HTTPSamplerBase getSampler() throws MalformedURLException,
282: IOException, ProtocolException {
283: return getSampler(null, null);
284: }
285:
286: private String getContentType() {
287: Header contentTypeHeader = (Header) headers.get(CONTENT_TYPE);
288: if (contentTypeHeader != null) {
289: return contentTypeHeader.getValue();
290: }
291: return null;
292: }
293:
294: private String getContentEncoding() {
295: String contentType = getContentType();
296: if (contentType != null) {
297: int charSetStartPos = contentType.toLowerCase().indexOf(
298: "charset=");
299: if (charSetStartPos >= 0) {
300: String charSet = contentType.substring(charSetStartPos
301: + "charset=".length());
302: if (charSet != null && charSet.length() > 0) {
303: // Remove quotes if present
304: charSet = JOrphanUtils.replaceAllChars(charSet,
305: '"', "");
306: if (charSet.length() > 0) {
307: return charSet;
308: }
309: }
310: }
311: }
312: return null;
313: }
314:
315: private boolean isMultipart(String contentType) {
316: if (contentType != null
317: && contentType
318: .startsWith(HTTPConstants.MULTIPART_FORM_DATA)) {
319: return true;
320: } else {
321: return false;
322: }
323: }
324:
325: private MultipartUrlConfig getMultipartConfig(String contentType) {
326: if (isMultipart(contentType)) {
327: // Get the boundary string for the multiparts from the content type
328: String boundaryString = contentType.substring(contentType
329: .toLowerCase().indexOf("boundary=")
330: + "boundary=".length());
331: return new MultipartUrlConfig(boundaryString);
332: } else {
333: return null;
334: }
335: }
336:
337: private void populateSampler(Map pageEncodings, Map formEncodings)
338: throws MalformedURLException, UnsupportedEncodingException {
339: sampler.setDomain(serverName());
340: if (log.isDebugEnabled())
341: log.debug("Proxy: setting server: " + sampler.getDomain());
342: sampler.setMethod(method);
343: log.debug("Proxy: setting method: " + sampler.getMethod());
344: sampler.setPort(serverPort());
345: if (log.isDebugEnabled())
346: log.debug("Proxy: setting port: " + sampler.getPort());
347: if (url.indexOf("//") > -1) {
348: String protocol = url.substring(0, url.indexOf(":"));
349: if (log.isDebugEnabled())
350: log.debug("Proxy: setting protocol to : " + protocol);
351: sampler.setProtocol(protocol);
352: } else if (sampler.getPort() == HTTPConstants.DEFAULT_HTTPS_PORT) {
353: sampler.setProtocol(HTTPS);
354: if (log.isDebugEnabled())
355: log.debug("Proxy: setting protocol to https");
356: } else {
357: if (log.isDebugEnabled())
358: log.debug("Proxy setting default protocol to: http");
359: sampler.setProtocol(HTTP);
360: }
361:
362: URL pageUrl = null;
363: if (sampler.isProtocolDefaultPort()) {
364: pageUrl = new URL(sampler.getProtocol(), sampler
365: .getDomain(), getPath());
366: } else {
367: pageUrl = new URL(sampler.getProtocol(), sampler
368: .getDomain(), sampler.getPort(), getPath());
369: }
370: String urlWithoutQuery = getUrlWithoutQuery(pageUrl);
371:
372: // Check if the request itself tells us what the encoding is
373: String contentEncoding = null;
374: String requestContentEncoding = getContentEncoding();
375: if (requestContentEncoding != null) {
376: contentEncoding = requestContentEncoding;
377: } else {
378: // Check if we know the encoding of the page
379: if (pageEncodings != null) {
380: synchronized (pageEncodings) {
381: contentEncoding = (String) pageEncodings
382: .get(urlWithoutQuery);
383: }
384: }
385: // Check if we know the encoding of the form
386: if (formEncodings != null) {
387: synchronized (formEncodings) {
388: String formEncoding = (String) formEncodings
389: .get(urlWithoutQuery);
390: // Form encoding has priority over page encoding
391: if (formEncoding != null) {
392: contentEncoding = formEncoding;
393: }
394: }
395: }
396: }
397:
398: // Get the post data using the content encoding of the request
399: String postData = null;
400: if (log.isDebugEnabled()) {
401: if (contentEncoding != null) {
402: log.debug("Using encoding " + contentEncoding
403: + " for request body");
404: } else {
405: log
406: .debug("No encoding found, using JRE default encoding for request body");
407: }
408: }
409: if (contentEncoding != null) {
410: postData = new String(rawPostData, contentEncoding);
411: } else {
412: // Use default encoding
413: postData = new String(rawPostData);
414: }
415:
416: if (contentEncoding != null) {
417: sampler.setPath(getPath(), contentEncoding);
418: } else {
419: // Although the spec says UTF-8 should be used for encoding URL parameters,
420: // most browser use ISO-8859-1 for default if encoding is not known.
421: // We use null for contentEncoding, then the url parameters will be added
422: // with the value in the URL, and the "encode?" flag set to false
423: sampler.setPath(getPath(), null);
424: }
425: if (log.isDebugEnabled())
426: log.debug("Proxy: setting path: " + sampler.getPath());
427: if (numberRequests) {
428: requestNumber++;
429: sampler.setName(requestNumber + " " + sampler.getPath());
430: } else {
431: sampler.setName(sampler.getPath());
432: }
433:
434: // Set the content encoding
435: if (contentEncoding != null) {
436: sampler.setContentEncoding(contentEncoding);
437: }
438:
439: // If it was a HTTP GET request, then all parameters in the URL
440: // has been handled by the sampler.setPath above, so we just need
441: // to do parse the rest of the request if it is not a GET request
442: if (!HTTPConstants.GET.equals(method)) {
443: // Check if it was a multipart http post request
444: final String contentType = getContentType();
445: MultipartUrlConfig urlConfig = getMultipartConfig(contentType);
446: if (urlConfig != null) {
447: urlConfig.parseArguments(postData);
448: // Tell the sampler to do a multipart post
449: sampler.setDoMultipartPost(true);
450: // Remove the header for content-type and content-length, since
451: // those values will most likely be incorrect when the sampler
452: // performs the multipart request, because the boundary string
453: // will change
454: getHeaderManager().removeHeaderNamed(CONTENT_TYPE);
455: getHeaderManager().removeHeaderNamed(CONTENT_LENGTH);
456:
457: // Set the form data
458: sampler.setArguments(urlConfig.getArguments());
459: // Set the file uploads
460: sampler.setFileField(urlConfig.getFileFieldName());
461: sampler.setFilename(urlConfig.getFilename());
462: sampler.setMimetype(urlConfig.getMimeType());
463: } else if (postData != null
464: && postData.trim().startsWith("<?")) {
465: // Not sure if this is needed anymore. I assume these requests
466: // do not have HTTPConstants.APPLICATION_X_WWW_FORM_URLENCODED as content type,
467: // and they would therefore be catched by the last else if of these if else if tests
468: sampler.addNonEncodedArgument("", postData, ""); //used when postData is pure xml (ex. an xml-rpc call)
469: } else if (contentType == null
470: || contentType
471: .startsWith(HTTPConstants.APPLICATION_X_WWW_FORM_URLENCODED)) {
472: // It is the most common post request, with parameter name and values
473: // We also assume this if no content type is present, to be most backwards compatible,
474: // but maybe we should only parse arguments if the content type is as expected
475: sampler
476: .parseArguments(postData.trim(),
477: contentEncoding); //standard name=value postData
478: } else if (postData != null && postData.length() > 0) {
479: // Just put the whole postbody as the value of a parameter
480: sampler.addNonEncodedArgument("", postData, ""); //used when postData is pure xml (ex. an xml-rpc call)
481: }
482: }
483: if (log.isDebugEnabled())
484: log.debug("sampler path = " + sampler.getPath());
485: }
486:
487: //
488: // Parsing Methods
489: //
490:
491: /**
492: * Find the //server.name from an url.
493: *
494: * @return server's internet name
495: */
496: private String serverName() {
497: // chop to "server.name:x/thing"
498: String str = url;
499: int i = str.indexOf("//"); // $NON-NLS-1$
500: if (i > 0) {
501: str = str.substring(i + 2);
502: }
503: // chop to server.name:xx
504: i = str.indexOf("/"); // $NON-NLS-1$
505: if (0 < i) {
506: str = str.substring(0, i);
507: }
508: // chop to server.name
509: i = str.indexOf(":"); // $NON-NLS-1$
510: if (0 < i) {
511: str = str.substring(0, i);
512: }
513: return str;
514: }
515:
516: // TODO replace repeated substr() above and below with more efficient method.
517:
518: /**
519: * Find the :PORT from http://server.ect:PORT/some/file.xxx
520: *
521: * @return server's port (or UNSPECIFIED if not found)
522: */
523: private int serverPort() {
524: String str = url;
525: // chop to "server.name:x/thing"
526: int i = str.indexOf("//");
527: if (i > 0) {
528: str = str.substring(i + 2);
529: }
530: // chop to server.name:xx
531: i = str.indexOf("/");
532: if (0 < i) {
533: str = str.substring(0, i);
534: }
535: // chop XX
536: i = str.indexOf(":");
537: if (0 < i) {
538: return Integer.parseInt(str.substring(i + 1).trim());
539: }
540: return HTTPSamplerBase.UNSPECIFIED_PORT;
541: }
542:
543: /**
544: * Find the /some/file.xxxx from http://server.ect:PORT/some/file.xxx
545: *
546: * @return the path
547: */
548: private String getPath() {
549: String str = url;
550: int i = str.indexOf("//");
551: if (i > 0) {
552: str = str.substring(i + 2);
553: }
554: i = str.indexOf("/");
555: if (i < 0) {
556: return "";
557: }
558: return str.substring(i);
559: }
560:
561: /**
562: * Returns the url string extracted from the first line of the client request.
563: *
564: * @return the url
565: */
566: public String getUrl() {
567: return url;
568: }
569:
570: /**
571: * Returns the next token in a string.
572: *
573: * @param tk
574: * String that is partially tokenized.
575: * @return The remainder
576: */
577: private String getToken(StringTokenizer tk) {
578: if (tk.hasMoreTokens()) {
579: return tk.nextToken();
580: }
581: return "";// $NON-NLS-1$
582: }
583:
584: /**
585: * Returns the remainder of a tokenized string.
586: *
587: * @param tk
588: * String that is partially tokenized.
589: * @return The remainder
590: */
591: private String getRemainder(StringTokenizer tk) {
592: StringBuffer strBuff = new StringBuffer();
593: if (tk.hasMoreTokens()) {
594: strBuff.append(tk.nextToken());
595: }
596: while (tk.hasMoreTokens()) {
597: strBuff.append(" "); // $NON-NLS-1$
598: strBuff.append(tk.nextToken());
599: }
600: return strBuff.toString();
601: }
602:
603: private String getUrlWithoutQuery(URL _url) {
604: String fullUrl = _url.toString();
605: String urlWithoutQuery = fullUrl;
606: String query = _url.getQuery();
607: if (query != null) {
608: // Get rid of the query and the ?
609: urlWithoutQuery = urlWithoutQuery.substring(0,
610: urlWithoutQuery.length() - query.length() - 1);
611: }
612: return urlWithoutQuery;
613: }
614: }
|