Java tutorial
/******************************************************************************* * Copyright (c) 2015 Institute for Pervasive Computing, ETH Zurich and others. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * and Eclipse Distribution License v1.0 which accompany this distribution. * * The Eclipse Public License is available at * http://www.eclipse.org/legal/epl-v10.html * and the Eclipse Distribution License is available at * http://www.eclipse.org/org/documents/edl-v10.html. * * Contributors: * Matthias Kovatsch - creator and main architect * Martin Lanter - architect and re-implementation * Francesco Corazza - HTTP cross-proxy * Paul LeMarquand - fix content type returned from getHttpEntity(), cleanup ******************************************************************************/ package org.eclipse.californium.proxy; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.InetAddress; import java.net.URI; import java.net.URISyntaxException; import java.net.URLDecoder; import java.net.UnknownHostException; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.CharacterCodingException; import java.nio.charset.Charset; import java.nio.charset.CharsetDecoder; import java.nio.charset.CharsetEncoder; import java.nio.charset.UnmappableCharacterException; import java.nio.charset.UnsupportedCharsetException; import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Properties; import java.util.StringTokenizer; import java.util.logging.Logger; import java.util.regex.Pattern; import org.apache.http.Header; import org.apache.http.HttpEntity; import org.apache.http.HttpEntityEnclosingRequest; import org.apache.http.HttpMessage; import org.apache.http.HttpRequest; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; import org.apache.http.HttpVersion; import org.apache.http.RequestLine; import org.apache.http.StatusLine; import org.apache.http.entity.AbstractHttpEntity; import org.apache.http.entity.ByteArrayEntity; import org.apache.http.entity.ContentType; import org.apache.http.entity.StringEntity; import org.apache.http.impl.EnglishReasonPhraseCatalog; import org.apache.http.message.BasicHeader; import org.apache.http.message.BasicHttpEntityEnclosingRequest; import org.apache.http.message.BasicHttpRequest; import org.apache.http.message.BasicRequestLine; import org.apache.http.message.BasicStatusLine; import org.apache.http.util.EntityUtils; import org.eclipse.californium.core.coap.CoAP.Code; import org.eclipse.californium.core.coap.CoAP.ResponseCode; import org.eclipse.californium.core.coap.CoAP.Type; import org.eclipse.californium.core.coap.MediaTypeRegistry; import org.eclipse.californium.core.coap.Message; import org.eclipse.californium.core.coap.Option; import org.eclipse.californium.core.coap.OptionNumberRegistry; import org.eclipse.californium.core.coap.OptionNumberRegistry.optionFormats; import org.eclipse.californium.core.coap.Request; import org.eclipse.californium.core.coap.Response; import org.json.simple.JSONArray; import org.json.simple.JSONObject; import org.json.simple.parser.JSONParser; import org.json.simple.parser.ParseException; /** * Class providing the translations (mappings) from the HTTP message * representations to the CoAP message representations and vice versa. */ public final class HttpTranslator { private static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1"); private static final Charset UTF_8 = Charset.forName("UTF-8"); private static final String KEY_COAP_CODE = "coap.response.code."; private static final String KEY_COAP_OPTION = "coap.message.option."; private static final String KEY_COAP_MEDIA = "coap.message.media."; private static final String KEY_HTTP_CODE = "http.response.code."; private static final String KEY_HTTP_METHOD = "http.request.method."; private static final String KEY_HTTP_HEADER = "http.message.header."; private static final String KEY_HTTP_CONTENT_TYPE = "http.message.content-type."; private static String slug = ""; private static String rt = ""; private static String ldpServerAddress = ""; private static String proxyIP = ""; private static String ldp_incl = ""; private static String ldp_omit = ""; /** * Property file containing the mappings between coap messages and http * messages. */ public static final Properties HTTP_TRANSLATION_PROPERTIES = new MappingProperties("Proxy.properties"); // Error constants public static final int STATUS_TIMEOUT = HttpStatus.SC_GATEWAY_TIMEOUT; public static final int STATUS_NOT_FOUND = HttpStatus.SC_BAD_GATEWAY; public static final int STATUS_TRANSLATION_ERROR = HttpStatus.SC_BAD_GATEWAY; public static final int STATUS_URI_MALFORMED = HttpStatus.SC_BAD_REQUEST; public static final int STATUS_WRONG_METHOD = HttpStatus.SC_NOT_IMPLEMENTED; protected static final Logger LOGGER = Logger.getLogger(HttpTranslator.class.getName()); /** * Gets the coap media type associated to the http entity. Firstly, it looks * for a valid mapping in the property file. If this step fails, then it * tries to explicitly map/parse the declared mime/type by the http entity. * If even this step fails, it sets application/octet-stream as * content-type. * * @param httpMessage * * * @return the coap media code associated to the http message entity. * @see * HttpHeader, ContentType, MediaTypeRegistry */ public static int getCoapMediaType(HttpMessage httpMessage) { if (httpMessage == null) { throw new IllegalArgumentException("httpMessage == null"); } // get the entity HttpEntity httpEntity = null; if (httpMessage instanceof HttpResponse) { httpEntity = ((HttpResponse) httpMessage).getEntity(); } else if (httpMessage instanceof HttpEntityEnclosingRequest) { httpEntity = ((HttpEntityEnclosingRequest) httpMessage).getEntity(); } // check that the entity is actually present in the http message if (httpEntity == null) { throw new IllegalArgumentException("The http message does not contain any httpEntity."); } // set the content-type with a default value int coapContentType = MediaTypeRegistry.UNDEFINED; // get the content-type from the entity ContentType contentType = ContentType.get(httpEntity); if (contentType == null) { // if the content-type is not set, search in the headers Header contentTypeHeader = httpMessage.getFirstHeader("content-type"); if (contentTypeHeader != null) { String contentTypeString = contentTypeHeader.getValue(); contentType = ContentType.parse(contentTypeString); } } // check if there is an associated content-type with the current http // message if (contentType != null) { // get the value of the content-type String httpContentTypeString = contentType.getMimeType(); // delete the last part (if any) httpContentTypeString = httpContentTypeString.split(";")[0]; // retrieve the mapping from the property file String coapContentTypeString = HTTP_TRANSLATION_PROPERTIES .getProperty(KEY_HTTP_CONTENT_TYPE + httpContentTypeString); if (coapContentTypeString != null) { coapContentType = Integer.parseInt(coapContentTypeString); } else { // try to parse the media type if the property file has given to // mapping coapContentType = MediaTypeRegistry.parse(httpContentTypeString); } } // if not recognized, the content-type should be // application/octet-stream (draft-castellani-core-http-mapping 6.2) if (coapContentType == MediaTypeRegistry.UNDEFINED) { coapContentType = MediaTypeRegistry.APPLICATION_OCTET_STREAM; } return coapContentType; } /** * Gets the coap options starting from an array of http headers. The * content-type is not handled by this method. The method iterates over an * array of headers and for each of them tries to find a mapping in the * properties file, if the mapping does not exists it skips the header * ignoring it. The method handles separately certain headers which are * translated to options (such as accept or cache-control) whose content * should be semantically checked or requires ad-hoc translation. Otherwise, * the headers content is translated with the appropriate format required by * the mapped option. * * @param headers * */ public static List<Option> getCoapOptions(Header[] headers) { if (headers == null) { throw new IllegalArgumentException("httpMessage == null"); } List<Option> optionList = new LinkedList<Option>(); // iterate over the headers for (Header header : headers) { try { String headerName = header.getName().toLowerCase(); // FIXME: CoAP does no longer support multiple accept-options. // If an HTTP request contains multiple accepts, this method // fails. Therefore, we currently skip accepts at the moment. if (headerName.startsWith("accept") && !(headerName.equals("accept"))) continue; if (headerName.equals("slug")) { slug = header.getValue().trim(); } if (headerName.equals("prefer")) { String value = header.getValue().trim(); if (value.contains("include")) { int start = value.indexOf("include="); String content = value.substring(start + 9, value.length()); int start_val = content.indexOf("#"); String prefer = content.substring(start_val + 1); if (!prefer.contains(" ")) { ldp_incl = "ldp:" + prefer.substring(0, prefer.length() - 1); } else ldp_incl = "\"ldp:" + prefer.replace("http://www.w3.org/ns/ldp#", "ldp:"); } else if (value.contains("omit")) { int start = value.indexOf("omit="); String content = value.substring(start + 6, value.length()); int start_val = content.indexOf("#"); String prefer = content.substring(start_val + 1); if (!prefer.contains(" ")) { ldp_omit = "ldp:" + prefer.substring(0, prefer.length() - 1); } else ldp_omit = "\"ldp:" + prefer.replace("http://www.w3.org/ns/ldp#", "ldp:"); } } if (headerName.equals("link")) { //slug = header.getValue().trim(); int index = header.getValue().indexOf(";"); String name = header.getValue().substring(0, index - 1); index = name.indexOf("#"); rt = name.substring(index + 1); } // get the mapping from the property file String optionCodeString = HTTP_TRANSLATION_PROPERTIES.getProperty(KEY_HTTP_HEADER + headerName); // ignore the header if not found in the properties file if (optionCodeString == null || optionCodeString.isEmpty()) { continue; } // get the option number int optionNumber = OptionNumberRegistry.RESERVED_0; try { optionNumber = Integer.parseInt(optionCodeString.trim()); } catch (Exception e) { LOGGER.warning("Problems in the parsing: " + e.getMessage()); // ignore the option if not recognized continue; } // ignore the content-type because it will be handled within the // payload if (optionNumber == OptionNumberRegistry.CONTENT_FORMAT) { continue; } // get the value of the current header String headerValue = header.getValue().trim(); // if the option is accept, it needs to translate the // values if (optionNumber == OptionNumberRegistry.ACCEPT) { // remove the part where the client express the weight of each // choice headerValue = headerValue.trim().split(";")[0].trim(); // iterate for each content-type indicated for (String headerFragment : headerValue.split(",")) { // translate the content-type Integer[] coapContentTypes = { MediaTypeRegistry.UNDEFINED }; if (headerFragment.equals("*/*")) { coapContentTypes[0] = MediaTypeRegistry.parse("text/turtle"); } else if (headerFragment.contains("*")) { coapContentTypes = MediaTypeRegistry.parseWildcard(headerFragment); } else { coapContentTypes[0] = MediaTypeRegistry.parse(headerFragment); } // if is present a conversion for the content-type, then add // a new option for (int coapContentType : coapContentTypes) { if (coapContentType != MediaTypeRegistry.UNDEFINED) { // create the option Option option = new Option(optionNumber, coapContentType); optionList.add(option); } } } } else if (optionNumber == OptionNumberRegistry.MAX_AGE) { int maxAge = 0; if (!headerValue.contains("no-cache")) { headerValue = headerValue.split(",")[0]; if (headerValue != null) { int index = headerValue.indexOf('='); try { maxAge = Integer.parseInt(headerValue.substring(index + 1).trim()); } catch (NumberFormatException e) { LOGGER.warning("Cannot convert cache control in max-age option"); continue; } } } // create the option Option option = new Option(optionNumber, maxAge); // option.setValue(headerValue.getBytes(Charset.forName("ISO-8859-1"))); optionList.add(option); } else { // create the option Option option = new Option(optionNumber); switch (OptionNumberRegistry.getFormatByNr(optionNumber)) { case INTEGER: option.setIntegerValue(Integer.parseInt(headerValue)); break; case OPAQUE: option.setValue(headerValue.getBytes(ISO_8859_1)); break; case STRING: default: option.setStringValue(headerValue); break; } // option.setValue(headerValue.getBytes(Charset.forName("ISO-8859-1"))); optionList.add(option); } } catch (RuntimeException e) { // Martin: I have added this try-catch block. The problem is // that HTTP support multiple Accepts while CoAP does not. A // headder line might look like this: // Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 // This cannot be parsed into a single CoAP Option and yields a // NumberFormatException LOGGER.warning("Could not parse header line " + header); } } // while (headerIterator.hasNext()) return optionList; } /** * Method to map the http entity of a http message in a coherent payload for * the coap message. The method simply gets the bytes from the entity and, * if needed changes the charset of the obtained bytes to UTF-8. * * @param httpEntity * the http entity * * @return byte[] * @throws TranslationException * the translation exception */ public static byte[] getCoapPayload(HttpEntity httpEntity) throws TranslationException { if (httpEntity == null) { throw new IllegalArgumentException("httpEntity == null"); } byte[] payload = null; try { // get the bytes from the entity payload = EntityUtils.toByteArray(httpEntity); if (payload != null && payload.length > 0 && looksLikeUTF8(payload)) { //modifica il payload per sostituire i riferimenti a http://proxyIP:8080/proxy/ String body = ""; try { body = new String(payload, "UTF-8"); } catch (UnsupportedEncodingException e1) { // TODO Auto-generated catch block e1.printStackTrace(); } body = body.replace("http://" + proxyIP + ":8080/proxy/", "coap://"); payload = body.getBytes(); // the only supported charset in CoAP is UTF-8 Charset coapCharset = UTF_8; // get the charset for the http entity ContentType httpContentType = ContentType.getOrDefault(httpEntity); Charset httpCharset = httpContentType.getCharset(); // check if the charset is the one allowed by coap if (httpCharset != null && !httpCharset.equals(coapCharset)) { // translate the payload to the utf-8 charset payload = changeCharset(payload, httpCharset, coapCharset); } } else { int i = 0; } } catch (IOException e) { LOGGER.warning("Cannot get the content of the http entity: " + e.getMessage()); throw new TranslationException("Cannot get the content of the http entity", e); } finally { try { // ensure all content has been consumed, so that the // underlying connection could be re-used EntityUtils.consume(httpEntity); } catch (IOException e) { } } return payload; } static boolean looksLikeUTF8(byte[] utf8) { boolean response = false; Pattern p = Pattern.compile("\\A(\n" + " [\\x09\\x0A\\x0D\\x20-\\x7E] # ASCII\\n" + "| [\\xC2-\\xDF][\\x80-\\xBF] # non-overlong 2-byte\n" + "| \\xE0[\\xA0-\\xBF][\\x80-\\xBF] # excluding overlongs\n" + "| [\\xE1-\\xEC\\xEE\\xEF][\\x80-\\xBF]{2} # straight 3-byte\n" + "| \\xED[\\x80-\\x9F][\\x80-\\xBF] # excluding surrogates\n" + "| \\xF0[\\x90-\\xBF][\\x80-\\xBF]{2} # planes 1-3\n" + "| [\\xF1-\\xF3][\\x80-\\xBF]{3} # planes 4-15\n" + "| \\xF4[\\x80-\\x8F][\\x80-\\xBF]{2} # plane 16\n" + ")*\\z", Pattern.COMMENTS); String phonyString; try { phonyString = new String(utf8, "ISO-8859-1"); response = p.matcher(phonyString).matches(); } catch (UnsupportedEncodingException e) { // TODO Auto-generated catch block response = false; } return response; } /** * Gets the coap request. Creates the CoAP request from the HTTP method and * mapping it through the properties file. The uri is translated using * regular expressions, the uri format expected is either the embedded * mapping (http://proxyname.domain:80/proxy/coapserver:5683/resource * converted in coap://coapserver:5683/resource) or the standard uri to * indicate a local request not to be forwarded. The method uses a decoder * to translate the application/x-www-form-urlencoded format of the uri. The * CoAP options are set translating the headers. If the HTTP message has an * enclosing entity, it is converted to create the payload of the CoAP * message; finally the content-type is set accordingly to the header and to * the entity type. * * @param httpRequest * the http request * @param proxyResource * the proxy resource, if present in the uri, indicates the need * of forwarding for the current request * @param proxyingEnabled * TODO * * * @return the coap request * @throws TranslationException the translation * exception */ public static Request getCoapRequest(HttpRequest httpRequest, String proxyResource, boolean proxyingEnabled) throws TranslationException { if (httpRequest == null) { throw new IllegalArgumentException("httpRequest == null"); } if (proxyResource == null) { throw new IllegalArgumentException("proxyResource == null"); } // get the http method String httpMethod = httpRequest.getRequestLine().getMethod().toLowerCase(); // get the coap method String coapMethodString = HTTP_TRANSLATION_PROPERTIES.getProperty(KEY_HTTP_METHOD + httpMethod); if (coapMethodString == null || coapMethodString.contains("error")) { throw new InvalidMethodException(httpMethod + " method not mapped"); } int coapMethod = 0; try { coapMethod = Integer.parseInt(coapMethodString.trim()); } catch (NumberFormatException e) { LOGGER.warning("Cannot convert the http method in coap method: " + e); throw new TranslationException("Cannot convert the http method in coap method", e); } // create the request -- since HTTP is reliable use CON Request coapRequest = new Request(Code.valueOf(coapMethod), Type.CON); // translate the http headers in coap options List<Option> coapOptions = getCoapOptions(httpRequest.getAllHeaders()); for (Option option : coapOptions) coapRequest.getOptions().addOption(option); // get the uri String uriString = httpRequest.getRequestLine().getUri(); // remove the initial "/" uriString = uriString.substring(1); //gestisco l'header Slug per il metodo POST if (httpMethod.equals("post")) { if (rt != null && rt != "") { if (slug != null && slug != "") { uriString = uriString + "?title=" + slug + "&rt=ldp:" + rt; slug = ""; } else { uriString = uriString + "?rt=ldp:" + rt; } } else { if (slug != null && slug != "") { uriString = uriString + "?title=" + slug + "&rt=ldp:Resource"; slug = ""; } else { uriString = uriString + "?rt=ldp:Resource"; } } } else if (httpMethod.equals("patch")) { uriString = uriString + "?ldp=patch"; } else if (httpMethod.equals("get")) { if (ldp_incl != null && ldp_incl != "") { if (!uriString.contains("?")) { uriString = uriString + "?ldp-incl=" + ldp_incl; } else { uriString = uriString + "&ldp-incl=" + ldp_incl; } ldp_incl = ""; } if (ldp_omit != null && ldp_omit != "") { if (!uriString.contains("?")) { uriString = uriString + "?ldp-omit=" + ldp_omit; } else { uriString = uriString + "&ldp-omit=" + ldp_omit; } ldp_omit = ""; } } // decode the uri to translate the application/x-www-form-urlencoded // format try { uriString = URLDecoder.decode(uriString, "UTF-8"); } catch (UnsupportedEncodingException e) { LOGGER.warning("Failed to decode the uri: " + e.getMessage()); throw new TranslationException("Failed decoding the uri: " + e.getMessage()); } catch (Throwable e) { LOGGER.warning("Malformed uri: " + e.getMessage()); throw new InvalidFieldException("Malformed uri: " + e.getMessage()); } // if the uri contains the proxy resource name, the request should be // forwarded and it is needed to get the real requested coap server's // uri // e.g.: // /proxy/vslab-dhcp-17.inf.ethz.ch:5684/helloWorld // proxy resource: /proxy // coap server: vslab-dhcp-17.inf.ethz.ch:5684 // coap resource: helloWorld if (uriString.matches(".?" + proxyResource + ".*")) { // find the first occurrence of the proxy resource int index = uriString.indexOf(proxyResource); // delete the slash index = uriString.indexOf('/', index); uriString = uriString.substring(index + 1); if (proxyingEnabled) { // if the uri hasn't the indication of the scheme, add it if (!uriString.matches("^coaps?://.*")) { uriString = "coap://" + uriString; } // the uri will be set as a proxy-uri option coapRequest.getOptions().setProxyUri(uriString.replace(" ", "%20").replace("\"", "%22")); } else { coapRequest.setURI(uriString); } // set the proxy as the sender to receive the response correctly try { // TODO check with multihomed hosts InetAddress localHostAddress = InetAddress.getLocalHost(); //get proxy IP proxyIP = localHostAddress.getHostAddress(); coapRequest.setDestination(localHostAddress); // TODO: setDestinationPort??? } catch (UnknownHostException e) { LOGGER.warning("Cannot get the localhost address: " + e.getMessage()); throw new TranslationException("Cannot get the localhost address: " + e.getMessage()); } } else { // if the uri does not contains the proxy resource, it means the // request is local to the proxy and it shouldn't be forwarded // set the uri string as uri-path option coapRequest.getOptions().setUriPath(uriString); } // set the payload if the http entity is present if (httpRequest instanceof HttpEntityEnclosingRequest) { HttpEntity httpEntity = ((HttpEntityEnclosingRequest) httpRequest).getEntity(); // translate the http entity in coap payload byte[] payload = getCoapPayload(httpEntity); coapRequest.setPayload(payload); // set the content-type int coapContentType = getCoapMediaType(httpRequest); coapRequest.getOptions().setContentFormat(coapContentType); } //retrieve LDP server address String provvisorio = uriString.substring(7); int end = provvisorio.indexOf("/"); ldpServerAddress = provvisorio.substring(0, end); return coapRequest; } public static Request createCoapRequestDiscovery(String proxyUri) throws TranslationException { // create the request -- since HTTP is reliable use CON Request coapRequest = new Request(Code.valueOf(1), Type.CON); String uri = ""; if (proxyUri.contains("?")) { int index = proxyUri.indexOf("?"); uri = proxyUri.substring(0, index); } else { uri = proxyUri; } String uriNoSchema = uri.substring(7); int index = uriNoSchema.indexOf('/', 0); String host = uriNoSchema.substring(0, index); String resource = uriNoSchema.substring(index + 1); StringTokenizer tokenizer = new StringTokenizer(resource, "/"); String resources = ""; while (tokenizer.hasMoreElements()) { resources = (String) tokenizer.nextElement(); } coapRequest.getOptions().setProxyUri("coap://" + host + "/.well-known/core?title=" + resources); // set the proxy as the sender to receive the response correctly try { // TODO check with multihomed hosts InetAddress localHostAddress = InetAddress.getLocalHost(); coapRequest.setDestination(localHostAddress); // TODO: setDestinationPort??? } catch (UnknownHostException e) { LOGGER.warning("Cannot get the localhost address: " + e.getMessage()); throw new TranslationException("Cannot get the localhost address: " + e.getMessage()); } return coapRequest; } public static Request createCoapRequestOptions(String proxyUri) throws TranslationException { // create the request -- since HTTP is reliable use CON Request coapRequest = new Request(Code.valueOf(1), Type.CON); if (proxyUri.contains("?")) { int index = proxyUri.indexOf("?"); String uri = proxyUri.substring(0, index); coapRequest.getOptions().setProxyUri(uri + "?ldp=options"); } else { coapRequest.getOptions().setProxyUri(proxyUri + "?ldp=options"); } // set the proxy as the sender to receive the response correctly try { // TODO check with multihomed hosts InetAddress localHostAddress = InetAddress.getLocalHost(); coapRequest.setDestination(localHostAddress); // TODO: setDestinationPort??? } catch (UnknownHostException e) { LOGGER.warning("Cannot get the localhost address: " + e.getMessage()); throw new TranslationException("Cannot get the localhost address: " + e.getMessage()); } return coapRequest; } /** * Gets the CoAP response from an incoming HTTP response. No null value is * returned. The response is created from a the mapping of the HTTP response * code retrieved from the properties file. If the code is 204, which has * multiple meaning, the mapping is handled looking on the request method * that has originated the response. The options are set thorugh the HTTP * headers and the option max-age, if not indicated, is set to the default * value (60 seconds). if the response has an enclosing entity, it is mapped * to a CoAP payload and the content-type of the CoAP message is set * properly. * * @param httpResponse * the http response * @param coapRequest * * * @return the coap response * @throws TranslationException the translation * exception */ public static Response getCoapResponse(HttpResponse httpResponse, Request coapRequest) throws TranslationException { if (httpResponse == null) { throw new IllegalArgumentException("httpResponse == null"); } if (coapRequest == null) { throw new IllegalArgumentException("coapRequest == null"); } // get/set the response code int httpCode = httpResponse.getStatusLine().getStatusCode(); ResponseCode coapCode; Code coapMethod = coapRequest.getCode(); // the code 204-"no content" should be managed // separately because it can be mapped to different coap codes // depending on the request that has originated the response if (httpCode == HttpStatus.SC_NO_CONTENT) { if (coapMethod == Code.DELETE) { coapCode = ResponseCode.DELETED; } else { coapCode = ResponseCode.CHANGED; } } else { // get the translation from the property file String coapCodeString = HTTP_TRANSLATION_PROPERTIES.getProperty(KEY_HTTP_CODE + httpCode); if (coapCodeString == null || coapCodeString.isEmpty()) { LOGGER.warning("coapCodeString == null"); throw new TranslationException("coapCodeString == null"); } try { coapCode = ResponseCode.valueOf(Integer.parseInt(coapCodeString.trim())); } catch (NumberFormatException e) { LOGGER.warning("Cannot convert the status code in number: " + e.getMessage()); throw new TranslationException("Cannot convert the status code in number", e); } } // create the coap reaponse Response coapResponse = new Response(coapCode); // translate the http headers in coap options List<Option> coapOptions = getCoapOptions(httpResponse.getAllHeaders()); for (Option option : coapOptions) coapResponse.getOptions().addOption(option); // the response should indicate a max-age value (RFC 7252, Section 10.1.1) if (!coapResponse.getOptions().hasMaxAge()) { // The Max-Age Option for responses to POST, PUT or DELETE requests // should always be set to 0 (draft-castellani-core-http-mapping). if (coapMethod == Code.GET) { coapResponse.getOptions().setMaxAge(OptionNumberRegistry.Defaults.MAX_AGE); } else { coapResponse.getOptions().setMaxAge(0); } } // get the entity HttpEntity httpEntity = httpResponse.getEntity(); if (httpEntity != null) { // translate the http entity in coap payload byte[] payload = getCoapPayload(httpEntity); if (payload != null && payload.length > 0) { coapResponse.setPayload(payload); // set the content-type int coapContentType = getCoapMediaType(httpResponse); coapResponse.getOptions().setContentFormat(coapContentType); } } return coapResponse; } /** * Generates an HTTP entity starting from a CoAP request. If the coap * message has no payload, it returns a null http entity. It takes the * payload from the CoAP message and encapsulates it in an entity. If the * content-type is recognized, and a mapping is present in the properties * file, it is translated to the correspondent in HTTP, otherwise it is set * to application/octet-stream. If the content-type has a charset, namely it * is printable, the payload is encapsulated in a StringEntity, if not it a * ByteArrayEntity is used. * * * @param coapMessage * the coap message * * * @return null if the request has no payload * @throws TranslationException * the translation exception */ public static HttpEntity getHttpEntity(Message coapMessage) throws TranslationException { if (coapMessage == null) { throw new IllegalArgumentException("coapMessage == null"); } // the result HttpEntity httpEntity = null; // check if coap request has a payload byte[] payload = coapMessage.getPayload(); if (payload != null && payload.length != 0) { ContentType contentType = null; // if the content type is not set, translate with octect-stream if (!coapMessage.getOptions().hasContentFormat()) { contentType = ContentType.APPLICATION_OCTET_STREAM; } else { int coapContentType = coapMessage.getOptions().getContentFormat(); // search for the media type inside the property file String coapContentTypeString = HTTP_TRANSLATION_PROPERTIES .getProperty(KEY_COAP_MEDIA + coapContentType); // if the content-type has not been found in the property file, // try to get its string value (expressed in mime type) if (coapContentTypeString == null || coapContentTypeString.isEmpty()) { coapContentTypeString = MediaTypeRegistry.toString(coapContentType); // if the coap content-type is printable, it is needed to // set the default charset (i.e., UTF-8) if (MediaTypeRegistry.isPrintable(coapContentType)) { coapContentTypeString += "; charset=UTF-8"; } } // parse the content type try { contentType = ContentType.parse(coapContentTypeString); } catch (UnsupportedCharsetException e) { LOGGER.finer("Cannot convert string to ContentType: " + e.getMessage()); contentType = ContentType.APPLICATION_OCTET_STREAM; } } // get the charset Charset charset = contentType.getCharset(); // if there is a charset, means that the content is not binary if (charset != null) { String body = ""; try { body = new String(payload, "UTF-8"); } catch (UnsupportedEncodingException e1) { // TODO Auto-generated catch block e1.printStackTrace(); } body = body.replace("coap://", "http://" + proxyIP + ":8080/proxy/"); payload = body.getBytes(); // according to the class ContentType the default content-type // with UTF-8 charset is application/json. If the content-type // parsed is different and is not iso encoded, a translation is // needed Charset isoCharset = ISO_8859_1; /*if (!charset.equals(isoCharset) && !contentType.getMimeType().equals(ContentType.APPLICATION_JSON.getMimeType())) { byte[] newPayload = changeCharset(payload, charset, isoCharset); // since ISO-8859-1 is a subset of UTF-8, it is needed to // check if the mapping could be accomplished, only if the // operation is succesful the payload and the charset should // be changed if (newPayload != null) { payload = newPayload; // if the charset is changed, also the entire // content-type must change contentType = ContentType.create(contentType.getMimeType(), isoCharset); } }*/ // create the content String payloadString = new String(payload, contentType.getCharset()); // create the entity httpEntity = new StringEntity(payloadString, ContentType.create(contentType.getMimeType(), contentType.getCharset())); } else { // create the entity httpEntity = new ByteArrayEntity(payload); } // set the content-type ((AbstractHttpEntity) httpEntity).setContentType(contentType.getMimeType()); } return httpEntity; } /** * Gets the http headers from a list of CoAP options. The method iterates * over the list looking for a translation of each option in the properties * file, this process ignores the proxy-uri and the content-type because * they are managed differently. If a mapping is present, the content of the * option is mapped to a string accordingly to its original format and set * as the content of the header. * * * @param optionList * the coap message * * @return Header[] */ public static Header[] getHttpHeaders(List<Option> optionList, String host) { if (optionList == null) { throw new IllegalArgumentException("coapMessage == null"); } List<Header> headers = new LinkedList<Header>(); String location_path = ""; String location_query = ""; // iterate over each option for (Option option : optionList) { if (option.toString().startsWith("Location-Path:")) { if (location_path != null && location_path != "") { location_path = location_path + "/" + option.getStringValue(); } else { location_path = "http://" + proxyIP + ":8080/" + host + "/" + ldpServerAddress + "/" + option.getStringValue(); } } else if (option.toString().startsWith("Location-Query:")) { String res = option.getStringValue(); if (res.contains("ldp-incl") || res.contains("ldp-omit")) { Header header = new BasicHeader("Preference-Applied", "return=representation"); headers.add(header); } else { location_query = "<http://www.w3.org/ns/ldp#"; int index = res.indexOf(":"); String value = res.substring(index + 1); location_query = location_query + value + ">; rel='type'"; Header header = new BasicHeader("Link", location_query); headers.add(header); } } else { // skip content-type because it should be translated while handling // the payload; skip proxy-uri because it has to be translated in a // different way int optionNumber = option.getNumber(); if (optionNumber != OptionNumberRegistry.CONTENT_FORMAT && optionNumber != OptionNumberRegistry.PROXY_URI) { // get the mapping from the property file String headerName = HTTP_TRANSLATION_PROPERTIES.getProperty(KEY_COAP_OPTION + optionNumber); // set the header if (headerName != null && !headerName.isEmpty()) { // format the value String stringOptionValue = null; if (OptionNumberRegistry.getFormatByNr(optionNumber) == optionFormats.STRING) { stringOptionValue = option.getStringValue(); } else if (OptionNumberRegistry.getFormatByNr(optionNumber) == optionFormats.INTEGER) { stringOptionValue = Integer.toString(option.getIntegerValue()); } else if (OptionNumberRegistry.getFormatByNr(optionNumber) == optionFormats.OPAQUE) { stringOptionValue = new String(option.getValue()); } else { // if the option is not formattable, skip it continue; } // custom handling for max-age // format: cache-control: max-age=60 if (optionNumber == OptionNumberRegistry.MAX_AGE) { stringOptionValue = "max-age=" + stringOptionValue; } Header header = new BasicHeader(headerName, stringOptionValue); headers.add(header); } } } } if (location_path != null && location_path != "") { Header header = new BasicHeader("Location", location_path); headers.add(header); } return headers.toArray(new Header[0]); } /** * Gets the http request starting from a CoAP request. The method creates * the HTTP request through its request line. The request line is built with * the uri coming from the string representing the CoAP method and the uri * obtained from the proxy-uri option. If a payload is provided, the HTTP * request encloses an HTTP entity and consequently the content-type is set. * Finally, the CoAP options are mapped to the HTTP headers. * * @param coapRequest * the coap request * * * * @return the http request * @throws TranslationException the translation * exception * @throws URISyntaxException the uRI syntax exception */ public static HttpRequest getHttpRequest(Request coapRequest) throws TranslationException { if (coapRequest == null) { throw new IllegalArgumentException("coapRequest == null"); } HttpRequest httpRequest = null; String coapMethod = null; switch (coapRequest.getCode()) { case GET: coapMethod = "GET"; break; case POST: coapMethod = "POST"; break; case PUT: coapMethod = "PUT"; break; case DELETE: coapMethod = "DELETE"; break; } // get the proxy-uri URI proxyUri; try { /* * The new draft (14) only allows one proxy-uri option. Thus, this * code segment has changed. */ String proxyUriString = URLDecoder.decode(coapRequest.getOptions().getProxyUri(), "UTF-8"); proxyUri = new URI(proxyUriString); } catch (UnsupportedEncodingException e) { LOGGER.warning("UTF-8 do not support this encoding: " + e); throw new TranslationException("UTF-8 do not support this encoding", e); } catch (URISyntaxException e) { LOGGER.warning("Cannot translate the server uri" + e); throw new InvalidFieldException("Cannot get the proxy-uri from the coap message", e); } // create the requestLine RequestLine requestLine = new BasicRequestLine(coapMethod, proxyUri.toString(), HttpVersion.HTTP_1_1); // get the http entity HttpEntity httpEntity = getHttpEntity(coapRequest); // create the http request if (httpEntity == null) { httpRequest = new BasicHttpRequest(requestLine); } else { httpRequest = new BasicHttpEntityEnclosingRequest(requestLine); ((HttpEntityEnclosingRequest) httpRequest).setEntity(httpEntity); // get the content-type from the entity and set the header ContentType contentType = ContentType.get(httpEntity); httpRequest.setHeader("content-type", contentType.toString()); } // set the headers Header[] headers = getHttpHeaders(coapRequest.getOptions().asSortedList(), ""); for (Header header : headers) { httpRequest.addHeader(header); } return httpRequest; } /** * Sets the parameters of the incoming http response from a CoAP response. * The status code is mapped through the properties file and is set through * the StatusLine. The options are translated to the corresponding headers * and the max-age (in the header cache-control) is set to the default value * (60 seconds) if not already present. If the request method was not HEAD * and the coap response has a payload, the entity and the content-type are * set in the http response. * * @param coapResponse * the coap response * @param httpResponse * * * * @param httpRequest * HttpRequest * @throws TranslationException * the translation exception */ public static void getHttpResponse(HttpRequest httpRequest, Response coapResponse, HttpResponse httpResponse) throws TranslationException { if (httpRequest == null) { throw new IllegalArgumentException("httpRequest == null"); } if (coapResponse == null) { throw new IllegalArgumentException("coapResponse == null"); } if (httpResponse == null) { throw new IllegalArgumentException("httpResponse == null"); } // get/set the response code ResponseCode coapCode = coapResponse.getCode(); String httpCodeString = HTTP_TRANSLATION_PROPERTIES.getProperty(KEY_COAP_CODE + coapCode.value); if (httpCodeString == null || httpCodeString.isEmpty()) { LOGGER.warning("httpCodeString == null"); throw new TranslationException("httpCodeString == null"); } int httpCode = 0; try { httpCode = Integer.parseInt(httpCodeString.trim()); } catch (NumberFormatException e) { LOGGER.warning("Cannot convert the coap code in http status code" + e); throw new TranslationException("Cannot convert the coap code in http status code", e); } // create the http response and set the status line String reason = EnglishReasonPhraseCatalog.INSTANCE.getReason(httpCode, Locale.ENGLISH); StatusLine statusLine = new BasicStatusLine(HttpVersion.HTTP_1_1, httpCode, reason); httpResponse.setStatusLine(statusLine); String uriString = httpRequest.getRequestLine().getUri(); int index_query = uriString.indexOf("//"); String query = uriString.substring(index_query + 2); int index_host = query.indexOf("/"); String host = query.substring(0, index_host); // set the headers Header[] headers = getHttpHeaders(coapResponse.getOptions().asSortedList(), host); httpResponse.setHeaders(headers); // set max-age if not already set if (!httpResponse.containsHeader("cache-control")) { httpResponse.setHeader("cache-control", "max-age=" + Long.toString(OptionNumberRegistry.Defaults.MAX_AGE)); } // get the http entity if the request was not HEAD if (!httpRequest.getRequestLine().getMethod().equalsIgnoreCase("head")) { if ((httpRequest.getRequestLine().getMethod().equalsIgnoreCase("put")) && (coapCode.value == 131)) { String linkPut = getLinkPut(coapResponse); Header link = new BasicHeader("Link", linkPut); httpResponse.addHeader(link); } // if the content-type is not set in the coap response and if the // response contains an error, then the content-type should set to // text-plain if (coapResponse.getOptions().getContentFormat() == MediaTypeRegistry.UNDEFINED && (ResponseCode.isClientError(coapCode) || ResponseCode.isServerError(coapCode))) { LOGGER.info("Set contenttype to TEXT_PLAIN"); coapResponse.getOptions().setContentFormat(MediaTypeRegistry.TEXT_PLAIN); } HttpEntity httpEntity = getHttpEntity(coapResponse); if (httpEntity != null) { httpResponse.setEntity(httpEntity); // get the content-type from the entity and set the header ContentType contentType = ContentType.get(httpEntity); httpResponse.setHeader("content-type", contentType.toString()); } } } //recupero l'header Link dalla put che mi restituisce 403 static String getLinkPut(Response coapResponse) { String linkHeader = ""; if (coapResponse == null) { throw new IllegalArgumentException("coapMessage == null"); } // check if coap request has a payload byte[] payload = coapResponse.getPayload(); if (payload != null && payload.length != 0) { //modifica il payload per sostituire i riferimenti a coap:// String body = ""; try { body = new String(payload, "UTF-8"); } catch (UnsupportedEncodingException e1) { // TODO Auto-generated catch block e1.printStackTrace(); } if (body.startsWith("Link")) { linkHeader = body.substring(6); } } return linkHeader; } /** * Change charset. * * @param payload * the payload * @param fromCharset * the from charset * @param toCharset * the to charset * * * @return the byte[] * @throws TranslationException the translation * exception */ private static byte[] changeCharset(byte[] payload, Charset fromCharset, Charset toCharset) throws TranslationException { try { // decode with the source charset CharsetDecoder decoder = fromCharset.newDecoder(); CharBuffer charBuffer = decoder.decode(ByteBuffer.wrap(payload)); decoder.flush(charBuffer); // encode to the destination charset CharsetEncoder encoder = toCharset.newEncoder(); ByteBuffer byteBuffer = encoder.encode(charBuffer); encoder.flush(byteBuffer); payload = byteBuffer.array(); } catch (UnmappableCharacterException e) { // thrown when an input character (or byte) sequence is valid but // cannot be mapped to an output byte (or character) sequence. // If the character sequence starting at the input buffer's current // position cannot be mapped to an equivalent byte sequence and the // current unmappable-character LOGGER.finer("Charset translation: cannot mapped to an output char byte: " + e.getMessage()); return null; } catch (CharacterCodingException e) { LOGGER.warning("Problem in the decoding/encoding charset: " + e.getMessage()); throw new TranslationException("Problem in the decoding/encoding charset", e); } return payload; } /** * The Constructor is private because the class is an helper class and * cannot be instantiated. */ private HttpTranslator() { } public static void getHttpDiscovery(HttpRequest httpRequest, Response coapResponse, HttpResponse httpResponse) { // TODO Auto-generated method stub String response = coapResponse.getPayloadString(); int start_index = response.indexOf("rt=\"", 0); String substring = response.substring(start_index + 4); int end_index = substring.indexOf('"', 0); String rt = substring.substring(0, end_index); StringTokenizer tokenizer = new StringTokenizer(rt, " "); String resources = ""; while (tokenizer.hasMoreElements()) { String resource = "<" + (String) tokenizer.nextElement() + ">; rel=\"type\", "; resources = resources + resource; } resources = resources.substring(0, resources.length() - 2); Header link = new BasicHeader("Link", resources); httpResponse.addHeader(link); } public static void getHttpOptions(HttpRequest httpRequest, Response coapResponse, HttpResponse httpResponse) { // TODO Auto-generated method stub String response = coapResponse.getPayloadString(); JSONParser parser = new JSONParser(); try { JSONObject obj = (JSONObject) parser.parse(response); JSONArray allow = (JSONArray) obj.get("Allow"); JSONArray accept_post = (JSONArray) obj.get("Accept-Post"); JSONArray accept_patch = (JSONArray) obj.get("Accept-Patch"); if (allow != null) { String allow_string = allow.toJSONString(); allow_string = allow_string.replace("\"", ""); allow_string = allow_string.replace("[", ""); allow_string = allow_string.replace("]", ""); Header allow_header = new BasicHeader("Allow", allow_string); httpResponse.addHeader(allow_header); } if (accept_post != null) { String accept_post_string = accept_post.toJSONString(); accept_post_string = accept_post_string.replace("\"", ""); accept_post_string = accept_post_string.replace("\\", ""); accept_post_string = accept_post_string.replace("[", ""); accept_post_string = accept_post_string.replace("]", ""); Header accept_post_header = new BasicHeader("Accept-Post", accept_post_string); httpResponse.addHeader(accept_post_header); } if (accept_patch != null) { String accept_patch_string = accept_patch.toJSONString(); accept_patch_string = accept_patch_string.replace("\"", ""); accept_patch_string = accept_patch_string.replace("\\", ""); accept_patch_string = accept_patch_string.replace("[", ""); accept_patch_string = accept_patch_string.replace("]", ""); Header accept_patch_header = new BasicHeader("Accept-Patch", accept_patch_string); httpResponse.addHeader(accept_patch_header); } } catch (ParseException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }