251 lines
11 KiB
Plaintext
251 lines
11 KiB
Plaintext
{
|
||
"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<S, E>(data: S) -> impl Stream<Item = HttpResult<Bytes>>\n",
|
||
"where\n",
|
||
" S: Stream<Item = Result<Bytes, E>> + 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::<u32>().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
|
||
}
|