Skip to content
Snippets Groups Projects
Commit b4038162 authored by Håkon Gylterud's avatar Håkon Gylterud
Browse files

No git history for Inchat.

parents
Branches master
No related tags found
No related merge requests found
Showing
with 1566 additions and 0 deletions
# INCHAT – The INsecure CHAT application
Welcome to this second mandatory assignment of INF226.
In this assignment you will be analysing the security
of a program called inChat – a very simple chat application,
in the shape of a [Jetty](https://www.eclipse.org/jetty/)
web application.
inChat has been especially crafted to contain a number
of security flaws. You can imagine that it has been
programmed by a less competent collegue, and that after
numerous securiy incidents, your organisation has decided
that you – a competent security professional – should take
some time to secure the app.
## Getting and building the project
Log into [`git.app.uib.no`](https://git.app.uib.no/Hakon.Gylterud/inf226-2020-inchat) and make your
own fork of the project there. *Make sure your fork is private!*
You can then clone your repo to your own machine.
To build the project you can use Maven on the command line, or configure
your IDE to use Maven to build the project.
- `mvn compile` builds the project
- `mvn test` runs the tests. (There are only a few unit test – feel free to add more).
- `mvn exec:java` runs the web app.
Once the web-app is running, you can access it on [`localhost:8080`](http://localhost:8080/).
## Updates
Most likely the source code of the project will be updated by Håkon
while you are working on it. Therefore, it will be part of
your assignment to merge any new commits into your own branch.
## Improvements?
Have you found a non-security related bug?
Feel free to open an issue on the project GitLab page.
The best way is to make a separate `git branch` for these
changes, which do not contain your sulutions.
(This is ofcourse completely volountary – and not a graded
part of the assignment)
If you want to add your own features to the chat app - feel free
to do so! If you want to share them, contact Håkon and we can
incorporate them into the main repo.
<!DOCTYPE html>
<html lang="en-GB">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
<link rel="stylesheet" href="style.css">
<title>Log in to Inforum</title>
</head>
<body>
<h1 class="topic">InChat</h1>
<div class="actionbar">
<a class="action" href="/register">Register</a>
<a class="action" href="/login">Login</a>
</div>
<p>Welcome to InChat, the insecure chatting platform!</p>
</body>
</html>
<!DOCTYPE html>
<html lang="en-GB">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
<link rel="stylesheet" href="style.css">
<title>Log in to Inforum</title>
</head>
<body>
<h1 class="topic">Login to inforum</h1>
<form class="login" action="/" method="post">
<div class="username"><input type="text" name="username" placeholder="Username"></div>
<div class="password"><input type="password" name="password" placeholder="Password"></div>
<div class="loginbutton"><input type="submit" name="login" value="Login"></div>
</form>
<p>No account? <a href="/register">Register here!</a></p>
</body>
</html>
<!DOCTYPE html>
<html lang="en-GB">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
<link rel="stylesheet" href="style.css">
<title>Log in to Inforum</title>
</head>
<body>
<h1 class="topic">InChat</h1>
<div class="actionbar">
<a class="action" href="/join">Join a channel!</a>
<a class="action" href="/logout">Logout</a>
</div>
<div class="main">
<ul class="chanlist">
<li>#Foochan</li>
<li><b>#Barchan</b></li>
</ul>
<main role="main" class="channel">
<div class="entry">
<div class="user">Joe</div>
<div class="text"><p>I started this thread in order to test the
layout of this web page.</p>
<p>I expect to write a lot here later – especially about lorem ipsum.
But for the moment I will content myself with a few lines.</p>
</div>
<div class="controls">
<a class="action" href="/delete">Delete</a>
<a class="action" href="/edit">Edit</a>
</div>
</div>
<div class="entry">
<div class="user">Delia</div>
<div class="text"><p>Hey, Joe!</p>
</div>
<div class="controls">
<a class="action" href="/delete">Delete</a>
<a class="action" href="/edit">Edit</a>
<div>
</div>
</main>
<aside class="chanmenu">
Here comes the menu.
</aside>
</div>
</body>
</html>
pom.xml 0 → 100644
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>inf226</groupId>
<artifactId>inchat</artifactId>
<version>0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>inChat</name>
<properties>
<jettyVersion>9.4.9.v20180320</jettyVersion>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<version>${jettyVersion}</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.4.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.4.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.28.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.lambdaworks/scrypt -->
<dependency>
<groupId>com.lambdaworks</groupId>
<artifactId>scrypt</artifactId>
<version>1.4.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>1.6.0</version>
<executions>
<execution><goals><goal>java</goal></goals></execution>
</executions>
<configuration>
<mainClass>inf226.inchat.Handler</mainClass>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<compilerArgs>
<arg>-Xlint:all,-options,-path</arg>
</compilerArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.1</version>
</plugin>
</plugins>
</build>
</project>
<!DOCTYPE html>
<html lang="en-GB">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
<link rel="stylesheet" href="style.css">
<title>Register an account on InChat</title>
</head>
<body>
<h1 class="topic">Register for InChat</h1>
<form class="register" action="/" method="post">
<p>Chose a username and a password:</p>
<div class="username"><input type="text" name="username" placeholder="Username"></div>
<div class="password"><input type="password" name="password" placeholder="Password"></div>
<div class="password"><input type="password" name="password_repeat" placeholder="Retype password"></div>
<div class="button"><input type="submit" name="register" value="Register"></div>
</form>
</body>
</html>
async function subscribe(id,vers) {
let response = await fetch("/subscribe/" + id +"?version=" + vers);
if (response.status == 502) {
// Status 502 is a connection timeout error,
// may happen when the connection was pending for too long,
// and the remote server or a proxy closed it
// let's reconnect
await subscribe();
} else if (response.status != 200) {
// An error - let's show it
alert(response.statusText);
// Reconnect in one second
await new Promise(resolve => setTimeout(resolve, 1000));
await subscribe();
} else {
// Get and show the message
let message = await response.text();
let lineend = message.indexOf("\n")
let newvers = message.substr(0, lineend);
let html = message.substr(lineend+1);
let chan = document.getElementById("channel");
let chanevents = document.getElementById("chanevents");
chan.replaceChild(htmlToElem(html),chanevents);
// Call subscribe() again to get the next message
await subscribe(id,newvers);
}
}
function htmlToElem(html) {
let temp = document.createElement('template');
html = html.trim(); // Never return a space text node as a result
temp.innerHTML = html;
return temp.content.firstChild;
}
function submitOnEnter(event){
if(event.which === 13 && !event.shiftKey){
event.target.form.submit();
//event.target.form.dispatchEvent(new Event("submit", {cancelable: true}));
event.preventDefault();
}
}
package inf226.inchat;
import inf226.util.immutable.List;
import inf226.util.Pair;
import inf226.storage.*;
/**
* The Account class holds all information private to
* a specific user.
**/
public final class Account {
/*
* A channel consists of a User object of public account info,
* and a list of channels which the user can post to.
*/
public final Stored<User> user;
public final List<Pair<String,Stored<Channel>>> channels;
public final String password;
public Account(final Stored<User> user,
final List<Pair<String,Stored<Channel>>> channels,
final String password) {
this.user = user;
this.channels = channels;
this.password = password;
}
/**
* Create a new Account.
*
* @param user The public User profile for this user.
* @param password The login password for this account.
**/
public static Account create(final Stored<User> user,
final String password) {
return new Account(user,List.empty(), password);
}
/**
* Join a channel with this account.
*
* @return A new account object with the cannnel added.
*/
public Account joinChannel(final String alias,
final Stored<Channel> channel) {
Pair<String,Stored<Channel>> entry
= new Pair<String,Stored<Channel>>(alias,channel);
return new Account
(user,
List.cons(entry,
channels),
password);
}
/**
* Check weather if a string is a correct password for
* this account.
*
* @return true if password matches.
*/
public boolean checkPassword(String password) {
return this.password.equals(password);
}
}
package inf226.inchat;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.time.Instant;
import java.util.UUID;
import inf226.storage.*;
import inf226.util.immutable.List;
import inf226.util.*;
/**
* This class stores accounts in the database.
*/
public final class AccountStorage
implements Storage<Account,SQLException> {
final Connection connection;
final Storage<User,SQLException> userStore;
final Storage<Channel,SQLException> channelStore;
/**
* Create a new account storage.
*
* @param connection The connection to the SQL database.
* @param userStore The storage for User data.
* @param channelStore The storage for channels.
*/
public AccountStorage(Connection connection,
Storage<User,SQLException> userStore,
Storage<Channel,SQLException> channelStore)
throws SQLException {
this.connection = connection;
this.userStore = userStore;
this.channelStore = channelStore;
connection.createStatement()
.executeUpdate("CREATE TABLE IF NOT EXISTS Account (id TEXT PRIMARY KEY, version TEXT, user TEXT, password TEXT, FOREIGN KEY(user) REFERENCES User(id) ON DELETE CASCADE)");
connection.createStatement()
.executeUpdate("CREATE TABLE IF NOT EXISTS AccountChannel (account TEXT, channel TEXT, alias TEXT, ordinal INTEGER, PRIMARY KEY(account,channel), FOREIGN KEY(account) REFERENCES Account(id) ON DELETE CASCADE, FOREIGN KEY(channel) REFERENCES Channel(id) ON DELETE CASCADE)");
}
@Override
public Stored<Account> save(Account account)
throws SQLException {
final Stored<Account> stored = new Stored<Account>(account);
String sql =
"INSERT INTO Account VALUES('" + stored.identity + "','"
+ stored.version + "','"
+ account.user.identity + "','"
+ account.password + "')";
connection.createStatement().executeUpdate(sql);
// Write the list of channels
final Maybe.Builder<SQLException> exception = Maybe.builder();
final Mutable<Integer> ordinal = new Mutable<Integer>(0);
account.channels.forEach(element -> {
String alias = element.first;
Stored<Channel> channel = element.second;
final String msql
= "INSERT INTO AccountChannel VALUES('" + stored.identity + "','"
+ channel.identity + "','"
+ alias + "','"
+ ordinal.get().toString() + "')";
try { connection.createStatement().executeUpdate(msql); }
catch (SQLException e) { exception.accept(e) ; }
ordinal.accept(ordinal.get() + 1);
});
Util.throwMaybe(exception.getMaybe());
return stored;
}
@Override
public synchronized Stored<Account> update(Stored<Account> account,
Account new_account)
throws UpdatedException,
DeletedException,
SQLException {
final Stored<Account> current = get(account.identity);
final Stored<Account> updated = current.newVersion(new_account);
if(current.version.equals(account.version)) {
String sql = "UPDATE Account SET" +
" (version,user) =('"
+ updated.version + "','"
+ new_account.user.identity
+ "') WHERE id='"+ updated.identity + "'";
connection.createStatement().executeUpdate(sql);
// Rewrite the list of channels
connection.createStatement().executeUpdate("DELETE FROM AccountChannel WHERE account='" + account.identity + "'");
final Maybe.Builder<SQLException> exception = Maybe.builder();
final Mutable<Integer> ordinal = new Mutable<Integer>(0);
new_account.channels.forEach(element -> {
String alias = element.first;
Stored<Channel> channel = element.second;
final String msql
= "INSERT INTO AccountChannel VALUES('" + account.identity + "','"
+ channel.identity + "','"
+ alias + "','"
+ ordinal.get().toString() + "')";
try { connection.createStatement().executeUpdate(msql); }
catch (SQLException e) { exception.accept(e) ; }
ordinal.accept(ordinal.get() + 1);
});
Util.throwMaybe(exception.getMaybe());
} else {
throw new UpdatedException(current);
}
return updated;
}
@Override
public synchronized void delete(Stored<Account> account)
throws UpdatedException,
DeletedException,
SQLException {
final Stored<Account> current = get(account.identity);
if(current.version.equals(account.version)) {
String sql = "DELETE FROM Account WHERE id ='" + account.identity + "'";
connection.createStatement().executeUpdate(sql);
} else {
throw new UpdatedException(current);
}
}
@Override
public Stored<Account> get(UUID id)
throws DeletedException,
SQLException {
final String accountsql = "SELECT version,user,password FROM Account WHERE id = '" + id.toString() + "'";
final String channelsql = "SELECT channel,alias,ordinal FROM AccountChannel WHERE account = '" + id.toString() + "' ORDER BY ordinal DESC";
final Statement accountStatement = connection.createStatement();
final Statement channelStatement = connection.createStatement();
final ResultSet accountResult = accountStatement.executeQuery(accountsql);
final ResultSet channelResult = channelStatement.executeQuery(channelsql);
if(accountResult.next()) {
final UUID version = UUID.fromString(accountResult.getString("version"));
final UUID userid =
UUID.fromString(accountResult.getString("user"));
final String password =
accountResult.getString("password");
final Stored<User> user = userStore.get(userid);
// Get all the channels associated with this account
final List.Builder<Pair<String,Stored<Channel>>> channels = List.builder();
while(channelResult.next()) {
final UUID channelId =
UUID.fromString(channelResult.getString("channel"));
final String alias = channelResult.getString("alias");
channels.accept(
new Pair<String,Stored<Channel>>(
alias,channelStore.get(channelId)));
}
return (new Stored<Account>(new Account(user,channels.getList(),password),id,version));
} else {
throw new DeletedException();
}
}
/**
* Look up an account based on their username.
*/
public Stored<Account> lookup(String username)
throws DeletedException,
SQLException {
final String sql = "SELECT Account.id from Account INNER JOIN User ON user=User.id where User.name='" + username + "'";
System.err.println(sql);
final Statement statement = connection.createStatement();
final ResultSet rs = statement.executeQuery(sql);
if(rs.next()) {
final UUID identity =
UUID.fromString(rs.getString("id"));
return get(identity);
}
throw new DeletedException();
}
}
package inf226.inchat;
import inf226.util.immutable.List;
import inf226.storage.Stored;
import java.time.Instant;
import java.util.UUID;
/**
* The Channel class represents a channel.
*/
public final class Channel {
public final String name;
public final List<Stored<Event>> events;
/**
* Construct a Channel object from name and events.
*/
public Channel(String name, List<Stored<Event>> events) {
this.name=name;
this.events=events;
}
/**
* Post a new event to the channel.
*/
public Channel postEvent(Stored<Event> event) {
return new Channel(name, List.cons(event,events));
}
/**
* The Event class represents different kinds of events
* in a channel, such as "join events" and "message events".
*/
public static class Event {
public static enum Type {
message(0),join(1);
public final Integer code;
Type(Integer code){this.code=code;}
public static Type fromInteger(Integer i) {
if (i.equals(0))
return message;
else if (i.equals(1))
return join;
else
throw new IllegalArgumentException("Invalid Channel.Event.Type code:" + i);
}
};
public final UUID channel;
public final Type type;
public final Instant time;
public final String sender;
public final String message;
/**
* Copy constructor
*/
public Event(UUID channel, Instant time, String sender, Type type, String message) {
if (time == null) {
throw new IllegalArgumentException("Event time cannot be null");
}
if (type.equals(message) && message == null) {
throw new IllegalArgumentException("null in Event creation");
}
this.channel=channel;
this.time =time;
this.sender =sender;
this.type =type;
this.message=message;
}
/**
* Create a message event, which represents a user writing to the channel.
*/
public static Event createMessageEvent(UUID channel, Instant time, String sender, String message) {
return new Event( channel,
time,
sender,
Event.Type.message,
message);
}
/**
* Create a message event, which represents a user joining the channel.
*/
public static Event createJoinEvent(UUID channel,Instant time, String user) {
return new Event( channel,
time,
user,
Event.Type.join,
null);
}
/**
* Create a new event with a different message.
*/
public Event setMessage(String message) {
return new Event(channel,time,sender,type,message);
}
}
}
package inf226.inchat;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.time.Instant;
import java.util.UUID;
import java.util.TreeMap;
import java.util.Map;
import java.util.function.Consumer;
import inf226.storage.*;
import inf226.util.immutable.List;
import inf226.util.*;
/**
* This class stores Channels in a SQL database.
*/
public final class ChannelStorage
implements Storage<Channel,SQLException> {
final Connection connection;
/* The waiters object represent the callbacks to
* make when the channel is updated.
*/
private Map<UUID,List<Consumer<Stored<Channel>>>> waiters
= new TreeMap<UUID,List<Consumer<Stored<Channel>>>>();
public final EventStorage eventStore;
public ChannelStorage(Connection connection)
throws SQLException {
this.connection = connection;
this.eventStore = new EventStorage(connection);
connection.createStatement()
.executeUpdate("CREATE TABLE IF NOT EXISTS Channel (id TEXT PRIMARY KEY, version TEXT, name TEXT)");
}
@Override
public Stored<Channel> save(Channel channel)
throws SQLException {
final Stored<Channel> stored = new Stored<Channel>(channel);
String sql = "INSERT INTO Channel VALUES('" + stored.identity + "','"
+ stored.version + "','"
+ channel.name + "')";
connection.createStatement().executeUpdate(sql);
return stored;
}
@Override
public synchronized Stored<Channel> update(Stored<Channel> channel,
Channel new_channel)
throws UpdatedException,
DeletedException,
SQLException {
final Stored<Channel> current = get(channel.identity);
final Stored<Channel> updated = current.newVersion(new_channel);
if(current.version.equals(channel.version)) {
String sql = "UPDATE Channel SET" +
" (version,name) =('"
+ updated.version + "','"
+ new_channel.name
+ "') WHERE id='"+ updated.identity + "'";
connection.createStatement().executeUpdate(sql);
} else {
throw new UpdatedException(current);
}
giveNextVersion(updated);
return updated;
}
@Override
public synchronized void delete(Stored<Channel> channel)
throws UpdatedException,
DeletedException,
SQLException {
final Stored<Channel> current = get(channel.identity);
if(current.version.equals(channel.version)) {
String sql = "DELETE FROM Channel WHERE id ='" + channel.identity + "'";
connection.createStatement().executeUpdate(sql);
} else {
throw new UpdatedException(current);
}
}
@Override
public Stored<Channel> get(UUID id)
throws DeletedException,
SQLException {
final String channelsql = "SELECT version,name FROM Channel WHERE id = '" + id.toString() + "'";
final String eventsql = "SELECT id,rowid FROM Event WHERE channel = '" + id.toString() + "' ORDER BY rowid ASC";
final Statement channelStatement = connection.createStatement();
final Statement eventStatement = connection.createStatement();
final ResultSet channelResult = channelStatement.executeQuery(channelsql);
final ResultSet eventResult = eventStatement.executeQuery(eventsql);
if(channelResult.next()) {
final UUID version =
UUID.fromString(channelResult.getString("version"));
final String name =
channelResult.getString("name");
// Get all the events associated with this channel
final List.Builder<Stored<Channel.Event>> events = List.builder();
while(eventResult.next()) {
final UUID eventId = UUID.fromString(eventResult.getString("id"));
events.accept(eventStore.get(eventId));
}
return (new Stored<Channel>(new Channel(name,events.getList()),id,version));
} else {
throw new DeletedException();
}
}
/**
* This function creates a "dummy" update.
* This function should be called when events are changed or
* deleted from the channel.
*/
public Stored<Channel> noChangeUpdate(UUID channelId)
throws SQLException, DeletedException {
String sql = "UPDATE Channel SET" +
" (version) =('" + UUID.randomUUID() + "') WHERE id='"+ channelId + "'";
connection.createStatement().executeUpdate(sql);
Stored<Channel> channel = get(channelId);
giveNextVersion(channel);
return channel;
}
/**
* Get the current version UUID for the specified channel.
* @param id UUID for the channel.
*/
public UUID getCurrentVersion(UUID id)
throws DeletedException,
SQLException {
final String channelsql = "SELECT version FROM Channel WHERE id = '" + id.toString() + "'";
final Statement channelStatement = connection.createStatement();
final ResultSet channelResult = channelStatement.executeQuery(channelsql);
if(channelResult.next()) {
return UUID.fromString(
channelResult.getString("version"));
}
throw new DeletedException();
}
/**
* Wait for a new version of a channel.
* This is a blocking call to get the next version of a channel.
* @param identity The identity of the channel.
* @param version The previous version accessed.
* @return The newest version after the specified one.
*/
public Stored<Channel> waitNextVersion(UUID identity, UUID version)
throws DeletedException,
SQLException {
var result
= Maybe.<Stored<Channel>>builder();
// Insert our result consumer
synchronized(waiters) {
var channelWaiters
= Maybe.just(waiters.get(identity));
waiters.put(identity
,List.cons(result
,channelWaiters.defaultValue(List.empty())));
}
// Test if there already is a new version avaiable
if(!getCurrentVersion(identity).equals(version)) {
return get(identity);
}
// Wait
synchronized(result) {
while(true) {
try {
result.wait();
return result.getMaybe().get();
} catch (InterruptedException e) {
System.err.println("Thread interrupted.");
} catch (Maybe.NothingException e) {
// Still no result, looping
}
}
}
}
/**
* Notify all waiters of a new version
*/
private void giveNextVersion(Stored<Channel> channel) {
synchronized(waiters) {
Maybe<List<Consumer<Stored<Channel>>>> channelWaiters
= Maybe.just(waiters.get(channel.identity));
try {
channelWaiters.get().forEach(w -> {
w.accept(channel);
synchronized(w) {
w.notifyAll();
}
});
} catch (Maybe.NothingException e) {
// No were waiting for us :'(
}
waiters.put(channel.identity,List.empty());
}
}
/**
* Get the channel belonging to a specific event.
*/
public Stored<Channel> lookupChannelForEvent(Stored<Channel.Event> e)
throws SQLException, DeletedException {
String sql = "SELECT channel FROM ChannelEvent WHERE event='" + e.identity + "'";
final ResultSet rs = connection.createStatement().executeQuery(sql);
if(rs.next()) {
final UUID channelId = UUID.fromString(rs.getString("channel"));
return get(channelId);
}
throw new DeletedException();
}
}
package inf226.inchat;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.time.Instant;
import java.util.UUID;
import java.util.function.Consumer;
import inf226.storage.*;
import inf226.util.*;
public final class EventStorage
implements Storage<Channel.Event,SQLException> {
private final Connection connection;
public EventStorage(Connection connection)
throws SQLException {
this.connection = connection;
connection.createStatement()
.executeUpdate("CREATE TABLE IF NOT EXISTS Event (id TEXT PRIMARY KEY, version TEXT, channel TEXT, type INTEGER, time TEXT, FOREIGN KEY(channel) REFERENCES Channel(id) ON DELETE CASCADE)");
connection.createStatement()
.executeUpdate("CREATE TABLE IF NOT EXISTS Message (id TEXT PRIMARY KEY, sender TEXT, content Text, FOREIGN KEY(id) REFERENCES Event(id) ON DELETE CASCADE)");
connection.createStatement()
.executeUpdate("CREATE TABLE IF NOT EXISTS Joined (id TEXT PRIMARY KEY, sender TEXT, FOREIGN KEY(id) REFERENCES Event(id) ON DELETE CASCADE)");
}
@Override
public Stored<Channel.Event> save(Channel.Event event)
throws SQLException {
final Stored<Channel.Event> stored = new Stored<Channel.Event>(event);
String sql = "INSERT INTO Event VALUES('" + stored.identity + "','"
+ stored.version + "','"
+ event.channel + "','"
+ event.type.code + "','"
+ event.time + "')";
connection.createStatement().executeUpdate(sql);
switch (event.type) {
case message:
sql = "INSERT INTO Message VALUES('" + stored.identity + "','"
+ event.sender + "','"
+ event.message +"')";
break;
case join:
sql = "INSERT INTO Joined VALUES('" + stored.identity + "','"
+ event.sender +"')";
break;
}
connection.createStatement().executeUpdate(sql);
return stored;
}
@Override
public synchronized Stored<Channel.Event> update(Stored<Channel.Event> event,
Channel.Event new_event)
throws UpdatedException,
DeletedException,
SQLException {
final Stored<Channel.Event> current = get(event.identity);
final Stored<Channel.Event> updated = current.newVersion(new_event);
if(current.version.equals(event.version)) {
String sql = "UPDATE Event SET" +
" (version,channel,time,type) =('"
+ updated.version + "','"
+ new_event.channel + "','"
+ new_event.time + "','"
+ new_event.type.code
+ "') WHERE id='"+ updated.identity + "'";
connection.createStatement().executeUpdate(sql);
switch (new_event.type) {
case message:
sql = "UPDATE Message SET (sender,content)=('" + new_event.sender + "','"
+ new_event.message +"') WHERE id='"+ updated.identity + "'";
break;
case join:
sql = "UPDATE Joined SET (sender)=('" + new_event.sender +"') WHERE id='"+ updated.identity + "'";
break;
}
connection.createStatement().executeUpdate(sql);
} else {
throw new UpdatedException(current);
}
return updated;
}
@Override
public synchronized void delete(Stored<Channel.Event> event)
throws UpdatedException,
DeletedException,
SQLException {
final Stored<Channel.Event> current = get(event.identity);
if(current.version.equals(event.version)) {
String sql = "DELETE FROM Event WHERE id ='" + event.identity + "'";
connection.createStatement().executeUpdate(sql);
} else {
throw new UpdatedException(current);
}
}
@Override
public Stored<Channel.Event> get(UUID id)
throws DeletedException,
SQLException {
final String sql = "SELECT version,channel,time,type FROM Event WHERE id = '" + id.toString() + "'";
final Statement statement = connection.createStatement();
final ResultSet rs = statement.executeQuery(sql);
if(rs.next()) {
final UUID version = UUID.fromString(rs.getString("version"));
final UUID channel =
UUID.fromString(rs.getString("channel"));
final Channel.Event.Type type =
Channel.Event.Type.fromInteger(rs.getInt("type"));
final Instant time =
Instant.parse(rs.getString("time"));
final Statement mstatement = connection.createStatement();
switch(type) {
case message:
final String msql = "SELECT sender,content FROM Message WHERE id = '" + id.toString() + "'";
final ResultSet mrs = mstatement.executeQuery(msql);
mrs.next();
return new Stored<Channel.Event>(
Channel.Event.createMessageEvent(channel,time,mrs.getString("sender"),mrs.getString("content")),
id,
version);
case join:
final String asql = "SELECT sender FROM Joined WHERE id = '" + id.toString() + "'";
final ResultSet ars = mstatement.executeQuery(asql);
ars.next();
return new Stored<Channel.Event>(
Channel.Event.createJoinEvent(channel,time,ars.getString("sender")),
id,
version);
}
}
throw new DeletedException();
}
}
This diff is collapsed.
package inf226.inchat;
import inf226.storage.*;
import inf226.util.Maybe;
import inf226.util.Util;
import java.util.TreeMap;
import java.util.Map;
import java.util.function.Consumer;
import java.util.UUID;
import java.time.Instant;
import java.sql.SQLException;
import java.sql.Connection;
import inf226.util.immutable.List;
/**
* This class models the chat logic.
*
* It provides an abstract interface to
* usual chat server actions.
*
**/
public class InChat {
private final Connection connection;
private final UserStorage userStore;
private final ChannelStorage channelStore;
private final EventStorage eventStore;
private final AccountStorage accountStore;
private final SessionStorage sessionStore;
private final Map<UUID,List<Consumer<Channel.Event>>> eventCallbacks
= new TreeMap<UUID,List<Consumer<Channel.Event>>>();
public InChat(UserStorage userStore,
ChannelStorage channelStore,
AccountStorage accountStore,
SessionStorage sessionStore,
Connection connection) {
this.userStore=userStore;
this.channelStore=channelStore;
this.eventStore=channelStore.eventStore;
this.accountStore=accountStore;
this.sessionStore=sessionStore;
this.connection=connection;
}
/**
* An atomic operation in Inchat.
* An operation has a function run(), which returns its
* result through a consumer.
*/
@FunctionalInterface
private interface Operation<T,E extends Throwable> {
void run(final Consumer<T> result) throws E,DeletedException;
}
/**
* Execute an operation atomically in SQL.
* Wrapper method for commit() and rollback().
*/
private<T> Maybe<T> atomic(Operation<T,SQLException> op) {
synchronized (connection) {
try {
Maybe.Builder<T> result = Maybe.builder();
op.run(result);
connection.commit();
return result.getMaybe();
}catch (SQLException e) {
System.err.println(e.toString());
}catch (DeletedException e) {
System.err.println(e.toString());
}
try {
connection.rollback();
} catch (SQLException e) {
System.err.println(e.toString());
}
return Maybe.nothing();
}
}
/**
* Log in a user to the chat.
*/
public Maybe<Stored<Session>> login(final String username,
final String password) {
return atomic(result -> {
final Stored<Account> account = accountStore.lookup(username);
final Stored<Session> session =
sessionStore.save(new Session(account, Instant.now().plusSeconds(60*60*24)));
// Check that password is not incorrect and not too long.
if (!(!account.value.password.equals(password) && !(password.length() > 1000))) {
result.accept(session);
}
});
}
/**
* Register a new user.
*/
public Maybe<Stored<Session>> register(final String username,
final String password) {
return atomic(result -> {
final Stored<User> user =
userStore.save(User.create(username));
final Stored<Account> account =
accountStore.save(Account.create(user, password));
final Stored<Session> session =
sessionStore.save(new Session(account, Instant.now().plusSeconds(60*60*24)));
result.accept(session);
});
}
/**
* Restore a previous session.
*/
public Maybe<Stored<Session>> restoreSession(UUID sessionId) {
return atomic(result ->
result.accept(sessionStore.get(sessionId))
);
}
/**
* Log out and invalidate the session.
*/
public void logout(Stored<Session> session) {
atomic(result ->
Util.deleteSingle(session,sessionStore));
}
/**
* Create a new channel.
*/
public Maybe<Stored<Channel>> createChannel(Stored<Account> account,
String name) {
return atomic(result -> {
Stored<Channel> channel
= channelStore.save(new Channel(name,List.empty()));
joinChannel(account, channel.identity);
result.accept(channel);
});
}
/**
* Join a channel.
*/
public Maybe<Stored<Channel>> joinChannel(Stored<Account> account,
UUID channelID) {
return atomic(result -> {
Stored<Channel> channel = channelStore.get(channelID);
Util.updateSingle(account,
accountStore,
a -> a.value.joinChannel(channel.value.name,channel));
Stored<Channel.Event> joinEvent
= channelStore.eventStore.save(
Channel.Event.createJoinEvent(channelID,
Instant.now(),
account.value.user.value.name));
result.accept(
Util.updateSingle(channel,
channelStore,
c -> c.value.postEvent(joinEvent)));
});
}
/**
* Post a message to a channel.
*/
public Maybe<Stored<Channel>> postMessage(Stored<Account> account,
Stored<Channel> channel,
String message) {
return atomic(result -> {
Stored<Channel.Event> event
= channelStore.eventStore.save(
Channel.Event.createMessageEvent(channel.identity,Instant.now(),
account.value.user.value.name, message));
result.accept (
Util.updateSingle(channel,
channelStore,
c -> c.value.postEvent(event)));
});
}
/**
* A blocking call which returns the next state of the channel.
*/
public Maybe<Stored<Channel>> waitNextChannelVersion(UUID identity, UUID version) {
try{
return Maybe.just(channelStore.waitNextVersion(identity, version));
} catch (DeletedException e) {
return Maybe.nothing();
} catch (SQLException e) {
return Maybe.nothing();
}
}
/**
* Get an event by its identity.
*/
public Maybe<Stored<Channel.Event>> getEvent(UUID eventID) {
return atomic(result ->
result.accept(channelStore.eventStore.get(eventID))
);
}
/**
* Delete an event.
*/
public Stored<Channel> deleteEvent(Stored<Channel> channel, Stored<Channel.Event> event) {
return this.<Stored<Channel>>atomic(result -> {
Util.deleteSingle(event , channelStore.eventStore);
result.accept(channelStore.noChangeUpdate(channel.identity));
}).defaultValue(channel);
}
/**
* Edit a message.
*/
public Stored<Channel> editMessage(Stored<Channel> channel,
Stored<Channel.Event> event,
String newMessage) {
return this.<Stored<Channel>>atomic(result -> {
Util.updateSingle(event,
channelStore.eventStore,
e -> e.value.setMessage(newMessage));
result.accept(channelStore.noChangeUpdate(channel.identity));
}).defaultValue(channel);
}
}
package inf226.inchat;
import java.time.Instant;
import inf226.storage.*;
/**
* The Session class represents a session.
* A session is created each time the user logs in.
*/
public final class Session {
final Stored<Account> account;
final Instant expiry;
public Session( Stored<Account> account, Instant expiry) {
this.account = account;
this.expiry = expiry;
}
}
package inf226.inchat;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.time.Instant;
import java.util.UUID;
import inf226.storage.*;
/**
* The SessionStorage stores Session objects in a SQL database.
*/
public final class SessionStorage
implements Storage<Session,SQLException> {
final Connection connection;
final Storage<Account,SQLException> accountStorage;
public SessionStorage(Connection connection,
Storage<Account,SQLException> accountStorage)
throws SQLException {
this.connection = connection;
this.accountStorage = accountStorage;
connection.createStatement()
.executeUpdate("CREATE TABLE IF NOT EXISTS Session (id TEXT PRIMARY KEY, version TEXT, account TEXT, expiry TEXT, FOREIGN KEY(account) REFERENCES Account(id) ON DELETE CASCADE)");
}
@Override
public Stored<Session> save(Session session)
throws SQLException {
final Stored<Session> stored = new Stored<Session>(session);
String sql = "INSERT INTO Session VALUES('" + stored.identity + "','"
+ stored.version + "','"
+ session.account.identity + "','"
+ session.expiry.toString() + "')";
connection.createStatement().executeUpdate(sql);
return stored;
}
@Override
public synchronized Stored<Session> update(Stored<Session> session,
Session new_session)
throws UpdatedException,
DeletedException,
SQLException {
final Stored<Session> current = get(session.identity);
final Stored<Session> updated = current.newVersion(new_session);
if(current.version.equals(session.version)) {
String sql = "UPDATE Session SET" +
" (version,account,expiry) =('"
+ updated.version + "','"
+ new_session.account.identity + "','"
+ new_session.expiry.toString()
+ "') WHERE id='"+ updated.identity + "'";
connection.createStatement().executeUpdate(sql);
} else {
throw new UpdatedException(current);
}
return updated;
}
@Override
public synchronized void delete(Stored<Session> session)
throws UpdatedException,
DeletedException,
SQLException {
final Stored<Session> current = get(session.identity);
if(current.version.equals(session.version)) {
String sql = "DELETE FROM Session WHERE id ='" + session.identity + "'";
connection.createStatement().executeUpdate(sql);
} else {
throw new UpdatedException(current);
}
}
@Override
public Stored<Session> get(UUID id)
throws DeletedException,
SQLException {
final String sql = "SELECT version,account,expiry FROM Session WHERE id = '" + id.toString() + "'";
final Statement statement = connection.createStatement();
final ResultSet rs = statement.executeQuery(sql);
if(rs.next()) {
final UUID version = UUID.fromString(rs.getString("version"));
final Stored<Account> account
= accountStorage.get(
UUID.fromString(rs.getString("account")));
final Instant expiry = Instant.parse(rs.getString("expiry"));
return (new Stored<Session>
(new Session(account,expiry),id,version));
} else {
throw new DeletedException();
}
}
}
package inf226.inchat;
import java.time.Instant;
/**
* The User class holds the public information
* about a user.
**/
public final class User {
public final String name;
public final Instant joined;
public User(String name,
Instant joined) {
this.name = name;
this.joined = joined;
}
/**
* Create a new user.
*/
public static User create(String name) {
return new User(name, Instant.now());
}
}
package inf226.inchat;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.time.Instant;
import java.util.UUID;
import inf226.storage.*;
import inf226.util.*;
/**
* The UserStore stores User objects in a SQL database.
*/
public final class UserStorage
implements Storage<User,SQLException> {
final Connection connection;
public UserStorage(Connection connection)
throws SQLException {
this.connection = connection;
connection.createStatement()
.executeUpdate("CREATE TABLE IF NOT EXISTS User (id TEXT PRIMARY KEY, version TEXT, name TEXT, joined TEXT)");
}
@Override
public Stored<User> save(User user)
throws SQLException {
final Stored<User> stored = new Stored<User>(user);
String sql = "INSERT INTO User VALUES('" + stored.identity + "','"
+ stored.version + "','"
+ user.name + "','"
+ user.joined.toString() + "')";
connection.createStatement().executeUpdate(sql);
return stored;
}
@Override
public synchronized Stored<User> update(Stored<User> user,
User new_user)
throws UpdatedException,
DeletedException,
SQLException {
final Stored<User> current = get(user.identity);
final Stored<User> updated = current.newVersion(new_user);
if(current.version.equals(user.version)) {
String sql = "UPDATE User SET" +
" (version,name,joined) =('"
+ updated.version + "','"
+ new_user.name + "','"
+ new_user.joined.toString()
+ "') WHERE id='"+ updated.identity + "'";
connection.createStatement().executeUpdate(sql);
} else {
throw new UpdatedException(current);
}
return updated;
}
@Override
public synchronized void delete(Stored<User> user)
throws UpdatedException,
DeletedException,
SQLException {
final Stored<User> current = get(user.identity);
if(current.version.equals(user.version)) {
String sql = "DELETE FROM User WHERE id ='" + user.identity + "'";
connection.createStatement().executeUpdate(sql);
} else {
throw new UpdatedException(current);
}
}
@Override
public Stored<User> get(UUID id)
throws DeletedException,
SQLException {
final String sql = "SELECT version,name,joined FROM User WHERE id = '" + id.toString() + "'";
final Statement statement = connection.createStatement();
final ResultSet rs = statement.executeQuery(sql);
if(rs.next()) {
final UUID version =
UUID.fromString(rs.getString("version"));
final String name = rs.getString("name");
final Instant joined = Instant.parse(rs.getString("joined"));
return (new Stored<User>
(new User(name,joined),id,version));
} else {
throw new DeletedException();
}
}
/**
* Look up a user by their username;
**/
public Maybe<Stored<User>> lookup(String name) {
final String sql = "SELECT id FROM User WHERE name = '" + name + "'";
try{
final Statement statement = connection.createStatement();
final ResultSet rs = statement.executeQuery(sql);
if(rs.next())
return Maybe.just(
get(UUID.fromString(rs.getString("id"))));
} catch (Exception e) {
}
return Maybe.nothing();
}
}
package inf226.storage;
/**
* This class signals that an object was deleted before
* action could be carried out.
*/
public class DeletedException extends Exception {
private static final long serialVersionUID = 416363032598879968L;
public DeletedException() {
super("Object was deleted");
}
@Override
public Throwable fillInStackTrace() {
return this;
}
}
package inf226.storage;
import java.util.function.Consumer;
import inf226.storage.*;
import java.util.UUID;
/**
* This provides an interface for an object storage
* which implements transactional updating of objects.
*
* The main limitation of this interface is that you
* cannot combine updates of several objects into one
* transaction.
**/
public interface Storage<T,E extends Exception> {
/**
* Save a new object into the storage.
*
* Use this when an object is created.
**/
public Stored<T> save(T value) throws E;
/**
* Update an already stored object with a new value.
*
* Use this when you want to save changes to an object.
* If an UpdatedException is thrown, redo changes with
* the new version and call update() again.
**/
public Stored<T> update(Stored<T> object, T new_object) throws UpdatedException,DeletedException,E;
/**
* Delete an object from the store.
**/
public void delete(Stored<T> object) throws UpdatedException,DeletedException,E;
/**
* Get a stored object based on UUID.
*
* Use this if you believe your object might be stale, or
* to retrieve an object from a serialised reference.
**/
public Stored<T> get(UUID id) throws DeletedException,E;
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment