UriValidator.java

/*
 * Copyright 2021 Red Hat, Inc. and/or its affiliates
 * and other 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.keycloak.validate.validators;

import org.keycloak.provider.ConfiguredProvider;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.validate.SimpleValidator;
import org.keycloak.validate.ValidationContext;
import org.keycloak.validate.ValidationError;
import org.keycloak.validate.ValidatorConfig;

import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * URI validation - accepts {@link URI}, {@link URL} and single String. Null input is valid, use other validators (like
 * {@link NotBlankValidator} or {@link NotEmptyValidator} to force field as required.
 */
public class UriValidator implements SimpleValidator, ConfiguredProvider {

    public static final UriValidator INSTANCE = new UriValidator();

    public static final String KEY_ALLOWED_SCHEMES = "allowedSchemes";
    public static final String KEY_ALLOW_FRAGMENT = "allowFragment";
    public static final String KEY_REQUIRE_VALID_URL = "requireValidUrl";

    public static final Set<String> DEFAULT_ALLOWED_SCHEMES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
            "http",
            "https"
    )));
    public static final String MESSAGE_INVALID_URI = "error-invalid-uri";
    public static final String MESSAGE_INVALID_SCHEME = "error-invalid-uri-scheme";
    public static final String MESSAGE_INVALID_FRAGMENT = "error-invalid-uri-fragment";

    public static boolean DEFAULT_ALLOW_FRAGMENT = true;

    public static boolean DEFAULT_REQUIRE_VALID_URL = true;

    public static final String ID = "uri";

    @Override
    public String getId() {
        return ID;
    }

    @Override
    public ValidationContext validate(Object input, String inputHint, ValidationContext context, ValidatorConfig config) {
    	
    	if(input == null || (input instanceof String && ((String) input).isEmpty())) {
    		return context;
    	}

        try {
            URI uri = toUri(input);

            if (uri == null) {
                context.addError(new ValidationError(ID, inputHint, MESSAGE_INVALID_URI, input));
            } else {
                Set<String> allowedSchemes = config.getStringSetOrDefault(KEY_ALLOWED_SCHEMES, DEFAULT_ALLOWED_SCHEMES);
                boolean allowFragment = config.getBooleanOrDefault(KEY_ALLOW_FRAGMENT, DEFAULT_ALLOW_FRAGMENT);
                boolean requireValidUrl = config.getBooleanOrDefault(KEY_REQUIRE_VALID_URL, DEFAULT_REQUIRE_VALID_URL);

                validateUri(uri, inputHint, context, allowedSchemes, allowFragment, requireValidUrl);
            }
        } catch (MalformedURLException | IllegalArgumentException | URISyntaxException e) {
            context.addError(new ValidationError(ID, inputHint, MESSAGE_INVALID_URI, input, e.getMessage()));
        }

        return context;
    }

    private URI toUri(Object input) throws URISyntaxException {

        if (input instanceof String) {
            String uriString = (String) input;
            return new URI(uriString);
        } else if (input instanceof URI) {
            return (URI) input;
        } else if (input instanceof URL) {
            return ((URL) input).toURI();
        }

        return null;
    }

    public boolean validateUri(URI uri, Set<String> allowedSchemes, boolean allowFragment, boolean requireValidUrl) {
        try {
            return validateUri(uri, "url", new ValidationContext(), allowedSchemes, allowFragment, requireValidUrl);
        } catch (MalformedURLException mue) {
            return false;
        }
    }

    public boolean validateUri(URI uri, String inputHint, ValidationContext context,
                               Set<String> allowedSchemes, boolean allowFragment, boolean requireValidUrl)
            throws MalformedURLException {

        boolean valid = true;
        if (uri.getScheme() != null && !allowedSchemes.contains(uri.getScheme())) {
            context.addError(new ValidationError(ID, inputHint, MESSAGE_INVALID_SCHEME, uri, uri.getScheme()));
            valid = false;
        }

        if (!allowFragment && uri.getFragment() != null) {
            context.addError(new ValidationError(ID, inputHint, MESSAGE_INVALID_FRAGMENT, uri, uri.getFragment()));
            valid = false;
        }

        // Don't check if URL is valid if there are other problems with it; otherwise it could lead to duplicate errors.
        // This cannot be moved higher because it acts on differently based on environment (e.g. sometimes it checks
        // scheme, sometimes it doesn't).
        if (requireValidUrl && valid) {
            URL ignored = uri.toURL(); // throws an exception
        }

        return valid;
    }

    @Override
    public String getHelpText() {
        return "Uri Validator";
    }

    @Override
    public List<ProviderConfigProperty> getConfigProperties() {
        return Collections.emptyList();
    }
}