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