Powrót do strony głównej

Dynamiczne routy net/http: implementacja DeadDrop

Artykuł omawia implementację dynamicznych routów i autentykacji cookie w czystym net/http dla serwisu DeadDrop. Ręczne parsowanie ścieżek, tokeny HMAC, architektura use case. Pełny cykl z testami.

net/http: dynamiczne routy i cookie auth w DeadDrop
Advertisement 728x90

Dynamiczne trasy i uwierzytelnianie za pomocą ciasteczek w net/http: implementacja DeadDrop

W kontynuacji cyklu o czystym net/http implementujemy dynamiczne routowanie dla usługi DeadDrop. Użytkownicy będą mogli przeglądać sekrety za pomocą linków w formacie /secret/{id}. Standardowy mux nie obsługuje parametrów ścieżek do wersji Go 1.22, dlatego piszemy własny parser.

Handler rejestruje się na /secret/ z końcowym ukośnikiem — to przechwytuje wszystkie podrzędne ścieżki. Ręcznie wyodrębniamy ID z r.URL.Path.

func extractIDFromPath(path string) (string, bool) {
    const prefix = "/secret/"

    if !strings.HasPrefix(path, prefix) {
        return "", false
    }

    id := strings.TrimPrefix(path, prefix)
    if id == "" || strings.Contains(id, "/") {
        return "", false
    }

    return id, true
}

Funkcja sprawdza prefiks, przycina go i wyklucza nieprawidłowe ID (puste lub z /). To podstawowa ochrona przed błędnymi żądaniami.

Google AdInline article slot

Handler SecretHandler z DI

Tworzymy strukturę-handler do wstrzykiwania zależności:

type SecretHandler struct {
    // atrapa dla DI
}

func NewSecretHandler() *SecretHandler {
    return &SecretHandler{}
}

func (h *SecretHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    id, ok := extractIDFromPath(r.URL.Path)
    if !ok {
        http.NotFound(w, r)
        return
    }
    w.Header().Set("Content-Type", "text/plain; charset=utf-8")
    w.WriteHeader(http.StatusOK)
    w.Write([]byte("Zażądano sekretu z ID: " + id))
}

Rejestrujemy w router.go: mux.Handle("/secret/", secretHandler). Testujemy przypadki brzegowe:

  • /secret/12345/ → 404
  • /secret/ → 404
  • /secret/axQ/233 → 404
  • /secret/ad31H234 → OK z ID

Use Case dla pobierania sekretu

Definiujemy żądanie i odpowiedź:

Google AdInline article slot
type GetSecretRequest struct {
    ID       string
    Password string
}

type GetSecretResponse struct {
    ID        string
    Message   string
    FileName  string
    FileExt   string
    HasFile   bool
    ExpiresAt time.Time
}

Logika use case:

  • Wyodrębniamy sekret z magazynu
  • Sprawdzamy termin ważności (time.After()), usuwamy przeterminowane
  • Weryfikujemy hasło przez CheckPasswordHash
  • Zwracamy dane lub błąd

Uwierzytelnianie za pomocą ciasteczek

Aby chronić pobieranie plików, wprowadzimy uwierzytelnianie przez ciasteczka. Po pomyślnej weryfikacji hasła ustawiamy sesyjne ciasteczko z zaszyfrowanym tokenem dostępu.

  • Zalety: bezstanowe, skalowalne, brak przechowywania sesji
  • Parametry ciasteczka: HttpOnly, Secure, SameSite=Strict

Przy żądaniu /download/{id}:

Google AdInline article slot
  • Wyodrębniamy ID ze ścieżki
  • Odczytujemy ciasteczko auth_token
  • Dekodujemy i sprawdzamy token (ID + hash hasła + nonce)
  • Przy ważności — udostępniamy plik

Struktura tokenu dostępu

Token tworzony jest jako base64(ID + ":" + hash + ":" + nonce), gdzie:

  • hash = HMAC-SHA256(ID + password, secretKey)
  • nonce — jednorazowy numer do ochrony przed atakami replay

Przykład walidacji:

func validateDownloadToken(id, token string) bool {
    // dekodujemy token
    // wyodrębniamy hash i nonce
    // przeliczamy HMAC
    // porównujemy
    return true // lub false
}

Pełny cykl pracy z sekretem

  • Tworzenie: POST /create → generujemy ID, haszujemy hasło, zapisujemy
  • Przeglądanie: GET /secret/{id} → formularz wprowadzania hasła
  • Uwierzytelnianie: POST hasło → ciasteczko + przekierowanie
  • Pobieranie: GET /download/{id} → sprawdzenie ciasteczka → plik
  • Czyszczenie: TTL wygasł → usunięcie

Prototyp UI: strona z polem hasła, przycisk "Otwórz". Po sukcesie — zawartość + link do pliku.

Testowanie routingu

| Ścieżka testowa | Oczekiwanie | Wynik |

|---------------|----------|-----------|

| /secret/123/ | 404 | 404 |

| /secret/ | 404 | 404 |

| /secret/id/with/slash | 404 | 404 |

| /secret/valid123 | OK: "ID: valid123" | OK |

Przypadki brzegowe pokrywają bezpieczeństwo parsera.

Co jest ważne

  • Dynamiczne trasy: ręczne parsowanie r.URL.Path bez zewnętrznych routerów
  • Bezpieczeństwo: walidacja ID, sprawdzanie ukośników, TTL dla sekretów
  • Uwierzytelnianie: ciasteczko z tokenem HMAC zamiast sesji
  • Architektura: use case + DI, czysta logika poza handlerem
  • Skalowalność: bezstanowe, gotowe do klastra

— Editorial Team

Advertisement 728x90

Czytaj dalej