/* * Copyright 2013 Cloud4SOA, www.cloud4soa.eu * * 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. */ /* * Copyright 2009-2012 the original author or authors. * * 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 org.cloudfoundry.caldecott.client; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; import org.springframework.http.client.ClientHttpRequest; import org.springframework.http.client.ClientHttpResponse; import org.springframework.http.client.CommonsClientHttpRequestFactory; import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.HttpStatusCodeException; import org.springframework.web.client.RequestCallback; import org.springframework.web.client.ResponseExtractor; import org.springframework.web.client.RestOperations; import org.springframework.web.client.RestTemplate; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.HashMap; import java.util.Map; /** * The Http implementation of a Tunnel designed to interact with the Caldecott server REST application. * * @author Thomas Risberg */ public class HttpTunnel implements Tunnel { protected final Log logger = LogFactory.getLog(getClass()); // configuration options for the tunnel private String url; private String host; private int port; private String auth; // REST template to use for tunnel communication private final RestOperations restOperations; // variables to keep track of communication state with the tunnel web service private Map<String, String> tunnelInfo; private long lastWrite = 0; private long lastRead = 0; public HttpTunnel(String url, String host, int port, String auth) { this(url, host, port, auth, createRestTemplate()); } public HttpTunnel(String url, String host, int port, String auth, RestOperations restOperations) { this.url = url; this.host = host; this.port = port; this.auth = auth; this.restOperations = restOperations; openTunnel(); } public void write(byte[] data) { sendBytes(data, ++lastWrite); } public byte[] read(boolean retry) { if (!retry) { lastRead++; } return receiveBytes(lastRead); } private void openTunnel() { String initMsg = "{\"host\":\"" + host + "\",\"port\":" + port + "}"; if (logger.isDebugEnabled()) { logger.debug("Initializing tunnel: " + initMsg); } HttpHeaders requestHeaders = new HttpHeaders(); requestHeaders.set("Auth-Token", auth); requestHeaders.set("Content-Length", initMsg.length()+""); HttpEntity<String> requestEntity = new HttpEntity<String>(initMsg, requestHeaders); String jsonResponse; try { jsonResponse = restOperations.postForObject(url + "/tunnels", requestEntity, String.class); } catch (RuntimeException e) { logger.error("Fatal error while opening tunnel: " + e.getMessage()); close(); throw e; } try { this.tunnelInfo = TunnelHelper.convertJsonToMap(jsonResponse); } catch (IOException ignore) { this.tunnelInfo = new HashMap<String, String>(); } } @SuppressWarnings({ "unchecked", "rawtypes" }) public void close() { if (logger.isDebugEnabled()) { logger.debug("Deleting tunnel " + this.tunnelInfo.get("path")); } HttpHeaders requestHeaders = new HttpHeaders(); requestHeaders.set("Auth-Token", auth); HttpEntity<?> requestEntity = new HttpEntity(requestHeaders); try { restOperations.exchange(url + this.tunnelInfo.get("path"), HttpMethod.DELETE, requestEntity, null); } catch (HttpClientErrorException e) { if (e.getStatusCode().value() == 404) { if (logger.isDebugEnabled()) { logger.debug("Tunnel not found [" + e.getStatusCode() + "] " + e.getStatusText()); } } else { logger.warn("Error while deleting tunnel [" + e.getStatusCode() + "] " + e.getStatusText()); } } } private void sendBytes(byte[] bytes, long page) { HttpHeaders requestHeaders = new HttpHeaders(); requestHeaders.set("Auth-Token", auth); requestHeaders.set("Content-Length", bytes.length+""); String dataUrl = url + this.tunnelInfo.get("path_in") + "/" + page; HttpEntity<byte[]> requestEntity = new HttpEntity<byte[]>(bytes, requestHeaders); if (logger.isTraceEnabled()) { logger.trace("SENDING: " + printBytes(bytes)); } ResponseEntity<?> response = restOperations.exchange(dataUrl, HttpMethod.PUT, requestEntity, null); if (logger.isDebugEnabled()) { logger.debug("[" + bytes.length + " bytes] PUT to " + dataUrl +" resulted in: " + response.getStatusCode()); } } private byte[] receiveBytes(long page) { byte[] response = receiveDataBuffered(page); if (logger.isTraceEnabled()) { logger.trace("RECEIVED: " + printBytes(response)); } return response; } private byte[] receiveDataBuffered(long page) { final String dataUrl = url + this.tunnelInfo.get("path_out") + "/" + page; byte[] responseBytes; try { responseBytes = restOperations.execute( dataUrl, HttpMethod.GET, new RequestCallback() { public void doWithRequest(ClientHttpRequest clientHttpRequest) throws IOException { clientHttpRequest.getHeaders().set("Auth-Token", auth); } }, new ResponseExtractor<byte[]>() { public byte[] extractData(ClientHttpResponse clientHttpResponse) throws IOException { if (logger.isDebugEnabled()) { logger.debug("HEADER: " + clientHttpResponse.getHeaders().toString()); } InputStream stream = clientHttpResponse.getBody(); byte[] data = readContentData(stream); if (logger.isDebugEnabled()) { logger.debug("[" + data.length + " bytes] GET from " + dataUrl + " resulted in: " + clientHttpResponse.getStatusCode()); } return data; } } ); } catch (HttpStatusCodeException e) { if (logger.isDebugEnabled()) { logger.debug("GET from " + dataUrl + " resulted in: " + e.getStatusCode().value()); } throw e; } return responseBytes; } @Override public String toString() { return "HttpTunnel for " + url + " on " + host + ":" + port; } private byte[] readContentData(InputStream stream) throws IOException { ByteArrayOutputStream data = new ByteArrayOutputStream(); while (true) { byte[] buffer = new byte[1024]; int len = stream.read(buffer); if (len < 0) { break; } data.write(buffer, 0, len); } return data.toByteArray(); } private static RestTemplate createRestTemplate() { RestTemplate restTemplate = new RestTemplate(); CommonsClientHttpRequestFactory requestFactory = new CommonsClientHttpRequestFactory(); requestFactory.setConnectTimeout(20000); requestFactory.setReadTimeout(20000); restTemplate.setRequestFactory(requestFactory); return restTemplate; } private static String printBytes(byte[] array) { StringBuilder printable = new StringBuilder(); printable.append("[" + array.length + "] = " + "0x"); for (int k = 0; k < array.length; k++) { printable.append(byteToHex(array[k])); } return printable.toString(); } private static String byteToHex(byte b) { // Returns hex String representation of byte b char hexDigit[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; char[] array = {hexDigit[(b >> 4) & 0x0f], hexDigit[b & 0x0f]}; return new String(array); } }