Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extension of TCP Server (livereload, binary matching, custom logic) #61

Merged
merged 3 commits into from Dec 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
15 changes: 14 additions & 1 deletion README.md
Expand Up @@ -128,7 +128,9 @@ simplehttpserver -rule rules.yaml -tcp -tls -domain localhost
The rules are written as follows:
```yaml
rules:
- match: regex
- match: regex-match
match-contains: literal-match
name: rule-name
response: response data
```

Expand All @@ -137,6 +139,7 @@ For example to handle two different paths simulating an HTTP server or SMTP comm
rules:
# HTTP Requests
- match: GET /path1
name: redirect
response: |
HTTP/1.0 200 OK
Server: httpd/2.0
Expand All @@ -149,13 +152,15 @@ rules:
<HTML><HEAD><script>top.location.href='/Main_Login.asp';</script>
</HEAD></HTML>
- match: GET /path2
name: "404"
response: |
HTTP/1.0 404 OK
Server: httpd/2.0

<HTML><HEAD></HEAD><BODY>Not found</BODY></HTML>
# SMTP Commands
- match: "EHLO example.com"
name: smtp
response: |
250-localhost Nice to meet you, [127.0.0.1]
250-PIPELINING
Expand All @@ -167,6 +172,14 @@ rules:
response: 250 Accepted
- match: "RCPT TO: <test@example.com>"
response: 250 Accepted

