├── exploding-heffalump.gif ├── .gitignore ├── LICENSE ├── heff ├── http.go └── markov.go ├── README.md └── heffalump.go /exploding-heffalump.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/earthboundkid/heffalump/HEAD/exploding-heffalump.gif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Carl Johnson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /heff/http.go: -------------------------------------------------------------------------------- 1 | package heff 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "net/http" 7 | "sync" 8 | "time" 9 | ) 10 | 11 | // DefaultHoneypot is an http.HandlerFunc that serves random HTML from the 12 | // DefaultMarkovMap, 100KB at a time. 13 | var DefaultHoneypot = NewHoneypot(DefaultMarkovMap, 100*1<<10) 14 | 15 | // NewHoneypot creates an http.HandlerFunc from a MarkovMap 16 | func NewHoneypot(mm MarkovMap, buffsize int) http.HandlerFunc { 17 | var pool sync.Pool 18 | 19 | getBuffer := func() []byte { 20 | x := pool.Get() 21 | if buf, ok := x.([]byte); ok { 22 | return buf 23 | } 24 | return make([]byte, buffsize) 25 | } 26 | 27 | putBuffer := func(buf []byte) { 28 | pool.Put(buf) 29 | } 30 | 31 | return func(w http.ResponseWriter, r *http.Request) { 32 | s := time.Now() 33 | log.Printf("Start FOR: %v USER AGENT: %q PATH: %q ", 34 | r.Header["X-Forwarded-For"], r.UserAgent(), r.URL) 35 | buf := getBuffer() 36 | defer putBuffer(buf) 37 | io.WriteString(w, "\n\n") 38 | n, err := io.CopyBuffer(w, mm, buf) 39 | log.Printf("Finish FOR: %v USER AGENT: %q PATH: %q "+ 40 | "BYTES: %d DURATION: %v ERROR: %v", 41 | r.Header["X-Forwarded-For"], r.UserAgent(), r.URL, 42 | n, time.Since(s), err) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Heffalump [![GoDoc](https://godoc.org/github.com/carlmjohnson/heffalump?status.svg)](https://godoc.org/github.com/carlmjohnson/heffalump) [![Go Report Card](https://goreportcard.com/badge/github.com/carlmjohnson/heffalump)](https://goreportcard.com/report/github.com/carlmjohnson/heffalump) 2 | Heffalump is an endless honeypot that gives malicious bots nightmares. To use, in your robots.txt tell robots not to go to a certain URL, which heffalump is reverse proxying. Any web agent that does go to the URL will receive an endless stream of random data, which will overflow its memory and/or storage if it doesn't have a max buffer size set or at the very least severely waste its time. 3 | 4 | The source of the honeypot data is [Once On a Time](http://www.gutenberg.org/files/27771/27771-h/27771-h.htm), one of A. A. Milne's most beloved and most public domain works. 5 | 6 | ![Exploding Heffalump](exploding-heffalump.gif) 7 | 8 | Live example: Do not follow this link. It will flood your browser's memory and likely cause a crash. 9 | 10 | ## Installation 11 | First install [Go](http://golang.org). 12 | 13 | If you just want to install the binary to your current directory and don't care about the source code, run 14 | 15 | ```shell 16 | GOBIN=$(pwd) GOPATH=$(mktemp -d) go get github.com/carlmjohnson/heffalump 17 | ``` 18 | 19 | ## Usage 20 | ``` 21 | Usage of heffalump: 22 | 23 | heffalump [opts] 24 | 25 | heffalump serves an endless HTTP honeypot 26 | 27 | -addr string 28 | Address to serve (default "127.0.0.1:8080") 29 | -path string 30 | Path to serve from. Path ending in / serves sub-paths. (default "/") 31 | ``` 32 | -------------------------------------------------------------------------------- /heffalump.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "io" 8 | "log" 9 | "net/http" 10 | "os" 11 | "os/signal" 12 | "syscall" 13 | "time" 14 | 15 | "github.com/carlmjohnson/heffalump/heff" 16 | ) 17 | 18 | const usage = `Usage of heffalump: 19 | 20 | heffalump [opts] 21 | 22 | heffalump serves an endless HTTP honeypot 23 | 24 | ` 25 | 26 | const robotsTxt = "User-agent: *\r\nDisallow: /\r\n" 27 | 28 | func main() { 29 | flag.Usage = func() { 30 | fmt.Fprintf(os.Stderr, usage) 31 | flag.PrintDefaults() 32 | } 33 | port := os.Getenv("PORT") 34 | if port == "" { 35 | port = "8080" 36 | } 37 | 38 | // subscribe to SIGINT signals 39 | stopChan := make(chan os.Signal, 1) 40 | signal.Notify(stopChan, syscall.SIGINT, syscall.SIGTERM) 41 | 42 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 43 | if r.URL.Path != "/" { 44 | http.NotFound(w, r) 45 | return 46 | } 47 | heff.DefaultHoneypot(w, r) 48 | }) 49 | 50 | http.HandleFunc("/robots.txt", func(w http.ResponseWriter, r *http.Request) { 51 | if _, err := io.WriteString(w, robotsTxt); err != nil { 52 | log.Printf("error serving robots.txt: %v", err) 53 | } 54 | }) 55 | 56 | srv := &http.Server{Addr: ":" + port, Handler: http.DefaultServeMux} 57 | 58 | go func() { 59 | // service connections 60 | err := srv.ListenAndServe() 61 | log.Printf("Finished listening: %v\n", err) 62 | }() 63 | 64 | <-stopChan // wait for SIGINT 65 | log.Println("Shutting down server...") 66 | 67 | // shut down gracefully, but wait no longer than 5 seconds before halting 68 | ctx, c := context.WithTimeout(context.Background(), 5*time.Second) 69 | defer c() 70 | srv.Shutdown(ctx) 71 | 72 | log.Println("Server gracefully stopped") 73 | } 74 | -------------------------------------------------------------------------------- /heff/markov.go: -------------------------------------------------------------------------------- 1 | package heff 2 | 3 | import ( 4 | "bufio" 5 | "io" 6 | "math/rand" 7 | "strings" 8 | "unicode" 9 | "unicode/utf8" 10 | ) 11 | 12 | // ScanHTML is a basic split function for a Scanner that returns each 13 | // space-separated word of text or HTML tag, with surrounding spaces deleted. 14 | // It will never return an empty string. The definition of space is set by 15 | // unicode.IsSpace. 16 | func ScanHTML(data []byte, atEOF bool) (advance int, token []byte, err error) { 17 | // Skip leading spaces. 18 | var r rune 19 | start := 0 20 | for width := 0; start < len(data); start += width { 21 | r, width = utf8.DecodeRune(data[start:]) 22 | if !unicode.IsSpace(r) { 23 | break 24 | } 25 | } 26 | if r == '<' { 27 | // Scan until closing bracket 28 | for i := start; i < len(data); i++ { 29 | if data[i] == '>' { 30 | return i + 1, data[start : i+1], nil 31 | } 32 | } 33 | } else { 34 | // Scan until space, marking end of word. 35 | for width, i := 0, start; i < len(data); i += width { 36 | var r rune 37 | r, width = utf8.DecodeRune(data[i:]) 38 | if unicode.IsSpace(r) { 39 | return i + width, data[start:i], nil 40 | } 41 | if r == '<' { 42 | return i, data[start:i], nil 43 | } 44 | } 45 | } 46 | // If we're at EOF, we have a final, non-empty, non-terminated word. Return it. 47 | if atEOF && len(data) > start { 48 | return len(data), data[start:], nil 49 | } 50 | // Request more data. 51 | return start, nil, nil 52 | } 53 | 54 | type tokenPair [2]string 55 | 56 | // DefaultMarkovMap is a Markov chain based on Src. 57 | var DefaultMarkovMap = MakeMarkovMap(strings.NewReader(Src)) 58 | 59 | // MarkovMap is a map that acts as a Markov chain generator. 60 | type MarkovMap map[tokenPair][]string 61 | 62 | // MakeMarkovMap makes an empty MakeMarkov and fills it with r. 63 | func MakeMarkovMap(r io.Reader) MarkovMap { 64 | m := MarkovMap{} 65 | m.Fill(r) 66 | return m 67 | } 68 | 69 | // Fill adds all the tokens in r to a MarkovMap 70 | func (mm MarkovMap) Fill(r io.Reader) { 71 | var w1, w2, w3 string 72 | 73 | s := bufio.NewScanner(r) 74 | s.Split(ScanHTML) 75 | for s.Scan() { 76 | w3 := s.Text() 77 | mm.Add(w1, w2, w3) 78 | w1, w2 = w2, w3 79 | } 80 | 81 | mm.Add(w1, w2, w3) 82 | } 83 | 84 | // Add adds a three token sequence to the map. 85 | func (mm MarkovMap) Add(w1, w2, w3 string) { 86 | p := tokenPair{w1, w2} 87 | mm[p] = append(mm[p], w3) 88 | } 89 | 90 | // Get pseudo-randomly chooses a possible suffix to w1 and w2. 91 | func (mm MarkovMap) Get(w1, w2 string) string { 92 | p := tokenPair{w1, w2} 93 | suffix, ok := mm[p] 94 | if !ok { 95 | return "" 96 | } 97 | 98 | r := rand.Intn(len(suffix)) 99 | return suffix[r] 100 | } 101 | 102 | // Read fills p with data from calling Get on the MarkovMap. 103 | func (mm MarkovMap) Read(p []byte) (n int, err error) { 104 | var w1, w2, w3 string 105 | 106 | for { 107 | w3 = mm.Get(w1, w2) 108 | if n+len(w3)+1 >= len(p) { 109 | break 110 | } 111 | n += copy(p[n:], w3) 112 | n += copy(p[n:], "\n") 113 | w1, w2 = w2, w3 114 | } 115 | 116 | return 117 | } 118 | --------------------------------------------------------------------------------