149 lines
4.5 KiB
Go
149 lines
4.5 KiB
Go
package server
|
|
|
|
import (
|
|
"log"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
func (s *Server) handleForwardsPage(w http.ResponseWriter, r *http.Request) {
|
|
id := r.PathValue("id")
|
|
a, err := s.db.GetAgent(id)
|
|
if err != nil || a == nil {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
rules, _ := s.db.ListForwardRulesByAgent(id)
|
|
errMsg := map[string]string{
|
|
"busy": "Этот порт хоста уже занят другим правилом.",
|
|
"tunnel": "Этот порт занят прямым туннелем панели (см. Настройки).",
|
|
"port": "Некорректный номер порта.",
|
|
"listen": "Не удалось занять порт на хосте (возможно, уже используется).",
|
|
"db": "Ошибка базы данных.",
|
|
}[r.URL.Query().Get("err")]
|
|
s.renderPage(w, r, "forwards", map[string]any{
|
|
"Agent": a,
|
|
"Rules": rules,
|
|
"CSRF": csrfToken(r),
|
|
"Error": errMsg,
|
|
"TunnelPort": s.TunnelPort(),
|
|
"PanelPort": panelPort(s.db.HostAddress()),
|
|
"UfwEnabled": s.ufw.enabled(),
|
|
})
|
|
}
|
|
|
|
// panelPort is the port of the panel URL agents dial, for the mode hints.
|
|
func panelPort(host string) string {
|
|
u, err := url.Parse(host)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
if p := u.Port(); p != "" {
|
|
return p
|
|
}
|
|
if u.Scheme == "http" {
|
|
return "80"
|
|
}
|
|
return "443"
|
|
}
|
|
|
|
// POST /agents/{id}/forwards/add
|
|
func (s *Server) handleForwardAdd(w http.ResponseWriter, r *http.Request) {
|
|
if !checkCSRF(r) {
|
|
http.Error(w, "bad csrf", http.StatusForbidden)
|
|
return
|
|
}
|
|
id := r.PathValue("id")
|
|
a, err := s.db.GetAgent(id)
|
|
if err != nil || a == nil {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
clientPort, _ := strconv.Atoi(strings.TrimSpace(r.FormValue("client_port")))
|
|
hostPort, _ := strconv.Atoi(strings.TrimSpace(r.FormValue("host_port")))
|
|
listenAddr := strings.TrimSpace(r.FormValue("listen_addr"))
|
|
if listenAddr == "" {
|
|
listenAddr = "0.0.0.0"
|
|
}
|
|
protoKind := strings.TrimSpace(r.FormValue("proto"))
|
|
switch protoKind {
|
|
case "http", "tcp", "udp":
|
|
default:
|
|
protoKind = "http"
|
|
}
|
|
if clientPort < 1 || clientPort > 65535 || hostPort < 1 || hostPort > 65535 {
|
|
http.Redirect(w, r, "/agents/"+id+"/forwards?err=port", http.StatusSeeOther)
|
|
return
|
|
}
|
|
// Reject a host port already bound by another forward rule or the tunnel.
|
|
if hostPort == s.TunnelPort() {
|
|
http.Redirect(w, r, "/agents/"+id+"/forwards?err=tunnel", http.StatusSeeOther)
|
|
return
|
|
}
|
|
if s.db.HostPortUsed(hostPort) {
|
|
http.Redirect(w, r, "/agents/"+id+"/forwards?err=busy", http.StatusSeeOther)
|
|
return
|
|
}
|
|
rule := ForwardRule{AgentID: id, ClientPort: clientPort, HostPort: hostPort, ListenAddr: listenAddr, Proto: protoKind,
|
|
NoUfw: r.FormValue("no_ufw") != "", Label: forwardLabel(r.FormValue("label"))}
|
|
ruleID, err := s.db.CreateForwardRule(rule)
|
|
if err != nil {
|
|
http.Redirect(w, r, "/agents/"+id+"/forwards?err=db", http.StatusSeeOther)
|
|
return
|
|
}
|
|
rule.ID = ruleID
|
|
if err := s.fwd.open(rule); err != nil {
|
|
// Roll back the rule if the port can't be bound.
|
|
_ = s.db.DeleteForwardRule(ruleID)
|
|
log.Printf("forward open %s:%d: %v", listenAddr, hostPort, err)
|
|
http.Redirect(w, r, "/agents/"+id+"/forwards?err=listen", http.StatusSeeOther)
|
|
return
|
|
}
|
|
// Open the port in ufw too (if integration is enabled and ufw is active).
|
|
s.ufw.allowForward(rule)
|
|
http.Redirect(w, r, "/agents/"+id+"/forwards", http.StatusSeeOther)
|
|
}
|
|
|
|
// POST /forwards/{rid}/delete
|
|
func (s *Server) handleForwardDelete(w http.ResponseWriter, r *http.Request) {
|
|
if !checkCSRF(r) {
|
|
http.Error(w, "bad csrf", http.StatusForbidden)
|
|
return
|
|
}
|
|
rid, _ := strconv.ParseInt(r.PathValue("rid"), 10, 64)
|
|
rule, _ := s.db.GetForwardRule(rid)
|
|
if rule == nil || !s.db.CanUseAgent(s.currentAdmin(r), rule.AgentID) {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
s.removeForward(rid)
|
|
dest := "/"
|
|
if rule != nil {
|
|
dest = "/agents/" + rule.AgentID + "/forwards"
|
|
}
|
|
http.Redirect(w, r, dest, http.StatusSeeOther)
|
|
}
|
|
|
|
// removeForward closes a forward rule's listener and deletes the rule, along
|
|
// with the ufw rule bound to it (the shared tunnel port stays while another
|
|
// direct forward needs it).
|
|
func (s *Server) removeForward(rid int64) {
|
|
s.fwd.close(rid)
|
|
_ = s.db.DeleteForwardRule(rid)
|
|
s.ufw.revokeForRule(rid)
|
|
}
|
|
|
|
// forwardLabelMax bounds a rule's label, in characters.
|
|
const forwardLabelMax = 80
|
|
|
|
// forwardLabel tidies the optional note of a rule: one line, bounded length.
|
|
func forwardLabel(v string) string {
|
|
v = strings.Join(strings.Fields(v), " ")
|
|
if r := []rune(v); len(r) > forwardLabelMax {
|
|
v = string(r[:forwardLabelMax])
|
|
}
|
|
return v
|
|
}
|