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 java.io.IOException;
022import java.net.URI;
023import java.net.URISyntaxException;
024import java.util.LinkedList;
025import java.util.List;
026
027import com.nimbusds.oauth2.sdk.ErrorObject;
028import com.nimbusds.oauth2.sdk.ParseException;
029import com.nimbusds.oauth2.sdk.WellKnownPathComposeStrategy;
030import com.nimbusds.oauth2.sdk.http.HTTPRequest;
031import com.nimbusds.oauth2.sdk.http.HTTPResponse;
032import com.nimbusds.oauth2.sdk.util.StringUtils;
033import com.nimbusds.openid.connect.sdk.federation.api.FetchEntityStatementRequest;
034import com.nimbusds.openid.connect.sdk.federation.api.FetchEntityStatementResponse;
035import com.nimbusds.openid.connect.sdk.federation.config.FederationEntityConfigurationRequest;
036import com.nimbusds.openid.connect.sdk.federation.config.FederationEntityConfigurationResponse;
037import com.nimbusds.openid.connect.sdk.federation.entities.EntityID;
038import com.nimbusds.openid.connect.sdk.federation.entities.EntityStatement;
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        /**
075         * Running list of the recorded HTTP requests.
076         */
077        private final List<URI> recordedRequests = new LinkedList<>();
078        
079        
080        /**
081         * Creates a new entity statement retriever using the default HTTP
082         * timeout settings.
083         */
084        public DefaultEntityStatementRetriever() {
085                this(DEFAULT_HTTP_CONNECT_TIMEOUT_MS, DEFAULT_HTTP_READ_TIMEOUT_MS);
086        }
087        
088        
089        /**
090         * Creates a new entity statement retriever.
091         *
092         * @param httpConnectTimeoutMs The HTTP connect timeout in
093         *                             milliseconds, zero means timeout
094         *                             determined by the underlying HTTP client.
095         * @param httpReadTimeoutMs    The HTTP read timeout in milliseconds,
096         *                             zero means timeout determined by the
097         *                             underlying HTTP client.
098         */
099        public DefaultEntityStatementRetriever(final int httpConnectTimeoutMs,
100                                               final int httpReadTimeoutMs) {
101                this.httpConnectTimeoutMs = httpConnectTimeoutMs;
102                this.httpReadTimeoutMs = httpReadTimeoutMs;
103        }
104        
105        
106        /**
107         * Returns the configured HTTP connect timeout.
108         *
109         * @return The configured HTTP connect timeout in milliseconds, zero
110         *         means timeout determined by the underlying HTTP client.
111         */
112        public int getHTTPConnectTimeout() {
113                return httpConnectTimeoutMs;
114        }
115        
116        
117        /**
118         * Returns the configured HTTP read timeout.
119         *
120         * @return The configured HTTP read timeout in milliseconds, zero
121         *         means timeout determined by the underlying HTTP client.
122         */
123        public int getHTTPReadTimeout() {
124                return httpReadTimeoutMs;
125        }
126        
127        
128        void applyTimeouts(final HTTPRequest httpRequest) {
129                httpRequest.setConnectTimeout(httpConnectTimeoutMs);
130                httpRequest.setReadTimeout(httpReadTimeoutMs);
131        }
132        
133        
134        @Override
135        public EntityStatement fetchSelfIssuedEntityStatement(final EntityID target)
136                throws ResolveException {
137                
138                FederationEntityConfigurationRequest request = new FederationEntityConfigurationRequest(target);
139                HTTPRequest httpRequest = request.toHTTPRequest();
140                applyTimeouts(httpRequest);
141                
142                record(httpRequest);
143                
144                HTTPResponse httpResponse;
145                try {
146                        httpResponse = httpRequest.send();
147                } catch (IOException e) {
148                        throw new ResolveException("Couldn't retrieve entity configuration for " + httpRequest.getURL() + ": " + e.getMessage(), e);
149                }
150                
151                if (StringUtils.isNotBlank(target.toURI().getPath()) && HTTPResponse.SC_NOT_FOUND == httpResponse.getStatusCode()) {
152                        // We have a path in the entity ID URL, try infix strategy
153                        request = new FederationEntityConfigurationRequest(target, WellKnownPathComposeStrategy.INFIX);
154                        httpRequest = request.toHTTPRequest();
155                        applyTimeouts(httpRequest);
156                        
157                        record(httpRequest);
158                        
159                        try {
160                                httpResponse = httpRequest.send();
161                        } catch (IOException e) {
162                                throw new ResolveException("Couldn't retrieve entity configuration for " + httpRequest.getURL() + ": " + e.getMessage(), e);
163                        }
164                }
165                
166                FederationEntityConfigurationResponse response;
167                try {
168                        response = FederationEntityConfigurationResponse.parse(httpResponse);
169                } catch (ParseException e) {
170                        throw new ResolveException("Error parsing entity configuration response from " + httpRequest.getURL() + ": " + e.getMessage(), e);
171                }
172                
173                if (! response.indicatesSuccess()) {
174                        ErrorObject errorObject = response.toErrorResponse().getErrorObject();
175                        throw new ResolveException("Entity configuration error response from " + httpRequest.getURL() + ": " +
176                                errorObject.getHTTPStatusCode() +
177                                (errorObject.getCode() != null ? " " + errorObject.getCode() : ""),
178                                errorObject);
179                }
180                
181                return response.toSuccessResponse().getEntityStatement();
182        }
183        
184        
185        @Override
186        public EntityStatement fetchEntityStatement(final URI federationAPIEndpoint, final EntityID issuer, final EntityID subject)
187                throws ResolveException {
188                
189                FetchEntityStatementRequest request = new FetchEntityStatementRequest(federationAPIEndpoint, issuer, subject, null);
190                HTTPRequest httpRequest = request.toHTTPRequest();
191                applyTimeouts(httpRequest);
192                
193                record(httpRequest);
194                
195                HTTPResponse httpResponse;
196                try {
197                        httpResponse = httpRequest.send();
198                } catch (IOException e) {
199                        throw new ResolveException("Couldn't fetch entity statement from " + issuer + " at " + federationAPIEndpoint + ": " + e.getMessage(), e);
200                }
201                
202                FetchEntityStatementResponse response;
203                try {
204                        response = FetchEntityStatementResponse.parse(httpResponse);
205                } catch (ParseException e) {
206                        throw new ResolveException("Error parsing entity statement response from " + issuer + " at " + federationAPIEndpoint + ": " + e.getMessage(), e);
207                }
208                
209                if (! response.indicatesSuccess()) {
210                        ErrorObject errorObject = response.toErrorResponse().getErrorObject();
211                        throw new ResolveException("Entity statement error response from " + issuer + " at " + federationAPIEndpoint + ": " +
212                                errorObject.getHTTPStatusCode() +
213                                (errorObject.getCode() != null ? " " + errorObject.getCode() : ""),
214                                errorObject);
215                }
216                
217                return response.toSuccessResponse().getEntityStatement();
218        }
219        
220        
221        private void record(final HTTPRequest httpRequest) {
222                
223                URI uri = null;
224                if (httpRequest.getQuery() == null) {
225                        uri = httpRequest.getURI();
226                } else {
227                        try {
228                                uri = new URI(httpRequest.getURL() + "?" + httpRequest.getQuery());
229                        } catch (URISyntaxException e) {
230                                // ignore
231                        }
232                }
233                
234                recordedRequests.add(uri);
235        }
236        
237        
238        /**
239         * Returns the running list of the recorded HTTP requests.
240         *
241         * @return The HTTP request URIs (with query parameters), empty if
242         *         none.
243         */
244        public List<URI> getRecordedRequests() {
245                return recordedRequests;
246        }
247}