Add Go rewrite with Postgres 17 and Dokploy Docker Compose
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,277 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"amnezia-share/internal/i18n"
|
||||
"amnezia-share/internal/models"
|
||||
"amnezia-share/internal/share"
|
||||
"amnezia-share/internal/web"
|
||||
)
|
||||
|
||||
func registerShare(a *web.App, mux *http.ServeMux) {
|
||||
mux.HandleFunc("GET /share", ShareGet(a))
|
||||
mux.HandleFunc("POST /share", a.CSRFProtect(SharePost(a)))
|
||||
mux.HandleFunc("GET /share/servers", ShareServers(a))
|
||||
mux.HandleFunc("GET /share/download", ShareDownload(a))
|
||||
}
|
||||
|
||||
type shareView struct {
|
||||
Token string
|
||||
Link *models.ShareLink
|
||||
Servers []models.ServerInfo
|
||||
Bundles []share.Bundle
|
||||
Maintenance bool
|
||||
Expired bool
|
||||
LimitReached bool
|
||||
NotFound bool
|
||||
OK string
|
||||
Error string
|
||||
}
|
||||
|
||||
func buildBundles(creations []models.ShareCreation) []share.Bundle {
|
||||
out := make([]share.Bundle, 0, len(creations))
|
||||
for _, c := range creations {
|
||||
if c.ResponseJSON == nil || strings.TrimSpace(*c.ResponseJSON) == "" {
|
||||
continue
|
||||
}
|
||||
b := share.BundleFromResponseJSON(c.Protocol, c.ConnectionName, *c.ResponseJSON)
|
||||
b.CreationID = c.ID
|
||||
b.CreatedAt = c.CreatedAt
|
||||
b.ServerID = c.ServerID
|
||||
out = append(out, b)
|
||||
}
|
||||
return share.SortBundlesWireguardFirst(out)
|
||||
}
|
||||
|
||||
func guestServersForLink(a *web.App, r *http.Request, link *models.ShareLink) []models.ServerInfo {
|
||||
ctx := r.Context()
|
||||
labels := a.Settings.ServerLabels(ctx)
|
||||
servers := share.BuildGuestServers(ctx, a.Settings, labels)
|
||||
if link != nil {
|
||||
servers = share.FilterServersForLink(*link, servers)
|
||||
}
|
||||
return servers
|
||||
}
|
||||
|
||||
// renderSharePage re-loads the link fresh (so post-action state is current) and
|
||||
// renders the guest share page.
|
||||
func renderSharePage(a *web.App, w http.ResponseWriter, r *http.Request, token, okMsg, errText string) {
|
||||
ctx := r.Context()
|
||||
view := shareView{Token: token, OK: okMsg, Error: errText}
|
||||
|
||||
link, err := a.Share.LinkByToken(ctx, token)
|
||||
if err != nil || link == nil {
|
||||
view.NotFound = true
|
||||
a.Render(w, r, "page_share", view)
|
||||
return
|
||||
}
|
||||
view.Link = link
|
||||
view.Expired = link.IsExpired() || link.CleanedAt != nil
|
||||
view.LimitReached = link.MaxUses > 0 && link.UseCount >= link.MaxUses
|
||||
view.Maintenance = a.Settings.Maintenance(ctx)
|
||||
view.Servers = guestServersForLink(a, r, link)
|
||||
|
||||
creations, _ := a.Share.ListCreationsForLink(ctx, link.ID)
|
||||
view.Bundles = buildBundles(creations)
|
||||
|
||||
a.Render(w, r, "page_share", view)
|
||||
}
|
||||
|
||||
// ShareGet renders the guest share page (share.php GET).
|
||||
func ShareGet(a *web.App) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if lg := i18n.LangFromRequest(r); lg != "" {
|
||||
i18n.Persist(w, r, a.Sessions, i18n.NormalizeLang(lg), false)
|
||||
}
|
||||
token := r.URL.Query().Get("k")
|
||||
renderSharePage(a, w, r, token, "", "")
|
||||
}
|
||||
}
|
||||
|
||||
func findActiveCreation(creations []models.ShareCreation, id int64) *models.ShareCreation {
|
||||
for i := range creations {
|
||||
if creations[i].ID == id {
|
||||
return &creations[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SharePost handles the guest actions on the share page: create / migrate /
|
||||
// renew (share.php POST). Responds JSON when X-Share-Async:1 is set, otherwise
|
||||
// re-renders the full page (share.php's classic form-post behaviour).
|
||||
func SharePost(a *web.App) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
token := r.FormValue("k")
|
||||
lang := i18n.Resolve(r, a.Sessions)
|
||||
|
||||
link, err := a.Share.LinkByToken(ctx, token)
|
||||
if err != nil || link == nil {
|
||||
finishShareAction(a, w, r, token, "", i18n.T(lang, "err_link_not_found", nil))
|
||||
return
|
||||
}
|
||||
|
||||
var actionErr error
|
||||
var okMsg string
|
||||
action := r.FormValue("action")
|
||||
maintenance := a.Settings.Maintenance(ctx)
|
||||
|
||||
switch action {
|
||||
case "create":
|
||||
if maintenance {
|
||||
actionErr = errors.New(i18n.T(lang, "err_maintenance_mode", nil))
|
||||
break
|
||||
}
|
||||
serverID := formInt(r, "server_id", -1)
|
||||
protocol := strings.TrimSpace(r.FormValue("protocol"))
|
||||
if !share.LinkServerAllowed(*link, serverID) {
|
||||
actionErr = errors.New(i18n.T(lang, "err_pick_server", nil))
|
||||
break
|
||||
}
|
||||
if a.Settings.DisabledServers(ctx)[serverID] {
|
||||
actionErr = errors.New(i18n.T(lang, "err_server_disabled", nil))
|
||||
break
|
||||
}
|
||||
if !share.ServerProtocolAllowed(ctx, a.Settings, serverID, protocol) {
|
||||
actionErr = errors.New(i18n.T(lang, "proto_unavailable", nil))
|
||||
break
|
||||
}
|
||||
_, addErr := a.Share.TryAddConnection(ctx, a.Panel, a.Settings, link, serverID, protocol)
|
||||
if addErr != nil {
|
||||
actionErr = addErr
|
||||
break
|
||||
}
|
||||
fresh, _ := a.Share.LinkByID(ctx, link.ID)
|
||||
remaining, max := 0, 0
|
||||
if fresh != nil {
|
||||
max = fresh.MaxUses
|
||||
remaining = fresh.MaxUses - fresh.UseCount
|
||||
}
|
||||
okMsg = i18n.T(lang, "ok_created", map[string]string{"remaining": fmt.Sprint(remaining), "max": fmt.Sprint(max)})
|
||||
|
||||
case "migrate":
|
||||
if maintenance {
|
||||
actionErr = errors.New(i18n.T(lang, "err_maintenance_mode", nil))
|
||||
break
|
||||
}
|
||||
creationID := formInt64(r, "creation_id", 0)
|
||||
newServerID := formInt(r, "new_server_id", -1)
|
||||
newProtocol := strings.TrimSpace(r.FormValue("new_protocol"))
|
||||
if !share.LinkServerAllowed(*link, newServerID) {
|
||||
actionErr = errors.New(i18n.T(lang, "err_pick_migrate_server", nil))
|
||||
break
|
||||
}
|
||||
if a.Settings.DisabledServers(ctx)[newServerID] {
|
||||
actionErr = errors.New(i18n.T(lang, "err_server_disabled", nil))
|
||||
break
|
||||
}
|
||||
if !share.ServerProtocolAllowed(ctx, a.Settings, newServerID, newProtocol) {
|
||||
actionErr = errors.New(i18n.T(lang, "proto_unavailable", nil))
|
||||
break
|
||||
}
|
||||
if err := a.Share.TryMigrateConnection(ctx, a.Panel, a.Settings, link, creationID, newServerID, newProtocol); err != nil {
|
||||
actionErr = err
|
||||
break
|
||||
}
|
||||
okMsg = i18n.T(lang, "ok_migrated", nil)
|
||||
|
||||
case "renew":
|
||||
code := r.FormValue("code")
|
||||
res, err := a.Share.TryRedeemGuest(ctx, token, code)
|
||||
if err != nil {
|
||||
actionErr = err
|
||||
break
|
||||
}
|
||||
until := ""
|
||||
if res.Link != nil {
|
||||
until = res.Link.ExpiresLabel()
|
||||
}
|
||||
okMsg = i18n.T(lang, "ok_renewed", map[string]string{"days": fmt.Sprint(res.AddDays), "until": until})
|
||||
|
||||
default:
|
||||
actionErr = errors.New("Неизвестное действие.")
|
||||
}
|
||||
|
||||
errText := ""
|
||||
if actionErr != nil {
|
||||
errText = i18n.TranslateGuestError(lang, actionErr.Error())
|
||||
}
|
||||
finishShareAction(a, w, r, token, okMsg, errText)
|
||||
}
|
||||
}
|
||||
|
||||
func finishShareAction(a *web.App, w http.ResponseWriter, r *http.Request, token, okMsg, errText string) {
|
||||
if wantsJSON(r) {
|
||||
if errText != "" {
|
||||
writeJSONError(w, http.StatusBadRequest, errText)
|
||||
return
|
||||
}
|
||||
writeJSONOK(w, map[string]any{"message": okMsg})
|
||||
return
|
||||
}
|
||||
renderSharePage(a, w, r, token, okMsg, errText)
|
||||
}
|
||||
|
||||
// ShareServers returns the guest-visible server catalog for a link as JSON,
|
||||
// used by the page's async refresh (share_servers.php).
|
||||
func ShareServers(a *web.App) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
token := r.URL.Query().Get("k")
|
||||
link, err := a.Share.LinkByToken(ctx, token)
|
||||
if err != nil || link == nil {
|
||||
writeJSONError(w, http.StatusNotFound, "Ссылка не найдена.")
|
||||
return
|
||||
}
|
||||
writeJSONOK(w, map[string]any{"servers": guestServersForLink(a, r, link)})
|
||||
}
|
||||
}
|
||||
|
||||
// ShareDownload streams a single downloadable artifact (.conf / .vpn / .zip)
|
||||
// for one of the link's created connections (share_downloads.php).
|
||||
func ShareDownload(a *web.App) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
q := r.URL.Query()
|
||||
token := q.Get("k")
|
||||
creationID := queryInt(r, "cre", 0)
|
||||
part := q.Get("part")
|
||||
|
||||
link, err := a.Share.LinkByToken(ctx, token)
|
||||
if err != nil || link == nil {
|
||||
http.Error(w, "Ссылка не найдена.", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
creations, err := a.Share.ListCreationsForLink(ctx, link.ID)
|
||||
if err != nil {
|
||||
http.Error(w, "Ошибка сервера.", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
creation := findActiveCreation(creations, int64(creationID))
|
||||
if creation == nil || creation.ResponseJSON == nil {
|
||||
http.Error(w, "Файл не найден.", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
fp := share.DownloadPayloadForPart(creation.Protocol, creation.ConnectionName, *creation.ResponseJSON, part)
|
||||
if fp == nil {
|
||||
http.Error(w, "Файл не найден.", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
serveFilePart(w, fp)
|
||||
}
|
||||
}
|
||||
|
||||
func serveFilePart(w http.ResponseWriter, fp *share.FilePart) {
|
||||
mime := fp.Mime
|
||||
if mime == "" {
|
||||
mime = "text/plain; charset=utf-8"
|
||||
}
|
||||
w.Header().Set("Content-Type", mime)
|
||||
w.Header().Set("Content-Disposition", `attachment; filename="`+strings.ReplaceAll(fp.Filename, `"`, "")+`"`)
|
||||
_, _ = w.Write([]byte(fp.Body))
|
||||
}
|
||||
Reference in New Issue
Block a user