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
+Go alternative of python SimpleHTTPServer
-[](https://opensource.org/licenses/MIT)
-[](https://goreportcard.com/report/github.com/projectdiscovery/simplehttpserver)
-[](https://github.com/projectdiscovery/simplehttpserver/issues)
-[](https://github.com/projectdiscovery/simplehttpserver/releases)
-[](https://twitter.com/pdiscoveryio)
-[](https://hub.docker.com/r/projectdiscovery/simplehttpserver)
-[](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
-
-
-
-
+- 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)
+ }
+}