001package ca.uhn.fhir.util; 002 003import ca.uhn.fhir.model.primitive.IdDt; 004import ca.uhn.fhir.rest.api.Constants; 005import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 006import com.google.common.escape.Escaper; 007import com.google.common.net.PercentEscaper; 008 009import java.io.UnsupportedEncodingException; 010import java.net.MalformedURLException; 011import java.net.URL; 012import java.net.URLDecoder; 013import java.util.*; 014import java.util.Map.Entry; 015 016import static org.apache.commons.lang3.StringUtils.defaultIfBlank; 017import static org.apache.commons.lang3.StringUtils.defaultString; 018import static org.apache.commons.lang3.StringUtils.isBlank; 019 020/* 021 * #%L 022 * HAPI FHIR - Core Library 023 * %% 024 * Copyright (C) 2014 - 2018 University Health Network 025 * %% 026 * Licensed under the Apache License, Version 2.0 (the "License"); 027 * you may not use this file except in compliance with the License. 028 * You may obtain a copy of the License at 029 * 030 * http://www.apache.org/licenses/LICENSE-2.0 031 * 032 * Unless required by applicable law or agreed to in writing, software 033 * distributed under the License is distributed on an "AS IS" BASIS, 034 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 035 * See the License for the specific language governing permissions and 036 * limitations under the License. 037 * #L% 038 */ 039 040public class UrlUtil { 041 private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(UrlUtil.class); 042 043 private static final String URL_FORM_PARAMETER_OTHER_SAFE_CHARS = "-_.*"; 044 private static final Escaper PARAMETER_ESCAPER = new PercentEscaper(URL_FORM_PARAMETER_OTHER_SAFE_CHARS, false); 045 046 047 /** 048 * Resolve a relative URL - THIS METHOD WILL NOT FAIL but will log a warning and return theEndpoint if the input is invalid. 049 */ 050 public static String constructAbsoluteUrl(String theBase, String theEndpoint) { 051 if (theEndpoint == null) { 052 return null; 053 } 054 if (isAbsolute(theEndpoint)) { 055 return theEndpoint; 056 } 057 if (theBase == null) { 058 return theEndpoint; 059 } 060 061 try { 062 return new URL(new URL(theBase), theEndpoint).toString(); 063 } catch (MalformedURLException e) { 064 ourLog.warn("Failed to resolve relative URL[" + theEndpoint + "] against absolute base[" + theBase + "]", e); 065 return theEndpoint; 066 } 067 } 068 069 public static String constructRelativeUrl(String theParentExtensionUrl, String theExtensionUrl) { 070 if (theParentExtensionUrl == null) { 071 return theExtensionUrl; 072 } 073 if (theExtensionUrl == null) { 074 return null; 075 } 076 077 int parentLastSlashIdx = theParentExtensionUrl.lastIndexOf('/'); 078 int childLastSlashIdx = theExtensionUrl.lastIndexOf('/'); 079 080 if (parentLastSlashIdx == -1 || childLastSlashIdx == -1) { 081 return theExtensionUrl; 082 } 083 084 if (parentLastSlashIdx != childLastSlashIdx) { 085 return theExtensionUrl; 086 } 087 088 if (!theParentExtensionUrl.substring(0, parentLastSlashIdx).equals(theExtensionUrl.substring(0, parentLastSlashIdx))) { 089 return theExtensionUrl; 090 } 091 092 if (theExtensionUrl.length() > parentLastSlashIdx) { 093 return theExtensionUrl.substring(parentLastSlashIdx + 1); 094 } 095 096 return theExtensionUrl; 097 } 098 099 /** 100 * URL encode a value according to RFC 3986 101 * <p> 102 * This method is intended to be applied to an individual parameter 103 * name or value. For example, if you are creating the URL 104 * <code>http://example.com/fhir/Patient?key=føø</code> 105 * it would be appropriate to pass the string "føø" to this method, 106 * but not appropriate to pass the entire URL since characters 107 * such as "/" and "?" would also be escaped. 108 * </P> 109 */ 110 public static String escapeUrlParam(String theUnescaped) { 111 if (theUnescaped == null) { 112 return null; 113 } 114 return PARAMETER_ESCAPER.escape(theUnescaped); 115 } 116 117 118 public static boolean isAbsolute(String theValue) { 119 String value = theValue.toLowerCase(); 120 return value.startsWith("http://") || value.startsWith("https://"); 121 } 122 123 public static boolean isNeedsSanitization(String theString) { 124 if (theString != null) { 125 for (int i = 0; i < theString.length(); i++) { 126 char nextChar = theString.charAt(i); 127 if (nextChar == '<' || nextChar == '"') { 128 return true; 129 } 130 } 131 } 132 return false; 133 } 134 135 public static boolean isValid(String theUrl) { 136 if (theUrl == null || theUrl.length() < 8) { 137 return false; 138 } 139 140 String url = theUrl.toLowerCase(); 141 if (url.charAt(0) != 'h') { 142 return false; 143 } 144 if (url.charAt(1) != 't') { 145 return false; 146 } 147 if (url.charAt(2) != 't') { 148 return false; 149 } 150 if (url.charAt(3) != 'p') { 151 return false; 152 } 153 int slashOffset; 154 if (url.charAt(4) == ':') { 155 slashOffset = 5; 156 } else if (url.charAt(4) == 's') { 157 if (url.charAt(5) != ':') { 158 return false; 159 } 160 slashOffset = 6; 161 } else { 162 return false; 163 } 164 165 if (url.charAt(slashOffset) != '/') { 166 return false; 167 } 168 if (url.charAt(slashOffset + 1) != '/') { 169 return false; 170 } 171 172 return true; 173 } 174 175 public static void main(String[] args) { 176 System.out.println(escapeUrlParam("http://snomed.info/sct?fhir_vs=isa/126851005")); 177 } 178 179 public static Map<String, String[]> parseQueryString(String theQueryString) { 180 HashMap<String, List<String>> map = new HashMap<>(); 181 parseQueryString(theQueryString, map); 182 return toQueryStringMap(map); 183 } 184 185 private static void parseQueryString(String theQueryString, HashMap<String, List<String>> map) { 186 String query = defaultString(theQueryString); 187 if (query.startsWith("?")) { 188 query = query.substring(1); 189 } 190 191 192 StringTokenizer tok = new StringTokenizer(query, "&"); 193 while (tok.hasMoreTokens()) { 194 String nextToken = tok.nextToken(); 195 if (isBlank(nextToken)) { 196 continue; 197 } 198 199 int equalsIndex = nextToken.indexOf('='); 200 String nextValue; 201 String nextKey; 202 if (equalsIndex == -1) { 203 nextKey = nextToken; 204 nextValue = ""; 205 } else { 206 nextKey = nextToken.substring(0, equalsIndex); 207 nextValue = nextToken.substring(equalsIndex + 1); 208 } 209 210 nextKey = unescape(nextKey); 211 nextValue = unescape(nextValue); 212 213 List<String> list = map.computeIfAbsent(nextKey, k -> new ArrayList<>()); 214 list.add(nextValue); 215 } 216 } 217 218 public static Map<String, String[]> parseQueryStrings(String... theQueryString) { 219 HashMap<String, List<String>> map = new HashMap<>(); 220 for (String next : theQueryString) { 221 parseQueryString(next, map); 222 } 223 return toQueryStringMap(map); 224 } 225 226 /** 227 * Parse a URL in one of the following forms: 228 * <ul> 229 * <li>[Resource Type]?[Search Params] 230 * <li>[Resource Type]/[Resource ID] 231 * <li>[Resource Type]/[Resource ID]/_history/[Version ID] 232 * </ul> 233 */ 234 public static UrlParts parseUrl(String theUrl) { 235 String url = theUrl; 236 UrlParts retVal = new UrlParts(); 237 if (url.startsWith("http")) { 238 if (url.startsWith("/")) { 239 url = url.substring(1); 240 } 241 242 int qmIdx = url.indexOf('?'); 243 if (qmIdx != -1) { 244 retVal.setParams(defaultIfBlank(url.substring(qmIdx + 1), null)); 245 url = url.substring(0, qmIdx); 246 } 247 248 IdDt id = new IdDt(url); 249 retVal.setResourceType(id.getResourceType()); 250 retVal.setResourceId(id.getIdPart()); 251 retVal.setVersionId(id.getVersionIdPart()); 252 return retVal; 253 } 254 if (url.matches("/[a-zA-Z]+\\?.*")) { 255 url = url.substring(1); 256 } 257 int nextStart = 0; 258 boolean nextIsHistory = false; 259 260 for (int idx = 0; idx < url.length(); idx++) { 261 char nextChar = url.charAt(idx); 262 boolean atEnd = (idx + 1) == url.length(); 263 if (nextChar == '?' || nextChar == '/' || atEnd) { 264 int endIdx = (atEnd && nextChar != '?') ? idx + 1 : idx; 265 String nextSubstring = url.substring(nextStart, endIdx); 266 if (retVal.getResourceType() == null) { 267 retVal.setResourceType(nextSubstring); 268 } else if (retVal.getResourceId() == null) { 269 retVal.setResourceId(nextSubstring); 270 } else if (nextIsHistory) { 271 retVal.setVersionId(nextSubstring); 272 } else { 273 if (nextSubstring.equals(Constants.URL_TOKEN_HISTORY)) { 274 nextIsHistory = true; 275 } else { 276 throw new InvalidRequestException("Invalid FHIR resource URL: " + url); 277 } 278 } 279 if (nextChar == '?') { 280 if (url.length() > idx + 1) { 281 retVal.setParams(url.substring(idx + 1, url.length())); 282 } 283 break; 284 } 285 nextStart = idx + 1; 286 } 287 } 288 289 return retVal; 290 291 } 292 293 /** 294 * This method specifically HTML-encodes the " and 295 * < characters in order to prevent injection attacks 296 */ 297 public static String sanitizeUrlPart(String theString) { 298 if (theString == null) { 299 return null; 300 } 301 302 boolean needsSanitization = isNeedsSanitization(theString); 303 304 if (needsSanitization) { 305 // Ok, we're sanitizing 306 StringBuilder buffer = new StringBuilder(theString.length() + 10); 307 for (int j = 0; j < theString.length(); j++) { 308 309 char nextChar = theString.charAt(j); 310 switch (nextChar) { 311 case '"': 312 buffer.append("""); 313 break; 314 case '<': 315 buffer.append("<"); 316 break; 317 default: 318 buffer.append(nextChar); 319 break; 320 } 321 322 } // for build escaped string 323 324 return buffer.toString(); 325 } 326 327 return theString; 328 } 329 330 private static Map<String, String[]> toQueryStringMap(HashMap<String, List<String>> map) { 331 HashMap<String, String[]> retVal = new HashMap<>(); 332 for (Entry<String, List<String>> nextEntry : map.entrySet()) { 333 retVal.put(nextEntry.getKey(), nextEntry.getValue().toArray(new String[0])); 334 } 335 return retVal; 336 } 337 338 public static String unescape(String theString) { 339 if (theString == null) { 340 return null; 341 } 342 for (int i = 0; i < theString.length(); i++) { 343 char nextChar = theString.charAt(i); 344 if (nextChar == '%' || nextChar == '+') { 345 try { 346 return URLDecoder.decode(theString, "UTF-8"); 347 } catch (UnsupportedEncodingException e) { 348 throw new Error("UTF-8 not supported, this shouldn't happen", e); 349 } 350 } 351 } 352 return theString; 353 } 354 355 public static class UrlParts { 356 private String myParams; 357 private String myResourceId; 358 private String myResourceType; 359 private String myVersionId; 360 361 public String getParams() { 362 return myParams; 363 } 364 365 public void setParams(String theParams) { 366 myParams = theParams; 367 } 368 369 public String getResourceId() { 370 return myResourceId; 371 } 372 373 public void setResourceId(String theResourceId) { 374 myResourceId = theResourceId; 375 } 376 377 public String getResourceType() { 378 return myResourceType; 379 } 380 381 public void setResourceType(String theResourceType) { 382 myResourceType = theResourceType; 383 } 384 385 public String getVersionId() { 386 return myVersionId; 387 } 388 389 public void setVersionId(String theVersionId) { 390 myVersionId = theVersionId; 391 } 392 } 393 394}