1. /*
  2. * $Header: /home/cvs/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/cookie/CookieSpecBase.java,v 1.27 2004/09/14 20:11:31 olegk Exp $
  3. * $Revision: 1.27 $
  4. * $Date: 2004/09/14 20:11:31 $
  5. *
  6. * ====================================================================
  7. *
  8. * Copyright 2002-2004 The Apache Software Foundation
  9. *
  10. * Licensed under the Apache License, Version 2.0 (the "License");
  11. * you may not use this file except in compliance with the License.
  12. * You may obtain a copy of the License at
  13. *
  14. * http://www.apache.org/licenses/LICENSE-2.0
  15. *
  16. * Unless required by applicable law or agreed to in writing, software
  17. * distributed under the License is distributed on an "AS IS" BASIS,
  18. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  19. * See the License for the specific language governing permissions and
  20. * limitations under the License.
  21. * ====================================================================
  22. *
  23. * This software consists of voluntary contributions made by many
  24. * individuals on behalf of the Apache Software Foundation. For more
  25. * information on the Apache Software Foundation, please see
  26. * <http://www.apache.org/>.
  27. *
  28. */
  29. package org.apache.commons.httpclient.cookie;
  30. import java.util.Collection;
  31. import java.util.Date;
  32. import java.util.LinkedList;
  33. import java.util.List;
  34. import org.apache.commons.httpclient.Cookie;
  35. import org.apache.commons.httpclient.Header;
  36. import org.apache.commons.httpclient.HeaderElement;
  37. import org.apache.commons.httpclient.NameValuePair;
  38. import org.apache.commons.httpclient.util.DateParseException;
  39. import org.apache.commons.httpclient.util.DateParser;
  40. import org.apache.commons.logging.Log;
  41. import org.apache.commons.logging.LogFactory;
  42. /**
  43. *
  44. * Cookie management functions shared by all specification.
  45. *
  46. * @author B.C. Holmes
  47. * @author <a href="mailto:jericho@thinkfree.com">Park, Sung-Gu</a>
  48. * @author <a href="mailto:dsale@us.britannica.com">Doug Sale</a>
  49. * @author Rod Waldhoff
  50. * @author dIon Gillard
  51. * @author Sean C. Sullivan
  52. * @author <a href="mailto:JEvans@Cyveillance.com">John Evans</a>
  53. * @author Marc A. Saegesser
  54. * @author <a href="mailto:oleg@ural.ru">Oleg Kalnichevski</a>
  55. * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a>
  56. *
  57. * @since 2.0
  58. */
  59. public class CookieSpecBase implements CookieSpec {
  60. /** Log object */
  61. protected static final Log LOG = LogFactory.getLog(CookieSpec.class);
  62. /** Valid date patterns */
  63. private Collection datepatterns = null;
  64. /** Default constructor */
  65. public CookieSpecBase() {
  66. super();
  67. }
  68. /**
  69. * Parses the Set-Cookie value into an array of <tt>Cookie</tt>s.
  70. *
  71. * <P>The syntax for the Set-Cookie response header is:
  72. *
  73. * <PRE>
  74. * set-cookie = "Set-Cookie:" cookies
  75. * cookies = 1#cookie
  76. * cookie = NAME "=" VALUE * (";" cookie-av)
  77. * NAME = attr
  78. * VALUE = value
  79. * cookie-av = "Comment" "=" value
  80. * | "Domain" "=" value
  81. * | "Max-Age" "=" value
  82. * | "Path" "=" value
  83. * | "Secure"
  84. * | "Version" "=" 1*DIGIT
  85. * </PRE>
  86. *
  87. * @param host the host from which the <tt>Set-Cookie</tt> value was
  88. * received
  89. * @param port the port from which the <tt>Set-Cookie</tt> value was
  90. * received
  91. * @param path the path from which the <tt>Set-Cookie</tt> value was
  92. * received
  93. * @param secure <tt>true</tt> when the <tt>Set-Cookie</tt> value was
  94. * received over secure conection
  95. * @param header the <tt>Set-Cookie</tt> received from the server
  96. * @return an array of <tt>Cookie</tt>s parsed from the Set-Cookie value
  97. * @throws MalformedCookieException if an exception occurs during parsing
  98. */
  99. public Cookie[] parse(String host, int port, String path,
  100. boolean secure, final String header)
  101. throws MalformedCookieException {
  102. LOG.trace("enter CookieSpecBase.parse("
  103. + "String, port, path, boolean, Header)");
  104. if (host == null) {
  105. throw new IllegalArgumentException(
  106. "Host of origin may not be null");
  107. }
  108. if (host.trim().equals("")) {
  109. throw new IllegalArgumentException(
  110. "Host of origin may not be blank");
  111. }
  112. if (port < 0) {
  113. throw new IllegalArgumentException("Invalid port: " + port);
  114. }
  115. if (path == null) {
  116. throw new IllegalArgumentException(
  117. "Path of origin may not be null.");
  118. }
  119. if (header == null) {
  120. throw new IllegalArgumentException("Header may not be null.");
  121. }
  122. if (path.trim().equals("")) {
  123. path = PATH_DELIM;
  124. }
  125. host = host.toLowerCase();
  126. String defaultPath = path;
  127. int lastSlashIndex = defaultPath.lastIndexOf(PATH_DELIM);
  128. if (lastSlashIndex >= 0) {
  129. if (lastSlashIndex == 0) {
  130. //Do not remove the very first slash
  131. lastSlashIndex = 1;
  132. }
  133. defaultPath = defaultPath.substring(0, lastSlashIndex);
  134. }
  135. HeaderElement[] headerElements = null;
  136. boolean isNetscapeCookie = false;
  137. int i1 = header.toLowerCase().indexOf("expires=");
  138. if (i1 != -1) {
  139. i1 += "expires=".length();
  140. int i2 = header.indexOf(";", i1);
  141. if (i2 == -1) {
  142. i2 = header.length();
  143. }
  144. try {
  145. DateParser.parseDate(header.substring(i1, i2), this.datepatterns);
  146. isNetscapeCookie = true;
  147. } catch (DateParseException e) {
  148. // Does not look like a valid expiry date
  149. }
  150. }
  151. if (isNetscapeCookie) {
  152. headerElements = new HeaderElement[] {
  153. new HeaderElement(header.toCharArray())
  154. };
  155. } else {
  156. headerElements = HeaderElement.parseElements(header.toCharArray());
  157. }
  158. Cookie[] cookies = new Cookie[headerElements.length];
  159. for (int i = 0; i < headerElements.length; i++) {
  160. HeaderElement headerelement = headerElements[i];
  161. Cookie cookie = null;
  162. try {
  163. cookie = new Cookie(host,
  164. headerelement.getName(),
  165. headerelement.getValue(),
  166. defaultPath,
  167. null,
  168. false);
  169. } catch (IllegalArgumentException e) {
  170. throw new MalformedCookieException(e.getMessage());
  171. }
  172. // cycle through the parameters
  173. NameValuePair[] parameters = headerelement.getParameters();
  174. // could be null. In case only a header element and no parameters.
  175. if (parameters != null) {
  176. for (int j = 0; j < parameters.length; j++) {
  177. parseAttribute(parameters[j], cookie);
  178. }
  179. }
  180. cookies[i] = cookie;
  181. }
  182. return cookies;
  183. }
  184. /**
  185. * Parse the <tt>"Set-Cookie"</tt> {@link Header} into an array of {@link
  186. * Cookie}s.
  187. *
  188. * <P>The syntax for the Set-Cookie response header is:
  189. *
  190. * <PRE>
  191. * set-cookie = "Set-Cookie:" cookies
  192. * cookies = 1#cookie
  193. * cookie = NAME "=" VALUE * (";" cookie-av)
  194. * NAME = attr
  195. * VALUE = value
  196. * cookie-av = "Comment" "=" value
  197. * | "Domain" "=" value
  198. * | "Max-Age" "=" value
  199. * | "Path" "=" value
  200. * | "Secure"
  201. * | "Version" "=" 1*DIGIT
  202. * </PRE>
  203. *
  204. * @param host the host from which the <tt>Set-Cookie</tt> header was
  205. * received
  206. * @param port the port from which the <tt>Set-Cookie</tt> header was
  207. * received
  208. * @param path the path from which the <tt>Set-Cookie</tt> header was
  209. * received
  210. * @param secure <tt>true</tt> when the <tt>Set-Cookie</tt> header was
  211. * received over secure conection
  212. * @param header the <tt>Set-Cookie</tt> received from the server
  213. * @return an array of <tt>Cookie</tt>s parsed from the <tt>"Set-Cookie"
  214. * </tt> header
  215. * @throws MalformedCookieException if an exception occurs during parsing
  216. */
  217. public Cookie[] parse(
  218. String host, int port, String path, boolean secure, final Header header)
  219. throws MalformedCookieException {
  220. LOG.trace("enter CookieSpecBase.parse("
  221. + "String, port, path, boolean, String)");
  222. if (header == null) {
  223. throw new IllegalArgumentException("Header may not be null.");
  224. }
  225. return parse(host, port, path, secure, header.getValue());
  226. }
  227. /**
  228. * Parse the cookie attribute and update the corresponsing {@link Cookie}
  229. * properties.
  230. *
  231. * @param attribute {@link HeaderElement} cookie attribute from the
  232. * <tt>Set- Cookie</tt>
  233. * @param cookie {@link Cookie} to be updated
  234. * @throws MalformedCookieException if an exception occurs during parsing
  235. */
  236. public void parseAttribute(
  237. final NameValuePair attribute, final Cookie cookie)
  238. throws MalformedCookieException {
  239. if (attribute == null) {
  240. throw new IllegalArgumentException("Attribute may not be null.");
  241. }
  242. if (cookie == null) {
  243. throw new IllegalArgumentException("Cookie may not be null.");
  244. }
  245. final String paramName = attribute.getName().toLowerCase();
  246. String paramValue = attribute.getValue();
  247. if (paramName.equals("path")) {
  248. if ((paramValue == null) || (paramValue.trim().equals(""))) {
  249. paramValue = "/";
  250. }
  251. cookie.setPath(paramValue);
  252. cookie.setPathAttributeSpecified(true);
  253. } else if (paramName.equals("domain")) {
  254. if (paramValue == null) {
  255. throw new MalformedCookieException(
  256. "Missing value for domain attribute");
  257. }
  258. if (paramValue.trim().equals("")) {
  259. throw new MalformedCookieException(
  260. "Blank value for domain attribute");
  261. }
  262. cookie.setDomain(paramValue);
  263. cookie.setDomainAttributeSpecified(true);
  264. } else if (paramName.equals("max-age")) {
  265. if (paramValue == null) {
  266. throw new MalformedCookieException(
  267. "Missing value for max-age attribute");
  268. }
  269. int age;
  270. try {
  271. age = Integer.parseInt(paramValue);
  272. } catch (NumberFormatException e) {
  273. throw new MalformedCookieException ("Invalid max-age "
  274. + "attribute: " + e.getMessage());
  275. }
  276. cookie.setExpiryDate(
  277. new Date(System.currentTimeMillis() + age * 1000L));
  278. } else if (paramName.equals("secure")) {
  279. cookie.setSecure(true);
  280. } else if (paramName.equals("comment")) {
  281. cookie.setComment(paramValue);
  282. } else if (paramName.equals("expires")) {
  283. if (paramValue == null) {
  284. throw new MalformedCookieException(
  285. "Missing value for expires attribute");
  286. }
  287. try {
  288. cookie.setExpiryDate(DateParser.parseDate(paramValue, this.datepatterns));
  289. } catch (DateParseException dpe) {
  290. LOG.debug("Error parsing cookie date", dpe);
  291. throw new MalformedCookieException(
  292. "Unable to parse expiration date parameter: "
  293. + paramValue);
  294. }
  295. } else {
  296. if (LOG.isDebugEnabled()) {
  297. LOG.debug("Unrecognized cookie attribute: "
  298. + attribute.toString());
  299. }
  300. }
  301. }
  302. public Collection getValidDateFormats() {
  303. return this.datepatterns;
  304. }
  305. public void setValidDateFormats(final Collection datepatterns) {
  306. this.datepatterns = datepatterns;
  307. }
  308. /**
  309. * Performs most common {@link Cookie} validation
  310. *
  311. * @param host the host from which the {@link Cookie} was received
  312. * @param port the port from which the {@link Cookie} was received
  313. * @param path the path from which the {@link Cookie} was received
  314. * @param secure <tt>true</tt> when the {@link Cookie} was received using a
  315. * secure connection
  316. * @param cookie The cookie to validate.
  317. * @throws MalformedCookieException if an exception occurs during
  318. * validation
  319. */
  320. public void validate(String host, int port, String path,
  321. boolean secure, final Cookie cookie)
  322. throws MalformedCookieException {
  323. LOG.trace("enter CookieSpecBase.validate("
  324. + "String, port, path, boolean, Cookie)");
  325. if (host == null) {
  326. throw new IllegalArgumentException(
  327. "Host of origin may not be null");
  328. }
  329. if (host.trim().equals("")) {
  330. throw new IllegalArgumentException(
  331. "Host of origin may not be blank");
  332. }
  333. if (port < 0) {
  334. throw new IllegalArgumentException("Invalid port: " + port);
  335. }
  336. if (path == null) {
  337. throw new IllegalArgumentException(
  338. "Path of origin may not be null.");
  339. }
  340. if (path.trim().equals("")) {
  341. path = PATH_DELIM;
  342. }
  343. host = host.toLowerCase();
  344. // check version
  345. if (cookie.getVersion() < 0) {
  346. throw new MalformedCookieException ("Illegal version number "
  347. + cookie.getValue());
  348. }
  349. // security check... we musn't allow the server to give us an
  350. // invalid domain scope
  351. // Validate the cookies domain attribute. NOTE: Domains without
  352. // any dots are allowed to support hosts on private LANs that don't
  353. // have DNS names. Since they have no dots, to domain-match the
  354. // request-host and domain must be identical for the cookie to sent
  355. // back to the origin-server.
  356. if (host.indexOf(".") >= 0) {
  357. // Not required to have at least two dots. RFC 2965.
  358. // A Set-Cookie2 with Domain=ajax.com will be accepted.
  359. // domain must match host
  360. if (!host.endsWith(cookie.getDomain())) {
  361. String s = cookie.getDomain();
  362. if (s.startsWith(".")) {
  363. s = s.substring(1, s.length());
  364. }
  365. if (!host.equals(s)) {
  366. throw new MalformedCookieException(
  367. "Illegal domain attribute \"" + cookie.getDomain()
  368. + "\". Domain of origin: \"" + host + "\"");
  369. }
  370. }
  371. } else {
  372. if (!host.equals(cookie.getDomain())) {
  373. throw new MalformedCookieException(
  374. "Illegal domain attribute \"" + cookie.getDomain()
  375. + "\". Domain of origin: \"" + host + "\"");
  376. }
  377. }
  378. // another security check... we musn't allow the server to give us a
  379. // cookie that doesn't match this path
  380. if (!path.startsWith(cookie.getPath())) {
  381. throw new MalformedCookieException(
  382. "Illegal path attribute \"" + cookie.getPath()
  383. + "\". Path of origin: \"" + path + "\"");
  384. }
  385. }
  386. /**
  387. * Return <tt>true</tt> if the cookie should be submitted with a request
  388. * with given attributes, <tt>false</tt> otherwise.
  389. * @param host the host to which the request is being submitted
  390. * @param port the port to which the request is being submitted (ignored)
  391. * @param path the path to which the request is being submitted
  392. * @param secure <tt>true</tt> if the request is using a secure connection
  393. * @param cookie {@link Cookie} to be matched
  394. * @return true if the cookie matches the criterium
  395. */
  396. public boolean match(String host, int port, String path,
  397. boolean secure, final Cookie cookie) {
  398. LOG.trace("enter CookieSpecBase.match("
  399. + "String, int, String, boolean, Cookie");
  400. if (host == null) {
  401. throw new IllegalArgumentException(
  402. "Host of origin may not be null");
  403. }
  404. if (host.trim().equals("")) {
  405. throw new IllegalArgumentException(
  406. "Host of origin may not be blank");
  407. }
  408. if (port < 0) {
  409. throw new IllegalArgumentException("Invalid port: " + port);
  410. }
  411. if (path == null) {
  412. throw new IllegalArgumentException(
  413. "Path of origin may not be null.");
  414. }
  415. if (cookie == null) {
  416. throw new IllegalArgumentException("Cookie may not be null");
  417. }
  418. if (path.trim().equals("")) {
  419. path = PATH_DELIM;
  420. }
  421. host = host.toLowerCase();
  422. if (cookie.getDomain() == null) {
  423. LOG.warn("Invalid cookie state: domain not specified");
  424. return false;
  425. }
  426. if (cookie.getPath() == null) {
  427. LOG.warn("Invalid cookie state: path not specified");
  428. return false;
  429. }
  430. return
  431. // only add the cookie if it hasn't yet expired
  432. (cookie.getExpiryDate() == null
  433. || cookie.getExpiryDate().after(new Date()))
  434. // and the domain pattern matches
  435. && (domainMatch(host, cookie.getDomain()))
  436. // and the path is null or matching
  437. && (pathMatch(path, cookie.getPath()))
  438. // and if the secure flag is set, only if the request is
  439. // actually secure
  440. && (cookie.getSecure() ? secure : true);
  441. }
  442. /**
  443. * Performs domain-match as implemented in common browsers.
  444. * @param host The target host.
  445. * @param domain The cookie domain attribute.
  446. * @return true if the specified host matches the given domain.
  447. */
  448. public boolean domainMatch(final String host, final String domain) {
  449. return host.endsWith(domain);
  450. }
  451. /**
  452. * Performs path-match as implemented in common browsers.
  453. * @param path The target path.
  454. * @param topmostPath The cookie path attribute.
  455. * @return true if the paths match
  456. */
  457. public boolean pathMatch(final String path, final String topmostPath) {
  458. boolean match = path.startsWith (topmostPath);
  459. // if there is a match and these values are not exactly the same we have
  460. // to make sure we're not matcing "/foobar" and "/foo"
  461. if (match && path.length() != topmostPath.length()) {
  462. if (!topmostPath.endsWith(PATH_DELIM)) {
  463. match = (path.charAt(topmostPath.length()) == PATH_DELIM_CHAR);
  464. }
  465. }
  466. return match;
  467. }
  468. /**
  469. * Return an array of {@link Cookie}s that should be submitted with a
  470. * request with given attributes, <tt>false</tt> otherwise.
  471. * @param host the host to which the request is being submitted
  472. * @param port the port to which the request is being submitted (currently
  473. * ignored)
  474. * @param path the path to which the request is being submitted
  475. * @param secure <tt>true</tt> if the request is using a secure protocol
  476. * @param cookies an array of <tt>Cookie</tt>s to be matched
  477. * @return an array of <tt>Cookie</tt>s matching the criterium
  478. */
  479. public Cookie[] match(String host, int port, String path,
  480. boolean secure, final Cookie cookies[]) {
  481. LOG.trace("enter CookieSpecBase.match("
  482. + "String, int, String, boolean, Cookie[])");
  483. if (cookies == null) {
  484. return null;
  485. }
  486. List matching = new LinkedList();
  487. for (int i = 0; i < cookies.length; i++) {
  488. if (match(host, port, path, secure, cookies[i])) {
  489. addInPathOrder(matching, cookies[i]);
  490. }
  491. }
  492. return (Cookie[]) matching.toArray(new Cookie[matching.size()]);
  493. }
  494. /**
  495. * Adds the given cookie into the given list in descending path order. That
  496. * is, more specific path to least specific paths. This may not be the
  497. * fastest algorythm, but it'll work OK for the small number of cookies
  498. * we're generally dealing with.
  499. *
  500. * @param list - the list to add the cookie to
  501. * @param addCookie - the Cookie to add to list
  502. */
  503. private static void addInPathOrder(List list, Cookie addCookie) {
  504. int i = 0;
  505. for (i = 0; i < list.size(); i++) {
  506. Cookie c = (Cookie) list.get(i);
  507. if (addCookie.compare(addCookie, c) > 0) {
  508. break;
  509. }
  510. }
  511. list.add(i, addCookie);
  512. }
  513. /**
  514. * Return a string suitable for sending in a <tt>"Cookie"</tt> header
  515. * @param cookie a {@link Cookie} to be formatted as string
  516. * @return a string suitable for sending in a <tt>"Cookie"</tt> header.
  517. */
  518. public String formatCookie(Cookie cookie) {
  519. LOG.trace("enter CookieSpecBase.formatCookie(Cookie)");
  520. if (cookie == null) {
  521. throw new IllegalArgumentException("Cookie may not be null");
  522. }
  523. StringBuffer buf = new StringBuffer();
  524. buf.append(cookie.getName());
  525. buf.append("=");
  526. String s = cookie.getValue();
  527. if (s != null) {
  528. buf.append(s);
  529. }
  530. return buf.toString();
  531. }
  532. /**
  533. * Create a <tt>"Cookie"</tt> header value containing all {@link Cookie}s in
  534. * <i>cookies</i> suitable for sending in a <tt>"Cookie"</tt> header
  535. * @param cookies an array of {@link Cookie}s to be formatted
  536. * @return a string suitable for sending in a Cookie header.
  537. * @throws IllegalArgumentException if an input parameter is illegal
  538. */
  539. public String formatCookies(Cookie[] cookies)
  540. throws IllegalArgumentException {
  541. LOG.trace("enter CookieSpecBase.formatCookies(Cookie[])");
  542. if (cookies == null) {
  543. throw new IllegalArgumentException("Cookie array may not be null");
  544. }
  545. if (cookies.length == 0) {
  546. throw new IllegalArgumentException("Cookie array may not be empty");
  547. }
  548. StringBuffer buffer = new StringBuffer();
  549. for (int i = 0; i < cookies.length; i++) {
  550. if (i > 0) {
  551. buffer.append("; ");
  552. }
  553. buffer.append(formatCookie(cookies[i]));
  554. }
  555. return buffer.toString();
  556. }
  557. /**
  558. * Create a <tt>"Cookie"</tt> {@link Header} containing all {@link Cookie}s
  559. * in <i>cookies</i>.
  560. * @param cookies an array of {@link Cookie}s to be formatted as a <tt>"
  561. * Cookie"</tt> header
  562. * @return a <tt>"Cookie"</tt> {@link Header}.
  563. */
  564. public Header formatCookieHeader(Cookie[] cookies) {
  565. LOG.trace("enter CookieSpecBase.formatCookieHeader(Cookie[])");
  566. return new Header("Cookie", formatCookies(cookies));
  567. }
  568. /**
  569. * Create a <tt>"Cookie"</tt> {@link Header} containing the {@link Cookie}.
  570. * @param cookie <tt>Cookie</tt>s to be formatted as a <tt>Cookie</tt>
  571. * header
  572. * @return a Cookie header.
  573. */
  574. public Header formatCookieHeader(Cookie cookie) {
  575. LOG.trace("enter CookieSpecBase.formatCookieHeader(Cookie)");
  576. return new Header("Cookie", formatCookie(cookie));
  577. }
  578. }