Browse Source

Init repo

master
xyrex 3 years ago
commit
ef83e0f263
100 changed files with 4639 additions and 0 deletions
  1. BIN
      cartography/cartography
  2. +0
    -0
      greenhouses/.#.#machine.containera00f809be94c1494.lck
  3. +4
    -0
      greenhouses/container/etc/cron.d/water
  4. +5
    -0
      greenhouses/container/etc/dbus-1/system-services/net.faustctf.SuDoD.service
  5. +23
    -0
      greenhouses/container/etc/dbus-1/system.d/net.faustctf.SuDoD.conf
  6. +4
    -0
      greenhouses/container/etc/hosts
  7. +0
    -0
      greenhouses/container/etc/network/if-down.d/.gitkeep
  8. +0
    -0
      greenhouses/container/etc/network/if-post-down.d/.gitkeep
  9. +0
    -0
      greenhouses/container/etc/network/if-pre-up.d/.gitkeep
  10. +0
    -0
      greenhouses/container/etc/network/if-up.d/.gitkeep
  11. +3
    -0
      greenhouses/container/etc/network/interfaces
  12. +7
    -0
      greenhouses/container/etc/os-release
  13. +22
    -0
      greenhouses/container/etc/polkit-1/rules.d/sudoers.rules
  14. +5
    -0
      greenhouses/container/etc/ssh/sshd_config
  15. +3
    -0
      greenhouses/container/etc/sudoers
  16. +1
    -0
      greenhouses/container/etc/sudoers.d/register
  17. +11
    -0
      greenhouses/container/etc/systemd/system/gate-user.service
  18. +14
    -0
      greenhouses/container/etc/systemd/system/gh-setup.service
  19. +14
    -0
      greenhouses/container/etc/systemd/system/multi-user.target.wants/cron.service
  20. +11
    -0
      greenhouses/container/etc/systemd/system/multi-user.target.wants/gate-user.service
  21. +14
    -0
      greenhouses/container/etc/systemd/system/multi-user.target.wants/gh-setup.service
  22. +15
    -0
      greenhouses/container/etc/systemd/system/multi-user.target.wants/ssh-setup.service
  23. +22
    -0
      greenhouses/container/etc/systemd/system/multi-user.target.wants/ssh.service
  24. +15
    -0
      greenhouses/container/etc/systemd/system/ssh-setup.service
  25. +6
    -0
      greenhouses/container/etc/systemd/system/sudod.service
  26. +1
    -0
      greenhouses/container/etc/sysusers.d/greenhouses
  27. +2
    -0
      greenhouses/container/opt/bin/ghshow
  28. +2
    -0
      greenhouses/container/opt/bin/ghsow
  29. +21
    -0
      greenhouses/container/opt/bin/register.sh
  30. +84
    -0
      greenhouses/container/opt/bin/sudoc.py
  31. +238
    -0
      greenhouses/container/opt/bin/sudod.py
  32. +4
    -0
      greenhouses/container/opt/gh/db.py
  33. +52
    -0
      greenhouses/container/opt/gh/show.py
  34. +11
    -0
      greenhouses/container/opt/gh/sow.py
  35. +4
    -0
      greenhouses/container/opt/gh/water.py
  36. +1
    -0
      greenhouses/container/root/.ssh/authorized_keys
  37. +20
    -0
      greenhouses/container/usr/share/polkit-1/actions/net.faustctf.SuDoD.policy
  38. +12
    -0
      ipps/README.md
  39. +109
    -0
      ipps/cmd/ipps/main.go
  40. +20
    -0
      ipps/config.toml
  41. +20
    -0
      ipps/configs/defaults.toml
  42. +1
    -0
      ipps/go.mod
  43. +647
    -0
      ipps/internal/grpc/ipps.pb.go
  44. +49
    -0
      ipps/internal/grpc/ipps.proto
  45. +158
    -0
      ipps/internal/grpc/jwt.go
  46. +173
    -0
      ipps/internal/grpc/server.go
  47. +542
    -0
      ipps/internal/http/handler.go
  48. +55
    -0
      ipps/internal/http/middleware.go
  49. +111
    -0
      ipps/internal/http/server.go
  50. +219
    -0
      ipps/internal/json/handler.go
  51. +28
    -0
      ipps/internal/json/json.go
  52. +30
    -0
      ipps/internal/json/middleware.go
  53. +113
    -0
      ipps/internal/session/session.go
  54. BIN
      ipps/ipps
  55. +80
    -0
      ipps/pkg/address/address.go
  56. +25
    -0
      ipps/pkg/address/storage.go
  57. +53
    -0
      ipps/pkg/credit/card.go
  58. +3
    -0
      ipps/pkg/credit/doc.go
  59. +47
    -0
      ipps/pkg/credit/storage.go
  60. +52
    -0
      ipps/pkg/feedback/feedback.go
  61. +18
    -0
      ipps/pkg/feedback/storage.go
  62. +56
    -0
      ipps/pkg/parcel/parcel.go
  63. +33
    -0
      ipps/pkg/parcel/storage.go
  64. +135
    -0
      ipps/pkg/postgres/address_storage.go
  65. +120
    -0
      ipps/pkg/postgres/credit_storage.go
  66. +79
    -0
      ipps/pkg/postgres/event_storage.go
  67. +133
    -0
      ipps/pkg/postgres/feedback_storage.go
  68. +105
    -0
      ipps/pkg/postgres/parcel_storage.go
  69. +58
    -0
      ipps/pkg/postgres/postgres.go
  70. +164
    -0
      ipps/pkg/postgres/user_storage.go
  71. +110
    -0
      ipps/pkg/user/forms.go
  72. +58
    -0
      ipps/pkg/user/storage.go
  73. +95
    -0
      ipps/pkg/user/user.go
  74. +27
    -0
      ipps/privkey.pem
  75. +9
    -0
      ipps/pubkey.pem
  76. +0
    -0
      ipps/setup
  77. +1
    -0
      ipps/web/static/css/aos.css
  78. +7
    -0
      ipps/web/static/css/bootstrap.min.css
  79. +1
    -0
      ipps/web/static/css/bootstrap.min.css.map
  80. +36
    -0
      ipps/web/static/css/icons.css
  81. BIN
      ipps/web/static/fonts/MaterialIcons-Regular.eot
  82. BIN
      ipps/web/static/fonts/MaterialIcons-Regular.ttf
  83. BIN
      ipps/web/static/fonts/MaterialIcons-Regular.woff
  84. BIN
      ipps/web/static/fonts/MaterialIcons-Regular.woff2
  85. BIN
      ipps/web/static/img/cargo.jpg
  86. BIN
      ipps/web/static/img/eo1.jpg
  87. BIN
      ipps/web/static/img/eo2.jpg
  88. BIN
      ipps/web/static/img/logo.png
  89. BIN
      ipps/web/static/img/logo_small.png
  90. BIN
      ipps/web/static/img/mo1.jpg
  91. BIN
      ipps/web/static/img/mo2.jpg
  92. +6
    -0
      ipps/web/static/js/address.js
  93. +32
    -0
      ipps/web/static/js/alert.js
  94. +1
    -0
      ipps/web/static/js/aos.js
  95. +156
    -0
      ipps/web/static/js/api.js
  96. +7
    -0
      ipps/web/static/js/bootstrap.min.js
  97. +1
    -0
      ipps/web/static/js/bootstrap.min.js.map
  98. +3
    -0
      ipps/web/static/js/init.js
  99. +2
    -0
      ipps/web/static/js/jquery-3.5.1.min.js
  100. +0
    -0
      ipps/web/static/js/jquery-3.5.1.min.map

