feat: introduce streamers that handle the stream and anonymization from github

This commit is contained in:
tdurieux
2024-04-03 11:13:01 +01:00
parent 73019c1b44
commit 4d12641c7e
64 changed files with 419 additions and 257 deletions

75
src/server/database.ts Normal file
View File

@@ -0,0 +1,75 @@
import mongoose, { ConnectOptions } from "mongoose";
import Repository from "../core/Repository";
import config from "../config";
import AnonymizedRepositoryModel from "../core/model/anonymizedRepositories/anonymizedRepositories.model";
import AnonymousError from "../core/AnonymousError";
import AnonymizedPullRequestModel from "../core/model/anonymizedPullRequests/anonymizedPullRequests.model";
import PullRequest from "../core/PullRequest";
const MONGO_URL = `mongodb://${config.DB_USERNAME}:${config.DB_PASSWORD}@${config.DB_HOSTNAME}:27017/`;
export const database = mongoose.connection;
export let isConnected = false;
export async function connect() {
mongoose.set("strictQuery", false);
await mongoose.connect(MONGO_URL + "production", {
authSource: "admin",
appName: "Anonymous GitHub Server",
compressors: "zstd",
} as ConnectOptions);
isConnected = true;
return database;
}
export async function getRepository(
repoId: string,
opts: {
includeFiles: boolean;
} = {
includeFiles: true,
}
) {
if (!repoId || repoId == "undefined") {
throw new AnonymousError("repo_not_found", {
object: repoId,
httpStatus: 404,
});
}
const project: any = {};
if (!opts.includeFiles) {
project.originalFiles = 0;
}
const data = await AnonymizedRepositoryModel.findOne(
{ repoId },
project
).collation({
locale: "en",
strength: 2,
});
if (!data)
throw new AnonymousError("repo_not_found", {
object: repoId,
httpStatus: 404,
});
return new Repository(data);
}
export async function getPullRequest(pullRequestId: string) {
if (!pullRequestId || pullRequestId == "undefined") {
throw new AnonymousError("pull_request_not_found", {
object: pullRequestId,
httpStatus: 404,
});
}
const data = await AnonymizedPullRequestModel.findOne({
pullRequestId,
});
if (!data)
throw new AnonymousError("pull_request_not_found", {
object: pullRequestId,
httpStatus: 404,
});
return new PullRequest(data);
}

220
src/server/index.ts Normal file
View File

@@ -0,0 +1,220 @@
import { config as dotenv } from "dotenv";
dotenv();
import { createClient } from "redis";
import { resolve, join } from "path";
import { existsSync } from "fs";
import rateLimit from "express-rate-limit";
import { slowDown } from "express-slow-down";
import RedisStore from "rate-limit-redis";
import * as express from "express";
import * as compression from "compression";
import * as passport from "passport";
import { connect } from "./database";
import { initSession, router as connectionRouter } from "./routes/connection";
import router from "./routes";
import AnonymizedRepositoryModel from "../core/model/anonymizedRepositories/anonymizedRepositories.model";
import { conferenceStatusCheck, repositoryStatusCheck } from "./schedule";
import { startWorker } from "../queue";
import AnonymizedPullRequestModel from "../core/model/anonymizedPullRequests/anonymizedPullRequests.model";
import { getUser } from "./routes/route-utils";
import config from "../config";
function indexResponse(req: express.Request, res: express.Response) {
if (
req.path.startsWith("/script") ||
req.path.startsWith("/style") ||
req.path.startsWith("/favicon") ||
req.path.startsWith("/api")
) {
return res.status(404).send("Not found");
}
if (
req.params.repoId &&
req.headers["accept"] &&
req.headers["accept"].indexOf("text/html") == -1
) {
const repoId = req.path.split("/")[2];
// if it is not an html request, it assumes that the browser try to load a different type of resource
return res.redirect(
`/api/repo/${repoId}/file/${req.path.substring(
req.path.indexOf(repoId) + repoId.length + 1
)}`
);
}
res.sendFile(resolve("public", "index.html"));
}
export default async function start() {
const app = express();
app.use(express.json());
app.use(compression());
app.set("etag", "strong");
// handle session and connection
app.use(initSession());
app.use(passport.initialize());
app.use(passport.session());
startWorker();
const redisClient = createClient({
socket: {
host: config.REDIS_HOSTNAME,
port: config.REDIS_PORT,
},
});
redisClient.on("error", (err) => console.log("Redis Client Error", err));
await redisClient.connect();
function keyGenerator(
request: express.Request,
_response: express.Response
): string {
if (request.headers["cf-connecting-ip"]) {
return request.headers["cf-connecting-ip"] as string;
}
if (!request.ip && request.socket.remoteAddress) {
console.error("Warning: request.ip is missing!");
return request.socket.remoteAddress;
}
// remove port number from IPv4 addresses
return (request.ip || "").replace(/:\d+[^:]*$/, "");
}
const rate = rateLimit({
store: new RedisStore({
sendCommand: (...args: string[]) => redisClient.sendCommand(args),
}),
windowMs: 15 * 60 * 1000, // 15 minutes
skip: async (request: express.Request, response: express.Response) => {
try {
const user = await getUser(request);
if (user && user.isAdmin) return true;
} catch (_) {
// ignore: user not connected
}
return false;
},
max: async (request: express.Request, response: express.Response) => {
try {
const user = await getUser(request);
if (user) return config.RATE_LIMIT;
} catch (_) {
// ignore: user not connected
}
// if not logged in, limit to half the rate
return config.RATE_LIMIT / 2;
},
keyGenerator,
standardHeaders: true,
legacyHeaders: false,
message: (request: express.Request, response: express.Response) => {
return `You can only make ${config.RATE_LIMIT} requests every 15min. Please try again later.`;
},
});
const speedLimiter = slowDown({
windowMs: 15 * 60 * 1000, // 15 minutes
delayAfter: 50,
delayMs: () => 150,
maxDelayMs: 5000,
keyGenerator,
});
const webViewSpeedLimiter = slowDown({
windowMs: 15 * 60 * 1000, // 15 minutes
delayAfter: 200,
delayMs: () => 150,
maxDelayMs: 5000,
keyGenerator,
});
app.use("/github", rate, speedLimiter, connectionRouter);
// api routes
const apiRouter = express.Router();
app.use("/api", rate, apiRouter);
apiRouter.use("/admin", router.admin);
apiRouter.use("/options", router.option);
apiRouter.use("/conferences", router.conference);
apiRouter.use("/user", router.user);
apiRouter.use("/repo", router.repositoryPublic);
apiRouter.use("/repo", speedLimiter, router.file);
apiRouter.use("/repo", speedLimiter, router.repositoryPrivate);
apiRouter.use("/pr", speedLimiter, router.pullRequestPublic);
apiRouter.use("/pr", speedLimiter, router.pullRequestPrivate);
apiRouter.get("/message", async (_, res) => {
if (existsSync("./message.txt")) {
return res.sendFile(resolve("message.txt"));
}
res.sendStatus(404);
});
let stat: any = {};
setInterval(() => {
stat = {};
}, 1000 * 60 * 60);
apiRouter.get("/stat", async (_, res) => {
if (stat.nbRepositories) {
res.json(stat);
return;
}
const [nbRepositories, users, nbPageViews, nbPullRequests] =
await Promise.all([
AnonymizedRepositoryModel.estimatedDocumentCount(),
AnonymizedRepositoryModel.distinct("owner"),
AnonymizedRepositoryModel.collection
.aggregate([
{
$group: { _id: null, total: { $sum: "$pageView" } },
},
])
.toArray(),
AnonymizedPullRequestModel.estimatedDocumentCount(),
]);
stat = {
nbRepositories,
nbUsers: users.length,
nbPageViews: nbPageViews[0]?.total || 0,
nbPullRequests,
};
res.json(stat);
});
// web view
app.use("/w/", rate, webViewSpeedLimiter, router.webview);
app
.get("/", indexResponse)
.get("/404", indexResponse)
.get("/anonymize", indexResponse)
.get("/r/:repoId/?*", indexResponse)
.get("/repository/:repoId/?*", indexResponse);
app.use(
express.static(join("public"), {
etag: true,
lastModified: true,
maxAge: 3600, // 1h
})
);
app.get("*", indexResponse);
// start schedules
conferenceStatusCheck();
repositoryStatusCheck();
await connect();
app.listen(config.PORT);
console.log("Database connected and Server started on port: " + config.PORT);
}
start();

