Skip to content

Commit 735bf5a

Browse files
committed
Tracker service (alpha version) + checker (only 'check')
1 parent 4f750fd commit 735bf5a

24 files changed

+982
-0
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.idea
2+
__pycache__
3+
.pytest_cache
4+
.venv

checkers/tracker/checker.py

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
#!/usr/bin/env python3
2+
import msgpack
3+
import secrets
4+
import socket
5+
import sys
6+
7+
PORT = 9090
8+
BUFFER_SIZE = 1024
9+
10+
SOCK_DCCP = 6
11+
IPROTO_DCCP = 33
12+
SOL_DCCP = 269
13+
DCCP_SOCKOPT_SERVICE = 2
14+
15+
16+
class Request:
17+
USER_REGISTER = 0x00
18+
USER_LOGIN = 0x01
19+
USER_LOGOUT = 0x02
20+
TRACKER_LIST = 0x10
21+
TRACKER_ADD = 0x11
22+
TRACKER_DELETE = 0x12
23+
POINT_ADD = 0x20
24+
TRACK_LIST = 0x30
25+
TRACK_GET = 0x31
26+
TRACK_DELETE = 0x32
27+
TRACK_REQUEST_SHARE = 0x33
28+
TRACK_SHARE = 0x34
29+
30+
31+
class Response:
32+
OK = 0x00
33+
BAD_REQUEST = 0x01
34+
FORBIDDEN = 0x02
35+
NOT_FOUND = 0x03
36+
INTERNAL_ERROR = 0x04
37+
38+
39+
class Client:
40+
def __init__(self, host, port=PORT):
41+
sock = socket.socket(socket.AF_INET, SOCK_DCCP, IPROTO_DCCP)
42+
sock.setsockopt(SOL_DCCP, DCCP_SOCKOPT_SERVICE, True)
43+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
44+
sock.connect((host, port))
45+
self.sock = sock
46+
47+
def query(self, *args, expect=None):
48+
response = self.query_raw(args)
49+
if expect is not None:
50+
assert response[0] == expect
51+
response = response[1:]
52+
return response[0] if len(response) == 1 else response
53+
54+
def query_raw(self, obj):
55+
self.sock.send(msgpack.packb(obj))
56+
return msgpack.unpackb(self.sock.recv(BUFFER_SIZE), raw=False)
57+
58+
59+
class ExitCode:
60+
OK = 101
61+
CORRUPT = 102
62+
MUMBLE = 103
63+
FAIL = 104
64+
INTERNAL_ERROR = 110
65+
66+
67+
def check(ip):
68+
password = secrets.token_urlsafe(16)
69+
70+
try:
71+
client = Client(ip)
72+
73+
user_id = client.query(Request.USER_REGISTER, password, expect=Response.OK)
74+
secret = client.query(Request.USER_LOGIN, user_id, password, expect=Response.OK)
75+
client.query(Request.USER_LOGOUT, secret, expect=Response.OK)
76+
except AssertionError:
77+
sys.exit(ExitCode.MUMBLE)
78+
except ConnectionError:
79+
sys.exit(ExitCode.FAIL)
80+
81+
sys.exit(ExitCode.OK)
82+
83+
84+
def put(id, flag):
85+
pass
86+
87+
88+
def get(id, flag):
89+
pass
90+
91+
92+
def main():
93+
if len(sys.argv) < 3:
94+
print("Usage: {} MODE IP".format(sys.argv[0]))
95+
sys.exit(ExitCode.INTERNAL_ERROR)
96+
97+
mode = sys.argv[1]
98+
args = sys.argv[2:]
99+
if mode == "check":
100+
check(*args)
101+
elif mode == "put":
102+
put(*args)
103+
elif mode == "get":
104+
put(*args)
105+
else:
106+
print("Error: unknown MODE.")
107+
sys.exit(ExitCode.INTERNAL_ERROR)
108+
109+
110+
if __name__ == "__main__":
111+
main()

services/tracker/app/__init__.py

Whitespace-only changes.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import app.api.point
2+
import app.api.track
3+
import app.api.tracker
4+
import app.api.user

