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         * <p>If {@code true} the disconnect method of the underlying
198         * {@link HttpURLConnection} will be called after trying to
199         * retrieve the resource.
200         *
201         * @param disconnectAfterUse If {@code true} the disconnect method of
202         *                           the underlying {@link HttpURLConnection}
203         *                           will be called after trying to retrieve
204         *                           the resource.
205         */
206        public void setDisconnectsAfterUse(final boolean disconnectAfterUse) {
207
208                this.disconnectAfterUse = disconnectAfterUse;
209        }
210
211        /**
212         * Returns the HTTP proxy to use when opening the HttpURLConnection to
213         * retrieve the resource. Note that the JVM may have a system-wide
214         * proxy configured via the {@code https.proxyHost} Java system
215         * property.
216         *
217         * @return The proxy to use or {@code null} if no proxy should be used.
218         */
219        public Proxy getProxy() {
220
221                return proxy;
222        }
223
224        /**
225         * Sets the HTTP proxy to use when opening the HttpURLConnection to
226         * retrieve the resource. Note that the JVM may have a system wide
227         * proxy configured via the {@code https.proxyHost} Java system
228         * property.
229         *
230         * @param proxy The proxy to use or {@code null} if no proxy should be
231         *              used.
232         */
233        public void setProxy(final Proxy proxy) {
234
235                this.proxy = proxy;
236        }
237
238
239        @Override
240        public Resource retrieveResource(final URL url)
241                throws IOException {
242
243                URLConnection con = null;
244                try {
245                        if ("file".equals(url.getProtocol())) {
246                                con = openFileConnection(url);
247                        } else {
248                                // TODO Switch to openHTTPConnected when the
249                                // deprecated openConnection method is removed
250                                con = openConnection(url);
251                        }
252
253                        con.setConnectTimeout(getConnectTimeout());
254                        con.setReadTimeout(getReadTimeout());
255                        
256                        if (con instanceof HttpsURLConnection && sslSocketFactory != null) {
257                                ((HttpsURLConnection)con).setSSLSocketFactory(sslSocketFactory);
258                        }
259                        
260                        if (con instanceof HttpURLConnection && getHeaders() != null && !getHeaders().isEmpty()) {
261                                for (Map.Entry<String, List<String>> entry : getHeaders().entrySet()) {
262                                        for (String value: entry.getValue()) {
263                                                con.addRequestProperty(entry.getKey(), value);
264                                        }
265                                }
266                        }
267
268                        final String content;
269                        try (InputStream inputStream = getInputStream(con, getSizeLimit())) {
270                                content = IOUtils.readInputStreamToString(inputStream, StandardCharset.UTF_8);
271                        }
272
273                        if (con instanceof HttpURLConnection) {
274                                // Check HTTP code + message
275                                HttpURLConnection httpCon = (HttpURLConnection) con;
276                                final int statusCode = httpCon.getResponseCode();
277                                final String statusMessage = httpCon.getResponseMessage();
278                                
279                                // Ensure 2xx status code
280                                if (statusCode > 299 || statusCode < 200) {
281                                        throw new IOException("HTTP " + statusCode + ": " + statusMessage);
282                                }
283                        }
284                        
285                        String contentType = con instanceof HttpURLConnection ? con.getContentType() : null;
286                        
287                        return new Resource(content, contentType);
288
289                } catch (Exception e) {
290                        
291                        if (e instanceof IOException) {
292                                throw e;
293                        }
294                        
295                        throw new IOException("Couldn't open URL connection: " + e.getMessage(), e);
296                        
297                } finally {
298                        if (disconnectAfterUse && con instanceof HttpURLConnection) {
299                                ((HttpURLConnection) con).disconnect();
300                        }
301                }
302        }
303        
304
305        /**
306         * Opens a connection the specified HTTP(S) URL. Uses the configured
307         * {@link Proxy} if available.
308         *
309         * @see #openHTTPConnection
310         *
311         * @param url The URL of the resource. Its scheme must be HTTP or
312         *            HTTPS. Must not be {@code null}.
313         *
314         * @return The opened HTTP(S) connection
315         *
316         * @throws IOException If the HTTP(S) connection to the specified URL
317         *                     failed.
318         */
319        @Deprecated
320        protected HttpURLConnection openConnection(final URL url) throws IOException {
321                return openHTTPConnection(url);
322        }
323        
324
325        /**
326         * Opens a connection the specified HTTP(S) URL. Uses the configured
327         * {@link Proxy} if available.
328         *
329         * @param url The URL of the resource. Its scheme must be "http" or
330         *            "https". Must not be {@code null}.
331         *
332         * @return The opened HTTP(S) connection
333         *
334         * @throws IOException If the HTTP(S) connection to the specified URL
335         *                     failed.
336         */
337        protected HttpURLConnection openHTTPConnection(final URL url) throws IOException {
338                if (proxy != null) {
339                        return (HttpURLConnection)url.openConnection(proxy);
340                } else {
341                        return (HttpURLConnection)url.openConnection();
342                }
343        }
344        
345        
346        /**
347         * Opens a connection the specified file URL.
348         *
349         * @param url The URL of the resource. Its scheme must be "file". Must
350         *            not be {@code null}.
351         *
352         * @return The opened file connection.
353         *
354         * @throws IOException If the file connection to the specified URL
355         *                     failed.
356         */
357        protected URLConnection openFileConnection(final URL url) throws IOException {
358                
359                return url.openConnection();
360        }
361
362
363        private InputStream getInputStream(final URLConnection con, final int sizeLimit)
364                throws IOException {
365
366                InputStream inputStream = con.getInputStream();
367
368                return sizeLimit > 0 ? new BoundedInputStream(inputStream, getSizeLimit()) : inputStream;
369        }
370}