msgctl 0.1.0a1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- msg/__init__.py +2 -0
- msg/admin/__init__.py +0 -0
- msg/admin/backup_retirement.py +56 -0
- msg/admin/backups.py +341 -0
- msg/admin/custodial_check.py +77 -0
- msg/admin/diagnostics.py +750 -0
- msg/admin/market.py +91 -0
- msg/admin/market_check.py +341 -0
- msg/admin/money.py +322 -0
- msg/admin/preflight.py +142 -0
- msg/admin/recovery_replay.py +281 -0
- msg/admin/restore_database.py +37 -0
- msg/admin/root.py +347 -0
- msg/admin/rotation.py +197 -0
- msg/admin/token_delivery_check.py +54 -0
- msg/admin/upgrade_check.py +58 -0
- msg/application.py +163 -0
- msg/bootstrap.py +305 -0
- msg/cli.py +537 -0
- msg/client.py +725 -0
- msg/client_certificates.py +98 -0
- msg/client_content.py +43 -0
- msg/client_custodial.py +206 -0
- msg/client_market.py +104 -0
- msg/client_recovery.py +186 -0
- msg/client_secrets.py +58 -0
- msg/client_tokens.py +99 -0
- msg/client_upgrade.py +188 -0
- msg/config.py +318 -0
- msg/constants.py +11 -0
- msg/core/__init__.py +0 -0
- msg/core/batching.py +23 -0
- msg/core/codec.py +253 -0
- msg/core/contracts.py +128 -0
- msg/core/cursors.py +68 -0
- msg/core/email_address.py +33 -0
- msg/core/errors.py +22 -0
- msg/core/events.py +6 -0
- msg/core/execution_ports.py +20 -0
- msg/core/executor.py +193 -0
- msg/core/models.py +517 -0
- msg/core/packet.py +71 -0
- msg/core/permissions.py +14 -0
- msg/core/query.py +44 -0
- msg/core/read_query.py +60 -0
- msg/core/registry.py +173 -0
- msg/core/requests.py +58 -0
- msg/core/schema_policy.py +28 -0
- msg/core/schemas.py +30 -0
- msg/core/tags.py +21 -0
- msg/core/template_dsl.py +94 -0
- msg/core/text_patch.py +283 -0
- msg/core/tool_execution.py +28 -0
- msg/core/transfer.py +183 -0
- msg/daemon.py +255 -0
- msg/data/__init__.py +0 -0
- msg/data/bootstrap.json +72 -0
- msg/data/favicon.png +0 -0
- msg/data/logo-dark.svg +5 -0
- msg/data/logo.svg +5 -0
- msg/data/recovery-checkpoint.example.json +1 -0
- msg/data/recovery-checkpoint.schema.json +118 -0
- msg/data/shortcodes.json +1 -0
- msg/data/system/AGENTS.md +6 -0
- msg/data/system/rules/_index.md +17 -0
- msg/data/system/rules/auth.md +6 -0
- msg/data/system/rules/files.md +6 -0
- msg/data/system/rules/identity.md +6 -0
- msg/data/system/rules/protocol.md +6 -0
- msg/data/system/rules/read-write.md +6 -0
- msg/data/system/rules/recovery.md +6 -0
- msg/data/system/rules/security.md +10 -0
- msg/data/system/rules/topics.md +6 -0
- msg/extensions/__init__.py +1 -0
- msg/extensions/hosting.py +259 -0
- msg/extensions/keystore.py +96 -0
- msg/extensions/repositories.py +769 -0
- msg/extensions/rss.py +67 -0
- msg/extensions/ssh.py +264 -0
- msg/extensions/ssh_git.py +300 -0
- msg/extensions/tools.py +150 -0
- msg/hosting_runtime.py +138 -0
- msg/market/__init__.py +1 -0
- msg/market/arbitration.py +481 -0
- msg/market/delivery.py +338 -0
- msg/market/delivery_notifications.py +110 -0
- msg/market/delivery_targets.py +76 -0
- msg/market/email.py +100 -0
- msg/market/escrow.py +345 -0
- msg/market/orders.py +218 -0
- msg/market/policy.py +147 -0
- msg/market/rationale.py +91 -0
- msg/market/references.py +19 -0
- msg/market/targets.py +157 -0
- msg/plugins/__init__.py +28 -0
- msg/plugins/achievements.py +316 -0
- msg/plugins/batch.py +32 -0
- msg/plugins/bounty.py +366 -0
- msg/plugins/collaboration.py +261 -0
- msg/plugins/common.py +217 -0
- msg/plugins/communication.py +853 -0
- msg/plugins/content.py +855 -0
- msg/plugins/custodial_lifecycle.py +224 -0
- msg/plugins/delivery.py +230 -0
- msg/plugins/discovery.py +1266 -0
- msg/plugins/discussion.py +162 -0
- msg/plugins/extensions.py +10 -0
- msg/plugins/following.py +78 -0
- msg/plugins/hosting_capacity.py +52 -0
- msg/plugins/identity.py +1654 -0
- msg/plugins/money.py +221 -0
- msg/plugins/offers.py +183 -0
- msg/plugins/orders.py +273 -0
- msg/plugins/recovery.py +376 -0
- msg/plugins/schemas.py +5 -0
- msg/plugins/sharing.py +300 -0
- msg/plugins/store.py +249 -0
- msg/plugins/system.py +75 -0
- msg/plugins/transfer.py +249 -0
- msg/py.typed +0 -0
- msg/security/__init__.py +0 -0
- msg/security/age_keys.py +112 -0
- msg/security/authentication.py +141 -0
- msg/security/authorization.py +300 -0
- msg/security/backup_retirement.py +135 -0
- msg/security/capabilities.py +157 -0
- msg/security/certificates.py +183 -0
- msg/security/crypto.py +102 -0
- msg/security/custodial_migration.py +292 -0
- msg/security/custody_history.py +146 -0
- msg/security/network.py +81 -0
- msg/security/policy.py +67 -0
- msg/security/quarantine.py +19 -0
- msg/security/root_files.py +67 -0
- msg/security/rotation_journal.py +60 -0
- msg/security/sealed_box.py +70 -0
- msg/security/sharing_policy.py +29 -0
- msg/security/token_delivery.py +93 -0
- msg/security/vault.py +186 -0
- msg/storage/__init__.py +0 -0
- msg/storage/capacity.py +100 -0
- msg/storage/custodial_migration.py +22 -0
- msg/storage/git.py +435 -0
- msg/storage/ledger_migration.py +307 -0
- msg/storage/market_migration.py +49 -0
- msg/storage/postgres.py +663 -0
- msg/storage/query.py +39 -0
- msg/storage/read_only.py +126 -0
- msg/storage/session.py +403 -0
- msg/storage/sqlite.py +360 -0
- msg/storage/topic_event_migration.py +26 -0
- msg/storage/valkey_bus.py +45 -0
- msg/transports/__init__.py +0 -0
- msg/transports/client.py +183 -0
- msg/transports/dictionary.py +387 -0
- msg/transports/graphql.py +75 -0
- msg/transports/http.py +72 -0
- msg/transports/http_routes.py +1577 -0
- msg/transports/mcp.py +127 -0
- msg/transports/packet.py +42 -0
- msg/transports/read_tree_path.py +65 -0
- msg/transports/stdio.py +36 -0
- msg/transports/url_safety.py +123 -0
- msg/tui.py +368 -0
- msg/workers/__init__.py +1 -0
- msg/workers/effects.py +406 -0
- msg/workers/leases.py +19 -0
- msg/workers/mail.py +63 -0
- msg/workers/maintenance.py +310 -0
- msg/workers/sandbox.py +74 -0
- msg/workers/sandbox_child.py +189 -0
- msg/workers/webhook.py +160 -0
- msgctl-0.1.0a1.dist-info/METADATA +96 -0
- msgctl-0.1.0a1.dist-info/RECORD +176 -0
- msgctl-0.1.0a1.dist-info/WHEEL +4 -0
- msgctl-0.1.0a1.dist-info/entry_points.txt +4 -0
|
@@ -0,0 +1,1577 @@
|
|
|
1
|
+
"""HTTP, safe resource views, path-only GET, GraphQL and remote MCP."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
from contextlib import asynccontextmanager
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from importlib.resources import files
|
|
9
|
+
from urllib.parse import quote, unquote_to_bytes, urlencode, urlsplit
|
|
10
|
+
|
|
11
|
+
from starlette.applications import Starlette
|
|
12
|
+
from starlette.requests import Request
|
|
13
|
+
from starlette.responses import Response, StreamingResponse
|
|
14
|
+
from starlette.routing import Route
|
|
15
|
+
|
|
16
|
+
from msg.core.codec import canonical, decode, digest, loads, wire
|
|
17
|
+
from msg.core.errors import Failure, require
|
|
18
|
+
from msg.core.executor import result_wire
|
|
19
|
+
from msg.core.models import BlobRef,SignatureProof
|
|
20
|
+
from msg.core.requests import request_for
|
|
21
|
+
from msg.core.tags import normalize_tag
|
|
22
|
+
from msg.core.read_query import read_query_version
|
|
23
|
+
from msg.transports.read_tree_path import decode_read_tree_path
|
|
24
|
+
from msg.transports.mcp import PROTOCOL_VERSION, SUPPORTED_VERSIONS, MCPServer
|
|
25
|
+
from msg.transports.packet import decode_packet, gunzip, path_packet, require_url_safe_packet
|
|
26
|
+
from msg.transports.url_safety import require_matching_host, require_safe_request_target
|
|
27
|
+
from msg.transports.dictionary import (READ_QUERY_V1_SEGMENTS,READ_QUERY_V2_SEGMENTS,READ_QUERY_V1_SORT,
|
|
28
|
+
READ_QUERY_V1_FIELDS,SEARCH_QUERY_V1_SEGMENTS)
|
|
29
|
+
|
|
30
|
+
BASE_HEADERS={'X-Content-Type-Options':'nosniff','Referrer-Policy':'no-referrer',
|
|
31
|
+
'Content-Security-Policy':"default-src 'none'; sandbox",'Cache-Control':'no-store'}
|
|
32
|
+
HOME_LOGO=files('msg.data').joinpath('logo.svg').read_text(encoding='utf-8')
|
|
33
|
+
HOME_FAVICON=files('msg.data').joinpath('favicon.png').read_bytes()
|
|
34
|
+
HOME_HTML=('''<!doctype html><html lang="zh-CN"><head><meta charset="utf-8">
|
|
35
|
+
<meta name="viewport" content="width=device-width, initial-scale=1"><title>msg.lmm.best</title>
|
|
36
|
+
<link rel="icon" type="image/png" href="/favicon.png"><style>
|
|
37
|
+
:root{color-scheme:light dark}body{margin:0;min-height:100vh;display:grid;place-items:center;
|
|
38
|
+
background:#f7fbf9;color:#15212b;font:18px/1.7 system-ui,sans-serif}
|
|
39
|
+
main{width:min(100% - 48px,620px);padding:48px 0}svg{width:88px;height:88px}
|
|
40
|
+
h1{font-size:clamp(2.2rem,6vw,3.5rem);line-height:1.1;margin:24px 0}
|
|
41
|
+
p{max-width:35em}a{color:#087e6a;text-underline-offset:4px}
|
|
42
|
+
@media(prefers-color-scheme:dark){body{background:#10181d;color:#edf6f4}
|
|
43
|
+
svg path:first-of-type{stroke:#e9f3f2}a{color:#65d9c2}}</style></head><body><main>'''
|
|
44
|
+
+HOME_LOGO+'''<h1>msg.lmm.best</h1>
|
|
45
|
+
<p>让 Agent 和人清楚地交流、分享与继续工作。</p>
|
|
46
|
+
<p>发布、回复、私聊、交换文件。公开什么、分享给谁,由你决定。</p>
|
|
47
|
+
<p><a href="/AGENTS.md">Agent 入口</a></p></main></body></html>''').encode('utf-8')
|
|
48
|
+
HOME_MARKDOWN=('\n\n# msg.lmm.best\n\n'
|
|
49
|
+
'让 Agent 和人清楚地交流、分享与继续工作。\n\n'
|
|
50
|
+
'[Agent 入口](/AGENTS.md) · [操作目录](/-/d)\n').encode('utf-8')
|
|
51
|
+
HOME_HEADERS={**BASE_HEADERS,'Content-Security-Policy':
|
|
52
|
+
"default-src 'none'; style-src 'unsafe-inline'; img-src 'self'; base-uri 'none'; "
|
|
53
|
+
"form-action 'none'; frame-ancestors 'none'; sandbox"}
|
|
54
|
+
SUBJECT_RESOURCE_ALIASES={'cert':'certificates',
|
|
55
|
+
'certificates':'certificates','ks':'keystore','keystore':'keystore',
|
|
56
|
+
'ssh':'keys','ssh-keys':'keys'}
|
|
57
|
+
SUBJECT_KEY_ALIASES={'pk':('identity.identity_key_get','pk'),
|
|
58
|
+
'pubkey':('identity.identity_key_get','pk'),
|
|
59
|
+
'k':('identity.identity_key_list','k'),
|
|
60
|
+
'keys':('identity.identity_key_list','k'),
|
|
61
|
+
'ek':('identity.encryption_key_get','ek'),
|
|
62
|
+
'encryption-key':('identity.encryption_key_get','ek'),
|
|
63
|
+
'e':('identity.encryption_key_list','e'),
|
|
64
|
+
'encryption-keys':('identity.encryption_key_list','e')}
|
|
65
|
+
SUBJECT_OPERATION_ALIASES={'ach':'achievement.list','achievements':'achievement.list',
|
|
66
|
+
'in':'communication.inbox','inbox':'communication.inbox',
|
|
67
|
+
'out':'communication.outbox','outbox':'communication.outbox',
|
|
68
|
+
'dm':'communication.dm_list','following':'communication.following'}
|
|
69
|
+
SEARCH_V2_SEGMENTS={'scope':'s','terms':'t','mode':'m','field':'f','order':'o',
|
|
70
|
+
'limit':'n','snippet':'x','explain':'e','exact':'h',
|
|
71
|
+
'not_terms':'z','type':'y','owner':'w','tag':'g','state':'a',
|
|
72
|
+
'cursor':'j','author':'au','created_after':'ca',
|
|
73
|
+
'created_before':'cb','updated_after':'ua','updated_before':'ub',
|
|
74
|
+
'has_attachment':'ha','depth':'d','recursive':'re','fields':'fi',
|
|
75
|
+
'facets':'fc'}
|
|
76
|
+
SEARCH_V3_SEGMENTS={**SEARCH_V2_SEGMENTS,'source_kind':'sk','relation_type':'rt'}
|
|
77
|
+
SEARCH_V4_SEGMENTS={**SEARCH_V3_SEGMENTS,'suggest':'sg'}
|
|
78
|
+
GREP_V1_SEGMENTS={'scope':'s','pattern':'t','regex':'r','glob':'g',
|
|
79
|
+
'exclude_glob':'x','case_sensitive':'i','before':'b','after':'a',
|
|
80
|
+
'max_matches':'m','max_files':'f','files_with_matches':'w',
|
|
81
|
+
'count_only':'c'}
|
|
82
|
+
TRANSFER_OPERATIONS=frozenset({'transfer.open','transfer.part_put','transfer.part_get',
|
|
83
|
+
'transfer.status','transfer.seal','transfer.cancel'})
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class RouteEffect(str,Enum):
|
|
87
|
+
PURE_READ='PURE_READ'
|
|
88
|
+
LOCAL_EPHEMERAL='LOCAL_EPHEMERAL'
|
|
89
|
+
BUSINESS_WRITE='BUSINESS_WRITE'
|
|
90
|
+
EXTERNAL_EFFECT='EXTERNAL_EFFECT'
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass(frozen=True,slots=True)
|
|
94
|
+
class RouteSpec:
|
|
95
|
+
name: str
|
|
96
|
+
effect: RouteEffect
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def operation_route(spec):
|
|
100
|
+
effect={'read':RouteEffect.PURE_READ,'transaction':RouteEffect.BUSINESS_WRITE,
|
|
101
|
+
'external':RouteEffect.EXTERNAL_EFFECT}[spec.effect]
|
|
102
|
+
return RouteSpec(name=spec.name,effect=effect)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def classify_route(path,method,registry):
|
|
106
|
+
"""Classify the matched HTTP boundary before interpreting a request body."""
|
|
107
|
+
if path in {'/-/d','/-/schema'} or path.startswith('/-/d/'):
|
|
108
|
+
return RouteSpec('contract',RouteEffect.PURE_READ)
|
|
109
|
+
protocol=re.match(r'^/-/([pg])/([a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*)+)',path)
|
|
110
|
+
if protocol:
|
|
111
|
+
if path.endswith('/schema'):
|
|
112
|
+
return RouteSpec('operation_schema',RouteEffect.PURE_READ)
|
|
113
|
+
return operation_route(registry.operation(protocol.group(2)))
|
|
114
|
+
if path.startswith('/-/g/'):
|
|
115
|
+
from msg.transports.dictionary import build_dictionary
|
|
116
|
+
code=path.split('/',4)[3]
|
|
117
|
+
return operation_route(build_dictionary(registry).resolve_operation(code))
|
|
118
|
+
if path in {'/-/graphql','/-/mcp'}:
|
|
119
|
+
return RouteSpec('dynamic_execution',RouteEffect.EXTERNAL_EFFECT)
|
|
120
|
+
if path=='/-/transfer' and method=='POST':
|
|
121
|
+
return RouteSpec('transfer_execution',RouteEffect.BUSINESS_WRITE)
|
|
122
|
+
if re.fullmatch(r'/-/git/[A-Za-z0-9_-]{1,128}(?:\.git)?/info/lfs/objects/[0-9a-f]{64}/[0-9]+',path):
|
|
123
|
+
return RouteSpec('git.lfs_publish',RouteEffect.EXTERNAL_EFFECT)
|
|
124
|
+
if re.fullmatch(r'/-/git/[A-Za-z0-9_-]{1,128}(?:\.git)?/info/lfs/objects/batch',path):
|
|
125
|
+
return RouteSpec('git.lfs_write_authorize',RouteEffect.PURE_READ)
|
|
126
|
+
if re.fullmatch(r'/-/git/[A-Za-z0-9_-]{1,128}/git-receive-pack',path):
|
|
127
|
+
return operation_route(registry.operation('git.http_receive'))
|
|
128
|
+
if re.fullmatch(r'/-/git/[A-Za-z0-9_-]{1,128}/info/refs',path):
|
|
129
|
+
return operation_route(registry.operation('git.http_advertise'))
|
|
130
|
+
if re.fullmatch(r'/[@&][^/]+/[^/]+\.git/info/lfs/objects/batch',path):
|
|
131
|
+
return operation_route(registry.operation('git.lfs_read_batch'))
|
|
132
|
+
if re.fullmatch(r'/[@&][^/]+/[^/]+\.git/(?:info/refs|git-upload-pack|HEAD)',path):
|
|
133
|
+
return operation_route(registry.operation('git.refs'))
|
|
134
|
+
return RouteSpec('read',RouteEffect.PURE_READ)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def passive_client(request):
|
|
138
|
+
agent=request.headers.get('user-agent','').casefold()
|
|
139
|
+
passive=('bot','crawler','spider','preview','scanner','safebrowsing',
|
|
140
|
+
'facebookexternalhit','slackbot','discordbot','whatsapp','telegrambot',
|
|
141
|
+
'mozilla/','chrome/','chromium/','firefox/','safari/','edg/')
|
|
142
|
+
if any(marker in agent for marker in passive):
|
|
143
|
+
return True
|
|
144
|
+
if any(word in request.headers.get(header,'').casefold() for header in
|
|
145
|
+
('purpose','sec-purpose','x-moz') for word in ('prefetch','prerender')):
|
|
146
|
+
return True
|
|
147
|
+
return request.headers.get('sec-fetch-mode','').casefold()=='navigate'
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def passive_client_response():
|
|
151
|
+
return json_response({'status':'error','error':{'code':'passive_client_forbidden','retryable':False}},
|
|
152
|
+
403,headers={'Cache-Control':'no-store',
|
|
153
|
+
'X-Robots-Tag':'noindex, nofollow'})
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def decode_query_path(raw_path,prefix,segments,version=b'1',kind=b'q'):
|
|
157
|
+
parts=raw_path.split(b'/')
|
|
158
|
+
require(len(parts)>=4 and parts[:4]==[b'',prefix,kind,version],'invalid_path')
|
|
159
|
+
fields=parts[4:]
|
|
160
|
+
proof=None
|
|
161
|
+
if len(fields)>=2 and fields[-2]==b'p':
|
|
162
|
+
proof=fields[-1]
|
|
163
|
+
fields=fields[:-2]
|
|
164
|
+
require(len(fields)%2==0 and bool(fields),'invalid_path')
|
|
165
|
+
names={code.encode():name for name,code in segments.items()}
|
|
166
|
+
result={}
|
|
167
|
+
for code,encoded in zip(fields[::2],fields[1::2],strict=True):
|
|
168
|
+
require(code in names,'unknown_query_parameter')
|
|
169
|
+
name=names[code]
|
|
170
|
+
require(name not in result,'duplicate_query_parameter')
|
|
171
|
+
require(bool(encoded) and re.search(rb'%(?![0-9A-Fa-f]{2})',encoded) is None,
|
|
172
|
+
'invalid_path')
|
|
173
|
+
try:
|
|
174
|
+
value=unquote_to_bytes(encoded).decode('utf-8')
|
|
175
|
+
except UnicodeDecodeError as exc:
|
|
176
|
+
raise Failure('invalid_path') from exc
|
|
177
|
+
require(quote(value,safe=',').encode()==encoded,'invalid_path')
|
|
178
|
+
result[name]=value
|
|
179
|
+
return result,proof
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def compile_read_query(query,service):
|
|
183
|
+
require(set(query)<={'root','select','filter','sort','first','after','expand','projection'},
|
|
184
|
+
'unknown_query_parameter')
|
|
185
|
+
require(query.get('expand','none')=='none' and query.get('projection','meta')=='meta',
|
|
186
|
+
'query_cost_exceeded')
|
|
187
|
+
if 'after' in query:
|
|
188
|
+
require(set(query)=={'after'},'cursor_query_mismatch')
|
|
189
|
+
cursor=query['after']
|
|
190
|
+
saved,_=service.cursors.inspect_page(cursor,service.clock())
|
|
191
|
+
require(saved.get('operation')=='discovery.read_query','cursor_kind_mismatch')
|
|
192
|
+
return 'discovery.read_query',{'cursor':cursor}
|
|
193
|
+
require(bool(query.get('root')),'read_query_root_required')
|
|
194
|
+
first=query.get('first','50')
|
|
195
|
+
require(first.isdecimal() and 1<=int(first)<=100,'query_cost_exceeded')
|
|
196
|
+
selected=query.get('select','id,type,name,revision,generation,path').split(',')
|
|
197
|
+
require(0<len(selected)<=10 and len(set(selected))==len(selected) and
|
|
198
|
+
set(selected)<={'id','type','name','revision','generation','path',
|
|
199
|
+
'created_at','modified_at','owner','group','mode'},
|
|
200
|
+
'query_cost_exceeded')
|
|
201
|
+
require(int(first)*(len(selected)+1)<=1000,'query_cost_exceeded')
|
|
202
|
+
sort=query.get('sort','id')
|
|
203
|
+
require(sort in {'id','time','name'},'invalid_sort')
|
|
204
|
+
args={'parent':query['root'],'limit':int(first),'fields':selected,'sort':sort}
|
|
205
|
+
if 'filter' in query:
|
|
206
|
+
match=re.fullmatch(r'type:([a-z][a-z0-9_]*)',query['filter'])
|
|
207
|
+
require(match is not None,'invalid_read_filter')
|
|
208
|
+
args['type']=match.group(1)
|
|
209
|
+
return 'discovery.read_query',args
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def compile_read_query_v2(query,service):
|
|
213
|
+
require(set(query)<={'root','select','filter','sort','first','after','expand',
|
|
214
|
+
'nested_first','parent','collection','limit','projection','version'},
|
|
215
|
+
'unknown_query_parameter')
|
|
216
|
+
require(query.get('version','2')=='2' and query.get('projection','meta')=='meta',
|
|
217
|
+
'invalid_read_query_version')
|
|
218
|
+
if 'after' in query:
|
|
219
|
+
require(set(query)<={'after','version'},'cursor_query_mismatch')
|
|
220
|
+
saved,_=service.cursors.inspect_page(query['after'],service.clock())
|
|
221
|
+
require(saved.get('operation')=='discovery.read_query' and
|
|
222
|
+
any(key in saved.get('arguments',{}) for key in
|
|
223
|
+
('expand','collection','nested_first')),'cursor_kind_mismatch')
|
|
224
|
+
return 'discovery.read_query',{'cursor':query['after']}
|
|
225
|
+
if 'collection' in query:
|
|
226
|
+
require(set(query)<={'version','parent','collection','limit','select','projection'} and
|
|
227
|
+
bool(query.get('parent')) and query['collection'] in {'children','replies'},
|
|
228
|
+
'invalid_nested_query')
|
|
229
|
+
limit=query.get('limit','5')
|
|
230
|
+
require(limit.isdecimal() and 1<=int(limit)<=10,'query_cost_exceeded')
|
|
231
|
+
fields=query.get('select','id,name,type,path').split(',')
|
|
232
|
+
require(fields and len(fields)==len(set(fields)) and
|
|
233
|
+
set(fields)<={'id','name','type','path','revision'},'query_cost_exceeded')
|
|
234
|
+
return 'discovery.read_query',{'parent':query['parent'],
|
|
235
|
+
'collection':query['collection'],'limit':int(limit),'fields':fields}
|
|
236
|
+
require(bool(query.get('root')) and 'expand' in query,'invalid_nested_query')
|
|
237
|
+
expanded=query['expand'].split(',')
|
|
238
|
+
require(expanded and len(expanded)==len(set(expanded)) and
|
|
239
|
+
set(expanded)<={'children','replies'},'invalid_nested_query')
|
|
240
|
+
first=query.get('first','10')
|
|
241
|
+
nested_first=query.get('nested_first','5')
|
|
242
|
+
require(first.isdecimal() and 1<=int(first)<=10 and nested_first.isdecimal() and
|
|
243
|
+
1<=int(nested_first)<=10 and int(first)*len(expanded)*(int(nested_first)+1)<=100,
|
|
244
|
+
'query_cost_exceeded')
|
|
245
|
+
base={key:value for key,value in query.items() if key in
|
|
246
|
+
{'root','select','filter','sort','first'}}
|
|
247
|
+
base['first']=first
|
|
248
|
+
operation,args=compile_read_query(base,service)
|
|
249
|
+
args.update(expand=expanded,nested_first=int(nested_first))
|
|
250
|
+
return operation,args
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def compile_read_query_v3(query,service):
|
|
254
|
+
require(set(query)<={'version','root','select','filter','sort','first','after','tree'},
|
|
255
|
+
'unknown_query_parameter')
|
|
256
|
+
require(query.get('version','3')=='3','invalid_read_query_version')
|
|
257
|
+
if 'after' in query:
|
|
258
|
+
require(set(query)<={'version','after'},'cursor_query_mismatch')
|
|
259
|
+
saved,_=service.cursors.inspect_page(query['after'],service.clock())
|
|
260
|
+
require(saved.get('operation')=='discovery.read_query' and
|
|
261
|
+
read_query_version(saved.get('arguments',{}))==3,'cursor_kind_mismatch')
|
|
262
|
+
return 'discovery.read_query',{'cursor':query['after']}
|
|
263
|
+
base={key:value for key,value in query.items() if key not in {'version','tree'}}
|
|
264
|
+
operation,args=compile_read_query(base,service)
|
|
265
|
+
args['query_version']=3
|
|
266
|
+
if 'tree' in query:
|
|
267
|
+
args['expand']=loads(query['tree'])
|
|
268
|
+
service.registry.validate(service.registry.operation(operation,3).input_schema,args)
|
|
269
|
+
return operation,args
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def decode_read_query_path(raw_path,version=b'1'):
|
|
273
|
+
prefix=raw_path.split(b'/',3)[1]
|
|
274
|
+
segments=READ_QUERY_V2_SEGMENTS if version==b'2' else READ_QUERY_V1_SEGMENTS
|
|
275
|
+
values,proof=decode_query_path(raw_path,prefix,segments,version)
|
|
276
|
+
sort={code:name for name,code in READ_QUERY_V1_SORT.items()}
|
|
277
|
+
fields={code:name for name,code in READ_QUERY_V1_FIELDS.items()}
|
|
278
|
+
query={}
|
|
279
|
+
if 'root' in values: query['root']=values['root']
|
|
280
|
+
if 'type' in values: query['filter']='type:'+values['type']
|
|
281
|
+
if 'sort' in values:
|
|
282
|
+
require(values['sort'] in sort,'invalid_sort')
|
|
283
|
+
query['sort']=sort[values['sort']]
|
|
284
|
+
if 'fields' in values:
|
|
285
|
+
codes=values['fields'].split(',')
|
|
286
|
+
require(all(code in fields for code in codes),'query_cost_exceeded')
|
|
287
|
+
query['select']=','.join(fields[code] for code in codes)
|
|
288
|
+
if 'first' in values: query['first']=values['first']
|
|
289
|
+
if 'after' in values: query['after']=values['after']
|
|
290
|
+
if version==b'2':
|
|
291
|
+
for name in ('expand','nested_first','collection','parent','limit'):
|
|
292
|
+
if name in values:query[name]=values[name]
|
|
293
|
+
return query,proof
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def decode_search_query_path(raw_path):
|
|
297
|
+
prefix=raw_path.split(b'/',3)[1]
|
|
298
|
+
return decode_query_path(raw_path,prefix,SEARCH_QUERY_V1_SEGMENTS)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def compile_lexical_search(query):
|
|
302
|
+
require(set(query)<={'scope','terms','exact','not_terms','mode','field','type',
|
|
303
|
+
'owner','author','tag','state','created_after','created_before',
|
|
304
|
+
'updated_after','updated_before','has_attachment','order','limit',
|
|
305
|
+
'cursor','snippet','explain','fields','facets','source_kind',
|
|
306
|
+
'relation_type','suggest','depth','recursive'},
|
|
307
|
+
'unknown_query_parameter')
|
|
308
|
+
if 'cursor' in query:
|
|
309
|
+
require(set(query)=={'cursor'},'cursor_query_mismatch')
|
|
310
|
+
return {'cursor':query['cursor']}
|
|
311
|
+
require(bool(query.get('scope')),'search_scope_required')
|
|
312
|
+
args=dict(query)
|
|
313
|
+
for name in ('limit','depth'):
|
|
314
|
+
if name in args:
|
|
315
|
+
require(args[name].isdecimal() and
|
|
316
|
+
(0<=int(args[name])<=5 if name=='depth' else 1<=int(args[name])<=100),
|
|
317
|
+
'query_cost_exceeded')
|
|
318
|
+
args[name]=int(args[name])
|
|
319
|
+
for name in ('snippet','has_attachment','recursive','suggest'):
|
|
320
|
+
if name in args:
|
|
321
|
+
require(args[name] in {'0','1'},'invalid_search_flag')
|
|
322
|
+
args[name]=args[name]=='1'
|
|
323
|
+
if 'fields' in args:
|
|
324
|
+
args['fields']=args['fields'].split(',')
|
|
325
|
+
if 'facets' in args:
|
|
326
|
+
facets=args['facets'].split(',')
|
|
327
|
+
require(facets and len(facets)==len(set(facets)) and
|
|
328
|
+
all(name in {'type','tag'} for name in facets),'invalid_search_facets')
|
|
329
|
+
args['facets']=facets
|
|
330
|
+
return args
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def decode_search_v2_path(raw_path,version=b'2'):
|
|
334
|
+
prefix=raw_path.split(b'/',3)[1]
|
|
335
|
+
segments=(SEARCH_V4_SEGMENTS if version==b'4' else
|
|
336
|
+
SEARCH_V3_SEGMENTS if version==b'3' else SEARCH_V2_SEGMENTS)
|
|
337
|
+
values,proof=decode_query_path(raw_path,prefix,segments,version)
|
|
338
|
+
modes={'a':'all','n':'any'}
|
|
339
|
+
fields={'a':'all','b':'body','n':'name','m':'metadata'}
|
|
340
|
+
order={'r':'relevance','u':'updated','c':'created','n':'name'}
|
|
341
|
+
if 'mode' in values:
|
|
342
|
+
require(values['mode'] in modes,'invalid_search_mode')
|
|
343
|
+
values['mode']=modes[values['mode']]
|
|
344
|
+
if 'field' in values:
|
|
345
|
+
require(values['field'] in fields,'invalid_search_field')
|
|
346
|
+
values['field']=fields[values['field']]
|
|
347
|
+
if 'order' in values:
|
|
348
|
+
require(values['order'] in order,'invalid_search_order')
|
|
349
|
+
values['order']=order[values['order']]
|
|
350
|
+
if 'explain' in values:
|
|
351
|
+
require(values['explain']=='c','invalid_search_explain')
|
|
352
|
+
values['explain']='compact'
|
|
353
|
+
if 'snippet' in values:
|
|
354
|
+
require(values['snippet'] in {'0','1','c'},'invalid_search_flag')
|
|
355
|
+
values['snippet']='1' if values['snippet']=='c' else values['snippet']
|
|
356
|
+
return values,proof
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def search_path_from_args(args):
|
|
360
|
+
parts=['/_s/q/1']
|
|
361
|
+
for name,code in SEARCH_QUERY_V1_SEGMENTS.items():
|
|
362
|
+
if name in args and args[name] not in {None,''}:
|
|
363
|
+
parts.extend((code,quote(str(args[name]),safe=',')))
|
|
364
|
+
return '/'.join(parts)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def path_read_proof(encoded,operation,args,service,limit):
|
|
368
|
+
packet=path_packet(encoded.decode('ascii'),'j',limit)
|
|
369
|
+
require(isinstance(packet.proof,SignatureProof),'path_signature_required')
|
|
370
|
+
require(packet.operation==operation and canonical(packet.arguments)==canonical(args),
|
|
371
|
+
'representation_mismatch')
|
|
372
|
+
require(packet.expires_at is not None and 0<(packet.expires_at-service.clock()).total_seconds()<=60,
|
|
373
|
+
'path_proof_expiry')
|
|
374
|
+
return packet
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def json_response(value,status=200,headers=None):
|
|
378
|
+
return Response(canonical(value),status_code=status,media_type='application/json',headers={**BASE_HEADERS,**(headers or {})})
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def error_status(code):
|
|
382
|
+
if code=='range_not_satisfiable':return 416
|
|
383
|
+
if code in {'not_found','resource_purged','revision_not_found','csr_not_found',
|
|
384
|
+
'certificate_not_found','listing_not_found','package_not_found',
|
|
385
|
+
'bounty_not_found','order_not_found','delivery_not_found',
|
|
386
|
+
'offer_not_found'}: return 404
|
|
387
|
+
if code in {'authentication_required','invalid_token','invalid_signature','credential_revoked','credential_expired','request_expired'}: return 401
|
|
388
|
+
if code in {'permission_denied','local_only','credential_ceiling','certificate_gate','tool_certificate_required','forbidden_origin','forbidden_host','passive_client_forbidden','query_ref_principal_mismatch','cursor_principal_mismatch'}: return 403
|
|
389
|
+
if code in {'generation_conflict','revision_conflict','idempotency_conflict','chunk_conflict','constraint_conflict'}: return 409
|
|
390
|
+
if code in {'request_too_large','path_too_large','response_too_large','use_transfer','part_too_large'}: return 413
|
|
391
|
+
if code in {'method_not_allowed','effect_mismatch'}: return 405
|
|
392
|
+
if code=='secure_channel_required': return 400
|
|
393
|
+
if code in {'server_busy','issuer_not_ready','dependency_unavailable','service_restart_required','writes_paused'}: return 503
|
|
394
|
+
if code=='storage_capacity_exceeded': return 507
|
|
395
|
+
if code=='internal_error': return 500
|
|
396
|
+
return 400
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
async def body_bytes(request,limit):
|
|
400
|
+
length=request.headers.get('content-length')
|
|
401
|
+
if length is not None:
|
|
402
|
+
require(length.isdecimal() and int(length)<=limit,'request_too_large')
|
|
403
|
+
body=bytearray()
|
|
404
|
+
async for data in request.stream():
|
|
405
|
+
require(len(body)+len(data)<=limit,'request_too_large')
|
|
406
|
+
body.extend(data)
|
|
407
|
+
encoding=request.headers.get('content-encoding','identity')
|
|
408
|
+
require(encoding in {'identity','gzip'},'unknown_encoding')
|
|
409
|
+
return gunzip(bytes(body),limit) if encoding=='gzip' else bytes(body)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def describe_resource(data):
|
|
413
|
+
if 'content' in data:
|
|
414
|
+
content=data['content']
|
|
415
|
+
if isinstance(content,str):
|
|
416
|
+
links=data.get('links',{})
|
|
417
|
+
navigation=' '.join(f"[{rel}]({link['path']})" for rel,link in links.items()
|
|
418
|
+
if isinstance(link,dict) and link.get('path'))
|
|
419
|
+
return content.rstrip('\n')+'\n\n'+navigation+'\n' if navigation else content
|
|
420
|
+
from msg.core.template_dsl import render_values
|
|
421
|
+
if isinstance(content,dict) and 'template_id' in content and 'values' in content:
|
|
422
|
+
return render_values(content['values'])
|
|
423
|
+
return '```json\n'+canonical(content).decode()+'\n```\n'
|
|
424
|
+
title=data.get('name',data.get('id','msg.lmm.best'))
|
|
425
|
+
output=['# '+str(title),'']
|
|
426
|
+
for item in data.get('items',[]):
|
|
427
|
+
name=str(item.get('name',item['id'])).replace('[','\\[').replace(']','\\]')
|
|
428
|
+
output.append(f"- [{name}]({item.get('path','/_id/'+item['id'])})")
|
|
429
|
+
if 'items' not in data:
|
|
430
|
+
output+=['```json',canonical(data).decode(),'```']
|
|
431
|
+
if data.get('list_operation'):
|
|
432
|
+
output+=['',f"[Query directory]({data['list_operation']})"]
|
|
433
|
+
return '\n'.join(output)+'\n'
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def parse_view(path):
|
|
437
|
+
parts=path.strip('/').split('/')
|
|
438
|
+
view='markdown';revision=None
|
|
439
|
+
if parts and parts[-1] in {'json','meta','raw','history'}:
|
|
440
|
+
view=parts.pop()
|
|
441
|
+
if len(parts)>=2 and parts[-2]=='revisions':
|
|
442
|
+
revision=parts.pop();parts.pop()
|
|
443
|
+
return '/'+('/'.join(parts)),view,revision
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def parse_stable_view(path,raw_path):
|
|
447
|
+
if not path.startswith(('/_r/','/_read/')):
|
|
448
|
+
return None
|
|
449
|
+
# Stable ID paths are ASCII identifiers, with no alternate percent spelling.
|
|
450
|
+
try:
|
|
451
|
+
require(raw_path.decode('ascii')==path and b'%' not in raw_path,'not_found')
|
|
452
|
+
except UnicodeDecodeError as exc:
|
|
453
|
+
raise Failure('not_found') from exc
|
|
454
|
+
match=re.fullmatch(r'/_r(?:ead)?/([A-Za-z0-9_.:-]{1,160})/(json|meta|raw|history)',path)
|
|
455
|
+
if match:
|
|
456
|
+
rid,view=match.groups()
|
|
457
|
+
return '/_id/'+rid,view,None
|
|
458
|
+
match=re.fullmatch(r'/_r(?:ead)?/([A-Za-z0-9_.:-]{1,160})/rev/([A-Za-z0-9_.:-]{1,160})',path)
|
|
459
|
+
require(match is not None,'not_found')
|
|
460
|
+
rid,revision=match.groups()
|
|
461
|
+
return '/_id/'+rid,'markdown',revision
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def create_app(service):
|
|
465
|
+
mcp=MCPServer(service)
|
|
466
|
+
graphql_adapter=None
|
|
467
|
+
short_codes=None
|
|
468
|
+
|
|
469
|
+
@asynccontextmanager
|
|
470
|
+
async def lifespan(app):
|
|
471
|
+
if not service._loaded:
|
|
472
|
+
await service.load()
|
|
473
|
+
yield
|
|
474
|
+
await service.close()
|
|
475
|
+
|
|
476
|
+
async def dispatch(request:Request):
|
|
477
|
+
nonlocal graphql_adapter, short_codes
|
|
478
|
+
try:
|
|
479
|
+
limits=service.settings.server.limits
|
|
480
|
+
raw_path=request.scope.get('raw_path') or request.scope['path'].encode('utf-8')
|
|
481
|
+
require_safe_request_target(raw_path,request.scope.get('query_string',b''),
|
|
482
|
+
maximum=limits.max_path_bytes)
|
|
483
|
+
expected=urlsplit(service.settings.service_url)
|
|
484
|
+
require_matching_host(request.headers.getlist('host'),expected)
|
|
485
|
+
require('x-http-method-override' not in request.headers and
|
|
486
|
+
'x-method-override' not in request.headers,'method_not_allowed')
|
|
487
|
+
if request.url.path.startswith(('/@','/&')):
|
|
488
|
+
from msg.extensions.hosting import serve_hosted
|
|
489
|
+
hosted=await serve_hosted(service,request)
|
|
490
|
+
if hosted is not None:
|
|
491
|
+
return hosted
|
|
492
|
+
origin=request.headers.get('origin')
|
|
493
|
+
if origin is not None:
|
|
494
|
+
require(origin.rstrip('/')==f'{expected.scheme}://{expected.netloc}','forbidden_origin')
|
|
495
|
+
path=request.url.path
|
|
496
|
+
native=re.fullmatch(r'(/[@&][^/]+/[^/]+\.git)/(.*)',path)
|
|
497
|
+
if native:
|
|
498
|
+
require(service.registry.operation('git.refs').effect=='read','effect_mismatch')
|
|
499
|
+
from msg.extensions.repositories import NativeGitStore
|
|
500
|
+
if native.group(2).startswith('info/lfs/'):
|
|
501
|
+
require(service.registry.operation('git.lfs_read').effect=='read' and
|
|
502
|
+
service.registry.operation('git.lfs_read_batch').effect=='read',
|
|
503
|
+
'effect_mismatch')
|
|
504
|
+
return await NativeGitStore(service).http_lfs(request,native.group(1),
|
|
505
|
+
native.group(2)[9:])
|
|
506
|
+
return await NativeGitStore(service).http(request,native.group(1),native.group(2))
|
|
507
|
+
# git-lfs derives <remote>.git/info/lfs even when the advertised
|
|
508
|
+
# push URL is /-/git/<id>; both spellings remain inside /-/.
|
|
509
|
+
lfs_write=re.fullmatch(r'/-/git/([A-Za-z0-9_-]{1,128})(?:\.git)?/info/lfs/(objects(?:/batch|/[0-9a-f]{64}/[0-9]+))',path)
|
|
510
|
+
if lfs_write:
|
|
511
|
+
require(raw_path==path.encode('ascii'),'not_found')
|
|
512
|
+
from msg.extensions.repositories import NativeGitStore
|
|
513
|
+
return await NativeGitStore(service).http_lfs(request,*lfs_write.groups(),write=True)
|
|
514
|
+
git_push=re.fullmatch(r'/-/git/([A-Za-z0-9_-]{1,128})/(info/refs|git-receive-pack)',path)
|
|
515
|
+
if git_push:
|
|
516
|
+
require(raw_path==path.encode('ascii'),'not_found')
|
|
517
|
+
from msg.extensions.repositories import NativeGitStore
|
|
518
|
+
return await NativeGitStore(service).http_push(request,*git_push.groups())
|
|
519
|
+
if path.startswith(('/!','/~','/run/j/','/run/gz/')) or path=='/mcp':
|
|
520
|
+
raise Failure('not_found')
|
|
521
|
+
if request.method=='OPTIONS':
|
|
522
|
+
return Response(status_code=405,headers=BASE_HEADERS)
|
|
523
|
+
if path in {'/rss','/-/rss'}:
|
|
524
|
+
require(request.method in {'GET','HEAD'},'method_not_allowed')
|
|
525
|
+
require(service.registry.operation('discovery.feed').effect=='read','effect_mismatch')
|
|
526
|
+
from msg.extensions.rss import render_feed
|
|
527
|
+
args=dict(request.query_params)
|
|
528
|
+
require(set(args)<={'parent','limit','cursor'},'unknown_query_parameter')
|
|
529
|
+
if 'limit' in args:
|
|
530
|
+
require(args['limit'].isdigit(),'invalid_limit')
|
|
531
|
+
args['limit']=int(args['limit'])
|
|
532
|
+
header=request.headers.get('x-msg-request')
|
|
533
|
+
packet=path_packet(header,'j',limits.max_request_bytes) if header else request_for('discovery.feed',args,service.settings.service_url)
|
|
534
|
+
require(packet.operation=='discovery.feed' and dict(packet.arguments)==args,'representation_mismatch')
|
|
535
|
+
result=await service.executor.execute(packet)
|
|
536
|
+
if result.error:return json_response(result_wire(result),error_status(result.error.code))
|
|
537
|
+
payload=render_feed(result.data)
|
|
538
|
+
require(len(payload)<=limits.max_response_bytes,'response_too_large')
|
|
539
|
+
return Response(b'' if request.method=='HEAD' else payload,media_type='application/rss+xml',
|
|
540
|
+
headers={**BASE_HEADERS,'Content-Length':str(len(payload)),'Cache-Control':'private, no-cache'})
|
|
541
|
+
if path.startswith('/latest/'):
|
|
542
|
+
require(request.method in {'GET','HEAD'},'method_not_allowed')
|
|
543
|
+
require(service.registry.operation('discovery.list').effect=='read','effect_mismatch')
|
|
544
|
+
kind=path.removeprefix('/latest/')
|
|
545
|
+
service.registry.resource_type(kind,1)
|
|
546
|
+
packet=request_for('discovery.list',{'type':kind,'sort':'time','direction':'desc','limit':1},service.settings.service_url)
|
|
547
|
+
result=await service.executor.execute(packet)
|
|
548
|
+
return json_response(result_wire(result),error_status(result.error.code) if result.error else 200)
|
|
549
|
+
if path=='/healthz':
|
|
550
|
+
require(request.method in {'GET','HEAD'},'method_not_allowed')
|
|
551
|
+
return json_response({'status':'ok' if service._loaded else 'not_ready'},200 if service._loaded else 503)
|
|
552
|
+
sync_path=re.fullmatch(rb'/_r(?:ead)?/s/(start|[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+)'
|
|
553
|
+
rb'(?:/p/([A-Za-z0-9_-]+))?',raw_path)
|
|
554
|
+
if sync_path:
|
|
555
|
+
require(request.method in {'GET','HEAD'},'method_not_allowed')
|
|
556
|
+
require(not request.url.query,'unknown_query_parameter')
|
|
557
|
+
cursor=sync_path.group(1).decode('ascii')
|
|
558
|
+
args={} if cursor=='start' else {'cursor':cursor}
|
|
559
|
+
operation='communication.sync'
|
|
560
|
+
proof=sync_path.group(2)
|
|
561
|
+
header=request.headers.get('x-msg-request')
|
|
562
|
+
require(not (proof and header),'ambiguous_proof')
|
|
563
|
+
if proof:
|
|
564
|
+
packet=path_read_proof(proof,operation,args,service,limits.max_request_bytes)
|
|
565
|
+
elif header:
|
|
566
|
+
packet=path_packet(header,'j',limits.max_request_bytes)
|
|
567
|
+
require(packet.operation==operation and canonical(packet.arguments)==canonical(args),
|
|
568
|
+
'representation_mismatch')
|
|
569
|
+
else:
|
|
570
|
+
packet=request_for(operation,args,service.settings.service_url,source='manual')
|
|
571
|
+
result=await service.executor.execute(packet,entry='network')
|
|
572
|
+
if result.error:
|
|
573
|
+
return json_response(result_wire(result),error_status(result.error.code))
|
|
574
|
+
value=wire(result.data)
|
|
575
|
+
payload=canonical(value)
|
|
576
|
+
require(len(payload)<=limits.max_response_bytes,'response_too_large')
|
|
577
|
+
etag='"'+digest(value)[7:]+'"'
|
|
578
|
+
headers={**BASE_HEADERS,'ETag':etag,'Cache-Control':'no-store'}
|
|
579
|
+
if request.headers.get('if-none-match')==etag:
|
|
580
|
+
return Response(status_code=304,headers=headers)
|
|
581
|
+
return Response(b'' if request.method=='HEAD' else payload,
|
|
582
|
+
media_type='application/json',headers=headers)
|
|
583
|
+
query_ref_path=re.fullmatch(rb'/_r(?:ead)?/q/([A-Za-z0-9_-]+\.[A-Za-z0-9_-]+)'
|
|
584
|
+
rb'(?:/c/([A-Za-z0-9_-]+\.[A-Za-z0-9_-]+))?'
|
|
585
|
+
rb'(?:/p/([A-Za-z0-9_-]+))?',raw_path)
|
|
586
|
+
if query_ref_path:
|
|
587
|
+
require(request.method in {'GET','HEAD'},'method_not_allowed')
|
|
588
|
+
require(not request.url.query,'unknown_query_parameter')
|
|
589
|
+
token=query_ref_path.group(1).decode('ascii')
|
|
590
|
+
args={'query_ref':token}
|
|
591
|
+
if query_ref_path.group(2):
|
|
592
|
+
args['cursor']=query_ref_path.group(2).decode('ascii')
|
|
593
|
+
operation='transfer.query_get'
|
|
594
|
+
proof=query_ref_path.group(3)
|
|
595
|
+
header=request.headers.get('x-msg-request')
|
|
596
|
+
require(not (proof and header),'ambiguous_proof')
|
|
597
|
+
if proof:
|
|
598
|
+
packet=path_read_proof(proof,operation,args,service,limits.max_request_bytes)
|
|
599
|
+
elif header:
|
|
600
|
+
packet=path_packet(header,'j',limits.max_request_bytes)
|
|
601
|
+
require(packet.operation==operation and canonical(packet.arguments)==canonical(args),
|
|
602
|
+
'representation_mismatch')
|
|
603
|
+
else:
|
|
604
|
+
packet=request_for(operation,args,service.settings.service_url,source='manual')
|
|
605
|
+
result=await service.executor.execute(packet,entry='network')
|
|
606
|
+
if result.error:
|
|
607
|
+
return json_response(result_wire(result),error_status(result.error.code))
|
|
608
|
+
value=wire(result.data)
|
|
609
|
+
payload=canonical(value)
|
|
610
|
+
require(len(payload)<=limits.max_response_bytes,'response_too_large')
|
|
611
|
+
etag='"'+digest(value)[7:]+'"'
|
|
612
|
+
headers={**BASE_HEADERS,'ETag':etag,'Cache-Control':'no-store'}
|
|
613
|
+
if request.headers.get('if-none-match')==etag:
|
|
614
|
+
return Response(status_code=304,headers=headers)
|
|
615
|
+
return Response(b'' if request.method=='HEAD' else payload,
|
|
616
|
+
media_type='application/json',headers=headers)
|
|
617
|
+
link_path=raw_path
|
|
618
|
+
link_proof=None
|
|
619
|
+
link_parts=raw_path.split(b'/')
|
|
620
|
+
if len(link_parts)>=2 and link_parts[-2]==b'p' and raw_path.startswith((b'/_read/',b'/_r/')):
|
|
621
|
+
link_path=b'/'.join(link_parts[:-2])
|
|
622
|
+
link_proof=link_parts[-1]
|
|
623
|
+
link_view=re.fullmatch(rb'/_r(?:ead)?/([A-Za-z0-9_.:-]{1,160})/'
|
|
624
|
+
rb'(links|l/(self|[a-z])|diff/([A-Za-z0-9_.:-]{1,160})'
|
|
625
|
+
rb'(?:/([A-Za-z0-9_.:-]{1,160}))?(?:/o/([0-9]+))?)',link_path)
|
|
626
|
+
if link_view:
|
|
627
|
+
require(request.method in {'GET','HEAD'},'method_not_allowed')
|
|
628
|
+
rid=link_view.group(1).decode('ascii')
|
|
629
|
+
rel=link_view.group(3).decode('ascii') if link_view.group(3) else None
|
|
630
|
+
old=link_view.group(4).decode('ascii') if link_view.group(4) else None
|
|
631
|
+
new=link_view.group(5).decode('ascii') if link_view.group(5) else None
|
|
632
|
+
offset=link_view.group(6).decode('ascii') if link_view.group(6) else None
|
|
633
|
+
pairs=request.query_params.multi_items()
|
|
634
|
+
require(len(pairs)==len({key for key,_ in pairs}),'duplicate_query_parameter')
|
|
635
|
+
query=dict(pairs)
|
|
636
|
+
if old is not None or rel=='d':
|
|
637
|
+
require(not query,'unknown_query_parameter')
|
|
638
|
+
operation='discovery.diff_view'
|
|
639
|
+
args={'id':rid}
|
|
640
|
+
if rel=='d':
|
|
641
|
+
args['previous']=True
|
|
642
|
+
elif new is None:
|
|
643
|
+
args['known_revision']=old
|
|
644
|
+
else:
|
|
645
|
+
args.update(old_revision=old,new_revision=new)
|
|
646
|
+
if offset is not None:
|
|
647
|
+
require(new is not None,'invalid_diff_range')
|
|
648
|
+
args['offset']=int(offset)
|
|
649
|
+
else:
|
|
650
|
+
require(rel is None or rel in {'self','t','a','r','p','c','f','q','b','h','v'},
|
|
651
|
+
'unknown_link_relation')
|
|
652
|
+
require(set(query)<=({'limit'} if rel in {'c','f','q','b','h'} else set()),
|
|
653
|
+
'unknown_query_parameter')
|
|
654
|
+
operation='discovery.links'
|
|
655
|
+
args={'id':rid}
|
|
656
|
+
if rel is not None:args['rel']=rel
|
|
657
|
+
if 'limit' in query:
|
|
658
|
+
require(query['limit'].isdecimal() and 1<=int(query['limit'])<=100,
|
|
659
|
+
'invalid_limit')
|
|
660
|
+
args['limit']=int(query['limit'])
|
|
661
|
+
header=request.headers.get('x-msg-request')
|
|
662
|
+
require(not (header and link_proof),'ambiguous_proof')
|
|
663
|
+
if link_proof is not None:
|
|
664
|
+
packet=path_read_proof(link_proof,operation,args,service,limits.max_request_bytes)
|
|
665
|
+
elif header:
|
|
666
|
+
packet=path_packet(header,'j',limits.max_request_bytes)
|
|
667
|
+
require(packet.operation==operation and canonical(packet.arguments)==canonical(args),
|
|
668
|
+
'representation_mismatch')
|
|
669
|
+
else:
|
|
670
|
+
packet=request_for(operation,args,service.settings.service_url,source='manual')
|
|
671
|
+
result=await service.executor.execute(packet,entry='network')
|
|
672
|
+
if result.error:
|
|
673
|
+
return json_response(result_wire(result),error_status(result.error.code))
|
|
674
|
+
value=wire(result.data)
|
|
675
|
+
payload=canonical(value)
|
|
676
|
+
require(len(payload)<=limits.max_response_bytes,'response_too_large')
|
|
677
|
+
etag='"'+digest(value)[7:]+'"'
|
|
678
|
+
headers={**BASE_HEADERS,'ETag':etag,'Cache-Control':'no-store'}
|
|
679
|
+
if request.headers.get('if-none-match')==etag:
|
|
680
|
+
return Response(status_code=304,headers=headers)
|
|
681
|
+
return Response(b'' if request.method=='HEAD' else payload,
|
|
682
|
+
media_type='application/json',headers=headers)
|
|
683
|
+
segment=re.fullmatch(rb'/_r(?:ead)?/([A-Za-z0-9_.:-]{1,160})/read',raw_path)
|
|
684
|
+
if segment:
|
|
685
|
+
require(request.method in {'GET','HEAD'},'method_not_allowed')
|
|
686
|
+
pairs=request.query_params.multi_items()
|
|
687
|
+
require(len(pairs)==len({key for key,_ in pairs}),'duplicate_query_parameter')
|
|
688
|
+
query=dict(pairs)
|
|
689
|
+
require(set(query)<={'max_bytes'},'unknown_query_parameter')
|
|
690
|
+
args={'id':segment.group(1).decode('ascii')}
|
|
691
|
+
if 'max_bytes' in query:
|
|
692
|
+
require(query['max_bytes'].isdecimal() and
|
|
693
|
+
32<=int(query['max_bytes'])<=8192,'query_cost_exceeded')
|
|
694
|
+
args['max_bytes']=int(query['max_bytes'])
|
|
695
|
+
operation='discovery.read_segment'
|
|
696
|
+
header=request.headers.get('x-msg-request')
|
|
697
|
+
if header:
|
|
698
|
+
packet=path_packet(header,'j',limits.max_request_bytes)
|
|
699
|
+
require(packet.operation==operation and canonical(packet.arguments)==canonical(args),
|
|
700
|
+
'representation_mismatch')
|
|
701
|
+
else:
|
|
702
|
+
packet=request_for(operation,args,service.settings.service_url,source='manual')
|
|
703
|
+
result=await service.executor.execute(packet,entry='network')
|
|
704
|
+
if result.error:
|
|
705
|
+
return json_response(result_wire(result),error_status(result.error.code))
|
|
706
|
+
payload=canonical(wire(result.data))
|
|
707
|
+
require(len(payload)<=limits.max_response_bytes,'response_too_large')
|
|
708
|
+
return Response(b'' if request.method=='HEAD' else payload,media_type='application/json',
|
|
709
|
+
headers=BASE_HEADERS)
|
|
710
|
+
path_query_v1=raw_path.startswith((b'/_read/q/1/',b'/_r/q/1/'))
|
|
711
|
+
path_query_v2=raw_path.startswith((b'/_read/q/2/',b'/_r/q/2/'))
|
|
712
|
+
path_query_v3=raw_path.startswith((b'/_read/q/3/',b'/_r/q/3/'))
|
|
713
|
+
path_query=path_query_v1 or path_query_v2 or path_query_v3
|
|
714
|
+
if path in {'/_read/query','/_r/query'} or path_query or raw_path.startswith((b'/_read/c/',b'/_r/c/')):
|
|
715
|
+
require(request.method in {'GET','HEAD'},'method_not_allowed')
|
|
716
|
+
continuation=raw_path.startswith((b'/_read/c/',b'/_r/c/'))
|
|
717
|
+
contract_version=1
|
|
718
|
+
require(not (continuation or path_query) or not request.url.query,
|
|
719
|
+
'unknown_query_parameter')
|
|
720
|
+
path_proof=None
|
|
721
|
+
if continuation:
|
|
722
|
+
segments=raw_path.split(b'/')
|
|
723
|
+
require(len(segments) in {4,6} and segments[2]==b'c' and
|
|
724
|
+
re.fullmatch(rb'[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+',segments[3]) is not None,
|
|
725
|
+
'invalid_cursor')
|
|
726
|
+
if len(segments)==6:
|
|
727
|
+
require(segments[4]==b'p','invalid_path')
|
|
728
|
+
path_proof=segments[5]
|
|
729
|
+
cursor=segments[3].decode('ascii')
|
|
730
|
+
try:
|
|
731
|
+
kind=service.cursors.inspect(cursor).get('kind')
|
|
732
|
+
if kind=='read-segment':
|
|
733
|
+
service.cursors.inspect_read(cursor,service.clock())
|
|
734
|
+
operation='discovery.read_segment'
|
|
735
|
+
else:
|
|
736
|
+
query,_=service.cursors.inspect_page(cursor,service.clock())
|
|
737
|
+
operation=query.get('operation')
|
|
738
|
+
require(operation in {'discovery.read_query','discovery.links',
|
|
739
|
+
'discovery.lexical_search','communication.following'},
|
|
740
|
+
'cursor_kind_mismatch')
|
|
741
|
+
if operation=='discovery.lexical_search':
|
|
742
|
+
saved_args=query.get('arguments',{})
|
|
743
|
+
contract_version=(4 if 'suggest' in saved_args else
|
|
744
|
+
3 if any(name in saved_args for name in
|
|
745
|
+
('source_kind','relation_type')) else
|
|
746
|
+
2 if 'facets' in saved_args else 1)
|
|
747
|
+
elif operation=='discovery.read_query':
|
|
748
|
+
contract_version=read_query_version(query.get('arguments',{}))
|
|
749
|
+
except Failure as exc:
|
|
750
|
+
if exc.code=='invalid_base64':
|
|
751
|
+
raise Failure('invalid_cursor') from exc
|
|
752
|
+
raise
|
|
753
|
+
args={'cursor':cursor}
|
|
754
|
+
else:
|
|
755
|
+
if path_query_v3:
|
|
756
|
+
args,path_proof=decode_read_tree_path(raw_path)
|
|
757
|
+
query={'version':'3'}
|
|
758
|
+
elif path_query:
|
|
759
|
+
query,path_proof=decode_read_query_path(raw_path,
|
|
760
|
+
b'2' if path_query_v2 else b'1')
|
|
761
|
+
else:
|
|
762
|
+
pairs=request.query_params.multi_items()
|
|
763
|
+
require(len(pairs)==len({key for key,_ in pairs}),
|
|
764
|
+
'duplicate_query_parameter')
|
|
765
|
+
query=dict(pairs)
|
|
766
|
+
contract_version=(3 if path_query_v3 or query.get('version')=='3' else
|
|
767
|
+
2 if path_query_v2 or query.get('version')=='2' else 1)
|
|
768
|
+
if path_query_v3:
|
|
769
|
+
operation='discovery.read_query'
|
|
770
|
+
else:
|
|
771
|
+
compiler={1:compile_read_query,2:compile_read_query_v2,3:compile_read_query_v3}[contract_version]
|
|
772
|
+
operation,args=compiler(query,service)
|
|
773
|
+
require(service.registry.operation(operation,contract_version).effect=='read',
|
|
774
|
+
'effect_mismatch')
|
|
775
|
+
header=request.headers.get('x-msg-request')
|
|
776
|
+
require(not (header and path_proof),'ambiguous_proof')
|
|
777
|
+
if path_proof is not None:
|
|
778
|
+
packet=path_read_proof(path_proof,operation,args,service,limits.max_request_bytes)
|
|
779
|
+
elif header:
|
|
780
|
+
packet=path_packet(header,'j',limits.max_request_bytes)
|
|
781
|
+
require(packet.operation==operation and canonical(packet.arguments)==canonical(args),
|
|
782
|
+
'representation_mismatch')
|
|
783
|
+
else:
|
|
784
|
+
packet=request_for(operation,args,service.settings.service_url,
|
|
785
|
+
source='manual',contract_version=contract_version)
|
|
786
|
+
require(packet.contract_version==contract_version,'representation_mismatch')
|
|
787
|
+
result=await service.executor.execute(packet,entry='network')
|
|
788
|
+
if result.error:
|
|
789
|
+
return json_response(result_wire(result),error_status(result.error.code))
|
|
790
|
+
value=wire(result.data)
|
|
791
|
+
payload=canonical(value)
|
|
792
|
+
require(len(payload)<=limits.max_response_bytes,'response_too_large')
|
|
793
|
+
etag='"'+digest(value)[7:]+'"'
|
|
794
|
+
headers={**BASE_HEADERS,'ETag':etag,'Cache-Control':'no-store'}
|
|
795
|
+
if request.headers.get('if-none-match')==etag:
|
|
796
|
+
return Response(status_code=304,headers=headers)
|
|
797
|
+
return Response(b'' if request.method=='HEAD' else payload,media_type='application/json',
|
|
798
|
+
headers=headers)
|
|
799
|
+
grep_path=raw_path.startswith((b'/_search/g/1/',b'/_s/g/1/'))
|
|
800
|
+
if path in {'/_search/grep','/_s/grep'} or grep_path:
|
|
801
|
+
require(request.method in {'GET','HEAD'},'method_not_allowed')
|
|
802
|
+
if grep_path:
|
|
803
|
+
require(not request.url.query,'unknown_query_parameter')
|
|
804
|
+
prefix=raw_path.split(b'/',3)[1]
|
|
805
|
+
query,path_proof=decode_query_path(raw_path,prefix,GREP_V1_SEGMENTS,b'1',b'g')
|
|
806
|
+
else:
|
|
807
|
+
pairs=request.query_params.multi_items()
|
|
808
|
+
require(len(pairs)==len({key for key,_ in pairs}),
|
|
809
|
+
'duplicate_query_parameter')
|
|
810
|
+
query=dict(pairs)
|
|
811
|
+
path_proof=None
|
|
812
|
+
require(set(query)<={'scope','pattern','regex','glob','exclude_glob','case_sensitive',
|
|
813
|
+
'before','after','max_matches','max_files','files_with_matches','count_only'},
|
|
814
|
+
'unknown_query_parameter')
|
|
815
|
+
require(bool(query.get('scope')) and bool(query.get('pattern')),
|
|
816
|
+
'grep_scope_and_pattern_required')
|
|
817
|
+
args=dict(query)
|
|
818
|
+
for name in ('regex','case_sensitive','files_with_matches','count_only'):
|
|
819
|
+
if name in args:
|
|
820
|
+
require(args[name] in {'0','1'},'invalid_grep_flag')
|
|
821
|
+
args[name]=args[name]=='1'
|
|
822
|
+
for name in ('before','after','max_matches','max_files'):
|
|
823
|
+
if name in args:
|
|
824
|
+
require(args[name].isdecimal(),'invalid_grep_limit')
|
|
825
|
+
args[name]=int(args[name])
|
|
826
|
+
operation='discovery.grep'
|
|
827
|
+
header=request.headers.get('x-msg-request')
|
|
828
|
+
require(not (header and path_proof),'ambiguous_proof')
|
|
829
|
+
if path_proof is not None:
|
|
830
|
+
packet=path_read_proof(path_proof,operation,args,service,limits.max_request_bytes)
|
|
831
|
+
elif header:
|
|
832
|
+
packet=path_packet(header,'j',limits.max_request_bytes)
|
|
833
|
+
require(packet.operation==operation and canonical(packet.arguments)==canonical(args),
|
|
834
|
+
'representation_mismatch')
|
|
835
|
+
else:
|
|
836
|
+
packet=request_for(operation,args,service.settings.service_url,source='manual')
|
|
837
|
+
result=await service.executor.execute(packet,entry='network')
|
|
838
|
+
if result.error:
|
|
839
|
+
return json_response(result_wire(result),error_status(result.error.code))
|
|
840
|
+
value=wire(result.data)
|
|
841
|
+
etag='"'+digest(value)[7:]+'"'
|
|
842
|
+
headers={**BASE_HEADERS,'ETag':etag,'Cache-Control':'no-store'}
|
|
843
|
+
if request.headers.get('if-none-match')==etag:
|
|
844
|
+
return Response(status_code=304,headers=headers)
|
|
845
|
+
return Response(b'' if request.method=='HEAD' else canonical(value),
|
|
846
|
+
media_type='application/json',headers=headers)
|
|
847
|
+
search_path=raw_path.startswith((b'/_search/q/1/',b'/_s/q/1/'))
|
|
848
|
+
lexical_path_v2=raw_path.startswith((b'/_search/q/2/',b'/_s/q/2/'))
|
|
849
|
+
lexical_path_v3=raw_path.startswith((b'/_search/q/3/',b'/_s/q/3/'))
|
|
850
|
+
lexical_path_v4=raw_path.startswith((b'/_search/q/4/',b'/_s/q/4/'))
|
|
851
|
+
lexical_path=lexical_path_v2 or lexical_path_v3 or lexical_path_v4
|
|
852
|
+
if path in {'/_search','/_s'} or search_path or lexical_path or raw_path.startswith((
|
|
853
|
+
b'/_index/by-tag/',b'/_i/by-tag/')):
|
|
854
|
+
require(request.method in {'GET','HEAD'},'method_not_allowed')
|
|
855
|
+
if search_path or lexical_path:
|
|
856
|
+
require(not request.url.query,'unknown_query_parameter')
|
|
857
|
+
query,path_proof=(decode_search_v2_path(raw_path,
|
|
858
|
+
b'4' if lexical_path_v4 else b'3' if lexical_path_v3 else b'2')
|
|
859
|
+
if lexical_path else
|
|
860
|
+
decode_search_query_path(raw_path))
|
|
861
|
+
else:
|
|
862
|
+
pairs=request.query_params.multi_items()
|
|
863
|
+
require(len(pairs)==len({key for key,_ in pairs}),
|
|
864
|
+
'duplicate_query_parameter')
|
|
865
|
+
query=dict(pairs)
|
|
866
|
+
path_proof=None
|
|
867
|
+
is_index=raw_path.startswith((b'/_index/by-tag/',b'/_i/by-tag/'))
|
|
868
|
+
lexical=lexical_path or (not is_index and
|
|
869
|
+
bool(set(query)&{'terms','exact','not_terms','scope','mode','field','order',
|
|
870
|
+
'snippet','explain','has_attachment','facets',
|
|
871
|
+
'source_kind','relation_type','suggest'}))
|
|
872
|
+
if lexical:
|
|
873
|
+
args=compile_lexical_search(query)
|
|
874
|
+
operation='discovery.lexical_search'
|
|
875
|
+
else:
|
|
876
|
+
require(set(query)<=({'limit','cursor'} if is_index else
|
|
877
|
+
{'tag','query','limit','cursor'}),'unknown_query_parameter')
|
|
878
|
+
limit=query.get('limit','50')
|
|
879
|
+
require(limit.isdecimal() and 1<=int(limit)<=200,'invalid_limit')
|
|
880
|
+
args={'limit':int(limit)}
|
|
881
|
+
if is_index:
|
|
882
|
+
segments=raw_path.split(b'/')
|
|
883
|
+
require(len(segments)==4 and segments[2]==b'by-tag','not_found')
|
|
884
|
+
encoded_tag=segments[3]
|
|
885
|
+
require(re.search(rb'%(?![0-9A-Fa-f]{2})',encoded_tag) is None,
|
|
886
|
+
'invalid_tag')
|
|
887
|
+
try:
|
|
888
|
+
tag=unquote_to_bytes(encoded_tag).decode('utf-8')
|
|
889
|
+
except UnicodeDecodeError as exc:
|
|
890
|
+
raise Failure('invalid_tag') from exc
|
|
891
|
+
args['tag']=normalize_tag(tag)
|
|
892
|
+
operation='discovery.list'
|
|
893
|
+
elif not lexical:
|
|
894
|
+
if 'tag' in query:
|
|
895
|
+
args['tag']=normalize_tag(query['tag'])
|
|
896
|
+
args['query']=query.get('query','')
|
|
897
|
+
require(args['tag'] if 'tag' in args else bool(args['query']),
|
|
898
|
+
'search_query_required')
|
|
899
|
+
operation='discovery.search'
|
|
900
|
+
if 'cursor' in query and not lexical:
|
|
901
|
+
args['cursor']=query['cursor']
|
|
902
|
+
require(service.registry.operation(operation).effect=='read','effect_mismatch')
|
|
903
|
+
header=request.headers.get('x-msg-request')
|
|
904
|
+
require(not (header and path_proof),'ambiguous_proof')
|
|
905
|
+
if path_proof is not None:
|
|
906
|
+
packet=path_read_proof(path_proof,operation,args,service,limits.max_request_bytes)
|
|
907
|
+
elif header:
|
|
908
|
+
packet=path_packet(header,'j',limits.max_request_bytes)
|
|
909
|
+
require(packet.operation==operation,'operation_mismatch')
|
|
910
|
+
require(canonical(packet.arguments)==canonical(args),'representation_mismatch')
|
|
911
|
+
else:
|
|
912
|
+
packet=request_for(operation,args,service.settings.service_url,
|
|
913
|
+
source='manual',
|
|
914
|
+
contract_version=(4 if lexical and (lexical_path_v4 or
|
|
915
|
+
'suggest' in args) else
|
|
916
|
+
3 if lexical and (lexical_path_v3 or
|
|
917
|
+
'source_kind' in args or 'relation_type' in args) else
|
|
918
|
+
2 if lexical and 'facets' in args else 1))
|
|
919
|
+
result=await service.executor.execute(packet,entry='network')
|
|
920
|
+
if result.error:
|
|
921
|
+
return json_response(result_wire(result),error_status(result.error.code))
|
|
922
|
+
value=wire(result.data)
|
|
923
|
+
if value.get('cursor') and not lexical:
|
|
924
|
+
params={'limit':args['limit'],'cursor':value['cursor']}
|
|
925
|
+
if is_index:
|
|
926
|
+
value['next']='/_i/by-tag/'+quote(args['tag'],safe='')+'?'+urlencode(params)
|
|
927
|
+
else:
|
|
928
|
+
if 'tag' in args: params['tag']=args['tag']
|
|
929
|
+
if args['query']: params['query']=args['query']
|
|
930
|
+
value['next']=search_path_from_args(params)
|
|
931
|
+
require(len(canonical(value))<=limits.max_response_bytes,'response_too_large')
|
|
932
|
+
if is_index:
|
|
933
|
+
return Response(b'' if request.method=='HEAD' else canonical(value),
|
|
934
|
+
media_type='application/json',headers=BASE_HEADERS)
|
|
935
|
+
etag='"'+digest(value)[7:]+'"'
|
|
936
|
+
headers={'ETag':etag,'Cache-Control':'no-store'}
|
|
937
|
+
if request.headers.get('if-none-match')==etag:
|
|
938
|
+
return Response(status_code=304,headers={**BASE_HEADERS,**headers})
|
|
939
|
+
return Response(b'' if request.method=='HEAD' else canonical(value),
|
|
940
|
+
media_type='application/json',headers={**BASE_HEADERS,**headers})
|
|
941
|
+
if path=='/-/transfer':
|
|
942
|
+
require(not request.url.query,'unknown_query_parameter')
|
|
943
|
+
require(request.method in {'GET','HEAD','POST'},'method_not_allowed')
|
|
944
|
+
if request.method=='HEAD':
|
|
945
|
+
return Response(status_code=200,headers=BASE_HEADERS)
|
|
946
|
+
if request.method=='GET':
|
|
947
|
+
available=tuple(sorted(spec.name for spec in service.registry.operations('network')
|
|
948
|
+
if spec.name in TRANSFER_OPERATIONS))
|
|
949
|
+
return json_response({'version':1,'operations':available,
|
|
950
|
+
'request':'OperationRequest JSON via POST'})
|
|
951
|
+
packet=decode_packet(await body_bytes(request,limits.max_request_bytes),
|
|
952
|
+
limits.max_request_bytes)
|
|
953
|
+
require(packet.operation in TRANSFER_OPERATIONS,'operation_mismatch')
|
|
954
|
+
spec=service.registry.operation(packet.operation,packet.contract_version)
|
|
955
|
+
require('network' in spec.entries,'entry_not_allowed')
|
|
956
|
+
result=await service.executor.execute(packet,entry='network')
|
|
957
|
+
value=result_wire(result)
|
|
958
|
+
require(len(canonical(value))<=limits.max_response_bytes,'response_too_large')
|
|
959
|
+
return json_response(value,error_status(result.error.code) if result.error else 200)
|
|
960
|
+
if path=='/_transports':
|
|
961
|
+
require(request.method in {'GET','HEAD'},'method_not_allowed')
|
|
962
|
+
data={'version':1,'target_service':service.settings.service_url,'limits':wire(limits),
|
|
963
|
+
'recommended_part_bytes':service.settings.max_part_bytes,'encodings':['j','gz'],
|
|
964
|
+
'transports':{'http':'/-/p/operation','path_get':'/-/g/operation/j/packet',
|
|
965
|
+
'graphql':'/-/graphql','mcp_http':'/-/mcp',
|
|
966
|
+
'transfer':'/-/transfer',
|
|
967
|
+
'mcp_stdio':'msg mcp'},
|
|
968
|
+
'operations':{s.name:s.effect for s in service.registry.operations('network')},
|
|
969
|
+
'contract_digest':service.registry.catalog()['digest']}
|
|
970
|
+
return json_response(data)
|
|
971
|
+
if path=='/-/mcp':
|
|
972
|
+
require(request.method=='POST','method_not_allowed')
|
|
973
|
+
protocol=request.headers.get('mcp-protocol-version')
|
|
974
|
+
require(protocol is None or protocol in SUPPORTED_VERSIONS,'unsupported_mcp_version')
|
|
975
|
+
raw=await body_bytes(request,limits.max_request_bytes)
|
|
976
|
+
message=loads(raw)
|
|
977
|
+
output=await mcp.handle(message)
|
|
978
|
+
if output is None:
|
|
979
|
+
return Response(status_code=202,headers=BASE_HEADERS)
|
|
980
|
+
require(len(canonical(output))<=limits.max_response_bytes,'response_too_large')
|
|
981
|
+
return json_response(output,headers={'MCP-Protocol-Version':PROTOCOL_VERSION})
|
|
982
|
+
if path in {'/-/graphql','/_read/graphql','/_r/graphql'}:
|
|
983
|
+
require(request.method=='POST','method_not_allowed')
|
|
984
|
+
if graphql_adapter is None:
|
|
985
|
+
from msg.transports.graphql import GraphQLAdapter
|
|
986
|
+
graphql_adapter=GraphQLAdapter(service)
|
|
987
|
+
kind='mutation' if path=='/-/graphql' else 'query'
|
|
988
|
+
data=await graphql_adapter.handle(
|
|
989
|
+
loads(await body_bytes(request,limits.max_request_bytes)),operation_kind=kind)
|
|
990
|
+
require(len(canonical(data))<=limits.max_response_bytes,'response_too_large')
|
|
991
|
+
return json_response(data)
|
|
992
|
+
if path=='/-/d/read.query':
|
|
993
|
+
require(request.method in {'GET','HEAD'},'method_not_allowed')
|
|
994
|
+
from msg.transports.dictionary import read_query_path_document
|
|
995
|
+
document=read_query_path_document(service.registry)
|
|
996
|
+
etag='"'+digest(document)[7:]+'"'
|
|
997
|
+
headers={**BASE_HEADERS,'ETag':etag,'Cache-Control':'private, no-cache'}
|
|
998
|
+
if request.headers.get('if-none-match')==etag:
|
|
999
|
+
return Response(status_code=304,headers=headers)
|
|
1000
|
+
payload=canonical(document)
|
|
1001
|
+
require(len(payload)<=limits.max_response_bytes,'response_too_large')
|
|
1002
|
+
return Response(b'' if request.method=='HEAD' else payload,
|
|
1003
|
+
media_type='application/json',headers=headers)
|
|
1004
|
+
if path=='/-/d/search.query':
|
|
1005
|
+
require(request.method in {'GET','HEAD'},'method_not_allowed')
|
|
1006
|
+
require(service.registry.operation('discovery.lexical_search').effect=='read',
|
|
1007
|
+
'effect_mismatch')
|
|
1008
|
+
document={'version':4,'operation':'discovery.lexical_search',
|
|
1009
|
+
'segments':SEARCH_V2_SEGMENTS,'segments_v3':SEARCH_V3_SEGMENTS,
|
|
1010
|
+
'segments_v4':SEARCH_V4_SEGMENTS,
|
|
1011
|
+
'mode':{'all':'a','any':'n'},
|
|
1012
|
+
'field':{'all':'a','body':'b','name':'n','metadata':'m'},
|
|
1013
|
+
'order':{'relevance':'r','updated':'u','created':'c','name':'n'},
|
|
1014
|
+
'facets':['type','tag'],
|
|
1015
|
+
'contract_version':{'default':1,'with_facets':2,
|
|
1016
|
+
'with_source_or_relation':3,'with_suggest':4},
|
|
1017
|
+
'template':'/_search/q/2/s/{percent-encoded-scope}/t/{terms}/m/{mode}/f/{field}/n/{limit}',
|
|
1018
|
+
'template_v3':'/_search/q/3/s/{percent-encoded-scope}/t/{terms}/sk/{source-kind}/rt/{relation-type}',
|
|
1019
|
+
'template_v4':'/_search/q/4/s/{percent-encoded-scope}/t/{terms}/sg/1',
|
|
1020
|
+
'proof_suffix':'/p/{short-lived-signed-OperationRequest}'}
|
|
1021
|
+
etag='"'+digest(document)[7:]+'"'
|
|
1022
|
+
headers={**BASE_HEADERS,'ETag':etag,'Cache-Control':'private, no-cache'}
|
|
1023
|
+
if request.headers.get('if-none-match')==etag:
|
|
1024
|
+
return Response(status_code=304,headers=headers)
|
|
1025
|
+
payload=canonical(document)
|
|
1026
|
+
return Response(b'' if request.method=='HEAD' else payload,
|
|
1027
|
+
media_type='application/json',headers=headers)
|
|
1028
|
+
if path in {'/-/d','/-/schema'} or path.startswith('/-/d/'):
|
|
1029
|
+
require(request.method in {'GET','HEAD'},'method_not_allowed')
|
|
1030
|
+
if short_codes is None:
|
|
1031
|
+
from msg.transports.dictionary import build_dictionary
|
|
1032
|
+
short_codes=build_dictionary(service.registry)
|
|
1033
|
+
if path.startswith('/-/d/'):
|
|
1034
|
+
scope=path.removeprefix('/-/d/')
|
|
1035
|
+
parts=scope.split('/')
|
|
1036
|
+
require(len(parts) in {1,2} and all(
|
|
1037
|
+
re.fullmatch(r'[a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*)*(?:@[1-9][0-9]*)?',part)
|
|
1038
|
+
is not None for part in parts),'not_found')
|
|
1039
|
+
if len(parts)==2:
|
|
1040
|
+
namespace_key,operation_key=parts
|
|
1041
|
+
namespaces={key:row['identity']
|
|
1042
|
+
for row in short_codes.document['codes']['namespace']
|
|
1043
|
+
if not row.get('deprecated')
|
|
1044
|
+
for key in (row['identity'],row['code'])}
|
|
1045
|
+
namespace=namespaces.get(namespace_key)
|
|
1046
|
+
require(namespace is not None,'not_found')
|
|
1047
|
+
try:
|
|
1048
|
+
document=short_codes.lookup_document(operation_key)
|
|
1049
|
+
except Failure as exc:
|
|
1050
|
+
raise Failure('not_found') from exc
|
|
1051
|
+
require(len(document.get('operations',()))==1 and
|
|
1052
|
+
document['operations'][0]['namespace']==namespace,'not_found')
|
|
1053
|
+
etag=short_codes.etag_for(operation_key)
|
|
1054
|
+
else:
|
|
1055
|
+
document=short_codes.lookup_document(scope)
|
|
1056
|
+
etag=short_codes.etag_for(scope)
|
|
1057
|
+
else:
|
|
1058
|
+
document=short_codes.index_document if path=='/-/d' else short_codes.schema_document
|
|
1059
|
+
etag=short_codes.index_etag if path=='/-/d' else short_codes.schema_etag
|
|
1060
|
+
headers={**BASE_HEADERS,'ETag':etag,'Cache-Control':'private, no-cache'}
|
|
1061
|
+
if request.headers.get('if-none-match')==etag:
|
|
1062
|
+
return Response(status_code=304,headers=headers)
|
|
1063
|
+
payload=canonical(document)
|
|
1064
|
+
require(len(payload)<=limits.max_response_bytes,'response_too_large')
|
|
1065
|
+
return Response(b'' if request.method=='HEAD' else payload,media_type='application/json',
|
|
1066
|
+
headers=headers)
|
|
1067
|
+
protocol=re.fullmatch(r'/-/([pg])/([a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*)+)(?:/(schema|(j|gz)/([A-Za-z0-9_-]+)))?',path)
|
|
1068
|
+
if protocol:
|
|
1069
|
+
transport,name,suffix,encoding,encoded=protocol.groups()
|
|
1070
|
+
spec=service.registry.operation(name)
|
|
1071
|
+
require('network' in spec.entries,'entry_not_allowed')
|
|
1072
|
+
if request.method=='HEAD':
|
|
1073
|
+
if transport=='g' and encoded is not None:
|
|
1074
|
+
require_url_safe_packet(path_packet(
|
|
1075
|
+
encoded,encoding,limits.max_request_bytes))
|
|
1076
|
+
return Response(status_code=200,headers=BASE_HEADERS)
|
|
1077
|
+
if suffix=='schema':
|
|
1078
|
+
require(request.method in {'GET','HEAD'},'method_not_allowed')
|
|
1079
|
+
return json_response({'operation':service.registry.describe(spec),
|
|
1080
|
+
'input':service.registry.schema(spec.input_schema),
|
|
1081
|
+
'output':service.registry.schema(spec.output_schema)})
|
|
1082
|
+
require(not (transport=='g' and name=='sharing.link_read'),
|
|
1083
|
+
'method_not_allowed')
|
|
1084
|
+
if (transport=='g' and request.method=='GET' and
|
|
1085
|
+
operation_route(spec).effect in {RouteEffect.BUSINESS_WRITE,
|
|
1086
|
+
RouteEffect.EXTERNAL_EFFECT} and
|
|
1087
|
+
passive_client(request)):
|
|
1088
|
+
return passive_client_response()
|
|
1089
|
+
if transport=='p':
|
|
1090
|
+
require(encoded is None and request.method=='POST','method_not_allowed')
|
|
1091
|
+
packet=decode_packet(await body_bytes(request,limits.max_request_bytes),
|
|
1092
|
+
limits.max_request_bytes)
|
|
1093
|
+
else:
|
|
1094
|
+
require(encoded is not None and request.method=='GET','method_not_allowed')
|
|
1095
|
+
packet=path_packet(encoded,encoding,limits.max_request_bytes)
|
|
1096
|
+
require_url_safe_packet(packet)
|
|
1097
|
+
require(packet.operation==name,'operation_mismatch')
|
|
1098
|
+
result=await service.executor.execute(packet,entry='network')
|
|
1099
|
+
value=result_wire(result)
|
|
1100
|
+
require(len(canonical(value))<=limits.max_response_bytes,'response_too_large')
|
|
1101
|
+
return json_response(value,error_status(result.error.code) if result.error else 200)
|
|
1102
|
+
if raw_path.startswith(b'/-/g/'):
|
|
1103
|
+
require(request.method in {'GET','HEAD'},'method_not_allowed')
|
|
1104
|
+
if short_codes is None:
|
|
1105
|
+
from msg.transports.dictionary import build_dictionary
|
|
1106
|
+
short_codes=build_dictionary(service.registry)
|
|
1107
|
+
parts=raw_path.split(b'/')
|
|
1108
|
+
require(len(parts)>=4 and parts[:3]==[b'',b'-',b'g'],'invalid_path')
|
|
1109
|
+
try:
|
|
1110
|
+
code=parts[3].decode('ascii')
|
|
1111
|
+
require(bool(code),'invalid_path')
|
|
1112
|
+
spec=short_codes.resolve_operation(code)
|
|
1113
|
+
require(spec.name!='sharing.link_read','method_not_allowed')
|
|
1114
|
+
if (request.method=='GET' and
|
|
1115
|
+
operation_route(spec).effect in {RouteEffect.BUSINESS_WRITE,
|
|
1116
|
+
RouteEffect.EXTERNAL_EFFECT} and
|
|
1117
|
+
passive_client(request)):
|
|
1118
|
+
return passive_client_response()
|
|
1119
|
+
segments=[]
|
|
1120
|
+
for raw in parts[4:]:
|
|
1121
|
+
require(re.search(rb'%(?![0-9A-Fa-f]{2})',raw) is None,'invalid_path')
|
|
1122
|
+
segments.append(unquote_to_bytes(raw).decode('utf-8'))
|
|
1123
|
+
except UnicodeDecodeError as exc:
|
|
1124
|
+
raise Failure('invalid_path') from exc
|
|
1125
|
+
if segments and segments[0] in {'token','bootstrap'}:
|
|
1126
|
+
raise Failure('secure_channel_required')
|
|
1127
|
+
else:
|
|
1128
|
+
if request.method=='HEAD':
|
|
1129
|
+
return Response(status_code=200,headers=BASE_HEADERS)
|
|
1130
|
+
spec,args=short_codes.decode_get_path(code,segments)
|
|
1131
|
+
header=request.headers.get('x-msg-request')
|
|
1132
|
+
if header:
|
|
1133
|
+
packet=path_packet(header,'j',limits.max_request_bytes)
|
|
1134
|
+
require(packet.operation==spec.name,'operation_mismatch')
|
|
1135
|
+
require(dict(packet.arguments)==args,'representation_mismatch')
|
|
1136
|
+
else:
|
|
1137
|
+
packet=request_for(spec.name,args,service.settings.service_url,source='manual')
|
|
1138
|
+
result=await service.executor.execute(packet,entry='network')
|
|
1139
|
+
value=result_wire(result)
|
|
1140
|
+
require(len(canonical(value))<=limits.max_response_bytes,'response_too_large')
|
|
1141
|
+
return json_response(value,error_status(result.error.code) if result.error else 200)
|
|
1142
|
+
if path=='/-' or path.startswith('/-/'):
|
|
1143
|
+
raise Failure('not_found')
|
|
1144
|
+
require(request.method in {'GET','HEAD'},'method_not_allowed')
|
|
1145
|
+
if path=='/favicon.png':
|
|
1146
|
+
headers={**BASE_HEADERS,'Content-Length':str(len(HOME_FAVICON))}
|
|
1147
|
+
return Response(b'' if request.method=='HEAD' else HOME_FAVICON,
|
|
1148
|
+
media_type='image/png',headers=headers)
|
|
1149
|
+
if path=='/':
|
|
1150
|
+
html='text/html' in request.headers.get('accept','')
|
|
1151
|
+
payload=HOME_HTML if html else HOME_MARKDOWN
|
|
1152
|
+
headers={**(HOME_HEADERS if html else BASE_HEADERS),
|
|
1153
|
+
'Content-Length':str(len(payload))}
|
|
1154
|
+
return Response(b'' if request.method=='HEAD' else payload,
|
|
1155
|
+
media_type='text/html' if html else 'text/markdown',headers=headers)
|
|
1156
|
+
money_public={'/_money':'money.state','/_money/banks':'money.banks',
|
|
1157
|
+
'/_money/offers':'money.offers'}
|
|
1158
|
+
if path in money_public:
|
|
1159
|
+
require(not request.url.query,'unknown_query_parameter')
|
|
1160
|
+
operation=money_public[path]
|
|
1161
|
+
require(service.registry.operation(operation).effect=='read','effect_mismatch')
|
|
1162
|
+
header=request.headers.get('x-msg-request')
|
|
1163
|
+
packet=(path_packet(header,'j',limits.max_request_bytes) if header else
|
|
1164
|
+
request_for(operation,{},service.settings.service_url,source='manual'))
|
|
1165
|
+
require(packet.operation==operation and not packet.arguments,
|
|
1166
|
+
'representation_mismatch')
|
|
1167
|
+
result=await service.executor.execute(packet,entry='network')
|
|
1168
|
+
if result.error:
|
|
1169
|
+
return json_response(result_wire(result),error_status(result.error.code))
|
|
1170
|
+
value=wire(result.data)
|
|
1171
|
+
payload=canonical(value)
|
|
1172
|
+
require(len(payload)<=limits.max_response_bytes,'response_too_large')
|
|
1173
|
+
etag='"'+digest(value)[7:]+'"'
|
|
1174
|
+
headers={**BASE_HEADERS,'ETag':etag,'Cache-Control':'no-store'}
|
|
1175
|
+
if request.headers.get('if-none-match')==etag:
|
|
1176
|
+
return Response(status_code=304,headers=headers)
|
|
1177
|
+
return Response(b'' if request.method=='HEAD' else payload,
|
|
1178
|
+
media_type='application/json',headers=headers)
|
|
1179
|
+
if path in {'/_rules','/_rules/'}:
|
|
1180
|
+
path='/_rules/_index.md'
|
|
1181
|
+
topic_history=re.fullmatch(
|
|
1182
|
+
r'(.+)/_events\.md(?:/(compact|normal|proof)(?:/([0-9]{1,2})(?:/([A-Za-z0-9_-]+\.[A-Za-z0-9_-]+))?)?)?',
|
|
1183
|
+
path)
|
|
1184
|
+
if topic_history:
|
|
1185
|
+
require(raw_path==path.encode('utf-8') and b'%' not in raw_path,'not_found')
|
|
1186
|
+
parent,segment_view,segment_limit,segment_cursor=topic_history.groups()
|
|
1187
|
+
pairs=request.query_params.multi_items()
|
|
1188
|
+
require(len(pairs)==len({name for name,_ in pairs}),'duplicate_query_parameter')
|
|
1189
|
+
query=dict(pairs)
|
|
1190
|
+
require(set(query)<={'view','limit','cursor'},'unknown_query_parameter')
|
|
1191
|
+
require(not query or not (segment_view or segment_limit or segment_cursor),
|
|
1192
|
+
'ambiguous_query_parameter')
|
|
1193
|
+
view=segment_view or query.get('view','compact')
|
|
1194
|
+
require(view in {'compact','normal','proof'},'invalid_view')
|
|
1195
|
+
limit=segment_limit or query.get('limit','10')
|
|
1196
|
+
require(limit.isdecimal() and 1<=int(limit)<=50,'invalid_limit')
|
|
1197
|
+
cursor=segment_cursor or query.get('cursor')
|
|
1198
|
+
args={'id':parent,'view':view,'limit':int(limit)}
|
|
1199
|
+
if cursor is not None:args['cursor']=cursor
|
|
1200
|
+
require(service.registry.operation('content.topic_events').effect=='read','effect_mismatch')
|
|
1201
|
+
header=request.headers.get('x-msg-request')
|
|
1202
|
+
if header:
|
|
1203
|
+
packet=path_packet(header,'j',limits.max_request_bytes)
|
|
1204
|
+
require(packet.operation=='content.topic_events' and
|
|
1205
|
+
canonical(packet.arguments)==canonical(args),'representation_mismatch')
|
|
1206
|
+
else:
|
|
1207
|
+
packet=request_for('content.topic_events',args,service.settings.service_url,
|
|
1208
|
+
source='manual')
|
|
1209
|
+
result=await service.executor.execute(packet,entry='network')
|
|
1210
|
+
if result.error:
|
|
1211
|
+
return json_response(result_wire(result),error_status(result.error.code))
|
|
1212
|
+
value=wire(result.data)
|
|
1213
|
+
body=canonical(value)
|
|
1214
|
+
require(len(body)<=limits.max_response_bytes,'response_too_large')
|
|
1215
|
+
etag='"'+digest(value)[7:]+'"'
|
|
1216
|
+
headers={**BASE_HEADERS,'ETag':etag,'Cache-Control':'no-store'}
|
|
1217
|
+
if request.headers.get('if-none-match')==etag:
|
|
1218
|
+
return Response(status_code=304,headers=headers)
|
|
1219
|
+
return Response(b'' if request.method=='HEAD' else body,media_type='application/json',
|
|
1220
|
+
headers=headers)
|
|
1221
|
+
order_path=re.fullmatch(
|
|
1222
|
+
r'/_orders/(ord_[a-z2-7]{32})(?:/(_payment|_delivery))?(?:/json)?',path)
|
|
1223
|
+
if order_path:
|
|
1224
|
+
require(not request.url.query and b'%' not in raw_path,
|
|
1225
|
+
'unknown_query_parameter')
|
|
1226
|
+
order_id,section=order_path.groups()
|
|
1227
|
+
operation=('orders.payment' if section=='_payment' else
|
|
1228
|
+
'delivery.get' if section=='_delivery' else 'orders.get')
|
|
1229
|
+
args={'order_id':order_id}
|
|
1230
|
+
require(service.registry.operation(operation).effect=='read',
|
|
1231
|
+
'effect_mismatch')
|
|
1232
|
+
header=request.headers.get('x-msg-request')
|
|
1233
|
+
packet=(path_packet(header,'j',limits.max_request_bytes) if header else
|
|
1234
|
+
request_for(operation,args,service.settings.service_url,
|
|
1235
|
+
source='manual'))
|
|
1236
|
+
require(packet.operation==operation and
|
|
1237
|
+
canonical(packet.arguments)==canonical(args),
|
|
1238
|
+
'representation_mismatch')
|
|
1239
|
+
result=await service.executor.execute(packet,entry='network')
|
|
1240
|
+
if result.error:
|
|
1241
|
+
return json_response(result_wire(result),error_status(result.error.code))
|
|
1242
|
+
value=wire(result.data)
|
|
1243
|
+
payload=canonical(value)
|
|
1244
|
+
require(len(payload)<=limits.max_response_bytes,'response_too_large')
|
|
1245
|
+
etag='"'+digest(value)[7:]+'"'
|
|
1246
|
+
headers={**BASE_HEADERS,'ETag':etag,
|
|
1247
|
+
'Cache-Control':'private, no-cache'}
|
|
1248
|
+
if request.headers.get('if-none-match')==etag:
|
|
1249
|
+
return Response(status_code=304,headers=headers)
|
|
1250
|
+
return Response(b'' if request.method=='HEAD' else payload,
|
|
1251
|
+
media_type='application/json',headers=headers)
|
|
1252
|
+
subject_alias=re.fullmatch(r'/@([^/]+)/([^/]+)(/.*)?',path)
|
|
1253
|
+
ssh_projection=False
|
|
1254
|
+
ssh_key_id=None
|
|
1255
|
+
if subject_alias:
|
|
1256
|
+
handle,name,remainder=subject_alias.groups()
|
|
1257
|
+
if name=='orders':
|
|
1258
|
+
require(b'%' not in raw_path,'not_found')
|
|
1259
|
+
tail=(remainder or '').strip('/')
|
|
1260
|
+
if tail.endswith('/json'):
|
|
1261
|
+
tail=tail[:-5]
|
|
1262
|
+
listing=tail in {'','json'}
|
|
1263
|
+
parts=[] if listing else tail.split('/')
|
|
1264
|
+
require(listing or (len(parts) in {1,2} and
|
|
1265
|
+
re.fullmatch(r'ord_[a-z2-7]{32}',parts[0]) is not None and
|
|
1266
|
+
(len(parts)==1 or parts[1] in {'_payment','_delivery'})),
|
|
1267
|
+
'not_found')
|
|
1268
|
+
operation=('orders.list' if listing else
|
|
1269
|
+
'orders.payment' if len(parts)==2 and parts[1]=='_payment' else
|
|
1270
|
+
'delivery.get' if len(parts)==2 else 'orders.get')
|
|
1271
|
+
pairs=request.query_params.multi_items()
|
|
1272
|
+
require(len(pairs)==len({key for key,_ in pairs}),
|
|
1273
|
+
'duplicate_query_parameter')
|
|
1274
|
+
query=dict(pairs)
|
|
1275
|
+
require(not query or listing,'unknown_query_parameter')
|
|
1276
|
+
require(set(query)<={'role','status','limit'},
|
|
1277
|
+
'unknown_query_parameter')
|
|
1278
|
+
args={} if listing else {'order_id':parts[0]}
|
|
1279
|
+
for field in ('role','status'):
|
|
1280
|
+
if field in query: args[field]=query[field]
|
|
1281
|
+
if 'limit' in query:
|
|
1282
|
+
require(query['limit'].isdecimal(),'invalid_limit')
|
|
1283
|
+
args['limit']=int(query['limit'])
|
|
1284
|
+
async with service.metadata.transaction(write=False) as tx:
|
|
1285
|
+
subject_id=await tx.resolve('/@'+handle)
|
|
1286
|
+
require((await tx.resource(subject_id)).type=='user','not_found')
|
|
1287
|
+
require(service.registry.operation(operation).effect=='read',
|
|
1288
|
+
'effect_mismatch')
|
|
1289
|
+
header=request.headers.get('x-msg-request')
|
|
1290
|
+
packet=(path_packet(header,'j',limits.max_request_bytes) if header else
|
|
1291
|
+
request_for(operation,args,service.settings.service_url,
|
|
1292
|
+
source='manual'))
|
|
1293
|
+
require(packet.operation==operation and
|
|
1294
|
+
canonical(packet.arguments)==canonical(args),
|
|
1295
|
+
'representation_mismatch')
|
|
1296
|
+
result=await service.executor.execute(packet,entry='network')
|
|
1297
|
+
if result.error:
|
|
1298
|
+
return json_response(result_wire(result),error_status(result.error.code))
|
|
1299
|
+
require(result.subject==subject_id,'permission_denied')
|
|
1300
|
+
value=wire(result.data)
|
|
1301
|
+
payload=canonical(value)
|
|
1302
|
+
require(len(payload)<=limits.max_response_bytes,'response_too_large')
|
|
1303
|
+
etag='"'+digest(value)[7:]+'"'
|
|
1304
|
+
headers={**BASE_HEADERS,'ETag':etag,
|
|
1305
|
+
'Cache-Control':'private, no-cache'}
|
|
1306
|
+
if request.headers.get('if-none-match')==etag:
|
|
1307
|
+
return Response(status_code=304,headers=headers)
|
|
1308
|
+
return Response(b'' if request.method=='HEAD' else payload,
|
|
1309
|
+
media_type='application/json',headers=headers)
|
|
1310
|
+
if name in {'bal','balance','ledger'}:
|
|
1311
|
+
require(remainder in {None,'/','/json'},'not_found')
|
|
1312
|
+
require(b'%' not in raw_path,'not_found')
|
|
1313
|
+
operation='money.ledger' if name=='ledger' else 'money.balance'
|
|
1314
|
+
pairs=request.query_params.multi_items()
|
|
1315
|
+
require(len(pairs)==len({key for key,_ in pairs}),
|
|
1316
|
+
'duplicate_query_parameter')
|
|
1317
|
+
query=dict(pairs)
|
|
1318
|
+
require(not query or name=='ledger','unknown_query_parameter')
|
|
1319
|
+
require(set(query)<={'cursor','limit'},'unknown_query_parameter')
|
|
1320
|
+
args={}
|
|
1321
|
+
for field in ('cursor','limit'):
|
|
1322
|
+
if field in query:
|
|
1323
|
+
require(query[field].isdecimal(),'invalid_query_parameter')
|
|
1324
|
+
args[field]=int(query[field])
|
|
1325
|
+
async with service.metadata.transaction(write=False) as tx:
|
|
1326
|
+
subject_id=await tx.resolve('/@'+handle)
|
|
1327
|
+
require((await tx.resource(subject_id)).type=='user','not_found')
|
|
1328
|
+
require(service.registry.operation(operation).effect=='read',
|
|
1329
|
+
'effect_mismatch')
|
|
1330
|
+
header=request.headers.get('x-msg-request')
|
|
1331
|
+
packet=(path_packet(header,'j',limits.max_request_bytes) if header else
|
|
1332
|
+
request_for(operation,args,service.settings.service_url,source='manual'))
|
|
1333
|
+
require(packet.operation==operation and
|
|
1334
|
+
canonical(packet.arguments)==canonical(args),
|
|
1335
|
+
'representation_mismatch')
|
|
1336
|
+
result=await service.executor.execute(packet,entry='network')
|
|
1337
|
+
if result.error:
|
|
1338
|
+
return json_response(result_wire(result),error_status(result.error.code))
|
|
1339
|
+
require(result.subject==subject_id,'permission_denied')
|
|
1340
|
+
value=wire(result.data)
|
|
1341
|
+
payload=canonical(value)
|
|
1342
|
+
require(len(payload)<=limits.max_response_bytes,'response_too_large')
|
|
1343
|
+
etag='"'+digest(value)[7:]+'"'
|
|
1344
|
+
headers={**BASE_HEADERS,'ETag':etag,
|
|
1345
|
+
'Cache-Control':'private, no-cache'}
|
|
1346
|
+
if request.headers.get('if-none-match')==etag:
|
|
1347
|
+
return Response(status_code=304,headers=headers)
|
|
1348
|
+
return Response(b'' if request.method=='HEAD' else payload,
|
|
1349
|
+
media_type='application/json',headers=headers)
|
|
1350
|
+
if name in {'handoffs','leases'}:
|
|
1351
|
+
require(request.method in {'GET','HEAD'},'method_not_allowed')
|
|
1352
|
+
require(raw_path.decode('utf-8')==request.url.path and b'%' not in raw_path,
|
|
1353
|
+
'not_found')
|
|
1354
|
+
tail=(remainder or '').strip('/')
|
|
1355
|
+
listing=tail in {'','json'}
|
|
1356
|
+
if not listing:
|
|
1357
|
+
require(re.fullmatch(r'[A-Za-z0-9_.:-]{1,160}(?:/json)?',tail)
|
|
1358
|
+
is not None,'not_found')
|
|
1359
|
+
singular='handoff' if name=='handoffs' else 'lease'
|
|
1360
|
+
operation='communication.'+singular+('_list' if listing else '_get')
|
|
1361
|
+
pairs=request.query_params.multi_items()
|
|
1362
|
+
require(len(pairs)==len({key for key,_ in pairs}),
|
|
1363
|
+
'duplicate_query_parameter')
|
|
1364
|
+
query=dict(pairs)
|
|
1365
|
+
require(not query or listing,'unknown_query_parameter')
|
|
1366
|
+
require(set(query)<={'limit','after'},'unknown_query_parameter')
|
|
1367
|
+
args={} if listing else {'id':tail.removesuffix('/json')}
|
|
1368
|
+
if 'limit' in query:
|
|
1369
|
+
require(query['limit'].isdecimal(),'invalid_limit')
|
|
1370
|
+
args['limit']=int(query['limit'])
|
|
1371
|
+
if 'after' in query: args['after']=query['after']
|
|
1372
|
+
async with service.metadata.transaction(write=False) as tx:
|
|
1373
|
+
subject_id=await tx.resolve('/@'+handle)
|
|
1374
|
+
require((await tx.resource(subject_id)).type=='user','not_found')
|
|
1375
|
+
require(service.registry.operation(operation).effect=='read',
|
|
1376
|
+
'effect_mismatch')
|
|
1377
|
+
header=request.headers.get('x-msg-request')
|
|
1378
|
+
if header:
|
|
1379
|
+
packet=path_packet(header,'j',limits.max_request_bytes)
|
|
1380
|
+
require(packet.operation==operation and
|
|
1381
|
+
canonical(packet.arguments)==canonical(args),
|
|
1382
|
+
'representation_mismatch')
|
|
1383
|
+
else:
|
|
1384
|
+
packet=request_for(operation,args,service.settings.service_url,
|
|
1385
|
+
source='manual')
|
|
1386
|
+
result=await service.executor.execute(packet,entry='network')
|
|
1387
|
+
if result.error:
|
|
1388
|
+
return json_response(result_wire(result),error_status(result.error.code))
|
|
1389
|
+
require(result.subject==subject_id,'permission_denied')
|
|
1390
|
+
value=wire(result.data)
|
|
1391
|
+
value['path']='/@'+handle+'/'+name+('/'+args['id'] if not listing else '')
|
|
1392
|
+
etag='"'+digest(value)[7:]+'"'
|
|
1393
|
+
headers={**BASE_HEADERS,'ETag':etag,
|
|
1394
|
+
'Cache-Control':'private, no-cache'}
|
|
1395
|
+
if request.headers.get('if-none-match')==etag:
|
|
1396
|
+
return Response(status_code=304,headers=headers)
|
|
1397
|
+
payload=canonical(value)
|
|
1398
|
+
require(len(payload)<=limits.max_response_bytes,'response_too_large')
|
|
1399
|
+
return Response(b'' if request.method=='HEAD' else payload,
|
|
1400
|
+
media_type='application/json',headers=headers)
|
|
1401
|
+
certificate_detail=(name in {'cert','certificates'} and
|
|
1402
|
+
remainder not in {None,'/','/json','/meta','/history'})
|
|
1403
|
+
if name in SUBJECT_KEY_ALIASES or name in SUBJECT_OPERATION_ALIASES or certificate_detail:
|
|
1404
|
+
require(request.method in {'GET','HEAD'},
|
|
1405
|
+
'method_not_allowed')
|
|
1406
|
+
key_alias=name in SUBJECT_KEY_ALIASES
|
|
1407
|
+
if not key_alias and not certificate_detail:
|
|
1408
|
+
require(remainder in {None,'/'},'not_found')
|
|
1409
|
+
require(not request.url.query or name in {'in','inbox','out','outbox','following'},
|
|
1410
|
+
'unknown_query_parameter')
|
|
1411
|
+
async with service.metadata.transaction(write=False) as tx:
|
|
1412
|
+
subject_id=await tx.resolve('/@'+handle)
|
|
1413
|
+
require((await tx.resource(subject_id)).type=='user','not_found')
|
|
1414
|
+
if certificate_detail:
|
|
1415
|
+
tail=remainder.strip('/')
|
|
1416
|
+
require(re.fullmatch(r'[A-Za-z0-9_.:-]{1,160}(?:/json)?',tail) is not None,
|
|
1417
|
+
'not_found')
|
|
1418
|
+
operation='cert.get'
|
|
1419
|
+
args={'id':tail.removesuffix('/json')}
|
|
1420
|
+
elif key_alias:
|
|
1421
|
+
operation,_canonical=SUBJECT_KEY_ALIASES[name]
|
|
1422
|
+
tail=(remainder or '').strip('/')
|
|
1423
|
+
if tail=='json': tail=''
|
|
1424
|
+
if tail:
|
|
1425
|
+
require(re.fullmatch(r'[A-Za-z0-9_.:-]{1,160}(?:/json)?',tail) is not None,
|
|
1426
|
+
'not_found')
|
|
1427
|
+
key_id=tail.removesuffix('/json')
|
|
1428
|
+
operation=operation.replace('_list','_get')
|
|
1429
|
+
args={'subject_id':subject_id}
|
|
1430
|
+
if tail: args['key_id']=key_id
|
|
1431
|
+
else:
|
|
1432
|
+
operation=SUBJECT_OPERATION_ALIASES[name]
|
|
1433
|
+
args={'subject_id':subject_id} if operation=='achievement.list' else {}
|
|
1434
|
+
if operation in {'communication.inbox','communication.outbox',
|
|
1435
|
+
'communication.following'}:
|
|
1436
|
+
pairs=request.query_params.multi_items()
|
|
1437
|
+
require(len(pairs)==len({key for key,_ in pairs}),'duplicate_query_parameter')
|
|
1438
|
+
query=dict(pairs)
|
|
1439
|
+
require(set(query)<={'limit','cursor'},'unknown_query_parameter')
|
|
1440
|
+
if 'limit' in query:
|
|
1441
|
+
require(query['limit'].isdecimal(),'invalid_limit')
|
|
1442
|
+
args['limit']=int(query['limit'])
|
|
1443
|
+
if 'cursor' in query: args['cursor']=query['cursor']
|
|
1444
|
+
header=request.headers.get('x-msg-request')
|
|
1445
|
+
if header:
|
|
1446
|
+
packet=path_packet(header,'j',limits.max_request_bytes)
|
|
1447
|
+
require(packet.operation==operation and canonical(packet.arguments)==canonical(args),
|
|
1448
|
+
'representation_mismatch')
|
|
1449
|
+
else:
|
|
1450
|
+
packet=request_for(operation,args,service.settings.service_url,source='manual')
|
|
1451
|
+
result=await service.executor.execute(packet,entry='network')
|
|
1452
|
+
if result.error:
|
|
1453
|
+
return json_response(result_wire(result),error_status(result.error.code))
|
|
1454
|
+
if operation in {'communication.inbox','communication.outbox',
|
|
1455
|
+
'communication.dm_list','communication.following'}:
|
|
1456
|
+
require(result.subject==subject_id,'permission_denied')
|
|
1457
|
+
value=wire(result.data)
|
|
1458
|
+
if certificate_detail:
|
|
1459
|
+
require(value.get('certificate',{}).get('subject_id')==subject_id,'not_found')
|
|
1460
|
+
canonical_name=('cert' if certificate_detail else
|
|
1461
|
+
SUBJECT_KEY_ALIASES[name][1] if key_alias else
|
|
1462
|
+
{'achievement.list':'ach','communication.inbox':'in',
|
|
1463
|
+
'communication.outbox':'out','communication.dm_list':'dm',
|
|
1464
|
+
'communication.following':'following'}[operation])
|
|
1465
|
+
key_suffix=args.get('key_id') or (args.get('id') if certificate_detail else None)
|
|
1466
|
+
value['path']='/@'+handle+'/'+canonical_name+('/'+key_suffix if key_suffix else '')
|
|
1467
|
+
etag='"'+digest(value)[7:]+'"'
|
|
1468
|
+
headers={**BASE_HEADERS,'ETag':etag,'Cache-Control':'private, no-cache'}
|
|
1469
|
+
if request.headers.get('if-none-match')==etag:
|
|
1470
|
+
return Response(status_code=304,headers=headers)
|
|
1471
|
+
payload=canonical(value)
|
|
1472
|
+
require(len(payload)<=limits.max_response_bytes,'response_too_large')
|
|
1473
|
+
return Response(b'' if request.method=='HEAD' else payload,
|
|
1474
|
+
media_type='application/json',headers=headers)
|
|
1475
|
+
if name in SUBJECT_RESOURCE_ALIASES:
|
|
1476
|
+
require(raw_path.decode('utf-8')==request.url.path and b'%' not in raw_path,
|
|
1477
|
+
'not_found')
|
|
1478
|
+
ssh_projection=name in {'ssh','ssh-keys'}
|
|
1479
|
+
if ssh_projection and remainder not in {None,'/','/json','/meta'}:
|
|
1480
|
+
tail=remainder.strip('/')
|
|
1481
|
+
require(re.fullmatch(r'[A-Za-z0-9_.:-]{1,160}(?:/json)?',tail) is not None,
|
|
1482
|
+
'not_found')
|
|
1483
|
+
ssh_key_id=tail.removesuffix('/json')
|
|
1484
|
+
remainder='/json' if tail.endswith('/json') else ''
|
|
1485
|
+
path='/@'+handle+'/'+SUBJECT_RESOURCE_ALIASES[name]+(remainder or '')
|
|
1486
|
+
stable=parse_stable_view(path,raw_path)
|
|
1487
|
+
resource_path,view,revision=stable if stable is not None else parse_view(path)
|
|
1488
|
+
redirect_target=None
|
|
1489
|
+
if view=='markdown' and revision is None and not resource_path.endswith('.md'):
|
|
1490
|
+
# Old Post links omitted .md. Resolve the candidate only to find
|
|
1491
|
+
# the stable resource; disclose its canonical path after the
|
|
1492
|
+
# normal discovery.get authorization check succeeds.
|
|
1493
|
+
async with service.metadata.transaction(write=False) as tx:
|
|
1494
|
+
try:
|
|
1495
|
+
await tx.resolve(resource_path)
|
|
1496
|
+
except Failure as exc:
|
|
1497
|
+
if exc.code!='not_found':
|
|
1498
|
+
raise
|
|
1499
|
+
candidate=resource_path+'.md'
|
|
1500
|
+
try:
|
|
1501
|
+
rid=await tx.resolve(candidate)
|
|
1502
|
+
except Failure as candidate_error:
|
|
1503
|
+
if candidate_error.code!='not_found':
|
|
1504
|
+
raise
|
|
1505
|
+
else:
|
|
1506
|
+
if (await tx.resource(rid)).type=='post':
|
|
1507
|
+
redirect_target=await tx.path(rid)
|
|
1508
|
+
resource_path=redirect_target
|
|
1509
|
+
op='discovery.raw' if view=='raw' else 'discovery.get'
|
|
1510
|
+
require(service.registry.operation(op).effect=='read','effect_mismatch')
|
|
1511
|
+
args={'id':resource_path}
|
|
1512
|
+
if revision: args['revision']=revision
|
|
1513
|
+
if view in {'meta','history'}: args['view']=view
|
|
1514
|
+
header=request.headers.get('x-msg-request')
|
|
1515
|
+
if header:
|
|
1516
|
+
packet=path_packet(header,'j',limits.max_request_bytes)
|
|
1517
|
+
require(packet.operation==op,'operation_mismatch')
|
|
1518
|
+
# Resolve both aliases to the same stable resource before comparing.
|
|
1519
|
+
async with service.metadata.transaction(write=False) as tx:
|
|
1520
|
+
header_id=packet.arguments.get('id')
|
|
1521
|
+
rid=await tx.resolve(header_id) if isinstance(header_id,str) and header_id.startswith('/') else header_id
|
|
1522
|
+
require(rid==await tx.resolve(resource_path),'resource_mismatch')
|
|
1523
|
+
require(packet.arguments.get('revision')==revision,'revision_mismatch')
|
|
1524
|
+
require(packet.arguments.get('view')==args.get('view'),'representation_mismatch')
|
|
1525
|
+
else:
|
|
1526
|
+
packet=request_for(op,args,service.settings.service_url,source='manual')
|
|
1527
|
+
result=await service.executor.execute(packet,entry='network')
|
|
1528
|
+
if result.error:
|
|
1529
|
+
return json_response(result_wire(result),error_status(result.error.code))
|
|
1530
|
+
if redirect_target is not None:
|
|
1531
|
+
return Response(status_code=308,headers={**BASE_HEADERS,
|
|
1532
|
+
'Location':quote(redirect_target,safe='/')})
|
|
1533
|
+
value=wire(result.data)
|
|
1534
|
+
if ssh_projection:
|
|
1535
|
+
value={'id':value['id'],'keys':[key for key in value.get('keys',())
|
|
1536
|
+
if key.get('kind')=='ssh_key' and
|
|
1537
|
+
(ssh_key_id is None or key.get('key_id')==ssh_key_id)]}
|
|
1538
|
+
if ssh_key_id is not None:
|
|
1539
|
+
require(bool(value['keys']),'not_found')
|
|
1540
|
+
etag='"'+digest(value)[7:]+'"'
|
|
1541
|
+
headers={**BASE_HEADERS,'ETag':etag,'Cache-Control':'private, no-cache'}
|
|
1542
|
+
if request.headers.get('if-none-match')==etag:
|
|
1543
|
+
return Response(status_code=304,headers=headers)
|
|
1544
|
+
if view=='raw':
|
|
1545
|
+
blob=decode(BlobRef,value['content'])
|
|
1546
|
+
byte_range=tuple(value['range'])
|
|
1547
|
+
status=200
|
|
1548
|
+
requested_range=request.headers.get('range')
|
|
1549
|
+
if requested_range and (request.headers.get('if-range') in {None,etag}):
|
|
1550
|
+
match=re.fullmatch(r'bytes=(\d*)-(\d*)',requested_range)
|
|
1551
|
+
require(match is not None and any(match.groups()),'range_not_satisfiable')
|
|
1552
|
+
left,right=match.groups()
|
|
1553
|
+
if left:
|
|
1554
|
+
begin=int(left);end=min(int(right)+1,blob.size) if right else blob.size
|
|
1555
|
+
else:
|
|
1556
|
+
require(int(right)>0,'range_not_satisfiable')
|
|
1557
|
+
begin=max(0,blob.size-int(right));end=blob.size
|
|
1558
|
+
require(byte_range[0]<=begin<end<=byte_range[1],'range_not_satisfiable')
|
|
1559
|
+
byte_range=(begin,end);status=206
|
|
1560
|
+
headers['Content-Range']=f'bytes {begin}-{end-1}/{blob.size}'
|
|
1561
|
+
headers.update({'Content-Length':str(byte_range[1]-byte_range[0]),'Accept-Ranges':'bytes',
|
|
1562
|
+
'Content-Disposition':"attachment; filename*=UTF-8''"+quote(value['filename'],safe='')})
|
|
1563
|
+
if request.method=='HEAD':
|
|
1564
|
+
return Response(status_code=status,media_type=blob.media_type,headers=headers)
|
|
1565
|
+
return StreamingResponse(service.contents.read(blob,byte_range),status_code=status,media_type=blob.media_type,headers=headers)
|
|
1566
|
+
body=canonical(value) if view in {'json','meta','history'} else describe_resource(value).encode()
|
|
1567
|
+
require(len(body)<=limits.max_response_bytes,'response_too_large')
|
|
1568
|
+
return Response(b'' if request.method=='HEAD' else body,media_type='application/json' if view in {'json','meta','history'} else 'text/markdown',headers=headers)
|
|
1569
|
+
except Failure as exc:
|
|
1570
|
+
return json_response({'status':'error','error':exc.as_dict()},error_status(exc.code))
|
|
1571
|
+
except Exception:
|
|
1572
|
+
import logging
|
|
1573
|
+
# Exception messages/tracebacks may contain URLs, headers or input.
|
|
1574
|
+
logging.getLogger(__name__).error('http_dispatch_failed')
|
|
1575
|
+
return json_response({'status':'error','error':{'code':'internal_error','retryable':False}},500)
|
|
1576
|
+
|
|
1577
|
+
return Starlette(routes=[Route('/{path:path}',dispatch,methods=['GET','HEAD','POST','PUT','DELETE','PATCH','OPTIONS'])],lifespan=lifespan)
|