001/*
002 * nimbus-jose-jwt
003 *
004 * Copyright 2012-2016, Connect2id Ltd.
005 *
006 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use
007 * this file except in compliance with the License. You may obtain a copy of the
008 * License at
009 *
010 *    http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software distributed
013 * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
014 * CONDITIONS OF ANY KIND, either express or implied. See the License for the
015 * specific language governing permissions and limitations under the License.
016 */
017
018package com.nimbusds.jose.util;
019
020
021import java.io.IOException;
022import java.io.InputStream;
023import java.net.HttpURLConnection;
024import java.net.Proxy;
025import java.net.URL;
026import java.net.URLConnection;
027import java.util.List;
028import java.util.Map;
029import javax.net.ssl.HttpsURLConnection;
030import javax.net.ssl.SSLSocketFactory;
031
032import net.jcip.annotations.ThreadSafe;
033
034
035/**
036 * The default retriever of resources specified by HTTP(S) or file based URL.
037 * Provides setting of a HTTP proxy, HTTP connect and read timeouts as well as
038 * a size limit of the retrieved entity. Caching header directives are not
039 * honoured.
040 *
041 * @author Vladimir Dzhuvinov
042 * @author Artun Subasi
043 * @author Imre Paladji
044 * @version 2022-04-07
045 */
046@ThreadSafe
047public class DefaultResourceRetriever extends AbstractRestrictedResourceRetriever implements RestrictedResourceRetriever {
048        
049        
050        /**
051         * If {@code true} the disconnect method of the underlying
052         * HttpURLConnection is called after a successful or failed retrieval.
053         */
054        private boolean disconnectAfterUse;
055        
056        
057        /**
058         * For establishing the TLS connections, {@code null} to use the
059         * default one.
060         */
061        private final SSLSocketFactory sslSocketFactory;
062
063
064        /**
065         * The proxy to use when opening the HttpURLConnection. Can be
066         * {@code null}.
067         */
068        private Proxy proxy;
069
070
071        /**
072         * Creates a new resource retriever. The HTTP timeouts and entity size
073         * limit are set to zero (infinite).
074         */
075        public DefaultResourceRetriever() {
076
077                this(0, 0);
078        }
079
080
081        /**
082         * Creates a new resource retriever. The HTTP entity size limit is set
083         * to zero (infinite).
084         *
085         * @param connectTimeout The HTTP connects timeout, in milliseconds,
086         *                       zero for infinite. Must not be negative.
087         * @param readTimeout    The HTTP read timeout, in milliseconds, zero
088         *                       for infinite. Must not be negative.
089         */
090        public DefaultResourceRetriever(final int connectTimeout, final int readTimeout) {
091
092                this(connectTimeout, readTimeout, 0);
093        }
094
095
096        /**
097         * Creates a new resource retriever.
098         *
099         * @param connectTimeout The HTTP connects timeout, in milliseconds,
100         *                       zero for infinite. Must not be negative.
101         * @param readTimeout    The HTTP read timeout, in milliseconds, zero
102         *                       for infinite. Must not be negative.
103         * @param sizeLimit      The HTTP entity size limit, in bytes, zero for
104         *                       infinite. Must not be negative.
105         */
106        public DefaultResourceRetriever(final int connectTimeout, final int readTimeout, final int sizeLimit) {
107
108                this(connectTimeout, readTimeout, sizeLimit, true);
109        }
110
111
112        /**
113         * Creates a new resource retriever.
114         *
115         * @param connectTimeout     The HTTP connects timeout, in
116         *                           milliseconds, zero for infinite. Must not
117         *                           be negative.
118         * @param readTimeout        The HTTP read timeout, in milliseconds,
119         *                           zero for infinite. Must not be negative.
120         * @param sizeLimit          The HTTP entity size limit, in bytes, zero
121         *                           for infinite. Must not be negative.
122         * @param disconnectAfterUse If {@code true} the disconnect method of
123         *                           the underlying {@link HttpURLConnection}
124         *                           will be called after trying to retrieve
125         *                           the resource. Whether the TCP socket is
126         *                           actually closed or reused depends on the
127         *                           underlying HTTP implementation and the
128         *                           setting of the {@code keep.alive} system
129         *                           property.
130         */
131        public DefaultResourceRetriever(final int connectTimeout,
132                                        final int readTimeout,
133                                        final int sizeLimit,
134                                        final boolean disconnectAfterUse) {
135                
136                this(connectTimeout, readTimeout, sizeLimit, disconnectAfterUse, null);
137        }
138        
139        
140        /**
141         * Creates a new resource retriever.
142         *
143         * @param connectTimeout     The HTTP connects timeout, in
144         *                           milliseconds, zero for infinite. Must not
145         *                           be negative.
146         * @param readTimeout        The HTTP read timeout, in milliseconds,
147         *                           zero for infinite. Must not be negative.
148         * @param sizeLimit          The HTTP entity size limit, in bytes, zero
149         *                           for infinite. Must not be negative.
150         * @param disconnectAfterUse If {@code true} the disconnect method of
151         *                           the underlying {@link HttpURLConnection}
152         *                           will be called after trying to retrieve
153         *                           the resource. Whether the TCP socket is
154         *                           actually closed or reused depends on the
155         *                           underlying HTTP implementation and the
156         *                           setting of the {@code keep.alive} system
157         *                           property.
158         * @param sslSocketFactory   An SSLSocketFactory for establishing the
159         *                           TLS connections, {@code null} to use the
160         *                           default one.
161         */
162        public DefaultResourceRetriever(final int connectTimeout,
163                                        final int readTimeout,
164                                        final int sizeLimit,
165                                        final boolean disconnectAfterUse,
166                                        final SSLSocketFactory sslSocketFactory) {
167                super(connectTimeout, readTimeout, sizeLimit);
168                this.disconnectAfterUse = disconnectAfterUse;
169                this.sslSocketFactory = sslSocketFactory;
170        }
171        
172        
173        /**
174         * Returns {@code true} if the disconnect method of the underlying
175         * {@link HttpURLConnection} will be called after trying to retrieve
176         * the resource. Whether the TCP socket is actually closed or reused
177         * depends on the underlying HTTP implementation and the setting of the
178         * {@code keep.alive} system property.
179         *
180         * @return If {@code true} the disconnect method of the underlying
181         *         {@link HttpURLConnection} will be called after trying to
182         *         retrieve the resource.
183         */
184        public boolean disconnectsAfterUse() {
185
186                return disconnectAfterUse;
187        }
188
189
190        /**
191         * Controls calling of the disconnect method the underlying
192         * {@link HttpURLConnection} after trying to retrieve the resource.
193         * Whether the TCP socket is actually closed or reused depends on the
194         * underlying HTTP implementation and the setting of the
195         * {@code keep.alive} system property.
196         *
197         * If {@code true} the disconnect method of the underlying
198         * {@link HttpURLConnection} will be called after trying to
199         * retrieve the resource.
200         */
201        public void setDisconnectsAfterUse(final boolean disconnectAfterUse) {
202
203                this.disconnectAfterUse = disconnectAfterUse;
204        }
205
206        /**
207         * Returns the HTTP proxy to use when opening the HttpURLConnection to
208         * retrieve the resource. Note that the JVM may have a system wide
209         * proxy configured via the {@code https.proxyHost} Java system
210         * property.
211         *
212         * @return The proxy to use or {@code null} if no proxy should be used.
213         */
214        public Proxy getProxy() {
215
216                return proxy;
217        }
218
219        /**
220         * Sets the HTTP proxy to use when opening the HttpURLConnection to
221         * retrieve the resource. Note that the JVM may have a system wide
222         * proxy configured via the {@code https.proxyHost} Java system
223         * property.
224         *
225         * @param proxy The proxy to use or {@code null} if no proxy should be
226         *              used.
227         */
228        public void setProxy(final Proxy proxy) {
229
230                this.proxy = proxy;
231        }
232
233
234        @Override
235        public Resource retrieveResource(final URL url)
236                throws IOException {
237
238                URLConnection con = null;
239                try {
240                        if ("file".equals(url.getProtocol())) {
241                                con = openFileConnection(url);
242                        } else {
243                                // TODO Switch to openHTTPConnected when the
244                                // deprecated openConnection method is removed
245                                con = openConnection(url);
246                        }
247
248                        con.setConnectTimeout(getConnectTimeout());
249                        con.setReadTimeout(getReadTimeout());
250                        
251                        if (con instanceof HttpsURLConnection && sslSocketFactory != null) {
252                                ((HttpsURLConnection)con).setSSLSocketFactory(sslSocketFactory);
253                        }
254                        
255                        if (con instanceof HttpURLConnection && getHeaders() != null && !getHeaders().isEmpty()) {
256                                for (Map.Entry<String, List<String>> entry : getHeaders().entrySet()) {
257                                        for (String value: entry.getValue()) {
258                                                con.addRequestProperty(entry.getKey(), value);
259                                        }
260                                }
261                        }
262
263                        final String content;
264                        try (InputStream inputStream = getInputStream(con, getSizeLimit())) {
265                                content = IOUtils.readInputStreamToString(inputStream, StandardCharset.UTF_8);
266                        }
267
268                        if (con instanceof HttpURLConnection) {
269                                // Check HTTP code + message
270                                HttpURLConnection httpCon = (HttpURLConnection) con;
271                                final int statusCode = httpCon.getResponseCode();
272                                final String statusMessage = httpCon.getResponseMessage();
273                                
274                                // Ensure 2xx status code
275                                if (statusCode > 299 || statusCode < 200) {
276                                        throw new IOException("HTTP " + statusCode + ": " + statusMessage);
277                                }
278                        }
279                        
280                        String contentType = con instanceof HttpURLConnection ? con.getContentType() : null;
281                        
282                        return new Resource(content, contentType);
283
284                } catch (Exception e) {
285                        
286                        if (e instanceof IOException) {
287                                throw e;
288                        }
289                        
290                        throw new IOException("Couldn't open URL connection: " + e.getMessage(), e);
291                        
292                } finally {
293                        if (disconnectAfterUse && con instanceof HttpURLConnection) {
294                                ((HttpURLConnection) con).disconnect();
295                        }
296                }
297        }
298        
299
300        /**
301         * Opens a connection the specified HTTP(S) URL. Uses the configured
302         * {@link Proxy} if available.
303         *
304         * @see #openHTTPConnection
305         *
306         * @param url The URL of the resource. Its scheme must be HTTP or
307         *            HTTPS. Must not be {@code null}.
308         *
309         * @return The opened HTTP(S) connection
310         *
311         * @throws IOException If the HTTP(S) connection to the specified URL
312         *                     failed.
313         */
314        @Deprecated
315        protected HttpURLConnection openConnection(final URL url) throws IOException {
316                return openHTTPConnection(url);
317        }
318        
319
320        /**
321         * Opens a connection the specified HTTP(S) URL. Uses the configured
322         * {@link Proxy} if available.
323         *
324         * @param url The URL of the resource. Its scheme must be "http" or
325         *            "https". Must not be {@code null}.
326         *
327         * @return The opened HTTP(S) connection
328         *
329         * @throws IOException If the HTTP(S) connection to the specified URL
330         *                     failed.
331         */
332        protected HttpURLConnection openHTTPConnection(final URL url) throws IOException {
333                if (proxy != null) {
334                        return (HttpURLConnection)url.openConnection(proxy);
335                } else {
336                        return (HttpURLConnection)url.openConnection();
337                }
338        }
339        
340        
341        /**
342         * Opens a connection the specified file URL.
343         *
344         * @param url The URL of the resource. Its scheme must be "file". Must
345         *            not be {@code null}.
346         *
347         * @return The opened file connection.
348         *
349         * @throws IOException If the file connection to the specified URL
350         *                     failed.
351         */
352        protected URLConnection openFileConnection(final URL url) throws IOException {
353                
354                return url.openConnection();
355        }
356
357
358        private InputStream getInputStream(final URLConnection con, final int sizeLimit)
359                throws IOException {
360
361                InputStream inputStream = con.getInputStream();
362
363                return sizeLimit > 0 ? new BoundedInputStream(inputStream, getSizeLimit()) : inputStream;
364        }
365}