Files
RexiMemo-OSE/hatena.py
T

254 lines
7.9 KiB
Python

from __future__ import annotations
import hashlib
import importlib.util
import os
import re
import sys
from urllib.parse import urlencode
from twisted.web import resource, static
ServerLog = None
Silent = False
def as_text(value):
return value.decode("utf-8", "replace") if isinstance(value, bytes) else str(value)
def request_path(request):
return as_text(request.path)
def request_headers(request):
return {as_text(k).lower(): as_text(v) for k, v in request.getAllHeaders().items()}
def request_args(request):
out = {}
for key, values in request.args.items():
out[as_text(key)] = [as_text(v) for v in values]
return out
def client_ip(request):
getter = getattr(request, "getClientIP", None)
if getter:
try:
value = getter()
if value:
return as_text(value)
except Exception:
pass
getter = getattr(request, "getClientAddress", None)
if getter:
try:
address = getter()
if address is not None:
return as_text(getattr(address, "host", address))
except Exception:
pass
return ""
def Log(request, path=None, silent=Silent):
if ServerLog is None:
return
path = path if path is not None else '"%s"' % request_path(request)
ServerLog.write("%s requested %s" % (client_ip(request), path), silent)
class AccessDeniedResource(resource.Resource):
isLeaf = True
def render(self, request):
request.setResponseCode(403)
request.setHeader(b"content-type", b"text/plain; charset=utf-8")
return b"403 - Access denied\nUse the server as a Flipnote Studio HTTP proxy and provide X-DSi-SID."
class BannedResource(resource.Resource):
isLeaf = True
def render(self, request):
request.setResponseCode(403)
request.setHeader(b"content-type", b"text/plain; charset=utf-8")
return b"403 - This IP address is blocked."
class NotFoundResource(resource.Resource):
isLeaf = True
def render(self, request):
args = request_args(request)
query = urlencode([(k, v) for k, values in args.items() for v in values])
path = request_path(request) + (("?" + query) if query else "")
if ServerLog is not None:
ServerLog.write('%s got 404 when requesting "%s"' % (client_ip(request), path), Silent)
request.setResponseCode(404)
request.setHeader(b"content-type", b"text/plain; charset=utf-8")
return b"404 - Not Found"
AccessDenied = AccessDeniedResource()
Banned = BannedResource()
NotFound = NotFoundResource()
class Root(resource.Resource):
isLeaf = False
def __init__(self, docs_enabled=False):
super().__init__()
self.dsResource = DSRoot()
self.cssResource = static.File("hatenadir/css/")
self.imagesResource = static.File("hatenadir/images/")
self.staticResource = static.File("web/static/")
from webapp import WebRoot
self.webResource = WebRoot(docs_enabled=docs_enabled)
def getChild(self, name, request):
from DB import Database
if Database.is_ip_banned(client_ip(request)):
return Banned
name = as_text(name)
if name == "css":
return self.cssResource
if name == "images":
return self.imagesResource
if name == "static":
return self.staticResource
headers = request_headers(request)
host = headers.get("host", "").split(":", 1)[0].lower()
dsi_host = host in {"flipnote.hatena.com", "ugomemo.hatena.ne.jp"}
if dsi_host:
if "x-dsi-sid" not in headers:
return AccessDenied
if name == "ds":
return self.dsResource
if name == "":
return self
return self
return self.webResource
def render(self, request):
from DB import Database
if Database.is_ip_banned(client_ip(request)):
return Banned.render(request)
headers = request_headers(request)
host = headers.get("host", "").split(":", 1)[0].lower()
if host not in {"flipnote.hatena.com", "ugomemo.hatena.ne.jp"}:
return self.webResource.render(request)
if "x-dsi-sid" not in headers:
return AccessDenied.render(request)
request.setHeader(b"content-type", b"text/plain; charset=utf-8")
return b"RexiMemo OSE\n"
class DSRoot(resource.Resource):
isLeaf = False
def __init__(self):
super().__init__()
self.region = UgoRoot()
self.regions = {"v2-xx", "v2-eu", "v2-us", "v2-jp"}
def getChild(self, name, request):
name = as_text(name)
if name in self.regions:
return self.region
if name == "":
return self
return NotFound
def render(self, request):
return b"ds"
class UgoRoot(resource.Resource):
isLeaf = False
def __init__(self):
super().__init__()
LoadHatenadirStructure(self)
def getChild(self, name, request):
text = as_text(name)
if text == "":
return self
if re.fullmatch(r"[0-9A-Fa-f]{16}", text):
try:
from DB import Database
room = Database.get_creator_room_by_fsid(text.upper())
creator_root = getattr(self, "children", {}).get(b"creator")
if room and creator_root is not None:
return creator_root.getChild(str(room["id"]).encode("ascii"), request)
except Exception:
pass
return NotFound
def render(self, request):
return b"ugo"
class FileResource(resource.Resource):
isLeaf = True
def __init__(self, filepath):
super().__init__()
self.filepath = filepath
self.html = filepath.rsplit(".", 1)[-1][:3].lower() == "htm"
def render(self, request):
Log(request)
if self.html:
request.setHeader(b"content-type", b"text/html; charset=utf-8")
return static.File(self.filepath).render(request)
class FolderResource(resource.Resource):
isLeaf = False
def getChild(self, name, request):
return self if as_text(name) == "" else NotFound
def render(self, request):
request.setResponseCode(403)
return b""
def _load_python_resource(filepath):
module_name = "hatena_dynamic_" + hashlib.sha1(os.path.abspath(filepath).encode("utf-8")).hexdigest()
spec = importlib.util.spec_from_file_location(module_name, filepath)
if spec is None or spec.loader is None:
raise ImportError("Could not load %s" % filepath)
module = importlib.util.module_from_spec(spec)
sys.modules[module_name] = module
spec.loader.exec_module(module)
return module
def LoadHatenadirStructure(Resource, path=os.path.join("hatenadir", "ds", "v2-xx")):
for root, dirs, files in os.walk(path):
if root != path:
continue
for filename in sorted(files, key=lambda f: (f.lower().endswith(".py"), f.lower())):
filepath = os.path.join(path, filename)
if filename.endswith(".py"):
child_name = filename[:-3]
child = _load_python_resource(os.path.abspath(filepath)).PyResource()
elif filename.endswith(".pyc"):
continue
else:
child_name = filename
child = FileResource(filepath)
Resource.putChild(child_name.encode("utf-8"), child)
if child_name.endswith(".ugo"):
Resource.putChild((child_name[:-4] + ".uls").encode("utf-8"), child)
for foldername in dirs:
if not foldername.startswith("__"):
folder = FolderResource()
LoadHatenadirStructure(folder, os.path.join(path, foldername))
Resource.putChild(foldername.encode("utf-8"), folder)
break
def Setup(docs_enabled=False):
return Root(docs_enabled=docs_enabled)