require('console-stamp')(console) const ipp = require('ipp') const express = require('express') const bodyParser = require('body-parser') // --- Config --- const PORT = Number(process.env.PRINTSERVANT_PORT) || 3199 const CONFIG = JSON.parse(process.env.PRINTSERVANT_CONFIG || 'null') const MAX_SIZE_MB = Number(process.env.PRINTSERVANT_MAX_SIZE_MB) || 10 if (!(PORT && CONFIG)) { throw new Error("PRINTSERVANT_PORT and PRINTSERVANT_CONFIG environment variables must be set") } const printers = [] for (const printerConfig of CONFIG.printers) { const { name, aliases = [], ipp_url } = printerConfig if (!(name && ipp_url)) { throw new Error("Printer config must have a name and url") } const ippPrinter = ipp.Printer(ipp_url) printers.push({ name, aliases, ippPrinter }) } const printerMap = new Map() printers.forEach(printer => { printerMap.set(printer.name.toLowerCase(), printer) printer.aliases.forEach(alias => { printerMap.set(alias.toLowerCase(), printer) }) }) function getPrinter(name) { return printerMap.get(name.toLowerCase()) } // --- Debug API --- const app = express() app.get('/', (req, res) => { res.send(`
Hello world! API routes available:
    - GET /
    - GET /health
    - GET /printers
      - Lists all printers (names and aliases)
    - GET /printers/:name/attributes
      - Prints IPP attributes
    - GET /printers/:name/jobs
      - Prints IPP jobs
    - POST /print?printer=:name
      - Prints a PDF
      - ?printer= printer name or alias (case insensitive)
      - ?copies= (optional) number of copies (1-10 allowed)
      - Body: PDF or PNG data; *or* any data if Content-Type header is passed
      - Response: 200 OK if sent to printer successfully (not necessarily printed), 4xx/5xx otherwise
  
`) }) app.get('/health', (req, res) => { res.send(`Printservant is healthy (probably)`) }) app.get('/printers', (req, res) => { res.send(printers.map(({ name, aliases }) => ({ name, aliases }))) }) function executeSimpleCommand(printerName, command, res) { const printer = getPrinter(printerName) if (!printer) { console.log(`Printer '${printerName}' not found`) res.status(404).send(`Printer '${printerName}' not found`) return } printer.ippPrinter.execute(command, null, function (error, result) { if (error || result.statusCode !== 'successful-ok') { console.error(error || result); res.status(500) } res.send({ result, error }) }); } app.get('/printers/:name/attributes', (req, res) => { const { name } = req.params console.log(`Getting attributes for printer: '${name}'`) executeSimpleCommand(name, "Get-Printer-Attributes", res) }) app.get('/printers/:name/jobs', (req, res) => { const { name } = req.params console.log(`Getting jobs for printer: '${name}'`) executeSimpleCommand(name, "Get-Jobs", res) }) // --- Print API --- app.post('/print', bodyParser.raw({ type: () => true, limit: MAX_SIZE_MB * 1_000_000 }), (req, res) => { const { body, query, headers } = req console.log("Received print job: ", { body, query }) // validate query params const { printer: printerName, copies: copiesParam, ...otherParams } = query if (!printerName) { res.status(400).send("No printer specified, pass ?printer=:name in the query params") return } else if (Object.keys(otherParams).length) { res.status(400).send("Unknown query params, only ?printer=:name is supported") return } const printer = getPrinter(printerName) if (!printer) { console.log(`Printer '${name}' not found`) res.status(404).send(`Printer '${name}' not found`) return } // validate body if (!body || !body.length) { res.status(400).send("No data received") return } const contentType = headers['content-type'] const hasValidContentType = contentType && contentType !== 'application/octet-stream' && contentType !== 'application/x-www-form-urlencoded' const isPDF = body.subarray(0, 4).toString() === "%PDF" const isPNG = body.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) const hasValidFormat = isPDF || isPNG || hasValidContentType if (!hasValidFormat) { res.status(415).send("Unknown media type. Pass a PDF or PNG file, or set Content-Type header to print something else.") return } // validate job attributes const copies = Number(copiesParam) || 1 if (!(copies >= 1 && copies <= 10)) { res.status(400).send("Copies must be between 1 and 10") return } // send print job const msg = { "operation-attributes-tag": { "document-format": (() => { if (isPDF) return "application/pdf" if (isPNG) return "image/png" if (contentType) return contentType throw new Error('unreachable') })(), }, "job-attributes-tag": { "copies": copies, }, data: body }; console.log("Sending print job: ", msg) printer.ippPrinter.execute("Print-Job", msg, function(error, result){ console.log({ result, error }); if (error) { res.status(500) } else if (result.statusCode !== 'successful-ok') { res.status(400) // not sure, depends lol } res.send({ result, error }) }); }) // --- Let's goooooooo --- app.listen(PORT, () => { console.log(`printservant listening on port ${PORT}`) })