Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/.claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,13 @@
"Bash(pnpm i18n:*)",
"Bash(pnpm info:*)",
"Bash(pnpm list:*)",
"Bash(pnpm list:*)",
"Bash(pnpm remove:*)",
"Bash(pnpm run:*)",
"Bash(pnpm ts-build:*)",
"Bash(pnpm tsc:*)",
"Bash(pnpm view:*)",
"Bash(pnpm update:*)",
"Bash(pnpm view:*)",
"Bash(pnpm why:*)",
"Bash(prettier -w:*)",
"Bash(psql:*)",
Expand All @@ -43,7 +44,6 @@
"mcp__github__get_issue_comments",
"mcp__github__get_pull_request",
"mcp__github__get_pull_request_comments",
"mcp__github__get_pull_request_comments",
"mcp__github__get_pull_request_status",
"mcp__github__list_workflow_runs",
"mcp__github__list_workflows"
Expand Down
4 changes: 2 additions & 2 deletions src/packages/hub/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"cookies": "^0.8.0",
"cors": "^2.8.5",
"debug": "^4.4.0",
"express": "^4.21.2",
"express": "^5.1.0",
"formidable": "^3.5.4",
"http-proxy-3": "^1.20.8",
"lodash": "^4.17.21",
Expand All @@ -47,7 +47,7 @@
},
"devDependenicesNotes": "For license and size reasons, we make @cocalc/crm a dev dependency so it is NOT installed unless explicitly installed as a separate step.",
"devDependencies": {
"@types/express": "^4.17.21",
"@types/express": "^5.0.3",
"@types/node": "^18.16.14",
"coffeescript": "^2.5.1"
},
Expand Down
7 changes: 4 additions & 3 deletions src/packages/hub/proxy/handle-upgrade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@

import { createProxyServer, type ProxyServer } from "http-proxy-3";
import LRU from "lru-cache";
import { getEventListeners } from "node:events";

import basePath from "@cocalc/backend/base-path";
import getLogger from "@cocalc/hub/logger";
import { getEventListeners } from "node:events";
import { proxyConatWebsocket } from "./proxy-conat";
import stripRememberMeCookie from "./strip-remember-me-cookie";
import { getTarget } from "./target";
import { stripBasePath } from "./util";
import { versionCheckFails } from "./version";
import { proxyConatWebsocket } from "./proxy-conat";
import basePath from "@cocalc/backend/base-path";

const LISTENERS_HACK = true;

Expand Down
35 changes: 26 additions & 9 deletions src/packages/hub/proxy/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
*/

import { Application } from "express";

import base_path from "@cocalc/backend/base-path";
import { ProjectControlFunction } from "@cocalc/server/projects/control";
import getLogger from "../logger";
import initRequest from "./handle-request";
import initUpgrade from "./handle-upgrade";
import base_path from "@cocalc/backend/base-path";
import { ProjectControlFunction } from "@cocalc/server/projects/control";

const logger = getLogger("proxy");

Expand All @@ -20,19 +21,35 @@ interface Options {
proxyConat: boolean;
}

// UUID regex pattern for project ID validation
const UUID_REGEX =
/^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$/;

/**
* Middleware to validate that the project_id route parameter is a valid UUID.
* If valid, continues to next middleware. If invalid, skips to next route.
*/
function uuidMiddleware(req, _res, next) {
if (UUID_REGEX.test(req.params.project_id)) {
return next();
}
// Not a valid project ID UUID: skip to next route
return next("route");
}

export default function initProxy(opts: Options) {
const proxy_regexp = `^${
base_path.length <= 1 ? "" : base_path
}\/[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}\/*`;
logger.info("creating proxy server with proxy_regexp", proxy_regexp);
const prefix = base_path.length <= 1 ? "" : base_path;
const routePath = `${prefix}/:project_id/{*splat}`;
logger.info("creating proxy server with route pattern", routePath);

// tcp connections:
const handleProxy = initRequest(opts);

// websocket upgrades:
// Create regex for upgrade handler (still needed for WebSocket matching)
const proxy_regexp = `^${prefix}\/[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}\/.*`;
const handleUpgrade = initUpgrade(opts, proxy_regexp);

opts.app.all(proxy_regexp, handleProxy);
// Use Express 5 path syntax with UUID validation middleware
opts.app.all(routePath, uuidMiddleware, handleProxy);

opts.httpServer.on("upgrade", handleUpgrade);
}
6 changes: 4 additions & 2 deletions src/packages/hub/servers/app/app-redirect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
This redirect is *undone* in @cocalc/frontend/client/handle-hash-url.ts
*/

