189 lines
5.2 KiB
JavaScript
189 lines
5.2 KiB
JavaScript
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(`<pre>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
|
|
</pre>`)
|
|
})
|
|
|
|
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}`)
|
|
})
|