Skip to content

Commit df878eb

Browse files
PolovinaDmhmdiaa
andauthored
Use RAM-friendly concurrent algorithm (#6)
* Use RAM-friendly concurrent algorithm; refactor * Add -t to readme Co-authored-by: Mohammed Diaa <[email protected]>
1 parent aa9e0ea commit df878eb

File tree

3 files changed

+267
-94
lines changed

3 files changed

+267
-94
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ go install github.com/trickest/mkpath@latest
4343
Generate files only, file names are appended to given domains (default false)
4444
-r string
4545
Regex to filter words from wordlist file
46+
-t int
47+
Number of threads for every path depth (default 100)
4648
-w string
4749
Wordlist file
4850
```

mkpath.go

Lines changed: 231 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -2,126 +2,114 @@ package main
22

33
import (
44
"bufio"
5+
"bytes"
56
"flag"
67
"fmt"
78
"os"
9+
"os/signal"
810
"regexp"
11+
"strconv"
912
"strings"
13+
"sync"
14+
"syscall"
15+
16+
roundChan "github.com/trickest/mkpath/round"
1017
)
1118

1219
const (
1320
fileRegex = "[^?*:;{}]+\\.[^/?*:;{}]+"
21+
22+
bufferSizeMB = 100
23+
maxWorkingThreads = 100000
24+
numberOfFiles = 1
1425
)
1526

16-
func generatePaths(wordSet map[string]bool, depth int) []string {
17-
results := make([]string, 0)
18-
for i := 0; i < depth; i += 1 {
19-
toMerge := results[0:]
20-
if len(toMerge) == 0 {
21-
for word := range wordSet {
22-
results = append(results, word)
23-
}
24-
} else {
25-
for _, sd := range toMerge {
26-
for word := range wordSet {
27-
results = append(results, fmt.Sprintf("%s/%s", word, sd))
28-
}
29-
}
30-
}
31-
}
32-
return results
33-
}
27+
var (
28+
domain string
29+
inputDomains []string
30+
domainFile string
3431

35-
func generateFiles(paths []string, files map[string]bool) []string {
36-
results := make([]string, 0)
37-
for _, path := range paths {
38-
for file := range files {
39-
results = append(results, path+"/"+file)
40-
}
41-
}
42-
return results
43-
}
32+
wordlist string
33+
toLowercase bool
34+
regex string
35+
dirWordSet map[string]bool
36+
fileWordSet map[string]bool
4437

45-
func generateAll(paths map[string]bool, files map[string]bool, depth int) []string {
46-
var results []string
47-
results = generatePaths(paths, depth)
48-
results = append(results, generateFiles(generatePaths(paths, depth), files)...)
49-
return results
50-
}
38+
depth int
39+
onlyDirs bool
40+
onlyFiles bool
41+
outputFileName string
42+
silent bool
5143

52-
func main() {
53-
domain := flag.String("d", "", "Input domain")
54-
domainFile := flag.String("df", "", "Input domain file, one domain per line")
55-
wordlist := flag.String("w", "", "Wordlist file")
56-
toLowercase := flag.Bool("lower", false, "Convert wordlist file content to lowercase (default false)")
57-
r := flag.String("r", "", "Regex to filter words from wordlist file")
58-
depth := flag.Int("l", 1, "URL path depth to generate (default 1)")
59-
output := flag.String("o", "", "Output file (optional)")
60-
onlyDirs := flag.Bool("only-dirs", false, "Generate directories only, files are filtered out (default false)")
61-
onlyFiles := flag.Bool("only-files", false, "Generate files only, file names are appended to given domains (default false)")
62-
flag.Parse()
44+
workers int
45+
workerThreadMax = make(chan struct{}, maxWorkingThreads)
46+
done = make(chan struct{})
47+
wg sync.WaitGroup
48+
wgWrite sync.WaitGroup
49+
robin roundChan.RoundRobin
50+
)
6351

64-
inputDomains := make([]string, 0)
65-
if *domain != "" {
66-
inputDomains = append(inputDomains, *domain)
52+
func readDomainFile() {
53+
inputFile, err := os.Open(domainFile)
54+
if err != nil {
55+
fmt.Println("Could not open file to read domains:", err)
56+
os.Exit(1)
6757
}
68-
if *domainFile != "" {
69-
inputFile, err := os.Open(*domainFile)
70-
if err != nil {
71-
fmt.Println(err.Error())
72-
os.Exit(1)
73-
}
74-
defer inputFile.Close()
75-
scanner := bufio.NewScanner(inputFile)
76-
for scanner.Scan() {
77-
inputDomains = append(inputDomains, scanner.Text())
78-
}
58+
defer inputFile.Close()
59+
60+
scanner := bufio.NewScanner(inputFile)
61+
for scanner.Scan() {
62+
inputDomains = append(inputDomains, strings.TrimSpace(scanner.Text()))
7963
}
80-
if len(inputDomains) == 0 {
81-
fmt.Println("No input provided")
64+
}
65+
66+
func prepareDomains() {
67+
if domain == "" && domainFile == "" {
68+
fmt.Println("No domain input provided!")
8269
os.Exit(1)
8370
}
8471

85-
wordlistFile, err := os.Open(*wordlist)
86-
if err != nil {
87-
fmt.Println(err.Error())
88-
os.Exit(1)
72+
inputDomains = make([]string, 0)
73+
if domain != "" {
74+
inputDomains = append(inputDomains, domain)
75+
} else {
76+
if domainFile != "" {
77+
readDomainFile()
78+
}
8979
}
90-
defer wordlistFile.Close()
80+
}
9181

82+
func readWordlistFile() {
9283
var reg *regexp.Regexp
93-
if *r != "" {
94-
reg, err = regexp.Compile(*r)
84+
var err error
85+
if regex != "" {
86+
reg, err = regexp.Compile(regex)
9587
if err != nil {
96-
fmt.Println(err.Error())
88+
fmt.Println(err)
9789
os.Exit(1)
9890
}
9991
}
10092

101-
var fileReg *regexp.Regexp
102-
fileReg, err = regexp.Compile(fileRegex)
93+
wordlistFile, err := os.Open(wordlist)
10394
if err != nil {
104-
fmt.Println(err.Error())
95+
fmt.Println("Could not open file to read wordlist:", err)
10596
os.Exit(1)
10697
}
98+
defer wordlistFile.Close()
10799

108-
var outputFile *os.File
109-
if *output != "" {
110-
outputFile, err = os.Create(*output)
111-
if err != nil {
112-
fmt.Println(err.Error())
113-
os.Exit(1)
114-
}
115-
defer outputFile.Close()
100+
fileReg, err := regexp.Compile(fileRegex)
101+
if err != nil {
102+
fmt.Println(err)
103+
os.Exit(1)
116104
}
117105

118-
dirWordSet := make(map[string]bool)
119-
fileWordSet := make(map[string]bool)
106+
dirWordSet = make(map[string]bool)
107+
fileWordSet = make(map[string]bool)
120108
scanner := bufio.NewScanner(wordlistFile)
121109

122110
for scanner.Scan() {
123111
word := scanner.Text()
124-
if *toLowercase {
112+
if toLowercase {
125113
word = strings.ToLower(word)
126114
}
127115
word = strings.Trim(word, "/")
@@ -138,24 +126,173 @@ func main() {
138126
}
139127
}
140128
}
129+
}
130+
131+
func closeWriters(number int) {
132+
for i := 0; i < number; i++ {
133+
done <- struct{}{}
134+
}
135+
}
136+
137+
func spawnWriters(number int) {
138+
for i := 0; i < number; i++ {
139+
var bf bytes.Buffer
140+
ch := make(chan string, 100000)
141+
142+
fileName := outputFileName
143+
fileSplit := strings.Split(fileName, ".")
144+
if len(fileSplit) == 1 {
145+
fileName += ".txt"
146+
}
147+
if number > 1 {
148+
fileSplit = strings.Split(fileName, ".")
149+
extension := "." + fileSplit[len(fileSplit)-1]
150+
fileName = strings.TrimSuffix(fileName, extension) + "-" + strconv.Itoa(i) + extension
151+
}
152+
file, err := os.Create(fileName)
153+
if err != nil {
154+
fmt.Println("Couldn't open file to write output:", err)
155+
os.Exit(1)
156+
}
157+
158+
wgWrite.Add(1)
159+
go write(file, &bf, &ch)
160+
161+
if robin == nil {
162+
robin = roundChan.New(&ch)
163+
continue
164+
}
165+
robin.Add(&ch)
166+
}
167+
}
168+
169+
func write(file *os.File, buffer *bytes.Buffer, ch *chan string) {
170+
mainLoop:
171+
for {
172+
select {
173+
case <-done:
174+
for {
175+
if !writeOut(file, buffer, ch) {
176+
break
177+
}
178+
}
179+
if buffer.Len() > 0 {
180+
if file != nil {
181+
_, _ = file.WriteString(buffer.String())
182+
buffer.Reset()
183+
}
184+
}
185+
break mainLoop
186+
default:
187+
writeOut(file, buffer, ch)
188+
}
189+
}
190+
wgWrite.Done()
191+
}
192+
193+
func writeOut(file *os.File, buffer *bytes.Buffer, outputChannel *chan string) bool {
194+
select {
195+
case s := <-*outputChannel:
196+
buffer.WriteString(s)
197+
if buffer.Len() >= bufferSizeMB*1024*1024 {
198+
_, _ = file.WriteString(buffer.String())
199+
buffer.Reset()
200+
}
201+
return true
202+
default:
203+
return false
204+
}
205+
}
206+
207+
func combo(_comb string, level int, wg *sync.WaitGroup, wt *chan struct{}) {
208+
defer wg.Done()
209+
workerThreadMax <- struct{}{}
210+
211+
if strings.Count(_comb, "/") > 0 {
212+
processOutput(_comb, robin.Next())
213+
}
141214

142-
var results []string
143-
if *onlyDirs != *onlyFiles {
144-
if *onlyDirs {
145-
results = generatePaths(dirWordSet, *depth)
146-
} else {
147-
results = generateFiles(results, fileWordSet)
215+
var nextLevelWaitGroup sync.WaitGroup
216+
if level > 1 {
217+
nextLevelWt := make(chan struct{}, workers)
218+
for dw := range dirWordSet {
219+
nextLevelWaitGroup.Add(1)
220+
nextLevelWt <- struct{}{}
221+
go combo(_comb+"/"+dw, level-1, &nextLevelWaitGroup, &nextLevelWt)
148222
}
149223
} else {
150-
results = generateAll(dirWordSet, fileWordSet, *depth)
224+
for dw := range dirWordSet {
225+
processOutput(_comb+"/"+dw, robin.Next())
226+
}
151227
}
152228

153-
for _, domain := range inputDomains {
154-
for _, subpath := range results {
155-
fmt.Println(domain + "/" + subpath)
156-
if outputFile != nil {
157-
_, _ = outputFile.WriteString(domain + "/" + subpath + "\n")
229+
nextLevelWaitGroup.Wait()
230+
<-workerThreadMax
231+
<-*wt
232+
}
233+
234+
func processOutput(out string, outChan *chan string) {
235+
if onlyDirs || onlyFiles == onlyDirs {
236+
if !silent {
237+
fmt.Print(out + "\n")
238+
}
239+
*outChan <- out + "\n"
240+
}
241+
242+
if onlyFiles || onlyFiles == onlyDirs {
243+
for file := range fileWordSet {
244+
if !silent {
245+
fmt.Print(out + "/" + file + "\n")
158246
}
247+
*outChan <- out + "/" + file + "\n"
159248
}
160249
}
161250
}
251+
252+
func main() {
253+
flag.StringVar(&domain, "d", "", "Input domain")
254+
flag.StringVar(&domainFile, "df", "", "Input domain file, one domain per line")
255+
flag.StringVar(&wordlist, "w", "", "Wordlist file")
256+
flag.BoolVar(&toLowercase, "lower", false, "Convert wordlist file content to lowercase (default false)")
257+
flag.StringVar(&regex, "r", "", "Regex to filter words from wordlist file")
258+
flag.IntVar(&depth, "l", 1, "URL path depth to generate")
259+
flag.StringVar(&outputFileName, "o", "", "Output file (optional)")
260+
flag.BoolVar(&onlyDirs, "only-dirs", false, "Generate directories only, files are filtered out (default false)")
261+
flag.BoolVar(&onlyFiles, "only-files", false, "Generate files only, file names are appended to given domains (default false)")
262+
flag.IntVar(&workers, "t", 100, "Number of threads for every path depth")
263+
flag.BoolVar(&silent, "silent", true, "Skip writing generated paths to stdout (faster)")
264+
flag.Parse()
265+
266+
go func() {
267+
signalChannel := make(chan os.Signal, 1)
268+
signal.Notify(signalChannel, os.Interrupt, syscall.SIGTERM, syscall.SIGKILL)
269+
<-signalChannel
270+
271+
fmt.Println("Program interrupted, exiting...")
272+
os.Exit(0)
273+
}()
274+
275+
if depth <= 0 || workers <= 0 {
276+
fmt.Println("Path depth and number of threads must be positive integers!")
277+
os.Exit(0)
278+
}
279+
280+
prepareDomains()
281+
readWordlistFile()
282+
spawnWriters(numberOfFiles)
283+
284+
if outputFileName == "" {
285+
silent = false
286+
}
287+
288+
for _, d := range inputDomains {
289+
wg.Add(1)
290+
wt := make(chan struct{}, 1)
291+
wt <- struct{}{}
292+
go combo(d, depth, &wg, &wt)
293+
}
294+
295+
wg.Wait()
296+
closeWriters(numberOfFiles)
297+
wgWrite.Wait()
298+
}

0 commit comments

Comments
 (0)