Implement RESTful API, JWT auth, SQLite storage
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à <lorenzo.dellaca@mind-overflow.net>
This commit is contained in:
parent
ce172c3dc4
commit
07ec036e4f
19
pom.xml
19
pom.xml
@ -10,6 +10,7 @@
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<dependencies>
|
||||
<!-- todo: clean up this mess -->
|
||||
<dependency>
|
||||
<groupId>ro.pippo</groupId>
|
||||
<artifactId>pippo</artifactId>
|
||||
@ -37,8 +38,24 @@
|
||||
<artifactId>snakeyaml</artifactId>
|
||||
<version>1.21</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>commons-codec</groupId>
|
||||
<artifactId>commons-codec</artifactId>
|
||||
<version>1.14</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>at.favre.lib</groupId>
|
||||
<artifactId>bcrypt</artifactId>
|
||||
<version>0.9.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.auth0</groupId>
|
||||
<artifactId>java-jwt</artifactId>
|
||||
<version>3.10.3</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
|
@ -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);
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
package net.mindoverflow.webmarker.utils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class URLMap {
|
||||
|
||||
private static List<Integer> ids = new ArrayList<>();
|
||||
private static List<String> 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<String> getUserUrls(int userId)
|
||||
{
|
||||
List<String>thisUserUrls = 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<Integer> newIds = new ArrayList<>();
|
||||
List<String> 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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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()
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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; }
|
||||
}
|
@ -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;
|
@ -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; }
|
||||
}
|
@ -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<String> 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<String> 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<String> 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<String> 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);
|
||||
|
||||
|
||||
}
|
||||
}
|
@ -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<FDatabaseColumn> columns = table.getColumns();
|
||||
List<MDatabaseColumn> columns = table.getColumns();
|
||||
List<SQLDataType> 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<String> 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<String> values = new ArrayList<>();
|
||||
while(resultSet.next())
|
||||
{
|
||||
values.add(resultSet.getString(columnSqlName));
|
||||
}
|
||||
return values;
|
||||
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace(); //todo: error
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -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<FDatabaseColumn> columns;
|
||||
private final List<MDatabaseColumn> columns;
|
||||
private final List<SQLDataType> columnTypes = new ArrayList<>();
|
||||
|
||||
public SQLTable(String tableName, List<FDatabaseColumn> columns)
|
||||
public SQLTable(String tableName, List<MDatabaseColumn> 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<FDatabaseColumn> getColumns()
|
||||
public List<MDatabaseColumn> getColumns()
|
||||
{ return columns; }
|
||||
|
||||
public List<SQLDataType> getDataTypes()
|
||||
|
@ -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<String> 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
|
||||
}
|
||||
|
@ -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!");
|
||||
}
|
||||
}
|
@ -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!");
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -1 +1,2 @@
|
||||
port: 7344
|
||||
port: 7344
|
||||
secret: '398JC3lDk1Ckaock3dcnc938COAk9d'
|
Loading…
Reference in New Issue
Block a user