diff --git a/.bumpversion.cfg b/.bumpversion.cfg deleted file mode 100644 index c042e94..0000000 --- a/.bumpversion.cfg +++ /dev/null @@ -1,8 +0,0 @@ -[bumpversion] -current_version = 0.1.4 -commit = True -tag = True - -[bumpversion:file:README.md] -search = version-{current_version}-orange.svg -replace = version-{new_version}-orange.svg diff --git a/.bumpversion.toml b/.bumpversion.toml new file mode 100644 index 0000000..606a9f6 --- /dev/null +++ b/.bumpversion.toml @@ -0,0 +1,30 @@ +[tool.bumpversion] +current_version = "0.1.4" +parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)" +serialize = ["{major}.{minor}.{patch}"] +search = "{current_version}" +replace = "{new_version}" +regex = false +ignore_missing_version = false +ignore_missing_files = false +tag = true +sign_tags = false +tag_name = "v{new_version}" +tag_message = "Bump version: {current_version} → {new_version}" +allow_dirty = false +commit = true +message = "Bump version: {current_version} → {new_version}" +commit_args = "--no-verify" +setup_hooks = [] +pre_commit_hooks = [] +post_commit_hooks = [] + +[[tool.bumpversion.files]] +filename = "README.md" +search = "version-{current_version}-orange.svg" +replace = "version-{new_version}-orange.svg" + +[[tool.bumpversion.files]] +filename = "release/release.go" +search = const Version string = "{current_version}" +replace = const Version string = "{new_version}" diff --git a/.github/workflows/go-lint.yml b/.github/workflows/go-lint.yml index b055e03..16c4ff5 100644 --- a/.github/workflows/go-lint.yml +++ b/.github/workflows/go-lint.yml @@ -1,22 +1,31 @@ -name: Golang CI Lint +name: golangci-lint on: pull_request: + paths: + - '**.go' + push: + branches: + - main + paths: + - '**.go' concurrency: - group: golangci-lint + group: basichttpdebugger-golangci-lint cancel-in-progress: true jobs: golangci: name: golangci linter - runs-on: ubuntu-latest - env: - GOPRIVATE: github.com/vbyazilim + runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version-file: "go.mod" + - name: golangci-lint - uses: golangci/golangci-lint-action@v3 + uses: golangci/golangci-lint-action@v6 with: - version: latest + version: v1.62 args: --timeout=5m diff --git a/.github/workflows/push-to-dockerhub.yml b/.github/workflows/push-to-dockerhub.yml index f1b9c66..c92a99f 100644 --- a/.github/workflows/push-to-dockerhub.yml +++ b/.github/workflows/push-to-dockerhub.yml @@ -5,25 +5,21 @@ on: jobs: docker: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - - - name: Checkout + - name: Checkout uses: actions/checkout@v4 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx + + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - - name: Login to DockerHub + + - name: Login to DockerHub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Build and push + + - name: Build and push uses: docker/build-push-action@v6 with: context: . diff --git a/.github/workflows/push-to-github-cr.yml b/.github/workflows/push-to-github-cr.yml index 320092e..7e43b05 100644 --- a/.github/workflows/push-to-github-cr.yml +++ b/.github/workflows/push-to-github-cr.yml @@ -5,14 +5,11 @@ on: jobs: docker: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: Checkout uses: actions/checkout@v4 - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 diff --git a/.golangci.yml b/.golangci.yml index fb89bed..9f8ef53 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,127 +1,238 @@ run: - concurrency: 4 - timeout: 1m + tests: false linters-settings: - tagalign: - align: false - sort: false - govet: - check-shadowing: true - enable: - - asmdecl - - assign - - atomic - - atomicalign - - bools - - buildtag - - cgocall - - composites - - copylocks - - deepequalerrors - - errorsas - - findcall - - framepointer - - httpresponse - - ifaceassert - - loopclosure - - lostcancel - - nilfunc - - nilness - - printf - - reflectvaluecompare - - shadow - - shift - - sigchanyzer - - sortslice - - stdmethods - - stringintconv - - structtag - - testinggoroutine - - tests - - unmarshal - - unreachable - - unsafeptr - - unusedresult - + # --------------------------------------------------------------------------- + nestif: + min-complexity: 6 + # --------------------------------------------------------------------------- + mnd: + ignored-numbers: + - '0' + - '2' + # --------------------------------------------------------------------------- + varnamelen: + min-name-length: 2 + ignore-names: + - c + - i + - q + - w + - r + - t + # --------------------------------------------------------------------------- errcheck: + check-type-assertions: true + # check-blank: true exclude-functions: + - os.Setenv + - fmt.Fprint - fmt.Fprintf - + - fmt.Fprintln + - fmt.Sscanf + - twerrors.(*TWError).Error + # --------------------------------------------------------------------------- + govet: + enable-all: true + settings: + shadow: + strict: true + # --------------------------------------------------------------------------- + wrapcheck: + ignoreSigs: + - .Errorf( + - errors.New( + - errors.Unwrap( + - errors.Join( + - .Wrap( + - .Wrapf( + - .WithMessage( + - .WithMessagef( + - .WithStack( + - .JSON + - .SendStatus + - services.HandleError + ignoreSigRegexps: + - \.New.*Error\( + ignorePackageGlobs: + - encoding/* + - github.com/pkg/* + # --------------------------------------------------------------------------- + gosec: + config: + G104: + os: + - Setenv + fmt: + - Fscanf + - Sscanf + # --------------------------------------------------------------------------- revive: ignore-generated-header: true severity: warning + # enable-all-rules: true rules: - - name: exported - severity: warning - - name: error-return - severity: warning + - name: atomic + - name: bool-literal-in-expr + - name: blank-imports + - name: comment-spacings + - name: confusing-naming + - name: confusing-results + - name: context-as-argument + arguments: + - allowTypesBefore: "*testing.T" + - name: context-keys-type + - name: datarace + - name: deep-exit + - name: defer + arguments: + - ["call-chain", "loop"] + - name: dot-imports + - name: duplicated-imports + - name: early-return + arguments: + - "preserveScope" + - name: empty-block + - name: empty-lines - name: error-naming - severity: warning + - name: error-return + - name: error-strings + - name: errorf + - name: exported + - name: get-return + - name: identical-branches - name: if-return - severity: warning - - name: var-naming - severity: warning - - name: var-declaration - severity: warning + - name: import-alias-naming + arguments: + - "^[a-z][a-z0-9]{0,}$" + - name: import-shadowing + - name: increment-decrement + - name: indent-error-flow + arguments: + - "preserveScope" + - name: line-length-limit + arguments: [120] # matches GOLINES + - name: modifies-parameter + - name: modifies-value-receiver + - name: optimize-operands-order + - name: range + - name: range-val-address + - name: range-val-in-closure - name: receiver-naming - severity: warning - - name: errorf - severity: warning - - name: empty-block - severity: warning - - name: unused-parameter - severity: warning - - name: unreachable-code - severity: warning - name: redefines-builtin-id - severity: warning + - name: redundant-import-alias + - name: string-of-int + - name: struct-tag + arguments: + - "json,inline" + - "bson,outline,gnu" - name: superfluous-else - severity: warning - - name: unexported-return - severity: warning - - name: indent-error-flow - severity: warning - - name: blank-imports - severity: warning - - name: range - severity: warning + arguments: + - "preserveScope" + - name: time-equal - name: time-naming - severity: warning - - name: context-as-argument - severity: warning - - name: context-keys-type - severity: warning - - name: indent-error-flow - severity: warning + - name: unchecked-type-assertion + arguments: + - acceptIgnoredAssertionResult: true + - name: unconditional-recursion + - name: unexported-naming + - name: unexported-return + - name: unhandled-error + arguments: + - "fmt.Fprint" + - "fmt.Printf" + - "fmt.Println" + - "fmt.Fprintf" + - "fmt.Fprintln" + - "fmt.Sscanf" + - "os.Setenv" + - name: unnecessary-stmt + - name: unreachable-code + - name: unused-parameter + arguments: + - allowRegex: "^_" + - name: unused-receiver + - name: use-any + - name: useless-break + - name: var-declaration + - name: var-naming + arguments: + - ["ID"] # AllowList + - ["VM"] # DenyList + - - upperCaseConst: true + - name: waitgroup-by-value + # --------------------------------------------------------------------------- linters: disable-all: true enable: - - asciicheck - - durationcheck - - errcheck - - errorlint - - exhaustive - - gosec - - govet - - makezero - - nilerr - - exportloopref - - staticcheck - - typecheck - - bodyclose - - noctx - - prealloc - - gosimple - - ineffassign - - unparam - - unused - presets: - - comment - - error - - format - - metalinter + - asasalint # bugs + - asciicheck # bugs, style + - bidichk # bugs + - bodyclose # bugs, performance + - copyloopvar # style + - dupword # comment + - durationcheck # bugs + - err113 # error + - errcheck # [default] bugs, error + - errchkjson # bugs + - errname # style + - errorlint # error + - wrapcheck # error + - exhaustive # bugs + - forcetypeassert # style + - gci # format, import + - gocheckcompilerdirectives # bugs + # - gochecknoglobals # style + - goconst # style + - gocritic # style, metalinter + - godot # comment + - godox # comment + - gofmt # format + - gofumpt # format + - goimports # format + - goprintffuncname # style + - gosec # bugs + - gosimple # [default] style + - govet # [default] bugs, metalinter + - inamedparam # style + - ineffassign # [default] unused + - intrange # style + - ireturn # style + # - lll # style (report long lines) + - loggercheck # style, bugs + - makezero # style, bugs + - misspell # style, comment + - mnd # style + - musttag # style, bugs + - nakedret # style + - nestif # complexity + - nilerr # bugs + - nilnil # style + - nlreturn # style + - noctx # performance, bugs + - nolintlint # style + - nonamedreturns # style + - perfsprint # performance + - prealloc # performance + - predeclared # style + - protogetter # bugs + - reassign # bugs + - revive # style, metalinter + - sloglint # format + - staticcheck # [default] bugs, metalinter + - unused # [default] unused + - tenv # test + - testableexamples # test + - testifylint # test + - testpackage # test + - unconvert # style + - unparam # unused + - varnamelen # style + - wastedassign # style + - wrapcheck # style, error + # - whitespace # style + - tagalign # format issues: - exclude-use-default: false \ No newline at end of file + exclude-use-default: false diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b8f989e..af19ce7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,3 +1,9 @@ +## Contributor(s) + +* [Uğur Özyılmazel](https://github.com/vigo) - Creator, maintainer + +--- + ## Contribute All PR’s are welcome! diff --git a/Dockerfile b/Dockerfile index 22a14ff..76f7717 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,12 @@ -FROM golang:1.21-alpine AS builder +FROM golang:1.23-alpine AS builder WORKDIR /build COPY . . -RUN GOOS=linux CGO_ENABLED=0 GOARCH=amd64 go build -o server . + +ARG GOOS +ARG GOARCH +ARG BUILD_INFORMATION +RUN CGO_ENABLED=0 GOOS=${GOOS} GOARCH=${GOARCH} go build -ldflags="-X 'github.com/vbyazilim/basichttpdebugger/release.BuildInformation=${BUILD_INFORMATION}'" -o server . FROM alpine:latest AS certs RUN apk add --update --no-cache ca-certificates diff --git a/README.md b/README.md index 2267dd0..828a582 100644 --- a/README.md +++ b/README.md @@ -16,43 +16,205 @@ debug 3^rd pary webhooks etc... ## Usage -You can download via; +You can install directly the latest version if you have go installation; ```bash -$ go install github.com/vbyazilim/basichttpdebugger@latest # install latest binary -$ basichttpdebugger # listens at :9002 -$ basichttpdebugger -listen ":8000" # listens at :8000 +go install github.com/vbyazilim/basichttpdebugger@latest +``` + +Then run: + +```bash +basichttpdebugger -h # help +``` + +Start the server; + +```bash +basichttpdebugger # listens at :9002 +``` + +Listen different port: + +```bash +basichttpdebugger -listen ":8000" # listens at :8000 +``` + +If you want to test HMAC validation; + +```bash +basichttpdebugger -listen ":8000" -hmac-secret "" -hmac-header-name "" +``` + +Instead of standard output, pipe everything to file! + +```bash +basichttpdebugger -listen ":8000" -hmac-secret "" -hmac-header-name "" -output "/tmp/foo" +``` + +Now, tail `/tmp/foo`: + +```bash +tail -f /tmp/foo +``` -# HMAC validation, listens at :8000, check http header name: "X-HEADER-NAME" for HMAC validation. -$ basichttpdebugger -listen ":8000" -hmac-secret "YOURSECRET" -hmac-header-name "X-HEADER-NAME" +Well, add some colors :) + +```bash +basichttpdebugger -listen ":8000" -color true ``` -Clone the repo and run it locally; +If you pipe output to a file, keep colors off. Enabling colors will include +ANSI escape sequences in the file as well. + +You can also clone the source repo and run it locally; ```bash -$ cd /path/to/go/develompent/ -$ git clone github.com/vbyazilim/basichttpdebugger -$ cd basichttpdebugger/ -$ go run . # listens at :9002 -$ go run . -listen ":8000" # listens at :8000 - -# or -$ rake # listens at :9002 -$ HOST=":8000" rake # listens at :8000 - -# HMAC validation, listens at :8000, check http header name: "X-HEADER-NAME" for HMAC validation. -$ HOST=":8000" HMAC_SECRET="YOURSECRET" HMAC_HEADER="X-HEADER-NAME" rake +cd /path/to/go/develompent/ +git clone github.com/vbyazilim/basichttpdebugger +cd basichttpdebugger/ + +go run . -h # help +Usage of basichttpdebugger: + -color + enable color + -hmac-header-name string + name of your signature header (default "X-Hub-Signature-256") + -hmac-secret string + your HMAC secret value + -listen string + listen addr (default ":9002") + -output string + output to (default "stdout") + +go run . # starts server, listens at :9002 + +go run . -listen ":8000" # listens at :8000 + +# or if you have ruby installed, use rake tasks! +rake # listens at :9002 + +HOST=":8000" rake # listens at :8000 +HOST=":8000" HMAC_SECRET="" HMAC_HEADER_NAME="" rake +HOST=":8000" HMAC_SECRET="" HMAC_HEADER_NAME="" OUTPUT="/tmp/foo" rake ``` -Environment variables are only valid for `rake` usage! +--- + +## Flags / Environment Variable Map + +| Flag | Environment Variable | Default Value | +|:-----|:---------------------|---------------| +| `-hmac-header-name` | `HMAC_HEADER_NAME` | `X-Hub-Signature-256` | +| `-hmac-secret` | `HMAC_SECRET` | Not set | +| `-color` | `COLOR` | `false` | +| `-listen` | `HOST` | `:9002` | +| `-output` | `OUTPUT` | `stdout` | + +--- + +## Output + +Here is how it looks, a GitHub webhook (trimmed, masked due to it’s huge data): + + +---------------------------------------------+ + | Basic HTTP Debugger - v0.1.4 - 1f15065600c8 | + +-----------------------+---------------------+ + | HTTP Method | POST | + | Matching Content-Type | text/plain | + +-----------------------+---------------------+ + +-------------------------------------------------------------------------------------------+ + | Request Headers | + +----------------------------------------+--------------------------------------------------+ + | Accept | */* | + | Accept-Encoding | gzip | + | Content-Length | 9853 | + | Content-Type | application/json | + | User-Agent | GitHub-Hookshot/68d5600 | + | X-Forwarded-For | ***.**.***.** | + | X-Forwarded-Host | ******-******-******.ngrok-free.app | + | X-Forwarded-Proto | https | + | X-Github-Delivery | 6b2db120-bfe4-11ef-91e7-6e465723772e | + | X-Github-Event | issues | + | X-Github-Hook-Id | 519902493 | + | X-Github-Hook-Installation-Target-Id | 906427850 | + | X-Github-Hook-Installation-Target-Type | repository | + | X-Hub-Signature | sha1=aea0d3b6577832e464********************** | + | X-Hub-Signature-256 | sha256=4b24fa2a16d12887665********************** | + | | ********************002 | + +----------------------------------------+--------------------------------------------------+ + +----------------------------------------------------------------------------------------------+ + | HMAC Validation | + +--------------------+-------------------------------------------------------------------------+ + | HMAC Secret Value | ********** | + | HMAC Header Name | X-Hub-Signature-256 | + | Incoming Signature | sha256=4b24fa2a16d128************************************************** | + | Expected Signature | sha256=4b24fa2a16d128************************************************** | + | Is Valid? | true | + +--------------------+-------------------------------------------------------------------------+ + { + "action": "closed", + "issue": { + "active_lock_reason": null, + "assignee": null, + "assignees": [], + : + : + "reactions": { + "+1": 0, + "-1": 0, + : + : + }, + "repository_url": "https://api.github.com/repos//", + "state": "closed", + "state_reason": "not_planned", + "timeline_url": "https://api.github.com/repos///issues/6/timeline", + : + "user": { + "avatar_url": "https://avatars.githubusercontent.com/u/82952?v=4", + : + : + } + }, + "organization": { + "avatar_url": "https://avatars.githubusercontent.com/u/159630054?v=4", + : + : + }, + "repository": { + "allow_forking": false, + : + : + "open_issues": 3, + "open_issues_count": 3, + "owner": { + "avatar_url": "https://avatars.githubusercontent.com/u/159630054?v=4", + : + : + }, + : + : + }, + "sender": { + "avatar_url": "https://avatars.githubusercontent.com/u/82952?v=4", + : + : + } + } + +--- + +## Docker For local docker usage, default expose port is: `9002`. ```bash docker build -t . + docker run -p 9002:9002 # run from default port docker run -p 8400:8400 -listen ":8400" # run from 8400 -docker run -p 8400:8400 -listen ":8400" -hmac-secret "YOURSECRET" -hmac-header-name "X-HEADER-NAME" +docker run -p 8400:8400 -listen ":8400" -hmac-secret "" -hmac-header-name "" ``` You can download/use from docker hub or ghcr: @@ -62,11 +224,12 @@ You can download/use from docker hub or ghcr: ```bash docker run vigo/basichttpdebugger + docker run -p 9002:9002 vigo/basichttpdebugger # run from default port docker run -p 8400:8400 vigo/basichttpdebugger -listen ":8400" # run from 8400 # run from docker hub on port 9100 with hmac support -docker run -p 9100:9100 vigo/basichttpdebugger -listen ":9100" -hmac-secret "YOURSECRET" -hmac-header-name "X-HEADER-NAME" +docker run -p 9100:9100 vigo/basichttpdebugger -listen ":9100" -hmac-secret "" -hmac-header-name "" # run from ghcr on default port docker run -p 9002:9002 ghcr.io/vbyazilim/basichttpdebugger/basichttpdebugger:latest @@ -75,13 +238,18 @@ docker run -p 9002:9002 ghcr.io/vbyazilim/basichttpdebugger/basichttpdebugger:la docker run -p 9100:9100 ghcr.io/vbyazilim/basichttpdebugger/basichttpdebugger:latest -listen ":9100" # run from ghcr on 9100 with hmac support -docker run -p 9100:9100 ghcr.io/vbyazilim/basichttpdebugger/basichttpdebugger:latest -listen ":9100" -hmac-secret "YOURSECRET" -hmac-header-name "X-HEADER-NAME" +docker run -p 9100:9100 ghcr.io/vbyazilim/basichttpdebugger/basichttpdebugger:latest -listen ":9100" -hmac-secret "" -hmac-header-name "" ``` --- ## Change Log +**2024-12-23** + +- many improvements, pretty output with colors! +- now you can pipe to file too! + **2024-09-17** - change default host port to `9002` @@ -117,24 +285,6 @@ rake run # run server (default port 9002) --- -## Contributor(s) - -* [Uğur Özyılmazel](https://github.com/vigo) - Creator, maintainer - ---- - -## Contribute - -All PR’s are welcome! - -1. `fork` (https://github.com/vbyazilim/basichttpdebugger/fork) -1. Create your `branch` (`git checkout -b my-feature`) -1. `commit` yours (`git commit -am 'add some functionality'`) -1. `push` your `branch` (`git push origin my-feature`) -1. Than create a new **Pull Request**! - ---- - ## License This project is licensed under MIT diff --git a/Rakefile b/Rakefile index b73f146..9694560 100644 --- a/Rakefile +++ b/Rakefile @@ -2,15 +2,12 @@ task :default => [:run] desc "run server (default port 9002)" task :run do - host = ENV['HOST'] || ":9002" - secret = ENV['HMAC_SECRET'] - header = ENV['HMAC_HEADER'] - - cmd_args = ["-listen", host] - cmd_args << "-hmac-secret" << secret if secret - cmd_args << "-hmac-header-name" << header if header - - system %{ go run . #{cmd_args.join(" ")} } + system %{ go run . } + status = $?&.exitstatus || 1 +rescue Interrupt + status = 0 +ensure + exit status end @@ -20,19 +17,19 @@ end task :is_repo_clean do abort 'please commit your changes first!' unless `git status -s | wc -l`.strip.to_i.zero? end -task :has_bumpversion do - Rake::Task['command_exists'].invoke('bumpversion') +task :has_bump_my_version do + Rake::Task['command_exists'].invoke('bump-my-version') end AVAILABLE_REVISIONS = %w[major minor patch].freeze -task :bump, [:revision] => [:has_bumpversion] do |_, args| +task :bump, [:revision] => [:has_bump_my_version] do |_, args| args.with_defaults(revision: 'patch') unless AVAILABLE_REVISIONS.include?(args.revision) abort "Please provide valid revision: #{AVAILABLE_REVISIONS.join(',')}" end - system "bumpversion #{args.revision}" + system %{ bump-my-version bump #{args.revision} } exit $?.exitstatus end @@ -48,8 +45,17 @@ namespace :docker do desc "build docker image locally" task :build do system %{ - docker build -t #{DOCKER_IMAGE_NAME} . + BUILD_INFORMATION=" - $(git rev-parse --short HEAD)" + GOOS="linux" + GOARCH=$(go env GOARCH) + + docker build \ + --build-arg="BUILD_INFORMATION=${BUILD_INFORMATION}" \ + --build-arg="GOOS=${GOOS}" \ + --build-arg="GOARCH=${GOARCH}" \ + -t #{DOCKER_IMAGE_NAME} . } + exit $?.exitstatus end desc "run docker image locally" @@ -57,5 +63,10 @@ namespace :docker do system %{ docker run -p "9002:9002" #{DOCKER_IMAGE_NAME} } + status = $?&.exitstatus || 1 + rescue Interrupt + status = 0 + ensure + exit status end -end \ No newline at end of file +end diff --git a/go.mod b/go.mod index ad75d9f..76ad1d7 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,16 @@ module github.com/vbyazilim/basichttpdebugger -go 1.21.5 +go 1.23.4 + +require ( + github.com/jedib0t/go-pretty/v6 v6.6.5 + github.com/vigo/accept v0.1.0 + golang.org/x/term v0.27.0 +) + +require ( + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/text v0.21.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d328e7b --- /dev/null +++ b/go.sum @@ -0,0 +1,23 @@ +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/jedib0t/go-pretty/v6 v6.6.5 h1:9PgMJOVBedpgYLI56jQRJYqngxYAAzfEUua+3NgSqAo= +github.com/jedib0t/go-pretty/v6 v6.6.5/go.mod h1:Uq/HrbhuFty5WSVNfjpQQe47x16RwVGXIveNGEyGtHs= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +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/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/vigo/accept v0.1.0 h1:z5kRrLWQiV5BS0+qUF404jNy/lJuElU2JBBwi1yALHc= +github.com/vigo/accept v0.1.0/go.mod h1:K1HtWwIW4FTTr8bZGmlrnSX4sAZSG9YBPjwnL+5PHi0= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index a569b1a..1ea433d 100644 --- a/main.go +++ b/main.go @@ -4,95 +4,232 @@ import ( "crypto/hmac" "crypto/sha256" "encoding/hex" + "encoding/json" + "errors" "flag" "fmt" "io" "log" "net/http" + "os" + "sort" "strings" "time" + + "github.com/jedib0t/go-pretty/v6/table" + "github.com/jedib0t/go-pretty/v6/text" + "github.com/vbyazilim/basichttpdebugger/release" + "github.com/vigo/accept" + "golang.org/x/term" +) + +const ( + defReadTimeout = 5 * time.Second + defReadHeaderTimeout = 5 * time.Second + defWriteTimeout = 10 * time.Second + defIdleTimeout = 15 * time.Second + defTerminalWidth = 120 ) -func printHeaders(h http.Header) { - maxLen := 0 - for k := range h { - if len(k) > maxLen { - maxLen = len(k) +var ( + defHMACSecret string + defHMACHeaderName = "X-Hub-Signature-256" + defListenAddr = ":9002" + defOutput = "stdout" + defColor bool +) + +func main() { + if val := os.Getenv("HMAC_SECRET"); val != "" { + defHMACSecret = val + } + if val := os.Getenv("HMAC_HEADER_NAME"); val != "" { + defHMACHeaderName = val + } + if val := os.Getenv("HOST"); val != "" { + defListenAddr = val + } + if val := os.Getenv("OUTPUT"); val != "" { + defOutput = val + } + if val := os.Getenv("COLOR"); val != "" { + defColor = true + } + + hmacSecretValue := flag.String("hmac-secret", defHMACSecret, "your HMAC secret value") + hmacHeaderName := flag.String("hmac-header-name", defHMACHeaderName, "name of your signature header") + listenAddr := flag.String("listen", defListenAddr, "listen addr") + color := flag.Bool("color", defColor, "enable color") + output := flag.String("output", defOutput, "output to") + flag.Parse() + + if *color { + text.EnableColors() + } else { + text.DisableColors() + } + + var outputWriter *os.File + + if *output == "stdout" { + outputWriter = os.Stdout + } else { + fileWriter, err := os.Create(*output) + if err != nil { + log.Fatal("can not create file", err) } + + outputWriter = fileWriter + + defer func() { _ = outputWriter.Close() }() + } + + cn := accept.New( + accept.WithSupportedMediaTypes("text/plain"), + accept.WithDefaultMediaType("text/plain"), + ) + + mux := http.NewServeMux() + mux.HandleFunc("/", debugHandlerFunc(cn, outputWriter, *hmacSecretValue, *hmacHeaderName)) + + server := &http.Server{ + Addr: *listenAddr, + Handler: mux, + ReadTimeout: defReadTimeout, + ReadHeaderTimeout: defReadHeaderTimeout, + WriteTimeout: defWriteTimeout, + IdleTimeout: defIdleTimeout, + } + + log.Printf("server listening at %s\n", *listenAddr) + if errsrv := server.ListenAndServe(); errsrv != nil && !errors.Is(errsrv, http.ErrServerClosed) { + log.Printf("server error: %v\n", errsrv) } +} - fmt.Println("http request headers ...................") - for k, v := range h { - dots := strings.Repeat(".", maxLen-len(k)+1) - fmt.Printf("%s %s %v\n", k, dots, v) +func getTerminalWidth() int { + if width, _, err := term.GetSize(int(os.Stdout.Fd())); err == nil { + return width } - fmt.Println(strings.Repeat(".", 40)) - fmt.Println() + + return defTerminalWidth } -func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) { - fmt.Println(strings.Repeat("-", 80)) - fmt.Println("method ...... ", r.Method) - fmt.Println() +func errorAsTable(fwriter *os.File, title string, err error) { + t := table.NewWriter() + t.SetOutputMirror(fwriter) + t.SetTitle(text.Colors{text.Bold, text.FgRed}.Sprint(title)) + t.AppendRow(table.Row{err.Error()}) + t.Render() +} - printHeaders(r.Header) +func debugHandlerFunc(cn *accept.ContentNegotiation, fwriter *os.File, hmsv string, hmhn string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + terminalWidth := getTerminalWidth() - body, err := io.ReadAll(r.Body) + acceptHeader := r.Header.Get("Accept") + requestContentType := r.Header.Get("Content-Type") + contentType := cn.Negotiate(acceptHeader) - fmt.Println("error ..................................") - fmt.Println(err) - fmt.Println(strings.Repeat(".", 40)) - fmt.Println() + w.Header().Set("Content-Type", contentType) + w.WriteHeader(http.StatusOK) + fmt.Fprintln(w, "OK") - if err == nil { - bodyStr := string(body) - if len(bodyStr) > 0 { - fmt.Println("body ...................................") - fmt.Println(bodyStr) - fmt.Println(strings.Repeat(".", 40)) + mainTitle := "Basic HTTP Debugger - v" + release.Version + release.BuildInformation + + fmt.Fprintln(fwriter, strings.Repeat("-", terminalWidth)) + t := table.NewWriter() + t.SetOutputMirror(fwriter) + t.SetTitle(text.Colors{text.Bold, text.FgWhite}.Sprint(mainTitle)) + t.SetColumnConfigs([]table.ColumnConfig{ + {Number: 1, Colors: text.Colors{text.FgYellow}}, + }) + + infoRows := []table.Row{ + {"HTTP Method", r.Method}, + {"Matching Content-Type", contentType}, } - if *optHMACSecret != "" && *optHMACHeader != "" { - fmt.Println() - fmt.Println("hmac validation ........................") - - signature := r.Header.Get(*optHMACHeader) - h := hmac.New(sha256.New, []byte(*optHMACSecret)) - h.Write(body) - - expectedSignature := hex.EncodeToString(h.Sum(nil)) - fmt.Println("expected signature...", expectedSignature) - fmt.Println("incoming signature...", signature) - fmt.Println("is valid?............", hmac.Equal([]byte(expectedSignature), []byte(signature))) - fmt.Println(strings.Repeat(".", 40)) + t.AppendRows(infoRows) + t.Render() + + t = table.NewWriter() + t.SetOutputMirror(fwriter) + t.SetTitle(text.Colors{text.Bold, text.FgWhite}.Sprint("Request Headers")) + + t.SetColumnConfigs([]table.ColumnConfig{ + {Number: 1, Colors: text.Colors{text.FgYellow}}, + {Number: 2, WidthMax: (terminalWidth / 2) - 2}, + }) + + headerKeys := make([]string, 0, len(r.Header)) + for key := range r.Header { + headerKeys = append(headerKeys, key) } - } - fmt.Println(strings.Repeat("-", 80)) - fmt.Println() - _, _ = fmt.Fprintf(w, "OK") -} + sort.Strings(headerKeys) -var ( - optHMACSecret *string - optHMACHeader *string - optListenADDR *string -) + for _, key := range headerKeys { + t.AppendRow(table.Row{key, strings.Join(r.Header[key], ",")}) + } + t.Render() -type server struct{} + switch r.Method { + case http.MethodPost, http.MethodPut, http.MethodPatch: + body, err := io.ReadAll(r.Body) + if err != nil { + errorAsTable(fwriter, "Body READ error", err) -func main() { - optHMACSecret = flag.String("hmac-secret", "", "HMAC secret") - optHMACHeader = flag.String("hmac-header-name", "", "Signature response header name") - optListenADDR = flag.String("listen", ":9002", "Listen address, default: ':9002'") - flag.Parse() + return + } + defer func() { _ = r.Body.Close() }() - srv := &http.Server{ - Addr: *optListenADDR, - Handler: new(server), - ReadTimeout: 5 * time.Second, - WriteTimeout: 10 * time.Second, - IdleTimeout: 15 * time.Second, - } + t = table.NewWriter() + t.SetOutputMirror(fwriter) + t.SetTitle(text.Colors{text.Bold, text.FgWhite}.Sprint("HMAC Validation")) + t.SetColumnConfigs([]table.ColumnConfig{ + {Number: 1, Colors: text.Colors{text.FgYellow}}, + }) + if hmsv != "" { + t.AppendRow(table.Row{"HMAC Secret Value", hmsv}) + } + if hmhn != "" { + t.AppendRow(table.Row{"HMAC Header Name", hmhn}) + } + + if hmsv != "" && hmhn != "" { + signature := r.Header.Get(hmhn) + h := hmac.New(sha256.New, []byte(hmsv)) + _, _ = h.Write(body) + expectedSignature := "sha256=" + hex.EncodeToString(h.Sum(nil)) + + t.AppendRows([]table.Row{ + {"Incoming Signature", signature}, + {"Expected Signature", expectedSignature}, + {"Is Valid?", hmac.Equal([]byte(expectedSignature), []byte(signature))}, + }) + } + t.Render() - fmt.Println("running server at", *optListenADDR) - log.Fatal(srv.ListenAndServe()) + switch requestContentType { + case "application/json": + var jsonBody map[string]any + if err = json.Unmarshal(body, &jsonBody); err != nil { + errorAsTable(fwriter, "json.Unmarshal error", err) + + return + } + + prettyJSON, errpj := json.MarshalIndent(jsonBody, "", " ") + if errpj != nil { + errorAsTable(fwriter, "json.MarshalIndent error", errpj) + + return + } + fmt.Fprintln(fwriter, string(prettyJSON)) + default: + fmt.Fprintln(fwriter, string(body)) + } + + fmt.Fprintln(fwriter, strings.Repeat("-", terminalWidth)) + } + } } diff --git a/release/release.go b/release/release.go new file mode 100644 index 0000000..13c169a --- /dev/null +++ b/release/release.go @@ -0,0 +1,7 @@ +package release + +// Version is the current version of service. +const Version string = "0.1.4" + +// BuildInformation holds current build information. +var BuildInformation string