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:
Bea 2020-08-22 12:51:33 +02:00
parent ce172c3dc4
commit 07ec036e4f
19 changed files with 667 additions and 154 deletions

19
pom.xml
View File

@ -10,6 +10,7 @@
<packaging>jar</packaging> <packaging>jar</packaging>
<dependencies> <dependencies>
<!-- todo: clean up this mess -->
<dependency> <dependency>
<groupId>ro.pippo</groupId> <groupId>ro.pippo</groupId>
<artifactId>pippo</artifactId> <artifactId>pippo</artifactId>
@ -37,8 +38,24 @@
<artifactId>snakeyaml</artifactId> <artifactId>snakeyaml</artifactId>
<version>1.21</version> <version>1.21</version>
</dependency> </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> <build>
<plugins> <plugins>
<plugin> <plugin>

View File

@ -1,18 +1,13 @@
package net.mindoverflow.webmarker; package net.mindoverflow.webmarker;
import net.mindoverflow.webmarker.runnables.StatsRunnable;
import net.mindoverflow.webmarker.utils.Cached; 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.ConfigEntries;
import net.mindoverflow.webmarker.utils.config.ConfigManager; import net.mindoverflow.webmarker.utils.config.ConfigManager;
import net.mindoverflow.webmarker.utils.messaging.Messenger; 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 ro.pippo.core.Pippo;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class WebMarker { public class WebMarker {
private static final Messenger msg = new Messenger(); private static final Messenger msg = new Messenger();
@ -33,7 +28,9 @@ public class WebMarker {
pippo.start(port); pippo.start(port);
msg.info("Started webserver."); msg.info("Started webserver.");
/* todo: enable to track ram usage
ScheduledExecutorService exec = Executors.newSingleThreadScheduledExecutor(); ScheduledExecutorService exec = Executors.newSingleThreadScheduledExecutor();
exec.scheduleAtFixedRate(new StatsRunnable(), 0, 5, TimeUnit.SECONDS); exec.scheduleAtFixedRate(new StatsRunnable(), 0, 5, TimeUnit.SECONDS);
*/
} }
} }

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -2,7 +2,10 @@ package net.mindoverflow.webmarker.utils.config;
public enum ConfigEntries public enum ConfigEntries
{ {
WEBSERVER_PORT("port", 7344); WEBSERVER_PORT("port", 7344),
JWT_SECRET("secret", "changeMe"),
;
private final String path; private final String path;
private Object value; private Object value;

View File

@ -64,7 +64,10 @@ public class ConfigManager
msg.sendLine(MessageLevel.NONE, "OK"); msg.sendLine(MessageLevel.NONE, "OK");
for(ConfigEntries entry : ConfigEntries.values()) 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() public static String getJarAbsolutePath()

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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; }
}

View File

@ -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.SQLColumn;
import net.mindoverflow.webmarker.utils.sql.primitives.SQLDataType; import net.mindoverflow.webmarker.utils.sql.primitives.SQLDataType;
public enum FDatabaseColumn public enum MDatabaseColumn
{ {
ALL(new SQLColumn("*"), null), ALL(new SQLColumn("*"), null),
USERNAME(new SQLColumn("username"), SQLDataType.VARCHAR_128), USERNAME(new SQLColumn("username"), SQLDataType.VARCHAR_128),
PASSWORD(new SQLColumn("password"), 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 SQLColumn column;
private final SQLDataType type; private final SQLDataType type;
FDatabaseColumn(SQLColumn column, SQLDataType type) MDatabaseColumn(SQLColumn column, SQLDataType type)
{ {
this.column = column; this.column = column;
this.type = type; this.type = type;

View File

@ -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; }
}

View File

@ -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);
}
}

View File

