Skip to content

Commit c82cd4e

Browse files
committed
v0.4.0: smart domain matching
Boundary-aware domain detection — matches subdomains, URLs, and emails while rejecting false positives. Output always user:pass.
1 parent 0f5b7af commit c82cd4e

5 files changed

Lines changed: 70 additions & 12 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "ulpExtractor"
3-
version = "0.3.1"
3+
version = "0.4.0"
44
edition = "2021"
55

66
[dependencies]

README.md

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
# ulpExtractor
22

3-
**Fast domain credential extractor** — parse large `domain:user:pass` lists and extract matching credentials by domain.
3+
**Fast domain credential extractor** — parse large `url:user:pass` lists and extract matching credentials by domain.
44

55
Built in Rust with a styled CLI, interactive prompt mode, and multi-file batch scanning.
66

77
## Features
88

9+
- **Smart domain matching** — boundary-aware, matches subdomains (`www.netflix.com` matches `netflix.com`), URLs (`https://deepseek.com/path:user:pass`), and emails (`user@domain.com`); rejects false positives like `mydeepseek.com`
910
- **Styled CLI** — boxed header, colored fields, live progress bar with real-time match counter
1011
- **Interactive mode** — guided prompts when run with no arguments, same visual design as CLI
1112
- **Multi-file scan** (`-a`) — scan all files in a directory matching given extensions
@@ -67,14 +68,26 @@ ulpExtractor -d netflix.com -a --dir ./data -o results.txt -t 8
6768

6869
## Input Format
6970

70-
Each line should be `domain<divider>user<divider>password`:
71+
Lines use `<url_or_domain><divider><user><divider><password>`. The domain can appear anywhere in the URL portion — bare, as a subdomain, inside an `https://` URL with paths, or in an email:
7172

7273
```
73-
fiverr.com:estheticdesigns:Ahmadraza
74-
google.com:user@gmail.com:password123
74+
netflix.com:john:secret123
75+
www.netflix.com:user@mail.com:pass456
76+
https://platform.deepseek.com/login:admin:pass789
77+
user@example.com:somepass
7578
```
7679

77-
Output is `user<divider>password` for matching lines only.
80+
Matching is **boundary-aware**`deepseek.com` matches `platform.deepseek.com` but NOT `mydeepseek.com`.
81+
82+
Output is `user<divider>password` for matching lines only. Lines without a user portion (`domain:pass`) are skipped.
83+
84+
## Upgrade Notes (v0.3.x → v0.4.0)
85+
86+
v0.4.0 introduces **smart domain matching**:
87+
- **Subdomain matching**: `netflix.com` now matches `www.netflix.com`, `login.netflix.com`, etc.
88+
- **URL support**: URLs like `https://domain.com/path:user:pass` are parsed correctly
89+
- **Boundary detection**: `deepseek.com` no longer false-matches `mydeepseek.com`
90+
- **Output format**: always `user:pass` — lines without a user portion are skipped
7891

7992
## Build from Source
8093

src/extractor.rs

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
55
use std::sync::Arc;
66
use std::time::Instant;
77

8-
use memchr::memchr;
8+
use memchr::{memchr, memrchr};
99
use memmap2::Mmap;
1010
use rayon::prelude::*;
1111

@@ -28,6 +28,43 @@ pub struct ExtractResult {
2828
}
2929

3030
/// Find all files in `dir` matching any of the given extensions.
31+
32+
/// Find `domain` inside `user_part` at a domain boundary. Returns the end
33+
/// position of the match so the caller can slice the remainder (user:pass).
34+
/// Handles URLs, emails, and bare domains:
35+
/// "platform.deepseek.com" ← subdomain (left boundary: '.')
36+
/// "https://deepseek.com/login" ← URL with path (left: '://', right: '/')
37+
/// "user@deepseek.com" ← email (left boundary: '@')
38+
/// "deepseek.com:user" ← username (right boundary: ':')
39+
/// Rejects partial-domain matches like "mydeepseek.com" (no boundary before).
40+
fn domain_end(user_part: &[u8], domain: &[u8]) -> Option<usize> {
41+
if domain.is_empty() || user_part.len() < domain.len() {
42+
return None;
43+
}
44+
if user_part == domain {
45+
return Some(domain.len());
46+
}
47+
let d0 = domain[0];
48+
let mut start = 0;
49+
while let Some(pos) = memchr(d0, &user_part[start..]) {
50+
let abs = start + pos;
51+
let end = abs + domain.len();
52+
start = abs + 1;
53+
if end > user_part.len() {
54+
continue;
55+
}
56+
if &user_part[abs..end] != domain {
57+
continue;
58+
}
59+
let left_ok = abs == 0 || matches!(user_part[abs - 1], b'.' | b'@' | b'/' | b':');
60+
let right_ok = end >= user_part.len() || matches!(user_part[end], b'/' | b':');
61+
if left_ok && right_ok {
62+
return Some(end);
63+
}
64+
}
65+
None
66+
}
67+
3168
pub fn find_files(dir: &Path, extensions: &[String]) -> std::io::Result<Vec<PathBuf>> {
3269
let exts: Vec<String> = extensions
3370
.iter()
@@ -171,9 +208,17 @@ pub fn extract_multi(
171208
line = &line[..line.len() - 1];
172209
}
173210

174-
if let Some(div_pos) = memchr(div, line) {
175-
if &line[..div_pos] == domain {
176-
local.push(line[div_pos + 1..].to_vec());
211+
if let Some(div_pos) = memrchr(div, line) {
212+
let user_part = &line[..div_pos];
213+
if let Some(end_pos) = domain_end(user_part, domain) {
214+
let after = &user_part[end_pos..];
215+
// must have user portion: url:user:pass (at least 3 parts)
216+
if let Some(last_colon) = memrchr(div, after) {
217+
let mut out = after[last_colon + 1..].to_vec();
218+
out.push(div);
219+
out.extend_from_slice(&line[div_pos + 1..]);
220+
local.push(out);
221+
}
177222
}
178223
}
179224

src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ fn print_header() {
6363
let top = "┌".to_string() + &"─".repeat(w.saturating_sub(2)) + "┐";
6464
let bot = "└".to_string() + &"─".repeat(w.saturating_sub(2)) + "┘";
6565

66-
let title = " ulpExtractor v0.3.1 ";
66+
let title = " ulpExtractor v0.4.0 ";
6767
let subtitle = " Domain credential extractor ";
6868

6969
let pad = (w.saturating_sub(title.len())) / 2;

0 commit comments

Comments
 (0)