darcsweb - README.md

summary shortlog log tree tags
[root] / README.md
# DarcsWeb

A web interface for browsing [darcs](https://darcs.net) repositories, inspired
by gitweb. Built in Haskell using the darcs library API directly (no CLI
invocation) and the [Scotty](https://github.com/scotty-web/scotty) web
framework. Pure server-side HTML and CSS, no client-side JavaScript.

## Building

DarcsWeb uses [Stack](https://docs.haskellstack.org/) as its build system.

### Prerequisites

Install Stack if you don't have it:

```
curl -sSL https://get.haskellstack.org/ | sh
```

Or via your package manager. A recent version of Stack is recommended (>= 2.9).
If you have an older version, upgrade with:

```
stack upgrade
```

Stack will automatically download and manage the correct GHC version.

You also need [Rocq](https://rocq-prover.org/) (formerly Coq) installed and
available as `rocq` on your `PATH`. The project uses Rocq to compile formally
verified pure functions that are extracted to Haskell during the build. Rocq can
be installed via [opam](https://opam.ocaml.org/):

```
opam install rocq-core rocq-stdlib
```

### Build

```
stack build
```

On the first run this downloads GHC and all dependencies, which takes a while.
Subsequent builds are fast.

The build automatically compiles the Rocq sources in `verified/` and extracts
Haskell modules into `gen/` before compiling the Haskell code (via a custom
`Setup.hs` pre-build hook).

### Test

```
stack test
```

This runs two phases:

1. **Rocq proof verification** -- checks all formal proofs in
   `verified/*_proofs.v`.
2. **QuickCheck property tests** -- runs property-based tests for the pure
   Haskell functions that are not formally verified.

### Install

To install the `darcsweb` binary into `~/.local/bin`:

```
stack install
```

## Configuration

DarcsWeb is configured through a plain-text configuration file. The `-c` flag
pointing to this file is required to start the application.

Copy the example and adjust it:

```
cp darcsweb.conf.example /etc/darcsweb.conf
```

### Configuration keys

| Key      | Description                                       | Default       |
|----------|---------------------------------------------------|---------------|
| `bind`   | IP address to bind to                             | `127.0.0.1`   |
| `port`   | TCP port to listen on                             | `3000`        |
| `repos`  | Directory containing darcs repositories           | `.`           |
| `title`  | Site title shown in the page header               | `DarcsWeb`    |
| `static` | Path to the static assets directory               | `static`      |

The file format is one `key = value` pair per line. Blank lines and lines
starting with `#` are ignored.

Example configuration:

```
# /etc/darcsweb.conf
bind = 0.0.0.0
port = 8080
repos = /srv/darcs
title = My Repositories
static = /usr/share/darcsweb/static
```

### Repository descriptions

To show a description for a repository on the index page, create the file
`_darcs/prefs/repo_description` inside the repository:

```
echo "My awesome project" > /srv/darcs/myrepo/_darcs/prefs/repo_description
```

## Running

```
darcsweb -c FILE [-d]
```

| Flag         | Description                              |
|--------------|------------------------------------------|
| `-c FILE`    | Path to the configuration file (required)|
| `-d`         | Run as daemon                            |

If `-c` is omitted, a usage message is printed and the process exits.

### Foreground mode (default)

Without `-d`, DarcsWeb runs in the foreground and logs to stdout:

```
stack run -- -c /etc/darcsweb.conf
```

Or, if installed:

```
darcsweb -c /etc/darcsweb.conf
```

### Daemon mode

With `-d`, DarcsWeb forks into the background, detaches from the terminal, and
logs to syslog (facility `daemon`):

```
darcsweb -c /etc/darcsweb.conf -d
```

Messages appear in the system log under the identifier `darcsweb`, e.g.:

```
journalctl -t darcsweb
```

### Docker

The included `Dockerfile` produces a minimal image with just the `darcsweb`
binary and static assets. The base images (`haskell:9.6-slim`,
`debian:bookworm-slim`) are multi-arch, so this works on both x86-64 and ARM64
hosts such as the Raspberry Pi 5.

Build the image:

```
docker build -t darcsweb .
```

Run it, bind-mounting your host repositories read-only:

```
docker run -d -p 3000:3000 -v /srv/darcs:/srv/darcs:ro darcsweb
```

The `:ro` mount is sufficient -- DarcsWeb only reads `_darcs/` contents. Because
bind mounts reflect the host filesystem in real time, new patches and new
repositories show up immediately without restarting the container.

To override the built-in configuration, mount your own file:

```
docker run -d -p 8080:8080 \
  -v /srv/darcs:/srv/darcs:ro \
  -v /etc/darcsweb.conf:/etc/darcsweb.conf:ro \
  darcsweb
```

Multiple repository directories can be served by mounting them under a common
prefix:

```
docker run -d -p 3000:3000 \
  -v /home/user/repos:/srv/darcs/personal:ro \
  -v /srv/shared:/srv/darcs/shared:ro \
  darcsweb
```

#### Cross-building for Raspberry Pi 5

If you prefer to build the image on a faster x86-64 machine and deploy to
a Pi 5:

```
docker buildx build --platform linux/arm64 -t darcsweb:arm64 --load .
docker save darcsweb:arm64 | ssh pi@raspberrypi docker load
```

This requires Docker Buildx with QEMU emulation (`docker run --privileged
--rm tonistiigi/binfmt --install arm64`).

#### TLS with Let's Encrypt

The included `docker-compose.yml` and `nginx.conf` add an nginx reverse proxy
with automatic Let's Encrypt certificates via certbot.

1. Replace `darcs.example.com` with your domain in both `nginx.conf` and the
   certbot command below.
2. If your repositories are not in `/srv/darcs`, update the volume mount in
   `docker-compose.yml`.
3. Start nginx and darcsweb (certbot needs nginx running for the ACME
   challenge):

```
docker compose up -d nginx darcsweb
```

4. Obtain the initial certificate:

```
docker compose run --rm certbot certonly --webroot \
  -w /var/www/certbot -d darcs.example.com
```

5. Reload nginx to pick up the new certificate, then start the certbot
   renewal sidecar:

```
docker compose exec nginx nginx -s reload
docker compose up -d certbot
```

The certbot container renews certificates automatically every 12 hours (a
no-op when they are not yet due). After renewal nginx must be reloaded; a
cron entry on the host handles this:

```
0 */12 * * * docker compose exec nginx nginx -s reload
```

Certificate data is stored in the `certbot_conf` volume and persists across
restarts.

## Pages

| Route                          | Description                                  |
|--------------------------------|----------------------------------------------|
| `/`                            | Repository index -- lists all repositories   |
| `/repo/:name/summary`         | Repository overview with recent patches and tags |
| `/repo/:name/shortlog`        | Compact patch log (date, author, name)       |
| `/repo/:name/log`             | Full patch log with descriptions and summaries |
| `/repo/:name/tags`            | All tagged patches                           |
| `/repo/:name/patch/:hash`     | Single patch detail with full diff           |

## Formal Verification

Security-critical pure functions are implemented in Gallina (the Rocq/Coq
specification language), formally proven correct, and extracted to Haskell for
use at runtime. This follows the same approach as
[hopify](https://github.com/...) -- identify pure functions, reimplement them in
Gallina, and export them to the target language.

### Verified functions

| Function | Source | Replaces |
|---|---|---|
| `esc` / `esc_char` | `verified/html_pure.v` | HTML entity escaping in `DarcsWeb.Html` |
| `is_safe_sub_path` / `split_path` | `verified/path_pure.v` | Path traversal validation in `DarcsWeb.Darcs` |
| `add_trailing_slash` | `verified/path_pure.v` | Jail-check path normalisation in `Main` |

### Proven properties

The proof files (`verified/*_proofs.v`) establish:

- **Concrete correctness**: each dangerous character (`<`, `>`, `&`, `"`, `'`)
  maps to the correct HTML entity.
- **Security theorems**: the output of `esc` contains no raw `<`, `>`, or `"`
  characters -- proven by exhaustive case analysis over all 256 ASCII values
  combined with structural induction over the input string.
- **Path safety**: `is_safe_sub_path` rejects `..`, `_darcs`, `.git`, hidden
  files (names starting with `.`), and empty paths; accepts legitimate nested
  paths.
- **Idempotence**: `add_trailing_slash` applied twice equals applying it once.

### Property-based tests

Functions that remain in Haskell (because they depend on types or libraries not
easily represented in Gallina) are tested with QuickCheck:

| Function | Properties |
|---|---|
| `highlightDiff` | Correct CSS class wrapping for `+`/`-`/`@@` prefixed lines |
| `shortAuthor` | Email stripping, plain name preservation |
| `formatSize` | Correct unit suffix (`B`/`KiB`/`MiB`) for each size range |
| `formatAbsolute` | Short strings unchanged, long date strings gain separators |

### Project layout

```
verified/           Rocq/Coq sources and Makefile
  html_pure.v         HTML escaping implementation + extraction
  path_pure.v         Path validation implementation + extraction
  html_pure_proofs.v  Proofs for HTML escaping
  path_pure_proofs.v  Proofs for path validation
  Makefile            Build rules for extraction and proof checking
gen/                Auto-generated Haskell (do not edit)
  HtmlPure.hs         Extracted from html_pure.v
  PathPure.hs         Extracted from path_pure.v
Setup.hs            Custom build hooks (runs Rocq extraction before build)
test/
  Spec.hs             Test runner (proofs + QuickCheck)
  Properties/Html.hs  QuickCheck property definitions
```

## License

GPL-2.0-or-later (same as darcs).