Discovery OK

This commit is contained in:
Douglas Barone 2023-06-21 13:48:40 -04:00
parent 4225d51140
commit 8280b7c3f7
11 changed files with 327 additions and 22 deletions

17
package-lock.json generated
View File

@ -10,13 +10,15 @@
"license": "ISC",
"dependencies": {
"@prisma/client": "^4.15.0",
"@types/netmask": "^2.0.1",
"body-parser": "^1.20.2",
"bree": "^9.1.3",
"dotenv": "^16.1.4",
"express": "^4.18.2",
"jsonwebtoken": "^9.0.0",
"ldapts": "^4.2.6",
"net-snmp": "^3.9.6"
"net-snmp": "^3.9.6",
"netmask": "^2.0.2"
},
"devDependencies": {
"@types/express": "^4.17.17",
@ -622,6 +624,11 @@
"integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==",
"dev": true
},
"node_modules/@types/netmask": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@types/netmask/-/netmask-2.0.1.tgz",
"integrity": "sha512-SSj+JNRxpaljp9Uy1td4MM6p6hJfmKk3Z/5h5MhJZbX7KI90DNvS8L3aNuevAOaGftaGAXLUrEd161Tk3iwaTg=="
},
"node_modules/@types/node": {
"version": "20.3.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.1.tgz",
@ -1912,6 +1919,14 @@
"smart-buffer": "^4.1.0"
}
},
"node_modules/netmask": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz",
"integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==",
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/nodemon": {
"version": "2.0.22",
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.22.tgz",

View File

@ -32,12 +32,14 @@
},
"dependencies": {
"@prisma/client": "^4.15.0",
"@types/netmask": "^2.0.1",
"body-parser": "^1.20.2",
"bree": "^9.1.3",
"dotenv": "^16.1.4",
"express": "^4.18.2",
"jsonwebtoken": "^9.0.0",
"ldapts": "^4.2.6",
"net-snmp": "^3.9.6"
"net-snmp": "^3.9.6",
"netmask": "^2.0.2"
}
}

View File

