| /******************************************************************************* |
| * Copyright (c) 2013-2020 LAAS-CNRS (www.laas.fr) |
| * 7 Colonel Roche 31077 Toulouse - France |
| * |
| * All rights reserved. This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License 2.0 |
| * which accompanies this distribution, and is available at |
| * https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * Initial Contributors: |
| * Thierry Monteil : Project manager, technical co-manager |
| * Mahdi Ben Alaya : Technical co-manager |
| * Samir Medjiah : Technical co-manager |
| * Khalil Drira : Strategy expert |
| * Guillaume Garzone : Developer |
| * François Aïssaoui : Developer |
| * |
| * New contributors : |
| *******************************************************************************/ |
| package org.eclipse.om2m.binding.http; |
| |
| import java.io.IOException; |
| import java.io.PrintWriter; |
| import java.math.BigInteger; |
| import java.nio.charset.Charset; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Base64; |
| import java.util.Enumeration; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| |
| import javax.servlet.ServletException; |
| import javax.servlet.http.HttpServlet; |
| import javax.servlet.http.HttpServletRequest; |
| import javax.servlet.http.HttpServletResponse; |
| import javax.xml.datatype.DatatypeConfigurationException; |
| import javax.xml.datatype.DatatypeFactory; |
| import javax.xml.datatype.Duration; |
| |
| import org.apache.commons.logging.Log; |
| import org.apache.commons.logging.LogFactory; |
| import org.eclipse.om2m.binding.http.constants.HttpHeaders; |
| import org.eclipse.om2m.binding.http.constants.HttpParameters; |
| import org.eclipse.om2m.commons.constants.MimeMediaType; |
| import org.eclipse.om2m.commons.constants.Operation; |
| import org.eclipse.om2m.commons.constants.ResponseStatusCode; |
| import org.eclipse.om2m.commons.exceptions.BadRequestException; |
| import org.eclipse.om2m.commons.resource.FilterCriteria; |
| import org.eclipse.om2m.commons.resource.RequestPrimitive; |
| import org.eclipse.om2m.commons.resource.ResponsePrimitive; |
| import org.eclipse.om2m.commons.resource.ResponseTypeInfo; |
| import org.eclipse.om2m.commons.resource.StatusCode; |
| import org.eclipse.om2m.commons.utils.Util; |
| import org.eclipse.om2m.core.service.CseService; |
| |
| /** |
| * Provides mapping from a HTTP-specific request to a protocol-independent request. |
| */ |
| public class RestHttpServlet extends HttpServlet { |
| /** Logger */ |
| private static Log LOGGER = LogFactory.getLog(RestHttpServlet.class); |
| /** Serial Version UID */ |
| private static final long serialVersionUID = 1L; |
| /** Bearer Auth Type */ |
| private static final String BEARER_AUTH_TYPE = "Bearer"; |
| /** Basic Auth Type */ |
| private static final String BASIC_AUTH_TYPE = "Basic"; |
| /** Authorization header */ |
| private static final String AUTHORIZATION_HEADER = "Authorization"; |
| /** Discovered CSE service */ |
| private static CseService cse; |
| |
| protected String CSE_BASE_CONTEXT = ""; |
| |
| /** |
| * Converts a {@link HttpServletRequest} to a {@link RequestIndication} and uses it to invoke the SCL service. |
| * Converts the received {@link ResponseConfirm} to a {@link HttpServletResponse} and returns it back. |
| */ |
| @Override |
| protected void service(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws ServletException, IOException { |
| LOGGER.info("----------------------------------------------------------------------------------------------"); |
| // Create generic request |
| RequestPrimitive request = new RequestPrimitive(); |
| |
| // Get the URI and split it to retrieve targetID |
| String uri = httpServletRequest.getRequestURI(); |
| |
| // Remove the context (ex: '/api') |
| String targetID = uri ; |
| if (CSE_BASE_CONTEXT.length() > 1 && uri.length() > CSE_BASE_CONTEXT.length()){ |
| targetID = uri.substring(CSE_BASE_CONTEXT.length()); |
| } |
| |
| if(targetID.startsWith("/~")){ |
| targetID = targetID.replaceFirst("/~/", "/"); |
| } else if(targetID.startsWith("/_")){ |
| targetID = targetID.replaceFirst("/_/", "//"); |
| } else if(targetID.startsWith("/")){ |
| targetID = targetID.replaceFirst("/", ""); |
| } |
| |
| // Set the To parameter of the primitive request |
| request.setTo(targetID); |
| // Get the body of the request |
| String content = null; |
| try { |
| if(httpServletRequest.getInputStream().available() > 0){ |
| content = Util.convertStreamToString(httpServletRequest.getInputStream()); |
| } |
| } catch (IOException e) { |
| LOGGER.error("Error reading httpServletRequest InputStream",e); |
| } |
| if (content != null && !content.isEmpty()){ |
| request.setContent(content); |
| } |
| |
| // Get request parameters |
| mapParameters(httpServletRequest, request); |
| |
| // Get the oneM2M operation |
| LOGGER.info("Built RequestPrimitive: " + request.toString()); |
| try{ |
| request.setOperation(getOneM2MOperation(httpServletRequest)); |
| } catch (BadRequestException e){ |
| httpServletResponse.setStatus(400); |
| httpServletResponse.getWriter().print(e.getMessage()); |
| httpServletResponse.setContentType(MimeMediaType.TEXT_PLAIN); |
| return; |
| } |
| |
| mapHeaders(httpServletRequest, request); |
| |
| request.getQueryStrings().putAll(getParamsFromQuery(httpServletRequest.getQueryString())); |
| |
| // Perform the request in the CSE |
| ResponsePrimitive response = null ; |
| if (cse != null){ |
| response = cse.doRequest(request); |
| } else { |
| response = new ResponsePrimitive(); |
| response.setResponseStatusCode(ResponseStatusCode.SERVICE_UNAVAILABLE); |
| response.setContent("CSE service is not available."); |
| response.setContentType(MimeMediaType.TEXT_PLAIN); |
| } |
| |
| // Set location header |
| if (response.getLocation() != null){ |
| httpServletResponse.setHeader(HttpHeaders.CONTENT_LOCATION, response.getLocation()); |
| } |
| // Set the request identifier header |
| if (response.getRequestIdentifier() != null){ |
| httpServletResponse.setHeader(HttpHeaders.REQUEST_IDENTIFIER, response.getRequestIdentifier()); |
| } |
| // Set the Origin header |
| if (response.getFrom() != null){ |
| httpServletResponse.setHeader(HttpHeaders.ORIGINATOR, response.getFrom()); |
| } |
| |
| // Set HTTP status code |
| int httpStatus = getHttpStatusCode(response.getResponseStatusCode()); |
| httpServletResponse.setStatus(httpStatus); |
| httpServletResponse.setHeader( |
| HttpHeaders.RESPONSE_STATUS_CODE, response.getResponseStatusCode().toString()); |
| |
| // Set the message body |
| if (response.getContent() != null){ |
| // Set the Content-Type header |
| httpServletResponse.setCharacterEncoding("UTF-8"); |
| httpServletResponse.setContentType(response.getContentType()); |
| |
| String body = (String) response.getContent(); |
| LOGGER.info("send UTF8"); |
| httpServletResponse.getOutputStream().write(body.getBytes(Charset.forName("UTF-8"))); |
| } |
| |
| httpServletResponse.getOutputStream().close(); |
| } |
| |
| /** |
| * Return the oneM2M method from http request informations. |
| * @param httpServletRequest |
| * @return |
| */ |
| private static BigInteger getOneM2MOperation(HttpServletRequest httpServletRequest) { |
| BigInteger result = null ; |
| switch(httpServletRequest.getMethod()){ |
| case "GET": |
| return Operation.RETRIEVE; |
| case "POST": |
| if (httpServletRequest.getHeader(HttpHeaders.CONTENT_TYPE) != null && |
| httpServletRequest.getHeader(HttpHeaders.CONTENT_TYPE).contains("ty=")){ |
| result = Operation.CREATE; |
| } else { |
| result = Operation.NOTIFY; |
| } |
| return result; |
| case "PUT": |
| return Operation.UPDATE; |
| case "DELETE": |
| return Operation.DELETE; |
| default: |
| throw new BadRequestException("HTTP Operation not supported: " + httpServletRequest.getMethod()); |
| } |
| } |
| |
| /** |
| * Converts a standard HTTP status code into a {@link StatusCode} object. |
| * @param statusCode - protocol-independent status code. |
| * @param isEmptyBody - request body existence |
| * @return standard HTTP status code. |
| */ |
| public static int getHttpStatusCode(BigInteger statusCode){ |
| if (statusCode.equals(ResponseStatusCode.OK) |
| || statusCode.equals(ResponseStatusCode.UPDATED) |
| || statusCode.equals(ResponseStatusCode.DELETED)){ |
| return 200; |
| } else if (statusCode.equals(ResponseStatusCode.CREATED)){ |
| return 201; |
| } else if (statusCode.equals(ResponseStatusCode.ACCEPTED)){ |
| return 202 ; |
| } else if (statusCode.equals(ResponseStatusCode.BAD_REQUEST) |
| || statusCode.equals(ResponseStatusCode.CONTENTS_UNACCEPTABLE) |
| || statusCode.equals(ResponseStatusCode.MAX_NUMBER_OF_MEMBER_EXCEEDED) |
| || statusCode.equals(ResponseStatusCode.MEMBER_TYPE_INCONSISTENT) |
| || statusCode.equals(ResponseStatusCode.INVALID_CMDTYPE) |
| || statusCode.equals(ResponseStatusCode.INSUFFICIENT_ARGUMENTS) |
| || statusCode.equals(ResponseStatusCode.ALREADY_COMPLETED) |
| || statusCode.equals(ResponseStatusCode.COMMAND_NOT_CANCELLABLE)){ |
| return 400; |
| } else if (statusCode.equals(ResponseStatusCode.ACCESS_DENIED) |
| || statusCode.equals(ResponseStatusCode.SUBSCRIPTION_CREATOR_HAS_NO_PRIVILEGE) |
| || statusCode.equals(ResponseStatusCode.NO_PRIVILEGE) |
| || statusCode.equals(ResponseStatusCode.ALREADY_EXISTS) |
| || statusCode.equals(ResponseStatusCode.TARGET_NOT_SUBSCRIBABLE) |
| || statusCode.equals(ResponseStatusCode.SUBSCRIPTION_HOST_HAS_NO_PRIVILEGE)){ |
| return 403; |
| } else if (statusCode.equals(ResponseStatusCode.NOT_FOUND) |
| || statusCode.equals(ResponseStatusCode.TARGET_NOT_REACHABLE) |
| || statusCode.equals(ResponseStatusCode.EXTERNAL_OBJECT_NOT_FOUND) |
| || statusCode.equals(ResponseStatusCode.EXTERNAL_OBJECT_NOT_REACHABLE)){ |
| return 404; |
| } else if (statusCode.equals(ResponseStatusCode.OPERATION_NOT_ALLOWED)){ |
| return 405; |
| } else if (statusCode.equals(ResponseStatusCode.REQUEST_TIMEOUT)){ |
| return 408; |
| } else if (statusCode.equals(ResponseStatusCode.CONFLICT) |
| || statusCode.equals(ResponseStatusCode.GROUP_REQUEST_IDENTIFIER_EXISTS)){ |
| return 409; |
| } else if (statusCode.equals(ResponseStatusCode.INTERNAL_SERVER_ERROR) |
| || statusCode.equals(ResponseStatusCode.SUBSCRIPTION_VERIFICATION_INITIATION_FAILED) |
| || statusCode.equals(ResponseStatusCode.MGMT_SESSION_CANNOT_BE_ESTABLISHED) |
| || statusCode.equals(ResponseStatusCode.MGMT_SESSION_ESTABLISHMENT_TIMEOUT) |
| || statusCode.equals(ResponseStatusCode.MGMT_CONVERSION_ERROR) |
| || statusCode.equals(ResponseStatusCode.MGMT_CANCELATION_FAILURE)){ |
| return 500; |
| } else if (statusCode.equals(ResponseStatusCode.NOT_IMPLEMENTED) |
| || statusCode.equals(ResponseStatusCode.NON_BLOCKING_REQUEST_NOT_SUPPORTED)){ |
| return 501; |
| } else if (statusCode.equals(ResponseStatusCode.SERVICE_UNAVAILABLE)){ |
| return 503; |
| } |
| return 501; |
| } |
| |
| /** |
| * Get the registered cse |
| * @return registered cse |
| */ |
| public static CseService getScl() { |
| return cse; |
| } |
| |
| /** |
| * Set the registered cse |
| * @param cse to register |
| */ |
| public static void setCse(CseService cse) { |
| RestHttpServlet.cse = cse; |
| } |
| |
| /** |
| * Method used to map uri parameters to generic oneM2M request primitive. |
| * @param request http request |
| * @param primitive oneM2M generic request |
| */ |
| private void mapParameters(HttpServletRequest request, RequestPrimitive primitive){ |
| if (request.getParameter(HttpParameters.RESPONSE_TYPE) != null){ |
| if(primitive.getResponseTypeInfo() == null){ |
| primitive.setResponseTypeInfo(new ResponseTypeInfo()); |
| } |
| primitive.getResponseTypeInfo().setResponseType(new BigInteger(request.getParameter(HttpParameters.RESPONSE_TYPE)));; |
| } |
| if(request.getParameter(HttpParameters.RESULT_CONTENT) != null){ |
| primitive.setResultContent(new BigInteger(request.getParameter(HttpParameters.RESULT_CONTENT))); |
| } |
| if (request.getParameter(HttpParameters.RESULT_PERSISTENCE) != null){ |
| try { |
| Duration duration = DatatypeFactory.newInstance(). |
| newDuration(request.getParameter(HttpParameters.RESULT_PERSISTENCE)); |
| primitive.setResultPersistence(duration); |
| } catch (DatatypeConfigurationException e) { |
| LOGGER.debug("Error in Duration creation",e); |
| } |
| } |
| if (request.getParameter(HttpParameters.DELIVERY_AGGREGATION) != null){ |
| primitive.setDeliveryAggregation(Boolean.parseBoolean(request.getParameter(HttpParameters.DELIVERY_AGGREGATION))); |
| } |
| |
| if (request.getParameter(HttpParameters.DISCOVERY_RESULT_TYPE) != null){ |
| primitive.setDiscoveryResultType(new BigInteger(request.getParameter(HttpParameters.DISCOVERY_RESULT_TYPE))); |
| } |
| |
| // create filter criteria |
| FilterCriteria filterCriteria = new FilterCriteria(); |
| primitive.setFilterCriteria(filterCriteria); |
| if (request.getParameter(HttpParameters.LEVEL) != null) { |
| filterCriteria.setLevel(new BigInteger(request.getParameter(HttpParameters.LEVEL))); |
| } |
| if (request.getParameter(HttpParameters.OFFSET) != null) { |
| filterCriteria.setOffset(new BigInteger(request.getParameter(HttpParameters.OFFSET))); |
| } |
| if(request.getParameter(HttpParameters.FILTER_USAGE) != null){ |
| filterCriteria.setFilterUsage(new BigInteger(request.getParameter(HttpParameters.FILTER_USAGE))); |
| } |
| if(request.getParameter(HttpParameters.LIMIT) != null){ |
| filterCriteria.setLimit(new BigInteger(request.getParameter(HttpParameters.LIMIT))); |
| } |
| if(request.getParameter(HttpParameters.LABELS) != null){ |
| filterCriteria.getLabels().addAll(Arrays.asList(request.getParameterValues(HttpParameters.LABELS))); |
| } |
| if(request.getParameter(HttpParameters.RESOURCE_TYPE) != null){ |
| filterCriteria.setResourceType(new BigInteger(request.getParameter(HttpParameters.RESOURCE_TYPE))); |
| } |
| } |
| |
| private void mapHeaders(HttpServletRequest httpServletRequest, |
| RequestPrimitive request) { |
| |
| String contentFormat = System.getProperty("org.eclipse.om2m.registration.contentFormat", MimeMediaType.XML); |
| |
| // Get the originator from the X-M2M-Origin header |
| |
| String originator = httpServletRequest.getHeader(HttpHeaders.ORIGINATOR); |
| if (originator != null) { |
| request.setFrom(originator); |
| } |
| |
| // authorization |
| parseAuthorizationHeader(request, httpServletRequest.getHeader(AUTHORIZATION_HEADER)); |
| |
| // Get the request identifier |
| String requestIdentifier = httpServletRequest.getHeader(HttpHeaders.REQUEST_IDENTIFIER) ; |
| if (requestIdentifier != null){ |
| request.setRequestIdentifier(requestIdentifier.trim()); |
| } |
| |
| // Get Request Content type |
| String contentTypeHeaders = httpServletRequest.getHeader(HttpHeaders.CONTENT_TYPE); |
| if (contentTypeHeaders != null){ |
| LOGGER.info("Content type headers: " + contentTypeHeaders); |
| for (String contentTypeHeader : contentTypeHeaders.split(";")){ |
| LOGGER.info("Header value: "+ contentTypeHeader); |
| if(contentTypeHeader.trim().startsWith("ty=")){ |
| String resourceType = contentTypeHeader.trim().split("ty=")[1]; |
| request.setResourceType(new BigInteger(resourceType)); |
| } |
| switch(contentTypeHeader.trim()){ |
| case MimeMediaType.XML: case MimeMediaType.XML_RESOURCE : |
| request.setRequestContentType(MimeMediaType.XML); |
| break; |
| case MimeMediaType.JSON: case MimeMediaType.JSON_RESOURCE: |
| request.setRequestContentType(MimeMediaType.JSON); |
| break; |
| default: |
| break; |
| } |
| } |
| } else{ |
| request.setRequestContentType(contentFormat); |
| } |
| |
| if(request.getRequestContentType() == null){ |
| request.setRequestContentType(contentFormat); |
| } |
| |
| // Get dataTypes |
| String acceptHeaders = httpServletRequest.getHeader(HttpHeaders.ACCEPT); |
| LOGGER.info("Accept header: " + acceptHeaders); |
| if (acceptHeaders != null){ |
| for (String acceptHeader : acceptHeaders.split(";")){ |
| switch(acceptHeader.trim()){ |
| case MimeMediaType.XML: case MimeMediaType.XML_RESOURCE: |
| request.setReturnContentType(MimeMediaType.XML); |
| break; |
| case MimeMediaType.JSON: case MimeMediaType.JSON_RESOURCE: |
| request.setReturnContentType(MimeMediaType.JSON); |
| break; |
| default: |
| break; |
| } |
| } |
| } else { |
| request.setReturnContentType(request.getRequestContentType()); |
| } |
| |
| if(request.getReturnContentType() == null){ |
| request.setReturnContentType(request.getRequestContentType()); |
| } |
| |
| // Map Response Type Uri for non-blocking request |
| String rtuHeader = httpServletRequest.getHeader(HttpHeaders.RESPONSE_TYPE); |
| if(rtuHeader != null){ |
| LOGGER.info("Response Type URI header: " + rtuHeader); |
| if(request.getResponseTypeInfo() == null){ |
| request.setResponseTypeInfo(new ResponseTypeInfo()); |
| } |
| for(String notifUri : rtuHeader.split("&")){ |
| request.getResponseTypeInfo().getNotificationURI().add(notifUri.trim()); |
| } |
| } |
| |
| } |
| |
| private void parseAuthorizationHeader(RequestPrimitive request, String authorizationHeader) { |
| LOGGER.info("parseAuthorizationHeader(authorizationHeader:" + authorizationHeader + ")" ); |
| if ((authorizationHeader == null) || (authorizationHeader.isEmpty())) { |
| return; |
| } |
| |
| if (authorizationHeader.startsWith(BASIC_AUTH_TYPE)) { |
| // basic type |
| LOGGER.debug("request with basic http auth"); |
| |
| String base64UsernameAndPassword = authorizationHeader.substring(BASIC_AUTH_TYPE.length() + 1).trim(); |
| String usernameAndPassword = new String(Base64.getDecoder().decode(base64UsernameAndPassword)); |
| |
| request.setFrom(usernameAndPassword); |
| } |
| |
| if (authorizationHeader.startsWith(BEARER_AUTH_TYPE)) { |
| // bearer type |
| String accessToken = authorizationHeader.substring(BEARER_AUTH_TYPE.length() + 1); |
| |
| request.getTokens().add(accessToken); |
| } |
| |
| } |
| |
| public static String httpRequestToString(HttpServletRequest request, String content){ |
| String heads = "{\n"; |
| Enumeration<String> headerNames = request.getHeaderNames(); |
| |
| while (headerNames.hasMoreElements()) { |
| |
| String headerName = headerNames.nextElement(); |
| heads += "\t" + (headerName) + ": "; |
| |
| Enumeration<String> headers = request.getHeaders(headerName); |
| while (headers.hasMoreElements()) { |
| String headerValue = headers.nextElement(); |
| heads += headerValue + "\n"; |
| } |
| } |
| heads += "}"; |
| return "HttpRequest [method=" + request.getMethod() + ", URI=" + request.getRequestURI() + ", representation=" + content + |
| ", queryString=" + request.getQueryString() + ", headers :\n" + heads + "]"; |
| } |
| |
| public static String httpResponseToString(int statusCode, String errorMessage){ |
| return "HttpResponse [statusCode=" + statusCode + ", errorMessage="+ errorMessage +"]"; |
| } |
| |
| /** |
| * Converts a standard HTTP query String into a protocol independent parameters map. |
| * @param query - standard HTTP query String. |
| * @return protocol independent parameters map. |
| */ |
| public static Map<String, List<String>> getParamsFromQuery(String query){ |
| Map<String, List<String>> parameters = new HashMap<String, List<String>>(); |
| if (query != null) { |
| String[] pairs = query.split("[&]"); |
| for (String pair : pairs) { |
| String[] param = pair.split("[=]"); |
| |
| String key = null; |
| String value = null; |
| if (param.length > 0) { |
| key = param[0]; |
| } |
| if (param.length > 1) { |
| value = param[1]; |
| } |
| if (parameters.containsKey(key)) { |
| parameters.get(key).add(value); |
| } else { |
| List<String> values = new ArrayList<String>(); |
| values.add(value); |
| parameters.put(key,values); |
| } |
| } |
| } |
| return parameters; |
| } |
| |
| } |