IRC gateway

Revision as of 22:00, 20 June 2022 by PureFox (talk | contribs) (Added Wren)

Create an IRC Gateway capable of connecting an IRC server with another IRC server or Chat server

IRC gateway is a draft programming task. It is not yet considered ready to be promoted as a complete task, for reasons that should be found in its talk page.

Go

Library: go-ircevent


Just a bare-bones gateway. <lang go>package main

import (

   "crypto/tls"
   "fmt"
   "github.com/thoj/go-ircevent"
   "log"
   "os"

)

func main() {

   if len(os.Args) != 9 {
       fmt.Println("To use this gateway, you need to pass 8 command line arguments, namely:")
       fmt.Println("  <server1> <channel1> <nick1> <user1> <server2> <channel2> <nick2> <user2>")
       return
   }
   server1, channel1, nick1, user1 := os.Args[1], os.Args[2], os.Args[3], os.Args[4]
   server2, channel2, nick2, user2 := os.Args[5], os.Args[6], os.Args[7], os.Args[8]
   irc1 := irc.IRC(nick1, user1)
   irc1.VerboseCallbackHandler = true
   irc1.Debug = false
   irc1.UseTLS = true
   irc1.TLSConfig = &tls.Config{InsecureSkipVerify: true}
   irc2 := irc.IRC(nick2, user2)
   irc2.VerboseCallbackHandler = true
   irc2.Debug = false
   irc2.UseTLS = true
   irc2.TLSConfig = &tls.Config{InsecureSkipVerify: true}
   irc1.AddCallback("001", func(e *irc.Event) {
       irc1.Join(channel1)
       msg := fmt.Sprintf("<gateway> Hello %s. Please send your first message to %s.", nick1, nick2)
       irc1.Privmsg(nick1, msg)
       log.Println(msg)
   })
   irc1.AddCallback("366", func(e *irc.Event) {})
   irc1.AddCallback("PRIVMSG", func(e *irc.Event) {
       msg := fmt.Sprintf("<%s> %s", nick1, e.Message)
       irc2.Privmsg(nick2, msg)
       log.Println(msg)
   })
   irc2.AddCallback("001", func(e *irc.Event) {
       irc2.Join(channel2)
       msg := fmt.Sprintf("<gateway> Hello %s. Please wait for your first message from %s.", nick2, nick1)
       irc2.Privmsg(nick2, msg)
       log.Println(msg)
   })
   irc2.AddCallback("366", func(e *irc.Event) {})
   irc2.AddCallback("PRIVMSG", func(e *irc.Event) {
       msg := fmt.Sprintf("<%s> %s", nick2, e.Message)
       irc1.Privmsg(nick1, msg)
       log.Println(msg)
   })
   err1 := irc1.Connect(server1)
   if err1 != nil {
       log.Fatal(err1)
   }
   err2 := irc2.Connect(server2)
   if err2 != nil {
       log.Fatal(err2)
   }
   go irc2.Loop()
   irc1.Loop()

}</lang>

Phix

For use with Chat_server#Phix

--
-- demo\rosetta\IRC_Gateway.exw
-- ============================
--
-- Run ChatServer first, then this, then ChatClient.exw with bViaGateway set to true.
--
-- Translation between the various IRC protocols left as an exercise for the reader,
-- this is just a simple passthrough service.
--
-- Also this uses a simpler server <--> gateway <--> {clients} model, rather than
-- a more sensible server <-> {gateway1 <-> client1, gateway2 <-> client2} model,
-- probably easily changed, see the {ci,gi} = connection[i] etc comments below.
-- Hence as is this will broadcast eg /nt (name taken) rather than send it to a
-- single specific client, which would obviously be better.
--
without js
constant dl = `Download rosetta\eulibnet\ from http://phix.x10.mx/pmwiki/pmwiki.php?n=Main.Eulibnet`
assert(get_file_type("eulibnet")=FILETYPE_DIRECTORY,dl)
include eulibnet/eulibnet.ew

atom gateway_listconn,
     server_listconn
constant IP = "127.0.0.1",
         server_port = "29029",
         gateway_port = "29030",
         server_address = IP & ":" & server_port,
         gateway_address = IP & ":" & gateway_port,
         timeout = 20,
         MAX_MSG = 550,
         Escape = #1B

sequence connections = {} -- (inbound on the gateway port)
                          -- (should maybe {inbound,outbound})  

procedure message(string msg, sequence args={})
    if length(args) then msg = sprintf(msg,args) end if
    printf(1,"%s\n",{msg})
