Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion cmd/tlsx/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ package main
import (
"os"
"strings"
"time"

"github.com/projectdiscovery/goflags"
"github.com/projectdiscovery/gologger"
"github.com/projectdiscovery/tlsx/internal/runner"
"github.com/projectdiscovery/tlsx/pkg/output"
"github.com/projectdiscovery/tlsx/pkg/tlsx/clients"
"github.com/projectdiscovery/tlsx/pkg/tlsx/openssl"
"github.com/projectdiscovery/utils/errkit"
Expand All @@ -29,6 +31,24 @@ func process() error {
if err := readFlags(); err != nil {
return errkit.Wrapf(err, "could not read flags")
}

// Initialize output coordinator if output file is specified
var coord *output.AsyncOutputCoordinator
var err error
if options.OutputFile != "" {
coord, err = output.NewAsyncOutputCoordinator(options.OutputFile, 10000, 1*time.Second)
if err != nil {
return errkit.Wrapf(err, "could not initialize output coordinator")
}
options.AsyncOutputCoordinator = coord
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for Options struct definition and AsyncOutputCoordinator field
rg -nP --type=go 'type\s+Options\s+struct|AsyncOutputCoordinator' -C4 pkg/tlsx/clients/clients.go

Repository: projectdiscovery/tlsx

Length of output: 350


🏁 Script executed:

#!/bin/bash
# Read the full Options struct definition
sed -n '46,120p' pkg/tlsx/clients/clients.go | cat -n

Repository: projectdiscovery/tlsx

Length of output: 3248


🏁 Script executed:

#!/bin/bash
# Continue reading the Options struct
sed -n '76,150p' pkg/tlsx/clients/clients.go | cat -n

Repository: projectdiscovery/tlsx

Length of output: 3120


🏁 Script executed:

#!/bin/bash
# Search for AsyncOutputCoordinator across the entire clients.go file
rg -n "AsyncOutputCoordinator" pkg/tlsx/clients/clients.go

Repository: projectdiscovery/tlsx

Length of output: 47


🏁 Script executed:

#!/bin/bash
# Search entire repository for AsyncOutputCoordinator
rg -n "AsyncOutputCoordinator" --type=go

Repository: projectdiscovery/tlsx

Length of output: 1116


🏁 Script executed:

#!/bin/bash
# Find where Options struct ends
rg -n "^}" pkg/tlsx/clients/clients.go -B5 | head -20

Repository: projectdiscovery/tlsx

Length of output: 805


🏁 Script executed:

#!/bin/bash
# Check the Options struct from line 46 to 188
sed -n '46,188p' pkg/tlsx/clients/clients.go | tail -20

Repository: projectdiscovery/tlsx

Length of output: 801


🏁 Script executed:

#!/bin/bash
# Check the context around line 43 in main.go
sed -n '35,50p' cmd/tlsx/main.go

Repository: projectdiscovery/tlsx

Length of output: 615


Compile error: AsyncOutputCoordinator field does not exist in Options struct.

Line 43 assigns options.AsyncOutputCoordinator = coord, but the Options struct in pkg/tlsx/clients/clients.go (lines 46–188) does not define this field. This will cause a compile-time error: "unknown field AsyncOutputCoordinator in struct literal".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/tlsx/main.go` at line 43, The assignment options.AsyncOutputCoordinator =
coord fails because Options (pkg/tlsx/clients/clients.go) lacks
AsyncOutputCoordinator; fix by either adding a matching field to the Options
struct or assigning coord to the correct existing field: add a field named
AsyncOutputCoordinator to Options with the same type as coord (use the concrete
type or interface used where coord is created), or if there is an intended/typo
field (e.g., AsyncCoordinator or OutputCoordinator), change the assignment to
that existing field name instead; update any constructors/newOptions functions
to accept and propagate the new field if you add it.

coord.HandleSignals()
defer func() {
if err := coord.GracefulShutdown(); err != nil {
gologger.Warning().Msgf("Error during graceful shutdown: %v", err)
}
}()
Comment on lines +44 to +50
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

find . -name "coordinator.go" -type f

Repository: projectdiscovery/tlsx

Length of output: 92


🏁 Script executed:

wc -l ./pkg/output/coordinator.go

Repository: projectdiscovery/tlsx

Length of output: 96


🏁 Script executed:

cat -n ./pkg/output/coordinator.go

Repository: projectdiscovery/tlsx

Length of output: 4786


Ensure GracefulShutdown() is idempotent before calling it from two paths.

coord.HandleSignals() triggers shutdown via goroutine on SIGINT/SIGTERM (line 138 in coordinator.go), and Lines 46-50 defer another shutdown call. The GracefulShutdown() implementation (lines 124-128) is not idempotent:

  • Reading from already-closed c.done channel on second call will panic
  • Closing an already-closed file on second call will error
  • No sync.Once or equivalent guard protects against repeated invocation

Protect GracefulShutdown() with sync.Once to make it safe for concurrent/repeated calls from both signal handler and deferred shutdown.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/tlsx/main.go` around lines 44 - 50, The GracefulShutdown() method is not
idempotent and can panic or error if called twice (from coord.HandleSignals()
goroutine and the deferred call); modify the coordinator to guard
GracefulShutdown() with a sync.Once (add a once field, e.g., shutdownOnce
sync.Once) and have GracefulShutdown() invoke the actual shutdown logic inside
shutdownOnce.Do(func(){ ... }) so reads from c.done, closing channels/files, and
other teardown run only once; update any callers (coord.HandleSignals() and the
deferred call in main) to continue calling coord.GracefulShutdown() without
additional guards.

}

runner, err := runner.New(options)
if err != nil {
return errkit.Wrapf(err, "could not create runner")
Expand Down Expand Up @@ -112,7 +132,7 @@ func readFlags(args ...string) error {
flagSet.StringSliceVarP(&options.Resolvers, "resolvers", "r", nil, "list of resolvers to use", goflags.FileCommaSeparatedStringSliceOptions),
flagSet.StringVarP(&options.CACertificate, "cacert", "cc", "", "client certificate authority file"),
flagSet.StringSliceVarP(&options.Ciphers, "cipher-input", "ci", nil, "ciphers to use with tls connection", goflags.FileCommaSeparatedStringSliceOptions),
flagSet.StringSliceVar(&options.ServerName, "sni", nil, "tls sni hostname to use", goflags.FileCommaSeparatedStringSliceOptions),
flagSet.StringSliceVar(&options.ServerName, "sni", nil, "tls sni hostname to use", goflags.NormalizedStringSliceOptions),
flagSet.BoolVarP(&options.RandomForEmptyServerName, "random-sni", "rs", false, "use random sni when empty"),
flagSet.BoolVarP(&options.ReversePtrSNI, "rev-ptr-sni", "rps", false, "perform reverse PTR to retrieve SNI from IP"),
flagSet.StringVar(&options.MinVersion, "min-version", "", "minimum tls version to accept (ssl30,tls10,tls11,tls12,tls13)"),
Expand Down
140 changes: 140 additions & 0 deletions pkg/output/coordinator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package output

import (
"bufio"
"context"
"encoding/json"
"os"
"os/signal"
"syscall"
"time"

"github.com/projectdiscovery/gologger"
)

// AsyncOutputCoordinator manages async writing of scan results to disk.
// Uses buffered channel for concurrent submission and periodic flushing.
type AsyncOutputCoordinator struct {
outputChan chan []byte
file *os.File
writer *bufio.Writer
shutdownCtx context.Context
cancel context.CancelFunc
flushTicker *time.Ticker
done chan struct{}
}

// NewAsyncOutputCoordinator creates a new coordinator.
// bufferSize: Size of the buffered channel (e.g., 10000 for 10k pending results).
// flushInterval: How often to flush the buffer to disk (e.g., 1*time.Second).
func NewAsyncOutputCoordinator(filename string, bufferSize int, flushInterval time.Duration) (*AsyncOutputCoordinator, error) {
file, err := os.Create(filename)
if err != nil {
return nil, err
}

ctx, cancel := context.WithCancel(context.Background())
coord := &AsyncOutputCoordinator{
outputChan: make(chan []byte, bufferSize),
file: file,
writer: bufio.NewWriter(file),
shutdownCtx: ctx,
cancel: cancel,
flushTicker: time.NewTicker(flushInterval),
done: make(chan struct{}),
}

go coord.writeLoop()
return coord, nil
}

// writeLoop is the dedicated goroutine for writing to disk.
// Uses periodic flushing instead of flushing after every write.
func (c *AsyncOutputCoordinator) writeLoop() {
defer func() {
c.flushTicker.Stop()
close(c.done)
}()

for {
select {
case data, ok := <-c.outputChan:
if !ok {
// Channel closed, drain remaining data
if err := c.writer.Flush(); err != nil {
gologger.Warning().Msgf("Failed to flush writer during shutdown: %v", err)
}
return
}
if _, err := c.writer.Write(data); err != nil {
gologger.Warning().Msgf("Failed to write data: %v", err)
continue
}

case <-c.flushTicker.C:
if err := c.writer.Flush(); err != nil {
gologger.Warning().Msgf("Failed to flush writer: %v", err)
}

case <-c.shutdownCtx.Done():
// Drain the channel before exiting
for {
select {
case data, ok := <-c.outputChan:
if !ok {
if err := c.writer.Flush(); err != nil {
gologger.Warning().Msgf("Failed to flush writer during shutdown: %v", err)
}
return
}
if _, err := c.writer.Write(data); err != nil {
gologger.Warning().Msgf("Failed to write data during shutdown: %v", err)
continue
}
default:
if err := c.writer.Flush(); err != nil {
gologger.Warning().Msgf("Failed to flush writer during shutdown: %v", err)
}
return
}
}
}
}
}

// Submit sends a result to the coordinator.
// Returns an error if the coordinator is shutting down.
func (c *AsyncOutputCoordinator) Submit(result interface{}) error {
data, err := json.Marshal(result)
if err != nil {
return err
}
data = append(data, '\n')

select {
case c.outputChan <- data:
return nil
case <-c.shutdownCtx.Done():
return context.Canceled
}
}

// GracefulShutdown waits for all data to be written and closes the file.
// Call this when the scan is complete or on program exit.
func (c *AsyncOutputCoordinator) GracefulShutdown() error {
c.cancel()
<-c.done
return c.file.Close()
}

// HandleSignals sets up signal handling for graceful shutdown on CTRL+C.
// Does not call os.Exit(), allowing defer statements to execute.
func (c *AsyncOutputCoordinator) HandleSignals() {
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sigChan
gologger.Info().Msg("Received interrupt signal. Shutting down gracefully...")
c.GracefulShutdown()
}()
}