BIN
cartography/cartography View File


+ 0
- 0
greenhouses/.#.#machine.containera00f809be94c1494.lck View File


+ 4
- 0
greenhouses/container/etc/cron.d/water View File

@@ -0,0 +1,4 @@
SHELL=/bin/bash
PATH=/sbin:/bin:/usr/sbin:/usr/bin
*/2 * * * * greenhouses python3 /opt/gh/water.py


+ 5
- 0
greenhouses/container/etc/dbus-1/system-services/net.faustctf.SuDoD.service View File

@@ -0,0 +1,5 @@
[D-BUS Service]
Name=net.faustctf.SuDoD
Exec=/bin/false
User=root
SystemdService=sudod.service

+ 23
- 0
greenhouses/container/etc/dbus-1/system.d/net.faustctf.SuDoD.conf View File

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

+ 4
- 0
greenhouses/container/etc/hosts View File

@@ -0,0 +1,4 @@
127.0.0.1 localhost
::1 localhost

fd4f:7367:c144:c7f::1 vulnbox

+ 0
- 0
greenhouses/container/etc/network/if-down.d/.gitkeep View File


+ 0
- 0
greenhouses/container/etc/network/if-post-down.d/.gitkeep View File


+ 0
- 0
greenhouses/container/etc/network/if-pre-up.d/.gitkeep View File


+ 0
- 0
greenhouses/container/etc/network/if-up.d/.gitkeep View File


+ 3
- 0
greenhouses/container/etc/network/interfaces View File

@@ -0,0 +1,3 @@
auto host
iface host inet6 static
address fd4f:7367:c144:c7f::2/64

