490 lines
16 KiB
Go
490 lines
16 KiB
Go
package handlers
|
|
|
|
import (
|
|
"log"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/gorilla/mux"
|
|
|
|
"github.com/orohi/vpn-panel/internal/db"
|
|
"github.com/orohi/vpn-panel/internal/models"
|
|
"github.com/orohi/vpn-panel/internal/xraykeys"
|
|
)
|
|
|
|
func (s *Server) profilesList(w http.ResponseWriter, r *http.Request) {
|
|
profiles, err := db.ListProfiles(s.DB)
|
|
if err != nil {
|
|
http.Error(w, "db error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
s.render(w, "profiles.html", pageData{
|
|
"Title": "Профили",
|
|
"UserName": s.Auth.CurrentName(r),
|
|
"Profiles": profiles,
|
|
"Active": "profiles",
|
|
"Flash": r.URL.Query().Get("ok"),
|
|
"Error": r.URL.Query().Get("err"),
|
|
})
|
|
}
|
|
|
|
func (s *Server) profilesNewGet(w http.ResponseWriter, r *http.Request) {
|
|
s.render(w, "profile_new.html", pageData{
|
|
"Title": "Новый профиль",
|
|
"UserName": s.Auth.CurrentName(r),
|
|
"Active": "profiles",
|
|
})
|
|
}
|
|
|
|
func (s *Server) profilesCreate(w http.ResponseWriter, r *http.Request) {
|
|
_ = r.ParseForm()
|
|
name := strings.TrimSpace(r.FormValue("name"))
|
|
desc := strings.TrimSpace(r.FormValue("description"))
|
|
if name == "" {
|
|
s.render(w, "profile_new.html", pageData{
|
|
"Title": "Новый профиль", "UserName": s.Auth.CurrentName(r), "Active": "profiles",
|
|
"Error": "Укажите имя профиля", "Form": map[string]any{"Name": name, "Description": desc},
|
|
})
|
|
return
|
|
}
|
|
p := &models.ConfigProfile{Name: name, Description: desc}
|
|
if err := db.CreateProfile(s.DB, p); err != nil {
|
|
log.Printf("create profile: %v", err)
|
|
s.render(w, "profile_new.html", pageData{
|
|
"Title": "Новый профиль", "UserName": s.Auth.CurrentName(r), "Active": "profiles",
|
|
"Error": "Не удалось создать (возможно имя занято)", "Form": map[string]any{"Name": name, "Description": desc},
|
|
})
|
|
return
|
|
}
|
|
http.Redirect(w, r, "/admin/profiles/"+p.ID.String()+"?ok=created", http.StatusSeeOther)
|
|
}
|
|
|
|
func (s *Server) profileView(w http.ResponseWriter, r *http.Request) {
|
|
id, err := uuid.Parse(mux.Vars(r)["id"])
|
|
if err != nil {
|
|
http.Error(w, "invalid id", http.StatusBadRequest)
|
|
return
|
|
}
|
|
p, err := db.GetProfile(s.DB, id)
|
|
if err != nil || p == nil {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
protocols, _ := db.ListProtocols(s.DB)
|
|
s.render(w, "profile_view.html", pageData{
|
|
"Title": p.Name,
|
|
"UserName": s.Auth.CurrentName(r),
|
|
"Active": "profiles",
|
|
"Profile": p,
|
|
"Protocols": inboundProtocols(protocols),
|
|
"Flash": r.URL.Query().Get("ok"),
|
|
"Error": r.URL.Query().Get("err"),
|
|
})
|
|
}
|
|
|
|
func (s *Server) profileUpdate(w http.ResponseWriter, r *http.Request) {
|
|
id, err := uuid.Parse(mux.Vars(r)["id"])
|
|
if err != nil {
|
|
http.Error(w, "invalid id", http.StatusBadRequest)
|
|
return
|
|
}
|
|
_ = r.ParseForm()
|
|
p := &models.ConfigProfile{
|
|
ID: id,
|
|
Name: strings.TrimSpace(r.FormValue("name")),
|
|
Description: strings.TrimSpace(r.FormValue("description")),
|
|
}
|
|
if p.Name == "" {
|
|
http.Redirect(w, r, "/admin/profiles/"+id.String()+"?err=name", http.StatusSeeOther)
|
|
return
|
|
}
|
|
if err := db.UpdateProfile(s.DB, p); err != nil {
|
|
http.Redirect(w, r, "/admin/profiles/"+id.String()+"?err=save", http.StatusSeeOther)
|
|
return
|
|
}
|
|
http.Redirect(w, r, "/admin/profiles/"+id.String()+"?ok=saved", http.StatusSeeOther)
|
|
}
|
|
|
|
func (s *Server) profileDelete(w http.ResponseWriter, r *http.Request) {
|
|
id, err := uuid.Parse(mux.Vars(r)["id"])
|
|
if err != nil {
|
|
http.Error(w, "invalid id", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if err := db.DeleteProfile(s.DB, id); err != nil {
|
|
http.Redirect(w, r, "/admin/profiles?err=delete", http.StatusSeeOther)
|
|
return
|
|
}
|
|
http.Redirect(w, r, "/admin/profiles?ok=deleted", http.StatusSeeOther)
|
|
}
|
|
|
|
func parseInboundForm(r *http.Request, profileID, protocolID uuid.UUID, proto *models.Protocol) (*models.Inbound, error) {
|
|
port, _ := strconv.Atoi(r.FormValue("port"))
|
|
if port <= 0 {
|
|
port = proto.Port
|
|
if port <= 0 {
|
|
port = 443
|
|
}
|
|
}
|
|
tag := strings.TrimSpace(r.FormValue("tag"))
|
|
if tag == "" {
|
|
tag = strings.ToUpper(proto.Code) + "-" + strconv.Itoa(port)
|
|
}
|
|
sortOrder, _ := strconv.Atoi(r.FormValue("sort_order"))
|
|
network := r.FormValue("network")
|
|
security := r.FormValue("security")
|
|
flow := strings.TrimSpace(r.FormValue("flow"))
|
|
if security == "reality" && network == "tcp" && flow == "" && proto.Code == "vless" {
|
|
flow = "xtls-rprx-vision"
|
|
}
|
|
in := &models.Inbound{
|
|
ProfileID: profileID,
|
|
Tag: tag,
|
|
ProtocolID: protocolID,
|
|
ProtocolCode: proto.Code,
|
|
Port: port,
|
|
Network: network,
|
|
Security: security,
|
|
Listen: strings.TrimSpace(r.FormValue("listen")),
|
|
Enabled: true,
|
|
SortOrder: sortOrder,
|
|
Remark: strings.TrimSpace(r.FormValue("remark")),
|
|
Path: strings.TrimSpace(r.FormValue("path")),
|
|
Host: strings.TrimSpace(r.FormValue("host")),
|
|
SNI: strings.TrimSpace(r.FormValue("sni")),
|
|
Fingerprint: strings.TrimSpace(r.FormValue("fingerprint")),
|
|
Flow: flow,
|
|
ALPN: strings.TrimSpace(r.FormValue("alpn")),
|
|
RealityDest: strings.TrimSpace(r.FormValue("reality_dest")),
|
|
RealityServerNames: strings.TrimSpace(r.FormValue("reality_server_names")),
|
|
RealityPrivateKey: strings.TrimSpace(r.FormValue("reality_private_key")),
|
|
RealityPublicKey: strings.TrimSpace(r.FormValue("reality_public_key")),
|
|
RealityShortID: strings.TrimSpace(r.FormValue("reality_short_id")),
|
|
RealityShortIDs: strings.TrimSpace(r.FormValue("reality_short_ids")),
|
|
SpiderX: strings.TrimSpace(r.FormValue("spider_x")),
|
|
SSMethod: strings.TrimSpace(r.FormValue("ss_method")),
|
|
Password: strings.TrimSpace(r.FormValue("password")),
|
|
FallbackDest: strings.TrimSpace(r.FormValue("fallback_dest")),
|
|
FallbacksJSON: strings.TrimSpace(r.FormValue("fallbacks_json")),
|
|
TLSCertPEM: strings.TrimSpace(r.FormValue("tls_cert_pem")),
|
|
TLSKeyPEM: strings.TrimSpace(r.FormValue("tls_key_pem")),
|
|
Sniffing: formCheckbox(r, "sniffing", true),
|
|
SniffingRouteOnly: formCheckbox(r, "sniffing_route_only", false),
|
|
}
|
|
if xver, err := strconv.Atoi(r.FormValue("reality_xver")); err == nil {
|
|
in.RealityXver = xver
|
|
}
|
|
if in.SpiderX == "" {
|
|
in.SpiderX = "/"
|
|
}
|
|
if in.Security == "reality" {
|
|
priv, pub, sid, dest, names, err := xraykeys.EnsureReality(
|
|
in.RealityPrivateKey, in.RealityPublicKey, in.RealityShortID,
|
|
in.RealityDest, in.RealityServerNames,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
in.RealityPrivateKey, in.RealityPublicKey, in.RealityShortID = priv, pub, sid
|
|
in.RealityDest, in.RealityServerNames = dest, names
|
|
if in.RealityShortIDs == "" {
|
|
in.RealityShortIDs = sid
|
|
}
|
|
if in.SNI == "" {
|
|
in.SNI = strings.Split(names, ",")[0]
|
|
}
|
|
if in.Fingerprint == "" {
|
|
in.Fingerprint = "chrome"
|
|
}
|
|
}
|
|
return in, nil
|
|
}
|
|
|
|
func (s *Server) syncProfileNodes(profileID uuid.UUID) {
|
|
nodes, _ := db.ListNodes(s.DB)
|
|
for _, n := range nodes {
|
|
if n.ProfileID != nil && *n.ProfileID == profileID {
|
|
_ = db.ApplyProfileInboundsToNodeProtocols(s.DB, n.ID)
|
|
if n.Status == models.NodeStatusOnline {
|
|
nn := n
|
|
_ = s.syncNodeConfig(&nn)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *Server) syncNodesForInbounds(inboundIDs []uuid.UUID) {
|
|
nodes, err := db.ListOnlineNodesByInboundIDs(s.DB, inboundIDs)
|
|
if err != nil {
|
|
log.Printf("list nodes for inbounds: %v", err)
|
|
return
|
|
}
|
|
for i := range nodes {
|
|
_ = s.syncNodeConfig(&nodes[i])
|
|
}
|
|
}
|
|
|
|
func (s *Server) inboundCreate(w http.ResponseWriter, r *http.Request) {
|
|
profileID, err := uuid.Parse(mux.Vars(r)["id"])
|
|
if err != nil {
|
|
http.Error(w, "invalid id", http.StatusBadRequest)
|
|
return
|
|
}
|
|
_ = r.ParseForm()
|
|
protocolID, err := uuid.Parse(r.FormValue("protocol_id"))
|
|
if err != nil {
|
|
http.Redirect(w, r, "/admin/profiles/"+profileID.String()+"?err=protocol", http.StatusSeeOther)
|
|
return
|
|
}
|
|
proto, err := db.GetProtocol(s.DB, protocolID)
|
|
if err != nil || proto == nil {
|
|
http.Redirect(w, r, "/admin/profiles/"+profileID.String()+"?err=protocol", http.StatusSeeOther)
|
|
return
|
|
}
|
|
switch proto.Code {
|
|
case "vless", "vmess", "trojan", "shadowsocks":
|
|
default:
|
|
http.Redirect(w, r, "/admin/profiles/"+profileID.String()+"?err=protocol", http.StatusSeeOther)
|
|
return
|
|
}
|
|
in, err := parseInboundForm(r, profileID, protocolID, proto)
|
|
if err != nil {
|
|
log.Printf("inbound form: %v", err)
|
|
http.Redirect(w, r, "/admin/profiles/"+profileID.String()+"?err=inbound", http.StatusSeeOther)
|
|
return
|
|
}
|
|
if err := db.CreateInbound(s.DB, in); err != nil {
|
|
log.Printf("create inbound: %v", err)
|
|
http.Redirect(w, r, "/admin/profiles/"+profileID.String()+"?err=inbound", http.StatusSeeOther)
|
|
return
|
|
}
|
|
nodes, _ := db.ListNodes(s.DB)
|
|
for _, n := range nodes {
|
|
if n.ProfileID != nil && *n.ProfileID == profileID {
|
|
_ = db.SetNodeInboundEnabled(s.DB, n.ID, in.ID, true)
|
|
if n.Status == models.NodeStatusOnline {
|
|
nn := n
|
|
_ = s.syncNodeConfig(&nn)
|
|
}
|
|
}
|
|
}
|
|
http.Redirect(w, r, "/admin/profiles/"+profileID.String()+"?ok=inbound_added", http.StatusSeeOther)
|
|
}
|
|
|
|
func (s *Server) inboundEditGet(w http.ResponseWriter, r *http.Request) {
|
|
profileID, err := uuid.Parse(mux.Vars(r)["id"])
|
|
if err != nil {
|
|
http.Error(w, "invalid id", http.StatusBadRequest)
|
|
return
|
|
}
|
|
inboundID, err := uuid.Parse(mux.Vars(r)["inboundID"])
|
|
if err != nil {
|
|
http.Error(w, "invalid inbound", http.StatusBadRequest)
|
|
return
|
|
}
|
|
in, err := db.GetInbound(s.DB, inboundID)
|
|
if err != nil || in == nil || in.ProfileID != profileID {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
p, err := db.GetProfile(s.DB, profileID)
|
|
if err != nil || p == nil {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
protocols, _ := db.ListProtocols(s.DB)
|
|
s.render(w, "inbound_edit.html", pageData{
|
|
"Title": "Inbound " + in.Tag,
|
|
"UserName": s.Auth.CurrentName(r),
|
|
"Active": "profiles",
|
|
"Profile": p,
|
|
"Inbound": in,
|
|
"Protocols": inboundProtocols(protocols),
|
|
"Flash": r.URL.Query().Get("ok"),
|
|
"Error": r.URL.Query().Get("err"),
|
|
})
|
|
}
|
|
|
|
func inboundProtocols(all []models.Protocol) []models.Protocol {
|
|
var out []models.Protocol
|
|
for _, p := range all {
|
|
switch p.Code {
|
|
case "vless", "vmess", "trojan", "shadowsocks":
|
|
out = append(out, p)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func formCheckbox(r *http.Request, name string, defaultOn bool) bool {
|
|
vals := r.Form[name]
|
|
if len(vals) == 0 {
|
|
return defaultOn
|
|
}
|
|
on := false
|
|
for _, v := range vals {
|
|
if v == "on" || v == "true" || v == "1" {
|
|
on = true
|
|
}
|
|
}
|
|
return on
|
|
}
|
|
|
|
func (s *Server) inboundUpdate(w http.ResponseWriter, r *http.Request) {
|
|
profileID, err := uuid.Parse(mux.Vars(r)["id"])
|
|
if err != nil {
|
|
http.Error(w, "invalid id", http.StatusBadRequest)
|
|
return
|
|
}
|
|
inboundID, err := uuid.Parse(mux.Vars(r)["inboundID"])
|
|
if err != nil {
|
|
http.Error(w, "invalid inbound", http.StatusBadRequest)
|
|
return
|
|
}
|
|
existing, err := db.GetInbound(s.DB, inboundID)
|
|
if err != nil || existing == nil || existing.ProfileID != profileID {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
_ = r.ParseForm()
|
|
protocolID, err := uuid.Parse(r.FormValue("protocol_id"))
|
|
if err != nil {
|
|
http.Redirect(w, r, "/admin/profiles/"+profileID.String()+"/inbounds/"+inboundID.String()+"?err=protocol", http.StatusSeeOther)
|
|
return
|
|
}
|
|
proto, err := db.GetProtocol(s.DB, protocolID)
|
|
if err != nil || proto == nil {
|
|
http.Redirect(w, r, "/admin/profiles/"+profileID.String()+"/inbounds/"+inboundID.String()+"?err=protocol", http.StatusSeeOther)
|
|
return
|
|
}
|
|
switch proto.Code {
|
|
case "vless", "vmess", "trojan", "shadowsocks":
|
|
default:
|
|
http.Redirect(w, r, "/admin/profiles/"+profileID.String()+"/inbounds/"+inboundID.String()+"?err=protocol", http.StatusSeeOther)
|
|
return
|
|
}
|
|
in, err := parseInboundForm(r, profileID, protocolID, proto)
|
|
if err != nil {
|
|
http.Redirect(w, r, "/admin/profiles/"+profileID.String()+"/inbounds/"+inboundID.String()+"?err=keys", http.StatusSeeOther)
|
|
return
|
|
}
|
|
in.ID = inboundID
|
|
in.Enabled = false
|
|
for _, v := range r.Form["enabled"] {
|
|
if v == "on" || v == "true" {
|
|
in.Enabled = true
|
|
}
|
|
}
|
|
if in.Security == "reality" {
|
|
if strings.TrimSpace(r.FormValue("reality_private_key")) == "" {
|
|
in.RealityPrivateKey = existing.RealityPrivateKey
|
|
}
|
|
if strings.TrimSpace(r.FormValue("reality_public_key")) == "" {
|
|
in.RealityPublicKey = existing.RealityPublicKey
|
|
}
|
|
if strings.TrimSpace(r.FormValue("reality_short_id")) == "" && existing.RealityShortID != "" {
|
|
in.RealityShortID = existing.RealityShortID
|
|
}
|
|
}
|
|
if err := db.UpdateInbound(s.DB, in); err != nil {
|
|
log.Printf("update inbound: %v", err)
|
|
http.Redirect(w, r, "/admin/profiles/"+profileID.String()+"/inbounds/"+inboundID.String()+"?err=save", http.StatusSeeOther)
|
|
return
|
|
}
|
|
s.syncProfileNodes(profileID)
|
|
http.Redirect(w, r, "/admin/profiles/"+profileID.String()+"/inbounds/"+inboundID.String()+"?ok=saved", http.StatusSeeOther)
|
|
}
|
|
|
|
func (s *Server) inboundDelete(w http.ResponseWriter, r *http.Request) {
|
|
profileID, err := uuid.Parse(mux.Vars(r)["id"])
|
|
if err != nil {
|
|
http.Error(w, "invalid id", http.StatusBadRequest)
|
|
return
|
|
}
|
|
inboundID, err := uuid.Parse(mux.Vars(r)["inboundID"])
|
|
if err != nil {
|
|
http.Error(w, "invalid inbound", http.StatusBadRequest)
|
|
return
|
|
}
|
|
_ = db.DeleteInbound(s.DB, inboundID)
|
|
s.syncProfileNodes(profileID)
|
|
http.Redirect(w, r, "/admin/profiles/"+profileID.String()+"?ok=inbound_deleted", http.StatusSeeOther)
|
|
}
|
|
|
|
func (s *Server) inboundToggle(w http.ResponseWriter, r *http.Request) {
|
|
profileID, err := uuid.Parse(mux.Vars(r)["id"])
|
|
if err != nil {
|
|
http.Error(w, "invalid id", http.StatusBadRequest)
|
|
return
|
|
}
|
|
inboundID, err := uuid.Parse(mux.Vars(r)["inboundID"])
|
|
if err != nil {
|
|
http.Error(w, "invalid inbound", http.StatusBadRequest)
|
|
return
|
|
}
|
|
_ = db.ToggleInbound(s.DB, inboundID)
|
|
s.syncProfileNodes(profileID)
|
|
http.Redirect(w, r, "/admin/profiles/"+profileID.String()+"?ok=inbound_toggled", http.StatusSeeOther)
|
|
}
|
|
|
|
func (s *Server) nodeAssignProfile(w http.ResponseWriter, r *http.Request) {
|
|
nodeID, err := uuid.Parse(mux.Vars(r)["id"])
|
|
if err != nil {
|
|
http.Error(w, "invalid id", http.StatusBadRequest)
|
|
return
|
|
}
|
|
_ = r.ParseForm()
|
|
raw := strings.TrimSpace(r.FormValue("profile_id"))
|
|
n, err := db.GetNode(s.DB, nodeID)
|
|
if err != nil || n == nil {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
if raw == "" || raw == "none" {
|
|
_ = db.ClearNodeProfile(s.DB, nodeID)
|
|
http.Redirect(w, r, "/admin/nodes/"+nodeID.String()+"?ok=profile_cleared", http.StatusSeeOther)
|
|
return
|
|
}
|
|
profileID, err := uuid.Parse(raw)
|
|
if err != nil {
|
|
http.Redirect(w, r, "/admin/nodes/"+nodeID.String()+"?err=profile", http.StatusSeeOther)
|
|
return
|
|
}
|
|
if err := db.AssignProfileToNode(s.DB, nodeID, profileID); err != nil {
|
|
log.Printf("assign profile: %v", err)
|
|
http.Redirect(w, r, "/admin/nodes/"+nodeID.String()+"?err=profile_assign", http.StatusSeeOther)
|
|
return
|
|
}
|
|
n2, _ := db.GetNode(s.DB, nodeID)
|
|
if n2 != nil && n2.Status == models.NodeStatusOnline {
|
|
_ = s.syncNodeConfig(n2)
|
|
}
|
|
http.Redirect(w, r, "/admin/nodes/"+nodeID.String()+"?ok=profile_assigned", http.StatusSeeOther)
|
|
}
|
|
|
|
func (s *Server) nodeInboundToggle(w http.ResponseWriter, r *http.Request) {
|
|
nodeID, err := uuid.Parse(mux.Vars(r)["id"])
|
|
if err != nil {
|
|
http.Error(w, "invalid id", http.StatusBadRequest)
|
|
return
|
|
}
|
|
inboundID, err := uuid.Parse(mux.Vars(r)["inboundID"])
|
|
if err != nil {
|
|
http.Error(w, "invalid inbound", http.StatusBadRequest)
|
|
return
|
|
}
|
|
_ = r.ParseForm()
|
|
enabled := r.FormValue("action") == "enable"
|
|
if err := db.SetNodeInboundEnabled(s.DB, nodeID, inboundID, enabled); err != nil {
|
|
http.Redirect(w, r, "/admin/nodes/"+nodeID.String()+"?err=inbound", http.StatusSeeOther)
|
|
return
|
|
}
|
|
n, _ := db.GetNode(s.DB, nodeID)
|
|
if n != nil && n.Status == models.NodeStatusOnline {
|
|
_ = s.syncNodeConfig(n)
|
|
}
|
|
http.Redirect(w, r, "/admin/nodes/"+nodeID.String()+"?ok=inbound_updated", http.StatusSeeOther)
|
|
}
|