diff --git a/.gitignore b/.gitignore index 428fc37..45c021d 100644 --- a/.gitignore +++ b/.gitignore @@ -12,12 +12,27 @@ token*.* # Внутренняя аналитика — не для публичного доступа PROJECT-AUDIT.md AGENT-DIAGNOSIS.md +AGENT.md CONTEXT.md prompt-opus-*.md docs/questions-devops.md +docs/PLAN-*.md +docs/STATE-*.md +docs/cluster-cleanup-*.md +docs/tests-*.md +docs/test-api.sh +docs/nubes-design-system.md +docs/deploy-vm.md # Результаты код-ревью агентов -research/opus-review-*.md +research/ + +# Тестовые данные (токены, HAR, etc) +Files/ +token*.* + +# Логи тестов +test-results/ research/opus-full-review-*.md research/REVIEW-SUMMARY.md docs/STATE-old.md diff --git a/AGENT.md b/AGENT.md deleted file mode 100644 index f0a126b..0000000 --- a/AGENT.md +++ /dev/null @@ -1,473 +0,0 @@ -# AGENT.md — Полное руководство по проекту IPWhiteList - -> Этот файл предназначен для AI-агентов и разработчиков без контекста. -> Читай этот файл **первым** перед любой задачей. -> Обновляй после значимых изменений. - ---- - -## 1. Суть проекта - -**IP WhiteList** — веб-приложение для управления белыми IP-списками (CIDR) по компаниям. - -- Каждая компания имеет свой список IPv4/CIDR записей. -- Есть лимит записей на компанию (по умолчанию 15, admin может менять). -- Есть одна роль admin (задаётся через env) и обычные пользователи. -- Аутентификация через JWT (RS256): в проде — внешний OIDC (Keycloak/Nubes auth-api), в dev — mock. -- Весь UI — server-side rendered (EJS). API — REST JSON. - ---- - -## 2. Репозиторий и деплой - -| Параметр | Значение | -|---|---| -| Репо (рабочий) | `https://gitea.services.ngcloud.ru/Nail/ipwhitelist-app.git` ветка `master` | -| Локальный путь | `/home/naeel/ipwhitelist-app` | -| Деплой URL | `https://white.nodejsk8s.dev.nubes.ru` | -| Платформа | Nubes k8s — редеплой через Nubes UI из Gitea после `git push` | -| Версия | `package.json` → поле `version` (сейчас `0.5.0`) | - -**Правило версий:** bump `version` в `package.json` при каждом значимом изменении. - ---- - -## 3. Стек - -``` -Node.js 18+ + Express 4 -EJS (server-side rendering) -PostgreSQL 17 (pg pool, SSL require) -jsonwebtoken RS256 -express-session + connect-pg-simple -helmet (CSP) -express-rate-limit -supertest (тесты) -``` - ---- - -## 4. Переменные окружения - -```env -NODE_ENV=development # production → отключает DEV_MODE, требует SESSION_SECRET -PORT=3000 - -# Auth -DEV_MODE=true # true → mock JWT, ЗАПРЕЩЕНО в production -DEV_SECRET= # опционально: пароль для /dev-login на staging -ADMIN_CLIENT_ID=WZ01112 # clientId пользователя с правами admin -JWT_ISSUER=mock-auth-api # issuer в JWT - -# OIDC (если DEV_MODE=false) -KC_BASE_URL=https://keycloak.nubes.ru/realms/cloud -KC_CLIENT_ID= # пусто = mock mode -KC_CLIENT_SECRET= -APP_URL=http://localhost:3000 - -# База данных -DB_HOST=write.bde8229b-1381-4330-b24b-727ad73fcb44.dev.nubes.ru -DB_PORT=5432 -DB_NAME=ipwhitelist -DB_USER=super -DB_PASS= -DB_SSLMODE=require - -# Сессия -SESSION_SECRET= # обязателен в production (fail-fast при запуске) - -# Прочее -DEFAULT_LIMIT=15 # лимит записей по умолчанию (если нет custom_limit) -API_BASE=http://localhost:3000 # UI-слой → куда идти за /api/v1/* -``` - ---- - -## 5. Структура файлов — карта ответственности - -``` -server.js ← точка входа: helmet, session, монтирование роутеров -src/ - config.js ← MOCK_USERS, APP_VERSION, backUrl() - db.js ← pg Pool, checkConnection() - auth.js ← initAuth() → { middleware, issueMockToken, ... } - queries.js ← ВСЕ SQL-запросы — единственное место работы с БД - validators.js ← validate(cidr), overlaps(), aggregateCIDRs(), BLOCKED_RANGES - api/ - index.js ← createApiRouter({auth, q}) — монтируется на /api/v1 - middleware/ - bearerAuth.js ← requireBearer: нет заголовка → 401 JSON - routes/ - entries.js ← GET/POST/PATCH/DELETE /api/v1/entries + GET /api/v1/entries/export - admin.js ← GET /api/v1/companies, PATCH limit, GET /api/v1/audit - middleware/ - rateLimit.js ← mutationLimiter, exportLimiter, authLimiter - session.js ← createSessionMiddleware(pool) - csrf.js ← initCsrf() — НЕ используется в новом UI - csp.js ← CSP_DIRECTIVES для helmet - routes/ - export.js ← GET /export — требует авторизации (Bearer) - exp.js ← ⚠️ ВРЕМЕННЫЙ GET /exp — без авторизации (удалить после prod) -ui/ - index.js ← createUiRouter({auth, MOCK_USERS, authLimiter}) - api-client.js ← HTTP-клиент к /api/v1/* (берёт Bearer из session.token) - routes/ - auth.js ← GET/POST /login, /login-token, /logout - entries.js ← /, /add, /edit/:id, /delete/:id - admin.js ← /admin, /admin/limit/:id, /audit - export.js ← /export (UI-обёртка, проксирует к /api/v1/entries/export) -views/ - index.ejs ← главная: таблица записей + форма добавления - admin.ejs ← список компаний с лимитами (admin only) - audit.ejs ← журнал аудита (admin only) - ui-login.ejs ← страница входа (mock-пресеты + вставить Bearer вручную) - error.ejs ← 403/404/500 -sql/ - schema.sql ← DDL: CREATE TABLE IF NOT EXISTS (идемпотентно) -tests/ - integration.js ← интеграционные тесты (supertest + реальный PG) - run-tests.js ← runner -``` - ---- - -## 6. Поток запроса — HTTP → БД - -### 6.1 REST API запрос - -``` -HTTP Request - → server.js: helmet, session, cookieParser - → GET /exp → src/routes/exp.js (без auth, временный) ← ТОЛЬКО ЭТО без auth - → /api/v1/* → src/api/index.js - → requireBearer : нет заголовка Authorization: Bearer → 401 JSON - → auth.middleware : валидация JWT → req.user = { clientId, isAdmin, email, ... } - → /entries/* → src/api/routes/entries.js - → resolveCompany() : admin+?company=id → getCompanyById() | иначе getOrCreateCompany() - → q.*() : SQL через pg pool - → JSON response - → /companies, /audit → src/api/routes/admin.js - → apiRequireAdmin() : !isAdmin → 403 JSON - → q.*() -``` - -### 6.2 UI запрос (SSR) - -``` -HTTP Request - → server.js → ui/index.js (createUiRouter) - → createAuthRouter : /login, /login-token, /logout — публичные - → requireToken : нет session.token → redirect /login - → resolveUser : jwt.decode(token) → req.user (без верификации подписи) - → createEntriesRouter, createAdminRouter, createExportRouter - → ui/api-client.js : HTTP запрос к /api/v1/* с Bearer из session.token - → разбирает JSON ответ - → res.render('view.ejs', data) - → Если API вернул 401 → destroy session → redirect /login -``` - ---- - -## 7. req.user — структура - -После прохождения `auth.middleware` (API) или `resolveUser` (UI): - -```js -req.user = { - clientId: 'WZ01112', // из JWT claim ClientID (заглавная!) - companyId: 'uuid-...', // из JWT claim company_id - companyName: 'Nubes Admin', // из JWT claim company_name - email: 'admin@nubes.ru', // из JWT claim email - isAdmin: true, // clientId === ADMIN_CLIENT_ID -} -``` - ---- - -## 8. База данных — схема - -```sql --- Компании (1 строка на WZ-номер, создаётся при первом входе) -companies ( - id SERIAL PRIMARY KEY, - client_id VARCHAR(64) UNIQUE NOT NULL, -- WZ01112, WZ01325 и т.д. - name VARCHAR(255), - custom_limit INTEGER DEFAULT NULL, -- NULL = использует DEFAULT_LIMIT - created_at TIMESTAMPTZ, - updated_at TIMESTAMPTZ -) - --- Записи белого списка (soft delete через deleted_at) -whitelist_entries ( - id SERIAL PRIMARY KEY, - company_id INTEGER REFERENCES companies(id), - value_cidr VARCHAR(18), -- '192.168.1.0/24' - comment VARCHAR(255), - created_by VARCHAR(255), -- email - created_at TIMESTAMPTZ, - updated_by VARCHAR(255), - updated_at TIMESTAMPTZ, - deleted_by VARCHAR(255), - deleted_at TIMESTAMPTZ -- NULL = активная запись -) --- INDEX: uq_entries_active_cidr(company_id, value_cidr) WHERE deleted_at IS NULL - --- Аудит -audit_log ( - id SERIAL PRIMARY KEY, - user_email VARCHAR(255), - company_id INTEGER, - action VARCHAR(32), -- 'CREATE' | 'UPDATE' | 'DELETE' - old_value TEXT, - new_value TEXT, - entry_id INTEGER, - created_at TIMESTAMPTZ -) -``` - ---- - -## 9. src/queries.js — все функции - -| Функция | Сигнатура | Описание | -|---|---|---| -| `getOrCreateCompany` | `(clientId, companyName) → company` | UPSERT компании по clientId | -| `getCompanyById` | `(id) → company\|null` | по числовому PK | -| `getLimit` | `(company) → number` | custom_limit ?? DEFAULT_LIMIT | -| `listEntries` | `(companyId, includeDeleted=false) → []` | список записей | -| `createEntry` | `(companyId, rawCidr, comment, email) → {entry, wasNormalized}` | INSERT + audit (транзакция) | -| `updateEntry` | `(entryId, companyId, rawCidr, comment, email) → {entry, wasNormalized}` | UPDATE + audit (транзакция) | -| `deleteEntry` | `(entryId, companyId, email) → void` | soft delete + audit (транзакция) | -| `getAllCompanies` | `() → []` | все компании + active_count | -| `setLimit` | `(companyId, newLimit) → void` | custom_limit = newLimit | -| `getExportCIDRs` | `(companyId=null) → string[]` | CIDR строки; null = все компании | -| `logAudit` | `(email, companyId, action, old, new, entryId, db?) → void` | внутренняя, вызывается из транзакций | -| `getAudit` | `(companyId=null) → []` | журнал с JOIN companies | - ---- - -## 10. src/validators.js — функции - -```js -validate(rawValue) - → { cidr: '1.2.3.0/24', wasNormalized: boolean } - → throws Error если невалидно - // Нормализует host bits, проверяет BLOCKED_RANGES - -overlaps(cidr1, cidr2) → boolean - // true если диапазоны пересекаются - -aggregateCIDRs(cidrArray) → string[] - // Объединяет пересекающиеся и смежные диапазоны → минимальный набор - -BLOCKED_RANGES // приватные/зарезервированные диапазоны — запрещены для добавления -``` - ---- - -## 11. src/auth.js — initAuth() - -```js -const auth = await initAuth(); -// Возвращает: -auth.middleware // Express middleware: валидирует Bearer JWT → req.user -auth.issueMockToken(claims) → jwtString // только в mock-режиме, для тестов -auth.jwksHandler // GET /.well-known/jwks.json (только mock) -// OIDC-режим: -auth.getAuthUrl(state, nonce) → string // redirect URL на Keycloak -auth.exchangeCode(code) → {token, user} // обмен code → JWT -``` - -**Логика определения режима:** -- `KC_CLIENT_ID && KC_CLIENT_SECRET` → OIDC-режим -- иначе → mock-режим (генерирует RSA-пару при старте) - ---- - -## 12. ui/api-client.js — интерфейс - -```js -api.token(req) // → Bearer string из req.session.token -api.get(path, token) // → { status, data } -api.post(path, token, body) // → { status, data } -api.patch(path, token, body) // → { status, data } -api.delete(path, token) // → { status, data } -// path примеры: '/api/v1/entries', '/api/v1/entries/42', '/api/v1/companies/5/limit' -``` - ---- - -## 13. Все HTTP эндпоинты - -### Публичные (без auth) -| Метод | URL | Файл | Описание | -|---|---|---|---| -| GET | `/healthz` | server.js | k8s liveness probe | -| GET | `/.well-known/jwks.json` | server.js | mock JWKS (только DEV_MODE) | -| GET | `/login` | ui/routes/auth.js | страница входа | -| POST | `/login` | ui/routes/auth.js | выбор mock-пользователя | -| POST | `/login-token` | ui/routes/auth.js | вставить Bearer вручную | -| GET | `/logout` | ui/routes/auth.js | destroy session | -| GET | `/callback` | ui/routes/auth.js | OIDC callback | -| GET | `/exp` | src/routes/exp.js | ⚠️ ВРЕМЕННЫЙ: экспорт без auth | - -### UI (требуют session.token) -| Метод | URL | Файл | Описание | -|---|---|---|---| -| GET | `/` | ui/routes/entries.js | главная страница | -| POST | `/add` | ui/routes/entries.js | добавить запись | -| POST | `/edit/:id` | ui/routes/entries.js | изменить запись | -| POST | `/delete/:id` | ui/routes/entries.js | удалить запись | -| GET | `/admin` | ui/routes/admin.js | список компаний (admin) | -| POST | `/admin/limit/:id` | ui/routes/admin.js | изменить лимит (admin) | -| GET | `/audit` | ui/routes/admin.js | журнал аудита (admin) | -| GET | `/export` | ui/routes/export.js | скачать whitelist.txt | - -### REST API `/api/v1` (требуют Bearer JWT) -| Метод | URL | Файл | Кто | Описание | -|---|---|---|---|---| -| GET | `/api/v1/entries` | api/routes/entries.js | все | список + limit + used | -| GET | `/api/v1/entries?company=` | api/routes/entries.js | admin | записи другой компании | -| POST | `/api/v1/entries` | api/routes/entries.js | все | создать | -| PATCH | `/api/v1/entries/:id` | api/routes/entries.js | все | обновить | -| DELETE | `/api/v1/entries/:id` | api/routes/entries.js | все | удалить | -| GET | `/api/v1/entries/export` | api/routes/entries.js | все | text/plain CIDR | -| GET | `/api/v1/companies` | api/routes/admin.js | admin | все компании | -| PATCH | `/api/v1/companies/:id/limit` | api/routes/admin.js | admin | установить лимит | -| GET | `/api/v1/audit` | api/routes/admin.js | admin | журнал аудита | - ---- - -## 14. Rate limiting - -| Лимитер | Применяется к | Лимит | -|---|---|---| -| `authLimiter` | POST /login, /login-token, GET /callback | 10 req / 5 мин | -| `exportLimiter` | GET /export, GET /exp, GET /api/v1/entries/export | 20 req / мин | -| `mutationLimiter` | POST /add, /edit/:id, /delete/:id | 30 req / мин | - ---- - -## 15. Mock-пользователи (DEV_MODE=true) - -| id | clientId | Роль | companyName | email | -|---|---|---|---|---| -| `admin` | `WZ01112` | **admin** | Nubes Admin | admin@nubes.ru | -| `test` | `WZ01325` | user | Тест | tazet@narod.ru | -| `client2` | `WZ02001` | user | Вторая Компания | user2@example.com | - -Файл: `src/config.js` → `MOCK_USERS` - ---- - -## 16. Бизнес-логика — важные правила - -1. **Изоляция компаний**: везде `company_id` в WHERE — данные одной компании недоступны другой. -2. **Транзакции с FOR UPDATE**: createEntry/updateEntry/deleteEntry блокируют строку компании — защита от race condition при параллельных вставках. -3. **Soft delete**: `deleted_at IS NULL` = активная запись. Удалённые остаются в БД. -4. **Дубли/пересечения**: при создании и обновлении проверяются через `overlaps()` против всех активных записей компании. -5. **Лимит**: снижение лимита не удаляет записи — только блокирует создание новых. -6. **CIDR нормализация**: `validate()` нормализует host bits (192.168.1.5/24 → 192.168.1.0/24), возвращает `wasNormalized: true`. -7. **Агрегация при экспорте**: `aggregateCIDRs()` объединяет смежные диапазоны — минимальный набор. - ---- - -## 17. HTTP коды ошибок API - -| Код | Когда | -|---|---| -| 400 | Невалидный CIDR, некорректные параметры | -| 401 | Нет/невалидный Bearer токен | -| 403 | Не admin на admin-endpoint | -| 404 | Запись/компания не найдена | -| 409 | Дубликат CIDR, пересечение, лимит исчерпан | -| 500 | Ошибки БД и прочие | - ---- - -## 18. Запуск и тесты - -```bash -# Установка -npm install - -# Запуск локально (нужен .env с DEV_MODE=true + БД) -node server.js - -# API-тесты (104 штуки — CRUD, валидация, изоляция, admin, лимиты) -npm test - -# Стресс-тесты (191 штука — concurrency, races, boundary IPs, masks, auth attacks) -npm run test:stress - -# Старый UI-тест (неактуален, использует /dev-login + CSRF) -npm run test:integration -``` - -**Все тесты требуют реального PG подключения** (переменные из `.env`). - -### Структура тестов (`tests/stress.js`): - -| Раздел | Что проверяет | -|---|---| -| S1 | Concurrency: FOR UPDATE lock, 10-15 параллельных вставок | -| S2 | Limit enforcement под нагрузкой, setLimit(0) | -| S3 | Mixed CRUD: concurrent update+delete+create | -| S4 | Auth attacks: fake key, none algorithm, missing claims | -| S5 | Edge cases: emoji, null byte, null/''/undefined | -| S6 | Validator: overlaps, 14 blocked ranges | -| S7 | Session vs Bearer conflict (bearerMiddleware) | -| S8 | All CIDR masks /0–/33 (34 теста) | -| S9 | IP boundary values: 0.0.0.0 … 255.255.255.255 (32 теста) | -| S10 | Token manipulation: expired, wrong issuer, future, no claims | -| S11 | DB: double delete, update deleted, isolation | -| S12 | Unicode: full-width, zero-width, RTL, NFC/NFD | -| S13 | Admin privilege escalation | -| S14 | Session: login→logout, cookie reuse | -| S15 | Rate limit /exp, /login | -| S16 | Export edge cases | -| S17 | Massive concurrency: 50 createEntry, 30 delete, 20 getOrCreate | -| S18 | CIDR normalization: host bits обнуление | -| S19 | Each blocked range individually (14) | -| S20 | Sequential CRUD patterns | -| S21 | Admin cross-company CRUD | -| S22 | Weird CIDR input: пробелы, табы, \n, 256, IPv6 (20) | - ---- - -## 19. Известные проблемы (баги) — исправлены в v0.5.0 - -| Проблема | Статус | -|---|---| -| `auth.middleware` читал `req.session.user` перед Bearer → Bearer-only API нарушалось | ✅ Добавлен `bearerMiddleware` | -| Mock admin `isAdmin: undefined` в сессии | ✅ `user.role === 'admin'` | -| Сброс лимита (пустое поле → NaN → 400) | ✅ `null` при пустом поле | -| `setLimit()` не проверял `rowCount` → 404 на неизвестную компанию | ✅ 404 с `e.status` | -| Версия не поднималась при правках | ✅ `0.4.1` → `0.5.0` (9 бампов) | -| CSP/ingress блокировал inline-обработчики (onclick, onchange) | ✅ Event delegation через `\n\n\n \n \n \n \n Личный кабинет\n \n \n \n \n \n \n \n \n \n\n" - }, - "redirectURL": "", - "headersSize": -1, - "bodySize": -1, - "_transferSize": 0, - "_error": null, - "_fetchedViaServiceWorker": true, - "_serviceWorkerResponseSource": "fallback-code" - }, - "serverIPAddress": "", - "startedDateTime": "2026-05-30T04:35:24.993Z", - "time": 42.025000031571835, - "timings": { - "blocked": 5.319000020686537, - "dns": -1, - "ssl": -1, - "connect": -1, - "send": 0, - "wait": 28.24699997755885, - "receive": 8.459000033326447, - "_blocked_queueing": 0.362000020686537, - "_workerStart": -1, - "_workerReady": -1, - "_workerFetchStart": 4.957, - "_workerRespondWithSettled": 32.943 - }, - "pageref": "page_3" - }, - { - "_initiator": { - "type": "parser", - "url": "https://deck-test.ngcloud.ru/authorization/callback?back=%2Fdashboard%2Fauthorization%2Fdashboard&code=604acf612225306a7184e63fa41e0615852ec2a05ce95a2c6cb1527a756a5508", - "lineNumber": 13 - }, - "_priority": "High", - "_resourceType": "script", - "cache": {}, - "request": { - "method": "GET", - "url": "https://deck-test.ngcloud.ru/spa.js", - "httpVersion": "http/2.0", - "headers": [ - { - "name": "Origin", - "value": "https://deck-test.ngcloud.ru" - }, - { - "name": "sec-ch-ua-platform", - "value": "\"Windows\"" - }, - { - "name": "Referer", - "value": "https://deck-test.ngcloud.ru/" - }, - { - "name": "User-Agent", - "value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36" - }, - { - "name": "sec-ch-ua", - "value": "\"Chromium\";v=\"148\", \"Google Chrome\";v=\"148\", \"Not/A)Brand\";v=\"99\"" - }, - { - "name": "DNT", - "value": "1" - }, - { - "name": "sec-ch-ua-mobile", - "value": "?0" - } - ], - "queryString": [], - "cookies": [], - "headersSize": -1, - "bodySize": 0 - }, - "response": { - "status": 200, - "statusText": "", - "httpVersion": "http/2.0", - "headers": [ - { - "name": "x-cache-status", - "value": "HIT" - }, - { - "name": "vary", - "value": "Accept-Encoding" - }, - { - "name": "content-security-policy", - "value": "upgrade-insecure-requests;" - }, - { - "name": "etag", - "value": "\"974a449bd2f06c060436c774dc1a2204\"" - }, - { - "name": "access-control-allow-methods", - "value": "GET, HEAD, OPTIONS" - }, - { - "name": "x-amz-request-id", - "value": "tx000009a46a5debf4f7940-006a197907-4fc114-dc1" - }, - { - "name": "accept-ranges", - "value": "bytes" - }, - { - "name": "access-control-allow-origin", - "value": "*" - }, - { - "name": "content-length", - "value": "44928" - }, - { - "name": "date", - "value": "Fri, 29 May 2026 18:27:09 GMT" - }, - { - "name": "x-rgw-object-type", - "value": "Normal" - }, - { - "name": "content-type", - "value": "text/javascript" - }, - { - "name": "last-modified", - "value": "Fri, 15 May 2026 14:10:30 GMT" - }, - { - "name": "server", - "value": "ddos-guard" - }, - { - "name": "access-control-allow-headers", - "value": "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range" - } - ], - "cookies": [], - "content": { - "size": 44928, - "mimeType": "text/javascript", - "text": "KGZ1bmN0aW9uKCl7bGV0IGU9ZG9jdW1lbnQuY3JlYXRlRWxlbWVudChgbGlua2ApLnJlbExpc3Q7aWYoZSYmZS5zdXBwb3J0cyYmZS5zdXBwb3J0cyhgbW9kdWxlcHJlbG9hZGApKXJldHVybjtmb3IobGV0IGUgb2YgZG9jdW1lbnQucXVlcnlTZWxlY3RvckFsbChgbGlua1tyZWw9Im1vZHVsZXByZWxvYWQiXWApKW4oZSk7bmV3IE11dGF0aW9uT2JzZXJ2ZXIoZT0+e2ZvcihsZXQgdCBvZiBlKWlmKHQudHlwZT09PWBjaGlsZExpc3RgKWZvcihsZXQgZSBvZiB0LmFkZGVkTm9kZXMpZS50YWdOYW1lPT09YExJTktgJiZlLnJlbD09PWBtb2R1bGVwcmVsb2FkYCYmbihlKX0pLm9ic2VydmUoZG9jdW1lbnQse2NoaWxkTGlzdDohMCxzdWJ0cmVlOiEwfSk7ZnVuY3Rpb24gdChlKXtsZXQgdD17fTtyZXR1cm4gZS5pbnRlZ3JpdHkmJih0LmludGVncml0eT1lLmludGVncml0eSksZS5yZWZlcnJlclBvbGljeSYmKHQucmVmZXJyZXJQb2xpY3k9ZS5yZWZlcnJlclBvbGljeSksZS5jcm9zc09yaWdpbj09PWB1c2UtY3JlZGVudGlhbHNgP3QuY3JlZGVudGlhbHM9YGluY2x1ZGVgOmUuY3Jvc3NPcmlnaW49PT1gYW5vbnltb3VzYD90LmNyZWRlbnRpYWxzPWBvbWl0YDp0LmNyZWRlbnRpYWxzPWBzYW1lLW9yaWdpbmAsdH1mdW5jdGlvbiBuKGUpe2lmKGUuZXApcmV0dXJuO2UuZXA9ITA7bGV0IG49dChlKTtmZXRjaChlLmhyZWYsbil9fSkoKSxjb25zb2xlLmluZm8oYEBudWJlcy9yb290LWFwcEAwLjIuMjUgbG9hZGVkYCk7ZnVuY3Rpb24gZShlKXtyZXR1cm57YWxsOmV8fD1uZXcgTWFwLG9uOmZ1bmN0aW9uKHQsbil7dmFyIHI9ZS5nZXQodCk7cj9yLnB1c2gobik6ZS5zZXQodCxbbl0pfSxvZmY6ZnVuY3Rpb24odCxuKXt2YXIgcj1lLmdldCh0KTtyJiYobj9yLnNwbGljZShyLmluZGV4T2Yobik+Pj4wLDEpOmUuc2V0KHQsW10pKX0sZW1pdDpmdW5jdGlvbih0LG4pe3ZhciByPWUuZ2V0KHQpO3ImJnIuc2xpY2UoKS5tYXAoZnVuY3Rpb24oZSl7ZShuKX0pLChyPWUuZ2V0KGAqYCkpJiZyLnNsaWNlKCkubWFwKGZ1bmN0aW9uKGUpe2UodCxuKX0pfX19dmFyIHQ9ZSgpLG49KCk9Pntjb25zb2xlLmxvZyhgU3RhcnRpbmcgdmVyc2lvbi11cGRhdGVyYCx0KTtsZXQgZT0hMTt0Lm9uKGB2ZXJzaW9uOmF2YWlsYWJsZWAsdD0+e2U9ITA7bGV0IG49dCYmYHBhY2thZ2VzYGluIHQmJnQucGFja2FnZXM/Lmxlbmd0aD9gIOKAlCB1cGRhdGVkIHBhY2thZ2VzOiAke3QucGFja2FnZXMuam9pbihgLCBgKX1gOmBgO2NvbnNvbGUuaW5mbyhgW3ZlcnNpb24tdXBkYXRlcl0gbmV3IHZlcnNpb24gYXZhaWxhYmxlLCB3aWxsIHJlbG9hZCBvbiBuZXh0IHJvdXRlIGNoYW5nZWAsdD8/YGApLG4mJmNvbnNvbGUuaW5mbyhgW3ZlcnNpb24tdXBkYXRlcl0ke259YCl9KSx3aW5kb3cuYWRkRXZlbnRMaXN0ZW5lcihgc2luZ2xlLXNwYTpiZWZvcmUtcm91dGluZy1ldmVudGAsdD0+e2lmKCFlKXJldHVybjtsZXR7b2xkVXJsOm4sbmV3VXJsOnJ9PXQuZGV0YWlsO24hPT1yJiZsb2NhdGlvbi5yZWxvYWQoKX0pfSxyPU9iamVjdC5mcmVlemUoe19fcHJvdG9fXzpudWxsLGdldCBzdGFydCgpe3JldHVybiBRZX0sZ2V0IGVuc3VyZUpRdWVyeVN1cHBvcnQoKXtyZXR1cm4gTWV9LGdldCBzZXRCb290c3RyYXBNYXhUaW1lKCl7cmV0dXJuIGhlfSxnZXQgc2V0TW91bnRNYXhUaW1lKCl7cmV0dXJuIGdlfSxnZXQgc2V0VW5tb3VudE1heFRpbWUoKXtyZXR1cm4gX2V9LGdldCBzZXRVbmxvYWRNYXhUaW1lKCl7cmV0dXJuIHZlfSxnZXQgcmVnaXN0ZXJBcHBsaWNhdGlvbigpe3JldHVybiBWZX0sZ2V0IHVucmVnaXN0ZXJBcHBsaWNhdGlvbigpe3JldHVybiBVZX0sZ2V0IGdldE1vdW50ZWRBcHBzKCl7cmV0dXJuIFJlfSxnZXQgZ2V0QXBwU3RhdHVzKCl7cmV0dXJuIHplfSxnZXQgdW5sb2FkQXBwbGljYXRpb24oKXtyZXR1cm4gV2V9LGdldCBjaGVja0FjdGl2aXR5RnVuY3Rpb25zKCl7cmV0dXJuIEhlfSxnZXQgZ2V0QXBwTmFtZXMoKXtyZXR1cm4gUn0sZ2V0IHBhdGhUb0FjdGl2ZVdoZW4oKXtyZXR1cm4gcWV9LGdldCBuYXZpZ2F0ZVRvVXJsKCl7cmV0dXJuIHhlfSxnZXQgcGF0Y2hIaXN0b3J5QXBpKCl7cmV0dXJuIGtlfSxnZXQgdHJpZ2dlckFwcENoYW5nZSgpe3JldHVybiBZZX0sZ2V0IGFkZEVycm9ySGFuZGxlcigpe3JldHVybiBjfSxnZXQgcmVtb3ZlRXJyb3JIYW5kbGVyKCl7cmV0dXJuIGx9LGdldCBtb3VudFJvb3RQYXJjZWwoKXtyZXR1cm4gZmV9LGdldCBOT1RfTE9BREVEKCl7cmV0dXJuIGZ9LGdldCBMT0FESU5HX1NPVVJDRV9DT0RFKCl7cmV0dXJuIHB9LGdldCBOT1RfQk9PVFNUUkFQUEVEKCl7cmV0dXJuIG19LGdldCBCT09UU1RSQVBQSU5HKCl7cmV0dXJuIGh9LGdldCBOT1RfTU9VTlRFRCgpe3JldHVybiBnfSxnZXQgTU9VTlRJTkcoKXtyZXR1cm4gX30sZ2V0IFVQREFUSU5HKCl7cmV0dXJuIGVlfSxnZXQgTE9BRF9FUlJPUigpe3JldHVybiB0ZX0sZ2V0IE1PVU5URUQoKXtyZXR1cm4gdn0sZ2V0IFVOTE9BRElORygpe3JldHVybiBifSxnZXQgVU5NT1VOVElORygpe3JldHVybiB5fSxnZXQgU0tJUF9CRUNBVVNFX0JST0tFTigpe3JldHVybiB4fX0pLGk9KHR5cGVvZiBnbG9iYWxUaGlzPGB1YD9nbG9iYWxUaGlzOnR5cGVvZiB3aW5kb3c8YHVgP3dpbmRvdzp0eXBlb2YgZ2xvYmFsPGB1YD9nbG9iYWw6dHlwZW9mIHNlbGY8YHVgP3NlbGY6e30pLkN1c3RvbUV2ZW50LGE9ZnVuY3Rpb24oKXt0cnl7dmFyIGU9bmV3IGkoYGNhdGAse2RldGFpbDp7Zm9vOmBiYXJgfX0pO3JldHVybiBlLnR5cGU9PT1gY2F0YCYmZS5kZXRhaWwuZm9vPT09YGJhcmB9Y2F0Y2h7fXJldHVybiExfSgpP2k6dHlwZW9mIGRvY3VtZW50PGB1YCYmdHlwZW9mIGRvY3VtZW50LmNyZWF0ZUV2ZW50PT1gZnVuY3Rpb25gP2Z1bmN0aW9uKGUsdCl7dmFyIG49ZG9jdW1lbnQuY3JlYXRlRXZlbnQoYEN1c3RvbUV2ZW50YCk7cmV0dXJuIHQ/bi5pbml0Q3VzdG9tRXZlbnQoZSx0LmJ1YmJsZXMsdC5jYW5jZWxhYmxlLHQuZGV0YWlsKTpuLmluaXRDdXN0b21FdmVudChlLCExLCExLHZvaWQgMCksbn06ZnVuY3Rpb24oZSx0KXt2YXIgbj1kb2N1bWVudC5jcmVhdGVFdmVudE9iamVjdCgpO3JldHVybiBuLnR5cGU9ZSx0PyhuLmJ1YmJsZXM9ISF0LmJ1YmJsZXMsbi5jYW5jZWxhYmxlPSEhdC5jYW5jZWxhYmxlLG4uZGV0YWlsPXQuZGV0YWlsKToobi5idWJibGVzPSExLG4uY2FuY2VsYWJsZT0hMSxuLmRldGFpbD12b2lkIDApLG59LG89W107ZnVuY3Rpb24gcyhlLHQsbil7bGV0IHI9ZChlLHQsbik7by5sZW5ndGg/by5mb3JFYWNoKChlPT5lKHIpKSk6c2V0VGltZW91dCgoKCk9Pnt0aHJvdyByfSkpfWZ1bmN0aW9uIGMoZSl7aWYodHlwZW9mIGUhPWBmdW5jdGlvbmApdGhyb3cgRXJyb3IodSgyOCwhMSkpO28ucHVzaChlKX1mdW5jdGlvbiBsKGUpe2lmKHR5cGVvZiBlIT1gZnVuY3Rpb25gKXRocm93IEVycm9yKHUoMjksITEpKTtsZXQgdD0hMTtyZXR1cm4gbz1vLmZpbHRlcigobj0+e2xldCByPW49PT1lO3JldHVybiB0fHw9ciwhcn0pKSx0fWZ1bmN0aW9uIHUoZSx0LC4uLm4pe3JldHVybmBzaW5nbGUtc3BhIG1pbmlmaWVkIG1lc3NhZ2UgIyR7ZX06ICR7dD90K2AgYDpgYH1TZWUgaHR0cHM6Ly9zaW5nbGUtc3BhLmpzLm9yZy9lcnJvci8/Y29kZT0ke2V9JHtuLmxlbmd0aD9gJmFyZz0ke24uam9pbihgJmFyZz1gKX1gOmBgfWB9ZnVuY3Rpb24gZChlLHQsbil7bGV0IHI9YCR7dyh0KX0gJyR7Qyh0KX0nIGRpZWQgaW4gc3RhdHVzICR7dC5zdGF0dXN9OiBgLGk7aWYoZSBpbnN0YW5jZW9mIEVycm9yKXt0cnl7ZS5tZXNzYWdlPXIrZS5tZXNzYWdlfWNhdGNoe31pPWV9ZWxzZXtjb25zb2xlLndhcm4odSgzMCwhMSx0LnN0YXR1cyxDKHQpKSk7dHJ5e2k9RXJyb3IocitKU09OLnN0cmluZ2lmeShlKSl9Y2F0Y2h7aT1lfX1yZXR1cm4gaS5hcHBPclBhcmNlbE5hbWU9Qyh0KSx0LnN0YXR1cz1uLGl9dmFyIGY9YE5PVF9MT0FERURgLHA9YExPQURJTkdfU09VUkNFX0NPREVgLG09YE5PVF9CT09UU1RSQVBQRURgLGg9YEJPT1RTVFJBUFBJTkdgLGc9YE5PVF9NT1VOVEVEYCxfPWBNT1VOVElOR2Asdj1gTU9VTlRFRGAsZWU9YFVQREFUSU5HYCx5PWBVTk1PVU5USU5HYCxiPWBVTkxPQURJTkdgLHRlPWBMT0FEX0VSUk9SYCx4PWBTS0lQX0JFQ0FVU0VfQlJPS0VOYDtmdW5jdGlvbiBTKGUpe3JldHVybiBlLnN0YXR1cz09PXZ9ZnVuY3Rpb24gbmUoZSl7dHJ5e3JldHVybiBlLmFjdGl2ZVdoZW4od2luZG93LmxvY2F0aW9uKX1jYXRjaCh0KXtyZXR1cm4gcyh0LGUseCksITF9fWZ1bmN0aW9uIEMoZSl7cmV0dXJuIGUubmFtZX1mdW5jdGlvbiByZShlKXtyZXR1cm4hIWUudW5tb3VudFRoaXNQYXJjZWx9ZnVuY3Rpb24gdyhlKXtyZXR1cm4gcmUoZSk/YHBhcmNlbGA6YGFwcGxpY2F0aW9uYH1mdW5jdGlvbiBUKCl7Zm9yKGxldCBlPWFyZ3VtZW50cy5sZW5ndGgtMTtlPjA7ZS0tKWZvcihsZXQgdCBpbiBhcmd1bWVudHNbZV0pdCE9PWBfX3Byb3RvX19gJiYoYXJndW1lbnRzW2UtMV1bdF09YXJndW1lbnRzW2VdW3RdKTtyZXR1cm4gYXJndW1lbnRzWzBdfWZ1bmN0aW9uIEUoZSx0KXtmb3IobGV0IG49MDtuPGUubGVuZ3RoO24rKylpZih0KGVbbl0pKXJldHVybiBlW25dO3JldHVybiBudWxsfWZ1bmN0aW9uIEQoZSl7cmV0dXJuIGUmJih0eXBlb2YgZT09YGZ1bmN0aW9uYHx8KHQ9ZSxBcnJheS5pc0FycmF5KHQpJiYhRSh0LChlPT50eXBlb2YgZSE9YGZ1bmN0aW9uYCkpKSk7dmFyIHR9ZnVuY3Rpb24gTyhlLHQpe2xldCBuPWVbdF18fFtdO249QXJyYXkuaXNBcnJheShuKT9uOltuXSxuLmxlbmd0aD09PTAmJihuPVsoKT0+UHJvbWlzZS5yZXNvbHZlKCldKTtsZXQgcj13KGUpLGk9QyhlKTtyZXR1cm4gZnVuY3Rpb24oZSl7cmV0dXJuIG4ucmVkdWNlKCgobixhLG8pPT5uLnRoZW4oKCgpPT57bGV0IG49YShlKTtyZXR1cm4gaWUobik/bjpQcm9taXNlLnJlamVjdCh1KDE1LCExLHIsaSx0LG8pKX0pKSksUHJvbWlzZS5yZXNvbHZlKCkpfX1mdW5jdGlvbiBpZShlKXtyZXR1cm4gZSYmdHlwZW9mIGUudGhlbj09YGZ1bmN0aW9uYCYmdHlwZW9mIGUuY2F0Y2g9PWBmdW5jdGlvbmB9dmFyIGFlPVtdO2Z1bmN0aW9uIG9lKGUsdCl7cmV0dXJuIFByb21pc2UucmVzb2x2ZSgpLnRoZW4oKCgpPT5lLnN0YXR1cz09PWBOT1RfQk9PVFNUUkFQUEVEYD8oZS5zdGF0dXM9aCxlLmJvb3RzdHJhcD9NKGUsYGJvb3RzdHJhcGApLnRoZW4obikuY2F0Y2goKG49PntpZih0KXRocm93IGQobixlLHgpO3JldHVybiBzKG4sZSx4KSxlfSkpOlByb21pc2UucmVzb2x2ZSgpLnRoZW4obikpOmUpKTtmdW5jdGlvbiBuKCl7cmV0dXJuIGUuc3RhdHVzPWcsZX19ZnVuY3Rpb24gayhlLHQpe3JldHVybiBQcm9taXNlLnJlc29sdmUoKS50aGVuKCgoKT0+e2lmKGUuc3RhdHVzIT09YE1PVU5URURgKXJldHVybiBlO2Uuc3RhdHVzPXk7bGV0IG49T2JqZWN0LmtleXMoZS5wYXJjZWxzKS5tYXAoKHQ9PmUucGFyY2Vsc1t0XS51bm1vdW50VGhpc1BhcmNlbCgpKSk7cmV0dXJuIFByb21pc2UuYWxsKG4pLnRoZW4ociwobj0+cigpLnRoZW4oKCgpPT57bGV0IHI9RXJyb3Iobi5tZXNzYWdlKTtpZih0KXRocm93IGQocixlLHgpO3MocixlLHgpfSkpKSkudGhlbigoKCk9PmUpKTtmdW5jdGlvbiByKCl7cmV0dXJuIE0oZSxgdW5tb3VudGApLnRoZW4oKCgpPT57ZS5zdGF0dXM9Z30pLChuPT57aWYodCl0aHJvdyBkKG4sZSx4KTtzKG4sZSx4KX0pKX19KSl9dmFyIHNlPSExLGNlPSExO2Z1bmN0aW9uIGxlKGUsdCl7cmV0dXJuIFByb21pc2UucmVzb2x2ZSgpLnRoZW4oKCgpPT5lLnN0YXR1cz09PWBOT1RfTU9VTlRFRGA/KHNlfHw9KHdpbmRvdy5kaXNwYXRjaEV2ZW50KG5ldyBhKGBzaW5nbGUtc3BhOmJlZm9yZS1maXJzdC1tb3VudGApKSwhMCksZS5zdGF0dXM9XyxNKGUsYG1vdW50YCkudGhlbigoKCk9PihlLnN0YXR1cz12LGNlfHw9KHdpbmRvdy5kaXNwYXRjaEV2ZW50KG5ldyBhKGBzaW5nbGUtc3BhOmZpcnN0LW1vdW50YCkpLCEwKSxlKSkpLmNhdGNoKChuPT57cmV0dXJuIGUuc3RhdHVzPXYsayhlLCEwKS50aGVuKHIscik7ZnVuY3Rpb24gcigpe2lmKHQpdGhyb3cgZChuLGUseCk7cmV0dXJuIHMobixlLHgpLGV9fSkpKTplKSl9dmFyIHVlPTAsZGU9e3BhcmNlbHM6e319O2Z1bmN0aW9uIGZlKCl7cmV0dXJuIHBlLmFwcGx5KGRlLGFyZ3VtZW50cyl9ZnVuY3Rpb24gcGUoZSx0KXtsZXQgbj10aGlzO2lmKCFlfHx0eXBlb2YgZSE9YG9iamVjdGAmJnR5cGVvZiBlIT1gZnVuY3Rpb25gKXRocm93IEVycm9yKHUoMiwhMSkpO2lmKGUubmFtZSYmdHlwZW9mIGUubmFtZSE9YHN0cmluZ2ApdGhyb3cgRXJyb3IodSgzLCExLHR5cGVvZiBlLm5hbWUpKTtsZXQgcj11ZSsrLGk9ZS5uYW1lfHxgcGFyY2VsLSR7cn1gO2lmKHR5cGVvZiB0IT1gb2JqZWN0YCl0aHJvdyBFcnJvcih1KDQsITEsaSx0eXBlb2YgdCkpO2lmKCF0LmRvbUVsZW1lbnQpdGhyb3cgRXJyb3IodSg1LCExLGkpKTtsZXQgYT10eXBlb2YgZT09YGZ1bmN0aW9uYCxvPWE/ZTooKT0+UHJvbWlzZS5yZXNvbHZlKGUpLHM9e2lkOnIscGFyY2Vsczp7fSxzdGF0dXM6YT9wOm0sY3VzdG9tUHJvcHM6dCxwYXJlbnROYW1lOkMobiksdW5tb3VudFRoaXNQYXJjZWw6KCk9PmgudGhlbigoKCk9PntpZihzLnN0YXR1cyE9PWBNT1VOVEVEYCl0aHJvdyBFcnJvcih1KDYsITEsaSxzLnN0YXR1cykpO3JldHVybiBrKHMsITApfSkpLnRoZW4oKGU9PihzLnBhcmVudE5hbWUmJmRlbGV0ZSBuLnBhcmNlbHNbcy5pZF0sZSkpKS50aGVuKChlPT4oZyhlKSxlKSkpLmNhdGNoKChlPT57dGhyb3cgcy5zdGF0dXM9eCxfKGUpLGV9KSl9LGM7bi5wYXJjZWxzW3JdPXM7bGV0IGw9bygpO2lmKCFsfHx0eXBlb2YgbC50aGVuIT1gZnVuY3Rpb25gKXRocm93IEVycm9yKHUoNywhMSkpO2w9bC50aGVuKChlPT57aWYoIWUpdGhyb3cgRXJyb3IodSg4LCExKSk7aWYoaT1lLm5hbWV8fGBwYXJjZWwtJHtyfWAsT2JqZWN0LnByb3RvdHlwZS5oYXNPd25Qcm9wZXJ0eS5jYWxsKGUsYGJvb3RzdHJhcGApJiYhRChlLmJvb3RzdHJhcCkpdGhyb3cgRXJyb3IodSg5LCExLGkpKTtpZighRChlLm1vdW50KSl0aHJvdyBFcnJvcih1KDEwLCExLGkpKTtpZighRChlLnVubW91bnQpKXRocm93IEVycm9yKHUoMTEsITEsaSkpO2lmKGUudXBkYXRlJiYhRChlLnVwZGF0ZSkpdGhyb3cgRXJyb3IodSgxMiwhMSxpKSk7bGV0IHQ9TyhlLGBib290c3RyYXBgKSxuPU8oZSxgbW91bnRgKSxhPU8oZSxgdW5tb3VudGApO3Muc3RhdHVzPW0scy5uYW1lPWkscy5ib290c3RyYXA9dCxzLm1vdW50PW4scy51bm1vdW50PWEscy50aW1lb3V0cz15ZShlLnRpbWVvdXRzKSxlLnVwZGF0ZSYmKHMudXBkYXRlPU8oZSxgdXBkYXRlYCksYy51cGRhdGU9ZnVuY3Rpb24oZSl7cmV0dXJuIHMuY3VzdG9tUHJvcHM9ZSxBKCh0PXMsUHJvbWlzZS5yZXNvbHZlKCkudGhlbigoKCk9PntpZih0LnN0YXR1cyE9PWBNT1VOVEVEYCl0aHJvdyBFcnJvcih1KDMyLCExLEModCkpKTtyZXR1cm4gdC5zdGF0dXM9YFVQREFUSU5HYCxNKHQsYHVwZGF0ZWApLnRoZW4oKCgpPT4odC5zdGF0dXM9YE1PVU5URURgLHQpKSkuY2F0Y2goKGU9Pnt0aHJvdyBkKGUsdCxgU0tJUF9CRUNBVVNFX0JST0tFTmApfSkpfSkpKSk7dmFyIHR9KX0pKTtsZXQgZj1sLnRoZW4oKCgpPT5vZShzLCEwKSkpLGg9Zi50aGVuKCgoKT0+bGUocywhMCkpKSxnLF8sdj1uZXcgUHJvbWlzZSgoKGUsdCk9PntnPWUsXz10fSkpO3JldHVybiBjPXttb3VudDooKT0+QShQcm9taXNlLnJlc29sdmUoKS50aGVuKCgoKT0+e2lmKHMuc3RhdHVzIT09YE5PVF9NT1VOVEVEYCl0aHJvdyBFcnJvcih1KDEzLCExLGkscy5zdGF0dXMpKTtyZXR1cm4gbi5wYXJjZWxzW3JdPXMsbGUocyl9KSkpLHVubW91bnQ6KCk9PkEocy51bm1vdW50VGhpc1BhcmNlbCgpKSxnZXRTdGF0dXM6KCk9PnMuc3RhdHVzLGxvYWRQcm9taXNlOkEobCksYm9vdHN0cmFwUHJvbWlzZTpBKGYpLG1vdW50UHJvbWlzZTpBKGgpLHVubW91bnRQcm9taXNlOkEodil9LGN9ZnVuY3Rpb24gQShlKXtyZXR1cm4gZS50aGVuKCgoKT0+bnVsbCkpfWZ1bmN0aW9uIG1lKGUpe2xldCB0PUMoZSksbj10eXBlb2YgZS5jdXN0b21Qcm9wcz09YGZ1bmN0aW9uYD9lLmN1c3RvbVByb3BzKHQsd2luZG93LmxvY2F0aW9uKTplLmN1c3RvbVByb3BzOyh0eXBlb2YgbiE9YG9iamVjdGB8fCFufHxBcnJheS5pc0FycmF5KG4pKSYmKG49e30sY29uc29sZS53YXJuKHUoNDAsITEpLHQsbikpO2xldCBpPVQoe30sbix7bmFtZTp0LG1vdW50UGFyY2VsOnBlLmJpbmQoZSksc2luZ2xlU3BhOnJ9KTtyZXR1cm4gcmUoZSkmJihpLnVubW91bnRTZWxmPWUudW5tb3VudFRoaXNQYXJjZWwpLGl9dmFyIGo9e2Jvb3RzdHJhcDp7bWlsbGlzOjRlMyxkaWVPblRpbWVvdXQ6ITEsd2FybmluZ01pbGxpczoxZTN9LG1vdW50OnttaWxsaXM6M2UzLGRpZU9uVGltZW91dDohMSx3YXJuaW5nTWlsbGlzOjFlM30sdW5tb3VudDp7bWlsbGlzOjNlMyxkaWVPblRpbWVvdXQ6ITEsd2FybmluZ01pbGxpczoxZTN9LHVubG9hZDp7bWlsbGlzOjNlMyxkaWVPblRpbWVvdXQ6ITEsd2FybmluZ01pbGxpczoxZTN9LHVwZGF0ZTp7bWlsbGlzOjNlMyxkaWVPblRpbWVvdXQ6ITEsd2FybmluZ01pbGxpczoxZTN9fTtmdW5jdGlvbiBoZShlLHQsbil7aWYodHlwZW9mIGUhPWBudW1iZXJgfHxlPD0wKXRocm93IEVycm9yKHUoMTYsITEpKTtqLmJvb3RzdHJhcD17bWlsbGlzOmUsZGllT25UaW1lb3V0OnQsd2FybmluZ01pbGxpczpufHwxZTN9fWZ1bmN0aW9uIGdlKGUsdCxuKXtpZih0eXBlb2YgZSE9YG51bWJlcmB8fGU8PTApdGhyb3cgRXJyb3IodSgxNywhMSkpO2oubW91bnQ9e21pbGxpczplLGRpZU9uVGltZW91dDp0LHdhcm5pbmdNaWxsaXM6bnx8MWUzfX1mdW5jdGlvbiBfZShlLHQsbil7aWYodHlwZW9mIGUhPWBudW1iZXJgfHxlPD0wKXRocm93IEVycm9yKHUoMTgsITEpKTtqLnVubW91bnQ9e21pbGxpczplLGRpZU9uVGltZW91dDp0LHdhcm5pbmdNaWxsaXM6bnx8MWUzfX1mdW5jdGlvbiB2ZShlLHQsbil7aWYodHlwZW9mIGUhPWBudW1iZXJgfHxlPD0wKXRocm93IEVycm9yKHUoMTksITEpKTtqLnVubG9hZD17bWlsbGlzOmUsZGllT25UaW1lb3V0OnQsd2FybmluZ01pbGxpczpufHwxZTN9fWZ1bmN0aW9uIE0oZSx0KXtsZXQgbj1lLnRpbWVvdXRzW3RdLHI9bi53YXJuaW5nTWlsbGlzLGk9dyhlKTtyZXR1cm4gbmV3IFByb21pc2UoKChhLG8pPT57bGV0IHM9ITEsYz0hMTtlW3RdKG1lKGUpKS50aGVuKChlPT57cz0hMCxhKGUpfSkpLmNhdGNoKChlPT57cz0hMCxvKGUpfSkpLHNldFRpbWVvdXQoKCgpPT5kKDEpKSxyKSxzZXRUaW1lb3V0KCgoKT0+ZCghMCkpLG4ubWlsbGlzKTtsZXQgbD11KDMxLCExLHQsaSxDKGUpLG4ubWlsbGlzKTtmdW5jdGlvbiBkKGUpe2lmKCFzKXtpZighMD09PWUpYz0hMCxuLmRpZU9uVGltZW91dD9vKEVycm9yKGwpKTpjb25zb2xlLmVycm9yKGwpO2Vsc2UgaWYoIWMpe2xldCB0PWUsaT10KnI7Y29uc29sZS53YXJuKGwpLGkrcjxuLm1pbGxpcyYmc2V0VGltZW91dCgoKCk9PmQodCsxKSkscil9fX19KSl9ZnVuY3Rpb24geWUoZSl7bGV0IHQ9e307Zm9yKGxldCBuIGluIGopdFtuXT1UKHt9LGpbbl0sZSYmZVtuXXx8e30pO3JldHVybiB0fWZ1bmN0aW9uIGJlKGUpe3JldHVybiBQcm9taXNlLnJlc29sdmUoKS50aGVuKCgoKT0+e2lmKGUubG9hZFByb21pc2UpcmV0dXJuIGUubG9hZFByb21pc2U7aWYoZS5zdGF0dXMhPT1gTk9UX0xPQURFRGAmJmUuc3RhdHVzIT09YExPQURfRVJST1JgKXJldHVybiBlO2xldCB0LG47cmV0dXJuIGUuc3RhdHVzPXAsZS5sb2FkUHJvbWlzZT1Qcm9taXNlLnJlc29sdmUoKS50aGVuKCgoKT0+e2xldCByPWUubG9hZEFwcChtZShlKSk7aWYoIWllKHIpKXRocm93IG49ITAsRXJyb3IodSgzMywhMSxDKGUpKSk7cmV0dXJuIHIudGhlbigobj0+e2xldCByO2UubG9hZEVycm9yVGltZT1udWxsLHQ9bix0eXBlb2YgdCE9YG9iamVjdGAmJihyPTM0KSxPYmplY3QucHJvdG90eXBlLmhhc093blByb3BlcnR5LmNhbGwodCxgYm9vdHN0cmFwYCkmJiFEKHQuYm9vdHN0cmFwKSYmKHI9MzUpLEQodC5tb3VudCl8fChyPTM2KSxEKHQudW5tb3VudCl8fChyPTM3KTtsZXQgaT13KHQpO2lmKHIpe2xldCBuO3RyeXtuPUpTT04uc3RyaW5naWZ5KHQpfWNhdGNoe31yZXR1cm4gY29uc29sZS5lcnJvcih1KHIsITEsaSxDKGUpLG4pLHQpLHModm9pZCAwLGUseCksZX1yZXR1cm4gdC5kZXZ0b29scyYmdC5kZXZ0b29scy5vdmVybGF5cyYmKGUuZGV2dG9vbHMub3ZlcmxheXM9VCh7fSxlLmRldnRvb2xzLm92ZXJsYXlzLHQuZGV2dG9vbHMub3ZlcmxheXMpKSxlLnN0YXR1cz1tLGUuYm9vdHN0cmFwPU8odCxgYm9vdHN0cmFwYCksZS5tb3VudD1PKHQsYG1vdW50YCksZS51bm1vdW50PU8odCxgdW5tb3VudGApLGUudW5sb2FkPU8odCxgdW5sb2FkYCksZS50aW1lb3V0cz15ZSh0LnRpbWVvdXRzKSxkZWxldGUgZS5sb2FkUHJvbWlzZSxlfSkpfSkpLmNhdGNoKCh0PT57bGV0IHI7cmV0dXJuIGRlbGV0ZSBlLmxvYWRQcm9taXNlLG4/cj14OihyPXRlLGUubG9hZEVycm9yVGltZT1uZXcgRGF0ZSgpLmdldFRpbWUoKSkscyh0LGUsciksZX0pKX0pKX12YXIgTj10eXBlb2Ygd2luZG93PGB1YCxQPXtoYXNoY2hhbmdlOltdLHBvcHN0YXRlOltdfSxGPVtgaGFzaGNoYW5nZWAsYHBvcHN0YXRlYF07ZnVuY3Rpb24geGUoZSl7bGV0IHQ7aWYodHlwZW9mIGU9PWBzdHJpbmdgKXQ9ZTtlbHNlIGlmKHRoaXMmJnRoaXMuaHJlZil0PXRoaXMuaHJlZjtlbHNle2lmKCEoZSYmZS5jdXJyZW50VGFyZ2V0JiZlLmN1cnJlbnRUYXJnZXQuaHJlZiYmZS5wcmV2ZW50RGVmYXVsdCkpdGhyb3cgRXJyb3IodSgxNCwhMSkpO3Q9ZS5jdXJyZW50VGFyZ2V0LmhyZWYsZS5wcmV2ZW50RGVmYXVsdCgpfWxldCBuPUFlKHdpbmRvdy5sb2NhdGlvbi5ocmVmKSxyPUFlKHQpO3QuaW5kZXhPZihgI2ApPT09MD93aW5kb3cubG9jYXRpb24uaGFzaD1yLmhhc2g6bi5ob3N0IT09ci5ob3N0JiZyLmhvc3Q/d2luZG93LmxvY2F0aW9uLmhyZWY9dDpyLnBhdGhuYW1lPT09bi5wYXRobmFtZSYmci5zZWFyY2g9PT1uLnNlYXJjaD93aW5kb3cubG9jYXRpb24uaGFzaD1yLmhhc2g6d2luZG93Lmhpc3RvcnkucHVzaFN0YXRlKG51bGwsbnVsbCx0KX1mdW5jdGlvbiBTZShlKXtpZihlKXtsZXQgdD1lWzBdLnR5cGU7Ri5pbmRleE9mKHQpPj0wJiZQW3RdLmZvckVhY2goKHQ9Pnt0cnl7dC5hcHBseSh0aGlzLGUpfWNhdGNoKGUpe3NldFRpbWVvdXQoKCgpPT57dGhyb3cgZX0pKX19KSl9fXZhciBDZTtmdW5jdGlvbiB3ZSgpe1YoW10sYXJndW1lbnRzKX1mdW5jdGlvbiBUZShlLHQpe3JldHVybiBmdW5jdGlvbigpe2xldCBuPXdpbmRvdy5sb2NhdGlvbi5ocmVmLHI9ZS5hcHBseSh0aGlzLGFyZ3VtZW50cyksaT13aW5kb3cubG9jYXRpb24uaHJlZjtyZXR1cm4gQ2UmJm49PT1pfHx3aW5kb3cuZGlzcGF0Y2hFdmVudChFZSh3aW5kb3cuaGlzdG9yeS5zdGF0ZSx0KSkscn19ZnVuY3Rpb24gRWUoZSx0KXtsZXQgbjt0cnl7bj1uZXcgUG9wU3RhdGVFdmVudChgcG9wc3RhdGVgLHtzdGF0ZTplfSl9Y2F0Y2h7bj1kb2N1bWVudC5jcmVhdGVFdmVudChgUG9wU3RhdGVFdmVudGApLG4uaW5pdFBvcFN0YXRlRXZlbnQoYHBvcHN0YXRlYCwhMSwhMSxlKX1yZXR1cm4gbi5zaW5nbGVTcGE9ITAsbi5zaW5nbGVTcGFUcmlnZ2VyPXQsbn12YXIgRGU9bnVsbCxPZT0hMTtmdW5jdGlvbiBrZShlKXtpZihPZSl0aHJvdyBFcnJvcih1KDQzLCExKSk7Q2U9IWV8fCFlLmhhc093blByb3BlcnR5KGB1cmxSZXJvdXRlT25seWApfHxlLnVybFJlcm91dGVPbmx5LE9lPSEwLERlPXdpbmRvdy5oaXN0b3J5LnJlcGxhY2VTdGF0ZSx3aW5kb3cuYWRkRXZlbnRMaXN0ZW5lcihgaGFzaGNoYW5nZWAsd2UpLHdpbmRvdy5hZGRFdmVudExpc3RlbmVyKGBwb3BzdGF0ZWAsd2UpO2xldCB0PXdpbmRvdy5hZGRFdmVudExpc3RlbmVyLG49d2luZG93LnJlbW92ZUV2ZW50TGlzdGVuZXI7d2luZG93LmFkZEV2ZW50TGlzdGVuZXI9ZnVuY3Rpb24oZSxuKXtpZighKHR5cGVvZiBuPT1gZnVuY3Rpb25gJiZGLmluZGV4T2YoZSk+PTApfHxFKFBbZV0sKGU9PmU9PT1uKSkpcmV0dXJuIHQuYXBwbHkodGhpcyxhcmd1bWVudHMpO1BbZV0ucHVzaChuKX0sd2luZG93LnJlbW92ZUV2ZW50TGlzdGVuZXI9ZnVuY3Rpb24oZSx0KXtyZXR1cm4gdHlwZW9mIHQ9PWBmdW5jdGlvbmAmJkYuaW5kZXhPZihlKT49MCYmKFBbZV09UFtlXS5maWx0ZXIoKGU9PmUhPT10KSkpLG4uYXBwbHkodGhpcyxhcmd1bWVudHMpfSx3aW5kb3cuaGlzdG9yeS5wdXNoU3RhdGU9VGUod2luZG93Lmhpc3RvcnkucHVzaFN0YXRlLGBwdXNoU3RhdGVgKSx3aW5kb3cuaGlzdG9yeS5yZXBsYWNlU3RhdGU9VGUoRGUsYHJlcGxhY2VTdGF0ZWApfWZ1bmN0aW9uIEFlKGUpe2xldCB0PWRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoYGFgKTtyZXR1cm4gdC5ocmVmPWUsdH1OJiYod2luZG93LnNpbmdsZVNwYU5hdmlnYXRlP2NvbnNvbGUud2Fybih1KDQxLCExKSk6d2luZG93LnNpbmdsZVNwYU5hdmlnYXRlPXhlKTt2YXIgamU9ITE7ZnVuY3Rpb24gTWUoZT13aW5kb3cualF1ZXJ5KXtpZihlfHx3aW5kb3cuJCYmd2luZG93LiQuZm4mJndpbmRvdy4kLmZuLmpxdWVyeSYmKGU9d2luZG93LiQpLGUmJiFqZSl7bGV0IHQ9ZS5mbi5vbixuPWUuZm4ub2ZmO2UuZm4ub249ZnVuY3Rpb24oZSxuKXtyZXR1cm4gTmUuY2FsbCh0aGlzLHQsd2luZG93LmFkZEV2ZW50TGlzdGVuZXIsZSxuLGFyZ3VtZW50cyl9LGUuZm4ub2ZmPWZ1bmN0aW9uKGUsdCl7cmV0dXJuIE5lLmNhbGwodGhpcyxuLHdpbmRvdy5yZW1vdmVFdmVudExpc3RlbmVyLGUsdCxhcmd1bWVudHMpfSxqZT0hMH19ZnVuY3Rpb24gTmUoZSx0LG4scixpKXtyZXR1cm4gdHlwZW9mIG49PWBzdHJpbmdgPyhuLnNwbGl0KC9ccysvKS5mb3JFYWNoKChlPT57Ri5pbmRleE9mKGUpPj0wJiYodChlLHIpLG49bi5yZXBsYWNlKGUsYGApKX0pKSxuLnRyaW0oKT09PWBgP3RoaXM6ZS5hcHBseSh0aGlzLGkpKTplLmFwcGx5KHRoaXMsaSl9dmFyIEk9e307ZnVuY3Rpb24gUGUoZSl7cmV0dXJuIFByb21pc2UucmVzb2x2ZSgpLnRoZW4oKCgpPT57bGV0IHQ9SVtDKGUpXTtpZighdClyZXR1cm4gZTtpZihlLnN0YXR1cz09PWBOT1RfTE9BREVEYClyZXR1cm4gRmUoZSx0KSxlO2lmKGUuc3RhdHVzPT09YFVOTE9BRElOR2ApcmV0dXJuIHQucHJvbWlzZS50aGVuKCgoKT0+ZSkpO2lmKGUuc3RhdHVzIT09YE5PVF9NT1VOVEVEYCYmZS5zdGF0dXMhPT1gTE9BRF9FUlJPUmApcmV0dXJuIGU7bGV0IG49ZS5zdGF0dXM9PT1gTE9BRF9FUlJPUmA/UHJvbWlzZS5yZXNvbHZlKCk6TShlLGB1bmxvYWRgKTtyZXR1cm4gZS5zdGF0dXM9YixuLnRoZW4oKCgpPT4oRmUoZSx0KSxlKSkpLmNhdGNoKChuPT4oZnVuY3Rpb24oZSx0LG4pe2RlbGV0ZSBJW0MoZSldLGRlbGV0ZSBlLmJvb3RzdHJhcCxkZWxldGUgZS5tb3VudCxkZWxldGUgZS51bm1vdW50LGRlbGV0ZSBlLnVubG9hZCxzKG4sZSx4KSx0LnJlamVjdChuKX0oZSx0LG4pLGUpKSl9KSl9ZnVuY3Rpb24gRmUoZSx0KXtkZWxldGUgSVtDKGUpXSxkZWxldGUgZS5ib290c3RyYXAsZGVsZXRlIGUubW91bnQsZGVsZXRlIGUudW5tb3VudCxkZWxldGUgZS51bmxvYWQsZS5zdGF0dXM9Zix0LnJlc29sdmUoKX1mdW5jdGlvbiBJZShlLHQsbixyKXtJW0MoZSldPXthcHA6ZSxyZXNvbHZlOm4scmVqZWN0OnJ9LE9iamVjdC5kZWZpbmVQcm9wZXJ0eShJW0MoZSldLGBwcm9taXNlYCx7Z2V0OnR9KX1mdW5jdGlvbiBMZShlKXtyZXR1cm4gSVtlXX12YXIgTD1bXTtmdW5jdGlvbiBSZSgpe3JldHVybiBMLmZpbHRlcihTKS5tYXAoQyl9ZnVuY3Rpb24gUigpe3JldHVybiBMLm1hcChDKX1mdW5jdGlvbiB6ZShlKXtsZXQgdD1FKEwsKHQ9PkModCk9PT1lKSk7cmV0dXJuIHQ/dC5zdGF0dXM6bnVsbH12YXIgQmU9ITE7ZnVuY3Rpb24gVmUoZSx0LG4scil7bGV0IGk9ZnVuY3Rpb24oZSx0LG4scil7bGV0IGk9e25hbWU6bnVsbCxsb2FkQXBwOm51bGwsYWN0aXZlV2hlbjpudWxsLGN1c3RvbVByb3BzOm51bGx9O3JldHVybiB0eXBlb2YgZT09YG9iamVjdGA/KGZ1bmN0aW9uKGUpe2lmKEFycmF5LmlzQXJyYXkoZSl8fGU9PT1udWxsKXRocm93IEVycm9yKHUoMzksITEpKTtsZXQgdD1bYG5hbWVgLGBhcHBgLGBhY3RpdmVXaGVuYCxgY3VzdG9tUHJvcHNgXSxuPU9iamVjdC5rZXlzKGUpLnJlZHVjZSgoKGUsbik9PnQuaW5kZXhPZihuKT49MD9lOmUuY29uY2F0KG4pKSxbXSk7aWYobi5sZW5ndGghPT0wKXRocm93IEVycm9yKHUoMzgsITEsdC5qb2luKGAsIGApLG4uam9pbihgLCBgKSkpO2lmKHR5cGVvZiBlLm5hbWUhPWBzdHJpbmdgfHxlLm5hbWUubGVuZ3RoPT09MHx8dHlwZW9mIGUuYXBwIT1gb2JqZWN0YCYmdHlwZW9mIGUuYXBwIT1gZnVuY3Rpb25gKXRocm93IEVycm9yKHUoMjAsITEpKTtsZXQgcj1lPT50eXBlb2YgZT09YHN0cmluZ2B8fHR5cGVvZiBlPT1gZnVuY3Rpb25gO2lmKCEocihlLmFjdGl2ZVdoZW4pfHxBcnJheS5pc0FycmF5KGUuYWN0aXZlV2hlbikmJmUuYWN0aXZlV2hlbi5ldmVyeShyKSkpdGhyb3cgRXJyb3IodSgyNCwhMSkpO2lmKCFLZShlLmN1c3RvbVByb3BzKSl0aHJvdyBFcnJvcih1KDIyLCExKSl9KGUpLGkubmFtZT1lLm5hbWUsaS5sb2FkQXBwPWUuYXBwLGkuYWN0aXZlV2hlbj1lLmFjdGl2ZVdoZW4saS5jdXN0b21Qcm9wcz1lLmN1c3RvbVByb3BzKTooZnVuY3Rpb24oZSx0LG4scil7aWYodHlwZW9mIGUhPWBzdHJpbmdgfHxlLmxlbmd0aD09PTApdGhyb3cgRXJyb3IodSgyMCwhMSkpO2lmKCF0KXRocm93IEVycm9yKHUoMjMsITEpKTtpZih0eXBlb2YgbiE9YGZ1bmN0aW9uYCl0aHJvdyBFcnJvcih1KDI0LCExKSk7aWYoIUtlKHIpKXRocm93IEVycm9yKHUoMjIsITEpKX0oZSx0LG4sciksaS5uYW1lPWUsaS5sb2FkQXBwPXQsaS5hY3RpdmVXaGVuPW4saS5jdXN0b21Qcm9wcz1yKSxpLmxvYWRBcHA9dHlwZW9mKGE9aS5sb2FkQXBwKT09YGZ1bmN0aW9uYD9hOigpPT5Qcm9taXNlLnJlc29sdmUoYSksaS5jdXN0b21Qcm9wcz1mdW5jdGlvbihlKXtyZXR1cm4gZXx8e319KGkuY3VzdG9tUHJvcHMpLGkuYWN0aXZlV2hlbj1mdW5jdGlvbihlKXtsZXQgdD1BcnJheS5pc0FycmF5KGUpP2U6W2VdO3JldHVybiB0PXQubWFwKChlPT50eXBlb2YgZT09YGZ1bmN0aW9uYD9lOnFlKGUpKSksZT0+dC5zb21lKCh0PT50KGUpKSl9KGkuYWN0aXZlV2hlbiksaTt2YXIgYX0oZSx0LG4scik7aWYoJGUoKXx8QmV8fChCZT0hMCxzZXRUaW1lb3V0KCgoKT0+eyRlKCl8fGNvbnNvbGUud2Fybih1KDEsITEpKX0pLDVlMykpLFIoKS5pbmRleE9mKGkubmFtZSkhPT0tMSl0aHJvdyBFcnJvcih1KDIxLCExLGkubmFtZSkpO0wucHVzaChUKHtsb2FkRXJyb3JUaW1lOm51bGwsc3RhdHVzOmYscGFyY2Vsczp7fSxkZXZ0b29sczp7b3ZlcmxheXM6e29wdGlvbnM6e30sc2VsZWN0b3JzOltdfX19LGkpKSxOJiYoTWUoKSxWKCkpfWZ1bmN0aW9uIEhlKGU9d2luZG93LmxvY2F0aW9uKXtyZXR1cm4gTC5maWx0ZXIoKHQ9PnQuYWN0aXZlV2hlbihlKSkpLm1hcChDKX1mdW5jdGlvbiBVZShlKXtpZihMLmZpbHRlcigodD0+Qyh0KT09PWUpKS5sZW5ndGg9PT0wKXRocm93IEVycm9yKHUoMjUsITEsZSkpO3JldHVybihOP1dlKGUse3dhaXRGb3JVbm1vdW50OiExfSk6UHJvbWlzZS5yZXNvbHZlKCkpLnRoZW4oKCgpPT57bGV0IHQ9TC5tYXAoQykuaW5kZXhPZihlKTtMLnNwbGljZSh0LDEpfSkpfWZ1bmN0aW9uIFdlKGUsdD17d2FpdEZvclVubW91bnQ6ITF9KXtpZih0eXBlb2YgZSE9YHN0cmluZ2ApdGhyb3cgRXJyb3IodSgyNiwhMSkpO2xldCBuPUUoTCwodD0+Qyh0KT09PWUpKTtpZighbil0aHJvdyBFcnJvcih1KDI3LCExLGUpKTtsZXQgcj1MZShDKG4pKTtpZih0JiZ0LndhaXRGb3JVbm1vdW50KXtpZihyKXJldHVybiByLnByb21pc2U7e2xldCBlPW5ldyBQcm9taXNlKCgodCxyKT0+e0llKG4sKCgpPT5lKSx0LHIpfSkpO3JldHVybiBlfX17bGV0IGU7cmV0dXJuIHI/KGU9ci5wcm9taXNlLEdlKG4sci5yZXNvbHZlLHIucmVqZWN0KSk6ZT1uZXcgUHJvbWlzZSgoKHQscik9PntJZShuLCgoKT0+ZSksdCxyKSxHZShuLHQscil9KSksZX19ZnVuY3Rpb24gR2UoZSx0LG4pe1Byb21pc2UucmVzb2x2ZSgpLnRoZW4oKCgpPT57aWYoRShIZSgpLCh0PT50PT09QyhlKSkpKXJldHVybiBZZSgpfSkpLnRoZW4oKCgpPT5rKGUpLnRoZW4oUGUpLnRoZW4oKCgpPT57dCgpLHNldFRpbWVvdXQoKCgpPT57VigpfSkpfSkpKSkuY2F0Y2gobil9ZnVuY3Rpb24gS2UoZSl7cmV0dXJuIWV8fHR5cGVvZiBlPT1gZnVuY3Rpb25gfHx0eXBlb2YgZT09YG9iamVjdGAmJiEhZSYmIUFycmF5LmlzQXJyYXkoZSl9ZnVuY3Rpb24gcWUoZSx0KXtsZXQgbj1mdW5jdGlvbihlLHQpe2xldCBuPTAscj0hMSxpPWBeYDtlWzBdIT09YC9gJiYoZT1gL2ArZSk7Zm9yKGxldCB0PTA7dDxlLmxlbmd0aDt0Kyspe2xldCBuPWVbdF07KCFyJiZuPT09YDpgfHxyJiZuPT09YC9gKSYmYSh0KX1yZXR1cm4gYShlLmxlbmd0aCksbmV3IFJlZ0V4cChpLGBpYCk7ZnVuY3Rpb24gYShhKXtsZXQgbz1lLnNsaWNlKG4sYSkucmVwbGFjZSgvW3xcXHt9KClbXF1eJCsqPy5dL2csYFxcJCZgKTtpZihpKz1yP2BbXi9dKy8/YDpvLGE9PT1lLmxlbmd0aClpZihyKXQmJihpKz1gJGApO2Vsc2V7bGV0IGU9dD9gYDpgLipgO2k9aS5jaGFyQXQoaS5sZW5ndGgtMSk9PT1gL2A/YCR7aX0ke2V9JGA6YCR7aX0oLyR7ZX0pPygjLiopPyRgfXI9IXIsbj1hfX0oZSx0KTtyZXR1cm4gZT0+e2xldCB0PWUub3JpZ2luO3R8fD1gJHtlLnByb3RvY29sfS8vJHtlLmhvc3R9YDtsZXQgcj1lLmhyZWYucmVwbGFjZSh0LGBgKS5yZXBsYWNlKGUuc2VhcmNoLGBgKS5zcGxpdChgP2ApWzBdO3JldHVybiBuLnRlc3Qocil9fXZhciB6PSExLEI9W10sSmU9TiYmd2luZG93LmxvY2F0aW9uLmhyZWY7ZnVuY3Rpb24gWWUoKXtyZXR1cm4gVigpfWZ1bmN0aW9uIFYoZT1bXSx0LG49ITEpe2lmKHopcmV0dXJuIG5ldyBQcm9taXNlKCgoZSxuKT0+e0IucHVzaCh7cmVzb2x2ZTplLHJlamVjdDpuLGV2ZW50QXJndW1lbnRzOnR9KX0pKTtsZXR7YXBwc1RvVW5sb2FkOnIsYXBwc1RvVW5tb3VudDppLGFwcHNUb0xvYWQ6byxhcHBzVG9Nb3VudDpzfT1mdW5jdGlvbigpe2xldCBlPVtdLHQ9W10sbj1bXSxyPVtdLGk9bmV3IERhdGUoKS5nZXRUaW1lKCk7cmV0dXJuIEwuZm9yRWFjaCgoYT0+e2xldCBvPWEuc3RhdHVzIT09YFNLSVBfQkVDQVVTRV9CUk9LRU5gJiZuZShhKTtzd2l0Y2goYS5zdGF0dXMpe2Nhc2UgdGU6byYmaS1hLmxvYWRFcnJvclRpbWU+PTIwMCYmbi5wdXNoKGEpO2JyZWFrO2Nhc2UgZjpjYXNlIHA6byYmbi5wdXNoKGEpO2JyZWFrO2Nhc2UgbTpjYXNlIGc6IW8mJkxlKEMoYSkpP2UucHVzaChhKTpvJiZyLnB1c2goYSk7YnJlYWs7Y2FzZSB2Om98fHQucHVzaChhKX19KSkse2FwcHNUb1VubG9hZDplLGFwcHNUb1VubW91bnQ6dCxhcHBzVG9Mb2FkOm4sYXBwc1RvTW91bnQ6cn19KCksYyxsPVtdLGQ9SmUsaD1KZT13aW5kb3cubG9jYXRpb24uaHJlZjtyZXR1cm4gJGUoKT8oej0hMCxjPXIuY29uY2F0KG8saSxzKSxQcm9taXNlLnJlc29sdmUoKS50aGVuKCgoKT0+KFMoYy5sZW5ndGg9PT0wP2BiZWZvcmUtbm8tYXBwLWNoYW5nZWA6YGJlZm9yZS1hcHAtY2hhbmdlYCxiKCEwKSksUyhgYmVmb3JlLXJvdXRpbmctZXZlbnRgLGIoITAse2NhbmNlbE5hdmlnYXRpb246X30pKSxQcm9taXNlLmFsbChsKS50aGVuKChuPT57aWYobi5zb21lKChlPT5lKSkpcmV0dXJuIERlLmNhbGwod2luZG93Lmhpc3RvcnksaGlzdG9yeS5zdGF0ZSxgYCxkLnN1YnN0cmluZyhsb2NhdGlvbi5vcmlnaW4ubGVuZ3RoKSksSmU9bG9jYXRpb24uaHJlZix6PSExLFYoZSx0LCEwKTtsZXQgYT1yLm1hcChQZSksYz1pLm1hcChrKS5tYXAoKGU9PmUudGhlbihQZSkpKS5jb25jYXQoYSksbD1Qcm9taXNlLmFsbChjKTtsLnRoZW4oKCgpPT57UyhgYmVmb3JlLW1vdW50LXJvdXRpbmctZXZlbnRgLGIoITApKX0pLChlPT57dGhyb3cgZX0pKTtsZXQgdT1vLm1hcCgoZT0+YmUoZSkudGhlbigoZT0+WGUoZSxsKSkpKSksZj1zLmZpbHRlcigoZT0+by5pbmRleE9mKGUpPDApKS5tYXAoKGU9PlhlKGUsbCkpKTtyZXR1cm4gbC5jYXRjaCgoZT0+e3Rocm93IHkoKSxlfSkpLnRoZW4oKCgpPT4oeSgpLFByb21pc2UuYWxsKHUuY29uY2F0KGYpKS5jYXRjaCgodD0+e3Rocm93IGUuZm9yRWFjaCgoZT0+ZS5yZWplY3QodCkpKSx0fSkpLnRoZW4oZWUpLnRoZW4oKCgpPT57fSksKGU9Pnt0aHJvdyBlfSkpKSkpfSkpKSkpKTooYz1vLFByb21pc2UucmVzb2x2ZSgpLnRoZW4oKCgpPT57bGV0IGU9by5tYXAoYmUpO3JldHVybiBQcm9taXNlLmFsbChlKS50aGVuKHkpLnRoZW4oKCgpPT5bXSkpLmNhdGNoKChlPT57dGhyb3cgeSgpLGV9KSkuZmluYWxseSgoKCk9Pnt9KSl9KSkpO2Z1bmN0aW9uIF8oZT0hMCl7bGV0IHQ9dHlwZW9mIGU/LnRoZW49PWBmdW5jdGlvbmA/ZTpQcm9taXNlLnJlc29sdmUoZSk7bC5wdXNoKHQuY2F0Y2goKGU9Pihjb25zb2xlLndhcm4oRXJyb3IodSg0MiwhMSkpKSxjb25zb2xlLndhcm4oZSksITEpKSkpfWZ1bmN0aW9uIGVlKCl7bGV0IHQ9UmUoKTtlLmZvckVhY2goKGU9PmUucmVzb2x2ZSh0KSkpO3RyeXtTKGMubGVuZ3RoPT09MD9gbm8tYXBwLWNoYW5nZWA6YGFwcC1jaGFuZ2VgLGIoKSksUyhgcm91dGluZy1ldmVudGAsYigpKX1jYXRjaChlKXtzZXRUaW1lb3V0KCgoKT0+e3Rocm93IGV9KSl9aWYoej0hMSxCLmxlbmd0aD4wKXtsZXQgZT1CO0I9W10sVihlKX1yZXR1cm4gdH1mdW5jdGlvbiB5KCl7bnx8KGUuZm9yRWFjaCgoZT0+e1NlKGUuZXZlbnRBcmd1bWVudHMpfSkpLFNlKHQpKX1mdW5jdGlvbiBiKGU9ITEsbil7bGV0IGE9e30sbD17W3ZdOltdLFtnXTpbXSxbZl06W10sW3hdOltdfTtlPyhvLmNvbmNhdChzKS5mb3JFYWNoKCgoZSx0KT0+e3AoZSx2KX0pKSxyLmZvckVhY2goKGU9PntwKGUsZil9KSksaS5mb3JFYWNoKChlPT57cChlLGcpfSkpKTpjLmZvckVhY2goKGU9PntwKGUpfSkpO2xldCB1PXtkZXRhaWw6e25ld0FwcFN0YXR1c2VzOmEsYXBwc0J5TmV3U3RhdHVzOmwsdG90YWxBcHBDaGFuZ2VzOmMubGVuZ3RoLG9yaWdpbmFsRXZlbnQ6dD8uWzBdLG9sZFVybDpkLG5ld1VybDpofX07cmV0dXJuIG4mJlQodS5kZXRhaWwsbiksdTtmdW5jdGlvbiBwKGUsdCl7bGV0IG49QyhlKTt0fHw9emUobiksYVtuXT10LChsW3RdPWxbdF18fFtdKS5wdXNoKG4pfX1mdW5jdGlvbiBTKGUsdCl7bnx8d2luZG93LmRpc3BhdGNoRXZlbnQobmV3IGEoYHNpbmdsZS1zcGE6JHtlfWAsdCkpfX1mdW5jdGlvbiBYZShlLHQpe3JldHVybiBuZShlKT9vZShlKS50aGVuKChlPT50LnRoZW4oKCgpPT5uZShlKT9sZShlKTplKSkpKTp0LnRoZW4oKCgpPT5lKSl9dmFyIFplPSExO2Z1bmN0aW9uIFFlKGUpe1plPSEwLE4mJihrZShlKSxWKCkpfWZ1bmN0aW9uICRlKCl7cmV0dXJuIFplfXZhciBldD17Z2V0UmF3QXBwRGF0YTpmdW5jdGlvbigpe3JldHVyblsuLi5MXX0scmVyb3V0ZTpWLE5PVF9MT0FERUQ6Zix0b0xvYWRQcm9taXNlOmJlLHRvQm9vdHN0cmFwUHJvbWlzZTpvZSx1bnJlZ2lzdGVyQXBwbGljYXRpb246VWUsZ2V0UHJvZmlsZXJEYXRhOmZ1bmN0aW9uKCl7cmV0dXJuIGFlfX07TiYmd2luZG93Ll9fU0lOR0xFX1NQQV9ERVZUT09MU19fJiYod2luZG93Ll9fU0lOR0xFX1NQQV9ERVZUT09MU19fLmV4cG9zZWRNZXRob2RzPWV0KTt2YXIgdHQ9e2RldjpgaHR0cHM6Ly9say1hcGktZ2F0ZXdheS1kZXYubmdjbG91ZC5ydS9hcGkvdjEvYCx0ZXN0OmBodHRwczovL2xrLWFwaS1nYXRld2F5LXRlc3QubmdjbG91ZC5ydS9hcGkvdjEvYCxwcm9kOmBodHRwczovL2xrLWFwaS1nYXRld2F5Lm5nY2xvdWQucnUvYXBpL3YxL2B9LG50PWBtb2R1bGVwcmVsb2FkYCxydD1mdW5jdGlvbihlKXtyZXR1cm5gL2ArZX0saXQ9e30sYXQ9ZnVuY3Rpb24oZSx0LG4pe2xldCByPVByb21pc2UucmVzb2x2ZSgpO2lmKHQmJnQubGVuZ3RoPjApe2xldCBlPWRvY3VtZW50LmdldEVsZW1lbnRzQnlUYWdOYW1lKGBsaW5rYCksaT1kb2N1bWVudC5xdWVyeVNlbGVjdG9yKGBtZXRhW3Byb3BlcnR5PWNzcC1ub25jZV1gKSxhPWk/Lm5vbmNlfHxpPy5nZXRBdHRyaWJ1dGUoYG5vbmNlYCk7ZnVuY3Rpb24gbyhlKXtyZXR1cm4gUHJvbWlzZS5hbGwoZS5tYXAoZT0+UHJvbWlzZS5yZXNvbHZlKGUpLnRoZW4oZT0+KHtzdGF0dXM6YGZ1bGZpbGxlZGAsdmFsdWU6ZX0pLGU9Pih7c3RhdHVzOmByZWplY3RlZGAscmVhc29uOmV9KSkpKX1yPW8odC5tYXAodD0+e2lmKHQ9cnQodCxuKSx0IGluIGl0KXJldHVybjtpdFt0XT0hMDtsZXQgcj10LmVuZHNXaXRoKGAuY3NzYCksaT1yP2BbcmVsPSJzdHlsZXNoZWV0Il1gOmBgO2lmKG4pZm9yKGxldCBuPWUubGVuZ3RoLTE7bj49MDtuLS0pe2xldCBpPWVbbl07aWYoaS5ocmVmPT09dCYmKCFyfHxpLnJlbD09PWBzdHlsZXNoZWV0YCkpcmV0dXJufWVsc2UgaWYoZG9jdW1lbnQucXVlcnlTZWxlY3RvcihgbGlua1tocmVmPSIke3R9Il0ke2l9YCkpcmV0dXJuO2xldCBvPWRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoYGxpbmtgKTtpZihvLnJlbD1yP2BzdHlsZXNoZWV0YDpudCxyfHwoby5hcz1gc2NyaXB0YCksby5jcm9zc09yaWdpbj1gYCxvLmhyZWY9dCxhJiZvLnNldEF0dHJpYnV0ZShgbm9uY2VgLGEpLGRvY3VtZW50LmhlYWQuYXBwZW5kQ2hpbGQobykscilyZXR1cm4gbmV3IFByb21pc2UoKGUsbik9PntvLmFkZEV2ZW50TGlzdGVuZXIoYGxvYWRgLGUpLG8uYWRkRXZlbnRMaXN0ZW5lcihgZXJyb3JgLCgpPT5uKEVycm9yKGBVbmFibGUgdG8gcHJlbG9hZCBDU1MgZm9yICR7dH1gKSkpfSl9KSl9ZnVuY3Rpb24gaShlKXtsZXQgdD1uZXcgRXZlbnQoYHZpdGU6cHJlbG9hZEVycm9yYCx7Y2FuY2VsYWJsZTohMH0pO2lmKHQucGF5bG9hZD1lLHdpbmRvdy5kaXNwYXRjaEV2ZW50KHQpLCF0LmRlZmF1bHRQcmV2ZW50ZWQpdGhyb3cgZX1yZXR1cm4gci50aGVuKHQ9Pntmb3IobGV0IGUgb2YgdHx8W10pZS5zdGF0dXM9PT1gcmVqZWN0ZWRgJiZpKGUucmVhc29uKTtyZXR1cm4gZSgpLmNhdGNoKGkpfSl9O3R5cGVvZiBsb2NhbFN0b3JhZ2U8YHVgJiZsb2NhbFN0b3JhZ2UuZ2V0SXRlbShgaW1wb3J0bWFwLWRlYnVnYCk9PT1gMWAmJmNvbnNvbGUuaW5mbyhgW2ltcG9ydG1hcC11cGRhdGVyXSBtb2R1bGUgbG9hZGVkYCk7dmFyIG90PSgpPT57dHJ5e3JldHVybiBsb2NhbFN0b3JhZ2UuZ2V0SXRlbShgaW1wb3J0bWFwLWRlYnVnYCk9PT1gMWB9Y2F0Y2h7cmV0dXJuITF9fSxIPSguLi5lKT0+e290KCkmJmNvbnNvbGUuaW5mbyhgW2ltcG9ydG1hcC11cGRhdGVyXWAsLi4uZSl9LFU9e30sc3Q9ZT0+VVtlXT8/ZSxXPW5ldyBTZXQsRz1gYCxjdD1gYCxsdD1lPT4vXmh0dHBzPzpcL1wvL2kudGVzdChlKT9lOmAke2N0LnJlcGxhY2UoL1wvKyQvLGBgKX0ke2Uuc3RhcnRzV2l0aChgL2ApP2U6YC8ke2V9YH1gLHV0PWFzeW5jKCk9PntpZighRyl0aHJvdyBFcnJvcihgW2ltcG9ydG1hcC11cGRhdGVyXSBnYXRld2F5QmFzZSBpcyBub3Qgc2V0IChBVVRIX0hPU1RTW2Vudmlyb25tZW50XSByZXR1cm5lZCBlbXB0eSB2YWx1ZSlgKTtsZXQgZT1gJHtHfSR7Ry5lbmRzV2l0aChgL2ApP2BgOmAvYH1pYW0vbWZlL2ltcG9ydG1hcGAsdD1hd2FpdCBmZXRjaChlLHtjYWNoZTpgbm8tc3RvcmVgfSk7aWYoIXQub2spdGhyb3cgRXJyb3IoYFtpbXBvcnRtYXAtdXBkYXRlcl0gR0VUICR7ZX0g4oaSICR7dC5zdGF0dXN9YCk7cmV0dXJuIHQuanNvbigpfSxkdD0oKT0+e2xldCBlPWRvY3VtZW50LnF1ZXJ5U2VsZWN0b3JBbGwoYHNjcmlwdFt0eXBlPSJpbXBvcnRtYXAiXWApLHQ9e307Zm9yKGxldCBuIG9mIGUpdHJ5e2xldCBlPUpTT04ucGFyc2Uobi50ZXh0Q29udGVudD8/YGApO2ZvcihsZXRbbixyXW9mIE9iamVjdC5lbnRyaWVzKGUuaW1wb3J0cz8/e30pKW4hPT1gQG51YmVzL3Jvb3QtYXBwYCYmKHRbbl09cil9Y2F0Y2h7fXJldHVybiB0fSxmdD0oKT0+e25hdmlnYXRvci5zZXJ2aWNlV29ya2VyLmNvbnRyb2xsZXI/LnBvc3RNZXNzYWdlKHt0eXBlOmByZWJ1aWxkLXJ1bGVzYH0pfSxwdD1hc3luYyBlPT57SChgSU1QT1JUTUFQX1VQREFURUQgcmVjZWl2ZWRgLGU/LmRhdGEpO3RyeXtsZXQgZT1hd2FpdCB1dCgpLG49ZS5pbXBvcnRzPz97fTtIKGBmZXRjaGVkIG5ldyBpbXBvcnRtYXA6YCxuKSxIKGBjdXJyZW50IGltcG9ydG1hcDpgLFUpO2xldCByPVtdO2ZvcihsZXRbZSx0XW9mIE9iamVjdC5lbnRyaWVzKG4pKXtsZXQgbj1sdCh0KTtVW2VdIT09biYmci5wdXNoKHtuYW1lOmUsb2xkVXJsOlVbZV0sbmV3VXJsOm59KX1pZihyLmxlbmd0aD09PTApe2NvbnNvbGUuaW5mbyhgW2ltcG9ydG1hcC11cGRhdGVyXSBJTVBPUlRNQVBfVVBEQVRFRCByZWNlaXZlZCDigJQgbm8gVVJMIGNoYW5nZXMsIHNraXBwaW5nIHVwZGF0ZWApO3JldHVybn1jb25zb2xlLmluZm8oYFtpbXBvcnRtYXAtdXBkYXRlcl0gZGV0ZWN0ZWQgdmVyc2lvbiBjaGFuZ2VzOmApO2ZvcihsZXR7bmFtZTplLG9sZFVybDp0LG5ld1VybDpufW9mIHIpY29uc29sZS5pbmZvKGAgIPCfk6YgJHtlfWApLGNvbnNvbGUuaW5mbyhgICAgICBPTEQ6ICR7dD8/YChub3QgdHJhY2tlZClgfWApLGNvbnNvbGUuaW5mbyhgICAgICBORVc6ICR7bn1gKTtsZXQgaT1uZXcgU2V0KFIoKSksYT0hMTtmb3IobGV0e25hbWU6ZSxuZXdVcmw6dH1vZiByKVVbZV09dCxpLmhhcyhlKT8oVy5hZGQoZSksSChg4pyFIFNQQS1hcHAgIiR7ZX0iIHNjaGVkdWxlZCBmb3IgdW5sb2FkIChob3Qtc3dhcCBvbiBuZXh0IHJvdXRlIGNoYW5nZSlgKSk6KGE9ITAsSChg8J+UhCAiJHtlfSIgdXBkYXRlZCDihpIgcmVsb2FkIHNjaGVkdWxlZCBvbiBuZXh0IG5hdmlnYXRpb25gKSk7dHJ5e2xvY2FsU3RvcmFnZS5zZXRJdGVtKGBtZmUtaW1wb3J0bWFwLWNhY2hlLTAuMi4yNWAsSlNPTi5zdHJpbmdpZnkoZSkpLEgoYGxvY2FsU3RvcmFnZSBpbXBvcnRtYXAgY2FjaGUgdXBkYXRlZGApfWNhdGNoe31pZihhKXtsZXQgZT1yLmZpbHRlcigoe25hbWU6ZX0pPT4hbmV3IFNldChSKCkpLmhhcyhlKSkubWFwKCh7bmFtZTplfSk9PmUpO3QuZW1pdChgdmVyc2lvbjphdmFpbGFibGVgLHtzb3VyY2U6YHdzYCxwYWNrYWdlczplfSl9ZnQoKX1jYXRjaChlKXtjb25zb2xlLmVycm9yKGBbaW1wb3J0bWFwLXVwZGF0ZXJdIGZhaWxlZCB0byBhcHBseSBpbXBvcnRtYXAgdXBkYXRlOmAsZSl9fSxtdD0oKT0+e2lmKFcuc2l6ZSE9PTApZm9yKGxldCBlIG9mWy4uLlddKVcuZGVsZXRlKGUpLEgoYHVubG9hZGluZyBvdXRkYXRlZCBTUEEtYXBwIGJlZm9yZSByb3V0ZSBjaGFuZ2U6YCxlKSxXZShlKS5jYXRjaCh0PT57Y29uc29sZS53YXJuKGBbaW1wb3J0bWFwLXVwZGF0ZXJdIHVubG9hZEFwcGxpY2F0aW9uIGZhaWxlZCBmb3JgLGUsdCl9KX0saHQ9YXN5bmMoKT0+e0goYHN0YXJ0SW1wb3J0bWFwVXBkYXRlciBjYWxsZWRgKTt0cnl7bGV0e2V2ZW50QnVzOmUsZ2V0VXJsczp0fT1hd2FpdCBhdCgoKT0+aW1wb3J0KGBAbnViZXMvY29tbW9uLXV0aWxzYCksW10pO0goYGNvbW1vbi11dGlscyBsb2FkZWQ7IGVudiA9YCx0LmVudmlyb25tZW50KSxHPXR0W3QuZW52aXJvbm1lbnRdPz9gYCxHfHxjb25zb2xlLndhcm4oYFtpbXBvcnRtYXAtdXBkYXRlcl0gR0FURVdBWV9IT1NUU1tlbnZpcm9ubWVudF0gaXMgZW1wdHkg4oCUIGltcG9ydG1hcCBmZXRjaCB3aWxsIGJlIHNraXBwZWRgKTtsZXQgbj1gL3MzLW1mZWAucmVwbGFjZSgvXC8rJC8sYGApO2N0PW4uc3RhcnRzV2l0aChgL2ApP2Ake3dpbmRvdy5sb2NhdGlvbi5vcmlnaW59JHtufWA6bixIKGBnYXRld2F5QmFzZSA9YCxHKSxIKGBtZmVCYXNlVXJsID1gLGN0KSxVPWR0KCksSChgY3VycmVudEltcG9ydHMgZnJvbSBET006YCxPYmplY3Qua2V5cyhVKSk7dHJ5e2xldCBlPWF3YWl0IHV0KCksdD0wO2ZvcihsZXRbbixyXW9mIE9iamVjdC5lbnRyaWVzKGUuaW1wb3J0cz8/e30pKVVbbl09PT12b2lkIDAmJihVW25dPWx0KHIpLHQrKyk7SChgYmFzZWxpbmUgZmlsbGVkIGZyb20gaW1wb3J0bWFwLUFQSSAoYWRkZWQgJHt0fSBlbnRyaWVzKWApfWNhdGNoKGUpe2NvbnNvbGUud2FybihgW2ltcG9ydG1hcC11cGRhdGVyXSBiYXNlbGluZSBpbXBvcnRtYXAgZmV0Y2ggZmFpbGVkOmAsZSl9aWYod2luZG93LmltcG9ydE1hcE92ZXJyaWRlcyl7bGV0IGU9W10sdD13aW5kb3cuaW1wb3J0TWFwT3ZlcnJpZGVzLmdldE92ZXJyaWRlTWFwKCk7Zm9yKGxldFtuLHJdb2YgT2JqZWN0LmVudHJpZXModC5pbXBvcnRzKSl7bGV0IHQ9VVtuXTt0JiZsdChyKT09PXQmJmUucHVzaChuKX1pZihlLmxlbmd0aD4wKXtIKGByZW1vdmluZyBzdGFsZSBJTU8gb3ZlcnJpZGVzIChzYW1lIGFzIGJvb3RzdHJhcCBpbXBvcnRtYXApOmAsZSk7Zm9yKGxldCB0IG9mIGUpd2luZG93LmltcG9ydE1hcE92ZXJyaWRlcy5yZW1vdmVPdmVycmlkZSh0KX19ZS5vbihgKmAsKGUsdCk9PntIKGBldmVudEJ1cyBldmVudCByZWNlaXZlZDpgLGUsdCl9KSxlLm9uKGBJTVBPUlRNQVBfVVBEQVRFRGAsZT0+e3B0KGUpfSksSChgc3Vic2NyaWJlZCB0byBldmVudEJ1cyAod2lsZGNhcmQgKyBJTVBPUlRNQVBfVVBEQVRFRClgKSx3aW5kb3cuYWRkRXZlbnRMaXN0ZW5lcihgc2luZ2xlLXNwYTpiZWZvcmUtcm91dGluZy1ldmVudGAsbXQpLEgoYOKchSBzdGFydGVkLCB0cmFja2luZ2AsT2JqZWN0LmtleXMoVSkubGVuZ3RoLGBNRkUgZW50cmllc2ApfWNhdGNoKGUpe2NvbnNvbGUuZXJyb3IoYFtpbXBvcnRtYXAtdXBkYXRlcl0g4p2MIGluaXQgZmFpbGVkOmAsZSl9fTtmdW5jdGlvbiBndChlLHQpe3ZhciBuPU9iamVjdC5rZXlzKGUpO2lmKE9iamVjdC5nZXRPd25Qcm9wZXJ0eVN5bWJvbHMpe3ZhciByPU9iamVjdC5nZXRPd25Qcm9wZXJ0eVN5bWJvbHMoZSk7dCYmKHI9ci5maWx0ZXIoKGZ1bmN0aW9uKHQpe3JldHVybiBPYmplY3QuZ2V0T3duUHJvcGVydHlEZXNjcmlwdG9yKGUsdCkuZW51bWVyYWJsZX0pKSksbi5wdXNoLmFwcGx5KG4scil9cmV0dXJuIG59ZnVuY3Rpb24gX3QoZSl7Zm9yKHZhciB0PTE7dDxhcmd1bWVudHMubGVuZ3RoO3QrKyl7dmFyIG49YXJndW1lbnRzW3RdPT1udWxsP3t9OmFyZ3VtZW50c1t0XTt0JTI/Z3QoT2JqZWN0KG4pLCEwKS5mb3JFYWNoKChmdW5jdGlvbih0KXt2dChlLHQsblt0XSl9KSk6T2JqZWN0LmdldE93blByb3BlcnR5RGVzY3JpcHRvcnM/T2JqZWN0LmRlZmluZVByb3BlcnRpZXMoZSxPYmplY3QuZ2V0T3duUHJvcGVydHlEZXNjcmlwdG9ycyhuKSk6Z3QoT2JqZWN0KG4pKS5mb3JFYWNoKChmdW5jdGlvbih0KXtPYmplY3QuZGVmaW5lUHJvcGVydHkoZSx0LE9iamVjdC5nZXRPd25Qcm9wZXJ0eURlc2NyaXB0b3Iobix0KSl9KSl9cmV0dXJuIGV9ZnVuY3Rpb24gSyhlKXtyZXR1cm4oSz10eXBlb2YgU3ltYm9sPT1gZnVuY3Rpb25gJiZ0eXBlb2YgU3ltYm9sLml0ZXJhdG9yPT1gc3ltYm9sYD9mdW5jdGlvbihlKXtyZXR1cm4gdHlwZW9mIGV9OmZ1bmN0aW9uKGUpe3JldHVybiBlJiZ0eXBlb2YgU3ltYm9sPT1gZnVuY3Rpb25gJiZlLmNvbnN0cnVjdG9yPT09U3ltYm9sJiZlIT09U3ltYm9sLnByb3RvdHlwZT9gc3ltYm9sYDp0eXBlb2YgZX0pKGUpfWZ1bmN0aW9uIHZ0KGUsdCxuKXtyZXR1cm4gdCBpbiBlP09iamVjdC5kZWZpbmVQcm9wZXJ0eShlLHQse3ZhbHVlOm4sZW51bWVyYWJsZTohMCxjb25maWd1cmFibGU6ITAsd3JpdGFibGU6ITB9KTplW3RdPW4sZX1mdW5jdGlvbiB5dChlKXtyZXR1cm4gZnVuY3Rpb24oZSl7aWYoQXJyYXkuaXNBcnJheShlKSlyZXR1cm4geHQoZSl9KGUpfHxmdW5jdGlvbihlKXtpZih0eXBlb2YgU3ltYm9sPGB1YCYmZVtTeW1ib2wuaXRlcmF0b3JdIT1udWxsfHxlW2BAQGl0ZXJhdG9yYF0hPW51bGwpcmV0dXJuIEFycmF5LmZyb20oZSl9KGUpfHxidChlKXx8ZnVuY3Rpb24oKXt0aHJvdyBUeXBlRXJyb3IoYEludmFsaWQgYXR0ZW1wdCB0byBzcHJlYWQgbm9uLWl0ZXJhYmxlIGluc3RhbmNlLgpJbiBvcmRlciB0byBiZSBpdGVyYWJsZSwgbm9uLWFycmF5IG9iamVjdHMgbXVzdCBoYXZlIGEgW1N5bWJvbC5pdGVyYXRvcl0oKSBtZXRob2QuYCl9KCl9ZnVuY3Rpb24gYnQoZSx0KXtpZihlKXtpZih0eXBlb2YgZT09YHN0cmluZ2ApcmV0dXJuIHh0KGUsdCk7dmFyIG49T2JqZWN0LnByb3RvdHlwZS50b1N0cmluZy5jYWxsKGUpLnNsaWNlKDgsLTEpO3JldHVybiBuPT09YE9iamVjdGAmJmUuY29uc3RydWN0b3ImJihuPWUuY29uc3RydWN0b3IubmFtZSksbj09PWBNYXBgfHxuPT09YFNldGA/QXJyYXkuZnJvbShlKTpuPT09YEFyZ3VtZW50c2B8fC9eKD86VWl8SSludCg/Ojh8MTZ8MzIpKD86Q2xhbXBlZCk/QXJyYXkkLy50ZXN0KG4pP3h0KGUsdCk6dm9pZCAwfX1mdW5jdGlvbiB4dChlLHQpeyh0PT1udWxsfHx0PmUubGVuZ3RoKSYmKHQ9ZS5sZW5ndGgpO2Zvcih2YXIgbj0wLHI9QXJyYXkodCk7bjx0O24rKylyW25dPWVbbl07cmV0dXJuIHJ9dmFyIHE9dHlwZW9mIHdpbmRvdzxgdWA7ZnVuY3Rpb24gSihlLHQpe2lmKEsodCkhPT1gb2JqZWN0YHx8QXJyYXkuaXNBcnJheSh0KXx8dD09PW51bGwpdGhyb3cgRXJyb3IoYEludmFsaWQgJHtlfTogcmVjZWl2ZWQgJHtBcnJheS5pc0FycmF5KHQpP2BhcnJheWA6dH0gYnV0IGV4cGVjdGVkIGEgcGxhaW4gb2JqZWN0YCl9ZnVuY3Rpb24gU3QoZSx0KXtpZih0eXBlb2YgdCE9YGJvb2xlYW5gKXRocm93IEVycm9yKGBJbnZhbGlkICR7ZX06IHJlY2VpdmVkICR7Syh0KX0sIGJ1dCBleHBlY3RlZCBhIGJvb2xlYW5gKX1mdW5jdGlvbiBDdChlLHQsbixyKXtpZighcil7dmFyIGk9T2JqZWN0LmtleXModCksYT1bXTtpLmZvckVhY2goKGZ1bmN0aW9uKGUpe24uaW5kZXhPZihlKTwwJiZhLnB1c2goZSl9KSksYS5sZW5ndGg+MCYmY29uc29sZS53YXJuKEVycm9yKGBJbnZhbGlkICR7ZX06IHJlY2VpdmVkIGludmFsaWQgcHJvcGVydGllcyAnJHthLmpvaW4oYCwgYCl9JywgYnV0IHZhbGlkIHByb3BlcnRpZXMgYXJlICR7bi5qb2luKGAsIGApfWApKX19ZnVuY3Rpb24gWShlLHQpe3ZhciBuPSEoYXJndW1lbnRzLmxlbmd0aD4yJiZhcmd1bWVudHNbMl0hPT12b2lkIDApfHxhcmd1bWVudHNbMl07aWYodHlwZW9mIHQhPWBzdHJpbmdgfHxuJiZ0LnRyaW0oKT09PWBgKXRocm93IEVycm9yKGBJbnZhbGlkICR7ZX06IHJlY2VpdmVkICcke3R9JywgYnV0IGV4cGVjdGVkIGEke24/YCBub24tYmxhbmtgOmBgfSBzdHJpbmdgKX1mdW5jdGlvbiB3dChlLHQpe2lmKFkoZSx0KSx0LmluZGV4T2YoYC9gKTwwKXRocm93IEVycm9yKGBJbnZhbGlkICR7ZX06IHJlY2VpdmVkICcke3R9JywgYnV0IGV4cGVjdGVkIGFuIGFic29sdXRlIHBhdGggdGhhdCBzdGFydHMgd2l0aCAvYCl9ZnVuY3Rpb24gVHQoZSx0LG4pe2lmKCFBcnJheS5pc0FycmF5KHQpJiYoSyhLKHQpKSE9PWBvYmplY3RgfHx0Lmxlbmd0aCE9PWBudW1iZXJgKSl0aHJvdyBFcnJvcihgSW52YWxpZCAke2V9OiByZWNlaXZlZCAnJHt0fScsIGJ1dCBleHBlY3RlZCBhbiBhcnJheWApO2Zvcih2YXIgcj1bLi4uYXJndW1lbnRzXS5zbGljZSgzKSxpPTA7aTx0Lmxlbmd0aDtpKyspbi5hcHBseSh2b2lkIDAsW3RbaV0sYCR7ZX1bJHtpfV1gXS5jb25jYXQocikpfWZ1bmN0aW9uIEV0KGUsdCl7dmFyIG47cmV0dXJuKG49ZS5zdWJzdHIoLTEpPT09YC9gP3RbMF09PT1gL2A/ZSt0LnNsaWNlKDEpOmUrdDp0WzBdPT09YC9gP2UrdDplK2AvYCt0KS5zdWJzdHIoLTEpPT09YC9gJiZuLmxlbmd0aD4xJiYobj1uLnNsaWNlKDAsbi5sZW5ndGgtMSkpLG59ZnVuY3Rpb24gRHQoZSx0KXtmb3IodmFyIG49MDtuPGUubGVuZ3RoO24rKylpZih0KGVbbl0pKXJldHVybiBlW25dO3JldHVybiBudWxsfXZhciBPdD10eXBlb2YgU3ltYm9sPGB1YD9TeW1ib2woKTpgQGA7ZnVuY3Rpb24ga3QoZSx0KXtpZihlJiZlLm5vZGVOYW1lfHx0eXBlb2YgZT09YHN0cmluZ2Ape2lmKHEmJiF0JiZ3aW5kb3cuc2luZ2xlU3BhTGF5b3V0RGF0YSYmKHQ9d2luZG93LnNpbmdsZVNwYUxheW91dERhdGEpLHR5cGVvZiBlPT1gc3RyaW5nYCl7aWYoIXEpdGhyb3cgRXJyb3IoYGNhbGxpbmcgY29uc3RydWN0Um91dGVzIHdpdGggYSBzdHJpbmcgb24gdGhlIHNlcnZlciBpcyBub3QgeWV0IHN1cHBvcnRlZGApO2lmKCEoZT1uZXcgRE9NUGFyc2VyKCkucGFyc2VGcm9tU3RyaW5nKGUsYHRleHQvaHRtbGApLmRvY3VtZW50RWxlbWVudC5xdWVyeVNlbGVjdG9yKGBzaW5nbGUtc3BhLXJvdXRlcmApKSl0aHJvdyBFcnJvcihgY29uc3RydWN0Um91dGVzIHNob3VsZCBiZSBjYWxsZWQgd2l0aCBhIHN0cmluZyBIVE1MIGRvY3VtZW50IHRoYXQgY29udGFpbnMgYSA8c2luZ2xlLXNwYS1yb3V0ZXI+IGVsZW1lbnQuYCl9ZT1mdW5jdGlvbihlKXt2YXIgdD1hcmd1bWVudHMubGVuZ3RoPjEmJmFyZ3VtZW50c1sxXSE9PXZvaWQgMD9hcmd1bWVudHNbMV06e307aWYoZS5ub2RlTmFtZS50b0xvd2VyQ2FzZSgpPT09YHRlbXBsYXRlYCYmKGU9KGUuY29udGVudHx8ZSkucXVlcnlTZWxlY3Rvcihgc2luZ2xlLXNwYS1yb3V0ZXJgKSksZS5ub2RlTmFtZS50b0xvd2VyQ2FzZSgpIT09YHNpbmdsZS1zcGEtcm91dGVyYCl0aHJvdyBFcnJvcihgc2luZ2xlLXNwYS1sYXlvdXQ6IFRoZSBIVE1MRWxlbWVudCBwYXNzZWQgdG8gY29uc3RydWN0Um91dGVzIG11c3QgYmUgPHNpbmdsZS1zcGEtcm91dGVyPiBvciBhIDx0ZW1wbGF0ZT4gY29udGFpbmluZyB0aGUgcm91dGVyLiBSZWNlaXZlZCAke2Uubm9kZU5hbWV9YCk7cSYmZS5pc0Nvbm5lY3RlZCYmZS5wYXJlbnROb2RlLnJlbW92ZUNoaWxkKGUpO3ZhciBuPXtyb3V0ZXM6W10scmVkaXJlY3RzOnt9fTtYKGUsYG1vZGVgKSYmKG4ubW9kZT1YKGUsYG1vZGVgKSksWChlLGBiYXNlYCkmJihuLmJhc2U9WChlLGBiYXNlYCkpLFgoZSxgY29udGFpbmVyRWxgKSYmKG4uY29udGFpbmVyRWw9WChlLGBjb250YWluZXJFbGApKTtmb3IodmFyIHI9MDtyPGUuY2hpbGROb2Rlcy5sZW5ndGg7cisrKXt2YXIgaTsoaT1uLnJvdXRlcykucHVzaC5hcHBseShpLHl0KFooZS5jaGlsZE5vZGVzW3JdLHQsbikpKX1yZXR1cm4gbn0oZSx0KX1lbHNlIGlmKHQpdGhyb3cgRXJyb3IoYGNvbnN0cnVjdFJvdXRlcyBzaG91bGQgYmUgY2FsbGVkIGVpdGhlciB3aXRoIGFuIEhUTUxFbGVtZW50IGFuZCBsYXlvdXREYXRhLCBvciBhIHNpbmdsZSBqc29uIG9iamVjdC5gKTtyZXR1cm4gZnVuY3Rpb24oZSl7Sihgcm91dGVzQ29uZmlnYCxlKTt2YXIgdCxuPWUuZGlzYWJsZVdhcm5pbmdzO2lmKEN0KGByb3V0ZXNDb25maWdgLGUsW2Btb2RlYCxgYmFzZWAsYGNvbnRhaW5lckVsYCxgcm91dGVzYCxgZGlzYWJsZVdhcm5pbmdzYCxgcmVkaXJlY3RzYF0sbiksZS5oYXNPd25Qcm9wZXJ0eShgY29udGFpbmVyRWxgKT9mdW5jdGlvbihlLHQpe2lmKHR5cGVvZiB0PT1gc3RyaW5nYD90LnRyaW0oKT09PWBgOiEocSYmdCBpbnN0YW5jZW9mIEhUTUxFbGVtZW50KSl0aHJvdyBFcnJvcihgSW52YWxpZCByb3V0ZXNDb25maWcuY29udGFpbmVyRWw6IHJlY2VpdmVkICR7dH0gYnV0IGV4cGVjdGVkIGVpdGhlciBub24tYmxhbmsgc3RyaW5nIG9yIEhUTUxFbGVtZW50YCl9KDAsZS5jb250YWluZXJFbCk6ZS5jb250YWluZXJFbD1gYm9keWAsZS5oYXNPd25Qcm9wZXJ0eShgbW9kZWApfHwoZS5tb2RlPWBoaXN0b3J5YCksZnVuY3Rpb24oZSx0LG4pe2lmKG4uaW5kZXhPZih0KTwwKXRocm93IEVycm9yKGBJbnZhbGlkIHJvdXRlc0NvbmZpZy5tb2RlOiByZWNlaXZlZCAnJHt0fScgYnV0IGV4cGVjdGVkICR7bi5qb2luKGAsIGApfWApfSgwLGUubW9kZSxbYGhpc3RvcnlgLGBoYXNoYF0pLGUuaGFzT3duUHJvcGVydHkoYGJhc2VgKT8oWShgcm91dGVzQ29uZmlnLmJhc2VgLGUuYmFzZSksZS5iYXNlPSgodD1lLmJhc2UpLmluZGV4T2YoYC9gKSE9PTAmJih0PWAvYCt0KSx0W3QubGVuZ3RoLTFdIT09YC9gJiYodCs9YC9gKSx0KSk6ZS5iYXNlPWAvYCxlLmhhc093blByb3BlcnR5KGByZWRpcmVjdHNgKSlmb3IodmFyIHIgaW4gSihgcm91dGVzQ29uZmlnLnJlZGlyZWN0c2AsZS5yZWRpcmVjdHMpLGUucmVkaXJlY3RzKXt2YXIgaT1lLnJlZGlyZWN0c1tyXTt3dChgcm91dGVzQ29uZmlnLnJlZGlyZWN0cyBrZXlgLHIpLHd0KGByb3V0ZXNDb25maWcucmVkaXJlY3RzWycke3J9J11gLGkpfXZhciBhPXE/d2luZG93LmxvY2F0aW9uLnBhdGhuYW1lOmAvYCxvPWUubW9kZT09PWBoYXNoYD9hK2AjYDpgYDtUdChgcm91dGVzQ29uZmlnLnJvdXRlc2AsZS5yb3V0ZXMsKGZ1bmN0aW9uIGUodCxyLGkpe3ZhciBhPWkucGFyZW50UGF0aCxvPWkuc2libGluZ0FjdGl2ZVdoZW5zLHM9aS5wYXJlbnRBY3RpdmVXaGVuO2lmKEoocix0KSx0LnR5cGU9PT1gYXBwbGljYXRpb25gKUN0KHIsdCxbYHR5cGVgLGBuYW1lYCxgcHJvcHNgLGBsb2FkZXJgLGBlcnJvcmAsYGNsYXNzTmFtZWBdLG4pLHQucHJvcHMmJkooYCR7cn0ucHJvcHNgLHQucHJvcHMpLFkoYCR7cn0ubmFtZWAsdC5uYW1lKTtlbHNlIGlmKHQudHlwZT09PWByb3V0ZWApe0N0KHIsdCxbYHR5cGVgLGBwYXRoYCxgcm91dGVzYCxgcHJvcHNgLGBkZWZhdWx0YCxgZXhhY3RgXSxuKSx0Lmhhc093blByb3BlcnR5KGBleGFjdGApJiZTdChgJHtyfS5leGFjdGAsdC5leGFjdCk7dmFyIGMsbD10Lmhhc093blByb3BlcnR5KGBwYXRoYCksdT10Lmhhc093blByb3BlcnR5KGBkZWZhdWx0YCk7aWYobClZKGAke3J9LnBhdGhgLHQucGF0aCksYz1FdChhLHQucGF0aCksdC5hY3RpdmVXaGVuPXFlKGMsdC5leGFjdCksby5wdXNoKHQuYWN0aXZlV2hlbik7ZWxzZXtpZighdSl0aHJvdyBFcnJvcihgSW52YWxpZCAke3J9OiByb3V0ZXMgbXVzdCBoYXZlIGVpdGhlciBhIHBhdGggb3IgZGVmYXVsdCBwcm9wZXJ0eS5gKTtTdChgJHtyfS5kZWZhdWx0YCx0LmRlZmF1bHQpLGM9YSx0LmFjdGl2ZVdoZW49ZnVuY3Rpb24oZSx0KXtyZXR1cm4gZnVuY3Rpb24obil7cmV0dXJuIHQobikmJiFlLnNvbWUoKGZ1bmN0aW9uKGUpe3JldHVybiBlKG4pfSkpfX0obyxzKX1pZihsJiZ1JiZ0LmRlZmF1bHQpdGhyb3cgRXJyb3IoYEludmFsaWQgJHtyfTogY2Fubm90IGhhdmUgYm90aCBwYXRoIGFuZCBzZXQgZGVmYXVsdCB0byB0cnVlLmApO3Qucm91dGVzJiZUdChgJHtyfS5yb3V0ZXNgLHQucm91dGVzLGUse3BhcmVudFBhdGg6YyxzaWJsaW5nQWN0aXZlV2hlbnM6W10scGFyZW50QWN0aXZlV2hlbjp0LmFjdGl2ZVdoZW59KX1lbHNle2lmKCEodHlwZW9mIE5vZGU8YHVgJiZ0IGluc3RhbmNlb2YgTm9kZSkpZm9yKHZhciBkIGluIHQpZCE9PWByb3V0ZXNgJiZkIT09YGF0dHJzYCYmWShgJHtyfVsnJHtkfSddYCx0W2RdLCExKTt0LnJvdXRlcyYmVHQoYCR7cn0ucm91dGVzYCx0LnJvdXRlcyxlLHtwYXJlbnRQYXRoOmEsc2libGluZ0FjdGl2ZVdoZW5zOm8scGFyZW50QWN0aXZlV2hlbjpzfSl9fSkse3BhcmVudFBhdGg6bytlLmJhc2UscGFyZW50QWN0aXZlV2hlbjpmdW5jdGlvbigpe3JldHVybiEwfSxzaWJsaW5nQWN0aXZlV2hlbnM6W119KSxkZWxldGUgZS5kaXNhYmxlV2FybmluZ3N9KGUpLGV9ZnVuY3Rpb24gWChlLHQpe2lmKHEpcmV0dXJuIGUuZ2V0QXR0cmlidXRlKHQpO3ZhciBuPUR0KGUuYXR0cnMsKGZ1bmN0aW9uKGUpe3JldHVybiBlLm5hbWU9PT10LnRvTG93ZXJDYXNlKCl9KSk7cmV0dXJuIG4/bi52YWx1ZTpudWxsfWZ1bmN0aW9uIEF0KGUsdCl7cmV0dXJuIHE/ZS5oYXNBdHRyaWJ1dGUodCk6ZS5hdHRycy5zb21lKChmdW5jdGlvbihlKXtyZXR1cm4gZS5uYW1lPT09dH0pKX1mdW5jdGlvbiBaKGUsdCxuKXtpZihlLm5vZGVOYW1lLnRvTG93ZXJDYXNlKCk9PT1gYXBwbGljYXRpb25gKXtpZihlLmNoaWxkTm9kZXMubGVuZ3RoPjApdGhyb3cgRXJyb3IoYDxhcHBsaWNhdGlvbj4gZWxlbWVudHMgbXVzdCBub3QgaGF2ZSBjaGlsZE5vZGVzLiBZb3UgbXVzdCBwdXQgaW4gYSBjbG9zaW5nIDwvYXBwbGljYXRpb24+IC0gc2VsZiBjbG9zaW5nIGlzIG5vdCBhbGxvd2VkYCk7dmFyIHI9e3R5cGU6YGFwcGxpY2F0aW9uYCxuYW1lOlgoZSxgbmFtZWApfSxpPVgoZSxgbG9hZGVyYCk7aWYoaSl7aWYodC5sb2FkZXJzJiZ0LmxvYWRlcnMuaGFzT3duUHJvcGVydHkoaSkpci5sb2FkZXI9dC5sb2FkZXJzW2ldO2Vsc2UgaWYocSl0aHJvdyBFcnJvcihgQXBwbGljYXRpb24gbG9hZGVyICcke2l9JyB3YXMgbm90IGRlZmluZWQgaW4gdGhlIGh0bWxMYXlvdXREYXRhYCl9dmFyIGE9WChlLGBlcnJvcmApO2lmKGEpe2lmKHQuZXJyb3JzJiZ0LmVycm9ycy5oYXNPd25Qcm9wZXJ0eShhKSlyLmVycm9yPXQuZXJyb3JzW2FdO2Vsc2UgaWYocSl0aHJvdyBFcnJvcihgQXBwbGljYXRpb24gZXJyb3IgaGFuZGxlciAnJHtpfScgd2FzIG5vdCBkZWZpbmVkIGluIHRoZSBodG1sTGF5b3V0RGF0YWApfXZhciBvPVgoZSxgY2xhc3NgKTtyZXR1cm4gbyYmKHIuY2xhc3NOYW1lPW8pLGp0KGUscix0KSxbcl19aWYoZS5ub2RlTmFtZS50b0xvd2VyQ2FzZSgpPT09YHJvdXRlYCl7dmFyIHM9e3R5cGU6YHJvdXRlYCxyb3V0ZXM6W119LGM9WChlLGBwYXRoYCk7YyYmKHMucGF0aD1jKSxBdChlLGBkZWZhdWx0YCkmJihzLmRlZmF1bHQ9ITApLEF0KGUsYGV4YWN0YCkmJihzLmV4YWN0PSEwKSxqdChlLHMsdCk7Zm9yKHZhciBsPTA7bDxlLmNoaWxkTm9kZXMubGVuZ3RoO2wrKyl7dmFyIHU7KHU9cy5yb3V0ZXMpLnB1c2guYXBwbHkodSx5dChaKGUuY2hpbGROb2Rlc1tsXSx0LG4pKSl9cmV0dXJuW3NdfWlmKGUubm9kZU5hbWUudG9Mb3dlckNhc2UoKT09PWByZWRpcmVjdGApcmV0dXJuIG4ucmVkaXJlY3RzW0V0KGAvYCxYKGUsYGZyb21gKSldPUV0KGAvYCxYKGUsYHRvYCkpLFtdO2lmKHR5cGVvZiBOb2RlPGB1YCYmZSBpbnN0YW5jZW9mIE5vZGUpe2lmKGUubm9kZVR5cGU9PT1Ob2RlLlRFWFRfTk9ERSYmZS50ZXh0Q29udGVudC50cmltKCk9PT1gYClyZXR1cm5bXTtpZihlLmNoaWxkTm9kZXMmJmUuY2hpbGROb2Rlcy5sZW5ndGg+MCl7ZS5yb3V0ZXM9W107Zm9yKHZhciBkPTA7ZDxlLmNoaWxkTm9kZXMubGVuZ3RoO2QrKyl7dmFyIGY7KGY9ZS5yb3V0ZXMpLnB1c2guYXBwbHkoZix5dChaKGUuY2hpbGROb2Rlc1tkXSx0LG4pKSl9fXJldHVybltlXX1pZihlLmNoaWxkTm9kZXMpe2Zvcih2YXIgcD17dHlwZTplLm5vZGVOYW1lLnRvTG93ZXJDYXNlKCkscm91dGVzOltdLGF0dHJzOmUuYXR0cnN9LG09MDttPGUuY2hpbGROb2Rlcy5sZW5ndGg7bSsrKXt2YXIgaDsoaD1wLnJvdXRlcykucHVzaC5hcHBseShoLHl0KFooZS5jaGlsZE5vZGVzW21dLHQsbikpKX1yZXR1cm5bcF19cmV0dXJuIGUubm9kZU5hbWU9PT1gI2NvbW1lbnRgP1t7dHlwZTpgI2NvbW1lbnRgLHZhbHVlOmUuZGF0YX1dOmUubm9kZU5hbWU9PT1gI3RleHRgP1t7dHlwZTpgI3RleHRgLHZhbHVlOmUudmFsdWV9XTp2b2lkIDB9ZnVuY3Rpb24ganQoZSx0LG4pe2Zvcih2YXIgcj0oWChlLGBwcm9wc2ApfHxgYCkuc3BsaXQoYCxgKSxpPTA7aTxyLmxlbmd0aDtpKyspe3ZhciBhPXJbaV0udHJpbSgpO2lmKGEubGVuZ3RoIT09MClpZih0LnByb3BzfHw9e30sbi5wcm9wcyYmbi5wcm9wcy5oYXNPd25Qcm9wZXJ0eShhKSl0LnByb3BzW2FdPW4ucHJvcHNbYV07ZWxzZXtpZihxKXRocm93IEVycm9yKGBQcm9wICcke2F9JyB3YXMgbm90IGRlZmluZWQgaW4gdGhlIGh0bWxMYXlvdXREYXRhLiBFaXRoZXIgcmVtb3ZlIHRoaXMgYXR0cmlidXRlIGZyb20gdGhlIEhUTUwgZWxlbWVudCBvciBwcm92aWRlIHRoZSBwcm9wJ3MgdmFsdWVgKTt0LnByb3BzW2FdPU90fX19ZnVuY3Rpb24gTXQoZSl7cmV0dXJue2Jvb3RzdHJhcDpmdW5jdGlvbigpe3JldHVybiBQcm9taXNlLnJlc29sdmUoKX0sbW91bnQ6ZnVuY3Rpb24odCl7cmV0dXJuIFByb21pc2UucmVzb2x2ZSgpLnRoZW4oKGZ1bmN0aW9uKCl7dC5kb21FbGVtZW50LmlubmVySFRNTD1lfSkpfSx1bm1vdW50OmZ1bmN0aW9uKGUpe3JldHVybiBQcm9taXNlLnJlc29sdmUoKS50aGVuKChmdW5jdGlvbigpe2UuZG9tRWxlbWVudC5pbm5lckhUTUw9YGB9KSl9fX1mdW5jdGlvbiBOdChlKXt2YXIgdD1lLnJvdXRlcztlLmFwcGxpY2F0aW9uczt2YXIgbj1lLmFjdGl2ZSxyPW49PT12b2lkIDB8fG4saT0hMSxhPXt9LG89cSYmISF3aW5kb3cuc2luZ2xlU3BhTGF5b3V0RGF0YTtpZighdCl0aHJvdyBFcnJvcihgc2luZ2xlLXNwYS1sYXlvdXQgY29uc3RydWN0TGF5b3V0RW5naW5lKG9wdHMpOiBvcHRzLnJvdXRlcyBtdXN0IGJlIHByb3ZpZGVkLiBWYWx1ZSB3YXMgJHtLKHQpfWApO3ZhciBzPXQuYmFzZS5zbGljZSgwLHQuYmFzZS5sZW5ndGgtMSksdT17aXNBY3RpdmU6ZnVuY3Rpb24oKXtyZXR1cm4gaX0sYWN0aXZhdGU6ZnVuY3Rpb24oKXtpfHwoaT0hMCxxJiYod2luZG93LmFkZEV2ZW50TGlzdGVuZXIoYHNpbmdsZS1zcGE6YmVmb3JlLXJvdXRpbmctZXZlbnRgLGYpLHdpbmRvdy5hZGRFdmVudExpc3RlbmVyKGBzaW5nbGUtc3BhOmJlZm9yZS1tb3VudC1yb3V0aW5nLWV2ZW50YCxwKSx3aW5kb3cuYWRkRXZlbnRMaXN0ZW5lcihgc2luZ2xlLXNwYTpyb3V0aW5nLWV2ZW50YCxtKSxjKGQpLG8mJmgoZygpLHQucm91dGVzKSxwKCkpKX0sZGVhY3RpdmF0ZTpmdW5jdGlvbigpe2kmJihpPSExLHEmJih3aW5kb3cucmVtb3ZlRXZlbnRMaXN0ZW5lcihgc2luZ2xlLXNwYTpiZWZvcmUtcm91dGluZy1ldmVudGAsZiksd2luZG93LnJlbW92ZUV2ZW50TGlzdGVuZXIoYHNpbmdsZS1zcGE6YmVmb3JlLW1vdW50LXJvdXRpbmctZXZlbnRgLHApLHdpbmRvdy5yZW1vdmVFdmVudExpc3RlbmVyKGBzaW5nbGUtc3BhOnJvdXRpbmctZXZlbnRgLG0pLGwoZCkpKX19O3JldHVybiByJiZ1LmFjdGl2YXRlKCksdTtmdW5jdGlvbiBkKGUpe3ZhciBuPUx0KHthcHBsaWNhdGlvbk5hbWU6ZS5hcHBPclBhcmNlbE5hbWUsbG9jYXRpb246d2luZG93LmxvY2F0aW9uLHJvdXRlczp0LnJvdXRlc30pO2lmKG4mJm4uZXJyb3Ipe3ZhciByPWRvY3VtZW50LmdldEVsZW1lbnRCeUlkKFEobi5uYW1lKSksaT10eXBlb2Ygbi5lcnJvcj09YHN0cmluZ2A/TXQobi5lcnJvcik6bi5lcnJvcjthW24ubmFtZV09ZmUoaSx7ZG9tRWxlbWVudDpyLGVycm9yOmV9KX1zZXRUaW1lb3V0KChmdW5jdGlvbigpe3Rocm93IGV9KSl9ZnVuY3Rpb24gZihlKXt2YXIgbj1lLmRldGFpbCxyPW4uY2FuY2VsTmF2aWdhdGlvbixpPW4ubmV3VXJsLG89QnQodCxWdChpKSkscz1mdW5jdGlvbihlKXt2YXIgbj10LnJlZGlyZWN0c1tlXTtpZihlPT09byl7aWYoIXIpdGhyb3cgRXJyb3IoYHNpbmdsZS1zcGEtbGF5b3V0OiA8cmVkaXJlY3Q+IHJlcXVpcmVzIHNpbmdsZS1zcGFAPj02LjAuMGApO3JldHVybiByKCksc2V0VGltZW91dCgoZnVuY3Rpb24oKXt4ZShuKX0pKSx7djp2b2lkIDB9fX07Zm9yKHZhciBjIGluIHQucmVkaXJlY3RzKXt2YXIgbD1zKGMpO2lmKEsobCk9PT1gb2JqZWN0YClyZXR1cm4gbC52fXZhciB1PVtdO0h0KGkpLmZvckVhY2goKGZ1bmN0aW9uKGUpe2FbZV0mJih1LnB1c2goYVtlXS51bm1vdW50KCkpLGRlbGV0ZSBhW2VdKX0pKSx1Lmxlbmd0aD4wJiZyKFByb21pc2UuYWxsKHUpLnRoZW4oKGZ1bmN0aW9uKCl7cmV0dXJuITF9KSkpfWZ1bmN0aW9uIHAoKXtpZihCdCh0KS5pbmRleE9mKHMpPT09MCl7dmFyIGU9UmUoKS5yZWR1Y2UoKGZ1bmN0aW9uKGUsdCl7cmV0dXJuIGVbdF09ZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoUSh0KSksZX0pLHt9KTtJdCh7bG9jYXRpb246d2luZG93LmxvY2F0aW9uLHJvdXRlczp0LnJvdXRlcyxwYXJlbnRDb250YWluZXI6ZygpLHNob3VsZE1vdW50OiEwLGFwcGxpY2F0aW9uQ29udGFpbmVyczplfSl9fWZ1bmN0aW9uIG0oZSl7dmFyIHQ9ZS5kZXRhaWwsbj10Lm5hdmlnYXRpb25Jc0NhbmNlbGVkLHI9dC5uZXdVcmw7bnx8SHQocikuZm9yRWFjaCgoZnVuY3Rpb24oZSl7dmFyIHQ9ZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoUShlKSk7dCYmdC5pc0Nvbm5lY3RlZCYmdC5wYXJlbnROb2RlLnJlbW92ZUNoaWxkKHQpfSkpfWZ1bmN0aW9uIGgoZSx0KXtpZihlJiZlLmNoaWxkTm9kZXMmJnQpZm9yKHZhciBuPXtuZXh0U2libGluZzplLmNoaWxkTm9kZXNbMF19LHI9MDtyPHQubGVuZ3RoO3IrKyl7dmFyIGk9dFtyXTtpZihpLnR5cGUhPT1gcm91dGVgKXtmb3IodmFyIGE9bj8ubmV4dFNpYmxpbmc7KG89YSk/Lm5vZGVUeXBlPT09Tm9kZS5URVhUX05PREUmJmEudGV4dENvbnRlbnQudHJpbSgpPT09YGA7KXt2YXIgbzthPWEubmV4dFNpYmxpbmd9bj1hLFB0KGkpJiZGdChhLGkpJiYoaS5jb25uZWN0ZWROb2RlPWEpLGkucm91dGVzJiZoKGEsaS5yb3V0ZXMpfWVsc2UgaChlLGkucm91dGVzKX19ZnVuY3Rpb24gZygpe3JldHVybiB0eXBlb2YgdC5jb250YWluZXJFbD09YHN0cmluZ2A/ZG9jdW1lbnQucXVlcnlTZWxlY3Rvcih0LmNvbnRhaW5lckVsKTp0LmNvbnRhaW5lckVsfX1mdW5jdGlvbiBQdChlKXtyZXR1cm4gdD1bYGFwcGxpY2F0aW9uYCxgcm91dGVgLGBmcmFnbWVudGAsYGFzc2V0c2AsYHJlZGlyZWN0YF0sbj1lLnR5cGUsIXQuc29tZSgoZnVuY3Rpb24oZSl7cmV0dXJuIGU9PT1ufSkpO3ZhciB0LG59ZnVuY3Rpb24gRnQoZSx0KXt2YXIgbixyO3JldHVybiEhZSYmKHI9dCBpbnN0YW5jZW9mIE5vZGU/dDpmdW5jdGlvbihlKXtzd2l0Y2goZS50eXBlKXtjYXNlYCN0ZXh0YDpyZXR1cm4gZG9jdW1lbnQuY3JlYXRlVGV4dE5vZGUoZS52YWx1ZSk7Y2FzZWAjY29tbWVudGA6cmV0dXJuIGRvY3VtZW50LmNyZWF0ZUNvbW1lbnQoZS52YWx1ZSk7ZGVmYXVsdDp2YXIgdD1kb2N1bWVudC5jcmVhdGVFbGVtZW50KGUudHlwZSk7cmV0dXJuIGUuYXR0cnMuZm9yRWFjaCgoZnVuY3Rpb24oZSl7dC5zZXRBdHRyaWJ1dGUoZS5uYW1lLGUudmFsdWUpfSkpLHR9fSh0KSwobj1lKS5ub2RlVHlwZT09PXIubm9kZVR5cGUmJm4ubm9kZU5hbWU9PT1yLm5vZGVOYW1lJiZmdW5jdGlvbihlLHQpe3ZhciBuPWUuZ2V0QXR0cmlidXRlTmFtZXM/ZS5nZXRBdHRyaWJ1dGVOYW1lcygpLnNvcnQoKTpbXSxyPWUuZ2V0QXR0cmlidXRlTmFtZXM/ZS5nZXRBdHRyaWJ1dGVOYW1lcygpLnNvcnQoKTpbXTtyZXR1cm4gbi5sZW5ndGg9PT1yLmxlbmd0aCYmIW4uc29tZSgoZnVuY3Rpb24obil7cmV0dXJuIGUuZ2V0QXR0cmlidXRlKG4pIT09dC5nZXRBdHRyaWJ1dGUobil9KSl9KG4scikpfWZ1bmN0aW9uIEl0KGUpe3ZhciB0PWUubG9jYXRpb24sbj1lLnJvdXRlcyxyPWUucGFyZW50Q29udGFpbmVyLGk9ZS5wcmV2aW91c1NpYmxpbmcsYT1lLnNob3VsZE1vdW50LG89ZS5hcHBsaWNhdGlvbkNvbnRhaW5lcnM7cmV0dXJuIG4uZm9yRWFjaCgoZnVuY3Rpb24oZSxuKXtpZihlLnR5cGU9PT1gYXBwbGljYXRpb25gKXtpZihhKXt2YXIgcyxjPVEoZS5uYW1lKTtvW2UubmFtZV0/cz1vW2UubmFtZV06ZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoYyk/cz1kb2N1bWVudC5nZXRFbGVtZW50QnlJZChjKToocz1kb2N1bWVudC5jcmVhdGVFbGVtZW50KGBkaXZgKSkuaWQ9Yyx0eXBlb2YgZS5jbGFzc05hbWU9PWBzdHJpbmdgP3MuY2xhc3NOYW1lPWUuY2xhc3NOYW1lOnR5cGVvZiBlLmNsYXNzTmFtZSE9YHN0cmluZ2AmJnR5cGVvZiBzLmNsYXNzTmFtZT09YHN0cmluZ2AmJnMucmVtb3ZlQXR0cmlidXRlKGBjbGFzc2ApLFJ0KHMscixpKSxpPXN9fWVsc2UgZS50eXBlPT09YHJvdXRlYD9pPUl0KHtsb2NhdGlvbjp0LHJvdXRlczplLnJvdXRlcyxwYXJlbnRDb250YWluZXI6cixwcmV2aW91c1NpYmxpbmc6aSxzaG91bGRNb3VudDphJiZlLmFjdGl2ZVdoZW4odCksYXBwbGljYXRpb25Db250YWluZXJzOm99KTooZSBpbnN0YW5jZW9mIE5vZGV8fHR5cGVvZiBlLnR5cGU9PWBzdHJpbmdgKSYmKGE/KGUuY29ubmVjdGVkTm9kZXx8PWUgaW5zdGFuY2VvZiBOb2RlP2UuY2xvbmVOb2RlKCExKTp6dChlKSxSdChlLmNvbm5lY3RlZE5vZGUscixpKSxlLnJvdXRlcyYmSXQoe2xvY2F0aW9uOnQscm91dGVzOmUucm91dGVzLHBhcmVudENvbnRhaW5lcjplLmNvbm5lY3RlZE5vZGUscHJldmlvdXNTaWJsaW5nOm51bGwsc2hvdWxkTW91bnQ6YSxhcHBsaWNhdGlvbkNvbnRhaW5lcnM6b30pLGk9ZS5jb25uZWN0ZWROb2RlKTooKGw9ZS5jb25uZWN0ZWROb2RlKSYmKGwucmVtb3ZlP2wucmVtb3ZlKCk6bC5wYXJlbnROb2RlLnJlbW92ZUNoaWxkKGwpKSxkZWxldGUgZS5jb25uZWN0ZWROb2RlKSk7dmFyIGx9KSksaX1mdW5jdGlvbiBMdChlKXtmb3IodmFyIHQ9ZS5hcHBsaWNhdGlvbk5hbWUsbj1lLmxvY2F0aW9uLHI9ZS5yb3V0ZXMsaT0wO2k8ci5sZW5ndGg7aSsrKXt2YXIgYT1yW2ldO2lmKGEudHlwZT09PWBhcHBsaWNhdGlvbmApe2lmKGEubmFtZT09PXQpcmV0dXJuIGF9ZWxzZSBpZihhLnR5cGU9PT1gcm91dGVgKXtpZihhLmFjdGl2ZVdoZW4obikpe3ZhciBvPUx0KHthcHBsaWNhdGlvbk5hbWU6dCxsb2NhdGlvbjpuLHJvdXRlczphLnJvdXRlc30pO2lmKG8pcmV0dXJuIG99fWVsc2UgaWYoYS5yb3V0ZXMpe3ZhciBzPUx0KHthcHBsaWNhdGlvbk5hbWU6dCxsb2NhdGlvbjpuLHJvdXRlczphLnJvdXRlc30pO2lmKHMpcmV0dXJuIHN9fX1mdW5jdGlvbiBSdChlLHQsbil7dmFyIHI9bj9uLm5leHRTaWJsaW5nOnQuZmlyc3RDaGlsZDtyIT09ZSYmdC5pbnNlcnRCZWZvcmUoZSxyKX1mdW5jdGlvbiBRKGUpe3JldHVybmBzaW5nbGUtc3BhLWFwcGxpY2F0aW9uOiR7ZX1gfWZ1bmN0aW9uIHp0KGUpe2lmKGUudHlwZS50b0xvd2VyQ2FzZSgpPT09YCN0ZXh0YClyZXR1cm4gZG9jdW1lbnQuY3JlYXRlVGV4dE5vZGUoZS52YWx1ZSk7aWYoZS50eXBlLnRvTG93ZXJDYXNlKCk9PT1gI2NvbW1lbnRgKXJldHVybiBkb2N1bWVudC5jcmVhdGVDb21tZW50KGUudmFsdWUpO3ZhciB0PWRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoZS50eXBlKTtyZXR1cm4oZS5hdHRyc3x8W10pLmZvckVhY2goKGZ1bmN0aW9uKGUpe3Quc2V0QXR0cmlidXRlKGUubmFtZSxlLnZhbHVlKX0pKSx0LnJvdXRlcyYmdC5yb3V0ZXMuZm9yRWFjaCgoZnVuY3Rpb24oZSl7dC5hcHBlbmRDaGlsZCh6dChlKSl9KSksdH1mdW5jdGlvbiBCdChlKXtyZXR1cm4oYXJndW1lbnRzLmxlbmd0aD4xJiZhcmd1bWVudHNbMV0hPT12b2lkIDA/YXJndW1lbnRzWzFdOmxvY2F0aW9uKVtlLm1vZGU9PT1gaGFzaGA/YGhhc2hgOmBwYXRobmFtZWBdfWZ1bmN0aW9uIFZ0KGUpe3RyeXtyZXR1cm4gbmV3IFVSTChlKX1jYXRjaHt2YXIgdD1kb2N1bWVudC5jcmVhdGVFbGVtZW50KGBhYCk7cmV0dXJuIHQuaHJlZj1lLHR9fWZ1bmN0aW9uIEh0KGUpe3ZhciB0PVtdLG49SGUoZT9WdChlKTpsb2NhdGlvbik7cmV0dXJuIFIoKS5mb3JFYWNoKChmdW5jdGlvbihlKXtuLmluZGV4T2YoZSk8MCYmdC5wdXNoKGUpfSkpLHR9ZnVuY3Rpb24gVXQoZSl7dmFyIHQ9ZS5yb3V0ZXMsbj1lLmxvYWRBcHAscj17fTtyZXR1cm4gV3QocixLdCx7fSx0LnJvdXRlcyksT2JqZWN0LmtleXMocikubWFwKChmdW5jdGlvbihlKXt2YXIgdD1yW2VdO3JldHVybntuYW1lOmUsY3VzdG9tUHJvcHM6ZnVuY3Rpb24oZSxuKXt2YXIgcj1EdCh0LChmdW5jdGlvbihlKXtyZXR1cm4gZS5hY3RpdmVXaGVuKG4pfSkpO3JldHVybiByP3IucHJvcHM6e319LGFjdGl2ZVdoZW46dC5tYXAoKGZ1bmN0aW9uKGUpe3JldHVybiBlLmFjdGl2ZVdoZW59KSksYXBwOmZ1bmN0aW9uKCl7dmFyIHI7cSYmKHI9RHQodCwoZnVuY3Rpb24oZSl7cmV0dXJuIGUuYWN0aXZlV2hlbih3aW5kb3cubG9jYXRpb24pfSkpKTt2YXIgaT1uKHtuYW1lOmV9KTtyZXR1cm4gciYmci5sb2FkZXI/ZnVuY3Rpb24oZSx0LG4pe3JldHVybiBQcm9taXNlLnJlc29sdmUoKS50aGVuKChmdW5jdGlvbigpe3ZhciByLGk9UShlKSxhPWRvY3VtZW50LmdldEVsZW1lbnRCeUlkKGkpO2F8fCgoYT1kb2N1bWVudC5jcmVhdGVFbGVtZW50KGBkaXZgKSkuaWQ9aSxhLnN0eWxlLmRpc3BsYXk9YG5vbmVgLGRvY3VtZW50LmJvZHkuYXBwZW5kQ2hpbGQoYSkscj1mdW5jdGlvbigpe2Euc3R5bGUucmVtb3ZlUHJvcGVydHkoYGRpc3BsYXlgKSxhLmdldEF0dHJpYnV0ZShgc3R5bGVgKT09PWBgJiZhLnJlbW92ZUF0dHJpYnV0ZShgc3R5bGVgKSx3aW5kb3cucmVtb3ZlRXZlbnRMaXN0ZW5lcihgc2luZ2xlLXNwYTpiZWZvcmUtbW91bnQtcm91dGluZy1ldmVudGAscil9LHdpbmRvdy5hZGRFdmVudExpc3RlbmVyKGBzaW5nbGUtc3BhOmJlZm9yZS1tb3VudC1yb3V0aW5nLWV2ZW50YCxyKSk7dmFyIG89ZmUodHlwZW9mIHQubG9hZGVyPT1gc3RyaW5nYD9NdCh0LmxvYWRlcik6dC5sb2FkZXIse25hbWU6YGFwcGxpY2F0aW9uLWxvYWRlcjoke2V9YCxkb21FbGVtZW50OmF9KTtmdW5jdGlvbiBzKCl7cmV0dXJuIG8udW5tb3VudCgpLnRoZW4oKGZ1bmN0aW9uKCl7ciYmcigpfSkpfXJldHVybiBQcm9taXNlLmFsbChbby5tb3VudFByb21pc2Usbl0pLnRoZW4oKGZ1bmN0aW9uKGUpe3ZhciB0LG4scj0obj0yLGZ1bmN0aW9uKGUpe2lmKEFycmF5LmlzQXJyYXkoZSkpcmV0dXJuIGV9KHQ9ZSl8fGZ1bmN0aW9uKGUsdCl7dmFyIG49ZT09bnVsbD9udWxsOnR5cGVvZiBTeW1ib2w8YHVgJiZlW1N5bWJvbC5pdGVyYXRvcl18fGVbYEBAaXRlcmF0b3JgXTtpZihuIT1udWxsKXt2YXIgcixpLGE9W10sbz0hMCxzPSExO3RyeXtmb3Iobj1uLmNhbGwoZSk7IShvPShyPW4ubmV4dCgpKS5kb25lKSYmKGEucHVzaChyLnZhbHVlKSwhdHx8YS5sZW5ndGghPT10KTtvPSEwKTt9Y2F0Y2goZSl7cz0hMCxpPWV9ZmluYWxseXt0cnl7b3x8bi5yZXR1cm49PW51bGx8fG4ucmV0dXJuKCl9ZmluYWxseXtpZihzKXRocm93IGl9fXJldHVybiBhfX0odCxuKXx8YnQodCxuKXx8ZnVuY3Rpb24oKXt0aHJvdyBUeXBlRXJyb3IoYEludmFsaWQgYXR0ZW1wdCB0byBkZXN0cnVjdHVyZSBub24taXRlcmFibGUgaW5zdGFuY2UuCkluIG9yZGVyIHRvIGJlIGl0ZXJhYmxlLCBub24tYXJyYXkgb2JqZWN0cyBtdXN0IGhhdmUgYSBbU3ltYm9sLml0ZXJhdG9yXSgpIG1ldGhvZC5gKX0oKSk7clswXTt2YXIgaT1yWzFdO3JldHVybiBzKCkudGhlbigoZnVuY3Rpb24oKXtyZXR1cm4gaX0pKX0pLChmdW5jdGlvbihlKXtyZXR1cm4gcygpLnRoZW4oKGZ1bmN0aW9uKCl7dGhyb3cgZX0pKX0pKX0pKX0oZSxyLGkpOml9fX0pKX1mdW5jdGlvbiBXdChlLHQsbixyKXtyLmZvckVhY2goKGZ1bmN0aW9uKHIpe3IudHlwZT09PWBhcHBsaWNhdGlvbmA/KGVbci5uYW1lXXx8KGVbci5uYW1lXT1bXSksZVtyLm5hbWVdLnB1c2goe2FjdGl2ZVdoZW46dCxwcm9wczpHdChuLHIucHJvcHMpLGxvYWRlcjpyLmxvYWRlcn0pKTpyLnR5cGU9PT1gcm91dGVgP1d0KGUsci5hY3RpdmVXaGVuLEd0KG4sci5wcm9wcyksci5yb3V0ZXMpOnIucm91dGVzJiZXdChlLHQsbixyLnJvdXRlcyl9KSl9ZnVuY3Rpb24gR3QoZSl7dmFyIHQ9YXJndW1lbnRzLmxlbmd0aD4xJiZhcmd1bWVudHNbMV0hPT12b2lkIDA/YXJndW1lbnRzWzFdOnt9O3JldHVybiBfdChfdCh7fSxlKSx0KX1mdW5jdGlvbiBLdCgpe3JldHVybiEwfXZhciBxdD1gPHNpbmdsZS1zcGEtcm91dGVyPgogIDxtYWluIGNsYXNzPSJzaW5nbGUtc3BhLW1haW4iPgogICAgPCEtLSDQk9C70L7QsdCw0LvRjNC90YvQtSDQv9GA0LjQu9C+0LbQtdC90LjRjyAtINC30LDQs9GA0YPQttCw0Y7RgtGB0Y8g0YHRgNCw0LfRgyAtLT4KICAgIDxhcHBsaWNhdGlvbiBuYW1lPSJAbnViZXMvbmF2aWdhdGlvbiI+PC9hcHBsaWNhdGlvbj4KICAgIDxhcHBsaWNhdGlvbiBuYW1lPSJAbnViZXMvc2lkZWJhciI+PC9hcHBsaWNhdGlvbj4KCiAgICA8IS0tINCf0YDQuNC70L7QttC10L3QuNGPINC/0L4g0LzQsNGA0YjRgNGD0YLQsNC8IC0g0LfQsNCz0YDRg9C20LDRjtGC0YHRjyDQu9C10L3QuNCy0L4g0L/RgNC4INC/0LXRgNC10YXQvtC00LUgLS0+CiAgICA8cm91dGUgcGF0aD0iYXV0aG9yaXphdGlvbiI+CiAgICAgIDxhcHBsaWNhdGlvbiBuYW1lPSJAbnViZXMvYXV0aG9yaXphdGlvbiIgZXJyb3I9ImNvbW1vbkVycm9yIiBsb2FkZXI9ImxhenlMb2FkZXIiPjwvYXBwbGljYXRpb24+CiAgICA8L3JvdXRlPgogICAgPHJvdXRlIHBhdGg9ImRhc2hib2FyZCI+CiAgICAgIDxhcHBsaWNhdGlvbiBuYW1lPSJAbnViZXMvZGFzaGJvYXJkIiBlcnJvcj0iY29tbW9uRXJyb3IiIGxvYWRlcj0ibGF6eUxvYWRlciI+PC9hcHBsaWNhdGlvbj4KICAgIDwvcm91dGU+CiAgICA8cm91dGUgcGF0aD0ic2VydmljZS1kZXNrIj4KICAgICAgPGFwcGxpY2F0aW9uIG5hbWU9IkBudWJlcy9zZXJ2aWNlLWRlc2siIGVycm9yPSJjb21tb25FcnJvciIgbG9hZGVyPSJsYXp5TG9hZGVyIj48L2FwcGxpY2F0aW9uPgogICAgPC9yb3V0ZT4KICAgIDxyb3V0ZSBwYXRoPSJzZXJ2aWNlcyI+CiAgICAgIDxhcHBsaWNhdGlvbiBuYW1lPSJAbnViZXMvc2VydmljZXMiIGVycm9yPSJjb21tb25FcnJvciIgbG9hZGVyPSJsYXp5TG9hZGVyIj48L2FwcGxpY2F0aW9uPgogICAgPC9yb3V0ZT4KICAgIDxyb3V0ZSBwYXRoPSJiMmIiPgogICAgICA8YXBwbGljYXRpb24gbmFtZT0iQG51YmVzL2IyYiIgZXJyb3I9ImNvbW1vbkVycm9yIiBsb2FkZXI9ImxhenlMb2FkZXIiPjwvYXBwbGljYXRpb24+CiAgICA8L3JvdXRlPgogICAgPHJvdXRlIHBhdGg9Im1vbml0b3JpbmctZ3JhZmFuYSI+CiAgICAgIDxhcHBsaWNhdGlvbiBuYW1lPSJAbnViZXMvbW9uaXRvcmluZy1ncmFmYW5hIiBlcnJvcj0iY29tbW9uRXJyb3IiIGxvYWRlcj0ibGF6eUxvYWRlciI+PC9hcHBsaWNhdGlvbj4KICAgIDwvcm91dGU+CiAgICA8cm91dGUgcGF0aD0iZmVlZGJhY2stcG9ydGFsIj4KICAgICAgPGFwcGxpY2F0aW9uIG5hbWU9IkBudWJlcy9mZWVkYmFjay1wb3J0YWwiIGVycm9yPSJjb21tb25FcnJvciIgbG9hZGVyPSJsYXp5TG9hZGVyIj48L2FwcGxpY2F0aW9uPgogICAgPC9yb3V0ZT4KCiAgICA8IS0tINCk0L7QvdC+0LLRi9C1INGB0LXRgNCy0LjRgdGLIC0g0LfQsNCz0YDRg9C20LDRjtGC0YHRjyDRgdGA0LDQt9GDIC0tPgogICAgPGFwcGxpY2F0aW9uIG5hbWU9IkBudWJlcy9ub3RpZmljYXRpb24tYXBpIj48L2FwcGxpY2F0aW9uPgogICAgPGFwcGxpY2F0aW9uIG5hbWU9IkBudWJlcy9zaGFyZWQtd29ya2VyIj48L2FwcGxpY2F0aW9uPgoKICAgIDxyb3V0ZSBwYXRoPSIvIiBleGFjdCAvPgogICAgPHJlZGlyZWN0IGZyb209Ii8iIHRvPSJkYXNoYm9hcmQiIC8+CgogICAgPHJvdXRlIGRlZmF1bHQ+CiAgICAgIDxzZWN0aW9uIGNsYXNzPSJweS01IGQtZmxleCBqdXN0aWZ5LWNvbnRlbnQtY2VudGVyIGFsaWduLWl0ZW1zLWNlbnRlciI+CiAgICAgICAgPGRpdiBjbGFzcz0iY29sLW1kLTEyIHRleHQtY2VudGVyIj4KICAgICAgICAgIDxoMT40MDQ8L2gxPgogICAgICAgICAgPGgyPlBhZ2UgTm90IEZvdW5kPC9oMj4KICAgICAgICAgIDxwPlNvcnJ5LCB0aGUgcGFnZSB5b3UgYXJlIGxvb2tpbmcgZm9yIGRvZXMgbm90IGV4aXN0LjwvcD4KICAgICAgICAgIDxhIGhyZWY9Ii9kYXNoYm9hcmQiIGNsYXNzPSJ0ZXh0LXdoaXRlIHRleHQtZGVjb3JhdGlvbi1ub25lIHB4LTQgcHktMyBiZy1zdWNjZXNzIGQtaW5saW5lLWJsb2NrIG10LTIgcm91bmRlZCI+CiAgICAgICAgICAgINCd0LAg0LPQu9Cw0LLQvdGD0Y4KICAgICAgICAgIDwvYT4KICAgICAgICA8L2Rpdj4KICAgICAgPC9zZWN0aW9uPgogICAgPC9yb3V0ZT4KICA8L21haW4+Cjwvc2luZ2xlLXNwYS1yb3V0ZXI+CmAsSnQ9ZSgpLFl0PSgpPT5sb2NhbFN0b3JhZ2UuZ2V0SXRlbShgcm9vdC1hcHAtZGVidWdgKT09PWAxYCwkPSguLi5lKT0+e1l0KCkmJmNvbnNvbGUuaW5mbyguLi5lKX07KCgpPT57JChgW3Jvb3QtYXBwXSBzdGFydFJvb3RBcHAgYmVnaW5gKTtsZXQgZTt0cnl7ZT1rdChxdCx7bG9hZGVyczp7bGF6eUxvYWRlcjpgPGRpdi8+YH0scHJvcHM6e30sZXJyb3JzOntjb21tb25FcnJvcjpgPGgyIGNsYXNzPSdwdC0zIHBzLTMgdGV4dC1kYW5nZXInPtCf0YDQuNC70L7QttC10L3QuNC1INC90LXQtNC+0YHRgtGD0L/QvdC+INCyINC00LDQvdC90YvQuSDQvNC+0LzQtdC90YIhPC9oMj5gfX0pLCQoYFtyb290LWFwcF0gY29uc3RydWN0Um91dGVzIE9LYCl9Y2F0Y2goZSl7dGhyb3cgY29uc29sZS5lcnJvcihgW3Jvb3QtYXBwXSBjb25zdHJ1Y3RSb3V0ZXMgRkFJTEVEOmAsZSksZX1sZXQgdDt0cnl7dD1VdCh7cm91dGVzOmUsbG9hZEFwcCh7bmFtZTplfSl7cmV0dXJuIGF0KCgpPT5pbXBvcnQoc3QoZSkpLFtdKX19KSwkKGBbcm9vdC1hcHBdIGNvbnN0cnVjdEFwcGxpY2F0aW9ucyBPSywgY291bnQ6YCx0Lmxlbmd0aCl9Y2F0Y2goZSl7dGhyb3cgY29uc29sZS5lcnJvcihgW3Jvb3QtYXBwXSBjb25zdHJ1Y3RBcHBsaWNhdGlvbnMgRkFJTEVEOmAsZSksZX10LmZvckVhY2goZT0+e2UuY3VzdG9tUHJvcHM9ey4uLmUuY3VzdG9tUHJvcHMsZXZlbnRCdXM6SnR9fSksJChgW3Jvb3QtYXBwXSBjdXN0b21Qcm9wcy5ldmVudEJ1cyBzZXQgZm9yIGFsbCBhcHBzYCk7bGV0IHI7dHJ5e3I9TnQoe3JvdXRlczplLGFwcGxpY2F0aW9uczp0fSksJChgW3Jvb3QtYXBwXSBjb25zdHJ1Y3RMYXlvdXRFbmdpbmUgT0tgKX1jYXRjaChlKXt0aHJvdyBjb25zb2xlLmVycm9yKGBbcm9vdC1hcHBdIGNvbnN0cnVjdExheW91dEVuZ2luZSBGQUlMRUQ6YCxlKSxlfXRyeXt0LmZvckVhY2goVmUpLCQoYFtyb290LWFwcF0gcmVnaXN0ZXJBcHBsaWNhdGlvbiBPS2ApfWNhdGNoKGUpe3Rocm93IGNvbnNvbGUuZXJyb3IoYFtyb290LWFwcF0gcmVnaXN0ZXJBcHBsaWNhdGlvbiBGQUlMRUQ6YCxlKSxlfXIuYWN0aXZhdGUoKSwkKGBbcm9vdC1hcHBdIGxheW91dEVuZ2luZS5hY3RpdmF0ZSBPS2ApLFFlKHt1cmxSZXJvdXRlT25seTohMH0pLCQoYFtyb290LWFwcF0gc2luZ2xlLXNwYSBzdGFydCBPS2ApLG4oKSwkKGBbcm9vdC1hcHBdIHN0YXJ0VmVyc2lvblVwZGF0ZXIgT0tgKSxodCgpLmNhdGNoKGU9Pntjb25zb2xlLmVycm9yKGBbcm9vdC1hcHBdIHN0YXJ0SW1wb3J0bWFwVXBkYXRlciBmYWlsZWQ6YCxlKX0pLCQoYFtyb290LWFwcF0gc3RhcnRJbXBvcnRtYXBVcGRhdGVyIGxhdW5jaGVkIChhc3luYylgKX0pKCk7", - "encoding": "base64" - }, - "redirectURL": "", - "headersSize": -1, - "bodySize": 0, - "_transferSize": 0, - "_error": null, - "_fetchedViaServiceWorker": true, - "_serviceWorkerResponseSource": "http-cache" - }, - "serverIPAddress": "", - "startedDateTime": "2026-05-30T04:35:25.040Z", - "time": 39.64799997629598, - "timings": { - "blocked": 20.400000008739532, - "dns": -1, - "ssl": -1, - "connect": -1, - "send": 0, - "wait": 15.388999978095294, - "receive": 3.8589999894611537, - "_blocked_queueing": 5.184000008739531, - "_workerStart": 0.002, - "_workerReady": 15.216, - "_workerFetchStart": 15.216, - "_workerRespondWithSettled": 30.441 - }, - "_fromCache": "disk", - "pageref": "page_3" - }, - { - "_initiator": { - "type": "parser", - "url": "https://deck-test.ngcloud.ru/authorization/callback?back=%2Fdashboard%2Fauthorization%2Fdashboard&code=604acf612225306a7184e63fa41e0615852ec2a05ce95a2c6cb1527a756a5508", - "lineNumber": 14 - }, - "_priority": "VeryHigh", - "_resourceType": "stylesheet", - "cache": {}, - "request": { - "method": "GET", - "url": "https://deck-test.ngcloud.ru/assets/index-VIpGGraV.css", - "httpVersion": "http/1.1", - "headers": [ - { - "name": "Origin", - "value": "https://deck-test.ngcloud.ru" - }, - { - "name": "sec-ch-ua-platform", - "value": "\"Windows\"" - }, - { - "name": "Referer", - "value": "https://deck-test.ngcloud.ru/" - }, - { - "name": "User-Agent", - "value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36" - }, - { - "name": "sec-ch-ua", - "value": "\"Chromium\";v=\"148\", \"Google Chrome\";v=\"148\", \"Not/A)Brand\";v=\"99\"" - }, - { - "name": "DNT", - "value": "1" - }, - { - "name": "sec-ch-ua-mobile", - "value": "?0" - } - ], - "queryString": [], - "cookies": [], - "headersSize": -1, - "bodySize": 0 - }, - "response": { - "status": 200, - "statusText": "", - "httpVersion": "http/1.1", - "headers": [ - { - "name": "Content-Type", - "value": "text/css" - } - ], - "cookies": [], - "content": { - "size": 1970, - "mimeType": "text/css", - "text": "@font-face{font-family:Ubuntu;font-weight:300;font-style:normal;src:url(/assets/Ubuntu-Light-aUGsJZOE.woff2)format(\"woff2\")}@font-face{font-family:Ubuntu;font-weight:300;font-style:italic;src:url(/assets/Ubuntu-LightItalic-BfM2gN_f.woff2)format(\"woff2\")}@font-face{font-family:Ubuntu;font-weight:400;font-style:normal;src:url(/assets/Ubuntu-Regular-QO2cqVZ2.woff2)format(\"woff2\")}@font-face{font-family:Ubuntu;font-weight:400;font-style:italic;src:url(/assets/Ubuntu-Italic-B6wntuzF.woff2)format(\"woff2\")}@font-face{font-family:Ubuntu;font-weight:500;font-style:normal;src:url(/assets/Ubuntu-Medium-DnOUJ_CQ.woff2)format(\"woff2\")}@font-face{font-family:Ubuntu;font-weight:500;font-style:italic;src:url(/assets/Ubuntu-MediumItalic-CELvDnK9.woff2)format(\"woff2\")}@font-face{font-family:Ubuntu;font-weight:700;font-style:normal;src:url(/assets/Ubuntu-Bold-CxbGmmhf.woff2)format(\"woff2\")}@font-face{font-family:Ubuntu;font-weight:700;font-style:italic;src:url(/assets/Ubuntu-BoldItalic-DS3aS511.woff2)format(\"woff2\")}*,:before,:after{box-sizing:border-box}:root{--page-height:auto;--max-content-height:auto}body{margin:0;font-family:Ubuntu,sans-serif;font-size:13px}.single-spa-main{grid-template-columns:min-content 1fr;grid-template-rows:min-content 0 minmax(0, var(--max-content-height));min-height:100vh;height:var(--page-height);display:grid}.single-spa-main input,.single-spa-main select,.single-spa-main textarea,.single-spa-main [data-slot=form-control]{scroll-margin-top:80px}.single-spa-main>div{grid-template-rows:subgrid;width:100%;display:grid;position:relative;overflow:hidden}.single-spa-main>div:first-of-type{grid-area:1/1/auto/-1}.single-spa-main>div:nth-of-type(2){grid-area:3/1;transform:translate(0)}.single-spa-main>div:nth-of-type(3){grid-area:3/2}.single-spa-main>div[id*=notification-api],.single-spa-main>div[id*=shared-worker],.single-spa-main>div[id*=common-css]{display:none}html,body,.single-spa-main,[class*=overflow]{scroll-padding-top:80px}\n" - }, - "redirectURL": "", - "headersSize": -1, - "bodySize": -1, - "_transferSize": 0, - "_error": null, - "_fetchedViaServiceWorker": true, - "_serviceWorkerResponseSource": "fallback-code" - }, - "serverIPAddress": "", - "startedDateTime": "2026-05-30T04:35:25.042Z", - "time": 40.17799999564886, - "timings": { - "blocked": 19.562000000763685, - "dns": -1, - "ssl": -1, - "connect": -1, - "send": 0, - "wait": 20.281000005166977, - "receive": 0.3349999897181988, - "_blocked_queueing": 3.5700000007636845, - "_workerStart": 0.002, - "_workerReady": 15.992, - "_workerFetchStart": 15.992, - "_workerRespondWithSettled": 36.141 - }, - "pageref": "page_3" - }, - { - "_initiator": { - "type": "parser", - "url": "https://deck-test.ngcloud.ru/authorization/callback?back=%2Fdashboard%2Fauthorization%2Fdashboard&code=604acf612225306a7184e63fa41e0615852ec2a05ce95a2c6cb1527a756a5508", - "lineNumber": 22 - }, - "_priority": "High", - "_resourceType": "script", - "cache": {}, - "connection": "443", - "request": { - "method": "GET", - "url": "https://deck-test.ngcloud.ru/libs/import-map-overrides.js", - "httpVersion": "http/2.0", - "headers": [ - { - "name": ":authority", - "value": "deck-test.ngcloud.ru" - }, - { - "name": ":method", - "value": "GET" - }, - { - "name": ":path", - "value": "/libs/import-map-overrides.js" - }, - { - "name": ":scheme", - "value": "https" - }, - { - "name": "accept", - "value": "*/*" - }, - { - "name": "accept-encoding", - "value": "gzip, deflate, br, zstd" - }, - { - "name": "accept-language", - "value": "en-US,en;q=0.9,ru;q=0.8,it;q=0.7" - }, - { - "name": "cache-control", - "value": "no-cache" - }, - { - "name": "dnt", - "value": "1" - }, - { - "name": "pragma", - "value": "no-cache" - }, - { - "name": "priority", - "value": "u=1" - }, - { - "name": "referer", - "value": "https://deck-test.ngcloud.ru/" - }, - { - "name": "sec-ch-ua", - "value": "\"Chromium\";v=\"148\", \"Google Chrome\";v=\"148\", \"Not/A)Brand\";v=\"99\"" - }, - { - "name": "sec-ch-ua-mobile", - "value": "?0" - }, - { - "name": "sec-ch-ua-platform", - "value": "\"Windows\"" - }, - { - "name": "sec-fetch-dest", - "value": "script" - }, - { - "name": "sec-fetch-mode", - "value": "no-cors" - }, - { - "name": "sec-fetch-site", - "value": "same-origin" - }, - { - "name": "user-agent", - "value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36" - } - ], - "queryString": [], - "cookies": [], - "headersSize": -1, - "bodySize": 0 - }, - "response": { - "status": 200, - "statusText": "", - "httpVersion": "http/2.0", - "headers": [ - { - "name": "access-control-allow-headers", - "value": "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization" - }, - { - "name": "access-control-allow-methods", - "value": "GET, OPTIONS" - }, - { - "name": "access-control-allow-origin", - "value": "*" - }, - { - "name": "cache-control", - "value": "max-age=31536000" - }, - { - "name": "cache-control", - "value": "public, max-age=31536000, immutable" - }, - { - "name": "content-encoding", - "value": "br" - }, - { - "name": "content-length", - "value": "14251" - }, - { - "name": "content-security-policy", - "value": "upgrade-insecure-requests;" - }, - { - "name": "content-type", - "value": "application/javascript" - }, - { - "name": "date", - "value": "Sat, 30 May 2026 04:35:24 GMT" - }, - { - "name": "etag", - "value": "\"6a07292d-37ab\"" - }, - { - "name": "expires", - "value": "Sun, 30 May 2027 04:35:24 GMT" - }, - { - "name": "last-modified", - "value": "Fri, 15 May 2026 14:09:49 GMT" - }, - { - "name": "server", - "value": "ddos-guard" - }, - { - "name": "strict-transport-security", - "value": "max-age=31536000; includeSubDomains" - }, - { - "name": "vary", - "value": "Accept-Encoding" - }, - { - "name": "x-content-type-options", - "value": "nosniff" - } - ], - "cookies": [], - "content": { - "size": 54245, - "mimeType": "application/javascript", - "text": "/* import-map-overrides@6.1.0 */\n!function(){\"use strict\";function e(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=Array(t);n=0;--i){var a=this.tryEntries[i],l=a.completion;if(\"root\"===a.tryLoc)return o(\"end\");if(a.tryLoc<=this.prev){var u=r.call(a,\"catchLoc\"),s=r.call(a,\"finallyLoc\");if(u&&s){if(this.prev=0;--n){var o=this.tryEntries[n];if(o.tryLoc<=this.prev&&r.call(o,\"finallyLoc\")&&this.prev=0;--t){var n=this.tryEntries[t];if(n.finallyLoc===e)return this.complete(n.completion,n.afterLoc),I(n),g}},catch:function(e){for(var t=this.tryEntries.length-1;t>=0;--t){var n=this.tryEntries[t];if(n.tryLoc===e){var r=n.completion;if(\"throw\"===r.type){var o=r.arg;I(n)}return o}}throw Error(\"illegal catch attempt\")},delegateYield:function(t,n,r){return this.delegate={iterator:C(t),resultName:n,nextLoc:r},\"next\"===this.method&&(this.arg=e),g}},t}function f(e,t){return f=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(e,t){return e.__proto__=t,e},f(e,t)}function m(e,t){return function(e){if(Array.isArray(e))return e}(e)||function(e,t){var n=null==e?null:\"undefined\"!=typeof Symbol&&e[Symbol.iterator]||e[\"@@iterator\"];if(null!=n){var r,o,i,a,l=[],u=!0,s=!1;try{if(i=(n=n.call(e)).next,0===t);else for(;!(u=(r=i.call(n)).done)&&(l.push(r.value),l.length!==t);u=!0);}catch(e){s=!0,o=e}finally{try{if(!u&&null!=n.return&&(a=n.return(),Object(a)!==a))return}finally{if(s)throw o}}return l}}(e,t)||g(e,t)||function(){throw new TypeError(\"Invalid attempt to destructure non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.\")}()}function h(e){var t=function(e){if(\"object\"!=typeof e||!e)return e;var t=e[Symbol.toPrimitive];if(void 0!==t){var n=t.call(e,\"string\");if(\"object\"!=typeof n)return n;throw new TypeError(\"@@toPrimitive must return a primitive value.\")}return String(e)}(e);return\"symbol\"==typeof t?t:t+\"\"}function v(e){return v=\"function\"==typeof Symbol&&\"symbol\"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&\"function\"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?\"symbol\":typeof e},v(e)}function g(t,n){if(t){if(\"string\"==typeof t)return e(t,n);var r={}.toString.call(t).slice(8,-1);return\"Object\"===r&&t.constructor&&(r=t.constructor.name),\"Map\"===r||\"Set\"===r?Array.from(t):\"Arguments\"===r||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r)?e(t,n):void 0}}function _(e){var t=\"function\"==typeof Map?new Map:void 0;return _=function(e){if(null===e||!function(e){try{return-1!==Function.toString.call(e).indexOf(\"[native code]\")}catch(t){return\"function\"==typeof e}}(e))return e;if(\"function\"!=typeof e)throw new TypeError(\"Super expression must either be null or a function\");if(void 0!==t){if(t.has(e))return t.get(e);t.set(e,n)}function n(){return function(e,t,n){if(s())return Reflect.construct.apply(null,arguments);var r=[null];r.push.apply(r,t);var o=new(e.bind.apply(e,r));return n&&f(o,n.prototype),o}(e,arguments,l(this).constructor)}return n.prototype=Object.create(e.prototype,{constructor:{value:n,enumerable:!1,writable:!0,configurable:!0}}),f(n,e)},_(e)}var y;function b(e,t){if(Array.isArray(e)){for(var n=0;n=0;throw Error(\"Can't call includes on \".concat(v(e)))}var w,x=\"import-map-override:\",O=\"import-map-overrides-disabled\",k=\"import-map-overrides-external-maps\",M=\"data-is-importmap-override\",E=\"import-map-overrides-domains\",N=\"allowlist:\",S=\"denylist:\",P=document.querySelector('meta[name=\"importmap-type\"]'),I=document.querySelector('meta[name=\"'.concat(E,'\"]')),U={},C=null!==(y=null==P?void 0:P.getAttribute(\"content\"))&&void 0!==y?y:\"importmap\";if(I){var L=I.getAttribute(\"content\");L||console.warn(\"Invalid \".concat(E,\" meta element - content required.\"));var j=function(e){return new RegExp(function(e){if(\"string\"!=typeof e)throw new TypeError(\"Expected a string\");return e.replace(/[|\\\\{}()[\\]^$+*?.]/g,\"\\\\$&\").replace(/-/g,\"\\\\x2d\")}(e).replace(\"\\\\*\",\".+\")).test(window.location.hostname)};0===L.indexOf(N)?w=!L.slice(10).split(\",\").some(j):0===L.indexOf(S)?w=L.slice(9).split(\",\").some(j):console.log(\"Invalid \".concat(E,\" meta content attribute - must start with \").concat(N,\" or \").concat(S))}else w=!1;function D(e,t){return Object.entries(e).reduce((function(e,n){var r=m(n,2),o=r[0],i=r[1];return e[o]=function(e,t){try{return new URL(e,t).href}catch(t){return e}}(i,t),e}),{})}(function(){try{return localStorage.getItem(\"test\"),!0}catch(e){return!1}})()||(console.warn(\"Disabling import-map-overrides, since local storage is not readable\"),w=!0),w||function(){var e,n=!!P&&P.hasAttribute(\"server-cookie\"),r=!!P&&P.hasAttribute(\"server-only\"),o=!!P&&P.hasAttribute(\"allow-query-param-override\");window.importMapOverrides={addOverride:function(e,t){/^\\d+$/g.test(t)&&(t=i.getUrlFromPort(e,t));var r=x+e;return localStorage.setItem(r,t),n&&(document.cookie=\"\".concat(r,\"=\").concat(t)),l(),i.getOverrideMap()},getOverrideMap:function(){for(var e=arguments.length>0&&void 0!==arguments[0]&&arguments[0],t={imports:{},scopes:{}},n=i.getDisabledOverrides(),r=function(r,o){!e&&n.indexOf(r)>=0||(t.imports[r]=o)},a=0;a1&&void 0!==arguments[1]?arguments[1]:window.location.href;e=e.replace(/[\\[\\]]/g,\"\\\\$&\");var n=new RegExp(\"[?&]\"+e+\"(=([^&#]*)|&|#|$)\").exec(t);return n?n[2]?decodeURIComponent(n[2].replace(/\\+/g,\" \")):\"\":null}(\"imo\",window.location!=window.parent.location?document.referrer:window.location.href);if(u){var s;try{s=JSON.parse(u)}catch(e){throw Error(\"Invalid importMap query param - text content must be json\")}Object.keys(s.imports).forEach((function(e){r(e,s.imports[e])}))}}return t},removeOverride:function(e){var t=x+e,r=null!==localStorage.getItem(t);return localStorage.removeItem(t),n&&(document.cookie=\"\".concat(t,\"=; expires=Thu, 01 Jan 1970 00:00:01 GMT;\")),i.enableOverride(e),l(),r},getOverrideScopes:function(){return function(e){return function(){var n=this,r=arguments;return new Promise((function(o,i){var a=e.apply(n,r);function l(e){t(a,o,i,l,u,\"next\",e)}function u(e){t(a,o,i,l,u,\"throw\",e)}l(void 0)}))}}(p().mark((function e(){var t,n,r,o,a,l,u,s,c;return p().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return t={},e.next=3,i.getDefaultMap();case 3:for(o in n=e.sent,(r=i.getOverrideMap()).imports)if(a=n.imports[o])for(c in l=new URL(a,window.location.href).href,u=new URL(r.imports[o],window.location.href).href,s=u.slice(0,u.lastIndexOf(\"/\"))+\"/\",n.scopes||{})l.startsWith(c)&&(t[s]=d(d({},t[s]||{}),n.scopes[c]));return e.abrupt(\"return\",{scopes:t});case 7:case\"end\":return e.stop()}}),e)})))()},resetOverrides:function(){return Object.keys(i.getOverrideMap(!0).imports).forEach((function(e){i.removeOverride(e)})),localStorage.removeItem(O),localStorage.removeItem(k),l(),i.getOverrideMap()},hasOverrides:function(){return Object.keys(i.getOverrideMap().imports).length>0},getUrlFromPort:function(e,t){var n=e.replace(/@/g,\"\").replace(/\\//g,\"-\");return\"//localhost:\".concat(t,\"/\").concat(n,\".js\")},enableUI:function(){var e=\"import-map-overrides-full\",t=\"show-when-local-storage\",n=document.querySelector(e);n||((n=document.createElement(e)).setAttribute(t,\"true\"),document.body.appendChild(n));var r=n.getAttribute(t);r&&(localStorage.setItem(r,!0),n.renderWithPreact())},mergeImportMap:function(e,t){var n={imports:{},scopes:{}};for(var r in e.imports)n.imports[r]=e.imports[r];for(var o in t.imports)n.imports[o]=t.imports[o];for(var i in e.scopes)n.scopes[i]=e.scopes[i];for(var a in t.scopes)n.scopes[a]=t.scopes[a];return n},resetDefaultMap:function(){e=null},getDefaultMap:function(){return e||(e=Array.prototype.reduce.call(document.querySelectorAll('script[type=\"'.concat(C,'\"], script[type=\"overridable-importmap\"]')),(function(e,t){return t.hasAttribute(M)?e:(n=t.src?y(t.src):Promise.resolve(JSON.parse(t.textContent)),Promise.all([e,n]).then((function(e){var t=m(e,2),n=t[0],r=t[1];return i.mergeImportMap(n,r)})));var n}),Promise.resolve({imports:{},scopes:{}})))},getCurrentPageMap:function(){return Promise.all([i.getDefaultMap(),i.getExternalOverrideMap(i.getCurrentPageExternalOverrides())]).then((function(e){var t=m(e,2),n=t[0],r=t[1];return i.mergeImportMap(i.mergeImportMap(n,r),c)}))},getCurrentPageExternalOverrides:function(){var e=[];return document.querySelectorAll(\"[\".concat(M,']:not([id=\"import-map-overrides\"])')).forEach((function(t){e.push(t.src)})),e},getNextPageMap:function(){return Promise.all([i.getDefaultMap(),i.getExternalOverrideMap()]).then((function(e){var t=m(e,2),n=t[0],r=t[1];return i.mergeImportMap(i.mergeImportMap(n,r),i.getOverrideMap())}))},disableOverride:function(e){var t=i.getDisabledOverrides();return!b(t,e)&&(localStorage.setItem(O,JSON.stringify(t.concat(e))),l(),!0)},enableOverride:function(e){var t=i.getDisabledOverrides(),n=t.indexOf(e);return n>=0&&(t.splice(n,1),localStorage.setItem(O,JSON.stringify(t)),l(),!0)},getDisabledOverrides:function(){var e=localStorage.getItem(O);return e?JSON.parse(e):[]},isDisabled:function(e){return b(i.getDisabledOverrides(),e)},getExternalOverrides:function(){var e=localStorage.getItem(k);return e?JSON.parse(e).sort():[]},addExternalOverride:function(e){e=new URL(e,document.baseURI).href;var t=i.getExternalOverrides();return!b(t,e)&&(localStorage.setItem(k,JSON.stringify(t.concat(e))),l(),!0)},removeExternalOverride:function(e){var t=i.getExternalOverrides();return!!b(t,e)&&(localStorage.setItem(k,JSON.stringify(t.filter((function(t){return t!==e})))),l(),!0)},getExternalOverrideMap:function(){return(arguments.length>0&&void 0!==arguments[0]?arguments[0]:i.getExternalOverrides()).reduce((function(e,t){var n=U[t]||(U[t]=y(t));return Promise.all([e,n]).then((function(e){var t=m(e,2),n=t[0],r=t[1];return i.mergeImportMap(n,r)}))}),Promise.resolve({imports:{},scopes:{}}))},isExternalMapValid:function(e){return(U[e]||(U[e]=y(e))).then((function(){return!b(i.invalidExternalMaps,e)}))},invalidExternalMaps:[]};var i=window.importMapOverrides,a=!0;try{CustomEvent?new CustomEvent(\"a\"):a=!1}catch(e){a=!1}function l(){u(\"change\")}function u(e){setTimeout((function(){var t=\"import-map-overrides:\".concat(e),n=a?new CustomEvent(t):document.createEvent(\"CustomEvent\");a||n.initCustomEvent(t,!0,!0,null),window.dispatchEvent(n)}))}var s,c=i.getOverrideMap(),f=i.getExternalOverrides();if(!r){var h=document.querySelector('script[type=\"overridable-importmap\"]');if(!(s=h)){var v=document.querySelectorAll('script[type=\"'.concat(C,'\"]'));s=v?v[v.length-1]:null}if(h){if(h.src)throw Error('import-map-overrides: external import maps with type=\"overridable-importmap\" are not supported');var g;try{g=JSON.parse(h.textContent)}catch(e){throw Error('Invalid - -<cfoutput>#pageTitle#</cfoutput> - - - - - - - - ---> - - - - - - - - - - -
-
- - -
- - - -
enctype="#ATTRIBUTES.pageInfo.formEncType#"> - - - -
-
- - - - -
- #title# -
-
- #controls# -
-
-
- -
- - - - #ATTRIBUTES.pageInfo.status.errorMessage# - - - - - - -
- - - -
- - -
- - - -
- - - - - - diff --git a/research/payg-sso-full/layout/render_query.cfm b/research/payg-sso-full/layout/render_query.cfm deleted file mode 100644 index 1cacb9d..0000000 --- a/research/payg-sso-full/layout/render_query.cfm +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - -
#col#
#ATTRIBUTES.query[col]#
-
\ No newline at end of file diff --git a/research/payg-sso-full/layout/xls.cfm b/research/payg-sso-full/layout/xls.cfm deleted file mode 100644 index 5d8a964..0000000 --- a/research/payg-sso-full/layout/xls.cfm +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#title##fieldArray[i]#
#dateFormat(data,'YYYY-MM-DD')# #timeFormat(data,'HH:mm')##Replace(data, '.', ',')##data##data#
-
-
- - - - - - \ No newline at end of file diff --git a/research/payg-sso-full/layout/xlshtm.cfm b/research/payg-sso-full/layout/xlshtm.cfm deleted file mode 100644 index b74ad64..0000000 --- a/research/payg-sso-full/layout/xlshtm.cfm +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#structFind(ATTRIBUTES.titleMap,field).title#
- #ATTRIBUTES.qRead[field]# -
- - -
-
- - - - \ No newline at end of file diff --git a/research/payg-sso-full/layout/xml.cfm b/research/payg-sso-full/layout/xml.cfm deleted file mode 100644 index 65e7e41..0000000 --- a/research/payg-sso-full/layout/xml.cfm +++ /dev/null @@ -1,239 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - xls-generator - - - - - - - - - - - - - - - ss:ExpandedColumnCount="#ListLen(qRead.ColumnList)#" - - ss:ExpandedRowCount="#(qRead.RecordCount + 1)#" - x:FullColumns="1" - x:FullRows="1"> - - - - - - - - - - - - - - - - - - select coalesce(max(length(#field#)),0) as maxlen from qRead - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #columns[i].title# - - - - - - - ss:StyleID="DateTime">#dateFormat(qRead[field],"YYYY-MM-DD")#T#timeFormat(qRead[field],"HH:mm:ss.l")##qRead[field]#
- -
- -
- -
- - - - - - - - \ No newline at end of file diff --git a/research/payg-sso-full/lib/ac.cfm b/research/payg-sso-full/lib/ac.cfm deleted file mode 100644 index 823d1cb..0000000 --- a/research/payg-sso-full/lib/ac.cfm +++ /dev/null @@ -1,79 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - select coalesce(max(permission),0) as permission - from usr_role ur - join acl on acl.role_id=ur.role_id - where ur.usr_id= - and acl.obj in () - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/research/payg-sso-full/lib/contenttype_icon.cfm b/research/payg-sso-full/lib/contenttype_icon.cfm deleted file mode 100644 index 1f8f3b1..0000000 --- a/research/payg-sso-full/lib/contenttype_icon.cfm +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/research/payg-sso-full/lib/controls/checkbox.cfm b/research/payg-sso-full/lib/controls/checkbox.cfm deleted file mode 100644 index 38ec2e2..0000000 --- a/research/payg-sso-full/lib/controls/checkbox.cfm +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - id=#ATTRIBUTES.id# value="#ATTRIBUTES.value#" checked="checked" disabled="disabled" #attr#="#structFind(ATTRIBUTES,attr)#"/> \ No newline at end of file diff --git a/research/payg-sso-full/lib/controls/column.cfm b/research/payg-sso-full/lib/controls/column.cfm deleted file mode 100644 index 47cf2d8..0000000 --- a/research/payg-sso-full/lib/controls/column.cfm +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/research/payg-sso-full/lib/controls/column_head.cfm b/research/payg-sso-full/lib/controls/column_head.cfm deleted file mode 100644 index f189b06..0000000 --- a/research/payg-sso-full/lib/controls/column_head.cfm +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -#ATTRIBUTES.title# - \ No newline at end of file diff --git a/research/payg-sso-full/lib/controls/combo.cfm b/research/payg-sso-full/lib/controls/combo.cfm deleted file mode 100644 index 13e2832..0000000 --- a/research/payg-sso-full/lib/controls/combo.cfm +++ /dev/null @@ -1,178 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - if (isArray(sel)) { - for(i=1; i <= ArrayLen(sel); i++){ - if (sel[i]==key) {return true;} - } - return false; - } else { //считаем, что из сложных типов тут бывает только массив - if (!isSimpleValue(sel)) {return false;} - /*return (sel == key);*/ - return listFind(sel,key); - } - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #preserveSingleQuotes(ATTRIBUTES.queryString)# - - - - - - - - - - - - - - - - - - - - - - select * from qSel where #ATTRIBUTES.key# in (#selectedList#)1=0 - - - -
#attr#="#structFind(ATTRIBUTES,attr)#"> - -
#Evaluate(ATTRIBUTES.display)#
-
- - -     - -     -
-
-
- - - - - -
- -
- - - diff --git a/research/payg-sso-full/lib/controls/date.cfm b/research/payg-sso-full/lib/controls/date.cfm deleted file mode 100644 index b6a3f6f..0000000 --- a/research/payg-sso-full/lib/controls/date.cfm +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - id=#ATTRIBUTES.id# value="#dateFormat(value, ATTRIBUTES.format)#" size="#ATTRIBUTES.size#" disabled="disabled" #attr#="#structFind(ATTRIBUTES,attr)#"/> \ No newline at end of file diff --git a/research/payg-sso-full/lib/controls/datetime.cfm b/research/payg-sso-full/lib/controls/datetime.cfm deleted file mode 100644 index 5b1f4fa..0000000 --- a/research/payg-sso-full/lib/controls/datetime.cfm +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - id=#ATTRIBUTES.id# value="#value#" size="#ATTRIBUTES.size#" disabled="disabled" #attr#="#structFind(ATTRIBUTES,attr)#"/> \ No newline at end of file diff --git a/research/payg-sso-full/lib/controls/detail_buttons.cfm b/research/payg-sso-full/lib/controls/detail_buttons.cfm deleted file mode 100644 index 1484050..0000000 --- a/research/payg-sso-full/lib/controls/detail_buttons.cfm +++ /dev/null @@ -1,18 +0,0 @@ - - \ No newline at end of file diff --git a/research/payg-sso-full/lib/controls/grid_head.cfm b/research/payg-sso-full/lib/controls/grid_head.cfm deleted file mode 100644 index 55090e8..0000000 --- a/research/payg-sso-full/lib/controls/grid_head.cfm +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/research/payg-sso-full/lib/controls/link_add.cfm b/research/payg-sso-full/lib/controls/link_add.cfm deleted file mode 100644 index b6fbf5b..0000000 --- a/research/payg-sso-full/lib/controls/link_add.cfm +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/research/payg-sso-full/lib/controls/link_del.cfm b/research/payg-sso-full/lib/controls/link_del.cfm deleted file mode 100644 index bd911e9..0000000 --- a/research/payg-sso-full/lib/controls/link_del.cfm +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/research/payg-sso-full/lib/controls/link_edit.cfm b/research/payg-sso-full/lib/controls/link_edit.cfm deleted file mode 100644 index eccea83..0000000 --- a/research/payg-sso-full/lib/controls/link_edit.cfm +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - -title="#ATTRIBUTES.title_edit#" class="edit"title="#ATTRIBUTES.title_view#" class="view"> \ No newline at end of file diff --git a/research/payg-sso-full/lib/controls/link_view.cfm b/research/payg-sso-full/lib/controls/link_view.cfm deleted file mode 100644 index 4cf9e70..0000000 --- a/research/payg-sso-full/lib/controls/link_view.cfm +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/research/payg-sso-full/lib/controls/link_view_edit.cfm b/research/payg-sso-full/lib/controls/link_view_edit.cfm deleted file mode 100644 index e676a5a..0000000 --- a/research/payg-sso-full/lib/controls/link_view_edit.cfm +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - -title="#ATTRIBUTES.title_edit#" class="edit"title="#ATTRIBUTES.title_view#" class="view"> \ No newline at end of file diff --git a/research/payg-sso-full/lib/controls/menu_item.cfm b/research/payg-sso-full/lib/controls/menu_item.cfm deleted file mode 100644 index 6c4a45d..0000000 --- a/research/payg-sso-full/lib/controls/menu_item.cfm +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - -
  • #ATTRIBUTES.label#
  • -
  • #ATTRIBUTES.label#
  • \ No newline at end of file diff --git a/research/payg-sso-full/lib/controls/nowbutton.cfm b/research/payg-sso-full/lib/controls/nowbutton.cfm deleted file mode 100644 index a142ece..0000000 --- a/research/payg-sso-full/lib/controls/nowbutton.cfm +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - #markup# - - - - \ No newline at end of file diff --git a/research/payg-sso-full/lib/data/del_condition.cfm b/research/payg-sso-full/lib/data/del_condition.cfm deleted file mode 100644 index 5cae40c..0000000 --- a/research/payg-sso-full/lib/data/del_condition.cfm +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/research/payg-sso-full/lib/data/dep_field.cfm b/research/payg-sso-full/lib/data/dep_field.cfm deleted file mode 100644 index 0d065da..0000000 --- a/research/payg-sso-full/lib/data/dep_field.cfm +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/research/payg-sso-full/lib/data/dep_param.cfm b/research/payg-sso-full/lib/data/dep_param.cfm deleted file mode 100644 index 7084ec2..0000000 --- a/research/payg-sso-full/lib/data/dep_param.cfm +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/research/payg-sso-full/lib/data/dependency.cfm b/research/payg-sso-full/lib/data/dependency.cfm deleted file mode 100644 index 19322dc..0000000 --- a/research/payg-sso-full/lib/data/dependency.cfm +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - select - - - , #field.expression# - - from - - #from.expression# - - where - - - AND - #condition.expression#= - - order by - - - , #order.expression# - - - - - - diff --git a/research/payg-sso-full/lib/data/dependency_condition.cfm b/research/payg-sso-full/lib/data/dependency_condition.cfm deleted file mode 100644 index 1b668ba..0000000 --- a/research/payg-sso-full/lib/data/dependency_condition.cfm +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/research/payg-sso-full/lib/data/dependency_field.cfm b/research/payg-sso-full/lib/data/dependency_field.cfm deleted file mode 100644 index 4a3c282..0000000 --- a/research/payg-sso-full/lib/data/dependency_field.cfm +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/research/payg-sso-full/lib/data/dependency_from.cfm b/research/payg-sso-full/lib/data/dependency_from.cfm deleted file mode 100644 index 83fbd4f..0000000 --- a/research/payg-sso-full/lib/data/dependency_from.cfm +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/research/payg-sso-full/lib/data/dependency_order_by.cfm b/research/payg-sso-full/lib/data/dependency_order_by.cfm deleted file mode 100644 index 48b990d..0000000 --- a/research/payg-sso-full/lib/data/dependency_order_by.cfm +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/research/payg-sso-full/lib/data/field.cfm b/research/payg-sso-full/lib/data/field.cfm deleted file mode 100644 index 8fa07da..0000000 --- a/research/payg-sso-full/lib/data/field.cfm +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/research/payg-sso-full/lib/data/field_set.cfm b/research/payg-sso-full/lib/data/field_set.cfm deleted file mode 100644 index b990b40..0000000 --- a/research/payg-sso-full/lib/data/field_set.cfm +++ /dev/null @@ -1,68 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/research/payg-sso-full/lib/data/param.cfm b/research/payg-sso-full/lib/data/param.cfm deleted file mode 100644 index 2433014..0000000 --- a/research/payg-sso-full/lib/data/param.cfm +++ /dev/null @@ -1,354 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/research/payg-sso-full/lib/data/status.cfc b/research/payg-sso-full/lib/data/status.cfc deleted file mode 100644 index 6e956c5..0000000 --- a/research/payg-sso-full/lib/data/status.cfc +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/research/payg-sso-full/lib/data/submit_collection.cfm b/research/payg-sso-full/lib/data/submit_collection.cfm deleted file mode 100644 index bc1c10b..0000000 --- a/research/payg-sso-full/lib/data/submit_collection.cfm +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/research/payg-sso-full/lib/data/submit_param.cfm b/research/payg-sso-full/lib/data/submit_param.cfm deleted file mode 100644 index 799a3ec..0000000 --- a/research/payg-sso-full/lib/data/submit_param.cfm +++ /dev/null @@ -1,69 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/research/payg-sso-full/lib/detail_page_info.cfc b/research/payg-sso-full/lib/detail_page_info.cfc deleted file mode 100644 index 1a0d1e8..0000000 --- a/research/payg-sso-full/lib/detail_page_info.cfc +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/research/payg-sso-full/lib/dispatch_detail.cfm b/research/payg-sso-full/lib/dispatch_detail.cfm deleted file mode 100644 index 30bffcd..0000000 --- a/research/payg-sso-full/lib/dispatch_detail.cfm +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/research/payg-sso-full/lib/encodingUtils.cfc b/research/payg-sso-full/lib/encodingUtils.cfc deleted file mode 100644 index 0a5cf2f..0000000 --- a/research/payg-sso-full/lib/encodingUtils.cfc +++ /dev/null @@ -1,283 +0,0 @@ -component { - - public any function init() { - variables.utcBaseDate = createObject( 'java', 'java.util.Date' ).init( javacast( 'int', 0 ) ); - variables.ECParameterSpecCache = { }; - } - - function convertDateToUnixTimestamp( required date dateToConvert ) { - return dateDiff( 's', utcBaseDate, parseDateTime( dateToConvert ) ); - } - - function convertUnixTimestampToDate( required numeric timestamp ) { - return dateAdd( 's', timestamp, utcBaseDate ); - } - - function base64UrlToBinary( base64url ) { - var base64 = base64url.replace( '-', '+', 'all' ).replace( '_', '/', 'all' ); - var padded = base64 & repeatString( '=', 4 - ( len( base64 ) % 4 ) ); - return binaryDecode( padded, 'base64' ); - } - - function binaryToBase64Url( source ) { - return binaryEncode( source, 'base64' ) - .replace( '+', '-', 'all' ) - .replace( '/', '_', 'all' ) - .replace( '=', '', 'all' ); - } - - /** - * The INTEGER encoding for DER consists of a 02 tag a length encoding of the value - * and then a signed, minimum sized, big endian encoding of the encoded number. - * - * - https://stackoverflow.com/questions/54718741/how-to-der-encode-an-ecdsa-signature - */ - function derEncodeIntegerBytes( byteArray ) { - // first remove any padding - for ( var i = 1; i <= arrayLen( byteArray ); i++ ) { - if ( byteArray[ i ] != 0 ) break; - } - var unpadded = arraySlice( byteArray, i ); - - // add sign if negative - if ( unpadded[ 1 ] < 0 ) { - unpadded.prepend( 0 ); - } - - // if len > 127 the length encoding will be wrong, but that won't happen for the supported signature sizes - var derEncoded = [ 2, unpadded.len() ]; - - derEncoded.append( unpadded, true ); - - return derEncoded; - } - - - /** - * The SEQUENCE encoding is simply a tag set to the byte value 30, the - * length encoding and then the concatenation of the two INTEGER - * structures. - * - * https://stackoverflow.com/questions/54718741/how-to-der-encode-an-ecdsa-signature - * - * Also see: - * https://crypto.stackexchange.com/questions/57731/ecdsa-signature-rs-to-asn1-der-encoding-question - */ - function convertP1363ToDER( signature ) { - var split = len( signature ) / 2; - var r = derEncodeIntegerBytes( arraySlice( signature, 1, split ) ); - var s = derEncodeIntegerBytes( arraySlice( signature, split + 1, split ) ); - - var DERSignature = [ 48 ]; - - var length = r.len() + s.len(); - - if ( length > 255 ) { - throw( - type = 'jwtcfml.InvalidSignature', - message = 'Invalid P1363 key.', - detail = 'The P1363 signature is too long.' - ); - } - - /* - The length is simply a single byte if it is smaller than 128 (or hex 80) - of the size. If it is larger then it is two byte: one byte set to 81, - which indicates that one length byte will follow, and one byte - containing the actual value. - - https://stackoverflow.com/questions/54718741/how-to-der-encode-an-ecdsa-signature - */ - if ( length > 127 ) { - DERSignature.append( -127 ); - length -= 256; - } - DERSignature.append( length ); - - DERSignature.append( r, true ); - DERSignature.append( s, true ); - - return javacast( 'byte[]', DERSignature ); - } - - function convertDERtoP1363( required any signature, required string algorithm ) { - // extract the two integers from the DER signature - // assuming a 02 tag byte followed by a single length byte since we should not see - // anything larger in the supported algorithms - var start = 3; - while ( signature[ start ] != 2 ) start++; - var r = arraySlice( signature, start + 2, signature[ start + 1 ] ); - var s = arraySlice( signature, start + 2 + r.len() + 2 ); - - if ( r[ 1 ] == 0 ) r = arraySlice( r, 2 ); - if ( s[ 1 ] == 0 ) s = arraySlice( s, 2 ); - - var lengthMap = { - ES256: 32, - ES384: 48, - ES512: 64 - }; - - var P1363Signature = [ ]; - - for ( var i = 1; i <= lengthMap[ algorithm ] - r.len(); i++ ) P1363Signature.append( 0 ); - P1363Signature.append( r, true ); - - for ( var i = 1; i <= lengthMap[ algorithm ] - s.len(); i++ ) P1363Signature.append( 0 ); - P1363Signature.append( s, true ); - - return javacast( 'byte[]', P1363Signature ); - } - - function parsePEMEncodedKey( required string pemKey ) { - if ( reFind( '^-----BEGIN (RSA|EC) (PARAMETERS|PRIVATE)', pemKey ) ) { - throw( - type = 'jwtcfml.InvalidPrivateKey', - message = 'Invalid private key format.', - detail = 'Please encode your private key in PKCS8 format, e.g.: `openssl pkcs8 -topk8 -nocrypt -in privatekey.pem -out privatekey.pk8' - ) - } - - var binaryKey = binaryDecode( - trim( pemKey ).reReplace( '-----[A-Z\s]+-----', '', 'all' ).reReplace( '[\r\n]', '', 'all' ), - 'base64' - ); - - if ( find( '-----BEGIN CERTIFICATE-----', pemKey ) ) { - var bis = createObject( 'java', 'java.io.ByteArrayInputStream' ).init( binaryKey ); - return createObject( 'java', 'java.security.cert.CertificateFactory' ) - .getInstance( 'X.509' ) - .generateCertificate( bis ) - .getPublicKey(); - } - - if ( find( '-----BEGIN PUBLIC KEY-----', pemKey ) ) { - var publicKeySpec = createObject( 'java', 'java.security.spec.X509EncodedKeySpec' ).init( binaryKey ); - try { - return createObject( 'java', 'java.security.KeyFactory' ) - .getInstance( 'RSA' ) - .generatePublic( publicKeySpec ); - } catch ( any e ) { - } - try { - return createObject( 'java', 'java.security.KeyFactory' ) - .getInstance( 'EC' ) - .generatePublic( publicKeySpec ); - } catch ( any e ) { - } - } - - if ( find( '-----BEGIN PRIVATE KEY-----', pemKey ) ) { - var privateKeySpec = createObject( 'java', 'java.security.spec.PKCS8EncodedKeySpec' ).init( binaryKey ); - try { - return createObject( 'java', 'java.security.KeyFactory' ) - .getInstance( 'RSA' ) - .generatePrivate( privateKeySpec ); - } catch ( any e ) { - } - try { - return createObject( 'java', 'java.security.KeyFactory' ) - .getInstance( 'EC' ) - .generatePrivate( privateKeySpec ); - } catch ( any e ) { - } - } - - throw( - type = 'jwtcfml.InvalidPEMKey', - message = 'Invalid PEM key.', - detail = 'Please ensure you are using an RSA or EC public or private key or certificate.' - ) - } - - function parseJWK( required struct jwk ) { - if ( jwk.kty == 'RSA' ) { - if ( jwk.keyExists( 'd' ) ) { - try { - var bigInts = bigIntegers( jwk, [ 'n', 'e', 'd', 'p', 'q', 'dp', 'dq', 'qi' ] ); - var keySpec = createObject( 'java', 'java.security.spec.RSAPrivateCrtKeySpec' ).init( - bigInts.n, - bigInts.e, - bigInts.d, - bigInts.p, - bigInts.q, - bigInts.dp, - bigInts.dq, - bigInts.qi - ); - var kf = createObject( 'java', 'java.security.KeyFactory' ).getInstance( 'RSA' ); - return kf.generatePrivate( keySpec ); - } catch ( any e ) { - } - - try { - var bigInts = bigIntegers( jwk, [ 'n', 'd' ] ); - var keySpec = createObject( 'java', 'java.security.spec.RSAPrivateKeySpec' ).init( - bigInts.n, - bigInts.d - ); - var kf = createObject( 'java', 'java.security.KeyFactory' ).getInstance( 'RSA' ); - return kf.generatePrivate( keySpec ); - } catch ( any e ) { - } - } else { - try { - var bigInts = bigIntegers( jwk, [ 'n', 'e' ] ); - var ks = createObject( 'java', 'java.security.spec.RSAPublicKeySpec' ).init( bigInts.n, bigInts.e ); - var kf = createObject( 'java', 'java.security.KeyFactory' ).getInstance( 'RSA' ); - return kf.generatePublic( ks ); - } catch ( any e ) { - } - } - } - - if ( jwk.kty == 'EC' ) { - var kf = createObject( 'java', 'java.security.KeyFactory' ).getInstance( 'EC' ); - var ECParameterSpec = getECParameterSpec( jwk.crv ); - - if ( jwk.keyExists( 'd' ) ) { - var bigInts = bigIntegers( jwk, [ 'd' ] ); - var ks = createObject( 'java', 'java.security.spec.ECPrivateKeySpec' ).init( - bigInts.d, - ECParameterSpec - ); - return kf.generatePrivate( ks ); - } else { - var bigInts = bigIntegers( jwk, [ 'x', 'y' ] ); - var ECPoint = createObject( 'java', 'java.security.spec.ECPoint' ).init( bigInts.x, bigInts.y ); - var ks = createObject( 'java', 'java.security.spec.ECPublicKeySpec' ).init( ECPoint, ECParameterSpec ); - return kf.generatePublic( ks ); - } - } - - throw( - type = 'jwtcfml.InvalidJWK', - message = 'Invalid JWK key.', - detail = 'Please ensure you are using an valid JWK RSA or EC public or private key.' - ) - } - - private function bigIntegers( jwk, keys ) { - var bigInts = { }; - for ( var key in keys ) { - bigInts[ key ] = createObject( 'java', 'java.math.BigInteger' ).init( 1, base64UrlToBinary( jwk[ key ] ) ); - } - return bigInts; - } - - private function getECParameterSpec( crv ) { - if ( !variables.ECParameterSpecCache.keyExists( crv ) ) { - var kpg = createObject( 'java', 'java.security.KeyPairGenerator' ).getInstance( 'EC' ); - var ecgp = createObject( 'java', 'java.security.spec.ECGenParameterSpec' ).init( - 'secp#crv.listLast( '-' )#r1' - ); - kpg.initialize( ecgp ); - variables.ECParameterSpecCache[ crv ] = kpg - .generateKeyPair() - .getPublic() - .getParams(); - } - return variables.ECParameterSpecCache[ crv ]; - } - -} diff --git a/research/payg-sso-full/lib/filter_build.cf_ b/research/payg-sso-full/lib/filter_build.cf_ deleted file mode 100644 index 146c197..0000000 --- a/research/payg-sso-full/lib/filter_build.cf_ +++ /dev/null @@ -1,131 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #item# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/research/payg-sso-full/lib/filter_build.cfm b/research/payg-sso-full/lib/filter_build.cfm deleted file mode 100644 index fb35c56..0000000 --- a/research/payg-sso-full/lib/filter_build.cfm +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - - - - - - AND #listGetAt(fltr.expression,i,"?")##fltr.field# = <> <= < >= > ILIKE NOT ILIKE ILIKE NOT ILIKE IN () = #fltr.field# - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/research/payg-sso-full/lib/filter_page_info.cfc b/research/payg-sso-full/lib/filter_page_info.cfc deleted file mode 100644 index ea82758..0000000 --- a/research/payg-sso-full/lib/filter_page_info.cfc +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/research/payg-sso-full/lib/filter_rebuild.cfm b/research/payg-sso-full/lib/filter_rebuild.cfm deleted file mode 100644 index e69de29..0000000 diff --git a/research/payg-sso-full/lib/filter_settings.cfm b/research/payg-sso-full/lib/filter_settings.cfm deleted file mode 100644 index 4d6089d..0000000 --- a/research/payg-sso-full/lib/filter_settings.cfm +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/research/payg-sso-full/lib/filterparam.cfm b/research/payg-sso-full/lib/filterparam.cfm deleted file mode 100644 index af5607c..0000000 --- a/research/payg-sso-full/lib/filterparam.cfm +++ /dev/null @@ -1,180 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - if (!IsSimpleValue(val)){ - if (isArray(val)) { - return (arrayLen(val) == 0); - } - return false; - } - return ((val==def) OR (val=="")); - - - - - - ", ",,,")/> - - diff --git a/research/payg-sso-full/lib/form_page_info.cfc b/research/payg-sso-full/lib/form_page_info.cfc deleted file mode 100644 index 5d0d6ac..0000000 --- a/research/payg-sso-full/lib/form_page_info.cfc +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/research/payg-sso-full/lib/generic_page_info.cfc b/research/payg-sso-full/lib/generic_page_info.cfc deleted file mode 100644 index 08c50d3..0000000 --- a/research/payg-sso-full/lib/generic_page_info.cfc +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/research/payg-sso-full/lib/init_settings.cfm b/research/payg-sso-full/lib/init_settings.cfm deleted file mode 100644 index f51c235..0000000 --- a/research/payg-sso-full/lib/init_settings.cfm +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/research/payg-sso-full/lib/jwt.cfc b/research/payg-sso-full/lib/jwt.cfc deleted file mode 100644 index e86a066..0000000 --- a/research/payg-sso-full/lib/jwt.cfc +++ /dev/null @@ -1,262 +0,0 @@ -component { - - variables.algorithmMap = { - HS256: 'HmacSHA256', - HS384: 'HmacSHA384', - HS512: 'HmacSHA512', - RS256: 'SHA256withRSA', - RS384: 'SHA384withRSA', - RS512: 'SHA512withRSA', - ES256: 'SHA256withECDSA', - ES384: 'SHA384withECDSA', - ES512: 'SHA512withECDSA' - }; - - public any function init() { - variables.encodingUtils = new encodingUtils(); - variables.jss = createObject( 'java', 'java.security.Signature' ); - variables.messageDigest = createObject( 'java', 'java.security.MessageDigest' ); - return this; - } - - public string function encode( - required struct payload, - required any key, - required string algorithm, - struct headers = { } - ) { - if ( !algorithmMap.keyExists( algorithm ) ) { - throw( - type = 'jwtcfml.InvalidAlgorithm', - message = 'Invalid JWT Algorithm.', - detail = 'The passed in algorithm is not supported.' - ); - } - - var header = { }; - header.append( headers ); - header.append( { - 'typ': 'JWT', - 'alg': algorithm - } ); - - var duplicatedPayload = duplicate( payload ); - for ( var claim in [ 'iat', 'exp', 'nbf' ] ) { - if ( duplicatedPayload.keyExists( claim ) && isDate( duplicatedPayload[ claim ] ) ) { - duplicatedPayload[ claim ] = encodingUtils.convertDateToUnixTimestamp( duplicatedPayload[ claim ] ); - } - } - - var stringToSignParts = [ - encodingUtils.binaryToBase64Url( charsetDecode( serializeJSON( header ), 'utf-8' ) ), - encodingUtils.binaryToBase64Url( charsetDecode( serializeJSON( duplicatedPayload ), 'utf-8' ) ) - ]; - var stringToSign = stringToSignParts.toList( '.' ); - - return stringToSign & '.' & encodingUtils.binaryToBase64Url( sign( stringToSign, key, algorithm ) ); - } - - public struct function decode( - required string token, - any key, - any algorithms = [ ], - struct claims = { }, - boolean verify = true - ) { - var parts = listToArray( token, '.' ); - - if ( arrayLen( parts ) != 3 ) { - throw( - type = 'jwtcfml.InvalidToken', - message = 'Invalid JWT.', - detail = 'The passed in token does not have three `.` delimited parts.' - ); - } - - algorithms = isArray( algorithms ) ? algorithms : [ algorithms ]; - - var decoded = { - header: deserializeJSON( charsetEncode( encodingUtils.base64UrlToBinary( parts[ 1 ] ), 'utf-8' ) ), - payload: deserializeJSON( charsetEncode( encodingUtils.base64UrlToBinary( parts[ 2 ] ), 'utf-8' ) ) - }; - - if ( verify ) { - if ( - !algorithms.find( decoded.header.alg ) || - !algorithmMap.keyExists( decoded.header.alg ) - ) { - throw( - type = 'jwtcfml.InvalidAlgorithm', - message = 'Unsupported or invalid algorithm', - detail = 'The passed in token does not have an algorithm declaration or its declared algorithm (#decoded.header.alg#) does not match the specified algorithms of #serializeJSON( algorithms )#.' - ); - } - - var stringToSign = parts[ 1 ] & '.' & parts[ 2 ]; - var signature = encodingUtils.base64UrlToBinary( parts[ 3 ] ); - - if ( - !verifySignature( - stringToSign, - key, - signature, - decoded.header.alg - ) - ) { - throw( - type = 'jwtcfml.InvalidSignature', - message = 'Signature is Invalid', - detail = 'The signature of the passed in token is invalid.' - ); - } - - var baseClaims = { - 'exp': true, - 'nbf': true - }; - baseClaims.append( claims ); - verifyClaims( decoded.payload, baseClaims ); - } - - for ( var claim in [ 'iat', 'exp', 'nbf' ] ) { - if ( decoded.payload.keyExists( claim ) ) { - decoded.payload[ claim ] = encodingUtils.convertUnixTimestampToDate( decoded.payload[ claim ] ); - } - } - - return decoded.payload; - } - - public struct function getHeader( required string token ) { - return deserializeJSON( charsetEncode( encodingUtils.base64UrlToBinary( listFirst( token, '.' ) ), 'utf-8' ) ); - } - - public function parsePEMEncodedKey( required string pemKey ) { - return encodingUtils.parsePEMEncodedKey( pemKey ); - } - - public function parseJWK( required struct jwk ) { - return encodingUtils.parseJWK( jwk ); - } - - private function sign( message, key, algorithm ) { - if ( left( algorithm, 1 ) == 'H' ) { - var sig = binaryDecode( - hmac( - message, - key, - algorithmMap[ algorithm ], - 'utf-8' - ), - 'hex' - ); - } else { - if ( isSimpleValue( key ) ) { - key = encodingUtils.parsePEMEncodedKey( key ); - } else if ( isStruct( key ) ) { - key = encodingUtils.parseJWK( key ); - } - - var jssInstance = variables.jss.getInstance( algorithmMap[ algorithm ] ); - jssInstance.initSign( key ); - jssInstance.update( charsetDecode( message, 'utf-8' ) ); - var sig = jssInstance.sign(); - if ( left( algorithm, 1 ) == 'E' ) { - sig = encodingUtils.convertDERtoP1363( sig, algorithm ); - } - } - return sig; - } - - private function verifySignature( message, key, signature, algorithm ) { - if ( left( algorithm, 1 ) == 'H' ) { - var sig = binaryDecode( - hmac( - message, - key, - algorithmMap[ algorithm ], - 'utf-8' - ), - 'hex' - ); - return MessageDigest.isEqual( signature, sig ); - } - - if ( left( algorithm, 1 ) == 'E' ) { - signature = encodingUtils.convertP1363ToDER( signature ); - } - - if ( isSimpleValue( key ) ) { - key = encodingUtils.parsePEMEncodedKey( key ); - } else if ( isStruct( key ) ) { - key = encodingUtils.parseJWK( key ); - } - - var jssInstance = variables.jss.getInstance( algorithmMap[ algorithm ] ); - jssInstance.initVerify( key ); - jssInstance.update( charsetDecode( message, 'utf-8' ) ); - return jssInstance.verify( signature ); - } - - private function verifyClaims( payload, claims ) { - if ( - structKeyExists( payload, 'exp' ) - && !verifyDateClaim( payload.exp, claims.exp, -1 ) - ) { - throw( - type = 'jwtcfml.ExpiredSignature', - message = 'Token has expired', - detail = 'The passed in token has expired.' - ); - } - - if ( - structKeyExists( payload, 'nbf' ) - && !verifyDateClaim( payload.nbf, claims.nbf, 1 ) - ) { - throw( - type = 'jwtcfml.NotBeforeException', - message = 'Token is not valid', - detail = 'The passed in token has not yet become valid.' - ); - } - - - - if ( structKeyExists( claims, 'iss' ) ) { - if ( !structKeyExists( payload, 'iss' ) || compare( payload.iss, claims.iss ) != 0 ) { - throw( - type = 'jwtcfml.InvalidIssuer', - message = 'Token has an invalid issuer', - detail = 'The passed in token either does not specify an issuer or the claimed issuer is not valid.' - ); - } - } - - if ( structKeyExists( claims, 'aud' ) ) { - var audArray = isArray( claims.aud ) ? claims.aud : [ claims.aud ]; - if ( !structKeyExists( payload, 'aud' ) || !audArray.find( payload.aud ) ) { - throw( - type = 'jwtcfml.InvalidAudience', - message = 'Token has an invalid audience', - detail = 'The passed in token either does not specify an audience or the claimed audience is not valid.' - ); - } - } - } - - private function verifyDateClaim( payloadDate, claim, failState ) { - var pd = encodingUtils.convertUnixTimestampToDate( payloadDate ); - var cd = claim; - if ( !isBoolean( cd ) || cd ) { - if ( isNumeric( cd ) ) { - cd = encodingUtils.convertUnixTimestampToDate( cd ); - } else if ( !isDate( cd ) ) { - cd = now(); - } - return dateCompare( pd, cd ) != failState; - } - return true; - } - -} diff --git a/research/payg-sso-full/lib/location.cfm b/research/payg-sso-full/lib/location.cfm deleted file mode 100644 index 06ceafe..0000000 --- a/research/payg-sso-full/lib/location.cfm +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/research/payg-sso-full/lib/ls_catch.cfm b/research/payg-sso-full/lib/ls_catch.cfm deleted file mode 100644 index 918f958..0000000 --- a/research/payg-sso-full/lib/ls_catch.cfm +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - Ошибка при обращении к базе данных. Возможно, программа некорректно сформировала условия фильтра или сортировки. В качестве временной меры попробуйте сбросить условия фильтра и сортировки. Просьба сообщить об ошибке администратору.
    - #ATTRIBUTES.catch.message#
    - #ATTRIBUTES.catch.detail#
    -
    -
    - - - - - - - - - - - - -
    \ No newline at end of file diff --git a/research/payg-sso-full/lib/ls_page_info.cfc b/research/payg-sso-full/lib/ls_page_info.cfc deleted file mode 100644 index 32e352c..0000000 --- a/research/payg-sso-full/lib/ls_page_info.cfc +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/research/payg-sso-full/lib/menu_item.cfm b/research/payg-sso-full/lib/menu_item.cfm deleted file mode 100644 index 632e78a..0000000 --- a/research/payg-sso-full/lib/menu_item.cfm +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/research/payg-sso-full/lib/mimetype_icon.cfm b/research/payg-sso-full/lib/mimetype_icon.cfm deleted file mode 100644 index 6128b1a..0000000 --- a/research/payg-sso-full/lib/mimetype_icon.cfm +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/research/payg-sso-full/lib/oauth2.cfc b/research/payg-sso-full/lib/oauth2.cfc deleted file mode 100644 index 41fa80d..0000000 --- a/research/payg-sso-full/lib/oauth2.cfc +++ /dev/null @@ -1,193 +0,0 @@ -/** -* @displayname oauth2 -* @output false -* @hint The oauth2 object. -* @author Matt Gifford -* @website https://www.monkehworks.com -* @purpose A ColdFusion Component to manage authentication using the OAuth2 protocol. -**/ -component accessors="true"{ - - property name="client_id" type="string"; - property name="client_secret" type="string"; - property name="authEndpoint" type="string"; - property name="accessTokenEndpoint" type="string"; - property name="redirect_uri" type="string"; - property name="PKCE" type="struct"; - - /** - * I return an initialized oauth2 object instance. - * @client_id The client ID for your application. - * @client_secret The client secret for your application. - * @authEndpoint The URL endpoint that handles the authorisation. - * @accessTokenEndpoint The URL endpoint that handles retrieving the access token. - * @redirect_uri The URL to redirect the user back to following authentication. - **/ - public oauth2 function init( - required string client_id, - required string client_secret, - required string authEndpoint, - required string accessTokenEndpoint, - required string redirect_uri - ){ - setClient_id( arguments.client_id ); - setClient_secret( arguments.client_secret ); - setAuthEndpoint( arguments.authEndpoint ); - setAccessTokenEndpoint( arguments.accessTokenEndpoint ); - setRedirect_uri( arguments.redirect_uri ); - return this; - } - - /** - * I return the URL as a string which we use to redirect the user for authentication. | - * The method will handle the client_id and redirect_uri values for you. Anything else you need to send to the provider in the URL can be sent via the parameters argument. - * @parameters A structure containing key / value pairs of data to be included in the URL string. - **/ - public string function buildRedirectToAuthURL( struct parameters={} ) { - var objAuthBuilder = new utils.authStringBuilder( - authEndpoint = getAuthEndpoint(), - client_id = getClient_id(), - redirect_uri = getRedirect_uri() - ); - return objAuthBuilder.withParams( arguments.parameters ).get(); - } - - /** - * I return an instance of the authStringBuilder class. The actual link string is generated by then calling the .get() method on the class object. - * The method will handle the client_id and redirect_uri values for you. Anything else you need to send to the provider in the URL can be sent via the parameters argument. - * @parameters A structure containing key / value pairs of data to be included in the URL string. - **/ - public authStringBuilder function buildRedirectToAuthURLWithBuilder( struct parameters={} ) { - var objAuthBuilder = new utils.authStringBuilder( - authEndpoint = getAuthEndpoint(), - client_id = getClient_id(), - redirect_uri = getRedirect_uri() - ); - return objAuthBuilder.withParams( arguments.parameters ); - } - - /** - * I make the HTTP request to obtain the access token. - * @code The code returned from the authentication request. - * @formfields An optional array of structs for the provider requirements to add new form fields. - * @headers An optional array of structs to add custom headers to the request if required. - **/ - public struct function makeAccessTokenRequest( - required string code, - array formfields = [], - array headers = [] - ){ - var stuResponse = {}; - var httpService = new http(); - httpService.setMethod( "post" ); - httpService.setCharset( "utf-8" ); - httpService.setUrl( getAccessTokenEndpoint() ); - if( arrayLen( arguments.headers ) ){ - for( var item in arguments.headers ){ - httpService.addParam( type="header", name=item[ 'name' ], value=item[ 'value' ] ); - } - } - httpService.addParam( type="formfield", name="client_id", value=getClient_id() ); - httpService.addParam( type="formfield", name="client_secret", value=getClient_secret() ); - httpService.addParam( type="formfield", name="code", value=arguments.code ); - httpService.addParam( type="formfield", name="redirect_uri", value=getRedirect_uri() ); - httpService.addParam( type="formfield", name="grant_type", value='authorization_code' ); - if( arrayLen( arguments.formfields ) ){ - for( var item in arguments.formfields ){ - httpService.addParam( type="formfield", name=item[ 'name' ], value=item[ 'value' ] ); - } - } - var result = httpService.send().getPrefix(); - if( '200' == result.ResponseHeader[ 'Status_Code' ] ) { - stuResponse.success = true; - stuResponse.content = result.FileContent; - } else { - stuResponse.success = false; - stuResponse.content = result.Statuscode; - } - //dump(var=formfields,label="form fields"); - return stuResponse; - } - - /** - * I make the HTTP request to refresh the access token. - * @refresh_token The refresh_token returned from the accessTokenRequest request. - **/ - public struct function refreshAccessTokenRequest( - required string refresh_token, - array formfields = [], - array headers = [] - ){ - var stuResponse = {}; - var httpService = new http(); - httpService.setMethod( "post" ); - httpService.setCharset( "utf-8" ); - httpService.setUrl( getAccessTokenEndpoint() ); - httpService.addParam( type="header", name="Content-Type", value="application/x-www-form-urlencoded" ); - if( arrayLen( arguments.headers ) ){ - for( var item in arguments.headers ){ - httpService.addParam( type="header", name=item[ 'name' ], value=item[ 'value' ] ); - } - } - httpService.addParam( type="formfield", name="client_id", value=getClient_id() ); - httpService.addParam( type="formfield", name="client_secret", value=getClient_secret() ); - httpService.addParam( type="formfield", name="refresh_token", value=arguments.refresh_token ); - httpService.addParam( type="formfield", name="grant_type", value="refresh_token" ); - if( arrayLen( arguments.formfields ) ){ - for( var item in arguments.formfields ){ - httpService.addParam( type="formfield", name=item[ 'name' ], value=item[ 'value' ] ); - } - } - var result = httpService.send().getPrefix(); - if( '200' == result.ResponseHeader[ 'Status_Code' ] ) { - stuResponse.success = true; - stuResponse.content = result.FileContent; - } else { - stuResponse.success = false; - stuResponse.content = result.Statuscode; - } - return stuResponse; - } - - /** - * I return a string containing any extra URL parameters to concatenate and pass through when authenticating. - * @argScope A structure containing key / value pairs of data to be included in the URL string. - **/ - public string function buildParamString( struct argScope={} ) { - var strURLParam = '?'; - if( structCount( arguments.argScope ) ) { - var intCount = 1; - for( var key in arguments.argScope ) { - if( listLen( strURLParam ) && intCount > 1 ) { - strURLParam = strURLParam & '&'; - } - strURLParam = strURLParam & lcase( key ) & '=' & trim( arguments.argScope[ key ] ); - intCount++; - } - } - return strURLParam; - } - - /** - * Returns the properties as a struct - */ - public struct function getMemento(){ - var result = {}; - for( var thisProp in getMetaData( this ).properties ){ - if( structKeyExists( variables, thisProp[ 'name' ] ) ){ - result[ thisProp[ 'name' ] ] = variables[ thisProp[ 'name' ] ]; - } - } - return result; - } - - /** - * Generates a struct containing the code verifier and code challenge - * values for use with the PKCE extension. - */ - public struct function generatePKCE(){ - var objPKCE = new utils.deps.pkce.pkce(); - return objPKCE.generatePKCE(); - } - -} \ No newline at end of file diff --git a/research/payg-sso-full/lib/order_build.cfm b/research/payg-sso-full/lib/order_build.cfm deleted file mode 100644 index bda0f97..0000000 --- a/research/payg-sso-full/lib/order_build.cfm +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/research/payg-sso-full/lib/prepare_detail.cfm b/research/payg-sso-full/lib/prepare_detail.cfm deleted file mode 100644 index d84a00a..0000000 --- a/research/payg-sso-full/lib/prepare_detail.cfm +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/research/payg-sso-full/lib/prepare_filter.cfm b/research/payg-sso-full/lib/prepare_filter.cfm deleted file mode 100644 index 5282f5c..0000000 --- a/research/payg-sso-full/lib/prepare_filter.cfm +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/research/payg-sso-full/lib/prepare_form.cfm b/research/payg-sso-full/lib/prepare_form.cfm deleted file mode 100644 index baccc32..0000000 --- a/research/payg-sso-full/lib/prepare_form.cfm +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/research/payg-sso-full/lib/prepare_ls.cfm b/research/payg-sso-full/lib/prepare_ls.cfm deleted file mode 100644 index 01a6e05..0000000 --- a/research/payg-sso-full/lib/prepare_ls.cfm +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/research/payg-sso-full/lib/rest_api_helper.cfc b/research/payg-sso-full/lib/rest_api_helper.cfc deleted file mode 100644 index 23221d9..0000000 --- a/research/payg-sso-full/lib/rest_api_helper.cfc +++ /dev/null @@ -1,396 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/research/payg-sso-full/lib/settings.cfm b/research/payg-sso-full/lib/settings.cfm deleted file mode 100644 index 4d1505a..0000000 --- a/research/payg-sso-full/lib/settings.cfm +++ /dev/null @@ -1,213 +0,0 @@ - - - - - - -Разрешение конфликта версий при обновлении софта -Конфликт версий: код требует параметр, отсутствующий в сохраненной версии. Например, одна версия фильтра сохранила свои настройки, -а другая, более новая, пытается прочесть несуществующий ключ. -Другой вариант - несуществующее поле (убранное в очередной версии) - этот конфликт проявится только при запросе к БД. -Варианты решения -- создание параметра с дефолтными значениями при запросе (во все запросы добавить значения по умолчанию). Недостаток - возможно создание помойки -- контроль номера версии. Выглядит сложным в сопровождении -*** Возможно, это была плохая идея - разрешить доступ по длинному ключу. Когда доставали и записывали целиком объект, было меньше возможностей для ошибок ----> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - select settings from usr where usr_id='#request.usr_id#' - - - - - - Err: - #cfcatch.message# - - - - - - - - - - - - - - - - - - select count(*) as cnt from usr_serrings where usr_id= - - - - - - - insert int usr_settings (usr_id, settings) - values(, ) - - - - update usr_settings - set settings= - where usr_id= - - - - - - - diff --git a/research/payg-sso-full/lib/silent.cfm b/research/payg-sso-full/lib/silent.cfm deleted file mode 100644 index 7f2f6f2..0000000 --- a/research/payg-sso-full/lib/silent.cfm +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/research/payg-sso-full/lib/sortparam.cfm b/research/payg-sso-full/lib/sortparam.cfm deleted file mode 100644 index 1648d2b..0000000 --- a/research/payg-sso-full/lib/sortparam.cfm +++ /dev/null @@ -1,76 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/research/payg-sso-full/lib/track.cfm b/research/payg-sso-full/lib/track.cfm deleted file mode 100644 index 169d97d..0000000 --- a/research/payg-sso-full/lib/track.cfm +++ /dev/null @@ -1,103 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/research/payg-sso-full/lib/upload.cfm b/research/payg-sso-full/lib/upload.cfm deleted file mode 100644 index 7d9c22e..0000000 --- a/research/payg-sso-full/lib/upload.cfm +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/research/payg-sso-full/lib/utils/authStringBuilder.cfc b/research/payg-sso-full/lib/utils/authStringBuilder.cfc deleted file mode 100644 index 990f047..0000000 --- a/research/payg-sso-full/lib/utils/authStringBuilder.cfc +++ /dev/null @@ -1,142 +0,0 @@ -component { - - property name="authEndpoint" type="string" default=""; - property name="client_id" type="string" default=""; - property name="redirect_uri" type="string" default=""; - property name="response_type" type="string" default="code"; - property name="state" type="string" default=""; - - property name="PKCECodeChallenge" type="string" default=""; - property name="PKCECodeChallengeMethod" type="string" default="S256"; - property name="PKCECodeVerifier" type="string" default=""; - - public authStringBuilder function init( - required string authEndpoint, - required string client_id, - required string redirect_uri, - string response_type = 'code' - ){ - setAuthEndpoint( arguments.authEndpoint ); - setClientId( arguments.client_id ); - setRedirectURI( arguments.redirect_uri ); - setResponseType( arguments.response_type ); - return this; - } - - public void function setAuthEndpoint( required string authEndpoint ){ - variables.authEndpoint = arguments.authEndpoint; - } - - public void function setClientId( required string client_id ){ - variables.params[ 'client_id' ] = arguments.client_id; - } - - public void function setRedirectURI( required string redirect_uri ){ - variables.params[ 'redirect_uri' ] = arguments.redirect_uri; - } - - public void function setResponseType( required string response_type ){ - variables.params[ 'response_type' ] = arguments.response_type; - } - - public void function setPKCECodeChallenge( required string code_challenge ){ - variables.params[ 'code_challenge' ] = arguments.code_challenge; - } - - public void function setPKCECodeChallengeMethod( required string code_challenge_method ){ - variables.params[ 'code_challenge_method' ] = arguments.code_challenge_method; - } - - public void function setPKCECodeVerifier( required string code_verifier ){ - variables.params[ 'code_verifier' ] = arguments.code_verifier; - } - - - - public authStringBuilder function withAuthEndpoint( - required string authEndpoint - ){ - setAuthEndpoint( arguments.authEndpoint ); - return this; - } - - public authStringBuilder function withClientId( - required string client_id - ){ - setClientId( arguments.client_id ); - return this; - } - - public authStringBuilder function withResponseType( - required string response_type - ){ - setResponseType( arguments.response_type ); - return this; - } - - public authStringBuilder function withRedirectURI( - required string redirect_uri - ){ - setRedirectURI( arguments.redirect_uri ); - return this; - } - - public authStringBuilder function withState( - required string state - ){ - variables.params[ 'state' ] = arguments.state; - return this; - } - - public authStringBuilder function withPKCE( - required string code_challenge, - required string code_challenge_method, - required string code_verifier - ){ - setPKCECodeChallenge( arguments.code_challenge ); - setPKCECodeChallengeMethod( arguments.code_challenge_method ); - setPKCECodeVerifier( arguments.code_verifier ); - return this; - } - - public authStringBuilder function withParams( required struct parameters ){ - for( var item in arguments.parameters ){ - variables.params[ item ] = arguments.parameters[ item ]; - } - return this; - } - - - public string function get(){ - var stuParams = duplicate( variables.params ); - structDelete( stuParams, 'code_verifier' ); - return variables.authEndpoint & buildParamString( argScope = stuParams ); - } - - /** - * Returns a struct representation of the params values stored in variables.params. - */ - public struct function paramsMemento(){ - return variables.params; - } - - /** - * I return a string containing any extra URL parameters to concatenate and pass through when authenticating. - * @argScope A structure containing key / value pairs of data to be included in the URL string. - **/ - public string function buildParamString( struct argScope={} ) { - var strURLParam = '?'; - if( structCount( arguments.argScope ) ) { - var intCount = 1; - for( var key in arguments.argScope ) { - if( listLen( strURLParam ) && intCount > 1 ) { - strURLParam = strURLParam & '&'; - } - strURLParam = strURLParam & lcase( key ) & '=' & trim( arguments.argScope[ key ] ); - intCount++; - } - } - return strURLParam; - } - -} \ No newline at end of file diff --git a/research/payg-sso-full/mod/conventions.txt b/research/payg-sso-full/mod/conventions.txt deleted file mode 100644 index c48422e..0000000 --- a/research/payg-sso-full/mod/conventions.txt +++ /dev/null @@ -1,3 +0,0 @@ -White label - на экране логина предусматривать место для custom текста - Помимо лого, предусматривать custom text \ No newline at end of file diff --git a/research/payg-sso-full/mod/crm_contract.cfm b/research/payg-sso-full/mod/crm_contract.cfm deleted file mode 100644 index 0b2fbf1..0000000 --- a/research/payg-sso-full/mod/crm_contract.cfm +++ /dev/null @@ -1,94 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/research/payg-sso-full/mod/payg.cfm b/research/payg-sso-full/mod/payg.cfm deleted file mode 100644 index 2ca1bf9..0000000 --- a/research/payg-sso-full/mod/payg.cfm +++ /dev/null @@ -1,428 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - #getTickCount() - request.startTickCount# - - - - - - select max(capacity_resource.timestamp) as dt_load from ngcloud_ru.capacity_resource - - - #getTickCount() - request.startTickCount# - - - - - - select max(capacity_storage.timestamp) as dt_load from ngcloud_ru.capacity_storage - - - - #getTickCount() - request.startTickCount# - - - - - - select max(timestamp_addition) as dt_load from s3billing.bucket_stat - - - #getTickCount() - request.startTickCount# - - - - - - select max(time) as dt_load from s3billing.usage_bucket_by_user - - - #getTickCount() - request.startTickCount# - - - - - - - - - - - -select s.catalogName as svc, s.component, s.fullCode as code, s.measureShort as unit -, s.priceWVat as price, (s.discount + s.serviceDiscount - s.discount*s.serviceDiscount/100) as discount -,s.serviceStart, s.serviceEnd, s.serviceHash, s.serviceCustomName, f.free_tier -from qSpecification s, qFreeTier f -WHERE s.fullCode=f.code - -UNION - -select s.catalogName as svc, s.component, s.fullCode as code, s.measureShort as unit -, s.priceWVat as price, (s.discount + s.serviceDiscount - s.discount*s.serviceDiscount/100) as discount -,s.serviceStart, s.serviceEnd, s.serviceHash, s.serviceCustomName, 0 -from qSpecification s -WHERE s.fullCode NOT IN () - - - - - - - - - - select 'WZ'||to_char(tenant.wzcode,'FM00000') as wz, vdc.name as vdc_name, - case - when vdc.name like '%-v1cl1%' then 'iaas.ngc.i29' - when vdc.name like '%-v1cl2%' then 'iaas.ngc.i31' - when vdc.name like '%-v1cl4%' then 'iaas.ngc.a28' - when vdc.name like '%-v1cl6%' then 'iaas.ngc.i28' - else '['||vdc.name||']' end as code, - sum(cpu_used/cpu_speed)/4. as core_h, - sum(mem_used)/1024/4. as gb_h, - capacity_resource.timestamp::date as dt - from ngcloud_ru.capacity_resource - join ngcloud_ru.vdc on capacity_resource.vdc_id=vdc.id - join ngcloud_ru.tenant on tenant.id=vdc.tenant_id - where tenant.wzcode = - AND capacity_resource.timestamp::date >= - AND capacity_resource.timestamp::date <= - group by tenant.wzcode, vdc.name, capacity_resource.timestamp::date - order by tenant.wzcode, vdc.name, capacity_resource.timestamp::date; - - - - #getTickCount() - request.startTickCount# - - - - - - - select 'WZ'||to_char(tenant.wzcode,'FM00000') as wz, vdc.name as vdc_name, - case - WHEN vdc.name LIKE '%-v1cl1%' THEN 'iaas.ngc.i29' - WHEN vdc.name LIKE '%-v1cl2%' THEN 'iaas.ngc.i31' - WHEN vdc.name LIKE '%-v1cl4%' THEN 'iaas.ngc.a28' - WHEN vdc.name LIKE '%-v1cl6%' THEN 'iaas.ngc.i28' - ELSE '['||vdc.name||']' END - || CASE - WHEN storage_profile_types.name ILIKE '%-SSD%' THEN '.ssd-m' - WHEN storage_profile_types.name ILIKE '%-SAS' THEN '.sas-m' - WHEN storage_profile_types.name ILIKE '%-SATA' THEN '.sata-m' - ELSE '.['|| storage_profile_types.name ||']' END - as code, - storage_profile_types.name, -- *** это не тип, а профайл - sum("limit")/1024./4 as gb_h_limit, -- 4 потому, что сбор раз в 15 минут - sum(used)/1024./4 as gb_h_used, - capacity_storage.timestamp::date as dt - from ngcloud_ru.capacity_storage - join ngcloud_ru.vdc on capacity_storage.vdc_id=vdc.id - join ngcloud_ru.tenant on tenant.id=vdc.tenant_id - join ngcloud_ru.storage_profile_types on capacity_storage.storage_profile_types_id=storage_profile_types.id - where tenant.wzcode = - AND capacity_storage.timestamp::date >= - AND capacity_storage.timestamp::date <= - group by tenant.wzcode, vdc.name, storage_profile_types.name, capacity_storage.timestamp::date - order by tenant.wzcode, vdc.name, storage_profile_types.name, capacity_storage.timestamp::date; - - - - - - - select /* *** ниже дублирование кода агрегации - round ceil etc, обратить внимание при правке */ - (sum(bucket_stat.usage_rgw_main_size_actual)/) as vol_B - ,ceil(sum(bucket_stat.usage_rgw_main_size_actual)//1024/1024/1024) as vol_GB - ,greatest(ceil(sum(bucket_stat.usage_rgw_main_size_actual)/ - /1024/1024/1024)- - case when bucket_stat.placement_id = 1 then 1 else 0 end, 0) as vol_GB_free_tier_subtracted /*считается не в точности так, как в функции*/ - ,case - when bucket_stat.placement_id = 1 then 'paas.s3.ceph' - when bucket_stat.placement_id = 2 then 'paas.s3.cphc' - else '['|| TO_CHAR(bucket_stat.placement_id,'FM9') ||']' end as code - --,bucket_stat.placement_id - ,'WZ'||lpad(bucket_stat.owner, 5, '0') as wz -- *** здесь не число, а строка, поэтому другая формула обратной сборки WZ - ,bucket_stat.timestamp_addition::date as dt - from s3billing.bucket_stat - join s3billing.placement ON s3billing.placement.id=bucket_stat.placement_id - join s3billing.bucket_info ON bucket_info.id=bucket_stat.bucket_id - where bucket_stat.placement_id in (1,2) - AND bucket_stat.owner = - AND bucket_stat.timestamp_addition::date >= - AND bucket_stat.timestamp_addition::date <= - group by bucket_stat.owner, bucket_stat.placement_id, bucket_stat.timestamp_addition::date - order by bucket_stat.owner, bucket_stat.placement_id, bucket_stat.timestamp_addition::date; - - - - #getTickCount() - request.startTickCount# - - - - - - - select - sum( - CASE WHEN - (category.name LIKE 'get%' - OR category.name LIKE 'head%' - OR category.name LIKE 'options%' - ) THEN ops ELSE 0 END - ) as get - ,ceil(sum( - CASE WHEN - (category.name LIKE 'get%' - OR category.name LIKE 'head%' - OR category.name LIKE 'options%' - ) THEN ops ELSE 0 END - )/10000) as get_10k - ,greatest(ceil(sum( - CASE WHEN - (category.name LIKE 'get%' - OR category.name LIKE 'head%' - OR category.name LIKE 'options%' - ) THEN ops ELSE 0 END - )/10000) - CASE WHEN bucket_info.placement_id = 1 THEN 10 ELSE 0 END, 0 - ) as get_10k_free_tier_subtracted - - ,sum( - CASE WHEN - (category.name LIKE 'put%' - OR category.name LIKE 'post%' - OR category.name LIKE 'patch%' - OR category.name LIKE 'list%' - ) THEN ops ELSE 0 END - ) as put - ,ceil(sum( - CASE WHEN - (category.name LIKE 'put%' - OR category.name LIKE 'post%' - OR category.name LIKE 'patch%' - OR category.name LIKE 'list%' - ) THEN ops ELSE 0 END - )/10000) as put_10k - ,greatest(ceil(sum( - CASE WHEN - (category.name LIKE 'put%' - OR category.name LIKE 'post%' - OR category.name LIKE 'patch%' - OR category.name LIKE 'list%' - ) THEN ops ELSE 0 END - )/10000) - CASE WHEN bucket_info.placement_id = 1 THEN 1 ELSE 0 END, 0 - ) as put_10k_free_tier_subtracted - - ,(sum(bytes_sent)) as bytes_sent - ,ceil(sum(bytes_sent)/1024/1024/1024) as bytes_sent_GB - ,greatest(ceil(sum(bytes_sent)/1024/1024/1024) - CASE WHEN bucket_info.placement_id = 1 THEN 100 ELSE 0 END, 0 - ) as bytes_sent_GB_free_tier_subtracted - ,case - when bucket_info.placement_id = 1 then 'paas.s3.ceph' - when bucket_info.placement_id = 2 then 'paas.s3.cphc' - else '['|| TO_CHAR(bucket_info.placement_id,'FM9') ||']' end as code --*** зачем делать там ключ bigint - ,bucket_info.placement_id - --category.name, bucket_info.name - ,'WZ'||lpad(user_info.name, 5, '0') as wz - ,usage_bucket_by_user.time::date as dt - from s3billing.usage_bucket_by_user - join s3billing.user_info on usage_bucket_by_user.user_id=user_info.id - join s3billing.category on usage_bucket_by_user.category_id=category.id - join s3billing.bucket_info on usage_bucket_by_user.bucketid=bucket_info.id - join s3billing.placement on bucket_info.placement_id=placement.id - where 1=1 - AND user_info.name = - AND usage_bucket_by_user.time::date >= - AND usage_bucket_by_user.time::date <= - group by user_info.name, bucket_info.placement_id, usage_bucket_by_user.time::date - order by user_info.name, bucket_info.placement_id, usage_bucket_by_user.time::date; - - - - #getTickCount() - request.startTickCount# - - - - - - - select dt, wz, code || '.vcpu-m' as code, core_h as raw_metric, core_h as metric from qComputing - union all - select dt, wz, code || '.ram-m' as code, gb_h as raw_metric, gb_h as metric from qComputing - union all - select dt, wz, code, gb_h_used as raw_metric, gb_h_used as metric from qStorage - union all - select dt, wz, code || '.vol-m', vol_b as raw_metric, vol_gb as metric from qS3Vol - union all - select dt, wz, code || '.get-m', get as raw_metric, get_10k as metric from qS3OpsTrf - union all - select dt, wz, code || '.put-m', put as raw_metric, put_10k as metric from qS3OpsTrf - union all - select dt, wz, code || '.trf-m', bytes_sent as raw_metric, bytes_sent_gb as metric from qS3OpsTrf - - order by dt, code - - - - - #getTickCount() - request.startTickCount# - - - - - - - - - - - - - SELECT - - s.code - m.code as m_code - s.svc - s.component - s.unit - s.price*(100-s.discount)/100 as discounted_price - s.price - s.discount - m.raw_metric - m.metric - s.serviceStart - s.serviceEnd - s.serviceHash - s.serviceCustomName - m.dt - s.free_tier - - FROM qSpec as s, qUnifiedMetric as m - WHERE m.wz= - AND s.code=m.code - AND s.serviceStart <= m.dt - AND (m.dt <= s.serviceEnd OR s.serviceEnd IS NULL) - - UNION - SELECT NULL, m.code, NULL, NULL, NULL, NULL, NULL, NULL, m.raw_metric, m.metric, NULL, NULL, NULL, NULL, m.dt, 0 - FROM qUnifiedMetric as m where m.code NOT IN () - - UNION - SELECT s.code, NULL, s.svc, s.component, s.unit, s.price*(100-s.discount)/100, s.price, s.discount, NULL, NULL, s.serviceStart, s.serviceEnd, s.serviceHash, s.serviceCustomName, NULL, 0 - FROM qSpec as s where s.code NOT IN () - - order by m.dt, s.code - - - - SELECT - - code - m_code - svc - component - unit - discounted_price - sum(raw_metric) as raw_metric - sum(metric) as metric - sum(metric) - free_tier as chargeable_metric - (sum(metric) - free_tier)*discounted_price as charge - serviceStart - serviceEnd - serviceHash - serviceCustomName - free_tier - year(dt) as year - month(dt) as month - - from qChargeDaily - group by code, m_code, svc, serviceCustomName, component, unit, discounted_price, serviceStart, ServiceEnd, serviceHash, free_tier, year(dt), month(dt) - order by year(dt), month(dt), code, m_code - - - - - - - - - - - - - - - #getTickCount() - request.startTickCount# - - - - - #getTickCount() - request.startTickCount# - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/research/payg-sso-full/mod/payg.cfm.1.bak b/research/payg-sso-full/mod/payg.cfm.1.bak deleted file mode 100644 index 57c4604..0000000 --- a/research/payg-sso-full/mod/payg.cfm.1.bak +++ /dev/null @@ -1,346 +0,0 @@ - - - - - - - - - - - - - - - - - - - - #getTickCount() - request.startTickCount# - - - - - - - - -select catalogName as svc, component, fullCode as code, measureShort as unit -, priceWVat as price, (discount + serviceDiscount - discount*serviceDiscount/100) as discount -,serviceStart, serviceEnd -from qSpecification - - - - - - select 'WZ'||to_char(tenant.wzcode,'FM00000') as wz, vdc.name as vdc_name, - case - when vdc.name like '%-v1cl1%' then 'iaas.ngc.i29' - when vdc.name like '%-v1cl2%' then 'iaas.ngc.i31' - when vdc.name like '%-v1cl4%' then 'iaas.ngc.a28' - when vdc.name like '%-v1cl6%' then 'iaas.ngc.i28' - else '['||vdc.name||']' end as code, - sum(cpu_used/cpu_speed)/4. as core_h, - sum(mem_used)/1024/4. as gb_h - from ngcloud_ru.capacity_resource - join ngcloud_ru.vdc on capacity_resource.vdc_id=vdc.id - join ngcloud_ru.tenant on tenant.id=vdc.tenant_id - where tenant.wzcode = - AND capacity_resource.timestamp >= - AND capacity_resource.timestamp < - group by tenant.wzcode, vdc.name - order by tenant.wzcode, vdc.name; - - - - #getTickCount() - request.startTickCount# - - - - - - select max(capacity_resource.timestamp) as dt_load from ngcloud_ru.capacity_resource - - - - #getTickCount() - request.startTickCount# - - - - - - - - select 'WZ'||to_char(tenant.wzcode,'FM00000') as wz, vdc.name as vdc_name, - case - WHEN vdc.name LIKE '%-v1cl1%' THEN 'iaas.ngc.i29' - WHEN vdc.name LIKE '%-v1cl2%' THEN 'iaas.ngc.i31' - WHEN vdc.name LIKE '%-v1cl4%' THEN 'iaas.ngc.a28' - WHEN vdc.name LIKE '%-v1cl6%' THEN 'iaas.ngc.i28' - ELSE '['||vdc.name||']' END - || CASE - WHEN storage_profile_types.name ILIKE '%-SSD%' THEN '.ssd-m' - WHEN storage_profile_types.name ILIKE '%-SAS' THEN '.sas-m' - WHEN storage_profile_types.name ILIKE '%-SATA' THEN '.sata-m' - ELSE '.['|| storage_profile_types.name ||']' END - as code, - storage_profile_types.name, -- *** это не тип, а профайл - sum("limit")/1024./4 as gb_h_limit, -- 4 потому, что сбор раз в 15 минут - sum(used)/1024./4 as gb_h_used - from ngcloud_ru.capacity_storage - join ngcloud_ru.vdc on capacity_storage.vdc_id=vdc.id - join ngcloud_ru.tenant on tenant.id=vdc.tenant_id - join ngcloud_ru.storage_profile_types on capacity_storage.storage_profile_types_id=storage_profile_types.id - where tenant.wzcode = - AND capacity_storage.timestamp >= - AND capacity_storage.timestamp < - group by tenant.wzcode, vdc.name, storage_profile_types.name - order by tenant.wzcode, vdc.name, storage_profile_types.name; - - - select max(capacity_storage.timestamp) as dt_load from ngcloud_ru.capacity_storage - - - - #getTickCount() - request.startTickCount# - - - - - - - - select /* *** ниже дублирование кода агрегации - round ceil etc, обратить внимание при правке */ - (sum(bucket_stat.usage_rgw_main_size_actual)/) as vol_B - ,ceil(sum(bucket_stat.usage_rgw_main_size_actual)//1024/1024/1024) as vol_GB - ,greatest(ceil(sum(bucket_stat.usage_rgw_main_size_actual)/ - /1024/1024/1024)- - case when bucket_stat.placement_id = 1 then 1 else 0 end, 0) as vol_GB_free_tier_subtracted /*считается не в точности так, как в функции*/ - ,case - when bucket_stat.placement_id = 1 then 'paas.s3.ceph' - when bucket_stat.placement_id = 2 then 'paas.s3.cphc' - else '['|| TO_CHAR(bucket_stat.placement_id,'FM9') ||']' end as code - --,bucket_stat.placement_id - ,'WZ'||lpad(bucket_stat.owner, 5, '0') as wz -- *** здесь не число, а строка, поэтому другая формула обратной сборки WZ - - from s3billing.bucket_stat - join s3billing.placement ON s3billing.placement.id=bucket_stat.placement_id - join s3billing.bucket_info ON bucket_info.id=bucket_stat.bucket_id - where bucket_stat.placement_id in (1,2) - AND bucket_stat.owner = - AND bucket_stat.timestamp_addition >= - AND bucket_stat.timestamp_addition < - group by bucket_stat.owner, bucket_stat.placement_id - order by bucket_stat.owner, bucket_stat.placement_id; - - - - #getTickCount() - request.startTickCount# - - - - - - select max(timestamp_addition) as dt_load from s3billing.bucket_stat - - - - #getTickCount() - request.startTickCount# - - - - - -select - sum( - CASE WHEN - (category.name LIKE 'get%' - OR category.name LIKE 'head%' - OR category.name LIKE 'options%' - ) THEN ops ELSE 0 END - ) as get - ,ceil(sum( - CASE WHEN - (category.name LIKE 'get%' - OR category.name LIKE 'head%' - OR category.name LIKE 'options%' - ) THEN ops ELSE 0 END - )/10000) as get_10k - ,greatest(ceil(sum( - CASE WHEN - (category.name LIKE 'get%' - OR category.name LIKE 'head%' - OR category.name LIKE 'options%' - ) THEN ops ELSE 0 END - )/10000) - CASE WHEN bucket_info.placement_id = 1 THEN 10 ELSE 0 END, 0 - ) as get_10k_free_tier_subtracted - - ,sum( - CASE WHEN - (category.name LIKE 'put%' - OR category.name LIKE 'post%' - OR category.name LIKE 'patch%' - OR category.name LIKE 'list%' - ) THEN ops ELSE 0 END - ) as put - ,ceil(sum( - CASE WHEN - (category.name LIKE 'put%' - OR category.name LIKE 'post%' - OR category.name LIKE 'patch%' - OR category.name LIKE 'list%' - ) THEN ops ELSE 0 END - )/10000) as put_10k - ,greatest(ceil(sum( - CASE WHEN - (category.name LIKE 'put%' - OR category.name LIKE 'post%' - OR category.name LIKE 'patch%' - OR category.name LIKE 'list%' - ) THEN ops ELSE 0 END - )/10000) - CASE WHEN bucket_info.placement_id = 1 THEN 1 ELSE 0 END, 0 - ) as put_10k_free_tier_subtracted - - ,(sum(bytes_sent)) as bytes_sent - ,ceil(sum(bytes_sent)/1024/1024/1024) as bytes_sent_GB - ,greatest(ceil(sum(bytes_sent)/1024/1024/1024) - CASE WHEN bucket_info.placement_id = 1 THEN 100 ELSE 0 END, 0 - ) as bytes_sent_GB_free_tier_subtracted - - ,case - when bucket_info.placement_id = 1 then 'paas.s3.ceph' - when bucket_info.placement_id = 2 then 'paas.s3.cphc' - else '['|| TO_CHAR(bucket_info.placement_id,'FM9') ||']' end as code --*** зачем делать там ключ bigint - ,bucket_info.placement_id - --category.name, bucket_info.name - ,'WZ'||lpad(user_info.name, 5, '0') as wz - from s3billing.usage_bucket_by_user - join s3billing.user_info on usage_bucket_by_user.user_id=user_info.id - join s3billing.category on usage_bucket_by_user.category_id=category.id - join s3billing.bucket_info on usage_bucket_by_user.bucketid=bucket_info.id - join s3billing.placement on bucket_info.placement_id=placement.id - where 1=1 - AND user_info.name = - AND usage_bucket_by_user.time >= - AND usage_bucket_by_user.time < - group by user_info.name, bucket_info.placement_id - order by user_info.name, bucket_info.placement_id; - - - - #getTickCount() - request.startTickCount# - - - - - - select max(time) as dt_load from s3billing.usage_bucket_by_user - - - - #getTickCount() - request.startTickCount# - - - - - - -select wz, code || '.vcpu-m' as code, core_h as raw_metric, core_h as metric, core_h as chargeable_metric from qComputing -union all -select wz, code || '.ram-m' as code, gb_h as raw_metric, gb_h as metric, gb_h as chargeable_metric from qComputing -union all -select wz, code, gb_h_used as raw_metric, gb_h_used as metric, gb_h_used as chargeable_metric from qStorage -union all -select wz, code || '.vol-m', vol_b as raw_metric, vol_gb as metric, vol_GB_free_tier_subtracted as chargeable_metric from qS3Vol -union all -select wz, code || '.get-m', get as raw_metric, get_10k as metric, get_10k_free_tier_subtracted as chargeable_metric from qS3OpsTrf -union all -select wz, code || '.put-m', put as raw_metric, put_10k as metric, put_10k_free_tier_subtracted as chargeable_metric from qS3OpsTrf -union all -select wz, code || '.trf-m', bytes_sent as raw_metric, bytes_sent_gb as metric, bytes_sent_gb_free_tier_subtracted as chargeable_metric from qS3OpsTrf - - - - #getTickCount() - request.startTickCount# - - - - - - - - - - - - -SELECT - - s.code - m.code as m_code - s.svc - s.component - m.chargeable_metric - s.unit - s.price*(100-s.discount)/100 as discounted_price - s.price*(100-s.discount)/100*m.chargeable_metric as charge - - - s.price - s.discount - m.raw_metric - m.metric - - - -FROM qSpec as s, qUnifiedMetric as m -WHERE m.wz= AND s.code=m.code -order by s.code - - - - #getTickCount() - request.startTickCount# - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/research/payg-sso-full/payg.cfm b/research/payg-sso-full/payg.cfm deleted file mode 100644 index 69140bd..0000000 --- a/research/payg-sso-full/payg.cfm +++ /dev/null @@ -1,174 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - Отчет по PAYG #request.auth.wz# - - - - - - - - - - -
    -Период с -по - - -
    - - - - - - - -Часов в периоде: #hours# Строк в отчете: #report.qCharge.recordCount#
    -Актуальность данных: -VCD Computing: #dateFormat(report.qComputingAge.dt_load,'YYYY-MM-DD')# #timeFormat(report.qComputingAge.dt_load,'HH:MM:SS')# -VCD Storage: #dateFormat(report.qStorageAge.dt_load,'YYYY-MM-DD')# #timeFormat(report.qStorageAge.dt_load,'HH:MM:SS')# -S3 хранение: #dateFormat(report.qS3VolAge.dt_load,'YYYY-MM-DD')# #timeFormat(report.qS3VolAge.dt_load,'HH:MM:SS')# -S3 операции и трафик: #dateFormat(report.qS3OpsTrfAge.dt_load,'YYYY-MM-DD')# #timeFormat(report.qS3OpsTrfAge.dt_load,'HH:MM:SS')# -Цены GPL
    - -
    - - - -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    АртикулКол-во (метрика) - Оплачиваемый объемЦена ₽ с НДССтоимость ₽ с НДС   УслугаКомпонентЕд.изм.
    #year#-#numberFormat(month,"00")#
    - #svc# - (#serviceCustomName#) - (не найдено в договоре) -
    #currentRow##code# (#m_code#)#metric##chargeable_metric##discounted_price##numberFormat(charge,".00")##svc##component##unit#
    Итого     #svc#:#NumFmt(acc,2)#
    - - - - - - - diff --git a/research/payg-sso-full/style/web.css b/research/payg-sso-full/style/web.css deleted file mode 100644 index 3463337..0000000 --- a/research/payg-sso-full/style/web.css +++ /dev/null @@ -1,321 +0,0 @@ -/*common elements*/ -html, body { - margin: 0; - padding: 0; - border: none; - height: 100%; -} -body, table, tr, td, th, input, select, textarea, button, form, ul, li, p { - font-size: 11px; - font-family: Tahoma, Verdana, Arial, Helvetica, sans-serif; - color:black; -} -form {margin: 0;padding:0;} -p {margin: 0.5em;} -table {border-collapse: collapse;} -img {border: none; margin: 0; vertical-align: middle;} -a, a:link, a:visited {color: #111111; text-decoration: underline;} -a:hover {color: #ff7600; text-decoration: underline;} - - -/* form elements */ -input {color: black; padding: 0 0.3em; } -input:read-only {color:#555; background-color:#f0f0f0;} -input:disabled {color:#555; background-color:#f0f0f0;} -select:disabled {color:#555;} -select option[disabled] { color:#ddd;} - -.detail .td input[type="text"]:disabled {border:1px solid #aaa; color:#555;} -.textbox-disabled {display:inline-block; border: 1px solid #aaa; color:#555; background-color:#f0f0f0; padding:.1em;} - -/*layout*/ - -div.vspacer {height:5px;} - -ul#mainmenu { - list-style-type:none; - padding:0; - margin:0; - display:block; - width:100%; -} -ul#mainmenu li { - text-align:right; - vertical-align:baseline; - padding:.2em 15px .2em 0 ; - margin:.3em 0; - background: url('../img/sarrow.gif') center right no-repeat; - font-size:120%; - font-weight:normal; - font-family: Verdana, Tahoma, Arial, Helvetica, sans-serif; - line-height: 1.1em; -} -ul#mainmenu li a{/*display:block;*/color:#cde;text-decoration:none;} -ul#mainmenu li a:hover{text-decoration:underline;} -ul#mainmenu li.selected {color: /*#4682b4;*/ #fa6 /*#00f*/;} -ul#mainmenu li.menu-item-ext {background: url('../img/external.gif') center right no-repeat;} -ul#mainmenu li.menu-item-noaction {background: none; color: #777;} -ul#mainmenu li.menu-title { - color: #111; - background:none white; - margin:.5em 0 0 0; - /*font-weight: bold;*/ - /*border:1px solid #ccc;*/ -} - -.page {display:table; table-layout: fixed; width:100%; height:100%;} - -.page-main {display:table-row; height:100%;} - -.page-footer {display:table-row; height:1px;} - -.menu {display:table-cell; width:150px; padding:0 0 0 .5em; background-color: #012; -border:1px solid red;} - -.content {display:table-cell; height:100%; vertical-align:top; padding: 3px;} - -.header {display:block; /*background:url('../img/portlet_dot.gif') center left no-repeat; padding-left:33px;*/ line-height: 21px; font-family: Verdana, Tahoma, sans-serif;} - -.control-bar {display:table; width:100%; color:/*#559*/white; /*border:1px solid #ccc;*/ height:19px; } -.header .control-bar a:link, .header .control-bar a:visited {color:white; padding: .2em .5em;} -.header .control-bar a:hover {background:white;color:#f90; padding: .2em .5em;} - -.title {display:table-cell; padding:1px 12px; vertical-align:middle; font-size:110%; background-color:#aaa/*#ccc*//*rgb(117,147,156)*/; /*color:#fa6;*/ border: 1px solid #bbb; border-right:none;} -.title b {color:#026;} - -.main-controls {display:table-cell; padding:0 7px; text-align:right; vertical-align: middle; background-color:#aaa/*rgb(117,147,156)*/; border: 1px solid #bbb; border-left:none;} - -.footer {height:21px; line-height: 21px; background-color:#e0e0e0; border:1px solid #bbb; text-align: right; vertical-align: middle; padding:0 3px;} - - -.table {display:table;} -.tr {display:table-row;} -.td, .th {display:table-cell; padding: 0 0.5em; vertical-align:top;} - - -.detail { - display:table; - width:100%; - margin: 3px 0; - border:1px solid #ddd; - border-collapse: collapse; -} -.detail .tr{ - line-height: 1.5em; -} -.detail .th { - display:table-cell; - text-align:right; - background-color:#e0e0e0; - padding:0.35em 0.5em; - border:none; - /*color: #5f5f5f;*/ - font-weight:normal; - vertical-align:top; - /*border:1px solid red;*/ -} -.detail .td { - display:table-cell; - background-color:#e7e7e7; - padding:0.2em 0.5em; - border:1px solid #ddd; - vertical-align:baseline; -} -.detail .td input, .detail .td select, .detail .td img, .detail .td button, .detail .td div, .detail .td span { - vertical-align: baseline; - margin: 0 0.3em; -} - -.detail .td label { - vertical-align: baseline; - margin: 0 0.3em 0 0; - line-height:normal; -} - -.detail .td input[type="checkbox"] { - vertical-align: top; margin: 0 0 0 0.3em; -} - -/*.detail .td input[type="text"] { - width: 97%; -}*/ - -.detail .td textarea { - color: black; - width: 97%; - margin-left: 0.2em; - padding: 0.2em 0.5em; -} -/***hack*/ -.detail .td a img {vertical-align:text-bottom;} - -.filter {background-color: #f7f7f7;} -.disabled, tr.disabled td, tr.disabled th {color: #888;} -.plate {background-color:#f0f0f0; padding:.1em; border:1px solid #ddd;} - -button, .button, .content a.button:link { - background: #fafafa; - -background: -webkit-linear-gradient(top, #fafafa 0%, #f4f4f4 40%,#e5e5e5 100%); - background:-webkit-linear-gradient(top, #dde 0%, #cde 40%,#bcd 100%); - border: 1px solid #aad; - padding: 1px 10px; - color: #black; - border-radius: 3px; - box-shadow: 0 1px 1px #ddd; - } - -button:disabled, .button:disabled, .maincontrol:disabled { - background: #fafafa; - background: -webkit-linear-gradient(top, #fafafa 0%,#f4f4f4 40%,#e5e5e5 100%); - border-color: #aaa; - color: #999; - box-shadow: none; -} - -button:not(:disabled):hover, -.button:not(:disabled):hover, -.maincontrol:not(:disabled):hover -{ - background: #fefefe; - background: -webkit-linear-gradient(top, #fefefe 0%,#f8f8f8 40%,#e9e9e9 100%); - box-shadow: 0 1px 3px rgba(7, 7, 7, 0.35); - border: 1px solid #aaa; - color: #ff7600; -} - -button:not(:disabled):active, .button:not(:disabled):active { - background: #f4f4f4; - background: -webkit-linear-gradient(top, #f4f4f4 0%,#efefef 40%,#dcdcdc 100%); - box-shadow: inset 0 1px 3px rgba(7, 7, 7, 0.35); -} - -button.maincontrol { - /*background-color:#c3c82f; - background:-webkit-linear-gradient(top, #cd6 0%, #cd6 40%,#bc5 100%);*/ - -background-color:#6fa0f0; - background-color:#6fa0f0; - background:-webkit-linear-gradient(top, #dde 0%, #cde 40%,#bcd 100%); - color: black; -} - -button.maincontrol:hover { - -} - - -/*не совсем аккуратно, при нажатии выглядит чуть иначе, чем кнопка*/ -.content button a, .content button a:link, .content button a:visited {color: #790; text-decoration:none; cursor:default;} -.content button:hover a, .content button:active {color: #ff7600; text-decoration:none; cursor:default;} -.content button.maincontrol a, .content button.maincontrol a:link,.content button.maincontrol a:visited {color: black;} -.content button.maincontrol:hover a, .content button.maincontrol:active a {color: #ff7600;} - -.submitnew { - background-image:url('../img/new.gif'); - width:13px; height:13px; border:none; padding:0; color:transparent; cursor:pointer; -} -.submitsave { - background-image:url('../img/save.gif'); - width:14px; height:14px; border:none; padding:0; color:transparent; cursor:pointer; -} -.submitdel { - background-image:url('../img/del.gif'); - width:13px; height:13px; border:none; padding:0; color:transparent; cursor:pointer; -} -.submitminus { - background-image:url('../img/minus.gif'); - width:13px; height:13px; border:none; padding:0; color:transparent; cursor:pointer; -} - -.content a, .content a:link, .content a:visited {color: #1111ee; text-decoration: none;} -.content a:hover {color: #ff7600; text-decoration: underline;} -.content h4 {margin-block-end: .5em;} -.content i {color:#555;} - -.cell {border: 1px solid #ddd; width:100%; padding: 0.2em;} - -.l, td.l, th.l{text-align: left;} -.r, td.r, th.r{text-align: right;} -.c, td.c, th.c{text-align: center;} -.nw {white-space: nowrap;} -.b {font-weight:bold;} -.i {font-style:italic;} -.wide {min-width:100%} -.wh100 {width:100%; height:100%;} -.table {display:table} -.tcell {display:table-cell} -.mid {vertical-align:middle} /*используется для заполнения ячейки таблицы якорем a href*/ - -.err {color: red;} -.warn {color: #f80;} - -a.up {background: url('../img/up.gif') center right no-repeat; padding-right: 7px;} -a.add {background: url('../img/add.gif') center right no-repeat; width:13px; height:13px; display:inline-block;} -a.edit {background: url('../img/edit.gif') center right no-repeat; width:13px; height:13px; display:inline-block;} -a.view {background: url('../img/view.gif') center right no-repeat; width:13px; height:13px; display:inline-block;} -a.del {background: url('../img/del.gif') center right no-repeat; width:13px; height:13px; display:inline-block;} - - -/* worktable classes */ - -table.worktable {border-collapse:separate; border-spacing:0; border-left:1px solid #ddd;} - -table.worktable th, -table.worktable td { - border-bottom: thin solid #ddd; - border-right: thin solid #ddd; -} -table.worktable thead th, -table.worktable thead td { - box-shadow: 0 -1px 0 0 #ddd; -} -table.worktable thead { - top: 0; - left: 0; - position: sticky; - z-index: 1; -} - -table.worktable td p {margin:0;} - -table.worktable thead tr td, table.worktable tr th{padding: 3px; background-color: #999;} - -table.worktable tr {height: 1.7em; background-color: white;} - -table.worktable tr td {padding:0.1em 0.3em; height:2em;} - -table.worktable tr:hover, table.worktable tr td:hover {background-color:#e5e5e5;} - -table.worktable th {font-weight:500; color:white;} - -/*double borders - not tested with border-collapse:separate*/ -table.worktable tbody tr td.lbd, -table.worktable thead tr td.lbd - {border-left: double #ccc;} - -table.worktable tbody tr td.rbd, -table.worktable thead tr td.rbd - {border-right: double #ccc;} - -table.worktable tbody tr td.tbd, -table.worktable thead tr td.tbd, -table.worktable tbody tr.tbd td, -table.worktable thead tr.tbd td - {border-top: double #ccc;} - -table.worktable tbody tr td.bbd, -table.worktable thead tr td.bbd, -table.worktable tbody tr.bbd td, -table.worktable thead tr.bbd td - {border-bottom: double #ccc;} - -table.worktable tr.hist {background-color: #f6f6f6;} -table.worktable tr.hist:hover, table.worktable tr.hist:hover td, table.worktable tr.comment:hover{background-color:#efefef;} -table.worktable tr.hist td.marked {background: #ddf5c0;} -table.worktable tr.hist:hover td.marked{background-color:#d0f0b9;} - - -@media print { - #mainmenu, .menu, .page-footer, .control-bar, .noprint {display:none;} -} - - - diff --git a/research/payg-sso/Application.cfc b/research/payg-sso/Application.cfc deleted file mode 100644 index d3f85af..0000000 --- a/research/payg-sso/Application.cfc +++ /dev/null @@ -1,522 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ), - DE( "https://" ), - DE( "http://" ) - ) & - cgi.http_host & - reReplace( - getDirectoryFromPath( arguments.template ), "([^\\/]+[\\/]){#local.requestDepth#}$", - "", - "one" - ) - ) /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - var requestData = GetHttpRequestData(); - var headers = requestData.headers; - - if (structKeyExists(headers,"authorization")) { // аутентификация для клиента ЛК - var authorizationHeader = headers.authorization; - if (authorizationHeader.startsWith("Bearer ")) { - var bearerToken = Mid(authorizationHeader, 8); - writeDump(bearerToken); - //var token=toString(binaryDecode(bearerToken, "base64")); - //writeDump(token); - //jwt = new lib.jwt(this.client_secret); - //lock scope="application" type="readonly" timeout="1" { - // var token_data = (jwt.decode(token, application.idpCertificate,"RS256")); - //} - var result=""; - //try { - var iamService = new http(method = "GET", charset = "utf-8", url = #this.iam_url#, timeout="5"); - iamService.addParam(type = "HEADER", name = "Accept", value = "application/json"); - iamService.addParam(type = "HEADER", name = "Authorization", value = "#authorizationHeader#"); //passthrough - //writedump(this.iamServiceUrl);abort; - var resp = iamService.send(); - //if (resp.status_code NEQ 200) throw("IDP response not OK"); - //writedump(resp);abort; - var prefix = resp.getPrefix(); - if (prefix.status_code NEQ 200) { - //writedump(this.iamServiceUrl); - //writedump(resp); - var iamStatusCode = (isValid("integer", prefix.status_code)) ? val(prefix.status_code) : 500; - throw(message="IAM error", detail="URL: #this.iam_url# Status Code: #prefix.status_code#"); - //abort; - //throw("IDP response not OK"); - } - result = prefix.filecontent; - var idpUserData=deserializeJson(result); - //request.auth.wz=idpUserData.userInfo.ClientId;//GUID! - - - request.auth.login = idpUserData.userInfo.login; - //request.usrUid=idpUserData.userInfo.contactId;//userId; //GUID! - //writedump(idpUserData);abort; - //} catch (e) { - /*if (fileExists("#GetDirectoryFromPath(GetCurrentTemplatePath())#/etc/local-debug")) { //true for local debug without IDP, etc/* does not go to repository - var result='{"accounts":[{"login":"","type":"telegram"}],"avatar":["d2d2b7ac-50af-432b-b7c4-f7d6561e288a"],"company":"ООО «НУБЕС»","companyId":"8ec70ac0-546d-42a7-8cff-339c8fb51a23","contactId":"983967a3-58c2-4cdd-84a5-8b427ccfac82","email":"smishchuk@nubes.ru","externalUser":false,"fio":{"fullName":"Мищук Сергей ","name":"Сергей","secondName":"","surname":"Мищук"},"groupIds":["94bf5be7-52f4-5c13-87c3-513786934685","ab12b6b8-0265-4683-a5c2-5e1a74a55216","aff008c3-7443-50f6-86e1-ecff3cd3b04d","d6000da0-c9aa-55eb-9882-f118b432730b","d89a33dc-3177-5854-9580-f7f860a5ab7c","ddfe2555-9ef4-42cb-9a2b-7f24e61e7747","df12926f-ecb8-5479-857f-6d291464baad","fda5c295-230a-5025-9797-b8b4e99e08aa","2be9b4b6-94d1-59f9-a649-cab228d82169","64fe6f5c-a91b-5fea-a7ca-d9823701ebd4"],"integration":{"serviceId":""},"login":"smishchuk@nubes.ru","mobilePhone":[],"position":"","userId":"d72530e1-66a4-412f-a046-38153c4e5405"}';} - */ - //writedump(resp); - //return representationOf( {"exception"=e} ); - //rethrow(e); - //} - } - } else { - //точно мы так не словим дедлок ВПР - lock scope="session" type="readonly" timeout="3" { - if (structKeyExists(session, "auth")) { - request.auth = structCopy(session.auth); - } - } - if (structKeyExists(request, "auth") AND Now() < request.auth.exp) { - - if (dateDiff("s", request.auth.iat, Now()) > request.auth.refresh_expires_in/1.5 AND CGI.REQUEST_METHOD EQ "GET") { - // доля времени, после которой происходит рефреш, прибита гвоздями - // По всей видимости, рефреш будет ломать обращения POSТ - // В связи с чем предлагается дождаться GET - // Приклеивать query_string как-то не пришлось, браузер сам справляется - - // refresh - var idp = new lib.oauth2(this.client_id, this.client_secret, this.auth_endpoint, this.access_token_endpoint, this.redirect_uri); - var resp = idp.refreshAccessTokenRequest(request.auth.refresh_token); - - lock scope="application" type="readonly" timeout="1" { - idpCertificate = application.idpCertificate; - } - - var auth = parseIdpresponse(resp, idpCertificate, this.client_secret); //все эти сложности из-за опасений насчет многопоточности и блокировок - - lock scope="session" type="exclusive" timeout="1" { - session.auth = auth; - } - request.auth = auth; - writeDump(auth); - } - } else { - // сессии SSO нет - // или токен протух - request.auth_state = createGUID().toString(); - lock scope="session" type="exclusive" timeout="1" { - structDelete(session, "auth"); // зачистили сессию от данных SSO - session.auth_state = request.auth_state; - } - - var idp = new lib.oauth2(this.client_id, this.client_secret, this.auth_endpoint, this.access_token_endpoint, this.redirect_uri); - /* - часто появляется ошибка: после неудачного запроса к IDP - в адресной строке браузера остается все, с чем его редиректил IDP, - а повторный запрос с этим контентом уже не работает. Получаем 400 Bad Request - - может быть, KeyCloak так защищается от циклических редиректов - Не очень понятно, как предотвратить повторный запрос (можно, конечно, использовать сессию, но странно) - */ - //writeOutput(getTickCount()-request.startTickCount); - if(structKeyExists(url, "code")) { //обрабатываем редирект от IDP *** наличие поля code в URL введет нас в заблуждение, - - try { - //writeOutput("idp.makeAccessTokenRequest"); - //writeOutput(getTickCount()-request.startTickCount); - var resp = idp.makeAccessTokenRequest(url.code); - - lock scope="application" type="readonly" timeout="1" { - idpCertificate = application.idpCertificate; - } - - var auth = parseIdpresponse(resp, idpCertificate, this.client_secret); //все эти сложности из-за опасений насчет многопоточности и блокировок - - lock scope="session" type="exclusive" timeout="1" { - session.auth = auth; - } - //request.auth = auth; - location(this.redirect_uri, false); - } catch (e) {echo('
    ****************** #e.message# : #e.detail# **************');} - - } else { //отправляем браузер к IDP - var strURL =idp.buildRedirectToAuthURL({"scope":'openid profile email',"state":request.auth_state,"allow_signup":false}); - echo('self-made Auth link (idp) #strURL#

    '); - location(strUrl,false); - } - - } - - } - - //request.logout_url = ""; - if (structKeyExists(request, "auth") AND structKeyExists(request.auth, "id_token")) { - request.logout_url = "#this.logout_endpoint#?id_token_hint=#request.auth.id_token#&post_logout_redirect_uri=#this.redirect_uri#?logout"; - // внимание, здесь подразумевается, что redirect_uri не содержит query_string - // после возвращения от IDP нам нужно будет зачистить сессию SSO, сделаем это по слову logout - } - - request.auth.wz='WZ01348'; //******************* -
    - - - - - - - - - - - ---> - - - - - -
    - - - - - - - - - - - - - - - - - - - - - - - - select value as stand from config - where name='STAND' - - - - - - - - - - - - - - - - - - - select 1; - - - - - - - - - - - - - - - - - - - - select 2; - - - - - - - - - - - - - - - - - - - //https://keycloak.nubes.ru/admin - //https://keycloak.nubes.ru/realms/SSH_CA/account //smishchuk@mgmt.nubes.ru - //https://stackoverflow.com/questions/28658735/what-are-keycloaks-oauth2-openid-connect-endpoints - //http://https://keycloak.nubes.ru/realms/SSH_CA/.well-known/openid-configuration - - //https://keycloak.nubes.ru/realms/cloud/.well-known/openid-configuration тут ссылка - //https://keycloak.nubes.ru/realms/cloud/protocol/openid-connect/certs тут серт - //только надо его обрамить -----BEGIN CERTIFICATE----- - private function getIdpCertificate(certUrl) { - - var httpService = new http(); - httpService.setMethod( "get" ); - httpService.setCharset( "utf-8" ); - httpService.setUrl(arguments.certUrl); - - var result = httpService.send().getPrefix(); - var status_code = result.ResponseHeader['Status_Code']; - - if ('200' == status_code) { - var content = deserializeJson(result.FileContent); - //мы подразумеваем структуру конкретно KeyCloak - var key = ""; - for (key in content.keys) { - if ("RSA" == key.kty AND "sig" == key.use) { - return "-----BEGIN CERTIFICATE-----" & key.x5c[1] & "-----END CERTIFICATE-----"; - } - } - throw(message="RSA cetificate not found"); - } else { - throw (message="Cannot obtain IDP certificate, request failed", detail="Status_Code #status_code#"); - } - } - - private struct function parseIdpResponse(struct resp, string certificate, string jwt_secret) { - - var auth = structNew("linked"); - var data = deserializeJson(arguments.resp.content); - var jwt = new lib.jwt(arguments.jwt_secret); - - auth.expires_in = data.expires_in; - auth.refresh_expires_in = data.refresh_expires_in; - auth.access_token = data.access_token; - auth.refresh_token = data.refresh_token; - auth.id_token = data.id_token; - auth.token_type = data.token_type; - - //idp_response_content = structCopy(data); //for debug - var token_data = (jwt.decode(data.access_token, arguments.certificate,"RS256")); - - auth.session_state = token_data.session_state; - auth.sid = token_data.sid; - auth.auth_time = token_data.auth_time; - auth.exp = token_data.exp; - auth.iat = token_data.iat; - - auth.login = token_data.preferred_username; - auth.wz = token_data.ClientID; - auth.fullname = token_data.name; - auth.groups = token_data.groups; - //auth.token_data = structCopy(token_data); //for debug - - return (auth); - } - - - - - - - - - - - - -
    \ No newline at end of file diff --git a/research/payg-sso/lib_jwt.cfc b/research/payg-sso/lib_jwt.cfc deleted file mode 100644 index e86a066..0000000 --- a/research/payg-sso/lib_jwt.cfc +++ /dev/null @@ -1,262 +0,0 @@ -component { - - variables.algorithmMap = { - HS256: 'HmacSHA256', - HS384: 'HmacSHA384', - HS512: 'HmacSHA512', - RS256: 'SHA256withRSA', - RS384: 'SHA384withRSA', - RS512: 'SHA512withRSA', - ES256: 'SHA256withECDSA', - ES384: 'SHA384withECDSA', - ES512: 'SHA512withECDSA' - }; - - public any function init() { - variables.encodingUtils = new encodingUtils(); - variables.jss = createObject( 'java', 'java.security.Signature' ); - variables.messageDigest = createObject( 'java', 'java.security.MessageDigest' ); - return this; - } - - public string function encode( - required struct payload, - required any key, - required string algorithm, - struct headers = { } - ) { - if ( !algorithmMap.keyExists( algorithm ) ) { - throw( - type = 'jwtcfml.InvalidAlgorithm', - message = 'Invalid JWT Algorithm.', - detail = 'The passed in algorithm is not supported.' - ); - } - - var header = { }; - header.append( headers ); - header.append( { - 'typ': 'JWT', - 'alg': algorithm - } ); - - var duplicatedPayload = duplicate( payload ); - for ( var claim in [ 'iat', 'exp', 'nbf' ] ) { - if ( duplicatedPayload.keyExists( claim ) && isDate( duplicatedPayload[ claim ] ) ) { - duplicatedPayload[ claim ] = encodingUtils.convertDateToUnixTimestamp( duplicatedPayload[ claim ] ); - } - } - - var stringToSignParts = [ - encodingUtils.binaryToBase64Url( charsetDecode( serializeJSON( header ), 'utf-8' ) ), - encodingUtils.binaryToBase64Url( charsetDecode( serializeJSON( duplicatedPayload ), 'utf-8' ) ) - ]; - var stringToSign = stringToSignParts.toList( '.' ); - - return stringToSign & '.' & encodingUtils.binaryToBase64Url( sign( stringToSign, key, algorithm ) ); - } - - public struct function decode( - required string token, - any key, - any algorithms = [ ], - struct claims = { }, - boolean verify = true - ) { - var parts = listToArray( token, '.' ); - - if ( arrayLen( parts ) != 3 ) { - throw( - type = 'jwtcfml.InvalidToken', - message = 'Invalid JWT.', - detail = 'The passed in token does not have three `.` delimited parts.' - ); - } - - algorithms = isArray( algorithms ) ? algorithms : [ algorithms ]; - - var decoded = { - header: deserializeJSON( charsetEncode( encodingUtils.base64UrlToBinary( parts[ 1 ] ), 'utf-8' ) ), - payload: deserializeJSON( charsetEncode( encodingUtils.base64UrlToBinary( parts[ 2 ] ), 'utf-8' ) ) - }; - - if ( verify ) { - if ( - !algorithms.find( decoded.header.alg ) || - !algorithmMap.keyExists( decoded.header.alg ) - ) { - throw( - type = 'jwtcfml.InvalidAlgorithm', - message = 'Unsupported or invalid algorithm', - detail = 'The passed in token does not have an algorithm declaration or its declared algorithm (#decoded.header.alg#) does not match the specified algorithms of #serializeJSON( algorithms )#.' - ); - } - - var stringToSign = parts[ 1 ] & '.' & parts[ 2 ]; - var signature = encodingUtils.base64UrlToBinary( parts[ 3 ] ); - - if ( - !verifySignature( - stringToSign, - key, - signature, - decoded.header.alg - ) - ) { - throw( - type = 'jwtcfml.InvalidSignature', - message = 'Signature is Invalid', - detail = 'The signature of the passed in token is invalid.' - ); - } - - var baseClaims = { - 'exp': true, - 'nbf': true - }; - baseClaims.append( claims ); - verifyClaims( decoded.payload, baseClaims ); - } - - for ( var claim in [ 'iat', 'exp', 'nbf' ] ) { - if ( decoded.payload.keyExists( claim ) ) { - decoded.payload[ claim ] = encodingUtils.convertUnixTimestampToDate( decoded.payload[ claim ] ); - } - } - - return decoded.payload; - } - - public struct function getHeader( required string token ) { - return deserializeJSON( charsetEncode( encodingUtils.base64UrlToBinary( listFirst( token, '.' ) ), 'utf-8' ) ); - } - - public function parsePEMEncodedKey( required string pemKey ) { - return encodingUtils.parsePEMEncodedKey( pemKey ); - } - - public function parseJWK( required struct jwk ) { - return encodingUtils.parseJWK( jwk ); - } - - private function sign( message, key, algorithm ) { - if ( left( algorithm, 1 ) == 'H' ) { - var sig = binaryDecode( - hmac( - message, - key, - algorithmMap[ algorithm ], - 'utf-8' - ), - 'hex' - ); - } else { - if ( isSimpleValue( key ) ) { - key = encodingUtils.parsePEMEncodedKey( key ); - } else if ( isStruct( key ) ) { - key = encodingUtils.parseJWK( key ); - } - - var jssInstance = variables.jss.getInstance( algorithmMap[ algorithm ] ); - jssInstance.initSign( key ); - jssInstance.update( charsetDecode( message, 'utf-8' ) ); - var sig = jssInstance.sign(); - if ( left( algorithm, 1 ) == 'E' ) { - sig = encodingUtils.convertDERtoP1363( sig, algorithm ); - } - } - return sig; - } - - private function verifySignature( message, key, signature, algorithm ) { - if ( left( algorithm, 1 ) == 'H' ) { - var sig = binaryDecode( - hmac( - message, - key, - algorithmMap[ algorithm ], - 'utf-8' - ), - 'hex' - ); - return MessageDigest.isEqual( signature, sig ); - } - - if ( left( algorithm, 1 ) == 'E' ) { - signature = encodingUtils.convertP1363ToDER( signature ); - } - - if ( isSimpleValue( key ) ) { - key = encodingUtils.parsePEMEncodedKey( key ); - } else if ( isStruct( key ) ) { - key = encodingUtils.parseJWK( key ); - } - - var jssInstance = variables.jss.getInstance( algorithmMap[ algorithm ] ); - jssInstance.initVerify( key ); - jssInstance.update( charsetDecode( message, 'utf-8' ) ); - return jssInstance.verify( signature ); - } - - private function verifyClaims( payload, claims ) { - if ( - structKeyExists( payload, 'exp' ) - && !verifyDateClaim( payload.exp, claims.exp, -1 ) - ) { - throw( - type = 'jwtcfml.ExpiredSignature', - message = 'Token has expired', - detail = 'The passed in token has expired.' - ); - } - - if ( - structKeyExists( payload, 'nbf' ) - && !verifyDateClaim( payload.nbf, claims.nbf, 1 ) - ) { - throw( - type = 'jwtcfml.NotBeforeException', - message = 'Token is not valid', - detail = 'The passed in token has not yet become valid.' - ); - } - - - - if ( structKeyExists( claims, 'iss' ) ) { - if ( !structKeyExists( payload, 'iss' ) || compare( payload.iss, claims.iss ) != 0 ) { - throw( - type = 'jwtcfml.InvalidIssuer', - message = 'Token has an invalid issuer', - detail = 'The passed in token either does not specify an issuer or the claimed issuer is not valid.' - ); - } - } - - if ( structKeyExists( claims, 'aud' ) ) { - var audArray = isArray( claims.aud ) ? claims.aud : [ claims.aud ]; - if ( !structKeyExists( payload, 'aud' ) || !audArray.find( payload.aud ) ) { - throw( - type = 'jwtcfml.InvalidAudience', - message = 'Token has an invalid audience', - detail = 'The passed in token either does not specify an audience or the claimed audience is not valid.' - ); - } - } - } - - private function verifyDateClaim( payloadDate, claim, failState ) { - var pd = encodingUtils.convertUnixTimestampToDate( payloadDate ); - var cd = claim; - if ( !isBoolean( cd ) || cd ) { - if ( isNumeric( cd ) ) { - cd = encodingUtils.convertUnixTimestampToDate( cd ); - } else if ( !isDate( cd ) ) { - cd = now(); - } - return dateCompare( pd, cd ) != failState; - } - return true; - } - -} diff --git a/research/payg-sso/lib_oauth2.cfc b/research/payg-sso/lib_oauth2.cfc deleted file mode 100644 index 41fa80d..0000000 --- a/research/payg-sso/lib_oauth2.cfc +++ /dev/null @@ -1,193 +0,0 @@ -/** -* @displayname oauth2 -* @output false -* @hint The oauth2 object. -* @author Matt Gifford -* @website https://www.monkehworks.com -* @purpose A ColdFusion Component to manage authentication using the OAuth2 protocol. -**/ -component accessors="true"{ - - property name="client_id" type="string"; - property name="client_secret" type="string"; - property name="authEndpoint" type="string"; - property name="accessTokenEndpoint" type="string"; - property name="redirect_uri" type="string"; - property name="PKCE" type="struct"; - - /** - * I return an initialized oauth2 object instance. - * @client_id The client ID for your application. - * @client_secret The client secret for your application. - * @authEndpoint The URL endpoint that handles the authorisation. - * @accessTokenEndpoint The URL endpoint that handles retrieving the access token. - * @redirect_uri The URL to redirect the user back to following authentication. - **/ - public oauth2 function init( - required string client_id, - required string client_secret, - required string authEndpoint, - required string accessTokenEndpoint, - required string redirect_uri - ){ - setClient_id( arguments.client_id ); - setClient_secret( arguments.client_secret ); - setAuthEndpoint( arguments.authEndpoint ); - setAccessTokenEndpoint( arguments.accessTokenEndpoint ); - setRedirect_uri( arguments.redirect_uri ); - return this; - } - - /** - * I return the URL as a string which we use to redirect the user for authentication. | - * The method will handle the client_id and redirect_uri values for you. Anything else you need to send to the provider in the URL can be sent via the parameters argument. - * @parameters A structure containing key / value pairs of data to be included in the URL string. - **/ - public string function buildRedirectToAuthURL( struct parameters={} ) { - var objAuthBuilder = new utils.authStringBuilder( - authEndpoint = getAuthEndpoint(), - client_id = getClient_id(), - redirect_uri = getRedirect_uri() - ); - return objAuthBuilder.withParams( arguments.parameters ).get(); - } - - /** - * I return an instance of the authStringBuilder class. The actual link string is generated by then calling the .get() method on the class object. - * The method will handle the client_id and redirect_uri values for you. Anything else you need to send to the provider in the URL can be sent via the parameters argument. - * @parameters A structure containing key / value pairs of data to be included in the URL string. - **/ - public authStringBuilder function buildRedirectToAuthURLWithBuilder( struct parameters={} ) { - var objAuthBuilder = new utils.authStringBuilder( - authEndpoint = getAuthEndpoint(), - client_id = getClient_id(), - redirect_uri = getRedirect_uri() - ); - return objAuthBuilder.withParams( arguments.parameters ); - } - - /** - * I make the HTTP request to obtain the access token. - * @code The code returned from the authentication request. - * @formfields An optional array of structs for the provider requirements to add new form fields. - * @headers An optional array of structs to add custom headers to the request if required. - **/ - public struct function makeAccessTokenRequest( - required string code, - array formfields = [], - array headers = [] - ){ - var stuResponse = {}; - var httpService = new http(); - httpService.setMethod( "post" ); - httpService.setCharset( "utf-8" ); - httpService.setUrl( getAccessTokenEndpoint() ); - if( arrayLen( arguments.headers ) ){ - for( var item in arguments.headers ){ - httpService.addParam( type="header", name=item[ 'name' ], value=item[ 'value' ] ); - } - } - httpService.addParam( type="formfield", name="client_id", value=getClient_id() ); - httpService.addParam( type="formfield", name="client_secret", value=getClient_secret() ); - httpService.addParam( type="formfield", name="code", value=arguments.code ); - httpService.addParam( type="formfield", name="redirect_uri", value=getRedirect_uri() ); - httpService.addParam( type="formfield", name="grant_type", value='authorization_code' ); - if( arrayLen( arguments.formfields ) ){ - for( var item in arguments.formfields ){ - httpService.addParam( type="formfield", name=item[ 'name' ], value=item[ 'value' ] ); - } - } - var result = httpService.send().getPrefix(); - if( '200' == result.ResponseHeader[ 'Status_Code' ] ) { - stuResponse.success = true; - stuResponse.content = result.FileContent; - } else { - stuResponse.success = false; - stuResponse.content = result.Statuscode; - } - //dump(var=formfields,label="form fields"); - return stuResponse; - } - - /** - * I make the HTTP request to refresh the access token. - * @refresh_token The refresh_token returned from the accessTokenRequest request. - **/ - public struct function refreshAccessTokenRequest( - required string refresh_token, - array formfields = [], - array headers = [] - ){ - var stuResponse = {}; - var httpService = new http(); - httpService.setMethod( "post" ); - httpService.setCharset( "utf-8" ); - httpService.setUrl( getAccessTokenEndpoint() ); - httpService.addParam( type="header", name="Content-Type", value="application/x-www-form-urlencoded" ); - if( arrayLen( arguments.headers ) ){ - for( var item in arguments.headers ){ - httpService.addParam( type="header", name=item[ 'name' ], value=item[ 'value' ] ); - } - } - httpService.addParam( type="formfield", name="client_id", value=getClient_id() ); - httpService.addParam( type="formfield", name="client_secret", value=getClient_secret() ); - httpService.addParam( type="formfield", name="refresh_token", value=arguments.refresh_token ); - httpService.addParam( type="formfield", name="grant_type", value="refresh_token" ); - if( arrayLen( arguments.formfields ) ){ - for( var item in arguments.formfields ){ - httpService.addParam( type="formfield", name=item[ 'name' ], value=item[ 'value' ] ); - } - } - var result = httpService.send().getPrefix(); - if( '200' == result.ResponseHeader[ 'Status_Code' ] ) { - stuResponse.success = true; - stuResponse.content = result.FileContent; - } else { - stuResponse.success = false; - stuResponse.content = result.Statuscode; - } - return stuResponse; - } - - /** - * I return a string containing any extra URL parameters to concatenate and pass through when authenticating. - * @argScope A structure containing key / value pairs of data to be included in the URL string. - **/ - public string function buildParamString( struct argScope={} ) { - var strURLParam = '?'; - if( structCount( arguments.argScope ) ) { - var intCount = 1; - for( var key in arguments.argScope ) { - if( listLen( strURLParam ) && intCount > 1 ) { - strURLParam = strURLParam & '&'; - } - strURLParam = strURLParam & lcase( key ) & '=' & trim( arguments.argScope[ key ] ); - intCount++; - } - } - return strURLParam; - } - - /** - * Returns the properties as a struct - */ - public struct function getMemento(){ - var result = {}; - for( var thisProp in getMetaData( this ).properties ){ - if( structKeyExists( variables, thisProp[ 'name' ] ) ){ - result[ thisProp[ 'name' ] ] = variables[ thisProp[ 'name' ] ]; - } - } - return result; - } - - /** - * Generates a struct containing the code verifier and code challenge - * values for use with the PKCE extension. - */ - public struct function generatePKCE(){ - var objPKCE = new utils.deps.pkce.pkce(); - return objPKCE.generatePKCE(); - } - -} \ No newline at end of file