Files
techircd/linking.go

398 lines
9.3 KiB
Go

package main
import (
"fmt"
"log"
"net"
"strings"
"sync"
"time"
)
// LinkedServer represents a connection to another IRC server
type LinkedServer struct {
name string
conn net.Conn
host string
port int
password string
hub bool
description string
connected bool
lastPing time.Time
server *Server
mu sync.RWMutex
}
// LinkConfig represents configuration for server linking
type LinkConfig struct {
Enable bool `json:"enable"`
ServerPort int `json:"server_port"`
Password string `json:"password"`
Hub bool `json:"hub"`
AutoConnect bool `json:"auto_connect"`
Links []struct {
Name string `json:"name"`
Host string `json:"host"`
Port int `json:"port"`
Password string `json:"password"`
AutoConnect bool `json:"auto_connect"`
Hub bool `json:"hub"`
Description string `json:"description"`
} `json:"links"`
}
// ServerMessage represents a server-to-server message
type ServerMessage struct {
Prefix string
Command string
Params []string
Source string
Target string
}
// NewLinkedServer creates a new linked server connection
func NewLinkedServer(name, host string, port int, password string, hub bool, description string, server *Server) *LinkedServer {
return &LinkedServer{
name: name,
host: host,
port: port,
password: password,
hub: hub,
description: description,
server: server,
connected: false,
lastPing: time.Now(),
}
}
// Connect establishes a connection to the remote server
func (ls *LinkedServer) Connect() error {
ls.mu.Lock()
defer ls.mu.Unlock()
if ls.connected {
return fmt.Errorf("server %s is already connected", ls.name)
}
// Connect to remote server
var addr string
if strings.Contains(ls.host, ":") {
// IPv6 address, enclose in brackets
addr = fmt.Sprintf("[%s]:%d", ls.host, ls.port)
} else {
addr = fmt.Sprintf("%s:%d", ls.host, ls.port)
}
conn, err := net.DialTimeout("tcp", addr, 30*time.Second)
if err != nil {
return fmt.Errorf("failed to connect to %s: %v", ls.name, err)
}
ls.conn = conn
ls.connected = true
log.Printf("Connected to server %s at %s", ls.name, addr)
// Start handling the connection
go ls.Handle()
// Send server introduction
ls.SendServerAuth()
return nil
}
// Disconnect closes the connection to the remote server
func (ls *LinkedServer) Disconnect() {
ls.mu.Lock()
defer ls.mu.Unlock()
if !ls.connected {
return
}
if ls.conn != nil {
ls.conn.Close()
ls.conn = nil
}
ls.connected = false
log.Printf("Disconnected from server %s", ls.name)
}
// SendServerAuth sends authentication to the remote server
func (ls *LinkedServer) SendServerAuth() {
// Send PASS command
ls.SendMessage(fmt.Sprintf("PASS %s", ls.password))
// Send SERVER command
ls.SendMessage(fmt.Sprintf("SERVER %s 1 :%s", ls.server.config.Server.Name, ls.server.config.Server.Description))
log.Printf("Sent authentication to server %s", ls.name)
}
// SendMessage sends a raw message to the linked server
func (ls *LinkedServer) SendMessage(message string) error {
ls.mu.RLock()
defer ls.mu.RUnlock()
if !ls.connected || ls.conn == nil {
return fmt.Errorf("server %s is not connected", ls.name)
}
_, err := fmt.Fprintf(ls.conn, "%s\r\n", message)
if err != nil {
log.Printf("Error sending message to server %s: %v", ls.name, err)
ls.connected = false
}
return err
}
// Handle processes messages from the linked server
func (ls *LinkedServer) Handle() {
defer func() {
if r := recover(); r != nil {
log.Printf("Panic in server link handler for %s: %v", ls.name, r)
}
ls.Disconnect()
}()
scanner := net.Conn(ls.conn)
buffer := make([]byte, 4096)
for ls.connected {
// Set read deadline
ls.conn.SetReadDeadline(time.Now().Add(5 * time.Minute))
n, err := scanner.Read(buffer)
if err != nil {
if ls.connected {
log.Printf("Read error from server %s: %v", ls.name, err)
}
break
}
data := string(buffer[:n])
lines := strings.Split(strings.TrimSpace(data), "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
ls.handleServerMessage(line)
}
}
}
// handleServerMessage processes a single message from the linked server
func (ls *LinkedServer) handleServerMessage(line string) {
log.Printf("Received from server %s: %s", ls.name, line)
parts := strings.Fields(line)
if len(parts) == 0 {
return
}
var command string
var params []string
// Parse message prefix
if parts[0][0] == ':' {
parts = parts[1:]
}
if len(parts) == 0 {
return
}
command = strings.ToUpper(parts[0])
params = parts[1:]
// Handle server-to-server commands
switch command {
case "PASS":
ls.handleServerPass(params)
case "SERVER":
ls.handleServerIntro(params)
case "PING":
ls.handleServerPing(params)
case "PONG":
ls.handleServerPong()
case "SQUIT":
ls.handleServerQuit(params)
case "NICK":
ls.handleRemoteNick(params)
case "USER":
ls.handleRemoteUser(params)
case "JOIN":
ls.handleRemoteJoin(params)
case "PART":
ls.handleRemotePart(params)
case "QUIT":
ls.handleRemoteQuit(params)
case "PRIVMSG":
ls.handleRemotePrivmsg(params)
default:
log.Printf("Unknown server command from %s: %s", ls.name, command)
}
}
// handleServerPass handles PASS command from remote server
func (ls *LinkedServer) handleServerPass(params []string) {
if len(params) < 1 {
log.Printf("Invalid PASS from server %s", ls.name)
ls.Disconnect()
return
}
password := params[0]
if password != ls.password {
log.Printf("Invalid password from server %s", ls.name)
ls.Disconnect()
return
}
log.Printf("Server %s authenticated successfully", ls.name)
}
// handleServerIntro handles SERVER command from remote server
func (ls *LinkedServer) handleServerIntro(params []string) {
if len(params) < 3 {
log.Printf("Invalid SERVER from server %s", ls.name)
ls.Disconnect()
return
}
serverName := params[0]
hopCount := params[1]
description := strings.Join(params[2:], " ")
if description[0] == ':' {
description = description[1:]
}
log.Printf("Server introduced: %s (hops: %s) - %s", serverName, hopCount, description)
// Send our user list and channel list to the remote server
ls.sendBurst()
}
// handleServerPing handles PING from remote server
func (ls *LinkedServer) handleServerPing(params []string) {
if len(params) < 1 {
return
}
token := params[0]
ls.SendMessage(fmt.Sprintf("PONG %s %s", ls.server.config.Server.Name, token))
}
// handleServerPong handles PONG from remote server
func (ls *LinkedServer) handleServerPong() {
ls.mu.Lock()
ls.lastPing = time.Now()
ls.mu.Unlock()
}
// handleServerQuit handles SQUIT from remote server
func (ls *LinkedServer) handleServerQuit(params []string) {
if len(params) >= 1 {
reason := strings.Join(params, " ")
log.Printf("Server %s quit: %s", ls.name, reason)
}
ls.Disconnect()
}
// sendBurst sends initial data to newly connected server
func (ls *LinkedServer) sendBurst() {
// Send all local users
for _, client := range ls.server.GetClients() {
if client.IsRegistered() {
ls.SendMessage(fmt.Sprintf("NICK %s 1 %d %s %s %s %s :%s",
client.Nick(),
time.Now().Unix(),
client.User(),
client.Host(),
ls.server.config.Server.Name,
client.Nick(),
client.Realname()))
}
}
// Send all channels and their members
for _, channel := range ls.server.GetChannels() {
// Send channel creation
ls.SendMessage(fmt.Sprintf("NJOIN %s :", channel.name))
// Send channel members
var members []string
for _, client := range channel.GetClients() {
members = append(members, client.Nick())
}
if len(members) > 0 {
ls.SendMessage(fmt.Sprintf("NJOIN %s :%s", channel.name, strings.Join(members, " ")))
}
// Send channel topic if exists
if channel.topic != "" {
ls.SendMessage(fmt.Sprintf("TOPIC %s :%s", channel.name, channel.topic))
}
}
// End of burst
ls.SendMessage("EOB")
log.Printf("Sent burst to server %s", ls.name)
}
// Remote user handling functions
func (ls *LinkedServer) handleRemoteNick(params []string) {
// Handle remote NICK command
log.Printf("Remote NICK from %s: %s", ls.name, strings.Join(params, " "))
}
func (ls *LinkedServer) handleRemoteUser(params []string) {
// Handle remote USER command
log.Printf("Remote USER from %s: %s", ls.name, strings.Join(params, " "))
}
func (ls *LinkedServer) handleRemoteJoin(params []string) {
// Handle remote JOIN command
log.Printf("Remote JOIN from %s: %s", ls.name, strings.Join(params, " "))
}
func (ls *LinkedServer) handleRemotePart(params []string) {
// Handle remote PART command
log.Printf("Remote PART from %s: %s", ls.name, strings.Join(params, " "))
}
func (ls *LinkedServer) handleRemoteQuit(params []string) {
// Handle remote QUIT command
log.Printf("Remote QUIT from %s: %s", ls.name, strings.Join(params, " "))
}
func (ls *LinkedServer) handleRemotePrivmsg(params []string) {
// Handle remote PRIVMSG command
log.Printf("Remote PRIVMSG from %s: %s", ls.name, strings.Join(params, " "))
}
// IsConnected returns whether the server is currently connected
func (ls *LinkedServer) IsConnected() bool {
ls.mu.RLock()
defer ls.mu.RUnlock()
return ls.connected
}
// Name returns the server name
func (ls *LinkedServer) Name() string {
return ls.name
}
// Description returns the server description
func (ls *LinkedServer) Description() string {
return ls.description
}