├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .goreleaser.yml ├── LICENSE ├── Makefile ├── README.md ├── attacker.go ├── config.go ├── config_test.go ├── example ├── ev.txt ├── ev2.txt ├── gzip.yml ├── minimal.http2.yml ├── minimal.yml ├── minimal_with_post.yml ├── nodes.yml ├── v1.txt ├── v2.txt └── vars.yml ├── go.mod ├── go.sum ├── indicator.go ├── main.go ├── remote.go └── statistics.go /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Check out code into the Go module directory 14 | uses: actions/checkout@v4 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v4 18 | with: 19 | go-version: 1.21 20 | 21 | - name: Use cache 22 | uses: actions/cache@v3 23 | with: 24 | path: ~/go/pkg/mod 25 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 26 | restore-keys: | 27 | ${{ runner.os }}-go- 28 | 29 | - name: Download Modules 30 | if: steps.cache.outputs.cache-hit != 'true' 31 | run: go mod download 32 | 33 | - name: Build 34 | run: | 35 | go build -o gohakai ./... 36 | rm gohakai 37 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*.*.*' 7 | 8 | jobs: 9 | goreleaser: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - 13 | name: Checkout 14 | uses: actions/checkout@v4 15 | - 16 | name: Unshallow 17 | run: git fetch --prune --unshallow 18 | - 19 | name: Set up Go 20 | uses: actions/setup-go@v4 21 | with: 22 | go-version: 1.21 23 | - 24 | name: Run GoReleaser 25 | uses: goreleaser/goreleaser-action@v4 26 | with: 27 | version: latest 28 | args: release --rm-dist 29 | key: ${{ secrets.YOUR_PRIVATE_KEY }} 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | project_name: gohakai 2 | env: 3 | - GO111MODULE=on 4 | before: 5 | hooks: 6 | - go mod tidy 7 | builds: 8 | - main: . 9 | binary: git-bump 10 | ldflags: 11 | - -s -w 12 | - -X main.Version={{.Version}} 13 | - -X main.GitCommit={{.ShortCommit}} 14 | env: 15 | - CGO_ENABLED=0 16 | archives: 17 | - name_template: '{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' 18 | replacements: 19 | darwin: darwin 20 | linux: linux 21 | windows: windows 22 | 386: i386 23 | amd64: x86_64 24 | format_overrides: 25 | - goos: windows 26 | format: zip 27 | release: 28 | prerelease: auto 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016-2018 KLab Inc. 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PKGDIR = pkg 2 | ZIPPED_PKGDIR = bz2pkg 3 | COMMIT = $$(git describe --always) 4 | 5 | all: build 6 | 7 | build: 8 | go build -ldflags "-X main.GitCommit=\"$(COMMIT)\"" -o gohakai 9 | 10 | build_all: 11 | @if [ ! -d $(PKGDIR) ]; then \ 12 | mkdir $(PKGDIR); \ 13 | fi 14 | @if [ ! -d $(ZIPPED_PKGDIR) ]; then \ 15 | mkdir $(ZIPPED_PKGDIR); \ 16 | fi 17 | GOOS=darwin GOARCH=386 go build -ldflags "-X main.GitCommit \"$(COMMIT)\"" -o $(PKGDIR)/gohakai.darwin.386 main.go indicator.go statistics.go config.go remote.go 18 | GOOS=darwin GOARCH=amd64 go build -ldflags "-X main.GitCommit \"$(COMMIT)\"" -o $(PKGDIR)/gohakai.darwin.amd64 main.go indicator.go statistics.go config.go remote.go 19 | GOOS=linux GOARCH=386 go build -ldflags "-X main.GitCommit \"$(COMMIT)\"" -o $(PKGDIR)/gohakai.linux.386 main.go indicator.go statistics.go config.go remote.go 20 | GOOS=linux GOARCH=amd64 go build -ldflags "-X main.GitCommit \"$(COMMIT)\"" -o $(PKGDIR)/gohakai.linux.amd64 main.go indicator.go statistics.go config.go remote.go 21 | GOOS=windows GOARCH=386 go build -ldflags "-X main.GitCommit \"$(COMMIT)\"" -o $(PKGDIR)/gohakai.windows.386 main.go indicator.go statistics.go config.go remote.go 22 | GOOS=windows GOARCH=amd64 go build -ldflags "-X main.GitCommit \"$(COMMIT)\"" -o $(PKGDIR)/gohakai.windows.amd64 main.go indicator.go statistics.go config.go remote.go 23 | bzip2 -c $(PKGDIR)/gohakai.darwin.386 > $(ZIPPED_PKGDIR)/gohakai.darwin.386.bz2 24 | bzip2 -c $(PKGDIR)/gohakai.darwin.amd64 > $(ZIPPED_PKGDIR)/gohakai.darwin.amd64.bz2 25 | bzip2 -c $(PKGDIR)/gohakai.linux.386 > $(ZIPPED_PKGDIR)/gohakai.linux.386.bz2 26 | bzip2 -c $(PKGDIR)/gohakai.linux.amd64 > $(ZIPPED_PKGDIR)/gohakai.linux.amd64.bz2 27 | bzip2 -c $(PKGDIR)/gohakai.windows.386 > $(ZIPPED_PKGDIR)/gohakai.windows.386.bz2 28 | bzip2 -c $(PKGDIR)/gohakai.windows.amd64 > $(ZIPPED_PKGDIR)/gohakai.windows.amd64.bz2 29 | 30 | clean: 31 | rm -f gohakai 32 | rm -rf $(PKGDIR) 33 | 34 | update-module: 35 | go get -u -v gopkg.in/yaml.v2 36 | go get -u -v golang.org/x/crypto/ssh 37 | go get -u -v golang.org/x/net/http2 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## gohakai [![CI](https://github.com/KLab/gohakai/actions/workflows/ci.yml/badge.svg)](https://github.com/KLab/gohakai/actions/workflows/ci.yml) 2 | 3 | Internet hakai with Go. 4 | 5 | Internet hakai is HTTP load testing tool. 6 | 7 | key feature: 8 | 9 | * request with test scenario 10 | * request from multi host 11 | * support HTTP/2 12 | 13 | 14 | ## Install 15 | ``` 16 | go get github.com/KLab/gohakai 17 | ``` 18 | 19 | 20 | ## Usage 21 | ``` 22 | $ gohakai -s 100 -c 10 config.yaml 23 | ``` 24 | -------------------------------------------------------------------------------- /attacker.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "compress/gzip" 5 | "fmt" 6 | "io" 7 | "log" 8 | "net/http" 9 | "net/url" 10 | "regexp" 11 | "strings" 12 | "sync" 13 | "time" 14 | ) 15 | 16 | type Attacker struct { 17 | Url *url.URL 18 | Client *http.Client 19 | Action map[string]interface{} 20 | UserAgent string 21 | Gzip bool 22 | QueryParams *map[string]string 23 | Headers *map[string]string 24 | ExVarOffset map[string]int 25 | sync.RWMutex 26 | } 27 | 28 | func (atk *Attacker) makeRequest() (req *http.Request, err error) { 29 | checkPath := ReplaceNames(atk.Action["path"].(string), atk.ExVarOffset) 30 | checkUrl, err := url.Parse(checkPath) 31 | if err != nil { 32 | log.Printf("url.Parse() Error: %v\n", err) 33 | return nil, err 34 | } 35 | 36 | atk.Url.Path = checkUrl.Path 37 | 38 | method, ret := atk.Action["method"] 39 | if !ret { 40 | method = "GET" 41 | } 42 | 43 | var content io.Reader 44 | values := url.Values{} 45 | postParams, retPostParams := atk.Action["post_params"] 46 | if method == "POST" && retPostParams { 47 | for k, v := range postParams.(map[interface{}]interface{}) { 48 | values.Add(k.(string), ReplaceNames(v.(string), atk.ExVarOffset)) 49 | } 50 | content = strings.NewReader(values.Encode()) 51 | } else { 52 | if _content, ret := atk.Action["content"]; ret { 53 | content = strings.NewReader(ReplaceNames(_content.(string), atk.ExVarOffset)) 54 | } 55 | } 56 | 57 | req, err = http.NewRequest(method.(string), atk.Url.String(), content) 58 | if err != nil { 59 | log.Printf("NewRequest Error: %v\n", err) 60 | return nil, err 61 | } 62 | contentType, ret := atk.Action["content_type"] 63 | if ret { 64 | req.Header.Set("Content-Type", contentType.(string)) 65 | } else if method == "POST" && retPostParams { 66 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 67 | } 68 | 69 | if atk.Gzip { 70 | req.Header.Set("Accept-Encoding", "gzip") 71 | } else { 72 | req.Header.Set("Accept-Encoding", "") 73 | } 74 | 75 | values = url.Values{} 76 | for k, v := range checkUrl.Query() { 77 | values.Add(k, v[0]) 78 | } 79 | for k, v := range *atk.QueryParams { 80 | values.Add(k, v) 81 | } 82 | req.URL.RawQuery = values.Encode() 83 | 84 | for k, v := range *atk.Headers { 85 | req.Header.Add(k, v) 86 | } 87 | 88 | req.Header.Set("User-Agent", atk.UserAgent) 89 | return req, err 90 | } 91 | 92 | func wrapRegexp(s string) interface{} { 93 | return regexp.MustCompile(s) 94 | } 95 | 96 | func (atk *Attacker) Attack() { 97 | req, err := atk.makeRequest() 98 | if err != nil { 99 | ok <- false 100 | return 101 | } 102 | 103 | if verbose { 104 | if len(req.URL.RawQuery) >= 1 { 105 | log.Printf("%s %s?%s\n", req.Method, req.URL.Path, req.URL.RawQuery) 106 | } else { 107 | log.Printf("%s %s\n", req.Method, req.URL.Path) 108 | } 109 | } 110 | 111 | t0 := time.Now() 112 | res, err := atk.Client.Do(req) 113 | if err != nil { 114 | log.Printf("request error: %v\n", err) 115 | ok <- false 116 | return 117 | } 118 | defer res.Body.Close() 119 | 120 | t1 := time.Now() 121 | diffTime := t1.Sub(t0) 122 | 123 | validRes := true 124 | atk.RLock() 125 | _scan, ret := atk.Action["scan"] 126 | atk.RUnlock() 127 | if ret { 128 | // check body text 129 | var reader io.ReadCloser 130 | switch res.Header.Get("Content-Encoding") { 131 | case "gzip", "deflate": 132 | reader, _ = gzip.NewReader(res.Body) 133 | defer reader.Close() 134 | default: 135 | reader = res.Body 136 | } 137 | body, _ := io.ReadAll(reader) 138 | 139 | // memoization 140 | var scan *regexp.Regexp 141 | _s, _ok := _scan.(string) 142 | if _ok { 143 | atk.Lock() 144 | atk.Action["scan"] = wrapRegexp(_s) 145 | atk.Unlock() 146 | } 147 | atk.RLock() 148 | scan = atk.Action["scan"].(*regexp.Regexp) 149 | atk.RUnlock() 150 | 151 | if scan.Match(body) { 152 | names := scan.SubexpNames() 153 | for _, tname := range scan.FindAllStringSubmatch(string(body), -1) { 154 | for i, name := range tname[1:] { 155 | VARS_MUTEX.Lock() 156 | SCANNED_VARS[names[i+1]] = name 157 | VARS_MUTEX.Unlock() 158 | } 159 | } 160 | } else { 161 | validRes = false 162 | log.Println(atk.Url) 163 | fmt.Print(string(body)) 164 | } 165 | } else { 166 | if _, err := io.ReadAll(res.Body); err != nil { 167 | log.Println(err) 168 | } 169 | } 170 | 171 | if verbose { 172 | log.Println(diffTime, res.StatusCode, res.ContentLength) 173 | } 174 | 175 | m.Lock() 176 | PathCount[atk.Url.Path] += 1 177 | PathTime[atk.Url.Path] += diffTime 178 | m.Unlock() 179 | 180 | if validRes && res.StatusCode/10 == 20 { 181 | ok <- true 182 | } else { 183 | ok <- false 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/gob" 7 | "log" 8 | "math/rand" 9 | "os" 10 | "os/user" 11 | "path/filepath" 12 | "regexp" 13 | "strconv" 14 | "strings" 15 | "sync" 16 | "time" 17 | 18 | "gopkg.in/yaml.v2" 19 | ) 20 | 21 | const GOB_FILE = ".gohakai.gob" 22 | 23 | var CONFIG_ROOT string 24 | var CONSTS map[string]string 25 | var EXVARS map[string]*ExVer 26 | var VARS map[string][]string 27 | var SCANNED_VARS map[string]string 28 | var NODES []Node 29 | var re *regexp.Regexp = regexp.MustCompile(`%\((.+?)\)%`) 30 | var VARS_MUTEX sync.RWMutex 31 | 32 | type ExVer struct { 33 | Value []string 34 | Offset int 35 | } 36 | 37 | // for remote config 38 | type AllVars struct { 39 | Vars map[string][]string 40 | ExVars map[string]*ExVer 41 | } 42 | 43 | type Config struct { 44 | Domain string `yaml:"domain"` 45 | UserAgent string `yaml:"user_agent"` 46 | ShowReport bool `yaml:"show_report"` 47 | Gzip bool `yaml:"gzip"` 48 | Timeout uint16 `yaml:"timeout"` 49 | Nodes []map[string]interface{} `yaml:"nodes"` 50 | Actions []map[string]interface{} `yaml:"actions"` 51 | QueryParams map[string]string `yaml:"query_params"` 52 | Consts map[string]string `yaml:"consts"` 53 | ExVars []map[string]string `yaml:"exvars"` 54 | Vars []map[string]string `yaml:"vars"` 55 | Headers map[string]string `yaml:"headers"` 56 | HTTPVersion int `yaml:"http_version"` 57 | } 58 | 59 | func ReplaceNames(input string, offset map[string]int) string { 60 | cb := func(s string) string { 61 | tname := re.FindAllStringSubmatch(s, -1) 62 | 63 | for idx, t := range tname { 64 | if c, ok := CONSTS[t[1]]; ok { 65 | return c 66 | } 67 | 68 | if v, ok := VARS[t[1]]; ok { 69 | return v[rand.Intn(len(v))] 70 | } 71 | 72 | if e, ok := EXVARS[t[1]]; ok { 73 | _e := e.Value[offset[t[1]]] 74 | return _e 75 | } 76 | 77 | VARS_MUTEX.RLock() 78 | s, ok := SCANNED_VARS[t[1]] 79 | VARS_MUTEX.RUnlock() 80 | if ok { 81 | return s 82 | } 83 | 84 | if idx == 0 { 85 | return t[0] 86 | } 87 | } 88 | 89 | return tname[0][0] 90 | } 91 | ret := re.ReplaceAllStringFunc(input, cb) 92 | 93 | return ret 94 | } 95 | 96 | func loadVarsFromFile(filename string) (lines []string) { 97 | f, err := os.Open(filepath.Join(CONFIG_ROOT, filename)) 98 | if err != nil { 99 | log.Printf("os.Open() error: %v\n", err) 100 | os.Exit(-1) 101 | } 102 | defer f.Close() 103 | 104 | scanner := bufio.NewScanner(f) 105 | for scanner.Scan() { 106 | lines = append(lines, scanner.Text()) 107 | } 108 | 109 | return lines 110 | } 111 | 112 | func loadVarsFromGobFile() { 113 | buf, err := os.ReadFile(GOB_FILE) 114 | if err != nil { 115 | log.Println("ReadFile() error:", err) 116 | os.Exit(-1) 117 | } 118 | 119 | b := bytes.NewBuffer(buf) 120 | var v AllVars 121 | dec := gob.NewDecoder(b) 122 | err = dec.Decode(&v) 123 | if err != nil { 124 | log.Fatal("decode:", err) 125 | } 126 | 127 | EXVARS = v.ExVars 128 | } 129 | 130 | // dump gob file 131 | func dumpVars(filename string, offset, procs, allProcs int) { 132 | var buf bytes.Buffer 133 | 134 | offsets := []int{} 135 | for i := offset; i < (offset + procs); i++ { 136 | offsets = append(offsets, i) 137 | } 138 | 139 | ex := map[string]*ExVer{} 140 | for key, val := range EXVARS { 141 | newValue := []string{} 142 | for _, o := range offsets { 143 | for i := o; i < len(val.Value); i += allProcs { 144 | newValue = append(newValue, val.Value[i]) 145 | } 146 | } 147 | 148 | ex[key] = &ExVer{Value: newValue, Offset: 0} 149 | } 150 | 151 | // Create an encoder and send a value. 152 | var v AllVars = AllVars{ExVars: ex, Vars: VARS} 153 | enc := gob.NewEncoder(&buf) 154 | err := enc.Encode(v) 155 | if err != nil { 156 | log.Fatal("encode:", err) 157 | } 158 | 159 | if err := os.WriteFile(filename, buf.Bytes(), os.ModePerm); err != nil { 160 | log.Fatal("write gob file error:", err) 161 | } 162 | } 163 | 164 | func (c *Config) loadVars() { 165 | if MODE_NORMAL != ExecMode { 166 | // when remote execution (from gob file) 167 | loadVarsFromGobFile() 168 | } else { 169 | // when local execution 170 | for _, v := range c.ExVars { 171 | EXVARS[v["name"]] = &ExVer{Value: loadVarsFromFile(v["file"])} 172 | } 173 | for _, v := range c.Vars { 174 | VARS[v["name"]] = loadVarsFromFile(v["file"]) 175 | } 176 | } 177 | } 178 | 179 | func (c *Config) loadNodes() { 180 | for _, v := range c.Nodes { 181 | // proc 182 | proc := 1 183 | tmp, ok := v["proc"] 184 | if ok { 185 | proc = tmp.(int) 186 | } 187 | 188 | if MODE_NORMAL != ExecMode { 189 | continue 190 | } 191 | 192 | // user 193 | var username string 194 | u, err := user.Current() 195 | if err != nil { 196 | panic("user.Current error:") 197 | } 198 | username = u.Username 199 | 200 | // host & port 201 | var hostname string 202 | var port int = 22 203 | tmp, ok = v["host"] 204 | if ok { 205 | s := tmp.(string) 206 | ss := strings.Split(s, "@") 207 | if len(ss) == 2 { 208 | username = ss[0] 209 | hostname = ss[1] 210 | } else { 211 | hostname = ss[0] 212 | } 213 | } 214 | ss := strings.Split(hostname, ":") 215 | if len(ss) == 2 { 216 | hostname = ss[0] 217 | port, err = strconv.Atoi(ss[1]) 218 | if err != nil { 219 | panic(v) 220 | } 221 | } 222 | 223 | // ssh key 224 | var sshKeyFile string = "~/.ssh/id_rsa" 225 | tmp, ok = v["ssh_key"] 226 | if ok { 227 | sshKeyFile = tmp.(string) 228 | } 229 | sshKeyFile = strings.Replace(sshKeyFile, "~", u.HomeDir, 1) 230 | 231 | n := Node{ 232 | Proc: proc, 233 | Port: port, 234 | Host: hostname, 235 | User: username, 236 | SSHKeyFile: sshKeyFile, 237 | } 238 | NODES = append(NODES, n) 239 | } 240 | } 241 | 242 | func (c *Config) Load(filename string) error { 243 | rand.Seed(time.Now().Unix()) 244 | buf, err := os.ReadFile(filename) 245 | if err != nil { 246 | return err 247 | } 248 | 249 | if err = yaml.Unmarshal(buf, &c); err != nil { 250 | log.Printf("'%s' yaml unmarshal error: %v\n", filename, err) 251 | return err 252 | } 253 | 254 | // set default value 255 | if c.Timeout <= 0 { 256 | c.Timeout = 1 257 | } 258 | if c.UserAgent == "" { 259 | c.UserAgent = DEFALUT_USER_AGENT 260 | } 261 | if c.Domain == "" { 262 | c.Domain = DEFALT_DOMAIN 263 | } 264 | 265 | CONFIG_ROOT = filepath.Dir(filename) 266 | 267 | NODES = []Node{} 268 | VARS = map[string][]string{} 269 | EXVARS = map[string]*ExVer{} 270 | CONSTS = c.Consts 271 | SCANNED_VARS = map[string]string{} 272 | 273 | c.loadVars() 274 | c.loadNodes() 275 | 276 | return nil 277 | } 278 | -------------------------------------------------------------------------------- /config_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | func TestReplaceNames(t *testing.T) { 6 | cases := []struct { 7 | name string 8 | path string 9 | params map[string]int 10 | want []string 11 | }{ 12 | { 13 | "const1", 14 | "/simple?v=%(c1)%", 15 | map[string]int{}, 16 | []string{"/simple?v=hoge"}, 17 | }, 18 | { 19 | "vars1", 20 | "/simple?v=%(v1)%", 21 | map[string]int{}, 22 | []string{"/simple?v=v1_0001", "/simple?v=v1_0002"}, 23 | }, 24 | { 25 | "exvars1", 26 | "/simple?v=%(ev1)%", 27 | map[string]int{}, 28 | []string{"/simple?v=10001"}, 29 | }, 30 | { 31 | "non vars", 32 | "/simple", 33 | map[string]int{}, 34 | []string{"/simple"}, 35 | }, 36 | { 37 | "empty expand vars", 38 | "/simple?v=%()%", 39 | map[string]int{}, 40 | []string{"/simple?v=%()%"}, 41 | }, 42 | { 43 | "non expand vars", 44 | "/simple?vvv=%(dummyvalue)%", 45 | map[string]int{}, 46 | []string{"/simple?vvv=%(dummyvalue)%"}, 47 | }, 48 | { 49 | "empty path", 50 | "", 51 | map[string]int{}, 52 | []string{""}, 53 | }, 54 | { 55 | "blank", 56 | " ", 57 | map[string]int{}, 58 | []string{" "}, 59 | }, 60 | } 61 | 62 | config := Config{} 63 | if err := config.Load("example/vars.yml"); err != nil { 64 | t.Fatal("fail config loading") 65 | } 66 | for _, tt := range cases { 67 | ret := ReplaceNames(tt.path, map[string]int{}) 68 | ok := false 69 | for _, w := range tt.want { 70 | if ret == w { 71 | ok = true 72 | break 73 | } 74 | } 75 | if !ok { 76 | t.Fatalf("%s invalid result: want=%s, ret=%s", tt.name, tt.want, ret) 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /example/ev.txt: -------------------------------------------------------------------------------- 1 | 10001 2 | 10002 3 | 10003 4 | 20001 5 | 20003 6 | 20006 7 | 40000 8 | 40009 9 | 40010 10 | 40015 11 | 50001 12 | 50002 13 | 50003 14 | 50004 15 | 50005 16 | 50006 17 | 50007 18 | 50008 19 | 50009 20 | 50010 21 | 50011 22 | 50012 23 | 50013 24 | 50014 25 | 50015 26 | 50016 27 | 60005 28 | 60006 29 | 60007 30 | 60008 31 | 60009 32 | 60010 33 | 60011 34 | 60012 35 | 60014 36 | 60017 37 | 60018 38 | 60019 39 | 60100 40 | 60101 41 | 60122 42 | 60123 43 | 60124 44 | 60125 45 | 60200 46 | 60201 47 | 60202 48 | 60203 49 | 60205 50 | -------------------------------------------------------------------------------- /example/ev2.txt: -------------------------------------------------------------------------------- 1 | 10 2 | 09 3 | 08 4 | 07 5 | -------------------------------------------------------------------------------- /example/gzip.yml: -------------------------------------------------------------------------------- 1 | # minimal.yml 2 | domain: http://localhost:8000/ 3 | 4 | timeout: 30 5 | 6 | gzip: true 7 | 8 | actions: 9 | - path: / 10 | scan: "Hello" 11 | - path: /tasks.html 12 | 13 | -------------------------------------------------------------------------------- /example/minimal.http2.yml: -------------------------------------------------------------------------------- 1 | # minimal.yml 2 | domain: https://localhost:8000 3 | 4 | http_version: 2 5 | 6 | actions: 7 | - path: / 8 | - path: /hello 9 | 10 | -------------------------------------------------------------------------------- /example/minimal.yml: -------------------------------------------------------------------------------- 1 | # minimal.yml 2 | domain: http://localhost:8000 3 | 4 | actions: 5 | - path: / 6 | - path: /hello 7 | 8 | -------------------------------------------------------------------------------- /example/minimal_with_post.yml: -------------------------------------------------------------------------------- 1 | # minimal.yml 2 | domain: http://localhost:8000 3 | 4 | actions: 5 | - path: / 6 | method: POST 7 | content: '{"a": true, "b": 1}' 8 | content_type: 'application/json' 9 | - path: /hello 10 | 11 | show_report: true 12 | -------------------------------------------------------------------------------- /example/nodes.yml: -------------------------------------------------------------------------------- 1 | domain: http://127.0.0.1/ 2 | 3 | log_level: 3 4 | 5 | nodes: 6 | - host: vagrant@192.168.1.100 7 | proc: 2 8 | ssh_key: ~/.ssh/id_rsa 9 | - host: vagrant@192.168.1.101 10 | proc: 2 11 | ssh_key: ~/.ssh/id_rsa 12 | - host: localhost 13 | proc: 1 14 | 15 | actions: 16 | - path: / 17 | 18 | exvars: 19 | - name: ev1 20 | file: ev.txt 21 | - name: ev2 22 | file: ev.txt 23 | 24 | vars: 25 | - name: v1 26 | file: v1.txt 27 | - name: v2 28 | file: v2.txt 29 | 30 | consts: 31 | c1: hoge 32 | c2: fuga 33 | 34 | query_params: 35 | qp1: "%(ev1)%" 36 | qp2: "%(c1)%" 37 | qp3: "%(c2)%" 38 | -------------------------------------------------------------------------------- /example/v1.txt: -------------------------------------------------------------------------------- 1 | v1_0001 2 | v1_0002 3 | -------------------------------------------------------------------------------- /example/v2.txt: -------------------------------------------------------------------------------- 1 | v2_2001 2 | v2_2002 3 | -------------------------------------------------------------------------------- /example/vars.yml: -------------------------------------------------------------------------------- 1 | domain: http://localhost:8000 2 | 3 | log_level: 3 4 | 5 | actions: 6 | - path: / 7 | - path: "/hello?v1=%(v1)%&non2=v5&ev2=%(ev2)%&v2=%(v2)%" 8 | 9 | exvars: 10 | - name: ev1 11 | file: ev.txt 12 | - name: ev2 13 | file: ev2.txt 14 | 15 | vars: 16 | - name: v1 17 | file: v1.txt 18 | - name: v2 19 | file: v2.txt 20 | 21 | consts: 22 | c1: hoge 23 | c2: fuga 24 | 25 | query_params: 26 | qp1: "%(ev1)%" 27 | qp2: "%(c1)%" 28 | qp3: "%(c2)%" 29 | 30 | headers: 31 | X-Foo: "%(ev2)%" 32 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/KLab/gohakai 2 | 3 | go 1.19 4 | 5 | require ( 6 | golang.org/x/crypto v0.17.0 7 | golang.org/x/net v0.17.0 8 | gopkg.in/yaml.v2 v2.4.0 9 | ) 10 | 11 | require ( 12 | golang.org/x/sys v0.15.0 // indirect 13 | golang.org/x/text v0.14.0 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= 2 | golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= 3 | golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= 4 | golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= 5 | golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= 6 | golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 7 | golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= 8 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 9 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 10 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 11 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 12 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 13 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 14 | -------------------------------------------------------------------------------- /indicator.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | ) 7 | 8 | var SUCCESS uint32 9 | var FAIL uint32 10 | 11 | func Indicator(fin chan bool, wg *sync.WaitGroup) { 12 | var skip int 13 | for { 14 | select { 15 | case ret := <-ok: 16 | if MODE_NORMAL != ExecMode { 17 | continue 18 | } 19 | 20 | skip += 1 21 | if ret { 22 | SUCCESS += 1 23 | if skip >= 100 { 24 | fmt.Printf(".") 25 | skip = 0 26 | } 27 | } else { 28 | FAIL += 1 29 | fmt.Printf("x") 30 | } 31 | case <-fin: 32 | wg.Done() 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/tls" 5 | "flag" 6 | "fmt" 7 | "log" 8 | "net/http" 9 | "net/http/cookiejar" 10 | "net/url" 11 | "os" 12 | "sync" 13 | "time" 14 | 15 | "golang.org/x/net/http2" 16 | ) 17 | 18 | const ( 19 | DEFALT_DOMAIN = "http://localhost:8000" 20 | DEFALUT_USER_AGENT = "gohakai" 21 | REMOTE_CONF = ".gohakai.config.yml" 22 | HAKAI_BIN_NAME = "gohakai" 23 | MODE_NORMAL = "default" 24 | MODE_NODE = "node" 25 | MODE_NODE_LOCAL = "node-local" 26 | ) 27 | 28 | var client http.Client 29 | 30 | var Version string 31 | var ExecMode string = MODE_NORMAL 32 | var GitCommit string 33 | var PathCount map[string]uint32 34 | var PathTime map[string]time.Duration 35 | var ok chan bool 36 | var verbose bool 37 | var m sync.Mutex 38 | 39 | type Worker struct { 40 | Client http.Client 41 | Config *Config 42 | ExVarOffset map[string]int 43 | } 44 | 45 | func hakai(c http.Client, config *Config, offset map[string]int) { 46 | u, err := url.Parse(config.Domain) 47 | if err != nil { 48 | log.Fatal(err) 49 | } 50 | 51 | queryParams := map[string]string{} 52 | for k, v := range config.QueryParams { 53 | vv := ReplaceNames(v, offset) 54 | queryParams[k] = vv 55 | } 56 | 57 | headers := map[string]string{} 58 | for k, v := range config.Headers { 59 | headers[k] = ReplaceNames(v, offset) 60 | } 61 | 62 | cookieJar, _ := cookiejar.New(nil) 63 | c.Jar = cookieJar 64 | attacker := Attacker{ 65 | Client: &c, 66 | Url: u, 67 | Gzip: config.Gzip, 68 | UserAgent: config.UserAgent, 69 | QueryParams: &queryParams, 70 | Headers: &headers, 71 | ExVarOffset: offset, 72 | } 73 | for _, action := range config.Actions { 74 | attacker.Action = action 75 | attacker.Attack() 76 | } 77 | } 78 | 79 | func worker(id int, wg *sync.WaitGroup, limiter chan Worker) { 80 | for { 81 | ret := <-limiter 82 | hakai(ret.Client, ret.Config, ret.ExVarOffset) 83 | wg.Done() 84 | } 85 | } 86 | 87 | func setupNode(configFile string) { 88 | var wg sync.WaitGroup 89 | var i, allProcs int 90 | 91 | for key := range NODES { 92 | allProcs += NODES[key].Proc 93 | } 94 | 95 | // scp when nodes option 96 | for key := range NODES { 97 | if NODES[key].Host == "localhost" { 98 | go func(_n Node, o, p int) { 99 | dumpVars(GOB_FILE, o, _n.Proc, p) 100 | }(NODES[key], i, allProcs) 101 | } else { 102 | wg.Add(1) 103 | go func(_n Node, o, p int) { 104 | srcGob, err := os.CreateTemp(os.TempDir(), fmt.Sprintf("%s.node.%s", GOB_FILE, _n.Host)) 105 | if err != nil { 106 | log.Println("CreateTemp() error:", err) 107 | return 108 | } 109 | defer os.Remove(srcGob.Name()) 110 | defer wg.Done() 111 | 112 | // scp for gohakai (self-propagation!!) 113 | // TODO: cofigurable? remote is same architecture, now. 114 | src := HAKAI_BIN_NAME 115 | dst := HAKAI_BIN_NAME 116 | if err := _n.Scp(src, dst); err != nil { 117 | log.Println("scp error:", err) 118 | return 119 | } 120 | 121 | // config file 122 | if err := _n.Scp(configFile, REMOTE_CONF); err != nil { 123 | log.Println("scp error:", err) 124 | return 125 | } 126 | 127 | // all vars file 128 | dst = GOB_FILE 129 | dumpVars(srcGob.Name(), o, _n.Proc, p) 130 | if err := _n.Scp(srcGob.Name(), dst); err != nil { 131 | log.Println("scp error:", err) 132 | return 133 | } 134 | }(NODES[key], i, allProcs) 135 | } 136 | 137 | i += NODES[key].Proc 138 | } 139 | 140 | wg.Wait() 141 | 142 | fmt.Println("setup node end") 143 | } 144 | 145 | func attackNode(configFile string, c chan string, wg *sync.WaitGroup) { 146 | for key := range NODES { 147 | wg.Add(1) 148 | if NODES[key].Host == "localhost" { 149 | go func(node Node) { 150 | if err := node.LocalAttack(configFile, c); err != nil { 151 | log.Println("local attack:", err, node) 152 | } 153 | }(NODES[key]) 154 | } else { 155 | go func(node Node) { 156 | if err := node.RemoteAttack(c); err != nil { 157 | log.Println("remote attack:", err, node) 158 | } 159 | }(NODES[key]) 160 | } 161 | } 162 | } 163 | 164 | func localMain(loop, maxScenario, maxRequest, totalDuration int, config *Config, stats *Statistics) { 165 | var wg sync.WaitGroup 166 | var wgIndicator sync.WaitGroup 167 | redirectFunc := func(req *http.Request, via []*http.Request) error { 168 | if len(via) > 10 { 169 | return fmt.Errorf("%d consecutive requests(redirects)", len(via)) 170 | } 171 | if len(via) == 0 { 172 | // No redirects 173 | return nil 174 | } 175 | // mutate the subsequent redirect requests with the first Header 176 | for key, val := range via[0].Header { 177 | req.Header[key] = val 178 | referer := req.Referer() 179 | checkUrl, err := url.Parse(referer) 180 | if err != nil { 181 | log.Printf("url.Parse() Error: %v\n", err) 182 | return err 183 | } 184 | req.URL.RawQuery = checkUrl.RawQuery 185 | } 186 | return nil 187 | } 188 | 189 | if config.HTTPVersion == 2 { 190 | client = http.Client{ 191 | Transport: &http2.Transport{ 192 | TLSClientConfig: &tls.Config{ 193 | InsecureSkipVerify: false, 194 | }, 195 | }, 196 | CheckRedirect: redirectFunc, 197 | } 198 | } else { 199 | client = http.Client{ 200 | Transport: &http.Transport{ 201 | MaxIdleConnsPerHost: maxRequest, // default is 2 202 | }, 203 | Timeout: time.Duration(config.Timeout) * time.Second, // default is 30 204 | CheckRedirect: redirectFunc, 205 | } 206 | } 207 | 208 | limiter := make(chan Worker, maxRequest) 209 | stats.MaxRequest = maxRequest 210 | stats.StartTime = time.Now() 211 | 212 | // exec worker 213 | for num := 0; num < maxRequest; num++ { 214 | go worker(num, &wg, limiter) 215 | } 216 | 217 | // exec indicator & total duration 218 | ok = make(chan bool) 219 | indicatorFin := make(chan bool) 220 | go Indicator(indicatorFin, &wgIndicator) 221 | wgIndicator.Add(1) 222 | if totalDuration != 0 { 223 | go stats.PrintAfterDuration(totalDuration) 224 | } 225 | 226 | // attack 227 | for i := 0; i < loop*maxScenario; i++ { 228 | wg.Add(1) 229 | offset := map[string]int{} 230 | for k := range EXVARS { 231 | offset[k] = EXVARS[k].Offset 232 | EXVARS[k].Offset += 1 233 | if EXVARS[k].Offset >= len(EXVARS[k].Value) { 234 | EXVARS[k].Offset = 0 235 | } 236 | } 237 | w := Worker{Client: client, Config: config, ExVarOffset: offset} 238 | limiter <- w 239 | } 240 | 241 | // wait all request & response 242 | wg.Wait() 243 | indicatorFin <- true 244 | wgIndicator.Wait() 245 | } 246 | 247 | func clean() { 248 | if _, err := os.Stat(GOB_FILE); err == nil { 249 | os.Remove(GOB_FILE) 250 | } 251 | 252 | if _, err := os.Stat(REMOTE_CONF); err == nil { 253 | os.Remove(REMOTE_CONF) 254 | } 255 | 256 | if ExecMode == MODE_NODE { 257 | os.Remove(HAKAI_BIN_NAME) 258 | } 259 | } 260 | 261 | func usage() { 262 | fmt.Fprintln(os.Stderr, "gohakai - Internet Hakai with Go") 263 | fmt.Fprintf(os.Stderr, "version:%s, id:%s\n\n", Version, GitCommit) 264 | fmt.Fprintln(os.Stderr, "Usage: gohakai [option] config.yaml") 265 | flag.PrintDefaults() 266 | os.Exit(0) 267 | } 268 | 269 | func main() { 270 | if MODE_NODE == os.Getenv("GOHAKAI") { 271 | ExecMode = MODE_NODE 272 | } else if MODE_NODE_LOCAL == os.Getenv("GOHAKAI") { 273 | ExecMode = MODE_NODE_LOCAL 274 | } 275 | 276 | config := Config{} 277 | statistics := Statistics{} 278 | statistics.Config = &config 279 | var maxScenario, maxRequest, loop, totalDuration int 280 | 281 | // command line option 282 | flag.IntVar(&maxScenario, "s", 1, "max scenario") 283 | flag.IntVar(&maxRequest, "c", 0, "max concurrency requests") 284 | flag.IntVar(&loop, "n", 1, "scenario exec N-loop") 285 | flag.IntVar(&totalDuration, "d", 0, "total duration") 286 | flag.BoolVar(&verbose, "verbose", false, "verbose mode") 287 | 288 | flag.Parse() 289 | args := flag.Args() 290 | if len(args) < 1 { 291 | usage() 292 | } 293 | configFile := args[0] 294 | 295 | if err := config.Load(configFile); err != nil { 296 | usage() 297 | } 298 | 299 | if maxRequest == 0 { 300 | maxRequest = maxScenario 301 | } 302 | 303 | PathCount = map[string]uint32{} 304 | PathTime = map[string]time.Duration{} 305 | 306 | if len(config.Nodes) >= 1 && ExecMode == MODE_NORMAL { 307 | statChan := make(chan string) 308 | var statWg sync.WaitGroup 309 | 310 | setupNode(configFile) 311 | go statistics.Collector(statChan, &statWg) 312 | 313 | attackNode(configFile, statChan, &statWg) 314 | statWg.Wait() 315 | } else { 316 | localMain(loop, maxScenario, maxRequest, totalDuration, &config, &statistics) 317 | finishTime := time.Now() 318 | statistics.Delta = finishTime.Sub(statistics.StartTime) 319 | } 320 | 321 | statistics.Print() 322 | 323 | clean() 324 | } 325 | -------------------------------------------------------------------------------- /remote.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "flag" 7 | "fmt" 8 | "io" 9 | "log" 10 | "os" 11 | "os/exec" 12 | "strings" 13 | 14 | "golang.org/x/crypto/ssh" 15 | ) 16 | 17 | type Node struct { 18 | Proc int 19 | Port int 20 | Host string 21 | User string 22 | SSHKeyFile string 23 | Session *ssh.Session 24 | } 25 | 26 | func (n *Node) NewSSHSession() (session *ssh.Session, err error) { 27 | pkey, err := os.ReadFile(n.SSHKeyFile) 28 | if err != nil { 29 | log.Println("ReadFile(sshkey):", err) 30 | return session, err 31 | } 32 | 33 | s, err := ssh.ParsePrivateKey(pkey) 34 | if err != nil { 35 | log.Println("ssh.ParsePrivateKey():", err) 36 | return session, err 37 | } 38 | 39 | config := &ssh.ClientConfig{ 40 | User: n.User, 41 | Auth: []ssh.AuthMethod{ 42 | ssh.PublicKeys(s), 43 | }, 44 | } 45 | 46 | host := fmt.Sprintf("%s:%d", n.Host, n.Port) 47 | client, err := ssh.Dial("tcp", host, config) 48 | if err != nil { 49 | log.Println("ssh.Dial:", err) 50 | return session, err 51 | } 52 | 53 | session, err = client.NewSession() 54 | if err != nil { 55 | log.Println("cli.NewSession():", err) 56 | return session, err 57 | } 58 | 59 | return session, err 60 | } 61 | 62 | // scp for self exec file and config.yml 63 | func (n *Node) Scp(src, dst string) (err error) { 64 | n.Session, err = n.NewSSHSession() 65 | if err != nil { 66 | log.Println("new ssh session error:", err) 67 | return err 68 | } 69 | defer n.Session.Close() 70 | 71 | go func() { 72 | w, _ := n.Session.StdinPipe() 73 | defer w.Close() 74 | src, _ := os.Open(src) 75 | srcStat, _ := src.Stat() 76 | fmt.Fprintln(w, "C0755", srcStat.Size(), dst) 77 | if _, err := io.Copy(w, src); err != nil { 78 | log.Println("io.copy error", err) 79 | return 80 | } 81 | fmt.Fprint(w, "\x00") 82 | }() 83 | 84 | var b bytes.Buffer 85 | n.Session.Stdout = &b 86 | if err := n.Session.Run("/usr/bin/scp -tr ./"); err != nil { 87 | log.Println("session.Run() error:", err) 88 | return err 89 | } 90 | 91 | return 92 | } 93 | 94 | // return []string{"-f 1", "-s 1", ...} 95 | // skip -f option 96 | func rebuildArgs() (ret []string) { 97 | args := []string{"s", "c", "n", "d"} 98 | for _, v := range args { 99 | if f := flag.Lookup(v); f != nil { 100 | ret = append(ret, fmt.Sprintf("-%s", v)) 101 | ret = append(ret, f.Value.String()) 102 | } 103 | } 104 | 105 | return ret 106 | } 107 | 108 | func (n *Node) LocalAttack(configFile string, c chan string) (err error) { 109 | args := rebuildArgs() 110 | args = append(args, "-f") 111 | args = append(args, fmt.Sprintf("%d", n.Proc)) 112 | args = append(args, configFile) 113 | cmd := exec.Command(fmt.Sprintf("./%s", HAKAI_BIN_NAME), args...) 114 | cmd.Env = []string{fmt.Sprintf("GOHAKAI=%s", MODE_NODE_LOCAL)} 115 | 116 | out, err := cmd.CombinedOutput() 117 | if err != nil { 118 | return err 119 | } 120 | 121 | c <- string(out) 122 | 123 | return err 124 | } 125 | 126 | func (n *Node) RemoteAttack(c chan string) (err error) { 127 | rawCmd := strings.Join(rebuildArgs(), " ") 128 | command := fmt.Sprintf("GOHAKAI=%s ./%s %s -f %d %s", 129 | MODE_NODE, HAKAI_BIN_NAME, rawCmd, n.Proc, REMOTE_CONF) 130 | 131 | n.Session, _ = n.NewSSHSession() 132 | defer n.Session.Close() 133 | 134 | var b bytes.Buffer 135 | n.Session.Stdout = &b 136 | if err := n.Session.Run(command); err != nil { 137 | log.Println("attack error:", err) 138 | return err 139 | } 140 | 141 | if len(b.String()) == 0 { 142 | return errors.New("non result error!!") 143 | } 144 | 145 | c <- b.String() 146 | 147 | return err 148 | } 149 | -------------------------------------------------------------------------------- /statistics.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/gob" 6 | "fmt" 7 | "log" 8 | "os" 9 | "sort" 10 | "sync" 11 | "time" 12 | ) 13 | 14 | type Statistics struct { 15 | MaxRequest int 16 | StartTime time.Time 17 | Delta time.Duration 18 | Config *Config 19 | } 20 | 21 | type NodeStats struct { 22 | Success uint32 23 | Fail uint32 24 | Concurrency int 25 | Time time.Duration 26 | PathCount map[string]uint32 27 | PathTime map[string]time.Duration 28 | } 29 | 30 | type AvarageTimeByPath struct { 31 | Path string 32 | Time float64 33 | } 34 | type AvarageTimeStats []AvarageTimeByPath 35 | 36 | func (s AvarageTimeStats) Len() int { 37 | return len(s) 38 | } 39 | func (s AvarageTimeStats) Swap(i, j int) { 40 | s[i], s[j] = s[j], s[i] 41 | } 42 | func (s AvarageTimeStats) Less(i, j int) bool { 43 | return s[i].Time < s[j].Time 44 | } 45 | 46 | func (s *Statistics) PrintAfterDuration(duration int) { 47 | time.Sleep(time.Duration(duration) * time.Second) 48 | s.Print() 49 | os.Exit(0) 50 | } 51 | 52 | func (s *Statistics) printGob() { 53 | delta := s.Delta 54 | 55 | var buf bytes.Buffer 56 | var n NodeStats = NodeStats{ 57 | Success: SUCCESS, 58 | Fail: FAIL, 59 | Concurrency: s.MaxRequest, 60 | Time: delta, 61 | PathCount: PathCount, 62 | PathTime: PathTime, 63 | } 64 | enc := gob.NewEncoder(&buf) 65 | err := enc.Encode(n) 66 | if err != nil { 67 | log.Fatal("encode:", err) 68 | } 69 | 70 | fmt.Print(buf.String()) 71 | } 72 | 73 | func (s *Statistics) printHumanReadable() { 74 | delta := s.Delta 75 | nreq := SUCCESS + FAIL 76 | rps := float64(nreq) / float64(delta.Seconds()) 77 | 78 | fmt.Printf("\nrequest count:%d, concurrency:%d, time:%.5f[s], %f[req/s]\n", 79 | nreq, s.MaxRequest, delta.Seconds(), rps) 80 | fmt.Printf("SUCCESS %d\n", SUCCESS) 81 | fmt.Printf("FAILED %d\n", FAIL) 82 | 83 | var avgTimeByPath map[string]float64 = map[string]float64{} 84 | var totalCount uint32 85 | var totalTime time.Duration 86 | for path, cnt := range PathCount { 87 | totalTime += PathTime[path] 88 | totalCount += cnt 89 | avgTimeByPath[path] += PathTime[path].Seconds() / float64(cnt) 90 | } 91 | fmt.Printf("Average response time[ms]: %v\n", 92 | 1000.*totalTime.Seconds()/float64(totalCount)) 93 | 94 | if s.Config.ShowReport { 95 | var stats AvarageTimeStats = []AvarageTimeByPath{} 96 | 97 | fmt.Printf("Average response time for each path (order by longest) [ms]:\n") 98 | for path, time := range avgTimeByPath { 99 | stats = append(stats, AvarageTimeByPath{Path: path, Time: time}) 100 | } 101 | sort.Sort(sort.Reverse(stats)) 102 | for i := 0; i < len(stats); i++ { 103 | fmt.Printf("%.3f : %s\n", stats[i].Time*1000., stats[i].Path) 104 | } 105 | } 106 | } 107 | 108 | func (s *Statistics) Print() { 109 | if MODE_NORMAL != ExecMode { 110 | s.printGob() 111 | } else { 112 | s.printHumanReadable() 113 | } 114 | } 115 | 116 | func parseResultGob(s string) (result NodeStats, err error) { 117 | b := bytes.NewBufferString(s) 118 | 119 | dec := gob.NewDecoder(b) 120 | err = dec.Decode(&result) 121 | if err != nil { 122 | log.Fatal("decode:", err) 123 | } 124 | 125 | return 126 | } 127 | 128 | func (s *Statistics) Collector(c chan string, wg *sync.WaitGroup) { 129 | s.Delta = time.Duration(0) 130 | for { 131 | ret := <-c 132 | n, _ := parseResultGob(ret) 133 | SUCCESS += n.Success 134 | FAIL += n.Fail 135 | s.MaxRequest += n.Concurrency 136 | s.Delta += n.Time 137 | for path, cnt := range n.PathCount { 138 | PathTime[path] += n.PathTime[path] 139 | PathCount[path] += cnt 140 | } 141 | wg.Done() 142 | } 143 | } 144 | --------------------------------------------------------------------------------