From 26401c130a36482a842fa5425d4ece3ae123a27e Mon Sep 17 00:00:00 2001 From: Evert Prants Date: Sun, 6 Nov 2022 16:19:19 +0200 Subject: [PATCH] icynet management --- .gitignore | 2 + data.db | Bin 40960 -> 0 bytes src/app.module.ts | 2 + src/config/configuration.ts | 1 + src/guards/icynet.guard.ts | 23 +++++ src/modules/icynet/icynet.controller.ts | 82 ++++++++++++++++++ src/modules/icynet/icynet.module.ts | 9 ++ src/modules/objects/manager/access.entity.ts | 2 +- .../objects/manager/manager.service.ts | 43 ++++++++- src/utility/token.ts | 4 +- 10 files changed, 162 insertions(+), 6 deletions(-) delete mode 100644 data.db create mode 100644 src/guards/icynet.guard.ts create mode 100644 src/modules/icynet/icynet.controller.ts create mode 100644 src/modules/icynet/icynet.module.ts diff --git a/.gitignore b/.gitignore index 989637b..0ba5c1d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ /dist /node_modules *.zone +*.env +*.db # Logs logs diff --git a/data.db b/data.db deleted file mode 100644 index efa4a899a7ea0a91ab4566764a4979bc87dc6273..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 40960 zcmeI)&u`jh7zc1WKOmt=$fRmqR_Ptllz2%xn-_x}P3jmp6Doy_fYO|hF)uA@0tq-< zGHnN%YCG(*KWT?urX45k)_-A_UMFp@jYAC~4QZETn!Z+?kjM7>KK{H9FIZS?f0=98 zjMQ80ebXjXSQJE2_>2%i5Q2OSd8^3(zR%YwzMd^k?6M^&+^>8vA6gY=g0ChPx^utH ztp;D+`8o7g=)uC@fj=%g9NQrP0SG_<0;2>%tD+i7CdC(%wpra{HrqdFwaxYkkKc3R zr`v<^$~UbhtFWfsuuu9s`-2ndqLC^Yq?GzRXOQT~eWGL~+NecIqiM4ztWCCx*+;43 zE_rC|l2o}=$mV$?kBoeYU10>Uzu&|soC~#u-eSDS*>E)QBvcN?8ZLp zZzmaJGgZ!&$jaHKmE~sZ>t#veO&%RM!7Cpry-;U@k)0j!hpda!uuhuHu9%kHYI`)U zNXPFo)=sm7cw-&oV!9cmoCFMaXtF-tA;UNAkS^r6OT|={%SSgKR%+Cgsg~3diE6B> z)RR^{9uPSDgY!-?}_KUnbXnZ(FWFV2Ev1t^IeA?J1-W~%=UL8oJkmuu+GkDpg zQ`_lO#&9C#t&CGhUSy37C8t+SRZB8GUNhr*tfnUPn5xKTU8jlRBiS9XYw{pF&SoB0E;4|VSxYy zAOHafKmY;|fB*y_009U<;JO7qotPFrp|ob6>@}V|wbyI>lY#yFjK$H^6bF&V*%&-Ukv{w@E<^Qq^N-OfjiO6Do@Gfn>scP2X z&Hiy^gVGTp=L`Pu`Tq?KF1mpL1Rwwb2tWV=5P$##AOHaf+>(Ir`oH&$uD6s2x`hA) zAOHafKmY;|fB*y_009U<;Q9o7*Z;HE*9%&P00bZa0SG_<0uX=z1Rwwb2teRA1@QfU zx2cc5ApijgKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb2tWV=|Bk>vJ}F!w diff --git a/src/app.module.ts b/src/app.module.ts index ad442d2..f53906f 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -6,6 +6,7 @@ import { AppService } from './app.service'; import { ObjectsModule } from './modules/objects/objects.module'; import { ZoneModule } from './modules/zone/zone.module'; import configuration from './config/configuration'; +import { IcynetModule } from './modules/icynet/icynet.module'; @Module({ imports: [ @@ -17,6 +18,7 @@ import configuration from './config/configuration'; }), ObjectsModule, ZoneModule, + IcynetModule, ], controllers: [AppController], providers: [AppService], diff --git a/src/config/configuration.ts b/src/config/configuration.ts index 4a632e1..e20390e 100644 --- a/src/config/configuration.ts +++ b/src/config/configuration.ts @@ -11,6 +11,7 @@ export default () => ({ }, cacheTTL: parseInt(process.env.ZONE_CACHE_TTL, 10) || 1600, zoneDir: '.', + icynetKey: process.env.ICYNET_KEY || 'ch4ng3 m3!', rndc: { host: process.env.RNDC_SERVER || '127.0.0.1', port: parseInt(process.env.RNDC_PORT, 10) || 953, diff --git a/src/guards/icynet.guard.ts b/src/guards/icynet.guard.ts new file mode 100644 index 0000000..228c8bb --- /dev/null +++ b/src/guards/icynet.guard.ts @@ -0,0 +1,23 @@ +import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { Request } from 'express'; +import { timingSafeEqual } from 'crypto'; + +@Injectable() +export class IcynetGuard implements CanActivate { + constructor(private config: ConfigService) {} + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + const authHeader = request.headers.authorization; + + if (!authHeader) return false; + + const [base, token] = authHeader.split(' '); + if (!base || base.toLowerCase() !== 'bearer' || !token) return false; + + const configured = this.config.get('icynetKey'); + + return timingSafeEqual(Buffer.from(token), Buffer.from(configured)); + } +} diff --git a/src/modules/icynet/icynet.controller.ts b/src/modules/icynet/icynet.controller.ts new file mode 100644 index 0000000..e150e5c --- /dev/null +++ b/src/modules/icynet/icynet.controller.ts @@ -0,0 +1,82 @@ +import { + Controller, + Delete, + Get, + HttpException, + Param, + Post, + Put, + Query, + UseGuards, +} from '@nestjs/common'; +import { ApiExcludeController } from '@nestjs/swagger'; +import { IcynetGuard } from 'src/guards/icynet.guard'; +import { ManagerService } from '../objects/manager/manager.service'; +import { ZoneEntity } from '../objects/manager/zone.entity'; + +@ApiExcludeController() +@UseGuards(IcynetGuard) +@Controller({ + path: 'api/v1/icynet', +}) +export class IcynetController { + constructor(private service: ManagerService) {} + + @Get('zones') + async getZoneList(@Query('uuid') uuid?: string) { + let list: ZoneEntity[] = []; + + if (uuid) { + list = await this.service.getZonesByIcynetUUID(uuid); + } else { + list = await this.service.getAllZones(); + } + + return list.map(({ zone }) => zone); + } + + @Get('access') + async getAccessList() { + return this.service.getZonesWithIcynet(); + } + + @Put('zone/:domain/:uuid') + async addZoneAccess( + @Param('uuid') uuid: string, + @Param('domain') domain: string, + ) { + const success = await this.service.authorizeIcynetUser(uuid, domain); + return { success }; + } + + @Delete('zone/:domain/:uuid') + async removeZoneAccess( + @Param('uuid') uuid: string, + @Param('domain') domain: string, + ) { + const success = await this.service.revokeIcynetUser(uuid, domain); + return { success }; + } + + @Post('zone/:domain/:uuid') + async getAccess( + @Param('uuid') uuid: string, + @Param('domain') domain: string, + ) { + const added = await this.service.createIcynetAccessKey(uuid, domain); + if (!added) throw new HttpException('Invalid request', 400); + return { token: added.key }; + } + + @Put('zone/:domain') + async addZone(@Param('domain') domain: string) { + const added = await this.service.addZone(domain); + return { success: !!added }; + } + + @Delete('zone/:domain') + async removeZone(@Param('domain') domain: string) { + const success = await this.service.removeZone(domain); + return { success }; + } +} diff --git a/src/modules/icynet/icynet.module.ts b/src/modules/icynet/icynet.module.ts new file mode 100644 index 0000000..fc9b2f4 --- /dev/null +++ b/src/modules/icynet/icynet.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { ObjectsModule } from '../objects/objects.module'; +import { IcynetController } from './icynet.controller'; + +@Module({ + imports: [ObjectsModule], + controllers: [IcynetController], +}) +export class IcynetModule {} diff --git a/src/modules/objects/manager/access.entity.ts b/src/modules/objects/manager/access.entity.ts index b32b9e0..0ce002f 100644 --- a/src/modules/objects/manager/access.entity.ts +++ b/src/modules/objects/manager/access.entity.ts @@ -25,6 +25,6 @@ export class AccessEntity { @UpdateDateColumn() public updated_at: Date; - @Column({ type: 'varchar', nullable: true }) + @Column({ type: 'datetime', nullable: true }) public expires_at?: Date; } diff --git a/src/modules/objects/manager/manager.service.ts b/src/modules/objects/manager/manager.service.ts index 12c9815..1eb15bf 100644 --- a/src/modules/objects/manager/manager.service.ts +++ b/src/modules/objects/manager/manager.service.ts @@ -42,6 +42,10 @@ export class ManagerService { }); } + public async getAllZones(): Promise { + return this.zone.find(); + } + public async getAllKeys(zone: string): Promise { const obj = await this.getZone(zone); if (!obj) return []; @@ -54,6 +58,7 @@ export class ManagerService { public async getZonesByIcynetUUID(uuid: string): Promise { const actor = await this.actors.findOne({ where: { icynetUUID: uuid }, + relations: ['zones'], }); if (!actor) return []; @@ -61,6 +66,20 @@ export class ManagerService { return actor.zones; } + public async getZonesWithIcynet(): Promise> { + const actors = await this.actors.find({ + relations: ['zones'], + }); + + return actors.reduce>((obj, current) => { + for (const item of current.zones) { + if (!obj[item.zone]) obj[item.zone] = []; + obj[item.zone].push(current.icynetUUID); + } + return obj; + }, {}); + } + /** * Create a new temporary API key for Icy Network user for their domain. * @param icynetUUID Icy Network user UUID @@ -80,7 +99,7 @@ export class ManagerService { const newObject = { key: getToken(), zone: entry, - expires_at: Date.now() + 24 * 60 * 60 * 1000, + expires_at: new Date(Date.now() + 24 * 60 * 60 * 1000), }; return this.access.save(newObject); @@ -103,10 +122,14 @@ export class ManagerService { return false; } - const actor = await this.actors.findOne({ + let actor = await this.actors.findOne({ where: { icynetUUID }, }); + if (!actor) { + actor = await this.actors.save({ icynetUUID }); + } + actor.zones = [...zones, zone]; await this.actors.save(actor); return true; @@ -125,12 +148,13 @@ export class ManagerService { if (!zone) return false; const zones = await this.getZonesByIcynetUUID(icynetUUID); - if (zones.length || !zones.some((item) => item.zone === zone.zone)) { + if (!zones.length || !zones.some((item) => item.zone === zone.zone)) { return false; } const actor = await this.actors.findOne({ where: { icynetUUID }, + relations: ['zones'], }); actor.zones = actor.zones.filter((item) => item.zone !== zone.zone); @@ -168,4 +192,17 @@ export class ManagerService { zone: domain, }); } + + /** + * Removes a managed zone. + * @param domain Zone to remove + * @returns boolean + */ + public async removeZone(domain: string): Promise { + const zone = await this.getZone(domain); + if (!zone) return false; + + await this.zone.remove(zone); + return true; + } } diff --git a/src/utility/token.ts b/src/utility/token.ts index 933ed9c..1da519a 100644 --- a/src/utility/token.ts +++ b/src/utility/token.ts @@ -1,5 +1,5 @@ -import crypto from 'crypto'; +import { randomBytes } from 'crypto'; export function getToken() { - return crypto.randomBytes(256 / 8).toString('hex'); + return randomBytes(256 / 8).toString('hex'); }