281
src/server/routes/admin.ts Normal file
View File

@@ -0,0 +1,281 @@
import { Queue } from "bullmq";
import * as express from "express";
import AnonymousError from "../../core/AnonymousError";
import AnonymizedRepositoryModel from "../../core/model/anonymizedRepositories/anonymizedRepositories.model";
import ConferenceModel from "../../core/model/conference/conferences.model";
import UserModel from "../../core/model/users/users.model";
import { cacheQueue, downloadQueue, removeQueue } from "../../queue";
import Repository from "../../core/Repository";
import User from "../../core/User";
import { ensureAuthenticated } from "./connection";
import { handleError, getUser, isOwnerOrAdmin, getRepo } from "./route-utils";
const router = express.Router();
// user needs to be connected for all user API
router.use(ensureAuthenticated);
router.use(
async (
req: express.Request,
res: express.Response,
next: express.NextFunction
) => {
const user = await getUser(req);
try {
// only admins are allowed here
isOwnerOrAdmin([], user);
next();
} catch (error) {
handleError(error, res, req);
}
}
);
router.post("/queue/:name/:repo_id", async (req, res) => {
let queue: Queue<Repository, void>;
if (req.params.name == "download") {
queue = downloadQueue;
} else if (req.params.name == "remove") {
queue = removeQueue;
} else {
return res.status(404).json({ error: "queue_not_found" });
}
const job = await queue.getJob(req.params.repo_id);
if (!job) {
return res.status(404).json({ error: "job_not_found" });
}
try {
await job.retry();
res.send("ok");
} catch (error) {
try {
await job.remove();
queue.add(job.name, job.data, job.opts);
res.send("ok");
} catch (error) {
res.status(500).send("error_retrying_job");
}
}
});
router.delete("/queue/:name/:repo_id", async (req, res) => {
let queue: Queue;
if (req.params.name == "download") {
queue = downloadQueue;
} else if (req.params.name == "remove") {
queue = removeQueue;
} else {
return res.status(404).json({ error: "queue_not_found" });
}
const job = await queue.getJob(req.params.repo_id);
if (!job) {
return res.status(404).json({ error: "job_not_found" });
}
await job.remove();
res.send("ok");
});
router.get("/queues", async (req, res) => {
const out = await Promise.all([
downloadQueue.getJobs([
"waiting",
"active",
"completed",
"failed",
"delayed",
]),
removeQueue.getJobs([
"waiting",
"active",
"completed",
"failed",
"delayed",
]),
cacheQueue.getJobs(["waiting", "active", "completed", "failed", "delayed"]),
]);
res.json({
downloadQueue: out[0],
removeQueue: out[1],
cacheQueue: out[2],
});
});
router.get("/repos", async (req, res) => {
const page = parseInt(req.query.page as string) || 1;
const limit = parseInt(req.query.limit as string) || 10;
const ready = req.query.ready == "true";
const error = req.query.error == "true";
const preparing = req.query.preparing == "true";
const remove = req.query.removed == "true";
const expired = req.query.expired == "true";
let sort: any = { _id: 1 };
if (req.query.sort) {
sort = {};
sort[req.query.sort as string] = -1;
}
let query = [];
if (req.query.search) {
query.push({ repoId: { $regex: req.query.search } });
}
const status: { status: string }[] = [];
query.push({ $or: status });
if (ready) {
status.push({ status: "ready" });
}
if (error) {
status.push({ status: "error" });
}
if (expired) {
status.push({ status: "expiring" });
status.push({ status: "expired" });
}
if (remove) {
status.push({ status: "removing" });
status.push({ status: "removed" });
}
if (preparing) {
status.push({ status: "preparing" });
status.push({ status: "download" });
}
const skipIndex = (page - 1) * limit;
const [total, results] = await Promise.all([
AnonymizedRepositoryModel.find(
{
$and: query,
},
{ originalFiles: 0 }
).countDocuments(),
AnonymizedRepositoryModel.find({ $and: query }, { originalFiles: 0 })
.skip(skipIndex)
.sort(sort)
.limit(limit)
.exec(),
]);
res.json({
query: { $and: query },
page,
total,
sort,
results,
});
});
// delete a repository
router.delete(
"/repos/:repoId/",
async (req: express.Request, res: express.Response) => {
const repo = await getRepo(req, res, { nocheck: true });
if (!repo) return;
try {
await cacheQueue.add(repo.repoId, repo, { jobId: repo.repoId });
return res.json({ status: repo.status });
} catch (error) {
handleError(error, res, req);
}
}
);
router.get("/users", async (req, res) => {
const page = parseInt(req.query.page as string) || 1;
const limit = parseInt(req.query.limit as string) || 10;
const skipIndex = (page - 1) * limit;
let sort: any = { _id: 1 };
if (req.query.sort) {
sort = {};
sort[req.query.sort as string] = -1;
}
let query = {};
if (req.query.search) {
query = { username: { $regex: req.query.search } };
}
res.json({
query: query,
page,
total: await UserModel.find(query).countDocuments(),
sort,
results: await UserModel.find(query)
.sort(sort)
.limit(limit)
.skip(skipIndex),
});
});
router.get(
"/users/:username",
async (req: express.Request, res: express.Response) => {
try {
const model = await UserModel.findOne({
username: req.params.username,
}).populate({
path: "repositories",
model: "Repository",
foreignField: "_id",
localField: "repositories",
});
if (!model) {
req.logout((error) => console.error(error));
throw new AnonymousError("user_not_found", {
httpStatus: 404,
});
}
const user = new User(model);
res.json(user);
} catch (error) {
handleError(error, res, req);
}
}
);
router.get(
"/users/:username/repos",
async (req: express.Request, res: express.Response) => {
try {
const model = await UserModel.findOne({ username: req.params.username });
if (!model) {
req.logout((error) => console.error(error));
throw new AnonymousError("user_not_found", {
httpStatus: 404,
});
}
const user = new User(model);
const repos = await user.getRepositories();
res.json(repos);
} catch (error) {
handleError(error, res, req);
}
}
);
router.get("/conferences", async (req, res) => {
const page = parseInt(req.query.page as string) || 1;
const limit = parseInt(req.query.limit as string) || 10;
const skipIndex = (page - 1) * limit;
let sort: any = { _id: 1 };
if (req.query.sort) {
sort = {};
sort[req.query.sort as string] = -1;
}
let query = {};
if (req.query.search) {
query = {
$or: [
{ name: { $regex: req.query.search } },
{ conferenceID: { $regex: req.query.search } },
],
};
}
res.json({
query: query,
page,
total: await ConferenceModel.find(query).estimatedDocumentCount(),
sort,
results: await ConferenceModel.find(query)
.sort(sort)
.limit(limit)
.skip(skipIndex),
});
});
export default router;

View File