end procedure

procedure shutDown()
    message("Shutting down euLibnet...")
    for i = 1 to length(connections) do
--DEV/SUG
--      integer {ci,gi} = connections[i]
--      if net_closeconn(ci) then crash("Error closing client connection!") end if
--      if net_closeconn(gi) then crash("Error closing gateway connection!") end if
        if net_closeconn(connections[i]) then crash("Error closing connection!") end if
    end for
    if net_closeconn(gateway_listconn) then crash("Error closing gateway_listconn!") end if
    if net_closeconn(server_listconn) then crash("Error closing server_listconn!") end if
    if net_shutdown() then crash("Error shutting down euLibnet!") end if
end procedure

--DEV to go if {ci,gi} model adopted... probably
procedure sendToAll(string msg)
    -- Send msg to all clients
    for i=1 to length(connections) do
        atom ci = connections[i]
        message("Sending to connection %d",{ci})
        if net_send_rdm(ci, msg) then
            message("Error sending to connection %d",{ci})
        end if
    end for
end procedure

message("Initializing euLibnet...")
if net_init() then crash("Error initializing euLibnet!") end if
message("done.")
message("Initializing driver...")
if net_initdriver(NET_DRIVER_WSOCK_WIN) != 1 then
    crash("Error initializing WinSock driver!")
end if
message("done.")
message("Opening port " & gateway_address & "...")
gateway_listconn = net_openconn(NET_DRIVER_WSOCK_WIN, gateway_address)
if gateway_listconn = NULL then
    crash("Couldn't open connection (gateway already running?)")
end if
message("done.")
if net_listen(gateway_listconn) then
    crash("Error trying to listen to port")
end if
message("Listening on port " & gateway_address)

--DEV some/all probably better as {ci,gi} in connections[i]:
message("Opening server connection...")
server_listconn = net_openconn(NET_DRIVER_WSOCK_WIN, NULL)
if server_listconn = NULL then
    crash("Couldn't open connection.")
end if
message("done.")
message("Attempting to connect to chat server...")
integer ret = net_connect_wait_time(server_listconn, server_address, timeout)
if ret < 0 then
    crash("Error trying to establish connection.")
elsif ret > 0 then
    crash("Timeout trying to establish connection.")
end if
message("done.")

-- main loop (poll until Escape keyed)
while get_key()!=Escape do
    integer conn = net_poll_listen(gateway_listconn)
--  integer ci = net_poll_listen(gateway_listconn), gi
    if conn != NULL then
--DEV make a new gateway connection to the server here...?
--       gi = <as server_listconn above?>
--       connections = append(connections, {ci,gi})
         connections = append(connections, conn)
         message("New connection open from " & net_getpeer(conn))
    end if

    -- Check for messages from clients
    for i=1 to length(connections) do
        integer ci = connections[i]
--      {ci,gi} = connections[i]
        if net_query_rdm(ci) > 0 then
            --Get the message
            sequence msg = net_receive_rdm(ci, MAX_MSG)
            message("received msg \"%s\" of length %d from %d",{msg[2],msg[1],ci})
            if msg[1] < 0 then --Exit on error
                {} = net_ignore_rdm(ci)
                crash("Server error: some data may be lost")
            end if

            msg = msg[2]
            message("Sending to server")
            if net_send_rdm(server_listconn, msg) then
--          if net_send_rdm(gi, msg) then
                crash("Error sending to server")
            end if
        end if
--      <as below but for gi instead of server_listcomm here?>
    end for

    if net_query_rdm(server_listconn) > 0 then --Check for message
--  if net_query_rdm(gi) > 0 then --Check for message
        sequence msg = net_receive_rdm(server_listconn, MAX_MSG)
--      sequence msg = net_receive_rdm(gi, MAX_MSG)
        if msg[1] < 0 then
           message("Error receiving message!")
           {} = net_ignore_rdm(server_listconn)
--         {} = net_ignore_rdm(gi)
        end if
        msg = msg[2]
        message(msg)
        sendToAll(msg)
--DEV maybe instead:
--      if net_send_rdm(ci, msg) then
--          crash("Error sending to clent")
--      end if
    end if

    sleep(1)
end while
shutDown()

--?"done"
--{} = wait_key()

Tcl

This code is called as a complete script, perhaps like this: <lang sh>./ircgateway.tcl irc://hostA.org/fishing bait irc://hostB.com:6667/haxors botfly</lang>

Library: Tcllib (Package: picoirc)

