enigma-bbs/core/connect.js

304 lines
10 KiB
JavaScript

/* jslint node: true */
'use strict';
// ENiGMA½
const ansi = require('./ansi_term.js');
const Events = require('./events.js');
const Config = require('./config.js').get;
const { Errors } = require('./enig_error.js');
// deps
const async = require('async');
exports.connectEntry = connectEntry;
const withCursorPositionReport = (client, cprHandler, failMessage, cb) => {
let giveUpTimer;
const done = function (err) {
client.removeListener('cursor position report', cprListener);
clearTimeout(giveUpTimer);
return cb(err);
};
const cprListener = pos => {
cprHandler(pos);
return done(null);
};
client.once('cursor position report', cprListener);
// give up after 2s
giveUpTimer = setTimeout(() => {
return done(Errors.General(failMessage));
}, 2000);
};
function ansiDiscoverHomePosition(client, cb) {
//
// We want to find the home position. ANSI-BBS and most terminals
// utilize 1,1 as home. However, some terminals such as ConnectBot
// think of home as 0,0. If this is the case, we need to offset
// our positioning to accommodate for such.
//
if (!Config().term.checkAnsiHomePosition) {
// Skip (and assume 1,1) if the home position check is disabled.
return cb(null);
}
withCursorPositionReport(
client,
pos => {
const [h, w] = pos;
//
// We expect either 0,0, or 1,1. Anything else will be filed as bad data
//
if (h > 1 || w > 1) {
return client.log.warn(
{ height: h, width: w },
'Ignoring ANSI home position CPR due to unexpected values'
);
}
if ((0 === h) & (0 === w)) {
//
// Store a CPR offset in the client. All CPR's from this point on will offset by this amount
//
client.log.info('Setting CPR offset to 1');
client.cprOffset = 1;
}
},
'ANSI query home position timed out',
cb
);
client.term.write(`${ansi.goHome()}${ansi.queryPos()}`); // go home, query pos
}
function ansiAttemptDetectUTF8(client, cb) {
//
// Trick to attempt and detect UTF-8. While there is a lot more than
// just UTF-8 and CP437, many those are the main concerns, when it comes
// terminals that for example tell us they are "xterm" but still want CP437.
//
// Try to detect UTF-8 by discovering the cursor position, writing some
// multi-byte UTF-8, and checking the position again. If the term is really
// UTF-8, we should get a proper position, otherwise we'll be further out.
//
// We currently only do this if the term hasn't already been ID'd as a
// "*nix" terminal -- that is, xterm, etc.
// Also skip this check if checkUtf8Encoding is disabled in the config
if (!client.term.isNixTerm() || !Config().term.checkUtf8Encoding) {
return cb(null);
}
let initialPosition;
const ASCIIPortion = ' Character encoding detection ';
withCursorPositionReport(
client,
pos => {
initialPosition = pos;
withCursorPositionReport(
client,
pos => {
const [_, w] = pos;
const len = w - initialPosition[1];
if (!isNaN(len) && len >= ASCIIPortion.length + 6) {
// CP437 displays 3 chars each Unicode skull
client.log.info(
'Terminal identified as UTF-8 but does not appear to be. Overriding to "ansi".'
);
client.setTermType('ansi');
}
},
'Detect UTF-8 stage 2 timed out',
cb
);
client.term.rawWrite(`\u9760${ASCIIPortion}\u9760`); // Unicode skulls on each side
client.term.rawWrite(ansi.queryPos());
},
'Detect UTF-8 stage 1 timed out',
err => {
if (err) {
// bailing early
return cb(err);
}
}
);
client.term.rawWrite(`${ansi.goHome()}${ansi.queryPos()}`);
}
const ansiQuerySyncTermFontSupport = (client, cb) => {
// If we know it's SyncTERM, then good to go
if (client.term.termClient === 'cterm') {
client.term.syncTermFontsEnabled = true;
return cb(null);
}
// Some terminals do not properly ignore an unknown ESC sequence for
// setting SyncTERM style fonts. Some draw only a 'D' while others
// show more of the sequence
//
// If the cursor remains at 1,1, they are at least ignoring it, if not
// supporting it.
//
// We also need CPR support. Since all known terminals that support
// SyncTERM fonts also support CPR, this works out.
//
withCursorPositionReport(
client,
pos => {
const [_, w] = pos;
if (w === 1) {
// cursor didn't move
client.log.info(`SyncTERM font support enabled on node ${client.node}`);
client.term.syncTermFontsEnabled = true;
}
},
'SyncTERM font detection timed out',
cb
);
client.term.rawWrite(
`${ansi.goto(1, 1)}${ansi.setSyncTermFont('cp437')}${ansi.queryPos()}`
);
};
function ansiQueryTermSizeIfNeeded(client, cb) {
if (client.term.termHeight > 0 || client.term.termWidth > 0) {
return cb(null);
}
withCursorPositionReport(
client,
pos => {
//
// If we've already found out, disregard
//
if (client.term.termHeight > 0 || client.term.termWidth > 0) {
return;
}
//
// NetRunner for example gives us 1x1 here. Not really useful. Ignore
// values that seem obviously bad. Included in the set is the explicit
// 999x999 values we asked to move to.
//
const [h, w] = pos;
if (h < 10 || h === 999 || w < 10 || w === 999) {
return client.log.warn(
{ height: h, width: w },
'Ignoring ANSI CPR screen size query response due to non-sane values'
);
}
client.term.termHeight = h;
client.term.termWidth = w;
client.log.debug(
{
termWidth: client.term.termWidth,
termHeight: client.term.termHeight,
source: 'ANSI CPR',
},
'Window size updated'
);
},
'No term size established by CPR within timeout',
cb
);
// Start the process:
// 1 - Ask to goto 999,999 -- a very much "bottom right" (generally 80x25 for example
// is the real size)
// 2 - Query for screen size with bansi.txt style specialized Device Status Report (DSR)
// request. We expect a CPR of:
// a - Terms that support bansi.txt style: Screen size
// b - Terms that do not support bansi.txt style: Since we moved to the bottom right
// we should still be able to determine a screen size.
//
client.term.rawWrite(`${ansi.goto(999, 999)}${ansi.queryScreenSize()}`);
}
function prepareTerminal(term) {
term.rawWrite(`${ansi.normal()}${ansi.clearScreen()}`);
}
function displayBanner(term) {
// note: intentional formatting:
term.pipeWrite(`
|06Connected to |02EN|10i|02GMA|10½ |06BBS version |12|VN
|06Copyright (c) 2014-2022 Bryan Ashby |14- |12http://l33t.codes/
|06Updates & source |14- |12https://github.com/NuSkooler/enigma-bbs/
|00`);
}
function connectEntry(client, nextMenu) {
const term = client.term;
async.series(
[
function basicPrepWork(callback) {
term.rawWrite(ansi.queryDeviceAttributes(0));
term.write('\nDetecting client features...');
return callback(null);
},
function discoverHomePosition(callback) {
return ansiDiscoverHomePosition(client, callback);
},
function queryTermSizeByNonStandardAnsi(callback) {
ansiQueryTermSizeIfNeeded(client, err => {
if (err) {
//
// Check again; We may have got via NAWS/similar before CPR completed.
//
if (0 === term.termHeight || 0 === term.termWidth) {
//
// We still don't have something good for term height/width.
// Default to DOS size 80x25.
//
// :TODO: Netrunner is currently hitting this and it feels wrong. Why is NAWS/ENV/CPR all failing???
client.log.warn(
{ reason: err.message },
'Failed to negotiate term size; Defaulting to 80x25!'
);
term.termHeight = 25;
term.termWidth = 80;
}
}
return callback(null);
});
},
function checkUtf8IfNeeded(callback) {
return ansiAttemptDetectUTF8(client, callback);
},
function querySyncTERMFontSupport(callback) {
return ansiQuerySyncTermFontSupport(client, callback);
},
],
() => {
prepareTerminal(term);
//
// Always show an ENiGMA½ banner
//
displayBanner(term);
// fire event
Events.emit(Events.getSystemEvents().TermDetected, { client: client });
setTimeout(() => {
return client.menuStack.goto(nextMenu);
}, 500);
}
);
}