Add DNS/Flask servers, logging, and stunnel config

Introduces a DNS server (dnsserver.py) that redirects Nintendo domains, a Flask server (server.py) for serving Gen4/Gen5 Pokémon event files and logging unknown requests, and supporting batch scripts for starting/stopping services. Adds stunnel configuration and certificates for SSL proxying, as well as initial requirements and log handling. Removes obsolete server.log and restructures event file serving and logging for better maintainability.
This commit is contained in:
2025-09-20 01:56:32 -04:00
parent bc1edc6fdf
commit 87d4b4ba78
30 changed files with 270 additions and 42 deletions
View File
@@ -0,0 +1,8 @@
Headers:
Host: 192.168.0.30:8080
Upgrade-Insecure-Requests: 1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 18_7_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/140.0.7339.122 Mobile/15E148 Safari/604.1
Accept-Language: en-US,en;q=0.9
Accept-Encoding: gzip, deflate
Connection: keep-alive
@@ -0,0 +1,6 @@
Headers:
Host: 192.168.0.30:8080
Connection: keep-alive
User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 18_7_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/140.0.7339.122 Mobile/15E148 Safari/604.1
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
@@ -0,0 +1,6 @@
Headers:
Host: 192.168.0.30:8080
Connection: keep-alive
User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 18_7_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/140.0.7339.122 Mobile/15E148 Safari/604.1
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
@@ -0,0 +1,6 @@
Headers:
Host: 192.168.0.30:8080
Connection: keep-alive
User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 18_7_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/140.0.7339.122 Mobile/15E148 Safari/604.1
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
@@ -0,0 +1,6 @@
Headers:
Host: 192.168.0.30:8080
Connection: keep-alive
User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 18_7_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/140.0.7339.122 Mobile/15E148 Safari/604.1
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
@@ -0,0 +1,6 @@
Headers:
Host: 192.168.0.30:8080
Connection: keep-alive
User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 18_7_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/140.0.7339.122 Mobile/15E148 Safari/604.1
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
@@ -0,0 +1,8 @@
Headers:
Host: 192.168.0.30:8080
Upgrade-Insecure-Requests: 1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 18_7_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/140.0.7339.122 Mobile/15E148 Safari/604.1
Accept-Language: en-US,en;q=0.9
Accept-Encoding: gzip, deflate
Connection: keep-alive
@@ -0,0 +1,8 @@
Headers:
Host: 192.168.0.30:8080
Upgrade-Insecure-Requests: 1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 18_7_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/140.0.7339.122 Mobile/15E148 Safari/604.1
Accept-Language: en-US,en;q=0.9
Accept-Encoding: gzip, deflate
Connection: keep-alive
+43
View File
@@ -0,0 +1,43 @@
from dnslib import DNSRecord, QTYPE, RR, A
from dnslib.server import DNSServer, BaseResolver
import socket
import datetime
# Your server's LAN IP
REDIRECT_IP = "192.168.0.30"
LOG_FILE = "dns_queries.log"
def log_query(qname, qtype, action, target):
"""Log queries to a file and print them to console"""
ts = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
line = f"[{ts}] {qname} ({qtype}) -> {action} {target}\n"
print(line.strip())
with open(LOG_FILE, "a", encoding="utf-8") as f:
f.write(line)
class NintendoResolver(BaseResolver):
def resolve(self, request, handler):
reply = request.reply()
qname = str(request.q.qname)
qtype = QTYPE[request.q.qtype]
# Redirect Nintendo domains to our Flask server
if "nintendowifi.net" in qname.lower() or "nintendo.net" in qname.lower():
reply.add_answer(RR(request.q.qname, QTYPE.A, rdata=A(REDIRECT_IP), ttl=60))
log_query(qname, qtype, "REDIRECT", REDIRECT_IP)
else:
# Forward everything else
try:
ip = socket.gethostbyname(qname[:-1]) # strip trailing dot
reply.add_answer(RR(request.q.qname, QTYPE.A, rdata=A(ip), ttl=60))
log_query(qname, qtype, "FORWARD", ip)
except Exception as e:
log_query(qname, qtype, "FAILED", str(e))
return reply
if __name__ == "__main__":
resolver = NintendoResolver()
server = DNSServer(resolver, port=53, address="0.0.0.0", tcp=False)
print(f"[*] DNS server started on 0.0.0.0:53 (redirecting Nintendo domains to {REDIRECT_IP})")
server.start()
-1
View File
@@ -1 +0,0 @@
[2025-09-15 19:05:32] Gen4 send Manaphy Egg [PPorg].pgt to 192.168.0.34
+46 -41
View File
@@ -1,51 +1,56 @@
from flask import Flask, request, send_file, abort, jsonify from flask import Flask, request, Response, send_from_directory
import os, random, time import os, random, datetime, xml.etree.ElementTree as ET
BASE = os.path.dirname(os.path.abspath(__file__))
GEN4_DIR = os.path.join(BASE, "wonder_gen4")
GEN5_DIR = os.path.join(BASE, "wonder_gen5")
LOGFILE = os.path.join(BASE, "server.log")
app = Flask(__name__) app = Flask(__name__)
def log(msg): BASE_DIR = os.path.dirname(__file__)
ts = time.strftime("%Y-%m-%d %H:%M:%S") LOG_DIR = os.path.join(BASE_DIR, "ac_logs")
line = f"[{ts}] {msg}\n" EVENTS_GEN4 = os.path.join(BASE_DIR, "wonder_gen4")
print(line, end="") EVENTS_GEN5 = os.path.join(BASE_DIR, "wonder_gen5")
with open(LOGFILE, "a") as f:
f.write(line)
def list_files(folder, exts): os.makedirs(LOG_DIR, exist_ok=True)
return [f for f in os.listdir(folder) if any(f.lower().endswith(e) for e in exts)]
@app.route("/") # ===== Logging helper =====
def index(): def save_raw_request(prefix: str, data: bytes, headers: dict):
return "Pokémon Wonder Card Server running" ts = datetime.datetime.utcnow().strftime("%Y%m%dT%H%M%S%fZ")
filename = f"{prefix}_{ts}.bin"
with open(os.path.join(LOG_DIR, filename), "wb") as f:
f.write(data)
with open(os.path.join(LOG_DIR, filename + ".meta"), "w", encoding="utf-8") as f:
for k, v in headers.items():
f.write(f"{k}: {v}\n")
@app.route("/gen4") # ===== Gen 4: random event =====
def gen4(): @app.route("/gen4/event", methods=["GET"])
files = list_files(GEN4_DIR, [".pcd", ".pgt"]) def gen4_event():
if not files: return abort(404, "No Gen4 files") files = [f for f in os.listdir(EVENTS_GEN4) if f.lower().endswith((".pgt", ".pcd"))]
chosen = request.args.get("file") or random.choice(files) if not files:
if chosen not in files: return abort(404, "File not found") return Response("<error>No Gen4 events found</error>", mimetype="application/xml")
log(f"Gen4 send {chosen} to {request.remote_addr}")
return send_file(os.path.join(GEN4_DIR, chosen), mimetype="application/octet-stream")
@app.route("/gen5") chosen = random.choice(files)
def gen5(): print(f"Serving Gen4 event: {chosen}")
files = list_files(GEN5_DIR, [".pgf"]) return send_from_directory(EVENTS_GEN4, chosen, mimetype="application/octet-stream")
if not files: return abort(404, "No Gen5 files")
chosen = request.args.get("file") or random.choice(files)
if chosen not in files: return abort(404, "File not found")
log(f"Gen5 send {chosen} to {request.remote_addr}")
return send_file(os.path.join(GEN5_DIR, chosen), mimetype="application/octet-stream")
@app.route("/list") # ===== Gen 5: random event =====
def list_all(): @app.route("/gen5/event", methods=["GET"])
return jsonify({ def gen5_event():
"gen4": list_files(GEN4_DIR, [".pcd", ".pgt"]), files = [f for f in os.listdir(EVENTS_GEN5) if f.lower().endswith(".pgf")]
"gen5": list_files(GEN5_DIR, [".pgf"]) if not files:
}) return Response("<error>No Gen5 events found</error>", mimetype="application/xml")
chosen = random.choice(files)
print(f"Serving Gen5 event: {chosen}")
return send_from_directory(EVENTS_GEN5, chosen, mimetype="application/octet-stream")
# ===== Catch-all =====
@app.route('/', defaults={'path': ''}, methods=['GET','POST'])
@app.route('/<path:path>', methods=['GET','POST'])
def catch_all(path):
raw = request.get_data()
headers = dict(request.headers)
save_raw_request(f"unknown_{path}", raw, headers)
print(f"Unhandled request: /{path}")
return Response("<error>Unhandled path</error>", mimetype="application/xml")
if __name__ == "__main__": if __name__ == "__main__":
app.run(host="0.0.0.0", port=80) app.run(host="0.0.0.0", port=8080)
+19
View File
@@ -0,0 +1,19 @@
@echo off
setlocal
REM Paths
set BASE_DIR=C:\Users\hocke\Documents\GitHub\Pokemon-DNS-DS-Server
set STUNNEL_EXE="C:\Program Files (x86)\stunnel\bin\stunnel.exe"
set STUNNEL_CONF=%BASE_DIR%\wii-wfc-dev\stunnel\stunnel.conf
echo === Starting DNS Server ===
start "DNS Server" cmd /k "cd /d %BASE_DIR% && python dnsserver.py"
echo === Starting Flask Server ===
start "Flask Server" cmd /k "cd /d %BASE_DIR% && python server.py"
echo === Starting stunnel ===
start "stunnel" %STUNNEL_EXE% %STUNNEL_CONF%
echo All services launched!
pause
+29
View File
@@ -0,0 +1,29 @@
@echo off
setlocal
REM Paths
set BASE_DIR=C:\Users\hocke\Documents\GitHub\Pokemon-DNS-DS-Server
set STUNNEL_EXE="C:\Program Files (x86)\stunnel\bin\stunnel.exe"
set STUNNEL_CONF=%BASE_DIR%\stunnel\stunnel.conf
REM Start DNS Server
start "DNS Server" cmd /c "cd /d %BASE_DIR% && python dnsserver.py"
REM Start Flask Server
start "Flask Server" cmd /c "cd /d %BASE_DIR% && python server.py"
REM Start stunnel
start "stunnel" %STUNNEL_EXE% %STUNNEL_CONF%
echo.
echo Servers are running. Close this window to stop them all.
echo.
REM Wait until user closes window
pause >nul
REM Kill services
taskkill /IM python.exe /F >nul 2>&1
taskkill /IM stunnel.exe /F >nul 2>&1
echo All services stopped.
+21
View File
@@ -0,0 +1,21 @@
-----BEGIN CERTIFICATE-----
MIIDfTCCAuagAwIBAgIBADANBgkqhkiG9w0BAQUFADCBjDELMAkGA1UEBhMCVVMx
EzARBgNVBAgTCldhc2hpbmd0b24xIDAeBgNVBAoTF05pbnRlbmRvIG9mIEFtZXJp
Y2EgSW5jMQwwCgYDVQQLEwNOT0ExFDASBgNVBAMTC05pbnRlbmRvIENBMSIwIAYJ
KoZIhvcNAQkBFhNjYUBub2EubmludGVuZG8uY29tMB4XDTA1MDgxMDA3MDIyMloX
DTQ5MTIyODEyMDAwMFowgYwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5n
dG9uMSAwHgYDVQQKExdOaW50ZW5kbyBvZiBBbWVyaWNhIEluYzEMMAoGA1UECxMD
Tk9BMRQwEgYDVQQDEwtOaW50ZW5kbyBDQTEiMCAGCSqGSIb3DQEJARYTY2FAbm9h
Lm5pbnRlbmRvLmNvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAs815l3dd
iq+GqOjXcxx33xCQH4H4QZ4hVd+8/GP7GUPx9sRyQkm9rURoTvPaHeZN2PlZiNyu
Pps4Ccp//9wkokR4eEmT1IRAELjsPtstk8gRyP14LWGtMa6GJrD9Wj+hPb/iS0ns
zmaYWCYSwPv0d2Ub6vvLf+CMywKjTl6M6psCAwEAAaOB7DCB6TAdBgNVHQ4EFgQU
e1dTPzGsd3Hx/UrmD0Ow1VVBn9IwgbkGA1UdIwSBsTCBroAUe1dTPzGsd3Hx/Urm
D0Ow1VVBn9KhgZKkgY8wgYwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5n
dG9uMSAwHgYDVQQKExdOaW50ZW5kbyBvZiBBbWVyaWNhIEluYzEMMAoGA1UECxMD
Tk9BMRQwEgYDVQQDEwtOaW50ZW5kbyBDQTEiMCAGCSqGSIb3DQEJARYTY2FAbm9h
Lm5pbnRlbmRvLmNvbYIBADAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4GB
AF/sccq8/vxDNIho4Zal/mZZ+cbggUn8AH9ZeYD2kLvCd1w9j4O0IgkqEaBnYbho
OR7R4ExsHSEGmFn1A7TNbkRgcRrcOIoO6HsRPmPiDX12NKEZyfN8cDwc0azKsJZd
eYVJXqFQRJNZz3h5DkUDlXxJQMD3xALVAMehncWFC7jc
-----END CERTIFICATE-----
View File
+5
View File
@@ -0,0 +1,5 @@
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgAAAAAAAAAAAAAAAA
R3PjA+68HXho4wtwNR2+qja1KjOhRANCAARNgTALVXey7qzm9xplJr10KCXkZcEA
vUfFK/d8DDELSmj68KRellomg04PUPPzA2BBiSIpTu3HBVjeUCfR2zf5
-----END PRIVATE KEY-----
+1
View File
@@ -0,0 +1 @@
004773E303EEBC1D7868E30B70351DBEAA36B52A33
+17
View File
@@ -0,0 +1,17 @@
-----BEGIN CERTIFICATE-----
MIICqjCCAhMCFFlac2Q+iXBDUiNXP9NLm6gXIWo7MA0GCSqGSIb3DQEBBQUAMIGT
MQswCQYDVQQGEwJVUzEVMBMGA1UECAwMUGVubnN5bHZhbmlhMRQwEgYDVQQHDAtN
dXJyeXN2aWxsZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRkw
FwYDVQQLDBBuaW50ZW5kb3dpZmkubmV0MRkwFwYDVQQDDBBuaW50ZW5kb3dpZmku
bmV0MB4XDTI1MDkyMDAzNTMwMFoXDTI2MDkyMDAzNTMwMFowgZMxCzAJBgNVBAYT
AlVTMRUwEwYDVQQIDAxQZW5uc3lsdmFuaWExFDASBgNVBAcMC011cnJ5c3ZpbGxl
MSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxGTAXBgNVBAsMEG5p
bnRlbmRvd2lmaS5uZXQxGTAXBgNVBAMMEG5pbnRlbmRvd2lmaS5uZXQwgZ8wDQYJ
KoZIhvcNAQEBBQADgY0AMIGJAoGBANbz7GfMN/KBqp12LWUSxVflSqDKCuudfsTI
+32SnvOji6SZDDPpOLn64cPV3/bmEBxjBCHXsYIiz4q/2VK4YRyrStFDnOVsXw52
awH3Kw7uTVpUQttJAm3aj0CJgJNoc1j+1Kb960EVk5jvGMSHK0ESDCCqOOPAEzqN
wP0gClvfAgMBAAEwDQYJKoZIhvcNAQEFBQADgYEAgorZg1VXbmb+f2RSKKflW3hQ
3TcHGsXX97Ai7F/U97ge89G+UT+0YHLILqsH/zlJc5sKKo4slMqzlwJXvGPTZ0iS
eGkO87zo/ibv38H9qhRkKH8OjdKHJf9LjCewBdeth2VN5EToTt60OyNenmnj3p5m
65bjEyE/+GDiTWbK1MQ=
-----END CERTIFICATE-----
+16
View File
@@ -0,0 +1,16 @@
-----BEGIN PRIVATE KEY-----
MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBANbz7GfMN/KBqp12
LWUSxVflSqDKCuudfsTI+32SnvOji6SZDDPpOLn64cPV3/bmEBxjBCHXsYIiz4q/
2VK4YRyrStFDnOVsXw52awH3Kw7uTVpUQttJAm3aj0CJgJNoc1j+1Kb960EVk5jv
GMSHK0ESDCCqOOPAEzqNwP0gClvfAgMBAAECgYBk7MR7++vs5iroodswkL6txU/j
7NlxxVIHLWK/k/8j4A8p6l5LZV6oG/6yx5HPyDeaTHddOHv04UlXGTr91uppAbf7
BN8iXwZzAh7tT3QVluGXnO+hlrtRiPCpnSRKuG04wR1UehYJV3WrDKLVBHmqhxxF
tdbOBRRcY+KrFnBWoQJBAPxqPyR/mtxzTH2KV8yHNaeku/U7OJTi0RXV/VbKviQs
qzQYql+CEJxJLYz5r16ebnn+J1kakq+JLj/yCcZ50BECQQDaAXfmTrUK3CiHzrln
nmyOia67N2biGzNoarz9O7IAqfAFVhnbw1pO/GDMXvq9yc4A4dlApDh02DTAXAIf
21zvAkBdIQYy/TGx7IhoOUazm59cPxdqH3N820Kxf8Z840jMJ/7ytbcGjgJ65zT7
MpRVoLa6I994rKk6Qq7hwPeCVydBAkAbEhebZ6afhJsb0hW/wjJkuMATy1thNFN7
4dU59NGzt2ZwozJu0aFDMZ+rgRfNvWNxRJo/pqTEDZzvTf2NiyXNAkAo4bDqN37K
0x86Gjn+L1a76jpgNtJ3o5L/GyJYlpQrK4Qn/ZX3TX/Lmv74BHjU8vy5lI66w5Yt
l81+P9zvyEVy
-----END PRIVATE KEY-----
+2
View File
@@ -0,0 +1,2 @@
cryptography>=3.3
flask>=2.0
+17
View File
@@ -0,0 +1,17 @@
; stunnel.conf
; Run in background (no console popup)
foreground = quiet
; Reduce verbosity (4 = info, 3 = warnings only)
debug = 4
; Write logs to file instead of popup
output = stunnel.log
[wfc]
accept = 0.0.0.0:443
connect = 127.0.0.1:8080
cert = ../certs/server.crt
key = ../certs/server.key
sslVersion = SSLv23
ciphers = ALL:!aNULL