package lighthouse.server;
import com.google.common.collect.*;
import com.sun.net.httpserver.*;
import joptsimple.*;
import kotlin.*;
import lighthouse.*;
import lighthouse.files.*;
import lighthouse.protocol.*;
import lighthouse.threading.*;
import org.bitcoinj.core.*;
import org.bitcoinj.core.listeners.*;
import org.bitcoinj.params.*;
import org.bitcoinj.utils.*;
import org.slf4j.Logger;
import org.slf4j.*;
import javax.net.ssl.*;
import java.io.*;
import java.lang.management.*;
import java.net.*;
import java.nio.file.*;
import java.security.*;
import java.util.logging.*;
import static lighthouse.LighthouseBackend.Mode.*;
/**
* PledgeServer is a standalone HTTP server that knows how to accept pledges and vend statuses (lists of pledges).
* It can help simplify the workflow when the project owner is capable of running a web server.
*/
public class PledgeServer {
private static final Logger log = LoggerFactory.getLogger(PledgeServer.class);
public static final short DEFAULT_LOCALHOST_PORT = (short) LHUtils.HTTP_LOCAL_TEST_PORT;
public static final String DEFAULT_PID_FILENAME = "lighthouse-server.pid";
public static void main(String[] args) throws Exception {
OptionParser parser = new OptionParser();
OptionSpec<String> dirFlag = parser.accepts("dir").withRequiredArg();
OptionSpec<String> netFlag = parser.accepts("net").withRequiredArg().defaultsTo("main");
OptionSpec<Short> portFlag = parser.accepts("port").withRequiredArg().ofType(Short.class).defaultsTo(DEFAULT_LOCALHOST_PORT);
OptionSpec<String> keystoreFlag = parser.accepts("keystore").withRequiredArg();
OptionSpec<String> pidFileFlag = parser.accepts("pidfile").withRequiredArg().defaultsTo(DEFAULT_PID_FILENAME);
parser.accepts("local-node");
OptionSpec<Void> logToConsole = parser.accepts("log-to-console");
OptionSet options = parser.parse(args);
NetworkParameters params;
switch (netFlag.value(options)) {
case "main": params = MainNetParams.get(); break;
case "test": params = TestNet3Params.get(); break;
case "regtest": params = RegTestParams.get(); break;
default:
System.err.println("--net must be one of main, test or regtest");
return;
}
HttpServer server = createServer(portFlag, keystoreFlag, options);
// Where will we store our projects and received pledges?
if (options.has(dirFlag))
AppDirectory.overrideAppDir(Paths.get(options.valueOf(dirFlag)));
Path appDir = AppDirectory.initAppDir("lighthouse-server"); // Create dir if necessary.
setupLogging(appDir, options.has(logToConsole));
Context bitcoinContext = new Context(params);
BitcoinBackend bitcoin;
try {
bitcoin = new BitcoinBackend(bitcoinContext, "Lighthouse Server", "2.0", null, false);
bitcoin.start(new DownloadProgressTracker());
} catch (ChainFileLockedException e) {
log.error("This server is already running");
return;
} catch (Exception e) {
log.error("Failed to start bitcoinj", e);
return;
}
writePidFile(appDir, pidFileFlag.value(options));
// This app is mostly single threaded. It handles all requests and state changes on a single thread.
// Speed should ideally not be an issue, as the backend blocks only rarely. If it's a problem then
// we'll have to split the backend thread from the http server thread.
AffinityExecutor.ServiceAffinityExecutor executor = new AffinityExecutor.ServiceAffinityExecutor("server");
server.setExecutor(executor);
LighthouseBackend backend = new LighthouseBackend(SERVER, params, bitcoin, executor);
backend.start();
DirectoryWatcher.watch(appDir, executor, (path, kind) -> {
if (path.toString().endsWith(LighthouseBackend.PROJECT_FILE_EXTENSION)) {
try {
if (kind == StandardWatchEventKinds.ENTRY_CREATE || kind == StandardWatchEventKinds.ENTRY_MODIFY) {
backend.importProjectFrom(path);
} else if (kind == StandardWatchEventKinds.ENTRY_DELETE) {
// TODO: Unload.
}
} catch (IOException e) {
log.error("Failed to load project from {}", path);
}
}
return Unit.INSTANCE;
});
server.createContext(LHUtils.HTTP_PATH_PREFIX, new ProjectHandler(backend));
server.createContext("/", exchange -> {
log.warn("404 Not Found: {}", exchange.getRequestURI());
exchange.sendResponseHeaders(404, -1);
});
log.info("****** STARTING WEB SERVER ON PORT {} ******", portFlag.value(options));
server.start();
}
private static void writePidFile(Path appDir, String fileNameOrPath) {
Path path = appDir.resolve(fileNameOrPath); // If fileNameOrPath starts with / then it will override the default.
Path filePath;
if (Files.isDirectory(path))
filePath = path.resolve(DEFAULT_PID_FILENAME);
else
filePath = path;
String pid = ManagementFactory.getRuntimeMXBean().getName().split("@")[0];
log.info("Our process ID is {}", pid);
LHUtils.ignoreAndLog(() -> {
Files.write(filePath, ImmutableList.of(pid), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
filePath.toFile().deleteOnExit();
});
}
private static HttpServer createServer(OptionSpec<Short> portFlag, OptionSpec<String> keystoreFlag, OptionSet options) throws Exception {
if (options.has(keystoreFlag)) {
// The amount of boilerplate this supposedly lightweight HTTPS server requires is stupid.
KeyStore keyStore = KeyStore.getInstance("JKS");
char[] password = "changeit".toCharArray();
try (FileInputStream stream = new FileInputStream(options.valueOf(keystoreFlag))) {
keyStore.load(stream, password);
}
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
kmf.init(keyStore, password);
TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
tmf.init(keyStore);
SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), new SecureRandom());
HttpsServer server = HttpsServer.create(new InetSocketAddress(portFlag.value(options)), 0);
server.setHttpsConfigurator(new HttpsConfigurator(sslContext));
return server;
} else {
return HttpServer.create(new InetSocketAddress(portFlag.value(options)), 0);
}
}
// Work around JDK misdesign/bug.
private static java.util.logging.Logger loggerPin;
private static void setupLogging(Path dir, boolean logToConsole) throws IOException {
java.util.logging.Logger logger = java.util.logging.Logger.getLogger("");
Handler handler = new FileHandler(dir.resolve("log.txt").toString(), true);
handler.setFormatter(new BriefLogFormatter());
logger.addHandler(handler);
if (logToConsole) {
logger.getHandlers()[0].setFormatter(new BriefLogFormatter());
} else {
logger.removeHandler(logger.getHandlers()[0]);
}
logger.setLevel(Level.INFO);
loggerPin = logger;
}
}