diff --git a/http.go b/http.go index 7c9d1d5..e199d8c 100644 --- a/http.go +++ b/http.go @@ -3,6 +3,7 @@ package main import ( "encoding/json" "fmt" + "log" "net/http" ) @@ -12,6 +13,7 @@ type ModeResponse struct { Target string `json:"target"` Channel string `json:"channel"` Status string `json:"status"` + Error string `json:"error"` } type KickResponse struct { Action string `json:"action"` @@ -19,24 +21,33 @@ type KickResponse struct { Channel string `json:"channel"` Reason string `json:"reason"` Status string `json:"status"` + Error string `json:"error"` } func startHTTP(cfg Config) { htport := cfg.Webapiport - http.HandleFunc("/admin/kick", handleKick) - http.HandleFunc("/admin/mode", handleModeAPI) - http.HandleFunc("/admin/list", handleListAPI) - http.HandleFunc("/admin/reloadmotd", handleMOTDreload) - fmt.Printf("[web] starting API on port %d\n", htport) + addAuthMiddleware := func(path string, next http.HandlerFunc) { + http.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { + guardHandlerAuth(w, r, next) + }) + } + addAuthMiddleware("/admin/kick", handleKick) + addAuthMiddleware("/admin/mode", handleModeAPI) + addAuthMiddleware("/admin/list", handleListAPI) + addAuthMiddleware("/admin/reloadmotd", handleMOTDreload) + log.Printf("[web] starting API on port %d\n", htport) http.ListenAndServe(fmt.Sprintf(":%d", htport), nil) } -func handleKick(w http.ResponseWriter, r *http.Request) { +func guardHandlerAuth(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { user, pass, ok := r.BasicAuth() if !ok || !checkCredentials(user, pass) || !globalOps[user] { w.Header().Set("WWW-Authenticate", `Basic realm="API"`) http.Error(w, "Unauthorized", http.StatusUnauthorized) return } + next(w, r) +} +func handleKick(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "POST required", http.StatusMethodNotAllowed) return @@ -82,17 +93,12 @@ func handleKick(w http.ResponseWriter, r *http.Request) { Channel: channelName, Reason: reason, Status: "ok", + Error: "", } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(resp) } func handleModeAPI(w http.ResponseWriter, r *http.Request) { - user, pass, ok := r.BasicAuth() - if !ok || !checkCredentials(user, pass) || !globalOps[user] { - w.Header().Set("WWW-Authenticate", `Basic realm="API"`) - http.Error(w, "Unauthorized", http.StatusUnauthorized) - return - } if r.Method != http.MethodPost { http.Error(w, "POST required", http.StatusMethodNotAllowed) return @@ -142,6 +148,7 @@ func handleModeAPI(w http.ResponseWriter, r *http.Request) { Target: targetNick, Channel: channelName, Status: "ok", + Error: "", } w.Header().Set("Content-Type", "application/json") enc := json.NewEncoder(w) @@ -156,12 +163,6 @@ type ChannelInfo struct { } func handleListAPI(w http.ResponseWriter, r *http.Request) { - user, pass, ok := r.BasicAuth() - if !ok || !checkCredentials(user, pass) || !globalOps[user] { - w.Header().Set("WWW-Authenticate", `Basic realm="API"`) - http.Error(w, "Unauthorized", http.StatusUnauthorized) - return - } chMu.Lock() defer chMu.Unlock() @@ -189,18 +190,9 @@ func handleListAPI(w http.ResponseWriter, r *http.Request) { } } func handleMOTDreload(w http.ResponseWriter, r *http.Request) { - user, pass, ok := r.BasicAuth() - if !ok || !checkCredentials(user, pass) || !globalOps[user] { - w.Header().Set("WWW-Authenticate", `Basic realm="API"`) - http.Error(w, "Unauthorized", http.StatusUnauthorized) - return - } loadMOTD(motdFile) - type htresp struct { - Status string `json:"status"` - } - resp := htresp{ - Status: "ok", + resp := map[string]any{ + "status": "ok", } w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(resp); err != nil { diff --git a/main.go b/main.go index 56a7bb8..f818c84 100644 --- a/main.go +++ b/main.go @@ -14,6 +14,7 @@ import ( "log" "net" "os" + "strconv" "strings" "sync" "time" @@ -38,8 +39,18 @@ type Channel struct { description string members map[string]*Client ops map[string]bool + history []HistoryMessage mu sync.RWMutex } +type HistoryMessage struct { + MsgID string + Time time.Time + Prefix string + Command string + Params []string + Text string +} + type Channelconfig struct { Channel string `json:"channel"` Description string `json:"description"` @@ -162,6 +173,12 @@ func handleConn(conn net.Conn) { delete(clients, oldNick) clients[newNick] = client cliMu.Unlock() + for _, ch := range client.channels { + ch.mu.Lock() + delete(ch.members, oldNick) + ch.members[newNick] = client + ch.mu.Unlock() + } client.nick = newNick for _, ch := range client.channels { broadcastNames(ch) @@ -188,8 +205,8 @@ func handleConn(conn net.Conn) { subcmd := strings.ToUpper(msg.Params[0]) switch subcmd { case "LS": - // explicitly tell we only support PLAIN SASL - client.fwrite(":server CAP * LS :sasl=PLAIN\r\n") + // explicitly tell we only support PLAIN SASL, as well as chathistory + client.fwrite(":server CAP * LS :sasl=PLAIN chathistory\r\n") case "REQ": if len(msg.Params) >= 2 && strings.Contains(strings.ToLower(msg.Params[1]), "sasl") { @@ -373,6 +390,15 @@ func handleConn(conn net.Conn) { } else { fmt.Fprintf(client.conn, ":server 461 %s NAMES :Not enough parameters\r\n", client.nick) } + case "CHATHISTORY": + if len(msg.Params) < 3 { + client.fwrite(":server 461 %s CHATHISTORY :Not enough parameters\r\n", client.nick) + continue + } + subcmd := strings.ToUpper(msg.Params[0]) + target := normalizeChannelName(msg.Params[1]) + count, _ := strconv.Atoi(msg.Params[2]) + client.serveHistory(subcmd, target, count, msg.Params) default: if client.nick != "" { fmt.Fprintf(conn, ":server 421 %s %s :Unknown command or command is not implemented\r\n", client.nick, msg.Command) @@ -398,6 +424,110 @@ func (c *Client) sendNames(ch *Channel) { fmt.Fprintf(c.conn, ":server 353 %s = %s :%s\r\n", c.nick, ch.name, strings.Join(nicks, " ")) fmt.Fprintf(c.conn, ":server 366 %s %s :End of /NAMES list.\r\n", c.nick, ch.name) } +func (c *Client) serveHistory(subcmd, target string, count int, params []string) { + chMu.Lock() + ch, ok := channels[target] + chMu.Unlock() + if !ok { + c.fwrite(":server 403 %s %s :No such channel\r\n", c.nick, target) + return + } + + ch.mu.RLock() + defer ch.mu.RUnlock() + + var msgs []HistoryMessage + + switch subcmd { + case "LATEST": + if count > len(ch.history) { + count = len(ch.history) + } + msgs = ch.history[len(ch.history)-count:] + + case "BEFORE": + if len(params) < 4 { + c.fwrite(":server 461 %s CHATHISTORY :Not enough parameters\r\n", c.nick) + return + } + msgid := params[2] // format: msgid=12345 + msgid = strings.TrimPrefix(msgid, "msgid=") + idx := findMsgIndex(ch.history, msgid) + if idx == -1 { + c.fwrite(":server 718 %s %s :No history available\r\n", c.nick, target) + return + } + start := idx - count + if start < 0 { + start = 0 + } + msgs = ch.history[start:idx] + + case "AFTER": + if len(params) < 4 { + c.fwrite(":server 461 %s CHATHISTORY :Not enough parameters\r\n", c.nick) + return + } + msgid := strings.TrimPrefix(params[2], "msgid=") + idx := findMsgIndex(ch.history, msgid) + if idx == -1 { + c.fwrite(":server 718 %s %s :No history available\r\n", c.nick, target) + return + } + end := idx + 1 + count + if end > len(ch.history) { + end = len(ch.history) + } + msgs = ch.history[idx+1 : end] + + case "AROUND": + if len(params) < 4 { + c.fwrite(":server 461 %s CHATHISTORY :Not enough parameters\r\n", c.nick) + return + } + msgid := strings.TrimPrefix(params[2], "msgid=") + idx := findMsgIndex(ch.history, msgid) + if idx == -1 { + c.fwrite(":server 718 %s %s :No history available\r\n", c.nick, target) + return + } + half := count / 2 + start := idx - half + if start < 0 { + start = 0 + } + end := idx + half + 1 + if end > len(ch.history) { + end = len(ch.history) + } + msgs = ch.history[start:end] + } + batchID := fmt.Sprintf("hist-%d", time.Now().UnixNano()) + c.fwrite(":server BATCH +%s chathistory %s\r\n", batchID, target) + + for _, m := range msgs { + tags := fmt.Sprintf("@msgid=%s;time=%s", + m.MsgID, + m.Time.UTC().Format(time.RFC3339)) + c.fwrite("%s :%s %s %s :%s\r\n", + tags, + m.Prefix, + m.Command, + strings.Join(m.Params, " "), + m.Text) + } + + c.fwrite(":server BATCH -%s\r\n", batchID) +} +func findMsgIndex(history []HistoryMessage, msgid string) int { + for i, m := range history { + if m.MsgID == msgid { + return i + } + } + return -1 +} + func kickUser(sender *Client, channelName, targetNick, reason string) { chMu.Lock() ch, ok := channels[channelName] @@ -482,6 +612,7 @@ func joinChannel(c *Client, name string) { name: name, members: make(map[string]*Client), ops: make(map[string]bool), + history: nil, description: "none", } channels[name] = ch @@ -501,6 +632,12 @@ func joinChannel(c *Client, name string) { for _, member := range ch.members { fmt.Fprintf(member.conn, ":%s JOIN %s\r\n", c.nick, name) } + + if ch.ops[c.nick] { + for _, member := range ch.members { + member.fwrite(":server MODE %s +o %s\r\n", name, c.nick) + } + } c.sendNames(ch) } func (c *Client) partChannel(name string) { @@ -518,7 +655,7 @@ func (c *Client) partChannel(name string) { fmt.Fprintf(member.conn, ":%s PART %s\r\n", c.nick, name) } ch.mu.Unlock() - broadcastNames(ch) + // broadcastNames(ch) } func addChannel(name string, description string) { name = normalizeChannelName(name) @@ -535,6 +672,7 @@ func addChannel(name string, description string) { } } func sendMessage(sender *Client, target, text string) { + // TODO: handle when a user sends a message to a channel theyre not in cliMu.Lock() if c, ok := clients[target]; ok { c.fwrite(":%s!%s@%s PRIVMSG %s :%s\r\n", sender.nick, sender.user, sender.host, target, text) @@ -548,6 +686,13 @@ func sendMessage(sender *Client, target, text string) { if !ok { return } + _, member := ch.members[sender.nick] + ch.mu.RUnlock() + if !member { + sender.fwrite(":server 404 %s %s :Cannot send to channel\r\n", + sender.nick, target) + return + } ch.mu.Lock() defer ch.mu.Unlock() for _, member := range ch.members { @@ -555,9 +700,9 @@ func sendMessage(sender *Client, target, text string) { fmt.Fprintf(member.conn, ":%s!%s@%s PRIVMSG %s :%s\r\n", sender.nick, sender.user, sender.host, target, text) } } + storeMessage(ch, sender, target, text) } func (c *Client) checkRegistration() { - //log.Printf("checkRegistration called: nick=%q user=%q registered=%v", c.nick, c.user, c.registered) if c.nick != "" && c.user != "" && !c.registered { if c.saslRequired && !c.saslComplete { return @@ -568,8 +713,12 @@ func (c *Client) checkRegistration() { c.fwrite(":server 003 %s :This server was created just now\r\n", c.nick) c.fwrite(":server 004 %s server irc 0.1\r\n", c.nick) fmt.Fprintf(c.conn, ":server 375 %s :- Message of the Day -\r\n", c.nick) - for _, i := range motdString { - c.fwrite(":server 372 %s :- %s\r\n", c.nick, i) + if motdString != nil { + for _, i := range motdString { + c.fwrite(":server 372 %s :- %s\r\n", c.nick, i) + } + } else { + c.fwrite("%s :MOTD File is missing", c.nick) } fmt.Fprintf(c.conn, ":server 376 %s :End of MOTD\r\n", c.nick) } @@ -633,6 +782,20 @@ func (c *Client) handleList() { // End of list c.fwrite(":server 323 %s :End of /LIST\r\n", c.nick) } +func storeMessage(ch *Channel, sender *Client, target, text string) { + msg := HistoryMessage{ + MsgID: fmt.Sprintf("%d", time.Now().UnixNano()), + Time: time.Now(), + Prefix: fmt.Sprintf("%s!%s@%s", sender.nick, sender.user, sender.host), + Command: "PRIVMSG", + Params: []string{target}, + Text: text, + } + ch.history = append(ch.history, msg) + if len(ch.history) > 1000 { + ch.history = ch.history[1:] // drop oldest + } +} func nicknameExists(nick string) bool { cliMu.RLock() defer cliMu.RUnlock() @@ -646,11 +809,13 @@ func (c *Client) cleanup() { for name, ch := range c.channels { ch.mu.Lock() delete(ch.members, c.nick) - for _, member := range ch.members { + for nick, member := range ch.members { member.fwrite(":%s PART %s\r\n", c.nick, name) + if member == c { + delete(ch.members, nick) + } } ch.mu.Unlock() - broadcastNames(ch) delete(c.channels, name) } cliMu.RLock() @@ -702,7 +867,7 @@ func normalizeChannelName(name string) string { func checkCredentials(user, pass string) bool { file, err := os.Open("accounts.txt") if err != nil { - log.Printf("checkCredentials: open error: %v", err) + log.Printf("open error: %v", err) return false } defer file.Close() diff --git a/motd.txt b/motd.txt index adcc736..6c1909d 100644 --- a/motd.txt +++ b/motd.txt @@ -1,4 +1,4 @@ -welcome to thinIRC +welcome to Gocirc! plz follow the rules: no discriminating by religion no racial slurs allowed \ No newline at end of file