001/*
002 * oauth2-oidc-sdk
003 *
004 * Copyright 2012-2016, 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.oauth2.sdk.util;
019
020
021import java.net.URI;
022import java.net.URISyntaxException;
023import java.util.*;
024
025
026/**
027 * URI operations.
028 */
029public final class URIUtils {
030
031
032        /**
033         * Gets the base part (schema, host, port and path) of the specified
034         * URI.
035         *
036         * @param uri The URI. May be {@code null}.
037         *
038         * @return The base part of the URI, {@code null} if the original URI
039         *         is {@code null} or doesn't specify a protocol.
040         */
041        public static URI getBaseURI(final URI uri) {
042
043                if (uri == null)
044                        return null;
045
046                try {
047                        return new URI(uri.getScheme(), null, uri.getHost(), uri.getPort(), uri.getPath(), null, null);
048
049                } catch (URISyntaxException e) {
050
051                        return null;
052                }
053        }
054        
055        
056        /**
057         * Prepends the specified path component to a URI. The prepended and
058         * any existing path component are always joined with a single slash
059         * ('/') between them
060         *
061         * @param uri           The URI, {@code null} if not specified.
062         * @param pathComponent The path component to prepend, {@code null} if
063         *                      not specified.
064         *
065         * @return The URI with prepended path component, {@code null} if the
066         *         original URI wasn't specified.
067         */
068        public static URI prependPath(final URI uri, final String pathComponent) {
069                
070                if (uri == null) {
071                        return null;
072                }
073                
074                if (StringUtils.isBlank(pathComponent)) {
075                        return uri;
076                }
077                
078                String origPath = uri.getPath();
079                if (origPath == null || origPath.isEmpty() || origPath.equals("/")) {
080                        origPath = null;
081                }
082                String joinedPath = joinPathComponents(pathComponent, origPath);
083                joinedPath = prependLeadingSlashIfMissing(joinedPath);
084                
085                try {
086                        return new URI(
087                                uri.getScheme(), null, uri.getHost(), uri.getPort(),
088                                joinedPath,
089                                uri.getQuery(), uri.getFragment());
090                } catch (URISyntaxException e) {
091                        // should never happen when starting from a legal URI
092                        return null;
093                }
094        }
095        
096        
097        /**
098         * Prepends a leading slash `/` if missing to the specified string.
099         *
100         * @param s The string, {@code null} if not specified.
101         *
102         * @return The string with leading slash, {@code null} if not
103         *         originally specified.
104         */
105        public static String prependLeadingSlashIfMissing(String s) {
106                if (s == null) {
107                        return null;
108                }
109                if (s.startsWith("/")) {
110                        return s;
111                }
112                return "/" + s;
113        }
114        
115        
116        /**
117         * Strips any leading slashes '/' if present from the specified string.
118         *
119         * @param s The string, {@code null} if not specified.
120         *
121         * @return The string with no leading slash, {@code null} if not
122         *         originally specified.
123         */
124        public static String stripLeadingSlashIfPresent(final String s) {
125                if (StringUtils.isBlank(s)) {
126                        return s;
127                }
128                if (s.startsWith("/")) {
129                        String tmp = s;
130                        while (tmp.startsWith("/")) {
131                                tmp = tmp.substring(1);
132                        }
133                        return tmp;
134                }
135                return s;
136        }
137        
138        
139        /**
140         * Joins two path components. If the two path components are not
141         * {@code null} or empty they are joined so that there is only a single
142         * slash ('/') between them.
143         *
144         * @param c1 The first path component, {@code null} if not specified.
145         * @param c2 The second path component, {@code null} if not specified.
146         *
147         * @return The joined path components, {@code null} if both are not
148         *         specified, or if one is {@code null} the other unmodified.
149         */
150        public static String joinPathComponents(final String c1, final String c2) {
151                
152                if (c1 == null && c2 == null) {
153                        return null;
154                }
155                
156                if (c1 == null || c1.isEmpty()) {
157                        return c2;
158                }
159                
160                if (c2 == null || c2.isEmpty()) {
161                        return c1;
162                }
163                
164                if (c1.endsWith("/") && ! c2.startsWith("/")) {
165                        return c1 + c2;
166                }
167                if (! c1.endsWith("/") && c2.startsWith("/")) {
168                        return c1 + c2;
169                }
170                if (c1.endsWith("/") && c2.startsWith("/")) {
171                        return c1 + stripLeadingSlashIfPresent(c2);
172                }
173                return c1 + "/" + c2;
174        }
175        
176        
177        /**
178         * Strips the query string from the specified URI.
179         *
180         * @param uri The URI. May be {@code null}.'
181         *
182         * @return The URI with stripped query string, {@code null} if the
183         *         original URI is {@code null} or doesn't specify a protocol.
184         */
185        public static URI stripQueryString(final URI uri) {
186                
187                if (uri == null)
188                        return null;
189                
190                try {
191                        return new URI(uri.getScheme(), null, uri.getHost(), uri.getPort(), uri.getPath(), null, uri.getFragment());
192                        
193                } catch (URISyntaxException e) {
194                        return null;
195                }
196        }
197        
198        
199        /**
200         * Removes the trailing slash ("/") from the specified URI, if present.
201         *
202         * @param uri The URI. May be {@code null}.
203         *
204         * @return The URI with no trailing slash, {@code null} if the original
205         *         URI is {@code null}.
206         */
207        public static URI removeTrailingSlash(final URI uri) {
208                
209                if (uri == null)
210                        return null;
211                
212                String uriString = uri.toString();
213                
214                if (uriString.charAt(uriString.length() - 1 ) == '/') {
215                        return URI.create(uriString.substring(0, uriString.length() - 1));
216                }
217                
218                return uri;
219        }
220        
221        
222        /**
223         * Ensures the scheme of the specified URI is https.
224         *
225         * @param uri The URI to check, {@code null} if not specified.
226         *
227         * @throws IllegalArgumentException If the URI is specified and the
228         *                                  scheme is not https.
229         */
230        public static void ensureSchemeIsHTTPS(final URI uri) {
231                
232                if (uri == null) {
233                        return;
234                }
235                
236                if (uri.getScheme() == null || ! "https".equalsIgnoreCase(uri.getScheme())) {
237                        throw new IllegalArgumentException("The URI scheme must be https");
238                }
239        }
240        
241        
242        /**
243         * Ensures the scheme of the specified URI is https or http.
244         *
245         * @param uri The URI to check, {@code null} if not specified.
246         *
247         * @throws IllegalArgumentException If the URI is specified and the
248         *                                  scheme is not https or http.
249         */
250        public static void ensureSchemeIsHTTPSorHTTP(final URI uri) {
251                
252                if (uri == null) {
253                        return;
254                }
255                
256                if (uri.getScheme() == null || ! Arrays.asList("http", "https").contains(uri.getScheme().toLowerCase())) {
257                        throw new IllegalArgumentException("The URI scheme must be https or http");
258                }
259        }
260        
261        
262        /**
263         * Ensures the scheme of the specified URI is not prohibited.
264         *
265         * @param uri                  The URI to check, {@code null} if not
266         *                             specified.
267         * @param prohibitedURISchemes The prohibited URI schemes (should be in
268         *                             lower case), empty or {@code null} if
269         *                             not specified.
270         *
271         * @throws IllegalArgumentException If the URI is specified and its
272         *                                  scheme is prohibited.
273         */
274        public static void ensureSchemeIsNotProhibited(final URI uri, final Set<String> prohibitedURISchemes) {
275                
276                if (uri == null || uri.getScheme() == null || prohibitedURISchemes == null || prohibitedURISchemes.isEmpty()) {
277                        return;
278                }
279                
280                if (prohibitedURISchemes.contains(uri.getScheme().toLowerCase())) {
281                        throw new IllegalArgumentException("The URI scheme " + uri.getScheme() + " is prohibited");
282                }
283        }
284
285
286        /**
287         * Ensures the query of the specified URI is not prohibited.
288         *
289         * @param uri                       The URI to check, {@code null} if
290         *                                  not specified.
291         * @param prohibitedQueryParamNames The prohibited query parameter
292         *                                  names, empty or {@code null} if not
293         *                                  specified.
294         *
295         * @throws IllegalArgumentException If the URI is specified and
296         *                                  includes prohibited query parameter
297         *                                  names.
298         */
299        public static void ensureQueryIsNotProhibited(final URI uri, final Set<String> prohibitedQueryParamNames) {
300
301                if (uri == null || uri.getQuery() == null || uri.getQuery().isEmpty() || prohibitedQueryParamNames == null || prohibitedQueryParamNames.isEmpty()) {
302                        return;
303                }
304
305                Map<String, List<String>> params = URLUtils.parseParameters(uri.getQuery());
306
307                for (String paramName: params.keySet()) {
308                        if (prohibitedQueryParamNames.contains(paramName)) {
309                                throw new IllegalArgumentException("The query parameter " + paramName + " is prohibited");
310                        }
311                }
312        }
313        
314        
315        /**
316         * Returns a string list representation of the specified URI
317         * collection. Collection items that are {@code null} are not returned.
318         *
319         * @param uriList The URI collection, {@code null} if not specified.
320         *
321         * @return The string list, {@code null} if not specified.
322         */
323        public static List<String> toStringList(final Collection<URI> uriList) {
324                
325                return toStringList(uriList, true);
326        }
327        
328        
329        /**
330         * Returns a string list representation of the specified URI
331         * collection.
332         *
333         * @param uriList     The URI collection, {@code null} if not
334         *                    specified.
335         * @param ignoreNulls {@code true} to not include {@code null} values.
336         *
337         * @return The string list, {@code null} if not specified.
338         */
339        public static List<String> toStringList(final Collection<URI> uriList, final boolean ignoreNulls) {
340                
341                if (uriList == null) {
342                        return null;
343                }
344                
345                if (uriList.isEmpty()) {
346                        return Collections.emptyList();
347                }
348                
349                List<String> out = new LinkedList<>();
350                for (URI uri: uriList) {
351                        if (uri != null) {
352                                out.add(uri.toString());
353                        } else if (! ignoreNulls) {
354                                out.add(null);
355                        }
356                }
357                return out;
358        }
359
360
361        /**
362         * Returns {@code true} if the specified URI is for a localhost,
363         * {@code 127.0.0.1} IPv4 or {@code ::1} / {@code 0:0:0:0:0:0:0:1}
364         * address.
365         *
366         * @param uri The URI. Must not be {@code null}.
367         *
368         * @return {@code true} if the URI is for a localhost, else
369         *         {@code false}.
370         */
371        public static boolean isLocalHost(final URI uri) {
372
373                String host = uri.getHost();
374
375                return "localhost".equals(host)
376                        || "127.0.0.1".equals(host)
377                        || "[::1]".equals(host)
378                        || "[0:0:0:0:0:0:0:1]".equals(host);
379        }
380
381
382        /**
383         * Prevents public instantiation.
384         */
385        private URIUtils() {}
386}