Commit a911525a authored by Daniel Gultsch's avatar Daniel Gultsch
Browse files

source code for the QuicksyServer as deployed on 2018/12/28

parents
.idea/
*.iml
dependency-reduced-pom.xml
target/
config.json
vouchers.json
Copyright 2018 Daniel Gultsch
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.
# QuicksyServer
QuicksyServer is the backend of the [Quicksy](https://quicksy.im)-App that handles both registration of new users (verified by SMS) and phone number to Jabber ID discovery.
{
"domain": "quicksy.im",
"twilio_auth_token": "***",
"validate_phone_numbers": true,
"min_version": "2.3.0",
"account_inactivity": "P28D",
"xmpp": {
"jid": "api.quicksy.im",
"secret": "***"
},
"web": {
"host": "127.0.0.1",
"port": 4567
},
"db": {
"username": "***",
"password": "***",
"databases": {
"ejabberd": "ejabberd",
"quicksy": "quicksy"
}
},
"pay_pal": {
"username": "***",
"password": "***",
"signature": "***"
},
"cim_auth_token": "***"
}
[Unit]
Description=Quicksy Server
[Service]
User=quicksy
Group=quicksy
ExecStart=/usr/bin/java -jar /opt/im.quicksy.server-0.1.jar -c /etc/quicksy.json
ExecReload=/bin/kill -HUP $MAINPID
[Install]
WantedBy=multi-user.target
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>commons-cli</groupId>
<artifactId>commons-cli</artifactId>
<version>1.4</version>
</dependency>
<dependency>
<groupId>com.sparkjava</groupId>
<artifactId>spark-core</artifactId>
<version>2.8.0</version>
</dependency>
<dependency>
<groupId>com.sparkjava</groupId>
<artifactId>spark-template-freemarker</artifactId>
<version>2.7.1</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>27.0-jre</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.21</version>
</dependency>
<dependency>
<groupId>com.googlecode.libphonenumber</groupId>
<artifactId>libphonenumber</artifactId>
<version>8.9.16</version>
</dependency>
<dependency>
<groupId>com.github.zafarkhaja</groupId>
<artifactId>java-semver</artifactId>
<version>0.9.0</version>
</dependency>
<dependency>
<groupId>rocks.xmpp</groupId>
<artifactId>xmpp-core-client</artifactId>
<version>0.8.0</version>
</dependency>
<dependency>
<groupId>rocks.xmpp</groupId>
<artifactId>xmpp-extensions-client</artifactId>
<version>0.8.0</version>
</dependency>
<dependency>
<groupId>com.github.inputmice</groupId>
<artifactId>xmpp-addr-adapter</artifactId>
<version>0.2.1</version>
</dependency>
<dependency>
<groupId>de.gultsch.ejabberd</groupId>
<artifactId>ejabberd-api</artifactId>
<version>0.0.10</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.5</version>
</dependency>
<dependency>
<groupId>org.sql2o</groupId>
<artifactId>sql2o</artifactId>
<version>1.6.0</version>
</dependency>
<dependency>
<groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.1.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
</dependencies>
<groupId>QuicksyServer</groupId>
<artifactId>im.quicksy.server</artifactId>
<version>0.1</version>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>im.quicksy.server.Main</mainClass>
</transformer>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
</transformers>
<minimizeJar>false</minimizeJar>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
\ No newline at end of file
/*
* Copyright 2018 Daniel Gultsch
*
* 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 im.quicksy.server;
import com.github.zafarkhaja.semver.Version;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonParseException;
import de.gultsch.xmpp.addr.adapter.Adapter;
import im.quicksy.server.json.DurationDeserializer;
import im.quicksy.server.json.VersionDeserializer;
import rocks.xmpp.addr.Jid;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.time.Duration;
import java.util.HashMap;
import java.util.Optional;
public class Configuration {
private static File FILE = new File("config.json");
private static Configuration INSTANCE;
private XMPP xmpp = new XMPP();
private Web web = new Web();
private DB db = new DB();
private PayPal payPal = new PayPal();
private String twilioAuthToken;
private String cimAuthToken;
private Version minVersion;
private Duration accountInactivity = Duration.ofDays(28);
private String domain;
private boolean validatePhoneNumbers = true;
private boolean preventRegistration = true;
private Configuration() {
}
public synchronized static void setFilename(String filename) throws FileNotFoundException {
if (INSTANCE != null) {
throw new IllegalStateException("Unable to set filename after instance has been created");
}
Configuration.FILE = new File(filename);
if (!Configuration.FILE.exists()) {
throw new FileNotFoundException();
}
}
public synchronized static Configuration getInstance() {
if (INSTANCE == null) {
INSTANCE = load();
}
return INSTANCE;
}
private static Configuration load() {
final GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES);
Adapter.register(gsonBuilder);
gsonBuilder.registerTypeAdapter(Version.class, new VersionDeserializer());
gsonBuilder.registerTypeAdapter(Duration.class, new DurationDeserializer());
final Gson gson = gsonBuilder.create();
try {
System.out.println("Reading configuration from " + FILE.getAbsolutePath());
return gson.fromJson(new FileReader(FILE), Configuration.class);
} catch (FileNotFoundException e) {
throw new RuntimeException("Configuration file not found");
} catch (JsonParseException e) {
throw new RuntimeException("Unable to parse configuration file", e);
}
}
public synchronized static boolean reload() {
if (Configuration.FILE.exists()) {
final Configuration newConfig = load();
if (!newConfig.check()) {
throw new RuntimeException("Configuration file is incomplete");
}
INSTANCE = newConfig;
return true;
} else {
return false;
}
}
public boolean check() {
return domain != null && minVersion != null && web != null && xmpp != null && xmpp.check() && db != null && db.check() && payPal != null && payPal.check();
}
public Duration getAccountInactivity() {
return accountInactivity;
}
public boolean isPreventRegistration() {
return preventRegistration;
}
public boolean isValidatePhoneNumbers() {
return validatePhoneNumbers;
}
public XMPP getXmpp() {
return xmpp;
}
public Web getWeb() {
return web;
}
public DB getDb() {
return db;
}
public String getTwilioAuthToken() {
return twilioAuthToken;
}
public Optional<String> getCimAuthToken() {
return Optional.ofNullable(cimAuthToken);
}
public String getDomain() {
return domain;
}
public PayPal getPayPal() {
return payPal;
}
public File getVoucherFile() {
return new File(FILE.getParentFile(), "vouchers.json");
}
public Version getMinVersion() {
return minVersion;
}
public static class XMPP {
private String host = "localhost";
private int port = 5347;
private Jid jid;
private String secret;
public String getHost() {
return host;
}
public int getPort() {
return port;
}
public Jid getJid() {
return jid;
}
public String getSecret() {
return secret;
}
public boolean check() {
return secret != null && jid != null;
}
}
public static class Web {
private String host = "127.0.0.1";
private int port = 4567;
public String getHost() {
return host;
}
public int getPort() {
return port;
}
}
public static class DB {
private String host = "127.0.0.1";
private int port = 3306;
private String username;
private String password;
private HashMap<String, String> databases;
private int poolSize = 1;
public String getHost() {
return host;
}
public int getPort() {
return port;
}
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
public String getJdbcUri(String database) {
return String.format("jdbc:mariadb://%s:%d/%s?characterEncoding=utf8", host, port, databases.get(database));
}
public boolean check() {
return username != null && password != null && databases != null && databases.containsKey("ejabberd") && databases.containsKey("quicksy");
}
public int getPoolSize() {
return poolSize;
}
}
public static class PayPal {
private String username;
private String password;
private String signature;
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
public String getSignature() {
return signature;
}
public boolean check() {
return username != null && password != null && signature != null;
}
}
}
/*
* Copyright 2018 Daniel Gultsch
*
* 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 im.quicksy.server;
import im.quicksy.server.controller.*;
import im.quicksy.server.xmpp.synchronization.Entry;
import im.quicksy.server.xmpp.synchronization.PhoneBook;
import org.apache.commons.cli.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import rocks.xmpp.core.XmppException;
import rocks.xmpp.core.session.Extension;
import rocks.xmpp.core.session.XmppSessionConfiguration;
import rocks.xmpp.core.session.debug.ConsoleDebugger;
import rocks.xmpp.extensions.bytestreams.s5b.model.Socks5ByteStream;
import rocks.xmpp.extensions.component.accept.ExternalComponent;
import rocks.xmpp.extensions.disco.ServiceDiscoveryManager;
import rocks.xmpp.extensions.disco.model.info.Identity;
import rocks.xmpp.extensions.muc.model.Muc;
import spark.TemplateEngine;
import spark.template.freemarker.FreeMarkerEngine;
import sun.misc.Signal;
import java.io.FileNotFoundException;
import java.util.Properties;
import static spark.Spark.*;
public class Main {
private static final Logger LOGGER = LoggerFactory.getLogger(Main.class);
private static final Options options;
private static final int RETRY_INTERVAL = 5000;
static {
options = new Options();
options.addOption(new Option("c", "config", true, "Path to the config file"));
options.addOption(new Option("v", "verbose", false, "Set log level to debug"));
options.addOption(new Option("x", "xmpp", false, "Print stanzas"));
options.addOption(new Option("h", "help", false, "Show this help"));
}
public static void main(String... args) {
try {
main(new DefaultParser().parse(options, args));
} catch (ParseException e) {
printHelp();
}
}
private static void main(CommandLine commandLine) {
if (commandLine.hasOption('h')) {
printHelp();
return;
}
final String configFilename = commandLine.getOptionValue("config");
if (configFilename != null) {
try {
Configuration.setFilename(configFilename);
} catch (FileNotFoundException e) {
System.err.println(e.getMessage());
return;
}
}
if (!Configuration.getInstance().check()) {
LOGGER.error("Configuration file is incomplete");
return;
}
logConfigurationInfo();
if (commandLine.hasOption("v")) {
Properties properties = System.getProperties();
properties.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "debug");
}
Signal.handle(new Signal("HUP"), signal -> {
try {
if (Configuration.reload()) {
LOGGER.info("reloaded config");
logConfigurationInfo();
} else {
LOGGER.error("unable to reload config. config file has moved");
}
} catch (RuntimeException e) {
LOGGER.error("Unable to load config file - " + e.getMessage());
}
});
setupWebServer();
setupXmppComponent(commandLine.hasOption("x"));
}
private static void printHelp() {
HelpFormatter formatter = new HelpFormatter();
formatter.printHelp("java -jar im.quicksy.server-0.1.jar", options);
}
private static void logConfigurationInfo() {
LOGGER.info("validating phone numbers: " + Boolean.toString(Configuration.getInstance().isValidatePhoneNumbers()));
LOGGER.info("prevent registration when logged in with another device: " + Boolean.toString(Configuration.getInstance().isPreventRegistration()));
LOGGER.info("minimum client version: " + Configuration.getInstance().getMinVersion().toString());
LOGGER.info("treat accounts as inactive after: " + Configuration.getInstance().getAccountInactivity());
}
private static void setupWebServer() {
ipAddress(Configuration.getInstance().getWeb().getHost());
port(Configuration.getInstance().getWeb().getPort());
final TemplateEngine templateEngine = new FreeMarkerEngine();
path("/api", () -> {