# DuckoNet: HTTP/3 к сайту через MASQUE

## HTTP/3 к сайту через MASQUE

MASQUE задаётся явно объектом `MasqueProxy`, импортируемым из `duckonet`:

```python
MasqueProxy(endpoint, username="", password="", carrier="h2",
            uri_template="/.well-known/masque/udp/{target_host}/{target_port}/")
```

`endpoint` — HTTPS URL прокси, без пути (кроме `/`), query и fragment. Credentials передавайте отдельными полями, а не в endpoint. HTTP API нормализует имя хоста и добавляет порт 443, если он пропущен. Для учётных данных поддерживается Basic authentication. Имя пользователя/пароль скрыты из `repr`, но сами объекты и ответы не следует произвольно журналировать.

```python
import os
from duckonet import HttpClient, MasqueProxy

proxy = MasqueProxy(
    "https://proxy.example:443",
    username=os.environ.get("MASQUE_USER", ""),
    password=os.environ.get("MASQUE_PASSWORD", ""),
)
with HttpClient(api_key=os.environ["DUCKONET_API_KEY"], browser="chrome",
                proxy=proxy, protocol="h3") as client:
    if not client.capabilities.http3_proxy:
        raise RuntimeError("Выбранный runtime/profile не предоставляет HTTP3 proxy")
    response = client.get("https://origin.example/data")
    response.raise_for_status()
    print(response.status_code, response.protocol)
```

Можно выбрать маршрут для одного запроса: `client.get(url, proxy=proxy, protocol="h3")`. Поддерживаются и `AsyncHttpClient` (`await client.get(...)`), и обычные sync/async `stream()` с теми же `proxy` и `protocol`. `client.capabilities.http3_proxy` и `client.capabilities.proxy_carriers` сообщают о возможности загруженной сессии; они не гарантируют доступность или пригодность конкретного сервера прокси.

Маршрут использует HTTP/2 поверх TLS к прокси, CONNECT-UDP с reliable capsules RFC9297 и строгий HTTP/3 к конечному HTTPS-серверу. Прокси обязан поддерживать этот способ передачи и канонический URI template. Другие `carrier` или `uri_template` отклоняются; HTTPS/3 carrier и datagram-only peer этим API не заявлены. Ответ `response.protocol` описывает протокол сайта, а не внешнее соединение к прокси.

Для `MasqueProxy` задайте `protocol="h3"`: фасад выберет `h3_only`. Расширенный вариант выбора того же режима — `application="h3_only"` при создании клиента. AUTO и H1/H2 для этого маршрута не предоставляются. Ошибка туннеля не превращается в прямой запрос или понижение протокола. Для обычных HTTP/HTTPS/SOCKS5/SOCKS5H proxy URLs прежняя семантика сохраняется: они не становятся MASQUE и не дают H3-туннель. SOCKS5 UDP ASSOCIATE не реализован.

Hostname и цепочка TLS проверяются для прокси и сайта. Один `ca_bundle` клиента используется для обеих проверок; отдельных proxy CA настроек нет. Если нужен свой доверенный CA, передайте абсолютный локальный путь к bundle, который подходит обоим узлам. Учётные данные прокси не предназначены для origin. Адрес самого прокси может разрешаться локально; целевое имя передаётся в CONNECT-UDP с сохранением SNI и проверки сертификата конечного сайта.

При фатальном отказе транспорта туннеля текущее действие возвращает исходную ошибку;
неисправное соединение исключается из повторного использования. Библиотека не повторяет
запрос автоматически. Следующий запрос выполняется только явным вызовом приложения
и может открыть новое соединение через тот же прокси; успех зависит от его доступности.
Решение повторить операцию принимает приложение с учётом возможных побочных эффектов.

Параметры запроса/stream, cookies, HTTP сроки и HTTP исключения остаются параметрами существующего HTTP API. Неподдержанные сочетания маршрута/протокола/профиля отклоняются; для анализа HTTP-ошибок используйте действующие `NativeError` и `describe_http_error`. Отдельного публичного `MasqueError` нет. Доступ по API-ключу и выбранным подписанным профилям сохраняет обычные правила DuckoNet.

### Асинхронный запрос через MASQUE

```python
import asyncio
import os
from duckonet import AsyncHttpClient, MasqueProxy

async def main():
    proxy = MasqueProxy(
        os.environ["MASQUE_ENDPOINT"],
        username=os.environ.get("MASQUE_USER", ""),
        password=os.environ.get("MASQUE_PASSWORD", ""),
    )
    async with AsyncHttpClient(
        api_key=os.environ["DUCKONET_API_KEY"], browser="chrome",
        proxy=proxy, protocol="h3",
    ) as client:
        response = await client.get("https://origin.example/data")
        response.raise_for_status()
        print(response.status_code, response.protocol)

asyncio.run(main())
```

`stream()` открывается через `with client.stream("GET", url, ...)` или
`async with client.stream("GET", url, ...)`; содержимое читается через
`stream.iter_bytes()` или `stream.iter_bytes()` в `async for` соответственно.
Маршрут, проверка TLS и строгий H3 действуют и для потока.

