Directory Browse #
Directory browsing memungkinkan pengunjung melihat daftar isi direktori ketika mereka mengakses path yang tidak memiliki index file. Secara default, Caddy mengembalikan 404 untuk direktori tanpa index — ini adalah perilaku yang aman. Untuk mengaktifkan directory listing, kamu perlu secara eksplisit menambahkan opsi browse di file_server.
Fitur ini berguna untuk file sharing server internal, repository download, atau dokumentasi yang disusun dalam folder tanpa halaman index.
Mengaktifkan Directory Browse #
files.example.com {
root * /var/www/files
file_server {
browse # Aktifkan directory listing
}
}
Dengan konfigurasi ini, mengakses https://files.example.com/ akan menampilkan daftar semua file dan subdirektori di /var/www/files/, lengkap dengan nama file, ukuran, dan tanggal modifikasi.
Tampilan Default Browse #
Caddy menyediakan template HTML bawaan yang cukup bersih dan fungsional:
Directory listing tampilan default:
┌─────────────────────────────────────────────────────┐
│ /downloads/ │
├─────────────────────────────────────────────────────┤
│ 📁 documents/ - 2024-01-15 10:00 │
│ 📁 images/ - 2024-01-14 09:30 │
│ 📄 README.md 1.2 KB 2024-01-13 14:25 │
│ 📄 manual.pdf 4.5 MB 2024-01-12 16:40 │
│ 📄 software.zip 23.1 MB 2024-01-11 11:15 │
└─────────────────────────────────────────────────────┘
Template default menyertakan:
- Nama file/direktori (dengan link)
- Ukuran file (untuk file, bukan direktori)
- Waktu modifikasi terakhir
- Link kembali ke parent directory
- Sorting berdasarkan nama (default)
Template Kustom #
Caddy mendukung template HTML kustom untuk mengubah tampilan directory listing sesuai kebutuhanmu. Template menggunakan format Go templates (text/template):
files.example.com {
root * /var/www/files
file_server {
browse /etc/caddy/browse-template.html
}
}
Template Sederhana #
<!-- /etc/caddy/browse-template.html -->
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{.Name}} — File Browser</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; background: #f5f5f5; color: #333; }
.container { max-width: 960px; margin: 2rem auto; padding: 0 1rem; }
h1 { font-size: 1.4rem; margin-bottom: 1rem; color: #1a1a1a; }
.breadcrumb { font-size: 0.9rem; margin-bottom: 1.5rem; }
.breadcrumb a { color: #0066cc; text-decoration: none; }
.breadcrumb a:hover { text-decoration: underline; }
table { width: 100%; background: white; border-radius: 8px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1); border-collapse: collapse; }
thead th { padding: 0.75rem 1rem; text-align: left; font-weight: 600;
border-bottom: 2px solid #e0e0e0; background: #fafafa; }
tbody td { padding: 0.6rem 1rem; border-bottom: 1px solid #f0f0f0; }
tbody tr:last-child td { border-bottom: none; }
tbody tr:hover { background: #f8f9ff; }
a { color: #0066cc; text-decoration: none; }
a:hover { text-decoration: underline; }
.dir { color: #e67e00; }
.size, .time { color: #666; font-size: 0.9rem; white-space: nowrap; }
.icon { margin-right: 0.4rem; }
</style>
</head>
<body>
<div class="container">
<h1>📁 {{.Name}}</h1>
<!-- Breadcrumb navigation -->
<div class="breadcrumb">
{{range .Breadcrumbs}}
<a href="{{.Link}}">{{.Text}}</a> /
{{end}}
</div>
<table>
<thead>
<tr>
<th>Nama</th>
<th>Ukuran</th>
<th>Terakhir Diubah</th>
</tr>
</thead>
<tbody>
<!-- Parent directory link -->
{{if ne .Path "/"}}
<tr>
<td><a href="../">⬆ Kembali ke folder sebelumnya</a></td>
<td class="size">—</td>
<td class="time">—</td>
</tr>
{{end}}
<!-- Direktori terlebih dahulu -->
{{range .Items}}
{{if .IsDir}}
<tr>
<td>
<span class="icon">📁</span>
<a class="dir" href="{{.URL}}">{{.Name}}/</a>
</td>
<td class="size">—</td>
<td class="time">{{.ModTime.Format "02 Jan 2006, 15:04"}}</td>
</tr>
{{end}}
{{end}}
<!-- File setelah direktori -->
{{range .Items}}
{{if not .IsDir}}
<tr>
<td>
<span class="icon">📄</span>
<a href="{{.URL}}">{{.Name}}</a>
</td>
<td class="size">{{.HumanSize}}</td>
<td class="time">{{.ModTime.Format "02 Jan 2006, 15:04"}}</td>
</tr>
{{end}}
{{end}}
</tbody>
</table>
</div>
</body>
</html>
Variabel yang Tersedia di Template #
// Variabel yang bisa digunakan dalam template browse
.Name string // Nama direktori saat ini
.Path string // Path lengkap
.URL string // URL direktori saat ini
.Items []FileInfo // Daftar file dan subdirektori
// FileInfo fields:
.Name string // Nama file/direktori
.Size int64 // Ukuran dalam bytes
.HumanSize string // Ukuran readable (misalnya "4.5 MB")
.URL string // URL untuk mengakses
.ModTime time.Time // Waktu modifikasi
.IsDir bool // True jika direktori
.Breadcrumbs []Crumb // Untuk navigasi breadcrumb
// Crumb fields:
.Text string // Label
.Link string // URL
Mengamankan Directory Browse #
Directory listing yang tidak dilindungi bisa menjadi risiko keamanan — siapa saja bisa melihat struktur file kamu. Selalu tambahkan layer keamanan:
Basic Authentication #
files.example.com {
root * /var/www/files
# Lindungi seluruh site dengan password
basicauth {
# Generate hash: caddy hash-password --plaintext "password123"
alice $2a$14$tR3.5pJb7B9vJMrg9BNi/u5mGUBjzFz8A.NlSFKxCN3h1R0LbYXRa
bob $2a$14$kQ7.8pLc9C0wMnsh0COj/v6nHVCkGGz9B.OmTGLyDO4i2S1McZYSb
}
file_server {
browse
hide .git .env
}
}
# Buat bcrypt hash untuk password
caddy hash-password --plaintext "password-kamu"
# Output: $2a$14$...hash...
Batasi Akses ke IP Tertentu #
internal-files.company.com {
root * /var/lib/shared-files
# Hanya izinkan akses dari jaringan internal
@external {
not remote_ip 10.0.0.0/8 192.168.0.0/16 172.16.0.0/12
}
respond @external 403
file_server {
browse
}
}
Gabungan IP Filter + Basic Auth #
secure-files.example.com {
root * /var/www/secure
# Layer 1: Hanya dari jaringan tertentu
@outsider {
not remote_ip 203.0.113.0/24 10.0.0.0/8
}
respond @outsider 403
# Layer 2: Password protection
basicauth {
admin $2a$14$adminHashHere
}
file_server {
browse
hide .git .env *.key *.pem
}
}
Menyembunyikan File dari Listing #
files.example.com {
root * /var/www/files
file_server {
browse
# File yang disembunyikan dari listing DAN tidak bisa diakses
# Glob patterns didukung
hide {
.git
.env
.htaccess
*.bak
*.tmp
*.log
config.php
secrets.json
.DS_Store # macOS metadata file
Thumbs.db # Windows thumbnail cache
}
}
}
Use Case: Internal File Sharing Server #
share.company.internal {
tls internal # Sertifikat dari internal CA Caddy
root * /var/lib/company-files
# Hanya bisa diakses dari jaringan internal
@external not remote_ip 10.0.0.0/8 192.168.0.0/16
respond @external 403
# Basic auth sebagai layer tambahan
basicauth * {
alice $2a$14$aliceHash
bob $2a$14$bobHash
charlie $2a$14$charlieHash
}
# Log akses untuk audit
log {
output file /var/log/caddy/fileshare-access.log
format json
}
file_server {
browse /etc/caddy/browse-corporate.html
hide .git .env *.key
}
}
Menambahkan Search ke Directory Listing #
Template kustom bisa menyertakan fungsionalitas search client-side:
<!-- Tambahkan ke bagian <head> template -->
<script>
function filterFiles(query) {
const rows = document.querySelectorAll('tbody tr.file-row');
const lq = query.toLowerCase();
rows.forEach(row => {
const name = row.dataset.name || '';
row.style.display = name.toLowerCase().includes(lq) ? '' : 'none';
});
}
</script>
<!-- Input search di atas tabel -->
<input type="search"
placeholder="Cari file..."
oninput="filterFiles(this.value)"
style="padding: 0.5rem; width: 100%; margin-bottom: 1rem;
border: 1px solid #ddd; border-radius: 4px;">
<!-- Tambahkan data-name ke setiap baris file -->
{{range .Items}}
<tr class="file-row" data-name="{{.Name}}">
...
</tr>
{{end}}
Browse di Subdirektori Tertentu Saja #
example.com {
root * /var/www/html
# Aktifkan browse hanya untuk /public-files/*
handle /public-files/* {
root * /var/www/public-files
file_server {
browse
}
}
# Semua path lain: static site biasa tanpa browse
handle {
file_server
}
}
Disable Browse untuk Keamanan #
Jika kamu pernah mengaktifkan browse dan ingin menonaktifkannya, cukup hapus opsi tersebut:
# ANTI-PATTERN: Browse aktif di production tanpa proteksi
example.com {
root * /var/www/production
file_server {
browse # ← BERBAHAYA jika tidak dilindungi!
}
}
# BENAR: Browse dinonaktifkan (default) atau dilindungi
example.com {
root * /var/www/production
file_server # Tanpa browse — direktori tanpa index → 404
}
Ringkasan #
- Directory browse dinonaktifkan secara default — aktifkan secara eksplisit dengan opsi
browsedifile_serverhanya jika diperlukan.- Selalu lindungi directory listing dengan basicauth atau IP restriction sebelum deploy ke production — jangan biarkan listing publik tanpa proteksi.
- Template kustom (
browse /path/template.html) menggunakan Go template syntax dengan variabel.Items,.Name,.Breadcrumbs, dan lainnya untuk layout yang lebih baik.- Gunakan
hideuntuk menyembunyikan file sensitif dari listing — file yang di-hide juga tidak bisa diakses via URL langsung.- Untuk file sharing internal, kombinasikan
tls internal+ IP restriction + basicauth untuk keamanan berlapis.- Opsi
browsedi dalam blokhandlememungkinkan kamu mengaktifkan listing hanya untuk subdirektori tertentu tanpa mempengaruhi path lain.