001package org.jsoup.helper; 002 003import org.jsoup.Connection; 004import org.jsoup.HttpStatusException; 005import org.jsoup.Progress; 006import org.jsoup.UnsupportedMimeTypeException; 007import org.jsoup.internal.ControllableInputStream; 008import org.jsoup.internal.StringUtil; 009import org.jsoup.nodes.Document; 010import org.jsoup.parser.Parser; 011import org.jsoup.parser.StreamParser; 012import org.jspecify.annotations.Nullable; 013 014import javax.net.ssl.SSLContext; 015import javax.net.ssl.SSLSocketFactory; 016import java.io.BufferedInputStream; 017import java.io.BufferedReader; 018import java.io.BufferedWriter; 019import java.io.ByteArrayInputStream; 020import java.io.IOException; 021import java.io.InputStream; 022import java.io.InputStreamReader; 023import java.io.OutputStream; 024import java.io.OutputStreamWriter; 025import java.io.UncheckedIOException; 026import java.net.CookieManager; 027import java.net.CookieStore; 028import java.net.InetSocketAddress; 029import java.net.MalformedURLException; 030import java.net.Proxy; 031import java.net.URL; 032import java.net.URLEncoder; 033import java.nio.Buffer; 034import java.nio.ByteBuffer; 035import java.nio.charset.Charset; 036import java.nio.charset.IllegalCharsetNameException; 037import java.nio.charset.StandardCharsets; 038import java.util.ArrayList; 039import java.util.Collection; 040import java.util.Collections; 041import java.util.LinkedHashMap; 042import java.util.List; 043import java.util.Map; 044import java.util.concurrent.locks.ReentrantLock; 045import java.util.regex.Pattern; 046import java.util.zip.GZIPInputStream; 047import java.util.zip.Inflater; 048import java.util.zip.InflaterInputStream; 049 050import static org.jsoup.Connection.Method.HEAD; 051import static org.jsoup.helper.DataUtil.UTF_8; 052import static org.jsoup.internal.Normalizer.lowerCase; 053import static org.jsoup.internal.SharedConstants.DefaultBufferSize; 054 055/** 056 * Implementation of {@link Connection}. 057 * @see org.jsoup.Jsoup#connect(String) 058 */ 059@SuppressWarnings("CharsetObjectCanBeUsed") 060public class HttpConnection implements Connection { 061 public static final String CONTENT_ENCODING = "Content-Encoding"; 062 /** 063 * Many users would get caught by not setting a user-agent and therefore getting different responses on their desktop 064 * vs in jsoup, which would otherwise default to {@code Java}. So by default, use a desktop UA. 065 */ 066 public static final String DEFAULT_UA = 067 "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"; 068 private static final String USER_AGENT = "User-Agent"; 069 public static final String CONTENT_TYPE = "Content-Type"; 070 public static final String MULTIPART_FORM_DATA = "multipart/form-data"; 071 public static final String FORM_URL_ENCODED = "application/x-www-form-urlencoded"; 072 private static final int HTTP_TEMP_REDIR = 307; // http/1.1 temporary redirect, not in Java's set. 073 static final String DefaultUploadType = "application/octet-stream"; 074 private static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1"); 075 076 private HttpConnection.Request req; 077 private Connection.@Nullable Response res; 078 @Nullable Object client; // The HttpClient for this Connection, if via the HttpClientExecutor 079 @Nullable RequestAuthenticator lastAuth; // The previous Authenticator used by this Connection, if via the HttpClientExecutor 080 081 /** 082 Create a new Connection, with the request URL specified. 083 @param url the URL to fetch from 084 @return a new Connection object 085 */ 086 public static Connection connect(String url) { 087 Connection con = new HttpConnection(); 088 con.url(url); 089 return con; 090 } 091 092 /** 093 Create a new Connection, with the request URL specified. 094 @param url the URL to fetch from 095 @return a new Connection object 096 */ 097 public static Connection connect(URL url) { 098 Connection con = new HttpConnection(); 099 con.url(url); 100 return con; 101 } 102 103 /** 104 Create a new, empty HttpConnection. 105 */ 106 public HttpConnection() { 107 req = new Request(); 108 req.connection = this; 109 } 110 111 /** 112 Create a new Request by deep-copying an existing Request. Note that the data and body of the original are not 113 copied. All other settings (proxy, parser, cookies, etc) are copied. 114 @param copy the request to copy 115 */ 116 HttpConnection(Request copy) { 117 req = new Request(copy); 118 } 119 120 static String encodeMimeName(String val) { 121 return val.replace("\"", "%22"); 122 } 123 124 @Override 125 public Connection newRequest() { 126 // copy the prototype request for the different settings, cookie manager, etc 127 return new HttpConnection(req); 128 } 129 130 /** Create a new Connection that just wraps the provided Request and Response */ 131 private HttpConnection(Request req, Response res) { 132 this.req = req; 133 this.res = res; 134 } 135 136 @Override 137 public Connection url(URL url) { 138 req.url(url); 139 return this; 140 } 141 142 @Override 143 public Connection url(String url) { 144 Validate.notEmptyParam(url, "url"); 145 try { 146 req.url(new URL(url)); 147 } catch (MalformedURLException e) { 148 throw new IllegalArgumentException(String.format("The supplied URL, '%s', is malformed. Make sure it is an absolute URL, and starts with 'http://' or 'https://'. See https://jsoup.org/cookbook/extracting-data/working-with-urls", url), e); 149 } 150 return this; 151 } 152 153 @Override 154 public Connection proxy(@Nullable Proxy proxy) { 155 req.proxy(proxy); 156 return this; 157 } 158 159 @Override 160 public Connection proxy(String host, int port) { 161 req.proxy(host, port); 162 return this; 163 } 164 165 @Override 166 public Connection userAgent(String userAgent) { 167 Validate.notNullParam(userAgent, "userAgent"); 168 req.header(USER_AGENT, userAgent); 169 return this; 170 } 171 172 @Override 173 public Connection timeout(int millis) { 174 req.timeout(millis); 175 return this; 176 } 177 178 @Override 179 public Connection maxBodySize(int bytes) { 180 req.maxBodySize(bytes); 181 return this; 182 } 183 184 @Override 185 public Connection followRedirects(boolean followRedirects) { 186 req.followRedirects(followRedirects); 187 return this; 188 } 189 190 @Override 191 public Connection referrer(String referrer) { 192 Validate.notNullParam(referrer, "referrer"); 193 req.header("Referer", referrer); 194 return this; 195 } 196 197 @Override 198 public Connection method(Method method) { 199 req.method(method); 200 return this; 201 } 202 203 @Override 204 public Connection ignoreHttpErrors(boolean ignoreHttpErrors) { 205 req.ignoreHttpErrors(ignoreHttpErrors); 206 return this; 207 } 208 209 @Override 210 public Connection ignoreContentType(boolean ignoreContentType) { 211 req.ignoreContentType(ignoreContentType); 212 return this; 213 } 214 215 @Override 216 public Connection data(String key, String value) { 217 req.data(KeyVal.create(key, value)); 218 return this; 219 } 220 221 @Override 222 @Deprecated 223 public Connection sslSocketFactory(SSLSocketFactory sslSocketFactory) { 224 req.sslSocketFactory(sslSocketFactory); 225 return this; 226 } 227 228 @Override 229 public Connection sslContext(SSLContext sslContext) { 230 req.sslContext(sslContext); 231 return this; 232 } 233 234 @Override 235 public Connection data(String key, String filename, InputStream inputStream) { 236 req.data(KeyVal.create(key, filename, inputStream)); 237 return this; 238 } 239 240 @Override 241 public Connection data(String key, String filename, InputStream inputStream, String contentType) { 242 req.data(KeyVal.create(key, filename, inputStream).contentType(contentType)); 243 return this; 244 } 245 246 @Override 247 public Connection data(Map<String, String> data) { 248 Validate.notNullParam(data, "data"); 249 for (Map.Entry<String, String> entry : data.entrySet()) { 250 req.data(KeyVal.create(entry.getKey(), entry.getValue())); 251 } 252 return this; 253 } 254 255 @Override 256 public Connection data(String... keyvals) { 257 Validate.notNullParam(keyvals, "keyvals"); 258 Validate.isTrue(keyvals.length %2 == 0, "Must supply an even number of key value pairs"); 259 for (int i = 0; i < keyvals.length; i += 2) { 260 String key = keyvals[i]; 261 String value = keyvals[i+1]; 262 Validate.notEmpty(key, "Data key must not be empty"); 263 Validate.notNull(value, "Data value must not be null"); 264 req.data(KeyVal.create(key, value)); 265 } 266 return this; 267 } 268 269 @Override 270 public Connection data(Collection<Connection.KeyVal> data) { 271 Validate.notNullParam(data, "data"); 272 for (Connection.KeyVal entry: data) { 273 req.data(entry); 274 } 275 return this; 276 } 277 278 @Override 279 public Connection.@Nullable KeyVal data(String key) { 280 Validate.notEmptyParam(key, "key"); 281 for (Connection.KeyVal keyVal : request().data()) { 282 if (keyVal.key().equals(key)) 283 return keyVal; 284 } 285 return null; 286 } 287 288 @Override 289 public Connection requestBody(String body) { 290 req.requestBody(body); 291 return this; 292 } 293 294 @Override 295 public Connection requestBodyStream(InputStream stream) { 296 req.requestBodyStream(stream); 297 return this; 298 } 299 300 @Override 301 public Connection header(String name, String value) { 302 req.header(name, value); 303 return this; 304 } 305 306 @Override 307 public Connection headers(Map<String,String> headers) { 308 Validate.notNullParam(headers, "headers"); 309 for (Map.Entry<String,String> entry : headers.entrySet()) { 310 req.header(entry.getKey(),entry.getValue()); 311 } 312 return this; 313 } 314 315 @Override 316 public Connection cookie(String name, String value) { 317 req.cookie(name, value); 318 return this; 319 } 320 321 @Override 322 public Connection cookies(Map<String, String> cookies) { 323 Validate.notNullParam(cookies, "cookies"); 324 for (Map.Entry<String, String> entry : cookies.entrySet()) { 325 req.cookie(entry.getKey(), entry.getValue()); 326 } 327 return this; 328 } 329 330 @Override 331 public Connection cookieStore(CookieStore cookieStore) { 332 // create a new cookie manager using the new store 333 req.cookieManager = new CookieManager(cookieStore, null); 334 return this; 335 } 336 337 @Override 338 public CookieStore cookieStore() { 339 return req.cookieManager.getCookieStore(); 340 } 341 342 @Override 343 public Connection parser(Parser parser) { 344 req.parser(parser); 345 return this; 346 } 347 348 @Override 349 public Document get() throws IOException { 350 req.method(Method.GET); 351 execute(); 352 Validate.notNull(res); 353 return res.parse(); 354 } 355 356 @Override 357 public Document post() throws IOException { 358 req.method(Method.POST); 359 execute(); 360 Validate.notNull(res); 361 return res.parse(); 362 } 363 364 @Override 365 public Connection.Response execute() throws IOException { 366 res = Response.execute(req); 367 return res; 368 } 369 370 @Override 371 public Connection.Request request() { 372 return req; 373 } 374 375 @Override 376 public Connection request(Connection.Request request) { 377 req = (HttpConnection.Request) request; // will throw a class-cast exception if the user has extended some but not all of Connection; that's desired 378 return this; 379 } 380 381 @Override 382 public Connection.Response response() { 383 if (res == null) { 384 throw new IllegalArgumentException("You must execute the request before getting a response."); 385 } 386 return res; 387 } 388 389 @Override 390 public Connection response(Connection.Response response) { 391 res = response; 392 return this; 393 } 394 395 @Override 396 public Connection postDataCharset(String charset) { 397 req.postDataCharset(charset); 398 return this; 399 } 400 401 @Override public Connection auth(@Nullable RequestAuthenticator authenticator) { 402 req.auth(authenticator); 403 return this; 404 } 405 406 @Override public Connection onResponseProgress(Progress<Connection.Response> handler) { 407 req.responseProgress = handler; 408 return this; 409 } 410 411 @SuppressWarnings("unchecked") 412 private static abstract class Base<T extends Connection.Base<T>> implements Connection.Base<T> { 413 private static final URL UnsetUrl; // only used if you created a new Request() 414 static { 415 try { 416 UnsetUrl = new URL("http://undefined/"); 417 } catch (MalformedURLException e) { 418 throw new IllegalStateException(e); 419 } 420 } 421 422 URL url = UnsetUrl; 423 Method method = Method.GET; 424 Map<String, List<String>> headers; 425 Map<String, String> cookies; 426 427 private Base() { 428 headers = new LinkedHashMap<>(); 429 cookies = new LinkedHashMap<>(); 430 } 431 432 private Base(Base<T> copy) { 433 url = copy.url; // unmodifiable object 434 method = copy.method; 435 headers = new LinkedHashMap<>(); 436 for (Map.Entry<String, List<String>> entry : copy.headers.entrySet()) { 437 headers.put(entry.getKey(), new ArrayList<>(entry.getValue())); 438 } 439 cookies = new LinkedHashMap<>(); cookies.putAll(copy.cookies); // just holds strings 440 } 441 442 @Override 443 public URL url() { 444 if (url == UnsetUrl) 445 throw new IllegalArgumentException("URL not set. Make sure to call #url(...) before executing the request."); 446 return url; 447 } 448 449 @Override 450 public T url(URL url) { 451 Validate.notNullParam(url, "url"); 452 this.url = new UrlBuilder(url).build(); 453 return (T) this; 454 } 455 456 @Override 457 public Method method() { 458 return method; 459 } 460 461 @Override 462 public T method(Method method) { 463 Validate.notNullParam(method, "method"); 464 this.method = method; 465 return (T) this; 466 } 467 468 @Override @Nullable 469 public String header(String name) { 470 Validate.notNullParam(name, "name"); 471 List<String> vals = getHeadersCaseInsensitive(name); 472 if (!vals.isEmpty()) { 473 // https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2 474 return StringUtil.join(vals, ", "); 475 } 476 477 return null; 478 } 479 480 @Override 481 public T addHeader(String name, @Nullable String value) { 482 Validate.notEmptyParam(name, "name"); 483 value = value == null ? "" : value; 484 485 List<String> values = headers(name); 486 if (values.isEmpty()) { 487 values = new ArrayList<>(); 488 headers.put(name, values); 489 } 490 values.add(value); 491 492 return (T) this; 493 } 494 495 @Override 496 public List<String> headers(String name) { 497 Validate.notEmptyParam(name, "name"); 498 return getHeadersCaseInsensitive(name); 499 } 500 501 @Override 502 public T header(String name, String value) { 503 Validate.notEmptyParam(name, "name"); 504 removeHeader(name); // ensures we don't get an "accept-encoding" and an "Accept-Encoding" 505 addHeader(name, value); 506 return (T) this; 507 } 508 509 @Override 510 public boolean hasHeader(String name) { 511 Validate.notEmptyParam(name, "name"); 512 return !getHeadersCaseInsensitive(name).isEmpty(); 513 } 514 515 /** 516 * Test if the request has a header with this value (case-insensitive). 517 */ 518 @Override 519 public boolean hasHeaderWithValue(String name, String value) { 520 Validate.notEmpty(name); 521 Validate.notEmpty(value); 522 List<String> values = headers(name); 523 for (String candidate : values) { 524 if (value.equalsIgnoreCase(candidate)) 525 return true; 526 } 527 return false; 528 } 529 530 @Override 531 public T removeHeader(String name) { 532 Validate.notEmptyParam(name, "name"); 533 Map.Entry<String, List<String>> entry = scanHeaders(name); // remove is case-insensitive too 534 if (entry != null) 535 headers.remove(entry.getKey()); // ensures correct case 536 return (T) this; 537 } 538 539 @Override 540 public Map<String, String> headers() { 541 LinkedHashMap<String, String> map = new LinkedHashMap<>(headers.size()); 542 for (Map.Entry<String, List<String>> entry : headers.entrySet()) { 543 String header = entry.getKey(); 544 List<String> values = entry.getValue(); 545 if (!values.isEmpty()) 546 map.put(header, values.get(0)); 547 } 548 return map; 549 } 550 551 @Override 552 public Map<String, List<String>> multiHeaders() { 553 return headers; 554 } 555 556 private List<String> getHeadersCaseInsensitive(String name) { 557 Validate.notNull(name); 558 559 for (Map.Entry<String, List<String>> entry : headers.entrySet()) { 560 if (name.equalsIgnoreCase(entry.getKey())) 561 return entry.getValue(); 562 } 563 564 return Collections.emptyList(); 565 } 566 567 private Map.@Nullable Entry<String, List<String>> scanHeaders(String name) { 568 String lc = lowerCase(name); 569 for (Map.Entry<String, List<String>> entry : headers.entrySet()) { 570 if (lowerCase(entry.getKey()).equals(lc)) 571 return entry; 572 } 573 return null; 574 } 575 576 @Override 577 public String cookie(String name) { 578 Validate.notEmptyParam(name, "name"); 579 return cookies.get(name); 580 } 581 582 @Override 583 public T cookie(String name, String value) { 584 Validate.notEmptyParam(name, "name"); 585 Validate.notNullParam(value, "value"); 586 cookies.put(name, value); 587 return (T) this; 588 } 589 590 @Override 591 public boolean hasCookie(String name) { 592 Validate.notEmptyParam(name, "name"); 593 return cookies.containsKey(name); 594 } 595 596 @Override 597 public T removeCookie(String name) { 598 Validate.notEmptyParam(name, "name"); 599 cookies.remove(name); 600 return (T) this; 601 } 602 603 @Override 604 public Map<String, String> cookies() { 605 return cookies; 606 } 607 } 608 609 public static class Request extends HttpConnection.Base<Connection.Request> implements Connection.Request { 610 static { 611 System.setProperty("sun.net.http.allowRestrictedHeaders", "true"); 612 // make sure that we can send Sec-Fetch-Site headers etc. 613 } 614 615 HttpConnection connection; 616 private @Nullable Proxy proxy; 617 private int timeoutMilliseconds; 618 private int maxBodySizeBytes; 619 private boolean followRedirects; 620 private final Collection<Connection.KeyVal> data; 621 private @Nullable Object body = null; // String or InputStream 622 @Nullable String mimeBoundary; 623 private boolean ignoreHttpErrors = false; 624 private boolean ignoreContentType = false; 625 private Parser parser; 626 private boolean parserDefined = false; // called parser(...) vs initialized in ctor 627 private String postDataCharset = DataUtil.defaultCharsetName; 628 private @Nullable SSLSocketFactory sslSocketFactory; 629 @Nullable SSLContext sslContext; 630 private CookieManager cookieManager; 631 @Nullable RequestAuthenticator authenticator; 632 private @Nullable Progress<Connection.Response> responseProgress; 633 634 private final ReentrantLock executing = new ReentrantLock(); // detects and warns if same request used concurrently 635 636 Request() { 637 super(); 638 timeoutMilliseconds = 30000; // 30 seconds 639 maxBodySizeBytes = 1024 * 1024 * 2; // 2MB 640 followRedirects = true; 641 data = new ArrayList<>(); 642 method = Method.GET; 643 addHeader("Accept-Encoding", "gzip"); 644 addHeader(USER_AGENT, DEFAULT_UA); 645 parser = Parser.htmlParser(); 646 cookieManager = new CookieManager(); // creates a default InMemoryCookieStore 647 } 648 649 Request(Request copy) { 650 super(copy); 651 connection = copy.connection; 652 proxy = copy.proxy; 653 postDataCharset = copy.postDataCharset; 654 timeoutMilliseconds = copy.timeoutMilliseconds; 655 maxBodySizeBytes = copy.maxBodySizeBytes; 656 followRedirects = copy.followRedirects; 657 data = new ArrayList<>(); // data not copied 658 //body not copied 659 ignoreHttpErrors = copy.ignoreHttpErrors; 660 ignoreContentType = copy.ignoreContentType; 661 parser = copy.parser.newInstance(); // parsers and their tree-builders maintain state, so need a fresh copy 662 parserDefined = copy.parserDefined; 663 sslSocketFactory = copy.sslSocketFactory; // these are all synchronized so safe to share 664 sslContext = copy.sslContext; 665 cookieManager = copy.cookieManager; 666 authenticator = copy.authenticator; 667 responseProgress = copy.responseProgress; 668 } 669 670 @Override @Nullable 671 public Proxy proxy() { 672 return proxy; 673 } 674 675 @Override 676 public Request proxy(@Nullable Proxy proxy) { 677 this.proxy = proxy; 678 return this; 679 } 680 681 @Override 682 public Request proxy(String host, int port) { 683 this.proxy = new Proxy(Proxy.Type.HTTP, InetSocketAddress.createUnresolved(host, port)); 684 return this; 685 } 686 687 @Override 688 public int timeout() { 689 return timeoutMilliseconds; 690 } 691 692 @Override 693 public Request timeout(int millis) { 694 Validate.isTrue(millis >= 0, "Timeout milliseconds must be 0 (infinite) or greater"); 695 timeoutMilliseconds = millis; 696 return this; 697 } 698 699 @Override 700 public int maxBodySize() { 701 return maxBodySizeBytes; 702 } 703 704 @Override 705 public Connection.Request maxBodySize(int bytes) { 706 Validate.isTrue(bytes >= 0, "maxSize must be 0 (unlimited) or larger"); 707 maxBodySizeBytes = bytes; 708 return this; 709 } 710 711 @Override 712 public boolean followRedirects() { 713 return followRedirects; 714 } 715 716 @Override 717 public Connection.Request followRedirects(boolean followRedirects) { 718 this.followRedirects = followRedirects; 719 return this; 720 } 721 722 @Override 723 public boolean ignoreHttpErrors() { 724 return ignoreHttpErrors; 725 } 726 727 @Override @Nullable 728 public SSLSocketFactory sslSocketFactory() { 729 return sslSocketFactory; 730 } 731 732 @Override 733 @Deprecated 734 public void sslSocketFactory(SSLSocketFactory sslSocketFactory) { 735 this.sslSocketFactory = sslSocketFactory; 736 } 737 738 @Override @Nullable 739 public SSLContext sslContext() { 740 return sslContext; 741 } 742 743 @Override 744 public Connection.Request sslContext(SSLContext sslContext) { 745 this.sslContext = sslContext; 746 return this; 747 } 748 749 @Override 750 public Connection.Request ignoreHttpErrors(boolean ignoreHttpErrors) { 751 this.ignoreHttpErrors = ignoreHttpErrors; 752 return this; 753 } 754 755 @Override 756 public boolean ignoreContentType() { 757 return ignoreContentType; 758 } 759 760 @Override 761 public Connection.Request ignoreContentType(boolean ignoreContentType) { 762 this.ignoreContentType = ignoreContentType; 763 return this; 764 } 765 766 @Override 767 public Request data(Connection.KeyVal keyval) { 768 Validate.notNullParam(keyval, "keyval"); 769 data.add(keyval); 770 return this; 771 } 772 773 @Override 774 public Collection<Connection.KeyVal> data() { 775 return data; 776 } 777 778 @Override 779 public Connection.Request requestBody(@Nullable String body) { 780 this.body = body; 781 return this; 782 } 783 784 @Override @Nullable 785 public String requestBody() { 786 return body instanceof String ? (String) body : null; 787 } 788 789 @Override 790 public Connection.Request requestBodyStream(InputStream stream) { 791 body = stream; 792 return this; 793 } 794 795 @Override 796 public Request parser(Parser parser) { 797 this.parser = parser; 798 parserDefined = true; 799 return this; 800 } 801 802 @Override 803 public Parser parser() { 804 return parser; 805 } 806 807 @Override 808 public Connection.Request postDataCharset(String charset) { 809 Validate.notNullParam(charset, "charset"); 810 if (!Charset.isSupported(charset)) throw new IllegalCharsetNameException(charset); 811 this.postDataCharset = charset; 812 return this; 813 } 814 815 @Override 816 public String postDataCharset() { 817 return postDataCharset; 818 } 819 820 CookieManager cookieManager() { 821 return cookieManager; 822 } 823 824 @Override public Connection.Request auth(@Nullable RequestAuthenticator authenticator) { 825 this.authenticator = authenticator; 826 return this; 827 } 828 829 @Override @Nullable public RequestAuthenticator auth() { 830 return authenticator; 831 } 832 } 833 834 public static class Response extends HttpConnection.Base<Connection.Response> implements Connection.Response { 835 private static final int MAX_REDIRECTS = 20; 836 private static final String LOCATION = "Location"; 837 int statusCode; 838 String statusMessage = ""; 839 private @Nullable ByteBuffer byteData; 840 private @Nullable ControllableInputStream bodyStream; 841 @Nullable RequestExecutor executor; 842 private @Nullable String charset; 843 @Nullable String contentType; 844 int contentLength; 845 private boolean executed = false; 846 private boolean inputStreamRead = false; 847 private int numRedirects = 0; 848 private final HttpConnection.Request req; 849 850 /* 851 * Matches XML content types (like text/xml, image/svg+xml, application/xhtml+xml;charset=UTF8, etc) 852 */ 853 private static final Pattern xmlContentTypeRxp = Pattern.compile("(\\w+)/\\w*\\+?xml.*"); 854 855 /** 856 <b>Internal only! </b>Creates a dummy HttpConnection.Response, useful for testing. All actual responses 857 are created from the HttpURLConnection and fields defined. 858 */ 859 Response() { 860 super(); 861 statusCode = 400; 862 statusMessage = "Request not made"; 863 req = new Request(); 864 contentType = null; 865 } 866 867 static Response execute(HttpConnection.Request req) throws IOException { 868 return execute(req, null); 869 } 870 871 static Response execute(HttpConnection.Request req, @Nullable Response prevRes) throws IOException { 872 Validate.isTrue(req.executing.tryLock(), "Multiple threads were detected trying to execute the same request concurrently. Make sure to use Connection#newRequest() and do not share an executing request between threads."); 873 Validate.notNullParam(req, "req"); 874 URL url = req.url(); 875 Validate.notNull(url, "URL must be specified to connect"); 876 String protocol = url.getProtocol(); 877 if (!protocol.equals("http") && !protocol.equals("https")) 878 throw new MalformedURLException("Only http & https protocols supported"); 879 final boolean supportsBody = req.method().hasBody(); 880 final boolean hasBody = req.body != null; 881 if (!supportsBody) 882 Validate.isFalse(hasBody, "Cannot set a request body for HTTP method " + req.method()); 883 884 // set up the request for execution 885 if (!req.data().isEmpty() && (!supportsBody || hasBody)) 886 serialiseRequestUrl(req); 887 else if (supportsBody) 888 setOutputContentType(req); 889 890 long startTime = System.nanoTime(); 891 RequestExecutor executor = RequestDispatch.get(req, prevRes); 892 Response res = null; 893 try { 894 res = executor.execute(); 895 896 // redirect if there's a location header (from 3xx, or 201 etc) 897 if (res.hasHeader(LOCATION) && req.followRedirects()) { 898 if (res.statusCode != HTTP_TEMP_REDIR) { 899 req.method(Method.GET); // always redirect with a get. any data param from original req are dropped. 900 req.data().clear(); 901 req.requestBody(null); 902 req.removeHeader(CONTENT_TYPE); 903 } 904 905 String location = res.header(LOCATION); 906 Validate.notNull(location); 907 if (location.startsWith("http:/") && location.charAt(6) != '/') // fix broken Location: http:/temp/AAG_New/en/index.php 908 location = location.substring(6); 909 URL redir = StringUtil.resolve(req.url(), location); 910 req.url(redir); 911 912 return execute(req, res); 913 } 914 if ((res.statusCode < 200 || res.statusCode >= 400) && !req.ignoreHttpErrors()) 915 throw new HttpStatusException("HTTP error fetching URL", res.statusCode, req.url().toString()); 916 917 // check that we can handle the returned content type; if not, abort before fetching it 918 String contentType = res.contentType(); 919 if (contentType != null 920 && !req.ignoreContentType() 921 && !contentType.startsWith("text/") 922 && !xmlContentTypeRxp.matcher(contentType).matches() 923 ) 924 throw new UnsupportedMimeTypeException("Unhandled content type. Must be text/*, */xml, or */*+xml", 925 contentType, req.url().toString()); 926 927 // switch to the XML parser if content type is xml and not parser not explicitly set 928 if (contentType != null && xmlContentTypeRxp.matcher(contentType).matches()) { 929 if (!req.parserDefined) req.parser(Parser.xmlParser()); 930 } 931 932 res.charset = DataUtil.getCharsetFromContentType(res.contentType); // may be null, readInputStream deals with it 933 if (res.contentLength != 0 && req.method() != HEAD) { // -1 means unknown, chunked. sun throws an IO exception on 500 response with no content when trying to read body 934 InputStream stream = executor.responseBody(); 935 if (res.hasHeaderWithValue(CONTENT_ENCODING, "gzip")) 936 stream = new GZIPInputStream(stream); 937 else if (res.hasHeaderWithValue(CONTENT_ENCODING, "deflate")) 938 stream = new InflaterInputStream(stream, new Inflater(true)); 939 940 res.bodyStream = ControllableInputStream.wrap( 941 stream, DefaultBufferSize, req.maxBodySize()) 942 .timeout(startTime, req.timeout()); 943 944 if (req.responseProgress != null) // set response progress listener 945 res.bodyStream.onProgress(res.contentLength, req.responseProgress, res); 946 } else { 947 res.byteData = DataUtil.emptyByteBuffer(); 948 } 949 } catch (IOException e) { 950 if (res != null) res.safeClose(); // will be non-null if got to conn 951 throw e; 952 } finally { 953 req.executing.unlock(); 954 955 // detach any thread local auth delegate 956 if (req.authenticator != null) 957 AuthenticationHandler.handler.remove(); 958 } 959 960 res.executed = true; 961 return res; 962 } 963 964 @Override 965 public int statusCode() { 966 return statusCode; 967 } 968 969 @Override 970 public String statusMessage() { 971 return statusMessage; 972 } 973 974 @Override @Nullable 975 public String charset() { 976 return charset; 977 } 978 979 @Override 980 public Response charset(String charset) { 981 this.charset = charset; 982 return this; 983 } 984 985 @Override @Nullable 986 public String contentType() { 987 return contentType; 988 } 989 990 /** Called from parse() or streamParser(), validates and prepares the input stream, and aligns common settings. */ 991 private ControllableInputStream prepareParse() { 992 Validate.isTrue(executed, "Request must be executed (with .execute(), .get(), or .post() before parsing response"); 993 ControllableInputStream stream = bodyStream; 994 if (byteData != null) { // bytes have been read in to the buffer, parse that 995 ByteArrayInputStream bytes = new ByteArrayInputStream(byteData.array(), 0, byteData.limit()); 996 stream = ControllableInputStream.wrap(bytes, 0); // no max 997 inputStreamRead = false; // ok to reparse if in bytes 998 } 999 Validate.isFalse(inputStreamRead, "Input stream already read and parsed, cannot re-read."); 1000 Validate.notNull(stream); 1001 inputStreamRead = true; 1002 return stream; 1003 } 1004 1005 @Override public Document parse() throws IOException { 1006 ControllableInputStream stream = prepareParse(); 1007 Document doc = DataUtil.parseInputStream(stream, charset, url.toExternalForm(), req.parser()); 1008 doc.connection(new HttpConnection(req, this)); // because we're static, don't have the connection obj. // todo - maybe hold in the req? 1009 charset = doc.outputSettings().charset().name(); // update charset from meta-equiv, possibly 1010 safeClose(); 1011 return doc; 1012 } 1013 1014 @Override public StreamParser streamParser() throws IOException { 1015 ControllableInputStream stream = prepareParse(); 1016 String baseUri = url.toExternalForm(); 1017 DataUtil.CharsetDoc charsetDoc = DataUtil.detectCharsetForStreamParser(stream, charset, baseUri, req.parser()); 1018 1019 // set up the stream parser and rig this connection up to the parsed doc: 1020 StreamParser streamer = new StreamParser(req.parser()); 1021 BufferedReader reader = new BufferedReader(new InputStreamReader(charsetDoc.input, charsetDoc.charset)); 1022 streamer.parse(reader, baseUri); // initializes the parse and the document, but does not step() it 1023 streamer.document().connection(new HttpConnection(req, this)); 1024 charset = charsetDoc.charset.name(); 1025 1026 // we don't safeClose() as in parse(); caller must close streamParser to close InputStream stream 1027 return streamer; 1028 } 1029 1030 /** 1031 Reads the bodyStream into byteData. A no-op if already executed. 1032 */ 1033 @Override 1034 public Connection.Response readFully() throws IOException { 1035 Validate.isTrue(executed, "Request must be executed (with .execute(), .get(), or .post() before getting response body"); 1036 if (bodyStream != null && byteData == null) { 1037 Validate.isFalse(inputStreamRead, "Request has already been read (with .parse())"); 1038 try { 1039 byteData = DataUtil.readToByteBuffer(bodyStream, req.maxBodySize()); 1040 } finally { 1041 inputStreamRead = true; 1042 safeClose(); 1043 } 1044 } 1045 return this; 1046 } 1047 1048 /** 1049 Reads the body, but throws an UncheckedIOException if an IOException occurs. 1050 @throws UncheckedIOException if an IOException occurs 1051 */ 1052 private void readByteDataUnchecked() { 1053 try { 1054 readFully(); 1055 } catch (IOException e) { 1056 throw new UncheckedIOException(e); 1057 } 1058 } 1059 1060 @Override 1061 public String readBody() throws IOException { 1062 readFully(); 1063 return body(); 1064 } 1065 1066 @Override 1067 public String body() { 1068 readByteDataUnchecked(); 1069 Validate.notNull(byteData); 1070 // charset gets set from header on execute, and from meta-equiv on parse. parse may not have happened yet 1071 String body = (charset == null ? UTF_8 : Charset.forName(charset)) 1072 .decode(byteData).toString(); 1073 ((Buffer)byteData).rewind(); // cast to avoid covariant return type change in jdk9 1074 return body; 1075 } 1076 1077 @Override 1078 public byte[] bodyAsBytes() { 1079 readByteDataUnchecked(); 1080 Validate.notNull(byteData); 1081 Validate.isTrue(byteData.hasArray()); // we made it, so it should 1082 1083 byte[] array = byteData.array(); 1084 int offset = byteData.arrayOffset(); 1085 int length = byteData.limit(); 1086 1087 if (offset == 0 && length == array.length) { // exact, just return it 1088 return array; 1089 } else { // trim to size 1090 byte[] exactArray = new byte[length]; 1091 System.arraycopy(array, offset, exactArray, 0, length); 1092 return exactArray; 1093 } 1094 } 1095 1096 @Override 1097 @Deprecated 1098 public Connection.Response bufferUp() { 1099 readByteDataUnchecked(); 1100 return this; 1101 } 1102 1103 @Override 1104 public BufferedInputStream bodyStream() { 1105 Validate.isTrue(executed, "Request must be executed (with .execute(), .get(), or .post() before getting response body"); 1106 1107 // if we have read to bytes (via readFully), return those as a stream. 1108 if (byteData != null) { 1109 return new BufferedInputStream( 1110 new ByteArrayInputStream(byteData.array(), 0, byteData.limit()), 1111 DefaultBufferSize); 1112 } 1113 1114 Validate.isFalse(inputStreamRead, "Request has already been read"); 1115 Validate.notNull(bodyStream); 1116 inputStreamRead = true; 1117 return bodyStream.inputStream(); 1118 } 1119 1120 /** 1121 * Call on completion of stream read, to close the body (or error) stream. The connection.disconnect allows 1122 * keep-alives to work (as the underlying connection is actually held open, despite the name). 1123 */ 1124 private void safeClose() { 1125 if (bodyStream != null) { 1126 try { 1127 bodyStream.close(); 1128 } catch (IOException e) { 1129 // no-op 1130 } finally { 1131 bodyStream = null; 1132 } 1133 } 1134 1135 if (executor != null) executor.safeClose(); // disconnect 1136 } 1137 1138 Response(HttpConnection.Request request) { 1139 this.req = request; 1140 } 1141 1142 // set up url, method, header, cookies 1143 void prepareResponse(Map<String, List<String>> resHeaders, HttpConnection.@Nullable Response previousResponse) throws IOException { 1144 processResponseHeaders(resHeaders); // includes cookie key/val read during header scan 1145 CookieUtil.storeCookies(req, this, url, resHeaders); // add set cookies to cookie store 1146 1147 if (previousResponse != null) { // was redirected 1148 // map previous response cookies into this response cookies() object 1149 for (Map.Entry<String, String> prevCookie : previousResponse.cookies().entrySet()) { 1150 if (!hasCookie(prevCookie.getKey())) 1151 cookie(prevCookie.getKey(), prevCookie.getValue()); 1152 } 1153 previousResponse.safeClose(); 1154 1155 // enforce too many redirects: 1156 numRedirects = previousResponse.numRedirects + 1; 1157 if (numRedirects >= MAX_REDIRECTS) 1158 throw new IOException(String.format("Too many redirects occurred trying to load URL %s", previousResponse.url())); 1159 } 1160 } 1161 1162 void processResponseHeaders(Map<String, List<String>> resHeaders) { 1163 for (Map.Entry<String, List<String>> entry : resHeaders.entrySet()) { 1164 String name = entry.getKey(); 1165 if (name == null) 1166 continue; // http/1.1 line 1167 1168 List<String> values = entry.getValue(); 1169 for (String value : values) { 1170 addHeader(name, fixHeaderEncoding(value)); 1171 } 1172 } 1173 } 1174 1175 /** 1176 Servers may encode response headers in UTF-8 instead of RFC defined 8859. The JVM decodes the headers (before we see them) as 8859, which can lead to mojibake data. 1177 <p>This method attempts to detect that and re-decode the string as UTF-8.</p> 1178 <p>However on Android, the headers will be decoded as UTF8, so we can detect and pass those directly.</p> 1179 * @param val a header value string that may have been incorrectly decoded as 8859. 1180 * @return a potentially re-decoded string. 1181 */ 1182 @Nullable 1183 static String fixHeaderEncoding(@Nullable String val) { 1184 if (val == null) return val; 1185 // If we can't encode the string as 8859, then it couldn't have been decoded as 8859 1186 if (!StandardCharsets.ISO_8859_1.newEncoder().canEncode(val)) 1187 return val; 1188 byte[] bytes = val.getBytes(ISO_8859_1); 1189 if (looksLikeUtf8(bytes)) 1190 return new String(bytes, UTF_8); 1191 else 1192 return val; 1193 } 1194 1195 private static boolean looksLikeUtf8(byte[] input) { 1196 int i = 0; 1197 // BOM: 1198 if (input.length >= 3 1199 && (input[0] & 0xFF) == 0xEF 1200 && (input[1] & 0xFF) == 0xBB 1201 && (input[2] & 0xFF) == 0xBF) { 1202 i = 3; 1203 } 1204 1205 int end; 1206 boolean foundNonAscii = false; 1207 for (int j = input.length; i < j; ++i) { 1208 int o = input[i]; 1209 if ((o & 0x80) == 0) { 1210 continue; // ASCII 1211 } 1212 foundNonAscii = true; 1213 1214 // UTF-8 leading: 1215 if ((o & 0xE0) == 0xC0) { 1216 end = i + 1; 1217 } else if ((o & 0xF0) == 0xE0) { 1218 end = i + 2; 1219 } else if ((o & 0xF8) == 0xF0) { 1220 end = i + 3; 1221 } else { 1222 return false; 1223 } 1224 1225 if (end >= input.length) 1226 return false; 1227 1228 while (i < end) { 1229 i++; 1230 o = input[i]; 1231 if ((o & 0xC0) != 0x80) { 1232 return false; 1233 } 1234 } 1235 } 1236 return foundNonAscii; 1237 } 1238 1239 private static void setOutputContentType(final HttpConnection.Request req) { 1240 final String contentType = req.header(CONTENT_TYPE); 1241 String bound = null; 1242 if (contentType != null) { 1243 // no-op; don't add content type as already set (e.g. for requestBody()) 1244 // todo - if content type already set, we could add charset 1245 1246 // if user has set content type to multipart/form-data, auto add boundary. 1247 if(contentType.contains(MULTIPART_FORM_DATA) && !contentType.contains("boundary")) { 1248 bound = DataUtil.mimeBoundary(); 1249 req.header(CONTENT_TYPE, MULTIPART_FORM_DATA + "; boundary=" + bound); 1250 } 1251 1252 } 1253 else if (needsMultipart(req)) { 1254 bound = DataUtil.mimeBoundary(); 1255 req.header(CONTENT_TYPE, MULTIPART_FORM_DATA + "; boundary=" + bound); 1256 } else { 1257 req.header(CONTENT_TYPE, FORM_URL_ENCODED + "; charset=" + req.postDataCharset()); 1258 } 1259 req.mimeBoundary = bound; 1260 } 1261 1262 static void writePost(final HttpConnection.Request req, final OutputStream outputStream) throws IOException { 1263 try (OutputStreamWriter osw = new OutputStreamWriter(outputStream, req.postDataCharset()); 1264 BufferedWriter w = new BufferedWriter(osw)) { 1265 implWritePost(req, w, outputStream); 1266 } 1267 } 1268 1269 private static void implWritePost(final HttpConnection.Request req, final BufferedWriter w, final OutputStream outputStream) throws IOException { 1270 final Collection<Connection.KeyVal> data = req.data(); 1271 final String boundary = req.mimeBoundary; 1272 1273 if (boundary != null) { // a multipart post 1274 for (Connection.KeyVal keyVal : data) { 1275 w.write("--"); 1276 w.write(boundary); 1277 w.write("\r\n"); 1278 w.write("Content-Disposition: form-data; name=\""); 1279 w.write(encodeMimeName(keyVal.key())); // encodes " to %22 1280 w.write("\""); 1281 final InputStream input = keyVal.inputStream(); 1282 if (input != null) { 1283 w.write("; filename=\""); 1284 w.write(encodeMimeName(keyVal.value())); 1285 w.write("\"\r\nContent-Type: "); 1286 String contentType = keyVal.contentType(); 1287 w.write(contentType != null ? contentType : DefaultUploadType); 1288 w.write("\r\n\r\n"); 1289 w.flush(); 1290 DataUtil.crossStreams(input, outputStream); 1291 outputStream.flush(); 1292 } else { 1293 w.write("\r\n\r\n"); 1294 w.write(keyVal.value()); 1295 } 1296 w.write("\r\n"); 1297 } 1298 w.write("--"); 1299 w.write(boundary); 1300 w.write("--"); 1301 } else if (req.body != null) { // a single body (bytes or plain text); data will be in query string 1302 if (req.body instanceof String) { 1303 w.write((String) req.body); 1304 } else if (req.body instanceof InputStream) { 1305 DataUtil.crossStreams((InputStream) req.body, outputStream); 1306 outputStream.flush(); 1307 } else { 1308 throw new IllegalStateException(); 1309 } 1310 } else { // regular form data (application/x-www-form-urlencoded) 1311 boolean first = true; 1312 for (Connection.KeyVal keyVal : data) { 1313 if (!first) w.append('&'); 1314 else first = false; 1315 1316 w.write(URLEncoder.encode(keyVal.key(), req.postDataCharset())); 1317 w.write('='); 1318 w.write(URLEncoder.encode(keyVal.value(), req.postDataCharset())); 1319 } 1320 } 1321 } 1322 1323 // for get url reqs, serialise the data map into the url 1324 private static void serialiseRequestUrl(Connection.Request req) throws IOException { 1325 UrlBuilder in = new UrlBuilder(req.url()); 1326 1327 for (Connection.KeyVal keyVal : req.data()) { 1328 Validate.isFalse(keyVal.hasInputStream(), "InputStream data not supported in URL query string."); 1329 in.appendKeyVal(keyVal); 1330 } 1331 req.url(in.build()); 1332 req.data().clear(); // moved into url as get params 1333 } 1334 } 1335 1336 private static boolean needsMultipart(Connection.Request req) { 1337 // multipart mode, for files. add the header if we see something with an inputstream, and return a non-null boundary 1338 for (Connection.KeyVal keyVal : req.data()) { 1339 if (keyVal.hasInputStream()) 1340 return true; 1341 } 1342 return false; 1343 } 1344 1345 public static class KeyVal implements Connection.KeyVal { 1346 private String key; 1347 private String value; 1348 private @Nullable InputStream stream; 1349 private @Nullable String contentType; 1350 1351 public static KeyVal create(String key, String value) { 1352 return new KeyVal(key, value); 1353 } 1354 1355 public static KeyVal create(String key, String filename, InputStream stream) { 1356 return new KeyVal(key, filename) 1357 .inputStream(stream); 1358 } 1359 1360 private KeyVal(String key, String value) { 1361 Validate.notEmptyParam(key, "key"); 1362 Validate.notNullParam(value, "value"); 1363 this.key = key; 1364 this.value = value; 1365 } 1366 1367 @Override 1368 public KeyVal key(String key) { 1369 Validate.notEmptyParam(key, "key"); 1370 this.key = key; 1371 return this; 1372 } 1373 1374 @Override 1375 public String key() { 1376 return key; 1377 } 1378 1379 @Override 1380 public KeyVal value(String value) { 1381 Validate.notNullParam(value, "value"); 1382 this.value = value; 1383 return this; 1384 } 1385 1386 @Override 1387 public String value() { 1388 return value; 1389 } 1390 1391 @Override 1392 public KeyVal inputStream(InputStream inputStream) { 1393 Validate.notNullParam(value, "inputStream"); 1394 this.stream = inputStream; 1395 return this; 1396 } 1397 1398 @Override @Nullable 1399 public InputStream inputStream() { 1400 return stream; 1401 } 1402 1403 @Override 1404 public boolean hasInputStream() { 1405 return stream != null; 1406 } 1407 1408 @Override 1409 public Connection.KeyVal contentType(String contentType) { 1410 Validate.notEmpty(contentType); 1411 this.contentType = contentType; 1412 return this; 1413 } 1414 1415 @Override @Nullable 1416 public String contentType() { 1417 return contentType; 1418 } 1419 1420 @Override 1421 public String toString() { 1422 return key + "=" + value; 1423 } 1424 } 1425}