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