Repository: syntaqx/serve Branch: main Commit: ad0ff656bad8 Files: 42 Total size: 48.0 KB Directory structure: gitextract_g0z6iav5/ ├── .dockerignore ├── .editorconfig ├── .gitattributes ├── .github/ │ ├── dependabot.yml │ └── workflows/ │ ├── codeql.yml │ ├── dependabot.yml │ ├── docker.yml │ ├── go.yml │ ├── golangci-lint.yml │ └── release.yml ├── .gitignore ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── cmd/ │ └── serve/ │ └── main.go ├── compose.yml ├── examples/ │ └── basic/ │ └── main.go ├── fixtures/ │ ├── cert.pem │ └── key.pem ├── go.mod ├── go.sum ├── internal/ │ ├── commands/ │ │ ├── server.go │ │ ├── server_test.go │ │ ├── version.go │ │ └── version_test.go │ ├── config/ │ │ ├── flags.go │ │ └── flags_test.go │ └── middleware/ │ ├── auth.go │ ├── auth_test.go │ ├── cors.go │ ├── cors_test.go │ ├── logger.go │ ├── logger_test.go │ ├── recover.go │ ├── recover_test.go │ └── statuswriter.go ├── mock/ │ └── http.go ├── serve.go ├── serve_test.go └── static/ └── index.html ================================================ FILE CONTENTS ================================================ ================================================ FILE: .dockerignore ================================================ *.yml *.yaml *.json *.md .git* bin dist build docs examples tmp vendor .editorconfig Dockerfile CODEOWNERS LICENSE Makefile .env compose.yml compose.override.yml README.md ================================================ FILE: .editorconfig ================================================ root = true [*] charset = utf-8 end_of_line = lf indent_style = space max_line_length = 120 insert_final_newline = true trim_trailing_whitespace = true [*.{json,yaml,yml}] indent_style = space indent_size = 2 [*.{sh,bash,envrc}] indent_style = space indent_size = 4 [*.go] indent_style = tab indent_size = 4 [{Makefile,makefile,GNUmakefile}] indent_style = tab indent_size = 4 [*.md] trim_trailing_whitespace = false ================================================ FILE: .gitattributes ================================================ * text=auto ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: github-actions directory: "/" schedule: interval: weekly - package-ecosystem: gomod directory: "/" schedule: interval: weekly ================================================ FILE: .github/workflows/codeql.yml ================================================ # For most projects, this workflow file will not need changing; you simply need # to commit it to your repository. # # You may wish to alter this file to override the set of languages analyzed, # or to provide custom queries or build logic. # # ******** NOTE ******** # We have attempted to detect the languages in your repository. Please check # the `language` matrix defined below to confirm you have the correct set of # supported CodeQL languages. # name: "CodeQL" on: push: branches: [ "main" ] pull_request: # The branches below must be a subset of the branches above branches: [ "main" ] schedule: - cron: '37 6 * * 0' jobs: analyze: name: Analyze runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} permissions: actions: read contents: read security-events: write strategy: fail-fast: false matrix: language: [ 'go' ] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] # Use only 'java' to analyze code written in Java, Kotlin or both # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support steps: - name: Checkout repository uses: actions/checkout@v6 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v4 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # 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 # queries: security-extended,security-and-quality # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild uses: github/codeql-action/autobuild@v4 # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun # If the Autobuild fails above, remove it and uncomment the following three lines. # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. # - run: | # echo "Run, Build Application using script" # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v4 with: category: "/language:${{matrix.language}}" ================================================ FILE: .github/workflows/dependabot.yml ================================================ name: Dependabot Approve & Auto-Merge on: pull_request: branches: [ main ] permissions: contents: write pull-requests: write jobs: dependabot: runs-on: ubuntu-latest if: ${{ github.actor == 'dependabot[bot]' }} steps: - name: Dependabot metadata id: metadata uses: dependabot/fetch-metadata@v3 with: github-token: "${{ secrets.GITHUB_TOKEN }}" - name: Output Metadata run: | echo "${{ steps.metadata.outputs.dependency-names }}" echo "${{ steps.metadata.outputs.dependency-type }}" echo "${{ steps.metadata.outputs.update-type }}" - name: Approve a PR run: gh pr review --approve "$PR_URL" env: PR_URL: ${{ github.event.pull_request.html_url }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Enable auto-merge for Dependabot PRs # if: ${{ steps.metadata.outputs.update-type == 'version-update:semver-patch' }} run: gh pr merge --auto --merge --delete-branch "$PR_URL" env: PR_URL: ${{ github.event.pull_request.html_url }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/docker.yml ================================================ name: Docker on: workflow_call: inputs: push: description: "Push the image to the registry" type: boolean default: false required: false outputs: image: description: "Output Image" value: ${{ github.repository }}:sha-${{ jobs.build.outputs.version }} jobs: build: runs-on: ubuntu-latest outputs: version: ${{ steps.vars.outputs.sha_short }} steps: - name: Checkout uses: actions/checkout@v6 - name: Set output vars id: vars run: echo "::set-output name=sha_short::$(git rev-parse --short HEAD)" - name: Docker meta id: meta uses: docker/metadata-action@v5 with: images: ${{ github.repository }} tags: | type=schedule type=raw,value=latest,enable={{is_default_branch}} type=ref,event=branch type=ref,event=pr type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}} type=sha - name: Set up QEMU uses: docker/setup-qemu-action@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 - name: Login to DockerHub uses: docker/login-action@v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push uses: docker/build-push-action@v7 with: context: . push: ${{ inputs.push }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=registry,ref=${{ github.repository }}:buildcache cache-to: type=registry,ref=${{ github.repository }}:buildcache,mode=max - if: ${{ github.event_name != 'pull_request' }} name: Update Docker Hub Description uses: peter-evans/dockerhub-description@v5 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} repository: ${{ github.repository }} ================================================ FILE: .github/workflows/go.yml ================================================ name: Go on: push: branches: [ main ] pull_request: branches: [ main ] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Set up Go uses: actions/setup-go@v6 with: cache: true - name: Build run: go build -v ./... - name: Test run: go test -race -coverprofile=coverage.out -covermode=atomic ./... - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v6 ================================================ FILE: .github/workflows/golangci-lint.yml ================================================ name: golangci-lint on: push: tags: - v* branches: - main pull_request: permissions: contents: read # Optional: allow read access to pull request. Use with `only-new-issues` option. # pull-requests: read jobs: golangci: name: lint runs-on: ubuntu-latest steps: - uses: actions/setup-go@v6 with: cache: false - uses: actions/checkout@v6 - name: golangci-lint uses: golangci/golangci-lint-action@v6 with: # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version version: latest # Optional: working directory, useful for monorepos # working-directory: somedir # Optional: golangci-lint command line arguments. # args: --issues-exit-code=0 # Optional: show only new issues if it's a pull request. The default value is `false`. # only-new-issues: true # Optional: if set to true then the all caching functionality will be complete disabled, # takes precedence over all other caching options. # skip-cache: true # Optional: if set to true then the action don't cache or restore ~/go/pkg. # skip-pkg-cache: true # Optional: if set to true then the action don't cache or restore ~/.cache/go-build. # skip-build-cache: true ================================================ FILE: .github/workflows/release.yml ================================================ name: cd on: push: tags: - v* workflow_dispatch: jobs: docker: uses: ./.github/workflows/docker.yml with: push: true secrets: inherit ================================================ FILE: .gitignore ================================================ # Binaries for programs and plugins *.exe *.dll *.so *.dylib # Test binary, build with `go test -c` *.test # SQLite Databases *.db # Log files *.log # Output of the go coverage tool, specifically when used with LiteIDE coverage* *.out # Dependency directories vendor/ _vendor-*/ # Build artifacts bin/ dist/ # Local configurations .env* compose.override.yml ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing [semver]: http://semver.org/ When contributing to this repository, please first discuss the change you wish to make via issue, email, or any other method with the owners of this repository before making a change. Please note we have a code of conduct, please follow it in all your interactions with the project. ## Pull Request Process 1. Ensure any install or build dependencies are removed before the end of the layer when performing a build. 2. Update the `README.md` or `docs` with details of change to the project, this includes new flags, environment variables, exposed ports, useful file locations and container parameters. 3. Specify how your change should affect our versioning scheme when merged. For more information on how we implement versioning, check out the [semver][] documentation. PRs will be grouped into logical version groups so that we aren't incrementing the version on every merge. 4. You may merge the Pull Request in once you have the sign-off of other developers, or if you do not have permission to do that, you may request a reviewer to merge it for you. ## Code of Conduct ### Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ### Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ### Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ### Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ### Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project maintainer at syntaqx [at] gmail.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ### Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ ================================================ FILE: Dockerfile ================================================ FROM golang:1.20-alpine AS builder ARG VERSION="0.0.0-docker" RUN apk add --update --no-cache \ ca-certificates tzdata openssh git mercurial && update-ca-certificates \ && rm -rf /var/cache/apk/* WORKDIR /src COPY go.mod* go.sum* ./ RUN --mount=type=cache,target=/go/pkg/mod go mod download COPY . . RUN --mount=type=cache,target=/go/pkg/mod \ --mount=type=cache,target=/root/.cache/go-build \ CGO_ENABLED=0 go install -ldflags "-X main.version=$VERSION" ./cmd/... FROM alpine RUN adduser -S -D -H -h /app appuser USER appuser COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ COPY --from=builder /go/bin/* /bin/ ENV PORT=8080 EXPOSE $PORT VOLUME ["/var/www"] CMD ["serve", "--dir", "/var/www"] ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) Chase Pierce 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. ================================================ FILE: Makefile ================================================ VERSION=`git --no-pager describe --tags --always` LDFLAGS+= LDFLAGS+=-X main.version=${VERSION} build: go build -ldflags "${LDFLAGS}" -o bin/serve ./cmd/serve install: go install -ldflags "${LDFLAGS}" ./cmd/serve ================================================ FILE: README.md ================================================ # `serve` is a static http server anywhere you need one. [homebrew]: https://brew.sh/ [git]: https://git-scm.com/ [golang]: https://golang.org/ [releases]: https://github.com/syntaqx/serve/releases [modules]: https://github.com/golang/go/wiki/Modules [docker-hub]: https://hub.docker.com/r/syntaqx/serve [![Mentioned in Awesome Go](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go) [![codecov](https://codecov.io/gh/syntaqx/serve/branch/main/graph/badge.svg?token=FGkU1ntp8z)](https://codecov.io/gh/syntaqx/serve) [![Go Report Card](https://goreportcard.com/badge/github.com/syntaqx/serve)](https://goreportcard.com/report/github.com/syntaqx/serve) [![Go Reference](https://pkg.go.dev/badge/github.com/syntaqx/serve.svg)](https://pkg.go.dev/github.com/syntaqx/serve) [![GitHub Release](https://img.shields.io/github/release-pre/syntaqx/serve.svg)][releases] [![Docker Pulls](https://img.shields.io/docker/pulls/syntaqx/serve.svg)][docker-hub] > 🚨 The `main` branch is currently in active R&D for the next release of `serve`. > To use `serve`, please be sure to download a previous [release](https://github.com/syntaqx/serve/releases) as no stability guarantees > are being made further progress has been made towards a release candidate. ## TL;DR > It's basically `python -m SimpleHTTPServer 8080` written in Go, because who > can remember that many letters? ### Features * HTTPS (TLS) * CORS support * Request logging * `net/http` compatible * Support for [BasicAuth](https://en.wikipedia.org/wiki/Basic_access_authentication) via `users.json` ## Installation `serve` can be installed in a handful of ways: ### Homebrew on macOS If you are using [Homebrew][] on macOS, you can install `serve` with the following command: ```sh brew install syntaqx/tap/serve ``` ### Docker The official [syntaqx/serve][docker-hub] image is available on Docker Hub. To get started, try hosting a directory from your docker host: ```sh docker run -v .:/var/www:ro -d syntaqx/serve ``` Alternatively, a simple `Dockerfile` can be used to generate a new image that includes the necessary content: ```dockerfile FROM syntaqx/serve COPY . /var/www ``` Place this in the same directory as your content, then `build` and `run` the container: ```sh docker build -t some-content-serve . docker run --name some-serve -d some-content-serve ``` #### Exposing an external port ```sh docker run --name some-serve -d -p 8080:8080 some-content-serve ``` Then you can navigate to http://localhost:8080/ or http://host-ip:8080/ in your browser. #### Using environment variables for configuration [12-factor-config]: https://12factor.net/config Currently, `serve` only supports using the `PORT` environment variable for setting the listening port. All other configurations are available as CLI flags. > In future releases, most configurations will be settable from both the CLI > flag as well as a compatible environment variable, aligning with the > expectations of a [12factor app][12-factor-config]. But, that will require a > fair amount of work before the functionality is made available. Here's an example using `compose.yml` to configure `serve` to use HTTPS: ```yaml version: '3' services: web: image: syntaqx/serve volumes: - ./static:/var/www - ./fixtures:/etc/ssl environment: - PORT=1234 ports: - 1234 command: serve -ssl -cert=/etc/ssl/cert.pem -key=/etc/ssl/key.pem -dir=/var/www ``` The project repository provides an example [compose](./compose.yml) that implements a variety of common use-cases for `serve`. Feel free to use those to help you get started. ### Download the binary Quickly download install the latest release: ```sh curl -sfL https://install.goreleaser.com/github.com/syntaqx/serve.sh | sh ``` Or manually download the [latest release][releases] binary for your system and architecture and install it into your `$PATH`. ### From source To build from source, check out the instructions on getting started with [development](#development). ## Usage ```sh serve [options] [path] ``` > `[path]` defaults to `.` (relative path to the current directory) Then simply open your browser to http://localhost:8080 to view your server. ### Options The following configuration options are available: * `--host` host address to bind to (defaults to `0.0.0.0`) * `--port` listening port (defaults to `8080`) * `--ssl` enable https (defaults to `false`) * `--cert` path to the ssl cert file (defaults to `cert.pem`) * `--key` path to the ssl key file (defaults to `key.pem`) * `--dir` directory path to serve (defaults to `.`, also configurable by `arg[0]`) * `--users` path to users file (defaults to `users.dat`); file should contain lines of username:password in plain text ## Development To develop `serve` or interact with its source code in any meaningful way, be sure you have the following installed: ### Prerequisites * [Git][git] * [Go][golang] ### Install You can download and install the project from GitHub by simply running: ```sh git clone git@github.com:syntaqx/serve.git && cd $(basename $_ .git) make install ``` This will install `serve` into your `$GOPATH/bin` directory, which assuming is properly appended to your `$PATH`, can now be used: ```sh $ serve version serve version v0.0.6-8-g5074d63 windows/amd64 ``` ## Using `serve` manually Besides running `serve` using the provided binary, you can also embed a `serve.FileServer` into your own Go program: ```go package main import ( "log" "net/http" "github.com/syntaqx/serve" ) func main() { fs := serve.NewFileServer() log.Fatal(http.ListenAndServe(":8080", fs)) } ``` ## License [MIT]: https://opensource.org/licenses/MIT `serve` is open source software released under the [MIT license][MIT]. As with all Docker images, these likely also contain other software which may be under other licenses (such as Bash, etc from the base distribution, along with any direct or indirect dependencies of the primary software being contained). As for any pre-built image usage, it is the image user's responsibility to ensure that any use of this image complies with any relevant licenses for all software contained within. ================================================ FILE: cmd/serve/main.go ================================================ // Package main implements the runtime for the serve binary. package main import ( "flag" "log" "os" "github.com/syntaqx/serve/internal/commands" "github.com/syntaqx/serve/internal/config" ) var version = "0.0.0-develop" func main() { var opt config.Flags flag.BoolVar(&opt.Debug, "debug", false, "enable debug output") flag.StringVar(&opt.Host, "host", "", "host address to bind to") flag.StringVar(&opt.Port, "port", "8080", "listening port") flag.BoolVar(&opt.EnableSSL, "ssl", false, "enable https") flag.StringVar(&opt.CertFile, "cert", "cert.pem", "path to the ssl cert file") flag.StringVar(&opt.KeyFile, "key", "key.pem", "path to the ssl key file") flag.StringVar(&opt.Directory, "dir", "", "directory path to serve") flag.StringVar(&opt.UsersFile, "users", "users.dat", "path to users file") flag.Parse() log := log.New(os.Stderr, "[serve] ", log.LstdFlags) // Allow port to be configured via the environment variable PORT. // This is both better for configuration, and required for Heroku. if port, ok := os.LookupEnv("PORT"); ok { opt.Port = port } cmd := flag.Arg(0) dir, err := config.SanitizeDir(opt.Directory, cmd) if err != nil { log.Printf("sanitize directory: %v", err) os.Exit(1) } switch cmd { case "version": err = commands.Version(version, os.Stderr) default: err = commands.Server(log, opt, dir) } if err != nil { log.Printf("cmd.%s: %v", cmd, err) os.Exit(1) } } ================================================ FILE: compose.yml ================================================ services: # Note: You probably will want to remove the `build: .` lines if you copy # these into your project. That is used to be able to rebuild the image # directly in the project repsitory. basic: build: . image: syntaqx/serve volumes: - ./static:/var/www ports: - 8080:8080 basic_ssl: build: . image: syntaqx/serve volumes: - ./static:/var/www - ./fixtures:/etc/ssl ports: - 8888:8080 command: serve -ssl -cert=/etc/ssl/cert.pem -key=/etc/ssl/key.pem -dir=/var/www ================================================ FILE: examples/basic/main.go ================================================ package main import ( "log" "net/http" "github.com/syntaqx/serve" ) func main() { fs := serve.NewFileServer(serve.Options{ Directory: "../../static", }) log.Print("serve started at http://localhost:8080/") log.Fatal(http.ListenAndServe(":8080", fs)) } ================================================ FILE: fixtures/cert.pem ================================================ -----BEGIN CERTIFICATE----- MIIEIDCCAwigAwIBAgIUG4x9A3w/n65jwz3y7Wo8MDrU6QEwDQYJKoZIhvcNAQEL BQAweTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk5ZMREwDwYDVQQHDAhOZXcgWW9y azEVMBMGA1UECgwMRXhhbXBsZSwgTExDMRIwEAYDVQQDDAlzaXRlLnRlc3QxHzAd BgkqhkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20wHhcNMTkwMTE3MTA0NDM0WhcN MjAwMTE3MTA0NDM0WjB5MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTlkxETAPBgNV BAcMCE5ldyBZb3JrMRUwEwYDVQQKDAxFeGFtcGxlLCBMTEMxEjAQBgNVBAMMCXNp dGUudGVzdDEfMB0GCSqGSIb3DQEJARYQdGVzdEBleGFtcGxlLmNvbTCCASIwDQYJ KoZIhvcNAQEBBQADggEPADCCAQoCggEBALLlVETDAxfpbMrL9vlTKu2y+G8y7qNv KIdp5FllHAtZVPMis1xV9U4xvpy7baKTKKPtKEYZGcy/gW4fEN9KlvHZSUqrLj7T X0ySTNkwGItZy+gm1gbwvbQGtL4atgu0jPsJB662DIzq4dLL1OAFMV6VfmY9r2Hs ARhe0XjGtXKlX+Fyqnbxsot02C01CtFDcEftHR5KUZeUHkoIHmO+5ZtRAgAIfhV/ DQfyn+GfXOfM7PWGfy7RdyyLMrD+SwdfJFpkeeqQTi7p3PIIuHmieGOBjIOUhRv2 IEA7PbMNwoernE3Ey6iwErPjshWhSdLFG4NfAPs/KxDKe0qByRLOfZECAwEAAaOB nzCBnDAdBgNVHQ4EFgQUWlS44ZoMP/8IkJhHwxzJcfZ7IuIwHwYDVR0jBBgwFoAU WlS44ZoMP/8IkJhHwxzJcfZ7IuIwCQYDVR0TBAIwADALBgNVHQ8EBAMCBaAwFAYD VR0RBA0wC4IJc2l0ZS50ZXN0MCwGCWCGSAGG+EIBDQQfFh1PcGVuU1NMIEdlbmVy YXRlZCBDZXJ0aWZpY2F0ZTANBgkqhkiG9w0BAQsFAAOCAQEASQ/wPIrRSsIEewDg t6dehznWR+iBMGWGMpDEVw/IpRSN1zxLJp3i/4Yjcr98bEIP4tW27OODSJSKz11R 6/Kb/B04g3s7N4iSAehpeQXPGktNlgGojZSXi7u2y5ON6QBAle5csFxIkuOWDVwH qM/lsVlNHGyM0BGVMm5VLi2OWSqspz6Lr6yguT7U/AJ/hPe+YjSU5Kc+OnCZ4IH0 NcdVG5aPpDFeZ7c9v1uHa7b725lyXUYO8xfWR3QV6CsTLgRFWhwYBXF51sZbBBsr fu78txegVWnYau4uh/nytqPoOnjoP4BAMKlynPfIpJ9TLWxosWeXro2xY5zvdFkp XH/+0g== -----END CERTIFICATE----- ================================================ FILE: fixtures/key.pem ================================================ -----BEGIN PRIVATE KEY----- MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCy5VREwwMX6WzK y/b5UyrtsvhvMu6jbyiHaeRZZRwLWVTzIrNcVfVOMb6cu22ikyij7ShGGRnMv4Fu HxDfSpbx2UlKqy4+019MkkzZMBiLWcvoJtYG8L20BrS+GrYLtIz7CQeutgyM6uHS y9TgBTFelX5mPa9h7AEYXtF4xrVypV/hcqp28bKLdNgtNQrRQ3BH7R0eSlGXlB5K CB5jvuWbUQIACH4Vfw0H8p/hn1znzOz1hn8u0XcsizKw/ksHXyRaZHnqkE4u6dzy CLh5onhjgYyDlIUb9iBAOz2zDcKHq5xNxMuosBKz47IVoUnSxRuDXwD7PysQyntK gckSzn2RAgMBAAECggEBAIJ5/q80KHJtPnrermAER6AcU1QPKrwq271//xswQncI jYvTeEvVKdgBMgvwK7NSb2a4FxKhRg7ucgEWSWECbvsvxmPeXBlYYv5fCguyJ4Sj VrQYdyuStFm0Nmkc5D+/TL/fQyoq/xZcTZ5IKhfF0c8xa4I4ZU0fK2FR7qePDlHx kAjInhIAPxCh7vhKk35duhr8r7IDQ33jVyPQ7DgsEIKRh85CVxkcwrtV1sY3LM/O xmrYWxHzpke06qZBJROjAFKv1kV7NT3eKzgKg16yDkFqYdh38RnFsTB6/zgZ+rko Jj23tynefYRx3e3feAvhnDQzY32HwKCA4fNm0brJrf0CgYEA7cdXzN0QLwvhvjem t0gNdcfk0f9pM0wcYh0n7ESANsKAkjAOBqlvJ6tRV1LaqeIX+y1yeBnUIVH+dNfA tM2nTiilvaasR1Er40c3eeyIhWJ8nC+wBGexxDg3Ys4B0azzcakCYkG6BuVdsAWD aYdqWf6Tl80l7HwonCVFsu8nX+cCgYEAwJrX3agdZWTuAcFcdGIXWK1m8+4yGv6t fvwh9X/rkDQHJ5HXDsHmTc8yh/Qa35OzcZJxBooW5azmzVpEbgE/HjnBpNDjp0VT Xk5k+bZkWgp6wN8BFrh2Me8hliRs93vsUZ+fnFJWgxMTPMpOvhcw9YjucG6lGpwk ynGkJ0/bZ8cCgYAs8hVioBbDDdfqANL+qhwBO3vBRio4jBaBZUl6m6gwsatj9rlw AO8F7Jg/jWXP3vDxhbGxihBTDBCxPWcrxgPt/jj2FF9US7+kAn42CcP0kp1DWLBI 5ODxWj796jrly29o+K1+rTXgv9Jpx2EDvZkY0cpMU3brsLxsZ485N4OV2QKBgQCV G0rinrOjO2/GjBs3Pnk0fYmmblD79Q37sNXZaR7ElIK1b4I+On5A3pcQCTqEu6O/ 2M8HcQAo7qH/eFJhlzV2AOCY595WMKVJ7QbfCwTFcDd3+Syumj9miOpHgguZzKY2 yoyWSGgRMUNDXJt5LhsI+ukcwYuv/hG9aBzdEkWZIQKBgGLj5nwaJZWPJ381adJX JhwQcnS7cZIKrAifCay1oOaOcdQq/07QdBEjR6YT/X7oZCPtiDOdat9vzWKLNEY/ nYY+XFijSz2CKvT+CScjJSxmrsCtiNBQRtaTSKWAcgCpSqN5S+mocWmInZBVtZev 1OueDMUyPAsCabIR4HiTgAIs -----END PRIVATE KEY----- ================================================ FILE: go.mod ================================================ module github.com/syntaqx/serve go 1.20 require github.com/stretchr/testify v1.11.1 require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: go.sum ================================================ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: internal/commands/server.go ================================================ package commands import ( "bufio" "io" "log" "net" "net/http" "os" "strings" "time" "github.com/syntaqx/serve" "github.com/syntaqx/serve/internal/config" "github.com/syntaqx/serve/internal/middleware" ) var getHTTPServerFunc = GetStdHTTPServer // HTTPServer defines a returnable interface type for http.Server type HTTPServer interface { ListenAndServe() error ListenAndServeTLS(certFile, keyFile string) error } // GetStdHTTPServer returns a standard net/http.Server configured for a given // address and handler, and other sane defaults. func GetStdHTTPServer(addr string, h http.Handler) HTTPServer { return &http.Server{ Addr: addr, Handler: h, ReadTimeout: 15 * time.Second, WriteTimeout: 15 * time.Second, ReadHeaderTimeout: 60 * time.Second, } } // GetAuthUsers returns a map of users from a given io.Reader func GetAuthUsers(r io.Reader) map[string]string { users := make(map[string]string) if r != nil { scanner := bufio.NewScanner(r) for scanner.Scan() { if line := strings.Split(scanner.Text(), ":"); len(line) == 2 { // use only if correct format users[line[0]] = line[1] } } if err := scanner.Err(); err != nil { log.Fatalf("error occurred during reading users file") } } return users } // Server implements the static http server command. func Server(log *log.Logger, opt config.Flags, dir string) error { fs := serve.NewFileServer(serve.Options{ Directory: dir, }) // Authorization var f io.Reader if _, err := os.Stat(opt.UsersFile); !os.IsNotExist(err) { // Config file exists, load data f, err = os.Open(opt.UsersFile) if err != nil { log.Fatalf("unable to open users file %s", opt.UsersFile) } } else if opt.Debug { log.Printf("%s does not exist, authentication skipped", opt.UsersFile) } fs.Use( middleware.Logger(log), middleware.Recover(), middleware.CORS(), middleware.Auth(GetAuthUsers(f)), ) addr := net.JoinHostPort(opt.Host, opt.Port) server := getHTTPServerFunc(addr, fs) if opt.EnableSSL { log.Printf("https server listening at %s", addr) return server.ListenAndServeTLS(opt.CertFile, opt.KeyFile) } log.Printf("http server listening at %s", addr) return server.ListenAndServe() } ================================================ FILE: internal/commands/server_test.go ================================================ package commands import ( "bytes" "log" "net/http" "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/syntaqx/serve/internal/config" "github.com/syntaqx/serve/mock" ) func getMockHTTPServerFunc(shouldError bool) func(addr string, h http.Handler) HTTPServer { return func(addr string, h http.Handler) HTTPServer { return &mock.HTTPServer{ShouldError: shouldError} } } func TestGetStdHTTPServer(t *testing.T) { _, ok := GetStdHTTPServer("", http.DefaultServeMux).(*http.Server) assert.True(t, ok) } func TestServer(t *testing.T) { getHTTPServerFunc = getMockHTTPServerFunc(false) assert := assert.New(t) var b bytes.Buffer log := log.New(&b, "[test] ", 0) opt := config.Flags{} assert.NoError(Server(log, opt, ".")) assert.Contains(b.String(), "http server listening at") getHTTPServerFunc = GetStdHTTPServer } func TestServerErr(t *testing.T) { getHTTPServerFunc = getMockHTTPServerFunc(true) assert := assert.New(t) var b bytes.Buffer log := log.New(&b, "[test] ", 0) opt := config.Flags{} time.Sleep(200 * time.Millisecond) assert.Error(Server(log, opt, ".")) time.Sleep(200 * time.Millisecond) getHTTPServerFunc = GetStdHTTPServer } func TestServerHTTPS(t *testing.T) { getHTTPServerFunc = getMockHTTPServerFunc(false) assert := assert.New(t) var b bytes.Buffer log := log.New(&b, "[test] ", 0) opt := config.Flags{ EnableSSL: true, CertFile: "../../fixtures/cert.pem", KeyFile: "../../fixtures/key.pem", } assert.NoError(Server(log, opt, ".")) assert.Contains(b.String(), "https server listening at") getHTTPServerFunc = GetStdHTTPServer } func TestGetAuthUsers(t *testing.T) { tests := []struct { input string output map[string]string }{ { // Single user "user1:pass1", map[string]string{ "user1": "pass1", }, }, { // Multiple users "user1:pass1\nuser2:pass2", map[string]string{ "user1": "pass1", "user2": "pass2", }, }, { // Empty file "", map[string]string{}, }, { // Incorrect structure "user1:pass1:field1", map[string]string{}, }, } for _, test := range tests { mockFile := strings.NewReader(test.input) assert.Equal(t, GetAuthUsers(mockFile), test.output) } } ================================================ FILE: internal/commands/version.go ================================================ package commands import ( "fmt" "io" "runtime" ) // Version implements the command `version` which outputs the current binary // release version, if any. func Version(version string, w io.Writer) error { fmt.Fprintf(w, "serve version %s %s/%s\n", version, runtime.GOOS, runtime.GOARCH) return nil } ================================================ FILE: internal/commands/version_test.go ================================================ package commands import ( "bytes" "testing" "github.com/stretchr/testify/assert" ) func TestVersion(t *testing.T) { t.Parallel() assert := assert.New(t) var b bytes.Buffer err := Version("mock", &b) assert.NoError(err) assert.Contains(b.String(), "version mock") } ================================================ FILE: internal/config/flags.go ================================================ package config import ( "fmt" "os" ) var getwd = os.Getwd // Flags are the expose configuration flags available to the serve binary. type Flags struct { Debug bool Host string Port string EnableSSL bool CertFile string KeyFile string Directory string UsersFile string } // SanitizeDir allows a directory source to be set from multiple values. If any // value is defined, that value is used. If none are defined, the current // working directory is retrieved. func SanitizeDir(dirs ...string) (string, error) { for _, dir := range dirs { if len(dir) > 0 { return dir, nil } } cwd, err := getwd() if err != nil { return "", fmt.Errorf("cannot determine cwd: %v", err) } return cwd, nil } ================================================ FILE: internal/config/flags_test.go ================================================ package config import ( "errors" "os" "testing" "github.com/stretchr/testify/assert" ) func TestSanitizeDir(t *testing.T) { t.Parallel() assert := assert.New(t) cwd, err := os.Getwd() assert.NoError(err) var tests = []struct { dirs []string expected string }{ {[]string{"foo", "bar"}, "foo"}, {[]string{"", "bar"}, "bar"}, {[]string{"", ""}, cwd}, } for _, tt := range tests { tt := tt t.Run("", func(t *testing.T) { t.Parallel() dir, err := SanitizeDir(tt.dirs...) assert.Equal(tt.expected, dir) assert.NoError(err) }) } } func TestSanitizeDirCwdErr(t *testing.T) { assert := assert.New(t) getwd = func() (string, error) { return "", errors.New("mock") } dir, err := SanitizeDir() assert.Empty(dir) assert.Error(err) getwd = os.Getwd } ================================================ FILE: internal/middleware/auth.go ================================================ package middleware import "net/http" // Auth sets basic HTTP authorization func Auth(users map[string]string) func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { // Only require auth if we have any users if len(users) > 0 { authUser, authPass, ok := r.BasicAuth() if !ok { // No username/password received w.Header().Set("WWW-Authenticate", "Basic realm=Authenticate") w.WriteHeader(http.StatusUnauthorized) } else { if pass, ok := users[authUser]; ok { // User exists if pass == authPass { // Authentication successful next.ServeHTTP(w, r) } else { http.Error(w, "Incorrect login details", http.StatusUnauthorized) return } } else { http.Error(w, "Incorrect login details", http.StatusUnauthorized) return } } } else { next.ServeHTTP(w, r) } } return http.HandlerFunc(fn) } } ================================================ FILE: internal/middleware/auth_test.go ================================================ package middleware import ( "net/http" "net/http/httptest" "testing" "github.com/stretchr/testify/assert" ) func TestAuth(t *testing.T) { t.Parallel() assert := assert.New(t) req, err := http.NewRequest(http.MethodGet, "/", nil) assert.NoError(err) res := httptest.NewRecorder() // No users testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) Auth(nil)(testHandler).ServeHTTP(res, req) assert.Equal("", res.Header().Get("WWW-Authenticate")) // Some users testUsers := map[string]string{ "user1": "pass1", "user2": "pass2", } Auth(testUsers)(testHandler).ServeHTTP(res, req) assert.Equal("Basic realm=Authenticate", res.Header().Get("WWW-Authenticate")) assert.Equal(http.StatusUnauthorized, res.Result().StatusCode) // Correct password // Recreate new environment testHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) req, err = http.NewRequest(http.MethodGet, "/", nil) assert.NoError(err) res = httptest.NewRecorder() req.SetBasicAuth("user1", "pass1") Auth(testUsers)(testHandler).ServeHTTP(res, req) assert.Equal("", res.Header().Get("WWW-Authenticate")) assert.Equal(http.StatusOK, res.Result().StatusCode) // Incorrect password // Recreate new environment testHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) req, err = http.NewRequest(http.MethodGet, "/", nil) assert.NoError(err) res = httptest.NewRecorder() req.SetBasicAuth("user1", "pass2") Auth(testUsers)(testHandler).ServeHTTP(res, req) assert.Equal(http.StatusUnauthorized, res.Result().StatusCode) } ================================================ FILE: internal/middleware/cors.go ================================================ package middleware import ( "net/http" "strings" ) // CORS sets permissive cross-origin resource sharing rules. func CORS() func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", strings.Join([]string{ http.MethodHead, http.MethodOptions, http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete, }, ", ")) next.ServeHTTP(w, r) } return http.HandlerFunc(fn) } } ================================================ FILE: internal/middleware/cors_test.go ================================================ package middleware import ( "net/http" "net/http/httptest" "testing" "github.com/stretchr/testify/assert" ) func TestCORS(t *testing.T) { t.Parallel() assert := assert.New(t) req, err := http.NewRequest(http.MethodGet, "/", nil) assert.NoError(err) res := httptest.NewRecorder() testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) CORS()(testHandler).ServeHTTP(res, req) assert.Equal("*", res.Header().Get("Access-Control-ALlow-Origin")) assert.Contains(res.Header().Get("Access-Control-Allow-Methods"), http.MethodGet) } ================================================ FILE: internal/middleware/logger.go ================================================ package middleware import ( "log" "net/http" ) // Logger is a middleware that logs each request, along with some useful data // about what was requested, and what the response was. func Logger(log *log.Logger) func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { sw := statusWriter{ResponseWriter: w} defer func() { log.Println(r.Method, r.URL.Path, sw.status, r.RemoteAddr, r.UserAgent()) }() next.ServeHTTP(&sw, r) } return http.HandlerFunc(fn) } } ================================================ FILE: internal/middleware/logger_test.go ================================================ package middleware import ( "bytes" "log" "net/http" "net/http/httptest" "strings" "testing" "github.com/stretchr/testify/assert" ) var logTests = []struct { in func(w http.ResponseWriter, r *http.Request) out string }{ { in: func(w http.ResponseWriter, _ *http.Request) { _, err := w.Write([]byte{}) if err != nil { panic(err) } }, out: "[test] GET / 200", }, { in: func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNotFound) }, out: "[test] GET / 404", }, } func TestLogger(t *testing.T) { t.Parallel() for _, tt := range logTests { assert := assert.New(t) var b bytes.Buffer log := log.New(&b, "[test] ", 0) req, err := http.NewRequest(http.MethodGet, "/", nil) assert.NoError(err) res := httptest.NewRecorder() testHandler := http.HandlerFunc(tt.in) Logger(log)(testHandler).ServeHTTP(res, req) assert.Equal(tt.out, strings.TrimSpace(b.String())) } } ================================================ FILE: internal/middleware/recover.go ================================================ package middleware import ( "fmt" "net/http" ) // Recover is a middleware that recovers from panics that occur for a request. func Recover() func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { defer func() { if err := recover(); err != nil { http.Error(w, fmt.Sprintf("[PANIC RECOVERED] %v", err), http.StatusInternalServerError) } }() next.ServeHTTP(w, r) } return http.HandlerFunc(fn) } } ================================================ FILE: internal/middleware/recover_test.go ================================================ package middleware import ( "net/http" "net/http/httptest" "strings" "testing" "github.com/stretchr/testify/assert" ) func TestRecover(t *testing.T) { t.Parallel() assert := assert.New(t) req, err := http.NewRequest(http.MethodGet, "/", nil) assert.NoError(err) res := httptest.NewRecorder() testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { panic("test") }) Recover()(testHandler).ServeHTTP(res, req) assert.Equal(http.StatusInternalServerError, res.Code) assert.Equal("[PANIC RECOVERED] test", strings.TrimSpace(res.Body.String())) } ================================================ FILE: internal/middleware/statuswriter.go ================================================ package middleware import "net/http" type statusWriter struct { http.ResponseWriter status int } func (w *statusWriter) WriteHeader(status int) { w.status = status w.ResponseWriter.WriteHeader(status) } func (w *statusWriter) Write(b []byte) (int, error) { if w.status == 0 { w.status = http.StatusOK } return w.ResponseWriter.Write(b) } ================================================ FILE: mock/http.go ================================================ package mock import "errors" // ErrMock is a mock error var ErrMock = errors.New("mock error") // HTTPServer is a mock http server type HTTPServer struct { ShouldError bool } // ListenAndServe is a mock http server method func (s *HTTPServer) ListenAndServe() error { if s.ShouldError { return ErrMock } return nil } // ListenAndServeTLS is a mock http server method func (s *HTTPServer) ListenAndServeTLS(certFile, keyFile string) error { if s.ShouldError { return ErrMock } return nil } ================================================ FILE: serve.go ================================================ // Package serve provides a static http server anywhere you need one. package serve import "net/http" // Options is a struct for specifying configuration options for a FileServer. type Options struct { // Directory is the root directory from which to serve files. Directory string // Prefix is a filepath prefix that should be ignored by the FileServer. Prefix string } // FileServer wraps an http.FileServer. type FileServer struct { opt Options handler http.Handler } // NewFileServer initializes a FileServer. func NewFileServer(options ...Options) *FileServer { var opt Options if len(options) > 0 { opt = options[0] } fs := &FileServer{ opt: opt, } fs.handler = http.StripPrefix(opt.Prefix, http.FileServer(http.Dir(opt.Directory))) return fs } // Use wraps the Handler with middleware(s). func (fs *FileServer) Use(mws ...func(http.Handler) http.Handler) { for _, h := range mws { fs.handler = h(fs.handler) } } // ServeHTTP implements the net/http.Handler interface. func (fs *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { fs.handler.ServeHTTP(w, r) } ================================================ FILE: serve_test.go ================================================ package serve import ( "net/http" "net/http/httptest" "testing" "github.com/stretchr/testify/assert" ) func TestFileServerDefaults(t *testing.T) { fs := NewFileServer() _ = fs } func TestFileServerOptions(t *testing.T) { fs := NewFileServer(Options{Directory: "test"}) _ = fs } func TestFileServerUse(t *testing.T) { t.Parallel() assert := assert.New(t) req, err := http.NewRequest(http.MethodGet, "/", nil) assert.NoError(err) res := httptest.NewRecorder() testMiddleware1 := func(next http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte("start\n")) next.ServeHTTP(w, r) } return http.HandlerFunc(fn) } testMiddleware2 := func(next http.Handler) http.Handler { fn := func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte("end\n")) } return http.HandlerFunc(fn) } fs := &FileServer{ handler: http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) { t.Fail() }), } fs.Use(testMiddleware2, testMiddleware1) fs.ServeHTTP(res, req) assert.Equal("start\nend\n", res.Body.String()) } func TestFileServerServeHTTP(t *testing.T) { t.Parallel() assert := assert.New(t) req, err := http.NewRequest(http.MethodGet, "/", nil) assert.NoError(err) res := httptest.NewRecorder() fs := &FileServer{ handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte("expected")) }), } fs.ServeHTTP(res, req) assert.Equal("expected", res.Body.String()) } ================================================ FILE: static/index.html ================================================ Welcome to your server!

serve

Your web server is working correctly!

You can edit this file at /var/www/index.html