202 lines
4.8 KiB
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)
|
|
}
|
|
}
|