Add Go rewrite with Postgres 17 and Dokploy Docker Compose

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
orohi
2026-07-27 08:21:21 +03:00
co-authored by Cursor
parent bac78dd3fc
commit 99a27f00be
67 changed files with 17864 additions and 0 deletions
+361
View File
@@ -0,0 +1,361 @@
// Package share implements the guest share-link domain: link/creation repository,
// renewal codes, guest server catalog building and panel-response bundling for
// downloads (config files, vpn:// / vless:// links, zip archives).
package share
import (
"archive/zip"
"bytes"
"encoding/base64"
"encoding/json"
"path/filepath"
"regexp"
"sort"
"strings"
"time"
)
// VpnLinkMaxStoreLen caps the size of vpn_link kept in the DB response_json blob
// (protects against a bloated page / storage row).
const VpnLinkMaxStoreLen = 524288
var (
importURIRe = regexp.MustCompile(`(?i)^(vless|vmess|trojan)://`)
vpnWrapperRe = regexp.MustCompile(`(?i)^vpn://`)
zipNameBadRe = regexp.MustCompile(`[^a-zA-Z0-9._-]`)
)
// IsAWGFamily reports whether protocol is one of the AmneziaWG variants.
func IsAWGFamily(protocol string) bool {
switch protocol {
case "awg", "awg2", "awg_legacy":
return true
default:
return false
}
}
// IsVLESSFamily reports whether protocol is VLESS/Xray.
func IsVLESSFamily(protocol string) bool {
return protocol == "vless" || protocol == "xray"
}
// UsesImportSlot reports whether the protocol has a dedicated "import link" download
// slot (AWG uses vpn://, VLESS uses vless://); WireGuard only has the .conf slot.
func UsesImportSlot(protocol string) bool {
return IsAWGFamily(protocol) || IsVLESSFamily(protocol)
}
// ProtocolAPI maps a guest-facing protocol id to the Amnezia Web Panel API protocol
// name used when calling /api/servers/{id}/connections/add|remove (vless -> xray).
func ProtocolAPI(protocol string) string {
if protocol == "vless" {
return "xray"
}
return protocol
}
// ProtocolSortRank orders protocols for display: WireGuard first, then AWG family,
// then VLESS/Xray, then anything else.
func ProtocolSortRank(protocol string) int {
switch {
case protocol == "wireguard":
return 0
case IsAWGFamily(protocol):
return 1
case IsVLESSFamily(protocol):
return 2
default:
return 3
}
}
// IsAmneziaVPNWrapper reports whether link is the Amnezia "vpn://base64(...)" wrapper.
func IsAmneziaVPNWrapper(link string) bool {
link = strings.TrimSpace(link)
return link != "" && vpnWrapperRe.MatchString(link)
}
// decodeLooseBase64 tries strict standard base64 first, then falls back to the
// URL-safe alphabet (mirrors PHP's base64_decode(strict) + strtr('-_','+/') dance).
func decodeLooseBase64(payload string) (string, bool) {
if b, err := base64.StdEncoding.DecodeString(payload); err == nil && len(b) > 0 {
return string(b), true
}
if b, err := base64.RawStdEncoding.DecodeString(payload); err == nil && len(b) > 0 {
return string(b), true
}
swapped := strings.NewReplacer("-", "+", "_", "/").Replace(payload)
if b, err := base64.StdEncoding.DecodeString(swapped); err == nil && len(b) > 0 {
return string(b), true
}
if b, err := base64.RawStdEncoding.DecodeString(swapped); err == nil && len(b) > 0 {
return string(b), true
}
return "", false
}
func decodeVpnWrapperPayload(vpnLink string) (string, bool) {
payload := vpnWrapperRe.ReplaceAllString(strings.TrimSpace(vpnLink), "")
if payload == "" {
return "", false
}
return decodeLooseBase64(payload)
}
// VlessImportURI returns the native vless:// (or vmess:// / trojan://) link suitable
// for import into Happ / v2rayTun / Hiddify style clients. It never returns the
// Amnezia vpn:// wrapper — only a plain URI, or "" if none could be resolved.
func VlessImportURI(configText, vpnLink string) string {
configText = strings.TrimSpace(configText)
vpnLink = strings.TrimSpace(vpnLink)
if configText != "" && importURIRe.MatchString(configText) {
return configText
}
if vpnLink != "" && importURIRe.MatchString(vpnLink) {
return vpnLink
}
if IsAmneziaVPNWrapper(vpnLink) {
if decoded, ok := decodeVpnWrapperPayload(vpnLink); ok {
decoded = strings.TrimSpace(decoded)
if decoded != "" && importURIRe.MatchString(decoded) {
return decoded
}
}
}
return ""
}
// AWGImportURI returns the AmneziaWG vpn:// import link from the panel response.
func AWGImportURI(vpnLink, configText string) string {
vpnLink = strings.TrimSpace(vpnLink)
if vpnLink != "" {
return vpnLink
}
return strings.TrimSpace(configText)
}
// ImportDownloadFilename returns the file name used for the "import" download slot
// (vless family -> .txt, AWG/others -> .vpn).
func ImportDownloadFilename(protocol, connectionName string) string {
if IsVLESSFamily(protocol) {
return connectionName + ".txt"
}
return connectionName + ".vpn"
}
// ConfExtension guesses the file extension for the raw "config" text returned by the
// panel, based on protocol and content sniffing.
func ConfExtension(protocol, configText string) string {
hasIface := strings.Contains(configText, "[Interface]")
hasPeer := strings.Contains(configText, "[Peer]")
if protocol == "wireguard" && (hasIface || hasPeer) {
return "conf"
}
if IsAWGFamily(protocol) && hasIface {
return "conf"
}
if IsVLESSFamily(protocol) {
t := strings.TrimLeft(configText, " \t\r\n")
if t != "" && (t[0] == '{' || t[0] == '[') {
return "json"
}
}
return "txt"
}
func vlessURIFromPanelJSON(configText, vpnLink string) string {
// Identical rules to VlessImportURI; kept as a separate name to mirror the PHP
// split between share_panel_json.php and share_downloads.php.
return VlessImportURI(configText, vpnLink)
}
// SlimPanelJSON prepares the panel's raw connection-add JSON response for storage:
// - drops an oversized vpn_link (defensive cap),
// - for VLESS/Xray, replaces "config" with the resolved vless:// URI and strips
// the vpn:// wrapper entirely (only vless:// is ever persisted for VLESS).
//
// Any other protocol JSON is normalized (re-marshalled) but left otherwise intact.
// If jsonBody cannot be parsed as an object, it is returned unchanged.
func SlimPanelJSON(protocol, jsonBody string) string {
var dec map[string]any
if err := json.Unmarshal([]byte(jsonBody), &dec); err != nil {
return jsonBody
}
if v, ok := dec["vpn_link"]; ok {
if s, ok := v.(string); ok && len(s) > VpnLinkMaxStoreLen {
delete(dec, "vpn_link")
}
}
if IsVLESSFamily(protocol) {
cfg, _ := dec["config"].(string)
vpn, _ := dec["vpn_link"].(string)
uri := vlessURIFromPanelJSON(strings.TrimSpace(cfg), strings.TrimSpace(vpn))
if uri != "" {
dec["config"] = uri
}
delete(dec, "vpn_link")
}
out, err := json.Marshal(dec)
if err != nil {
return jsonBody
}
return string(out)
}
// FilePart is a single downloadable artifact (filename + raw body).
type FilePart struct {
Filename string
Body string
Mime string
}
// Bundle groups all downloadable artifacts derived from one panel connection-add
// response: the raw config file, the "import" link file (vpn:// / vless://), and
// (for WireGuard) a convenience .zip archive wrapping the .conf.
type Bundle struct {
Base string
Conf *FilePart
Vpn *FilePart
Zip *FilePart
CreationID int64
CreatedAt time.Time
Protocol string
ConnectionName string
ServerID int
}
// BundleFromResponseJSON extracts "config" / "vpn_link" from a (possibly already
// slimmed) panel JSON response and builds the full download bundle: which files to
// offer, under what names, and — for WireGuard — an in-memory .zip archive.
func BundleFromResponseJSON(protocol, connectionName, jsonBody string) Bundle {
var dec map[string]any
_ = json.Unmarshal([]byte(jsonBody), &dec)
cfgText, _ := dec["config"].(string)
vpnLink, _ := dec["vpn_link"].(string)
isAwg := IsAWGFamily(protocol)
isVless := IsVLESSFamily(protocol)
useImportSlot := UsesImportSlot(protocol)
importBody := ""
switch {
case isVless:
importBody = VlessImportURI(cfgText, vpnLink)
case isAwg:
importBody = AWGImportURI(vpnLink, cfgText)
}
bundle := Bundle{Base: connectionName, Protocol: protocol, ConnectionName: connectionName}
if cfgText != "" {
ext := ConfExtension(protocol, cfgText)
if isAwg && ext == "txt" {
ext = "conf"
}
skipConfDup := isVless && importBody != "" && strings.TrimSpace(cfgText) == importBody
if !skipConfDup {
bundle.Conf = &FilePart{Filename: connectionName + "." + ext, Body: cfgText}
}
}
if useImportSlot && importBody != "" {
bundle.Vpn = &FilePart{Filename: ImportDownloadFilename(protocol, connectionName), Body: importBody}
} else if bundle.Conf == nil && importBody != "" {
ext := ConfExtension(protocol, importBody)
bundle.Conf = &FilePart{Filename: connectionName + "." + ext, Body: importBody}
}
if protocol == "wireguard" && bundle.Conf != nil && bundle.Conf.Body != "" {
if zipBytes, err := WireGuardZipBytes(bundle.Conf.Filename, bundle.Conf.Body); err == nil && len(zipBytes) > 0 {
bundle.Zip = &FilePart{Filename: connectionName + ".zip", Body: string(zipBytes), Mime: "application/zip"}
}
}
return bundle
}
// DownloadPayloadForPart resolves a single download slot ("conf" | "vpn" | "zip")
// for an existing stored connection, given its protocol/connection name/response JSON.
func DownloadPayloadForPart(protocol, connectionName, jsonBody, part string) *FilePart {
part = strings.ToLower(strings.TrimSpace(part))
if part != "conf" && part != "vpn" && part != "zip" {
return nil
}
if strings.TrimSpace(jsonBody) == "" {
return nil
}
bundle := BundleFromResponseJSON(protocol, connectionName, jsonBody)
switch part {
case "conf":
return bundle.Conf
case "vpn":
return bundle.Vpn
case "zip":
return bundle.Zip
default:
return nil
}
}
// WireGuardZipBytes builds an in-memory .zip archive containing a single file
// (the WireGuard .conf) — handy for sending through messengers that mangle bare
// .conf attachments.
func WireGuardZipBytes(innerFilename, confBody string) ([]byte, error) {
name := sanitizeZipEntryName(innerFilename)
buf := &bytes.Buffer{}
zw := zip.NewWriter(buf)
w, err := zw.Create(name)
if err != nil {
return nil, err
}
if _, err := w.Write([]byte(confBody)); err != nil {
return nil, err
}
if err := zw.Close(); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func sanitizeZipEntryName(name string) string {
base := filepath.Base(strings.ReplaceAll(name, "\\", "/"))
base = zipNameBadRe.ReplaceAllString(base, "_")
if base == "" || base == "_" {
base = "amnezia.conf"
}
return base
}
// SortBundlesWireguardFirst orders bundles WireGuard -> AWG -> VLESS -> other, then
// newest first within the same protocol group.
func SortBundlesWireguardFirst(bundles []Bundle) []Bundle {
sort.SliceStable(bundles, func(i, j int) bool {
ri, rj := ProtocolSortRank(bundles[i].Protocol), ProtocolSortRank(bundles[j].Protocol)
if ri != rj {
return ri < rj
}
return bundles[i].CreatedAt.After(bundles[j].CreatedAt)
})
return bundles
}
// DisplayBody truncates a config body for inline display on the guest page (full
// text remains available via download).
func DisplayBody(body string, maxLen int) string {
if maxLen < 256 {
maxLen = 256
}
if len(body) <= maxLen {
return body
}
return body[:maxLen] + "\n\n… (сокращено для отображения; скачайте файл целиком)"
}
// QRMaxPayloadLen is the maximum string length considered safe to render as a QR
// code on the guest page.
const QRMaxPayloadLen = 1800