diff --git a/.gitignore b/.gitignore index 921d1fe..24da6c3 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,10 @@ go.work.sum # env file .env +#sensitive config file and cred file +config.json +accounts.txt +ircserver # Editor/IDE # .idea/ # .vscode/ diff --git a/http.go b/http.go index cb21f54..16f1c2f 100644 --- a/http.go +++ b/http.go @@ -1,19 +1,201 @@ package main import ( + "encoding/json" "fmt" "net/http" ) +type ModeResponse struct { + Action string `json:"action"` + Mode string `json:"mode"` + Target string `json:"target"` + Channel string `json:"channel"` + Status string `json:"status"` +} +type KickResponse struct { + Action string `json:"action"` + Target string `json:"target"` + Channel string `json:"channel"` + Reason string `json:"reason"` + Status string `json:"status"` +} + func startHTTP(cfg Config) { htport := cfg.Webapiport - http.HandleFunc("/admin/", handleAdmin) + 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) http.ListenAndServe(fmt.Sprintf(":%d", htport), nil) } -func handleAdmin(w http.ResponseWriter, r *http.Request) { - switch r.URL.Path { +func handleKick(w http.ResponseWriter, r *http.Request) { + user, pass, ok := r.BasicAuth() + if !ok || !checkCredentials(user, pass) || !globalOps[user] { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + if r.Method != http.MethodPost { + http.Error(w, "POST required", http.StatusMethodNotAllowed) + return + } + channelName := normalizeChannelName(r.FormValue("channel")) + targetNick := r.FormValue("nick") + reason := r.FormValue("reason") + if reason == "" { + reason = "Kicked via API" + } + + chMu.Lock() + ch, ok := channels[channelName] + chMu.Unlock() + if !ok { + http.Error(w, "No such channel", http.StatusNotFound) + return + } + + ch.mu.Lock() + defer ch.mu.Unlock() + + target, ok := ch.members[targetNick] + if !ok { + http.Error(w, "Target not in channel", http.StatusNotFound) + return + } + if globalOps[targetNick] { + http.Error(w, "Cannot kick global operator", http.StatusForbidden) + return + } + for _, member := range ch.members { + member.fwrite(":server KICK %s %s :%s\r\n", channelName, targetNick, reason) + } + + // Remove target + delete(ch.members, targetNick) + delete(target.channels, channelName) + + resp := KickResponse{ + Action: "kick", + Target: targetNick, + Channel: channelName, + Reason: reason, + Status: "ok", + } + 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] { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + if r.Method != http.MethodPost { + http.Error(w, "POST required", http.StatusMethodNotAllowed) + return + } + channelName := normalizeChannelName(r.FormValue("channel")) + mode := r.FormValue("mode") + targetNick := r.FormValue("nick") + + chMu.Lock() + ch, ok := channels[channelName] + chMu.Unlock() + if !ok { + http.Error(w, "No such channel", http.StatusNotFound) + return + } + + ch.mu.Lock() + defer ch.mu.Unlock() + + switch mode { + case "+o": + if targetNick != "" { + ch.ops[targetNick] = true + for _, member := range ch.members { + member.fwrite(":api MODE %s +o %s\r\n", channelName, targetNick) + } + } + case "-o": + if targetNick != "" { + if globalOps[targetNick] { + http.Error(w, "Cannot remove global operator", http.StatusForbidden) + return + } + delete(ch.ops, targetNick) + for _, member := range ch.members { + member.fwrite(":api MODE %s -o %s\r\n", channelName, targetNick) + } + } default: - http.NotFound(w, r) + http.Error(w, "Unknown MODE flag", http.StatusBadRequest) + return + } + + resp := ModeResponse{ + Action: "mode", + Mode: mode, + Target: targetNick, + Channel: channelName, + Status: "ok", + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(resp) +} + +type ChannelInfo struct { + Name string `json:"name"` + Description string `json:"description"` + Users int `json:"users"` +} + +func handleListAPI(w http.ResponseWriter, r *http.Request) { + user, pass, ok := r.BasicAuth() + if !ok || !checkCredentials(user, pass) || !globalOps[user] { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + chMu.Lock() + defer chMu.Unlock() + + var list []ChannelInfo + for _, ch := range channels { + ch.mu.Lock() + count := len(ch.members) + desc := ch.description + name := ch.name + ch.mu.Unlock() + + list = append(list, ChannelInfo{ + Name: name, + Description: desc, + Users: count, + }) + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(list); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} +func handleMOTDreload(w http.ResponseWriter, r *http.Request) { + user, pass, ok := r.BasicAuth() + if !ok || !checkCredentials(user, pass) || !globalOps[user] { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + loadMOTD(motdFile) + type htresp struct { + Status string `json:"status"` + } + resp := htresp{ + Status: "ok", + } + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(resp); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) } } diff --git a/main.go b/main.go index 1d3f7ba..a6386bf 100644 --- a/main.go +++ b/main.go @@ -64,11 +64,11 @@ func loadConfig() (*Config, error) { var conf Config jsdata, err := os.ReadFile("config.json") if err != nil { - return 0, err + return nil, err } err = json.Unmarshal(jsdata, &conf) if err != nil { - return 0, err + return nil, err } for _, cfg := range conf.Channels { addChannel(cfg.Channel, cfg.Description) @@ -78,7 +78,7 @@ func loadConfig() (*Config, error) { globalOps[cleanedNick] = true } motdFile = conf.Motd - return conf.Port, nil + return &conf, nil } func loadMOTD(filename string) error { file, err := os.Open(filename)