This commit is contained in:
Andras Bacsai
2026-01-14 09:40:14 +01:00
commit b5705acac6
17 changed files with 897 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
.gitignore
CLAUDE.md
LICENSE
README.md
test.sh
+28
View File
@@ -0,0 +1,28 @@
# Binaries for programs and plugins
coollabs-cdn
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool
*.out
# Dependency directories
vendor/
# Go workspace file
go.work
# IDE and editor files
.vscode/
.idea/
*.swp
*.swo
*~
.DS_Store
+108
View File
@@ -0,0 +1,108 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
coolLabs CDN is a lightweight Go-based CDN server that serves static JSON files and images with HTTP caching support. It's designed to be deployed as a minimal Docker container (scratch-based, ~10MB) with multi-architecture support (AMD64/ARM64).
## Core Architecture
### Embedded File System
- All JSON files in the `json/` directory and images in the `images/` directory are embedded into the Go binary at build time using `go:embed`
- The `loadJSONFiles()` and `loadImageFiles()` functions recursively walk the embedded filesystem on startup
- Files are loaded into memory maps (`files` and `etags`) with pre-calculated MD5 ETags
- URL paths mirror the directory structure (e.g., `json/api/v1/data.json``/api/v1/data.json`, `images/logo.png``/logo.png`)
### Request Handling Flow
The main request handler (`handleRequest()`) processes requests in this order:
1. Sets CORS headers for all requests
2. Handles OPTIONS preflight requests (returns 204)
3. Root path (`/`) → redirects to `https://{BASE_FQDN}`
4. Health check (`/health`) → returns "healthy" with 200
5. Known files → serves JSON or images with appropriate Content-Type and ETag caching
6. Unknown files (404) → redirects to `https://{BASE_FQDN}{RequestURI}`
### Caching Strategy
- MD5-based ETags are calculated once at startup for all files
- Client sends `If-None-Match` header with ETag
- Server returns 304 Not Modified if ETag matches
- Also sets `Last-Modified` and supports Range requests via `http.ServeContent()`
## Development Commands
### Building
```bash
# Local build
go build -o coollabs-cdn main.go
# Docker build (multi-arch)
docker buildx build --platform linux/amd64,linux/arm64 -t coollabs-cdn .
# Docker build with custom redirect domain
docker buildx build --platform linux/amd64,linux/arm64 --build-arg BASE_FQDN=yourdomain.com -t coollabs-cdn .
```
### Running
```bash
# Run locally (requires json/ and images/ directories)
go run main.go
# Run with custom BASE_FQDN
BASE_FQDN=example.com go run main.go
# Run Docker container
docker run -p 8080:80 coollabs-cdn
# Run with runtime BASE_FQDN override
docker run -e BASE_FQDN=mysite.com -p 8080:80 coollabs-cdn
```
### Testing
```bash
# Run full test suite against localhost:8080
./test.sh
# Test against custom port
./test.sh 8081
# Test against custom host and port
./test.sh 8081 example.com
```
The test script (`test.sh`) verifies all functionality including health checks, JSON and image serving, ETag caching, CORS, redirects, and HTTP headers.
## Configuration
### BASE_FQDN
The only configurable parameter controls where root and 404 requests redirect:
- **Build-time**: `--build-arg BASE_FQDN=domain.com` (default: `coollabs.io`)
- **Runtime**: `-e BASE_FQDN=domain.com` environment variable
- Used in `main.go:62-65` and `main.go:102,118`
## Adding New Files
### JSON Files
1. Place files in `json/` directory (supports nested subdirectories)
2. Rebuild the Docker image (files are embedded at build time)
3. Files automatically become available at their path (e.g., `json/data.json``/data.json`)
### Image Files
1. Place image files in `images/` directory (supports nested subdirectories)
2. Supported formats: PNG, JPEG, GIF, WEBP, SVG, BMP, ICO
3. Rebuild the Docker image (files are embedded at build time)
4. Files automatically become available at their path (e.g., `images/logo.png``/logo.png`)
## Key Implementation Details
### Why Scratch Base Image
Uses `FROM scratch` for minimal attack surface and size. No shell, no package manager, just the statically-compiled binary and embedded files.
### Why Embed vs Runtime Loading
Files are embedded using `//go:embed json/*` and `//go:embed images/*` because:
- Scratch containers have no filesystem to mount
- Faster startup (no disk I/O)
- Immutable deployments (files can't change at runtime)
### CORS Configuration
All responses include `Access-Control-Allow-Origin: *` to allow cross-origin requests from any domain. Handles OPTIONS preflight requests for complex CORS scenarios.
+41
View File
@@ -0,0 +1,41 @@
# Build stage
FROM --platform=$BUILDPLATFORM golang:1.21-alpine AS builder
WORKDIR /app
# Copy go mod file (go.sum may not exist for projects with no external deps)
COPY go.mod* go.sum* ./
# Download dependencies
RUN go mod download
# Copy source code
COPY main.go healthcheck.go ./
COPY json/ ./json/
COPY images/ ./images/
# Build the binaries with optimizations for the target platform
ARG TARGETOS TARGETARCH BASE_FQDN=coollabs.io
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -a -installsuffix cgo -o coollabs-cdn main.go
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -a -installsuffix cgo -o healthcheck healthcheck.go
# Final stage
FROM scratch
# Copy the binaries from builder stage
COPY --from=builder /app/coollabs-cdn /coollabs-cdn
COPY --from=builder /app/healthcheck /healthcheck
# Set the base FQDN environment variable
ARG BASE_FQDN=coollabs.io
ENV BASE_FQDN=$BASE_FQDN
# Expose port 80
EXPOSE 80
# Add healthcheck
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD ["/healthcheck"]
# Run the binary
CMD ["/coollabs-cdn"]
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 coolLabs Solutions Kft.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+123
View File
@@ -0,0 +1,123 @@
# coolLabs CDN
A lightweight Go-based CDN for serving static JSON files with ETag support.
## Directory Structure
```
.
├── Dockerfile
├── main.go
├── go.mod
├── json/ # Place your JSON files here (supports subdirectories)
│ ├── file.json # Served at /file.json
│ └── subdir/
│ └── data.json # Served at /subdir/data.json
└── README.md
```
## Features
- **Recursive Directory Support**: Serves JSON files from nested subdirectories
- **Dynamic File Loading**: Automatically discovers and serves all `*.json` files
- **Configurable Redirects**: Customizable base domain for redirects via `BASE_FQDN`
- **ETag Support**: MD5-based ETag generation for efficient cache validation
- **CORS Enabled**: Full cross-origin request support with preflight handling
- **JSON MIME Types**: Proper Content-Type headers for JSON files
- **HTTP Caching**: Last-Modified headers and 304 Not Modified responses
- **Range Requests**: Support for partial content requests (Accept-Ranges: bytes)
- **Scratch-based**: Ultra-small image size (~10MB)
- **Multi-arch**: Compatible with AMD64 and ARM64 architectures
- **Health Check Endpoint**: Available at `/health`
- **Embedded Files**: JSON files embedded in binary for instant startup
- **HEAD Method**: Full support for HEAD requests
## Usage
1. Add your JSON files to the `json/` directory (supports nested directories):
```bash
# Root level files
echo '{"message": "Hello World"}' > json/example.json
echo '{"version": "1.0.0", "data": []}' > json/config.json
# Nested directories
mkdir -p json/api/v1
echo '{"endpoints": ["/users", "/posts"]}' > json/api/v1/routes.json
echo '{"database": "connected"}' > json/api/v1/health.json
```
2. Build the Docker image:
```bash
docker buildx build --platform linux/amd64,linux/arm64 --build-arg BASE_FQDN=yourdomain.com -t coollabs-cdn .
```
3. Run the container:
```bash
docker run -p 8080:80 coollabs-cdn # (built with --build-arg BASE_FQDN=yourdomain.com)
```
4. Test the implementation:
```bash
# Run tests
./test.sh 8080
# Or test manually:
# Health check
curl http://localhost:8080/health
# Access any JSON file (including nested paths)
curl http://localhost:8080/example.json
curl http://localhost:8080/config.json
curl http://localhost:8080/api/v1/routes.json
curl http://localhost:8080/status/health.json
# ETag caching works for all files
curl -i http://localhost:8080/example.json
curl -i -H 'If-None-Match: "ETAG_VALUE"' http://localhost:8080/example.json
```
## Configuration
### BASE_FQDN Environment Variable
The `BASE_FQDN` environment variable controls the domain used for redirects (root path and 404 errors). It defaults to `coollabs.io` if not set.
**Options:**
- **Environment Variable**: Set `BASE_FQDN=yourdomain.com` when running the container
- **Build Argument**: Set `--build-arg BASE_FQDN=yourdomain.com` when building the image
- **Default**: `coollabs.io`
**Examples:**
```bash
# Runtime configuration
docker run -e BASE_FQDN=mysite.com -p 8080:80 coollabs-cdn
# Build-time configuration
docker build --build-arg BASE_FQDN=mysite.com -t coollabs-cdn .
docker run -p 8080:80 coollabs-cdn # Uses mysite.com for redirects
```
## Health Check
The service provides a health check endpoint:
```bash
curl http://localhost:8080/health
```
## Testing
Run the test suite to verify all functionality:
```bash
# Test against running container on port 8080
./test.sh
# Test against different port/host
./test.sh 8081 myhost.com
```
The test script verifies:
- Health endpoint functionality
- JSON file serving with proper headers
- ETag caching (304 responses)
- CORS headers and preflight requests
- Root and 404 redirects
- Content-Type headers
- Cache-Control headers
+3
View File
@@ -0,0 +1,3 @@
module coollabs-cdn
go 1.21
+19
View File
@@ -0,0 +1,19 @@
package main
import (
"net/http"
"os"
)
func main() {
resp, err := http.Get("http://localhost:80/health")
if err != nil {
os.Exit(1)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
os.Exit(1)
}
os.Exit(0)
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

+6
View File
@@ -0,0 +1,6 @@
{
"community": 20000,
"mrr": 15000,
"gh-sponsors": 4500,
"open-collective": 1200
}
+29
View File
@@ -0,0 +1,29 @@
{
"coolify": {
"v4": {
"version": "4.0.0-beta.460"
},
"nightly": {
"version": "4.0.0-beta.461"
},
"helper": {
"version": "1.0.12"
},
"realtime": {
"version": "1.0.10"
},
"sentinel": {
"version": "0.0.18"
}
},
"traefik": {
"v3.6": "3.6.5",
"v3.5": "3.5.6",
"v3.4": "3.4.5",
"v3.3": "3.3.7",
"v3.2": "3.2.5",
"v3.1": "3.1.7",
"v3.0": "3.0.4",
"v2.11": "2.11.32"
}
}
+259
View File
@@ -0,0 +1,259 @@
package main
import (
"bytes"
"crypto/md5"
"embed"
"fmt"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"time"
)
//go:embed json/*
var jsonFiles embed.FS
//go:embed images/*
var imageFiles embed.FS
// loadJSONFiles recursively loads all JSON files from the embedded filesystem
func loadJSONFiles(dir, prefix string, files map[string]*fileData, etags map[string]string) error {
entries, err := jsonFiles.ReadDir(dir)
if err != nil {
return err
}
for _, entry := range entries {
fullPath := dir + "/" + entry.Name()
if entry.IsDir() {
// Recursively process subdirectories
newPrefix := prefix + "/" + entry.Name()
if err := loadJSONFiles(fullPath, newPrefix, files, etags); err != nil {
return err
}
} else if strings.HasSuffix(entry.Name(), ".json") {
// Load JSON file
content, err := jsonFiles.ReadFile(fullPath)
if err != nil {
log.Printf("Failed to read embedded file %s: %v", fullPath, err)
continue
}
// Create URL path (remove "json" prefix and add leading slash)
urlPath := prefix + "/" + entry.Name()
files[urlPath] = &fileData{
content: content,
modTime: time.Now(), // Use build time as mod time
}
// Calculate ETag
hash := md5.Sum(content)
etags[urlPath] = fmt.Sprintf("\"%x\"", hash)
}
}
return nil
}
// loadImageFiles recursively loads all image files from the embedded filesystem
func loadImageFiles(dir, prefix string, files map[string]*fileData, etags map[string]string) error {
entries, err := imageFiles.ReadDir(dir)
if err != nil {
return err
}
for _, entry := range entries {
fullPath := dir + "/" + entry.Name()
if entry.IsDir() {
// Recursively process subdirectories
newPrefix := prefix + "/" + entry.Name()
if err := loadImageFiles(fullPath, newPrefix, files, etags); err != nil {
return err
}
} else if isImageFile(entry.Name()) {
// Load image file
content, err := imageFiles.ReadFile(fullPath)
if err != nil {
log.Printf("Failed to read embedded file %s: %v", fullPath, err)
continue
}
// Create URL path (remove "images" prefix and add leading slash)
urlPath := prefix + "/" + entry.Name()
files[urlPath] = &fileData{
content: content,
modTime: time.Now(), // Use build time as mod time
}
// Calculate ETag
hash := md5.Sum(content)
etags[urlPath] = fmt.Sprintf("\"%x\"", hash)
}
}
return nil
}
// isImageFile checks if a file has a supported image extension
func isImageFile(filename string) bool {
extensions := []string{".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".bmp", ".ico"}
for _, ext := range extensions {
if strings.HasSuffix(strings.ToLower(filename), ext) {
return true
}
}
return false
}
// getContentType returns the appropriate Content-Type header for a file path
func getContentType(path string) string {
lowerPath := strings.ToLower(path)
switch {
case strings.HasSuffix(lowerPath, ".json"):
return "application/json"
case strings.HasSuffix(lowerPath, ".png"):
return "image/png"
case strings.HasSuffix(lowerPath, ".jpg"), strings.HasSuffix(lowerPath, ".jpeg"):
return "image/jpeg"
case strings.HasSuffix(lowerPath, ".gif"):
return "image/gif"
case strings.HasSuffix(lowerPath, ".webp"):
return "image/webp"
case strings.HasSuffix(lowerPath, ".svg"):
return "image/svg+xml"
case strings.HasSuffix(lowerPath, ".bmp"):
return "image/bmp"
case strings.HasSuffix(lowerPath, ".ico"):
return "image/x-icon"
default:
return ""
}
}
func main() {
// Read base FQDN from environment variable, default to coollabs.io
baseFQDN := os.Getenv("BASE_FQDN")
if baseFQDN == "" {
baseFQDN = "coollabs.io"
}
// Create a map of embedded files with metadata
files := make(map[string]*fileData)
etags := make(map[string]string)
// Recursively load all JSON files from embedded json directory
err := loadJSONFiles("json", "", files, etags)
if err != nil {
log.Fatal("Failed to load JSON files:", err)
}
// Recursively load all image files from embedded images directory
err = loadImageFiles("images", "", files, etags)
if err != nil {
log.Fatal("Failed to load image files:", err)
}
log.Printf("Loaded %d files total: %v", len(files), getFileList(files))
log.Printf("Base FQDN for redirects: %s", baseFQDN)
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
handleRequest(w, r, baseFQDN, files, etags)
})
log.Println("Starting server on :80")
log.Fatal(http.ListenAndServe(":80", nil))
}
func handleRequest(w http.ResponseWriter, r *http.Request, baseFQDN string, files map[string]*fileData, etags map[string]string) {
// Set CORS headers
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept")
// Handle OPTIONS requests for CORS preflight
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusNoContent)
return
}
// Handle root path redirect
if r.URL.Path == "/" {
http.Redirect(w, r, "https://"+baseFQDN, http.StatusFound)
return
}
// Handle health check
if r.URL.Path == "/health" {
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusOK)
w.Write([]byte("healthy\n"))
return
}
// Backward compatibility: serve files at old /coolify/ paths
switch r.URL.Path {
case "/coolify/versions.json":
r.URL.Path = "/versions.json"
case "/coolify/upgrade.sh":
r.URL.Path = "/upgrade.sh"
}
// Redirect /coolify/install.sh to cdn.coolify.io
if r.URL.Path == "/coolify/install.sh" {
http.Redirect(w, r, "https://cdn.coolify.io/install.sh", http.StatusMovedPermanently)
return
}
// Check if file exists
fileData, exists := files[r.URL.Path]
if !exists {
// 404 redirect to base FQDN (without path)
http.Redirect(w, r, "https://"+baseFQDN, http.StatusFound)
return
}
// Set content type based on file extension
contentType := getContentType(r.URL.Path)
if contentType != "" {
w.Header().Set("Content-Type", contentType)
}
w.Header().Set("Cache-Control", "public, must-revalidate, max-age=600")
// Handle ETag caching manually for embedded files
etag := etags[r.URL.Path]
w.Header().Set("ETag", etag)
// Check If-None-Match header
if match := r.Header.Get("If-None-Match"); match == etag {
w.WriteHeader(http.StatusNotModified)
// Include ETag in 304 response as per HTTP spec
w.Header().Set("ETag", etag)
return
}
// Use http.ServeContent for range request support and Last-Modified handling
reader := bytes.NewReader(fileData.content)
http.ServeContent(w, r, filepath.Base(r.URL.Path), fileData.modTime, reader)
}
type fileData struct {
content []byte
modTime time.Time
}
func getFileList(files map[string]*fileData) []string {
var names []string
for path := range files {
names = append(names, path)
}
return names
}
Executable
+255
View File
@@ -0,0 +1,255 @@
#!/bin/bash
# Test script for coolLabs CDN
# Usage: ./test.sh [port] [host]
# Default: port=8080, host=localhost
PORT=${1:-8080}
HOST=${2:-localhost}
BASE_URL="http://$HOST:$PORT"
echo "🧪 Testing coolLabs CDN"
echo "📍 Target: $BASE_URL"
echo "────────────────────────────────────────"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
PASSED=0
FAILED=0
test_result() {
local test_name="$1"
local result="$2"
local expected="$3"
local actual="$4"
if [ "$result" = "PASS" ]; then
echo -e "${GREEN}$test_name${NC}"
((PASSED++))
else
echo -e "${RED}$test_name${NC}"
echo -e " Expected: $expected"
echo -e " Actual: $actual"
((FAILED++))
fi
}
# Test 1: Health endpoint
echo -e "\n🏥 Testing Health Endpoint"
response=$(curl -s -w "HTTPSTATUS:%{http_code}" "$BASE_URL/health")
http_code=$(echo "$response" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://')
body=$(echo "$response" | sed -e 's/HTTPSTATUS:.*//g')
if [ "$http_code" = "200" ] && [ "$body" = "healthy" ]; then
test_result "Health endpoint returns 200 and 'healthy'" "PASS"
else
test_result "Health endpoint returns 200 and 'healthy'" "FAIL" "200, healthy" "$http_code, $body"
fi
# Test 2: Health endpoint headers
echo -e "\n📋 Testing Health Endpoint Headers"
response=$(curl -s -I "$BASE_URL/health")
content_type=$(echo "$response" | grep -i "content-type" | tr -d '\r')
cors_origin=$(echo "$response" | grep -i "access-control-allow-origin" | tr -d '\r')
if echo "$content_type" | grep -q "text/plain"; then
test_result "Health endpoint has correct Content-Type" "PASS"
else
test_result "Health endpoint has correct Content-Type" "FAIL" "text/plain" "$content_type"
fi
if [ "$cors_origin" = "Access-Control-Allow-Origin: *" ]; then
test_result "Health endpoint has CORS headers" "PASS"
else
test_result "Health endpoint has CORS headers" "FAIL" "Access-Control-Allow-Origin: *" "$cors_origin"
fi
# Test 3: JSON endpoint
echo -e "\n📄 Testing JSON Endpoint"
response=$(curl -s -w "HTTPSTATUS:%{http_code}" "$BASE_URL/releases.json")
http_code=$(echo "$response" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://')
body=$(echo "$response" | sed -e 's/HTTPSTATUS:.*//g' | tr -d '\r\n')
if [ "$http_code" = "200" ] && echo "$body" | grep -q '"url"'; then
test_result "JSON endpoint returns 200 and valid JSON" "PASS"
else
test_result "JSON endpoint returns 200 and valid JSON" "FAIL" "200, valid JSON" "$http_code, $body"
fi
# Test 4: JSON endpoint headers
echo -e "\n🏷️ Testing JSON Endpoint Headers"
response=$(curl -s -I "$BASE_URL/releases.json")
content_type=$(echo "$response" | grep -i "content-type" | tr -d '\r')
etag=$(echo "$response" | grep -i "etag" | tr -d '\r')
cache_control=$(echo "$response" | grep -i "cache-control" | tr -d '\r')
cors_origin=$(echo "$response" | grep -i "access-control-allow-origin" | tr -d '\r')
if echo "$content_type" | grep -q "application/json"; then
test_result "JSON endpoint has correct Content-Type" "PASS"
else
test_result "JSON endpoint has correct Content-Type" "FAIL" "application/json" "$content_type"
fi
if echo "$etag" | grep -i -q "etag:"; then
test_result "JSON endpoint has ETag header" "PASS"
else
test_result "JSON endpoint has ETag header" "FAIL" "ETag present" "$etag"
fi
if [ "$cache_control" = "Cache-Control: public, must-revalidate, max-age=600" ]; then
test_result "JSON endpoint has correct Cache-Control" "PASS"
else
test_result "JSON endpoint has correct Cache-Control" "FAIL" "public, must-revalidate, max-age=600" "$cache_control"
fi
if [ "$cors_origin" = "Access-Control-Allow-Origin: *" ]; then
test_result "JSON endpoint has CORS headers" "PASS"
else
test_result "JSON endpoint has CORS headers" "FAIL" "Access-Control-Allow-Origin: *" "$cors_origin"
fi
# Test 5: ETag caching
echo -e "\n🏷️ Testing ETag Caching"
# First get the ETag
response=$(curl -s -I "$BASE_URL/releases.json")
etag=$(echo "$response" | grep -i "etag" | tr -d '\r' | sed 's/ETag: //I')
# Then test with If-None-Match
response=$(curl -s -w "HTTPSTATUS:%{http_code}" -H "If-None-Match: $etag" "$BASE_URL/releases.json")
http_code=$(echo "$response" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://')
if [ "$http_code" = "304" ]; then
test_result "ETag caching returns 304 Not Modified" "PASS"
else
test_result "ETag caching returns 304 Not Modified" "FAIL" "304" "$http_code"
fi
# Test 6: Root redirect
echo -e "\n🏠 Testing Root Redirect"
response=$(curl -s -w "HTTPSTATUS:%{http_code}" -I "$BASE_URL/")
http_code=$(echo "$response" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://')
location=$(echo "$response" | grep -i "location" | tr -d '\r')
if [ "$http_code" = "302" ] && echo "$location" | grep -q "https://"; then
test_result "Root path redirects to configured domain" "PASS"
else
test_result "Root path redirects to configured domain" "FAIL" "302, https://<domain>" "$http_code, $location"
fi
# Test 7: 404 redirect
echo -e "\n❓ Testing 404 Redirect"
response=$(curl -s -w "HTTPSTATUS:%{http_code}" -I "$BASE_URL/nonexistent.json")
http_code=$(echo "$response" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://')
location=$(echo "$response" | grep -i "location" | tr -d '\r')
if [ "$http_code" = "302" ] && echo "$location" | grep -q "https://" && echo "$location" | grep -q "nonexistent.json"; then
test_result "404 redirects to configured domain with path" "PASS"
else
test_result "404 redirects to configured domain with path" "FAIL" "302, https://<domain>/nonexistent.json" "$http_code, $location"
fi
# Test 8: OPTIONS request (CORS preflight)
echo -e "\n🌐 Testing CORS Preflight"
response=$(curl -s -w "HTTPSTATUS:%{http_code}" -X OPTIONS "$BASE_URL/releases.json")
http_code=$(echo "$response" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://')
if [ "$http_code" = "204" ]; then
test_result "OPTIONS request returns 204 No Content" "PASS"
else
test_result "OPTIONS request returns 204 No Content" "FAIL" "204" "$http_code"
fi
# Test 9: HEAD request
echo -e "\n📋 Testing HEAD Request"
response=$(curl -s -w "HTTPSTATUS:%{http_code}" -I "$BASE_URL/releases.json")
http_code=$(echo "$response" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://')
if [ "$http_code" = "200" ]; then
test_result "HEAD request returns 200 OK" "PASS"
else
test_result "HEAD request returns 200 OK" "FAIL" "200" "$http_code"
fi
# Test 10: PNG image endpoint
echo -e "\n🖼️ Testing PNG Image Endpoint"
http_code=$(curl -s -w "%{http_code}" -o /dev/null "$BASE_URL/cl-logo.png")
content_length=$(curl -s -I "$BASE_URL/cl-logo.png" | grep -i "content-length" | tr -d '\r' | sed 's/.*: //')
if [ "$http_code" = "200" ] && [ -n "$content_length" ] && [ "$content_length" -gt 0 ]; then
test_result "PNG image endpoint returns 200 and content" "PASS"
else
test_result "PNG image endpoint returns 200 and content" "FAIL" "200, content" "$http_code, length=$content_length"
fi
# Test 11: PNG image headers
echo -e "\n🏷️ Testing PNG Image Headers"
response=$(curl -s -I "$BASE_URL/cl-logo.png")
content_type=$(echo "$response" | grep -i "content-type" | tr -d '\r')
etag=$(echo "$response" | grep -i "etag" | tr -d '\r')
cache_control=$(echo "$response" | grep -i "cache-control" | tr -d '\r')
cors_origin=$(echo "$response" | grep -i "access-control-allow-origin" | tr -d '\r')
if echo "$content_type" | grep -q "image/png"; then
test_result "PNG image has correct Content-Type" "PASS"
else
test_result "PNG image has correct Content-Type" "FAIL" "image/png" "$content_type"
fi
if echo "$etag" | grep -i -q "etag:"; then
test_result "PNG image has ETag header" "PASS"
else
test_result "PNG image has ETag header" "FAIL" "ETag present" "$etag"
fi
if echo "$cache_control" | grep -q "public"; then
test_result "PNG image has Cache-Control header" "PASS"
else
test_result "PNG image has Cache-Control header" "FAIL" "Cache-Control present" "$cache_control"
fi
if [ "$cors_origin" = "Access-Control-Allow-Origin: *" ]; then
test_result "PNG image has CORS headers" "PASS"
else
test_result "PNG image has CORS headers" "FAIL" "Access-Control-Allow-Origin: *" "$cors_origin"
fi
# Test 12: WEBP image endpoint
echo -e "\n🖼️ Testing WEBP Image Endpoint"
http_code=$(curl -s -w "%{http_code}" -o /dev/null "$BASE_URL/discord-support-search1.webp")
content_length=$(curl -s -I "$BASE_URL/discord-support-search1.webp" | grep -i "content-length" | tr -d '\r' | sed 's/.*: //')
if [ "$http_code" = "200" ] && [ -n "$content_length" ] && [ "$content_length" -gt 0 ]; then
test_result "WEBP image endpoint returns 200 and content" "PASS"
else
test_result "WEBP image endpoint returns 200 and content" "FAIL" "200, content" "$http_code, length=$content_length"
fi
# Test 13: WEBP image headers
echo -e "\n🏷️ Testing WEBP Image Headers"
response=$(curl -s -I "$BASE_URL/discord-support-search1.webp")
content_type=$(echo "$response" | grep -i "content-type" | tr -d '\r')
if echo "$content_type" | grep -q "image/webp"; then
test_result "WEBP image has correct Content-Type" "PASS"
else
test_result "WEBP image has correct Content-Type" "FAIL" "image/webp" "$content_type"
fi
# Summary
echo -e "\n────────────────────────────────────────"
echo -e "📊 Test Summary:"
echo -e " ${GREEN}Passed: $PASSED${NC}"
echo -e " ${RED}Failed: $FAILED${NC}"
echo -e " Total: $((PASSED + FAILED))"
if [ $FAILED -eq 0 ]; then
echo -e "\n${GREEN}🎉 All tests passed! The CDN is working correctly.${NC}"
exit 0
else
echo -e "\n${RED}⚠️ Some tests failed. Please check the implementation.${NC}"
exit 1
fi