| /* |
| * 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.jena.fuseki.server; |
| |
| import static java.lang.String.format; |
| import static org.apache.commons.lang3.StringUtils.isEmpty; |
| import static org.apache.jena.fuseki.server.Operation.*; |
| import static org.apache.jena.fuseki.server.Operation.GSP_RW; |
| import static org.apache.jena.fuseki.server.Operation.Query; |
| import static org.apache.jena.fuseki.server.Operation.Update; |
| import static org.apache.jena.fuseki.servlets.ActionExecLib.allocHttpAction; |
| |
| import java.util.Collection; |
| import java.util.List; |
| |
| import javax.servlet.http.HttpServletRequest; |
| import javax.servlet.http.HttpServletResponse; |
| |
| import org.apache.jena.atlas.lib.InternalErrorException; |
| import org.apache.jena.fuseki.Fuseki; |
| import org.apache.jena.fuseki.auth.Auth; |
| import org.apache.jena.fuseki.servlets.*; |
| import org.apache.jena.riot.web.HttpNames; |
| import org.apache.jena.web.HttpSC; |
| import org.slf4j.Logger; |
| |
| /** |
| * Dispatch on registered datasets. This is the entry point into Fuseki for dataset |
| * operations. |
| * |
| * Administration operations, and directly registered servlets and static content are |
| * called through the usual web server process. |
| * |
| * HTTP Request URLs, after servlet context removed, take the form {@code /dataset} or {@code /dataset/service}. |
| * The most general URL is {@code /context/dataset/service}. |
| * The {@link DataAccessPointRegistry} maps {@code /dataset} to a {@link DataAccessPoint}. |
| */ |
| public class Dispatcher { |
| |
| // Development debugging only. Excessive for normal operation. |
| private static final boolean LogDispatch = false; |
| private static Logger LOG = Fuseki.serverLog; |
| |
| /** |
| * Handle an HTTP request if it is sent to a registered dataset. |
| * |
| * Fuseki uses dynamic dispatch, the set of registered datasets can change while |
| * the server is running, so dispatch is driven off Fuseki system registries. |
| * |
| * If the request URL matches a registered dataset, process the request, and send |
| * the response. |
| * |
| * This function is called by {@link FusekiFilter#doFilter}. |
| * |
| * @param request |
| * HttpServletRequest |
| * @param response |
| * HttpServletResponse |
| * @return Returns {@code true} if the request has been handled, else false (no |
| * response sent). |
| */ |
| public static boolean dispatch(HttpServletRequest request, HttpServletResponse response) { |
| // Path component of the URI, without context path |
| String uri = ActionLib.actionURI(request); |
| String datasetUri = ActionLib.mapRequestToDataset(uri); |
| |
| if ( LogDispatch ) { |
| LOG.info("Filter: Request URI = " + request.getRequestURI()); |
| LOG.info("Filter: Action URI = " + uri); |
| LOG.info("Filter: Dataset URI = " + datasetUri); |
| } |
| |
| if ( datasetUri == null ) |
| return false; |
| |
| DataAccessPointRegistry registry = DataAccessPointRegistry.get(request.getServletContext()); |
| if ( !registry.isRegistered(datasetUri) ) { |
| if ( LogDispatch ) |
| LOG.debug("No dispatch for '"+datasetUri+"'"); |
| return false; |
| } |
| DataAccessPoint dap = registry.get(datasetUri); |
| return process(dap, request, response); |
| } |
| |
| /** Set up and handle a HTTP request for a dataset. */ |
| private static boolean process(DataAccessPoint dap, HttpServletRequest request, HttpServletResponse response) { |
| HttpAction action = allocHttpAction(dap, Fuseki.actionLog, request, response); |
| return dispatchAction(action); |
| } |
| |
| /** |
| * Determine and call the {@link ActionProcessor} to handle this |
| * {@link HttpAction}, including access control at the dataset and service levels. |
| */ |
| public static boolean dispatchAction(HttpAction action) { |
| return ActionExecLib.execAction(action, ()->chooseProcessor(action)); |
| } |
| |
| /** |
| * Find the ActionProcessor or return null if there can't determine one. |
| * |
| * This function sends the appropriate HTTP error response. |
| * |
| * Returning null indicates an HTTP error response, and the HTTP response has been done. |
| * |
| * Process |
| * <li> mapRequestToEndpointName -> endpoint name |
| * <li> chooseEndpoint(action, dataService, endpointName) -> Endpoint. |
| * <li> Endpoint to Operation (endpoint carries Operation). |
| * <li> target(action, operation) -> ActionProcess. |
| * |
| * @param action |
| * @return ActionProcessor or null if the request URI does not name a service or the dataset. |
| * |
| */ |
| private static ActionProcessor chooseProcessor(HttpAction action) { |
| // "return null" indicates that processing failed to find a ActionProcessor |
| DataAccessPoint dataAccessPoint = action.getDataAccessPoint(); |
| DataService dataService = action.getDataService(); |
| |
| if ( !dataService.isAcceptingRequests() ) { |
| ServletOps.error(HttpSC.SERVICE_UNAVAILABLE_503, "Dataset not currently active"); |
| return null; |
| } |
| |
| // ---- Determine Endpoint. |
| String endpointName = mapRequestToEndpointName(action, dataAccessPoint); |
| |
| Endpoint endpoint = chooseEndpoint(action, dataService, endpointName); |
| if ( endpoint == null ) { |
| if ( isEmpty(endpointName) ) |
| ServletOps.errorBadRequest("No operation for request: "+action.getActionURI()); |
| else { |
| // No dispatch - the filter passes these through if the ActionProcessor is null. |
| return null; |
| // If this is used, resources (servlets, sttaic files) under "/dataset/" are not accessible. |
| //ServletOps.errorNotFound("No endpoint: "+action.getActionURI()); |
| } |
| return null; |
| } |
| |
| Operation operation = endpoint.getOperation(); |
| if ( operation == null ) { |
| ServletOps.errorNotFound("No operation: "+action.getActionURI()); |
| return null; |
| } |
| |
| action.setEndpoint(endpoint); |
| |
| // ---- Authorization |
| // -- Server-level authorization. |
| // Checking was carried out by servlet filter AuthFilter. |
| // Need to check Data service and endpoint authorization policies. |
| String user = action.getUser(); |
| |
| // -- Data service level authorization |
| if ( dataService.authPolicy() != null ) { |
| if ( ! dataService.authPolicy().isAllowed(user) ) |
| ServletOps.errorForbidden(); |
| } |
| |
| // -- Endpoint level authorization |
| // Make sure all contribute authentication. |
| Auth.allow(user, action.getEndpoint().getAuthPolicy(), ServletOps::errorForbidden); |
| if ( isEmpty(endpointName) && ! endpoint.isUnnamed() ) { |
| // [DISPATCH LEGACY] |
| // If choice was by looking in all named endpoints for a unnamed endpoint |
| // request, ensure all choices allow access. |
| // There may be several endpoints for the operation. |
| // Authorization is the AND of all endpoints. |
| Collection<Endpoint> x = getEndpoints(dataService, operation); |
| if ( x.isEmpty() ) |
| throw new InternalErrorException("Inconsistent: no endpoints for "+operation); |
| x.forEach(ept-> |
| Auth.allow(user, ept.getAuthPolicy(), ServletOps::errorForbidden)); |
| } |
| // ---- Authorization checking. |
| |
| // ---- Handler. |
| // Decide the code to execute the request. |
| ActionProcessor processor = endpoint.getProcessor(); |
| if ( processor == null ) |
| ServletOps.errorBadRequest(format("No processor: dataset=%s: op=%s", dataAccessPoint.getName(), operation.getName())); |
| return processor; |
| } |
| |
| /** |
| * Map request to operation name. |
| * Returns the service name (the part after the "/" of the dataset part) or "". |
| */ |
| protected static String mapRequestToEndpointName(HttpAction action, DataAccessPoint dataAccessPoint) { |
| return ActionLib.mapRequestToEndpointName(action, dataAccessPoint); |
| } |
| |
| // Find the endpoints for an operation. |
| // This is GSP_R/GSP_RW aware. |
| // If asked for GSP_R and there are no endpoints for GSP_R, try GSP_RW. |
| private static Collection<Endpoint> getEndpoints(DataService dataService, Operation operation) { |
| Collection<Endpoint> x = dataService.getEndpoints(operation); |
| if ( x == null || x.isEmpty() ) { |
| if ( operation == GSP_R ) |
| x = dataService.getEndpoints(GSP_RW); |
| } |
| return x; |
| } |
| |
| /** |
| * Choose an endpoint. This can be with or without endpointName. |
| * If there is no endpoint and the action is on the data service itself (unnamed endpoint) |
| * look for a named endpoint that supplies the operation. |
| */ |
| private static Endpoint chooseEndpoint(HttpAction action, DataService dataService, String endpointName) { |
| Endpoint ep = chooseEndpointNoLegacy(action, dataService, endpointName); |
| if ( ep != null ) |
| return ep; |
| // No dispatch so far. |
| |
| if ( ! isEmpty(endpointName) ) |
| return ep; |
| // [DISPATCH LEGACY] |
| |
| // When it is a unnamed service request (operation on the dataset) and there |
| // is no match, search the named services. |
| Operation operation = chooseOperation(action); |
| // Search for an endpoint that provides the operation. |
| // No guarantee it has the access controls for the operation |
| // but in this case, access control will validate against all possible endpoints. |
| ep = findEndpointForOperation(action, dataService, operation, true); |
| return ep; |
| } |
| |
| /** |
| * Choose an endpoint. |
| * <ul> |
| * <li>Look by service name to get the EndpointSet</li> |
| * <li>If empty set, return null.</li> |
| * <li>If there is only one choice, return that (may even be the wrong operation |
| * - processor implmentations must be defensive).</li> |
| * <li>If multiple choices, classify the operation |
| * (includes custom content-type) and look up by operation.</li> |
| * <li>Return a match wit a r |
| * </ul> |
| */ |
| private static Endpoint chooseEndpointNoLegacy(HttpAction action, DataService dataService, String endpointName) { |
| EndpointSet epSet = isEmpty(endpointName) ? dataService.getEndpointSet() : dataService.getEndpointSet(endpointName); |
| |
| if ( epSet == null || epSet.isEmpty() ) |
| // No matches by name. |
| return null; |
| |
| // If there is one endpoint, dispatch there directly. |
| Endpoint ep = epSet.getOnly(); |
| if ( ep != null ) |
| return ep; |
| // No single direct dispatch. Multiple choices (different operation, same endpoint name) |
| // Work out which operation we are looking for. |
| Operation operation = chooseOperation(action); |
| ep = epSet.get(operation); |
| // This also happens in findEndpointForOperation |
| // If a GSP-R request, try for GSP-RW |
| if ( ep == null && Operation.GSP_R.equals(operation) ) |
| ep = epSet.get(Operation.GSP_RW); |
| return ep; |
| } |
| |
| /** |
| * Find an endpoint for an operation. |
| * This searches all endpoints of a {@link DataService} that provide the {@link Operation}. |
| * This understands that GSP_RW can service GSP_R. |
| * Used for legacy dispatch. |
| */ |
| private static Endpoint findEndpointForOperation(HttpAction action, DataService dataService, Operation operation, boolean preferUnnamed) { |
| Endpoint ep = findEndpointForOperationExact(dataService, operation, preferUnnamed); |
| if ( ep != null ) |
| return ep; |
| // Try to find "R" functionality from an RW. |
| if ( GSP_R.equals(operation) ) |
| return findEndpointForOperationExact(dataService, GSP_RW, preferUnnamed); |
| // Instead of 404, return 405 if asked for RW but only R available. |
| if ( GSP_RW.equals(operation) && dataService.hasOperation(GSP_R) ) |
| ServletOps.errorMethodNotAllowed(action.getMethod()); |
| return null; |
| } |
| |
| /** Find a matching endpoint for exactly this operation. |
| * If multiple choices, prefer either named or unnamed according |
| * to the flag {@code preferUnnamed}. |
| */ |
| private static Endpoint findEndpointForOperationExact(DataService dataService, Operation operation, boolean preferUnnamed) { |
| List<Endpoint> eps = dataService.getEndpoints(operation); |
| if ( eps == null || eps.isEmpty() ) |
| return null; |
| // ==== Legacy compatibility. |
| // Find a named service, with preference for named/unnamed. |
| Endpoint epAlt = null; |
| for ( Endpoint ep : eps ) { |
| if ( operation.equals(ep.getOperation()) ) { |
| if ( ep.isUnnamed() && preferUnnamed ) |
| return ep; |
| if ( ! ep.isUnnamed() && ! preferUnnamed ) |
| return ep; |
| epAlt = ep; |
| } |
| } |
| // Did not find a preferred one. |
| return epAlt; |
| } |
| |
| /** |
| * Identify the operation being requested. |
| * It is analysing the HTTP request using global configuration. |
| * The decision is based on |
| * <ul> |
| * <li>Query parameters (URL query string or HTML form)</li> |
| * <li>Content-Type header</li> |
| * <li>Otherwise it is a plain REST (quads) operation.chooseOperation</li> |
| * </ul> |
| * The HTTP Method is not considered. |
| * <p> |
| * The operation is not guaranteed to be supported on every {@link DataService} |
| * nor that access control will allow it to be performed. |
| */ |
| public static Operation chooseOperation(HttpAction action) { |
| HttpServletRequest request = action.getRequest(); |
| |
| // ---- Dispatch based on HttpParams : Query, Update, GSP. |
| // -- Query |
| boolean isQuery = request.getParameter(HttpNames.paramQuery) != null; |
| if ( isQuery ) |
| return Query; |
| |
| // -- Update |
| // Standards name "update", non-standard name "request" (old use by Fuseki) |
| boolean isUpdate = request.getParameter(HttpNames.paramUpdate) != null || request.getParameter(HttpNames.paramRequest) != null; |
| if ( isUpdate ) |
| // The SPARQL_Update servlet will deal with using GET. |
| return Update; |
| |
| // ---- Content-type |
| // This does not have the ";charset=" |
| String ct = request.getContentType(); |
| if ( ct != null ) { |
| Operation operation = action.getOperationRegistry().findByContentType(ct); |
| if ( operation != null ) |
| return operation; |
| } |
| // We don't wire in all the RDF syntaxes. |
| // Instead, "Quads" drops through to the default operation. |
| |
| // -- SPARQL Graph Store Protocol |
| boolean hasParamGraph = request.getParameter(HttpNames.paramGraph) != null; |
| boolean hasParamGraphDefault = request.getParameter(HttpNames.paramGraphDefault) != null; |
| if ( hasParamGraph || hasParamGraphDefault ) |
| return gspOperation(action, request); |
| |
| // -- Any other queryString |
| // Query string now unexpected. |
| // Place for an extension point. |
| boolean hasParams = request.getParameterMap().size() > 0; |
| if ( hasParams ) { |
| // Unrecognized ?key=value |
| ServletOps.errorBadRequest("Malformed request: unrecognized query string parameters: " + request.getQueryString()); |
| } |
| |
| |
| // ---- No registered content type, no query parameters. |
| // Plain HTTP operation on the dataset handled as quads or rejected. |
| return quadsOperation(action, request); |
| } |
| |
| /** |
| * Determine the {@link Operation} for a SPARQL Graph Store Protocol (GSP) action. |
| * <p> |
| * Assumes, and does not check, that the action is a GSP action. |
| * |
| * @throws ActionErrorException |
| * (which causes a servlet 4xx response) if the operaton is not permitted. |
| */ |
| private static Operation gspOperation(HttpAction action, HttpServletRequest request) throws ActionErrorException { |
| // Check enabled. |
| if ( isReadMethod(request) ) |
| return GSP_R; |
| else |
| return GSP_RW; |
| } |
| |
| /** |
| * Determine the {@link Operation} for a Quads operation. (GSP, except on the |
| * whole dataset). |
| * <p> |
| * Assumes, and does not check, that the action is a Quads action. |
| * |
| * @throws ActionErrorException |
| * (which causes a servlet 405 response) if the operaton is not permitted. |
| */ |
| private static Operation quadsOperation(HttpAction action, HttpServletRequest request) throws ActionErrorException { |
| // Check enabled. Extends GSP. |
| if ( isReadMethod(request) ) |
| return GSP_R; |
| else |
| return GSP_RW; |
| } |
| |
| private static boolean isReadMethod(HttpServletRequest request) { |
| String method = request.getMethod(); |
| // REST dataset. |
| boolean isGET = method.equals(HttpNames.METHOD_GET); |
| boolean isHEAD = method.equals(HttpNames.METHOD_HEAD); |
| return isGET || isHEAD; |
| } |
| } |