/** * Licensed to JumpMind Inc under one or more contributor * license agreements. See the NOTICE file distributed * with this work for additional information regarding * copyright ownership. JumpMind Inc licenses this file * to you under the GNU General Public License, version 3.0 (GPLv3) * (the "License"); you may not use this file except in compliance * with the License. * * You should have received a copy of the GNU General Public License, * version 3.0 (GPLv3) along with this library; if not, see * <http://www.gnu.org/licenses/>. * * 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.jumpmind.symmetric.transport.http; import static org.apache.commons.lang.StringUtils.isNotBlank; import static org.jumpmind.symmetric.web.WebConstants.MAKE_RESERVATION_PATH; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.UnsupportedEncodingException; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLEncoder; import java.util.List; import java.util.Map; import java.util.zip.GZIPInputStream; import org.apache.commons.codec.binary.Base64; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.jumpmind.exception.IoException; import org.jumpmind.symmetric.ISymmetricEngine; import org.jumpmind.symmetric.common.Constants; import org.jumpmind.symmetric.common.ParameterConstants; import org.jumpmind.symmetric.io.IoConstants; import org.jumpmind.symmetric.model.BatchId; import org.jumpmind.symmetric.model.IncomingBatch; import org.jumpmind.symmetric.model.Node; import org.jumpmind.symmetric.model.OutgoingBatch; import org.jumpmind.symmetric.transport.AbstractTransportManager; import org.jumpmind.symmetric.transport.IIncomingTransport; import org.jumpmind.symmetric.transport.IOutgoingWithResponseTransport; import org.jumpmind.symmetric.transport.ITransportManager; import org.jumpmind.symmetric.transport.TransportUtils; import org.jumpmind.symmetric.util.SymmetricUtils; import org.jumpmind.symmetric.web.WebConstants; import org.jumpmind.util.AppUtils; /** * Allow remote communication to nodes, in order to push data, pull data, and * send messages. */ public class HttpTransportManager extends AbstractTransportManager implements ITransportManager { private ISymmetricEngine engine; public HttpTransportManager() { } public HttpTransportManager(ISymmetricEngine engine) { super(engine.getExtensionService()); this.engine = engine; } public int sendCopyRequest(Node local) throws IOException { StringBuilder data = new StringBuilder(); Map<String, BatchId> batchIds = engine.getIncomingBatchService().findMaxBatchIdsByChannel(); for (String channelId : batchIds.keySet()) { if (!Constants.CHANNEL_CONFIG.equals(channelId) && !Constants.CHANNEL_HEARTBEAT.equals(channelId)) { BatchId batchId = batchIds.get(channelId); append(data, channelId + "-" + batchId.getNodeId(), batchId.getBatchId()); } } String securityToken = engine.getNodeService().findNodeSecurity(local.getNodeId()) .getNodePassword(); String url = addParameterTokens(engine.getParameterService().getRegistrationUrl() + "/copy", local.getNodeId(), null, securityToken); url = add(url, WebConstants.EXTERNAL_ID, engine.getParameterService().getExternalId(), "&"); url = add(url, WebConstants.NODE_GROUP_ID, engine.getParameterService().getNodeGroupId(), "&"); log.info("Contact server to do node copy using a url of: " + url); return sendMessage(new URL(url), data.toString()); } public int sendAcknowledgement(Node remote, List<IncomingBatch> list, Node local, String securityToken, String registrationUrl) throws IOException { if (list != null && list.size() > 0) { String data = getAcknowledgementData(remote.requires13Compatiblity(), local.getNodeId(), list); return sendMessage("ack", remote, local, data, securityToken, registrationUrl); } return HttpURLConnection.HTTP_OK; } public void writeAcknowledgement(OutputStream out, Node remote, List<IncomingBatch> list, Node local, String securityToken) throws IOException { writeMessage(out, getAcknowledgementData(remote.requires13Compatiblity(), local.getNodeId(), list)); } protected int sendMessage(String action, Node remote, Node local, String data, String securityToken, String registrationUrl) throws IOException { return sendMessage( new URL(buildURL(action, remote, local, securityToken, null, registrationUrl)), data); } protected int sendMessage(URL url, String data) throws IOException { HttpURLConnection conn = openConnection(url, getBasicAuthUsername(), getBasicAuthPassword()); conn.setRequestMethod("POST"); conn.setAllowUserInteraction(false); conn.setDoOutput(true); conn.setConnectTimeout(getHttpTimeOutInMs()); conn.setReadTimeout(getHttpTimeOutInMs()); OutputStream os = conn.getOutputStream(); try { writeMessage(os, data); return conn.getResponseCode(); } finally { IOUtils.closeQuietly(os); } } public static HttpURLConnection openConnection(URL url, String username, String password) throws IOException { HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestProperty(WebConstants.HEADER_ACCEPT_CHARSET, IoConstants.ENCODING); setBasicAuthIfNeeded(conn, username, password); return conn; } public static void setBasicAuthIfNeeded(HttpURLConnection conn, String username, String password) { if (StringUtils.isNotEmpty(username) && StringUtils.isNotEmpty(password)) { String userpassword = username + ":" + password; String encodedAuthorization = new String(Base64.encodeBase64(userpassword.getBytes())); conn.setRequestProperty("Authorization", "Basic " + encodedAuthorization); } } public int getOutputStreamSize() { return engine.getParameterService().getInt(ParameterConstants.TRANSPORT_HTTP_PUSH_STREAM_SIZE); } public boolean isOutputStreamEnabled() { return engine.getParameterService().is(ParameterConstants.TRANSPORT_HTTP_PUSH_STREAM_ENABLED); } public int getHttpTimeOutInMs() { return engine.getParameterService().getInt(ParameterConstants.TRANSPORT_HTTP_TIMEOUT); } public boolean isUseCompression() { return engine.getParameterService().is(ParameterConstants.TRANSPORT_HTTP_USE_COMPRESSION_CLIENT); } public int getCompressionLevel() { return engine.getParameterService().getInt(ParameterConstants.TRANSPORT_HTTP_COMPRESSION_LEVEL); } public int getCompressionStrategy() { return engine.getParameterService().getInt(ParameterConstants.TRANSPORT_HTTP_COMPRESSION_STRATEGY); } public String getBasicAuthUsername() { return engine.getParameterService().getString(ParameterConstants.TRANSPORT_HTTP_BASIC_AUTH_USERNAME); } public String getBasicAuthPassword() { return engine.getParameterService().getString(ParameterConstants.TRANSPORT_HTTP_BASIC_AUTH_PASSWORD); } public void writeMessage(OutputStream out, String data) throws IOException { PrintWriter pw = new PrintWriter(new OutputStreamWriter(out, IoConstants.ENCODING), true); pw.println(data); pw.flush(); } public IIncomingTransport getFilePullTransport(Node remote, Node local, String securityToken, Map<String, String> requestProperties, String registrationUrl) throws IOException { HttpURLConnection conn = createGetConnectionFor(new URL(buildURL("filesync/pull", remote, local, securityToken, null, registrationUrl))); if (requestProperties != null) { for (String key : requestProperties.keySet()) { conn.addRequestProperty(key, requestProperties.get(key)); } } return new HttpIncomingTransport(conn, engine.getParameterService()); } public IIncomingTransport getPullTransport(Node remote, Node local, String securityToken, Map<String, String> requestProperties, String registrationUrl) throws IOException { HttpURLConnection conn = createGetConnectionFor(new URL(buildURL("pull", remote, local, securityToken, null, registrationUrl))); if (requestProperties != null) { for (String key : requestProperties.keySet()) { conn.addRequestProperty(key, requestProperties.get(key)); } } return new HttpIncomingTransport(conn, engine.getParameterService()); } public IIncomingTransport getAckStatusTransport(OutgoingBatch batch, Node remote, Node local, String securityToken, String registrationUrl) { try { HttpURLConnection conn = createGetConnectionFor(new URL(buildURL(String.format("ackstatus/%s", batch.getBatchId()), remote, local, securityToken, null, registrationUrl))); return new HttpIncomingTransport(conn, engine.getParameterService()); } catch (IOException ex) { throw new IoException(ex); } } @Override public void makeReservationTransport(String poolId, String channelId, Node remote, Node local, String securityToken, String registrationUrl) { try { HttpURLConnection conn = createGetConnectionFor(new URL(buildURL(String.format("%s/%s", MAKE_RESERVATION_PATH, poolId), remote, local, securityToken, channelId, registrationUrl))); conn.connect(); SymmetricUtils.analyzeResponseCode(conn.getResponseCode()); } catch (IOException ex) { throw new IoException(ex); } } public IOutgoingWithResponseTransport getPushTransport(Node remote, Node local, String securityToken, String channelId, String registrationUrl) throws IOException { URL url = new URL(buildURL("push", remote, local, securityToken, channelId, registrationUrl)); return new HttpOutgoingTransport(url, getHttpTimeOutInMs(), isUseCompression(), getCompressionStrategy(), getCompressionLevel(), getBasicAuthUsername(), getBasicAuthPassword(), isOutputStreamEnabled(), getOutputStreamSize(), false); } public IOutgoingWithResponseTransport getFilePushTransport(Node remote, Node local, String securityToken, String registrationUrl) throws IOException { URL url = new URL(buildURL("filesync/push", remote, local, securityToken, null, registrationUrl)); return new HttpOutgoingTransport(url, getHttpTimeOutInMs(), isUseCompression(), getCompressionStrategy(), getCompressionLevel(), getBasicAuthUsername(), getBasicAuthPassword(), isOutputStreamEnabled(), getOutputStreamSize(), true); } public IIncomingTransport getRegisterTransport(Node node, String registrationUrl) throws IOException { return new HttpIncomingTransport(createGetConnectionFor(new URL(buildRegistrationUrl( registrationUrl, node))), engine.getParameterService()); } public static String buildRegistrationUrl(String baseUrl, Node node) throws IOException { if (baseUrl == null) { baseUrl = ""; } StringBuilder builder = new StringBuilder(baseUrl); builder.append("/registration?"); append(builder, WebConstants.NODE_GROUP_ID, node.getNodeGroupId()); append(builder, WebConstants.EXTERNAL_ID, node.getExternalId()); append(builder, WebConstants.SYNC_URL, node.getSyncUrl()); append(builder, WebConstants.SCHEMA_VERSION, node.getSchemaVersion()); append(builder, WebConstants.DATABASE_TYPE, node.getDatabaseType()); append(builder, WebConstants.DATABASE_VERSION, node.getDatabaseVersion()); append(builder, WebConstants.SYMMETRIC_VERSION, node.getSymmetricVersion()); append(builder, WebConstants.HOST_NAME, AppUtils.getHostName()); append(builder, WebConstants.IP_ADDRESS, AppUtils.getIpAddress()); return builder.toString(); } protected HttpURLConnection createGetConnectionFor(URL url) throws IOException { HttpURLConnection conn = HttpTransportManager.openConnection(url, getBasicAuthUsername(), getBasicAuthPassword()); conn.setRequestProperty("accept-encoding", "gzip"); conn.setConnectTimeout(getHttpTimeOutInMs()); conn.setReadTimeout(getHttpTimeOutInMs()); conn.setRequestMethod("GET"); return conn; } protected static InputStream getInputStreamFrom(HttpURLConnection connection) throws IOException { String type = connection.getContentEncoding(); InputStream in = connection.getInputStream(); if (!StringUtils.isBlank(type) && type.equals("gzip")) { in = new GZIPInputStream(in); } return in; } /** * If the content is gzip'd, then uncompress. */ protected static BufferedReader getReaderFrom(HttpURLConnection connection) throws IOException { String type = connection.getContentEncoding(); InputStream in = connection.getInputStream(); if (!StringUtils.isBlank(type) && type.equals("gzip")) { in = new GZIPInputStream(in); } return TransportUtils.toReader(in); } /** * Build a url for an action. Include the nodeid and the security token. */ protected String buildURL(String action, Node remote, Node local, String securityToken, String channelId, String registrationUrl) throws IOException { return addParameterTokens((resolveURL(remote.getSyncUrl(), registrationUrl) + "/" + action), local.getNodeId(), channelId, securityToken); } protected String addParameterTokens(String base, String nodeId, String channelId, String securityToken) { StringBuilder sb = new StringBuilder(addNodeId(base, nodeId, "?")); sb.append("&"); sb.append(WebConstants.SECURITY_TOKEN); sb.append("="); sb.append(securityToken); if (isNotBlank(channelId)) { append(sb, WebConstants.CHANNEL_ID, channelId); } append(sb, WebConstants.HOST_NAME, AppUtils.getHostName()); append(sb, WebConstants.IP_ADDRESS, AppUtils.getIpAddress()); return sb.toString(); } protected String addNodeId(String base, String nodeId, String connector) { return add(base, WebConstants.NODE_ID, nodeId, connector); } protected String add(String base, String key, String value, String connector) { StringBuilder sb = new StringBuilder(base); sb.append(connector); sb.append(key); sb.append("="); try { sb.append(URLEncoder.encode(value, IoConstants.ENCODING)); } catch (UnsupportedEncodingException e) { throw new IoException(e); } return sb.toString(); } }