001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.tools;
003
004import java.io.BufferedOutputStream;
005import java.io.ByteArrayInputStream;
006import java.io.IOException;
007import java.io.InputStream;
008import java.io.OutputStream;
009import java.net.HttpURLConnection;
010import java.net.URL;
011import java.util.Collections;
012import java.util.List;
013import java.util.Map;
014import java.util.Map.Entry;
015import java.util.Optional;
016import java.util.TreeMap;
017
018import org.openstreetmap.josm.data.Version;
019import org.openstreetmap.josm.gui.progress.ProgressMonitor;
020import org.openstreetmap.josm.io.ProgressOutputStream;
021
022/**
023 * Provides a uniform access for a HTTP/HTTPS 1.0/1.1 server.
024 * @since 15229
025 */
026public final class Http1Client extends HttpClient {
027
028    private HttpURLConnection connection; // to allow disconnecting before `response` is set
029
030    /**
031     * Constructs a new {@code Http1Client}.
032     * @param url URL to access
033     * @param requestMethod HTTP request method (GET, POST, PUT, DELETE...)
034     */
035    public Http1Client(URL url, String requestMethod) {
036        super(url, requestMethod);
037    }
038
039    @Override
040    protected void setupConnection(ProgressMonitor progressMonitor) throws IOException {
041        connection = (HttpURLConnection) getURL().openConnection();
042        connection.setRequestMethod(getRequestMethod());
043        connection.setRequestProperty("User-Agent", Version.getInstance().getFullAgentString());
044        connection.setConnectTimeout(getConnectTimeout());
045        connection.setReadTimeout(getReadTimeout());
046        connection.setInstanceFollowRedirects(false); // we do that ourselves
047        if (getIfModifiedSince() > 0) {
048            connection.setIfModifiedSince(getIfModifiedSince());
049        }
050        connection.setUseCaches(isUseCache());
051        if (!isUseCache()) {
052            connection.setRequestProperty("Cache-Control", "no-cache");
053        }
054        for (Map.Entry<String, String> header : getHeaders().entrySet()) {
055            if (header.getValue() != null) {
056                connection.setRequestProperty(header.getKey(), header.getValue());
057            }
058        }
059
060        notifyConnect(progressMonitor);
061
062        if (requiresBody()) {
063            logRequestBody();
064            byte[] body = getRequestBody();
065            connection.setFixedLengthStreamingMode(body.length);
066            connection.setDoOutput(true);
067            try (OutputStream out = new BufferedOutputStream(
068                    new ProgressOutputStream(connection.getOutputStream(), body.length,
069                            progressMonitor, getOutputMessage(), isFinishOnCloseOutput()))) {
070                out.write(body);
071            }
072        }
073    }
074
075    @Override
076    protected ConnectionResponse performConnection() throws IOException {
077        try {
078            connection.connect();
079        } catch (RuntimeException e) {
080            throw new IOException(e);
081        }
082        return new ConnectionResponse() {
083            @Override
084            public String getResponseVersion() {
085                String headerField = connection.getHeaderField(0);
086                if (headerField != null && headerField.startsWith("HTTP")) {
087                    return headerField.replaceFirst(" .*", "");
088                }
089                return "HTTP/1";
090            }
091
092            @Override
093            public int getResponseCode() throws IOException {
094                return connection.getResponseCode();
095            }
096
097            @Override
098            public String getHeaderField(String name) {
099                return connection.getHeaderField(name);
100            }
101
102            @Override
103            public long getContentLengthLong() {
104                return connection.getContentLengthLong();
105            }
106
107            @Override
108            public Map<String, List<String>> getHeaderFields() {
109                return connection.getHeaderFields();
110            }
111        };
112    }
113
114    @Override
115    protected void performDisconnection() throws IOException {
116        connection.disconnect();
117    }
118
119    @Override
120    protected Response buildResponse(ProgressMonitor progressMonitor) throws IOException {
121        return new Http1Response(connection, progressMonitor);
122    }
123
124    /**
125     * A wrapper for the HTTP 1.x response.
126     */
127    public static final class Http1Response extends Response {
128        private final HttpURLConnection connection;
129
130        private Http1Response(HttpURLConnection connection, ProgressMonitor progressMonitor) throws IOException {
131            super(progressMonitor, connection.getResponseCode(), connection.getResponseMessage());
132            this.connection = connection;
133            debugRedirect();
134        }
135
136        @Override
137        public URL getURL() {
138            return connection.getURL();
139        }
140
141        @Override
142        public String getRequestMethod() {
143            return connection.getRequestMethod();
144        }
145
146        @Override
147        public InputStream getInputStream() throws IOException {
148            InputStream in;
149            try {
150                in = connection.getInputStream();
151            } catch (IOException ioe) {
152                Logging.debug(ioe);
153                in = Optional.ofNullable(connection.getErrorStream()).orElseGet(() -> new ByteArrayInputStream(new byte[]{}));
154            }
155            return in;
156        }
157
158        @Override
159        public String getContentEncoding() {
160            return connection.getContentEncoding();
161        }
162
163        @Override
164        public String getContentType() {
165            return connection.getHeaderField("Content-Type");
166        }
167
168        @Override
169        public long getExpiration() {
170            return connection.getExpiration();
171        }
172
173        @Override
174        public long getLastModified() {
175            return connection.getLastModified();
176        }
177
178        @Override
179        public long getContentLength() {
180            return connection.getContentLengthLong();
181        }
182
183        @Override
184        public String getHeaderField(String name) {
185            return connection.getHeaderField(name);
186        }
187
188        @Override
189        public Map<String, List<String>> getHeaderFields() {
190            // returned map from HttpUrlConnection is case sensitive, use case insensitive TreeMap to conform to RFC 2616
191            Map<String, List<String>> ret = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
192            for (Entry<String, List<String>> e: connection.getHeaderFields().entrySet()) {
193                if (e.getKey() != null) {
194                    ret.put(e.getKey(), e.getValue());
195                }
196            }
197            return Collections.unmodifiableMap(ret);
198        }
199
200        @Override
201        public void disconnect() {
202            Http1Client.disconnect(connection);
203        }
204    }
205
206    /**
207     * @see HttpURLConnection#disconnect()
208     */
209    @Override
210    public void disconnect() {
211        Http1Client.disconnect(connection);
212    }
213
214    private static void disconnect(final HttpURLConnection connection) {
215        if (connection != null) {
216            // Fix upload aborts - see #263
217            connection.setConnectTimeout(100);
218            connection.setReadTimeout(100);
219            try {
220                Thread.sleep(100);
221            } catch (InterruptedException ex) {
222                Logging.warn("InterruptedException in " + Http1Client.class + " during cancel");
223                Thread.currentThread().interrupt();
224            }
225            connection.disconnect();
226        }
227    }
228}