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}