/* * Copyright 2013 Google Inc. * * 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 com.google.devcoin.protocols.channels; import java.io.IOException; import java.math.BigInteger; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.SocketAddress; import javax.annotation.Nonnull; import javax.annotation.Nullable; import com.google.devcoin.core.Sha256Hash; import com.google.devcoin.core.TransactionBroadcaster; import com.google.devcoin.core.Wallet; import com.google.devcoin.protocols.niowrapper.ProtobufParser; import com.google.devcoin.protocols.niowrapper.ProtobufParserFactory; import com.google.devcoin.protocols.niowrapper.ProtobufServer; import org.devcoin.paymentchannel.Protos; import static com.google.common.base.Preconditions.checkNotNull; /** * Implements a listening TCP server that can accept connections from payment channel clients, and invokes the provided * event listeners when new channels are opened or payments arrive. This is the highest level class in the payment * channels API. Internally, sends protobuf messages to/from a newly created {@link PaymentChannelServer}. */ public class PaymentChannelServerListener { // The wallet and peergroup which are used to complete/broadcast transactions private final Wallet wallet; private final TransactionBroadcaster broadcaster; // The event handler factory which creates new ServerConnectionEventHandler per connection private final HandlerFactory eventHandlerFactory; private final BigInteger minAcceptedChannelSize; private final ProtobufServer server; /** * A factory which generates connection-specific event handlers. */ public static interface HandlerFactory { /** * Called when a new connection completes version handshake to get a new connection-specific listener. * If null is returned, the connection is immediately closed. */ @Nullable public ServerConnectionEventHandler onNewConnection(SocketAddress clientAddress); } private class ServerHandler { public ServerHandler(final SocketAddress address, final int timeoutSeconds) { paymentChannelManager = new PaymentChannelServer(broadcaster, wallet, minAcceptedChannelSize, new PaymentChannelServer.ServerConnection() { @Override public void sendToClient(Protos.TwoWayChannelMessage msg) { socketProtobufHandler.write(msg); } @Override public void destroyConnection(PaymentChannelCloseException.CloseReason reason) { if (closeReason != null) closeReason = reason; socketProtobufHandler.closeConnection(); } @Override public void channelOpen(Sha256Hash contractHash) { socketProtobufHandler.setSocketTimeout(0); eventHandler.channelOpen(contractHash); } @Override public void paymentIncrease(BigInteger by, BigInteger to) { eventHandler.paymentIncrease(by, to); } }); protobufHandlerListener = new ProtobufParser.Listener<Protos.TwoWayChannelMessage>() { @Override public synchronized void messageReceived(ProtobufParser handler, Protos.TwoWayChannelMessage msg) { paymentChannelManager.receiveMessage(msg); } @Override public synchronized void connectionClosed(ProtobufParser handler) { paymentChannelManager.connectionClosed(); if (closeReason != null) eventHandler.channelClosed(closeReason); else eventHandler.channelClosed(PaymentChannelCloseException.CloseReason.CONNECTION_CLOSED); eventHandler.setConnectionChannel(null); } @Override public synchronized void connectionOpen(ProtobufParser handler) { ServerConnectionEventHandler eventHandler = eventHandlerFactory.onNewConnection(address); if (eventHandler == null) handler.closeConnection(); else { ServerHandler.this.eventHandler = eventHandler; paymentChannelManager.connectionOpen(); } } }; socketProtobufHandler = new ProtobufParser<Protos.TwoWayChannelMessage> (protobufHandlerListener, Protos.TwoWayChannelMessage.getDefaultInstance(), Short.MAX_VALUE, timeoutSeconds*1000); } private PaymentChannelCloseException.CloseReason closeReason; // The user-provided event handler @Nonnull private ServerConnectionEventHandler eventHandler; // The payment channel server which does the actual payment channel handling private final PaymentChannelServer paymentChannelManager; // The connection handler which puts/gets protobufs from the TCP socket private final ProtobufParser<Protos.TwoWayChannelMessage> socketProtobufHandler; // The listener which connects to socketProtobufHandler private final ProtobufParser.Listener<Protos.TwoWayChannelMessage> protobufHandlerListener; } /** * Binds to the given port and starts accepting new client connections. * @throws Exception If binding to the given port fails (eg SocketException: Permission denied for privileged ports) */ public void bindAndStart(int port) throws Exception { server.start(new InetSocketAddress(port)); } /** * Sets up a new payment channel server which listens on the given port. * * @param broadcaster The PeerGroup on which transactions will be broadcast - should have multiple connections. * @param wallet The wallet which will be used to complete transactions * @param timeoutSeconds The read timeout between messages. This should accommodate latency and client ECDSA * signature operations. * @param minAcceptedChannelSize The minimum amount of coins clients must lock in to create a channel. Clients which * are unwilling or unable to lock in at least this value will immediately disconnect. * For this reason, a fairly conservative value (in terms of average value spent on a * channel) should generally be chosen. * @param eventHandlerFactory A factory which generates event handlers which are created for each new connection */ public PaymentChannelServerListener(TransactionBroadcaster broadcaster, Wallet wallet, final int timeoutSeconds, BigInteger minAcceptedChannelSize, HandlerFactory eventHandlerFactory) throws IOException { this.wallet = checkNotNull(wallet); this.broadcaster = checkNotNull(broadcaster); this.eventHandlerFactory = checkNotNull(eventHandlerFactory); this.minAcceptedChannelSize = checkNotNull(minAcceptedChannelSize); server = new ProtobufServer(new ProtobufParserFactory() { @Override public ProtobufParser getNewParser(InetAddress inetAddress, int port) { return new ServerHandler(new InetSocketAddress(inetAddress, port), timeoutSeconds).socketProtobufHandler; } }); } /** * <p>Closes all client connections currently connected gracefully.</p> * * <p>Note that this does <i>not</i> close the actual payment channels (and broadcast payment transactions), which * must be done using the {@link StoredPaymentChannelServerStates} which manages the states for the associated * wallet.</p> */ public void close() { try { server.stop(); } catch (InterruptedException e) { throw new RuntimeException(e); } } }