/**
* 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.apache.ambari.server.controller.internal;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.KeyStore;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManagerFactory;
import org.apache.ambari.server.configuration.ComponentSSLConfiguration;
import org.apache.ambari.server.controller.utilities.StreamProvider;
import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.HttpStatus;
/**
* URL based implementation of a stream provider.
*/
public class URLStreamProvider implements StreamProvider {
public static final String COOKIE = "Cookie";
private static final String WWW_AUTHENTICATE = "WWW-Authenticate";
private static final String NEGOTIATE = "Negotiate";
private static Log LOG = LogFactory.getLog(URLStreamProvider.class);
private boolean setupTruststoreForHttps;
private final int connTimeout;
private final int readTimeout;
private final String trustStorePath;
private final String trustStorePassword;
private final String trustStoreType;
private volatile SSLSocketFactory sslSocketFactory = null;
private AppCookieManager appCookieManager = null;
// ----- Constructors ------------------------------------------------------
/**
* Provide the connection timeout for the underlying connection.
*
* @param connectionTimeout
* time, in milliseconds, to attempt a connection
* @param readTimeout
* the read timeout in milliseconds
* @param configuration configuration holding TrustStore information
*/
public URLStreamProvider(int connectionTimeout, int readTimeout,
ComponentSSLConfiguration configuration) {
this(connectionTimeout, readTimeout,
configuration.getTruststorePath(),
configuration.getTruststorePassword(),
configuration.getTruststoreType());
}
/**
* Provide the connection timeout for the underlying connection.
*
* @param connectionTimeout time, in milliseconds, to attempt a connection
* @param readTimeout the read timeout in milliseconds
* @param trustStorePath the path to the truststore required for secure connections
* @param trustStorePassword the truststore password
* @param trustStoreType the truststore type (e.g. "JKS")
*/
public URLStreamProvider(int connectionTimeout, int readTimeout, String trustStorePath,
String trustStorePassword, String trustStoreType) {
this.connTimeout = connectionTimeout;
this.readTimeout = readTimeout;
this.trustStorePath = trustStorePath;
this.trustStorePassword = trustStorePassword;
this.trustStoreType = trustStoreType;
this.setupTruststoreForHttps = true;
}
public void setSetupTruststoreForHttps(boolean setupTruststoreForHttps) {
this.setupTruststoreForHttps = setupTruststoreForHttps;
}
public boolean getSetupTruststoreForHttps() {
return this.setupTruststoreForHttps;
}
// ----- StreamProvider ----------------------------------------------------
@Override
public InputStream readFrom(String spec, String requestMethod, String params) throws IOException {
return processURL(spec, requestMethod, params, null).getInputStream();
}
@Override
public InputStream readFrom(String spec) throws IOException {
return readFrom(spec, "GET", null);
}
// ----- URLStreamProvider -------------------------------------------------
/**
* Get a URL connection from the given spec.
*
* @param spec the String to parse as a URL
* @param requestMethod the HTTP method (GET,POST,PUT,etc.).
* @param body the body of the request; may be null
* @param headers the headers of the request; may be null
*
* @return a URL connection
*
* @throws IOException if the URL connection can not be established
*/
public HttpURLConnection processURL(String spec, String requestMethod, String body, Map<String, List<String>> headers)
throws IOException {
return processURL(spec, requestMethod, body == null ? null : body.getBytes(), headers);
}
/**
* Get a URL connection from the given spec.
*
* @param spec the String to parse as a URL
* @param requestMethod the HTTP method (GET,POST,PUT,etc.).
* @param body the body of the request; may be null
* @param headers the headers of the request; may be null
*
* @return a URL connection
*
* @throws IOException if the URL connection can not be established
*/
public HttpURLConnection processURL(String spec, String requestMethod, InputStream body, Map<String, List<String>> headers)
throws IOException {
return processURL(spec, requestMethod, body == null ? null : IOUtils.toByteArray(body), headers);
}
/**
* Get a URL connection from the given spec.
*
* @param spec the String to parse as a URL
* @param requestMethod the HTTP method (GET,POST,PUT,etc.).
* @param body the body of the request; may be null
* @param headers the headers of the request; may be null
*
* @return a URL connection
*
* @throws IOException if the URL connection can not be established
*/
public HttpURLConnection processURL(String spec, String requestMethod, byte[] body, Map<String, List<String>> headers)
throws IOException {
if (LOG.isDebugEnabled()) {
LOG.debug("readFrom spec:" + spec);
}
HttpURLConnection connection = (spec.startsWith("https") && this.setupTruststoreForHttps) ?
getSSLConnection(spec) : getConnection(spec);
AppCookieManager appCookieManager = getAppCookieManager();
String appCookie = appCookieManager.getCachedAppCookie(spec);
if (appCookie != null) {
LOG.debug("Using cached app cookie for URL:" + spec);
// allow for additional passed in cookies
if (headers == null || headers.isEmpty()) {
headers = Collections.singletonMap(COOKIE, Collections.singletonList(appCookie));
} else {
headers = new HashMap<>(headers);
List<String> cookieList = headers.get(COOKIE);
String cookies = cookieList == null || cookieList.isEmpty() ? null : cookieList.get(0);
headers.put(COOKIE, Collections.singletonList(appendCookie(cookies, appCookie)));
}
}
connection.setConnectTimeout(connTimeout);
connection.setReadTimeout(readTimeout);
connection.setDoOutput(true);
connection.setRequestMethod(requestMethod);
if (headers != null) {
for (Map.Entry<String, List<String>> entry: headers.entrySet()) {
String paramValue = entry.getValue().toString();
connection.setRequestProperty(entry.getKey(), paramValue.substring(1, paramValue.length() - 1));
}
}
if (body != null) {
connection.getOutputStream().write(body);
}
int statusCode = connection.getResponseCode();
if (statusCode == HttpStatus.SC_UNAUTHORIZED ) {
String wwwAuthHeader = connection.getHeaderField(WWW_AUTHENTICATE);
if (LOG.isInfoEnabled()) {
LOG.info("Received WWW-Authentication header:" + wwwAuthHeader + ", for URL:" + spec);
}
if (wwwAuthHeader != null &&
wwwAuthHeader.trim().startsWith(NEGOTIATE)) {
connection = spec.startsWith("https") ?
getSSLConnection(spec) : getConnection(spec);
appCookie = appCookieManager.getAppCookie(spec, true);
connection.setRequestProperty(COOKIE, appCookie);
connection.setConnectTimeout(connTimeout);
connection.setReadTimeout(readTimeout);
connection.setDoOutput(true);
return connection;
} else {
// no supported authentication type found
// we would let the original response propagate
LOG.error("Unsupported WWW-Authentication header:" + wwwAuthHeader+ ", for URL:" + spec);
return connection;
}
} else {
// not a 401 Unauthorized status code
// we would let the original response propagate
if (statusCode == HttpStatus.SC_NOT_FOUND || statusCode == HttpStatus.SC_FORBIDDEN){
LOG.error(String.format("Received HTTP %s response from URL: %s", statusCode, spec));
}
return connection;
}
}
/**
* Get the associated app cookie manager.
*
* @return the app cookie manager
*/
public synchronized AppCookieManager getAppCookieManager() {
if (appCookieManager == null) {
appCookieManager = new AppCookieManager();
}
return appCookieManager;
}
/**
* Utility method to append a new cookie value to an existing list of cookies.
*
* @param cookies the semicolon separated list of cookie values
* @param newCookie a new cookie value to be appended.
*
* @return the new list of cookie values
*/
public static String appendCookie(String cookies, String newCookie) {
if (cookies == null || cookies.length() == 0) {
return newCookie;
}
return cookies + "; " + newCookie;
}
// ----- helper methods ----------------------------------------------------
// Get a connection
protected HttpURLConnection getConnection(String spec) throws IOException {
return (HttpURLConnection) new URL(spec).openConnection();
}
// Get an ssl connection
protected HttpsURLConnection getSSLConnection(String spec) throws IOException, IllegalStateException {
if (sslSocketFactory == null) {
synchronized (this) {
if (sslSocketFactory == null) {
if (trustStorePath == null || trustStorePassword == null) {
String msg =
String.format("Can't get secure connection to %s. Truststore path or password is not set.", spec);
LOG.error(msg);
throw new IllegalStateException(msg);
}
FileInputStream in = null;
try {
in = new FileInputStream(new File(trustStorePath));
KeyStore store = KeyStore.getInstance(trustStoreType == null ?
KeyStore.getDefaultType() : trustStoreType);
store.load(in, trustStorePassword.toCharArray());
TrustManagerFactory tmf = TrustManagerFactory
.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(store);
SSLContext context = SSLContext.getInstance("TLS");
context.init(null, tmf.getTrustManagers(), null);
sslSocketFactory = context.getSocketFactory();
} catch (Exception e) {
throw new IOException("Can't get connection.", e);
} finally {
if (in != null) {
in.close();
}
}
}
}
}
HttpsURLConnection connection = (HttpsURLConnection) (new URL(spec)
.openConnection());
connection.setSSLSocketFactory(sslSocketFactory);
return connection;
}
}