mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-10 17:26:38 +08:00
refactor(backend): rename OpenFlare directory to lowercase openflare
This commit is contained in:
@@ -0,0 +1,426 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package pages
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/netip"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type remoteSourceResolverFunc func(context.Context, string, string) ([]netip.Addr, error)
|
||||
|
||||
func (function remoteSourceResolverFunc) LookupNetIP(
|
||||
ctx context.Context,
|
||||
network string,
|
||||
host string,
|
||||
) ([]netip.Addr, error) {
|
||||
return function(ctx, network, host)
|
||||
}
|
||||
|
||||
func TestFetchRemoteSourceTrustedInternalSelfSignedAndSafeLabel(t *testing.T) {
|
||||
packageBytes := makeRemoteSourceZIP(t)
|
||||
server := httptest.NewTLSServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
if request.URL.Query().Get("token") != "source-secret" {
|
||||
t.Error("signed query did not reach the artifact server")
|
||||
}
|
||||
if request.Header.Get("Accept-Encoding") != "identity" {
|
||||
t.Error("artifact request must disable automatic HTTP decompression")
|
||||
}
|
||||
writer.Header().Set("Content-Disposition", `attachment; filename="redirected.tar.gz"`)
|
||||
_, _ = writer.Write(packageBytes)
|
||||
}))
|
||||
server.Config.ErrorLog = log.New(io.Discard, "", 0)
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
candidate, err := FetchRemoteSource(t.Context(), RemoteSourceRequest{
|
||||
URL: server.URL + "/original/site.zip?token=source-secret",
|
||||
AllowInsecure: true,
|
||||
MaxPackageBytes: int64(len(packageBytes) + 1),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("FetchRemoteSource() error = %v", err)
|
||||
}
|
||||
if candidate.Format != "zip" {
|
||||
t.Fatalf("Format = %q, want zip", candidate.Format)
|
||||
}
|
||||
if candidate.SafeLabel != "site.zip" {
|
||||
t.Fatalf("SafeLabel = %q, want original path basename", candidate.SafeLabel)
|
||||
}
|
||||
if candidate.PackageSize != int64(len(packageBytes)) {
|
||||
t.Fatalf("PackageSize = %d, want %d", candidate.PackageSize, len(packageBytes))
|
||||
}
|
||||
wantChecksum := sha256.Sum256(packageBytes)
|
||||
if candidate.Checksum != hex.EncodeToString(wantChecksum[:]) {
|
||||
t.Fatalf("Checksum = %q, want SHA-256", candidate.Checksum)
|
||||
}
|
||||
downloaded, err := os.ReadFile(candidate.TempPath) //nolint:gosec // provider-owned test temp file
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFile() error = %v", err)
|
||||
}
|
||||
if !bytes.Equal(downloaded, packageBytes) {
|
||||
t.Fatal("downloaded package differs from response body")
|
||||
}
|
||||
tempPath := candidate.TempPath
|
||||
if err := candidate.Cleanup(); err != nil {
|
||||
t.Fatalf("Cleanup() error = %v", err)
|
||||
}
|
||||
if err := candidate.Cleanup(); err != nil {
|
||||
t.Fatalf("second Cleanup() error = %v", err)
|
||||
}
|
||||
if _, err := os.Stat(tempPath); !errors.Is(err, os.ErrNotExist) {
|
||||
t.Fatalf("temporary file still exists: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchRemoteSourceKeepsOriginalLabelAcrossRedirect(t *testing.T) {
|
||||
packageBytes := makeRemoteSourceZIP(t)
|
||||
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
if request.URL.Path == "/original/site.zip" {
|
||||
writer.Header().Set("Location", "/delivery/final.tar.gz?token=redirect-secret")
|
||||
writer.WriteHeader(http.StatusFound)
|
||||
return
|
||||
}
|
||||
if request.Header.Get("Referer") != "" {
|
||||
t.Error("redirect must not forward a signed source URL as Referer")
|
||||
}
|
||||
writer.Header().Set("Content-Disposition", `attachment; filename="response.7z"`)
|
||||
_, _ = writer.Write(packageBytes)
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
candidate, err := FetchRemoteSource(t.Context(), RemoteSourceRequest{
|
||||
URL: server.URL + "/original/site.zip?token=initial-secret",
|
||||
AllowInsecure: true,
|
||||
MaxPackageBytes: int64(len(packageBytes) + 1),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("FetchRemoteSource() error = %v", err)
|
||||
}
|
||||
defer func() { _ = candidate.Cleanup() }()
|
||||
if candidate.SafeLabel != "site.zip" || candidate.Format != "zip" {
|
||||
t.Fatalf("candidate = label %q format %q, want original site.zip", candidate.SafeLabel, candidate.Format)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchRemoteSourcePublicAllowsPrivateAddresses(t *testing.T) {
|
||||
packageBytes := makeRemoteSourceZIP(t)
|
||||
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) {
|
||||
_, _ = writer.Write(packageBytes)
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
candidate, err := FetchRemoteSource(t.Context(), RemoteSourceRequest{
|
||||
URL: server.URL + "/site.zip?token=private-secret",
|
||||
AllowInsecure: false,
|
||||
MaxPackageBytes: int64(len(packageBytes) + 1),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("FetchRemoteSource() error = %v", err)
|
||||
}
|
||||
defer func() { _ = candidate.Cleanup() }()
|
||||
if candidate.Format != "zip" {
|
||||
t.Fatalf("candidate format = %q, want zip", candidate.Format)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchRemoteSourcePublicUsesDirectDialer(t *testing.T) {
|
||||
packageBytes := makeRemoteSourceZIP(t)
|
||||
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) {
|
||||
_, _ = writer.Write(packageBytes)
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
var dialedAddress string
|
||||
dependencies := mappedRemoteSourceDependencies(server.Listener.Addr().String(), staticPublicRemoteSourceResolver())
|
||||
dependencies.dialContext = func(ctx context.Context, network string, address string) (net.Conn, error) {
|
||||
dialedAddress = address
|
||||
dialer := &net.Dialer{}
|
||||
return dialer.DialContext(ctx, network, server.Listener.Addr().String())
|
||||
}
|
||||
candidate, err := fetchRemoteSource(t.Context(), RemoteSourceRequest{
|
||||
URL: "http://artifact.example/site.zip",
|
||||
AllowInsecure: false,
|
||||
MaxPackageBytes: int64(len(packageBytes) + 1),
|
||||
}, dependencies)
|
||||
if err != nil {
|
||||
t.Fatalf("fetchRemoteSource() error = %v", err)
|
||||
}
|
||||
defer func() { _ = candidate.Cleanup() }()
|
||||
if dialedAddress != "artifact.example:80" {
|
||||
t.Fatalf("direct dial address = %q, want hostname dial", dialedAddress)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchRemoteSourcePublicAllowsPrivateRedirect(t *testing.T) {
|
||||
packageBytes := makeRemoteSourceZIP(t)
|
||||
var privateServer *httptest.Server
|
||||
privateServer = httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) {
|
||||
_, _ = writer.Write(packageBytes)
|
||||
}))
|
||||
t.Cleanup(privateServer.Close)
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) {
|
||||
writer.Header().Set("Location", privateServer.URL+"/private.zip?token=redirect-secret")
|
||||
writer.WriteHeader(http.StatusFound)
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
candidate, err := FetchRemoteSource(t.Context(), RemoteSourceRequest{
|
||||
URL: server.URL + "/start.zip?token=initial-secret",
|
||||
AllowInsecure: false,
|
||||
MaxPackageBytes: int64(len(packageBytes) + 1),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("FetchRemoteSource() error = %v", err)
|
||||
}
|
||||
defer func() { _ = candidate.Cleanup() }()
|
||||
if candidate.Format != "zip" {
|
||||
t.Fatalf("candidate format = %q, want zip", candidate.Format)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchRemoteSourcePublicRejectsSelfSignedTLS(t *testing.T) {
|
||||
packageBytes := makeRemoteSourceZIP(t)
|
||||
server := httptest.NewTLSServer(http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) {
|
||||
_, _ = writer.Write(packageBytes)
|
||||
}))
|
||||
server.Config.ErrorLog = log.New(io.Discard, "", 0)
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
dependencies := mappedRemoteSourceDependencies(server.Listener.Addr().String(), staticPublicRemoteSourceResolver())
|
||||
rawURL := "https://artifact.example/site.zip?signature=tls-secret"
|
||||
_, err := fetchRemoteSource(t.Context(), RemoteSourceRequest{
|
||||
URL: rawURL,
|
||||
AllowInsecure: false,
|
||||
MaxPackageBytes: int64(len(packageBytes) + 1),
|
||||
}, dependencies)
|
||||
if !errors.Is(err, errRemoteProviderDownloadFailed) {
|
||||
t.Fatalf("fetchRemoteSource() error = %v, want strict TLS failure", err)
|
||||
}
|
||||
assertRemoteSourceErrorRedacted(t, err, rawURL, "tls-secret", "signature=")
|
||||
}
|
||||
|
||||
func TestFetchRemoteSourceRejectsChunkedBodyOverLimitAndCleansTemp(t *testing.T) {
|
||||
const maxPackageBytes = int64(64)
|
||||
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) {
|
||||
_, _ = writer.Write(bytes.Repeat([]byte{'x'}, int(maxPackageBytes)))
|
||||
if flusher, ok := writer.(http.Flusher); ok {
|
||||
flusher.Flush()
|
||||
}
|
||||
_, _ = writer.Write([]byte("overflow"))
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
tempDir := t.TempDir()
|
||||
dependencies := defaultRemoteSourceDependenciesForTest()
|
||||
dependencies.createTemp = func(_ string, pattern string) (*os.File, error) {
|
||||
return os.CreateTemp(tempDir, pattern)
|
||||
}
|
||||
rawURL := server.URL + "/site.zip?token=chunk-secret"
|
||||
_, err := fetchRemoteSource(t.Context(), RemoteSourceRequest{
|
||||
URL: rawURL,
|
||||
AllowInsecure: true,
|
||||
MaxPackageBytes: maxPackageBytes,
|
||||
}, dependencies)
|
||||
if !errors.Is(err, errRemoteProviderTooLarge) {
|
||||
t.Fatalf("fetchRemoteSource() error = %v, want actual stream limit", err)
|
||||
}
|
||||
assertRemoteSourceTempDirEmpty(t, tempDir)
|
||||
assertRemoteSourceErrorRedacted(t, err, rawURL, "chunk-secret", "token=")
|
||||
}
|
||||
|
||||
func TestFetchRemoteSourceRejectsContentLengthBeforeCreatingTemp(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) {
|
||||
writer.Header().Set("Content-Length", "4096")
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
var createCount atomic.Int32
|
||||
dependencies := defaultRemoteSourceDependenciesForTest()
|
||||
dependencies.createTemp = func(directory string, pattern string) (*os.File, error) {
|
||||
createCount.Add(1)
|
||||
return os.CreateTemp(directory, pattern)
|
||||
}
|
||||
_, err := fetchRemoteSource(t.Context(), RemoteSourceRequest{
|
||||
URL: server.URL + "/site.zip",
|
||||
AllowInsecure: true,
|
||||
MaxPackageBytes: 1024,
|
||||
}, dependencies)
|
||||
if !errors.Is(err, errRemoteProviderTooLarge) {
|
||||
t.Fatalf("fetchRemoteSource() error = %v, want Content-Length rejection", err)
|
||||
}
|
||||
if createCount.Load() != 0 {
|
||||
t.Fatalf("CreateTemp called %d times before Content-Length rejection", createCount.Load())
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchRemoteSourceSniffsAtLeast512BytesForTar(t *testing.T) {
|
||||
packageBytes := make([]byte, remoteSourceMagicSniffBytes)
|
||||
copy(packageBytes[257:], []byte("ustar"))
|
||||
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) {
|
||||
_, _ = writer.Write(packageBytes)
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
candidate, err := FetchRemoteSource(t.Context(), RemoteSourceRequest{
|
||||
URL: server.URL + "/download",
|
||||
AllowInsecure: true,
|
||||
MaxPackageBytes: int64(len(packageBytes) + 1),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("FetchRemoteSource() error = %v", err)
|
||||
}
|
||||
defer func() { _ = candidate.Cleanup() }()
|
||||
if candidate.Format != "tar" {
|
||||
t.Fatalf("Format = %q, want tar detected at byte 257", candidate.Format)
|
||||
}
|
||||
if candidate.SafeLabel != "download.tar" {
|
||||
t.Fatalf("SafeLabel = %q, want download.tar", candidate.SafeLabel)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchRemoteSourceRedactsURLHeadersAndBodyFromErrors(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) {
|
||||
writer.Header().Set("X-Artifact-Secret", "header-secret")
|
||||
_, _ = writer.Write([]byte("response-body-secret"))
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
tempDir := t.TempDir()
|
||||
dependencies := defaultRemoteSourceDependenciesForTest()
|
||||
dependencies.createTemp = func(_ string, pattern string) (*os.File, error) {
|
||||
return os.CreateTemp(tempDir, pattern)
|
||||
}
|
||||
rawURL := server.URL + "/download?token=query-secret"
|
||||
_, err := fetchRemoteSource(t.Context(), RemoteSourceRequest{
|
||||
URL: rawURL,
|
||||
AllowInsecure: true,
|
||||
MaxPackageBytes: 1024,
|
||||
}, dependencies)
|
||||
if !errors.Is(err, errRemoteProviderUnsupported) {
|
||||
t.Fatalf("fetchRemoteSource() error = %v, want unsupported archive", err)
|
||||
}
|
||||
assertRemoteSourceTempDirEmpty(t, tempDir)
|
||||
assertRemoteSourceErrorRedacted(
|
||||
t,
|
||||
err,
|
||||
rawURL,
|
||||
"query-secret",
|
||||
"header-secret",
|
||||
"response-body-secret",
|
||||
"token=",
|
||||
)
|
||||
}
|
||||
|
||||
func TestFetchRemoteSourceAllowsFiveRedirectsOnly(t *testing.T) {
|
||||
packageBytes := makeRemoteSourceZIP(t)
|
||||
var requestCount atomic.Int32
|
||||
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
requestCount.Add(1)
|
||||
redirectNumber, _ := strconv.Atoi(strings.TrimPrefix(request.URL.Path, "/"))
|
||||
if redirectNumber < remoteSourceMaxRedirects+1 {
|
||||
writer.Header().Set("Location", "/"+strconv.Itoa(redirectNumber+1))
|
||||
writer.WriteHeader(http.StatusFound)
|
||||
return
|
||||
}
|
||||
_, _ = writer.Write(packageBytes)
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
dependencies := mappedRemoteSourceDependencies(server.Listener.Addr().String(), staticPublicRemoteSourceResolver())
|
||||
_, err := fetchRemoteSource(t.Context(), RemoteSourceRequest{
|
||||
URL: "http://artifact.example/0",
|
||||
AllowInsecure: false,
|
||||
MaxPackageBytes: int64(len(packageBytes) + 1),
|
||||
}, dependencies)
|
||||
if !errors.Is(err, errRemoteProviderRedirectLimit) {
|
||||
t.Fatalf("fetchRemoteSource() error = %v, want redirect limit", err)
|
||||
}
|
||||
if requestCount.Load() != remoteSourceMaxRedirects+1 {
|
||||
t.Fatalf("request count = %d, want initial plus five redirects", requestCount.Load())
|
||||
}
|
||||
}
|
||||
|
||||
func staticPublicRemoteSourceResolver() remoteSourceResolver {
|
||||
return remoteSourceResolverFunc(func(context.Context, string, string) ([]netip.Addr, error) {
|
||||
return []netip.Addr{netip.MustParseAddr("93.184.216.34")}, nil
|
||||
})
|
||||
}
|
||||
|
||||
func defaultRemoteSourceDependenciesForTest() remoteSourceDependencies {
|
||||
dialer := &net.Dialer{}
|
||||
return remoteSourceDependencies{
|
||||
resolver: net.DefaultResolver,
|
||||
dialContext: dialer.DialContext,
|
||||
createTemp: os.CreateTemp,
|
||||
}
|
||||
}
|
||||
|
||||
func mappedRemoteSourceDependencies(targetAddress string, resolver remoteSourceResolver) remoteSourceDependencies {
|
||||
dialer := &net.Dialer{}
|
||||
return remoteSourceDependencies{
|
||||
resolver: resolver,
|
||||
dialContext: func(ctx context.Context, network string, _ string) (net.Conn, error) {
|
||||
return dialer.DialContext(ctx, network, targetAddress)
|
||||
},
|
||||
createTemp: os.CreateTemp,
|
||||
}
|
||||
}
|
||||
|
||||
func makeRemoteSourceZIP(t *testing.T) []byte {
|
||||
t.Helper()
|
||||
var buffer bytes.Buffer
|
||||
archive := zip.NewWriter(&buffer)
|
||||
file, err := archive.Create("index.html")
|
||||
if err != nil {
|
||||
t.Fatalf("zip.Create() error = %v", err)
|
||||
}
|
||||
if _, err := file.Write([]byte("<h1>OpenFlare</h1>")); err != nil {
|
||||
t.Fatalf("zip entry Write() error = %v", err)
|
||||
}
|
||||
if err := archive.Close(); err != nil {
|
||||
t.Fatalf("zip.Close() error = %v", err)
|
||||
}
|
||||
return buffer.Bytes()
|
||||
}
|
||||
|
||||
func assertRemoteSourceTempDirEmpty(t *testing.T, directory string) {
|
||||
t.Helper()
|
||||
entries, err := os.ReadDir(directory)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadDir() error = %v", err)
|
||||
}
|
||||
if len(entries) != 0 {
|
||||
t.Fatalf("temporary directory contains %d leaked files", len(entries))
|
||||
}
|
||||
}
|
||||
|
||||
func assertRemoteSourceErrorRedacted(t *testing.T, err error, sensitiveValues ...string) {
|
||||
t.Helper()
|
||||
if err == nil {
|
||||
t.Fatal("expected an error")
|
||||
}
|
||||
message := err.Error()
|
||||
for _, sensitiveValue := range sensitiveValues {
|
||||
if sensitiveValue != "" && strings.Contains(message, sensitiveValue) {
|
||||
t.Fatalf("error %q contains sensitive value %q", message, sensitiveValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user