mirror of
https://github.com/thelounge/thelounge
synced 2025-02-18 06:08:26 +00:00
We only respond once to data, then half-close the connection. Hence, we should only listen to a single data event as well, else if the remote doesn't stop sending data we keep trying to write to the closed write end of the pipe.
176 lines
4.7 KiB
TypeScript
176 lines
4.7 KiB
TypeScript
import fs from "fs";
|
|
import net, {Socket} from "net";
|
|
import colors from "chalk";
|
|
import Helper from "./helper";
|
|
import Config from "./config";
|
|
import log from "./log";
|
|
|
|
type Connection = {
|
|
socket: Socket;
|
|
user: string;
|
|
};
|
|
class Identification {
|
|
private connectionId: number;
|
|
private connections: Map<number, Connection>;
|
|
private oidentdFile?: string;
|
|
|
|
constructor(startedCallback: (identHandler: Identification, err?: Error) => void) {
|
|
this.connectionId = 0;
|
|
this.connections = new Map();
|
|
|
|
if (typeof Config.values.oidentd === "string") {
|
|
this.oidentdFile = Helper.expandHome(Config.values.oidentd);
|
|
log.info(`Oidentd file: ${colors.green(this.oidentdFile)}`);
|
|
|
|
this.refresh();
|
|
}
|
|
|
|
if (Config.values.identd.enable) {
|
|
if (this.oidentdFile) {
|
|
log.warn(
|
|
"Using both identd and oidentd at the same time, this is most likely not intended."
|
|
);
|
|
}
|
|
|
|
const server = net.createServer(this.serverConnection.bind(this));
|
|
|
|
server.on("error", (err) => {
|
|
startedCallback(this, err);
|
|
});
|
|
|
|
server.listen(
|
|
{
|
|
port: Config.values.identd.port || 113,
|
|
host: Config.values.bind,
|
|
},
|
|
() => {
|
|
const address = server.address();
|
|
|
|
if (typeof address === "string") {
|
|
log.info(`Identd server available on ${colors.green(address)}`);
|
|
} else if (address?.address) {
|
|
log.info(
|
|
`Identd server available on ${colors.green(
|
|
address.address + ":" + address.port.toString()
|
|
)}`
|
|
);
|
|
}
|
|
|
|
startedCallback(this);
|
|
}
|
|
);
|
|
} else {
|
|
startedCallback(this);
|
|
}
|
|
}
|
|
|
|
serverConnection(socket: Socket) {
|
|
socket.on("error", (err: string) => log.error(`Identd socket error: ${err}`));
|
|
socket.once("data", (data) => {
|
|
this.respondToIdent(socket, data);
|
|
socket.end();
|
|
});
|
|
}
|
|
|
|
respondToIdent(socket: Socket, buffer: Buffer) {
|
|
if (!socket.remoteAddress) {
|
|
log.warn("identd: no remote address");
|
|
return;
|
|
}
|
|
|
|
const data = buffer.toString().split(",");
|
|
|
|
const lport = parseInt(data[0], 10) || 0;
|
|
const fport = parseInt(data[1], 10) || 0;
|
|
|
|
if (lport < 1 || fport < 1 || lport > 65535 || fport > 65535) {
|
|
log.warn(`identd: bogus request from ${socket.remoteAddress}`);
|
|
return;
|
|
}
|
|
|
|
log.debug(`identd: remote ${socket.remoteAddress} query ${lport}, ${fport}`);
|
|
|
|
for (const connection of this.connections.values()) {
|
|
// we only want to respond if all the ip,port tuples match, to avoid user enumeration
|
|
if (
|
|
connection.socket.remotePort === fport &&
|
|
connection.socket.localPort === lport &&
|
|
socket.remoteAddress === connection.socket.remoteAddress &&
|
|
socket.localAddress === connection.socket.localAddress
|
|
) {
|
|
const reply = `${lport}, ${fport} : USERID : TheLounge : ${connection.user}\r\n`;
|
|
log.debug(`identd: reply is ${reply.trimEnd()}`);
|
|
socket.write(reply);
|
|
return;
|
|
}
|
|
}
|
|
|
|
const reply = `${lport}, ${fport} : ERROR : NO-USER\r\n`;
|
|
log.debug(`identd: reply is ${reply.trimEnd()}`);
|
|
socket.write(reply);
|
|
}
|
|
|
|
addSocket(socket: Socket, user: string) {
|
|
const id = ++this.connectionId;
|
|
|
|
this.connections.set(id, {socket, user});
|
|
|
|
if (this.oidentdFile) {
|
|
this.refresh();
|
|
}
|
|
|
|
return id;
|
|
}
|
|
|
|
removeSocket(id: number) {
|
|
this.connections.delete(id);
|
|
|
|
if (this.oidentdFile) {
|
|
this.refresh();
|
|
}
|
|
}
|
|
|
|
refresh() {
|
|
let file = "# Warning: file generated by The Lounge: changes will be overwritten!\n";
|
|
|
|
this.connections.forEach((connection, id) => {
|
|
if (!connection.socket.remotePort || !connection.socket.localPort) {
|
|
// Race condition: this can happen when more than one socket gets disconnected at
|
|
// once, since we `refresh()` for each one being added/removed. This results
|
|
// in there possibly being one or more disconnected sockets remaining when we get here.
|
|
log.warn(
|
|
`oidentd: socket has no remote or local port (id=${id}). See https://github.com/thelounge/thelounge/pull/4695.`
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (!connection.socket.remoteAddress) {
|
|
log.warn(`oidentd: socket has no remote address, will not respond to queries`);
|
|
return;
|
|
}
|
|
|
|
if (!connection.socket.localAddress) {
|
|
log.warn(`oidentd: socket has no local address, will not respond to queries`);
|
|
return;
|
|
}
|
|
|
|
// we only want to respond if all the ip,port tuples match, to avoid user enumeration
|
|
file +=
|
|
`to ${connection.socket.remoteAddress}` +
|
|
` fport ${connection.socket.remotePort}` +
|
|
` from ${connection.socket.localAddress}` +
|
|
` lport ${connection.socket.localPort}` +
|
|
` { reply "${connection.user}" }\n`;
|
|
});
|
|
|
|
if (this.oidentdFile) {
|
|
fs.writeFile(this.oidentdFile, file, {flag: "w+"}, function (err) {
|
|
if (err) {
|
|
log.error("Failed to update oidentd file!", err.message);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
export default Identification;
|