mirror of
https://github.com/fosrl/pangolin.git
synced 2026-01-28 22:00:51 +00:00
Add flag for generate own certs
This commit is contained in:
@@ -8,9 +8,7 @@ import { db, exitNodes } from "@server/db";
|
|||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { getCurrentExitNodeId } from "@server/lib/exitNodes";
|
import { getCurrentExitNodeId } from "@server/lib/exitNodes";
|
||||||
import { getTraefikConfig } from "#dynamic/lib/traefik";
|
import { getTraefikConfig } from "#dynamic/lib/traefik";
|
||||||
import {
|
import { getValidCertificatesForDomains } from "#dynamic/lib/certificates";
|
||||||
getValidCertificatesForDomains,
|
|
||||||
} from "#dynamic/lib/certificates";
|
|
||||||
import { sendToExitNode } from "#dynamic/lib/exitNodes";
|
import { sendToExitNode } from "#dynamic/lib/exitNodes";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
|
|
||||||
@@ -311,84 +309,92 @@ export class TraefikConfigManager {
|
|||||||
this.lastActiveDomains = new Set(domains);
|
this.lastActiveDomains = new Set(domains);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scan current local certificate state
|
if (
|
||||||
this.lastLocalCertificateState =
|
process.env.GENERATE_OWN_CERTIFICATES === "true" &&
|
||||||
await this.scanLocalCertificateState();
|
build != "oss"
|
||||||
|
) {
|
||||||
|
// Scan current local certificate state
|
||||||
|
this.lastLocalCertificateState =
|
||||||
|
await this.scanLocalCertificateState();
|
||||||
|
|
||||||
// Only fetch certificates if needed (domain changes, missing certs, or daily renewal check)
|
// Only fetch certificates if needed (domain changes, missing certs, or daily renewal check)
|
||||||
let validCertificates: Array<{
|
let validCertificates: Array<{
|
||||||
id: number;
|
id: number;
|
||||||
domain: string;
|
domain: string;
|
||||||
wildcard: boolean | null;
|
wildcard: boolean | null;
|
||||||
certFile: string | null;
|
certFile: string | null;
|
||||||
keyFile: string | null;
|
keyFile: string | null;
|
||||||
expiresAt: number | null;
|
expiresAt: number | null;
|
||||||
updatedAt?: number | null;
|
updatedAt?: number | null;
|
||||||
}> = [];
|
}> = [];
|
||||||
|
|
||||||
if (this.shouldFetchCertificates(domains)) {
|
if (this.shouldFetchCertificates(domains)) {
|
||||||
// Filter out domains that are already covered by wildcard certificates
|
// Filter out domains that are already covered by wildcard certificates
|
||||||
const domainsToFetch = new Set<string>();
|
const domainsToFetch = new Set<string>();
|
||||||
for (const domain of domains) {
|
for (const domain of domains) {
|
||||||
if (
|
if (
|
||||||
!isDomainCoveredByWildcard(
|
!isDomainCoveredByWildcard(
|
||||||
domain,
|
domain,
|
||||||
this.lastLocalCertificateState
|
this.lastLocalCertificateState
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
domainsToFetch.add(domain);
|
domainsToFetch.add(domain);
|
||||||
} else {
|
} else {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Domain ${domain} is covered by existing wildcard certificate, skipping fetch`
|
`Domain ${domain} is covered by existing wildcard certificate, skipping fetch`
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (domainsToFetch.size > 0) {
|
if (domainsToFetch.size > 0) {
|
||||||
// Get valid certificates for domains not covered by wildcards
|
// Get valid certificates for domains not covered by wildcards
|
||||||
validCertificates =
|
validCertificates =
|
||||||
await getValidCertificatesForDomains(domainsToFetch);
|
await getValidCertificatesForDomains(
|
||||||
this.lastCertificateFetch = new Date();
|
domainsToFetch
|
||||||
this.lastKnownDomains = new Set(domains);
|
);
|
||||||
|
this.lastCertificateFetch = new Date();
|
||||||
|
this.lastKnownDomains = new Set(domains);
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`Fetched ${validCertificates.length} certificates from remote (${domains.size - domainsToFetch.size} domains covered by wildcards)`
|
`Fetched ${validCertificates.length} certificates from remote (${domains.size - domainsToFetch.size} domains covered by wildcards)`
|
||||||
);
|
);
|
||||||
|
|
||||||
// Download and decrypt new certificates
|
// Download and decrypt new certificates
|
||||||
await this.processValidCertificates(validCertificates);
|
await this.processValidCertificates(validCertificates);
|
||||||
|
} else {
|
||||||
|
logger.info(
|
||||||
|
"All domains are covered by existing wildcard certificates, no fetch needed"
|
||||||
|
);
|
||||||
|
this.lastCertificateFetch = new Date();
|
||||||
|
this.lastKnownDomains = new Set(domains);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always ensure all existing certificates (including wildcards) are in the config
|
||||||
|
await this.updateDynamicConfigFromLocalCerts(domains);
|
||||||
} else {
|
} else {
|
||||||
logger.info(
|
const timeSinceLastFetch = this.lastCertificateFetch
|
||||||
"All domains are covered by existing wildcard certificates, no fetch needed"
|
? Math.round(
|
||||||
);
|
(Date.now() -
|
||||||
this.lastCertificateFetch = new Date();
|
this.lastCertificateFetch.getTime()) /
|
||||||
this.lastKnownDomains = new Set(domains);
|
(1000 * 60)
|
||||||
|
)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
// logger.debug(
|
||||||
|
// `Skipping certificate fetch - no changes detected and within 24-hour window (last fetch: ${timeSinceLastFetch} minutes ago)`
|
||||||
|
// );
|
||||||
|
|
||||||
|
// Still need to ensure config is up to date with existing certificates
|
||||||
|
await this.updateDynamicConfigFromLocalCerts(domains);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always ensure all existing certificates (including wildcards) are in the config
|
// Clean up certificates for domains no longer in use
|
||||||
await this.updateDynamicConfigFromLocalCerts(domains);
|
await this.cleanupUnusedCertificates(domains);
|
||||||
} else {
|
|
||||||
const timeSinceLastFetch = this.lastCertificateFetch
|
|
||||||
? Math.round(
|
|
||||||
(Date.now() - this.lastCertificateFetch.getTime()) /
|
|
||||||
(1000 * 60)
|
|
||||||
)
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
// logger.debug(
|
// wait 1 second for traefik to pick up the new certificates
|
||||||
// `Skipping certificate fetch - no changes detected and within 24-hour window (last fetch: ${timeSinceLastFetch} minutes ago)`
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||||
// );
|
|
||||||
|
|
||||||
// Still need to ensure config is up to date with existing certificates
|
|
||||||
await this.updateDynamicConfigFromLocalCerts(domains);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up certificates for domains no longer in use
|
|
||||||
await this.cleanupUnusedCertificates(domains);
|
|
||||||
|
|
||||||
// wait 1 second for traefik to pick up the new certificates
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
||||||
|
|
||||||
// Write traefik config as YAML to a second dynamic config file if changed
|
// Write traefik config as YAML to a second dynamic config file if changed
|
||||||
await this.writeTraefikDynamicConfig(traefikConfig);
|
await this.writeTraefikDynamicConfig(traefikConfig);
|
||||||
|
|
||||||
@@ -690,7 +696,12 @@ export class TraefikConfigManager {
|
|||||||
|
|
||||||
for (const cert of validCertificates) {
|
for (const cert of validCertificates) {
|
||||||
try {
|
try {
|
||||||
if (!cert.certFile || !cert.keyFile) {
|
if (
|
||||||
|
!cert.certFile ||
|
||||||
|
!cert.keyFile ||
|
||||||
|
cert.certFile.length === 0 ||
|
||||||
|
cert.keyFile.length === 0
|
||||||
|
) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`Certificate for domain ${cert.domain} is missing cert or key file`
|
`Certificate for domain ${cert.domain} is missing cert or key file`
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -105,7 +105,12 @@ export async function getTraefikConfig(
|
|||||||
const priority = row.priority ?? 100;
|
const priority = row.priority ?? 100;
|
||||||
|
|
||||||
// Create a unique key combining resourceId, path config, and rewrite config
|
// Create a unique key combining resourceId, path config, and rewrite config
|
||||||
const pathKey = [targetPath, pathMatchType, rewritePath, rewritePathType]
|
const pathKey = [
|
||||||
|
targetPath,
|
||||||
|
pathMatchType,
|
||||||
|
rewritePath,
|
||||||
|
rewritePathType
|
||||||
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join("-");
|
.join("-");
|
||||||
const mapKey = [resourceId, pathKey].filter(Boolean).join("-");
|
const mapKey = [resourceId, pathKey].filter(Boolean).join("-");
|
||||||
@@ -120,13 +125,15 @@ export async function getTraefikConfig(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!validation.isValid) {
|
if (!validation.isValid) {
|
||||||
logger.error(`Invalid path rewrite configuration for resource ${resourceId}: ${validation.error}`);
|
logger.error(
|
||||||
|
`Invalid path rewrite configuration for resource ${resourceId}: ${validation.error}`
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
resourcesMap.set(key, {
|
resourcesMap.set(key, {
|
||||||
resourceId: row.resourceId,
|
resourceId: row.resourceId,
|
||||||
name: resourceName,
|
name: resourceName,
|
||||||
fullDomain: row.fullDomain,
|
fullDomain: row.fullDomain,
|
||||||
ssl: row.ssl,
|
ssl: row.ssl,
|
||||||
http: row.http,
|
http: row.http,
|
||||||
@@ -239,21 +246,18 @@ export async function getTraefikConfig(
|
|||||||
preferWildcardCert = configDomain.prefer_wildcard_cert;
|
preferWildcardCert = configDomain.prefer_wildcard_cert;
|
||||||
}
|
}
|
||||||
|
|
||||||
let tls = {};
|
const tls = {
|
||||||
if (build == "oss") {
|
certResolver: certResolver,
|
||||||
tls = {
|
...(preferWildcardCert
|
||||||
certResolver: certResolver,
|
? {
|
||||||
...(preferWildcardCert
|
domains: [
|
||||||
? {
|
{
|
||||||
domains: [
|
main: wildCard
|
||||||
{
|
}
|
||||||
main: wildCard
|
]
|
||||||
}
|
}
|
||||||
]
|
: {})
|
||||||
}
|
};
|
||||||
: {})
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const additionalMiddlewares =
|
const additionalMiddlewares =
|
||||||
config.getRawConfig().traefik.additional_middlewares || [];
|
config.getRawConfig().traefik.additional_middlewares || [];
|
||||||
@@ -264,11 +268,12 @@ export async function getTraefikConfig(
|
|||||||
];
|
];
|
||||||
|
|
||||||
// Handle path rewriting middleware
|
// Handle path rewriting middleware
|
||||||
if (resource.rewritePath &&
|
if (
|
||||||
|
resource.rewritePath &&
|
||||||
resource.path &&
|
resource.path &&
|
||||||
resource.pathMatchType &&
|
resource.pathMatchType &&
|
||||||
resource.rewritePathType) {
|
resource.rewritePathType
|
||||||
|
) {
|
||||||
// Create a unique middleware name
|
// Create a unique middleware name
|
||||||
const rewriteMiddlewareName = `rewrite-r${resource.resourceId}-${key}`;
|
const rewriteMiddlewareName = `rewrite-r${resource.resourceId}-${key}`;
|
||||||
|
|
||||||
@@ -287,7 +292,10 @@ export async function getTraefikConfig(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// the middleware to the config
|
// the middleware to the config
|
||||||
Object.assign(config_output.http.middlewares, rewriteResult.middlewares);
|
Object.assign(
|
||||||
|
config_output.http.middlewares,
|
||||||
|
rewriteResult.middlewares
|
||||||
|
);
|
||||||
|
|
||||||
// middlewares to the router middleware chain
|
// middlewares to the router middleware chain
|
||||||
if (rewriteResult.chain) {
|
if (rewriteResult.chain) {
|
||||||
@@ -298,9 +306,13 @@ export async function getTraefikConfig(
|
|||||||
routerMiddlewares.push(rewriteMiddlewareName);
|
routerMiddlewares.push(rewriteMiddlewareName);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug(`Created path rewrite middleware ${rewriteMiddlewareName}: ${resource.pathMatchType}(${resource.path}) -> ${resource.rewritePathType}(${resource.rewritePath})`);
|
logger.debug(
|
||||||
|
`Created path rewrite middleware ${rewriteMiddlewareName}: ${resource.pathMatchType}(${resource.path}) -> ${resource.rewritePathType}(${resource.rewritePath})`
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Failed to create path rewrite middleware for resource ${resource.resourceId}: ${error}`);
|
logger.error(
|
||||||
|
`Failed to create path rewrite middleware for resource ${resource.resourceId}: ${error}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,7 +328,9 @@ export async function getTraefikConfig(
|
|||||||
value: string;
|
value: string;
|
||||||
}[];
|
}[];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.warn(`Failed to parse headers for resource ${resource.resourceId}: ${e}`);
|
logger.warn(
|
||||||
|
`Failed to parse headers for resource ${resource.resourceId}: ${e}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
headersArr.forEach((header) => {
|
headersArr.forEach((header) => {
|
||||||
@@ -482,14 +496,14 @@ export async function getTraefikConfig(
|
|||||||
})(),
|
})(),
|
||||||
...(resource.stickySession
|
...(resource.stickySession
|
||||||
? {
|
? {
|
||||||
sticky: {
|
sticky: {
|
||||||
cookie: {
|
cookie: {
|
||||||
name: "p_sticky", // TODO: make this configurable via config.yml like other cookies
|
name: "p_sticky", // TODO: make this configurable via config.yml like other cookies
|
||||||
secure: resource.ssl,
|
secure: resource.ssl,
|
||||||
httpOnly: true
|
httpOnly: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
: {})
|
: {})
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -590,13 +604,13 @@ export async function getTraefikConfig(
|
|||||||
})(),
|
})(),
|
||||||
...(resource.stickySession
|
...(resource.stickySession
|
||||||
? {
|
? {
|
||||||
sticky: {
|
sticky: {
|
||||||
ipStrategy: {
|
ipStrategy: {
|
||||||
depth: 0,
|
depth: 0,
|
||||||
sourcePort: true
|
sourcePort: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
: {})
|
: {})
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -148,6 +148,10 @@ export class PrivateConfig {
|
|||||||
if (parsedPrivateConfig.stripe?.s3Region) {
|
if (parsedPrivateConfig.stripe?.s3Region) {
|
||||||
process.env.S3_REGION = parsedPrivateConfig.stripe.s3Region;
|
process.env.S3_REGION = parsedPrivateConfig.stripe.s3Region;
|
||||||
}
|
}
|
||||||
|
if (parsedPrivateConfig.flags?.generate_own_certificates) {
|
||||||
|
process.env.GENERATE_OWN_CERTIFICATES =
|
||||||
|
parsedPrivateConfig.flags.generate_own_certificates.toString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.rawPrivateConfig = parsedPrivateConfig;
|
this.rawPrivateConfig = parsedPrivateConfig;
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { MemoryStore, Store } from "express-rate-limit";
|
|||||||
import RedisStore from "#private/lib/redisStore";
|
import RedisStore from "#private/lib/redisStore";
|
||||||
|
|
||||||
export function createStore(): Store {
|
export function createStore(): Store {
|
||||||
if (build != "oss" && privateConfig.getRawPrivateConfig().flags?.enable_redis) {
|
if (build != "oss" && privateConfig.getRawPrivateConfig().flags.enable_redis) {
|
||||||
const rateLimitStore: Store = new RedisStore({
|
const rateLimitStore: Store = new RedisStore({
|
||||||
prefix: "api-rate-limit", // Optional: customize Redis key prefix
|
prefix: "api-rate-limit", // Optional: customize Redis key prefix
|
||||||
skipFailedRequests: true, // Don't count failed requests
|
skipFailedRequests: true, // Don't count failed requests
|
||||||
|
|||||||
@@ -20,141 +20,151 @@ import { build } from "@server/build";
|
|||||||
|
|
||||||
const portSchema = z.number().positive().gt(0).lte(65535);
|
const portSchema = z.number().positive().gt(0).lte(65535);
|
||||||
|
|
||||||
export const privateConfigSchema = z
|
export const privateConfigSchema = z.object({
|
||||||
.object({
|
app: z
|
||||||
app: z.object({
|
.object({
|
||||||
region: z.string().optional().default("default"),
|
region: z.string().optional().default("default"),
|
||||||
base_domain: z.string().optional()
|
base_domain: z.string().optional()
|
||||||
}).optional().default({
|
})
|
||||||
|
.optional()
|
||||||
|
.default({
|
||||||
region: "default"
|
region: "default"
|
||||||
}),
|
}),
|
||||||
server: z.object({
|
server: z
|
||||||
|
.object({
|
||||||
encryption_key_path: z
|
encryption_key_path: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
.default("./config/encryption.pem")
|
.default("./config/encryption.pem")
|
||||||
.pipe(z.string().min(8)),
|
.pipe(z.string().min(8)),
|
||||||
resend_api_key: z.string().optional(),
|
resend_api_key: z.string().optional(),
|
||||||
reo_client_id: z.string().optional(),
|
reo_client_id: z.string().optional()
|
||||||
}).optional().default({
|
})
|
||||||
|
.optional()
|
||||||
|
.default({
|
||||||
encryption_key_path: "./config/encryption.pem"
|
encryption_key_path: "./config/encryption.pem"
|
||||||
}),
|
}),
|
||||||
redis: z
|
redis: z
|
||||||
.object({
|
.object({
|
||||||
host: z.string(),
|
host: z.string(),
|
||||||
port: portSchema,
|
port: portSchema,
|
||||||
password: z.string().optional(),
|
password: z.string().optional(),
|
||||||
db: z.number().int().nonnegative().optional().default(0),
|
db: z.number().int().nonnegative().optional().default(0),
|
||||||
replicas: z
|
replicas: z
|
||||||
.array(
|
.array(
|
||||||
z.object({
|
z.object({
|
||||||
host: z.string(),
|
host: z.string(),
|
||||||
port: portSchema,
|
port: portSchema,
|
||||||
password: z.string().optional(),
|
password: z.string().optional(),
|
||||||
db: z.number().int().nonnegative().optional().default(0)
|
db: z.number().int().nonnegative().optional().default(0)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.optional()
|
||||||
|
// tls: z
|
||||||
|
// .object({
|
||||||
|
// reject_unauthorized: z
|
||||||
|
// .boolean()
|
||||||
|
// .optional()
|
||||||
|
// .default(true)
|
||||||
|
// })
|
||||||
|
// .optional()
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
gerbil: z
|
||||||
|
.object({
|
||||||
|
local_exit_node_reachable_at: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.default("http://gerbil:3003")
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
.default({}),
|
||||||
|
flags: z
|
||||||
|
.object({
|
||||||
|
enable_redis: z.boolean().optional().default(false),
|
||||||
|
generate_own_certificates: z.boolean().optional().default(false)
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
.default({}),
|
||||||
|
branding: z
|
||||||
|
.object({
|
||||||
|
app_name: z.string().optional(),
|
||||||
|
background_image_path: z.string().optional(),
|
||||||
|
colors: z
|
||||||
|
.object({
|
||||||
|
light: colorsSchema.optional(),
|
||||||
|
dark: colorsSchema.optional()
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
logo: z
|
||||||
|
.object({
|
||||||
|
light_path: z.string().optional(),
|
||||||
|
dark_path: z.string().optional(),
|
||||||
|
auth_page: z
|
||||||
|
.object({
|
||||||
|
width: z.number().optional(),
|
||||||
|
height: z.number().optional()
|
||||||
})
|
})
|
||||||
)
|
.optional(),
|
||||||
.optional()
|
navbar: z
|
||||||
// tls: z
|
.object({
|
||||||
// .object({
|
width: z.number().optional(),
|
||||||
// reject_unauthorized: z
|
height: z.number().optional()
|
||||||
// .boolean()
|
|
||||||
// .optional()
|
|
||||||
// .default(true)
|
|
||||||
// })
|
|
||||||
// .optional()
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
gerbil: z
|
|
||||||
.object({
|
|
||||||
local_exit_node_reachable_at: z.string().optional().default("http://gerbil:3003")
|
|
||||||
})
|
|
||||||
.optional()
|
|
||||||
.default({}),
|
|
||||||
flags: z
|
|
||||||
.object({
|
|
||||||
enable_redis: z.boolean().optional(),
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
branding: z
|
|
||||||
.object({
|
|
||||||
app_name: z.string().optional(),
|
|
||||||
background_image_path: z.string().optional(),
|
|
||||||
colors: z
|
|
||||||
.object({
|
|
||||||
light: colorsSchema.optional(),
|
|
||||||
dark: colorsSchema.optional()
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
logo: z
|
|
||||||
.object({
|
|
||||||
light_path: z.string().optional(),
|
|
||||||
dark_path: z.string().optional(),
|
|
||||||
auth_page: z
|
|
||||||
.object({
|
|
||||||
width: z.number().optional(),
|
|
||||||
height: z.number().optional()
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
navbar: z
|
|
||||||
.object({
|
|
||||||
width: z.number().optional(),
|
|
||||||
height: z.number().optional()
|
|
||||||
})
|
|
||||||
.optional()
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
favicon_path: z.string().optional(),
|
|
||||||
footer: z
|
|
||||||
.array(
|
|
||||||
z.object({
|
|
||||||
text: z.string(),
|
|
||||||
href: z.string().optional()
|
|
||||||
})
|
})
|
||||||
)
|
.optional()
|
||||||
.optional(),
|
})
|
||||||
login_page: z
|
.optional(),
|
||||||
.object({
|
favicon_path: z.string().optional(),
|
||||||
subtitle_text: z.string().optional(),
|
footer: z
|
||||||
title_text: z.string().optional()
|
.array(
|
||||||
|
z.object({
|
||||||
|
text: z.string(),
|
||||||
|
href: z.string().optional()
|
||||||
})
|
})
|
||||||
.optional(),
|
)
|
||||||
signup_page: z
|
.optional(),
|
||||||
.object({
|
login_page: z
|
||||||
subtitle_text: z.string().optional(),
|
.object({
|
||||||
title_text: z.string().optional()
|
subtitle_text: z.string().optional(),
|
||||||
})
|
title_text: z.string().optional()
|
||||||
.optional(),
|
})
|
||||||
resource_auth_page: z
|
.optional(),
|
||||||
.object({
|
signup_page: z
|
||||||
show_logo: z.boolean().optional(),
|
.object({
|
||||||
hide_powered_by: z.boolean().optional(),
|
subtitle_text: z.string().optional(),
|
||||||
title_text: z.string().optional(),
|
title_text: z.string().optional()
|
||||||
subtitle_text: z.string().optional()
|
})
|
||||||
})
|
.optional(),
|
||||||
.optional(),
|
resource_auth_page: z
|
||||||
emails: z
|
.object({
|
||||||
.object({
|
show_logo: z.boolean().optional(),
|
||||||
signature: z.string().optional(),
|
hide_powered_by: z.boolean().optional(),
|
||||||
colors: z
|
title_text: z.string().optional(),
|
||||||
.object({
|
subtitle_text: z.string().optional()
|
||||||
primary: z.string().optional()
|
})
|
||||||
})
|
.optional(),
|
||||||
.optional()
|
emails: z
|
||||||
})
|
.object({
|
||||||
.optional()
|
signature: z.string().optional(),
|
||||||
})
|
colors: z
|
||||||
.optional(),
|
.object({
|
||||||
stripe: z
|
primary: z.string().optional()
|
||||||
.object({
|
})
|
||||||
secret_key: z.string(),
|
.optional()
|
||||||
webhook_secret: z.string(),
|
})
|
||||||
s3Bucket: z.string(),
|
.optional()
|
||||||
s3Region: z.string().default("us-east-1"),
|
})
|
||||||
localFilePath: z.string()
|
.optional(),
|
||||||
})
|
stripe: z
|
||||||
.optional(),
|
.object({
|
||||||
});
|
secret_key: z.string(),
|
||||||
|
webhook_secret: z.string(),
|
||||||
|
s3Bucket: z.string(),
|
||||||
|
s3Region: z.string().default("us-east-1"),
|
||||||
|
localFilePath: z.string()
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
});
|
||||||
|
|
||||||
export function readPrivateConfigFile() {
|
export function readPrivateConfigFile() {
|
||||||
if (build == "oss") {
|
if (build == "oss") {
|
||||||
@@ -182,9 +192,7 @@ export function readPrivateConfigFile() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!environment) {
|
if (!environment) {
|
||||||
throw new Error(
|
throw new Error("No private configuration file found.");
|
||||||
"No private configuration file found."
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return environment;
|
return environment;
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ class RedisManager {
|
|||||||
this.isEnabled = false;
|
this.isEnabled = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.isEnabled = privateConfig.getRawPrivateConfig().flags?.enable_redis || false;
|
this.isEnabled = privateConfig.getRawPrivateConfig().flags.enable_redis || false;
|
||||||
if (this.isEnabled) {
|
if (this.isEnabled) {
|
||||||
this.initializeClients();
|
this.initializeClients();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,11 +21,10 @@ import {
|
|||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import { and, eq, inArray, or, isNull, ne, isNotNull, desc } from "drizzle-orm";
|
import { and, eq, inArray, or, isNull, ne, isNotNull, desc } from "drizzle-orm";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
import { orgs, resources, sites, Target, targets } from "@server/db";
|
import { orgs, resources, sites, Target, targets } from "@server/db";
|
||||||
import { build } from "@server/build";
|
|
||||||
import { sanitize } from "@server/lib/traefik/utils";
|
import { sanitize } from "@server/lib/traefik/utils";
|
||||||
|
import privateConfig from "#private/lib/config";
|
||||||
|
|
||||||
const redirectHttpsMiddlewareName = "redirect-to-https";
|
const redirectHttpsMiddlewareName = "redirect-to-https";
|
||||||
const redirectToRootMiddlewareName = "redirect-to-root";
|
const redirectToRootMiddlewareName = "redirect-to-root";
|
||||||
@@ -79,7 +78,7 @@ export async function getTraefikConfig(
|
|||||||
path: targets.path,
|
path: targets.path,
|
||||||
pathMatchType: targets.pathMatchType,
|
pathMatchType: targets.pathMatchType,
|
||||||
priority: targets.priority,
|
priority: targets.priority,
|
||||||
|
|
||||||
// Site fields
|
// Site fields
|
||||||
siteId: sites.siteId,
|
siteId: sites.siteId,
|
||||||
siteType: sites.type,
|
siteType: sites.type,
|
||||||
@@ -234,12 +233,13 @@ export async function getTraefikConfig(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resource.certificateStatus !== "valid") {
|
// TODO: for now dont filter it out because if you have multiple domain ids and one is failed it causes all of them to fail
|
||||||
logger.debug(
|
// if (resource.certificateStatus !== "valid" && privateConfig.getRawPrivateConfig().flags.generate_own_certificates) {
|
||||||
`Resource ${resource.resourceId} has certificate stats ${resource.certificateStats}`
|
// logger.debug(
|
||||||
);
|
// `Resource ${resource.resourceId} has certificate stats ${resource.certificateStats}`
|
||||||
continue;
|
// );
|
||||||
}
|
// continue;
|
||||||
|
// }
|
||||||
|
|
||||||
// add routers and services empty objects if they don't exist
|
// add routers and services empty objects if they don't exist
|
||||||
if (!config_output.http.routers) {
|
if (!config_output.http.routers) {
|
||||||
@@ -264,18 +264,21 @@ export async function getTraefikConfig(
|
|||||||
|
|
||||||
const configDomain = config.getDomain(resource.domainId);
|
const configDomain = config.getDomain(resource.domainId);
|
||||||
|
|
||||||
let certResolver: string, preferWildcardCert: boolean;
|
|
||||||
if (!configDomain) {
|
|
||||||
certResolver = config.getRawConfig().traefik.cert_resolver;
|
|
||||||
preferWildcardCert =
|
|
||||||
config.getRawConfig().traefik.prefer_wildcard_cert;
|
|
||||||
} else {
|
|
||||||
certResolver = configDomain.cert_resolver;
|
|
||||||
preferWildcardCert = configDomain.prefer_wildcard_cert;
|
|
||||||
}
|
|
||||||
|
|
||||||
let tls = {};
|
let tls = {};
|
||||||
if (build == "oss") {
|
if (
|
||||||
|
!privateConfig.getRawPrivateConfig().flags
|
||||||
|
.generate_own_certificates
|
||||||
|
) {
|
||||||
|
let certResolver: string, preferWildcardCert: boolean;
|
||||||
|
if (!configDomain) {
|
||||||
|
certResolver = config.getRawConfig().traefik.cert_resolver;
|
||||||
|
preferWildcardCert =
|
||||||
|
config.getRawConfig().traefik.prefer_wildcard_cert;
|
||||||
|
} else {
|
||||||
|
certResolver = configDomain.cert_resolver;
|
||||||
|
preferWildcardCert = configDomain.prefer_wildcard_cert;
|
||||||
|
}
|
||||||
|
|
||||||
tls = {
|
tls = {
|
||||||
certResolver: certResolver,
|
certResolver: certResolver,
|
||||||
...(preferWildcardCert
|
...(preferWildcardCert
|
||||||
@@ -419,7 +422,7 @@ export async function getTraefikConfig(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
(targets as TargetWithSite[])
|
(targets as TargetWithSite[])
|
||||||
.filter((target: TargetWithSite) => {
|
.filter((target: TargetWithSite) => {
|
||||||
if (!target.enabled) {
|
if (!target.enabled) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -440,7 +443,7 @@ export async function getTraefikConfig(
|
|||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else if (target.site.type === "newt") {
|
} else if (target.site.type === "newt") {
|
||||||
if (
|
if (
|
||||||
!target.internalPort ||
|
!target.internalPort ||
|
||||||
!target.method ||
|
!target.method ||
|
||||||
@@ -448,10 +451,10 @@ export async function getTraefikConfig(
|
|||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
})
|
})
|
||||||
.map((target: TargetWithSite) => {
|
.map((target: TargetWithSite) => {
|
||||||
if (
|
if (
|
||||||
target.site.type === "local" ||
|
target.site.type === "local" ||
|
||||||
target.site.type === "wireguard"
|
target.site.type === "wireguard"
|
||||||
@@ -459,14 +462,14 @@ export async function getTraefikConfig(
|
|||||||
return {
|
return {
|
||||||
url: `${target.method}://${target.ip}:${target.port}`
|
url: `${target.method}://${target.ip}:${target.port}`
|
||||||
};
|
};
|
||||||
} else if (target.site.type === "newt") {
|
} else if (target.site.type === "newt") {
|
||||||
const ip =
|
const ip =
|
||||||
target.site.subnet!.split("/")[0];
|
target.site.subnet!.split("/")[0];
|
||||||
return {
|
return {
|
||||||
url: `${target.method}://${ip}:${target.internalPort}`
|
url: `${target.method}://${ip}:${target.internalPort}`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// filter out duplicates
|
// filter out duplicates
|
||||||
.filter(
|
.filter(
|
||||||
(v, i, a) =>
|
(v, i, a) =>
|
||||||
@@ -709,4 +712,4 @@ export async function getTraefikConfig(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return config_output;
|
return config_output;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,15 +15,19 @@ import { Certificate, certificates, db, domains } from "@server/db";
|
|||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { Transaction } from "@server/db";
|
import { Transaction } from "@server/db";
|
||||||
import { eq, or, and, like } from "drizzle-orm";
|
import { eq, or, and, like } from "drizzle-orm";
|
||||||
import { build } from "@server/build";
|
import privateConfig from "#private/lib/config";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if a certificate exists for the given domain.
|
* Checks if a certificate exists for the given domain.
|
||||||
* If not, creates a new certificate in 'pending' state.
|
* If not, creates a new certificate in 'pending' state.
|
||||||
* Wildcard certs cover subdomains.
|
* Wildcard certs cover subdomains.
|
||||||
*/
|
*/
|
||||||
export async function createCertificate(domainId: string, domain: string, trx: Transaction | typeof db) {
|
export async function createCertificate(
|
||||||
if (build !== "saas") {
|
domainId: string,
|
||||||
|
domain: string,
|
||||||
|
trx: Transaction | typeof db
|
||||||
|
) {
|
||||||
|
if (!privateConfig.getRawPrivateConfig().flags.generate_own_certificates) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,7 +43,7 @@ export async function createCertificate(domainId: string, domain: string, trx: T
|
|||||||
|
|
||||||
let existing: Certificate[] = [];
|
let existing: Certificate[] = [];
|
||||||
if (domainRecord.type == "ns") {
|
if (domainRecord.type == "ns") {
|
||||||
const domainLevelDown = domain.split('.').slice(1).join('.');
|
const domainLevelDown = domain.split(".").slice(1).join(".");
|
||||||
existing = await trx
|
existing = await trx
|
||||||
.select()
|
.select()
|
||||||
.from(certificates)
|
.from(certificates)
|
||||||
@@ -49,7 +53,7 @@ export async function createCertificate(domainId: string, domain: string, trx: T
|
|||||||
eq(certificates.wildcard, true), // only NS domains can have wildcard certs
|
eq(certificates.wildcard, true), // only NS domains can have wildcard certs
|
||||||
or(
|
or(
|
||||||
eq(certificates.domain, domain),
|
eq(certificates.domain, domain),
|
||||||
eq(certificates.domain, domainLevelDown),
|
eq(certificates.domain, domainLevelDown)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -67,9 +71,7 @@ export async function createCertificate(domainId: string, domain: string, trx: T
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (existing.length > 0) {
|
if (existing.length > 0) {
|
||||||
logger.info(
|
logger.info(`Certificate already exists for domain ${domain}`);
|
||||||
`Certificate already exists for domain ${domain}`
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -117,8 +117,8 @@ export default function ResourceRules(props: {
|
|||||||
const [openAddRuleCountrySelect, setOpenAddRuleCountrySelect] = useState(false);
|
const [openAddRuleCountrySelect, setOpenAddRuleCountrySelect] = useState(false);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const env = useEnvContext();
|
const { env } = useEnvContext();
|
||||||
const isMaxmindAvailable = env.env.server.maxmind_db_path && env.env.server.maxmind_db_path.length > 0;
|
const isMaxmindAvailable = env.server.maxmind_db_path && env.server.maxmind_db_path.length > 0;
|
||||||
|
|
||||||
const RuleAction = {
|
const RuleAction = {
|
||||||
ACCEPT: t('alwaysAllow'),
|
ACCEPT: t('alwaysAllow'),
|
||||||
|
|||||||
@@ -13,12 +13,14 @@ import {
|
|||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
import CertificateStatus from "@app/components/private/CertificateStatus";
|
import CertificateStatus from "@app/components/private/CertificateStatus";
|
||||||
import { toUnicode } from 'punycode';
|
import { toUnicode } from "punycode";
|
||||||
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
|
|
||||||
type ResourceInfoBoxType = {};
|
type ResourceInfoBoxType = {};
|
||||||
|
|
||||||
export default function ResourceInfoBox({ }: ResourceInfoBoxType) {
|
export default function ResourceInfoBox({}: ResourceInfoBoxType) {
|
||||||
const { resource, authInfo } = useResourceContext();
|
const { resource, authInfo } = useResourceContext();
|
||||||
|
const { env } = useEnvContext();
|
||||||
|
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
|
||||||
@@ -28,7 +30,13 @@ export default function ResourceInfoBox({ }: ResourceInfoBoxType) {
|
|||||||
<Alert>
|
<Alert>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
{/* 4 cols because of the certs */}
|
{/* 4 cols because of the certs */}
|
||||||
<InfoSections cols={resource.http && build != "oss" ? 4 : 3}>
|
<InfoSections
|
||||||
|
cols={
|
||||||
|
resource.http && env.flags.generateOwnCertificates
|
||||||
|
? 4
|
||||||
|
: 3
|
||||||
|
}
|
||||||
|
>
|
||||||
{resource.http ? (
|
{resource.http ? (
|
||||||
<>
|
<>
|
||||||
<InfoSection>
|
<InfoSection>
|
||||||
@@ -37,9 +45,9 @@ export default function ResourceInfoBox({ }: ResourceInfoBoxType) {
|
|||||||
</InfoSectionTitle>
|
</InfoSectionTitle>
|
||||||
<InfoSectionContent>
|
<InfoSectionContent>
|
||||||
{authInfo.password ||
|
{authInfo.password ||
|
||||||
authInfo.pincode ||
|
authInfo.pincode ||
|
||||||
authInfo.sso ||
|
authInfo.sso ||
|
||||||
authInfo.whitelist ? (
|
authInfo.whitelist ? (
|
||||||
<div className="flex items-start space-x-2 text-green-500">
|
<div className="flex items-start space-x-2 text-green-500">
|
||||||
<ShieldCheck className="w-4 h-4 mt-0.5" />
|
<ShieldCheck className="w-4 h-4 mt-0.5" />
|
||||||
<span>{t("protected")}</span>
|
<span>{t("protected")}</span>
|
||||||
@@ -126,25 +134,28 @@ export default function ResourceInfoBox({ }: ResourceInfoBoxType) {
|
|||||||
{/* </InfoSectionContent> */}
|
{/* </InfoSectionContent> */}
|
||||||
{/* </InfoSection> */}
|
{/* </InfoSection> */}
|
||||||
{/* Certificate Status Column */}
|
{/* Certificate Status Column */}
|
||||||
{resource.http && resource.domainId && resource.fullDomain && build != "oss" && (
|
{resource.http &&
|
||||||
<InfoSection>
|
resource.domainId &&
|
||||||
<InfoSectionTitle>
|
resource.fullDomain &&
|
||||||
{t("certificateStatus", {
|
build != "oss" && (
|
||||||
defaultValue: "Certificate"
|
<InfoSection>
|
||||||
})}
|
<InfoSectionTitle>
|
||||||
</InfoSectionTitle>
|
{t("certificateStatus", {
|
||||||
<InfoSectionContent>
|
defaultValue: "Certificate"
|
||||||
<CertificateStatus
|
})}
|
||||||
orgId={resource.orgId}
|
</InfoSectionTitle>
|
||||||
domainId={resource.domainId}
|
<InfoSectionContent>
|
||||||
fullDomain={resource.fullDomain}
|
<CertificateStatus
|
||||||
autoFetch={true}
|
orgId={resource.orgId}
|
||||||
showLabel={false}
|
domainId={resource.domainId}
|
||||||
polling={true}
|
fullDomain={resource.fullDomain}
|
||||||
/>
|
autoFetch={true}
|
||||||
</InfoSectionContent>
|
showLabel={false}
|
||||||
</InfoSection>
|
polling={true}
|
||||||
)}
|
/>
|
||||||
|
</InfoSectionContent>
|
||||||
|
</InfoSection>
|
||||||
|
)}
|
||||||
<InfoSection>
|
<InfoSection>
|
||||||
<InfoSectionTitle>{t("visibility")}</InfoSectionTitle>
|
<InfoSectionTitle>{t("visibility")}</InfoSectionTitle>
|
||||||
<InfoSectionContent>
|
<InfoSectionContent>
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ const AuthPageSettings = forwardRef<AuthPageSettingsRef, AuthPageSettingsProps>(
|
|||||||
const api = createApiClient(useEnvContext());
|
const api = createApiClient(useEnvContext());
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
const { env } = useEnvContext();
|
||||||
|
|
||||||
const subscription = useSubscriptionStatusContext();
|
const subscription = useSubscriptionStatusContext();
|
||||||
const subscribed = subscription?.getTier() === TierId.STANDARD;
|
const subscribed = subscription?.getTier() === TierId.STANDARD;
|
||||||
@@ -435,8 +436,8 @@ const AuthPageSettings = forwardRef<AuthPageSettingsRef, AuthPageSettingsProps>(
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Certificate Status */}
|
{/* Certificate Status */}
|
||||||
{(build !== "saas" ||
|
{(
|
||||||
(build === "saas" && subscribed)) &&
|
(env.flags.generateOwnCertificates && subscribed)) &&
|
||||||
loginPage?.domainId &&
|
loginPage?.domainId &&
|
||||||
loginPage?.fullDomain &&
|
loginPage?.fullDomain &&
|
||||||
!hasUnsavedChanges && (
|
!hasUnsavedChanges && (
|
||||||
|
|||||||
@@ -48,7 +48,11 @@ export function pullEnv(): Env {
|
|||||||
enableClients:
|
enableClients:
|
||||||
process.env.FLAGS_ENABLE_CLIENTS === "true" ? true : false,
|
process.env.FLAGS_ENABLE_CLIENTS === "true" ? true : false,
|
||||||
hideSupporterKey:
|
hideSupporterKey:
|
||||||
process.env.HIDE_SUPPORTER_KEY === "true" ? true : false
|
process.env.HIDE_SUPPORTER_KEY === "true" ? true : false,
|
||||||
|
generateOwnCertificates:
|
||||||
|
process.env.GENERATE_OWN_CERTIFICATES === "true"
|
||||||
|
? true
|
||||||
|
: false
|
||||||
},
|
},
|
||||||
|
|
||||||
branding: {
|
branding: {
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export type Env = {
|
|||||||
disableBasicWireguardSites: boolean;
|
disableBasicWireguardSites: boolean;
|
||||||
enableClients: boolean;
|
enableClients: boolean;
|
||||||
hideSupporterKey: boolean;
|
hideSupporterKey: boolean;
|
||||||
|
generateOwnCertificates: boolean;
|
||||||
},
|
},
|
||||||
branding: {
|
branding: {
|
||||||
appName?: string;
|
appName?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user