460 lines
10 KiB
Go
460 lines
10 KiB
Go
// backend/server_folders_proxy.go
|
|
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type serverFoldersAgentConfig struct {
|
|
MilestoneHost string
|
|
Scheme string
|
|
Port string
|
|
Token string
|
|
}
|
|
|
|
func (s *Server) handleProxyListServerFolders(w http.ResponseWriter, r *http.Request) {
|
|
s.proxyServerFoldersRequest(w, r, http.MethodGet)
|
|
}
|
|
|
|
func (s *Server) handleProxyCreateServerFolder(w http.ResponseWriter, r *http.Request) {
|
|
s.proxyServerFoldersRequest(w, r, http.MethodPost)
|
|
}
|
|
|
|
func (s *Server) handleProxyRenameServerFolder(w http.ResponseWriter, r *http.Request) {
|
|
s.proxyServerFoldersRequest(w, r, http.MethodPatch)
|
|
}
|
|
|
|
func (s *Server) handleProxyDeleteServerFolder(w http.ResponseWriter, r *http.Request) {
|
|
s.proxyServerFoldersRequest(w, r, http.MethodDelete)
|
|
}
|
|
|
|
func (s *Server) proxyServerFoldersRequest(w http.ResponseWriter, r *http.Request, method string) {
|
|
config, err := s.getServerFoldersAgentConfig(r.Context())
|
|
if err != nil {
|
|
writeError(w, http.StatusBadGateway, "Server-Folders-Agent-Konfiguration konnte nicht geladen werden")
|
|
return
|
|
}
|
|
|
|
targetURL, err := serverFoldersAgentURL(config, forwardedServerFoldersQuery(r.URL.Query()))
|
|
if err != nil {
|
|
writeError(w, http.StatusBadGateway, err.Error())
|
|
return
|
|
}
|
|
|
|
var body []byte
|
|
if method == http.MethodPost || method == http.MethodPatch || method == http.MethodDelete {
|
|
body, err = io.ReadAll(io.LimitReader(r.Body, 1<<20))
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "Request konnte nicht gelesen werden")
|
|
return
|
|
}
|
|
}
|
|
|
|
request, err := http.NewRequestWithContext(
|
|
r.Context(),
|
|
method,
|
|
targetURL,
|
|
bytes.NewReader(body),
|
|
)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadGateway, "Proxy-Request konnte nicht erstellt werden")
|
|
return
|
|
}
|
|
|
|
request.Header.Set("Accept", "application/json")
|
|
|
|
if method == http.MethodPost || method == http.MethodPatch || method == http.MethodDelete {
|
|
request.Header.Set("Content-Type", "application/json")
|
|
}
|
|
|
|
if config.Token != "" {
|
|
request.Header.Set("X-Server-Folders-Token", config.Token)
|
|
}
|
|
|
|
client := &http.Client{
|
|
Timeout: 20 * time.Second,
|
|
}
|
|
|
|
response, err := client.Do(request)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadGateway, "Server-Folders-Agent ist nicht erreichbar")
|
|
return
|
|
}
|
|
defer response.Body.Close()
|
|
|
|
responseBody, err := io.ReadAll(io.LimitReader(response.Body, 4<<20))
|
|
if err != nil {
|
|
writeError(w, http.StatusBadGateway, "Antwort vom Server-Folders-Agent konnte nicht gelesen werden")
|
|
return
|
|
}
|
|
|
|
contentType := response.Header.Get("Content-Type")
|
|
if strings.TrimSpace(contentType) == "" {
|
|
contentType = "application/json"
|
|
}
|
|
|
|
w.Header().Set("Content-Type", contentType)
|
|
w.WriteHeader(response.StatusCode)
|
|
_, _ = w.Write(responseBody)
|
|
}
|
|
|
|
func (s *Server) requestServerFoldersAgent(
|
|
ctx context.Context,
|
|
method string,
|
|
query url.Values,
|
|
payload any,
|
|
) ([]byte, int, error) {
|
|
config, err := s.getServerFoldersAgentConfig(ctx)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
targetURL, err := serverFoldersAgentURL(config, query.Encode())
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
var body []byte
|
|
if payload != nil {
|
|
body, err = json.Marshal(payload)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
}
|
|
|
|
request, err := http.NewRequestWithContext(
|
|
ctx,
|
|
method,
|
|
targetURL,
|
|
bytes.NewReader(body),
|
|
)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
request.Header.Set("Accept", "application/json")
|
|
if payload != nil {
|
|
request.Header.Set("Content-Type", "application/json")
|
|
}
|
|
|
|
if config.Token != "" {
|
|
request.Header.Set("X-Server-Folders-Token", config.Token)
|
|
}
|
|
|
|
client := &http.Client{
|
|
Timeout: 30 * time.Second,
|
|
}
|
|
|
|
response, err := client.Do(request)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
defer response.Body.Close()
|
|
|
|
responseBody, err := io.ReadAll(io.LimitReader(response.Body, 4<<20))
|
|
if err != nil {
|
|
return nil, response.StatusCode, err
|
|
}
|
|
|
|
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
|
return responseBody, response.StatusCode, fmt.Errorf(
|
|
"server-folders-agent request failed: method=%s url=%s status=%d body=%s",
|
|
method,
|
|
targetURL,
|
|
response.StatusCode,
|
|
string(responseBody),
|
|
)
|
|
}
|
|
|
|
return responseBody, response.StatusCode, nil
|
|
}
|
|
|
|
type serverFolderAgentFolderResponse struct {
|
|
OK bool `json:"ok"`
|
|
Folder struct {
|
|
Name string `json:"name"`
|
|
DisplayName string `json:"displayName,omitempty"`
|
|
Path string `json:"path"`
|
|
HasChildren bool `json:"hasChildren"`
|
|
} `json:"folder"`
|
|
}
|
|
|
|
func (s *Server) deleteServerFolderPath(
|
|
ctx context.Context,
|
|
rootType string,
|
|
pathValue string,
|
|
) error {
|
|
pathValue = strings.TrimSpace(pathValue)
|
|
if pathValue == "" {
|
|
return nil
|
|
}
|
|
|
|
query := url.Values{}
|
|
query.Set("rootType", rootType)
|
|
query.Set("path", pathValue)
|
|
|
|
responseBody, status, err := s.requestServerFoldersAgent(
|
|
ctx,
|
|
http.MethodDelete,
|
|
query,
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
if status == http.StatusNotFound {
|
|
return nil
|
|
}
|
|
|
|
return fmt.Errorf(
|
|
"Ordner konnte nicht gelöscht werden: path=%s status=%d body=%s error=%w",
|
|
pathValue,
|
|
status,
|
|
string(responseBody),
|
|
err,
|
|
)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) renameServerFolderPath(
|
|
ctx context.Context,
|
|
rootType string,
|
|
pathValue string,
|
|
nextName string,
|
|
) (string, error) {
|
|
pathValue = strings.TrimSpace(pathValue)
|
|
nextName = strings.TrimSpace(nextName)
|
|
|
|
if pathValue == "" || nextName == "" {
|
|
return pathValue, nil
|
|
}
|
|
|
|
query := url.Values{}
|
|
query.Set("rootType", rootType)
|
|
|
|
responseBody, _, err := s.requestServerFoldersAgent(
|
|
ctx,
|
|
http.MethodPatch,
|
|
query,
|
|
map[string]any{
|
|
"path": pathValue,
|
|
"name": nextName,
|
|
},
|
|
)
|
|
if err != nil {
|
|
return "", fmt.Errorf(
|
|
"Ordner konnte nicht umbenannt werden: path=%s name=%s body=%s error=%w",
|
|
pathValue,
|
|
nextName,
|
|
string(responseBody),
|
|
err,
|
|
)
|
|
}
|
|
|
|
var response serverFolderAgentFolderResponse
|
|
if err := json.Unmarshal(responseBody, &response); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if strings.TrimSpace(response.Folder.Path) == "" {
|
|
return "", fmt.Errorf("Server-Folders-Agent hat keinen neuen Pfad zurückgegeben")
|
|
}
|
|
|
|
return response.Folder.Path, nil
|
|
}
|
|
|
|
func (s *Server) getServerFoldersAgentConfig(ctx context.Context) (serverFoldersAgentConfig, error) {
|
|
var config serverFoldersAgentConfig
|
|
|
|
err := s.db.QueryRow(
|
|
ctx,
|
|
`
|
|
SELECT
|
|
host,
|
|
COALESCE(server_folders_agent_scheme, 'http'),
|
|
COALESCE(server_folders_agent_port, '8099'),
|
|
COALESCE(pgp_sym_decrypt(server_folders_agent_token_encrypted, $1), '')
|
|
FROM milestone_settings
|
|
WHERE id = 1
|
|
LIMIT 1
|
|
`,
|
|
s.milestoneEncryptionKey(),
|
|
).Scan(
|
|
&config.MilestoneHost,
|
|
&config.Scheme,
|
|
&config.Port,
|
|
&config.Token,
|
|
)
|
|
if err != nil {
|
|
return config, err
|
|
}
|
|
|
|
config.Scheme = normalizeServerFoldersAgentScheme(config.Scheme)
|
|
if config.Scheme == "" {
|
|
config.Scheme = "http"
|
|
}
|
|
|
|
config.Port = normalizeServerFoldersAgentPort(config.Port)
|
|
if config.Port == "" {
|
|
config.Port = "8099"
|
|
}
|
|
|
|
config.MilestoneHost = strings.TrimSpace(config.MilestoneHost)
|
|
config.Token = strings.TrimSpace(config.Token)
|
|
|
|
return config, nil
|
|
}
|
|
|
|
func serverFoldersAgentURL(config serverFoldersAgentConfig, rawQuery string) (string, error) {
|
|
return serverFoldersAgentURLForPath(config, "/server-folders", rawQuery)
|
|
}
|
|
|
|
func serverFoldersAgentURLForPath(config serverFoldersAgentConfig, path string, rawQuery string) (string, error) {
|
|
if strings.TrimSpace(config.MilestoneHost) == "" {
|
|
return "", fmt.Errorf("Milestone Host fehlt")
|
|
}
|
|
|
|
normalizedMilestoneHost := strings.TrimSpace(config.MilestoneHost)
|
|
if !strings.Contains(normalizedMilestoneHost, "://") {
|
|
normalizedMilestoneHost = "http://" + normalizedMilestoneHost
|
|
}
|
|
|
|
parsedMilestoneURL, err := url.Parse(normalizedMilestoneHost)
|
|
if err != nil || parsedMilestoneURL.Hostname() == "" {
|
|
return "", fmt.Errorf("Milestone Host ist ungültig")
|
|
}
|
|
|
|
agentURL := url.URL{
|
|
Scheme: config.Scheme,
|
|
Host: parsedMilestoneURL.Hostname() + ":" + config.Port,
|
|
Path: path,
|
|
RawQuery: rawQuery,
|
|
}
|
|
|
|
return agentURL.String(), nil
|
|
}
|
|
|
|
// generateServerFoldersAgentToken erzeugt einen kryptografisch zufälligen Token.
|
|
func generateServerFoldersAgentToken() (string, error) {
|
|
randomBytes := make([]byte, 32)
|
|
|
|
if _, err := rand.Read(randomBytes); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return base64.RawURLEncoding.EncodeToString(randomBytes), nil
|
|
}
|
|
|
|
// pushServerFoldersAgentToken setzt bzw. rotiert den Token am Agent. Die
|
|
// Authentifizierung erfolgt mit dem aktuell hinterlegten Token (config.Token).
|
|
// Solange am Agent noch kein Token gesetzt ist, akzeptiert dieser den Request
|
|
// auch ohne gültigen Authentifizierungs-Header (Bootstrap).
|
|
func (s *Server) pushServerFoldersAgentToken(
|
|
ctx context.Context,
|
|
config serverFoldersAgentConfig,
|
|
newToken string,
|
|
) error {
|
|
targetURL, err := serverFoldersAgentURLForPath(config, "/agent-token", "")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
body, err := json.Marshal(map[string]string{
|
|
"token": newToken,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
request, err := http.NewRequestWithContext(ctx, http.MethodPut, targetURL, bytes.NewReader(body))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
request.Header.Set("Accept", "application/json")
|
|
request.Header.Set("Content-Type", "application/json")
|
|
|
|
if config.Token != "" {
|
|
request.Header.Set("X-Server-Folders-Token", config.Token)
|
|
}
|
|
|
|
client := &http.Client{
|
|
Timeout: 15 * time.Second,
|
|
}
|
|
|
|
response, err := client.Do(request)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer response.Body.Close()
|
|
|
|
responseBody, _ := io.ReadAll(io.LimitReader(response.Body, 1<<20))
|
|
|
|
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
|
return fmt.Errorf(
|
|
"server-folders-agent token rotation failed: url=%s status=%d body=%s",
|
|
targetURL,
|
|
response.StatusCode,
|
|
string(responseBody),
|
|
)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func forwardedServerFoldersQuery(values url.Values) string {
|
|
nextValues := url.Values{}
|
|
|
|
for key, value := range values {
|
|
switch strings.ToLower(strings.TrimSpace(key)) {
|
|
case "schema", "scheme", "agentschema", "agentscheme", "port", "agentport", "token", "agenttoken":
|
|
continue
|
|
default:
|
|
for _, item := range value {
|
|
nextValues.Add(key, item)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nextValues.Encode()
|
|
}
|
|
|
|
func normalizeServerFoldersAgentScheme(value string) string {
|
|
value = strings.TrimSpace(strings.ToLower(value))
|
|
|
|
switch value {
|
|
case "", "http", "https":
|
|
return value
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func normalizeServerFoldersAgentPort(value string) string {
|
|
value = strings.TrimSpace(value)
|
|
value = strings.TrimPrefix(value, ":")
|
|
|
|
if value == "" {
|
|
return ""
|
|
}
|
|
|
|
for _, char := range value {
|
|
if char < '0' || char > '9' {
|
|
return ""
|
|
}
|
|
}
|
|
|
|
return value
|
|
}
|