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