### Все параметры MasqueProxy

| Параметр | Значение по умолчанию и ограничения |
|---|---|
| `endpoint` | Обязателен. HTTPS URL с hostname; порт 443 по умолчанию, явный порт 1…65535. Только пустой путь или `/`, без query, fragment, пробелов, обратной косой черты и credentials в URL. Длина нормализованного endpoint — до 4096 UTF-8 байт. |
| `username` | `""`. Строка до 1024 UTF-8 байт, без управляющих символов. |
| `password` | `""`. Те же ограничения; передавайте отдельно от URL. |
| `carrier` | `"h2"`. Поддерживается только это значение: HTTP/2 поверх TLS к прокси. |
| `uri_template` | `"/.well-known/masque/udp/{target_host}/{target_port}/"`. Поддерживается только этот шаблон. Хост и порт назначения подставляет транспорт. |

Объект описывает неизменяемый маршрут. Проверка аргументов выполняется при использовании
в HTTP API; само создание `MasqueProxy(...)` не устанавливает соединение и не проверяет сервер.
Пустые credentials означают работу без заданного имени и пароля; сервер может требовать авторизацию.
Неверные параметры дают `ValueError` или `TypeError`; неподдержанный профиль или транспорт
может дать `NativeError`. Отказы прокси, TLS, аутентификации и CONNECT-UDP проверяйте по
HTTP диагностике `describe_http_error(error)`. Успешное создание объекта и capability
не заменяют успешного запроса.

## Профиль, cookies и полный прогон

WebSocket и MASQUE используют существующую установку, каталог `state_dir`, API-ключ
и подписанные профили DuckoNet. Для обновления программы сохраняйте прежний `state_dir`:
в нём находятся идентичность установки и история доверия. Cookies живут в памяти клиента;
для сохранения после закрытия приложения используйте export_data() и import_data().
Выбор доступных версий браузера по ключу остаётся общим для HTTP и WS.

Когда используется `version="last"`, клиент подготавливает новые доступные поколения
в фоне. Обрамляйте весь связанный прогон `client.run()`: HTTP, открываемые в нём WS
и потоки используют закреплённое поколение до завершения своих операций.
Следующий внешний `run()` выбирает уже подготовленное поколение; вложенный `run()`
наследует текущий. Обновление не переключает профиль внутри открытого WS.
При явно выбранной версии профиль остаётся закреплённым.

```python
import os
from duckonet import HttpClient

with HttpClient(
    api_key=os.environ["DUCKONET_API_KEY"], browser="chrome", version="last",
) as client:
    with client.run():
        response = client.get("https://app.example/session")
        response.raise_for_status()
        with client.websocket("wss://app.example/events", origin="https://app.example") as ws:
            ws.send("start")
            print(ws.recv())
        print(client.browser_profile)
```

В асинхронном коде используются `async with client.run()` и `async with client.websocket(...)`.
Закрывайте WS и потоки внутри своего `run()`; сам выход из `run()` не является командой
немедленно прервать уже открытое соединение. Без явного `run()` автоматические операции
используют исходное поколение клиента. `client.profile_update_status` показывает состояние
подготовки профиля, а `client.browser_profile` — фактический профиль выбранного прогона.
Поколение не даёт бессрочное право на сетевую работу: действуют обычные проверки
срока действия, отзыва и доступа по ключу, в том числе при обмене по WS.

Cookies принадлежат клиенту и сохраняют штатные domain/path/secure/SameSite правила.
Они доступны HTTP и WS в пределах того же владельца и переживают штатную смену поколения.
Для общего cookie-состояния переиспользуйте один клиент; два независимых клиента не становятся
одним cookie jar только из-за одинакового адреса сервера. `cookies=` — разовое наложение,
а постоянными cookies управляйте через `client.cookies`.

## Поддерживаемые маршруты

| Сценарий | Поддержка |
|---|---|
| `ws://` / `wss://` напрямую | HTTP/1.1 Upgrade; текст, binary и permessage-deflate по согласованию. |
| WS через обычный HTTP/HTTPS/SOCKS5/SOCKS5H proxy | С учётом возможностей профиля и транспорта. `socks5` использует локальное разрешение цели; `socks5h` передаёт имя прокси. Для `wss` сохраняется проверка TLS сайта. |
| HTTP/3 к HTTPS-сайту через `MasqueProxy` | Строгий H3 внутри CONNECT-UDP, внешний carrier H2 с reliable capsules. |
| WS поверх H2/H3, WS через MASQUE | Не предоставляются. |
| H3 через обычную HTTP/HTTPS/SOCKS строку | Не предоставляется; SOCKS5 UDP ASSOCIATE отсутствует. |
| MASQUE AUTO, H1/H2 к сайту, внешний H1/H3 carrier или другой URI template | Не предоставляются в этом API. |

Внешнее соединение с прокси и соединение с сайтом имеют разные назначения.
`response.protocol` относится к сайту. Настройки браузерного профиля применяются к соединению с целевым сайтом.
TLS-соединение с прокси устанавливается отдельно. Библиотека не гарантирует полное
совпадение всех сетевых признаков WebSocket и TLS прокси с браузером.
