{ "cells": [ { "cell_type": "markdown", "id": "46c5ef50", "metadata": {}, "source": [ "# anki sync文档\n", "## 数据包\n", "### http\n", "```python\n", "same = {\n", " 'SERVER_PROTOCOL': 'HTTP/1.0', 'REQUEST_METHOD': 'POST', 'PATH_INFO': '/msync/begin', 'QUERY_STRING': '', \n", "}\n", "```\n", "### android 包\n", "```python\n", "env = { 'CONTENT_LENGTH': '449','CONTENT_TYPE': 'multipart/form-data; boundary=Anki-sync-boundary', 'HTTP_ACCEPT_ENCODING': 'gzip', 'HTTP_USER_AGENT': 'AnkiDroid-2.15.6'}\n", "```\n", "### pc包\n", "```python\n", "b'{\"v\":\"anki,2.1.64 (581f82c5),win:10\"}'\n", "```\n", "```python\n", "env = { 'CONTENT_LENGTH': '46', 'CONTENT_TYPE': 'application/octet-stream', 'HTTP_ANKI_SYNC': '{\"v\":11,\"k\":\"a8b327a1e7ccbf704c4a60a23a8c4a57\",\"c\":\"2.1.64,581f82c5,windows\",\"s\":\"cJcynh\"}' }\n", "```\n", "\n", "## PC数据\n", "### 数据协议\n", "- 输入\n", " - path is ::/msync/begin body is::b'(\\xb5/\\xfd\\x00X)\\x01\\x00{\"v\":\"anki,2.1.64 (581f82c5),win:10\"}'\n", " - path is ::/sync/meta body is::b'(\\xb5/\\xfd\\x00Xi\\x01\\x00{\"v\":11,\"cv\":\"anki,2.1.64 (581f82c5),win:10\"}'\n", " - path is ::/sync/hostKey body is::b'(\\xb5/\\xfd\\x00X\\xc9\\x00\\x00{\"u\":\"anki\",\"p\":\"ouczbs\"}'\n", "- 输出\n", " - 不加密,不压缩\n", "### 流式解析\n", "- 在数据较大时,使用结果不太对\n", "```python\n", "import pyzstd\n", "data = b'{\"v\":\"anki,2.1.64 (581f82c5),win:10\"}'\n", "encoder = pyzstd.ZstdCompressor()\n", "encoder.compress(data)\n", "body = encoder.flush()\n", "print(body)\n", "decoder = pyzstd.ZstdDecompressor()\n", "data = decoder.decompress(body)\n", "print(data)\n", "```\n", "### 快速解析\n", "```python\n", "import pyzstd\n", "data = b'(\\xb5/\\xfd\\x00X)\\x01\\x00{\"v\":\"anki,2.1.64 (581f82c5),win:10\"}'\n", "# with open(\"conf/data.txt\",\"wb\")as f:\n", "# f.write(data)\n", "with open(\"data.txt\",\"rb\")as f:\n", " data = f.read()\n", "print(len(data))\n", "body = pyzstd.compress(data)\n", "print(len(body))\n", "data = pyzstd.decompress(body)\n", "print(len(data))\n", "```\n", "## Android 数据\n", "### 数据协议\n", "- 输入\n", "```python\n", "data = b'--Anki-sync-boundary\\r\\nContent-Disposition: form-data; name=\"k\"\\r\\n\\r\\n0361e82baf7e86bf1b5f4339382fccd4\\r\\n--Anki-sync-boundary\\r\\nContent-Disposition: form-data; name=\"s\"\\r\\n\\r\\na87d9d0a\\r\\n--Anki-sync-boundary\\r\\nContent-Disposition: form-data; name=\"c\"\\r\\n\\r\\n1\\r\\n--Anki-sync-boundary\\r\\nContent-Disposition: form-data; name=\"data\"; filename=\"data\"\\r\\nContent-Type: application/octet-stream\\r\\n\\r\\n\\x1f\\x8b\\x08\\x00\\x00\\x00\\x00\\x00\\x00\\x00e\\x91MN\\xc30\\x10\\x85\\xafb\\xcd\\x86\\x8d\\xd5$(M\\x9b\\xa8t\\xc3\\x16!\\x84\\xd8U]\\xb8\\xf1\\xa4\\xb1\\xea\\xd8\\x95\\xed6TUwH\\xdc\\xa0\\x1b\\x16p\\x07\\x84\\xc4\\x81\\x10\\xa2\\xb7\\xc0I\\xf9+\\xdd\\xcd|\\xf3\\xe4yo\\xbc\\x86\\xbcdj\\x8a\\x16\\xb25T\\x9a\\xa3\\xf4\\xd5hL\\x81c>k\\xca\\xd1\\x1a\\x04\\x87,J\\xd2\\xb0\\x9b\\xc4q\\x14\\xa7Q\\x8f6\\xca\\x1f\\xd6Mc\\n\\x8aU\\x08\\x19|\\xbcl\\xdf^\\xb7\\xbb\\x87\\xbb\\xdd\\xe3\\xf3\\xfb\\xd3=PXX\\xe5\\x95\\x11\\x05i\\xd4\\x8d\\xe6l\\xe5\\xdf\\x8ch\\xe87\\x18\\\\\\x1e\\x02\\x85\\xf5!p\\xa2\\xc2_\\x92\\xf4{\\xa9\\x87\\xb9\\x96\\x92\\xcd-z\\x03\\x05\\x93\\x16)L\\x8c\\xae-\\x9a\\xf3\\xa3\\x01G\\x9b{SW\\x12\\x99Eb\\x11\\x89+\\x91\\x0c\\x18)\\r\\x16g\\'\\xa5ss\\x9b\\x05\\x01S3Q\\xe3\\xa4\\xa3\\xd0\\x05\\xb6d\\x06y T\\xa1\\x83^\\xdc\\x8d\\x92~\\x18\\x9f\\x9e\\x0c\\xf7\\x984G!s6\\xc5A\\xc0\\x86\\xa4\\xd0\\x86T\\xda i\\xd4\\x1d\\x9f\\x95\\xaf|\\xd6\\xb0M&\\xb0\\xbe\\x10\\x95p_\\xf6\\xd5B\\xca6\\xe01\\xfc#\\xfe\\xaf\\xfb\\xees\\xad\\n\\x7fD\\nx\\xebP\\xf1K\\xac\\xdb5\\xfb\\xee\\x1a\\x97\\xbe\\xdb\\x8c\\xe9h\\xdc\\x9c\\x8cM\\xdb\\x0f\\xdcl>\\x019A\\x97\\x92\\xda\\x01\\x00\\x00\\r\\n--Anki-sync-boundary--\\r\\n'\n", "\n", "data = b'--Anki-sync-boundary\\r\\nContent-Disposition: form-data; name=\"k\"\\r\\n\\r\\nxQ0tcIOYlDyeJhZV\\r\\n--Anki-sync-boundary\\r\\nContent-Disposition: form-data; name=\"s\"\\r\\n\\r\\n8d6272d6\\r\\n--Anki-sync-boundary\\r\\nContent-Disposition: form-data; name=\"c\"\\r\\n\\r\\n1\\r\\n--Anki-sync-boundary\\r\\nContent-Disposition: form-data; name=\"data\"; filename=\"data\"\\r\\nContent-Type: application/octet-stream\\r\\n\\r\\n\\x1f\\x8b\\x08\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xabV*S\\xb224\\xd0QJ\\x06\\xd2J\\x89y\\xd9\\x99)E\\xf9\\x99):Fz\\x86\\xa6zf:\\x89y`\\xae\\x95\\xa1\\x91\\x95\\xaf\\x91\\x81\\x81\\xb9\\x97a\\xb0\\xb3R-\\x00\\x16\\x80\\xd5\\x825\\x00\\x00\\x00\\r\\n--Anki-sync-boundary--\\r\\n'\n", "```\n", "- 输出\n", " - 不加密,zstd压缩\n", "### 正则匹配\n", "```python\n", "import re\n", "pattern = b'--Anki-sync-boundary\\r\\nContent-Disposition: form-data; name=\"(.*?)\"\\\n", "(?:; filename=\".*?\"\\r\\nContent-Type: application/octet-stream)*\\r\\n\\r\\n(.*?|[\\s\\S]*?)\\r\\n(?:--Anki-sync-boundary--\\r\\n$)*'\n", "res = re.findall(pattern,data)\n", "print(res)\n", "```\n", "\n", "## python\n", "### api 版本号\n", "`sync.py >> SYNC_VER = 11`\n", "### 数据包\n", "```python\n", "#class Request\n", "def parseAndroid(self, body, env):\n", " env[\"APP_CLIENT_TYPE\"] = ClientType.Android\n", " request_items_dict = {}\n", " pattern = b'--Anki-sync-boundary\\r\\nContent-Disposition: form-data; name=\"(.*?)\"\\\n", "(?:; filename=\".*?\"\\r\\nContent-Type: application/octet-stream)*\\r\\n\\r\\n(.*?|[\\s\\S]*?)\\r\\n(?:--Anki-sync-boundary--\\r\\n$)*'\n", " res = re.findall(pattern , body)\n", " for k in res:\n", " if len(k) < 2:\n", " logger.error(f\"error pattern match: {k}\")\n", " continue\n", " v = k[1] if k[0] == b\"data\" else k[1].decode()\n", " request_items_dict[k[0].decode()] = v\n", " if \"data\" in request_items_dict and \"c\" in request_items_dict \\\n", " and int(request_items_dict[\"c\"]):\n", " data = request_items_dict[\"data\"] \n", " with gzip.GzipFile(mode=\"rb\", fileobj=io.BytesIO(data)) as gz:\n", " data = gz.read()\n", " request_items_dict[\"data\"] = data\n", " if \"data\" not in request_items_dict:\n", " #pdb.set_trace()\n", " pass\n", " return request_items_dict\n", "def parsePC(self, body, env):\n", " env[\"APP_CLIENT_TYPE\"] = ClientType.PC\n", " request_items_dict = {}\n", " body = pyzstd.decompress(body)\n", " request_items_dict[\"data\"] = body\n", " http_anki_sync = env.get(\"HTTP_ANKI_SYNC\", \"\")\n", " if http_anki_sync != \"\":\n", " anki_sync = json.loads(http_anki_sync)\n", " for k in anki_sync.keys():\n", " request_items_dict[k] = anki_sync[k]\n", " return request_items_dict\n", " \n", "def parse(self):\n", " if 'application/octet-stream' in env.get(\"CONTENT_TYPE\",\"\"):\n", " request_items_dict = self.parsePC(body, env)\n", " else:\n", " request_items_dict = self.parseAndroid(body, env)\n", "def wrap_body(self, body, env):\n", " if \"APP_CLIENT_TYPE\" not in env:\n", " return body\n", " if env[\"APP_CLIENT_TYPE\"] == ClientType.PC:\n", " return pyzstd.compress(body)\n", " # if env[\"APP_CLIENT_TYPE\"] == ClientType.Android:\n", " # return body\n", " return body\n", "```\n", "\n", "### 注解\n", "```python\n", "import types\n", "from functools import wraps\n", "class chunked(object):\n", " \"\"\"decorator\"\"\"\n", " def __init__(self, func):\n", " wraps(func)(self)\n", " print(\"__init__ chunked\",func)\n", "\n", " def __call__(self, *args, **kwargs):\n", " clss = args[0]\n", " environ = args[1]\n", " start_response = args[2]\n", " args = (\n", " clss,\n", " environ,\n", " )\n", " w = self.__wrapped__(*args, **kwargs)\n", " print(\"__call__ chunked\",clss,environ,start_response,kwargs,w)\n", " return \"4\"\n", "\n", " def __get__(self, instance, cls):\n", " print(\"__get__ chunked\", instance , cls)\n", " if instance is None:\n", " return self\n", " else:\n", " return types.MethodType(self, instance)\n", "class SyncApp:\n", " def __init__(self, config):\n", " print(\"__init__SyncApp\",config)\n", "\n", " @chunked\n", " def __call__(self, req):\n", " print(\"__call__SyncApp\",req)\n", " return \"3\"\n", "sync = SyncApp(\"config\")\n", "sync(1,2)\n", " ```\n", "### webob\n", "- app\n", "- environ\n", " - 环境变量\n", " - 请求数据\n", " - body env.get(\"wsgi.input\")\n", " - header env.get(\"HTTP_ANKI_SYNC\", \"\")\n", "- request\n", "- response\n", "\n", "## rust\n", "```rust\n", "pub fn decode_zstd_body_stream_for_client(data: S) -> impl Stream>\n", "where\n", " S: Stream> + Unpin,\n", " E: Display,\n", "{\n", " let response_total = resp\n", " .headers()\n", " .get(&ORIGINAL_SIZE)\n", " .and_then(|v| v.to_str().ok())\n", " .and_then(|v| v.parse::().ok())\n", " .or_bad_request(\"missing original size\")?;\n", " stream.map(move |res| match res {\n", " Ok(bytes) => {\n", " let mut inner = inner.lock().unwrap();\n", " inner.last_activity = Instant::now();\n", " if sending {\n", " inner.bytes_sent += bytes.len() as u32;\n", " } else {\n", " inner.bytes_received += bytes.len() as u32;\n", " }\n", " Ok(bytes)\n", " }\n", " err => err.or_http_err(StatusCode::SEE_OTHER, \"stream failure\"),\n", " })\n", " \n", "}\n", "```\n", "- and_then map match\n", "- |v| ?\n", "- where impl trait" ] }, { "cell_type": "code", "execution_count": null, "id": "0e63eb4d", "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.11.4" } }, "nbformat": 4, "nbformat_minor": 5 }