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 <syntaqx@gmail.com>
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
================================================
# <img src="https://raw.githubusercontent.com/syntaqx/serve/main/docs/logo.svg?sanitize=true" width="250">
`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
[](https://github.com/avelino/awesome-go)
[](https://codecov.io/gh/syntaqx/serve)
[](https://goreportcard.com/report/github.com/syntaqx/serve)
[](https://pkg.go.dev/github.com/syntaqx/serve)
[][releases]
[][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
================================================
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Welcome to your server!</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Mukta">
<style>
*, ::after, ::before {
box-sizing: border-box;
}
html, body {
background-color: #eee;
margin: 0;
height: 100vh;
}
body {
font-family: 'Mukta', sans-serif;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #656f79;
}
a {
text-decoration: none;
color: #5ba7ce;
}
a:hover {
text-decoration: underline;
}
h1, h2, h3, h4, h5, h6 {
margin-top: 0;
margin-bottom: .5rem;
line-height: 1.2;
}
h1 {
font-size: 2.5rem;
}
h2 {
font-size: 1.8rem;
}
h3 {
font-size: 1.75rem;
}
.vh-100 {
height: 100vh;
}
.mb-2 {
margin-bottom: 2rem;
}
.flex-center {
align-items: center;
justify-content: center;
display: flex;
}
.position-rel {
position: relative;
}
.text-center {
text-align: center;
}
.title {
margin-top: 2rem;
letter-spacing: -.2rem;
font-weight: 100;
font-size: 4rem;
color: #363a3f;
}
.links {
margin: 0;
padding: 0;
list-style: none;
}
.links li {
margin: 0 .5rem 0 0;
padding: 0;
display: inline-block;
}
</style>
</head>
<body>
<div class="flex-center position-rel vh-100">
<div class="text-center">
<header class="mb-2">
<img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgMTYuMSA4LjgiPjxkZWZzPjxzdHlsZT4uY2xzLTF7ZmlsbDp1cmwoI2xpbmVhci1ncmFkaWVudCk7fTwvc3R5bGU+PGxpbmVhckdyYWRpZW50IGlkPSJsaW5lYXItZ3JhZGllbnQiIHkxPSI4LjgiIHgyPSIxNCIgeTI9IjguOCIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgxLCAwLCAwLCAtMSwgMCwgMTMuMikiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiMwMDAwOTIiIC8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjZmYwMGYzIiAvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjx0aXRsZT5Bc3NldCAyPC90aXRsZT48ZyBpZD0iTGF5ZXJfMiIgZGF0YS1uYW1lPSJMYXllciAyIj48ZyBpZD0iTGF5ZXJfMS0yIiBkYXRhLW5hbWU9IkxheWVyIDEiPjxwYXRoIGNsYXNzPSJjbHMtMSIgZD0iTTgsMEExLjU0LDEuNTQsMCwwLDAsNi41LDEuNWExLjM1LDEuMzUsMCwwLDAsMCwuMTgsNi41LDYuNSwwLDAsMC01LDYuMTJILjZhLjU2LjU2LDAsMCwwLS42LjUuNTYuNTYsMCwwLDAsLjYuNUgxNS40Yy4zLDAsLjctLjIuNy0uNWEuNTYuNTYsMCwwLDAtLjYtLjVoLTFhNi41LDYuNSwwLDAsMC01LTYuMTIsMSwxLDAsMCwwLDAtLjE4QTEuNTQsMS41NCwwLDAsMCw4LDBaTTgsMWEuNDcuNDcsMCwwLDEsLjUuNXYwYTQuMTgsNC4xOCwwLDAsMC0xLDB2MEEuNDcuNDcsMCwwLDEsOCwxWk04LDIuNWE1LjUxLDUuNTEsMCwwLDEsNS40OSw1LjNoLTExQTUuNTEsNS41MSwwLDAsMSw4LDIuNVptLS44OS43OGEuMzkuMzksMCwwLDAtLjIxLDBBNC44OCw0Ljg4LDAsMCwwLDMuMyw2LjlhLjU1LjU1LDAsMCwwLC40LjZoLjFhLjQzLjQzLDAsMCwwLC40LS40LDQuMTksNC4xOSwwLDAsMSwzLTIuOS40OS40OSwwLDAsMCwuMy0uNkEuNTMuNTMsMCwwLDAsNy4xMSwzLjI4WiIgLz48L2c+PC9nPjwvc3ZnPg==" width="250">
<h1 class="title">serve</h1>
<h2>Your web server is working correctly!</h2>
</header>
<p>You can edit this file at <code>/var/www/index.html</code></p>
<ul class="links">
<li><a href="https://github.com/syntaqx/serve/tree/main/examples">Examples</a></li>
<li><a href="https://github.com/syntaqx/serve">GitHub</a></li>
<li><a href="https://hub.docker.com/r/syntaqx/serve">Docker Hub</a></li>
</ul>
</div>
</div>
</body>
</html>
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
SYMBOL INDEX (41 symbols across 20 files)
FILE: cmd/serve/main.go
function main (line 15) | func main() {
FILE: examples/basic/main.go
function main (line 10) | func main() {
FILE: internal/commands/server.go
type HTTPServer (line 21) | type HTTPServer interface
function GetStdHTTPServer (line 28) | func GetStdHTTPServer(addr string, h http.Handler) HTTPServer {
function GetAuthUsers (line 39) | func GetAuthUsers(r io.Reader) map[string]string {
function Server (line 59) | func Server(log *log.Logger, opt config.Flags, dir string) error {
FILE: internal/commands/server_test.go
function getMockHTTPServerFunc (line 16) | func getMockHTTPServerFunc(shouldError bool) func(addr string, h http.Ha...
function TestGetStdHTTPServer (line 22) | func TestGetStdHTTPServer(t *testing.T) {
function TestServer (line 27) | func TestServer(t *testing.T) {
function TestServerErr (line 42) | func TestServerErr(t *testing.T) {
function TestServerHTTPS (line 59) | func TestServerHTTPS(t *testing.T) {
function TestGetAuthUsers (line 79) | func TestGetAuthUsers(t *testing.T) {
FILE: internal/commands/version.go
function Version (line 11) | func Version(version string, w io.Writer) error {
FILE: internal/commands/version_test.go
function TestVersion (line 10) | func TestVersion(t *testing.T) {
FILE: internal/config/flags.go
type Flags (line 11) | type Flags struct
function SanitizeDir (line 25) | func SanitizeDir(dirs ...string) (string, error) {
FILE: internal/config/flags_test.go
function TestSanitizeDir (line 11) | func TestSanitizeDir(t *testing.T) {
function TestSanitizeDirCwdErr (line 38) | func TestSanitizeDirCwdErr(t *testing.T) {
FILE: internal/middleware/auth.go
function Auth (line 6) | func Auth(users map[string]string) func(next http.Handler) http.Handler {
FILE: internal/middleware/auth_test.go
function TestAuth (line 11) | func TestAuth(t *testing.T) {
FILE: internal/middleware/cors.go
function CORS (line 9) | func CORS() func(next http.Handler) http.Handler {
FILE: internal/middleware/cors_test.go
function TestCORS (line 11) | func TestCORS(t *testing.T) {
FILE: internal/middleware/logger.go
function Logger (line 10) | func Logger(log *log.Logger) func(next http.Handler) http.Handler {
FILE: internal/middleware/logger_test.go
function TestLogger (line 35) | func TestLogger(t *testing.T) {
FILE: internal/middleware/recover.go
function Recover (line 9) | func Recover() func(next http.Handler) http.Handler {
FILE: internal/middleware/recover_test.go
function TestRecover (line 12) | func TestRecover(t *testing.T) {
FILE: internal/middleware/statuswriter.go
type statusWriter (line 5) | type statusWriter struct
method WriteHeader (line 10) | func (w *statusWriter) WriteHeader(status int) {
method Write (line 15) | func (w *statusWriter) Write(b []byte) (int, error) {
FILE: mock/http.go
type HTTPServer (line 9) | type HTTPServer struct
method ListenAndServe (line 14) | func (s *HTTPServer) ListenAndServe() error {
method ListenAndServeTLS (line 22) | func (s *HTTPServer) ListenAndServeTLS(certFile, keyFile string) error {
FILE: serve.go
type Options (line 7) | type Options struct
type FileServer (line 16) | type FileServer struct
method Use (line 37) | func (fs *FileServer) Use(mws ...func(http.Handler) http.Handler) {
method ServeHTTP (line 44) | func (fs *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
function NewFileServer (line 22) | func NewFileServer(options ...Options) *FileServer {
FILE: serve_test.go
function TestFileServerDefaults (line 11) | func TestFileServerDefaults(t *testing.T) {
function TestFileServerOptions (line 16) | func TestFileServerOptions(t *testing.T) {
function TestFileServerUse (line 21) | func TestFileServerUse(t *testing.T) {
function TestFileServerServeHTTP (line 57) | func TestFileServerServeHTTP(t *testing.T) {
Condensed preview — 42 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (54K chars).
[
{
"path": ".dockerignore",
"chars": 174,
"preview": "*.yml\n*.yaml\n*.json\n*.md\n\n.git*\nbin\ndist\nbuild\ndocs\nexamples\ntmp\nvendor\n\n.editorconfig\nDockerfile\nCODEOWNERS\nLICENSE\nMak"
},
{
"path": ".editorconfig",
"chars": 423,
"preview": "root = true\n\n[*]\ncharset = utf-8\nend_of_line = lf\nindent_style = space\nmax_line_length = 120\ninsert_final_newline = true"
},
{
"path": ".gitattributes",
"chars": 12,
"preview": "* text=auto\n"
},
{
"path": ".github/dependabot.yml",
"chars": 183,
"preview": "version: 2\nupdates:\n- package-ecosystem: github-actions\n directory: \"/\"\n schedule:\n interval: weekly\n- package-ecos"
},
{
"path": ".github/workflows/codeql.yml",
"chars": 3002,
"preview": "# For most projects, this workflow file will not need changing; you simply need\n# to commit it to your repository.\n#\n# Y"
},
{
"path": ".github/workflows/dependabot.yml",
"chars": 1203,
"preview": "name: Dependabot Approve & Auto-Merge\non:\n pull_request:\n branches: [ main ]\n\npermissions:\n contents: write\n pull-"
},
{
"path": ".github/workflows/docker.yml",
"chars": 2200,
"preview": "name: Docker\n\non:\n workflow_call:\n inputs:\n push:\n description: \"Push the image to the registry\"\n "
},
{
"path": ".github/workflows/go.yml",
"chars": 535,
"preview": "name: Go\n\non:\n push:\n branches: [ main ]\n pull_request:\n branches: [ main ]\n\njobs:\n\n test:\n runs-on: ubuntu-"
},
{
"path": ".github/workflows/golangci-lint.yml",
"chars": 1430,
"preview": "name: golangci-lint\non:\n push:\n tags:\n - v*\n branches:\n - main\n pull_request:\n\npermissions:\n contents"
},
{
"path": ".github/workflows/release.yml",
"chars": 171,
"preview": "name: cd\n\non:\n push:\n tags:\n - v*\n workflow_dispatch:\n\njobs:\n\n docker:\n uses: ./.github/workflows/docker.y"
},
{
"path": ".gitignore",
"chars": 365,
"preview": "# 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 Da"
},
{
"path": "CONTRIBUTING.md",
"chars": 4354,
"preview": "# Contributing\n\n[semver]: http://semver.org/\n\nWhen contributing to this repository, please first discuss the change you "
},
{
"path": "Dockerfile",
"chars": 797,
"preview": "FROM golang:1.20-alpine AS builder\n\nARG VERSION=\"0.0.0-docker\"\n\nRUN apk add --update --no-cache \\\n ca-certificates tzda"
},
{
"path": "LICENSE",
"chars": 1094,
"preview": "The MIT License (MIT)\n\nCopyright (c) Chase Pierce <syntaqx@gmail.com>\n\nPermission is hereby granted, free of charge, to "
},
{
"path": "Makefile",
"chars": 218,
"preview": "VERSION=`git --no-pager describe --tags --always`\n\nLDFLAGS+=\nLDFLAGS+=-X main.version=${VERSION}\n\nbuild:\n\tgo build -ldfl"
},
{
"path": "README.md",
"chars": 6351,
"preview": "# <img src=\"https://raw.githubusercontent.com/syntaqx/serve/main/docs/logo.svg?sanitize=true\" width=\"250\">\n\n`serve` is a"
},
{
"path": "cmd/serve/main.go",
"chars": 1442,
"preview": "// 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/s"
},
{
"path": "compose.yml",
"chars": 548,
"preview": "services:\n\n # Note: You probably will want to remove the `build: .` lines if you copy\n # these into your project. That"
},
{
"path": "examples/basic/main.go",
"chars": 265,
"preview": "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"
},
{
"path": "fixtures/cert.pem",
"chars": 1493,
"preview": "-----BEGIN CERTIFICATE-----\nMIIEIDCCAwigAwIBAgIUG4x9A3w/n65jwz3y7Wo8MDrU6QEwDQYJKoZIhvcNAQEL\nBQAweTELMAkGA1UEBhMCVVMxCzA"
},
{
"path": "fixtures/key.pem",
"chars": 1704,
"preview": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCy5VREwwMX6WzK\ny/b5UyrtsvhvMu6jbyiHaeRZZRw"
},
{
"path": "go.mod",
"chars": 233,
"preview": "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-"
},
{
"path": "go.sum",
"chars": 883,
"preview": "github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.m"
},
{
"path": "internal/commands/server.go",
"chars": 2253,
"preview": "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"
},
{
"path": "internal/commands/server_test.go",
"chars": 2230,
"preview": "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/asse"
},
{
"path": "internal/commands/version.go",
"chars": 306,
"preview": "package commands\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"runtime\"\n)\n\n// Version implements the command `version` which outputs the curr"
},
{
"path": "internal/commands/version_test.go",
"chars": 279,
"preview": "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) "
},
{
"path": "internal/config/flags.go",
"chars": 734,
"preview": "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 "
},
{
"path": "internal/config/flags_test.go",
"chars": 801,
"preview": "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 *te"
},
{
"path": "internal/middleware/auth.go",
"chars": 1000,
"preview": "package middleware\n\nimport \"net/http\"\n\n// Auth sets basic HTTP authorization\nfunc Auth(users map[string]string) func(nex"
},
{
"path": "internal/middleware/auth_test.go",
"chars": 1600,
"preview": "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 "
},
{
"path": "internal/middleware/cors.go",
"chars": 623,
"preview": "package middleware\n\nimport (\n\t\"net/http\"\n\t\"strings\"\n)\n\n// CORS sets permissive cross-origin resource sharing rules.\nfunc"
},
{
"path": "internal/middleware/cors_test.go",
"chars": 570,
"preview": "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 "
},
{
"path": "internal/middleware/logger.go",
"chars": 565,
"preview": "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 use"
},
{
"path": "internal/middleware/logger_test.go",
"chars": 949,
"preview": "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/stretc"
},
{
"path": "internal/middleware/recover.go",
"chars": 511,
"preview": "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"
},
{
"path": "internal/middleware/recover_test.go",
"chars": 590,
"preview": "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/asser"
},
{
"path": "internal/middleware/statuswriter.go",
"chars": 351,
"preview": "package middleware\n\nimport \"net/http\"\n\ntype statusWriter struct {\n\thttp.ResponseWriter\n\tstatus int\n}\n\nfunc (w *statusWri"
},
{
"path": "mock/http.go",
"chars": 505,
"preview": "package mock\n\nimport \"errors\"\n\n// ErrMock is a mock error\nvar ErrMock = errors.New(\"mock error\")\n\n// HTTPServer is a moc"
},
{
"path": "serve.go",
"chars": 1112,
"preview": "// Package serve provides a static http server anywhere you need one.\npackage serve\n\nimport \"net/http\"\n\n// Options is a "
},
{
"path": "serve_test.go",
"chars": 1523,
"preview": "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 TestF"
},
{
"path": "static/index.html",
"chars": 3616,
"preview": "<!doctype html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, in"
}
]
About this extraction
This page contains the full source code of the syntaqx/serve GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 42 files (48.0 KB), approximately 15.7k tokens, and a symbol index with 41 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.