// // Instance routing. Repositories are matched to a configured Gitea instance by // their git remote's host, directly or through a configured host alias for // servers that answer SSH on a different hostname than the web UI. // const config = require("../util/config.js"); const credentials = require("./credentials.js"); const log = require("../util/log.js"); const remotes = require("./remotes.js"); const { GiteaClient } = require("./client.js"); const { GiteaApi } = require("./api.js"); // "ssh.example.com = https://git.example.com", also accepting -> and =>. const ALIAS_SEPARATOR = /\s*(?:=>|->|=)\s*/; class InstanceRouter { constructor() { this.apis = new Map(); this._aliases = null; this._probedHosts = new Set(); } /** Base URLs of every configured instance, default first. */ get baseUrls() { return config.instances(); } apiFor(baseUrl) { const normalized = config.normalizeBaseUrl(baseUrl); if (!normalized) return null; let api = this.apis.get(normalized); if (!api) { api = new GiteaApi(new GiteaClient(normalized, credentials.getToken)); this.apis.set(normalized, api); } return api; } get defaultApi() { const urls = this.baseUrls; return urls.length ? this.apiFor(urls[0]) : null; } /** * Alternate remote hosts mapped onto instance hosts, parsed from the * "remote-host = instance" preference entries. Blank lines, comments, and * malformed entries are ignored rather than breaking discovery. */ get aliases() { if (this._aliases) return this._aliases; const map = new Map(); for (const entry of config.hostAliasEntries()) { const text = String(entry || "").trim(); if (!text || text.startsWith("#")) continue; const parts = text.split(ALIAS_SEPARATOR); if (parts.length !== 2) continue; const from = remotes.hostOf(parts[0]); const to = remotes.hostOf(parts[1]); if (from && to && from !== to) map.set(from, to); } this._aliases = map; return map; } /** * The instance whose host matches `host`, directly or through an alias, or * null when none does. */ baseUrlForHost(host) { if (!host) return null; const target = String(host).toLowerCase(); return ( this._instanceForHost(target) || this._instanceForHost(this.aliases.get(target)) || null ); } _instanceForHost(host) { if (!host) return null; for (const baseUrl of this.baseUrls) { if (remotes.hostOf(baseUrl) === host) return baseUrl; } return null; } apiForHost(host) { const baseUrl = this.baseUrlForHost(host); return baseUrl ? this.apiFor(baseUrl) : null; } /** Drops cached clients and tokens so the next call re-reads preferences. */ invalidate() { this.apis.clear(); this._aliases = null; this._probedHosts.clear(); credentials.invalidate(); } /** * Works out which instance serves `host` by asking each one for the * repository and comparing the clone URLs it reports. Gitea publishes its * SSH hostname in a repository's ssh_url, so a server that answers SSH on a * different name than its web UI identifies itself here. * * Probes each host at most once per session and remembers what it learns. */ async detectHostAlias(host, owner, name) { if (!host || !owner || !name) return null; if (this._probedHosts.has(host)) return null; this._probedHosts.add(host); for (const baseUrl of this.baseUrls) { if (!credentials.hasToken(baseUrl)) continue; let repository; try { repository = await this.apiFor(baseUrl).repository({ owner: owner, name: name }); } catch (error) { log.debug(`${baseUrl} does not serve ${owner}/${name}:`, String(error.message || error)); continue; } const advertised = [repository.ssh_url, repository.clone_url, repository.html_url] .map((url) => remotes.hostOf(url)) .filter(Boolean); if (!advertised.includes(host)) continue; this.rememberAlias(host, baseUrl); log.info(`detected that git remotes on ${host} belong to ${baseUrl}`); return baseUrl; } return null; } /** Records an alias for this session and persists it for the next one. */ rememberAlias(host, baseUrl) { this.aliases.set(remotes.hostOf(host), remotes.hostOf(baseUrl)); config.addHostAlias(host, baseUrl); } } exports.InstanceRouter = InstanceRouter;