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}