diff --git a/config/config.example.yml b/config/config.example.yml index e62af16d..50c1a623 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -9,7 +9,6 @@ server: internal_port: 3001 next_port: 3002 internal_hostname: "pangolin" - secure_cookies: true session_cookie_name: "p_session_token" resource_access_token_param: "p_token" resource_session_request_param: "p_session_request" diff --git a/config/traefik/traefik_config.example.yml b/config/traefik/traefik_config.example.yml index b822f493..01d05903 100644 --- a/config/traefik/traefik_config.example.yml +++ b/config/traefik/traefik_config.example.yml @@ -4,13 +4,7 @@ api: providers: http: - endpoint: "http://pangolin:3001/api/v1/traefik-config/http" - pollInterval: "5s" - udp: - endpoint: "http://pangolin:3001/api/v1/traefik-config/udp" - pollInterval: "5s" - tcp: - endpoint: "http://pangolin:3001/api/v1/traefik-config/tcp" + endpoint: "http://pangolin:{{.INTERNAL_PORT}}/api/v1/traefik-config" pollInterval: "5s" file: filename: "/etc/traefik/dynamic_config.yml" diff --git a/install/fs/config.yml b/install/fs/config.yml index 3ccec1e5..4a97dbb5 100644 --- a/install/fs/config.yml +++ b/install/fs/config.yml @@ -9,7 +9,6 @@ server: internal_port: 3001 next_port: 3002 internal_hostname: "pangolin" - secure_cookies: true session_cookie_name: "p_session_token" resource_access_token_param: "p_token" resource_session_request_param: "p_session_request" @@ -40,7 +39,7 @@ rate_limits: {{if .EnableEmail}} email: smtp_host: "{{.EmailSMTPHost}}" - smtp_port: "{{.EmailSMTPPort}}" + smtp_port: {{.EmailSMTPPort}} smtp_user: "{{.EmailSMTPUser}}" smtp_pass: "{{.EmailSMTPPass}}" no_reply: "{{.EmailNoReply}}" diff --git a/install/fs/traefik/traefik_config.yml b/install/fs/traefik/traefik_config.yml index bb97b3ca..40507c24 100644 --- a/install/fs/traefik/traefik_config.yml +++ b/install/fs/traefik/traefik_config.yml @@ -4,13 +4,7 @@ api: providers: http: - endpoint: "http://pangolin:3001/api/v1/traefik-config/http" - pollInterval: "5s" - udp: - endpoint: "http://pangolin:3001/api/v1/traefik-config/udp" - pollInterval: "5s" - tcp: - endpoint: "http://pangolin:3001/api/v1/traefik-config/tcp" + endpoint: "http://pangolin:3001/api/v1/traefik-config" pollInterval: "5s" file: filename: "/etc/traefik/dynamic_config.yml" diff --git a/package.json b/package.json index 446e3d37..eceba242 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@fosrl/pangolin", - "version": "1.0.0-beta.9", + "version": "1.0.0-beta.10", "private": true, "type": "module", "description": "Tunneled Reverse Proxy Management Server with Identity and Access Control and Dashboard UI", diff --git a/server/auth/index.ts b/server/auth/index.ts deleted file mode 100644 index 4bf2c40d..00000000 --- a/server/auth/index.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { - encodeBase32LowerCaseNoPadding, - encodeHexLowerCase, -} from "@oslojs/encoding"; -import { sha256 } from "@oslojs/crypto/sha2"; -import { Session, sessions, User, users } from "@server/db/schema"; -import db from "@server/db"; -import { eq } from "drizzle-orm"; -import config from "@server/lib/config"; -import type { RandomReader } from "@oslojs/crypto/random"; -import { generateRandomString } from "@oslojs/crypto/random"; - -export const SESSION_COOKIE_NAME = config.getRawConfig().server.session_cookie_name; -export const SESSION_COOKIE_EXPIRES = 1000 * 60 * 60 * 24 * 30; -export const SECURE_COOKIES = config.getRawConfig().server.secure_cookies; -export const COOKIE_DOMAIN = "." + config.getBaseDomain(); - -export function generateSessionToken(): string { - const bytes = new Uint8Array(20); - crypto.getRandomValues(bytes); - const token = encodeBase32LowerCaseNoPadding(bytes); - return token; -} - -export async function createSession( - token: string, - userId: string, -): Promise { - const sessionId = encodeHexLowerCase( - sha256(new TextEncoder().encode(token)), - ); - const session: Session = { - sessionId: sessionId, - userId, - expiresAt: new Date(Date.now() + SESSION_COOKIE_EXPIRES).getTime(), - }; - await db.insert(sessions).values(session); - return session; -} - -export async function validateSessionToken( - token: string, -): Promise { - const sessionId = encodeHexLowerCase( - sha256(new TextEncoder().encode(token)), - ); - const result = await db - .select({ user: users, session: sessions }) - .from(sessions) - .innerJoin(users, eq(sessions.userId, users.userId)) - .where(eq(sessions.sessionId, sessionId)); - if (result.length < 1) { - return { session: null, user: null }; - } - const { user, session } = result[0]; - if (Date.now() >= session.expiresAt) { - await db - .delete(sessions) - .where(eq(sessions.sessionId, session.sessionId)); - return { session: null, user: null }; - } - if (Date.now() >= session.expiresAt - SESSION_COOKIE_EXPIRES / 2) { - session.expiresAt = new Date( - Date.now() + SESSION_COOKIE_EXPIRES, - ).getTime(); - await db - .update(sessions) - .set({ - expiresAt: session.expiresAt, - }) - .where(eq(sessions.sessionId, session.sessionId)); - } - return { session, user }; -} - -export async function invalidateSession(sessionId: string): Promise { - await db.delete(sessions).where(eq(sessions.sessionId, sessionId)); -} - -export async function invalidateAllSessions(userId: string): Promise { - await db.delete(sessions).where(eq(sessions.userId, userId)); -} - -export function serializeSessionCookie(token: string): string { - if (SECURE_COOKIES) { - return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Secure; Domain=${COOKIE_DOMAIN}`; - } else { - return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Domain=${COOKIE_DOMAIN}`; - } -} - -export function createBlankSessionTokenCookie(): string { - if (SECURE_COOKIES) { - return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Secure; Domain=${COOKIE_DOMAIN}`; - } else { - return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Domain=${COOKIE_DOMAIN}`; - } -} - -const random: RandomReader = { - read(bytes: Uint8Array): void { - crypto.getRandomValues(bytes); - }, -}; - -export function generateId(length: number): string { - const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789"; - return generateRandomString(random, alphabet, length); -} - -export function generateIdFromEntropySize(size: number): string { - const buffer = crypto.getRandomValues(new Uint8Array(size)); - return encodeBase32LowerCaseNoPadding(buffer); -} - -export type SessionValidationResult = - | { session: Session; user: User } - | { session: null; user: null }; diff --git a/server/auth/sessions/app.ts b/server/auth/sessions/app.ts index 29c54eee..e58ff815 100644 --- a/server/auth/sessions/app.ts +++ b/server/auth/sessions/app.ts @@ -24,7 +24,6 @@ export const SESSION_COOKIE_EXPIRES = 60 * 60 * config.getRawConfig().server.dashboard_session_length_hours; -export const SECURE_COOKIES = config.getRawConfig().server.secure_cookies; export const COOKIE_DOMAIN = "." + new URL(config.getRawConfig().app.dashboard_url).hostname; @@ -108,12 +107,7 @@ export function serializeSessionCookie( isSecure: boolean ): string { if (isSecure) { - logger.debug("Setting cookie for secure origin"); - if (SECURE_COOKIES) { - return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES / 1000}; Path=/; Secure; Domain=${COOKIE_DOMAIN}`; - } else { - return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES / 1000}; Path=/; Domain=${COOKIE_DOMAIN}`; - } + return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES / 1000}; Path=/; Secure; Domain=${COOKIE_DOMAIN}`; } else { return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Lax; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/;`; } @@ -121,11 +115,7 @@ export function serializeSessionCookie( export function createBlankSessionTokenCookie(isSecure: boolean): string { if (isSecure) { - if (SECURE_COOKIES) { - return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Secure; Domain=${COOKIE_DOMAIN}`; - } else { - return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Domain=${COOKIE_DOMAIN}`; - } + return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Secure; Domain=${COOKIE_DOMAIN}`; } else { return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Lax; Max-Age=0; Path=/;`; } diff --git a/server/auth/sessions/resource.ts b/server/auth/sessions/resource.ts index e9dd9b96..0bc7f092 100644 --- a/server/auth/sessions/resource.ts +++ b/server/auth/sessions/resource.ts @@ -9,7 +9,6 @@ export const SESSION_COOKIE_NAME = config.getRawConfig().server.session_cookie_name; export const SESSION_COOKIE_EXPIRES = 1000 * 60 * 60 * config.getRawConfig().server.resource_session_length_hours; -export const SECURE_COOKIES = config.getRawConfig().server.secure_cookies; export async function createResourceSession(opts: { token: string; @@ -170,7 +169,7 @@ export function serializeResourceSessionCookie( token: string, isHttp: boolean = false ): string { - if (SECURE_COOKIES && !isHttp) { + if (!isHttp) { return `${cookieName}_s=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES / 1000}; Path=/; Secure; Domain=${"." + domain}`; } else { return `${cookieName}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES / 1000}; Path=/; Domain=${"." + domain}`; @@ -179,9 +178,10 @@ export function serializeResourceSessionCookie( export function createBlankResourceSessionTokenCookie( cookieName: string, - domain: string + domain: string, + isHttp: boolean = false ): string { - if (SECURE_COOKIES) { + if (!isHttp) { return `${cookieName}_s=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Secure; Domain=${"." + domain}`; } else { return `${cookieName}=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Domain=${"." + domain}`; diff --git a/server/lib/config.ts b/server/lib/config.ts index 66dd6764..14e96af1 100644 --- a/server/lib/config.ts +++ b/server/lib/config.ts @@ -60,7 +60,6 @@ const configSchema = z.object({ .transform(stoi) .pipe(portSchema), internal_hostname: z.string().transform((url) => url.toLowerCase()), - secure_cookies: z.boolean(), session_cookie_name: z.string(), resource_access_token_param: z.string(), resource_session_request_param: z.string(), diff --git a/server/routers/badger/exchangeSession.ts b/server/routers/badger/exchangeSession.ts index 18925279..093dfbb9 100644 --- a/server/routers/badger/exchangeSession.ts +++ b/server/routers/badger/exchangeSession.ts @@ -12,8 +12,7 @@ import { serializeResourceSessionCookie, validateResourceSessionToken } from "@server/auth/sessions/resource"; -import { generateSessionToken } from "@server/auth"; -import { SESSION_COOKIE_EXPIRES } from "@server/auth/sessions/app"; +import { generateSessionToken, SESSION_COOKIE_EXPIRES } from "@server/auth/sessions/app"; import { SESSION_COOKIE_EXPIRES as RESOURCE_SESSION_COOKIE_EXPIRES } from "@server/auth/sessions/resource"; import config from "@server/lib/config"; import { response } from "@server/lib"; diff --git a/server/routers/badger/verifySession.ts b/server/routers/badger/verifySession.ts index b1a6fb12..5830d805 100644 --- a/server/routers/badger/verifySession.ts +++ b/server/routers/badger/verifySession.ts @@ -26,8 +26,8 @@ import { import { Resource, roleResources, userResources } from "@server/db/schema"; import logger from "@server/logger"; import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken"; -import { generateSessionToken } from "@server/auth"; import NodeCache from "node-cache"; +import { generateSessionToken } from "@server/auth/sessions/app"; // We'll see if this speeds anything up const cache = new NodeCache({ diff --git a/server/routers/internal.ts b/server/routers/internal.ts index 2d68b368..ead70d13 100644 --- a/server/routers/internal.ts +++ b/server/routers/internal.ts @@ -1,12 +1,11 @@ import { Router } from "express"; import * as gerbil from "@server/routers/gerbil"; import * as traefik from "@server/routers/traefik"; +import * as resource from "./resource"; +import * as badger from "./badger"; import * as auth from "@server/routers/auth"; import HttpCode from "@server/types/HttpCode"; import { verifyResourceAccess, verifySessionUserMiddleware } from "@server/middlewares"; -import { getExchangeToken } from "./resource/getExchangeToken"; -import { verifyResourceSession } from "./badger"; -import { exchangeSession } from "./badger/exchangeSession"; // Root routes const internalRouter = Router(); @@ -26,7 +25,7 @@ internalRouter.post( `/resource/:resourceId/get-exchange-token`, verifySessionUserMiddleware, verifyResourceAccess, - getExchangeToken + resource.getExchangeToken ); // Gerbil routes @@ -40,7 +39,7 @@ gerbilRouter.post("/receive-bandwidth", gerbil.receiveBandwidth); const badgerRouter = Router(); internalRouter.use("/badger", badgerRouter); -badgerRouter.post("/verify-session", verifyResourceSession); -badgerRouter.post("/exchange-session", exchangeSession); +badgerRouter.post("/verify-session", badger.verifyResourceSession); +badgerRouter.post("/exchange-session", badger.exchangeSession); export default internalRouter; diff --git a/server/routers/newt/handleRegisterMessage.ts b/server/routers/newt/handleRegisterMessage.ts index 704382eb..0f086698 100644 --- a/server/routers/newt/handleRegisterMessage.ts +++ b/server/routers/newt/handleRegisterMessage.ts @@ -106,6 +106,7 @@ export const handleRegisterMessage: MessageHandler = async (context) => { eq(targets.enabled, true) ) ) + .where(eq(resources.siteId, siteId)) .groupBy(resources.resourceId); let tcpTargets: string[] = []; diff --git a/server/setup/migrations.ts b/server/setup/migrations.ts index 7d1d7da4..2f1aae21 100644 --- a/server/setup/migrations.ts +++ b/server/setup/migrations.ts @@ -4,7 +4,7 @@ import path from "path"; import semver from "semver"; import { versionMigrations } from "@server/db/schema"; import { desc } from "drizzle-orm"; -import { __DIRNAME, APP_PATH } from "@server/lib/consts"; +import { __DIRNAME } from "@server/lib/consts"; import { loadAppVersion } from "@server/lib/loadAppVersion"; import m1 from "./scripts/1.0.0-beta1"; import m2 from "./scripts/1.0.0-beta2"; @@ -12,6 +12,7 @@ import m3 from "./scripts/1.0.0-beta3"; import m4 from "./scripts/1.0.0-beta5"; import m5 from "./scripts/1.0.0-beta6"; import m6 from "./scripts/1.0.0-beta9"; +import m7 from "./scripts/1.0.0-beta10"; // THIS CANNOT IMPORT ANYTHING FROM THE SERVER // EXCEPT FOR THE DATABASE AND THE SCHEMA @@ -23,7 +24,8 @@ const migrations = [ { version: "1.0.0-beta.3", run: m3 }, { version: "1.0.0-beta.5", run: m4 }, { version: "1.0.0-beta.6", run: m5 }, - { version: "1.0.0-beta.9", run: m6 } + { version: "1.0.0-beta.9", run: m6 }, + { version: "1.0.0-beta.10", run: m7 } // Add new migrations here as they are created ] as const; diff --git a/server/setup/scripts/1.0.0-beta10.ts b/server/setup/scripts/1.0.0-beta10.ts new file mode 100644 index 00000000..6fd5289b --- /dev/null +++ b/server/setup/scripts/1.0.0-beta10.ts @@ -0,0 +1,45 @@ +import { configFilePath1, configFilePath2 } from "@server/lib/consts"; +import fs from "fs"; +import yaml from "js-yaml"; + +export default async function migration() { + console.log("Running setup script 1.0.0-beta.10..."); + + try { + // Determine which config file exists + const filePaths = [configFilePath1, configFilePath2]; + let filePath = ""; + for (const path of filePaths) { + if (fs.existsSync(path)) { + filePath = path; + break; + } + } + + if (!filePath) { + throw new Error( + `No config file found (expected config.yml or config.yaml).` + ); + } + + // Read and parse the YAML file + let rawConfig: any; + const fileContents = fs.readFileSync(filePath, "utf8"); + rawConfig = yaml.load(fileContents); + + delete rawConfig.server.secure_cookies; + + // Write the updated YAML back to the file + const updatedYaml = yaml.dump(rawConfig); + fs.writeFileSync(filePath, updatedYaml, "utf8"); + + console.log(`Removed deprecated config option: secure_cookies.`); + } catch (e) { + console.log( + `Was unable to remove deprecated config option: secure_cookies. Error: ${e}` + ); + return; + } + + console.log("Done."); +} diff --git a/src/app/[orgId]/settings/resources/CreateResourceForm.tsx b/src/app/[orgId]/settings/resources/CreateResourceForm.tsx index 753d843d..11f215d4 100644 --- a/src/app/[orgId]/settings/resources/CreateResourceForm.tsx +++ b/src/app/[orgId]/settings/resources/CreateResourceForm.tsx @@ -62,6 +62,7 @@ import { import { subdomainSchema } from "@server/schemas/subdomainSchema"; import Link from "next/link"; import { SquareArrowOutUpRight } from "lucide-react"; +import CopyTextBox from "@app/components/CopyTextBox"; const createResourceFormSchema = z .object({ @@ -129,6 +130,10 @@ export default function CreateResourceForm({ const [sites, setSites] = useState([]); const [domainSuffix, setDomainSuffix] = useState(org.org.domain); + const [showSnippets, setShowSnippets] = useState(false); + + const [resourceId, setResourceId] = useState(null); + const form = useForm({ resolver: zodResolver(createResourceFormSchema), defaultValues: { @@ -186,11 +191,21 @@ export default function CreateResourceForm({ if (res && res.status === 201) { const id = res.data.data.resourceId; - // navigate to the resource page - router.push(`/${orgId}/settings/resources/${id}`); + setResourceId(id); + + if (data.http) { + goToResource(); + } else { + setShowSnippets(true); + } } } + function goToResource() { + // navigate to the resource page + router.push(`/${orgId}/settings/resources/${resourceId}`); + } + return ( <> -
- - ( - - Name - - - - - This is the name that will be - displayed for this resource. - - - - )} - /> - - {!env.flags.allowRawResources || ( + {!showSnippets && ( + + ( - -
- - HTTP Resource - - - Toggle if this is an - HTTP resource or a raw - TCP/UDP resource - -
- - - -
- )} - /> - )} - - {form.watch("http") && ( - ( - Subdomain + Name - - form.setValue( - "subdomain", - value - ) - } + - This is the fully qualified - domain name that will be - used to access the resource. + This is the name that will + be displayed for this + resource. )} /> - )} - {!form.watch("http") && ( - - - Learn how to configure TCP/UDP resources - - - - )} - - {!form.watch("http") && ( - <> + {!env.flags.allowRawResources || ( ( - - - Protocol - - - - The protocol to use for - the resource - - + +
+ + HTTP Resource + + + Toggle if this is an + HTTP resource or a + raw TCP/UDP resource + +
+ + +
)} /> + )} + + {form.watch("http") && ( ( - Port Number + Subdomain - - field.onChange( - e.target - .value - ? parseInt( - e - .target - .value - ) - : null + domainSuffix={ + domainSuffix + } + placeholder="Enter subdomain" + onChange={(value) => + form.setValue( + "subdomain", + value ) } /> - The port number to proxy - requests to (required - for non-HTTP resources) + This is the fully + qualified domain name + that will be used to + access the resource. )} /> - - )} - - ( - - Site - - - - - - - - - - - - No site found. - - - {sites.map( - (site) => ( - { - form.setValue( - "siteId", - site.siteId - ); - }} - > - - { - site.name - } - - ) - )} - - - - - - - This is the site that will be - used in the dashboard. - - - )} - /> - - + + {!form.watch("http") && ( + + + Learn how to configure TCP/UDP + resources + + + + )} + + {!form.watch("http") && ( + <> + ( + + + Protocol + + + + The protocol to use + for the resource + + + + )} + /> + ( + + + Port Number + + + + field.onChange( + e.target + .value + ? parseInt( + e + .target + .value + ) + : null + ) + } + /> + + + The port number to + proxy requests to + (required for + non-HTTP resources) + + + + )} + /> + + )} + + ( + + Site + + + + + + + + + + + + No site + found. + + + {sites.map( + ( + site + ) => ( + { + form.setValue( + "siteId", + site.siteId + ); + }} + > + + { + site.name + } + + ) + )} + + + + + + + This is the site that will + be used in the dashboard. + + + + )} + /> + + + )} + + {showSnippets && ( +
+
+
+ 1 +
+
+

+ Traefik: Add Entrypoints +

+ +
+
+ +
+
+ 2 +
+
+

+ Gerbil: Expose Ports in Docker + Compose +

+ +
+
+ + + + Make sure to follow the full guide + + + +
+ )}
- + } + + {showSnippets && } + diff --git a/src/app/[orgId]/settings/resources/[resourceId]/CustomDomainInput.tsx b/src/app/[orgId]/settings/resources/[resourceId]/CustomDomainInput.tsx index 29ae9639..5c1cf01c 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/CustomDomainInput.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/CustomDomainInput.tsx @@ -37,7 +37,7 @@ export default function CustomDomainInput({ className="rounded-r-none flex-grow" />
- {domainSuffix} + .{domainSuffix}
diff --git a/src/app/[orgId]/settings/resources/[resourceId]/connectivity/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/connectivity/page.tsx index c40102e3..3c2b54c6 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/connectivity/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/connectivity/page.tsx @@ -130,7 +130,7 @@ export default function ReverseProxyTargets(props: { const addTargetForm = useForm({ resolver: zodResolver(addTargetSchema), defaultValues: { - ip: "localhost", + ip: "", method: resource.http ? "http" : null, port: resource.http ? 80 : resource.proxyPort || 1234 // protocol: "TCP",