Full Code of OJ/gobuster for AI

master c77583fbb882 cached
79 files
634.1 KB
265.4k tokens
260 symbols
1 requests
Download .txt
Showing preview only (662K chars total). Download the full file or copy to clipboard to get everything.
Repository: OJ/gobuster
Branch: master
Commit: c77583fbb882
Files: 79
Total size: 634.1 KB

Directory structure:
gitextract_sdpr052u/

├── .devcontainer/
│   └── devcontainer.json
├── .dockerignore
├── .gitattributes
├── .github/
│   ├── FUNDING.yml
│   ├── dependabot.yml
│   └── workflows/
│       ├── auto-merge-dependabot.yml
│       ├── docker.yml
│       ├── go.yml
│       ├── golangci-lint.yml
│       ├── hadolint.yml
│       ├── release.yml
│       ├── update.yml
│       ├── vhs.yml
│       └── yamllint.yml
├── .gitignore
├── .golangci.yml
├── .goreleaser.yaml
├── .yamllint.yml
├── Dockerfile
├── LICENSE
├── README.md
├── Taskfile.yml
├── cli/
│   ├── const.go
│   ├── const_windows.go
│   ├── dir/
│   │   ├── dir.go
│   │   └── dir_test.go
│   ├── dns/
│   │   └── dns.go
│   ├── fuzz/
│   │   └── fuzz.go
│   ├── gcs/
│   │   └── gcs.go
│   ├── gobuster.go
│   ├── options.go
│   ├── s3/
│   │   └── s3.go
│   ├── tftp/
│   │   └── tftp.go
│   └── vhost/
│       ├── vhost.go
│       └── vhost_test.go
├── go.mod
├── go.sum
├── gobusterdir/
│   ├── gobusterdir.go
│   ├── gobusterdir_test.go
│   ├── options.go
│   ├── options_test.go
│   └── result.go
├── gobusterdns/
│   ├── gobusterdns.go
│   ├── options.go
│   └── result.go
├── gobusterfuzz/
│   ├── gobusterfuzz.go
│   ├── options.go
│   ├── options_test.go
│   └── result.go
├── gobustergcs/
│   ├── gobustersgcs.go
│   ├── options.go
│   ├── result.go
│   └── types.go
├── gobusters3/
│   ├── gobusters3.go
│   ├── options.go
│   ├── result.go
│   └── types.go
├── gobustertftp/
│   ├── gobustertftp.go
│   ├── options.go
│   └── result.go
├── gobustervhost/
│   ├── gobustervhost.go
│   ├── options.go
│   └── result.go
├── libgobuster/
│   ├── errors.go
│   ├── helpers.go
│   ├── helpers_test.go
│   ├── http.go
│   ├── http_test.go
│   ├── interfaces.go
│   ├── libgobuster.go
│   ├── logger.go
│   ├── options.go
│   ├── options_http.go
│   ├── progress.go
│   ├── useragents.go
│   └── version.go
├── main.go
└── vhs/
    ├── gobuster_dir.tape
    └── server.go

================================================
FILE CONTENTS
================================================

================================================
FILE: .devcontainer/devcontainer.json
================================================
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/go
{
  "name": "Go",
  "image": "mcr.microsoft.com/devcontainers/go",
  // Features to add to the dev container. More info: https://containers.dev/features.
  "features": {
    "ghcr.io/guiyomh/features/golangci-lint:0": {},
    "ghcr.io/devcontainers-extra/features/go-task:1": {}
  },
  "postCreateCommand": {
    "install dependencies": "task deps"
  },
  "customizations": {
    "vscode": {
      "extensions": [
        "golang.go",
        "shardulm94.trailing-spaces",
        "IBM.output-colorizer",
        "github.vscode-github-actions",
        "ms-azuretools.vscode-docker",
        "task.vscode-task",
        "redhat.vscode-yaml",
        "usernamehw.errorlens",
        "Gruntfuggly.todo-tree"
      ]
    }
  }
}

================================================
FILE: .dockerignore
================================================
*.exe
*.out
*.prof
*.txt
*.swp
.vscode/
gobuster
.git/
mitm*
.idea/


================================================
FILE: .gitattributes
================================================
# https://github.com/gitattributes/gitattributes/blob/master/Common.gitattributes

# Common settings that generally should always be used with your language specific settings

# Auto detect text files and perform LF normalization
*          text=auto

#
# The above will handle all files NOT found below
#

# Documents
*.bibtex   text diff=bibtex
*.doc      diff=astextplain
*.DOC      diff=astextplain
*.docx     diff=astextplain
*.DOCX     diff=astextplain
*.dot      diff=astextplain
*.DOT      diff=astextplain
*.pdf      diff=astextplain
*.PDF      diff=astextplain
*.rtf      diff=astextplain
*.RTF      diff=astextplain
*.md       text diff=markdown
*.mdx      text diff=markdown
*.tex      text diff=tex
*.adoc     text
*.textile  text
*.mustache text
*.csv      text eol=crlf
*.tab      text
*.tsv      text
*.txt      text
*.sql      text
*.epub     diff=astextplain

# Graphics
*.png      binary
*.jpg      binary
*.jpeg     binary
*.gif      binary
*.tif      binary
*.tiff     binary
*.ico      binary
# SVG treated as text by default.
*.svg      text
# If you want to treat it as binary,
# use the following line instead.
# *.svg    binary
*.eps      binary

# Scripts
*.bash     text eol=lf
*.fish     text eol=lf
*.sh       text eol=lf
*.zsh      text eol=lf
# These are explicitly windows files and should use crlf
*.bat      text eol=crlf
*.cmd      text eol=crlf
*.ps1      text eol=crlf

# Serialisation
*.json     text
*.toml     text
*.xml      text
*.yaml     text
*.yml      text

# Archives
*.7z       binary
*.gz       binary
*.tar      binary
*.tgz      binary
*.zip      binary

# Text files where line endings should be preserved
*.patch    -text

#
# Exclude files from exporting
#

.gitattributes export-ignore
.gitignore     export-ignore
.gitkeep       export-ignore

# https://github.com/gitattributes/gitattributes/blob/master/Go.gitattributes

# Treat all Go files in this repo as binary, with no git magic updating
# line endings. Windows users contributing to Go will need to use a
# modern version of git and editors capable of LF line endings.

*.go -text diff=golang

================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms

github: [OJ, firefart]
patreon: OJReeves
open_collective: gobuster
ko_fi: OJReeves


================================================
FILE: .github/dependabot.yml
================================================
# 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:
  - package-ecosystem: "gomod"
    directory: "/"
    target-branch: "dev"
    schedule:
      interval: "weekly"

  - package-ecosystem: "github-actions"
    directory: "/"
    target-branch: "dev"
    schedule:
      interval: "daily"

  - package-ecosystem: docker
    directory: "/"
    target-branch: "dev"
    schedule:
      interval: "daily"

  - package-ecosystem: "devcontainers"
    directory: "/"
    target-branch: "dev"
    schedule:
      interval: "daily"


================================================
FILE: .github/workflows/auto-merge-dependabot.yml
================================================
name: Auto-merge dependabot updates

on:
  pull_request:
    branches: [main, dev]

permissions:
  pull-requests: write
  contents: write

jobs:

  dependabot-merge:

    runs-on: ubuntu-latest

    if: github.event.pull_request.user.login == 'dependabot[bot]' && startsWith(github.repository, 'firefart/')

    steps:
      - name: Dependabot metadata
        id: metadata
        uses: dependabot/fetch-metadata@v2.4.0
        with:
          github-token: "${{ secrets.GITHUB_TOKEN }}"

      - name: Enable auto-merge for Dependabot PRs
        # Only if version bump is not a major version change
        if: ${{steps.metadata.outputs.update-type != 'version-update:semver-major'}}
        run: gh pr merge --auto --merge "$PR_URL"
        env:
          PR_URL: ${{github.event.pull_request.html_url}}
          GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}


================================================
FILE: .github/workflows/docker.yml
================================================
name: Build Docker Images

on:
  push:
    branches:
      - main
  workflow_dispatch:
  schedule:
    - cron: "0 0 * * *"

jobs:
  Dockerhub:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
      - name: checkout sources
        uses: actions/checkout@v5

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.repository_owner }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push
        uses: docker/build-push-action@v6
        with:
          push: true
          platforms: linux/amd64,linux/arm/v7,linux/arm64/v8,linux/386,linux/ppc64le
          tags: |
            ghcr.io/oj/gobuster:latest


================================================
FILE: .github/workflows/go.yml
================================================
name: Go
on: [push, pull_request]
permissions:
  contents: read
jobs:
  build:
    name: Build
    runs-on: ubuntu-latest
    steps:
      - name: Check out code
        uses: actions/checkout@v5

      - name: Set up Go
        uses: actions/setup-go@v6
        with:
          go-version: "stable"

      - name: Install Task
        uses: arduino/setup-task@v2
        with:
          repo-token: ${{ secrets.GITHUB_TOKEN }}

      - name: Get dependencies
        run: task deps

      - name: Build linux
        run: task linux

      - name: Build windows
        run: task windows

      - name: Test
        run: task test


================================================
FILE: .github/workflows/golangci-lint.yml
================================================
name: golangci-lint
on: [push, pull_request, workflow_dispatch]
permissions:
  contents: read
jobs:
  golangci:
    name: lint
    timeout-minutes: 30
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5

      - uses: actions/setup-go@v6
        with:
          go-version: "stable"

      - name: golangci-lint
        uses: golangci/golangci-lint-action@v8
        with:
          version: latest
          args: --timeout=5m


================================================
FILE: .github/workflows/hadolint.yml
================================================
name: Hadolint
on:
  push:
    paths:
      - "**/Dockerfile"
  pull_request:
  workflow_dispatch:
permissions:
  contents: read
jobs:
  hadolint:
    name: hadolint
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: hadolint/hadolint-action@v3.2.0
        with:
          dockerfile: Dockerfile
          # DL3007: Using latest is prone to errors if the image will ever update. Pin the version explicitly to a release tag
          # DL3018  Pin versions in apk add. Instead of `apk add <package>` use `apk add <package>=<version>`
          ignore: DL3007,DL3018


================================================
FILE: .github/workflows/release.yml
================================================
name: goreleaser

on:
  push:
    tags:
      - "*"

permissions:
  contents: write

jobs:
  goreleaser:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v5
        with:
          fetch-depth: 0

      - name: Fetch all tags
        run: git fetch --force --tags

      - name: Set up Go
        uses: actions/setup-go@v6
        with:
          go-version: "stable"

      - name: Run GoReleaser
        uses: goreleaser/goreleaser-action@v6.4.0
        with:
          distribution: goreleaser
          version: latest
          args: release --clean
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}


================================================
FILE: .github/workflows/update.yml
================================================
name: Update Dependencies

on:
  schedule:
    - cron: "0 4 * * *"
  workflow_dispatch:

permissions:
  contents: write

jobs:
  update-dependencies:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
        with:
          ref: dev

      - name: Set up Go
        uses: actions/setup-go@v6
        with:
          go-version: "stable"

      - name: Update all dependencies
        run: |
          go get -u ./...
          go mod tidy

      - name: Commit and push changes
        uses: stefanzweifel/git-auto-commit-action@v5
        with:
          commit_message: "chore: update dependencies [automated]"
          branch: dev


================================================
FILE: .github/workflows/vhs.yml
================================================
name: vhs
on:
  push:
    paths:
      - vhs/**.tape

permissions:
  contents: write

jobs:
  vhs:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5

      - name: Set up Go
        uses: actions/setup-go@v6
        with:
          go-version: "stable"

      - name: Install Task
        uses: arduino/setup-task@v2
        with:
          repo-token: ${{ secrets.GITHUB_TOKEN }}

      - name: Get dependencies
        run: task deps

      - name: Build linux
        run: task linux

      - name: Install deps
        run: |
          sudo apt update
          sudo apt install -y ffmpeg ttyd

      - uses: charmbracelet/vhs-action@v2
        with:
          path: "vhs/gobuster_dir.tape"

      - name: commit and push changes
        run: |
          git config user.name "Github"
          git config user.email "<>"
          git add vhs/*.gif
          git commit -m "update vhs gifs" || echo "no changes to commit"
          git push origin master


================================================
FILE: .github/workflows/yamllint.yml
================================================
name: yamllint
on:
  push:
    paths:
      - "**.yml"
      - "**.yaml"
  pull_request:
  workflow_dispatch:
permissions:
  contents: read
jobs:
  yamllint:
    name: yamllint
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: karancode/yamllint-github-action@master
        with:
          # fail on warnings and errors
          yamllint_strict: true
          yamllint_config_filepath: ".yamllint.yml"


================================================
FILE: .gitignore
================================================
# https://github.com/github/gitignore/blob/main/Go.gitignore

# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib

# Test binary, built with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Dependency directories (remove the comment below to include it)
# vendor/

# Go workspace file
go.work

config.json
!config_sample.json
*.secret
*.env
.vscode/
gobuster
*.txt
dist/


================================================
FILE: .golangci.yml
================================================
# based on https://gist.github.com/maratori/47a4d00457a92aa426dbd48a18776322
version: "2"
run:
  relative-path-mode: gomod
linters:
  default: none
  enable:
    - asasalint
    - asciicheck
    - bidichk
    - bodyclose
    - canonicalheader
    - copyloopvar
    - depguard
    - durationcheck
    - errcheck
    - errname
    - errorlint
    - exhaustive
    - exptostd
    - fatcontext
    - forbidigo
    - gocheckcompilerdirectives
    - gochecknoinits
    - gochecksumtype
    - goconst
    - gocritic
    - gomoddirectives
    - goprintffuncname
    - gosec
    - govet
    - iface
    - ineffassign
    - intrange
    - loggercheck
    - makezero
    - mirror
    - musttag
    - nakedret
    - nilerr
    - nilnesserr
    - nilnil
    - noctx
    - nonamedreturns
    - nosprintfhostport
    - perfsprint
    - predeclared
    - promlinter
    - protogetter
    - recvcheck
    - revive
    - rowserrcheck
    - sloglint
    - spancheck
    - sqlclosecheck
    - staticcheck
    - testableexamples
    - testifylint
    - tparallel
    - unconvert
    - unparam
    - unused
    - usestdlibvars
    - usetesting
    - wastedassign
    - whitespace
  settings:
    depguard:
      rules:
        deprecated:
          files:
            - $all
          deny:
            - pkg: github.com/golang/protobuf
              desc: Use google.golang.org/protobuf instead, see https://developers.google.com/protocol-buffers/docs/reference/go/faq#modules
            - pkg: github.com/satori/go.uuid
              desc: Use github.com/google/uuid instead, satori's package is not maintained
            - pkg: github.com/gofrs/uuid$
              desc: Use github.com/gofrs/uuid/v5 or later, it was not a go module before v5
        non-main files:
          files:
            - "!**/main.go"
          deny:
            - pkg: log$
              desc: Use log/slog instead, see https://go.dev/blog/slog
        non-test files:
          files:
            - "!$test"
          deny:
            - pkg: math/rand$
              desc: Use math/rand/v2 instead, see https://go.dev/blog/randv2
    errcheck:
      check-type-assertions: true
    exhaustive:
      check:
        - switch
        - map
    exhaustruct:
      exclude:
        - ^net/http.Client$
        - ^net/http.Cookie$
        - ^net/http.Request$
        - ^net/http.Response$
        - ^net/http.Server$
        - ^net/http.Transport$
        - ^net/url.URL$
        - ^os/exec.Cmd$
        - ^reflect.StructField$
        - ^github.com/Shopify/sarama.Config$
        - ^github.com/Shopify/sarama.ProducerMessage$
        - ^github.com/mitchellh/mapstructure.DecoderConfig$
        - ^github.com/prometheus/client_golang/.+Opts$
        - ^github.com/spf13/cobra.Command$
        - ^github.com/spf13/cobra.CompletionOptions$
        - ^github.com/stretchr/testify/mock.Mock$
        - ^github.com/testcontainers/testcontainers-go.+Request$
        - ^github.com/testcontainers/testcontainers-go.FromDockerfile$
        - ^golang.org/x/tools/go/analysis.Analyzer$
        - ^google.golang.org/protobuf/.+Options$
        - ^gopkg.in/yaml.v3.Node$
    gocognit:
      min-complexity: 20
    gochecksumtype:
      default-signifies-exhaustive: false
    gocritic:
      settings:
        captLocal:
          paramsOnly: false
        underef:
          skipRecvDeref: false
    govet:
      disable:
        - fieldalignment
        - shadow
      enable-all: true
    inamedparam:
      skip-single-param: true
    perfsprint:
      strconcat: false
    rowserrcheck:
      packages:
        - github.com/jmoiron/sqlx
    sloglint:
      no-global: all
      context: ""
    usetesting:
      os-temp-dir: true
  exclusions:
    generated: lax
    presets:
      - comments
      - common-false-positives
      - legacy
      - std-error-handling
    rules:
      - linters:
          - gocritic
        source: //noinspection
      - linters:
          - bodyclose
          - errcheck
          - goconst
          - gosec
          - noctx
          - wrapcheck
        path: _test\.go
    paths:
      - third_party$
      - builtin$
      - examples$
issues:
  max-same-issues: 50
formatters:
  enable:
    - goimports
  exclusions:
    generated: lax
    paths:
      - third_party$
      - builtin$
      - examples$


================================================
FILE: .goreleaser.yaml
================================================
# This is an example .goreleaser.yml file with some sensible defaults.
# Make sure to check the documentation at https://goreleaser.com

# The lines below are called `modelines`. See `:help modeline`
# Feel free to remove those if you don't want/need to use them.
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
# vim: set ts=2 sw=2 tw=0 fo=cnqoj

version: 2

before:
  hooks:
    # You may remove this if you don't use go modules.
    - go mod tidy
    # you may remove this if you don't need go generate
    - go generate ./...

builds:
  - env:
      - CGO_ENABLED=0
    goos:
      - linux
      - windows
      - darwin

archives:
  - formats: [tar.gz]
    # this name template makes the OS and Arch compatible with the results of `uname`.
    name_template: >-
      {{ .ProjectName }}_
      {{- title .Os }}_
      {{- if eq .Arch "amd64" }}x86_64
      {{- else if eq .Arch "386" }}i386
      {{- else }}{{ .Arch }}{{ end }}
      {{- if .Arm }}v{{ .Arm }}{{ end }}
    # use zip for windows archives
    format_overrides:
      - goos: windows
        formats: [zip]

changelog:
  sort: asc
  filters:
    exclude:
      - "^docs:"
      - "^test:"


================================================
FILE: .yamllint.yml
================================================
---
extends: default

rules:
  truthy: disable
  line-length: disable
  document-start: disable


================================================
FILE: Dockerfile
================================================
# syntax=docker/dockerfile:1
FROM golang:latest AS build-env
WORKDIR /src
ENV CGO_ENABLED=0
COPY go.mod /src/
RUN go mod download
COPY . .
RUN go build -a -o gobuster -trimpath

FROM alpine:latest

