Files
kanhole/server/admin/handler.go
T

1058 lines
28 KiB
Go

package admin
import (
"context"
"crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"html/template"
"net/http"
"strconv"
"sync"
"time"
"github.com/gorilla/mux"
"golang.org/x/crypto/bcrypt"
"github.com/fatedier/frp/pkg/db"
"github.com/fatedier/frp/pkg/db/ent"
"github.com/fatedier/frp/pkg/db/ent/frpcclient"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/db/ent/proxy"
"github.com/fatedier/frp/pkg/db/ent/user"
)
type sessionInfo struct {
UserID int
Username string
Expires time.Time
}
var (
sessionsMu sync.RWMutex
sessions = map[string]*sessionInfo{}
)
const sessionCookieName = "frp_admin_session"
const sessionMaxAge = 24 * time.Hour
func generateSessionID() string {
b := make([]byte, 32)
rand.Read(b)
return hex.EncodeToString(b)
}
func getSession(r *http.Request) *sessionInfo {
c, err := r.Cookie(sessionCookieName)
if err != nil {
return nil
}
sessionsMu.RLock()
s := sessions[c.Value]
sessionsMu.RUnlock()
if s == nil || time.Now().After(s.Expires) {
if s != nil {
sessionsMu.Lock()
delete(sessions, c.Value)
sessionsMu.Unlock()
}
return nil
}
return s
}
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
s := getSession(r)
if s == nil {
http.Redirect(w, r, "/admin/login", http.StatusFound)
return
}
next.ServeHTTP(w, r)
})
}
type Handler struct{}
func NewHandler() *Handler {
return &Handler{}
}
func (h *Handler) RegisterRoutes(r *mux.Router) {
s := r.PathPrefix("/admin").Subrouter()
s.HandleFunc("/setup", h.handleSetup).Methods("GET")
s.HandleFunc("/setup", h.handleSetupPost).Methods("POST")
s.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/admin/setup", http.StatusFound)
})
s.HandleFunc("/login", h.handleLogin).Methods("GET")
s.HandleFunc("/login", h.handleLoginPost).Methods("POST")
s.HandleFunc("/logout", h.handleLogout).Methods("POST")
// Frpc config API — key-based auth, no session required
s.HandleFunc("/api/frpc/proxy-config/{key}", h.handleFrpcProxyConfig).Methods("GET")
p := s.NewRoute().Subrouter()
p.Use(func(next http.Handler) http.Handler { return authMiddleware(next) })
// Client auth API — no session required
s.HandleFunc("/api/client/auth", h.handleClientAuth).Methods("POST")
p.HandleFunc("/dashboard", h.handleDashboard).Methods("GET")
p.HandleFunc("/clients", h.handleClients).Methods("GET")
p.HandleFunc("/clients/new", h.handleNewClient).Methods("GET")
p.HandleFunc("/clients/new", h.handleNewClientPost).Methods("POST")
p.HandleFunc("/clients/{id:[0-9]+}", h.handleClientDetail).Methods("GET")
p.HandleFunc("/clients/{id:[0-9]+}/provision", h.handleClientProvision).Methods("GET")
p.HandleFunc("/clients/{id:[0-9]+}/config", h.handleClientConfigDownload).Methods("GET")
p.HandleFunc("/clients/{id:[0-9]+}/generate-token", h.handleClientGenerateToken).Methods("POST")
p.HandleFunc("/proxies", h.handleProxies).Methods("GET")
p.HandleFunc("/proxies/new", h.handleNewProxy).Methods("GET")
p.HandleFunc("/proxies/new", h.handleNewProxyPost).Methods("POST")
p.HandleFunc("/proxies/{id:[0-9]+}", h.handleProxyDetail).Methods("GET")
p.HandleFunc("/settings", h.handleSettings).Methods("GET")
p.HandleFunc("/settings", h.handleSettingsSave).Methods("POST")
}
func (h *Handler) handleSetup(w http.ResponseWriter, r *http.Request) {
if db.HasAdmin() {
http.Redirect(w, r, "/admin/login", http.StatusFound)
return
}
renderTemplate(w, "setup.html", map[string]any{
"Error": "",
})
}
func (h *Handler) handleSetupPost(w http.ResponseWriter, r *http.Request) {
if db.HasAdmin() {
http.Redirect(w, r, "/admin/login", http.StatusFound)
return
}
r.ParseForm()
username := r.FormValue("username")
password := r.FormValue("password")
confirmPassword := r.FormValue("confirm_password")
name := r.FormValue("name")
if username == "" || password == "" {
renderTemplate(w, "setup.html", map[string]any{
"Error": "Username and password are required.",
})
return
}
if password != confirmPassword {
renderTemplate(w, "setup.html", map[string]any{
"Error": "Passwords do not match.",
})
return
}
if name == "" {
name = username
}
if err := db.CreateAdmin(username, password, name); err != nil {
renderTemplate(w, "setup.html", map[string]any{
"Error": "Failed to create admin user: " + err.Error(),
})
return
}
cfg := db.DefaultServerConfig()
if v := r.FormValue("bind_addr"); v != "" {
cfg.BindAddr = v
}
if v := r.FormValue("bind_port"); v != "" {
cfg.BindPort = atoi(v, 7000)
}
if v := r.FormValue("dashboard_addr"); v != "" {
cfg.WebServer.Addr = v
}
if v := r.FormValue("dashboard_port"); v != "" {
cfg.WebServer.Port = atoi(v, 7500)
}
if v := r.FormValue("auth_token"); v != "" {
cfg.Auth.Token = v
}
if err := db.SaveServerConfig(cfg); err != nil {
renderTemplate(w, "setup.html", map[string]any{
"Error": "Failed to save configuration: " + err.Error(),
})
return
}
http.Redirect(w, r, "/admin/login", http.StatusFound)
}
func (h *Handler) handleLogin(w http.ResponseWriter, r *http.Request) {
if !db.HasAdmin() {
http.Redirect(w, r, "/admin/setup", http.StatusFound)
return
}
if getSession(r) != nil {
http.Redirect(w, r, "/admin/dashboard", http.StatusFound)
return
}
renderTemplate(w, "login.html", map[string]any{
"Error": "",
})
}
func (h *Handler) handleLoginPost(w http.ResponseWriter, r *http.Request) {
username := r.FormValue("username")
password := r.FormValue("password")
if username == "" || password == "" {
renderTemplate(w, "login.html", map[string]any{
"Error": "Username and password are required.",
})
return
}
ctx := context.Background()
u, err := db.Client().User.Query().Where(user.UsernameEQ(username)).Only(ctx)
if err != nil {
renderTemplate(w, "login.html", map[string]any{
"Error": "Invalid username or password.",
})
return
}
if err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password)); err != nil {
renderTemplate(w, "login.html", map[string]any{
"Error": "Invalid username or password.",
})
return
}
sessionID := generateSessionID()
sessionsMu.Lock()
sessions[sessionID] = &sessionInfo{
UserID: u.ID,
Username: u.Username,
Expires: time.Now().Add(sessionMaxAge),
}
sessionsMu.Unlock()
http.SetCookie(w, &http.Cookie{
Name: sessionCookieName,
Value: sessionID,
Path: "/admin",
HttpOnly: true,
MaxAge: int(sessionMaxAge.Seconds()),
SameSite: http.SameSiteLaxMode,
})
http.Redirect(w, r, "/admin/dashboard", http.StatusFound)
}
func (h *Handler) handleLogout(w http.ResponseWriter, r *http.Request) {
c, err := r.Cookie(sessionCookieName)
if err == nil {
sessionsMu.Lock()
delete(sessions, c.Value)
sessionsMu.Unlock()
}
http.SetCookie(w, &http.Cookie{
Name: sessionCookieName,
Value: "",
Path: "/admin",
MaxAge: -1,
HttpOnly: true,
})
http.Redirect(w, r, "/admin/login", http.StatusFound)
}
func (h *Handler) handleDashboard(w http.ResponseWriter, r *http.Request) {
ctx := context.Background()
c := db.Client()
totalClients, _ := c.FrpcClient.Query().Count(ctx)
onlineClients, _ := c.FrpcClient.Query().Where(frpcclient.StatusEQ("online")).Count(ctx)
offlineClients, _ := c.FrpcClient.Query().Where(frpcclient.StatusEQ("offline")).Count(ctx)
totalUsers, _ := c.User.Query().Count(ctx)
activeProxies, _ := c.Proxy.Query().Where(proxy.StatusEQ("active")).Count(ctx)
allProxies, _ := c.Proxy.Query().All(ctx)
typeCounts := map[string]int{}
for _, p := range allProxies {
typeCounts[p.ProxyType]++
}
breakdown := make([]proxyBreakdown, 0, len(typeCounts))
for t, n := range typeCounts {
breakdown = append(breakdown, proxyBreakdown{Type: t, Count: n})
}
renderPage(w, r, "Dashboard", "dashboard.html", map[string]any{
"ActiveProxies": activeProxies,
"TotalClients": totalClients,
"TotalUsers": totalUsers,
"Uptime": "—",
"OnlineClients": onlineClients,
"OfflineClients": offlineClients,
"ProxyBreakdown": breakdown,
})
}
type proxyBreakdown struct {
Type string
Count int
}
type clientRow struct {
ID int
Name string
Addr string
Port int
Key string
Status string
LastSeen string
}
func (h *Handler) handleClients(w http.ResponseWriter, r *http.Request) {
ctx := context.Background()
all, err := db.Client().FrpcClient.Query().Order(ent.Desc(frpcclient.FieldCreatedAt)).All(ctx)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
rows := make([]clientRow, 0, len(all))
for _, c := range all {
lastSeen := ""
if !c.LastSeen.IsZero() {
lastSeen = c.LastSeen.Format("2006-01-02 15:04")
}
rows = append(rows, clientRow{
ID: c.ID, Name: c.Name, Addr: c.Addr, Port: c.Port,
Key: c.Key, Status: c.Status, LastSeen: lastSeen,
})
}
renderPage(w, r, "Clients", "clients.html", map[string]any{"Clients": rows})
}
func (h *Handler) handleClientDetail(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id, err := strconv.Atoi(vars["id"])
if err != nil {
http.Error(w, "invalid id", http.StatusBadRequest)
return
}
ctx := context.Background()
c, err := db.Client().FrpcClient.Get(ctx, id)
if err != nil {
http.Error(w, "client not found", http.StatusNotFound)
return
}
proxies, _ := db.Client().Proxy.Query().Where(proxy.ClientIDEQ(c.ID)).All(ctx)
type proxyRow struct {
ID int
Name string
ProxyType string
LocalIP string
LocalPort int
RemotePort int
Status string
}
prows := make([]proxyRow, 0, len(proxies))
for _, p := range proxies {
prows = append(prows, proxyRow{
ID: p.ID, Name: p.Name, ProxyType: p.ProxyType,
LocalIP: p.LocalIP, LocalPort: p.LocalPort,
RemotePort: p.RemotePort, Status: p.Status,
})
}
lastSeen := ""
if !c.LastSeen.IsZero() {
lastSeen = c.LastSeen.Format("2006-01-02 15:04")
}
renderPage(w, r, "Client: "+c.Name, "client_detail.html", map[string]any{
"Client": map[string]any{
"ID": c.ID, "Name": c.Name, "Addr": c.Addr, "Port": c.Port,
"Key": c.Key, "Status": c.Status,
"CreatedAt": c.CreatedAt.Format("2006-01-02 15:04"),
"LastSeen": lastSeen, "Metadata": c.Metadata,
},
"Proxies": prows,
})
}
type proxyRow struct {
ID int
Name string
ProxyType string
ClientName string
LocalIP string
LocalPort int
RemotePort int
Status string
}
func (h *Handler) handleProxies(w http.ResponseWriter, r *http.Request) {
ctx := context.Background()
all, err := db.Client().Proxy.Query().Order(ent.Desc(proxy.FieldCreatedAt)).All(ctx)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
rows := make([]proxyRow, 0, len(all))
for _, p := range all {
clientName := ""
if p.ClientID > 0 {
c, err := db.Client().FrpcClient.Get(ctx, p.ClientID)
if err == nil {
clientName = c.Name
}
}
rows = append(rows, proxyRow{
ID: p.ID, Name: p.Name, ProxyType: p.ProxyType,
ClientName: clientName, LocalIP: p.LocalIP,
LocalPort: p.LocalPort, RemotePort: p.RemotePort, Status: p.Status,
})
}
renderPage(w, r, "Proxies", "proxies.html", map[string]any{"Proxies": rows})
}
func (h *Handler) handleProxyDetail(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id, err := strconv.Atoi(vars["id"])
if err != nil {
http.Error(w, "invalid id", http.StatusBadRequest)
return
}
ctx := context.Background()
p, err := db.Client().Proxy.Get(ctx, id)
if err != nil {
http.Error(w, "proxy not found", http.StatusNotFound)
return
}
clientName := ""
if p.ClientID > 0 {
c, err := db.Client().FrpcClient.Get(ctx, p.ClientID)
if err == nil {
clientName = c.Name
}
}
renderPage(w, r, "Proxy: "+p.Name, "proxy_detail.html", map[string]any{
"Proxy": map[string]any{
"ID": p.ID, "Name": p.Name, "ProxyType": p.ProxyType,
"LocalIP": p.LocalIP, "LocalPort": p.LocalPort,
"RemotePort": p.RemotePort, "Status": p.Status,
"CustomDomains": p.CustomDomains, "ClientID": p.ClientID,
"CreatedAt": p.CreatedAt.Format("2006-01-02 15:04"),
"Metadata": p.Metadata,
},
"ClientName": clientName,
})
}
func generateClientKey() string {
b := make([]byte, 16)
rand.Read(b)
return hex.EncodeToString(b)
}
func (h *Handler) handleNewClient(w http.ResponseWriter, r *http.Request) {
renderPage(w, r, "New Client", "client_new.html", nil)
}
func (h *Handler) handleNewClientPost(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
name := r.FormValue("name")
if name == "" {
renderPage(w, r, "New Client", "client_new.html", map[string]any{
"Error": "Client name is required.",
})
return
}
ctx := context.Background()
key := generateClientKey()
c, err := db.Client().FrpcClient.Create().
SetName(name).
SetKey(key).
SetStatus("offline").
SetMetadata("{}").
Save(ctx)
if err != nil {
if ent.IsConstraintError(err) {
renderPage(w, r, "New Client", "client_new.html", map[string]any{
"Error": "A client with this name already exists.",
})
return
}
renderPage(w, r, "New Client", "client_new.html", map[string]any{
"Error": "Failed to create client: " + err.Error(),
})
return
}
http.Redirect(w, r, fmt.Sprintf("/admin/clients/%d/provision", c.ID), http.StatusFound)
}
func (h *Handler) handleClientProvision(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id, err := strconv.Atoi(vars["id"])
if err != nil {
http.Error(w, "invalid id", http.StatusBadRequest)
return
}
ctx := context.Background()
c, err := db.Client().FrpcClient.Get(ctx, id)
if err != nil {
http.Error(w, "client not found", http.StatusNotFound)
return
}
cfg, err := db.LoadServerConfig()
if err != nil {
http.Error(w, "failed to load config", http.StatusInternalServerError)
return
}
serverAddr := cfg.BindAddr
serverPort := cfg.BindPort
authToken := cfg.Auth.Token
frpcConfig := fmt.Sprintf(`serverAddr = %q
serverPort = %d
auth.token = %q
`, serverAddr, serverPort, authToken)
oneLiner := fmt.Sprintf(`curl -sL https://github.com/fatedier/frp/releases/latest/download/frp_linux_amd64.tar.gz | tar xz && cat > frpc.toml << 'EOF'
%s
EOF
./frp_*/frpc -c frpc.toml`, frpcConfig)
renderPage(w, r, "Provision: "+c.Name, "client_provision.html", map[string]any{
"Client": map[string]any{
"ID": c.ID,
"Name": c.Name,
"Key": c.Key,
},
"ServerAddr": serverAddr,
"ServerPort": serverPort,
"AuthToken": authToken,
"FrpcConfig": frpcConfig,
"OneLiner": oneLiner,
})
}
func (h *Handler) handleClientConfigDownload(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id, err := strconv.Atoi(vars["id"])
if err != nil {
http.Error(w, "invalid id", http.StatusBadRequest)
return
}
ctx := context.Background()
c, err := db.Client().FrpcClient.Get(ctx, id)
if err != nil {
http.Error(w, "client not found", http.StatusNotFound)
return
}
cfg, err := db.LoadServerConfig()
if err != nil {
http.Error(w, "failed to load config", http.StatusInternalServerError)
return
}
proxies, _ := db.Client().Proxy.Query().Where(proxy.ClientIDEQ(c.ID)).All(ctx)
content := fmt.Sprintf(`# frpc config for %s
# Auto-generated by frp admin dashboard
serverAddr = %q
serverPort = %d
auth.token = %q
`, c.Name, cfg.BindAddr, cfg.BindPort, cfg.Auth.Token)
for _, p := range proxies {
content += fmt.Sprintf(`
[[proxies]]
name = %q
type = %q
localIP = %q
localPort = %d
remotePort = %d
`, p.Name, p.ProxyType, p.LocalIP, p.LocalPort, p.RemotePort)
if p.CustomDomains != "" {
content += fmt.Sprintf("customDomains = [%q]\n", p.CustomDomains)
}
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=frpc-%s.toml", c.Name))
w.Write([]byte(content))
}
func (h *Handler) handleClientProvisionScript(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id, err := strconv.Atoi(vars["id"])
if err != nil {
http.Error(w, "invalid id", http.StatusBadRequest)
return
}
ctx := context.Background()
c, err := db.Client().FrpcClient.Get(ctx, id)
if err != nil {
http.Error(w, "client not found", http.StatusNotFound)
return
}
key := c.Key
serverHost := r.Host
script := fmt.Sprintf(`#!/bin/bash
# frp auto-provisioning for: %s
set -e
CONFIG_URL="http://%s/admin/api/frpc/proxy-config/%s"
echo "==> Downloading frpc with server-config support..."
curl -sL https://github.com/fatedier/frp/releases/latest/download/frp_linux_amd64.tar.gz | tar xz
FRPC_DIR=$(ls -d frp_* 2>/dev/null | head -1)
echo "==> Starting frpc for %s..."
"$FRPC_DIR/frpc" --server-config "$CONFIG_URL"
`, c.Name, serverHost, key, c.Name)
w.Header().Set("Content-Type", "text/x-shellscript; charset=utf-8")
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=provision-%s.sh", c.Name))
w.Write([]byte(script))
}
func (h *Handler) handleClientGenerateToken(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id, err := strconv.Atoi(vars["id"])
if err != nil {
http.Error(w, "invalid id", http.StatusBadRequest)
return
}
ctx := context.Background()
c, err := db.Client().FrpcClient.Get(ctx, id)
if err != nil {
http.Error(w, "client not found", http.StatusNotFound)
return
}
tokenBytes := make([]byte, 32)
rand.Read(tokenBytes)
token := hex.EncodeToString(tokenBytes)
expires := time.Now().Add(30 * time.Minute).Unix()
meta := map[string]any{}
if c.Metadata != "" && c.Metadata != "{}" {
json.Unmarshal([]byte(c.Metadata), &meta)
}
meta["one_time_token"] = token
meta["one_time_token_expires"] = expires
metaBytes, _ := json.Marshal(meta)
db.Client().FrpcClient.Update().SetMetadata(string(metaBytes)).Where(frpcclient.ID(id)).Exec(ctx)
if r.Header.Get("HX-Request") == "true" {
html := fmt.Sprintf(`<div style="padding:8px;border:1px solid var(--border);font-size:12px">
<p><strong>One-Time Token:</strong></p>
<pre class="text-mono" style="margin:4px 0;padding:8px;background:var(--bg2);border:1px solid var(--border);word-break:break-all">%s</pre>
<p style="color:var(--fg2);margin-top:4px">Valid for 30 minutes. Use with: <code class="text-mono">frpc auth token %s --server http://%s</code></p>
</div>`, token, token, r.Host)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write([]byte(html))
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]any{
"token": token,
"expires": expires,
})
}
func (h *Handler) handleClientAuth(w http.ResponseWriter, r *http.Request) {
var req struct {
Token string `json:"token"`
Username string `json:"username"`
Password string `json:"password"`
ClientName string `json:"client_name"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid request body", http.StatusBadRequest)
return
}
ctx := context.Background()
// One-time token auth
if req.Token != "" {
all, err := db.Client().FrpcClient.Query().All(ctx)
if err != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
for _, c := range all {
meta := map[string]any{}
if c.Metadata != "" && c.Metadata != "{}" {
json.Unmarshal([]byte(c.Metadata), &meta)
}
storedToken, _ := meta["one_time_token"].(string)
if storedToken == "" || storedToken != req.Token {
continue
}
expires, _ := meta["one_time_token_expires"].(float64)
if time.Now().Unix() > int64(expires) {
http.Error(w, "token expired", http.StatusUnauthorized)
return
}
// Clear the one-time token (one-time use)
delete(meta, "one_time_token")
delete(meta, "one_time_token_expires")
metaBytes, _ := json.Marshal(meta)
db.Client().FrpcClient.Update().SetMetadata(string(metaBytes)).Where(frpcclient.ID(c.ID)).Exec(ctx)
h.writeClientConfig(w, ctx, c.ID)
return
}
http.Error(w, "invalid token", http.StatusUnauthorized)
return
}
// Login auth
if req.Username != "" && req.Password != "" {
u, err := db.Client().User.Query().Where(user.UsernameEQ(req.Username)).Only(ctx)
if err != nil {
http.Error(w, "invalid credentials", http.StatusUnauthorized)
return
}
if err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(req.Password)); err != nil {
http.Error(w, "invalid credentials", http.StatusUnauthorized)
return
}
if req.ClientName == "" {
// Return list of client names
all, _ := db.Client().FrpcClient.Query().Select(frpcclient.FieldName, frpcclient.FieldID).All(ctx)
names := make([]map[string]any, 0, len(all))
for _, c := range all {
names = append(names, map[string]any{"id": c.ID, "name": c.Name})
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]any{"clients": names, "requires_client_name": true})
return
}
client, err := db.Client().FrpcClient.Query().Where(frpcclient.NameEQ(req.ClientName)).Only(ctx)
if err != nil {
http.Error(w, "client not found", http.StatusNotFound)
return
}
h.writeClientConfig(w, ctx, client.ID)
return
}
http.Error(w, "provide token or username/password", http.StatusBadRequest)
}
func (h *Handler) writeClientConfig(w http.ResponseWriter, ctx context.Context, clientID int) {
c, err := db.Client().FrpcClient.Get(ctx, clientID)
if err != nil {
http.Error(w, "client not found", http.StatusNotFound)
return
}
cfg, err := db.LoadServerConfig()
if err != nil {
http.Error(w, "server config error", http.StatusInternalServerError)
return
}
proxies, _ := db.Client().Proxy.Query().Where(proxy.ClientIDEQ(c.ID)).All(ctx)
content := fmt.Sprintf(`# frpc config for %s
# Auto-generated by frp admin server
serverAddr = %q
serverPort = %d
auth.token = %q
`, c.Name, cfg.BindAddr, cfg.BindPort, cfg.Auth.Token)
for _, p := range proxies {
content += fmt.Sprintf(`
[[proxies]]
name = %q
type = %q
localIP = %q
localPort = %d
remotePort = %d
`, p.Name, p.ProxyType, p.LocalIP, p.LocalPort, p.RemotePort)
if p.CustomDomains != "" {
content += fmt.Sprintf("customDomains = [%q]\n", p.CustomDomains)
}
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Write([]byte(content))
}
func (h *Handler) handleFrpcProxyConfig(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
key := vars["key"]
if key == "" {
http.Error(w, "missing key", http.StatusBadRequest)
return
}
ctx := context.Background()
client, err := db.Client().FrpcClient.Query().Where(frpcclient.KeyEQ(key)).Only(ctx)
if err != nil {
http.Error(w, "invalid key", http.StatusNotFound)
return
}
cfg, err := db.LoadServerConfig()
if err != nil {
http.Error(w, "server config error", http.StatusInternalServerError)
return
}
proxies, _ := db.Client().Proxy.Query().Where(proxy.ClientIDEQ(client.ID)).All(ctx)
content := fmt.Sprintf(`# frpc config for %s
# Auto-generated by frp admin server
serverAddr = %q
serverPort = %d
auth.token = %q
`, client.Name, cfg.BindAddr, cfg.BindPort, cfg.Auth.Token)
for _, p := range proxies {
content += fmt.Sprintf(`
[[proxies]]
name = %q
type = %q
localIP = %q
localPort = %d
remotePort = %d
`, p.Name, p.ProxyType, p.LocalIP, p.LocalPort, p.RemotePort)
if p.CustomDomains != "" {
content += fmt.Sprintf("customDomains = [%q]\n", p.CustomDomains)
}
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Write([]byte(content))
}
func (h *Handler) handleNewProxy(w http.ResponseWriter, r *http.Request) {
ctx := context.Background()
allClients, err := db.Client().FrpcClient.Query().Order(ent.Asc(frpcclient.FieldName)).All(ctx)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
selectedClient, _ := strconv.Atoi(r.URL.Query().Get("client_id"))
renderPage(w, r, "New Proxy", "proxy_new.html", map[string]any{
"Clients": allClients,
"SelectedClient": selectedClient,
})
}
func (h *Handler) handleNewProxyPost(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
ctx := context.Background()
name := r.FormValue("name")
proxyType := r.FormValue("proxy_type")
localIP := r.FormValue("local_ip")
localPortS := r.FormValue("local_port")
remotePortS := r.FormValue("remote_port")
clientIDS := r.FormValue("client_id")
customDomains := r.FormValue("custom_domains")
if name == "" || localIP == "" || localPortS == "" || clientIDS == "" {
allClients, _ := db.Client().FrpcClient.Query().Order(ent.Asc(frpcclient.FieldName)).All(ctx)
renderPage(w, r, "New Proxy", "proxy_new.html", map[string]any{
"Clients": allClients,
"Error": "Name, local IP, local port, and client are required.",
})
return
}
localPort := atoi(localPortS, 0)
remotePort := atoi(remotePortS, 0)
clientID := atoi(clientIDS, 0)
if localPort == 0 {
allClients, _ := db.Client().FrpcClient.Query().Order(ent.Asc(frpcclient.FieldName)).All(ctx)
renderPage(w, r, "New Proxy", "proxy_new.html", map[string]any{
"Clients": allClients,
"Error": "Invalid local port.",
})
return
}
if proxyType == "" {
proxyType = "tcp"
}
_, err := db.Client().Proxy.Create().
SetName(name).
SetProxyType(proxyType).
SetLocalIP(localIP).
SetLocalPort(localPort).
SetRemotePort(remotePort).
SetClientID(clientID).
SetCustomDomains(customDomains).
SetStatus("inactive").
SetMetadata("{}").
Save(ctx)
if err != nil {
allClients, _ := db.Client().FrpcClient.Query().Order(ent.Asc(frpcclient.FieldName)).All(ctx)
renderPage(w, r, "New Proxy", "proxy_new.html", map[string]any{
"Clients": allClients,
"Error": "Failed to create proxy: " + err.Error(),
})
return
}
http.Redirect(w, r, "/admin/proxies", http.StatusFound)
}
func (h *Handler) handleSettings(w http.ResponseWriter, r *http.Request) {
cfg, err := db.LoadServerConfig()
if err != nil {
http.Error(w, "failed to load config", http.StatusInternalServerError)
return
}
renderPage(w, r, "Settings", "settings.html", map[string]any{
"Config": cfg,
"Saved": r.URL.Query().Get("saved") == "1",
})
}
func (h *Handler) handleSettingsSave(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
cfg, err := db.LoadServerConfig()
if err != nil {
http.Error(w, "failed to load config", http.StatusInternalServerError)
return
}
if v := r.FormValue("bind_addr"); v != "" {
cfg.BindAddr = v
}
if v := r.FormValue("bind_port"); v != "" {
cfg.BindPort = atoi(v, 7000)
}
if v := r.FormValue("dashboard_addr"); v != "" {
cfg.WebServer.Addr = v
}
if v := r.FormValue("dashboard_port"); v != "" {
cfg.WebServer.Port = atoi(v, 7500)
}
if v := r.FormValue("dashboard_user"); v != "" {
cfg.WebServer.User = v
}
if v := r.FormValue("dashboard_password"); v != "" {
cfg.WebServer.Password = v
}
if v := r.FormValue("auth_token"); v != "" {
cfg.Auth.Token = v
}
if v := r.FormValue("auth_method"); v != "" {
cfg.Auth.Method = v1.AuthMethod(v)
}
if v := r.FormValue("log_to"); v != "" {
cfg.Log.To = v
}
if v := r.FormValue("log_level"); v != "" {
cfg.Log.Level = v
}
if v := r.FormValue("log_max_days"); v != "" {
cfg.Log.MaxDays = int64(atoi(v, 3))
}
if v := r.FormValue("kcp_bind_port"); v != "" {
cfg.KCPBindPort = atoi(v, 0)
}
if v := r.FormValue("quic_bind_port"); v != "" {
cfg.QUICBindPort = atoi(v, 0)
}
if v := r.FormValue("vhost_http_port"); v != "" {
cfg.VhostHTTPPort = atoi(v, 0)
}
if v := r.FormValue("vhost_https_port"); v != "" {
cfg.VhostHTTPSPort = atoi(v, 0)
}
if v := r.FormValue("subdomain_host"); v != "" {
cfg.SubDomainHost = v
}
if r.FormValue("tcp_mux") == "true" {
val := true
cfg.Transport.TCPMux = &val
} else if r.Form.Has("tcp_mux") {
val := false
cfg.Transport.TCPMux = &val
}
if v := r.FormValue("max_pool_count"); v != "" {
cfg.Transport.MaxPoolCount = int64(atoi(v, 0))
}
if v := r.FormValue("proxy_bind_addr"); v != "" {
cfg.ProxyBindAddr = v
}
cfg.EnablePrometheus = r.FormValue("enable_prometheus") == "true"
if err := db.SaveServerConfig(cfg); err != nil {
http.Error(w, "failed to save config: "+err.Error(), http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/admin/settings?saved=1", http.StatusFound)
}
func atoi(s string, defaultVal int) int {
v := 0
fmt.Sscanf(s, "%d", &v)
if v == 0 {
return defaultVal
}
return v
}
func renderPage(w http.ResponseWriter, r *http.Request, title, tmplName string, data map[string]any) {
content := renderToString(tmplName, data)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if r.Header.Get("HX-Request") == "true" {
w.Write([]byte(content))
} else {
if err := renderLayout(w, title, template.HTML(content)); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
}