services/tracker/app/api/point.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import logging
2+
import time
3+
4+
from app.common import handler
5+
from app.enums import Response, Request, TrackAccess
6+
7+
log = logging.getLogger()
8+
9+
NEW_TRACK_GAP_SECONDS = 2
10+
11+
12+
# FIXME: copy-paste?
13+
async def auth(db, token):
14+
row = await db.fetchrow("SELECT id, user_id FROM tracker WHERE token=$1", token)
15+
if row is None:
16+
return None, None
17+
return row["id"], row["user_id"]
18+
19+
20+
@handler(Request.POINT_ADD)
21+
async def add(db, token, latitude, longitude, meta):
22+
try:
23+
latitude = float(latitude)
24+
longitude = float(longitude)
25+
except ValueError:
26+
return Response.BAD_REQUEST
27+
28+
tracker_id, user_id = await auth(db, token)
29+
if tracker_id is None:
30+
return Response.FORBIDDEN
31+
32+
now = time.time()
33+
34+
prev = await db.fetchrow("SELECT track_id FROM point WHERE tracker_id=$1 AND timestamp >= $2 "
35+
"ORDER BY timestamp DESC LIMIT 1", tracker_id, now - NEW_TRACK_GAP_SECONDS)
36+
if prev is None:
37+
track = await db.fetchrow("INSERT INTO track(user_id, started_at, access) VALUES($1, $2, $3) RETURNING id",
38+
user_id, now, TrackAccess.PRIVATE)
39+
track_id = track["id"]
40+
log.debug("Created new track: %d.", track_id)
41+
else:
42+
track_id = prev["track_id"]
43+
log.debug("Using old track: %d.", track_id)
44+
45+
await db.execute("INSERT INTO point(latitude, longitude, tracker_id, track_id, meta, timestamp) "
46+
"VALUES($1, $2, $3, $4, $5, $6)",
47+
latitude, longitude, tracker_id, track_id, meta, time.time())
48+
log.debug("Added new point.")
49+
50+
return Response.OK, track_id

services/tracker/app/api/track.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import logging
2+
3+
from app.common import handler
4+
from app.enums import Response, Request, TrackAccess
5+
6+
log = logging.getLogger()
7+
8+
9+
# FIXME: copy-paste?
10+
async def auth(db, secret):
11+
row = await db.fetchrow("SELECT * FROM secret WHERE value=$1", secret)
12+
if row is None:
13+
return None
14+
return row["user_id"]
15+
16+
17+
@handler(Request.TRACK_LIST)
18+
async def track_list(db, secret):
19+
user_id = await auth(db, secret)
20+
if user_id is None:
21+
return Response.FORBIDDEN
22+
23+
rows = await db.fetch("SELECT * FROM track WHERE user_id=$1", user_id)
24+
tracks = [(r["id"], r["started_at"], r["access"]) for r in rows]
25+
26+
return Response.OK, tracks
27+
28+
29+
@handler(Request.TRACK_GET)
30+
async def track_get(db, secret, track_id):
31+
user_id = await auth(db, secret)
32+
if user_id is None:
33+
return Response.FORBIDDEN
34+
35+
row = await db.fetchrow("SELECT * FROM track WHERE id=$1", track_id)
36+
if row is None:
37+
return Response.NOT_FOUND
38+
39+
access = row["access"]
40+
if access is TrackAccess.PRIVATE or access is TrackAccess.PENDING:
41+
if user_id != row["user_id"]:
42+
log.warning("Get track is forbidden for user %d (owner: %d)", user_id, row["user_id"])
43+
return Response.FORBIDDEN
44+
45+
if TrackAccess.GROUP_ACCESS_MIN <= access <= TrackAccess.GROUP_ACCESS_MAX:
46+
# FIXME: check access == user_group
47+
return Response.FORBIDDEN
48+
49+
rows = await db.fetch("SELECT timestamp, latitude, longitude, meta FROM point "
50+
"WHERE track_id=$1 ORDER BY id", track_id)
51+
points = [(r["timestamp"], r["latitude"], r["longitude"], r["meta"]) for r in rows]
52+
return Response.OK, points
53+
54+
55+
@handler(Request.TRACK_DELETE)
56+
async def track_delete(db, secret, track_id):
57+
user_id = await auth(db, secret)
58+
if user_id is None:
59+
return Response.FORBIDDEN
60+
61+
row = await db.fetchrow("SELECT * FROM track WHERE id=$1 AND user_id=$2", track_id, user_id)
62+
if row is None:
63+
return Response.NOT_FOUND
64+
65+
await db.execute("DELETE FROM track WHERE id=$1 AND user_id=$2", track_id, user_id)
66+
await db.execute("DELETE FROM point WHERE track_id=$1", track_id)
67+
68+
return Response.OK
69+
70+
71+
@handler(Request.TRACK_REQUEST_SHARE)
72+
async def track_request_share(db, secret, track_id):
73+
user_id = await auth(db, secret)
74+
if user_id is None:
75+
return Response.FORBIDDEN
76+
77+
row = await db.fetchrow("SELECT * FROM track WHERE id=$1", track_id)
78+
if row is None:
79+
return Response.NOT_FOUND
80+
81+
if row["access"] is not TrackAccess.PRIVATE:
82+
return Response.FORBIDDEN
83+
84+
await db.fetchrow("UPDATE track SET access=$1 WHERE id=$2", TrackAccess.PENDING, track_id)
85+
return Response.OK
86+
87+
88+
@handler(Request.TRACK_SHARE)
89+
async def track_share(db, secret, track_id):
90+
user_id = await auth(db, secret)
91+
if user_id is None:
92+
return Response.FORBIDDEN
93+
94+
row = await db.fetchrow("SELECT * FROM track WHERE id=$1 AND user_id=$2", track_id, user_id)
95+
if row is None:
96+
return Response.NOT_FOUND
97+
98+
if row["access"] is not TrackAccess.PENDING:
99+
return Response.BAD_REQUEST
100+
101+
await db.fetchrow("UPDATE track SET access=$1 WHERE id=$2", TrackAccess.PUBLIC, track_id)
102+
return Response.OK
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import logging
2+
3+
from app.common import handler, generate_token
4+
from app.enums import Response, Request
5+
6+
MAX_TRACKERS = 8
7+
8+
log = logging.getLogger()
9+
10+
11+
# FIXME: copy-paste?
12+
async def auth(db, secret):
13+
row = await db.fetchrow("SELECT * FROM secret WHERE value=$1", secret)
14+
if row is None:
15+
return None
16+
return row["user_id"]
17+
18+
19+
@handler(Request.TRACKER_LIST)
20+
async def list(db, secret):
21+
user_id = await auth(db, secret)
22+
if user_id is None:
23+
return Response.FORBIDDEN
24+
25+
trackers = await db.fetch("SELECT id, name FROM tracker WHERE user_id=$1", user_id)
26+
return Response.OK, [(t["id"], t["name"]) for t in trackers]
27+
28+
29+
@handler(Request.TRACKER_ADD)
30+
async def add(db, secret, name):
31+
user_id = await auth(db, secret)
32+
if user_id is None:
33+
return Response.FORBIDDEN
34+
35+
row = await db.fetchrow("SELECT COUNT(*) AS count FROM tracker WHERE user_id=$1", user_id)
36+
if row["count"] >= MAX_TRACKERS:
37+
return Response.BAD_REQUEST, "Too many trackers."
38+
39+
token = generate_token()
40+
await db.execute("INSERT INTO tracker(user_id, name, token) VALUES($1, $2, $3)",
41+
user_id, name, token)
42+
log.debug("Added new tracker.")
43+
44+
return Response.OK, token
45+
46+
47+
@handler(Request.TRACKER_DELETE)
48+
async def delete(db, secret, id):
49+
user_id = await auth(db, secret)
50+
if user_id is None:
51+
return Response.FORBIDDEN
52+
53+
res = await db.execute("DELETE FROM tracker WHERE id=$1 AND user_id=$2", id, user_id)
54+
return Response.OK if res == "DELETE 1" else Response.NOT_FOUND

