/** * Copyright 2010 Newcastle University * * http://research.ncl.ac.uk/smart/ * * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.picketlink.oauth; import org.picketlink.oauth.common.OAuthConstants; import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.net.URLEncoder; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.StringTokenizer; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Common OAuth Utils class. * <p/> * Some methods based on the Utils class from OAuth V1.0a library available at: http://oauth.googlecode.com/svn/code/java/core/ * * * * */ public final class OAuthUtils { private static final String ENCODING = "UTF-8"; private static final String PARAMETER_SEPARATOR = "&"; private static final String NAME_VALUE_SEPARATOR = "="; public static final String AUTH_SCHEME = OAuthConstants.HEADER_NAME; private static final Pattern OAUTH_HEADER = Pattern.compile("\\s*(\\w*)\\s+(.*)"); private static final Pattern NVP = Pattern.compile("(\\S*)\\s*\\=\\s*\"([^\"]*)\""); public static final String MULTIPART = "multipart/"; private static final String DEFAULT_CONTENT_CHARSET = ENCODING; /** * Translates parameters into <code>application/x-www-form-urlencoded</code> String * * @param parameters parameters to sign * @param encoding The name of a supported <a href="../lang/package-summary.html#charenc">character encoding</a>. * @return Translated string */ public static String format(final Collection<? extends Map.Entry<String, Object>> parameters, final String encoding) { final StringBuilder result = new StringBuilder(); for (final Map.Entry<String, Object> parameter : parameters) { String value = parameter.getValue() == null ? null : String.valueOf(parameter.getValue()); if (!OAuthUtils.isEmpty(parameter.getKey()) && !OAuthUtils.isEmpty(value)) { final String encodedName = encode(parameter.getKey(), encoding); final String encodedValue = value != null ? encode(value, encoding) : ""; if (result.length() > 0) { result.append(PARAMETER_SEPARATOR); } result.append(encodedName); result.append(NAME_VALUE_SEPARATOR); result.append(encodedValue); } } return result.toString(); } private static String encode(final String content, final String encoding) { try { return URLEncoder.encode(content, encoding != null ? encoding : "UTF-8"); } catch (UnsupportedEncodingException problem) { throw new IllegalArgumentException(problem); } } /** * Read data from Input Stream and save it as a String. * * @param is InputStream to be read * @return String that was read from the stream */ public static String saveStreamAsString(InputStream is) throws IOException { return toString(is, ENCODING); } /** * Get the entity content as a String, using the provided default character set if none is found in the entity. If * defaultCharset is null, the default "UTF-8" is used. * * @param is input stream to be saved as string * @param defaultCharset character set to be applied if none found in the entity * @return the entity content as a String * @throws IllegalArgumentException if entity is null or if content length > Integer.MAX_VALUE * @throws IOException if an error occurs reading the input stream */ public static String toString(final InputStream is, final String defaultCharset) throws IOException { if (is == null) { throw new IllegalArgumentException("InputStream may not be null"); } String charset = defaultCharset; if (charset == null) { charset = DEFAULT_CONTENT_CHARSET; } Reader reader = new InputStreamReader(is, charset); StringBuilder sb = new StringBuilder(); int l; try { char[] tmp = new char[4096]; while ((l = reader.read(tmp)) != -1) { sb.append(tmp, 0, l); } } finally { reader.close(); } return sb.toString(); } /** * Parse a form-urlencoded document. */ public static Map<String, Object> decodeForm(String form) { Map<String, Object> params = new HashMap<String, Object>(); if (!OAuthUtils.isEmpty(form)) { for (String nvp : form.split("\\&")) { int equals = nvp.indexOf('='); String name; String value; if (equals < 0) { name = decodePercent(nvp); value = null; } else { name = decodePercent(nvp.substring(0, equals)); value = decodePercent(nvp.substring(equals + 1)); } params.put(name, value); } } return params; } /** * Return true if the given Content-Type header means FORM_ENCODED. */ public static boolean isFormEncoded(String contentType) { if (contentType == null) { return false; } int semi = contentType.indexOf(";"); if (semi >= 0) { contentType = contentType.substring(0, semi); } return OAuthConstants.ContentType.URL_ENCODED.equalsIgnoreCase(contentType.trim()); } public static String decodePercent(String s) { try { return URLDecoder.decode(s, ENCODING); // This implements http://oauth.pbwiki.com/FlexibleDecoding } catch (java.io.UnsupportedEncodingException wow) { throw new RuntimeException(wow.getMessage(), wow); } } /** * Construct a &-separated list of the given values, percentEncoded. */ @SuppressWarnings("rawtypes") public static String percentEncode(Iterable values) { StringBuilder p = new StringBuilder(); for (Object v : values) { String stringValue = toString(v); if (!isEmpty(stringValue)) { if (p.length() > 0) { p.append("&"); } p.append(OAuthUtils.percentEncode(toString(v))); } } return p.toString(); } public static String percentEncode(String s) { if (s == null) { return ""; } try { return URLEncoder.encode(s, ENCODING) // OAuth encodes some characters differently: .replace("+", "%20").replace("*", "%2A").replace("%7E", "~"); // This could be done faster with more hand-crafted code. } catch (UnsupportedEncodingException wow) { throw new RuntimeException(wow.getMessage(), wow); } } private static String toString(Object from) { return (from == null) ? null : from.toString(); } @SuppressWarnings("unused") private static boolean isEmpty(Set<String> missingParams) { if (missingParams == null || missingParams.size() == 0) { return true; } return false; } public static String getAuthHeaderField(String authHeader) { if (authHeader != null) { Matcher m = OAUTH_HEADER.matcher(authHeader); if (m.matches()) { if (AUTH_SCHEME.equalsIgnoreCase(m.group(1))) { return m.group(2); } } } return null; } public static Map<String, String> decodeOAuthHeader(String header) { Map<String, String> headerValues = new HashMap<String, String>(); if (header != null) { Matcher m = OAUTH_HEADER.matcher(header); if (m.matches()) { if (AUTH_SCHEME.equalsIgnoreCase(m.group(1))) { for (String nvp : m.group(2).split("\\s*,\\s*")) { m = NVP.matcher(nvp); if (m.matches()) { String name = decodePercent(m.group(1)); String value = decodePercent(m.group(2)); headerValues.put(name, value); } } } } } return headerValues; } // todo: implement method to build header form (with no challenge) /** * Construct a WWW-Authenticate or Authorization header with the OAuth challenge/credentials */ public static String encodeOAuthHeader(Map<String, Object> entries) { StringBuffer sb = new StringBuffer(); sb.append(OAuthConstants.HEADER_NAME).append(" "); for (Map.Entry<String, Object> entry : entries.entrySet()) { String value = entry.getValue() == null ? null : String.valueOf(entry.getValue()); if (!OAuthUtils.isEmpty(entry.getKey()) && !OAuthUtils.isEmpty(value)) { sb.append(entry.getKey()); sb.append("=\""); sb.append(value); sb.append("\","); } } return sb.substring(0, sb.length() - 1); } public static boolean isEmpty(String value) { return value == null || "".equals(value); } public static boolean hasEmptyValues(String[] array) { if (array == null || array.length == 0) { return true; } for (String s : array) { if (isEmpty(s)) { return true; } } return false; } public static String getAuthzMethod(String header) { if (header != null) { Matcher m = OAUTH_HEADER.matcher(header); if (m.matches()) { return m.group(1); } } return null; } public static Set<String> decodeScopes(String s) { Set<String> scopes = new HashSet<String>(); if (!OAuthUtils.isEmpty(s)) { StringTokenizer tokenizer = new StringTokenizer(s, " "); while (tokenizer.hasMoreElements()) { scopes.add(tokenizer.nextToken()); } } return scopes; } public static String encodeScopes(Set<String> s) { StringBuffer scopes = new StringBuffer(); for (String scope : s) { scopes.append(scope).append(" "); } return scopes.toString().trim(); } public static boolean isMultipart(HttpServletRequest request) { if (!"post".equals(request.getMethod().toLowerCase())) { return false; } String contentType = request.getContentType(); if (contentType == null) { return false; } if (contentType.toLowerCase().startsWith(MULTIPART)) { return true; } return false; } public static boolean hasContentType(String requestContentType, String requiredContentType) { if (OAuthUtils.isEmpty(requiredContentType) || OAuthUtils.isEmpty(requestContentType)) { return false; } StringTokenizer tokenizer = new StringTokenizer(requestContentType, ";"); while (tokenizer.hasMoreTokens()) { if (requiredContentType.equals(tokenizer.nextToken())) { return true; } } return false; } }