diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..cbf59f9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,14 @@ +blank_issues_enabled: false + +contact_links: + - name: Ask an question / advise on using simplehttpserver + url: https://github.com/projectdiscovery/simplehttpserver/discussions/categories/q-a + about: Ask a question or request support for using simplehttpserver + + - name: Share idea / feature to discuss for simplehttpserver + url: https://github.com/projectdiscovery/simplehttpserver/discussions/categories/ideas + about: Share idea / feature to discuss for simplehttpserver + + - name: Connect with PD Team (Discord) + url: https://discord.gg/projectdiscovery + about: Connect with PD Team for direct communication \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..7d33dd1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,18 @@ +--- +name: Feature request +about: Request feature to implement in this project +labels: 'Type: Enhancement' +--- + + + +### Please describe your feature request: + + +### Describe the use case of this feature: + diff --git a/.github/ISSUE_TEMPLATE/issue-report.md b/.github/ISSUE_TEMPLATE/issue-report.md new file mode 100644 index 0000000..d689b0f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/issue-report.md @@ -0,0 +1,36 @@ +--- +name: Issue report +about: Create a report to help us to improve the project +labels: 'Type: Bug' + +--- + + + + + +### simplehttpserver version: + + + + +### Current Behavior: + + +### Expected Behavior: + + +### Steps To Reproduce: + + + +### Anything else: + \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..69d9543 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,37 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + + # Maintain dependencies for GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + target-branch: "dev" + commit-message: + prefix: "chore" + include: "scope" + + # Maintain dependencies for go modules + - package-ecosystem: "gomod" + directory: "/" + schedule: + interval: "weekly" + target-branch: "dev" + commit-message: + prefix: "chore" + include: "scope" + + # Maintain dependencies for docker + - package-ecosystem: "docker" + directory: "/" + schedule: + interval: "weekly" + target-branch: "dev" + commit-message: + prefix: "chore" + include: "scope" \ No newline at end of file diff --git a/.github/feature_request.md b/.github/feature_request.md deleted file mode 100644 index 5a71fc0..0000000 --- a/.github/feature_request.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project -title: "[feature]" -labels: '' -assignees: '' - ---- - -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. diff --git a/.github/issue-report.md b/.github/issue-report.md deleted file mode 100644 index 691d1f4..0000000 --- a/.github/issue-report.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -name: Issue report -about: Create a report to help us improve -title: "[issue]" -labels: '' -assignees: '' - ---- - -**Describe the bug** -A clear and concise description of what the bug is. - -**Nuclei version** -Please share the version of the nuclei you are running with `simplehttpserver -version` - - -**Screenshot of the error or bug** -please add the screenshot showing bug or issue you are facing. diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml new file mode 100644 index 0000000..c9baa4e --- /dev/null +++ b/.github/workflows/build-test.yml @@ -0,0 +1,30 @@ +name: 🔨 Build Test + +on: + pull_request: + paths: + - '**.go' + - '**.mod' + workflow_dispatch: + + +jobs: + build: + name: Test Builds + runs-on: ubuntu-latest + steps: + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: 1.19 + + - name: Check out code + uses: actions/checkout@v3 + + - name: Test + run: go test . + working-directory: cmd/simplehttpserver/ + + - name: Build + run: go build . + working-directory: cmd/simplehttpserver/ \ No newline at end of file diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml deleted file mode 100644 index 008bca6..0000000 --- a/.github/workflows/build.yaml +++ /dev/null @@ -1,39 +0,0 @@ -name: Build -on: - push: - branches: - - master - pull_request: - -jobs: - golangci-lint: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v2 - - name: Run golangci-lint - uses: golangci/golangci-lint-action@v2.4.0 - with: - # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version. - version: v1.31 - args: --timeout 5m - - build: - name: Build - runs-on: ubuntu-latest - steps: - - name: Set up Go - uses: actions/setup-go@v2 - with: - go-version: 1.14 - - - name: Check out code - uses: actions/checkout@v2 - - - name: Test - run: go test . - working-directory: . - - - name: Build - run: go build . - working-directory: . diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..2863865 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,41 @@ +name: 🚨 CodeQL Analysis + +on: + workflow_dispatch: + pull_request: + paths: + - '**.go' + - '**.mod' + branches: + - dev + +jobs: + analyze: + name: Analyze + runs-on: 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' ] + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 \ No newline at end of file diff --git a/.github/workflows/dockerhub-push.yml b/.github/workflows/dockerhub-push.yml index b46f4e0..d141bff 100644 --- a/.github/workflows/dockerhub-push.yml +++ b/.github/workflows/dockerhub-push.yml @@ -1,17 +1,36 @@ -# dockerhub-push pushes docker build to dockerhub automatically -# on the creation of a new release -name: Publish to Dockerhub on creation of a new release -on: - release: - types: [published] +name: 🌥 Docker Push + +on: + workflow_run: + workflows: ["🎉 Release Binary"] + types: + - completed + workflow_dispatch: + jobs: - build: + docker: runs-on: ubuntu-latest steps: - - uses: actions/checkout@master - - name: Publish to Dockerhub Registry - uses: elgohr/Publish-Docker-Github-Action@master - with: - name: projectdiscovery/simplehttpserver - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} \ No newline at end of file + - + name: Checkout + uses: actions/checkout@v3 + - + name: Set up QEMU + uses: docker/setup-qemu-action@v2 + - + name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - + name: Login to DockerHub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + - + name: Build and push + uses: docker/build-push-action@v4 + with: + context: . + platforms: linux/amd64,linux/arm64,linux/arm + push: true + tags: projectdiscovery/simplehttpserver:latest \ No newline at end of file diff --git a/.github/workflows/lint-test.yml b/.github/workflows/lint-test.yml new file mode 100644 index 0000000..7f4c078 --- /dev/null +++ b/.github/workflows/lint-test.yml @@ -0,0 +1,26 @@ +name: 🙏🏻 Lint Test + +on: + pull_request: + paths: + - '**.go' + - '**.mod' + workflow_dispatch: + +jobs: + lint: + name: Lint Test + runs-on: ubuntu-latest + steps: + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: 1.19 + - name: Checkout code + uses: actions/checkout@v3 + - name: Run golangci-lint + uses: golangci/golangci-lint-action@v3.4.0 + with: + version: latest + args: --timeout 5m + working-directory: . \ No newline at end of file diff --git a/.github/workflows/release-binary.yml b/.github/workflows/release-binary.yml new file mode 100644 index 0000000..8581b0b --- /dev/null +++ b/.github/workflows/release-binary.yml @@ -0,0 +1,31 @@ +name: 🎉 Release Binary + +on: + push: + tags: + - v* + workflow_dispatch: + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: "Set up Go" + uses: actions/setup-go@v4 + with: + go-version: 1.19 + - name: "Check out code" + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: "Create release on GitHub" + uses: goreleaser/goreleaser-action@v4 + env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + SLACK_WEBHOOK: "${{ secrets.RELEASE_SLACK_WEBHOOK }}" + DISCORD_WEBHOOK_ID: "${{ secrets.DISCORD_WEBHOOK_ID }}" + DISCORD_WEBHOOK_TOKEN: "${{ secrets.DISCORD_WEBHOOK_TOKEN }}" + with: + args: "release --rm-dist" + version: latest + workdir: . \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 70cb60a..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Release -on: - create: - tags: - - v* - -jobs: - release: - runs-on: ubuntu-latest - steps: - - - name: "Check out code" - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - - name: "Set up Go" - uses: actions/setup-go@v2 - with: - go-version: 1.14 - - - env: - GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" - name: "Create release on GitHub" - uses: goreleaser/goreleaser-action@v2 - with: - args: "release --rm-dist" - version: latest \ No newline at end of file diff --git a/.gitignore b/.gitignore index dabd7a7..4f9c0f3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ *.exe -simplehttpserver +cmd/simplehttpserver/simplehttpserver diff --git a/.golangci.yml b/.golangci.yml deleted file mode 100644 index d5e9089..0000000 --- a/.golangci.yml +++ /dev/null @@ -1,122 +0,0 @@ -linters-settings: - dupl: - threshold: 100 - exhaustive: - default-signifies-exhaustive: false - # funlen: - # lines: 100 - # statements: 50 - goconst: - min-len: 2 - min-occurrences: 2 - gocritic: - enabled-tags: - - diagnostic - - experimental - - opinionated - - performance - - style - disabled-checks: - - dupImport # https://github.com/go-critic/go-critic/issues/845 - - ifElseChain - # gocyclo: - # min-complexity: 15 - goimports: - local-prefixes: github.com/golangci/golangci-lint - golint: - min-confidence: 0 - gomnd: - settings: - mnd: - # don't include the "operation" and "assign" - checks: argument,case,condition,return - govet: - check-shadowing: true - settings: - printf: - funcs: - - (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof - - (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf - - (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf - - (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf - # lll: - # line-length: 140 - maligned: - suggest-new: true - misspell: - locale: US - nolintlint: - allow-leading-space: true # don't require machine-readable nolint directives (i.e. with no leading space) - allow-unused: false # report any unused nolint directives - require-explanation: false # don't require an explanation for nolint directives - require-specific: false # don't require nolint directives to be specific about which linter is being skipped - -linters: - # please, do not use `enable-all`: it's deprecated and will be removed soon. - # inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint - disable-all: true - enable: - - bodyclose - - deadcode - - dogsled - - dupl - - errcheck - - exhaustive - - gochecknoinits - - goconst - - gocritic - - gofmt - - goimports - - golint - - gomnd - - goprintffuncname - - gosimple - - govet - - ineffassign - - interfacer - - maligned - - misspell - - nakedret - - noctx - - nolintlint - - rowserrcheck - - scopelint - - staticcheck - - structcheck - - stylecheck - - typecheck - - unconvert - - unparam - - unused - - varcheck - - whitespace - - # don't enable: - # - depguard - # - asciicheck - # - funlen - # - gochecknoglobals - # - gocognit - # - gocyclo - # - godot - # - godox - # - goerr113 - # - gosec - # - lll - # - nestif - # - prealloc - # - testpackage - # - wsl - -issues: - exclude-use-default: false - exclude: - # should have a package comment, unless it's in another file for this package (golint) - - 'in another file for this package' - -# golangci.com configuration -# https://github.com/golangci/golangci/wiki/Configuration -service: - golangci-lint-version: 1.31.x # use the fixed version to not introduce new linters unexpectedly - prepare: - - echo "here I can run custom commands, but no preparation needed for this repo" diff --git a/.goreleaser.yml b/.goreleaser.yml index 50bb74f..e0a4795 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,6 +1,10 @@ +before: + hooks: + - go mod tidy + builds: - binary: simplehttpserver - main: simplehttpserver.go + main: cmd/simplehttpserver/simplehttpserver.go goos: - linux - windows @@ -12,10 +16,20 @@ builds: - arm64 archives: - - id: tgz - format: tar.gz - replacements: - darwin: macOS - format_overrides: - - goos: windows - format: zip \ No newline at end of file +- format: zip + replacements: + darwin: macOS + +checksum: + algorithm: sha256 + +announce: + slack: + enabled: true + channel: '#release' + username: GoReleaser + message_template: 'New Release: {{ .ProjectName }} {{ .Tag }} is published! Check it out at {{ .ReleaseURL }}' + + discord: + enabled: true + message_template: '**New Release: {{ .ProjectName }} {{.Tag}}** is published! Check it out at {{ .ReleaseURL }}' \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index a01590c..cbcb4d2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,7 @@ -FROM golang:1.14-alpine AS builder -RUN apk add --no-cache git -RUN GO111MODULE=auto go get -u -v github.com/projectdiscovery/simplehttpserver +FROM golang:1.20.2-alpine as build-env +RUN go install -v github.com/projectdiscovery/simplehttpserver/cmd/simplehttpserver@latest FROM alpine:latest -COPY --from=builder /go/bin/simplehttpserver /usr/local/bin/ - -ENTRYPOINT ["simplehttpserver"] +RUN apk add --no-cache bind-tools ca-certificates +COPY --from=build-env /go/bin/simplehttpserver /usr/local/bin/simplehttpserver +ENTRYPOINT ["simplehttpserver"] \ No newline at end of file diff --git a/README.md b/README.md index f88164f..8d087ae 100644 --- a/README.md +++ b/README.md @@ -1,92 +1,195 @@ -

