/* This file is part of VoltDB.
* Copyright (C) 2008-2017 VoltDB Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with VoltDB. If not, see <http://www.gnu.org/licenses/>.
*/
package org.voltdb.utils;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.URL;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.LinkedBlockingQueue;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.bind.annotation.XmlAttribute;
import org.apache.http.entity.ContentType;
import org.codehaus.jackson.JsonFactory;
import org.codehaus.jackson.JsonGenerator;
import org.codehaus.jackson.JsonParseException;
import org.codehaus.jackson.annotate.JsonIgnore;
import org.codehaus.jackson.annotate.JsonProperty;
import org.codehaus.jackson.annotate.JsonPropertyOrder;
import org.codehaus.jackson.map.DeserializationConfig;
import org.codehaus.jackson.map.JsonMappingException;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.SerializationConfig;
import org.codehaus.jackson.schema.JsonSchema;
import org.eclipse.jetty.continuation.Continuation;
import org.eclipse.jetty.continuation.ContinuationSupport;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.SecureRequestCustomizer;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
import org.eclipse.jetty.server.handler.ResourceHandler;
import org.eclipse.jetty.server.handler.gzip.GzipHandler;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.json_voltpatches.JSONArray;
import org.json_voltpatches.JSONException;
import org.json_voltpatches.JSONObject;
import org.voltcore.logging.VoltLogger;
import org.voltdb.AuthenticationResult;
import org.voltdb.CatalogContext;
import org.voltdb.ClientResponseImpl;
import org.voltdb.HTTPClientInterface;
import org.voltdb.VoltDB;
import org.voltdb.VoltTable;
import org.voltdb.client.BatchTimeoutOverrideType;
import org.voltdb.client.ClientResponse;
import org.voltdb.client.SyncCallback;
import org.voltdb.common.Permission;
import org.voltdb.compiler.deploymentfile.DeploymentType;
import org.voltdb.compiler.deploymentfile.ExportType;
import org.voltdb.compiler.deploymentfile.PathsType;
import org.voltdb.compiler.deploymentfile.ServerExportEnum;
import org.voltdb.compiler.deploymentfile.UsersType;
import org.voltdb.compiler.deploymentfile.UsersType.User;
import org.voltdb.compilereport.ReportMaker;
import com.google_voltpatches.common.base.Charsets;
import com.google_voltpatches.common.base.Throwables;
import com.google_voltpatches.common.io.Resources;
import com.google_voltpatches.common.net.HostAndPort;
public class HTTPAdminListener {
private static final VoltLogger m_log = new VoltLogger("HOST");
public static final String REALM = "VoltDBRealm";
// static resources
private static final String RESOURCE_BASE = "dbmonitor";
private static final String CSS_TARGET = "css";
private static final String IMAGES_TARGET = "images";
private static final String JS_TARGET = "js";
// content types
private static final String JSON_CONTENT_TYPE = ContentType.APPLICATION_JSON.toString();
private static final String HTML_CONTENT_TYPE = "text/html;charset=utf-8";
Server m_server;
HTTPClientInterface httpClientInterface = new HTTPClientInterface();
final boolean m_jsonEnabled;
Map<String, String> m_htmlTemplates = new HashMap<>();
final boolean m_mustListen;
final DeploymentRequestHandler m_deploymentHandler;
final String m_publicIntf;
// ObjectMapper is thread safe, and uses a lot of memory to cache
// class specific serializers and deserializers. Use JSR-133
// initialization on demand holder to hold a sole instance
public final static class MapperHolder {
final static public ObjectMapper mapper;
final static public JsonFactory factory = new JsonFactory();
static {
ObjectMapper configurable = new ObjectMapper();
// configurable.setSerializationInclusion(Inclusion.NON_NULL);
configurable.configure(DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES, false);
configurable.configure(JsonGenerator.Feature.AUTO_CLOSE_TARGET, false);
SerializationConfig serializationConfig = configurable.getSerializationConfig();
serializationConfig.addMixInAnnotations(UsersType.User.class, IgnorePasswordMixIn.class);
serializationConfig.addMixInAnnotations(ExportType.class, IgnoreLegacyExportAttributesMixIn.class);
//These mixins are to ignore the "key" and redirect "path" to getNodePath()
serializationConfig.addMixInAnnotations(PathsType.Commandlog.class,
IgnoreNodePathKeyMixIn.class);
serializationConfig.addMixInAnnotations(PathsType.Commandlogsnapshot.class,
IgnoreNodePathKeyMixIn.class);
serializationConfig.addMixInAnnotations(PathsType.Droverflow.class,
IgnoreNodePathKeyMixIn.class);
serializationConfig.addMixInAnnotations(PathsType.Exportoverflow.class,
IgnoreNodePathKeyMixIn.class);
serializationConfig.addMixInAnnotations(PathsType.Snapshots.class,
IgnoreNodePathKeyMixIn.class);
serializationConfig.addMixInAnnotations(PathsType.Voltdbroot.class, IgnoreNodePathKeyMixIn.class);
mapper = configurable;
}
}
//Somewhat like Filter but we dont have Filter in version and jars we use.
class VoltRequestHandler extends AbstractHandler {
VoltLogger logger = new VoltLogger("HOST");
private String m_hostHeader = null;
protected String getHostHeader() {
if (m_hostHeader != null) {
return m_hostHeader;
}
if (!m_publicIntf.isEmpty()) {
m_hostHeader = m_publicIntf;
return m_hostHeader;
}
InetAddress addr = null;
int httpPort = VoltDB.DEFAULT_HTTP_PORT;
try {
String localMetadata = VoltDB.instance().getLocalMetadata();
JSONObject jsObj = new JSONObject(localMetadata);
JSONArray interfaces = jsObj.getJSONArray("interfaces");
//The first interface is external interface if specified.
String iface = interfaces.getString(0);
addr = InetAddress.getByName(iface);
httpPort = jsObj.getInt("httpPort");
} catch (Exception e) {
logger.warn("Failed to get HTTP interface information.", e);
}
if (addr == null) {
addr = org.voltcore.utils.CoreUtils.getLocalAddress();
}
//Make the header string.
m_hostHeader = addr.getHostAddress() + ":" + httpPort;
return m_hostHeader;
}
public AuthenticationResult authenticate(Request request) {
return httpClientInterface.authenticate(request);
}
@Override
public void handle(String string, Request rqst, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
response.setHeader("Host", getHostHeader());
}
}
//Build a client response based json response
private static String buildClientResponse(String jsonp, byte code, String msg) {
ClientResponseImpl rimpl = new ClientResponseImpl(code, new VoltTable[0], msg);
return HTTPClientInterface.asJsonp(jsonp, rimpl.toJSONString());
}
class DBMonitorHandler extends VoltRequestHandler {
@Override
public void handle(String target, Request baseRequest,
HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
super.handle(target, baseRequest, request, response);
if (baseRequest.isHandled()) return;
try{
// if this is an internal jetty retry, then just tell
// jetty we're still working on it. There is a risk of
// masking other errors in doing this, but it's probably
// low compared with the default policy of retrys.
Continuation cont = ContinuationSupport.getContinuation(baseRequest);
// this is set to false on internal jetty retrys
if (!cont.isInitial()) {
// The continuation object has been woken up by the
// retry. Tell it to go back to sleep.
cont.suspend();
return;
}
//Special handling for API as they continue and setHandled differently
if (baseRequest.getRequestURI().contains("/api/1.0/")) {
baseRequest.setHandled(false);
return;
}
if (baseRequest.getRequestURI().contains(File.separator + CSS_TARGET) ||
baseRequest.getRequestURI().contains(File.separator + IMAGES_TARGET) ||
baseRequest.getRequestURI().contains(File.separator + JS_TARGET)) {
// will be processed by individual resource handler
return;
}
//Send old /studio back to "/"
if (baseRequest.getRequestURI().contains("/studio")) {
response.sendRedirect("/");
baseRequest.setHandled(true);
return;
}
// redirect the base dir
if (target.equals("/")) target = "/index.htm";
// check if a file exists
URL url = VoltDB.class.getResource("dbmonitor" + target);
if (url == null) {
// write 404
String msg = "404: Resource not found.\n";
response.setContentType("text/plain;charset=utf-8");
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
baseRequest.setHandled(true);
response.getWriter().print(msg);
return;
}
// read the template
InputStream is = VoltDB.class.getResourceAsStream("dbmonitor" + target);
if (target.endsWith("/index.htm")) {
// set the headers
response.setContentType(HTML_CONTENT_TYPE);
response.setStatus(HttpServletResponse.SC_OK);
baseRequest.setHandled(true);
OutputStream os = response.getOutputStream();
BufferedInputStream bis = new BufferedInputStream(is);
int c = -1;
while ((c = bis.read()) != -1) {
os.write(c);
}
}
else {
// js, css, images handled by resource handler. files corresponding to it
// should be placed in the specific location
assert !target.endsWith(".js") : " Javascript should in the resource path "
+ RESOURCE_BASE + File.separator + JS_TARGET;
assert !target.endsWith(".css") : " Stylesheet should in the resource path "
+ RESOURCE_BASE + File.separator + CSS_TARGET;
assert !target.endsWith(".gif") : target + " should in the resource path "
+ RESOURCE_BASE + File.separator + IMAGES_TARGET;
assert !target.endsWith(".png") : target + " should in the resource path "
+ RESOURCE_BASE + File.separator + IMAGES_TARGET;
assert !target.endsWith(".jpg") : target + " should in the resource path "
+ RESOURCE_BASE + File.separator + IMAGES_TARGET;
assert !target.endsWith(".jpeg") : target + "image should in the resource path "
+ RESOURCE_BASE + File.separator + IMAGES_TARGET;
// set the headers
response.setContentType(HTML_CONTENT_TYPE);
response.setStatus(HttpServletResponse.SC_OK);
baseRequest.setHandled(true);
// write the file out
BufferedInputStream bis = new BufferedInputStream(is);
OutputStream os = response.getOutputStream();
int c = -1;
while ((c = bis.read()) != -1) {
os.write(c);
}
}
}catch(Exception ex){
logger.info("Not servicing url: " + baseRequest.getRequestURI() + " Details: "+ ex.getMessage());
}
}
}
class CatalogRequestHandler extends VoltRequestHandler {
@Override
public void handle(String target,
Request baseRequest,
HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException {
super.handle(target, baseRequest, request, response);
if (baseRequest.isHandled()) return;
handleReportPage(baseRequest, response);
}
}
class DDLRequestHandler extends VoltRequestHandler {
@Override
public void handle(String target,
Request baseRequest,
HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException {
super.handle(target, baseRequest, request, response);
if (baseRequest.isHandled()) return;
byte[] reportbytes = VoltDB.instance().getCatalogContext().getFileInJar("autogen-ddl.sql");
String ddl = new String(reportbytes, Charsets.UTF_8);
response.setContentType("text/plain;charset=utf-8");
response.setStatus(HttpServletResponse.SC_OK);
baseRequest.setHandled(true);
response.getWriter().print(ddl);
}
}
/*
* Utility handler class to enable caching of static resources.
* The static resources are package in jar file
*/
class CacheStaticResourceHandler extends ResourceHandler {
// target Directory location for folder w.r.t. resource base folder - dbmonitor
public CacheStaticResourceHandler(final String target, int maxAge) {
super();
final String path = VoltDB.class.getResource(RESOURCE_BASE + File.separator + target).toExternalForm();
if (m_log.isDebugEnabled()) {
m_log.debug("Resource base path: " + path);
}
setResourceBase(path);
// set etags along with cache age so that the http client's requests for fetching the
// static resource is rate limited. Without cache age, client will requesting for
// static more than needed
setCacheControl("max-age=" + maxAge +", private");
setEtags(true);
}
@SuppressWarnings("unused")
private CacheStaticResourceHandler() {
super();
assert false : "Target location for static resource is needed to initialize the resource handler";
}
@Override
public void handle(String target,
Request baseRequest,
HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException {
super.handle(target, baseRequest, request, response);
if (!baseRequest.isHandled() && m_log.isDebugEnabled()) {
m_log.debug("Failed to process static resource: " + Paths.get(getResourceBase()));
}
}
}
//This is a wrapper to generate JSON for profile of authenticated user.
private final class Profile {
private final String user;
private final String permissions[];
public Profile(String u, String[] p) {
user = u;
permissions = p;
}
@SuppressWarnings("unused")
public String getUser() {
return user;
}
@SuppressWarnings("unused")
public String[] getPermissions() {
return permissions;
}
}
// /profile handler
class UserProfileHandler extends VoltRequestHandler {
private final ObjectMapper m_mapper = MapperHolder.mapper;
public UserProfileHandler() {
}
// GET on /profile resources.
@Override
public void handle(String target,
Request baseRequest,
HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException {
super.handle(target, baseRequest, request, response);
if (baseRequest.isHandled()) return;
//jsonp is specified when response is expected to go to javascript function.
String jsonp = request.getParameter(HTTPClientInterface.JSONP);
AuthenticationResult authResult = null;
try {
response.setContentType(JSON_CONTENT_TYPE);
if (!HTTPClientInterface.validateJSONP(jsonp, baseRequest, response)) {
return;
}
response.setStatus(HttpServletResponse.SC_OK);
authResult = authenticate(baseRequest);
if (!authResult.isAuthenticated()) {
response.getWriter().print(buildClientResponse(jsonp, ClientResponse.UNEXPECTED_FAILURE, authResult.m_message));
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
baseRequest.setHandled(true);
return;
}
if (!target.equals("/")) {
response.getWriter().print(buildClientResponse(jsonp, ClientResponse.UNEXPECTED_FAILURE, "Resource not found"));
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
baseRequest.setHandled(true);
return;
}
if (jsonp != null) {
response.getWriter().write(jsonp + "(");
}
m_mapper.writeValue(response.getWriter(), new Profile(authResult.m_user, authResult.m_perms));
if (jsonp != null) {
response.getWriter().write(")");
}
baseRequest.setHandled(true);
} catch (Exception ex) {
logger.info("Not servicing url: " + baseRequest.getRequestURI() + " Details: "+ ex.getMessage(), ex);
}
}
}
//This is for password on User in the deployment to not to be reported.
abstract class IgnorePasswordMixIn {
@JsonIgnore abstract String getPassword();
}
abstract class IgnoreLegacyExportAttributesMixIn {
@JsonIgnore abstract String getExportconnectorclass();
@JsonIgnore abstract ServerExportEnum getTarget();
@JsonIgnore abstract Boolean isEnabled();
}
abstract class IgnoreNodePathKeyMixIn {
@JsonProperty("path") abstract String getNodePath();
@JsonIgnore abstract String getKey();
}
@JsonPropertyOrder({"name","roles","password","plaintext","id"})
public class IdUser extends UsersType.User {
@XmlAttribute(name = "id")
protected String id;
IdUser(UsersType.User user, String header) {
this.name = user.getName();
this.roles = user.getRoles();
this.password = user.getPassword();
this.plaintext = user.isPlaintext();
this.id = header + "/deployment/users/" + this.name;
}
@JsonProperty("id")
public void setId(String value) {
this.id = value;
}
@JsonProperty("id")
public String getId() {
return id;
}
}
class DeploymentRequestHandler extends VoltRequestHandler {
final ObjectMapper m_mapper = MapperHolder.mapper;
String m_schema = "";
public DeploymentRequestHandler() {
try {
JsonSchema schema = m_mapper.generateJsonSchema(DeploymentType.class);
m_schema = schema.toString();
} catch (JsonMappingException ex) {
m_log.warn("Failed to generate JSON schema: ", ex);
}
}
private CatalogContext getCatalogContext() {
return VoltDB.instance().getCatalogContext();
}
//Get deployment from catalog context
private DeploymentType getDeployment() {
//If running with new verbs add runtime paths.
DeploymentType dt = CatalogUtil.updateRuntimeDeploymentPaths(getCatalogContext().getDeployment());
return dt;
}
//Get deployment bytes from catalog context
private byte[] getDeploymentBytes() {
return VoltDB.instance().getCatalogContext().getDeploymentBytes();
}
private UsersType.User findUser(String user, DeploymentType dep) {
if (dep.getUsers() != null) {
for(User u : dep.getUsers().getUser()) {
if (user.equalsIgnoreCase(u.getName())) {
return u;
}
}
}
return null;
}
// TODO - subresources.
// We support
// /deployment/cluster
// /deployment/paths
// /deployment/partitionDetection
// /deployment/adminMode
// /deployment/heartbeat
// /deployment/httpd
// /deployment/replication
// /deployment/snapshot
// /deployment/export
// /deployment/users
// /deployment/commandlog
// /deployment/systemsettings
// /deployment/security
@Override
public void handle(String target,
Request baseRequest,
HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException {
super.handle(target, baseRequest, request, response);
if (baseRequest.isHandled()) return;
//jsonp is specified when response is expected to go to javascript function.
String jsonp = request.getParameter(HTTPClientInterface.JSONP);
AuthenticationResult authResult = null;
try {
response.setContentType(JSON_CONTENT_TYPE);
if (!HTTPClientInterface.validateJSONP(jsonp, baseRequest, response)) {
return;
}
response.setStatus(HttpServletResponse.SC_OK);
//Requests require authentication.
authResult = authenticate(baseRequest);
if (!authResult.isAuthenticated()) {
response.getWriter().print(buildClientResponse(jsonp, ClientResponse.UNEXPECTED_FAILURE, authResult.m_message));
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
baseRequest.setHandled(true);
return;
}
//Authenticated but has no permissions.
if (!authResult.m_authUser.hasPermission(Permission.ADMIN)) {
response.getWriter().print(buildClientResponse(jsonp, ClientResponse.UNEXPECTED_FAILURE, "Permission denied"));
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
baseRequest.setHandled(true);
return;
}
if (!target.endsWith("/")) { // the URI may or may not end with /
target += "/";
}
//Authenticated and has ADMIN permission
if (target.equals("/download/")) {
//Deployment xml is text/xml
response.setContentType("text/xml;charset=utf-8");
DeploymentType dt = CatalogUtil.shallowClusterAndPathsClone(this.getDeployment());
// reflect the actual number of cluster members
dt.getCluster().setHostcount(getCatalogContext().getClusterSettings().hostcount());
response.getWriter().write(CatalogUtil.getDeployment(dt, true));
} else if (target.startsWith("/users/")) { // username may be passed in after the / (not as a param)
if (request.getMethod().equalsIgnoreCase("POST")) {
handleUpdateUser(jsonp, target, baseRequest, request, response, authResult);
} else if (request.getMethod().equalsIgnoreCase("PUT")) {
handleCreateUser(jsonp, target, baseRequest, request, response, authResult);
} else if (request.getMethod().equalsIgnoreCase("DELETE")) {
handleRemoveUser(jsonp, target, baseRequest, request, response, authResult);
} else {
handleGetUsers(jsonp, target, baseRequest, request, response);
}
} else if (target.equals("/export/types/")) {
handleGetExportTypes(jsonp, response);
} else if (target.equals("/")) { // just deployment
if (request.getMethod().equalsIgnoreCase("POST")) {
handleUpdateDeployment(jsonp, baseRequest, request, response, authResult);
} else {
//non POST
response.setCharacterEncoding("UTF-8");
if (jsonp != null) {
response.getWriter().write(jsonp + "(");
}
DeploymentType dt = getDeployment();
// reflect the actual number of cluster members
dt.getCluster().setHostcount(getCatalogContext().getClusterSettings().hostcount());
m_mapper.writeValue(response.getWriter(), dt);
if (jsonp != null) {
response.getWriter().write(")");
}
}
} else {
response.getWriter().print(buildClientResponse(jsonp, ClientResponse.UNEXPECTED_FAILURE, "Resource not found"));
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
}
baseRequest.setHandled(true);
} catch (Exception ex) {
logger.info("Not servicing url: " + baseRequest.getRequestURI() + " Details: "+ ex.getMessage(), ex);
}
}
//Update the deployment
public void handleUpdateDeployment(String jsonp,
Request baseRequest,
HttpServletRequest request,
HttpServletResponse response, AuthenticationResult ar)
throws IOException, ServletException {
String deployment = request.getParameter("deployment");
if (deployment == null || deployment.length() == 0) {
response.getWriter().print(buildClientResponse(jsonp, ClientResponse.UNEXPECTED_FAILURE, "Failed to get deployment information."));
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return;
}
try {
DeploymentType newDeployment = m_mapper.readValue(deployment, DeploymentType.class);
if (newDeployment == null) {
response.getWriter().print(buildClientResponse(jsonp, ClientResponse.UNEXPECTED_FAILURE, "Failed to parse deployment information."));
return;
}
DeploymentType currentDeployment = this.getDeployment();
if (currentDeployment.getUsers() != null) {
newDeployment.setUsers(currentDeployment.getUsers());
}
// reset the host count so that it wont fail the deployment checks
newDeployment.getCluster().setHostcount(currentDeployment.getCluster().getHostcount());
String dep = CatalogUtil.getDeployment(newDeployment);
if (dep == null || dep.trim().length() <= 0) {
response.getWriter().print(buildClientResponse(jsonp, ClientResponse.UNEXPECTED_FAILURE, "Failed to build deployment information."));
return;
}
Object[] params = new Object[] { null, dep};
SyncCallback cb = new SyncCallback();
httpClientInterface.callProcedure(ar, BatchTimeoutOverrideType.NO_TIMEOUT, cb, "@UpdateApplicationCatalog", params);
cb.waitForResponse();
ClientResponseImpl r = ClientResponseImpl.class.cast(cb.getResponse());
if (r.getStatus() == ClientResponse.SUCCESS) {
response.getWriter().print(buildClientResponse(jsonp, ClientResponse.SUCCESS, "Deployment Updated."));
} else {
response.getWriter().print(HTTPClientInterface.asJsonp(jsonp, r.toJSONString()));
}
baseRequest.setHandled(true);
} catch(JsonParseException e) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.getWriter().print(buildClientResponse(jsonp, ClientResponse.UNEXPECTED_FAILURE, "Unparsable JSON"));
baseRequest.setHandled(true);
} catch (Exception ex) {
logger.error("Failed to update deployment from API", ex);
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
response.getWriter().print(buildClientResponse(jsonp, ClientResponse.UNEXPECTED_FAILURE, Throwables.getStackTraceAsString(ex)));
baseRequest.setHandled(true);
}
}
//Handle POST for users
public void handleUpdateUser(String jsonp, String target,
Request baseRequest,
HttpServletRequest request,
HttpServletResponse response, AuthenticationResult ar)
throws IOException, ServletException {
String update = request.getParameter("user");
if (update == null || update.trim().length() == 0) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.getWriter().print(buildClientResponse(jsonp, ClientResponse.UNEXPECTED_FAILURE, "Failed to get user information."));
return;
}
try {
User newUser = m_mapper.readValue(update, User.class);
if (newUser == null) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.getWriter().print(buildClientResponse(jsonp, ClientResponse.UNEXPECTED_FAILURE, "Failed to parse user information."));
return;
}
DeploymentType newDeployment = CatalogUtil.getDeployment(new ByteArrayInputStream(getDeploymentBytes()));
User user = null;
String[] splitTarget = target.split("/");
if (splitTarget.length == 3) {
user = findUser(splitTarget[2], newDeployment);
}
if (user == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
response.getWriter().print(buildClientResponse(jsonp, ClientResponse.UNEXPECTED_FAILURE, "User not found"));
return;
}
user.setName(newUser.getName());
user.setPassword(newUser.getPassword());
user.setPlaintext(newUser.isPlaintext());
user.setRoles(newUser.getRoles());
String dep = CatalogUtil.getDeployment(newDeployment);
if (dep == null || dep.trim().length() <= 0) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.getWriter().print(buildClientResponse(jsonp, ClientResponse.UNEXPECTED_FAILURE, "Failed to build deployment information."));
return;
}
Object[] params = new Object[] { null, dep};
//Call sync as nothing else can happen when this is going on.
SyncCallback cb = new SyncCallback();
httpClientInterface.callProcedure(ar, BatchTimeoutOverrideType.NO_TIMEOUT, cb, "@UpdateApplicationCatalog", params);
cb.waitForResponse();
ClientResponseImpl r = ClientResponseImpl.class.cast(cb.getResponse());
if (r.getStatus() == ClientResponse.SUCCESS) {
response.getWriter().print(buildClientResponse(jsonp, ClientResponse.SUCCESS, "User Updated."));
} else {
response.getWriter().print(HTTPClientInterface.asJsonp(jsonp, r.toJSONString()));
}
} catch (Exception ex) {
logger.error("Failed to update user from API", ex);
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.getWriter().print(buildClientResponse(jsonp, ClientResponse.UNEXPECTED_FAILURE, Throwables.getStackTraceAsString(ex)));
}
}
//Handle PUT for users
public void handleCreateUser(String jsonp, String target,
Request baseRequest,
HttpServletRequest request,
HttpServletResponse response, AuthenticationResult ar)
throws IOException, ServletException {
String update = request.getParameter("user");
if (update == null || update.trim().length() == 0) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.getWriter().print(buildClientResponse(jsonp, ClientResponse.UNEXPECTED_FAILURE, "Failed to get user information."));
return;
}
try {
User newUser = m_mapper.readValue(update, User.class);
if (newUser == null) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.getWriter().print(buildClientResponse(jsonp, ClientResponse.UNEXPECTED_FAILURE, "Failed to parse user information."));
return;
}
DeploymentType newDeployment = CatalogUtil.getDeployment(new ByteArrayInputStream(getDeploymentBytes()));
User user = null;
String[] splitTarget = target.split("/");
if (splitTarget.length == 3) {
user = findUser(splitTarget[2], newDeployment);
} else {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
response.getWriter().print(buildClientResponse(jsonp, ClientResponse.UNEXPECTED_FAILURE, "User not found"));
return;
}
String returnString = "User created";
if (user == null) {
if (newDeployment.getUsers() == null) {
newDeployment.setUsers(new UsersType());
}
newDeployment.getUsers().getUser().add(newUser);
response.setStatus(HttpServletResponse.SC_CREATED);
} else {
user.setName(newUser.getName());
user.setPassword(newUser.getPassword());
user.setPlaintext(newUser.isPlaintext());
user.setRoles(newUser.getRoles());
returnString = "User updated";
response.setStatus(HttpServletResponse.SC_OK);
}
String dep = CatalogUtil.getDeployment(newDeployment);
if (dep == null || dep.trim().length() <= 0) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.getWriter().print(buildClientResponse(jsonp, ClientResponse.UNEXPECTED_FAILURE, "Failed to build deployment information."));
return;
}
Object[] params = new Object[] { null, dep};
//Call sync as nothing else can happen when this is going on.
SyncCallback cb = new SyncCallback();
httpClientInterface.callProcedure(ar, BatchTimeoutOverrideType.NO_TIMEOUT, cb, "@UpdateApplicationCatalog", params);
cb.waitForResponse();
ClientResponseImpl r = ClientResponseImpl.class.cast(cb.getResponse());
if (r.getStatus() == ClientResponse.SUCCESS) {
response.getWriter().print(buildClientResponse(jsonp, ClientResponse.SUCCESS, returnString));
} else {
response.getWriter().print(HTTPClientInterface.asJsonp(jsonp, r.toJSONString()));
}
} catch (Exception ex) {
logger.error("Failed to create user from API", ex);
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.getWriter().print(buildClientResponse(jsonp, ClientResponse.UNEXPECTED_FAILURE, Throwables.getStackTraceAsString(ex)));
}
}
//Handle DELETE for users
public void handleRemoveUser(String jsonp, String target,
Request baseRequest,
HttpServletRequest request,
HttpServletResponse response, AuthenticationResult ar)
throws IOException, ServletException {
try {
DeploymentType newDeployment = CatalogUtil.getDeployment(new ByteArrayInputStream(getDeploymentBytes()));
User user = null;
String[] splitTarget = target.split("/");
if (splitTarget.length == 3) {
user = findUser(splitTarget[2], newDeployment);
}
if (user == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
response.getWriter().print(buildClientResponse(jsonp, ClientResponse.UNEXPECTED_FAILURE, "User not found"));
return;
}
if (newDeployment.getUsers().getUser().size() == 1) {
newDeployment.setUsers(null);
} else {
newDeployment.getUsers().getUser().remove(user);
}
String dep = CatalogUtil.getDeployment(newDeployment);
if (dep == null || dep.trim().length() <= 0) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.getWriter().print(buildClientResponse(jsonp, ClientResponse.UNEXPECTED_FAILURE, "Failed to build deployment information."));
return;
}
Object[] params = new Object[] { null, dep};
//Call sync as nothing else can happen when this is going on.
SyncCallback cb = new SyncCallback();
httpClientInterface.callProcedure(ar, BatchTimeoutOverrideType.NO_TIMEOUT, cb, "@UpdateApplicationCatalog", params);
cb.waitForResponse();
ClientResponseImpl r = ClientResponseImpl.class.cast(cb.getResponse());
response.setStatus(HttpServletResponse.SC_NO_CONTENT);
if (r.getStatus() == ClientResponse.SUCCESS) {
response.getWriter().print(buildClientResponse(jsonp, ClientResponse.SUCCESS, "User Removed."));
} else {
response.getWriter().print(HTTPClientInterface.asJsonp(jsonp, r.toJSONString()));
}
} catch (Exception ex) {
logger.error("Failed to update role from API", ex);
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.getWriter().print(buildClientResponse(jsonp, ClientResponse.UNEXPECTED_FAILURE, Throwables.getStackTraceAsString(ex)));
}
}
//Handle GET for users
public void handleGetUsers(String jsonp, String target,
Request baseRequest,
HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException {
ObjectMapper mapper = new ObjectMapper();
User user = null;
String[] splitTarget = target.split("/");
if (splitTarget.length < 3 || splitTarget[2].isEmpty()) {
if (jsonp != null) {
response.getWriter().write(jsonp + "(");
}
if (getDeployment().getUsers() != null) {
List<IdUser> id = new ArrayList<>();
for(UsersType.User u : getDeployment().getUsers().getUser()) {
id.add(new IdUser(u, getHostHeader()));
}
mapper.writeValue(response.getWriter(), id);
} else {
response.getWriter().write("[]");
}
if (jsonp != null) {
response.getWriter().write(")");
}
return;
}
user = findUser(splitTarget[2], getDeployment());
if (user == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
response.getWriter().print(buildClientResponse(jsonp, ClientResponse.UNEXPECTED_FAILURE, "User not found"));
return;
} else {
if (jsonp != null) {
response.getWriter().write(jsonp + "(");
}
mapper.writeValue(response.getWriter(), new IdUser(user, getHostHeader()));
if (jsonp != null) {
response.getWriter().write(")");
}
}
}
//Handle GET for export types
public void handleGetExportTypes(String jsonp, HttpServletResponse response)
throws IOException, ServletException {
if (jsonp != null) {
response.getWriter().write(jsonp + "(");
}
JSONObject exportTypes = new JSONObject();
HashSet<String> exportList = new HashSet<>();
for (ServerExportEnum type : ServerExportEnum.values()) {
exportList.add(type.value().toUpperCase());
}
try {
exportTypes.put("types", exportList);
} catch (JSONException e) {
m_log.error("Failed to generate exportTypes JSON: ", e);
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
response.getWriter().print(buildClientResponse(jsonp, ClientResponse.UNEXPECTED_FAILURE, "Type list failed to build"));
return;
}
response.getWriter().write(exportTypes.toString());
if (jsonp != null) {
response.getWriter().write(")");
}
}
}
class APIRequestHandler extends VoltRequestHandler {
@Override
public void handle(String target, Request baseRequest,
HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
super.handle(target, baseRequest, request, response);
if (baseRequest.isHandled()) return;
try {
// http://www.ietf.org/rfc/rfc4627.txt dictates this mime type
response.setContentType(JSON_CONTENT_TYPE);
if (m_jsonEnabled) {
if (target.equals("/")) {
httpClientInterface.process(baseRequest, response);
} else {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
response.getWriter().println("Resource not found");
baseRequest.setHandled(true);
}
// used for perf testing of the http interface
/*String msg = "{\"status\":1,\"appstatus\":-128,\"statusstring\":null,\"appstatusstring\":null,\"exception\":null,\"results\":[{\"status\":-128,\"schema\":[{\"name\":\"SVAL1\",\"type\":9},{\"name\":\"SVAL2\",\"type\":9},{\"name\":\"SVAL3\",\"type\":9}],\"data\":[[\"FOO\",\"BAR\",\"BOO\"]]}]}";
response.setStatus(HttpServletResponse.SC_OK);
baseRequest.setHandled(true);
response.getWriter().print(msg);*/
} else {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
baseRequest.setHandled(true);
response.getWriter().println("JSON API IS CURRENTLY DISABLED");
}
} catch(Exception ex){
logger.info("Not servicing url: " + baseRequest.getRequestURI() + " Details: "+ ex.getMessage(), ex);
}
}
}
/**
* Draw the catalog report page, mostly by pulling it from the JAR.
*/
void handleReportPage(Request baseRequest, HttpServletResponse response) {
try {
String report = ReportMaker.liveReport();
response.setContentType(HTML_CONTENT_TYPE);
response.setStatus(HttpServletResponse.SC_OK);
baseRequest.setHandled(true);
response.getWriter().print(report);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
/**
* Load a template for the admin page, fill it out and return the value.
* @param params The key-value set of variables to replace in the template.
* @return The completed template.
*/
String getHTMLForAdminPage(Map<String,String> params) {
try {
String template = m_htmlTemplates.get("admintemplate.html");
for (Entry<String, String> e : params.entrySet()) {
String key = e.getKey().toUpperCase();
String value = e.getValue();
if (key == null) continue;
if (value == null) value = "NULL";
template = template.replace("#" + key + "#", value);
}
return template;
}
catch (Exception e) {
e.printStackTrace();
}
return "<html><body>An unrecoverable error was encountered while generating this page.</body></html>";
}
private void loadTemplate(Class<?> clz, String name) throws Exception {
URL url = Resources.getResource(clz, name);
String contents = Resources.toString(url, Charsets.UTF_8);
m_htmlTemplates.put(name, contents);
}
public HTTPAdminListener(
boolean jsonEnabled, String intf, String publicIntf, int port,
SslContextFactory sslContextFactory, boolean mustListen
) throws Exception {
int poolsize = Integer.getInteger("HTTP_POOL_SIZE", 50);
int timeout = Integer.getInteger("HTTP_REQUEST_TIMEOUT_SECONDS", 15);
int cacheMaxAge = Integer.getInteger("HTTP_STATIC_CACHE_MAXAGE", 24*60*60); // 24 hours
String resolvedIntf = intf == null ? "" : intf.trim().isEmpty() ? ""
: HostAndPort.fromHost(intf).withDefaultPort(port).toString();
m_publicIntf = publicIntf == null ? resolvedIntf : publicIntf.trim().isEmpty() ? resolvedIntf
: HostAndPort.fromHost(publicIntf).withDefaultPort(port).toString();
/*
* Don't force us to look at a huge pile of threads
*/
final QueuedThreadPool qtp = new QueuedThreadPool(
poolsize,
1, // minimum threads
timeout * 1000,
new LinkedBlockingQueue<>(poolsize + 16)
);
m_server = new Server(qtp);
m_server.setAttribute(
"org.eclipse.jetty.server.Request.maxFormContentSize",
new Integer(HTTPClientInterface.MAX_QUERY_PARAM_SIZE)
);
m_mustListen = mustListen;
// PRE-LOAD ALL HTML TEMPLATES (one for now)
try {
loadTemplate(HTTPAdminListener.class, "admintemplate.html");
}
catch (Exception e) {
VoltLogger logger = new VoltLogger("HOST");
logger.error("Unable to load HTML templates from jar for admin pages.", e);
throw e;
}
// NOW START SocketConnector and create Jetty server but dont start.
ServerConnector connector = null;
try {
if (sslContextFactory == null) { // basic HTTP
// The socket channel connector seems to be faster for our use
//SelectChannelConnector connector = new SelectChannelConnector();
connector = new ServerConnector(m_server);
if (intf != null && !intf.trim().isEmpty()) {
connector.setHost(intf);
}
connector.setPort(port);
connector.setName("VoltDB-HTTPD");
//open the connector here so we know if port is available and Init work can retry with next port.
connector.open();
m_server.addConnector(connector);
} else { // HTTPS
m_server.addConnector(getSSLServerConnector(sslContextFactory, intf, port));
}
//m_server.setConnectors(new Connector[] { connector, sslConnector });
//"/"
ContextHandler dbMonitorHandler = new ContextHandler("/");
dbMonitorHandler.setHandler(new DBMonitorHandler());
///api/1.0/
ContextHandler apiRequestHandler = new ContextHandler("/api/1.0");
// the default is 200k which well short of out 2M row size limit
apiRequestHandler.setMaxFormContentSize(HTTPClientInterface.MAX_QUERY_PARAM_SIZE);
// close another attack vector where potentially one may send a large number of keys
apiRequestHandler.setMaxFormKeys(HTTPClientInterface.MAX_FORM_KEYS);
apiRequestHandler.setHandler(new APIRequestHandler());
///catalog
ContextHandler catalogRequestHandler = new ContextHandler("/catalog");
catalogRequestHandler.setHandler(new CatalogRequestHandler());
///catalog
ContextHandler ddlRequestHandler = new ContextHandler("/ddl");
ddlRequestHandler.setHandler(new DDLRequestHandler());
///deployment
ContextHandler deploymentRequestHandler = new ContextHandler("/deployment");
m_deploymentHandler = new DeploymentRequestHandler();
deploymentRequestHandler.setHandler(m_deploymentHandler);
deploymentRequestHandler.setAllowNullPathInfo(true);
///profile
ContextHandler profileRequestHandler = new ContextHandler("/profile");
profileRequestHandler.setHandler(new UserProfileHandler());
ContextHandler cssResourceHandler = new ContextHandler("/css");
ResourceHandler cssResource = new CacheStaticResourceHandler(CSS_TARGET, cacheMaxAge);
cssResourceHandler.setHandler(cssResource);
ContextHandler imageResourceHandler = new ContextHandler("/images");
ResourceHandler imagesResource = new CacheStaticResourceHandler(IMAGES_TARGET, cacheMaxAge);
imageResourceHandler.setHandler(imagesResource);
ContextHandler jsResourceHandler = new ContextHandler("/js");
ResourceHandler jsResource = new CacheStaticResourceHandler(JS_TARGET, cacheMaxAge);
jsResourceHandler.setHandler(jsResource);
ContextHandlerCollection handlers = new ContextHandlerCollection();
handlers.setHandlers(new Handler[] {
apiRequestHandler,
catalogRequestHandler,
ddlRequestHandler,
deploymentRequestHandler,
profileRequestHandler,
dbMonitorHandler,
cssResourceHandler,
imageResourceHandler,
jsResourceHandler
});
GzipHandler compressResourcesHandler = new GzipHandler();
compressResourcesHandler.setHandler(handlers);
compressResourcesHandler.addExcludedMimeTypes(JSON_CONTENT_TYPE);
compressResourcesHandler.setIncludedMimeTypes("application/x-javascript", "text/css" ,
"image/gif", "image/png", "image/jpeg", HTML_CONTENT_TYPE);
m_server.setHandler(compressResourcesHandler);
httpClientInterface.setTimeout(timeout);
m_jsonEnabled = jsonEnabled;
} catch (Exception e) {
// double try to make sure the port doesn't get eaten
try { connector.close(); } catch (Exception e2) {}
try { m_server.destroy(); } catch (Exception e2) {}
throw new Exception(e);
}
}
private ServerConnector getSSLServerConnector(SslContextFactory sslContextFactory, String intf, int port)
throws IOException {
// SSL HTTP Configuration
HttpConfiguration httpsConfig = new HttpConfiguration();
httpsConfig.setSecureScheme("ssl");
httpsConfig.setSecurePort(port);
//Add this customizer to indicate we are in ssl land
httpsConfig.addCustomizer(new SecureRequestCustomizer());
HttpConnectionFactory factory = new HttpConnectionFactory(httpsConfig);
// SSL Connector
ServerConnector connector = new ServerConnector(m_server,
new SslConnectionFactory(sslContextFactory,HttpVersion.HTTP_1_1.asString()),
factory);
if (intf != null && !intf.trim().isEmpty()) {
connector.setHost(intf);
}
connector.setPort(port);
connector.setName("VoltDB-HTTPS");
connector.open();
return connector;
}
public void start() throws Exception {
try {
m_server.start();
} catch (Exception e) {
// double try to make sure the port doesn't get eaten
try { m_server.stop(); } catch (Exception e2) {}
try { m_server.destroy(); } catch (Exception e2) {}
//We only throw exception to halt and we expect to mustListen;
if (m_mustListen) {
throw new Exception(e);
}
}
}
public void stop() {
if (httpClientInterface != null) {
httpClientInterface.stop();
}
try {
m_server.stop();
m_server.join();
}
catch (Exception e) {}
try { m_server.destroy(); } catch (Exception e2) {}
m_server = null;
}
public void notifyOfCatalogUpdate() {
if (httpClientInterface != null) {
httpClientInterface.notifyOfCatalogUpdate();
}
}
}