Commit 394eb42f authored by Evert Prants's avatar Evert Prants

MAJOR OVERHAUL: Working WebSocket client! No translation server, yet.

parent e7b9836d
{
"presets": ["@babel/preset-env"]
"presets": ["@babel/preset-env"],
"plugins": ["@babel/plugin-transform-runtime"]
}
The MIT License (MIT)
Copyright (c) 2016 Evert Prants
Copyright (c) 2016-2020 Evert Prants
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.
\ No newline at end of file
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.
const fs = require('fs')
const path = require('path')
const toml = require('toml')
const filename = path.join(__dirname, 'client.config.toml')
const pkg = require(path.join(__dirname, 'package.json'))
let config
try {
config = toml.parse(fs.readFileSync(filename))
} catch (e) {
console.error(e)
process.exit(1)
}
config.client.version = pkg.version
config.client.description = pkg.description
module.exports = config
This diff is collapsed.
{
"name": "teemantirc",
"version": "2.0.0",
"description": "Web-based IRC client",
"version": "2.0.1",
"description": "A Web-based IRC client",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
......@@ -22,30 +22,32 @@
"author": "Evert",
"license": "MIT",
"dependencies": {
"express": "^4.16.4",
"seedrandom": "^2.4.4",
"socket.io": "^2.2.0",
"toml": "^2.3.5"
"express": "^4.17.1",
"toml": "^2.3.6",
"ws": "^7.2.1"
},
"repository": {
"type": "git",
"url": "git://gitlab.icynet.eu/IcyNetwork/teemant.git"
},
"devDependencies": {
"@babel/cli": "^7.2.3",
"@babel/core": "^7.2.2",
"@babel/preset-env": "^7.2.3",
"babel-loader": "^8.0.5",
"concurrently": "^4.1.0",
"copy-webpack-plugin": "^4.6.0",
"css-loader": "^2.1.0",
"@babel/cli": "^7.8.4",
"@babel/core": "^7.8.4",
"@babel/plugin-transform-runtime": "^7.8.3",
"@babel/preset-env": "^7.8.4",
"@babel/runtime": "^7.8.4",
"babel-loader": "^8.0.6",
"concurrently": "^4.1.2",
"copy-webpack-plugin": "^5.1.1",
"css-loader": "^2.1.1",
"file-loader": "^3.0.1",
"html-webpack-plugin": "^3.2.0",
"mini-css-extract-plugin": "^0.5.0",
"standard": "^12.0.1",
"stylus": "^0.54.5",
"stylus": "^0.54.7",
"stylus-loader": "^3.0.2",
"webpack": "^4.28.1",
"webpack-command": "^0.4.2"
"webpack": "^4.41.5",
"webpack-cli": "^3.3.10",
"seedrandom": "^2.4.4"
}
}
......@@ -3,14 +3,8 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>TeemantIRC</title>
<script src="//twemoji.maxcdn.com/2/twemoji.min.js?11.2"></script>
<script type="text/javascript" src="/socket.io/socket.io.js"></script>
<script type="text/javascript" src="main.js"></script>
<link href="https://fonts.googleapis.com/css?family=Open+Sans" rel="stylesheet" type="text/css">
<link rel="stylesheet" type="text/css" href="style/layout.css">
<link rel="stylesheet" type="text/css" href="style/theme_default.css" id="theme_stylesheet">
......
/* global WebSocket */
import { EventEmitter } from 'events'
import net from 'net'
import tls from 'tls'
import util from 'util'
import parse from './parser'
import { format } from 'util'
let translatorSocket
class FakeSocket {
constructor (server, socket) {
this.open = true
this.server = server
this.socket = socket
this.handlers = []
this.socket.addEventListener('message', (msg) => {
let tm = msg.split(' ')
if (tm[0] === 'TRANSLATOR') {
if ((tm[1] === 'CLOSE' || tm[1] === 'ERROR') && tm[2] === this.server) {
return this._socketClosed()
}
}
if (tm[0] === server) {
this._handle('message', tm.slice(1).join(' '))
}
})
this.socket.addEventListener('close', () => {
this._socketClosed()
})
}
send () {
if (!this.open) return
this.socket.send(this.server + ' ' + format.apply(Array.prototype.slice.call(arguments)))
}
addEventListener (type, fn) {
this.handlers.push({ target: type, fn })
}
_socketClosed () {
this.open = false
for (let a in this.handlers) {
let at = this.handlers[a]
if (at.target === 'close') {
at.fn.apply(this, [])
}
}
}
_handle (ev) {
if (!this.open) return
for (let a in this.handlers) {
let ap = this.handlers[a]
if (ap.target === ev) {
ap.fn.apply(this.socket, Array.prototype.slice.call(arguments, 1))
}
}
}
close () {
this.open = false
this.socket.send('TRANSLATOR DISCONNECT ' + this.server)
}
static addListenerPassthrough (fs, type) {
fs.socket.addEventListener(type, () => {
this._handle.apply(this, [type].concat(arguments))
})
}
}
async function _waitOpen (s) {
let o
let c
try {
await new Promise((resolve, reject) => {
o = function () {
console.log('removing events', o, c)
s.removeEventListener('close', c)
s.removeEventListener('open', o)
resolve()
}
s.addEventListener('open', o)
c = function (err) {
reject(err)
}
s.addEventListener('close', c)
})
} catch (e) {
throw e
}
return true
}
async function _trySocket (address, port, ssl, useTranslator) {
// Attempt a direct ws connection
let proto = 'ws'
if (ssl) proto = 'wss'
if (!useTranslator) {
let conn = address
if (port && (port < 6000 || port > 7000)) {
conn = address + ':' + port
}
let tSock = new WebSocket(proto + '://' + conn)
try {
console.log('Waiting to open')
await _waitOpen(tSock)
} catch (e) {
console.error(e)
console.error('Waiting to open failed')
return _trySocket(address, port, ssl, true)
}
tSock.open = true
tSock.addEventListener('close', () => { tSock.open = false })
return tSock
}
if (translatorSocket) {
translatorSocket.send('TRANSLATOR SERVER ' + address + ' ' + port + (ssl ? ' SSL' : ''))
return new FakeSocket(address, translatorSocket)
}
translatorSocket = new WebSocket(`ws${window.location.protocol === 'https:' ? 's' : ''}://${window.location.host}`)
console.log('Opening translator socket')
await _waitOpen(translatorSocket)
return new FakeSocket(address, translatorSocket)
}
class IRCConnectionHandler {
constructor (connection) {
......@@ -122,11 +248,11 @@ class IRCConnectionHandler {
let resp = '\x01' + line[0] + ' %s\x01'
if (line[0] === 'PING' && line[1] != null && line[1] !== '') {
resp = util.format(resp, line.slice(1).join(' '))
resp = format(resp, line.slice(1).join(' '))
} else if (line[0] === 'CLIENTINFO') {
resp = util.format(resp, 'CLIENTINFO PING ' + Object.keys(this.conn.extras.ctcps).join(' '))
resp = format(resp, 'CLIENTINFO PING ' + Object.keys(this.conn.extras.ctcps).join(' '))
} else if (this.conn.extras.ctcps && this.conn.extras.ctcps[line[0]] != null) {
resp = util.format(resp, this.conn.extras.ctcps[line[0]](data, this.conn))
resp = format(resp, this.conn.extras.ctcps[line[0]](data, this.conn))
} else {
resp = null
}
......@@ -376,6 +502,7 @@ class IRCConnectionHandler {
case '323':
case '351':
case '381':
case '489':
this.conn.emit('pass_to_client', {
type: 'server_message', messageType: 'regular', message: line.trailing, server: serverName, from: realServerName
})
......@@ -471,7 +598,7 @@ class IRCConnectionHandler {
// start whois queue
list = {
nickname: line.arguments[1],
hostmask: util.format('%s!%s@%s', line.arguments[1], line.arguments[2], line.arguments[3]),
hostmask: format('%s!%s@%s', line.arguments[1], line.arguments[2], line.arguments[3]),
realname: line.trailing || ''
}
this.whoisManage(line.arguments[1], list)
......@@ -620,39 +747,19 @@ class IRCConnection extends EventEmitter {
}
connect () {
this.socket = (this.config.secure ? tls : net).connect({
port: this.config.port,
host: this.config.server,
rejectUnauthorized: this.config.rejectUnauthorized
}, () => {
this.connected = true
this.authenticate()
})
this.socket.setEncoding(this.globalConfig.encoding)
this.socket.setTimeout(this.globalConfig.timeout)
this.socket.on('error', (data) => {
this.emit('connerror', { type: 'sock_error', message: 'A socket error occured.', raw: data })
})
this.socket.on('lookup', (err, address, family, host) => {
if (err) {
this.emit('connerror', { type: 'resolve_error', message: 'Failed to resolve host.' })
} else {
this.emit('lookup', { address: address, family: address, host: host })
this.config.address = address
async function wrapped (argument) {
try {
this.socket = await _trySocket(this.config.server, this.config.port, this.config.secure)
} catch (e) {
this.emit('connerror', { type: 'sock_error', message: 'A socket error occured.' })
throw e
}
})
let buffer = ''
this.socket.on('data', (chunk) => {
buffer += chunk
let data = buffer.split('\r\n')
buffer = data.pop()
data.forEach((line) => {
this.socket.addEventListener('message', (e) => {
let line = e.data
if (line.indexOf('PING') === 0) {
this.write('PONG %s\r\n', line.substring(4))
console.log('%s server pinged us (%s)', new Date(), line)
this.write('PONG %s', line.substring(4))
return
}
......@@ -668,21 +775,26 @@ class IRCConnection extends EventEmitter {
console.error(e.stack)
}
})
})
this.socket.on('close', (data) => {
if (!this.queue['close']) {
this.emit('closed', { type: 'sock_closed', raw: data, message: 'Connection closed.' })
}
this.socket.addEventListener('close', (data) => {
if (!this.queue['close']) {
this.emit('closed', { type: 'sock_closed', raw: data, message: 'Connection closed.' })
}
this.connected = false
this.authenticated = false
})
this.connected = false
this.authenticated = false
})
this.connected = true
this.authenticate()
}
return wrapped.call(this)
}
authenticate () {
if (this.config.password) {
this.write('PASS %s\r\n', this.config.password)
this.write('PASS %s', this.config.password)
}
if (this.extras.authenticationSteps) {
......@@ -692,8 +804,23 @@ class IRCConnection extends EventEmitter {
}
}
this.write('USER %s 8 * :%s\r\n', this.config.username, this.config.realname)
this.write('NICK %s\r\n', this.config.nickname)
this.write('USER %s 8 * :%s', this.config.username, this.config.realname)
this.write('NICK %s', this.config.nickname)
this.bindFinal()
this.sendPing()
}
bindFinal () {
this.on('userinput', (data) => {
return this.handler.handleUserLine(data)
})
}
sendPing () {
if (!this.connected) return
this.write('PING :' + this.data.actualServer)
setTimeout(() => this.sendPing(), 5000)
}
disconnect (message) {
......@@ -703,17 +830,17 @@ class IRCConnection extends EventEmitter {
}
this.queue['close'] = true
this.write('QUIT :%s\r\n', (message != null ? message : this.globalConfig.default_quit_msg))
this.write('QUIT :%s', (message != null ? message : this.globalConfig.default_quit_msg))
}
write () {
let message = util.format.apply(null, arguments)
let message = format.apply(null, arguments)
if (!this.connected) {
return this.emit('connerror', { type: 'sock_closed', message: 'Connection is closed.' })
}
this.socket.write(message + '\r\n')
this.socket.send(message + '\r\n')
}
}
module.exports = IRCConnection
export { IRCConnection }
This diff is collapsed.
const imgs = ['.png', '.jpg', '.jpeg', '.svg']
const embeds = [
{
match: /youtu.?be\//is,
exec: function (pgurl) {
let dat = pgurl.match(/(?:be|com)\/([^?&#]+)/i)
if (!dat) {
dat = pgurl.match('[\\?&]v=([^&#]*)')
}
if (!dat) return null
return 'https://www.youtube.com/embed/' + dat[1] + '?autoplay=1'
}
}
]
export function handleUrlElement (elem) {
let cover = null
let ext = elem.href.split('.')
ext = ext[ext.length - 1]
if (ext && imgs.indexOf(ext) !== -1) {
cover = document.createElement('img')
cover.src = elem.href
}
if (!cover) {
for (let a in embeds) {
if (cover) break
let fn = embeds[a]
if (elem.href.match(fn.match)) {
let r = fn.exec(elem.href)
if (r) {
cover = document.createElement('iframe')
cover.src = r
}
}
}
}
if (cover) {
cover.className = 'preview'
let contelem = document.createElement('span')
contelem.className = 'preview-box'
let contbutton = document.createElement('button')
contbutton.className = 'preview-btn'
contbutton.innerHTML = 'Show Preview'
elem.parentNode.appendChild(contelem)
contelem.appendChild(elem)
contelem.appendChild(contbutton)
contbutton.addEventListener('click', function (e) {
e.preventDefault()
if (cover.parentNode) {
cover.remove()
contbutton.innerHTML = 'Show Preview'
} else {
contelem.appendChild(cover)
contbutton.innerHTML = 'Hide Preview'
}
}, false)
return contelem
}
return elem
}
import fs from 'fs'
import path from 'path'
import toml from 'toml'
const filename = path.join(__dirname, '..', 'client.config.toml')
let config
try {
config = toml.parse(fs.readFileSync(filename))
} catch (e) {
throw new Error('config.toml parse error: ', e.message)
}
module.exports = config
'use strict'
import express from 'express'
import path from 'path'
import sockio from 'socket.io'
import dns from 'dns'
import pkginfo from '../package.json'
import irclib from './irc'
import webirc from './webirc'
import config from './config'
import config from '../globals'
import logger from './logger'
const app = express()
......@@ -17,21 +11,6 @@ const pubdir = path.join(__dirname, 'public')
const port = config.server.port || 8080
const cacheAge = 365 * 24 * 60 * 60 * 1000
let runtimeStats = {
connectionsMade: 0
}
let connections = {}
let customCTCPs = {
VERSION: function (data, connection) {
return `TeemantIRC ver. ${pkginfo.version} - ${pkginfo.description} - https://teemant.icynet.eu/`
},
SOURCE: function (data, connection) {
return 'https://gitlab.icynet.eu/IcyNetwork/teemant'
}
}
process.stdin.resume()
router.get('/', function (req, res) {
......@@ -51,186 +30,6 @@ app.use('/:server', express.static(path.join(pubdir, 'icons'), { maxAge: cacheAg
app.use('/:server', express.static(path.join(pubdir, 'static'), { maxAge: cacheAge }))
app.use('/', router)
const io = sockio.listen(app.listen(port, function () {
app.listen(port, function () {
logger.log(`*** Listening on http://localhost:${port}/`)
setInterval(() => {
logger.printRuntimeStats(runtimeStats, connections)
}, 3600000)
}))
function resolveHostname (ipaddr) {
return new Promise(function (resolve, reject) {
dns.reverse(ipaddr, function (err, hostnames) {
if (err != null) return reject(err)
resolve(hostnames)
})
})
}
io.sockets.on('connection', function (socket) {
let userip = socket.handshake.headers['x-real-ip'] || socket.handshake.headers['x-forwarded-for'] ||
socket.request.connection._peername.address || '127.0.0.1'
if (userip.indexOf('::ffff:') === 0) {
userip = userip.substring(7)
}
logger.debugLog(`clientID: ${socket.id} from: ${userip}`)
socket.emit('defaults', {
server: config.client.default_server,
port: config.client.default_port,
ssl: config.client.secure_by_default
})
// New object for connections
connections[socket.id] = {
host: {
ipaddr: userip,
hostname: userip
}
}
// Get the hostname of the connecting user
let hostQuery = resolveHostname(userip)
hostQuery.then((arr) => {
if (arr.length > 0) {
connections[socket.id].host.hostname = arr[0]
}
logger.debugLog(`Hostname of ${socket.id} was determined to be ${connections[socket.id].host.hostname}`)
}).catch((err) => {
logger.debugLog(`Host resolve for ${socket.id} failed:`, err.message)
})
socket.on('disconnect', function () {
for (let d in connections[socket.id]) {
if (connections[socket.id][d].ipaddr) continue
if (connections[socket.id][d].connected === true) {
connections[socket.id][d].disconnect()
}
}
delete connections[socket.id]
logger.debugLog(`clientID: ${socket.id} disconnect`)
})
socket.on('error', (e) => {
logger.errorLog(e, 'Socket error')
})
socket.on('userinput', (data) => {
let serv = connections[socket.id][data.server]
if (!serv) return
if (serv.authenticated === false) return
logger.debugLog(`[${socket.id}] ->`, data)
try {
serv.handler.handleUserLine(data)
} catch (e) {
console.error('An error occured while handling message from client', data)
console.error(e.stack)
}
})
socket.on('irc_create', function (connectiondata) {
logger.debugLog(`${socket.id} created irc connection: `, connectiondata)
socket.emit('act_client', { type: 'connect_message', message: 'Connecting to server..', error: false })
if (connectiondata.port === null || connectiondata.server === null) {
return socket.emit('act_client', {
type: 'connect_message',
server: connectiondata.server,
message: 'Failed to connect to the server!',
error: true
})
}
let newConnection = new irclib.IRCConnection(connectiondata, config.client,
{
authenticationSteps: [
new webirc.Authenticator(connections[socket.id].host)
],
ctcps: customCTCPs
})
newConnection.connect()
connections[socket.id][connectiondata.server] = newConnection
newConnection.on('authenticated', () => {
socket.emit('act_client', {
type: 'event_connect',
address: connectiondata.server,
network: newConnection.data.network,
supportedModes: newConnection.data.supportedModes,
nickname: newConnection.config.nickname,
max_channel_length: newConnection.data.max_channel_length
})
runtimeStats.connectionsMade += 1
})
if (config.server.debug) {
newConnection.on('line', function (line) {
logger.debugLog(`[${socket.id}] <-`, line)
})
newConnection.on('debug_log', function (data) {
logger.debugLog(`[${socket.id}] <-`, data)
})
}
newConnection.on('connerror', (data) => {
logger.debugLog(data)
if (newConnection.authenticated === false) {
socket.emit('act_client', {
type: 'connect_message',
server: connectiondata.server,
message: 'Failed to connect to the server!',
error: true
})
}
})
newConnection.on('pass_to_client', (data) => {
socket.emit('act_client', data)
})