Skip to content

Commit b785955

Browse files
authored
Merge branch 'main' into chore/node-upgrades
2 parents e398a7b + f31592b commit b785955

27 files changed

+274
-230
lines changed

index.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ import path from 'path';
44
import yargs from 'yargs';
55
import { hideBin } from 'yargs/helpers';
66
import * as fs from 'fs';
7-
import { configFile, setConfigFile, validate } from './src/config/file';
7+
import { getConfigFile, setConfigFile, validate } from './src/config/file';
88
import { initUserConfig } from './src/config';
9-
import Proxy from './src/proxy';
10-
import service from './src/service';
9+
import { Proxy } from './src/proxy';
10+
import { Service } from './src/service';
1111

1212
const argv = yargs(hideBin(process.argv))
1313
.usage('Usage: $0 [options]')
@@ -30,9 +30,11 @@ const argv = yargs(hideBin(process.argv))
3030
.strict()
3131
.parseSync();
3232

33+
console.log('Setting config file to: ' + (argv.c as string) || '');
3334
setConfigFile((argv.c as string) || '');
3435
initUserConfig();
3536

37+
const configFile = getConfigFile();
3638
if (argv.v) {
3739
if (!fs.existsSync(configFile)) {
3840
console.error(
@@ -46,10 +48,14 @@ if (argv.v) {
4648
process.exit(0);
4749
}
4850

51+
console.log('validating config');
4952
validate();
5053

54+
console.log('Setting up the proxy and Service');
55+
56+
// The deferred imports should cause these to be loaded on first access
5157
const proxy = new Proxy();
5258
proxy.start();
53-
service.start(proxy);
59+
Service.start(proxy);
5460

55-
export { proxy, service };
61+
export { proxy, Service };

src/config/file.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { readFileSync } from 'fs';
22
import { join } from 'path';
33
import { Convert } from './generated/config';
44

5-
export let configFile: string = join(__dirname, '../../proxy.config.json');
5+
let configFile: string = join(__dirname, '../../proxy.config.json');
66

77
/**
88
* Sets the path to the configuration file.
@@ -14,6 +14,15 @@ export function setConfigFile(file: string) {
1414
configFile = file;
1515
}
1616

17+
/**
18+
* Gets the path to the current configuration file.
19+
*
20+
* @return {string} file - The path to the configuration file.
21+
*/
22+
export function getConfigFile() {
23+
return configFile;
24+
}
25+
1726
export function validate(filePath: string = configFile): boolean {
1827
// Use QuickType to validate the configuration
1928
const configContent = readFileSync(filePath, 'utf-8');

src/config/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { GitProxyConfig, Convert } from './generated/config';
55
import { ConfigLoader } from './ConfigLoader';
66
import { Configuration } from './types';
77
import { serverConfig } from './env';
8-
import { configFile } from './file';
8+
import { getConfigFile } from './file';
99

1010
// Cache for current configuration
1111
let _currentConfig: GitProxyConfig | null = null;
@@ -52,7 +52,7 @@ function loadFullConfiguration(): GitProxyConfig {
5252
const defaultConfig = cleanUndefinedValues(rawDefaultConfig);
5353

5454
let userSettings: Partial<GitProxyConfig> = {};
55-
const userConfigFile = process.env.CONFIG_FILE || configFile;
55+
const userConfigFile = process.env.CONFIG_FILE || getConfigFile();
5656

5757
if (existsSync(userConfigFile)) {
5858
try {

src/db/file/helper.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,6 @@
1+
import { existsSync, mkdirSync } from 'fs';
2+
13
export const getSessionStore = (): undefined => undefined;
4+
export const initializeFolders = () => {
5+
if (!existsSync('./.data/db')) mkdirSync('./.data/db', { recursive: true });
6+
};

src/db/file/pushes.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import fs from 'fs';
21
import _ from 'lodash';
32
import Datastore from '@seald-io/nedb';
43
import { Action } from '../../proxy/actions/Action';
@@ -7,12 +6,6 @@ import { PushQuery } from '../types';
76

87
const COMPACTION_INTERVAL = 1000 * 60 * 60 * 24; // once per day
98

10-
// these don't get coverage in tests as they have already been run once before the test
11-
/* istanbul ignore if */
12-
if (!fs.existsSync('./.data')) fs.mkdirSync('./.data');
13-
/* istanbul ignore if */
14-
if (!fs.existsSync('./.data/db')) fs.mkdirSync('./.data/db');
15-
169
// export for testing purposes
1710
export let db: Datastore;
1811
if (process.env.NODE_ENV === 'test') {

src/db/index.ts

Lines changed: 73 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,31 @@ import * as mongo from './mongo';
66
import * as neDb from './file';
77
import { Action } from '../proxy/actions/Action';
88
import MongoDBStore from 'connect-mongo';
9-
10-
let sink: Sink;
11-
if (config.getDatabase().type === 'mongo') {
12-
sink = mongo;
13-
} else if (config.getDatabase().type === 'fs') {
14-
sink = neDb;
15-
}
9+
import { processGitUrl } from '../proxy/routes/helper';
10+
import { initializeFolders } from './file/helper';
11+
12+
let _sink: Sink | null = null;
13+
14+
/** The start function is before any attempt to use the DB adaptor and causes the configuration
15+
* to be read. This allows the read of the config to be deferred, otherwise it will occur on
16+
* import.
17+
*/
18+
const start = () => {
19+
if (!_sink) {
20+
if (config.getDatabase().type === 'mongo') {
21+
console.log('Loading MongoDB database adaptor');
22+
_sink = mongo;
23+
} else if (config.getDatabase().type === 'fs') {
24+
console.log('Loading neDB database adaptor');
25+
initializeFolders();
26+
_sink = neDb;
27+
} else {
28+
console.error(`Unsupported database type: ${config.getDatabase().type}`);
29+
process.exit(1);
30+
}
31+
}
32+
return _sink;
33+
};
1634

1735
const isBlank = (str: string) => {
1836
return !str || /^\s*$/.test(str);
@@ -57,6 +75,7 @@ export const createUser = async (
5775
const errorMessage = `email cannot be empty`;
5876
throw new Error(errorMessage);
5977
}
78+
const sink = start();
6079
const existingUser = await sink.findUser(username);
6180
if (existingUser) {
6281
const errorMessage = `user ${username} already exists`;
@@ -95,7 +114,7 @@ export const createRepo = async (repo: AuthorisedRepo) => {
95114
throw new Error('URL cannot be empty');
96115
}
97116

98-
return sink.createRepo(toCreate) as Promise<Required<Repo>>;
117+
return start().createRepo(toCreate) as Promise<Required<Repo>>;
99118
};
100119

101120
export const isUserPushAllowed = async (url: string, user: string) => {
@@ -114,7 +133,7 @@ export const canUserApproveRejectPush = async (id: string, user: string) => {
114133
return false;
115134
}
116135

117-
const theRepo = await sink.getRepoByUrl(action.url);
136+
const theRepo = await start().getRepoByUrl(action.url);
118137

119138
if (theRepo?.users?.canAuthorise?.includes(user)) {
120139
console.log(`user ${user} can approve/reject for repo ${action.url}`);
@@ -140,35 +159,55 @@ export const canUserCancelPush = async (id: string, user: string) => {
140159
}
141160
};
142161

143-
export const getSessionStore = (): MongoDBStore | undefined =>
144-
sink.getSessionStore ? sink.getSessionStore() : undefined;
145-
export const getPushes = (query: Partial<PushQuery>): Promise<Action[]> => sink.getPushes(query);
146-
export const writeAudit = (action: Action): Promise<void> => sink.writeAudit(action);
147-
export const getPush = (id: string): Promise<Action | null> => sink.getPush(id);
148-
export const deletePush = (id: string): Promise<void> => sink.deletePush(id);
162+
export const getSessionStore = (): MongoDBStore | undefined => start().getSessionStore();
163+
export const getPushes = (query: Partial<PushQuery>): Promise<Action[]> => start().getPushes(query);
164+
export const writeAudit = (action: Action): Promise<void> => start().writeAudit(action);
165+
export const getPush = (id: string): Promise<Action | null> => start().getPush(id);
166+
export const deletePush = (id: string): Promise<void> => start().deletePush(id);
149167
export const authorise = (id: string, attestation: any): Promise<{ message: string }> =>
150-
sink.authorise(id, attestation);
151-
export const cancel = (id: string): Promise<{ message: string }> => sink.cancel(id);
168+
start().authorise(id, attestation);
169+
export const cancel = (id: string): Promise<{ message: string }> => start().cancel(id);
152170
export const reject = (id: string, attestation: any): Promise<{ message: string }> =>
153-
sink.reject(id, attestation);
154-
export const getRepos = (query?: Partial<RepoQuery>): Promise<Repo[]> => sink.getRepos(query);
155-
export const getRepo = (name: string): Promise<Repo | null> => sink.getRepo(name);
156-
export const getRepoByUrl = (url: string): Promise<Repo | null> => sink.getRepoByUrl(url);
157-
export const getRepoById = (_id: string): Promise<Repo | null> => sink.getRepoById(_id);
171+
start().reject(id, attestation);
172+
export const getRepos = (query?: Partial<RepoQuery>): Promise<Repo[]> => start().getRepos(query);
173+
export const getRepo = (name: string): Promise<Repo | null> => start().getRepo(name);
174+
export const getRepoByUrl = (url: string): Promise<Repo | null> => start().getRepoByUrl(url);
175+
export const getRepoById = (_id: string): Promise<Repo | null> => start().getRepoById(_id);
158176
export const addUserCanPush = (_id: string, user: string): Promise<void> =>
159-
sink.addUserCanPush(_id, user);
177+
start().addUserCanPush(_id, user);
160178
export const addUserCanAuthorise = (_id: string, user: string): Promise<void> =>
161-
sink.addUserCanAuthorise(_id, user);
179+
start().addUserCanAuthorise(_id, user);
162180
export const removeUserCanPush = (_id: string, user: string): Promise<void> =>
163-
sink.removeUserCanPush(_id, user);
181+
start().removeUserCanPush(_id, user);
164182
export const removeUserCanAuthorise = (_id: string, user: string): Promise<void> =>
165-
sink.removeUserCanAuthorise(_id, user);
166-
export const deleteRepo = (_id: string): Promise<void> => sink.deleteRepo(_id);
167-
export const findUser = (username: string): Promise<User | null> => sink.findUser(username);
168-
export const findUserByEmail = (email: string): Promise<User | null> => sink.findUserByEmail(email);
169-
export const findUserByOIDC = (oidcId: string): Promise<User | null> => sink.findUserByOIDC(oidcId);
170-
export const getUsers = (query?: Partial<UserQuery>): Promise<User[]> => sink.getUsers(query);
171-
export const deleteUser = (username: string): Promise<void> => sink.deleteUser(username);
172-
173-
export const updateUser = (user: Partial<User>): Promise<void> => sink.updateUser(user);
183+
start().removeUserCanAuthorise(_id, user);
184+
export const deleteRepo = (_id: string): Promise<void> => start().deleteRepo(_id);
185+
export const findUser = (username: string): Promise<User | null> => start().findUser(username);
186+
export const findUserByEmail = (email: string): Promise<User | null> =>
187+
start().findUserByEmail(email);
188+
export const findUserByOIDC = (oidcId: string): Promise<User | null> =>
189+
start().findUserByOIDC(oidcId);
190+
export const getUsers = (query?: Partial<UserQuery>): Promise<User[]> => start().getUsers(query);
191+
export const deleteUser = (username: string): Promise<void> => start().deleteUser(username);
192+
193+
export const updateUser = (user: Partial<User>): Promise<void> => start().updateUser(user);
194+
/**
195+
* Collect the Set of all host (host and port if specified) that we
196+
* will be proxying requests for, to be used to initialize the proxy.
197+
*
198+
* @return {string[]} an array of origins
199+
*/
200+
201+
export const getAllProxiedHosts = async (): Promise<string[]> => {
202+
const repos = await getRepos();
203+
const origins = new Set<string>();
204+
repos.forEach((repo) => {
205+
const parsedUrl = processGitUrl(repo.url);
206+
if (parsedUrl) {
207+
origins.add(parsedUrl.host);
208+
} // failures are logged by parsing util fn
209+
});
210+
return Array.from(origins);
211+
};
212+
174213
export type { PushQuery, Repo, Sink, User } from './types';

src/db/mongo/helper.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@ import { MongoClient, Db, Collection, Filter, Document, FindOptions } from 'mong
22
import { getDatabase } from '../../config';
33
import MongoDBStore from 'connect-mongo';
44

5-
const dbConfig = getDatabase();
6-
const connectionString = dbConfig.connectionString;
7-
const options = dbConfig.options;
8-
95
let _db: Db | null = null;
106

117
export const connect = async (collectionName: string): Promise<Collection> => {
8+
//retrieve config at point of use (rather than import)
9+
const dbConfig = getDatabase();
10+
const connectionString = dbConfig.connectionString;
11+
const options = dbConfig.options;
12+
1213
if (!_db) {
1314
if (!connectionString) {
1415
throw new Error('MongoDB connection string is not provided');
@@ -41,6 +42,10 @@ export const findOneDocument = async <T>(
4142
};
4243

4344
export const getSessionStore = () => {
45+
//retrieve config at point of use (rather than import)
46+
const dbConfig = getDatabase();
47+
const connectionString = dbConfig.connectionString;
48+
const options = dbConfig.options;
4449
return new MongoDBStore({
4550
mongoUrl: connectionString,
4651
collectionName: 'user_session',

src/proxy/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ const getServerOptions = (): ServerOptions => ({
3535
cert: getTLSEnabled() && getTLSCertPemPath() ? fs.readFileSync(getTLSCertPemPath()!) : undefined,
3636
});
3737

38-
export default class Proxy {
38+
export class Proxy {
3939
private httpServer: http.Server | null = null;
4040
private httpsServer: https.Server | null = null;
4141
private expressApp: Express | null = null;

src/proxy/routes/helper.ts

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import * as db from '../../db';
2-
31
/** Regex used to analyze un-proxied Git URLs */
42
const GIT_URL_REGEX = /(.+:\/\/)([^/]+)(\/.+\.git)(\/.+)*/;
53

@@ -174,21 +172,3 @@ export const validGitRequest = (gitPath: string, headers: any): boolean => {
174172
}
175173
return false;
176174
};
177-
178-
/**
179-
* Collect the Set of all host (host and port if specified) that we
180-
* will be proxying requests for, to be used to initialize the proxy.
181-
*
182-
* @return {string[]} an array of origins
183-
*/
184-
export const getAllProxiedHosts = async (): Promise<string[]> => {
185-
const repos = await db.getRepos();
186-
const origins = new Set<string>();
187-
repos.forEach((repo) => {
188-
const parsedUrl = processGitUrl(repo.url);
189-
if (parsedUrl) {
190-
origins.add(parsedUrl.host);
191-
} // failures are logged by parsing util fn
192-
});
193-
return Array.from(origins);
194-
};

src/proxy/routes/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import proxy from 'express-http-proxy';
33
import { PassThrough } from 'stream';
44
import getRawBody from 'raw-body';
55
import { executeChain } from '../chain';
6-
import { processUrlPath, validGitRequest, getAllProxiedHosts } from './helper';
6+
import { processUrlPath, validGitRequest } from './helper';
7+
import { getAllProxiedHosts } from '../../db';
78
import { ProxyOptions } from 'express-http-proxy';
89

910
enum ActionType {

0 commit comments

Comments
 (0)