/* * Copyright © 2014-2016 Cask Data, Inc. * * 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 co.cask.cdap.gateway.router; import co.cask.cdap.common.conf.Constants; import co.cask.http.AbstractHttpHandler; import org.apache.commons.lang.StringUtils; import org.jboss.netty.handler.codec.http.HttpRequest; /** * Class to match the request path to corresponding service like app-fabric, or metrics service. */ public final class RouterPathLookup extends AbstractHttpHandler { @SuppressWarnings("unused") private enum AllowedMethod { GET, PUT, POST, DELETE } /** * Returns the CDAP service which will handle the HttpRequest * * @param fallbackService service to which we fall back to if we can't determine the destination from the URI path * @param requestPath Normalized (and query string removed) URI path * @param httpRequest HttpRequest used to get the Http method and account id * @return destination service */ public String getRoutingService(String fallbackService, String requestPath, HttpRequest httpRequest) { try { String method = httpRequest.getMethod().getName(); AllowedMethod requestMethod = AllowedMethod.valueOf(method); String[] uriParts = StringUtils.split(requestPath, '/'); //Check if the call should go to webapp //If service contains "$HOST" and if first split element is NOT the gateway version, then send it to WebApp //WebApp serves only static files (HTML, CSS, JS) and so /<appname> calls should go to WebApp //But stream calls issued by the UI should be routed to the appropriate CDAP service if (fallbackService.contains("$HOST") && (uriParts.length >= 1) && !("/" + uriParts[0]).equals(Constants.Gateway.API_VERSION_3)) { return fallbackService; } if (uriParts[0].equals(Constants.Gateway.API_VERSION_3_TOKEN)) { return getV3RoutingService(uriParts, requestMethod); } } catch (Exception e) { // Ignore exception. Default routing to app-fabric. } return Constants.Service.APP_FABRIC_HTTP; } private String getV3RoutingService(String [] uriParts, AllowedMethod requestMethod) { if ((uriParts.length >= 2) && uriParts[1].equals("feeds")) { // TODO find a better way to handle that - this looks hackish return null; } else if ((uriParts.length >= 9) && "services".equals(uriParts[5]) && "methods".equals(uriParts[7])) { //User defined services handle methods on them: //Path: "/v3/namespaces/{namespace-id}/apps/{app-id}/services/{service-id}/methods/<user-defined-method-path>" //Discoverable Service Name -> "service.%s.%s.%s", namespaceId, appId, serviceId return String.format("service.%s.%s.%s", uriParts[2], uriParts[4], uriParts[6]); } else if (matches(uriParts, "v3", "system", "services", null, "logs")) { //Log Handler Path /v3/system/services/<service-id>/logs return Constants.Service.METRICS; } else if (matches(uriParts, "v3", "namespaces", null, "apps", null, "metadata") || matches(uriParts, "v3", "namespaces", null, "apps", null, null, null, "metadata") || matches(uriParts, "v3", "namespaces", null, "artifacts", null, "versions", null, "metadata") || matches(uriParts, "v3", "namespaces", null, "datasets", null, "metadata") || matches(uriParts, "v3", "namespaces", null, "streams", null, "metadata") || matches(uriParts, "v3", "namespaces", null, "streams", null, "views", null, "metadata") || matches(uriParts, "v3", "namespaces", null, "apps", null, "metadata", "properties") || matches(uriParts, "v3", "namespaces", null, "artifacts", null, "versions", null, "metadata", "properties") || matches(uriParts, "v3", "namespaces", null, "apps", null, null, null, "metadata", "properties") || matches(uriParts, "v3", "namespaces", null, "datasets", null, "metadata", "properties") || matches(uriParts, "v3", "namespaces", null, "streams", null, "metadata", "properties") || matches(uriParts, "v3", "namespaces", null, "streams", null, "views", null, "metadata", "properties") || matches(uriParts, "v3", "namespaces", null, "apps", null, "metadata", "tags") || matches(uriParts, "v3", "namespaces", null, "artifacts", null, "versions", null, "metadata", "tags") || matches(uriParts, "v3", "namespaces", null, "apps", null, null, null, "metadata", "tags") || matches(uriParts, "v3", "namespaces", null, "datasets", null, "metadata", "tags") || matches(uriParts, "v3", "namespaces", null, "streams", null, "metadata", "tags") || matches(uriParts, "v3", "namespaces", null, "streams", null, "views", null, "metadata", "tags") || matches(uriParts, "v3", "namespaces", null, "metadata", "search") || matches(uriParts, "v3", "namespaces", null, "datasets", null, "lineage") || matches(uriParts, "v3", "namespaces", null, "streams", null, "lineage") || matches(uriParts, "v3", "namespaces", null, "apps", null, null, null, "runs", null, "metadata")) { return Constants.Service.METADATA_SERVICE; } else if (matches(uriParts, "v3", "security", "authorization")) { // Authorization Handler currently runs in App Fabric return Constants.Service.APP_FABRIC_HTTP; } else if ((matches(uriParts, "v3", "namespaces", null, "streams", null, "programs") || matches(uriParts, "v3", "namespaces", null, "data", "datasets", null, "programs")) && requestMethod.equals(AllowedMethod.GET)) { return Constants.Service.APP_FABRIC_HTTP; } else if ((uriParts.length >= 4) && uriParts[1].equals("namespaces") && uriParts[3].equals("streams")) { // /v3/namespaces/<namespace>/streams goes to AppFabricHttp // All else go to Stream Handler if (uriParts.length == 4) { return Constants.Service.APP_FABRIC_HTTP; } else { return Constants.Service.STREAMS; } } else if ((uriParts.length >= 8 && uriParts[7].equals("logs")) || (uriParts.length >= 10 && uriParts[9].equals("logs")) || (uriParts.length >= 6 && uriParts[5].equals("logs"))) { //Log Handler Paths: // /v3/namespaces/<namespaceid>/apps/<appid>/<programid-type>/<programid>/logs // /v3/namespaces/{namespace-id}/apps/{app-id}/{program-type}/{program-id}/runs/{run-id}/logs return Constants.Service.METRICS; } else if (uriParts.length >= 2 && uriParts[1].equals("metrics")) { //Metrics Search Handler Path /v3/metrics return Constants.Service.METRICS; } else if (uriParts.length >= 5 && uriParts[1].equals("data") && uriParts[2].equals("explore") && (uriParts[3].equals("queries") || uriParts[3].equals("jdbc") || uriParts[3].equals("namespaces"))) { // non-namespaced explore operations. For example, /v3/data/explore/queries/{id} return Constants.Service.EXPLORE_HTTP_USER_SERVICE; } else if (uriParts.length >= 6 && uriParts[3].equals("data") && uriParts[4].equals("explore") && (uriParts[5].equals("queries") || uriParts[5].equals("streams") || uriParts[5].equals("datasets") || uriParts[5].equals("tables") || uriParts[5].equals("jdbc"))) { // namespaced explore operations. For example, /v3/namespaces/{namespace-id}/data/explore/streams/{stream}/enable return Constants.Service.EXPLORE_HTTP_USER_SERVICE; } else if ((uriParts.length == 3) && uriParts[1].equals("explore") && uriParts[2].equals("status")) { return Constants.Service.EXPLORE_HTTP_USER_SERVICE; } else if (uriParts.length == 7 && uriParts[3].equals("data") && uriParts[4].equals("datasets") && (uriParts[6].equals("flows") || uriParts[6].equals("workers") || uriParts[6].equals("mapreduce"))) { // namespaced app fabric data operations: // /v3/namespaces/{namespace-id}/data/datasets/{name}/flows // /v3/namespaces/{namespace-id}/data/datasets/{name}/workers // /v3/namespaces/{namespace-id}/data/datasets/{name}/mapreduce return Constants.Service.APP_FABRIC_HTTP; } else if ((uriParts.length >= 4) && uriParts[3].equals("data")) { // other data operations. For example: // /v3/namespaces/{namespace-id}/data/datasets // /v3/namespaces/{namespace-id}/data/datasets/{name} // /v3/namespaces/{namespace-id}/data/datasets/{name}/properties // /v3/namespaces/{namespace-id}/data/datasets/{name}/admin/{method} return Constants.Service.DATASET_MANAGER; } return Constants.Service.APP_FABRIC_HTTP; } /** * Determines if actual matches expected. * * - actual may be longer than expected, but we'll return true as long as expected was found * - null in expected means "accept any string" * * @param actual actual string array to check * @param expected expected string array format * @return true if actual matches expected */ private boolean matches(String[] actual, String... expected) { if (actual.length < expected.length) { return false; } for (int i = 0; i < expected.length; i++) { if (expected[i] == null) { continue; } if (!expected[i].equals(actual[i])) { return false; } } return true; } }