services/tracker/app/api/user.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import logging
2+
import time
3+
import uuid
4+
5+
from app.common import hash_password, is_uuid, generate_secret, connect_db, handler
6+
from app.enums import Response, Request
7+
8+
MIN_PASSWORD_LEN = 8
9+
MAX_PASSWORD_LEN = 64
10+
MAX_ACTIVE_SESSIONS = 3
11+
12+
log = logging.getLogger()
13+
14+
15+
@handler(Request.USER_REGISTER)
16+
async def register(db, password):
17+
if len(password) < MIN_PASSWORD_LEN:
18+
return Response.BAD_REQUEST, "Password is too short."
19+
if len(password) > MAX_PASSWORD_LEN:
20+
return Response.BAD_REQUEST, "Password is too long."
21+
22+
# FIXME: check proof of work
23+
24+
user_id = uuid.uuid4()
25+
await db.execute('INSERT INTO "user"(id, hash, timestamp) VALUES($1, $2, $3)',
26+
user_id, hash_password(password), time.time())
27+
28+
return Response.OK, str(user_id)
29+
30+
31+
@handler(Request.USER_LOGIN)
32+
async def login(db, user_id, password):
33+
if not is_uuid(user_id):
34+
return Response.BAD_REQUEST
35+
36+
user = await db.fetchrow('SELECT * FROM "user" WHERE id=$1 AND hash=$2',
37+
user_id, hash_password(password))
38+
if user is None:
39+
return Response.FORBIDDEN
40+
41+
secrets = await db.fetch("SELECT * FROM secret WHERE user_id=$1", user_id)
42+
if len(secrets) >= MAX_ACTIVE_SESSIONS:
43+
log.warning("Too many active secrets (%d), will remove old.", len(secrets))
44+
secrets_to_delete = sorted(secrets, key=lambda s: s["order"], reverse=True)[MAX_ACTIVE_SESSIONS-1:]
45+
for secret in secrets_to_delete:
46+
log.debug("Removing secret %s", secret["value"])
47+
await db.execute("DELETE FROM secret WHERE value=$1", secret["value"])
48+
49+
secret = generate_secret()
50+
await db.execute("INSERT INTO secret(value, user_id, timestamp) VALUES($1, $2, $3)",
51+
secret, user_id, time.time())
52+
log.debug("Login successful.")
53+
return Response.OK, secret
54+
55+
56+
@handler(Request.USER_LOGOUT)
57+
async def logout(db, secret):
58+
res = await db.execute('DELETE FROM secret WHERE value=$1', secret)
59+
return Response.OK if res == "DELETE 1" else Response.NOT_FOUND

0 commit comments

Comments
 (0)