File size: 7,674 Bytes
4b4d673
 
 
fcc10d7
f1cca8f
fcc10d7
f1cca8f
fcc10d7
4b4d673
 
 
f1cca8f
4b4d673
 
 
fcc10d7
 
4b4d673
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fcc10d7
 
 
 
4b4d673
fcc10d7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f1cca8f
 
71e57c9
f1cca8f
cce7bbe
 
 
 
 
f1cca8f
 
 
fcc10d7
 
 
 
 
f1cca8f
 
 
 
 
 
 
 
cce7bbe
 
 
 
 
 
 
 
f1cca8f
 
 
 
 
 
 
 
 
 
 
 
fcc10d7
 
cce7bbe
fcc10d7
 
 
 
 
 
 
 
 
 
 
 
 
f1cca8f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cce7bbe
f1cca8f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cce7bbe
 
 
 
 
 
f1cca8f
cce7bbe
 
 
f1cca8f
 
71e57c9
006920e
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Utility functions for the OpenADMET CYP Challenge Gradio app."""

import io
import ipaddress
import re
import socket
import time
from urllib.parse import urljoin, urlparse

import boto3
import pandas as pd
import requests
from config import AWS_DEFAULT_REGION, S3_BUCKET
from loguru import logger

_MAX_REDIRECTS = 5

# S3FS was causing 403 errors in testing on some machines, unclear why.
# Switching to boto3 client which works reliably
s3_client = boto3.client("s3", region_name=AWS_DEFAULT_REGION)


def _load_csv_from_s3(key: str, parquet: bool = False) -> pd.DataFrame:
    """Load a CSV file from S3 into a DataFrame."""
    logger.info(f"Downloading from S3: {key}")
    obj = s3_client.get_object(Bucket=S3_BUCKET, Key=key)
    if parquet:
        return pd.read_parquet(io.BytesIO(obj["Body"].read()))
    return pd.read_csv(io.BytesIO(obj["Body"].read()))


def _safeify_username(username: str) -> str:
    """Sanitise a HuggingFace username for use in S3 keys and file paths.

    HF usernames for organisations use the format ``org/user``, which would create
    unintended S3 path nesting. Spaces are also replaced for safety. Lowercased so
    that HF-username casing variants (which all resolve to the same account) map to
    the same S3 prefix — otherwise the per-user submission cooldown
    (``_fetch_last_submission_date``) could be bypassed by alternating case.
    """
    return str(username.strip()).lower().replace("/", "_").replace(" ", "_")


def _is_safe_public_url(url: str) -> bool:
    """Reject URLs whose host resolves to a private/loopback/link-local address.

    Guards ``check_page_exists`` against SSRF: without this, a user-supplied URL
    (e.g. the "Method Report Link" field) could point the server at internal
    infrastructure and have it echo back reachability.
    """
    hostname = urlparse(url).hostname
    if not hostname:
        return False
    try:
        infos = socket.getaddrinfo(hostname, None)
    except socket.gaierror:
        return False
    for info in infos:
        ip = ipaddress.ip_address(info[4][0])
        if (
            ip.is_private
            or ip.is_loopback
            or ip.is_link_local
            or ip.is_reserved
            or ip.is_multicast
            or ip.is_unspecified
        ):
            return False
    return True


# TODO: Maybe use Tenacity for retrying?
def check_page_exists(
    url: str,
    delay: float = 0.2,
    max_retries: int = 3,
    current_retries: int = 0,
    restrict_to_public: bool = True,
):
    """Check if a web page exists at the given URL with a retry limit for 429 errors.

    Redirects are followed manually (rather than via ``requests``' built-in
    ``allow_redirects``) so every hop can be checked against
    ``_is_safe_public_url`` before it is requested, closing off SSRF via a
    redirect to an internal address.

    Params:
        url (str): The URL of the page to check.
        delay (float, optional): Seconds to wait until submitting another request.
            Defaults to 0.
        max_retries (int, optional): Maximum number of times to retry on a 429 error.
            Defaults to 3.
        current_retries (int, optional): Current number of retries performed (internal
            counter). Defaults to 0.
        restrict_to_public (bool, optional): Reject hosts that resolve to a
            private/loopback/internal address. Only meaningful protection when
            ``url`` (or its host) is attacker-controlled — e.g. a user-supplied
            link. Should be disabled for calls against a hardcoded, trusted
            domain (e.g. huggingface.co), since some platforms resolve their own
            domain to an internal address for intra-network callers (split-horizon
            DNS), which this check would otherwise incorrectly reject. Defaults to
            True.

    Returns:
        bool: True if the page exists (status code 200), False otherwise.

    """
    safe_url = str(url).strip()

    # Attempt to fix url
    if not safe_url.startswith(("http://", "https://")):
        safe_url = f"https://{safe_url}"

    try:
        response = None
        for _ in range(_MAX_REDIRECTS + 1):
            if restrict_to_public and not _is_safe_public_url(safe_url):
                logger.warning(f"Refusing to fetch non-public URL: {safe_url}")
                return False
            response = requests.get(safe_url, timeout=5, allow_redirects=False)
            if (
                response.status_code in (301, 302, 303, 307, 308)
                and "Location" in response.headers
            ):
                safe_url = urljoin(safe_url, response.headers["Location"])
                continue
            break
        else:
            logger.warning(f"Too many redirects for {url}")
            return False

        # Check for Rate Limit Error and retry if under the limit
        if response.status_code == 429:
            if current_retries < max_retries:
                # Make wait time exponential
                wait_time = 5 * (2**current_retries)
                logger.warning(
                    f"Warning: Rate limit hit on {safe_url}. Attempt "
                    f"{current_retries + 1}/{max_retries}. Waiting for {wait_time} "
                    "seconds..."
                )
                time.sleep(wait_time)
                # Recurse with an incremented retry counter
                return check_page_exists(
                    safe_url,
                    delay=delay,
                    max_retries=max_retries,
                    current_retries=current_retries + 1,
                    restrict_to_public=restrict_to_public,
                )
            else:
                logger.error(
                    f"Error: Max retries ({max_retries}) reached for rate limit on "
                    f"{safe_url}."
                )
                return False  # Give up after max retries

        # Return True only for a successful status code (200)
        return response.status_code == 200

    except requests.exceptions.RequestException as e:
        logger.error(f"Error checking URL {safe_url}: {e}")
        return False

    finally:
        # Sleep after every request to avoid HTTPS error
        time.sleep(delay)


def validate_hf_username(username: str) -> bool:
    """Validate that the Hugging Face username exists by checking the profile page."""
    # restrict_to_public=False: the target here is always the literal huggingface.co
    # domain (hardcoded below, or enforced by the regex above), never an
    # attacker-supplied host, so the SSRF guard in check_page_exists is not needed —
    # and huggingface.co can legitimately resolve to an internal address when called
    # from within HF's own infrastructure (split-horizon DNS), which that guard
    # would otherwise reject.
    if re.match(r"^https?://huggingface\.co/([^/]+)/?$", str(username).strip()):
        return check_page_exists(
            str(username).strip(), delay=1, max_retries=10, restrict_to_public=False
        )
    username = str(username).strip()
    hf_url = f"https://huggingface.co/{username}"
    return check_page_exists(hf_url, delay=1, max_retries=10, restrict_to_public=False)


def validate_model_details(tag: str) -> str:
    """Validate that the model details link is a valid URL and exists."""
    if tag is None or str(tag).strip() == "":
        return "Not submitted"
    safe_tag = str(tag).strip()
    if not safe_tag.startswith("https://"):
        return "Invalid link"
    is_real_url = check_page_exists(safe_tag, delay=2)
    if not is_real_url:
        return "Invalid link"
    else:
        return safe_tag