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.nio.charset.StandardCharsets;
027import java.util.List;
028import java.util.Map;
029
030import net.jcip.annotations.ThreadSafe;
031
032
033/**
034 * The default retriever of resources specified by URL. Provides setting of a
035 * HTTP proxy, HTTP connect and read timeouts as well as a size limit of the
036 * retrieved entity. Caching header directives are not honoured.
037 *
038 * @author Vladimir Dzhuvinov
039 * @author Artun Subasi
040 * @version 2019-08-23
041 */
042@ThreadSafe
043public class DefaultResourceRetriever extends AbstractRestrictedResourceRetriever implements RestrictedResourceRetriever {
044
045
046        /**
047         * If {@code true} the disconnect method of the underlying
048         * HttpURLConnection is called after a successful or failed retrieval.
049         */
050        private boolean disconnectAfterUse;
051
052
053        /**
054         * The proxy to use when opening the HttpURLConnection. Can be
055         * {@code null}.
056         */
057        private Proxy proxy;
058
059
060        /**
061         * Creates a new resource retriever. The HTTP timeouts and entity size
062         * limit are set to zero (infinite).
063         */
064        public DefaultResourceRetriever() {
065
066                this(0, 0);
067        }
068
069
070        /**
071         * Creates a new resource retriever. The HTTP entity size limit is set
072         * to zero (infinite).
073         *
074         * @param connectTimeout The HTTP connects timeout, in milliseconds,
075         *                       zero for infinite. Must not be negative.
076         * @param readTimeout    The HTTP read timeout, in milliseconds, zero
077         *                       for infinite. Must not be negative.
078         */
079        public DefaultResourceRetriever(final int connectTimeout, final int readTimeout) {
080
081                this(connectTimeout, readTimeout, 0);
082        }
083
084
085        /**
086         * Creates a new resource retriever.
087         *
088         * @param connectTimeout The HTTP connects timeout, in milliseconds,
089         *                       zero for infinite. Must not be negative.
090         * @param readTimeout    The HTTP read timeout, in milliseconds, zero
091         *                       for infinite. Must not be negative.
092         * @param sizeLimit      The HTTP entity size limit, in bytes, zero for
093         *                       infinite. Must not be negative.
094         */
095        public DefaultResourceRetriever(final int connectTimeout, final int readTimeout, final int sizeLimit) {
096
097                this(connectTimeout, readTimeout, sizeLimit, true);
098        }
099
100
101        /**
102         * Creates a new resource retriever.
103         *
104         * @param connectTimeout     The HTTP connects timeout, in
105         *                           milliseconds, zero for infinite. Must not
106         *                           be negative.
107         * @param readTimeout        The HTTP read timeout, in milliseconds,
108         *                           zero for infinite. Must not be negative.
109         * @param sizeLimit          The HTTP entity size limit, in bytes, zero
110         *                           for infinite. Must not be negative.
111         * @param disconnectAfterUse If {@code true} the disconnect method of
112         *                           the underlying {@link HttpURLConnection}
113         *                           will be called after trying to retrieve
114         *                           the resource. Whether the TCP socket is
115         *                           actually closed or reused depends on the
116         *                           underlying HTTP implementation and the
117         *                           setting of the {@code keep.alive} system
118         *                           property.
119         */
120        public DefaultResourceRetriever(final int connectTimeout,
121                                        final int readTimeout,
122                                        final int sizeLimit,
123                                        final boolean disconnectAfterUse) {
124
125                super(connectTimeout, readTimeout, sizeLimit);
126                this.disconnectAfterUse = disconnectAfterUse;
127        }
128
129
130        /**
131         * Returns {@code true} if the disconnect method of the underlying
132         * {@link HttpURLConnection} will be called after trying to retrieve
133         * the resource. Whether the TCP socket is actually closed or reused
134         * depends on the underlying HTTP implementation and the setting of the
135         * {@code keep.alive} system property.
136         *
137         * @return If {@code true} the disconnect method of the underlying
138         *         {@link HttpURLConnection} will be called after trying to
139         *         retrieve the resource.
140         */
141        public boolean disconnectsAfterUse() {
142
143                return disconnectAfterUse;
144        }
145
146
147        /**
148         * Controls calling of the disconnect method the underlying
149         * {@link HttpURLConnection} after trying to retrieve the resource.
150         * Whether the TCP socket is actually closed or reused depends on the
151         * underlying HTTP implementation and the setting of the
152         * {@code keep.alive} system property.
153         *
154         * If {@code true} the disconnect method of the underlying
155         * {@link HttpURLConnection} will be called after trying to
156         * retrieve the resource.
157         */
158        public void setDisconnectsAfterUse(final boolean disconnectAfterUse) {
159
160                this.disconnectAfterUse = disconnectAfterUse;
161        }
162
163        /**
164         * Returns the HTTP proxy to use when opening the HttpURLConnection to
165         * retrieve the resource. Note that the JVM may have a system wide
166         * proxy configured via the {@code https.proxyHost} Java system
167         * property.
168         *
169         * @return The proxy to use or {@code null} if no proxy should be used.
170         */
171        public Proxy getProxy() {
172
173                return proxy;
174        }
175
176        /**
177         * Sets the HTTP proxy to use when opening the HttpURLConnection to
178         * retrieve the resource. Note that the JVM may have a system wide
179         * proxy configured via the {@code https.proxyHost} Java system
180         * property.
181         *
182         * @param proxy The proxy to use or {@code null} if no proxy should be
183         *              used.
184         */
185        public void setProxy(final Proxy proxy) {
186
187                this.proxy = proxy;
188        }
189
190
191        @Override
192        public Resource retrieveResource(final URL url)
193                throws IOException {
194
195                HttpURLConnection con = null;
196                try {
197                        con = openConnection(url);
198
199                        con.setConnectTimeout(getConnectTimeout());
200                        con.setReadTimeout(getReadTimeout());
201
202                        if(getHeaders() != null && !getHeaders().isEmpty()) {
203                                for (Map.Entry<String, List<String>> entry : getHeaders().entrySet()) {
204                                        for (String value: entry.getValue()) {
205                                                con.addRequestProperty(entry.getKey(), value);
206                                        }
207                                }
208                        }
209
210                        final String content;
211                        try (InputStream inputStream = getInputStream(con, getSizeLimit())) {
212                                content = IOUtils.readInputStreamToString(inputStream, StandardCharsets.UTF_8);
213                        }
214
215                        // Check HTTP code + message
216                        final int statusCode = con.getResponseCode();
217                        final String statusMessage = con.getResponseMessage();
218
219                        // Ensure 2xx status code
220                        if (statusCode > 299 || statusCode < 200) {
221                                throw new IOException("HTTP " + statusCode + ": " + statusMessage);
222                        }
223
224                        return new Resource(content, con.getContentType());
225
226                } catch (ClassCastException e) {
227                        throw new IOException("Couldn't open HTTP(S) connection: " + e.getMessage(), e);
228                } finally {
229                        if (disconnectAfterUse && con != null) {
230                                con.disconnect();
231                        }
232                }
233        }
234
235        /**
236         * Opens a connection the specified HTTP(S) URL. Uses the configured
237         * {@link Proxy} if available.
238         *
239         * @param url The URL of the resource. Its scheme must be HTTP or
240         *            HTTPS. Must not be {@code null}.
241         *
242         * @return The opened HTTP(S) connection
243         *
244         * @throws IOException If the HTTP(S) connection to the specified URL
245         *                     failed.
246         */
247        protected HttpURLConnection openConnection(final URL url) throws IOException {
248                if (proxy != null) {
249                        return (HttpURLConnection)url.openConnection(proxy);
250                } else {
251                        return (HttpURLConnection)url.openConnection();
252                }
253        }
254
255
256        private InputStream getInputStream(final HttpURLConnection con, final int sizeLimit)
257                throws IOException {
258
259                InputStream inputStream = con.getInputStream();
260
261                return sizeLimit > 0 ? new BoundedInputStream(inputStream, getSizeLimit()) : inputStream;
262        }
263}