+ 7
- 0
greenhouses/container/etc/os-release View File

@@ -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"

+ 22
- 0
greenhouses/container/etc/polkit-1/rules.d/sudoers.rules View File

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

+ 5
- 0
greenhouses/container/etc/ssh/sshd_config View File

@@ -0,0 +1,5 @@
Match user gate
PermitEmptyPasswords yes
ForceCommand /opt/bin/sudoc.py /opt/bin/register.sh
PermitTTY no
DisableForwarding=yes

+ 3
- 0
greenhouses/container/etc/sudoers View File

@@ -0,0 +1,3 @@
root ALL=(ALL) ALL
#includedir /etc/sudoers.d


+ 1
- 0
greenhouses/container/etc/sudoers.d/register View File

@@ -0,0 +1 @@
gate ALL = (root) NOPASSWD: /register.sh

+ 11
- 0
greenhouses/container/etc/systemd/system/gate-user.service View File

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

+ 14
- 0
greenhouses/container/etc/systemd/system/gh-setup.service View File

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



+ 14
- 0
greenhouses/container/etc/systemd/system/multi-user.target.wants/cron.service View File

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

+ 11
- 0
greenhouses/container/etc/systemd/system/multi-user.target.wants/gate-user.service View File

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

+ 14
- 0
greenhouses/container/etc/systemd/system/multi-user.target.wants/gh-setup.service View File

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



+ 15
- 0
greenhouses/container/etc/systemd/system/multi-user.target.wants/ssh-setup.service View File

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


+ 22
- 0
greenhouses/container/etc/systemd/system/multi-user.target.wants/ssh.service View File

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

+ 15
- 0
greenhouses/container/etc/systemd/system/ssh-setup.service View File

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


+ 6
- 0
greenhouses/container/etc/systemd/system/sudod.service View File

@@ -0,0 +1,6 @@
[Unit]
Description=SuDo as a Service

[Service]
BusName=org.freedesktop.hostname1
ExecStart=/opt/bin/sudod.py

+ 1
- 0
greenhouses/container/etc/sysusers.d/greenhouses View File

@@ -0,0 +1 @@
u gate - "Gatekeeper for the Greenhouses"

+ 2
- 0
greenhouses/container/opt/bin/ghshow View File

@@ -0,0 +1,2 @@
#!/bin/sh
exec /opt/bin/sudoc.py -u greenhouses /opt/gh/show.py

+ 2
- 0
greenhouses/container/opt/bin/ghsow View File

@@ -0,0 +1,2 @@
#!/bin/sh
exec /opt/bin/sudoc.py -u greenhouses /opt/gh/sow.py

+ 21
- 0
greenhouses/container/opt/bin/register.sh View File

@@ -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"

+ 84
- 0
greenhouses/container/opt/bin/sudoc.py View File

@@ -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()

+ 238
- 0
greenhouses/container/opt/bin/sudod.py View File

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


+ 4
- 0
greenhouses/container/opt/gh/db.py View File

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

+ 52
- 0
greenhouses/container/opt/gh/show.py View File

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


+ 11
- 0
greenhouses/container/opt/gh/sow.py View File

@@ -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()


+ 4
- 0
greenhouses/container/opt/gh/water.py View File

@@ -0,0 +1,4 @@
from db import c, conn

c.execute("update seeds set generation = generation+1")
conn.commit()

+ 1
- 0
greenhouses/container/root/.ssh/authorized_keys View File

@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEpawv6A0tpdlhGIezISVGgrKspHS2nlHv1WGuN5iW33 greenhouses-checker@2020.faustctf.net

+ 20
- 0
greenhouses/container/usr/share/polkit-1/actions/net.faustctf.SuDoD.policy View File

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

+ 12
- 0
ipps/README.md View File

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

+ 109
- 0
ipps/cmd/ipps/main.go View File

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

+ 20
- 0
ipps/config.toml View File

@@ -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"

+ 20
- 0
ipps/configs/defaults.toml View File

@@ -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"

+ 1
- 0
ipps/go.mod View File

@@ -0,0 +1 @@
module gitlab.cs.fau.de/faust/faustctf-2020/ipps

+ 647
- 0
ipps/internal/grpc/ipps.pb.go View File

@@ -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",
}

+ 49
- 0
ipps/internal/grpc/ipps.proto View File

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

+ 158
- 0
ipps/internal/grpc/jwt.go View File

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

+ 173
- 0
ipps/internal/grpc/server.go View File

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

+ 542
- 0
ipps/internal/http/handler.go View File

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

+ 55
- 0
ipps/internal/http/middleware.go View File

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

+ 111
- 0
ipps/internal/http/server.go View File

@@ -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", &registerHandler{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
}

