Skip to content

Commit bbfd855

Browse files
authored
Merge pull request #61 from c-f/livereload
Extension of TCP Server (livereload, binary matching, custom logic)
2 parents a556dcc + 420a99f commit bbfd855

File tree

9 files changed

+192
-17
lines changed

9 files changed

+192
-17
lines changed

README.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,9 @@ simplehttpserver -rule rules.yaml -tcp -tls -domain localhost
128128
The rules are written as follows:
129129
```yaml
130130
rules:
131-
- match: regex
131+
- match: regex-match
132+
match-contains: literal-match
133+
name: rule-name
132134
response: response data
133135
```
134136
@@ -137,6 +139,7 @@ For example to handle two different paths simulating an HTTP server or SMTP comm
137139
rules:
138140
# HTTP Requests
139141
- match: GET /path1
142+
name: redirect
140143
response: |
141144
HTTP/1.0 200 OK
142145
Server: httpd/2.0
@@ -149,13 +152,15 @@ rules:
149152
<HTML><HEAD><script>top.location.href='/Main_Login.asp';</script>
150153
</HEAD></HTML>
151154
- match: GET /path2
155+
name: "404"
152156
response: |
153157
HTTP/1.0 404 OK
154158
Server: httpd/2.0
155159
156160
<HTML><HEAD></HEAD><BODY>Not found</BODY></HTML>
157161
# SMTP Commands
158162
- match: "EHLO example.com"
163+
name: smtp
159164
response: |
160165
250-localhost Nice to meet you, [127.0.0.1]
161166
250-PIPELINING
@@ -167,6 +172,14 @@ rules:
167172
response: 250 Accepted
168173
- match: "RCPT TO: <[email protected]>"
169174
response: 250 Accepted
175+
176+
- match-contains: !!binary |
177+
MAwCAQFgBwIBAwQAgAA=
178+
name: "ldap"
179+
# Request: 300c 0201 0160 0702 0103 0400 8000 0....`........
180+
# Response: 300c 0201 0161 070a 0100 0400 0400 0....a........
181+
response: !!binary |
182+
MAwCAQFhBwoBAAQABAA=
170183
```
171184
172185
## Note

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module github.com/projectdiscovery/simplehttpserver
33
go 1.14
44

55
require (
6+
github.com/fsnotify/fsnotify v1.5.1 // indirect
67
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2
78
github.com/projectdiscovery/gologger v1.1.4
89
github.com/projectdiscovery/sslcert v0.0.0-20210416140253-8f56bec1bb5e

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
22
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
33
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
44
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
5+
github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI=
6+
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
57
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
68
github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68=
79
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
@@ -31,6 +33,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
3133
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
3234
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
3335
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
36+
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
37+
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
3438
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
3539
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
3640
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

internal/runner/runner.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ func New(options *Options) (*Runner, error) {
4242
if err != nil {
4343
return nil, err
4444
}
45+
watcher, err := watchFile(r.options.RulesFile, serverTCP.LoadTemplate)
46+
if err != nil {
47+
return nil, err
48+
}
49+
defer watcher.Close()
50+
4551
r.serverTCP = serverTCP
4652
return &r, nil
4753
}

internal/runner/watchdog.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package runner
2+
3+
import (
4+
"log"
5+
6+
"github.com/fsnotify/fsnotify"
7+
)
8+
9+
type WatchEvent func(fname string) error
10+
11+
func watchFile(fname string, callback WatchEvent) (watcher *fsnotify.Watcher, err error) {
12+
watcher, err = fsnotify.NewWatcher()
13+
if err != nil {
14+
return
15+
}
16+
go func() {
17+
for {
18+
select {
19+
case event, ok := <-watcher.Events:
20+
if !ok {
21+
continue
22+
}
23+
if event.Op&fsnotify.Write == fsnotify.Write {
24+
if err := callback(fname); err != nil {
25+
log.Println("err", err)
26+
}
27+
}
28+
case <-watcher.Errors:
29+
// ignore errors for now
30+
}
31+
}
32+
}()
33+
34+
err = watcher.Add(fname)
35+
return
36+
}

pkg/tcpserver/addr.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package tcpserver
2+
3+
// ContextType is the key type stored in ctx
4+
type ContextType string
5+
6+
var (
7+
// Addr is the contextKey where the net.Addr is stored
8+
Addr ContextType = "addr"
9+
)

pkg/tcpserver/responseengine.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@ import (
66

77
// BuildResponse according to rules
88
func (t *TCPServer) BuildResponse(data []byte) ([]byte, error) {
9+
t.mux.RLock()
10+
defer t.mux.RUnlock()
11+
912
// Process all the rules
10-
for _, rule := range t.options.rules {
11-
if rule.matchRegex.Match(data) {
13+
for _, rule := range t.rules {
14+
if rule.MatchInput(data) {
1215
return []byte(rule.Response), nil
1316
}
1417
}

pkg/tcpserver/rule.go

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package tcpserver
22

3-
import "regexp"
3+
import (
4+
"regexp"
5+
"strings"
6+
)
47

58
// RulesConfiguration from yaml
69
type RulesConfiguration struct {
@@ -9,17 +12,54 @@ type RulesConfiguration struct {
912

1013
// Rule to apply to various requests
1114
type Rule struct {
12-
Match string `yaml:"match,omitempty"`
13-
matchRegex *regexp.Regexp
14-
Response string `yaml:"response,omitempty"`
15+
Name string `yaml:"name,omitempty"`
16+
Match string `yaml:"match,omitempty"`
17+
MatchContains string `yaml:"match-contains,omitempty"`
18+
matchRegex *regexp.Regexp
19+
Response string `yaml:"response,omitempty"`
1520
}
1621

17-
// NewRule from model
22+
// NewRule creates a new Rule - default is regex
1823
func NewRule(match, response string) (*Rule, error) {
24+
return NewRegexRule(match, response)
25+
}
26+
27+
// NewRegexRule returns a new regex-match Rule
28+
func NewRegexRule(match, response string) (*Rule, error) {
1929
regxp, err := regexp.Compile(match)
2030
if err != nil {
2131
return nil, err
2232
}
2333

2434
return &Rule{Match: match, matchRegex: regxp, Response: response}, nil
2535
}
36+
37+
// NewLiteralRule returns a new literal-match Rule
38+
func NewLiteralRule(match, response string) (*Rule, error) {
39+
return &Rule{MatchContains: match, Response: response}, nil
40+
}
41+
42+
// NewRuleFromTemplate "copies" a new Rule
43+
func NewRuleFromTemplate(r Rule) (newRule *Rule, err error) {
44+
newRule = &Rule{
45+
Name: r.Name,
46+
Response: r.Response,
47+
MatchContains: r.MatchContains,
48+
Match: r.Match,
49+
}
50+
if newRule.Match != "" {
51+
newRule.matchRegex, err = regexp.Compile(newRule.Match)
52+
}
53+
54+
return
55+
}
56+
57+
// MatchInput returns if the input was matches with one of the matchers
58+
func (r *Rule) MatchInput(input []byte) bool {
59+
if r.matchRegex != nil && r.matchRegex.Match(input) {
60+
return true
61+
} else if r.MatchContains != "" && strings.Contains(string(input), r.MatchContains) {
62+
return true
63+
}
64+
return false
65+
}

pkg/tcpserver/tcpserver.go

Lines changed: 72 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package tcpserver
22

33
import (
4+
"context"
45
"crypto/tls"
6+
"errors"
57
"io/ioutil"
68
"net"
9+
"sync"
710
"time"
811

912
"github.com/projectdiscovery/gologger"
@@ -24,20 +27,35 @@ type Options struct {
2427
Verbose bool
2528
}
2629

30+
// CallBackFunc handles what is send back to the client, based on the incomming question
31+
type CallBackFunc func(ctx context.Context, question []byte) (answer []byte, err error)
32+
2733
// TCPServer instance
2834
type TCPServer struct {
2935
options *Options
3036
listener net.Listener
37+
38+
// Callbacks to retrieve information about the system
39+
HandleMessageFnc CallBackFunc
40+
41+
mux sync.RWMutex
42+
rules []Rule
3143
}
3244

3345
// New tcp server instance with specified options
3446
func New(options *Options) (*TCPServer, error) {
35-
return &TCPServer{options: options}, nil
47+
srv := &TCPServer{options: options}
48+
srv.HandleMessageFnc = srv.BuildResponseWithContext
49+
srv.rules = options.rules
50+
return srv, nil
3651
}
3752

3853
// AddRule to the server
3954
func (t *TCPServer) AddRule(rule Rule) error {
40-
t.options.rules = append(t.options.rules, rule)
55+
t.mux.Lock()
56+
defer t.mux.Unlock()
57+
58+
t.rules = append(t.rules, rule)
4159
return nil
4260
}
4361

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

54-
func (t *TCPServer) handleConnection(conn net.Conn) error {
72+
func (t *TCPServer) handleConnection(conn net.Conn, callback CallBackFunc) error {
5573
defer conn.Close() //nolint
5674

75+
// Create Context
76+
ctx := context.WithValue(context.Background(), Addr, conn.RemoteAddr())
77+
5778
buf := make([]byte, 4096)
5879
for {
5980
if err := conn.SetReadDeadline(time.Now().Add(readTimeout * time.Second)); err != nil {
6081
gologger.Info().Msgf("%s\n", err)
6182
}
62-
_, err := conn.Read(buf)
83+
n, err := conn.Read(buf)
6384
if err != nil {
6485
return err
6586
}
6687

67-
gologger.Print().Msgf("%s\n", buf)
88+
gologger.Print().Msgf("%s\n", buf[:n])
6889

69-
resp, err := t.BuildResponse(buf)
90+
resp, err := callback(ctx, buf[:n])
7091
if err != nil {
92+
gologger.Info().Msgf("Closing connection: %s\n", err)
7193
return err
7294
}
7395

@@ -112,7 +134,7 @@ func (t *TCPServer) run() error {
112134
if err != nil {
113135
return err
114136
}
115-
go t.handleConnection(c) //nolint
137+
go t.handleConnection(c, t.HandleMessageFnc) //nolint
116138
}
117139
}
118140

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

158+
t.mux.Lock()
159+
defer t.mux.Unlock()
160+
161+
t.rules = make([]Rule, 0)
136162
for _, ruleTemplate := range config.Rules {
137-
rule, err := NewRule(ruleTemplate.Match, ruleTemplate.Response)
163+
rule, err := NewRuleFromTemplate(ruleTemplate)
138164
if err != nil {
139165
return err
140166
}
141-
t.options.rules = append(t.options.rules, *rule)
167+
t.rules = append(t.rules, *rule)
142168
}
143169

170+
gologger.Info().Msgf("TCP configuration loaded. Rules: %d\n", len(t.rules))
171+
144172
return nil
145173
}
174+
175+
// MatchRule returns the rule, which was matched first
176+
func (t *TCPServer) MatchRule(data []byte) (rule Rule, err error) {
177+
t.mux.RLock()
178+
defer t.mux.RUnlock()
179+
180+
// Process all the rules
181+
for _, rule := range t.rules {
182+
if rule.MatchInput(data) {
183+
return rule, nil
184+
}
185+
}
186+
return Rule{}, errors.New("no matched rule")
187+
}
188+
189+
// BuildResponseWithContext is a wrapper with context
190+
func (t *TCPServer) BuildResponseWithContext(ctx context.Context, data []byte) ([]byte, error) {
191+
return t.BuildResponse(data)
192+
}
193+
194+
// BuildResponseWithContext is a wrapper with context
195+
func (t *TCPServer) BuildRuleResponse(ctx context.Context, data []byte) ([]byte, error) {
196+
addr := "unknown"
197+
if netAddr, ok := ctx.Value(Addr).(net.Addr); ok {
198+
addr = netAddr.String()
199+
}
200+
rule, err := t.MatchRule(data)
201+
if err != nil {
202+
return []byte(":) "), err
203+
}
204+
205+
gologger.Info().Msgf("Incoming TCP request(%s) from: %s\n", rule.Name, addr)
206+
207+
return []byte(rule.Response), nil
208+
}

0 commit comments

Comments
 (0)