diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..4d5617f --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,34 @@ +# 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" + commit-message: + prefix: "chore" + include: "scope" + + # Maintain dependencies for go modules + - package-ecosystem: "gomod" + directory: "/" + schedule: + interval: "weekly" + commit-message: + prefix: "chore" + include: "scope" + + # Maintain dependencies for docker + - package-ecosystem: "docker" + directory: "/" + schedule: + interval: "weekly" + commit-message: + prefix: "chore" + include: "scope" diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index a157f84..4179b91 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -6,27 +6,18 @@ on: pull_request: jobs: - lint: - name: golangci-lint + golangci-lint: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v2 - name: Run golangci-lint - uses: golangci/golangci-lint-action@v2.2.0 + 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 - # Optional: working directory, useful for monorepos - # working-directory: somedir - - # Optional: golangci-lint command line arguments. - # args: --issues-exit-code=0 - - # Optional: show only new issues if it's a pull request. The default value is `false`. - # only-new-issues: true build: name: Build runs-on: ubuntu-latest @@ -41,8 +32,8 @@ jobs: - name: Test run: go test . - working-directory: . + working-directory: cmd/simplehttpserver - name: Build run: go build . - working-directory: . + working-directory: cmd/simplehttpserver/ 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 index d5e9089..31b66f3 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -41,8 +41,6 @@ linters-settings: - (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf # lll: # line-length: 140 - maligned: - suggest-new: true misspell: locale: US nolintlint: @@ -73,14 +71,12 @@ linters: - gosimple - govet - ineffassign - - interfacer - - maligned - misspell - nakedret - noctx - nolintlint - rowserrcheck - - scopelint + - exportloopref - staticcheck - structcheck - stylecheck diff --git a/.goreleaser.yml b/.goreleaser.yml index 50bb74f..f6cb1e2 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,6 +1,6 @@ builds: - binary: simplehttpserver - main: simplehttpserver.go + main: cmd/simplehttpserver/simplehttpserver.go goos: - linux - windows diff --git a/Dockerfile b/Dockerfile index a01590c..0919a3c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,9 @@ -FROM golang:1.14-alpine AS builder +FROM golang:1.16-alpine AS builder RUN apk add --no-cache git -RUN GO111MODULE=auto go get -u -v github.com/projectdiscovery/simplehttpserver +RUN GO111MODULE=auto go get -u -v github.com/projectdiscovery/simplehttpserver/cmd/simplehttpserver FROM alpine:latest +RUN apk add --no-cache bind-tools ca-certificates COPY --from=builder /go/bin/simplehttpserver /usr/local/bin/ ENTRYPOINT ["simplehttpserver"] diff --git a/README.md b/README.md index f88164f..426fd74 100644 --- a/README.md +++ b/README.md @@ -1,65 +1,44 @@ -

- simplehttpserver -
-

+

SimpleHTTPserver

+

Go alternative of python SimpleHTTPServer

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

+ + + + + + +

-# Resources +

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

