CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/next/lib/share/handle-raw.ts
Views: 687
1
/*
2
This handles request to share/raw/[sha1]/[relative path].
3
4
It confirms that the request is valid (so the content is
5
actually currently publicly shared) then sends the result.
6
*/
7
8
import type { Request, Response } from "express";
9
import { static as ExpressStatic } from "express";
10
import LRU from "lru-cache";
11
import ms from "ms";
12
import { join } from "path";
13
import DirectoryListing from "serve-index";
14
15
import { pathFromID } from "./path-to-files";
16
import { getExtension, isSha1Hash } from "./util";
17
18
const MAX_AGE = Math.round(ms("15 minutes") / 1000);
19
20
interface Options {
21
id: string;
22
path: string;
23
res: Response;
24
req: Request;
25
download?: boolean; // if true, cause download
26
next: (value?) => void;
27
}
28
29
export default async function handle(options: Options): Promise<void> {
30
try {
31
await handleRequest(options);
32
} catch (err) {
33
// some other error
34
options.res.send(`Error: ${err}`);
35
}
36
}
37
38
async function handleRequest(opts: Options): Promise<void> {
39
const {
40
id, // id of a public_path
41
path: pathEncoded, // full path in the project to requested file or directory
42
req,
43
res,
44
download,
45
next,
46
} = opts;
47
res.setHeader("Cache-Control", `public, max-age=${MAX_AGE}`);
48
49
if (!isSha1Hash(id)) {
50
throw Error(`id=${id} is not a sha1 hash`);
51
}
52
53
// store the URI decoded string from pathEncoded in path
54
// BUGFIX: https://github.com/sagemathinc/cocalc/issues/5928
55
// This does not work with file names containing a percent sign, because next.js itself does decode the path as well.
56
const path = decodeURIComponent(pathEncoded);
57
58
// the above must come before this check (since dots could be somehow encoded)
59
if (path.includes("..")) {
60
throw Error(`path (="${path}") must not include ".."`);
61
}
62
63
let { fsPath, projectPath } = await pathFromID(id);
64
65
if (!path.startsWith(projectPath)) {
66
// The projectPath absolutely must be an initial segment of the requested path.
67
// We do NOT just use a relative path *inside* the share, because the share might be a file itself
68
// and then the MIME type wouldn't be a function of the URL.
69
throw Error(`path (="${path}") must start with "${projectPath}"`);
70
}
71
72
let url = path.slice(projectPath.length);
73
const target = join(fsPath, url);
74
75
const ext = getExtension(target);
76
if (download || ext == "html" || ext == "svg") {
77
// NOTE: We *always* download .html, since it is far too dangerous to render
78
// an arbitrary html file from our domain.
79
res.download(target, next);
80
return;
81
}
82
83
if (!url) {
84
const i = fsPath.lastIndexOf("/");
85
if (i == -1) {
86
// This can't actually happen, since fsPath is an absolute filesystem path, hence starts with /
87
throw Error(`invalid fsPath=${fsPath}`);
88
}
89
url = fsPath.slice(i);
90
fsPath = fsPath.slice(0, i);
91
}
92
93
req.url = url;
94
staticHandler(fsPath, req, res, next);
95
}
96
97
export function staticHandler(
98
fsPath: string,
99
req: Request,
100
res: Response,
101
next: Function,
102
) {
103
// console.log("staticHandler", { fsPath, url: req.url });
104
const handler = getStaticFileHandler(fsPath);
105
// @ts-ignore -- TODO
106
handler(req, res, () => {
107
// Static handler didn't work, so try the directory listing handler.
108
//console.log("directoryHandler", { fsPath, url: req.url });
109
const handler = getDirectoryHandler(fsPath);
110
try {
111
handler(req, res, next);
112
} catch (err) {
113
// I noticed in logs that if reeq.url is malformed then this directory listing handler --
114
// which is just some old middleware not updated in 6+ years -- can throw an exception
115
// which is not caught. So we catch it here and respond with some sort of generic
116
// server error, but without crashing the server.
117
// Respond with a 500 Internal Server Error status code.
118
if (!res.headersSent) {
119
res
120
.status(500)
121
.send(
122
`Something went wrong on the server, please try again later. -- ${err}`,
123
);
124
} else {
125
// In case headers were already sent, end the response without sending any data.
126
res.end();
127
}
128
}
129
});
130
}
131
132
const staticFileCache = new LRU<string, ReturnType<typeof ExpressStatic>>({
133
max: 200,
134
});
135
function getStaticFileHandler(path: string): ReturnType<typeof ExpressStatic> {
136
const sfh = staticFileCache.get(path);
137
if (sfh) {
138
return sfh;
139
}
140
const handler = ExpressStatic(path);
141
staticFileCache.set(path, handler);
142
return handler;
143
}
144
145
const directoryCache = new LRU<string, ReturnType<typeof DirectoryListing>>({
146
max: 200,
147
});
148
function getDirectoryHandler(
149
path: string,
150
): ReturnType<typeof DirectoryListing> {
151
const dh = directoryCache.get(path);
152
if (dh) {
153
return dh;
154
}
155
const handler = DirectoryListing(path, { icons: true, view: "details" });
156
directoryCache.set(path, handler);
157
return handler;
158
}
159
160