@@ -0,0 +1,284 @@
import * as express from "express";
import AnonymousError from "../../core/AnonymousError";
import Conference from "../../core/Conference";
import ConferenceModel from "../../core/model/conference/conferences.model";
import { ensureAuthenticated } from "./connection";
import { handleError, getUser, isOwnerOrAdmin } from "./route-utils";
import { IConferenceDocument } from "../../core/model/conference/conferences.types";
const router = express.Router();
// user needs to be connected for all user API
router.use(ensureAuthenticated);
const plans = [
{
id: "free_conference",
name: "Free",
pricePerRepo: 0,
storagePerRepo: -1,
description: `<li><strong>Quota is deducted from user account</strong></li>
<li>No-download</li>
<li>Conference dashboard</li>`,
},
{
id: "premium_conference",
name: "Premium",
pricePerRepo: 0.5,
storagePerRepo: 500 * 8 * 1024,
description: `<li>500Mo / repository</li>
<li>Repository download</li>
<li>Conference dashboard</li>`,
},
{
id: "unlimited_conference",
name: "Unlimited",
pricePerRepo: 3,
storagePerRepo: 0,
description: `<li><strong>Unlimited</strong> repository size</li>
<li>Repository download</li>
<li>Conference dashboard</li>`,
},
];
router.get("/plans", async (req: express.Request, res: express.Response) => {
res.json(plans);
});
router.get("/", async (req: express.Request, res: express.Response) => {
try {
const user = await getUser(req);
const conferences = await Promise.all(
(
await ConferenceModel.find({
owners: { $in: user.model.id },
})
).map(async (data) => {
const conf = new Conference(data);
if (data.endDate < new Date() && data.status == "ready") {
await conf.updateStatus("expired");
}
return conf;
})
);
res.json(conferences.map((conf) => conf.toJSON()));
} catch (error) {
handleError(error, res, req);
}
});
function validateConferenceForm(conf: any) {
if (!conf.name)
throw new AnonymousError("conf_name_missing", {
object: conf,
httpStatus: 400,
});
if (!conf.conferenceID)
throw new AnonymousError("conf_id_missing", {
object: conf,
httpStatus: 400,
});
if (!conf.startDate)
throw new AnonymousError("conf_start_date_missing", {
object: conf,
httpStatus: 400,
});
if (!conf.endDate)
throw new AnonymousError("conf_end_date_missing", {
object: conf,
httpStatus: 400,
});
if (new Date(conf.startDate) > new Date(conf.endDate))
throw new AnonymousError("conf_start_date_invalid", {
object: conf,
httpStatus: 400,
});
if (new Date() > new Date(conf.endDate))
throw new AnonymousError("conf_end_date_invalid", {
object: conf,
httpStatus: 400,
});
if (plans.filter((p) => p.id == conf.plan.planID).length != 1)
throw new AnonymousError("invalid_plan", {
object: conf,
httpStatus: 400,
});
const plan = plans.filter((p) => p.id == conf.plan.planID)[0];
if (plan.pricePerRepo > 0) {
const billing = conf.billing;
if (!billing)
throw new AnonymousError("billing_missing", {
object: conf,
httpStatus: 400,
});
if (!billing.name)
throw new AnonymousError("billing_name_missing", {
object: conf,
httpStatus: 400,
});
if (!billing.email)
throw new AnonymousError("billing_email_missing", {
object: conf,
httpStatus: 400,
});
if (!billing.address)
throw new AnonymousError("billing_address_missing", {
object: conf,
httpStatus: 400,
});
if (!billing.city)
throw new AnonymousError("billing_city_missing", {
object: conf,
httpStatus: 400,
});
if (!billing.zip)
throw new AnonymousError("billing_zip_missing", {
object: conf,
httpStatus: 400,
});
if (!billing.country)
throw new AnonymousError("billing_country_missing", {
object: conf,
httpStatus: 400,
});
}
}
router.post(
"/:conferenceID?",
async (req: express.Request, res: express.Response) => {
try {
const user = await getUser(req);
let model: IConferenceDocument = new ConferenceModel();
if (req.params.conferenceID) {
const queryModel = await ConferenceModel.findOne({
conferenceID: req.params.conferenceID,
});
if (!queryModel) {
throw new AnonymousError("conference_not_found", {
httpStatus: 404,
});
}
model = queryModel;
isOwnerOrAdmin(model.owners, user);
}
validateConferenceForm(req.body);
model.name = req.body.name;
model.startDate = new Date(req.body.startDate);
model.endDate = new Date(req.body.endDate);
model.status = "ready";
model.url = req.body.url;
model.repositories = [];
model.options = req.body.options;
if (!req.params.conferenceID) {
model.owners.push(user.model.id);
model.conferenceID = req.body.conferenceID;
model.plan = {
planID: req.body.plan.planID,
pricePerRepository: plans.filter(
(p) => p.id == req.body.plan.planID
)[0].pricePerRepo,
quota: {
size: plans.filter((p) => p.id == req.body.plan.planID)[0]
.storagePerRepo,
file: 0,
repository: 0,
},
};
if (req.body.billing)
model.billing = {
name: req.body.billing.name,
email: req.body.billing.email,
address: req.body.billing.address,
address2: req.body.billing.address2,
city: req.body.billing.city,
zip: req.body.billing.zip,
country: req.body.billing.country,
vat: req.body.billing.vat,
};
}
await model.save();
res.send("ok");
} catch (error) {
if (
error instanceof Error &&
error.message?.indexOf(" duplicate key") > -1
) {
return handleError(
new AnonymousError("conf_id_used", {
object: req.params.conferenceID,
httpStatus: 400,
}),
res
);
}
handleError(error, res, req);
}
}
);
router.get(
"/:conferenceID",
async (req: express.Request, res: express.Response) => {
try {
const data = await ConferenceModel.findOne({
conferenceID: req.params.conferenceID,
});
if (!data)
throw new AnonymousError("conf_not_found", {
object: req.params.conferenceID,
httpStatus: 404,
});
const user = await getUser(req);
const conference = new Conference(data);
try {
isOwnerOrAdmin(conference.ownerIDs, user);
const o: any = conference.toJSON();
o.repositories = (await conference.repositories()).map((r) =>
r.toJSON()
);
res.json(o);
} catch (error) {
return res.json({
conferenceID: conference.conferenceID,
name: conference.name,
url: conference.url,
startDate: conference.startDate,
endDate: conference.endDate,
options: conference.options,
});
}
} catch (error) {
handleError(error, res, req);
}
}
);
router.delete(
"/:conferenceID",
async (req: express.Request, res: express.Response) => {
try {
const user = await getUser(req);
const data = await ConferenceModel.findOne({
conferenceID: req.params.conferenceID,
});
if (!data)
throw new AnonymousError("conf_not_found", {
object: req.params.conferenceID,
httpStatus: 400,
});
const conference = new Conference(data);
isOwnerOrAdmin(conference.ownerIDs, user);
await conference.remove();
res.send("ok");
} catch (error) {
handleError(error, res, req);
}
}
);
export default router;

View File

@@ -0,0 +1,130 @@
import { createClient } from "redis";
import * as passport from "passport";
import * as session from "express-session";
import RedisStore from "connect-redis";
import * as OAuth2Strategy from "passport-oauth2";
import { Profile, Strategy } from "passport-github2";
import * as express from "express";
import config from "../../config";
import UserModel from "../../core/model/users/users.model";
import { IUserDocument } from "../../core/model/users/users.types";
import AnonymousError from "../../core/AnonymousError";
export function ensureAuthenticated(
req: express.Request,
res: express.Response,
next: express.NextFunction
) {
if (req.isAuthenticated()) {
return next();
}
res.status(401).json({ error: "not_connected" });
}
const verify = async (
accessToken: string,
refreshToken: string,
profile: Profile,
done: OAuth2Strategy.VerifyCallback
): Promise<void> => {
let user: IUserDocument | null = null;
try {
user = await UserModel.findOne({ "externalIDs.github": profile.id });
if (user) {
user.accessTokens.github = accessToken;
} else {
const photo = profile.photos ? profile.photos[0]?.value : null;
user = new UserModel({
username: profile.username,
accessTokens: {
github: accessToken,
},
externalIDs: {
github: profile.id,
},
emails: profile.emails?.map((email) => {
return { email: email.value, default: false };
}),
photo,
});
if (user.emails?.length) user.emails[0].default = true;
}
await user.save();
} catch (error) {
console.error(error);
throw new AnonymousError("unable_to_connect_user", {
httpStatus: 500,
object: profile,
cause: error as Error,
});
} finally {
done(null, {
username: profile.username,
accessToken,
refreshToken,
profile,
user,
});
}
};
passport.use(
new Strategy(
{
clientID: config.CLIENT_ID,
clientSecret: config.CLIENT_SECRET,
callbackURL: config.AUTH_CALLBACK,
},
verify
)
);
passport.serializeUser((user: Express.User, done) => {
done(null, user);
});
passport.deserializeUser((user: Express.User, done) => {
done(null, user);
});
export function initSession() {
const redisClient = createClient({
legacyMode: false,
socket: {
port: config.REDIS_PORT,
host: config.REDIS_HOSTNAME,
},
});
redisClient.on("error", (err) => console.log("Redis Client Error", err));
redisClient.connect();
const redisStore = new RedisStore({
client: redisClient,
prefix: "anoGH_session:",
});
return session({
secret: config.SESSION_SECRET,
store: redisStore,
saveUninitialized: false,
resave: false,
});
}
export const router = express.Router();
router.get(
"/login",
passport.authenticate("github", { scope: ["repo"] }), // Note the scope here
function (req: express.Request, res: express.Response) {
res.redirect("/");
}
);
router.get(
"/auth",
passport.authenticate("github", { failureRedirect: "/" }),
function (req: express.Request, res: express.Response) {
res.redirect("/");
}
);

