Skip to content

Commit

Permalink
implemented github auth & account checks
Browse files Browse the repository at this point in the history
  • Loading branch information
pk910 committed Jun 24, 2023
1 parent 7c0bc7b commit e545241
Show file tree
Hide file tree
Showing 8 changed files with 408 additions and 37 deletions.
24 changes: 19 additions & 5 deletions src/db/FaucetDatabase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,14 +238,28 @@ export class FaucetDatabase {
await this.db.run("DELETE FROM KeyValueStore WHERE " + SQL.field("Key") + " = ?", [key]);
}

private async selectSessions(whereSql: string, whereArgs: any[], skipData?: boolean): Promise<FaucetSessionStoreData[]> {


private selectSessions(whereSql: string, whereArgs: any[], skipData?: boolean): Promise<FaucetSessionStoreData[]> {
let sql = [
"SELECT SessionId,Status,StartTime,TargetAddr,DropAmount,RemoteIP,Tasks",
(skipData ? "" : ",Data,ClaimData"),
" FROM Sessions WHERE ",
"FROM Sessions WHERE ",
whereSql
].join("");
let rows = await this.db.all(sql, whereArgs) as {
return this.selectSessionsSql(sql, whereArgs, skipData);
}

public async selectSessionsSql(selectSql: string, args: any[], skipData?: boolean): Promise<FaucetSessionStoreData[]> {
let fields = ["SessionId","Status","StartTime","TargetAddr","DropAmount","RemoteIP","Tasks"];
if(!skipData)
fields.push("Data","ClaimData");

let sql = [
"SELECT ",
fields.map((f) => "Sessions." + f).join(","),
" ",
selectSql
].join("");
let rows = await this.db.all(sql, args) as {
SessionId: string;
Status: string;
StartTime: number;
Expand Down
16 changes: 15 additions & 1 deletion src/modules/github/GithubConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,22 @@ export interface IGithubConfig extends IBaseModuleConfig {
appClientId: string;
appSecret: string;
authTimeout: number;

cacheTime: number;
checks: IGithubCheckConfig[];
restrictions: IGithubRestrictionConfig[];
}

export interface IGithubCheckConfig {
minAccountAge?: number;
minRepoCount?: number;
minFollowers?: number;
minOwnRepoCount?: number;
minOwnRepoStars?: number;
required?: boolean;
message?: string;
rewardFactor?: number;
}

export interface IGithubRestrictionConfig {
limitCount: number;
limitAmount: number;
Expand All @@ -22,5 +34,7 @@ export const defaultConfig: IGithubConfig = {
appClientId: null,
appSecret: null,
authTimeout: 86400,
cacheTime: 86400,
checks: [],
restrictions: [],
}
127 changes: 127 additions & 0 deletions src/modules/github/GithubDB.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { FaucetDbDriver } from '../../db/FaucetDatabase';
import { FaucetModuleDB } from '../../db/FaucetModuleDB';
import { SQL } from '../../db/SQL';
import { FaucetSessionStoreData } from '../../session/FaucetSession';
import { IGithubInfo } from './GithubResolver';

export class GithubDB extends FaucetModuleDB {
protected override latestSchemaVersion = 1;

protected override async upgradeSchema(version: number): Promise<number> {
switch(version) {
case 0:
version = 1;
await this.db.exec(SQL.driverSql({
[FaucetDbDriver.SQLITE]: `
CREATE TABLE "GithubCache" (
"UserId" TEXT NOT NULL UNIQUE,
"Json" TEXT NOT NULL,
"Timeout" INTEGER NOT NULL,
PRIMARY KEY("UserId")
);`,
[FaucetDbDriver.MYSQL]: `
CREATE TABLE GithubCache (
UserId VARCHAR(40) NOT NULL,
Json TEXT NOT NULL,
Timeout INT(11) NOT NULL,
PRIMARY KEY(UserId)
);`,
}));
await this.db.exec(SQL.driverSql({
[FaucetDbDriver.SQLITE]: `CREATE INDEX "GithubCacheTimeIdx" ON "GithubCache" ("Timeout" ASC);`,
[FaucetDbDriver.MYSQL]: `ALTER TABLE GithubCache ADD INDEX GithubCacheTimeIdx (Timeout);`,
}));
await this.db.exec(SQL.driverSql({
[FaucetDbDriver.SQLITE]: `
CREATE TABLE "GithubSessions" (
"SessionId" TEXT NOT NULL UNIQUE,
"UserId" TEXT NOT NULL,
PRIMARY KEY("SessionId")
);`,
[FaucetDbDriver.MYSQL]: `
CREATE TABLE GithubSessions (
SessionId CHAR(36) NOT NULL,
UserId VARCHAR(40) NOT NULL,
PRIMARY KEY(SessionId)
);`,
}));
await this.db.exec(SQL.driverSql({
[FaucetDbDriver.SQLITE]: `CREATE INDEX "GithubSessionsUserIdx" ON "GithubSessions" ("UserId" ASC);`,
[FaucetDbDriver.MYSQL]: `ALTER TABLE GithubSessions ADD INDEX GithubSessionsUserIdx (UserId);`,
}));
}
return version;
}

public override async cleanStore(): Promise<void> {
await this.db.run("DELETE FROM GithubCache WHERE Timeout < ?", [this.now()]);

let rows = await this.db.all([
"SELECT GithubSessions.SessionId",
"FROM GithubSessions",
"LEFT JOIN Sessions ON Sessions.SessionId = GithubSessions.SessionId",
"WHERE Sessions.SessionId IS NULL",
].join(" "));
let dataIdx = 0;
let promises: Promise<void>[] = [];
while(dataIdx < rows.length) {
let batchLen = Math.min(rows.length - dataIdx, 100);
let dataBatch = rows.slice(dataIdx, dataIdx + batchLen);
dataIdx += batchLen;
promises.push(this.db.run(
"DELETE FROM GithubSessions WHERE SessionId IN (" + dataBatch.map(b => "?").join(",") + ")",
dataBatch.map(b => b.SessionId) as any[]
).then())
}
await Promise.all(promises);
}

public async getGithubInfo(userId: number): Promise<IGithubInfo> {
let row = await this.db.get(
"SELECT Json FROM GithubCache WHERE UserId = ? AND Timeout > ?",
[userId.toString(), this.now()]
) as {Json: string};
if(!row)
return null;

return JSON.parse(row.Json);
}

public async setGithubInfo(userId: number, info: IGithubInfo, duration?: number): Promise<void> {
let now = this.now();
let row = await this.db.get("SELECT Timeout FROM GithubCache WHERE UserId = ?", [userId.toString()]);

let timeout = now + (typeof duration === "number" ? duration : 86400);
let infoJson = JSON.stringify(info);

if(row) {
await this.db.run("UPDATE GithubCache SET Json = ?, Timeout = ? WHERE UserId = ?", [infoJson, timeout, userId.toString()]);
}
else {
await this.db.run("INSERT INTO GithubCache (UserId, Json, Timeout) VALUES (?, ?, ?)", [userId.toString(), infoJson, timeout]);
}
}

public getGithubSessions(userId: number, duration: number, skipData?: boolean): Promise<FaucetSessionStoreData[]> {
let now = this.now();
return this.faucetStore.selectSessionsSql([
"FROM GithubSessions",
"INNER JOIN Sessions ON Sessions.SessionId = GithubSessions.SessionId",
"WHERE GithubSessions.UserId = ? AND Sessions.StartTime > ? AND Sessions.Status IN ('claimable','claiming','finished')",
].join(" "), [ userId.toString(), now - duration ], skipData);
}

public async setGithubSession(sessionId: string, userId: number): Promise<void> {
await this.db.run(
SQL.driverSql({
[FaucetDbDriver.SQLITE]: "INSERT OR REPLACE INTO GithubSessions (SessionId,UserId) VALUES (?,?)",
[FaucetDbDriver.MYSQL]: "REPLACE INTO GithubSessions (SessionId,UserId) VALUES (?,?)",
}),
[
sessionId,
userId.toString(),
]
);
}

}
145 changes: 123 additions & 22 deletions src/modules/github/GithubModule.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ServiceManager } from "../../common/ServiceManager";
import { EthWalletManager } from "../../eth/EthWalletManager";
import { FaucetSession, FaucetSessionStoreData } from "../../session/FaucetSession";
import { FaucetSession } from "../../session/FaucetSession";
import { BaseModule } from "../BaseModule";
import { ModuleHookAction } from "../ModuleManager";
import { defaultConfig, IGithubRestrictionConfig, IGithubConfig } from './GithubConfig';
Expand All @@ -11,13 +11,18 @@ import { FaucetWebApi, IFaucetApiUrl } from "../../webserv/FaucetWebApi";
import { IncomingMessage } from "http";
import { faucetConfig } from "../../config/FaucetConfig";
import { FaucetHttpResponse } from "../../webserv/FaucetHttpServer";
import { GithubResolver, IGithubAuthInfo } from './GithubResolver';
import { GithubResolver, IGithubInfo, IGithubInfoOpts } from './GithubResolver';
import { GithubDB } from './GithubDB';
import { FaucetLogLevel, FaucetProcess } from "../../common/FaucetProcess";
import { ISessionRewardFactor } from "../../session/SessionRewardFactor";

export class GithubModule extends BaseModule<IGithubConfig> {
protected readonly moduleDefaultConfig = defaultConfig;
private githubDb: GithubDB;
private githubResolver: GithubResolver;

protected override startModule(): Promise<void> {
protected override async startModule(): Promise<void> {
this.githubDb = await ServiceManager.GetService(FaucetDatabase).createModuleDb(GithubDB, this);
this.githubResolver = new GithubResolver(this);
this.moduleManager.addActionHook(
this, ModuleHookAction.ClientConfig, 1, "github login config",
Expand All @@ -32,6 +37,14 @@ export class GithubModule extends BaseModule<IGithubConfig> {
this, ModuleHookAction.SessionStart, 6, "Github login check",
(session: FaucetSession, userInput: any) => this.processSessionStart(session, userInput)
);
this.moduleManager.addActionHook(
this, ModuleHookAction.SessionComplete, 5, "Github save session",
(session: FaucetSession) => this.processSessionComplete(session)
);
this.moduleManager.addActionHook(
this, ModuleHookAction.SessionRewardFactor, 5, "Github reward factor",
(session: FaucetSession, rewardFactors: ISessionRewardFactor[]) => this.processSessionRewardFactor(session, rewardFactors)
);
ServiceManager.GetService(FaucetWebApi).registerApiEndpoint(
"githubCallback",
(req: IncomingMessage, url: IFaucetApiUrl, body: Buffer) => this.processGithubAuthCallback(req, url, body)
Expand All @@ -43,8 +56,102 @@ export class GithubModule extends BaseModule<IGithubConfig> {
return Promise.resolve();
}

public getGithubDb(): GithubDB {
return this.githubDb;
}

private async processSessionStart(session: FaucetSession, userInput: any): Promise<void> {
await Promise.all(this.moduleConfig.restrictions.map((limit) => this.checkLimit(session, limit)));
let infoOpts: IGithubInfoOpts = {
loadOwnRepo: false,
};
this.moduleConfig.checks.forEach((check) => {
if(check.minOwnRepoCount || check.minOwnRepoStars)
infoOpts.loadOwnRepo = true;
});

let githubInfo: IGithubInfo;
try{
githubInfo = await this.githubResolver.getGithubInfo(userInput.githubToken, {
loadOwnRepo: true,
});
} catch(ex) {
ServiceManager.GetService(FaucetProcess).emitLog(FaucetLogLevel.WARNING, "Error while fetching github info: " + ex.toString());
}

let now = Math.floor((new Date()).getTime() / 1000);
let rewardFactor: number = null;
for(let i = 0; i < this.moduleConfig.checks.length; i++) {
let check = this.moduleConfig.checks[i];
let passed: boolean = null;
let errmsg: string = null;

if(!githubInfo) {
passed = false;
errmsg = "missing or invalid github token";
}
if((passed || passed === null) && check.minAccountAge) {
if(!(passed = (now - githubInfo.info.createTime > check.minAccountAge)))
errmsg = "account age check failed";
}
if((passed || passed === null) && check.minRepoCount) {
if(!(passed = (githubInfo.info.repoCount >= check.minRepoCount)))
errmsg = "repository count check failed";
}
if((passed || passed === null) && check.minFollowers) {
if(!(passed = (githubInfo.info.followers >= check.minFollowers)))
errmsg = "follower count check failed";
}
if((passed || passed === null) && check.minOwnRepoCount) {
if(!(passed = (githubInfo.info.followers >= check.minOwnRepoCount)))
errmsg = "own repository count check failed";
}
if((passed || passed === null) && check.minOwnRepoStars) {
if(!(passed = (githubInfo.info.followers >= check.minOwnRepoStars)))
errmsg = "own repository star count check failed";
}

if(check.required && passed === false) {
let errMsg: string;
if(check.message)
errMsg = check.message.replace("{0}", errmsg);
else
errMsg = "Your github account does not meet the minimum requirements: " + errmsg;
throw new FaucetError(
"GITHUB_CHECK",
errMsg,
);
}
if(typeof check.rewardFactor === "number" && (rewardFactor === null || check.rewardFactor < rewardFactor)) {
rewardFactor = check.rewardFactor;
}
}

session.setSessionData("github.uid", githubInfo?.uid);
session.setSessionData("github.user", githubInfo?.user);
if(rewardFactor !== null)
session.setSessionData("github.factor", rewardFactor);

if(githubInfo) {
await Promise.all(this.moduleConfig.restrictions.map((restriction) => this.checkRestriction(githubInfo.uid, restriction)));
}
}

private async processSessionComplete(session: FaucetSession): Promise<void> {
let githubUserId = session.getSessionData("github.uid");
if(!githubUserId)
return;

await this.githubDb.setGithubSession(session.getSessionId(), githubUserId);
}

private async processSessionRewardFactor(session: FaucetSession, rewardFactors: ISessionRewardFactor[]): Promise<void> {
let githubFactor = session.getSessionData("github.factor");
if(typeof githubFactor !== "number")
return;
rewardFactors.push({
factor: githubFactor,
module: this.moduleName,
});
}

private async processGithubAuthCallback(req: IncomingMessage, url: IFaucetApiUrl, body: Buffer): Promise<any> {
Expand Down Expand Up @@ -103,41 +210,35 @@ export class GithubModule extends BaseModule<IGithubConfig> {
].join("");
}

private async checkLimit(session: FaucetSession, limit: IGithubRestrictionConfig): Promise<void> {
let finishedSessions: FaucetSessionStoreData[];
if(limit.byAddrOnly)
finishedSessions = await ServiceManager.GetService(FaucetDatabase).getFinishedSessions(session.getTargetAddr(), null, limit.duration, true);
else if(limit.byIPOnly)
finishedSessions = await ServiceManager.GetService(FaucetDatabase).getFinishedSessions(null, session.getRemoteIP(), limit.duration, true);
else
finishedSessions = await ServiceManager.GetService(FaucetDatabase).getFinishedSessions(session.getTargetAddr(), session.getRemoteIP(), limit.duration, true);

if(limit.limitCount > 0 && finishedSessions.length >= limit.limitCount) {
let errMsg = limit.message || [
private async checkRestriction(githubUserId: number, restriction: IGithubRestrictionConfig): Promise<void> {
let finishedSessions = await this.githubDb.getGithubSessions(githubUserId, restriction.duration, true);

if(restriction.limitCount > 0 && finishedSessions.length >= restriction.limitCount) {
let errMsg = restriction.message || [
"You have already created ",
finishedSessions.length,
(finishedSessions.length > 1 ? " sessions" : " session"),
" in the last ",
renderTimespan(limit.duration)
renderTimespan(restriction.duration)
].join("");
throw new FaucetError(
"RECURRING_LIMIT",
"GITHUB_LIMIT",
errMsg,
);
}

if(limit.limitAmount > 0) {
if(restriction.limitAmount > 0) {
let totalAmount = 0n;
finishedSessions.forEach((session) => totalAmount += BigInt(session.dropAmount));
if(totalAmount >= BigInt(limit.limitAmount)) {
let errMsg = limit.message || [
if(totalAmount >= BigInt(restriction.limitAmount)) {
let errMsg = restriction.message || [
"You have already requested ",
ServiceManager.GetService(EthWalletManager).readableAmount(totalAmount),
" in the last ",
renderTimespan(limit.duration)
renderTimespan(restriction.duration)
].join("");
throw new FaucetError(
"RECURRING_LIMIT",
"GITHUB_LIMIT",
errMsg,
);
}
Expand Down
Loading

0 comments on commit e545241

Please sign in to comment.