File size: 13,226 Bytes
41d9c00
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
"""公开Git托管平台的固定版本Skill根仓库检查。"""
import asyncio
import re
from urllib.parse import quote, urlencode

from proxy_resources import ProxyResources
from skill_archive_source import public_skill_source, read_metadata_response, read_metadata_body, SkillSourceError, METADATA_SECONDS
from skill_manifest import MAX_TREE_ENTRIES, skill_segment, skill_sha, skill_tree

MAX_REQUESTS = 256
PAGE_SIZE = 100


def mirror_head(body):
    if not isinstance(body, bytes):
        raise SkillSourceError('invalid_metadata')
    position = 0
    def packet():
        nonlocal position
        header = body[position:position + 4]
        if len(header) != 4 or not re.fullmatch(b'[0-9a-fA-F]{4}', header):
            raise SkillSourceError('invalid_metadata')
        size = int(header, 16)
        position += 4
        if size == 0:
            return None
        if not 4 < size <= 65520 or position + size - 4 > len(body):
            raise SkillSourceError('invalid_metadata')
        value = body[position:position + size - 4]
        position += size - 4
        return value
    if packet() != b'# service=git-upload-pack\n' or packet() is not None:
        raise SkillSourceError('invalid_metadata')
    head, head_ref, seen = None, None, {}
    for index in range(513):
        value = packet()
        if value is None:
            if head is None or position != len(body) or head_ref is not None and seen.get(head_ref) != head:
                raise SkillSourceError('invalid_metadata')
            return head
        if index == 512 or not value.endswith(b'\n'):
            raise SkillSourceError('invalid_metadata')
        value = value[:-1]
        if index == 0:
            parts = value.split(b'\0')
            if len(parts) != 2 or len(parts[1]) > 16384 or any(c < 32 or c > 126 for c in parts[1]):
                raise SkillSourceError('invalid_metadata')
            capabilities = parts[1].split(b' ')
            if any(cap.startswith(b'object-format=') and cap != b'object-format=sha1' for cap in capabilities):
                raise SkillSourceError('invalid_metadata')
            symbolic = [cap[len(b'symref=HEAD:'):] for cap in capabilities if cap.startswith(b'symref=HEAD:')]
            if len(symbolic) > 1 or symbolic and not symbolic[0].startswith(b'refs/heads/'):
                raise SkillSourceError('invalid_metadata')
            head_ref = symbolic[0] if symbolic else None
            value = parts[0]
        if b'\0' in value or len(value) > 4096:
            raise SkillSourceError('invalid_metadata')
        parts = value.split(b' ')
        if (len(parts) != 2 or any(c < 33 or c > 126 for c in parts[1])
            or not re.fullmatch(b'[0-9a-fA-F]{40}', parts[0]) or parts[0] == b'0' * 40
            or parts[1] in seen):
            raise SkillSourceError('invalid_metadata')
        seen[parts[1]] = parts[0].decode('ascii').lower()
        if index == 0:
            if parts[1] != b'HEAD':
                raise SkillSourceError('invalid_metadata')
            head = parts[0].decode('ascii').lower()
    raise SkillSourceError('invalid_metadata')


def _branch(value):
    try:
        valid = (isinstance(value, str) and value and len(value.encode('utf-8')) <= 4096
            and all(skill_segment(part) for part in value.split('/')))
    except UnicodeError:
        valid = False
    if not valid:
        raise SkillSourceError('invalid_metadata')
    return value


