package water;
import org.eclipse.jetty.plus.jaas.JAASLoginService;
import org.eclipse.jetty.security.*;
import org.eclipse.jetty.security.authentication.BasicAuthenticator;
import org.eclipse.jetty.security.authentication.FormAuthenticator;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.bio.SocketConnector;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.eclipse.jetty.server.handler.HandlerCollection;
import org.eclipse.jetty.server.handler.HandlerWrapper;
import org.eclipse.jetty.server.session.HashSessionIdManager;
import org.eclipse.jetty.server.session.HashSessionManager;
import org.eclipse.jetty.server.session.SessionHandler;
import org.eclipse.jetty.server.ssl.SslSocketConnector;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.util.security.Constraint;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import water.api.DatasetServlet;
import water.api.NpsBinServlet;
import water.api.PostFileServlet;
import water.api.RequestServer;
import water.api.schemas3.H2OErrorV3;
import water.exceptions.H2OAbstractRuntimeException;
import water.exceptions.H2OFailException;
import water.util.HttpResponseStatus;
import water.util.Log;
import water.util.StringUtils;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.MalformedURLException;
import java.net.URLDecoder;
import java.util.*;
/**
* Embedded Jetty instance inside H2O.
* This is intended to be a singleton per H2O node.
*/
public class JettyHTTPD {
//------------------------------------------------------------------------------------------
// Thread-specific things.
//------------------------------------------------------------------------------------------
private static final ThreadLocal<Long> _startMillis = new ThreadLocal<>();
private static final ThreadLocal<Integer> _status = new ThreadLocal<>();
private static final ThreadLocal<String> _userAgent = new ThreadLocal<>();
private static void startRequestLifecycle() {
_startMillis.set(System.currentTimeMillis());
_status.set(999);
}
private static void setStatus(int sc) {
_status.set(sc);
}
private static int getStatus() {
return _status.get();
}
protected static long getStartMillis() {
return _startMillis.get();
}
public static void startTransaction(String userAgent) {
_userAgent.set(userAgent);
}
public static void endTransaction() {
_userAgent.remove();
}
/**
* @return Thread-local User-Agent for this transaction.
*/
public static String getUserAgent() {
return _userAgent.get();
}
//------------------------------------------------------------------------------------------
//------------------------------------------------------------------------------------------
public static void setResponseStatus(HttpServletResponse response, int sc) {
setStatus(sc);
response.setStatus(sc);
}
public static void sendResponseError(HttpServletResponse response, int sc, String msg) throws java.io.IOException {
setStatus(sc);
response.sendError(sc, msg);
}
//------------------------------------------------------------------------------------------
// Object-specific things.
//------------------------------------------------------------------------------------------
private static volatile boolean _acceptRequests = false;
private String _ip;
private int _port;
// Jetty server object.
private Server _server;
/**
* Create bare Jetty object.
*/
public JettyHTTPD() {
}
/**
* @return URI scheme
*/
public String getScheme() {
if (H2O.ARGS.jks != null) {
return "https";
}
else {
return "http";
}
}
/**
* @return Port number
*/
public int getPort() {
return _port;
}
/**
* @return IP address
*/
public String getIp() {
return _ip;
}
/**
* @return Server object
*/
public Server getServer() {
return _server;
}
public void setServer(Server value) {
_server = value;
}
public void setup(String ip, int port) {
_ip = ip;
_port = port;
System.setProperty("org.eclipse.jetty.server.Request.maxFormContentSize", Integer.toString(Integer.MAX_VALUE));
}
/**
* Choose a Port and IP address and start the Jetty server.
*
* @throws Exception
*/
public void start(String ip, int port) throws Exception {
setup(ip, port);
if (H2O.ARGS.jks != null) {
startHttps();
}
else {
startHttp();
}
}
public void acceptRequests() {
_acceptRequests = true;
}
protected void createServer(Connector connector) throws Exception {
_server.setConnectors(new Connector[]{connector});
if (H2O.ARGS.hash_login || H2O.ARGS.ldap_login || H2O.ARGS.kerberos_login || H2O.ARGS.pam_login) {
// REFER TO http://www.eclipse.org/jetty/documentation/9.1.4.v20140401/embedded-examples.html#embedded-secured-hello-handler
if (H2O.ARGS.login_conf == null) {
Log.err("Must specify -login_conf argument");
H2O.exit(1);
}
LoginService loginService;
if (H2O.ARGS.hash_login) {
Log.info("Configuring HashLoginService");
loginService = new HashLoginService("H2O", H2O.ARGS.login_conf);
}
else if (H2O.ARGS.ldap_login) {
Log.info("Configuring JAASLoginService (with LDAP)");
System.setProperty("java.security.auth.login.config", H2O.ARGS.login_conf);
loginService = new JAASLoginService("ldaploginmodule");
}
else if (H2O.ARGS.kerberos_login) {
Log.info("Configuring JAASLoginService (with Kerberos)");
System.setProperty("java.security.auth.login.config",H2O.ARGS.login_conf);
loginService = new JAASLoginService("krb5loginmodule");
}
else if (H2O.ARGS.pam_login) {
Log.info("Configuring JAASLoginService (with PAM)");
System.setProperty("java.security.auth.login.config",H2O.ARGS.login_conf);
loginService = new JAASLoginService("pamloginmodule");
}
else {
throw H2O.fail();
}
IdentityService identityService = new DefaultIdentityService();
loginService.setIdentityService(identityService);
_server.addBean(loginService);
// Set a security handler as the first handler in the chain.
ConstraintSecurityHandler security = new ConstraintSecurityHandler();
// Set up a constraint to authenticate all calls, and allow certain roles in.
Constraint constraint = new Constraint();
constraint.setName("auth");
constraint.setAuthenticate(true);
// Configure role stuff (to be disregarded). We are ignoring roles, and only going off the user name.
//
// Jetty 8 and prior.
//
// Jetty 8 requires the security.setStrict(false) and ANY_ROLE.
security.setStrict(false);
constraint.setRoles(new String[]{Constraint.ANY_ROLE});
// Jetty 9 and later.
//
// Jetty 9 and later uses a different servlet spec, and ANY_AUTH gives the same behavior
// for that API version as ANY_ROLE did previously. This required some low-level debugging
// to figure out, so I'm documenting it here.
// Jetty 9 did not require security.setStrict(false).
//
// constraint.setRoles(new String[]{Constraint.ANY_AUTH});
ConstraintMapping mapping = new ConstraintMapping();
mapping.setPathSpec("/*"); // Lock down all API calls
mapping.setConstraint(constraint);
security.setConstraintMappings(Collections.singletonList(mapping));
// Authentication / Authorization
Authenticator authenticator;
if (H2O.ARGS.form_auth) {
BasicAuthenticator basicAuthenticator = new BasicAuthenticator();
FormAuthenticator formAuthenticator = new FormAuthenticator("/login", "/loginError", false);
authenticator = new DelegatingAuthenticator(basicAuthenticator, formAuthenticator);
} else {
authenticator = new BasicAuthenticator();
}
security.setLoginService(loginService);
security.setAuthenticator(authenticator);
HashSessionIdManager idManager = new HashSessionIdManager();
_server.setSessionIdManager(idManager);
HashSessionManager manager = new HashSessionManager();
if (H2O.ARGS.session_timeout > 0)
manager.setMaxInactiveInterval(H2O.ARGS.session_timeout * 60);
SessionHandler sessionHandler = new SessionHandler(manager);
sessionHandler.setHandler(security);
// Pass-through to H2O if authenticated.
registerHandlers(security);
_server.setHandler(sessionHandler);
} else {
registerHandlers(_server);
}
_server.start();
}
protected void startHttp() throws Exception {
_server = new Server();
// QueuedThreadPool p = new QueuedThreadPool();
// p.setName("jetty-h2o");
// p.setMinThreads(3);
// p.setMaxThreads(50);
// p.setMaxIdleTimeMs(3000);
// _server.setThreadPool(p);
Connector connector=new SocketConnector();
connector.setHost(_ip);
connector.setPort(_port);
createServer(connector);
}
/**
* This implementation is based on http://blog.denevell.org/jetty-9-ssl-https.html
*
* @throws Exception
*/
private void startHttps() throws Exception {
_server = new Server();
SslContextFactory sslContextFactory = new SslContextFactory(H2O.ARGS.jks);
sslContextFactory.setKeyStorePassword(H2O.ARGS.jks_pass);
SslSocketConnector httpsConnector = new SslSocketConnector(sslContextFactory);
if (getIp() != null) {
httpsConnector.setHost(getIp());
}
httpsConnector.setPort(getPort());
createServer(httpsConnector);
}
/**
* Stop Jetty server after it has been started.
* This is unlikely to ever be called by H2O until H2O supports graceful shutdown.
*
* @throws Exception
*/
public void stop() throws Exception {
if (_server != null) {
_server.stop();
}
}
/**
* Hook up Jetty handlers. Do this before start() is called.
*/
public void registerHandlers(HandlerWrapper handlerWrapper) {
// Both security and session handlers are already created (Note: we don't want to create a new separate session
// handler just for ServletContextHandler - we want to have just one SessionHandler & SessionManager)
ServletContextHandler context = new ServletContextHandler(
ServletContextHandler.NO_SECURITY | ServletContextHandler.NO_SESSIONS
);
if(null != H2O.ARGS.context_path && !H2O.ARGS.context_path.isEmpty()) {
context.setContextPath(H2O.ARGS.context_path);
} else {
context.setContextPath("/");
}
context.addServlet(NpsBinServlet.class, "/3/NodePersistentStorage.bin/*");
context.addServlet(PostFileServlet.class, "/3/PostFile.bin");
context.addServlet(PostFileServlet.class, "/3/PostFile");
context.addServlet(DatasetServlet.class, "/3/DownloadDataset");
context.addServlet(DatasetServlet.class, "/3/DownloadDataset.bin");
context.addServlet(RequestServer.class, "/");
// Handlers that can only be invoked for an authenticated user (if auth is enabled)
HandlerCollection authHandlers = new HandlerCollection();
authHandlers.setHandlers(new Handler[]{
new AuthenticationHandler(),
new ExtensionHandler1(),
context,
});
// LoginHandler handles directly login requests and delegates the rest to the authHandlers
LoginHandler loginHandler = new LoginHandler("/login", "/loginError");
loginHandler.setHandler(authHandlers);
HandlerCollection hc = new HandlerCollection();
hc.setHandlers(new Handler[]{
new GateHandler(),
loginHandler
});
handlerWrapper.setHandler(hc);
}
public class GateHandler extends AbstractHandler {
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) {
startRequestLifecycle();
while (!_acceptRequests) {
try { Thread.sleep(100); }
catch (Exception ignore) {}
}
setCommonResponseHttpHeaders(response);
}
}
@SuppressWarnings("unused")
protected void handle1(String target,
Request baseRequest,
HttpServletRequest request,
HttpServletResponse response) throws IOException, ServletException {}
public class ExtensionHandler1 extends AbstractHandler {
public ExtensionHandler1() {}
public void handle(String target,
Request baseRequest,
HttpServletRequest request,
HttpServletResponse response) throws IOException, ServletException {
H2O.getJetty().handle1(target, baseRequest, request, response);
}
}
public class LoginHandler extends HandlerWrapper {
private String _loginTarget;
private String _errorTarget;
public LoginHandler(String loginTarget, String errorTarget) {
_loginTarget = loginTarget;
_errorTarget = errorTarget;
}
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
if (isLoginTarget(target)) {
if (isPageRequest(request))
sendLoginForm(request, response);
else
sendResponseError(response, HttpServletResponse.SC_UNAUTHORIZED, "Access denied. Please login.");
baseRequest.setHandled(true);
} else {
// not for us, invoke wrapped handler
super.handle(target, baseRequest, request, response);
}
}
private void sendLoginForm(HttpServletRequest request, HttpServletResponse response) {
String uri = JettyHTTPD.getDecodedUri(request);
try {
byte[] bytes;
try (InputStream resource = water.init.JarHash.getResource2("/login.html")) {
if (resource == null)
throw new IllegalStateException("Login form not found");
ByteArrayOutputStream baos = new ByteArrayOutputStream();
water.util.FileUtils.copyStream(resource, baos, 2048);
bytes = baos.toByteArray();
}
response.setContentType(RequestServer.MIME_HTML);
response.setContentLength(bytes.length);
setResponseStatus(response, HttpServletResponse.SC_OK);
OutputStream os = response.getOutputStream();
water.util.FileUtils.copyStream(new ByteArrayInputStream(bytes), os, 2048);
} catch (Exception e) {
sendErrorResponse(response, e, uri);
} finally {
logRequest("GET", request, response);
}
}
private boolean isPageRequest(HttpServletRequest request) {
String accept = request.getHeader("Accept");
return (accept != null) && accept.contains(RequestServer.MIME_HTML);
}
private boolean isLoginTarget(String target) {
return target.equals(_loginTarget) || target.equals(_errorTarget);
}
}
public class AuthenticationHandler extends AbstractHandler {
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
if (!H2O.ARGS.ldap_login && !H2O.ARGS.kerberos_login && !H2O.ARGS.pam_login) return;
String loginName = request.getUserPrincipal().getName();
if (!loginName.equals(H2O.ARGS.user_name)) {
Log.warn("Login name (" + loginName + ") does not match cluster owner name (" + H2O.ARGS.user_name + ")");
sendResponseError(response, HttpServletResponse.SC_UNAUTHORIZED, "Login name does not match cluster owner name");
baseRequest.setHandled(true);
}
}
}
public static InputStream extractPartInputStream (HttpServletRequest request, HttpServletResponse response) throws
IOException {
String ct = request.getContentType();
if (! ct.startsWith("multipart/form-data")) {
setResponseStatus(response, HttpServletResponse.SC_BAD_REQUEST);
response.getWriter().write("Content type must be multipart/form-data");
return null;
}
String boundaryString;
int idx = ct.indexOf("boundary=");
if (idx < 0) {
setResponseStatus(response, HttpServletResponse.SC_BAD_REQUEST);
response.getWriter().write("Boundary missing");
return null;
}
boundaryString = ct.substring(idx + "boundary=".length());
byte[] boundary = StringUtils.bytesOf(boundaryString);
// Consume headers of the mime part.
InputStream is = request.getInputStream();
String line = readLine(is);
while ((line != null) && (line.trim().length()>0)) {
line = readLine(is);
}
return new InputStreamWrapper(is, boundary);
}
public static void sendErrorResponse(HttpServletResponse response, Exception e, String uri) {
if (e instanceof H2OFailException) {
H2OFailException ee = (H2OFailException) e;
H2OError error = ee.toH2OError(uri);
Log.fatal("Caught exception (fatal to the cluster): " + error.toString());
throw(H2O.fail(error.toString()));
}
else if (e instanceof H2OAbstractRuntimeException) {
H2OAbstractRuntimeException ee = (H2OAbstractRuntimeException) e;
H2OError error = ee.toH2OError(uri);
Log.warn("Caught exception: " + error.toString());
setResponseStatus(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
// Note: don't use Schema.schema(version, error) because we have to work at bootstrap:
try {
@SuppressWarnings("unchecked")
String s = new H2OErrorV3().fillFromImpl(error).toJsonString();
response.getWriter().write(s);
}
catch (Exception ignore) {
ignore.printStackTrace();
}
}
else { // make sure that no Exception is ever thrown out from the request
H2OError error = new H2OError(e, uri);
// some special cases for which we return 400 because it's likely a problem with the client request:
if (e instanceof IllegalArgumentException)
error._http_status = HttpResponseStatus.BAD_REQUEST.getCode();
else if (e instanceof FileNotFoundException)
error._http_status = HttpResponseStatus.BAD_REQUEST.getCode();
else if (e instanceof MalformedURLException)
error._http_status = HttpResponseStatus.BAD_REQUEST.getCode();
setResponseStatus(response, error._http_status);
Log.warn("Caught exception: " + error.toString());
// Note: don't use Schema.schema(version, error) because we have to work at bootstrap:
try {
@SuppressWarnings("unchecked")
String s = new H2OErrorV3().fillFromImpl(error).toJsonString();
response.getWriter().write(s);
}
catch (Exception ignore) {
ignore.printStackTrace();
}
}
}
public static String getDecodedUri(HttpServletRequest request) {
try {
return URLDecoder.decode(request.getRequestURI(), "UTF-8");
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
private static void setCommonResponseHttpHeaders(HttpServletResponse response) {
response.setHeader("X-h2o-build-project-version", H2O.ABV.projectVersion());
response.setHeader("X-h2o-rest-api-version-max", Integer.toString(water.api.RequestServer.H2O_REST_API_VERSION));
response.setHeader("X-h2o-cluster-id", Long.toString(H2O.CLUSTER_ID));
response.setHeader("X-h2o-cluster-good", Boolean.toString(H2O.CLOUD.healthy()));
response.setHeader("X-h2o-context-path", sanatizeContextPath(H2O.ARGS.context_path));
}
private static String sanatizeContextPath(String context_path) {
if(null == context_path || context_path.isEmpty()) {
return "/";
}
return context_path + "/";
}
//--------------------------------------------------
@SuppressWarnings("unused")
public static void logRequest(String method, HttpServletRequest request, HttpServletResponse response) {
Log.httpd(method, request.getRequestURI(), getStatus(), System.currentTimeMillis() - getStartMillis());
}
private static String readLine(InputStream in) throws IOException {
StringBuilder sb = new StringBuilder();
byte[] mem = new byte[1024];
while (true) {
int sz = readBufOrLine(in,mem);
sb.append(new String(mem,0,sz));
if (sz < mem.length)
break;
if (mem[sz-1]=='\n')
break;
}
if (sb.length()==0)
return null;
String line = sb.toString();
if (line.endsWith("\r\n"))
line = line.substring(0,line.length()-2);
else if (line.endsWith("\n"))
line = line.substring(0,line.length()-1);
return line;
}
@SuppressWarnings("all")
private static int readBufOrLine(InputStream in, byte[] mem) throws IOException {
byte[] bb = new byte[1];
int sz = 0;
while (true) {
byte b;
byte b2;
if (sz==mem.length)
break;
try {
in.read(bb,0,1);
b = bb[0];
mem[sz++] = b;
} catch (EOFException e) {
break;
}
if (b == '\n')
break;
if (sz==mem.length)
break;
if (b == '\r') {
try {
in.read(bb,0,1);
b2 = bb[0];
mem[sz++] = b2;
} catch (EOFException e) {
break;
}
if (b2 == '\n')
break;
}
}
return sz;
}
@SuppressWarnings("all")
private static final class InputStreamWrapper extends InputStream {
static final byte[] BOUNDARY_PREFIX = { '\r', '\n', '-', '-' };
final InputStream _wrapped;
final byte[] _boundary;
final byte[] _lookAheadBuf;
int _lookAheadLen;
public InputStreamWrapper(InputStream is, byte[] boundary) {
_wrapped = is;
_boundary = Arrays.copyOf(BOUNDARY_PREFIX, BOUNDARY_PREFIX.length + boundary.length);
System.arraycopy(boundary, 0, _boundary, BOUNDARY_PREFIX.length, boundary.length);
_lookAheadBuf = new byte[_boundary.length];
_lookAheadLen = 0;
}
@Override public void close() throws IOException { _wrapped.close(); }
@Override public int available() throws IOException { return _wrapped.available(); }
@Override public long skip(long n) throws IOException { return _wrapped.skip(n); }
@Override public void mark(int readlimit) { _wrapped.mark(readlimit); }
@Override public void reset() throws IOException { _wrapped.reset(); }
@Override public boolean markSupported() { return _wrapped.markSupported(); }
@Override public int read() throws IOException { throw new UnsupportedOperationException(); }
@Override public int read(byte[] b) throws IOException { return read(b, 0, b.length); }
@Override public int read(byte[] b, int off, int len) throws IOException {
if(_lookAheadLen == -1)
return -1;
int readLen = readInternal(b, off, len);
if (readLen != -1) {
int pos = findBoundary(b, off, readLen);
if (pos != -1) {
_lookAheadLen = -1;
return pos - off;
}
}
return readLen;
}
private int readInternal(byte b[], int off, int len) throws IOException {
if (len < _lookAheadLen ) {
System.arraycopy(_lookAheadBuf, 0, b, off, len);
_lookAheadLen -= len;
System.arraycopy(_lookAheadBuf, len, _lookAheadBuf, 0, _lookAheadLen);
return len;
}
if (_lookAheadLen > 0) {
System.arraycopy(_lookAheadBuf, 0, b, off, _lookAheadLen);
off += _lookAheadLen;
len -= _lookAheadLen;
int r = Math.max(_wrapped.read(b, off, len), 0) + _lookAheadLen;
_lookAheadLen = 0;
return r;
} else {
return _wrapped.read(b, off, len);
}
}
private int findBoundary(byte[] b, int off, int len) throws IOException {
int bidx = -1; // start index of boundary
int idx = 0; // actual index in boundary[]
for(int i = off; i < off+len; i++) {
if (_boundary[idx] != b[i]) { // reset
idx = 0;
bidx = -1;
}
if (_boundary[idx] == b[i]) {
if (idx == 0) bidx = i;
if (++idx == _boundary.length) return bidx; // boundary found
}
}
if (bidx != -1) { // it seems that there is boundary but we did not match all boundary length
assert _lookAheadLen == 0; // There should not be not read lookahead
_lookAheadLen = _boundary.length - idx;
int readLen = _wrapped.read(_lookAheadBuf, 0, _lookAheadLen);
if (readLen < _boundary.length - idx) { // There is not enough data to match boundary
_lookAheadLen = readLen;
return -1;
}
for (int i = 0; i < _boundary.length - idx; i++)
if (_boundary[i+idx] != _lookAheadBuf[i])
return -1; // There is not boundary => preserve lookahead buffer
// Boundary found => do not care about lookAheadBuffer since all remaining data are ignored
}
return bidx;
}
}
}