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

No git history available for InChat.

parents
No related branches found
No related tags found
No related merge requests found
Showing
with 1680 additions and 0 deletions
README.md 0 → 100644
# INCHAT – The INsecure CHAT application
Welcome to this second mandatory assignment of INF226.
In this assignment you will be improving 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.
For your convenience, the task is separated into specific
exercises or tasks. These task might have been the result
of a security analysis – like the one you had in the
second assignment. If you discover any security issues
beyond these tasks, you can make a note of them at the
end of this report.
For each task, you should make a short note how you solved
it – ideally with a reference to the relevant git-commits you
have made.
## Evaluation
This assignment is mandatory for the course, and counts 20%
of your final grade. The assigment is graded 0–20 points,
where you must get a minimum of 6 points in order to pass
the assignment.
## Groups
As with the previous assignments, you can work in groups of 1–3 students
on this assginment. Make sure that everyone is signed up for the group
on [MittUiB](https://mitt.uib.no/courses/24957/groups#tab-8746).
One good way to collaborate is that one person on the group makes a
fork and adds the other group members to that project.
## 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 persion 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/).
## Handing in the assignment
Before you hand in your assignment, make sure that you
have included all dependencies in the file `pom.xml`, and
that your program compiles and runs well. One good way
to test this is to make a fresh clone from the GitLab repo,
compile and test the app.
Once you are done, you submit the assignment on
[`mitt.uib.no`](https://mitt.uib.no/) as a link to your
fork – one link per group. This means you should not commit to the
repository after the deadline has passed. Include the commit hash
of the final commit (which you can find `git log`, for instance) in
your submission on MittUiB.
## 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.
## Tasks
The tasks below has been separated out, and marked with their *approximate* weight. Each task has a section called "Notes" where you can
write notes on how you have solved the task.
### Task 0 – Authentication (4 points)
The original authentication mechanisms of inChat was so insecure it
had to be removed immediately and all traces of the old passwords
have been purged from the database. Therefore, the code in
`inf226.inchat.Account`, which is supposed to check the password,
always returns `true`.
#### Task 0 – Part A
*Update the code to use a secure password authentication method – one
of the methods we have discussed in lecture.*
Any data you need to store for the password check can be kept in the `Account` class, with
appropriate updates to `storage.AccountStorage`. Remember that the `Account` class is *immutable*.
Any new field must be immutable and `final` as well.
**Hint**:
- An implementation of `scrypt` is already included as a dependency in `pom.xml`.
If you prefer to use `argon2`, make sure to include it as well.
### Task 0 – Part B
Create two new, immutable, classes `UserName` and `Password` in the
inf226.inchat package, and replace `String` with these
classes in User and Account classes and other places in
the application where it makes sense.
Decide on a set of password criteria which satisfies
the NIST requirements, and implement these as invariants
in the Password class, and check these upon registration.
### Task 0 – Part C
*While the session cookie is an unguessable UUID, you must set the
correct protection flags on the session cookie.*
#### Notes – task 0
Here you write your notes about how this task was performed.
### Task 1 – SQL injection (4 points)
The SQL code is currently wildly concatenating strings, leaving
it wide open to injection attacks.
*Take measures to prevent SQL injection attacks on the application.*
#### Notes – task 1
Here you write your notes about how this task was performed.
### Task 2 – Cross-site scripting (4 points)
The user interface is generated in `inf226.inchat.Handler`. The current
implementation is returning a lot of user data without properly
escaping it for the context it is displayed (for instance HTML body).
*Take measures to prevent XSS attacks on inChat.*
**Hint**: In addition to the books and the lecture slides, you should
take a look at the [OWASP XSS prevention cheat sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html)
#### Notes – task 2
Here you write your notes about how this task was performed.
### Task 3 – Cross-site request forgery (1 point)
While the code uses UUIDs to identify most objects, some
form actions are still susceptible to cross-site request forgery attacks
(for instance the `newmessage` and the `createchannel` actions.)
*Implement anti-CSRF tokens or otherwise prevent CSRF on
the vulnerable forms.*
**Hint:** it is OK to use the session cookie as such a token.
#### Notes – task 3
Here you write your notes about how this task was performed.
### Task 4 – Access control (5 points)
inChat has no working access control. The channel side-bar has a form
to set roles for the channel,
but the actual functionality is not implemented.
In this task you should implement access control for inChat.
- Identify which actions need access control, and decide
on how you want to structure the access control.
Connect the user interface in the channel side-bar to your
access control system so that the security roles work as
intended. The security roles in a channel are:
- *Owner*: Can set roles, delete and edit any message, as
well as read and post to the channel.
- *Moderator*: Can delete and edit any message, as
well as read and post to the channel.
- *Participant*: Can delete and edit their own messages, as
well as read and post to the channel.
- *Observer*: Can read messages in the channel.
- *Banned*: Has no access to the channel.
The default behaviour should be that the creator of the
channel becomes the owner, and that inviting someone
puts them at the "Participant" level.
Also, make sure that your system satisfies the invariant:
- Every channel has at least one owner.
**Hint:** The InChat class is best suited to implement the
access control checks since it in charge of all the operations
on the chat. Implement a "setRole" method there, and add
security checks to all other methods.
#### Notes – task 4
Here you write your notes about how this task was performed.
### Task ω – Other security holes (2 points)
There are more security issues in this web application.
Improve the security of the application to the best of your
ability.
A note about HTTPS: We assume that inChat will be running
behind a reverse proxy which takes care of HTTPS, so you
can ignore issues related HTTPS.
#### Notes – task ω
Here you write your notes about how this task was performed.
<!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>1.9</maven.compiler.source>
<maven.compiler.target>1.9</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 {
public final Stored<User> user;
public final List<Pair<String,Stored<Channel>>> channels;
public Account(Stored<User> user,
List<Pair<String,Stored<Channel>>> channels) {
this.user = user;
this.channels = channels;
}
/**
* Create a new Account.
**/
public static Account create(Stored<User> user,
String password) {
return new Account(user,List.empty());
}
public Account joinChannel(String alias, Stored<Channel> channel) {
Pair<String,Stored<Channel>> entry
= new Pair<String,Stored<Channel>>(alias,channel);
return new Account
(user,
List.cons(entry,
channels));
}
public boolean checkPassword(String password) {
return true;
}
}
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.*;
public final class AccountStorage
implements Storage<Account,SQLException> {
final Connection connection;
final Storage<User,SQLException> userStore;
final Storage<Channel,SQLException> channelStore;
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, 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 + "')";
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 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 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()),id,version));
} else {
throw new DeletedException();
}
}
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;
public final class Channel {
public final String name;
public final List<Stored<Event>> events;
public Channel(String name, List<Stored<Event>> events) {
this.name=name;
this.events=events;
}
public Channel postEvent(Stored<Event> event) {
return new Channel(name, List.cons(event,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 Type type;
public final Instant time;
public final String sender;
public final String message;
/**
* Copy constructor
**/
public Event(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.time=time;
this.sender=sender;
this.type=type;
this.message=message;
}
public static Event createMessageEvent(Instant time, String sender, String message) {
return new Event(time,
sender,
Type.message,
message);
}
public static Event createJoinEvent(Instant time, String user) {
return new Event(time,
user,
Type.join,
null);
}
public Event setMessage(String message) {
return new Event(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.*;
public final class ChannelStorage
implements Storage<Channel,SQLException> {
final Connection connection;
private Map<UUID,List<Consumer<Stored<Channel>>>> waiters
= new TreeMap<UUID,List<Consumer<Stored<Channel>>>>();
public final EventStorage eventStore;
public ChannelStorage(Connection connection,
EventStorage eventStore)
throws SQLException {
this.connection = connection;
this.eventStore = eventStore;
connection.createStatement()
.executeUpdate("CREATE TABLE IF NOT EXISTS Channel (id TEXT PRIMARY KEY, version TEXT, name TEXT)");
connection.createStatement()
.executeUpdate("CREATE TABLE IF NOT EXISTS ChannelEvent (channel TEXT, event TEXT, ordinal INTEGER, PRIMARY KEY(channel,event), FOREIGN KEY(channel) REFERENCES Channel(id) ON DELETE CASCADE, FOREIGN KEY(event) REFERENCES Event(id) ON DELETE CASCADE)");
}
@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);
// Write the list of events
final Maybe.Builder<SQLException> exception = Maybe.builder();
final Mutable<Integer> ordinal = new Mutable<Integer>(0);
channel.events.forEach(event -> {
final String msql = "INSERT INTO ChannelEvent VALUES('" + stored.identity + "','"
+ event.identity + "','"
+ 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<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);
// Rewrite the list of events
connection.createStatement().executeUpdate("DELETE FROM ChannelEvent WHERE channel='" + channel.identity + "'");
final Maybe.Builder<SQLException> exception = Maybe.builder();
final Mutable<Integer> ordinal = new Mutable<Integer>(0);
new_channel.events.forEach(event -> {
final String msql = "INSERT INTO ChannelEvent VALUES('" + channel.identity + "','"
+ event.identity + "','"
+ 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);
}
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 event,ordinal FROM ChannelEvent WHERE channel = '" + id.toString() + "' ORDER BY ordinal DESC";
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("event"));
events.accept(eventStore.get(eventId));
}
return (new Stored<Channel>(new Channel(name,events.getList()),id,version));
} else {
throw new DeletedException();
}
}
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;
}
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
**/
public Stored<Channel> waitNextVersion(UUID identity, UUID version)
throws DeletedException,
SQLException {
Maybe.Builder<Stored<Channel>> result
= Maybe.builder();
// Insert our result consumer
synchronized(waiters) {
Maybe<List<Consumer<Stored<Channel>>>> 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, type INTEGER, time TEXT)");
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.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,time,type) =('"
+ updated.version + "','"
+ 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,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 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(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(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 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 UserStorage userStore;
private final ChannelStorage channelStore;
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) {
this.userStore=userStore;
this.channelStore=channelStore;
this.accountStore=accountStore;
this.sessionStore=sessionStore;
}
/**
* Log in a user to the chat.
*/
public Maybe<Stored<Session>> login(String username, String password) {
// Here you can implement login.
try {
final Stored<Account> account = accountStore.lookup(username);
final Stored<Session> session =
sessionStore.save(new Session(account, Instant.now().plusSeconds(60*60*24)));
return Maybe.just(session);
} catch (SQLException e) {
} catch (DeletedException e) {
}
return Maybe.nothing();
}
/**
* Register a new user.
*/
public Maybe<Stored<Session>> register(String username, String password) {
try {
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)));
return Maybe.just(session);
} catch (SQLException e) {
return Maybe.nothing();
}
}
/**
* Restore a previous session.
*/
public Maybe<Stored<Session>> restoreSession(UUID sessionId) {
try {
return Maybe.just(sessionStore.get(sessionId));
} catch (SQLException e) {
System.err.println("When restoring session:" + e);
return Maybe.nothing();
} catch (DeletedException e) {
return Maybe.nothing();
}
}
/**
* Log out and invalidate the session.
*/
public void logout(Stored<Session> session) {
try {
Util.deleteSingle(session,sessionStore);
} catch (SQLException e) {
System.err.println("When loging out of session:" + e);
}
}
/**
* Create a new channel.
*/
public Maybe<Stored<Channel>> createChannel(Stored<Account> account,
String name) {
try {
Stored<Channel> channel
= channelStore.save(new Channel(name,List.empty()));
return joinChannel(account, channel.identity);
} catch (SQLException e) {
System.err.println("When trying to create channel " + name +":\n" + e);
}
return Maybe.nothing();
}
/**
* Join a channel.
*/
public Maybe<Stored<Channel>> joinChannel(Stored<Account> account,
UUID channelID) {
try {
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(Instant.now(),
account.value.user.value.name));
return Maybe.just(
Util.updateSingle(channel,
channelStore,
c -> c.value.postEvent(joinEvent)));
} catch (DeletedException e) {
// This channel has been deleted.
} catch (SQLException e) {
System.err.println("When trying to join " + channelID +":\n" + e);
}
return Maybe.nothing();
}
/**
* Post a message to a channel.
*/
public Maybe<Stored<Channel>> postMessage(Stored<Account> account,
Stored<Channel> channel,
String message) {
try {
Stored<Channel.Event> event
= channelStore.eventStore.save(
Channel.Event.createMessageEvent(Instant.now(),
account.value.user.value.name, message));
try {
return Maybe.just(
Util.updateSingle(channel,
channelStore,
c -> c.value.postEvent(event)));
} catch (DeletedException e) {
// Channel was already deleted.
// Let us pretend this never happened
Util.deleteSingle(event, channelStore.eventStore);
}
} catch (SQLException e) {
System.err.println("When trying to post message in " + channel.identity +":\n" + e);
}
return Maybe.nothing();
}
/**
* 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 (SQLException e) {
System.err.println("While waiting for the next message in " + identity +":\n" + e);
} catch (DeletedException e) {
// Channel deleted.
}
return Maybe.nothing();
}
public Maybe<Stored<Channel.Event>> getEvent(UUID eventID) {
try {
return Maybe.just(channelStore.eventStore.get(eventID));
} catch (SQLException e) {
return Maybe.nothing();
} catch (DeletedException e) {
return Maybe.nothing();
}
}
public Stored<Channel> deleteEvent(Stored<Channel> channel, Stored<Channel.Event> event) {
try {
Util.deleteSingle(event , channelStore.eventStore);
return channelStore.noChangeUpdate(channel.identity);
} catch (SQLException er) {
System.err.println("While deleting event " + event.identity +":\n" + er);
} catch (DeletedException er) {
}
return channel;
}
public Stored<Channel> editMessage(Stored<Channel> channel,
Stored<Channel.Event> event,
String newMessage) {
try{
Util.updateSingle(event,
channelStore.eventStore,
e -> e.value.setMessage(newMessage));
return channelStore.noChangeUpdate(channel.identity);
} catch (SQLException er) {
System.err.println("While deleting event " + event.identity +":\n" + er);
} catch (DeletedException er) {
}
return channel;
}
}
package inf226.inchat;
import java.time.Instant;
import inf226.storage.*;
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.*;
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;
}
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.*;
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;
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;
/**
* Get a call back when a stored object is updated.
*
public void onUpdate(Stored<T> object, Consumer<Stored<T>> callback);*/
/**
* Get a call back when a stored object is deleted.
*
public void onDelete(Stored<T> Object, Consumer<T> callback);*/
}
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