import { join } from "path";
import { join } from "node:path";
import { Router } from "express";

import basePath from "@cocalc/backend/base-path";
import { getLogger } from "@cocalc/hub/logger";
import { APP_ROUTES } from "@cocalc/util/routing/app";
Expand All @@ -14,7 +15,8 @@ export default function init(router: Router) {
const winston = getLogger("app-redirect");
const v: string[] = [];
for (const path of APP_ROUTES) {
v.push(`/${path}*`);
v.push(`/${path}`);
v.push(`/${path}/{*splat}`);
}
router.get(v, (req, res) => {
winston.debug(req.url);
Expand Down
2 changes: 1 addition & 1 deletion src/packages/hub/servers/app/blobs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { getLogger } from "@cocalc/hub/logger";
const logger = getLogger("hub:servers:app:blobs");
export default function init(router: Router) {
// return uuid-indexed blobs (mainly used for graphics)
router.get("/blobs/*", async (req, res) => {
router.get("/blobs/{*splat}", async (req, res) => {
logger.debug(`${JSON.stringify(req.query)}, ${req.path}`);
const uuid = `${req.query.uuid}`;
if (req.headers["if-none-match"] === uuid) {
Expand Down
18 changes: 11 additions & 7 deletions src/packages/hub/servers/app/next.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,15 @@ import { join } from "path";

// @ts-ignore -- TODO: typescript doesn't like @cocalc/next/init (it is a js file).
import initNextServer from "@cocalc/next/init";

import basePath from "@cocalc/backend/base-path";
import { getLogger } from "@cocalc/hub/logger";
import handleRaw from "@cocalc/next/lib/share/handle-raw";
import { callback2 } from "@cocalc/util/async-utils";
import { separate_file_extension } from "@cocalc/util/misc";
import { database } from "../database";
import createLandingRedirect from "./landing-redirect";
import shareRedirect from "./share-redirect";
import { separate_file_extension } from "@cocalc/util/misc";

export default async function init(app: Application) {
const winston = getLogger("nextjs");
Expand Down Expand Up @@ -54,7 +55,7 @@ export default async function init(app: Application) {
// 1: The raw static server:
const raw = join(shareBasePath, "raw");
app.all(
join(raw, "*"),
join(raw, "{*splat}"),
(req: Request, res: Response, next: NextFunction) => {
// Embedding only enabled for PDF files -- see note above
const download =
Expand All @@ -76,7 +77,7 @@ export default async function init(app: Application) {
// 2: The download server -- just like raw, but files always get sent via download.
const download = join(shareBasePath, "download");
app.all(
join(download, "*"),
join(download, "{*splat}"),
(req: Request, res: Response, next: NextFunction) => {
try {
handleRaw({
Expand All @@ -95,21 +96,24 @@ export default async function init(app: Application) {
// 3: Redirects for backward compat; unfortunately there's slight
// overhead for doing this on every request.

app.all(join(shareBasePath, "*"), shareRedirect(shareBasePath));
app.all(join(shareBasePath), shareRedirect(shareBasePath));
app.all(join(shareBasePath, "{*splat}"), shareRedirect(shareBasePath));
}

const landingRedirect = createLandingRedirect();
app.all(join(basePath, "index.html"), landingRedirect);
app.all(join(basePath, "doc*"), landingRedirect);
app.all(join(basePath, "policies*"), landingRedirect);
app.all(join(basePath, "doc"), landingRedirect);
app.all(join(basePath, "doc", "{*splat}"), landingRedirect);
app.all(join(basePath, "policies"), landingRedirect);
app.all(join(basePath, "policies", "{*splat}"), landingRedirect);

// The next.js server that serves everything else.
winston.info(
"Now using next.js packages/share handler to handle all endpoints not otherwise handled",
);

// nextjs listens on everything else
app.all("*", handler);
app.all("{*splat}", handler);
}

function parseURL(req: Request, base): { id: string; path: string } {
Expand Down
4 changes: 2 additions & 2 deletions src/packages/next/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
"basic-auth": "^2.0.1",
"csv-stringify": "^6.3.0",
"dayjs": "^1.11.11",
"express": "^4.21.2",
"express": "^5.1.0",
"lodash": "^4.17.21",
"lru-cache": "^7.18.3",
"ms": "2.1.2",
Expand All @@ -98,7 +98,7 @@
"devDependencies": {
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"@types/express": "^4.17.21",
"@types/express": "^5.0.3",
"@types/node": "^18.16.14",
"@types/react": "^19.1.10",
"@uiw/react-textarea-code-editor": "^3.1.1",
Expand Down
Loading
Loading