@@ -0,0 +1,4 @@ | |||
SHELL=/bin/bash | |||
PATH=/sbin:/bin:/usr/sbin:/usr/bin | |||
*/2 * * * * greenhouses python3 /opt/gh/water.py | |||
@@ -0,0 +1,5 @@ | |||
[D-BUS Service] | |||
Name=net.faustctf.SuDoD | |||
Exec=/bin/false | |||
User=root | |||
SystemdService=sudod.service |
@@ -0,0 +1,23 @@ | |||
<!DOCTYPE busconfig PUBLIC | |||
"-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN" | |||
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd"> | |||
<busconfig> | |||
<policy user="root"> | |||
<!-- root is root anyways --> | |||
<allow own="*"/> | |||
</policy> | |||
<policy context="default"> | |||
<allow send_destination="net.faustctf.SuDoD"/> | |||
<allow receive_sender="net.faustctf.SuDoD"/> | |||
</policy> | |||
<policy context="default"> | |||
<allow send_type="method_call" send_interface="net.faustctf.SuDoD.Guard"/> | |||
<allow send_type="method_call" send_interface="org.freedesktop.DBus.Introspectable"/> | |||
</policy> | |||
<servicedir>/etc/dbus-1/system-services</servicedir> | |||
</busconfig> |
@@ -0,0 +1,4 @@ | |||
127.0.0.1 localhost | |||
::1 localhost | |||
fd4f:7367:c144:c7f::1 vulnbox |
@@ -0,0 +1,3 @@ | |||
auto host | |||
iface host inet6 static | |||
address fd4f:7367:c144:c7f::2/64 |
@@ -0,0 +1,7 @@ | |||
PRETTY_NAME="garden-variety greenhouse conatainer" | |||
NAME="Greenhouse Conainer" | |||
VERSION_ID="2020" | |||
VERSION="2020" | |||
VERSION_CODENAME=faustctf | |||
ID=greenhouse | |||
HOME_URL="https://faustctf.net" |
@@ -0,0 +1,22 @@ | |||
/* Allow users in wheel group to use blueman feature requiring root without authentication */ | |||
polkit.addRule(function(action, subject) { | |||
if(action.id == "net.faustctf.SuDoD.RunCommand"){ | |||
// basic rules: everything as one self is allowed, root is allowed | |||
if(action.lookup("as_user") == subject.user) return polkit.Result.YES; | |||
if(subject.user == "root") return Result.YES; | |||
// allow user registration | |||
if(subject.user == "gate" && action.lookup("argv_0") == "/opt/bin/register.sh"){ | |||
return polkit.Result.YES; | |||
} | |||
// allow greenhouse access | |||
if(action.lookup("as_user") == "greenhouses") { | |||
var prog = action.lookup("argv_0"); | |||
var ok = ["/opt/gh/sow.py", "/opt/gh/show.py"]; | |||
if(ok.includes(prog)){ | |||
return polkit.Result.YES; | |||
} | |||
} | |||
} | |||
}); |
@@ -0,0 +1,5 @@ | |||
Match user gate | |||
PermitEmptyPasswords yes | |||
ForceCommand /opt/bin/sudoc.py /opt/bin/register.sh | |||
PermitTTY no | |||
DisableForwarding=yes |
@@ -0,0 +1,3 @@ | |||
root ALL=(ALL) ALL | |||
#includedir /etc/sudoers.d | |||
@@ -0,0 +1 @@ | |||
gate ALL = (root) NOPASSWD: /register.sh |
@@ -0,0 +1,11 @@ | |||
[Unit] | |||
Description=Configure gate User for Registration | |||
Before=ssh.service | |||
ConditionFirstBoot=yes | |||
[Service] | |||
Type=oneshot | |||
ExecStart=useradd --system --password '' gate | |||
[Install] | |||
WantedBy=multi-user.target |
@@ -0,0 +1,14 @@ | |||
[Unit] | |||
Description=Create greenhouses user | |||
ConditionFirstBoot=yes | |||
Before=cron.service | |||
[Service] | |||
Type=oneshot | |||
ExecStart=useradd --system --create-home --home-dir /var/greenhouses --gid daemon greenhouses | |||
User=root | |||
[Install] | |||
WantedBy=multi-user.target | |||
@@ -0,0 +1,14 @@ | |||
[Unit] | |||
Description=Regular background program processing daemon | |||
Documentation=man:cron(8) | |||
After=remote-fs.target nss-user-lookup.target | |||
[Service] | |||
EnvironmentFile=-/etc/default/cron | |||
ExecStart=/usr/sbin/cron -f $EXTRA_OPTS | |||
IgnoreSIGPIPE=false | |||
KillMode=process | |||
Restart=on-failure | |||
[Install] | |||
WantedBy=multi-user.target |
@@ -0,0 +1,11 @@ | |||
[Unit] | |||
Description=Configure gate User for Registration | |||
Before=ssh.service | |||
ConditionFirstBoot=yes | |||
[Service] | |||
Type=oneshot | |||
ExecStart=useradd --system --password '' gate | |||
[Install] | |||
WantedBy=multi-user.target |
@@ -0,0 +1,14 @@ | |||
[Unit] | |||
Description=Create greenhouses user | |||
ConditionFirstBoot=yes | |||
Before=cron.service | |||
[Service] | |||
Type=oneshot | |||
ExecStart=useradd --system --create-home --home-dir /var/greenhouses --gid daemon greenhouses | |||
User=root | |||
[Install] | |||
WantedBy=multi-user.target | |||
@@ -0,0 +1,15 @@ | |||
[Unit] | |||
Description=Generate SSH host keys and ssh privilege separation user | |||
ConditionFirstBoot=yes | |||
Before=ssh.service | |||
[Service] | |||
Type=oneshot | |||
ExecStart=/usr/bin/ssh-keygen -A | |||
ExecStart=useradd --system --home-dir /run/sshd --gid nogroup --shell /bin/false sshd | |||
ExecStart=usermod --password '*' root | |||
User=root | |||
[Install] | |||
WantedBy=multi-user.target | |||
@@ -0,0 +1,22 @@ | |||
[Unit] | |||
Description=OpenBSD Secure Shell server | |||
Documentation=man:sshd(8) man:sshd_config(5) | |||
After=network.target auditd.service | |||
ConditionPathExists=!/etc/ssh/sshd_not_to_be_run | |||
[Service] | |||
EnvironmentFile=-/etc/default/ssh | |||
ExecStartPre=/usr/sbin/sshd -t | |||
ExecStart=/usr/sbin/sshd -D $SSHD_OPTS | |||
ExecReload=/usr/sbin/sshd -t | |||
ExecReload=/bin/kill -HUP $MAINPID | |||
KillMode=process | |||
Restart=on-failure | |||
RestartPreventExitStatus=255 | |||
Type=notify | |||
RuntimeDirectory=sshd | |||
RuntimeDirectoryMode=0755 | |||
[Install] | |||
WantedBy=multi-user.target | |||
Alias=sshd.service |
@@ -0,0 +1,15 @@ | |||
[Unit] | |||
Description=Generate SSH host keys and ssh privilege separation user | |||
ConditionFirstBoot=yes | |||
Before=ssh.service | |||
[Service] | |||
Type=oneshot | |||
ExecStart=/usr/bin/ssh-keygen -A | |||
ExecStart=useradd --system --home-dir /run/sshd --gid nogroup --shell /bin/false sshd | |||
ExecStart=usermod --password '*' root | |||
User=root | |||
[Install] | |||
WantedBy=multi-user.target | |||
@@ -0,0 +1,6 @@ | |||
[Unit] | |||
Description=SuDo as a Service | |||
[Service] | |||
BusName=org.freedesktop.hostname1 | |||
ExecStart=/opt/bin/sudod.py |
@@ -0,0 +1 @@ | |||
u gate - "Gatekeeper for the Greenhouses" |
@@ -0,0 +1,2 @@ | |||
#!/bin/sh | |||
exec /opt/bin/sudoc.py -u greenhouses /opt/gh/show.py |
@@ -0,0 +1,2 @@ | |||
#!/bin/sh | |||
exec /opt/bin/sudoc.py -u greenhouses /opt/gh/sow.py |
@@ -0,0 +1,21 @@ | |||
#!/bin/bash | |||
set -e | |||
if [ -z "$SSH_ORIGINAL_COMMAND" ] | |||
then | |||
echo to register, please use the base64 encoded part of your ssh-ed25519 public key as command, for example: | |||
echo ssh -o StrictHostKeyChecking=no -p 2222 gate@fd66:666:$(cat /etc/team-num)::2 AAAAC3NzaC1lZDI1NTE5AAAAIGxJ1XYRi7wLu1olOC+hK7YPNvc/WSFQ2iNU+bkxsral | |||
exit 1 | |||
fi | |||
set -u | |||
U=user$(sha256sum <<< "$SSH_ORIGINAL_COMMAND" | cut -b 1-28) | |||
SUDOC=/opt/bin/sudoc.py | |||
$SUDOC useradd --create-home --shell /bin/bash --password '*' "$U" | |||
$SUDOC -u "$U" sh -e -c 'mkdir ~'$U'/.ssh; cat > ~'$U'/.ssh/authorized_keys' <<< "ssh-ed25519 $SSH_ORIGINAL_COMMAND" | |||
echo now you can log in as user "$U" |
@@ -0,0 +1,84 @@ | |||
#!/usr/bin/env python3 | |||
import os | |||
import dbus | |||
import sys | |||
import signal | |||
from dbus.types import UnixFd | |||
from dbus.mainloop.glib import DBusGMainLoop | |||
from gi.repository import GLib | |||
from sudod import getClient, SERVICE_NAME, SERVICE_INTERFACE, GUARD_INTERFACE | |||
import pwd, grp | |||
as_user = "root" | |||
as_group = None | |||
while True: | |||
if len(sys.argv) >= 3 and sys.argv[1] == "-u": | |||
as_user = sys.argv[2] | |||
sys.argv[1:3] = [] | |||
elif len(sys.argv) >= 3 and sys.argv[1] == "-g": | |||
as_group = sys.argv[2] | |||
sys.argv[1:3] = [] | |||
else: | |||
break | |||
command_argv = sys.argv[1:] | |||
if as_group is None: | |||
as_group = grp.getgrgid(pwd.getpwnam(as_user).pw_gid).gr_name | |||
DBusGMainLoop(set_as_default=True) | |||
system_bus = dbus.SystemBus() | |||
#print("i am",system_bus.get_unique_name()) | |||
c = getClient(system_bus) | |||
cwdfd = os.open(".", os.O_RDONLY) | |||
dbus_cwd = UnixFd(cwdfd) | |||
os.close(cwdfd) | |||
loop = GLib.MainLoop() | |||
peer = c.createSession(command_argv, as_user, as_group) | |||
peer = system_bus.get_object(peer, "/guard") | |||
peer = dbus.Interface(peer, GUARD_INTERFACE) | |||
peer.simpleAuth() | |||
peer.chdirFD(dbus_cwd) | |||
for (key, val) in os.environ.items(): | |||
try: | |||
peer.setEnv(key, val) | |||
except: | |||
pass | |||
for fd in range(3): | |||
x = peer.connectFD(UnixFd(fd)) | |||
peer.dupFD(x, fd) | |||
peer.closeFD(x) | |||
def on_exit(status): | |||
# relay exit info by dying the same way | |||
if os.WIFEXITED(status): | |||
os._exit(os.WEXITSTATUS(status)) | |||
if os.WIFSIGNALED(status): | |||
sig = os.WTERMSIG(status) | |||
try: | |||
signal.signal(sig, signal.SIG_DFL) | |||
except OSError: # KILL or STOP, they work anyways | |||
pass | |||
os.kill(os.getpid(), os.WTERMSIG(status)) | |||
os.exit(1) | |||
peer.connect_to_signal("exited", on_exit) | |||
peer.run() | |||
loop.run() |
@@ -0,0 +1,238 @@ | |||
#!/usr/bin/env python3 | |||
import traceback | |||
import os | |||
import sys | |||
from gi.repository import GLib | |||
#from pydbus import SystemBus | |||
import dbus | |||
import dbus.service | |||
import dbus.mainloop.glib | |||
from dataclasses import dataclass | |||
from typing import List, Tuple, Dict | |||
import signal | |||
import fcntl | |||
import collections | |||
import time | |||
import secrets | |||
import pwd | |||
import grp | |||
SERVICE_NAME = 'net.faustctf.SuDoD' | |||
SERVICE_INTERFACE = SERVICE_NAME | |||
OBJECT_PATH = "/" + SERVICE_NAME.replace(".", "/") | |||
GUARD_INTERFACE = SERVICE_INTERFACE+".Guard" | |||
@dataclass | |||
class Command: | |||
argv: List[str] | |||
user: str | |||
group: str | |||
class CommandGuard(dbus.service.Object): | |||
def __init__(self, bus, command, session, allowed_client): | |||
super().__init__(conn = bus, object_path = "/guard") | |||
self.allowed_env = "XAUTHORIZATION XAUTHORITY PS2 PS1 LS_COLORS KRB5CCNAME HOSTNAME DPKG_COLORS DISPLAY COLORS SSH_ORIGINAL_COMMAND".split() | |||
self.allowed_client = allowed_client | |||
self.command = command | |||
self.session = session | |||
self.polkit = dbus.Interface( | |||
bus.get_object("org.freedesktop.PolicyKit1", "/org/freedesktop/PolicyKit1/Authority") | |||
, "org.freedesktop.PolicyKit1.Authority") | |||
self.org_freedesktop_DBus = dbus.Interface( | |||
bus.get_object("org.freedesktop.DBus", "/org/freedesktop/DBus"), | |||
"org.freedesktop.DBus") | |||
sudod = getClient(bus) | |||
self.authorized = None | |||
self.pid = None | |||
sender_uid = self.org_freedesktop_DBus.GetConnectionUnixUser(allowed_client) | |||
sender_user = pwd.getpwuid(sender_uid).pw_name | |||
self.sender_user = sender_user | |||
os.environ["SUDO_USER"] = sender_user | |||
# tell the server our busname, so it can return it to the client | |||
sudod.registerSession(session) | |||
# set the uid/gid now, so that chdirFD can do the correct permission check | |||
os.chdir("/") | |||
os.setgid(grp.getgrnam(command.group).gr_gid) | |||
os.setgroups(os.getgrouplist(command.user, os.getgid())) | |||
os.setuid(pwd.getpwnam(command.user).pw_uid) | |||
@dbus.service.method(GUARD_INTERFACE, out_signature = "b", in_signature="", sender_keyword = "sender") | |||
def polkitAuth(self, sender): | |||
if sender != self.allowed_client: raise RuntimeError("No") | |||
# start polkit authorization | |||
auth_details = {} | |||
for i,a in enumerate(self.command.argv): | |||
auth_details["argv_"+str(i)] = a | |||
auth_details["as_user"] = str(self.command.user) | |||
auth_details["as_group"] = str(self.command.group) | |||
(self.authorized, _, _) = self.polkit.CheckAuthorization(("system-bus-name", {"name":self.allowed_client}), "net.faustctf.SuDoD.RunCommand", auth_details, 1, self.session) | |||
return self.authorized | |||
@dbus.service.method(GUARD_INTERFACE, out_signature = "b", in_signature="", sender_keyword = "sender") | |||
def simpleAuth(self, sender): | |||
if self.command.user == self.sender_user and grp.getgrnam(self.command.group).gr_gid in os.getgrouplist(self.sender_user, pwd.getpwnam(self.sender_user).pw_gid): | |||
self.authorized = True | |||
elif self.sender_user == "root": | |||
self.authorized = True | |||
elif self.command.argv[0] in ["/opt/gh/sow.py", "/opt/gh/show.py"] and self.command.user == "greenhouses": | |||
self.authorized = True | |||
elif self.sender_user == "gate" and self.command.argv[0] == "/opt/bin/register.sh": | |||
self.authorized = True | |||
else: | |||
self.authorized = False | |||
return True | |||
@dbus.service.method(GUARD_INTERFACE, out_signature = "i", in_signature = "h", sender_keyword = "sender") | |||
def connectFD(self, fd, sender): | |||
if sender != self.allowed_client: raise RuntimeError("No") | |||
n = fd.take() | |||
return n | |||
@dbus.service.method(GUARD_INTERFACE, out_signature = "", in_signature = "i", sender_keyword = "sender") | |||
def closeFD(self, fd, sender): | |||
if sender != self.allowed_client: raise RuntimeError("No") | |||
os.close(fd) | |||
@dbus.service.method(GUARD_INTERFACE, out_signature = "", in_signature = "ii", sender_keyword = "sender") | |||
def dupFD(self, oldnum, newnum, sender): | |||
if sender != self.allowed_client: raise RuntimeError("No") | |||
os.dup2(oldnum, newnum) | |||
@dbus.service.method(GUARD_INTERFACE, out_signature = "", in_signature = "h", sender_keyword = "sender") | |||
def chdirFD(self, d, sender): | |||
if sender != self.allowed_client: raise RuntimeError("No") | |||
fd = d.take() | |||
os.fchdir(fd) | |||
os.close(fd) | |||
@dbus.service.method(GUARD_INTERFACE, out_signature = "", in_signature = "ss", sender_keyword = "sender") | |||
def setEnv(self, key, val, sender): | |||
if sender != self.allowed_client: raise RuntimeError("No") | |||
if not key in self.allowed_env: | |||
raise RuntimeError("Variable Forbidden") | |||
os.environ[key] = val | |||
@dbus.service.method(GUARD_INTERFACE, out_signature = "i", in_signature = "", sender_keyword = "sender") | |||
def run(self, sender): | |||
if sender != self.allowed_client: raise RuntimeError("No") | |||
if not self.authorized: | |||
raise RuntimeError("Not Authorized") | |||
signal.signal(signal.SIGCHLD, signal.SIG_DFL) | |||
pid = os.fork() | |||
if pid == 0: | |||
try: | |||
os.execvp(self.command.argv[0], self.command.argv) | |||
except Exception: | |||
os.write(2, f"Exception from SuDoD:\n{traceback.format_exc()}".encode()) | |||
finally: | |||
# NEVER return/raise into server process!! | |||
os._exit(1) | |||
self.pid = pid | |||
# TODO: Hopefully this PRIORITY_HIGH means, that | |||
# the callback is performed before a dbus call to kill | |||
GLib.child_watch_add(GLib.PRIORITY_HIGH, pid, self.on_wait) | |||
return pid | |||
def on_wait(self, pid, exitstatus): | |||
self.exited(exitstatus) | |||
# we are done | |||
os._exit(0) | |||
@dbus.service.method(GUARD_INTERFACE, out_signature = "", in_signature = "i", sender_keyword = "sender") | |||
def kill(self, sig, sender): | |||
if sender != self.allowed_client: raise RuntimeError("No") | |||
if self.pid is not None: | |||
os.kill(self.pid, sig) | |||
@dbus.service.signal(GUARD_INTERFACE, signature="i") | |||
def exited(self, status): | |||
pass | |||
class SuDoD(dbus.service.Object): | |||
def __init__(self, bus): | |||
self.bus = bus | |||
self.sessions = {} | |||
print("i am",bus.get_unique_name()) | |||
super().__init__(object_path = OBJECT_PATH, conn = self.bus) | |||
self.org_freedesktop_DBus = dbus.Interface( | |||
bus.get_object("org.freedesktop.DBus", "/org/freedesktop/DBus"), | |||
"org.freedesktop.DBus") | |||
# from `sudo sudo -V` on debian sid | |||
signal.signal(signal.SIGCHLD, signal.SIG_IGN) | |||
@dbus.service.method(SERVICE_INTERFACE, out_signature = "s", in_signature = "asss", sender_keyword = "sender", async_callbacks = ("return_cb", "error_cb")) | |||
def createSession(self, argv, user, group, sender, return_cb, error_cb): | |||
print("createSession called from", sender) | |||
session = secrets.token_hex(16) | |||
self.sessions[session] = (return_cb, error_cb) | |||
pid = os.fork() | |||
print("forked, pid =", pid) | |||
if pid == 0: | |||
try: | |||
os.execlp(sys.executable, sys.executable, __file__, "--session", session, user, group, sender, *argv) | |||
except Exception: | |||
os.write(2, f"Exception from SuDoD:\n{traceback.format_exc()}".encode()) | |||
finally: | |||
os._exit(0) | |||
@dbus.service.method(SERVICE_INTERFACE, out_signature = "", in_signature = "s", sender_keyword = "sender") | |||
def registerSession(self, session, sender): | |||
x = self.sessions[session] | |||
if isinstance(x, str): return | |||
(return_cb, error_cb) = self.sessions.pop(session) | |||
return_cb(sender) | |||
#system_bus.publish(SERVICE_NAME, SuDoD()) | |||
def getClient(system_bus): | |||
o = system_bus.get_object(SERVICE_NAME, OBJECT_PATH) | |||
i = dbus.Interface(o, SERVICE_INTERFACE) | |||
return i | |||
def guard_main(): | |||
_me, _flag, session, user, group, sender, *argv = sys.argv | |||
command = Command(argv, user=user, group=group) | |||
loop = GLib.MainLoop() | |||
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) | |||
system_bus = dbus.SystemBus() | |||
g = CommandGuard(system_bus, command, session, sender) | |||
loop.run() | |||
def server_main(): | |||
loop = GLib.MainLoop() | |||
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) | |||
system_bus = dbus.SystemBus() | |||
name = dbus.service.BusName(SERVICE_NAME, bus = system_bus) | |||
sudod = SuDoD(system_bus) | |||
loop.run() | |||
if __name__ == "__main__": | |||
args = sys.argv[1:] | |||
if len(sys.argv) <= 1: | |||
server_main() | |||
elif sys.argv[1] == "--session": | |||
guard_main() | |||
else: | |||
print("invalid usage, please look at the code.") | |||
exit(1) | |||
@@ -0,0 +1,4 @@ | |||
import sqlite3 | |||
conn = sqlite3.connect('/var/greenhouses/greenhouses.db') | |||
c = conn.cursor() | |||
c.execute('CREATE TABLE IF NOT EXISTS seeds (owner VARCHAR(64), seed varchar(64), generation integer default 0);') |
@@ -0,0 +1,52 @@ | |||
#!/usr/bin/python3 | |||
import os | |||
import random | |||
N = 20 | |||
WATER = 100 | |||
NATURE = [ | |||
[" "], | |||
["\x1b[33m. "], | |||
["\x1b[32m* "], | |||
["\x1b[34m* "], | |||
["\x1b[31mo "], | |||
] | |||
def newfield(): | |||
return [[(0," ") for j in range(20)] for i in range(20)] | |||
def change(rand, field): | |||
for _ in range(WATER): | |||
row = rand.randrange(N) | |||
col = rand.randrange(N) | |||
level,_ = field[row][col] | |||
level += 1 | |||
plant = rand.choice(NATURE[level % len(NATURE)]) | |||
field[row][col] = (level,plant) | |||
def genpic(seed, generation): | |||
f = newfield() | |||
for g in range(generation): | |||
rand = random.Random(seed + "-" + str(g)) | |||
change(rand, f) | |||
res = ["Seed: %s\n"%seed, "Generation: %d\n"%generation] | |||
res.append("+" + (2*N) * "-" + "+\n") | |||
for line in f: | |||
res.append("|") | |||
for (_,plant) in line: | |||
res.append(plant) | |||
res.append("\x1b(B\x1b[m|\n") | |||
res.append("+" + (2*N) * "-" + "+\n") | |||
return "".join(res) | |||
if __name__ == "__main__": | |||
from db import c | |||
owner = os.environ["SUDO_USER"] | |||
for (seed, generation) in c.execute("SELECT seed, generation from seeds where owner = ? order by seed", [owner]): | |||
print(genpic(seed, generation)) | |||
@@ -0,0 +1,11 @@ | |||
#!/usr/bin/python3 | |||
import os | |||
from db import c, conn | |||
owner = os.environ["SUDO_USER"] | |||
print("Welcome to your new greenhouse.") | |||
seed = input("What would you like to sow?> ") | |||
c.execute("INSERT INTO seeds (owner, seed) values (?,?)", (owner, seed)) | |||
conn.commit() | |||
@@ -0,0 +1,4 @@ | |||
from db import c, conn | |||
c.execute("update seeds set generation = generation+1") | |||
conn.commit() |
@@ -0,0 +1 @@ | |||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEpawv6A0tpdlhGIezISVGgrKspHS2nlHv1WGuN5iW33 greenhouses-checker@2020.faustctf.net |
@@ -0,0 +1,20 @@ | |||
<?xml version="1.0" encoding="UTF-8"?> | |||
<!DOCTYPE policyconfig PUBLIC "-//freedesktop//DTD polkit Policy Configuration 1.0//EN" | |||
"http://www.freedesktop.org/software/polkit/policyconfig-1.dtd"> | |||
<policyconfig> | |||
<vendor>FAUST-CTF 2020</vendor> | |||
<vendor_url>https://2020.faustctf.net/</vendor_url> | |||
<action id="net.faustctf.SuDoD.RunCommand"> | |||
<description>Run a Command with SuDoD</description> | |||
<message>Authentication is required to run $(argv_0)</message> | |||
<icon_name>preferences-system</icon_name> | |||
<defaults> | |||
<allow_any>no</allow_any> | |||
<allow_inactive>no</allow_inactive> | |||
<allow_active>auth_admin_keep</allow_active> | |||
</defaults> | |||
</action> | |||
</policyconfig> |
@@ -0,0 +1,12 @@ | |||
# ![Logo](./web/static/img/logo_small.png "IPPS") Interplanetary Parcel Service (IPPS) | |||
This is the repository of IPPS's web services. | |||
## Building | |||
`go build cmd/ipps` | |||
## Running | |||
1. Copy the default configuration file `configs/defaults.toml` to `./config.toml` | |||
2. Apply changes to the configuration file as necessary for the server infrastructure. | |||
3. Copy to the systemd configuration `init/systemd` to `/etc/systemd/` | |||
4. Start the systemd service |
@@ -0,0 +1,109 @@ | |||
package main | |||
import ( | |||
"flag" | |||
"gitlab.cs.fau.de/faust/faustctf-2020/ipps/internal/grpc" | |||
"log" | |||
"github.com/BurntSushi/toml" | |||
"gitlab.cs.fau.de/faust/faustctf-2020/ipps/internal/http" | |||
"gitlab.cs.fau.de/faust/faustctf-2020/ipps/internal/session" | |||
"gitlab.cs.fau.de/faust/faustctf-2020/ipps/pkg/postgres" | |||
) | |||
type config struct { | |||
Database *postgres.Config | |||
Server *http.Config | |||
Session *session.Config | |||
GRPC *grpc.Config | |||
} | |||
func main() { | |||
var configPath string | |||
flag.StringVar(&configPath, "c", "./config.toml", | |||
"use another configuration file") | |||
flag.Parse() | |||
conf := &config{} | |||
_, err := toml.DecodeFile(configPath, conf) | |||
if err != nil { | |||
log.Fatal(err) | |||
} | |||
db, err := postgres.Connect(conf.Database) | |||
if err != nil { | |||
log.Fatal(err) | |||
} | |||
defer db.Close() | |||
err = postgres.InstallTables(db) | |||
if err != nil { | |||
log.Fatalf("error installing tables: %v\n", err) | |||
} | |||
as, err := postgres.NewAddressStorage(db) | |||
if err != nil { | |||
log.Fatal(err) | |||
} | |||
cs, err := postgres.NewCreditCardStorage(db) | |||
if err != nil { | |||
log.Fatal(err) | |||
} | |||
defer cs.Close() | |||
es, err := postgres.NewEventStorage(db) | |||
if err != nil { | |||
log.Fatal(err) | |||
} | |||
defer es.Close() | |||
fs, err := postgres.NewFeedbackStorage(db) | |||
if err != nil { | |||
log.Fatal(err) | |||
} | |||
defer fs.Close() | |||
ps, err := postgres.NewParcelStorage(db) | |||
if err != nil { | |||
log.Fatal(err) | |||
} | |||
us, err := postgres.NewUserStorage(db) | |||
if err != nil { | |||
log.Fatal(err) | |||
} | |||
defer us.Close() | |||
go runGRPCServer(conf) | |||
s := http.Server{ | |||
AddressStorage: as, | |||
CreditStorage: cs, | |||
EventStorage: es, | |||
FeedbackStorage: fs, | |||
ParcelStorage: ps, | |||
UserStorage: us, | |||
} | |||
log.Fatal(s.ListenAndServe(conf.Server, conf.Session)) | |||
} | |||
func runGRPCServer(c *config) { | |||
db, err := postgres.Connect(c.Database) | |||
if err != nil { | |||
log.Fatal(err) | |||
} | |||
defer db.Close() | |||
as, err := postgres.NewAddressStorage(db) | |||
if err != nil { | |||
log.Fatal(err) | |||
} | |||
cs, err := postgres.NewCreditCardStorage(db) | |||
if err != nil { | |||
log.Fatal(err) | |||
} | |||
defer cs.Close() | |||
us, err := postgres.NewUserStorage(db) | |||
if err != nil { | |||
log.Fatal(err) | |||
} | |||
defer us.Close() | |||
s, err := grpc.NewServer(c.GRPC, as, cs, us) | |||
if err != nil { | |||
log.Fatal(err) | |||
} | |||
log.Fatal(s.ListenAndServe()) | |||
} |
@@ -0,0 +1,20 @@ | |||
[database] | |||
hostname = "/run/postgresql" | |||
port = 5432 | |||
name = "ipps" | |||
username = "ipps" | |||
password = "" | |||
[server] | |||
address = ":8000" | |||
read_timeout = "15s" | |||
write_timeout = "15s" | |||
[grpc] | |||
address = ":8001" | |||
private_key_file = "./privkey.pem" | |||
public_key_file = "./pubkey.pem" | |||
[session] | |||
Name = "ipps_session" | |||
Key = "d3f4u1t5_c4n_b3_r3411y_d4ng3r0u5" |
@@ -0,0 +1,20 @@ | |||
[database] | |||
hostname = "/run/postgresql" | |||
port = 5432 | |||
name = "ipps" | |||
username = "ipps" | |||
password = "" | |||
[server] | |||
address = ":8000" | |||
read_timeout = "15s" | |||
write_timeout = "15s" | |||
[grpc] | |||
address = ":8001" | |||
private_key_file = "./privkey.pem" | |||
public_key_file = "./pubkey.pem" | |||
[session] | |||
Name = "ipps_session" | |||
Key = "d3f4u1t5_c4n_b3_r3411y_d4ng3r0u5" |
@@ -0,0 +1 @@ | |||
module gitlab.cs.fau.de/faust/faustctf-2020/ipps |
@@ -0,0 +1,647 @@ | |||
// Code generated by protoc-gen-go. DO NOT EDIT. | |||
// source: ipps.proto | |||
package grpc | |||
import ( | |||
"context" | |||
"fmt" | |||
"github.com/golang/protobuf/proto" | |||
"github.com/golang/protobuf/ptypes/empty" | |||
"google.golang.org/grpc" | |||
"google.golang.org/grpc/codes" | |||
"google.golang.org/grpc/status" | |||
"math" | |||
) | |||
// Reference imports to suppress errors if they are not otherwise used. | |||
var _ = proto.Marshal | |||
var _ = fmt.Errorf | |||
var _ = math.Inf | |||
// This is a compile-time assertion to ensure that this generated file | |||
// is compatible with the proto package it is being compiled against. | |||
// A compilation error at this line likely means your copy of the | |||
// proto package needs to be updated. | |||
const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package | |||
type LoginRequest struct { | |||
Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"` | |||
Password []byte `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"` | |||
XXX_NoUnkeyedLiteral struct{} `json:"-"` | |||
XXX_unrecognized []byte `json:"-"` | |||
XXX_sizecache int32 `json:"-"` | |||
} | |||
func (m *LoginRequest) Reset() { *m = LoginRequest{} } | |||
func (m *LoginRequest) String() string { return proto.CompactTextString(m) } | |||
func (*LoginRequest) ProtoMessage() {} | |||
func (*LoginRequest) Descriptor() ([]byte, []int) { | |||
return fileDescriptor_e433d43e56f7944c, []int{0} | |||
} | |||
func (m *LoginRequest) XXX_Unmarshal(b []byte) error { | |||
return xxx_messageInfo_LoginRequest.Unmarshal(m, b) | |||
} | |||
func (m *LoginRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { | |||
return xxx_messageInfo_LoginRequest.Marshal(b, m, deterministic) | |||
} | |||
func (m *LoginRequest) XXX_Merge(src proto.Message) { | |||
xxx_messageInfo_LoginRequest.Merge(m, src) | |||
} | |||
func (m *LoginRequest) XXX_Size() int { | |||
return xxx_messageInfo_LoginRequest.Size(m) | |||
} | |||
func (m *LoginRequest) XXX_DiscardUnknown() { | |||
xxx_messageInfo_LoginRequest.DiscardUnknown(m) | |||
} | |||
var xxx_messageInfo_LoginRequest proto.InternalMessageInfo | |||
func (m *LoginRequest) GetUsername() string { | |||
if m != nil { | |||
return m.Username | |||
} | |||
return "" | |||
} | |||
func (m *LoginRequest) GetPassword() []byte { | |||
if m != nil { | |||
return m.Password | |||
} | |||
return nil | |||
} | |||
type LoginResponse struct { | |||
AuthToken string `protobuf:"bytes,1,opt,name=authToken,proto3" json:"authToken,omitempty"` | |||
XXX_NoUnkeyedLiteral struct{} `json:"-"` | |||
XXX_unrecognized []byte `json:"-"` | |||
XXX_sizecache int32 `json:"-"` | |||
} | |||
func (m *LoginResponse) Reset() { *m = LoginResponse{} } | |||
func (m *LoginResponse) String() string { return proto.CompactTextString(m) } | |||
func (*LoginResponse) ProtoMessage() {} | |||
func (*LoginResponse) Descriptor() ([]byte, []int) { | |||
return fileDescriptor_e433d43e56f7944c, []int{1} | |||
} | |||
func (m *LoginResponse) XXX_Unmarshal(b []byte) error { | |||
return xxx_messageInfo_LoginResponse.Unmarshal(m, b) | |||
} | |||
func (m *LoginResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { | |||
return xxx_messageInfo_LoginResponse.Marshal(b, m, deterministic) | |||
} | |||
func (m *LoginResponse) XXX_Merge(src proto.Message) { | |||
xxx_messageInfo_LoginResponse.Merge(m, src) | |||
} | |||
func (m *LoginResponse) XXX_Size() int { | |||
return xxx_messageInfo_LoginResponse.Size(m) | |||
} | |||
func (m *LoginResponse) XXX_DiscardUnknown() { | |||
xxx_messageInfo_LoginResponse.DiscardUnknown(m) | |||
} | |||
var xxx_messageInfo_LoginResponse proto.InternalMessageInfo | |||
func (m *LoginResponse) GetAuthToken() string { | |||
if m != nil { | |||
return m.AuthToken | |||
} | |||
return "" | |||
} | |||
type PublicKey struct { | |||
Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` | |||
XXX_NoUnkeyedLiteral struct{} `json:"-"` | |||
XXX_unrecognized []byte `json:"-"` | |||
XXX_sizecache int32 `json:"-"` | |||
} | |||
func (m *PublicKey) Reset() { *m = PublicKey{} } | |||
func (m *PublicKey) String() string { return proto.CompactTextString(m) } | |||
func (*PublicKey) ProtoMessage() {} | |||
func (*PublicKey) Descriptor() ([]byte, []int) { | |||
return fileDescriptor_e433d43e56f7944c, []int{2} | |||
} | |||
func (m *PublicKey) XXX_Unmarshal(b []byte) error { | |||
return xxx_messageInfo_PublicKey.Unmarshal(m, b) | |||
} | |||
func (m *PublicKey) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { | |||
return xxx_messageInfo_PublicKey.Marshal(b, m, deterministic) | |||
} | |||
func (m *PublicKey) XXX_Merge(src proto.Message) { | |||
xxx_messageInfo_PublicKey.Merge(m, src) | |||
} | |||
func (m *PublicKey) XXX_Size() int { | |||
return xxx_messageInfo_PublicKey.Size(m) | |||
} | |||
func (m *PublicKey) XXX_DiscardUnknown() { | |||
xxx_messageInfo_PublicKey.DiscardUnknown(m) | |||
} | |||
var xxx_messageInfo_PublicKey proto.InternalMessageInfo | |||
func (m *PublicKey) GetKey() string { | |||
if m != nil { | |||
return m.Key | |||
} | |||
return "" | |||
} | |||
type CreditCard struct { | |||
Number string `protobuf:"bytes,1,opt,name=number,proto3" json:"number,omitempty"` | |||
XXX_NoUnkeyedLiteral struct{} `json:"-"` | |||
XXX_unrecognized []byte `json:"-"` | |||
XXX_sizecache int32 `json:"-"` | |||
} | |||
func (m *CreditCard) Reset() { *m = CreditCard{} } | |||
func (m *CreditCard) String() string { return proto.CompactTextString(m) } | |||
func (*CreditCard) ProtoMessage() {} | |||
func (*CreditCard) Descriptor() ([]byte, []int) { | |||
return fileDescriptor_e433d43e56f7944c, []int{3} | |||
} | |||
func (m *CreditCard) XXX_Unmarshal(b []byte) error { | |||
return xxx_messageInfo_CreditCard.Unmarshal(m, b) | |||
} | |||
func (m *CreditCard) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { | |||
return xxx_messageInfo_CreditCard.Marshal(b, m, deterministic) | |||
} | |||
func (m *CreditCard) XXX_Merge(src proto.Message) { | |||
xxx_messageInfo_CreditCard.Merge(m, src) | |||
} | |||
func (m *CreditCard) XXX_Size() int { | |||
return xxx_messageInfo_CreditCard.Size(m) | |||
} | |||
func (m *CreditCard) XXX_DiscardUnknown() { | |||
xxx_messageInfo_CreditCard.DiscardUnknown(m) | |||
} | |||
var xxx_messageInfo_CreditCard proto.InternalMessageInfo | |||
func (m *CreditCard) GetNumber() string { | |||
if m != nil { | |||
return m.Number | |||
} | |||
return "" | |||
} | |||
type CreditCards struct { | |||
Cards []*CreditCard `protobuf:"bytes,1,rep,name=cards,proto3" json:"cards,omitempty"` | |||
XXX_NoUnkeyedLiteral struct{} `json:"-"` | |||
XXX_unrecognized []byte `json:"-"` | |||
XXX_sizecache int32 `json:"-"` | |||
} | |||
func (m *CreditCards) Reset() { *m = CreditCards{} } | |||
func (m *CreditCards) String() string { return proto.CompactTextString(m) } | |||
func (*CreditCards) ProtoMessage() {} | |||
func (*CreditCards) Descriptor() ([]byte, []int) { | |||
return fileDescriptor_e433d43e56f7944c, []int{4} | |||
} | |||
func (m *CreditCards) XXX_Unmarshal(b []byte) error { | |||
return xxx_messageInfo_CreditCards.Unmarshal(m, b) | |||
} | |||
func (m *CreditCards) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { | |||
return xxx_messageInfo_CreditCards.Marshal(b, m, deterministic) | |||
} | |||
func (m *CreditCards) XXX_Merge(src proto.Message) { | |||
xxx_messageInfo_CreditCards.Merge(m, src) | |||
} | |||
func (m *CreditCards) XXX_Size() int { | |||
return xxx_messageInfo_CreditCards.Size(m) | |||
} | |||
func (m *CreditCards) XXX_DiscardUnknown() { | |||
xxx_messageInfo_CreditCards.DiscardUnknown(m) | |||
} | |||
var xxx_messageInfo_CreditCards proto.InternalMessageInfo | |||
func (m *CreditCards) GetCards() []*CreditCard { | |||
if m != nil { | |||
return m.Cards | |||
} | |||
return nil | |||
} | |||
type Address struct { | |||
Street string `protobuf:"bytes,1,opt,name=street,proto3" json:"street,omitempty"` | |||
Zip string `protobuf:"bytes,2,opt,name=zip,proto3" json:"zip,omitempty"` | |||
City string `protobuf:"bytes,3,opt,name=city,proto3" json:"city,omitempty"` | |||
Country string `protobuf:"bytes,4,opt,name=country,proto3" json:"country,omitempty"` | |||
Planet string `protobuf:"bytes,5,opt,name=planet,proto3" json:"planet,omitempty"` | |||
XXX_NoUnkeyedLiteral struct{} `json:"-"` | |||
XXX_unrecognized []byte `json:"-"` | |||
XXX_sizecache int32 `json:"-"` | |||
} | |||
func (m *Address) Reset() { *m = Address{} } | |||
func (m *Address) String() string { return proto.CompactTextString(m) } | |||
func (*Address) ProtoMessage() {} | |||
func (*Address) Descriptor() ([]byte, []int) { | |||
return fileDescriptor_e433d43e56f7944c, []int{5} | |||
} | |||
func (m *Address) XXX_Unmarshal(b []byte) error { | |||
return xxx_messageInfo_Address.Unmarshal(m, b) | |||
} | |||
func (m *Address) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { | |||
return xxx_messageInfo_Address.Marshal(b, m, deterministic) | |||
} | |||
func (m *Address) XXX_Merge(src proto.Message) { | |||
xxx_messageInfo_Address.Merge(m, src) | |||
} | |||
func (m *Address) XXX_Size() int { | |||
return xxx_messageInfo_Address.Size(m) | |||
} | |||
func (m *Address) XXX_DiscardUnknown() { | |||
xxx_messageInfo_Address.DiscardUnknown(m) | |||
} | |||
var xxx_messageInfo_Address proto.InternalMessageInfo | |||
func (m *Address) GetStreet() string { | |||
if m != nil { | |||
return m.Street | |||
} | |||
return "" | |||
} | |||
func (m *Address) GetZip() string { | |||
if m != nil { | |||
return m.Zip | |||
} | |||
return "" | |||
} | |||
func (m *Address) GetCity() string { | |||
if m != nil { | |||
return m.City | |||
} | |||
return "" | |||
} | |||
func (m *Address) GetCountry() string { | |||
if m != nil { | |||
return m.Country | |||
} | |||
return "" | |||
} | |||
func (m *Address) GetPlanet() string { | |||
if m != nil { | |||
return m.Planet | |||
} | |||
return "" | |||
} | |||
type Addresses struct { | |||
Addresses []*Address `protobuf:"bytes,1,rep,name=addresses,proto3" json:"addresses,omitempty"` | |||
XXX_NoUnkeyedLiteral struct{} `json:"-"` | |||
XXX_unrecognized []byte `json:"-"` | |||
XXX_sizecache int32 `json:"-"` | |||
} | |||
func (m *Addresses) Reset() { *m = Addresses{} } | |||
func (m *Addresses) String() string { return proto.CompactTextString(m) } | |||
func (*Addresses) ProtoMessage() {} | |||
func (*Addresses) Descriptor() ([]byte, []int) { | |||
return fileDescriptor_e433d43e56f7944c, []int{6} | |||
} | |||
func (m *Addresses) XXX_Unmarshal(b []byte) error { | |||
return xxx_messageInfo_Addresses.Unmarshal(m, b) | |||
} | |||
func (m *Addresses) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { | |||
return xxx_messageInfo_Addresses.Marshal(b, m, deterministic) | |||
} | |||
func (m *Addresses) XXX_Merge(src proto.Message) { | |||
xxx_messageInfo_Addresses.Merge(m, src) | |||
} | |||
func (m *Addresses) XXX_Size() int { | |||
return xxx_messageInfo_Addresses.Size(m) | |||
} | |||
func (m *Addresses) XXX_DiscardUnknown() { | |||
xxx_messageInfo_Addresses.DiscardUnknown(m) | |||
} | |||
var xxx_messageInfo_Addresses proto.InternalMessageInfo | |||
func (m *Addresses) GetAddresses() []*Address { | |||
if m != nil { | |||
return m.Addresses | |||
} | |||
return nil | |||
} | |||
func init() { | |||
proto.RegisterType((*LoginRequest)(nil), "grpc.LoginRequest") | |||
proto.RegisterType((*LoginResponse)(nil), "grpc.LoginResponse") | |||
proto.RegisterType((*PublicKey)(nil), "grpc.PublicKey") | |||
proto.RegisterType((*CreditCard)(nil), "grpc.CreditCard") | |||
proto.RegisterType((*CreditCards)(nil), "grpc.CreditCards") | |||
proto.RegisterType((*Address)(nil), "grpc.Address") | |||
proto.RegisterType((*Addresses)(nil), "grpc.Addresses") | |||
} | |||
func init() { | |||
proto.RegisterFile("ipps.proto", fileDescriptor_e433d43e56f7944c) | |||
} | |||
var fileDescriptor_e433d43e56f7944c = []byte{ | |||
// 467 bytes of a gzipped FileDescriptorProto | |||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x84, 0x53, 0x5d, 0x6b, 0xd4, 0x4c, | |||
0x14, 0xde, 0xed, 0xee, 0xb6, 0x6f, 0x4e, 0x77, 0x5f, 0xeb, 0x08, 0x25, 0x44, 0x85, 0x65, 0x10, | |||
0x59, 0x90, 0x26, 0x25, 0x52, 0xb4, 0x88, 0x17, 0x6b, 0xd1, 0x22, 0x7a, 0xb1, 0x44, 0xaf, 0xbc, | |||
0x4b, 0x32, 0x67, 0x63, 0x68, 0x36, 0x19, 0xe7, 0x03, 0x89, 0x7f, 0xd7, 0x3f, 0x22, 0x93, 0xc9, | |||
0xc7, 0x56, 0xa9, 0xde, 0x84, 0xf3, 0x9c, 0xf3, 0xe4, 0x39, 0x9f, 0x03, 0x90, 0x73, 0x2e, 0x7d, | |||
0x2e, 0x2a, 0x55, 0x91, 0x69, 0x26, 0x78, 0xea, 0x3d, 0xcc, 0xaa, 0x2a, 0x2b, 0x30, 0x68, 0x7c, | |||
0x89, 0xde, 0x06, 0xb8, 0xe3, 0xaa, 0xb6, 0x14, 0xfa, 0x0e, 0xe6, 0x1f, 0xab, 0x2c, 0x2f, 0x23, | |||
0xfc, 0xa6, 0x51, 0x2a, 0xe2, 0xc1, 0x7f, 0x5a, 0xa2, 0x28, 0xe3, 0x1d, 0xba, 0xe3, 0xe5, 0x78, | |||
0xe5, 0x44, 0x3d, 0x36, 0x31, 0x1e, 0x4b, 0xf9, 0xbd, 0x12, 0xcc, 0x3d, 0x58, 0x8e, 0x57, 0xf3, | |||
0xa8, 0xc7, 0xf4, 0x0c, 0x16, 0xad, 0x8e, 0xe4, 0x55, 0x29, 0x91, 0x3c, 0x02, 0x27, 0xd6, 0xea, | |||
0xeb, 0xe7, 0xea, 0x06, 0xcb, 0x56, 0x69, 0x70, 0xd0, 0xc7, 0xe0, 0x6c, 0x74, 0x52, 0xe4, 0xe9, | |||
0x07, 0xac, 0xc9, 0x09, 0x4c, 0x6e, 0xb0, 0x6e, 0x49, 0xc6, 0xa4, 0x4f, 0x00, 0xae, 0x04, 0xb2, | |||
0x5c, 0x5d, 0xc5, 0x82, 0x91, 0x53, 0x38, 0x2c, 0xf5, 0x2e, 0x41, 0xd1, 0x52, 0x5a, 0x44, 0x2f, | |||
0xe0, 0x78, 0x60, 0x49, 0xf2, 0x14, 0x66, 0xa9, 0x31, 0xdc, 0xf1, 0x72, 0xb2, 0x3a, 0x0e, 0x4f, | |||
0x7c, 0xd3, 0xbd, 0x3f, 0x30, 0x22, 0x1b, 0xa6, 0x35, 0x1c, 0xad, 0x19, 0x13, 0x28, 0xa5, 0x51, | |||
0x96, 0x4a, 0x20, 0xaa, 0x4e, 0xd9, 0x22, 0x53, 0xd1, 0x8f, 0x9c, 0x37, 0x4d, 0x3a, 0x91, 0x31, | |||
0x09, 0x81, 0x69, 0x9a, 0xab, 0xda, 0x9d, 0x34, 0xae, 0xc6, 0x26, 0x2e, 0x1c, 0xa5, 0x95, 0x2e, | |||
0x95, 0xa8, 0xdd, 0x69, 0xe3, 0xee, 0xa0, 0xd1, 0xe5, 0x45, 0x5c, 0xa2, 0x72, 0x67, 0x56, 0xd7, | |||
0x22, 0xfa, 0x12, 0x9c, 0x36, 0x35, 0x4a, 0xf2, 0x0c, 0x9c, 0xb8, 0x03, 0x6d, 0xcd, 0x0b, 0x5b, | |||
0x73, 0xcb, 0x89, 0x86, 0x78, 0xf8, 0xf3, 0x00, 0xa6, 0xef, 0x37, 0x9b, 0x4f, 0x24, 0x84, 0x59, | |||
0x33, 0x68, 0x42, 0x2c, 0x77, 0x7f, 0x7b, 0xde, 0x83, 0x5b, 0x3e, 0xbb, 0x09, 0x3a, 0x22, 0x97, | |||
0x30, 0xbf, 0x46, 0x35, 0x0c, 0xfc, 0xd4, 0xb7, 0x27, 0xe1, 0x77, 0x27, 0xe1, 0xbf, 0x35, 0x27, | |||
0xe1, 0xdd, 0xb3, 0xbf, 0xf7, 0x44, 0x3a, 0x22, 0x17, 0x00, 0x6b, 0xc6, 0xba, 0x79, 0xdd, 0xae, | |||
0xcf, 0xbb, 0x43, 0xa7, 0xcf, 0x38, 0xf4, 0xfa, 0x8f, 0x8c, 0x3d, 0x91, 0x8e, 0xc8, 0x2b, 0x58, | |||
0xac, 0x19, 0xdb, 0x5b, 0xff, 0x1f, 0x8b, 0xfc, 0x4b, 0xde, 0xd7, 0xf0, 0xff, 0x35, 0xaa, 0xfd, | |||
0xab, 0xb8, 0x2b, 0xf3, 0xfd, 0xdf, 0x55, 0x25, 0x1d, 0xbd, 0xb9, 0xfc, 0xf2, 0x22, 0xcb, 0x55, | |||
0x11, 0x27, 0x7e, 0x2a, 0xfd, 0x6d, 0xac, 0x7d, 0x86, 0xc1, 0x36, 0xd6, 0x52, 0xd9, 0x6f, 0xaa, | |||
0xb6, 0x67, 0xe1, 0x79, 0x78, 0x1e, 0x98, 0x37, 0x16, 0xe4, 0xa5, 0x32, 0x0f, 0xa3, 0x08, 0x8c, | |||
0x50, 0x72, 0xd8, 0xe8, 0x3f, 0xff, 0x15, 0x00, 0x00, 0xff, 0xff, 0x97, 0xc8, 0x47, 0xb8, 0x80, | |||
0x03, 0x00, 0x00, | |||
} | |||
// Reference imports to suppress errors if they are not otherwise used. | |||
var _ context.Context | |||
var _ grpc.ClientConnInterface | |||
// This is a compile-time assertion to ensure that this generated file | |||
// is compatible with the grpc package it is being compiled against. | |||
const _ = grpc.SupportPackageIsVersion6 | |||
// IPPSClient is the client API for IPPS service. | |||
// | |||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. | |||
type IPPSClient interface { | |||
Login(ctx context.Context, in *LoginRequest, opts ...grpc.CallOption) (*LoginResponse, error) | |||
GetPublicKey(ctx context.Context, in *empty.Empty, opts ...grpc.CallOption) (*PublicKey, error) | |||
AddAddress(ctx context.Context, in *Address, opts ...grpc.CallOption) (*empty.Empty, error) | |||
GetAddresses(ctx context.Context, in *empty.Empty, opts ...grpc.CallOption) (*Addresses, error) | |||
AddCreditCard(ctx context.Context, in *CreditCard, opts ...grpc.CallOption) (*empty.Empty, error) | |||
GetCreditCards(ctx context.Context, in *empty.Empty, opts ...grpc.CallOption) (*CreditCards, error) | |||
} | |||
type iPPSClient struct { | |||
cc grpc.ClientConnInterface | |||
} | |||
func NewIPPSClient(cc grpc.ClientConnInterface) IPPSClient { | |||
return &iPPSClient{cc} | |||
} | |||
func (c *iPPSClient) Login(ctx context.Context, in *LoginRequest, opts ...grpc.CallOption) (*LoginResponse, error) { | |||
out := new(LoginResponse) | |||
err := c.cc.Invoke(ctx, "/grpc.IPPS/Login", in, out, opts...) | |||
if err != nil { | |||
return nil, err | |||
} | |||
return out, nil | |||
} | |||
func (c *iPPSClient) GetPublicKey(ctx context.Context, in *empty.Empty, opts ...grpc.CallOption) (*PublicKey, error) { | |||
out := new(PublicKey) | |||
err := c.cc.Invoke(ctx, "/grpc.IPPS/GetPublicKey", in, out, opts...) | |||
if err != nil { | |||
return nil, err | |||
} | |||
return out, nil | |||
} | |||
func (c *iPPSClient) AddAddress(ctx context.Context, in *Address, opts ...grpc.CallOption) (*empty.Empty, error) { | |||
out := new(empty.Empty) | |||
err := c.cc.Invoke(ctx, "/grpc.IPPS/AddAddress", in, out, opts...) | |||
if err != nil { | |||
return nil, err | |||
} | |||
return out, nil | |||
} | |||
func (c *iPPSClient) GetAddresses(ctx context.Context, in *empty.Empty, opts ...grpc.CallOption) (*Addresses, error) { | |||
out := new(Addresses) | |||
err := c.cc.Invoke(ctx, "/grpc.IPPS/GetAddresses", in, out, opts...) | |||
if err != nil { | |||
return nil, err | |||
} | |||
return out, nil | |||
} | |||
func (c *iPPSClient) AddCreditCard(ctx context.Context, in *CreditCard, opts ...grpc.CallOption) (*empty.Empty, error) { | |||
out := new(empty.Empty) | |||
err := c.cc.Invoke(ctx, "/grpc.IPPS/AddCreditCard", in, out, opts...) | |||
if err != nil { | |||
return nil, err | |||
} | |||
return out, nil | |||
} | |||
func (c *iPPSClient) GetCreditCards(ctx context.Context, in *empty.Empty, opts ...grpc.CallOption) (*CreditCards, error) { | |||
out := new(CreditCards) | |||
err := c.cc.Invoke(ctx, "/grpc.IPPS/GetCreditCards", in, out, opts...) | |||
if err != nil { | |||
return nil, err | |||
} | |||
return out, nil | |||
} | |||
// IPPSServer is the server API for IPPS service. | |||
type IPPSServer interface { | |||
Login(context.Context, *LoginRequest) (*LoginResponse, error) | |||
GetPublicKey(context.Context, *empty.Empty) (*PublicKey, error) | |||
AddAddress(context.Context, *Address) (*empty.Empty, error) | |||
GetAddresses(context.Context, *empty.Empty) (*Addresses, error) | |||
AddCreditCard(context.Context, *CreditCard) (*empty.Empty, error) | |||
GetCreditCards(context.Context, *empty.Empty) (*CreditCards, error) | |||
} | |||
// UnimplementedIPPSServer can be embedded to have forward compatible implementations. | |||
type UnimplementedIPPSServer struct { | |||
} | |||
func (*UnimplementedIPPSServer) Login(ctx context.Context, req *LoginRequest) (*LoginResponse, error) { | |||
return nil, status.Errorf(codes.Unimplemented, "method Login not implemented") | |||
} | |||
func (*UnimplementedIPPSServer) GetPublicKey(ctx context.Context, req *empty.Empty) (*PublicKey, error) { | |||
return nil, status.Errorf(codes.Unimplemented, "method GetPublicKey not implemented") | |||
} | |||
func (*UnimplementedIPPSServer) AddAddress(ctx context.Context, req *Address) (*empty.Empty, error) { | |||
return nil, status.Errorf(codes.Unimplemented, "method AddAddress not implemented") | |||
} | |||
func (*UnimplementedIPPSServer) GetAddresses(ctx context.Context, req *empty.Empty) (*Addresses, error) { | |||
return nil, status.Errorf(codes.Unimplemented, "method GetAddresses not implemented") | |||
} | |||
func (*UnimplementedIPPSServer) AddCreditCard(ctx context.Context, req *CreditCard) (*empty.Empty, error) { | |||
return nil, status.Errorf(codes.Unimplemented, "method AddCreditCard not implemented") | |||
} | |||
func (*UnimplementedIPPSServer) GetCreditCards(ctx context.Context, req *empty.Empty) (*CreditCards, error) { | |||
return nil, status.Errorf(codes.Unimplemented, "method GetCreditCards not implemented") | |||
} | |||
func RegisterIPPSServer(s *grpc.Server, srv IPPSServer) { | |||
s.RegisterService(&_IPPS_serviceDesc, srv) | |||
} | |||
func _IPPS_Login_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { | |||
in := new(LoginRequest) | |||
if err := dec(in); err != nil { | |||
return nil, err | |||
} | |||
if interceptor == nil { | |||
return srv.(IPPSServer).Login(ctx, in) | |||
} | |||
info := &grpc.UnaryServerInfo{ | |||
Server: srv, | |||
FullMethod: "/grpc.IPPS/Login", | |||
} | |||
handler := func(ctx context.Context, req interface{}) (interface{}, error) { | |||
return srv.(IPPSServer).Login(ctx, req.(*LoginRequest)) | |||
} | |||
return interceptor(ctx, in, info, handler) | |||
} | |||
func _IPPS_GetPublicKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { | |||
in := new(empty.Empty) | |||
if err := dec(in); err != nil { | |||
return nil, err | |||
} | |||
if interceptor == nil { | |||
return srv.(IPPSServer).GetPublicKey(ctx, in) | |||
} | |||
info := &grpc.UnaryServerInfo{ | |||
Server: srv, | |||
FullMethod: "/grpc.IPPS/GetPublicKey", | |||
} | |||
handler := func(ctx context.Context, req interface{}) (interface{}, error) { | |||
return srv.(IPPSServer).GetPublicKey(ctx, req.(*empty.Empty)) | |||
} | |||
return interceptor(ctx, in, info, handler) | |||
} | |||
func _IPPS_AddAddress_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { | |||
in := new(Address) | |||
if err := dec(in); err != nil { | |||
return nil, err | |||
} | |||
if interceptor == nil { | |||
return srv.(IPPSServer).AddAddress(ctx, in) | |||
} | |||
info := &grpc.UnaryServerInfo{ | |||
Server: srv, | |||
FullMethod: "/grpc.IPPS/AddAddress", | |||
} | |||
handler := func(ctx context.Context, req interface{}) (interface{}, error) { | |||
return srv.(IPPSServer).AddAddress(ctx, req.(*Address)) | |||
} | |||
return interceptor(ctx, in, info, handler) | |||
} | |||
func _IPPS_GetAddresses_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { | |||
in := new(empty.Empty) | |||
if err := dec(in); err != nil { | |||
return nil, err | |||
} | |||
if interceptor == nil { | |||
return srv.(IPPSServer).GetAddresses(ctx, in) | |||
} | |||
info := &grpc.UnaryServerInfo{ | |||
Server: srv, | |||
FullMethod: "/grpc.IPPS/GetAddresses", | |||
} | |||
handler := func(ctx context.Context, req interface{}) (interface{}, error) { | |||
return srv.(IPPSServer).GetAddresses(ctx, req.(*empty.Empty)) | |||
} | |||
return interceptor(ctx, in, info, handler) | |||
} | |||
func _IPPS_AddCreditCard_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { | |||
in := new(CreditCard) | |||
if err := dec(in); err != nil { | |||
return nil, err | |||
} | |||
if interceptor == nil { | |||
return srv.(IPPSServer).AddCreditCard(ctx, in) | |||
} | |||
info := &grpc.UnaryServerInfo{ | |||
Server: srv, | |||
FullMethod: "/grpc.IPPS/AddCreditCard", | |||
} | |||
handler := func(ctx context.Context, req interface{}) (interface{}, error) { | |||
return srv.(IPPSServer).AddCreditCard(ctx, req.(*CreditCard)) | |||
} | |||
return interceptor(ctx, in, info, handler) | |||
} | |||
func _IPPS_GetCreditCards_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { | |||
in := new(empty.Empty) | |||
if err := dec(in); err != nil { | |||
return nil, err | |||
} | |||
if interceptor == nil { | |||
return srv.(IPPSServer).GetCreditCards(ctx, in) | |||
} | |||
info := &grpc.UnaryServerInfo{ | |||
Server: srv, | |||
FullMethod: "/grpc.IPPS/GetCreditCards", | |||
} | |||
handler := func(ctx context.Context, req interface{}) (interface{}, error) { | |||
return srv.(IPPSServer).GetCreditCards(ctx, req.(*empty.Empty)) | |||
} | |||
return interceptor(ctx, in, info, handler) | |||
} | |||
var _IPPS_serviceDesc = grpc.ServiceDesc{ | |||
ServiceName: "grpc.IPPS", | |||
HandlerType: (*IPPSServer)(nil), | |||
Methods: []grpc.MethodDesc{ | |||
{ | |||
MethodName: "Login", | |||
Handler: _IPPS_Login_Handler, | |||
}, | |||
{ | |||
MethodName: "GetPublicKey", | |||
Handler: _IPPS_GetPublicKey_Handler, | |||
}, | |||
{ | |||
MethodName: "AddAddress", | |||
Handler: _IPPS_AddAddress_Handler, | |||
}, | |||
{ | |||
MethodName: "GetAddresses", | |||
Handler: _IPPS_GetAddresses_Handler, | |||
}, | |||
{ | |||
MethodName: "AddCreditCard", | |||
Handler: _IPPS_AddCreditCard_Handler, | |||
}, | |||
{ | |||
MethodName: "GetCreditCards", | |||
Handler: _IPPS_GetCreditCards_Handler, | |||
}, | |||
}, | |||
Streams: []grpc.StreamDesc{}, | |||
Metadata: "ipps.proto", | |||
} |
@@ -0,0 +1,49 @@ | |||
syntax = "proto3"; | |||
import "google/protobuf/empty.proto"; | |||
package grpc; | |||
option go_package = "gitlab.cs.fau.de/faust/faustctf-2020/ipps/internal/grpc"; | |||
service IPPS { | |||
rpc Login(LoginRequest) returns (LoginResponse) {}; | |||
rpc GetPublicKey(google.protobuf.Empty) returns (PublicKey) {}; | |||
rpc AddAddress(Address) returns (google.protobuf.Empty) {}; | |||
rpc GetAddresses(google.protobuf.Empty) returns (Addresses) {}; | |||
rpc AddCreditCard(CreditCard) returns (google.protobuf.Empty) {}; | |||
rpc GetCreditCards(google.protobuf.Empty) returns (CreditCards) {}; | |||
} | |||
message LoginRequest { | |||
string username = 1; | |||
bytes password = 2; | |||
} | |||
message LoginResponse { | |||
string authToken = 1; | |||
} | |||
message PublicKey { | |||
string key = 1; | |||
} | |||
message CreditCard { | |||
string number = 1; | |||
} | |||
message CreditCards { | |||
repeated CreditCard cards = 1; | |||
} | |||
message Address { | |||
string street = 1; | |||
string zip = 2; | |||
string city = 3; | |||
string country = 4; | |||
string planet = 5; | |||
} | |||
message Addresses { | |||
repeated Address addresses = 1; | |||
} |
@@ -0,0 +1,158 @@ | |||
package grpc | |||
import ( | |||
"context" | |||
"errors" | |||
"log" | |||
"strings" | |||
"time" | |||
"github.com/dgrijalva/jwt-go" | |||
"gitlab.cs.fau.de/faust/faustctf-2020/ipps/pkg/user" | |||
"google.golang.org/grpc" | |||
"google.golang.org/grpc/codes" | |||
"google.golang.org/grpc/metadata" | |||
"google.golang.org/grpc/status" | |||
) | |||
var ( | |||
ErrUnsupportedAlgorithm = errors.New("jwt: the signing method is not supported") | |||
ErrNoAuthHeader = status.Error(codes.Unauthenticated, "no authorization header in request") | |||
ErrInvalidAuthHeader = status.Error(codes.Unauthenticated, "authorization header is invalid") | |||
ErrJWTInvalid = status.Error(codes.InvalidArgument, "authorization token is invalid") | |||
) | |||
type Claims struct { | |||
Username string `json:"username"` | |||
jwt.StandardClaims | |||
} | |||
func NewJWT(username string, algorithm string, key []byte) (string, error) { | |||
var err error | |||
var k interface{} | |||
var signingMethod jwt.SigningMethod | |||
switch algorithm { | |||
case "HMAC": | |||
k = key | |||
signingMethod = jwt.SigningMethodHS256 | |||
case "RSA": | |||
k, err = jwt.ParseRSAPrivateKeyFromPEM(key) | |||
if err != nil { | |||
return "", err | |||
} | |||
signingMethod = jwt.SigningMethodRS256 | |||
default: | |||
return "", ErrUnsupportedAlgorithm | |||
} | |||
tok := jwt.NewWithClaims(signingMethod, &Claims{ | |||
Username: username, | |||
StandardClaims: jwt.StandardClaims{ | |||
ExpiresAt: time.Now().Add(24 * time.Hour * 31).Unix(), | |||
Issuer: "ipps", | |||
NotBefore: time.Now().Unix(), | |||
}, | |||
}) | |||
s, err := tok.SignedString(k) | |||
if err != nil { | |||
return "", err | |||
} | |||
return s, nil | |||
} | |||
// JWTCredentials is the type implementing the | |||
// grpc/credentials.PerRPCCredentials interface. | |||
type JWTCredentials struct { | |||
token string | |||
} | |||
func NewJWTCredentials(jwToken string) *JWTCredentials { | |||
return &JWTCredentials{token: jwToken} | |||
} | |||
func (c *JWTCredentials) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) { | |||
return map[string]string{ | |||
"Authorization": c.token, | |||
}, nil | |||
} | |||
func (c *JWTCredentials) RequireTransportSecurity() bool { | |||
return false | |||
} | |||
func (s *Server) authenticate(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) { | |||
if info.FullMethod == "/grpc.IPPS/Login" || | |||
info.FullMethod == "/grpc.IPPS/GetPublicKey" { | |||
return handler(ctx, req) | |||
} | |||
tok, err := extractJWT(ctx) | |||
if err != nil { | |||
log.Printf("grpc: %v\n", err) | |||
return nil, err | |||
} | |||
username, err := authenticateUser(tok, s.publicKey) | |||
if err != nil { | |||
log.Printf("grpc: %v\n", err) | |||
return nil, err | |||
} | |||
u, err := s.userStorage.ByUsername(username) | |||
if err != nil { | |||
log.Printf("ByUsername: %v\n", err) | |||
return nil, status.Error(codes.Internal, err.Error()) | |||
} | |||
ctx = user.NewContext(ctx, u) | |||
return handler(ctx, req) | |||
} | |||
func authenticateUser(jwToken string, key []byte) (string, error) { | |||
tok, err := jwt.ParseWithClaims(jwToken, &Claims{}, | |||
func(token *jwt.Token) (interface{}, error) { | |||
alg := token.Method.Alg() | |||
if len(alg) < 2 { | |||
return nil, status.Error(codes.InvalidArgument, ErrUnsupportedAlgorithm.Error()) | |||
} | |||
alg = strings.ToUpper(alg[:2]) | |||
switch alg { | |||
case "HS": | |||
return key, nil | |||
case "RS": | |||
k, err := jwt.ParseRSAPublicKeyFromPEM(key) | |||
if err != nil { | |||
return nil, status.Error(codes.Internal, err.Error()) | |||
} | |||
return k, nil | |||
default: | |||
return nil, status.Error(codes.InvalidArgument, ErrUnsupportedAlgorithm.Error()) | |||
} | |||
}) | |||
if err != nil { | |||
return "", err | |||
} | |||
if !tok.Valid { | |||
return "", ErrJWTInvalid | |||
} | |||
c := tok.Claims.(*Claims) | |||
return c.Username, nil | |||
} | |||
func extractJWT(ctx context.Context) (string, error) { | |||
md, ok := metadata.FromIncomingContext(ctx) | |||
if !ok { | |||
return "", ErrNoAuthHeader | |||
} | |||
aa, ok := md["authorization"] | |||
if !ok { | |||
return "", ErrNoAuthHeader | |||
} | |||
if len(aa) != 1 { | |||
return "", ErrInvalidAuthHeader | |||
} | |||
return aa[0], nil | |||
} |
@@ -0,0 +1,173 @@ | |||
package grpc | |||
import ( | |||
"context" | |||
"io/ioutil" | |||
"net" | |||
"github.com/golang/protobuf/ptypes/empty" | |||
"gitlab.cs.fau.de/faust/faustctf-2020/ipps/pkg/address" | |||
"gitlab.cs.fau.de/faust/faustctf-2020/ipps/pkg/credit" | |||
"gitlab.cs.fau.de/faust/faustctf-2020/ipps/pkg/user" | |||
"google.golang.org/grpc" | |||
"google.golang.org/grpc/codes" | |||
"google.golang.org/grpc/status" | |||
) | |||
type Config struct { | |||
Address string `toml:"address"` | |||
JWTRSAPrivateKeyFile string `toml:"private_key_file"` | |||
JWTRSAPublicKeyFile string `toml:"public_key_file"` | |||
} | |||
type Server struct { | |||
UnimplementedIPPSServer | |||
config Config | |||
addressStorage address.Storage | |||
creditStorage credit.Storage | |||
userStorage user.Storage | |||
privateKey []byte | |||
publicKey []byte | |||
} | |||
func NewServer(config *Config, as address.Storage, cs credit.Storage, us user.Storage) (*Server, error) { | |||
sk, err := ioutil.ReadFile(config.JWTRSAPrivateKeyFile) | |||
if err != nil { | |||
return nil, err | |||
} | |||
pk, err := ioutil.ReadFile(config.JWTRSAPublicKeyFile) | |||
if err != nil { | |||
return nil, err | |||
} | |||
s := &Server{ | |||
config: *config, | |||
addressStorage: as, | |||
creditStorage: cs, | |||
userStorage: us, | |||
privateKey: sk, | |||
publicKey: pk, | |||
} | |||
return s, nil | |||
} | |||
func (s *Server) ListenAndServe() error { | |||
rpcSrv := grpc.NewServer(grpc.UnaryInterceptor(s.authenticate)) | |||
RegisterIPPSServer(rpcSrv, s) | |||
sock, err := net.Listen("tcp", s.config.Address) | |||
if err != nil { | |||
return err | |||
} | |||
return rpcSrv.Serve(sock) | |||
} | |||
var ErrUserOrPasswordWrong = status.Error(codes.PermissionDenied, | |||
"user does not exist or password is wrong") | |||
// Login is the RPC call that logs a user in, returning a JSON Web Token | |||
// which is used to authenticate users by the GRPC API and other services. | |||
func (s *Server) Login(ctx context.Context, req *LoginRequest) (*LoginResponse, error) { | |||
username := req.GetUsername() | |||
u, err := s.userStorage.ByUsername(username) | |||
if err == user.ErrUserNotExists { | |||
return nil, ErrUserOrPasswordWrong | |||
} else if err != nil { | |||
return nil, err | |||
} | |||
pw := req.GetPassword() | |||
if !u.PasswordEquals(string(pw)) { | |||
return nil, ErrUserOrPasswordWrong | |||
} | |||
tok, err := NewJWT(u.Username, "RSA", []byte(s.privateKey)) | |||
if err != nil { | |||
return nil, status.Error(codes.Internal, err.Error()) | |||
} | |||
return &LoginResponse{AuthToken: tok}, nil | |||
} | |||
// GetPublicKey returns the server's PEM encoded public RSA key which | |||
// is used to validate the signature of JSON Web Tokens handed out by | |||
// the GRPC API. It is mainly intended to be used by other company's | |||
// web services that use JWT Tokens to authenticate our users. | |||
func (s *Server) GetPublicKey(ctx context.Context, req *empty.Empty) (*PublicKey, error) { | |||
return &PublicKey{Key: string(s.publicKey)}, nil | |||
} | |||
// AddAddress adds addr to the user's addresses. | |||
func (s *Server) AddAddress(ctx context.Context, addr *Address) (*empty.Empty, error) { | |||
u := user.MustFromContext(ctx) | |||
a, err := address.NewForUser(u) | |||
if err != nil { | |||
return nil, status.Error(codes.Internal, err.Error()) | |||
} | |||
a.Street = addr.Street | |||
a.Zip = addr.Zip | |||
a.City = addr.City | |||
a.Country = addr.Country | |||
a.Planet = addr.Planet | |||
err = s.addressStorage.Insert(a) | |||
if err != nil { | |||
return nil, status.Error(codes.Internal, err.Error()) | |||
} | |||
return &empty.Empty{}, nil | |||
} | |||
// GetAddress returns the current user's addresses. | |||
func (s *Server) GetAddresses(ctx context.Context, req *empty.Empty) (*Addresses, error) { | |||
u := user.MustFromContext(ctx) | |||
aa, err := s.addressStorage.ByUser(u) | |||
if err != nil { | |||
return nil, status.Error(codes.Internal, err.Error()) | |||
} | |||
addrs := make([]*Address, 0, len(aa)) | |||
for _, a := range aa { | |||
addrs = append(addrs, &Address{ | |||
Street: a.Street, | |||
Zip: a.Zip, | |||
City: a.City, | |||
Country: a.Country, | |||
Planet: a.Planet, | |||
}) | |||
} | |||
return &Addresses{Addresses: addrs}, nil | |||
} | |||
// AddCreditCard adds a credit card to the current user's payment options. | |||
func (s *Server) AddCreditCard(ctx context.Context, card *CreditCard) (*empty.Empty, error) { | |||
u := user.MustFromContext(ctx) | |||
c, err := credit.NewCard(u) | |||
if err != nil { | |||
return nil, status.Error(codes.Internal, err.Error()) | |||
} | |||
c.Number = card.Number | |||
err = s.creditStorage.Insert(c) | |||
if err != nil { | |||
return nil, status.Error(codes.Internal, err.Error()) | |||
} | |||
return &empty.Empty{}, nil | |||
} | |||
// GetCreditCards returns the current user's credit cards. | |||
func (s *Server) GetCreditCards(ctx context.Context, req *empty.Empty) (*CreditCards, error) { | |||
u := user.MustFromContext(ctx) | |||
cc, err := s.creditStorage.ByUser(u) | |||
if err != nil { | |||
return nil, status.Error(codes.Internal, err.Error()) | |||
} | |||
cards := make([]*CreditCard, 0, len(cc)) | |||
for _, c := range cc { | |||
card := &CreditCard{ | |||
Number: c.Number, | |||
} | |||
cards = append(cards, card) | |||
} | |||
return &CreditCards{Cards: cards}, nil | |||
} |
@@ -0,0 +1,542 @@ | |||
package http | |||
import ( | |||
"encoding/gob" | |||
"fmt" | |||
"html/template" | |||
"log" | |||
"net/http" | |||
"net/mail" | |||
"strconv" | |||
"github.com/google/uuid" | |||
"github.com/gorilla/mux" | |||
"github.com/gorilla/sessions" | |||
"gitlab.cs.fau.de/faust/faustctf-2020/ipps/internal/session" | |||
"gitlab.cs.fau.de/faust/faustctf-2020/ipps/pkg/address" | |||
"gitlab.cs.fau.de/faust/faustctf-2020/ipps/pkg/credit" | |||
"gitlab.cs.fau.de/faust/faustctf-2020/ipps/pkg/feedback" | |||
"gitlab.cs.fau.de/faust/faustctf-2020/ipps/pkg/parcel" | |||
"gitlab.cs.fau.de/faust/faustctf-2020/ipps/pkg/user" | |||
) | |||
func init() { | |||
// Gorilla sessions gob encodes all data structures, so | |||
gob.Register(&mail.Address{}) | |||
} | |||
type templateHandler struct { | |||
templates *template.Template | |||
name string | |||
title string | |||
} | |||
type Page struct { | |||
Title string | |||
Success string | |||
Errors []string | |||
User *user.User | |||
} | |||
func NewPage(title string, r *http.Request) *Page { | |||
s := session.MustFromContext(r.Context()) | |||
u, ok := user.FromContext(r.Context()) | |||
if !ok { | |||
u = nil | |||
} | |||
return &Page{ | |||
Title: title, | |||
Success: sessionMessage(s, "success"), | |||
Errors: sessionMessages(s, "errors"), | |||
User: u, | |||
} | |||
} | |||
func newTemplateHandler(templates *template.Template, name, title string) *templateHandler { | |||
return &templateHandler{ | |||
templates: templates, | |||
name: name, | |||
title: title, | |||
} | |||
} | |||
func (th *templateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |||
err := th.templates.ExecuteTemplate(w, th.name, NewPage(th.title, r)) | |||
if err != nil { | |||
log.Print(err) | |||
return | |||
} | |||
} | |||
func loginFormHandler(t *template.Template) http.HandlerFunc { | |||
return func(w http.ResponseWriter, r *http.Request) { | |||
_, isloggedIn := user.FromContext(r.Context()) | |||
if isloggedIn { | |||
http.Redirect(w, r, "/", http.StatusFound) | |||
return | |||
} | |||
s := session.MustFromContext(r.Context()) | |||
p := &Page{ | |||
Success: sessionMessage(s, "success"), | |||
Errors: sessionMessages(s, "errors"), | |||
} | |||
err := t.ExecuteTemplate(w, "login.html", p) | |||
if err != nil { | |||
log.Print(err) | |||
return | |||
} | |||
} | |||
} | |||
func registerFormHandler(t *template.Template) http.HandlerFunc { | |||
return func(w http.ResponseWriter, r *http.Request) { | |||
_, isloggedIn := user.FromContext(r.Context()) | |||
if isloggedIn { | |||
http.Redirect(w, r, "/", http.StatusFound) | |||
return | |||
} | |||
s := session.MustFromContext(r.Context()) | |||
p := &Page{ | |||
Success: sessionMessage(s, "success"), | |||
Errors: sessionMessages(s, "errors"), | |||
} | |||
err := t.ExecuteTemplate(w, "register.html", p) | |||
if err != nil { | |||
log.Print(err) | |||
return | |||
} | |||
} | |||
} | |||
func sessionMessages(s *sessions.Session, key string) []string { | |||
ff := s.Flashes(key) | |||
mm := make([]string, 0, len(ff)) | |||
for _, f := range ff { | |||
str, ok := f.(string) | |||
if !ok { | |||
continue | |||
} | |||
mm = append(mm, str) | |||
} | |||
return mm | |||
} | |||
func sessionMessage(s *sessions.Session, key string) string { | |||
ff := s.Flashes(key) | |||
if len(ff) == 0 { | |||
return "" | |||
} | |||
f, ok := ff[0].(string) | |||
if !ok { | |||
return "" | |||
} | |||
return f | |||
} | |||
func newFileServer(root, prefixToStrip string) http.Handler { | |||
fs := http.FileServer(http.Dir(root)) | |||
if prefixToStrip != "" { | |||
fs = http.StripPrefix(prefixToStrip, fs) | |||
} | |||
return fs | |||
} | |||
type loginHandler struct { | |||
UserStorage user.Storage | |||
} | |||
func (h *loginHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |||
sess := session.MustFromContext(r.Context()) | |||
f, err := user.ParseLoginForm(r) | |||
if err != nil { | |||
log.Print(err) | |||
sess.AddFlash(err.Error(), "errors") | |||
http.Redirect(w, r, "/login", http.StatusFound) | |||
return | |||
} | |||
u, err := h.UserStorage.ByUsername(f.Username) | |||
if err == user.ErrUserNotExists { | |||
sess.AddFlash("User does not exist or password is wrong!", "errors") | |||
http.Redirect(w, r, "/login", http.StatusFound) | |||
return | |||
} else if err != nil { | |||
log.Print(err) | |||
sess.AddFlash(err.Error(), "errors") | |||
http.Redirect(w, r, "/login", http.StatusFound) | |||
} | |||
if !u.PasswordEquals(f.Password) { | |||
sess.AddFlash("User does not exist or password is wrong!", "errors") | |||
http.Redirect(w, r, "/login", http.StatusFound) | |||
return | |||
} | |||
sess.Values["user"] = u.Username | |||
http.Redirect(w, r, "/", http.StatusFound) | |||
} | |||
func handleLogout(w http.ResponseWriter, r *http.Request) { | |||
sess := session.MustFromContext(r.Context()) | |||
if _, ok := sess.Values["user"]; !ok { | |||
http.Redirect(w, r, "/", http.StatusFound) | |||
return | |||
} | |||
delete(sess.Values, "user") | |||
http.Redirect(w, r, "/", http.StatusFound) | |||
} | |||
type registerHandler struct { | |||
UserStorage user.Storage | |||
} | |||
func (h *registerHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |||
sess := session.MustFromContext(r.Context()) | |||
u, err := user.ParseRegistrationForm(r) | |||
if err != nil { | |||
sess.AddFlash(err.Error(), "errors") | |||
http.Redirect(w, r, "/signup", http.StatusFound) | |||
return | |||
} | |||
err = h.UserStorage.Insert(u) | |||
if err != nil { | |||
sess.AddFlash(err.Error(), "errors") | |||
http.Redirect(w, r, "/signup", http.StatusFound) | |||
return | |||
} | |||
sess.AddFlash("Your account has been created successfully!", "success") | |||
sess.Values["user"] = u.Username | |||
http.Redirect(w, r, "/", http.StatusFound) | |||
} | |||
type updateProfileHandler struct { | |||
UserStorage user.Storage | |||
} | |||
func (uph *updateProfileHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |||
sess := session.MustFromContext(r.Context()) | |||
u := user.MustFromContext(r.Context()) | |||
u, err := uph.UserStorage.ByID(u.ID) | |||
if err != nil { | |||
log.Print(err) | |||
sess.AddFlash(http.StatusText(http.StatusInternalServerError), "errors") | |||
http.Error(w, err.Error(), http.StatusInternalServerError) | |||
http.Redirect(w, r, "/profile", http.StatusFound) | |||
return | |||
} | |||
err = user.UpdateFromEditForm(u, r) | |||
if err != nil { | |||
sess.AddFlash(err.Error(), "errors") | |||
http.Redirect(w, r, "/profile", http.StatusFound) | |||
return | |||
} | |||
err = uph.UserStorage.Update(u) | |||
if err != nil { | |||
log.Print(err) | |||
sess.AddFlash(http.StatusText(http.StatusInternalServerError), "errors") | |||
http.Redirect(w, r, "/profile", http.StatusFound) | |||
return | |||
} | |||
sess.AddFlash("Your profile has been updated successfully!", "success") | |||
sess.Values["user"] = u.Email | |||
http.Redirect(w, r, "/profile", http.StatusFound) | |||
} | |||
type paymentOptionsHandler struct { | |||
Templates *template.Template | |||
CardStorage credit.Storage | |||
} | |||
type paymentPage struct { | |||
*Page | |||
Cards []*credit.Card | |||
} | |||
func (ph *paymentOptionsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |||
u := user.MustFromContext(r.Context()) | |||
cc, err := ph.CardStorage.ByUser(u) | |||
if err != nil { | |||
log.Print(err) | |||
http.Error(w, err.Error(), http.StatusInternalServerError) | |||
return | |||
} | |||
p := &paymentPage{ | |||
Page: NewPage("Payment Options", r), | |||
Cards: cc, | |||
} | |||
err = ph.Templates.ExecuteTemplate(w, "payment_options.html", p) | |||
if err != nil { | |||
log.Print(err) | |||
return | |||
} | |||
} | |||
type addPaymantOptionHandler struct { | |||
CardStorage credit.Storage | |||
} | |||
func (ph *addPaymantOptionHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |||
sess := session.MustFromContext(r.Context()) | |||
u := user.MustFromContext(r.Context()) | |||
c, err := credit.NewCardFromForm(u, r) | |||
if err != nil { | |||
log.Print(err) | |||
sess.AddFlash(err.Error(), "errors") | |||
http.Redirect(w, r, "/profile/payment-options", http.StatusFound) | |||
return | |||
} | |||
err = ph.CardStorage.Insert(c) | |||
if err != nil { | |||
log.Print(err) | |||
sess.AddFlash(http.StatusText(http.StatusInternalServerError), "errors") | |||
http.Redirect(w, r, "/profile/payment-options", http.StatusFound) | |||
return | |||
} | |||
sess.AddFlash("Your credit card has been added successfully!", "success") | |||
http.Redirect(w, r, "/profile/payment-options", http.StatusFound) | |||
} | |||
type feedbackPage struct { | |||
*Page | |||
Feedbacks []feedback.Feedback | |||
} | |||
type feedbackHandler struct { | |||
Templates *template.Template | |||
Storage feedback.Storage | |||
} | |||
func (fh *feedbackHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |||
err := r.ParseForm() | |||
if err != nil { | |||
http.Error(w, "Internal Server Error", http.StatusInternalServerError) | |||
return | |||
} | |||
ostr := r.PostForm.Get("offset") | |||
offset, err := strconv.ParseUint(ostr, 10, 64) | |||
if err != nil { | |||
offset = 0 | |||
} | |||
ff, err := fh.Storage.Multiple(20, uint(offset)) | |||
if err != nil { | |||
http.Error(w, "Internal Server Error", http.StatusInternalServerError) | |||
return | |||
} | |||
p := &feedbackPage{ | |||
Page: NewPage("Feedback", r), | |||
Feedbacks: ff, | |||
} | |||
err = fh.Templates.ExecuteTemplate(w, "feedback.html", p) | |||
if err != nil { | |||
log.Print(err) | |||
return | |||
} | |||
} | |||
type addFeedbackHandler struct { | |||
Storage feedback.Storage | |||
} | |||
func (fh *addFeedbackHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |||
sess := session.MustFromContext(r.Context()) | |||
u := user.MustFromContext(r.Context()) | |||
err := r.ParseForm() | |||
if err != nil { | |||
sess.AddFlash(http.StatusText(http.StatusInternalServerError), "errors") | |||
http.Redirect(w, r, "/feedback", http.StatusFound) | |||
return | |||
} | |||
rating, err := strconv.ParseUint(r.PostForm.Get("rating"), 10, 8) | |||
if err != nil { | |||
sess.AddFlash("Rating must be a number between 1 and 5", "errors") | |||
http.Redirect(w, r, "/feedback", http.StatusFound) | |||
return | |||
} | |||
f, err := feedback.New(u.Username, uint8(rating), r.PostForm.Get("text")) | |||
if err != nil { | |||
sess.AddFlash(err.Error(), "errors") | |||
http.Redirect(w, r, "/feedback", http.StatusFound) | |||
return | |||
} | |||
err = fh.Storage.Insert(f) | |||
if err != nil { | |||
sess.AddFlash(err.Error(), "errors") | |||
http.Redirect(w, r, "/feedback", http.StatusFound) | |||
return | |||
} | |||
sess.AddFlash("Thank you, for your feedback!", "success") | |||
http.Redirect(w, r, "/feedback", http.StatusFound) | |||
} | |||
type addressPage struct { | |||
*Page | |||
Addresses []*address.Address | |||
} | |||
type addressHandler struct { | |||
Templates *template.Template | |||
Storage address.Storage | |||
} | |||
func (h *addressHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |||
u := user.MustFromContext(r.Context()) | |||
aa, err := h.Storage.ByUser(u) | |||
if err != nil { | |||
log.Println(err) | |||
http.Error(w, "Internal Server Error", http.StatusInternalServerError) | |||
return | |||
} | |||
p := &addressPage{ | |||
Page: NewPage("Addresses", r), | |||
Addresses: aa, | |||
} | |||
err = h.Templates.ExecuteTemplate(w, "addresses.html", p) | |||
if err != nil { | |||
log.Println(err) | |||
} | |||
} | |||
type addAddressHandler struct { | |||
Storage address.Storage | |||
} | |||
func (h *addAddressHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |||
u := user.MustFromContext(r.Context()) | |||
sess := session.MustFromContext(r.Context()) | |||
a, err := address.NewFromFormForUser(r, u) | |||
if err != nil { | |||
sess.AddFlash(err.Error(), "errors") | |||
http.Redirect(w, r, "/profile/addresses", http.StatusFound) | |||
return | |||
} | |||
err = h.Storage.Insert(a) | |||
if err != nil { | |||
if err == address.ErrAddressAlreadyAdded { | |||
sess.AddFlash("You have already added this address.", "errors") | |||
} else { | |||
sess.AddFlash(err.Error(), "errors") | |||
} | |||
http.Redirect(w, r, "/profile/addresses", http.StatusFound) | |||
return | |||
} | |||
sess.AddFlash("Your address has been added successfully!", "success") | |||
http.Redirect(w, r, "/profile/addresses", http.StatusFound) | |||
} | |||
type findParcelHandler struct { | |||
Storage parcel.Storage | |||
} | |||
func (h *findParcelHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |||
sess := session.MustFromContext(r.Context()) | |||
err := r.ParseForm() | |||
if err != nil { | |||
sess.AddFlash(err.Error()) | |||
http.Redirect(w, r, "/tracking", http.StatusFound) | |||
} | |||
ids, ok := r.PostForm["tracking-id"] | |||
if !ok || len(ids) < 1 { | |||
sess.AddFlash("The tracking number is missing", "errors") | |||
http.Redirect(w, r, "/tracking", http.StatusFound) | |||
return | |||
} | |||
id, err := uuid.Parse(ids[0]) | |||
if err != nil { | |||
sess.AddFlash("The tracking number you provided is invalid", "errors") | |||
http.Redirect(w, r, "/tracking", http.StatusFound) | |||
return | |||
} | |||
p, err := h.Storage.ByID(id) | |||
if err != nil { | |||
sess.AddFlash("An internal server error occured, please try again later", "errors") | |||
http.Redirect(w, r, "/tracking", http.StatusFound) | |||
return | |||
} else if p == nil { | |||
sess.AddFlash("A parcel with that tracking number does not exist in our database."+ | |||
" Please make sure that the tracking number you entered is correct or"+ | |||
" contact the sender.", "warnings") | |||
http.Redirect(w, r, "/tracking", http.StatusFound) | |||
return | |||
} | |||
http.Redirect(w, r, fmt.Sprintf("/tracking/%s", id.String()), http.StatusFound) | |||
} | |||
type trackingHandler struct { | |||
templates *template.Template | |||
eventStorage parcel.EventAccesser | |||
parcelStorage parcel.Accesser | |||
} | |||
type trackingPage struct { | |||
*Page | |||
Parcel *parcel.Parcel | |||
Events []*parcel.Event | |||
} | |||
func (h *trackingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |||
sess := session.MustFromContext(r.Context()) | |||
v := mux.Vars(r) | |||
idStr, ok := v["id"] | |||
if !ok { | |||
sess.AddFlash("The tracking number is missing", "errors") | |||
http.Redirect(w, r, "/tracking", http.StatusFound) | |||
return | |||
} | |||
id, err := uuid.Parse(idStr) | |||
if err != nil { | |||
sess.AddFlash("The tracking number you supplied is invalid", "errors") | |||
http.Redirect(w, r, "/tracking", http.StatusFound) | |||
return | |||
} | |||
p, err := h.parcelStorage.ByID(id) | |||
if err != nil { | |||
log.Println(err) | |||
sess.AddFlash("An internal server error occured, please try again later", "errors") | |||
http.Redirect(w, r, "/tracking", http.StatusFound) | |||
return | |||
} else if p == nil { | |||
sess.AddFlash("A parcel with that tracking number does not exist in our database."+ | |||
" Please make sure that the tracking number you entered is correct or"+ | |||
" contact the sender.", "warnings") | |||
http.Redirect(w, r, "/tracking", http.StatusFound) | |||
return | |||
} | |||
ee, err := h.eventStorage.ByParcel(p) | |||
if err != nil { | |||
log.Println(err) | |||
sess.AddFlash("An internal server error occurred, please try again later", | |||
"errors") | |||
http.Redirect(w, r, "/tracking", http.StatusFound) | |||
return | |||
} | |||
err = h.templates.ExecuteTemplate(w, "parcel_events.html", &trackingPage{ | |||
Page: NewPage("Tracking Information", r), | |||
Parcel: p, | |||
Events: ee, | |||
}) | |||
if err != nil { | |||
log.Println(err) | |||
} | |||
} |
@@ -0,0 +1,55 @@ | |||
package http | |||
import ( | |||
"log" | |||
"net/http" | |||
"github.com/gorilla/mux" | |||
"gitlab.cs.fau.de/faust/faustctf-2020/ipps/internal/session" | |||
"gitlab.cs.fau.de/faust/faustctf-2020/ipps/pkg/user" | |||
) | |||
// authMiddleware returns a middleware that stores the request session's | |||
// user as a User struct in the HTTP request's context | |||
func authMiddleware(us user.Storage) mux.MiddlewareFunc { | |||
return func(next http.Handler) http.Handler { | |||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |||
s := session.MustFromContext(r.Context()) | |||
v, ok := s.Values["user"] | |||
if !ok { | |||
next.ServeHTTP(w, r) | |||
return | |||
} | |||
username, ok := v.(string) | |||
if !ok { | |||
http.Error(w, "Session cookie is corrupt", http.StatusBadRequest) | |||
return | |||
} | |||
u, err := us.ByUsername(username) | |||
if err != nil { | |||
log.Print(err) | |||
http.Error(w, "Internal Server Error", http.StatusInternalServerError) | |||
return | |||
} | |||
r = r.WithContext(user.NewContext(r.Context(), u)) | |||
next.ServeHTTP(w, r) | |||
}) | |||
} | |||
} | |||
// loginChecker is the middleware that checks, whether the current | |||
// request is from an authorized user, sending a redirect to the login | |||
// page, if that is not the case | |||
func loginChecker(next http.Handler) http.Handler { | |||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |||
_, ok := user.FromContext(r.Context()) | |||
if !ok { | |||
sess := session.MustFromContext(r.Context()) | |||
sess.AddFlash("You must be logged in in order to view this page", "errors") | |||
http.Redirect(w, r, "/login", http.StatusFound) | |||
return | |||
} | |||
next.ServeHTTP(w, r) | |||
}) | |||
} |
@@ -0,0 +1,111 @@ | |||
// Package server contains Webfoo's web server implementation | |||
package http | |||
import ( | |||
"html/template" | |||
"net/http" | |||
"time" | |||
"github.com/gorilla/mux" | |||
"gitlab.cs.fau.de/faust/faustctf-2020/ipps/internal/json" | |||
"gitlab.cs.fau.de/faust/faustctf-2020/ipps/internal/session" | |||
"gitlab.cs.fau.de/faust/faustctf-2020/ipps/pkg/address" | |||
"gitlab.cs.fau.de/faust/faustctf-2020/ipps/pkg/credit" | |||
"gitlab.cs.fau.de/faust/faustctf-2020/ipps/pkg/feedback" | |||
"gitlab.cs.fau.de/faust/faustctf-2020/ipps/pkg/parcel" | |||
"gitlab.cs.fau.de/faust/faustctf-2020/ipps/pkg/user" | |||
) | |||
type Config struct { | |||
Address string | |||
ReadTimeout Duration | |||
WriteTimeout Duration | |||
} | |||
// Duration is an implementation of the TextMarshaler and TextUnmarshaler | |||
// interfaces. It wraps the standard library's duration type, so durations may | |||
// be parsed by the TOML parser. | |||
type Duration struct { | |||
duration time.Duration | |||
} | |||
func (d *Duration) MarshalText() ([]byte, error) { | |||
return []byte(d.duration.String()), nil | |||
} | |||
func (d *Duration) UnmarshalText(text []byte) error { | |||
var err error | |||
d.duration, err = time.ParseDuration(string(text)) | |||
if err != nil { | |||
return err | |||
} | |||
return nil | |||
} | |||
type Server struct { | |||
AddressStorage address.Storage | |||
CreditStorage credit.Storage | |||
EventStorage parcel.EventStorage | |||
FeedbackStorage feedback.Storage | |||
ParcelStorage parcel.Storage | |||
UserStorage user.Storage | |||
} | |||
func (s *Server) ListenAndServe(config *Config, sessionConfig *session.Config) error { | |||
r, err := s.newRouter(sessionConfig) | |||
if err != nil { | |||
return err | |||
} | |||
srv := &http.Server{ | |||
Addr: config.Address, | |||
Handler: r, | |||
ReadTimeout: config.ReadTimeout.duration, | |||
WriteTimeout: config.WriteTimeout.duration, | |||
} | |||
return srv.ListenAndServe() | |||
} | |||
func (s *Server) newRouter(sessionConfig *session.Config) (*mux.Router, error) { | |||
t, err := template.ParseGlob("web/template/*.html") | |||
if err != nil { | |||
return nil, err | |||
} | |||
r := mux.NewRouter() | |||
r.Use(session.NewMiddleware(sessionConfig), authMiddleware(s.UserStorage)) | |||
r.Handle("/", newTemplateHandler(t, "home.html", "Home")) | |||
r.PathPrefix("/static/").Handler(newFileServer("web/static/", "/static/")) | |||
r.Handle("/login", loginFormHandler(t)).Methods("GET") | |||
r.Handle("/login", &loginHandler{UserStorage: s.UserStorage}).Methods("POST") | |||
r.HandleFunc("/logout", handleLogout) | |||
r.Handle("/signup", registerFormHandler(t)).Methods("GET") | |||
r.Handle("/signup", ®isterHandler{UserStorage: s.UserStorage}).Methods("POST") | |||
r.Handle("/feedback", &feedbackHandler{ | |||
Templates: t, | |||
Storage: s.FeedbackStorage, | |||
}).Methods("GET") | |||
r.Handle("/tracking", | |||
newTemplateHandler(t, "tracking.html", "Tracking")).Methods("GET") | |||
r.Handle("/tracking", &findParcelHandler{Storage: s.ParcelStorage}).Methods("POST") | |||
r.Handle("/tracking/{id}", &trackingHandler{parcelStorage: nil}) | |||
r.Handle("/feedback", &addFeedbackHandler{ | |||
Storage: s.FeedbackStorage, | |||
}).Methods("POST") | |||
pr := r.PathPrefix("/profile").Subrouter() | |||
pr.Use(loginChecker) | |||
pr.Handle("", newTemplateHandler(t, "profile.html", "Profile")) | |||
pr.Handle("/update", &updateProfileHandler{UserStorage: s.UserStorage}) | |||
pr.Handle("/addresses", &addressHandler{Templates: t, Storage: s.AddressStorage}) | |||
pr.Handle("/addresses/add", &addAddressHandler{Storage: s.AddressStorage}) | |||
pr.Handle("/payment-options", &paymentOptionsHandler{CardStorage: s.CreditStorage, Templates: t}) | |||
pr.Handle("/add-payment-option", &addPaymantOptionHandler{CardStorage: s.CreditStorage}). | |||
Methods("POST") | |||
ar := r.PathPrefix("/api").Subrouter() | |||
json.AddAPIRoutes(ar, s.AddressStorage, s.CreditStorage, s.FeedbackStorage, s.UserStorage) | |||
return r, nil | |||
} |
@@ -0,0 +1,219 @@ | |||
package json | |||
import ( | |||
"encoding/json" | |||
"log" | |||
"net/http" | |||
"strconv" | |||
"github.com/gorilla/mux" | |||
"gitlab.cs.fau.de/faust/faustctf-2020/ipps/internal/session" | |||
"gitlab.cs.fau.de/faust/faustctf-2020/ipps/pkg/address" | |||
"gitlab.cs.fau.de/faust/faustctf-2020/ipps/pkg/credit" | |||
"gitlab.cs.fau.de/faust/faustctf-2020/ipps/pkg/feedback" | |||
"gitlab.cs.fau.de/faust/faustctf-2020/ipps/pkg/user" | |||
) | |||
type APIHandler struct { | |||
as address.Storage | |||
cs credit.Storage | |||
fs feedback.Storage | |||
us user.Storage | |||
} | |||
func NewAPIHandler(as address.Storage, cs credit.Storage, fs feedback.Storage, us user.Storage) *APIHandler { | |||
return &APIHandler{ | |||
as: as, | |||
cs: cs, | |||
fs: fs, | |||
us: us, | |||
} | |||
} | |||
func (h *APIHandler) login(w http.ResponseWriter, r *http.Request) { | |||
u, ok := user.FromContext(r.Context()) | |||
if ok { | |||
sendResult(w, u.Username) | |||
return | |||
} | |||
err := r.ParseMultipartForm(0) | |||
if err != nil { | |||
sendError(w, http.StatusInternalServerError, err) | |||
return | |||
} | |||
lf, err := user.ParseLoginForm(r) | |||
if err != nil { | |||
sendError(w, http.StatusBadRequest, err) | |||
return | |||
} | |||
u, err = h.us.ByUsername(lf.Username) | |||
if err == user.ErrUserNotExists { | |||
sendError(w, http.StatusBadRequest, err) | |||
return | |||
} else if err != nil { | |||
sendError(w, http.StatusInternalServerError, err) | |||
return | |||
} | |||
if !u.PasswordEquals(lf.Password) { | |||
sendError(w, http.StatusBadRequest, err) | |||
return | |||
} | |||
sess := session.MustFromContext(r.Context()) | |||
sess.Values["user"] = u.Username | |||
sendResult(w, u.Username) | |||
} | |||
func (h *APIHandler) serveRecentFeedback(w http.ResponseWriter, r *http.Request) { | |||
err := r.ParseForm() | |||
if err != nil { | |||
http.Error(w, err.Error(), http.StatusInternalServerError) | |||
return | |||
} | |||
os := r.Form.Get("offset") | |||
offset := uint(0) | |||
if os != "" { | |||
pos, err := strconv.ParseUint(os, 10, 32) | |||
if err != nil { | |||
http.Error(w, err.Error(), http.StatusBadRequest) | |||
return | |||
} | |||
offset = uint(pos) | |||
} | |||
ff, err := h.fs.Multiple(20, offset) | |||
if err != nil { | |||
http.Error(w, err.Error(), http.StatusInternalServerError) | |||
return | |||
} | |||
sendResult(w, ff) | |||
} | |||
func (h *APIHandler) addCreditCard(w http.ResponseWriter, r *http.Request) { | |||
v := mux.Vars(r) | |||
u, err := h.us.ByUsername(v["user"]) | |||
if err != nil { | |||
sendError(w, http.StatusInternalServerError, err) | |||
return | |||
} | |||
err = r.ParseMultipartForm(0) | |||
if err != nil { | |||
sendError(w, http.StatusBadRequest, err) | |||
return | |||
} | |||
c, err := credit.NewCardFromForm(u, r) | |||
if err != nil { | |||
sendError(w, http.StatusBadRequest, err) | |||
return | |||
} | |||
err = h.cs.Insert(c) | |||
if err != nil { | |||
sendError(w, http.StatusInternalServerError, err) | |||
return | |||
} | |||
sendResult(w, c) | |||
} | |||
func (h *APIHandler) serveCreditCards(w http.ResponseWriter, r *http.Request) { | |||
v := mux.Vars(r) | |||
u, err := h.us.ByUsername(v["user"]) | |||
if err == user.ErrUserNotExists { | |||
sendError(w, http.StatusNotFound, err) | |||
return | |||
} else if err != nil { | |||
sendError(w, http.StatusInternalServerError, err) | |||
return | |||
} | |||
cc, err := h.cs.ByUser(u) | |||
if err != nil { | |||
sendError(w, http.StatusInternalServerError, err) | |||
return | |||
} | |||
sendResult(w, cc) | |||
} | |||
func (h *APIHandler) addAddress(w http.ResponseWriter, r *http.Request) { | |||
v := mux.Vars(r) | |||
u, err := h.us.ByUsername(v["user"]) | |||
if err == user.ErrUserNotExists { | |||
sendError(w, http.StatusNotFound, err) | |||
return | |||
} else if err != nil { | |||
sendError(w, http.StatusInternalServerError, err) | |||
return | |||
} | |||
err = r.ParseMultipartForm(0) | |||
if err != nil { | |||
sendError(w, http.StatusInternalServerError, err) | |||
return | |||
} | |||
a, err := address.NewFromFormForUser(r, u) | |||
if err != nil { | |||
sendError(w, http.StatusBadRequest, err) | |||
return | |||
} | |||
err = h.as.Insert(a) | |||
if err == address.ErrAddressAlreadyAdded { | |||
sendError(w, http.StatusBadRequest, err) | |||
return | |||
} else if err != nil { | |||
sendError(w, http.StatusInternalServerError, err) | |||
return | |||
} | |||
a, err = h.as.ByID(a.ID) | |||
if err != nil { | |||
sendError(w, http.StatusInternalServerError, err) | |||
return | |||
} | |||
sendResult(w, a) | |||
} | |||
func (h *APIHandler) serveAddresses(w http.ResponseWriter, r *http.Request) { | |||
v := mux.Vars(r) | |||
u, err := h.us.ByUsername(v["user"]) | |||
if err == user.ErrUserNotExists { | |||
sendError(w, http.StatusNotFound, err) | |||
return | |||
} else if err != nil { | |||
sendError(w, http.StatusInternalServerError, err) | |||
return | |||
} | |||
aa, err := h.as.ByUser(u) | |||
if err != nil { | |||
sendError(w, http.StatusInternalServerError, err) | |||
return | |||
} | |||
sendResult(w, aa) | |||
} | |||
func sendResult(w http.ResponseWriter, result interface{}) { | |||
jw := json.NewEncoder(w) | |||
w.Header().Set("Content-Type", "application/json") | |||
err := jw.Encode(&Response{Result: result}) | |||
if err != nil { | |||
log.Println(err) | |||
} | |||
} | |||
func sendError(w http.ResponseWriter, status int, err error) { | |||
if status == http.StatusInternalServerError { | |||
log.Println(err) | |||
} | |||
jw := json.NewEncoder(w) | |||
w.Header().Set("Content-Type", "application/json") | |||
w.WriteHeader(status) | |||
err = jw.Encode(&Response{Error: err.Error()}) | |||
if err != nil { | |||
log.Println(err) | |||
} | |||
} |
@@ -0,0 +1,28 @@ | |||
// Package json implements the web service's JSON API. | |||
package json | |||
import ( | |||
"github.com/gorilla/mux" | |||
"gitlab.cs.fau.de/faust/faustctf-2020/ipps/pkg/address" | |||
"gitlab.cs.fau.de/faust/faustctf-2020/ipps/pkg/credit" | |||
"gitlab.cs.fau.de/faust/faustctf-2020/ipps/pkg/feedback" | |||
"gitlab.cs.fau.de/faust/faustctf-2020/ipps/pkg/user" | |||
) | |||
type Response struct { | |||
Error string `json:"error,omitempty"` | |||
Result interface{} `json:"result,omitempty"` | |||
} | |||
func AddAPIRoutes(r *mux.Router, as address.Storage, cs credit.Storage, fs feedback.Storage, us user.Storage) { | |||
h := NewAPIHandler(as, cs, fs, us) | |||
r.HandleFunc("/login", h.login).Methods("POST") | |||
r.HandleFunc("/recent-feedback", h.serveRecentFeedback).Methods("GET") | |||
ur := r.PathPrefix("/user/{user}").Subrouter() | |||
ur.HandleFunc("/add-address", h.addAddress).Methods("POST") | |||
ur.HandleFunc("/get-addresses", h.serveAddresses).Methods("GET") | |||
ur.HandleFunc("/add-credit-card", h.addCreditCard).Methods("POST") | |||
ur.HandleFunc("/get-credit-cards", h.serveCreditCards).Methods("GET") | |||
} |
@@ -0,0 +1,30 @@ | |||
package json | |||
import ( | |||
"net/http" | |||
"github.com/gorilla/mux" | |||
"gitlab.cs.fau.de/faust/faustctf-2020/ipps/pkg/user" | |||
) | |||
// loginChecker is the middleware that checks, whether the current | |||
// request is from an authorized user, denying access if that is | |||
// not the case or if the user tries to access data of other users. | |||
func loginChecker(next http.Handler) http.Handler { | |||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |||
u, ok := user.FromContext(r.Context()) | |||
if !ok { | |||
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) | |||
return | |||
} | |||
v := mux.Vars(r) | |||
vu := v["user"] | |||
if vu != u.Username { | |||
http.Error(w, "You are note allowed to access other users' data", | |||
http.StatusForbidden) | |||
return | |||
} | |||
next.ServeHTTP(w, r) | |||
}) | |||
} |
@@ -0,0 +1,113 @@ | |||
// Package session implements the web application's session management. | |||
package session | |||
import ( | |||
"context" | |||
"log" | |||
"net/http" | |||
"github.com/gorilla/mux" | |||
"github.com/gorilla/sessions" | |||
) | |||
type ctxKey int | |||
const key ctxKey = iota | |||
// Config is the type wrapping a session's configuration options. | |||
type Config struct { | |||
Name string | |||
Key string | |||
} | |||
// NewMiddleware creates and returns a Middleware that stores a user's | |||
// session in the HTTP Request's Context. Additionally, it makes sure | |||
// that the session is saved exactly once, before the the server writes | |||
// its response to the client. | |||
func NewMiddleware(conf *Config) mux.MiddlewareFunc { | |||
store := sessions.NewCookieStore([]byte(conf.Key)) | |||
store.Options.SameSite = http.SameSiteStrictMode | |||
store.Options.HttpOnly = true | |||
mw := func(next http.Handler) http.Handler { | |||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |||
s, err := store.Get(r, conf.Name) | |||
if err != nil { | |||
log.Print(err) | |||
http.Error(w, err.Error(), http.StatusInternalServerError) | |||
return | |||
} | |||
r = r.WithContext(context.WithValue(r.Context(), key, s)) | |||
rw := &responseWriter{ | |||
w: w, | |||
r: r, | |||
} | |||
next.ServeHTTP(rw, r) | |||
if !rw.saved { | |||
rw.saveSession() | |||
} | |||
}) | |||
} | |||
return mw | |||
} | |||
type responseWriter struct { | |||
w http.ResponseWriter | |||
r *http.Request | |||
saved bool | |||
} | |||
func (w *responseWriter) Header() http.Header { | |||
return w.w.Header() | |||
} | |||
func (w *responseWriter) Write(b []byte) (int, error) { | |||
if !w.saved { | |||
w.WriteHeader(http.StatusOK) | |||
} | |||
return w.w.Write(b) | |||
} | |||
func (w *responseWriter) WriteHeader(statusCode int) { | |||
if !w.saved { | |||
w.saveSession() | |||
} | |||
w.w.WriteHeader(statusCode) | |||
} | |||
func (w *responseWriter) saveSession() { | |||
if w.saved { | |||
return | |||
} | |||
w.saved = true | |||
s, ok := FromContext(w.r.Context()) | |||
if !ok { | |||
return | |||
} | |||
err := s.Save(w.r, w.w) | |||
if err != nil { | |||
// This should never happen, log it just to be safe. | |||
log.Print(err) | |||
return | |||
} | |||
} | |||
// From context returns the session stored in ctx, if any. | |||
func FromContext(ctx context.Context) (*sessions.Session, bool) { | |||
s, ok := ctx.Value(key).(*sessions.Session) | |||
return s, ok | |||
} | |||
// MustFromContext is like FromContext, except that it panics | |||
// if no session is stored in ctx. | |||
func MustFromContext(ctx context.Context) *sessions.Session { | |||
s, ok := FromContext(ctx) | |||
if !ok { | |||
panic("session not found in context") | |||
} | |||
return s | |||
} |
@@ -0,0 +1,80 @@ | |||
// Package address defines interfaces and data structures for working with customer addresses. | |||
package address | |||
import ( | |||
"errors" | |||
"net/http" | |||
"github.com/google/uuid" | |||
"github.com/gorilla/schema" | |||
"gitlab.cs.fau.de/faust/faustctf-2020/ipps/pkg/user" | |||
) | |||
var ErrAddressAlreadyAdded = errors.New("address: user has already added this address") | |||
type Address struct { | |||
ID uuid.UUID `json:"id" schema:"id"` | |||
Street string `json:"street" schema:"street,required"` | |||
Zip string `json:"zip" schema:"zip,required"` | |||
City string `json:"city" schema:"city,required"` | |||
Country string `json:"country" schema:"country,required"` | |||
Planet string `json:"planet" schema:"planet"` | |||
User *user.User `json:"-" schema:"-"` | |||
} | |||
// NewForUser creates and returns a new Address, with its User member set to u. | |||
func NewForUser(u *user.User) (*Address, error) { | |||
id, err := uuid.NewRandom() | |||
if err != nil { | |||
return nil, err | |||
} | |||
return &Address{ | |||
ID: id, | |||
User: u, | |||
}, nil | |||
} | |||
var formDecoder = schema.NewDecoder() | |||
// NewFromFormForUser parses r's post form, decoding it into an Address, | |||
// using a newly generated ID as the address's ID and setting the address's | |||
// User member to u. | |||
func NewFromFormForUser(r *http.Request, u *user.User) (*Address, error) { | |||
err := r.ParseForm() | |||
if err != nil { | |||
return nil, err | |||
} | |||
a := &Address{User: u} | |||
err = formDecoder.Decode(a, r.PostForm) | |||
if err != nil { | |||
return nil, err | |||
} | |||
// Ignore user supplied ID | |||
id, err := uuid.NewRandom() | |||
if err != nil { | |||
return nil, err | |||
} | |||
a.ID = id | |||
return a, nil | |||
} | |||
// FromFormForUser parses r's post form into an address, using the id | |||
// provided in the form as the address's ID and setting the address's | |||
// User member to u. | |||
func FromFormForUser(r *http.Request, u *user.User) (*Address, error) { | |||
err := r.ParseForm() | |||
if err != nil { | |||
return nil, err | |||
} | |||
a := &Address{User: u} | |||
err = formDecoder.Decode(a, r.PostForm) | |||
if err != nil { | |||
return nil, err | |||
} | |||
return a, nil | |||
} |
@@ -0,0 +1,25 @@ | |||
package address | |||
import ( | |||
"github.com/google/uuid" | |||
"gitlab.cs.fau.de/faust/faustctf-2020/ipps/pkg/user" | |||
) | |||
type Accesser interface { | |||
ByID(id uuid.UUID) (*Address, error) | |||
ByUser(u *user.User) ([]*Address, error) | |||
} | |||
type Inserter interface { | |||
Insert(a *Address) error | |||
} | |||
type Updater interface { | |||
Update(a *Address) error | |||
} | |||
type Storage interface { | |||
Accesser | |||
Inserter | |||
Updater | |||
} |
@@ -0,0 +1,53 @@ | |||
package credit | |||
import ( | |||
"net/http" | |||
"github.com/google/uuid" | |||
"github.com/gorilla/schema" | |||
"gitlab.cs.fau.de/faust/faustctf-2020/ipps/pkg/user" | |||
) | |||
var formDecoder = schema.NewDecoder() | |||
// Card is the representation of a single credit card. | |||
type Card struct { | |||
// ID is the (internal) unique identifier of the credit card. | |||
ID uuid.UUID `schema:"-" json:"id"` | |||
// Number is the credit card's number. | |||
Number string `schema:"number,required" json:"number"` | |||
// User is the user to which the credit card belongs. | |||
User *user.User `schema:"-" json:"-"` | |||
} | |||
// NewCard returns a new credit card for user | |||
func NewCard(user *user.User) (*Card, error) { | |||
id, err := uuid.NewRandom() | |||
if err != nil { | |||
return nil, err | |||
} | |||
return &Card{ | |||
ID: id, | |||
User: user, | |||
}, nil | |||
} | |||
// NewCard parses the request's form and fills a new credit card | |||
// according to the form's values for user. | |||
func NewCardFromForm(user *user.User, r *http.Request) (*Card, error) { | |||
err := r.ParseForm() | |||
if err != nil { | |||
return nil, err | |||
} | |||
c, err := NewCard(user) | |||
if err != nil { | |||
return nil, err | |||
} | |||
err = formDecoder.Decode(c, r.PostForm) | |||
if err != nil { | |||
return nil, err | |||
} | |||
return c, nil | |||
} |
@@ -0,0 +1,3 @@ | |||
// Package credit contains interfaces and data structures for | |||
// managing customer's credit card data. | |||
package credit |
@@ -0,0 +1,47 @@ | |||
package credit | |||
import ( | |||
"errors" | |||
"gitlab.cs.fau.de/faust/faustctf-2020/ipps/pkg/user" | |||
) | |||
var ErrNoCards = errors.New("user does not have any credit cards") | |||
// Inserter is the interfaces for insertying credit cards into | |||
// a persistent storage. | |||
// | |||
// Insert inserts c into the Inserter's underlying storage. | |||
type Inserter interface { | |||
Insert(c *Card) error | |||
} | |||
// Accesser is the interface wrapping the ByUser method. | |||
// | |||
// ByUser returns all of a users credit cards. | |||
type Accesser interface { | |||
ByUser(u *user.User) ([]*Card, error) | |||
} | |||
// Updater is the interfaces wrapping the Update method. | |||
// | |||
// Update updates m in the Updater's underlying storage. | |||
type Updater interface { | |||
Update(c *Card) error | |||
} | |||
// Deleter is the interfaces wrapping the Delete method. | |||
// | |||
// Delete removes m from the Deleter's underlying storage. | |||
type Deleter interface { | |||
Delete(c *Card) error | |||
} | |||
// Storage is the interface wrapping all interfaces for inserting, | |||
// retrieving, updating and deleting credit card information. | |||
type Storage interface { | |||
Inserter | |||
Accesser | |||
Updater | |||
Deleter | |||
} |
@@ -0,0 +1,52 @@ | |||
// Package feedback defines interfaces and data | |||
// structures for handling customer feedback. | |||
package feedback | |||
import ( | |||
"errors" | |||
"html/template" | |||
"strings" | |||
"time" | |||
"github.com/google/uuid" | |||
) | |||
var ( | |||
ErrEmptyFeedback = errors.New("feedback text is empty") | |||
) | |||
// Feedback is the representation of a customer's feedback message. | |||
type Feedback struct { | |||
ID uuid.UUID `json:"id"` | |||
Author string `json:"author"` | |||
Rating uint8 `json:"rating"` | |||
Text string `json:"text"` | |||
Date time.Time `json:"datePosted"` | |||
} | |||
func New(author string, rating uint8, text string) (*Feedback, error) { | |||
id, err := uuid.NewRandom() | |||
if err != nil { | |||
return nil, err | |||
} | |||
if text == "" { | |||
return nil, ErrEmptyFeedback | |||
} | |||
return &Feedback{ | |||
ID: id, | |||
Author: author, | |||
Rating: rating, | |||
Text: text, | |||
Date: time.Now().Local(), | |||
}, nil | |||
} | |||
func (f *Feedback) Stars() template.HTML { | |||
var b strings.Builder | |||
for i := uint8(0); i < f.Rating; i++ { | |||
b.WriteString(`<span class="material-icons rating-star">stare_rate</span>`) | |||
} | |||
return template.HTML(b.String()) | |||
} |
@@ -0,0 +1,18 @@ | |||
package feedback | |||
type Accesser interface { | |||
// Recent returns all feedback from the last 24 hours. | |||
Recent() ([]Feedback, error) | |||
// Multiple returns up to n feedback recent posts, skipping | |||
// offset posts. | |||
Multiple(n, offset uint) ([]Feedback, error) | |||
} | |||
type Inserter interface { | |||
Insert(feedback *Feedback) error | |||
} | |||
type Storage interface { | |||
Accesser | |||
Inserter | |||
} |
@@ -0,0 +1,56 @@ | |||
// Package parcel implements data structures and | |||
// interfaces for working with parcel information. | |||
package parcel | |||
import ( | |||
"time" | |||
"github.com/google/uuid" | |||
"gitlab.cs.fau.de/faust/faustctf-2020/ipps/pkg/address" | |||
) | |||
// Parcel is the data type representing a single parcel. | |||
type Parcel struct { | |||
// ID is the parcel's unique identifier. This is also its tracking id. | |||
ID uuid.UUID | |||
ReturnAddress *address.Address | |||
DestinationAddress *address.Address | |||
} | |||
type EventType int | |||
const ( | |||
DataReceived EventType = iota | |||
DeliveredToIPPS | |||
DeliveredToProcessing | |||
LoadedIntoRocket | |||
LoadedIntoVehicle | |||
DeliveredToDestination | |||
) | |||
func (t EventType) String() string { | |||
switch t { | |||
case DataReceived: | |||
return "We have received parcel processing information from the sender" | |||
case DeliveredToIPPS: | |||
return "The sender has delivered the parcel to one of our shops" | |||
case DeliveredToProcessing: | |||
return "The parcel has been delivered to one of our logistics centers" | |||
case LoadedIntoRocket: | |||
return "The parcel has been loaded into one of our delivery rockets" | |||
case LoadedIntoVehicle: | |||
return "The parcel has been loaded into a vehicle and is going to be delivered to its final destination" | |||
case DeliveredToDestination: | |||
return "The package has been delivered to its destination" | |||
default: | |||
return "Unknown event" | |||
} | |||
} | |||
// Event is the type representing tracking events for parcels. | |||
type Event struct { | |||
ID uuid.UUID | |||
Parcel *Parcel | |||
Type EventType | |||
Time time.Time | |||
} |
@@ -0,0 +1,33 @@ | |||
package parcel | |||
import ( | |||
"github.com/google/uuid" | |||
"gitlab.cs.fau.de/faust/faustctf-2020/ipps/pkg/address" | |||
) | |||
type Inserter interface { | |||
Insert(p *Parcel) error | |||
} | |||
type Accesser interface { | |||
ByID(id uuid.UUID) (*Parcel, error) | |||
ByDestination(a *address.Address) ([]*Parcel, error) | |||
} | |||
type Storage interface { | |||
Inserter | |||
Accesser | |||
} | |||
type EventInserter interface { | |||
Insert(e *Event) error | |||
} | |||
type EventAccesser interface { | |||
ByParcel(p *Parcel) ([]*Event, error) | |||
} | |||
type EventStorage interface { | |||
EventInserter | |||
EventAccesser | |||
} |
@@ -0,0 +1,135 @@ | |||
package postgres | |||
import ( | |||
"database/sql" | |||
"github.com/google/uuid" | |||
"github.com/lib/pq" | |||
"gitlab.cs.fau.de/faust/faustctf-2020/ipps/pkg/address" | |||
"gitlab.cs.fau.de/faust/faustctf-2020/ipps/pkg/user" | |||
) | |||
const ( | |||
installAddressTable = `CREATE TABLE IF NOT EXISTS ipps_address ( | |||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(), | |||
street text NOT NULL, | |||
zip text NOT NULL, | |||
city text NOT NULL, | |||
country text NOT NULL, | |||
planet text NOT NULL DEFAULT 'Mars', | |||
user_id uuid NOT NULL CONSTRAINT ipps_address_user_fkey | |||
REFERENCES ipps_user ON DELETE CASCADE ON UPDATE CASCADE, | |||
CONSTRAINT ipps_address_unique_per_user | |||
UNIQUE (street, zip, city, country, planet, user_id) | |||
);` | |||
addressByID = `SELECT id, street, zip, city, country, planet | |||
FROM ipps_address | |||
WHERE id = $1;` | |||
addressByUser = `SELECT id, street, zip, city, country, planet | |||
FROM ipps_address | |||
WHERE user_id = $1;` | |||
insertAddress = `INSERT INTO ipps_address (id, street, zip, city, country, planet, user_id) | |||
VALUES ($1, $2, $3, $4, $5, $6, $7);` | |||
updateAddress = `UPDATE ipps_address | |||
SET (street, zip, city, country, planet) = ($2, $3, $4, $5, $6) | |||
WHERE id = $1;` | |||
) | |||
// AddressStorage is the type implemented the address.Storage interface. | |||
type AddressStorage struct { | |||
byID *sql.Stmt | |||
byUser *sql.Stmt | |||
insert *sql.Stmt | |||
update *sql.Stmt | |||
} | |||
func NewAddressStorage(db *sql.DB) (*AddressStorage, error) { | |||
s := &AddressStorage{} | |||
var err error | |||
s.byID, err = db.Prepare(addressByID) | |||
if err != nil { | |||
return nil, err | |||
} | |||
s.byUser, err = db.Prepare(addressByUser) | |||
if err != nil { | |||
return nil, err | |||
} | |||
s.insert, err = db.Prepare(insertAddress) | |||
if err != nil { | |||
return nil, err | |||
} | |||
s.update, err = db.Prepare(updateAddress) | |||
if err != nil { | |||
return nil, err | |||
} | |||
return s, nil | |||
} | |||
func (s *AddressStorage) ByID(id uuid.UUID) (*address.Address, error) { | |||
a := &address.Address{} | |||
err := s.byID.QueryRow(id).Scan(&a.ID, &a.Street, &a.Zip, &a.City, &a.Country, &a.Planet) | |||
if err == sql.ErrNoRows { | |||
return nil, nil | |||
} else if err != nil { | |||
return nil, err | |||
} | |||
return a, nil | |||
} | |||
func (s *AddressStorage) ByUser(u *user.User) ([]*address.Address, error) { | |||
rr, err := s.byUser.Query(u.ID) | |||
if err != nil { | |||
return nil, err | |||
} | |||
var aa []*address.Address | |||
for rr.Next() { | |||
a := &address.Address{User: u} | |||
err := rr.Scan(&a.ID, &a.Street, &a.Zip, &a.City, &a.Country, &a.Planet) | |||
if err != nil { | |||
return nil, err | |||
} | |||
aa = append(aa, a) | |||
} | |||
return aa, nil | |||
} | |||
func (s *AddressStorage) Insert(a *address.Address) error { | |||
_, err := s.insert.Exec(a.ID, a.Street, a.Zip, a.City, a.Country, a.Planet, a.User.ID) | |||
if err != nil { | |||
pgErr, ok := err.(*pq.Error) | |||
if ok && pgErr.Constraint == "ipps_address_unique_per_user" { | |||
return address.ErrAddressAlreadyAdded | |||
} | |||
} | |||
return err | |||
} | |||
func (s *AddressStorage) Update(a *address.Address) error { | |||
_, err := s.update.Exec(a.ID, a.Street, a.Zip, a.City, a.Country, a.Planet) | |||
if err != nil { | |||
pgErr, ok := err.(*pq.Error) | |||
if ok && pgErr.Constraint == "ipps_address_unique_per_user" { | |||
return address.ErrAddressAlreadyAdded | |||
} | |||
} | |||
return nil | |||
} | |||
func (s *AddressStorage) Close() error { | |||
err := s.byUser.Close() | |||
if err != nil { | |||
return err | |||
} | |||
err = s.insert.Close() | |||
if err != nil { | |||
return err | |||
} | |||
return s.update.Close() | |||
} |
@@ -0,0 +1,120 @@ | |||
package postgres | |||
import ( | |||
"database/sql" | |||
"github.com/google/uuid" | |||
"gitlab.cs.fau.de/faust/faustctf-2020/ipps/pkg/credit" | |||
"gitlab.cs.fau.de/faust/faustctf-2020/ipps/pkg/user" | |||
) | |||
const ( | |||
installCardTable = `CREATE TABLE IF NOT EXISTS ipps_card ( | |||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(), | |||
num text NOT NULL, | |||
user_id uuid NOT NULL CONSTRAINT ipps_card_user_fkey REFERENCES ipps_user | |||
ON UPDATE CASCADE | |||
ON DELETE CASCADE | |||
);` | |||
insertCardStmt = `INSERT INTO ipps_card (id, num, user_id) | |||
VALUES ($1, $2, $3);` | |||
cardByUserStmt = `SELECT id, num, user_id | |||
FROM ipps_card | |||
WHERE user_id = $1;` | |||
updateCardStmt = `UPDATE ipps_card | |||
SET num = $2 | |||
WHERE id = $1;` | |||
deleteCardStmt = `DELETE | |||
FROM ipps_card | |||
WHERE id = $1;` | |||
) | |||
// CreditCardStorage is an implementation of the credit.Storage interface | |||
// using a PostgreSQL database as its underlying storage. | |||
type CreditCardStorage struct { | |||
insert *sql.Stmt | |||
byUser *sql.Stmt | |||
update *sql.Stmt | |||
delete *sql.Stmt | |||
} | |||
// New CreditCardStorage returns | |||
func NewCreditCardStorage(db *sql.DB) (*CreditCardStorage, error) { | |||
cs := &CreditCardStorage{} | |||
var err error | |||
cs.insert, err = db.Prepare(insertCardStmt) | |||
if err != nil { | |||
return nil, err | |||
} | |||
cs.update, err = db.Prepare(updateCardStmt) | |||
if err != nil { | |||
return nil, err | |||
} | |||
cs.byUser, err = db.Prepare(cardByUserStmt) | |||
if err != nil { | |||
return nil, err | |||
} | |||
cs.delete, err = db.Prepare(deleteCardStmt) | |||
if err != nil { | |||
return nil, err | |||
} | |||
return cs, nil | |||
} | |||
func (cs *CreditCardStorage) Insert(c *credit.Card) error { | |||
_, err := cs.insert.Exec(c.ID, c.Number, c.User.ID) | |||
return err | |||
} | |||
func (cs *CreditCardStorage) ByUser(u *user.User) ([]*credit.Card, error) { | |||
rows, err := cs.byUser.Query(u.ID) | |||
if err == sql.ErrNoRows { | |||
return nil, credit.ErrNoCards | |||
} else if err != nil { | |||
return nil, err | |||
} | |||
cc := make([]*credit.Card, 0) | |||
for rows.Next() { | |||
var uid uuid.UUID | |||
c := &credit.Card{User: u} | |||
err := rows.Scan(&c.ID, &c.Number, &uid) | |||
if err != nil { | |||
return nil, err | |||
} | |||
cc = append(cc, c) | |||
} | |||
return cc, nil | |||
} | |||
func (cs *CreditCardStorage) Update(c *credit.Card) error { | |||
_, err := cs.update.Exec(c.ID, c.Number, c.User.ID) | |||
return err | |||
} | |||
func (cs *CreditCardStorage) Delete(c *credit.Card) error { | |||
_, err := cs.delete.Exec(c.ID) | |||
return err | |||
} | |||
func (cs *CreditCardStorage) Close() error { | |||
err := cs.insert.Close() | |||
if err != nil { | |||
return err | |||
} | |||
err = cs.byUser.Close() | |||
if err != nil { | |||
return err | |||
} | |||
err = cs.update.Close() | |||
if err != nil { | |||
return err | |||
} | |||
err = cs.delete.Close() | |||
if err != nil { | |||
return err | |||
} | |||
return nil | |||
} |
@@ -0,0 +1,79 @@ | |||
package postgres | |||
import ( | |||
"database/sql" | |||
"gitlab.cs.fau.de/faust/faustctf-2020/ipps/pkg/parcel" | |||
) | |||
const ( | |||
installParcelEventTable = `CREATE TABLE IF NOT EXISTS ipps_parcel_event( | |||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(), | |||
event_type integer NOT NULL, | |||
event_time timestamptz NOT NULL, | |||
parcel uuid NOT NULL CONSTRAINT ipps_parcel_event_parcel_fkey | |||
REFERENCES ipps_parcel (id) ON DELETE CASCADE ON UPDATE CASCADE | |||
);` | |||
insertParcelEventStmt = `INSERT INTO ipps_parcel_event (id, event_type, event_time, parcel) | |||
VALUES ($1, $2, $3, $4);` | |||
parcelEventByParcelStmt = `SELECT (id, event_type, event_time) | |||
FROM ipps_parcel_event | |||
WHERE parcel = $1 | |||
ORDER BY event_time ASC;` | |||
) | |||
type EventStorage struct { | |||
insert *sql.Stmt | |||
byParcel *sql.Stmt | |||
} | |||
func NewEventStorage(db *sql.DB) (*EventStorage, error) { | |||
s := &EventStorage{} | |||
var err error | |||
s.insert, err = db.Prepare(insertParcelEventStmt) | |||
if err != nil { | |||
return nil, err | |||
} | |||
s.byParcel, err = db.Prepare(parcelEventByParcelStmt) | |||
if err != nil { | |||
return nil, err | |||
} | |||
return s, nil | |||
} | |||
func (es *EventStorage) Insert(e *parcel.Event) error { | |||
_, err := es.insert.Exec(e.ID, e.Type, e.Time, e.Parcel.ID) | |||
return err | |||
} | |||
func (es *EventStorage) ByParcel(p *parcel.Parcel) ([]*parcel.Event, error) { | |||
rows, err := es.byParcel.Query(p.ID) | |||
if err == sql.ErrNoRows { | |||
return nil, nil | |||
} else if err != nil { | |||
return nil, err | |||
} | |||
var ee []*parcel.Event | |||
for rows.Next() { | |||
e := &parcel.Event{Parcel: p} | |||
err := rows.Scan(&e.ID, &e.Type, &e.Time) | |||
if err != nil { | |||
return nil, err | |||
} | |||
ee = append(ee, e) | |||
} | |||
return ee, nil | |||
} | |||
func (es *EventStorage) Close() error { | |||
err := es.insert.Close() | |||
if err != nil { | |||
return err | |||
} | |||
return es.byParcel.Close() | |||
} |
@@ -0,0 +1,133 @@ | |||
package postgres | |||
import ( | |||
"database/sql" | |||
"time" | |||
"github.com/google/uuid" | |||
"gitlab.cs.fau.de/faust/faustctf-2020/ipps/pkg/feedback" | |||
) | |||
const ( | |||
installFeedbackTable = `CREATE TABLE IF NOT EXISTS ipps_feedback( | |||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(), | |||
author varchar(128) NOT NULL CONSTRAINT ipps_feedback_author_fkey | |||
REFERENCES ipps_user (username) ON DELETE CASCADE ON UPDATE CASCADE, | |||
rating integer NOT NULL CHECK (rating > 0 and rating <= 5), | |||
feedback text NOT NULL, | |||
date_posted timestamptz NOT NULL | |||
);` | |||
multipleFeedbackStmt = `SELECT id, author, rating, feedback, date_posted | |||
FROM ipps_feedback | |||
WHERE date_posted >= NOW() - INTERVAL '1 hour' | |||
ORDER BY date_posted DESC | |||
OFFSET $1 ROWS | |||
FETCH FIRST $2 ROWS ONLY;` | |||
recentFeedbackStmt = `SELECT id, author, rating, feedback, date_posted | |||
FROM ipps_feedback | |||
WHERE date_posted >= NOW() - INTERVAL '1 hour' | |||
ORDER BY date_posted DESC;` | |||
insertFeedbackStmt = `INSERT INTO ipps_feedback (id, author, rating, feedback, date_posted) | |||
VALUES ($1, $2, $3, $4, $5);` | |||
) | |||
// FeedbackStorage is the postgres implementation of the feedback.Storage interface. | |||
type FeedbackStorage struct { | |||
multiple *sql.Stmt | |||
recent *sql.Stmt | |||
insert *sql.Stmt | |||
} | |||
func NewFeedbackStorage(db *sql.DB) (*FeedbackStorage, error) { | |||
ms, err := db.Prepare(multipleFeedbackStmt) | |||
if err != nil { | |||
return nil, err | |||
} | |||
rs, err := db.Prepare(recentFeedbackStmt) | |||
if err != nil { | |||
return nil, err | |||
} | |||
is, err := db.Prepare(insertFeedbackStmt) | |||
if err != nil { | |||
return nil, err | |||
} | |||
return &FeedbackStorage{ | |||
multiple: ms, | |||
recent: rs, | |||
insert: is, | |||
}, nil | |||
} | |||
func (fs *FeedbackStorage) Multiple(n, offset uint) ([]feedback.Feedback, error) { | |||
rows, err := fs.multiple.Query(offset, n) | |||
if err == sql.ErrNoRows { | |||
return nil, nil | |||
} else if err != nil { | |||
return nil, err | |||
} | |||
ff := make([]feedback.Feedback, n) | |||
i := 0 | |||
for rows.Next() { | |||
f := &ff[i] | |||
err := rows.Scan(&f.ID, &f.Author, &f.Rating, &f.Text, &f.Date) | |||
if err != nil { | |||
return nil, err | |||
} | |||
i++ | |||
} | |||
ff = ff[:i] | |||
return ff, nil | |||
} | |||
func (fs *FeedbackStorage) Recent() ([]feedback.Feedback, error) { | |||
rows, err := fs.recent.Query() | |||
if err == sql.ErrNoRows { | |||
return nil, nil | |||
} else if err != nil { | |||
return nil, err | |||
} | |||
ff := make([]feedback.Feedback, 0, 10) | |||
for rows.Next() { | |||
var id uuid.UUID | |||
var author string | |||
var rating uint8 | |||
var text string | |||
var datePosted time.Time | |||
err := rows.Scan(&id, &author, &rating, &text, &datePosted) | |||
if err != nil { | |||
return nil, err | |||
} | |||
ff = append(ff, feedback.Feedback{ | |||
ID: id, | |||
Author: author, | |||
Rating: rating, | |||
Text: text, | |||
Date: datePosted, | |||
}) | |||
} | |||
return ff, nil | |||
} | |||
func (fs *FeedbackStorage) Insert(f *feedback.Feedback) error { | |||
_, err := fs.insert.Exec(&f.ID, &f.Author, &f.Rating, &f.Text, &f.Date) | |||
return err | |||
} | |||
func (fs *FeedbackStorage) Close() error { | |||
err := fs.insert.Close() | |||
if err != nil { | |||
return err | |||
} | |||
err = fs.recent.Close() | |||
if err != nil { | |||
return err | |||
} | |||
return fs.multiple.Close() | |||
} |
@@ -0,0 +1,105 @@ | |||
package postgres | |||
import ( | |||
"database/sql" | |||
"github.com/google/uuid" | |||
"gitlab.cs.fau.de/faust/faustctf-2020/ipps/pkg/address" | |||
"gitlab.cs.fau.de/faust/faustctf-2020/ipps/pkg/parcel" | |||
) | |||
const ( | |||
installParcelTable = `CREATE TABLE IF NOT EXISTS ipps_parcel( | |||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(), | |||
destination_address uuid CONSTRAINT ipps_parcel_dest_addr_fkey | |||
REFERENCES ipps_address (id) ON DELETE SET NULL ON UPDATE CASCADE, | |||
return_address uuid CONSTRAINT ipps_parcel_return_addr_fkey | |||
REFERENCES ipps_address (id) ON DELETE SET NULL ON UPDATE CASCADE | |||
);` | |||
insertParcelStmt = `INSERT INTO ipps_parcel(id, destination_address, return_address) | |||
VALUES ($1, $2, $3);` | |||
parcelByIDStmt = `SELECT (id, destination_address, return_address) | |||
FROM ipps_parcel | |||
WHERE id = $1;` | |||
parcelByDestinationStmt = `SELECT (id, destination_address, return_address) | |||
FROM ipps_parcel | |||
WHERE destination_address = $1;` | |||
) | |||
// ParcelStorage is the PostgreSQL based implementation of | |||
// the parcel.Storage and parcel.EventStorage interfaces. | |||
type ParcelStorage struct { | |||
insert *sql.Stmt | |||
byID *sql.Stmt | |||
byDestination *sql.Stmt | |||
} | |||
func NewParcelStorage(db *sql.DB) (*ParcelStorage, error) { | |||
ps := &ParcelStorage{} | |||
var err error | |||
ps.insert, err = db.Prepare(insertParcelStmt) | |||
if err != nil { | |||
return nil, err | |||
} | |||
ps.byID, err = db.Prepare(parcelByIDStmt) | |||
if err != nil { | |||
return nil, err | |||
} | |||
ps.byDestination, err = db.Prepare(parcelByDestinationStmt) | |||
if err != nil { | |||
return nil, err | |||
} | |||
return ps, nil | |||
} | |||
func (ps *ParcelStorage) Insert(p *parcel.Parcel) error { | |||
_, err := ps.insert.Exec(p.ID, p.DestinationAddress.ID, p.DestinationAddress.ID) | |||
return err | |||
} | |||
func (ps *ParcelStorage) ByID(id uuid.UUID) (*parcel.Parcel, error) { | |||
p := &parcel.Parcel{} | |||
err := ps.byID.QueryRow(id).Scan(&p.ID, &p.DestinationAddress, &p.ReturnAddress) | |||
if err == sql.ErrNoRows { | |||
return nil, nil | |||
} else if err != nil { | |||
return nil, err | |||
} | |||
return p, nil | |||
} | |||
func (ps *ParcelStorage) ByDestination(a *address.Address) ([]*parcel.Parcel, error) { | |||
rows, err := ps.byDestination.Query(a.ID) | |||
if err == sql.ErrNoRows { | |||
return nil, nil | |||
} else if err != nil { | |||
return nil, err | |||
} | |||
pp := make([]*parcel.Parcel, 0) | |||
for rows.Next() { | |||
p := &parcel.Parcel{} | |||
err := rows.Scan(&p.ID, &p.DestinationAddress, &p.ReturnAddress) | |||
if err != nil { | |||
return nil, err | |||
} | |||
pp = append(pp, p) | |||
} | |||
return pp, nil | |||
} | |||
func (ps *ParcelStorage) Close() error { | |||
err := ps.insert.Close() | |||
if err != nil { | |||
return err | |||
} | |||
err = ps.byDestination.Close() | |||
if err != nil { | |||
return err | |||
} | |||
return ps.byID.Close() | |||
} |
@@ -0,0 +1,58 @@ | |||
// Package postgres implements interfaces for retrieving data from a | |||
// PostgreSQL database. | |||
package postgres | |||
import ( | |||
"database/sql" | |||
"fmt" | |||
_ "github.com/lib/pq" | |||
) | |||
type Config struct { | |||
Host string `toml:"hostname"` | |||
Port uint16 | |||
// Name is the database name. | |||
Name string | |||
// User is the username. | |||
User string `toml:"username"` | |||
// Password is the database user's password | |||
Password string | |||
} | |||
const connFmt = "host=%s port=%d user=%s password=%s dbname=%s connect_timeout=5" | |||
// Connect connects to the Postgres database using the connection settings specified | |||
// in conf. | |||
func Connect(conf *Config) (*sql.DB, error) { | |||
connStr := fmt.Sprintf(connFmt, conf.Host, conf.Port, conf.User, conf.Password, conf.Name) | |||
return sql.Open("postgres", connStr) | |||
} | |||
// InstallTables installs all tables necessary to run the website, using | |||
// PostgreSQL as the underlying storage. | |||
func InstallTables(db *sql.DB) error { | |||
_, err := db.Exec(installUserTable) | |||
if err != nil { | |||
return err | |||
} | |||
_, err = db.Exec(installCardTable) | |||
if err != nil { | |||
return err | |||
} | |||
_, err = db.Exec(installFeedbackTable) | |||
if err != nil { | |||
return err | |||
} | |||
_, err = db.Exec(installAddressTable) | |||
if err != nil { | |||
return err | |||
} | |||
_, err = db.Exec(installParcelTable) | |||
if err != nil { | |||
return err | |||
} | |||
_, err = db.Exec(installParcelEventTable) | |||
return err | |||
} |
@@ -0,0 +1,164 @@ | |||
package postgres | |||
import ( | |||
"database/sql" | |||
"net/mail" | |||
"github.com/google/uuid" | |||
"github.com/lib/pq" | |||
"gitlab.cs.fau.de/faust/faustctf-2020/ipps/pkg/user" | |||
) | |||
const ( | |||
installUserTable = `CREATE TABLE IF NOT EXISTS ipps_user ( | |||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(), | |||
username varchar(128) NOT NULL CONSTRAINT ipps_user_username_key UNIQUE, | |||
email varchar(128) NOT NULL CONSTRAINT ipps_user_email_key UNIQUE, | |||
full_name varchar(128) NOT NULL, | |||
password varchar(64) NOT NULL | |||
);` | |||
insertUserStmt = `INSERT INTO ipps_user (id, username, email, password, full_name) | |||
VALUES ($1, $2, $3, $4, $5);` | |||
updateUserStmt = `UPDATE ipps_user | |||
SET (email, password, full_name) = ($2, $3, $4) | |||
WHERE id = $1;` | |||
deleteUserStmt = `DELETE FROM ipps_user WHERE id = $1;` | |||
userByIDStmt = `SELECT id, username, email, password, full_name | |||
FROM ipps_user | |||
WHERE id = $1;` | |||
userByNameStmt = `SELECT id, username, email, password, full_name | |||
FROM ipps_user | |||
WHERE username = $1;` | |||
userByEmailStmt = `SELECT id, username, email, password, full_name | |||
FROM ipps_user | |||
WHERE email = $1;` | |||
) | |||
// UserStorage implements the user.Storage interface for a postgres | |||
// database. | |||
type UserStorage struct { | |||
insert *sql.Stmt | |||
update *sql.Stmt | |||
delete *sql.Stmt | |||
byID *sql.Stmt | |||
byUsername *sql.Stmt | |||
byEmail *sql.Stmt | |||
} | |||
// NewUserStorage returns a new user storage that runs its database | |||
// queries on db. | |||
func NewUserStorage(db *sql.DB) (*UserStorage, error) { | |||
us := &UserStorage{} | |||
var err error | |||
us.insert, err = db.Prepare(insertUserStmt) | |||
if err != nil { | |||
return nil, err | |||
} | |||
us.update, err = db.Prepare(updateUserStmt) | |||
if err != nil { | |||
return nil, err | |||
} | |||
us.delete, err = db.Prepare(deleteUserStmt) | |||
if err != nil { | |||
return nil, err | |||
} | |||
us.byID, err = db.Prepare(userByIDStmt) | |||
if err != nil { | |||
return nil, err | |||
} | |||
us.byUsername, err = db.Prepare(userByNameStmt) | |||
if err != nil { | |||
return nil, err | |||
} | |||
us.byEmail, err = db.Prepare(userByEmailStmt) | |||
if err != nil { | |||
return nil, err | |||
} | |||
return us, nil | |||
} | |||
func (us *UserStorage) Insert(u *user.User) error { | |||
_, err := us.insert.Exec(u.ID, u.Username, u.Email.Address, u.Password, u.Name) | |||
if err == nil { | |||
return nil | |||
} | |||
pgErr, ok := err.(*pq.Error) | |||
if !ok { | |||
return err | |||
} | |||
if pgErr.Constraint == "ipps_user_email_key" && pgErr.Code.Name() == "unique_violation" { | |||
return user.ErrEmailExists | |||
} else if pgErr.Constraint == "ipps_user_username_key" && | |||
pgErr.Code.Name() == "unique_violation" { | |||
return user.ErrUserExists | |||
} | |||
return err | |||
} | |||
func (us *UserStorage) ByID(id uuid.UUID) (*user.User, error) { | |||
return userFromRow(us.byID.QueryRow(id)) | |||
} | |||
func (us *UserStorage) ByEmail(address *mail.Address) (*user.User, error) { | |||
return userFromRow(us.byEmail.QueryRow(address.Address)) | |||
} | |||
func (us *UserStorage) ByUsername(username string) (*user.User, error) { | |||
return userFromRow(us.byUsername.QueryRow(username)) | |||
} | |||
func (us *UserStorage) Update(user *user.User) error { | |||
_, err := us.update.Exec(user.ID, user.Email.Address, user.Password, user.Name) | |||
return err | |||
} | |||
func (us *UserStorage) Delete(user *user.User) error { | |||
_, err := us.update.Exec(user.ID) | |||
return err | |||
} | |||
// Close closes the us's underlying database connection. | |||
func (us *UserStorage) Close() error { | |||
err := us.insert.Close() | |||
if err != nil { | |||
return err | |||
} | |||
err = us.byID.Close() | |||
if err != nil { | |||
return err | |||
} | |||
err = us.byEmail.Close() | |||
if err != nil { | |||
return err | |||
} | |||
err = us.update.Close() | |||
if err != nil { | |||
return err | |||
} | |||
err = us.delete.Close() | |||
if err != nil { | |||
return err | |||
} | |||
return nil | |||
} | |||
func userFromRow(row *sql.Row) (*user.User, error) { | |||
u := &user.User{} | |||
var email string | |||
err := row.Scan(&u.ID, &u.Username, &email, &u.Password, &u.Name) | |||
if err == sql.ErrNoRows { | |||
return nil, user.ErrUserNotExists | |||
} else if err != nil { | |||
return nil, err | |||
} | |||
u.Email, err = mail.ParseAddress(email) | |||
if err != nil { | |||
return nil, err | |||
} | |||
return u, nil | |||
} |
@@ -0,0 +1,110 @@ | |||
package user | |||
import ( | |||
"errors" | |||
"net/http" | |||
"net/mail" | |||
"github.com/gorilla/schema" | |||
) | |||
type LoginForm struct { | |||
Username string `schema:"username,required"` | |||
Password string `schema:"password,required"` | |||
} | |||
var formDecoder = schema.NewDecoder() | |||
func ParseLoginForm(r *http.Request) (*LoginForm, error) { | |||
err := r.ParseForm() | |||
if err != nil { | |||
return nil, err | |||
} | |||
lf := &LoginForm{} | |||
err = formDecoder.Decode(lf, r.PostForm) | |||
if err != nil { | |||
return nil, err | |||
} | |||
return lf, nil | |||
} | |||
var ErrPasswdConfirmMismatch = errors.New("password and its confirmation do not match") | |||
type registrationForm struct { | |||
Username string `schema:"username,required"` | |||
Email string `schema:"email,required"` | |||
Password string `schema:"password,required"` | |||
PasswordConfirmation string `schema:"password-confirm,required"` | |||
Name string `schema:"name,required"` | |||
} | |||
// ParseRegistrationForm parses a POST request's form data and | |||
// returns a new User initialized with values from the form. | |||
func ParseRegistrationForm(r *http.Request) (*User, error) { | |||
err := r.ParseForm() | |||
if err != nil { | |||
return nil, err | |||
} | |||
f := ®istrationForm{} | |||
err = formDecoder.Decode(f, r.PostForm) | |||
if err != nil { | |||
return nil, err | |||
} | |||
if f.Password != f.PasswordConfirmation { | |||
return nil, ErrPasswdConfirmMismatch | |||
} | |||
u, err := New(f.Username, f.Email, f.Password) | |||
if err != nil { | |||
return nil, err | |||
} | |||
u.Name = f.Name | |||
return u, nil | |||
} | |||
var ( | |||
ErrPasswordRequired = errors.New("current password is required to update password") | |||
) | |||
type editForm struct { | |||
Name string `schema:"name,required"` | |||
Email string `schema:"email,required"` | |||
CurrentPassword string `schema:"current-password"` | |||
NewPassword string `schema:"new-password"` | |||
NewPasswordConfirmation string `schema:"new-password-confirmation"` | |||
} | |||
func UpdateFromEditForm(u *User, r *http.Request) error { | |||
err := r.ParseForm() | |||
if err != nil { | |||
return err | |||
} | |||
f := &editForm{} | |||
err = formDecoder.Decode(f, r.PostForm) | |||
if err != nil { | |||
return err | |||
} | |||
u.Name = f.Name | |||
m, err := mail.ParseAddress(f.Email) | |||
if err != nil { | |||
return err | |||
} | |||
u.Email = m | |||
if f.NewPassword == "" { | |||
return nil | |||
} | |||
if f.CurrentPassword == "" { | |||
return ErrPasswordRequired | |||
} | |||
if f.NewPassword != f.NewPasswordConfirmation { | |||
return ErrPasswdConfirmMismatch | |||
} | |||
err = u.SetPassword(f.NewPassword) | |||
if err != nil { | |||
return err | |||
} | |||
return nil | |||
} |
@@ -0,0 +1,58 @@ | |||
package user | |||
import ( | |||
"errors" | |||
"net/mail" | |||
"github.com/google/uuid" | |||
) | |||
var ErrUserExists = errors.New("a user with that username already exists") | |||
var ErrUserNotExists = errors.New("user does not exist") | |||
var ErrEmailExists = errors.New("a user with that email address is already registered") | |||
// Inserter is the interface for inserting users to a storage. | |||
// | |||
// Insert inserts the user into the Inserter's underyling storage. | |||
type Inserter interface { | |||
Insert(user *User) error | |||
} | |||
// Accesser is the interface wrapping methods for accessing user data from its | |||
// underlying storage. | |||
// | |||
// ByID returns the user identified by id or nil, if the user does not exist. | |||
// | |||
// ByUsername returns the user identified by username or nil if no user | |||
// with that name exists. | |||
// ByEmail returns the user identified by the email address or nil if no user | |||
// with that email address exists. | |||
type Accesser interface { | |||
ByID(id uuid.UUID) (*User, error) | |||
ByUsername(username string) (*User, error) | |||
ByEmail(address *mail.Address) (*User, error) | |||
} | |||
// Update is the interface wrapping the Update method. | |||
// | |||
// Update updates user in the Updater's underlying storage. | |||
type Updater interface { | |||
Update(user *User) error | |||
} | |||
// Deleter is the interface wrapping the Delete method. | |||
// | |||
// Delete deletes a user from the Deleter's underlying storage. | |||
type Deleter interface { | |||
Delete(user *User) error | |||
} | |||
// Storage is the interface wrapping methods for creating, accessing, updating | |||
// and deleting users from its underlying storage. | |||
type Storage interface { | |||
Inserter | |||
Accesser | |||
Updater | |||
Deleter | |||
} |
@@ -0,0 +1,95 @@ | |||
// Package user contains interfaces and data structures for working with users. | |||
package user | |||
import ( | |||
"context" | |||
"net/mail" | |||
"github.com/google/uuid" | |||
"golang.org/x/crypto/bcrypt" | |||
) | |||
// User is the type representing a single user of the website. | |||
type User struct { | |||
ID uuid.UUID `json:"id"` | |||
Username string `json:"username"` | |||
Password []byte `json:"-"` | |||
Email *mail.Address `json:"email"` | |||
Name string `json:"name"` | |||
} | |||
// New initializes and returns a new User object. | |||
// The user's ID field is a randomly generated UUID. | |||
func New(username, email, password string) (*User, error) { | |||
id, err := uuid.NewRandom() | |||
if err != nil { | |||
return nil, err | |||
} | |||
addr, err := mail.ParseAddress(email) | |||
if err != nil { | |||
return nil, err | |||
} | |||
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) | |||
if err != nil { | |||
return nil, err | |||
} | |||
return &User{ | |||
ID: id, | |||
Username: username, | |||
Password: hash, | |||
Email: addr, | |||
Name: "", | |||
}, nil | |||
} | |||
// PasswordEquals returns, whether the given plaintext password is | |||
// equal to the user's current password. | |||
func (u *User) PasswordEquals(plaintext string) bool { | |||
err := bcrypt.CompareHashAndPassword(u.Password, []byte(plaintext)) | |||
if err != nil { | |||
return false | |||
} | |||
return true | |||
} | |||
// SetPassword updates a user's password hash member, using plaintext | |||
// as the new password. | |||
func (u *User) SetPassword(plaintext string) error { | |||
hash, err := bcrypt.GenerateFromPassword([]byte(plaintext), bcrypt.DefaultCost) | |||
if err != nil { | |||
return err | |||
} | |||
u.Password = hash | |||
return nil | |||
} | |||
type ctxKey int | |||
const key ctxKey = iota | |||
// New Context returns a copy of ctx, appending user to ctx's values. | |||
func NewContext(ctx context.Context, user *User) context.Context { | |||
return context.WithValue(ctx, key, user) | |||
} | |||
// FromContext returns the user object stored in ctx. If ctx does not | |||
// have a User object, the second returns value is false. | |||
func FromContext(ctx context.Context) (*User, bool) { | |||
u, ok := ctx.Value(key).(*User) | |||
return u, ok | |||
} | |||
// MustFromContext is like FromContext, except that it panics if ctx does not | |||
// contain a User object. | |||
func MustFromContext(ctx context.Context) *User { | |||
u, ok := FromContext(ctx) | |||
if !ok { | |||
panic("context has no user") | |||
} | |||
return u | |||
} |
@@ -0,0 +1,27 @@ | |||
-----BEGIN RSA PRIVATE KEY----- | |||
MIIEpAIBAAKCAQEA4Thvifi+Ts69kame0pojgbW50impr0TmXB5LV2gdgMm1pmt6 | |||
4QERVqkCvWfgGAsh0g7/BPLng+bGZZAlNC55t3E08HkdzqptC8wsIRW394cnWW1r | |||
RBKjBUZTOjtA97V/4zLWtgoKGyv0KoigKftMUffTXUJhHBb/ajXou366ZTg+q/b9 | |||
5woN0ZwQJ7nRhv+in7xmdTlg+eWzg8N6gtjDmAwubPIRfMBxfNnd4FQFsrLVrBnG | |||
DR+Wm6vJr5555Kkmr+q5pmDVun4Gy4ciBsTvWPBOgVJ5ygJxOdbVB0zVwvz8/7q/ | |||
jqW2sii8mfYRyi3dXF5ledsubDjMno4oAgY9JwIDAQABAoIBAQCLUhsFkZ9AJvnz | |||
yqbaBsniKmWJ0YYLSybpY0AeEOT3T1AUY7Z+y+dK4YA1ZLWmifRg+i/dgtmeqbqf | |||
Bz1Me1eGF/y0qWe7+Yc9Xg8KZGIKOEwqMNrDIHhCAg/oHNGCqn8zL7bMo4c+6cDA | |||
MwZJEhBTQGg6754c/0j/DdwraCisBabr3pBVOXV+w7cxI/ld0WDSc86zhwr624HA | |||
X8k8/UqFhDv1lt20Ys8YFygYTGSFWykrZPFzefQB54CFkX0By4CmPGp6ZpoU85MB | |||
MKlB+9tIWP5kkz3qIKECL7JmlmvY5CvIL91rq1i8tk3Tp2Bh3jfSsLe+kN1K2vbQ | |||
rhMWz8kRAoGBAPYyfiihRXE+fSzuNu1AjYLLOAIHUCmGeJ5R42Qf6ECm2otCDlp8 | |||
ioJed8AQOOpdBmWUZO9t/S0WiCNiDThoybUlPx78at30HQlVdZj0nrE6T/VNchAn | |||
gCNZ4Msoi/QqungWVTpQZ1nL9EU3iiV1ruXIKyPdYu6PPORjuHItnYXZAoGBAOow | |||
H/qpnzy/0gH3CvYygyG+mf5aDz1pmPcjWs0p8mpPZYV6OIsTLS3SwHSQ9pJY8LOe | |||
DHeL4o5SiyMHR5LOyVvLvefZLkwdj3Jj2l5JErDkdfxHn+mKeONmOU9/1wLaDS6O | |||
ru4c0Mu4UqWOmn/YAZldoEZolC+mK/T80ES/qPr/AoGAAUA+bdxr6uhjYHARbWEv | |||
luOLdE8vNBbP1BYcbqzO1E1EvQJn6kPJvGHYf+xVLbOtTaTUYncPm0QLCwr7gDbg | |||
F4CJ8pFbxabw4tRBVbage8wNDfUHyFc7CnLxdnbNRz9UVTnf0v0HmWg05IkktY4E | |||
hnxe477DOu0VZR+wlzvuGfkCgYBUqQkmiON0BrRY2YIw9pnJPSpWdSBFR0NxNGrC | |||
+IMWQ5Wj50dBn7EZe7LvcOhyh4ycompHXV6NrPF3vE33mKHaeZExm6XNBnKxG7/5 | |||
jdkf8bdleE8rElAZhP766nBEK6fQSOycT/Z7bysRhrf7t478bohea7gGccA6VJrF | |||
/7OK6QKBgQCzRpFxIXG9kYovL6zt4V2FfZSUKn8dC8QDd4+VjUBQ7b8sGMdzOgOg | |||
DfLoBpOZhpBpF5nmtOTlYABFM0fDyO6SN5HA34YU+Z4tYlZaJvKAJLlt1SmJJPxI | |||
89zcEPNrs5Ux1caHCUFL/5rtS+2FA+q/ry2QRDZKbAqgNASCQ+k7og== | |||
-----END RSA PRIVATE KEY----- |
@@ -0,0 +1,9 @@ | |||
-----BEGIN PUBLIC KEY----- | |||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4Thvifi+Ts69kame0poj | |||
gbW50impr0TmXB5LV2gdgMm1pmt64QERVqkCvWfgGAsh0g7/BPLng+bGZZAlNC55 | |||
t3E08HkdzqptC8wsIRW394cnWW1rRBKjBUZTOjtA97V/4zLWtgoKGyv0KoigKftM | |||
UffTXUJhHBb/ajXou366ZTg+q/b95woN0ZwQJ7nRhv+in7xmdTlg+eWzg8N6gtjD | |||
mAwubPIRfMBxfNnd4FQFsrLVrBnGDR+Wm6vJr5555Kkmr+q5pmDVun4Gy4ciBsTv | |||
WPBOgVJ5ygJxOdbVB0zVwvz8/7q/jqW2sii8mfYRyi3dXF5ledsubDjMno4oAgY9 | |||
JwIDAQAB | |||
-----END PUBLIC KEY----- |
@@ -0,0 +1,36 @@ | |||
@font-face { | |||
font-family: 'Material Icons'; | |||
font-style: normal; | |||
font-weight: 400; | |||
src: url(static/fonts/MaterialIcons-Regular.eot); /* For IE6-8 */ | |||
src: local('Material Icons'), | |||
local('MaterialIcons-Regular'), | |||
url(/static/fonts/MaterialIcons-Regular.woff2) format('woff2'), | |||
url(/static/fonts/MaterialIcons-Regular.woff) format('woff'), | |||
url(/static/fonts/MaterialIcons-Regular.ttf) format('truetype'); | |||
} | |||
.material-icons { | |||
font-family: 'Material Icons'; | |||
font-weight: normal; | |||
font-style: normal; | |||
font-size: 24px; /* Preferred icon size */ | |||
display: inline-block; | |||
line-height: 1; | |||
text-transform: none; | |||
letter-spacing: normal; | |||
word-wrap: normal; | |||
white-space: nowrap; | |||
direction: ltr; | |||
/* Support for all WebKit browsers. */ | |||
-webkit-font-smoothing: antialiased; | |||
/* Support for Safari and Chrome. */ | |||
text-rendering: optimizeLegibility; | |||
/* Support for Firefox. */ | |||
-moz-osx-font-smoothing: grayscale; | |||
/* Support for IE. */ | |||
font-feature-settings: 'liga'; | |||
} |
@@ -0,0 +1,6 @@ | |||
window.addEventListener("load", function() { | |||
let form = document.getElementById("add-address-form"); | |||
let btn = form.querySelector("button[type=submit]"); | |||
btn.addEventListener("click", addAddress); | |||
}); |
@@ -0,0 +1,32 @@ | |||
function Alert(type, message, title = "") { | |||
if (type == null || type === "") { | |||
throw new Error("alert type may not be empty") | |||
} else if (message == null || message === "") { | |||
throw new Error("alert message may not be empty") | |||
} | |||
let template = document.getElementById('alert-template').content; | |||
let fragment = document.importNode(template,true); | |||
let alert = fragment.querySelector(".alert"); | |||
alert.classList.add("alert-" + type); | |||
let heading = alert.querySelector(".alert-heading"); | |||
if (title !== "") { | |||
heading.innerText = title; | |||
} else { | |||
switch (type) { | |||
case "danger": | |||
heading.innerText = "An error has occurred!" | |||
break; | |||
case "warning": | |||
heading.innerText = "Warning!" | |||
break; | |||
default: | |||
heading.classList.add("d-none"); | |||
} | |||
} | |||
let msg = alert.querySelector("p"); | |||
msg.innerText = message; | |||
let alerts = document.getElementById("alerts"); | |||
alerts.appendChild(alert); | |||
} |
@@ -0,0 +1,156 @@ | |||
function login(event) { | |||
event.preventDefault(); | |||
let form = document.getElementById("login-form"); | |||
let data = new FormData(form); | |||
event.target.disabled = true; | |||
let spinner = event.target.querySelector(".spinner-border"); | |||
spinner.classList.remove("d-none"); | |||
fetch("/api/login", { | |||
method: "POST", | |||
body: data, | |||
}).then((raw) => raw.json()).then((json) => { | |||
if (json.error != null && json.error !== "") { | |||
throw new Error(json.error); | |||
} | |||
sessionStorage.setItem('username', json.result.toString()) | |||
window.location.replace("/"); | |||
}).catch((reason) => { | |||
new Alert("danger", reason.message); | |||
}).finally(() => { | |||
spinner.classList.add("d-none"); | |||
event.target.disabled = false; | |||
}); | |||
} | |||
function getUsername() { | |||
let username = sessionStorage.getItem('username'); | |||
if (username != null && username !== "") { | |||
return Promise.resolve(username); | |||
} | |||
return fetch("/api/login", { | |||
method: "POST", | |||
body: new FormData(), | |||
}).then((raw) => raw.json()).then((response) => { | |||
if (response.error != null && response.error !== "") { | |||
throw new Error( | |||
"Your session has expired. Please log in again to resolve this issue."); | |||
} | |||
sessionStorage.setItem('username', response.result); | |||
return response.result; | |||
}); | |||
} | |||
function addAddress(event) { | |||
event.preventDefault() | |||
let form = document.getElementById("add-address-form"); | |||
let data = new FormData(form); | |||
getUsername().then((username) => { | |||
event.target.disabled = true; | |||
let spinner = event.target.querySelector(".spinner-border"); | |||
spinner.classList.remove("d-none"); | |||
fetch("/api/user/" + username + "/add-address", { | |||
method: "POST", | |||
body: data, | |||
}).then((raw) => raw.json()).then((response) => { | |||
if (response.error != null && response.error !== "") { | |||
new Alert("danger", response.error); | |||
return; | |||
} | |||
new Alert("success", "Your address has been added successfully!"); | |||
reloadAddresses(); | |||
}).catch((reason) => { | |||
new Alert("danger", reason.message); | |||
}).finally(() => { | |||
spinner.classList.add("d-none"); | |||
event.target.disabled = false; | |||
}); | |||
}); | |||
} | |||
function reloadAddresses() { | |||
getUsername().then((username) => { | |||
fetch("/api/user/" + username + "/get-addresses") | |||
.then((raw) => raw.json()).then((response) => { | |||
if (response.error != null && response.error !== "") { | |||
new Alert("danger", response.error); | |||
return; | |||
} | |||
let addresses = document.querySelector("#addresses tbody"); | |||
addresses.innerHTML = ""; | |||
for (let address of response.result) { | |||
let row = document.createElement("tr"); | |||
row.innerHTML = `<td>${address.street}</td> | |||
<td>${address.zip}</td> | |||
<td>${address.city}</td> | |||
<td>${address.country}</td> | |||
<td>${address.planet}</td>`; | |||
addresses.appendChild(row); | |||
} | |||
}); | |||
}); | |||
} | |||
function addCreditCard(event) { | |||
event.preventDefault(); | |||
let form = document.getElementById("add-credit-card-form"); | |||
let data = new FormData(form); | |||
getUsername().then((username) => { | |||
event.target.disabled = true; | |||
let spinner = event.target.querySelector(".spinner-border"); | |||
spinner.classList.remove("d-none"); | |||
fetch("/api/user/"+ username + "/add-credit-card", { | |||
method: "POST", | |||
body: data, | |||
}).then((raw) => raw.json()).then((response) => { | |||
if (response.error != null && response.error !== "") { | |||
new Alert("danger", response.error); | |||
return; | |||
} | |||
new Alert("success", "Your credit card has been added successfully!"); | |||
reloadCreditCards(); | |||
}).catch((reason) => { | |||
new Alert("danger", reason.message); | |||
}).finally(() => { | |||
spinner.classList.add("d-none"); | |||
event.target.disabled = false; | |||
}); | |||
}); | |||
} | |||
function reloadCreditCards() { | |||
getUsername().then((username) => { | |||
fetch("/api/user/" + username + "/get-credit-cards") | |||
.then((raw) => raw.json()).then((response) => { | |||
if (response.error != null && response.error !== "") { | |||
new Alert("danger", response.error); | |||
return; | |||
} | |||
let creditCards = document.querySelector("#credit-cards tbody"); | |||
creditCards.innerHTML = ""; | |||
for (let card of response.result) { | |||
let row = document.createElement("tr"); | |||
row.innerHTML = ` | |||
<td>MarsCard</td> | |||
<td>${card.number}</td> | |||
<td>TODO</td>`; | |||
creditCards.appendChild(row); | |||
} | |||
}); | |||
}); | |||
} |
@@ -0,0 +1,3 @@ | |||
window.addEventListener("load", (event) => { | |||
AOS.init(); | |||
}) |