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/backend/data.ts
Views: 687
1
/*
2
Where Data is Stored:
3
4
We centralize here determination of all directories on the file system
5
where data is stored for any of the components of CoCalc, run in any way.
6
7
All information here must be determinable when this module is initialized,
8
e.g., from environment variables or heuristics involving the file system.
9
In particular, nothing here can be impacted by command line flags
10
or content of a database.
11
*/
12
13
import Dict = NodeJS.Dict;
14
15
const DEFINITION = `CoCalc Environment Variables:
16
- root -- if COCALC_ROOT is set then it; otherwise use [cocalc-source]/src/.
17
- data -- if the environment variable DATA is set, use that. Otherwise, use {root}/data
18
- pgdata -- if env var PGDATA is set, use that; otherwise, it is {data}/postgres: where data data is stored (if running locally)
19
- pghost - if env var PGHOST is set, use that; otherwise, it is {data}/postgres/socket: what database connects to
20
- projects -- If env var PROJECTS is set, use that; otherwise, it is {data}"/projects/[project_id]";
21
This is where project home directories are (or shared files for share server), and it MUST
22
contain the string "[project_id]".
23
- secrets -- if env var SECRETS is set, use that; otherwise, it is {data}/secrets: where to store secrets
24
- logs -- if env var LOGS is set, use that; otherwise, {data}/logs: directory in which to store logs
25
`;
26
27
import { join, resolve } from "path";
28
import { ConnectionOptions } from "node:tls";
29
import { readFileSync } from "fs";
30
import { isEmpty } from "lodash";
31
32
function determineRootFromPath(): string {
33
const cur = __dirname;
34
const search = "/src/";
35
const i = cur.lastIndexOf(search);
36
const root = resolve(cur.slice(0, i + search.length - 1));
37
process.env.COCALC_ROOT = root;
38
return root;
39
}
40
41
// Each field value in this interface is to be treated as though it originated from a raw
42
// environment variable. These environment variables are used to configure CoCalc's SSL connection
43
// to the database.
44
//
45
interface CoCalcSSLEnvConfig extends Dict<string> {
46
SMC_DB_SSL?: string;
47
SMC_DB_SSL_CA_FILE?: string;
48
SMC_DB_SSL_CLIENT_CERT_FILE?: string;
49
SMC_DB_SSL_CLIENT_KEY_FILE?: string;
50
SMC_DB_SSL_CLIENT_KEY_PASSPHRASE?:string;
51
}
52
53
// This interface is used to specify environment variables to be passed to the "psql" command for
54
// SSL configuration.
55
//
56
// See https://www.postgresql.org/docs/current/libpq-envars.html for more information.
57
//
58
export interface PsqlSSLEnvConfig {
59
// We could also add "verify-ca" here, but it's probably best to assume that we'd like the
60
// most secure option out of the box. The differences between "verify-ca" and "verify-full"
61
// can be found here: https://www.postgresql.org/docs/current/libpq-ssl.html#LIBPQ-SSL-CLIENTCERT
62
//
63
PGSSLMODE?: "verify-full" | "require";
64
// This typing is redundant but included for clarity.
65
//
66
PGSSLROOTCERT?: "system" | string;
67
PGSSLCERT?: string;
68
PGSSLKEY?: string;
69
}
70
71
// A full list of property types and SSL config options can be found here:
72
//
73
// http://nodejs.org/api/tls.html#tls_tls_connect_options_callback
74
//
75
// We extend the existing ConnectionOptions interface to include certificate file paths, since these
76
// are used when connecting to Postgres outside of Node (e.g., for raw psql queries).
77
//
78
export type SSLConfig = ConnectionOptions & {
79
caFile?: string;
80
clientCertFile?: string;
81
clientKeyFile?: string;
82
} | boolean | undefined;
83
84
/**
85
* Converts an environment-variable-driven SSLEnvConfig into a superset of the SSL context expected
86
* by node when generating SSL connections.
87
*
88
* @param env
89
*/
90
export function sslConfigFromCoCalcEnv(env: CoCalcSSLEnvConfig = process.env): SSLConfig {
91
const sslConfig: SSLConfig = {};
92
93
if (env.SMC_DB_SSL_CA_FILE) {
94
sslConfig.caFile = env.SMC_DB_SSL_CA_FILE;
95
sslConfig.ca = readFileSync(env.SMC_DB_SSL_CA_FILE);
96
}
97
98
if (env.SMC_DB_SSL_CLIENT_CERT_FILE) {
99
sslConfig.clientCertFile = env.SMC_DB_SSL_CLIENT_CERT_FILE;
100
sslConfig.cert = readFileSync(env.SMC_DB_SSL_CLIENT_CERT_FILE);
101
}
102
103
if (env.SMC_DB_SSL_CLIENT_KEY_FILE) {
104
sslConfig.clientKeyFile = env.SMC_DB_SSL_CLIENT_KEY_FILE
105
sslConfig.key = readFileSync(env.SMC_DB_SSL_CLIENT_KEY_FILE);
106
}
107
108
if (env.SMC_DB_SSL_CLIENT_KEY_PASSPHRASE) {
109
sslConfig.passphrase = env.SMC_DB_SSL_CLIENT_KEY_PASSPHRASE;
110
}
111
112
return isEmpty(sslConfig) ? (env.SMC_DB_SSL?.toLowerCase() === "true") : sslConfig;
113
}
114
115
/**
116
* Converts a provided SSLConfig object into (a subset of) its corresponding `psql` environment
117
* variables. See
118
*
119
* http://nodejs.org/api/tls.html#tls_tls_connect_options_callback
120
*
121
* for more information about these options.
122
*
123
* @param config
124
*/
125
export function sslConfigToPsqlEnv(config: SSLConfig): PsqlSSLEnvConfig {
126
if (!config) {
127
return {};
128
} else if (config === true) {
129
return {
130
PGSSLMODE: "require",
131
};
132
}
133
134
// If SSL config is anything other than a boolean, require CA validation
135
//
136
const psqlArgs: PsqlSSLEnvConfig = {
137
PGSSLMODE: "verify-full",
138
};
139
140
// Server CA. Uses CA file when provided and system certs otherwise.
141
//
142
if (config.caFile) {
143
psqlArgs.PGSSLROOTCERT = `${config.caFile}`;
144
} else {
145
psqlArgs.PGSSLROOTCERT = "system";
146
}
147
148
// Client cert
149
//
150
if (config.clientCertFile) {
151
psqlArgs.PGSSLCERT = `${config.clientCertFile}`;
152
}
153
154
// Client key
155
//
156
if (config.clientKeyFile) {
157
psqlArgs.PGSSLKEY = `${config.clientKeyFile}`;
158
}
159
160
return psqlArgs;
161
}
162
163
export const root: string = process.env.COCALC_ROOT ?? determineRootFromPath();
164
export const data: string = process.env.DATA ?? join(root, "data");
165
export const pguser: string = process.env.PGUSER ?? "smc";
166
export const pgdata: string = process.env.PGDATA ?? join(data, "postgres");
167
export const pghost: string = process.env.PGHOST ?? join(pgdata, "socket");
168
export const pgssl = sslConfigFromCoCalcEnv();
169
export const pgdatabase: string =
170
process.env.SMC_DB ?? process.env.PGDATABASE ?? "smc";
171
export const projects: string =
172
process.env.PROJECTS ?? join(data, "projects", "[project_id]");
173
export const secrets: string = process.env.SECRETS ?? join(data, "secrets");
174
export const logs: string = process.env.LOGS ?? join(data, "logs");
175
export const blobstore: "disk" | "sqlite" =
176
(process.env.COCALC_JUPYTER_BLOBSTORE_IMPL as any) ?? "sqlite";
177
178
export let apiKey: string = process.env.API_KEY ?? "";
179
export let apiServer: string = process.env.API_SERVER ?? "";
180
181
// Delete API_KEY from environment to reduce chances of it leaking, e.g., to
182
// spawned terminal subprocess.
183
// Important note: It's critical that only one version of the @cocalc/backend
184
// package is being used, or some parts of the code will get the API_KEY and
185
// others will not.
186
delete process.env.API_KEY;
187
188
export function setApi({ key, server }: { key?: string; server?: string }) {
189
if (key != null) {
190
apiKey = key;
191
}
192
if (server != null) {
193
checkApiServer(server);
194
apiServer = server;
195
}
196
}
197
198
function sanityChecks() {
199
// Do a sanity check on projects:
200
if (!projects.includes("[project_id]")) {
201
throw Error(
202
`${DEFINITION}\n\nenv variable PROJECTS must contain "[project_id]" but it is "${process.env.PROJECTS}"`,
203
);
204
}
205
if ((blobstore as any) != "sqlite" && (blobstore as any) != "disk") {
206
throw Error(
207
"If set, COCALC_JUPYTER_BLOBSTORE_IMPL must be 'sqlite' or 'disk'",
208
);
209
}
210
checkApiServer(apiServer);
211
}
212
213
function checkApiServer(server) {
214
if (!server) return;
215
if (server.endsWith("/")) {
216
throw Error("API_SERVER must not end in /");
217
}
218
if (!server.startsWith("http://") && !server.startsWith("https://")) {
219
throw Error("API_SERVER must start with http:// or https://");
220
}
221
}
222
223
sanityChecks();
224
225