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)) }