1890 lines
299 KiB
JavaScript
1890 lines
299 KiB
JavaScript
import crypto from "node:crypto";
|
||
import dns from "node:dns/promises";
|
||
import { execFile } from "node:child_process";
|
||
import fs from "node:fs";
|
||
import fsp from "node:fs/promises";
|
||
import http from "node:http";
|
||
import net from "node:net";
|
||
import dgram from "node:dgram";
|
||
import path from "node:path";
|
||
import { fileURLToPath } from "node:url";
|
||
import { promisify } from "node:util";
|
||
import AdmZip from "adm-zip";
|
||
import express from "express";
|
||
import multer from "multer";
|
||
import QRCode from "qrcode";
|
||
import { LOCAL_INSTANCE_ID, openStorage } from "./storage.js";
|
||
import { generateTotpSecret, verifyTotp, otpauthUri, generateRecoveryCodes } from "./totp.js";
|
||
|
||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||
const packageMetadata = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8"));
|
||
const appVersion = process.env.APP_VERSION || packageMetadata.version;
|
||
const publicDir = path.join(__dirname, "public");
|
||
const dataDir = path.resolve(process.env.DATA_DIR || "/data");
|
||
const sitesDir = path.join(dataDir, "sites");
|
||
const uploadDir = path.join(dataDir, ".uploads");
|
||
const caddyDir = path.join(dataDir, "caddy");
|
||
const iconsDir = path.join(dataDir, "icons");
|
||
const logsDir = path.join(dataDir, "logs");
|
||
const backupsDir = path.join(dataDir, "backups");
|
||
const defaultSiteDir = path.join(dataDir, "default-site");
|
||
const certificatesRoot = path.join(dataDir, "certificates");
|
||
const customCertificatesDir = path.join(certificatesRoot, "custom");
|
||
const managedCertificatesDir = path.join(certificatesRoot, "managed");
|
||
const certificateExportsDir = path.join(certificatesRoot, "exports");
|
||
const accessLogPath = path.join(logsDir, "access.json");
|
||
const activityLogPath = path.join(logsDir, "activity.jsonl");
|
||
const certificateDir = path.join(managedCertificatesDir, "certificates");
|
||
const iconCatalogPath = path.join(iconsDir, "catalog.json");
|
||
const caddyfilePath = path.join(caddyDir, "Caddyfile");
|
||
const execFileAsync = promisify(execFile);
|
||
const scryptAsync = promisify(crypto.scrypt);
|
||
const adminPort = numberEnv("ADMIN_PORT", 8080);
|
||
const minPort = numberEnv("SITE_PORT_MIN", 9000);
|
||
const maxPort = numberEnv("SITE_PORT_MAX", 9099);
|
||
const adminUser = process.env.ADMIN_USERNAME || "admin";
|
||
const adminPassword = process.env.ADMIN_PASSWORD || "change-this-password";
|
||
const sessionSecret = process.env.SESSION_SECRET || crypto.createHash("sha256").update(`${adminUser}:${adminPassword}`).digest("hex");
|
||
const scheduledBackupPassword = process.env.BACKUP_PASSWORD || "";
|
||
const activeServers = new Map();
|
||
const activeStreams = new Map();
|
||
let sites = [];
|
||
let proxies = [];
|
||
let users = [];
|
||
let redirects = [];
|
||
let streams = [];
|
||
let accessLists = [];
|
||
let groups = [];
|
||
let settings = {};
|
||
let publicIpState = { address: null, checkedAt: null, error: null };
|
||
let gatewayError = null;
|
||
let lastGatewayReload = null;
|
||
let caddyVersion = "Unknown";
|
||
const recentActivity = [];
|
||
const upstreamHealth = new Map();
|
||
const certificateStatusCache = new Map();
|
||
const loginAttempts = new Map();
|
||
let currentAuditActor = null;
|
||
const probeFailures = { gateway: 0, http: 0, https: 0 };
|
||
let iconCatalog = null;
|
||
let storage;
|
||
|
||
function recordActivity(message, status = "ok") {
|
||
const entry = { message, status, at: new Date().toISOString() };
|
||
recentActivity.unshift(entry);
|
||
recentActivity.splice(20);
|
||
try { storage?.recordActivity(message, status); } catch (error) { console.warn("Could not record SQLite activity event:", error.message); }
|
||
fsp.appendFile(activityLogPath, `${JSON.stringify(entry)}\n`).catch(() => {});
|
||
try { storage?.recordAudit(message, status, null, currentAuditActor); } catch (error) { console.warn("Could not record SQLite audit event:", error.message); }
|
||
}
|
||
|
||
async function directorySize(directory) {
|
||
let total = 0;
|
||
const entries = await fsp.readdir(directory, { withFileTypes: true }).catch(error => error.code === "ENOENT" ? [] : Promise.reject(error));
|
||
for (const entry of entries) {
|
||
const itemPath = path.join(directory, entry.name);
|
||
if (entry.isDirectory()) total += await directorySize(itemPath);
|
||
else if (entry.isFile()) total += (await fsp.stat(itemPath)).size;
|
||
}
|
||
return total;
|
||
}
|
||
|
||
function numberEnv(name, fallback) {
|
||
const value = Number.parseInt(process.env[name] || "", 10);
|
||
return Number.isInteger(value) ? value : fallback;
|
||
}
|
||
|
||
function safeEqual(a, b) {
|
||
const left = Buffer.from(String(a));
|
||
const right = Buffer.from(String(b));
|
||
return left.length === right.length && crypto.timingSafeEqual(left, right);
|
||
}
|
||
|
||
async function passwordRecord(password) {
|
||
const salt = crypto.randomBytes(16).toString("hex");
|
||
const hash = await scryptAsync(String(password), salt, 64);
|
||
return { algorithm: "scrypt", salt, hash: hash.toString("hex") };
|
||
}
|
||
|
||
async function passwordMatches(password, record) {
|
||
if (!record?.salt || !record?.hash) return false;
|
||
const hash = await scryptAsync(String(password), record.salt, 64);
|
||
return safeEqual(hash.toString("hex"), record.hash);
|
||
}
|
||
|
||
function publicUser(user) {
|
||
const { password, sessionVersion, mfaSecret, mfaPendingSecret, mfaRecoveryCodes, ...safe } = user;
|
||
return { ...safe, mfaEnabled: Boolean(user.mfaEnabled) };
|
||
}
|
||
|
||
function activeAdministrators() {
|
||
return users.filter(user => user.role === "administrator" && user.status === "active");
|
||
}
|
||
|
||
function slugify(value) {
|
||
return value.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 48);
|
||
}
|
||
|
||
function sign(value) {
|
||
return crypto.createHmac("sha256", sessionSecret).update(value).digest("hex");
|
||
}
|
||
|
||
function cookieMap(header = "") {
|
||
return Object.fromEntries(header.split(";").map(v => v.trim().split("=").map(decodeURIComponent)).filter(v => v.length === 2));
|
||
}
|
||
|
||
function sessionUser(req) {
|
||
const token = cookieMap(req.headers.cookie).webserver_session;
|
||
if (!token) return null;
|
||
const [userId, expires, sessionVersion, signature] = token.split(".");
|
||
const user = users.find(item => item.id === userId && item.status === "active");
|
||
if (!user || !expires || !sessionVersion || Number(expires) <= Date.now() || sessionVersion !== user.sessionVersion || !safeEqual(signature || "", sign(`${userId}.${expires}.${sessionVersion}`))) return null;
|
||
return user;
|
||
}
|
||
|
||
const saveSites = async () => storage.saveCollection("sites", sites);
|
||
const saveProxies = async () => storage.saveCollection("proxies", proxies);
|
||
const saveUsers = async () => storage.saveCollection("users", users);
|
||
const saveGroups = async () => storage.saveCollection("groups", groups);
|
||
const saveRedirects = async () => storage.saveCollection("redirects", redirects);
|
||
const saveStreams = async () => storage.saveCollection("streams", streams);
|
||
const saveAccessLists = async () => storage.saveCollection("access_lists", accessLists);
|
||
const saveSettings = async () => storage.saveSettings(settings);
|
||
|
||
async function clearDirectoryContents(directory) {
|
||
await fsp.mkdir(directory, { recursive: true });
|
||
let lastError = null;
|
||
for (let attempt = 0; attempt < 4; attempt++) {
|
||
lastError = null;
|
||
for (const entry of await fsp.readdir(directory, { withFileTypes: true })) {
|
||
try { await fsp.rm(path.join(directory, entry.name), { recursive: true, force: true, maxRetries: 2, retryDelay: 100 }); }
|
||
catch (error) { lastError = error; }
|
||
}
|
||
if (!(await fsp.readdir(directory)).length) return;
|
||
await new Promise(resolve => setTimeout(resolve, 150 * (attempt + 1)));
|
||
}
|
||
if (lastError) throw lastError;
|
||
throw new Error(`Could not clear ${directory}: directory is not empty.`);
|
||
}
|
||
|
||
async function loadSites() {
|
||
await Promise.all([fsp.mkdir(sitesDir, { recursive: true }), fsp.mkdir(uploadDir, { recursive: true }), fsp.mkdir(caddyDir, { recursive: true }), fsp.mkdir(iconsDir, { recursive: true }), fsp.mkdir(logsDir, { recursive: true }), fsp.mkdir(backupsDir, { recursive: true }), fsp.mkdir(defaultSiteDir, { recursive: true }), fsp.mkdir(customCertificatesDir, { recursive: true }), fsp.mkdir(managedCertificatesDir, { recursive: true }), fsp.mkdir(certificateExportsDir, { recursive: true })]);
|
||
if (!storage) storage = await openStorage(dataDir, backupsDir);
|
||
storage.humanizeGatewayErrors?.();
|
||
if (storage.snapshot) { recordActivity(`Legacy JSON migrated to SQLite. Safety backup: ${storage.snapshot.filename}.`); storage.snapshot = null; }
|
||
sites = storage.loadCollection("sites").map(item => ({ ...item, healthEnabled: !(item.healthEnabled === false || String(item.healthEnabled).toLowerCase() === "false") }));
|
||
proxies = storage.loadCollection("proxies").map(item => ({ ...item, healthEnabled: !(item.healthEnabled === false || String(item.healthEnabled).toLowerCase() === "false") }));
|
||
try { const legacyAccess = await readAccessLogs(5000); storage.recordAccessEvents(legacyAccess.map((entry, index) => ({ ...entry, source: `legacy-${entry.at || "unknown"}-${index}` }))); } catch (error) { console.warn("Could not import access logs into SQLite:", error.message); }
|
||
try {
|
||
const storedActivity = storage.listActivity(20);
|
||
if (storedActivity.length) recentActivity.push(...storedActivity);
|
||
else {
|
||
const lines = (await fsp.readFile(activityLogPath, "utf8")).trim().split("\n").slice(-20).reverse();
|
||
const legacy = lines.filter(Boolean).map(line => JSON.parse(line));
|
||
recentActivity.push(...legacy);
|
||
for (const entry of legacy.reverse()) storage.recordActivity(entry.message, entry.status);
|
||
}
|
||
} catch { /* Activity history starts empty on a new installation. */ }
|
||
users = storage.loadCollection("users");
|
||
if (!users.length) {
|
||
const now = new Date().toISOString();
|
||
users = [{ id: crypto.randomUUID(), username: adminUser.toLowerCase(), displayName: "Administrator", role: "administrator", status: "active", password: await passwordRecord(adminPassword), source: "bootstrap", setupRequired: true, sessionVersion: crypto.randomBytes(16).toString("hex"), createdAt: now, updatedAt: now, lastLoginAt: null }];
|
||
await saveUsers();
|
||
}
|
||
let usersChanged = false;
|
||
for (const user of users) {
|
||
if (user.setupRequired === undefined) { user.setupRequired = false; usersChanged = true; }
|
||
if (!user.sessionVersion) { user.sessionVersion = crypto.randomBytes(16).toString("hex"); usersChanged = true; }
|
||
if (user.mfaEnabled === undefined) { user.mfaEnabled = false; usersChanged = true; }
|
||
if (!Array.isArray(user.mfaRecoveryCodes)) { user.mfaRecoveryCodes = []; usersChanged = true; }
|
||
}
|
||
if (usersChanged) await saveUsers();
|
||
redirects = storage.loadCollection("redirects");
|
||
streams = storage.loadCollection("streams").map(item => ({ ...item, healthEnabled: !(item.healthEnabled === false || String(item.healthEnabled).toLowerCase() === "false") }));
|
||
accessLists = storage.loadCollection("access_lists");
|
||
groups = storage.loadCollection("groups");
|
||
const defaultSettings = {
|
||
defaultSite: { mode: "themed404", redirectUrl: "", redirectCode: 302, preservePath: true, title: "Route not found", message: "The gateway is responding, but this address has not been configured.", customHtml: "" },
|
||
backups: { enabled: false, frequency: "daily", hour: 2, retention: 7, type: "complete", includeLogs: false, encrypt: false, lastRunAt: null, lastStatus: null },
|
||
certificateHealth: { warningDays: 30, criticalDays: 7, staleMinutes: 10 },
|
||
logsRetention: { accessDays: 30, activityDays: 90, auditDays: 365, certificateDays: 365, securityDays: 365, pruningEnabled: false }
|
||
};
|
||
const storedSettings = storage.loadSettings() || defaultSettings;
|
||
settings = { ...defaultSettings, ...storedSettings, defaultSite: { ...defaultSettings.defaultSite, ...(storedSettings.defaultSite || {}) }, backups: { ...defaultSettings.backups, ...(storedSettings.backups || {}) }, certificateHealth: { ...defaultSettings.certificateHealth, ...(storedSettings.certificateHealth || {}) }, logsRetention: { ...defaultSettings.logsRetention, ...(storedSettings.logsRetention || {}) } };
|
||
await saveSettings();
|
||
}
|
||
|
||
function normalizeDomain(value) {
|
||
return String(value || "").trim().toLowerCase().replace(/^https?:\/\//, "").replace(/\/$/, "");
|
||
}
|
||
function normalizeDomains(primary, aliases = []) {
|
||
return [...new Set([primary, ...(Array.isArray(aliases) ? aliases : String(aliases || "").split(/[\n,]+/))].map(normalizeDomain).filter(Boolean))];
|
||
}
|
||
function validateDomains(domains, exceptId) {
|
||
for (const domain of domains) { const error = validateDomain(domain, exceptId); if (error) return error; }
|
||
return null;
|
||
}
|
||
|
||
function validateDomain(domain, exceptId) {
|
||
if (!domain) return null;
|
||
if (domain.length > 253 || !/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/.test(domain)) return "Enter a valid public domain such as app.example.com.";
|
||
if ([...sites, ...proxies, ...redirects].some(item => normalizeDomains(item.domain, item.domains).includes(domain) && item.id !== exceptId)) return "That domain is already assigned.";
|
||
return null;
|
||
}
|
||
|
||
function validateTarget(value) {
|
||
try {
|
||
const target = new URL(String(value || ""));
|
||
if (!["http:", "https:"].includes(target.protocol) || !target.hostname || (target.pathname && target.pathname !== "/") || target.search || target.hash) throw new Error();
|
||
return target.toString().replace(/\/$/, "");
|
||
} catch {
|
||
throw Object.assign(new Error("Target must be an HTTP or HTTPS address such as http://192.168.1.20:3000."), { status: 400 });
|
||
}
|
||
}
|
||
|
||
function validateStreamPort(value) {
|
||
const port = Number(value);
|
||
if (!Number.isInteger(port) || port < 1 || port > 65535) throw Object.assign(new Error("Incoming port must be between 1 and 65535."), { status: 400 });
|
||
return port;
|
||
}
|
||
|
||
function validateStreamHostPort(value) {
|
||
const raw = String(value || "").trim();
|
||
const match = raw.match(/^\[?([^\s\]]+)\]?:(\d{1,5})$/);
|
||
if (!match) throw Object.assign(new Error("Forward to must be host:port, such as 192.168.1.20:22."), { status: 400 });
|
||
const port = Number(match[2]);
|
||
if (!match[1] || port < 1 || port > 65535) throw Object.assign(new Error("Forward to must be host:port, such as 192.168.1.20:22."), { status: 400 });
|
||
return `${match[1]}:${port}`;
|
||
}
|
||
|
||
function streamPortConflict(port, exceptId) {
|
||
if (port === adminPort || port === 80 || port === 443 || (port >= minPort && port <= maxPort)) return "That port is already reserved by the gateway.";
|
||
if (streams.some(item => item.port === port && item.id !== exceptId)) return "That port is already used by another streaming host.";
|
||
return null;
|
||
}
|
||
|
||
function cleanHeaders(value) {
|
||
if (!Array.isArray(value)) return [];
|
||
return value.slice(0, 30).map(item => ({ name: String(item.name || "").trim(), value: String(item.value || "").trim() }))
|
||
.filter(item => /^[A-Za-z0-9-]{1,80}$/.test(item.name) && item.value.length <= 500);
|
||
}
|
||
|
||
function cleanLocations(value) {
|
||
if (!Array.isArray(value)) return [];
|
||
return value.slice(0, 20).map(item => {
|
||
const location = { path: String(item.path || "").trim(), target: validateTarget(item.target), stripPrefix: Boolean(item.stripPrefix), requestHeaders: cleanHeaders(item.requestHeaders), upstreamTlsServerName: String(item.upstreamTlsServerName || "").trim().slice(0, 253), upstreamTlsInsecure: Boolean(item.upstreamTlsInsecure) };
|
||
if (!/^\/[A-Za-z0-9._~!$&'()*+,;=:@%/-]*\*?$/.test(location.path)) throw Object.assign(new Error("Custom Location paths must start with / and may end with *."), { status: 400 });
|
||
return location;
|
||
});
|
||
}
|
||
|
||
function cleanCustomConfig(value) {
|
||
const config = String(value || "").trim();
|
||
if (config.length > 20000) throw Object.assign(new Error("Custom Caddy configuration must be 20 KB or less."), { status: 400 });
|
||
if (/(^|\n)\s*(?:\{|admin\b|storage\b|import\b|persist_config\b)/i.test(config)) throw Object.assign(new Error("Global blocks, imports, and Caddy administration settings are not allowed here."), { status: 400 });
|
||
return config;
|
||
}
|
||
|
||
function applyAdvancedSettings(item, body) {
|
||
if (body.upstreams !== undefined) {
|
||
if (!Array.isArray(body.upstreams) || body.upstreams.length > 10) throw Object.assign(new Error("Add up to 10 upstream targets."), { status: 400 });
|
||
item.upstreams = body.upstreams.map(validateTarget);
|
||
}
|
||
if (body.lbPolicy !== undefined) item.lbPolicy = ["random", "round_robin", "least_conn", "ip_hash"].includes(body.lbPolicy) ? body.lbPolicy : "random";
|
||
if (body.accessListId !== undefined) item.accessListId = String(body.accessListId || "");
|
||
if (body.compression !== undefined) item.compression = ["off", "gzip", "automatic"].includes(body.compression) ? body.compression : "automatic";
|
||
if (body.hstsSubdomains !== undefined) item.hstsSubdomains = Boolean(body.hstsSubdomains);
|
||
if (body.blockCommonExploits !== undefined) item.blockCommonExploits = Boolean(body.blockCommonExploits);
|
||
if (body.requestHeaders !== undefined) item.requestHeaders = cleanHeaders(body.requestHeaders);
|
||
if (body.responseHeaders !== undefined) item.responseHeaders = cleanHeaders(body.responseHeaders);
|
||
if (body.upstreamTlsServerName !== undefined) item.upstreamTlsServerName = String(body.upstreamTlsServerName || "").trim().slice(0, 253);
|
||
if (body.upstreamTlsInsecure !== undefined) item.upstreamTlsInsecure = Boolean(body.upstreamTlsInsecure);
|
||
if (body.healthEnabled !== undefined) item.healthEnabled = body.healthEnabled === true || (typeof body.healthEnabled === "string" && body.healthEnabled.toLowerCase() === "true");
|
||
if (body.healthPath !== undefined) item.healthPath = /^\//.test(body.healthPath || "") ? String(body.healthPath).slice(0, 500) : "/";
|
||
if (body.healthMethod !== undefined) item.healthMethod = ["GET", "HEAD"].includes(body.healthMethod) ? body.healthMethod : "GET";
|
||
if (body.healthExpected !== undefined) {
|
||
const expected = String(body.healthExpected || "200-499").trim().slice(0, 80);
|
||
if (!/^\d{3}(?:\s*-\s*\d{3})?(?:\s*,\s*\d{3}(?:\s*-\s*\d{3})?)*$/.test(expected)) throw Object.assign(new Error("Expected status must contain HTTP codes or ranges, such as 200,204 or 200-399."), { status: 400 });
|
||
item.healthExpected = expected;
|
||
}
|
||
if (body.healthTimeoutSeconds !== undefined) item.healthTimeoutSeconds = Math.min(Math.max(Number(body.healthTimeoutSeconds) || 4, 1), 60);
|
||
if (body.healthRetries !== undefined) item.healthRetries = Math.min(Math.max(Number(body.healthRetries) || 0, 0), 3);
|
||
if (body.customConfig !== undefined) item.customConfig = cleanCustomConfig(body.customConfig);
|
||
if (body.locations !== undefined) item.locations = cleanLocations(body.locations);
|
||
}
|
||
|
||
function expectedStatusMatches(status, specification = "200-499") {
|
||
return String(specification).split(",").some(part => {
|
||
const value = part.trim();
|
||
if (/^\d{3}$/.test(value)) return status === Number(value);
|
||
const match = value.match(/^(\d{3})\s*-\s*(\d{3})$/);
|
||
return match ? status >= Number(match[1]) && status <= Number(match[2]) : false;
|
||
});
|
||
}
|
||
|
||
function caddySiteAddress(item) {
|
||
const domains = normalizeDomains(item.domain, item.domains);
|
||
return (item.tls === "http" ? domains.map(domain => `http://${domain}`) : domains).join(" ");
|
||
}
|
||
|
||
function caddyQuote(value) {
|
||
// Caddy's Caddyfile lexer only special-cases \" inside a quoted string — it does NOT
|
||
// collapse \\ into a single backslash (confirmed in caddyconfig/caddyfile/lexer.go: "all is
|
||
// literal in quoted area, so only escape quotes"). Doubling backslashes here, as this used to,
|
||
// corrupts any value that legitimately contains one (e.g. a regex like eval\( becomes eval\\(,
|
||
// which Caddy then reads as an escaped backslash followed by an unclosed real group).
|
||
return `"${String(value).replaceAll('"', '\\"').replaceAll("\n", " ")}"`;
|
||
}
|
||
|
||
function accessDirectives(accessListId) {
|
||
const list = accessLists.find(item => item.id === accessListId && item.enabled !== false);
|
||
if (!list) return [];
|
||
const output = [];
|
||
if (list.deniedNetworks?.length) output.push(` @blocked-${list.id} remote_ip ${list.deniedNetworks.join(" ")}`, ` abort @blocked-${list.id}`);
|
||
if (list.networks?.length) {
|
||
output.push(` @outside-${list.id} not remote_ip ${list.networks.join(" ")}`, ` abort @outside-${list.id}`);
|
||
}
|
||
if (list.credentials?.length || list.groups?.length) {
|
||
output.push(` @protected-${list.id} not path /_site-gateway/*`, ` forward_auth @protected-${list.id} 127.0.0.1:${adminPort} {`, ` uri /api/access-check?list=${list.id}`, " }", ` handle /_site-gateway/* {`, ` reverse_proxy 127.0.0.1:${adminPort}`, " }");
|
||
}
|
||
return output;
|
||
}
|
||
|
||
// Static, general-purpose ruleset for the "Block common exploits" toggle — not a full WAF. Rejects
|
||
// requests whose path matches common exploit-probe patterns before they reach the upstream: directory
|
||
// traversal, WordPress/PHP admin and scanner paths, dotfile exposure attempts, and SQL-injection-style
|
||
// query strings. One named matcher + one respond directive per host, so it's cheap to add or remove.
|
||
const COMMON_EXPLOIT_PATTERN = String.raw`(?i)(\.\./|\.\.\\|/etc/passwd|/wp-login\.php|/wp-admin(?:/|$)|/xmlrpc\.php|/\.env(?:$|\?)|/\.git/|/\.aws/|/vendor/phpunit|/phpunit(?:/|$)|eval\(|base64_decode\(|union(?:\s|%20|\+)+select|<script)`;
|
||
|
||
function exploitBlockDirectives(id) {
|
||
return [` @blocked-exploit-${id} {`, ` path_regexp ${caddyQuote(COMMON_EXPLOIT_PATTERN)}`, " }", ` respond @blocked-exploit-${id} 403`];
|
||
}
|
||
|
||
function commonHostDirectives(item) {
|
||
const output = [...accessDirectives(item.accessListId)];
|
||
if (item.blockCommonExploits) output.push(...exploitBlockDirectives(item.id));
|
||
if (item.compression !== "off") output.push(item.compression === "gzip" ? " encode gzip" : " encode zstd gzip");
|
||
for (const header of item.responseHeaders || []) output.push(` header ${header.name} ${caddyQuote(header.value)}`);
|
||
if (item.hsts && item.tls !== "http") output.push(` header Strict-Transport-Security ${caddyQuote(`max-age=31536000${item.hstsSubdomains ? "; includeSubDomains" : ""}`)}`);
|
||
if (item.tls === "internal") output.push(" tls internal");
|
||
if (item.tls === "custom" && item.certificatePath && item.keyPath) output.push(` tls ${caddyQuote(item.certificatePath)} ${caddyQuote(item.keyPath)}`);
|
||
return output;
|
||
}
|
||
|
||
function proxyBlock(target, item, indent = " ") {
|
||
const targets = Array.isArray(item.upstreams) && item.upstreams.length ? item.upstreams : [target];
|
||
const output = [`${indent}reverse_proxy ${targets.join(" ")} {`];
|
||
if (targets.length > 1) { const policy = ["round_robin", "least_conn", "ip_hash"].includes(item.lbPolicy) ? item.lbPolicy : "random"; output.push(`${indent} lb_policy ${policy}`); }
|
||
const timeout = Math.min(Math.max(Number(item.healthTimeoutSeconds) || 4, 1), 60);
|
||
const httpsUpstream = targets.length > 0 && targets.every(value => /^https:\/\//i.test(String(value).trim()));
|
||
if (httpsUpstream && (item.upstreamTlsServerName || item.upstreamTlsInsecure)) output.push(`${indent} transport http {`, ...(item.upstreamTlsServerName ? [`${indent} tls_server_name ${item.upstreamTlsServerName}`] : []), ...(item.upstreamTlsInsecure ? [`${indent} tls_insecure_skip_verify`] : []), `${indent} response_header_timeout ${timeout}s`, `${indent} }`);
|
||
for (const header of item.requestHeaders || []) output.push(`${indent} header_up ${header.name} ${caddyQuote(header.value)}`);
|
||
output.push(`${indent}}`);
|
||
return output;
|
||
}
|
||
|
||
async function writeDefaultSitePage() {
|
||
const selected = settings.defaultSite || {};
|
||
const title = String(selected.title || (selected.mode === "welcome" ? "Gateway ready" : "Route not found")).replace(/[<>]/g, "");
|
||
const message = String(selected.message || "The gateway is responding, but this address has not been configured.").replace(/[<>]/g, "");
|
||
const html = selected.mode === "custom" && selected.customHtml
|
||
? String(selected.customHtml)
|
||
: `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="color-scheme" content="dark light"><title>${title}</title><style>:root{color-scheme:dark light;--bg:#08101d;--card:#101a2b;--line:#25344c;--text:#eef4ff;--muted:#95a4ba;--green:#62e6a7}*{box-sizing:border-box}body{margin:0;min-height:100vh;display:grid;place-items:center;padding:24px;background:radial-gradient(circle at 50% 0,#163829 0,transparent 42%),var(--bg);color:var(--text);font-family:Inter,system-ui,sans-serif}.card{width:min(620px,100%);padding:44px;border:1px solid var(--line);border-radius:22px;background:color-mix(in srgb,var(--card) 94%,transparent);box-shadow:0 28px 80px #0006}.mark-stack{display:flex;flex-direction:column;align-items:flex-start;gap:16px;margin-bottom:32px}.mark-icon{width:88px;height:88px;object-fit:contain;display:block}.mark-wordmark{width:290px;max-width:100%;height:auto;object-fit:contain;display:block;margin-top:-36px}h1{margin:0;font-size:clamp(34px,7vw,56px);letter-spacing:-.05em;line-height:1.02}p{color:var(--muted);font-size:17px;line-height:1.65;margin:20px 0 0}.foot{padding-top:28px;margin-top:30px;border-top:1px solid var(--line);font-size:13px;color:var(--muted)}@media(prefers-color-scheme:light){:root{--bg:#f3f6fa;--card:#fff;--line:#d6dfeb;--text:#132033;--muted:#637188;--green:#138a5b}}</style></head><body><main class="card"><div class="mark-stack"><img class="mark-icon" src="data:image/png;base64,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" alt=""><img class="mark-wordmark" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAlgAAADICAYAAAA0n5+2AAEAAElEQVR42uz9eXBkSX7fCf5+7v7OuBABBK5EZiIzqzKrEFXVXZXVZ/WBZjdbvJoiJaFJDVdDaURpVpRsV7MzuxrN7jBRMpPtymZsbcck42goyWZmJY2kho4RmyuKVFPMPtlX9VFVQFVm5YG8cAUQAcT53nP332//eBGBQFb2yW52k3wfM1QFIgMRL97z5/713wmQkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZHxPQK/x6/LyMjIyMh4JCI7BRkZmcjKyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyPhDB479AGTxrxkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGX+cwewUZGRk931GRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkbGDzOYnYLvP8z8Lc8zIvI3ev34v2VkZGRkZGT88KP+OAud77dw+UbCanV1FR/x2m/6PpnI+mOv0r/7zVA2djIyMjL+wPkjZcF6WNCMC5nV1VX+5mvQ92YRepSoGh7H6uoqrK2t4fp6FQEAlpfTf796FaBWqzMAwPr6On+j481E1h9fIXVlbCxv1GonXrMCAGuPeAwAAGtrsLS0NBo3L46Pq2w8ZWRkZGQC6xsd/7jlZyhkag8tQA8zFDEPiSD+XoiYbyDyxPLyMly/fh0BAKLTp0Vxd3fwukUA2ITWzAz709MEL70EW1tbXKvVeGVlhTKR9cflTkQAIoTVVbwyJqSWVlZ4Y20Nl9bXEZYBtq/PI1wGmLu1hQA1aGxvY2Vujhv5bQQAqOw2EBYXATY3odGq8FMv+PTSSwBzF7cYrgJs1Gq8tLLOq7B6PFDHxxQCAAECQjbOMjIyMv64CayhiFldXcWNWg1XAGC9WsX569cR4DJE0Z4oFncxFS8AMzN9fuON9G8ffxyg0+m8afGoVqtUr9dHz6+srPDq6up3JLweJa6Wl5fF9evXsVwuY7fbFblcDgFANgHktFPFXq+LjtMj13Wp1QrI949oO58nf3qattpt3qjXeWl9nR91HMPP+6bHxoyZpeKHfkAjQGql2qjVcKm6jgAA29e3ES5fhrlbW7jZBVHJFbHo57EeaZFzfQzdLkJMIu55CKUSxL3e2PhrATmCXCzYbhJxEPWZfEHFaos36kBLVaBavcbr6+sMqwCwmv5VauFCyARWRkZGxh8TgTUuXlYBcPnqVVEoFPCzn90TxWKAuVwdfd/HqFAQUK9LrUNRKgEcAUCoc+x5fQIA6DgOAgDEWnMhSRgAIEkS9n2fDg4KNDPTZ4BUhJ0/f57b7TbX63VeWVkhIQR/o1iph8XV2tqaqFareP16AV33FZkkk0LOkAyUUmh9N/Ac1/dcCQAQicQ6RPqoDSa0YLQGA+DZ7e1D8v17dPHiRf7Vep0/trJC367AepSrMrN+/fAJq6HrbySs6iAa2xWsFIvYyjWw6OcRIi0i6EgMiyIhKT0W0g1QKhISHFcYiqUSEo3WaMhhS5alY1kJz+qoZ3uJIJm3NjnSrMBY5ZZ0KKxtRR0224pi3yeA1Mq1Ua/x0vo6v7i6ypkwz8jIyPgjLrCYGVdXV7FWq+F6tYq1el00fV+Vo4KI832RDwIRxLHodlF5FVdwx0rjOMJxlBi+h1KGPPI4jmPsErFMNGG+YLvdLkw4LnVEz0JLWtcNKEk0+36bomiK8/lDmp6epo9/vM0AV2lgmeKHxcq4oFlbWxPNZlOUy2WsVquiXq8rHYYOWt8t5tHvJeC36odOh0igAem4Qhf8UFerJWuIdWygL3pJEoaJdl3X1ut1qlardPXqVfpbf+tv0VDk8XcZ+JwJrR8ei1U6ptcR6jUB1a7o3NuXolgWIrECQyvAywlER+UgUVJ6DoNU0gEZx0Yluq/6SEpbEkhwPNYFkCM9EsA25wbGEjFYTRbBQmxMz+XIOjbpb1mrytq6+4klX1Cj2+LFHBBUgeAq0Mialc4U2ZjJyMjI+KMisMaFVWoNuo6ue1Ha8pFTTHxHSlQQkgT2ZRj4Csh6AKwAAIwRSNIKTqQE0JAQMwpLgJ4toLBWWnIch6VyyFhDcaKtY0gLoWwcJxS5MeVpwkh5ZI8KBcofHtK9e/eoXC7T+kMuuzeLq/MiOr0n5iYmRHx46OSiSccpWl8i+Rs378mZqcriudPzby3lwgWUIkgS22q0une3H+zdiHRUP3tqqh23u62+Y/puFOlSqaSvXy/YcvkWPSou67u++JnQ+oEJq3GLFXQXRWdeSnXUV0lRSJ+EzOVcpRLPYdBqr9cUW72W6PbjvJNXE76Ty3mO73tS5XLSLTlSFABYSUQGoqhvdLeVRFHLxN1W1Otq6rc9UElB5exsWLBlr9D3GKJYcqJbkaain3AzMtQ6pKPpCRsfbJGXzNsPDcY6AMCLqy9yJrK+0XX9FvNodt4yMjKB9UNzUIhARDgMEP/IRz6Ce3t7whgjD6x1c86EH4Lroac8AOt1O23ZjGOHDQVsOe97bpALc57re7nAVwUllLTWMlnNndgYE1GUxNFRbJOWsUIrR2hPiX65EEbKAQOeZ2z/SPcijEG6SUhomqJte/eMeeGF8+azn/0sl8tlAkgD5ldXV3kYYL+8vCzq1bpYaC3I+92um9eeK3Nu/qjTzne6dva9zz/zE7MzEz/tOc7iuIUtSZK429e37j7Y++z6tRu/7bjOG54THDmB2+nsHCTVqoqvXy/Yv/yXL9thbNjq6iqOf/a3w/jfZULrD1BYCcE8GNO1Wg3XYV22oCiL1dMSDrsq8cgRAn3fEW5sSVzb3XYObDcfeuFkber0XK166vRCqXqh7BcWi35Y9qWTBwRfSPQQhRICQTAAAtqEbKwtaU0m6eheJ4qTg3r/aP9Be//gxuHOwd1W40GvF21NeMHhtF/u5YG7hrgbg4z7SUeLLmnlGV2EloUq0Am3YRaflc6a/GhhxSPFhZnQysjIBNYPn9VqIAQQAEStVhMAIF13XiV+x3EgyClUpajf9XdbPSkYCtPVyuMzU+UnysXihWLOn/MDb8JxRM5RKkREDwERgJEBwFoiY0hro3uxpmavH3Xa3V6z1ercaB61Xmu3+1vIfJQv5KIwH/RdobqR7nehr2KlbBLlSYuGbwuFNtWrVWpGEV8cxGkN3ZfdblUsvg1U44EbTuS8wl7zcD7nBe949/NP/9JkuXAJAMBaa4fzMSICAEghBAIA3NuuX/u9L7/2dz1PvaJQ1E2vfZTksJvXOrp3b5rK5Vu0vr7Oj8qWXF9fx1qtxgAA1WpaDmKYvQgAsLV1kQHSshDr3yB4PuN7b7UCAFhZWxNDq1WndMEhjr2gS55TzAfS2sL95Ch80GwUJ/MTp5+ZP1erTZ1+ZrFcPTeVK03nPD8nUEIEBmIw0IMEItCQgGFDzAYMWGZgRACBKFmijwpy4EIJfPBAAoCFTrdrdnpHzVuHe/de279382b9wZ1u3H+l6hfvTHm5TtTvdQDcdsKyd9De19JOWe/pxDZvlemPfWzWIwTVldUrDz23Chu1NRyWx9iobeDS+tKxFTATWhkZmcD6QYmr4Q6/2WyK06dPi/19qfw5oSSFngKTa/eS/GGjXZqenlp48vypt8zOTL4rDP23+J47MdAnJyFiYk7T4AFAgADAN28wtTbUj5ODfi+6XW+0XtutNzYe7B/cYJL71WK+5/ncQ+10mZPIhKxlt2uJquboqEf5/BR1OgkvLQF8qV4Xk4WCEP2+50VBIVa2kkTJ+5ff+db/e3kiX7XWJgAgBj9pPjwzMAAgAgEDSyWd+zsHr//u57/230/lc9csw27CdGTdpJfv9/W96Wm62G7z9evXcatcxhosQT5/F3eD4PhbbQIMA/8PDz0EAOhPxFyOIt7O56nR6fB8s8mPcnlmt8X36N4axMqNXIKwLqG6KAC6KukHAQsqCQzyd/t177Dbmn5u/sLz71movfvS1MIzk4XCNEsFB9CDBnWpRX3qguGILVhksGzRQuottsCQkEFCYMOUjiVAUCjZAQEOI/ioIEQHyyKAigxlBUKUhqjRbrVf2938+n+8t/5bG/V7G/NuYe+sP9HqSzrUAnsqQn0krG1GZbNUXSe4mn7oHzuhxYBDMbVRq+He+jouD/5pe34bAS4DAECzvJXeg+sA5fl5bpa3sNyc5zSBYIOX1pc4E1oZGZnA+oGJq/Pnz4v2K6/Iem5RQTF2895ESF3tvnz7lnrLExeffurJcz9XnZp4T+B5c8M/t8ZaQGAmRiHwOAAcxx4zAApkYABOzfjje1KUQsrh73EcR51edGN77/BL128/eOnoqHVjspjbLoVOX2sTx8pNCHomMAVNpIzrNq3neei6Lu4Zo6S1vot+cfPu/syHP/D8fz8/XXmn1iaWQqjUmpaWGuKh+GMYuR4YmJRS6tXrmx/7yss3/nG1Wto1Ot4V8VGbaDZWas8CAARBgO12W0SFgsj3AxEHfeF0FUIJ0vTJEkASRUL1FRpj2HV96vUiAlA2ihL2/SOCdbDrsE4AqwSw+sgA/ozv3nJ1ZXUVYRkE1Gtiu9lU5fPg2NgNpkre5PX9rdkjk0y//+xbau8/88SHTpWqzzp+4O5Ci7Zsy9SpA11KUINFy4yDnNHBQGa2zGCYkIDBAgExgx24pwQiCACQIEECgkQBAhAEI0gQ7AuHKyLEc6osTkFB6H58dO3gwcv/4fbXf+fTN1/5woXCzOb5mdneVmMr1tJNsLeru91F88fSmjWYJVbWPibKzaa4G70qZorPoZdsCoB5mJsD2AaAwmHrkfNpu1/kXPk4Y7NZ/gSNhFYmsjIyMoH1ByGuYHlZVPp9ec4YqZRSHQCvGpRzr93f9U5VJ888//Sln5mamvgzrutMAwBYaw0zAwIiI6AAGM1WOHSXnJwoATAVV+lrcHAGeOisG014iDhy2bU7vcP7W/tfu33vwb/Y22l8fnqhYh3wjHSiHgsRRQcmMQYNTAG4SmHURicshWGv3gzm5iY/ePmpi/+DQBSAqTswbX0DMF7xYWDJQkRkYmKllDhqd7Y/8emv/T8D39lwJN+L2GmSE8Wi4VuYB6Aokqy1pMR1SxKdHlvpGJPGdfl+at1QRkAEIJUibQw5bqB7+wcWi8Ie3e9YKUPdbL5uGo2GBQDKBNb3RlyNB7I3+hXp7pcV+bFXLLv5AxuFdxv1J9+58Pif+Milt79nsTJzSSuhrsO+vmsOqcMRxmyEYQvpDyMNx+5QXwlmywSWAQk4NX2OVPrxmBKYiioBODCbIoih2AIBHiqewJDPypK8ICeFk1B0s37/S//7jZf+4W/d+vJXf3TmyZgtRz0V9cOOnzR90M1oy+ytAy0D0Isvvshjt90fWcvVRq2GsL4uw0UQUzIvq35R9UFI0CTcssKE+lLHDkIYAkAPOBGUdwR1AKDgKMK4S1tNbZVn9H1o2b11oE+uvmgzgZWRkQms75uwAhjEWw3E1Vyn40Cx6Oa9iRBsUrz3oDH/7DMXf2zp0rmfywX+AgCAMcakWiVVUEOhMtRTfOIz0i85NBKNnuCB2HnkSRnYlgYaTUqhABC6vaiztXvwH9bfuPMJreHO9ITfiATUvdh0jAHt+wFFUV8YV7qBG+Sv37ud+8n3vfNvzM9M/rzWxkgpRHqsj+iCA0MjG48sbMBsP//V6/9gr9H+rbwvb0lr65FnEtF2rOMo1Lrnel4ubOpW0Ora0EMOUQqXCR10QIFhCcCSiJCZmYh1rONeHJlDkNgpBTmtwXb60OlPAyTr6+u0urpqM4H1+7daDV2CLahJcroe+NFEQYXlV5s7xZnSRO0Xlt7zi0/PnHk+cYS4ZQ/1Az7iQ+6KBCxoMqDBgmYLFgiYAQgACI53DIzEnI4WHI7/VHwNKyukIyr9EWOP098QAAQiOCDBQQUeOFxAj07LCfGknFYyMc3P3nvt0//L+n/8QmDd108VS5tHvdY+QNArkYz3SlIvwqKBq1dp1H7nj8K4eSjOauAWFI1KRbrlRB3Gwou9uLLf784qJQqOFA4KwURgJAhrJQAyMRIYALKEQN0o1myodWam2uMjt4eTUZRz/WTnf/6MWfvYxyirN5aR8UeTH4pmz7VaDdevXhXlCxeUys95Et1ibKL83n5r7gPvvfx/e/zcwgcHFiuNAAIhtQYda5XhrIgjEYVDxYUjMZcuMQNRhSM71rHeGdq8Rs7FwdtZSxYAOBf6ucfPnfrZqcnS+2/c3vn4rc3tfz9RDix4OTBR1O0JslqBUEAKXFKFIDdRyPnnR+LvWNu9SeTy4DukH8oAjKQc5ZSK+XMPdhuTbsHd0o6UXl+IuBBA3yQy8P1Cu9etTpYrT56eCy5aa4tKYB5R+MzsWWbJZAUxCyZGZiBAiJXAjjHmzubdB58KFdwpmKJtNhOC5eVkkFyQTfi/D3F1dRnE8iCQHY6OPMejsgH33FcbD0791KW3f+BnH3/uI34YVq5BI3lg29DlWHSoj31OIGELBgxYSN19Q7sUj4Q/Dp5LbVbpkEcARhwOXCYGFszpPwkQTIgMA5lFg0RABGQADQSSCWJhMWYru1bDA9vSp2Wp9N4Lb/npU6Xq+/7xy1c/v753719enDzz9aTfPqTAtor3NTYmX+aX0+9ML36LXp9/GJUWA8AqrML2/DwCbEnKT/mI3WA2LL/7bYu1jxKwQ8gCGC0Cxq5ytK8cVkpYwcISERtLaNmwNmbzS3de+030oz3R81rNIKG9pSWb3TgZGZnA+r6xurqK8/PzYnFxWczNGXUUizyzKXYOktmf+eB7/uvpannZWmsAEBBRDiw7I2GEgOnaMnTzIQCgGKRRH9dJHNv8j8QVjkmcYQj80F04sHjh2AvRWiJmonIxLD9bO/efTpULp7/88o3/wfGxXy14HKGJpOcCMgnbM2KiOJFTyikNP/9NLpzBh/CxbQ0e3stKJcoWxJQFFRjdF+C5wnP7EBtQOjIlx/Wefr72+N/0PXcREAgBJSIgIgIyMoj0zQkAgGionFBrY6J+FN55sPuvK5P59oyb1+X6hIBajYeuyuz2+M7FFSyDeKZfkT235XJsPbfq5vc77cm6Pqz9led/7Offu/jE83fEEV4zW3GbE6nBQsQaIkog5tRyZZFHiQ+jTUS6Yxh5lVPrFQFwmiMrBuObkZmYjr3ebEGAAGQcDAUeWWkBYZBbyGCZwKAFzQYMatG1kb1PTfPc1ELuv3jnR3701zc+3/9nr35m87npC9BJOoihCznZtdM14A2o8ZXVVXgxjSf8Qz5uTvY23ajVcDa/LRoHoObzgfe7r3/N/7u/8Nd/6h1naj8ZmX488LyCZRKAgAIFyPRB2tMRABwQaIjInwh6//Lzv/MbtelzZr/XjS99ZD755Opq2sqKs9stIyMTWN+zNWkYdwXidP+0kDKRLcp7jozy9WY885MffOevTFfL7zbGaESUOHCHDNLtRqsEj283R7YXSu1B47aYY2MWDAPMx/8tfTNOjQA4NtficaZfaiJAabQlFILPnZn5kYlimHv12r3/V6sTXw9yaAFzxnU0JJHGfjdGk2YNplY1FMeRMnx82McH8fA8S5QkmhxPeugAusJjy4he7IjAYXGIckJJeNJx5LQQSAP/ogXGke2D6bgmjwBgEIKNsewoKT0/eJwIZwW4D7rqIHKSLbOyAvabXbOBMMxWgzFxdWV1FTc2arj9oXnh9SPhJzk3znPokVO8vbtXEMXg4uq7PvqX5iertd81d5K75hAISRAzGLCQpEUWQAOBBQbigX2KT677Y4XLBop5MGqY08jCoTDjwbUfJs8yHltscRSaOPKvpzqfgNiCgdSC5oIUEWj+nfgGP+ZX+Weefe9HzlZm4v/usx//F5fKs0IGCRlXJWF10XrXm7xRq9FgAP+R633Z2i3i1AQJgY6qFsv5pm+mvwpb3OYuCxDMCGCRyXJ67QABwAx2OowQoEM55bh9l04p9HOMnHjSKm8vElcA7ItEWb/QjIxMYH1vGIROQa1WQ6iD2I+k8qfZC4Qu7TT7p398+e3/19mhuAKUx+Yd4FSZ4LEu4tSlhwJHj2HoPzzhA8QTf8dwUkgN3C3HTw1NPUNnIx47EhEFAqdB9qVC+NzUZP78g/3WtWDSB9vvcq/nsOco6vYPj+LE3C3k4GmGgXdnGEiDI6HHg0zHwbkZ2eJAJ8a0O/0dz5F7yNgV0rFSKgLRQ5UUBOpWTrDKpSsrYLqoskh/wTHlmVoWLABKIh5Un2dich3Py7tKu1Ln5GHJEb/2a5tia2uVmXlUpX5cUGXi6s1sbGzg0i8DNvoVUelIp+lx6KMzdbuzV5yarjz919/+0/9nyqml39RvxAfcGQSvE1hmNkBogWA8UH3g/AN6k6d2mGbKo3uIB1bctAbWSFqN7TkQGImRx424OKohkVq/AAQLRAQQQGCZIWELCi16oPA1s0NN7OPl84///H/r/ZmFX/3cb/6jGTd4VZPs9npdUzxteSlYZ1hdR3jxRfqjdG3LzU+IXPlDCIERoEiUnMDbsoduDAq71BMChMCx6YYBIN3TpOIKAUEKgT4qqJseCBAOOShtUpKq0xIbaS277J7KyPgjiPhBfOiwSntaCHNByop2Q6dQeLDdmHrns7X/46m56o9orQ0iymMXHzz8YLi+nPAa4rFBamSNShP4jssv87FWGsVFAeJxkPyYJ2/4Wh4prIFIE8hSSnVzc+uTX964/cpUJVRxS1spHRtgbIhRO4Fs7dWbv2etZYEitYKJQWmI8WB3PmGoAABgJaVoHnWi/aPeywUvuIMWutJxrZcYshSyVolgRBcQPGaWw5MjUAwWXz6OtUm/PwIAUmrtSH9BJZXreIiu4tCXxX5OlMtlfNhixcw4/vj7ZdH8Rj8/7NarvV9eSjMFZVntK/YcR0zePdiemZ+Yet9/+Y4/+f/o5mDpU/pW0oSu1JwWCo3ZQAIGhmLLMIGFtPSChdRAxYCD4PbB7zgUXgDEgx8AtABgBTONguE5/T+mYo2YgZDS3wfPWQAgBKCB1cwgsUnLPoABggQsxKChBxpiNqJOLfh8spmcPXXmPf/Z+3/8v26o5JxlKctlR+50jNi+vo0/6Gvxpp/fB8NEBYDL0PWbmMgIY0MCQ+W2UXtt0NBDA13W0IUEOpBAhxPssMYeGOizgRjTn4gNxoJBCwBHgiAHpTQkRKkqys2mGB1/Rsbv+z4YBWrit2zhlPFHS2CNL5i1Wg3r9apw3T1VkdI/ajbcc2cXnr6wOPfjRGSFSK1E8JBwwjElMha3Pqa2TkSZjETSeLDwSDjhsRBDZhiErsDDoo6BASgtrTAwZbEQIB882O1+7Y17nysUc5oTY4Qs2MQYiiLHWoqSmXIZr2/uvtzt9jelFE7qQeA36UQe1sNKzxFIKUknsbi1VX/JWLrFIPp9Ru1pQ3GgKbTEAhEVShRSEophUQocNxOOPof5IS2HIzUmHSVd9DyUUmCS9EW32xWPqhD//RwPV65cEQ+3+xn+vrq6+ibBhfhDMG+MLYiXCvPY3E+UoztuvuDm7sfNiaA48a6/8raf+OuNUC98Tt+JOhCLPhvos8GELRhk0MBggNgCD6B0WA92DYwnTRs03BNgKo54zHIyFFY8SJcYWq+G1tlUlPFQlJ34O8sMJg2qZ4I0JssAg2YCzQY6lMARx7DFHfjXtGE/07894xuZ70PvpCj5fiwWo3GMjxZR30xMfTuiZfw1w8fjY/EyQO+whVq5iIqEBXANs0yAIGbCBC1EQBCxgR4b6LGGLifQAw0d0NAZ/N7iGGI2whNKMbsy55JIbFM2y1sI30Grq9+/++DbOJffI5H6h06U/KCE0Hd49U4+gyc3GGNGhIHl4RtfS4aHrvf34Bz8YRJ1fwDH+gNxEa6uruLy8jLW6xuy2w0VBqH/YPdB8CPvedefCnzP1doYIYRAoEGh0OMeX8fj7NjMNIxpYkCAQSVrMTZ1pt64QcFRHmYLPnRucRBpxYwnA+JxWAyUYRAbo6TkbreHr956cMsw3JlQbhRba/Lakgldy4U2+EppSogEUHfj9ta/e+bimb8SBq60li2kmZA4HnM1OB4WQpA1xr1+e/v6nQeNf18q5VoE1M+5fpL4fRpGSEkpBQvNAsAMI3LGLHM47uvkQeGk4ZNEDIggpBKuo5QEACgIiRD6wvcnRaFQwPE+h6sPZYh9t0Hw49aowfvC6kBsD54TwzY/tVoN1tbWcPh/gLTvIwDAr/zKr+APtPL8Q0Htc21XNqZmZVsdBhRzrq/wsf/LO3/srzTyevYLyZ0oASM1W4jZghnIIMvjjjwetbYbVPQfRPKM/gvjcXXDegw0uL4DC+xxpsTgziBmHFoxj4vrPsoQnN5PhAxAxyUdUgFmAdhCAoal46obr922D766efX8xOyW8iybrqSwWmFofn/O7yqtjsQ2rK5+R2/z4uoqn1hYHjVWhs+NvW5cKM7dKmNnJoBEatTWKIeFY4FQgwEDNJpZKI1jYzsI/qSx6QoZwDJCbI3PSrjEWlo/lCEIXBp83ovDY/h+jOdHLK5XvoWoG2WFHhds5je953d6rAyPKKTz8DgcPeTv2SKKJ4f+t3zt93lD9g3Py6PO56P/7ltez1VIOw6sjD/5sY8BAMDSyjrD6uBFsHp8rYGHmxr+rs/18D3etL6Oten8Xl/j38+1f6gAwfdjP/MHasEaVmsHAAlQdb2KzR0c9otn506/8z3PP/n3pZTBYGONY5WhhgU6x2r8jMctDc/QYClKq1gLeGTvHAYgZoJja9LgKozqZKfx7vjQcjVQeQJZJ7F4aWNz7+7e0W+U8uG/UoLvCTItFUCvAGUNANAT1rVRUpIOzzQP48dmpvIfubQ4+2MTE8WctcSIaPlEeS4WyCyiOIHrd3Zu3bx38E9D3/lqGMgdi9wQLI/A1REcAkjJDuW9ibhj3uI48oMvPHfxF6QUORgETuF4utl4kH564zAxs0DA3/v6jS/s7DX/Vbmc/x0ms8/dJFIqF9frkADUBlJuDdZXVni4uD2qSfS3ElwP9ZeEYSukoYngMgDcesHH7d+5i/A4wOMA0Ol0eB0AaumUAOuwAfPNJm9dvMhw9SpsbGzw0lLa421cbH3fMyAfEleNfkXCAXi2LEPpB5Uvb9+Yu/LhX/ibhenyBz+X3I4S1lKDAU0GTSrt+USU1NBaxeNxPGml//FhlwaqA4NARoEICApkWrEktVKmcXfMBJYoLevOlJZzo2HFksGfIqafwCctvYOsEeSHghEZkUCi2r2xu9Nc3/nX08Xi7zLb2y7TPS2TXtm65uWgYT+5vGq/o8l5uEiPWY6uAMBGbQMBVmAFANYAYK+6jn91ucbrV9cHR7UM22P9NS9fBnjpJYC5ixcZ4CrAVYCNWo2H/QBfXF3lK6ur+O2UkmA4zh4sn2+KuO3Kacc6XS1ziFyt7+wuPv6+t/zt4tzE01EUawQQgCO79Cj+aqSMEUACslRK3X397j9pvrrzr+amw22rcU+6TpP6rf5OZ84sDTYPq6urJ1fU73qxYxz/PgBprCCsrMDS+jrCMsD29XkESM/b9vXr+ObzB7C0tM6PFF7fRKA+inEB8O00qH8xfSF/V+cBEYCG9+kV/Aa7/BOCPT3v30AUfDci46ExhYOx/W1tCr4NroyuaQ1hoKKGYmq9uo7De2Ru0LqpsV3BylyDNwCgfH6e4SWAuYtbDFfhxFz6pmP4Zud9fFPyLa7n+Lg+eS/i915Qjz5n+PjKt3HeX+Tvh/D7gQis+Y98RJZv3VJhGPoyN1m6fuPe5M/+xPLfOD1fXdHaGCmEGK/+M15JdLzwOsNxsC8AspSCgUhGcQy9ftTv9uOWsXxIzDECCCUx57lOwXPdnOc6nue5UkgBQEw0EHXjVhYeCyZGBiAEUgLVjdsPbn/htXv/rJwP1lmIm4Ew+wZVuyTDyPd7dhcAXGZHWC8wkS4K6c4ddtqTgec88djC1E9MVorP5AI/L6VMFzuyEMeJOWx1W5vbzS/fq7f+QylwbztKbgPTAeeoM2GDuO8mVnYEald4iKIaRXFNSVx+4dkn/hMpRThM4cJRjDufMGbBIJRs2Prwiy/f/L3dRvPj5UL46ZhpByx0Rc/E/T7oKEq4W415pt/nNwDghelpag+bWo8tBm+e21KRM26pGlqoqtUqXr9ewPILPnbrdZGre9j09zHwPPSPXHRdB9tuCycBACoABw2AQpJwkiQcRSX2/SPqVqsMm5uwmcvRcrVK3+h4vm8iaxB3NRRXfhK5u1IVS4XS9PrezuKffOrtf/a5S0/86S+a+6bLEdpBwVADhMQDQxSM7+NTHTQMaLdw3L9ptP1CSVJJZGRpjEGdGG0j0zKJ7mhtE2C2DBgjI4IEHxWWlOeGjit9z1FKuoqBkMhaRiZAYDEszDbKnIVRaZPjdgbAgEISKeHcf/XOXnej/rfn5spfNuAc+Vq3EoOdmUo/+tQr18xPbc3Zb7t1zrhIHSx2G2truHRiYSgjLAFU7m5jY7eIsAjQqjcQFk5Dcb+JHS/A/lELq9X0LTtJn+noLBWrXd7cBFh8W45gbR1gGWijXhsd06jFzzdZuMbF1VQipXGtIzzMRVbP7u0cXLj0vmd+pTg9UYviSCMDskjdr6NUGsZjkTWQrUpJtbmx+c87r+/9q5n5yQcO8J4Vutndp2jufNlsR02ea6cCZ3i8S4MeofjtioxHLHi1Wg3TBRdgvjCPW7fK2Mhvo+oY0W22EOYAclGR08fzMAdp25+5gkuNl1scv+DTZUgFbCrGBotyrcbf7Fwef/4Grg3E8nq1iieE3ICh0Bu0cwQYiOWNep1PXLNvd2yNhHo67yxVqzj8vNFnjVspx75TauFJN5TfUQFdRAAiHBcao88fCNqHNwaj7zw6txe5Vq/zWrqvhaWPDSxNY2yMhW+M3y+XLwNs3dpCgBo08tvY2i1iJZfDot/EzmELYWYG8nGftwd/m4s63OhWuNhq8VMv+PTSQHCNj70TYuuh1iPjG5GV1LuAsDz4fieuY3puB/uilPFzvX4s4F/8bhJkTgiqKzjcnO2tr+Ol+XkcndzRQcHohM9dnOOrg2Obrm3w0srS6Hx/r9pY4R+kuAIA+OjaR8USrMgagCtLpdBht9I1fOmn3v+Ov5vLh6etMXZYop2ZcRRLgqmzK21vc7y1x1HyIGO/H9NWvbG9VW++1DzqvdbTdt9oagOgRQQlkH3fc3OBJ8uh758qhN7i5ER+tlzMTedzYV5IAcxsmIY1P4dWstQJicDYODyKvnrtwT/s9uNP5XL+vrV2XyI1u7rVO90uJMUnivYGAMjbPVUoKA8CJ+h3ehPgiskopko3isrFwHtyIh8+47pqRiK6xtjDbqI3O329ScR3c55zACz3QfK+K7ktEhsJoUwLADwnEg4GOWN5NuonS1LiCy889+TPSSWCgZ0Dh0vmw17VgXlnpE0///KNL+03W79ZKuY+Z6N4y7BpeyT6XdW3OWPY1Au8k9/jhSCguufZ7rWYc7kqVav1kbBZ/QYT7NramhiJqkIBy34qqibbbUFEMsnlhKMUOkrhsF9ih60MA1+6jhYAAP0+AKCwICKLXWH7bkzTnkc7WvNRp0PTABYAbLVapfryMsPaGgyP6fstsLY/Mi9hCxwACDFU0/vd5vlKYeIjf/adP/KLG86+2LMdZrZo2KT9AtOYKWZ+KK4KkZEJB5YioIHASrP+kIXjsLZWdhpHUeuwc3B41N6njllXPfOqNHBkyJKQwiIKTVZLQhFKBSWUckIqnPT94LQ3GZ4LpwrVsJjPS0ciW0tMFkezE+JY3sVxdwQUgkiiuPvK7U73+sHfO7Uw/fG+1h1fuEe+h73W/fsJACRL62C+kwXwyuoqwuoq1NbWcL26jtuFeZy7VUaodkXLayAAQHE/j/WCFtW+FZHjY6RcFLERmJDwlEJNQrpSoY5j5JIkTvrkdK3tl/OW+i2qBJK2Cw7NtV1q9Fv81LRPW+10AXnT4vEIceXtnRau3Fdt0Vd5BU5H+aEku7Bb37/45Huf+hv56YknoijSjIz88K02tCMPriMDslBS3Xl9c63/euNfTlUL21LRnqPlkTRJv620rRYcaiVlbsRdLvZbvNNp8FJ1mWpj9xl+I6vRmBXwytii94lmU8yVt7CRr6DqGFE8n8f6LS1k3gpDjvR7DkIxfUuvF2GiXNRKIUAHlAitoZ5xZdn2j1rYivocThQ5F5XZ5Lep0mkwVOFk8+8T53EDl6pLuH19Hr3TkajsFrGVayDAaYB796BYaQ1evwjbSSIKQQv7foBBqc+7uwCxL2g2ryg9D6lI/jaEFgLzcSeFahWhXhep4GggLC7CIgyE+oBGt8IAm1CcqTC8cQN25ipcbs7z3NYWnxCR34YlhwHgo2trYqm6jtvX57FZ3sIlqMFmtysWFzehVS9i0W/hzcM8zgDA7swMAOzCDADALkC73+HYXaTizPD6A9WWa7y2diyotq/Pj8RUI1/BSlDEVr2BRT+PHa+FzT6JXNtHkScBMAGGO9JTLrqBz7FNGFoAXqj5CABsT1LJldRNIjbNOsXuIi3mcjTfbPJLADC3tcW1Wm20gR1agseF+/D6tnYbWMkVseu3sHfYQQCAw36eL19cJID0ux+f8xb3ckBpE/RUVJ4Y49+RFfxkE/Zysym805FQHSO6fh57g/6gM7ADALOQnuodCCcuMmxvQa5cZDMYZ+XmNgMAXNua4+naBq+trNFYFNIPdwyWQGRixhVYge18HvMAwgjh77ei8OLZhYu+786StTQeAIUC09rjY2UHhgEiODAFEDMIAXh42O69fP3+F3YP2y87wrkRhn49n8M+oDJqqJbAIhFgbIzq9JPgoNEq3rhTL+RDZ3quWn7rzFTxuamJ4pTrOEAAmpnFsDCEQOZOp4ev3979t61If7XgywTZ9HwVdI3gWGvPFJVrv5YkfD6KOClMkzujbXu/b71ARTaRnVAloZ8P48jAG/f32jsWKIfMjgDsOUp1PFfFriv6xnLkKB2DRUsC2SSJ0C4oJQRK9jyr7QRJWULAAqJUIHDMKsJjQ4JPLJ6ptYxRSMFExMAcSqXyZDl0Ha8QkBARGzeninEn7rIqABWdMjWNMGBaZjGnbB1atl6vQrU6ipV+U3zdRm0Df7n6y3i9UMD83p6AdltCoSByxkgTBEoiqgBC5StXKiWF53vCKiPKpCSRcWMIpWcMBiGAA2gYXEMua9etaH3Ush4pc8YPjLVgtAZT9zy7/Zu/Sf75JsH6MgMAXblyBV988W9RurGk743bcKzH4OxeXsTBPYcU+krbYs/os//JU2/7ka2gp3bjtrFshWEDltMSDIBjfiQcupPSIlU8thEbXjapFBOD3Ntr2vrNB7ej3c5nXHRecz1nJ5f391QZ+wqVNa60yMRCSebYSEZCSywoNk4Saz8+6JU7O0dn6s7OYn6qeL5yuno+nJ1YkK5CMpqASAgUo7psKJCJAQQKNgqcN774emLutD82f2rqs4mmyAO/4/a7cdQHM5uv6J3OZyzA0re1u0+N9at4FUBc+rVfw8bpSFRgEWErEZ2LEea3lCSoCBEb4U6U5KIHkgKplGWZj7TqykRiQQlLWkKsZWJBuIEExyJJr6yVK3XOFRotGp0InduPTSQT6yc5un7gUt5dtADrNLRqXVldHbmirgwsrfPnm2LrlS3ZmQ+kE0q30LNOxwsdQTogIXKIkENGZcfKajCPfPGp6xWBcThvAYAQghkJiFlKF10hlY/W5KwnrfU8SW2r95u+Lfj7NHEYM3mCqlClVr1u1wEsLAN9dO2jfGU9dXe+yR135Yq4sro6iKkB8aHzTfz0XiTmfF/Wo6qQYIUXBrKxp1S16soAiqpvIhc8o6wQGCcx2CAAa1kERBDghAVpTOBVkj4YA6UiybxlJYR1g6ZV3RyB79tGP7KV5QYMz+U4S9Ul3KiDWLroins3Dx0xSVJoEABH4F0KeO8oLQ9YKvXBSaxgVUSpIuzuApbyvlVorO5I40V53tycspBbp41azcLq6kD9PyImbEzclc/Pi8betoh9X5okksWZCsYmwcNOD8ERkDheeg6dQ/Z0wGGnD73KY1xoRZwrdwyUa3apWqWNWm0Yp4LfLEbqyuoqfnSwwG92QXqwJapQFY14W8zmfNzbmhEisKIN81j2I0zyeZjpdBAgBwAAyZRmMAWesIdGdxyzsFC0m9daAtaAANZhCWqwCYuiUm7g9Q0S/dNVNHVHtoSRqPOiU7IC3SkxK1Gi7wnlgYxjrUAWhRUabaLBU8xqQjFLh+aVIKfE1sSBdbSldv6MkdQ3DdGydZAEvkObFxcJ6kDb86ngXFlb441aDZaq67heXxebm4vCm0tEN4kkwIRsAQkJBRHKQnptqx636g2GYhGgWISkFyEAQFidIWnaBubAtupf4U/nFcXz8/TRtTWClRX6tuP7cNCZAgCuwqqYhnUBc6C6SSgLjisdcyBD6WK+AMDtOWhGMTq+x6VgkaGZQMufAnu0zxICW2iF1NRzXHaAFheBvGbZrqytwNKVNX5x9bt3G/6BCSwaFhYFwMXFZYwnDoXr5P0o7k1MlUpLSinHWqsBQQ4XmrRI5tC3BXAc9ztwICKDQAGdTld/9drdTx519ZenJ4p7iGILAQ4IRI9Ya5BEkh22loUSiNLxFSj0yXNCaymIEr577f7etTfu17+wMF2pnT819c7pydI8MbMQ0giBQNY6d3YOPr932P0PxULYkJQcgCNb1uq+5p4Gj2yr4FK13mYoFBAAoNlsQugpMhEQSSQGR0sAHUrkwFFWStG3RDEDawTQ2ljNFrVUzEKgIKuU1eShp6SnAd1CoCxSnmwyoxBmY8Qpz1E5BBZw3Cx6tEqnvaxPxvilll5GQCApRc515aTriDIQMkmn76DsU2J7oRckiS+Mp9E6FOkO56N6kXWehOl0WpCO7BMxDFir1bBWq8FydRmv1utiGUDUg0BNA6i8CaVxpQukPSUDVyeRqjeOsNnsSDKJ6wSeAywcz4VQKDcUQghjDFviKLFRyxrZDRxMisWACkHRKjfWrbZJojBJKg2lz5qi2d2as+94xyn7ay+9hLVaja5c+RUxtLB9L2Ozyuebwj2IVEdHbjHI5V6rb0/86NLlt/qT4dlX9JYxQEJzWoJhkPXHyOOVcI9rlI1bZAc1lBgdKZrtjrr/+r2t/k7783nlfaE6NXlXBWqPhGgCUyJi0OgZY7W2khy22oAjNEYMIC0JDh3lCvKc0K8D464R9rrtJVP7r9xfUJv7PzZxfvpy8fSUYh1bJqLU3IJgGUBISSTA3fz6jdv2fus/zk5XPstMbQSnI0zSTwDipGN0pdqyf/fHP0aw8q1ufhoJ0+3z8+KZd0RCdcoiDvZkZBJZEVYkzb6EoKzOeJ48sAAb9Vt4o9VUPRt5JRXkJ3KFCc918h46vqOEEoRIzGw0UdzVpmv7nV6v13bAjyp5Xy/kZuNKodzvm35iqG0j0bfKuOZM6YJpnJoy5cI9girwSr3GsLaGV5cB5wtN/FzblVPzF+Qhx94M+Z5fnlNBr+e3hZjQpMu+500qqQJrU8vkSUc8jpIRxhORiQhFugnKCSnLAjBxhQJg8NnIqOjl+o5yE0BpcgVjHBnaBNq2Fz/Qde2Y7uaiKSfzdqNWpkHsEJ+Iw6nVcAMAPvRSU7z6jkhsb4H0VEW13EhVvaJyJDlKFrDR3hefunkdd3THrYQTM2UvNyWVI32h2EGBJIg7Rpso7vePdLfPvaQ9OTHZPxdWqRpOkRJsG722EYHVLXaMH+X0RscYgHULy0DjrqCNOogFKMrekVW5+Sk/Z6VrZSxVgqgox5VJn30AgAAgZ0gYkcO4q4SZkujJhBQGsWuK2oN79sjd0wtJ0dyHdfhYrWbXv0HXgKFI3m7Oi/gVV6qcVpXQOp4357hSoSNjTPwQnYLExEay04hlJSjZwNdG+y7LKMG8E9L9Vt3EZj3uboIpJ/P2CgC/+E2sGCfcyq+4spcE3nw5cPOVimBtBThW5B0ShlCCcYXKFREgBhPkEWIA8ADiGKDogdUWNXmNOGoEZrpcMp3WHSovzXPnIMJivyWjMJBCWSH7VqDrO55BPzwlZTMSXK9vw42oBe34yJlyK5OFXFB2UCpHCBaECK6ESCcUx0nSM0m/FR92HC168xNz5ly5SNO5ojWEuqkj7bmgy/XE1qFrYc6hxumKnX0jdTBudgGtzEtWLVmJjPLzUzI366q27jngkFB5ieAPBAblGTwAlUjsFgR6ABAZQbJQiLmbmD0Ay0mk7cWG3oMtvrK6jt9uHBoPyj1dHXTPUBNGdO9rJwe+OzGRcwyVJJRIgCERKxYzVE7rS1rLqmg5DwDGVlmitTQrzem4QPtobbe7rwHmknITYGNjhQDWfvgtWEPm5+cxl/Ow5zjoC3QkiHyxmDsDJ5aehwIEh2nFOF7/PA36FQjyzvb+jWYnfrk6UdxC4gcgqS5ZHqGkqAuOhkRb6TkkHAekEGhMIoGMIkIXwfHDnPDzJTfUCW/tHLS+VD84+vjFczMfXpiZ/OlCPpezZOHug929m/f2/0UY+PcYqSmQj4BlByIdTxdOm47ftO1BzIDv+9i+dVPmlqpS99hrtOO8tvEsIsy4SobAKEEyKkQBgIqIFDMpTWQQVGDJ+kCscp6rPM/RlpAQSB3UD3xNdlYKsQAoTpOl2UIxdxGFcGCwVI/ODqZNfflkxXpEHFooEBPLQasTne5BdCkypkWGLaBJtLaJZYjIUkw66WmD+9OlfMMpOT1tuQ/lLvb7mmo14OHGtVarYbVaxUJhGff23hCX/YI0pulICr3Adzwnr/xeNw6ajSM/ts2JiXxubm56+vSTj589V8rnZ33frXiOChHRAwQFaRYCJXHcj43d6XT720ed/r3dg4P7242DHWY+nMgHccHNRT3WHeM6vSoGcad5x8yZvo4ATK1Ws2tra/jNXJnfCYMFTXjF06J9656yFddt9EwhVyw+der0wodvUEP0bcwGbFqRfbhRGIirQRHZcXk12hyngeXIwlVy6/YDu7ux9WkvgV+fq5avkVJNQbarTb/rCDcyzJp6jqFQ2uBIcxKk2zjf8dGfBGjft8IvW6F1QTlsVCJhVyIEbiFXAMm3bFvv7r1853a70X2ienHuceEoj601aciVJCPAvfO1a1+1Nxv/88LCqZuJpUMHcV9A0u0JJ4ZCX5en8wY+3iJYBgAhHt3q5aGEgKUqiO1boMLAd3RVOzldcWPs+6Vi6Me9xPlq+6bYT3r+mVylWpu+cPrHLk2fOVWozOe9cFq5zlQenaJA9BBRpJmWSGQMdThJWnG33e729u53GvU7nYPd262dWy/dubE5k6scni9MalfJPiF1OJA9aBwksAemoSq2fL5J8BLAbL4iXmqD9OYTp12H0EOR++yDGxNG0WlPqVMgnRmUOIMEi48zFiQgDcJScLgBRIbxOshjmZjpXdiy/TON3v4792z7kBEOFfFhZOOWttQhQ5pYMFqmmHS7JMX+E8X5lswHR9K1UVxyoVxowgrUaGlw06VusHUsF5ro7UXipS1fegqUp5TnenEYSDfs9mLnK3u3VazN5KXJhVM/8/T7Ty2WZs/mcsGlvBdO55XrOkIKR0hGECQBTWSS5DBqdw+j3oNrzQebX9vffHD14Np9tLx3sbQQ5YVMtLV9ZNOdVm6Py9W4VZ+25h3bBG/cAOg/BmW/IbtR3vHz7L9x83a+qTtTYRhMeo7jcV9YK8Awg2DWDlt2AKUiBpE2ziYdGXPYTg7rjxfmewthJWolXrvs56O1aB2gVrNXVlfhxdFOm0dCE6pVjKtdKY+sQ1YEr7UPvFfuftHPT5QqvhShAumAo5QE4ThCOKIvBVgL2loyQJYSc7SYm75Hfv7IKDcC2Erg6ipfuQr04nEs8JsC+8vnmwK2thyVK/pzk+X87+6+7h7uv+b7MheEoes7gj3WwgEgR0khgZUCiVIiKMmKUVijje147Nx/cmqmo5iidpDowJ8wuX0ATvpCOr4TeuCQ63sCwW9R5L7c2lbNvSQ3MzExf35ybuHdpWfnz5dnquWgcCp0vcqEE3iAIHhYL89am7CJG/1uL9Hx/na3sXvjcKd+7WBr+3M7r9z02dm7NDuXTDiFOCr0u9CGKOd2EkPKdM9GDABQdB/DuLnrYtF4X95uuEfNu9OeIydc8HKuI5VARu5LslozExAhS8HooAAhWAoi1rG19b7t7z81vZAodnuqrlrTAP3t+by5srpq33SuHzGvjMTVyxXZfCJRMlZO6BQKzfiw9Mnr13KO45YcR3oKXclsBbEVIASSAWHBApMl0MRGG4NISVfrjrDUfKZyqkOs2gcw19v+EOiVj67AGq/Rd2PF+gONwVpbWxPrAHLe99XZyPXjSrjQbPaf+cgH3/E3K+XSkjHGIKaNBHEUDcyDqsjDUtRpFtawD0gUxfiFV27920jbT5d874Ym3nMENhXJrlIUNcHRVRUaY4iTacsAAO6exChqyVzOl21uOoIDB4Eca9BDRwYUae/+ftOdnZ5451MXTv1lBJz54vrt/3ds6dM51zswQhwWbdyLw1Ki2ndtv39KB8EDW6/XuVqt4qlTp+SNBnhF0863CKZyXvAnpZIvELOLyCxAtKXEGBAdAeAwsyRgSWk3HgsMsVQyarXbr8WWb7hSYj+K85fOzf1YPgyeNEQKmUPL7OZCP5iayLs8qNM1jPsf5ew/XHICeFTNYbvRiuI46TlCtogtDSLNePAfAcAEANoYu31t88E/sEZf8xW2SfpHEyrqvPLKK6ZcLhMAQHWlivW1qgBoyWo1lnHsuVpBkA/cQrufhDvbh+5EKTx//uyp90xXJ95RyAUXcoFXchz1HYwhgG6/32u1e/d36o2NOw92vrp/1H2jEHh7hUruIGoedZUXdlXAfWoGcRTdMtWxQPjfV2zWQ7FX7QKEZQgmXj+4d/pdT731v1y8uPCTm3E9TtgIDXZQhiGtO4XD2kPAY2bEh+u3CQZHqvs37h/tfmXz31enJ39DVXLXdNJtAXs9jEUcFEBDW1vwYts+VHRm2id4CeDaxS2eXq7xCgB84qWmaPpbOOtWcGffiLKbx2ZbCyg4EnrKFRz56OeKYJKZ7n5rxi3nnp15ZvE/xbw7DcQJsvU2X3r95eR28/9TPTt9Q8fYcpVuGU09y0EUPubG3m8ntvmJT9Daxz5G39CUf0JcLYtN2FTyyDpBznFzKg5AewUQXNhtHxYaUed0OSieff7U42ffOn320kJp6kLO86ue7wsChC4Y6EICMWjoQwIG0rlOQNr3T4EADxQUwAUfFGBC0I66zc3m3s0v79545eW9u29YoAfz+Ym7ruvuRH3dBqP7R3nUJVE0jbjLXjsR3nSoqBXnkXT1wOpzP//Ee/5C1c892zVxYNOWgmgR5H7ZuLfdJibapv2pcDzbG0bWrKFdkpEZpOKFQ9/Md7yopFxyQJAnVCIANQihiZmNtS4wC18qc+Nw9+trG5/7J6eDytd6ZFolkvFeSepFWDSwvExXr66KS9fnsflCGcN6VeRgXcVx7PrC9QMnP93vRdVrRzvBQlCZft+ZJ19Ymj777vnS1EIpnw8RJbQghiOIoQcaItBAQKBAQA4cCMEDByS4oIDAQLfdi/c6jd2NvTtf/Ozd175Yj4/2zoXVo7wXHLR1tCelbbc6EE/4pF1ZtgC70BZFFZowfH339fAna+/6+fcsLP0CEPuMqCSAESiMZRKarCIiacAKRmTBggHBuij6UorDf3P9i//u+u79z1a84G63qPcP2mEyPw+6+XcG429s83MVQEzXaqIKe67yvNx2vBM+O3Pp7T918W1/yTBNx9Z6DCTT7gUSMN0aSMvkEDNLFHyo291/ef0LLzaT5DPFMBdzp9HbuuMn7/g/NfSL8IhN2uoqvh9ALC4vKhU3QtcEE7cOtmf+wrMf/slnZxc/EJFxANFjIoeYBKelfiQzKmYWgCBTUwFbXzl8q7P/b/7Xlz7xvzxenTP9OI4saaO8AgswShKHzKK4nxyV9luHM3OFyem3n3pi4YmpU89X8xNL+SAoeWEABAx9sBCBBgYGPWgeT8AgQYI7+MmBk27CtYFWt5NstfffWN/d/Nzn77+x0TK93TOF2X0XxJ7GbkuRF0XCWugA9D0SE1L6X9u56/+5Z3/0J56fOf+XEpsoBnBwuJMEZmK2QEyELJlBcdo0QiKADV2vfRT37/yjr/32rwvD14DFPZZc72mKy828mdv6y/bFVXh0ksFgbllZWxOpsAWn0254haIbNmO76Avvfb/41h/58ZIbVA1YiQhi2AiPCAQACGYWg8qDDEyMDDrv+Horat37nz77G/9faezn/Thu6n4c7T1X1MtXgb6bIPw/cAtWDQCiQw+TCVckCcnQdUKlVJ75pOBj5IG2wlH6Gx6HFQGlcV2otU20ph2l5JEljgRBwkJoDqXuQ2SrFJmt0JrpVote2Uvf+wXfx3Y7R/2+NDZ2TVzoJmBDpYBi6vdjDRzMTZdye43Dz3xFmzuVQn62FyfrpVJ4BLFtFdnrdTrteDpsmjYAjYurQmEZHzibWAjbkhNPqYgWnq0t/rVCLpg2xqQxZghp2e3BDgxRAOMgujg1c1gphftgt/HFL75y83/3AiG8wJ09O1/9E4Hv+mPGPQYgsJYZh+UYhn1jT/RG4bFK9se1IRamy35qpIepb3HZnmz3os+uX79/N5jw2VVRP45j+cILL9goisStWz72f9PFhYV9cSiEA/2C6ziYAxcnbj7YDUthsLD87mc+ODdd/vF8PjwzPCYittbatC3PeEAXIw4ihfmh3QDmwyDIh8HF+ZnJixfPnfrg1l7j6xu37v3mgzsHn507VXIcqVw2LI5EHzq+D9v9vvXPn6caAK2uro6Xq/iOSWOvtkVjGlRgcm7P9IMwn5ubmq+8Zde0KGKDli0YYKBhzyMctRpAxPFrIEYJsgzAwpFy89b9/f2NB/9qanrqyyJQ29DvHbrotpUn4sOwZSabU7ZX3aK9OtB0p8ZzP77CcHkV5gDgRVjhJVjFucurtrm2hjuD8gZlN4/tvhUzkDM2kSaeNInpxiY01kxMl5POwVHr3mdfg7m3X/xrualicONzL3/F3Gv/49mF2Zt94gMHbNfBXI/zndibcZPNzUWzvHWV5paWvuX5WllbE9vz86JZ/4oql/OuReEXfJMTFEzc7G7lHrSapXfNP/HWP1t7309cnD71ZCmfL0UScBtafJ3qpm1j0+dUUBEDEhPaQS2649zh1ForgVmBhBBdmMAAJ/PhxIXi4vNPzJ95y37raOfTd9e//u/vvPxxX0pzvjzttVi0fd3vRSqIi31l4yBBRZHkIPR7PVOaLOSXKqcmf0R5TuhBaAEQFSD0QUNH70Fs0wZHBAxs0wSFYbxj2j1hLPaRGQQRqLKv8lPl/BTkIEgDJwUAowGGBAjkIKLLB1eIoDebvM7rbUE3NHdiqQI7lXTpC7bF77gKMF2vidhdF9WNLeU4i4omA2fKy+UM2PxXtzdnFydm3/5fPf/T73x29txzfiGcOYAY7kPHrlMz6dg+9NlgDAQJWdR8vCGQiKxQgAABLkgoCA+qYU4tFGbOXJo5s/BTF972rq9sv/Hax2996YuvNe6+/PjEnLDC8cKyadmujaLoSLvhBAvSCjHKWWsnVSl4V3F2/kkDXR6emWHqkDNqD0WcLkgCEAAlMOcgh2foTPiVuze3K2HQVS1sl5yI4naRln556UTpjY2NGl76UBObcE8ehokzF/j+6zuN/C++54m/MD17+oP70GQfZCqIByMIAUAAggJkCQgeSDoNZ+V12/zQP/rtj3/1XReeck1pwqpzW/bqVbBwdZXg4cQIANj+yDaqW1XlFvLeHvSKpydnn1+6cP6v5sLShAINCAIsEOhRrnDqXUg/H0ABggJgDwL8+v7eB8jaf98HahOCJ5S0DFoocPw7nb1gr92ovHDmiXf/H5be/75L1YVzQRiUYwW4Bx26R03TMdvcZc0R27QsDBMOOpyiAMHIApRQrADBAQQPFRSFD1MToTw/cbb21Py5J37q4tt3v75z+9V/fePLV3eO9r/6RPWsG2O/o9hGNCHITyRK3/fz6BZtDp6amp1/IoI+m7HSyGrYBR1wvHfKsFAWIgBMwsyTbzl63PndV77yz0+XZ9rtfuuo4JL1Tkd0dWuVr6wCjWIOH56zV1cRajXw9iJhC1J4KlShCIPXD27N/dl3fHjlsXOPPXsEHXIHPVpcEKAAQQ42ZQAABpg1EGpgJiD2wcHDw875DkWvzCbO146kJ+OqFpeubyPA3B8OFyGkIf2gO4mwylNSKt91HD91qOCYvX2U+TYW0HacSTjIMgTHkcJ1lEnIshAsXccT6FjQxGy4wIA+ALShffkyl9fS61yvXkVYrpMLyzj1xhu0v1+0cdyyANo6pYIVSd8qAclMaTI0qF+/s3WwXir5wge3q0qqKyOKOxOkpy9fNnUAWhmMqqtXr2Kx+IY4OOpJML6ExLhkbVkJ4QCATRsyH5svjjO500VfCACAQQA6ACGRbxM9rZUjpIOTDGSJrLGWYaxhL473+RkXKkPhAsM21cNQtsF5ttYCwKhJNDxUoTRd/NM/FciAAqVnEbViT5mCK1utoozjOgMkKGUoduPYEdb3AomFFsXB4f3O1NueufQz58/M/kw+HywAAJAlPTg2wWnJslHc2MjQM5bfNt63KF3MLDEACwDO58L8xXPhexZmq88+2K7/xu+9ev1jrivrU0Ho+oXAlj3Hmj0Fam8P7jWbUBs0JP6O47GG1qv5eRH7vhTtxOES+vf2G+rpSxef4lDOH+q21mTEsKfgsB4DH3ezTC/HeMYeABAgKNfB3fs7dvvlO/9xdrL6BXCxzgQHKJ22SqgXJoEpFz5gr0Gbpzdy/MmVlbGd1OpY7aDB5L+ywsPKONNra6KwAvTqS01xGcDKtmtNv8W2HBJYBr+Yk1Gj+/W9lzd/CwvuAu10fn1qvrqZaH0QsDzS0okTt5MkZPRjm4tmcfkqwfLYZ31zyxVuwD1VLuddpdygEori9tFh4Y2D7Znlxbdc/utv/1Mfemxy5jntyfwdaNJX7B1zkHSgRxoNkxg07mQGQBoshoNYw+MNWFpqAmmQBiMA4T4egmOV9VFyRebg3GR54U+V33P6PWeXLqxd/72137t//UtPT55uJip/CBS3dEnF+QSo19YCvZ5nkfMkwXuZdvqxNiqyWlhksEQYsRlkeh73jBwvH0OCGZEBKe3tyJj2sUIi3KZ93tcNCMABhYIJ0KYN2Wno1mdiBlQS2qbHiDBruF9AFbasL2TzqCwKyZHYDDZxYaGBik6rXt9xSR8Gvq7kbjS2vS6Y2V++/BO/8Pb5x346n88Xb0OTN+1ecmC7GEEClkkYZkxbKNGoZktao3lwFAMJJAChZXuwg0dwQ+zpIoZ8Ll+eef7x2uxTc+ee+9Tdjav/fONz/2Y2LMjJXN6LPHMkctBLNBKhVJbQn5mYKl5/sLkfnp4wkTSkyaIdO29Ewyk/3WpIRJCpRCVPuELnxLyX8589Msl2UMo/ECY28miP4bEabdTSKWp1dRU3VmrQXCpj4YZU6Hfco6jvvXX+zDyX3Sd/x75GfRtrRsbjzz/ueiDSWmXggqIpVRT5ydITE4X8HEndNF3S88G8bta3zEptg9ceisXaqG1ge+894kzZkZ4SuXY9qjxz4cJzd9127kvJVt8yCQ2ENu2UAMR0PGVzOl4dkKhQEnhSvXTv9aYvg6IhrazkqOTnYb/bxtdaW5UfWahd/rF3/an3n65U39r3wNuEBj0wO6aVRJwwCTvo7zDq2gDHjd/TAnfpKoCU2u8UMApAqFvBd7QEX7i6KAOeKRbmni8+Nf/MqfPPXt189df/yauf+a3HyjO7nuv1EqK+kEjdOHJPT854X7j96h0z4/fcIHQMaWaGsRJL6dw9PJZBmCcwMAtEyktf5qcrTyrl1SKMN2UlEPZQy+Z+oi59ZJ7g41uPLEEyErbNplALRrR2SbEi98hqv1qanJuarZ7+nL2tm9S1Np1HBr6v4QI5MNug4IHQAs1E6Dmwcfe1rtuyri1NqELchaCVx2Y9j3OrAPC3vvN8wh+IwPKPXFSeRYqtA550j2st8qg7Bg/dWzgIGuVj3TWqIMRAuVzozE4Vzr++uXvbL+RMJEzsMMeklJGJZmOJ53s9aF69irBUpfNRxO32MsPVq9CGq9xO0/y5nqtzclTmUghEibTtI5P4HvQtxWpmKgfQj03X9kynTclCblrD/fv21toara+v8/og5fz8+fP44IGDblWh309k0xhXsPAAQACRGPg2xXDrTYNU/EHLArCMDGCHYgkNsGsY8ozMQMJHRsEMAjEdIjyq1D3eBugha9CbhBefLIg/6NiIw0cjPXOiYaJARCmQlRRCGCNFWBAibt+USVJmd74jWCjHi4PQK3ilra2mn8/lLvzEh97+16anSu+FVMzpwVIkeFA1Uwyv5piI5nF/4LAo5jATdRjIP9jOWGuJmdn3VPj4+YWfm6pMvPWrGzf/6Z2dnc9Pz1ejTr/Puqzi3EHRXLx4UV+/fh0+urYGH1tZIRxrsP2tM4EBPlqrYXt7W8xXclJHvqIo8clCeapaflcbYxFRnO6NMQ1nZ6ZjJ9HwOwzqqo2uEyMIpah91HG3Xr3/2YlC8TdQ0X0kbio3bPQb/QgAdHk6oeatW7T8LWo4PYqllUGdmVs1mktLItieZ3S5Q2jzhcOEOhxO5mPuxv/IbnfkxHS5mxA08sI9clSuF2PflGjOFItgNzbqvPRtfOaVQWzE4uamCheFJ4VTcFjkP3Hvtfw7z1x87r/74J/4ufPVuXd1Xfa/bPfMXX2YdDhCTVaYwcJPwyKso1IExy2w0tZVY8MYB4OYU9uABAQJhBEL7HICO7ZlKiIPS5WZJ//icx/+G49PLvzm//byp/71ufLsA3SsTICO2POMhQSVg9JoQjZGdjgCA4AGLFrioTBASvtGwnFEHZ/IaEqjIWlYRRYsAAhgsIComSBBAsVisKk47hsqUjMYOyBASkRXSIeldAmsirtWQck1VfC4Xu8COEKpAnsudHOOW5j49P3XKx86/8zlP//0+34uXyo+ex0a9rZ+kHQgQcNaaB7UY2MaiUMayzQ+vu/H6nkNHkkQELPFHhlsxD26Jvb5XFgu/ujS8z/79PS5x//e137rf7ve2H9tsTid12DqJG1faYFWWszlcrB3cNjYax+2RcUratJkGQTxyez34V5QAIJAZgAhHTY84efy+UL+8VazOemD6+uerwUbu7FxTwKctuNFYZutjkQfHCUrwe3oXvDOC08/FTs816AuabCSGY6tjnBce25YcFeAFoklKAbeubmp2ceaUXw956LuUb8fdhfjXq5mh4kFw5IF2x/5kOhvbcmenPEEygJpmvYn85d2oCMPqEsELNIuoCPzwImADeR0xCqhsN/TornXPPI8VU6Y8w7K+OXtTX586tQTV9678pMXpuafiRx2XqK6vmsOk56NhQGLhlgMe4zyoI9J2reUj0NFxsobA6cWNHkcq4sSJHfIiBbHvG3b5qZo8vncxNSP1p7/i2cr08/+o69c/Wd9azYKQdCKrY4YrXRCH1v1w/qDnZ369Pn5MwnHhgHBMuFQ3I3KWA7DqNP4X5AsZAyWp8r5yfJU5em9g4MvTEHJ73nWuh2XvGIkNgYb4m/kTQBYx2ZclX6epWsx2G7uFp9+bOkJr+BXGnxkDGip2WLaY5WRBlGxPFpeBCIKkIgsBYokSrB5Z+dBPsztWUWsMeZ8MMmhU2RY3fquijX8YCxYACClRJRCMQhpLGlPAAhGHhSfOi4wisgDIwcfZxMO5wJGa4nPnKq+b3v/SHR70SthzstpKxzGSElfHUEi+03j60LhjK5C3hq1zd6FezR3c5H1Kc1JkvDGBsDSUpXqpRyogwNu+z6VZGja0EIpBHSNx0WpWOZjqpbK1j3YsyupFYFXBtaCtcEhnTq1yA+SPUicAwFaK0brMoAaKCI8jr05YXQafC8eVPsaPEccoEJfCDBCoDpeUFJdNTxV6W6eR0UZRgs4HnemGIhTPC6dMwzUpGHpIxAgxoy5owrwKKRIE/gHbyhEglGfRbNgZLlpoORPOabfKkrlz29tN0qLZ2bf/dwT5/+zMPRmjbFmIKPkUCyf0FBj7eZGImSsweSo3+TYdx/TiDgYGWSM4fJE/tK7Lz/53xavh//8y6/f+sdnpyu+JjqEPPfu91v96PTpeKXTodXVVSSib9tViADw/uo6TkNRRId1JXOTTi8x/tx05TkI1TNHSdcMg0jTHeqggcqJQMdhKXYeLWaUXnC5c3PrHvfMv/BPFb+OxnRBc0/FjWgmmEk2YdE2b/0qr618jGBlBR6ue/SdCa1Vnru6KrxTFYrdOS2b2xCUwcbWbUkUd51iATtxggoc4yg3RtPUbS+27WLRLtWrBPCr8E1LMhwfkwj6FSkft47XL1b22/szB1H3wn/17p/66fedrf0J4zvFL9GWuZc0kh4naICFBQOWmTUzEg4m54GlmuSwGt1YA1IxXJRHNcNStwvadJEGO4rRkmREnw3v2I6eVxPqPY8/8zOh4z3297/+O//gbGHqa5gwgKI+A7JhqRiRtNUcMYEBSJtwAyEjg0ViSwOHFh53ix9ua5ARxrtPDHfyhAjMAgSkvR916gI/4boHTOPKNAqIwFDMZCWxcB1XsLLSh0TFcSz8souSlQcEpcjKU290di//+bd+4Md+/NJb373vGfdz9mbSolgYMjJhAxrSBt6DcqhskUcxOaPZaGTBguPiygORRQxgWaMRhjWQIGJ4jXd5S3T1M1PzT/837/7Z/+Z/+vp//N31+p1PnStMfi2iZM8KYZiQfc/r9w8Ot3f2mvenJmdqEZvUqjEsFf3mfvfpJkogCxLgOb5TqhRP79b3Ji06QRhgzws89JoSYb4prlaBLxWaGL/iSvdJ5bAQPntcMLGeLlVK7z+U2mnbJBlYzAcS2IIdlJEcWHfS3mGMaIiMVLJanZv6wPU3bhwFTiVRgXMULnZVr75JG7WaXVpf52HmYHtvW0yqinIVBd1+MpkrBJdU0TvfhIhitkhACAhgmE98x0Hc1cBZaVlJFHfvb7UPD466k8XKuXa3bVtJ3/kzT77r8nsfq71AgeN/1myZA91J+qRFarFKXbsWBo0bRhtkGl1LHvNGpFX20utqGUAjMhCiQAEKCAEF99mgZIExW2jbyN7ABlyem3/ur7/rJx//H7/8H/7FVuvgN6byE+04RishkSoImrt3d17NLUzOJ8IAcWpLJqaTZa6HB5d2nEAlJFsmiD1Qp0/PL93Z2z5rEV5zfTBJNbS3Xycz32zSldVVflS9uqvLgIuwKIpBSzRi60o9UQQhF07Pzz/flkYc6Qg1GTF0fw8qGYz7KQdWBQJgBsdx5fb97Xb7QeNz5cnKzcTquKQ922sIutfa4l9bfZHTmi4/xAJrZWWFf+3Xfo2DxRKbKOJ8OE2tfmK1MYlgD+3Y3YZ4onXVeF/B42bOiEBMHPpe/vmnL7z/lWt3CvVGu+B5LuRcV3Esw56I2h7InlE26nbrxnFCCy2wmxNA/UaDzuTzdP58mVoty1C33IZJ8pJtJliwZchhkliO5yxPa+AbN0Koqi5v3LpMly+f6B0NKysr/NJLL8HX9n9PzJcfRyFj4UgpjWGJD+UV8aNW8EGhyVGBJAmAiEIASkxdCQKPu6cc25ZwfCePxx1QRv9+vMwfS7Dx1kA48sQOpd3oHA/diogCgR0WJBDTOEbPMaIY5aQ3KWQ37udcEczWG82Fpy6e/Ymlx878vOsoxxijEVDCWCsjHLZ6HFbjx7EZ9riZ9snnRtmQfKKJ9XB2ZkwxxhjfceVzTz3+55RSwZfXb/yzuemJncRPdoUMrN9um+b58wTr6/Y7HbvLsAzb/nXsVxOhEuk2Gp2JuaXHnk8UlGJjjGUSZsxKleYHAPBgg8BvSjFmcDwHtu9t66O7B1+uTJdvMVBbI/cgPxn7tKc9N7HLl5fpRVh+c6HD8eKS8OY2FRtracXnjUGzoUFQFCzBMuwkdV5IIj4KJCko6gm3azqWRVgMwbRCcK1m5ZCNA4dm9yzFbpO3r38Cy5fLePUqpAH1ax/FtZWPDWvWjK7FIGMQCp4RQddx67o5OVOcfPd/8e4//Z8vzsw+/grs2ZvmIOlQJNIJ0IBBBgsD6wLz8aYBxlPxx8X5yK3Fw8Ke6cQNIBhB4HF7MTEY44IsKjZ4nfdgn/r6vYuPP/VzHP/SP/76p/72k8VpiI1WrgJthXHTXlsIJBAsEBgksIPrmcaAwcBCMF7cejgrDWWLOGkq5tTNSYO4GzEWvzu899JEHmAECwkTGEuSBIuEtXR9JbsEjvIqjB5KFccFiGl2q3/0zC8++/6//PYLT5z/st01d/VhoslKywyWU2FFkArDQcD9CesNDZpq4dDvPtjgiMEOAQfxAZIZYhKo0YBGBh8UJLaNv2Ov6yV/Ov9XnvvwR/7By58ofHX7xtZjk6d6vbjXE0ycgDVO6Bwd7B/cLZmpS1YAEllmZhxG7NDQio1j8xoDSCbocML5qeIk35STltlLZOToiGP2uhj7Z/ESAHh7kYBJEOowVDH2vcTosFIqLjgFf6lBXYjIICGNLNbj0+KxGy3dYBproSEiEVTyNQDxeXJUU8d9P0n6DiwUbdlt8gbUaJg5uJAY0WqBMsIPWlF7cmaqcgkCVerYnrVIInXDDhusExw3Gz3hIaCujUR9Z3/PQy/pcFLJu0Hxr77tx9959tT84qtUt/eTI23S2noyYTphieN0EwA0jGXlk8LqRAzuSSULOLC3GkCQTCOBptlChAo7nMAnolv6bZWFwp9//kf+3N/74v9vq5NEX/QcYRMy5IVe0my0rrcancveTK6qtaVh02kenNuhQ4T42MOigUEwY910yKvmq8V84bG+7heF40dgYjNNXd184ZxpbpQR3uSWreGlQhO3tjaFiotKecJrU7tQKZXOBRO587v2iHpk0JIZuScJ4UQkWFoZkdPIDWLQaMWDu7s7jlCvWKS6sDZu2tAmT27SdGfnu85Al39QGYTDb/bSSy+JahAIF4tKOaLQ6vbLiwszby3kwxkmtulG4jhGG+FkAYdR8OjgLUVaPJMC31VzUxOzge9Ue72Idg/bh704cgVDqJTwlVKuVI4XWev1kq4jTCwLYVl0dU+xp2Xc6khnFpA6sTgMGWTP56NqRNOqR3e05rDb5Vwu5n/zb5r8V//q4qNOOG5vb4vc3JzwDvsqcq3fj0xBMJ4+c6r6o46SDg9nL3hkLYoTGUhCCjxsdY8e1A/v+q5nHSXDM3OT56UQanwTBAyjnTDimCUMT2iU4zTyYVDLwM04kn5jwuV4Zz0YJEKIrYPmlw6avds539FsbU8mvpGuK/sdCj1HTu7sNstPPnb6p566ePYXpRRoLVlElOOpCziWacX4LXJYH/r30V8OfUHDN0A8Yc4iJkYEnp4s1ZSSnZv3dm+HgYcWKYqFMJ3tbZ50HPg7f+fvwMrKCn7yk5/8lu1HrqyuIiwu4h72lHHRF1ZVunHvwvxjCz9lQpyNbEIWAInHLRqDfQEO+/vBQ02HBcSkxe2Xb+/mjfqUP+G/qnVc9wliDEA7uRkbvSWgX4Uaf9MCh7CKG2s1XK/XhXcwLzrbBXHvs20JS9Oi3d+WZ92OyLsdsb3UEeVXhDyEQ5Hf7cqjKRRmvyXcnMFuS2KMMdCBRldJhqMj6GAbgX3gkoRmUhZiYgZz1edxZq8v6IGQk2JG/PiD34TpL9WxtgK4srYOy8tXcaNeQ/PAERxGarpwyv/8zVerf+5dH/4L1bm5t/9Wcr2/aQ+wR4lIwEDEBhIgMMBgYBiKe2xdGVS/Hz0mHthgRiIBBm5ZhoFTIpUQw78ZLDzp+xEYTB/1WOMOt21t6uxcAd3mF+5u3Krm8m7CJAmFb7QOwZGnS3NTl7W0niFCxkFsy1hxUX5I8jGMmVtHC8rxnpGOxwUM3guJIT3+wbggRmCJkPQ1HN6r3/AD/xVW2JZk2TAKFOwIY0JkMXmnfXDml9724T93bnHhLZ9KbidbfAQxJ6JPBlPLlU0FIlBq5Rh9Ng+iyI5DCEa+mIGVeZAdOXR0jl5DkAo3DRZjtmiQcNt2iFzB7516bPF+p7l9q7F9eyLIOxqFsJS4Qqig3enkC7PlJ8FXviELBJjGJXF6bSzzoN0Qjz4jvbbASir3cL/zmk7Ma66LfcPW+KExlb2IbE6x6hrEHKsesefm/Pz+4VF5pjrzbGWh+qEj6ikDg4K/OPy8dCtpmYEEMx1fD2AESIAYJHBz+/CaIDx0lWwB+n3ZB2YK+dqDT4E+04dTR45stxIncXIeKK4cdjuPX7hw9j1qMjzTthExsLCQuqcsUDqGEY9F+jB+T0qOe304urZ3s+AHexEnzi+98BPvKExXzn1WbyZb9hASNiJiDREb0JjW2LPAYHFwrgZCnfBYsNJoXB4HmvNQcEBaw3sU/jomdglT+WYH5WZYAG7ZtjlVmPSWSnOl37r50mvloMDaoECFKonivAWeyc9OzmuwxMjIgzg/HitVMhSBgxI2aJEhJsvCdxwT6/3duwev5EtOYhTGToCJ07tndW+ahm11hhu4+vQ01vWhVAYdsI5vHWei2WpNnz937h3+TPFdTdsFQxbtYNwPY/6ONxUwiqO0wAhK8GG7I+6/dONq0S/8HlnbBIaDnjyIFg+eNOdv+fTJD3zyh7vQ6KBuBczPz0MUTTEUgXLgRsSdZqvd35mbhmdGUc7j5tSh1WKoCuDYrcQ8zJgDNMawlEKdPz174fTs1NxRu/fWg6POG3sHrfuHnd7NaD/ZUlJ0Pd+JfMeLPEf0BEGMJmcsKWM9bX3tGieMjQEwUO3aif2ivTsFBEnCAADtdptrtWVeW1vDgWvwTera3dvDvusKlUjhootW2pMhUXDC8jKSQOPNq8dkBSEKA8gWhLDjpvQTMUTj/xtt0vA4qum4Nv7wfDHzmwzzo9edfF8EIgar09gxIVCw4zoAiQ9WSU/J0m6zWzi/OP+upy6e+bNCIBMRI2KafownIlUeKapw7Fw8HBuFD7kTTyxiJ+LMeCC4RdoyQCA9/fiZn2t3u/Wb93c+M1UtdSViNDM/TzcBYOmXf1nD1avf3k2zCrCxdlUU3NNC+dLtmc5EmMvNi9CZ63E8CsIeLrKCx+LJjh3+xxYPZgBH8H79gPRh8nowNXknim3XIdLsWGuKwi49qBLAVYaHq3jyca2zVVjFq1dBTC+BWMwtCjiYFM32XVGYP41xJ8bHKxVoAABUAM60PIwnuhh2XEymDJZiRh1MpS5fGaHHU+AUIkxED2HSgYDyTIkkKxyasvusgoLt7m0xFHNQbCnsFnMcN3w7m9+2O50lhuVlgqtXYam6DhsA6M1cgK1mA89NTuHHb3zp3mKpZa3Lkq1BxmPhAwN3IB13Fx1ZUnkUOzLqC8o4tmUYjgUe5H9junCw4GFpl9HuYZS3ZpDABYAGd/BVu0VnF0//1OLuqVuHcf+W56o+Gs1agCesdWMeW2qGN86YyBoLvxr03xyM97Fb7vjGF+lXGFjVBh0nx9pt4+j+UIhM6c2vDIN0DDiMknwplE7YUQ5OXKvvnfqld37oo+WFyts/ndzWLY4EMY0Wb4CTocY8Wj5xzDswMP3B8SwxLhjHE5F5YHkbWkMMIAggkGRBgsSvJ1uk3Rn5p55+70f//pd+82az194MHE9rYhIK4yS29w/3j/YrpWrJAhtGAqbhpuS4Sep4LBggQkSGQ8/zcuViaf/+A88tTnox2MjTrmwFRez5OYSJe6Lc9aWbk4qsyBkdlypTkxdjSYG21logMYrpg0GvyKENf+Ba45GFEhCIyAlUsTiZP9XZbd1RXiHPNnK54Gjbk7J6usaFeoN3FowIY1cKi44hCH3llAoTE1NtStAwYZo/NBA8o4StkzXxGJCVBGw2Wn3d03WeCGLRF/Iz7ZvoFSe4Z3uYttKCgYAaWNx4vODayS4QJ6/ksdUIxroP4LCawshZyWOxhDyIj+U0dpAZDEj55eSeeUt17pkPP/bcj33i2ld++3RlrmdIk8p7Rwf7jdsT3bgm88o1JhlZrUYbEX7zZiTtdmBQg4XiXOU0vnG/TCjaIFTHJv1+XCjGe9V1XFkDMWxvNYy3gyQ99z0TeAJMCSxXCpPlSy2IVUw6vYwD8U7DeLeTBgceuImZJcr6ve22beuvwbToRDF3lfD1fJK317a2eA7elD/2wxuDtXXxInfq9+GSCdlArq8U1g+O2hvW0gdTqx2f/DqIJ2oHjSwEDyXwp6WymI2xRgr0pyrFt0xVim9ZPFXVvX5cb3X6tw4O21t7zc5Bo915QJbvup5q5AM3CRUmYRD2ieKelbavTDF2j3qJpUNdtI6Zjsq23U6o39/j8+dfona7ymtrACsrxyd8bW0NAZbw/PkylIXFju4JKbRAlvIRxqox6XAy0BvHTJkMpIEhAcCEiDXToDL0WKzSm2Kmjm+aMSfgmNVozGvx8CJw4vcTZmYElGiFYJZSMSO7moQSiEHfRpVyLnzuLU+c/mtCYI6IjEAhYBTUOeaCh4dcoXAy9mpQvmKgNXkUeIcPC6qTHsKT4jAVacjEJKTIP/vkhV86aHVkqxX/thO63a7u8+lqlaFatZDujL7xjTMsYLhWw7DaFRZAWsd43Z4OypOl0+yJsklzPnHMfjGuXZEH8R10wrWbVuKImr19F8SXSYkH0kCfVWjJl/ZMC2ij/qu8Bh97hPWKB5ovFVfPnKrIO3stV9UD79RE5AUTk45jQaIjlY6MmkWB1BJotcFcXyI4FpR1BRgAzzUgrGRPBihNjFJKJAqlQEQii6AICA0LWWJrifMckJDEokRcJJXISb+zlUDXrTyWbO9dN3Pna2Y7ajJUywAmAiHJTk1M9nf29n43vJ0/N7l0+oN9ShJiEnCiCCsfx9uN9pnH15NGqhJHBsz0mouR0LFjWahDYYYoThhDERAsACSDd9wzLQsOzNQuXPjZ3/rS539jQhUbIKBvmBxgDC2wJACwTGgHfqX0rflEUeRhVuqgdRciIBOc3KTAyZD4Yw0hxt3jx2EqCTARAJMAoa2QALFnwZOO70zcONg597NPvfNPz5+a/uBX9X064lgYIhy2ZTp2h4y1JGUaffpxKQnBlsfvdhyd5FQY0CBcf1BSQKT520MvAjGiBQQJDIqlWE+2DYWnZn+69q7//B9+5t/+U4nODiC1LAiLLhy0Do6uF85MnqP09sShe5JPeDlwJAQQCIktko+YK4dz23c4b8l2AuX0EyNVoZAIqAP0wJN9SBRrDLVwpoR0HgsnwmdiNIMYpdTKeSxAeKgxxoYfn5jvhBBBOF1+6mjn6L5l3o0du+MZP2r1t0VhWgmYKALEniRTkBEduVGchBOl0jnMubM96lgDFkfibXjWcZiWzUOzETMCx9ao+lb9PgK8EiM1hCvUV19ef7WWf3renQjdJOkPfB9DgYRjawWemMRHghiP4xaRhr0xR9lkqXdoGJs7jCvB4/ExjOkSgxg8HjSlXze7sHjuzHL1wa3tfhxdk65sK99tdxrda4f7+8+UC/OXLLAdJEk+FOR+UmQNSlVgbBObK+VPV6rlx/rtTssLwm4snB7EqjddT/vNDsfx3vo6zuYrAgJPtpK+qzwoJb14vlgqvU2W/Kd6FNnUcj1wofJYGMpYaZd0jAlARIiMwaOdxidz+WCjr0zbsdT2weieAzRd2+AXV9YYXvwhbpWT6qbBDXT1KlQWF7nuVHlCibiYC492dw9fiuPkIPT9qgFLYqjvx93UoxExXkwAjwNNj6UXMiAPg6sdKWW5lJ8vl/LzZ09VodPtc6cXt1vd/uZhu3e92ezc2m0fbiNyPRcGh4Hn9Hyfe4adTtKDjmjZWPe2DYA0QXDG3EpadqlatYXCS7y2dpkGccfpASwBHJg6upEvHCWEta5QPEgRHQWVn4hvGq9CMMoyweHWIh2FESAmTBSNjX7gNMUUx9+Hmd8kRHg8ig1PChUe9zPCQ5X0H+qeroTQDGDSjEDtCgMOO1Bqd5ILH3jHxb8Y+N6kMUYjonyoevz4QjQeA3YylVFAWqZiGKQCkgfZh4PgxKHbE4+tXAgPF1EdH3TCWmvyYTD17KULv/CJL63fCkvBrtU5o0VsnVbLrlerxDyWDPANMuK25+dFr9SSXmFCGCPcKNJ5N59bJGTPGtJ0YlqC48CgYQXKQZobD1urCMHWWpm0ojd8x30DGY6E70R9xaSKkb32oMGfXPnYNy1qt7FWw2lYF10n5xaszQvsTfzW/fsFRaLkOTKvCHMA0kNgmSo8C0oIZiBkRAQrAcEwoqRhfDWiEAKFEggOIztkSRKDAEOpAYYQBwZNbYHbBLR9Oj/1IASz3wuS9vatZh/mQZejJsMWsCxb02V5OHWmem/72tavy3JhXs0XntRxPwFggcwn3GwwViCXCY5zIoTgQe7tcToSpB0JhjFvw1QNMZzZ04wOGIYyjzupzcAlZUHClm6Z8mT4zFRlqtE8OnrZDd0WW6NBOJ5lkEQDW9OxxRxp7LZlZDju5DgoxzEekTUaWoRjju5jKzaNOeM5vaslEBgmJraWmIVQKFGGwkUs1Hvts2+ZPf8zFy8s/uirvGMbA8uVYRr0vYRheZVR0P2YLXuUZCEQWSAwSIGAKMXQgsbERMTW0iDtC9KyEwBALFJL4fCewfQFllOLpGKB69F2/OTk3NK7Hn/rz37mta//u+li8a4h03TCoBMdtF9NouSdGKriIDRpkNBwHD6Q2iBTS83grGJsNbgFbxGknEamtmbRcQX0dyIpi2WJYBypD4UCCWGn15qdKBXf6oTeQsP2yMKx8DzRC3RoQjoxT446uWIChv2J/Hl0vSc10F3H8W4Y1srLsfbaJGBmBmK3L/VhokTgO8l+u1A6c/aJ2KVcnGgCIHGcYHSyo8YJC71E6Hba2G901ythuG7Z9hzfJZnY7vZrd52F5x//URZSWLADn54Yc/sdB2kfl4NJ9QQijBo5WcEjH2C6AU3TwsctlcMZFPhkgnnqTraDJYRxX3eME5QmHz+z+L6vXLvWmPCKkWXqu0Gw295p3CgtVB9jKYCMPWlAgGNhOV59J+2Pa4k9kZ88Nfv2a19/5Z7ygp6UfNTrCqfTBQOL6wRpT0i4dL6Jt7dAFrvWMcJ1pbXVVr9/cfGxxbdbFyYSPbAd0tAVPhR2CCejTNJdPLhStOoHSWev/flqoXAgAdoWvL5XQnPPBfrk1SWGj8IfnlY5tdRqQC4eWctlEzqOvlOv391vtF4+s+B/CAwTDGN3xqw94w1Vj0tl8QmLy3HF7HSDNjSlkiGLgISIkM8FmM8FhdnqxDPW0jNxoo9a7d72wVH39fpB6+vb+0d3Efgon8u18qHTRMaOCfKxJkyky3HeiCSKylgsztvz51+Cq1fbvLy8TCuwAuuwDgZSFZJggkAgeSAX0nZvY1vYgSUHx8yoDxlj0t67gBaALSJqIdAKIcmSZeTURZJaeQb1TcbOyNCjcbzjF8wngnFh1Cx76M0aikAEASAoDRoBZGsIAdkYgzYyiWJjQbkqt7d7WHzL0tkPTE+VzhlrE4Eojx09YxmMww8UYqCRT5QUBcEsEmMgjrVNtDGW2EoEVEoqz3Ol46i0vAsDoEDkN2XQnRRco9sJURhj9enZqfknF+eee/X+1lerkyVjIle7qE2tWrWwCvStTL/NchnB1egYIRHBEWALrq+mDBJQahtAYgJxYkTim9viDDO1BAsdG21b8et+4PS0oAQEmbLPFr7W4HesrNInR7FajxB9sIpXq8u4CIuirxMv9p18yPbsX3viwx8WwHMxGd9aDgDYIWYxDGZ1EFmiYEzlkkBEEoDMyNYyE6WXTTGwAmLFzIJ4MMsyg2UWlDa+JIkYMWD9C/Xbv9OMOl8Ow5ztFhxd2WvYeNqnuOBSzuQ0+VGbo/a+Xw5v3//qzY+fyi9NybxbsUmURjYNY28QgAZSTwrBSglEKZAIMEkMsmFLTEjM4ApFKAUKJYSQktkykbGDiiUMAgejDI9diEMxRIPaQxYANBAyaY4dVoX50oW9g/37AXrakGCJyEJJImHJ2NTrywQ4qno4iN1J3ZvHok6MyakTGbMj+Y3HYceDxgqjrL1RZQoGEMiolEUhWAgWhkgCciiJTy9dvPD2W26Td+IOGLBATGCJgE5MmmPZtnwcsyYQWTkOoFAiihNIOr2EE90DAkYhXHSl5/meUp6DliyxoYGyHgbwDz2h8rg6HQMgEkhOy2NcM7vJzOL8Y+W7t5+IrG0qJZvKFaZ91NuOmr2dIFcqWyBteVj9Kt1Y4WiKT1UJi9QS2DcxOXlnyi8Gs1En3vZCP0gc6OQPExX3Lemir4yMHceasJckk2dPLZzVCrxYa04njtRiBmPlAsYn2nFT1qCgNVqy7OS8vFv0z/RbnQkv7/sERiV+UcToisJWE2EilBaMkmgdQFEJKoWFLhikMafs8L153EaWzmM8WALUYf2oY9vJa2q20AKkIwCInZzTbWzVjX1NVWaeXnxXQkYDMMqxjLhRnCcDCCkYlQQpJBIAGqK0UjlbxnT3BCglCGZgIraWGQmEGG1ax1p3jc1bwKl1mIAGpgsUu9Sx4Vx53r3lVLU1W4CYKD/odg9a97uNdtObKU5aMGn2/zBmcrCED8+BYEzdw8ggGKGtYwomcxeV601rGx2ygFxY1Krs5OV+3+W9apqYNLtXEWcKUjTBkdiN3D6JMki5MDE9udCDeFiU+KFzPrb5HlrBRVprDaWQ+w/q2zKyt2lS9SmKIq2kvrcV2+U/D/TJ5e8ue/AHFYPFqSsNoNUKaGoKTdf2k2I+17u2uf1b1amJ93quVDwmORkf8l+NLdp8IobnOANmPOcIh2logGkRSGMQEYducQg9pxgG5dLsdPmJ+Ezy4W4vubvXaF/b2mt84d6DxheLlQKUC0HoWY7ipN93Oey15V6sekK3Jydt9bJPV69eBagCVKEKAAA90UMHPAQgRYAKEFmkVos3ueVGfere7KQDCywtE5GlGAQmxhpHGqGM1sM7g9MsP8FCioGF5EQdi1HByxMaAgGI0tQWQxbTNQkBBYIUAhAJwA4CIpkBQUASGRIKDTM7BEJ1ukluolR48sKZ2Q8QkMXjEA0YBc+Pm+fenI7NAlG2Ox3eO2jtbu8fbjXa0Z7W1CamRAC4UoiwmPdm5v7/3P1ZrCVZlp6JrbX23jac6d5zJ/frU3h4DBnpnnNmDWRWMYPNElmtZjXB7vYEQTUEPkgUIEAC9KQnobz6RdCrXgTVk0CAaoj+0gIlNsGpI1GsOedMj8iYPDx89jvfM5nZ3nutpQczO8eORxar1V3FKvYFIjzCI/zec46Z7b32Wv///Tsbr+3vbY0H/QxQKFJ9isPP9Ok6o8PO/YFECG9dv/zNdx8+/6cqLCzBz0OIzyfvxru33uWGrfWnJqYXPifIFk4JB5TZYVDW2iVEoFp3K1b6NV1uuLUeYKk1UyEylQ8TKPUZbTi2pOy9ckGJPLtxSW/CnTWA6M/ThMHbAMdTTxt7vex8+mLw2v5rX/7VV7/4D3NjbAERWtuDLIcjuoyVMQ0tW1fKnFo/vrIeoAUSC6TUOsqarafVICGoHkNp7r8/oe//9L0PXx8NFlWxKJ5vxjAGkHTo+dCfIMwH1TixZ6WTFwLyo6Mf3/+dK7/wxt8B45QlAKPWYmNRVUMgFl0VGYrDs+iPirnMqllV+kMJ/Bi1nsBZNI6s2baDZCfdzDcGF0ajbJTbGEIE7fDVVmFbtR6jeYfcnGAJEUgBS/aQbfe30zwZxxhfqIKEEEtmQUpdRs3YjQBARSCEyNwMAal792kz4FwZSTojEQC1hpSUEAjscoquoEjLLkRUAHEA5+UihciMiJVHxozS9PnRi/wbN7/wpWIDt59Vp0GUKSh3LPq6gp62XYOW+aQKZIyqNXRyeGoOH5+88GfFJ7pYHEElB6hQAlGCBvYwtXvDvdGl0bXtK/3tIWGE0HZfV0+btAXhkkvW8L4wxDljZvIrr1z50nvv/eyjzc3xY0ERcGYxPTz/NL00eot12RdpIXc1pGelWWrJiCgCgtb0863e1bNPD9/HQdZLTEjCiC1Yx5l4y1S6ALaHrFvD7c29mVYUFAShdg/yZ0am+tLnpcuNBZspATu0+fbG/vnJ+SiP6DQ3Bv3CIFQ6yzchiRMDDpNy4bPesLdNo2SrEL80zjVFNqp+dqUSqTtJUaI5enH6JHXuPpNZAJpJwuxDGaW3O3z27N1H70Bi39x48+JW9CFaQiSpD3QCIGTIUGIphgjF6VzKs3m1OF8U1bwqOMSAqpISkXEuc4M8zzf6aX97kGSjnDgyR45qtB2mrw2KO07HVeFFCDDnSrCXjzZ3ti4dHx3fS1wqBrQUkcOzhwfPd/dGOwx103nVtNPlmtgYUxQUgARBAZElRNtPdzf3tl45fvTiaLi3ubnwxYuEk2q4GWU4uqUA9+D5JFJCW6Sz0sTMZGU1Hw0HvT03TEdnXImCNn5Z0cZitDapb7u5qAhEBqbzBcyen94fDJIXEHwVYwxxHvlzX7zEcOcp/A8dDf57L7DaMeG9e/f07bfflu9//1DMVohZ3Ck2+pJ9eP/pH1/b33vnc6/t/y1m9kBgVldneWz6uYIZXVNQ48s9lI5fYzn6aoprBQZgjREQUZ2z2dY4eWtrPHjr+uXtv356Pv+d+48O/vXDx8efjAfZZDDMpgp85sXNF9VxCRb85PdHXBQP8dq1a83LugAAU+DIRKhkAE39g2Vt0X9JRLQU4raorHo8pswCJRmaB9bixx88+v8CwjaiSQFgI8a4McjT4a3Xr/Saro5iCx9VXGmdXvrQms6e/uD9h3FW+MIgThR1BgqhNtdIjWhBZUfESHh6cjb76daoLypRE5dkZ5PZ6Ctfv/5rWZpsxhADYoPFbaeR0Ik3amu95gyDRKos5v6TZyf3nxz9i+Pz8n1DMEkSN80yV1kijQIuxNA7mxcbp+fzqw+enVx//drezWuXdq8aAibC9RXzpYKuA3CkGJk3Br03vvDq5W/8+JNH/2ZrexBLH6q02i3v7QLegzu4etU/B2h3EyB5yGTzCifBWyQ3jAYHqnVPonVliUInCEI67fbVpiu1U4D8wp9p5DMiLAJDJYZ5Ok1kAA/+1Af63Vu3cPbBB3j1l3I0RbQuxHSOYfwv9SOJkUMFQqxCos1CBlLzaaCmZBskMEhACkupREtcbrqeahSBqDnttdCn9s8BQkTgU/TmBS3GTmk0CXzGEOw4GeBpNsaDw3uydwhc3BgEVwxK8kfH/XGvtziYv3P8/pOrW1+4+o2o4kEBkQjJgZvNFnB2/+xZ9fTsp/64/NhEPM1yN0l77tDY9JTIBAY2BJIp68gfz3aq5+cXZ58cXxld3/7S+PrOq0IoQXSl813qpqB5tmTZMRJoCcaimCWDfNS7OD+d/SRJk4WvwqMn7338z8ia/bKs+oqaWkBCh/nmG5cuoyWrIqtmqtYxM43xewWXbbvExsDznz44q06LaTLIKmOoBKRQl2zAICxAJoioR0tSnc5LQ/j9qDqlCLCg0uX9/uXNSzu/fCwLqJRRlIGVQQVXa1u7QeqKG6agCsaC52Cf/PDj2fzByU8Sa9/p97NHZjgsYWw9AQRlRBE/8Au/e/7hiyuTh8df2nxz79bem1c368bxMtVqTbCtHWFLHXpDdM6FDC9tXc0f9V+d++q9FE3lUldNj88+GpYXfhVSzJW5eYLaMArVlQi605NXUIPoks3B6/7+8x/0WObCcCKaWZgGrPpsrDo7L8vRcDC6bobZ7nksVJSxDeFZKrzWnUDN5tJ9H9LwAAEZRXrbg+34CV7wEFK/wNRpGtglkBcTrEbkoLT5ZD4dXH99/wZkdhhCydq6Q6Ejy9B1kIeCoiGjxaKE6mR+b5jnzyqOk4xwWqgGtNYDsBlsDd87uPfpH6Rbg1/r7W3a6CsBVLDOITpy09Oz8uz++cPZ8/ODeFYsVHRiiI5UdYIAnpBICBOFKvGHk+ycdNNl6YXR/taV7RsXrtheYjRGYeg0VJfdVFmbDkjbChYAk6pNRv3X/dPn33eSvSiRQzrMnp0fnN4bTos3bN8lMQbFzqlVXxb8trDtek6iASQZ7+988ejhiwOJMnfJ8PFcq1Ikajp5BL3quvL4iOTx1FQUnMvMcFL4S9vXrt/wCSXBB0CEZexTLXfTtqBCVFwmLagKgLX4/JOnzGflD9z2xnkoqgINlNCH+P70fzj76i90RHjnzh399t27evsq8CTGON7yVTV304sXN/S7P/ngH20Msi9d3N24yKoBAMxS3ddsFd2xGr500XDZY+/yoXTZDV6NFbEr+qDlmE5VRWIkQs0yO9zPxv/J1njwt145mX708aODf/vkcPKvd0e9x87oGScwgdIuRObV5uZmTJJb8uTJAxyPAQBmAJADiRAaawCARFZ96SakTruUavz5BhBBVUmcnTjEsxfH03crAWcANhXgalmFr13eG3+OiK7rGnepme69XFythoRKRHoyLRZHZ/MPe9b+DiM8AtUZB/WlNAZ3FrYGK2PpaLvvCoVokiSxRVm5C9ubn7+4vfF1EeEmWmvtWvzch6oue9VXnt776OGjex89+2+SLP297Y3+JEvsHAkqiBgQiTFFlAgpofSF7LvzxWLz++8/vuKD/M/fvH7xF1WpCW5eeRC7wdbacZ8qqFhj3PWL27/xo/uffkhAT1PXX4y1mvN0Gk7uANyBO7IyGqyP5i4+fIYnW1s1W38i1hgcB6M9AJEav9T2vxWXOwRSQ3Zq6PMrUENNiYlyAqrHanSBQUt0JubFRIezW/pbcFv/FJAn3PjmGHX2zIRkWx3kUIQye6TnxmtEETUtUbYusTqRGY0DlZBWjKiajbKUPeJqfquqNYVatenikAFEhKAK0RpTsfQAqIeJOlgYszjOcbx9qnB4Sw92dyU/+DGHPIQ0G08Dnz/NLvbo6YdP/pGOe0nyyvjLyhik8u7o44OPJx8d/7dwvPj9fr//6cb+ziIdmCiMwhLEMQo5qxIQGQ0aI5RwPzU2Dn3Q8cl7T6+z9393+/NXvxmMKLE055qu26rhanVGyVzntSk4MEk/e+Xk+CyxNju1ZI6KF+cPy6IyjGoJTIIiQxqmb258zvw9QHVdaQkiqXa2bGlBltQ8k2hMde5/NL9/8vtwYfM4cjwF1kJIAxmscdMKESN7ZAq9rX5wOxsVhhjRpMnR6Qvz9bdu/aL2k/2ZnwUGQV6mMuqaI3l5jGs1N8Zg6av44I8++BE/L35v59L4j7KeeRQVFuI1JAkzqlEmIadJlvbyQW+7/xF7/97Z+y9eNx5+ee9L178cJDptWUmq3Ypl+Rk3fTksY8S873o7F3e+8ODjh7+bpYNTm6Y6PTu/X82rZ1mv/4aweCVFVcGVLWCl1ekek1lY+sP+W5iaz6nwNMT0wAAam4gKkzEEzheL7b1Xrn/ZJzD0IYioki6VVbhSSXY3kO5KhZ3PDBGCRHV910vS5JV5UQz7g/7UWIoJGmBOkCqfScJDlnCxPx5+JRg10Qu3IBHRjnOxG+zeaEiRyC5OZxOaxz+mnXTGJBNPdp6IBvRUevJiUmtcTP/Vix9/ile+2f/rmjtnCU15Ntfjn7344/PHp/9vni4+clleDUd9cZkVUPECplRhL4RikLDOalNC1txXYfvk4ydXzo9Ofv3Vr7/115JhZkKomvMwLTV2ra4XAYBbr+FSo8ZihulVJHyNOX6gRktw9tz76XdnL86+MH794hcFoydoxuu6YnKteZOwPnCjElYSOd8avJ4N80m5qIj6cl+tLszZUM7cXIptrzQbU7/PRiWkBfM4TfO3+he2XptrteKpNS97qVXryGDqHUjAoJHSV3b66OSeRfeDKLFMU1pURVLBvIh78K7+WdQ7/14LrHbj+s3f/E299/bbcuswjS+Oq9B3XA6SofEuPvjuT+//17/4pdf+V9vjQR8QuS2yuk7CNcAfduyga7P1nx8RIx3f8Qq0jB3zP9bsF1ZRjWoN0ZWLW29tbwze/PT58cUfvffp/2N3c0QM6LwpDWQRXDmGyeR7nKZ7ai0h+AFAysAMaEztECVCjVGaEq9eQZZidliJ97XzOcVGKIiKC0U6Gw0GJSKjCsxVRWbOXsuS5BoirYo17Tr2OoXNEm4HSg1dzRoT0sQdDvrph6rywmBSEkUvaiTGCEQiZF1AhsrHGB2KgRiz07Mq/dyXr3w9S5M01MJ2WjF/sI09BNR1mrkCqqrQh588e/reJ8//1dZ4+GHi3AQRDxmg6NnUe4nROBHQDLIs2ohmWpR+0c+T0hHBu/ef/rNe6oavXr1wSxTCMnpoLVS0e9URQJVYRIa9/M2djdGb86KaJT08YyWXxWgu/cYlefr1p3/q/RtLj6Bo1GAuBLbV1bWLh8Jq4a7dkHUfcYkcWN56At77yiiWwIEBNMYF8dbsTG/ePvksmuHnfE0fPsPtLQCAEhQFPaBZNOBOlXpyvHRHvaQBwSaKsaWd1Rr8Wi9BL42UW55OexpX5eY+RURLyCKJJetA2JATgp1GhAgAe4eHOr6RSbWdxMVHRQUWJyTuWTrM3ePv3v/HF/M3x1H96PBnT/+5Hlf/Ymcw+Fg+tzXnUJWkHMJ5YKWEoQ8c0apNEiUMTQvNGKUQQtRgUOP21W06/PTFv0y2hhd7lzY/xzEGWm5rsvZcqa7L2xQUKmCl3A05ci+gzMlQkQx7QsNcAwgaUBuCDE1uLiCSYD2QV9RVJEi7wK8KhWZs2KDt7LA3yTbTR73t3okSHanAApGjYZBoRY2giEGxjBLUaIysxgIpi+u5fHt7f+8XJlpoaKzzzf32WYitdhIDCDVwsJ/84MN/S0f+v7lwfeeIGJ+LhwkCzrIMgsT68bVOCJjmUWCOaubGmNnowub89OOnVTLo9QavX/hi9J6XyaYvx27V8NBmzixYKMtgZ2OfHpgdUT2XWlw9WRxOn6W7wzekcSTBMo2ia8JpeJXNDRpFOO25jWyQXysL/3FuaRBJXSEKDtB61gRVt/Lt0cWCYkNj6jpVu6e/l6QLqGu5S+3bkchIqYXeuH/p8P75MBv28lBoBf2SgxNSQSeee8Ne/1IyzK8WoVRuRqny8o9pXJwrryQKo9rpi9OPDbn7YjQ46RU+zHw/Tf1pNMamRjkoJqP03emLCb/4yUOz9eVX/ubBw2ePz3729F/gIv7OYGv4NLmYB3DKgQ0zo1gCj6rBG2DwFZgkAY+MFjMUkcImLgz2d2RycvqDj3/04aXXf/nWGxGBavxXEw4pnYFh96jWfE6lMGhuEjU0DgpEiF5Z0OTJ0fT52c8GV3ZvqSVgkVYT3EFZrx7CuuBpTBTC4LI0G17YuvH8kycv+sNkOzIf0bYPBhOm0iirtyWVTtH1itl0vLO1ew0Gaa+MU4bmucBV8iDIMk5udfwQRUVn4MXjp9Xi4Oz3N/rDAy9V6WdQJZzG2Ref8Xf+6U2Fb/8HVmB1u1gAIHfv3mUz3Qhhq4eFn+Nm5srjRfG9P/rJx/1v3Hzl21vj0SYZExW649xVE0q7tkvVtRHh6vnHTrNKu/+pdSp11oe2ndhUwFhzWiRySFOLb726/7e3R72t79779LdJ5aHFFEn6MQQGa0d+NJrILEkIPEACAF5MnfIJtYBX4SU5VGfj7VB7lusLKpIooqgwklRCOhNVNS4hjIyI4cCQFK1JqMZPIbSO7LXWdxeEvsK2MyBMAORAhI4IfWkSVwGLOEU1gCKIKkYVRTEQpRxZkxQ39rYGbzXiaVRZ61AtT2xt6DGuItTx6HQ2/ejx4R9ubW48c5bmzpgpWCqBpBL1QSByklmuSo8zJe6raobGKqmxucv3zMbG/ceH93bGoxuDQZ6IdM7s2LVc40qMVWvOOMuS/Ore1vXvffTow3E27LObUn7tGma2xPHdMcJt0JXbc3U5nl/b1+ThM+BRHyME29gepT3NS+eyytKVs8o/aUANjYOgZnhzqKuQABYYArhhpQcAcnMpv8I/sXvVfvlphtwDCFDHyXhlicLUAhu7Dnx9CcXWmhmaxhrWrolOTFHj4Kot2o1ovKHV1l1kA6pBy+CRY7QxoAFrsTqdYG/oAOAu3Lx9U9+9e0sgAywKH8ywj0lSTnpmcEQyf+/o9z76v3jv0yxNPuhdHBUcQKkqFYPVIJXSqCeOvXKBAGkBUDD4yqlQAEhIIwAkDgDYMrPE3nA4OX948H5ve/gqETUwfV31spcB3Ou1gYpiJFZv1CGiQ9CgZBcBpBRBUbQoKLkiGwQsDODSobUEcaq8JKrtEMOx9q2LAUBAz0Hm1tG5KE+RsIrCnGi/2dkCigFIQ0rRlA4dZacnR8m1K698XnN7Zc7zEJVrQji0rn9skQC1/RKo1dcpOUfPfvzp1D+efH/v6s6EQ/TgbHAiMdhESh85cfV26tGIzVQYFLBijN6gYiQaDEaH7z9+N7u89ZomJlcOa+r9dgrGiPWGBgAWBCrxQhtp7kb5RuUjmVTV9JI4Ozw/HCz2BBIE5Vo1181V7TLBWochA0uw5NJhvnN+tthOenaTvX2aWlaNaVJKlWcbo207zgZl9Cq4EoLj2gFdVzibpZNzXX6JS92kQNAI+YXRSD5+1gehBA0n1VwiOasG1c2KRbK9Nb7GCW1G9rwEbLYdm+7BauViAGMtTM9nMH1y8v72YDhn8SH2TezBRjgIJg7HXsrTAnLMZhjw8WBngP58os9+996j4nDyYX80+oj2QCREiizsoovWeAG0XAmKsc2ylCTL91UuCnTOIgICh4rzYf/s9PTs/dMXR7ubl7e3g/f1QeszatkmtLmjdVaJykaMWEyBI2tiKxIW08+S2enZ08XZbJpfGPaFWaClIXYmNs2JBAEFREmJ6vu20mjc3mgcP3m0FVn7QpDzIpSWRpym51JUAwq274D9IIawPdzf2ilNJA7Cre6tY6PtrH8r+YqS0UqCOXjw9NBE+iRYrZBt1R+PfJGf8GC6r3DnjsJv/db/6C4WwV/Q1507d2D33j3Z2eHo+jH00VWew+lGnj2dzcv/7vd++OH//cmzw0eg6mBNKFf/uoKtQbPI4ZpMb03oDZ1OVUODXp3ul/f88hrImja8HiPGKBhCDLvbG3/1V772+v8pzdKv+BiHKNUQ8iorrCSnp+LkyFtflbSIkVgYwRCstUi1eb1aOwtXb0VXmokmr5MMACESIklkjBClTAXnErQwqFMVPCeiijrC7mUElXYiExBXCQ2d12EMRGSdOzWTRGUiRGco9iwNdGq24AQsnbFfTBPkIskopogYywhX97b3+71sL7Losg2v2tWYaUtWqOfs9cvxPuKnz47fR2Meu8QEWz9V0ABM0SNinmdQlYQVIVJV1Yu1sWJMJozEibUKhPL8eHJORLCiJepnQK2rzRRBRcEQ0e5w9DoibinHkV3k1s9m9Ozh4E/kNNy8fU/hXQDY2mps1aQiGgFBlFoKN8KKSt2N/wCICsgKwFLb6NuOEJJhYRZlrt9jzBWgDpHtHCP+3V2sYak+ZVVkZFCqgLXQCF7rqIugDEGkjl0BhaiAEeqYiqACXrn5VepfQcGDdP7SOouvEYZLM/YQVYhQg9mYVBVFBBmZAgIAnN5/it3P7+DwnkRbcgLzaBOqyMuZGaQHvc3Bu9t7458kmwMfZxEwiIKS0UTsYNi3WaQkpV4ydMO0L70MKpsnTtPEJWnCmlLkJBSceBXLwADOQjUrT/2imiERYWf3X1Kqm7Eg1z0FjaoQQaFSheiQFMj4iJHBlwI8BRtPDMYztTIlIi8hzhtVGtVxNG3MT3vNcc2A0x7kVoE9UCrpIlCcI+PE+f75EMzZaJCcp6GcGHGTak4zllCwsx6jSYrCb2zujr9Y2pgGEW1/rrQuaaw9cktKN6oqomJidHI+wemDo9PNrY1pYFWobwGIxhI4NAJsCyZTMBkp2JYRDc2E2KAoKoOIN7mdlWX1fH44OUJLhhVUVrT3FWG/lr43rDHFShiCgzTf6G+XZWGEVcElsZrMXizOZgUYW4e2d3i8jXapSU4EYCTlJgA8gGCy0dtWkd3IuK3IWUR0Yk2/WCwGw/3xJUgxCxI0qmJseEhNnmSNsugQxeuMxdU1ZFWIohA7n28IQc0oH2WbvY1Q+QwUUs7QMaLzFtKqXIz6Oxuvi4GERaVNfGwDy9t1ISrU31sAoqIGA3T+5DjKNHysGS0UXSUlczFPZO/wgcSDklFMlDTxGO0U1DxPU/tRyvjPx5d2PqK+EWVlJFIUpdKyUTAWhRKKIVfGnkGXG3Q5I+QCJlNnsgokq0SSIECqEJwxp/PjyZnoCv1QX09cXtf2V+68HxZRJqCImnlm4KCBgUpAnIOB5+fPjp6KohHA5fqx9lf7/RrERxRBBcUqBqSNvJ+OBzuLWdUzNs3QuMQRuKi5dQQOTUgWZTVI0uxitjXcK9ULK2BUrdfa5npz665tr3FrdrBGT8/OzOLZ6YdZ3z2iEBdxGis+esiz4SX+zuG7+u+OGflL3sFqBO8AAPLOO+/Ae4eHtDcYxO3BYCaVPtwY9eaTWXn8+z958PG08P/g6v7WL/Xy3JCxsaNzWc6Ju2Ss1nvYjgtfCgeGjg6qg59pssywIdDpirqNS30DgoJSiOyHg95rX715/f/4Rz/6+P98XoXfHyY9DRCIchsSztVhdKrRgqks1s0sWiMkr8ESWkBoqx9sqzAAAZKmiABEYQEM1qXBhwoATQQkT0QCBNBmDK83yLAbTLUU3rYkC44SI0jBpBWwlIPULWZVVQ7ABDgEdRcNzo+cdYqpiHPWiVuUPr28t/mmtcaGGCMiUBvOul7gLHXj7Ysyk1k5PZksHo76KVpDBEAb1uClyDCgCCUZ6733klLKiUUUQqMACRKmADxwRGMR2cqzdHO2KDVGkfbnd6242CHR6qr4RBbgXi99JXfZm1Fo4sX3dTZbZDHyvd1dvr1+k64FKh+fnMB2P1MEihxjiKKqQKANe0iWV69mMq1xwFb+c4jNvWpTx2XUKlqO1ojGaUCwNYLht+DOkgbwJ70eAIAk9hWJFMCBAJrYfH9VfRn/BKiN7bQRu7cGCEFp8yPacK5lrk+DAFm7jHUvrm2/IwbQgEwcUDCWADadY3oTAN69DQD36kzE3XswvLavRXkqxiRRUy4oxilZRzHGATE7Y5JMUQZVUWIB3hx6tgFUWYOCECoIgjFgDak1yJacJugwS8FRgCwSDJBoV1Xz6KN3NdC/g0EhleUmXn8+gojSODyp3owxAseExANDxYRFUgbPA4vKSJGVLYqwgjKAsi5l9B01UnvvvZRfoAJqaBGZSyX0EKHyQ1fMp1DNho4hFgBDBwAMg/DUBNjELehD5X066OeX3UbvCzOuJMgyVXBNgIydsJ7lYYMMnB+fcWbtNO+lVlRza8ijwCgCEZahym0aIAIHAHBEAFUFrECmECsKqVFIDRkT+mmszqdnG7ytvBKtLmHGL/OdBBBYGCBNrNvsXQkfsc1GtFARjqKP58eTR+nFjdcZlLvziKVXZXlorrvEqIDKAslmfw+duxGYjyChjwPg3Jk4VoCrvc3RFz0yMteTR2kREi9tAAjLMdIK6axLYHVz4hVAUGTFaG0ySsajz00fvHjUH48qUIwSgokedo1LP+82h7c8irJqE/jeBVyupAvSkPEERDliMj+efuIy9yHnOgPvFi5U4dgXutm8znR7pNPpQkcWPQPOgPEEElIRzQygRUySiKFXsFhflFkVg2VlilHIADpAdapAlihaZ6cJmUWSJGDQJkZ0IEY3yJCJCx+WXDJppzjrcUrLtIXmfTHUBxNBTVGB0HKMXlTRoM16x7MXJz/ZWFy5RhmlyvGl4M6u62C5WdTLmwCYJDMb+zuXnx1+PEbq93yEmXLwkjKrR2sqGBTlYvvS5Ss3NbdjHwpRRRRdlzY05tRuJ7m+tgR08viohEn4A7iaPqrKcDbM8soMtuKN+2P53r3/ceyrv/ACq9vFunXrln7rxo14/76HYyDYyOcqZhiHg6QUcNUP3n/yjx4fTR7cuLT1y7tbw2u9LDPGGkakhpFWb2bSKaa6bcgVEbDbLF6FYrZaFO1EtLTW2lY4XdOZlx1eU4XoN4b5ha/cuv6/+c5333uUmepZmmaoJEVEFQFwCpCAGmsIk7ZTqA1d+uW45zYioitBkLbvokCqaiACmNQKcyEZOW4s0WrIQGOMqo+ta8sGtouGdgstQAQRAWZWYVFgYkCKaFwsBcKz8Dz8wu6uHD5/TpvuIsxj5RSBnEldnrr+xrB/fR3x2UUVd4Giq4+UkPBstjiLoN4Zm/kY+4hoQpAxGvIM4jX6aBCZKSpGQSACAHVEkCNizxgcmsTtWNDdPMtcZFbnDOhL+TnSJa7oCtjBAppaN+wl9o1ZrJ7lxg4WGxtnoRz6S8PfxTt33vm5VPfxjVOtyr6y6UmMp1Gjeo7AdbEByrLkRH8m4HSJAHsJAEnOJAAUSJxIMUdJczw8LGhpN/13fN28fU/fvXtLD/IDTZMeeK7Qi6JTXHZT6pGzrBQfql3Q0NIfBKody4euBWyjdoKIl8/MKjQlgkBkRUOiABYgiwC6DQCvwsHuPYS7t+Bg9x7uHd6iHszpAAJd3h5gEeZEomTR0pyjOykm40VRbDgw4y1Md7by4cXBcLC92R8mfZeaxBgiQiyBoQglz0O5mAU/PwuLYlFV4rVQMASZs5nLk52EiEhRuZnPtl2RrhatFTxLA4GVesSFAiARMCpATHMTNSOOQYkIo4gIo6KAIossQ3ZXlHTofE4tJ7heS0QElCWgUBBBQcMcpoEBnsa//fXVNX/37rsIN2/i6bOeVuzNdDFNh6ON14PTC0WsmBsW2BIyiS9HbDWHQyRFUS0Wi4C50dO42GUVVq89Edhg5UKEfVSIJMpqiEWiIIKAIVVhEhWHiFlq3JCMSTCWfl9ADFKtAeuM5KWLbOic/oMIYJ5eUUM9FT2NIt720uP50fkHo3D5FSE0yrqe670SLgE1B2pRBGRWk7mB7SXXQhGeJYndUaQ0+LiTD/Kbppe8WnkvNZqkLhYaILNii7BBXY1uloHP7WbQgZGuQulVLRi72fuaL8ufZpAX4CkqSBaDv5D3+7dML73ouXbicZPxp+20tgF8KTT6XxUAS7qYzaU6m9/rD3oHXMQFOqhIbHQ7M53u/BIOj96jJ6Uxg3RAPgj5WEIfMBpwsSrndDKfDnyUTWft1kbev7iTDC8Mh4PBIMkyZ4yLyq7y3k6LAudVEYtQHgSVJ9NiVioCqSOTJC4xBof140VNzDMsD2jajfmiVki1QqCwAohAAsJWY8JCEJVEKDWn1Xn5weTg7Ksb1/dej5EDqC5Hj2tOTm3GtrjE8mLgSns7ox07yC9Vs/A+JSaLOZQuOFOhpGB0EwWu5nubnyswGhaV9qiogMtjKCljbGAqS/8TklbB2/nR+Yf9LPtRLMI0IZpheRgPT6fyz5+eKPwZuAf/shRY2owKFW7d4iv5rp6fO/VbhE4rSg3i7nj06Hy6+M4f3/v0471x/wv7u5tf3hoNrgwHvTRNLBhrI5JRVW44gw0OTVu8wzIYuF6WmhP6GsZ/WVZ1Elo7AsnlprJkbYGpAvvd8eDzX33r+t/545/e/8eXeg7LuVICPmKGaFSsICBZ2yS3rE4CL4H7V/qljt1EhIGjxDqJQlKiFlWSQgUCtoalRcR61CGinQJxBcFCpPokr6vFpV3whcUiomVkNEkKs/kMNjatJg8LLctSL1++rE+819z3FEIkVDaDYW8jzZL9FT6j0+FonSf4EhlCEYKw5r2098aV/c9ZQxWLeERQqoFeRK27EbF71aRGLoIDpBQJUmcpJaQNZyk3hlpQJRLWi/CK/YVd42T7z2oMWWdpJ1a4aRwmbi52Zg4tH1wNcOcprz1YiAr6m/U3OjoGGCgYcSLMc2EuQYlYVQRqxtK66BfXVJVMzZtSQWQBdXYnkvas56lJE2tjYa4NtvDdu/sIt0EB6U89QSXjkUosUEQpaISootzxpFO7OIrU/9bQLdueLmpHh6JdvUDHuo6oKo2Tt3VDNpuIKIMioyJhbHVhkzn6yRxne5dwNjzF3dEvUTF8ZKYHpR07csVinjvL25Oi2jw4eT4c5Bs3vrz76tfe3LpwY6s/3s976UZMTF5YtkhAFgwaVDBYU1tYBYKwlCKxUK64Chy9rwpfFefl3J8szpIszZIAAHFZaNf3iCyDMbEx5Ot61BQRIiJGAHDWKgDAHGqNm6sqYICIqhpVMNZ9CFwWoF1OVNsdUa11SbWpBFgjMokyoahDSRaF+gTgt6AeC/8m3EGA2zB9+AxhD4CDwaoos50r+1c8igsco6iSSosNXWY0dluV9f1PCioCl8a7sNlPdnuU/nIKdmqFvEMKhOhVNQqoRhEFoib+UCVBYgJSJEQBMVG571E35wnvK5IEEdAl2LSDpdRVQmSLUlVhYQtbSrrpQ3wKKgrWlovp7GBxvpi5rXQsoEJNvY/rB7SV9KHRgkiqRBvZRpwWW0azDQQ1viqHWxd2rrPDPguzNqMiBQEVRam5sA1/gGBFYSYE5WXnXVs9ZWcqoqoI7KMdJTdwkLzmfTyhxAKhyauF39y8ePGCJORqNlpTDK4xyNp7gpqtRJTI4PzFeaEFP6JNMxcxVe4LiY5xODVUVPdNjAPT7/lU5j4D1X4vzYZH06PR6fl8fKU//txXL73x+ctbe6+MNzb2+oP+iJPaeAMIpCiozZ7DqsosKsw3IvOsLCsfSs9nxbQ6m0/92eJMXD/pO7LgMTaFk6xizrRRXwkudczLcW69tFsCNGxEEWxAgRA4nFA/fXz2+Pmn+aXxa4wARgS7wAZRXAMkEoICMRAQcIiaDbI0v7D5yuTB4TDbGkysp2rBIi7hrKjiOO/3L5vN7KKPnkUVOzYeVEEFFZQuvUlr0wSlBk4OjqA4OP+DUX/4rAreG8vRXx3w9w5/SeAfflv/+8gz/lIXWF0Sd1NkyS0A/WA41KvFSIEYuChQLChrhM1hzyvCfDKrTo7On308yNMb25v9a+NR/8qon+0OBz3nnK3t5kTcJZPXeISO0FCWJ3ntULTq8kY6Ft5OKuSqM9ER6KmSDyyXdjf+9t7W8KeTSbjXcw4kx7lTVRUyQVr8X33oVFkVId2JZYfL0/TYak+XqLKIGkVKEMQyR4qKRIlFrjtGEQjjSojc1n/LlFZVaU9Q6zDT5mE3CpgGFQPzQBEs9c8M7l67pmVZ1i/vEKBICEeZMTMRO8rTbUs4ZpYGYdNmh62ph1e+A20rHpXLO1ubxsAGvMSJlZoHuDpjENVUb5LOzJS009xBAEDmGlK5dON3ydWd8fFqbCFgDFFmTT+KH9iY2aBsbC8an27SHbgDd/RObbFr79E7dwB+AyDd6GuVpCrFXFR04aMvjFpkWemuUHTFQ9b1MYTKatQMgVUTsyep2efIZ9qjtMx6NitPcDjbB4A7f0qw6B24f+O3dT+ClkXdS4oxaqzHlcsUZAFUEFk66aB5HpAa84G2wcP1JyjLnLRVB0HaPD9dlavYjBpVAFiYEBigjkiHZNTX82Bw92BCRT5CPj4yaZI4t8GJeOyFebnx4eHTy5cGu1/8m7d+5ZtvXLrypVEvv+Cd2EOZ4gd8rHMppWQP3vP6cKeB4SIgAZAjY23es9AfOMgh113t8SW5oEcyM/fDBEAEDXY3hM6JClcOSVAA0wgBRYlQgCRGnJeAdlQvkSRWYwyMhOqViUGaRVuwC+RbYi50FdPBUDt4Y2SnCkQqygWAT3KcffAhwjt3EBrt3cHuvfpbTDcRc4MAlJs82fHAGDmCINVDs3aHWnp6OrFhqFDDUwxc3r9iL5jB5iYmmxk4RFQgICCpXflEtBwDERhQUhWOTW/BADWeGVbGc/D4U/8cPccGS/uy4KFlvSkS1pZCUZHgIFNjRtELYoqqhCWLHJ4fnZ5s7VzeYo1NF3A1VkVZjaRXNC9FVVEz7vf18cmWSNwmY61G2UrHG1cCKXEUbvMr264lNFDT9vWBQSheTCDbGoE12NC7OvTvJcqh6Uwys+nZvhsP3giHi8cWTQ4mpsxxs7e1seOVMQg3XTxZHaIRlpiAZfghkRaezeST46PMJE9RXCEAcQYMW9mIzs8hSYnJxHniS9NLErMxO5tsPzp+fvlrl177/N+7detrO9tbX4QMx1MTzCkU+oCPdC5ePbNGjcAiNf0AUJv4KzBIhhxmWeqgP05gD/f1klxijswVB3whBXjhGohU095rM8vLDsxaY6fSBk/X60yTvBJiicLGwJlz+fPZwemn5wcnVf/ytuUY1nIuZNk5bzAcAEgKKg3AtoIIvYvjN44ePN/NmM9ZISga5ojDsKg2t67sX9PEDIL3AqDI7Si5nUB1+RiNBkABlBHM2YMXL1wpP9S+qYR9CWEU8x8/Vfjf39M/61rnL1KDhS+5CvXu3buQ7+7qiwBh3Hfgi8ikNgDRnFlOewN7kMf0SeHLdx8enCefPjvdHA3z64M8+fxGP7s87GW7eeZG/V5unHNgDAkRySqKptFedblQTbBwx0m47L+orAKYV3EKy6IIRYHTJBm/fnXvb/7+jx9M8p4zsfBgnAusZAF8bZVYbXHQMa+sfl90lW7RgSNGFgACEBE0AOCcUwFSAgGMgIDIVI/911T6n23ddI1htdZMAJSVREXRCBKbSEYTfFrMabI3wmsAcHSUUZED5ViSmMyEmU8GlzauEFGvpn/jcga7ng2I+nLgogKgZ9bmWIGIqzT3LqekrtYYmvlLZ3flbkY2YtOBaOcUnSuzFsqLjSaq1TobQ2CMscJgUxISJexVKZXFlO7evYvfvn37M1qn96dv6tXxI40vADCjiGgWsQozUGkebARoeVHY4cV0dWBLV5migjI6O8BN96pM5BFkWCbiktnwEgHstjcf/klRObXN8NIKHxEjkNSC9TofWBGaYk/WwqWWlIhO/lgN0V6iWbVjz9bVKRy1Q7omWObTK6KKsARn2BkGb+Y4zDKCYQYbJRtvJlnJSb8HyeDJyZPcGPrCf/q1b/0X1y5d/uUyxfEDmfEJv+BFKJmV601UOs9gN6+wXiwb6r4B5QjT1fAPFdRSPc/ARkNTd7F0CZWDz5ghdKW1ZK2tuwYZvSdIEhQ4BDDDoBWYmlROwKyqrIovM/ZeQrF9Bt/AyqgiGEIA8gESY3Tw5r7CP4S2qG6+rkNVnVAKzqDoniR23wMLtyJ2rYOasYl/0VWccNOKbDtoAo/CCT4Pp0AIarEOuGlj6JrhCRCSUl1ugUCTaagC3DqpsRmFijQZGNoJ0oU1O7d2R+EIoMIq1qSSUr+aRbZ9V4lIMKn52eLw9KfDGxdeVcQ19o528VqN+KNpnwKzYrLZSyUxW9HLVZIwsNZdcRu9y0FZROtD18tGgyVyBOsz5+RnzyX5xT5FZxFYP+sshTVfP6Ahk20PL0+eT6850E0fOTO99LLdzHZLqSQseXGyJsFbHvjq0gHIWZwdn7E/mb073Bo/YS48gdPUOvSLuUNjTAnkEvBDG9PtRy8OLl/aGH/r7//S3/wr1y5dem1uQ+9nchKP40zLECKLtuBXbMOh2gKmDe7G7vbWCimoJodYMjYxFovg68NqK2x/aSvB2v+srZFBGqG6iAIpUIgBCJx3qY1BPIDAiTH2Z9NHR59k+1tvISBTE0Hd7huyBlNajSYREGKIkYbplWSU3aiKcJIkgkoqaHRDI1/NdzZvemDihkTYxGytB4cs99Rmf3JWi8mcik9PftwfDD+ch7LIjJY+lnyYVfKnBWj8BzUi7MIc2wLn9u3bcvfuXbq+uxuLvT2NkwlL2fNydlSk4+FUozskpMzkeS4gOTG4ua++dz71o+dmOs5cstfvJa/0c/fWsJ+/kmduq5clgyxNTGKtGmvFUM18EWbSVeR3PYF/2YWmS+cdajdYuX3RhMgqPN4Y/uLGoP8iRv2DxFovCDNLaiM7IrKodUrJUqGhXeEdrDM6dDXcW3ZnEUm4Zu1JlqNGD2AsKHlSakKEmwxaWLWxsAP6WgZB1RlYAMBcx8sRojjnMLJHA4jj8RgePyGEAmBvz2A+nZMbbZFqaQUkTxN7GYCsiMTGWr0St3cJci/hi2t9nJKsBXKtKOfY+R66yj5am6RiR0ukbfu+I8JcFntLfzc2+w3qiuBhyFiDLMpBiIIpUE2CeZL8ibaRt+Ed+L0XAxhAHzSm7IydhSKcLKNCGmsEoGCX2Lz+ESyTw0AEBDObmJ3Bl6rnBz/pbY5LryEDsG5v9zDevntL796Gzwrdm6/fAoBvTZ8qDC9BAQWINRUSISSmz6JVxcsT7JIqToBISN1c8VVnj1WFY73vrj7SmlPTQT4s/6xIs1pbVSWNTByDV+IEtaps3CyhKIT64LJKzHbq8OL7Tx6Nb+5e+cY3v/y1vw/j9OqH/phPy4WPGiHWGYewFG/XMd+4OjisTqOKWNcPIIir+ANq78GG17k80CisGEuqjLJKjG8b2fUkSxWCsIhGipyjMUGCy0THhfhoDCasWgGQgkSQ1tH8WbPyz4EpqgoqWlBAA0KoRAiQw3QY5erjOgJkeV0BoEpPsB8TFAwOnbsajOygsIoqrtADDb69GxzcIW03z1bNXsaadirLj7HVISEwai1G79yrrRVf6nVl2Q2CxvG15tLtYm+WXX5s1RgggKqEDvJkyJNpMCEtEWKkXlpWh7N3ykX5i66fXq4Naoj6kiC9XSiXZ1wWNGniKE8349y/iqK7w/Homumlm2WYqUJbA8qa7m5ZsJOFxXQW/NOzA5mHHRm4HCvRdT9Q5x2JYl0QCGSb/f2pw6uqMAshDPvj0XXO7cD7Ums8Ra2P+wyrcenABQBUszg6PyWWH2mChxosCyoFy9YpEpqQJZKNIiQXjw8PX/srb3zxN976/FvfqobqvuufhllZBBZBUCXoUssV1kwOsl4lIgABkeqyy1rnRUGUAHPxaJBAdF3CoiqrrjvoMgtNQZGBaicpAjAwAFuhHsWKJTgmLEhm1E8f+OPpOzIPlzA3w8ZGXX9Ga9Y0XXGA6wUGRUBMYge9vc2vnH/w7IntbSQAAKHyO9mo92WzmV+dx6o+WHWbC7pyFrWu1OVvG0NnT49LPi//SC6PTrDgRUnqzQLi4Ol+rb36MxwP/oVrsH7e1717txUA9Dd+43t4cDCC2eaR5m4ofF4ybQ6iaqispnOD4soqpJuDUSpDmaHySfDwaL6IPz2ZzP9Q4XzfEO1nqd3vpe7SRj/b3xzkO4N+r9frJWitFYSG29I6CBWwS7HRTmDqqlbp5DIpIDNIktjexii7eXg6e+YGyQsACIImIVQiIoI23XtN3NdOjBHwpc4WLK2+igDIiMg1y5PBTzyyAQTnkFGIm4mJKuNKWAnLDspqGorLcMd2q1fF5kgOiIhIWKIxGX4uNbhzOegni0PsbW+h1Yq4sg6NzQzhBlJr2VsX2K5R9WFVCK1aaB1alXaLKf05JPuX7vMOy2kVxNMky8s6zrSFfq4wrrgSH9c8clRVjgAQPKFN/t06wbffuQMJAGTbRmLBERzMw6R4ChwZsfHaqWB3NLBqHeFaXEvdgwfkyGq2+2+IxV2JOgHncl6UyX04DXADFOBO/dfP4V+1X0d+pk4wYJ7OwiLce/ajBz+KHNIYxJkoaokQEB07StEQGrIMoAwsRlUcgIoJAv2L4356YSMDH6DuLNY6lRZw9tLnvrwWqgIsCqhBSUSjKNEwt0VhKCNwFfqhMXjxyfNnV3/p1Zu/9o0vfuE/fUin/Uezx5VqzTXlZjOIos1nI9jVhXViVJoFs67VEVsgQs3SaTM9ZYXVB6QVkWwdI1KPb6Vx67atO6n5AyQSiQnALhYAfQKSqEoWMHUColGRtCZ1dB1LKw/wGrobGx0bAAhLrcDybAAqAHCrhuQdALj7Ls5u/BqG4wmlSYUKaAV1xAZS32qFsNbDIDatrA6gZvXcra5T/dYYBToab+2kn64dWDrT4eZzip2kgoaX2hzgZKlpXTscdpwd2Fr+CUAI+gwAaDQwqQ9EEkr/pDiePKLR7hWONdKrO13ApauviUlrFNFIiHbYy8vzcqwoeTYe7QVgG7kuqWt7vuJq3Lc6cwECzg4nkRfhkZ8UibvQ6wtAbD1/XXbciviOEKOo6yd9TJMLMXImUTbc1mC3VE+ReZnP+PJZUzqsNSGrRVma6ePDF/0kfcQ+FuCsIEaEAA4NuRhgI2hxYXE6f+Nv/cK3/sudG/tf+1AO4tl0VqGCqTlbsrxW+tL+pEtNStdwoK0bEzsn3kbSjkCrPPflc4LNEWYZb4bYYUnV/19kgegDGSYQAomxEsUsFosFaZ4XarPTUE7enx2dPRi8uvuVELyshJDtDkvL194GstdvRECYwW0PriPRZa6iJUOuLMvt3f0r14NDFwpW6nbtBBARayl1fZJafv6IJCEEO3189CxJsweqYRFJy515EvLyhP/5u8/1z6Oe+QsvsNZHhfXTeevWXfwe3IDxNa+Dh2eS5Ps6HUYdTkGKvBfzdE7RpyYilUjGQYwLa9IkTdW5XJN+dBNUOCgif+zLMJgXcfDseDrupfbiME9fGw57r2+P+lfHG32bpilTfYJtGxIv2a0R18CBsGqGSDMusYgw6KXjp8fTiwq2bzhOBSGJKnWeiyjKshhpg2DxM9Eya9BTAeDIIAgcVQUlooIhNaoRFSkiigqqALIszfarMZnCZ9vtq4A2YFZgFQMKBhExEKLVHlgzxaPEY/VkGxOwOE+naBgoyxPDUY11xrZh0S3HcTVW0q4BaKWnaCCon/UVrIBjuJZeBCty4tp4E9YymZe+rU5ICeC6fK5pxKxgiKIgLCisqhrUBo8wK9eAfC9bEL4FAGk10ukUgCMEQjetjucv1IfSJCYjZf1MTdhxDK53NZrTFUNww/wC7fRf44KfYoI9W4VMq8IP7AX51jug33n7jkAjgP55X5s7VmYvXKmpn8Vp8R2+P/0IQS9bpLFRdGjQgDHOWsqBKGlihYFjJGRxCJjLwvdktPHFiPgGAkRqkAKwRDO0K7c2Jt3liBlUuB5PAoAQIbM3cW4sAANjyHpZNnz67Fn2xRu3fuXNL3/+P/8RP6WT2SQgGIrYdBga6KPW93IHRAerjaC5kEhNSHnrOFv7fJuZNa6KdpKXOiFtowdbE0wz/FQBVAFhgchqDFJN7un1oL43miG1VGrRsQqCEC6LlbUOLq7bwttutLCAREFFMqxsgCyGo0mr81O4cwfhFkBx/ynajQGcFw7TpEJVsAxgVFnrSBlauT2X4PSOGRQBCKRuWkEdhySrOWinSNZ1RCCsj1iW+rF2rergOxpR/1ocT0dLsbxujdRPGQAkSsbMNlpSX2K0RgVTU5YHZwfJ1R3BJi4HO4mgqtLoiFaxLaARIyPSKEtilL41krjxYFCyh7bAqrlHshyVtUkaQqgc2cwfHZe9xBwujs7HGze296V2yKyKo7VDY+0iQvCgWWJwIxuHZxNCQyO7NexV7Ot7kmDFMHxJFtCu+WIJ5s/PoXox+XBr78KJaIxkMzVRTdBoImNPkUdHB0c7//Gv/M/+i+S1na9/f/6gClERUEkkdjqML8mEmz5BV56wVvAui7/mwLIssevRJXZHtC2CY9mvqrPXCDujVBII3BywUERVNLLTTOYCQ/I6Lyxn2cwkyfHpx48euf3Rl6ld0FWWc0oEAWrtDZ2fX295LLafjM1m70Y4L4myxDHIVro92q1iAG2YV42Wsta9wPIEtjKqgSi6BE4fvtDq05Oj4Xh4HudVaftpFeYLnmVfEPgn/1f5kyUZ/+EWWF0dFt66dQt3d3fxgw9u4Ph+hoNBgvD66/DgyQPo+1R3Us9Tm8iUAcqFUJI535h/LEHlhJ0RYw1glQJQlucuyxPbV9ReZOlx4PuTonrveLq4+uxw8ovjzf7NS7vjrZ3xiJw13OlmNZs6dlwI8BLlpmmzN1C2NEl6CrDFIj0FsgRiDZh6Ka8738sQNMWXHmKE5Qkdl+1NhVivqVEE2UCdR4CEmDZ1gwiiaK2d7zph1kzbnUzG9vYxjbOwASMuHWDt187enlZTXqmf0lQpJdZCApLhFUhTP9NWfymCudN9aoj2a7mIndCNRjoirTHxJWwKrDfmVh25TqNuNcaqN21sokJaUn6rvwlRlRW8RuToRDVn9YVfZ04tPcQAe4e3tLc7l2fViSi6AM4s9EX1Ik78qe71LlM7z+pWVl2icKeTt9RkCCsmJk0uj77pf3z4UZIOjiLZYxGtgjuQg8mW3oZ9vVu3N1568O/od96+I1//XoaQQzUwqlrkceOLvUUM5qnG2GONKYkhUTQA7IygA1ALoFYViKImCriN02Lb9bOrYB1yFaBt3+NqP14TqGhjhF6OjgQEBYVF1BggpJgIGUR1/cPDs+SV/Ve/dOkLr/5nPwmP7bwquLYuRJDlSKPhi0B33LoWdbDiiEkTzNwRk7T/N9UB27rMeRYBJVw7bLQTwVYvt6xUmps4iAKrKKsoBUYQAJMnKl5VidQLi9r6IxJdQYHXVICyKqyXgjVtxi3Moo1LUaNSSAcIJQDcqfVXt+E23IN7EJMcbVIgnAOwiHBzOgeV9kbH9glYbpPa0KcQIAqhQVTEVq/VOB9a6cDP06E1LmpsWd7ta64/cVz9vi5pN63maF3ria1nApRq9pjUIEkyQshlFLASQmSkfl74o+njOC8XmNkeNhh4at9vc68hNUCetrgTVtxIk2hkaNNMYZD2qrhouFcCUQS7I8ylvocMlPMFhOP5AxrnT8vzaT4o/Rtq0Ui3a1rHo0P7ahrhqEYVot2NcXh4jP3R5tAOe2kVi1UsVXtgxFo3uzo4U9s2ovL5ycwGfFcdzZjFS7lQMWjEU8pGBkcHh4NvfOMX/g6+svFXfzp94oWFABhrF6quKTBaB3V7j60eVQFcut4ZCEmbxwdbAxB2mgXdjQM7BZa0ykZtTRCdAlQQojJEUTFIzJRIZguJMWeZVBIpVmpgaoaDI//i+OPyxWmZXhonGEPHdlq/TpWObLdjNBARwMykdnf05vz5VAnBJFl+AUe9zTKGdu6/BMZq51CDjS26HXEKKp59/KxwYj5Ca04M+1LLKsh5jIPJ0z8zsOhfmgLrJZH7srg6PNylN9/s03R6TMZEhE8ALl0YAwDA9MULGL75JhfvBR0AMFyu//zkaBIy90o5LE8pZnPDkBlma5KhIbNgV0RIUwupOO7lkg6qqI8r7x8+P51eW5TxdWPNX9veGI6RGh2AtobClfIcl7jAJcu0rZtAlDRxNkXAUai4ZwmtWrCsYoBoObJSVfjM8EtX6sPW8LA8cyCxqEQEDExGLADEgJglCYoyGEA2RLJmTMQVhmmFUFjumVpPFuqOFwLERrIA9RlENB4P1cway/04aqobwoWXKI6N2IWIVA3/pLm/1whQL3sVV6d57Wg0miKm3SwaesAqrPPl6LBuwbiej9QWpR2jQhNgj6uAsdZSTE3hxCpegWeGjBciXhyRbNuxApz/3Hv17r17evvWLaHBJEbbqxTn3hh8qOfVj2SvfwUQFBthtkht/oZOKkR7P9WFtTQgVCRkjcn+1lv+yfQ/5qk/cuP8IErwoWDuGS/36p/LcPfbePf2P5GXi6zv3f+2fGv3f+ufFD/myznGs0lY2Gl5xpsOU48UDaGQR6kCOpcRgloI3kUxhtBk1tkLAnxFEBZUh/loU78ALbskrR91DXXWLILNLq7gRUBKjCaJWcK2TIzaXp6aazu3rv2vH8LJeF4uPKoYXXKCOrZ+/ZOOXq3WH1TrcV/d/19Kr2qNWMvrwlZ43ow5WdcK/C4lak2H1pYLQSJE1pCqhmhSibAA6m2q9QVUi7mKIUZm5Zq0hOvcs85D+NkkepR2T0JtaPoA8XyGB7u7647RmwD80Rz7FimmRgPrzAurA9ee+V5yx9bPcwvJxKbjy806hg2ZeT1Aud3IPjuGhzVaYPsOlyTAzz6buoxSbnJIGBBMbc5pN2pYYsciuBhhoQFTBEpdWZ3OPvIn84fu2vgWCIZ6EqjrMDhh7No0kEXTfmIoNbtuI4fSauqngWo3GoL8vLfFguSAyqNzT2V8HzfTF366CGFSfpm2e/siGtvj6LIzUl85bF4MagSlrf6QSZ3d7KeckJW5ILaYAGkxQSvbQT3dRCRDEnxw1fPJh/mg/3HJWhCZAlJiXABRAunk5Cy99Mor33Sv7/3tj8qDKJFrXRfUxcbagbwpkaRundVZEo3UZS0fiAiAqC58VVdSVZEmNBa76mBd0vSb/17rsGito9XeR6x10I0IiuEgnCaMAxtlEpCyIQYuCuHsDFTfXzw+euQub7+JRIFUlpFKqlyvMUS69rDWzQ6MgdHujfbZPEWeldB/9ZWtKsEsziMgCjavt3Z6ocBaql4Td6jO6fx0ZvzTkw9Gef4H0YcTn5qqV3ofBvP4B1cf659TffWXAtOwLK5+XBTm1aw0RT6zMaKzKZGrMuHnorEvmtK2+PNzgNd7MX/jCcM79ffIAfiD5BmWuxkOkhFuZhlNT07IkMUcEqo2MwvT4FLC6YLjFBFnvSyd56KTMsZnT56dzPpZ/p9nmUvXuS4vEauwG+DZFC9Sn7DIGgMIPS8xQ2sNqRpVNUj1syC6XqGvVcz60uysUYLXzn5kVRUSEE8IJgEoOFJmCKXOSsTl/Yi6xo6BVQt5Jc1oZi3cmoFAgLlCR4lylitmrIf+GQwu72ic7Oo2lDpPWIiF1WKsfKiY286DrB64JYKn/lxozVlY/0dCo4TUBFOsqsHlY97aW5bIO4CacCcvCRhXnSKktvKV1YiiLo611RNR0+wQEQ0hoCoXrHLGwJVRjfnY8VOYyr8qP+PEWO7EB+/ckeH0UgSYB8mzIvb8mR4X9/AG/o0I5BBk2VlQUaytebimt2uBly0agZgB0wx7N/b+SvG9xz9QlvsKiS8tM9go+X3Qe+/eg93dm3L77l26e/uernWzbv8T+Q7cwdt3b+m92T3d3YVY7YwQkgzjWWs0dEB9gNn9QPm4MBw5QQsOkbxYTBlkQMbpKuKj/vbcPdN1WXFtd4uaeBFWNICMxiipmNIyOXT5+fFp+tpXv/Drs6G8Op3MPAFQ7U6DTgYnrk2rpOlitjRtck6NI+QgFCov7H3kykcMquIQKUmMTQzaxJFxjtCYWn7uGYAbDk47S1xpA3FpSFwSRJooliqCqIYoGiTxklinMJ1CwarO5mIkKiNAbN6Htl0KWWnU1v0ZLaqj/v4sTV4KAGQ5QNXfhdkHHyC8+y7CzZvNK7wFAJ+AGI8oqbLweYwSbZsiKtLlNNaRpc39RW2RtebOal5NK45fdVO6XL91Y0x7ICJQbh64eputp6ttnaud2T7Wv6fYuA611ri1fRcNzCGRuIjYq9BAEI0Ihkqw8Lg4PPvIXN261Qb/8ktaIHhJW6CRUVNQO8gz2hpCIaHJUEBg7RpNurgEUFExixfnExPp4wA6kRiLxensQb47uCRN16ON6elKE+p/JJUQ0WSZo1FmzEaPfPQk3MBeZX1fA+xIKAAAjcHFwYnIcfl9c2H7RNAXJqAHZ1VJMRqTSIxbozcv//qJKV1ZVh6hhWe9bIeEZSXdwpTrfyUFY1UtAgsbDhG1FohFUkC1aJAMWGPBOAdkDCizQoigvGy9dj6D+qprg58A0XpO2Fz2KFIXYi6RIKwyYRXoCewW4M4ATQFVlfgpbfQeLw4n9/Lz8kYyzEB8BdIZLddVXcdTuFRVEUoQsP1+asf9verZmbiLW70qVCjKbZcN17LF2o5qGwKACGIRz54eVjTjP4Q9+5E35RSqfrU4ncWP9n9N4PZt6fpW/yczIuwWV0Vx2eznT1xZZUkuIZdqYWdg9MKQTEEcE+MYbZ+HCxfOnpZmOhnGty6P+Ic//KHu7u7K+P7XZXf3HRwOhwiTiZxnGQ6SBA8lo43oeLBdxmIinFmjQgykDopQmY08TxZl9bPKVw9Hw+xzlY+80vqsWwpUOkz45RGV6qgUUQKFFJASUjQKaMSoBUCn7eh5pahdIhsazDqsiTuJQFg0MteHjKYoTwBACBEFUYRJQK0COlVCRW5dEx3rNrad464cCkjr54pVEREJwELAgOiJyirBK+kVOIO62jiFU7C8rZhY9X6CLOqZG7uuvixu72imuqRnBAieYTKdLAWy7cK8LGKRcE0hCwpEpLVuSrDbEaDGYanSGrihcZTpGlfeUBuTuDxu43xemum8PI0iz8sqlGpjcGcuAhzKzXv3FL797Z/7oO0dvqsHcxDevRBNKaVmWcUvpg9oGl7opntFSx9VO1P89fnE8vBfn47rtgwjIlUVu71Rr//WxV86v/fk35prYzZsgGMCfrwBdjHHJ0USZzdOBe7eErgNnxkX3r19B27fvcUHcE9CMsAiGyPcAPAPnyG8DhCenOD+lZFOqgH0YSKSOAUCYM8mKpM1mQlAta697Up2NjjsQhO1LpRV6qIhMlsLakSUIqoVH8kzpHm//3ne7v/N+WIhrIoivHTr6UtCFV26JRsjCJHG1KKfzDA8PJ3Fo/mLWPinUIbzLKBX1ggIJJaccTannh2aPN2zm+mO2RlkbjxwdTnQ6k5kqXZdHm+wpT6swA0xRgFWRUMKnkE4IDkDxF7LDJXKABCBArRgu9XYvduG0zaip4YKgUGEKAI+RJcYA8qCCy/IwznC1zfh9vj2spbw//p3EN7YQjE9JCTEGKcc4pwBtlBXKvrViL4tnLBZjVZOUZYIUmOuWxsGpMYC1SwtNM1hppULCIqKrJ6hGhRvtI2CqMs3UlCFBBHAgMZGTE4Nua92GkIrasXIahbVHKvJ9MxCMlE1pY3nQQhIgqmg1zupjqZP04VnTBCb2WCn4a7wcn6vgoDEiHBhDDJKoarKRgK1EslrV/QtAGRIy8qDfzF53Ou7p1Eii6G8eHH23Lyyrai1QWi56S+7pLDkQSkIRImYXt0hHaVYhbJmocmyh9fo1Zpbbnk+EwUAO3t0dChlvEdWCwFT6cBFqCoMqbGL87kbX7t4K4yTN8rFnEmVVhwvfKmr2HZLpT1hAhmrlBgt53MtHpyG6mj6mCfVYxu0VB8DiBh1pocGhzZxfTfIh2Z7ME52N5JkmDfnGulwH9vOf6uV7Ey9m8U/cgQRFUGQ0Ckm8ilIESDm2dDbUBTYz07jdP7j8snxL7rPX73EdTQq6noIfae7Cmv7SFBGs9VLzHmhMExMVcxb9AV2mx/QMYotNXCIWnlvpx8/e9RPkg8j8ByrpJxVz+KbJ/v80ck9hdvf/nOrcexfVPeqLa5OT2/QYDCi2eyJy+Iwo/FiVE5CMh5vbWWZy3/6wfMHr726OTSEIsxhkojPt32I3AsfnUC4ceObcTr9QG7evCfvwi4cAsDu4aHsTqcKAPBoOJTB0xFd+Pwb/KT/M52fEA2cNYxkcoMTBR6TUuV9OGmzg7tE8hVnaZUhgsvQAFzZWet5hRFVo4ikNVTGiEhSi8JXsQA/T4cFXZ3UKmRTFRGZ1QAIiTEoAcAiQAQLHCOJqFER5Aa3JbpiGLVrFHaNfM3DGWtUUn34QkBgIs085RBoOo00gy24kQCcwhhKU2CyMEhqtPCxFAURWblutHNyX0MT6EpkrgDmg09fHByfTR8RkTZYCSZEBpCmu4UICLmq9FTRAoCpDcbQSFc0at0XNgpgteYRsYpIvdBI7YADICQwqmI4CscYPQMURGamQKeTcvH9fm6eCcSFVnmIkDLs7spv1QnqP48pAvBPbsNe/5ZMT59xvEDeRyk5xqf6YvoTHW9fF12uGGszKe3ozX7eeFiZScsyJq9sfS2bV9+aPjr61/mFTQuoRkSRKSxMIf5oehK+tftUv3P3LkE7LrxzB7vclr3D2vI/mD5VeBtgVl7Cwj9FuLwF4WcZlfmpSdI+BQ02KWKKppctyiqNGFMFW+fHqax1PDrezPUA4/pe08jiOIormB1oBHTWhqPzfv7Wjb86d7ofixBVGLk+6WPj1l37nmvdCgT1pHj23sNp+b3HH6fRfJqOsgd5L/vUbvVLRCCyRhQgghdgz33wsiGzxdXiYP5G+Mnz/cEvXL+Uvrnb03m5yivVDgRsOeLH5YhPRYEVlBUkqlEEBkqdzn2p/TwFlqghABhCI6AYVVWlQ7/XDv2+W8opIxhSZgD1XLdVwYISIE9mCNnm8vod7N5DGLwOACcgVUDtCWmQuXg+Z4Br7TxVOyO+ldi5kbTTSmQzWyx0+i8/CpQmBaS0QKLgLCERQOT6oqDUvNkGxVDHIQsQAhALEyKhQUI0BFy/XUIkTQkVFSQwewUVUjKoghFB64AvCAjoydpzX5UPoOT/lveGkzgPZUVDH0JlrKEqSfzMv5g/i6fzwlzeyDFw00yStg20NFcsu54AoJUH2spq/WgV2kPX6tRHtBqDKoIagsXxDPS8eozbvZMQIpnEyvzk/NDNC297ztaK7WaNFEFsVN314UgQAJXLArMLmxgMYqx8a6TQpiHYZPmtBBOgCmiMzssFFI+ODrJh/ryKUtnMRgAAIYvA6MJ5kadffuOrniDnKIFbVZKuoSoajVcLXQUAZYDEqko0J3/8Yagenk0TNA8Muv9PkuDHZIyBPLEKasDHVCsZaRH2/OFiP3784log3Rldv7i1/VfeGEjll532Goxch9Sv6ZCXmUgEzApRlFk1ZAYlCgDAIZzbLd6BqKEMwSRJ5ZVn1M8/nj8+vJdc271kLIKyNA7pdYxGjbOi5YEYEJU5AO/1TA4GgkaIIdTl79Im3qHZ6BoHDICczp8fIR9M79nh5vMQoQRThX6Z8nfaY9efMZrhL6zA6rZQ3711Cy+dntLVqyPy/iDJdocpBhqHUi/Pgtn5tS9c+9/1eunrW9vD/9dPPn72w15Csyw3p0nECbr+YkBQlA7KU9IAcC0+ib2YRdHhtpfiYYKza15vlKV+HQDuX/M6n4PO46aQPeXIToRQRETIktj6KCeGWmYMdLpKK5Ffp37AldJTAcFAEwDrnZIwiwEirK2iRFznCWq33bu0dq8ZcBC47jBBnQ4O9Z8iUkVS9B6ssSoEalWFECoR9Qwr0Gi39bq03rVFVtPKh3Y8AsRECsTgNLU2ZTWole1fSwNUJzC1FvuTkZlYsUJKxhhdLMK5qEQAIukIXLsnHHlJNIUM4BIHmxv94tnJ2U9yZyMAFgDoRaUCNVEEBQkUhQ0RGlG1hEqiqIwQJLIgQKzVQVJrOGt/kQVEK40sW5hrbYAIRI6KgD6KVl7jPEp1DhLPkn52TJocVsGXM4Z4MjiTXz2c/bseMr1776b+5tu78r3paSzc1EtI57qRnhYPD7+LV/Jfg8Q4DPURmjp4D63LvTVnzvJ83i5dEjQCZsmbu/8gqarN4vn0e3a7/4QoPqIsO9UBzbMyqT6YnvmbcDPuvtNAKd8GgHfebjbnd2D2wSWEG88A4OtQ3HuKkI1h8u7MbG14grywozRPquB7SbRjNXZ38uzZ9f03XnsbN/PPzeYzRmaSZertOjt2lV+kwMKIhkAUUVgwMpATdqxoka2NIhdhnN3yEkAkqohgExdWF8Bdy+jy+aoDriIqHf/gk4P4kxf/dOPC9gc0sCeA5gUgHQcjAQAgUYRIKGoV0SQ924dNlfSAWM5pQq8h4tAz99rcrGV3YVleNbquZqzc3q+1tlBZnLAWIDyNCinAPHpNUVQjYLRowwqkqdzUVaiftSuKNAIyURTROo+WgEIiKMJmw13C84OSDnZrgvTsg2cIV7fqhXnUkxghMuosLKoji4TL1bORELRyvpW6rHbIKtbJYdjLwWzmTOfhE0rcj1Dg2AQJoBqc6lwix+amJAMERhW5VjMTqDqjzgKCNaBGRAhRrJIxJOLqhAkRFimJJQoQkIKQQCmsBSIUamCh7A+zLH0I24NZjMXCZFCaWCn5kcZYeunlC4DJ4+L5yUG+v/EqgDK0Yx/tOFjWDistFCG0PTVgkHXKq8jKclIH/ML8yVEggQeVg4kVRCUTwqx8WJ4ujvPR9iX1ZagHnYprrslms5ZG/eSRAWIAaF2WoEhAjeANl/qlNuwWDcDk2RnwNHxoLmyfIEupwqGqGCiAUZe4NEuuyCD7YuW9doXeL1mSl5EvAsuQV+CigNM/fP9DPCju9Xc2Hts8eT8S/EQIpgIARslgVEv91GAMvcgwJupdtKqvuLK6RCBfCiJvSr3b4JLVqF1dbKsDwRY1A1EEWSSwShXJCACA9QOF2YlOADjPIBCA1wrmNOg9Lp4c/tvi8Owr2bWdHQlRsGuKapmKgGsxPYKKHCOYfo/cjS2oinLphFxtcwrYSZ/QVUqDikQ7u//0ua30d2VTDyiG0wDRj7OqOaT+2eUO/qUZEd65cwdvAmD5S1t0f/bE7Q1MRlGGicVrh6flF3/1q2/8/Y1R7xcCC7z1yt7/YdDLPv3wycmPynn40Dn8iZI+qwSnsZSZxaJgI0UuU99L0uheBIZrXjZHI74/GcHuPusuABzCA7hwnsGiP1BiVKWIkRMC5EQVB1mS9FZuIPwMKK6rk8LlGr0Kkw+eVVXngsotCnEpsG7yEVf3aQdPsBr2Y6toanWKKqgKGEVVOEbtJ4kyi7KomMRGLrQiQ6UCQVvsaAdy10HxrMJnW4xBPVRjBUIxmOVoHSo4ULBnR8Fu7qQxPM/MYjs4U4oTMUSOaOH9aWQpkGhUH3wV16jlulrsu4IpjgqbG6PNLDnBNHWnCdIpkC4EqAABTwaCgkSNEpyxIcSohlRVjSpEJTJNU7wejxIhgZIhhIQl9lg0q8ciyqocWG3BMZSCUoEznqNWEbgk9sWkmi8o7c9n51xBhLi1m/O9P/zDP3E82Frp3717V+Og5Lxwfm6qQnvJVD89+Qk/m3wIr299yUT19clfXxLp62pjaBapbgEPgKjeC6Z2lN26/L9gefbV6tnZf0ej7PcoT3u2xDOwdnrlws50vsBqNnTi/EyfVRO9Dg/gATwAgOsAbwLAdB84HePW8YSq/ie0vZuYKibGJyEhNv28op2A/Gpxev7Va2++/q3RzWtfeerPTFV5pQ6HCBVxvfe2pDzV/86CYgkiK7gadGCiMFJV5CZLL2pmL3KMolxHDaxsoY0QtZNa0IiUQB3B7Okx+PcOf390eft3Q6LHJsix2zSnqDQrDVa2Qimaz9MWamXLJFUVjlVxYS2icCQR+Tyr7BHUL6w965o2RQNWriXRZbULzNB4/VAEhXXU43zCElKlUpzWC4dSjQBAJa1D0/ElbX6DFFhu0qD1wUljtMpqKIihJKGCZ2ZnBnQ4A81vXNJiDBjyE3QFQDE9V5NvRiFX+Enx2LIKIhBIC7/UDjNuZRJBbDRBLECp0+T1HVv++MmBy+CPWPRASefGwgJNNmMTvEHL6AHQsnpfvweXGRRWigiWnLWw8NYCmSghAYKMA+ZWpE8JWReUGcUDSaUBSiY6Z2PnjjkAogdNKsuLGKsqBijZ+Ix5NwW/ODeajoJE73HYe7p4evKh/fzlV8giaJB2tod1ZwqXOIzVWqO1eLwOXK8pT20Xse1MNheWjBFfluSfnh300v6nonERiVgJvZL7tDyavude2b7UasCWtZF2/aDSHLIJmKXlSnUYVNrh7jVbPi6FrjR/fHxqWP9IUlhAJQUyBAQAdokJ83k/2Ri85RO6qMxMdXcKVyn2tSW6LbjaHqwggJKayQ/uv+AHZ/+4//n99yKE06hwaGF2VgYpN9xAzqNQGpWMqFUyKWo8YKuHymaiolMSuMiqb0RpR84rHZt00AktT18BVY1irA2dPgJ6ZBQLPQAIkJ9e0sGbT/VJscUAJ954WKjEY0Tzw/mDFz+gK+O/BQa59qM3sr7m8q3T6hSWDXViqEwFzLFxqcr64a8DUmouDYA1Wk7nVDw6+Tdbo/yHFfNJauR8g2wYPN3XP+/u1b/XAqs7GoQ7AJd++xJC3DfjUZpQTHuZgfGzk8WFm69d+vU3X9n9Bc/iywhYRYYru6NXtkf53oODyRefHE72Z/PwB6mhA5fiNESdk9KUnJmzqC9yCJnshPLgjK+MesJT0siiQxadJp4iSCJEqZWYE9qhIzd2GV7cGGT7ITbI/aXYd9Wz6jDRV3ye5ahPcFFWHIKUiVEWBATTYBQaUaosF94VBFNfamu2lZs0HawmZD4qSFRV9ZUHzI0ataI+aORam1j/jNUNutrMmpwn1SbDDepuQuNsFFV0hAYI0gp94tAlAFnoUybzil2yC6QlJATOsQkmBWemi2JaVvGs1082mYOs2jTY1aRq186oCCgivDHsj7a3R1cPDs4+TTcHAZUXBnXKiAsU9GLUE1CoJLJBVSaryIDLvANrAYTJEFoC44TU1TmxoNOqYmbQQeoAFAIiL6zDGSuUIUbPwFVV+pIjlJT3q2JW+VG6Gb52pYrvvPPOnzwe7Hz9k9u35Ru//duydX3CvnQ+slZuc3BSfXT2B/HS6KZaA6aGlqF24KkqXUBww3laEhTrqy+IKJUXTBUGX7v+JfzkfHj07v3TOJ2R3d4ccISz6aRKIKEpLzhSlUi/f0UPIcKILyjFM4VNAJldQIZHOAOwbtGz3pSOWJwVk7H3G/OzybZx9qtvfOmLfzd7ZfuVJ8WJel8x1uPttW4kdrUoLSlBVrbCoAIxMqoyFRIJkABFyeZ2FBz0hVlXLKJVaPqKEK1rnhIRpurpZNHPe5+KgwmonUACs1j4kvumwnOubF4PZeL5DN0453Mz08QYIk6mHnhhnKm0l7i6ayT1GLyZm9WaQ1pmN3SmL8AqGIVVWSLXeWrCs1ltupgCwAAgREEytf5KuiHES2AprpANHc+4NPEgUTSxypZRrcTojGd3mtgw3xvB5P4MrlwZgZ0+pfNqQIkDiOzF9NJYnkyPkypU5NA1MidYU7o33YU6N12XID2pvLq9URp6B3mcMpsBiRqIHE0FUlYWsIykjBjUIgoLQA8ApCJUB6RiDHi2Clo/a2JUvRoLnAEDzJ6cBEyN9IYD4CCqhjwwloi+BIWoPgTIol8k4OcRYjLMxBdbWh4+oP7EIo9m4oGizZNZeHz+fnUy/+V0f7MPQZogw446p9Wb6UqYT7CWXohtP2nJw2tZmgnC7Okp6Gn1KW31jivR0iAEK6Kml577w7OPove/SoYQItdpeO0IDl6KIFtGn63jd9c7ssvhBpAxUkzmSXw++ag36D3ylQ+puioCspqIyJ605Ix2kv1o1EFgYVFabhYd4093SK8AoJa0OJuif3T+JL+2/SwU1cwYmmgWZwF6i2TTljInyWcz9IMtIoqWKTJNIpJAokgFizJba2Kd5yeigHV8/bKEXx3Wl7IZQQVQYQYQiGiJvRitYA5DSGDw5lP9zuEt/fqNUwnHA174kyBzLtzm4HTx6Pi95HT2q26jl0DkVbEMawfzNc0dgALHABzjWjQaQheRgl2aUr3WEJrp/RdzOlj8ILyyM8EiLtRG3zu2snfrXYXbd/9cu1f/3jtYdxrey627t/BZOaD90jljqQc2bB4t4mh7a/TW1z535VdZgYugJAoYRCHEGFNH6RuXxtcvbQ3Gj44mrz0/LX4yn/sHqTXPDclxYDNDiRVSVpbkC2Odt6KxYpGcbDwDVYdEKWRpFRcDJDd21u6z6NX9rf5Xs9TtTithllYkjutGsgaL1oVkti41FsXJvAoisABEFhQEQZRGSC66QgXAcsNpi6xuFdpGEhCIaJ3iLOSBJTpMORoRI6JYVQqZqckKoAYIUXgF/FTtCiGbeY8siywQASAkQjJJFM4SxtyRTa2xCTtggWCIUgVAYqgsSZKiGMckrip9cTZbPB+MetfFfwYdupIL4Ep52ITKKiLC69cufPnoaPJ+iBxdYheqIkQQ1GqVYFqyiT7VLNT7nwe0iCCZUWUjkciSEjqDLGBJmQDITauY3Lp++QtZQoPvv/vwp/1+AoQ0icwsilEi+8RkpThTFs5XUnEwG8/i5Dzjt9++LX/97bf/9FMMoqIq3h6PZcpldGbhFWwpvSzAk+Mf06PZ+/LG3i2NhW/MwWujZn2JF0ANiwmhQR00WSFQeVSZxcGbO69eurz5v5z+7JPfO3ty/IEgPE2H/U+txZOAUlWq0XJgmQaUtKoxFDOr4mcIZDBH6zBjpxWkwS/SycF8kCld2nnl6jc2P3/9P/EDuvR49oIh1rAfEX2JoL9SGa74Q40FneqzfRSBKFxnRisQqKgGERjkiVhKNLJGaLUiDUFLOmdUWqXFKtWORPVcQuKm7GxJAF4kiQplLKqpbOUDOU9mDADAV0bozyZEBaixuQQKdTyaVQOZyxsBY4NR6WT2CdcyPVxRcbkOEQZWVlXwFIQBAaTwyP0RQlqBlAFBWVVRudFstZ+ZdhHay0dClrJwYAbIEoBBloWz8wwiDg1CX3r9qdrI4xNvk/GWFNUCow5oQ8AuKLc4qyz2XIzPz0/Lk8ks29/cZhCGlWumQ7RowO5tyhuiYmQwWQbZm/vXzv7w434yGE+MSBBnC6qjXcAEEO+jqO1LmgdlAIghEpegiQsaACCJZDQBUAGiBPMw416c+/Ev/Y23v3lyeHD43o9++t3x7i4qR19ZIGSrEYElTSJPiXeHZUxKiLtDkO/M9vXrpxm9GM+Aej66EKOg8QpwP7w4fZ5e2npTEIO2QgbplDeyLoiOzSyvZW8JvoTIaBjRiojV00lpwH3CqZkkIAVHiN4ImTxdhGeHD6qTxVm6M9qBwFynMnx25LuEL3QdsO1UoO20tSHY7TdwBNODs0jn8SPd70+AuZSBC6GMkoAlcWo4LlKfmD6AUE2kVeyQaGod33rGXjPqMOAnhRJSAcaVIGUF6iqtwEfwYX7iuW+3GAZbUM4imSRq4IU1WcZUsLJVA9Fnkph+VEBmqUPC2+QJ7Jpd6u6ddl6HCCgDRFZRhAowYwVI6lH3+JYOpm/qx+ffFwoak8wUbHEhUT/wj08fms3+5wQgYgMIxQ4Mt2uAaruX2Cm0fy5bZOXhbR0sWvlgigcHR4MseyGRA7F4zzGGwRfi3XsjhW//+Xav/qJGhHQPgC69Gk3UJPOVjJPc7cXIV3/1q6//3TRPeqcLDixKIssTCpaimhiQXuJGn7+6+42ru9Vbh2fFk6Pz8sN5Ub0rwi8QtUgwzAHsBDBdlNFHS4YDQCSMmrADpSozxoyR6DVj3c39zd5X9sb55+cRoO1MS7cVs5KkNLGjy7fRWKMBCh/xdF5OFPVY1BQGlVEIokaS2rWvUbTzdLx0/tFun6AeaIgCsIiyMCvnketemPqgmjgRYseRJdYH89YO3Cmu4LP3IYoiIWpghix11O9l+WQ66xPhJgCMFIJmZKxoKhoCeiWjbMhDsAzalwgpGMuHJ7OP9nbGv4iAKJ1iTpf5Y3WHAFsHTmuPV+XxqL/9hc9d/mvfe/fR7+2MBwYVPJINllRAJTrnPHsAa1UBHFAMEOqOIRpiYmccMPWM0SEzjgrPO1/73OVfeP3S1q85a1Jn7A+++7OHv5MkWIaoFZGGJEkghkIVmSMWHLHkRXKV4d47cufePYD/6r/6E2BMn/26ee+evvP222zuvRP0er+AoBPdHDzl+wf/ii5uXg2560MI0lLTup0sbBbujv176XLXVYIEaAh0PjmMada/0f/6a9cGr119Vjw8vD99+uIPJ0dHP5EEJ5TlgWIWHYEYIFQxpItacx0loPjC+ZOQuoCDwWhw+epbb97MLm1/lUfJqy+4cIuzmRAYqp1XAtg4MteyFFUayz01vfrW/qzIhMiiwJGDkokYAdiRglaqtm8iKHFkkBZLK9psgKvoIGBFpSVQqh71WGQWDRio9kFQVYNYIF8NRwBgayp0nm+S8ZGqRbTWQSaLop/38ytmlG3GUNawD13Ph1s6ubGLrlJgVowiYEA1AAD7iPXiOAGAFAByQFOocFBRaV5J7VzR5YPXGah2E91Va1zIzuByeHy8nxE4BY0o3rsCXMxtlDAVYIBevgenxQlIKTkp5YjCqPLEPz95ZC6Ndxk10qoVvpaDuOza1KIwJCTQysfk8vYrgzfLv3H6/qf/arC7M0OJBKTM4jQ4A5LkAdFrAagAfUhBQNycfIyOjHHeQpJ47YHI1vTw/MIg79340q/9yq/bC4OvXLk4rIDw2s/e++jfbow3U4p+jqSLKlYRix5Ydybnn1zir+7v89137inAPfjerVtyE54yx14sXRq4mheunz0Jh+cfxCq+oUioEltn4LoWFtbdoICiIC2gQpeThSWjjEhD6Y1/MTkY5OknEWGSRFuxAUGKJVIsY+Rn1dH0sbmwtafgIzZBSl2Q7OpvjXhaVi0UXtOd1teGmxJJmW04mh5lhB8IwSIhU/kyshmAlBWQ9WQIwAmKbYPJqe2wdhOn1wKkV05fQQWwGGtoFjEQCElUIABTjjS5eaLwLkAJI+BkgWAGIOWMxJBDgc3g5TJsDnYjqMYl5VnXqrmuTrLJxlUBBK4/fkYlRUFFNvrCALz15r7CIQDAO5ANtgQg5TCD6Hhe2u38UfX44PvpGxdfV6I6vXsVB1IfSrpmhqbVIWsRRuuPWndQuHz20kSnT1+AHs8eyPbWQYh+kRooioDhey/+nwp3vvPn3r3691ZgqSq2rsF7ADSCK2ZAnEaOwyx3Fw/Oi+tvf/3N/3Jns/eF4wWHIEAssLS/1hQSBa+KkVVTq5onbvjapeTm1d3BG5NF+OXjWfF8OqtOSh9PfJQjkXCmoh4xChEBELgFcJYADdLUXRj1stf2NvMrw8yNFhG0ijXLWF/KBFwSwV8KemkfcGtIzyZzOZ0tPnLGfoLEE1HjDYFp7ptlbbUKYl7dDi3YsfMAIymqKpIwNwfsqGxIkCynKhIiYpIYBsUSAHyrj1nmsa2gIMtQ7ZaFyA1ELkscvHZlZ/S9d2e7qnpVURXBTIKYqm7WgC2KguaV0CC1FBhmzqhLE0MvTiY/e7UIT/NeeiUGjtCpDtZgw40GZcUEAix95Fcvbd+UyFs/vX/4wyx3WYa6KaAzZZ0bonOyWgADK6gKOURCwohWM5NahlGA+Erl9dXUmqtff3P/zWsXx1crBo1B5M1ru381xLj/h+894sQZw2x6EisXwEQToRh5gAo2NCsK3b91S2/fvq2/9Vv/PZ80RL2jCt++e1envBWK05OF7NhjtDahF/N/Ix8+35UvXfl73NyvXdtw3eZpimBZOS6X0Md1sDaIAIXZjAtTYt7PX+l/+eor/Tf2fymcF0+r4/PjMF0UVRFmRVVOy1ksNKiQsCPAXuLSfm8wzjcubA16u9vbtJXt+VQHZ2EGs/lJEBZFRYrKIG2CfZMzg6slrVV/gNaIhU6xXLNDGqIqoyoLCBs1HIFsZFXhgFF5ObPGLjxEO6MAVmwRA0gG0o1eyg+PUhy5TFUdOOeYvEvQhvPESnbeo9DfpLJ66jLsJQvGPEUYKcOValb8wu6t177FljIuakpVjZNQ0G4I7woWtRz2CCgIqwFhY2RlDpw0n0QfAKKKYlD19ZwOSaSe5+tylVhuAtjZkAARwXvO98evTz568WvFpPgoyXuXQGRLAU9JNQISlqGiw4OneGl7r0dgJ+ehmlMQxH7/mJ+e/W74nP+cOpthbX9p4AndDVhBGhFyA82q35+vNH/r4q+L9/vTxycfpZu9+wbND1H4QBUKClwiuSJj7wGmEMEaa9QRmR5q0keEfuGLPEyLNy9cufzrV776ua+UfTM+PDusejZJ9r/4+t+Ogjc+vPfBHw2GfamihiTpnXMuEiGrsv0TvQv7dSRQPcqAd98GuXJoI/iJJ4IFDtJjfzj7bpgt/gpuZGOJKkYVUWUZx6m6Pg5agZQVWqwqdR6kWnVNUL6YKJ4WH5ndnU8iyLmC9wK5kFiIWhWun534w+n9jOVragjQx+WJEdeIK9pB9zTPLKEuiw9t0aJ1VSbWiF9UFJ+efeB66f0osSSwMZjIXPYE/cSqzSkqk9FVBNuyuFp2bmTVthIERUWh+qlzox5GR6isRi1YRW9jahJPidvAyMdPBgQbAEO2hiIkEjmL3gwN0cVqNv2S6+ffzC7v7s2r+coI+5LAfjkSbbST3Nx3rIAsqjGIShohmyVwYQPgOwAA9+7pd27dgptwT483Bjy2IcyrWJkMJsXj89/3B5O/5q5uX5ZSIiHWMFnRmpqP9fBXsTV01AkDrb5tBed+uYfQ8BmJhFGx/PjFqYv0e8HSGS78zM7iYiNPw+GtvbrnDv8TKbCa8SD89m//Nl1PErIXzlxxZvN8ONh6cXy+/eb1K7/yxis73zwtOXpWqkmyS40hINRQw4ZZi2UEqKKosxgTg7QxzC5sjfILPoRYBfaF54WPPPMhehBgICBCTBJj0zQxWS+zeWJtKgAwqVRDnUO1HOVpRxPVktCxAdFh68dVBSTQyEJPD06nVenvZ0N3EkXmTqlSVIeqVlrHzzJLayUt0BX3sEMbqo/WzQlBySCjS4QlKGqpYo1QKcIioioxqoT69VKdH/+ys0jXpjzLFkIVGK5cGGXzxd6V9z49fqWXYZ44LBE1gggYg3ZrYzj+2t7oy2lqzb/5/if/NxDwgIRF4c+fHZx/eP36hSssa9K0VaDsUmKzek9tInsRlD9/4+L+sJ/13n907IrSP7JqpwnRRJTPVHCGgLGW9CKiiomojr3mDLCXu+TrV3Z7N2/sb+4M+pk7WzCzgiIqlVHiW6/uv2ptcvt3f/LJP3MOPiXnMox+IYmdpv0xnswDZlC0Wrr/vx40RNTf/M3f1HfefptzeMfPDj1aCseyNzD83rN/IePeLbm+/WUJMVALwWg0HVHXyQ+wJiIVBKR6VChLvjLG6MGfV/GcztQkSZrvDl7vX7r2+jY7MB649JVXRQ9SI9IIKTWGUsoS4gRgwnOYLo7VL8oodRYUirRjuw5gEjr81sYbv9YHbfWjyw48giCCRK1UwCuy954FUEwsvecQOnRoRdLWKIhKbRB4c1JtdYwQVdz+1tB/fHpFJ+X7ZphFrIStIQkZYFJUCScAUk1MMGkaQpWSJkMPPA5PTm5c+sIb/xFe2bo0mZ8yAqDEFsop6xFSqmsRye3eKQrg0CgQI5QRF5JoWniBUFE1FAFPGjkysipaeimCR1E66QHYGd0hAIiPYPOc9r/61lfO//DDYXV4ug0Whwbp1CtX4IPpb25e/MKXfvmLV6+/evMHP/vxf336ycP/1g7yQDlGfnT0rjw7/xDfuPAV8fPYHSF1svvqdawJvAWs5QZaBSicwOgrr341H45eOfnZ46shVlM7zAmjiklgAQoTNckcVZQ8G2VNK4YRl9NNLov84oVLr+5/5Sv/me4Pbh76qRSnZSBAs4iVnqqH/S/d+NI1zzsPfvbh0+HO6IRjJCEnLlI5n3uCfwBcY0WaIuvwlmbPfkdOX8kCsitSky6g8h+Vz88fplv9HQbPrah0NYJdV910/YXY7YziUusMDELzZ0eF8fCu9OzTUJVzKqsQjBWXOTARSjfIprOTyYd+VlQ2c1awjRtoQo7Xgv+ae5foZaD+cuy/pME7g8XDU2+m/sdydeNEuapiauP/r70/DbIsufI7sf9x97u8JV7Ei8zIzIjKrMragQg01AC6G90NdjNJYYYixaYNW5MwjUaaMUkfJA1l+jD6qLHJSJsvspFGJhuZKLKHYxQpM2qI5DSJJtkrWsxqoFFdAAooVFVELVm5Z0ZkbO/FW+/i7ufow71viawCGkADhQJ4f2ZhuUXGu9evX/fjZ/mfUIUWcEhhPBsvSsg7X5Q9Tk/48wm18+GUmS1N3nlRCw3os63T6Z3uQrDaaiqGg2UJ1VgSXzMNRF7SnFKVGaUQkU1aCngiOx5c5NR+8vxf+dRTiRbYsSu7euOEyv80H5MmzczLFlmTVjpeCgGtDBg2gdGplmBQtgK5fFm2r2/xU3eGPm/UnAdyYjUC0d303v575qnT58vgPc0fTuYFSOUx+aLpBiMzvbK59iBFer4Chp0uuQfdN+qNxtsuH49EcZK0YXdfWPa4viwfjnn1YRhYpZV07do1au+0aWsFZq3rQhXU6oPMxnEUPfGLn3zqr2dCKrHsJ0neJwTIJjlMpfubCEREsLlQQhAF77QGAqUoCHTUjlWkgLZMKuSZp33DWQDrPXqZsCvVd6f5UaWrVyYtxGR+XheabjRTPBYiQm+QqPuPjo+I0IFwriVIJETmHJEtDHB9QvhsdhKj2Ul+PjkVUIqEBfBelPdcqq4bEdZFznsYloLpyufW515EeJrrJXPJggRFZTuRk/skAYLUQT7x7LmVlaXGLx4O0h3vJQ0USSMO4qVmdHqpGa3GoWns9tKRVvoJFn+bAAmCgO483L976lQrj+NIOV/2lBKeiafKfOLi9FUAAbBecJx4d+HcUnN1ufnLDw77F/ePxzv9sd1PrTsSog7AueSehUixIIpj0zi9EJ9ZWYyfP7NUf75ei+s5Qw6G3nkWUgqkieAENMy9e/HJU8+HAf7m9e/c+l1tyBlt9vIsO+paa0PR3g2XFDD8oab01c1NuXztmnx7tyPh6rJXeZhC+aFZbh34b935V7JYu4BGtOSTlKmozKH5PrrzC6bI+zsH0LThsdDEmCEmuMxylnS4p4mNNqS1BiltlNJGyro4FoET6/3Qe3EMKovPStFGwlR886RZP78z0XxoZr7hwMTLRRpsFB1/+7YP7vXfxBMruWNKVeg9Z0a50bjvPacUqLpyxfvCwjQNsMi8oVMmc4AgjgXNyNQ+e/HXev+/t+6w8zumFTVZRUfk1VA0WQRAYMU41qHKfJQPekshq9WLn/vMX6GL7YuP0q5nBtG84jk/lus4J5VRjDEJF22OhNl7Zi2GjVCci05rgiDhVEXemNzx2ObirNdRTPMSAqWy56xtImZ96VRpSGbJWMIzS9FTf/2zz7oHB0vpYHyWPHqRiVTrdGtx8cyZi7wQLb2jh2rX9j5Fqf1GWo8ewbHwYn2cv3X/hllb/iRrTdqBGG6S2XZC/pjnpUDKlc9bh56M/NLHzi1eONf+5PDOvh09OHw56fY7Y7iRN3TEzH1YFoEEoTKNeqPx9PKZM8888fRTz0bn2p/aV0n7UW/PUiGeWugFKyLkOR5SN3/ql9fXWgvNS996+ZvvLZxrg1K2iMx43EDymVe7/tWNDcbmZuHJunaNFuNP8EjteIxgbZgmqhEe5zudh/TC2U/TfO74JON5KgtDJ0NDNOuLOou5FyHpLHPKPuz141r0wHmbhKwzu2hs4CC5G1FEKudamLjdo1tpd3BUu3BqlXM4dUJx87EWXlwWKM3LQshj81qR5N7r/MHRbo31TSa2wlHmGI517jEYQAex9zbwpFQuqU0LWZk5Pb35X+fuc+JL46L3tw/Xzz9jj0afSu8djYKzS7EKqaZg6oh0ltrMBzFADkZZU7dpdjo57q8tBPWPnf8f/sILyblYD3td1jIRz527l/mSpbmcWhFPzEUFLbNQ0fDdFVLYOwBexcxbebAtp3/u8/7Bdt9RzGkOTszZpWF67/BdfdD7S7rdALt8VnI+6SM4EeqduB1OhGDnPFaT/o9lgbBAgbXC6NZOonvJDT7X7Ke5jFGL8iUcM67/+KUZPmQPFmFz80q5t15UtaChc+JwsRbGN28fmv/oNz/3G81mvLI/cFaEihL3x8N0cxooNNMZnjpMFBUdKPLpEyribjRtJVAUdvC0fnM2ZybGTlGpIWWyYpnAOvciq0lftOkfBFoINx8c4Wgwfne5Ve94YCSKxuQ4g0A58cJcVASKFJ015joKT5otv88o9yzkBbBM5KEE3gFgeLYCGAFEAglYKZE082PvhSEEnnS4n2kpwjOTIhJFpTE5c7BTZosWfmdOLa6dO72wqiZLA2nFIpR58VnGuUDHtTg+d9g9vh0HhsPQ8KA/2t191Ln/zNNPPEvEVqTwPNK8FTdJlpz2SZu6tUgAHI68xIGqPbW6/PSFM0tPjDM7TnM/tM6NmCVlEQ60CqJA1+tx0IoDs6iUilLP0kk9Oz/bNouxLRJOcyE6Zu+eXj31bGCC3/iDb7z9zyG4FUQmimyQK8q8Xcpc991uaRX+gG0SiOTalSvyly8t++xbfWUvnE3TzA6lobs6MW/yN269ZD/37N/mQDNlXAg8lafhSX41i7yvW9Ljf5CJTH0hp4PppuNFe+/Ew5Y9yqYNM0CQoiN4ocZVtjnhmYbQY/3y3mf0Udnip2wDA5nV5/gyFMKBkvH2A6Pe2PtKdGrhT1PmDJ5G3rIEoSbftfuSc4+jcIFg/QkpQeaikm/mIp7pYAFks9zRqdrTzb/+if+l33r0ujscPBx1R29nZB+SUrn3uReHINY6qjdbZ888ffHnG8+ufjZvBs8dDY/BjhXJJAG9lJ2YT+acaOioWRe96QHOC3vrrPHgDCkUDI6biQCQKBuyFzhJ3dAnbqwWCBYkqvTOPa7pIjP1ielCo4jQGxxLWovC1vPt1Vitni3ji6oPVh3fE9vPnY0DTe36M6nLzwXe7zgnoppRkt/ff5Nv7f5a+IkL52x/6MGF7D5NF7Rpt1Eq1PROyNuSt54OekdSb9RqzU89+esLGxc+xaP8kQztAWf5AUT6gTFUi2uLcb1+Wmrm6Tyk031yYSfZ9aMstbqQ9CSeC90oKNg81fdHe251/cLnPhXowbe/8vIXayvLligf1erZ6PCNHfeZ/FW8utEt2pJsXZFXL0HOH/RFLUbe9kzCzXjAh/0b1E8y04w0bC4yp5kznaKKZpJY5aI6a/RAVLYiFzGBuP5QcWd4h9qn9sS53LPNR73Mq4ZILSbyGXLyNlVK7WR7nVvBxdNrMtFe57nelXOltVK4CWdljIXI9PTPIgJRWuwwUfyov6WWFu5bm2dGgoQDnYYeDrnHOAgMKW9hdIrj8RF7trpYfE9UgdOJZuuT5P2iu6o45iw2YfMvf/w33ev3lkc3919PJN9HYI450hmL5FAsQY46aTrdbDafufCJj603X7z46aHJlnu9jldSVBWc0Oqbq0QnzIVHZ5px4pkL7wUZzjxEu1yAHWBjY/YybK3Lq2jzc6dTdzTkPExNghrGnNobyd2Dg/j0whkUaQaFlxsnW3TJrE73ZIvPuYwdKST4wVqJKJIsGWl/+2gvrtXfy+F6QTpOiI1rHIwZG9sfku/qQzOwBMAmdtfWaBWnVKBSHZu4fv2bb+F/+z/5/L9/8YlTf/lg6K3nwr3kJ1LjJ5oj01xez3x5brFpcZkkPvmzzPUlmc3NuWoDmlvyZZIWKjhZvk1zrtFJX6lyFWOB0SRHvcS8d2/vZqjVVwGzR4wDa92QTGCN1sTsGiySc9mChIWnYc/5JL4TZbClheSZwSLCnuEc4NkiVAFr4yWHCGMgARkapdkgZ84IOpoowUy3K5mq0pOfOihoqiKpBJQ5Ru5ZtCIYXaZ1sPNzWlmkg0A36tGZWw8SiZeXEnachLVI33q4d/306fapZj1uOWEuPVdTlc2pCsFcjHLa1JqFNAijjDHOGIGmMDBB1IqiZUUnclUL5XnP0s9ZnGdfVH3RCUG8SZkmkUCRIAMo8+zOnW5d/Gu/9OJ/8KWXvtM5GAweriw3XOIU5wd7Nmm36crmJl2dPFeZlwT+c2Pe8tLmJn9mbeQOVxJZOsbAjdwuFms+OBp8KX/tbtt95plLFJIlm0/6jk4LAHjWHBKzyBKdSNmUxzxINJfgRpPqPCoK1meJ8zJr3jo5XZQbhcjJRsc0lyFNSgkmuk3lRJw+hImILZFwqJFs3Tfmtd2v1Zdb/28b8INAbMciGkbKwemaqFG+40f2Hi81ngScL5Rui3xkKjQ75jIOyzdPqSI1SoR4nHmqmydrv/rchXqCYTjMHlHm7+WJTZRWOozCJhZqS74ZnUk1nz7kXGW9gS9cuEyuNKgmjaAxJ6WgJi+aL9sYgAAFKYTNnTCTByzgjWBQbrILdSHuCtXq3lrX4VFy4M3iRZoq3s8ygyZNZaZKPjKXAFn2qc7GKfayjImIqHCxCwl5TZo0KcWaxcZmiRWtsGcP5Zxj5XHm9Jvu3Qe/LxeW/hcIDeCs6MleO5cMTqViXNlGfbZ6UtGHdDRKMBonPjTBQtCMFsPF+otGLSAgDQEwFkFPHMZZX5J+6j1zroWUVkTiPTlgTrWeQGLhiJA6R0O3y088s/zXN9QvLmz/4dd/Wy3UbH0hGqGR8uHF1ewz7wKvbl6ZvmN61JE+znEc6AxBOE4HR++4/f4DWjz3jAj8JDRIJ8a4fC1YaLbGTGY+lx774qiYP+pmNLTf5ifUvrNuREbbMFpywAFGtEKhhuU0z0x7oZ8/On49GSa/qpUiJa48FM6HqeaWBu+JmE4UgMucp1QU6XynO6Be+hU5ferIi+s5ykZgZDxgdj5UAXLvvEslCMd0MLjvh+lQmrVlUJFXUFQQyUmLYk7yiSYx98xxHgeLtV/92L+38MnnPydHwz3fHx/4JO0TxAVRHIfN+pJut86odm0lC/XSfjqgvJ+yglZevLDI4yGW+fjgzKlRVEyKF4H3HgLxmQIr5YRcQ5SLBLgG4PJ0ncS1L1A4XBeD2HM8SpgaPd1q3Mjv7L1KHzv/N4w2pNmB5wO/crKJDh6r6pw4JmQuJ1ocE0eRpHcOifaHr+knVt+TJDm28TBd2nvotgHG1iWZXNrPhoFFJLhyhQCgWTumGs6aBw8O+T/8G7/6K5/82Np/PEg9Z05UEeKanSan2icnD9zFRC7zC2i6IUws/cn+WATkJw3I5bHTJampCuw0wXby8/n9CfrTsE3Zj3yS8Edvvns/6x4Pf+fsqcWbStwxadXRJkyADIRAs6ex95LKrF3xLE/+sXubbwRN86HKMl2axYj3Ik5CJnbeRMqpMJT+KBmkuRvUIlNnkBORuaTMWUXG9CwyJ45XlhWTLvWNvC8qyQiz9jogEaWBej0+R0LExGMhZKHSdjBMvnXj9oPFjz9/8W9royA40Yf5saqqE/XF0+IHQtHVwllGaiET/XvMabBwGSorLpiUyMlKotLxOPW2TO0lDzocOnvu9OK5f+/XP/m/+aNX33pn5/B4K6yxjYKVJLEH7wtl/0DzWgSvbm76z2ANhy/spEvHOB4N4dRyQ+IHg//G0d2Uf/7JvyaGmHI/dZfIXCk/gWb5BWo+ejjJE5rpRdGcd0TkZJAPnmneICKlcKJiY76Nz3wZ9GSoPRMpCM2Nw6R0WgQQrYRDouzN+5pe3/mD+Gz7H2aE+1rHXQn9QLKxTUxIZBNSoTqg3f53/LnFzwmV2pdShsp4FjqeGIXTutqigogIQn6c+9Q4cBg1w7O1FwITvFBMHYWUgMTnSNMBbJq70kWnPIp5LIw5YVeeCwfStCnwXL9MAYpKFJd5CTwjd75QcqhFQkoLcIihq0uUG6+MOfaHw1t4Vn6xUJmgUh+hWK/UXKXTvH6SwizfjUhBMykpHIVCk7wWOHIgcMbiQx15o0+rlD1ipJ6RBJGyeSf74/zbt58PfvXjnyOjrFinFM+b5aUVV+Y7zGsnkSKQx2SeUOYyyZPMjVR5MCvmzCQrRsrepQSQ9tOfh7mTYOkpVKrM2VNAOqa7Lvfnnjn76y/+O5999v7X3/yvk1G+FyLOx4DvXYgE7W3G1rrgYFsajXUeZB3JNTuNKNe18KHb7b2hn1t9VrQScoWM6GRdU6VcDuZy6yaG+vQgXWr3s/fa7w33jNC3PfuBOB5nWWxbiy0P9MFHYzK1uvOQ3DfrGd3afccdj49wavG0wHqaVzYTTM4J03w34iJXURRNU0yKQVPswEG+298K2XzDwo80+b5lkyx1Ixv0O5KuGjUIcqtSneqQRm6c38LB8AEWm6dRpNFN3z+aN67mxWVFQOKJCGTHLE5biWrx2fC5U2cDdRZh6V00xiARj5xzZMkYvp97YhEiTa7Qiit6R86n08pc82XMijZECmFXD4H3DHaedJmOWRhXmBlXk0Pr1rpsr63J6uoOj1Kfu74bBY3aPt/b/ard7/+yvnD6lLfe0fTD+UQ+8kwKRaZze2Yb0LTAi4jEA9o+6HRCj5dtQIfa6V48Uvnd3Rc9VlcZm1flxy0u+hNJcv9MWZETmJR29w7cmZWVXx24oD7OfC6AZi4r4eYlGQkQPpmMKlOhw2LCKZo1Vz0pNjbX+PPxvdHP/Yy5Ulg+Gdk8matBs9kdmIDfvHE/uHFn51utRrQt3h17E3bFykhJatlr5SPlMju2mbd+WkUoNF9tC3lMWG26MJMSzx6OGQIPKyKBEXG5SJpZTzWLMItcHPs8s3mn0xvvPrlWO5u7OU1Lmm/XItO8qKkA6JwjrehjWITXMJf8MHGwqGIRPW8CveQyeyiKciLdbzQb5ubdR68EUXTx+acv/IJS7FiK9tGTzXuWg0hzMkvF53tX9A6kWRiK5vs1z7yX9Fi7jHlZv8mzLhJ71VxFpgiQe1EHA5dHzaXzz6xd/J9tPbh+5YxZMsd8oH4khwcRPHPtGuNdYAc7edCUUTDKumql+TC+vf/PUoHKPvPUXxVFZFIrVGrpTztP4KQrnuZyoU64xWXOIzJt/Iii+kZm3tzp850qHX+AvpXMH4ZlZnOxJ5xMeS+8I4Fhp0Rn37hlg7cP/3n8xMrv+MAfaG+OXaiOx76baRN6tpkyFCl7aiEJ3tt/nZ891/GNYNmkmZ8kkWPu7mj6/lGpsTWXjkggcQznEh4Ox+ynsbDiuhQRFBERVNEBYXoomTV2nspkUNEEeTrpZdqmc3r/hbaVK9y2qiiyHwz6iGuRqJTFLYC1HVl3umHx4OiOvLjqVC0gXbZMmeTo8IlQylxO3Vwbcy6cvbP+65P8kYnH3CtBHMLXzFk6SuFV6Eisd6yFl5tHeGfnd5NmuGY2nnpaOZ9rsFZyMqGCHtPWK9T3AVLzVTCFshH5UlyF506xxWkS8wXUMxHPiVhu+e9cvseKQdBwjrE76o6jZ1tP4F7rF/NvvPMl89wFtZhBoXnyFdru7srZ2oJHxM4PXeZa0cDvHL5re6PMxDooLOaTSc/seWrQzs8jmWtKrIyWfJiS3+3eiBZq933iLfss01BejyB3Gy0+3+pjEECrVFsDyi3LgToYPOSz7ZUTB/L5XpkyH+8o59FE+2qy2CnADhJg7/hts7LQ85nLRDjXQeTvNsC4uMzPJR1kQ++QWutjk6pYHbg7nS08tfrzRc6lnNiIaH7xk/kDEE1knwjCcMOxGwyHEKUEqnhHCv05BjFIKUWFr1ggzEUHED4RcJuTF5l5tCZhblFTIWw4z1BeFAlLJkbGLaAdLQsOrgH0BZmtkQA2r0j4AjjrtVxghplQOEKkb9p7ezfMU6dPQwPKyyRxZm6x58fUGGSa6M7Txuel8aWVpL2BUnc6N6KlxZsuHY29l7R5Wnt8vcv4+7/1oYYHJwerD6OCUF59FXj0HeDBztB/7IXz5kt//PJv/+vrr7398HAUphkDRN4zxHPhMC+aI6uTOVjl6UzmPDzMhfo6M+C5PIky4BlgIXgUTY25/PMkkd0LwZcTpfh9UbnHUkygyXs9KeueRLyU1vz23b3gG6+/ewjgOyoMjrz4rtIyBLtUtOTKkBXrPTFzsTPMSownRiSXDZ2FcaKEnEtlds8s8PBgYRFIKiLaOK5T7kPRLneZzXIMAqW6OztHW6OR5ZM98GRaeDONoJ0Yt9JrxbPxcExwDDhfjJ8v/p0g4qMgWA6DcNU6J0pEvM1T9q6z2Kod3bj14Mu3Hhw8YGhj9GR7o6lRJyLTz+O56+DJOEw/j2Cl0COznmA9wfnCs+Z8oddYXhM8A04AX8gplbphgPXF//ceyJyAQdwbs/6XX3mbf+9rr99YqtVUBgC9H938vnb5Mj/TbnO7DcdPh6k0mn0e+w6dO/Wwfvvgt2tfu/UHcNC2GRtRikUZYaWFoQpXu8yqcnja6Pvk38nkXidzlIqTm+fZ//EiRWiZi7+f/GwPgZPZnyff62TyfaWWT6lt48t6IScieS3k3Lkgu/5uZl7b/0fB2tI/9eL3/Nh1bTY+9oeDcS1VWRx3bRhENpRBbkKfZb3Be7jT/RbFETGReBCslD9/8t5NrpMF1svc+yuwnpF5Ru6ZXFEPp1B0q1QAKS+icmbK2CH3DOs9vHdgz/Ak8OLhhct7LT6XqRgLj6JwpBw38nPvIjMryaBgXZFumQ5FJU3Rww7nw7FTtdDlh/2H9n6n40ygHSCOymdDxfNxhbZKcY/leDqeaNsJcu/Lr+K6LXtYZlgIcgic91DGgE41zrr+mEiLc0rZDC7xOR+btfZb9K37X8zuHR7mjVroQOxA5IXIY/bMi2vg8s+AF4bzDOsZ1hXjZT0j5+Ir8x6Z88i9Q+6La/LMxc9gKYRlheEmc8sLvCt+lnMO1hX3lQWK++k4uvVP/w2Gr7x3uPjsGqfZiLvZWHADwNY6TfJz8MKqBDFz0mFnOcl1XE+oN75v97rHTpNywuLKOSKlIezYw/LkWc7GmMsG9p6BHIJsrwscJ6+j1RiwcpnS3pl2XBg5K+C46djQ2BttrB8nVhq65/cHN71lYa2LhbeozS7mEYrxtMLl+wQ4YXjPxVj4YnxzYsru7Is+Su66JeO0s5khZQMZemAbuLQt793POELkMxfl3vpUlltDd3fvVewPuhIGugh2UzmnBG6yZpb2EE/3ruJZFOumwLEQiyLxUOJEeSckThQYqui7xsXccwzrHJz1Re4uycwzNbdueCq+pp9XOLum2l9CEGsBeCfkcgHufPAiubEtjQNwIEM/lsAnVlKzvHzkb++9mR/12AXqxPPkuXvzkJNrhpR7fPkcChuA4UhUfnOX1XH2TVc3Q8ncOOOxvdtvCdbX5X31ET9DBhatru5I8xPe1xYCKzrKIsU3dePgawAAQSdJREFU79+9/X/79mtv/MlbN3eSo0EeEClRqgjDnZSOmuSATJpuFkbJZHGePnwmeJ69ZNPBl8mkLCcGZpu855mxwTzZgMrvmYWoCskTpfjte4fB11979wGYf6dWi15XQnuKpS/jZBRqlUguee5G7DQzF90zJ11Ap6nIMjGsuFzY5WSa8qTLoWexzvvcusyJTfwoyT1U6LPe2HktOedJL4704aP9o1f2Dnv3oFQw2YamHsDSEzJJqJ83TkVm1zD/5csNojByAS8itVoU1WrRk6MkDZ33EEWJJukKyU4YqLfeuXHrS+/dfTS2nozW5GfNQkvD9cSiQOW4T14WgeViE/KlceSmm+9EyG9iQMwtLCefP3H55X2x2GgT8PHIBi+9+g7eufnePwpM9iXOQ+9ckqUMv9t9Rt7nkfphvFgArm1tyQY2fOtB3w/8cOxEHXLm7vO5Uzdrj3r/YuGlG//CdLMet5cCH5iiESkRWGkRpYVVUeE6mZ8zQ6QwhNzk+UxLpBmTNkzTzXzue4qNlU8smM7zdIGaGWHl4eTkZ4sNAvaN0NgHHWO//Pat2u74/xk+f+6/d8rvKMU7HMmBROmwGz7K9uKm3U1XXYS+T4eB9V2bqKfb+/KdW1/iR71RVgu1o3LDwGwRd8xzRl+xeHK5qTsWOC+wPPniYhMpF1U3Z4xNDAgWwGmCHyTF95Qb39Sg5DkDc7KQs8B5JieFigQTewsgRQqqRaKSpjQudHwgoc+DVpZanwSt6I575963XGqVNVocJgYvZvdHmPTIgwfBE8ECsOV9Tu7JMZfXyfDew3sP5zx5z8BCtMI2q5MDK/Fej11G4GMntBOcan5VfeWd/5bvHT3yC/XAE9gRy8ygm9ug5sfLz+aBLcfX+dl4e56NrxeBmxjnmPuaPK/5cfcCxyK50TLs96PB773Wqd0+/ifR6vKXR6PEmZydCZnfW12WE4nG16/jQdOxiZ2zhjJmGStN97Fz9MCKwEKkMOpK425i3Mxd19TYIYgvvjhjp9zO8VCLejc1knuPJPNRHqHvsQXGwba898qymD5zYuGct5ksLPTlqPeaH+VDp5X2BJmshQ7lF5XvV/nn4v0sc2bZw0Mkc17xw+6xCuJbPpfci08z5/PggBkr64xrAF5YlQe7jgMy1npKQDSAxhu8feebHASUB5odFfPGE8GDyAsKA7p8b6aH5Tlng/cC5ybzqfhy7It3SQqD2IuIExYHFqdFHAQud3CKZgbNdN0o5pOl8n6ZJyLYYO/BIgyVE7Sj73lo3VqX7RVwEDMrjryBpC4Oe8HYvm5v7z3yRptivGfvK08MK54ZXYzis4svmf7eAZJnVrtbB7dIBy9b+CFbN9IdtoizIjT4wQGtHyv6Q0hyp5deuoQzf+eA3GiEGCmynoE2AZq18Hg46H6jO8ze6g+z5dzLai0KTT0yngpXJHkBycRiKJNS5/PvhOaU0d+n9DuLg50o4VVFyFmI8L7iqknspJQ3AAGB0WI9Y+vmo+C1N999ndP0/7XUiL9pQnNDa3WkYxoxhUmKxHofCCtRmlU4zn2j1aw9f+5M+3OeRUSIJrk1k/BEKXc00XEsHopWklmv7u4cved9+vVAh30tuuehxkZnLs+HAlUDuUgELBnbYZb7pN5o/lIjMoonNXxz4aTHVKxxUghj3htNkwalc2MBgLR6uNfJjzq9rSgKh5mTY+X1CHBjknDs2R0eHB6PHennm41mPTDKeWElE4NSZqHCqVeS5mqc5kJiPJ+Ue0L9/rGqUprF4OefnVKgwAR8f68bvPzttw7uPbj/Dxbr9N9TLTzMXNaz42SUn9LZ6VvGA3fkpUuXSv2FH7J29+pV4KWXZHt9nQ4OruPiwUUePc2WWFI3dgNeaHa081vRe4dv6ka9YU8tPmkDNspaV7ZzIZnTeOE5j6dM2rLMd5gvyyaneQgiM0f6fH7qNHQ2MXYZ/Jja//yxjkmEtRapR5pza/jrtw701x/849gE/9CvxX/Gzh4yUye11DMmHnVOJRm23vE4M2asrKA/jrBIKeW1mihqigw6hzzKVvPnz28IkSNrFeZUqGUuQXyaLC6z8PIk0Z/nw+vTf6cTcWJmEd+IyW/dZXWjm7kXzmjJPcmskvhEA/eJ6K8vy/m9IuKHnSzoZf8GS+Y2sU/QlSwej92d2g6PMosmLUFjTLTQhL+zd+DjcF1dOHOGrXMioubKVgpDl2YFM/OfPen0VgZEil6lpeexSJQu8vJ4nIX2zsG/oSh+YL1zhDAlw5m3lElAqTbmjn/t9nvcqv0cnz+1JJ69Yg+BIpmowpZj6DGbB9ND3uS5zz2HkznOZauh8jVkms+3Lg6CE2kYbwznoaZ098jY69tfrz0a/13z7OnfT73cpXzcd2GUHJzNLL5dYxyszJaejTGQP0kt58klI0UsWmqKcTResxdOfUpCzVRIDtNcNGG6fvAk9aBcT5kFniAuyQN6a+ddLcGXsKAOvcUgbrhkz+xYvHTAQOHRGP0PQqodHatcyKBhIjzowK+d/jSfbp6RLPeTAqHCyzPbc3j6HDF73iBho9n1x0a9ufudYGnhSyLZsbfxMAp0uudzB3W7yD9rbAMuwJlgWezxGClyilYaPr+1O3LN5i/JE2dbyDNf5tmXqYsTZVOieTVzoflUS5k+c6ZZRf5kHZnPb/ZaMUcg+9I7uV+owy/VNXJXSoBNHA3TApeJk4JEAK8I/nik1MPjt6UV/ykSHkqGvNaH7YUHjC9uYyqFcBWE6y8B3zhD/WadGlmicxMRcqfIaOsPe8v+mdV10oqU5zlhYMLJ9DM5maONOe99aCTbOdD07Xu/ry8s/4E4OebEDuo1m46Objlcuvuh5l59iB6s4oS/vrUln2+3ufNm6oecZolKh+NxrxvE9SPh9NX9nTv/1dtvv/cPvvnm7Xtv3znQ/VFmQBCtix52UoaSTuhJTV6qiXt44qXiWU4Gz0V1y4cmwqXApNA03XxiLXMp9ChSGDpaad496OuvfvNd85033/ljsul/2awHr1Og7ljGnh/ngywdJJobGY/btl1r25zJCXnPznsvvvDm0CzleJbwTnPGjpxQe/csZK334r31ntlaZq9CLzr2tcUVhxpsyv0s1TiuRXHv4aOdP3jv9sPfPx57o5Ty0xdjftHkWS4Dz3mzpieD8v55LtztZRLeYImj8AxEFp21im2Wi5OR47TvXXoQx+qRIfmTG+/e/Puvbd28td9NAgiVHsnSKKA51WzMXN7TMOLEGJjzHgoVCwVPntF8iGkSzp14IkWgtOFx7vGNN28Hf/TSN7Z2Hj74L1ux+r1c0KU07Y+9HdmRz1uA3944+NG+cJtXBbjEGxsbvv3qtgP6ScjHfW0H+9bggV+IXql/9cZ/3f7T23+v1rW73GqGPtKqWPoKxarJfPXiib0n8cWv7DyJK7ds5uliyczlMy4XzXKMGDMv3/xCORlbN/FwFSdA8Vqzr0WaQx3I2w+G+J3Xfte8uf9fRE+f/v/m7eg9NaRuCnssLu/Xs3DUOdXJMPw1B7zE2LomONgWrID3fNObrOPyw4NUnmoP1N2D39Nf2XpbgiC0UcAOkPI0PvVa+XlP1mR+yCxMwHwydDp/cuWyFboLjHJb9xgv3XxNPXlm38Wh4igUNgasqAxzzH72JPQ19dxaR+xYxIpzTphGRlBLJP74RcaX24I7Y47GcNa4NBumffPU6jv0jXf/kXt4OHCthvGkxJEqQrhz4+/mwrFuzuvBhYeYPAOOMdWw8xB458lnOUsUxH4xWrH9sVdifR5zniuXusAOU2c7NpauWmt/na6/9d/4V9697QMT5I0aeRCENBh62mh+Fn7GCQ+Fm/w9z31PaTz4aShoPrRchEBdGXoUIrFBwMk4Mckr7wj/3hu/HXXy/zOePv1KPkz2KUl6LlhImrpu8cqyn9YRbV4VbF4VXAPQ3ZW9mFmHmcvDMPcL0RjHyVvSHfY40GpSqjAb19m6PvXYUfk9zOIJ4KOB4DB5TxaiPg2tVaJsNM4cttZ5Gi7a2BYcgI0PvY6jnBynAB1hp3vXk4LTSvxj4TguW+NMQ5PlM2OQsC/CmbjXcWZsX5PIDLQ3uUrZmS4z4qwwrjavCrbWBe0u322AY2esMZIkIz8Oz7Xfopffuib3DxNuNo0jsCs6Bcy1oJwZvv7xtII57zczFe9/GcqdeX4AH4bMkQrsy+/uyo3ON91ywzvryvWiXJ9LL6wnzM2FYv4wM3zuISwOFqDAyAcICNAJn9HWumC4LIE86Ukyr63NZGnxmA76L8uj432JQu1Lpcmi6n62j8973mUicjoXfXHeKXn3UWoc3mRxYzfOx0Yl+d5Qe2yckQ87NPgherAA4Cq99NJ1rK9fQ632WQwf3YWPAlExicoVB0RQJrJis91Bv3/36Hg0OOqOmqOxXSRldBQoDgMtutQ/YS4aRHJ5kp3kMbHMPdeJCnwp50mzRl3T8s4TRkfxIpEiggmUBEZz53hk3nh3R3/7zZudo8POH8aBuhaHtfsq8B1HdMxwY4o46ThlL3jrO8ikwwNqIFTi8rA3GsdLrebTZ1dO/bpnEubZiwKZk3U8WbcFo5WMUqvvPDh4D3n2jVDrRAfco1TG7tEDr8bMPohEWSPsUyHWEsekD/Z7x4njT4dRbbkWaatU0WZu2sphmnz7WOEAJh6Bk54iIhJdrtH73ZG5de9RLR2PXjFEd1nzEbMe2zy1iI0lq3LxDE3UPdw/fKfbH8fjjJ8Kw1DHUeAnhtZ8FUxZtT9X2HCycWdhmKppkV+RszUxDmdeHa1IjFKS5o4e7B3rb71+Q9658d4fwY//YasZvy3Qh1rSzsjQaDzmXNSSqz3zbV75xoFc3dwsSic3N+mH9mBNpzmA6y9h+9o6DjYO5DzOIw1zHvm6pyR3YaBs2goz2jm6t/Be770aRwbLS23bChusy26rrqjqmUiz+onGjgBMSngu8Z+lkJHEnHdh4vWZN7imJz3IXF0HFS2OQyPSrBmuxVrt9EfqT2+9pt589P8xjfq/VE823/NpOhathipXQ2X1uD94lI+fWLbIf8Fja6vo6XWp3Ky+cYZw44CW4ifh4zF06g3aLTE3Hz1E5s66i6tPuIBE5U6IoYrrKqQDhGhm9M9rU01mBp/0zhVGAomPQpF6FMi7O7l88+6/DhZbX8MLa8+71dYZypyfCAnPH15mXsGZ4ekhCg+Pk7Cf/aEsRHdsliVh7LOHDIfkVQF+Uc4FHeQSkgktmBGAdM/ffsRypr3hTy2GbJ2HdzQ9LM2lI+CEoSMnvEYnGy0U3VAQB45BAd86fEcLv0xxjSFIw05qTeod6o6d00JONJbiY3rz/k057Bs5tXjBnV0MmMST9QL2xZrDk8PTxACk6edz2TdkXp4Gc56rqdcdmKqls1LsNElmvc7u7Bv++q2d8Hb3n+vF5j+jxfihH417OaGnjB/X1Dh/4IcOT94XHKxgLlwDbG0DgybBr1DTLCoHMQqIVS9p+jD8lDx55izljifFdPy4msC0QbCaXas2JO/u5cHe4Ku8HL/uHY4NbHKU1Cyeu+nxd/6u4NJLwLXLQLBLiWRUc0pZm4QSxiEdD57zF898WrRmEkbRzxInq9oxM9hnXiIStk7jjYePAtG/45vmtic3CHSe1da07d867bF5tXhlJ59/AIzWdqhhQSwBGDABU8+/c8/xcutFe/ZURM565WV6/5OXg2Xu0DQXdeDy8D7N3WQGMxGzEDPAUSACH/iXbzzitw9/m1bbHXlx9eehoMn6ucjPY15fLno7AgSvSORoqNX+4A211PiadnYAMuke4HBjxePSS+Whc26zuX6JEOxSy0XKh1C5JiNALIMRITAfp2fPPSlpzmWTq2lV/bxXe/beyOQ+RYyCOx4b+uadB6ZZ+z2r+a4WO4oozBI7cjhYZ7z0kvwkDKwPq4pQANDVq5ty5comv74MXBquCmogkb7YvOl4SSUmDm3MDJv1dg/2jl89Ojr69L3d1q+0lxZXT68sY2WpgVpAYgLtRRWnd+8n1egzjSWiWYiw7EAwq5orq2nmq3y0UmJ0US+SW1bH3UQ/eHSkb955eNTv918Jib9eq5kdo4Jdr/ODIOOBcaMEITKHoV3ebfLW6oFsYB13+w+VX255ttaRRSrCOUhEEQlDTRdXwrRaB0rR1JlFJAi1Yq3ALOTZO5uJc/mx+GzR8ThcYDRWePn2PYmfdtDpGkbZSMcL2sR18+Dg0e4/z3L3t8+vnbuwerqBeqSd9ULlaaZUbSCZbzUwEadXZSsbrQqVekXgbi8N3nt4iNt3do6G3c4ftRr6JnMwyBJJWuogIz7tXTdRfJatH5OnHEmtHmW97uE/7vW63Ud7y3917cITrbUzbTQi5bVW7L3QCfWA8oQ2kRmguYilYpmGTj3P+oAqpURpJcKM/igzB0dD3Lr70O4fHN1y6fBrzTr9ro7qDzLrjjWyXtyIR9Z3cu27brnT8dt/d1uuffGL8kPlXX1vXV2BXAUgsn3tC/hMty2HOy0Zb4TsksyRalheCV2iou8E7zw8PP3e0bfy51Z+afxc65Npu9HOsxw8TDw75tLVKsUEUdMFb1rVpcomzMxTeY+T/VsKCQEuK4BIIF6RkDaQKNCIjDKpg3rY66mbR98Mbh59XUK1zc8u70kmHd8f9y2bVOc2E22y3jDP0ao7XNtmfPEq4zLmhPpB2FoXbGz7XQBIDTUTSUj7R/rp098O395Naegv579w/q+40w2teqmnxBXKQdMKQjqp9zWVcqOphEUZ6hcOjEikDY0yUt+8M6A7h/+dfuLMdf+oE6skGyBnEe+ZJjIfE80Wmiu+52nIQUQDMMqRSGJFcmO80z3PCHYFuMTY2Kb3um1Gz7jFc3Ga2bwbNSIjPf+v8Ttf1/RrH/+P/M+dD3hsnEoyUexp0pLwsZy9UhajNLlk0gdSCRklrKGZoN1O18i37x6a+4O3so8vs06yLE697awai1FLMDjgBclgFcP7QMJnzlrs9rv0pW/e0RtP/E33qfOr0m5CjzIvueOi8s+rWenqSWWSWWsfnlWjTguSi4wDpRREQxyzduMk4Icd0I39jnk0+nYQhV/B6tLbNseO9v7QO4xq6jjt5Gdsrzly+HK38BzNG1fFBizYvMSId+moZ9xCmNu8HqSmHh1g9+gdGeUfZ6MYOVOx8U6cIgyU65QoBaV0YTUrAltHuN/tIjYPyecJwWTKDT1Mq/AgXZ6Elq8Krl1m7J7zQcNY2xsn7lR9GNx6dF86/VTOnzLkvLCGEDyJn5T1isxk4or8O3gWCYzw0RA0yL4jy4t3XebTICRrwqZ7sJvORC6n78xVYPMKI498F7CLoSRZTzp5LY609/9SfvfVof5LG5fl5544Y60TPXJeeU88v9gUnob5vsyzKmpFZUeI8jymNIkSTQ/3gT977z4dpf8tnlm5R6P8Y4B3ohAKIOKLNF6Z6tRN9OJoIt4t3mgp1h9JPCR3mXDtPBg3OgIsv3/739wkbAL4LUC3+6K6IWNBmNh7c3Z57O4c3JXO8C9xZEiN3EToj+h96qIn62RJABgjcveQzdjd9u1mh4ZJrrXYo4OxQ2ufcfXayaLzn0EDa7r8X70KXLlyhbc3INjays+3Wt4sLub1g+M0WwjSUNcGQWAWYuC+z9PXR53Rv+4fd5+8v7P/VKNR/9hCo/Ziq1lrn1puoVmvITQEo5UnKrqFTGQcSGHaN/B9i0igZFLFLp4pyazqjVJ12Onj8LCTdfqj3eFw8GfK59drUfAgDsJcGz0guC7GfpDU8iRNOVseGv/pT674AxzIVwHsdlP1YrvH6NXB9ciPBm4caJUvNgM1GEESZ2mSUIy5eLZygKKynJwg3lvkHso61sa6UZq4VNckX1ILftzY4Q0c+K1PrtDy6wny9hE1dD31Y5AyarcW+D/sH+688e4o+ez+UftvrJ5dXltuRqhFgZ9oBHMp2sJlw9giDFuUvhMJkjTXvWFGB0d9/XDvaKdz3Lsu+fjfLDXMTROZnvPUDbJ0PIqCfOgT3+8c0MfyZ/K0vp+JDkcI6Lil6500yXrdw70/7Q1GH3/w6NQvn2ovPHf21GLYrIeIQ8OB0TxV2RcQF67nqVKDUnraXl4TwRjNLEzWeZWkVh8Pxtg/7OLR/tHRcW/4p9l48Epk5F5jQe+R0QecqmMtbgw7TvcHd+yo2XTd1VVe7nTk2rVrJ42rH6WhVdTlE7Ygr25cZuysyvoWuL+R+XE2smHeTBGlx3bJHA7z7G7j1dsvt9+Kn/NPn/ml/OKpT/r26oU0lniEMWU2AWe5h2Mm54uSHpKZ5hUJRCkS9oWbR0202gptK4FiKFKIDZE2hsIQyiuYzigx94/eMncP36Cj4Wte0738XNxXkAGG7tix9KKglgyGziHM3UgPGP1VQb/F+OJvndwo5g3LzSvAxrZH9xzVtE+TJjrSCR2fbY3Nnb2H+sHhVr5x7jfyF9eexkpbU5qKynJP1ouUhmKp24GJwAIUiWgCaVV4J7QyNM6gvnWnr9589BVj5Y/VWvM7Ht6jexy4wCS+EZM4p4hICTOR94VFVUqDzHoglopYRpHWcD5JDhUvJRraHpzRHl03C29sbDN2Vn2vuZsthjXJ+1YoDj2U+qL8/mt7eNT5W/zzz67LmWXFNveUJIzMQ7wrchgUzdwCCoAoQBOgFSAwkjvIbjdTtw5v093jNwz8H+LJU1+j/bEL6otpJ+44bJVhto0VHnR3uY22y73L3GFvpM/GXRr6e/Rnt/9Mb9//q/yJJ3/BPf/kk3JqoSZgVrnzlDvAu6IMq8xrotKzPqmvAQNiIGIUxOjiROZZizChM4Lc2R+q9w7vmMPhNw3wDZxt7fi67svAHYuSIzY0HGGYjjq5BVqM/7TF2Fmdea4IJxWkAaD9eUb7mo/3VzJmHqEW7eHB/h/LOP0r7slTS5TkhZbxfO5oudsrrcBKg1hIEYRv7gTqoPMtOb3wuvd+EMJkvXqU44Vlj0tXTzrLcRnobMlRe8gLoUlZyYCP03fkYLDPn7j4pCjKAVGFC8hDZK7tsDBREUwBvBApGLzZzVRv/FV/urVDaZoYUvlRoh3eyRiddcEXPuCdwRWP39pFL0+wuFjr5/2cpRaOEJh9/Yevvok7j/4D/8mnftleOBOSiOgs85Q7EceQOfHIon2PAgkV+X+kBGEINDRBaUODMejbt/bVGw+/pgP9e/7Z07dokGhJ/Ir3MFILIQ1V9sIt3EcsTFBFF1lRBGhTSHhoaNJKwfGBEp+w5J52ZBJ6JWxeOWFFY3ubcA3AZ9rArfNAex+UBYIsE9RjRQ87IxyOWC6eIkEmwoVQ3HybnJN5uAKgTPEdplpu7D5CoL9mhfdCiscD7XJ8rObw5RNZRR86P6HIZHFi3byySQDU7toaNYZD0zJnAuQSI2yEcRCFNkAU6sgIWyNwsfW8LFRbZ5hPBnG4Wq/VzsZRuFqr15YWGg1dr0WI4xBGa2iNqWimANPy9Ull0TjJMBwmPBynvj9MxsPBcCdNky1w/pbWcrMRh/dJ6SFyn1uocQ12nDRqKTjJlzvIOp1X/CbAm/OVqBsb9JXdpnliNYjJSSMfSnTmXOvTF5568v+ysz9qZ6llrUkpVcSUhUUcg71nKivCBCKOAJdbF3WPj/+7swv4++KRUa1x2B9lY1yEw/XrpZv5klp+PdF5W5uGtqGnpVpmk6bWpp6nY5NxdDFYaP3mQrPxuaXFxulmPa4tNOsUhQZ6JqEH5xi59egNE3R7I9ftDZL+cPQgS0dfI2v/tBGr2yZQAyIaG8/DSOtxSivpU7GxW1sHDABrawu0075F6MampcJYh7rpa8GipH7BOhenmT2twvrPNxYWfyWsBU836rV2q1lfaC8uUC0KoJSCUsUeRFT0ufJSOCGsZzjrMRxnOO4N0RuM7GA06iej4R2X5VuK3StBxG+pMEwC6JSFRml2PE7GSBSnWfOpmxZbRWRkGhL8cPy2s/eraBel1gE1ugg1HsFEEcJxYGu6HtRs6humbxdbLlhbOHP25/TZUy/6lcb5/hKdGQd8KiVXYy1w3hOXdbUsTMyeuOgBWQR5FRGUVqU3QhSLGE+sc2R0nPT1/mhX7wze0fu91/w4eTM/VztyzZqDy1mlnAaRG7vcj5qhTXYBi3a3OPVPc8weM6w+6J43Qdi4TMC6xt2bwWLjfJhTVjf1VgPDwWl9f/h8vhh/GutP/DJfWHxWTtXb1AiVQM3lCXJxEvAsIkyKGRhlkL2e0MPuI/Xg+BXTy75qzrZfQzvouCzLYSIle8fKv3Dhf+qfWflfYZxEYDYAC7jQ7xD2IEVSigYzW6aieJCC4M0Hf2Ac/nM0w4MojdOjOM2x+sjh8jWePsPtDcLnv6wwbJhFxZGtU037oOXILeLm/hNYqv2qPH/+kjx76kWs1BcQmNKamvV4FBbAOdA4hfRGgu7Yyd7gEIf9b+uD5BVt1LtmrfnIxbrjM9cLo2jQ51aKNhywUYZmAWxCYbmjkYem3RoYm0ahE6qZhaBJw3yJHg0u+ij8OV5d+qy/sLCBlcYSNWsh1QLAGEAVodlJwqOwB7FACUG8B1sHGSaQfmJxMOijmzzUh8Mb5jh7WUXxNq2GBz6SRGVk8+Ew17qRBsEo7R1HOS6t5rgOBq4WXqrvNmcm70j5fmC5o9tJFidNs0R3Oyv+F5//38vTy39LhqmB90bYlxHxUpLWKFbGSKG26RRIDL+zc6jfPrzKz599xSRZN7EyRLKaYGPbT5/lY+8lWv0Ai8Y0cq7J3tH5vFX/3+EXn/73FUiLFmFDDEUgpYskXueLJqNUtkUvuotH+Oq7f6ofpf8FX1jYDRD1R83hGPcvZNPP/qBxEFBxHdsGrfN62bggy23smlGdI7UQ3+5e5FR+xT135nN4/uzHsdZapqWaFjJlJK18V2Sm08VEEM+gUQrsHQOPBnfVzvF1Pche1hdOv82hG+DYArEmeXD0lP2V564i1B+D974IYUCgNYREyvx+IseTPHsPo7R6+/BhuNP7P7kX268EYzWqLYTJQbNjUVv2RU7mPgFlAdHBpHJ0XZ9Nh0GqgzC3qk7Kn5be8KmAzGX/t37lf+5hHY1TIi4SE2fip3yikK3MRROJDPN7Dwz+4M1/YVZP/RPP7q2aind76TjBxtjiC9cE79cP/1k3sKaSe3TlyiZtb2zQ+tYKdZZf12HeNsYnhhqxWYiXDGsOTdQMaCyRMkEkxi+A3SJbH1nvI8d6hUmdJ1KndBC1Am3qQlQnoCZATRkdAqyEFZxzTkCZeD/wbDvO5j3xvu9JOjWjHxoV7IUaI1Y6EdDIumxonU0C1cxqOLC6tujCrnerq0N3+fJl/iA5irW139A72AniZjsKg6zePbZL4/7454n0s0ojDrTShlRRycieWeAcCztmiC8q5IUoDQPq1MPoRmzCjjXjodV6gGiYblyDv/zF8rM3N2kTl9TycqI7TzuNmz5cfuJUkCRZLVK1GsjW8jQ5lZF+lqGeIh2shTo4L4rOKKVrIIKwsHfeefZJnmf77Ny+sL0HkTv12OxpFY+8tkNjpZdROm5JnEZZlB804Hbzd/3qzo7f3NwUbG7StY0N2tqCbjbTYLgUR0FQqwUc1Y3RdS9cB9CwuT9jWVYEcop0tGqi6LzWasXooGUCExEpQ4oMhEWYnWe2eZqnWZ6P0swe2yzdF/E7pLEXEA6jgHrKUM959MW5oVWUxN6maAQp8v18DbBfbrd5fWtLPlTj6rstohvbhG5bPZdGKl29qOLdO8F4qRGoWjNOwE3n8rof+qYZJsvG69NN0zgdxgtPUCs8h6XaIi01GrpRa0hs6on2xioKiHSgmcjlY08gp9mknCZj6o/HdDzs8yjblePkIfeHD1Kf7nHTHGRNM6I4cCrNcuV9nkNZnbtcs7L1+lK+uwOHjW0/Tcr9QSpwJpvmtcsKWNfo7po2EOR+IdKaFpySJTXI6mZ/3BZlnuEzzY/J2YWLaDVWuBY2JdCh12Ikt4pGKcsgzVUvGdBO7xDHwxua6E05tXhDztS7ytvEQI0zURZCimMV68NBW/aHz7KVFcW+BkEoRAaKDAwpGECJ8uzYF/Fql+vA9NXpxlu+0XgnEur3jpMc6Dpg3Z0Ia02e4da6xhrM4vg4dCSR9nrBLdSaOkliPNg/443+hLRrH0MrWuUgXKRALxCo7oQJqXWU5GMZJsc4TvaQ+AeG1E3Tjm/LSvPYa5UqcWPv3CD0jWGjOUp3d7oOG5dL4+qqzF1HaSBs65VoReeqFeR+UPNA08TxQpC6Bez0lnmUP+Vj9RSWmqtYaZ7iU42WtGoLolSdCEaEA2ROIbOOUk5pkAy5Nz6kXnIkw2TPOHlIUXQXK81dXqr1Kctyl0uiAzVWuc2NIxsukD24eeDxsbbDzqr/geaNCOHaFxS2oNGMgmYe1G0zaNHOYFWGbl0cVgQ+KvxqhaXKWguU8qTgQGA4DxDnOo7u0NnFm56pE8fxcY9Vhn7LAuD3hSgBYPMva+CSAnZNew1BYs0SDoZPydH4E+xkTRkdwZApyjtJyv6frKAYrDwUO1I0ltAdUNR4O1hs7DmPYS1Ww06nn+PSRYfr+O4q4vPvS7etMGyYZeOCPA5Cl7i6D7CkvVo0j7pnuJ8+I+368zjfvkDnlldQj5d8w9Q4CkLSWiG3jEFiZZCkstfr0+2DfTke3dFh8E08sXxDn633VZInVnQKo0Uki4xXDb5/+KIM7CeEuQmlA1WceMUrsqW3lYg9k7AFOCETDMNG454719iSGg4DiyQgY4983SMEA3eAzphw4Twwagn6HcHzAA5Ds+ibgYvyyDlVV6GcVTf3nqPPvPC/dr/03CXsdy246CdatGAXyGPNnadVwYqYiZRcf+NYv733/6AnT7+MUfZuNtZHuDPIsL7ucPXqT8x79RM2sCZJSAUTQ6v95a5aXW1Tv3Vfr62sqMOd2Cw2jMm06BaFga/pmFnqpINIkdEE0TbLQybRzlrFTMTkSRgapGJA1xV8BEVGRESRzkVhZJQaEcGpUhtQlMkASY2izKYud4Qsy9LE11Re98ojPOLd/Od8t31rulnLY4Hdzc1N2tjYoC1AD9PF4PTQhi42TWOw7Lxui5M6hBWMYiVKFDE57yCsRRMzETGImaGtgRpYuLFnGYGG434fSau/ZademPJz5z8ToxXV1Lk2PgycDkOotBawWkjBLYGLXWZDdj7KmWMvOlRQGuJDKDJGKa+MyQMgI6X7AhkobYac5aMc+XgBC2PXPbBJO7bLqfFxfJ/b7TY/bmhOjMy3h7dMy2TBQnw+TKNhpD1FLEFda2qK1nXtOXTehc67epq7JrzELBx6UABIQKQ0kZAilQOcGKNGWuskIEo8VOrF51CUW+FUAQPJ7TADZ4kktj62+f5Y22g3993PfwSMq+9maG2t0/oGVB99Pc5cEI2DMCMT67gWw6NpOV/MfbYgOcfKcUwpN0LoWqBNpEmHYFaKAU1aEyhw4iGCAbMfeOesVexFqSSrSY8DNVANM1IwqXI2MxKkGTKrUu80K9sLrEMWeRjHiD/BaH+58Fxdvcp/ofvc3iZ8vq2ws6pP1cdBfk6HdpjXgwA1D1VnhZo6Tlqqm7Y4zVrEvgYvgRfRHBpSpIVIjV2NukEjGvjFekKERNu056wfaallisdWUYNZRspHQchCkcRhIDABwIGQCg27UHJvIN6USq0CxYJAmCSwSpmRzt2Qbd4bYThC2HbolOG49+UNzQxlDLtm2ZwL8jgIfWpjE1KNlY6VR4MHWcN2RrFO8lisbYjIgmhliOAR64yjIKWaGVJc66s4GDqXJzUyo9y6XDHZIAjSHqsMzZGbehK/27V8ua0QRQqfyPRyGpncZqGDrplQNzSZOocqZs+xjNO69G3NDcehshLB+kiYAxBCGKWhtSNNGRs1pig4pno4RmgshSpXQkOd2nGuXE6pczqOMzNM8l7TWwy1B8YMrPN0zH5Qo3zzCuESFK7fMUjHUSOOIre40ORANcAcGtJamEPRZMBc+N5IM3LvnZAPAmHynDtFKaVJFqTRcNhqjNAcfbDBN/UwXyFsQgHbCmtt09rxcb4QNnw9WNTWtkSoJpZDaFKAhy/68rDxYEfKkUhG0CPVlD6NYVXqXdCoJb2lpRR34LAJBr6P8ZiMwdquRphp7OugtbwYuMEo8i6q6wgt1tTQw6zG3XFdRtkisbSgEHgCsQIgWsBiSdMYhsbcrg3MqYWRUjJSIzf2IYZqjEyp2GFhCJ8HIedS50A1JIpiiIt0nocQMsKKSJE4XXjDjdEMJY5IO1JBosmMbc5jI25sFgOrhkW0lMOUENZIxlmRDWaYVR4LhzGxyrRXKvTGR8ZjQbxe453Dz+i//at/x9fNGvojN00QKCu0Hi+CKlMghJVi6Y9D+VevvhKo4O9TEzeVpnfHetxDf98Cl/jfcgPrA04xm5t0BcD2xgatr6zQ2rsLtBO+oYeHWituqeWFkeGobcQlxlBNJz4zkakZR6K9JgoBeKVIO0UIvFIw2mbOiPYqQACnmOHFQYuDgzNC3kvmHQIbkrUU1l3WGXoE1vUz+FYfHljh7Y0D+Z4bdZkxevkLX1Dr6/8JdZYTHbYPTdMvBKFbiHIeRwYucFaTCQJxngURgMQrADBBIEAKkOY0t2zEWIQqT5HafIh81Oy61Z0dPzGu5rkyMbJWVggHB6p1H7ofQaMRmJBPBTocRnaMSIUq0BRoj0wHUlNOkgASK0imAMCSYq2QsefEe5vAqjyS3Oqayk0rd3d2d3n5k/8Hj+vXAVznE9cyGRMRunztC2p9a52ADYWLKwqjA7PUCIzPtUkRBKJ9KFbCWlgLEva1MKbAWx8oMjr3Viui4miiRMBw0JSTh+OAPJLceSWZyyQzsbPII5dJbiMe2WH+yB8OT/toN/fvrO7IpVlIED/Jl+x7hkXmPVrOqCzKdN4ITJyYMAvjmkYeM0zA4IC1jkQkgDglORMMQLkShxSiQgqUEqfEkZYcrCQULRSQdRly7ZNcE2WZKKsTdsY418lDD3WPYc4VJeTtbiGC+MUvllJqP4IxuwKFjcuElXXCdSis7ZrlNDKubozLdMgYR6yC2DgfS2gCkMwqm70iEJi8zl00yuIstllivVI2N5lJBw1lkYceihn9jqA1JvShgbpZiJqa69rIMFVSIyVWtAipovMlAKTFL04L1QKnmKzRWd7vRzk6tyx+87N+6nV4PCz6mJGM5Y5GJ9NYjkzLGeO1DzwQBZLXmBDkymiQaAEUhFQAwIn3cFxszglnKuRMu4bV9b7t5dpD5VwYLRcdNrZlzpN4ci5PQrK4gqnRl0YK7pFaNueC3Aah91koSgJmE4axBBlJMc6sSZQnCCkYUnCiIV6gyVNOFpCMmCxZdhTA6izPjQrzXt14dB4xWvDor5c5YeU1Fom2/EPtRVdA2P5iEYLdyTSaXi86Dh3XQjaiy/4vSixrGE8wWpACBGakzFRjJkteIXfD2oJF0M3x6XX7Pb1HJ0J0pZHVOq/R3w8aWIi55kK2CKBFi/NERgu8FSCe1O8ywTmVWKebKh+omkNiPFoHHs12YRhPwpLfz/s0nVsbBGxp3O9rRJnGQIK6pshTXhM2oUQIAq1DETK2yFtk+JygIoL2QmQcNDvKyFKaW8Wc6VotC+Is7/YXHIYDwRqAY6exEJlGnofehaGINRClREhBe4I3BO0ERgsZLVAhg4SVjhzl7PSCscqA0QOwCPh0ZLw2IUsYRHFTS9JXSrRPlXCMGGmYqYAp9pIvEEWn/a1HT8WfeO5v4dde/Hx22BFiT5PKYSp1eoTmWyVNqzZFQgN6/Z6V69t/zzxz5g/Zpjs2Dh6g3x9iGx7XrvH3se7/WJPfP1oG1uMTTRV13JevXVPrWyu0u/YubaQX1B03VGucqv12SymfK/JWAYuIalbiJKCezxVxrCZtr/IgKe5TB5zFTqK8LugDcc0J6YiTuhX0howgZuwcMTYavNvtyufbbd66fFmuTvJtv9eDkFl74y9c+4Ja3/pPCBsHCqMV1e8ckOJUNWsxjUd9On3qNMajQTn2p6Y/Ypzmcma1xsMkk1Y7FbPb5Imn6AtbW4Krmx9YmDT57Imh9eVuV62224TRimo1IurvP9D1OCQ5zFVyNqA4DCgdsg7VbDMbAgiZPIYpwzQdYucQHjEaDcbKCm8fHMj65cuCzU1sbm6e7Iw8b3BOSpM2N+kKNrG9fY3W11eKsdhaUVgbqGYtov3dRNVD0ZERHSw2dJawBjtltSbUACRA4L3YmDySjKEDznIn0CH7gXP1JeX7w1ScafJyZyi7a5PntSVXsfnjSV7/cRlZmwCubRC2tghruwR8BtjZ0WefHWrbzTWWARnXyatc11WsmUcKaGCYWQKABgBygYwJvBBnMsxDoTwTUsxaMWvt+SBwjGFDYByjsyxY2xXsrBaq2pfXy5yeH0MjVCo1AaYGyT4BZxRQV1i7qHB8Xy+GXsuxUXI6Jinvafrf80AGxAzVdBiPBHCMVlemRuHkHiabO6CwtkuFkWEU+qPi560B6C8QhvcIzWh2j8O6ABkDF3k6JhNVhe+VczbblIGNDUL3ywo7qwTcUViLFPoLtBzm2rNSnDgtMi4iW2FEyC0hD2QE61Bfthj3PJAxllsC3ADiJ0/e19WrMt+H97uO8aRaazLOG2cURusKd6BWVg7UwYFW7VpGPvGaw7rmwGsZpQp1AKKU2IAosEJOebLKj2KyqBt/wojFmHFpnXGwIbh2DVhfl0JQ8kfiKaAynFHcw8o64fp2MVdaywQ8APptQnNMGLbK6gAA45HgTM6zZ1l60k4apn/+Rnul9LiurxOwrfDZc4S3jAIrhVpCGKWERizAAXAAACtAsy8Y1uW7zsmrP5SCeJG4fu0LCivrhHfL+dzJNOpOI1zQC0lHT6OlUEpEKwkcwQaF1yhM/dgqDxW7E8/wQstjBYzr5Set7RLaq4T7fQ0+UOBVhTgtnkOSEWrlu9KIBUfldpXWBa2hIGkKakM6lSvFXikLGziHmg5V2x71nsg9t+OVZTJKpaz0kIoOH1pBGqLwhNvvPh1z8Inm5V//5WOT1lwvBYkr+0rPlTOIzEKEZX4mQCwsgfzRG183j3r/Vz67eNvk+V4aqmPcNClWV/0P7X3/t8LA+i4GROHZ2qK1d9dop92m5d0m3XFD1VpekX7ngACg1YypWYsIewCwh0flj2m2lqQ1TOUBgNbyiuDOHXSWz8on4jP8Kl7F6s4LUniqLstVbAITD833u0kTyoaHc9daGoYA8Bl8BgCw075Vjvs6Orv3ps9gYiis7rxQft51fN+J2Y+NEQBMPnt1p03YAJZ3m9Rp7VG/05iO07B/TPNjc8c0+RPxGd55YSAbBwdSrKFznru5e/xzx2bO6NwECJvX1ePX49xQ7UyNz5Dm7E3gCKg3ckEQc2t4Wh7gZbT6ywI0GJdWGNevY3tjYypk+5EJA/7FEuExrcTpfl5hZ4ewUfzVUyOoPIfi/rDQxKklpJKaAHtQrbrodkMe3Aew3BLcAbDckakx1f58sdhcLhOk8WMyqL6f+7x2WWFrnXAJOLmBfgD9jmDjIqO7JsCrKDavDQGu4YRXhzCTs58YrZulsfU4O13CWnt23+830vADhBaobGY782it7RJ2VgnLHUKnT4VnrU2FkTcuu1WvMJY7gjsTg6VMAr4GTOUMTsrV/YBGCgjblwnr+4TdFwmr5fUAQKdPQKSArkJ9ofi7cUbAMlAfCVSboZiLcHHG0zEq5uW84SA/1r1pavDsF9e4++JsRD4DFAYtAGwDu+XzXH1H5kJDP+j4zQy87W3CZWB2IJjj8bkzWau3z8x63v1wn/+9x+DSpeJ9WT5HcEaBDxRwFlPjb0IjFiQ1mRrFk/dnZ0ewvS344lzRxmStmc7bLmFjHdgt58rq8pxm2WRtmrvCERRGB2bRBsYuBbFWpjW+v/v8pd/8D//T0d4jev3PvvaOZdfVYdTTSjtRpMjaJqfJs/Gp5fX2/+hXnj1ekHjU7RG8KGE319y+bKY9kWqYeLGEIVoz7h4a/NHW36UzrX+qvBzmmg+RH4xwfZDjpet+vkV2ZWD9IHlbUhgSpacE2NxUwCW8z5DZuUVra7N+cxMjqjh0XpJruIbp5lz8rLkO4EI/5GZdFufNDB5gE9sb12h967IAm9je3ihe3g9g4iUCgKs/qIEnJx1LkzHa3tggXAMul5+5tbVCAKbjBQCrL7wgGwfFmHzPa/jhxwVXrlwpF6piPCZG4LQCcfISz7/AW1vApUuM69exsbEh1ybjtLUl0+ujyav4U8zEC0GzDkrTTRsALq8LrpWektniTljbLe77hVXBu7uEF8oF//rcZjgxpk6aBD/Zu51kERZzVE1P0/O8sCq4fh3YODPzss3z/k2eHptwVE5iwZXN2b9tb89+/8Eb4g+zMNOJz5x2ybgO4BKwu3vSOACAnXdkuikX+jXFGjTzlv9FwxdUbtI4YTBsnBFsrRN2dwmr3cKA31oHtreL/9VuC1ZXi814Nj6Pj4l8OG+F4MSz+6BnOP8cTz5L+aHW7olh8/18Fj5wTv6ox2d2PRNja/fF4tl1zhGem/vO1WXB1nZhBLY/z4XFfhko18sPOFQVP3sTwBe2v+u+hMtz9zzvbQ9XNfRQtzKOclILvjdcihaWfuP0Fz7/n52OY6GdzlH39oOHowePHrlhMgYIarFxSq2desF8/PzZgXZq3OkLQbR4TygNqJnnSj5AAwsCgcJX3h7IW7v/uXq2/XWd+/0MWRc7nTGWP2t/TM/h3wYD64PeQaZ5g+IEhQF28h3Y/LBCSPLY9cwZKX/ubf2Q1/WBP3u2Tl/Z3KRJiE8AUJnz9j5j6vGfMzVihP5Cxsx3MQInz2r72jWaGnjl1/tDkvPX8jNgXP0gY/dBc/y78f3IKnx0zEuc2NRwwsv23f1if/7aJt/nOic/0nvBFcKVD/jXyWb93Q27H/3m/N3G4MSm/QFGw084QfgD9qgP81l+8Hh9d88U/Rg39O/+7HBdYePMSe/n+429eUNdvqd3+Xsdiea/7wuXFT7fVghXNfbvB83TZ+M81Mv82rtn6//jv/R/dJ+58BsyGiTNuB5EYQRlwdp6LxSwDU2QcWZGgy67JC/CgcwE9pM2FGWulcx6Ck/0RkUgRnnVGYXye995FYquqoa6a8UfoDcYoncuxW/9lvuQDwI/ywbW9zBoZN5Y+QsaBhU/emPhu87I6nn9wO+w/FQYUt+PMfTj3qw+CmusfITukX5GxvnHdS8f9blI32VuyY9tXOarHYcNg+YorOnFhjN6Nbi1+2n8x3/tP/ML5oIaDW3Z245AiogUKVLw7EWKBrdzvVWZ5jpWTxPaad6BJWVzBCUKf3YjwWsP/it6YvFfuSQ9gAl6wJ0U1+Hw0kv+o/K8DH6meGxDpu/xbxU/4Uf153mgquf1fS93P/1Hpb9oeO6n6d4+avf4b8NY/yyPj/zE36dhQ/OSjXn3YJEvrn6KF+prSPree1HCQkUjRzddqEolfCpEd3maxD7Nt5pecZnozlM/Fshowb095d+498eq3XxZpRjA6yEOjnLgRY9Lq4yXXvrIPJyfMQOr4qfaIK6oqKio+OizsU3YAmHFaCg2LI267o6W/S+f/wRrMpRZJ8IEllmMmonKzlQgmTjei764NNfYeVo1KCeTr4RIKM00f/t2hzL+KkVqmHM2QjbIETmPR13Gb/3WRyGsPUVVM6WioqKioqLi+2JSfbh8juC1iscUySBpIG4+5dfaz3EyYvEMcR5StqgTLrxV4j3EM5gZ4hlwIHgU3qxJ258yRIi5L2KBKCX8zgMtNx/9CZ1deo+sHyJTCepti9eT71f3qjKwKioqKioqKj5yFMbVl9sK7pGCGxs2QSwHndO4uPbz1IpOI838iYpA8YWHSqQwqphBzACcFF8sE+OKJo2r5ysJHYM1MR4dhvL1m29Qs/nbFPhdq6mLKM2wG3lcusQfxcGqQoQVFRUVFR+VzbtKG/ioU0jHEPrQjWZgcvILlLin8PTZT0GJgfVlFZ+UBhMgIgShMilEUFpcU92FiRxDmfZeeK0wrRpk6o8C/MnbO3yc/T35+NkbPLL7GKdDXBxYYN1/RCpe30flwaqoqKio+Cjw/cowVPwkuQYAdQWEijk3MhrF1GpelPNnzkueO4gUdlXRlbJ4qlwkq6NMbC++ytDh1NCaq3sSFGFErViNEq3+5O1cHoz+MZ45+zrG4z7S0QjqOEMn8UWHk4+mYV4ZWBUVFRUVHyUjq+Kjzp07QJoT4oaSBwch1p9+1jejGoZjhnBhXQkKg6nQahAQy8SYEp7LsSorCSd5WsRlqNBoxnE/wJffZLl5+C/UE/VXApsNIRjCcY7DNY/tM/JRtsmrEGFFRUVFRUXF90+rJohDgTNC4zTDSntZaibEOEiQWSKIEgiVuoZlTpWckKqcuisFcwYZIEYJiECPjgL603eOcHfwz+R86yuK1CObJV2wHiHxOaI3Pf7Rqx+5xPZ5dDVTKioqKn6qIVShtYoPa65dvgwca8LCIcUDrWUpcO7OXo/S7BksNc7hdCtAoAUCJvZSJLUXojwkHtO4IQPEZcYVEaC1iCLQODfqtduEr7z7Mg5G/3e6sPSStu6h8/k+xlkfJkhh7zus/k1fal59ZOd+ZWBVVFRU/PQbWBUVHw4vvQQ88wywEiDPBvDNGoVDu6O/9s435ebOPiU5KFKLqIUN1GJNxhA0iRAEpIoYYWFQAUoRlAIcKzroGXVjB3j11g16/eE/IRX+A31h+T2VZsc2dx0IDzDop3hr5PBz/y5/VPoNVi9mRUVFRUVFxY/GbrhyhYDrCrhosH9Yqy0sNfxyvMCddJEedJ5CPVyXlYVP44n287K2uMKLC02KwwAAEXtI7oHhGBhkHoMkw0FvQAf9R+ilW6Lk67SycENps2fHaR9aj5GOx4jrKUa3Cs/VR7RqsDKwKioqKioqKv4CdoMAVzYJu/9KA6sBlltRzQ9rXteaHEVLkrkWOqOm6ifL3rtzVI9XUQ/aok0MQJFzFqlN4cVCOAOZDtXCR9IIHiGkI+Vdx+bqGIIEGXI0kONjtxy+/AyXgqKoDKyKioqKioqKn0EjC8DlywrttkK3a3AxMxirEAGFkTZx1qxHgVaRKAqQ2QBjHwg7DQdQCO9EswlIORaCIA8gI+v9GEAChxwIM2Q9h8h5LCdFxeAPZlz9xHXVKgOroqKi4sNfdys5goqfAa4oXEEhPnoNGut1hZteoy0aDdHIRcOJhofGoie4kOAcwRhBMgaMEWh4JNaizjk4YQwiQTZgtGqC7IDnQoL4aXtvKgOroqKiojKyKip++Ll8pexPeP26wosvEqJ7CstnCbtHCgtLBNVXSBceszeOil9GocCwxz3n8eSTDLwKrDYF22cE6+vy05JvVRlYFRUVP+m1Rr7HGlQZHBWVsfzT/o5fuULY3i5+v79PeHFI6D5DODeYvev9hNCqzcYsO2B0n+HSoJpfC+Sn+ZlVBlZFRcVPysB6fP2pNqmKysD62XrfAVwhXAGmRtcH8VPuqaoMrIqKio+y0VVtUhUVP/PG1gdSvfcVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFR8RKhmWioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKnzkIVQPfioqKioqKioofuYFVUVFRUVFRUVFRUVFRUVFRUVFR8cNSedgqKioqKioqKioqKioqKioqKip+ktBjv1ZUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFT8hakKlioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKip+5vn/A93fe22ajW+2AAAAAElFTkSuQmCC" alt="Site Gateway"></div><h1>${title}</h1><p>${message}</p><div class="foot">Host. Proxy. Secure.</div></main></body></html>`;
|
||
await fsp.writeFile(path.join(defaultSiteDir, "index.html"), html);
|
||
}
|
||
|
||
function renderCaddyfile() {
|
||
const email = String(process.env.ACME_EMAIL || "").trim();
|
||
const lines = ["{", " admin localhost:2019", " persist_config off", ` storage file_system ${managedCertificatesDir}`];
|
||
if (email) lines.push(` email ${email}`);
|
||
const logging = [" log {", ` output file ${accessLogPath} {`, " roll_size 10mb", " roll_keep 5", " roll_keep_for 168h", " roll_uncompressed", " }", " format json", " }"];
|
||
lines.push("}", "", ":80 {", ...logging);
|
||
const defaultSite = settings.defaultSite || {};
|
||
if (defaultSite.mode === "abort") lines.push(" abort");
|
||
else if (defaultSite.mode === "redirect" && defaultSite.redirectUrl) lines.push(` redir ${caddyQuote(`${defaultSite.redirectUrl}${defaultSite.preservePath ? "{uri}" : ""}`)} ${[301, 302, 307, 308].includes(Number(defaultSite.redirectCode)) ? Number(defaultSite.redirectCode) : 302}`);
|
||
else lines.push(` root * ${defaultSiteDir}`, " rewrite * /index.html", ` file_server {`, ` status ${defaultSite.mode === "welcome" ? 200 : 404}`, " }");
|
||
lines.push("}");
|
||
for (const site of sites.filter(item => item.enabled && normalizeDomains(item.domain, item.domains).length)) {
|
||
lines.push("", `${caddySiteAddress(site)} {`, ...logging, ...commonHostDirectives(site), ` root * ${path.join(sitesDir, site.id)}`, " file_server");
|
||
lines.push("}");
|
||
}
|
||
for (const proxy of proxies.filter(item => item.enabled && item.domain)) {
|
||
lines.push("", `${caddySiteAddress(proxy)} {`, ...logging, ...commonHostDirectives(proxy));
|
||
for (const location of proxy.locations || []) {
|
||
lines.push(` ${location.stripPrefix ? "handle_path" : "handle"} ${location.path} {`, ...proxyBlock(location.target, location, " "), " }");
|
||
}
|
||
if ((proxy.locations || []).length) lines.push(" handle {", ...proxyBlock(proxy.target, proxy, " "), " }");
|
||
else lines.push(...proxyBlock(proxy.target, proxy));
|
||
if (proxy.customConfig) lines.push(" # Administrator-provided custom configuration", ...String(proxy.customConfig).split("\n").map(line => ` ${line}`));
|
||
lines.push("}");
|
||
}
|
||
for (const redirect of redirects.filter(item => item.enabled && item.domain)) {
|
||
const target = `${redirect.target}${redirect.preservePath ? "{uri}" : ""}`;
|
||
lines.push("", `${caddySiteAddress(redirect)} {`, ...logging, ...commonHostDirectives(redirect), ` redir ${caddyQuote(target)} ${redirect.code || 302}`, "}");
|
||
}
|
||
return `${lines.join("\n")}\n`;
|
||
}
|
||
|
||
async function syncCaddy() {
|
||
const nextPath = `${caddyfilePath}.next`;
|
||
const previous = await fsp.readFile(caddyfilePath, "utf8").catch(() => null);
|
||
const previousDefaultPage = await fsp.readFile(path.join(defaultSiteDir, "index.html")).catch(() => null);
|
||
await writeDefaultSitePage();
|
||
await fsp.writeFile(nextPath, renderCaddyfile());
|
||
try {
|
||
await execFileAsync("caddy", ["fmt", "--overwrite", nextPath]);
|
||
await execFileAsync("caddy", ["validate", "--config", nextPath, "--adapter", "caddyfile"]);
|
||
await fsp.rename(nextPath, caddyfilePath);
|
||
await execFileAsync("caddy", ["reload", "--config", caddyfilePath, "--adapter", "caddyfile"]);
|
||
gatewayError = null;
|
||
lastGatewayReload = new Date().toISOString();
|
||
} catch (error) {
|
||
const rejectedReason = error.stderr || error.message;
|
||
let rollbackSucceeded = false;
|
||
await fsp.rm(nextPath, { force: true });
|
||
if (previous !== null) {
|
||
await fsp.writeFile(caddyfilePath, previous);
|
||
rollbackSucceeded = await execFileAsync("caddy", ["reload", "--config", caddyfilePath, "--adapter", "caddyfile"]).then(() => true).catch(() => false);
|
||
}
|
||
if (previousDefaultPage !== null) await fsp.writeFile(path.join(defaultSiteDir, "index.html"), previousDefaultPage);
|
||
try {
|
||
sites = storage.loadCollection("sites"); proxies = storage.loadCollection("proxies"); redirects = storage.loadCollection("redirects"); streams = storage.loadCollection("streams"); accessLists = storage.loadCollection("access_lists"); settings = storage.loadSettings() || settings;
|
||
} catch { /* Startup may not have completed database initialization yet. */ }
|
||
gatewayError = rollbackSucceeded ? null : rejectedReason;
|
||
const friendly = /upstream address scheme is HTTP but transport is configured for HTTP\+TLS/i.test(rejectedReason) ? "This host forwards to HTTP, but Ignore upstream TLS certificate errors is enabled. Turn that option off or change the upstream to HTTPS." : /upstream address scheme is HTTPS but transport is configured for plain HTTP/i.test(rejectedReason) ? "This host forwards to HTTPS, but its upstream transport is configured for plain HTTP. Use HTTPS transport settings or change the upstream to HTTP." : /duplicate.*address|already.*site address/i.test(rejectedReason) ? "This hostname or address is already used by another host. Choose a unique hostname and port." : /dial tcp|no such host|lookup .* no such host|upstream.*(invalid|malformed)/i.test(rejectedReason) ? "The upstream address could not be reached or is invalid. Check the hostname, IP address, and port." : /invalid hostname|host name.*invalid|malformed.*host/i.test(rejectedReason) ? "The hostname is not valid. Use a valid domain name without a protocol or path." : /unrecognized directive|unknown directive|parsing caddyfile tokens/i.test(rejectedReason) ? "The gateway configuration contains an unsupported or malformed directive. Check the selected host settings." : /certificate|tls.*(config|handshake)|no certificate/i.test(rejectedReason) ? "The TLS certificate configuration is invalid or unavailable. Check the certificate, key, and HTTPS settings." : "The gateway rejected this configuration. Check the host, upstream address, and TLS settings.";
|
||
const detail = `${friendly}${rollbackSucceeded ? " The previous working configuration remains active." : ""}\nDetails: ${rejectedReason}`;
|
||
throw Object.assign(new Error(detail), { status: 400 });
|
||
}
|
||
}
|
||
|
||
function siteStatus(site) {
|
||
if (!site.enabled) return "disabled";
|
||
if (site.domain && gatewayError) return "error";
|
||
return activeServers.has(site.id) ? "running" : "error";
|
||
}
|
||
|
||
function publicSite(site) {
|
||
return { ...site, domains: normalizeDomains(site.domain, site.domains), status: siteStatus(site), url: `http://${site.host || "localhost"}:${site.port}`, upstream: upstreamHealth.get(site.id) || null };
|
||
}
|
||
|
||
function publicProxy(proxy, includeAdvanced = false) {
|
||
const { certificatePath, keyPath, ...safe } = proxy;
|
||
if (!includeAdvanced) { delete safe.customConfig; delete safe.requestHeaders; }
|
||
return { ...safe, domains: normalizeDomains(proxy.domain, proxy.domains), certificatePath: certificatePath ? "installed" : null, hasCustomCertificate: Boolean(certificatePath && keyPath), status: proxy.enabled ? (gatewayError ? "error" : "running") : "disabled", upstream: upstreamHealth.get(proxy.id) || null };
|
||
}
|
||
|
||
function publicStream(stream) {
|
||
return { ...stream, status: stream.enabled === false ? "disabled" : activeStreams.has(stream.id) ? "running" : "error", upstream: upstreamHealth.get(stream.id) || null };
|
||
}
|
||
|
||
async function walkFiles(directory) {
|
||
const output = [];
|
||
for (const entry of await fsp.readdir(directory, { withFileTypes: true }).catch(error => error.code === "ENOENT" ? [] : Promise.reject(error))) {
|
||
const fullPath = path.join(directory, entry.name);
|
||
if (entry.isDirectory()) output.push(...await walkFiles(fullPath));
|
||
else if (entry.isFile()) output.push(fullPath);
|
||
}
|
||
return output;
|
||
}
|
||
|
||
function certificateNames(certificate) {
|
||
const names = [];
|
||
for (const part of String(certificate.subjectAltName || "").split(/,\s*/)) if (part.startsWith("DNS:")) names.push(part.slice(4).toLowerCase());
|
||
return names;
|
||
}
|
||
|
||
async function certificateInventory() {
|
||
const configured = [...sites.map(item => ({ ...item, kind: "Hosted site" })), ...proxies.map(item => ({ ...item, kind: "Proxy host" })), ...redirects.map(item => ({ ...item, kind: "Redirect host" }))]
|
||
.filter(item => item.enabled && item.domain && item.tls !== "http");
|
||
const configuredDomains = configured.flatMap(item => normalizeDomains(item.domain, item.domains).map(domain => ({ ...item, domain })));
|
||
const parsed = [];
|
||
const certificateFiles = [...await walkFiles(certificateDir), ...await walkFiles(customCertificatesDir)];
|
||
for (const filename of certificateFiles.filter(file => /\.(?:crt|pem)$/i.test(file))) {
|
||
try {
|
||
const certificate = new crypto.X509Certificate(await fsp.readFile(filename));
|
||
const stat = await fsp.stat(filename);
|
||
parsed.push({ certificate, names: certificateNames(certificate), updatedAt: stat.mtime.toISOString(), filename, source: filename.startsWith(customCertificatesDir) ? "Custom upload" : "Caddy / ACME" });
|
||
} catch { /* Ignore non-certificate PEM files and unreadable entries. */ }
|
||
}
|
||
const certificates = configuredDomains.map(item => {
|
||
const found = parsed.find(entry => entry.names.some(name => name === item.domain || (name.startsWith("*.") && item.domain.endsWith(name.slice(1)))));
|
||
if (!found) {
|
||
const customForRoute = item.tls === "custom" ? parsed.find(entry => entry.source === "Custom upload" && entry.filename.includes(item.id)) : null;
|
||
return { domain: item.domain, name: item.name, kind: item.kind, status: customForRoute ? "mismatch" : "pending", daysRemaining: null, expiresAt: null, issuer: null, updatedAt: customForRoute?.updatedAt || null, source: item.tls === "internal" ? "Caddy internal CA" : item.tls === "custom" ? "Custom upload" : "Caddy / ACME", mismatch: Boolean(customForRoute), coveredNames: customForRoute?.names || [] };
|
||
}
|
||
const expiresAt = new Date(found.certificate.validTo);
|
||
const daysRemaining = Math.ceil((expiresAt.getTime() - Date.now()) / 86400000);
|
||
const warningDays = settings.certificateHealth?.warningDays || 30, criticalDays = settings.certificateHealth?.criticalDays || 7;
|
||
const status = daysRemaining <= 0 ? "expired" : daysRemaining <= criticalDays ? "critical" : daysRemaining <= warningDays ? "warning" : "healthy";
|
||
return { domain: item.domain, name: item.name, kind: item.kind, status, daysRemaining, validFrom: new Date(found.certificate.validFrom).toISOString(), expiresAt: expiresAt.toISOString(), issuer: found.certificate.issuer, subject: found.certificate.subject, serialNumber: found.certificate.serialNumber, updatedAt: found.updatedAt, fingerprint: found.certificate.fingerprint256, coveredNames: found.names, source: item.tls === "internal" ? "Caddy internal CA" : found.source, mismatch: false };
|
||
});
|
||
for (const certificate of certificates) { const previous = certificateStatusCache.get(certificate.domain); if (previous && previous !== certificate.status) recordActivity(`Certificate status changed for ${certificate.domain}: ${previous} → ${certificate.status}.`, certificate.status === "healthy" ? "ok" : "error"); certificateStatusCache.set(certificate.domain, certificate.status); }
|
||
const latestError = recentActivity.find(item => item.status === "error" && /cert|tls|acme|caddy|gateway/i.test(item.message)) || null;
|
||
return { checkedAt: new Date().toISOString(), thresholds: settings.certificateHealth, latestError, summary: { total: certificates.length, healthy: certificates.filter(item => item.status === "healthy").length, within30Days: certificates.filter(item => item.daysRemaining != null && item.daysRemaining <= 30 && item.daysRemaining > 0).length, within7Days: certificates.filter(item => item.daysRemaining != null && item.daysRemaining <= 7 && item.daysRemaining > 0).length, warning: certificates.filter(item => item.status === "warning").length, critical: certificates.filter(item => item.status === "critical").length, expired: certificates.filter(item => item.status === "expired").length, pending: certificates.filter(item => item.status === "pending").length, mismatch: certificates.filter(item => item.status === "mismatch").length }, certificates };
|
||
}
|
||
|
||
async function pruneOrphanedCertificates(candidateDomains) {
|
||
const domains = [...new Set((candidateDomains || []).filter(Boolean).map(domain => String(domain).toLowerCase()))];
|
||
if (!domains.length) return;
|
||
const stillInUse = new Set([...sites, ...proxies, ...redirects].filter(item => item.enabled).flatMap(item => normalizeDomains(item.domain, item.domains)).map(domain => domain.toLowerCase()));
|
||
const orphaned = domains.filter(domain => !stillInUse.has(domain));
|
||
if (!orphaned.length) return;
|
||
const files = await walkFiles(certificateDir).catch(() => []);
|
||
const removed = new Set();
|
||
for (const file of files) {
|
||
const directory = path.dirname(file);
|
||
if (orphaned.includes(path.basename(directory).toLowerCase()) && !removed.has(directory)) {
|
||
await fsp.rm(directory, { recursive: true, force: true }).catch(() => {});
|
||
removed.add(directory);
|
||
}
|
||
}
|
||
if (removed.size) recordActivity(`Removed stored certificate data for ${orphaned.join(", ")} (no longer in use).`);
|
||
}
|
||
|
||
async function domainReadiness() {
|
||
const routes = [...sites.map(item => ({ ...item, kind: "Hosted site" })), ...proxies.map(item => ({ ...item, kind: "Proxy host" })), ...redirects.map(item => ({ ...item, kind: "Redirect host" }))].filter(item => item.enabled && item.domain).flatMap(item => normalizeDomains(item.domain, item.domains).map(domain => ({ ...item, domain })));
|
||
const certs = await certificateInventory();
|
||
const [httpResponding, httpsResponding] = await Promise.all([tcpProbe(80), tcpProbe(443)]);
|
||
return Promise.all(routes.map(async item => {
|
||
let addresses = [], dnsError = null;
|
||
try { addresses = [...new Set((await dns.lookup(item.domain, { all: true })).map(value => value.address))]; } catch (error) { dnsError = error.code || error.message; }
|
||
const certificate = certs.certificates.find(cert => cert.domain === item.domain) || null;
|
||
const upstream = item.kind === "Proxy host" ? upstreamHealth.get(item.id) || null : null;
|
||
return { id: item.id, domain: item.domain, name: item.name, kind: item.kind, dns: { healthy: addresses.length > 0, addresses, error: dnsError }, ports: { http: httpResponding, https: item.tls === "http" ? null : httpsResponding }, tls: item.tls === "http" ? { status: "not-configured" } : { status: certificate?.status || "pending" }, upstream };
|
||
}));
|
||
}
|
||
|
||
async function checkProxy(proxy) {
|
||
if (!proxy.enabled) { const result = { status: "disabled", checkedAt: new Date().toISOString(), history: [] }; upstreamHealth.set(proxy.id, result); return result; }
|
||
if (proxy.healthEnabled === false) { const result = { status: "unmonitored", checkedAt: null, history: [] }; upstreamHealth.set(proxy.id, result); return result; }
|
||
const started = performance.now();
|
||
const attempts = Math.min(Math.max(Number(proxy.healthRetries) || 0, 0), 3) + 1;
|
||
let result;
|
||
for (let attempt = 0; attempt < attempts; attempt++) try {
|
||
const target = new URL(proxy.healthPath || "/", `${proxy.target}/`).toString();
|
||
const response = await fetch(target, { method: proxy.healthMethod || "GET", redirect: "manual", signal: AbortSignal.timeout((proxy.healthTimeoutSeconds || 4) * 1000), headers: { "user-agent": "Site-Gateway-Health/1.0" } });
|
||
await response.body?.cancel();
|
||
const responseMs = Math.round(performance.now() - started);
|
||
const accepted = expectedStatusMatches(response.status, proxy.healthExpected);
|
||
result = { status: accepted ? "healthy" : "unhealthy", httpStatus: response.status, responseMs, attempts: attempt + 1, checkedAt: new Date().toISOString(), error: accepted ? null : `Expected ${proxy.healthExpected || "200-499"}; received HTTP ${response.status}` };
|
||
if (accepted) break;
|
||
} catch (error) {
|
||
result = { status: "unhealthy", httpStatus: null, responseMs: Math.round(performance.now() - started), attempts: attempt + 1, checkedAt: new Date().toISOString(), error: error.name === "TimeoutError" ? `Timed out after ${proxy.healthTimeoutSeconds || 4} seconds` : error.message };
|
||
}
|
||
const previous = upstreamHealth.get(proxy.id);
|
||
result.history = [{ status: result.status, responseMs: result.responseMs, httpStatus: result.httpStatus, checkedAt: result.checkedAt }, ...(previous?.history || [])].slice(0, 20);
|
||
upstreamHealth.set(proxy.id, result);
|
||
return result;
|
||
}
|
||
|
||
async function checkAllProxies() {
|
||
await Promise.all([...proxies.map(checkProxy), ...sites.map(site => checkProxy({ ...site, target: `http://127.0.0.1:${site.port}`, healthPath: site.healthPath || "/", healthMethod: site.healthMethod || "GET", healthExpected: site.healthExpected || "200-499", healthTimeoutSeconds: site.healthTimeoutSeconds || 4, healthRetries: site.healthRetries || 0, healthEnabled: site.healthEnabled })), ...streams.map(checkStream)]);
|
||
return proxies.map(publicProxy);
|
||
}
|
||
|
||
const SENSITIVE_QUERY_PARAM_PATTERNS = [/token/i, /secret/i, /password/i, /passwd/i, /auth/i, /session/i, /api[-_]?key/i, /credential/i];
|
||
|
||
function redactUri(uri) {
|
||
const str = String(uri || "");
|
||
const queryIndex = str.indexOf("?");
|
||
if (queryIndex === -1) return str;
|
||
const pathPart = str.slice(0, queryIndex);
|
||
let params;
|
||
try { params = new URLSearchParams(str.slice(queryIndex + 1)); } catch { return `${pathPart}?REDACTED`; }
|
||
let redactedAny = false;
|
||
for (const name of [...params.keys()]) {
|
||
if (SENSITIVE_QUERY_PARAM_PATTERNS.some(pattern => pattern.test(name))) { params.set(name, "REDACTED"); redactedAny = true; }
|
||
}
|
||
return redactedAny ? `${pathPart}?${params.toString()}` : str;
|
||
}
|
||
|
||
async function readAccessLogs(limit = 100, host = "") {
|
||
const files = (await fsp.readdir(logsDir).catch(() => [])).filter(name => name === "access.json" || name.startsWith("access.json.")).sort().reverse();
|
||
const entries = [];
|
||
for (const name of files) {
|
||
const content = await fsp.readFile(path.join(logsDir, name), "utf8").catch(() => "");
|
||
for (const line of content.trim().split("\n").reverse()) {
|
||
try {
|
||
const raw = JSON.parse(line); const request = raw.request || {}; const requestHost = String(request.host || "").split(":")[0];
|
||
if (host && requestHost !== host) continue;
|
||
entries.push({ at: raw.ts ? new Date(raw.ts * 1000).toISOString() : null, host: requestHost, method: request.method, uri: redactUri(request.uri), status: raw.status, size: raw.size, durationMs: Number.isFinite(raw.duration) ? Math.round(raw.duration * 1000) : null, remoteIp: request.remote_ip || null });
|
||
if (entries.length >= limit) return entries;
|
||
} catch { /* Skip incomplete lines while Caddy writes. */ }
|
||
}
|
||
}
|
||
return entries;
|
||
}
|
||
|
||
async function importAccessLogsToSqlite() {
|
||
if (!storage?.recordAccessEvents) return;
|
||
try {
|
||
const entries = await readAccessLogs(5000);
|
||
const events = entries.map(entry => ({ ...entry, source: crypto.createHash("sha1").update(JSON.stringify([entry.at, entry.host, entry.method, entry.uri, entry.status, entry.size, entry.durationMs, entry.remoteIp])).digest("hex") }));
|
||
storage.recordAccessEvents(events);
|
||
} catch (error) { console.warn("Could not import access logs into SQLite:", error.message); }
|
||
}
|
||
|
||
function tcpProbe(port, timeoutMs = 1000) {
|
||
return new Promise(resolve => {
|
||
const socket = net.createConnection({ host: "127.0.0.1", port });
|
||
const finish = result => { socket.destroy(); resolve(result); };
|
||
socket.setTimeout(timeoutMs);
|
||
socket.once("connect", () => finish(true));
|
||
socket.once("timeout", () => finish(false));
|
||
socket.once("error", () => finish(false));
|
||
});
|
||
}
|
||
|
||
function tcpProbeHost(host, port, timeoutMs = 4000) {
|
||
return new Promise(resolve => {
|
||
if (!host || !Number.isInteger(port) || port < 1 || port > 65535) return resolve(false);
|
||
const socket = net.createConnection({ host, port });
|
||
const finish = result => { socket.destroy(); resolve(result); };
|
||
socket.setTimeout(timeoutMs);
|
||
socket.once("connect", () => finish(true));
|
||
socket.once("timeout", () => finish(false));
|
||
socket.once("error", () => finish(false));
|
||
});
|
||
}
|
||
|
||
function stableProbe(name, responding) {
|
||
if (responding) { probeFailures[name] = 0; return { status: "ready", healthy: true, responding: true }; }
|
||
probeFailures[name] += 1;
|
||
return probeFailures[name] < 2
|
||
? { status: "checking", healthy: true, responding: false }
|
||
: { status: "error", healthy: false, responding: false };
|
||
}
|
||
|
||
async function loadIconCatalog() {
|
||
if (iconCatalog) return iconCatalog;
|
||
try {
|
||
const response = await fetch("https://raw.githubusercontent.com/homarr-labs/dashboard-icons/main/metadata.json", { signal: AbortSignal.timeout(5000) });
|
||
if (!response.ok) throw new Error(`Icon catalogue returned ${response.status}.`);
|
||
const text = await response.text();
|
||
if (text.length > 8 * 1024 * 1024) throw new Error("Icon catalogue is unexpectedly large.");
|
||
iconCatalog = JSON.parse(text);
|
||
await fsp.writeFile(iconCatalogPath, text);
|
||
} catch (error) {
|
||
try { iconCatalog = JSON.parse(await fsp.readFile(iconCatalogPath, "utf8")); }
|
||
catch { throw Object.assign(new Error("The icon catalogue is temporarily unavailable."), { status: 503 }); }
|
||
}
|
||
return iconCatalog;
|
||
}
|
||
|
||
function iconLabel(slug) {
|
||
return slug.split("-").map(word => word ? word[0].toUpperCase() + word.slice(1) : "").join(" ");
|
||
}
|
||
|
||
async function cacheIcon(slug) {
|
||
if (!/^[a-z0-9][a-z0-9-]{0,100}$/.test(slug)) throw Object.assign(new Error("Invalid icon selection."), { status: 400 });
|
||
const catalog = await loadIconCatalog();
|
||
const metadata = catalog[slug];
|
||
if (!metadata) throw Object.assign(new Error("Icon not found."), { status: 404 });
|
||
const response = await fetch(`https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/${slug}.svg`, { signal: AbortSignal.timeout(7000) });
|
||
if (!response.ok) throw Object.assign(new Error("The selected icon could not be downloaded."), { status: 502 });
|
||
const svg = await response.text();
|
||
if (svg.length > 512 * 1024 || !/<svg[\s>]/i.test(svg) || /<(?:script|foreignObject)\b|\son\w+\s*=|(?:href|xlink:href)\s*=\s*["'](?:https?:|\/\/)/i.test(svg)) {
|
||
throw Object.assign(new Error("The selected icon did not pass safety validation."), { status: 400 });
|
||
}
|
||
const filename = `${slug}.svg`;
|
||
await fsp.writeFile(path.join(iconsDir, filename), svg);
|
||
return `/site-icons/${filename}`;
|
||
}
|
||
|
||
async function dashboardSnapshot() {
|
||
const hosted = sites.map(publicSite);
|
||
const proxyHosts = proxies.map(publicProxy);
|
||
const enabledStreams = streams.filter(item => item.enabled !== false);
|
||
const streamingPorts = { total: enabledStreams.length, listening: enabledStreams.filter(item => activeStreams.has(item.id)).length };
|
||
const certificates = await certificateInventory();
|
||
const tlsDomains = [...sites, ...proxies].filter(item => item.enabled && item.domain && item.tls !== "http").length;
|
||
const [storageWritable, gatewayResponding, httpResponding, httpsResponding] = await Promise.all([
|
||
fsp.access(dataDir, fs.constants.R_OK | fs.constants.W_OK).then(() => true).catch(() => false),
|
||
tcpProbe(2019),
|
||
tcpProbe(80),
|
||
tlsDomains ? tcpProbe(443) : Promise.resolve(false)
|
||
]);
|
||
let gatewayProbe = stableProbe("gateway", gatewayResponding);
|
||
if (gatewayError) gatewayProbe = { status: "error", healthy: false, responding: gatewayResponding };
|
||
const httpProbe = stableProbe("http", httpResponding);
|
||
const httpsProbe = tlsDomains ? stableProbe("https", httpsResponding) : { status: "unconfigured", healthy: true, responding: false };
|
||
const attention = [];
|
||
if (gatewayError) attention.push({ kind: "gateway", name: "Gateway configuration", message: "Caddy rejected the current configuration." });
|
||
if (gatewayProbe.status === "error" && !gatewayResponding) attention.push({ kind: "gateway", name: "Caddy gateway", message: "The Caddy administration endpoint is not responding." });
|
||
if (httpProbe.status === "error") attention.push({ kind: "http", name: "HTTP · Port 80", message: "Port 80 is not accepting connections inside the container." });
|
||
if (httpsProbe.status === "error") attention.push({ kind: "https", name: "HTTPS · Port 443", message: "TLS domains are enabled but port 443 is not accepting connections." });
|
||
if (!storageWritable) attention.push({ kind: "storage", name: "Persistent storage", message: "The data directory is not readable and writable." });
|
||
for (const site of hosted.filter(item => item.status === "error")) attention.push({ kind: "hosted", name: site.name, message: `Hosted site is not responding on port ${site.port}.` });
|
||
for (const proxy of proxyHosts.filter(item => item.status === "error")) attention.push({ kind: "proxy", name: proxy.name, message: "Proxy route needs attention." });
|
||
for (const proxy of proxyHosts.filter(item => item.enabled && item.upstream?.status === "unhealthy")) attention.push({ kind: "upstream", name: proxy.name, message: `Upstream is unavailable${proxy.upstream.error ? ` · ${proxy.upstream.error}` : ""}.` });
|
||
for (const certificate of certificates.certificates.filter(item => ["warning", "critical", "expired", "mismatch"].includes(item.status))) attention.push({ kind: "certificate", target: "certificates", name: certificate.domain, message: certificate.status === "expired" ? "Certificate has expired." : certificate.status === "mismatch" ? "The uploaded certificate does not cover this domain." : `Certificate expires in ${certificate.daysRemaining} day${certificate.daysRemaining === 1 ? "" : "s"}.` });
|
||
const disk = await fsp.statfs(dataDir).catch(() => null);
|
||
const databaseIntegrity = storage.integrity();
|
||
return {
|
||
checkedAt: new Date().toISOString(),
|
||
gateway: { ...gatewayProbe, lastReload: lastGatewayReload },
|
||
services: {
|
||
http: { ...httpProbe, port: 80 },
|
||
https: { ...httpsProbe, port: 443, activeDomains: tlsDomains },
|
||
storage: { status: storageWritable ? "ready" : "error", healthy: storageWritable, path: dataDir }
|
||
},
|
||
hosted: { total: hosted.length, running: hosted.filter(item => item.status === "running").length, disabled: hosted.filter(item => item.status === "disabled").length, errors: hosted.filter(item => item.status === "error").length },
|
||
proxies: { total: proxyHosts.length, running: proxyHosts.filter(item => item.status === "running").length, disabled: proxyHosts.filter(item => item.status === "disabled").length, errors: proxyHosts.filter(item => item.status === "error").length },
|
||
tlsDomains,
|
||
certificates: certificates.summary,
|
||
upstreams: { total: proxyHosts.filter(item => item.enabled).length, healthy: proxyHosts.filter(item => item.upstream?.status === "healthy").length, unhealthy: proxyHosts.filter(item => item.upstream?.status === "unhealthy").length },
|
||
streamingPorts,
|
||
throughput: { liveRequests: storage.performanceLiveCount(60) },
|
||
attention,
|
||
system: {
|
||
uptimeSeconds: Math.floor(process.uptime()),
|
||
memoryBytes: process.memoryUsage().rss,
|
||
dataBytes: await directorySize(dataDir),
|
||
diskFreeBytes: disk ? disk.bavail * disk.bsize : null,
|
||
diskTotalBytes: disk ? disk.blocks * disk.bsize : null,
|
||
appVersion,
|
||
caddyVersion,
|
||
nodeVersion: process.version,
|
||
databaseEngine: "SQLite",
|
||
databaseStatus: databaseIntegrity.length === 1 && databaseIntegrity[0] === "ok" ? "Healthy" : "Needs attention",
|
||
databaseBytes: (await fsp.stat(storage.databasePath).catch(() => null))?.size || 0,
|
||
publicIp: publicIpState.address,
|
||
publicIpCheckedAt: publicIpState.checkedAt,
|
||
publicIpError: publicIpState.error,
|
||
jobs: [{ name: "Upstream checks", enabled: true, schedule: "60s" }, { name: "Scheduled backups", enabled: Boolean(settings.backups?.enabled), schedule: settings.backups?.enabled ? settings.backups.frequency : "off" }, { name: "Log pruning", enabled: Boolean(settings.logsRetention?.pruningEnabled), schedule: settings.logsRetention?.pruningEnabled ? "15m" : "off" }, { name: "Access-log import", enabled: true, schedule: "30s" }, { name: "Public IP check", enabled: true, schedule: "60m" }]
|
||
},
|
||
activity: recentActivity
|
||
};
|
||
}
|
||
|
||
async function startSite(site) {
|
||
if (!site.enabled || activeServers.has(site.id)) return;
|
||
const root = path.join(sitesDir, site.id);
|
||
const app = express();
|
||
app.disable("x-powered-by");
|
||
app.use(express.static(root, { extensions: ["html"], index: "index.html", fallthrough: true }));
|
||
app.use((req, res) => res.status(404).sendFile(path.join(publicDir, "site-404.html")));
|
||
const server = http.createServer(app);
|
||
await new Promise((resolve, reject) => {
|
||
server.once("error", reject);
|
||
server.listen(site.port, "0.0.0.0", resolve);
|
||
});
|
||
activeServers.set(site.id, server);
|
||
console.log(`Serving ${site.name} on port ${site.port}`);
|
||
}
|
||
|
||
async function stopSite(id) {
|
||
const server = activeServers.get(id);
|
||
if (!server) return;
|
||
await new Promise(resolve => server.close(resolve));
|
||
activeServers.delete(id);
|
||
}
|
||
|
||
async function restartSite(site) {
|
||
await stopSite(site.id);
|
||
if (site.enabled) await startSite(site);
|
||
}
|
||
|
||
// Streaming hosts relay raw TCP/UDP on a specific port straight to a host:port target — no domain, no HTTP,
|
||
// no Caddy involvement. This is the same pattern as startSite()/stopSite() above: a dedicated listener Site
|
||
// Gateway owns directly, just for a plain socket instead of an HTTP server.
|
||
async function startStream(stream) {
|
||
if (stream.enabled === false || activeStreams.has(stream.id)) return;
|
||
const [targetHost, targetPortRaw] = String(stream.target || "").split(":");
|
||
const targetPort = Number(targetPortRaw);
|
||
const handle = { tcpServer: null, udpSocket: null, udpSessions: new Map() };
|
||
try {
|
||
if (stream.tcp !== false) {
|
||
const tcpServer = net.createServer(socket => {
|
||
const upstream = net.createConnection({ host: targetHost, port: targetPort });
|
||
const destroyBoth = () => { socket.destroy(); upstream.destroy(); };
|
||
socket.on("error", destroyBoth); upstream.on("error", destroyBoth);
|
||
socket.on("close", () => upstream.destroy()); upstream.on("close", () => socket.destroy());
|
||
socket.pipe(upstream); upstream.pipe(socket);
|
||
});
|
||
await new Promise((resolve, reject) => { tcpServer.once("error", reject); tcpServer.listen(stream.port, "0.0.0.0", resolve); });
|
||
tcpServer.on("error", error => console.warn(`Streaming host “${stream.name}” TCP error:`, error.message));
|
||
handle.tcpServer = tcpServer;
|
||
}
|
||
if (stream.udp) {
|
||
const udpSocket = dgram.createSocket("udp4");
|
||
udpSocket.on("message", (message, rinfo) => {
|
||
const key = `${rinfo.address}:${rinfo.port}`;
|
||
let session = handle.udpSessions.get(key);
|
||
if (!session) {
|
||
const outbound = dgram.createSocket("udp4");
|
||
session = { outbound, timer: null, connected: false, pending: [] };
|
||
outbound.on("message", reply => { try { udpSocket.send(reply, rinfo.port, rinfo.address); } catch { /* client socket may already be gone */ } });
|
||
outbound.on("error", () => {});
|
||
// connect() is asynchronous — sending before it completes silently drops the datagram, which would
|
||
// lose the first packet of every new UDP session. Queue until the callback confirms it's connected.
|
||
outbound.connect(targetPort, targetHost, () => { session.connected = true; for (const buffered of session.pending.splice(0)) { try { outbound.send(buffered); } catch { /* upstream may be unreachable */ } } });
|
||
handle.udpSessions.set(key, session);
|
||
}
|
||
clearTimeout(session.timer);
|
||
session.timer = setTimeout(() => { session.outbound.close(); handle.udpSessions.delete(key); }, 60000).unref();
|
||
if (session.connected) { try { session.outbound.send(message); } catch { /* upstream may be unreachable; drop this datagram */ } }
|
||
else session.pending.push(message);
|
||
});
|
||
await new Promise((resolve, reject) => { udpSocket.once("error", reject); udpSocket.bind(stream.port, "0.0.0.0", resolve); });
|
||
udpSocket.on("error", error => console.warn(`Streaming host “${stream.name}” UDP error:`, error.message));
|
||
handle.udpSocket = udpSocket;
|
||
}
|
||
} catch (error) {
|
||
if (handle.tcpServer) await new Promise(resolve => handle.tcpServer.close(resolve));
|
||
if (handle.udpSocket) handle.udpSocket.close();
|
||
throw error;
|
||
}
|
||
activeStreams.set(stream.id, handle);
|
||
console.log(`Streaming “${stream.name}” on port ${stream.port}`);
|
||
}
|
||
|
||
async function stopStream(id) {
|
||
const handle = activeStreams.get(id);
|
||
if (!handle) return;
|
||
if (handle.tcpServer) await new Promise(resolve => handle.tcpServer.close(resolve));
|
||
if (handle.udpSocket) {
|
||
for (const session of handle.udpSessions.values()) { clearTimeout(session.timer); session.outbound.close(); }
|
||
handle.udpSocket.close();
|
||
}
|
||
activeStreams.delete(id);
|
||
}
|
||
|
||
async function restartStream(stream) {
|
||
await stopStream(stream.id);
|
||
if (stream.enabled !== false) await startStream(stream);
|
||
}
|
||
|
||
async function checkStream(stream) {
|
||
if (stream.enabled === false) { const result = { status: "disabled", checkedAt: new Date().toISOString(), history: [] }; upstreamHealth.set(stream.id, result); return result; }
|
||
if (stream.healthEnabled === false) { const result = { status: "unmonitored", checkedAt: null, history: [] }; upstreamHealth.set(stream.id, result); return result; }
|
||
const started = performance.now();
|
||
const [targetHost, targetPortRaw] = String(stream.target || "").split(":");
|
||
const healthy = await tcpProbeHost(targetHost, Number(targetPortRaw), 4000);
|
||
const responseMs = Math.round(performance.now() - started);
|
||
const result = { status: healthy ? "healthy" : "unhealthy", responseMs, checkedAt: new Date().toISOString(), error: healthy ? null : `Could not open a TCP connection to ${stream.target}` };
|
||
const previous = upstreamHealth.get(stream.id);
|
||
result.history = [{ status: result.status, responseMs, checkedAt: result.checkedAt }, ...(previous?.history || [])].slice(0, 7);
|
||
upstreamHealth.set(stream.id, result);
|
||
return result;
|
||
}
|
||
|
||
function validatePort(port, exceptId) {
|
||
if (!Number.isInteger(port) || port < minPort || port > maxPort) return `Port must be between ${minPort} and ${maxPort}.`;
|
||
if (sites.some(site => site.port === port && site.id !== exceptId)) return "That port is already assigned.";
|
||
return null;
|
||
}
|
||
|
||
async function installUpload(site, file) {
|
||
const destination = path.join(sitesDir, site.id);
|
||
const staging = `${destination}.staging-${Date.now()}`;
|
||
await fsp.mkdir(staging, { recursive: true });
|
||
try {
|
||
if (file.originalname.toLowerCase().endsWith(".zip")) {
|
||
const zip = new AdmZip(file.path);
|
||
for (const entry of zip.getEntries()) {
|
||
const normalized = path.normalize(entry.entryName).replace(/^(\.\.(\/|\\|$))+/, "");
|
||
const target = path.resolve(staging, normalized);
|
||
if (!target.startsWith(`${path.resolve(staging)}${path.sep}`) && target !== path.resolve(staging)) throw new Error("Unsafe path in ZIP file.");
|
||
if (entry.isDirectory) await fsp.mkdir(target, { recursive: true });
|
||
else {
|
||
await fsp.mkdir(path.dirname(target), { recursive: true });
|
||
await fsp.writeFile(target, entry.getData());
|
||
}
|
||
}
|
||
const children = await fsp.readdir(staging, { withFileTypes: true });
|
||
if (children.length === 1 && children[0].isDirectory()) {
|
||
const nested = path.join(staging, children[0].name);
|
||
const nestedChildren = await fsp.readdir(nested);
|
||
for (const child of nestedChildren) await fsp.rename(path.join(nested, child), path.join(staging, child));
|
||
await fsp.rmdir(nested);
|
||
}
|
||
} else {
|
||
await fsp.copyFile(file.path, path.join(staging, "index.html"));
|
||
}
|
||
await fsp.access(path.join(staging, "index.html"));
|
||
await fsp.rm(destination, { recursive: true, force: true });
|
||
await fsp.rename(staging, destination);
|
||
} finally {
|
||
await fsp.rm(file.path, { force: true });
|
||
await fsp.rm(staging, { recursive: true, force: true });
|
||
}
|
||
}
|
||
|
||
const portableCollections = { "sites.json": () => sites, "proxies.json": () => proxies, "redirects.json": () => redirects, "streams.json": () => streams, "access-lists.json": () => accessLists, "users.json": () => users, "groups.json": () => groups, "settings.json": () => settings };
|
||
|
||
async function protectBackup(buffer, password) {
|
||
if (!password) return buffer;
|
||
const salt = crypto.randomBytes(16), iv = crypto.randomBytes(12), key = await scryptAsync(password, salt, 32), cipher = crypto.createCipheriv("aes-256-gcm", key, iv), encrypted = Buffer.concat([cipher.update(buffer), cipher.final()]);
|
||
return Buffer.concat([Buffer.from("SGBK1"), salt, iv, cipher.getAuthTag(), encrypted]);
|
||
}
|
||
|
||
async function openBackup(filename, password = "") {
|
||
let buffer = await fsp.readFile(filename), encrypted = false;
|
||
if (buffer.subarray(0, 5).toString() === "SGBK1") {
|
||
encrypted = true; if (!password) throw Object.assign(new Error("This backup is encrypted. Enter its password."), { status: 400 });
|
||
try { const salt = buffer.subarray(5, 21), iv = buffer.subarray(21, 33), tag = buffer.subarray(33, 49), key = await scryptAsync(password, salt, 32), decipher = crypto.createDecipheriv("aes-256-gcm", key, iv); decipher.setAuthTag(tag); buffer = Buffer.concat([decipher.update(buffer.subarray(49)), decipher.final()]); }
|
||
catch { throw Object.assign(new Error("The backup password is incorrect or the file is damaged."), { status: 400 }); }
|
||
}
|
||
return { zip: new AdmZip(buffer), encrypted };
|
||
}
|
||
|
||
async function createBackup(type = "configuration", includeLogs = false, prefix = "site-gateway-backup", password = "") {
|
||
const safeType = type === "complete" ? "complete" : "configuration";
|
||
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
||
const filename = `${prefix}-${stamp}.sgbackup`;
|
||
const destination = path.join(backupsDir, filename);
|
||
const zip = new AdmZip();
|
||
const manifest = { format: 2, product: "Site Gateway", appVersion, database: "sqlite", schemaVersion: 1, instanceId: LOCAL_INSTANCE_ID, createdAt: new Date().toISOString(), type: safeType, includeLogs: Boolean(includeLogs), encrypted: Boolean(password), files: [] };
|
||
const databaseSnapshot = path.join(uploadDir, `database-${crypto.randomUUID()}.sqlite`);
|
||
storage.backupTo(databaseSnapshot); zip.addLocalFile(databaseSnapshot, "database", "site-gateway.sqlite"); await fsp.rm(databaseSnapshot, { force: true });
|
||
for (const [name, getter] of Object.entries(portableCollections)) zip.addFile(`portable-json/${name}`, Buffer.from(JSON.stringify(getter(), null, 2)));
|
||
if (safeType === "complete") {
|
||
for (const [directory, archivePath] of [[sitesDir, "sites"], [iconsDir, "icons"], [defaultSiteDir, "default-site"], [certificatesRoot, "certificates"]]) {
|
||
if (fs.existsSync(directory)) zip.addLocalFolder(directory, archivePath);
|
||
}
|
||
}
|
||
if (includeLogs && fs.existsSync(logsDir)) zip.addLocalFolder(logsDir, "logs");
|
||
manifest.files = zip.getEntries().filter(entry => !entry.isDirectory).map(entry => entry.entryName);
|
||
manifest.checksums = Object.fromEntries(zip.getEntries().filter(entry => !entry.isDirectory).map(entry => [entry.entryName, crypto.createHash("sha256").update(entry.getData()).digest("hex")]));
|
||
zip.addFile("manifest.json", Buffer.from(JSON.stringify(manifest, null, 2)));
|
||
await fsp.writeFile(destination, await protectBackup(zip.toBuffer(), password));
|
||
recordActivity(`${safeType === "complete" ? "Complete" : "Configuration"} backup created.`);
|
||
return { filename, path: destination, ...manifest, size: (await fsp.stat(destination)).size };
|
||
}
|
||
|
||
async function listBackups() {
|
||
const names = (await fsp.readdir(backupsDir)).filter(name => name.endsWith(".sgbackup"));
|
||
return Promise.all(names.map(async filename => {
|
||
const stat = await fsp.stat(path.join(backupsDir, filename));
|
||
let manifest = {}; const header = Buffer.alloc(5); const handle = await fsp.open(path.join(backupsDir, filename), "r"); await handle.read(header, 0, 5, 0); await handle.close(); const encrypted = header.toString() === "SGBK1";
|
||
if (!encrypted) try { manifest = JSON.parse(new AdmZip(path.join(backupsDir, filename)).readAsText("manifest.json")); } catch { /* Report unreadable archive in UI. */ }
|
||
return { filename, size: stat.size, createdAt: manifest.createdAt || stat.mtime.toISOString(), type: encrypted ? "encrypted" : manifest.type || "unknown", appVersion: encrypted ? "protected" : manifest.appVersion || "unknown", valid: encrypted || Boolean(manifest.format), encrypted };
|
||
})).then(items => items.sort((a, b) => b.createdAt.localeCompare(a.createdAt)));
|
||
}
|
||
|
||
async function restoreBackup(filename, password = "", createSafetyBackup = true) {
|
||
const source = path.resolve(backupsDir, filename);
|
||
if (!source.startsWith(`${backupsDir}${path.sep}`) || !filename.endsWith(".sgbackup")) throw Object.assign(new Error("Invalid backup selection."), { status: 400 });
|
||
const { zip } = await openBackup(source, password); const manifest = JSON.parse(zip.readAsText("manifest.json") || "null");
|
||
if (!manifest || manifest.product !== "Site Gateway" || ![1,2].includes(manifest.format)) throw Object.assign(new Error("This is not a supported Site Gateway backup."), { status: 400 });
|
||
for (const [name, expected] of Object.entries(manifest.checksums || {})) {
|
||
const entry = zip.getEntry(name); if (!entry || crypto.createHash("sha256").update(entry.getData()).digest("hex") !== expected) throw Object.assign(new Error(`Backup integrity check failed for ${name}.`), { status: 400 });
|
||
}
|
||
const safetyBackup = createSafetyBackup ? await createBackup("complete", true, "pre-restore") : null;
|
||
const staging = path.join(uploadDir, `restore-${crypto.randomUUID()}`); await fsp.mkdir(staging, { recursive: true });
|
||
try {
|
||
for (const entry of zip.getEntries()) {
|
||
if (entry.entryName === "manifest.json") continue;
|
||
const target = path.resolve(staging, entry.entryName);
|
||
if (!target.startsWith(`${staging}${path.sep}`)) throw Object.assign(new Error("Unsafe path in backup."), { status: 400 });
|
||
if (entry.isDirectory) await fsp.mkdir(target, { recursive: true }); else { await fsp.mkdir(path.dirname(target), { recursive: true }); await fsp.writeFile(target, entry.getData()); }
|
||
}
|
||
const restoredDatabase = path.join(staging, "database", "site-gateway.sqlite");
|
||
if (fs.existsSync(restoredDatabase)) {
|
||
const candidate = new (await import("node:sqlite")).DatabaseSync(restoredDatabase, { readOnly: true }); const check = candidate.prepare("PRAGMA integrity_check").get(); candidate.close();
|
||
if (Object.values(check)[0] !== "ok") throw Object.assign(new Error("The restored SQLite database failed its integrity check."), { status: 400 });
|
||
const activeDatabasePath = storage.databasePath; storage.close();
|
||
await Promise.all([fsp.rm(`${activeDatabasePath}-wal`, { force: true }), fsp.rm(`${activeDatabasePath}-shm`, { force: true })]);
|
||
await fsp.copyFile(restoredDatabase, activeDatabasePath); storage = await openStorage(dataDir, backupsDir);
|
||
} else {
|
||
const legacyRoot = fs.existsSync(path.join(staging, "portable-json")) ? path.join(staging, "portable-json") : fs.existsSync(path.join(staging, "legacy-json")) ? path.join(staging, "legacy-json") : path.join(staging, "config");
|
||
storage.saveCollection("sites", []); storage.saveCollection("proxies", []); storage.saveCollection("redirects", []); storage.saveCollection("streams", []);
|
||
for (const [name, kind] of Object.entries({ "access-lists.json":"access_lists", "sites.json":"sites", "proxies.json":"proxies", "redirects.json":"redirects", "users.json":"users" })) { const candidate = path.join(legacyRoot, name); if (fs.existsSync(candidate)) storage.saveCollection(kind, JSON.parse(await fsp.readFile(candidate, "utf8"))); }
|
||
const settingsCandidate = path.join(legacyRoot, "settings.json"); if (fs.existsSync(settingsCandidate)) storage.saveSettings(JSON.parse(await fsp.readFile(settingsCandidate, "utf8")));
|
||
}
|
||
if (manifest.type === "complete") for (const name of ["sites", "icons", "default-site", "certificates"]) {
|
||
const candidate = path.join(staging, name); if (!fs.existsSync(candidate)) continue;
|
||
const destination = path.join(dataDir, name); await fsp.rm(destination, { recursive: true, force: true }); await fsp.cp(candidate, destination, { recursive: true });
|
||
}
|
||
if (manifest.type === "complete" && fs.existsSync(path.join(staging, "custom-certificates"))) {
|
||
await fsp.mkdir(customCertificatesDir, { recursive: true }); await fsp.cp(path.join(staging, "custom-certificates"), customCertificatesDir, { recursive: true });
|
||
}
|
||
await Promise.all([...activeServers.keys()].map(stopSite)); await Promise.all([...activeStreams.keys()].map(stopStream)); sites = []; proxies = []; users = []; redirects = []; streams = []; accessLists = []; groups = []; settings = {}; recentActivity.splice(0); await loadSites();
|
||
if (manifest.type === "complete") for (const site of sites) { const contentRoot = path.join(sitesDir, site.id); if (!fs.existsSync(path.join(contentRoot, "index.html"))) throw new Error(`Restored hosted site “${site.name || site.id}” is missing index.html.`); }
|
||
for (const site of sites.filter(item => item.enabled)) await startSite(site);
|
||
for (const stream of streams.filter(item => item.enabled !== false)) { try { await startStream(stream); } catch (error) { console.error(`Could not start streaming host “${stream.name}”:`, error.message); } }
|
||
await syncCaddy(); recordActivity(`Backup ${filename} restored.`);
|
||
} catch (error) {
|
||
if (safetyBackup) {
|
||
try { await restoreBackup(safetyBackup.filename, "", false); recordActivity(`Restore of ${filename} failed; the pre-restore state was recovered.`, "error"); }
|
||
catch (rollbackError) { error.message = `${error.message} Automatic rollback also failed: ${rollbackError.message}`; }
|
||
}
|
||
throw error;
|
||
} finally { await fsp.rm(staging, { recursive: true, force: true }); }
|
||
return manifest;
|
||
}
|
||
|
||
await loadSites();
|
||
try {
|
||
const result = await execFileAsync("caddy", ["version"]);
|
||
caddyVersion = result.stdout.trim().split(/\s+/)[0] || "Unknown";
|
||
} catch (error) {
|
||
console.warn("Could not detect Caddy version:", error.message);
|
||
}
|
||
for (const site of sites.filter(item => item.enabled)) {
|
||
try { await startSite(site); } catch (error) { console.error(`Could not start ${site.name}:`, error.message); }
|
||
}
|
||
for (const stream of streams.filter(item => item.enabled !== false)) {
|
||
try { await startStream(stream); } catch (error) { console.error(`Could not start streaming host “${stream.name}”:`, error.message); }
|
||
}
|
||
for (let attempt = 0; attempt < 10; attempt++) {
|
||
try { await syncCaddy(); break; }
|
||
catch (error) {
|
||
if (attempt === 9) console.error(error.message);
|
||
else await new Promise(resolve => setTimeout(resolve, 500));
|
||
}
|
||
}
|
||
|
||
const app = express();
|
||
const upload = multer({ dest: uploadDir, limits: { fileSize: 250 * 1024 * 1024, files: 1 } });
|
||
const certificateUpload = multer({ dest: uploadDir, limits: { fileSize: 5 * 1024 * 1024, files: 2 } });
|
||
const iconUpload = multer({ dest: uploadDir, limits: { fileSize: 2 * 1024 * 1024, files: 1 } });
|
||
app.disable("x-powered-by");
|
||
app.use(express.json());
|
||
app.use(express.urlencoded({ extended: false }));
|
||
app.get(["/", "/index.html"], (req, res) => {
|
||
const html = fs.readFileSync(path.join(publicDir, "index.html"), "utf8")
|
||
.replace(/\/(app|features)\.js\?v=[^"']+/g, `/$1.js?v=${appVersion}`)
|
||
.replace(/\/styles\.css\?v=[^"']+/g, `/styles.css?v=${appVersion}`);
|
||
res.type("html").send(html);
|
||
});
|
||
app.use(express.static(publicDir));
|
||
app.use("/site-icons", express.static(iconsDir, { immutable: true, maxAge: "30d", setHeaders: res => res.setHeader("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'") }));
|
||
|
||
app.get("/api/session", (req, res) => {
|
||
const user = sessionUser(req);
|
||
res.json({ authenticated: Boolean(user), setupRequired: Boolean(user?.setupRequired), installationSetupPending: users.some(item => item.setupRequired), user: user ? publicUser(user) : null, username: user?.username || null });
|
||
});
|
||
function checkLoginRateLimit(key) {
|
||
const attempt = loginAttempts.get(key) || { count: 0, resetAt: Date.now() + 15 * 60 * 1000 };
|
||
if (attempt.resetAt <= Date.now()) { attempt.count = 0; attempt.resetAt = Date.now() + 15 * 60 * 1000; }
|
||
return attempt;
|
||
}
|
||
function issueSessionCookie(res, user) {
|
||
if (!user.sessionVersion) user.sessionVersion = crypto.randomBytes(16).toString("hex");
|
||
const expires = String(Date.now() + 12 * 60 * 60 * 1000);
|
||
const value = `${user.id}.${expires}.${user.sessionVersion}`;
|
||
res.setHeader("Set-Cookie", [`webserver_session=${value}.${sign(value)}; Path=/; HttpOnly; SameSite=Strict; Max-Age=43200`, "pending_mfa=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0"]);
|
||
}
|
||
function issuePendingMfaCookie(res, user) {
|
||
const expires = String(Date.now() + 5 * 60 * 1000);
|
||
const value = `${user.id}.${expires}.mfa`;
|
||
res.setHeader("Set-Cookie", `pending_mfa=${value}.${sign(value)}; Path=/; HttpOnly; SameSite=Strict; Max-Age=300`);
|
||
}
|
||
function pendingMfaUser(req) {
|
||
const token = cookieMap(req.headers.cookie).pending_mfa;
|
||
if (!token) return null;
|
||
const [userId, expires, marker, signature] = token.split(".");
|
||
const user = users.find(item => item.id === userId && item.status === "active");
|
||
if (!user || marker !== "mfa" || !expires || Number(expires) <= Date.now() || !safeEqual(signature || "", sign(`${userId}.${expires}.${marker}`))) return null;
|
||
return user;
|
||
}
|
||
app.post("/api/login", async (req, res, next) => {
|
||
try {
|
||
const key = req.ip || req.socket.remoteAddress || "unknown";
|
||
const attempt = checkLoginRateLimit(key);
|
||
if (attempt.count >= 8) { recordActivity(`Security: sign-in rate limit reached for ${key}.`, "error"); return res.status(429).json({ error: "Too many sign-in attempts. Try again in 15 minutes." }); }
|
||
const username = String(req.body.username || "").trim().toLowerCase();
|
||
const user = users.find(item => item.username === username);
|
||
if (!user || user.status !== "active" || !await passwordMatches(req.body.password || "", user.password)) {
|
||
attempt.count += 1; loginAttempts.set(key, attempt); recordActivity(`Security: failed sign-in attempt for ${username || "unknown user"}.`, "error");
|
||
return res.status(401).json({ error: "Incorrect username or password." });
|
||
}
|
||
loginAttempts.delete(key);
|
||
if (user.mfaEnabled) { issuePendingMfaCookie(res, user); return res.json({ mfaRequired: true }); }
|
||
user.lastLoginAt = new Date().toISOString(); user.updatedAt = user.lastLoginAt; await saveUsers();
|
||
issueSessionCookie(res, user);
|
||
res.json({ ok: true, user: publicUser(user) });
|
||
} catch (error) { next(error); }
|
||
});
|
||
app.post("/api/login/mfa", async (req, res, next) => {
|
||
try {
|
||
const key = req.ip || req.socket.remoteAddress || "unknown";
|
||
const attempt = checkLoginRateLimit(key);
|
||
if (attempt.count >= 8) { recordActivity(`Security: sign-in rate limit reached for ${key}.`, "error"); return res.status(429).json({ error: "Too many sign-in attempts. Try again in 15 minutes." }); }
|
||
const user = pendingMfaUser(req);
|
||
if (!user || !user.mfaEnabled) { attempt.count += 1; loginAttempts.set(key, attempt); return res.status(401).json({ error: "Your sign-in session expired. Please sign in again." }); }
|
||
const code = String(req.body.code || "").trim();
|
||
let matchedRecoveryCode = null;
|
||
const isValidTotp = verifyTotp(user.mfaSecret, code);
|
||
if (!isValidTotp) {
|
||
for (const entry of user.mfaRecoveryCodes || []) {
|
||
if (entry.usedAt) continue;
|
||
if (await passwordMatches(code, entry.hash)) { matchedRecoveryCode = entry; break; }
|
||
}
|
||
}
|
||
if (!isValidTotp && !matchedRecoveryCode) {
|
||
attempt.count += 1; loginAttempts.set(key, attempt); recordActivity(`Security: failed two-factor code for “${user.username}”.`, "error");
|
||
return res.status(401).json({ error: "That code didn't match. Try again." });
|
||
}
|
||
loginAttempts.delete(key);
|
||
if (matchedRecoveryCode) { matchedRecoveryCode.usedAt = new Date().toISOString(); recordActivity(`User “${user.username}” signed in using a two-factor recovery code.`); }
|
||
user.lastLoginAt = new Date().toISOString(); user.updatedAt = user.lastLoginAt; await saveUsers();
|
||
issueSessionCookie(res, user);
|
||
res.json({ ok: true, user: publicUser(user) });
|
||
} catch (error) { next(error); }
|
||
});
|
||
app.post("/api/logout", (req, res) => {
|
||
res.setHeader("Set-Cookie", "webserver_session=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0");
|
||
res.json({ ok: true });
|
||
});
|
||
function accessSession(req, listId) {
|
||
const token = cookieMap(req.headers.cookie).site_gateway_access; if (!token) return null;
|
||
const [storedList, username, expires, signature] = token.split(".");
|
||
if (storedList !== listId || Number(expires) <= Date.now() || !safeEqual(signature || "", sign(`${storedList}.${username}.${expires}`))) return null;
|
||
return username;
|
||
}
|
||
function accessUserAllowed(list, username) { if (list.credentials?.some(item => item.username === username)) return true; return (list.groups || []).some(groupId => { const group = groups.find(item => item.id === groupId && item.enabled !== false); return Boolean(group?.members?.some(userId => users.some(user => user.id === userId && user.status === "active" && user.username === username))); }); }
|
||
app.get("/api/access-check", (req, res) => {
|
||
const listId = String(req.query.list || ""), list = accessLists.find(item => item.id === listId && item.enabled !== false);
|
||
if (!list || !list.credentials?.length) return res.status(204).end();
|
||
const username = accessSession(req, listId); if (username && accessUserAllowed(list, username)) { res.setHeader("X-Site-Gateway-User", username); return res.status(204).end(); }
|
||
const original = String(req.headers["x-forwarded-uri"] || "/"); const safeReturn = original.startsWith("/") && !original.startsWith("//") ? original : "/";
|
||
res.redirect(302, `/_site-gateway/login?list=${encodeURIComponent(listId)}&return=${encodeURIComponent(safeReturn)}`);
|
||
});
|
||
app.get("/_site-gateway/login", (req, res) => {
|
||
const listId = String(req.query.list || ""), list = accessLists.find(item => item.id === listId && item.enabled !== false);
|
||
if (!list) return res.status(404).send("Access policy not found."); const safeReturn = String(req.query.return || "/").startsWith("/") ? String(req.query.return || "/") : "/";
|
||
res.type("html").send(`<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="color-scheme" content="dark light"><title>Sign in · Site Gateway</title><style>:root{color-scheme:dark light;--bg:#08101d;--panel:#101a2b;--line:#25344c;--text:#eef4ff;--muted:#95a4ba;--green:#62e6a7}*{box-sizing:border-box}body{margin:0;min-height:100vh;display:grid;place-items:center;padding:22px;background:radial-gradient(circle at 50% 0,#17372b 0,transparent 44%),var(--bg);color:var(--text);font-family:Inter,system-ui,sans-serif}.card{width:min(430px,100%);padding:34px;border:1px solid var(--line);border-radius:20px;background:var(--panel);box-shadow:0 24px 70px #0007}.mark{width:48px;height:48px;display:grid;place-items:center;border-radius:14px;background:#18362b;color:var(--green);font-weight:900}.eyebrow{margin:25px 0 7px;color:var(--green);font-size:11px;font-weight:850;letter-spacing:.13em;text-transform:uppercase}h1{margin:0;font-size:34px;letter-spacing:-.045em}p{color:var(--muted);line-height:1.55}label{display:block;margin-top:17px;font-size:13px;font-weight:700}input{display:block;width:100%;height:46px;margin-top:7px;padding:0 12px;border:1px solid var(--line);border-radius:10px;background:#0a1423;color:var(--text);font:inherit}button{width:100%;height:46px;margin-top:22px;border:0;border-radius:10px;background:var(--green);color:#05251a;font-weight:850;cursor:pointer}.error{color:#ff7185;font-size:13px}@media(prefers-color-scheme:light){:root{--bg:#f3f6fa;--panel:#fff;--line:#d6dfeb;--text:#132033;--muted:#637188;--green:#138a5b}input{background:#fff}}</style></head><body><form class="card" method="post" action="/_site-gateway/login"><div class="mark">SG</div><div class="eyebrow">Protected by Site Gateway</div><h1>Sign in to continue</h1><p>This service uses the <strong>${String(list.name).replace(/[<>]/g, "")}</strong> access policy.</p>${req.query.error ? '<p class="error">That username or password was not accepted.</p>' : ""}<input type="hidden" name="list" value="${listId}"><input type="hidden" name="return" value="${safeReturn.replaceAll('"', '"')}"><label>Username<input name="username" autocomplete="username" required></label><label>Password<input name="password" type="password" autocomplete="current-password" required></label><button>Sign in</button></form></body></html>`);
|
||
});
|
||
app.post("/_site-gateway/login", async (req, res, next) => {
|
||
try {
|
||
const listId = String(req.body.list || ""), list = accessLists.find(item => item.id === listId && item.enabled !== false), username = String(req.body.username || "").trim(); const credential = list?.credentials?.find(item => item.username === username) || ((list && accessUserAllowed(list, username)) ? users.find(user => user.username === username && user.status === "active") : null);
|
||
const safeReturn = String(req.body.return || "/").startsWith("/") && !String(req.body.return).startsWith("//") ? String(req.body.return) : "/";
|
||
if (!credential?.password || !await passwordMatches(req.body.password || "", credential.password)) return res.redirect(303, `/_site-gateway/login?list=${encodeURIComponent(listId)}&return=${encodeURIComponent(safeReturn)}&error=1`);
|
||
const expires = String(Date.now() + 12 * 60 * 60 * 1000), value = `${listId}.${username}.${expires}`; const secure = String(req.headers["x-forwarded-proto"] || "").includes("https") ? "; Secure" : "";
|
||
res.setHeader("Set-Cookie", `site_gateway_access=${value}.${sign(value)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=43200${secure}`); res.redirect(303, safeReturn);
|
||
} catch (error) { next(error); }
|
||
});
|
||
app.use("/api", (req, res, next) => {
|
||
const user = sessionUser(req);
|
||
if (!user) return res.status(401).json({ error: "Please sign in." });
|
||
req.user = user;
|
||
next();
|
||
});
|
||
app.post("/api/setup/admin", async (req, res, next) => {
|
||
try {
|
||
if (!req.user.setupRequired || req.user.source !== "bootstrap" || req.user.role !== "administrator") return res.status(409).json({ error: "Initial administrator setup has already been completed." });
|
||
const username = String(req.body.username || "").trim().toLowerCase();
|
||
const displayName = String(req.body.displayName || "").trim();
|
||
const password = String(req.body.password || "");
|
||
const confirmation = String(req.body.confirmPassword || "");
|
||
if (!/^[a-z0-9][a-z0-9._-]{2,63}$/.test(username)) return res.status(400).json({ error: "Username must be 3–64 characters using letters, numbers, periods, hyphens, or underscores." });
|
||
if (users.some(user => user.id !== req.user.id && user.username === username)) return res.status(409).json({ error: "That username already exists." });
|
||
if (!displayName || displayName.length > 80) return res.status(400).json({ error: "Display name is required and must be 80 characters or fewer." });
|
||
if (password.length < 8) return res.status(400).json({ error: "Password must contain at least 8 characters." });
|
||
if (!safeEqual(password, confirmation)) return res.status(400).json({ error: "The passwords do not match." });
|
||
req.user.username = username; req.user.displayName = displayName; req.user.password = await passwordRecord(password);
|
||
req.user.source = "local"; req.user.setupRequired = false; req.user.sessionVersion = crypto.randomBytes(16).toString("hex"); req.user.updatedAt = new Date().toISOString();
|
||
await saveUsers(); recordActivity(`Initial administrator setup completed for “${username}”.`);
|
||
res.setHeader("Set-Cookie", "webserver_session=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0");
|
||
res.json({ ok: true });
|
||
} catch (error) { next(error); }
|
||
});
|
||
app.use("/api", (req, res, next) => { currentAuditActor = req.user?.id || null; return req.user.setupRequired ? res.status(428).json({ error: "Complete the initial administrator setup before continuing." }) : next(); });
|
||
app.use("/api", (req, res, next) => { if (req.path.startsWith("/account/")) return next(); if (req.method === "GET" || req.user.role === "administrator") return next(); const operational = /^\/(sites|proxies|redirects|streams|access-lists)(\/|$)/.test(req.path); if (req.user.role === "standard" && operational) return next(); return res.status(403).json({ error: "Administrator access is required for this action." }); });
|
||
app.post("/api/account/password", async (req, res, next) => {
|
||
try {
|
||
const currentPassword = String(req.body.currentPassword || "");
|
||
const newPassword = String(req.body.newPassword || "");
|
||
if (!await passwordMatches(currentPassword, req.user.password)) return res.status(400).json({ error: "Your current password is incorrect." });
|
||
if (newPassword.length < 8) return res.status(400).json({ error: "New password must contain at least 8 characters." });
|
||
req.user.password = await passwordRecord(newPassword);
|
||
req.user.updatedAt = new Date().toISOString();
|
||
await saveUsers(); recordActivity(`User “${req.user.username}” changed their password.`);
|
||
issueSessionCookie(res, req.user);
|
||
res.json({ ok: true });
|
||
} catch (error) { next(error); }
|
||
});
|
||
app.post("/api/account/mfa/setup", async (req, res, next) => {
|
||
try {
|
||
if (req.user.mfaEnabled) return res.status(409).json({ error: "Two-factor authentication is already enabled. Disable it first to start over." });
|
||
const secret = generateTotpSecret();
|
||
req.user.mfaPendingSecret = secret;
|
||
await saveUsers();
|
||
const uri = otpauthUri({ secret, username: req.user.username });
|
||
const qrSvg = await QRCode.toString(uri, { type: "svg", margin: 1, width: 220 });
|
||
res.json({ secret, otpauthUri: uri, qrSvg });
|
||
} catch (error) { next(error); }
|
||
});
|
||
app.post("/api/account/mfa/confirm", async (req, res, next) => {
|
||
try {
|
||
if (!req.user.mfaPendingSecret) return res.status(400).json({ error: "Start two-factor setup before confirming a code." });
|
||
if (!verifyTotp(req.user.mfaPendingSecret, req.body.code)) return res.status(400).json({ error: "That code didn't match. Try again." });
|
||
req.user.mfaSecret = req.user.mfaPendingSecret;
|
||
req.user.mfaPendingSecret = null;
|
||
req.user.mfaEnabled = true;
|
||
const codes = generateRecoveryCodes(10);
|
||
req.user.mfaRecoveryCodes = await Promise.all(codes.map(async code => ({ hash: await passwordRecord(code), usedAt: null })));
|
||
req.user.updatedAt = new Date().toISOString();
|
||
await saveUsers(); recordActivity(`User “${req.user.username}” enabled two-factor authentication.`);
|
||
res.json({ ok: true, recoveryCodes: codes });
|
||
} catch (error) { next(error); }
|
||
});
|
||
app.post("/api/account/mfa/disable", async (req, res, next) => {
|
||
try {
|
||
if (!await passwordMatches(req.body.password || "", req.user.password)) return res.status(400).json({ error: "Your current password is incorrect." });
|
||
req.user.mfaEnabled = false; req.user.mfaSecret = null; req.user.mfaPendingSecret = null; req.user.mfaRecoveryCodes = [];
|
||
req.user.updatedAt = new Date().toISOString();
|
||
await saveUsers(); recordActivity(`User “${req.user.username}” disabled two-factor authentication.`, "warning");
|
||
res.json({ ok: true });
|
||
} catch (error) { next(error); }
|
||
});
|
||
app.post("/api/account/mfa/recovery-codes", async (req, res, next) => {
|
||
try {
|
||
if (!req.user.mfaEnabled) return res.status(400).json({ error: "Two-factor authentication isn't enabled." });
|
||
if (!await passwordMatches(req.body.password || "", req.user.password)) return res.status(400).json({ error: "Your current password is incorrect." });
|
||
const codes = generateRecoveryCodes(10);
|
||
req.user.mfaRecoveryCodes = await Promise.all(codes.map(async code => ({ hash: await passwordRecord(code), usedAt: null })));
|
||
req.user.updatedAt = new Date().toISOString();
|
||
await saveUsers(); recordActivity(`User “${req.user.username}” regenerated two-factor recovery codes.`);
|
||
res.json({ ok: true, recoveryCodes: codes });
|
||
} catch (error) { next(error); }
|
||
});
|
||
app.get("/api/config", (req, res) => res.json({ version: appVersion, minPort, maxPort, adminPort, storage: { engine: "sqlite", databasePath: storage.databasePath, instanceId: LOCAL_INSTANCE_ID, backupsPath: backupsDir, certificatesPath: certificatesRoot }, gateway: { enabled: true, error: gatewayError } }));
|
||
app.get("/api/users", (req, res) => req.user.role === "administrator" ? res.json(users.map(publicUser)) : res.status(403).json({ error: "Administrator access is required." }));
|
||
app.get("/api/audit", (req, res) => req.user.role === "administrator" ? res.json(storage.listAudit({ user: req.query.user, action: req.query.action, status: req.query.status }).map(item => ({ ...item, actor: users.find(user => user.id === item.actor_id)?.username || "System" }))) : res.status(403).json({ error: "Administrator access is required." }));
|
||
app.post("/api/users", async (req, res, next) => {
|
||
try {
|
||
const username = String(req.body.username || "").trim().toLowerCase();
|
||
const displayName = String(req.body.displayName || "").trim();
|
||
const password = String(req.body.password || "");
|
||
const role = ["administrator", "standard", "viewer"].includes(req.body.role) ? req.body.role : "standard";
|
||
if (!/^[a-z0-9][a-z0-9._-]{2,63}$/.test(username)) return res.status(400).json({ error: "Username must be 3–64 characters using letters, numbers, periods, hyphens, or underscores." });
|
||
if (users.some(user => user.username === username)) return res.status(409).json({ error: "That username already exists." });
|
||
if (!displayName || displayName.length > 80) return res.status(400).json({ error: "Display name is required and must be 80 characters or fewer." });
|
||
if (password.length < 8) return res.status(400).json({ error: "Password must contain at least 8 characters." });
|
||
const now = new Date().toISOString();
|
||
const user = { id: crypto.randomUUID(), username, displayName, role, status: "active", password: await passwordRecord(password), source: "local", createdAt: now, updatedAt: now, lastLoginAt: null };
|
||
users.push(user); await saveUsers(); recordActivity(`User “${user.username}” created as ${role === "administrator" ? "Administrator" : role === "viewer" ? "Viewer" : "Standard User"}.`);
|
||
res.status(201).json(publicUser(user));
|
||
} catch (error) { next(error); }
|
||
});
|
||
app.patch("/api/users/:id", async (req, res, next) => {
|
||
try {
|
||
const user = users.find(item => item.id === req.params.id);
|
||
if (!user) return res.status(404).json({ error: "User not found." });
|
||
const nextRole = req.body.role === undefined ? user.role : ["administrator", "standard", "viewer"].includes(req.body.role) ? req.body.role : null;
|
||
if (!nextRole) return res.status(400).json({ error: "Invalid user role." });
|
||
const nextStatus = req.body.status === undefined ? user.status : ["active", "disabled", "archived"].includes(req.body.status) ? req.body.status : null;
|
||
if (!nextStatus) return res.status(400).json({ error: "Invalid user status." });
|
||
const removesActiveAdmin = user.role === "administrator" && user.status === "active" && (nextRole !== "administrator" || nextStatus !== "active");
|
||
if (removesActiveAdmin && activeAdministrators().length === 1) return res.status(400).json({ error: "At least one active Administrator is required." });
|
||
if (user.id === req.user.id && nextStatus !== "active") return res.status(400).json({ error: "You cannot disable or archive your own account." });
|
||
if (user.id === req.user.id && nextRole !== user.role) return res.status(400).json({ error: "Another Administrator must change your role." });
|
||
user.role = nextRole; user.status = nextStatus;
|
||
if (req.body.displayName !== undefined) {
|
||
const displayName = String(req.body.displayName).trim();
|
||
if (!displayName || displayName.length > 80) return res.status(400).json({ error: "Display name is required and must be 80 characters or fewer." });
|
||
user.displayName = displayName;
|
||
}
|
||
if (req.body.password !== undefined) {
|
||
const password = String(req.body.password);
|
||
if (password.length < 8) return res.status(400).json({ error: "Password must contain at least 8 characters." });
|
||
user.password = await passwordRecord(password);
|
||
}
|
||
user.updatedAt = new Date().toISOString(); await saveUsers(); recordActivity(`User “${user.username}” updated · ${user.role === "administrator" ? "Administrator" : user.role === "viewer" ? "Viewer" : "Standard User"} · ${user.status}.`);
|
||
res.json(publicUser(user));
|
||
} catch (error) { next(error); }
|
||
});
|
||
app.delete("/api/users/:id", async (req, res, next) => {
|
||
try {
|
||
if (req.user.role !== "administrator") return res.status(403).json({ error: "Administrator access is required." });
|
||
if (req.params.id === req.user.id) return res.status(400).json({ error: "You cannot delete your own account." });
|
||
const index = users.findIndex(user => user.id === req.params.id);
|
||
if (index < 0) return res.status(404).json({ error: "User not found." });
|
||
const [removed] = users.splice(index, 1);
|
||
groups.forEach(group => { group.members = (group.members || []).filter(id => id !== removed.id); });
|
||
await Promise.all([saveUsers(), saveGroups()]);
|
||
recordActivity(`User “${removed.username}” permanently deleted.`);
|
||
res.status(204).end();
|
||
} catch (error) { next(error); }
|
||
});
|
||
app.get("/api/sites", (req, res) => res.json(sites.map(publicSite)));
|
||
app.get("/api/proxies", (req, res) => res.json(proxies.map(proxy => publicProxy(proxy, req.user.role === "administrator"))));
|
||
app.get("/api/redirects", (req, res) => res.json(redirects));
|
||
app.get("/api/access-lists", (req, res) => res.json(accessLists.map(({ credentials, ...item }) => ({ ...item, credentials: (credentials || []).map(({ username }) => ({ username })), groups: item.groups || [] }))));
|
||
app.get("/api/groups", (req, res) => req.user.role === "administrator" ? res.json(groups.map(group => ({ ...group, memberIds: [...(group.members || [])], members: (group.members || []).map(id => users.find(user => user.id === id)?.username).filter(Boolean) }))) : res.status(403).json({ error: "Administrator access is required." }));
|
||
app.post("/api/access-lists/:id/groups", async (req, res, next) => { try { if (req.user.role !== "administrator") return res.status(403).json({ error: "Administrator access is required." }); const list = accessLists.find(value => value.id === req.params.id); if (!list) return res.status(404).json({ error: "Access List not found." }); list.groups = Array.isArray(req.body.groups) ? [...new Set(req.body.groups)].filter(id => groups.some(group => group.id === id && group.enabled !== false)) : []; await saveAccessLists(); recordActivity("Groups updated for Access List “" + list.name + "”."); res.json({ groups: list.groups }); } catch (error) { next(error); } });
|
||
app.post("/api/groups", async (req, res, next) => { try { if (req.user.role !== "administrator") return res.status(403).json({ error: "Administrator access is required." }); const name = String(req.body.name || "").trim().slice(0, 80); if (!name) return res.status(400).json({ error: "Group name is required." }); if (groups.some(group => group.name.toLowerCase() === name.toLowerCase())) return res.status(409).json({ error: "That group already exists." }); const group = { id: "group-" + crypto.randomBytes(4).toString("hex"), name, enabled: true, members: [], createdAt: new Date().toISOString() }; groups.push(group); await saveGroups(); recordActivity("Group “" + name + "” created."); res.status(201).json(group); } catch (error) { next(error); } });
|
||
app.patch("/api/groups/:id", async (req, res, next) => { try { if (req.user.role !== "administrator") return res.status(403).json({ error: "Administrator access is required." }); const group = groups.find(value => value.id === req.params.id); if (!group) return res.status(404).json({ error: "Group not found." }); if (req.body.name !== undefined) { const name = String(req.body.name || "").trim().slice(0, 80); if (!name) return res.status(400).json({ error: "Group name is required." }); group.name = name; } if (req.body.enabled !== undefined) group.enabled = Boolean(req.body.enabled); if (Array.isArray(req.body.members)) group.members = [...new Set(req.body.members)].filter(id => users.some(user => user.id === id)); await saveGroups(); recordActivity("Group “" + group.name + "” updated."); res.json(group); } catch (error) { next(error); } });
|
||
app.delete("/api/groups/:id", async (req, res, next) => { try { if (req.user.role !== "administrator") return res.status(403).json({ error: "Administrator access is required." }); const index = groups.findIndex(value => value.id === req.params.id); if (index < 0) return res.status(404).json({ error: "Group not found." }); const [group] = groups.splice(index, 1); await saveGroups(); recordActivity("Group “" + group.name + "” deleted."); res.status(204).end(); } catch (error) { next(error); } });
|
||
app.get("/api/settings", (req, res) => req.user.role === "administrator" ? res.json({ ...settings, backupDirectory: backupsDir }) : res.status(403).json({ error: "Administrator access is required." }));
|
||
app.post("/api/settings/verify-admin", async (req, res, next) => { try { if (req.user.role !== "administrator") return res.status(403).json({ error:"Administrator access is required." }); if (String(req.body.username || "").trim().toLowerCase() !== String(req.user.username || "").toLowerCase() || !await passwordMatches(String(req.body.password || ""), req.user.password)) return res.status(422).json({ error:"Administrator username or password is incorrect." }); res.json({ ok:true }); } catch (error) { next(error); } });
|
||
app.post("/api/settings/verify-username", (req, res) => { if (req.user.role !== "administrator") return res.status(403).json({ error:"Administrator access is required." }); const username = String(req.body.username || "").trim().toLowerCase(); res.json({ valid: Boolean(username && username === String(req.user.username || "").toLowerCase()) }); });
|
||
app.get("/api/dashboard", async (req, res, next) => {
|
||
try { res.json(await dashboardSnapshot()); }
|
||
catch (error) { next(error); }
|
||
});
|
||
app.get("/api/certificates", async (req, res, next) => {
|
||
try { res.json(await certificateInventory()); }
|
||
catch (error) { next(error); }
|
||
});
|
||
app.post("/api/health/check", async (req, res, next) => {
|
||
try { await checkAllProxies(); res.json({ dashboard: await dashboardSnapshot(), certificates: await certificateInventory(), readiness: await domainReadiness() }); }
|
||
catch (error) { next(error); }
|
||
});
|
||
app.get("/api/readiness", async (req, res, next) => { try { res.json({ checkedAt: new Date().toISOString(), routes: await domainReadiness() }); } catch (error) { next(error); } });
|
||
app.get("/api/support-report", async (req, res, next) => {
|
||
try {
|
||
if (req.user.role !== "administrator") return res.status(403).json({ error: "Administrator access is required." });
|
||
const certificateReport = await certificateInventory();
|
||
certificateReport.latestError = certificateReport.latestError ? { present:true, at:certificateReport.latestError.at } : null;
|
||
const report = { product: "Site Gateway", generatedAt: new Date().toISOString(), version: appVersion, caddyVersion, nodeVersion: process.version, storage: { engine: "SQLite", integrity: storage.integrity() }, gateway: { healthy: !gatewayError, lastReload: lastGatewayReload }, routes: { hosted: sites.map(({ id,name,domain,tls,enabled,port }) => ({ id,name,domain,tls,enabled,port })), proxies: proxies.map(({ id,name,domain,tls,enabled,target,healthEnabled,healthExpected }) => ({ id,name,domain,tls,enabled,target,healthEnabled,healthExpected })), redirects: redirects.map(({ id,name,domain,tls,enabled,code }) => ({ id,name,domain,tls,enabled,code })) }, certificates: certificateReport, readiness: await domainReadiness(), recentEvents: recentActivity.slice(0,20).map(item => ({ at:item.at, status:item.status, message:item.status === "error" ? "Operational error recorded; review the protected in-app event log for details." : item.message })) };
|
||
res.setHeader("Content-Disposition", `attachment; filename="site-gateway-support-${new Date().toISOString().slice(0,10)}.json"`); res.type("json").send(JSON.stringify(report, null, 2));
|
||
} catch (error) { next(error); }
|
||
});
|
||
app.get("/api/upstreams", (req, res) => res.json(proxies.map(publicProxy)));
|
||
app.post("/api/upstreams/check", async (req, res, next) => {
|
||
try { res.json(await checkAllProxies()); }
|
||
catch (error) { next(error); }
|
||
});
|
||
app.get("/api/logs", async (req, res, next) => {
|
||
try {
|
||
const limit = Math.min(Math.max(Number.parseInt(req.query.limit, 10) || 100, 1), 250);
|
||
const host = normalizeDomain(req.query.host);
|
||
res.json({ entries: storage.listAccessEvents(limit, host), hosts: [...new Set([...sites, ...proxies, ...redirects].flatMap(item => normalizeDomains(item.domain, item.domains)))].sort(), activity: recentActivity });
|
||
} catch (error) { next(error); }
|
||
});
|
||
app.get("/api/performance", (req, res, next) => {
|
||
try {
|
||
const host = normalizeDomain(req.query.host);
|
||
const hours = Math.min(Math.max(Number.parseInt(req.query.hours, 10) || 6, 1), 168);
|
||
const bucketMinutes = hours > 24 ? 60 : 15;
|
||
const breakdownByHost = new Map();
|
||
for (const row of storage.performanceErrorBreakdown()) { if (!breakdownByHost.has(row.host)) breakdownByHost.set(row.host, []); breakdownByHost.get(row.host).push({ status: row.status, count: row.count }); }
|
||
res.json({
|
||
checkedAt: new Date().toISOString(),
|
||
liveRequests: storage.performanceLiveCount(60),
|
||
routes: storage.performanceRoutes().map(row => ({ host: row.host, hourRequests: row.hourRequests || 0, hourErrors: row.hourErrors || 0, hourAvgMs: row.hourAvgMs != null ? Math.round(row.hourAvgMs) : null, dayRequests: row.dayRequests || 0, dayErrors: row.dayErrors || 0, dayAvgMs: row.dayAvgMs != null ? Math.round(row.dayAvgMs) : null, errorBreakdown: (breakdownByHost.get(row.host) || []).slice(0, 3) })),
|
||
trend: storage.performanceTrend(host, hours, bucketMinutes),
|
||
hosts: [...new Set([...sites, ...proxies, ...redirects].flatMap(item => normalizeDomains(item.domain, item.domains)))].sort()
|
||
});
|
||
} catch (error) { next(error); }
|
||
});
|
||
app.get("/api/icons/search", async (req, res, next) => {
|
||
try {
|
||
const query = String(req.query.q || "").trim().toLowerCase().slice(0, 80);
|
||
if (query.length < 2) return res.json([]);
|
||
const catalog = await loadIconCatalog();
|
||
const results = Object.entries(catalog).map(([slug, metadata]) => {
|
||
const aliases = metadata.aliases || [];
|
||
const searchText = [slug, ...aliases, ...(metadata.categories || [])].join(" ").toLowerCase();
|
||
const score = slug === query ? 0 : slug.startsWith(query) ? 1 : aliases.some(alias => alias.toLowerCase() === query) ? 2 : searchText.includes(query) ? 3 : 99;
|
||
return { slug, metadata, aliases, score };
|
||
}).filter(item => item.score < 99).sort((left, right) => left.score - right.score || left.slug.localeCompare(right.slug)).slice(0, 30)
|
||
.map(({ slug, aliases }) => ({ slug, label: iconLabel(slug), aliases: aliases.slice(0, 3), preview: `https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/${slug}.svg` }));
|
||
res.json(results);
|
||
} catch (error) { next(error); }
|
||
});
|
||
function entryLabel(item) {
|
||
return item?.name || item?.displayName || item?.username || "item";
|
||
}
|
||
app.put("/api/:kind/:id/icon", async (req, res, next) => {
|
||
try {
|
||
const collection = req.params.kind === "sites" ? sites : req.params.kind === "proxies" ? proxies : req.params.kind === "redirects" ? redirects : req.params.kind === "streams" ? streams : req.params.kind === "access-lists" ? accessLists : req.params.kind === "groups" ? groups : req.params.kind === "users" ? users : null;
|
||
if (!collection) return res.status(404).json({ error: "Entry type not found." });
|
||
const item = collection.find(entry => entry.id === req.params.id);
|
||
if (!item) return res.status(404).json({ error: "Entry not found." });
|
||
if (req.body.url !== undefined) {
|
||
const url = String(req.body.url || "").trim();
|
||
if (!/^https:\/\//i.test(url) || url.length > 2048) return res.status(400).json({ error: "Icon URL must be a valid HTTPS URL under 2048 characters." });
|
||
item.iconSlug = null; item.icon = url;
|
||
if (collection === sites) await saveSites(); else if (collection === proxies) await saveProxies(); else if (collection === redirects) await saveRedirects(); else if (collection === streams) await saveStreams(); else if (collection === groups) await saveGroups(); else if (collection === users) await saveUsers(); else await saveAccessLists();
|
||
recordActivity(`Icon URL updated for “${entryLabel(item)}”.`);
|
||
return res.json(item);
|
||
}
|
||
const slug = String(req.body.slug || "").trim();
|
||
const icon = slug ? await cacheIcon(slug) : null;
|
||
item.iconSlug = slug || null;
|
||
item.icon = icon;
|
||
if (collection === sites) await saveSites(); else if (collection === proxies) await saveProxies(); else if (collection === redirects) await saveRedirects(); else if (collection === streams) await saveStreams(); else if (collection === groups) await saveGroups(); else await saveAccessLists();
|
||
recordActivity(`${slug ? "Icon updated" : "Icon reset"} for “${entryLabel(item)}”.`);
|
||
res.json(item);
|
||
} catch (error) { next(error); }
|
||
});
|
||
app.post("/api/:kind/:id/icon", iconUpload.single("icon"), async (req, res, next) => {
|
||
try {
|
||
const collection = req.params.kind === "sites" ? sites : req.params.kind === "proxies" ? proxies : req.params.kind === "redirects" ? redirects : req.params.kind === "streams" ? streams : req.params.kind === "access-lists" ? accessLists : req.params.kind === "groups" ? groups : req.params.kind === "users" ? users : null;
|
||
if (!collection) return res.status(404).json({ error: "Entry type not found." });
|
||
const item = collection.find(entry => entry.id === req.params.id);
|
||
if (!item) return res.status(404).json({ error: "Entry not found." });
|
||
if (!req.file) return res.status(400).json({ error: "Choose an icon image." });
|
||
if (!/^image\/(png|jpeg|webp|gif|svg\+xml)$/.test(req.file.mimetype)) return res.status(400).json({ error: "Use PNG, JPEG, WebP, GIF, or SVG." });
|
||
const extension = req.file.mimetype === "image/svg+xml" ? "svg" : req.file.mimetype.split("/")[1].replace("jpeg", "jpg");
|
||
const filename = `${req.params.kind}-${item.id}.${extension}`;
|
||
await fsp.rename(req.file.path, path.join(iconsDir, filename));
|
||
item.iconSlug = null; item.icon = `/site-icons/${filename}`;
|
||
if (collection === sites) await saveSites(); else if (collection === proxies) await saveProxies(); else if (collection === redirects) await saveRedirects(); else if (collection === streams) await saveStreams(); else if (collection === groups) await saveGroups(); else if (collection === users) await saveUsers(); else await saveAccessLists();
|
||
recordActivity(`Custom icon uploaded for “${entryLabel(item)}”.`);
|
||
res.json(item);
|
||
} catch (error) { next(error); }
|
||
finally { if (req.file?.path) await fsp.rm(req.file.path, { force: true }).catch(() => {}); }
|
||
});
|
||
app.post("/api/sites", upload.single("files"), async (req, res, next) => {
|
||
try {
|
||
const name = String(req.body.name || "").trim();
|
||
const port = Number.parseInt(req.body.port, 10);
|
||
const domain = normalizeDomain(req.body.domain);
|
||
const domains = normalizeDomains(domain, req.body.domains);
|
||
const tls = ["http", "automatic", "internal"].includes(req.body.tls) ? req.body.tls : "automatic";
|
||
const hsts = req.body.hsts === "true";
|
||
const id = `${slugify(name) || "site"}-${crypto.randomBytes(3).toString("hex")}`;
|
||
if (!name) throw Object.assign(new Error("Site name is required."), { status: 400 });
|
||
const portError = validatePort(port);
|
||
if (portError) throw Object.assign(new Error(portError), { status: 400 });
|
||
const domainError = validateDomains(domains);
|
||
if (domainError) throw Object.assign(new Error(domainError), { status: 400 });
|
||
if (!req.file) throw Object.assign(new Error("Choose a ZIP file or index.html."), { status: 400 });
|
||
const accessListId = String(req.body.accessListId || "");
|
||
const site = { id, name, port, domain, domains, accessListId, tls, hsts, enabled: true, createdAt: new Date().toISOString() };
|
||
applyAdvancedSettings(site, { accessListId, compression: req.body.compression, hstsSubdomains: req.body.hstsSubdomains === "true", customConfig: req.body.customConfig, healthEnabled: req.body.healthEnabled === true || (typeof req.body.healthEnabled === "string" && req.body.healthEnabled.toLowerCase() === "true"), healthPath: req.body.healthPath, healthMethod: req.body.healthMethod, healthExpected: req.body.healthExpected, healthTimeoutSeconds: req.body.healthTimeoutSeconds, healthRetries: req.body.healthRetries });
|
||
await installUpload(site, req.file);
|
||
sites.push(site);
|
||
try { await startSite(site); } catch (error) { sites = sites.filter(item => item.id !== site.id); await fsp.rm(path.join(sitesDir, site.id), { recursive: true, force: true }); throw Object.assign(new Error(`Could not start the hosted site on port ${port}: ${error.message}`), { status: 409 }); }
|
||
await syncCaddy();
|
||
await saveSites();
|
||
recordActivity(`Hosted site “${site.name}” created.`);
|
||
res.status(201).json(publicSite(site));
|
||
} catch (error) {
|
||
if (req.file) await fsp.rm(req.file.path, { force: true });
|
||
next(error);
|
||
}
|
||
});
|
||
app.post("/api/sites/:id/toggle", async (req, res, next) => {
|
||
try {
|
||
const site = sites.find(item => item.id === req.params.id);
|
||
if (!site) return res.status(404).json({ error: "Site not found." });
|
||
site.enabled = !site.enabled;
|
||
await restartSite(site);
|
||
await syncCaddy();
|
||
await saveSites();
|
||
recordActivity(`Hosted site “${site.name}” ${site.enabled ? "enabled" : "disabled"}.`);
|
||
res.json(publicSite(site));
|
||
} catch (error) { next(error); }
|
||
});
|
||
app.post("/api/sites/:id/files", upload.single("files"), async (req, res, next) => {
|
||
try {
|
||
const site = sites.find(item => item.id === req.params.id);
|
||
if (!site) return res.status(404).json({ error: "Site not found." });
|
||
if (!req.file) return res.status(400).json({ error: "Choose a ZIP file or index.html." });
|
||
await installUpload(site, req.file);
|
||
await syncCaddy();
|
||
recordActivity(`Files replaced for “${site.name}”.`);
|
||
res.json(publicSite(site));
|
||
} catch (error) { next(error); }
|
||
});
|
||
app.delete("/api/sites/:id", async (req, res, next) => {
|
||
try {
|
||
const index = sites.findIndex(item => item.id === req.params.id);
|
||
if (index < 0) return res.status(404).json({ error: "Site not found." });
|
||
const [site] = sites.splice(index, 1);
|
||
await stopSite(site.id);
|
||
await syncCaddy();
|
||
await fsp.rm(path.join(sitesDir, site.id), { recursive: true, force: true });
|
||
await pruneOrphanedCertificates(normalizeDomains(site.domain, site.domains));
|
||
await saveSites();
|
||
recordActivity(`Hosted site “${site.name}” deleted.`);
|
||
res.status(204).end();
|
||
} catch (error) { next(error); }
|
||
});
|
||
app.patch("/api/sites/:id", async (req, res, next) => {
|
||
try {
|
||
const site = sites.find(item => item.id === req.params.id);
|
||
if (!site) return res.status(404).json({ error: "Site not found." });
|
||
const previousDomains = normalizeDomains(site.domain, site.domains);
|
||
const domain = normalizeDomain(req.body.domain);
|
||
const domains = normalizeDomains(domain, req.body.domains !== undefined ? req.body.domains : site.domains);
|
||
const domainError = validateDomains(domains, site.id);
|
||
if (domainError) return res.status(400).json({ error: domainError });
|
||
site.domain = domain; site.domains = domains;
|
||
site.tls = ["http", "automatic", "internal"].includes(req.body.tls) ? req.body.tls : "automatic";
|
||
site.hsts = req.body.hsts === true;
|
||
applyAdvancedSettings(site, { accessListId: req.body.accessListId, compression: req.body.compression, hstsSubdomains: req.body.hstsSubdomains, requestHeaders: req.body.requestHeaders, responseHeaders: req.body.responseHeaders, customConfig: req.body.customConfig, healthEnabled: req.body.healthEnabled, healthPath: req.body.healthPath, healthMethod: req.body.healthMethod, healthExpected: req.body.healthExpected, healthTimeoutSeconds: req.body.healthTimeoutSeconds, healthRetries: req.body.healthRetries });
|
||
if (site.healthEnabled === false) upstreamHealth.set(site.id, { status: "unmonitored", checkedAt: null, history: [] });
|
||
else { upstreamHealth.set(site.id, { status: "pending", checkedAt: null, history: [] }); checkProxy({ ...site, target: `http://127.0.0.1:${site.port}` }).catch(error => console.warn("Hosted site health check failed:", error.message)); }
|
||
await syncCaddy();
|
||
await pruneOrphanedCertificates(previousDomains);
|
||
await saveSites();
|
||
recordActivity(`Gateway settings updated for “${site.name}”.`);
|
||
res.json(publicSite(site));
|
||
} catch (error) { next(error); }
|
||
});
|
||
app.post("/api/proxies", async (req, res, next) => {
|
||
try {
|
||
const name = String(req.body.name || "").trim();
|
||
const domain = normalizeDomain(req.body.domain);
|
||
const domains = normalizeDomains(domain, req.body.domains);
|
||
if (!name) return res.status(400).json({ error: "Proxy name is required." });
|
||
const domainError = validateDomains(domains);
|
||
if (domainError || !domain) return res.status(400).json({ error: domainError || "Domain is required." });
|
||
const proxy = {
|
||
id: `${slugify(name) || "proxy"}-${crypto.randomBytes(3).toString("hex")}`,
|
||
name,
|
||
domain, domains,
|
||
target: validateTarget(req.body.target),
|
||
tls: ["http", "automatic", "internal"].includes(req.body.tls) ? req.body.tls : "automatic",
|
||
hsts: req.body.hsts === true,
|
||
enabled: true,
|
||
createdAt: new Date().toISOString()
|
||
};
|
||
applyAdvancedSettings(proxy, req.body);
|
||
if (proxy.healthEnabled === false) upstreamHealth.set(proxy.id, { status: "unmonitored", checkedAt: null, history: [] });
|
||
else { upstreamHealth.set(proxy.id, { status: "pending", checkedAt: null, history: [] }); checkProxy(proxy).catch(error => console.warn("Proxy health check failed:", error.message)); }
|
||
proxies.push(proxy);
|
||
await syncCaddy();
|
||
await saveProxies();
|
||
recordActivity(`Proxy host “${proxy.name}” created.`);
|
||
res.status(201).json(publicProxy(proxy));
|
||
} catch (error) { next(error); }
|
||
});
|
||
app.patch("/api/proxies/:id", async (req, res, next) => {
|
||
try {
|
||
const proxy = proxies.find(item => item.id === req.params.id);
|
||
if (!proxy) return res.status(404).json({ error: "Proxy host not found." });
|
||
const previousDomains = normalizeDomains(proxy.domain, proxy.domains);
|
||
if (req.body.domain !== undefined) {
|
||
const domain = normalizeDomain(req.body.domain);
|
||
const domains = normalizeDomains(domain, req.body.domains !== undefined ? req.body.domains : proxy.domains);
|
||
const domainError = validateDomains(domains, proxy.id);
|
||
if (domainError || !domain) return res.status(400).json({ error: domainError || "Domain is required." });
|
||
proxy.domain = domain; proxy.domains = domains;
|
||
}
|
||
if (req.body.domains !== undefined && req.body.domain === undefined) { const domains = normalizeDomains(proxy.domain, req.body.domains); const domainError = validateDomains(domains, proxy.id); if (domainError) return res.status(400).json({ error: domainError }); proxy.domains = domains; }
|
||
if (req.body.name !== undefined) {
|
||
const name = String(req.body.name).trim();
|
||
if (!name) return res.status(400).json({ error: "Proxy name is required." });
|
||
proxy.name = name;
|
||
}
|
||
if (req.body.target !== undefined) proxy.target = validateTarget(req.body.target);
|
||
if (req.body.tls !== undefined) proxy.tls = req.body.tls === "custom" && proxy.certificatePath && proxy.keyPath ? "custom" : ["http", "automatic", "internal"].includes(req.body.tls) ? req.body.tls : proxy.tls;
|
||
if (req.body.hsts !== undefined) proxy.hsts = req.body.hsts === true;
|
||
applyAdvancedSettings(proxy, req.body);
|
||
// Mirror the Hosted Site PATCH handler: react immediately instead of waiting on the 60s
|
||
// background checkAllProxies() timer or a page refresh to pick up a Monitor toggle/edit.
|
||
if (proxy.healthEnabled === false) upstreamHealth.set(proxy.id, { status: "unmonitored", checkedAt: null, history: [] });
|
||
else { upstreamHealth.set(proxy.id, { status: "pending", checkedAt: null, history: [] }); checkProxy(proxy).catch(error => console.warn("Proxy health check failed:", error.message)); }
|
||
await syncCaddy();
|
||
await pruneOrphanedCertificates(previousDomains);
|
||
await saveProxies();
|
||
recordActivity(`Proxy host “${proxy.name}” updated.`);
|
||
res.json(publicProxy(proxy));
|
||
} catch (error) { next(error); }
|
||
});
|
||
app.post("/api/proxies/:id/certificate", certificateUpload.fields([{ name: "certificate", maxCount: 1 }, { name: "privateKey", maxCount: 1 }]), async (req, res, next) => {
|
||
const files = Object.values(req.files || {}).flat();
|
||
try {
|
||
const proxy = proxies.find(item => item.id === req.params.id); if (!proxy) return res.status(404).json({ error: "Proxy host not found." });
|
||
const certificateFile = req.files?.certificate?.[0], keyFile = req.files?.privateKey?.[0];
|
||
if (!certificateFile || !keyFile) return res.status(400).json({ error: "Choose both the PEM certificate and private key." });
|
||
const certificatePem = await fsp.readFile(certificateFile.path, "utf8"), keyPem = await fsp.readFile(keyFile.path, "utf8");
|
||
const certificate = new crypto.X509Certificate(certificatePem), privateKey = crypto.createPrivateKey(keyPem), publicFromKey = crypto.createPublicKey(privateKey);
|
||
const certificatePublic = certificate.publicKey.export({ type: "spki", format: "der" }), suppliedPublic = publicFromKey.export({ type: "spki", format: "der" });
|
||
if (!certificatePublic.equals(suppliedPublic)) return res.status(400).json({ error: "The private key does not match the certificate." });
|
||
const certificateDomains = normalizeDomains(proxy.domain, proxy.domains); if (!certificateDomains.every(domain => certificate.checkHost(domain))) return res.status(400).json({ error: "The certificate must cover the primary domain and every additional domain." });
|
||
const destination = path.join(customCertificatesDir, proxy.id); await fsp.mkdir(destination, { recursive: true });
|
||
const certificatePath = path.join(destination, "certificate.pem"), keyPath = path.join(destination, "private-key.pem");
|
||
await fsp.writeFile(certificatePath, certificatePem, { mode: 0o600 }); await fsp.writeFile(keyPath, keyPem, { mode: 0o600 });
|
||
proxy.tls = "custom"; proxy.certificatePath = certificatePath; proxy.keyPath = keyPath; await syncCaddy(); await saveProxies(); recordActivity(`Custom certificate installed for “${proxy.name}”.`); res.json(publicProxy(proxy));
|
||
} catch (error) { next(Object.assign(new Error(error.message || "Could not read that certificate."), { status: error.status || 400 })); }
|
||
finally { await Promise.all(files.map(file => fsp.rm(file.path, { force: true }))); }
|
||
});
|
||
app.post("/api/proxies/:id/toggle", async (req, res, next) => {
|
||
try {
|
||
const proxy = proxies.find(item => item.id === req.params.id);
|
||
if (!proxy) return res.status(404).json({ error: "Proxy host not found." });
|
||
proxy.enabled = !proxy.enabled;
|
||
await syncCaddy();
|
||
await saveProxies();
|
||
recordActivity(`Proxy host “${proxy.name}” ${proxy.enabled ? "enabled" : "disabled"}.`);
|
||
res.json(publicProxy(proxy));
|
||
} catch (error) { next(error); }
|
||
});
|
||
app.delete("/api/proxies/:id", async (req, res, next) => {
|
||
try {
|
||
const index = proxies.findIndex(item => item.id === req.params.id);
|
||
if (index < 0) return res.status(404).json({ error: "Proxy host not found." });
|
||
const [proxy] = proxies.splice(index, 1);
|
||
await syncCaddy();
|
||
await fsp.rm(path.join(customCertificatesDir, proxy.id), { recursive: true, force: true }).catch(() => {});
|
||
await pruneOrphanedCertificates(normalizeDomains(proxy.domain, proxy.domains));
|
||
await saveProxies();
|
||
recordActivity(`Proxy host “${proxy.name}” deleted.`);
|
||
res.status(204).end();
|
||
} catch (error) { next(error); }
|
||
});
|
||
|
||
app.post("/api/access-lists", async (req, res, next) => {
|
||
try {
|
||
const name = String(req.body.name || "").trim();
|
||
if (!name || name.length > 80) return res.status(400).json({ error: "Access List name is required and must be 80 characters or fewer." });
|
||
const networks = String(req.body.networks || "").split(/[\n,]+/).map(value => value.trim()).filter(Boolean);
|
||
const deniedNetworks = String(req.body.deniedNetworks || "").split(/[\n,]+/).map(value => value.trim()).filter(Boolean);
|
||
if (networks.some(value => !/^(?:private_ranges|(?:\d{1,3}\.){3}\d{1,3}(?:\/\d{1,2})?|[0-9a-f:]+(?:\/\d{1,3})?)$/i.test(value))) return res.status(400).json({ error: "Enter IP addresses, CIDR ranges, or private_ranges, one per line." });
|
||
if (deniedNetworks.some(value => !/^(?:private_ranges|(?:\d{1,3}\.){3}\d{1,3}(?:\/\d{1,2})?|[0-9a-f:]+(?:\/\d{1,3})?)$/i.test(value))) return res.status(400).json({ error: "Enter valid denied IP addresses or CIDR ranges." });
|
||
const credentials = [];
|
||
for (const entry of Array.isArray(req.body.credentials) ? req.body.credentials.slice(0, 25) : []) {
|
||
const username = String(entry.username || "").trim(); const password = String(entry.password || "");
|
||
if (!/^[A-Za-z0-9._-]{1,64}$/.test(username) || password.length < 8) return res.status(400).json({ error: "Access usernames must be valid and passwords must contain at least 8 characters." });
|
||
const { stdout } = await execFileAsync("caddy", ["hash-password", "--plaintext", password]);
|
||
credentials.push({ username, hash: stdout.trim(), password: await passwordRecord(password) });
|
||
}
|
||
if (!networks.length && !deniedNetworks.length && !credentials.length) return res.status(400).json({ error: "Add at least one network rule or login." });
|
||
const selectedGroups = Array.isArray(req.body.groups) ? [...new Set(req.body.groups)].filter(id => groups.some(group => group.id === id && group.enabled !== false)) : [];
|
||
const item = { id: `access-${crypto.randomBytes(4).toString("hex")}`, name, networks, deniedNetworks, credentials, groups: selectedGroups, enabled: true, createdAt: new Date().toISOString() };
|
||
accessLists.push(item); await syncCaddy(); await saveAccessLists(); recordActivity(`Access List “${name}” created.`);
|
||
res.status(201).json({ ...item, credentials: credentials.map(({ username }) => ({ username })) });
|
||
} catch (error) { next(error); }
|
||
});
|
||
app.patch("/api/access-lists/:id", async (req, res, next) => {
|
||
try {
|
||
const item = accessLists.find(value => value.id === req.params.id); if (!item) return res.status(404).json({ error: "Access List not found." });
|
||
if (req.body.enabled !== undefined) item.enabled = Boolean(req.body.enabled);
|
||
if (req.body.name) item.name = String(req.body.name).trim().slice(0, 80);
|
||
if (req.body.networks !== undefined) {
|
||
const networks = String(req.body.networks || "").split(/[\n,]+/).map(value => value.trim()).filter(Boolean);
|
||
if (networks.some(value => !/^(?:private_ranges|(?:\d{1,3}\.){3}\d{1,3}(?:\/\d{1,2})?|[0-9a-f:]+(?:\/\d{1,3})?)$/i.test(value))) return res.status(400).json({ error: "Enter IP addresses, CIDR ranges, or private_ranges, one per line." });
|
||
item.networks = networks;
|
||
}
|
||
if (req.body.deniedNetworks !== undefined) {
|
||
const deniedNetworks = String(req.body.deniedNetworks || "").split(/[\n,]+/).map(value => value.trim()).filter(Boolean);
|
||
if (deniedNetworks.some(value => !/^(?:private_ranges|(?:\d{1,3}\.){3}\d{1,3}(?:\/\d{1,2})?|[0-9a-f:]+(?:\/\d{1,3})?)$/i.test(value))) return res.status(400).json({ error: "Enter valid denied IP addresses or CIDR ranges." });
|
||
item.deniedNetworks = deniedNetworks;
|
||
}
|
||
if (Array.isArray(req.body.credentials)) {
|
||
const credentials = [];
|
||
for (const entry of req.body.credentials.slice(0, 25)) { const username = String(entry.username || "").trim(), password = String(entry.password || ""); if (!/^[A-Za-z0-9._-]{1,64}$/.test(username)) return res.status(400).json({ error: "Access usernames must use letters, numbers, dots, underscores, or hyphens." }); if (!password) { const existing = item.credentials?.find(value => value.username === username); if (!existing) return res.status(400).json({ error: `Enter a password for new user ${username}.` }); credentials.push(existing); continue; } if (password.length < 8) return res.status(400).json({ error: "Passwords must contain at least 8 characters." }); const { stdout } = await execFileAsync("caddy", ["hash-password", "--plaintext", password]); credentials.push({ username, hash: stdout.trim(), password: await passwordRecord(password) }); }
|
||
item.credentials = credentials;
|
||
}
|
||
if (!(item.networks || []).length && !(item.deniedNetworks || []).length && !(item.credentials || []).length) return res.status(400).json({ error: "Keep at least one network rule or login." });
|
||
await syncCaddy(); await saveAccessLists(); recordActivity(`Access List “${item.name}” updated.`); res.json({ ...item, credentials: (item.credentials || []).map(({ username }) => ({ username })) });
|
||
} catch (error) { next(error); }
|
||
});
|
||
app.post("/api/access-lists/:id/assignments", async (req, res, next) => {
|
||
try {
|
||
const list = accessLists.find(value => value.id === req.params.id); if (!list) return res.status(404).json({ error: "Access List not found." });
|
||
const collections = { sites, proxies, redirects }; const kind = String(req.body.kind || ""); const collection = collections[kind]; const host = collection?.find(value => value.id === req.body.hostId);
|
||
if (!host) return res.status(404).json({ error: "Host not found." });
|
||
host.accessListId = req.body.assigned === false ? "" : list.id;
|
||
await syncCaddy(); if (kind === "sites") await saveSites(); else if (kind === "proxies") await saveProxies(); else await saveRedirects();
|
||
recordActivity("Access List " + list.name + (host.accessListId ? " assigned to " : " removed from ") + (host.name || host.domain) + ".");
|
||
res.json({ ok: true, accessListId: host.accessListId });
|
||
} catch (error) { next(error); }
|
||
});
|
||
app.delete("/api/access-lists/:id", async (req, res, next) => {
|
||
try {
|
||
if ([...sites, ...proxies, ...redirects].some(item => item.accessListId === req.params.id)) return res.status(409).json({ error: "Remove this Access List from all hosts before deleting it." });
|
||
const index = accessLists.findIndex(item => item.id === req.params.id); if (index < 0) return res.status(404).json({ error: "Access List not found." });
|
||
const [item] = accessLists.splice(index, 1); await saveAccessLists(); recordActivity(`Access List “${item.name}” deleted.`); res.status(204).end();
|
||
} catch (error) { next(error); }
|
||
});
|
||
|
||
app.post("/api/redirects", async (req, res, next) => {
|
||
try {
|
||
const name = String(req.body.name || "").trim(); const domain = normalizeDomain(req.body.domain); const domains = normalizeDomains(domain, req.body.domains); const target = String(req.body.target || "").trim().replace(/\/$/, "");
|
||
const domainError = validateDomains(domains); if (!name || domainError || !domain) return res.status(400).json({ error: domainError || "Name and primary source domain are required." });
|
||
try { const parsed = new URL(target); if (!['http:', 'https:'].includes(parsed.protocol)) throw new Error(); } catch { return res.status(400).json({ error: "Destination must be a complete HTTP or HTTPS URL." }); }
|
||
const item = { id: `redirect-${crypto.randomBytes(4).toString("hex")}`, name, domain, domains, target, code: [301,302,307,308].includes(Number(req.body.code)) ? Number(req.body.code) : 302, preservePath: req.body.preservePath !== false, tls: ["http","automatic","internal"].includes(req.body.tls) ? req.body.tls : "automatic", hsts: Boolean(req.body.hsts), accessListId: String(req.body.accessListId || ""), enabled: true, createdAt: new Date().toISOString() };
|
||
redirects.push(item); await syncCaddy(); await saveRedirects(); recordActivity(`Redirect Host “${name}” created.`); res.status(201).json(item);
|
||
} catch (error) { next(error); }
|
||
});
|
||
app.patch("/api/redirects/:id", async (req, res, next) => {
|
||
try {
|
||
const item = redirects.find(value => value.id === req.params.id); if (!item) return res.status(404).json({ error: "Redirect Host not found." });
|
||
const previousDomains = normalizeDomains(item.domain, item.domains);
|
||
if (req.body.domain !== undefined || req.body.domains !== undefined) { const domain = normalizeDomain(req.body.domain ?? item.domain); const domains = normalizeDomains(domain, req.body.domains !== undefined ? req.body.domains : item.domains); const error = validateDomains(domains, item.id); if (error || !domain) return res.status(400).json({ error: error || "Primary source domain is required." }); item.domain = domain; item.domains = domains; }
|
||
if (req.body.enabled !== undefined) item.enabled = Boolean(req.body.enabled);
|
||
for (const key of ["name","target","accessListId"]) if (req.body[key] !== undefined) item[key] = String(req.body[key]).trim();
|
||
if (req.body.target !== undefined) { try { const parsed = new URL(item.target); if (!['http:','https:'].includes(parsed.protocol)) throw new Error(); } catch { return res.status(400).json({ error: "Destination must be a complete HTTP or HTTPS URL." }); } }
|
||
if (req.body.code !== undefined && [301,302,307,308].includes(Number(req.body.code))) item.code = Number(req.body.code);
|
||
if (req.body.preservePath !== undefined) item.preservePath = Boolean(req.body.preservePath);
|
||
if (req.body.tls !== undefined) item.tls = ["http","automatic","internal"].includes(req.body.tls) ? req.body.tls : item.tls;
|
||
if (req.body.hsts !== undefined) item.hsts = Boolean(req.body.hsts);
|
||
await syncCaddy(); await pruneOrphanedCertificates(previousDomains); await saveRedirects(); recordActivity(`Redirect Host “${item.name}” updated.`); res.json(item);
|
||
} catch (error) { next(error); }
|
||
});
|
||
app.delete("/api/redirects/:id", async (req, res, next) => {
|
||
try { const index = redirects.findIndex(item => item.id === req.params.id); if (index < 0) return res.status(404).json({ error: "Redirect Host not found." }); const [item] = redirects.splice(index, 1); await syncCaddy(); await pruneOrphanedCertificates(normalizeDomains(item.domain, item.domains)); await saveRedirects(); recordActivity(`Redirect Host “${item.name}” deleted.`); res.status(204).end(); } catch (error) { next(error); }
|
||
});
|
||
|
||
app.get("/api/streams", (req, res) => res.json(streams.map(publicStream)));
|
||
app.post("/api/streams", async (req, res, next) => {
|
||
try {
|
||
const name = String(req.body.name || "").trim();
|
||
if (!name) return res.status(400).json({ error: "Name is required." });
|
||
const port = validateStreamPort(req.body.port);
|
||
const portError = streamPortConflict(port); if (portError) return res.status(400).json({ error: portError });
|
||
const target = validateStreamHostPort(req.body.target);
|
||
const tcp = req.body.tcp !== false, udp = req.body.udp === true;
|
||
if (!tcp && !udp) return res.status(400).json({ error: "Enable TCP, UDP, or both." });
|
||
const stream = { id: `stream-${crypto.randomBytes(4).toString("hex")}`, name, port, target, tcp, udp, healthEnabled: req.body.healthEnabled !== false, enabled: true, createdAt: new Date().toISOString() };
|
||
try { await startStream(stream); } catch (error) { return res.status(409).json({ error: `Could not bind port ${port}: ${error.message}` }); }
|
||
streams.push(stream);
|
||
if (stream.healthEnabled === false) upstreamHealth.set(stream.id, { status: "unmonitored", checkedAt: null, history: [] });
|
||
else { upstreamHealth.set(stream.id, { status: "pending", checkedAt: null, history: [] }); checkStream(stream).catch(error => console.warn("Streaming host health check failed:", error.message)); }
|
||
await saveStreams();
|
||
recordActivity(`Streaming host “${stream.name}” created.`);
|
||
res.status(201).json(publicStream(stream));
|
||
} catch (error) { next(error); }
|
||
});
|
||
app.patch("/api/streams/:id", async (req, res, next) => {
|
||
try {
|
||
const stream = streams.find(item => item.id === req.params.id); if (!stream) return res.status(404).json({ error: "Streaming host not found." });
|
||
const next_ = { ...stream };
|
||
if (req.body.name !== undefined) { const name = String(req.body.name).trim(); if (!name) return res.status(400).json({ error: "Name is required." }); next_.name = name; }
|
||
if (req.body.port !== undefined) { const port = validateStreamPort(req.body.port); const portError = streamPortConflict(port, stream.id); if (portError) return res.status(400).json({ error: portError }); next_.port = port; }
|
||
if (req.body.target !== undefined) next_.target = validateStreamHostPort(req.body.target);
|
||
if (req.body.tcp !== undefined) next_.tcp = Boolean(req.body.tcp);
|
||
if (req.body.udp !== undefined) next_.udp = Boolean(req.body.udp);
|
||
if (!next_.tcp && !next_.udp) return res.status(400).json({ error: "Enable TCP, UDP, or both." });
|
||
if (req.body.healthEnabled !== undefined) next_.healthEnabled = req.body.healthEnabled === true || (typeof req.body.healthEnabled === "string" && req.body.healthEnabled.toLowerCase() === "true");
|
||
if (req.body.enabled !== undefined) next_.enabled = Boolean(req.body.enabled);
|
||
const portOrProtocolChanged = next_.port !== stream.port || next_.target !== stream.target || next_.tcp !== stream.tcp || next_.udp !== stream.udp || next_.enabled !== stream.enabled;
|
||
Object.assign(stream, next_);
|
||
if (portOrProtocolChanged) { try { await restartStream(stream); } catch (error) { return res.status(409).json({ error: `Could not bind port ${stream.port}: ${error.message}` }); } }
|
||
if (stream.healthEnabled === false) upstreamHealth.set(stream.id, { status: "unmonitored", checkedAt: null, history: [] });
|
||
else { upstreamHealth.set(stream.id, { status: "pending", checkedAt: null, history: [] }); checkStream(stream).catch(error => console.warn("Streaming host health check failed:", error.message)); }
|
||
await saveStreams();
|
||
recordActivity(`Streaming host “${stream.name}” updated.`);
|
||
res.json(publicStream(stream));
|
||
} catch (error) { next(error); }
|
||
});
|
||
app.post("/api/streams/:id/toggle", async (req, res, next) => {
|
||
try {
|
||
const stream = streams.find(item => item.id === req.params.id); if (!stream) return res.status(404).json({ error: "Streaming host not found." });
|
||
stream.enabled = !stream.enabled;
|
||
try { await restartStream(stream); } catch (error) { stream.enabled = !stream.enabled; return res.status(409).json({ error: `Could not bind port ${stream.port}: ${error.message}` }); }
|
||
if (stream.enabled === false) upstreamHealth.set(stream.id, { status: "unmonitored", checkedAt: null, history: [] });
|
||
await saveStreams();
|
||
recordActivity(`Streaming host “${stream.name}” ${stream.enabled ? "enabled" : "disabled"}.`);
|
||
res.json(publicStream(stream));
|
||
} catch (error) { next(error); }
|
||
});
|
||
app.delete("/api/streams/:id", async (req, res, next) => {
|
||
try {
|
||
const index = streams.findIndex(item => item.id === req.params.id); if (index < 0) return res.status(404).json({ error: "Streaming host not found." });
|
||
const [item] = streams.splice(index, 1);
|
||
await stopStream(item.id); upstreamHealth.delete(item.id);
|
||
await saveStreams();
|
||
recordActivity(`Streaming host “${item.name}” deleted.`);
|
||
res.status(204).end();
|
||
} catch (error) { next(error); }
|
||
});
|
||
|
||
app.patch("/api/settings", async (req, res, next) => {
|
||
try {
|
||
if (req.body.defaultSite) {
|
||
const value = req.body.defaultSite; const mode = ["welcome","themed404","abort","redirect","custom"].includes(value.mode) ? value.mode : "themed404";
|
||
settings.defaultSite = { mode, redirectUrl: String(value.redirectUrl || "").trim(), redirectCode: [301,302,307,308].includes(Number(value.redirectCode)) ? Number(value.redirectCode) : 302, preservePath: value.preservePath !== false, title: String(value.title || "").slice(0, 100), message: String(value.message || "").slice(0, 500), customHtml: String(value.customHtml || "").slice(0, 250000) };
|
||
}
|
||
if (req.body.backups) settings.backups = { ...settings.backups, ...req.body.backups, hour: Math.min(Math.max(Number(req.body.backups.hour) || 0, 0), 23), retention: Math.min(Math.max(Number(req.body.backups.retention) || 7, 1), 100) };
|
||
if (req.body.certificateHealth) {
|
||
const warningDays = Math.min(Math.max(Number(req.body.certificateHealth.warningDays) || 30, 8), 120);
|
||
const criticalDays = Math.min(Math.max(Number(req.body.certificateHealth.criticalDays) || 7, 1), warningDays - 1);
|
||
settings.certificateHealth = { warningDays, criticalDays, staleMinutes: Math.min(Math.max(Number(req.body.certificateHealth.staleMinutes) || 10, 2), 1440) };
|
||
}
|
||
if (req.body.logsRetention) {
|
||
const value = req.body.logsRetention;
|
||
const days = key => Math.min(Math.max(Number(value[key]) || 30, 7), 3650);
|
||
settings.logsRetention = { ...settings.logsRetention, accessDays: days("accessDays"), activityDays: days("activityDays"), auditDays: days("auditDays"), certificateDays: days("certificateDays"), securityDays: days("securityDays"), pruningEnabled: value.pruningEnabled === true };
|
||
}
|
||
await syncCaddy(); await saveSettings(); recordActivity("Administration settings updated."); res.json({ ...settings, backupDirectory: backupsDir });
|
||
} catch (error) { next(error); }
|
||
});
|
||
app.post("/api/logs/prune", async (req, res, next) => { try { if (req.user.role !== "administrator") return res.status(403).json({ error: "Administrator access is required." }); if (!settings.logsRetention?.pruningEnabled) return res.status(409).json({ error: "Automatic pruning is disabled. Enable it and save the retention policy first." }); const mode = req.body?.mode === "scheduled" ? "scheduled" : "manual"; const stamp = new Date().toISOString().replace(/[:.]/g, "-"); const snapshot = path.join(backupsDir, `pre-prune-${stamp}.sqlite`); storage.backupTo(snapshot); const counts = storage.pruneEvents(settings.logsRetention); settings.logsRetention = { ...settings.logsRetention, lastRunAt: new Date().toISOString(), lastRunMode: mode, lastRunCounts: counts, lastRunSnapshot: snapshot }; await saveSettings(); recordActivity(`${mode === "scheduled" ? "Scheduled" : "Manual"} log pruning completed: ${Object.values(counts).reduce((sum, value) => sum + value, 0)} records removed.`); res.json({ counts, snapshot }); } catch (error) { next(error); } });
|
||
app.get("/api/logs/prune/preview", (req, res, next) => { try { if (req.user.role !== "administrator") return res.status(403).json({ error: "Administrator access is required." }); res.json({ enabled: settings.logsRetention?.pruningEnabled === true, counts: storage.previewPruneEvents(settings.logsRetention || {}) }); } catch (error) { next(error); } });
|
||
app.get("/api/logs/download", async (req, res, next) => { try { if (req.user.role !== "administrator") return res.status(403).json({ error: "Administrator access is required." }); const payload = { product: "Site Gateway", generatedAt: new Date().toISOString(), access: storage.listAccessEvents(500), activity: storage.listActivity(500), audit: storage.listAudit({}) }; res.setHeader("Content-Disposition", `attachment; filename="site-gateway-logs-${new Date().toISOString().slice(0, 10)}.json"`); res.json(payload); } catch (error) { next(error); } });
|
||
app.post("/api/settings/reset-defaults", async (req, res, next) => { try { if (req.user.role !== "administrator") return res.status(403).json({ error:"Administrator access is required." }); if (String(req.body.confirmation || "") !== "RESTORE DEFAULT") return res.status(400).json({ error:"Type RESTORE DEFAULT exactly to continue." }); if (String(req.body.username || "").trim().toLowerCase() !== String(req.user.username || "").toLowerCase() || !await passwordMatches(String(req.body.password || ""), req.user.password)) return res.status(401).json({ error:"Administrator credentials were not accepted." }); settings.defaultSite = { mode:"themed404", redirectUrl:"", redirectCode:302, preservePath:true, title:"Route not found", message:"The gateway is responding, but this address has not been configured.", customHtml:"" }; settings.backups = { enabled:false, frequency:"daily", hour:2, retention:7, type:"complete", includeLogs:false, encrypt:false, lastRunAt:null, lastStatus:null }; settings.certificateHealth = { warningDays:30, criticalDays:7, staleMinutes:10 }; await saveSettings(); recordActivity("Gateway preferences restored to defaults."); res.json({ ...settings, backupDirectory:backupsDir }); } catch (error) { next(error); } });
|
||
app.post("/api/factory-reset", async (req, res, next) => { try { if (String(req.body.confirmation || "") !== "FACTORY RESET") return res.status(400).json({ error:"Type FACTORY RESET exactly to continue." }); if (String(req.body.username || "").toLowerCase() !== String(req.user.username || "").toLowerCase() || !await passwordMatches(String(req.body.password || ""), req.user.password)) return res.status(401).json({ error:"Administrator credentials were not accepted." }); await Promise.all([...activeServers.keys()].map(stopSite)); await Promise.all([...activeStreams.keys()].map(stopStream)); storage.close(); for (const directory of [sitesDir, uploadDir, caddyDir, iconsDir, logsDir, backupsDir, defaultSiteDir, certificatesRoot, path.join(dataDir,"database")]) await clearDirectoryContents(directory); storage = await openStorage(dataDir, backupsDir); sites = []; proxies = []; users = []; redirects = []; streams = []; accessLists = []; groups = []; settings = {}; recentActivity.splice(0); await loadSites(); await syncCaddy(); res.setHeader("Set-Cookie", "webserver_session=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0"); res.status(202).json({ ok:true }); } catch (error) { next(error); } });
|
||
app.use("/api/backups", (req, res, next) => req.user.role === "administrator" ? next() : res.status(403).json({ error: "Administrator access is required." }));
|
||
app.get("/api/backups", async (req, res, next) => { try { res.json(await listBackups()); } catch (error) { next(error); } });
|
||
app.post("/api/backups", async (req, res, next) => {
|
||
try { const backup = await createBackup(req.body.type, Boolean(req.body.includeLogs), "site-gateway-backup", String(req.body.password || "")); res.status(201).json(backup); } catch (error) { next(error); }
|
||
});
|
||
app.post("/api/backups/import", upload.single("backup"), async (req, res, next) => {
|
||
try {
|
||
if (!req.file) return res.status(400).json({ error: "Choose a .sgbackup file." });
|
||
const { zip } = await openBackup(req.file.path, String(req.body.password || "")); const manifest = JSON.parse(zip.readAsText("manifest.json") || "null");
|
||
if (!manifest || manifest.product !== "Site Gateway" || ![1,2].includes(manifest.format)) throw Object.assign(new Error("This is not a supported Site Gateway backup."), { status: 400 });
|
||
const filename = `imported-${new Date().toISOString().replace(/[:.]/g, "-")}.sgbackup`; await fsp.rename(req.file.path, path.join(backupsDir, filename));
|
||
recordActivity(`Backup imported from this computer.`); res.status(201).json({ filename, manifest });
|
||
} catch (error) { if (req.file) await fsp.rm(req.file.path, { force: true }); next(error); }
|
||
});
|
||
app.get("/api/backups/:filename/download", async (req, res, next) => {
|
||
try { const filename = path.basename(req.params.filename); const file = path.join(backupsDir, filename); await fsp.access(file); res.download(file, filename); } catch (error) { next(Object.assign(new Error("Backup not found."), { status: 404 })); }
|
||
});
|
||
app.post("/api/backups/:filename/restore", async (req, res, next) => {
|
||
try { res.json({ ok: true, manifest: await restoreBackup(path.basename(req.params.filename), String(req.body.password || "")) }); } catch (error) { next(error); }
|
||
});
|
||
app.delete("/api/backups/:filename", async (req, res, next) => {
|
||
try { const filename = path.basename(req.params.filename); if (!filename.endsWith(".sgbackup")) return res.status(400).json({ error: "Invalid backup." }); await fsp.rm(path.join(backupsDir, filename)); recordActivity(`Backup ${filename} deleted.`); res.status(204).end(); } catch (error) { next(error); }
|
||
});
|
||
function humanizeGatewayActivityError(message) { const text = String(message || "Unexpected gateway error"); if (/upstream address scheme is HTTP but transport is configured for HTTP\+TLS/i.test(text)) return "Gateway configuration rejected: HTTP upstream cannot use HTTPS transport. Disable upstream TLS verification or change the upstream URL to HTTPS."; if (/upstream address scheme is HTTPS but transport is configured for plain HTTP/i.test(text)) return "Gateway configuration rejected: HTTPS upstream requires HTTPS transport settings. Change the upstream URL or transport setting."; if (/duplicate.*address|already.*site address/i.test(text)) return "Gateway configuration rejected: This hostname or address is already used by another host. Choose a unique hostname and port."; if (/dial tcp|no such host|lookup .* no such host|upstream.*(invalid|malformed)/i.test(text)) return "Gateway configuration rejected: The upstream address could not be reached or is invalid. Check the hostname, IP address, and port."; if (/invalid hostname|host name.*invalid|malformed.*host/i.test(text)) return "Gateway configuration rejected: The hostname is not valid. Use a valid domain name without a protocol or path."; if (/unrecognized directive|unknown directive|parsing caddyfile tokens/i.test(text)) return "Gateway configuration rejected: The gateway configuration contains an unsupported or malformed directive. Check the selected host settings."; if (/certificate|tls.*(config|handshake)|no certificate/i.test(text)) return "Gateway configuration rejected: The TLS certificate configuration is invalid or unavailable. Check the certificate, key, and HTTPS settings."; return text.replace(/^Gateway configuration was rejected:\s*/i, "Gateway configuration rejected: ").replace(/\s+Details:\s+[\s\S]*$/i, ""); }
|
||
const GATEWAY_CONFIG_ROUTE = /^\/api\/(sites|proxies|redirects|streams|access-lists)(\/|$)/i;
|
||
app.use((error, req, res, next) => {
|
||
console.error(error);
|
||
const rawMessage = error.message || "Something went wrong.";
|
||
const isConfigRoute = GATEWAY_CONFIG_ROUTE.test(req.path) && ["PATCH", "POST", "DELETE", "PUT"].includes(req.method);
|
||
let logMessage = rawMessage;
|
||
if (isConfigRoute) {
|
||
const humanized = humanizeGatewayActivityError(rawMessage);
|
||
logMessage = /^Gateway configuration rejected:/i.test(humanized) ? humanized : `Gateway configuration rejected: ${humanized}`;
|
||
}
|
||
recordActivity(`${req.method} ${req.path}: ${logMessage}`, "error");
|
||
res.status(error.status || 500).json({ error: rawMessage });
|
||
});
|
||
|
||
app.listen(adminPort, "0.0.0.0", () => {
|
||
console.log(`Site Gateway dashboard listening on port ${adminPort}`);
|
||
if (adminPassword === "change-this-password") console.warn("WARNING: Change ADMIN_PASSWORD before exposing the dashboard.");
|
||
});
|
||
|
||
setTimeout(() => checkAllProxies().catch(error => console.warn("Initial upstream checks failed:", error.message)), 1500).unref();
|
||
setInterval(() => checkAllProxies().catch(error => console.warn("Upstream checks failed:", error.message)), 60000).unref();
|
||
|
||
async function runScheduledBackup() {
|
||
const schedule = settings.backups || {}; if (!schedule.enabled || Number(schedule.hour) !== new Date().getHours()) return;
|
||
const last = schedule.lastRunAt ? new Date(schedule.lastRunAt) : null; const elapsed = last ? Date.now() - last.getTime() : Infinity;
|
||
const due = schedule.frequency === "monthly" ? elapsed >= 27 * 86400000 : schedule.frequency === "weekly" ? elapsed >= 6 * 86400000 : elapsed >= 20 * 3600000;
|
||
if (!due) return;
|
||
try {
|
||
if (schedule.encrypt && !scheduledBackupPassword) throw new Error("BACKUP_PASSWORD is required for encrypted scheduled backups.");
|
||
await createBackup(schedule.type, Boolean(schedule.includeLogs), "scheduled", schedule.encrypt ? scheduledBackupPassword : "");
|
||
schedule.lastRunAt = new Date().toISOString(); schedule.lastStatus = "ok";
|
||
const backups = (await listBackups()).filter(item => item.filename.startsWith("scheduled-"));
|
||
for (const item of backups.slice(Math.max(Number(schedule.retention) || 7, 1))) await fsp.rm(path.join(backupsDir, item.filename), { force: true });
|
||
} catch (error) { schedule.lastRunAt = new Date().toISOString(); schedule.lastStatus = `error: ${error.message}`; recordActivity(`Scheduled backup failed: ${error.message}`, "error"); }
|
||
await saveSettings();
|
||
}
|
||
setTimeout(() => runScheduledBackup().catch(error => console.warn("Scheduled backup check failed:", error.message)), 5000).unref();
|
||
setInterval(() => runScheduledBackup().catch(error => console.warn("Scheduled backup check failed:", error.message)), 15 * 60000).unref();
|
||
async function runScheduledPruning() { if (!settings.logsRetention?.pruningEnabled || !storage?.pruneEvents) return; try { const stamp = new Date().toISOString().replace(/[:.]/g, "-"); const snapshot = path.join(backupsDir, `pre-prune-${stamp}.sqlite`); storage.backupTo(snapshot); const counts = storage.pruneEvents(settings.logsRetention); settings.logsRetention = { ...settings.logsRetention, lastRunAt: new Date().toISOString(), lastRunMode: "scheduled", lastRunCounts: counts, lastRunSnapshot: snapshot }; await saveSettings(); recordActivity(`Scheduled log pruning completed: ${Object.values(counts).reduce((sum, value) => sum + value, 0)} records removed.`); } catch (error) { recordActivity(`Scheduled log pruning failed: ${error.message}`, "error"); } }
|
||
setInterval(() => runScheduledPruning(), 15 * 60000).unref();
|
||
setTimeout(() => importAccessLogsToSqlite(), 8000).unref();
|
||
setInterval(() => importAccessLogsToSqlite(), 30000).unref();
|
||
|
||
async function checkPublicIp() {
|
||
try {
|
||
const response = await fetch("https://api.ipify.org?format=json", { signal: AbortSignal.timeout(6000), headers: { "user-agent": "Site-Gateway-DDNS-Check/1.0" } });
|
||
if (!response.ok) throw new Error(`IP lookup returned HTTP ${response.status}.`);
|
||
const body = await response.json();
|
||
const address = String(body.ip || "").trim();
|
||
if (!/^\d{1,3}(\.\d{1,3}){3}$/.test(address) && !address.includes(":")) throw new Error("IP lookup returned an unexpected value.");
|
||
const changed = publicIpState.address && publicIpState.address !== address;
|
||
publicIpState = { address, checkedAt: new Date().toISOString(), error: null };
|
||
if (changed) recordActivity(`Public IP address changed to ${address}.`);
|
||
} catch (error) { publicIpState = { ...publicIpState, checkedAt: new Date().toISOString(), error: error.message }; }
|
||
}
|
||
setTimeout(() => checkPublicIp(), 4000).unref();
|
||
setInterval(() => checkPublicIp(), 60 * 60000).unref();
|
||
|
||
async function shutdown() {
|
||
await Promise.all([...activeServers.keys()].map(stopSite));
|
||
await Promise.all([...activeStreams.keys()].map(stopStream));
|
||
try { storage?.close(); } catch { /* Database may already be closed during restore. */ }
|
||
process.exit(0);
|
||
}
|
||
process.on("SIGTERM", shutdown);
|
||
process.on("SIGINT", shutdown);
|