From 07ec036e4fcb59d8e34495dc3c26cceaf334cab2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenzo=20Dellac=C3=A0?= Date: Sat, 22 Aug 2020 12:51:33 +0200 Subject: [PATCH] Implement RESTful API, JWT auth, SQLite storage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This update brings a huge change to the whole system's structure. A new RESTful API has been implemented, which allows users to register, login and store data. The API only supports HTTP POST, and can be accessed via /api/v1/. Requests must contain a JSON body with the necessary entries, which are: /api/v1/register AND /api/v1/login: { "username": "username", "password": "password", "encoding": "plaintext/base64" } (Note: passwords can be encoded via "base64" or "plaintext".) /api/v1/store: { "jwt": "encrypted_key_here", "url": "https://google.com/" } The flow is: - register via /api/v1/register; - login via /api/v1/login, listen for JWT token in response; - store via /api/v1/store, by sending JWT and URL to store. The SQLite database now has 2 tables, "users" and "history". The "users" table is used to store user data: - username; - password, secured via bcrypt; - random user UUID. The "history" table is used to store browsing history: - user UUID, to identify the user; - browsed url. The secret used to sign JWTs is stored in the config.yml file. Other new features include SQL-injection protection, multiple validity/security checks on usernames and passwords, etc. Signed-off-by: Lorenzo DellacĂ  --- pom.xml | 19 +- .../net/mindoverflow/webmarker/WebMarker.java | 11 +- .../webmarker/utils/FileUtils.java | 14 ++ .../mindoverflow/webmarker/utils/URLMap.java | 55 ------ .../webmarker/utils/config/ConfigEntries.java | 5 +- .../webmarker/utils/config/ConfigManager.java | 5 +- .../utils/security/EncryptionUtils.java | 65 +++++++ .../webmarker/utils/security/SafetyCheck.java | 33 ++++ .../webmarker/utils/sql/FDatabaseTable.java | 26 --- ...tabaseColumn.java => MDatabaseColumn.java} | 8 +- .../webmarker/utils/sql/MDatabaseTable.java | 33 ++++ .../webmarker/utils/sql/MarkerSQLUtils.java | 82 +++++++++ .../webmarker/utils/sql/SQLiteManager.java | 86 +++++++++- .../utils/sql/primitives/SQLTable.java | 10 +- .../webmarker/webserver/WebApplication.java | 54 +----- .../controllers/LoginController.java | 82 +++++++++ .../controllers/RegisterController.java | 68 ++++++++ .../controllers/StorageController.java | 162 ++++++++++++++++++ src/main/resources/config.yml | 3 +- 19 files changed, 667 insertions(+), 154 deletions(-) create mode 100644 src/main/java/net/mindoverflow/webmarker/utils/FileUtils.java delete mode 100644 src/main/java/net/mindoverflow/webmarker/utils/URLMap.java create mode 100644 src/main/java/net/mindoverflow/webmarker/utils/security/EncryptionUtils.java create mode 100644 src/main/java/net/mindoverflow/webmarker/utils/security/SafetyCheck.java delete mode 100644 src/main/java/net/mindoverflow/webmarker/utils/sql/FDatabaseTable.java rename src/main/java/net/mindoverflow/webmarker/utils/sql/{FDatabaseColumn.java => MDatabaseColumn.java} (74%) create mode 100644 src/main/java/net/mindoverflow/webmarker/utils/sql/MDatabaseTable.java create mode 100644 src/main/java/net/mindoverflow/webmarker/utils/sql/MarkerSQLUtils.java create mode 100644 src/main/java/net/mindoverflow/webmarker/webserver/controllers/LoginController.java create mode 100644 src/main/java/net/mindoverflow/webmarker/webserver/controllers/RegisterController.java create mode 100644 src/main/java/net/mindoverflow/webmarker/webserver/controllers/StorageController.java diff --git a/pom.xml b/pom.xml index 7cd7ea1..a38c4dd 100644 --- a/pom.xml +++ b/pom.xml @@ -10,6 +10,7 @@ jar + ro.pippo pippo @@ -37,8 +38,24 @@ snakeyaml 1.21 - + + commons-codec + commons-codec + 1.14 + + + + at.favre.lib + bcrypt + 0.9.0 + + + com.auth0 + java-jwt + 3.10.3 + + diff --git a/src/main/java/net/mindoverflow/webmarker/WebMarker.java b/src/main/java/net/mindoverflow/webmarker/WebMarker.java index be10a16..e1985cb 100644 --- a/src/main/java/net/mindoverflow/webmarker/WebMarker.java +++ b/src/main/java/net/mindoverflow/webmarker/WebMarker.java @@ -1,18 +1,13 @@ package net.mindoverflow.webmarker; -import net.mindoverflow.webmarker.runnables.StatsRunnable; import net.mindoverflow.webmarker.utils.Cached; -import net.mindoverflow.webmarker.utils.sql.SQLiteManager; -import net.mindoverflow.webmarker.webserver.WebApplication; import net.mindoverflow.webmarker.utils.config.ConfigEntries; import net.mindoverflow.webmarker.utils.config.ConfigManager; import net.mindoverflow.webmarker.utils.messaging.Messenger; +import net.mindoverflow.webmarker.utils.sql.SQLiteManager; +import net.mindoverflow.webmarker.webserver.WebApplication; import ro.pippo.core.Pippo; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; - public class WebMarker { private static final Messenger msg = new Messenger(); @@ -33,7 +28,9 @@ public class WebMarker { pippo.start(port); msg.info("Started webserver."); + /* todo: enable to track ram usage ScheduledExecutorService exec = Executors.newSingleThreadScheduledExecutor(); exec.scheduleAtFixedRate(new StatsRunnable(), 0, 5, TimeUnit.SECONDS); + */ } } diff --git a/src/main/java/net/mindoverflow/webmarker/utils/FileUtils.java b/src/main/java/net/mindoverflow/webmarker/utils/FileUtils.java new file mode 100644 index 0000000..76f25c4 --- /dev/null +++ b/src/main/java/net/mindoverflow/webmarker/utils/FileUtils.java @@ -0,0 +1,14 @@ +package net.mindoverflow.webmarker.utils; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +public class FileUtils { + + + public static JsonObject stringToJson(String body) + { + JsonParser jsonParser = new JsonParser(); + return (JsonObject) jsonParser.parse(body); + } +} diff --git a/src/main/java/net/mindoverflow/webmarker/utils/URLMap.java b/src/main/java/net/mindoverflow/webmarker/utils/URLMap.java deleted file mode 100644 index 45e5b70..0000000 --- a/src/main/java/net/mindoverflow/webmarker/utils/URLMap.java +++ /dev/null @@ -1,55 +0,0 @@ -package net.mindoverflow.webmarker.utils; - -import java.util.ArrayList; -import java.util.List; - -public class URLMap { - - private static List ids = new ArrayList<>(); - private static List urls = new ArrayList<>(); - - public static void saveUrl(int userId, String url) - { - ids.add(userId); - urls.add(url); - System.out.println(); - System.out.println("Saved ID: " + userId + "; URL: " + url); - System.out.println("table is now:"); - for(int pos = 0; pos < ids.size(); pos++) - { - System.out.println("ID = " + ids.get(pos) + "; URL = " + urls.get(pos)); - } - System.out.println(); - } - - public static List getUserUrls(int userId) - { - ListthisUserUrls = new ArrayList<>(); - for(int pos = 0; pos < ids.size(); pos++) - { - if(userId == ids.get(pos)) - { - thisUserUrls.add(urls.get(pos)); - } - } - - return thisUserUrls; - } - - public static void dropUser(int userId) - { - List newIds = new ArrayList<>(); - List newUrls = new ArrayList<>(); - for(int pos = 0; pos < ids.size(); pos++) - { - if(ids.get(pos) != userId) - { - newIds.add(ids.get(pos)); - newUrls.add(urls.get(pos)); - } - } - - ids = newIds; - urls = newUrls; - } -} diff --git a/src/main/java/net/mindoverflow/webmarker/utils/config/ConfigEntries.java b/src/main/java/net/mindoverflow/webmarker/utils/config/ConfigEntries.java index 7a7346f..10444f6 100644 --- a/src/main/java/net/mindoverflow/webmarker/utils/config/ConfigEntries.java +++ b/src/main/java/net/mindoverflow/webmarker/utils/config/ConfigEntries.java @@ -2,7 +2,10 @@ package net.mindoverflow.webmarker.utils.config; public enum ConfigEntries { - WEBSERVER_PORT("port", 7344); + WEBSERVER_PORT("port", 7344), + JWT_SECRET("secret", "changeMe"), + + ; private final String path; private Object value; diff --git a/src/main/java/net/mindoverflow/webmarker/utils/config/ConfigManager.java b/src/main/java/net/mindoverflow/webmarker/utils/config/ConfigManager.java index 2850968..861acff 100644 --- a/src/main/java/net/mindoverflow/webmarker/utils/config/ConfigManager.java +++ b/src/main/java/net/mindoverflow/webmarker/utils/config/ConfigManager.java @@ -64,7 +64,10 @@ public class ConfigManager msg.sendLine(MessageLevel.NONE, "OK"); for(ConfigEntries entry : ConfigEntries.values()) - { msg.info(entry.name() + ": " + entry.getValue()); } + { + if(entry == ConfigEntries.JWT_SECRET) continue; // we don't want to log encryption secret key + msg.info(entry.name() + ": " + entry.getValue()); + } } public static String getJarAbsolutePath() diff --git a/src/main/java/net/mindoverflow/webmarker/utils/security/EncryptionUtils.java b/src/main/java/net/mindoverflow/webmarker/utils/security/EncryptionUtils.java new file mode 100644 index 0000000..d43f70c --- /dev/null +++ b/src/main/java/net/mindoverflow/webmarker/utils/security/EncryptionUtils.java @@ -0,0 +1,65 @@ +package net.mindoverflow.webmarker.utils.security; + +import at.favre.lib.crypto.bcrypt.BCrypt; +import org.apache.commons.codec.binary.Hex; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.io.UnsupportedEncodingException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; + +public class EncryptionUtils { + public static String hmacSha256(String key, String value) + { + byte[] keyBytes = key.getBytes(); + SecretKeySpec signingKey = new SecretKeySpec(keyBytes, "HmacSHA256"); + + try { + Mac mac = Mac.getInstance("HmacSHA256"); + mac.init(signingKey); + + byte[] rawHmac = mac.doFinal(value.getBytes()); + + byte[] hexBytes = new Hex().encode(rawHmac); + + return new String(hexBytes, "UTF-8"); + } catch (NoSuchAlgorithmException | InvalidKeyException | UnsupportedEncodingException e) { + e.printStackTrace(); + } + + return null; + } + + public static String bcrypt(String value) + { + return BCrypt.withDefaults().hashToString(12, value.toCharArray()); // todo: custom salt + } + + public static boolean bcryptMatches(String storedValue, String unencrypted) + { + BCrypt.Result result = BCrypt.verifyer().verify(unencrypted.toCharArray(), storedValue); + return result.verified; + } + + public static String handleEncoding(String encoding, String encodedPassword) + { + String password; + + switch (encoding.toLowerCase()) + { + case "plaintext": + password = encodedPassword; + break; + case "base64": + password = new String(Base64.getDecoder().decode(encodedPassword)); + break; + default: + password = ""; + break; + } + + return password; + } +} diff --git a/src/main/java/net/mindoverflow/webmarker/utils/security/SafetyCheck.java b/src/main/java/net/mindoverflow/webmarker/utils/security/SafetyCheck.java new file mode 100644 index 0000000..58ce3df --- /dev/null +++ b/src/main/java/net/mindoverflow/webmarker/utils/security/SafetyCheck.java @@ -0,0 +1,33 @@ +package net.mindoverflow.webmarker.utils.security; + +public class SafetyCheck { + + public static boolean isSafeUsername(String username) + { + // todo: allow configuration + if(!username.matches("[a-zA-Z0-9]*")) return false; + if(username.length() > 15) return false; + if(username.length() < 3) return false; + if(username.equalsIgnoreCase("null")) return false; + + return true; + } + + public static boolean isSafePassword(String password) + { + if(password.length() < 6) return false; + if(password.getBytes().length > 71) return false; // see https://github.com/patrickfav/bcrypt#handling-for-overlong-passwords + + // todo: more password security + + return true; + } + + public static boolean isValidEncoding(String encoding) + { + if(encoding.equalsIgnoreCase("base64")) return true; + if(encoding.equalsIgnoreCase("plaintext")) return true; + + return false; + } +} diff --git a/src/main/java/net/mindoverflow/webmarker/utils/sql/FDatabaseTable.java b/src/main/java/net/mindoverflow/webmarker/utils/sql/FDatabaseTable.java deleted file mode 100644 index 3d5b182..0000000 --- a/src/main/java/net/mindoverflow/webmarker/utils/sql/FDatabaseTable.java +++ /dev/null @@ -1,26 +0,0 @@ -package net.mindoverflow.webmarker.utils.sql; - - -import net.mindoverflow.webmarker.utils.sql.primitives.SQLTable; - -import java.util.ArrayList; - -public enum FDatabaseTable -{ - USERS(new SQLTable("users", // table name - new ArrayList<>(){{ // columns - add(FDatabaseColumn.USERID); - add(FDatabaseColumn.USERNAME); - add(FDatabaseColumn.PASSWORD); - }})), - - ; - - private final SQLTable table; - - FDatabaseTable(SQLTable table) - { this.table = table; } - - public SQLTable getTable() - { return table; } -} diff --git a/src/main/java/net/mindoverflow/webmarker/utils/sql/FDatabaseColumn.java b/src/main/java/net/mindoverflow/webmarker/utils/sql/MDatabaseColumn.java similarity index 74% rename from src/main/java/net/mindoverflow/webmarker/utils/sql/FDatabaseColumn.java rename to src/main/java/net/mindoverflow/webmarker/utils/sql/MDatabaseColumn.java index b0fedee..a5780cf 100644 --- a/src/main/java/net/mindoverflow/webmarker/utils/sql/FDatabaseColumn.java +++ b/src/main/java/net/mindoverflow/webmarker/utils/sql/MDatabaseColumn.java @@ -3,14 +3,14 @@ package net.mindoverflow.webmarker.utils.sql; import net.mindoverflow.webmarker.utils.sql.primitives.SQLColumn; import net.mindoverflow.webmarker.utils.sql.primitives.SQLDataType; -public enum FDatabaseColumn +public enum MDatabaseColumn { ALL(new SQLColumn("*"), null), USERNAME(new SQLColumn("username"), SQLDataType.VARCHAR_128), PASSWORD(new SQLColumn("password"), SQLDataType.VARCHAR_128), - USERID(new SQLColumn("userid"), SQLDataType.VARCHAR_128), - + USER_UUID(new SQLColumn("userid"), SQLDataType.VARCHAR_128), + WEB_DOMAIN(new SQLColumn("domain"), SQLDataType.VARCHAR_128), ; @@ -18,7 +18,7 @@ public enum FDatabaseColumn private final SQLColumn column; private final SQLDataType type; - FDatabaseColumn(SQLColumn column, SQLDataType type) + MDatabaseColumn(SQLColumn column, SQLDataType type) { this.column = column; this.type = type; diff --git a/src/main/java/net/mindoverflow/webmarker/utils/sql/MDatabaseTable.java b/src/main/java/net/mindoverflow/webmarker/utils/sql/MDatabaseTable.java new file mode 100644 index 0000000..3b3b85e --- /dev/null +++ b/src/main/java/net/mindoverflow/webmarker/utils/sql/MDatabaseTable.java @@ -0,0 +1,33 @@ +package net.mindoverflow.webmarker.utils.sql; + + +import net.mindoverflow.webmarker.utils.sql.primitives.SQLTable; + +import java.util.ArrayList; + +public enum MDatabaseTable +{ + USERS(new SQLTable("users", // table name + new ArrayList<>(){{ // columns + add(MDatabaseColumn.USER_UUID); + add(MDatabaseColumn.USERNAME); + add(MDatabaseColumn.PASSWORD); + }})), + + HISTORY(new SQLTable("history", + new ArrayList<>(){{ + add(MDatabaseColumn.USER_UUID); + add(MDatabaseColumn.WEB_DOMAIN); + + }})) + + ; + + private final SQLTable table; + + MDatabaseTable(SQLTable table) + { this.table = table; } + + public SQLTable getTable() + { return table; } +} diff --git a/src/main/java/net/mindoverflow/webmarker/utils/sql/MarkerSQLUtils.java b/src/main/java/net/mindoverflow/webmarker/utils/sql/MarkerSQLUtils.java new file mode 100644 index 0000000..0e7b6bb --- /dev/null +++ b/src/main/java/net/mindoverflow/webmarker/utils/sql/MarkerSQLUtils.java @@ -0,0 +1,82 @@ +package net.mindoverflow.webmarker.utils.sql; + +import net.mindoverflow.webmarker.utils.Cached; + +import java.util.List; +import java.util.UUID; + +public class MarkerSQLUtils { + + public static boolean addUser(UUID randomId, String name, String password) + { + + String query = "INSERT INTO " + MDatabaseTable.USERS.getTable().getTableSQLName() + " (" + + MDatabaseColumn.USER_UUID.getColumn().getColumnSQLName() + ", " + + MDatabaseColumn.USERNAME.getColumn().getColumnSQLName() + ", " + + MDatabaseColumn.PASSWORD.getColumn().getColumnSQLName() + ") VALUES (?, ?, ?);"; + + return Cached.sqlManager.executeUpdate(query, randomId.toString(), name, password); + } + + public static boolean userExists(String name) + { + String query = "SELECT " + MDatabaseColumn.USERNAME.getColumn().getColumnSQLName() + + " FROM " + MDatabaseTable.USERS.getTable().getTableSQLName() + + " WHERE " + MDatabaseColumn.USERNAME.getColumn().getColumnSQLName() + + " = ? ;"; + + List result = Cached.sqlManager.executeStatement(query, MDatabaseColumn.USERNAME, name); + return result.size() > 0; + } + + public static boolean uuidTaken(UUID randomId) + { + String query = "SELECT " + MDatabaseColumn.USER_UUID.getColumn().getColumnSQLName() + + " FROM " + MDatabaseTable.USERS.getTable().getTableSQLName() + + " WHERE " + MDatabaseColumn.USER_UUID.getColumn().getColumnSQLName() + + " = ? ;"; + + List result = Cached.sqlManager.executeStatement(query, MDatabaseColumn.USER_UUID, randomId.toString()); + if(result.size() > 0) return true; + + return false; + } + + public static UUID getUserUUID(String username) + { + String query = "SELECT " + MDatabaseColumn.USER_UUID.getColumn().getColumnSQLName() + + " FROM " + MDatabaseTable.USERS.getTable().getTableSQLName() + + " WHERE " + MDatabaseColumn.USERNAME.getColumn().getColumnSQLName() + + " = ? ;"; + + List result = Cached.sqlManager.executeStatement(query, MDatabaseColumn.USER_UUID, username); + if(result.size() != 1) return null; //todo: error! + + return UUID.fromString(result.get(0)); + } + + // todo: use UUID? + public static String getUserBcryptedPassword(String username) + { + String query = "SELECT " + MDatabaseColumn.PASSWORD.getColumn().getColumnSQLName() + + " FROM " + MDatabaseTable.USERS.getTable().getTableSQLName() + + " WHERE " + MDatabaseColumn.USERNAME.getColumn().getColumnSQLName() + + " = ? ;"; + + List result = Cached.sqlManager.executeStatement(query, MDatabaseColumn.PASSWORD, username); + if(result.size() != 1) return null; // todo: error! + + return result.get(0); + } + + public static boolean addHistoryRecord(UUID uuid, String url) + { + String query = "INSERT INTO " + MDatabaseTable.HISTORY.getTable().getTableSQLName() + " (" + + MDatabaseColumn.USER_UUID.getColumn().getColumnSQLName() + ", " + + MDatabaseColumn.WEB_DOMAIN.getColumn().getColumnSQLName() + ") VALUES (?, ?);"; + + return Cached.sqlManager.executeUpdate(query, uuid.toString(), url); + + + } +} diff --git a/src/main/java/net/mindoverflow/webmarker/utils/sql/SQLiteManager.java b/src/main/java/net/mindoverflow/webmarker/utils/sql/SQLiteManager.java index 8dac86e..4eda119 100644 --- a/src/main/java/net/mindoverflow/webmarker/utils/sql/SQLiteManager.java +++ b/src/main/java/net/mindoverflow/webmarker/utils/sql/SQLiteManager.java @@ -7,6 +7,7 @@ import net.mindoverflow.webmarker.utils.sql.primitives.SQLDataType; import net.mindoverflow.webmarker.utils.sql.primitives.SQLTable; import java.sql.*; +import java.util.ArrayList; import java.util.List; public class SQLiteManager { @@ -37,17 +38,17 @@ public class SQLiteManager { private void doInitialSetup() { - for(FDatabaseTable currentTable : FDatabaseTable.values()) + for(MDatabaseTable currentTable : MDatabaseTable.values()) { if(!tableExists(currentTable)) { - msg.info("Creating SQLite table `" + currentTable.getTable().getTableSQLName() + "`"); + msg.info("Creating SQLite table '" + currentTable.getTable().getTableSQLName() + "'"); createTable(currentTable); } } } - private boolean tableExists(FDatabaseTable tableEnum) + private boolean tableExists(MDatabaseTable tableEnum) { String name = tableEnum.getTable().getTableSQLName(); @@ -70,16 +71,16 @@ public class SQLiteManager { return false; } - private void createTable(FDatabaseTable tableEnum) + private void createTable(MDatabaseTable tableEnum) { SQLTable table = tableEnum.getTable(); - List columns = table.getColumns(); + List columns = table.getColumns(); List dataTypes = table.getDataTypes(); StringBuilder query = new StringBuilder("CREATE TABLE IF NOT EXISTS ").append(table.getTableSQLName()).append(" ("); int pos = 0; - for(FDatabaseColumn column : columns) + for(MDatabaseColumn column : columns) { query.append(column.getColumn().getColumnSQLName()).append(" ").append(dataTypes.get(pos).getSQLName()); pos++; @@ -91,7 +92,8 @@ public class SQLiteManager { executeUpdate(query.toString()); } - private void executeUpdate(String query) + @Deprecated + boolean executeUpdate(String query) { try { @@ -99,15 +101,85 @@ public class SQLiteManager { { msg.critical("Lost connection to SQLite database!"); System.exit(1); + return false; } Statement statement = connection.createStatement(); statement.executeUpdate(query); + return true; } catch (SQLException e) { e.printStackTrace(); msg.critical("Error executing SQLite update!"); System.exit(1); + return false; } } + + public boolean executeUpdate(String query, String... strings) + { + try { + + if(connection == null || connection.isClosed()) + { + msg.critical("Lost connection to SQLite database!"); + System.exit(1); + return false; + } + + PreparedStatement statement = connection.prepareStatement(query); + + int pos = 1; + for(String s : strings) + { + statement.setString(pos, s); + pos++; + } + statement.executeUpdate(); + return true; + + + } catch (SQLException throwables) { + throwables.printStackTrace(); + // todo: error + } + + return false; + } + + public List executeStatement(String query, MDatabaseColumn column, String... strings) + { + try { + if(connection == null || connection.isClosed()) + { + msg.critical("Lost connection to SQLite database!"); + System.exit(1); + return null; + } + + PreparedStatement statement = connection.prepareStatement(query); + + String columnSqlName = column.getColumn().getColumnSQLName(); + + int pos = 1; + for(String s : strings) + { + statement.setString(pos, s); + pos++; + } + + ResultSet resultSet = statement.executeQuery(); + List values = new ArrayList<>(); + while(resultSet.next()) + { + values.add(resultSet.getString(columnSqlName)); + } + return values; + + } catch (SQLException e) { + e.printStackTrace(); //todo: error + } + + return null; + } } diff --git a/src/main/java/net/mindoverflow/webmarker/utils/sql/primitives/SQLTable.java b/src/main/java/net/mindoverflow/webmarker/utils/sql/primitives/SQLTable.java index aa856d7..9d3b524 100644 --- a/src/main/java/net/mindoverflow/webmarker/utils/sql/primitives/SQLTable.java +++ b/src/main/java/net/mindoverflow/webmarker/utils/sql/primitives/SQLTable.java @@ -1,6 +1,6 @@ package net.mindoverflow.webmarker.utils.sql.primitives; -import net.mindoverflow.webmarker.utils.sql.FDatabaseColumn; +import net.mindoverflow.webmarker.utils.sql.MDatabaseColumn; import java.util.ArrayList; import java.util.List; @@ -8,22 +8,22 @@ import java.util.List; public class SQLTable { private final String tableName; - private final List columns; + private final List columns; private final List columnTypes = new ArrayList<>(); - public SQLTable(String tableName, List columns) + public SQLTable(String tableName, List columns) { this.tableName = tableName; this.columns = columns; - for(FDatabaseColumn column : columns) + for(MDatabaseColumn column : columns) { columnTypes.add(column.getDataType()); } } public String getTableSQLName() { return tableName; } - public List getColumns() + public List getColumns() { return columns; } public List getDataTypes() diff --git a/src/main/java/net/mindoverflow/webmarker/webserver/WebApplication.java b/src/main/java/net/mindoverflow/webmarker/webserver/WebApplication.java index 188975a..578a500 100644 --- a/src/main/java/net/mindoverflow/webmarker/webserver/WebApplication.java +++ b/src/main/java/net/mindoverflow/webmarker/webserver/WebApplication.java @@ -1,57 +1,19 @@ package net.mindoverflow.webmarker.webserver; -import net.mindoverflow.webmarker.utils.URLMap; +import net.mindoverflow.webmarker.webserver.controllers.LoginController; +import net.mindoverflow.webmarker.webserver.controllers.RegisterController; +import net.mindoverflow.webmarker.webserver.controllers.StorageController; import ro.pippo.controller.ControllerApplication; import ro.pippo.core.route.TrailingSlashHandler; -import java.util.List; - public class WebApplication extends ControllerApplication { @Override - protected void onInit() { - - GET("/save", routeContext -> routeContext.send("Please append an user ID")); - GET("/save/{id}", routeContext -> routeContext.send("Please append an URL")); - GET("/save/{id}/{url}", routeContext -> - { - int userId = routeContext.getParameter("id").toInt(); - String saveUrl = routeContext.getParameter("url").toString(); - - URLMap.saveUrl(userId, saveUrl); - routeContext.send("Saved!"); - }); - - GET("/get", routeContext -> routeContext.send("Please append an user ID")); - GET("/get/{id}", routeContext -> - { - int userId = routeContext.getParameter("id").toInt(); - - List urls = URLMap.getUserUrls(userId); - StringBuilder urlsSB = new StringBuilder(" | "); - for(String url : urls) - { - urlsSB.append(url).append(" | "); - } - routeContext.send(urlsSB.toString()); - - }); - - GET("/drop", routeContext -> routeContext.send("Please append an user ID")); - GET("/drop/{id}", routeContext -> - { - int userId = routeContext.getParameter("id").toInt(); - URLMap.dropUser(userId); - routeContext.send("Dropped!"); - - }); - - POST("/post", routeContext -> { - int userId = routeContext.getParameter("id").toInt(); - String url = routeContext.getParameter("url").toString(); - - routeContext.send("AAAAAAAAAAAAA " + url); - }); + protected void onInit() + { + addControllers(new LoginController()); + addControllers(new RegisterController()); + addControllers(new StorageController()); ANY("/.*", new TrailingSlashHandler(false)); // remove trailing slash } diff --git a/src/main/java/net/mindoverflow/webmarker/webserver/controllers/LoginController.java b/src/main/java/net/mindoverflow/webmarker/webserver/controllers/LoginController.java new file mode 100644 index 0000000..280cba3 --- /dev/null +++ b/src/main/java/net/mindoverflow/webmarker/webserver/controllers/LoginController.java @@ -0,0 +1,82 @@ +package net.mindoverflow.webmarker.webserver.controllers; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.algorithms.Algorithm; +import com.google.gson.JsonObject; +import net.mindoverflow.webmarker.utils.FileUtils; +import net.mindoverflow.webmarker.utils.config.ConfigEntries; +import net.mindoverflow.webmarker.utils.messaging.Messenger; +import net.mindoverflow.webmarker.utils.security.EncryptionUtils; +import net.mindoverflow.webmarker.utils.security.SafetyCheck; +import net.mindoverflow.webmarker.utils.sql.MarkerSQLUtils; +import ro.pippo.controller.Controller; +import ro.pippo.controller.POST; +import ro.pippo.controller.Path; +import ro.pippo.core.route.RouteContext; + +import java.time.ZonedDateTime; +import java.util.Date; + +@Path("/api/v1/login") +public class LoginController extends Controller +{ + private final Messenger msg = new Messenger(); + + @POST + public void login() + { + RouteContext routeContext = getRouteContext(); + + + String body = routeContext.getRequest().getBody(); + JsonObject jsonObject = FileUtils.stringToJson(body); + + String username = jsonObject.get("username").getAsString(); + String encodedPassword = jsonObject.get("password").getAsString(); + String encoding = jsonObject.get("encoding").getAsString(); + + if(!SafetyCheck.isValidEncoding(encoding)) + { + routeContext.send("Invalid encoding: '" + encoding + "'!"); + return; + } + + String password = EncryptionUtils.handleEncoding(encoding, encodedPassword); + + if(!SafetyCheck.isSafeUsername(username)) + { + routeContext.send("Invalid username!"); + return; + } + + if(!SafetyCheck.isSafePassword(password)) + { + routeContext.send("Invalid password!"); + return; + } + + if(!MarkerSQLUtils.userExists(username)) + { + routeContext.send("User does not exist!"); + return; + } + + String bcryptedStoredPassword = MarkerSQLUtils.getUserBcryptedPassword(username); + + if(!EncryptionUtils.bcryptMatches(bcryptedStoredPassword, password)) + { + routeContext.send("Wrong password!"); + return; + } + + // JWT + Algorithm algorithm = Algorithm.HMAC256((String) ConfigEntries.JWT_SECRET.getValue()); + String token = JWT.create() + .withClaim("username", username) + .withExpiresAt(Date.from(ZonedDateTime.now().plusMinutes(60).toInstant())) + .sign(algorithm); + + routeContext.send(token); + msg.info("User " + username + " logged in!"); + } +} diff --git a/src/main/java/net/mindoverflow/webmarker/webserver/controllers/RegisterController.java b/src/main/java/net/mindoverflow/webmarker/webserver/controllers/RegisterController.java new file mode 100644 index 0000000..f3d417c --- /dev/null +++ b/src/main/java/net/mindoverflow/webmarker/webserver/controllers/RegisterController.java @@ -0,0 +1,68 @@ +package net.mindoverflow.webmarker.webserver.controllers; + +import com.google.gson.JsonObject; +import net.mindoverflow.webmarker.utils.FileUtils; +import net.mindoverflow.webmarker.utils.security.EncryptionUtils; +import net.mindoverflow.webmarker.utils.security.SafetyCheck; +import net.mindoverflow.webmarker.utils.sql.MarkerSQLUtils; +import ro.pippo.controller.Controller; +import ro.pippo.controller.POST; +import ro.pippo.controller.Path; +import ro.pippo.core.route.RouteContext; + +import java.util.UUID; + +@Path("/api/v1/register") +public class RegisterController extends Controller +{ + @POST + public void register() + { + RouteContext routeContext = getRouteContext(); + + String body = routeContext.getRequest().getBody(); + JsonObject jsonObject = FileUtils.stringToJson(body); + + String username = jsonObject.get("username").getAsString(); + String encodedPassword = jsonObject.get("password").getAsString(); + String encoding = jsonObject.get("encoding").getAsString(); + + if(!SafetyCheck.isValidEncoding(encoding)) + { + routeContext.send("Invalid encoding: '" + encoding + "'!"); + return; + } + + String password = EncryptionUtils.handleEncoding(encoding, encodedPassword); + + if(!SafetyCheck.isSafeUsername(username)) + { + routeContext.send("Invalid username!"); + return; + } + + if(!SafetyCheck.isSafePassword(password)) + { + routeContext.send("Invalid password!"); + return; + } + + if(MarkerSQLUtils.userExists(username)) + { + routeContext.send("User exists!"); + return; + } + + // generate a random UUID, to identify same user in different tables + UUID randomId = UUID.randomUUID(); + + // check if the UUID is already taken by another user + while(MarkerSQLUtils.uuidTaken(randomId)) + { + randomId = UUID.randomUUID(); + } + + if(MarkerSQLUtils.addUser(randomId, username, EncryptionUtils.bcrypt(password))) routeContext.send("Added user!"); + } + +} diff --git a/src/main/java/net/mindoverflow/webmarker/webserver/controllers/StorageController.java b/src/main/java/net/mindoverflow/webmarker/webserver/controllers/StorageController.java new file mode 100644 index 0000000..7f48c4b --- /dev/null +++ b/src/main/java/net/mindoverflow/webmarker/webserver/controllers/StorageController.java @@ -0,0 +1,162 @@ +package net.mindoverflow.webmarker.webserver.controllers; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.JWTVerifier; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.exceptions.JWTVerificationException; +import com.auth0.jwt.interfaces.Claim; +import com.auth0.jwt.interfaces.DecodedJWT; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import net.mindoverflow.webmarker.utils.FileUtils; +import net.mindoverflow.webmarker.utils.config.ConfigEntries; +import net.mindoverflow.webmarker.utils.security.SafetyCheck; +import net.mindoverflow.webmarker.utils.sql.MarkerSQLUtils; +import ro.pippo.controller.Controller; +import ro.pippo.controller.POST; +import ro.pippo.controller.Path; +import ro.pippo.core.route.RouteContext; + +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.UUID; + +@Path("/api/v1/store") +public class StorageController extends Controller +{ + + private RouteContext routeContext; + + @POST + public void store() + { + // Load RouteContext + routeContext = getRouteContext(); + + // Load JSON object from body + String body = routeContext.getRequest().getBody(); + JsonObject jsonObject = FileUtils.stringToJson(body); + + // Load JWT element + JsonElement jwtElement = getJwtFromJson(jsonObject); + if(jwtElement == null) return; + + // Decrypt/Verify JWT + DecodedJWT jwt = decryptAndVerifyJwt(jwtElement); + if(jwt == null) return; + + // Load username from JWT + String username = getFromJwt(jwt, "username"); + if(username == null) return; + + // Check some stuff about the user + if(!userCheckPassed(username)) return; + + // Load UUID from username + UUID uuid = MarkerSQLUtils.getUserUUID(username); + if(uuid == null) + { + routeContext.send("Server error: missing UUID!"); + return; + } + + // Load url from Json body + String url = getFromJson(jsonObject, "url"); + if(url == null) return; + + // Verify url validity + URI confirmedUrl = verifyUrl(url); + if(confirmedUrl == null) return; + + MarkerSQLUtils.addHistoryRecord(uuid, confirmedUrl.toString()); + routeContext.send("OK!"); + + } + + private JsonElement getJwtFromJson(JsonObject jsonObject) + { + JsonElement jwtElement = jsonObject.get("jwt"); + if(jwtElement == null || jwtElement.isJsonNull()) + { + routeContext.send("Invalid JWT!"); //todo: throw exception instead? + return null; + } + return jwtElement; + } + + private DecodedJWT decryptAndVerifyJwt(JsonElement jwtElement) + { + String token = jwtElement.getAsString(); + + try { + Algorithm algorithm = Algorithm.HMAC256((String) ConfigEntries.JWT_SECRET.getValue()); + JWTVerifier jwtVerifier = JWT.require(algorithm) + .build(); + + return jwtVerifier.verify(token); + } + catch (JWTVerificationException e) + { + routeContext.send("Invalid JWT!"); + return null; + } + } + + private String getFromJwt(DecodedJWT jwt, String claimName) + { + Claim claim = jwt.getClaim(claimName); + if(claim == null || claim.isNull()) + { + routeContext.send("JWT missing '" + claimName + "' claim!"); + return null; + } + + return claim.asString(); + } + + private boolean userCheckPassed(String username) + { + if(!SafetyCheck.isSafeUsername(username)) + { + routeContext.send("Invalid username!"); + return false; + } + + if(!MarkerSQLUtils.userExists(username)) + { + routeContext.send("User does not exist!"); + return false; + } + + return true; + } + + private String getFromJson(JsonObject jsonObject, String name) + { + + JsonElement jsonElement = jsonObject.get(name); + if(jsonElement == null || jsonElement.isJsonNull()) + { + routeContext.send("JSON body missing '" + name + "' entry!"); + return null; + } + + return jsonElement.getAsString(); + } + + private URI verifyUrl(String url) + { + try { + return new URL(url).toURI(); + } catch (URISyntaxException e) { + routeContext.send("Invalid URI!"); + } + catch (MalformedURLException e) { + routeContext.send("Invalid URL!"); + } + + return null; + } +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 20a8781..62d7364 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -1 +1,2 @@ -port: 7344 \ No newline at end of file +port: 7344 +secret: '398JC3lDk1Ckaock3dcnc938COAk9d' \ No newline at end of file