59
src/server/routes/file.ts Normal file
View File

@@ -0,0 +1,59 @@
import * as express from "express";
import AnonymizedFile from "../../core/AnonymizedFile";
import AnonymousError from "../../core/AnonymousError";
import { getRepo, handleError } from "./route-utils";
export const router = express.Router();
router.get(
"/:repoId/file/:path*",
async (req: express.Request, res: express.Response) => {
const anonymizedPath = decodeURI(
new URL(req.url, `${req.protocol}://${req.hostname}`).pathname.replace(
`/${req.params.repoId}/file/`,
""
)
);
if (anonymizedPath.endsWith("/")) {
return handleError(
new AnonymousError("folder_not_supported", {
httpStatus: 404,
object: anonymizedPath,
}),
res
);
}
const repo = await getRepo(req, res, {
nocheck: false,
includeFiles: false,
});
if (!repo) return;
try {
const f = new AnonymizedFile({
repository: repo,
anonymizedPath,
});
if (!f.isFileSupported()) {
throw new AnonymousError("file_not_supported", {
httpStatus: 403,
object: f,
});
}
if (req.query.download) {
res.attachment(
anonymizedPath.substring(anonymizedPath.lastIndexOf("/") + 1)
);
}
// cache the file for 5min
res.header("Cache-Control", "max-age=300");
await repo.countView();
await f.send(res);
} catch (error) {
return handleError(error, res, req);
}
}
);
export default router;

View File

@@ -0,0 +1,23 @@
import pullRequestPrivate from "./pullRequest-private";
import pullRequestPublic from "./pullRequest-public";
import repositoryPrivate from "./repository-private";
import repositoryPublic from "./repository-public";
import conference from "./conference";
import file from "./file";
import webview from "./webview";
import user from "./user";
import option from "./option";
import admin from "./admin";
export default {
pullRequestPrivate,
pullRequestPublic,
repositoryPrivate,
repositoryPublic,
file,
webview,
user,
option,
conference,
admin,
};

View File

@@ -0,0 +1,14 @@
import * as express from "express";
import config from "../../config";
export const router = express.Router();
router.get("/", async (req: express.Request, res: express.Response) => {
res.json({
ENABLE_DOWNLOAD: config.ENABLE_DOWNLOAD,
MAX_FILE_SIZE: config.MAX_FILE_SIZE,
MAX_REPO_SIZE: config.MAX_REPO_SIZE,
ANONYMIZATION_MASK: config.ANONYMIZATION_MASK,
});
});
export default router;

View File

@@ -0,0 +1,244 @@
import * as express from "express";
import { ensureAuthenticated } from "./connection";
import {
getPullRequest,
getUser,
handleError,
isOwnerOrAdmin,
} from "./route-utils";
import AnonymousError from "../../core/AnonymousError";
import { IAnonymizedPullRequestDocument } from "../../core/model/anonymizedPullRequests/anonymizedPullRequests.types";
import PullRequest from "../../core/PullRequest";
import AnonymizedPullRequestModel from "../../core/model/anonymizedPullRequests/anonymizedPullRequests.model";
import { RepositoryStatus } from "../../core/types";
const router = express.Router();
// user needs to be connected for all user API
router.use(ensureAuthenticated);
// refresh pullRequest
router.post(
"/:pullRequestId/refresh",
async (req: express.Request, res: express.Response) => {
try {
const pullRequest = await getPullRequest(req, res, { nocheck: true });
if (!pullRequest) return;
const user = await getUser(req);
isOwnerOrAdmin([pullRequest.owner.id], user);
await pullRequest.updateIfNeeded({ force: true });
res.json({ status: pullRequest.status });
} catch (error) {
handleError(error, res, req);
}
}
);
// delete a pullRequest
router.delete(
"/:pullRequestId/",
async (req: express.Request, res: express.Response) => {
const pullRequest = await getPullRequest(req, res, { nocheck: true });
if (!pullRequest) return;
try {
if (pullRequest.status == "removed")
throw new AnonymousError("is_removed", {
object: req.params.pullRequestId,
httpStatus: 410,
});
const user = await getUser(req);
isOwnerOrAdmin([pullRequest.owner.id], user);
await pullRequest.remove();
return res.json({ status: pullRequest.status });
} catch (error) {
handleError(error, res, req);
}
}
);
router.get(
"/:owner/:repository/:pullRequestId",
async (req: express.Request, res: express.Response) => {
const user = await getUser(req);
try {
const pullRequest = new PullRequest(
new AnonymizedPullRequestModel({
owner: user.id,
source: {
pullRequestId: parseInt(req.params.pullRequestId),
repositoryFullName: `${req.params.owner}/${req.params.repository}`,
},
})
);
pullRequest.owner = user;
await pullRequest.download();
res.json(pullRequest.toJSON());
} catch (error) {
handleError(error, res, req);
}
}
);
// get pullRequest information
router.get(
"/:pullRequestId/",
async (req: express.Request, res: express.Response) => {
try {
const pullRequest = await getPullRequest(req, res, { nocheck: true });
if (!pullRequest) return;
const user = await getUser(req);
isOwnerOrAdmin([pullRequest.owner.id], user);
res.json(pullRequest.toJSON());
} catch (error) {
handleError(error, res, req);
}
}
);
function validateNewPullRequest(pullRequestUpdate: any): void {
const validCharacters = /^[0-9a-zA-Z\-\_]+$/;
if (
!pullRequestUpdate.pullRequestId.match(validCharacters) ||
pullRequestUpdate.pullRequestId.length < 3
) {
throw new AnonymousError("invalid_pullRequestId", {
object: pullRequestUpdate,
httpStatus: 400,
});
}
if (!pullRequestUpdate.source.repositoryFullName) {
throw new AnonymousError("repository_not_specified", {
object: pullRequestUpdate,
httpStatus: 400,
});
}
if (!pullRequestUpdate.source.pullRequestId) {
throw new AnonymousError("pullRequestId_not_specified", {
object: pullRequestUpdate,
httpStatus: 400,
});
}
if (
parseInt(pullRequestUpdate.source.pullRequestId) !=
pullRequestUpdate.source.pullRequestId
) {
throw new AnonymousError("pullRequestId_is_not_a_number", {
object: pullRequestUpdate,
httpStatus: 400,
});
}
if (!pullRequestUpdate.options) {
throw new AnonymousError("options_not_provided", {
object: pullRequestUpdate,
httpStatus: 400,
});
}
if (!Array.isArray(pullRequestUpdate.terms)) {
throw new AnonymousError("invalid_terms_format", {
object: pullRequestUpdate,
httpStatus: 400,
});
}
}
function updatePullRequestModel(
model: IAnonymizedPullRequestDocument,
pullRequestUpdate: any
) {
model.options = {
terms: pullRequestUpdate.terms,
expirationMode: pullRequestUpdate.options.expirationMode,
expirationDate: pullRequestUpdate.options.expirationDate
? new Date(pullRequestUpdate.options.expirationDate)
: undefined,
update: pullRequestUpdate.options.update,
image: pullRequestUpdate.options.image,
link: pullRequestUpdate.options.link,
body: pullRequestUpdate.options.body,
title: pullRequestUpdate.options.title,
username: pullRequestUpdate.options.username,
origin: pullRequestUpdate.options.origin,
diff: pullRequestUpdate.options.diff,
comments: pullRequestUpdate.options.comments,
date: pullRequestUpdate.options.date,
};
}
// update a pullRequest
router.post(
"/:pullRequestId/",
async (req: express.Request, res: express.Response) => {
try {
const pullRequest = await getPullRequest(req, res, { nocheck: true });
if (!pullRequest) return;
const user = await getUser(req);
isOwnerOrAdmin([pullRequest.owner.id], user);
const pullRequestUpdate = req.body;
validateNewPullRequest(pullRequestUpdate);
pullRequest.model.anonymizeDate = new Date();
updatePullRequestModel(pullRequest.model, pullRequestUpdate);
// TODO handle conference
pullRequest.model.conference = pullRequestUpdate.conference;
await pullRequest.updateStatus(RepositoryStatus.PREPARING);
await pullRequest.updateIfNeeded({ force: true });
res.json(pullRequest.toJSON());
} catch (error) {
return handleError(error, res, req);
}
}
);
// add pullRequest
router.post("/", async (req: express.Request, res: express.Response) => {
const user = await getUser(req);
const pullRequestUpdate = req.body;
try {
validateNewPullRequest(pullRequestUpdate);
const pullRequest = new PullRequest(
new AnonymizedPullRequestModel({
owner: user.id,
options: pullRequestUpdate.options,
})
);
pullRequest.model.pullRequestId = pullRequestUpdate.pullRequestId;
pullRequest.model.anonymizeDate = new Date();
pullRequest.model.owner = user.id;
updatePullRequestModel(pullRequest.model, pullRequestUpdate);
pullRequest.source.accessToken = user.accessToken;
pullRequest.source.pullRequestId = pullRequestUpdate.source.pullRequestId;
pullRequest.source.repositoryFullName =
pullRequestUpdate.source.repositoryFullName;
pullRequest.conference = pullRequestUpdate.conference;
await pullRequest.anonymize();
res.send(pullRequest.toJSON());
} catch (error) {
if (
error instanceof Error &&
error.message.indexOf(" duplicate key") > -1
) {
return handleError(
new AnonymousError("pullRequestId_already_used", {
httpStatus: 400,
cause: error,
object: pullRequestUpdate,
}),
res,
req
);
}
return handleError(error, res, req);
}
});
export default router;