- simplehttpserver -
-

+

SimpleHTTPserver

+

Go alternative of python SimpleHTTPServer

-[![License](https://img.shields.io/badge/license-MIT-_red.svg)](https://opensource.org/licenses/MIT) -[![Go Report Card](https://goreportcard.com/badge/github.com/projectdiscovery/simplehttpserver)](https://goreportcard.com/report/github.com/projectdiscovery/simplehttpserver) -[![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/projectdiscovery/simplehttpserver/issues) -[![GitHub Release](https://img.shields.io/github/release/projectdiscovery/simplehttpserver)](https://github.com/projectdiscovery/simplehttpserver/releases) -[![Follow on Twitter](https://img.shields.io/twitter/follow/pdiscoveryio.svg?logo=twitter)](https://twitter.com/pdiscoveryio) -[![Docker Images](https://img.shields.io/docker/pulls/projectdiscovery/simplehttpserver.svg)](https://hub.docker.com/r/projectdiscovery/simplehttpserver) -[![Chat on Discord](https://img.shields.io/discord/695645237418131507.svg?logo=discord)](https://discord.gg/KECAGdH) -simplehttpserver is a go enhanced version of the well known python simplehttpserver. +

+ + + + + + +

-# Resources +

+ Features • + Usage • + Installation • + Run SimpleHTTPserver • + Join Discord +

+ +--- + +SimpleHTTPserver is a go enhanced version of the well known python simplehttpserver with in addition a fully customizable TCP server, both supporting TLS. -- [Features](#features) -- [Usage](#usage) -- [Installation Instructions](#installation-instructions) -- [Running simplehttpserver](#running-simplehttpserver-in-the-current-folder ) -- [Thanks](#thanks) # Features -

- simplehttpserver -
-

+- HTTP/S Web Server +- File Server with arbitrary directory support +- HTTP request/response dump +- Configurable ip address and listening port +- Configurable HTTP/TCP server with customizable response via YAML template + + +# Installing SimpleHTTPserver + +SimpleHTTPserver requires **go1.17+** to install successfully. Run the following command to get the repo - + +```sh +go install -v github.com/projectdiscovery/simplehttpserver/cmd/simplehttpserver@latest +``` - - File server in arbitrary directory - - Full request/response dump - - Configurable ip address and listening port +# Usage +```sh +simplehttpserver -h +``` -# Installation Instructions +This will display help for the tool. Here are all the switches it supports. +| Flag | Description | Example | +|------------------|---------------------------------------------------------|----------------------------------------------------| +| `-listen` | Configure listening ip:port (default 127.0.0.1:8000) | `simplehttpserver -listen 127.0.0.1:8000` | +| `-path` | Fileserver folder (default current directory) | `simplehttpserver -path /var/docs` | +| `-verbose` | Verbose (dump request/response, default false) | `simplehttpserver -verbose` | +| `-tcp` | TCP server (default 127.0.0.1:8000) | `simplehttpserver -tcp 127.0.0.1:8000` | +| `-tls` | Enable TLS for TCP server | `simplehttpserver -tls` | +| `-rules` | File containing yaml rules | `simplehttpserver -rules rule.yaml` | +| `-upload` | Enable file upload in case of http server | `simplehttpserver -upload` | +| `-max-file-size` | Max Upload File Size (default 50 MB) | `simplehttpserver -max-file-size 100` | +| `-sandbox` | Enable sandbox mode | `simplehttpserver -sandbox` | +| `-https` | Enable HTTPS in case of http server | `simplehttpserver -https` | +| `-http1` | Enable only HTTP1 | `simplehttpserver -http1` | +| `-cert` | HTTPS/TLS certificate (self generated if not specified) | `simplehttpserver -cert cert.pem` | +| `-key` | HTTPS/TLS certificate private key | `simplehttpserver -key cert.key` | +| `-domain` | Domain name to use for the self-generated certificate | `simplehttpserver -domain projectdiscovery.io` | +| `-cors` | Enable cross-origin resource sharing (CORS) | `simplehttpserver -cors` | +| `-basic-auth` | Basic auth (username:password) | `simplehttpserver -basic-auth user:password` | +| `-realm` | Basic auth message | `simplehttpserver -realm "insert the credentials"` | +| `-version` | Show version | `simplehttpserver -version` | +| `-silent` | Show only results | `simplehttpserver -silent` | +| `-py` | Emulate Python Style | `simplehttpserver -py` | +| `-header` | HTTP response header (can be used multiple times) | `simplehttpserver -header 'X-Powered-By: Go'` | -### From Binary +### Running simplehttpserver in the current folder -The installation is easy. You can download the pre-built binaries for your platform from the [Releases](https://github.com/projectdiscovery/simplehttpserver/releases/) page. Extract them using tar, move it to your `$PATH`and you're ready to go. +This will run the tool exposing the current directory on port 8000 ```sh -Download latest binary from https://github.com/projectdiscovery/simplehttpserver/releases +simplehttpserver -▶ tar -xvf simplehttpserver-linux-amd64.tar -▶ mv simplehttpserver-linux-amd64 /usr/local/bin/simplehttpserver -▶ simplehttpserver -h +2021/01/11 21:40:48 Serving . on http://0.0.0.0:8000/... +2021/01/11 21:41:15 [::1]:50181 "GET / HTTP/1.1" 200 383 +2021/01/11 21:41:15 [::1]:50181 "GET /favicon.ico HTTP/1.1" 404 19 ``` -### From Source +### Running simplehttpserver in the current folder with HTTPS -simplehttpserver requires **go1.14+** to install successfully. Run the following command to get the repo - +This will run the tool exposing the current directory on port 8000 over HTTPS with user provided certificate: ```sh -▶ GO111MODULE=on go get -v github.com/projectdiscovery/simplehttpserver -``` +simplehttpserver -https -cert cert.pen -key cert.key -### From Github +2021/01/11 21:40:48 Serving . on http://0.0.0.0:8000/... +2021/01/11 21:41:15 [::1]:50181 "GET / HTTP/1.1" 200 383 +2021/01/11 21:41:15 [::1]:50181 "GET /favicon.ico HTTP/1.1" 404 19 +``` +Instead, to run with self-signed certificate and specific domain name: ```sh -▶ git clone https://github.com/projectdiscovery/simplehttpserver.git; cd simplehttpserver; go build; mv simplehttpserver /usr/local/bin/; simplehttpserver -h +simplehttpserver -https -domain localhost + +2021/01/11 21:40:48 Serving . on http://0.0.0.0:8000/... +2021/01/11 21:41:15 [::1]:50181 "GET / HTTP/1.1" 200 383 +2021/01/11 21:41:15 [::1]:50181 "GET /favicon.ico HTTP/1.1" 404 19 ``` -# Usage +### Running simplehttpserver with basic auth and file upload + +This will run the tool and will request the user to enter username and password before authorizing file uploads ```sh -simplehttpserver -h +simplehttpserver -basic-auth root:root -upload + +2021/01/11 21:40:48 Serving . on http://0.0.0.0:8000/... ``` -This will display help for the tool. Here are all the switches it supports. +To upload files use the following curl request with basic auth header: +```sh +curl -v --user 'root:root' --upload-file file.txt http://localhost:8000/file.txt +``` -| Flag | Description | Example | -| ------ | ---------------------------------------------------- | --------------------------------------- | -| listen | Configure listening ip:port (default 127.0.0.1:8000) | simplehttpserver -listen 127.0.0.1:8000 | -| path | Fileserver folder (default current directory) | simplehttpserver -path /var/docs | -| v | Verbose (dump request/response, default false) | simplehttpserver -v | +### Running TCP server with custom responses -### Running simplehttpserver in the current folder - -This will run the tool exposing the current directory on port 8000 +This will run the tool as TLS TCP server and enable custom responses based on YAML templates: ```sh -▶ simplehttpserver -2021/01/11 21:40:48 Serving . on http://0.0.0.0:8000/... -2021/01/11 21:41:15 [::1]:50181 "GET / HTTP/1.1" 200 383 -2021/01/11 21:41:15 [::1]:50181 "GET /favicon.ico HTTP/1.1" 404 19 +simplehttpserver -rules rules.yaml -tcp -tls -domain localhost ``` +The rules are written as follows: +```yaml +rules: + - match: regex-match + match-contains: literal-match + name: rule-name + response: response data +``` + +For example to handle two different paths simulating an HTTP server or SMTP commands: +```yaml +rules: + # HTTP Requests + - match: GET /path1 + name: redirect + response: | + HTTP/1.0 200 OK + Server: httpd/2.0 + x-frame-options: SAMEORIGIN + x-xss-protection: 1; mode=block + Date: Fri, 16 Apr 2021 14:30:32 GMT + Content-Type: text/html + Connection: close + + + + - match: GET /path2 + name: "404" + response: | + HTTP/1.0 404 OK + Server: httpd/2.0 + + Not found + # SMTP Commands + - match: "EHLO example.com" + name: smtp + response: | + 250-localhost Nice to meet you, [127.0.0.1] + 250-PIPELINING + 250-8BITMIME + 250-SMTPUTF8 + 250-AUTH LOGIN PLAIN + 250 STARTTLS + - match: "MAIL FROM: " + response: 250 Accepted + - match: "RCPT TO: " + response: 250 Accepted + + - match-contains: !!binary | + MAwCAQFgBwIBAwQAgAA= + name: "ldap" + # Request: 300c 0201 0160 0702 0103 0400 8000 0....`........ + # Response: 300c 0201 0161 070a 0100 0400 0400 0....a........ + response: !!binary | + MAwCAQFhBwoBAAQABAA= +``` + +## Note + +- This project is intended for development purposes only; it should not be used in production. + # Thanks -simplehttpserver is made with 🖤 by the [projectdiscovery](https://projectdiscovery.io) team. Community contributions have made the project what it is. See the **[Thanks.md](https://github.com/projectdiscovery/simplehttpserver/blob/master/THANKS.md)** file for more details. +SimpleHTTPserver is made with 🖤 by the [projectdiscovery](https://projectdiscovery.io) team. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..7052910 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,5 @@ +# Security Policy + +## Reporting a Vulnerability + +DO NOT CREATE AN ISSUE to report a security problem. Instead, please send an email to [security@projectdiscovery.io](mailto:security@projectdiscovery.io), and we will acknowledge it within 3 working days. diff --git a/cmd/simplehttpserver/simplehttpserver.go b/cmd/simplehttpserver/simplehttpserver.go new file mode 100644 index 0000000..ed82b40 --- /dev/null +++ b/cmd/simplehttpserver/simplehttpserver.go @@ -0,0 +1,20 @@ +package main + +import ( + "github.com/projectdiscovery/gologger" + "github.com/projectdiscovery/simplehttpserver/internal/runner" +) + +func main() { + // Parse the command line flags and read config files + options := runner.ParseOptions() + r, err := runner.New(options) + if err != nil { + gologger.Fatal().Msgf("Could not create runner: %s\n", err) + } + + if err := r.Run(); err != nil { + gologger.Info().Msgf("%s\n", err) + } + defer r.Close() //nolint +} diff --git a/go.mod b/go.mod index 1876095..01a2ea1 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,27 @@ module github.com/projectdiscovery/simplehttpserver -go 1.15 +go 1.19 + +require ( + github.com/fsnotify/fsnotify v1.6.0 + github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 + github.com/projectdiscovery/gologger v1.1.8 + github.com/projectdiscovery/sslcert v0.0.0-20210416140253-8f56bec1bb5e + gopkg.in/yaml.v2 v2.4.0 +) + +require ( + github.com/dsnet/compress v0.0.1 // indirect + github.com/golang/snappy v0.0.1 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/logrusorgru/aurora v2.0.3+incompatible // indirect + github.com/mholt/archiver v3.1.1+incompatible // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/nwaples/rardecode v1.1.0 // indirect + github.com/pierrec/lz4 v2.6.0+incompatible // indirect + github.com/ulikunitz/xz v0.5.7 // indirect + github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect + golang.org/x/sys v0.0.0-20220908164124-27713097b956 // indirect + gopkg.in/djherbis/times.v1 v1.3.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..866d4e0 --- /dev/null +++ b/go.sum @@ -0,0 +1,70 @@ +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q= +github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo= +github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= +github.com/frankban/quicktest v1.11.3 h1:8sXhOn0uLys67V8EsXLc6eszDs8VXWxL3iRvebPhedY= +github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= +github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= +github.com/mholt/archiver v3.1.1+incompatible h1:1dCVxuqs0dJseYEhi5pl7MYPH9zDa1wBi7mF09cbNkU= +github.com/mholt/archiver v3.1.1+incompatible/go.mod h1:Dh2dOXnSdiLxRiPoVfIr/fI1TwETms9B8CTWfeh7ROU= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/nwaples/rardecode v1.1.0 h1:vSxaY8vQhOcVr4mm5e8XllHWTiM4JF507A0Katqw7MQ= +github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= +github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc= +github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= +github.com/pierrec/lz4 v2.6.0+incompatible h1:Ix9yFKn1nSPBLFl/yZknTp8TU5G4Ps0JDmguYK6iH1A= +github.com/pierrec/lz4 v2.6.0+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +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/projectdiscovery/gologger v1.1.8 h1:CFlCzGlqAhPqWIrAXBt1OVh5jkMs1qgoR/z4xhdzLNE= +github.com/projectdiscovery/gologger v1.1.8/go.mod h1:bNyVaC1U/NpJtFkJltcesn01NR3K8Hg6RsLVce6yvrw= +github.com/projectdiscovery/sslcert v0.0.0-20210416140253-8f56bec1bb5e h1:IZa08TUGbU7I0HUb9QQt/8wuu2fPZqfnMXwWhtMxei8= +github.com/projectdiscovery/sslcert v0.0.0-20210416140253-8f56bec1bb5e/go.mod h1:jSp8W5zIkNPxAqVdcoFlfv0K5cqogTe65fMinR0Fvuk= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= +github.com/ulikunitz/xz v0.5.7 h1:YvTNdFzX6+W5m9msiYg/zpkSURPPtOlzbqYjrFn7Yt4= +github.com/ulikunitz/xz v0.5.7/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= +github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= +golang.org/x/sys v0.0.0-20220908164124-27713097b956 h1:XeJjHH1KiLpKGb6lvMiksZ9l0fVUh+AmGcm0nOMEBOY= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/djherbis/times.v1 v1.3.0 h1:uxMS4iMtH6Pwsxog094W0FYldiNnfY/xba00vq6C2+o= +gopkg.in/djherbis/times.v1 v1.3.0/go.mod h1:AQlg6unIsrsCEdQYhTzERy542dz6SFdQFZFv6mUY0P8= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/runner/banner.go b/internal/runner/banner.go new file mode 100644 index 0000000..ed4193f --- /dev/null +++ b/internal/runner/banner.go @@ -0,0 +1,21 @@ +package runner + +import "github.com/projectdiscovery/gologger" + +const banner = ` + _____ _ __ __ __________________ + / ___/(_)___ ___ ____ / /__ / / / /_ __/_ __/ __ \________ ______ _____ _____ + \__ \/ / __ -__ \/ __ \/ / _ \/ /_/ / / / / / / /_/ / ___/ _ \/ ___/ | / / _ \/ ___/ + ___/ / / / / / / / /_/ / / __/ __ / / / / / / ____(__ ) __/ / | |/ / __/ / +/____/_/_/ /_/ /_/ .___/_/\___/_/ /_/ /_/ /_/ /_/ /____/\___/_/ |___/\___/_/ + /_/ - v0.0.6 +` + +// Version is the current version +const Version = `0.0.6` + +// showBanner is used to show the banner to the user +func showBanner() { + gologger.Print().Msgf("%s\n", banner) + gologger.Print().Msgf("\t\tprojectdiscovery.io\n\n") +} diff --git a/internal/runner/doc.go b/internal/runner/doc.go new file mode 100644 index 0000000..6d6e364 --- /dev/null +++ b/internal/runner/doc.go @@ -0,0 +1,2 @@ +// Package runner contains the internal logic +package runner diff --git a/internal/runner/options.go b/internal/runner/options.go new file mode 100644 index 0000000..2890a04 --- /dev/null +++ b/internal/runner/options.go @@ -0,0 +1,141 @@ +package runner + +import ( + "flag" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/projectdiscovery/gologger" + "github.com/projectdiscovery/gologger/levels" + "github.com/projectdiscovery/simplehttpserver/pkg/httpserver" +) + +// Options of the tool +type Options struct { + ListenAddress string + Folder string + BasicAuth string + username string + password string + Realm string + TLSCertificate string + TLSKey string + TLSDomain string + HTTPS bool + Verbose bool + EnableUpload bool + EnableTCP bool + RulesFile string + TCPWithTLS bool + Version bool + Silent bool + Sandbox bool + MaxFileSize int + HTTP1Only bool + MaxDumpBodySize int + Python bool + CORS bool + HTTPHeaders HTTPHeaders +} + +// ParseOptions parses the command line options for application +func ParseOptions() *Options { + options := &Options{} + flag.StringVar(&options.ListenAddress, "listen", "0.0.0.0:8000", "Address:Port") + flag.BoolVar(&options.EnableTCP, "tcp", false, "TCP Server") + flag.BoolVar(&options.TCPWithTLS, "tls", false, "Enable TCP TLS") + flag.StringVar(&options.RulesFile, "rules", "", "Rules yaml file") + currentPath := "." + if p, err := os.Getwd(); err == nil { + currentPath = p + } + flag.StringVar(&options.Folder, "path", currentPath, "Folder") + flag.BoolVar(&options.EnableUpload, "upload", false, "Enable upload via PUT") + flag.BoolVar(&options.HTTPS, "https", false, "HTTPS") + flag.StringVar(&options.TLSCertificate, "cert", "", "HTTPS Certificate") + flag.StringVar(&options.TLSKey, "key", "", "HTTPS Certificate Key") + flag.StringVar(&options.TLSDomain, "domain", "local.host", "Domain") + flag.BoolVar(&options.Verbose, "verbose", false, "Verbose") + flag.StringVar(&options.BasicAuth, "basic-auth", "", "Basic auth (username:password)") + flag.StringVar(&options.Realm, "realm", "Please enter username and password", "Realm") + flag.BoolVar(&options.Version, "version", false, "Show version of the software") + flag.BoolVar(&options.Silent, "silent", false, "Show only results in the output") + flag.BoolVar(&options.Sandbox, "sandbox", false, "Enable sandbox mode") + flag.BoolVar(&options.HTTP1Only, "http1", false, "Enable only HTTP1") + flag.IntVar(&options.MaxFileSize, "max-file-size", 50, "Max Upload File Size") + flag.IntVar(&options.MaxDumpBodySize, "max-dump-body-size", -1, "Max Dump Body Size") + flag.BoolVar(&options.Python, "py", false, "Emulate Python Style") + flag.BoolVar(&options.CORS, "cors", false, "Enable Cross-Origin Resource Sharing (CORS)") + flag.Var(&options.HTTPHeaders, "header", "Add HTTP Response Header (name: value), can be used multiple times") + flag.Parse() + + // Read the inputs and configure the logging + options.configureOutput() + + showBanner() + + if options.Version { + gologger.Info().Msgf("Current Version: %s\n", Version) + os.Exit(0) + } + + options.validateOptions() + + return options +} + +func (options *Options) validateOptions() { + if flag.NArg() > 0 && options.Folder == "." { + options.Folder = flag.Args()[0] + } + + if options.BasicAuth != "" { + baTokens := strings.SplitN(options.BasicAuth, ":", 2) + if len(baTokens) > 0 { + options.username = baTokens[0] + } + if len(baTokens) > 1 { + options.password = baTokens[1] + } + } +} + +// configureOutput configures the output on the screen +func (options *Options) configureOutput() { + // If the user desires verbose output, show verbose output + if options.Verbose { + gologger.DefaultLogger.SetMaxLevel(levels.LevelVerbose) + } + if options.Silent { + gologger.DefaultLogger.SetMaxLevel(levels.LevelSilent) + } +} + +// FolderAbsPath of the fileserver folder +func (options *Options) FolderAbsPath() string { + abspath, err := filepath.Abs(options.Folder) + if err != nil { + return options.Folder + } + return abspath +} + +// HTTPHeaders is a slice of HTTPHeader structs +type HTTPHeaders []httpserver.HTTPHeader + +func (h *HTTPHeaders) String() string { + return fmt.Sprint(*h) +} + +// Set sets a new header, which must be a string of the form 'name: value' +func (h *HTTPHeaders) Set(value string) error { + tokens := strings.SplitN(value, ":", 2) + if len(tokens) != 2 { + return fmt.Errorf("header '%s' not in format 'name: value'", value) + } + + *h = append(*h, httpserver.HTTPHeader{Name: tokens[0], Value: tokens[1]}) + return nil +} diff --git a/internal/runner/runner.go b/internal/runner/runner.go new file mode 100644 index 0000000..dc63940 --- /dev/null +++ b/internal/runner/runner.go @@ -0,0 +1,116 @@ +package runner + +import ( + "github.com/projectdiscovery/gologger" + "github.com/projectdiscovery/simplehttpserver/pkg/binder" + "github.com/projectdiscovery/simplehttpserver/pkg/httpserver" + "github.com/projectdiscovery/simplehttpserver/pkg/tcpserver" + "github.com/projectdiscovery/simplehttpserver/pkg/unit" +) + +// Runner is a client for running the enumeration process. +type Runner struct { + options *Options + serverTCP *tcpserver.TCPServer + httpServer *httpserver.HTTPServer +} + +// New instance of runner +func New(options *Options) (*Runner, error) { + r := Runner{options: options} + // Check if the process can listen on the specified ip:port + if !binder.CanListenOn(r.options.ListenAddress) { + newListenAddress, err := binder.GetRandomListenAddress(r.options.ListenAddress) + if err != nil { + return nil, err + } + gologger.Print().Msgf("Can't listen on %s: %s - Using %s\n", r.options.ListenAddress, err, newListenAddress) + r.options.ListenAddress = newListenAddress + } + + if r.options.EnableTCP { + serverTCP, err := tcpserver.New(&tcpserver.Options{ + Listen: r.options.ListenAddress, + TLS: r.options.TCPWithTLS, + Domain: "local.host", + Verbose: r.options.Verbose, + }) + if err != nil { + return nil, err + } + err = serverTCP.LoadTemplate(r.options.RulesFile) + if err != nil { + return nil, err + } + watcher, err := watchFile(r.options.RulesFile, serverTCP.LoadTemplate) + if err != nil { + return nil, err + } + defer watcher.Close() + + r.serverTCP = serverTCP + return &r, nil + } + + httpServer, err := httpserver.New(&httpserver.Options{ + Folder: r.options.Folder, + EnableUpload: r.options.EnableUpload, + ListenAddress: r.options.ListenAddress, + TLS: r.options.HTTPS, + Certificate: r.options.TLSCertificate, + CertificateKey: r.options.TLSKey, + CertificateDomain: r.options.TLSDomain, + BasicAuthUsername: r.options.username, + BasicAuthPassword: r.options.password, + BasicAuthReal: r.options.Realm, + Verbose: r.options.Verbose, + Sandbox: r.options.Sandbox, + MaxFileSize: r.options.MaxFileSize, + HTTP1Only: r.options.HTTP1Only, + MaxDumpBodySize: unit.ToMb(r.options.MaxDumpBodySize), + Python: r.options.Python, + CORS: r.options.CORS, + HTTPHeaders: r.options.HTTPHeaders, + }) + if err != nil { + return nil, err + } + r.httpServer = httpServer + + return &r, nil +} + +// Run logic +func (r *Runner) Run() error { + if r.options.EnableTCP { + if r.options.TCPWithTLS { + gologger.Print().Msgf("Serving TCP rule based tls server on tcp://%s", r.options.ListenAddress) + return r.serverTCP.ListenAndServeTLS() + } + gologger.Print().Msgf("Serving TCP rule based server on tcp://%s", r.options.ListenAddress) + return r.serverTCP.ListenAndServe() + } + + if r.options.HTTPS { + gologger.Print().Msgf("Serving %s on https://%s/", r.options.FolderAbsPath(), r.options.ListenAddress) + return r.httpServer.ListenAndServeTLS() + } + + gologger.Print().Msgf("Serving %s on http://%s/", r.options.FolderAbsPath(), r.options.ListenAddress) + return r.httpServer.ListenAndServe() +} + +// Close the listening services +func (r *Runner) Close() error { + if r.serverTCP != nil { + if err := r.serverTCP.Close(); err != nil { + return err + } + } + if r.httpServer != nil { + if err := r.httpServer.Close(); err != nil { + return err + } + } + return nil +} diff --git a/internal/runner/watchdog.go b/internal/runner/watchdog.go new file mode 100644 index 0000000..2cdde4c --- /dev/null +++ b/internal/runner/watchdog.go @@ -0,0 +1,36 @@ +package runner + +import ( + "log" + + "github.com/fsnotify/fsnotify" +) + +type WatchEvent func(fname string) error + +func watchFile(fname string, callback WatchEvent) (watcher *fsnotify.Watcher, err error) { + watcher, err = fsnotify.NewWatcher() + if err != nil { + return + } + go func() { + for { + select { + case event, ok := <-watcher.Events: + if !ok { + continue + } + if event.Op&fsnotify.Write == fsnotify.Write { + if err := callback(fname); err != nil { + log.Println("err", err) + } + } + case <-watcher.Errors: + // ignore errors for now + } + } + }() + + err = watcher.Add(fname) + return +} diff --git a/pkg/binder/binder.go b/pkg/binder/binder.go new file mode 100644 index 0000000..549e9d9 --- /dev/null +++ b/pkg/binder/binder.go @@ -0,0 +1,36 @@ +package binder + +import ( + "fmt" + "net" + + "github.com/phayes/freeport" + "github.com/projectdiscovery/gologger" +) + +// CanListenOn the specified address +func CanListenOn(address string) bool { + listener, err := net.Listen("tcp4", address) + if err != nil { + return false + } + if err := listener.Close(); err != nil { + gologger.Info().Msgf("%s\n", err) + } + return true +} + +// GetRandomListenAddress from the specified one +func GetRandomListenAddress(currentAddress string) (string, error) { + addrOrig, _, err := net.SplitHostPort(currentAddress) + if err != nil { + return "", err + } + + newPort, err := freeport.GetFreePort() + if err != nil { + return "", err + } + + return net.JoinHostPort(addrOrig, fmt.Sprintf("%d", newPort)), nil +} diff --git a/pkg/binder/doc.go b/pkg/binder/doc.go new file mode 100644 index 0000000..709dc10 --- /dev/null +++ b/pkg/binder/doc.go @@ -0,0 +1,2 @@ +// Package binder contains binding helpers +package binder diff --git a/pkg/httpserver/authlayer.go b/pkg/httpserver/authlayer.go new file mode 100644 index 0000000..297d863 --- /dev/null +++ b/pkg/httpserver/authlayer.go @@ -0,0 +1,19 @@ +package httpserver + +import ( + "fmt" + "net/http" +) + +func (t *HTTPServer) basicauthlayer(handler http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + user, pass, ok := r.BasicAuth() + if !ok || user != t.options.BasicAuthUsername || pass != t.options.BasicAuthPassword { + w.Header().Set("WWW-Authenticate", fmt.Sprintf("Basic realm=\"%s\"", t.options.BasicAuthReal)) + w.WriteHeader(http.StatusUnauthorized) + w.Write([]byte("Unauthorized.\n")) //nolint + return + } + handler.ServeHTTP(w, r) + }) +} diff --git a/pkg/httpserver/corslayer.go b/pkg/httpserver/corslayer.go new file mode 100644 index 0000000..70549d0 --- /dev/null +++ b/pkg/httpserver/corslayer.go @@ -0,0 +1,28 @@ +package httpserver + +import ( + "net/http" + "strings" +) + +func (t *HTTPServer) corslayer(handler http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + headers := w.Header() + headers.Set("Access-Control-Allow-Origin", "*") + if r.Method != http.MethodOptions { + handler.ServeHTTP(w, r) + return + } + + headers.Add("Vary", "Origin") + headers.Add("Vary", "Access-Control-Request-Method") + headers.Add("Vary", "Access-Control-Request-Headers") + + reqMethod := r.Header.Get("Access-Control-Request-Method") + if reqMethod != "" { + headers.Set("Access-Control-Allow-Methods", strings.ToUpper(reqMethod)) + } + + w.WriteHeader(http.StatusOK) + }) +} diff --git a/pkg/httpserver/doc.go b/pkg/httpserver/doc.go new file mode 100644 index 0000000..5344c9f --- /dev/null +++ b/pkg/httpserver/doc.go @@ -0,0 +1,2 @@ +// Package httpserver contains the http server logic +package httpserver diff --git a/pkg/httpserver/headerlayer.go b/pkg/httpserver/headerlayer.go new file mode 100644 index 0000000..0a0dac7 --- /dev/null +++ b/pkg/httpserver/headerlayer.go @@ -0,0 +1,20 @@ +package httpserver + +import ( + "net/http" +) + +// HTTPHeader represents an HTTP header +type HTTPHeader struct { + Name string + Value string +} + +func (t *HTTPServer) headerlayer(handler http.Handler, headers []HTTPHeader) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + for _, header := range headers { + w.Header().Set(header.Name, header.Value) + } + handler.ServeHTTP(w, r) + }) +} diff --git a/pkg/httpserver/httpserver.go b/pkg/httpserver/httpserver.go new file mode 100644 index 0000000..5dd65a6 --- /dev/null +++ b/pkg/httpserver/httpserver.go @@ -0,0 +1,131 @@ +package httpserver + +import ( + "crypto/tls" + "errors" + "net/http" + "os" + "path/filepath" + + "github.com/projectdiscovery/sslcert" +) + +// Options of the http server +type Options struct { + Folder string + EnableUpload bool + ListenAddress string + TLS bool + Certificate string + CertificateKey string + CertificateDomain string + BasicAuthUsername string + BasicAuthPassword string + BasicAuthReal string + Verbose bool + Sandbox bool + HTTP1Only bool + MaxFileSize int // 50Mb + MaxDumpBodySize int64 + Python bool + CORS bool + HTTPHeaders []HTTPHeader +} + +// HTTPServer instance +type HTTPServer struct { + options *Options + layers http.Handler +} + +// LayerHandler is the interface of all layer funcs +type Middleware func(http.Handler) http.Handler + +// New http server instance with options +func New(options *Options) (*HTTPServer, error) { + var h HTTPServer + EnableUpload = options.EnableUpload + EnableVerbose = options.Verbose + folder, err := filepath.Abs(options.Folder) + if err != nil { + return nil, err + } + if _, err := os.Stat(folder); os.IsNotExist(err) { + return nil, errors.New("path does not exist") + } + options.Folder = folder + var dir http.FileSystem + dir = http.Dir(options.Folder) + if options.Sandbox { + dir = SandboxFileSystem{fs: http.Dir(options.Folder), RootFolder: options.Folder} + } + + var httpHandler http.Handler + if options.Python { + httpHandler = PythonStyle(dir.(http.Dir)) + } else { + httpHandler = http.FileServer(dir) + } + + addHandler := func(newHandler Middleware) { + httpHandler = newHandler(httpHandler) + } + + // middleware + if options.EnableUpload { + addHandler(h.uploadlayer) + } + + if options.BasicAuthUsername != "" || options.BasicAuthPassword != "" { + addHandler(h.basicauthlayer) + } + + if options.CORS { + addHandler(h.corslayer) + } + + httpHandler = h.loglayer(httpHandler) + httpHandler = h.headerlayer(httpHandler, options.HTTPHeaders) + + // add handler + h.layers = httpHandler + h.options = options + + return &h, nil +} + +func (t *HTTPServer) makeHTTPServer(tlsConfig *tls.Config) *http.Server { + httpServer := &http.Server{Addr: t.options.ListenAddress} + if t.options.HTTP1Only { + httpServer.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler)) + } + httpServer.TLSConfig = tlsConfig + httpServer.Handler = t.layers + return httpServer +} + +// ListenAndServe requests over http +func (t *HTTPServer) ListenAndServe() error { + httpServer := t.makeHTTPServer(nil) + return httpServer.ListenAndServe() +} + +// ListenAndServeTLS requests over https +func (t *HTTPServer) ListenAndServeTLS() error { + if t.options.Certificate == "" || t.options.CertificateKey == "" { + tlsOptions := sslcert.DefaultOptions + tlsOptions.Host = t.options.CertificateDomain + tlsConfig, err := sslcert.NewTLSConfig(tlsOptions) + if err != nil { + return err + } + httpServer := t.makeHTTPServer(tlsConfig) + return httpServer.ListenAndServeTLS("", "") + } + return http.ListenAndServeTLS(t.options.ListenAddress, t.options.Certificate, t.options.CertificateKey, t.layers) +} + +// Close the service +func (t *HTTPServer) Close() error { + return nil +} diff --git a/pkg/httpserver/loglayer.go b/pkg/httpserver/loglayer.go new file mode 100644 index 0000000..f3fb4f7 --- /dev/null +++ b/pkg/httpserver/loglayer.go @@ -0,0 +1,72 @@ +package httpserver + +import ( + "bytes" + "net/http" + "net/http/httputil" + "time" + "github.com/projectdiscovery/gologger" +) + +// Convenience globals +var ( + EnableUpload bool + EnableVerbose bool +) + +func (t *HTTPServer) shouldDumpBody(bodysize int64) bool { + return t.options.MaxDumpBodySize > 0 && bodysize > t.options.MaxDumpBodySize +} + +func (t *HTTPServer) loglayer(handler http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var fullRequest []byte + if t.shouldDumpBody(r.ContentLength) { + fullRequest, _ = httputil.DumpRequest(r, false) + } else { + fullRequest, _ = httputil.DumpRequest(r, true) + } + lrw := newLoggingResponseWriter(w, t.options.MaxDumpBodySize) + handler.ServeHTTP(lrw, r) + + if EnableVerbose { + headers := new(bytes.Buffer) + lrw.Header().Write(headers) //nolint + gologger.Print().Msgf("\n[%s]\nRemote Address: %s\n%s\n%s %d %s\n%s\n%s\n", time.Now().Format("2006-01-02 15:04:05"), r.RemoteAddr, string(fullRequest), r.Proto, lrw.statusCode, http.StatusText(lrw.statusCode), headers.String(), string(lrw.Data)) + } else { + gologger.Print().Msgf("[%s] %s \"%s %s %s\" %d %d", time.Now().Format("2006-01-02 15:04:05"), r.RemoteAddr, r.Method, r.URL, r.Proto, lrw.statusCode, lrw.Size) + } + }) +} + +type loggingResponseWriter struct { + http.ResponseWriter + statusCode int + Data []byte + Size int + MaxDumpSize int64 +} + +func newLoggingResponseWriter(w http.ResponseWriter, maxSize int64) *loggingResponseWriter { + return &loggingResponseWriter{w, http.StatusOK, []byte{}, 0, maxSize} +} + +// Write the data +func (lrw *loggingResponseWriter) Write(data []byte) (int, error) { + if len(lrw.Data) < int(lrw.MaxDumpSize) { + lrw.Data = append(lrw.Data, data...) + } + lrw.Size += len(data) + return lrw.ResponseWriter.Write(data) +} + +// Header of the response +func (lrw *loggingResponseWriter) Header() http.Header { + return lrw.ResponseWriter.Header() +} + +// WriteHeader status code +func (lrw *loggingResponseWriter) WriteHeader(code int) { + lrw.statusCode = code + lrw.ResponseWriter.WriteHeader(code) +} diff --git a/pkg/httpserver/pythonliststyle.go b/pkg/httpserver/pythonliststyle.go new file mode 100644 index 0000000..ee6342f --- /dev/null +++ b/pkg/httpserver/pythonliststyle.go @@ -0,0 +1,97 @@ +package httpserver + +import ( + "bytes" + "fmt" + "io" + "net/http" + "os" + "path/filepath" +) + +const ( + preTag = "
"
+	preTagClose = "
" + aTag = " + + + + +Directory listing for %s + + +` + htmlFooter = `
+ + +` +) + +type pythonStyleHandler struct { + origWriter http.ResponseWriter + root http.Dir +} + +func (h *pythonStyleHandler) Header() http.Header { + return h.origWriter.Header() +} + +func (h *pythonStyleHandler) writeListItem(b []byte, written *int) { + var i int + i, _ = fmt.Fprint(h.origWriter, "
  • ") + *written += i + i, _ = h.origWriter.Write(bytes.Trim(b, "\r\n")) + *written += i + i, _ = fmt.Fprint(h.origWriter, "
  • \n") + *written += i +} + +func (h *pythonStyleHandler) Write(b []byte) (int, error) { + var i int + written := 0 + + if bytes.HasPrefix(b, []byte(preTag)) { + _, _ = io.Discard.Write(b) + i, _ = fmt.Fprintln(h.origWriter, "
      ") + written += i + return written, nil + } + if bytes.HasPrefix(b, []byte(preTagClose)) { + _, _ = io.Discard.Write(b) + i, _ = fmt.Fprintln(h.origWriter, "
    ") + written += i + return written, nil + } + + if bytes.HasPrefix(b, []byte(aTag)) { + h.writeListItem(b, &written) + } + return i, nil +} + +func (h *pythonStyleHandler) WriteHeader(statusCode int) { + h.origWriter.WriteHeader(statusCode) +} + +func (h *pythonStyleHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) { + target := filepath.Join(string(h.root), filepath.Clean(request.URL.Path)) + file, err := os.Stat(target) + + if err != nil || !file.IsDir() { + http.ServeFile(writer, request, target) + return + } else { + _, _ = fmt.Fprintf(writer, htmlHeader, request.URL.Path) + _, _ = fmt.Fprintf(writer, "

    Directory listing for %s

    \n
    \n", request.URL.Path) + h.origWriter = writer + http.ServeFile(h, request, target) + _, _ = fmt.Fprint(writer, htmlFooter) + } +} + +func PythonStyle(root http.Dir) http.Handler { + return &pythonStyleHandler{ + root: root, + } +} diff --git a/pkg/httpserver/sandboxfs.go b/pkg/httpserver/sandboxfs.go new file mode 100644 index 0000000..cde5c04 --- /dev/null +++ b/pkg/httpserver/sandboxfs.go @@ -0,0 +1,57 @@ +package httpserver + +import ( + "errors" + "net/http" + "path/filepath" +) + +// SandboxFileSystem implements superbasic security checks +type SandboxFileSystem struct { + fs http.FileSystem + RootFolder string +} + +// Open performs basic security checks before providing folder/file content +func (sbfs SandboxFileSystem) Open(path string) (http.File, error) { + abspath, err := filepath.Abs(filepath.Join(sbfs.RootFolder, path)) + if err != nil { + return nil, err + } + + filename := filepath.Base(abspath) + // rejects names starting with a dot like .file + dotmatch, err := filepath.Match(".*", filename) + if err != nil { + return nil, err + } else if dotmatch { + return nil, errors.New("invalid file") + } + + // reject symlinks + symlinkCheck, err := filepath.EvalSymlinks(abspath) + if err != nil { + return nil, err + } + if symlinkCheck != abspath { + return nil, errors.New("symlinks not allowed") + } + + // check if the path is within the configured folder + if sbfs.RootFolder != abspath { + pattern := sbfs.RootFolder + string(filepath.Separator) + "*" + matched, err := filepath.Match(pattern, abspath) + if err != nil { + return nil, err + } else if !matched { + return nil, errors.New("invalid file") + } + } + + f, err := sbfs.fs.Open(path) + if err != nil { + return nil, err + } + + return f, nil +} diff --git a/pkg/httpserver/uploadlayer.go b/pkg/httpserver/uploadlayer.go new file mode 100644 index 0000000..571d3c1 --- /dev/null +++ b/pkg/httpserver/uploadlayer.go @@ -0,0 +1,99 @@ +package httpserver + +import ( + "errors" + "io" + "net/http" + "os" + "path" + "path/filepath" + "strings" + + "github.com/projectdiscovery/gologger" + "github.com/projectdiscovery/simplehttpserver/pkg/unit" +) + +// uploadlayer handles PUT requests and save the file to disk +func (t *HTTPServer) uploadlayer(handler http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Handles file write if enabled + if EnableUpload && r.Method == http.MethodPut { + // sandbox - calcolate absolute path + if t.options.Sandbox { + absPath, err := filepath.Abs(filepath.Join(t.options.Folder, r.URL.Path)) + if err != nil { + gologger.Print().Msgf("%s\n", err) + w.WriteHeader(http.StatusBadRequest) + return + } + // check if the path is within the configured folder + pattern := t.options.Folder + string(filepath.Separator) + "*" + matched, err := filepath.Match(pattern, absPath) + if err != nil { + gologger.Print().Msgf("%s\n", err) + w.WriteHeader(http.StatusBadRequest) + return + } else if !matched { + gologger.Print().Msg("pointing to unauthorized directory") + w.WriteHeader(http.StatusBadRequest) + return + } + } + + var ( + data []byte + err error + ) + if t.options.Sandbox { + maxFileSize := unit.ToMb(t.options.MaxFileSize) + // check header content length + if r.ContentLength > maxFileSize { + gologger.Print().Msg("request too large") + return + } + // body max length + r.Body = http.MaxBytesReader(w, r.Body, maxFileSize) + } + + data, err = io.ReadAll(r.Body) + if err != nil { + gologger.Print().Msgf("%s\n", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + sanitizedPath := filepath.FromSlash(path.Clean("/" + strings.Trim(r.URL.Path, "/"))) + + err = handleUpload(t.options.Folder, sanitizedPath, data) + if err != nil { + gologger.Print().Msgf("%s\n", err) + w.WriteHeader(http.StatusInternalServerError) + return + } else { + w.WriteHeader(http.StatusCreated) + return + } + } + + handler.ServeHTTP(w, r) + }) +} + +func handleUpload(base, file string, data []byte) error { + // rejects all paths containing a non exhaustive list of invalid characters - This is only a best effort as the tool is meant for development + if strings.ContainsAny(file, "\\`\"':") { + return errors.New("invalid character") + } + + untrustedPath := filepath.Clean(filepath.Join(base, file)) + if !strings.HasPrefix(untrustedPath, filepath.Clean(base)) { + return errors.New("invalid path") + } + trustedPath := untrustedPath + + if _, err := os.Stat(filepath.Dir(trustedPath)); os.IsNotExist(err) { + return errors.New("invalid path") + } + + return os.WriteFile(trustedPath, data, 0655) +} diff --git a/pkg/tcpserver/addr.go b/pkg/tcpserver/addr.go new file mode 100644 index 0000000..b678b30 --- /dev/null +++ b/pkg/tcpserver/addr.go @@ -0,0 +1,9 @@ +package tcpserver + +// ContextType is the key type stored in ctx +type ContextType string + +var ( + // Addr is the contextKey where the net.Addr is stored + Addr ContextType = "addr" +) diff --git a/pkg/tcpserver/doc.go b/pkg/tcpserver/doc.go new file mode 100644 index 0000000..4ab6d69 --- /dev/null +++ b/pkg/tcpserver/doc.go @@ -0,0 +1,2 @@ +// Package tcpserver contains the tcp server logic +package tcpserver diff --git a/pkg/tcpserver/responseengine.go b/pkg/tcpserver/responseengine.go new file mode 100644 index 0000000..80fb795 --- /dev/null +++ b/pkg/tcpserver/responseengine.go @@ -0,0 +1,19 @@ +package tcpserver + +import ( + "errors" +) + +// BuildResponse according to rules +func (t *TCPServer) BuildResponse(data []byte) ([]byte, error) { + t.mux.RLock() + defer t.mux.RUnlock() + + // Process all the rules + for _, rule := range t.rules { + if rule.MatchInput(data) { + return []byte(rule.Response), nil + } + } + return nil, errors.New("no matched rule") +} diff --git a/pkg/tcpserver/rule.go b/pkg/tcpserver/rule.go new file mode 100644 index 0000000..aa9e6e8 --- /dev/null +++ b/pkg/tcpserver/rule.go @@ -0,0 +1,65 @@ +package tcpserver + +import ( + "regexp" + "strings" +) + +// RulesConfiguration from yaml +type RulesConfiguration struct { + Rules []Rule `yaml:"rules"` +} + +// Rule to apply to various requests +type Rule struct { + Name string `yaml:"name,omitempty"` + Match string `yaml:"match,omitempty"` + MatchContains string `yaml:"match-contains,omitempty"` + matchRegex *regexp.Regexp + Response string `yaml:"response,omitempty"` +} + +// NewRule creates a new Rule - default is regex +func NewRule(match, response string) (*Rule, error) { + return NewRegexRule(match, response) +} + +// NewRegexRule returns a new regex-match Rule +func NewRegexRule(match, response string) (*Rule, error) { + regxp, err := regexp.Compile(match) + if err != nil { + return nil, err + } + + return &Rule{Match: match, matchRegex: regxp, Response: response}, nil +} + +// NewLiteralRule returns a new literal-match Rule +func NewLiteralRule(match, response string) (*Rule, error) { + return &Rule{MatchContains: match, Response: response}, nil +} + +// NewRuleFromTemplate "copies" a new Rule +func NewRuleFromTemplate(r Rule) (newRule *Rule, err error) { + newRule = &Rule{ + Name: r.Name, + Response: r.Response, + MatchContains: r.MatchContains, + Match: r.Match, + } + if newRule.Match != "" { + newRule.matchRegex, err = regexp.Compile(newRule.Match) + } + + return +} + +// MatchInput returns if the input was matches with one of the matchers +func (r *Rule) MatchInput(input []byte) bool { + if r.matchRegex != nil && r.matchRegex.Match(input) { + return true + } else if r.MatchContains != "" && strings.Contains(string(input), r.MatchContains) { + return true + } + return false +} diff --git a/pkg/tcpserver/tcpserver.go b/pkg/tcpserver/tcpserver.go new file mode 100644 index 0000000..3b996f5 --- /dev/null +++ b/pkg/tcpserver/tcpserver.go @@ -0,0 +1,208 @@ +package tcpserver + +import ( + "context" + "crypto/tls" + "errors" + "os" + "net" + "sync" + "time" + + "github.com/projectdiscovery/gologger" + "github.com/projectdiscovery/sslcert" + "gopkg.in/yaml.v2" +) + +const readTimeout = 5 + +// Options of the tcp server +type Options struct { + Listen string + TLS bool + Certificate string + Key string + Domain string + rules []Rule + Verbose bool +} + +// CallBackFunc handles what is send back to the client, based on the incomming question +type CallBackFunc func(ctx context.Context, question []byte) (answer []byte, err error) + +// TCPServer instance +type TCPServer struct { + options *Options + listener net.Listener + + // Callbacks to retrieve information about the system + HandleMessageFnc CallBackFunc + + mux sync.RWMutex + rules []Rule +} + +// New tcp server instance with specified options +func New(options *Options) (*TCPServer, error) { + srv := &TCPServer{options: options} + srv.HandleMessageFnc = srv.BuildResponseWithContext + srv.rules = options.rules + return srv, nil +} + +// AddRule to the server +func (t *TCPServer) AddRule(rule Rule) error { + t.mux.Lock() + defer t.mux.Unlock() + + t.rules = append(t.rules, rule) + return nil +} + +// ListenAndServe requests +func (t *TCPServer) ListenAndServe() error { + listener, err := net.Listen("tcp4", t.options.Listen) + if err != nil { + return err + } + t.listener = listener + return t.run() +} + +func (t *TCPServer) handleConnection(conn net.Conn, callback CallBackFunc) error { + defer conn.Close() //nolint + + // Create Context + ctx := context.WithValue(context.Background(), Addr, conn.RemoteAddr()) + + buf := make([]byte, 4096) + for { + if err := conn.SetReadDeadline(time.Now().Add(readTimeout * time.Second)); err != nil { + gologger.Info().Msgf("%s\n", err) + } + n, err := conn.Read(buf) + if err != nil { + return err + } + + gologger.Print().Msgf("%s\n", buf[:n]) + + resp, err := callback(ctx, buf[:n]) + if err != nil { + gologger.Info().Msgf("Closing connection: %s\n", err) + return err + } + + if _, err := conn.Write(resp); err != nil { + gologger.Info().Msgf("%s\n", err) + } + + gologger.Print().Msgf("%s\n", resp) + } +} + +// ListenAndServeTLS requests over tls +func (t *TCPServer) ListenAndServeTLS() error { + var tlsConfig *tls.Config + if t.options.Certificate != "" && t.options.Key != "" { + cert, err := tls.LoadX509KeyPair(t.options.Certificate, t.options.Key) + if err != nil { + return err + } + tlsConfig = &tls.Config{Certificates: []tls.Certificate{cert}} + } else { + tlsOptions := sslcert.DefaultOptions + tlsOptions.Host = t.options.Domain + cfg, err := sslcert.NewTLSConfig(tlsOptions) + if err != nil { + return err + } + tlsConfig = cfg + } + + listener, err := tls.Listen("tcp", t.options.Listen, tlsConfig) + if err != nil { + return err + } + t.listener = listener + return t.run() +} + +func (t *TCPServer) run() error { + for { + c, err := t.listener.Accept() + if err != nil { + return err + } + go t.handleConnection(c, t.HandleMessageFnc) //nolint + } +} + +// Close the service +func (t *TCPServer) Close() error { + return t.listener.Close() +} + +// LoadTemplate from yaml +func (t *TCPServer) LoadTemplate(templatePath string) error { + var config RulesConfiguration + yamlFile, err := os.ReadFile(templatePath) + if err != nil { + return err + } + err = yaml.Unmarshal(yamlFile, &config) + if err != nil { + return err + } + + t.mux.Lock() + defer t.mux.Unlock() + + t.rules = make([]Rule, 0) + for _, ruleTemplate := range config.Rules { + rule, err := NewRuleFromTemplate(ruleTemplate) + if err != nil { + return err + } + t.rules = append(t.rules, *rule) + } + + gologger.Info().Msgf("TCP configuration loaded. Rules: %d\n", len(t.rules)) + + return nil +} + +// MatchRule returns the rule, which was matched first +func (t *TCPServer) MatchRule(data []byte) (rule Rule, err error) { + t.mux.RLock() + defer t.mux.RUnlock() + + // Process all the rules + for _, rule := range t.rules { + if rule.MatchInput(data) { + return rule, nil + } + } + return Rule{}, errors.New("no matched rule") +} + +// BuildResponseWithContext is a wrapper with context +func (t *TCPServer) BuildResponseWithContext(ctx context.Context, data []byte) ([]byte, error) { + return t.BuildResponse(data) +} + +// BuildResponseWithContext is a wrapper with context +func (t *TCPServer) BuildRuleResponse(ctx context.Context, data []byte) ([]byte, error) { + addr := "unknown" + if netAddr, ok := ctx.Value(Addr).(net.Addr); ok { + addr = netAddr.String() + } + rule, err := t.MatchRule(data) + if err != nil { + return []byte(":) "), err + } + + gologger.Info().Msgf("Incoming TCP request(%s) from: %s\n", rule.Name, addr) + + return []byte(rule.Response), nil +} diff --git a/pkg/unit/unit.go b/pkg/unit/unit.go new file mode 100644 index 0000000..98cdb35 --- /dev/null +++ b/pkg/unit/unit.go @@ -0,0 +1,6 @@ +package unit + +// ToMb converts bytes to megabytes +func ToMb(n int) int64 { + return int64(n) * 1024 * 1024 +} diff --git a/simplehttpserver.go b/simplehttpserver.go deleted file mode 100644 index 2e7ad19..0000000 --- a/simplehttpserver.go +++ /dev/null @@ -1,72 +0,0 @@ -package main - -import ( - "bytes" - "flag" - "fmt" - "log" - "net/http" - "net/http/httputil" -) - -type options struct { - ListenAddress string - Folder string - Verbose bool -} - -var opts options - -func main() { - flag.StringVar(&opts.ListenAddress, "listen", "0.0.0.0:8000", "Address:Port") - flag.StringVar(&opts.Folder, "path", ".", "Folder") - flag.BoolVar(&opts.Verbose, "v", false, "Verbose") - flag.Parse() - - if flag.NArg() > 0 && opts.Folder == "." { - opts.Folder = flag.Args()[0] - } - - log.Printf("Serving %s on http://%s/...", opts.Folder, opts.ListenAddress) - fmt.Println(http.ListenAndServe(opts.ListenAddress, loglayer(http.FileServer(http.Dir(opts.Folder))))) -} - -func loglayer(handler http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - fullRequest, _ := httputil.DumpRequest(r, true) - lrw := newLoggingResponseWriter(w) - handler.ServeHTTP(lrw, r) - - if opts.Verbose { - headers := new(bytes.Buffer) - lrw.Header().Write(headers) //nolint - log.Printf("\nRemote Address: %s\n%s\n%s %d %s\n%s\n%s\n", r.RemoteAddr, string(fullRequest), r.Proto, lrw.statusCode, http.StatusText(lrw.statusCode), headers.String(), string(lrw.Data)) - } else { - log.Printf("%s \"%s %s %s\" %d %d", r.RemoteAddr, r.Method, r.URL, r.Proto, lrw.statusCode, len(lrw.Data)) - } - }) -} - -type loggingResponseWriter struct { - http.ResponseWriter - statusCode int - Data []byte -} - -func newLoggingResponseWriter(w http.ResponseWriter) *loggingResponseWriter { - return &loggingResponseWriter{w, http.StatusOK, []byte{}} -} - -func (lrw *loggingResponseWriter) Write(data []byte) (int, error) { - lrw.Data = append(lrw.Data, data...) - return lrw.ResponseWriter.Write(data) -} - -func (lrw *loggingResponseWriter) Header() http.Header { - return lrw.ResponseWriter.Header() -} - -func (lrw *loggingResponseWriter) WriteHeader(code int) { - lrw.statusCode = code - lrw.ResponseWriter.WriteHeader(code) -} diff --git a/test/fixture/pythonliststyle/test file.txt b/test/fixture/pythonliststyle/test file.txt new file mode 100644 index 0000000..5cd8fbc --- /dev/null +++ b/test/fixture/pythonliststyle/test file.txt @@ -0,0 +1,2 @@ +This is the content of "test file.txt". +这是“test file.txt”文件的内容。 diff --git a/test/pythonliststyle_test.go b/test/pythonliststyle_test.go new file mode 100644 index 0000000..0ee449a --- /dev/null +++ b/test/pythonliststyle_test.go @@ -0,0 +1,76 @@ +package test + +import ( + "bytes" + "github.com/projectdiscovery/simplehttpserver/pkg/httpserver" + "io" + "net/http/httptest" + "os" + "strings" + "testing" +) + +func TestServePythonStyleHtmlPageForDirectories(t *testing.T) { + const want = ` + + + + +Directory listing for / + + +

    Directory listing for /

    +
    + +
    + + +` + py := httpserver.PythonStyle("./fixture/pythonliststyle") + + w := httptest.NewRecorder() + py.ServeHTTP(w, httptest.NewRequest("GET", "http://example.com/", nil)) + b, _ := io.ReadAll(w.Result().Body) + + body := string(b) + if strings.Compare(want, body) != 0 { + t.Errorf("want:\n%s\ngot:\n%s", want, body) + } +} + +func TestServeFileContentForFiles(t *testing.T) { + want, _ := os.ReadFile("./fixture/pythonliststyle/test file.txt") + + py := httpserver.PythonStyle("./fixture/pythonliststyle") + + w := httptest.NewRecorder() + py.ServeHTTP(w, httptest.NewRequest( + "GET", + "http://example.com/test%20file.txt", + nil, + )) + got, _ := io.ReadAll(w.Result().Body) + if !bytes.Equal(want, got) { + t.Errorf("want:\n%x\ngot:\n%x", want, got) + } +} + +func TestResponseNotFound(t *testing.T) { + const want = `404 page not found +` + + py := httpserver.PythonStyle("./fixture/pythonliststyle") + + w := httptest.NewRecorder() + py.ServeHTTP(w, httptest.NewRequest( + "GET", + "http://example.com/does-not-exist.txt", + nil, + )) + got, _ := io.ReadAll(w.Result().Body) + if strings.Compare(want, string(got)) != 0 { + t.Errorf("want:\n%s\ngot:\n%s", want, got) + } +}