diff --git a/README.md b/README.md index 0a973df..7731627 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,16 @@ bomber ====== -Mass sender of email samples +Simple program to send huge amounts of predefined email samples to an SMTP server. +Just an excuse to try out Go, really... + +``` +Usage of bomber: + -c="all": Category of messages to send + -l=false: Only list available categories. + -n=100: Number of message to be sent + -s="localhost": SMTP server to send the message to + -samples="./samples/": Directory containing email message samples in JSON + -throttle=0: Throttle the message flow (msg/second). + -v=false: Prints details of execution. +``` diff --git a/bomber.go b/bomber.go new file mode 100644 index 0000000..df04472 --- /dev/null +++ b/bomber.go @@ -0,0 +1,184 @@ +package main + +import ( + "github.com/fmgoncalves/bomber/encoding/mail/sample" + "fmt" + "io" + "io/ioutil" + "net/smtp" + "flag" + "time" + "math/rand" + "strings" + "runtime" + "os" +) + +const ( + default_host = "localhost" + default_dir = "samples" + default_port = 25 +) + +func die(err error) { + if err != nil { + panic(fmt.Sprintf("%v", err)) + } + return +} + +func send_default_header(key string, value string, sample_keys map[string] struct{}, wc io.WriteCloser){ + if _, contains := sample_keys[strings.ToLower(key)] ; !contains { + wc.Write([]byte(key + ":" + value + "\r\n")) + } +} + +func hasCategory(s sample.MailSample, category string) bool { + c := strings.ToLower(category) + for _, s_category := range s.Type { + if c == strings.ToLower(s_category) { + return true + } + } + return false +} + +func send_sample(host string, s sample.MailSample) error { + var err error + // Connect to the remote SMTP server. + var port int + if s.Port > 0 { + port = s.Port + } else { + port = default_port + } + c, err := smtp.Dial(fmt.Sprintf("%s:%d",host, port)) + if err != nil { return err } + //die(err) + defer c.Quit() + // MAIL_FROM, TO + c.Mail(s.From) + c.Rcpt(s.To) + // DATA + wc, err := c.Data() + if err != nil { return err } + //die(err) + defer wc.Close() + // Default headers + keyz := s.DefinedHeaders() + send_default_header("From", s.From, keyz, wc) + send_default_header("To", s.To, keyz, wc) + send_default_header("Date", time.Now().UTC().Format(time.RFC822), keyz, wc) + send_default_header("Message-Id", fmt.Sprintf("<%v@crap>",rand.Int()), keyz, wc) + + for _, h := range s.Headers { + wc.Write([]byte(h + "\r\n")) + } + wc.Write([]byte("\r\n")) + wc.Write([]byte(s.Body)) + return err +} + +func main() { + var ( + SAMPLES_DIRECTORY string + HOST string + CATEGORY string + N_MESSAGES int + THROTTLING float64 + LIST_ONLY bool + VERBOSE bool + ) + + flag.StringVar(&HOST, "s", default_host, "SMTP server to send the message to") + //flag.StringVar(&SAMPLES_DIRECTORY, "samples", default_dir, "Directory containing email message samples in JSON") + samples_dir := os.Getenv("BOMBER_SAMPLES") + if len(samples_dir) == 0 { + samples_dir = "samples" + } + flag.StringVar(&SAMPLES_DIRECTORY, "samples", samples_dir, "Directory containing email message samples in JSON") + flag.StringVar(&CATEGORY, "c", "all", "Category of messages to send") + flag.IntVar(&N_MESSAGES, "n", 100, "Number of message to be sent") + flag.Float64Var(&THROTTLING, "throttle", 0, "Throttle the message flow (msg/second).") + flag.BoolVar(&LIST_ONLY, "l", false, "Only list available categories.") + flag.BoolVar(&VERBOSE, "v", false, "Prints details of execution.") + flag.Parse() + + l, err := ioutil.ReadDir(SAMPLES_DIRECTORY) + die(err) + sample_list := make([]sample.MailSample,len(l)) + categories := make(map[string]bool) + idx := 0 + for _, val := range l { + if ! val.IsDir() && strings.HasSuffix(val.Name(),".json") { + s, err := sample.Unmarshal(SAMPLES_DIRECTORY+"/"+val.Name()) + if err != nil { + if VERBOSE { + fmt.Printf("Failed to load %s sample\n",val.Name()) + } + } + if VERBOSE { + fmt.Printf("Loaded %s sample\n",val.Name()) + } + if LIST_ONLY { + for _, s_category := range s.Type { + asdfg := strings.ToLower(s_category) + categories[asdfg] = true + } + } + if CATEGORY == "all" || hasCategory(s,CATEGORY) { + sample_list[idx] = s + idx++ + } + } + } + if LIST_ONLY { + fmt.Printf("Categories found in samples:\n") + for cat, _ := range categories { + fmt.Printf("\t%v\n",cat) + } + return + } + + if ( idx > 0 ){ + sample_list = sample_list[0:idx] + } else { + panic("No samples to use.") + } + + pn := runtime.NumCPU() + //runtime.GOMAXPROCS(pn) + if THROTTLING > 0 { + pn = int(THROTTLING) + } + if VERBOSE { + fmt.Printf("Sending %v messages in batches of %v\n", N_MESSAGES, pn); + } + + c := make(chan int, pn) + for i:= 0; i < pn; i++ { + c <- 1 + } + for i:= 0; i < N_MESSAGES; i++ { + <-c + go func (j int) { + if VERBOSE { + fmt.Printf("Sending message #%v\n",j+1) + } + err := send_sample(HOST, sample_list[rand.Int() % len(sample_list)]) + if err != nil { + fmt.Printf("Failed to send message #%v: %v\n", j+1, err) + } + if THROTTLING > 0 { + // TODO throttling should consider the running time of send_sample + time.Sleep(time.Duration( (float64(pn) /THROTTLING) * 1000.0 * float64(time.Millisecond))) + } + c <- 1 + }(i) + } + for i:= 0; i < pn; i++ { + <- c + } + + return +} diff --git a/encoding/mail/sample/sample.go b/encoding/mail/sample/sample.go new file mode 100644 index 0000000..13c8e3c --- /dev/null +++ b/encoding/mail/sample/sample.go @@ -0,0 +1,85 @@ +package sample + +import ( + "encoding/json" + "io/ioutil" + "path" + "strings" +) + +type MailSample struct { + Type []string + Port int + To string + From string + Headers []string + HeadersFile string + Body string + BodyFile string +} + +func (s MailSample) DefinedHeaders() map[string]struct{} { + keys_map := make(map[string] struct{}) + for _, val := range s.Headers { + key := strings.SplitN(val,":",2)[0] + keys_map[strings.ToLower(key)] = struct{}{}; + } + return keys_map; +} + + + +func (s *MailSample) buildHeaders(file_path string) error { + var err error + if len(s.Headers) == 0 && len(s.HeadersFile) != 0 { + f, err := ioutil.ReadFile(path.Dir(file_path)+"/"+s.HeadersFile) + if err != nil { + return err + } + headers_aux := strings.Split(string(f), "\n") + headers := make([]string, len(headers_aux)) + idx := -1 + for _, val := range headers_aux { + if ! strings.HasPrefix(val," ") && ! strings.HasPrefix(val,"\t") { + idx++ + } else { + headers[idx] += "\r\n" + } + headers[idx] += strings.TrimRight(val,"\r") + } + headers = headers[0:idx+1] + s.Headers = headers + } + return err +} + +func (s *MailSample) buildBody(file_path string) error { + var err error + if len(s.Body) == 0 && len(s.BodyFile) != 0 { + f, err := ioutil.ReadFile(path.Dir(file_path)+"/"+s.BodyFile) + if err != nil { + return err + } + s.Body = string(f) + } + return err +} + +func Unmarshal(file_path string) (MailSample, error){ + var s MailSample + f, err := ioutil.ReadFile(file_path) + if err != nil { + return s, err + } + err = json.Unmarshal(f, &s) + if err != nil { + return s, err + } + if err := s.buildHeaders(file_path); err != nil { + return s, err + } + if err := s.buildBody(file_path) ; err != nil { + return s, err + } + return s, nil +} diff --git a/example_samples/eicar.body b/example_samples/eicar.body new file mode 100644 index 0000000..77c9198 --- /dev/null +++ b/example_samples/eicar.body @@ -0,0 +1 @@ +X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H* diff --git a/example_samples/eicar.json b/example_samples/eicar.json new file mode 100644 index 0000000..3656c7f --- /dev/null +++ b/example_samples/eicar.json @@ -0,0 +1,7 @@ +{ + "From":"virus.sender@fakedomain.tld", + "To":"eicar@fakedomain.tld", + "Type": ["virus"], + "Headers": ["Subject: Eicar virus message"], + "BodyFile":"eicar.body" +} diff --git a/example_samples/gtube.json b/example_samples/gtube.json new file mode 100644 index 0000000..7a4876c --- /dev/null +++ b/example_samples/gtube.json @@ -0,0 +1,7 @@ +{ + "From":"gtube.spamassassin@fakedomain.tld", + "To":"gTuBe@fakedomain.tld", + "Type": ["spam","gtube"], + "Headers": ["Subject: Generic Test for Unsolicited Bulk Email"], + "Body":"XJS*C4JDBQADN1.NSBN3*2IDNEN*GTUBE-STANDARD-ANTI-UBE-TEST-EMAIL*C.34X" +}