/* * Copyright (c) 2016 Dell EMC Software * All Rights Reserved */ package com.iwave.ext.windows.winrm; import java.net.MalformedURLException; import java.net.URL; import java.util.Arrays; import javax.xml.soap.SOAPEnvelope; import javax.xml.soap.SOAPFactory; import javax.xml.soap.SOAPFault; import javax.xml.xpath.XPathExpression; import org.apache.commons.lang.StringUtils; import org.apache.http.HttpException; import org.apache.http.HttpHost; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; import org.apache.http.StatusLine; import org.apache.http.auth.AuthSchemeProvider; import org.apache.http.auth.AuthScope; import org.apache.http.auth.NTCredentials; import org.apache.http.client.CredentialsProvider; import org.apache.http.client.config.AuthSchemes; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.protocol.HttpClientContext; import org.apache.http.config.Registry; import org.apache.http.config.RegistryBuilder; import org.apache.http.conn.HttpClientConnectionManager; import org.apache.http.conn.socket.ConnectionSocketFactory; import org.apache.http.conn.socket.PlainConnectionSocketFactory; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.conn.ssl.SSLContextBuilder; import org.apache.http.conn.ssl.SSLContexts; import org.apache.http.conn.ssl.TrustSelfSignedStrategy; import org.apache.http.entity.ContentType; import org.apache.http.entity.StringEntity; import org.apache.http.impl.auth.BasicSchemeFactory; import org.apache.http.impl.auth.DigestSchemeFactory; import org.apache.http.impl.auth.KerberosSchemeFactory; import org.apache.http.impl.auth.NTLMSchemeFactory; import org.apache.http.impl.client.BasicCredentialsProvider; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.apache.http.protocol.HttpContext; import org.apache.http.util.EntityUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; import com.iwave.ext.windows.winrm.encryption.EncryptedHttpRequestExecutor; import com.iwave.ext.xml.XmlUtils; public class WinRMTarget { private static final Logger logger = LoggerFactory.getLogger(WinRMTarget.class); protected static final ContentType SOAP = ContentType.create("application/soap+xml", "UTF-8"); public static final int DEFAULT_HTTP_PORT = 5985; public static final int DEFAULT_HTTPS_PORT = 5986; private static final int DEFAULT_CONNECTION_TIMEOUT = 60 * 60 * 1000; // 1 hour private String host; private int port; private boolean secure; private String username; private String password; private CloseableHttpClient client; private HttpContext context; public WinRMTarget(String host, String username, String password) { this(host, DEFAULT_HTTP_PORT, false, username, password); } public WinRMTarget(String host, int port, boolean secure, String username, String password) { this.host = host; this.port = port; this.secure = secure; this.username = username; this.password = password; } //TODO: Remove this method, probably no longer needed. /** * This will normalize a windows style username into the appropriate format for Kerberos. Windows usernames are * specified as <i>DOMAIN\\username</i>, but kerberos requires the names to be <i>username@DOMAIN</i>. * * @param username * the username to normalize. * @return the normalized username. */ public static String normalizeUsername(String username) { boolean isDomainUser = StringUtils.contains(username, '\\'); if (isDomainUser) { // Uppercase the domain name String domain = StringUtils.upperCase(StringUtils.substringBefore(username, "\\")); String name = StringUtils.substringAfter(username, "\\"); return String.format("%s@%s", name, domain); } else { // Not a domain username, leave as is return username; } } public String getHost() { return host; } public int getPort() { return port; } public boolean isSecure() { return secure; } public String getUsername() { return username; } public String getPassword() { return password; } protected String getProtocol() { return secure ? "https" : "http"; } public URL getUrl() { try { return new URL(getProtocol(), host, port, "/wsman"); } catch (MalformedURLException e) { return null; } } public String sendMessage(String request) throws WinRMException { try { HttpPost post = new HttpPost(getUrl().toExternalForm()); post.setEntity(new StringEntity(request, SOAP)); HttpHost targetHost = new HttpHost(getHost(), getPort(), getProtocol()); CloseableHttpClient client = getClient(); HttpContext context = getContext(); HttpResponse response = client.execute(targetHost, post, context); String text = EntityUtils.toString(response.getEntity()); if (response.getStatusLine().getStatusCode() != 200) { handleError(response, text); } return text; } catch (WinRMException e) { throw e; } catch (Exception e) { throw new WinRMException(e); } } protected void handleError(HttpResponse response, String content) throws WinRMException { StatusLine statusLine = response.getStatusLine(); if (statusLine.getStatusCode() == HttpStatus.SC_UNAUTHORIZED) { throw new WinRMException("Authentication Failed"); } else if (statusLine.getStatusCode() == HttpStatus.SC_INTERNAL_SERVER_ERROR) { // Check to see if the response is a SOAP fault SOAPFault fault = getSOAPFault(content); if (fault != null) { String wMIErrorMsg = getWMIError(fault); if (wMIErrorMsg != null) { throw new WinRMSoapException(wMIErrorMsg, fault); } else { throw new WinRMSoapException(fault); } } } throw new WinRMException(String.format("HTTP response: %d, %s", statusLine.getStatusCode(), statusLine.getReasonPhrase())); } protected SOAPFault getSOAPFault(String content) { try { Document doc = XmlUtils.parseXml(content); SOAPEnvelope e = (SOAPEnvelope) SOAPFactory.newInstance().createElement(doc.getDocumentElement()); SOAPFault fault = e.getBody().getFault(); return fault; } catch (Exception e) { return null; } } protected String getWMIError(SOAPFault soapFault) { XPathExpression xpath = XmlUtils.compileXPath(WinRMConstants.XPATH, "s:Detail/f:WSManFault/f:Message/f:ProviderFault/f:ExtendedError"); Element extendedError = XmlUtils.selectElement(xpath, soapFault); if (extendedError != null) { NodeList descriptionElements = extendedError.getElementsByTagNameNS("*", "Description"); if (descriptionElements.getLength() != 0) { if (!StringUtils.isBlank(descriptionElements.item(0).getTextContent())) { return descriptionElements.item(0).getTextContent(); } } } return null; // No Description, or it's empty } protected CloseableHttpClient getClient() throws HttpException { if (client == null) { try { client = createHttpClient(); } catch (HttpException e) { throw e; } } return client; } protected HttpContext getContext() { if (context == null) { context = createHttpClientContext(); } return context; } /** * HttpClient builder * @return HttpClient * @throws HttpException */ protected CloseableHttpClient createHttpClient() throws HttpException { HttpClientBuilder httpClient = HttpClientBuilder.create(); //Build the request config identifying the target preferred authentication schemes and other socket connection parameters. RequestConfig.Builder requestConfig = RequestConfig.custom() .setTargetPreferredAuthSchemes( Arrays.asList(AuthSchemes.SPNEGO, AuthSchemes.NTLM, AuthSchemes.DIGEST, AuthSchemes.BASIC)); requestConfig.setConnectTimeout(DEFAULT_CONNECTION_TIMEOUT); requestConfig.setSocketTimeout(DEFAULT_CONNECTION_TIMEOUT); httpClient.setDefaultRequestConfig(requestConfig.build()); //Set the request executor. The EncryptedHttpRequestExecutor is a custom request executor that is capable of encryption and works //using the Windows NTLM authentication scheme. httpClient.setRequestExecutor(new EncryptedHttpRequestExecutor()); //Build a list of the authentication schemes Registry<AuthSchemeProvider> authSchemeRegistry = RegistryBuilder.<AuthSchemeProvider> create() .register(AuthSchemes.NTLM, new NTLMSchemeFactory()) .register(AuthSchemes.BASIC, new BasicSchemeFactory()) .register(AuthSchemes.DIGEST, new DigestSchemeFactory()) .register(AuthSchemes.KERBEROS, new KerberosSchemeFactory()) .register(AuthSchemes.SPNEGO, new CustomSPNegoSchemeFactory()).build(); try { httpClient.setConnectionManager(createClientConnectionManager()); } catch (Exception e) { throw new HttpException(e.getMessage()); } httpClient.setDefaultAuthSchemeRegistry(authSchemeRegistry); return httpClient.build(); } private HttpClientConnectionManager createClientConnectionManager() throws Exception { SSLContextBuilder contextBuilder = SSLContexts.custom(); try { contextBuilder.loadTrustMaterial(null, new TrustSelfSignedStrategy()); SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(SSLContexts.custom() .loadTrustMaterial(null, new TrustSelfSignedStrategy()).build(), SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory> create() .register("http", PlainConnectionSocketFactory.INSTANCE) .register("https", socketFactory) .build(); return (new PoolingHttpClientConnectionManager(registry)); } catch (Exception e) { throw new HttpException(e.getMessage()); } } protected HttpClientContext createHttpClientContext() { HttpClientContext httpClientContext = HttpClientContext.create(); //Build the credential provider. Note that the credentials are using NTCredentials class which is a derived class of UserPasswordCredentials //This is specifically needed for NTLM authentication. //NTCredentials requires user name in the format "user" and NOT "domain\\user" String[] tokens = StringUtils.split(getUsername(), "\\", 2); CredentialsProvider credsProvider = new BasicCredentialsProvider(); credsProvider.setCredentials( AuthScope.ANY, new NTCredentials(tokens.length > 1 ? tokens[1] : getUsername(), getPassword(), System.getProperty("hostname"), tokens.length > 1 ? tokens[0] : null) ); httpClientContext.setCredentialsProvider(credsProvider); httpClientContext.setTargetHost(new HttpHost(getHost())); return httpClientContext; } }