com.imaginary.home.cloud.api.RestApi.java Source code

Java tutorial

Introduction

Here is the source code for com.imaginary.home.cloud.api.RestApi.java

Source

/*
 * Copyright (C) 2013 George Reese
 *
 * Licensed 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 com.imaginary.home.cloud.api;

import com.imaginary.home.cloud.Configuration;
import com.imaginary.home.cloud.ControllerRelay;
import com.imaginary.home.cloud.api.call.CommandCall;
import com.imaginary.home.cloud.api.call.DeviceCall;
import com.imaginary.home.cloud.api.call.LocationCall;
import com.imaginary.home.cloud.api.call.RelayCall;
import com.imaginary.home.cloud.user.ApiKey;
import com.imaginary.home.cloud.user.User;
import com.imaginary.home.controller.CloudService;
import org.dasein.persist.PersistenceException;
import org.json.JSONObject;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

/**
 * Primary servlet dispatcher for incoming REST API calls. This class handles the incoming calls for both
 * the external API and the controller relay API.
 * <p>Created by George Reese: 1/12/13 3:47 PM</p>
 * @author George Reese
 */
public class RestApi extends HttpServlet {
    static public final String[] VERSIONS = { "2013-01" };

    static public final String API_KEY = "x-imaginary-api-key";
    static public final String SIGNATURE = "x-imaginary-signature";
    static public final String TIMESTAMP = "x-imaginary-timestamp";
    static public final String VERSION = "x-imaginary-version";

    static private final HashMap<String, APICall> apiCalls = new HashMap<String, APICall>();

    static {
        apiCalls.put("location", new LocationCall());
        apiCalls.put("relay", new RelayCall());
        apiCalls.put("command", new CommandCall());
        apiCalls.put("device", new DeviceCall());
    }

    @SuppressWarnings("UnusedDeclaration") // this is going to be unused until a new API version comes out
    static public boolean supports(@Nonnull String requiredVersion, @Nonnull String clientVersion) {
        if (clientVersion.equals(requiredVersion)) {
            return true;
        }
        for (String v : VERSIONS) {
            if (v.equals(clientVersion)) {
                return true;
            }
            if (v.equals(requiredVersion)) {
                return false;
            }
        }
        return false;
    }

    public @Nullable String authenticate(@Nonnull String method, @Nonnull HttpServletRequest request,
            Map<String, Object> headers) throws RestException {
        Number timestamp = (Number) headers.get(TIMESTAMP);
        String apiKey = (String) headers.get(API_KEY);
        String signature = (String) headers.get(SIGNATURE);
        String version = (String) headers.get(VERSION);

        if (timestamp == null || apiKey == null || signature == null || version == null) {
            throw new RestException(HttpServletResponse.SC_BAD_REQUEST, RestException.INCOMPLETE_HEADERS,
                    "Incomplete authentication headers, requires: " + API_KEY + " - " + TIMESTAMP + " - "
                            + SIGNATURE + " - " + VERSION);
        }
        if (signature.length() < 1) {
            throw new RestException(HttpServletResponse.SC_FORBIDDEN, RestException.NO_SIGNATURE,
                    "No signature was provided for authentication");
        }
        try {
            ControllerRelay relay = ControllerRelay.getRelay(apiKey);
            String userId = null;
            String customSalt;
            String secret;

            if (relay == null) {
                ApiKey key = ApiKey.getApiKey(apiKey);

                if (key == null) {
                    throw new RestException(HttpServletResponse.SC_FORBIDDEN, RestException.INVALID_KEY,
                            "Invalid API key");
                }
                secret = key.getApiKeySecret();
                userId = key.getUserId();
                customSalt = userId;
            } else {
                secret = relay.getApiKeySecret();
                customSalt = relay.getLocationId();
            }
            String stringToSign;

            if (relay != null) {
                String token = Configuration.decrypt(relay.getLocationId(), relay.getToken());

                stringToSign = method.toLowerCase() + ":" + request.getPathInfo().toLowerCase() + ":" + apiKey + ":"
                        + token + ":" + timestamp.longValue() + ":" + version;
            } else {
                stringToSign = method.toLowerCase() + ":" + request.getPathInfo().toLowerCase() + ":" + apiKey + ":"
                        + timestamp.longValue() + ":" + version;
            }
            String expected;

            try {
                expected = CloudService.sign(Configuration.decrypt(customSalt, secret).getBytes("utf-8"),
                        stringToSign);
            } catch (Exception e) {
                throw new RestException(e);
            }
            if (!signature.equals(expected)) {
                throw new RestException(HttpServletResponse.SC_FORBIDDEN, RestException.INVALID_SIGNATURE,
                        "String to sign was: " + stringToSign);
            }
            return userId;
        } catch (PersistenceException e) {
            throw new RestException(e);
        }
    }