-- [Features](#features) -- [Usage](#usage) -- [Installation Instructions](#installation-instructions) -- [Running simplehttpserver](#running-simplehttpserver-in-the-current-folder ) -- [Thanks](#thanks) - -# Features - -

- simplehttpserver -
-

- - - File server in arbitrary directory - - Full request/response dump - - Configurable ip address and listening port - - -# Installation Instructions +--- +SimpleHTTPserver is a go enhanced version of the well known python simplehttpserver with in addition a fully customizable TCP server, both supporting TLS. -### From Binary -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. +# Features -```sh -Download latest binary from https://github.com/projectdiscovery/simplehttpserver/releases +- HTTPS support +- File server in arbitrary directory +- Full request/response dump +- Configurable ip address and listening port +- Configurable HTTP/TCP server with customizable response via YAML template -▶ tar -xvf simplehttpserver-linux-amd64.tar -▶ mv simplehttpserver-linux-amd64 /usr/local/bin/simplehttpserver -▶ simplehttpserver -h -``` -### From Source +# Installing SimpleHTTPserver -simplehttpserver requires **go1.14+** to install successfully. Run the following command to get the repo - +SimpleHTTPserver requires **go1.14+** to install successfully. Run the following command to get the repo - ```sh -▶ GO111MODULE=on go get -v github.com/projectdiscovery/simplehttpserver -``` - -### From Github - -```sh -▶ git clone https://github.com/projectdiscovery/simplehttpserver.git; cd simplehttpserver; go build; mv simplehttpserver /usr/local/bin/; simplehttpserver -h +▶ GO111MODULE=on go get -v github.com/projectdiscovery/cmd/simplehttpserver ``` # Usage @@ -70,11 +49,23 @@ 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 | -| v | Verbose (dump request/response, default false) | simplehttpserver -v | +| 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 -v | +| 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 | ### Running simplehttpserver in the current folder @@ -87,6 +78,91 @@ This will run the tool exposing the current directory on port 8000 2021/01/11 21:41:15 [::1]:50181 "GET /favicon.ico HTTP/1.1" 404 19 ``` +### Running simplehttpserver in the current folder with HTTPS + +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 +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 +▶ 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 +``` + +### 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 -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 +``` + +### Running TCP server with custom responses + +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 +``` + +The rules are written as follows: +```yaml +rules: + - match: regex + response: response data +``` + +For example to handle two different paths simulating an HTTP server or SMTP commands: +```yaml +rules: + # HTTP Requests + - match: GET /path1 + 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 + response: | + HTTP/1.0 404 OK + Server: httpd/2.0 + + Not found + # SMTP Commands + - match: "EHLO example.com" + 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 +``` + # 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/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..f4c4a9d 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,10 @@ module github.com/projectdiscovery/simplehttpserver -go 1.15 +go 1.14 + +require ( + github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 + github.com/projectdiscovery/gologger v1.1.4 + github.com/projectdiscovery/sslcert v0.0.0-20210416140253-8f56bec1bb5e + gopkg.in/yaml.v2 v2.4.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..aafe4a2 --- /dev/null +++ b/go.sum @@ -0,0 +1,41 @@ +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/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/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/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/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/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/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= +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/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..f727190 --- /dev/null +++ b/internal/runner/banner.go @@ -0,0 +1,24 @@ +package runner + +import "github.com/projectdiscovery/gologger" + +const banner = ` + _____ _ __ __ __________________ + / ___/(_)___ ___ ____ / /__ / / / /_ __/_ __/ __ \________ ______ _____ _____ + \__ \/ / __ -__ \/ __ \/ / _ \/ /_/ / / / / / / /_/ / ___/ _ \/ ___/ | / / _ \/ ___/ + ___/ / / / / / / / /_/ / / __/ __ / / / / / / ____(__ ) __/ / | |/ / __/ / +/____/_/_/ /_/ /_/ .___/_/\___/_/ /_/ /_/ /_/ /_/ /____/\___/_/ |___/\___/_/ + /_/ - v0.0.2 +` + +// Version is the current version +const Version = `0.0.2` + +// 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/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..bf69db3 --- /dev/null +++ b/internal/runner/options.go @@ -0,0 +1,104 @@ +package runner + +import ( + "flag" + "os" + "path/filepath" + "strings" + + "github.com/projectdiscovery/gologger" + "github.com/projectdiscovery/gologger/levels" +) + +// 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 +} + +// 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") + flag.StringVar(&options.Folder, "path", ".", "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.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 +} diff --git a/internal/runner/runner.go b/internal/runner/runner.go new file mode 100644 index 0000000..7d69e25 --- /dev/null +++ b/internal/runner/runner.go @@ -0,0 +1,98 @@ +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" +) + +// 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 + } + 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, + }) + if err != nil { + return nil, err + } + r.httpServer = httpServer + + return &r, nil +} + +// Run logic +func (r *Runner) Run() error { + if r.options.EnableTCP { + 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/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..f2eff4b --- /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.HandlerFunc { + 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/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/httpserver.go b/pkg/httpserver/httpserver.go new file mode 100644 index 0000000..1747017 --- /dev/null +++ b/pkg/httpserver/httpserver.go @@ -0,0 +1,70 @@ +package httpserver + +import ( + "net/http" + + "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 +} + +// HTTPServer instance +type HTTPServer struct { + options *Options + layers http.Handler +} + +// New http server instance with options +func New(options *Options) (*HTTPServer, error) { + var h HTTPServer + EnableUpload = options.EnableUpload + EnableVerbose = options.Verbose + layers := h.loglayer(http.FileServer(http.Dir(options.Folder))) + if options.BasicAuthUsername != "" || options.BasicAuthPassword != "" { + layers = h.loglayer(h.basicauthlayer(http.FileServer(http.Dir(options.Folder)))) + } + + return &HTTPServer{options: options, layers: layers}, nil +} + +// ListenAndServe requests over http +func (t *HTTPServer) ListenAndServe() error { + return http.ListenAndServe(t.options.ListenAddress, t.layers) +} + +// 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 := &http.Server{ + Addr: t.options.ListenAddress, + TLSConfig: tlsConfig, + } + httpServer.Handler = t.layers + 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..1e64b8f --- /dev/null +++ b/pkg/httpserver/loglayer.go @@ -0,0 +1,72 @@ +package httpserver + +import ( + "bytes" + "io/ioutil" + "net/http" + "net/http/httputil" + "path" + + "github.com/projectdiscovery/gologger" +) + +// Convenience globals +var ( + EnableUpload bool + EnableVerbose bool +) + +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) + } + } + + 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)) + } else { + gologger.Print().Msgf("%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{}} +} + +// Write the data +func (lrw *loggingResponseWriter) Write(data []byte) (int, error) { + lrw.Data = append(lrw.Data, 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/uploadlayer.go b/pkg/httpserver/uploadlayer.go new file mode 100644 index 0000000..2663fba --- /dev/null +++ b/pkg/httpserver/uploadlayer.go @@ -0,0 +1,7 @@ +package httpserver + +import "io/ioutil" + +func handleUpload(file string, data []byte) error { + return ioutil.WriteFile(file, data, 0655) +} 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..ec15da0 --- /dev/null +++ b/pkg/tcpserver/responseengine.go @@ -0,0 +1,16 @@ +package tcpserver + +import ( + "errors" +) + +// BuildResponse according to rules +func (t *TCPServer) BuildResponse(data []byte) ([]byte, error) { + // Process all the rules + for _, rule := range t.options.rules { + if rule.matchRegex.Match(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..903331b --- /dev/null +++ b/pkg/tcpserver/rule.go @@ -0,0 +1,25 @@ +package tcpserver + +import "regexp" + +// RulesConfiguration from yaml +type RulesConfiguration struct { + Rules []Rule `yaml:"rules"` +} + +// Rule to apply to various requests +type Rule struct { + Match string `yaml:"match,omitempty"` + matchRegex *regexp.Regexp + Response string `yaml:"response,omitempty"` +} + +// NewRule from model +func NewRule(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 +} diff --git a/pkg/tcpserver/tcpserver.go b/pkg/tcpserver/tcpserver.go new file mode 100644 index 0000000..876fbb4 --- /dev/null +++ b/pkg/tcpserver/tcpserver.go @@ -0,0 +1,145 @@ +package tcpserver + +import ( + "crypto/tls" + "io/ioutil" + "net" + "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 +} + +// TCPServer instance +type TCPServer struct { + options *Options + listener net.Listener +} + +// New tcp server instance with specified options +func New(options *Options) (*TCPServer, error) { + return &TCPServer{options: options}, nil +} + +// AddRule to the server +func (t *TCPServer) AddRule(rule Rule) error { + t.options.rules = append(t.options.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) error { + defer conn.Close() //nolint + + 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) + if err != nil { + return err + } + + gologger.Print().Msgf("%s\n", buf) + + resp, err := t.BuildResponse(buf) + if err != nil { + 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) //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 := ioutil.ReadFile(templatePath) + if err != nil { + return err + } + err = yaml.Unmarshal(yamlFile, &config) + if err != nil { + return err + } + + for _, ruleTemplate := range config.Rules { + rule, err := NewRule(ruleTemplate.Match, ruleTemplate.Response) + if err != nil { + return err + } + t.options.rules = append(t.options.rules, *rule) + } + + return nil +} 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) -}