001/*******************************************************************************
002 * Copyright 2017 The MIT Internet Trust Consortium
003 *
004 * Portions copyright 2011-2013 The MITRE Corporation
005 *
006 * Licensed under the Apache License, Version 2.0 (the "License");
007 * you may not use this file except in compliance with the License.
008 * You may obtain a copy of the 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
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 *******************************************************************************/
018package org.mitre.discovery.util;
019
020import java.util.regex.Matcher;
021import java.util.regex.Pattern;
022
023import org.slf4j.Logger;
024import org.slf4j.LoggerFactory;
025import org.springframework.util.StringUtils;
026import org.springframework.web.util.UriComponents;
027import org.springframework.web.util.UriComponentsBuilder;
028
029import com.google.common.base.Strings;
030
031/**
032 * Provides utility methods for normalizing and parsing URIs for use with Webfinger Discovery.
033 *
034 * @author wkim
035 *
036 */
037public class WebfingerURLNormalizer {
038
039        /**
040         * Logger for this class
041         */
042        private static final Logger logger = LoggerFactory.getLogger(WebfingerURLNormalizer.class);
043
044        // pattern used to parse user input; we can't use the built-in java URI parser
045        private static final Pattern pattern = Pattern.compile("^" +
046                        "((https|acct|http|mailto|tel|device):(//)?)?" + // scheme
047                        "(" +
048                        "(([^@]+)@)?" + // userinfo
049                        "(([^\\?#:/]+)" + // host
050                        "(:(\\d*))?)" + // port
051                        ")" +
052                        "([^\\?#]+)?" + // path
053                        "(\\?([^#]+))?" + // query
054                        "(#(.*))?" +  // fragment
055                        "$"
056                        );
057
058
059
060        /**
061         * Private constructor to prevent instantiation.
062         */
063        private WebfingerURLNormalizer() {
064                // intentionally blank
065        }
066
067        /**
068         * Normalize the resource string as per OIDC Discovery.
069         * @param identifier
070         * @return the normalized string, or null if the string can't be normalized
071         */
072        public static UriComponents normalizeResource(String identifier) {
073                // try to parse the URI
074                // NOTE: we can't use the Java built-in URI class because it doesn't split the parts appropriately
075
076                if (Strings.isNullOrEmpty(identifier)) {
077                        logger.warn("Can't normalize null or empty URI: " + identifier);
078                        return null; // nothing we can do
079                } else {
080
081                        //UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(identifier);
082                        UriComponentsBuilder builder = UriComponentsBuilder.newInstance();
083
084                        Matcher m = pattern.matcher(identifier);
085                        if (m.matches()) {
086                                builder.scheme(m.group(2));
087                                builder.userInfo(m.group(6));
088                                builder.host(m.group(8));
089                                String port = m.group(10);
090                                if (!Strings.isNullOrEmpty(port)) {
091                                        builder.port(Integer.parseInt(port));
092                                }
093                                builder.path(m.group(11));
094                                builder.query(m.group(13));
095                                builder.fragment(m.group(15)); // we throw away the hash, but this is the group it would be if we kept it
096                        } else {
097                                // doesn't match the pattern, throw it out
098                                logger.warn("Parser couldn't match input: " + identifier);
099                                return null;
100                        }
101
102                        UriComponents n = builder.build();
103
104                        if (Strings.isNullOrEmpty(n.getScheme())) {
105                                if (!Strings.isNullOrEmpty(n.getUserInfo())
106                                                && Strings.isNullOrEmpty(n.getPath())
107                                                && Strings.isNullOrEmpty(n.getQuery())
108                                                && n.getPort() < 0) {
109
110                                        // scheme empty, userinfo is not empty, path/query/port are empty
111                                        // set to "acct" (rule 2)
112                                        builder.scheme("acct");
113
114                                } else {
115                                        // scheme is empty, but rule 2 doesn't apply
116                                        // set scheme to "https" (rule 3)
117                                        builder.scheme("https");
118                                }
119                        }
120
121                        // fragment must be stripped (rule 4)
122                        builder.fragment(null);
123
124                        return builder.build();
125                }
126
127
128        }
129
130
131        public static String serializeURL(UriComponents uri) {
132                if (uri.getScheme() != null &&
133                                (uri.getScheme().equals("acct") ||
134                                                uri.getScheme().equals("mailto") ||
135                                                uri.getScheme().equals("tel") ||
136                                                uri.getScheme().equals("device")
137                                                )) {
138
139                        // serializer copied from HierarchicalUriComponents but with "//" removed
140
141                        StringBuilder uriBuilder = new StringBuilder();
142
143                        if (uri.getScheme() != null) {
144                                uriBuilder.append(uri.getScheme());
145                                uriBuilder.append(':');
146                        }
147
148                        if (uri.getUserInfo() != null || uri.getHost() != null) {
149                                if (uri.getUserInfo() != null) {
150                                        uriBuilder.append(uri.getUserInfo());
151                                        uriBuilder.append('@');
152                                }
153                                if (uri.getHost() != null) {
154                                        uriBuilder.append(uri.getHost());
155                                }
156                                if (uri.getPort() != -1) {
157                                        uriBuilder.append(':');
158                                        uriBuilder.append(uri.getPort());
159                                }
160                        }
161
162                        String path = uri.getPath();
163                        if (StringUtils.hasLength(path)) {
164                                if (uriBuilder.length() != 0 && path.charAt(0) != '/') {
165                                        uriBuilder.append('/');
166                                }
167                                uriBuilder.append(path);
168                        }
169
170                        String query = uri.getQuery();
171                        if (query != null) {
172                                uriBuilder.append('?');
173                                uriBuilder.append(query);
174                        }
175
176                        if (uri.getFragment() != null) {
177                                uriBuilder.append('#');
178                                uriBuilder.append(uri.getFragment());
179                        }
180
181                        return uriBuilder.toString();
182                } else {
183                        return uri.toUriString();
184                }
185
186        }
187
188
189}