Gocirc/http.go

202 lines
4.8 KiB
Go

package main
import (
"encoding/json"
"fmt"
"log"
"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"`
Error string `json:"error"`
}
type KickResponse struct {
Action string `json:"action"`
Target string `json:"target"`
Channel string `json:"channel"`
Reason string `json:"reason"`
Status string `json:"status"`
Error string `json:"error"`
}
func startHTTP(cfg Config) {
htport := cfg.Webapiport
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 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
}
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",
Error: "",
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}
func handleModeAPI(w http.ResponseWriter, r *http.Request) {
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.Error(w, "Unknown MODE flag", http.StatusBadRequest)
return
}
resp := ModeResponse{
Action: "mode",
Mode: mode,
Target: targetNick,
Channel: channelName,
Status: "ok",
Error: "",
}
w.Header().Set("Content-Type", "application/json")
enc := json.NewEncoder(w)
enc.SetIndent(" ", " ")
enc.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) {
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")
enc := json.NewEncoder(w)
enc.SetIndent(" ", " ")
if err := enc.Encode(list); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
func handleMOTDreload(w http.ResponseWriter, r *http.Request) {
loadMOTD(motdFile)
resp := map[string]any{
"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)
}
}