3
0
mirror of https://github.com/ergochat/ergo.git synced 2024-12-22 18:52:41 +01:00

Merge pull request #22 from jlatt/cleanup

cleanup
This commit is contained in:
Jeremy Latt 2014-03-13 17:17:17 -07:00
commit 5df8173df2
16 changed files with 547 additions and 478 deletions

View File

@ -38,7 +38,7 @@ hostname lookups.
```sh
go get
go install
ergonomadic -conf ergonomadic.conf -initdb
ergonomadic initdb -conf ergonomadic.conf
```
## Configuration
@ -48,16 +48,16 @@ bcrypted byte strings. You can generate them with the `genpasswd`
subcommand.
```sh
ergonomadic -genpasswd 'hunter2!'
ergonomadic genpasswd 'hunter2!'
```
## Running the Server
```sh
ergonomadic -conf ergonomadic.conf
ergonomadic run -conf ergonomadic.conf
```
## Helpful Documentation
## IRC Documentation
- [RFC 1459: Internet Relay Chat Protocol](http://tools.ietf.org/html/rfc1459)
- [RFC 2811: IRC Channel Management](http://tools.ietf.org/html/rfc2811)

View File

@ -3,14 +3,9 @@ name = "irc.example.com" ; required, usually a hostname
database = "ergonomadic.db" ; path relative to this file
listen = "localhost:6667" ; see `net.Listen` for examples
listen = "[::1]:6667" ; multiple `listen`s are allowed.
log = "debug" ; error, warn, info, debug
motd = "motd.txt" ; path relative to this file
password = "JDJhJDA0JHJzVFFlNXdOUXNhLmtkSGRUQVVEVHVYWXRKUmdNQ3FKVTRrczRSMTlSWGRPZHRSMVRzQmtt" ; 'test'
[operator "root"]
password = "JDJhJDA0JEhkcm10UlNFRkRXb25iOHZuSDVLZXVBWlpyY0xyNkQ4dlBVc1VMWVk1LlFjWFpQbGxZNUtl" ; 'toor'
[debug]
net = true
client = false
channel = false
server = false

View File

@ -9,51 +9,68 @@ import (
"path/filepath"
)
func usage() {
fmt.Fprintln(os.Stderr, "ergonomadic <run|genpasswd|initdb|upgradedb> [options]")
fmt.Fprintln(os.Stderr, " run -conf <config> -- run server")
fmt.Fprintln(os.Stderr, " initdb -conf <config> -- initialize database")
fmt.Fprintln(os.Stderr, " upgrade -conf <config> -- upgrade database")
fmt.Fprintln(os.Stderr, " genpasswd <password> -- bcrypt a password")
flag.PrintDefaults()
}
func loadConfig(conf string) *irc.Config {
config, err := irc.LoadConfig(conf)
if err != nil {
log.Fatalln("error loading config:", err)
}
err = os.Chdir(filepath.Dir(conf))
if err != nil {
log.Fatalln("chdir error:", err)
}
return config
}
func genPasswd() {
}
func main() {
conf := flag.String("conf", "ergonomadic.conf", "ergonomadic config file")
initdb := flag.Bool("initdb", false, "initialize database")
upgradedb := flag.Bool("upgradedb", false, "update database")
passwd := flag.String("genpasswd", "", "bcrypt a password")
var conf string
flag.Usage = usage
runFlags := flag.NewFlagSet("run", flag.ExitOnError)
runFlags.Usage = usage
runFlags.StringVar(&conf, "conf", "ergonomadic.conf", "ergonomadic config file")
flag.Parse()
if *passwd != "" {
encoded, err := irc.GenerateEncodedPassword(*passwd)
switch flag.Arg(0) {
case "genpasswd":
encoded, err := irc.GenerateEncodedPassword(flag.Arg(1))
if err != nil {
log.Fatal("encoding error: ", err)
log.Fatalln("encoding error:", err)
}
fmt.Println(encoded)
return
}
config, err := irc.LoadConfig(*conf)
if err != nil {
log.Fatal("error loading config: ", err)
}
err = os.Chdir(filepath.Dir(*conf))
if err != nil {
log.Fatal("chdir error: ", err)
}
if *initdb {
case "initdb":
runFlags.Parse(flag.Args()[1:])
config := loadConfig(conf)
irc.InitDB(config.Server.Database)
log.Println("database initialized: ", config.Server.Database)
return
}
if *upgradedb {
case "upgradedb":
runFlags.Parse(flag.Args()[1:])
config := loadConfig(conf)
irc.UpgradeDB(config.Server.Database)
log.Println("database upgraded: ", config.Server.Database)
return
default:
runFlags.Parse(flag.Args()[1:])
config := loadConfig(conf)
irc.Log.SetLevel(config.Server.Log)
server := irc.NewServer(config)
log.Println(irc.SEM_VER, "running")
defer log.Println(irc.SEM_VER, "exiting")
server.Run()
}
// TODO move to data structures
irc.DEBUG_NET = config.Debug.Net
irc.DEBUG_CLIENT = config.Debug.Client
irc.DEBUG_CHANNEL = config.Debug.Channel
irc.DEBUG_SERVER = config.Debug.Server
server := irc.NewServer(config)
log.Println(irc.SEM_VER, "running")
defer log.Println(irc.SEM_VER, "exiting")
server.Run()
}

116
irc/capability.go Normal file
View File

@ -0,0 +1,116 @@
package irc
import (
"strings"
)
type CapSubCommand string
const (
CAP_LS CapSubCommand = "LS"
CAP_LIST CapSubCommand = "LIST"
CAP_REQ CapSubCommand = "REQ"
CAP_ACK CapSubCommand = "ACK"
CAP_NAK CapSubCommand = "NAK"
CAP_CLEAR CapSubCommand = "CLEAR"
CAP_END CapSubCommand = "END"
)
// Capabilities are optional features a client may request from a server.
type Capability string
const (
MultiPrefix Capability = "multi-prefix"
SASL Capability = "sasl"
)
var (
SupportedCapabilities = CapabilitySet{
MultiPrefix: true,
}
)
func (capability Capability) String() string {
return string(capability)
}
// CapModifiers are indicators showing the state of a capability after a REQ or
// ACK.
type CapModifier rune
const (
Ack CapModifier = '~'
Disable CapModifier = '-'
Sticky CapModifier = '='
)
func (mod CapModifier) String() string {
return string(mod)
}
type CapState uint
const (
CapNone CapState = iota
CapNegotiating CapState = iota
CapNegotiated CapState = iota
)
type CapabilitySet map[Capability]bool
func (set CapabilitySet) String() string {
strs := make([]string, len(set))
index := 0
for capability := range set {
strs[index] = string(capability)
index += 1
}
return strings.Join(strs, " ")
}
func (set CapabilitySet) DisableString() string {
parts := make([]string, len(set))
index := 0
for capability := range set {
parts[index] = Disable.String() + capability.String()
index += 1
}
return strings.Join(parts, " ")
}
func (msg *CapCommand) HandleRegServer(server *Server) {
client := msg.Client()
switch msg.subCommand {
case CAP_LS:
client.capState = CapNegotiating
client.Reply(RplCap(client, CAP_LS, SupportedCapabilities))
case CAP_LIST:
client.Reply(RplCap(client, CAP_LIST, client.capabilities))
case CAP_REQ:
for capability := range msg.capabilities {
if !SupportedCapabilities[capability] {
client.Reply(RplCap(client, CAP_NAK, msg.capabilities))
return
}
}
for capability := range msg.capabilities {
client.capabilities[capability] = true
}
client.Reply(RplCap(client, CAP_ACK, msg.capabilities))
case CAP_CLEAR:
reply := RplCap(client, CAP_ACK, client.capabilities.DisableString())
client.capabilities = make(CapabilitySet)
client.Reply(reply)
case CAP_END:
client.capState = CapNegotiated
server.tryRegister(client)
default:
client.ErrInvalidCapCmd(msg.subCommand)
}
}

View File

@ -444,8 +444,8 @@ func (channel *Channel) Persist() (err error) {
(name, flags, key, topic, user_limit, ban_list, except_list,
invite_list)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
channel.name, channel.flags.String(), channel.key, channel.topic,
channel.userLimit, channel.lists[BanMask].String(),
channel.name.String(), channel.flags.String(), channel.key.String(),
channel.topic.String(), channel.userLimit, channel.lists[BanMask].String(),
channel.lists[ExceptMask].String(), channel.lists[InviteMask].String())
} else {
_, err = channel.server.db.Exec(`

View File

@ -2,11 +2,16 @@ package irc
import (
"fmt"
"log"
"net"
"time"
)
const (
LOGIN_TIMEOUT = time.Minute / 2 // how long the client has to login
IDLE_TIMEOUT = time.Minute // how long before a client is considered idle
QUIT_TIMEOUT = time.Minute // how long after idle before a client is kicked
)
type Client struct {
atime time.Time
authorized bool
@ -14,7 +19,7 @@ type Client struct {
capabilities CapabilitySet
capState CapState
channels ChannelSet
commands chan editableCommand
commands chan Command
ctime time.Time
flags map[UserMode]bool
hasQuit bool
@ -23,9 +28,9 @@ type Client struct {
idleTimer *time.Timer
loginTimer *time.Timer
nick Name
phase Phase
quitTimer *time.Timer
realname Text
registered bool
server *Server
socket *Socket
username Name
@ -39,10 +44,9 @@ func NewClient(server *Server, conn net.Conn) *Client {
capState: CapNone,
capabilities: make(CapabilitySet),
channels: make(ChannelSet),
commands: make(chan editableCommand),
commands: make(chan Command),
ctime: now,
flags: make(map[UserMode]bool),
phase: Registration,
server: server,
}
client.socket = NewSocket(conn, client.commands)
@ -115,7 +119,10 @@ func (client *Client) Idle() {
}
func (client *Client) Register() {
client.phase = Normal
if client.registered {
return
}
client.registered = true
client.loginTimer.Stop()
client.Touch()
}
@ -145,9 +152,7 @@ func (client *Client) destroy() {
client.socket.Close()
if DEBUG_CLIENT {
log.Printf("%s: destroyed", client)
}
Log.debug.Printf("%s: destroyed", client)
}
func (client *Client) IdleTime() time.Duration {

View File

@ -88,25 +88,19 @@ func (clients *ClientLookupSet) FindAll(userhost Name) (set ClientSet) {
`SELECT nickname FROM client WHERE userhost LIKE ? ESCAPE '\'`,
QuoteLike(userhost))
if err != nil {
if DEBUG_SERVER {
log.Println("ClientLookupSet.FindAll.Query:", err)
}
Log.error.Println("ClientLookupSet.FindAll.Query:", err)
return
}
for rows.Next() {
var nickname Name
err := rows.Scan(&nickname)
if err != nil {
if DEBUG_SERVER {
log.Println("ClientLookupSet.FindAll.Scan:", err)
}
Log.error.Println("ClientLookupSet.FindAll.Scan:", err)
return
}
client := clients.Get(nickname)
if client == nil {
if DEBUG_SERVER {
log.Println("ClientLookupSet.FindAll: missing client:", nickname)
}
Log.error.Println("ClientLookupSet.FindAll: missing client:", nickname)
continue
}
set.Add(client)
@ -122,9 +116,7 @@ func (clients *ClientLookupSet) Find(userhost Name) *Client {
var nickname Name
err := row.Scan(&nickname)
if err != nil {
if DEBUG_SERVER {
log.Println("ClientLookupSet.Find:", err)
}
Log.error.Println("ClientLookupSet.Find:", err)
return nil
}
return clients.Get(nickname)
@ -161,21 +153,17 @@ func NewClientDB() *ClientDB {
func (db *ClientDB) Add(client *Client) {
_, err := db.db.Exec(`INSERT INTO client (nickname, userhost) VALUES (?, ?)`,
client.Nick(), client.UserHost())
client.Nick().String(), client.UserHost().String())
if err != nil {
if DEBUG_SERVER {
log.Println("ClientDB.Add:", err)
}
Log.error.Println("ClientDB.Add:", err)
}
}
func (db *ClientDB) Remove(client *Client) {
_, err := db.db.Exec(`DELETE FROM client WHERE nickname = ?`,
client.Nick())
client.Nick().String())
if err != nil {
if DEBUG_SERVER {
log.Println("ClientDB.Remove:", err)
}
Log.error.Println("ClientDB.Remove:", err)
}
}

View File

@ -8,10 +8,11 @@ import (
"strings"
)
type editableCommand interface {
Command
SetCode(StringCode)
type Command interface {
Client() *Client
Code() StringCode
SetClient(*Client)
SetCode(StringCode)
}
type checkPasswordCommand interface {
@ -19,7 +20,7 @@ type checkPasswordCommand interface {
CheckPassword()
}
type parseCommandFunc func([]string) (editableCommand, error)
type parseCommandFunc func([]string) (Command, error)
var (
NotEnoughArgsError = errors.New("not enough arguments")
@ -78,7 +79,7 @@ func (command *BaseCommand) SetCode(code StringCode) {
command.code = code
}
func ParseCommand(line string) (cmd editableCommand, err error) {
func ParseCommand(line string) (cmd Command, err error) {
code, args := ParseLine(line)
constructor := parseCommandFuncs[code]
if constructor == nil {
@ -154,7 +155,7 @@ func (cmd *PingCommand) String() string {
return fmt.Sprintf("PING(server=%s, server2=%s)", cmd.server, cmd.server2)
}
func NewPingCommand(args []string) (editableCommand, error) {
func NewPingCommand(args []string) (Command, error) {
if len(args) < 1 {
return nil, NotEnoughArgsError
}
@ -179,7 +180,7 @@ func (cmd *PongCommand) String() string {
return fmt.Sprintf("PONG(server1=%s, server2=%s)", cmd.server1, cmd.server2)
}
func NewPongCommand(args []string) (editableCommand, error) {
func NewPongCommand(args []string) (Command, error) {
if len(args) < 1 {
return nil, NotEnoughArgsError
}
@ -216,7 +217,7 @@ func (cmd *PassCommand) CheckPassword() {
cmd.err = ComparePassword(cmd.hash, cmd.password)
}
func NewPassCommand(args []string) (editableCommand, error) {
func NewPassCommand(args []string) (Command, error) {
if len(args) < 1 {
return nil, NotEnoughArgsError
}
@ -236,7 +237,7 @@ func (m *NickCommand) String() string {
return fmt.Sprintf("NICK(nickname=%s)", m.nickname)
}
func NewNickCommand(args []string) (editableCommand, error) {
func NewNickCommand(args []string) (Command, error) {
if len(args) != 1 {
return nil, NotEnoughArgsError
}
@ -286,7 +287,7 @@ func (cmd *RFC2812UserCommand) Flags() []UserMode {
return flags
}
func NewUserCommand(args []string) (editableCommand, error) {
func NewUserCommand(args []string) (Command, error) {
if len(args) != 4 {
return nil, NotEnoughArgsError
}
@ -321,7 +322,7 @@ func (cmd *QuitCommand) String() string {
return fmt.Sprintf("QUIT(message=%s)", cmd.message)
}
func NewQuitCommand(args []string) (editableCommand, error) {
func NewQuitCommand(args []string) (Command, error) {
msg := &QuitCommand{}
if len(args) > 0 {
msg.message = NewText(args[0])
@ -341,7 +342,7 @@ func (cmd *JoinCommand) String() string {
return fmt.Sprintf("JOIN(channels=%s, zero=%t)", cmd.channels, cmd.zero)
}
func NewJoinCommand(args []string) (editableCommand, error) {
func NewJoinCommand(args []string) (Command, error) {
msg := &JoinCommand{
channels: make(map[Name]Text),
}
@ -388,7 +389,7 @@ func (cmd *PartCommand) String() string {
return fmt.Sprintf("PART(channels=%s, message=%s)", cmd.channels, cmd.message)
}
func NewPartCommand(args []string) (editableCommand, error) {
func NewPartCommand(args []string) (Command, error) {
if len(args) < 1 {
return nil, NotEnoughArgsError
}
@ -413,7 +414,7 @@ func (cmd *PrivMsgCommand) String() string {
return fmt.Sprintf("PRIVMSG(target=%s, message=%s)", cmd.target, cmd.message)
}
func NewPrivMsgCommand(args []string) (editableCommand, error) {
func NewPrivMsgCommand(args []string) (Command, error) {
if len(args) < 2 {
return nil, NotEnoughArgsError
}
@ -436,7 +437,7 @@ func (cmd *TopicCommand) String() string {
return fmt.Sprintf("TOPIC(channel=%s, topic=%s)", cmd.channel, cmd.topic)
}
func NewTopicCommand(args []string) (editableCommand, error) {
func NewTopicCommand(args []string) (Command, error) {
if len(args) < 1 {
return nil, NotEnoughArgsError
}
@ -486,7 +487,7 @@ type ModeCommand struct {
}
// MODE <nickname> *( ( "+" / "-" ) *( "i" / "w" / "o" / "O" / "r" ) )
func NewUserModeCommand(nickname Name, args []string) (editableCommand, error) {
func NewUserModeCommand(nickname Name, args []string) (Command, error) {
cmd := &ModeCommand{
nickname: nickname,
changes: make(ModeChanges, 0),
@ -563,7 +564,7 @@ type ChannelModeCommand struct {
}
// MODE <channel> *( ( "-" / "+" ) *<modes> *<modeparams> )
func NewChannelModeCommand(channel Name, args []string) (editableCommand, error) {
func NewChannelModeCommand(channel Name, args []string) (Command, error) {
cmd := &ChannelModeCommand{
channel: channel,
changes: make(ChannelModeChanges, 0),
@ -609,7 +610,7 @@ func (msg *ChannelModeCommand) String() string {
return fmt.Sprintf("MODE(channel=%s, changes=%s)", msg.channel, msg.changes)
}
func NewModeCommand(args []string) (editableCommand, error) {
func NewModeCommand(args []string) (Command, error) {
if len(args) == 0 {
return nil, NotEnoughArgsError
}
@ -629,7 +630,7 @@ type WhoisCommand struct {
}
// WHOIS [ <target> ] <mask> *( "," <mask> )
func NewWhoisCommand(args []string) (editableCommand, error) {
func NewWhoisCommand(args []string) (Command, error) {
if len(args) < 1 {
return nil, NotEnoughArgsError
}
@ -661,7 +662,7 @@ type WhoCommand struct {
}
// WHO [ <mask> [ "o" ] ]
func NewWhoCommand(args []string) (editableCommand, error) {
func NewWhoCommand(args []string) (Command, error) {
cmd := &WhoCommand{}
if len(args) > 0 {
@ -693,7 +694,7 @@ func (msg *OperCommand) LoadPassword(server *Server) {
}
// OPER <name> <password>
func NewOperCommand(args []string) (editableCommand, error) {
func NewOperCommand(args []string) (Command, error) {
if len(args) < 2 {
return nil, NotEnoughArgsError
}
@ -716,7 +717,7 @@ func (msg *CapCommand) String() string {
msg.subCommand, msg.capabilities)
}
func NewCapCommand(args []string) (editableCommand, error) {
func NewCapCommand(args []string) (Command, error) {
if len(args) < 1 {
return nil, NotEnoughArgsError
}
@ -750,7 +751,7 @@ func (msg *ProxyCommand) String() string {
return fmt.Sprintf("PROXY(sourceIP=%s, sourcePort=%s)", msg.sourceIP, msg.sourcePort)
}
func NewProxyCommand(args []string) (editableCommand, error) {
func NewProxyCommand(args []string) (Command, error) {
if len(args) < 5 {
return nil, NotEnoughArgsError
}
@ -774,7 +775,7 @@ func (msg *AwayCommand) String() string {
return fmt.Sprintf("AWAY(%s)", msg.text)
}
func NewAwayCommand(args []string) (editableCommand, error) {
func NewAwayCommand(args []string) (Command, error) {
cmd := &AwayCommand{}
if len(args) > 0 {
@ -794,7 +795,7 @@ func (msg *IsOnCommand) String() string {
return fmt.Sprintf("ISON(nicks=%s)", msg.nicks)
}
func NewIsOnCommand(args []string) (editableCommand, error) {
func NewIsOnCommand(args []string) (Command, error) {
if len(args) == 0 {
return nil, NotEnoughArgsError
}
@ -809,7 +810,7 @@ type MOTDCommand struct {
target Name
}
func NewMOTDCommand(args []string) (editableCommand, error) {
func NewMOTDCommand(args []string) (Command, error) {
cmd := &MOTDCommand{}
if len(args) > 0 {
cmd.target = NewName(args[0])
@ -827,7 +828,7 @@ func (cmd *NoticeCommand) String() string {
return fmt.Sprintf("NOTICE(target=%s, message=%s)", cmd.target, cmd.message)
}
func NewNoticeCommand(args []string) (editableCommand, error) {
func NewNoticeCommand(args []string) (Command, error) {
if len(args) < 2 {
return nil, NotEnoughArgsError
}
@ -850,7 +851,7 @@ func (msg *KickCommand) Comment() Text {
return msg.comment
}
func NewKickCommand(args []string) (editableCommand, error) {
func NewKickCommand(args []string) (Command, error) {
if len(args) < 2 {
return nil, NotEnoughArgsError
}
@ -881,7 +882,7 @@ type ListCommand struct {
target Name
}
func NewListCommand(args []string) (editableCommand, error) {
func NewListCommand(args []string) (Command, error) {
cmd := &ListCommand{}
if len(args) > 0 {
cmd.channels = NewNames(strings.Split(args[0], ","))
@ -898,7 +899,7 @@ type NamesCommand struct {
target Name
}
func NewNamesCommand(args []string) (editableCommand, error) {
func NewNamesCommand(args []string) (Command, error) {
cmd := &NamesCommand{}
if len(args) > 0 {
cmd.channels = NewNames(strings.Split(args[0], ","))
@ -914,7 +915,7 @@ type DebugCommand struct {
subCommand Name
}
func NewDebugCommand(args []string) (editableCommand, error) {
func NewDebugCommand(args []string) (Command, error) {
if len(args) == 0 {
return nil, NotEnoughArgsError
}
@ -929,7 +930,7 @@ type VersionCommand struct {
target Name
}
func NewVersionCommand(args []string) (editableCommand, error) {
func NewVersionCommand(args []string) (Command, error) {
cmd := &VersionCommand{}
if len(args) > 0 {
cmd.target = NewName(args[0])
@ -943,7 +944,7 @@ type InviteCommand struct {
channel Name
}
func NewInviteCommand(args []string) (editableCommand, error) {
func NewInviteCommand(args []string) (Command, error) {
if len(args) < 2 {
return nil, NotEnoughArgsError
}
@ -959,7 +960,7 @@ type TimeCommand struct {
target Name
}
func NewTimeCommand(args []string) (editableCommand, error) {
func NewTimeCommand(args []string) (Command, error) {
cmd := &TimeCommand{}
if len(args) > 0 {
cmd.target = NewName(args[0])
@ -973,7 +974,7 @@ type KillCommand struct {
comment Text
}
func NewKillCommand(args []string) (editableCommand, error) {
func NewKillCommand(args []string) (Command, error) {
if len(args) < 2 {
return nil, NotEnoughArgsError
}
@ -990,7 +991,7 @@ type WhoWasCommand struct {
target Name
}
func NewWhoWasCommand(args []string) (editableCommand, error) {
func NewWhoWasCommand(args []string) (Command, error) {
if len(args) < 1 {
return nil, NotEnoughArgsError
}

View File

@ -23,18 +23,12 @@ type Config struct {
PassConfig
Database string
Listen []string
Log string
MOTD string
Name string
}
Operator map[string]*PassConfig
Debug struct {
Net bool
Client bool
Channel bool
Server bool
}
}
func (conf *Config) Operators() map[Name][]byte {

View File

@ -1,30 +1,10 @@
package irc
import (
"errors"
"time"
)
var (
// debugging flags
DEBUG_NET = false
DEBUG_CLIENT = false
DEBUG_CHANNEL = false
DEBUG_SERVER = false
// errors
ErrAlreadyDestroyed = errors.New("already destroyed")
)
const (
SEM_VER = "ergonomadic-1.3.1"
CRLF = "\r\n"
MAX_REPLY_LEN = 512 - len(CRLF)
LOGIN_TIMEOUT = time.Minute / 2 // how long the client has to login
IDLE_TIMEOUT = time.Minute // how long before a client is considered idle
QUIT_TIMEOUT = time.Minute // how long after idle before a client is kicked
// string codes
AWAY StringCode = "AWAY"
CAP StringCode = "CAP"
@ -195,67 +175,4 @@ const (
ERR_NOOPERHOST NumericCode = 491
ERR_UMODEUNKNOWNFLAG NumericCode = 501
ERR_USERSDONTMATCH NumericCode = 502
CAP_LS CapSubCommand = "LS"
CAP_LIST CapSubCommand = "LIST"
CAP_REQ CapSubCommand = "REQ"
CAP_ACK CapSubCommand = "ACK"
CAP_NAK CapSubCommand = "NAK"
CAP_CLEAR CapSubCommand = "CLEAR"
CAP_END CapSubCommand = "END"
Add ModeOp = '+'
List ModeOp = '='
Remove ModeOp = '-'
Away UserMode = 'a'
Invisible UserMode = 'i'
LocalOperator UserMode = 'O'
Operator UserMode = 'o'
Restricted UserMode = 'r'
ServerNotice UserMode = 's' // deprecated
WallOps UserMode = 'w'
Anonymous ChannelMode = 'a' // flag
BanMask ChannelMode = 'b' // arg
ChannelCreator ChannelMode = 'O' // flag
ChannelOperator ChannelMode = 'o' // arg
ExceptMask ChannelMode = 'e' // arg
InviteMask ChannelMode = 'I' // arg
InviteOnly ChannelMode = 'i' // flag
Key ChannelMode = 'k' // flag arg
Moderated ChannelMode = 'm' // flag
NoOutside ChannelMode = 'n' // flag
OpOnlyTopic ChannelMode = 't' // flag
Persistent ChannelMode = 'P' // flag
Private ChannelMode = 'p' // flag
Quiet ChannelMode = 'q' // flag
ReOp ChannelMode = 'r' // flag
Secret ChannelMode = 's' // flag, deprecated
UserLimit ChannelMode = 'l' // flag arg
Voice ChannelMode = 'v' // arg
MultiPrefix Capability = "multi-prefix"
SASL Capability = "sasl"
Disable CapModifier = '-'
Ack CapModifier = '~'
Sticky CapModifier = '='
)
var (
SupportedCapabilities = CapabilitySet{
MultiPrefix: true,
}
)
const (
Registration Phase = iota
Normal Phase = iota
)
const (
CapNone CapState = iota
CapNegotiating CapState = iota
CapNegotiated CapState = iota
)

60
irc/logging.go Normal file
View File

@ -0,0 +1,60 @@
package irc
import (
"io"
"log"
"os"
)
type Logging struct {
debug *log.Logger
info *log.Logger
warn *log.Logger
error *log.Logger
}
var (
levels = map[string]uint8{
"debug": 4,
"info": 3,
"warn": 2,
"error": 1,
}
devNull io.Writer
)
func init() {
var err error
devNull, err = os.Open(os.DevNull)
if err != nil {
log.Fatal(err)
}
}
func NewLogger(on bool) *log.Logger {
return log.New(output(on), "", log.LstdFlags)
}
func output(on bool) io.Writer {
if on {
return os.Stdout
}
return devNull
}
func (logging *Logging) SetLevel(level string) {
logging.debug = NewLogger(levels[level] >= levels["debug"])
logging.info = NewLogger(levels[level] >= levels["info"])
logging.warn = NewLogger(levels[level] >= levels["warn"])
logging.error = NewLogger(levels[level] >= levels["error"])
}
func NewLogging(level string) *Logging {
logging := &Logging{}
logging.SetLevel(level)
return logging
}
var (
Log = NewLogging("warn")
)

162
irc/modes.go Normal file
View File

@ -0,0 +1,162 @@
package irc
import (
"strings"
)
// user mode flags
type UserMode rune
func (mode UserMode) String() string {
return string(mode)
}
type UserModes []UserMode
func (modes UserModes) String() string {
strs := make([]string, len(modes))
for index, mode := range modes {
strs[index] = mode.String()
}
return strings.Join(strs, "")
}
// channel mode flags
type ChannelMode rune
func (mode ChannelMode) String() string {
return string(mode)
}
type ChannelModes []ChannelMode
func (modes ChannelModes) String() string {
strs := make([]string, len(modes))
for index, mode := range modes {
strs[index] = mode.String()
}
return strings.Join(strs, "")
}
type ModeOp rune
func (op ModeOp) String() string {
return string(op)
}
const (
Add ModeOp = '+'
List ModeOp = '='
Remove ModeOp = '-'
)
const (
Away UserMode = 'a'
Invisible UserMode = 'i'
LocalOperator UserMode = 'O'
Operator UserMode = 'o'
Restricted UserMode = 'r'
ServerNotice UserMode = 's' // deprecated
WallOps UserMode = 'w'
)
var (
SupportedUserModes = UserModes{
Away, Invisible, Operator,
}
)
const (
Anonymous ChannelMode = 'a' // flag
BanMask ChannelMode = 'b' // arg
ChannelCreator ChannelMode = 'O' // flag
ChannelOperator ChannelMode = 'o' // arg
ExceptMask ChannelMode = 'e' // arg
InviteMask ChannelMode = 'I' // arg
InviteOnly ChannelMode = 'i' // flag
Key ChannelMode = 'k' // flag arg
Moderated ChannelMode = 'm' // flag
NoOutside ChannelMode = 'n' // flag
OpOnlyTopic ChannelMode = 't' // flag
Persistent ChannelMode = 'P' // flag
Private ChannelMode = 'p' // flag
Quiet ChannelMode = 'q' // flag
ReOp ChannelMode = 'r' // flag
Secret ChannelMode = 's' // flag, deprecated
UserLimit ChannelMode = 'l' // flag arg
Voice ChannelMode = 'v' // arg
)
var (
SupportedChannelModes = ChannelModes{
BanMask, ExceptMask, InviteMask, InviteOnly, Key, NoOutside,
OpOnlyTopic, Persistent, Private, UserLimit,
}
)
//
// commands
//
func (m *ModeCommand) HandleServer(s *Server) {
client := m.Client()
target := s.clients.Get(m.nickname)
if target == nil {
client.ErrNoSuchNick(m.nickname)
return
}
if client != target && !client.flags[Operator] {
client.ErrUsersDontMatch()
return
}
changes := make(ModeChanges, 0, len(m.changes))
for _, change := range m.changes {
switch change.mode {
case Invisible, ServerNotice, WallOps:
switch change.op {
case Add:
if target.flags[change.mode] {
continue
}
target.flags[change.mode] = true
changes = append(changes, change)
case Remove:
if !target.flags[change.mode] {
continue
}
delete(target.flags, change.mode)
changes = append(changes, change)
}
case Operator, LocalOperator:
if change.op == Remove {
if !target.flags[change.mode] {
continue
}
delete(target.flags, change.mode)
changes = append(changes, change)
}
}
}
// Who should get these replies?
if len(changes) > 0 {
client.Reply(RplMode(client, target, changes))
}
}
func (msg *ChannelModeCommand) HandleServer(server *Server) {
client := msg.Client()
channel := server.channels.Get(msg.channel)
if channel == nil {
client.ErrNoSuchChannel(msg.channel)
return
}
channel.Mode(client, msg.changes)
}

View File

@ -6,7 +6,23 @@ import (
"time"
)
func NewStringReply(source Identifier, code StringCode,
type ReplyCode interface {
String() string
}
type StringCode string
func (code StringCode) String() string {
return string(code)
}
type NumericCode uint
func (code NumericCode) String() string {
return fmt.Sprintf("%03d", code)
}
func NewStringReply(source Identifiable, code StringCode,
format string, args ...interface{}) string {
var header string
if source == nil {
@ -79,15 +95,15 @@ func (target *Client) MultilineReply(names []string, code NumericCode, format st
// messaging replies
//
func RplPrivMsg(source Identifier, target Identifier, message Text) string {
func RplPrivMsg(source Identifiable, target Identifiable, message Text) string {
return NewStringReply(source, PRIVMSG, "%s :%s", target.Nick(), message)
}
func RplNotice(source Identifier, target Identifier, message Text) string {
func RplNotice(source Identifiable, target Identifiable, message Text) string {
return NewStringReply(source, NOTICE, "%s :%s", target.Nick(), message)
}
func RplNick(source Identifier, newNick Name) string {
func RplNick(source Identifiable, newNick Name) string {
return NewStringReply(source, NICK, newNick.String())
}
@ -108,11 +124,11 @@ func RplChannelMode(client *Client, channel *Channel,
return NewStringReply(client, MODE, "%s %s", channel, changes)
}
func RplTopicMsg(source Identifier, channel *Channel) string {
func RplTopicMsg(source Identifiable, channel *Channel) string {
return NewStringReply(source, TOPIC, "%s :%s", channel, channel.topic)
}
func RplPing(target Identifier) string {
func RplPing(target Identifiable) string {
return NewStringReply(nil, PING, ":%s", target.Nick())
}
@ -165,7 +181,8 @@ func (target *Client) RplCreated() {
func (target *Client) RplMyInfo() {
target.NumericReply(RPL_MYINFO,
"%s %s aiOorsw abeIikmntpqrsl", target.server.name, SEM_VER)
"%s %s %s %s",
target.server.name, SEM_VER, SupportedUserModes, SupportedChannelModes)
}
func (target *Client) RplUModeIs(client *Client) {

View File

@ -16,6 +16,16 @@ import (
"time"
)
type ServerCommand interface {
Command
HandleServer(*Server)
}
type RegServerCommand interface {
Command
HandleRegServer(*Server)
}
type Server struct {
channels ChannelNameMap
clients *ClientLookupSet
@ -32,19 +42,24 @@ type Server struct {
whoWas *WhoWasList
}
var (
SERVER_SIGNALS = []os.Signal{syscall.SIGINT, syscall.SIGHUP,
syscall.SIGTERM, syscall.SIGQUIT}
)
func NewServer(config *Config) *Server {
server := &Server{
channels: make(ChannelNameMap),
clients: NewClientLookupSet(),
commands: make(chan Command, 16),
commands: make(chan Command),
ctime: time.Now(),
db: OpenDB(config.Server.Database),
idle: make(chan *Client, 16),
idle: make(chan *Client),
motdFile: config.Server.MOTD,
name: NewName(config.Server.Name),
newConns: make(chan net.Conn, 16),
newConns: make(chan net.Conn),
operators: config.Operators(),
signals: make(chan os.Signal, 1),
signals: make(chan os.Signal, len(SERVER_SIGNALS)),
whoWas: NewWhoWasList(100),
}
@ -58,8 +73,7 @@ func NewServer(config *Config) *Server {
go server.listen(addr)
}
signal.Notify(server.signals, syscall.SIGINT, syscall.SIGHUP,
syscall.SIGTERM, syscall.SIGQUIT)
signal.Notify(server.signals, SERVER_SIGNALS...)
return server
}
@ -80,9 +94,7 @@ func (server *Server) loadChannels() {
log.Fatal("error loading channels: ", err)
}
for rows.Next() {
var name Name
var flags string
var key, topic Text
var name, flags, key, topic string
var userLimit uint64
var banList, exceptList, inviteList string
err = rows.Scan(&name, &flags, &key, &topic, &userLimit, &banList,
@ -92,12 +104,12 @@ func (server *Server) loadChannels() {
continue
}
channel := NewChannel(server, name)
channel := NewChannel(server, NewName(name))
for _, flag := range flags {
channel.flags[ChannelMode(flag)] = true
}
channel.key = key
channel.topic = topic
channel.key = NewText(key)
channel.topic = NewText(topic)
channel.userLimit = userLimit
loadChannelList(channel, banList, BanMask)
loadChannelList(channel, exceptList, ExceptMask)
@ -107,38 +119,35 @@ func (server *Server) loadChannels() {
func (server *Server) processCommand(cmd Command) {
client := cmd.Client()
if DEBUG_SERVER {
log.Printf("%s → %s %s", client, server, cmd)
}
Log.debug.Printf("%s → %s %s", client, server, cmd)
switch client.phase {
case Registration:
if !client.registered {
regCmd, ok := cmd.(RegServerCommand)
if !ok {
client.Quit("unexpected command")
return
}
regCmd.HandleRegServer(server)
case Normal:
srvCmd, ok := cmd.(ServerCommand)
if !ok {
client.ErrUnknownCommand(cmd.Code())
return
}
switch srvCmd.(type) {
case *PingCommand, *PongCommand:
client.Touch()
case *QuitCommand:
// no-op
default:
client.Active()
client.Touch()
}
srvCmd.HandleServer(server)
return
}
srvCmd, ok := cmd.(ServerCommand)
if !ok {
client.ErrUnknownCommand(cmd.Code())
return
}
switch srvCmd.(type) {
case *PingCommand, *PongCommand:
client.Touch()
case *QuitCommand:
// no-op
default:
client.Active()
client.Touch()
}
srvCmd.HandleServer(server)
}
func (server *Server) Shutdown() {
@ -178,21 +187,15 @@ func (s *Server) listen(addr string) {
log.Fatal(s, "listen error: ", err)
}
if DEBUG_SERVER {
log.Printf("%s listening on %s", s, addr)
}
Log.info.Printf("%s listening on %s", s, addr)
for {
conn, err := listener.Accept()
if err != nil {
if DEBUG_SERVER {
log.Printf("%s accept error: %s", s, err)
}
Log.error.Printf("%s accept error: %s", s, err)
continue
}
if DEBUG_SERVER {
log.Printf("%s accept: %s", s, conn.RemoteAddr())
}
Log.debug.Printf("%s accept: %s", s, conn.RemoteAddr())
s.newConns <- conn
}
@ -203,14 +206,17 @@ func (s *Server) listen(addr string) {
//
func (s *Server) tryRegister(c *Client) {
if c.HasNick() && c.HasUsername() && (c.capState != CapNegotiating) {
c.Register()
c.RplWelcome()
c.RplYourHost()
c.RplCreated()
c.RplMyInfo()
s.MOTD(c)
if c.registered || !c.HasNick() || !c.HasUsername() ||
(c.capState == CapNegotiating) {
return
}
c.Register()
c.RplWelcome()
c.RplYourHost()
c.RplCreated()
c.RplMyInfo()
s.MOTD(c)
}
func (server *Server) MOTD(client *Client) {
@ -281,44 +287,6 @@ func (msg *ProxyCommand) HandleRegServer(server *Server) {
msg.Client().hostname = msg.hostname
}
func (msg *CapCommand) HandleRegServer(server *Server) {
client := msg.Client()
switch msg.subCommand {
case CAP_LS:
client.capState = CapNegotiating
client.Reply(RplCap(client, CAP_LS, SupportedCapabilities))
case CAP_LIST:
client.Reply(RplCap(client, CAP_LIST, client.capabilities))
case CAP_REQ:
client.capState = CapNegotiating
for capability := range msg.capabilities {
if !SupportedCapabilities[capability] {
client.Reply(RplCap(client, CAP_NAK, msg.capabilities))
return
}
}
for capability := range msg.capabilities {
client.capabilities[capability] = true
}
client.Reply(RplCap(client, CAP_ACK, msg.capabilities))
case CAP_CLEAR:
reply := RplCap(client, CAP_ACK, client.capabilities.DisableString())
client.capabilities = make(CapabilitySet)
client.Reply(reply)
case CAP_END:
client.capState = CapNegotiated
server.tryRegister(client)
default:
client.ErrInvalidCapCmd(msg.subCommand)
}
}
func (m *NickCommand) HandleRegServer(s *Server) {
client := m.Client()
if !client.authorized {
@ -369,7 +337,7 @@ func (msg *RFC2812UserCommand) HandleRegServer(server *Server) {
}
flags := msg.Flags()
if len(flags) > 0 {
for _, mode := range msg.Flags() {
for _, mode := range flags {
client.flags[mode] = true
}
client.RplUModeIs(client)
@ -521,58 +489,6 @@ func (msg *PrivMsgCommand) HandleServer(server *Server) {
}
}
func (m *ModeCommand) HandleServer(s *Server) {
client := m.Client()
target := s.clients.Get(m.nickname)
if target == nil {
client.ErrNoSuchNick(m.nickname)
return
}
if client != target && !client.flags[Operator] {
client.ErrUsersDontMatch()
return
}
changes := make(ModeChanges, 0, len(m.changes))
for _, change := range m.changes {
switch change.mode {
case Invisible, ServerNotice, WallOps:
switch change.op {
case Add:
if target.flags[change.mode] {
continue
}
target.flags[change.mode] = true
changes = append(changes, change)
case Remove:
if !target.flags[change.mode] {
continue
}
delete(target.flags, change.mode)
changes = append(changes, change)
}
case Operator, LocalOperator:
if change.op == Remove {
if !target.flags[change.mode] {
continue
}
delete(target.flags, change.mode)
changes = append(changes, change)
}
}
}
// Who should get these replies?
if len(changes) > 0 {
client.Reply(RplMode(client, target, changes))
}
}
func (client *Client) WhoisChannelsNames() []string {
chstrs := make([]string, len(client.channels))
index := 0
@ -609,17 +525,6 @@ func (m *WhoisCommand) HandleServer(server *Server) {
}
}
func (msg *ChannelModeCommand) HandleServer(server *Server) {
client := msg.Client()
channel := server.channels.Get(msg.channel)
if channel == nil {
client.ErrNoSuchChannel(msg.channel)
return
}
channel.Mode(client, msg.changes)
}
func whoChannel(client *Client, channel *Channel, friends ClientSet) {
for member := range channel.members {
if !client.flags[Invisible] || friends[client] {

View File

@ -3,7 +3,6 @@ package irc
import (
"bufio"
"io"
"log"
"net"
"strings"
)
@ -20,7 +19,7 @@ type Socket struct {
writer *bufio.Writer
}
func NewSocket(conn net.Conn, commands chan<- editableCommand) *Socket {
func NewSocket(conn net.Conn, commands chan<- Command) *Socket {
socket := &Socket{
conn: conn,
reader: bufio.NewReader(conn),
@ -38,12 +37,10 @@ func (socket *Socket) String() string {
func (socket *Socket) Close() {
socket.conn.Close()
if DEBUG_NET {
log.Printf("%s closed", socket)
}
Log.debug.Printf("%s closed", socket)
}
func (socket *Socket) readLines(commands chan<- editableCommand) {
func (socket *Socket) readLines(commands chan<- Command) {
commands <- &ProxyCommand{
hostname: AddrLookupHostname(socket.conn.RemoteAddr()),
}
@ -57,9 +54,7 @@ func (socket *Socket) readLines(commands chan<- editableCommand) {
if len(line) == 0 {
continue
}
if DEBUG_NET {
log.Printf("%s → %s", socket, line)
}
Log.debug.Printf("%s → %s", socket, line)
msg, err := ParseCommand(line)
if err != nil {
@ -87,16 +82,14 @@ func (socket *Socket) Write(line string) (err error) {
return
}
if DEBUG_NET {
log.Printf("%s ← %s", socket, line)
}
Log.debug.Printf("%s ← %s", socket, line)
return
}
func (socket *Socket) isError(err error, dir rune) bool {
if err != nil {
if DEBUG_NET && (err != io.EOF) {
log.Printf("%s %c error: %s", socket, dir, err)
if err != io.EOF {
Log.debug.Printf("%s %c error: %s", socket, dir, err)
}
return true
}

View File

@ -9,83 +9,6 @@ import (
// simple types
//
type CapSubCommand string
type Capability string
func (capability Capability) String() string {
return string(capability)
}
type CapModifier rune
func (mod CapModifier) String() string {
return string(mod)
}
type CapState uint
type CapabilitySet map[Capability]bool
func (set CapabilitySet) String() string {
strs := make([]string, len(set))
index := 0
for capability := range set {
strs[index] = string(capability)
index += 1
}
return strings.Join(strs, " ")
}
func (set CapabilitySet) DisableString() string {
parts := make([]string, len(set))
index := 0
for capability := range set {
parts[index] = Disable.String() + capability.String()
index += 1
}
return strings.Join(parts, " ")
}
// add, remove, list modes
type ModeOp rune
func (op ModeOp) String() string {
return string(op)
}
// user mode flags
type UserMode rune
func (mode UserMode) String() string {
return string(mode)
}
type Phase uint
type ReplyCode interface {
String() string
}
type StringCode Name
func (code StringCode) String() string {
return string(code)
}
type NumericCode uint
func (code NumericCode) String() string {
return fmt.Sprintf("%03d", code)
}
// channel mode flags
type ChannelMode rune
func (mode ChannelMode) String() string {
return string(mode)
}
type ChannelNameMap map[Name]*Channel
func (channels ChannelNameMap) Get(name Name) *Channel {
@ -181,31 +104,7 @@ func (channels ChannelSet) First() *Channel {
// interfaces
//
type Identifier interface {
type Identifiable interface {
Id() Name
Nick() Name
}
type Replier interface {
Reply(...string)
}
type Command interface {
Code() StringCode
Client() *Client
}
type ServerCommand interface {
Command
HandleServer(*Server)
}
type AuthServerCommand interface {
Command
HandleAuthServer(*Server)
}
type RegServerCommand interface {
Command
HandleRegServer(*Server)
}