001/*
002 * oauth2-oidc-sdk
003 *
004 * Copyright 2012-2020, Connect2id Ltd and contributors.
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.openid.connect.sdk.federation.trust;
019
020
021import com.nimbusds.oauth2.sdk.ErrorObject;
022import com.nimbusds.oauth2.sdk.ParseException;
023import com.nimbusds.oauth2.sdk.WellKnownPathComposeStrategy;
024import com.nimbusds.oauth2.sdk.http.HTTPRequest;
025import com.nimbusds.oauth2.sdk.http.HTTPRequestSender;
026import com.nimbusds.oauth2.sdk.http.HTTPResponse;
027import com.nimbusds.oauth2.sdk.util.StringUtils;
028import com.nimbusds.openid.connect.sdk.federation.api.FetchEntityStatementRequest;
029import com.nimbusds.openid.connect.sdk.federation.api.FetchEntityStatementResponse;
030import com.nimbusds.openid.connect.sdk.federation.config.FederationEntityConfigurationRequest;
031import com.nimbusds.openid.connect.sdk.federation.config.FederationEntityConfigurationResponse;
032import com.nimbusds.openid.connect.sdk.federation.entities.EntityID;
033import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
034
035import java.io.IOException;
036import java.net.URI;
037import java.util.LinkedList;
038import java.util.List;
039
040
041/**
042 * The default entity statement retriever for resolving trust chains. Supports
043 * the {@link WellKnownPathComposeStrategy#POSTFIX postfix} and
044 * {@link WellKnownPathComposeStrategy#INFIX infix} well-known path composition
045 * strategies.
046 */
047public class DefaultEntityStatementRetriever implements EntityStatementRetriever {
048        
049        
050        /**
051         * The HTTP connect timeout in milliseconds.
052         */
053        private final int httpConnectTimeoutMs;
054        
055        
056        /**
057         * The HTTP read timeout in milliseconds.
058         */
059        private final int httpReadTimeoutMs;
060        
061        
062        /**
063         * The default HTTP connect timeout in milliseconds.
064         */
065        public static final int DEFAULT_HTTP_CONNECT_TIMEOUT_MS = 1000;
066        
067        
068        /**
069         * The default HTTP read timeout in milliseconds.
070         */
071        public static final int DEFAULT_HTTP_READ_TIMEOUT_MS = 1000;
072        
073        
074        private final HTTPRequestSender httpRequestSender;
075        
076        
077        /**
078         * Running list of the recorded HTTP requests.
079         */
080        private final List<URI> recordedRequests = new LinkedList<>();
081        
082        
083        /**
084         * Creates a new entity statement retriever using the default HTTP
085         * timeout settings.
086         */
087        public DefaultEntityStatementRetriever() {
088                this(DEFAULT_HTTP_CONNECT_TIMEOUT_MS, DEFAULT_HTTP_READ_TIMEOUT_MS);
089        }
090        
091        
092        /**
093         * Creates a new entity statement retriever.
094         *
095         * @param httpConnectTimeoutMs The HTTP connect timeout in
096         *                             milliseconds, zero means timeout
097         *                             determined by the underlying HTTP client.
098         * @param httpReadTimeoutMs    The HTTP read timeout in milliseconds,
099         *                             zero means timeout determined by the
100         *                             underlying HTTP client.
101         */
102        public DefaultEntityStatementRetriever(final int httpConnectTimeoutMs,
103                                               final int httpReadTimeoutMs) {
104                this.httpConnectTimeoutMs = httpConnectTimeoutMs;
105                this.httpReadTimeoutMs = httpReadTimeoutMs;
106                httpRequestSender = null;
107        }
108
109
110        /**
111         * Creates a new entity statement retriever.
112         *
113         * @param httpRequestSender The HTTP request sender to use. Must not be
114         *                          {@code null}.
115         */
116        public DefaultEntityStatementRetriever(final HTTPRequestSender httpRequestSender) {
117
118                if (httpRequestSender == null) {
119                        throw new IllegalArgumentException("The HTTP request sender must not be null");
120                }
121                this.httpRequestSender = httpRequestSender;
122                httpConnectTimeoutMs = 0;
123                httpReadTimeoutMs = 0;
124        }
125        
126        
127        /**
128         * Returns the configured HTTP connect timeout.
129         *
130         * @return The configured HTTP connect timeout in milliseconds, zero
131         *         means timeout determined by the underlying HTTP client.
132         */
133        public int getHTTPConnectTimeout() {
134                return httpConnectTimeoutMs;
135        }
136        
137        
138        /**
139         * Returns the configured HTTP read timeout.
140         *
141         * @return The configured HTTP read timeout in milliseconds, zero
142         *         means timeout determined by the underlying HTTP client.
143         */
144        public int getHTTPReadTimeout() {
145                return httpReadTimeoutMs;
146        }
147        
148        
149        void applyTimeouts(final HTTPRequest httpRequest) {
150                httpRequest.setConnectTimeout(httpConnectTimeoutMs);
151                httpRequest.setReadTimeout(httpReadTimeoutMs);
152        }
153        
154        
155        @Override
156        public EntityStatement fetchEntityConfiguration(final EntityID target)
157                throws ResolveException {
158                
159                FederationEntityConfigurationRequest request = new FederationEntityConfigurationRequest(target);
160                HTTPRequest httpRequest = request.toHTTPRequest();
161                applyTimeouts(httpRequest);
162                
163                record(httpRequest);
164                
165                HTTPResponse httpResponse;
166                try {
167                        if (httpRequestSender != null) {
168                                httpResponse = httpRequest.send(httpRequestSender);
169                        } else {
170                                httpResponse = httpRequest.send();
171                        }
172                } catch (IOException e) {
173                        throw new ResolveException("Couldn't retrieve entity configuration for " + httpRequest.getURL() + ": " + e.getMessage(), e);
174                }
175                
176                if (StringUtils.isNotBlank(target.toURI().getPath()) && HTTPResponse.SC_NOT_FOUND == httpResponse.getStatusCode()) {
177                        // We have a path in the entity ID URL, try infix strategy
178                        request = new FederationEntityConfigurationRequest(target, WellKnownPathComposeStrategy.INFIX);
179                        httpRequest = request.toHTTPRequest();
180                        applyTimeouts(httpRequest);
181                        
182                        record(httpRequest);
183                        
184                        try {
185                                httpResponse = httpRequest.send();
186                        } catch (IOException e) {
187                                throw new ResolveException("Couldn't retrieve entity configuration for " + httpRequest.getURL() + ": " + e.getMessage(), e);
188                        }
189                }
190                
191                FederationEntityConfigurationResponse response;
192                try {
193                        response = FederationEntityConfigurationResponse.parse(httpResponse);
194                } catch (ParseException e) {
195                        throw new ResolveException("Error parsing entity configuration response from " + httpRequest.getURL() + ": " + e.getMessage(), e);
196                }
197                
198                if (! response.indicatesSuccess()) {
199                        ErrorObject errorObject = response.toErrorResponse().getErrorObject();
200                        throw new ResolveException("Entity configuration error response from " + httpRequest.getURL() + ": " +
201                                errorObject.getHTTPStatusCode() +
202                                (errorObject.getCode() != null ? " " + errorObject.getCode() : ""),
203                                errorObject);
204                }
205                
206                return response.toSuccessResponse().getEntityStatement();
207        }
208        
209        
210        @Override
211        public EntityStatement fetchEntityStatement(final URI federationAPIEndpoint, final EntityID issuer, final EntityID subject)
212                throws ResolveException {
213                
214                FetchEntityStatementRequest request = new FetchEntityStatementRequest(federationAPIEndpoint, issuer, subject);
215                HTTPRequest httpRequest = request.toHTTPRequest();
216                applyTimeouts(httpRequest);
217                
218                record(httpRequest);
219                
220                HTTPResponse httpResponse;
221                try {
222                        httpResponse = httpRequest.send();
223                } catch (IOException e) {
224                        throw new ResolveException("Couldn't fetch entity statement from " + issuer + " at " + federationAPIEndpoint + ": " + e.getMessage(), e);
225                }
226                
227                FetchEntityStatementResponse response;
228                try {
229                        response = FetchEntityStatementResponse.parse(httpResponse);
230                } catch (ParseException e) {
231                        throw new ResolveException("Error parsing entity statement response from " + issuer + " at " + federationAPIEndpoint + ": " + e.getMessage(), e);
232                }
233                
234                if (! response.indicatesSuccess()) {
235                        ErrorObject errorObject = response.toErrorResponse().getErrorObject();
236                        throw new ResolveException("Entity statement error response from " + issuer + " at " + federationAPIEndpoint + ": " +
237                                errorObject.getHTTPStatusCode() +
238                                (errorObject.getCode() != null ? " " + errorObject.getCode() : ""),
239                                errorObject);
240                }
241                
242                return response.toSuccessResponse().getEntityStatement();
243        }
244        
245        
246        private void record(final HTTPRequest httpRequest) {
247                
248                recordedRequests.add(httpRequest.getURI());
249        }
250        
251        
252        /**
253         * Returns the running list of the recorded HTTP requests.
254         *
255         * @return The HTTP request URIs (with query parameters), empty if
256         *         none.
257         */
258        public List<URI> getRecordedRequests() {
259                return recordedRequests;
260        }
261}