<lang tcl>#!/bin/env tclsh8.5 package require picoirc

      1. Parse script arguments
  1. URL form: irc://foobar.org/secret

if {$argc != 4} {

   puts stderr "wrong # args: should be \"$argv0 ircA nickA ircB nickB\""
   exit 1

} lassign $argv url1 nick1 url2 nick2

      1. How to do the forwarding from one side to the other

proc handle {from to -> state args} {

   upvar #0 conn($from) f conn($to) t chan($to) chan
   switch -exact -- $state {

"chat" { lassign $args target nick message type if {![string match "*>>*<<*" $message]} { picoirc::post $t $chan ">>$nick said<< $message" } } "traffic" { lassign $args action channel nick newnick switch -exact -- $action { "entered" - "left" { picoirc::post $t $chan ">>$nick has $action<<" } } } "close" { exit }

   }

}

      1. Connect and run the event loop

set chan(1) [lindex [picoirc::splituri $url1] 2] set chan(2) [lindex [picoirc::splituri $url1] 2] interp alias {} handle1to2 {} handle 1 2 interp alias {} handle2to1 {} handle 2 1 set conn(1) [picoirc::connect handle1to2 $nick1 $url1] set conn(2) [picoirc::connect handle2to1 $nick2 $url2] vwait forever</lang>

Wren

Translation of: Go
Library: WrenGo
Library: go-ircevent
Library: Wren-dynamic

An embedded application with a Go host so we can use the 'go-ircevent' library.

As it's not possible to access command line arguments directly from Wren when it is being embedded, we instead ask the gateway user to input details of the connections needed. <lang ecmascript>/* irc_gateway.wren */

import "./dynamic" for Tuple

var Connection = Tuple.create("Connection", ["server", "channel", "nick", "user"])

foreign class IRC {

   construct new(number, nick, user) {}
   foreign connect(server)
   foreign verboseCallbackHandler=(arg)
   foreign debug=(arg)
   foreign useTLS=(arg)
   foreign configTLS=(arg)
   foreign addCallback(number, code, msg, channel, nick, otherNick)

}

foreign class Reader {

   construct new() {}
   foreign readLine()

}

var reader = Reader.new() var Connections = List.filled(2, null) System.print("To use this gateway, please enter the following:\n") for (i in 0..1) {

   System.print("Details for connection %(i+1):")
   System.write("  Server   : ")
   var server = reader.readLine()
   System.write("  Channel  : ")
   var channel = reader.readLine()
   System.write("  Nickname : ")
   var nick = reader.readLine()
   System.write("  User     : ")
   var user = reader.readLine()
   Connections[i] = Connection.new(server, channel, nick, user)
   System.print()

}

for (i in 0..1) {

   var c = Connections[i]
   var irc = IRC.new(i, c.nick, c.user)
   irc.verboseCallbackHandler = true
   irc.debug = false
   irc.useTLS = true
   irc.configTLS = true
   var otherNick = (i == 0) ? Connections[1].nick : Connections[0].nick
   var msg
   if (i == 0) {
       msg = "<gateway> Hello %(c.nick). Please send your first message to %(otherNick)."
   } else {
       msg = "<gateway> Hello %(c.nick). Please wait for your first message from %(otherNick)."
   }
   irc.addCallback(i, "001",     msg, c.channel, c.nick, otherNick)
   irc.addCallback(i, "366",     "" , c.channel, c.nick, otherNick)
   irc.addCallback(i, "PRIVMSG", "" , c.channel, c.nick, otherNick)
   irc.connect(c.server)

}</lang> We now embed this script in the following Go program and run it from a terminal. To close the gateway just press Ctrl-C. <lang go>/* go run irc_gateway.go */

package main

import (

   "bufio"
   "crypto/tls"
   "fmt"
   wren "github.com/crazyinfin8/WrenGo"
   "github.com/thoj/go-ircevent"
   "log"
   "os"
   "strings"

)

type any = interface{} // not needed if using Go v1.18 or later

var ircObjs [2]*irc.Connection

func newIRC(vm *wren.VM, parameters []any) (any, error) {

   number := int(parameters[1].(float64))
   nick := parameters[2].(string)
   user := parameters[3].(string)
   ircObjs[number] = irc.IRC(nick, user)
   return &ircObjs[number], nil

}

func connect(vm *wren.VM, parameters []any) (any, error) {

   handle := parameters[0].(*wren.ForeignHandle)
   ifc, _ := handle.Get()
   ircObj := *(ifc.(**irc.Connection))
   server := parameters[1].(string)
   err := ircObj.Connect(server)
   if err != nil {
       log.Fatal(err)
   }
   return nil, nil

}