def _normalized_rows(values, provider, path, commit):
    if not isinstance(values, list) or len(values) > MAX_TREE_ENTRIES:
        raise SkillSourceError('invalid_metadata')
    result, aliases = {}, set()
    for entry in values:
        if not isinstance(entry, dict) or not isinstance(entry.get('path'), str):
            raise SkillSourceError('invalid_metadata')
        prefix = path + '/' if path else ''
        if not entry['path'].startswith(prefix):
            raise SkillSourceError('invalid_metadata')
        name = entry['path'][len(prefix):]
        if not skill_segment(name) or name.casefold() in aliases:
            raise SkillSourceError('invalid_metadata')
        aliases.add(name.casefold())
        if provider == 'gitlab.com':
            if entry.get('name') != name:
                raise SkillSourceError('invalid_metadata')
            sha = skill_sha(entry.get('id'))
            kind, mode = entry.get('type'), entry.get('mode')
            if (kind, mode) not in {('blob', '100644'), ('blob', '100755'), ('blob', '120000'), ('tree', '040000'), ('commit', '160000')}:
                raise SkillSourceError('invalid_metadata')
        else:
            info = entry.get('commit')
            if not isinstance(info, dict) or info.get('type') != 'commit' or skill_sha(info.get('hash')) != commit:
                raise SkillSourceError('invalid_metadata')
            attributes = entry.get('attributes', [] if entry.get('type') == 'commit_directory' else None)
            if not isinstance(attributes, list) or len(attributes) > 32 or any(not isinstance(v, str) or len(v) > 128 for v in attributes):
                raise SkillSourceError('invalid_metadata')
            if len(set(attributes)) != len(attributes) or not set(attributes) <= {'executable', 'link', 'subrepository', 'binary'}:
                raise SkillSourceError('invalid_metadata')
            kind = entry.get('type')
            if kind == 'commit_directory':
                kind, mode = 'tree', '040000'
                if attributes:
                    raise SkillSourceError('invalid_metadata')
            elif kind == 'commit_file':
                if type(entry.get('size')) is not int or entry['size'] < 0:
                    raise SkillSourceError('invalid_metadata')
                kind = 'blob'
                if 'subrepository' in attributes:
                    if len(attributes) != 1:
                        raise SkillSourceError('invalid_metadata')
                    kind, mode = 'commit', '160000'
                else:
                    mode = '120000' if 'link' in attributes else '100755' if 'executable' in attributes else '100644'
            else:
                raise SkillSourceError('invalid_metadata')
            sha = commit
        result[name] = dict(path=name, sha=sha, type=kind, mode=mode)
    return result


