278 lines
8.4 KiB
Go
278 lines
8.4 KiB
Go
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))
|
|
}
|