/* * JBoss, Home of Professional Open Source * Copyright 2015 Red Hat, Inc., and individual contributors * as indicated by the @author tags. * * Licensed 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.jboss.as.controller.security; import static org.jboss.as.controller.logging.ControllerLogger.ROOT_LOGGER; import java.net.URI; import java.net.URISyntaxException; import java.util.HashMap; import java.util.Map; import java.util.Set; /** * This class provides parsing for URIs with scheme "cr-store". * * <p> Credential Store URI is used for configuring/referencing credential stores. * It can specify complete information about credential store including parameters as well as reference * of stored secured credentials (such as passwords). * * <h3> Credential Store URI scheme </h3> * * <blockquote> * crStoreURI = <i>scheme</i> {@code :} {@code //}<i>store_name</i> [{@code /} <i>storage_file</i>] [?<i>query</i>] [{@code #} <i>attribute_name</i>] * * <i>scheme</i> = <b>cr-store</b> * * <i>store_name</i> = {@code //} alpha *alphanum * * <i>storage_file</i> = file_name_uri * * <i>query</i> = store_parameter = value *[{@code ;} <i>store_parameter</i> = <i>value</i>] * * <i>store_parameter</i> = alpha *alphanum * * <i>value</i> = {@code '}alpha *alphanum{@code '} {@code |} alpha *alphanum * * <i>attribute_name</i> = alpha *alphanum * </blockquote> * * <p> Credential Store URI has to be absolute with <b>store_name></b> always defined. * <p> parameters to {@code {@link org.wildfly.security.credential.store.CredentialStoreSpi}} implementation are supplied * through <b>query</b> part of URI. In case they need to decode binary value Base64 encoding method should be used. * * @author <a href="mailto:pskopek@redhat.com">Peter Skopek</a>. */ public class CredentialStoreURIParser { /** * Credential Store URI scheme name ("cr-store"). */ public static final String CR_STORE_SCHEME = "cr-store"; private String name; private String storageFile; private final HashMap<String, String> options = new HashMap<>(); private String attribute; /** * Creates {@link CredentialStoreURIParser} based on given URI * * @param uri URI to parse * @throws URISyntaxException in case of problems parsing given URI */ public CredentialStoreURIParser(final String uri) throws URISyntaxException { int schemeInd = 0; if (uri.startsWith(CR_STORE_SCHEME + ":")) { schemeInd = (CR_STORE_SCHEME + ":").length(); } int fragmentInd = uri.indexOf('#'); URI uriToParse; if (fragmentInd == 0) { throw ROOT_LOGGER.credentialStoreHasNoName(safeCRStoreURI(uri)); } else if (fragmentInd > -1) { String fragment = uri.substring(fragmentInd + 1); if (fragment.indexOf('#') > -1) { throw new URISyntaxException(uri, ROOT_LOGGER.moreThanOneFragmentDefined(), fragmentInd + fragment.indexOf('#')); } uriToParse = new URI(CR_STORE_SCHEME, uri.substring(schemeInd, fragmentInd), fragment); } else { uriToParse = new URI(CR_STORE_SCHEME, uri.substring(schemeInd), null); } parse(uriToParse); } /** * Creates {@link CredentialStoreURIParser} based on given {@link URI} * * @param uri URI to parse */ public CredentialStoreURIParser(final URI uri) { parse(uri); } private void parse(final URI uri) { if (! uri.isAbsolute()) { throw ROOT_LOGGER.credentialStoreURIisNotAbsolute(safeCRStoreURI(uri.toString())); } if (! CR_STORE_SCHEME.equals(uri.getScheme())) { throw ROOT_LOGGER.credentialStoreURIWrongScheme(safeCRStoreURI(uri.toString())); } String authority = uri.getAuthority(); if (authority != null) { name = authority; } else { throw ROOT_LOGGER.credentialStoreHasNoName(safeCRStoreURI(uri.toString())); } String path = uri.getPath(); if (path != null && path.length() > 1) { storageFile = path.substring(1); } else { storageFile = null; } parseQueryParameter(uri.getQuery(), uri.toString()); String fragment = uri.getFragment(); if (fragment != null && fragment.length() >= 0) { if (fragment.isEmpty()) { throw ROOT_LOGGER.credentialStoreURIAttributeEmpty(CredentialStoreURIParser.safeCRStoreURI(uri.toString())); } attribute = fragment; } else { attribute = null; } } /** * Parses and creates {@code options} map with all Credential Store URI query parameters separated. * key value pairs are separated by {@code ;} semicolon. * @param query part of the Credential Store URI * @param uri {@code String} for logging and error messages */ private void parseQueryParameter(final String query, final String uri) { if (query == null) { return; } int i = 0; int state = 0; // possible states KEY = 0 | VALUE = 1 StringBuilder token = new StringBuilder(); String key = null; String value = null; while (i < query.length()) { char c = query.charAt(i); if (state == 0) { // KEY state if (c == '=') { state = 1; key = token.toString(); value = null; token.setLength(0); } else { token.append(c); } i++; } else if (state == 1) { // VALUE state if (c == '\'') { if (query.charAt(i - 1) != '=') { throw ROOT_LOGGER.credentialStoreURIParameterOpeningQuote(CredentialStoreURIParser.safeCRStoreURI(uri)); } int inQuotes = i + 1; c = query.charAt(inQuotes); while (inQuotes < query.length() && c != '\'') { token.append(c); inQuotes++; c = query.charAt(inQuotes); } if (c == '\'') { i = inQuotes + 1; if (i < query.length() && query.charAt(i) != ';') { throw ROOT_LOGGER.credentialStoreURIParameterClosingQuote(CredentialStoreURIParser.safeCRStoreURI(uri)); } } else { throw ROOT_LOGGER.credentialStoreURIParameterUnexpectedEnd(CredentialStoreURIParser.safeCRStoreURI(uri)); } } else if (c == ';') { value = token.toString(); if (key == null) { throw ROOT_LOGGER.credentialStoreURIParameterNameExpected(CredentialStoreURIParser.safeCRStoreURI(uri)); } // put to options and reset key, value and token options.put(key, value); i++; key = null; value = null; token.setLength(0); // set state to KEY state = 0; } else { token.append(c); i++; } } } if (key != null && token.length() > 0) { options.put(key, token.toString()); } else { throw ROOT_LOGGER.credentialStoreURIParameterUnexpectedEnd(CredentialStoreURIParser.safeCRStoreURI(uri)); } } /** * Returns parsed credential store name. * * @return credential store name */ public String getName() { return name; } /** * Returns scheme handled by this parser. * * @return Credential Store URI scheme (always {@code CR_STORE_SCHEME}) */ public String getScheme() { return CR_STORE_SCHEME; } /** * Transforms given parameter to safely displayed {@code String} by stripping potentially sensitive information from the URI. * * @param uri original URI string * @return {@code String} safe to display */ public static String safeCRStoreURI(String uri) { // for now, just easy stripping int startOfQuery = uri.indexOf('?'); if (startOfQuery > -1) { return uri.substring(0, startOfQuery) + "..."; } else { return uri; } } /** * If storage file was not specified by the Credential Store URI returns {@code null} * @return storage file as parsed from Credential Store URI as {@code String} */ public String getStorageFile() { return storageFile; } /** * Returns attribute specified by parsed Credential Store URI * * @return attribute from Credential Store URI, if attribute was not specified then {@code null} */ public String getAttribute() { return attribute; } /** * Fetch parameter value from query string. * * @param param name of wanted parameter * @return parameter value as a {@code String} or {@code null} if parameter was not specified in query part of the URI */ public String getParameter(final String param) { return options.get(param); } /** * Returns {@code Set<String>} of parameters specified by the parsed Credential Store URI. * @return set of parameter names */ public Set<String> getParameters() { return options.keySet(); } /** * Returns new {@code Map<String, Object>} for use in {@code {@link org.wildfly.security.credential.store.CredentialStoreSpi} * to initialize it. * @return Map of options parsed from the Credential Store URI */ public Map<String, String> getOptionsMap() { return new HashMap<>(options); } }