async def probe_public_skill(item, *, send, client_factory, transport_factory):
    source = public_skill_source(item)
    host, owner, repo = source['host'], source['owner'], source['repo']
    client = client_factory(follow_redirects=False, trust_env=False, transport=transport_factory())
    resources = ProxyResources(client)
    headers = {'Accept': 'application/json', 'Accept-Encoding': 'identity', 'User-Agent': 'ComfyUI-Ranking-SaaS'}
    requests = 0
    async def response(url, accept='application/json'):
        nonlocal requests
        requests += 1
        if requests > MAX_REQUESTS:
            raise SkillSourceError('metadata_too_large')
        return resources.track(await send(client, 'GET', url, headers=headers | {'Accept': accept}, timeout=METADATA_SECONDS, max_redirects=0))
    async def read(url, paginated=False):
        return await read_metadata_response(await response(url), resources, allow_pagination=paginated)

    try:
        async with asyncio.timeout(METADATA_SECONDS):
            if host == 'kkgithub.com':
                advertisement = await response(f'https://kkgithub.com/{owner}/{repo}.git/info/refs?service=git-upload-pack', 'application/x-git-upload-pack-advertisement')
                content_type = advertisement.headers.get('content-type', '').split(';', 1)[0].strip().lower()
                body = await read_metadata_body(advertisement, resources)
                if content_type != 'application/x-git-upload-pack-advertisement':
                    raise SkillSourceError('invalid_metadata')
                commit = mirror_head(body)
                base = f'https://api.github.com/repos/{owner}/{repo}'
                metadata = await read(base + '/git/commits/' + commit)
                if not isinstance(metadata, dict) or skill_sha(metadata.get('sha')) != commit or not isinstance(metadata.get('tree'), dict):
                    raise SkillSourceError('invalid_metadata')
                root_tree = skill_sha(metadata['tree'].get('sha'))
            else:
                if host == 'gitlab.com':
                    base = 'https://gitlab.com/api/v4/projects/' + quote(owner + '/' + repo, safe='')
                    repository = await read(base)
                    branch = _branch(repository.get('default_branch') if isinstance(repository, dict) else None)
                    metadata = await read(base + '/repository/branches/' + quote(branch, safe=''))
                    if not isinstance(metadata, dict) or metadata.get('name') != branch or not isinstance(metadata.get('commit'), dict):
                        raise SkillSourceError('invalid_metadata')
                    commit = skill_sha(metadata['commit'].get('id'))
                elif host == 'gitee.com':
                    base = f'https://gitee.com/api/v5/repos/{owner}/{repo}'
                    repository = await read(base)
                    branch = _branch(repository.get('default_branch') if isinstance(repository, dict) else None)
                    metadata = await read(base + '/branches/' + quote(branch, safe=''))
                    if not isinstance(metadata, dict) or metadata.get('name') != branch or not isinstance(metadata.get('commit'), dict):
                        raise SkillSourceError('invalid_metadata')
                    commit = skill_sha(metadata['commit'].get('sha'))
                    details = metadata['commit'].get('commit')
                    if not isinstance(details, dict) or not isinstance(details.get('tree'), dict):
                        raise SkillSourceError('invalid_metadata')
                    root_tree = skill_sha(details['tree'].get('sha'))
                else:
                    base = f'https://api.bitbucket.org/2.0/repositories/{owner}/{repo}'
                    repository = await read(base)
                    mainbranch = repository.get('mainbranch') if isinstance(repository, dict) else None
                    branch = _branch(mainbranch.get('name') if isinstance(mainbranch, dict) else None)
                    metadata = await read(base + '/refs/branches/' + quote(branch, safe=''))
                    if not isinstance(metadata, dict) or metadata.get('name') != branch or metadata.get('type') != 'branch' or not isinstance(metadata.get('target'), dict):
                        raise SkillSourceError('invalid_metadata')
                    if metadata['target'].get('type') != 'commit':
                        raise SkillSourceError('invalid_metadata')
                    commit = skill_sha(metadata['target'].get('hash'))

            async def directory(path='', tree_id=None):
                if host in ('gitee.com', 'kkgithub.com'):
                    value = await read(base + '/git/trees/' + (tree_id or root_tree))
                    return skill_tree(value, tree_id or root_tree)
                values = []
                for page in range(1, MAX_TREE_ENTRIES + 2):
                    if host == 'gitlab.com':
                        url = base + '/repository/tree?' + urlencode(dict(ref=commit, path=path, per_page=PAGE_SIZE, page=page))
                        value = await read(url, True)
                        if not isinstance(value, list) or len(value) > PAGE_SIZE:
                            raise SkillSourceError('invalid_metadata')
                        more = bool(value)
                        entries = value
                    else:
                        url = base + '/src/' + commit + '/' + (quote(path, safe='') + '/' if path else '') + '?' + urlencode(dict(pagelen=PAGE_SIZE, page=page))
                        value = await read(url, True)
                        if (not isinstance(value, dict) or type(value.get('page')) is not int or value['page'] != page
                            or type(value.get('pagelen')) is not int or not 1 <= value['pagelen'] <= PAGE_SIZE
                            or not isinstance(value.get('values'), list) or len(value['values']) > value['pagelen']):
                            raise SkillSourceError('invalid_metadata')
                        next_page = value.get('next')
                        if next_page is not None and (not isinstance(next_page, str) or not 0 < len(next_page) <= 8192):
                            raise SkillSourceError('invalid_metadata')
                        more, entries = next_page is not None, value['values']
                        if more and not entries:
                            raise SkillSourceError('invalid_metadata')
                    values.extend(entries)
                    if len(values) > MAX_TREE_ENTRIES:
                        raise SkillSourceError('metadata_too_large')
                    if not more:
                        return _normalized_rows(values, host, path, commit)
                raise SkillSourceError('metadata_too_large')

            await directory()
            return True
    finally:
        await resources.close()