View File

@@ -0,0 +1,84 @@
import * as express from "express";
import { getPullRequest, handleError } from "./route-utils";
import AnonymousError from "../../core/AnonymousError";
const router = express.Router();
router.get(
"/:pullRequestId/options",
async (req: express.Request, res: express.Response) => {
try {
res.header("Cache-Control", "no-cache");
const pr = await getPullRequest(req, res, { nocheck: true });
if (!pr) return;
let redirectURL = null;
if (pr.status == "expired" && pr.options.expirationMode == "redirect") {
redirectURL = `https://github.com/${pr.source.repositoryFullName}/pull/${pr.source.pullRequestId}`;
} else {
if (
pr.status == "expired" ||
pr.status == "expiring" ||
pr.status == "removing" ||
pr.status == "removed"
) {
throw new AnonymousError("pull_request_expired", {
object: pr,
httpStatus: 410,
});
}
const fiveMinuteAgo = new Date();
fiveMinuteAgo.setMinutes(fiveMinuteAgo.getMinutes() - 5);
if (pr.status != "ready") {
if (
pr.model.statusDate < fiveMinuteAgo
// && repo.status != "preparing"
) {
await pr.updateIfNeeded({ force: true });
}
if (pr.status == "error") {
throw new AnonymousError(
pr.model.statusMessage
? pr.model.statusMessage
: "pull_request_not_available",
{
object: pr,
httpStatus: 500,
}
);
}
throw new AnonymousError("pull_request_not_ready", {
httpStatus: 404,
object: pr,
});
}
await pr.updateIfNeeded();
}
res.json({
url: redirectURL,
lastUpdateDate: pr.model.statusDate,
});
} catch (error) {
handleError(error, res, req);
}
}
);
router.get(
"/:pullRequestId/content",
async (req: express.Request, res: express.Response) => {
const pullRequest = await getPullRequest(req, res);
if (!pullRequest) return;
try {
await pullRequest.countView();
res.header("Cache-Control", "no-cache");
res.json(pullRequest.content());
} catch (error) {
handleError(error, res, req);
}
}
);
export default router;

View File