    @Override
    public void doDelete(@Nonnull HttpServletRequest req, @Nonnull HttpServletResponse resp)
            throws IOException, ServletException {
        String requestId = request(req);

        try {
            Map<String, Object> headers = parseHeaders(req);
            String[] path = getPath(req);

            if (path.length < 1) {
                throw new RestException(HttpServletResponse.SC_METHOD_NOT_ALLOWED, RestException.INVALID_OPERATION,
                        "No DELETE is allowed against /");
            } else if (apiCalls.containsKey(path[0])) {
                Map<String, Object> parameters = parseParameters(req);
                APICall call = apiCalls.get(path[0]);

                String userId = authenticate("DELETE", req, headers);

                call.delete(requestId, userId, path, req, resp, headers, parameters);
            } else {
                throw new RestException(HttpServletResponse.SC_NOT_FOUND, RestException.NO_SUCH_RESOURCE,
                        "No " + path[0] + " resource exists in this API");
            }
        } catch (RestException e) {
            HashMap<String, Object> error = new HashMap<String, Object>();

            error.put("code", e.getStatus());
            error.put("message", e.getMessage());
            error.put("description", e.getDescription());
            resp.setStatus(e.getStatus());
            resp.getWriter().println((new JSONObject(error)).toString());
            resp.getWriter().flush();
        } catch (Throwable t) {
            t.printStackTrace();
            HashMap<String, Object> error = new HashMap<String, Object>();

            error.put("code", HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            error.put("message", RestException.INTERNAL_ERROR);
            error.put("description", t.getMessage());
            resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            resp.getWriter().println((new JSONObject(error)).toString());
            resp.getWriter().flush();
        }
    }

    @Override
    public void doGet(@Nonnull HttpServletRequest req, @Nonnull HttpServletResponse resp)
            throws IOException, ServletException {
        String requestId = request(req);

        try {
            Map<String, Object> headers = parseHeaders(req);
            String[] path = getPath(req);

            if (path.length < 1) {
                // TODO: documentation
                throw new RestException(HttpServletResponse.SC_METHOD_NOT_ALLOWED, RestException.INVALID_OPERATION,
                        "No GET is allowed against /");
            } else if (apiCalls.containsKey(path[0])) {
                Map<String, Object> parameters = parseParameters(req);
                APICall call = apiCalls.get(path[0]);

                String userId = authenticate("GET", req, headers);

                call.get(requestId, userId, path, req, resp, headers, parameters);
            } else {
                throw new RestException(HttpServletResponse.SC_NOT_FOUND, RestException.NO_SUCH_RESOURCE,
                        "No " + path[0] + " resource exists in this API");
            }
        } catch (RestException e) {
            HashMap<String, Object> error = new HashMap<String, Object>();

            error.put("code", e.getStatus());
            error.put("message", e.getMessage());
            error.put("description", e.getDescription());
            resp.setStatus(e.getStatus());
            resp.getWriter().println((new JSONObject(error)).toString());
            resp.getWriter().flush();
        } catch (Throwable t) {
            t.printStackTrace();
            HashMap<String, Object> error = new HashMap<String, Object>();

            error.put("code", HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            error.put("message", RestException.INTERNAL_ERROR);
            error.put("description", t.getMessage());
            resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            resp.getWriter().println((new JSONObject(error)).toString());
            resp.getWriter().flush();
        }
    }

    @Override
    public void doHead(@Nonnull HttpServletRequest req, @Nonnull HttpServletResponse resp)
            throws IOException, ServletException {
        String requestId = request(req);

        try {
            Map<String, Object> headers = parseHeaders(req);
            String[] path = getPath(req);

            if (path.length < 1) {
                // TODO: documentation
                throw new RestException(HttpServletResponse.SC_METHOD_NOT_ALLOWED, RestException.INVALID_OPERATION,
                        "No HEAD is allowed against /");
            } else if (apiCalls.containsKey(path[0])) {
                Map<String, Object> parameters = parseParameters(req);
                APICall call = apiCalls.get(path[0]);

                String userId = authenticate("HEAD", req, headers);

                call.head(requestId, userId, path, req, resp, headers, parameters);
            } else {
                throw new RestException(HttpServletResponse.SC_NOT_FOUND, RestException.NO_SUCH_RESOURCE,
                        "No " + path[0] + " resource exists in this API");
            }
        } catch (RestException e) {
            HashMap<String, Object> error = new HashMap<String, Object>();

            error.put("code", e.getStatus());
            error.put("message", e.getMessage());
            error.put("description", e.getDescription());
            resp.setStatus(e.getStatus());
            resp.getWriter().println((new JSONObject(error)).toString());
            resp.getWriter().flush();
        } catch (Throwable t) {
            t.printStackTrace();
            HashMap<String, Object> error = new HashMap<String, Object>();

            error.put("code", HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            error.put("message", RestException.INTERNAL_ERROR);
            error.put("description", t.getMessage());
            resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            resp.getWriter().println((new JSONObject(error)).toString());
            resp.getWriter().flush();
        }
    }

    @Override
    public void doPost(@Nonnull HttpServletRequest req, @Nonnull HttpServletResponse resp) throws IOException {
        String requestId = request(req);

        try {
            Map<String, Object> headers = parseHeaders(req);
            String[] path = getPath(req);

            if (path.length < 1) {
                throw new RestException(HttpServletResponse.SC_METHOD_NOT_ALLOWED, RestException.INVALID_OPERATION,
                        "No POST is allowed against /");
            } else if (path[0].equals("token")) {
                String token = generateToken("POST", req, headers);
                HashMap<String, Object> json = new HashMap<String, Object>();

                json.put("token", token);
                resp.setStatus(HttpServletResponse.SC_CREATED);
                resp.getWriter().println((new JSONObject(json)).toString());
                resp.getWriter().flush();
            } else if (path[0].equals("relay")) {
                Map<String, Object> parameters = parseParameters(req);
                APICall call = apiCalls.get("relay");

                call.post(requestId, null, path, req, resp, headers, parameters);
            } else if (path[0].equals("user")) {
                BufferedReader reader = new BufferedReader(new InputStreamReader(req.getInputStream()));
                StringBuilder source = new StringBuilder();
                String line;

                while ((line = reader.readLine()) != null) {
                    source.append(line);
                    source.append(" ");
                }
                String email = null, firstName = null, lastName = null, password = null;
                JSONObject object = new JSONObject(source.toString());

                if (object.has("email") && !object.isNull("email")) {
                    email = object.getString("email").toLowerCase();
                }
                if (object.has("firstName") && !object.isNull("firstName")) {
                    firstName = object.getString("firstName");
                }
                if (object.has("lastName") && !object.isNull("lastName")) {
                    lastName = object.getString("lastName");
                }
                if (object.has("password") && !object.isNull("password")) {
                    password = object.getString("password");
                }
                if (email == null || firstName == null || lastName == null || password == null) {
                    throw new RestException(HttpServletResponse.SC_BAD_REQUEST, RestException.MISSING_DATA,
                            "Required fields: email, firstName, lastName, password");
                }
                User user = User.create(email, firstName, lastName, password);
                ApiKey key = ApiKey.create(user, "default");

                HashMap<String, Object> json = new HashMap<String, Object>();

                json.put("email", email);
                json.put("firstName", firstName);
                json.put("lastName", lastName);
                json.put("userId", user.getUserId());

                HashMap<String, Object> k = new HashMap<String, Object>();

                k.put("apiKeyId", key.getApiKeyId());
                k.put("apiKeySecret", Configuration.decrypt(user.getUserId(), key.getApiKeySecret()));
                k.put("userId", user.getUserId());

                json.put("apiKeys", k);

                resp.setStatus(HttpServletResponse.SC_CREATED);
                resp.getWriter().println((new JSONObject(json)).toString());
                resp.getWriter().flush();
            } else if (apiCalls.containsKey(path[0])) {
                Map<String, Object> parameters = parseParameters(req);
                APICall call = apiCalls.get(path[0]);

                String userId = authenticate("POST", req, headers);

                call.post(requestId, userId, path, req, resp, headers, parameters);
            } else {
                throw new RestException(HttpServletResponse.SC_NOT_FOUND, RestException.NO_SUCH_RESOURCE,
                        "No " + path[0] + " resource exists in this API");
            }
        } catch (RestException e) {
            HashMap<String, Object> error = new HashMap<String, Object>();

            error.put("code", e.getStatus());
            error.put("message", e.getMessage());
            error.put("description", e.getDescription());
            resp.setStatus(e.getStatus());
            resp.getWriter().println((new JSONObject(error)).toString());
            resp.getWriter().flush();
        } catch (Throwable t) {
            t.printStackTrace();
            HashMap<String, Object> error = new HashMap<String, Object>();

            error.put("code", HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            error.put("message", RestException.INTERNAL_ERROR);
            error.put("description", t.getMessage());
            resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            resp.getWriter().println((new JSONObject(error)).toString());
            resp.getWriter().flush();
        }
    }

    @Override
    public void doPut(@Nonnull HttpServletRequest req, @Nonnull HttpServletResponse resp)
            throws IOException, ServletException {
        String requestId = request(req);

        try {
            Map<String, Object> headers = parseHeaders(req);
            String[] path = getPath(req);

            if (path.length < 1) {
                throw new RestException(HttpServletResponse.SC_METHOD_NOT_ALLOWED, RestException.INVALID_OPERATION,
                        "No PUT is allowed against /");
            } else if (apiCalls.containsKey(path[0])) {
                Map<String, Object> parameters = parseParameters(req);
                APICall call = apiCalls.get(path[0]);

                String userId = authenticate("PUT", req, headers);

                call.put(requestId, userId, path, req, resp, headers, parameters);
            } else {
                throw new RestException(HttpServletResponse.SC_NOT_FOUND, RestException.NO_SUCH_RESOURCE,
                        "No " + path[0] + " resource exists in this API");
            }
        } catch (RestException e) {
            HashMap<String, Object> error = new HashMap<String, Object>();

            error.put("code", e.getStatus());
            error.put("message", e.getMessage());
            error.put("description", e.getDescription());
            resp.setStatus(e.getStatus());
            resp.getWriter().println((new JSONObject(error)).toString());
            resp.getWriter().flush();
        } catch (Throwable t) {
            t.printStackTrace();
            HashMap<String, Object> error = new HashMap<String, Object>();

            error.put("code", HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            error.put("message", RestException.INTERNAL_ERROR);
            error.put("description", t.getMessage());
            resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            resp.getWriter().println((new JSONObject(error)).toString());
            resp.getWriter().flush();
        }
    }

    public @Nonnull String generateToken(@Nonnull String method, @Nonnull HttpServletRequest request,
            Map<String, Object> headers) throws RestException {
        Number timestamp = (Number) headers.get(TIMESTAMP);
        String apiKey = (String) headers.get(API_KEY);
        String signature = (String) headers.get(SIGNATURE);
        String version = (String) headers.get(VERSION);

        if (timestamp == null || apiKey == null || signature == null || version == null) {
            throw new RestException(HttpServletResponse.SC_BAD_REQUEST,
                    "Incomplete authentication headers, requires: " + API_KEY + " - " + TIMESTAMP + " - "
                            + SIGNATURE + " - " + VERSION);
        }
        if (signature.length() < 1) {
            throw new RestException(HttpServletResponse.SC_FORBIDDEN,
                    "No signature was provided for authentication");
        }
        try {
            ControllerRelay relay = ControllerRelay.getRelay(apiKey);
            String secret, customSalt;

            if (relay == null) {
                throw new RestException(HttpServletResponse.SC_BAD_REQUEST, RestException.BAD_TOKEN,
                        "User keys don't use token authentication");
            } else {
                secret = relay.getApiKeySecret();
                customSalt = relay.getLocationId();
            }

            String stringToSign = method.toLowerCase() + ":" + request.getPathInfo().toLowerCase() + ":" + apiKey
                    + ":" + timestamp.longValue() + ":" + version;
            String expected;

            try {
                expected = CloudService.sign(Configuration.decrypt(customSalt, secret).getBytes("utf-8"),
                        stringToSign);
            } catch (Exception e) {
                throw new RestException(e);
            }
            if (signature.equals(expected)) {
                String token = Configuration.generateToken(40, 60);

                relay.setToken(token);
                return token;
            }
            throw new RestException(HttpServletResponse.SC_FORBIDDEN, "Illegal Access",
                    "Illegal access to requested resource");
        } catch (PersistenceException e) {
            throw new RestException(e);
        }
    }

    private @Nullable Object getHeader(@Nonnull String key, Enumeration<String> values) throws RestException {
        if (values == null || !values.hasMoreElements()) {
            return null;
        }
        if (key.equalsIgnoreCase(API_KEY) || key.equals(SIGNATURE) || key.equals(VERSION)) {
            return values.nextElement();
        }
        if (key.equalsIgnoreCase(TIMESTAMP)) {
            try {
                return Long.parseLong(values.nextElement());
            } catch (NumberFormatException e) {
                throw new RestException(HttpServletResponse.SC_BAD_REQUEST,
                        "Timestamps are the UNIX timestamp as the number of seconds since the Unix epoch.");
            }
        }
        return values;
    }

    private String[] getPath(@Nonnull HttpServletRequest req) {
        String p = req.getPathInfo().toLowerCase();

        while (p.startsWith("/") && !p.equals("/")) {
            p = p.substring(1);
        }
        while (p.endsWith("/") && !p.equals("/")) {
            p = p.substring(p.length() - 1);
        }
        String[] parts = p.split("/");

        if (parts.length < 1) {
            if (p.equals("") || p.equals("/")) {
                parts = new String[0];
            } else {
                parts = new String[] { p };
            }
        }
        return parts;
    }

    private @Nonnull Map<String, Object> parseHeaders(@Nonnull HttpServletRequest req) throws RestException {
        @SuppressWarnings("unchecked")
        Enumeration<String> names = (Enumeration<String>) req.getHeaderNames();
        HashMap<String, Object> headers = new HashMap<String, Object>();

        while (names.hasMoreElements()) {
            String name = names.nextElement();

            //noinspection unchecked
            headers.put(name, getHeader(name, req.getHeaders(name)));
        }
        return headers;
    }

    private Map<String, Object> parseParameters(@Nonnull HttpServletRequest req) throws RestException {
        // TODO: implement me
        return new HashMap<String, Object>();
    }

    private String request(HttpServletRequest req) {
        // TODO: implement request tracking
        return UUID.randomUUID().toString();
    }

}