View Javadoc
1   /*
2     (C) Copyright IBM Corp. 2005, 2013
3   
4     THIS FILE IS PROVIDED UNDER THE TERMS OF THE ECLIPSE PUBLIC LICENSE
5     ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THIS FILE
6     CONSTITUTES RECIPIENTS ACCEPTANCE OF THE AGREEMENT.
7   
8     You can obtain a current copy of the Eclipse Public License from
9     http://www.opensource.org/licenses/eclipse-1.0.php
10  
11    @author : Roberto Pineiro, IBM, roberto.pineiro@us.ibm.com
12   * @author : Chung-hao Tan, IBM, chungtan@us.ibm.com
13   * 
14   * 
15   * Change History
16   * Flag     Date        Prog         Description
17   *------------------------------------------------------------------------------- 
18   * 17970    2005-08-11  pineiro5     Logon from z/OS not possible
19   * 1353138  2005-11-24  fiuczy       CIMObject element in HTTP header wrong encoded
20   * 1535756  2006-08-07  lupusalex    Make code warning free
21   * 1516242  2006-11-27  lupusalex    Support of OpenPegasus local authentication
22   * 1565892  2006-11-28  lupusalex    Make SBLIM client JSR48 compliant
23   * 1688273  2007-04-19  lupusalex    Full support of HTTP trailers
24   * 1715612  2007-05-09  lupusalex    FVT: Status 0 in trailer is parsed as error
25   * 2003590  2008-06-30  blaschke-oss Change licensing from CPL to EPL
26   * 2204488  2008-10-28  raman_arora  Fix code to remove compiler warnings
27   * 2524131  2009-01-21  raman_arora  Upgrade client to JDK 1.5 (Phase 1)
28   * 2531371  2009-02-10  raman_arora  Upgrade client to JDK 1.5 (Phase 2)
29   * 2641758  2009-02-27  blaschke-oss CIM Client does not recognize HTTP extension headers
30   * 3304058  2011-05-20  blaschke-oss Use same date format in change history
31   * 3553858  2012-08-06  blaschke-oss Append duplicate HTTP header fields instead of replace
32   * 3601894  2013-01-23  blaschke-oss Enhance HTTP and CIM-XML tracing
33   *    2635  2013-05-16  blaschke-oss Slowloris DoS attack for CIM indication listener port
34   *    2718  2013-11-29  blaschke-oss Bad CIMStatusCode generates NumberFormatException
35   */
36  
37  package org.metricshub.wbem.sblim.cimclient.internal.http;
38  
39  /*-
40   * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲
41   * WBEM Java Client
42   * ჻჻჻჻჻჻
43   * Copyright 2023 - 2025 MetricsHub
44   * ჻჻჻჻჻჻
45   * Licensed under the Apache License, Version 2.0 (the "License");
46   * you may not use this file except in compliance with the License.
47   * You may obtain a copy of the License at
48   *
49   *      http://www.apache.org/licenses/LICENSE-2.0
50   *
51   * Unless required by applicable law or agreed to in writing, software
52   * distributed under the License is distributed on an "AS IS" BASIS,
53   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
54   * See the License for the specific language governing permissions and
55   * limitations under the License.
56   * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱
57   */
58  
59  import java.io.BufferedWriter;
60  import java.io.ByteArrayOutputStream;
61  import java.io.IOException;
62  import java.io.InputStream;
63  import java.io.OutputStreamWriter;
64  import java.io.UnsupportedEncodingException;
65  import java.net.URLDecoder;
66  import java.security.AccessController;
67  import java.security.PrivilegedAction;
68  import java.util.BitSet;
69  import java.util.Hashtable;
70  import java.util.Iterator;
71  import java.util.Map.Entry;
72  import java.util.logging.Level;
73  import org.metricshub.wbem.javax.wbem.WBEMException;
74  import org.metricshub.wbem.sblim.cimclient.internal.http.io.ASCIIPrintStream;
75  import org.metricshub.wbem.sblim.cimclient.internal.http.io.TrailerException;
76  import org.metricshub.wbem.sblim.cimclient.internal.logging.LogAndTraceBroker;
77  import org.metricshub.wbem.sblim.cimclient.internal.logging.Messages;
78  import org.metricshub.wbem.sblim.cimclient.internal.util.WBEMConstants;
79  
80  /**
81   * Class HttpHeader represents a http header block
82   *
83   */
84  public class HttpHeader {
85  	private static BitSet cDontNeedEncoding;
86  
87  	private static final String HEX_STR = "0123456789ABCDEF";
88  
89  	private static String cDfltEncName = null;
90  
91  	static {
92  		cDontNeedEncoding = new BitSet(256);
93  		int i;
94  		for (i = 'a'; i <= 'z'; i++) {
95  			cDontNeedEncoding.set(i);
96  		}
97  		for (i = 'A'; i <= 'Z'; i++) {
98  			cDontNeedEncoding.set(i);
99  		}
100 		for (i = '0'; i <= '9'; i++) {
101 			cDontNeedEncoding.set(i);
102 		}
103 		cDontNeedEncoding.set(' ');
104 		cDontNeedEncoding.set('-');
105 		cDontNeedEncoding.set('_');
106 		cDontNeedEncoding.set('/');
107 		cDontNeedEncoding.set('.');
108 		cDontNeedEncoding.set('*');
109 	}
110 
111 	private Hashtable<HeaderEntry, String> iFields = new Hashtable<HeaderEntry, String>();
112 
113 	/**
114 	 * Ctor.
115 	 */
116 	public HttpHeader() {
117 		// empty
118 	}
119 
120 	/**
121 	 * Ctor. Parses the header from an input stream
122 	 *
123 	 * @param pReader
124 	 *            The input stream
125 	 * @throws IOException
126 	 */
127 	public HttpHeader(InputStream pReader) throws IOException {
128 		this(pReader, 0);
129 	}
130 
131 	/**
132 	 * Ctor. Parses the header from an input stream
133 	 *
134 	 * @param pReader
135 	 *            The input stream
136 	 * @param pTimeout
137 	 *            Maximum allowable time to read header
138 	 * @throws IOException
139 	 */
140 	public HttpHeader(InputStream pReader, long pTimeout) throws IOException {
141 		String line = null;
142 		long timeStart = System.currentTimeMillis();
143 		// TODO: this needs to be optimized!!!
144 		while (((line = HttpMethod.readLine(pReader)) != null) && (line.length() > 0)) {
145 			// get the header
146 			if (pTimeout > 0 && (System.currentTimeMillis() - timeStart > pTimeout)) {
147 				throw new IOException(WBEMConstants.INDICATION_DOS_EXCEPTION_MESSAGE);
148 			}
149 			try {
150 				int separator;
151 				if ((separator = line.indexOf(':')) > -1) {
152 					String header;
153 					String value;
154 					int headerStartIndex = 0;
155 
156 					// Ignore prefix-match from HTTP extension (RFC 2774), it'll
157 					// look like "nn-"
158 					if (line.indexOf('-') == 2 && Character.isDigit(line.charAt(0)) && Character.isDigit(line.charAt(1))) {
159 						headerStartIndex = 3;
160 					}
161 
162 					header = line.substring(headerStartIndex, separator);
163 					value = line.substring(separator + 1);
164 					// TODO validate header and value, they must not be empty
165 					// entries
166 					if (value.length() > 0 && value.startsWith(" ")) addParsedField(
167 						header,
168 						value.substring(1)
169 					); else addParsedField(header, value);
170 				} else {
171 					LogAndTraceBroker.getBroker().message(Messages.HTTP_INVALID_HEADER, line);
172 				}
173 			} catch (Exception e) {
174 				LogAndTraceBroker.getBroker().trace(Level.FINER, "Exception while parsing http header", e);
175 				LogAndTraceBroker.getBroker().message(Messages.HTTP_INVALID_HEADER, line);
176 			}
177 		}
178 		return;
179 	}
180 
181 	/**
182 	 * Adds a header field for client output (this means duplicate header
183 	 * entries are replaced)
184 	 *
185 	 * @param pName
186 	 *            The name of the header field
187 	 * @param pValue
188 	 *            The value
189 	 */
190 	public void addField(String pName, String pValue) {
191 		if (pName == null || pName.length() == 0) return;
192 
193 		if (pValue != null) {
194 			this.iFields.put(new HeaderEntry(pName), pValue);
195 		} else {
196 			this.iFields.remove(new HeaderEntry(pName));
197 		}
198 	}
199 
200 	/**
201 	 * Adds a header field from parsed server input (this means duplicate header
202 	 * entries are appended in comma-separated list as defined by RFC 2616)
203 	 *
204 	 * @param pName
205 	 *            The name of the header field
206 	 * @param pValue
207 	 *            The value
208 	 */
209 	public void addParsedField(String pName, String pValue) {
210 		if (pName == null || pName.length() == 0) return;
211 
212 		if (pValue != null) {
213 			String oldValue = this.iFields.put(new HeaderEntry(pName), pValue);
214 			if (oldValue != null) {
215 				// Field already exists, so append value to existing value; it
216 				// is done checking put() return code, instead of checking get()
217 				// return code prior to put(), because typical user case does
218 				// not include duplicate fields
219 				StringBuilder combinedValue = new StringBuilder(oldValue);
220 				combinedValue.append(',');
221 				combinedValue.append(pValue);
222 				this.iFields.put(new HeaderEntry(pName), combinedValue.toString());
223 			}
224 		} else {
225 			this.iFields.remove(new HeaderEntry(pName));
226 		}
227 	}
228 
229 	/**
230 	 * Clears all header fields
231 	 */
232 	public void clear() {
233 		this.iFields.clear();
234 	}
235 
236 	/**
237 	 * Return an iterator over the header fields
238 	 *
239 	 * @return The iterator
240 	 */
241 	public Iterator<Entry<HeaderEntry, String>> iterator() {
242 		return this.iFields.entrySet().iterator();
243 	}
244 
245 	/**
246 	 * Parses a line from a header block
247 	 *
248 	 * @param pLine
249 	 *            The line
250 	 * @return The http header
251 	 */
252 	public static HttpHeader parse(String pLine) {
253 		int prev = 0;
254 		int next = 0;
255 		HttpHeader header = new HttpHeader();
256 		if (pLine != null && pLine.length() > 0) {
257 			next = pLine.indexOf(',');
258 			while (next > -1) {
259 				String hdr = pLine.substring(prev, next);
260 				int separator = hdr.indexOf('=');
261 				if (separator > -1) {
262 					String key;
263 					String value;
264 					key = hdr.substring(0, separator);
265 					value = hdr.substring(separator + 1);
266 
267 					header.addParsedField(key, value);
268 				} else {
269 					// something goes wrong. no separator found
270 				}
271 				prev = next + 1;
272 				while (Character.isSpaceChar(pLine.charAt(prev))) prev++;
273 				next = pLine.indexOf(',', prev);
274 			}
275 			String hdr = pLine.substring(prev);
276 			int separator = hdr.indexOf('=');
277 			if (separator > -1) {
278 				header.addParsedField(hdr.substring(0, separator), hdr.substring(separator + 1));
279 			}
280 		}
281 
282 		return header;
283 	}
284 
285 	@Override
286 	public String toString() {
287 		StringBuffer buf = new StringBuffer();
288 		int i = 0;
289 		Iterator<Entry<HeaderEntry, String>> iterator = this.iFields.entrySet().iterator();
290 		while (iterator.hasNext()) {
291 			if (i++ > 0) buf.append(',');
292 			Entry<HeaderEntry, String> entry = iterator.next();
293 			buf.append(entry.getKey().toString());
294 			buf.append(": ");
295 			buf.append(entry.getValue().toString());
296 		}
297 		return buf.toString();
298 	}
299 
300 	/**
301 	 * Removes a field from the header
302 	 *
303 	 * @param pName
304 	 *            The name of the field
305 	 */
306 	public void removeField(String pName) {
307 		this.iFields.remove(new HeaderEntry(pName));
308 	}
309 
310 	/**
311 	 * Returns a field from the header
312 	 *
313 	 * @param pName
314 	 *            The name of the field
315 	 * @return The value
316 	 */
317 	public String getField(String pName) {
318 		return this.iFields.get(new HeaderEntry(pName));
319 	}
320 
321 	/**
322 	 * Writes a header block to a stream
323 	 *
324 	 * @param pWriter
325 	 *            The stream
326 	 */
327 	public void write(ASCIIPrintStream pWriter) {
328 		Iterator<Entry<HeaderEntry, String>> iterator = this.iFields.entrySet().iterator();
329 		while (iterator.hasNext()) {
330 			Entry<HeaderEntry, String> entry = iterator.next();
331 			pWriter.print(entry.getKey().toString());
332 			pWriter.print(": ");
333 			pWriter.print(entry.getValue().toString());
334 			pWriter.print("\r\n");
335 		}
336 		pWriter.print("\r\n");
337 	}
338 
339 	/**
340 	 * Encodes raw data
341 	 *
342 	 * @param pData
343 	 *            The raw data
344 	 * @return The encoded data
345 	 */
346 	public static synchronized String encode(byte[] pData) {
347 		String str = null;
348 		try {
349 			if (cDfltEncName == null) cDfltEncName = (String) AccessController.doPrivileged(new GetProperty("file.encoding"));
350 			str = encode(pData, cDfltEncName);
351 		} catch (UnsupportedEncodingException e) {
352 			LogAndTraceBroker.getBroker().trace(Level.FINER, "Exception while encoding http header data", e);
353 		}
354 		return str;
355 	}
356 
357 	/**
358 	 * Encodes raw data for a given character set
359 	 *
360 	 * @param pData
361 	 *            The raw data
362 	 * @param pEnc
363 	 *            The character set
364 	 * @return The encoded data
365 	 * @throws UnsupportedEncodingException
366 	 */
367 	public static String encode(byte[] pData, String pEnc) throws UnsupportedEncodingException {
368 		int maxBytesPerChar = 10;
369 		// BufferedWriter validates encoding
370 		ByteArrayOutputStream buf = new ByteArrayOutputStream(maxBytesPerChar);
371 		new BufferedWriter(new OutputStreamWriter(buf, pEnc));
372 		StringBuffer out = new StringBuffer(pData.length);
373 
374 		for (int i = 0; i < pData.length; i++) {
375 			int c = pData[i] & 0xFF;
376 			if (cDontNeedEncoding.get(c)) {
377 				if (c == ' ') {
378 					out.append("%20");
379 				} else {
380 					out.append((char) c);
381 				}
382 			} else {
383 				out.append('%');
384 				out.append(HEX_STR.charAt((c >> 4) & 0x0f));
385 				out.append(HEX_STR.charAt(c & 0x0f));
386 			}
387 		}
388 
389 		return out.toString();
390 	}
391 
392 	/**
393 	 * Encodes a given string for a given character set
394 	 *
395 	 * @param pData
396 	 *            The source string
397 	 * @param pSourceEnc
398 	 *            The source character set
399 	 * @param pTargetEnc
400 	 *            The target character set
401 	 * @return The encoded string
402 	 * @throws UnsupportedEncodingException
403 	 */
404 	public static String encode(String pData, String pSourceEnc, String pTargetEnc) throws UnsupportedEncodingException {
405 		return encode(pData.getBytes(pSourceEnc), pTargetEnc);
406 	}
407 
408 	/**
409 	 * Class HeaderEntry represents a single header field
410 	 *
411 	 */
412 	public static class HeaderEntry {
413 		String iHeader;
414 
415 		int iHashcode;
416 
417 		/**
418 		 * Ctor.
419 		 *
420 		 * @param pName
421 		 *            The name of the header field
422 		 */
423 		public HeaderEntry(String pName) {
424 			this.iHeader = pName;
425 			this.iHashcode = pName.toUpperCase().hashCode();
426 		}
427 
428 		@Override
429 		public boolean equals(Object obj) {
430 			if (obj == null || !(obj instanceof HeaderEntry)) return false;
431 			return this.iHeader.equalsIgnoreCase(((HeaderEntry) obj).iHeader);
432 		}
433 
434 		@Override
435 		public String toString() {
436 			return this.iHeader;
437 		}
438 
439 		@Override
440 		public int hashCode() {
441 			return this.iHashcode;
442 		}
443 	}
444 
445 	/**
446 	 * Class GetProperty implements privileged access to system properties
447 	 *
448 	 */
449 	private static class GetProperty implements PrivilegedAction<Object> {
450 		String iPropertyName;
451 
452 		GetProperty(String propertyName) {
453 			this.iPropertyName = propertyName;
454 		}
455 
456 		public Object run() {
457 			return System.getProperty(this.iPropertyName);
458 		}
459 	}
460 
461 	/**
462 	 * Throws a TrailerException if it contains recognized CIM errors in http
463 	 * trailer entries.
464 	 *
465 	 * @throws TrailerException
466 	 */
467 	public void examineTrailer() throws TrailerException {
468 		examineTrailer(null);
469 	}
470 
471 	/**
472 	 * Throws a TrailerException if it contains recognized CIM errors in http
473 	 * trailer entries.
474 	 *
475 	 * @param pOrigin
476 	 *            The origin of the trailer (response, request, etc.)
477 	 * @throws TrailerException
478 	 */
479 	public void examineTrailer(String pOrigin) throws TrailerException {
480 		Iterator<Entry<HeaderEntry, String>> itr = this.iterator();
481 		int code = 0, i = 0;
482 		String desc = null;
483 		StringBuilder hdrs = null;
484 		if (LogAndTraceBroker.getBroker().isLoggableTrace(Level.FINER)) hdrs = new StringBuilder();
485 		while (itr.hasNext()) {
486 			Entry<HeaderEntry, String> ent = itr.next();
487 			String keyStr = ent.getKey().toString();
488 			if (hdrs != null) {
489 				if (i++ > 0) hdrs.append(',');
490 				hdrs.append(keyStr);
491 				hdrs.append(": ");
492 				hdrs.append(this.getField(keyStr));
493 			}
494 			try {
495 				if (keyStr.equalsIgnoreCase(WBEMConstants.HTTP_TRAILER_STATUS_CODE)) {
496 					String valStr = URLDecoder.decode(this.getField(keyStr), WBEMConstants.UTF8);
497 					try {
498 						code = Integer.parseInt(valStr);
499 					} catch (NumberFormatException e) {
500 						String msg = new String(
501 							WBEMConstants.HTTP_TRAILER_STATUS_CODE + " \"" + valStr + "\" invalid, setting to CIM_ERR_FAILED"
502 						);
503 						LogAndTraceBroker.getBroker().trace(Level.FINER, msg, e);
504 						code = WBEMException.CIM_ERR_FAILED;
505 						if (desc == null) desc = msg;
506 					}
507 				} else if (keyStr.equalsIgnoreCase(WBEMConstants.HTTP_TRAILER_STATUS_DESCRIPTION)) {
508 					desc = URLDecoder.decode(this.getField(keyStr), WBEMConstants.UTF8);
509 				}
510 			} catch (UnsupportedEncodingException e) {
511 				// if UTF-8 isn't supported we're in real trouble
512 				throw new Error(e);
513 			}
514 		}
515 		if (hdrs != null && hdrs.length() > 0) LogAndTraceBroker
516 			.getBroker()
517 			.trace(Level.FINER, (pOrigin == null ? "Unknown" : pOrigin) + " HTTP Trailer Headers= " + hdrs.toString());
518 		if (code > 0) {
519 			if (desc != null) {
520 				throw new TrailerException(new WBEMException(code, desc));
521 			}
522 			throw new TrailerException(new WBEMException(code));
523 		}
524 	}
525 }