@@ -0,0 +1,559 @@
import * as express from "express";
import { ensureAuthenticated } from "./connection";
import * as db from "../database";
import { getRepo, getUser, handleError, isOwnerOrAdmin } from "./route-utils";
import { getRepositoryFromGitHub } from "../../core/source/GitHubRepository";
import gh = require("parse-github-url");
import AnonymizedRepositoryModel from "../../core/model/anonymizedRepositories/anonymizedRepositories.model";
import { IAnonymizedRepositoryDocument } from "../../core/model/anonymizedRepositories/anonymizedRepositories.types";
import Repository from "../../core/Repository";
import UserModel from "../../core/model/users/users.model";
import ConferenceModel from "../../core/model/conference/conferences.model";
import AnonymousError from "../../core/AnonymousError";
import { downloadQueue, removeQueue } from "../../queue";
import RepositoryModel from "../../core/model/repositories/repositories.model";
import User from "../../core/User";
import { RepositoryStatus } from "../../core/types";
import { IUserDocument } from "../../core/model/users/users.types";
import { checkToken } from "../../core/GitHubUtils";
const router = express.Router();
// user needs to be connected for all user API
router.use(ensureAuthenticated);
async function getTokenForAdmin(user: User, req: express.Request) {
if (user.isAdmin) {
try {
const existingRepo = await AnonymizedRepositoryModel.findOne(
{
"source.repositoryName": `${req.params.owner}/${req.params.repo}`,
},
{
"source.accessToken": 1,
owner: 1,
}
).populate({
path: "owner",
model: UserModel,
});
const user: IUserDocument = existingRepo?.owner as any;
if (user instanceof UserModel) {
const check = await checkToken(user.accessTokens.github);
if (check) {
return user.accessTokens.github;
}
}
if (existingRepo) {
return existingRepo.source.accessToken;
}
} catch (error) {
console.log(error);
}
}
}
// claim a repository
router.post("/claim", async (req: express.Request, res: express.Response) => {
const user = await getUser(req);
try {
if (!req.body.repoId) {
throw new AnonymousError("repoId_not_defined", {
object: req.body,
httpStatus: 400,
});
}
if (!req.body.repoUrl) {
throw new AnonymousError("repoUrl_not_defined", {
object: req.body,
httpStatus: 400,
});
}
const repoConfig = await db.getRepository(req.body.repoId);
if (repoConfig == null) {
throw new AnonymousError("repo_not_found", {
object: req.body,
httpStatus: 404,
});
}
const r = gh(req.body.repoUrl);
if (!r?.owner || !r?.name) {
throw new AnonymousError("repo_not_found", {
object: req.body,
httpStatus: 404,
});
}
const repo = await getRepositoryFromGitHub({
owner: r.owner,
repo: r.name,
accessToken: user.accessToken,
});
if (!repo) {
throw new AnonymousError("repo_not_found", {
object: req.body,
httpStatus: 404,
});
}
const dbRepo = await RepositoryModel.findById(
repoConfig.model.source.repositoryId
);
if (!dbRepo || dbRepo.externalId != repo.id) {
throw new AnonymousError("repo_not_found", {
object: req.body,
httpStatus: 404,
});
}
console.log(`${user.username} claims ${r.repository}.`);
repoConfig.owner = user;
await AnonymizedRepositoryModel.updateOne(
{ repoId: repoConfig.repoId },
{ $set: { owner: user.model.id } }
);
return res.send("Ok");
} catch (error) {
handleError(error, res, req);
}
});
// refresh repository
router.post(
"/:repoId/refresh",
async (req: express.Request, res: express.Response) => {
try {
const repo = await getRepo(req, res, {
nocheck: true,
includeFiles: false,
});
if (!repo) return;
if (
repo.status == "preparing" ||
repo.status == "removing" ||
repo.status == "expiring"
)
return;
const user = await getUser(req);
isOwnerOrAdmin([repo.owner.id], user);
await repo.updateIfNeeded({ force: true });
res.json({ status: repo.status });
} catch (error) {
handleError(error, res, req);
}
}
);
// delete a repository
router.delete(
"/:repoId/",
async (req: express.Request, res: express.Response) => {
const repo = await getRepo(req, res, {
nocheck: true,
includeFiles: false,
});
if (!repo) return;
// if (repo.status == "removing") return res.json({ status: repo.status });
try {
if (repo.status == "removed")
throw new AnonymousError("is_removed", {
object: req.params.repoId,
httpStatus: 410,
});
const user = await getUser(req);
isOwnerOrAdmin([repo.owner.id], user);
await repo.updateStatus(RepositoryStatus.REMOVING);
await removeQueue.add(repo.repoId, repo, { jobId: repo.repoId });
return res.json({ status: repo.status });
} catch (error) {
handleError(error, res, req);
}
}
);
router.get(
"/:owner/:repo/",
async (req: express.Request, res: express.Response) => {
const user = await getUser(req);
let token = user.accessToken;
if (user.isAdmin) {
token = (await getTokenForAdmin(user, req)) || token;
}
try {
const repo = await getRepositoryFromGitHub({
owner: req.params.owner,
repo: req.params.repo,
accessToken: token,
});
res.json(repo.toJSON());
} catch (error) {
handleError(error, res, req);
}
}
);
router.get(
"/:owner/:repo/branches",
async (req: express.Request, res: express.Response) => {
const user = await getUser(req);
let token = user.accessToken;
if (user.isAdmin) {
token = (await getTokenForAdmin(user, req)) || token;
}
try {
const repository = await getRepositoryFromGitHub({
accessToken: token,
owner: req.params.owner,
repo: req.params.repo,
});
return res.json(
await repository.branches({
accessToken: token,
force: req.query.force == "1",
})
);
} catch (error) {
handleError(error, res, req);
}
}
);
router.get(
"/:owner/:repo/readme",
async (req: express.Request, res: express.Response) => {
try {
const user = await getUser(req);
let token = user.accessToken;
if (user.isAdmin) {
token = (await getTokenForAdmin(user, req)) || token;
}
const repo = await getRepositoryFromGitHub({
owner: req.params.owner,
repo: req.params.repo,
accessToken: token,
});
if (!repo) {
throw new AnonymousError("repo_not_found", {
object: req.params.repoId,
httpStatus: 404,
});
}
return res.send(
await repo.readme({
accessToken: token,
force: req.query.force == "1",
branch: req.query.branch as string,
})
);
} catch (error) {
handleError(error, res, req);
}
}
);
// get repository information
router.get("/:repoId/", async (req: express.Request, res: express.Response) => {
try {
const repo = await getRepo(req, res, {
nocheck: true,
includeFiles: false,
});
if (!repo) return;
const user = await getUser(req);
isOwnerOrAdmin([repo.owner.id], user);
res.json((await db.getRepository(req.params.repoId)).toJSON());
} catch (error) {
handleError(error, res, req);
}
});
function validateNewRepo(repoUpdate: any): void {
const validCharacters = /^[0-9a-zA-Z\-\_]+$/;
if (
!repoUpdate.repoId.match(validCharacters) ||
repoUpdate.repoId.length < 3
) {
throw new AnonymousError("invalid_repoId", {
object: repoUpdate,
httpStatus: 400,
});
}
if (!repoUpdate.source.branch) {
throw new AnonymousError("branch_not_specified", {
object: repoUpdate,
httpStatus: 400,
});
}
if (!repoUpdate.source.commit) {
throw new AnonymousError("commit_not_specified", {
object: repoUpdate,
httpStatus: 400,
});
}
if (!repoUpdate.options) {
throw new AnonymousError("options_not_provided", {
object: repoUpdate,
httpStatus: 400,
});
}
if (!Array.isArray(repoUpdate.terms)) {
throw new AnonymousError("invalid_terms_format", {
object: repoUpdate,
httpStatus: 400,
});
}
if (!/^[a-fA-F0-9]+$/.test(repoUpdate.source.commit)) {
throw new AnonymousError("invalid_commit_format", {
object: repoUpdate,
httpStatus: 400,
});
}
}
function updateRepoModel(
model: IAnonymizedRepositoryDocument,
repoUpdate: any
) {
if (repoUpdate.source.type) {
model.source.type = repoUpdate.source.type;
if (
model.source.type != "GitHubStream" &&
model.source.type != "GitHubDownload"
) {
model.source.type = "GitHubStream";
}
}
model.source.commit = repoUpdate.source.commit;
model.source.branch = repoUpdate.source.branch;
model.options = {
terms: repoUpdate.terms,
expirationMode: repoUpdate.options.expirationMode,
expirationDate: repoUpdate.options.expirationDate
? new Date(repoUpdate.options.expirationDate)
: undefined,
update: repoUpdate.options.update,
image: repoUpdate.options.image,
pdf: repoUpdate.options.pdf,
notebook: repoUpdate.options.notebook,
link: repoUpdate.options.link,
page: repoUpdate.options.page,
pageSource: repoUpdate.options.pageSource,
};
}
// update a repository
router.post(
"/:repoId/",
async (req: express.Request, res: express.Response) => {
try {
const repo = await getRepo(req, res, {
nocheck: true,
includeFiles: false,
});
if (!repo) return;
const user = await getUser(req);
isOwnerOrAdmin([repo.owner.id], user);
const repoUpdate = req.body;
validateNewRepo(repoUpdate);
if (repoUpdate.source.commit != repo.model.source.commit) {
repo.model.anonymizeDate = new Date();
repo.model.source.commit = repoUpdate.source.commit;
await repo.remove();
}
updateRepoModel(repo.model, repoUpdate);
repo.source.type = "GitHubStream";
const removeRepoFromConference = async (conferenceID: string) => {
const conf = await ConferenceModel.findOne({
conferenceID,
});
if (conf) {
const r = conf.repositories.filter((r) => r.id == repo.model.id);
if (r.length == 1) r[0].removeDate = new Date();
await conf.save();
}
};
if (!repoUpdate.conference) {
// remove conference
if (repo.model.conference) {
await removeRepoFromConference(repo.model.conference);
}
} else if (repoUpdate.conference != repo.model.conference) {
// update/add conference
const conf = await ConferenceModel.findOne({
conferenceID: repoUpdate.conference,
});
if (conf) {
if (
new Date() < conf.startDate ||
new Date() > conf.endDate ||
conf.status !== "ready"
) {
throw new AnonymousError("conf_not_activated", {
object: conf,
httpStatus: 400,
});
}
const f = conf.repositories.filter((r) => r.id == repo.model.id);
if (f.length) {
// the repository already referenced the conference
f[0].addDate = new Date();
f[0].removeDate = undefined;
} else {
conf.repositories.push({
id: repo.model.id,
addDate: new Date(),
});
}
if (repo.model.conference) {
await removeRepoFromConference(repo.model.conference);
}
await conf.save();
}
}
repo.model.conference = repoUpdate.conference;
await repo.updateStatus(RepositoryStatus.PREPARING);
res.json({ status: repo.status });
await downloadQueue.add(repo.repoId, repo, { jobId: repo.repoId });
} catch (error) {
return handleError(error, res, req);
}
}
);
// add repository
router.post("/", async (req: express.Request, res: express.Response) => {
const user = await getUser(req);
const repoUpdate = req.body;
try {
try {
await db.getRepository(repoUpdate.repoId, { includeFiles: false });
throw new AnonymousError("repoId_already_used", {
httpStatus: 400,
object: repoUpdate,
});
} catch (error: any) {
if (error.message == "repo_not_found") {
// the repository does not exist yet
} else {
throw error;
}
}
validateNewRepo(repoUpdate);
const r = gh(repoUpdate.fullName);
if (!r?.owner || !r?.name) {
throw new AnonymousError("repo_not_found", {
object: req.body,
httpStatus: 404,
});
}
const repository = await getRepositoryFromGitHub({
accessToken: user.accessToken,
owner: r.owner,
repo: r.name,
});
if (!repository) {
throw new AnonymousError("repo_not_found", {
object: req.body,
httpStatus: 404,
});
}
const repo = new AnonymizedRepositoryModel();
repo.repoId = repoUpdate.repoId;
repo.anonymizeDate = new Date();
repo.owner = user.id;
updateRepoModel(repo, repoUpdate);
repo.source.type = "GitHubStream";
repo.source.accessToken = user.accessToken;
repo.source.repositoryId = repository.model.id;
repo.source.repositoryName = repoUpdate.fullName;
// if (repo.source.type === "GitHubDownload") {
// // details.size is in kilobytes
// if (
// repository.size === undefined ||
// repository.size > config.MAX_REPO_SIZE
// ) {
// throw new AnonymousError("invalid_mode", {
// object: repository,
// httpStatus: 400,
// });
// }
// }
// if (
// repository.size !== undefined &&
// repository.size < config.AUTO_DOWNLOAD_REPO_SIZE
// ) {
// repo.source.type = "GitHubDownload";
// }
repo.conference = repoUpdate.conference;
await repo.save();
if (repoUpdate.conference) {
const conf = await ConferenceModel.findOne({
conferenceID: repoUpdate.conference,
});
if (conf) {
if (
new Date() < conf.startDate ||
new Date() > conf.endDate ||
conf.status !== "ready"
) {
await repo.deleteOne();
throw new AnonymousError("conf_not_activated", {
object: conf,
httpStatus: 400,
});
}
conf.repositories.push({
id: repo.id,
addDate: new Date(),
});
await conf.save();
}
}
res.send({ status: repo.status });
downloadQueue.add(repo.repoId, new Repository(repo), {
jobId: repo.repoId,
attempts: 3,
});
} catch (error) {
if (
error instanceof Error &&
error.message?.indexOf(" duplicate key") > -1
) {
return handleError(
new AnonymousError("repoId_already_used", {
httpStatus: 400,
cause: error,
object: repoUpdate,
}),
res,
req
);
}
return handleError(error, res, req);
}
});
export default router;

