package api
import (
"fmt"
"net/http"
"strings"
"sourcecraft.dev/bigbes/outline-distro/internal/awgserver"
"sourcecraft.dev/bigbes/outline-distro/internal/store"
)
func (h *Handler) getServer(w http.ResponseWriter, r *http.Request) {
srv := h.store.GetServer()
resp := ServerResponse{
Name: srv.Name,
ServerID: srv.ID,
MetricsEnabled: srv.MetricsEnabled,
CreatedTimestampMs: srv.CreatedTimestampMs,
Version: h.version,
HostnameForAccessKeys: srv.Hostname,
PortForNewAccessKeys: srv.PortForNewAccessKeys,
}
if srv.AccessKeyDataLimit != nil {
resp.AccessKeyDataLimit = &DataLimitJSON{Bytes: srv.AccessKeyDataLimit.Bytes}
}
writeJSON(w, http.StatusOK, resp)
}
func (h *Handler) renameServer(w http.ResponseWriter, r *http.Request) {
var req SetNameRequest
if err := readJSON(r, &req); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
return
}
if err := h.store.UpdateServer(func(s *store.ServerState) {
s.Name = req.Name
}); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
w.WriteHeader(http.StatusNoContent)
}
func (h *Handler) listAccessKeys(w http.ResponseWriter, r *http.Request) {
keys := h.store.ListKeys()
resp := make([]AccessKeyResponse, len(keys))
for i, k := range keys {
resp[i] = h.keyToResponse(k)
}
writeJSON(w, http.StatusOK, map[string]any{"accessKeys": resp})
}
func (h *Handler) getAccessKey(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
key, ok := h.store.GetKey(id)
if !ok {
writeError(w, http.StatusNotFound, "key not found")
return
}
writeJSON(w, http.StatusOK, h.keyToResponse(key))
}
func (h *Handler) createAccessKey(w http.ResponseWriter, r *http.Request) {
var req CreateAccessKeyRequest
if r.ContentLength > 0 {
if err := readJSON(r, &req); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
return
}
}
h.doCreateKey(w, req)
}
func (h *Handler) createAccessKeyWithID(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
var req CreateAccessKeyRequest
if r.ContentLength > 0 {
if err := readJSON(r, &req); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
return
}
}
req.ID = id
h.doCreateKey(w, req)
}
func (h *Handler) doCreateKey(w http.ResponseWriter, req CreateAccessKeyRequest) {
srv := h.store.GetServer()
password := req.Password
if password == "" {
var err error
password, err = store.GeneratePassword()
if err != nil {
writeError(w, http.StatusInternalServerError, "failed to generate password")
return
}
}
id := req.ID
if id == "" {
id = fmt.Sprintf("%d", srv.NextID)
if err := h.store.UpdateServer(func(s *store.ServerState) {
s.NextID++
}); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
}
method := req.Method
if method == "" {
method = srv.DefaultCipher
}
port := req.Port
if port == 0 {
port = srv.PortForNewAccessKeys
}
ak := store.AccessKey{
ID: id,
Name: req.Name,
Password: password,
Port: port,
Method: method,
}
if req.DataLimit != nil {
ak.DataLimit = &store.DataLimit{Bytes: req.DataLimit.Bytes}
}
// Generate AWG credentials if AWG is enabled.
if h.awgServer != nil && h.awgConfig != nil {
awgData, err := h.generateAWGKey()
if err != nil {
h.logger.Error("Failed to generate AWG key.", "err", err)
writeError(w, http.StatusInternalServerError, "failed to generate AWG credentials")
return
}
ak.AWG = awgData
}
if err := h.store.CreateKey(ak); err != nil {
writeError(w, http.StatusConflict, err.Error())
return
}
if err := h.syncKeys(); err != nil {
h.logger.Error("Failed to sync keys after create.", "err", err)
writeError(w, http.StatusInternalServerError, "failed to apply key configuration")
return
}
writeJSON(w, http.StatusCreated, h.keyToResponse(ak))
}
func (h *Handler) deleteAccessKey(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
if err := h.store.DeleteKey(id); err != nil {
writeError(w, http.StatusNotFound, "key not found")
return
}
if err := h.syncKeys(); err != nil {
h.logger.Error("Failed to sync keys after delete.", "err", err)
}
w.WriteHeader(http.StatusNoContent)
}
func (h *Handler) renameAccessKey(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
var req SetNameRequest
if err := readJSON(r, &req); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
return
}
if err := h.store.UpdateKey(id, func(k *store.AccessKey) {
k.Name = req.Name
}); err != nil {
writeError(w, http.StatusNotFound, "key not found")
return
}
w.WriteHeader(http.StatusNoContent)
}
func (h *Handler) setKeyDataLimit(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
var req SetDataLimitRequest
if err := readJSON(r, &req); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
return
}
if err := h.store.UpdateKey(id, func(k *store.AccessKey) {
k.DataLimit = &store.DataLimit{Bytes: req.Limit.Bytes}
}); err != nil {
writeError(w, http.StatusNotFound, "key not found")
return
}
w.WriteHeader(http.StatusNoContent)
}
func (h *Handler) removeKeyDataLimit(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
if err := h.store.UpdateKey(id, func(k *store.AccessKey) {
k.DataLimit = nil
}); err != nil {
writeError(w, http.StatusNotFound, "key not found")
return
}
w.WriteHeader(http.StatusNoContent)
}
func (h *Handler) setDefaultDataLimit(w http.ResponseWriter, r *http.Request) {
var req SetDataLimitRequest
if err := readJSON(r, &req); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
return
}
if err := h.store.UpdateServer(func(s *store.ServerState) {
s.AccessKeyDataLimit = &store.DataLimit{Bytes: req.Limit.Bytes}
}); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
w.WriteHeader(http.StatusNoContent)
}
func (h *Handler) removeDefaultDataLimit(w http.ResponseWriter, r *http.Request) {
if err := h.store.UpdateServer(func(s *store.ServerState) {
s.AccessKeyDataLimit = nil
}); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
w.WriteHeader(http.StatusNoContent)
}
func (h *Handler) setDefaultPort(w http.ResponseWriter, r *http.Request) {
var req SetPortRequest
if err := readJSON(r, &req); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
return
}
if req.Port < 1 || req.Port > 65535 {
writeError(w, http.StatusBadRequest, "port must be between 1 and 65535")
return
}
if err := h.store.UpdateServer(func(s *store.ServerState) {
s.PortForNewAccessKeys = req.Port
}); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
w.WriteHeader(http.StatusNoContent)
}
func (h *Handler) setHostname(w http.ResponseWriter, r *http.Request) {
var req SetHostnameRequest
if err := readJSON(r, &req); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
return
}
if err := h.store.UpdateServer(func(s *store.ServerState) {
s.Hostname = req.Hostname
}); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
w.WriteHeader(http.StatusNoContent)
}
func (h *Handler) getMetricsEnabled(w http.ResponseWriter, r *http.Request) {
srv := h.store.GetServer()
writeJSON(w, http.StatusOK, MetricsEnabledResponse{MetricsEnabled: srv.MetricsEnabled})
}
func (h *Handler) setMetricsEnabled(w http.ResponseWriter, r *http.Request) {
var req MetricsEnabledResponse
if err := readJSON(r, &req); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
return
}
if err := h.store.UpdateServer(func(s *store.ServerState) {
s.MetricsEnabled = req.MetricsEnabled
}); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
w.WriteHeader(http.StatusNoContent)
}
func (h *Handler) getTransferMetrics(w http.ResponseWriter, r *http.Request) {
transfer := h.tracker.GetTransferByKey()
writeJSON(w, http.StatusOK, TransferMetricsResponse{BytesTransferredByUserId: transfer})
}
func (h *Handler) keyToResponse(k store.AccessKey) AccessKeyResponse {
resp := AccessKeyResponse{
ID: k.ID,
Name: k.Name,
Password: k.Password,
Port: k.Port,
Method: k.Method,
AccessURL: h.accessURL(k),
}
if k.DataLimit != nil {
resp.DataLimit = &DataLimitJSON{Bytes: k.DataLimit.Bytes}
}
if k.AWG != nil && h.awgConfig != nil {
srv := h.store.GetServer()
hostname := srv.Hostname
if hostname == "" {
hostname = "localhost"
}
resp.AWG = &AWGKeyResponse{
PublicKey: k.AWG.PublicKey,
AllowedIP: k.AWG.AllowedIP,
Endpoint: fmt.Sprintf("%s:%d", hostname, h.awgConfig.ListenPort),
}
}
return resp
}
func (h *Handler) generateAWGKey() (*store.AWGKeyData, error) {
privKey, err := awgserver.GeneratePrivateKey()
if err != nil {
return nil, fmt.Errorf("generating private key: %w", err)
}
pubKey, err := awgserver.PublicKeyFromPrivate(privKey)
if err != nil {
return nil, fmt.Errorf("deriving public key: %w", err)
}
// Collect used IPs from existing keys.
keys := h.store.ListKeys()
used := make([]string, 0, len(keys))
for _, k := range keys {
if k.AWG != nil {
used = append(used, k.AWG.AllowedIP)
}
}
alloc, err := awgserver.NewIPAllocator(h.awgConfig.Address)
if err != nil {
return nil, fmt.Errorf("creating IP allocator: %w", err)
}
ip, err := alloc.Allocate(used)
if err != nil {
return nil, fmt.Errorf("allocating IP: %w", err)
}
return &store.AWGKeyData{
PrivateKey: privKey,
PublicKey: pubKey,
AllowedIP: ip,
}, nil
}
func (h *Handler) getAWGConfig(w http.ResponseWriter, r *http.Request) {
if h.awgServer == nil || h.awgConfig == nil {
writeError(w, http.StatusNotFound, "AmneziaWG is not enabled")
return
}
id := r.PathValue("id")
key, ok := h.store.GetKey(id)
if !ok {
writeError(w, http.StatusNotFound, "key not found")
return
}
if key.AWG == nil {
writeError(w, http.StatusNotFound, "key has no AWG credentials")
return
}
srv := h.store.GetServer()
hostname := srv.Hostname
if hostname == "" {
hostname = "localhost"
}
var b strings.Builder
fmt.Fprintf(&b, "[Interface]\n")
fmt.Fprintf(&b, "PrivateKey = %s\n", key.AWG.PrivateKey)
fmt.Fprintf(&b, "Address = %s\n", key.AWG.AllowedIP)
if h.awgConfig.DNS != "" {
fmt.Fprintf(&b, "DNS = %s\n", h.awgConfig.DNS)
}
fmt.Fprintf(&b, "MTU = %d\n", h.awgConfig.MTU)
// Obfuscation parameters.
if h.awgConfig.Jc > 0 {
fmt.Fprintf(&b, "Jc = %d\n", h.awgConfig.Jc)
fmt.Fprintf(&b, "Jmin = %d\n", h.awgConfig.Jmin)
fmt.Fprintf(&b, "Jmax = %d\n", h.awgConfig.Jmax)
}
fmt.Fprintf(&b, "S1 = %d\n", h.awgConfig.S1)
fmt.Fprintf(&b, "S2 = %d\n", h.awgConfig.S2)
fmt.Fprintf(&b, "S3 = %d\n", h.awgConfig.S3)
fmt.Fprintf(&b, "S4 = %d\n", h.awgConfig.S4)
fmt.Fprintf(&b, "H1 = %s\n", h.awgConfig.H1)
fmt.Fprintf(&b, "H2 = %s\n", h.awgConfig.H2)
fmt.Fprintf(&b, "H3 = %s\n", h.awgConfig.H3)
fmt.Fprintf(&b, "H4 = %s\n", h.awgConfig.H4)
fmt.Fprintf(&b, "\n[Peer]\n")
fmt.Fprintf(&b, "PublicKey = %s\n", srv.AWGPublicKey)
fmt.Fprintf(&b, "Endpoint = %s:%d\n", hostname, h.awgConfig.ListenPort)
fmt.Fprintf(&b, "AllowedIPs = 0.0.0.0/0, ::/0\n")
fmt.Fprintf(&b, "PersistentKeepalive = 25\n")
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s-awg.conf"`, key.Name))
w.WriteHeader(http.StatusOK)
w.Write([]byte(b.String()))
}