/* * Copyright 2016 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.client.admin.cli.util; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import org.apache.http.HeaderIterator; import org.apache.http.HttpHeaders; import org.apache.http.HttpResponse; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpHead; import org.apache.http.client.methods.HttpOptions; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPut; import org.apache.http.client.methods.HttpRequestBase; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.entity.InputStreamEntity; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.ssl.SSLContexts; import org.keycloak.client.admin.cli.httpcomponents.HttpDelete; import org.keycloak.client.admin.cli.operations.LocalSearch; import org.keycloak.client.admin.cli.operations.RoleOperations; import org.keycloak.util.JsonSerialization; import javax.net.ssl.SSLContext; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.security.KeyManagementException; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import static org.keycloak.common.util.ObjectUtil.capitalize; /** * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a> */ public class HttpUtil { public static final String APPLICATION_XML = "application/xml"; public static final String APPLICATION_JSON = "application/json"; public static final String APPLICATION_FORM_URL_ENCODED = "application/x-www-form-urlencoded"; public static final String UTF_8 = "utf-8"; private static HttpClient httpClient; private static SSLConnectionSocketFactory sslsf; public static InputStream doGet(String url, String acceptType, String authorization) { try { HttpGet request = new HttpGet(url); request.setHeader(HttpHeaders.ACCEPT, acceptType); return doRequest(authorization, request); } catch (IOException e) { throw new RuntimeException("Failed to send request - " + e.getMessage(), e); } } public static InputStream doPost(String url, String contentType, String acceptType, String content, String authorization) { try { return doPostOrPut(contentType, acceptType, content, authorization, new HttpPost(url)); } catch (IOException e) { throw new RuntimeException("Failed to send request - " + e.getMessage(), e); } } public static InputStream doPut(String url, String contentType, String acceptType, String content, String authorization) { try { return doPostOrPut(contentType, acceptType, content, authorization, new HttpPut(url)); } catch (IOException e) { throw new RuntimeException("Failed to send request - " + e.getMessage(), e); } } public static void doDelete(String url, String authorization) { try { HttpDelete request = new HttpDelete(url); doRequest(authorization, request); } catch (IOException e) { throw new RuntimeException("Failed to send request - " + e.getMessage(), e); } } public static HeadersBodyStatus doGet(String url, HeadersBody request) throws IOException { return doRequest("get", url, request); } public static HeadersBodyStatus doPost(String url, HeadersBody request) throws IOException { return doRequest("post", url, request); } public static HeadersBodyStatus doPut(String url, HeadersBody request) throws IOException { return doRequest("put", url, request); } public static HeadersBodyStatus doDelete(String url, HeadersBody request) throws IOException { return doRequest("delete", url, request); } public static HeadersBodyStatus doRequest(String type, String url, HeadersBody request) throws IOException { HttpRequestBase req; switch (type) { case "get": req = new HttpGet(url); break; case "post": req = new HttpPost(url); break; case "put": req = new HttpPut(url); break; case "delete": req = new HttpDelete(url); break; case "options": req = new HttpOptions(url); break; case "head": req = new HttpHead(url); break; default: throw new RuntimeException("Method not supported: " + type); } addHeaders(req, request.getHeaders()); if (request.getBody() != null) { if (req instanceof HttpEntityEnclosingRequestBase == false) { throw new RuntimeException("Request type does not support body: " + type); } ((HttpEntityEnclosingRequestBase) req).setEntity(new InputStreamEntity(request.getBody())); } HttpResponse res = getHttpClient().execute(req); InputStream responseStream = null; if (res.getEntity() != null) { responseStream = res.getEntity().getContent(); } else { responseStream = new InputStream() { @Override public int read () throws IOException { return -1; } }; } Headers headers = new Headers(); HeaderIterator it = res.headerIterator(); while (it.hasNext()) { org.apache.http.Header header = it.nextHeader(); headers.add(header.getName(), header.getValue()); } return new HeadersBodyStatus(res.getStatusLine().toString(), headers, responseStream); } private static void addHeaders(HttpRequestBase request, Headers headers) { for (Header header: headers) { request.setHeader(header.getName(), header.getValue()); } } private static InputStream doPostOrPut(String contentType, String acceptType, String content, String authorization, HttpEntityEnclosingRequestBase request) throws IOException { request.setHeader(HttpHeaders.CONTENT_TYPE, contentType); request.setHeader(HttpHeaders.ACCEPT, acceptType); if (content != null) { request.setEntity(new StringEntity(content)); } return doRequest(authorization, request); } private static InputStream doRequest(String authorization, HttpRequestBase request) throws IOException { addAuth(request, authorization); HttpResponse response = getHttpClient().execute(request); InputStream responseStream = null; if (response.getEntity() != null) { responseStream = response.getEntity().getContent(); } int code = response.getStatusLine().getStatusCode(); if (code >= 200 && code < 300) { return responseStream; } else { Map<String, String> error = null; try { org.apache.http.Header header = response.getEntity().getContentType(); if (header != null && APPLICATION_JSON.equals(header.getValue())) { error = JsonSerialization.readValue(responseStream, Map.class); } } catch (Exception e) { throw new RuntimeException("Failed to read error response - " + e.getMessage(), e); } finally { responseStream.close(); } String message = null; if (error != null) { message = error.get("error_description") + " [" + error.get("error") + "]"; } throw new RuntimeException(message != null ? message : response.getStatusLine().getStatusCode() + " " + response.getStatusLine().getReasonPhrase()); } } private static void addAuth(HttpRequestBase request, String authorization) { if (authorization != null) { request.setHeader(HttpHeaders.AUTHORIZATION, authorization); } } public static HttpClient getHttpClient() { if (httpClient == null) { if (sslsf != null) { httpClient = HttpClientBuilder.create().useSystemProperties().setSSLSocketFactory(sslsf).build(); } else { httpClient = HttpClientBuilder.create().useSystemProperties().build(); } } return httpClient; } public static String urlencode(String value) { try { return URLEncoder.encode(value, UTF_8); } catch (UnsupportedEncodingException e) { throw new RuntimeException("Failed to urlencode", e); } } public static void setTruststore(File file, String password) throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException, KeyManagementException { if (!file.isFile()) { throw new RuntimeException("Truststore file not found: " + file.getAbsolutePath()); } SSLContext theContext = SSLContexts.custom() .useProtocol("TLS") .loadTrustMaterial(file, password == null ? null : password.toCharArray()) .build(); sslsf = new SSLConnectionSocketFactory(theContext); } public static String extractIdFromLocation(String location) { int last = location.lastIndexOf("/"); if (last != -1) { return location.substring(last + 1); } return null; } public static String addQueryParamsToUri(String uri, String ... queryParams) { if (queryParams == null) { return uri; } if (queryParams.length % 2 != 0) { throw new RuntimeException("Value missing for query parameter: " + queryParams[queryParams.length-1]); } Map<String, String> params = new LinkedHashMap<>(); for (int i = 0; i < queryParams.length; i += 2) { params.put(queryParams[i], queryParams[i+1]); } return addQueryParamsToUri(uri, params); } public static String addQueryParamsToUri(String uri, Map<String, String> queryParams) { if (queryParams.size() == 0) { return uri; } StringBuilder query = new StringBuilder(); for (Map.Entry<String, String> params: queryParams.entrySet()) { try { if (query.length() > 0) { query.append("&"); } query.append(params.getKey()).append("=").append(URLEncoder.encode(params.getValue(), "utf-8")); } catch (Exception e) { throw new RuntimeException("Failed to encode query params: " + params.getKey() + "=" + params.getValue()); } } return uri + (uri.indexOf("?") == -1 ? "?" : "&") + query; } public static String composeResourceUrl(String adminRoot, String realm, String uri) { if (!uri.startsWith("http:") && !uri.startsWith("https:")) { if ("realms".equals(uri) || uri.startsWith("realms/")) { uri = normalize(adminRoot) + uri; } else if ("serverinfo".equals(uri)) { uri = normalize(adminRoot) + uri; } else { uri = normalize(adminRoot) + "realms/" + realm + "/" + uri; } } return uri; } public static String normalize(String value) { return value.endsWith("/") ? value : value + "/"; } public static void checkSuccess(String url, HeadersBodyStatus response) { try { response.checkSuccess(); } catch (HttpResponseException e) { if (e.getStatusCode() == 404) { throw new RuntimeException("Resource not found for url: " + url, e); } throw e; } } public static <T> T doGetJSON(Class<T> type, String resourceUrl, String auth) { Headers headers = new Headers(); if (auth != null) { headers.add("Authorization", auth); } headers.add("Accept", "application/json"); HeadersBodyStatus response; try { response = HttpUtil.doRequest("get", resourceUrl, new HeadersBody(headers)); } catch (IOException e) { throw new RuntimeException("HTTP request failed: GET " + resourceUrl, e); } checkSuccess(resourceUrl, response); T result; try { result = JsonSerialization.readValue(response.getBody(), type); } catch (IOException e) { throw new RuntimeException("Failed to read JSON response", e); } return result; } public static void doPostJSON(String resourceUrl, String auth, Object content) { Headers headers = new Headers(); if (auth != null) { headers.add("Authorization", auth); } headers.add("Content-Type", "application/json"); HeadersBodyStatus response; byte[] body; try { body = JsonSerialization.writeValueAsBytes(content); } catch (IOException e) { throw new RuntimeException("Failed to serialize JSON", e); } try { response = HttpUtil.doRequest("post", resourceUrl, new HeadersBody(headers, new ByteArrayInputStream(body))); } catch (IOException e) { throw new RuntimeException("HTTP request failed: POST " + resourceUrl + "\n" + new String(body), e); } checkSuccess(resourceUrl, response); } public static void doDeleteJSON(String resourceUrl, String auth, Object content) { Headers headers = new Headers(); if (auth != null) { headers.add("Authorization", auth); } headers.add("Content-Type", "application/json"); HeadersBodyStatus response; byte[] body; try { body = JsonSerialization.writeValueAsBytes(content); } catch (IOException e) { throw new RuntimeException("Failed to serialize JSON", e); } try { response = HttpUtil.doRequest("delete", resourceUrl, new HeadersBody(headers, new ByteArrayInputStream(body))); } catch (IOException e) { throw new RuntimeException("HTTP request failed: DELETE " + resourceUrl + "\n" + new String(body), e); } checkSuccess(resourceUrl, response); } public static String getIdForType(String rootUrl, String realm, String auth, String resourceEndpoint, String attrName, String attrValue) { return getAttrForType(rootUrl, realm, auth, resourceEndpoint, attrName, attrValue, "id"); } public static String getAttrForType(String rootUrl, String realm, String auth, String resourceEndpoint, String attrName, String attrValue, String returnAttrName) { String resourceUrl = composeResourceUrl(rootUrl, realm, resourceEndpoint); resourceUrl = HttpUtil.addQueryParamsToUri(resourceUrl, attrName, attrValue, "first", "0", "max", "2"); List<ObjectNode> users = doGetJSON(RoleOperations.LIST_OF_NODES.class, resourceUrl, auth); ObjectNode user; try { user = new LocalSearch(users).exactMatchOne(attrValue, attrName); } catch (Exception e) { throw new RuntimeException("Multiple " + resourceEndpoint + " found for " + attrName + ": " + attrValue, e); } String typeName = singularize(resourceEndpoint); if (user == null) { throw new RuntimeException(capitalize(typeName) + " not found for " + attrName + ": " + attrValue); } JsonNode attr = user.get(returnAttrName); if (attr == null) { throw new RuntimeException("Returned " + typeName + " info has no '" + returnAttrName + "' attribute"); } return attr.asText(); } public static String singularize(String value) { return value.substring(0, value.length()-1); } }