- match-contains: !!binary |
MAwCAQFgBwIBAwQAgAA=
name: "ldap"
# Request: 300c 0201 0160 0702 0103 0400 8000 0....`........
# Response: 300c 0201 0161 070a 0100 0400 0400 0....a........
response: !!binary |
MAwCAQFhBwoBAAQABAA=
```

## Note
Expand Down
1 change: 1 addition & 0 deletions go.mod
Expand Up @@ -3,6 +3,7 @@ module github.com/projectdiscovery/simplehttpserver
go 1.14

require (
github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2
github.com/projectdiscovery/gologger v1.1.4
github.com/projectdiscovery/sslcert v0.0.0-20210416140253-8f56bec1bb5e
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Expand Up @@ -2,6 +2,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI=
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
Expand Down Expand Up @@ -31,6 +33,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
Expand Down
6 changes: 6 additions & 0 deletions internal/runner/runner.go
Expand Up @@ -42,6 +42,12 @@ func New(options *Options) (*Runner, error) {
if err != nil {
return nil, err
}
watcher, err := watchFile(r.options.RulesFile, serverTCP.LoadTemplate)
if err != nil {
return nil, err
}
defer watcher.Close()

r.serverTCP = serverTCP
return &r, nil
}
Expand Down
36 changes: 36 additions & 0 deletions internal/runner/watchdog.go
@@ -0,0 +1,36 @@
package runner

import (
"log"

"github.com/fsnotify/fsnotify"
)

type WatchEvent func(fname string) error

func watchFile(fname string, callback WatchEvent) (watcher *fsnotify.Watcher, err error) {
watcher, err = fsnotify.NewWatcher()
if err != nil {
return
}
go func() {
for {
select {
case event, ok := <-watcher.Events:
if !ok {
continue
}
if event.Op&fsnotify.Write == fsnotify.Write {
if err := callback(fname); err != nil {
log.Println("err", err)
}
}
case <-watcher.Errors:
// ignore errors for now
}
}
}()

err = watcher.Add(fname)
return
}
9 changes: 9 additions & 0 deletions pkg/tcpserver/addr.go
@@ -0,0 +1,9 @@
package tcpserver

// ContextType is the key type stored in ctx
type ContextType string

var (
// Addr is the contextKey where the net.Addr is stored
Addr ContextType = "addr"
)
7 changes: 5 additions & 2 deletions pkg/tcpserver/responseengine.go
Expand Up @@ -6,9 +6,12 @@ import (

// BuildResponse according to rules
func (t *TCPServer) BuildResponse(data []byte) ([]byte, error) {
t.mux.RLock()
defer t.mux.RUnlock()

// Process all the rules
for _, rule := range t.options.rules {
if rule.matchRegex.Match(data) {
for _, rule := range t.rules {
if rule.MatchInput(data) {
return []byte(rule.Response), nil
}
}
Expand Down
50 changes: 45 additions & 5 deletions pkg/tcpserver/rule.go
@@ -1,6 +1,9 @@
package tcpserver

import "regexp"
import (
"regexp"
"strings"
)

// RulesConfiguration from yaml
type RulesConfiguration struct {
Expand All @@ -9,17 +12,54 @@ type RulesConfiguration struct {

// Rule to apply to various requests
type Rule struct {
Match string `yaml:"match,omitempty"`
matchRegex *regexp.Regexp
Response string `yaml:"response,omitempty"`
Name string `yaml:"name,omitempty"`
Match string `yaml:"match,omitempty"`
MatchContains string `yaml:"match-contains,omitempty"`
matchRegex *regexp.Regexp
Response string `yaml:"response,omitempty"`
}

// NewRule from model
// NewRule creates a new Rule - default is regex
func NewRule(match, response string) (*Rule, error) {
return NewRegexRule(match, response)
}

// NewRegexRule returns a new regex-match Rule
func NewRegexRule(match, response string) (*Rule, error) {
regxp, err := regexp.Compile(match)
if err != nil {
return nil, err
}

return &Rule{Match: match, matchRegex: regxp, Response: response}, nil
}

// NewLiteralRule returns a new literal-match Rule
func NewLiteralRule(match, response string) (*Rule, error) {
return &Rule{MatchContains: match, Response: response}, nil
}

// NewRuleFromTemplate "copies" a new Rule
func NewRuleFromTemplate(r Rule) (newRule *Rule, err error) {
newRule = &Rule{
Name: r.Name,
Response: r.Response,
MatchContains: r.MatchContains,
Match: r.Match,
}
if newRule.Match != "" {
newRule.matchRegex, err = regexp.Compile(newRule.Match)
}

return
}

// MatchInput returns if the input was matches with one of the matchers
func (r *Rule) MatchInput(input []byte) bool {
if r.matchRegex != nil && r.matchRegex.Match(input) {
return true
} else if r.MatchContains != "" && strings.Contains(string(input), r.MatchContains) {
return true
}
return false
}
81 changes: 72 additions & 9 deletions pkg/tcpserver/tcpserver.go
@@ -1,9 +1,12 @@
package tcpserver

import (
"context"
"crypto/tls"
"errors"
"io/ioutil"
"net"
"sync"
"time"

"github.com/projectdiscovery/gologger"
Expand All @@ -24,20 +27,35 @@ type Options struct {
Verbose bool
}

// CallBackFunc handles what is send back to the client, based on the incomming question
type CallBackFunc func(ctx context.Context, question []byte) (answer []byte, err error)

// TCPServer instance
type TCPServer struct {
options *Options
listener net.Listener

// Callbacks to retrieve information about the system
HandleMessageFnc CallBackFunc

mux sync.RWMutex
rules []Rule
}

// New tcp server instance with specified options
func New(options *Options) (*TCPServer, error) {
return &TCPServer{options: options}, nil
srv := &TCPServer{options: options}
srv.HandleMessageFnc = srv.BuildResponseWithContext
srv.rules = options.rules
return srv, nil
}

// AddRule to the server
func (t *TCPServer) AddRule(rule Rule) error {
t.options.rules = append(t.options.rules, rule)
t.mux.Lock()
defer t.mux.Unlock()

t.rules = append(t.rules, rule)
return nil
}

Expand All @@ -51,23 +69,27 @@ func (t *TCPServer) ListenAndServe() error {
return t.run()
}

func (t *TCPServer) handleConnection(conn net.Conn) error {
func (t *TCPServer) handleConnection(conn net.Conn, callback CallBackFunc) error {
defer conn.Close() //nolint

// Create Context
ctx := context.WithValue(context.Background(), Addr, conn.RemoteAddr())

buf := make([]byte, 4096)
for {
if err := conn.SetReadDeadline(time.Now().Add(readTimeout * time.Second)); err != nil {
gologger.Info().Msgf("%s\n", err)
}
_, err := conn.Read(buf)
n, err := conn.Read(buf)
if err != nil {
return err
}

gologger.Print().Msgf("%s\n", buf)
gologger.Print().Msgf("%s\n", buf[:n])

resp, err := t.BuildResponse(buf)
resp, err := callback(ctx, buf[:n])
if err != nil {
gologger.Info().Msgf("Closing connection: %s\n", err)
return err
}

Expand Down Expand Up @@ -112,7 +134,7 @@ func (t *TCPServer) run() error {
if err != nil {
return err
}
go t.handleConnection(c) //nolint
go t.handleConnection(c, t.HandleMessageFnc) //nolint
}
}

Expand All @@ -133,13 +155,54 @@ func (t *TCPServer) LoadTemplate(templatePath string) error {
return err
}

t.mux.Lock()
defer t.mux.Unlock()

t.rules = make([]Rule, 0)
for _, ruleTemplate := range config.Rules {
rule, err := NewRule(ruleTemplate.Match, ruleTemplate.Response)
rule, err := NewRuleFromTemplate(ruleTemplate)
if err != nil {
return err
}
t.options.rules = append(t.options.rules, *rule)
t.rules = append(t.rules, *rule)
}

gologger.Info().Msgf("TCP configuration loaded. Rules: %d\n", len(t.rules))

return nil
}

// MatchRule returns the rule, which was matched first
func (t *TCPServer) MatchRule(data []byte) (rule Rule, err error) {
t.mux.RLock()
defer t.mux.RUnlock()

// Process all the rules
for _, rule := range t.rules {
if rule.MatchInput(data) {
return rule, nil
}
}
return Rule{}, errors.New("no matched rule")
}

// BuildResponseWithContext is a wrapper with context
func (t *TCPServer) BuildResponseWithContext(ctx context.Context, data []byte) ([]byte, error) {
return t.BuildResponse(data)
}

// BuildResponseWithContext is a wrapper with context
func (t *TCPServer) BuildRuleResponse(ctx context.Context, data []byte) ([]byte, error) {
addr := "unknown"
if netAddr, ok := ctx.Value(Addr).(net.Addr); ok {
addr = netAddr.String()
}
rule, err := t.MatchRule(data)
if err != nil {
return []byte(":) "), err
}

gologger.Info().Msgf("Incoming TCP request(%s) from: %s\n", rule.Name, addr)

return []byte(rule.Response), nil
}