UNPKG

lp-daemon

Version:
505 lines (438 loc) 13.3 kB
/** * (c) 2018 cepharum GmbH, Berlin, http://cepharum.de * * The MIT License (MIT) * * Copyright (c) 2018 cepharum GmbH * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. * * @author: cepharum */ "use strict"; const { Transform } = require( "stream" ); const Debug = require( "debug" )( "processor" ); /** * @typedef {object} RequestContext * @property {int} requestId ID of request to be unique in context of current runtime */ /** * Implements code processing tokens generated by RequestTokenizer. */ class RequestProcessor extends Transform { /** * @param {AbstractBackend} backend backend for actually performing requested actions * @param {object} streamOptions options customizing stream */ constructor( backend, streamOptions = {} ) { super( Object.assign( {}, streamOptions, { writableObjectMode: true, readableObjectMode: false, objectMode: false, } ) ); Object.defineProperties( this, { /** * Provides backend used by processor for actually performing * requested actions. * * @name RequestProcessor#backend * @property {AbstractBackend} * @readonly */ backend: { value: backend }, } ); if ( !this.constructor.SUCCESS ) { Object.defineProperties( this.constructor, { /** * Provides sole buffer to be pushed for responding on success. * * @name RequestProcessor.SUCCESS * @property {Buffer} * @readonly */ SUCCESS: { value: Buffer.from( [0x00] ) }, /** * Provides sole buffer to be pushed for responding on generic * failure. * * @name RequestProcessor.FAILURE * @property {Buffer} * @readonly */ FAILURE: { value: Buffer.from( [0x01] ) }, } ); } } /** * Maps one or more operands to strings handling encoding if either operand * is given as instance of `Buffer`. * * @param {Buffer|string|Array<Buffer|String>} operands one or more operands to be mapped * @returns {string|string[]} provided operand(s) mapped to string value(s) * @protected */ static _mapToStrings( operands ) { const gotArray = Array.isArray( operands ); const _operands = Array.isArray( operands ) ? operands : operands ? [operands] : []; const numOperands = _operands.length; const copies = new Array( numOperands ); for ( let i = 0; i < numOperands; i++ ) { const operand = _operands[i]; if ( Buffer.isBuffer( operand ) ) { copies[i] = this._fixEncoding( operand ); } else { copies[i] = String( operand ); } } return gotArray ? copies : copies[0]; } /** * Converts buffer to string replacing any non-UTF8-encoded non-ASCII * characters with question mark. * * @param {Buffer} buffer buffer containing some probably UTF8-encoded string * @returns {string} string extracted from buffer with invalid bytes replaced with `?` * @protected */ static _fixEncoding( buffer ) { const numOctets = buffer.length; let result = ""; const masks = [ 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01 ]; for ( let i = 0; i < numOctets; i++ ) { const octet = buffer[i]; if ( octet & 0x80 ) { // got probable start of a UTF8-encoded character let size = 0; for ( ; size < 7; size++ ) { if ( !( octet & masks[size] ) ) { break; } } if ( size ) { // check to have declared number of follow-up bytes let offset = 1; for ( ; offset <= size; offset++ ) { if ( buffer[i + offset] & 0xC0 !== 0x80 ) { // not a follow-up byte as expected -> not a UTF-8 encoded character either break; } } if ( offset <= size ) { result += "_"; } else { result += buffer.slice( i, i + offset ).toString( "utf8" ); } } else { // got an unexpected follow-up byte? -> not a UTF8-encoded character result += "_"; } } else { result += String.fromCharCode( octet ); } } return result; } /** * Parses content of control file. * * @param {Buffer} content content of control file to be parsed * @returns {object} parsed elements of control file * @private */ static _parseControlFile( content ) { const result = { print: [], }; let offset = 0; let nextLF; while ( ( nextLF = content.indexOf( 0x0a, offset ) ) > -1 ) { const lineType = String.fromCharCode( content[offset] ); const operand = this._fixEncoding( content.slice( offset + 1, nextLF ) ); offset = nextLF + 1; switch ( lineType ) { case "C" : result.bannerPageClass = operand; break; case "H" : result.hostName = operand; break; case "I" : if ( !/^\d+$/.test( operand ) ) { throw new Error( "indent printing control value may consist of numbers, only" ); } result.indentPrinting = parseInt( operand ); break; case "J" : result.bannerPageJobName = operand; break; case "L" : result.bannerPage = operand.trim() || true; break; case "M" : result.mailToWhenPrinted = operand; break; case "N" : result.fileName = operand; break; case "P" : result.userId = operand; break; case "S" : { const operands = operand.split( /\s+/ ).filter( i => i.length > 0 ); result.symbolicLinkData = { device: operands[0], inode: operands[1], }; break; } case "T" : result.jobTitle = operand; break; case "U" : result.unlinkFile = operand; break; case "W" : if ( !/^\d+$/.test( operand ) ) { throw new Error( "width control value may consist of numbers, only" ); } result.width = parseInt( operand ); break; case "1" : result.troffRFile = operand; break; case "2" : result.troffIFile = operand; break; case "3" : result.troffBFile = operand; break; case "4" : result.troffSFile = operand; break; case "c" : case "d" : case "f" : case "g" : case "k" : case "l" : case "n" : case "o" : case "p" : case "r" : case "t" : case "v" : case "z" : { const map = { c: "CIF", d: "DVI", f: "formatted", g: "plot", k: "kerberized", l: "formattedUrl", n: "ditroff", o: "postscript", p: "pr", r: "fortran", t: "troff", v: "raster", z: "palladium", }; const type = map[lineType]; result.print.push( type ); result.print[type] = operand; break; } } } if ( !result.hasOwnProperty( "hostName" ) ) { throw new Error( "missing required host name in control file" ); } if ( !result.hasOwnProperty( "userId" ) ) { throw new Error( "missing required user ID in control file" ); } return result; } /** * Processes tokens generated by RequestTokenizer stream. * * @param {RequestTokenizerOutput} token token generated by RequestTokenizer * @param {?string} encoding not used * @param {function} callback callback to invoke when object has been processed * @returns {void} */ _transform( token, encoding, callback ) { Debug( `got token of type "${token.type}" in request ${token.context.requestId}` ); const { context } = token; const { requestId } = context; const statics = this.constructor; switch ( token.type ) { case "command" : { const { command } = token; const operands = statics._mapToStrings( token.operands ); if ( operands < 1 ) { Debug( `ERROR: missing name of printer in request ${requestId}` ); callback( null, statics.FAILURE ); break; } const queueName = operands[0]; if ( !queueName ) { Debug( `ERROR: invalid name of printer in request ${requestId}` ); callback( null, statics.FAILURE ); break; } context.queueName = queueName; switch ( command ) { case 1 : // print any waiting job this.backend.releaseJobs( queueName ) .then( success => { callback( null, success ? statics.SUCCESS : statics.FAILURE ); } ) .catch( error => { Debug( `ERROR: backend failed on releasing jobs in request #${requestId}: ${error.message}` ); callback( null, statics.FAILURE ); } ); break; case 2 : // receive a printer job context.job = {}; callback( null, statics.SUCCESS ); break; case 3 : case 4 : // send queue state this.backend.report( queueName, command === 3, operands.slice( 1 ) ) .then( report => { callback( null, Buffer.from( String( report ), "ascii" ) ); } ) .catch( error => { Debug( `ERROR: backend failed on generating ${command === 3 ? "brief" : "full"} report in request #${requestId}: ${error.message}` ); callback( null, statics.FAILURE ); } ); break; case 5 : // remove jobs this.backend.removeJob( queueName, operands[1], operands.slice( 2 ) ) .then( success => { callback( null, success ? statics.SUCCESS : statics.FAILURE ); } ) .catch( error => { Debug( `ERROR: backend failed on removing jobs in request #${requestId}: ${error.message}` ); callback( null, statics.FAILURE ); } ); break; default : Debug( `command 0x${( "0" + command.toString( 16 ) ).slice( -2 )} is not supported` ); callback( null, statics.FAILURE ); } break; } case "subcommand" : { const { command } = token; // const operands = statics._mapToStrings( token.operands ); switch ( command ) { case 1 : // abort job context.job = null; callback( null, statics.SUCCESS ); break; case 2 : // receive control file if ( context.job == null ) { Debug( `ERROR: got sub-command after aborting job in request ${requestId}` ); callback( null, statics.FAILURE ); break; } context.job.expect = "control"; callback( null, statics.SUCCESS ); break; case 3 : // receive data file if ( context.job == null ) { Debug( `ERROR: got sub-command after aborting job in request ${requestId}` ); callback( null, statics.FAILURE ); break; } context.job.expect = "data"; callback( null, statics.SUCCESS ); break; } break; } case "data" : { const { job } = context; if ( job == null ) { Debug( `ERROR: got data after aborting job in request ${requestId}` ); callback( null, statics.FAILURE ); break; } const { expect } = job; if ( expect == null ) { Debug( `ERROR: not expecting any data in request ${requestId}` ); callback( null, statics.FAILURE ); break; } job.expect = null; switch ( expect ) { case "data" : // receiving data file now -> actually start printing job in backend this.backend.printJob( context.queueName, job.control || {}, token.stream ) .then( jobId => { Debug( `backend has printed job and assigned job ID ${jobId} in request #${requestId}` ); callback( null, statics.SUCCESS ); } ) .catch( error => { Debug( `ERROR: backend failed on printing job in request #${requestId}: ${error.message}` ); callback( null, statics.FAILURE ); } ); context.job = null; break; case "control" : { const chunks = []; const { stream } = token; stream.on( "data", chunk => { chunks.push( chunk ); } ); stream.once( "end", () => { if ( !job.control ) { job.control = {}; } Object.assign( job.control, statics._parseControlFile( Buffer.concat( chunks ) ) ); callback( null, statics.SUCCESS ); } ); stream.once( "error", error => { Debug( `ERROR: aborting job due to failed submission of control file in request ${requestId}: ${error.message}` ); callback( null, statics.FAILURE ); } ); break; } } break; } case "eof" : // ignore end-of-request tokens callback(); break; default : Debug( "token type not supported" ); callback( new Error( `invalid token of type ${token.type}` ) ); } } } module.exports = RequestProcessor;