@ -7,6 +7,7 @@ import net.mindoverflow.webmarker.utils.sql.primitives.SQLDataType;
import net.mindoverflow.webmarker.utils.sql.primitives.SQLTable; import net.mindoverflow.webmarker.utils.sql.primitives.SQLTable;
import java.sql.*; import java.sql.*;
import java.util.ArrayList;
import java.util.List; import java.util.List;
public class SQLiteManager { public class SQLiteManager {
@ -37,17 +38,17 @@ public class SQLiteManager {
private void doInitialSetup() private void doInitialSetup()
{ {
for(FDatabaseTable currentTable : FDatabaseTable.values()) for(MDatabaseTable currentTable : MDatabaseTable.values())
{ {
if(!tableExists(currentTable)) if(!tableExists(currentTable))
{ {
msg.info("Creating SQLite table `" + currentTable.getTable().getTableSQLName() + "`"); msg.info("Creating SQLite table '" + currentTable.getTable().getTableSQLName() + "'");
createTable(currentTable); createTable(currentTable);
} }
} }
} }
private boolean tableExists(FDatabaseTable tableEnum) private boolean tableExists(MDatabaseTable tableEnum)
{ {
String name = tableEnum.getTable().getTableSQLName(); String name = tableEnum.getTable().getTableSQLName();
@ -70,16 +71,16 @@ public class SQLiteManager {
return false; return false;
} }
private void createTable(FDatabaseTable tableEnum) private void createTable(MDatabaseTable tableEnum)
{ {
SQLTable table = tableEnum.getTable(); SQLTable table = tableEnum.getTable();
List<FDatabaseColumn> columns = table.getColumns(); List<MDatabaseColumn> columns = table.getColumns();
List<SQLDataType> dataTypes = table.getDataTypes(); List<SQLDataType> dataTypes = table.getDataTypes();
StringBuilder query = new StringBuilder("CREATE TABLE IF NOT EXISTS ").append(table.getTableSQLName()).append(" ("); StringBuilder query = new StringBuilder("CREATE TABLE IF NOT EXISTS ").append(table.getTableSQLName()).append(" (");
int pos = 0; int pos = 0;
for(FDatabaseColumn column : columns) for(MDatabaseColumn column : columns)
{ {
query.append(column.getColumn().getColumnSQLName()).append(" ").append(dataTypes.get(pos).getSQLName()); query.append(column.getColumn().getColumnSQLName()).append(" ").append(dataTypes.get(pos).getSQLName());
pos++; pos++;
@ -91,7 +92,8 @@ public class SQLiteManager {
executeUpdate(query.toString()); executeUpdate(query.toString());
} }
private void executeUpdate(String query) @Deprecated
boolean executeUpdate(String query)
{ {
try try
{ {
@ -99,15 +101,85 @@ public class SQLiteManager {
{ {
msg.critical("Lost connection to SQLite database!"); msg.critical("Lost connection to SQLite database!");
System.exit(1); System.exit(1);
return false;
} }
Statement statement = connection.createStatement(); Statement statement = connection.createStatement();
statement.executeUpdate(query); statement.executeUpdate(query);
return true;
} catch (SQLException e) } catch (SQLException e)
{ {
e.printStackTrace(); e.printStackTrace();
msg.critical("Error executing SQLite update!"); msg.critical("Error executing SQLite update!");
System.exit(1); 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;
}
} }

View File

@ -1,6 +1,6 @@
package net.mindoverflow.webmarker.utils.sql.primitives; 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.ArrayList;
import java.util.List; import java.util.List;
@ -8,22 +8,22 @@ import java.util.List;
public class SQLTable public class SQLTable
{ {
private final String tableName; private final String tableName;
private final List<FDatabaseColumn> columns; private final List<MDatabaseColumn> columns;
private final List<SQLDataType> columnTypes = new ArrayList<>(); 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.tableName = tableName;
this.columns = columns; this.columns = columns;
for(FDatabaseColumn column : columns) for(MDatabaseColumn column : columns)
{ columnTypes.add(column.getDataType()); } { columnTypes.add(column.getDataType()); }
} }
public String getTableSQLName() public String getTableSQLName()
{ return tableName; } { return tableName; }
public List<FDatabaseColumn> getColumns() public List<MDatabaseColumn> getColumns()
{ return columns; } { return columns; }
public List<SQLDataType> getDataTypes() public List<SQLDataType> getDataTypes()

View File

@ -1,57 +1,19 @@
package net.mindoverflow.webmarker.webserver; 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.controller.ControllerApplication;
import ro.pippo.core.route.TrailingSlashHandler; import ro.pippo.core.route.TrailingSlashHandler;
import java.util.List;
public class WebApplication extends ControllerApplication { public class WebApplication extends ControllerApplication {
@Override @Override
protected void onInit() { 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(); addControllers(new LoginController());
String saveUrl = routeContext.getParameter("url").toString(); addControllers(new RegisterController());
addControllers(new StorageController());
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);
});
ANY("/.*", new TrailingSlashHandler(false)); // remove trailing slash ANY("/.*", new TrailingSlashHandler(false)); // remove trailing slash
} }

View File

@ -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!");
}
}

View File

@ -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!");
}
}

View File

@ -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;
}
}

View File

@ -1 +1,2 @@
port: 7344 port: 7344
secret: '398JC3lDk1Ckaock3dcnc938COAk9d'