View Javadoc
1   /*
2    * Copyright (c) 2002-2026 Gargoyle Software Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * https://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  package org.htmlunit.httpclient;
16  
17  import java.net.IDN;
18  import java.net.MalformedURLException;
19  import java.net.URL;
20  import java.util.ArrayList;
21  import java.util.Collection;
22  import java.util.List;
23  import java.util.Set;
24  
25  import org.apache.http.conn.util.InetAddressUtils;
26  import org.apache.http.conn.util.PublicSuffixMatcher;
27  import org.apache.http.conn.util.PublicSuffixMatcherLoader;
28  import org.apache.http.cookie.ClientCookie;
29  import org.apache.http.cookie.Cookie;
30  import org.apache.http.cookie.CookieOrigin;
31  import org.apache.http.cookie.CookieSpec;
32  import org.apache.http.cookie.MalformedCookieException;
33  import org.apache.http.impl.cookie.BasicClientCookie;
34  import org.apache.http.message.BufferedHeader;
35  import org.apache.http.util.CharArrayBuffer;
36  import org.htmlunit.BrowserVersion;
37  import org.htmlunit.util.UrlUtils;
38  
39  /**
40   * Helper methods to convert from/to HttpClient.
41   *
42   * @author Ronald Brill
43   */
44  public final class HttpClientConverter {
45  
46      private static final PublicSuffixMatcher PUBLIC_SUFFIX_MATCHER = PublicSuffixMatcherLoader.getDefault();
47  
48      private HttpClientConverter() {
49          // util class
50      }
51  
52      /**
53       * Returns the "registrable domain" (effective TLD+1) for the given host, per the
54       * public suffix list (https://publicsuffix.org/). Backed by Apache HttpClient's own
55       * {@link PublicSuffixMatcher}, already on the classpath as part of the httpclient
56       * dependency this class already uses elsewhere for cookie handling - no new
57       * dependency needed, and it avoids the wrong results a naive "last two labels"
58       * heuristic would give for domains under multi-part public suffixes (e.g. a
59       * {@code co.uk}-style suffix, or a private-section entry like {@code github.io}).
60       * <p>
61       * Note this is deliberately unrelated to {@link HtmlUnitDomainHandler}'s cookie
62       * domain-matching: RFC 6265 cookie domain-match is a simple suffix/subdomain check
63       * against a single cookie-supplied domain, not a registrable-domain computation
64       * between two hosts, so there is nothing to share between the two.
65       * </p>
66       * @param host the host part of a URL
67       * @return the registrable domain, or the host itself if it doesn't have one (e.g.
68       *         a bare public suffix, a single-label host like {@code localhost}, or an
69       *         IP address - which per the HTML "obtain a site" algorithm is always its
70       *         own site, never run through the public suffix list at all)
71       */
72      public static String registrableDomain(final String host) {
73          if (InetAddressUtils.isIPv4Address(host) || InetAddressUtils.isIPv6Address(host)) {
74              return host;
75          }
76          final String domainRoot = IDN.toASCII(PUBLIC_SUFFIX_MATCHER.getDomainRoot(host));
77          return domainRoot == null ? host : domainRoot;
78      }
79  
80      /**
81       * Helper that builds a CookieOrigin.
82       * @param url the url to be used
83       * @return the new CookieOrigin
84       */
85      public static CookieOrigin buildCookieOrigin(final URL url) {
86          final URL normalizedUrl = replaceForCookieIfNecessary(url);
87  
88          int port = normalizedUrl.getPort();
89          if (port == -1) {
90              port = normalizedUrl.getDefaultPort();
91          }
92  
93          return new CookieOrigin(
94                  normalizedUrl.getHost(),
95                  port,
96                  normalizedUrl.getPath(),
97                  "https".equals(normalizedUrl.getProtocol()));
98      }
99  
100     /**
101      * {@link CookieOrigin} doesn't like empty hosts and negative ports,
102      * but these things happen if we're dealing with a local file.
103      * This method allows us to work around this limitation in HttpClient by feeding it a bogus host and port.
104      *
105      * @param url the URL to replace if necessary
106      * @return the replacement URL, or the original URL if no replacement was necessary
107      */
108     public static URL replaceForCookieIfNecessary(URL url) {
109         final String protocol = url.getProtocol();
110         final boolean file = "file".equals(protocol);
111         if (file) {
112             try {
113                 url = UrlUtils.getUrlWithNewHostAndPort(url,
114                         HtmlUnitBrowserCompatCookieSpec.LOCAL_FILESYSTEM_DOMAIN, 0);
115             }
116             catch (final MalformedURLException e) {
117                 throw new RuntimeException(e);
118             }
119         }
120         return url;
121     }
122 
123     /**
124      * Parses the specified cookie string into HtmlUnit cookie objects.
125      *
126      * @param cookieString the {@code Set-Cookie} header value to parse
127      * @param pageUrl the URL of the page from which the cookie originates
128      * @param browserVersion the browser version used to determine cookie parsing behavior
129      * @return a list of parsed {@link org.htmlunit.http.Cookie} instances
130      * @throws MalformedCookieException if the cookie string does not conform to the cookie specification
131      */
132     public static List<org.htmlunit.http.Cookie> parseCookie(final String cookieString, final URL pageUrl,
133                                                              final BrowserVersion browserVersion)
134             throws MalformedCookieException {
135         final CharArrayBuffer buffer = new CharArrayBuffer(cookieString.length() + 22);
136         buffer.append("Set-Cookie: ");
137         buffer.append(cookieString);
138 
139         final CookieSpec cookieSpec = new HtmlUnitBrowserCompatCookieSpec(browserVersion);
140         final List<Cookie> cookies = cookieSpec.parse(new BufferedHeader(buffer), buildCookieOrigin(pageUrl));
141 
142         final List<org.htmlunit.http.Cookie> htmlUnitCookies = new ArrayList<>(cookies.size());
143         for (final Cookie cookie : cookies) {
144             final org.htmlunit.http.Cookie htmlUnitCookie = new HttpClientCookie((ClientCookie) cookie);
145             htmlUnitCookies.add(htmlUnitCookie);
146         }
147         return htmlUnitCookies;
148     }
149 
150     /**
151      * Converts the specified collection of cookies into a collection of HttpClient cookies.
152      * @param cookies the cookies to be converted
153      * @return the specified cookies, as HttpClient cookies
154      */
155     public static List<Cookie> toHttpClient(final Collection<org.htmlunit.http.Cookie> cookies) {
156         final ArrayList<Cookie> array = new ArrayList<>(cookies.size());
157         for (final org.htmlunit.http.Cookie cookie : cookies) {
158             array.add(toHttpClient(cookie));
159         }
160         return array;
161     }
162 
163     /**
164      * Adds all matching cookies to the provided set.
165      * @param cookies the cookies to select from
166      * @param normalizedUrl the url to match against
167      * @param browserVersion the {@link BrowserVersion}
168      * @param matches the set to add
169      */
170     public static void addMatching(final Set<org.htmlunit.http.Cookie> cookies,
171             final URL normalizedUrl, final BrowserVersion browserVersion,
172             final Set<org.htmlunit.http.Cookie> matches) {
173         if (!cookies.isEmpty()) {
174             final CookieOrigin cookieOrigin = HttpClientConverter.buildCookieOrigin(normalizedUrl);
175             final CookieSpec cookieSpec = new HtmlUnitBrowserCompatCookieSpec(browserVersion);
176             for (final org.htmlunit.http.Cookie cookie : cookies) {
177                 if (cookieSpec.match(toHttpClient(cookie), cookieOrigin)) {
178                     matches.add(cookie);
179                 }
180             }
181         }
182     }
183 
184     private static ClientCookie toHttpClient(final org.htmlunit.http.Cookie cookie) {
185         if (cookie instanceof HttpClientCookie) {
186             return ((HttpClientCookie) cookie).getHttpClientCookie();
187         }
188 
189         final BasicClientCookie httpClientCookie = new BasicClientCookie(cookie.getName(),
190                 cookie.getValue() == null ? "" : cookie.getValue());
191 
192         httpClientCookie.setDomain(cookie.getDomain());
193         // BasicDomainHandler.match(Cookie, CookieOrigin) checks the attribute also (see #333)
194         httpClientCookie.setAttribute(ClientCookie.DOMAIN_ATTR, cookie.getDomain());
195 
196         httpClientCookie.setPath(cookie.getPath());
197         httpClientCookie.setExpiryDate(cookie.getExpires());
198 
199         httpClientCookie.setSecure(cookie.isSecure());
200         if (cookie.isHttpOnly()) {
201             httpClientCookie.setAttribute("httponly", "true");
202         }
203 
204         if (cookie.getSameSite() != null) {
205             httpClientCookie.setAttribute("samesite", cookie.getSameSite());
206         }
207 
208         return httpClientCookie;
209     }
210 }