package sqlite import ( "database/sql" _ "embed" "errors" "fmt" "log" "os" "path/filepath" "sort" "strings" "time" _ "modernc.org/sqlite" ) //go:embed sql/schema.sql var embeddedSchema string //go:embed sql/data.sql var embeddedSeedData string type Repository struct { db *sql.DB } func (r *Repository) DB() *sql.DB { if r == nil { return nil } return r.db } type User struct { ID int64 User string Pwd string RoleID int ExpTime int64 Flow int64 InFlow int64 OutFlow int64 FlowResetTime int64 Num int CreatedTime int64 UpdatedTime sql.NullInt64 Status int } type ViteConfig struct { ID int64 `json:"id"` Name string `json:"name"` Value string `json:"value"` Time int64 `json:"time"` } type UserTunnelDetail struct { ID int64 UserID int64 TunnelID int64 TunnelName string TunnelFlow int Flow int64 InFlow int64 OutFlow int64 Num int FlowResetTime int64 ExpTime int64 SpeedID sql.NullInt64 SpeedLimit sql.NullString Speed sql.NullInt64 } type UserForwardDetail struct { ID int64 Name string TunnelID int64 TunnelName string InIP string InPort sql.NullInt64 RemoteAddr string InFlow int64 OutFlow int64 Status int CreatedAt int64 } type StatisticsFlow struct { ID int64 `json:"id"` UserID int64 `json:"userId"` Flow int64 `json:"flow"` TotalFlow int64 `json:"totalFlow"` Time string `json:"time"` } type Node struct { ID int64 Secret string Version sql.NullString HTTP int TLS int Socks int Status int } func Open(path string) (*Repository, error) { if err := ensureParentDir(path); err != nil { return nil, err } db, err := sql.Open("sqlite", path) if err != nil { return nil, err } if err := db.Ping(); err != nil { _ = db.Close() return nil, err } if err := bootstrapSchema(db); err != nil { _ = db.Close() return nil, err } return &Repository{db: db}, nil } func (r *Repository) Close() error { if r == nil || r.db == nil { return nil } return r.db.Close() } func (r *Repository) GetUserByUsername(username string) (*User, error) { if r == nil || r.db == nil { return nil, errors.New("repository not initialized") } row := r.db.QueryRow(` SELECT id, user, pwd, role_id, exp_time, flow, in_flow, out_flow, flow_reset_time, num, created_time, updated_time, status FROM user WHERE user = ? LIMIT 1 `, username) user := &User{} if err := row.Scan( &user.ID, &user.User, &user.Pwd, &user.RoleID, &user.ExpTime, &user.Flow, &user.InFlow, &user.OutFlow, &user.FlowResetTime, &user.Num, &user.CreatedTime, &user.UpdatedTime, &user.Status, ); err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, nil } return nil, err } return user, nil } func (r *Repository) GetConfigByName(name string) (*ViteConfig, error) { if r == nil || r.db == nil { return nil, errors.New("repository not initialized") } row := r.db.QueryRow(`SELECT id, name, value, time FROM vite_config WHERE name = ? LIMIT 1`, name) cfg := &ViteConfig{} if err := row.Scan(&cfg.ID, &cfg.Name, &cfg.Value, &cfg.Time); err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, nil } return nil, err } return cfg, nil } func (r *Repository) ListConfigs() (map[string]string, error) { if r == nil || r.db == nil { return nil, errors.New("repository not initialized") } rows, err := r.db.Query(`SELECT name, value FROM vite_config`) if err != nil { return nil, err } defer rows.Close() result := make(map[string]string) for rows.Next() { var name, value string if err := rows.Scan(&name, &value); err != nil { return nil, err } result[name] = value } if err := rows.Err(); err != nil { return nil, err } return result, nil } func (r *Repository) UpsertConfig(name, value string, now int64) error { if r == nil || r.db == nil { return errors.New("repository not initialized") } _, err := r.db.Exec(` INSERT INTO vite_config(name, value, time) VALUES(?, ?, ?) ON CONFLICT(name) DO UPDATE SET value=excluded.value, time=excluded.time `, name, value, now) return err } func (r *Repository) GetUserByID(id int64) (*User, error) { if r == nil || r.db == nil { return nil, errors.New("repository not initialized") } row := r.db.QueryRow(` SELECT id, user, pwd, role_id, exp_time, flow, in_flow, out_flow, flow_reset_time, num, created_time, updated_time, status FROM user WHERE id = ? LIMIT 1 `, id) user := &User{} if err := row.Scan( &user.ID, &user.User, &user.Pwd, &user.RoleID, &user.ExpTime, &user.Flow, &user.InFlow, &user.OutFlow, &user.FlowResetTime, &user.Num, &user.CreatedTime, &user.UpdatedTime, &user.Status, ); err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, nil } return nil, err } return user, nil } func (r *Repository) UsernameExistsExceptID(username string, exceptID int64) (bool, error) { if r == nil || r.db == nil { return false, errors.New("repository not initialized") } row := r.db.QueryRow(`SELECT COUNT(1) FROM user WHERE user = ? AND id != ?`, username, exceptID) var count int if err := row.Scan(&count); err != nil { return false, err } return count > 0, nil } func (r *Repository) UpdateUserNameAndPassword(userID int64, username, passwordMD5 string, now int64) error { if r == nil || r.db == nil { return errors.New("repository not initialized") } _, err := r.db.Exec(`UPDATE user SET user = ?, pwd = ?, updated_time = ? WHERE id = ?`, username, passwordMD5, now, userID) return err } func (r *Repository) GetUserPackageTunnels(userID int64) ([]UserTunnelDetail, error) { if r == nil || r.db == nil { return nil, errors.New("repository not initialized") } rows, err := r.db.Query(` SELECT ut.id, ut.user_id, ut.tunnel_id, t.name, t.flow, ut.flow, ut.in_flow, ut.out_flow, ut.num, ut.flow_reset_time, ut.exp_time, ut.speed_id, sl.name, sl.speed FROM user_tunnel ut LEFT JOIN tunnel t ON t.id = ut.tunnel_id LEFT JOIN speed_limit sl ON sl.id = ut.speed_id WHERE ut.user_id = ? ORDER BY ut.id ASC `, userID) if err != nil { return nil, err } defer rows.Close() items := make([]UserTunnelDetail, 0) for rows.Next() { var item UserTunnelDetail if err := rows.Scan( &item.ID, &item.UserID, &item.TunnelID, &item.TunnelName, &item.TunnelFlow, &item.Flow, &item.InFlow, &item.OutFlow, &item.Num, &item.FlowResetTime, &item.ExpTime, &item.SpeedID, &item.SpeedLimit, &item.Speed, ); err != nil { return nil, err } items = append(items, item) } if err := rows.Err(); err != nil { return nil, err } return items, nil } func (r *Repository) GetUserPackageForwards(userID int64) ([]UserForwardDetail, error) { if r == nil || r.db == nil { return nil, errors.New("repository not initialized") } rows, err := r.db.Query(` SELECT f.id, f.name, f.tunnel_id, t.name, f.remote_addr, f.in_flow, f.out_flow, f.status, f.created_time FROM forward f LEFT JOIN tunnel t ON t.id = f.tunnel_id WHERE f.user_id = ? ORDER BY f.id ASC `, userID) if err != nil { return nil, err } defer rows.Close() items := make([]UserForwardDetail, 0) for rows.Next() { var item UserForwardDetail if err := rows.Scan( &item.ID, &item.Name, &item.TunnelID, &item.TunnelName, &item.RemoteAddr, &item.InFlow, &item.OutFlow, &item.Status, &item.CreatedAt, ); err != nil { return nil, err } inIP, inPort, err := resolveForwardIngress(r.db, item.ID, item.TunnelID) if err != nil { return nil, err } item.InIP = inIP item.InPort = inPort items = append(items, item) } if err := rows.Err(); err != nil { return nil, err } return items, nil } func (r *Repository) GetStatisticsFlows(userID int64, limit int) ([]StatisticsFlow, error) { if r == nil || r.db == nil { return nil, errors.New("repository not initialized") } rows, err := r.db.Query(` SELECT id, user_id, flow, total_flow, time FROM statistics_flow WHERE user_id = ? ORDER BY id DESC LIMIT ? `, userID, limit) if err != nil { return nil, err } defer rows.Close() items := make([]StatisticsFlow, 0) for rows.Next() { var item StatisticsFlow if err := rows.Scan(&item.ID, &item.UserID, &item.Flow, &item.TotalFlow, &item.Time); err != nil { return nil, err } items = append(items, item) } if err := rows.Err(); err != nil { return nil, err } return items, nil } func (r *Repository) NodeExistsBySecret(secret string) (bool, error) { if r == nil || r.db == nil { return false, errors.New("repository not initialized") } row := r.db.QueryRow(`SELECT COUNT(1) FROM node WHERE secret = ?`, secret) var count int if err := row.Scan(&count); err != nil { return false, err } return count > 0, nil } func (r *Repository) GetNodeBySecret(secret string) (*Node, error) { if r == nil || r.db == nil { return nil, errors.New("repository not initialized") } row := r.db.QueryRow(`SELECT id, secret, version, http, tls, socks, status FROM node WHERE secret = ? LIMIT 1`, secret) var n Node if err := row.Scan(&n.ID, &n.Secret, &n.Version, &n.HTTP, &n.TLS, &n.Socks, &n.Status); err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, nil } return nil, err } return &n, nil } func (r *Repository) UpdateNodeOnline(nodeID int64, status int, version string, httpVal, tlsVal, socksVal int) error { if r == nil || r.db == nil { return errors.New("repository not initialized") } _, err := r.db.Exec(`UPDATE node SET status = ?, version = ?, http = ?, tls = ?, socks = ?, updated_time = ? WHERE id = ?`, status, version, httpVal, tlsVal, socksVal, unixMilliNow(), nodeID) return err } func (r *Repository) UpdateNodeStatus(nodeID int64, status int) error { if r == nil || r.db == nil { return errors.New("repository not initialized") } _, err := r.db.Exec(`UPDATE node SET status = ?, updated_time = ? WHERE id = ?`, status, unixMilliNow(), nodeID) return err } func (r *Repository) AddFlow(forwardID, userID int64, userTunnelID int64, inFlow, outFlow int64) error { if r == nil || r.db == nil { return errors.New("repository not initialized") } tx, err := r.db.Begin() if err != nil { return err } defer func() { if err != nil { _ = tx.Rollback() } }() if _, err = tx.Exec(`UPDATE forward SET in_flow = in_flow + ?, out_flow = out_flow + ? WHERE id = ?`, inFlow, outFlow, forwardID); err != nil { return err } if _, err = tx.Exec(`UPDATE user SET in_flow = in_flow + ?, out_flow = out_flow + ? WHERE id = ?`, inFlow, outFlow, userID); err != nil { return err } if userTunnelID > 0 { if _, err = tx.Exec(`UPDATE user_tunnel SET in_flow = in_flow + ?, out_flow = out_flow + ? WHERE id = ?`, inFlow, outFlow, userTunnelID); err != nil { return err } } err = tx.Commit() return err } func (r *Repository) ListNodes() ([]map[string]interface{}, error) { if r == nil || r.db == nil { return nil, errors.New("repository not initialized") } rows, err := r.db.Query(` SELECT id, inx, name, server_ip, server_ip_v4, server_ip_v6, port, tcp_listen_addr, udp_listen_addr, version, http, tls, socks, status FROM node ORDER BY inx ASC, id ASC `) if err != nil { return nil, err } defer rows.Close() items := make([]map[string]interface{}, 0) for rows.Next() { var id, inx int64 var name, serverIP, port string var serverIPV4, serverIPV6, tcpListen, udpListen, version sql.NullString var httpVal, tlsVal, socksVal, status int if err := rows.Scan(&id, &inx, &name, &serverIP, &serverIPV4, &serverIPV6, &port, &tcpListen, &udpListen, &version, &httpVal, &tlsVal, &socksVal, &status); err != nil { return nil, err } items = append(items, map[string]interface{}{ "id": id, "inx": inx, "name": name, "ip": serverIP, "serverIp": serverIP, "serverIpV4": nullableString(serverIPV4), "serverIpV6": nullableString(serverIPV6), "port": port, "tcpListenAddr": nullableString(tcpListen), "udpListenAddr": nullableString(udpListen), "version": nullableString(version), "http": httpVal, "tls": tlsVal, "socks": socksVal, "status": status, }) } if err := rows.Err(); err != nil { return nil, err } return items, nil } func (r *Repository) ListUsers() ([]map[string]interface{}, error) { if r == nil || r.db == nil { return nil, errors.New("repository not initialized") } rows, err := r.db.Query(` SELECT id, user, role_id, exp_time, flow, in_flow, out_flow, flow_reset_time, num, created_time, updated_time, status FROM user WHERE role_id != 0 ORDER BY id ASC `) if err != nil { return nil, err } defer rows.Close() items := make([]map[string]interface{}, 0) for rows.Next() { var id int64 var user string var roleID int var expTime, flow, inFlow, outFlow, flowResetTime, createdTime int64 var num, status int var updatedTime sql.NullInt64 if err := rows.Scan(&id, &user, &roleID, &expTime, &flow, &inFlow, &outFlow, &flowResetTime, &num, &createdTime, &updatedTime, &status); err != nil { return nil, err } items = append(items, map[string]interface{}{ "id": id, "user": user, "name": user, "roleId": roleID, "status": status, "flow": flow, "num": num, "expTime": expTime, "flowResetTime": flowResetTime, "createdTime": createdTime, "updatedTime": nullableInt64(updatedTime), "inFlow": inFlow, "outFlow": outFlow, }) } if err := rows.Err(); err != nil { return nil, err } return items, nil } func (r *Repository) ListSpeedLimits() ([]map[string]interface{}, error) { if r == nil || r.db == nil { return nil, errors.New("repository not initialized") } rows, err := r.db.Query(` SELECT id, name, speed, tunnel_id, tunnel_name, status, created_time, updated_time FROM speed_limit ORDER BY id ASC `) if err != nil { return nil, err } defer rows.Close() items := make([]map[string]interface{}, 0) for rows.Next() { var id, tunnelID, createdTime int64 var name, tunnelName string var speed, status int var updatedTime sql.NullInt64 if err := rows.Scan(&id, &name, &speed, &tunnelID, &tunnelName, &status, &createdTime, &updatedTime); err != nil { return nil, err } items = append(items, map[string]interface{}{ "id": id, "name": name, "speed": speed, "tunnelId": tunnelID, "tunnelName": tunnelName, "status": status, "createdTime": createdTime, "updatedTime": nullableInt64(updatedTime), }) } if err := rows.Err(); err != nil { return nil, err } return items, nil } func (r *Repository) ListForwards() ([]map[string]interface{}, error) { if r == nil || r.db == nil { return nil, errors.New("repository not initialized") } rows, err := r.db.Query(` SELECT f.id, f.user_id, f.user_name, f.name, f.tunnel_id, t.name, f.remote_addr, f.strategy, f.in_flow, f.out_flow, f.created_time, f.status, f.inx FROM forward f LEFT JOIN tunnel t ON t.id = f.tunnel_id ORDER BY f.inx ASC, f.id ASC `) if err != nil { return nil, err } defer rows.Close() items := make([]map[string]interface{}, 0) for rows.Next() { var id, userID, tunnelID, inFlow, outFlow, createdTime, inx int64 var userName, name, tunnelName, remoteAddr, strategy string var status int if err := rows.Scan(&id, &userID, &userName, &name, &tunnelID, &tunnelName, &remoteAddr, &strategy, &inFlow, &outFlow, &createdTime, &status, &inx); err != nil { return nil, err } inIP, inPort, err := resolveForwardIngress(r.db, id, tunnelID) if err != nil { return nil, err } items = append(items, map[string]interface{}{ "id": id, "userId": userID, "userName": userName, "name": name, "tunnelId": tunnelID, "tunnelName": tunnelName, "inIp": nullableForwardIngress(inIP), "inPort": nullableInt64(inPort), "remoteAddr": remoteAddr, "strategy": strategy, "inFlow": inFlow, "outFlow": outFlow, "createdTime": createdTime, "status": status, "inx": inx, }) } if err := rows.Err(); err != nil { return nil, err } return items, nil } func (r *Repository) ListUserAccessibleTunnels(userID int64) ([]map[string]interface{}, error) { if r == nil || r.db == nil { return nil, errors.New("repository not initialized") } rows, err := r.db.Query(` SELECT t.id, t.name FROM user_tunnel ut JOIN tunnel t ON t.id = ut.tunnel_id WHERE ut.user_id = ? AND ut.status = 1 ORDER BY t.inx ASC, t.id ASC `, userID) if err != nil { return nil, err } defer rows.Close() items := make([]map[string]interface{}, 0) for rows.Next() { var id int64 var name string if err := rows.Scan(&id, &name); err != nil { return nil, err } items = append(items, map[string]interface{}{"id": id, "name": name}) } if err := rows.Err(); err != nil { return nil, err } return items, nil } func (r *Repository) ListTunnels() ([]map[string]interface{}, error) { if r == nil || r.db == nil { return nil, errors.New("repository not initialized") } rows, err := r.db.Query(` SELECT id, inx, name, type, flow, traffic_ratio, status, created_time, in_ip FROM tunnel ORDER BY inx ASC, id ASC `) if err != nil { return nil, err } defer rows.Close() tunnelMap := make(map[int64]map[string]interface{}) orderedIDs := make([]int64, 0) for rows.Next() { var id, inx, flow, createdTime int64 var name string var typ, status int var trafficRatio float64 var inIP sql.NullString if err := rows.Scan(&id, &inx, &name, &typ, &flow, &trafficRatio, &status, &createdTime, &inIP); err != nil { return nil, err } tunnelMap[id] = map[string]interface{}{ "id": id, "inx": inx, "name": name, "type": typ, "flow": flow, "trafficRatio": trafficRatio, "status": status, "createdTime": createdTime, "inIp": nullableString(inIP), "inNodeId": make([]map[string]interface{}, 0), "outNodeId": make([]map[string]interface{}, 0), "chainNodes": make([][]map[string]interface{}, 0), } orderedIDs = append(orderedIDs, id) } if err := rows.Err(); err != nil { return nil, err } nodeIPMap := map[int64]string{} nRows, err := r.db.Query(`SELECT id, server_ip FROM node`) if err == nil { for nRows.Next() { var id int64 var ip string if scanErr := nRows.Scan(&id, &ip); scanErr == nil { nodeIPMap[id] = ip } } _ = nRows.Close() } chainRows, err := r.db.Query(` SELECT tunnel_id, chain_type, node_id, protocol, strategy, COALESCE(inx, 0) FROM chain_tunnel ORDER BY tunnel_id ASC, chain_type ASC, inx ASC, id ASC `) if err != nil { return nil, err } defer chainRows.Close() chainBucket := map[int64]map[int][]map[string]interface{}{} inNodeIPs := map[int64][]string{} for chainRows.Next() { var tunnelID, nodeID, inx int64 var chainType int var protocol, strategy sql.NullString if err := chainRows.Scan(&tunnelID, &chainType, &nodeID, &protocol, &strategy, &inx); err != nil { return nil, err } t, ok := tunnelMap[tunnelID] if !ok { continue } nodeObj := map[string]interface{}{ "nodeId": nodeID, "chainType": chainType, "inx": inx, } if protocol.Valid { nodeObj["protocol"] = protocol.String } if strategy.Valid { nodeObj["strategy"] = strategy.String } switch chainType { case 1: t["inNodeId"] = append(t["inNodeId"].([]map[string]interface{}), nodeObj) if ip, ok := nodeIPMap[nodeID]; ok && ip != "" { inNodeIPs[tunnelID] = append(inNodeIPs[tunnelID], ip) } case 2: if _, ok := chainBucket[tunnelID]; !ok { chainBucket[tunnelID] = map[int][]map[string]interface{}{} } chainBucket[tunnelID][int(inx)] = append(chainBucket[tunnelID][int(inx)], nodeObj) case 3: t["outNodeId"] = append(t["outNodeId"].([]map[string]interface{}), nodeObj) } } if err := chainRows.Err(); err != nil { return nil, err } for tunnelID, groups := range chainBucket { t := tunnelMap[tunnelID] if t == nil { continue } keys := make([]int, 0, len(groups)) for k := range groups { keys = append(keys, k) } sort.Ints(keys) ordered := make([][]map[string]interface{}, 0, len(keys)) for _, k := range keys { ordered = append(ordered, groups[k]) } t["chainNodes"] = ordered if s, ok := t["inIp"].(string); !ok || strings.TrimSpace(s) == "" { if ips := inNodeIPs[tunnelID]; len(ips) > 0 { t["inIp"] = strings.Join(ips, ",") } } } result := make([]map[string]interface{}, 0, len(orderedIDs)) for _, id := range orderedIDs { if t, ok := tunnelMap[id]; ok { result = append(result, t) } } return result, nil } func (r *Repository) ListTunnelGroups() ([]map[string]interface{}, error) { if r == nil || r.db == nil { return nil, errors.New("repository not initialized") } rows, err := r.db.Query(`SELECT id, name, status, created_time FROM tunnel_group ORDER BY id ASC`) if err != nil { return nil, err } defer rows.Close() result := make([]map[string]interface{}, 0) for rows.Next() { var id, createdTime int64 var name string var status int if err := rows.Scan(&id, &name, &status, &createdTime); err != nil { return nil, err } ids, names, err := r.listTunnelGroupMembers(id) if err != nil { return nil, err } result = append(result, map[string]interface{}{ "id": id, "name": name, "status": status, "tunnelIds": ids, "tunnelNames": names, "createdTime": createdTime, }) } if err := rows.Err(); err != nil { return nil, err } return result, nil } func (r *Repository) ListUserGroups() ([]map[string]interface{}, error) { if r == nil || r.db == nil { return nil, errors.New("repository not initialized") } rows, err := r.db.Query(`SELECT id, name, status, created_time FROM user_group ORDER BY id ASC`) if err != nil { return nil, err } defer rows.Close() result := make([]map[string]interface{}, 0) for rows.Next() { var id, createdTime int64 var name string var status int if err := rows.Scan(&id, &name, &status, &createdTime); err != nil { return nil, err } ids, names, err := r.listUserGroupMembers(id) if err != nil { return nil, err } result = append(result, map[string]interface{}{ "id": id, "name": name, "status": status, "userIds": ids, "userNames": names, "createdTime": createdTime, }) } if err := rows.Err(); err != nil { return nil, err } return result, nil } func (r *Repository) ListGroupPermissions() ([]map[string]interface{}, error) { if r == nil || r.db == nil { return nil, errors.New("repository not initialized") } rows, err := r.db.Query(` SELECT gp.id, gp.user_group_id, ug.name, gp.tunnel_group_id, tg.name, gp.created_time FROM group_permission gp LEFT JOIN user_group ug ON ug.id = gp.user_group_id LEFT JOIN tunnel_group tg ON tg.id = gp.tunnel_group_id ORDER BY gp.id ASC `) if err != nil { return nil, err } defer rows.Close() result := make([]map[string]interface{}, 0) for rows.Next() { var id, userGroupID, tunnelGroupID, createdTime int64 var userGroupName, tunnelGroupName sql.NullString if err := rows.Scan(&id, &userGroupID, &userGroupName, &tunnelGroupID, &tunnelGroupName, &createdTime); err != nil { return nil, err } result = append(result, map[string]interface{}{ "id": id, "userGroupId": userGroupID, "userGroupName": nullableString(userGroupName), "tunnelGroupId": tunnelGroupID, "tunnelGroupName": nullableString(tunnelGroupName), "createdTime": createdTime, }) } if err := rows.Err(); err != nil { return nil, err } return result, nil } func (r *Repository) listTunnelGroupMembers(groupID int64) ([]int64, []string, error) { rows, err := r.db.Query(` SELECT t.id, t.name FROM tunnel_group_tunnel tgt JOIN tunnel t ON t.id = tgt.tunnel_id WHERE tgt.tunnel_group_id = ? ORDER BY t.id ASC `, groupID) if err != nil { return nil, nil, err } defer rows.Close() ids := make([]int64, 0) names := make([]string, 0) for rows.Next() { var id int64 var name string if err := rows.Scan(&id, &name); err != nil { return nil, nil, err } ids = append(ids, id) names = append(names, name) } if err := rows.Err(); err != nil { return nil, nil, err } return ids, names, nil } func (r *Repository) listUserGroupMembers(groupID int64) ([]int64, []string, error) { rows, err := r.db.Query(` SELECT u.id, u.user FROM user_group_user ugu JOIN user u ON u.id = ugu.user_id WHERE ugu.user_group_id = ? ORDER BY u.id ASC `, groupID) if err != nil { return nil, nil, err } defer rows.Close() ids := make([]int64, 0) names := make([]string, 0) for rows.Next() { var id int64 var name string if err := rows.Scan(&id, &name); err != nil { return nil, nil, err } ids = append(ids, id) names = append(names, name) } if err := rows.Err(); err != nil { return nil, nil, err } return ids, names, nil } func nullableString(v sql.NullString) interface{} { if v.Valid { return v.String } return nil } func nullableForwardIngress(v string) interface{} { v = strings.TrimSpace(v) if v == "" { return nil } return v } func resolveForwardIngress(db *sql.DB, forwardID int64, tunnelID int64) (string, sql.NullInt64, error) { var tunnelInIP sql.NullString if err := db.QueryRow(`SELECT in_ip FROM tunnel WHERE id = ? LIMIT 1`, tunnelID).Scan(&tunnelInIP); err != nil { if !errors.Is(err, sql.ErrNoRows) { return "", sql.NullInt64{}, err } } rows, err := db.Query(` SELECT fp.port, n.server_ip FROM forward_port fp LEFT JOIN node n ON n.id = fp.node_id WHERE fp.forward_id = ? ORDER BY fp.id ASC `, forwardID) if err != nil { return "", sql.NullInt64{}, err } defer rows.Close() ports := make([]int64, 0) nodePairs := make([]string, 0) seenPorts := make(map[int64]struct{}) seenPairs := make(map[string]struct{}) for rows.Next() { var port sql.NullInt64 var nodeIP sql.NullString if err := rows.Scan(&port, &nodeIP); err != nil { return "", sql.NullInt64{}, err } if !port.Valid { continue } if _, ok := seenPorts[port.Int64]; !ok { seenPorts[port.Int64] = struct{}{} ports = append(ports, port.Int64) } if nodeIP.Valid && strings.TrimSpace(nodeIP.String) != "" { pair := fmt.Sprintf("%s:%d", strings.TrimSpace(nodeIP.String), port.Int64) if _, ok := seenPairs[pair]; !ok { seenPairs[pair] = struct{}{} nodePairs = append(nodePairs, pair) } } } if err := rows.Err(); err != nil { return "", sql.NullInt64{}, err } if len(ports) == 0 { return "", sql.NullInt64{}, nil } inPort := sql.NullInt64{Int64: ports[0], Valid: true} entries := make([]string, 0) if tunnelInIP.Valid && strings.TrimSpace(tunnelInIP.String) != "" { tunnelIPs := strings.Split(tunnelInIP.String, ",") seen := make(map[string]struct{}) for _, ip := range tunnelIPs { ip = strings.TrimSpace(ip) if ip == "" { continue } if _, ok := seen[ip]; ok { continue } seen[ip] = struct{}{} for _, port := range ports { entries = append(entries, fmt.Sprintf("%s:%d", ip, port)) } } } else { entries = append(entries, nodePairs...) } return strings.Join(entries, ","), inPort, nil } func nullableInt64(v sql.NullInt64) interface{} { if v.Valid { return v.Int64 } return nil } func unixMilliNow() int64 { return time.Now().UnixMilli() } func ensureParentDir(dbPath string) error { if dbPath == "" { return fmt.Errorf("empty db path") } dir := filepath.Dir(dbPath) if dir == "" || dir == "." { return nil } return osMkdirAll(dir) } func bootstrapSchema(db *sql.DB) error { if db == nil { return errors.New("nil db") } var exists int err := db.QueryRow(`SELECT COUNT(1) FROM sqlite_master WHERE type='table' AND name='user'`).Scan(&exists) if err != nil { return fmt.Errorf("check schema: %w", err) } if exists > 0 { return nil } log.Printf("sqlite schema not found, bootstrapping embedded schema") if _, err := db.Exec(embeddedSchema); err != nil { return fmt.Errorf("apply schema.sql: %w", err) } if _, err := db.Exec(embeddedSeedData); err != nil { return fmt.Errorf("apply data.sql: %w", err) } return nil } var osMkdirAll = func(path string) error { return os.MkdirAll(path, 0o755) }