RUN apk add --no-cache ca-certificates \
    && rm -rf /var/cache/*

RUN mkdir -p /app \
    && adduser -D gobuster \
    && chown -R gobuster:gobuster /app

USER gobuster
WORKDIR /app

COPY --from=build-env /src/gobuster .

ENTRYPOINT [ "./gobuster" ]


================================================
FILE: LICENSE
================================================

                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS

   APPENDIX: How to apply the Apache License to your work.

      To apply the Apache License to your work, attach the following
      boilerplate notice, with the fields enclosed by brackets "[]"
      replaced with your own identifying information. (Don't include
      the brackets!)  The text should be enclosed in the appropriate
      comment syntax for the file format. We also recommend that a
      file or class name and description of purpose be included on the
      same "printed page" as the copyright notice for easier
      identification within third-party archives.

   Copyright [yyyy] [name of copyright owner]

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.

================================================
FILE: README.md
================================================
# Gobuster

[![Go Report Card](https://goreportcard.com/badge/github.com/OJ/gobuster/v3)](https://goreportcard.com/report/github.com/OJ/gobuster/v3) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/OJ/gobuster/blob/master/LICENSE) [![Backers on Open Collective](https://opencollective.com/gobuster/backers/badge.svg)](https://opencollective.com/gobuster) [![Sponsors on Open Collective](https://opencollective.com/gobuster/sponsors/badge.svg)](https://opencollective.com/gobuster)

## 💻 Introduction

> A fast and flexible brute-forcing tool written in Go

**Gobuster** is a high-performance directory/file, DNS and virtual host brute-forcing tool written in Go. It's designed to be fast, reliable, and easy to use for security professionals and penetration testers.

## ✨ Features

- 🚀 **High Performance**: Multi-threaded scanning with configurable concurrency
- 🔍 **Multiple Modes**: Directory, DNS, virtual host, S3, GCS, TFTP, and fuzzing modes
- 🛡️ **Security Focused**: Built for penetration testing and security assessments
- 🐳 **Docker Support**: Available as a Docker container
- 🔧 **Extensible**: Pattern-based scanning and custom wordlists

## 🎯 What Can Gobuster Do?

- **Web Directory/File Enumeration**: Discover hidden directories and files on web servers
- **DNS Subdomain Discovery**: Find subdomains with wildcard support
- **Virtual Host Detection**: Identify virtual hosts on target web servers
- **Cloud Storage Enumeration**: Discover open Amazon S3 and Google Cloud Storage buckets
- **TFTP File Discovery**: Find files on TFTP servers
- **Custom Fuzzing**: Flexible fuzzing with customizable parameters

## 🚀 Quick Start

```bash
# Install gobuster
go install github.com/OJ/gobuster/v3@latest

# Basic directory enumeration
gobuster dir -u https://example.com -w /path/to/wordlist.txt

# DNS subdomain enumeration
gobuster dns -do example.com -w /path/to/wordlist.txt

# Virtual host discovery
gobuster vhost -u https://example.com -w /path/to/wordlist.txt

# S3 bucket enumeration
gobuster s3 -w /path/to/bucket-names.txt
```

## 📦 Installation

### Quick Install (Recommended)

```bash
go install github.com/OJ/gobuster/v3@latest
```

**Requirements**: Go 1.24 or higher

### Alternative Installation Methods

#### Using Binary Releases

Download pre-compiled binaries from the [releases page](https://github.com/OJ/gobuster/releases).

#### Using Docker

```bash
# Pull the latest image
docker pull ghcr.io/oj/gobuster:latest

# Run gobuster in Docker
docker run --rm -it ghcr.io/oj/gobuster:latest dir -u https://example.com -w /usr/share/wordlists/dirb/common.txt
```

#### Building from Source

```bash
git clone https://github.com/OJ/gobuster.git
cd gobuster
go mod tidy
go build
```

### Troubleshooting Installation

If you encounter issues:

- Ensure Go version 1.24+ is installed: `go version`
- Check your `$GOPATH` and `$GOBIN` environment variables
- Verify `$GOPATH/bin` is in your `$PATH`

## 🎯 Usage

Gobuster uses a mode-based approach. Each mode is designed for specific enumeration tasks:

```bash
gobuster [mode] [options]
```

### Getting Help

```bash
gobuster help                   # Show general help
gobuster help [mode]            # Show help for specific mode
gobuster [mode] --help          # Alternative help syntax
```

### 📊 Available Modes

#### 🌐 Directory Mode (`dir`)

Enumerate directories and files on web servers.

**Basic Usage:**

```bash
gobuster dir -u https://example.com -w wordlist.txt
```

**Advanced Options:**

```bash
# With file extensions
gobuster dir -u https://example.com -w wordlist.txt -x php,html,js,txt

# With custom headers and cookies
gobuster dir -u https://example.com -w wordlist.txt -H "Authorization: Bearer token" -c "session=value"

# Show response length
gobuster dir -u https://example.com -w wordlist.txt -l

# Filter by status codes
gobuster dir -u https://example.com -w wordlist.txt -s 200,301,302
```

#### 🔍 DNS Mode (`dns`)

Discover subdomains through DNS resolution.

**Basic Usage:**

```bash
gobuster dns -do example.com -w wordlist.txt
```

**Advanced Options:**

```bash
# Use custom DNS server
gobuster dns -do example.com -w wordlist.txt -r 8.8.8.8:53

# Increase threads for faster scanning
gobuster dns -do example.com -w wordlist.txt -t 50
```

#### 🏠 Virtual Host Mode (`vhost`)

Discover virtual hosts on web servers.

**Basic Usage:**

```bash
gobuster vhost -u https://example.com --append-domain -w wordlist.txt
```

#### ☁️ S3 Mode (`s3`)

Enumerate Amazon S3 buckets.

**Basic Usage:**

```bash
gobuster s3 -w bucket-names.txt
```

**With Debug Output:**

```bash
gobuster s3 -w bucket-names.txt --debug
```

#### 🖥️ TFTP Mode (`tftp`)

Enumerate files on tftp servers.

**Basic Usage:**

```bash
gobuster tftp -s 10.0.0.1 -w wordlist.txt
```

#### ☁️ GCS Mode (`gcs`)

Enumerate Google Cloud Storage Buckets.

**Basic Usage:**

```bash
gobuster gcs -w bucket-names.txt
```

**With Debug Output:**

```bash
gobuster gcs -w bucket-names.txt --debug
```

#### 🔧 Fuzz Mode (`fuzz`)

Custom fuzzing with the `FUZZ` keyword.

**Basic Usage:**

```bash
gobuster fuzz -u https://example.com?FUZZ=test -w wordlist.txt
```

**Advanced Examples:**

```bash
# Fuzz URL parameters
gobuster fuzz -u https://example.com?param=FUZZ -w wordlist.txt

# Fuzz headers
gobuster fuzz -u https://example.com -H "X-Custom-Header: FUZZ" -w wordlist.txt

# Fuzz POST data
gobuster fuzz -u https://example.com -d "username=admin&password=FUZZ" -w passwords.txt
```

## 💰 Support

[![Backers on Open Collective](https://opencollective.com/gobuster/backers/badge.svg)](https://opencollective.com/gobuster) [![Sponsors on Open Collective](https://opencollective.com/gobuster/sponsors/badge.svg)](https://opencollective.com/gobuster)

### Love this tool? Back it!

If you're backing us already, you rock. If you're not, that's cool too! Want to back us? [Become a backer](https://opencollective.com/gobuster#backer)!

[![Backers](https://opencollective.com/gobuster/backers.svg?width=890)](https://opencollective.com/gobuster#backers)

All funds that are donated to this project will be donated to charity. A full log of charity donations will be available in this repository as they are processed.

## 💡 Common Use Cases

### Web Application Security Testing

```bash
# Comprehensive directory enumeration
gobuster dir -u https://target.com -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -x php,html,js,txt,asp,aspx,jsp

# API endpoint discovery
gobuster dir -u https://api.target.com -w /usr/share/wordlists/dirb/common.txt -x json

# Admin panel discovery
gobuster dir -u https://target.com -w admin-panels.txt -s 200,301,302,403
```

### DNS Reconnaissance

```bash
# Comprehensive subdomain enumeration
gobuster dns -do target.com -w /usr/share/wordlists/dnsrecon/subdomains-top1mil-5000.txt -t 50
```

### Cloud Storage Assessment

```bash
# S3 bucket enumeration with patterns
gobuster s3 -w company-names.txt -v

# GCS bucket enumeration
gobuster gcs -w company-names.txt -v
```

## 🔧 Troubleshooting

### Common Issues

#### "Permission Denied" or "Access Denied"

- Try reducing thread count with `-t` flag
- Add delays between requests with `--delay`
- Use different user agent with `-a` flag

#### "Connection Timeout"

- Increase timeout with `--timeout` flag
- Reduce thread count for slower targets
- Check your internet connection

#### "No Results Found"

- Verify the target URL is accessible
- Try different wordlists
- Check status code filtering with `-s` flag

### Performance Issues

#### Slow Scanning

- Increase thread count with `-t` flag (but be careful not to overwhelm the target)
- Use smaller, more targeted wordlists

## 🎯 Best Practices

### Security Testing Guidelines

1. **Always get proper authorization** before testing any target
2. **Start with low thread counts** to avoid overwhelming servers
3. **Use appropriate wordlists** for the target technology
4. **Respect rate limits** and implement delays if needed
5. **Monitor your network traffic** to avoid detection

### Wordlist Selection

- **For web applications**: Use technology-specific wordlists (PHP, ASP.NET, etc.)
- **For APIs**: Focus on common API endpoints and versioning patterns
- **For DNS**: Use subdomain-specific wordlists with common patterns
- **For cloud storage**: Use company/brand-specific patterns

### Output Management

```bash
# Save results to file
gobuster dir -u https://example.com -w wordlist.txt -o results.txt

# Use quiet mode for clean output
gobuster dir -u https://example.com -w wordlist.txt -q
```

## 📚 Additional Resources

### Recommended Wordlists

- **SecLists**: [https://github.com/danielmiessler/SecLists](https://github.com/danielmiessler/SecLists)
- **FuzzDB**: [https://github.com/fuzzdb-project/fuzzdb](https://github.com/fuzzdb-project/fuzzdb)
- **Seclists DNS**: [https://github.com/danielmiessler/SecLists/tree/master/Discovery/DNS](https://github.com/danielmiessler/SecLists/tree/master/Discovery/DNS)

---

**Happy hacking! 🚀**

_Remember: Always test responsibly and with proper authorization._

# Changes

<details>

<summary>3.8.2</summary>

## 3.8.2

- Fix expanded mode to show the full url again

</details>

<details>

<summary>3.8.1</summary>

## 3.8.1

- Fix expanded mode showing the entries twice

</details>

<details>

<summary>3.8</summary>

## 3.8

- Add exclude-hostname-length flag to dynamically adjust exclude-length by @0xyy66
- Fix Fuzzing query parameters
- Add `--force` flag in `dir` mode to continue execution if precheck errors occur

</details>

<details>

<summary>3.7</summary>

## 3.7

- use new cli library
- a lot more short options due to the new cli library
- more user friendly error messages
- clean up DNS mode
- renamed `show-cname` to `check-cname` in dns mode
- got rid of `verbose` flag and introduced `debug` instead
- the version command now also shows some build variables for more info
- switched to another pkcs12 library to support p12s generated with openssl3 that use SHA256 HMAC
- comments in wordlists (strings starting with #) are no longer ignored
- warn in vhost mode if the --append-domain switch might have been forgotten
- allow to exclude status code and length in vhost mode
- added automaxprocs for use in docker with cpu limits
- log http requests with debug enabled
- allow fuzzing of Host header in fuzz mode
- automatically disable progress output when output is redirected
- fix extra special characters when run with `--no-progress`
- warn when using vhost mode with a proxy and http based urls as this might not work as expected
- add `interface` and `local-ip` parameters to specify the outgoing interface for http requests
- add support for tls renegotiation
- fix progress with patterns by @acammack
- fix backup discovery by @acammack
- support tcp protocol on dns servers
- add support for URL query parameters

</details>

<details>
<summary>3.6</summary>

## 3.6

- Wordlist offset parameter to skip x lines from the wordlist
- prevent double slashes when building up an url in dir mode
- allow for multiple values and ranges on `--exclude-length`
- `no-fqdn` parameter on dns bruteforce to disable the use of the systems search domains. This should speed up the run if you have configured some search domains. [https://github.com/OJ/gobuster/pull/418](https://github.com/OJ/gobuster/pull/418)

</details>

<details>
<summary>3.5</summary>

## 3.5

- Allow Ranges in status code and status code blacklist. Example: 200,300-305,404

</details>

<details>
<summary>3.4</summary>

## 3.4

- Enable TLS1.0 and TLS1.1 support
- Add TFTP mode to search for files on tftp servers

</details>

<details>
<summary>3.3</summary>

## 3.3

- Support TLS client certificates / mtls
- support loading extensions from file
- support fuzzing POST body, HTTP headers and basic auth
- new option to not canonicalize header names

</details>

<details>
<summary>3.2</summary>

## 3.2

- Use go 1.19
- use contexts in the correct way
- get rid of the wildcard flag (except in DNS mode)
- color output
- retry on timeout
- google cloud bucket enumeration
- fix nil reference errors

</details>

<details>
<summary>3.1</summary>

## 3.1

- enumerate public AWS S3 buckets
- fuzzing mode
- specify HTTP method
- added support for patterns. You can now specify a file containing patterns that are applied to every word, one by line. Every occurrence of the term `{GOBUSTER}` in it will be replaced with the current wordlist item. Please use with caution as this can cause increase the number of requests issued a lot.
- The shorthand `p` flag which was assigned to proxy is now used by the pattern flag

</details>

<details>
<summary>3.0</summary>

## 3.0

- New CLI options so modes are strictly separated (`-m` is now gone!)
- Performance Optimizations and better connection handling
- Ability to enumerate vhost names
- Option to supply custom HTTP headers

</details>


================================================
FILE: Taskfile.yml
================================================
version: "3"

vars:
  PROGRAM: gobuster

tasks:
  deps:
    cmds:
      - go mod tidy -v

  update:
    cmds:
      - go get -u
      - go get -u tool
      - go mod tidy -v

  check:
    cmds:
      - go fmt ./...
      - go tool gofumpt -l -w .
      - go vet ./...

  build:
    aliases: [default]
    deps: [deps, check]
    cmds:
      - go build -o {{.OUTPUT_FILE | default .PROGRAM}}
    env:
      CGO_ENABLED: 0
      GOOS: '{{.GOOS | default "linux"}}'
      GOARCH: '{{.GOARCH | default "amd64"}}'

  linux:
    cmds:
      - task: build
        vars:
          GOOS: linux
          GOARCH: amd64

  windows:
    cmds:
      - task: build
        vars:
          OUTPUT_FILE: "{{.PROGRAM}}.exe"
          GOOS: windows
          GOARCH: amd64

  test:
    deps: [deps, check]
    env:
      CGO_ENABLED: 1
    cmds:
      - go test -race -cover ./...

  lint:
    cmds:
      - golangci-lint run ./... --timeout=30m
      - go mod tidy

  lint-update:
    cmds:
      - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b {{ .GOPATH }}/bin
      - golangci-lint --version
    vars:
      GOPATH:
        sh: go env GOPATH

  tag:
    cmds:
      - git tag -a "${TAG}" -m "${TAG}"
      - git push origin "${TAG}"
    preconditions:
      - sh: '[[ -n "${TAG}" ]]'
        msg: "Please set the TAG environment variable"


================================================
FILE: cli/const.go
================================================
//go:build !windows

package cli

const (
	TerminalClearLine = "\r\x1b[2K"
)


================================================
FILE: cli/const_windows.go
================================================
//go:build windows

package cli

const (
	TerminalClearLine = "\r\r"
)


================================================
FILE: cli/dir/dir.go
================================================
package dir

import (
	"errors"
	"fmt"

	internalcli "github.com/OJ/gobuster/v3/cli"
	"github.com/OJ/gobuster/v3/gobusterdir"
	"github.com/OJ/gobuster/v3/libgobuster"
	"github.com/urfave/cli/v2"
)

func Command() *cli.Command {
	cmd := cli.Command{
		Name:   "dir",
		Usage:  "Uses directory/file enumeration mode",
		Action: run,
		Flags:  getFlags(),
	}
	return &cmd
}

func getFlags() []cli.Flag {
	var flags []cli.Flag
	flags = append(flags, internalcli.CommonHTTPOptions()...)
	flags = append(flags, internalcli.GlobalOptions()...)
	flags = append(flags, []cli.Flag{
		&cli.StringFlag{Name: "status-codes", Aliases: []string{"s"}, Usage: "Positive status codes (will be overwritten with status-codes-blacklist if set). Can also handle ranges like 200,300-400,404"},
		&cli.StringFlag{Name: "status-codes-blacklist", Aliases: []string{"b"}, Usage: "Negative status codes (will override status-codes if set). Can also handle ranges like 200,300-400,404.", Value: "404"},
		&cli.StringFlag{Name: "extensions", Aliases: []string{"x"}, Usage: "File extension(s) to search for"},
		&cli.StringFlag{Name: "extensions-file", Aliases: []string{"X"}, Usage: "Read file extension(s) to search from the file"},
		&cli.BoolFlag{Name: "expanded", Aliases: []string{"e"}, Value: false, Usage: "Expanded mode, print full URLs"},
		&cli.BoolFlag{Name: "no-status", Aliases: []string{"n"}, Value: false, Usage: "Don't print status codes"},
		&cli.BoolFlag{Name: "hide-length", Aliases: []string{"hl"}, Value: false, Usage: "Hide the length of the body in the output"},
		&cli.BoolFlag{Name: "add-slash", Aliases: []string{"f"}, Value: false, Usage: "Append / to each request"},
		&cli.BoolFlag{Name: "discover-backup", Aliases: []string{"db"}, Value: false, Usage: "Upon finding a file search for backup files by appending multiple backup extensions"},
		&cli.StringFlag{Name: "exclude-length", Aliases: []string{"xl"}, Usage: "exclude the following content lengths (completely ignores the status). You can separate multiple lengths by comma and it also supports ranges like 203-206"},
		&cli.BoolFlag{Name: "force", Value: false, Usage: "Continue even if the prechecks fail. Please only use this if you know what you are doing, it can lead to unexpected results."},
	}...)
	return flags
}

func run(c *cli.Context) error {
	pluginOpts := gobusterdir.NewOptions()

	httpOptions, err := internalcli.ParseCommonHTTPOptions(c)
	if err != nil {
		return err
	}
	pluginOpts.HTTPOptions = httpOptions

	pluginOpts.Extensions = c.String("extensions")
	ret, err := libgobuster.ParseExtensions(pluginOpts.Extensions)
	if err != nil {
		return fmt.Errorf("invalid value for extensions: %w", err)
	}
	pluginOpts.ExtensionsParsed = ret

	pluginOpts.ExtensionsFile = c.String("extensions-file")
	if pluginOpts.ExtensionsFile != "" {
		extensions, err := libgobuster.ParseExtensionsFile(pluginOpts.ExtensionsFile)
		if err != nil {
			return fmt.Errorf("invalid value for extensions file: %w", err)
		}
		pluginOpts.ExtensionsParsed.AddRange(extensions)
	}

	pluginOpts.StatusCodes = c.String("status-codes")
	ret2, err := libgobuster.ParseCommaSeparatedInt(pluginOpts.StatusCodes)
	if err != nil {
		return fmt.Errorf("invalid value for status-codes: %w", err)
	}
	pluginOpts.StatusCodesParsed = ret2

	pluginOpts.StatusCodesBlacklist = c.String("status-codes-blacklist")
	ret3, err := libgobuster.ParseCommaSeparatedInt(pluginOpts.StatusCodesBlacklist)
	if err != nil {
		return fmt.Errorf("invalid value for status-codes-blacklist: %w", err)
	}
	pluginOpts.StatusCodesBlacklistParsed = ret3

	if pluginOpts.StatusCodes != "" && pluginOpts.StatusCodesBlacklist != "" {
		return fmt.Errorf("status-codes (%q) and status-codes-blacklist (%q) are both set - please set only one. status-codes-blacklist is set by default so you might want to disable it by supplying an empty string",
			pluginOpts.StatusCodes, pluginOpts.StatusCodesBlacklist)
	}

	if pluginOpts.StatusCodes == "" && pluginOpts.StatusCodesBlacklist == "" {
		return errors.New("status-codes and status-codes-blacklist are both not set, please set one")
	}

	pluginOpts.UseSlash = c.Bool("add-slash")
	pluginOpts.Expanded = c.Bool("expanded")
	pluginOpts.NoStatus = c.Bool("no-status")
	pluginOpts.HideLength = c.Bool("hide-length")
	pluginOpts.DiscoverBackup = c.Bool("discover-backup")
	pluginOpts.Force = c.Bool("force")
	pluginOpts.ExcludeLength = c.String("exclude-length")
	ret4, err := libgobuster.ParseCommaSeparatedInt(pluginOpts.ExcludeLength)
	if err != nil {
		return fmt.Errorf("invalid value for exclude-length: %w", err)
	}
	pluginOpts.ExcludeLengthParsed = ret4

	globalOpts, err := internalcli.ParseGlobalOptions(c)
	if err != nil {
		return err
	}

	log := libgobuster.NewLogger(globalOpts.Debug)

	plugin, err := gobusterdir.New(&globalOpts, pluginOpts, log)
	if err != nil {
		return fmt.Errorf("error on creating gobusterdir: %w", err)
	}

	if err := internalcli.Gobuster(c.Context, &globalOpts, plugin, log); err != nil {
		var wErr *gobusterdir.WildcardError
		if errors.As(err, &wErr) {
			return fmt.Errorf("%w. To continue please exclude the status code or the length", wErr)
		}
		log.Debugf("%#v", err)
		return fmt.Errorf("error on running gobuster on %s: %w", pluginOpts.URL, err)
	}
	return nil
}


================================================
FILE: cli/dir/dir_test.go
================================================
package dir

import (
	"fmt"
	"net/http"
	"net/http/httptest"
	"net/url"
	"os"
	"testing"
	"time"

	"github.com/OJ/gobuster/v3/cli"
	"github.com/OJ/gobuster/v3/gobusterdir"
	"github.com/OJ/gobuster/v3/libgobuster"
)

func httpServer(b *testing.B, content string) *httptest.Server {
	b.Helper()
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		if _, err := fmt.Fprint(w, content); err != nil {
			b.Fatalf("%v", err)
		}
	}))
	return ts
}

func BenchmarkDirMode(b *testing.B) {
	h := httpServer(b, "test")
	defer h.Close()

	u, err := url.Parse(h.URL)
	if err != nil {
		b.Fatalf("could not parse URL: %v", err)
	}
	pluginopts := gobusterdir.NewOptions()
	pluginopts.URL = u
	pluginopts.Timeout = 10 * time.Second

	pluginopts.Extensions = ".php,.csv"
	tmpExt, err := libgobuster.ParseExtensions(pluginopts.Extensions)
	if err != nil {
		b.Fatalf("could not parse extensions: %v", err)
	}
	pluginopts.ExtensionsParsed = tmpExt

	pluginopts.StatusCodes = "200,204,301,302,307,401,403"
	tmpStat, err := libgobuster.ParseCommaSeparatedInt(pluginopts.StatusCodes)
	if err != nil {
		b.Fatalf("could not parse status codes: %v", err)
	}
	pluginopts.StatusCodesParsed = tmpStat

	wordlist, err := os.CreateTemp(b.TempDir(), "")
	if err != nil {
		b.Fatalf("could not create tempfile: %v", err)
	}
	defer os.Remove(wordlist.Name())
	for w := range 1000 {
		_, _ = fmt.Fprintf(wordlist, "%d\n", w)
	}
	if err := wordlist.Close(); err != nil {
		b.Fatalf("%v", err)
	}

	globalopts := libgobuster.Options{
		Threads:    10,
		Wordlist:   wordlist.Name(),
		NoProgress: true,
	}

	ctx := b.Context()
	oldStdout := os.Stdout
	oldStderr := os.Stderr
	defer func(out, err *os.File) { os.Stdout = out; os.Stderr = err }(oldStdout, oldStderr)
	devnull, err := os.Open(os.DevNull)
	if err != nil {
		b.Fatalf("could not get devnull %v", err)
	}
	defer devnull.Close()
	log := libgobuster.NewLogger(false)

	// Run the real benchmark
	for b.Loop() {
		os.Stdout = devnull
		os.Stderr = devnull
		plugin, err := gobusterdir.New(&globalopts, pluginopts, log)
		if err != nil {
			b.Fatalf("error on creating gobusterdir: %v", err)
		}

		if err := cli.Gobuster(ctx, &globalopts, plugin, log); err != nil {
			b.Fatalf("error on running gobuster: %v", err)
		}
		os.Stdout = oldStdout
		os.Stderr = oldStderr
	}
}


================================================
FILE: cli/dns/dns.go
================================================
package dns

import (
	"errors"
	"fmt"
	"runtime"
	"time"

	internalcli "github.com/OJ/gobuster/v3/cli"
	"github.com/OJ/gobuster/v3/gobusterdns"
	"github.com/OJ/gobuster/v3/libgobuster"
	"github.com/urfave/cli/v2"
)

func Command() *cli.Command {
	cmd := cli.Command{
		Name:   "dns",
		Usage:  "Uses DNS subdomain enumeration mode",
		Action: run,
		Flags:  getFlags(),
	}
	return &cmd
}

func getFlags() []cli.Flag {
	var flags []cli.Flag
	flags = append(flags, []cli.Flag{
		&cli.StringFlag{Name: "domain", Aliases: []string{"do"}, Usage: "The target domain", Required: true},
		&cli.BoolFlag{Name: "check-cname", Aliases: []string{"c"}, Value: false, Usage: "Also check CNAME records"},
		&cli.DurationFlag{Name: "timeout", Aliases: []string{"to"}, Value: 1 * time.Second, Usage: "DNS resolver timeout"},
		&cli.BoolFlag{Name: "wildcard", Aliases: []string{"wc"}, Value: false, Usage: "Force continued operation when wildcard found"},
		&cli.BoolFlag{Name: "no-fqdn", Aliases: []string{"nf"}, Value: false, Usage: "Do not automatically add a trailing dot to the domain, so the resolver uses the DNS search domain"},
		&cli.StringFlag{Name: "resolver", Usage: "Use custom DNS server (format server.com or server.com:port)"},
		&cli.StringFlag{Name: "protocol", Value: "udp", Usage: "Use either 'udp' or 'tcp' as protocol on the custom resolver"},
	}...)
	flags = append(flags, internalcli.GlobalOptions()...)
	return flags
}

func run(c *cli.Context) error {
	pluginOpts := gobusterdns.NewOptions()

	pluginOpts.Domain = c.String("domain")
	pluginOpts.CheckCNAME = c.Bool("check-cname")
	pluginOpts.Timeout = c.Duration("timeout")
	pluginOpts.WildcardForced = c.Bool("wildcard")
	pluginOpts.NoFQDN = c.Bool("no-fqdn")
	pluginOpts.Resolver = c.String("resolver")
	pluginOpts.Protocol = c.String("protocol")

	if pluginOpts.Resolver != "" && runtime.GOOS == "windows" {
		return errors.New("currently can not set custom dns resolver on windows. See https://golang.org/pkg/net/#hdr-Name_Resolution")
	}

	if pluginOpts.Protocol != "udp" && pluginOpts.Protocol != "tcp" {
		return errors.New("protocol must be either 'udp' or 'tcp'")
	}

	if pluginOpts.Protocol != "udp" && pluginOpts.Resolver == "" {
		return errors.New("custom protocol can only be set if a custom resolver is set")
	}

	globalOpts, err := internalcli.ParseGlobalOptions(c)
	if err != nil {
		return err
	}

	plugin, err := gobusterdns.New(&globalOpts, pluginOpts)
	if err != nil {
		return fmt.Errorf("error on creating gobusterdns: %w", err)
	}

	log := libgobuster.NewLogger(globalOpts.Debug)
	if err := internalcli.Gobuster(c.Context, &globalOpts, plugin, log); err != nil {
		var wErr *gobusterdns.WildcardError
		if errors.As(err, &wErr) {
			return fmt.Errorf("%w. To force processing of Wildcard DNS, specify the '--wildcard' switch", wErr)
		}
		log.Debugf("%#v", err)
		return fmt.Errorf("error on running gobuster on %s: %w", pluginOpts.Domain, err)
	}
	return nil
}


================================================
FILE: cli/fuzz/fuzz.go
================================================
package fuzz

import (
	"fmt"
	"strings"

	internalcli "github.com/OJ/gobuster/v3/cli"
	"github.com/OJ/gobuster/v3/gobusterfuzz"
	"github.com/OJ/gobuster/v3/libgobuster"
	"github.com/urfave/cli/v2"
)

func Command() *cli.Command {
	cmd := cli.Command{
		Name:   "fuzz",
		Usage:  fmt.Sprintf("Uses fuzzing mode. Replaces the keyword %s in the URL, Headers and the request body", gobusterfuzz.FuzzKeyword),
		Action: run,
		Flags:  getFlags(),
	}
	return &cmd
}

func getFlags() []cli.Flag {
	var flags []cli.Flag
	flags = append(flags, internalcli.CommonHTTPOptions()...)
	flags = append(flags, internalcli.GlobalOptions()...)
	flags = append(flags, []cli.Flag{
		&cli.StringFlag{Name: "exclude-statuscodes", Aliases: []string{"b"}, Usage: "Excluded status codes. Can also handle ranges like 200,300-400,404."},
		&cli.StringFlag{Name: "exclude-length", Aliases: []string{"xl"}, Usage: "exclude the following content lengths (completely ignores the status). You can separate multiple lengths by comma and it also supports ranges like 203-206"},
		&cli.StringFlag{Name: "body", Aliases: []string{"B"}, Usage: "Request body"},
	}...)

	return flags
}

func run(c *cli.Context) error {
	pluginOpts := gobusterfuzz.NewOptions()

	httpOptions, err := internalcli.ParseCommonHTTPOptions(c)
	if err != nil {
		return err
	}
	pluginOpts.HTTPOptions = httpOptions

	pluginOpts.ExcludedStatusCodes = c.String("exclude-statuscodes")
	ret, err := libgobuster.ParseCommaSeparatedInt(pluginOpts.ExcludedStatusCodes)
	if err != nil {
		return fmt.Errorf("invalid value for excludestatuscodes: %w", err)
	}
	pluginOpts.ExcludedStatusCodesParsed = ret

	pluginOpts.ExcludeLength = c.String("exclude-length")
	ret2, err := libgobuster.ParseCommaSeparatedInt(pluginOpts.ExcludeLength)
	if err != nil {
		return fmt.Errorf("invalid value for exclude-length: %w", err)
	}
	pluginOpts.ExcludeLengthParsed = ret2

	pluginOpts.RequestBody = c.String("body")

	globalOpts, err := internalcli.ParseGlobalOptions(c)
	if err != nil {
		return err
	}

	if !containsFuzzKeyword(*pluginOpts) {
		return fmt.Errorf("please provide the %s keyword", gobusterfuzz.FuzzKeyword)
	}

	log := libgobuster.NewLogger(globalOpts.Debug)

	plugin, err := gobusterfuzz.New(&globalOpts, pluginOpts, log)
	if err != nil {
		return fmt.Errorf("error on creating gobusterfuzz: %w", err)
	}

	if err := internalcli.Gobuster(c.Context, &globalOpts, plugin, log); err != nil {
		log.Debugf("%#v", err)
		return fmt.Errorf("error on running gobuster on %s: %w", pluginOpts.URL, err)
	}
	return nil
}

func containsFuzzKeyword(pluginopts gobusterfuzz.OptionsFuzz) bool {
	if strings.Contains(pluginopts.URL.String(), gobusterfuzz.FuzzKeyword) {
		return true
	}

	if strings.Contains(pluginopts.RequestBody, gobusterfuzz.FuzzKeyword) {
		return true
	}

	for _, h := range pluginopts.Headers {
		if strings.Contains(h.Name, gobusterfuzz.FuzzKeyword) || strings.Contains(h.Value, gobusterfuzz.FuzzKeyword) {
			return true
		}
	}

	if strings.Contains(pluginopts.Username, gobusterfuzz.FuzzKeyword) {
		return true
	}

	if strings.Contains(pluginopts.Password, gobusterfuzz.FuzzKeyword) {
		return true
	}

	return false
}


================================================
FILE: cli/gcs/gcs.go
================================================
package gcs

import (
	"fmt"

	internalcli "github.com/OJ/gobuster/v3/cli"
	"github.com/OJ/gobuster/v3/gobustergcs"
	"github.com/OJ/gobuster/v3/libgobuster"
	"github.com/urfave/cli/v2"
)

func Command() *cli.Command {
	cmd := cli.Command{
		Name:   "gcs",
		Usage:  "Uses gcs bucket enumeration mode",
		Action: run,
		Flags:  getFlags(),
	}
	return &cmd
}

func getFlags() []cli.Flag {
	var flags []cli.Flag
	flags = append(flags, []cli.Flag{
		&cli.IntFlag{Name: "max-files", Aliases: []string{"m"}, Value: 5, Usage: "max files to list when listing buckets"},
		&cli.BoolFlag{Name: "show-files", Aliases: []string{"s"}, Value: true, Usage: "show files from found buckets"},
	}...)
	flags = append(flags, internalcli.GlobalOptions()...)
	flags = append(flags, internalcli.BasicHTTPOptions()...)
	return flags
}

func run(c *cli.Context) error {
	pluginOpts := gobustergcs.NewOptions()

	httpOptions, err := internalcli.ParseBasicHTTPOptions(c)
	if err != nil {
		return err
	}
	pluginOpts.BasicHTTPOptions = httpOptions

	pluginOpts.MaxFilesToList = c.Int("max-files")
	pluginOpts.ShowFiles = c.Bool("show-files")

	globalOpts, err := internalcli.ParseGlobalOptions(c)
	if err != nil {
		return err
	}

	log := libgobuster.NewLogger(globalOpts.Debug)

	plugin, err := gobustergcs.New(&globalOpts, pluginOpts, log)
	if err != nil {
		return fmt.Errorf("error on creating gobustergcs: %w", err)
	}

	if err := internalcli.Gobuster(c.Context, &globalOpts, plugin, log); err != nil {
		log.Debugf("%#v", err)
		return fmt.Errorf("error on running gobuster: %w", err)
	}
	return nil
}


================================================
FILE: cli/gobuster.go
================================================
package cli

import (
	"context"
	"errors"
	"fmt"
	"math"
	"os"
	"strings"
	"sync"
	"time"

	"github.com/OJ/gobuster/v3/libgobuster"
)

const (
	ruler             = "==============================================================="
	cliProgressUpdate = 500 * time.Millisecond
)

// resultWorker outputs the results as they come in. This needs to be a range and should not handle
// the context so the channel always has a receiver and libgobuster will not block.
func resultWorker(g *libgobuster.Gobuster, filename string, wg *sync.WaitGroup) {
	defer wg.Done()

	var f *os.File
	var err error
	if filename != "" {
		f, err = os.Create(filename)
		if err != nil {
			g.Logger.Fatalf("error on creating output file: %v", err)
		}
		defer f.Close()
	}

	for r := range g.Progress.ResultChan {
		s, err := r.ResultToString()
		if err != nil {
			g.Logger.Fatal(err)
		}
		if s != "" {
			s = strings.TrimSpace(s)
			if g.Opts.NoProgress || g.Opts.Quiet {
				_, _ = fmt.Printf("%s\n", s) // nolint forbidigo
			} else {
				// only print the clear line when progress output is enabled
				_, _ = fmt.Printf("%s%s\n", TerminalClearLine, s) // nolint forbidigo
			}
			if f != nil {
				err = writeToFile(f, s)
				if err != nil {
					g.Logger.Fatalf("error on writing output file: %v", err)
				}
			}
		}
	}
}

// errorWorker outputs the errors as they come in. This needs to be a range and should not handle
// the context so the channel always has a receiver and libgobuster will not block.
func errorWorker(g *libgobuster.Gobuster, wg *sync.WaitGroup) {
	defer wg.Done()

	for e := range g.Progress.ErrorChan {
		if !g.Opts.Quiet && !g.Opts.NoError {
			g.Logger.Error(e.Error())
			g.Logger.Debugf("%#v", e)
		}
	}
}

// messageWorker outputs messages as they come in. This needs to be a range and should not handle
// the context so the channel always has a receiver and libgobuster will not block.
func messageWorker(g *libgobuster.Gobuster, wg *sync.WaitGroup) {
	defer wg.Done()

	for msg := range g.Progress.MessageChan {
		if !g.Opts.Quiet {
			switch msg.Level {
			case libgobuster.LevelDebug:
				g.Logger.Debug(msg.Message)
			case libgobuster.LevelError:
				g.Logger.Error(msg.Message)
			case libgobuster.LevelWarn:
				g.Logger.Warn(msg.Message)
			case libgobuster.LevelInfo:
				g.Logger.Info(msg.Message)
			default:
				panic(fmt.Sprintf("invalid level %d", msg.Level))
			}
		}
	}
}

func printProgress(g *libgobuster.Gobuster) {
	requestsIssued := g.Progress.RequestsIssued()
	requestsExpected := g.Progress.RequestsExpected()
	if requestsExpected == 0 {
		requestsExpected = 1 // avoid division by zero
	}
	percent := float32(requestsIssued) * 100.0 / float32(requestsExpected)
	if math.IsNaN(float64(percent)) {
		percent = 0.0
	}
	s := fmt.Sprintf("%sProgress: %d / %d (%3.2f%%)", TerminalClearLine, requestsIssued, requestsExpected, percent)
	_, _ = fmt.Fprint(os.Stderr, s)
}

// progressWorker outputs the progress every tick. It will stop once cancel() is called
// on the context
func progressWorker(ctx context.Context, g *libgobuster.Gobuster, wg *sync.WaitGroup) {
	defer wg.Done()

	tick := time.NewTicker(cliProgressUpdate)

	for {
		select {
		case <-tick.C:
			printProgress(g)
		case <-ctx.Done():
			// print the final progress so we end at 100%
			printProgress(g)
			fmt.Println() // nolint:forbidigo
			return
		}
	}
}

func writeToFile(f *os.File, output string) error {
	_, err := fmt.Fprintf(f, "%s\n", output)
	if err != nil {
		return fmt.Errorf("[!] Unable to write to file %w", err)
	}
	return nil
}

// Gobuster is the main entry point for the CLI
func Gobuster(ctx context.Context, opts *libgobuster.Options, plugin libgobuster.GobusterPlugin, log *libgobuster.Logger) error {
	// Sanity checks
	if opts == nil {
		return errors.New("please provide valid options")
	}

	if plugin == nil {
		return errors.New("please provide a valid plugin")
	}

	ctxCancel, cancel := context.WithCancel(ctx)
	defer cancel()

	gobuster, err := libgobuster.NewGobuster(opts, plugin, log)
	if err != nil {
		return err
	}

	if !opts.Quiet {
		log.Println(ruler)
		log.Printf("Gobuster v%s\n", libgobuster.VERSION)
		log.Println("by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)")
		log.Println(ruler)
		c, err := gobuster.GetConfigString()
		if err != nil {
			return fmt.Errorf("error on creating config string: %w", err)
		}
		log.Println(c)
		log.Println(ruler)
		gobuster.Logger.Printf("Starting gobuster in %s mode", plugin.Name())
		if opts.WordlistOffset > 0 {
			gobuster.Logger.Printf("Skipping the first %d elements...", opts.WordlistOffset)
		}
		log.Println(ruler)
	}

	fi, err := os.Stdout.Stat()
	if err != nil {
		return err
	}
	// check if we are not in a terminal. If so, disable output
	if (fi.Mode() & os.ModeCharDevice) != os.ModeCharDevice {
		opts.NoProgress = true
	}

	// our waitgroup for all goroutines
	// this ensures all goroutines are finished
	// when we call wg.Wait()
	var wg sync.WaitGroup

	wg.Add(1)
	go resultWorker(gobuster, opts.OutputFilename, &wg)

	wg.Add(1)
	go errorWorker(gobuster, &wg)

	wg.Add(1)
	go messageWorker(gobuster, &wg)

	if !opts.Quiet && !opts.NoProgress {
		// if not quiet add a new workgroup entry and start the goroutine
		wg.Add(1)
		go progressWorker(ctxCancel, gobuster, &wg)
	}

	err = gobuster.Run(ctxCancel)

	// call cancel func so progressWorker will exit (the only goroutine in this
	// file using the context) and to free resources
	cancel()
	// wait for all spun up goroutines to finish (all have to call wg.Done())
	wg.Wait()

	// Late error checking to finish all threads
	if err != nil {
		return err
	}

	if !opts.Quiet {
		log.Println(ruler)
		gobuster.Logger.Println("Finished")
		log.Println(ruler)
	}
	return nil
}


================================================
FILE: cli/options.go
================================================
package cli

import (
	"bufio"
	"crypto/tls"
	"errors"
	"fmt"
	"net"
	"net/url"
	"os"
	"regexp"
	"strconv"
	"strings"
	"syscall"
	"time"

	"github.com/OJ/gobuster/v3/libgobuster"
	"github.com/fatih/color"
	"github.com/urfave/cli/v2"
	"golang.org/x/term"
	"software.sslmate.com/src/go-pkcs12"
)

func BasicHTTPOptions() []cli.Flag {
	return []cli.Flag{
		&cli.StringFlag{Name: "useragent", Aliases: []string{"a"}, Value: libgobuster.DefaultUserAgent(), Usage: "Set the User-Agent string"},
		&cli.BoolFlag{Name: "random-agent", Aliases: []string{"rua"}, Value: false, Usage: "Use a random User-Agent string"},
		&cli.StringFlag{Name: "proxy", Usage: "Proxy to use for requests [http(s)://host:port] or [socks5://host:port]"},
		&cli.DurationFlag{Name: "timeout", Aliases: []string{"to"}, Value: 10 * time.Second, Usage: "HTTP Timeout"},
		&cli.BoolFlag{Name: "no-tls-validation", Aliases: []string{"k"}, Value: false, Usage: "Skip TLS certificate verification"},
		&cli.BoolFlag{Name: "retry", Value: false, Usage: "Should retry on request timeout"},
		&cli.IntFlag{Name: "retry-attempts", Aliases: []string{"ra"}, Value: 3, Usage: "Times to retry on request timeout"},
		&cli.StringFlag{Name: "client-cert-pem", Aliases: []string{"ccp"}, Usage: "public key in PEM format for optional TLS client certificates]"},
		&cli.StringFlag{Name: "client-cert-pem-key", Aliases: []string{"ccpk"}, Usage: "private key in PEM format for optional TLS client certificates (this key needs to have no password)"},
		&cli.StringFlag{Name: "client-cert-p12", Aliases: []string{"ccp12"}, Usage: "a p12 file to use for options TLS client certificates"},
		&cli.StringFlag{Name: "client-cert-p12-password", Aliases: []string{"ccp12p"}, Usage: "the password to the p12 file"},
		&cli.BoolFlag{Name: "tls-renegotiation", Value: false, Usage: "Enable TLS renegotiation"},
		&cli.StringFlag{Name: "interface", Aliases: []string{"iface"}, Usage: "specify network interface to use. Can't be used with local-ip"},
		&cli.StringFlag{Name: "local-ip", Usage: "specify local ip of network interface to use. Can't be used with interface"},
	}
}

func ParseBasicHTTPOptions(c *cli.Context) (libgobuster.BasicHTTPOptions, error) {
	var opts libgobuster.BasicHTTPOptions
	opts.UserAgent = c.String("useragent")
	randomUA := c.Bool("random-agent")
	if randomUA {
		ua, err := libgobuster.GetRandomUserAgent()
		if err != nil {
			return opts, err
		}
		opts.UserAgent = ua
	}
	opts.Proxy = c.String("proxy")
	opts.Timeout = c.Duration("timeout")
	opts.NoTLSValidation = c.Bool("no-tls-validation")
	opts.RetryOnTimeout = c.Bool("retry")
	opts.RetryAttempts = c.Int("retry-attempts")

	pemFile := c.String("client-cert-pem")
	pemKeyFile := c.String("client-cert-pem-key")
	p12File := c.String("client-cert-p12")
	p12Pass := c.String("client-cert-p12-password")

	if pemFile != "" && p12File != "" {
		return opts, errors.New("please supply either a pem or a p12, not both")
	}

	if pemFile != "" {
		cert, err := tls.LoadX509KeyPair(pemFile, pemKeyFile)
		if err != nil {
			return opts, fmt.Errorf("could not load supplied pem key: %w", err)
		}
		opts.TLSCertificate = &cert
	} else if p12File != "" {
		p12Content, err := os.ReadFile(p12File)
		if err != nil {
			return opts, fmt.Errorf("could not read p12 %s: %w", p12File, err)
		}
		privKey, pubKey, _, err := pkcs12.DecodeChain(p12Content, p12Pass)
		if err != nil {
			return opts, fmt.Errorf("could not load P12: %w", err)
		}
		opts.TLSCertificate = &tls.Certificate{
			Certificate: [][]byte{pubKey.Raw},
			PrivateKey:  privKey,
		}
	}

	opts.TLSRenegotiation = c.Bool("tls-renegotiation")

	iface := c.String("interface")
	localIP := c.String("local-ip")
	if iface != "" && localIP != "" {
		return opts, errors.New("can not set both interface and local-ip")
	}

	switch {
	case iface != "":
		a, err := getLocalAddrFromInterface(iface)
		if err != nil {
			return opts, err
		}
		opts.LocalAddr = a
	case localIP != "":
		if !strings.Contains(localIP, ":") {
			localIP = fmt.Sprintf("%s:0", localIP)
		}
		a, err := net.ResolveIPAddr("ip", localIP)
		if err != nil {
			return opts, err
		}
		localTCPAddr := net.TCPAddr{
			IP: a.IP,
		}
		opts.LocalAddr = &localTCPAddr
	}

	return opts, nil
}

func CommonHTTPOptions() []cli.Flag {
	var flags []cli.Flag
	flags = append(flags, []cli.Flag{
		&cli.StringFlag{Name: "url", Aliases: []string{"u"}, Usage: "The target URL", Required: true},
		&cli.StringFlag{Name: "cookies", Aliases: []string{"c"}, Usage: "Cookies to use for the requests"},
		&cli.StringFlag{Name: "username", Aliases: []string{"U"}, Usage: "Username for Basic Auth"},
		&cli.StringFlag{Name: "password", Aliases: []string{"P"}, Usage: "Password for Basic Auth"},
		&cli.BoolFlag{Name: "follow-redirect", Aliases: []string{"r"}, Value: false, Usage: "Follow redirects"},
		&cli.StringSliceFlag{Name: "headers", Aliases: []string{"H"}, Usage: "Specify HTTP headers, -H 'Header1: val1' -H 'Header2: val2'"},
		&cli.BoolFlag{Name: "no-canonicalize-headers", Aliases: []string{"nch"}, Value: false, Usage: "Do not canonicalize HTTP header names. If set header names are sent as is"},
		&cli.StringFlag{Name: "method", Aliases: []string{"m"}, Value: "GET", Usage: "the password to the p12 file"},
	}...)
	flags = append(flags, BasicHTTPOptions()...)
	return flags
}

func ParseCommonHTTPOptions(c *cli.Context) (libgobuster.HTTPOptions, error) {
	var opts libgobuster.HTTPOptions
	basic, err := ParseBasicHTTPOptions(c)
	if err != nil {
		return opts, err
	}
	opts.BasicHTTPOptions = basic

	urlInput := c.String("url")
	if !strings.HasPrefix(urlInput, "http") {
		// check to see if a port was specified
		re := regexp.MustCompile(`^[^/]+:(\d+)`)
		match := re.FindStringSubmatch(urlInput)

		if len(match) < 2 {
			// no port, default to http on 80
			urlInput = fmt.Sprintf("http://%s", urlInput)
		} else {
			port, err2 := strconv.Atoi(match[1])
			switch {
			case err2 != nil || (port != 80 && port != 443):
				return opts, errors.New("url scheme not specified")
			case port == 80:
				urlInput = fmt.Sprintf("http://%s", urlInput)
			default:
				urlInput = fmt.Sprintf("https://%s", urlInput)
			}
		}
	}

	if opts.URL, err = url.Parse(urlInput); err != nil {
		return opts, fmt.Errorf("url %q is not valid: %w", urlInput, err)
	}

	opts.Cookies = c.String("cookies")
	opts.Username = c.String("username")
	opts.Password = c.String("password")

	// Prompt for PW if not provided
	if opts.Username != "" && opts.Password == "" {
		fmt.Printf("[?] Auth Password: ") // nolint:forbidigo
		// please don't remove the int cast here as it is sadly needed on windows :/
		passBytes, err := term.ReadPassword(int(syscall.Stdin)) //nolint:unconvert
		// print a newline to simulate the newline that was entered
		// this means that formatting/printing after doesn't look bad.
		fmt.Println("") // nolint:forbidigo
		if err != nil {
			return opts, errors.New("username given but reading of password failed")
		}
		opts.Password = string(passBytes)
	}
	// if it's still empty bail out
	if opts.Username != "" && opts.Password == "" {
		return opts, errors.New("username was provided but password is missing")
	}

	opts.FollowRedirect = c.Bool("follow-redirect")
	opts.NoCanonicalizeHeaders = c.Bool("no-canonicalize-headers")
	opts.Method = c.String("method")

	for _, h := range c.StringSlice("headers") {
		keyAndValue := strings.SplitN(h, ":", 2)
		if len(keyAndValue) != 2 {
			return opts, fmt.Errorf("invalid header format for header %q", h)
		}
		key := strings.TrimSpace(keyAndValue[0])
		value := strings.TrimSpace(keyAndValue[1])
		if len(key) == 0 {
			return opts, fmt.Errorf("invalid header format for header %q - name is empty", h)
		}
		header := libgobuster.HTTPHeader{Name: key, Value: value}
		opts.Headers = append(opts.Headers, header)
	}

	return opts, nil
}

func GlobalOptions() []cli.Flag {
	return []cli.Flag{
		&cli.StringFlag{Name: "wordlist", Aliases: []string{"w"}, Usage: "Path to the wordlist. Set to - to use STDIN.", Required: true},
		&cli.DurationFlag{Name: "delay", Aliases: []string{"d"}, Usage: "Time each thread waits between requests (e.g. 1500ms)"},
		&cli.IntFlag{Name: "threads", Aliases: []string{"t"}, Value: 10, Usage: "Number of concurrent threads"},
		&cli.IntFlag{Name: "wordlist-offset", Aliases: []string{"wo"}, Value: 0, Usage: "Resume from a given position in the wordlist"},
		&cli.StringFlag{Name: "output", Aliases: []string{"o"}, Usage: "Output file to write results to (defaults to stdout)"},
		&cli.BoolFlag{Name: "quiet", Aliases: []string{"q"}, Value: false, Usage: "Don't print the banner and other noise"},
		&cli.BoolFlag{Name: "no-progress", Aliases: []string{"np"}, Value: false, Usage: "Don't display progress"},
		&cli.BoolFlag{Name: "no-error", Aliases: []string{"ne"}, Value: false, Usage: "Don't display errors"},
		&cli.StringFlag{Name: "pattern", Aliases: []string{"p"}, Usage: "File containing replacement patterns"},
		&cli.StringFlag{Name: "discover-pattern", Aliases: []string{"pd"}, Usage: "File containing replacement patterns applied to successful guesses"},
		&cli.BoolFlag{Name: "no-color", Aliases: []string{"nc"}, Value: false, Usage: "Disable color output"},
		&cli.BoolFlag{Name: "debug", Value: false, Usage: "enable debug output"},
	}
}

func ParseGlobalOptions(c *cli.Context) (libgobuster.Options, error) {
	var opts libgobuster.Options

	opts.Wordlist = c.String("wordlist")
	if opts.Wordlist == "-" { // nolint:revive
		// STDIN
	} else if _, err := os.Stat(opts.Wordlist); os.IsNotExist(err) {
		return opts, fmt.Errorf("wordlist file %q does not exist: %w", opts.Wordlist, err)
	}

	opts.Delay = c.Duration("delay")
	opts.Threads = c.Int("threads")
	opts.WordlistOffset = c.Int("wordlist-offset")
	if opts.Wordlist == "-" && opts.WordlistOffset > 0 {
		return opts, errors.New("wordlist-offset is not supported when reading from STDIN")
	} else if opts.WordlistOffset < 0 {
		return opts, errors.New("wordlist-offset must be bigger or equal to 0")
	}

	opts.OutputFilename = c.String("output")
	opts.Quiet = c.Bool("quiet")
	opts.NoProgress = c.Bool("no-progress")
	opts.NoError = c.Bool("no-error")
	opts.PatternFile = c.String("pattern")
	if opts.PatternFile != "" {
		if _, err := os.Stat(opts.PatternFile); os.IsNotExist(err) {
			return opts, fmt.Errorf("pattern file %q does not exist: %w", opts.PatternFile, err)
		}
		patternFile, err := os.Open(opts.PatternFile)
		if err != nil {
			return opts, fmt.Errorf("could not open pattern file %q: %w", opts.PatternFile, err)
		}
		defer patternFile.Close()

		scanner := bufio.NewScanner(patternFile)
		for scanner.Scan() {
			opts.Patterns = append(opts.Patterns, scanner.Text())
		}
		if err := scanner.Err(); err != nil {
			return opts, fmt.Errorf("could not read pattern file %q: %w", opts.PatternFile, err)
		}
	}

	opts.DiscoverPatternFile = c.String("discover-pattern")
	if opts.DiscoverPatternFile != "" {
		if _, err := os.Stat(opts.PatternFile); os.IsNotExist(err) {
			return opts, fmt.Errorf("discover pattern file %q does not exist: %w", opts.DiscoverPatternFile, err)
		}
		discoverPatternFile, err := os.Open(opts.DiscoverPatternFile)
		if err != nil {
			return opts, fmt.Errorf("could not open discover pattern file %q: %w", opts.DiscoverPatternFile, err)
		}
		defer discoverPatternFile.Close()

		scanner := bufio.NewScanner(discoverPatternFile)
		for scanner.Scan() {
			opts.DiscoverPatterns = append(opts.DiscoverPatterns, scanner.Text())
		}
		if err := scanner.Err(); err != nil {
			return opts, fmt.Errorf("could not read discover pattern file %q: %w", opts.DiscoverPatternFile, err)
		}
	}

	if c.Bool("no-color") {
		color.NoColor = true
	}

	opts.Debug = c.Bool("debug")
	return opts, nil
}

func getLocalAddrFromInterface(iface string) (*net.TCPAddr, error) {
	i, err := net.InterfaceByName(iface)
	if err != nil {
		return nil, fmt.Errorf("could not get interface %s: %w", iface, err)
	}
	addrs, err := i.Addrs()
	if err != nil {
		return nil, fmt.Errorf("could not get local addrs for iface %s: %w", i.Name, err)
	}

	if len(addrs) == 0 {
		return nil, fmt.Errorf("no ip addresses on interface %s", iface)
	}

	tmp, ok := addrs[0].(*net.IPNet)
	if !ok {
		return nil, fmt.Errorf("could not get ipnet address from interface %s", iface)
	}

	tcpAddr := &net.TCPAddr{
		IP: tmp.IP,
	}
	return tcpAddr, err
}


================================================
FILE: cli/s3/s3.go
================================================
package s3

import (
	"fmt"

	internalcli "github.com/OJ/gobuster/v3/cli"
	"github.com/OJ/gobuster/v3/gobusters3"
	"github.com/OJ/gobuster/v3/libgobuster"
	"github.com/urfave/cli/v2"
)

func Command() *cli.Command {
	cmd := cli.Command{
		Name:   "s3",
		Usage:  "Uses aws bucket enumeration mode",
		Action: run,
		Flags:  getFlags(),
	}
	return &cmd
}

func getFlags() []cli.Flag {
	var flags []cli.Flag
	flags = append(flags, []cli.Flag{
		&cli.IntFlag{Name: "max-files", Aliases: []string{"m"}, Value: 5, Usage: "max files to list when listing buckets"},
		&cli.BoolFlag{Name: "show-files", Aliases: []string{"s"}, Value: true, Usage: "show files from found buckets"},
	}...)
	flags = append(flags, internalcli.GlobalOptions()...)
	flags = append(flags, internalcli.BasicHTTPOptions()...)
	return flags
}

func run(c *cli.Context) error {
	pluginOpts := gobusters3.NewOptions()

	httpOptions, err := internalcli.ParseBasicHTTPOptions(c)
	if err != nil {
		return err
	}
	pluginOpts.BasicHTTPOptions = httpOptions

	pluginOpts.MaxFilesToList = c.Int("max-files")
	pluginOpts.ShowFiles = c.Bool("show-files")

	globalOpts, err := internalcli.ParseGlobalOptions(c)
	if err != nil {
		return err
	}

	log := libgobuster.NewLogger(globalOpts.Debug)

	plugin, err := gobusters3.New(&globalOpts, pluginOpts, log)
	if err != nil {
		return fmt.Errorf("error on creating gobusters3: %w", err)
	}

	if err := internalcli.Gobuster(c.Context, &globalOpts, plugin, log); err != nil {
		log.Debugf("%#v", err)
		return fmt.Errorf("error on running gobuster: %w", err)
	}
	return nil
}


================================================
FILE: cli/tftp/tftp.go
================================================
package tftp

import (
	"fmt"
	"strings"
	"time"

	internalcli "github.com/OJ/gobuster/v3/cli"
	"github.com/OJ/gobuster/v3/gobustertftp"
	"github.com/OJ/gobuster/v3/libgobuster"
	"github.com/urfave/cli/v2"
)

func Command() *cli.Command {
	cmd := cli.Command{
		Name:   "tftp",
		Usage:  "Uses TFTP enumeration mode",
		Action: run,
		Flags:  getFlags(),
	}
	return &cmd
}

func getFlags() []cli.Flag {
	var flags []cli.Flag
	flags = append(flags, []cli.Flag{
		&cli.StringFlag{Name: "server", Aliases: []string{"s"}, Usage: "The target TFTP server", Required: true},
		&cli.DurationFlag{Name: "timeout", Aliases: []string{"to"}, Value: 1 * time.Second, Usage: "TFTP timeout"},
	}...)
	flags = append(flags, internalcli.GlobalOptions()...)
	return flags
}

func run(c *cli.Context) error {
	pluginOpts := gobustertftp.NewOptions()

	pluginOpts.Server = c.String("server")
	if !strings.Contains(pluginOpts.Server, ":") {
		pluginOpts.Server = fmt.Sprintf("%s:69", pluginOpts.Server)
	}

	pluginOpts.Timeout = c.Duration("timeout")

	globalOpts, err := internalcli.ParseGlobalOptions(c)
	if err != nil {
		return err
	}

	plugin, err := gobustertftp.New(&globalOpts, pluginOpts)
	if err != nil {
		return fmt.Errorf("error on creating gobustertftp: %w", err)
	}

	log := libgobuster.NewLogger(globalOpts.Debug)
	if err := internalcli.Gobuster(c.Context, &globalOpts, plugin, log); err != nil {
		log.Debugf("%#v", err)
		return fmt.Errorf("error on running gobuster on %s: %w", pluginOpts.Server, err)
	}
	return nil
}


================================================
FILE: cli/vhost/vhost.go
================================================
package vhost

import (
	"errors"
	"fmt"
	"strings"

	internalcli "github.com/OJ/gobuster/v3/cli"
	"github.com/OJ/gobuster/v3/gobustervhost"
	"github.com/OJ/gobuster/v3/libgobuster"
	"github.com/urfave/cli/v2"
)

func Command() *cli.Command {
	cmd := cli.Command{
		Name:   "vhost",
		Usage:  "Uses VHOST enumeration mode (you most probably want to use the IP address as the URL parameter)",
		Action: run,
		Flags:  getFlags(),
	}
	return &cmd
}

func getFlags() []cli.Flag {
	var flags []cli.Flag
	flags = append(flags, internalcli.CommonHTTPOptions()...)
	flags = append(flags, internalcli.GlobalOptions()...)
	flags = append(flags, []cli.Flag{
		&cli.BoolFlag{Name: "append-domain", Aliases: []string{"ad"}, Value: false, Usage: "Append main domain from URL to words from wordlist. Otherwise the fully qualified domains need to be specified in the wordlist."},
		&cli.StringFlag{Name: "exclude-length", Aliases: []string{"xl"}, Usage: "exclude the following content lengths. You can separate multiple lengths by comma and it also supports ranges like 203-206"},
		&cli.StringFlag{Name: "exclude-status", Aliases: []string{"xs"}, Usage: "exclude the following status codes. Can also handle ranges like 200,300-400,404.", Value: ""},
		&cli.StringFlag{Name: "domain", Aliases: []string{"do"}, Usage: "the domain to append when using an IP address as URL. If left empty and you specify a domain based URL the hostname from the URL is extracted"},
		&cli.BoolFlag{Name: "force", Value: false, Usage: "Force execution even when result is not guaranteed."},
		&cli.BoolFlag{Name: "exclude-hostname-length", Aliases: []string{"xh"}, Value: false, Usage: "Automatically adjust exclude-length based on dynamic hostname length in responses"},
	}...)

	return flags
}

func run(c *cli.Context) error {
	pluginOpts := gobustervhost.NewOptions()

	httpOptions, err := internalcli.ParseCommonHTTPOptions(c)
	if err != nil {
		return err
	}
	pluginOpts.HTTPOptions = httpOptions

	pluginOpts.AppendDomain = c.Bool("append-domain")
	pluginOpts.ExcludeLength = c.String("exclude-length")
	ret, err := libgobuster.ParseCommaSeparatedInt(pluginOpts.ExcludeLength)
	if err != nil {
		return fmt.Errorf("invalid value for exclude-length: %w", err)
	}
	pluginOpts.ExcludeLengthParsed = ret

	pluginOpts.ExcludeHostnameLength = c.Bool("exclude-hostname-length")
	if pluginOpts.ExcludeHostnameLength && pluginOpts.ExcludeLengthParsed.Length() == 0 {
		return errors.New("exclude-hostname-length requires exclude-length to be set")
	}

	pluginOpts.ExcludeStatus = c.String("exclude-status")
	ret2, err := libgobuster.ParseCommaSeparatedInt(pluginOpts.ExcludeStatus)
	if err != nil {
		return fmt.Errorf("invalid value for exclude-status: %w", err)
	}
	pluginOpts.ExcludeStatusParsed = ret2

	pluginOpts.Domain = c.String("domain")

	globalOpts, err := internalcli.ParseGlobalOptions(c)
	if err != nil {
		return err
	}

	force := c.Bool("force")
	if !force &&
		(strings.HasPrefix(pluginOpts.Proxy, "http://") || strings.HasPrefix(pluginOpts.Proxy, "https://")) &&
		strings.EqualFold(pluginOpts.URL.Scheme, "http") {
		return errors.New("VHOST mode does not work with a http proxy when using plain text http urls as golang strictly adheres to the http standard. This results in always sending the requests to the IP of the VHOST domain instead of the specified target. See https://github.com/golang/go/issues/30775 for example. You need to either disable the proxy, use a https based url or use the --force switch to continue. When using --force you may need to do some rewrites in your proxy to get the expected result")
	}

	log := libgobuster.NewLogger(globalOpts.Debug)

	plugin, err := gobustervhost.New(&globalOpts, pluginOpts, log)
	if err != nil {
		return fmt.Errorf("error on creating gobustervhost: %w", err)
	}

	if err := internalcli.Gobuster(c.Context, &globalOpts, plugin, log); err != nil {
		log.Debugf("%#v", err)
		return fmt.Errorf("error on running gobuster on %s: %w", pluginOpts.URL, err)
	}
	return nil
}


================================================
FILE: cli/vhost/vhost_test.go
================================================
package vhost

import (
	"fmt"
	"net/http"
	"net/http/httptest"
	"net/url"
	"os"
	"testing"
	"time"

	"github.com/OJ/gobuster/v3/cli"
	"github.com/OJ/gobuster/v3/gobustervhost"
	"github.com/OJ/gobuster/v3/libgobuster"
)

func httpServer(b *testing.B, content string) *httptest.Server {
	b.Helper()
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		if _, err := fmt.Fprint(w, content); err != nil {
			b.Fatalf("%v", err)
		}
	}))
	return ts
}

func BenchmarkVhostMode(b *testing.B) {
	h := httpServer(b, "test")
	defer h.Close()

	u, err := url.Parse(h.URL)
	if err != nil {
		b.Fatalf("could not parse URL %v: %v", h.URL, err)
	}
	pluginopts := gobustervhost.NewOptions()
	pluginopts.URL = u
	pluginopts.Timeout = 10 * time.Second

	wordlist, err := os.CreateTemp(b.TempDir(), "")
	if err != nil {
		b.Fatalf("could not create tempfile: %v", err)
	}
	defer os.Remove(wordlist.Name())
	for w := range 1000 {
		_, _ = fmt.Fprintf(wordlist, "%d\n", w)
	}
	if err := wordlist.Close(); err != nil {
		b.Fatalf("%v", err)
	}

	globalopts := libgobuster.Options{
		Threads:    10,
		Wordlist:   wordlist.Name(),
		NoProgress: true,
	}

	ctx := b.Context()
	oldStdout := os.Stdout
	oldStderr := os.Stderr
	defer func(out, err *os.File) { os.Stdout = out; os.Stderr = err }(oldStdout, oldStderr)
	devnull, err := os.Open(os.DevNull)
	if err != nil {
		b.Fatalf("could not get devnull %v", err)
	}
	defer devnull.Close()
	log := libgobuster.NewLogger(false)

	// Run the real benchmark
	for b.Loop() {
		os.Stdout = devnull
		os.Stderr = devnull
		plugin, err := gobustervhost.New(&globalopts, pluginopts, log)
		if err != nil {
			b.Fatalf("error on creating gobusterdir: %v", err)
		}

		if err := cli.Gobuster(ctx, &globalopts, plugin, log); err != nil {
			b.Fatalf("error on running gobuster: %v", err)
		}
		os.Stdout = oldStdout
		os.Stderr = oldStderr
	}
}


================================================
FILE: go.mod
================================================
module github.com/OJ/gobuster/v3

go 1.25

require (
	github.com/fatih/color v1.18.0
	github.com/google/uuid v1.6.0
	github.com/pin/tftp/v3 v3.1.0
	github.com/urfave/cli/v2 v2.27.7
	go.uber.org/automaxprocs v1.6.0
	golang.org/x/term v0.37.0
	software.sslmate.com/src/go-pkcs12 v0.6.0
)

require (
	github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
	github.com/google/go-cmp v0.7.0 // indirect
	github.com/mattn/go-colorable v0.1.14 // indirect
	github.com/mattn/go-isatty v0.0.20 // indirect
	github.com/russross/blackfriday/v2 v2.1.0 // indirect
	github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect
	golang.org/x/crypto v0.45.0 // indirect
	golang.org/x/mod v0.27.0 // indirect
	golang.org/x/net v0.47.0 // indirect
	golang.org/x/sync v0.16.0 // indirect
	golang.org/x/sys v0.38.0 // indirect
	golang.org/x/tools v0.36.0 // indirect
	mvdan.cc/gofumpt v0.9.0 // indirect
)

tool mvdan.cc/gofumpt


================================================
FILE: go.sum
================================================
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
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/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/pin/tftp/v3 v3.1.0 h1:rQaxd4pGwcAJnpId8zC+O2NX3B2/NscjDZQaqEjuE7c=
github.com/pin/tftp/v3 v3.1.0/go.mod h1:xwQaN4viYL019tM4i8iecm++5cGxSqen6AJEOEyEI0w=
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/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=
github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4=
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 h1:FnBeRrxr7OU4VvAzt5X7s6266i6cSVkkFPS0TuXWbIg=
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
mvdan.cc/gofumpt v0.9.0 h1:W0wNHMSvDBDIyZsm3nnGbVfgp5AknzBrGJnfLCy501w=
mvdan.cc/gofumpt v0.9.0/go.mod h1:3xYtNemnKiXaTh6R4VtlqDATFwBbdXI8lJvH/4qk7mw=
software.sslmate.com/src/go-pkcs12 v0.6.0 h1:f3sQittAeF+pao32Vb+mkli+ZyT+VwKaD014qFGq6oU=
software.sslmate.com/src/go-pkcs12 v0.6.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=


================================================
FILE: gobusterdir/gobusterdir.go
================================================
package gobusterdir

import (
	"bufio"
	"bytes"
	"context"
	"errors"
	"fmt"
	"io"
	"net/http"
	"os"
	"strings"
	"syscall"
	"text/tabwriter"
	"unicode/utf8"

	"github.com/OJ/gobuster/v3/libgobuster"
	"github.com/google/uuid"
)

// nolint:gochecknoglobals
var (
	backupExtensions    = []string{"~", ".bak", ".bak2", ".old", ".1"}
	backupDotExtensions = []string{".swp"}
)

// WildcardError is returned if a wildcard response is found
type WildcardError struct {
	url        string
	location   string
	statusCode int
	length     int64
}

// Error is the implementation of the error interface
func (e *WildcardError) Error() string {
	var addInfo string
	if e.location != "" {
		addInfo = fmt.Sprintf("%s => %d (redirect to %s) (Length: %d)", e.url, e.statusCode, e.location, e.length)
	} else {
		addInfo = fmt.Sprintf("%s => %d (Length: %d)", e.url, e.statusCode, e.length)
	}
	return fmt.Sprintf("the server returns a status code that matches the provided options for non existing urls. %s. Please exclude the response length or the status code or set the wildcard option.", addInfo)
}

// GobusterDir is the main type to implement the interface
type GobusterDir struct {
	options    *OptionsDir
	globalopts *libgobuster.Options
	http       *libgobuster.HTTPClient
}

// New creates a new initialized GobusterDir
func New(globalopts *libgobuster.Options, opts *OptionsDir, logger *libgobuster.Logger) (*GobusterDir, error) {
	if globalopts == nil {
		return nil, errors.New("please provide valid global options")
	}

	if opts == nil {
		return nil, errors.New("please provide valid plugin options")
	}

	g := GobusterDir{
		options:    opts,
		globalopts: globalopts,
	}

	basicOptions := libgobuster.BasicHTTPOptions{
		Proxy:            opts.Proxy,
		Timeout:          opts.Timeout,
		UserAgent:        opts.UserAgent,
		NoTLSValidation:  opts.NoTLSValidation,
		RetryOnTimeout:   opts.RetryOnTimeout,
		RetryAttempts:    opts.RetryAttempts,
		TLSCertificate:   opts.TLSCertificate,
		TLSRenegotiation: opts.TLSRenegotiation,
		LocalAddr:        opts.LocalAddr,
	}

	httpOpts := libgobuster.HTTPOptions{
		BasicHTTPOptions:      basicOptions,
		FollowRedirect:        opts.FollowRedirect,
		Username:              opts.Username,
		Password:              opts.Password,
		Headers:               opts.Headers,
		NoCanonicalizeHeaders: opts.NoCanonicalizeHeaders,
		Cookies:               opts.Cookies,
		Method:                opts.Method,
	}

	h, err := libgobuster.NewHTTPClient(&httpOpts, logger)
	if err != nil {
		return nil, err
	}
	g.http = h

	return &g, nil
}

// Name should return the name of the plugin
func (d *GobusterDir) Name() string {
	return "directory enumeration"
}

// PreRun is the pre run implementation of gobusterdir
func (d *GobusterDir) PreRun(ctx context.Context, pr *libgobuster.Progress) error {
	// add trailing slash
	if !strings.HasSuffix(d.options.URL.Path, "/") {
		d.options.URL.Path = fmt.Sprintf("%s/", d.options.URL.Path)
	}

	_, _, _, _, err := d.http.Request(ctx, *d.options.URL, libgobuster.RequestOptions{})
	if err != nil {
		var retErr error
		switch {
		case errors.Is(err, io.EOF):
			retErr = libgobuster.ErrEOF
		case os.IsTimeout(err):
			retErr = libgobuster.ErrTimeout
		case errors.Is(err, syscall.ECONNREFUSED):
			retErr = libgobuster.ErrConnectionRefused
		default:
			retErr = fmt.Errorf("unable to connect to %s: %w", d.options.URL, err)
		}
		if !d.options.Force {
			return retErr
		}
		// if force is set, we continue even if the preRun fails
		pr.MessageChan <- libgobuster.Message{
			Level:   libgobuster.LevelWarn,
			Message: fmt.Sprintf("PreRun failed with error: %s. Continuing because force is set.", retErr),
		}
	}

	guid := uuid.New()
	url := *d.options.URL
	url.Path = fmt.Sprintf("%s%s", url.Path, guid)
	if d.options.UseSlash {
		url.Path = fmt.Sprintf("%s/", url.Path)
	}

	wildcardResp, wildcardLength, wildcardHeader, _, err := d.http.Request(ctx, url, libgobuster.RequestOptions{})
	if err != nil {
		var retErr error
		switch {
		case errors.Is(err, io.EOF):
			retErr = libgobuster.ErrEOF
		case os.IsTimeout(err):
			retErr = libgobuster.ErrTimeout
		case errors.Is(err, syscall.ECONNREFUSED):
			retErr = libgobuster.ErrConnectionRefused
		default:
			retErr = fmt.Errorf("unable to connect to %s: %w", url.String(), err)
		}
		if !d.options.Force {
			return retErr
		}
		// if force is set, we continue even if the preRun fails
		pr.MessageChan <- libgobuster.Message{
			Level:   libgobuster.LevelWarn,
			Message: fmt.Sprintf("PreRun failed with error: %s. Continuing because force is set.", retErr),
		}
	}

	if d.options.ExcludeLengthParsed.Contains(int(wildcardLength)) {
		// we are done and ignore the request as the length is excluded
		return nil
	}

	switch {
	case d.options.StatusCodesBlacklistParsed.Length() > 0:
		if !d.options.StatusCodesBlacklistParsed.Contains(wildcardResp) {
			return &WildcardError{url: url.String(), statusCode: wildcardResp, length: wildcardLength, location: wildcardHeader.Get("Location")}
		}
	case d.options.StatusCodesParsed.Length() > 0:
		if d.options.StatusCodesParsed.Contains(wildcardResp) {
			return &WildcardError{url: url.String(), statusCode: wildcardResp, length: wildcardLength, location: wildcardHeader.Get("Location")}
		}
	default:
		return errors.New("StatusCodes and StatusCodesBlacklist are both not set which should not happen")
	}

	return nil
}

func (d *GobusterDir) AdditionalSuccessWords(word string) []string {
	if d.options.DiscoverBackup {
		ret := make([]string, len(backupExtensions)+len(backupDotExtensions))
		i := 0
		for _, b := range backupExtensions {
			ret[i] = fmt.Sprintf("%s%s", word, b)
			i++
		}
		for _, b := range backupDotExtensions {
			ret[i] = fmt.Sprintf(".%s%s", word, b)
			i++
		}

		return ret
	}

	return []string{}
}

func (d *GobusterDir) AdditionalWordsLen() int {
	return len(d.options.ExtensionsParsed.Set)
}

func (d *GobusterDir) AdditionalWords(word string) []string {
	words := make([]string, 0, d.AdditionalWordsLen())
	// build list of urls to check
	//   1: No extension
	//   2: With extension
	for ext := range d.options.ExtensionsParsed.Set {
		filename := fmt.Sprintf("%s.%s", word, ext)
		words = append(words, filename)
	}
	return words
}

// ProcessWord is the process implementation of gobusterdir
func (d *GobusterDir) ProcessWord(ctx context.Context, word string, progress *libgobuster.Progress) (libgobuster.Result, error) {
	suffix := ""
	if d.options.UseSlash {
		suffix = "/"
	}
	entity := fmt.Sprintf("%s%s", word, suffix)

	// prevent double slashes by removing leading /
	if strings.HasPrefix(entity, "/") {
		// get size of first rune and trim it
		_, i := utf8.DecodeRuneInString(entity)
		entity = entity[i:]
	}
	url := *d.options.URL
	url.Path = fmt.Sprintf("%s%s", url.Path, entity)

	// add some debug output
	if d.globalopts.Debug {
		progress.MessageChan <- libgobuster.Message{
			Level:   libgobuster.LevelDebug,
			Message: fmt.Sprintf("trying %s", entity),
		}
	}

	tries := 1
	if d.options.RetryOnTimeout && d.options.RetryAttempts > 0 {
		// add it so it will be the overall max requests
		tries += d.options.RetryAttempts
	}

	var statusCode int
	var size int64
	var header http.Header
	for i := 1; i <= tries; i++ {
		var err error
		statusCode, size, header, _, err = d.http.Request(ctx, url, libgobuster.RequestOptions{})
		if err != nil {
			// check if it's a timeout and if we should try again and try again
			// otherwise the timeout error is raised
			if os.IsTimeout(err) && i != tries {
				continue
			} else if strings.Contains(err.Error(), "invalid control character in URL") {
				// put error in error chan, so it's printed out and ignore it
				// so gobuster will not quit
				progress.ErrorChan <- err
				continue
			}

			switch {
			case errors.Is(err, io.EOF):
				return nil, libgobuster.ErrEOF
			case os.IsTimeout(err):
				return nil, libgobuster.ErrTimeout
			case errors.Is(err, syscall.ECONNREFUSED):
				return nil, libgobuster.ErrConnectionRefused
			}
			return nil, err
		}
		break
	}

	if statusCode != 0 {
		resultStatus := false

		switch {
		case d.options.StatusCodesBlacklistParsed.Length() > 0:
			if !d.options.StatusCodesBlacklistParsed.Contains(statusCode) {
				resultStatus = true
			}
		case d.options.StatusCodesParsed.Length() > 0:
			if d.options.StatusCodesParsed.Contains(statusCode) {
				resultStatus = true
			}
		default:
			return nil, errors.New("StatusCodes and StatusCodesBlacklist are both not set which should not happen")
		}

		if resultStatus && !d.options.ExcludeLengthParsed.Contains(int(size)) {
			path := fmt.Sprintf("%-20s", entity)
			if d.options.Expanded {
				// expanded mode should show the full url
				path = url.String()
			}

			r := Result{
				Path:       path,
				Header:     header,
				StatusCode: -1,
				Size:       -1,
			}
			if !d.options.NoStatus {
				r.StatusCode = statusCode
			}
			if !d.options.HideLength {
				r.Size = size
			}
			return r, nil
		}
	}

	return nil, nil // nolint:nilnil
}

// GetConfigString returns the string representation of the current config
func (d *GobusterDir) GetConfigString() (string, error) {
	var buffer bytes.Buffer
	bw := bufio.NewWriter(&buffer)
	tw := tabwriter.NewWriter(bw, 0, 5, 3, ' ', 0)
	o := d.options
	if _, err := fmt.Fprintf(tw, "[+] Url:\t%s\n", o.URL); err != nil {
		return "", err
	}

	if _, err := fmt.Fprintf(tw, "[+] Method:\t%s\n", o.Method); err != nil {
		return "", err
	}

	if _, err := fmt.Fprintf(tw, "[+] Threads:\t%d\n", d.globalopts.Threads); err != nil {
		return "", err
	}

	if d.globalopts.Delay > 0 {
		if _, err := fmt.Fprintf(tw, "[+] Delay:\t%s\n", d.globalopts.Delay); err != nil {
			return "", err
		}
	}

	wordlist := "stdin (pipe)"
	if d.globalopts.Wordlist != "-" {
		wordlist = d.globalopts.Wordlist
	}
	if _, err := fmt.Fprintf(tw, "[+] Wordlist:\t%s\n", wordlist); err != nil {
		return "", err
	}

	if d.globalopts.PatternFile != "" {
		if _, err := fmt.Fprintf(tw, "[+] Patterns:\t%s (%d entries)\n", d.globalopts.PatternFile, len(d.globalopts.Patterns)); err != nil {
			return "", err
		}
	}

	if o.StatusCodesBlacklistParsed.Length() > 0 {
		if _, err := fmt.Fprintf(tw, "[+] Negative Status codes:\t%s\n", o.StatusCodesBlacklistParsed.Stringify()); err != nil {
			return "", err
		}
	} else if o.StatusCodesParsed.Length() > 0 {
		if _, err := fmt.Fprintf(tw, "[+] Status codes:\t%s\n", o.StatusCodesParsed.Stringify()); err != nil {
			return "", err
		}
	}

	if len(o.ExcludeLength) > 0 {
		if _, err := fmt.Fprintf(tw, "[+] Exclude Length:\t%s\n", d.options.ExcludeLengthParsed.Stringify()); err != nil {
			return "", err
		}
	}

	if o.Proxy != "" {
		if _, err := fmt.Fprintf(tw, "[+] Proxy:\t%s\n", o.Proxy); err != nil {
			return "", err
		}
	}

	if o.Cookies != "" {
		if _, err := fmt.Fprintf(tw, "[+] Cookies:\t%s\n", o.Cookies); err != nil {
			return "", err
		}
	}

	if o.UserAgent != "" {
		if _, err := fmt.Fprintf(tw, "[+] User Agent:\t%s\n", o.UserAgent); err != nil {
			return "", err
		}
	}

	if o.LocalAddr != nil {
		if _, err := fmt.Fprintf(tw, "[+] Local IP:\t%s\n", o.LocalAddr); err != nil {
			return "", err
		}
	}

	if o.HideLength {
		if _, err := fmt.Fprintf(tw, "[+] Show length:\tfalse\n"); err != nil {
			return "", err
		}
	}

	if o.Username != "" {
		if _, err := fmt.Fprintf(tw, "[+] Auth User:\t%s\n", o.Username); err != nil {
			return "", err
		}
	}

	if o.Extensions != "" || o.ExtensionsFile != "" {
		if _, err := fmt.Fprintf(tw, "[+] Extensions:\t%s\n", o.ExtensionsParsed.Stringify()); err != nil {
			return "", err
		}
	}

	if o.ExtensionsFile != "" {
		if _, err := fmt.Fprintf(tw, "[+] Extensions file:\t%s\n", o.ExtensionsFile); err != nil {
			return "", err
		}
	}

	if o.UseSlash {
		if _, err := fmt.Fprintf(tw, "[+] Add Slash:\ttrue\n"); err != nil {
			return "", err
		}
	}

	if o.FollowRedirect {
		if _, err := fmt.Fprintf(tw, "[+] Follow Redirect:\ttrue\n"); err != nil {
			return "", err
		}
	}

	if o.Expanded {
		if _, err := fmt.Fprintf(tw, "[+] Expanded:\ttrue\n"); err != nil {
			return "", err
		}
	}

	if o.NoStatus {
		if _, err := fmt.Fprintf(tw, "[+] No status:\ttrue\n"); err != nil {
			return "", err
		}
	}

	if _, err := fmt.Fprintf(tw, "[+] Timeout:\t%s\n", o.Timeout.String()); err != nil {
		return "", err
	}

	if err := tw.Flush(); err != nil {
		return "", fmt.Errorf("error on tostring: %w", err)
	}

	if err := bw.Flush(); err != nil {
		return "", fmt.Errorf("error on tostring: %w", err)
	}

	return strings.TrimSpace(buffer.String()), nil
}


================================================
FILE: gobusterdir/gobusterdir_test.go
================================================
package gobusterdir

import (
	"testing"

	"github.com/OJ/gobuster/v3/libgobuster"
)

func TestAdditionalWordsLen(t *testing.T) {
	t.Parallel()

	tt := []struct {
		testName   string
		extensions map[string]bool
	}{
		{"No extensions", map[string]bool{}},
		{"Some extensions", map[string]bool{"htm": true, "html": true, "php": true}},
	}

	globalOpts := libgobuster.Options{}
	for _, x := range tt {
		t.Run(x.testName, func(t *testing.T) {
			t.Parallel()

			opts := OptionsDir{}
			opts.ExtensionsParsed.Set = x.extensions

			d, err := New(&globalOpts, &opts, nil)
			if err != nil {
				t.Fatalf("got error creating gobusterdir: %v", err)
			}

			calculatedLen := d.AdditionalWordsLen()
			wordsLen := len(d.AdditionalWords("dummy"))

			if calculatedLen != wordsLen {
				t.Fatalf("Mismatched additional words length: %d got %d generated words %v", calculatedLen, wordsLen, d.AdditionalWords("dummy"))
			}
		})
	}
}


================================================
FILE: gobusterdir/options.go
================================================
package gobusterdir

import (
	"github.com/OJ/gobuster/v3/libgobuster"
)

// OptionsDir is the struct to hold all options for this plugin
type OptionsDir struct {
	libgobuster.HTTPOptions
	Extensions                 string
	ExtensionsParsed           libgobuster.Set[string]
	ExtensionsFile             string
	StatusCodes                string
	StatusCodesParsed          libgobuster.Set[int]
	StatusCodesBlacklist       string
	StatusCodesBlacklistParsed libgobuster.Set[int]
	UseSlash                   bool
	HideLength                 bool
	Expanded                   bool
	NoStatus                   bool
	DiscoverBackup             bool
	ExcludeLength              string
	ExcludeLengthParsed        libgobuster.Set[int]
	Force                      bool
}

// NewOptions returns a new initialized OptionsDir
func NewOptions() *OptionsDir {
	return &OptionsDir{
		StatusCodesParsed:          libgobuster.NewSet[int](),
		StatusCodesBlacklistParsed: libgobuster.NewSet[int](),
		ExtensionsParsed:           libgobuster.NewSet[string](),
		ExcludeLengthParsed:        libgobuster.NewSet[int](),
	}
}


================================================
FILE: gobusterdir/options_test.go
================================================
package gobusterdir

import "testing"

func TestNewOptions(t *testing.T) {
	t.Parallel()

	o := NewOptions()
	if o.StatusCodesParsed.Set == nil {
		t.Fatal("StatusCodesParsed not initialized")
	}

	if o.ExtensionsParsed.Set == nil {
		t.Fatal("ExtensionsParsed not initialized")
	}
}


================================================
FILE: gobusterdir/result.go
================================================
package gobusterdir

import (
	"bytes"
	"fmt"
	"net/http"

	"github.com/fatih/color"
)

var (
	white  = color.New(color.FgWhite).FprintfFunc()
	yellow = color.New(color.FgYellow).FprintfFunc()
	green  = color.New(color.FgGreen).FprintfFunc()
	blue   = color.New(color.FgBlue).FprintfFunc()
	red    = color.New(color.FgRed).FprintfFunc()
	cyan   = color.New(color.FgCyan).FprintfFunc()
)

// Result represents a single result
type Result struct {
	Path       string
	Header     http.Header
	StatusCode int
	Size       int64
}

// ResultToString converts the Result to its textual representation
func (r Result) ResultToString() (string, error) {
	buf := &bytes.Buffer{}
	if _, err := buf.WriteString(r.Path); err != nil {
		return "", err
	}

	if r.StatusCode >= 0 {
		textColor := white
		switch {
		case r.StatusCode == http.StatusOK:
			textColor = green
		case r.StatusCode >= 300 && r.StatusCode < 400:
			textColor = cyan
		case r.StatusCode >= 400 && r.StatusCode < 500:
			textColor = yellow
		case r.StatusCode >= 500 && r.StatusCode < 600:
			textColor = red
		}

		textColor(buf, " (Status: %d)", r.StatusCode)
	}

	if r.Size >= 0 {
		if _, err := fmt.Fprintf(buf, " [Size: %d]", r.Size); err != nil {
			return "", err
		}
	}

	location := r.Header.Get("Location")
	if location != "" {
		blue(buf, " [--> %s]", location)
	}

	if _, err := fmt.Fprintf(buf, "\n"); err != nil {
		return "", err
	}

	s := buf.String()

	return s, nil
}


================================================
FILE: gobusterdns/gobusterdns.go
================================================
package gobusterdns

import (
	"bufio"
	"bytes"
	"context"
	"errors"
	"fmt"
	"net"
	"net/netip"
	"strings"
	"text/tabwriter"

	"github.com/OJ/gobuster/v3/libgobuster"
	"github.com/google/uuid"
)

// WildcardError is returned if a wildcard response is found
type WildcardError struct {
	wildcardIps libgobuster.Set[netip.Addr]
}

// Error is the implementation of the error interface
func (e *WildcardError) Error() string {
	return fmt.Sprintf("the DNS Server returned the same IP for every domain. IP address(es) returned: %s", e.wildcardIps.Stringify())
}

// GobusterDNS is the main type to implement the interface
type GobusterDNS struct {
	resolver    *net.Resolver
	globalopts  *libgobuster.Options
	options     *OptionsDNS
	isWildcard  bool
	wildcardIps libgobuster.Set[netip.Addr]
}

func newCustomDialer(server string, protocol string) func(ctx context.Context, network, address string) (net.Conn, error) {
	return func(ctx context.Context, _, _ string) (net.Conn, error) {
		d := net.Dialer{}
		if !strings.Contains(server, ":") {
			server = fmt.Sprintf("%s:53", server)
		}
		return d.DialContext(ctx, protocol, server)
	}
}

// New creates a new initialized GobusterDNS
func New(globalopts *libgobuster.Options, opts *OptionsDNS) (*GobusterDNS, error) {
	if globalopts == nil {
		return nil, errors.New("please provide valid global options")
	}

	if opts == nil {
		return nil, errors.New("please provide valid plugin options")
	}

	resolver := net.DefaultResolver
	if opts.Resolver != "" {
		resolver = &net.Resolver{
			PreferGo: true,
			Dial:     newCustomDialer(opts.Resolver, opts.Protocol),
		}
	}

	g := GobusterDNS{
		options:     opts,
		globalopts:  globalopts,
		wildcardIps: libgobuster.NewSet[netip.Addr](),
		resolver:    resolver,
	}
	return &g, nil
}

// Name should return the name of the plugin
func (d *GobusterDNS) Name() string {
	return "DNS enumeration"
}

// PreRun is the pre run implementation of gobusterdns
func (d *GobusterDNS) PreRun(ctx context.Context, progress *libgobuster.Progress) error {
	// Resolve a subdomain that probably shouldn't exist
	guid := uuid.New()
	wildcardIps, err := d.dnsLookup(ctx, fmt.Sprintf("%s.%s", guid, d.options.Domain))
	if err == nil {
		d.isWildcard = true
		d.wildcardIps.AddRange(wildcardIps)
		if !d.options.WildcardForced {
			return &WildcardError{wildcardIps: d.wildcardIps}
		}
	}

	if !d.globalopts.Quiet {
		// Provide a warning if the base domain doesn't resolve (in case of typo)
		_, err = d.dnsLookup(ctx, d.options.Domain)
		if err != nil {
			// Not an error, just a warning. Eg. `yp.to` doesn't resolve, but `cr.yp.to` does!
			progress.MessageChan <- libgobuster.Message{
				Level:   libgobuster.LevelInfo,
				Message: fmt.Sprintf("[-] Unable to validate base domain: %s (%v)", d.options.Domain, err),
			}
			progress.MessageChan <- libgobuster.Message{
				Level:   libgobuster.LevelDebug,
				Message: fmt.Sprintf("%#v", err),
			}
		}
	}

	return nil
}

// ProcessWord is the process implementation of gobusterdns
func (d *GobusterDNS) ProcessWord(ctx context.Context, word string, progress *libgobuster.Progress) (libgobuster.Result, error) {
	subdomain := fmt.Sprintf("%s.%s", word, d.options.Domain)
	if !d.options.NoFQDN && !strings.HasSuffix(subdomain, ".") {
		// add a . to indicate this is the full domain, and we do not want to traverse the search domains on the system
		subdomain = fmt.Sprintf("%s.", subdomain)
	}

	// add some debug output
	if d.globalopts.Debug {
		progress.MessageChan <- libgobuster.Message{
			Level:   libgobuster.LevelDebug,
			Message: fmt.Sprintf("trying subdomain %s", subdomain),
		}
	}

	ips, err := d.dnsLookup(ctx, subdomain)
	if err != nil {
		var wErr *net.DNSError
		if errors.As(err, &wErr) && wErr.IsNotFound {
			// host not found is the expected error here
			return nil, nil // nolint:nilnil
		}
		return nil, err
	}

	if !d.isWildcard || !d.wildcardIps.ContainsAny(ips) {
		result := Result{
			Subdomain: strings.TrimSuffix(subdomain, "."),
		}

		result.IPs = ips
		if d.options.CheckCNAME {
			cname, err := d.dnsLookupCname(ctx, subdomain)
			if err == nil {
				result.CNAME = cname
			} else {
				var wErr *net.DNSError
				if !errors.As(err, &wErr) && !wErr.IsNotFound {
					// host not found is the expected error here, send all other errors to the error channel
					progress.ErrorChan <- err
				}
			}
		}
		return result, nil
	}
	return nil, nil // nolint:nilnil
}

func (d *GobusterDNS) AdditionalWordsLen() int {
	return 0
}

func (d *GobusterDNS) AdditionalWords(_ string) []string {
	return []string{}
}

func (d *GobusterDNS) AdditionalSuccessWords(_ string) []string {
	return []string{}
}

// GetConfigString returns the string representation of the current config
func (d *GobusterDNS) GetConfigString() (string, error) {
	var buffer bytes.Buffer
	bw := bufio.NewWriter(&buffer)
	tw := tabwriter.NewWriter(bw, 0, 5, 3, ' ', 0)
	o := d.options

	if _, err := fmt.Fprintf(tw, "[+] Domain:\t%s\n", o.Domain); err != nil {
		return "", err
	}

	if _, err := fmt.Fprintf(tw, "[+] Threads:\t%d\n", d.globalopts.Threads); err != nil {
		return "", err
	}

	if d.globalopts.Delay > 0 {
		if _, err := fmt.Fprintf(tw, "[+] Delay:\t%s\n", d.globalopts.Delay); err != nil {
			return "", err
		}
	}

	if o.Resolver != "" {
		if _, err := fmt.Fprintf(tw, "[+] Resolver:\t%s\n", o.Resolver); err != nil {
			return "", err
		}
	}

	if o.CheckCNAME {
		if _, err := fmt.Fprintf(tw, "[+] Check CNAME:\ttrue\n"); err != nil {
			return "", err
		}
	}

	if o.WildcardForced {
		if _, err := fmt.Fprintf(tw, "[+] Wildcard forced:\ttrue\n"); err != nil {
			return "", err
		}
	}

	if _, err := fmt.Fprintf(tw, "[+] Timeout:\t%s\n", o.Timeout.String()); err != nil {
		return "", err
	}

	wordlist := "stdin (pipe)"
	if d.globalopts.Wordlist != "-" {
		wordlist = d.globalopts.Wordlist
	}
	if _, err := fmt.Fprintf(tw, "[+] Wordlist:\t%s\n", wordlist); err != nil {
		return "", err
	}

	if d.globalopts.PatternFile != "" {
		if _, err := fmt.Fprintf(tw, "[+] Patterns:\t%s (%d entries)\n", d.globalopts.PatternFile, len(d.globalopts.Patterns)); err != nil {
			return "", err
		}
	}

	if err := tw.Flush(); err != nil {
		return "", fmt.Errorf("error on tostring: %w", err)
	}

	if err := bw.Flush(); err != nil {
		return "", fmt.Errorf("error on tostring: %w", err)
	}

	return strings.TrimSpace(buffer.String()), nil
}

func (d *GobusterDNS) dnsLookup(ctx context.Context, domain string) ([]netip.Addr, error) {
	ctx2, cancel := context.WithTimeout(ctx, d.options.Timeout)
	defer cancel()
	return d.resolver.LookupNetIP(ctx2, "ip", domain)
}

func (d *GobusterDNS) dnsLookupCname(ctx context.Context, domain string) (string, error) {
	ctx2, cancel := context.WithTimeout(ctx, d.options.Timeout)
	defer cancel()
	return d.resolver.LookupCNAME(ctx2, domain)
}


================================================
FILE: gobusterdns/options.go
================================================
package gobusterdns

import (
	"time"
)

// OptionsDNS holds all options for the dns plugin
type OptionsDNS struct {
	Domain         string
	CheckCNAME     bool
	WildcardForced bool
	Resolver       string
	Protocol       string
	NoFQDN         bool
	Timeout        time.Duration
}

// NewOptions returns a new initialized OptionsDNS
func NewOptions() *OptionsDNS {
	return &OptionsDNS{}
}


================================================
FILE: gobusterdns/result.go
================================================
package gobusterdns

import (
	"bytes"
	"fmt"
	"net/netip"
	"strings"

	"github.com/fatih/color"
)

var green = color.New(color.FgGreen).FprintfFunc()

// Result represents a single result
type Result struct {
	Subdomain string
	IPs       []netip.Addr
	CNAME     string
}

// ResultToString converts the Result to its textual representation
func (r Result) ResultToString() (string, error) {
	buf := &bytes.Buffer{}

	if _, err := fmt.Fprintf(buf, "%s", r.Subdomain); err != nil {
		return "", err
	}

	if len(r.IPs) > 0 {
		ips := make([]string, len(r.IPs))
		for i := range r.IPs {
			ips[i] = r.IPs[i].String()
		}
		green(buf, " %s", strings.Join(ips, ","))
	}

	if r.CNAME != "" {
		green(buf, " CNAME: %s", r.CNAME)
	}

	if _, err := fmt.Fprintf(buf, "\n"); err != nil {
		return "", err
	}

	s := buf.String()
	return s, nil
}


================================================
FILE: gobusterfuzz/gobusterfuzz.go
================================================
package gobusterfuzz

import (
	"bufio"
	"bytes"
	"context"
	"errors"
	"fmt"
	"io"
	"net/http"
	"os"
	"strings"
	"syscall"
	"text/tabwriter"

	"github.com/OJ/gobuster/v3/libgobuster"
)

const FuzzKeyword = "FUZZ"

// WildcardError is returned if a wildcard response is found
type WildcardError struct {
	url        string
	statusCode int
}

// Error is the implementation of the error interface
func (e *WildcardError) Error() string {
	return fmt.Sprintf("the server returns a status code that matches the provided options for non existing urls. %s => %d. Please exclude the response length or the status code or set the wildcard option.", e.url, e.statusCode)
}

// GobusterFuzz is the main type to implement the interface
type GobusterFuzz struct {
	options    *OptionsFuzz
	globalopts *libgobuster.Options
	http       *libgobuster.HTTPClient
}

// New creates a new initialized GobusterFuzz
func New(globalopts *libgobuster.Options, opts *OptionsFuzz, logger *libgobuster.Logger) (*GobusterFuzz, error) {
	if globalopts == nil {
		return nil, errors.New("please provide valid global options")
	}

	if opts == nil {
		return nil, errors.New("please provide valid plugin options")
	}

	g := GobusterFuzz{
		options:    opts,
		globalopts: globalopts,
	}

	basicOptions := libgobuster.BasicHTTPOptions{
		Proxy:           opts.Proxy,
		Timeout:         opts.Timeout,
		UserAgent:       opts.UserAgent,
		NoTLSValidation: opts.NoTLSValidation,
		RetryOnTimeout:  opts.RetryOnTimeout,
		RetryAttempts:   opts.RetryAttempts,
		TLSCertificate:  opts.TLSCertificate,
	}

	httpOpts := libgobuster.HTTPOptions{
		BasicHTTPOptions:      basicOptions,
		FollowRedirect:        opts.FollowRedirect,
		Username:              opts.Username,
		Password:              opts.Password,
		Headers:               opts.Headers,
		NoCanonicalizeHeaders: opts.NoCanonicalizeHeaders,
		Cookies:               opts.Cookies,
		Method:                opts.Method,
	}

	h, err := libgobuster.NewHTTPClient(&httpOpts, logger)
	if err != nil {
		return nil, err
	}
	g.http = h
	return &g, nil
}

// Name should return the name of the plugin
func (d *GobusterFuzz) Name() string {
	return "fuzzing"
}

// PreRun is the pre run implementation of gobusterfuzz
func (d *GobusterFuzz) PreRun(_ context.Context, _ *libgobuster.Progress) error {
	return nil
}

// ProcessWord is the process implementation of gobusterfuzz
func (d *GobusterFuzz) ProcessWord(ctx context.Context, word string, progress *libgobuster.Progress) (libgobuster.Result, error) {
	url := *d.options.URL
	url.Fragment = strings.ReplaceAll(url.Fragment, FuzzKeyword, word)
	url.Host = strings.ReplaceAll(url.Host, FuzzKeyword, word)
	url.Path = strings.ReplaceAll(url.Path, FuzzKeyword, word)
	url.Scheme = strings.ReplaceAll(url.Scheme, FuzzKeyword, word)

	query := url.Query()
	for key, value := range query {
		query.Del(key)
		key = strings.ReplaceAll(key, FuzzKeyword, word)
		for _, v := range value {
			// replace the FuzzKeyword in the value
			query.Add(key, strings.ReplaceAll(v, FuzzKeyword, word))
		}
	}
	url.RawQuery = query.Encode()

	requestOptions := libgobuster.RequestOptions{}

	if len(d.options.Headers) > 0 {
		requestOptions.ModifiedHeaders = make([]libgobuster.HTTPHeader, len(d.options.Headers))
		for i := range d.options.Headers {
			// Host header can't be set via Headers, needs to be a separate field
			if http.CanonicalHeaderKey(d.options.Headers[i].Name) == "Host" {
				requestOptions.Host = strings.ReplaceAll(d.options.Headers[i].Value, FuzzKeyword, word)
				continue
			}

			requestOptions.ModifiedHeaders[i] = libgobuster.HTTPHeader{
				Name:  strings.ReplaceAll(d.options.Headers[i].Name, FuzzKeyword, word),
				Value: strings.ReplaceAll(d.options.Headers[i].Value, FuzzKeyword, word),
			}
		}
	}

	if d.options.RequestBody != "" {
		data := strings.ReplaceAll(d.options.RequestBody, FuzzKeyword, word)
		buffer := strings.NewReader(data)
		requestOptions.Body = buffer
	}

	// fuzzing of basic auth
	if strings.Contains(d.options.Username, FuzzKeyword) || strings.Contains(d.options.Password, FuzzKeyword) {
		requestOptions.UpdatedBasicAuthUsername = strings.ReplaceAll(d.options.Username, FuzzKeyword, word)
		requestOptions.UpdatedBasicAuthPassword = strings.ReplaceAll(d.options.Password, FuzzKeyword, word)
	}

	// add some debug output
	if d.globalopts.Debug {
		progress.MessageChan <- libgobuster.Message{
			Level:   libgobuster.LevelDebug,
			Message: fmt.Sprintf("trying word %s", word),
		}
	}

	tries := 1
	if d.options.RetryOnTimeout && d.options.RetryAttempts > 0 {
		// add it so it will be the overall max requests
		tries += d.options.RetryAttempts
	}

	var statusCode int
	var size int64
	var responseHeaders http.Header
	for i := 1; i <= tries; i++ {
		var err error
		statusCode, size, responseHeaders, _, err = d.http.Request(ctx, url, requestOptions)
		if err != nil {
			// check if it's a timeout and if we should try again and try again
			// otherwise the timeout error is raised
			switch {
			case os.IsTimeout(err) && i != tries:
				continue
			case strings.Contains(err.Error(), "invalid control character in URL"):
				// put error in error chan, so it's printed out and ignore it
				// so gobuster will not quit
				progress.ErrorChan <- err
				continue
			default:
				switch {
				case errors.Is(err, io.EOF):
					return nil, libgobuster.ErrEOF
				case os.IsTimeout(err):
					return nil, libgobuster.ErrTimeout
				case errors.Is(err, syscall.ECONNREFUSED):
					return nil, libgobuster.ErrConnectionRefused
				}
				return nil, err
			}
		}
		break
	}

	if statusCode != 0 {
		resultStatus := true

		if d.options.ExcludeLengthParsed.Contains(int(size)) {
			resultStatus = false
		}

		if d.options.ExcludedStatusCodesParsed.Length() > 0 {
			if d.options.ExcludedStatusCodesParsed.Contains(statusCode) {
				resultStatus = false
			}
		}

		if resultStatus {
			r := Result{
				Path:       url.String(),
				StatusCode: statusCode,
				Size:       size,
				Word:       word,
				Header:     responseHeaders,
			}
			return r, nil
		}
	}
	return nil, nil // nolint:nilnil
}

func (d *GobusterFuzz) AdditionalWordsLen() int {
	return 0
}

func (d *GobusterFuzz) AdditionalWords(_ string) []string {
	return []string{}
}

func (d *GobusterFuzz) AdditionalSuccessWords(_ string) []string {
	return []string{}
}

// GetConfigString returns the string representation of the current config
func (d *GobusterFuzz) GetConfigString() (string, error) {
	var buffer bytes.Buffer
	bw := bufio.NewWriter(&buffer)
	tw := tabwriter.NewWriter(bw, 0, 5, 3, ' ', 0)
	o := d.options
	if _, err := fmt.Fprintf(tw, "[+] Url:\t%s\n", o.URL); err != nil {
		return "", err
	}

	if _, err := fmt.Fprintf(tw, "[+] Method:\t%s\n", o.Method); err != nil {
		return "", err
	}

	if _, err := fmt.Fprintf(tw, "[+] Threads:\t%d\n", d.globalopts.Threads); err != nil {
		return "", err
	}

	if d.globalopts.Delay > 0 {
		if _, err := fmt.Fprintf(tw, "[+] Delay:\t%s\n", d.globalopts.Delay); err != nil {
			return "", err
		}
	}

	wordlist := "stdin (pipe)"
	if d.globalopts.Wordlist != "-" {
		wordlist = d.globalopts.Wordlist
	}
	if _, err := fmt.Fprintf(tw, "[+] Wordlist:\t%s\n", wordlist); err != nil {
		return "", err
	}

	if d.globalopts.PatternFile != "" {
		if _, err := fmt.Fprintf(tw, "[+] Patterns:\t%s (%d entries)\n", d.globalopts.PatternFile, len(d.globalopts.Patterns)); err != nil {
			return "", err
		}
	}

	if o.ExcludedStatusCodesParsed.Length() > 0 {
		if _, err := fmt.Fprintf(tw, "[+] Excluded Status codes:\t%s\n", o.ExcludedStatusCodesParsed.Stringify()); err != nil {
			return "", err
		}
	}

	if len(o.ExcludeLength) > 0 {
		if _, err := fmt.Fprintf(tw, "[+] Exclude Length:\t%s\n", d.options.ExcludeLengthParsed.Stringify()); err != nil {
			return "", err
		}
	}

	if o.Proxy != "" {
		if _, err := fmt.Fprintf(tw, "[+] Proxy:\t%s\n", o.Proxy); err != nil {
			return "", err
		}
	}

	if o.Cookies != "" {
		if _, err := fmt.Fprintf(tw, "[+] Cookies:\t%s\n", o.Cookies); err != nil {
			return "", err
		}
	}

	if o.UserAgent != "" {
		if _, err := fmt.Fprintf(tw, "[+] User Agent:\t%s\n", o.UserAgent); err != nil {
			return "", err
		}
	}

	if o.LocalAddr != nil {
		if _, err := fmt.Fprintf(tw, "[+] Local IP:\t%s\n", o.LocalAddr); err != nil {
			return "", err
		}
	}

	if o.Username != "" {
		if _, err := fmt.Fprintf(tw, "[+] Auth User:\t%s\n", o.Username); err != nil {
			return "", err
		}
	}

	if o.FollowRedirect {
		if _, err := fmt.Fprintf(tw, "[+] Follow Redirect:\ttrue\n"); err != nil {
			return "", err
		}
	}

	if _, err := fmt.Fprintf(tw, "[+] Timeout:\t%s\n", o.Timeout.String()); err != nil {
		return "", err
	}

	if err := tw.Flush(); err != nil {
		return "", fmt.Errorf("error on tostring: %w", err)
	}

	if err := bw.Flush(); err != nil {
		return "", fmt.Errorf("error on tostring: %w", err)
	}

	return strings.TrimSpace(buffer.String()), nil
}


================================================
FILE: gobusterfuzz/options.go
================================================
package gobusterfuzz

import (
	"github.com/OJ/gobuster/v3/libgobuster"
)

// OptionsFuzz is the struct to hold all options for this plugin
type OptionsFuzz struct {
	libgobuster.HTTPOptions
	ExcludedStatusCodes       string
	ExcludedStatusCodesParsed libgobuster.Set[int]
	ExcludeLength             string
	ExcludeLengthParsed       libgobuster.Set[int]
	RequestBody               string
}

// NewOptions returns a new initialized OptionsFuzz
func NewOptions() *OptionsFuzz {
	return &OptionsFuzz{
		ExcludedStatusCodesParsed: libgobuster.NewSet[int](),
		ExcludeLengthParsed:       libgobuster.NewSet[int](),
	}
}


================================================
FILE: gobusterfuzz/options_test.go
================================================
package gobusterfuzz

import "testing"

func TestNewOptions(t *testing.T) {
	t.Parallel()

	o := NewOptions()
	if o.ExcludedStatusCodesParsed.Set == nil {
		t.Fatal("StatusCodesParsed not initialized")
	}
}


================================================
FILE: gobusterfuzz/result.go
================================================
package gobusterfuzz

import (
	"bytes"
	"fmt"
	"net/http"

	"github.com/fatih/color"
)

var (
	green = color.New(color.FgGreen).FprintfFunc()
	blue  = color.New(color.FgBlue).FprintfFunc()
)

// Result represents a single result
type Result struct {
	Word       string
	Path       string
	StatusCode int
	Size       int64
	Header     http.Header
}

// ResultToString converts the Result to its textual representation
func (r Result) ResultToString() (string, error) {
	buf := &bytes.Buffer{}

	green(buf, "[Status=%d] [Length=%d] [Word=%s] %s", r.StatusCode, r.Size, r.Word, r.Path)

	location := r.Header.Get("Location")
	if location != "" {
		blue(buf, " [--> %s]", location)
	}

	if _, err := fmt.Fprintf(buf, "\n"); err != nil {
		return "", err
	}
	s := buf.String()
	return s, nil
}


================================================
FILE: gobustergcs/gobustersgcs.go
================================================
package gobustergcs

import (
	"bufio"
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"regexp"
	"strings"
	"syscall"
	"text/tabwriter"

	"github.com/OJ/gobuster/v3/libgobuster"
)

// GobusterGCS is the main type to implement the interface
type GobusterGCS struct {
	options     *OptionsGCS
	globalopts  *libgobuster.Options
	http        *libgobuster.HTTPClient
	bucketRegex *regexp.Regexp
}

// New creates a new initialized GobusterGCS
func New(globalopts *libgobuster.Options, opts *OptionsGCS, logger *libgobuster.Logger) (*GobusterGCS, error) {
	if globalopts == nil {
		return nil, errors.New("please provide valid global options")
	}

	if opts == nil {
		return nil, errors.New("please provide valid plugin options")
	}

	g := GobusterGCS{
		options:    opts,
		globalopts: globalopts,
	}

	basicOptions := libgobuster.BasicHTTPOptions{
		Proxy:           opts.Proxy,
		Timeout:         opts.Timeout,
		UserAgent:       opts.UserAgent,
		NoTLSValidation: opts.NoTLSValidation,
		RetryOnTimeout:  opts.RetryOnTimeout,
		RetryAttempts:   opts.RetryAttempts,
		TLSCertificate:  opts.TLSCertificate,
	}

	httpOpts := libgobuster.HTTPOptions{
		BasicHTTPOptions: basicOptions,
		// needed so we can list bucket contents
		FollowRedirect: true,
	}

	h, err := libgobuster.NewHTTPClient(&httpOpts, logger)
	if err != nil {
		return nil, err
	}
	g.http = h
	// https://cloud.google.com/storage/docs/naming-buckets
	g.bucketRegex = regexp.MustCompile(`^[a-z0-9][a-z0-9\-_.]{1,61}[a-z0-9](\.[a-z0-9][a-z0-9\-_.]{1,61}[a-z0-9])*$`)

	return &g, nil
}

// Name should return the name of the plugin
func (s *GobusterGCS) Name() string {
	return "GCS bucket enumeration"
}

// PreRun is the pre run implementation of GobusterS3
func (s *GobusterGCS) PreRun(_ context.Context, _ *libgobuster.Progress) error {
	return nil
}

// ProcessWord is the process implementation of GobusterS3
func (s *GobusterGCS) ProcessWord(ctx context.Context, word string, progress *libgobuster.Progress) (libgobuster.Result, error) {
	// only check for valid bucket names
	if !s.isValidBucketName(word) {
		return nil, nil // nolint:nilnil
	}

	bucketURL := fmt.Sprintf("https://storage.googleapis.com/storage/v1/b/%s/o?maxResults=%d", url.PathEscape(word), s.options.MaxFilesToList)
	u, err := url.Parse(bucketURL)
	if err != nil {
		return nil, fmt.Errorf("could not parse bucket URL %s: %w", bucketURL, err)
	}

	// add some debug output
	if s.globalopts.Debug {
		progress.MessageChan <- libgobuster.Message{
			Level:   libgobuster.LevelDebug,
			Message: fmt.Sprintf("trying word %s", word),
		}
	}

	tries := 1
	if s.options.RetryOnTimeout && s.options.RetryAttempts > 0 {
		// add it so it will be the overall max requests
		tries += s.options.RetryAttempts
	}

	var statusCode int
	var body []byte
	for i := 1; i <= tries; i++ {
		var err error
		statusCode, _, _, body, err = s.http.Request(ctx, *u, libgobuster.RequestOptions{ReturnBody: true})
		if err != nil {
			// check if it's a timeout and if we should try again and try again
			// otherwise the timeout error is raised
			switch {
			case os.IsTimeout(err) && i != tries:
				continue
			case strings.Contains(err.Error(), "invalid control character in URL"):
				// put error in error chan, so it's printed out and ignore it
				// so gobuster will not quit
				progress.ErrorChan <- err
				continue
			default:
				switch {
				case errors.Is(err, io.EOF):
					return nil, libgobuster.ErrEOF
				case os.IsTimeout(err):
					return nil, libgobuster.ErrTimeout
				case errors.Is(err, syscall.ECONNREFUSED):
					return nil, libgobuster.ErrConnectionRefused
				}
				return nil, err
			}
		}
		break
	}

	if statusCode == 0 || body == nil {
		return nil, nil // nolint:nilnil
	}

	// looks like 401, 403, and 404 are the only negative status codes
	var found bool
	switch statusCode {
	case http.StatusUnauthorized,
		http.StatusForbidden,
		http.StatusNotFound:
		found = false
	case http.StatusOK:
		// listing enabled
		found = true
	default:
		// default to found as we use negative status codes
		found = true
	}

	// nothing found, bail out
	// may add the result later if we want to enable verbose output
	if !found {
		return nil, nil // nolint:nilnil
	}

	var extraStrBuilder strings.Builder

	if s.options.ShowFiles {
		// get status
		var result map[string]interface{}
		err := json.Unmarshal(body, &result)
		if err != nil {
			return nil, fmt.Errorf("could not parse response json: %w", err)
		}

		if _, exist := result["error"]; exist {
			// https://cloud.google.com/storage/docs/json_api/v1/status-codes
			gcsError := GCSError{}
			err := json.Unmarshal(body, &gcsError)
			if err != nil {
				return nil, fmt.Errorf("could not parse error json: %w", err)
			}
			_, err = fmt.Fprintf(&extraStrBuilder, "Error: %s (%d)", gcsError.Error.Message, gcsError.Error.Code)
			if err != nil {
				return nil, fmt.Errorf("fmt.Fprintf to strings.Builder failed: %w", err)
			}
		} else if v, exist := result["kind"]; exist && v == "storage#objects" {
			// https://cloud.google.com/storage/docs/json_api/v1/status-codes
			// bucket listing enabled
			gcsListing := GCSListing{}
			err := json.Unmarshal(body, &gcsListing)
			if err != nil {
				return nil, fmt.Errorf("could not parse result json: %w", err)
			}
			extraStrBuilder.WriteString("Bucket Listing enabled: ")
			for i, x := range gcsListing.Items {
				if i > 0 {
					extraStrBuilder.WriteString(", ")
				}
				_, err := fmt.Fprintf(&extraStrBuilder, "%s (%sb)", x.Name, x.Size)
				if err != nil {
					return nil, fmt.Errorf("fmt.Fprintf to strings.Builder failed: %w", err)
				}
			}
		}
	}

	r := Result{
		Found:      found,
		BucketName: word,
		Status:     extraStrBuilder.String(),
	}

	return r, nil
}

func (s *GobusterGCS) AdditionalWordsLen() int {
	return 0
}

func (s *GobusterGCS) AdditionalWords(_ string) []string {
	return []string{}
}

func (s *GobusterGCS) AdditionalSuccessWords(_ string) []string {
	return []string{}
}

// GetConfigString returns the string representation of the current config
func (s *GobusterGCS) GetConfigString() (string, error) {
	var buffer bytes.Buffer
	bw := bufio.NewWriter(&buffer)
	tw := tabwriter.NewWriter(bw, 0, 5, 3, ' ', 0)
	o := s.options

	if _, err := fmt.Fprintf(tw, "[+] Threads:\t%d\n", s.globalopts.Threads); err != nil {
		return "", err
	}

	if s.globalopts.Delay > 0 {
		if _, err := fmt.Fprintf(tw, "[+] Delay:\t%s\n", s.globalopts.Delay); err != nil {
			return "", err
		}
	}

	wordlist := "stdin (pipe)"
	if s.globalopts.Wordlist != "-" {
		wordlist = s.globalopts.Wordlist
	}
	if _, err := fmt.Fprintf(tw, "[+] Wordlist:\t%s\n", wordlist); err != nil {
		return "", err
	}

	if s.globalopts.PatternFile != "" {
		if _, err := fmt.Fprintf(tw, "[+] Patterns:\t%s (%d entries)\n", s.globalopts.PatternFile, len(s.globalopts.Patterns)); err != nil {
			return "", err
		}
	}

	if o.Proxy != "" {
		if _, err := fmt.Fprintf(tw, "[+] Proxy:\t%s\n", o.Proxy); err != nil {
			return "", err
		}
	}

	if o.UserAgent != "" {
		if _, err := fmt.Fprintf(tw, "[+] User Agent:\t%s\n", o.UserAgent); err != nil {
			return "", err
		}
	}

	if o.LocalAddr != nil {
		if _, err := fmt.Fprintf(tw, "[+] Local IP:\t%s\n", o.LocalAddr); err != nil {
			return "", err
		}
	}

	if _, err := fmt.Fprintf(tw, "[+] Timeout:\t%s\n", o.Timeout.String()); err != nil {
		return "", err
	}

	if s.options.ShowFiles {
		if _, err := fmt.Fprintf(tw, "[+] Show Files:\ttrue\n"); err != nil {
			return "", err
		}
	}

	if _, err := fmt.Fprintf(tw, "[+] Maximum files to list:\t%d\n", o.MaxFilesToList); err != nil {
		return "", err
	}

	if err := tw.Flush(); err != nil {
		return "", fmt.Errorf("error on tostring: %w", err)
	}

	if err := bw.Flush(); err != nil {
		return "", fmt.Errorf("error on tostring: %w", err)
	}

	return strings.TrimSpace(buffer.String()), nil
}

// https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html
func (s *GobusterGCS) isValidBucketName(bucketName string) bool {
	if len(bucketName) > 222 || !s.bucketRegex.MatchString(bucketName) {
		return false
	}
	if strings.HasPrefix(bucketName, "-") || strings.HasSuffix(bucketName, "-") ||
		strings.HasPrefix(bucketName, "_") || strings.HasSuffix(bucketName, "_") ||
		strings.HasPrefix(bucketName, ".") || strings.HasSuffix(bucketName, ".") {
		return false
	}
	return true
}


================================================
FILE: gobustergcs/options.go
================================================
package gobustergcs

import (
	"github.com/OJ/gobuster/v3/libgobuster"
)

// OptionsGCS is the struct to hold all options for this plugin
type OptionsGCS struct {
	libgobuster.BasicHTTPOptions
	MaxFilesToList int
	ShowFiles      bool
}

// NewOptions returns a new initialized OptionsS3
func NewOptions() *OptionsGCS {
	return &OptionsGCS{}
}


================================================
FILE: gobustergcs/result.go
================================================
package gobustergcs

import (
	"bytes"

	"github.com/fatih/color"
)

var green = color.New(color.FgGreen).FprintfFunc()

// Result represents a single result
type Result struct {
	Found      bool
	BucketName string
	Status     string
}

// ResultToString converts the Result to its textual representation
func (r Result) ResultToString() (string, error) {
	buf := &bytes.Buffer{}

	c := green

	c(buf, "https://storage.googleapis.com/storage/v1/b/%s/o", r.BucketName)

	if r.Status != "" {
		c(buf, " [%s]", r.Status)
	}
	c(buf, "\n")

	str := buf.String()
	return str, nil
}


================================================
FILE: gobustergcs/types.go
================================================
package gobustergcs

// GCSError represents a returned error from GCS
type GCSError struct {
	Error struct {
		Code    int    `json:"code"`
		Message string `json:"message"`
		Errors  []struct {
			Message      string `json:"message"`
			Reason       string `json:"reason"`
			LocationType string `json:"locationType"`
			Location     string `json:"location"`
		} `json:"errors"`
	} `json:"error"`
}

// GCSListing contains only a subset of returned properties
type GCSListing struct {
	IsTruncated string `json:"nextPageToken"`
	Items       []struct {
		Name         string `json:"name"`
		LastModified string `json:"updated"`
		Size         string `json:"size"`
	} `json:"items"`
}


================================================
FILE: gobusters3/gobusters3.go
================================================
package gobusters3

import (
	"bufio"
	"bytes"
	"context"
	"encoding/xml"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"regexp"
	"strings"
	"syscall"
	"text/tabwriter"

	"github.com/OJ/gobuster/v3/libgobuster"
)

// GobusterS3 is the main type to implement the interface
type GobusterS3 struct {
	options     *OptionsS3
	globalopts  *libgobuster.Options
	http        *libgobuster.HTTPClient
	bucketRegex *regexp.Regexp
}

// New creates a new initialized GobusterS3
func New(globalopts *libgobuster.Options, opts *OptionsS3, logger *libgobuster.Logger) (*GobusterS3, error) {
	if globalopts == nil {
		return nil, errors.New("please provide valid global options")
	}

	if opts == nil {
		return nil, errors.New("please provide valid plugin options")
	}

	g := GobusterS3{
		options:    opts,
		globalopts: globalopts,
	}

	basicOptions := libgobuster.BasicHTTPOptions{
		Proxy:           opts.Proxy,
		Timeout:         opts.Timeout,
		UserAgent:       opts.UserAgent,
		NoTLSValidation: opts.NoTLSValidation,
		RetryOnTimeout:  opts.RetryOnTimeout,
		RetryAttempts:   opts.RetryAttempts,
		TLSCertificate:  opts.TLSCertificate,
	}

	httpOpts := libgobuster.HTTPOptions{
		BasicHTTPOptions: basicOptions,
		// needed so we can list bucket contents
		FollowRedirect: true,
	}

	h, err := libgobuster.NewHTTPClient(&httpOpts, logger)
	if err != nil {
		return nil, err
	}
	g.http = h
	g.bucketRegex = regexp.MustCompile(`^[a-z0-9\-.]{3,63}$`)

	return &g, nil
}

// Name should return the name of the plugin
func (s *GobusterS3) Name() string {
	return "S3 bucket enumeration"
}

// PreRun is the pre run implementation of GobusterS3
func (s *GobusterS3) PreRun(_ context.Context, _ *libgobuster.Progress) error {
	return nil
}

// ProcessWord is the process implementation of GobusterS3
func (s *GobusterS3) ProcessWord(ctx context.Context, word string, progress *libgobuster.Progress) (libgobuster.Result, error) {
	// only check for valid bucket names
	if !s.isValidBucketName(word) {
		return nil, nil // nolint:nilnil
	}

	bucketURL := fmt.Sprintf("https://%s.s3.amazonaws.com/?max-keys=%d", url.PathEscape(word), s.options.MaxFilesToList)
	u, err := url.Parse(bucketURL)
	if err != nil {
		return nil, fmt.Errorf("could not parse bucket URL %s: %w", bucketURL, err)
	}

	// add some debug output
	if s.globalopts.Debug {
		progress.MessageChan <- libgobuster.Message{
			Level:   libgobuster.LevelDebug,
			Message: fmt.Sprintf("trying word %s", word),
		}
	}

	tries := 1
	if s.options.RetryOnTimeout && s.options.RetryAttempts > 0 {
		// add it so it will be the overall max requests
		tries += s.options.RetryAttempts
	}

	var statusCode int
	var body []byte
	for i := 1; i <= tries; i++ {
		var err error
		statusCode, _, _, body, err = s.http.Request(ctx, *u, libgobuster.RequestOptions{ReturnBody: true})
		if err != nil {
			// check if it's a timeout and if we should try again and try again
			// otherwise the timeout error is raised
			if os.IsTimeout(err) && i != tries {
				continue
			} else if strings.Contains(err.Error(), "invalid control character in URL") {
				// put error in error chan, so it's printed out and ignore it
				// so gobuster will not quit
				progress.ErrorChan <- err
				continue
			}

			switch {
			case errors.Is(err, io.EOF):
				return nil, libgobuster.ErrEOF
			case os.IsTimeout(err):
				return nil, libgobuster.ErrTimeout
			case errors.Is(err, syscall.ECONNREFUSED):
				return nil, libgobuster.ErrConnectionRefused
			}
			return nil, err
		}
		break
	}

	if statusCode == 0 || body == nil {
		return nil, nil // nolint:nilnil
	}

	// looks like 404 and 400 are the only negative status codes
	found := false
	switch statusCode {
	case http.StatusBadRequest:
	case http.StatusNotFound:
		found = false
	case http.StatusOK:
		// listing enabled
		found = true
		// parse xml
	default:
		// default to found as we use negative status codes
		found = true
	}

	// nothing found, bail out
	// may add the result later if we want to enable verbose output
	if !found {
		return nil, nil // nolint:nilnil
	}

	var extraStrBuilder strings.Builder

	if s.options.ShowFiles {
		// get status
		if bytes.Contains(body, []byte("<Error>")) {
			awsError := AWSError{}
			err := xml.Unmarshal(body, &awsError)
			if err != nil {
				return nil, fmt.Errorf("could not parse error xml: %w", err)
			}
			// https://docs.aws.amazon.com/AmazonS3/latest/API/ErrorResponses.html#ErrorCodeList
			_, err = fmt.Fprintf(&extraStrBuilder, "Error: %s (%s)", awsError.Message, awsError.Code)
			if err != nil {
				return nil, fmt.Errorf("fmt.Fprintf to strings.Builder failed: %w", err)
			}
		} else if bytes.Contains(body, []byte("<ListBucketResult ")) {
			// bucket listing enabled
			awsListing := AWSListing{}
			err := xml.Unmarshal(body, &awsListing)
			if err != nil {
				return nil, fmt.Errorf("could not parse result xml: %w", err)
			}
			extraStrBuilder.WriteString("Bucket Listing enabled: ")
			for i, x := range awsListing.Contents {
				if i > 0 {
					extraStrBuilder.WriteString(", ")
				}
				_, err := fmt.Fprintf(&extraStrBuilder, "%s (%db)", x.Key, x.Size)
				if err != nil {
					return nil, fmt.Errorf("fmt.Fprintf to strings.Builder failed: %w", err)
				}
			}
		}
	}

	r := Result{
		Found:      found,
		BucketName: word,
		Status:     extraStrBuilder.String(),
	}

	return r, nil
}

func (s *GobusterS3) AdditionalWordsLen() int {
	return 0
}

func (s *GobusterS3) AdditionalWords(_ string) []string {
	return []string{}
}

func (s *GobusterS3) AdditionalSuccessWords(_ string) []string {
	return []string{}
}

// GetConfigString returns the string representation of the current config
func (s *GobusterS3) GetConfigString() (string, error) {
	var buffer bytes.Buffer
	bw := bufio.NewWriter(&buffer)
	tw := tabwriter.NewWriter(bw, 0, 5, 3, ' ', 0)
	o := s.options

	if _, err := fmt.Fprintf(tw, "[+] Threads:\t%d\n", s.globalopts.Threads); err != nil {
		return "", err
	}

	if s.globalopts.Delay > 0 {
		if _, err := fmt.Fprintf(tw, "[+] Delay:\t%s\n", s.globalopts.Delay); err != nil {
			return "", err
		}
	}

	wordlist := "stdin (pipe)"
	if s.globalopts.Wordlist != "-" {
		wordlist = s.globalopts.Wordlist
	}
	if _, err := fmt.Fprintf(tw, "[+] Wordlist:\t%s\n", wordlist); err != nil {
		return "", err
	}

	if s.globalopts.PatternFile != "" {
		if _, err := fmt.Fprintf(tw, "[+] Patterns:\t%s (%d entries)\n", s.globalopts.PatternFile, len(s.globalopts.Patterns)); err != nil {
			return "", err
		}
	}

	if o.Proxy != "" {
		if _, err := fmt.Fprintf(tw, "[+] Proxy:\t%s\n", o.Proxy); err != nil {
			return "", err
		}
	}

	if o.UserAgent != "" {
		if _, err := fmt.Fprintf(tw, "[+] User Agent:\t%s\n", o.UserAgent); err != nil {
			return "", err
		}
	}

	if o.LocalAddr != nil {
		if _, err := fmt.Fprintf(tw, "[+] Local IP:\t%s\n", o.LocalAddr); err != nil {
			return "", err
		}
	}

	if _, err := fmt.Fprintf(tw, "[+] Timeout:\t%s\n", o.Timeout.String()); err != nil {
		return "", err
	}

	if s.options.ShowFiles {
		if _, err := fmt.Fprintf(tw, "[+] Show Files:\ttrue\n"); err != nil {
			return "", err
		}
	}

	if _, err := fmt.Fprintf(tw, "[+] Maximum files to list:\t%d\n", o.MaxFilesToList); err != nil {
		return "", err
	}

	if err := tw.Flush(); err != nil {
		return "", fmt.Errorf("error on tostring: %w", err)
	}

	if err := bw.Flush(); err != nil {
		return "", fmt.Errorf("error on tostring: %w", err)
	}

	return strings.TrimSpace(buffer.String()), nil
}

// https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html
func (s *GobusterS3) isValidBucketName(bucketName string) bool {
	if !s.bucketRegex.MatchString(bucketName) {
		return false
	}
	if strings.HasSuffix(bucketName, "-") ||
		strings.HasPrefix(bucketName, ".") ||
		strings.HasPrefix(bucketName, "-") ||
		strings.Contains(bucketName, "..") ||
		strings.Contains(bucketName, ".-") ||
		strings.Contains(bucketName, "-.") {
		return false
	}
	return true
}


================================================
FILE: gobusters3/options.go
================================================
package gobusters3

import (
	"github.com/OJ/gobuster/v3/libgobuster"
)

// OptionsS3 is the struct to hold all options for this plugin
type OptionsS3 struct {
	libgobuster.BasicHTTPOptions
	MaxFilesToList int
	ShowFiles      bool
}

// NewOptions returns a new initialized OptionsS3
func NewOptions() *OptionsS3 {
	return &OptionsS3{}
}


================================================
FILE: gobusters3/result.go
================================================
package gobusters3

import (
	"bytes"

	"github.com/fatih/color"
)

var green = color.New(color.FgGreen).FprintfFunc()

// Result represents a single result
type Result struct {
	Found      bool
	BucketName string
	Status     string
}

// ResultToString converts the Result to its textual representation
func (r Result) ResultToString() (string, error) {
	buf := &bytes.Buffer{}

	c := green

	c(buf, "http://%s.s3.amazonaws.com/", r.BucketName)

	if r.Status != "" {
		c(buf, " [%s]", r.Status)
	}
	c(buf, "\n")

	str := buf.String()
	return str, nil
}


================================================
FILE: gobusters3/types.go
================================================
package gobusters3

import "encoding/xml"

// AWSError represents a returned error from AWS
type AWSError struct {
	XMLName   xml.Name `xml:"Error"`
	Code      string   `xml:"Code"`
	Message   string   `xml:"Message"`
	RequestID string   `xml:"RequestId"`
	HostID    string   `xml:"HostId"`
}

// AWSListing contains only a subset of returned properties
type AWSListing struct {
	XMLName     xml.Name `xml:"ListBucketResult"`
	Name        string   `xml:"Name"`
	IsTruncated string   `xml:"IsTruncated"`
	Contents    []struct {
		Key          string `xml:"Key"`
		LastModified string `xml:"LastModified"`
		Size         int    `xml:"Size"`
	} `xml:"Contents"`
}


================================================
FILE: gobustertftp/gobustertftp.go
================================================
package gobustertftp

import (
	"bufio"
	"bytes"
	"context"
	"errors"
	"fmt"
	"strings"
	"text/tabwriter"

	"github.com/OJ/gobuster/v3/libgobuster"

	"github.com/pin/tftp/v3"
)

// GobusterTFTP is the main type to implement the interface
type GobusterTFTP struct {
	globalopts *libgobuster.Options
	options    *OptionsTFTP
}

// New creates a new initialized NewGobusterTFTP
func New(globalopts *libgobuster.Options, opts *OptionsTFTP) (*GobusterTFTP, error) {
	if globalopts == nil {
		return nil, errors.New("please provide valid global options")
	}

	if opts == nil {
		return nil, errors.New("please provide valid plugin options")
	}

	g := GobusterTFTP{
		options:    opts,
		globalopts: globalopts,
	}
	return &g, nil
}

// Name should return the name of the plugin
func (d *GobusterTFTP) Name() string {
	return "TFTP enumeration"
}

// PreRun is the pre run implementation of gobustertftp
func (d *GobusterTFTP) PreRun(_ context.Context, _ *libgobuster.Progress) error {
	_, err := tftp.NewClient(d.options.Server)
	if err != nil {
		return err
	}
	return nil
}

// ProcessWord is the process implementation of gobustertftp
func (d *GobusterTFTP) ProcessWord(_ context.Context, word string, progress *libgobuster.Progress) (libgobuster.Result, error) {
	// add some debug output
	if d.globalopts.Debug {
		progress.MessageChan <- libgobuster.Message{
			Level:   libgobuster.LevelDebug,
			Message: fmt.Sprintf("trying word %s", word),
		}
	}

	c, err := tftp.NewClient(d.options.Server)
	if err != nil {
		return nil, err
	}
	c.SetTimeout(d.options.Timeout)
	wt, err := c.Receive(word, "octet")
	if err != nil {
		// file not found
		return nil, nil // nolint:nilerr,nilnil
	}
	result := Result{
		Filename: word,
	}
	wt2, ok := wt.(tftp.IncomingTransfer)
	if !ok {
		return nil, errors.New("could not cast to IncomingTransfer")
	}
	if n, ok := wt2.Size(); ok {
		result.Size = n
	}
	return result, nil
}

func (d *GobusterTFTP) AdditionalWordsLen() int {
	return 0
}

func (d *GobusterTFTP) AdditionalWords(_ string) []string {
	return []string{}
}

func (d *GobusterTFTP) AdditionalSuccessWords(_ string) []string {
	return []string{}
}

// GetConfigString returns the string representation of the current config
func (d *GobusterTFTP) GetConfigString() (string, error) {
	var buffer bytes.Buffer
	bw := bufio.NewWriter(&buffer)
	tw := tabwriter.NewWriter(bw, 0, 5, 3, ' ', 0)
	o := d.options

	if _, err := fmt.Fprintf(tw, "[+] Server:\t%s\n", o.Server); err != nil {
		return "", err
	}

	if _, err := fmt.Fprintf(tw, "[+] Threads:\t%d\n", d.globalopts.Threads); err != nil {
		return "", err
	}

	if d.globalopts.Delay > 0 {
		if _, err := fmt.Fprintf(tw, "[+] Delay:\t%s\n", d.globalopts.Delay); err != nil {
			return "", err
		}
	}

	if _, err := fmt.Fprintf(tw, "[+] Timeout:\t%s\n", o.Timeout.String()); err != nil {
		return "", err
	}

	wordlist := "stdin (pipe)"
	if d.globalopts.Wordlist != "-" {
		wordlist = d.globalopts.Wordlist
	}
	if _, err := fmt.Fprintf(tw, "[+] Wordlist:\t%s\n", wordlist); err != nil {
		return "", err
	}

	if d.globalopts.PatternFile != "" {
		if _, err := fmt.Fprintf(tw, "[+] Patterns:\t%s (%d entries)\n", d.globalopts.PatternFile, len(d.globalopts.Patterns)); err != nil {
			return "", err
		}
	}

	if err := tw.Flush(); err != nil {
		return "", fmt.Errorf("error on tostring: %w", err)
	}

	if err := bw.Flush(); err != nil {
		return "", fmt.Errorf("error on tostring: %w", err)
	}

	return strings.TrimSpace(buffer.String()), nil
}


================================================
FILE: gobustertftp/options.go
================================================
package gobustertftp

import (
	"time"
)

// OptionsTFTP holds all options for the tftp plugin
type OptionsTFTP struct {
	Server  string
	Timeout time.Duration
}

// NewOptions returns a new initialized OptionsTFTP
func NewOptions() *OptionsTFTP {
	return &OptionsTFTP{}
}


================================================
FILE: gobustertftp/result.go
================================================
package gobustertftp

import (
	"bytes"
	"fmt"

	"github.com/fatih/color"
)

var green = color.New(color.FgGreen).FprintfFunc()

// Result represents a single result
type Result struct {
	Filename     string
	Size         int64
	ErrorMessage string
}

// ResultToString converts the Result to its textual representation
func (r Result) ResultToString() (string, error) {
	buf := &bytes.Buffer{}

	green(buf, "%s", r.Filename)
	if r.Size > 0 {
		if _, err := fmt.Fprintf(buf, " [%d]", r.Size); err != nil {
			return "", err
		}
	}

	if _, err := fmt.Fprintf(buf, "\n"); err != nil {
		return "", err
	}

	s := buf.String()
	return s, nil
}


================================================
FILE: gobustervhost/gobustervhost.go
================================================
package gobustervhost

import (
	"bufio"
	"bytes"
	"context"
	"errors"
	"fmt"
	"io"
	"net/http"
	"os"
	"strings"
	"sync"
	"syscall"
	"text/tabwriter"

	"github.com/OJ/gobuster/v3/libgobuster"
	"github.com/google/uuid"
)

// GobusterVhost is the main type to implement the interface
type GobusterVhost struct {
	options      *OptionsVhost
	globalopts   *libgobuster.Options
	http         *libgobuster.HTTPClient
	domain       string
	normalBody   []byte
	abnormalBody []byte
	once         sync.Once
}

// New creates a new initialized GobusterDir
func New(globalopts *libgobuster.Options, opts *OptionsVhost, logger *libgobuster.Logger) (*GobusterVhost, error) {
	if globalopts == nil {
		return nil, errors.New("please provide valid global options")
	}

	if opts == nil {
		return nil, errors.New("please provide valid plugin options")
	}

	g := GobusterVhost{
		options:    opts,
		globalopts: globalopts,
	}

	basicOptions := libgobuster.BasicHTTPOptions{
		Proxy:           opts.Proxy,
		Timeout:         opts.Timeout,
		UserAgent:       opts.UserAgent,
		NoTLSValidation: opts.NoTLSValidation,
		RetryOnTimeout:  opts.RetryOnTimeout,
		RetryAttempts:   opts.RetryAttempts,
		TLSCertificate:  opts.TLSCertificate,
	}

	httpOpts := libgobuster.HTTPOptions{
		BasicHTTPOptions:      basicOptions,
		FollowRedirect:        opts.FollowRedirect,
		Username:              opts.Username,
		Password:              opts.Password,
		Headers:               opts.Headers,
		NoCanonicalizeHeaders: opts.NoCanonicalizeHeaders,
		Cookies:               opts.Cookies,
		Method:                opts.Method,
	}

	h, err := libgobuster.NewHTTPClient(&httpOpts, logger)
	if err != nil {
		return nil, err
	}
	g.http = h
	return &g, nil
}

// Name should return the name of the plugin
func (v *GobusterVhost) Name() string {
	return "VHOST enumeration"
}

// PreRun is the pre run implementation of gobusterdir
func (v *GobusterVhost) PreRun(ctx context.Context, _ *libgobuster.Progress) error {
	// add trailing slash
	if !strings.HasSuffix(v.options.URL.Path, "/") {
		v.options.URL.Path = fmt.Sprintf("%s/", v.options.URL.Path)
	}

	if v.options.Domain != "" {
		v.domain = v.options.Domain
	} else {
		v.domain = v.options.URL.Host
	}

	// request default vhost for normalBody
	_, _, _, body, err := v.http.Request(ctx, *v.options.URL, libgobuster.RequestOptions{ReturnBody: true})
	if err != nil {
		switch {
		case errors.Is(err, io.EOF):
			return libgobuster.ErrEOF
		case os.IsTimeout(err):
			return libgobuster.ErrTimeout
		case errors.Is(err, syscall.ECONNREFUSED):
			return libgobuster.ErrConnectionRefused
		}
		return fmt.Errorf("unable to connect to %s: %w", v.options.URL, err)
	}
	v.normalBody = body

	// request non existent vhost for abnormalBody
	subdomain := fmt.Sprintf("%s.%s", uuid.New(), v.domain)
	_, _, _, body, err = v.http.Request(ctx, *v.options.URL, libgobuster.RequestOptions{Host: subdomain, ReturnBody: true})
	if err != nil {
		switch {
		case errors.Is(err, io.EOF):
			return libgobuster.ErrEOF
		case os.IsTimeout(err):
			return libgobuster.ErrTimeout
		case errors.Is(err, syscall.ECONNREFUSED):
			return libgobuster.ErrConnectionRefused
		}
		return fmt.Errorf("unable to connect to %s: %w", v.options.URL, err)
	}
	v.abnormalBody = body
	return nil
}

// ProcessWord is the process implementation of gobusterdir
func (v *GobusterVhost) ProcessWord(ctx context.Context, word string, progress *libgobuster.Progress) (libgobuster.Result, error) {
	var subdomain string
	var hostnameLength int
	if v.options.AppendDomain {
		subdomain = fmt.Sprintf("%s.%s", word, v.domain)
	} else {
		// wordlist needs to include full domains
		subdomain = word
	}
	if v.options.ExcludeHostnameLength {
		hostnameLength = len(subdomain)
	} else {
		hostnameLength = 0
	}

	// warn people when there is no . detected so they might want to use the other options
	v.once.Do(func() {
		if !strings.Contains(subdomain, ".") {
			progress.MessageChan <- libgobuster.Message{
				Level:   libgobuster.LevelWarn,
				Message: fmt.Sprintf("the first subdomain to try does not contain a dot (%s). You might want to use the option to append the base domain otherwise the vhost will be tried as is", subdomain),
			}
		}
	})

	// add some debug output
	if v.globalopts.Debug {
		progress.MessageChan <- libgobuster.Message{
			Level:   libgobuster.LevelDebug,
			Message: fmt.Sprintf("trying vhost %s", subdomain),
		}
	}

	tries := 1
	if v.options.RetryOnTimeout && v.options.RetryAttempts > 0 {
		// add it so it will be the overall max requests
		tries += v.options.RetryAttempts
	}

	var statusCode int
	var size int64
	var header http.Header
	var body []byte
	for i := 1; i <= tries; i++ {
		var err error
		statusCode, size, header, body, err = v.http.Request(ctx, *v.options.URL, libgobuster.RequestOptions{Host: subdomain, ReturnBody: true})
		if err != nil {
			// check if it's a timeout and if we should try again and try again
			// otherwise the timeout error is raised
			switch {
			case os.IsTimeout(err) && i != tries:
				continue
			case strings.Contains(err.Error(), "invalid control character in URL"):
				// put error in error chan, so it's printed out and ignore it
				// so gobuster will not quit
				progress.ErrorChan <- err
				continue
			default:
				switch {
				case errors.Is(err, io.EOF):
					return nil, libgobuster.ErrEOF
				case os.IsTimeout(err):
					return nil, libgobuster.ErrTimeout
				case errors.Is(err, syscall.ECONNREFUSED):
					return nil, libgobuster.ErrConnectionRefused
				}
				return nil, err
			}
		}
		break
	}

	// subdomain must not match default vhost and non existent vhost
	// or verbose mode is enabled
	found := body != nil && !bytes.Equal(body, v.normalBody) && !bytes.Equal(body, v.abnormalBody)
	if found && !v.options.ExcludeLengthParsed.Contains(int(size)-hostnameLength) && !v.options.ExcludeStatusParsed.Contains(statusCode) {
		r := Result{
			Vhost:      subdomain,
			StatusCode: statusCode,
			Size:       size,
			Header:     header,
		}
		return r, nil
	}
	return nil, nil // nolint:nilnil
}

func (v *GobusterVhost) AdditionalWordsLen() int {
	return 0
}

func (v *GobusterVhost) AdditionalWords(_ string) []string {
	return []string{}
}

func (v *GobusterVhost) AdditionalSuccessWords(_ string) []string {
	return []string{}
}

// GetConfigString returns the string representation of the current config
func (v *GobusterVhost) GetConfigString() (string, error) {
	var buffer bytes.Buffer
	bw := bufio.NewWriter(&buffer)
	tw := tabwriter.NewWriter(bw, 0, 5, 3, ' ', 0)
	o := v.options
	if _, err := fmt.Fprintf(tw, "[+] Url:\t%s\n", o.URL); err != nil {
		return "", err
	}

	if _, err := fmt.Fprintf(tw, "[+] Method:\t%s\n", o.Method); err != nil {
		return "", err
	}

	if _, err := fmt.Fprintf(tw, "[+] Threads:\t%d\n", v.globalopts.Threads); err != nil {
		return "", err
	}

	if v.globalopts.Delay > 0 {
		if _, err := fmt.Fprintf(tw, "[+] Delay:\t%s\n", v.globalopts.Delay); err != nil {
			return "", err
		}
	}

	wordlist := "stdin (pipe)"
	if v.globalopts.Wordlist != "-" {
		wordlist = v.globalopts.Wordlist
	}
	if _, err := fmt.Fprintf(tw, "[+] Wordlist:\t%s\n", wordlist); err != nil {
		return "", err
	}

	if v.globalopts.PatternFile != "" {
		if _, err := fmt.Fprintf(tw, "[+] Patterns:\t%s (%d entries)\n", v.globalopts.PatternFile, len(v.globalopts.Patterns)); err != nil {
			return "", err
		}
	}

	if o.Proxy != "" {
		if _, err := fmt.Fprintf(tw, "[+] Proxy:\t%s\n", o.Proxy); err != nil {
			return "", err
		}
	}

	if o.Cookies != "" {
		if _, err := fmt.Fprintf(tw, "[+] Cookies:\t%s\n", o.Cookies); err != nil {
			return "", err
		}
	}

	if o.UserAgent != "" {
		if _, err := fmt.Fprintf(tw, "[+] User Agent:\t%s\n", o.UserAgent); err != nil {
			return "", err
		}
	}

	if o.LocalAddr != nil {
		if _, err := fmt.Fprintf(tw, "[+] Local IP:\t%s\n", o.LocalAddr); err != nil {
			return "", err
		}
	}

	if o.Username != "" {
		if _, err := fmt.Fprintf(tw, "[+] Auth User:\t%s\n", o.Username); err != nil {
			return "", err
		}
	}

	if _, err := fmt.Fprintf(tw, "[+] Timeout:\t%s\n", o.Timeout.String()); err != nil {
		return "", err
	}

	if _, err := fmt.Fprintf(tw, "[+] Append Domain:\t%t\n", v.options.AppendDomain); err != nil {
		return "", err
	}

	if len(o.ExcludeLength) > 0 {
		if _, err := fmt.Fprintf(tw, "[+] Exclude Length:\t%s\n", v.options.ExcludeLengthParsed.Stringify()); err != nil {
			return "", err
		}
	}

	if _, err := fmt.Fprintf(tw, "[+] Exclude Hostname Length:\t%t\n", v.options.ExcludeHostnameLength); err != nil {
		return "", err
	}

	if err := tw.Flush(); err != nil {
		return "", fmt.Errorf("error on tostring: %w", err)
	}

	if err := bw.Flush(); err != nil {
		return "", fmt.Errorf("error on tostring: %w", err)
	}

	return strings.TrimSpace(buffer.String()), nil
}


================================================
FILE: gobustervhost/options.go
================================================
package gobustervhost

import (
	"github.com/OJ/gobuster/v3/libgobuster"
)

// OptionsVhost is the struct to hold all options for this plugin
type OptionsVhost struct {
	libgobuster.HTTPOptions
	AppendDomain          bool
	ExcludeLength         string
	ExcludeLengthParsed   libgobuster.Set[int]
	ExcludeStatus         string
	ExcludeStatusParsed   libgobuster.Set[int]
	Domain                string
	ExcludeHostnameLength bool
}

// NewOptions returns a new initialized OptionsVhost
func NewOptions() *OptionsVhost {
	return &OptionsVhost{
		ExcludeLengthParsed: libgobuster.NewSet[int](),
		ExcludeStatusParsed: libgobuster.NewSet[int](),
	}
}


================================================
FILE: gobustervhost/result.go
================================================
package gobustervhost

import (
	"fmt"
	"net/http"

	"github.com/fatih/color"
)

var (
	white  = color.New(color.FgWhite).SprintFunc()
	yellow = color.New(color.FgYellow).SprintFunc()
	green  = color.New(color.FgGreen).SprintFunc()
	blue   = color.New(color.FgBlue).SprintFunc()
	red    = color.New(color.FgRed).SprintFunc()
	cyan   = color.New(color.FgCyan).SprintFunc()
)

// Result represents a single result
type Result struct {
	Vhost      string
	StatusCode int
	Size       int64
	Header     http.Header
}

// ResultToString converts the Result to its textual representation
func (r Result) ResultToString() (string, error) {
	statusCodeColor := white
	switch {
	case r.StatusCode == http.StatusOK:
		statusCodeColor = green
	case r.StatusCode >= 300 && r.StatusCode < 400:
		statusCodeColor = cyan
	case r.StatusCode >= 400 && r.StatusCode < 500:
		statusCodeColor = yellow
	case r.StatusCode >= 500 && r.StatusCode < 600:
		statusCodeColor = red
	}

	statusCode := statusCodeColor(fmt.Sprintf("Status: %d", r.StatusCode))

	location := r.Header.Get("Location")
	locationString := ""
	if location != "" {
		locationString = blue(fmt.Sprintf(" [--> %s]", location))
	}

	return fmt.Sprintf("%s %s [Size: %d]%s\n", r.Vhost, statusCode, r.Size, locationString), nil
}


================================================
FILE: libgobuster/errors.go
================================================
package libgobuster

import "errors"

var (
	ErrTimeout           = errors.New("timeout occurred during the request")
	ErrEOF               = errors.New("server closed connection without sending any data back. Maybe you are connecting via https to on http port or vice versa?")
	ErrConnectionRefused = errors.New("connection refused")
)


================================================
FILE: libgobuster/helpers.go
================================================
package libgobuster

import (
	"bufio"
	"bytes"
	"errors"
	"fmt"
	"io"
	"os"
	"regexp"
	"strconv"
	"strings"
)

// Set is a set of Ts
type Set[T comparable] struct {
	Set map[T]bool
}

// NewSet creates a new initialized Set
func NewSet[T comparable]() Set[T] {
	return Set[T]{Set: map[T]bool{}}
}

// Add an element to a set
func (set *Set[T]) Add(s T) bool {
	_, found := set.Set[s]
	set.Set[s] = true
	return !found
}

// AddRange adds a list of elements to a set
func (set *Set[T]) AddRange(ss []T) {
	for _, s := range ss {
		set.Set[s] = true
	}
}

// Contains tests if an element is in a set
func (set *Set[T]) Contains(s T) bool {
	_, found := set.Set[s]
	return found
}

// ContainsAny checks if any of the elements exist
func (set *Set[T]) ContainsAny(ss []T) bool {
	for _, s := range ss {
		if set.Set[s] {
			return true
		}
	}
	return false
}

// Length returns the length of the Set
func (set *Set[T]) Length() int {
	return len(set.Set)
}

// Stringify the set
func (set *Set[T]) Stringify() string {
	values := make([]string, len(set.Set))
	i := 0
	for s := range set.Set {
		values[i] = fmt.Sprint(s)
		i++
	}
	return strings.Join(values, ",")
}

// this method is much faster than lineCounter_slow but has the following errors:
// - empty files are reported as 1 line
// - files only containing a newline are reported as 1 line
// - also counts lines with comments
func lineCounter(r io.Reader) (int, error) {
	buf := make([]byte, 32*1024)
	count := 1
	lineSep := []byte{'\n'}
	var lastChar byte

	for {
		c, err := r.Read(buf)
		count += bytes.Count(buf[:c], lineSep)

		// store last character received if we got any bytes
		if c > 0 {
			lastChar = buf[c-1]
		}

		switch {
		case errors.Is(err, io.EOF):
			// account for trailing new line
			if lastChar == '\n' {
				count--
			}
			return count, nil

		case err != nil:
			return -1, err
		}
	}
}

func lineCounterSlow(r io.Reader) (int, error) {
	scanner := bufio.NewScanner(r)
	scanner.Split(bufio.ScanLines)
	var count int
	for scanner.Scan() {
		w := scanner.Text()
		if w == "" {
			continue
		}

		count++
	}
	if err := scanner.Err(); err != nil {
		return -1, err
	}
	return count, nil
}

// DefaultUserAgent returns the default user agent to use in HTTP requests
func DefaultUserAgent() string {
	return fmt.Sprintf("gobuster/%s", VERSION)
}

// ParseExtensions parses the extensions provided as a comma separated list
func ParseExtensions(extensions string) (Set[string], error) {
	ret := NewSet[string]()

	if extensions == "" {
		return ret, nil
	}

	for _, e := range strings.Split(extensions, ",") {
		e = strings.TrimSpace(e)
		// remove leading . from extensions
		ret.Add(strings.TrimPrefix(e, "."))
	}
	return ret, nil
}

func ParseExtensionsFile(file string) ([]string, error) {
	var ret []string

	stream, err := os.Open(file)
	if err != nil {
		return ret, err
	}
	defer stream.Close()

	scanner := bufio.NewScanner(stream)
	for scanner.Scan() {
		e := scanner.Text()
		e = strings.TrimSpace(e)
		// remove leading . from extensions
		ret = append(ret, strings.TrimPrefix(e, "."))
	}

	if err := scanner.Err(); err != nil {
		return nil, err
	}

	return ret, nil
}

// ParseCommaSeparatedInt parses the status codes provided as a comma separated list
func ParseCommaSeparatedInt(inputString string) (Set[int], error) {
	ret := NewSet[int]()

	if inputString == "" {
		return ret, nil
	}

	for _, part := range strings.Split(inputString, ",") {
		part = strings.TrimSpace(part)
		// check for range
		if strings.Contains(part, "-") {
			re := regexp.MustCompile(`^\s*(\d+)\s*-\s*(\d+)\s*$`)
			match := re.FindStringSubmatch(part)
			if match == nil || len(match) != 3 {
				return NewSet[int](), fmt.Errorf("invalid range given: %s", part)
			}
			from := strings.TrimSpace(match[1])
			to := strings.TrimSpace(match[2])
			fromI, err := strconv.Atoi(from)
			if err != nil {
				return NewSet[int](), fmt.Errorf("invalid string in range %s: %s", part, from)
			}
			toI, err := strconv.Atoi(to)
			if err != nil {
				return NewSet[int](), fmt.Errorf("invalid string in range %s: %s", part, to)
			}
			if toI < fromI {
				return NewSet[int](), fmt.Errorf("invalid range given: %s", part)
			}
			for i := fromI; i <= toI; i++ {
				ret.Add(i)
			}
		} else {
			i, err := strconv.Atoi(part)
			if err != nil {
				return NewSet[int](), fmt.Errorf("invalid string given: %s", part)
			}
			ret.Add(i)
		}
	}
	return ret, nil
}


================================================
FILE: libgobuster/helpers_test.go
================================================
package libgobuster

import (
	"errors"
	"io"
	"os"
	"reflect"
	"strconv"
	"strings"
	"testing"
	"testing/iotest"
)

func TestNewSet(t *testing.T) {
	t.Parallel()
	if NewSet[string]().Set == nil {
		t.Fatal("NewSet[string] returned nil Set")
	}

	if NewSet[int]().Set == nil {
		t.Fatal("NewSet[int] returned nil Set")
	}
}

func TestSetAdd(t *testing.T) {
	t.Parallel()
	x := NewSet[string]()
	x.Add("test")
	if len(x.Set) != 1 {
		t.Fatalf("Unexpected string size. Should have 1 Got %v", len(x.Set))
	}

	y := NewSet[int]()
	y.Add(1)
	if len(y.Set) != 1 {
		t.Fatalf("Unexpected int size. Should have 1 Got %v", len(y.Set))
	}
}

func TestSetAddDouble(t *testing.T) {
	t.Parallel()
	x := NewSet[string]()
	x.Add("test")
	x.Add("test")
	if len(x.Set) != 1 {
		t.Fatalf("Unexpected string size. Should be 1 (unique) Got %v", len(x.Set))
	}

	y := NewSet[int]()
	y.Add(1)
	y.Add(1)
	if len(y.Set) != 1 {
		t.Fatalf("Unexpected int size. Should be 1 (unique) Got %v", len(y.Set))
	}
}

func TestSetAddRange(t *testing.T) {
	t.Parallel()
	x := NewSet[string]()
	x.AddRange([]string{"string1", "string2"})
	if len(x.Set) != 2 {
		t.Fatalf("Unexpected string size. Should have 2 Got %v", len(x.Set))
	}

	y := NewSet[int]()
	y.AddRange([]int{1, 2})
	if len(y.Set) != 2 {
		t.Fatalf("Unexpected int size. Should have 2 Got %v", len(y.Set))
	}
}

func TestSetAddRangeDouble(t *testing.T) {
	t.Parallel()
	x := NewSet[string]()
	x.AddRange([]string{"string1", "string2", "string1", "string2"})
	if len(x.Set) != 2 {
		t.Fatalf("Unexpected string size. Should be 2 (unique) Got %v", len(x.Set))
	}

	y := NewSet[int]()
	y.AddRange([]int{1, 2, 1, 2})
	if len(y.Set) != 2 {
		t.Fatalf("Unexpected int size. Should be 2 (unique) Got %v", len(y.Set))
	}
}

func TestSetContains(t *testing.T) {
	t.Parallel()
	x := NewSet[string]()
	v := []string{"string1", "string2", "1234", "5678"}
	x.AddRange(v)
	for _, i := range v {
		if !x.Contains(i) {
			t.Fatalf("Did not find value %s in array. %v", i, x.Set)
		}
	}

	y := NewSet[int]()
	v2 := []int{1, 2312, 123121, 999, -99}
	y.AddRange(v2)
	for _, i := range v2 {
		if !y.Contains(i) {
			t.Fatalf("Did not find value %d in array. %v", i, y.Set)
		}
	}
}

func TestSetContainsAny(t *testing.T) {
	t.Parallel()
	x := NewSet[string]()
	v := []string{"string1", "string2", "1234", "5678"}
	x.AddRange(v)
	if !x.ContainsAny(v) {
		t.Fatalf("Did not find any")
	}

	// test not found
	if x.ContainsAny([]string{"mmmm", "nnnnn"}) {
		t.Fatal("Found unexpected values")
	}

	y := NewSet[int]()
	v2 := []int{1, 2312, 123121, 999, -99}
	y.AddRange(v2)
	if !y.ContainsAny(v2) {
		t.Fatalf("Did not find any")
	}

	// test not found
	if y.ContainsAny([]int{9235, 2398532}) {
		t.Fatal("Found unexpected values")
	}
}

func TestSetStringify(t *testing.T) {
	t.Parallel()
	x := NewSet[string]()
	v := []string{"string1", "string2", "1234", "5678"}
	x.AddRange(v)
	z := x.Stringify()
	// order is random
	for _, i := range v {
		if !strings.Contains(z, i) {
			t.Fatalf("Did not find value %q in %q", i, z)
		}
	}

	y := NewSet[int]()
	v2 := []int{1, 2312, 123121, 999, -99}
	y.AddRange(v2)
	z = y.Stringify()
	// order is random
	for _, i := range v2 {
		if !strings.Contains(z, strconv.Itoa(i)) {
			t.Fatalf("Did not find value %q in %q", i, z)
		}
	}
}

func TestLineCounter(t *testing.T) {
	t.Parallel()
	tt := []struct {
		testName string
		s        string
		expected int
	}{
		{"One Line", "test", 1},
		{"3 Lines", "TestString\nTest\n1234", 3},
		{"Trailing newline", "TestString\nTest\n1234\n", 3},
		{"3 Lines cr lf", "TestString\r\nTest\r\n1234", 3},
		{"Empty", "", 1},       // these are wrong, but I've found no good way to handle those
		{"Empty 2", "\n", 1},   // these are wrong, but I've found no good way to handle those
		{"Empty 3", "\r\n", 1}, // these are wrong, but I've found no good way to handle those
	}
	for _, x := range tt {
		t.Run(x.testName, func(t *testing.T) {
			t.Parallel()
			r := strings.NewReader(x.s)
			l, err := lineCounter(r)
			if err != nil {
				t.Fatalf("Got error: %v", err)
			}
			if l != x.expected {
				t.Fatalf("wrong line count! Got %d expected %d", l, x.expected)
			}
		})
	}
}

func TestLineCounterSlow(t *testing.T) {
	t.Parallel()
	tt := []struct {
		testName string
		s        string
		expected int
	}{
		{"One Line", "test", 1},
		{"3 Lines", "TestString\nTest\n1234", 3},
		{"Trailing newline", "TestString\nTest\n1234\n", 3},
		{"3 Lines cr lf", "TestString\r\nTest\r\n1234", 3},
		{"Empty", "", 0},
		{"Empty 2", "\n", 0},
		{"Empty 3", "\r\n", 0},
	}
	for _, x := range tt {
		t.Run(x.testName, func(t *testing.T) {
			t.Parallel()
			r := strings.NewReader(x.s)
			l, err := lineCounterSlow(r)
			if err != nil {
				t.Fatalf("Got error: %v", err)
			}
			if l != x.expected {
				t.Fatalf("wrong line count! Got %d expected %d", l, x.expected)
			}
		})
	}
}

func BenchmarkLineCounter(b *testing.B) {
	r, err := os.Open("../rockyou.txt")
	if err != nil {
		b.Fatalf("Got error: %v", err)
	}
	defer r.Close()
	for b.Loop() {
		_, err := r.Seek(0, io.SeekStart)
		if err != nil {
			b.Fatalf("Got error: %v", err)
		}
		c, err := lineCounter(r)
		if err != nil {
			b.Fatalf("Got error: %v", err)
		}
		if c != 14344391 {
			b.Errorf("invalid count. Expected 14344391, got %d", c)
		}
	}
}

func BenchmarkLineCounterSlow(b *testing.B) {
	r, err := os.Open("../rockyou.txt")
	if err != nil {
		b.Fatalf("Got error: %v", err)
	}
	defer r.Close()
	for b.Loop() {
		_, err := r.Seek(0, io.SeekStart)
		if err != nil {
			b.Fatalf("Got error: %v", err)
		}
		c, err := lineCounterSlow(r)
		if err != nil {
			b.Fatalf("Got error: %v", err)
		}
		if c != 14336792 {
			b.Errorf("invalid count. Expected 14336792, got %d", c)
		}
	}
}

func TestLineCounterError(t *testing.T) {
	t.Parallel()
	r := iotest.TimeoutReader(strings.NewReader("test"))
	_, err := lineCounter(r)
	if !errors.Is(err, iotest.ErrTimeout) {
		t.Fatalf("Got wrong error! %v", err)
	}
}

func TestParseExtensions(t *testing.T) {
	t.Parallel()
	tt := []struct {
		testName           string
		extensions         string
		expectedExtensions Set[string]
		expectedError      string
	}{
		{"Valid extensions", "php,asp,txt", Set[string]{Set: map[string]bool{"php": true, "asp": true, "txt": true}}, ""},
		{"Spaces", "php, asp , txt", Set[string]{Set: map[string]bool{"php": true, "asp": true, "txt": true}}, ""},
		{"Double extensions", "php,asp,txt,php,asp,txt", Set[string]{Set: map[string]bool{"php": true, "asp": true, "txt": true}}, ""},
		{"Leading dot", ".php,asp,.txt", Set[string]{Set: map[string]bool{"php": true, "asp": true, "txt": true}}, ""},
		{"Empty string", "", NewSet[string](), "invalid extension string provided"},
	}

	for _, x := range tt {
		t.Run(x.testName, func(t *testing.T) {
			t.Parallel()
			ret, err := ParseExtensions(x.extensions)
			if x.expectedError != "" {
				if err != nil && err.Error() != x.expectedError {
					t.Fatalf("Expected error %q but got %q", x.expectedError, err.Error())
				}
			} else if !reflect.DeepEqual(x.expectedExtensions, ret) {
				t.Fatalf("Expected %v but got %v", x.expectedExtensions, ret)
			}
		})
	}
}

func TestParseCommaSeparatedInt(t *testing.T) {
	t.Parallel()
	tt := []struct {
		stringCodes   string
		expectedCodes []int
		expectedError string
	}{
		{"200,100,202", []int{200, 100, 202}, ""},
		{"200, 100 , 202", []int{200, 100, 202}, ""},
		{"200, 100, 202, 100", []int{200, 100, 202}, ""},
		{"200,AAA", []int{}, "invalid string given: AAA"},
		{"2000000000000000000000000000000", []int{}, "invalid string given: 2000000000000000000000000000000"},
		{"", []int{}, "invalid string provided"},
		{"200-205", []int{200, 201, 202, 203, 204, 205}, ""},
		{"200-202,203-205", []int{200, 201, 202, 203, 204, 205}, ""},
		{"200-202,204-205", []int{200, 201, 202, 204, 205}, ""},
		{"200-202,205", []int{200, 201, 202, 205}, ""},
		{"205,200,100-101,103-105", []int{100, 101, 103, 104, 105, 200, 205}, ""},
		{"200-200", []int{200}, ""},
		{"200 - 202", []int{200, 201, 202}, ""},
		{"200 -202", []int{200, 201, 202}, ""},
		{"200- 202", []int{200, 201, 202}, ""},
		{"200              -                202", []int{200, 201, 202}, ""},
		{"230-200", []int{}, "invalid range given: 230-200"},
		{"A-200", []int{}, "invalid range given: A-200"},
		{"230-A", []int{}, "invalid range given: 230-A"},
		{"200,202-205,A,206-210", []int{}, "invalid string given: A"},
		{"200,202-205,A-1,206-210", []int{}, "invalid range given: A-1"},
		{"200,202-205,1-A,206-210", []int{}, "invalid range given: 1-A"},
	}

	for _, x := range tt {
		t.Run(x.stringCodes, func(t *testing.T) {
			t.Parallel()
			want := NewSet[int]()
			want.AddRange(x.expectedCodes)
			ret, err := ParseCommaSeparatedInt(x.stringCodes)
			if x.expectedError != "" {
				if err != nil && err.Error() != x.expectedError {
					t.Fatalf("Expected error %q but got %q", x.expectedError, err.Error())
				}
			} else if !reflect.DeepEqual(want, ret) {
				t.Fatalf("Expected %v but got %v", want, ret)
			}
		})
	}
}

func BenchmarkParseExtensions(b *testing.B) {
	tt := []struct {
		testName           string
		extensions         string
		expectedExtensions Set[string]
		expectedError      string
	}{
		{"Valid extensions", "php,asp,txt", Set[string]{Set: map[string]bool{"php": true, "asp": true, "txt": true}}, ""},
		{"Spaces", "php, asp , txt", Set[string]{Set: map[string]bool{"php": true, "asp": true, "txt": true}}, ""},
		{"Double extensions", "php,asp,txt,php,asp,txt", Set[string]{Set: map[string]bool{"php": true, "asp": true, "txt": true}}, ""},
		{"Leading dot", ".php,asp,.txt", Set[string]{Set: map[string]bool{"php": true, "asp": true, "txt": true}}, ""},
		{"Empty string", "", NewSet[string](), "invalid extension string provided"},
	}

	for _, x := range tt {
		b.Run(x.testName, func(b2 *testing.B) {
			for b2.Loop() {
				_, _ = ParseExtensions(x.extensions)
			}
		})
	}
}

func BenchmarkParseCommaSeparatedInt(b *testing.B) {
	tt := []struct {
		testName      string
		stringCodes   string
		expectedCodes Set[int]
		expectedError string
	}{
		{"Valid codes", "200,100,202", Set[int]{Set: map[int]bool{100: true, 200: true, 202: true}}, ""},
		{"Spaces", "200, 100 , 202", Set[int]{Set: map[int]bool{100: true, 200: true, 202: true}}, ""},
		{"Double codes", "200, 100, 202, 100", Set[int]{Set: map[int]bool{100: true, 200: true, 202: true}}, ""},
		{"Invalid code", "200,AAA", NewSet[int](), "invalid string given: AAA"},
		{"Invalid integer", "2000000000000000000000000000000", NewSet[int](), "invalid string given: 2000000000000000000000000000000"},
		{"Empty string", "", NewSet[int](), "invalid string string provided"},
	}

	for _, x := range tt {
		b.Run(x.testName, func(b2 *testing.B) {
			for b2.Loop() {
				_, _ = ParseCommaSeparatedInt(x.stringCodes)
			}
		})
	}
}


================================================
FILE: libgobuster/http.go
================================================
package libgobuster

import (
	"context"
	"crypto/tls"
	"errors"
	"fmt"
	"io"
	"net"
	"net/http"
	"net/http/httputil"
	"net/url"
	"strings"
)

// HTTPHeader holds a single key value pair of a HTTP header
type HTTPHeader struct {
	Name  string
	Value string
}

// HTTPClient represents a http object
type HTTPClient struct {
	client                *http.Client
	userAgent             string
	defaultUserAgent      string
	username              string
	password              string
	headers               []HTTPHeader
	noCanonicalizeHeaders bool
	cookies               string
	method                string
	host                  string
	logger                *Logger
}

// RequestOptions is used to pass options to a single individual request
type RequestOptions struct {
	Host                     string
	Body                     io.Reader
	ReturnBody               bool
	ModifiedHeaders          []HTTPHeader
	UpdatedBasicAuthUsername string
	UpdatedBasicAuthPassword string
}

// NewHTTPClient returns a new HTTPClient
func NewHTTPClient(opt *HTTPOptions, logger *Logger) (*HTTPClient, error) {
	var proxyURLFunc func(*http.Request) (*url.URL, error)
	var client HTTPClient
	proxyURLFunc = http.ProxyFromEnvironment

	if opt == nil {
		return nil, errors.New("options is nil")
	}

	if opt.Proxy != "" {
		proxyURL, err := url.Parse(opt.Proxy)
		if err != nil {
			return nil, fmt.Errorf("proxy URL is invalid (%w)", err)
		}
		proxyURLFunc = http.ProxyURL(proxyURL)
	}

	var redirectFunc func(req *http.Request, via []*http.Request) error
	if !opt.FollowRedirect {
		redirectFunc = func(_ *http.Request, _ []*http.Request) error {
			return http.ErrUseLastResponse
		}
	} else {
		redirectFunc = nil
	}

	tlsConfig := tls.Config{
		InsecureSkipVerify: opt.NoTLSValidation, // nolint:gosec
		// enable TLS1.0 and TLS1.1 support
		MinVersion: tls.VersionTLS10,
	}
	if opt.TLSCertificate != nil {
		tlsConfig.Certificates = []tls.Certificate{*opt.TLSCertificate}
	}
	if opt.TLSRenegotiation {
		tlsConfig.Renegotiation = tls.RenegotiateOnceAsClient
	}

	transport := &http.Transport{
		Proxy:               proxyURLFunc,
		MaxIdleConns:        100,
		MaxIdleConnsPerHost: 100,
		TLSClientConfig:     &tlsConfig,
	}

	// set specific network interface
	if opt.LocalAddr != nil {
		logger.Debugf("Setting local address to %s", opt.LocalAddr.String())
		dialer := &net.Dialer{
			Timeout:   opt.Timeout,
			LocalAddr: opt.LocalAddr,
		}
		transport.DialContext = dialer.DialContext
	}

	client.client = &http.Client{
		Timeout:       opt.Timeout,
		CheckRedirect: redirectFunc,
		Transport:     transport,
	}

	client.username = opt.Username
	client.password = opt.Password
	client.userAgent = opt.UserAgent
	client.defaultUserAgent = DefaultUserAgent()
	client.headers = opt.Headers
	client.noCanonicalizeHeaders = opt.NoCanonicalizeHeaders
	client.cookies = opt.Cookies
	client.method = opt.Method
	if client.method == "" {
		client.method = http.MethodGet
	}
	// Host header needs to be set separately
	for _, h := range opt.Headers {
		if h.Name == "Host" {
			client.host = h.Value
			break
		}
	}
	client.logger = logger
	return &client, nil
}

// Request makes a http request and returns the status, the content length, the headers, the body and an error
// if you want the body returned set the corresponding property inside RequestOptions
func (client *HTTPClient) Request(ctx context.Context, fullURL url.URL, opts RequestOptions) (int, int64, http.Header, []byte, error) {
	resp, err := client.makeRequest(ctx, fullURL, opts)
	if err != nil {
		// ignore context canceled errors
		if errors.Is(ctx.Err(), context.Canceled) {
			return 0, 0, nil, nil, nil
		}
		return 0, 0, nil, nil, err
	}
	defer resp.Body.Close()

	var body []byte
	var length int64
	if opts.ReturnBody {
		body, err = io.ReadAll(resp.Body)
		if err != nil {
			return 0, 0, nil, nil, fmt.Errorf("could not read body %w", err)
		}
		length = int64(len(body))
	} else {
		// DO NOT REMOVE!
		// absolutely needed so golang will reuse connections!
		length, err = io.Copy(io.Discard, resp.Body)
		if err != nil {
			return 0, 0, nil, nil, err
		}
	}

	return resp.StatusCode, length, resp.Header, body, nil
}

func (client *HTTPClient) makeRequest(ctx context.Context, fullURL url.URL, opts RequestOptions) (*http.Response, error) {
	req, err := http.NewRequestWithContext(ctx, client.method, fullURL.String(), opts.Body)
	if err != nil {
		return nil, err
	}

	if client.cookies != "" {
		req.Header.Set("Cookie", client.cookies)
	}

	// Use host for VHOST mode on a per-request basis, otherwise the one provided from headers
	if opts.Host != "" {
		req.Host = opts.Host
	} else if client.host != "" {
		req.Host = client.host
	}

	if client.userAgent != "" {
		req.Header.Set("User-Agent", client.userAgent)
	} else {
		req.Header.Set("User-Agent", client.defaultUserAgent)
	}

	// add custom headers
	// if ModifiedHeaders are supplied use those, otherwise use the original ones
	// currently only relevant on fuzzing
	if len(opts.ModifiedHeaders) > 0 {
		for _, h := range opts.ModifiedHeaders {
			// empty headers are not valid (happens when fuzzing the host header for example because the slice is initialized with the provided header length)
			if h.Name == "" {
				continue
			}

			if client.noCanonicalizeHeaders {
				// https://stackoverflow.com/questions/26351716/how-to-keep-key-case-sensitive-in-request-header-using-golang
				req.Header[h.Name] = []string{h.Value}
			} else {
				req.Header.Set(h.Name, h.Value)
			}
		}
	} else {
		for _, h := range client.headers {
			if client.noCanonicalizeHeaders {
				// https://stackoverflow.com/questions/26351716/how-to-keep-key-case-sensitive-in-request-header-using-golang
				req.Header[h.Name] = []string{h.Value}
			} else {
				req.Header.Set(h.Name, h.Value)
			}
		}
	}

	if opts.UpdatedBasicAuthUsername != "" {
		req.SetBasicAuth(opts.UpdatedBasicAuthUsername, opts.UpdatedBasicAuthPassword)
	} else if client.username != "" {
		req.SetBasicAuth(client.username, client.password)
	}

	if client.logger.debug {
		dump, err := httputil.DumpRequestOut(req, false)
		if err != nil {
			return nil, err
		}
		client.logger.Debugf("%s", dump)
	}

	resp, err := client.client.Do(req)
	if err != nil {
		var ue *url.Error
		if errors.As(err, &ue) {
			if strings.HasPrefix(ue.Err.Error(), "x509") {
				return nil, fmt.Errorf("invalid certificate: %w", ue.Err)
			}
		}
		return nil, err
	}

	return resp, nil
}


================================================
FILE: libgobuster/http_test.go
================================================
package libgobuster

import (
	"bytes"
	"crypto/rand"
	"fmt"
	"math/big"
	"net/http"
	"net/http/httptest"
	"net/url"
	"testing"
)

func httpServerB(b *testing.B, content string) *httptest.Server {
	b.Helper()
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		if _, err := fmt.Fprint(w, content); err != nil {
			b.Fatalf("%v", err)
		}
	}))
	return ts
}

func httpServerT(t *testing.T, content string) *httptest.Server {
	t.Helper()
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		if _, err := fmt.Fprint(w, content); err != nil {
			t.Fatalf("%v", err)
		}
	}))
	return ts
}

func randomString(length int) (string, error) {
	letter := []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
	letterLen := len(letter)

	b := make([]byte, length)
	for i := range b {
		n, err := rand.Int(rand.Reader, big.NewInt(int64(letterLen)))
		if err != nil {
			return "", err
		}
		b[i] = letter[n.Int64()]
	}
	return string(b), nil
}

func TestRequest(t *testing.T) {
	t.Parallel()
	ret, err := randomString(100)
	if err != nil {
		t.Fatal(err)
	}
	h := httpServerT(t, ret)
	defer h.Close()
	var o HTTPOptions
	log := NewLogger(false)
	c, err := NewHTTPClient(&o, log)
	if err != nil {
		t.Fatalf("Got Error: %v", err)
	}
	u, err := url.Parse(h.URL)
	if err != nil {
		t.Fatalf("could not parse URL %v: %v", h.URL, err)
	}
	status, length, _, body, err := c.Request(t.Context(), *u, RequestOptions{ReturnBody: true})
	if err != nil {
		t.Fatalf("Got Error: %v", err)
	}
	if status != 200 {
		t.Fatalf("Invalid status returned: %d", status)
	}
	if length != int64(len(ret)) {
		t.Fatalf("Invalid length returned: %d", length)
	}
	if body == nil || !bytes.Equal(body, []byte(ret)) {
		t.Fatalf("Invalid body returned: %d", body)
	}
}

func BenchmarkRequestWithoutBody(b *testing.B) {
	r, err := randomString(10000)
	if err != nil {
		b.Fatal(err)
	}
	h := httpServerB(b, r)
	defer h.Close()
	var o HTTPOptions
	log := NewLogger(false)
	c, err := NewHTTPClient(&o, log)
	if err != nil {
		b.Fatalf("Got Error: %v", err)
	}
	u, err := url.Parse(h.URL)
	if err != nil {
		b.Fatalf("could not parse URL %v: %v", h.URL, err)
	}
	for b.Loop() {
		_, _, _, _, err := c.Request(b.Context(), *u, RequestOptions{ReturnBody: false})
		if err != nil {
			b.Fatalf("Got Error: %v", err)
		}
	}
}

func BenchmarkRequestWitBody(b *testing.B) {
	r, err := randomString(10000)
	if err != nil {
		b.Fatal(err)
	}
	h := httpServerB(b, r)
	defer h.Close()
	var o HTTPOptions
	log := NewLogger(false)
	c, err := NewHTTPClient(&o, log)
	if err != nil {
		b.Fatalf("Got Error: %v", err)
	}
	u, err := url.Parse(h.URL)
	if err != nil {
		b.Fatalf("could not parse URL %v: %v", h.URL, err)
	}
	for b.Loop() {
		_, _, _, _, err := c.Request(b.Context(), *u, RequestOptions{ReturnBody: true})
		if err != nil {
			b.Fatalf("Got Error: %v", err)
		}
	}
}

func BenchmarkNewHTTPClient(b *testing.B) {
	r, err := randomString(500)
	if err != nil {
		b.Fatal(err)
	}
	h := httpServerB(b, r)
	defer h.Close()
	var o HTTPOptions
	log := NewLogger(false)
	for b.Loop() {
		_, err := NewHTTPClient(&o, log)
		if err != nil {
			b.Fatalf("Got Error: %v", err)
		}
	}
}


================================================
FILE: libgobuster/interfaces.go
================================================
package libgobuster

import "context"

// GobusterPlugin is an interface which plugins must implement
type GobusterPlugin interface {
	Name() string
	PreRun(context.Context, *Progress) error
	ProcessWord(context.Context, string, *Progress) (Result, error)
	AdditionalWords(string) []string
	AdditionalWordsLen() int
	AdditionalSuccessWords(string) []string
	GetConfigString() (string, error)
}

// Result is an interface for the Result object
type Result interface {
	ResultToString() (string, error)
}


================================================
FILE: libgobuster/libgobuster.go
================================================
package libgobuster

import (
	"bufio"
	"context"
	"errors"
	"fmt"
	"io"
	"os"
	"strings"
	"sync"
	"time"
)

// PATTERN is the pattern for wordlist replacements in pattern file
const PATTERN = "{GOBUSTER}"

// SetupFunc is the "setup" function prototype for implementations
type SetupFunc func(*Gobuster) error

// ProcessFunc is the "process" function prototype for implementations
type ProcessFunc func(*Gobuster, string) ([]Result, error)

// ResultToStringFunc is the "to string" function prototype for implementations
type ResultToStringFunc func(*Gobuster, *Result) (*string, error)

// Gobuster is the main object when creating a new run
type Gobuster struct {
	Opts     *Options
	Logger   *Logger
	plugin   GobusterPlugin
	Progress *Progress
}

type Guess struct {
	word              string
	discoverOnSuccess bool
}

type Wordlist struct {
	scanner        *bufio.Scanner
	guessesPerLine int
	isStream       bool
}

// NewGobuster returns a new Gobuster object
func NewGobuster(opts *Options, plugin GobusterPlugin, logger *Logger) (*Gobuster, error) {
	var g Gobuster
	g.Opts = opts
	g.plugin = plugin
	g.Logger = logger
	g.Progress = NewProgress()

	return &g, nil
}

func (g *Gobuster) worker(ctx context.Context, guessChan <-chan *Guess, successChan chan<- *Guess, wg *sync.WaitGroup) {
	defer wg.Done()
	for {
		// Prioritize stopping when the context is done
		select {
		case <-ctx.Done():
			return
		default:
		}
		select {
		case <-ctx.Done():
			return
		case guess := <-guessChan:

			// Mode-specific processing
			res, err := g.plugin.ProcessWord(ctx, guess.word, g.Progress)
			if err != nil {
				// do not exit and continue
				g.Progress.ErrorChan <- fmt.Errorf("error on word %s: %w", guess.word, err)
			}

			if res != nil {
				g.Progress.ResultChan <- res

				select {
				case <-ctx.Done():
					g.Progress.incrementRequests()
					return
				case successChan <- guess:
				}
			}

			g.Progress.incrementRequests()

			select {
			case <-ctx.Done():
			case <-time.After(g.Opts.Delay):
			}
		}
	}
}

func feed(ctx context.Context, guessChan chan<- *Guess, words []string, discoverOnSuccess bool) {
	for _, w := range words {
		guess := &Guess{word: w, discoverOnSuccess: discoverOnSuccess}
		// Prioritize stopping when the context is done
		select {
		case <-ctx.Done():
			return
		default:
		}
		select {
		// need to check here too otherwise guessChan will block
		case <-ctx.Done():
			return
		case guessChan <- guess:
		}
	}
}

func (g *Gobuster) feeder(ctx context.Context, guessChan chan<- *Guess, words []string, discoverOnSuccess bool, wg *sync.WaitGroup) {
	defer wg.Done()

	feed(ctx, guessChan, words, discoverOnSuccess)
}

func (g *Gobuster) feedWordlist(ctx context.Context, guessChan chan<- *Guess, wordlist *Wordlist, wg *sync.WaitGroup) {
	defer wg.Done()

	for wordlist.scanner.Scan() {
		// Prioritize stopping when the context is done
		select {
		case <-ctx.Done():
			return
		default:
		}

		word := strings.TrimSpace(wordlist.scanner.Text())

		switch {
		case wordlist.isStream && len(word) != 0:
			// Increment to keep track of expected work
			g.Progress.IncrementTotalRequests(wordlist.guessesPerLine)
		case wordlist.isStream && len(word) == 0:
			// Skip empty lines without incrementing
			continue
		case len(word) == 0:
			// Skip empty lines removing expected work
			g.Progress.IncrementTotalRequests(-1 * wordlist.guessesPerLine)
			continue
		}

		if len(g.Opts.Patterns) > 0 {
			for _, w := range g.processPatterns(word) {
				guess := &Guess{word: w, discoverOnSuccess: true}
				select {
				case <-ctx.Done():
					return
				case guessChan <- guess:
				}

				feed(ctx, guessChan, g.plugin.AdditionalWords(w), true)
			}
		} else {
			guess := &Guess{word: word, discoverOnSuccess: true}

			select {
			case <-ctx.Done():
				return
			case guessChan <- guess:
			}

			feed(ctx, guessChan, g.plugin.AdditionalWords(word), true)
		}
	}
}

func (g *Gobuster) getWordlist(wordlist io.ReadSeeker) (*Wordlist, error) {
	// calculate expected requests
	var guessesPerLine int
	if len(g.Opts.Patterns) > 0 {
		nPats := len(g.Opts.Patterns)
		guessesPerLine = nPats + nPats*g.plugin.AdditionalWordsLen()
	} else {
		guessesPerLine = 1 + g.plugin.AdditionalWordsLen()
	}

	if g.Opts.Wordlist == "-" {
		// Read directly from stdin
		return &Wordlist{scanner: bufio.NewScanner(os.Stdin), guessesPerLine: guessesPerLine, isStream: true}, nil
	}

	lines, err := lineCounter(wordlist)
	if err != nil {
		return nil, fmt.Errorf("failed to
Download .txt
gitextract_sdpr052u/

├── .devcontainer/
│   └── devcontainer.json
├── .dockerignore
├── .gitattributes
├── .github/
│   ├── FUNDING.yml
│   ├── dependabot.yml
│   └── workflows/
│       ├── auto-merge-dependabot.yml
│       ├── docker.yml
│       ├── go.yml
│       ├── golangci-lint.yml
│       ├── hadolint.yml
│       ├── release.yml
│       ├── update.yml
│       ├── vhs.yml
│       └── yamllint.yml
├── .gitignore
├── .golangci.yml
├── .goreleaser.yaml
├── .yamllint.yml
├── Dockerfile
├── LICENSE
├── README.md
├── Taskfile.yml
├── cli/
│   ├── const.go
│   ├── const_windows.go
│   ├── dir/
│   │   ├── dir.go
│   │   └── dir_test.go
│   ├── dns/
│   │   └── dns.go
│   ├── fuzz/
│   │   └── fuzz.go
│   ├── gcs/
│   │   └── gcs.go
│   ├── gobuster.go
│   ├── options.go
│   ├── s3/
│   │   └── s3.go
│   ├── tftp/
│   │   └── tftp.go
│   └── vhost/
│       ├── vhost.go
│       └── vhost_test.go
├── go.mod
├── go.sum
├── gobusterdir/
│   ├── gobusterdir.go
│   ├── gobusterdir_test.go
│   ├── options.go
│   ├── options_test.go
│   └── result.go
├── gobusterdns/
│   ├── gobusterdns.go
│   ├── options.go
│   └── result.go
├── gobusterfuzz/
│   ├── gobusterfuzz.go
│   ├── options.go
│   ├── options_test.go
│   └── result.go
├── gobustergcs/
│   ├── gobustersgcs.go
│   ├── options.go
│   ├── result.go
│   └── types.go
├── gobusters3/
│   ├── gobusters3.go
│   ├── options.go
│   ├── result.go
│   └── types.go
├── gobustertftp/
│   ├── gobustertftp.go
│   ├── options.go
│   └── result.go
├── gobustervhost/
│   ├── gobustervhost.go
│   ├── options.go
│   └── result.go
├── libgobuster/
│   ├── errors.go
│   ├── helpers.go
│   ├── helpers_test.go
│   ├── http.go
│   ├── http_test.go
│   ├── interfaces.go
│   ├── libgobuster.go
│   ├── logger.go
│   ├── options.go
│   ├── options_http.go
│   ├── progress.go
│   ├── useragents.go
│   └── version.go
├── main.go
└── vhs/
    ├── gobuster_dir.tape
    └── server.go
Download .txt
SYMBOL INDEX (260 symbols across 53 files)

FILE: cli/const.go
  constant TerminalClearLine (line 6) | TerminalClearLine = "\r\x1b[2K"

FILE: cli/const_windows.go
  constant TerminalClearLine (line 6) | TerminalClearLine = "\r\r"

FILE: cli/dir/dir.go
  function Command (line 13) | func Command() *cli.Command {
  function getFlags (line 23) | func getFlags() []cli.Flag {
  function run (line 43) | func run(c *cli.Context) error {

FILE: cli/dir/dir_test.go
  function httpServer (line 17) | func httpServer(b *testing.B, content string) *httptest.Server {
  function BenchmarkDirMode (line 27) | func BenchmarkDirMode(b *testing.B) {

FILE: cli/dns/dns.go
  function Command (line 15) | func Command() *cli.Command {
  function getFlags (line 25) | func getFlags() []cli.Flag {
  function run (line 40) | func run(c *cli.Context) error {

FILE: cli/fuzz/fuzz.go
  function Command (line 13) | func Command() *cli.Command {
  function getFlags (line 23) | func getFlags() []cli.Flag {
  function run (line 36) | func run(c *cli.Context) error {
  function containsFuzzKeyword (line 84) | func containsFuzzKeyword(pluginopts gobusterfuzz.OptionsFuzz) bool {

FILE: cli/gcs/gcs.go
  function Command (line 12) | func Command() *cli.Command {
  function getFlags (line 22) | func getFlags() []cli.Flag {
  function run (line 33) | func run(c *cli.Context) error {

FILE: cli/gobuster.go
  constant ruler (line 17) | ruler             = "===================================================...
  constant cliProgressUpdate (line 18) | cliProgressUpdate = 500 * time.Millisecond
  function resultWorker (line 23) | func resultWorker(g *libgobuster.Gobuster, filename string, wg *sync.Wai...
  function errorWorker (line 61) | func errorWorker(g *libgobuster.Gobuster, wg *sync.WaitGroup) {
  function messageWorker (line 74) | func messageWorker(g *libgobuster.Gobuster, wg *sync.WaitGroup) {
  function printProgress (line 95) | func printProgress(g *libgobuster.Gobuster) {
  function progressWorker (line 111) | func progressWorker(ctx context.Context, g *libgobuster.Gobuster, wg *sy...
  function writeToFile (line 129) | func writeToFile(f *os.File, output string) error {
  function Gobuster (line 138) | func Gobuster(ctx context.Context, opts *libgobuster.Options, plugin lib...

FILE: cli/options.go
  function BasicHTTPOptions (line 24) | func BasicHTTPOptions() []cli.Flag {
  function ParseBasicHTTPOptions (line 43) | func ParseBasicHTTPOptions(c *cli.Context) (libgobuster.BasicHTTPOptions...
  function CommonHTTPOptions (line 122) | func CommonHTTPOptions() []cli.Flag {
  function ParseCommonHTTPOptions (line 138) | func ParseCommonHTTPOptions(c *cli.Context) (libgobuster.HTTPOptions, er...
  function GlobalOptions (line 215) | func GlobalOptions() []cli.Flag {
  function ParseGlobalOptions (line 232) | func ParseGlobalOptions(c *cli.Context) (libgobuster.Options, error) {
  function getLocalAddrFromInterface (line 303) | func getLocalAddrFromInterface(iface string) (*net.TCPAddr, error) {

FILE: cli/s3/s3.go
  function Command (line 12) | func Command() *cli.Command {
  function getFlags (line 22) | func getFlags() []cli.Flag {
  function run (line 33) | func run(c *cli.Context) error {

FILE: cli/tftp/tftp.go
  function Command (line 14) | func Command() *cli.Command {
  function getFlags (line 24) | func getFlags() []cli.Flag {
  function run (line 34) | func run(c *cli.Context) error {

FILE: cli/vhost/vhost.go
  function Command (line 14) | func Command() *cli.Command {
  function getFlags (line 24) | func getFlags() []cli.Flag {
  function run (line 40) | func run(c *cli.Context) error {

FILE: cli/vhost/vhost_test.go
  function httpServer (line 17) | func httpServer(b *testing.B, content string) *httptest.Server {
  function BenchmarkVhostMode (line 27) | func BenchmarkVhostMode(b *testing.B) {

FILE: gobusterdir/gobusterdir.go
  type WildcardError (line 28) | type WildcardError struct
    method Error (line 36) | func (e *WildcardError) Error() string {
  type GobusterDir (line 47) | type GobusterDir struct
    method Name (line 101) | func (d *GobusterDir) Name() string {
    method PreRun (line 106) | func (d *GobusterDir) PreRun(ctx context.Context, pr *libgobuster.Prog...
    method AdditionalSuccessWords (line 186) | func (d *GobusterDir) AdditionalSuccessWords(word string) []string {
    method AdditionalWordsLen (line 205) | func (d *GobusterDir) AdditionalWordsLen() int {
    method AdditionalWords (line 209) | func (d *GobusterDir) AdditionalWords(word string) []string {
    method ProcessWord (line 222) | func (d *GobusterDir) ProcessWord(ctx context.Context, word string, pr...
    method GetConfigString (line 326) | func (d *GobusterDir) GetConfigString() (string, error) {
  function New (line 54) | func New(globalopts *libgobuster.Options, opts *OptionsDir, logger *libg...

FILE: gobusterdir/gobusterdir_test.go
  function TestAdditionalWordsLen (line 9) | func TestAdditionalWordsLen(t *testing.T) {

FILE: gobusterdir/options.go
  type OptionsDir (line 8) | type OptionsDir struct
  function NewOptions (line 28) | func NewOptions() *OptionsDir {

FILE: gobusterdir/options_test.go
  function TestNewOptions (line 5) | func TestNewOptions(t *testing.T) {

FILE: gobusterdir/result.go
  type Result (line 21) | type Result struct
    method ResultToString (line 29) | func (r Result) ResultToString() (string, error) {

FILE: gobusterdns/gobusterdns.go
  type WildcardError (line 19) | type WildcardError struct
    method Error (line 24) | func (e *WildcardError) Error() string {
  type GobusterDNS (line 29) | type GobusterDNS struct
    method Name (line 75) | func (d *GobusterDNS) Name() string {
    method PreRun (line 80) | func (d *GobusterDNS) PreRun(ctx context.Context, progress *libgobuste...
    method ProcessWord (line 112) | func (d *GobusterDNS) ProcessWord(ctx context.Context, word string, pr...
    method AdditionalWordsLen (line 160) | func (d *GobusterDNS) AdditionalWordsLen() int {
    method AdditionalWords (line 164) | func (d *GobusterDNS) AdditionalWords(_ string) []string {
    method AdditionalSuccessWords (line 168) | func (d *GobusterDNS) AdditionalSuccessWords(_ string) []string {
    method GetConfigString (line 173) | func (d *GobusterDNS) GetConfigString() (string, error) {
    method dnsLookup (line 240) | func (d *GobusterDNS) dnsLookup(ctx context.Context, domain string) ([...
    method dnsLookupCname (line 246) | func (d *GobusterDNS) dnsLookupCname(ctx context.Context, domain strin...
  function newCustomDialer (line 37) | func newCustomDialer(server string, protocol string) func(ctx context.Co...
  function New (line 48) | func New(globalopts *libgobuster.Options, opts *OptionsDNS) (*GobusterDN...

FILE: gobusterdns/options.go
  type OptionsDNS (line 8) | type OptionsDNS struct
  function NewOptions (line 19) | func NewOptions() *OptionsDNS {

FILE: gobusterdns/result.go
  type Result (line 15) | type Result struct
    method ResultToString (line 22) | func (r Result) ResultToString() (string, error) {

FILE: gobusterfuzz/gobusterfuzz.go
  constant FuzzKeyword (line 19) | FuzzKeyword = "FUZZ"
  type WildcardError (line 22) | type WildcardError struct
    method Error (line 28) | func (e *WildcardError) Error() string {
  type GobusterFuzz (line 33) | type GobusterFuzz struct
    method Name (line 84) | func (d *GobusterFuzz) Name() string {
    method PreRun (line 89) | func (d *GobusterFuzz) PreRun(_ context.Context, _ *libgobuster.Progre...
    method ProcessWord (line 94) | func (d *GobusterFuzz) ProcessWord(ctx context.Context, word string, p...
    method AdditionalWordsLen (line 215) | func (d *GobusterFuzz) AdditionalWordsLen() int {
    method AdditionalWords (line 219) | func (d *GobusterFuzz) AdditionalWords(_ string) []string {
    method AdditionalSuccessWords (line 223) | func (d *GobusterFuzz) AdditionalSuccessWords(_ string) []string {
    method GetConfigString (line 228) | func (d *GobusterFuzz) GetConfigString() (string, error) {
  function New (line 40) | func New(globalopts *libgobuster.Options, opts *OptionsFuzz, logger *lib...

FILE: gobusterfuzz/options.go
  type OptionsFuzz (line 8) | type OptionsFuzz struct
  function NewOptions (line 18) | func NewOptions() *OptionsFuzz {

FILE: gobusterfuzz/options_test.go
  function TestNewOptions (line 5) | func TestNewOptions(t *testing.T) {

FILE: gobusterfuzz/result.go
  type Result (line 17) | type Result struct
    method ResultToString (line 26) | func (r Result) ResultToString() (string, error) {

FILE: gobustergcs/gobustersgcs.go
  type GobusterGCS (line 23) | type GobusterGCS struct
    method Name (line 73) | func (s *GobusterGCS) Name() string {
    method PreRun (line 78) | func (s *GobusterGCS) PreRun(_ context.Context, _ *libgobuster.Progres...
    method ProcessWord (line 83) | func (s *GobusterGCS) ProcessWord(ctx context.Context, word string, pr...
    method AdditionalWordsLen (line 216) | func (s *GobusterGCS) AdditionalWordsLen() int {
    method AdditionalWords (line 220) | func (s *GobusterGCS) AdditionalWords(_ string) []string {
    method AdditionalSuccessWords (line 224) | func (s *GobusterGCS) AdditionalSuccessWords(_ string) []string {
    method GetConfigString (line 229) | func (s *GobusterGCS) GetConfigString() (string, error) {
    method isValidBucketName (line 303) | func (s *GobusterGCS) isValidBucketName(bucketName string) bool {
  function New (line 31) | func New(globalopts *libgobuster.Options, opts *OptionsGCS, logger *libg...

FILE: gobustergcs/options.go
  type OptionsGCS (line 8) | type OptionsGCS struct
  function NewOptions (line 15) | func NewOptions() *OptionsGCS {

FILE: gobustergcs/result.go
  type Result (line 12) | type Result struct
    method ResultToString (line 19) | func (r Result) ResultToString() (string, error) {

FILE: gobustergcs/types.go
  type GCSError (line 4) | type GCSError struct
  type GCSListing (line 18) | type GCSListing struct

FILE: gobusters3/gobusters3.go
  type GobusterS3 (line 23) | type GobusterS3 struct
    method Name (line 72) | func (s *GobusterS3) Name() string {
    method PreRun (line 77) | func (s *GobusterS3) PreRun(_ context.Context, _ *libgobuster.Progress...
    method ProcessWord (line 82) | func (s *GobusterS3) ProcessWord(ctx context.Context, word string, pro...
    method AdditionalWordsLen (line 207) | func (s *GobusterS3) AdditionalWordsLen() int {
    method AdditionalWords (line 211) | func (s *GobusterS3) AdditionalWords(_ string) []string {
    method AdditionalSuccessWords (line 215) | func (s *GobusterS3) AdditionalSuccessWords(_ string) []string {
    method GetConfigString (line 220) | func (s *GobusterS3) GetConfigString() (string, error) {
    method isValidBucketName (line 294) | func (s *GobusterS3) isValidBucketName(bucketName string) bool {
  function New (line 31) | func New(globalopts *libgobuster.Options, opts *OptionsS3, logger *libgo...

FILE: gobusters3/options.go
  type OptionsS3 (line 8) | type OptionsS3 struct
  function NewOptions (line 15) | func NewOptions() *OptionsS3 {

FILE: gobusters3/result.go
  type Result (line 12) | type Result struct
    method ResultToString (line 19) | func (r Result) ResultToString() (string, error) {

FILE: gobusters3/types.go
  type AWSError (line 6) | type AWSError struct
  type AWSListing (line 15) | type AWSListing struct

FILE: gobustertftp/gobustertftp.go
  type GobusterTFTP (line 18) | type GobusterTFTP struct
    method Name (line 41) | func (d *GobusterTFTP) Name() string {
    method PreRun (line 46) | func (d *GobusterTFTP) PreRun(_ context.Context, _ *libgobuster.Progre...
    method ProcessWord (line 55) | func (d *GobusterTFTP) ProcessWord(_ context.Context, word string, pro...
    method AdditionalWordsLen (line 87) | func (d *GobusterTFTP) AdditionalWordsLen() int {
    method AdditionalWords (line 91) | func (d *GobusterTFTP) AdditionalWords(_ string) []string {
    method AdditionalSuccessWords (line 95) | func (d *GobusterTFTP) AdditionalSuccessWords(_ string) []string {
    method GetConfigString (line 100) | func (d *GobusterTFTP) GetConfigString() (string, error) {
  function New (line 24) | func New(globalopts *libgobuster.Options, opts *OptionsTFTP) (*GobusterT...

FILE: gobustertftp/options.go
  type OptionsTFTP (line 8) | type OptionsTFTP struct
  function NewOptions (line 14) | func NewOptions() *OptionsTFTP {

FILE: gobustertftp/result.go
  type Result (line 13) | type Result struct
    method ResultToString (line 20) | func (r Result) ResultToString() (string, error) {

FILE: gobustervhost/gobustervhost.go
  type GobusterVhost (line 22) | type GobusterVhost struct
    method Name (line 77) | func (v *GobusterVhost) Name() string {
    method PreRun (line 82) | func (v *GobusterVhost) PreRun(ctx context.Context, _ *libgobuster.Pro...
    method ProcessWord (line 128) | func (v *GobusterVhost) ProcessWord(ctx context.Context, word string, ...
    method AdditionalWordsLen (line 215) | func (v *GobusterVhost) AdditionalWordsLen() int {
    method AdditionalWords (line 219) | func (v *GobusterVhost) AdditionalWords(_ string) []string {
    method AdditionalSuccessWords (line 223) | func (v *GobusterVhost) AdditionalSuccessWords(_ string) []string {
    method GetConfigString (line 228) | func (v *GobusterVhost) GetConfigString() (string, error) {
  function New (line 33) | func New(globalopts *libgobuster.Options, opts *OptionsVhost, logger *li...

FILE: gobustervhost/options.go
  type OptionsVhost (line 8) | type OptionsVhost struct
  function NewOptions (line 20) | func NewOptions() *OptionsVhost {

FILE: gobustervhost/result.go
  type Result (line 20) | type Result struct
    method ResultToString (line 28) | func (r Result) ResultToString() (string, error) {

FILE: libgobuster/helpers.go
  type Set (line 16) | type Set struct
  function NewSet (line 21) | func NewSet[T comparable]() Set[T] {
  method Add (line 26) | func (set *Set[T]) Add(s T) bool {
  method AddRange (line 33) | func (set *Set[T]) AddRange(ss []T) {
  method Contains (line 40) | func (set *Set[T]) Contains(s T) bool {
  method ContainsAny (line 46) | func (set *Set[T]) ContainsAny(ss []T) bool {
  method Length (line 56) | func (set *Set[T]) Length() int {
  method Stringify (line 61) | func (set *Set[T]) Stringify() string {
  function lineCounter (line 75) | func lineCounter(r io.Reader) (int, error) {
  function lineCounterSlow (line 104) | func lineCounterSlow(r io.Reader) (int, error) {
  function DefaultUserAgent (line 123) | func DefaultUserAgent() string {
  function ParseExtensions (line 128) | func ParseExtensions(extensions string) (Set[string], error) {
  function ParseExtensionsFile (line 143) | func ParseExtensionsFile(file string) ([]string, error) {
  function ParseCommaSeparatedInt (line 168) | func ParseCommaSeparatedInt(inputString string) (Set[int], error) {

FILE: libgobuster/helpers_test.go
  function TestNewSet (line 14) | func TestNewSet(t *testing.T) {
  function TestSetAdd (line 25) | func TestSetAdd(t *testing.T) {
  function TestSetAddDouble (line 40) | func TestSetAddDouble(t *testing.T) {
  function TestSetAddRange (line 57) | func TestSetAddRange(t *testing.T) {
  function TestSetAddRangeDouble (line 72) | func TestSetAddRangeDouble(t *testing.T) {
  function TestSetContains (line 87) | func TestSetContains(t *testing.T) {
  function TestSetContainsAny (line 108) | func TestSetContainsAny(t *testing.T) {
  function TestSetStringify (line 135) | func TestSetStringify(t *testing.T) {
  function TestLineCounter (line 160) | func TestLineCounter(t *testing.T) {
  function TestLineCounterSlow (line 190) | func TestLineCounterSlow(t *testing.T) {
  function BenchmarkLineCounter (line 220) | func BenchmarkLineCounter(b *testing.B) {
  function BenchmarkLineCounterSlow (line 241) | func BenchmarkLineCounterSlow(b *testing.B) {
  function TestLineCounterError (line 262) | func TestLineCounterError(t *testing.T) {
  function TestParseExtensions (line 271) | func TestParseExtensions(t *testing.T) {
  function TestParseCommaSeparatedInt (line 301) | func TestParseCommaSeparatedInt(t *testing.T) {
  function BenchmarkParseExtensions (line 349) | func BenchmarkParseExtensions(b *testing.B) {
  function BenchmarkParseCommaSeparatedInt (line 372) | func BenchmarkParseCommaSeparatedInt(b *testing.B) {

FILE: libgobuster/http.go
  type HTTPHeader (line 17) | type HTTPHeader struct
  type HTTPClient (line 23) | type HTTPClient struct
    method Request (line 133) | func (client *HTTPClient) Request(ctx context.Context, fullURL url.URL...
    method makeRequest (line 164) | func (client *HTTPClient) makeRequest(ctx context.Context, fullURL url...
  type RequestOptions (line 38) | type RequestOptions struct
  function NewHTTPClient (line 48) | func NewHTTPClient(opt *HTTPOptions, logger *Logger) (*HTTPClient, error) {

FILE: libgobuster/http_test.go
  function httpServerB (line 14) | func httpServerB(b *testing.B, content string) *httptest.Server {
  function httpServerT (line 24) | func httpServerT(t *testing.T, content string) *httptest.Server {
  function randomString (line 34) | func randomString(length int) (string, error) {
  function TestRequest (line 49) | func TestRequest(t *testing.T) {
  function BenchmarkRequestWithoutBody (line 82) | func BenchmarkRequestWithoutBody(b *testing.B) {
  function BenchmarkRequestWitBody (line 107) | func BenchmarkRequestWitBody(b *testing.B) {
  function BenchmarkNewHTTPClient (line 132) | func BenchmarkNewHTTPClient(b *testing.B) {

FILE: libgobuster/interfaces.go
  type GobusterPlugin (line 6) | type GobusterPlugin interface
  type Result (line 17) | type Result interface

FILE: libgobuster/libgobuster.go
  constant PATTERN (line 16) | PATTERN = "{GOBUSTER}"
  type SetupFunc (line 19) | type SetupFunc
  type ProcessFunc (line 22) | type ProcessFunc
  type ResultToStringFunc (line 25) | type ResultToStringFunc
  type Gobuster (line 28) | type Gobuster struct
    method worker (line 57) | func (g *Gobuster) worker(ctx context.Context, guessChan <-chan *Guess...
    method feeder (line 117) | func (g *Gobuster) feeder(ctx context.Context, guessChan chan<- *Guess...
    method feedWordlist (line 123) | func (g *Gobuster) feedWordlist(ctx context.Context, guessChan chan<- ...
    method getWordlist (line 174) | func (g *Gobuster) getWordlist(wordlist io.ReadSeeker) (*Wordlist, err...
    method Run (line 226) | func (g *Gobuster) Run(ctx context.Context) error {
    method GetConfigString (line 327) | func (g *Gobuster) GetConfigString() (string, error) {
    method processPatterns (line 331) | func (g *Gobuster) processPatterns(word string) []string {
    method processDiscoverPatterns (line 339) | func (g *Gobuster) processDiscoverPatterns(word string) []string {
    method applyPatterns (line 347) | func (g *Gobuster) applyPatterns(word string, patterns []string) []str...
  type Guess (line 35) | type Guess struct
  type Wordlist (line 40) | type Wordlist struct
  function NewGobuster (line 47) | func NewGobuster(opts *Options, plugin GobusterPlugin, logger *Logger) (...
  function feed (line 99) | func feed(ctx context.Context, guessChan chan<- *Guess, words []string, ...

FILE: libgobuster/logger.go
  type Logger (line 10) | type Logger struct
    method Debug (line 30) | func (l Logger) Debug(v ...any) {
    method Debugf (line 37) | func (l Logger) Debugf(format string, v ...any) {
    method Warn (line 44) | func (l Logger) Warn(v ...any) {
    method Warnf (line 48) | func (l Logger) Warnf(format string, v ...any) {
    method Info (line 52) | func (l Logger) Info(v ...any) {
    method Infof (line 56) | func (l Logger) Infof(format string, v ...any) {
    method Print (line 60) | func (l Logger) Print(v ...any) {
    method Printf (line 64) | func (l Logger) Printf(format string, v ...any) {
    method Println (line 68) | func (l Logger) Println(v ...any) {
    method Error (line 72) | func (l Logger) Error(v ...any) {
    method Errorf (line 76) | func (l Logger) Errorf(format string, v ...any) {
    method Fatal (line 80) | func (l Logger) Fatal(v ...any) {
    method Fatalf (line 84) | func (l Logger) Fatalf(format string, v ...any) {
    method Fatalln (line 88) | func (l Logger) Fatalln(v ...any) {
  function NewLogger (line 19) | func NewLogger(debug bool) *Logger {

FILE: libgobuster/options.go
  type Options (line 8) | type Options struct

FILE: libgobuster/options_http.go
  type BasicHTTPOptions (line 11) | type BasicHTTPOptions struct
  type HTTPOptions (line 24) | type HTTPOptions struct

FILE: libgobuster/progress.go
  type MessageLevel (line 5) | type MessageLevel
  constant LevelDebug (line 8) | LevelDebug MessageLevel = iota
  constant LevelInfo (line 9) | LevelInfo
  constant LevelWarn (line 10) | LevelWarn
  constant LevelError (line 11) | LevelError
  type Message (line 14) | type Message struct
  type Progress (line 19) | type Progress struct
    method RequestsExpected (line 40) | func (p *Progress) RequestsExpected() int {
    method RequestsIssued (line 46) | func (p *Progress) RequestsIssued() int {
    method incrementRequestsIssues (line 52) | func (p *Progress) incrementRequestsIssues(by int) {
    method incrementRequests (line 58) | func (p *Progress) incrementRequests() {
    method IncrementTotalRequests (line 64) | func (p *Progress) IncrementTotalRequests(by int) {
  function NewProgress (line 29) | func NewProgress() *Progress {

FILE: libgobuster/useragents.go
  function GetRandomUserAgent (line 4670) | func GetRandomUserAgent() (string, error) {

FILE: libgobuster/version.go
  constant VERSION (line 11) | VERSION = "3.8.2"
  function GetVersion (line 14) | func GetVersion() string {

FILE: main.go
  function main (line 22) | func main() {

FILE: vhs/server.go
  type route (line 9) | type route struct
  type RegexpHandler (line 14) | type RegexpHandler struct
    method Handler (line 18) | func (h *RegexpHandler) Handler(pattern *regexp.Regexp, handler http.H...
    method HandleFunc (line 22) | func (h *RegexpHandler) HandleFunc(pattern *regexp.Regexp, handler fun...
    method ServeHTTP (line 26) | func (h *RegexpHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque...
  function main (line 37) | func main() {
Condensed preview — 79 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (690K chars).
[
  {
    "path": ".devcontainer/devcontainer.json",
    "chars": 888,
    "preview": "// For format details, see https://aka.ms/devcontainer.json. For config options, see the\n// README at: https://github.co"
  },
  {
    "path": ".dockerignore",
    "chars": 68,
    "preview": "*.exe\n*.out\n*.prof\n*.txt\n*.swp\n.vscode/\ngobuster\n.git/\nmitm*\n.idea/\n"
  },
  {
    "path": ".gitattributes",
    "chars": 2107,
    "preview": "# https://github.com/gitattributes/gitattributes/blob/master/Common.gitattributes\n\n# Common settings that generally shou"
  },
  {
    "path": ".github/FUNDING.yml",
    "chars": 130,
    "preview": "# These are supported funding model platforms\n\ngithub: [OJ, firefart]\npatreon: OJReeves\nopen_collective: gobuster\nko_fi:"
  },
  {
    "path": ".github/dependabot.yml",
    "chars": 815,
    "preview": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where "
  },
  {
    "path": ".github/workflows/auto-merge-dependabot.yml",
    "chars": 858,
    "preview": "name: Auto-merge dependabot updates\n\non:\n  pull_request:\n    branches: [main, dev]\n\npermissions:\n  pull-requests: write\n"
  },
  {
    "path": ".github/workflows/docker.yml",
    "chars": 933,
    "preview": "name: Build Docker Images\n\non:\n  push:\n    branches:\n      - main\n  workflow_dispatch:\n  schedule:\n    - cron: \"0 0 * * "
  },
  {
    "path": ".github/workflows/go.yml",
    "chars": 632,
    "preview": "name: Go\non: [push, pull_request]\npermissions:\n  contents: read\njobs:\n  build:\n    name: Build\n    runs-on: ubuntu-lates"
  },
  {
    "path": ".github/workflows/golangci-lint.yml",
    "chars": 448,
    "preview": "name: golangci-lint\non: [push, pull_request, workflow_dispatch]\npermissions:\n  contents: read\njobs:\n  golangci:\n    name"
  },
  {
    "path": ".github/workflows/hadolint.yml",
    "chars": 600,
    "preview": "name: Hadolint\non:\n  push:\n    paths:\n      - \"**/Dockerfile\"\n  pull_request:\n  workflow_dispatch:\npermissions:\n  conten"
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 663,
    "preview": "name: goreleaser\n\non:\n  push:\n    tags:\n      - \"*\"\n\npermissions:\n  contents: write\n\njobs:\n  goreleaser:\n    runs-on: ub"
  },
  {
    "path": ".github/workflows/update.yml",
    "chars": 685,
    "preview": "name: Update Dependencies\n\non:\n  schedule:\n    - cron: \"0 4 * * *\"\n  workflow_dispatch:\n\npermissions:\n  contents: write\n"
  },
  {
    "path": ".github/workflows/vhs.yml",
    "chars": 982,
    "preview": "name: vhs\non:\n  push:\n    paths:\n      - vhs/**.tape\n\npermissions:\n  contents: write\n\njobs:\n  vhs:\n    runs-on: ubuntu-l"
  },
  {
    "path": ".github/workflows/yamllint.yml",
    "chars": 441,
    "preview": "name: yamllint\non:\n  push:\n    paths:\n      - \"**.yml\"\n      - \"**.yaml\"\n  pull_request:\n  workflow_dispatch:\npermission"
  },
  {
    "path": ".gitignore",
    "chars": 618,
    "preview": "# https://github.com/github/gitignore/blob/main/Go.gitignore\n\n# If you prefer the allow list template instead of the den"
  },
  {
    "path": ".golangci.yml",
    "chars": 4295,
    "preview": "# based on https://gist.github.com/maratori/47a4d00457a92aa426dbd48a18776322\nversion: \"2\"\nrun:\n  relative-path-mode: gom"
  },
  {
    "path": ".goreleaser.yaml",
    "chars": 1185,
    "preview": "# This is an example .goreleaser.yml file with some sensible defaults.\n# Make sure to check the documentation at https:/"
  },
  {
    "path": ".yamllint.yml",
    "chars": 96,
    "preview": "---\nextends: default\n\nrules:\n  truthy: disable\n  line-length: disable\n  document-start: disable\n"
  },
  {
    "path": "Dockerfile",
    "chars": 451,
    "preview": "# syntax=docker/dockerfile:1\nFROM golang:latest AS build-env\nWORKDIR /src\nENV CGO_ENABLED=0\nCOPY go.mod /src/\nRUN go mod"
  },
  {
    "path": "LICENSE",
    "chars": 11357,
    "preview": "\n                                 Apache License\n                           Version 2.0, January 2004\n                  "
  },
  {
    "path": "README.md",
    "chars": 12926,
    "preview": "# Gobuster\n\n[![Go Report Card](https://goreportcard.com/badge/github.com/OJ/gobuster/v3)](https://goreportcard.com/repor"
  },
  {
    "path": "Taskfile.yml",
    "chars": 1379,
    "preview": "version: \"3\"\n\nvars:\n  PROGRAM: gobuster\n\ntasks:\n  deps:\n    cmds:\n      - go mod tidy -v\n\n  update:\n    cmds:\n      - go"
  },
  {
    "path": "cli/const.go",
    "chars": 77,
    "preview": "//go:build !windows\n\npackage cli\n\nconst (\n\tTerminalClearLine = \"\\r\\x1b[2K\"\n)\n"
  },
  {
    "path": "cli/const_windows.go",
    "chars": 71,
    "preview": "//go:build windows\n\npackage cli\n\nconst (\n\tTerminalClearLine = \"\\r\\r\"\n)\n"
  },
  {
    "path": "cli/dir/dir.go",
    "chars": 5265,
    "preview": "package dir\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\n\tinternalcli \"github.com/OJ/gobuster/v3/cli\"\n\t\"github.com/OJ/gobuster/v3/gobuste"
  },
  {
    "path": "cli/dir/dir_test.go",
    "chars": 2336,
    "preview": "package dir\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/OJ/gobus"
  },
  {
    "path": "cli/dns/dns.go",
    "chars": 2946,
    "preview": "package dns\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"runtime\"\n\t\"time\"\n\n\tinternalcli \"github.com/OJ/gobuster/v3/cli\"\n\t\"github.com/OJ/"
  },
  {
    "path": "cli/fuzz/fuzz.go",
    "chars": 3168,
    "preview": "package fuzz\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\tinternalcli \"github.com/OJ/gobuster/v3/cli\"\n\t\"github.com/OJ/gobuster/v3/gobus"
  },
  {
    "path": "cli/gcs/gcs.go",
    "chars": 1581,
    "preview": "package gcs\n\nimport (\n\t\"fmt\"\n\n\tinternalcli \"github.com/OJ/gobuster/v3/cli\"\n\t\"github.com/OJ/gobuster/v3/gobustergcs\"\n\t\"gi"
  },
  {
    "path": "cli/gobuster.go",
    "chars": 5741,
    "preview": "package cli\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"os\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/OJ/gobuster/v3/"
  },
  {
    "path": "cli/options.go",
    "chars": 12326,
    "preview": "package cli\n\nimport (\n\t\"bufio\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/url\"\n\t\"os\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\t\""
  },
  {
    "path": "cli/s3/s3.go",
    "chars": 1575,
    "preview": "package s3\n\nimport (\n\t\"fmt\"\n\n\tinternalcli \"github.com/OJ/gobuster/v3/cli\"\n\t\"github.com/OJ/gobuster/v3/gobusters3\"\n\t\"gith"
  },
  {
    "path": "cli/tftp/tftp.go",
    "chars": 1517,
    "preview": "package tftp\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\tinternalcli \"github.com/OJ/gobuster/v3/cli\"\n\t\"github.com/OJ/gobuster/"
  },
  {
    "path": "cli/vhost/vhost.go",
    "chars": 4009,
    "preview": "package vhost\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\n\tinternalcli \"github.com/OJ/gobuster/v3/cli\"\n\t\"github.com/OJ/gobust"
  },
  {
    "path": "cli/vhost/vhost_test.go",
    "chars": 1898,
    "preview": "package vhost\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/OJ/gob"
  },
  {
    "path": "go.mod",
    "chars": 919,
    "preview": "module github.com/OJ/gobuster/v3\n\ngo 1.25\n\nrequire (\n\tgithub.com/fatih/color v1.18.0\n\tgithub.com/google/uuid v1.6.0\n\tgit"
  },
  {
    "path": "go.sum",
    "chars": 5364,
    "preview": "github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=\ngithub.com/cpuguy83/go-md2man/v2"
  },
  {
    "path": "gobusterdir/gobusterdir.go",
    "chars": 12549,
    "preview": "package gobusterdir\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"strings\"\n\t\"syscall"
  },
  {
    "path": "gobusterdir/gobusterdir_test.go",
    "chars": 925,
    "preview": "package gobusterdir\n\nimport (\n\t\"testing\"\n\n\t\"github.com/OJ/gobuster/v3/libgobuster\"\n)\n\nfunc TestAdditionalWordsLen(t *tes"
  },
  {
    "path": "gobusterdir/options.go",
    "chars": 1103,
    "preview": "package gobusterdir\n\nimport (\n\t\"github.com/OJ/gobuster/v3/libgobuster\"\n)\n\n// OptionsDir is the struct to hold all option"
  },
  {
    "path": "gobusterdir/options_test.go",
    "chars": 284,
    "preview": "package gobusterdir\n\nimport \"testing\"\n\nfunc TestNewOptions(t *testing.T) {\n\tt.Parallel()\n\n\to := NewOptions()\n\tif o.Statu"
  },
  {
    "path": "gobusterdir/result.go",
    "chars": 1445,
    "preview": "package gobusterdir\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/fatih/color\"\n)\n\nvar (\n\twhite  = color.New(color."
  },
  {
    "path": "gobusterdns/gobusterdns.go",
    "chars": 6819,
    "preview": "package gobusterdns\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/netip\"\n\t\"strings\"\n\t\"text/tabwri"
  },
  {
    "path": "gobusterdns/options.go",
    "chars": 389,
    "preview": "package gobusterdns\n\nimport (\n\t\"time\"\n)\n\n// OptionsDNS holds all options for the dns plugin\ntype OptionsDNS struct {\n\tDo"
  },
  {
    "path": "gobusterdns/result.go",
    "chars": 834,
    "preview": "package gobusterdns\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"net/netip\"\n\t\"strings\"\n\n\t\"github.com/fatih/color\"\n)\n\nvar green = color.Ne"
  },
  {
    "path": "gobusterfuzz/gobusterfuzz.go",
    "chars": 8906,
    "preview": "package gobusterfuzz\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"strings\"\n\t\"syscal"
  },
  {
    "path": "gobusterfuzz/options.go",
    "chars": 616,
    "preview": "package gobusterfuzz\n\nimport (\n\t\"github.com/OJ/gobuster/v3/libgobuster\"\n)\n\n// OptionsFuzz is the struct to hold all opti"
  },
  {
    "path": "gobusterfuzz/options_test.go",
    "chars": 207,
    "preview": "package gobusterfuzz\n\nimport \"testing\"\n\nfunc TestNewOptions(t *testing.T) {\n\tt.Parallel()\n\n\to := NewOptions()\n\tif o.Excl"
  },
  {
    "path": "gobusterfuzz/result.go",
    "chars": 790,
    "preview": "package gobusterfuzz\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/fatih/color\"\n)\n\nvar (\n\tgreen = color.New(color."
  },
  {
    "path": "gobustergcs/gobustersgcs.go",
    "chars": 8378,
    "preview": "package gobustergcs\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url"
  },
  {
    "path": "gobustergcs/options.go",
    "chars": 343,
    "preview": "package gobustergcs\n\nimport (\n\t\"github.com/OJ/gobuster/v3/libgobuster\"\n)\n\n// OptionsGCS is the struct to hold all option"
  },
  {
    "path": "gobustergcs/result.go",
    "chars": 576,
    "preview": "package gobustergcs\n\nimport (\n\t\"bytes\"\n\n\t\"github.com/fatih/color\"\n)\n\nvar green = color.New(color.FgGreen).FprintfFunc()\n"
  },
  {
    "path": "gobustergcs/types.go",
    "chars": 684,
    "preview": "package gobustergcs\n\n// GCSError represents a returned error from GCS\ntype GCSError struct {\n\tError struct {\n\t\tCode    i"
  },
  {
    "path": "gobusters3/gobusters3.go",
    "chars": 7932,
    "preview": "package gobusters3\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/xml\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n"
  },
  {
    "path": "gobusters3/options.go",
    "chars": 338,
    "preview": "package gobusters3\n\nimport (\n\t\"github.com/OJ/gobuster/v3/libgobuster\"\n)\n\n// OptionsS3 is the struct to hold all options "
  },
  {
    "path": "gobusters3/result.go",
    "chars": 554,
    "preview": "package gobusters3\n\nimport (\n\t\"bytes\"\n\n\t\"github.com/fatih/color\"\n)\n\nvar green = color.New(color.FgGreen).FprintfFunc()\n\n"
  },
  {
    "path": "gobusters3/types.go",
    "chars": 661,
    "preview": "package gobusters3\n\nimport \"encoding/xml\"\n\n// AWSError represents a returned error from AWS\ntype AWSError struct {\n\tXMLN"
  },
  {
    "path": "gobustertftp/gobustertftp.go",
    "chars": 3492,
    "preview": "package gobustertftp\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\t\"text/tabwriter\"\n\n\t\"github.com/"
  },
  {
    "path": "gobustertftp/options.go",
    "chars": 273,
    "preview": "package gobustertftp\n\nimport (\n\t\"time\"\n)\n\n// OptionsTFTP holds all options for the tftp plugin\ntype OptionsTFTP struct {"
  },
  {
    "path": "gobustertftp/result.go",
    "chars": 640,
    "preview": "package gobustertftp\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\n\t\"github.com/fatih/color\"\n)\n\nvar green = color.New(color.FgGreen).Fprint"
  },
  {
    "path": "gobustervhost/gobustervhost.go",
    "chars": 8808,
    "preview": "package gobustervhost\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"strings\"\n\t\"sync\""
  },
  {
    "path": "gobustervhost/options.go",
    "chars": 646,
    "preview": "package gobustervhost\n\nimport (\n\t\"github.com/OJ/gobuster/v3/libgobuster\"\n)\n\n// OptionsVhost is the struct to hold all op"
  },
  {
    "path": "gobustervhost/result.go",
    "chars": 1272,
    "preview": "package gobustervhost\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/fatih/color\"\n)\n\nvar (\n\twhite  = color.New(color.FgWhite"
  },
  {
    "path": "libgobuster/errors.go",
    "chars": 337,
    "preview": "package libgobuster\n\nimport \"errors\"\n\nvar (\n\tErrTimeout           = errors.New(\"timeout occurred during the request\")\n\tE"
  },
  {
    "path": "libgobuster/helpers.go",
    "chars": 4425,
    "preview": "package libgobuster\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n)\n\n// Set i"
  },
  {
    "path": "libgobuster/helpers_test.go",
    "chars": 10784,
    "preview": "package libgobuster\n\nimport (\n\t\"errors\"\n\t\"io\"\n\t\"os\"\n\t\"reflect\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\t\"testing/iotest\"\n)\n\nfun"
  },
  {
    "path": "libgobuster/http.go",
    "chars": 6451,
    "preview": "package libgobuster\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/httputil\"\n\t\""
  },
  {
    "path": "libgobuster/http_test.go",
    "chars": 3225,
    "preview": "package libgobuster\n\nimport (\n\t\"bytes\"\n\t\"crypto/rand\"\n\t\"fmt\"\n\t\"math/big\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"t"
  },
  {
    "path": "libgobuster/interfaces.go",
    "chars": 503,
    "preview": "package libgobuster\n\nimport \"context\"\n\n// GobusterPlugin is an interface which plugins must implement\ntype GobusterPlugi"
  },
  {
    "path": "libgobuster/libgobuster.go",
    "chars": 9039,
    "preview": "package libgobuster\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n)\n\n// PATTERN "
  },
  {
    "path": "libgobuster/logger.go",
    "chars": 1716,
    "preview": "package libgobuster\n\nimport (\n\t\"log\" // nolint:depguard\n\t\"os\"\n\n\t\"github.com/fatih/color\"\n)\n\ntype Logger struct {\n\tlog   "
  },
  {
    "path": "libgobuster/options.go",
    "chars": 489,
    "preview": "package libgobuster\n\nimport (\n\t\"time\"\n)\n\n// Options holds all options that can be passed to libgobuster\ntype Options str"
  },
  {
    "path": "libgobuster/options_http.go",
    "chars": 756,
    "preview": "package libgobuster\n\nimport (\n\t\"crypto/tls\"\n\t\"net\"\n\t\"net/url\"\n\t\"time\"\n)\n\n// BasicHTTPOptions defines only core http opti"
  },
  {
    "path": "libgobuster/progress.go",
    "chars": 1444,
    "preview": "package libgobuster\n\nimport \"sync\"\n\ntype MessageLevel int\n\nconst (\n\tLevelDebug MessageLevel = iota\n\tLevelInfo\n\tLevelWarn"
  },
  {
    "path": "libgobuster/useragents.go",
    "chars": 442570,
    "preview": "package libgobuster\n\nimport (\n\t\"crypto/rand\"\n\t\"math/big\"\n)\n\n// molint:gochecknoglobals\nvar userAgents = [...]string{\n\t\"M"
  },
  {
    "path": "libgobuster/version.go",
    "chars": 849,
    "preview": "package libgobuster\n\nimport (\n\t\"fmt\"\n\t\"runtime/debug\"\n\t\"strconv\"\n)\n\nconst (\n\t// VERSION contains the current gobuster ve"
  },
  {
    "path": "main.go",
    "chars": 1371,
    "preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"runtime/debug\"\n\n\t\"github.com/OJ/gobuster/v3/cli/dir\"\n\t\"github.com/OJ/gobust"
  },
  {
    "path": "vhs/gobuster_dir.tape",
    "chars": 3238,
    "preview": "# VHS documentation\n#\n# Output:\n#   Output <path>.gif               Create a GIF output at the given <path>\n#   Output <"
  },
  {
    "path": "vhs/server.go",
    "chars": 1335,
    "preview": "package main\n\nimport (\n\t\"log\" // nolint:depguard\n\t\"net/http\"\n\t\"regexp\"\n)\n\ntype route struct {\n\tpattern *regexp.Regexp\n\th"
  }
]

About this extraction

This page contains the full source code of the OJ/gobuster GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 79 files (634.1 KB), approximately 265.4k tokens, and a symbol index with 260 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!