View File

@@ -0,0 +1,166 @@
import { promisify } from "util";
import * as express from "express";
import * as stream from "stream";
import config from "../../config";
import { getRepo, handleError } from "./route-utils";
import AnonymousError from "../../core/AnonymousError";
import { downloadQueue } from "../../queue";
import { RepositoryStatus } from "../../core/types";
const router = express.Router();
router.get(
"/:repoId/zip",
async (req: express.Request, res: express.Response) => {
const pipeline = promisify(stream.pipeline);
try {
if (!config.ENABLE_DOWNLOAD) {
throw new AnonymousError("download_not_enabled", {
httpStatus: 403,
object: req.params.repoId,
});
}
const repo = await getRepo(req, res);
if (!repo) return;
let download = false;
const conference = await repo.conference();
if (conference) {
download =
conference.quota.size > -1 &&
!!config.ENABLE_DOWNLOAD &&
repo.source.type == "GitHubDownload";
}
if (
repo.size.storage < config.FREE_DOWNLOAD_REPO_SIZE * 1024 &&
repo.source.type == "GitHubDownload"
) {
download = true;
}
if (!download) {
throw new AnonymousError("download_not_enabled", {
httpStatus: 403,
object: req.params.repoId,
});
}
res.attachment(`${repo.repoId}.zip`);
// cache the file for 6 hours
res.header("Cache-Control", "max-age=21600");
await pipeline(await repo.zip(), res);
} catch (error) {
handleError(error, res, req);
}
}
);
router.get(
"/:repoId/files",
async (req: express.Request, res: express.Response) => {
res.header("Cache-Control", "no-cache");
const repo = await getRepo(req, res, { includeFiles: true });
if (!repo) return;
try {
res.json(await repo.anonymizedFiles({ includeSha: false }));
} catch (error) {
handleError(error, res, req);
}
}
);
router.get(
"/:repoId/options",
async (req: express.Request, res: express.Response) => {
try {
res.header("Cache-Control", "no-cache");
const repo = await getRepo(req, res, {
nocheck: true,
includeFiles: false,
});
if (!repo) return;
let redirectURL = null;
if (
repo.status == "expired" &&
repo.options.expirationMode == "redirect" &&
repo.model.source.repositoryName
) {
redirectURL = `https://github.com/${repo.model.source.repositoryName}`;
} else {
if (
repo.status == "expired" ||
repo.status == "expiring" ||
repo.status == "removing" ||
repo.status == "removed"
) {
throw new AnonymousError("repository_expired", {
object: repo,
httpStatus: 410,
});
}
const fiveMinuteAgo = new Date();
fiveMinuteAgo.setMinutes(fiveMinuteAgo.getMinutes() - 5);
if (repo.status != "ready") {
if (
repo.model.statusDate < fiveMinuteAgo
// && repo.status != "preparing"
) {
await repo.updateStatus(RepositoryStatus.PREPARING);
await downloadQueue.add(repo.repoId, repo, {
jobId: repo.repoId,
attempts: 3,
});
}
if (repo.status == "error") {
throw new AnonymousError(
repo.model.statusMessage
? repo.model.statusMessage
: "repository_not_available",
{
object: repo,
httpStatus: 500,
}
);
}
throw new AnonymousError("repository_not_ready", {
httpStatus: 404,
object: repo,
});
}
await repo.updateIfNeeded();
}
let download = false;
const conference = await repo.conference();
if (conference) {
download =
conference.quota.size > -1 &&
!!config.ENABLE_DOWNLOAD &&
repo.source.type == "GitHubDownload";
}
if (
repo.size.storage < config.FREE_DOWNLOAD_REPO_SIZE * 1024 &&
repo.source.type == "GitHubDownload"
) {
download = true;
}
res.json({
url: redirectURL,
download,
lastUpdateDate: repo.model.source.commitDate
? repo.model.source.commitDate
: repo.model.anonymizeDate,
});
} catch (error) {
handleError(error, res, req);
}
}
);
export default router;

View File

@@ -0,0 +1,153 @@
import * as express from "express";
import AnonymousError from "../../core/AnonymousError";
import * as db from "../database";
import UserModel from "../../core/model/users/users.model";
import User from "../../core/User";
import { HTTPError } from "got";
export async function getPullRequest(
req: express.Request,
res: express.Response,
opt?: { nocheck?: boolean }
) {
try {
const pullRequest = await db.getPullRequest(req.params.pullRequestId);
if (opt?.nocheck == true) {
} else {
// redirect if the repository is expired
if (
pullRequest.status == "expired" &&
pullRequest.options.expirationMode == "redirect"
) {
res.redirect(
`http://github.com/${pullRequest.source.repositoryFullName}/pull/${pullRequest.source.pullRequestId}`
);
return null;
}
pullRequest.check();
}
return pullRequest;
} catch (error) {
handleError(error, res, req);
return null;
}
}
export async function getRepo(
req: express.Request,
res: express.Response,
opt: { nocheck?: boolean; includeFiles?: boolean } = {
nocheck: false,
includeFiles: false,
}
) {
try {
const repo = await db.getRepository(req.params.repoId, {
includeFiles: opt.includeFiles === true,
});
if (opt.nocheck == true) {
} else {
// redirect if the repository is expired
if (
repo.status == "expired" &&
repo.options.expirationMode == "redirect" &&
repo.model.source.repositoryId
) {
res.redirect(`https://github.com/${repo.model.source.repositoryName}`);
return null;
}
repo.check();
}
return repo;
} catch (error) {
handleError(error, res, req);
return null;
}
}
export function isOwnerOrAdmin(authorizedUsers: string[], user: User) {
if (authorizedUsers.indexOf(user.model.id) == -1 && !user.isAdmin) {
throw new AnonymousError("not_authorized", {
httpStatus: 401,
});
}
}
function printError(error: any, req?: express.Request) {
if (error instanceof AnonymousError) {
let message = `[ERROR] ${error.toString()} ${error.stack
?.split("\n")[1]
.trim()}`;
if (req) {
message += ` ${req.originalUrl}`;
// ignore common error
if (req.originalUrl === "/api/repo/undefined/options") return;
}
console.error(message);
} else if (error instanceof HTTPError) {
let message = `[ERROR] HTTP.${
error.code
} ${error.message.toString()} ${error.stack?.split("\n")[1].trim()}`;
console.error(message);
} else if (error instanceof Error) {
console.error(error);
} else {
console.error(error);
}
}
export function handleError(
error: any,
res?: express.Response,
req?: express.Request
) {
printError(error, req);
let message = error;
if (error instanceof Error) {
message = error.message;
}
let status = 500;
if (error.httpStatus) {
status = error.httpStatus;
} else if (error.$metadata?.httpStatusCode) {
status = error.$metadata.httpStatusCode;
} else if (
message &&
(message.indexOf("not_found") > -1 || message.indexOf("(Not Found)") > -1)
) {
status = 404;
} else if (message && message.indexOf("not_connected") > -1) {
status = 401;
}
if (res && !res.headersSent) {
res.status(status).send({ error: message });
}
return;
}
export async function getUser(req: express.Request) {
function notConnected(): never {
req.logout((error) => {
if (error) {
console.error(`[ERROR] Error while logging out: ${error}`);
}
});
throw new AnonymousError("not_connected", {
httpStatus: 401,
});
}
if (!req.user) {
notConnected();
}
const user = (req.user as any).user;
if (!user) {
notConnected();
}
const model = await UserModel.findById(user._id);
if (!model) {
notConnected();
}
return new User(model);
}

