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 index 4d5617f..69d9543 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -11,6 +11,7 @@ updates: directory: "/" schedule: interval: "weekly" + target-branch: "dev" commit-message: prefix: "chore" include: "scope" @@ -20,6 +21,7 @@ updates: directory: "/" schedule: interval: "weekly" + target-branch: "dev" commit-message: prefix: "chore" include: "scope" @@ -29,6 +31,7 @@ updates: directory: "/" schedule: interval: "weekly" + target-branch: "dev" commit-message: prefix: "chore" - include: "scope" + 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 8d43441..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.5.2 - 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: cmd/simplehttpserver - - - name: Build - run: go build . - working-directory: cmd/simplehttpserver/ 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/.golangci.yml b/.golangci.yml deleted file mode 100644 index 31b66f3..0000000 --- a/.golangci.yml +++ /dev/null @@ -1,118 +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 - 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 - - misspell - - nakedret - - noctx - - nolintlint - - rowserrcheck - - exportloopref - - 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 f6cb1e2..e0a4795 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,3 +1,7 @@ +before: + hooks: + - go mod tidy + builds: - binary: simplehttpserver main: cmd/simplehttpserver/simplehttpserver.go @@ -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 806904b..cbcb4d2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ -FROM golang:1.16-alpine as build-env -RUN GO111MODULE=on go get -v github.com/projectdiscovery/simplehttpserver/cmd/simplehttpserver +FROM golang:1.20.2-alpine as build-env +RUN go install -v github.com/projectdiscovery/simplehttpserver/cmd/simplehttpserver@latest FROM alpine:latest RUN apk add --no-cache bind-tools ca-certificates diff --git a/README.md b/README.md index fba4ea3..8d087ae 100644 --- a/README.md +++ b/README.md @@ -26,19 +26,19 @@ SimpleHTTPserver is a go enhanced version of the well known python simplehttpser # Features -- HTTPS support -- File server in arbitrary directory -- Full request/response dump +- 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.14+** to install successfully. Run the following command to get the repo - +SimpleHTTPserver requires **go1.17+** to install successfully. Run the following command to get the repo - ```sh -▶ GO111MODULE=on go get -v github.com/projectdiscovery/simplehttpserver/cmd/simplehttpserver +go install -v github.com/projectdiscovery/simplehttpserver/cmd/simplehttpserver@latest ``` # Usage @@ -49,30 +49,37 @@ simplehttpserver -h 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 | -| https | Enable HTTPS in case of http server | simplehttpserver -https | -| cert | HTTPS/TLS certificate (self generated if not specified) | simplehttpserver -cert cert.pem | -| key | HTTPS/TLS certificate private key (self generated if not specified) | simplehttpserver -key cert.key | -| domain | Domain name to use for the self-generated certificate | simplehttpserver -domain projectdiscovery.io | -| 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 | +| 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'` | ### Running simplehttpserver in the current folder This will run the tool exposing the current directory on port 8000 ```sh -▶ simplehttpserver +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 @@ -83,7 +90,8 @@ This will run the tool exposing the current directory on port 8000 This will run the tool exposing the current directory on port 8000 over HTTPS with user provided certificate: ```sh -▶ simplehttpserver -https -cert cert.pen -key cert.key +simplehttpserver -https -cert cert.pen -key cert.key + 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 @@ -91,7 +99,8 @@ This will run the tool exposing the current directory on port 8000 over HTTPS wi Instead, to run with self-signed certificate and specific domain name: ```sh -▶ simplehttpserver -https -domain localhost +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 @@ -102,13 +111,14 @@ Instead, to run with self-signed certificate and specific domain name: This will run the tool and will request the user to enter username and password before authorizing file uploads ```sh -▶ simplehttpserver -basic-auth root:root -upload +simplehttpserver -basic-auth root:root -upload + 2021/01/11 21:40:48 Serving . on http://0.0.0.0:8000/... ``` 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 +curl -v --user 'root:root' --upload-file file.txt http://localhost:8000/file.txt ``` ### Running TCP server with custom responses @@ -116,13 +126,15 @@ To upload files use the following curl request with basic auth header: This will run the tool as TLS TCP server and enable custom responses based on YAML templates: ```sh -▶ simplehttpserver -rule rules.yaml -tcp -tls -domain localhost +simplehttpserver -rules rules.yaml -tcp -tls -domain localhost ``` The rules are written as follows: ```yaml rules: - - match: regex + - match: regex-match + match-contains: literal-match + name: rule-name response: response data ``` @@ -131,6 +143,7 @@ For example to handle two different paths simulating an HTTP server or SMTP comm rules: # HTTP Requests - match: GET /path1 + name: redirect response: | HTTP/1.0 200 OK Server: httpd/2.0 @@ -143,6 +156,7 @@ rules: - match: GET /path2 + name: "404" response: | HTTP/1.0 404 OK Server: httpd/2.0 @@ -150,6 +164,7 @@ rules: Not found # SMTP Commands - match: "EHLO example.com" + name: smtp response: | 250-localhost Nice to meet you, [127.0.0.1] 250-PIPELINING @@ -161,8 +176,20 @@ rules: 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. 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/go.mod b/go.mod index f4c4a9d..01a2ea1 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,27 @@ module github.com/projectdiscovery/simplehttpserver -go 1.14 +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.4 + 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 index aafe4a2..866d4e0 100644 --- a/go.sum +++ b/go.sum @@ -2,9 +2,22 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 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.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +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= @@ -13,27 +26,43 @@ 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 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +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.4 h1:qWxGUq7ukHWT849uGPkagPKF3yBPYAsTtMKunQ8O2VI= -github.com/projectdiscovery/gologger v1.1.4/go.mod h1:Bhb6Bdx2PV1nMaFLoXNBmHIU85iROS9y1tBuv7T5pMY= +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= diff --git a/internal/runner/banner.go b/internal/runner/banner.go index a6b729d..ed4193f 100644 --- a/internal/runner/banner.go +++ b/internal/runner/banner.go @@ -8,17 +8,14 @@ const banner = ` \__ \/ / __ -__ \/ __ \/ / _ \/ /_/ / / / / / / /_/ / ___/ _ \/ ___/ | / / _ \/ ___/ ___/ / / / / / / / /_/ / / __/ __ / / / / / / ____(__ ) __/ / | |/ / __/ / /____/_/_/ /_/ /_/ .___/_/\___/_/ /_/ /_/ /_/ /_/ /____/\___/_/ |___/\___/_/ - /_/ - v0.0.3 + /_/ - v0.0.6 ` // Version is the current version -const Version = `0.0.3` +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") - - gologger.Print().Msgf("Use with caution. You are responsible for your actions\n") - gologger.Print().Msgf("Developers assume no liability and are not responsible for any misuse or damage.\n") } diff --git a/internal/runner/options.go b/internal/runner/options.go index bf69db3..2890a04 100644 --- a/internal/runner/options.go +++ b/internal/runner/options.go @@ -2,33 +2,42 @@ 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 + 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 @@ -38,7 +47,11 @@ func ParseOptions() *Options { 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") - flag.StringVar(&options.Folder, "path", ".", "Folder") + 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") @@ -49,7 +62,13 @@ func ParseOptions() *Options { 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 @@ -102,3 +121,21 @@ func (options *Options) FolderAbsPath() string { } 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 index 7d69e25..dc63940 100644 --- a/internal/runner/runner.go +++ b/internal/runner/runner.go @@ -5,6 +5,7 @@ import ( "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. @@ -41,6 +42,12 @@ func New(options *Options) (*Runner, error) { 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 } @@ -57,6 +64,13 @@ func New(options *Options) (*Runner, error) { 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 @@ -69,6 +83,10 @@ func New(options *Options) (*Runner, error) { // 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() } 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/httpserver/authlayer.go b/pkg/httpserver/authlayer.go index f2eff4b..297d863 100644 --- a/pkg/httpserver/authlayer.go +++ b/pkg/httpserver/authlayer.go @@ -5,7 +5,7 @@ import ( "net/http" ) -func (t *HTTPServer) basicauthlayer(handler http.Handler) http.HandlerFunc { +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 { 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/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 index d516caf..5dd65a6 100644 --- a/pkg/httpserver/httpserver.go +++ b/pkg/httpserver/httpserver.go @@ -1,7 +1,11 @@ package httpserver import ( + "crypto/tls" + "errors" "net/http" + "os" + "path/filepath" "github.com/projectdiscovery/sslcert" ) @@ -19,6 +23,13 @@ type Options struct { BasicAuthPassword string BasicAuthReal string Verbose bool + Sandbox bool + HTTP1Only bool + MaxFileSize int // 50Mb + MaxDumpBodySize int64 + Python bool + CORS bool + HTTPHeaders []HTTPHeader } // HTTPServer instance @@ -27,23 +38,76 @@ type HTTPServer struct { 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 - h.layers = h.loglayer(http.FileServer(http.Dir(options.Folder))) + 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 != "" { - h.layers = h.loglayer(h.basicauthlayer(http.FileServer(http.Dir(options.Folder)))) + 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 { - return http.ListenAndServe(t.options.ListenAddress, t.layers) + httpServer := t.makeHTTPServer(nil) + return httpServer.ListenAndServe() } // ListenAndServeTLS requests over https @@ -55,11 +119,7 @@ func (t *HTTPServer) ListenAndServeTLS() error { if err != nil { return err } - httpServer := &http.Server{ - Addr: t.options.ListenAddress, - TLSConfig: tlsConfig, - } - httpServer.Handler = t.layers + httpServer := t.makeHTTPServer(tlsConfig) return httpServer.ListenAndServeTLS("", "") } return http.ListenAndServeTLS(t.options.ListenAddress, t.options.Certificate, t.options.CertificateKey, t.layers) diff --git a/pkg/httpserver/loglayer.go b/pkg/httpserver/loglayer.go index 1e64b8f..f3fb4f7 100644 --- a/pkg/httpserver/loglayer.go +++ b/pkg/httpserver/loglayer.go @@ -2,11 +2,9 @@ package httpserver import ( "bytes" - "io/ioutil" "net/http" "net/http/httputil" - "path" - + "time" "github.com/projectdiscovery/gologger" ) @@ -16,47 +14,49 @@ var ( 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) { - fullRequest, _ := httputil.DumpRequest(r, true) - lrw := newLoggingResponseWriter(w) - handler.ServeHTTP(lrw, r) - - // Handles file write if enabled - if EnableUpload && r.Method == http.MethodPut { - data, err := ioutil.ReadAll(r.Body) - if err != nil { - gologger.Print().Msgf("%s\n", err) - } - err = handleUpload(path.Base(r.URL.Path), data) - if err != nil { - gologger.Print().Msgf("%s\n", err) - } + 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("\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)) + 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\" %d %d", r.RemoteAddr, r.Method, r.URL, r.Proto, lrw.statusCode, len(lrw.Data)) + 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 + statusCode int + Data []byte + Size int + MaxDumpSize int64 } -func newLoggingResponseWriter(w http.ResponseWriter) *loggingResponseWriter { - return &loggingResponseWriter{w, http.StatusOK, []byte{}} +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) { - lrw.Data = append(lrw.Data, data...) + if len(lrw.Data) < int(lrw.MaxDumpSize) { + lrw.Data = append(lrw.Data, data...) + } + lrw.Size += len(data) return lrw.ResponseWriter.Write(data) } 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(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 index 2663fba..571d3c1 100644 --- a/pkg/httpserver/uploadlayer.go +++ b/pkg/httpserver/uploadlayer.go @@ -1,7 +1,99 @@ package httpserver -import "io/ioutil" +import ( + "errors" + "io" + "net/http" + "os" + "path" + "path/filepath" + "strings" -func handleUpload(file string, data []byte) error { - return ioutil.WriteFile(file, data, 0655) + "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/responseengine.go b/pkg/tcpserver/responseengine.go index ec15da0..80fb795 100644 --- a/pkg/tcpserver/responseengine.go +++ b/pkg/tcpserver/responseengine.go @@ -6,9 +6,12 @@ import ( // 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.options.rules { - if rule.matchRegex.Match(data) { + for _, rule := range t.rules { + if rule.MatchInput(data) { return []byte(rule.Response), nil } } diff --git a/pkg/tcpserver/rule.go b/pkg/tcpserver/rule.go index 903331b..aa9e6e8 100644 --- a/pkg/tcpserver/rule.go +++ b/pkg/tcpserver/rule.go @@ -1,6 +1,9 @@ package tcpserver -import "regexp" +import ( + "regexp" + "strings" +) // RulesConfiguration from yaml type RulesConfiguration struct { @@ -9,13 +12,20 @@ type RulesConfiguration struct { // Rule to apply to various requests type Rule struct { - Match string `yaml:"match,omitempty"` - matchRegex *regexp.Regexp - Response string `yaml:"response,omitempty"` + 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 from model +// 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 @@ -23,3 +33,33 @@ func NewRule(match, response string) (*Rule, error) { 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 index 876fbb4..3b996f5 100644 --- a/pkg/tcpserver/tcpserver.go +++ b/pkg/tcpserver/tcpserver.go @@ -1,9 +1,12 @@ package tcpserver import ( + "context" "crypto/tls" - "io/ioutil" + "errors" + "os" "net" + "sync" "time" "github.com/projectdiscovery/gologger" @@ -24,20 +27,35 @@ type Options struct { 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) { - return &TCPServer{options: options}, nil + 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.options.rules = append(t.options.rules, rule) + t.mux.Lock() + defer t.mux.Unlock() + + t.rules = append(t.rules, rule) return nil } @@ -51,23 +69,27 @@ func (t *TCPServer) ListenAndServe() error { return t.run() } -func (t *TCPServer) handleConnection(conn net.Conn) error { +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) } - _, err := conn.Read(buf) + n, err := conn.Read(buf) if err != nil { return err } - gologger.Print().Msgf("%s\n", buf) + gologger.Print().Msgf("%s\n", buf[:n]) - resp, err := t.BuildResponse(buf) + resp, err := callback(ctx, buf[:n]) if err != nil { + gologger.Info().Msgf("Closing connection: %s\n", err) return err } @@ -112,7 +134,7 @@ func (t *TCPServer) run() error { if err != nil { return err } - go t.handleConnection(c) //nolint + go t.handleConnection(c, t.HandleMessageFnc) //nolint } } @@ -124,7 +146,7 @@ func (t *TCPServer) Close() error { // LoadTemplate from yaml func (t *TCPServer) LoadTemplate(templatePath string) error { var config RulesConfiguration - yamlFile, err := ioutil.ReadFile(templatePath) + yamlFile, err := os.ReadFile(templatePath) if err != nil { return err } @@ -133,13 +155,54 @@ func (t *TCPServer) LoadTemplate(templatePath string) error { return err } + t.mux.Lock() + defer t.mux.Unlock() + + t.rules = make([]Rule, 0) for _, ruleTemplate := range config.Rules { - rule, err := NewRule(ruleTemplate.Match, ruleTemplate.Response) + rule, err := NewRuleFromTemplate(ruleTemplate) if err != nil { return err } - t.options.rules = append(t.options.rules, *rule) + 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/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) + } +}