+ 219
- 0
ipps/internal/json/handler.go View File

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

+ 28
- 0
ipps/internal/json/json.go View File

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

+ 30
- 0
ipps/internal/json/middleware.go View File

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

+ 113
- 0
ipps/internal/session/session.go View File

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

BIN
ipps/ipps View File


+ 80
- 0
ipps/pkg/address/address.go View File

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

+ 25
- 0
ipps/pkg/address/storage.go View File

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

+ 53
- 0
ipps/pkg/credit/card.go View File

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

+ 3
- 0
ipps/pkg/credit/doc.go View File

@@ -0,0 +1,3 @@
// Package credit contains interfaces and data structures for
// managing customer's credit card data.
package credit

+ 47
- 0
ipps/pkg/credit/storage.go View File

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

+ 52
- 0
ipps/pkg/feedback/feedback.go View File

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

+ 18
- 0
ipps/pkg/feedback/storage.go View File

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

+ 56
- 0
ipps/pkg/parcel/parcel.go View File

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

+ 33
- 0
ipps/pkg/parcel/storage.go View File

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

+ 135
- 0
ipps/pkg/postgres/address_storage.go View File

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

+ 120
- 0
ipps/pkg/postgres/credit_storage.go View File

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

+ 79
- 0
ipps/pkg/postgres/event_storage.go View File

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

+ 133
- 0
ipps/pkg/postgres/feedback_storage.go View File

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

+ 105
- 0
ipps/pkg/postgres/parcel_storage.go View File

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

+ 58
- 0
ipps/pkg/postgres/postgres.go View File

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

+ 164
- 0
ipps/pkg/postgres/user_storage.go View File

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

+ 110
- 0
ipps/pkg/user/forms.go View File

@@ -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 := &registrationForm{}
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
}

+ 58
- 0
ipps/pkg/user/storage.go View File

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

+ 95
- 0
ipps/pkg/user/user.go View File

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

+ 27
- 0
ipps/privkey.pem View File

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

+ 9
- 0
ipps/pubkey.pem View File

@@ -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
ipps/setup View File


+ 1
- 0
ipps/web/static/css/aos.css
File diff suppressed because it is too large
View File


+ 7
- 0
ipps/web/static/css/bootstrap.min.css
File diff suppressed because it is too large
View File


+ 1
- 0
ipps/web/static/css/bootstrap.min.css.map
File diff suppressed because it is too large
View File


+ 36
- 0
ipps/web/static/css/icons.css View File

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

BIN
ipps/web/static/fonts/MaterialIcons-Regular.eot View File


BIN
ipps/web/static/fonts/MaterialIcons-Regular.ttf View File


BIN
ipps/web/static/fonts/MaterialIcons-Regular.woff View File


BIN
ipps/web/static/fonts/MaterialIcons-Regular.woff2 View File


BIN
ipps/web/static/img/cargo.jpg View File

Before After
Width: 2643  |  Height: 1280  |  Size: 4.2MB

BIN
ipps/web/static/img/eo1.jpg View File

Before After
Width: 2423  |  Height: 1200  |  Size: 312KB

BIN
ipps/web/static/img/eo2.jpg View File

Before After
Width: 4256  |  Height: 2832  |  Size: 869KB

BIN
ipps/web/static/img/logo.png View File

Before After
Width: 500  |  Height: 592  |  Size: 865KB

BIN
ipps/web/static/img/logo_small.png View File

Before After
Width: 34  |  Height: 40  |  Size: 8.5KB

BIN
ipps/web/static/img/mo1.jpg View File

Before After
Width: 1280  |  Height: 720  |  Size: 73KB

BIN
ipps/web/static/img/mo2.jpg View File

Before After
Width: 1024  |  Height: 576  |  Size: 54KB

+ 6
- 0
ipps/web/static/js/address.js View File

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

+ 32
- 0
ipps/web/static/js/alert.js View File

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

+ 1
- 0
ipps/web/static/js/aos.js
File diff suppressed because it is too large
View File


+ 156
- 0
ipps/web/static/js/api.js View File

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

+ 7
- 0
ipps/web/static/js/bootstrap.min.js
File diff suppressed because it is too large
View File


+ 1
- 0
ipps/web/static/js/bootstrap.min.js.map
File diff suppressed because it is too large
View File


+ 3
- 0
ipps/web/static/js/init.js View File

@@ -0,0 +1,3 @@
window.addEventListener("load", (event) => {
AOS.init();
})

+ 2
- 0
ipps/web/static/js/jquery-3.5.1.min.js
File diff suppressed because it is too large
View File


+ 0
- 0
ipps/web/static/js/jquery-3.5.1.min.map View File


Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save