├── README.md ├── help.go ├── responses.go ├── connection_test.go ├── bus.go └── connection.go /README.md: -------------------------------------------------------------------------------- 1 | # goIRC 2 | 3 | ##IRC server written in Go 4 | 5 | [IRC Spec](https://tools.ietf.org/html/rfc1459) 6 | 7 | ###Connection Steps 8 | 1. ```telnet ec2-54-191-196-95.us-west-2.compute.amazonaws.com 3030``` 9 | 10 | ###Commands 11 | 1. ```PASS ``` 12 | 1. ```JOIN #``` 13 | 1. ```PRIVMSG #:``` 14 | 1. ```HELP``` 15 | 1. ```LIST``` 16 | 1. ```PART #``` 17 | 18 | ### To Run Locally 19 | 1. ```go run *[^_t].go``` 20 | 2. ```telnet localhost 3030``` 21 | 22 | -------------------------------------------------------------------------------- /help.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type HelpCommand struct { 4 | Summary string 5 | Syntax string 6 | // Help string - more detailed help instructions 7 | // Ops boolean - flag to see who can use the command 8 | } 9 | 10 | var Help = map[string]HelpCommand{ 11 | "JOIN": HelpCommand{ 12 | Summary: "join a new channel", 13 | Syntax: "JOIN <#channel>", 14 | }, 15 | "PRIVMSG": HelpCommand{ 16 | Summary: "send a message to a channel", 17 | Syntax: "PRIVMSG <#channel>: ", 18 | }, 19 | "NICK": HelpCommand{ 20 | Summary: "change your nickname", 21 | Syntax: "NICK ", 22 | }, 23 | "PASS": HelpCommand{ 24 | Summary: "~*~hidden command~*~ short hand for registering and setting a username", 25 | Syntax: "PASS ", 26 | }, 27 | "TOPIC": HelpCommand{ 28 | Summary: "see the topic for a certain channel", 29 | Syntax: "TOPIC <#channel>: ", 30 | }, 31 | "LIST": HelpCommand{ 32 | Summary: "lists out all available channels to join", 33 | Syntax: "LIST", 34 | }, 35 | // "USERS": HelpCommand{ 36 | // Summary: "get a list of all users in a channel", 37 | // Syntax: "syntax: USERS <#channel>", 38 | // }, 39 | // "NICK": HelpCommand{ 40 | // Summary: "change your nickname", 41 | // Syntax: "NICK ", 42 | // }, 43 | // "QUIT": HelpCommand{ 44 | // Summary: "quit the server", 45 | // Syntax: "", 46 | // }, 47 | // "PART": HelpCommand{ 48 | // Summary: "leave the channel", 49 | // Syntax: "PART <#channel>", 50 | // }, 51 | // "KICK": HelpCommand{ 52 | // Summary: "kick out a user from the channel - ops only", 53 | // Syntax: "KICK <#channel>: ", 54 | // }, 55 | // "INVITE": HelpCommand{ 56 | // Summary: "invite a user to the channel", 57 | // Syntax: "INVITE <#channel>: ", 58 | // }, 59 | // "KILL": HelpCommand{ 60 | // Summary: "disconnect a user - ops only", 61 | // Syntax: "KILL ", 62 | // }, 63 | } 64 | -------------------------------------------------------------------------------- /responses.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | var canned_responses map[int]string 6 | 7 | const HOST_STRING = "goirc.capitalonelabs.com" 8 | 9 | const ( 10 | ERR_NOSUCHCHANNEL int = iota 11 | ERR_NOSUCHNICK 12 | ERR_UNKNOWNERROR 13 | ERR_CANNOTSENDTOCHAN 14 | RPL_WELCOME 15 | RPL_YOURHOST 16 | RPL_CREATED 17 | RPL_MYINFO 18 | RPL_ISUPPORT 19 | RPL_YOURID 20 | RPL_MOTDSTART 21 | RPL_MOTD 22 | RPL_ENDOFMOTD 23 | ) 24 | 25 | func loadMessages() { 26 | canned_responses = make(map[int]string) 27 | 28 | canned_responses[RPL_WELCOME] = " 001 %s :Welcome to the Capital One Labs IRC Network" 29 | canned_responses[RPL_YOURHOST] = " 002 %s :Your host is goirc.capitalonelabs.com, running goIRC-0.0.1" 30 | canned_responses[RPL_CREATED] = " 003 %s :This server was created at some point in the past" 31 | canned_responses[RPL_MYINFO] = " 004 %s :some server modes go here or something" 32 | canned_responses[RPL_ISUPPORT] = " 005 %s :info about limits and so env variables will go here" 33 | canned_responses[RPL_YOURID] = " 006 %s :unique id goes here maybe? (ircnet)" 34 | canned_responses[RPL_MOTDSTART] = " 372 %s: we don't have an motd yet!!" 35 | canned_responses[RPL_MOTD] = " 375 %s :" + HOST_STRING + " message of the day" 36 | canned_responses[RPL_ENDOFMOTD] = " 376 %s :end of motd" 37 | canned_responses[ERR_UNKNOWNERROR] = " 400 %s : unknown error" 38 | canned_responses[ERR_NOSUCHNICK] = " 400 %s :no such nick" 39 | canned_responses[ERR_NOSUCHCHANNEL] = " 403 %s :no such channel" 40 | canned_responses[ERR_CANNOTSENDTOCHAN] = " 404 %s cannot send to channel" 41 | 42 | for i, v := range canned_responses { 43 | canned_responses[i] = ":" + HOST_STRING + v 44 | } 45 | } 46 | 47 | func sendWelcome(user *User) { 48 | user.Write("PING :" + HOST_STRING) 49 | user.Write(":" + HOST_STRING + " NOTICE Auth :welcome!") 50 | types := []int{RPL_WELCOME, RPL_CREATED, RPL_YOURHOST, RPL_MYINFO, RPL_ISUPPORT, RPL_YOURID, RPL_MOTDSTART, RPL_MOTD, RPL_ENDOFMOTD} 51 | 52 | for _, val := range types { 53 | user.Write(fmt.Sprintf(canned_responses[val], user.Nick)) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /connection_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // import ( 4 | // "net" 5 | // "sync" 6 | // "testing" 7 | // ) 8 | 9 | // func handleNick(buses map[string]*EventBus, client *User, target string, data string) { 10 | // client.Nick = target 11 | // client.Conn.Write([]byte("nick set to:" + client.Nick + "\n")) 12 | // } 13 | 14 | // func TestHandleNick(t *testing.T) { 15 | // nick := "randomNick" 16 | 17 | // wasWritten := false 18 | // client := User{Nick: "anotherNick"} 19 | // newDualStackServer([]net.Listener) 20 | // client.Conn.Write = func(b []byte) (int, error) { wasWritten = true; return 0, nil } 21 | 22 | // handleNick(nil, &client, nick, "") 23 | 24 | // if client.Nick != nick { 25 | // t.Error("nick did not change") 26 | // } 27 | 28 | // } 29 | 30 | // /////////////////////////////////// 31 | // ///////MOCK SERVER STUFFS////////// 32 | 33 | // type streamListener struct { 34 | // net, addr string 35 | // ln net.Listener 36 | // } 37 | 38 | // type dualStackServer struct { 39 | // lnmu sync.RWMutex 40 | // lns []streamListener 41 | // port string 42 | 43 | // cmu sync.RWMutex 44 | // cs []net.Conn // established connections at the passive open side 45 | // } 46 | 47 | // func (dss *dualStackServer) buildup(server func(*dualStackServer, net.Listener)) error { 48 | // for i := range dss.lns { 49 | // go server(dss, dss.lns[i].ln) 50 | // } 51 | // return nil 52 | // } 53 | 54 | // func (dss *dualStackServer) putConn(c net.Conn) error { 55 | // dss.cmu.Lock() 56 | // dss.cs = append(dss.cs, c) 57 | // dss.cmu.Unlock() 58 | // return nil 59 | // } 60 | 61 | // func (dss *dualStackServer) teardownNetwork(net string) error { 62 | // dss.lnmu.Lock() 63 | // for i := range dss.lns { 64 | // if net == dss.lns[i].net && dss.lns[i].ln != nil { 65 | // dss.lns[i].ln.Close() 66 | // dss.lns[i].ln = nil 67 | // } 68 | // } 69 | // dss.lnmu.Unlock() 70 | // return nil 71 | // } 72 | 73 | // func (dss *dualStackServer) teardown() error { 74 | // dss.lnmu.Lock() 75 | // for i := range dss.lns { 76 | // if dss.lns[i].ln != nil { 77 | // dss.lns[i].ln.Close() 78 | // } 79 | // } 80 | // dss.lnmu.Unlock() 81 | // dss.cmu.Lock() 82 | // for _, c := range dss.cs { 83 | // c.Close() 84 | // } 85 | // dss.cmu.Unlock() 86 | // return nil 87 | // } 88 | 89 | // func newDualStackServer(lns []streamListener) (*dualStackServer, error) { 90 | // dss := &dualStackServer{lns: lns, port: "0"} 91 | // for i := range dss.lns { 92 | // ln, err := net.Listen(dss.lns[i].net, dss.lns[i].addr+":"+dss.port) 93 | // if err != nil { 94 | // dss.teardown() 95 | // return nil, err 96 | // } 97 | // dss.lns[i].ln = ln 98 | // if dss.port == "0" { 99 | // if _, dss.port, err = net.SplitHostPort(ln.Addr().String()); err != nil { 100 | // dss.teardown() 101 | // return nil, err 102 | // } 103 | // } 104 | // } 105 | // return dss, nil 106 | // } 107 | -------------------------------------------------------------------------------- /bus.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "sync" 7 | ) 8 | 9 | type EventType int 10 | type Mode int 11 | 12 | const ( 13 | UserJoin EventType = iota 14 | UserPart 15 | PrivMsg 16 | Topic 17 | ) 18 | 19 | const ( 20 | Voice Mode = iota 21 | Moderator 22 | None 23 | ) 24 | 25 | type Subscriber interface { 26 | OnEvent(*Event) 27 | GetInfo() string 28 | } 29 | 30 | type EventBus struct { 31 | subscribers map[EventType][]Subscriber 32 | channel *Channel 33 | sync.Mutex 34 | } 35 | 36 | type Event struct { 37 | event_type EventType 38 | event_data string 39 | } 40 | 41 | type Channel struct { 42 | name string 43 | topic string 44 | mode map[string]Mode 45 | } 46 | 47 | func (u *User) GetInfo() string { 48 | return u.Nick 49 | } 50 | 51 | func (c *Channel) GetInfo() string { 52 | return c.name 53 | } 54 | 55 | func (u *User) Write(line string) { 56 | u.Conn.Write([]byte(line + "\r\n")) 57 | } 58 | 59 | func (u *User) WriteLines(lines []string) { 60 | for _, v := range lines { 61 | u.Write(v) 62 | } 63 | } 64 | 65 | func (u *User) OnEvent(event *Event) { 66 | switch event.event_type { 67 | case UserJoin: 68 | //fmt.Printf("%q(%d)> %q\n", s.Nick, event.event_type, event.event_data) 69 | _, err := u.Conn.Write([]byte(event.event_data)) 70 | if err != nil { 71 | fmt.Println("Not looking too good") 72 | } 73 | case PrivMsg: 74 | _, err := u.Conn.Write([]byte(event.event_data)) 75 | if err != nil { 76 | fmt.Println("Not looking too good") 77 | } 78 | case Topic: 79 | _, err := u.Conn.Write([]byte(event.event_data)) 80 | if err != nil { 81 | fmt.Println("Not looking too good") 82 | } 83 | default: 84 | u.Conn.Write([]byte(event.event_data)) 85 | } 86 | } 87 | func (bus *EventBus) Publish(event *Event) { 88 | fmt.Printf("\npublishing -%d- data: %q\n", event.event_type, event.event_data) 89 | for _, subscriber := range bus.subscribers[event.event_type] { 90 | go subscriber.OnEvent(event) //currently slower than without the goroutine 91 | } 92 | fmt.Println("done publishing") 93 | } 94 | 95 | func (bus *EventBus) Subscribe(event_type EventType, subscriber Subscriber) { 96 | bus.subscribers[event_type] = append(bus.subscribers[event_type], subscriber) 97 | } 98 | 99 | func (bus *EventBus) Unsubscribe(event_type EventType, subscriber Subscriber) { 100 | //find the index 101 | i := -1 102 | 103 | for index, val := range bus.subscribers[event_type] { 104 | if val.GetInfo() == subscriber.GetInfo() { 105 | i = index 106 | break 107 | } 108 | } 109 | 110 | if i > -1 { //we found someone 111 | cur := bus.subscribers[event_type] 112 | endIndex := i + 1 //will break if index is last element! 113 | cur = append(cur[0:i], cur[endIndex:]...) 114 | 115 | bus.Lock() //lock the eventbus while we remove the subscriber from the array 116 | bus.subscribers[event_type] = cur 117 | bus.Unlock() 118 | } 119 | 120 | } 121 | 122 | var buses map[string]*EventBus 123 | 124 | func init() { 125 | loadMessages() 126 | } 127 | func main() { 128 | // init event bus map 129 | buses := make(map[string]*EventBus) 130 | 131 | ln, err := net.Listen("tcp", ":3030") 132 | 133 | if err != nil { 134 | panic("Listen not WORKING") 135 | } 136 | for { 137 | conn, err := ln.Accept() 138 | if err != nil { 139 | panic("nope not Accepting") 140 | } 141 | go handleConnection(conn, buses) 142 | } 143 | 144 | } 145 | -------------------------------------------------------------------------------- /connection.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "net" 7 | "strings" 8 | ) 9 | 10 | type ConnectionStatus int 11 | 12 | const ( 13 | SocketConnected ConnectionStatus = iota 14 | UserPassSent 15 | UserNickSent 16 | UserUserInfoSent 17 | UserRegistered 18 | ) 19 | 20 | type User struct { 21 | Nick string 22 | Ident string 23 | RealName string 24 | Conn net.Conn 25 | Status ConnectionStatus 26 | Host string 27 | } 28 | 29 | func (u *User) getHead() string { 30 | //return fmt.Sprintf(":%s!%s@%s", u.Nick, u.Ident, u.Host) 31 | return fmt.Sprintf(":%s!%s@127.0.0.1", u.Nick, u.Ident) 32 | } 33 | 34 | func handleConnection(conn net.Conn, buses map[string]*EventBus) { 35 | client := User{Status: UserPassSent, Conn: conn} 36 | //myIP := net.Conn.RemoteAddr().String() 37 | reader := bufio.NewReader(conn) 38 | 39 | commands := make(map[string]func(map[string]*EventBus, *User, string, string)) 40 | commands["JOIN"] = handleJoin 41 | commands["TOPIC"] = handleTopic 42 | commands["PRIVMSG"] = handleMsg 43 | commands["NICK"] = handleNick 44 | commands["PART"] = handlePart 45 | commands["HELP"] = handleHelp 46 | commands["LIST"] = handleList 47 | commands["PING"] = handlePing 48 | commands["PONG"] = handlePong 49 | 50 | for { 51 | status, err := reader.ReadString('\n') 52 | if err != nil { 53 | return 54 | } 55 | 56 | status = strings.TrimSpace(status) 57 | statLen := strings.Split(status, " ") 58 | 59 | // allows user to enter empty strings 60 | if len(status) == 0 { 61 | conn.Write([]byte("")) 62 | continue 63 | } else if len(statLen) < 2 { 64 | cmd := strings.SplitN(status, " ", 1) 65 | cmd[0] = strings.ToUpper(cmd[0]) 66 | if _, ok := commands[cmd[0]]; ok { 67 | commands[cmd[0]](buses, &client, "", "") 68 | } 69 | } else { 70 | if client.Status < UserRegistered { 71 | regCmd := strings.SplitN(status, " ", 2) 72 | regCmd[0] = strings.ToUpper(regCmd[0]) 73 | fmt.Println("-" + regCmd[0] + "-" + regCmd[1]) 74 | switch regCmd[0] { 75 | case "NICK": 76 | //client.Nick = regCmd[1] 77 | handleNick(buses, &client, regCmd[1], "") 78 | client.Status = UserNickSent 79 | case "USER": 80 | fmt.Println("hit user case") 81 | var uname, hname, sname, rname string 82 | fmt.Sscanf(regCmd[1], "%s %s %s :%s", uname, hname, sname, rname) //TODO(jz) need to split on : in case real name has spaces 83 | fmt.Println(hname + uname) 84 | //client.Ident = uname 85 | client.RealName = rname 86 | client.Status = UserRegistered 87 | client.Ident = client.Nick 88 | fmt.Println("username:" + client.Ident) 89 | buses[client.Nick] = &EventBus{subscribers: make(map[EventType][]Subscriber), channel: nil} 90 | buses[client.Nick].Subscribe(PrivMsg, &client) 91 | sendWelcome(&client) 92 | case "PASS": //need to remove this at some point! 93 | client.Nick = regCmd[1] 94 | client.Ident = regCmd[1] 95 | client.RealName = regCmd[1] 96 | 97 | buses[client.Nick] = &EventBus{subscribers: make(map[EventType][]Subscriber), channel: nil} 98 | buses[client.Nick].Subscribe(PrivMsg, &client) 99 | client.Status = UserRegistered 100 | sendWelcome(&client) 101 | 102 | //conn.Write([]byte("Welcome " + regCmd[1] + ") 103 | 104 | default: 105 | client.Write("you must register first. try nick or user?") 106 | } 107 | 108 | } else { 109 | // split : 110 | 111 | var cmd, target, data string 112 | s := strings.SplitN(status, ":", 2) 113 | if len(s) > 1 { 114 | data = s[1] 115 | } 116 | _, err = fmt.Sscanf(s[0], "%s %s", &cmd, &target) 117 | if err != nil { 118 | fmt.Println(err) 119 | client.Write("Invalid input! CHECK YOUR(self) SYNTAX") 120 | continue 121 | } 122 | cmd = strings.ToUpper(cmd) 123 | if _, ok := commands[cmd]; ok { 124 | commands[cmd](buses, &client, target, data) 125 | } 126 | } 127 | } 128 | } 129 | } 130 | 131 | func checkEventBus(buses map[string]*EventBus, client *User, target string) (*EventBus, bool) { 132 | b, ok := buses[target] 133 | if !ok { 134 | client.Write(fmt.Sprintf(canned_responses[ERR_NOSUCHCHANNEL], client.Nick)) 135 | } 136 | return b, ok 137 | } 138 | func checkSubscribed(bus *EventBus, client *User, event_type EventType) bool { 139 | for _, v := range bus.subscribers[event_type] { 140 | if v == client { 141 | return true 142 | } 143 | } 144 | return false 145 | } 146 | func isChannel(target string) bool { 147 | return string(target[0]) == "#" 148 | } 149 | func handlePart(buses map[string]*EventBus, client *User, target string, data string) { 150 | message := fmt.Sprintf("%s parted %s!\n", client.Nick, target) 151 | b, ok := checkEventBus(buses, client, target) 152 | if !ok { 153 | return 154 | } 155 | if ok = checkSubscribed(b, client, UserPart); !ok { 156 | return 157 | } 158 | buses[target].Publish(&Event{event_type: UserPart, event_data: message}) 159 | delete(b.channel.mode, client.Nick) 160 | 161 | buses[target].Unsubscribe(UserPart, client) 162 | buses[target].Unsubscribe(UserJoin, client) 163 | buses[target].Unsubscribe(Topic, client) 164 | buses[target].Unsubscribe(PrivMsg, client) 165 | // possibile race condition 166 | if len(b.channel.mode) == 0 { 167 | delete(buses, target) 168 | fmt.Println(target + " closed") 169 | } 170 | } 171 | 172 | func handlePing(buses map[string]*EventBus, client *User, target string, data string) { 173 | client.Write("PONG " + target) 174 | } 175 | 176 | func handlePong(buses map[string]*EventBus, client *User, target string, data string) { 177 | //no op for fun 178 | } 179 | 180 | func handleJoin(buses map[string]*EventBus, client *User, target string, data string) { 181 | fmt.Println("!!!!!!!!! JOIN") 182 | if !isChannel(target) { 183 | return 184 | } 185 | b, ok := checkEventBus(buses, client, target) 186 | if !ok { 187 | newChannel := Channel{name: target, topic: "gogo new channel!", mode: make(map[string]Mode)} 188 | buses[newChannel.name] = &EventBus{subscribers: make(map[EventType][]Subscriber), channel: &newChannel} 189 | b = buses[newChannel.name] 190 | } 191 | if ok = checkSubscribed(b, client, UserJoin); !ok { 192 | b.channel.mode[client.Nick] = Voice 193 | b.Subscribe(UserJoin, client) 194 | b.Subscribe(UserPart, client) 195 | b.Subscribe(PrivMsg, client) 196 | b.Subscribe(Topic, client) 197 | //message := fmt.Sprintf("%s joined %s!\n", client.Nick, target) 198 | message := fmt.Sprintf("%s JOIN %s\n", client.getHead(), target) 199 | //send names 200 | var names string 201 | for _, val := range buses[target].subscribers[PrivMsg] { 202 | names = names + " " + val.GetInfo() 203 | } 204 | client.Write(":" + HOST_STRING + " 332 " + client.Nick + " " + target + ":no topic set") 205 | client.Write(":" + HOST_STRING + " 333 " + client.Nick + " " + target + " admin!admin@localhost 1419044230") 206 | client.Write(":" + HOST_STRING + " 353 " + client.Nick + " " + target + " :" + names) 207 | client.Write(":" + HOST_STRING + " 366 " + client.Nick + " * :END of /NAMES list.") 208 | ///end send names 209 | b.Publish(&Event{UserJoin, message}) 210 | } 211 | 212 | } 213 | 214 | func handleTopic(buses map[string]*EventBus, client *User, target string, data string) { 215 | b, ok := checkEventBus(buses, client, target) 216 | if !ok { 217 | return 218 | } 219 | if ok = checkSubscribed(b, client, Topic); !ok { 220 | return 221 | } 222 | 223 | if len(data) > 0 { 224 | b.channel.topic = data 225 | message := fmt.Sprintf("%s changed the channel topic to %s", client.Nick, data) 226 | b.Publish(&Event{Topic, message}) 227 | } else { 228 | message := fmt.Sprintf("%s\n", b.channel.topic) 229 | client.Write(message) 230 | } 231 | } 232 | 233 | func handleNick(buses map[string]*EventBus, client *User, target string, data string) { 234 | client.Nick = target 235 | client.Write("nick set to:" + client.Nick) 236 | } 237 | 238 | func handleMsg(buses map[string]*EventBus, client *User, target string, data string) { 239 | b, ok := checkEventBus(buses, client, target) 240 | if !ok { 241 | return 242 | } 243 | if !isChannel(target) { 244 | message := fmt.Sprintf("%s PRIVMSG %s: %s\n", client.getHead(), target, data) 245 | b.Publish(&Event{event_type: PrivMsg, event_data: message}) 246 | buses[client.Nick].Publish(&Event{event_type: PrivMsg, event_data: message}) 247 | } 248 | if ok = checkSubscribed(b, client, PrivMsg); !ok { 249 | return 250 | } 251 | message := fmt.Sprintf("%s PRIVMSG %s: %s\n", client.getHead(), target, data) 252 | b.Publish(&Event{event_type: PrivMsg, event_data: message}) 253 | } 254 | 255 | //func handleList(conn net.Conn, buses map[string]*EventBus) { 256 | func handleList(buses map[string]*EventBus, client *User, target string, data string) { 257 | if len(buses) == 0 { 258 | client.Write("No Channels Exist") 259 | } else { 260 | client.Write("Channels") 261 | for k, _ := range buses { 262 | if k[:1] == "#" { 263 | client.Conn.Write([]byte(k + "\n")) 264 | } 265 | } 266 | client.Conn.Write([]byte("End of List\n")) 267 | //client.Write(k) 268 | } 269 | } 270 | 271 | func handleHelp(buses map[string]*EventBus, client *User, target string, data string) { 272 | k, ok := Help[target] 273 | if !ok { 274 | client.Write("\nAvailable Commands: (Enter HELP for further details") 275 | for h := range Help { 276 | client.Write(h) 277 | } 278 | } else { 279 | client.Write("Summary: " + k.Summary + "\r\nUsage: " + k.Syntax) 280 | } 281 | } 282 | --------------------------------------------------------------------------------