/*
* 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.ignite.internal.processors.rest.protocols.http.jetty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.ignite.IgniteCheckedException;
import org.apache.ignite.IgniteLogger;
import org.apache.ignite.internal.processors.rest.GridRestCommand;
import org.apache.ignite.internal.processors.rest.GridRestProtocolHandler;
import org.apache.ignite.internal.processors.rest.GridRestResponse;
import org.apache.ignite.internal.processors.rest.request.DataStructuresRequest;
import org.apache.ignite.internal.processors.rest.request.GridRestCacheRequest;
import org.apache.ignite.internal.processors.rest.request.GridRestLogRequest;
import org.apache.ignite.internal.processors.rest.request.GridRestRequest;
import org.apache.ignite.internal.processors.rest.request.GridRestTaskRequest;
import org.apache.ignite.internal.processors.rest.request.GridRestTopologyRequest;
import org.apache.ignite.internal.processors.rest.request.RestQueryRequest;
import org.apache.ignite.internal.util.typedef.F;
import org.apache.ignite.internal.util.typedef.internal.U;
import org.apache.ignite.lang.IgniteClosure;
import org.apache.ignite.plugin.security.SecurityCredentials;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.jetbrains.annotations.Nullable;
import static org.apache.ignite.internal.processors.rest.GridRestCommand.CACHE_CONTAINS_KEYS;
import static org.apache.ignite.internal.processors.rest.GridRestCommand.CACHE_GET_ALL;
import static org.apache.ignite.internal.processors.rest.GridRestCommand.CACHE_PUT_ALL;
import static org.apache.ignite.internal.processors.rest.GridRestCommand.CACHE_REMOVE_ALL;
import static org.apache.ignite.internal.processors.rest.GridRestCommand.EXECUTE_SQL_QUERY;
import static org.apache.ignite.internal.processors.rest.GridRestResponse.STATUS_FAILED;
/**
* Jetty REST handler. The following URL format is supported: {@code /ignite?cmd=cmdName¶m1=abc¶m2=123}
*/
public class GridJettyRestHandler extends AbstractHandler {
/** Used to sent request charset. */
private static final String CHARSET = StandardCharsets.UTF_8.name();
/** Logger. */
private final IgniteLogger log;
/** Authentication checker. */
private final IgniteClosure<String, Boolean> authChecker;
/** Request handlers. */
private GridRestProtocolHandler hnd;
/** Default page. */
private volatile String dfltPage;
/** Favicon. */
private volatile byte[] favicon;
/** Mapper from Java object to JSON. */
private final ObjectMapper jsonMapper;
/**
* Creates new HTTP requests handler.
*
* @param hnd Handler.
* @param authChecker Authentication checking closure.
* @param log Logger.
*/
GridJettyRestHandler(GridRestProtocolHandler hnd, IgniteClosure<String, Boolean> authChecker, IgniteLogger log) {
assert hnd != null;
assert log != null;
this.hnd = hnd;
this.log = log;
this.authChecker = authChecker;
this.jsonMapper = new GridJettyObjectMapper();
// Init default page and favicon.
try {
initDefaultPage();
if (log.isDebugEnabled())
log.debug("Initialized default page.");
}
catch (IOException e) {
U.warn(log, "Failed to initialize default page: " + e.getMessage());
}
try {
initFavicon();
if (log.isDebugEnabled())
log.debug(favicon != null ? "Initialized favicon, size: " + favicon.length : "Favicon is null.");
}
catch (IOException e) {
U.warn(log, "Failed to initialize favicon: " + e.getMessage());
}
}
/**
* Retrieves long value from parameters map.
*
* @param key Key.
* @param params Parameters map.
* @param dfltVal Default value.
* @return Long value from parameters map or {@code dfltVal} if null or not exists.
* @throws IgniteCheckedException If parsing failed.
*/
@Nullable private static Long longValue(String key, Map<String, Object> params,
Long dfltVal) throws IgniteCheckedException {
assert key != null;
String val = (String)params.get(key);
try {
return val == null ? dfltVal : Long.valueOf(val);
}
catch (NumberFormatException ignore) {
throw new IgniteCheckedException("Failed to parse parameter of Long type [" + key + "=" + val + "]");
}
}
/**
* Retrieves int value from parameters map.
*
* @param key Key.
* @param params Parameters map.
* @param dfltVal Default value.
* @return Integer value from parameters map or {@code dfltVal} if null or not exists.
* @throws IgniteCheckedException If parsing failed.
*/
private static int intValue(String key, Map<String, Object> params, int dfltVal) throws IgniteCheckedException {
assert key != null;
String val = (String)params.get(key);
try {
return val == null ? dfltVal : Integer.parseInt(val);
}
catch (NumberFormatException ignore) {
throw new IgniteCheckedException("Failed to parse parameter of Integer type [" + key + "=" + val + "]");
}
}
/**
* Retrieves UUID value from parameters map.
*
* @param key Key.
* @param params Parameters map.
* @return UUID value from parameters map or {@code null} if null or not exists.
* @throws IgniteCheckedException If parsing failed.
*/
@Nullable private static UUID uuidValue(String key, Map<String, Object> params) throws IgniteCheckedException {
assert key != null;
String val = (String)params.get(key);
try {
return val == null ? null : UUID.fromString(val);
}
catch (NumberFormatException ignore) {
throw new IgniteCheckedException("Failed to parse parameter of UUID type [" + key + "=" + val + "]");
}
}
/**
* @throws IOException If failed.
*/
private void initDefaultPage() throws IOException {
assert dfltPage == null;
InputStream in = getClass().getResourceAsStream("rest.html");
if (in != null) {
LineNumberReader rdr = new LineNumberReader(new InputStreamReader(in, CHARSET));
try {
StringBuilder buf = new StringBuilder(2048);
for (String line = rdr.readLine(); line != null; line = rdr.readLine()) {
buf.append(line);
if (!line.endsWith(" "))
buf.append(' ');
}
dfltPage = buf.toString();
}
finally {
U.closeQuiet(rdr);
}
}
}
/**
* @throws IOException If failed.
*/
private void initFavicon() throws IOException {
assert favicon == null;
InputStream in = getClass().getResourceAsStream("favicon.ico");
if (in != null) {
BufferedInputStream bis = new BufferedInputStream(in);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try {
byte[] buf = new byte[2048];
while (true) {
int n = bis.read(buf);
if (n == -1)
break;
bos.write(buf, 0, n);
}
favicon = bos.toByteArray();
}
finally {
U.closeQuiet(bis);
}
}
}
/** {@inheritDoc} */
@Override public void handle(String target, Request req, HttpServletRequest srvReq, HttpServletResponse res)
throws IOException, ServletException {
if (log.isDebugEnabled())
log.debug("Handling request [target=" + target + ", req=" + req + ", srvReq=" + srvReq + ']');
if (target.startsWith("/ignite")) {
processRequest(target, srvReq, res);
req.setHandled(true);
}
else if (target.startsWith("/favicon.ico")) {
if (favicon == null) {
res.setStatus(HttpServletResponse.SC_NOT_FOUND);
req.setHandled(true);
return;
}
res.setStatus(HttpServletResponse.SC_OK);
res.setContentType("image/x-icon");
res.getOutputStream().write(favicon);
res.getOutputStream().flush();
req.setHandled(true);
}
else {
if (dfltPage == null) {
res.setStatus(HttpServletResponse.SC_NOT_FOUND);
req.setHandled(true);
return;
}
res.setStatus(HttpServletResponse.SC_OK);
res.setContentType("text/html");
res.getWriter().write(dfltPage);
res.getWriter().flush();
req.setHandled(true);
}
}
/**
* Process HTTP request.
*
* @param act Action.
* @param req Http request.
* @param res Http response.
*/
private void processRequest(String act, HttpServletRequest req, HttpServletResponse res) {
res.setContentType("application/json");
res.setCharacterEncoding("UTF-8");
GridRestCommand cmd = command(req);
if (cmd == null) {
res.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return;
}
if (!authChecker.apply(req.getHeader("X-Signature"))) {
res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
GridRestResponse cmdRes;
Map<String, Object> params = parameters(req);
try {
GridRestRequest cmdReq = createRequest(cmd, params, req);
if (log.isDebugEnabled())
log.debug("Initialized command request: " + cmdReq);
cmdRes = hnd.handle(cmdReq);
if (cmdRes == null)
throw new IllegalStateException("Received null result from handler: " + hnd);
byte[] sesTok = cmdRes.sessionTokenBytes();
if (sesTok != null)
cmdRes.setSessionToken(U.byteArray2HexString(sesTok));
res.setStatus(HttpServletResponse.SC_OK);
}
catch (Throwable e) {
res.setStatus(HttpServletResponse.SC_OK);
U.error(log, "Failed to process HTTP request [action=" + act + ", req=" + req + ']', e);
cmdRes = new GridRestResponse(STATUS_FAILED, e.getMessage());
if (e instanceof Error)
throw (Error)e;
}
String json;
try {
json = jsonMapper.writeValueAsString(cmdRes);
}
catch (JsonProcessingException e1) {
U.error(log, "Failed to convert response to JSON: " + cmdRes, e1);
GridRestResponse resFailed = new GridRestResponse(STATUS_FAILED, e1.getMessage());
try {
json = jsonMapper.writeValueAsString(resFailed);
}
catch (JsonProcessingException e2) {
json = "{\"successStatus\": \"1\", \"error:\" \"" + e2.getMessage() + "\"}}";
}
}
try {
if (log.isDebugEnabled())
log.debug("Parsed command response into JSON object: " + json);
res.getWriter().write(json);
if (log.isDebugEnabled())
log.debug("Processed HTTP request [action=" + act + ", jsonRes=" + cmdRes + ", req=" + req + ']');
}
catch (IOException e) {
U.error(log, "Failed to send HTTP response: " + json, e);
}
}
/**
* Creates REST request.
*
* @param cmd Command.
* @param params Parameters.
* @param req Servlet request.
* @return REST request.
* @throws IgniteCheckedException If creation failed.
*/
@Nullable private GridRestRequest createRequest(GridRestCommand cmd,
Map<String, Object> params, HttpServletRequest req) throws IgniteCheckedException {
GridRestRequest restReq;
switch (cmd) {
case GET_OR_CREATE_CACHE:
case DESTROY_CACHE: {
GridRestCacheRequest restReq0 = new GridRestCacheRequest();
restReq0.cacheName((String)params.get("cacheName"));
restReq = restReq0;
break;
}
case ATOMIC_DECREMENT:
case ATOMIC_INCREMENT: {
DataStructuresRequest restReq0 = new DataStructuresRequest();
restReq0.key(params.get("key"));
restReq0.initial(longValue("init", params, null));
restReq0.delta(longValue("delta", params, null));
restReq = restReq0;
break;
}
case CACHE_CONTAINS_KEY:
case CACHE_CONTAINS_KEYS:
case CACHE_GET:
case CACHE_GET_ALL:
case CACHE_GET_AND_PUT:
case CACHE_GET_AND_REPLACE:
case CACHE_PUT_IF_ABSENT:
case CACHE_GET_AND_PUT_IF_ABSENT:
case CACHE_PUT:
case CACHE_PUT_ALL:
case CACHE_REMOVE:
case CACHE_REMOVE_VALUE:
case CACHE_REPLACE_VALUE:
case CACHE_GET_AND_REMOVE:
case CACHE_REMOVE_ALL:
case CACHE_ADD:
case CACHE_CAS:
case CACHE_METRICS:
case CACHE_SIZE:
case CACHE_METADATA:
case CACHE_REPLACE:
case CACHE_APPEND:
case CACHE_PREPEND: {
GridRestCacheRequest restReq0 = new GridRestCacheRequest();
String cacheName = (String)params.get("cacheName");
restReq0.cacheName(F.isEmpty(cacheName) ? null : cacheName);
restReq0.key(params.get("key"));
restReq0.value(params.get("val"));
restReq0.value2(params.get("val2"));
Object val1 = params.get("val1");
if (val1 != null)
restReq0.value(val1);
restReq0.cacheFlags(intValue("cacheFlags", params, 0));
restReq0.ttl(longValue("exp", params, null));
if (cmd == CACHE_GET_ALL || cmd == CACHE_PUT_ALL || cmd == CACHE_REMOVE_ALL ||
cmd == CACHE_CONTAINS_KEYS) {
List<Object> keys = values("k", params);
List<Object> vals = values("v", params);
if (keys.size() < vals.size())
throw new IgniteCheckedException("Number of keys must be greater or equals to number of values.");
Map<Object, Object> map = U.newHashMap(keys.size());
Iterator<Object> keyIt = keys.iterator();
Iterator<Object> valIt = vals.iterator();
while (keyIt.hasNext())
map.put(keyIt.next(), valIt.hasNext() ? valIt.next() : null);
restReq0.values(map);
}
restReq = restReq0;
break;
}
case TOPOLOGY:
case NODE: {
GridRestTopologyRequest restReq0 = new GridRestTopologyRequest();
restReq0.includeMetrics(Boolean.parseBoolean((String)params.get("mtr")));
restReq0.includeAttributes(Boolean.parseBoolean((String)params.get("attr")));
restReq0.nodeIp((String)params.get("ip"));
restReq0.nodeId(uuidValue("id", params));
restReq = restReq0;
break;
}
case EXE:
case RESULT:
case NOOP: {
GridRestTaskRequest restReq0 = new GridRestTaskRequest();
restReq0.taskId((String)params.get("id"));
restReq0.taskName((String)params.get("name"));
restReq0.params(values("p", params));
restReq0.async(Boolean.parseBoolean((String)params.get("async")));
restReq0.timeout(longValue("timeout", params, 0L));
restReq = restReq0;
break;
}
case LOG: {
GridRestLogRequest restReq0 = new GridRestLogRequest();
restReq0.path((String)params.get("path"));
restReq0.from(intValue("from", params, -1));
restReq0.to(intValue("to", params, -1));
restReq = restReq0;
break;
}
case NAME:
case VERSION: {
restReq = new GridRestRequest();
break;
}
case EXECUTE_SQL_QUERY:
case EXECUTE_SQL_FIELDS_QUERY: {
RestQueryRequest restReq0 = new RestQueryRequest();
restReq0.sqlQuery((String)params.get("qry"));
restReq0.arguments(values("arg", params).toArray());
restReq0.typeName((String)params.get("type"));
String pageSize = (String)params.get("pageSize");
if (pageSize != null)
restReq0.pageSize(Integer.parseInt(pageSize));
String distributedJoins = (String)params.get("distributedJoins");
if (distributedJoins != null)
restReq0.distributedJoins(Boolean.parseBoolean(distributedJoins));
restReq0.cacheName((String)params.get("cacheName"));
if (cmd == EXECUTE_SQL_QUERY)
restReq0.queryType(RestQueryRequest.QueryType.SQL);
else
restReq0.queryType(RestQueryRequest.QueryType.SQL_FIELDS);
restReq = restReq0;
break;
}
case EXECUTE_SCAN_QUERY: {
RestQueryRequest restReq0 = new RestQueryRequest();
restReq0.sqlQuery((String)params.get("qry"));
String pageSize = (String)params.get("pageSize");
if (pageSize != null)
restReq0.pageSize(Integer.parseInt(pageSize));
restReq0.cacheName((String)params.get("cacheName"));
restReq0.className((String)params.get("className"));
restReq0.queryType(RestQueryRequest.QueryType.SCAN);
restReq = restReq0;
break;
}
case FETCH_SQL_QUERY: {
RestQueryRequest restReq0 = new RestQueryRequest();
String qryId = (String)params.get("qryId");
if (qryId != null)
restReq0.queryId(Long.parseLong(qryId));
String pageSize = (String)params.get("pageSize");
if (pageSize != null)
restReq0.pageSize(Integer.parseInt(pageSize));
restReq0.cacheName((String)params.get("cacheName"));
restReq = restReq0;
break;
}
case CLOSE_SQL_QUERY: {
RestQueryRequest restReq0 = new RestQueryRequest();
String qryId = (String)params.get("qryId");
if (qryId != null)
restReq0.queryId(Long.parseLong(qryId));
restReq0.cacheName((String)params.get("cacheName"));
restReq = restReq0;
break;
}
default:
throw new IgniteCheckedException("Invalid command: " + cmd);
}
restReq.address(new InetSocketAddress(req.getRemoteAddr(), req.getRemotePort()));
restReq.command(cmd);
if (params.containsKey("ignite.login") || params.containsKey("ignite.password")) {
SecurityCredentials cred = new SecurityCredentials(
(String)params.get("ignite.login"), (String)params.get("ignite.password"));
restReq.credentials(cred);
}
String clientId = (String)params.get("clientId");
try {
if (clientId != null)
restReq.clientId(UUID.fromString(clientId));
}
catch (Exception ignored) {
// Ignore invalid client id. Rest handler will process this logic.
}
String destId = (String)params.get("destId");
try {
if (destId != null)
restReq.destinationId(UUID.fromString(destId));
}
catch (IllegalArgumentException ignored) {
// Don't fail - try to execute locally.
}
String sesTokStr = (String)params.get("sessionToken");
try {
if (sesTokStr != null)
restReq.sessionToken(U.hexString2ByteArray(sesTokStr));
}
catch (IllegalArgumentException ignored) {
// Ignore invalid session token.
}
return restReq;
}
/**
* Gets values referenced by sequential keys, e.g. {@code key1...keyN}.
*
* @param keyPrefix Key prefix, e.g. {@code key} for {@code key1...keyN}.
* @param params Parameters map.
* @return Values.
*/
protected List<Object> values(String keyPrefix, Map<String, Object> params) {
assert keyPrefix != null;
List<Object> vals = new LinkedList<>();
for (int i = 1; ; i++) {
String key = keyPrefix + i;
if (params.containsKey(key))
vals.add(params.get(key));
else
break;
}
return vals;
}
/**
* @param req Request.
* @return Command.
*/
@Nullable private GridRestCommand command(ServletRequest req) {
String cmd = req.getParameter("cmd");
return cmd == null ? null : GridRestCommand.fromKey(cmd.toLowerCase());
}
/**
* Parses HTTP parameters in an appropriate format and return back map of values to predefined list of names.
*
* @param req Request.
* @return Map of parsed parameters.
*/
@SuppressWarnings({"unchecked"})
private Map<String, Object> parameters(ServletRequest req) {
Map<String, String[]> params = req.getParameterMap();
if (F.isEmpty(params))
return Collections.emptyMap();
Map<String, Object> map = U.newHashMap(params.size());
for (Map.Entry<String, String[]> entry : params.entrySet())
map.put(entry.getKey(), parameter(entry.getValue()));
return map;
}
/**
* @param obj Parameter object.
* @return Parameter value.
*/
@Nullable private String parameter(Object obj) {
if (obj instanceof String)
return (String)obj;
else if (obj instanceof String[] && ((String[])obj).length > 0)
return ((String[])obj)[0];
return null;
}
}