fix and update
This commit is contained in:
@@ -1,9 +1,23 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { Controller, Get, ServiceUnavailableException } from '@nestjs/common';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { CoreGrpcService } from '../core-grpc.service';
|
||||
|
||||
@Controller('health')
|
||||
export class HealthController {
|
||||
constructor(private readonly core: CoreGrpcService) {}
|
||||
|
||||
@Get()
|
||||
check() {
|
||||
return { status: 'ok', service: 'api-gateway' };
|
||||
async check() {
|
||||
try {
|
||||
await firstValueFrom(this.core.settings.GetSetting({ key: 'PROJECT_NAME' }));
|
||||
return { status: 'ok', service: 'api-gateway', grpc: 'ok' };
|
||||
} catch {
|
||||
throw new ServiceUnavailableException({
|
||||
status: 'error',
|
||||
service: 'api-gateway',
|
||||
grpc: 'unavailable',
|
||||
message: 'Не удалось связаться с sso-core по gRPC'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,15 @@ import type { Request } from 'express';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { CoreGrpcService } from '../core-grpc.service';
|
||||
import { resolveFrontendUrl, resolveOAuthIssuer } from './oauth-issuer';
|
||||
import {
|
||||
hostsMatch,
|
||||
normalizePublicBaseUrl,
|
||||
preferBrowserReachableBase,
|
||||
readRequestHost,
|
||||
readRequestProto,
|
||||
resolvePublicApiBaseFromRequest,
|
||||
resolvePublicFrontendBaseFromRequest
|
||||
} from './public-url';
|
||||
|
||||
export interface FedcmEndpoints {
|
||||
issuer: string;
|
||||
@@ -16,94 +25,9 @@ export interface FedcmEndpoints {
|
||||
webIdentityUrl: string;
|
||||
}
|
||||
|
||||
function normalizeBaseUrl(url: string) {
|
||||
return url.trim().replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
function readRequestHost(req?: Request): string | undefined {
|
||||
if (!req) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const forwarded = req.headers['x-forwarded-host'];
|
||||
const host = (Array.isArray(forwarded) ? forwarded[0] : forwarded) ?? req.headers.host;
|
||||
return host?.split(',')[0]?.trim() || undefined;
|
||||
}
|
||||
|
||||
function readRequestProto(req?: Request): string {
|
||||
if (!req) {
|
||||
return 'https';
|
||||
}
|
||||
|
||||
const forwarded = req.headers['x-forwarded-proto'];
|
||||
const proto = (Array.isArray(forwarded) ? forwarded[0] : forwarded) ?? req.protocol;
|
||||
return proto?.split(',')[0]?.trim() || 'https';
|
||||
}
|
||||
|
||||
function hostsMatch(a: string, b: string) {
|
||||
if (a === b) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return a === 'localhost' && b === 'localhost';
|
||||
}
|
||||
|
||||
function resolveSameOriginFrontend(storedFrontend: string, req?: Request): string {
|
||||
const host = readRequestHost(req);
|
||||
if (!host) {
|
||||
return storedFrontend;
|
||||
}
|
||||
|
||||
const requestOrigin = normalizeBaseUrl(`${readRequestProto(req)}://${host}`);
|
||||
|
||||
try {
|
||||
const frontendUrl = new URL(storedFrontend);
|
||||
const requestUrl = new URL(requestOrigin);
|
||||
|
||||
if (hostsMatch(frontendUrl.hostname, requestUrl.hostname)) {
|
||||
return requestOrigin;
|
||||
}
|
||||
} catch {
|
||||
return storedFrontend;
|
||||
}
|
||||
|
||||
return storedFrontend;
|
||||
}
|
||||
|
||||
export function resolveSameOriginIssuer(storedIssuer: string, storedFrontend: string, req?: Request): string {
|
||||
const host = readRequestHost(req);
|
||||
if (!host) {
|
||||
return storedIssuer;
|
||||
}
|
||||
|
||||
const requestOrigin = normalizeBaseUrl(`${readRequestProto(req)}://${host}`);
|
||||
const sameOriginApi = `${requestOrigin}/idp-api`;
|
||||
|
||||
try {
|
||||
const issuerUrl = new URL(storedIssuer);
|
||||
const frontendUrl = new URL(storedFrontend);
|
||||
const requestUrl = new URL(requestOrigin);
|
||||
|
||||
const hostMatches =
|
||||
hostsMatch(issuerUrl.hostname, requestUrl.hostname) || hostsMatch(frontendUrl.hostname, requestUrl.hostname);
|
||||
|
||||
if (!hostMatches) {
|
||||
return storedIssuer;
|
||||
}
|
||||
|
||||
if (normalizeBaseUrl(storedIssuer) !== sameOriginApi) {
|
||||
return sameOriginApi;
|
||||
}
|
||||
} catch {
|
||||
return storedIssuer;
|
||||
}
|
||||
|
||||
return storedIssuer;
|
||||
}
|
||||
|
||||
function buildFedcmEndpointUrls(issuer: string, frontendUrl: string) {
|
||||
const base = normalizeBaseUrl(issuer);
|
||||
const front = normalizeBaseUrl(frontendUrl);
|
||||
const base = normalizePublicBaseUrl(issuer);
|
||||
const front = normalizePublicBaseUrl(frontendUrl);
|
||||
|
||||
let webIdentityOrigin = front;
|
||||
try {
|
||||
@@ -123,6 +47,71 @@ function buildFedcmEndpointUrls(issuer: string, frontendUrl: string) {
|
||||
};
|
||||
}
|
||||
|
||||
function resolveSameOriginFrontend(storedFrontend: string, req?: Request): string {
|
||||
const fromRequest = resolvePublicFrontendBaseFromRequest(req);
|
||||
const preferred = preferBrowserReachableBase(storedFrontend, fromRequest);
|
||||
if (preferred !== storedFrontend) {
|
||||
return preferred;
|
||||
}
|
||||
|
||||
const host = readRequestHost(req);
|
||||
if (!host) {
|
||||
return storedFrontend;
|
||||
}
|
||||
|
||||
const requestOrigin = normalizePublicBaseUrl(`${readRequestProto(req)}://${host}`);
|
||||
|
||||
try {
|
||||
const frontendUrl = new URL(storedFrontend);
|
||||
const requestUrl = new URL(requestOrigin);
|
||||
|
||||
if (hostsMatch(frontendUrl.hostname, requestUrl.hostname)) {
|
||||
return requestOrigin;
|
||||
}
|
||||
} catch {
|
||||
return storedFrontend;
|
||||
}
|
||||
|
||||
return storedFrontend;
|
||||
}
|
||||
|
||||
export function resolveSameOriginIssuer(storedIssuer: string, storedFrontend: string, req?: Request): string {
|
||||
const fromRequest = resolvePublicApiBaseFromRequest(req);
|
||||
const preferred = preferBrowserReachableBase(storedIssuer, fromRequest);
|
||||
if (preferred !== storedIssuer) {
|
||||
return preferred;
|
||||
}
|
||||
|
||||
const host = readRequestHost(req);
|
||||
if (!host) {
|
||||
return storedIssuer;
|
||||
}
|
||||
|
||||
const requestOrigin = normalizePublicBaseUrl(`${readRequestProto(req)}://${host}`);
|
||||
const sameOriginApi = `${requestOrigin}/idp-api`;
|
||||
|
||||
try {
|
||||
const issuerUrl = new URL(storedIssuer);
|
||||
const frontendUrl = new URL(storedFrontend);
|
||||
const requestUrl = new URL(requestOrigin);
|
||||
|
||||
const hostMatches =
|
||||
hostsMatch(issuerUrl.hostname, requestUrl.hostname) || hostsMatch(frontendUrl.hostname, requestUrl.hostname);
|
||||
|
||||
if (!hostMatches) {
|
||||
return storedIssuer;
|
||||
}
|
||||
|
||||
if (normalizePublicBaseUrl(storedIssuer) !== sameOriginApi) {
|
||||
return sameOriginApi;
|
||||
}
|
||||
} catch {
|
||||
return fromRequest ?? storedIssuer;
|
||||
}
|
||||
|
||||
return storedIssuer;
|
||||
}
|
||||
|
||||
export async function resolveFedcmEndpoints(core: CoreGrpcService, req?: Request): Promise<FedcmEndpoints> {
|
||||
const storedIssuer = await resolveOAuthIssuer(core);
|
||||
const storedFrontend = await resolveFrontendUrl(core);
|
||||
|
||||
@@ -1,17 +1,35 @@
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { CoreGrpcService } from '../core-grpc.service';
|
||||
import { normalizePublicBaseUrl, isInternalHostname } from './public-url';
|
||||
|
||||
function normalizeBaseUrl(url: string) {
|
||||
return url.trim().replace(/\/+$/, '');
|
||||
return normalizePublicBaseUrl(url);
|
||||
}
|
||||
|
||||
function appendIdpApiPath(base: string) {
|
||||
const normalized = normalizeBaseUrl(base);
|
||||
return normalized.endsWith('/idp-api') ? normalized : `${normalized}/idp-api`;
|
||||
}
|
||||
|
||||
function sanitizeStoredPublicUrl(url: string) {
|
||||
const normalized = normalizeBaseUrl(url);
|
||||
try {
|
||||
if (isInternalHostname(new URL(normalized).hostname)) {
|
||||
return undefined;
|
||||
}
|
||||
} catch {
|
||||
return normalized;
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
export async function resolveOAuthIssuer(core: CoreGrpcService, fallback = 'http://localhost:3000'): Promise<string> {
|
||||
const envIssuer = process.env.PUBLIC_API_URL?.trim();
|
||||
try {
|
||||
const response = (await firstValueFrom(core.settings.GetSetting({ key: 'PUBLIC_API_URL' }))) as { value?: string };
|
||||
const fromDb = response.value?.trim();
|
||||
const fromDb = sanitizeStoredPublicUrl(response.value ?? '');
|
||||
if (fromDb) {
|
||||
return normalizeBaseUrl(fromDb);
|
||||
return fromDb;
|
||||
}
|
||||
} catch {
|
||||
// fallback ниже
|
||||
@@ -26,9 +44,9 @@ export async function resolveOAuthIssuer(core: CoreGrpcService, fallback = 'http
|
||||
const domain = response.value?.trim();
|
||||
if (domain) {
|
||||
if (domain.startsWith('http://') || domain.startsWith('https://')) {
|
||||
return normalizeBaseUrl(domain);
|
||||
return appendIdpApiPath(domain);
|
||||
}
|
||||
return `https://${domain.replace(/^\/+/, '')}`;
|
||||
return appendIdpApiPath(`https://${domain.replace(/^\/+/, '')}`);
|
||||
}
|
||||
} catch {
|
||||
// fallback ниже
|
||||
@@ -45,9 +63,9 @@ export async function resolveFrontendUrl(core: CoreGrpcService, fallback = 'http
|
||||
|
||||
try {
|
||||
const response = (await firstValueFrom(core.settings.GetSetting({ key: 'PUBLIC_FRONTEND_URL' }))) as { value?: string };
|
||||
const fromDb = response.value?.trim();
|
||||
const fromDb = sanitizeStoredPublicUrl(response.value ?? '');
|
||||
if (fromDb) {
|
||||
return normalizeBaseUrl(fromDb);
|
||||
return fromDb;
|
||||
}
|
||||
} catch {
|
||||
// fallback ниже
|
||||
|
||||
122
apps/api-gateway/src/lib/public-url.ts
Normal file
122
apps/api-gateway/src/lib/public-url.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import type { Request } from 'express';
|
||||
|
||||
const INTERNAL_HOSTNAMES = new Set([
|
||||
'api-gateway',
|
||||
'sso-core',
|
||||
'frontend',
|
||||
'docs',
|
||||
'media-ws',
|
||||
'minio',
|
||||
'postgres',
|
||||
'redis',
|
||||
'rabbitmq',
|
||||
'ldap-auth'
|
||||
]);
|
||||
|
||||
export function normalizePublicBaseUrl(url: string) {
|
||||
return url.trim().replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
export function isInternalHostname(hostname: string) {
|
||||
const host = hostname.trim().toLowerCase();
|
||||
if (!host) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (INTERNAL_HOSTNAMES.has(host)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (/^\d+\.\d+\.\d+\.\d+$/.test(host) || host === 'localhost') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Одно слово без точки — типичное имя Docker-сервиса (api-gateway, sso-core).
|
||||
return !host.includes('.');
|
||||
}
|
||||
|
||||
export function isBrowserReachableBaseUrl(url: string) {
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
||||
return false;
|
||||
}
|
||||
return !isInternalHostname(parsed.hostname);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function readRequestHost(req?: Request): string | undefined {
|
||||
if (!req) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const forwarded = req.headers['x-forwarded-host'];
|
||||
const host = (Array.isArray(forwarded) ? forwarded[0] : forwarded) ?? req.headers.host;
|
||||
return host?.split(',')[0]?.trim() || undefined;
|
||||
}
|
||||
|
||||
export function readRequestProto(req?: Request): string {
|
||||
if (!req) {
|
||||
return 'https';
|
||||
}
|
||||
|
||||
const forwarded = req.headers['x-forwarded-proto'];
|
||||
const proto = (Array.isArray(forwarded) ? forwarded[0] : forwarded) ?? req.protocol;
|
||||
return proto?.split(',')[0]?.trim() || 'https';
|
||||
}
|
||||
|
||||
export function resolvePublicOriginFromRequest(req?: Request): string | null {
|
||||
const host = readRequestHost(req);
|
||||
if (!host) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hostname = host.split(':')[0];
|
||||
if (isInternalHostname(hostname)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return normalizePublicBaseUrl(`${readRequestProto(req)}://${host}`);
|
||||
}
|
||||
|
||||
export function resolvePublicApiBaseFromRequest(req?: Request): string | null {
|
||||
const origin = resolvePublicOriginFromRequest(req);
|
||||
if (!origin) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return `${origin}/idp-api`;
|
||||
}
|
||||
|
||||
export function resolvePublicFrontendBaseFromRequest(req?: Request): string | null {
|
||||
return resolvePublicOriginFromRequest(req);
|
||||
}
|
||||
|
||||
export function preferBrowserReachableBase(stored: string, fromRequest: string | null) {
|
||||
if (fromRequest && isBrowserReachableBaseUrl(fromRequest)) {
|
||||
try {
|
||||
const storedUrl = new URL(stored);
|
||||
if (isInternalHostname(storedUrl.hostname)) {
|
||||
return fromRequest;
|
||||
}
|
||||
} catch {
|
||||
return fromRequest;
|
||||
}
|
||||
|
||||
if (normalizePublicBaseUrl(stored) !== normalizePublicBaseUrl(fromRequest)) {
|
||||
return fromRequest;
|
||||
}
|
||||
}
|
||||
|
||||
return stored;
|
||||
}
|
||||
|
||||
export function hostsMatch(a: string, b: string) {
|
||||
if (a === b) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return a === 'localhost' && b === 'localhost';
|
||||
}
|
||||
Reference in New Issue
Block a user