162
src/server/routes/user.ts Normal file
View File

@@ -0,0 +1,162 @@
import * as express from "express";
import config from "../../config";
import { ensureAuthenticated } from "./connection";
import { handleError, getUser, isOwnerOrAdmin } from "./route-utils";
import UserModel from "../../core/model/users/users.model";
import User from "../../core/User";
const router = express.Router();
// user needs to be connected for all user API
router.use(ensureAuthenticated);
router.get("/logout", async (req: express.Request, res: express.Response) => {
try {
req.logout((error) => {
if (error) {
console.error(`[ERROR] Logout error: ${error}`);
}
});
res.redirect("/");
} catch (error) {
handleError(error, res, req);
}
});
router.get("/", async (req: express.Request, res: express.Response) => {
try {
const user = await getUser(req);
res.json({
username: user.username,
photo: user.photo,
isAdmin: user.isAdmin,
});
} catch (error) {
handleError(error, res, req);
}
});
router.get("/quota", async (req: express.Request, res: express.Response) => {
try {
const user = await getUser(req);
const repositories = await user.getRepositories();
const sizes = await Promise.all(
repositories
.filter((r) => r.status == "ready")
.map((r) => r.computeSize())
);
res.json({
storage: {
used: sizes.reduce((sum, i) => sum + i.storage, 0),
total: config.DEFAULT_QUOTA,
},
file: {
used: sizes.reduce((sum, i) => sum + i.file, 0),
total: 0,
},
repository: {
used: repositories.filter((f) => f.status == "ready").length,
total: 20,
},
});
} catch (error) {
handleError(error, res, req);
}
});
router.get("/default", async (req: express.Request, res: express.Response) => {
try {
const user = await getUser(req);
res.json(user.default);
} catch (error) {
handleError(error, res, req);
}
});
router.post("/default", async (req: express.Request, res: express.Response) => {
try {
const user = await getUser(req);
const d = req.body;
user.model.default = d;
await user.model.save();
res.send("ok");
} catch (error) {
handleError(error, res, req);
}
});
router.get(
"/anonymized_repositories",
async (req: express.Request, res: express.Response) => {
try {
const user = await getUser(req);
res.json(
(await user.getRepositories()).map((x) => {
return x.toJSON();
})
);
} catch (error) {
handleError(error, res, req);
}
}
);
router.get(
"/anonymized_pull_requests",
async (req: express.Request, res: express.Response) => {
try {
const user = await getUser(req);
res.json(
(await user.getPullRequests()).map((x) => {
return x.toJSON();
})
);
} catch (error) {
handleError(error, res, req);
}
}
);
async function getAllRepositories(user: User, force: boolean) {
const repos = await user.getGitHubRepositories({
force,
});
return repos.map((x) => {
return {
fullName: x.fullName,
id: x.id,
};
});
}
router.get(
"/all_repositories",
async (req: express.Request, res: express.Response) => {
try {
const user = await getUser(req);
res.json(await getAllRepositories(user, req.query.force == "1"));
} catch (error) {
handleError(error, res, req);
}
}
);
router.get(
"/:username/all_repositories",
async (req: express.Request, res: express.Response) => {
try {
const loggedUser = await getUser(req);
isOwnerOrAdmin([req.params.username], loggedUser);
const model = await UserModel.findOne({ username: req.params.username });
if (!model) {
throw new Error("User not found");
}
const user = new User(model);
res.json(await getAllRepositories(user, req.query.force == "1"));
} catch (error) {
handleError(error, res, req);
}
}
);
export default router;

View File

@@ -0,0 +1,118 @@
import * as express from "express";
import { getRepo, handleError } from "./route-utils";
import * as path from "path";
import AnonymizedFile from "../../core/AnonymizedFile";
import AnonymousError from "../../core/AnonymousError";
import { Tree, TreeElement } from "../../core/types";
import * as marked from "marked";
import { streamToString } from "../../core/anonymize-utils";
const router = express.Router();
const indexPriority = [
"index.html",
"index.htm",
"index.md",
"index.txt",
"index.org",
"index.1st",
"index",
"readme.md",
"readme.txt",
"readme.org",
"readme.1st",
"readme",
];
async function webView(req: express.Request, res: express.Response) {
const repo = await getRepo(req, res);
if (!repo) return;
try {
if (!repo.options.page || !repo.options.pageSource) {
throw new AnonymousError("page_not_activated", {
httpStatus: 400,
object: repo,
});
}
if (repo.options.pageSource?.branch != repo.model.source.branch) {
throw new AnonymousError("page_not_supported_on_different_branch", {
httpStatus: 400,
object: repo,
});
}
let requestPath = path.join(
repo.options.pageSource?.path,
req.path.substring(
req.path.indexOf(req.params.repoId) + req.params.repoId.length
)
);
let f = new AnonymizedFile({
repository: repo,
anonymizedPath: requestPath,
});
if (requestPath[requestPath.length - 1] == "/") {
// find index file
const paths = f.anonymizedPath.trim().split("/");
let currentAnonymized: TreeElement = await repo.anonymizedFiles({
includeSha: true,
});
for (let i = 0; i < paths.length; i++) {
const fileName = paths[i];
if (fileName == "") {
continue;
}
if (!(currentAnonymized as Tree)[fileName]) {
throw new AnonymousError("file_not_found", {
object: repo,
httpStatus: 404,
});
}
currentAnonymized = (currentAnonymized as Tree)[fileName];
}
let best_match = null;
indexSelector: for (const p of indexPriority) {
for (let filename in currentAnonymized) {
if (filename.toLowerCase() == p) {
best_match = filename;
break indexSelector;
}
}
}
if (best_match) {
requestPath = path.join(requestPath, best_match);
f = new AnonymizedFile({
repository: repo,
anonymizedPath: requestPath,
});
}
}
if (!f.isFileSupported()) {
throw new AnonymousError("file_not_supported", {
httpStatus: 400,
object: f,
});
}
if (f.extension() == "md") {
const content = await streamToString(await f.anonymizedContent());
res
.contentType("html")
.send(marked.marked(content, { headerIds: false, mangle: false }));
} else {
f.send(res);
}
} catch (error) {
handleError(error, res, req);
}
}
router.get("/:repoId/*", webView);
router.get("/:repoId", (req: express.Request, res: express.Response) => {
res.redirect("/w" + req.url + "/");
});
export default router;

53
src/server/schedule.ts Normal file
View File

@@ -0,0 +1,53 @@
import * as schedule from "node-schedule";
import Conference from "../core/Conference";
import AnonymizedRepositoryModel from "../core/model/anonymizedRepositories/anonymizedRepositories.model";
import ConferenceModel from "../core/model/conference/conferences.model";
import Repository from "../core/Repository";
export function conferenceStatusCheck() {
// check every 6 hours the status of the conferences
const job = schedule.scheduleJob("0 */6 * * *", async () => {
(await ConferenceModel.find({ status: { $eq: "ready" } })).forEach(
async (data) => {
const conference = new Conference(data);
if (conference.isExpired() && conference.status == "ready") {
try {
await conference.expire();
} catch (error) {
console.error(error);
}
}
}
);
});
}
export function repositoryStatusCheck() {
// check every 6 hours the status of the repositories
const job = schedule.scheduleJob("0 */6 * * *", async () => {
console.log("[schedule] Check repository status and unused repositories");
(
await AnonymizedRepositoryModel.find({
status: { $eq: "ready" },
isReseted: { $eq: false },
})
).forEach((data) => {
const repo = new Repository(data);
try {
repo.check();
} catch (error) {
console.log(`Repository ${repo.repoId} is expired`);
}
const fourMonthAgo = new Date();
fourMonthAgo.setMonth(fourMonthAgo.getMonth() - 4);
if (repo.model.lastView < fourMonthAgo) {
repo.removeCache().then(() => {
console.log(
`Repository ${repo.repoId} not visited for 4 months remove the cached files`
);
});
}
});
});
}