/*
* A simple rsync command line server implementation
*
* Copyright (C) 1996-2011 by Andrew Tridgell, Wayne Davison, and others
* Copyright (C) 2013-2015 Per Lundqvist
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.github.perlundq.yajsync.ui;
import java.io.IOException;
import java.io.PrintStream;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.charset.Charset;
import java.nio.charset.IllegalCharsetNameException;
import java.nio.charset.UnsupportedCharsetException;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.github.perlundq.yajsync.RsyncServer;
import com.github.perlundq.yajsync.internal.channels.ChannelException;
import com.github.perlundq.yajsync.internal.util.ArgumentParser;
import com.github.perlundq.yajsync.internal.util.ArgumentParsingError;
import com.github.perlundq.yajsync.internal.util.Environment;
import com.github.perlundq.yajsync.internal.util.Option;
import com.github.perlundq.yajsync.internal.util.Util;
import com.github.perlundq.yajsync.net.DuplexByteChannel;
import com.github.perlundq.yajsync.net.SSLServerChannelFactory;
import com.github.perlundq.yajsync.net.ServerChannel;
import com.github.perlundq.yajsync.net.ServerChannelFactory;
import com.github.perlundq.yajsync.net.StandardServerChannelFactory;
import com.github.perlundq.yajsync.server.module.ModuleException;
import com.github.perlundq.yajsync.server.module.ModuleProvider;
import com.github.perlundq.yajsync.server.module.Modules;
public final class YajsyncServer
{
private static final Logger _log =
Logger.getLogger(YajsyncServer.class.getName());
private static final int THREAD_FACTOR = 4;
private boolean _isTLS;
private CountDownLatch _isListeningLatch;
private int _numThreads = Runtime.getRuntime().availableProcessors() *
THREAD_FACTOR;
private int _port = RsyncServer.DEFAULT_LISTEN_PORT;
private int _verbosity;
private InetAddress _address = InetAddress.getLoopbackAddress();
private int _timeout = 0;
private ModuleProvider _moduleProvider = ModuleProvider.getDefault();
private PrintStream _out = System.out;
private PrintStream _err = System.err;
private final RsyncServer.Builder _serverBuilder = new RsyncServer.Builder();
public YajsyncServer() {}
public YajsyncServer setStandardOut(PrintStream out)
{
_out = out;
return this;
}
public YajsyncServer setStandardErr(PrintStream err)
{
_err = err;
return this;
}
public YajsyncServer setIsListeningLatch(CountDownLatch isListeningLatch)
{
_isListeningLatch = isListeningLatch;
return this;
}
public void setModuleProvider(ModuleProvider moduleProvider)
{
_moduleProvider = moduleProvider;
}
private Iterable<Option> options()
{
List<Option> options = new LinkedList<>();
options.add(Option.newStringOption(Option.Policy.OPTIONAL, "charset", "",
"which charset to use (default UTF-8)",
option -> {
String charsetName = (String) option.getValue();
try {
Charset charset = Charset.forName(charsetName);
_serverBuilder.charset(charset);
return ArgumentParser.Status.CONTINUE;
} catch (IllegalCharsetNameException |
UnsupportedCharsetException e) {
throw new ArgumentParsingError(String.format(
"failed to set character set to %s: %s",
charsetName, e));
}}));
options.add(Option.newWithoutArgument(Option.Policy.OPTIONAL, "verbose", "v",
String.format("output verbosity (default %d)",
_verbosity),
option -> {
_verbosity++;
return ArgumentParser.Status.CONTINUE;
}));
options.add(Option.newStringOption(Option.Policy.OPTIONAL, "address", "",
String.format("address to bind to (default %s)",
_address),
option -> {
try {
String name = (String) option.getValue();
_address = InetAddress.getByName(name);
return ArgumentParser.Status.CONTINUE;
} catch (UnknownHostException e) {
throw new ArgumentParsingError(e);
}}));
options.add(Option.newIntegerOption(Option.Policy.OPTIONAL, "port", "",
String.format("port number to listen on (default %d)",
_port),
option -> {
_port = (int) option.getValue();
return ArgumentParser.Status.CONTINUE;
}));
options.add(Option.newIntegerOption(Option.Policy.OPTIONAL, "threads", "",
String.format("size of thread pool (default %d)",
_numThreads),
option -> {
_numThreads = (int) option.getValue();
return ArgumentParser.Status.CONTINUE;
}));
String deferredWriteHelp = "receiver defers writing into target tempfile as long as " +
"possible to reduce I/O, at the cost of highly increased risk " +
"of the file being modified by a process already having it " +
"opened (default false)";
options.add(Option.newWithoutArgument(Option.Policy.OPTIONAL, "defer-write", "",
deferredWriteHelp,
option -> {
_serverBuilder.isDeferWrite(true);
return ArgumentParser.Status.CONTINUE;
}));
options.add(Option.newIntegerOption(Option.Policy.OPTIONAL, "timeout", "",
"set I/O timeout in seconds",
option -> {
int timeout = (int) option.getValue();
if (timeout < 0) {
throw new ArgumentParsingError(String.format(
"invalid timeout %d - must be " +
"greater than or equal to 0", timeout));
}
_timeout = timeout * 1000;
// Timeout socket operations depend on
// ByteBuffer.array and ByteBuffer.arrayOffset.
// Disable direct allocation if the resulting
// ByteBuffer won't have an array.
if (timeout > 0 &&
!Environment.hasAllocateDirectArray())
{
Environment.setAllocateDirect(false);
}
return ArgumentParser.Status.CONTINUE;
}));
options.add(Option.newWithoutArgument(Option.Policy.OPTIONAL, "tls", "",
String.format("tunnel all data over TLS/SSL " +
"(default %s)", _isTLS),
option -> {
_isTLS = true;
// SSLChannel.read and SSLChannel.write depends on
// ByteBuffer.array and ByteBuffer.arrayOffset.
// Disable direct allocation if the resulting
// ByteBuffer won't have an array.
if (!Environment.hasAllocateDirectArray()) {
Environment.setAllocateDirect(false);
}
return ArgumentParser.Status.CONTINUE;
}));
return options;
}
private Callable<Boolean> createCallable(final RsyncServer server,
final DuplexByteChannel sock,
final boolean isInterruptible)
{
return new Callable<Boolean>() {
@Override
public Boolean call() {
boolean isOK = false;
try {
Modules modules;
if (sock.peerPrincipal().isPresent()) {
if (_log.isLoggable(Level.FINE)) {
_log.fine(String.format("%s connected from %s",
sock.peerPrincipal().get(),
sock.peerAddress()));
}
modules = _moduleProvider.newAuthenticated(
sock.peerAddress(),
sock.peerPrincipal().get());
} else {
if (_log.isLoggable(Level.FINE)) {
_log.fine("got anonymous connection from " +
sock.peerAddress());
}
modules = _moduleProvider.newAnonymous(
sock.peerAddress());
}
isOK = server.serve(modules, sock, sock, isInterruptible);
} catch (ModuleException e) {
if (_log.isLoggable(Level.SEVERE)) {
_log.severe(String.format(
"Error: failed to initialise modules for " +
"principal %s using ModuleProvider %s: %s%n",
sock.peerPrincipal().get(), _moduleProvider, e));
}
} catch (ChannelException e) {
if (_log.isLoggable(Level.SEVERE)) {
_log.severe("Error: communication closed with peer: " +
e.getMessage());
}
} catch (Throwable t) {
if (_log.isLoggable(Level.SEVERE)) {
_log.log(Level.SEVERE, "", t);
}
} finally {
try {
sock.close();
} catch (IOException e) {
if (_log.isLoggable(Level.SEVERE)) {
_log.severe(String.format(
"Got error during close of socket %s: %s",
sock, e.getMessage()));
}
}
}
if (_log.isLoggable(Level.FINE)) {
_log.fine("Thread exit status: " + (isOK ? "OK" : "ERROR"));
}
return isOK;
}
};
}
public int start(String[] args) throws IOException, InterruptedException
{
ArgumentParser argsParser = ArgumentParser.newNoUnnamed(getClass().getSimpleName());
try {
argsParser.addHelpTextDestination(_out);
for (Option o : options()) {
argsParser.add(o);
}
for (Option o : _moduleProvider.options()) {
argsParser.add(o);
}
ArgumentParser.Status rc = argsParser.parse(Arrays.asList(args)); // throws ArgumentParsingError
if (rc != ArgumentParser.Status.CONTINUE) {
return rc == ArgumentParser.Status.EXIT_OK ? 0 : 1;
}
} catch (ArgumentParsingError e) {
_err.println(e.getMessage());
_err.println(argsParser.toUsageString());
return -1;
}
Level logLevel = Util.getLogLevelForNumber(Util.WARNING_LOG_LEVEL_NUM +
_verbosity);
Util.setRootLogLevel(logLevel);
ServerChannelFactory socketFactory =
_isTLS ? new SSLServerChannelFactory().setWantClientAuth(true)
: new StandardServerChannelFactory();
socketFactory.setReuseAddress(true);
//socketFactory.setKeepAlive(true);
boolean isInterruptible = !_isTLS;
ExecutorService executor = Executors.newFixedThreadPool(_numThreads);
RsyncServer server = _serverBuilder.build(executor);
try (ServerChannel listenSock = socketFactory.open(_address, _port, _timeout)) { // throws IOException
if (_isListeningLatch != null) {
_isListeningLatch.countDown();
}
while (true) {
DuplexByteChannel sock = listenSock.accept(); // throws IOException
Callable<Boolean> c = createCallable(server, sock,
isInterruptible);
executor.submit(c); // NOTE: result discarded
}
} finally {
if (_log.isLoggable(Level.INFO)) {
_log.info("shutting down...");
}
executor.shutdown();
_moduleProvider.close();
while (!executor.awaitTermination(5, TimeUnit.MINUTES)) {
_log.info("some sessions are still running, waiting for them " +
"to finish before exiting");
}
if (_log.isLoggable(Level.INFO)) {
_log.info("done");
}
}
}
}