[
  {
    "path": ".dockerignore",
    "content": "*.yml\n*.yaml\n*.json\n*.md\n\n.git*\nbin\ndist\nbuild\ndocs\nexamples\ntmp\nvendor\n\n.editorconfig\nDockerfile\nCODEOWNERS\nLICENSE\nMakefile\n.env\ncompose.yml\ncompose.override.yml\nREADME.md\n"
  },
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\ncharset = utf-8\nend_of_line = lf\nindent_style = space\nmax_line_length = 120\ninsert_final_newline = true\ntrim_trailing_whitespace = true\n\n[*.{json,yaml,yml}]\nindent_style = space\nindent_size = 2\n\n[*.{sh,bash,envrc}]\nindent_style = space\nindent_size = 4\n\n[*.go]\nindent_style = tab\nindent_size = 4\n\n[{Makefile,makefile,GNUmakefile}]\nindent_style = tab\nindent_size = 4\n\n[*.md]\ntrim_trailing_whitespace = false\n"
  },
  {
    "path": ".gitattributes",
    "content": "* text=auto\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n- package-ecosystem: github-actions\n  directory: \"/\"\n  schedule:\n    interval: weekly\n- package-ecosystem: gomod\n  directory: \"/\"\n  schedule:\n    interval: weekly\n"
  },
  {
    "path": ".github/workflows/codeql.yml",
    "content": "# For most projects, this workflow file will not need changing; you simply need\n# to commit it to your repository.\n#\n# You may wish to alter this file to override the set of languages analyzed,\n# or to provide custom queries or build logic.\n#\n# ******** NOTE ********\n# We have attempted to detect the languages in your repository. Please check\n# the `language` matrix defined below to confirm you have the correct set of\n# supported CodeQL languages.\n#\nname: \"CodeQL\"\n\non:\n  push:\n    branches: [ \"main\" ]\n  pull_request:\n    # The branches below must be a subset of the branches above\n    branches: [ \"main\" ]\n  schedule:\n    - cron: '37 6 * * 0'\n\njobs:\n  analyze:\n    name: Analyze\n    runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}\n    permissions:\n      actions: read\n      contents: read\n      security-events: write\n\n    strategy:\n      fail-fast: false\n      matrix:\n        language: [ 'go' ]\n        # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]\n        # Use only 'java' to analyze code written in Java, Kotlin or both\n        # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both\n        # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support\n\n    steps:\n    - name: Checkout repository\n      uses: actions/checkout@v6\n\n    # Initializes the CodeQL tools for scanning.\n    - name: Initialize CodeQL\n      uses: github/codeql-action/init@v4\n      with:\n        languages: ${{ matrix.language }}\n        # If you wish to specify custom queries, you can do so here or in a config file.\n        # By default, queries listed here will override any specified in a config file.\n        # Prefix the list here with \"+\" to use these queries and those in the config file.\n\n        # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs\n        # queries: security-extended,security-and-quality\n\n\n    # Autobuild attempts to build any compiled languages  (C/C++, C#, Go, or Java).\n    # If this step fails, then you should remove it and run the build manually (see below)\n    - name: Autobuild\n      uses: github/codeql-action/autobuild@v4\n\n    # ℹ️ Command-line programs to run using the OS shell.\n    # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun\n\n    #   If the Autobuild fails above, remove it and uncomment the following three lines.\n    #   modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.\n\n    # - run: |\n    #     echo \"Run, Build Application using script\"\n    #     ./location_of_script_within_repo/buildscript.sh\n\n    - name: Perform CodeQL Analysis\n      uses: github/codeql-action/analyze@v4\n      with:\n        category: \"/language:${{matrix.language}}\"\n"
  },
  {
    "path": ".github/workflows/dependabot.yml",
    "content": "name: Dependabot Approve & Auto-Merge\non:\n  pull_request:\n    branches: [ main ]\n\npermissions:\n  contents: write\n  pull-requests: write\n\njobs:\n\n  dependabot:\n    runs-on: ubuntu-latest\n    if: ${{ github.actor == 'dependabot[bot]' }}\n    steps:\n      -\n        name: Dependabot metadata\n        id: metadata\n        uses: dependabot/fetch-metadata@v3\n        with:\n          github-token: \"${{ secrets.GITHUB_TOKEN }}\"\n      -\n        name: Output Metadata\n        run: |\n          echo \"${{ steps.metadata.outputs.dependency-names }}\"\n          echo \"${{ steps.metadata.outputs.dependency-type }}\"\n          echo \"${{ steps.metadata.outputs.update-type }}\"\n      -\n        name: Approve a PR\n        run: gh pr review --approve \"$PR_URL\"\n        env:\n          PR_URL: ${{ github.event.pull_request.html_url }}\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n      -\n        name: Enable auto-merge for Dependabot PRs\n        # if: ${{ steps.metadata.outputs.update-type == 'version-update:semver-patch' }}\n        run: gh pr merge --auto --merge --delete-branch \"$PR_URL\"\n        env:\n          PR_URL: ${{ github.event.pull_request.html_url }}\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/docker.yml",
    "content": "name: Docker\n\non:\n  workflow_call:\n    inputs:\n      push:\n        description: \"Push the image to the registry\"\n        type: boolean\n        default: false\n        required: false\n    outputs:\n      image:\n        description: \"Output Image\"\n        value: ${{ github.repository }}:sha-${{ jobs.build.outputs.version }}\n\njobs:\n\n  build:\n    runs-on: ubuntu-latest\n    outputs:\n      version: ${{ steps.vars.outputs.sha_short }}\n    steps:\n      -\n        name: Checkout\n        uses: actions/checkout@v6\n      - name: Set output vars\n        id: vars\n        run: echo \"::set-output name=sha_short::$(git rev-parse --short HEAD)\"\n      -\n        name: Docker meta\n        id: meta\n        uses: docker/metadata-action@v5\n        with:\n          images: ${{ github.repository }}\n          tags: |\n            type=schedule\n            type=raw,value=latest,enable={{is_default_branch}}\n            type=ref,event=branch\n            type=ref,event=pr\n            type=semver,pattern={{version}}\n            type=semver,pattern={{major}}.{{minor}}\n            type=semver,pattern={{major}}\n            type=sha\n      -\n        name: Set up QEMU\n        uses: docker/setup-qemu-action@v4\n      -\n        name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v4\n      -\n        name: Login to DockerHub\n        uses: docker/login-action@v4\n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n      -\n        name: Build and push\n        uses: docker/build-push-action@v7\n        with:\n          context: .\n          push: ${{ inputs.push }}\n          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}\n          cache-from: type=registry,ref=${{ github.repository }}:buildcache\n          cache-to: type=registry,ref=${{ github.repository }}:buildcache,mode=max\n      -\n        if: ${{ github.event_name != 'pull_request' }}\n        name: Update Docker Hub Description\n        uses: peter-evans/dockerhub-description@v5\n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n          repository: ${{ github.repository }}\n"
  },
  {
    "path": ".github/workflows/go.yml",
    "content": "name: Go\n\non:\n  push:\n    branches: [ main ]\n  pull_request:\n    branches: [ main ]\n\njobs:\n\n  test:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n      -\n        name: Set up Go\n        uses: actions/setup-go@v6\n        with:\n          cache: true\n      -\n        name: Build\n        run: go build -v ./...\n      -\n        name: Test\n        run: go test -race -coverprofile=coverage.out -covermode=atomic ./...\n      -\n        name: Upload coverage reports to Codecov\n        uses: codecov/codecov-action@v6\n"
  },
  {
    "path": ".github/workflows/golangci-lint.yml",
    "content": "name: golangci-lint\non:\n  push:\n    tags:\n      - v*\n    branches:\n      - main\n  pull_request:\n\npermissions:\n  contents: read\n  # Optional: allow read access to pull request. Use with `only-new-issues` option.\n  # pull-requests: read\njobs:\n\n  golangci:\n    name: lint\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/setup-go@v6\n        with:\n          cache: false\n      - uses: actions/checkout@v6\n      - name: golangci-lint\n        uses: golangci/golangci-lint-action@v6\n        with:\n          # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version\n          version: latest\n\n          # Optional: working directory, useful for monorepos\n          # working-directory: somedir\n\n          # Optional: golangci-lint command line arguments.\n          # args: --issues-exit-code=0\n\n          # Optional: show only new issues if it's a pull request. The default value is `false`.\n          # only-new-issues: true\n\n          # Optional: if set to true then the all caching functionality will be complete disabled,\n          #           takes precedence over all other caching options.\n          # skip-cache: true\n\n          # Optional: if set to true then the action don't cache or restore ~/go/pkg.\n          # skip-pkg-cache: true\n\n          # Optional: if set to true then the action don't cache or restore ~/.cache/go-build.\n          # skip-build-cache: true\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: cd\n\non:\n  push:\n    tags:\n      - v*\n  workflow_dispatch:\n\njobs:\n\n  docker:\n    uses: ./.github/workflows/docker.yml\n    with:\n      push: true\n    secrets: inherit\n"
  },
  {
    "path": ".gitignore",
    "content": "# Binaries for programs and plugins\n*.exe\n*.dll\n*.so\n*.dylib\n\n# Test binary, build with `go test -c`\n*.test\n\n# SQLite Databases\n*.db\n\n# Log files\n*.log\n\n# Output of the go coverage tool, specifically when used with LiteIDE\ncoverage*\n*.out\n\n# Dependency directories\nvendor/\n_vendor-*/\n\n# Build artifacts\nbin/\ndist/\n\n# Local configurations\n.env*\ncompose.override.yml\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing\n\n[semver]: http://semver.org/\n\nWhen contributing to this repository, please first discuss the change you wish\nto make via issue, email, or any other method with the owners of this repository\nbefore making a change.\n\nPlease note we have a code of conduct, please follow it in all your interactions\nwith the project.\n\n## Pull Request Process\n\n1. Ensure any install or build dependencies are removed before the end of the\n   layer when performing a build.\n2. Update the `README.md` or `docs` with details of change to the project, this\n   includes new flags, environment variables, exposed ports, useful file\n   locations and container parameters.\n3. Specify how your change should affect our versioning scheme when merged. For\n   more information on how we implement versioning, check out the [semver][]\n   documentation. PRs will be grouped into logical version groups so that we\n   aren't incrementing the version on every merge.\n4. You may merge the Pull Request in once you have the sign-off of other\n   developers, or if you do not have permission to do that, you may request a\n   reviewer to merge it for you.\n\n## Code of Conduct\n\n### Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as\ncontributors and maintainers pledge to making participation in our project and\nour community a harassment-free experience for everyone, regardless of age, body\nsize, disability, ethnicity, gender identity and expression, level of experience,\nnationality, personal appearance, race, religion, or sexual identity and\norientation.\n\n### Our Standards\n\nExamples of behavior that contributes to creating a positive environment\ninclude:\n\n* Using welcoming and inclusive language\n* Being respectful of differing viewpoints and experiences\n* Gracefully accepting constructive criticism\n* Focusing on what is best for the community\n* Showing empathy towards other community members\n\nExamples of unacceptable behavior by participants include:\n\n* The use of sexualized language or imagery and unwelcome sexual attention or\n  advances\n* Trolling, insulting/derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or electronic\n  address, without explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n### Our Responsibilities\n\nProject maintainers are responsible for clarifying the standards of acceptable\nbehavior and are expected to take appropriate and fair corrective action in\nresponse to any instances of unacceptable behavior.\n\nProject maintainers have the right and responsibility to remove, edit, or\nreject comments, commits, code, wiki edits, issues, and other contributions\nthat are not aligned to this Code of Conduct, or to ban temporarily or\npermanently any contributor for other behaviors that they deem inappropriate,\nthreatening, offensive, or harmful.\n\n### Scope\n\nThis Code of Conduct applies both within project spaces and in public spaces\nwhen an individual is representing the project or its community. Examples of\nrepresenting a project or community include using an official project e-mail\naddress, posting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event. Representation of a project may be\nfurther defined and clarified by project maintainers.\n\n### Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported by contacting the project maintainer at syntaqx [at] gmail.com. All\ncomplaints will be reviewed and investigated and will result in a response that\nis deemed necessary and appropriate to the circumstances. The project team is\nobligated to maintain confidentiality with regard to the reporter of an incident.\nFurther details of specific enforcement policies may be posted separately.\n\nProject maintainers who do not follow or enforce the Code of Conduct in good\nfaith may face temporary or permanent repercussions as determined by other\nmembers of the project's leadership.\n\n### Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage],\nversion 1.4, available at [http://contributor-covenant.org/version/1/4][version]\n\n[homepage]: http://contributor-covenant.org\n[version]: http://contributor-covenant.org/version/1/4/\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM golang:1.20-alpine AS builder\n\nARG VERSION=\"0.0.0-docker\"\n\nRUN apk add --update --no-cache \\\n  ca-certificates tzdata openssh git mercurial && update-ca-certificates \\\n  && rm -rf /var/cache/apk/*\n\nWORKDIR /src\n\nCOPY go.mod* go.sum* ./\nRUN --mount=type=cache,target=/go/pkg/mod go mod download\n\nCOPY . .\nRUN --mount=type=cache,target=/go/pkg/mod \\\n    --mount=type=cache,target=/root/.cache/go-build \\\n  CGO_ENABLED=0 go install -ldflags \"-X main.version=$VERSION\" ./cmd/...\n\nFROM alpine\n\nRUN adduser -S -D -H -h /app appuser\nUSER appuser\n\nCOPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo\nCOPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/\nCOPY --from=builder /go/bin/* /bin/\n\nENV PORT=8080\nEXPOSE $PORT\n\nVOLUME [\"/var/www\"]\n\nCMD [\"serve\", \"--dir\", \"/var/www\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) Chase Pierce <syntaqx@gmail.com>\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "Makefile",
    "content": "VERSION=`git --no-pager describe --tags --always`\n\nLDFLAGS+=\nLDFLAGS+=-X main.version=${VERSION}\n\nbuild:\n\tgo build -ldflags \"${LDFLAGS}\" -o bin/serve ./cmd/serve\n\ninstall:\n\tgo install -ldflags \"${LDFLAGS}\" ./cmd/serve\n"
  },
  {
    "path": "README.md",
    "content": "# <img src=\"https://raw.githubusercontent.com/syntaqx/serve/main/docs/logo.svg?sanitize=true\" width=\"250\">\n\n`serve` is a static http server anywhere you need one.\n\n[homebrew]:   https://brew.sh/\n[git]:        https://git-scm.com/\n[golang]:     https://golang.org/\n[releases]:   https://github.com/syntaqx/serve/releases\n[modules]:    https://github.com/golang/go/wiki/Modules\n[docker-hub]: https://hub.docker.com/r/syntaqx/serve\n\n[![Mentioned in Awesome Go](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go)\n\n[![codecov](https://codecov.io/gh/syntaqx/serve/branch/main/graph/badge.svg?token=FGkU1ntp8z)](https://codecov.io/gh/syntaqx/serve)\n[![Go Report Card](https://goreportcard.com/badge/github.com/syntaqx/serve)](https://goreportcard.com/report/github.com/syntaqx/serve)\n[![Go Reference](https://pkg.go.dev/badge/github.com/syntaqx/serve.svg)](https://pkg.go.dev/github.com/syntaqx/serve)\n\n[![GitHub Release](https://img.shields.io/github/release-pre/syntaqx/serve.svg)][releases]\n[![Docker Pulls](https://img.shields.io/docker/pulls/syntaqx/serve.svg)][docker-hub]\n\n> 🚨 The `main` branch is currently in active R&D for the next release of `serve`.\n> To use `serve`, please be sure to download a previous [release](https://github.com/syntaqx/serve/releases) as no stability guarantees\n> are being made further progress has been made towards a release candidate.\n\n## TL;DR\n\n> It's basically `python -m SimpleHTTPServer 8080` written in Go, because who\n> can remember that many letters?\n\n### Features\n\n* HTTPS (TLS)\n* CORS support\n* Request logging\n* `net/http` compatible\n* Support for [BasicAuth](https://en.wikipedia.org/wiki/Basic_access_authentication) via `users.json`\n\n## Installation\n\n`serve` can be installed in a handful of ways:\n\n### Homebrew on macOS\n\nIf you are using [Homebrew][] on macOS, you can install `serve` with the\nfollowing command:\n\n```sh\nbrew install syntaqx/tap/serve\n```\n\n### Docker\n\nThe official [syntaqx/serve][docker-hub] image is available on Docker Hub.\n\nTo get started, try hosting a directory from your docker host:\n\n```sh\ndocker run -v .:/var/www:ro -d syntaqx/serve\n```\n\nAlternatively, a simple `Dockerfile` can be used to generate a new image that\nincludes the necessary content:\n\n```dockerfile\nFROM syntaqx/serve\nCOPY . /var/www\n```\n\nPlace this in the same directory as your content, then `build` and `run` the\ncontainer:\n\n```sh\ndocker build -t some-content-serve .\ndocker run --name some-serve -d some-content-serve\n```\n\n#### Exposing an external port\n\n```sh\ndocker run --name some-serve -d -p 8080:8080 some-content-serve\n```\n\nThen you can navigate to http://localhost:8080/ or http://host-ip:8080/ in your\nbrowser.\n\n#### Using environment variables for configuration\n\n[12-factor-config]: https://12factor.net/config\n\nCurrently, `serve` only supports using the `PORT` environment variable for\nsetting the listening port. All other configurations are available as CLI flags.\n\n> In future releases, most configurations will be settable from both the CLI\n> flag as well as a compatible environment variable, aligning with the\n> expectations of a [12factor app][12-factor-config]. But, that will require a\n> fair amount of work before the functionality is made available.\n\nHere's an example using `compose.yml` to configure `serve` to use HTTPS:\n\n```yaml\nversion: '3'\nservices:\n  web:\n    image: syntaqx/serve\n    volumes:\n      - ./static:/var/www\n      - ./fixtures:/etc/ssl\n    environment:\n      - PORT=1234\n    ports:\n      - 1234\n    command: serve -ssl -cert=/etc/ssl/cert.pem -key=/etc/ssl/key.pem -dir=/var/www\n```\n\nThe project repository provides an example [compose](./compose.yml) that\nimplements a variety of common use-cases for `serve`. Feel free to use those to\nhelp you get started.\n\n### Download the binary\n\nQuickly download install the latest release:\n\n```sh\ncurl -sfL https://install.goreleaser.com/github.com/syntaqx/serve.sh | sh\n```\n\nOr manually download the [latest release][releases] binary for your system and\narchitecture and install it into your `$PATH`.\n\n### From source\n\nTo build from source, check out the instructions on getting started with\n[development](#development).\n\n## Usage\n\n```sh\nserve [options] [path]\n```\n\n> `[path]` defaults to `.` (relative path to the current directory)\n\nThen simply open your browser to http://localhost:8080 to view your server.\n\n### Options\n\nThe following configuration options are available:\n\n* `--host` host address to bind to (defaults to `0.0.0.0`)\n* `--port` listening port (defaults to `8080`)\n* `--ssl` enable https (defaults to `false`)\n* `--cert` path to the ssl cert file (defaults to `cert.pem`)\n* `--key` path to the ssl key file (defaults to `key.pem`)\n* `--dir` directory path to serve (defaults to `.`, also configurable by `arg[0]`)\n* `--users` path to users file (defaults to `users.dat`); file should contain lines of username:password in plain text\n\n## Development\n\nTo develop `serve` or interact with its source code in any meaningful way, be\nsure you have the following installed:\n\n### Prerequisites\n\n* [Git][git]\n* [Go][golang]\n\n### Install\n\nYou can download and install the project from GitHub by simply running:\n\n```sh\ngit clone git@github.com:syntaqx/serve.git && cd $(basename $_ .git)\nmake install\n```\n\nThis will install `serve` into your `$GOPATH/bin` directory, which assuming is\nproperly appended to your `$PATH`, can now be used:\n\n```sh\n$ serve version\nserve version v0.0.6-8-g5074d63 windows/amd64\n```\n\n## Using `serve` manually\n\nBesides running `serve` using the provided binary, you can also embed a\n`serve.FileServer` into your own Go program:\n\n```go\npackage main\n\nimport (\n    \"log\"\n    \"net/http\"\n\n    \"github.com/syntaqx/serve\"\n)\n\nfunc main() {\n    fs := serve.NewFileServer()\n    log.Fatal(http.ListenAndServe(\":8080\", fs))\n}\n```\n\n## License\n\n[MIT]: https://opensource.org/licenses/MIT\n\n`serve` is open source software released under the [MIT license][MIT].\n\nAs with all Docker images, these likely also contain other software which may be\nunder other licenses (such as Bash, etc from the base distribution, along with\nany direct or indirect dependencies of the primary software being contained).\n\nAs for any pre-built image usage, it is the image user's responsibility to\nensure that any use of this image complies with any relevant licenses for all\nsoftware contained within.\n"
  },
  {
    "path": "cmd/serve/main.go",
    "content": "// Package main implements the runtime for the serve binary.\npackage main\n\nimport (\n\t\"flag\"\n\t\"log\"\n\t\"os\"\n\n\t\"github.com/syntaqx/serve/internal/commands\"\n\t\"github.com/syntaqx/serve/internal/config\"\n)\n\nvar version = \"0.0.0-develop\"\n\nfunc main() {\n\tvar opt config.Flags\n\tflag.BoolVar(&opt.Debug, \"debug\", false, \"enable debug output\")\n\tflag.StringVar(&opt.Host, \"host\", \"\", \"host address to bind to\")\n\tflag.StringVar(&opt.Port, \"port\", \"8080\", \"listening port\")\n\tflag.BoolVar(&opt.EnableSSL, \"ssl\", false, \"enable https\")\n\tflag.StringVar(&opt.CertFile, \"cert\", \"cert.pem\", \"path to the ssl cert file\")\n\tflag.StringVar(&opt.KeyFile, \"key\", \"key.pem\", \"path to the ssl key file\")\n\tflag.StringVar(&opt.Directory, \"dir\", \"\", \"directory path to serve\")\n\tflag.StringVar(&opt.UsersFile, \"users\", \"users.dat\", \"path to users file\")\n\tflag.Parse()\n\n\tlog := log.New(os.Stderr, \"[serve] \", log.LstdFlags)\n\n\t// Allow port to be configured via the environment variable PORT.\n\t// This is both better for configuration, and required for Heroku.\n\tif port, ok := os.LookupEnv(\"PORT\"); ok {\n\t\topt.Port = port\n\t}\n\n\tcmd := flag.Arg(0)\n\n\tdir, err := config.SanitizeDir(opt.Directory, cmd)\n\tif err != nil {\n\t\tlog.Printf(\"sanitize directory: %v\", err)\n\t\tos.Exit(1)\n\t}\n\n\tswitch cmd {\n\tcase \"version\":\n\t\terr = commands.Version(version, os.Stderr)\n\tdefault:\n\t\terr = commands.Server(log, opt, dir)\n\t}\n\n\tif err != nil {\n\t\tlog.Printf(\"cmd.%s: %v\", cmd, err)\n\t\tos.Exit(1)\n\t}\n}\n"
  },
  {
    "path": "compose.yml",
    "content": "services:\n\n  # Note: You probably will want to remove the `build: .` lines if you copy\n  # these into your project. That is used to be able to rebuild the image\n  # directly in the project repsitory.\n\n  basic:\n    build: .\n    image: syntaqx/serve\n    volumes:\n      - ./static:/var/www\n    ports:\n      - 8080:8080\n\n  basic_ssl:\n    build: .\n    image: syntaqx/serve\n    volumes:\n      - ./static:/var/www\n      - ./fixtures:/etc/ssl\n    ports:\n      - 8888:8080\n    command: serve -ssl -cert=/etc/ssl/cert.pem -key=/etc/ssl/key.pem -dir=/var/www\n"
  },
  {
    "path": "examples/basic/main.go",
    "content": "package main\n\nimport (\n\t\"log\"\n\t\"net/http\"\n\n\t\"github.com/syntaqx/serve\"\n)\n\nfunc main() {\n\tfs := serve.NewFileServer(serve.Options{\n\t\tDirectory: \"../../static\",\n\t})\n\n\tlog.Print(\"serve started at http://localhost:8080/\")\n\tlog.Fatal(http.ListenAndServe(\":8080\", fs))\n}\n"
  },
  {
    "path": "fixtures/cert.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIEIDCCAwigAwIBAgIUG4x9A3w/n65jwz3y7Wo8MDrU6QEwDQYJKoZIhvcNAQEL\nBQAweTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk5ZMREwDwYDVQQHDAhOZXcgWW9y\nazEVMBMGA1UECgwMRXhhbXBsZSwgTExDMRIwEAYDVQQDDAlzaXRlLnRlc3QxHzAd\nBgkqhkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20wHhcNMTkwMTE3MTA0NDM0WhcN\nMjAwMTE3MTA0NDM0WjB5MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTlkxETAPBgNV\nBAcMCE5ldyBZb3JrMRUwEwYDVQQKDAxFeGFtcGxlLCBMTEMxEjAQBgNVBAMMCXNp\ndGUudGVzdDEfMB0GCSqGSIb3DQEJARYQdGVzdEBleGFtcGxlLmNvbTCCASIwDQYJ\nKoZIhvcNAQEBBQADggEPADCCAQoCggEBALLlVETDAxfpbMrL9vlTKu2y+G8y7qNv\nKIdp5FllHAtZVPMis1xV9U4xvpy7baKTKKPtKEYZGcy/gW4fEN9KlvHZSUqrLj7T\nX0ySTNkwGItZy+gm1gbwvbQGtL4atgu0jPsJB662DIzq4dLL1OAFMV6VfmY9r2Hs\nARhe0XjGtXKlX+Fyqnbxsot02C01CtFDcEftHR5KUZeUHkoIHmO+5ZtRAgAIfhV/\nDQfyn+GfXOfM7PWGfy7RdyyLMrD+SwdfJFpkeeqQTi7p3PIIuHmieGOBjIOUhRv2\nIEA7PbMNwoernE3Ey6iwErPjshWhSdLFG4NfAPs/KxDKe0qByRLOfZECAwEAAaOB\nnzCBnDAdBgNVHQ4EFgQUWlS44ZoMP/8IkJhHwxzJcfZ7IuIwHwYDVR0jBBgwFoAU\nWlS44ZoMP/8IkJhHwxzJcfZ7IuIwCQYDVR0TBAIwADALBgNVHQ8EBAMCBaAwFAYD\nVR0RBA0wC4IJc2l0ZS50ZXN0MCwGCWCGSAGG+EIBDQQfFh1PcGVuU1NMIEdlbmVy\nYXRlZCBDZXJ0aWZpY2F0ZTANBgkqhkiG9w0BAQsFAAOCAQEASQ/wPIrRSsIEewDg\nt6dehznWR+iBMGWGMpDEVw/IpRSN1zxLJp3i/4Yjcr98bEIP4tW27OODSJSKz11R\n6/Kb/B04g3s7N4iSAehpeQXPGktNlgGojZSXi7u2y5ON6QBAle5csFxIkuOWDVwH\nqM/lsVlNHGyM0BGVMm5VLi2OWSqspz6Lr6yguT7U/AJ/hPe+YjSU5Kc+OnCZ4IH0\nNcdVG5aPpDFeZ7c9v1uHa7b725lyXUYO8xfWR3QV6CsTLgRFWhwYBXF51sZbBBsr\nfu78txegVWnYau4uh/nytqPoOnjoP4BAMKlynPfIpJ9TLWxosWeXro2xY5zvdFkp\nXH/+0g==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "fixtures/key.pem",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCy5VREwwMX6WzK\ny/b5UyrtsvhvMu6jbyiHaeRZZRwLWVTzIrNcVfVOMb6cu22ikyij7ShGGRnMv4Fu\nHxDfSpbx2UlKqy4+019MkkzZMBiLWcvoJtYG8L20BrS+GrYLtIz7CQeutgyM6uHS\ny9TgBTFelX5mPa9h7AEYXtF4xrVypV/hcqp28bKLdNgtNQrRQ3BH7R0eSlGXlB5K\nCB5jvuWbUQIACH4Vfw0H8p/hn1znzOz1hn8u0XcsizKw/ksHXyRaZHnqkE4u6dzy\nCLh5onhjgYyDlIUb9iBAOz2zDcKHq5xNxMuosBKz47IVoUnSxRuDXwD7PysQyntK\ngckSzn2RAgMBAAECggEBAIJ5/q80KHJtPnrermAER6AcU1QPKrwq271//xswQncI\njYvTeEvVKdgBMgvwK7NSb2a4FxKhRg7ucgEWSWECbvsvxmPeXBlYYv5fCguyJ4Sj\nVrQYdyuStFm0Nmkc5D+/TL/fQyoq/xZcTZ5IKhfF0c8xa4I4ZU0fK2FR7qePDlHx\nkAjInhIAPxCh7vhKk35duhr8r7IDQ33jVyPQ7DgsEIKRh85CVxkcwrtV1sY3LM/O\nxmrYWxHzpke06qZBJROjAFKv1kV7NT3eKzgKg16yDkFqYdh38RnFsTB6/zgZ+rko\nJj23tynefYRx3e3feAvhnDQzY32HwKCA4fNm0brJrf0CgYEA7cdXzN0QLwvhvjem\nt0gNdcfk0f9pM0wcYh0n7ESANsKAkjAOBqlvJ6tRV1LaqeIX+y1yeBnUIVH+dNfA\ntM2nTiilvaasR1Er40c3eeyIhWJ8nC+wBGexxDg3Ys4B0azzcakCYkG6BuVdsAWD\naYdqWf6Tl80l7HwonCVFsu8nX+cCgYEAwJrX3agdZWTuAcFcdGIXWK1m8+4yGv6t\nfvwh9X/rkDQHJ5HXDsHmTc8yh/Qa35OzcZJxBooW5azmzVpEbgE/HjnBpNDjp0VT\nXk5k+bZkWgp6wN8BFrh2Me8hliRs93vsUZ+fnFJWgxMTPMpOvhcw9YjucG6lGpwk\nynGkJ0/bZ8cCgYAs8hVioBbDDdfqANL+qhwBO3vBRio4jBaBZUl6m6gwsatj9rlw\nAO8F7Jg/jWXP3vDxhbGxihBTDBCxPWcrxgPt/jj2FF9US7+kAn42CcP0kp1DWLBI\n5ODxWj796jrly29o+K1+rTXgv9Jpx2EDvZkY0cpMU3brsLxsZ485N4OV2QKBgQCV\nG0rinrOjO2/GjBs3Pnk0fYmmblD79Q37sNXZaR7ElIK1b4I+On5A3pcQCTqEu6O/\n2M8HcQAo7qH/eFJhlzV2AOCY595WMKVJ7QbfCwTFcDd3+Syumj9miOpHgguZzKY2\nyoyWSGgRMUNDXJt5LhsI+ukcwYuv/hG9aBzdEkWZIQKBgGLj5nwaJZWPJ381adJX\nJhwQcnS7cZIKrAifCay1oOaOcdQq/07QdBEjR6YT/X7oZCPtiDOdat9vzWKLNEY/\nnYY+XFijSz2CKvT+CScjJSxmrsCtiNBQRtaTSKWAcgCpSqN5S+mocWmInZBVtZev\n1OueDMUyPAsCabIR4HiTgAIs\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/syntaqx/serve\n\ngo 1.20\n\nrequire github.com/stretchr/testify v1.11.1\n\nrequire (\n\tgithub.com/davecgh/go-spew v1.1.1 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.0 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "internal/commands/server.go",
    "content": "package commands\n\nimport (\n\t\"bufio\"\n\t\"io\"\n\t\"log\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/syntaqx/serve\"\n\t\"github.com/syntaqx/serve/internal/config\"\n\t\"github.com/syntaqx/serve/internal/middleware\"\n)\n\nvar getHTTPServerFunc = GetStdHTTPServer\n\n// HTTPServer defines a returnable interface type for http.Server\ntype HTTPServer interface {\n\tListenAndServe() error\n\tListenAndServeTLS(certFile, keyFile string) error\n}\n\n// GetStdHTTPServer returns a standard net/http.Server configured for a given\n// address and handler, and other sane defaults.\nfunc GetStdHTTPServer(addr string, h http.Handler) HTTPServer {\n\treturn &http.Server{\n\t\tAddr:              addr,\n\t\tHandler:           h,\n\t\tReadTimeout:       15 * time.Second,\n\t\tWriteTimeout:      15 * time.Second,\n\t\tReadHeaderTimeout: 60 * time.Second,\n\t}\n}\n\n// GetAuthUsers returns a map of users from a given io.Reader\nfunc GetAuthUsers(r io.Reader) map[string]string {\n\tusers := make(map[string]string)\n\n\tif r != nil {\n\t\tscanner := bufio.NewScanner(r)\n\t\tfor scanner.Scan() {\n\t\t\tif line := strings.Split(scanner.Text(), \":\"); len(line) == 2 { // use only if correct format\n\t\t\t\tusers[line[0]] = line[1]\n\t\t\t}\n\t\t}\n\n\t\tif err := scanner.Err(); err != nil {\n\t\t\tlog.Fatalf(\"error occurred during reading users file\")\n\t\t}\n\t}\n\n\treturn users\n}\n\n// Server implements the static http server command.\nfunc Server(log *log.Logger, opt config.Flags, dir string) error {\n\tfs := serve.NewFileServer(serve.Options{\n\t\tDirectory: dir,\n\t})\n\n\t// Authorization\n\tvar f io.Reader\n\tif _, err := os.Stat(opt.UsersFile); !os.IsNotExist(err) {\n\t\t// Config file exists, load data\n\t\tf, err = os.Open(opt.UsersFile)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"unable to open users file %s\", opt.UsersFile)\n\t\t}\n\t} else if opt.Debug {\n\t\tlog.Printf(\"%s does not exist, authentication skipped\", opt.UsersFile)\n\t}\n\n\tfs.Use(\n\t\tmiddleware.Logger(log),\n\t\tmiddleware.Recover(),\n\t\tmiddleware.CORS(),\n\t\tmiddleware.Auth(GetAuthUsers(f)),\n\t)\n\n\taddr := net.JoinHostPort(opt.Host, opt.Port)\n\tserver := getHTTPServerFunc(addr, fs)\n\n\tif opt.EnableSSL {\n\t\tlog.Printf(\"https server listening at %s\", addr)\n\t\treturn server.ListenAndServeTLS(opt.CertFile, opt.KeyFile)\n\t}\n\n\tlog.Printf(\"http server listening at %s\", addr)\n\treturn server.ListenAndServe()\n}\n"
  },
  {
    "path": "internal/commands/server_test.go",
    "content": "package commands\n\nimport (\n\t\"bytes\"\n\t\"log\"\n\t\"net/http\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/syntaqx/serve/internal/config\"\n\t\"github.com/syntaqx/serve/mock\"\n)\n\nfunc getMockHTTPServerFunc(shouldError bool) func(addr string, h http.Handler) HTTPServer {\n\treturn func(addr string, h http.Handler) HTTPServer {\n\t\treturn &mock.HTTPServer{ShouldError: shouldError}\n\t}\n}\n\nfunc TestGetStdHTTPServer(t *testing.T) {\n\t_, ok := GetStdHTTPServer(\"\", http.DefaultServeMux).(*http.Server)\n\tassert.True(t, ok)\n}\n\nfunc TestServer(t *testing.T) {\n\tgetHTTPServerFunc = getMockHTTPServerFunc(false)\n\n\tassert := assert.New(t)\n\n\tvar b bytes.Buffer\n\tlog := log.New(&b, \"[test] \", 0)\n\topt := config.Flags{}\n\n\tassert.NoError(Server(log, opt, \".\"))\n\tassert.Contains(b.String(), \"http server listening at\")\n\n\tgetHTTPServerFunc = GetStdHTTPServer\n}\n\nfunc TestServerErr(t *testing.T) {\n\tgetHTTPServerFunc = getMockHTTPServerFunc(true)\n\n\tassert := assert.New(t)\n\n\tvar b bytes.Buffer\n\tlog := log.New(&b, \"[test] \", 0)\n\topt := config.Flags{}\n\n\ttime.Sleep(200 * time.Millisecond)\n\n\tassert.Error(Server(log, opt, \".\"))\n\ttime.Sleep(200 * time.Millisecond)\n\n\tgetHTTPServerFunc = GetStdHTTPServer\n}\n\nfunc TestServerHTTPS(t *testing.T) {\n\tgetHTTPServerFunc = getMockHTTPServerFunc(false)\n\n\tassert := assert.New(t)\n\n\tvar b bytes.Buffer\n\tlog := log.New(&b, \"[test] \", 0)\n\n\topt := config.Flags{\n\t\tEnableSSL: true,\n\t\tCertFile:  \"../../fixtures/cert.pem\",\n\t\tKeyFile:   \"../../fixtures/key.pem\",\n\t}\n\n\tassert.NoError(Server(log, opt, \".\"))\n\tassert.Contains(b.String(), \"https server listening at\")\n\n\tgetHTTPServerFunc = GetStdHTTPServer\n}\n\nfunc TestGetAuthUsers(t *testing.T) {\n\ttests := []struct {\n\t\tinput  string\n\t\toutput map[string]string\n\t}{\n\t\t{ // Single user\n\t\t\t\"user1:pass1\", map[string]string{\n\t\t\t\t\"user1\": \"pass1\",\n\t\t\t},\n\t\t},\n\t\t{ // Multiple users\n\t\t\t\"user1:pass1\\nuser2:pass2\", map[string]string{\n\t\t\t\t\"user1\": \"pass1\",\n\t\t\t\t\"user2\": \"pass2\",\n\t\t\t},\n\t\t},\n\t\t{ // Empty file\n\t\t\t\"\", map[string]string{},\n\t\t},\n\t\t{ // Incorrect structure\n\t\t\t\"user1:pass1:field1\", map[string]string{},\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tmockFile := strings.NewReader(test.input)\n\t\tassert.Equal(t, GetAuthUsers(mockFile), test.output)\n\t}\n\n}\n"
  },
  {
    "path": "internal/commands/version.go",
    "content": "package commands\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"runtime\"\n)\n\n// Version implements the command `version` which outputs the current binary\n// release version, if any.\nfunc Version(version string, w io.Writer) error {\n\tfmt.Fprintf(w, \"serve version %s %s/%s\\n\", version, runtime.GOOS, runtime.GOARCH)\n\treturn nil\n}\n"
  },
  {
    "path": "internal/commands/version_test.go",
    "content": "package commands\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestVersion(t *testing.T) {\n\tt.Parallel()\n\tassert := assert.New(t)\n\n\tvar b bytes.Buffer\n\terr := Version(\"mock\", &b)\n\n\tassert.NoError(err)\n\tassert.Contains(b.String(), \"version mock\")\n}\n"
  },
  {
    "path": "internal/config/flags.go",
    "content": "package config\n\nimport (\n\t\"fmt\"\n\t\"os\"\n)\n\nvar getwd = os.Getwd\n\n// Flags are the expose configuration flags available to the serve binary.\ntype Flags struct {\n\tDebug     bool\n\tHost      string\n\tPort      string\n\tEnableSSL bool\n\tCertFile  string\n\tKeyFile   string\n\tDirectory string\n\tUsersFile string\n}\n\n// SanitizeDir allows a directory source to be set from multiple values. If any\n// value is defined, that value is used. If none are defined, the current\n// working directory is retrieved.\nfunc SanitizeDir(dirs ...string) (string, error) {\n\tfor _, dir := range dirs {\n\t\tif len(dir) > 0 {\n\t\t\treturn dir, nil\n\t\t}\n\t}\n\n\tcwd, err := getwd()\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"cannot determine cwd: %v\", err)\n\t}\n\n\treturn cwd, nil\n}\n"
  },
  {
    "path": "internal/config/flags_test.go",
    "content": "package config\n\nimport (\n\t\"errors\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestSanitizeDir(t *testing.T) {\n\tt.Parallel()\n\tassert := assert.New(t)\n\n\tcwd, err := os.Getwd()\n\tassert.NoError(err)\n\n\tvar tests = []struct {\n\t\tdirs     []string\n\t\texpected string\n\t}{\n\t\t{[]string{\"foo\", \"bar\"}, \"foo\"},\n\t\t{[]string{\"\", \"bar\"}, \"bar\"},\n\t\t{[]string{\"\", \"\"}, cwd},\n\t}\n\n\tfor _, tt := range tests {\n\t\ttt := tt\n\t\tt.Run(\"\", func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tdir, err := SanitizeDir(tt.dirs...)\n\t\t\tassert.Equal(tt.expected, dir)\n\t\t\tassert.NoError(err)\n\t\t})\n\t}\n}\n\nfunc TestSanitizeDirCwdErr(t *testing.T) {\n\tassert := assert.New(t)\n\n\tgetwd = func() (string, error) {\n\t\treturn \"\", errors.New(\"mock\")\n\t}\n\n\tdir, err := SanitizeDir()\n\tassert.Empty(dir)\n\tassert.Error(err)\n\n\tgetwd = os.Getwd\n}\n"
  },
  {
    "path": "internal/middleware/auth.go",
    "content": "package middleware\n\nimport \"net/http\"\n\n// Auth sets basic HTTP authorization\nfunc Auth(users map[string]string) func(next http.Handler) http.Handler {\n\treturn func(next http.Handler) http.Handler {\n\t\tfn := func(w http.ResponseWriter, r *http.Request) {\n\t\t\t// Only require auth if we have any users\n\t\t\tif len(users) > 0 {\n\t\t\t\tauthUser, authPass, ok := r.BasicAuth()\n\t\t\t\tif !ok {\n\t\t\t\t\t// No username/password received\n\t\t\t\t\tw.Header().Set(\"WWW-Authenticate\", \"Basic realm=Authenticate\")\n\t\t\t\t\tw.WriteHeader(http.StatusUnauthorized)\n\t\t\t\t} else {\n\t\t\t\t\tif pass, ok := users[authUser]; ok {\n\t\t\t\t\t\t// User exists\n\t\t\t\t\t\tif pass == authPass {\n\t\t\t\t\t\t\t// Authentication successful\n\t\t\t\t\t\t\tnext.ServeHTTP(w, r)\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\thttp.Error(w, \"Incorrect login details\", http.StatusUnauthorized)\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\thttp.Error(w, \"Incorrect login details\", http.StatusUnauthorized)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tnext.ServeHTTP(w, r)\n\t\t\t}\n\t\t}\n\n\t\treturn http.HandlerFunc(fn)\n\t}\n}\n"
  },
  {
    "path": "internal/middleware/auth_test.go",
    "content": "package middleware\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestAuth(t *testing.T) {\n\tt.Parallel()\n\tassert := assert.New(t)\n\n\treq, err := http.NewRequest(http.MethodGet, \"/\", nil)\n\tassert.NoError(err)\n\tres := httptest.NewRecorder()\n\n\t// No users\n\ttestHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})\n\tAuth(nil)(testHandler).ServeHTTP(res, req)\n\tassert.Equal(\"\", res.Header().Get(\"WWW-Authenticate\"))\n\n\t// Some users\n\ttestUsers := map[string]string{\n\t\t\"user1\": \"pass1\",\n\t\t\"user2\": \"pass2\",\n\t}\n\tAuth(testUsers)(testHandler).ServeHTTP(res, req)\n\tassert.Equal(\"Basic realm=Authenticate\", res.Header().Get(\"WWW-Authenticate\"))\n\tassert.Equal(http.StatusUnauthorized, res.Result().StatusCode)\n\n\t// Correct password\n\t// Recreate new environment\n\ttestHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})\n\treq, err = http.NewRequest(http.MethodGet, \"/\", nil)\n\tassert.NoError(err)\n\tres = httptest.NewRecorder()\n\n\treq.SetBasicAuth(\"user1\", \"pass1\")\n\tAuth(testUsers)(testHandler).ServeHTTP(res, req)\n\tassert.Equal(\"\", res.Header().Get(\"WWW-Authenticate\"))\n\tassert.Equal(http.StatusOK, res.Result().StatusCode)\n\n\t// Incorrect password\n\t// Recreate new environment\n\ttestHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})\n\treq, err = http.NewRequest(http.MethodGet, \"/\", nil)\n\tassert.NoError(err)\n\tres = httptest.NewRecorder()\n\n\treq.SetBasicAuth(\"user1\", \"pass2\")\n\tAuth(testUsers)(testHandler).ServeHTTP(res, req)\n\tassert.Equal(http.StatusUnauthorized, res.Result().StatusCode)\n}\n"
  },
  {
    "path": "internal/middleware/cors.go",
    "content": "package middleware\n\nimport (\n\t\"net/http\"\n\t\"strings\"\n)\n\n// CORS sets permissive cross-origin resource sharing rules.\nfunc CORS() func(next http.Handler) http.Handler {\n\treturn func(next http.Handler) http.Handler {\n\t\tfn := func(w http.ResponseWriter, r *http.Request) {\n\t\t\tw.Header().Set(\"Access-Control-Allow-Origin\", \"*\")\n\t\t\tw.Header().Set(\"Access-Control-Allow-Methods\", strings.Join([]string{\n\t\t\t\thttp.MethodHead,\n\t\t\t\thttp.MethodOptions,\n\t\t\t\thttp.MethodGet,\n\t\t\t\thttp.MethodPost,\n\t\t\t\thttp.MethodPut,\n\t\t\t\thttp.MethodPatch,\n\t\t\t\thttp.MethodDelete,\n\t\t\t}, \", \"))\n\t\t\tnext.ServeHTTP(w, r)\n\t\t}\n\t\treturn http.HandlerFunc(fn)\n\t}\n}\n"
  },
  {
    "path": "internal/middleware/cors_test.go",
    "content": "package middleware\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestCORS(t *testing.T) {\n\tt.Parallel()\n\tassert := assert.New(t)\n\n\treq, err := http.NewRequest(http.MethodGet, \"/\", nil)\n\tassert.NoError(err)\n\tres := httptest.NewRecorder()\n\n\ttestHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})\n\tCORS()(testHandler).ServeHTTP(res, req)\n\n\tassert.Equal(\"*\", res.Header().Get(\"Access-Control-ALlow-Origin\"))\n\tassert.Contains(res.Header().Get(\"Access-Control-Allow-Methods\"), http.MethodGet)\n}\n"
  },
  {
    "path": "internal/middleware/logger.go",
    "content": "package middleware\n\nimport (\n\t\"log\"\n\t\"net/http\"\n)\n\n// Logger is a middleware that logs each request, along with some useful data\n// about what was requested, and what the response was.\nfunc Logger(log *log.Logger) func(next http.Handler) http.Handler {\n\treturn func(next http.Handler) http.Handler {\n\t\tfn := func(w http.ResponseWriter, r *http.Request) {\n\t\t\tsw := statusWriter{ResponseWriter: w}\n\n\t\t\tdefer func() {\n\t\t\t\tlog.Println(r.Method, r.URL.Path, sw.status, r.RemoteAddr, r.UserAgent())\n\t\t\t}()\n\t\t\tnext.ServeHTTP(&sw, r)\n\t\t}\n\t\treturn http.HandlerFunc(fn)\n\t}\n}\n"
  },
  {
    "path": "internal/middleware/logger_test.go",
    "content": "package middleware\n\nimport (\n\t\"bytes\"\n\t\"log\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nvar logTests = []struct {\n\tin  func(w http.ResponseWriter, r *http.Request)\n\tout string\n}{\n\t{\n\t\tin: func(w http.ResponseWriter, _ *http.Request) {\n\t\t\t_, err := w.Write([]byte{})\n\t\t\tif err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t},\n\t\tout: \"[test] GET / 200\",\n\t},\n\t{\n\t\tin: func(w http.ResponseWriter, _ *http.Request) {\n\t\t\tw.WriteHeader(http.StatusNotFound)\n\t\t},\n\t\tout: \"[test] GET / 404\",\n\t},\n}\n\nfunc TestLogger(t *testing.T) {\n\tt.Parallel()\n\n\tfor _, tt := range logTests {\n\t\tassert := assert.New(t)\n\n\t\tvar b bytes.Buffer\n\t\tlog := log.New(&b, \"[test] \", 0)\n\n\t\treq, err := http.NewRequest(http.MethodGet, \"/\", nil)\n\t\tassert.NoError(err)\n\t\tres := httptest.NewRecorder()\n\n\t\ttestHandler := http.HandlerFunc(tt.in)\n\t\tLogger(log)(testHandler).ServeHTTP(res, req)\n\n\t\tassert.Equal(tt.out, strings.TrimSpace(b.String()))\n\t}\n}\n"
  },
  {
    "path": "internal/middleware/recover.go",
    "content": "package middleware\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n)\n\n// Recover is a middleware that recovers from panics that occur for a request.\nfunc Recover() func(next http.Handler) http.Handler {\n\treturn func(next http.Handler) http.Handler {\n\t\tfn := func(w http.ResponseWriter, r *http.Request) {\n\t\t\tdefer func() {\n\t\t\t\tif err := recover(); err != nil {\n\t\t\t\t\thttp.Error(w, fmt.Sprintf(\"[PANIC RECOVERED] %v\", err), http.StatusInternalServerError)\n\t\t\t\t}\n\t\t\t}()\n\t\t\tnext.ServeHTTP(w, r)\n\t\t}\n\t\treturn http.HandlerFunc(fn)\n\t}\n}\n"
  },
  {
    "path": "internal/middleware/recover_test.go",
    "content": "package middleware\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestRecover(t *testing.T) {\n\n\tt.Parallel()\n\tassert := assert.New(t)\n\n\treq, err := http.NewRequest(http.MethodGet, \"/\", nil)\n\tassert.NoError(err)\n\tres := httptest.NewRecorder()\n\n\ttestHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tpanic(\"test\")\n\t})\n\n\tRecover()(testHandler).ServeHTTP(res, req)\n\n\tassert.Equal(http.StatusInternalServerError, res.Code)\n\tassert.Equal(\"[PANIC RECOVERED] test\", strings.TrimSpace(res.Body.String()))\n}\n"
  },
  {
    "path": "internal/middleware/statuswriter.go",
    "content": "package middleware\n\nimport \"net/http\"\n\ntype statusWriter struct {\n\thttp.ResponseWriter\n\tstatus int\n}\n\nfunc (w *statusWriter) WriteHeader(status int) {\n\tw.status = status\n\tw.ResponseWriter.WriteHeader(status)\n}\n\nfunc (w *statusWriter) Write(b []byte) (int, error) {\n\tif w.status == 0 {\n\t\tw.status = http.StatusOK\n\t}\n\treturn w.ResponseWriter.Write(b)\n}\n"
  },
  {
    "path": "mock/http.go",
    "content": "package mock\n\nimport \"errors\"\n\n// ErrMock is a mock error\nvar ErrMock = errors.New(\"mock error\")\n\n// HTTPServer is a mock http server\ntype HTTPServer struct {\n\tShouldError bool\n}\n\n// ListenAndServe is a mock http server method\nfunc (s *HTTPServer) ListenAndServe() error {\n\tif s.ShouldError {\n\t\treturn ErrMock\n\t}\n\treturn nil\n}\n\n// ListenAndServeTLS is a mock http server method\nfunc (s *HTTPServer) ListenAndServeTLS(certFile, keyFile string) error {\n\tif s.ShouldError {\n\t\treturn ErrMock\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "serve.go",
    "content": "// Package serve provides a static http server anywhere you need one.\npackage serve\n\nimport \"net/http\"\n\n// Options is a struct for specifying configuration options for a FileServer.\ntype Options struct {\n\t// Directory is the root directory from which to serve files.\n\tDirectory string\n\n\t// Prefix is a filepath prefix that should be ignored by the FileServer.\n\tPrefix string\n}\n\n// FileServer wraps an http.FileServer.\ntype FileServer struct {\n\topt     Options\n\thandler http.Handler\n}\n\n// NewFileServer initializes a FileServer.\nfunc NewFileServer(options ...Options) *FileServer {\n\tvar opt Options\n\tif len(options) > 0 {\n\t\topt = options[0]\n\t}\n\n\tfs := &FileServer{\n\t\topt: opt,\n\t}\n\n\tfs.handler = http.StripPrefix(opt.Prefix, http.FileServer(http.Dir(opt.Directory)))\n\treturn fs\n}\n\n// Use wraps the Handler with middleware(s).\nfunc (fs *FileServer) Use(mws ...func(http.Handler) http.Handler) {\n\tfor _, h := range mws {\n\t\tfs.handler = h(fs.handler)\n\t}\n}\n\n// ServeHTTP implements the net/http.Handler interface.\nfunc (fs *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {\n\tfs.handler.ServeHTTP(w, r)\n}\n"
  },
  {
    "path": "serve_test.go",
    "content": "package serve\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestFileServerDefaults(t *testing.T) {\n\tfs := NewFileServer()\n\t_ = fs\n}\n\nfunc TestFileServerOptions(t *testing.T) {\n\tfs := NewFileServer(Options{Directory: \"test\"})\n\t_ = fs\n}\n\nfunc TestFileServerUse(t *testing.T) {\n\tt.Parallel()\n\tassert := assert.New(t)\n\n\treq, err := http.NewRequest(http.MethodGet, \"/\", nil)\n\tassert.NoError(err)\n\tres := httptest.NewRecorder()\n\n\ttestMiddleware1 := func(next http.Handler) http.Handler {\n\t\tfn := func(w http.ResponseWriter, r *http.Request) {\n\t\t\t_, _ = w.Write([]byte(\"start\\n\"))\n\t\t\tnext.ServeHTTP(w, r)\n\t\t}\n\t\treturn http.HandlerFunc(fn)\n\t}\n\n\ttestMiddleware2 := func(next http.Handler) http.Handler {\n\t\tfn := func(w http.ResponseWriter, _ *http.Request) {\n\t\t\t_, _ = w.Write([]byte(\"end\\n\"))\n\t\t}\n\t\treturn http.HandlerFunc(fn)\n\t}\n\n\tfs := &FileServer{\n\t\thandler: http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {\n\t\t\tt.Fail()\n\t\t}),\n\t}\n\n\tfs.Use(testMiddleware2, testMiddleware1)\n\n\tfs.ServeHTTP(res, req)\n\n\tassert.Equal(\"start\\nend\\n\", res.Body.String())\n}\n\nfunc TestFileServerServeHTTP(t *testing.T) {\n\tt.Parallel()\n\tassert := assert.New(t)\n\n\treq, err := http.NewRequest(http.MethodGet, \"/\", nil)\n\tassert.NoError(err)\n\tres := httptest.NewRecorder()\n\n\tfs := &FileServer{\n\t\thandler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {\n\t\t\t_, _ = w.Write([]byte(\"expected\"))\n\t\t}),\n\t}\n\n\tfs.ServeHTTP(res, req)\n\n\tassert.Equal(\"expected\", res.Body.String())\n}\n"
  },
  {
    "path": "static/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"utf-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n\n  <title>Welcome to your server!</title>\n  <link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css?family=Mukta\">\n\n  <style>\n    *, ::after, ::before {\n      box-sizing: border-box;\n    }\n\n    html, body {\n      background-color: #eee;\n      margin: 0;\n      height: 100vh;\n    }\n\n    body {\n      font-family: 'Mukta', sans-serif;\n      font-size: 1rem;\n      font-weight: 400;\n      line-height: 1.5;\n      color: #656f79;\n    }\n\n    a {\n      text-decoration: none;\n      color: #5ba7ce;\n    }\n\n    a:hover {\n      text-decoration: underline;\n    }\n\n    h1, h2, h3, h4, h5, h6 {\n      margin-top: 0;\n      margin-bottom: .5rem;\n      line-height: 1.2;\n    }\n\n    h1 {\n      font-size: 2.5rem;\n    }\n\n    h2 {\n      font-size: 1.8rem;\n    }\n\n    h3 {\n      font-size: 1.75rem;\n    }\n\n    .vh-100 {\n      height: 100vh;\n    }\n\n    .mb-2 {\n      margin-bottom: 2rem;\n    }\n\n    .flex-center {\n      align-items: center;\n      justify-content: center;\n      display: flex;\n    }\n\n    .position-rel {\n      position: relative;\n    }\n\n    .text-center {\n      text-align: center;\n    }\n\n    .title {\n      margin-top: 2rem;\n      letter-spacing: -.2rem;\n      font-weight: 100;\n      font-size: 4rem;\n      color: #363a3f;\n    }\n\n    .links {\n      margin: 0;\n      padding: 0;\n      list-style: none;\n    }\n\n    .links li {\n      margin: 0 .5rem 0 0;\n      padding: 0;\n      display: inline-block;\n    }\n\n  </style>\n</head>\n<body>\n\n  <div class=\"flex-center position-rel vh-100\">\n    <div class=\"text-center\">\n      <header class=\"mb-2\">\n        <img src=\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgMTYuMSA4LjgiPjxkZWZzPjxzdHlsZT4uY2xzLTF7ZmlsbDp1cmwoI2xpbmVhci1ncmFkaWVudCk7fTwvc3R5bGU+PGxpbmVhckdyYWRpZW50IGlkPSJsaW5lYXItZ3JhZGllbnQiIHkxPSI4LjgiIHgyPSIxNCIgeTI9IjguOCIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgxLCAwLCAwLCAtMSwgMCwgMTMuMikiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiMwMDAwOTIiIC8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjZmYwMGYzIiAvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjx0aXRsZT5Bc3NldCAyPC90aXRsZT48ZyBpZD0iTGF5ZXJfMiIgZGF0YS1uYW1lPSJMYXllciAyIj48ZyBpZD0iTGF5ZXJfMS0yIiBkYXRhLW5hbWU9IkxheWVyIDEiPjxwYXRoIGNsYXNzPSJjbHMtMSIgZD0iTTgsMEExLjU0LDEuNTQsMCwwLDAsNi41LDEuNWExLjM1LDEuMzUsMCwwLDAsMCwuMTgsNi41LDYuNSwwLDAsMC01LDYuMTJILjZhLjU2LjU2LDAsMCwwLS42LjUuNTYuNTYsMCwwLDAsLjYuNUgxNS40Yy4zLDAsLjctLjIuNy0uNWEuNTYuNTYsMCwwLDAtLjYtLjVoLTFhNi41LDYuNSwwLDAsMC01LTYuMTIsMSwxLDAsMCwwLDAtLjE4QTEuNTQsMS41NCwwLDAsMCw4LDBaTTgsMWEuNDcuNDcsMCwwLDEsLjUuNXYwYTQuMTgsNC4xOCwwLDAsMC0xLDB2MEEuNDcuNDcsMCwwLDEsOCwxWk04LDIuNWE1LjUxLDUuNTEsMCwwLDEsNS40OSw1LjNoLTExQTUuNTEsNS41MSwwLDAsMSw4LDIuNVptLS44OS43OGEuMzkuMzksMCwwLDAtLjIxLDBBNC44OCw0Ljg4LDAsMCwwLDMuMyw2LjlhLjU1LjU1LDAsMCwwLC40LjZoLjFhLjQzLjQzLDAsMCwwLC40LS40LDQuMTksNC4xOSwwLDAsMSwzLTIuOS40OS40OSwwLDAsMCwuMy0uNkEuNTMuNTMsMCwwLDAsNy4xMSwzLjI4WiIgLz48L2c+PC9nPjwvc3ZnPg==\" width=\"250\">\n        <h1 class=\"title\">serve</h1>\n        <h2>Your web server is working correctly!</h2>\n      </header>\n      <p>You can edit this file at <code>/var/www/index.html</code></p>\n      <ul class=\"links\">\n        <li><a href=\"https://github.com/syntaqx/serve/tree/main/examples\">Examples</a></li>\n        <li><a href=\"https://github.com/syntaqx/serve\">GitHub</a></li>\n        <li><a href=\"https://hub.docker.com/r/syntaqx/serve\">Docker Hub</a></li>\n      </ul>\n    </div>\n  </div>\n\n</body>\n</html>\n"
  }
]