├── .gitignore ├── LICENSE ├── README.md ├── ergonomadic.go ├── ergonomadic.yaml └── irc ├── capability.go ├── channel.go ├── client.go ├── client_lookup_set.go ├── commands.go ├── config.go ├── constants.go ├── database.go ├── debug.go ├── logging.go ├── modes.go ├── net.go ├── nickname.go ├── password.go ├── reply.go ├── server.go ├── socket.go ├── strings.go ├── theater.go ├── types.go ├── websocket.go └── whowas.go /.gitignore: -------------------------------------------------------------------------------- 1 | /ircd.* 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Jeremy Latt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Development of ergonomadic is discontinued. 2 | 3 | It lives on as: https://github.com/oragono/oragono . 4 | -------------------------------------------------------------------------------- /ergonomadic.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "syscall" 7 | 8 | "github.com/docopt/docopt-go" 9 | "github.com/edmund-huber/ergonomadic/irc" 10 | "golang.org/x/crypto/ssh/terminal" 11 | ) 12 | 13 | func main() { 14 | version := irc.SEM_VER 15 | usage := `ergonomadic. 16 | Usage: 17 | ergonomadic initdb [--conf ] 18 | ergonomadic upgradedb [--conf ] 19 | ergonomadic genpasswd [--conf ] 20 | ergonomadic run [--conf ] 21 | ergonomadic -h | --help 22 | ergonomadic --version 23 | Options: 24 | --conf Configuration file to use [default: ircd.yaml]. 25 | -h --help Show this screen. 26 | --version Show version.` 27 | 28 | arguments, _ := docopt.Parse(usage, nil, true, version, false) 29 | 30 | configfile := arguments["--conf"].(string) 31 | config, err := irc.LoadConfig(configfile) 32 | if err != nil { 33 | log.Fatal("Config file did not load successfully:", err.Error()) 34 | } 35 | 36 | if arguments["genpasswd"].(bool) { 37 | fmt.Print("Enter Password: ") 38 | bytePassword, err := terminal.ReadPassword(int(syscall.Stdin)) 39 | if err != nil { 40 | log.Fatal("Error reading password:", err.Error()) 41 | } 42 | password := string(bytePassword) 43 | encoded, err := irc.GenerateEncodedPassword(password) 44 | if err != nil { 45 | log.Fatalln("encoding error:", err) 46 | } 47 | fmt.Print("\n") 48 | fmt.Println(encoded) 49 | } else if arguments["initdb"].(bool) { 50 | irc.InitDB(config.Server.Database) 51 | log.Println("database initialized: ", config.Server.Database) 52 | } else if arguments["upgradedb"].(bool) { 53 | irc.UpgradeDB(config.Server.Database) 54 | log.Println("database upgraded: ", config.Server.Database) 55 | } else if arguments["run"].(bool) { 56 | irc.Log.SetLevel(config.Server.Log) 57 | server := irc.NewServer(config) 58 | log.Println(irc.SEM_VER, "running") 59 | defer log.Println(irc.SEM_VER, "exiting") 60 | server.Run() 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /ergonomadic.yaml: -------------------------------------------------------------------------------- 1 | # ergonomadic IRCd config 2 | server: 3 | # server name 4 | name: ergonomadic.test 5 | 6 | # database filename (sqlite db) 7 | database: ircd.db 8 | 9 | # addresses to listen on 10 | listen: 11 | - ":6667" 12 | - "127.0.0.1:6668" 13 | - "[::1]:6668" 14 | 15 | # websocket listening port 16 | wslisten: ":8080" 17 | 18 | # password to login to the server 19 | # generated using "ergonomadic genpasswd" 20 | #password: "" 21 | 22 | # log level, one of error, warn, info, debug 23 | log: debug 24 | 25 | # motd filename 26 | motd: ircd.motd 27 | 28 | # ircd operators 29 | operator: 30 | # operator named 'dan' 31 | dan: 32 | # password to login with /OPER command 33 | # generated using "ergonomadic genpasswd" 34 | password: JDJhJDA0JE1vZmwxZC9YTXBhZ3RWT2xBbkNwZnV3R2N6VFUwQUI0RUJRVXRBRHliZVVoa0VYMnlIaGsu 35 | -------------------------------------------------------------------------------- /irc/capability.go: -------------------------------------------------------------------------------- 1 | package irc 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | type CapSubCommand string 8 | 9 | const ( 10 | CAP_LS CapSubCommand = "LS" 11 | CAP_LIST CapSubCommand = "LIST" 12 | CAP_REQ CapSubCommand = "REQ" 13 | CAP_ACK CapSubCommand = "ACK" 14 | CAP_NAK CapSubCommand = "NAK" 15 | CAP_CLEAR CapSubCommand = "CLEAR" 16 | CAP_END CapSubCommand = "END" 17 | ) 18 | 19 | // Capabilities are optional features a client may request from a server. 20 | type Capability string 21 | 22 | const ( 23 | MultiPrefix Capability = "multi-prefix" 24 | SASL Capability = "sasl" 25 | ) 26 | 27 | var ( 28 | SupportedCapabilities = CapabilitySet{ 29 | MultiPrefix: true, 30 | } 31 | ) 32 | 33 | func (capability Capability) String() string { 34 | return string(capability) 35 | } 36 | 37 | // CapModifiers are indicators showing the state of a capability after a REQ or 38 | // ACK. 39 | type CapModifier rune 40 | 41 | const ( 42 | Ack CapModifier = '~' 43 | Disable CapModifier = '-' 44 | Sticky CapModifier = '=' 45 | ) 46 | 47 | func (mod CapModifier) String() string { 48 | return string(mod) 49 | } 50 | 51 | type CapState uint 52 | 53 | const ( 54 | CapNone CapState = iota 55 | CapNegotiating CapState = iota 56 | CapNegotiated CapState = iota 57 | ) 58 | 59 | type CapabilitySet map[Capability]bool 60 | 61 | func (set CapabilitySet) String() string { 62 | strs := make([]string, len(set)) 63 | index := 0 64 | for capability := range set { 65 | strs[index] = string(capability) 66 | index += 1 67 | } 68 | return strings.Join(strs, " ") 69 | } 70 | 71 | func (set CapabilitySet) DisableString() string { 72 | parts := make([]string, len(set)) 73 | index := 0 74 | for capability := range set { 75 | parts[index] = Disable.String() + capability.String() 76 | index += 1 77 | } 78 | return strings.Join(parts, " ") 79 | } 80 | 81 | func (msg *CapCommand) HandleRegServer(server *Server) { 82 | client := msg.Client() 83 | 84 | switch msg.subCommand { 85 | case CAP_LS: 86 | client.capState = CapNegotiating 87 | client.Reply(RplCap(client, CAP_LS, SupportedCapabilities)) 88 | 89 | case CAP_LIST: 90 | client.Reply(RplCap(client, CAP_LIST, client.capabilities)) 91 | 92 | case CAP_REQ: 93 | for capability := range msg.capabilities { 94 | if !SupportedCapabilities[capability] { 95 | client.Reply(RplCap(client, CAP_NAK, msg.capabilities)) 96 | return 97 | } 98 | } 99 | for capability := range msg.capabilities { 100 | client.capabilities[capability] = true 101 | } 102 | client.Reply(RplCap(client, CAP_ACK, msg.capabilities)) 103 | 104 | case CAP_CLEAR: 105 | reply := RplCap(client, CAP_ACK, client.capabilities.DisableString()) 106 | client.capabilities = make(CapabilitySet) 107 | client.Reply(reply) 108 | 109 | case CAP_END: 110 | client.capState = CapNegotiated 111 | server.tryRegister(client) 112 | 113 | default: 114 | client.ErrInvalidCapCmd(msg.subCommand) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /irc/channel.go: -------------------------------------------------------------------------------- 1 | package irc 2 | 3 | import ( 4 | "log" 5 | "strconv" 6 | ) 7 | 8 | type Channel struct { 9 | flags ChannelModeSet 10 | lists map[ChannelMode]*UserMaskSet 11 | key Text 12 | members MemberSet 13 | name Name 14 | server *Server 15 | topic Text 16 | userLimit uint64 17 | } 18 | 19 | // NewChannel creates a new channel from a `Server` and a `name` 20 | // string, which must be unique on the server. 21 | func NewChannel(s *Server, name Name) *Channel { 22 | channel := &Channel{ 23 | flags: make(ChannelModeSet), 24 | lists: map[ChannelMode]*UserMaskSet{ 25 | BanMask: NewUserMaskSet(), 26 | ExceptMask: NewUserMaskSet(), 27 | InviteMask: NewUserMaskSet(), 28 | }, 29 | members: make(MemberSet), 30 | name: name, 31 | server: s, 32 | } 33 | 34 | s.channels.Add(channel) 35 | 36 | return channel 37 | } 38 | 39 | func (channel *Channel) IsEmpty() bool { 40 | return len(channel.members) == 0 41 | } 42 | 43 | func (channel *Channel) Names(client *Client) { 44 | client.RplNamReply(channel) 45 | client.RplEndOfNames(channel) 46 | } 47 | 48 | func (channel *Channel) ClientIsOperator(client *Client) bool { 49 | return client.flags[Operator] || channel.members.HasMode(client, ChannelOperator) 50 | } 51 | 52 | func (channel *Channel) Nicks(target *Client) []string { 53 | isMultiPrefix := (target != nil) && target.capabilities[MultiPrefix] 54 | nicks := make([]string, len(channel.members)) 55 | i := 0 56 | for client, modes := range channel.members { 57 | if isMultiPrefix { 58 | if modes[ChannelOperator] { 59 | nicks[i] += "@" 60 | } 61 | if modes[Voice] { 62 | nicks[i] += "+" 63 | } 64 | } else { 65 | if modes[ChannelOperator] { 66 | nicks[i] += "@" 67 | } else if modes[Voice] { 68 | nicks[i] += "+" 69 | } 70 | } 71 | nicks[i] += client.Nick().String() 72 | i += 1 73 | } 74 | return nicks 75 | } 76 | 77 | func (channel *Channel) Id() Name { 78 | return channel.name 79 | } 80 | 81 | func (channel *Channel) Nick() Name { 82 | return channel.name 83 | } 84 | 85 | func (channel *Channel) String() string { 86 | return channel.Id().String() 87 | } 88 | 89 | // 90 | func (channel *Channel) ModeString(client *Client) (str string) { 91 | isMember := client.flags[Operator] || channel.members.Has(client) 92 | showKey := isMember && (channel.key != "") 93 | showUserLimit := channel.userLimit > 0 94 | 95 | // flags with args 96 | if showKey { 97 | str += Key.String() 98 | } 99 | if showUserLimit { 100 | str += UserLimit.String() 101 | } 102 | 103 | // flags 104 | for mode := range channel.flags { 105 | str += mode.String() 106 | } 107 | 108 | str = "+" + str 109 | 110 | // args for flags with args: The order must match above to keep 111 | // positional arguments in place. 112 | if showKey { 113 | str += " " + channel.key.String() 114 | } 115 | if showUserLimit { 116 | str += " " + strconv.FormatUint(channel.userLimit, 10) 117 | } 118 | 119 | return 120 | } 121 | 122 | func (channel *Channel) IsFull() bool { 123 | return (channel.userLimit > 0) && 124 | (uint64(len(channel.members)) >= channel.userLimit) 125 | } 126 | 127 | func (channel *Channel) CheckKey(key Text) bool { 128 | return (channel.key == "") || (channel.key == key) 129 | } 130 | 131 | func (channel *Channel) Join(client *Client, key Text) { 132 | if channel.members.Has(client) { 133 | // already joined, no message? 134 | return 135 | } 136 | 137 | if channel.IsFull() { 138 | client.ErrChannelIsFull(channel) 139 | return 140 | } 141 | 142 | if !channel.CheckKey(key) { 143 | client.ErrBadChannelKey(channel) 144 | return 145 | } 146 | 147 | isInvited := channel.lists[InviteMask].Match(client.UserHost()) 148 | if channel.flags[InviteOnly] && !isInvited { 149 | client.ErrInviteOnlyChan(channel) 150 | return 151 | } 152 | 153 | if channel.lists[BanMask].Match(client.UserHost()) && 154 | !isInvited && 155 | !channel.lists[ExceptMask].Match(client.UserHost()) { 156 | client.ErrBannedFromChan(channel) 157 | return 158 | } 159 | 160 | client.channels.Add(channel) 161 | channel.members.Add(client) 162 | if !channel.flags[Persistent] && (len(channel.members) == 1) { 163 | channel.members[client][ChannelCreator] = true 164 | channel.members[client][ChannelOperator] = true 165 | } 166 | 167 | reply := RplJoin(client, channel) 168 | for member := range channel.members { 169 | member.Reply(reply) 170 | } 171 | channel.GetTopic(client) 172 | channel.Names(client) 173 | } 174 | 175 | func (channel *Channel) Part(client *Client, message Text) { 176 | if !channel.members.Has(client) { 177 | client.ErrNotOnChannel(channel) 178 | return 179 | } 180 | 181 | reply := RplPart(client, channel, message) 182 | for member := range channel.members { 183 | member.Reply(reply) 184 | } 185 | channel.Quit(client) 186 | } 187 | 188 | func (channel *Channel) GetTopic(client *Client) { 189 | if !channel.members.Has(client) { 190 | client.ErrNotOnChannel(channel) 191 | return 192 | } 193 | 194 | if channel.topic == "" { 195 | // clients appear not to expect this 196 | //replier.Reply(RplNoTopic(channel)) 197 | return 198 | } 199 | 200 | client.RplTopic(channel) 201 | } 202 | 203 | func (channel *Channel) SetTopic(client *Client, topic Text) { 204 | if !(client.flags[Operator] || channel.members.Has(client)) { 205 | client.ErrNotOnChannel(channel) 206 | return 207 | } 208 | 209 | if channel.flags[OpOnlyTopic] && !channel.ClientIsOperator(client) { 210 | client.ErrChanOPrivIsNeeded(channel) 211 | return 212 | } 213 | 214 | channel.topic = topic 215 | 216 | reply := RplTopicMsg(client, channel) 217 | for member := range channel.members { 218 | member.Reply(reply) 219 | } 220 | 221 | if err := channel.Persist(); err != nil { 222 | log.Println("Channel.Persist:", channel, err) 223 | } 224 | } 225 | 226 | func (channel *Channel) CanSpeak(client *Client) bool { 227 | if client.flags[Operator] { 228 | return true 229 | } 230 | if channel.flags[NoOutside] && !channel.members.Has(client) { 231 | return false 232 | } 233 | if channel.flags[Moderated] && !(channel.members.HasMode(client, Voice) || 234 | channel.members.HasMode(client, ChannelOperator)) { 235 | return false 236 | } 237 | return true 238 | } 239 | 240 | func (channel *Channel) PrivMsg(client *Client, message Text) { 241 | if !channel.CanSpeak(client) { 242 | client.ErrCannotSendToChan(channel) 243 | return 244 | } 245 | reply := RplPrivMsg(client, channel, message) 246 | for member := range channel.members { 247 | if member == client { 248 | continue 249 | } 250 | member.Reply(reply) 251 | } 252 | } 253 | 254 | func (channel *Channel) applyModeFlag(client *Client, mode ChannelMode, 255 | op ModeOp) bool { 256 | if !channel.ClientIsOperator(client) { 257 | client.ErrChanOPrivIsNeeded(channel) 258 | return false 259 | } 260 | 261 | switch op { 262 | case Add: 263 | if channel.flags[mode] { 264 | return false 265 | } 266 | channel.flags[mode] = true 267 | return true 268 | 269 | case Remove: 270 | if !channel.flags[mode] { 271 | return false 272 | } 273 | delete(channel.flags, mode) 274 | return true 275 | } 276 | return false 277 | } 278 | 279 | func (channel *Channel) applyModeMember(client *Client, mode ChannelMode, 280 | op ModeOp, nick Name) bool { 281 | if !channel.ClientIsOperator(client) { 282 | client.ErrChanOPrivIsNeeded(channel) 283 | return false 284 | } 285 | 286 | if nick == "" { 287 | client.ErrNeedMoreParams("MODE") 288 | return false 289 | } 290 | 291 | target := channel.server.clients.Get(nick) 292 | if target == nil { 293 | client.ErrNoSuchNick(nick) 294 | return false 295 | } 296 | 297 | if !channel.members.Has(target) { 298 | client.ErrUserNotInChannel(channel, target) 299 | return false 300 | } 301 | 302 | switch op { 303 | case Add: 304 | if channel.members[target][mode] { 305 | return false 306 | } 307 | channel.members[target][mode] = true 308 | return true 309 | 310 | case Remove: 311 | if !channel.members[target][mode] { 312 | return false 313 | } 314 | channel.members[target][mode] = false 315 | return true 316 | } 317 | return false 318 | } 319 | 320 | func (channel *Channel) ShowMaskList(client *Client, mode ChannelMode) { 321 | for lmask := range channel.lists[mode].masks { 322 | client.RplMaskList(mode, channel, lmask) 323 | } 324 | client.RplEndOfMaskList(mode, channel) 325 | } 326 | 327 | func (channel *Channel) applyModeMask(client *Client, mode ChannelMode, op ModeOp, 328 | mask Name) bool { 329 | list := channel.lists[mode] 330 | if list == nil { 331 | // This should never happen, but better safe than panicky. 332 | return false 333 | } 334 | 335 | if (op == List) || (mask == "") { 336 | channel.ShowMaskList(client, mode) 337 | return false 338 | } 339 | 340 | if !channel.ClientIsOperator(client) { 341 | client.ErrChanOPrivIsNeeded(channel) 342 | return false 343 | } 344 | 345 | if op == Add { 346 | return list.Add(mask) 347 | } 348 | 349 | if op == Remove { 350 | return list.Remove(mask) 351 | } 352 | 353 | return false 354 | } 355 | 356 | func (channel *Channel) applyMode(client *Client, change *ChannelModeChange) bool { 357 | switch change.mode { 358 | case BanMask, ExceptMask, InviteMask: 359 | return channel.applyModeMask(client, change.mode, change.op, 360 | NewName(change.arg)) 361 | 362 | case InviteOnly, Moderated, NoOutside, OpOnlyTopic, Persistent, Private: 363 | return channel.applyModeFlag(client, change.mode, change.op) 364 | 365 | case Key: 366 | if !channel.ClientIsOperator(client) { 367 | client.ErrChanOPrivIsNeeded(channel) 368 | return false 369 | } 370 | 371 | switch change.op { 372 | case Add: 373 | if change.arg == "" { 374 | client.ErrNeedMoreParams("MODE") 375 | return false 376 | } 377 | key := NewText(change.arg) 378 | if key == channel.key { 379 | return false 380 | } 381 | 382 | channel.key = key 383 | return true 384 | 385 | case Remove: 386 | channel.key = "" 387 | return true 388 | } 389 | 390 | case UserLimit: 391 | limit, err := strconv.ParseUint(change.arg, 10, 64) 392 | if err != nil { 393 | client.ErrNeedMoreParams("MODE") 394 | return false 395 | } 396 | if (limit == 0) || (limit == channel.userLimit) { 397 | return false 398 | } 399 | 400 | channel.userLimit = limit 401 | return true 402 | 403 | case ChannelOperator, Voice: 404 | return channel.applyModeMember(client, change.mode, change.op, 405 | NewName(change.arg)) 406 | 407 | default: 408 | client.ErrUnknownMode(change.mode, channel) 409 | } 410 | return false 411 | } 412 | 413 | func (channel *Channel) Mode(client *Client, changes ChannelModeChanges) { 414 | if len(changes) == 0 { 415 | client.RplChannelModeIs(channel) 416 | return 417 | } 418 | 419 | applied := make(ChannelModeChanges, 0) 420 | for _, change := range changes { 421 | if channel.applyMode(client, change) { 422 | applied = append(applied, change) 423 | } 424 | } 425 | 426 | if len(applied) > 0 { 427 | reply := RplChannelMode(client, channel, applied) 428 | for member := range channel.members { 429 | member.Reply(reply) 430 | } 431 | 432 | if err := channel.Persist(); err != nil { 433 | log.Println("Channel.Persist:", channel, err) 434 | } 435 | } 436 | } 437 | 438 | func (channel *Channel) Persist() (err error) { 439 | if channel.flags[Persistent] { 440 | _, err = channel.server.db.Exec(` 441 | INSERT OR REPLACE INTO channel 442 | (name, flags, key, topic, user_limit, ban_list, except_list, 443 | invite_list) 444 | VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, 445 | channel.name.String(), channel.flags.String(), channel.key.String(), 446 | channel.topic.String(), channel.userLimit, channel.lists[BanMask].String(), 447 | channel.lists[ExceptMask].String(), channel.lists[InviteMask].String()) 448 | } else { 449 | _, err = channel.server.db.Exec(` 450 | DELETE FROM channel WHERE name = ?`, channel.name.String()) 451 | } 452 | return 453 | } 454 | 455 | func (channel *Channel) Notice(client *Client, message Text) { 456 | if !channel.CanSpeak(client) { 457 | client.ErrCannotSendToChan(channel) 458 | return 459 | } 460 | reply := RplNotice(client, channel, message) 461 | for member := range channel.members { 462 | if member == client { 463 | continue 464 | } 465 | member.Reply(reply) 466 | } 467 | } 468 | 469 | func (channel *Channel) Quit(client *Client) { 470 | channel.members.Remove(client) 471 | client.channels.Remove(channel) 472 | 473 | if !channel.flags[Persistent] && channel.IsEmpty() { 474 | channel.server.channels.Remove(channel) 475 | } 476 | } 477 | 478 | func (channel *Channel) Kick(client *Client, target *Client, comment Text) { 479 | if !(client.flags[Operator] || channel.members.Has(client)) { 480 | client.ErrNotOnChannel(channel) 481 | return 482 | } 483 | if !channel.ClientIsOperator(client) { 484 | client.ErrChanOPrivIsNeeded(channel) 485 | return 486 | } 487 | if !channel.members.Has(target) { 488 | client.ErrUserNotInChannel(channel, target) 489 | return 490 | } 491 | 492 | reply := RplKick(channel, client, target, comment) 493 | for member := range channel.members { 494 | member.Reply(reply) 495 | } 496 | channel.Quit(target) 497 | } 498 | 499 | func (channel *Channel) Invite(invitee *Client, inviter *Client) { 500 | if channel.flags[InviteOnly] && !channel.ClientIsOperator(inviter) { 501 | inviter.ErrChanOPrivIsNeeded(channel) 502 | return 503 | } 504 | 505 | if !channel.members.Has(inviter) { 506 | inviter.ErrNotOnChannel(channel) 507 | return 508 | } 509 | 510 | if channel.flags[InviteOnly] { 511 | channel.lists[InviteMask].Add(invitee.UserHost()) 512 | if err := channel.Persist(); err != nil { 513 | log.Println("Channel.Persist:", channel, err) 514 | } 515 | } 516 | 517 | inviter.RplInviting(invitee, channel.name) 518 | invitee.Reply(RplInviteMsg(inviter, invitee, channel.name)) 519 | if invitee.flags[Away] { 520 | inviter.RplAway(invitee) 521 | } 522 | } 523 | -------------------------------------------------------------------------------- /irc/client.go: -------------------------------------------------------------------------------- 1 | package irc 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "time" 7 | ) 8 | 9 | const ( 10 | IDLE_TIMEOUT = time.Minute // how long before a client is considered idle 11 | QUIT_TIMEOUT = time.Minute // how long after idle before a client is kicked 12 | ) 13 | 14 | type Client struct { 15 | atime time.Time 16 | authorized bool 17 | awayMessage Text 18 | capabilities CapabilitySet 19 | capState CapState 20 | channels ChannelSet 21 | ctime time.Time 22 | flags map[UserMode]bool 23 | hasQuit bool 24 | hops uint 25 | hostname Name 26 | idleTimer *time.Timer 27 | nick Name 28 | quitTimer *time.Timer 29 | realname Text 30 | registered bool 31 | server *Server 32 | socket *Socket 33 | username Name 34 | } 35 | 36 | func NewClient(server *Server, conn net.Conn) *Client { 37 | now := time.Now() 38 | client := &Client{ 39 | atime: now, 40 | authorized: server.password == nil, 41 | capState: CapNone, 42 | capabilities: make(CapabilitySet), 43 | channels: make(ChannelSet), 44 | ctime: now, 45 | flags: make(map[UserMode]bool), 46 | server: server, 47 | socket: NewSocket(conn), 48 | } 49 | client.Touch() 50 | go client.run() 51 | 52 | return client 53 | } 54 | 55 | // 56 | // command goroutine 57 | // 58 | 59 | func (client *Client) run() { 60 | var command Command 61 | var err error 62 | var line string 63 | 64 | // Set the hostname for this client. The client may later send a PROXY 65 | // command from stunnel that sets the hostname to something more accurate. 66 | client.send(NewProxyCommand(AddrLookupHostname( 67 | client.socket.conn.RemoteAddr()))) 68 | 69 | for err == nil { 70 | if line, err = client.socket.Read(); err != nil { 71 | command = NewQuitCommand("connection closed") 72 | 73 | } else if command, err = ParseCommand(line); err != nil { 74 | switch err { 75 | case ErrParseCommand: 76 | //TODO(dan): use the real failed numeric for this (400) 77 | client.Reply(RplNotice(client.server, client, 78 | NewText("failed to parse command"))) 79 | 80 | case NotEnoughArgsError: 81 | // TODO 82 | } 83 | // so the read loop will continue 84 | err = nil 85 | continue 86 | 87 | } else if checkPass, ok := command.(checkPasswordCommand); ok { 88 | checkPass.LoadPassword(client.server) 89 | // Block the client thread while handling a potentially expensive 90 | // password bcrypt operation. Since the server is single-threaded 91 | // for commands, we don't want the server to perform the bcrypt, 92 | // blocking anyone else from sending commands until it 93 | // completes. This could be a form of DoS if handled naively. 94 | checkPass.CheckPassword() 95 | } 96 | 97 | client.send(command) 98 | } 99 | } 100 | 101 | func (client *Client) send(command Command) { 102 | command.SetClient(client) 103 | client.server.commands <- command 104 | } 105 | 106 | // quit timer goroutine 107 | 108 | func (client *Client) connectionTimeout() { 109 | client.send(NewQuitCommand("connection timeout")) 110 | } 111 | 112 | // 113 | // idle timer goroutine 114 | // 115 | 116 | func (client *Client) connectionIdle() { 117 | client.server.idle <- client 118 | } 119 | 120 | // 121 | // server goroutine 122 | // 123 | 124 | func (client *Client) Active() { 125 | client.atime = time.Now() 126 | } 127 | 128 | func (client *Client) Touch() { 129 | if client.quitTimer != nil { 130 | client.quitTimer.Stop() 131 | } 132 | 133 | if client.idleTimer == nil { 134 | client.idleTimer = time.AfterFunc(IDLE_TIMEOUT, client.connectionIdle) 135 | } else { 136 | client.idleTimer.Reset(IDLE_TIMEOUT) 137 | } 138 | } 139 | 140 | func (client *Client) Idle() { 141 | client.Reply(RplPing(client.server)) 142 | 143 | if client.quitTimer == nil { 144 | client.quitTimer = time.AfterFunc(QUIT_TIMEOUT, client.connectionTimeout) 145 | } else { 146 | client.quitTimer.Reset(QUIT_TIMEOUT) 147 | } 148 | } 149 | 150 | func (client *Client) Register() { 151 | if client.registered { 152 | return 153 | } 154 | client.registered = true 155 | client.Touch() 156 | } 157 | 158 | func (client *Client) destroy() { 159 | // clean up channels 160 | 161 | for channel := range client.channels { 162 | channel.Quit(client) 163 | } 164 | 165 | // clean up server 166 | 167 | client.server.clients.Remove(client) 168 | 169 | // clean up self 170 | 171 | if client.idleTimer != nil { 172 | client.idleTimer.Stop() 173 | } 174 | if client.quitTimer != nil { 175 | client.quitTimer.Stop() 176 | } 177 | 178 | client.socket.Close() 179 | 180 | Log.debug.Printf("%s: destroyed", client) 181 | } 182 | 183 | func (client *Client) IdleTime() time.Duration { 184 | return time.Since(client.atime) 185 | } 186 | 187 | func (client *Client) SignonTime() int64 { 188 | return client.ctime.Unix() 189 | } 190 | 191 | func (client *Client) IdleSeconds() uint64 { 192 | return uint64(client.IdleTime().Seconds()) 193 | } 194 | 195 | func (client *Client) HasNick() bool { 196 | return client.nick != "" 197 | } 198 | 199 | func (client *Client) HasUsername() bool { 200 | return client.username != "" 201 | } 202 | 203 | // 204 | func (c *Client) ModeString() (str string) { 205 | for flag := range c.flags { 206 | str += flag.String() 207 | } 208 | 209 | if len(str) > 0 { 210 | str = "+" + str 211 | } 212 | return 213 | } 214 | 215 | func (c *Client) UserHost() Name { 216 | username := "*" 217 | if c.HasUsername() { 218 | username = c.username.String() 219 | } 220 | return Name(fmt.Sprintf("%s!%s@%s", c.Nick(), username, c.hostname)) 221 | } 222 | 223 | func (c *Client) Nick() Name { 224 | if c.HasNick() { 225 | return c.nick 226 | } 227 | return Name("*") 228 | } 229 | 230 | func (c *Client) Id() Name { 231 | return c.UserHost() 232 | } 233 | 234 | func (c *Client) String() string { 235 | return c.Id().String() 236 | } 237 | 238 | func (client *Client) Friends() ClientSet { 239 | friends := make(ClientSet) 240 | friends.Add(client) 241 | for channel := range client.channels { 242 | for member := range channel.members { 243 | friends.Add(member) 244 | } 245 | } 246 | return friends 247 | } 248 | 249 | func (client *Client) SetNickname(nickname Name) { 250 | if client.HasNick() { 251 | Log.error.Printf("%s nickname already set!", client) 252 | return 253 | } 254 | client.nick = nickname 255 | client.server.clients.Add(client) 256 | } 257 | 258 | func (client *Client) ChangeNickname(nickname Name) { 259 | // Make reply before changing nick to capture original source id. 260 | reply := RplNick(client, nickname) 261 | client.server.clients.Remove(client) 262 | client.server.whoWas.Append(client) 263 | client.nick = nickname 264 | client.server.clients.Add(client) 265 | for friend := range client.Friends() { 266 | friend.Reply(reply) 267 | } 268 | } 269 | 270 | func (client *Client) Reply(reply string) error { 271 | return client.socket.Write(reply) 272 | } 273 | 274 | func (client *Client) Quit(message Text) { 275 | if client.hasQuit { 276 | return 277 | } 278 | 279 | client.hasQuit = true 280 | client.Reply(RplError("quit")) 281 | client.server.whoWas.Append(client) 282 | friends := client.Friends() 283 | friends.Remove(client) 284 | client.destroy() 285 | 286 | if len(friends) > 0 { 287 | reply := RplQuit(client, message) 288 | for friend := range friends { 289 | friend.Reply(reply) 290 | } 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /irc/client_lookup_set.go: -------------------------------------------------------------------------------- 1 | package irc 2 | 3 | import ( 4 | "database/sql" 5 | "errors" 6 | "log" 7 | "regexp" 8 | "strings" 9 | ) 10 | 11 | var ( 12 | ErrNickMissing = errors.New("nick missing") 13 | ErrNicknameInUse = errors.New("nickname in use") 14 | ErrNicknameMismatch = errors.New("nickname mismatch") 15 | wildMaskExpr = regexp.MustCompile(`\*|\?`) 16 | likeQuoter = strings.NewReplacer( 17 | `\`, `\\`, 18 | `%`, `\%`, 19 | `_`, `\_`, 20 | `*`, `%`, 21 | `?`, `_`) 22 | ) 23 | 24 | func HasWildcards(mask string) bool { 25 | return wildMaskExpr.MatchString(mask) 26 | } 27 | 28 | func ExpandUserHost(userhost Name) (expanded Name) { 29 | expanded = userhost 30 | // fill in missing wildcards for nicks 31 | if !strings.Contains(expanded.String(), "!") { 32 | expanded += "!*" 33 | } 34 | if !strings.Contains(expanded.String(), "@") { 35 | expanded += "@*" 36 | } 37 | return 38 | } 39 | 40 | func QuoteLike(userhost Name) string { 41 | return likeQuoter.Replace(userhost.String()) 42 | } 43 | 44 | type ClientLookupSet struct { 45 | byNick map[Name]*Client 46 | db *ClientDB 47 | } 48 | 49 | func NewClientLookupSet() *ClientLookupSet { 50 | return &ClientLookupSet{ 51 | byNick: make(map[Name]*Client), 52 | db: NewClientDB(), 53 | } 54 | } 55 | 56 | func (clients *ClientLookupSet) Get(nick Name) *Client { 57 | return clients.byNick[nick.ToLower()] 58 | } 59 | 60 | func (clients *ClientLookupSet) Add(client *Client) error { 61 | if !client.HasNick() { 62 | return ErrNickMissing 63 | } 64 | if clients.Get(client.nick) != nil { 65 | return ErrNicknameInUse 66 | } 67 | clients.byNick[client.Nick().ToLower()] = client 68 | clients.db.Add(client) 69 | return nil 70 | } 71 | 72 | func (clients *ClientLookupSet) Remove(client *Client) error { 73 | if !client.HasNick() { 74 | return ErrNickMissing 75 | } 76 | if clients.Get(client.nick) != client { 77 | return ErrNicknameMismatch 78 | } 79 | delete(clients.byNick, client.nick.ToLower()) 80 | clients.db.Remove(client) 81 | return nil 82 | } 83 | 84 | func (clients *ClientLookupSet) FindAll(userhost Name) (set ClientSet) { 85 | userhost = ExpandUserHost(userhost) 86 | set = make(ClientSet) 87 | rows, err := clients.db.db.Query( 88 | `SELECT nickname FROM client WHERE userhost LIKE ? ESCAPE '\'`, 89 | QuoteLike(userhost)) 90 | if err != nil { 91 | Log.error.Println("ClientLookupSet.FindAll.Query:", err) 92 | return 93 | } 94 | for rows.Next() { 95 | var sqlNickname string 96 | err := rows.Scan(&sqlNickname) 97 | if err != nil { 98 | Log.error.Println("ClientLookupSet.FindAll.Scan:", err) 99 | return 100 | } 101 | nickname := Name(sqlNickname) 102 | client := clients.Get(nickname) 103 | if client == nil { 104 | Log.error.Println("ClientLookupSet.FindAll: missing client:", nickname) 105 | continue 106 | } 107 | set.Add(client) 108 | } 109 | return 110 | } 111 | 112 | func (clients *ClientLookupSet) Find(userhost Name) *Client { 113 | userhost = ExpandUserHost(userhost) 114 | row := clients.db.db.QueryRow( 115 | `SELECT nickname FROM client WHERE userhost LIKE ? ESCAPE '\' LIMIT 1`, 116 | QuoteLike(userhost)) 117 | var nickname Name 118 | err := row.Scan(&nickname) 119 | if err != nil { 120 | Log.error.Println("ClientLookupSet.Find:", err) 121 | return nil 122 | } 123 | return clients.Get(nickname) 124 | } 125 | 126 | // 127 | // client db 128 | // 129 | 130 | type ClientDB struct { 131 | db *sql.DB 132 | } 133 | 134 | func NewClientDB() *ClientDB { 135 | db := &ClientDB{ 136 | db: OpenDB(":memory:"), 137 | } 138 | stmts := []string{ 139 | `CREATE TABLE client ( 140 | nickname TEXT NOT NULL COLLATE NOCASE UNIQUE, 141 | userhost TEXT NOT NULL COLLATE NOCASE, 142 | UNIQUE (nickname, userhost) ON CONFLICT REPLACE)`, 143 | `CREATE UNIQUE INDEX idx_nick ON client (nickname COLLATE NOCASE)`, 144 | `CREATE UNIQUE INDEX idx_uh ON client (userhost COLLATE NOCASE)`, 145 | } 146 | for _, stmt := range stmts { 147 | _, err := db.db.Exec(stmt) 148 | if err != nil { 149 | log.Fatal("NewClientDB: ", stmt, err) 150 | } 151 | } 152 | return db 153 | } 154 | 155 | func (db *ClientDB) Add(client *Client) { 156 | _, err := db.db.Exec(`INSERT INTO client (nickname, userhost) VALUES (?, ?)`, 157 | client.Nick().String(), client.UserHost().String()) 158 | if err != nil { 159 | Log.error.Println("ClientDB.Add:", err) 160 | } 161 | } 162 | 163 | func (db *ClientDB) Remove(client *Client) { 164 | _, err := db.db.Exec(`DELETE FROM client WHERE nickname = ?`, 165 | client.Nick().String()) 166 | if err != nil { 167 | Log.error.Println("ClientDB.Remove:", err) 168 | } 169 | } 170 | 171 | // 172 | // usermask to regexp 173 | // 174 | 175 | type UserMaskSet struct { 176 | masks map[Name]bool 177 | regexp *regexp.Regexp 178 | } 179 | 180 | func NewUserMaskSet() *UserMaskSet { 181 | return &UserMaskSet{ 182 | masks: make(map[Name]bool), 183 | } 184 | } 185 | 186 | func (set *UserMaskSet) Add(mask Name) bool { 187 | if set.masks[mask] { 188 | return false 189 | } 190 | set.masks[mask] = true 191 | set.setRegexp() 192 | return true 193 | } 194 | 195 | func (set *UserMaskSet) AddAll(masks []Name) (added bool) { 196 | for _, mask := range masks { 197 | if !added && !set.masks[mask] { 198 | added = true 199 | } 200 | set.masks[mask] = true 201 | } 202 | set.setRegexp() 203 | return 204 | } 205 | 206 | func (set *UserMaskSet) Remove(mask Name) bool { 207 | if !set.masks[mask] { 208 | return false 209 | } 210 | delete(set.masks, mask) 211 | set.setRegexp() 212 | return true 213 | } 214 | 215 | func (set *UserMaskSet) Match(userhost Name) bool { 216 | if set.regexp == nil { 217 | return false 218 | } 219 | return set.regexp.MatchString(userhost.String()) 220 | } 221 | 222 | func (set *UserMaskSet) String() string { 223 | masks := make([]string, len(set.masks)) 224 | index := 0 225 | for mask := range set.masks { 226 | masks[index] = mask.String() 227 | index += 1 228 | } 229 | return strings.Join(masks, " ") 230 | } 231 | 232 | // Generate a regular expression from the set of user mask 233 | // strings. Masks are split at the two types of wildcards, `*` and 234 | // `?`. All the pieces are meta-escaped. `*` is replaced with `.*`, 235 | // the regexp equivalent. Likewise, `?` is replaced with `.`. The 236 | // parts are re-joined and finally all masks are joined into a big 237 | // or-expression. 238 | func (set *UserMaskSet) setRegexp() { 239 | if len(set.masks) == 0 { 240 | set.regexp = nil 241 | return 242 | } 243 | 244 | maskExprs := make([]string, len(set.masks)) 245 | index := 0 246 | for mask := range set.masks { 247 | manyParts := strings.Split(mask.String(), "*") 248 | manyExprs := make([]string, len(manyParts)) 249 | for mindex, manyPart := range manyParts { 250 | oneParts := strings.Split(manyPart, "?") 251 | oneExprs := make([]string, len(oneParts)) 252 | for oindex, onePart := range oneParts { 253 | oneExprs[oindex] = regexp.QuoteMeta(onePart) 254 | } 255 | manyExprs[mindex] = strings.Join(oneExprs, ".") 256 | } 257 | maskExprs[index] = strings.Join(manyExprs, ".*") 258 | } 259 | expr := "^" + strings.Join(maskExprs, "|") + "$" 260 | set.regexp, _ = regexp.Compile(expr) 261 | } 262 | -------------------------------------------------------------------------------- /irc/commands.go: -------------------------------------------------------------------------------- 1 | package irc 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "regexp" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | type Command interface { 12 | Client() *Client 13 | Code() StringCode 14 | SetClient(*Client) 15 | SetCode(StringCode) 16 | } 17 | 18 | type checkPasswordCommand interface { 19 | LoadPassword(*Server) 20 | CheckPassword() 21 | } 22 | 23 | type parseCommandFunc func([]string) (Command, error) 24 | 25 | var ( 26 | NotEnoughArgsError = errors.New("not enough arguments") 27 | ErrParseCommand = errors.New("failed to parse message") 28 | parseCommandFuncs = map[StringCode]parseCommandFunc{ 29 | AWAY: ParseAwayCommand, 30 | CAP: ParseCapCommand, 31 | DEBUG: ParseDebugCommand, 32 | INVITE: ParseInviteCommand, 33 | ISON: ParseIsOnCommand, 34 | JOIN: ParseJoinCommand, 35 | KICK: ParseKickCommand, 36 | KILL: ParseKillCommand, 37 | LIST: ParseListCommand, 38 | MODE: ParseModeCommand, 39 | MOTD: ParseMOTDCommand, 40 | NAMES: ParseNamesCommand, 41 | NICK: ParseNickCommand, 42 | NOTICE: ParseNoticeCommand, 43 | ONICK: ParseOperNickCommand, 44 | OPER: ParseOperCommand, 45 | PART: ParsePartCommand, 46 | PASS: ParsePassCommand, 47 | PING: ParsePingCommand, 48 | PONG: ParsePongCommand, 49 | PRIVMSG: ParsePrivMsgCommand, 50 | PROXY: ParseProxyCommand, 51 | QUIT: ParseQuitCommand, 52 | THEATER: ParseTheaterCommand, // nonstandard 53 | TIME: ParseTimeCommand, 54 | TOPIC: ParseTopicCommand, 55 | USER: ParseUserCommand, 56 | VERSION: ParseVersionCommand, 57 | WHO: ParseWhoCommand, 58 | WHOIS: ParseWhoisCommand, 59 | WHOWAS: ParseWhoWasCommand, 60 | } 61 | ) 62 | 63 | type BaseCommand struct { 64 | client *Client 65 | code StringCode 66 | } 67 | 68 | func (command *BaseCommand) Client() *Client { 69 | return command.client 70 | } 71 | 72 | func (command *BaseCommand) SetClient(client *Client) { 73 | command.client = client 74 | } 75 | 76 | func (command *BaseCommand) Code() StringCode { 77 | return command.code 78 | } 79 | 80 | func (command *BaseCommand) SetCode(code StringCode) { 81 | command.code = code 82 | } 83 | 84 | func ParseCommand(line string) (cmd Command, err error) { 85 | code, args := ParseLine(line) 86 | constructor := parseCommandFuncs[code] 87 | if constructor == nil { 88 | cmd = ParseUnknownCommand(args) 89 | } else { 90 | cmd, err = constructor(args) 91 | } 92 | if cmd != nil { 93 | cmd.SetCode(code) 94 | } 95 | return 96 | } 97 | 98 | var ( 99 | spacesExpr = regexp.MustCompile(` +`) 100 | ) 101 | 102 | func splitArg(line string) (arg string, rest string) { 103 | parts := spacesExpr.Split(line, 2) 104 | if len(parts) > 0 { 105 | arg = parts[0] 106 | } 107 | if len(parts) > 1 { 108 | rest = parts[1] 109 | } 110 | return 111 | } 112 | 113 | func ParseLine(line string) (command StringCode, args []string) { 114 | args = make([]string, 0) 115 | if strings.HasPrefix(line, ":") { 116 | _, line = splitArg(line) 117 | } 118 | arg, line := splitArg(line) 119 | command = StringCode(NewName(strings.ToUpper(arg))) 120 | for len(line) > 0 { 121 | if strings.HasPrefix(line, ":") { 122 | args = append(args, line[len(":"):]) 123 | break 124 | } 125 | arg, line = splitArg(line) 126 | args = append(args, arg) 127 | } 128 | return 129 | } 130 | 131 | // [args...] 132 | 133 | type UnknownCommand struct { 134 | BaseCommand 135 | args []string 136 | } 137 | 138 | func ParseUnknownCommand(args []string) *UnknownCommand { 139 | return &UnknownCommand{ 140 | args: args, 141 | } 142 | } 143 | 144 | // PING [ ] 145 | 146 | type PingCommand struct { 147 | BaseCommand 148 | server Name 149 | server2 Name 150 | } 151 | 152 | func ParsePingCommand(args []string) (Command, error) { 153 | if len(args) < 1 { 154 | return nil, NotEnoughArgsError 155 | } 156 | msg := &PingCommand{ 157 | server: NewName(args[0]), 158 | } 159 | if len(args) > 1 { 160 | msg.server2 = NewName(args[1]) 161 | } 162 | return msg, nil 163 | } 164 | 165 | // PONG [ ] 166 | 167 | type PongCommand struct { 168 | BaseCommand 169 | server1 Name 170 | server2 Name 171 | } 172 | 173 | func ParsePongCommand(args []string) (Command, error) { 174 | if len(args) < 1 { 175 | return nil, NotEnoughArgsError 176 | } 177 | message := &PongCommand{ 178 | server1: NewName(args[0]), 179 | } 180 | if len(args) > 1 { 181 | message.server2 = NewName(args[1]) 182 | } 183 | return message, nil 184 | } 185 | 186 | // PASS 187 | 188 | type PassCommand struct { 189 | BaseCommand 190 | hash []byte 191 | password []byte 192 | err error 193 | } 194 | 195 | func (cmd *PassCommand) LoadPassword(server *Server) { 196 | cmd.hash = server.password 197 | } 198 | 199 | func (cmd *PassCommand) CheckPassword() { 200 | if cmd.hash == nil { 201 | return 202 | } 203 | cmd.err = ComparePassword(cmd.hash, cmd.password) 204 | } 205 | 206 | func ParsePassCommand(args []string) (Command, error) { 207 | if len(args) < 1 { 208 | return nil, NotEnoughArgsError 209 | } 210 | return &PassCommand{ 211 | password: []byte(args[0]), 212 | }, nil 213 | } 214 | 215 | // NICK 216 | 217 | func ParseNickCommand(args []string) (Command, error) { 218 | if len(args) != 1 { 219 | return nil, NotEnoughArgsError 220 | } 221 | return &NickCommand{ 222 | nickname: NewName(args[0]), 223 | }, nil 224 | } 225 | 226 | type UserCommand struct { 227 | BaseCommand 228 | username Name 229 | realname Text 230 | } 231 | 232 | // USER 233 | type RFC1459UserCommand struct { 234 | UserCommand 235 | hostname Name 236 | servername Name 237 | } 238 | 239 | // USER 240 | type RFC2812UserCommand struct { 241 | UserCommand 242 | mode uint8 243 | unused string 244 | } 245 | 246 | func (cmd *RFC2812UserCommand) Flags() []UserMode { 247 | flags := make([]UserMode, 0) 248 | if (cmd.mode & 4) == 4 { 249 | flags = append(flags, WallOps) 250 | } 251 | if (cmd.mode & 8) == 8 { 252 | flags = append(flags, Invisible) 253 | } 254 | return flags 255 | } 256 | 257 | func ParseUserCommand(args []string) (Command, error) { 258 | if len(args) != 4 { 259 | return nil, NotEnoughArgsError 260 | } 261 | mode, err := strconv.ParseUint(args[1], 10, 8) 262 | if err == nil { 263 | msg := &RFC2812UserCommand{ 264 | mode: uint8(mode), 265 | unused: args[2], 266 | } 267 | msg.username = NewName(args[0]) 268 | msg.realname = NewText(args[3]) 269 | return msg, nil 270 | } 271 | 272 | msg := &RFC1459UserCommand{ 273 | hostname: NewName(args[1]), 274 | servername: NewName(args[2]), 275 | } 276 | msg.username = NewName(args[0]) 277 | msg.realname = NewText(args[3]) 278 | return msg, nil 279 | } 280 | 281 | // QUIT [ ] 282 | 283 | type QuitCommand struct { 284 | BaseCommand 285 | message Text 286 | } 287 | 288 | func NewQuitCommand(message Text) *QuitCommand { 289 | cmd := &QuitCommand{ 290 | message: message, 291 | } 292 | cmd.code = QUIT 293 | return cmd 294 | } 295 | 296 | func ParseQuitCommand(args []string) (Command, error) { 297 | msg := &QuitCommand{} 298 | if len(args) > 0 { 299 | msg.message = NewText(args[0]) 300 | } 301 | return msg, nil 302 | } 303 | 304 | // JOIN ( *( "," ) [ *( "," ) ] ) / "0" 305 | 306 | type JoinCommand struct { 307 | BaseCommand 308 | channels map[Name]Text 309 | zero bool 310 | } 311 | 312 | func ParseJoinCommand(args []string) (Command, error) { 313 | msg := &JoinCommand{ 314 | channels: make(map[Name]Text), 315 | } 316 | 317 | if len(args) == 0 { 318 | return nil, NotEnoughArgsError 319 | } 320 | 321 | if args[0] == "0" { 322 | msg.zero = true 323 | return msg, nil 324 | } 325 | 326 | channels := strings.Split(args[0], ",") 327 | keys := make([]string, len(channels)) 328 | if len(args) > 1 { 329 | for i, key := range strings.Split(args[1], ",") { 330 | if i >= len(channels) { 331 | break 332 | } 333 | keys[i] = key 334 | } 335 | } 336 | for i, channel := range channels { 337 | msg.channels[NewName(channel)] = NewText(keys[i]) 338 | } 339 | 340 | return msg, nil 341 | } 342 | 343 | // PART *( "," ) [ ] 344 | 345 | type PartCommand struct { 346 | BaseCommand 347 | channels []Name 348 | message Text 349 | } 350 | 351 | func (cmd *PartCommand) Message() Text { 352 | if cmd.message == "" { 353 | return cmd.Client().Nick().Text() 354 | } 355 | return cmd.message 356 | } 357 | 358 | func ParsePartCommand(args []string) (Command, error) { 359 | if len(args) < 1 { 360 | return nil, NotEnoughArgsError 361 | } 362 | msg := &PartCommand{ 363 | channels: NewNames(strings.Split(args[0], ",")), 364 | } 365 | if len(args) > 1 { 366 | msg.message = NewText(args[1]) 367 | } 368 | return msg, nil 369 | } 370 | 371 | // PRIVMSG 372 | 373 | type PrivMsgCommand struct { 374 | BaseCommand 375 | target Name 376 | message Text 377 | } 378 | 379 | func ParsePrivMsgCommand(args []string) (Command, error) { 380 | if len(args) < 2 { 381 | return nil, NotEnoughArgsError 382 | } 383 | return &PrivMsgCommand{ 384 | target: NewName(args[0]), 385 | message: NewText(args[1]), 386 | }, nil 387 | } 388 | 389 | // TOPIC [newtopic] 390 | 391 | type TopicCommand struct { 392 | BaseCommand 393 | channel Name 394 | setTopic bool 395 | topic Text 396 | } 397 | 398 | func ParseTopicCommand(args []string) (Command, error) { 399 | if len(args) < 1 { 400 | return nil, NotEnoughArgsError 401 | } 402 | msg := &TopicCommand{ 403 | channel: NewName(args[0]), 404 | } 405 | if len(args) > 1 { 406 | msg.setTopic = true 407 | msg.topic = NewText(args[1]) 408 | } 409 | return msg, nil 410 | } 411 | 412 | type ModeChange struct { 413 | mode UserMode 414 | op ModeOp 415 | } 416 | 417 | func (change *ModeChange) String() string { 418 | return fmt.Sprintf("%s%s", change.op, change.mode) 419 | } 420 | 421 | type ModeChanges []*ModeChange 422 | 423 | func (changes ModeChanges) String() string { 424 | if len(changes) == 0 { 425 | return "" 426 | } 427 | 428 | op := changes[0].op 429 | str := changes[0].op.String() 430 | for _, change := range changes { 431 | if change.op == op { 432 | str += change.mode.String() 433 | } else { 434 | op = change.op 435 | str += " " + change.op.String() 436 | } 437 | } 438 | return str 439 | } 440 | 441 | type ModeCommand struct { 442 | BaseCommand 443 | nickname Name 444 | changes ModeChanges 445 | } 446 | 447 | // MODE *( ( "+" / "-" ) *( "i" / "w" / "o" / "O" / "r" ) ) 448 | func ParseUserModeCommand(nickname Name, args []string) (Command, error) { 449 | cmd := &ModeCommand{ 450 | nickname: nickname, 451 | changes: make(ModeChanges, 0), 452 | } 453 | 454 | for _, modeChange := range args { 455 | if len(modeChange) == 0 { 456 | continue 457 | } 458 | op := ModeOp(modeChange[0]) 459 | if (op != Add) && (op != Remove) { 460 | return nil, ErrParseCommand 461 | } 462 | 463 | for _, mode := range modeChange[1:] { 464 | cmd.changes = append(cmd.changes, &ModeChange{ 465 | mode: UserMode(mode), 466 | op: op, 467 | }) 468 | } 469 | } 470 | 471 | return cmd, nil 472 | } 473 | 474 | type ChannelModeChange struct { 475 | mode ChannelMode 476 | op ModeOp 477 | arg string 478 | } 479 | 480 | func (change *ChannelModeChange) String() (str string) { 481 | if (change.op == Add) || (change.op == Remove) { 482 | str = change.op.String() 483 | } 484 | str += change.mode.String() 485 | if change.arg != "" { 486 | str += " " + change.arg 487 | } 488 | return 489 | } 490 | 491 | type ChannelModeChanges []*ChannelModeChange 492 | 493 | func (changes ChannelModeChanges) String() (str string) { 494 | if len(changes) == 0 { 495 | return 496 | } 497 | 498 | str = "+" 499 | if changes[0].op == Remove { 500 | str = "-" 501 | } 502 | for _, change := range changes { 503 | str += change.mode.String() 504 | } 505 | for _, change := range changes { 506 | if change.arg == "" { 507 | continue 508 | } 509 | str += " " + change.arg 510 | } 511 | return 512 | } 513 | 514 | type ChannelModeCommand struct { 515 | BaseCommand 516 | channel Name 517 | changes ChannelModeChanges 518 | } 519 | 520 | // MODE *( ( "-" / "+" ) * * ) 521 | func ParseChannelModeCommand(channel Name, args []string) (Command, error) { 522 | cmd := &ChannelModeCommand{ 523 | channel: channel, 524 | changes: make(ChannelModeChanges, 0), 525 | } 526 | 527 | for len(args) > 0 { 528 | if len(args[0]) == 0 { 529 | args = args[1:] 530 | continue 531 | } 532 | 533 | modeArg := args[0] 534 | op := ModeOp(modeArg[0]) 535 | if (op == Add) || (op == Remove) { 536 | modeArg = modeArg[1:] 537 | } else { 538 | op = List 539 | } 540 | 541 | skipArgs := 1 542 | for _, mode := range modeArg { 543 | change := &ChannelModeChange{ 544 | mode: ChannelMode(mode), 545 | op: op, 546 | } 547 | switch change.mode { 548 | case Key, BanMask, ExceptMask, InviteMask, UserLimit, 549 | ChannelOperator, ChannelCreator, Voice: 550 | if len(args) > skipArgs { 551 | change.arg = args[skipArgs] 552 | skipArgs += 1 553 | } 554 | } 555 | cmd.changes = append(cmd.changes, change) 556 | } 557 | args = args[skipArgs:] 558 | } 559 | 560 | return cmd, nil 561 | } 562 | 563 | func ParseModeCommand(args []string) (Command, error) { 564 | if len(args) == 0 { 565 | return nil, NotEnoughArgsError 566 | } 567 | 568 | name := NewName(args[0]) 569 | if name.IsChannel() { 570 | return ParseChannelModeCommand(name, args[1:]) 571 | } else { 572 | return ParseUserModeCommand(name, args[1:]) 573 | } 574 | } 575 | 576 | type WhoisCommand struct { 577 | BaseCommand 578 | target Name 579 | masks []Name 580 | } 581 | 582 | // WHOIS [ ] *( "," ) 583 | func ParseWhoisCommand(args []string) (Command, error) { 584 | if len(args) < 1 { 585 | return nil, NotEnoughArgsError 586 | } 587 | 588 | var masks string 589 | var target string 590 | 591 | if len(args) > 1 { 592 | target = args[0] 593 | masks = args[1] 594 | } else { 595 | masks = args[0] 596 | } 597 | 598 | return &WhoisCommand{ 599 | target: NewName(target), 600 | masks: NewNames(strings.Split(masks, ",")), 601 | }, nil 602 | } 603 | 604 | type WhoCommand struct { 605 | BaseCommand 606 | mask Name 607 | operatorOnly bool 608 | } 609 | 610 | // WHO [ [ "o" ] ] 611 | func ParseWhoCommand(args []string) (Command, error) { 612 | cmd := &WhoCommand{} 613 | 614 | if len(args) > 0 { 615 | cmd.mask = NewName(args[0]) 616 | } 617 | 618 | if (len(args) > 1) && (args[1] == "o") { 619 | cmd.operatorOnly = true 620 | } 621 | 622 | return cmd, nil 623 | } 624 | 625 | type OperCommand struct { 626 | PassCommand 627 | name Name 628 | } 629 | 630 | func (msg *OperCommand) LoadPassword(server *Server) { 631 | msg.hash = server.operators[msg.name] 632 | } 633 | 634 | // OPER 635 | func ParseOperCommand(args []string) (Command, error) { 636 | if len(args) < 2 { 637 | return nil, NotEnoughArgsError 638 | } 639 | 640 | cmd := &OperCommand{ 641 | name: NewName(args[0]), 642 | } 643 | cmd.password = []byte(args[1]) 644 | return cmd, nil 645 | } 646 | 647 | type CapCommand struct { 648 | BaseCommand 649 | subCommand CapSubCommand 650 | capabilities CapabilitySet 651 | } 652 | 653 | func ParseCapCommand(args []string) (Command, error) { 654 | if len(args) < 1 { 655 | return nil, NotEnoughArgsError 656 | } 657 | 658 | cmd := &CapCommand{ 659 | subCommand: CapSubCommand(strings.ToUpper(args[0])), 660 | capabilities: make(CapabilitySet), 661 | } 662 | 663 | if len(args) > 1 { 664 | strs := spacesExpr.Split(args[1], -1) 665 | for _, str := range strs { 666 | cmd.capabilities[Capability(str)] = true 667 | } 668 | } 669 | return cmd, nil 670 | } 671 | 672 | // HAPROXY support 673 | type ProxyCommand struct { 674 | BaseCommand 675 | net Name 676 | sourceIP Name 677 | destIP Name 678 | sourcePort Name 679 | destPort Name 680 | hostname Name // looked up in socket thread 681 | } 682 | 683 | func NewProxyCommand(hostname Name) *ProxyCommand { 684 | cmd := &ProxyCommand{ 685 | hostname: hostname, 686 | } 687 | cmd.code = PROXY 688 | return cmd 689 | } 690 | 691 | func ParseProxyCommand(args []string) (Command, error) { 692 | if len(args) < 5 { 693 | return nil, NotEnoughArgsError 694 | } 695 | return &ProxyCommand{ 696 | net: NewName(args[0]), 697 | sourceIP: NewName(args[1]), 698 | destIP: NewName(args[2]), 699 | sourcePort: NewName(args[3]), 700 | destPort: NewName(args[4]), 701 | hostname: LookupHostname(NewName(args[1])), 702 | }, nil 703 | } 704 | 705 | type AwayCommand struct { 706 | BaseCommand 707 | text Text 708 | } 709 | 710 | func ParseAwayCommand(args []string) (Command, error) { 711 | cmd := &AwayCommand{} 712 | 713 | if len(args) > 0 { 714 | cmd.text = NewText(args[0]) 715 | } 716 | 717 | return cmd, nil 718 | } 719 | 720 | type IsOnCommand struct { 721 | BaseCommand 722 | nicks []Name 723 | } 724 | 725 | func ParseIsOnCommand(args []string) (Command, error) { 726 | if len(args) == 0 { 727 | return nil, NotEnoughArgsError 728 | } 729 | 730 | return &IsOnCommand{ 731 | nicks: NewNames(args), 732 | }, nil 733 | } 734 | 735 | type MOTDCommand struct { 736 | BaseCommand 737 | target Name 738 | } 739 | 740 | func ParseMOTDCommand(args []string) (Command, error) { 741 | cmd := &MOTDCommand{} 742 | if len(args) > 0 { 743 | cmd.target = NewName(args[0]) 744 | } 745 | return cmd, nil 746 | } 747 | 748 | type NoticeCommand struct { 749 | BaseCommand 750 | target Name 751 | message Text 752 | } 753 | 754 | func ParseNoticeCommand(args []string) (Command, error) { 755 | if len(args) < 2 { 756 | return nil, NotEnoughArgsError 757 | } 758 | return &NoticeCommand{ 759 | target: NewName(args[0]), 760 | message: NewText(args[1]), 761 | }, nil 762 | } 763 | 764 | type KickCommand struct { 765 | BaseCommand 766 | kicks map[Name]Name 767 | comment Text 768 | } 769 | 770 | func (msg *KickCommand) Comment() Text { 771 | if msg.comment == "" { 772 | return msg.Client().Nick().Text() 773 | } 774 | return msg.comment 775 | } 776 | 777 | func ParseKickCommand(args []string) (Command, error) { 778 | if len(args) < 2 { 779 | return nil, NotEnoughArgsError 780 | } 781 | channels := NewNames(strings.Split(args[0], ",")) 782 | users := NewNames(strings.Split(args[1], ",")) 783 | if (len(channels) != len(users)) && (len(users) != 1) { 784 | return nil, NotEnoughArgsError 785 | } 786 | cmd := &KickCommand{ 787 | kicks: make(map[Name]Name), 788 | } 789 | for index, channel := range channels { 790 | if len(users) == 1 { 791 | cmd.kicks[channel] = users[0] 792 | } else { 793 | cmd.kicks[channel] = users[index] 794 | } 795 | } 796 | if len(args) > 2 { 797 | cmd.comment = NewText(args[2]) 798 | } 799 | return cmd, nil 800 | } 801 | 802 | type ListCommand struct { 803 | BaseCommand 804 | channels []Name 805 | target Name 806 | } 807 | 808 | func ParseListCommand(args []string) (Command, error) { 809 | cmd := &ListCommand{} 810 | if len(args) > 0 { 811 | cmd.channels = NewNames(strings.Split(args[0], ",")) 812 | } 813 | if len(args) > 1 { 814 | cmd.target = NewName(args[1]) 815 | } 816 | return cmd, nil 817 | } 818 | 819 | type NamesCommand struct { 820 | BaseCommand 821 | channels []Name 822 | target Name 823 | } 824 | 825 | func ParseNamesCommand(args []string) (Command, error) { 826 | cmd := &NamesCommand{} 827 | if len(args) > 0 { 828 | cmd.channels = NewNames(strings.Split(args[0], ",")) 829 | } 830 | if len(args) > 1 { 831 | cmd.target = NewName(args[1]) 832 | } 833 | return cmd, nil 834 | } 835 | 836 | type DebugCommand struct { 837 | BaseCommand 838 | subCommand Name 839 | } 840 | 841 | func ParseDebugCommand(args []string) (Command, error) { 842 | if len(args) == 0 { 843 | return nil, NotEnoughArgsError 844 | } 845 | 846 | return &DebugCommand{ 847 | subCommand: NewName(strings.ToUpper(args[0])), 848 | }, nil 849 | } 850 | 851 | type VersionCommand struct { 852 | BaseCommand 853 | target Name 854 | } 855 | 856 | func ParseVersionCommand(args []string) (Command, error) { 857 | cmd := &VersionCommand{} 858 | if len(args) > 0 { 859 | cmd.target = NewName(args[0]) 860 | } 861 | return cmd, nil 862 | } 863 | 864 | type InviteCommand struct { 865 | BaseCommand 866 | nickname Name 867 | channel Name 868 | } 869 | 870 | func ParseInviteCommand(args []string) (Command, error) { 871 | if len(args) < 2 { 872 | return nil, NotEnoughArgsError 873 | } 874 | 875 | return &InviteCommand{ 876 | nickname: NewName(args[0]), 877 | channel: NewName(args[1]), 878 | }, nil 879 | } 880 | 881 | func ParseTheaterCommand(args []string) (Command, error) { 882 | if len(args) < 1 { 883 | return nil, NotEnoughArgsError 884 | } else if upperSubCmd := strings.ToUpper(args[0]); upperSubCmd == "IDENTIFY" && len(args) == 3 { 885 | return &TheaterIdentifyCommand{ 886 | channel: NewName(args[1]), 887 | PassCommand: PassCommand{password: []byte(args[2])}, 888 | }, nil 889 | } else if upperSubCmd == "PRIVMSG" && len(args) == 4 { 890 | return &TheaterPrivMsgCommand{ 891 | channel: NewName(args[1]), 892 | asNick: NewName(args[2]), 893 | message: NewText(args[3]), 894 | }, nil 895 | } else if upperSubCmd == "ACTION" && len(args) == 4 { 896 | return &TheaterActionCommand{ 897 | channel: NewName(args[1]), 898 | asNick: NewName(args[2]), 899 | action: NewCTCPText(args[3]), 900 | }, nil 901 | } else { 902 | return nil, ErrParseCommand 903 | } 904 | } 905 | 906 | type TimeCommand struct { 907 | BaseCommand 908 | target Name 909 | } 910 | 911 | func ParseTimeCommand(args []string) (Command, error) { 912 | cmd := &TimeCommand{} 913 | if len(args) > 0 { 914 | cmd.target = NewName(args[0]) 915 | } 916 | return cmd, nil 917 | } 918 | 919 | type KillCommand struct { 920 | BaseCommand 921 | nickname Name 922 | comment Text 923 | } 924 | 925 | func ParseKillCommand(args []string) (Command, error) { 926 | if len(args) < 2 { 927 | return nil, NotEnoughArgsError 928 | } 929 | return &KillCommand{ 930 | nickname: NewName(args[0]), 931 | comment: NewText(args[1]), 932 | }, nil 933 | } 934 | 935 | type WhoWasCommand struct { 936 | BaseCommand 937 | nicknames []Name 938 | count int64 939 | target Name 940 | } 941 | 942 | func ParseWhoWasCommand(args []string) (Command, error) { 943 | if len(args) < 1 { 944 | return nil, NotEnoughArgsError 945 | } 946 | cmd := &WhoWasCommand{ 947 | nicknames: NewNames(strings.Split(args[0], ",")), 948 | } 949 | if len(args) > 1 { 950 | cmd.count, _ = strconv.ParseInt(args[1], 10, 64) 951 | } 952 | if len(args) > 2 { 953 | cmd.target = NewName(args[2]) 954 | } 955 | return cmd, nil 956 | } 957 | 958 | func ParseOperNickCommand(args []string) (Command, error) { 959 | if len(args) < 2 { 960 | return nil, NotEnoughArgsError 961 | } 962 | 963 | return &OperNickCommand{ 964 | target: NewName(args[0]), 965 | nick: NewName(args[1]), 966 | }, nil 967 | } 968 | -------------------------------------------------------------------------------- /irc/config.go: -------------------------------------------------------------------------------- 1 | package irc 2 | 3 | import ( 4 | "errors" 5 | "io/ioutil" 6 | "log" 7 | 8 | "gopkg.in/yaml.v2" 9 | ) 10 | 11 | type PassConfig struct { 12 | Password string 13 | } 14 | 15 | func (conf *PassConfig) PasswordBytes() []byte { 16 | bytes, err := DecodePassword(conf.Password) 17 | if err != nil { 18 | log.Fatal("decode password error: ", err) 19 | } 20 | return bytes 21 | } 22 | 23 | type Config struct { 24 | Server struct { 25 | PassConfig 26 | Database string 27 | Listen []string 28 | Wslisten string 29 | Log string 30 | MOTD string 31 | Name string 32 | } 33 | 34 | Operator map[string]*PassConfig 35 | 36 | Theater map[string]*PassConfig 37 | } 38 | 39 | func (conf *Config) Operators() map[Name][]byte { 40 | operators := make(map[Name][]byte) 41 | for name, opConf := range conf.Operator { 42 | operators[NewName(name)] = opConf.PasswordBytes() 43 | } 44 | return operators 45 | } 46 | 47 | func (conf *Config) Theaters() map[Name][]byte { 48 | theaters := make(map[Name][]byte) 49 | for s, theaterConf := range conf.Theater { 50 | name := NewName(s) 51 | if !name.IsChannel() { 52 | log.Fatal("config uses a non-channel for a theater!") 53 | } 54 | theaters[name] = theaterConf.PasswordBytes() 55 | } 56 | return theaters 57 | } 58 | 59 | func LoadConfig(filename string) (config *Config, err error) { 60 | data, err := ioutil.ReadFile(filename) 61 | if err != nil { 62 | return nil, err 63 | } 64 | 65 | err = yaml.Unmarshal(data, &config) 66 | if err != nil { 67 | return nil, err 68 | } 69 | 70 | if config.Server.Name == "" { 71 | return nil, errors.New("Server name missing") 72 | } 73 | if config.Server.Database == "" { 74 | return nil, errors.New("Server database missing") 75 | } 76 | if len(config.Server.Listen) == 0 { 77 | return nil, errors.New("Server listening addresses missing") 78 | } 79 | return config, nil 80 | } 81 | -------------------------------------------------------------------------------- /irc/constants.go: -------------------------------------------------------------------------------- 1 | package irc 2 | 3 | const ( 4 | SEM_VER = "ergonomadic-1.4.4" 5 | CRLF = "\r\n" 6 | MAX_REPLY_LEN = 512 - len(CRLF) 7 | 8 | // string codes 9 | AWAY StringCode = "AWAY" 10 | CAP StringCode = "CAP" 11 | DEBUG StringCode = "DEBUG" 12 | ERROR StringCode = "ERROR" 13 | INVITE StringCode = "INVITE" 14 | ISON StringCode = "ISON" 15 | JOIN StringCode = "JOIN" 16 | KICK StringCode = "KICK" 17 | KILL StringCode = "KILL" 18 | LIST StringCode = "LIST" 19 | MODE StringCode = "MODE" 20 | MOTD StringCode = "MOTD" 21 | NAMES StringCode = "NAMES" 22 | NICK StringCode = "NICK" 23 | NOTICE StringCode = "NOTICE" 24 | ONICK StringCode = "ONICK" 25 | OPER StringCode = "OPER" 26 | PART StringCode = "PART" 27 | PASS StringCode = "PASS" 28 | PING StringCode = "PING" 29 | PONG StringCode = "PONG" 30 | PRIVMSG StringCode = "PRIVMSG" 31 | PROXY StringCode = "PROXY" 32 | QUIT StringCode = "QUIT" 33 | THEATER StringCode = "THEATER" // nonstandard 34 | TIME StringCode = "TIME" 35 | TOPIC StringCode = "TOPIC" 36 | USER StringCode = "USER" 37 | VERSION StringCode = "VERSION" 38 | WHO StringCode = "WHO" 39 | WHOIS StringCode = "WHOIS" 40 | WHOWAS StringCode = "WHOWAS" 41 | 42 | // numeric codes 43 | RPL_WELCOME NumericCode = 1 44 | RPL_YOURHOST NumericCode = 2 45 | RPL_CREATED NumericCode = 3 46 | RPL_MYINFO NumericCode = 4 47 | RPL_BOUNCE NumericCode = 5 48 | RPL_TRACELINK NumericCode = 200 49 | RPL_TRACECONNECTING NumericCode = 201 50 | RPL_TRACEHANDSHAKE NumericCode = 202 51 | RPL_TRACEUNKNOWN NumericCode = 203 52 | RPL_TRACEOPERATOR NumericCode = 204 53 | RPL_TRACEUSER NumericCode = 205 54 | RPL_TRACESERVER NumericCode = 206 55 | RPL_TRACESERVICE NumericCode = 207 56 | RPL_TRACENEWTYPE NumericCode = 208 57 | RPL_TRACECLASS NumericCode = 209 58 | RPL_TRACERECONNECT NumericCode = 210 59 | RPL_STATSLINKINFO NumericCode = 211 60 | RPL_STATSCOMMANDS NumericCode = 212 61 | RPL_ENDOFSTATS NumericCode = 219 62 | RPL_UMODEIS NumericCode = 221 63 | RPL_SERVLIST NumericCode = 234 64 | RPL_SERVLISTEND NumericCode = 235 65 | RPL_STATSUPTIME NumericCode = 242 66 | RPL_STATSOLINE NumericCode = 243 67 | RPL_LUSERCLIENT NumericCode = 251 68 | RPL_LUSEROP NumericCode = 252 69 | RPL_LUSERUNKNOWN NumericCode = 253 70 | RPL_LUSERCHANNELS NumericCode = 254 71 | RPL_LUSERME NumericCode = 255 72 | RPL_ADMINME NumericCode = 256 73 | RPL_ADMINLOC1 NumericCode = 257 74 | RPL_ADMINLOC2 NumericCode = 258 75 | RPL_ADMINEMAIL NumericCode = 259 76 | RPL_TRACELOG NumericCode = 261 77 | RPL_TRACEEND NumericCode = 262 78 | RPL_TRYAGAIN NumericCode = 263 79 | RPL_AWAY NumericCode = 301 80 | RPL_USERHOST NumericCode = 302 81 | RPL_ISON NumericCode = 303 82 | RPL_UNAWAY NumericCode = 305 83 | RPL_NOWAWAY NumericCode = 306 84 | RPL_WHOISUSER NumericCode = 311 85 | RPL_WHOISSERVER NumericCode = 312 86 | RPL_WHOISOPERATOR NumericCode = 313 87 | RPL_WHOWASUSER NumericCode = 314 88 | RPL_ENDOFWHO NumericCode = 315 89 | RPL_WHOISIDLE NumericCode = 317 90 | RPL_ENDOFWHOIS NumericCode = 318 91 | RPL_WHOISCHANNELS NumericCode = 319 92 | RPL_LIST NumericCode = 322 93 | RPL_LISTEND NumericCode = 323 94 | RPL_CHANNELMODEIS NumericCode = 324 95 | RPL_UNIQOPIS NumericCode = 325 96 | RPL_NOTOPIC NumericCode = 331 97 | RPL_TOPIC NumericCode = 332 98 | RPL_INVITING NumericCode = 341 99 | RPL_SUMMONING NumericCode = 342 100 | RPL_INVITELIST NumericCode = 346 101 | RPL_ENDOFINVITELIST NumericCode = 347 102 | RPL_EXCEPTLIST NumericCode = 348 103 | RPL_ENDOFEXCEPTLIST NumericCode = 349 104 | RPL_VERSION NumericCode = 351 105 | RPL_WHOREPLY NumericCode = 352 106 | RPL_NAMREPLY NumericCode = 353 107 | RPL_LINKS NumericCode = 364 108 | RPL_ENDOFLINKS NumericCode = 365 109 | RPL_ENDOFNAMES NumericCode = 366 110 | RPL_BANLIST NumericCode = 367 111 | RPL_ENDOFBANLIST NumericCode = 368 112 | RPL_ENDOFWHOWAS NumericCode = 369 113 | RPL_INFO NumericCode = 371 114 | RPL_MOTD NumericCode = 372 115 | RPL_ENDOFINFO NumericCode = 374 116 | RPL_MOTDSTART NumericCode = 375 117 | RPL_ENDOFMOTD NumericCode = 376 118 | RPL_YOUREOPER NumericCode = 381 119 | RPL_REHASHING NumericCode = 382 120 | RPL_YOURESERVICE NumericCode = 383 121 | RPL_TIME NumericCode = 391 122 | RPL_USERSSTART NumericCode = 392 123 | RPL_USERS NumericCode = 393 124 | RPL_ENDOFUSERS NumericCode = 394 125 | RPL_NOUSERS NumericCode = 395 126 | ERR_NOSUCHNICK NumericCode = 401 127 | ERR_NOSUCHSERVER NumericCode = 402 128 | ERR_NOSUCHCHANNEL NumericCode = 403 129 | ERR_CANNOTSENDTOCHAN NumericCode = 404 130 | ERR_TOOMANYCHANNELS NumericCode = 405 131 | ERR_WASNOSUCHNICK NumericCode = 406 132 | ERR_TOOMANYTARGETS NumericCode = 407 133 | ERR_NOSUCHSERVICE NumericCode = 408 134 | ERR_NOORIGIN NumericCode = 409 135 | ERR_INVALIDCAPCMD NumericCode = 410 136 | ERR_NORECIPIENT NumericCode = 411 137 | ERR_NOTEXTTOSEND NumericCode = 412 138 | ERR_NOTOPLEVEL NumericCode = 413 139 | ERR_WILDTOPLEVEL NumericCode = 414 140 | ERR_BADMASK NumericCode = 415 141 | ERR_UNKNOWNCOMMAND NumericCode = 421 142 | ERR_NOMOTD NumericCode = 422 143 | ERR_NOADMININFO NumericCode = 423 144 | ERR_FILEERROR NumericCode = 424 145 | ERR_NONICKNAMEGIVEN NumericCode = 431 146 | ERR_ERRONEUSNICKNAME NumericCode = 432 147 | ERR_NICKNAMEINUSE NumericCode = 433 148 | ERR_NICKCOLLISION NumericCode = 436 149 | ERR_UNAVAILRESOURCE NumericCode = 437 150 | ERR_USERNOTINCHANNEL NumericCode = 441 151 | ERR_NOTONCHANNEL NumericCode = 442 152 | ERR_USERONCHANNEL NumericCode = 443 153 | ERR_NOLOGIN NumericCode = 444 154 | ERR_SUMMONDISABLED NumericCode = 445 155 | ERR_USERSDISABLED NumericCode = 446 156 | ERR_NOTREGISTERED NumericCode = 451 157 | ERR_NEEDMOREPARAMS NumericCode = 461 158 | ERR_ALREADYREGISTRED NumericCode = 462 159 | ERR_NOPERMFORHOST NumericCode = 463 160 | ERR_PASSWDMISMATCH NumericCode = 464 161 | ERR_YOUREBANNEDCREEP NumericCode = 465 162 | ERR_YOUWILLBEBANNED NumericCode = 466 163 | ERR_KEYSET NumericCode = 467 164 | ERR_CHANNELISFULL NumericCode = 471 165 | ERR_UNKNOWNMODE NumericCode = 472 166 | ERR_INVITEONLYCHAN NumericCode = 473 167 | ERR_BANNEDFROMCHAN NumericCode = 474 168 | ERR_BADCHANNELKEY NumericCode = 475 169 | ERR_BADCHANMASK NumericCode = 476 170 | ERR_NOCHANMODES NumericCode = 477 171 | ERR_BANLISTFULL NumericCode = 478 172 | ERR_NOPRIVILEGES NumericCode = 481 173 | ERR_CHANOPRIVSNEEDED NumericCode = 482 174 | ERR_CANTKILLSERVER NumericCode = 483 175 | ERR_RESTRICTED NumericCode = 484 176 | ERR_UNIQOPPRIVSNEEDED NumericCode = 485 177 | ERR_NOOPERHOST NumericCode = 491 178 | ERR_UMODEUNKNOWNFLAG NumericCode = 501 179 | ERR_USERSDONTMATCH NumericCode = 502 180 | ) 181 | -------------------------------------------------------------------------------- /irc/database.go: -------------------------------------------------------------------------------- 1 | package irc 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | _ "github.com/mattn/go-sqlite3" 7 | "log" 8 | "os" 9 | ) 10 | 11 | func InitDB(path string) { 12 | os.Remove(path) 13 | db := OpenDB(path) 14 | defer db.Close() 15 | _, err := db.Exec(` 16 | CREATE TABLE channel ( 17 | name TEXT NOT NULL UNIQUE, 18 | flags TEXT DEFAULT '', 19 | key TEXT DEFAULT '', 20 | topic TEXT DEFAULT '', 21 | user_limit INTEGER DEFAULT 0, 22 | ban_list TEXT DEFAULT '', 23 | except_list TEXT DEFAULT '', 24 | invite_list TEXT DEFAULT '')`) 25 | if err != nil { 26 | log.Fatal("initdb error: ", err) 27 | } 28 | } 29 | 30 | func UpgradeDB(path string) { 31 | db := OpenDB(path) 32 | alter := `ALTER TABLE channel ADD COLUMN %s TEXT DEFAULT ''` 33 | cols := []string{"ban_list", "except_list", "invite_list"} 34 | for _, col := range cols { 35 | _, err := db.Exec(fmt.Sprintf(alter, col)) 36 | if err != nil { 37 | log.Fatal("updatedb error: ", err) 38 | } 39 | } 40 | } 41 | 42 | func OpenDB(path string) *sql.DB { 43 | db, err := sql.Open("sqlite3", path) 44 | if err != nil { 45 | log.Fatal("open db error: ", err) 46 | } 47 | return db 48 | } 49 | -------------------------------------------------------------------------------- /irc/debug.go: -------------------------------------------------------------------------------- 1 | package irc 2 | 3 | import ( 4 | "os" 5 | "runtime" 6 | "runtime/debug" 7 | "runtime/pprof" 8 | "time" 9 | ) 10 | 11 | func (msg *DebugCommand) HandleServer(server *Server) { 12 | client := msg.Client() 13 | if !client.flags[Operator] { 14 | return 15 | } 16 | 17 | switch msg.subCommand { 18 | case "GCSTATS": 19 | stats := debug.GCStats{ 20 | Pause: make([]time.Duration, 10), 21 | PauseQuantiles: make([]time.Duration, 5), 22 | } 23 | debug.ReadGCStats(&stats) 24 | 25 | server.Replyf(client, "last GC: %s", stats.LastGC.Format(time.RFC1123)) 26 | server.Replyf(client, "num GC: %d", stats.NumGC) 27 | server.Replyf(client, "pause total: %s", stats.PauseTotal) 28 | server.Replyf(client, "pause quantiles min%%: %s", stats.PauseQuantiles[0]) 29 | server.Replyf(client, "pause quantiles 25%%: %s", stats.PauseQuantiles[1]) 30 | server.Replyf(client, "pause quantiles 50%%: %s", stats.PauseQuantiles[2]) 31 | server.Replyf(client, "pause quantiles 75%%: %s", stats.PauseQuantiles[3]) 32 | server.Replyf(client, "pause quantiles max%%: %s", stats.PauseQuantiles[4]) 33 | 34 | case "NUMGOROUTINE": 35 | count := runtime.NumGoroutine() 36 | server.Replyf(client, "num goroutines: %d", count) 37 | 38 | case "PROFILEHEAP": 39 | profFile := "ergonomadic.mprof" 40 | file, err := os.Create(profFile) 41 | if err != nil { 42 | server.Replyf(client, "error: %s", err) 43 | break 44 | } 45 | defer file.Close() 46 | pprof.Lookup("heap").WriteTo(file, 0) 47 | server.Replyf(client, "written to %s", profFile) 48 | 49 | case "STARTCPUPROFILE": 50 | profFile := "ergonomadic.prof" 51 | file, err := os.Create(profFile) 52 | if err != nil { 53 | server.Replyf(client, "error: %s", err) 54 | break 55 | } 56 | if err := pprof.StartCPUProfile(file); err != nil { 57 | defer file.Close() 58 | server.Replyf(client, "error: %s", err) 59 | break 60 | } 61 | 62 | server.Replyf(client, "CPU profile writing to %s", profFile) 63 | 64 | case "STOPCPUPROFILE": 65 | pprof.StopCPUProfile() 66 | server.Reply(client, "CPU profiling stopped") 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /irc/logging.go: -------------------------------------------------------------------------------- 1 | package irc 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "os" 7 | ) 8 | 9 | type Logging struct { 10 | debug *log.Logger 11 | info *log.Logger 12 | warn *log.Logger 13 | error *log.Logger 14 | } 15 | 16 | var ( 17 | levels = map[string]uint8{ 18 | "debug": 4, 19 | "info": 3, 20 | "warn": 2, 21 | "error": 1, 22 | } 23 | devNull io.Writer 24 | ) 25 | 26 | func init() { 27 | var err error 28 | devNull, err = os.Open(os.DevNull) 29 | if err != nil { 30 | log.Fatal(err) 31 | } 32 | } 33 | 34 | func NewLogger(on bool) *log.Logger { 35 | return log.New(output(on), "", log.LstdFlags) 36 | } 37 | 38 | func output(on bool) io.Writer { 39 | if on { 40 | return os.Stdout 41 | } 42 | return devNull 43 | } 44 | 45 | func (logging *Logging) SetLevel(level string) { 46 | logging.debug = NewLogger(levels[level] >= levels["debug"]) 47 | logging.info = NewLogger(levels[level] >= levels["info"]) 48 | logging.warn = NewLogger(levels[level] >= levels["warn"]) 49 | logging.error = NewLogger(levels[level] >= levels["error"]) 50 | } 51 | 52 | func NewLogging(level string) *Logging { 53 | logging := &Logging{} 54 | logging.SetLevel(level) 55 | return logging 56 | } 57 | 58 | var ( 59 | Log = NewLogging("warn") 60 | ) 61 | -------------------------------------------------------------------------------- /irc/modes.go: -------------------------------------------------------------------------------- 1 | package irc 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | // user mode flags 8 | type UserMode rune 9 | 10 | func (mode UserMode) String() string { 11 | return string(mode) 12 | } 13 | 14 | type UserModes []UserMode 15 | 16 | func (modes UserModes) String() string { 17 | strs := make([]string, len(modes)) 18 | for index, mode := range modes { 19 | strs[index] = mode.String() 20 | } 21 | return strings.Join(strs, "") 22 | } 23 | 24 | // channel mode flags 25 | type ChannelMode rune 26 | 27 | func (mode ChannelMode) String() string { 28 | return string(mode) 29 | } 30 | 31 | type ChannelModes []ChannelMode 32 | 33 | func (modes ChannelModes) String() string { 34 | strs := make([]string, len(modes)) 35 | for index, mode := range modes { 36 | strs[index] = mode.String() 37 | } 38 | return strings.Join(strs, "") 39 | } 40 | 41 | type ModeOp rune 42 | 43 | func (op ModeOp) String() string { 44 | return string(op) 45 | } 46 | 47 | const ( 48 | Add ModeOp = '+' 49 | List ModeOp = '=' 50 | Remove ModeOp = '-' 51 | ) 52 | 53 | const ( 54 | Away UserMode = 'a' 55 | Invisible UserMode = 'i' 56 | LocalOperator UserMode = 'O' 57 | Operator UserMode = 'o' 58 | Restricted UserMode = 'r' 59 | ServerNotice UserMode = 's' // deprecated 60 | WallOps UserMode = 'w' 61 | ) 62 | 63 | var ( 64 | SupportedUserModes = UserModes{ 65 | Away, Invisible, Operator, 66 | } 67 | ) 68 | 69 | const ( 70 | Anonymous ChannelMode = 'a' // flag 71 | BanMask ChannelMode = 'b' // arg 72 | ChannelCreator ChannelMode = 'O' // flag 73 | ChannelOperator ChannelMode = 'o' // arg 74 | ExceptMask ChannelMode = 'e' // arg 75 | InviteMask ChannelMode = 'I' // arg 76 | InviteOnly ChannelMode = 'i' // flag 77 | Key ChannelMode = 'k' // flag arg 78 | Moderated ChannelMode = 'm' // flag 79 | NoOutside ChannelMode = 'n' // flag 80 | OpOnlyTopic ChannelMode = 't' // flag 81 | Persistent ChannelMode = 'P' // flag 82 | Private ChannelMode = 'p' // flag 83 | Quiet ChannelMode = 'q' // flag 84 | ReOp ChannelMode = 'r' // flag 85 | Secret ChannelMode = 's' // flag, deprecated 86 | Theater ChannelMode = 'T' // flag, nonstandard 87 | UserLimit ChannelMode = 'l' // flag arg 88 | Voice ChannelMode = 'v' // arg 89 | ) 90 | 91 | var ( 92 | SupportedChannelModes = ChannelModes{ 93 | BanMask, ExceptMask, InviteMask, InviteOnly, Key, NoOutside, 94 | OpOnlyTopic, Persistent, Private, Theater, UserLimit, 95 | } 96 | ) 97 | 98 | // 99 | // commands 100 | // 101 | 102 | func (m *ModeCommand) HandleServer(s *Server) { 103 | client := m.Client() 104 | target := s.clients.Get(m.nickname) 105 | 106 | if target == nil { 107 | client.ErrNoSuchNick(m.nickname) 108 | return 109 | } 110 | 111 | if client != target && !client.flags[Operator] { 112 | client.ErrUsersDontMatch() 113 | return 114 | } 115 | 116 | changes := make(ModeChanges, 0, len(m.changes)) 117 | 118 | for _, change := range m.changes { 119 | switch change.mode { 120 | case Invisible, ServerNotice, WallOps: 121 | switch change.op { 122 | case Add: 123 | if target.flags[change.mode] { 124 | continue 125 | } 126 | target.flags[change.mode] = true 127 | changes = append(changes, change) 128 | 129 | case Remove: 130 | if !target.flags[change.mode] { 131 | continue 132 | } 133 | delete(target.flags, change.mode) 134 | changes = append(changes, change) 135 | } 136 | 137 | case Operator, LocalOperator: 138 | if change.op == Remove { 139 | if !target.flags[change.mode] { 140 | continue 141 | } 142 | delete(target.flags, change.mode) 143 | changes = append(changes, change) 144 | } 145 | } 146 | } 147 | 148 | if len(changes) > 0 { 149 | client.Reply(RplModeChanges(client, target, changes)) 150 | } else if client == target { 151 | client.RplUModeIs(client) 152 | } 153 | client.Reply(RplCurrentMode(client, target)) 154 | } 155 | 156 | func (msg *ChannelModeCommand) HandleServer(server *Server) { 157 | client := msg.Client() 158 | channel := server.channels.Get(msg.channel) 159 | if channel == nil { 160 | client.ErrNoSuchChannel(msg.channel) 161 | return 162 | } 163 | 164 | channel.Mode(client, msg.changes) 165 | } 166 | -------------------------------------------------------------------------------- /irc/net.go: -------------------------------------------------------------------------------- 1 | package irc 2 | 3 | import ( 4 | "net" 5 | "strings" 6 | ) 7 | 8 | func IPString(addr net.Addr) Name { 9 | addrStr := addr.String() 10 | ipaddr, _, err := net.SplitHostPort(addrStr) 11 | if err != nil { 12 | return Name(addrStr) 13 | } 14 | return Name(ipaddr) 15 | } 16 | 17 | func AddrLookupHostname(addr net.Addr) Name { 18 | return LookupHostname(IPString(addr)) 19 | } 20 | 21 | func LookupHostname(addr Name) Name { 22 | names, err := net.LookupAddr(addr.String()) 23 | if err != nil { 24 | return Name(addr) 25 | } 26 | 27 | hostname := strings.TrimSuffix(names[0], ".") 28 | return Name(hostname) 29 | } 30 | -------------------------------------------------------------------------------- /irc/nickname.go: -------------------------------------------------------------------------------- 1 | package irc 2 | 3 | type NickCommand struct { 4 | BaseCommand 5 | nickname Name 6 | } 7 | 8 | func (m *NickCommand) HandleRegServer(s *Server) { 9 | client := m.Client() 10 | if !client.authorized { 11 | client.ErrPasswdMismatch() 12 | client.Quit("bad password") 13 | return 14 | } 15 | 16 | if client.capState == CapNegotiating { 17 | client.capState = CapNegotiated 18 | } 19 | 20 | if m.nickname == "" { 21 | client.ErrNoNicknameGiven() 22 | return 23 | } 24 | 25 | if s.clients.Get(m.nickname) != nil { 26 | client.ErrNickNameInUse(m.nickname) 27 | return 28 | } 29 | 30 | if !m.nickname.IsNickname() { 31 | client.ErrErroneusNickname(m.nickname) 32 | return 33 | } 34 | 35 | client.SetNickname(m.nickname) 36 | s.tryRegister(client) 37 | } 38 | 39 | func (msg *NickCommand) HandleServer(server *Server) { 40 | client := msg.Client() 41 | 42 | if msg.nickname == "" { 43 | client.ErrNoNicknameGiven() 44 | return 45 | } 46 | 47 | if !msg.nickname.IsNickname() { 48 | client.ErrErroneusNickname(msg.nickname) 49 | return 50 | } 51 | 52 | if msg.nickname == client.nick { 53 | return 54 | } 55 | 56 | target := server.clients.Get(msg.nickname) 57 | if (target != nil) && (target != client) { 58 | client.ErrNickNameInUse(msg.nickname) 59 | return 60 | } 61 | 62 | client.ChangeNickname(msg.nickname) 63 | } 64 | 65 | type OperNickCommand struct { 66 | BaseCommand 67 | target Name 68 | nick Name 69 | } 70 | 71 | func (msg *OperNickCommand) HandleServer(server *Server) { 72 | client := msg.Client() 73 | 74 | if !client.flags[Operator] { 75 | client.ErrNoPrivileges() 76 | return 77 | } 78 | 79 | if !msg.nick.IsNickname() { 80 | client.ErrErroneusNickname(msg.nick) 81 | return 82 | } 83 | 84 | if msg.nick == client.nick { 85 | return 86 | } 87 | 88 | target := server.clients.Get(msg.target) 89 | if target == nil { 90 | client.ErrNoSuchNick(msg.target) 91 | return 92 | } 93 | 94 | if server.clients.Get(msg.nick) != nil { 95 | client.ErrNickNameInUse(msg.nick) 96 | return 97 | } 98 | 99 | target.ChangeNickname(msg.nick) 100 | } 101 | -------------------------------------------------------------------------------- /irc/password.go: -------------------------------------------------------------------------------- 1 | package irc 2 | 3 | import ( 4 | "golang.org/x/crypto/bcrypt" 5 | "encoding/base64" 6 | "errors" 7 | ) 8 | 9 | var ( 10 | EmptyPasswordError = errors.New("empty password") 11 | ) 12 | 13 | func GenerateEncodedPassword(passwd string) (encoded string, err error) { 14 | if passwd == "" { 15 | err = EmptyPasswordError 16 | return 17 | } 18 | bcrypted, err := bcrypt.GenerateFromPassword([]byte(passwd), bcrypt.MinCost) 19 | if err != nil { 20 | return 21 | } 22 | encoded = base64.StdEncoding.EncodeToString(bcrypted) 23 | return 24 | } 25 | 26 | func DecodePassword(encoded string) (decoded []byte, err error) { 27 | if encoded == "" { 28 | err = EmptyPasswordError 29 | return 30 | } 31 | decoded, err = base64.StdEncoding.DecodeString(encoded) 32 | return 33 | } 34 | 35 | func ComparePassword(hash, password []byte) error { 36 | return bcrypt.CompareHashAndPassword(hash, password) 37 | } 38 | -------------------------------------------------------------------------------- /irc/reply.go: -------------------------------------------------------------------------------- 1 | package irc 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "time" 7 | ) 8 | 9 | type ReplyCode interface { 10 | String() string 11 | } 12 | 13 | type StringCode string 14 | 15 | func (code StringCode) String() string { 16 | return string(code) 17 | } 18 | 19 | type NumericCode uint 20 | 21 | func (code NumericCode) String() string { 22 | return fmt.Sprintf("%03d", code) 23 | } 24 | 25 | func NewStringReply(source Identifiable, code StringCode, 26 | format string, args ...interface{}) string { 27 | var header string 28 | if source == nil { 29 | header = code.String() + " " 30 | } else { 31 | header = fmt.Sprintf(":%s %s ", source, code) 32 | } 33 | var message string 34 | if len(args) > 0 { 35 | message = fmt.Sprintf(format, args...) 36 | } else { 37 | message = format 38 | } 39 | return header + message 40 | } 41 | 42 | func NewNumericReply(target *Client, code NumericCode, 43 | format string, args ...interface{}) string { 44 | header := fmt.Sprintf(":%s %s %s ", target.server.Id(), code, target.Nick()) 45 | var message string 46 | if len(args) > 0 { 47 | message = fmt.Sprintf(format, args...) 48 | } else { 49 | message = format 50 | } 51 | return header + message 52 | } 53 | 54 | func (target *Client) NumericReply(code NumericCode, 55 | format string, args ...interface{}) { 56 | target.Reply(NewNumericReply(target, code, format, args...)) 57 | } 58 | 59 | // 60 | // multiline replies 61 | // 62 | 63 | func joinedLen(names []string) int { 64 | var l = len(names) - 1 // " " between names 65 | for _, name := range names { 66 | l += len(name) 67 | } 68 | return l 69 | } 70 | 71 | func (target *Client) MultilineReply(names []string, code NumericCode, format string, 72 | args ...interface{}) { 73 | baseLen := len(NewNumericReply(target, code, format)) 74 | tooLong := func(names []string) bool { 75 | return (baseLen + joinedLen(names)) > MAX_REPLY_LEN 76 | } 77 | argsAndNames := func(names []string) []interface{} { 78 | return append(args, strings.Join(names, " ")) 79 | } 80 | from, to := 0, 1 81 | for to < len(names) { 82 | if (from < (to - 1)) && tooLong(names[from:to]) { 83 | target.NumericReply(code, format, argsAndNames(names[from:to-1])...) 84 | from = to - 1 85 | } else { 86 | to += 1 87 | } 88 | } 89 | if from < len(names) { 90 | target.NumericReply(code, format, argsAndNames(names[from:])...) 91 | } 92 | } 93 | 94 | // 95 | // messaging replies 96 | // 97 | 98 | func RplPrivMsg(source Identifiable, target Identifiable, message Text) string { 99 | return NewStringReply(source, PRIVMSG, "%s :%s", target.Nick(), message) 100 | } 101 | 102 | func RplCTCPAction(source Identifiable, target Identifiable, action CTCPText) string { 103 | return RplPrivMsg(source, target, NewText(fmt.Sprintf("\x01ACTION %s\x01", action))) 104 | } 105 | 106 | func RplNotice(source Identifiable, target Identifiable, message Text) string { 107 | return NewStringReply(source, NOTICE, "%s :%s", target.Nick(), message) 108 | } 109 | 110 | func RplNick(source Identifiable, newNick Name) string { 111 | return NewStringReply(source, NICK, newNick.String()) 112 | } 113 | 114 | func RplJoin(client *Client, channel *Channel) string { 115 | return NewStringReply(client, JOIN, channel.name.String()) 116 | } 117 | 118 | func RplPart(client *Client, channel *Channel, message Text) string { 119 | return NewStringReply(client, PART, "%s :%s", channel, message) 120 | } 121 | 122 | func RplModeChanges(client *Client, target *Client, changes ModeChanges) string { 123 | return NewStringReply(client, MODE, "%s :%s", target.Nick(), changes) 124 | } 125 | 126 | func RplCurrentMode(client *Client, target *Client) string { 127 | globalFlags := "global:" 128 | for mode, _ := range target.flags { 129 | globalFlags += mode.String() 130 | } 131 | 132 | perChannelFlags := "" 133 | for channel, _ := range target.channels { 134 | perChannelFlags += fmt.Sprintf(" %s:%s", channel.name, channel.members[target]) 135 | } 136 | 137 | response := NewText(fmt.Sprintf("user %s has %s%s", target.nick, globalFlags, perChannelFlags)) 138 | return RplNotice(client.server, client, response) 139 | } 140 | 141 | func RplChannelMode(client *Client, channel *Channel, 142 | changes ChannelModeChanges) string { 143 | return NewStringReply(client, MODE, "%s %s", channel, changes) 144 | } 145 | 146 | func RplTopicMsg(source Identifiable, channel *Channel) string { 147 | return NewStringReply(source, TOPIC, "%s :%s", channel, channel.topic) 148 | } 149 | 150 | func RplPing(target Identifiable) string { 151 | return NewStringReply(nil, PING, ":%s", target.Nick()) 152 | } 153 | 154 | func RplPong(client *Client, msg Text) string { 155 | // #5: IRC for Android will time out if it doesn't get the prefix back. 156 | return NewStringReply(client, PONG, "%s :%s", client.server, msg.String()) 157 | } 158 | 159 | func RplQuit(client *Client, message Text) string { 160 | return NewStringReply(client, QUIT, ":%s", message) 161 | } 162 | 163 | func RplError(message string) string { 164 | return NewStringReply(nil, ERROR, ":%s", message) 165 | } 166 | 167 | func RplInviteMsg(inviter *Client, invitee *Client, channel Name) string { 168 | return NewStringReply(inviter, INVITE, "%s :%s", invitee.Nick(), channel) 169 | } 170 | 171 | func RplKick(channel *Channel, client *Client, target *Client, comment Text) string { 172 | return NewStringReply(client, KICK, "%s %s :%s", 173 | channel, target.Nick(), comment) 174 | } 175 | 176 | func RplKill(client *Client, target *Client, comment Text) string { 177 | return NewStringReply(client, KICK, 178 | "%s :%s", target.Nick(), comment) 179 | } 180 | 181 | func RplCap(client *Client, subCommand CapSubCommand, arg interface{}) string { 182 | return NewStringReply(nil, CAP, "%s %s :%s", client.Nick(), subCommand, arg) 183 | } 184 | 185 | // numeric replies 186 | 187 | func (target *Client) RplWelcome() { 188 | target.NumericReply(RPL_WELCOME, 189 | ":Welcome to the Internet Relay Network %s", target.Id()) 190 | } 191 | 192 | func (target *Client) RplYourHost() { 193 | target.NumericReply(RPL_YOURHOST, 194 | ":Your host is %s, running version %s", target.server.name, SEM_VER) 195 | } 196 | 197 | func (target *Client) RplCreated() { 198 | target.NumericReply(RPL_CREATED, 199 | ":This server was created %s", target.server.ctime.Format(time.RFC1123)) 200 | } 201 | 202 | func (target *Client) RplMyInfo() { 203 | target.NumericReply(RPL_MYINFO, 204 | "%s %s %s %s", 205 | target.server.name, SEM_VER, SupportedUserModes, SupportedChannelModes) 206 | } 207 | 208 | func (target *Client) RplUModeIs(client *Client) { 209 | target.NumericReply(RPL_UMODEIS, client.ModeString()) 210 | } 211 | 212 | func (target *Client) RplNoTopic(channel *Channel) { 213 | target.NumericReply(RPL_NOTOPIC, 214 | "%s :No topic is set", channel.name) 215 | } 216 | 217 | func (target *Client) RplTopic(channel *Channel) { 218 | target.NumericReply(RPL_TOPIC, 219 | "%s :%s", channel.name, channel.topic) 220 | } 221 | 222 | // 223 | // NB: correction in errata 224 | func (target *Client) RplInvitingMsg(invitee *Client, channel Name) { 225 | target.NumericReply(RPL_INVITING, 226 | "%s %s", invitee.Nick(), channel) 227 | } 228 | 229 | func (target *Client) RplEndOfNames(channel *Channel) { 230 | target.NumericReply(RPL_ENDOFNAMES, 231 | "%s :End of NAMES list", channel.name) 232 | } 233 | 234 | // :You are now an IRC operator 235 | func (target *Client) RplYoureOper() { 236 | target.NumericReply(RPL_YOUREOPER, 237 | ":You are now an IRC operator") 238 | } 239 | 240 | func (target *Client) RplWhois(client *Client) { 241 | target.RplWhoisUser(client) 242 | if client.flags[Operator] { 243 | target.RplWhoisOperator(client) 244 | } 245 | target.RplWhoisIdle(client) 246 | target.RplWhoisChannels(client) 247 | target.RplEndOfWhois() 248 | } 249 | 250 | func (target *Client) RplWhoisUser(client *Client) { 251 | target.NumericReply(RPL_WHOISUSER, 252 | "%s %s %s * :%s", client.Nick(), client.username, client.hostname, 253 | client.realname) 254 | } 255 | 256 | func (target *Client) RplWhoisOperator(client *Client) { 257 | target.NumericReply(RPL_WHOISOPERATOR, 258 | "%s :is an IRC operator", client.Nick()) 259 | } 260 | 261 | func (target *Client) RplWhoisIdle(client *Client) { 262 | target.NumericReply(RPL_WHOISIDLE, 263 | "%s %d %d :seconds idle, signon time", 264 | client.Nick(), client.IdleSeconds(), client.SignonTime()) 265 | } 266 | 267 | func (target *Client) RplEndOfWhois() { 268 | target.NumericReply(RPL_ENDOFWHOIS, 269 | ":End of WHOIS list") 270 | } 271 | 272 | func (target *Client) RplChannelModeIs(channel *Channel) { 273 | target.NumericReply(RPL_CHANNELMODEIS, 274 | "%s %s", channel, channel.ModeString(target)) 275 | } 276 | 277 | // ( "H" / "G" ) ["*"] [ ( "@" / "+" ) ] 278 | // : 279 | func (target *Client) RplWhoReply(channel *Channel, client *Client) { 280 | channelName := "*" 281 | flags := "" 282 | 283 | if client.flags[Away] { 284 | flags = "G" 285 | } else { 286 | flags = "H" 287 | } 288 | if client.flags[Operator] { 289 | flags += "*" 290 | } 291 | 292 | if channel != nil { 293 | channelName = channel.name.String() 294 | if target.capabilities[MultiPrefix] { 295 | if channel.members[client][ChannelOperator] { 296 | flags += "@" 297 | } 298 | if channel.members[client][Voice] { 299 | flags += "+" 300 | } 301 | } else { 302 | if channel.members[client][ChannelOperator] { 303 | flags += "@" 304 | } else if channel.members[client][Voice] { 305 | flags += "+" 306 | } 307 | } 308 | } 309 | target.NumericReply(RPL_WHOREPLY, 310 | "%s %s %s %s %s %s :%d %s", channelName, client.username, client.hostname, 311 | client.server.name, client.Nick(), flags, client.hops, client.realname) 312 | } 313 | 314 | // :End of WHO list 315 | func (target *Client) RplEndOfWho(name Name) { 316 | target.NumericReply(RPL_ENDOFWHO, 317 | "%s :End of WHO list", name) 318 | } 319 | 320 | func (target *Client) RplMaskList(mode ChannelMode, channel *Channel, mask Name) { 321 | switch mode { 322 | case BanMask: 323 | target.RplBanList(channel, mask) 324 | 325 | case ExceptMask: 326 | target.RplExceptList(channel, mask) 327 | 328 | case InviteMask: 329 | target.RplInviteList(channel, mask) 330 | } 331 | } 332 | 333 | func (target *Client) RplEndOfMaskList(mode ChannelMode, channel *Channel) { 334 | switch mode { 335 | case BanMask: 336 | target.RplEndOfBanList(channel) 337 | 338 | case ExceptMask: 339 | target.RplEndOfExceptList(channel) 340 | 341 | case InviteMask: 342 | target.RplEndOfInviteList(channel) 343 | } 344 | } 345 | 346 | func (target *Client) RplBanList(channel *Channel, mask Name) { 347 | target.NumericReply(RPL_BANLIST, 348 | "%s %s", channel, mask) 349 | } 350 | 351 | func (target *Client) RplEndOfBanList(channel *Channel) { 352 | target.NumericReply(RPL_ENDOFBANLIST, 353 | "%s :End of channel ban list", channel) 354 | } 355 | 356 | func (target *Client) RplExceptList(channel *Channel, mask Name) { 357 | target.NumericReply(RPL_EXCEPTLIST, 358 | "%s %s", channel, mask) 359 | } 360 | 361 | func (target *Client) RplEndOfExceptList(channel *Channel) { 362 | target.NumericReply(RPL_ENDOFEXCEPTLIST, 363 | "%s :End of channel exception list", channel) 364 | } 365 | 366 | func (target *Client) RplInviteList(channel *Channel, mask Name) { 367 | target.NumericReply(RPL_INVITELIST, 368 | "%s %s", channel, mask) 369 | } 370 | 371 | func (target *Client) RplEndOfInviteList(channel *Channel) { 372 | target.NumericReply(RPL_ENDOFINVITELIST, 373 | "%s :End of channel invite list", channel) 374 | } 375 | 376 | func (target *Client) RplNowAway() { 377 | target.NumericReply(RPL_NOWAWAY, 378 | ":You have been marked as being away") 379 | } 380 | 381 | func (target *Client) RplUnAway() { 382 | target.NumericReply(RPL_UNAWAY, 383 | ":You are no longer marked as being away") 384 | } 385 | 386 | func (target *Client) RplAway(client *Client) { 387 | target.NumericReply(RPL_AWAY, 388 | "%s :%s", client.Nick(), client.awayMessage) 389 | } 390 | 391 | func (target *Client) RplIsOn(nicks []string) { 392 | target.NumericReply(RPL_ISON, 393 | ":%s", strings.Join(nicks, " ")) 394 | } 395 | 396 | func (target *Client) RplMOTDStart() { 397 | target.NumericReply(RPL_MOTDSTART, 398 | ":- %s Message of the day - ", target.server.name) 399 | } 400 | 401 | func (target *Client) RplMOTD(line string) { 402 | target.NumericReply(RPL_MOTD, 403 | ":- %s", line) 404 | } 405 | 406 | func (target *Client) RplMOTDEnd() { 407 | target.NumericReply(RPL_ENDOFMOTD, 408 | ":End of MOTD command") 409 | } 410 | 411 | func (target *Client) RplList(channel *Channel) { 412 | target.NumericReply(RPL_LIST, 413 | "%s %d :%s", channel, len(channel.members), channel.topic) 414 | } 415 | 416 | func (target *Client) RplListEnd(server *Server) { 417 | target.NumericReply(RPL_LISTEND, 418 | ":End of LIST") 419 | } 420 | 421 | func (target *Client) RplNamReply(channel *Channel) { 422 | target.MultilineReply(channel.Nicks(target), RPL_NAMREPLY, 423 | "= %s :%s", channel) 424 | } 425 | 426 | func (target *Client) RplWhoisChannels(client *Client) { 427 | target.MultilineReply(client.WhoisChannelsNames(), RPL_WHOISCHANNELS, 428 | "%s :%s", client.Nick()) 429 | } 430 | 431 | func (target *Client) RplVersion() { 432 | target.NumericReply(RPL_VERSION, 433 | "%s %s", SEM_VER, target.server.name) 434 | } 435 | 436 | func (target *Client) RplInviting(invitee *Client, channel Name) { 437 | target.NumericReply(RPL_INVITING, 438 | "%s %s", invitee.Nick(), channel) 439 | } 440 | 441 | func (target *Client) RplTime() { 442 | target.NumericReply(RPL_TIME, 443 | "%s :%s", target.server.name, time.Now().Format(time.RFC1123)) 444 | } 445 | 446 | func (target *Client) RplWhoWasUser(whoWas *WhoWas) { 447 | target.NumericReply(RPL_WHOWASUSER, 448 | "%s %s %s * :%s", 449 | whoWas.nickname, whoWas.username, whoWas.hostname, whoWas.realname) 450 | } 451 | 452 | func (target *Client) RplEndOfWhoWas(nickname Name) { 453 | target.NumericReply(RPL_ENDOFWHOWAS, 454 | "%s :End of WHOWAS", nickname) 455 | } 456 | 457 | // 458 | // errors (also numeric) 459 | // 460 | 461 | func (target *Client) ErrAlreadyRegistered() { 462 | target.NumericReply(ERR_ALREADYREGISTRED, 463 | ":You may not reregister") 464 | } 465 | 466 | func (target *Client) ErrNickNameInUse(nick Name) { 467 | target.NumericReply(ERR_NICKNAMEINUSE, 468 | "%s :Nickname is already in use", nick) 469 | } 470 | 471 | func (target *Client) ErrUnknownCommand(code StringCode) { 472 | target.NumericReply(ERR_UNKNOWNCOMMAND, 473 | "%s :Unknown command", code) 474 | } 475 | 476 | func (target *Client) ErrUsersDontMatch() { 477 | target.NumericReply(ERR_USERSDONTMATCH, 478 | ":Cannot change mode for other users") 479 | } 480 | 481 | func (target *Client) ErrNeedMoreParams(command StringCode) { 482 | target.NumericReply(ERR_NEEDMOREPARAMS, 483 | "%s :Not enough parameters", command) 484 | } 485 | 486 | func (target *Client) ErrNoSuchChannel(channel Name) { 487 | target.NumericReply(ERR_NOSUCHCHANNEL, 488 | "%s :No such channel", channel) 489 | } 490 | 491 | func (target *Client) ErrUserOnChannel(channel *Channel, member *Client) { 492 | target.NumericReply(ERR_USERONCHANNEL, 493 | "%s %s :is already on channel", member.Nick(), channel.name) 494 | } 495 | 496 | func (target *Client) ErrNotOnChannel(channel *Channel) { 497 | target.NumericReply(ERR_NOTONCHANNEL, 498 | "%s :You're not on that channel", channel.name) 499 | } 500 | 501 | func (target *Client) ErrInviteOnlyChannel(channel *Channel) { 502 | target.NumericReply(ERR_INVITEONLYCHAN, 503 | "%s :Cannot join channel (+i)", channel.name) 504 | } 505 | 506 | func (target *Client) ErrBadChannelKey(channel *Channel) { 507 | target.NumericReply(ERR_BADCHANNELKEY, 508 | "%s :Cannot join channel (+k)", channel.name) 509 | } 510 | 511 | func (target *Client) ErrNoSuchNick(nick Name) { 512 | target.NumericReply(ERR_NOSUCHNICK, 513 | "%s :No such nick/channel", nick) 514 | } 515 | 516 | func (target *Client) ErrPasswdMismatch() { 517 | target.NumericReply(ERR_PASSWDMISMATCH, ":Password incorrect") 518 | } 519 | 520 | func (target *Client) ErrNoChanModes(channel *Channel) { 521 | target.NumericReply(ERR_NOCHANMODES, 522 | "%s :Channel doesn't support modes", channel) 523 | } 524 | 525 | func (target *Client) ErrNoPrivileges() { 526 | target.NumericReply(ERR_NOPRIVILEGES, ":Permission Denied") 527 | } 528 | 529 | func (target *Client) ErrRestricted() { 530 | target.NumericReply(ERR_RESTRICTED, ":Your connection is restricted!") 531 | } 532 | 533 | func (target *Client) ErrNoSuchServer(server Name) { 534 | target.NumericReply(ERR_NOSUCHSERVER, "%s :No such server", server) 535 | } 536 | 537 | func (target *Client) ErrUserNotInChannel(channel *Channel, client *Client) { 538 | target.NumericReply(ERR_USERNOTINCHANNEL, 539 | "%s %s :They aren't on that channel", client.Nick(), channel) 540 | } 541 | 542 | func (target *Client) ErrCannotSendToChan(channel *Channel) { 543 | target.NumericReply(ERR_CANNOTSENDTOCHAN, 544 | "%s :Cannot send to channel", channel) 545 | } 546 | 547 | // :You're not channel operator 548 | func (target *Client) ErrChanOPrivIsNeeded(channel *Channel) { 549 | target.NumericReply(ERR_CHANOPRIVSNEEDED, 550 | "%s :You're not channel operator", channel) 551 | } 552 | 553 | func (target *Client) ErrNoMOTD() { 554 | target.NumericReply(ERR_NOMOTD, ":MOTD File is missing") 555 | } 556 | 557 | func (target *Client) ErrNoNicknameGiven() { 558 | target.NumericReply(ERR_NONICKNAMEGIVEN, ":No nickname given") 559 | } 560 | 561 | func (target *Client) ErrErroneusNickname(nick Name) { 562 | target.NumericReply(ERR_ERRONEUSNICKNAME, 563 | "%s :Erroneous nickname", nick) 564 | } 565 | 566 | func (target *Client) ErrUnknownMode(mode ChannelMode, channel *Channel) { 567 | target.NumericReply(ERR_UNKNOWNMODE, 568 | "%s :is unknown mode char to me for %s", mode, channel) 569 | } 570 | 571 | func (target *Client) ErrConfiguredMode(mode ChannelMode) { 572 | target.NumericReply(ERR_UNKNOWNMODE, 573 | "%s :can only change this mode in daemon configuration", mode) 574 | } 575 | 576 | func (target *Client) ErrChannelIsFull(channel *Channel) { 577 | target.NumericReply(ERR_CHANNELISFULL, 578 | "%s :Cannot join channel (+l)", channel) 579 | } 580 | 581 | func (target *Client) ErrWasNoSuchNick(nickname Name) { 582 | target.NumericReply(ERR_WASNOSUCHNICK, 583 | "%s :There was no such nickname", nickname) 584 | } 585 | 586 | func (target *Client) ErrInvalidCapCmd(subCommand CapSubCommand) { 587 | target.NumericReply(ERR_INVALIDCAPCMD, 588 | "%s :Invalid CAP subcommand", subCommand) 589 | } 590 | 591 | func (target *Client) ErrBannedFromChan(channel *Channel) { 592 | target.NumericReply(ERR_BANNEDFROMCHAN, 593 | "%s :Cannot join channel (+b)", channel) 594 | } 595 | 596 | func (target *Client) ErrInviteOnlyChan(channel *Channel) { 597 | target.NumericReply(ERR_INVITEONLYCHAN, 598 | "%s :Cannot join channel (+i)", channel) 599 | } 600 | -------------------------------------------------------------------------------- /irc/server.go: -------------------------------------------------------------------------------- 1 | package irc 2 | 3 | import ( 4 | "bufio" 5 | "database/sql" 6 | "fmt" 7 | "log" 8 | "net" 9 | "net/http" 10 | "os" 11 | "os/signal" 12 | "strings" 13 | "syscall" 14 | "time" 15 | ) 16 | 17 | type ServerCommand interface { 18 | Command 19 | HandleServer(*Server) 20 | } 21 | 22 | type RegServerCommand interface { 23 | Command 24 | HandleRegServer(*Server) 25 | } 26 | 27 | type Server struct { 28 | channels ChannelNameMap 29 | clients *ClientLookupSet 30 | commands chan Command 31 | ctime time.Time 32 | db *sql.DB 33 | idle chan *Client 34 | motdFile string 35 | name Name 36 | newConns chan net.Conn 37 | operators map[Name][]byte 38 | password []byte 39 | signals chan os.Signal 40 | whoWas *WhoWasList 41 | theaters map[Name][]byte 42 | } 43 | 44 | var ( 45 | SERVER_SIGNALS = []os.Signal{syscall.SIGINT, syscall.SIGHUP, 46 | syscall.SIGTERM, syscall.SIGQUIT} 47 | ) 48 | 49 | func NewServer(config *Config) *Server { 50 | server := &Server{ 51 | channels: make(ChannelNameMap), 52 | clients: NewClientLookupSet(), 53 | commands: make(chan Command), 54 | ctime: time.Now(), 55 | db: OpenDB(config.Server.Database), 56 | idle: make(chan *Client), 57 | motdFile: config.Server.MOTD, 58 | name: NewName(config.Server.Name), 59 | newConns: make(chan net.Conn), 60 | operators: config.Operators(), 61 | signals: make(chan os.Signal, len(SERVER_SIGNALS)), 62 | whoWas: NewWhoWasList(100), 63 | theaters: config.Theaters(), 64 | } 65 | 66 | if config.Server.Password != "" { 67 | server.password = config.Server.PasswordBytes() 68 | } 69 | 70 | server.loadChannels() 71 | 72 | for _, addr := range config.Server.Listen { 73 | server.listen(addr) 74 | } 75 | 76 | if config.Server.Wslisten != "" { 77 | server.wslisten(config.Server.Wslisten) 78 | } 79 | 80 | signal.Notify(server.signals, SERVER_SIGNALS...) 81 | 82 | return server 83 | } 84 | 85 | func loadChannelList(channel *Channel, list string, maskMode ChannelMode) { 86 | if list == "" { 87 | return 88 | } 89 | channel.lists[maskMode].AddAll(NewNames(strings.Split(list, " "))) 90 | } 91 | 92 | func (server *Server) loadChannels() { 93 | rows, err := server.db.Query(` 94 | SELECT name, flags, key, topic, user_limit, ban_list, except_list, 95 | invite_list 96 | FROM channel`) 97 | if err != nil { 98 | log.Fatal("error loading channels: ", err) 99 | } 100 | for rows.Next() { 101 | var name, flags, key, topic string 102 | var userLimit uint64 103 | var banList, exceptList, inviteList string 104 | err = rows.Scan(&name, &flags, &key, &topic, &userLimit, &banList, 105 | &exceptList, &inviteList) 106 | if err != nil { 107 | log.Println("Server.loadChannels:", err) 108 | continue 109 | } 110 | 111 | channel := NewChannel(server, NewName(name)) 112 | for _, flag := range flags { 113 | channel.flags[ChannelMode(flag)] = true 114 | } 115 | channel.key = NewText(key) 116 | channel.topic = NewText(topic) 117 | channel.userLimit = userLimit 118 | loadChannelList(channel, banList, BanMask) 119 | loadChannelList(channel, exceptList, ExceptMask) 120 | loadChannelList(channel, inviteList, InviteMask) 121 | } 122 | } 123 | 124 | func (server *Server) processCommand(cmd Command) { 125 | client := cmd.Client() 126 | 127 | if !client.registered { 128 | regCmd, ok := cmd.(RegServerCommand) 129 | if !ok { 130 | client.Quit("unexpected command") 131 | return 132 | } 133 | regCmd.HandleRegServer(server) 134 | return 135 | } 136 | 137 | srvCmd, ok := cmd.(ServerCommand) 138 | if !ok { 139 | client.ErrUnknownCommand(cmd.Code()) 140 | return 141 | } 142 | 143 | switch srvCmd.(type) { 144 | case *PingCommand, *PongCommand: 145 | client.Touch() 146 | 147 | case *QuitCommand: 148 | // no-op 149 | 150 | default: 151 | client.Active() 152 | client.Touch() 153 | } 154 | 155 | srvCmd.HandleServer(server) 156 | } 157 | 158 | func (server *Server) Shutdown() { 159 | server.db.Close() 160 | for _, client := range server.clients.byNick { 161 | client.Reply(RplNotice(server, client, "shutting down")) 162 | } 163 | } 164 | 165 | func (server *Server) Run() { 166 | done := false 167 | for !done { 168 | select { 169 | case <-server.signals: 170 | server.Shutdown() 171 | done = true 172 | 173 | case conn := <-server.newConns: 174 | NewClient(server, conn) 175 | 176 | case cmd := <-server.commands: 177 | server.processCommand(cmd) 178 | 179 | case client := <-server.idle: 180 | client.Idle() 181 | } 182 | } 183 | } 184 | 185 | // 186 | // listen goroutine 187 | // 188 | 189 | func (s *Server) listen(addr string) { 190 | listener, err := net.Listen("tcp", addr) 191 | if err != nil { 192 | log.Fatal(s, "listen error: ", err) 193 | } 194 | 195 | Log.info.Printf("%s listening on %s", s, addr) 196 | 197 | go func() { 198 | for { 199 | conn, err := listener.Accept() 200 | if err != nil { 201 | Log.error.Printf("%s accept error: %s", s, err) 202 | continue 203 | } 204 | Log.debug.Printf("%s accept: %s", s, conn.RemoteAddr()) 205 | 206 | s.newConns <- conn 207 | } 208 | }() 209 | } 210 | 211 | // 212 | // websocket listen goroutine 213 | // 214 | 215 | func (s *Server) wslisten(addr string) { 216 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 217 | if r.Method != "GET" { 218 | Log.error.Printf("%s method not allowed", s) 219 | return 220 | } 221 | 222 | // We don't have any subprotocols, so if someone attempts to `new 223 | // WebSocket(server, "subprotocol")` they'll break here, instead of 224 | // getting the default, ambiguous, response from gorilla. 225 | if v, ok := r.Header["Sec-Websocket-Protocol"]; ok { 226 | http.Error(w, fmt.Sprintf("WebSocket subprocotols (e.g. %s) not supported", v), 400) 227 | } 228 | 229 | ws, err := upgrader.Upgrade(w, r, nil) 230 | if err != nil { 231 | Log.error.Printf("%s websocket upgrade error: %s", s, err) 232 | return 233 | } 234 | 235 | s.newConns <- WSContainer{ws} 236 | }) 237 | go func() { 238 | Log.info.Printf("%s listening on %s", s, addr) 239 | err := http.ListenAndServe(addr, nil) 240 | if err != nil { 241 | Log.error.Printf("%s listenAndServe error: %s", s, err) 242 | } 243 | }() 244 | } 245 | 246 | // 247 | // server functionality 248 | // 249 | 250 | func (s *Server) tryRegister(c *Client) { 251 | if c.registered || !c.HasNick() || !c.HasUsername() || 252 | (c.capState == CapNegotiating) { 253 | return 254 | } 255 | 256 | c.Register() 257 | c.RplWelcome() 258 | c.RplYourHost() 259 | c.RplCreated() 260 | c.RplMyInfo() 261 | s.MOTD(c) 262 | } 263 | 264 | func (server *Server) MOTD(client *Client) { 265 | if server.motdFile == "" { 266 | client.ErrNoMOTD() 267 | return 268 | } 269 | 270 | file, err := os.Open(server.motdFile) 271 | if err != nil { 272 | client.ErrNoMOTD() 273 | return 274 | } 275 | defer file.Close() 276 | 277 | client.RplMOTDStart() 278 | reader := bufio.NewReader(file) 279 | for { 280 | line, err := reader.ReadString('\n') 281 | if err != nil { 282 | break 283 | } 284 | line = strings.TrimRight(line, "\r\n") 285 | 286 | client.RplMOTD(line) 287 | } 288 | client.RplMOTDEnd() 289 | } 290 | 291 | func (s *Server) Id() Name { 292 | return s.name 293 | } 294 | 295 | func (s *Server) String() string { 296 | return s.name.String() 297 | } 298 | 299 | func (s *Server) Nick() Name { 300 | return s.Id() 301 | } 302 | 303 | func (server *Server) Reply(target *Client, message string) { 304 | target.Reply(RplPrivMsg(server, target, NewText(message))) 305 | } 306 | 307 | func (server *Server) Replyf(target *Client, format string, args ...interface{}) { 308 | server.Reply(target, fmt.Sprintf(format, args...)) 309 | } 310 | 311 | // 312 | // registration commands 313 | // 314 | 315 | func (msg *PassCommand) HandleRegServer(server *Server) { 316 | client := msg.Client() 317 | if msg.err != nil { 318 | client.ErrPasswdMismatch() 319 | client.Quit("bad password") 320 | return 321 | } 322 | 323 | client.authorized = true 324 | } 325 | 326 | func (msg *ProxyCommand) HandleRegServer(server *Server) { 327 | msg.Client().hostname = msg.hostname 328 | } 329 | 330 | func (msg *RFC1459UserCommand) HandleRegServer(server *Server) { 331 | client := msg.Client() 332 | if !client.authorized { 333 | client.ErrPasswdMismatch() 334 | client.Quit("bad password") 335 | return 336 | } 337 | msg.setUserInfo(server) 338 | } 339 | 340 | func (msg *RFC2812UserCommand) HandleRegServer(server *Server) { 341 | client := msg.Client() 342 | if !client.authorized { 343 | client.ErrPasswdMismatch() 344 | client.Quit("bad password") 345 | return 346 | } 347 | flags := msg.Flags() 348 | if len(flags) > 0 { 349 | for _, mode := range flags { 350 | client.flags[mode] = true 351 | } 352 | client.RplUModeIs(client) 353 | } 354 | msg.setUserInfo(server) 355 | } 356 | 357 | func (msg *UserCommand) setUserInfo(server *Server) { 358 | client := msg.Client() 359 | if client.capState == CapNegotiating { 360 | client.capState = CapNegotiated 361 | } 362 | 363 | server.clients.Remove(client) 364 | client.username, client.realname = msg.username, msg.realname 365 | server.clients.Add(client) 366 | 367 | server.tryRegister(client) 368 | } 369 | 370 | func (msg *QuitCommand) HandleRegServer(server *Server) { 371 | msg.Client().Quit(msg.message) 372 | } 373 | 374 | // 375 | // normal commands 376 | // 377 | 378 | func (m *PassCommand) HandleServer(s *Server) { 379 | m.Client().ErrAlreadyRegistered() 380 | } 381 | 382 | func (m *PingCommand) HandleServer(s *Server) { 383 | client := m.Client() 384 | client.Reply(RplPong(client, m.server.Text())) 385 | } 386 | 387 | func (m *PongCommand) HandleServer(s *Server) { 388 | // no-op 389 | } 390 | 391 | func (m *UserCommand) HandleServer(s *Server) { 392 | m.Client().ErrAlreadyRegistered() 393 | } 394 | 395 | func (msg *QuitCommand) HandleServer(server *Server) { 396 | msg.Client().Quit(msg.message) 397 | } 398 | 399 | func (m *JoinCommand) HandleServer(s *Server) { 400 | client := m.Client() 401 | 402 | if m.zero { 403 | for channel := range client.channels { 404 | channel.Part(client, client.Nick().Text()) 405 | } 406 | return 407 | } 408 | 409 | for name, key := range m.channels { 410 | if !name.IsChannel() { 411 | client.ErrNoSuchChannel(name) 412 | continue 413 | } 414 | 415 | channel := s.channels.Get(name) 416 | if channel == nil { 417 | channel = NewChannel(s, name) 418 | } 419 | channel.Join(client, key) 420 | } 421 | } 422 | 423 | func (m *PartCommand) HandleServer(server *Server) { 424 | client := m.Client() 425 | for _, chname := range m.channels { 426 | channel := server.channels.Get(chname) 427 | 428 | if channel == nil { 429 | m.Client().ErrNoSuchChannel(chname) 430 | continue 431 | } 432 | 433 | channel.Part(client, m.Message()) 434 | } 435 | } 436 | 437 | func (msg *TopicCommand) HandleServer(server *Server) { 438 | client := msg.Client() 439 | channel := server.channels.Get(msg.channel) 440 | if channel == nil { 441 | client.ErrNoSuchChannel(msg.channel) 442 | return 443 | } 444 | 445 | if msg.setTopic { 446 | channel.SetTopic(client, msg.topic) 447 | } else { 448 | channel.GetTopic(client) 449 | } 450 | } 451 | 452 | func (msg *PrivMsgCommand) HandleServer(server *Server) { 453 | client := msg.Client() 454 | if msg.target.IsChannel() { 455 | channel := server.channels.Get(msg.target) 456 | if channel == nil { 457 | client.ErrNoSuchChannel(msg.target) 458 | return 459 | } 460 | 461 | channel.PrivMsg(client, msg.message) 462 | return 463 | } 464 | 465 | target := server.clients.Get(msg.target) 466 | if target == nil { 467 | client.ErrNoSuchNick(msg.target) 468 | return 469 | } 470 | target.Reply(RplPrivMsg(client, target, msg.message)) 471 | if target.flags[Away] { 472 | client.RplAway(target) 473 | } 474 | } 475 | 476 | func (client *Client) WhoisChannelsNames() []string { 477 | chstrs := make([]string, len(client.channels)) 478 | index := 0 479 | for channel := range client.channels { 480 | switch { 481 | case channel.members[client][ChannelOperator]: 482 | chstrs[index] = "@" + channel.name.String() 483 | 484 | case channel.members[client][Voice]: 485 | chstrs[index] = "+" + channel.name.String() 486 | 487 | default: 488 | chstrs[index] = channel.name.String() 489 | } 490 | index += 1 491 | } 492 | return chstrs 493 | } 494 | 495 | func (m *WhoisCommand) HandleServer(server *Server) { 496 | client := m.Client() 497 | 498 | // TODO implement target query 499 | 500 | for _, mask := range m.masks { 501 | matches := server.clients.FindAll(mask) 502 | if len(matches) == 0 { 503 | client.ErrNoSuchNick(mask) 504 | continue 505 | } 506 | for mclient := range matches { 507 | client.RplWhois(mclient) 508 | } 509 | } 510 | } 511 | 512 | func whoChannel(client *Client, channel *Channel, friends ClientSet) { 513 | for member := range channel.members { 514 | if !client.flags[Invisible] || friends[client] { 515 | client.RplWhoReply(channel, member) 516 | } 517 | } 518 | } 519 | 520 | func (msg *WhoCommand) HandleServer(server *Server) { 521 | client := msg.Client() 522 | friends := client.Friends() 523 | mask := msg.mask 524 | 525 | if mask == "" { 526 | for _, channel := range server.channels { 527 | whoChannel(client, channel, friends) 528 | } 529 | } else if mask.IsChannel() { 530 | // TODO implement wildcard matching 531 | channel := server.channels.Get(mask) 532 | if channel != nil { 533 | whoChannel(client, channel, friends) 534 | } 535 | } else { 536 | for mclient := range server.clients.FindAll(mask) { 537 | client.RplWhoReply(nil, mclient) 538 | } 539 | } 540 | 541 | client.RplEndOfWho(mask) 542 | } 543 | 544 | func (msg *OperCommand) HandleServer(server *Server) { 545 | client := msg.Client() 546 | 547 | if (msg.hash == nil) || (msg.err != nil) { 548 | client.ErrPasswdMismatch() 549 | return 550 | } 551 | 552 | client.flags[Operator] = true 553 | client.RplYoureOper() 554 | client.Reply(RplModeChanges(client, client, ModeChanges{&ModeChange{ 555 | mode: Operator, 556 | op: Add, 557 | }})) 558 | } 559 | 560 | func (msg *AwayCommand) HandleServer(server *Server) { 561 | client := msg.Client() 562 | if len(msg.text) > 0 { 563 | client.flags[Away] = true 564 | } else { 565 | delete(client.flags, Away) 566 | } 567 | client.awayMessage = msg.text 568 | 569 | var op ModeOp 570 | if client.flags[Away] { 571 | op = Add 572 | client.RplNowAway() 573 | } else { 574 | op = Remove 575 | client.RplUnAway() 576 | } 577 | client.Reply(RplModeChanges(client, client, ModeChanges{&ModeChange{ 578 | mode: Away, 579 | op: op, 580 | }})) 581 | } 582 | 583 | func (msg *IsOnCommand) HandleServer(server *Server) { 584 | client := msg.Client() 585 | 586 | ison := make([]string, 0) 587 | for _, nick := range msg.nicks { 588 | if iclient := server.clients.Get(nick); iclient != nil { 589 | ison = append(ison, iclient.Nick().String()) 590 | } 591 | } 592 | 593 | client.RplIsOn(ison) 594 | } 595 | 596 | func (msg *MOTDCommand) HandleServer(server *Server) { 597 | server.MOTD(msg.Client()) 598 | } 599 | 600 | func (msg *NoticeCommand) HandleServer(server *Server) { 601 | client := msg.Client() 602 | if msg.target.IsChannel() { 603 | channel := server.channels.Get(msg.target) 604 | if channel == nil { 605 | client.ErrNoSuchChannel(msg.target) 606 | return 607 | } 608 | 609 | channel.Notice(client, msg.message) 610 | return 611 | } 612 | 613 | target := server.clients.Get(msg.target) 614 | if target == nil { 615 | client.ErrNoSuchNick(msg.target) 616 | return 617 | } 618 | target.Reply(RplNotice(client, target, msg.message)) 619 | } 620 | 621 | func (msg *KickCommand) HandleServer(server *Server) { 622 | client := msg.Client() 623 | for chname, nickname := range msg.kicks { 624 | channel := server.channels.Get(chname) 625 | if channel == nil { 626 | client.ErrNoSuchChannel(chname) 627 | continue 628 | } 629 | 630 | target := server.clients.Get(nickname) 631 | if target == nil { 632 | client.ErrNoSuchNick(nickname) 633 | continue 634 | } 635 | 636 | channel.Kick(client, target, msg.Comment()) 637 | } 638 | } 639 | 640 | func (msg *ListCommand) HandleServer(server *Server) { 641 | client := msg.Client() 642 | 643 | // TODO target server 644 | if msg.target != "" { 645 | client.ErrNoSuchServer(msg.target) 646 | return 647 | } 648 | 649 | if len(msg.channels) == 0 { 650 | for _, channel := range server.channels { 651 | if !client.flags[Operator] && channel.flags[Private] { 652 | continue 653 | } 654 | client.RplList(channel) 655 | } 656 | } else { 657 | for _, chname := range msg.channels { 658 | channel := server.channels.Get(chname) 659 | if channel == nil || (!client.flags[Operator] && channel.flags[Private]) { 660 | client.ErrNoSuchChannel(chname) 661 | continue 662 | } 663 | client.RplList(channel) 664 | } 665 | } 666 | client.RplListEnd(server) 667 | } 668 | 669 | func (msg *NamesCommand) HandleServer(server *Server) { 670 | client := msg.Client() 671 | if len(server.channels) == 0 { 672 | for _, channel := range server.channels { 673 | channel.Names(client) 674 | } 675 | return 676 | } 677 | 678 | for _, chname := range msg.channels { 679 | channel := server.channels.Get(chname) 680 | if channel == nil { 681 | client.ErrNoSuchChannel(chname) 682 | continue 683 | } 684 | channel.Names(client) 685 | } 686 | } 687 | 688 | func (msg *VersionCommand) HandleServer(server *Server) { 689 | client := msg.Client() 690 | if (msg.target != "") && (msg.target != server.name) { 691 | client.ErrNoSuchServer(msg.target) 692 | return 693 | } 694 | 695 | client.RplVersion() 696 | } 697 | 698 | func (msg *InviteCommand) HandleServer(server *Server) { 699 | client := msg.Client() 700 | 701 | target := server.clients.Get(msg.nickname) 702 | if target == nil { 703 | client.ErrNoSuchNick(msg.nickname) 704 | return 705 | } 706 | 707 | channel := server.channels.Get(msg.channel) 708 | if channel == nil { 709 | client.RplInviting(target, msg.channel) 710 | target.Reply(RplInviteMsg(client, target, msg.channel)) 711 | return 712 | } 713 | 714 | channel.Invite(target, client) 715 | } 716 | 717 | func (msg *TimeCommand) HandleServer(server *Server) { 718 | client := msg.Client() 719 | if (msg.target != "") && (msg.target != server.name) { 720 | client.ErrNoSuchServer(msg.target) 721 | return 722 | } 723 | client.RplTime() 724 | } 725 | 726 | func (msg *KillCommand) HandleServer(server *Server) { 727 | client := msg.Client() 728 | if !client.flags[Operator] { 729 | client.ErrNoPrivileges() 730 | return 731 | } 732 | 733 | target := server.clients.Get(msg.nickname) 734 | if target == nil { 735 | client.ErrNoSuchNick(msg.nickname) 736 | return 737 | } 738 | 739 | quitMsg := fmt.Sprintf("KILLed by %s: %s", client.Nick(), msg.comment) 740 | target.Quit(NewText(quitMsg)) 741 | } 742 | 743 | func (msg *WhoWasCommand) HandleServer(server *Server) { 744 | client := msg.Client() 745 | for _, nickname := range msg.nicknames { 746 | results := server.whoWas.Find(nickname, msg.count) 747 | if len(results) == 0 { 748 | client.ErrWasNoSuchNick(nickname) 749 | } else { 750 | for _, whoWas := range results { 751 | client.RplWhoWasUser(whoWas) 752 | } 753 | } 754 | client.RplEndOfWhoWas(nickname) 755 | } 756 | } 757 | -------------------------------------------------------------------------------- /irc/socket.go: -------------------------------------------------------------------------------- 1 | package irc 2 | 3 | import ( 4 | "bufio" 5 | "io" 6 | "net" 7 | ) 8 | 9 | const ( 10 | R = '→' 11 | W = '←' 12 | ) 13 | 14 | type Socket struct { 15 | closed bool 16 | conn net.Conn 17 | scanner *bufio.Scanner 18 | writer *bufio.Writer 19 | } 20 | 21 | func NewSocket(conn net.Conn) *Socket { 22 | return &Socket{ 23 | conn: conn, 24 | scanner: bufio.NewScanner(conn), 25 | writer: bufio.NewWriter(conn), 26 | } 27 | } 28 | 29 | func (socket *Socket) String() string { 30 | return socket.conn.RemoteAddr().String() 31 | } 32 | 33 | func (socket *Socket) Close() { 34 | if socket.closed { 35 | return 36 | } 37 | socket.closed = true 38 | socket.conn.Close() 39 | Log.debug.Printf("%s closed", socket) 40 | } 41 | 42 | func (socket *Socket) Read() (line string, err error) { 43 | if socket.closed { 44 | err = io.EOF 45 | return 46 | } 47 | 48 | for socket.scanner.Scan() { 49 | line = socket.scanner.Text() 50 | if len(line) == 0 { 51 | continue 52 | } 53 | Log.debug.Printf("%s → %s", socket, line) 54 | return 55 | } 56 | 57 | err = socket.scanner.Err() 58 | socket.isError(err, R) 59 | if err == nil { 60 | err = io.EOF 61 | } 62 | return 63 | } 64 | 65 | func (socket *Socket) Write(line string) (err error) { 66 | if socket.closed { 67 | err = io.EOF 68 | return 69 | } 70 | 71 | if _, err = socket.writer.WriteString(line); socket.isError(err, W) { 72 | return 73 | } 74 | 75 | if _, err = socket.writer.WriteString(CRLF); socket.isError(err, W) { 76 | return 77 | } 78 | 79 | if err = socket.writer.Flush(); socket.isError(err, W) { 80 | return 81 | } 82 | 83 | Log.debug.Printf("%s ← %s", socket, line) 84 | return 85 | } 86 | 87 | func (socket *Socket) isError(err error, dir rune) bool { 88 | if err != nil { 89 | if err != io.EOF { 90 | Log.debug.Printf("%s %c error: %s", socket, dir, err) 91 | } 92 | return true 93 | } 94 | return false 95 | } 96 | -------------------------------------------------------------------------------- /irc/strings.go: -------------------------------------------------------------------------------- 1 | package irc 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | 7 | "golang.org/x/text/unicode/norm" 8 | ) 9 | 10 | var ( 11 | // regexps 12 | ChannelNameExpr = regexp.MustCompile(`^[&!#+][\pL\pN]{1,63}$`) 13 | NicknameExpr = regexp.MustCompile("^[\\pL\\pN\\pP\\pS]{1,32}$") 14 | ) 15 | 16 | // Names are normalized and canonicalized to remove formatting marks 17 | // and simplify usage. They are things like hostnames and usermasks. 18 | type Name string 19 | 20 | func NewName(str string) Name { 21 | return Name(norm.NFKC.String(str)) 22 | } 23 | 24 | func NewNames(strs []string) []Name { 25 | names := make([]Name, len(strs)) 26 | for index, str := range strs { 27 | names[index] = NewName(str) 28 | } 29 | return names 30 | } 31 | 32 | // tests 33 | 34 | func (name Name) IsChannel() bool { 35 | return ChannelNameExpr.MatchString(name.String()) 36 | } 37 | 38 | func (name Name) IsNickname() bool { 39 | namestr := name.String() 40 | // * is used for unregistered clients 41 | // , is used as a separator by the protocol 42 | // # is a channel prefix 43 | // @+ are channel membership prefixes 44 | if namestr == "*" || strings.Contains(namestr, ",") || strings.Contains("#@+", string(namestr[0])) { 45 | return false 46 | } 47 | return NicknameExpr.MatchString(namestr) 48 | } 49 | 50 | // conversions 51 | 52 | func (name Name) String() string { 53 | return string(name) 54 | } 55 | 56 | func (name Name) ToLower() Name { 57 | return Name(strings.ToLower(name.String())) 58 | } 59 | 60 | // It's safe to coerce a Name to Text. Name is a strict subset of Text. 61 | func (name Name) Text() Text { 62 | return Text(name) 63 | } 64 | 65 | // Text is PRIVMSG, NOTICE, or TOPIC data. It's canonicalized UTF8 66 | // data to simplify but keeps all formatting. 67 | type Text string 68 | 69 | func NewText(str string) Text { 70 | return Text(norm.NFC.String(str)) 71 | } 72 | 73 | func (text Text) String() string { 74 | return string(text) 75 | } 76 | 77 | // CTCPText is text suitably escaped for CTCP. 78 | type CTCPText string 79 | 80 | var ctcpEscaper = strings.NewReplacer("\x00", "\x200", "\n", "\x20n", "\r", "\x20r") 81 | 82 | func NewCTCPText(str string) CTCPText { 83 | return CTCPText(ctcpEscaper.Replace(str)) 84 | } 85 | -------------------------------------------------------------------------------- /irc/theater.go: -------------------------------------------------------------------------------- 1 | package irc 2 | 3 | type TheaterClient Name 4 | 5 | func (c TheaterClient) Id() Name { 6 | return Name(c) 7 | } 8 | 9 | func (c TheaterClient) Nick() Name { 10 | return Name(c) 11 | } 12 | 13 | type TheaterSubCommand string 14 | 15 | type theaterSubCommand interface { 16 | String() string 17 | } 18 | 19 | type TheaterIdentifyCommand struct { 20 | PassCommand 21 | channel Name 22 | } 23 | 24 | func (m *TheaterIdentifyCommand) LoadPassword(s *Server) { 25 | m.hash = s.theaters[m.channel] 26 | } 27 | 28 | func (m *TheaterIdentifyCommand) HandleServer(s *Server) { 29 | client := m.Client() 30 | if !m.channel.IsChannel() { 31 | client.ErrNoSuchChannel(m.channel) 32 | return 33 | } 34 | 35 | channel := s.channels.Get(m.channel) 36 | if channel == nil { 37 | client.ErrNoSuchChannel(m.channel) 38 | return 39 | } 40 | 41 | if (m.hash == nil) || (m.err != nil) { 42 | client.ErrPasswdMismatch() 43 | return 44 | } 45 | 46 | if channel.members.AnyHasMode(Theater) { 47 | client.Reply(RplNotice(s, client, "someone else is +T in this channel")) 48 | return 49 | } 50 | 51 | channel.members[client][Theater] = true 52 | } 53 | 54 | type TheaterPrivMsgCommand struct { 55 | BaseCommand 56 | channel Name 57 | asNick Name 58 | message Text 59 | } 60 | 61 | func (m *TheaterPrivMsgCommand) HandleServer(s *Server) { 62 | client := m.Client() 63 | 64 | if !m.channel.IsChannel() { 65 | client.ErrNoSuchChannel(m.channel) 66 | return 67 | } 68 | 69 | channel := s.channels.Get(m.channel) 70 | if channel == nil { 71 | client.ErrNoSuchChannel(m.channel) 72 | return 73 | } 74 | 75 | if !channel.members.HasMode(client, Theater) { 76 | client.Reply(RplNotice(s, client, "you are not +T")) 77 | return 78 | } 79 | 80 | reply := RplPrivMsg(TheaterClient(m.asNick), channel, m.message) 81 | for member := range channel.members { 82 | member.Reply(reply) 83 | } 84 | } 85 | 86 | type TheaterActionCommand struct { 87 | BaseCommand 88 | channel Name 89 | asNick Name 90 | action CTCPText 91 | } 92 | 93 | func (m *TheaterActionCommand) HandleServer(s *Server) { 94 | client := m.Client() 95 | 96 | if !m.channel.IsChannel() { 97 | client.ErrNoSuchChannel(m.channel) 98 | return 99 | } 100 | 101 | channel := s.channels.Get(m.channel) 102 | if channel == nil { 103 | client.ErrNoSuchChannel(m.channel) 104 | return 105 | } 106 | 107 | if !channel.members.HasMode(client, Theater) { 108 | client.Reply(RplNotice(s, client, "you are not +T")) 109 | return 110 | } 111 | 112 | reply := RplCTCPAction(TheaterClient(m.asNick), channel, m.action) 113 | for member := range channel.members { 114 | member.Reply(reply) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /irc/types.go: -------------------------------------------------------------------------------- 1 | package irc 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // 9 | // simple types 10 | // 11 | 12 | type ChannelNameMap map[Name]*Channel 13 | 14 | func (channels ChannelNameMap) Get(name Name) *Channel { 15 | return channels[name.ToLower()] 16 | } 17 | 18 | func (channels ChannelNameMap) Add(channel *Channel) error { 19 | if channels[channel.name.ToLower()] != nil { 20 | return fmt.Errorf("%s: already set", channel.name) 21 | } 22 | channels[channel.name.ToLower()] = channel 23 | return nil 24 | } 25 | 26 | func (channels ChannelNameMap) Remove(channel *Channel) error { 27 | if channel != channels[channel.name.ToLower()] { 28 | return fmt.Errorf("%s: mismatch", channel.name) 29 | } 30 | delete(channels, channel.name.ToLower()) 31 | return nil 32 | } 33 | 34 | type ChannelModeSet map[ChannelMode]bool 35 | 36 | func (set ChannelModeSet) String() string { 37 | if len(set) == 0 { 38 | return "" 39 | } 40 | strs := make([]string, len(set)) 41 | index := 0 42 | for mode := range set { 43 | strs[index] = mode.String() 44 | index += 1 45 | } 46 | return strings.Join(strs, "") 47 | } 48 | 49 | type ClientSet map[*Client]bool 50 | 51 | func (clients ClientSet) Add(client *Client) { 52 | clients[client] = true 53 | } 54 | 55 | func (clients ClientSet) Remove(client *Client) { 56 | delete(clients, client) 57 | } 58 | 59 | func (clients ClientSet) Has(client *Client) bool { 60 | return clients[client] 61 | } 62 | 63 | type MemberSet map[*Client]ChannelModeSet 64 | 65 | func (members MemberSet) Add(member *Client) { 66 | members[member] = make(ChannelModeSet) 67 | } 68 | 69 | func (members MemberSet) Remove(member *Client) { 70 | delete(members, member) 71 | } 72 | 73 | func (members MemberSet) Has(member *Client) bool { 74 | _, ok := members[member] 75 | return ok 76 | } 77 | 78 | func (members MemberSet) HasMode(member *Client, mode ChannelMode) bool { 79 | modes, ok := members[member] 80 | if !ok { 81 | return false 82 | } 83 | return modes[mode] 84 | } 85 | 86 | func (members MemberSet) AnyHasMode(mode ChannelMode) bool { 87 | for _, modes := range members { 88 | if modes[mode] { 89 | return true 90 | } 91 | } 92 | return false 93 | } 94 | 95 | type ChannelSet map[*Channel]bool 96 | 97 | func (channels ChannelSet) Add(channel *Channel) { 98 | channels[channel] = true 99 | } 100 | 101 | func (channels ChannelSet) Remove(channel *Channel) { 102 | delete(channels, channel) 103 | } 104 | 105 | func (channels ChannelSet) First() *Channel { 106 | for channel := range channels { 107 | return channel 108 | } 109 | return nil 110 | } 111 | 112 | // 113 | // interfaces 114 | // 115 | 116 | type Identifiable interface { 117 | Id() Name 118 | Nick() Name 119 | } 120 | -------------------------------------------------------------------------------- /irc/websocket.go: -------------------------------------------------------------------------------- 1 | package irc 2 | 3 | import ( 4 | "github.com/gorilla/websocket" 5 | "net/http" 6 | "time" 7 | ) 8 | 9 | var upgrader = websocket.Upgrader{ 10 | ReadBufferSize: 1024, 11 | WriteBufferSize: 1024, 12 | // If a WS session contains sensitive information, and you choose to use 13 | // cookies for authentication (during the HTTP(S) upgrade request), then 14 | // you should check that Origin is a domain under your control. If it 15 | // isn't, then it is possible for users of your site, visiting a naughty 16 | // Origin, to have a WS opened using their credentials. See 17 | // http://www.christian-schneider.net/CrossSiteWebSocketHijacking.html#main. 18 | // We don't care about Origin because the (IRC) authentication is contained 19 | // in the WS stream -- the WS session is not privileged when it is opened. 20 | CheckOrigin: func(r *http.Request) bool { return true }, 21 | } 22 | 23 | type WSContainer struct { 24 | *websocket.Conn 25 | } 26 | 27 | func (this WSContainer) Read(msg []byte) (int, error) { 28 | ty, bytes, err := this.ReadMessage() 29 | if ty == websocket.TextMessage { 30 | n := copy(msg, []byte(string(bytes)+CRLF+CRLF)) 31 | return n, err 32 | } 33 | // Binary, and other kinds of messages, are thrown away. 34 | return 0, nil 35 | } 36 | 37 | func (this WSContainer) Write(msg []byte) (int, error) { 38 | err := this.WriteMessage(websocket.TextMessage, msg) 39 | return len(msg), err 40 | } 41 | 42 | func (this WSContainer) SetDeadline(t time.Time) error { 43 | if err := this.SetWriteDeadline(t); err != nil { 44 | return err 45 | } 46 | return this.SetReadDeadline(t) 47 | } 48 | -------------------------------------------------------------------------------- /irc/whowas.go: -------------------------------------------------------------------------------- 1 | package irc 2 | 3 | type WhoWasList struct { 4 | buffer []*WhoWas 5 | start int 6 | end int 7 | } 8 | 9 | type WhoWas struct { 10 | nickname Name 11 | username Name 12 | hostname Name 13 | realname Text 14 | } 15 | 16 | func NewWhoWasList(size uint) *WhoWasList { 17 | return &WhoWasList{ 18 | buffer: make([]*WhoWas, size), 19 | } 20 | } 21 | 22 | func (list *WhoWasList) Append(client *Client) { 23 | list.buffer[list.end] = &WhoWas{ 24 | nickname: client.Nick(), 25 | username: client.username, 26 | hostname: client.hostname, 27 | realname: client.realname, 28 | } 29 | list.end = (list.end + 1) % len(list.buffer) 30 | if list.end == list.start { 31 | list.start = (list.end + 1) % len(list.buffer) 32 | } 33 | } 34 | 35 | func (list *WhoWasList) Find(nickname Name, limit int64) []*WhoWas { 36 | results := make([]*WhoWas, 0) 37 | for whoWas := range list.Each() { 38 | if nickname != whoWas.nickname { 39 | continue 40 | } 41 | results = append(results, whoWas) 42 | if int64(len(results)) >= limit { 43 | break 44 | } 45 | } 46 | return results 47 | } 48 | 49 | func (list *WhoWasList) prev(index int) int { 50 | index -= 1 51 | if index < 0 { 52 | index += len(list.buffer) 53 | } 54 | return index 55 | } 56 | 57 | // Iterate the buffer in reverse. 58 | func (list *WhoWasList) Each() <-chan *WhoWas { 59 | ch := make(chan *WhoWas) 60 | go func() { 61 | defer close(ch) 62 | if list.start == list.end { 63 | return 64 | } 65 | start := list.prev(list.end) 66 | end := list.prev(list.start) 67 | for start != end { 68 | ch <- list.buffer[start] 69 | start = list.prev(start) 70 | } 71 | }() 72 | return ch 73 | } 74 | --------------------------------------------------------------------------------