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