func setVerboseCallbackHandler(vm *wren.VM, parameters []any) (any, error) {

   handle := parameters[0].(*wren.ForeignHandle)
   ifc, _ := handle.Get()
   ircObj := *(ifc.(**irc.Connection))
   value := parameters[1].(bool)
   ircObj.VerboseCallbackHandler = value
   return nil, nil

}

func setDebug(vm *wren.VM, parameters []any) (any, error) {

   handle := parameters[0].(*wren.ForeignHandle)
   ifc, _ := handle.Get()
   ircObj := *(ifc.(**irc.Connection))
   value := parameters[1].(bool)
   ircObj.Debug = value
   return nil, nil

}

func setUseTLS(vm *wren.VM, parameters []any) (any, error) {

   handle := parameters[0].(*wren.ForeignHandle)
   ifc, _ := handle.Get()
   ircObj := *(ifc.(**irc.Connection))
   value := parameters[1].(bool)
   ircObj.UseTLS = value
   return nil, nil

}

func setConfigTLS(vm *wren.VM, parameters []any) (any, error) {

   handle := parameters[0].(*wren.ForeignHandle)
   ifc, _ := handle.Get()
   ircObj := *(ifc.(**irc.Connection))
   value := parameters[1].(bool)
   ircObj.TLSConfig = &tls.Config{InsecureSkipVerify: value}
   return nil, nil

}

func addCallback(vm *wren.VM, parameters []any) (any, error) {

   handle := parameters[0].(*wren.ForeignHandle)
   ifc, _ := handle.Get()
   ircObj := *(ifc.(**irc.Connection))
   number := int(parameters[1].(float64))
   code := parameters[2].(string)
   msg := parameters[3].(string)
   channel := parameters[4].(string)
   nick := parameters[5].(string)
   otherNick := parameters[6].(string)
   if code == "001" {
       ircObj.AddCallback("001", func(e *irc.Event) {
           ircObj.Join(channel)
           ircObj.Privmsg(nick, msg)
           log.Println(msg)
       })
   } else if code == "366" {
       ircObj.AddCallback("366", func(e *irc.Event) {})
   } else if code == "PRIVMSG" {
       ircObj.AddCallback("PRIVMSG", func(e *irc.Event) {
           msg := fmt.Sprintf("<%s> %s", nick, e.Message)
           if number == 0 {
               ircObjs[1].Privmsg(otherNick, msg)
           } else {
               ircObjs[0].Privmsg(otherNick, msg)
           }
           log.Println(msg)
       })
   }
   return nil, nil

}

func newReader(vm *wren.VM, parameters []any) (any, error) {

   reader := bufio.NewReader(os.Stdin)
   return &reader, nil

}

func readLine(vm *wren.VM, parameters []any) (any, error) {

   handle := parameters[0].(*wren.ForeignHandle)
   ifc, _ := handle.Get()
   bufin := *(ifc.(**bufio.Reader))
   s, _ := bufin.ReadString('\n') // includes the delimiter
   return s[:len(s)-1], nil

}

func moduleFn(vm *wren.VM, name string) (string, bool) {

   if name != "meta" && name != "random" && !strings.HasSuffix(name, ".wren") {
       name += ".wren"
   }
   return wren.DefaultModuleLoader(vm, name)

}

func main() {

   cfg := wren.NewConfig()
   cfg.LoadModuleFn = moduleFn
   vm := cfg.NewVM()
   var fileName = "irc_gateway.wren"
   IRCMethodMap := wren.MethodMap{
       "connect(_)":                 connect,
       "verboseCallbackHandler=(_)": setVerboseCallbackHandler,
       "debug=(_)":                  setDebug,
       "useTLS=(_)":                 setUseTLS,
       "configTLS=(_)":              setConfigTLS,
       "addCallback(_,_,_,_,_,_)":   addCallback,
   }
   readerMethodMap := wren.MethodMap{"readLine()": readLine}
   classMap := wren.ClassMap{
       "IRC":    wren.NewClass(newIRC, nil, IRCMethodMap),
       "Reader": wren.NewClass(newReader, nil, readerMethodMap),
   }
   module := wren.NewModule(classMap)
   vm.SetModule(fileName, module)
   vm.InterpretFile(fileName)
   go ircObjs[1].Loop()
   ircObjs[0].Loop()
   vm.Free()

}</lang>