@ -0,0 +1,38 @@
/*
Warnings:
- You are about to drop the column `hostname` on the `Printer` table. All the data in the column will be lost.
- You are about to drop the column `createdAt` on the `PrinterStatus` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "Printer" DROP COLUMN "hostname";
-- AlterTable
ALTER TABLE "PrinterStatus" DROP COLUMN "createdAt",
ADD COLUMN "timestamp" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
-- CreateTable
CREATE TABLE "Network" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"shortName" TEXT NOT NULL,
"cidr" TEXT NOT NULL,
CONSTRAINT "Network_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "Network_name_key" ON "Network"("name");
-- CreateIndex
CREATE UNIQUE INDEX "Network_shortName_key" ON "Network"("shortName");
-- CreateIndex
CREATE UNIQUE INDEX "Network_cidr_key" ON "Network"("cidr");
-- CreateIndex
CREATE INDEX "Network_id_idx" ON "Network"("id");
-- CreateIndex
CREATE INDEX "PrinterStatus_timestamp_idx" ON "PrinterStatus"("timestamp");

View File

@ -26,6 +26,7 @@ model User {
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
campus String?
roles Role[] @default([USER])
}
@ -37,7 +38,6 @@ model Printer {
location String?
serialNumber String? @unique
hostname String?
ip String @unique
model String
@ -49,7 +49,9 @@ model Printer {
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
status PrinterStatus[]
status PrinterStatus[]
network Network @relation(fields: [networkId], references: [id])
networkId Int
}
model PrinterStatus {
@ -62,8 +64,21 @@ model PrinterStatus {
counter Int
createdAt DateTime @default(now())
timestamp DateTime @default(now())
printerId Int
printer Printer @relation(fields: [printerId], references: [id], onDelete: Cascade)
@@index([timestamp])
}
model Network {
id Int @id @default(autoincrement())
name String @unique
shortName String @unique
cidr String @unique
printers Printer[]
@@index([id])
}

View File

@ -3,37 +3,119 @@ import { PrismaClient } from '@prisma/client'
export const prisma = new PrismaClient()
async function main() {
console.log('Seeding printers...')
console.log('Seeding subnets...')
await prisma.network.createMany({
data: [
{
shortName: 'RT1',
name: 'Reitoria',
cidr: '10.0.0.0/21'
},
{
shortName: 'RT2',
name: 'Reitoria 2',
cidr: '10.1.0.0/21'
},
{
shortName: 'AQ',
name: 'Aquidauana',
cidr: '10.2.0.0/21'
},
{
shortName: 'CG',
name: 'Campo Grande',
cidr: '10.3.0.0/21'
},
{
shortName: 'CB',
name: 'Corumbá',
cidr: '10.4.0.0/21'
},
{
shortName: 'CX',
name: 'Coxim',
cidr: '10.5.0.0/21'
},
{
shortName: 'NA',
name: 'Nova Andradina',
cidr: '10.6.0.0/21'
},
{
shortName: 'PP',
name: 'Ponta Porã',
cidr: '10.7.0.0/21'
},
{
shortName: 'TL',
name: 'Três Lagoas',
cidr: '10.8.0.0/21'
},
{
shortName: 'JD',
name: 'Jardim',
cidr: '10.9.0.0/21'
},
{
shortName: 'NV',
name: 'Naviraí',
cidr: '10.10.0.0/21'
},
{
shortName: 'DR',
name: 'Dourados',
cidr: '10.11.0.0/21'
}
],
skipDuplicates: true
})
await prisma.printer.createMany({
data: [
{
friendlyName: 'P04',
ip: '10.7.0.134',
model: 'ECOSYS M3655idn',
serialNumber: 'R4P1478461'
serialNumber: 'R4P1478461',
networkId: 8
},
{
friendlyName: 'P05',
ip: '10.7.0.135',
model: 'ECOSYS M2040dn',
serialNumber: 'VR91483974'
serialNumber: 'VR91483974',
networkId: 8
},
{
friendlyName: 'P06',
ip: '10.7.0.136',
model: 'ECOSYS M2040dn',
serialNumber: 'VR91586433'
serialNumber: 'VR91586433',
networkId: 8
},
{
friendlyName: 'P07',
ip: '10.7.0.137',
model: 'ECOSYS M2040dn',
serialNumber: 'VR91586432'
serialNumber: 'VR91586432',
networkId: 8
},
{
friendlyName: 'P08',
ip: '10.7.0.138',
model: 'ECOSYS P6235cdn',
serialNumber: 'RCG0304510'
serialNumber: 'RCG0304510',
networkId: 8
},
{
friendlyName: 'PNA',
ip: '10.6.0.32',
model: 'ECOSYS M2040dn',
serialNumber: 'VR91586430',
networkId: 8
}
]
})

View File

@ -4,6 +4,8 @@ import { hasRolesMiddleware } from '../middlewares/hasRolesMiddleware.js'
import { prisma } from '../prisma.js'
import { PrinterStatusService } from '../services/PrinterStatusService.js'
import { distributedCopy } from '../utils/distributedCopy.js'
import { Netmask } from 'netmask'
import { isIPv4 } from 'net'
const router = Router()
@ -16,22 +18,22 @@ class PrinterController {
static async show(req: Request, res: Response) {
const { id } = req.params
const { take, minutes = 43200 } = req.query
const { take = 64, days = 60 } = req.query
const gte = new Date(Date.now() - 1000 * 60 * Number(minutes))
const gte = new Date(Date.now() - 1000 * 60 * 60 * 24 * Number(days))
const printer = await prisma.printer.findUnique({
where: { id: Number(id) },
include: {
status: {
where: {
createdAt: {
timestamp: {
gte
}
},
orderBy: {
createdAt: 'desc'
timestamp: 'desc'
}
}
}
@ -48,10 +50,24 @@ class PrinterController {
static async create(req: Request, res: Response) {
const { friendlyName, ip } = req.body
const ipBlock = new Netmask(ip)
if (!isIPv4(ip)) {
res.status(400).json({ error: 'Invalid IP' })
return
}
try {
const model = await PrinterStatusService.getPrinterModel(ip)
const printer = await prisma.printer.create({
data: { friendlyName, ip, model }
data: {
friendlyName,
ip,
model,
network: {
connect: {}
}
}
})
new PrinterStatusService(printer)
@ -60,7 +76,7 @@ class PrinterController {
} catch (e) {
res
.status(400)
.json({ error: 'Este endereço não é de uma impressora suportada.' })
.json({ error: 'Este IP não é de uma impressora suportada.' })
return
}
}

View File

@ -0,0 +1,58 @@
import { Router, Request, Response } from 'express'
import { hasRolesMiddleware } from '../middlewares/hasRolesMiddleware.js'
import { PrinterDiscoveryService } from '../services/PrinterDiscoveryService.js'
import { prisma } from '../prisma.js'
import { PrinterStatusService } from '../services/PrinterStatusService.js'
import { Printer } from '@prisma/client'
const router = Router()
class PrinterDiscoveryController {
static async discovery(req: Request, res: Response) {
const networks = await prisma.network.findMany()
const newPrinters: Printer[] = []
const discoveredPrintersIPs: string[] = []
for (const network of networks) {
console.log('Discovering printers for network', network.cidr)
try {
const discoveredPrintersIPsForNetwork =
await PrinterDiscoveryService.discovery(network.cidr)
discoveredPrintersIPs.push(...discoveredPrintersIPsForNetwork)
} catch (error) {
console.log(error)
}
const printers = await prisma.printer.findMany()
const newPrintersIPs = discoveredPrintersIPs.filter(
ip => !printers.find(printer => printer.ip === ip)
)
await Promise.allSettled(
newPrintersIPs.map(async ip => {
const model = await PrinterStatusService.getPrinterModel(ip)
const printer = await prisma.printer.create({
data: { ip, model, networkId: network.id }
})
new PrinterStatusService(printer)
newPrinters.push(printer)
})
)
}
res.json({ discoveredPrintersIPs, newPrinters })
}
}
router.use(hasRolesMiddleware(['ADMIN', 'INSPECTOR']))
router.post('/', PrinterDiscoveryController.discovery)
export default router

View File

@ -7,6 +7,7 @@ import { authMiddleware } from './middlewares/authMiddleware.js'
import LoginRouter from './controllers/LoginController.js'
import PrinterRouter from './controllers/PrinterController.js'
import PrinterStatusRouter from './controllers/PrinterStatusController.js'
import PrinterDiscoveryRouter from './controllers/PrinterDiscoveryController.js'
export const app = express()
@ -18,6 +19,7 @@ app.use(populateUserMiddleware)
app.use('/api/login', LoginRouter)
app.use('/api/printer', PrinterRouter)
app.use('/api/printer-status', PrinterStatusRouter)
app.use('/api/discovery', PrinterDiscoveryRouter)
app.get('/api/me', authMiddleware, async (req: Request, res: Response) =>
res.json(res.locals.user)

View File

@ -16,6 +16,7 @@ type LdapUser = {
displayName: string
thumbnailPhoto: string | null
groups?: string[]
campus?: string
}
export class LdapService extends Client implements LdapClientInterface {
@ -56,7 +57,8 @@ export class LdapService extends Client implements LdapClientInterface {
'sAMAccountName',
'displayName',
'thumbnailPhoto',
'dn'
'dn',
'extensionAttribute1'
],
explicitBufferAttributes: ['thumbnailPhoto']
})
@ -64,8 +66,14 @@ export class LdapService extends Client implements LdapClientInterface {
if (!searchEntries.length)
throw new Error('User not found on LDAP server.')
const { sAMAccountName, displayName, mail, thumbnailPhoto, dn } =
searchEntries[0]
const {
sAMAccountName,
displayName,
mail,
thumbnailPhoto,
dn,
extensionAttribute1
} = searchEntries[0]
const ldapUser: LdapUser = {
username: sAMAccountName.toString(),
@ -74,7 +82,8 @@ export class LdapService extends Client implements LdapClientInterface {
thumbnailPhoto: `data:image/png;base64,${Buffer.from(
thumbnailPhoto as Buffer
).toString('base64')}`,
groups: await this.getGroupsForUser(dn.toString())
groups: await this.getGroupsForUser(dn.toString()),
campus: extensionAttribute1?.toString().split('-')[0] || '--'
}
return ldapUser

View File

@ -0,0 +1,68 @@
import snmp from 'net-snmp'
import netmask from 'netmask'
export class PrinterDiscoveryService {
private static async isPrinter(ip: string) {
if (ip == '10.7.0.51') return false
return new Promise((resolve, reject) => {
const session = snmp.createSession(ip, 'public', { timeout: 1000 })
const CHECK_OID = '1.3.6.1.2.1.1.1.0'
const CHECK_STRING = 'KYOCERA Document Solutions Printing System'
session.get(
[CHECK_OID],
(error: any, varbinds: any) => {
if (error) {
resolve(false)
} else {
if (varbinds[0].value.toString() === CHECK_STRING) {
resolve(true)
} else {
resolve(false)
}
}
session.close()
},
{ timeout: 1000 }
)
})
}
// Check every IP in the range for a printer and return an array of IPs that are printers
static async discovery(cdir: string): Promise<string[]> {
const printers: string[] = []
const blockIPs: string[] = []
try {
const block = new netmask.Netmask(cdir)
const BLACK_LISTED_IPS = ['10.7.1.1', '10.7.0.51']
block.forEach(ip => {
if (!BLACK_LISTED_IPS.includes(ip)) blockIPs.push(ip)
})
} catch (err) {
throw new Error('Invalid IP CIDR')
}
try {
await Promise.allSettled(
blockIPs.map(async ip => {
try {
if (await PrinterDiscoveryService.isPrinter(ip)) {
printers.push(ip)
console.log(`Found printer at ${ip}!`)
}
} catch (error: any) {
console.log(`Error checking ${ip}: ${error.message}`)
}
})
)
} catch (err) {
console.log(err)
}
return printers
}
}

View File

@ -93,7 +93,7 @@ export class PrinterStatusService {
private deBufferizeVarbinds(varbinds: Varbind[]) {
const varbindsString: VarbindString[] = []
varbinds.forEach((varbind: Varbind) => {
varbinds?.forEach((varbind: Varbind) => {
if (varbind.value instanceof Buffer)
varbindsString.push({ ...varbind, value: varbind.value.toString() })
else varbindsString.push({ ...varbind, value: varbind.value })
@ -144,7 +144,7 @@ export class PrinterStatusService {
if (typeof current === 'undefined' || typeof max === 'undefined')
throw new Error('current or max is undefined')
return (+current! / +max!) * 100
return Math.floor((+current! / +max!) * 100)
}
private objectIDsToPrinterInfo(varbinds: Varbind[]): PrinterInfo {