Принятая в Go реализация конкурентности через обмен сообщениями между независимыми друг от друга потоками называется CSP (Communicating Sequential Processes). В ее основе — каналы, передающие сообщения от одного потока (горутины) к другому.
Если ты знаком с концепцией системных каналов (pipes), то уже примерно понимаешь, что к чему. Однако, рантайм Go не использует средства операционной системы, у него имеется собственная реализация горутин и каналов.
В саму методику фаззинга мы здесь погружаться не будем: все‑таки основная цель — освоить Go, а потому концентрируемся сейчас на инструментах, предоставляемых языком программирования. Так что реализация будет максимально простая: добавляем к имени хоста слово из словаря, затем выполняем GET запрос по получившемуся адресу и сохраняем код ответа.
Словарь можно взять, например, здесь, но я советую не брать очень большой список, а написать свой буквально из десятка позиций — этого более чем достаточно для начала.
info
Часто модель CSP описывают одним утверждением: «не реализовывай коммуникацию посредством совместного использования памяти; вместо этого совместно используй память посредством коммуникации».
www
Модель CSP разработана и описана Чарльзом Энтони Хоаром — возможно, ты уже слышал его имя в связи с алгоритмом быстрой сортировки (qsort).
Создадим новый модуль:
mkdir dirfuzzer && cd $_go mod init dirfuzzer
touch main.go
Разделяем код на модули
В файл main.go вставим следующий код:
package mainimport ( "bufio" "fmt" "io" "net/http" "os" "strings" "sync" "time")type Result struct { Name string Code int}// produce генерирует задания для обработки,// комбинируя host со значениями, считанными из filename,// и помещает их в канал outCh.func produce(filename string, host string, outCh chan<- string) { file, err := os.Open(filename) if err != nil { fmt.Fprintf(os.Stderr, "opening %s: %v\n", filename, err) return } defer file.Close() scanner := bufio.NewScanner(file) for scanner.Scan() { s := strings.TrimSpace(scanner.Text()) if s == "" { continue } outCh <- "https://" + host + "/" + s } if err := scanner.Err(); err != nil { fmt.Fprintf(os.Stderr, "reading %s: %v\n", filename, err) }}// worker получает значения из канала inCh, пока он остается открытым, выполняет обработку и помещает результаты в outCh.func worker(c *http.Client, inCh <-chan string, outCh chan<- Result) { for job := range inCh { resp, err := c.Get(job) if err != nil { continue } resp.Body.Close() io.Copy(io.Discard, resp.Body) result := Result{ Name: job, Code: resp.StatusCode, } outCh <- result }}// collect получает значения из канала resultCh, пока он остается открытым, и записывает их в файл filename.func collect(filename string, resultCh <-chan Result) { dstFile, err := os.Create(filename) if err != nil { fmt.Fprintf(os.Stderr, "creating %s: %v\n", filename, err) return } defer dstFile.Close() writer := bufio.NewWriter(dstFile) for r := range resultCh { s := fmt.Sprintf("%s - %d %s\n", r.Name, r.Code, http.StatusText(r.Code)) _, err = writer.WriteString(s) if err != nil { fmt.Fprintf(os.Stderr, "writing to %s: %v\n", filename, err) } } if err = writer.Flush(); err != nil { fmt.Fprintf(os.Stderr, "writing to %s: %v\n", filename, err) }}func main() { // TODO}Как видишь, мы логически распределили фрагменты кода между отдельными изолированными функциями в соответствии с ролями, которые на этот код возложены.
Функция produce( генерирует задания для последующей обработки. Она вычитывает строки из списка и формирует URL, по которым будут осуществляться попытки соединения. Сама обработка лежит вне зоны ответственности этой функции и никак не влияет на ее код. Другие часто используемые имена для такого кода: generate, source.
Функция worker( обрабатывает задания. В данном случае, выполняется GET запрос к серверу и сохранение кода ответа. Как ты видишь, функция «не знает» ничего ни о происхождении заданий, ни о дальнейшей судьбе результатов — это к ней не относится. Другие часто встречающиеся имена для такой роли: process, handle.
Функция collect( вычитывает результаты и сохраняет их в файл. По аналогии с предыдущими она ничего не знает и не должна знать о том, откуда и каким образом эти результаты взялись. У нее есть только одна четко очерченная зона ответственности — собрать и сохранить. Другие часто встречающиеся имена: consume, sink, save.
Подобное разделение воплощает принцип построения программных систем loose couping, high cohesion (один из вариантов перевода: «слабая связанность, сильное сцепление»). Согласно этому принципу, система строится из отдельных компонентов, которые минимально зависят друг от друга (это и есть loose coupling). При этом каждый такой компонент выполняет четко очерченную роль, и код, ответственный за ту или иную функцию, сконцентрирован внутри «своего» компонента, а не размазан по другим (а это — high cohesion).
Такой подход облегчает понимание работы системы, позволяет применять модульное тестирование, улучшает повторное использование кода и дает возможность в будущем наращивать возможности.
Если мы захотим, к примеру, сохранять результаты в другом формате или вообще писать их не в файл, а, скажем, отправлять по сети на другой сервер — нам нужно только заменить компонент collect, не трогая остальные и даже не читая их код. Это общий принцип, он применим не только к отдельным функциям или классам, но и к любым компонентам программной системы в широком понимании этого слова.
info
Разделение большого монолитного куска кода, решающего ту или иную задачу, на отдельные компоненты, называется модуляризацией (modularization). Не знаю, как тебе, а мне это трудно даже произнести, поэтому я предпочитаю родственное понятие — декомпозиция. Хотя, конечно, оно больше про подход к решению задачи, нежели про структурирование реализации этого решения.
Каналы
Запускать эти функции будем как отдельные горутины. При этом мы откажемся от «классического» подхода к обмену информацией через совместно используемые объекты, защищенные мьютексами. Вместо этого воспользуемся каналами — специальным инструментом коммуникации между горутинами. Каналы потокобезопасны по определению и не требуют использования мьютексов или иных примитивов синхронизации.
Продолжение доступно только участникам
Материалы из последних выпусков становятся доступны по отдельности только через два месяца после публикации. Чтобы продолжить чтение, необходимо стать участником сообщества «Xakep.ru».
Присоединяйся к сообществу «Xakep.ru»!
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
