FossilRepo
| c588255… | ragelink | 1 | #!/bin/bash |
| c588255… | ragelink | 2 | # fossil-shell — Forced command for SSH-based Fossil clone/push/pull. |
| c588255… | ragelink | 3 | # |
| c588255… | ragelink | 4 | # Each authorized_keys entry uses: |
| c588255… | ragelink | 5 | # command="/usr/local/bin/fossil-shell <username>",no-port-forwarding,... |
| c588255… | ragelink | 6 | # |
| c588255… | ragelink | 7 | # When a Fossil client connects via SSH, it sends a command like: |
| c588255… | ragelink | 8 | # fossil http /path/to/repo.fossil |
| c588255… | ragelink | 9 | # which arrives in $SSH_ORIGINAL_COMMAND. |
| c588255… | ragelink | 10 | # |
| c588255… | ragelink | 11 | # This script: |
| c588255… | ragelink | 12 | # 1. Extracts the repo name from the SSH command |
| c588255… | ragelink | 13 | # 2. Maps it to the on-disk .fossil file |
| c588255… | ragelink | 14 | # 3. Runs fossil http in CGI mode with --localauth |
| c588255… | ragelink | 15 | # |
| c588255… | ragelink | 16 | # Auth is already handled by the SSH key → user mapping in authorized_keys. |
| c588255… | ragelink | 17 | |
| c588255… | ragelink | 18 | set -euo pipefail |
| c588255… | ragelink | 19 | |
| c588255… | ragelink | 20 | FOSSIL_USER="${1:-anonymous}" |
| c588255… | ragelink | 21 | REPO_DIR="${FOSSIL_DATA_DIR:-/data/repos}" |
| c588255… | ragelink | 22 | |
| c588255… | ragelink | 23 | # Validate SSH_ORIGINAL_COMMAND |
| c588255… | ragelink | 24 | if [ -z "${SSH_ORIGINAL_COMMAND:-}" ]; then |
| c588255… | ragelink | 25 | echo "Error: Interactive SSH sessions are not supported." >&2 |
| c588255… | ragelink | 26 | echo "Use: fossil clone ssh://fossil@<host>/<project-slug> local.fossil" >&2 |
| c588255… | ragelink | 27 | exit 1 |
| c588255… | ragelink | 28 | fi |
| c588255… | ragelink | 29 | |
| c588255… | ragelink | 30 | # Fossil SSH sends: fossil http <repo-path> --args... |
| c588255… | ragelink | 31 | # We only allow "fossil http" commands. |
| c588255… | ragelink | 32 | if ! echo "$SSH_ORIGINAL_COMMAND" | grep -qE '^fossil\s+http\s+'; then |
| c588255… | ragelink | 33 | echo "Error: Only fossil http commands are allowed." >&2 |
| c588255… | ragelink | 34 | exit 1 |
| c588255… | ragelink | 35 | fi |
| c588255… | ragelink | 36 | |
| c588255… | ragelink | 37 | # Extract the repo identifier (second argument after "fossil http") |
| c588255… | ragelink | 38 | REPO_ARG=$(echo "$SSH_ORIGINAL_COMMAND" | awk '{print $3}') |
| c588255… | ragelink | 39 | |
| c588255… | ragelink | 40 | if [ -z "$REPO_ARG" ]; then |
| c588255… | ragelink | 41 | echo "Error: No repository specified." >&2 |
| c588255… | ragelink | 42 | exit 1 |
| c588255… | ragelink | 43 | fi |
| c588255… | ragelink | 44 | |
| c588255… | ragelink | 45 | # Strip any path components — only allow bare slugs or slug.fossil |
| c588255… | ragelink | 46 | REPO_NAME=$(basename "$REPO_ARG" .fossil) |
| c588255… | ragelink | 47 | |
| c588255… | ragelink | 48 | # Sanitize: only allow alphanumeric, hyphens, underscores |
| c588255… | ragelink | 49 | if ! echo "$REPO_NAME" | grep -qE '^[a-zA-Z0-9_-]+$'; then |
| c588255… | ragelink | 50 | echo "Error: Invalid repository name." >&2 |
| c588255… | ragelink | 51 | exit 1 |
| c588255… | ragelink | 52 | fi |
| c588255… | ragelink | 53 | |
| c588255… | ragelink | 54 | REPO_PATH="${REPO_DIR}/${REPO_NAME}.fossil" |
| c588255… | ragelink | 55 | |
| c588255… | ragelink | 56 | if [ ! -f "$REPO_PATH" ]; then |
| c588255… | ragelink | 57 | echo "Error: Repository '${REPO_NAME}' not found." >&2 |
| c588255… | ragelink | 58 | exit 1 |
| c588255… | ragelink | 59 | fi |
| c588255… | ragelink | 60 | |
| c588255… | ragelink | 61 | # Log the access |
| c588255… | ragelink | 62 | logger -t fossil-shell "user=${FOSSIL_USER} repo=${REPO_NAME} action=ssh-sync" |
| c588255… | ragelink | 63 | |
| c588255… | ragelink | 64 | # Run fossil http in CGI mode |
| c588255… | ragelink | 65 | exec fossil http "$REPO_PATH" --localauth |