1use std::collections::HashSet;
2
3use futures::stream::{self, StreamExt};
4use rand::seq::SliceRandom;
5use tracing::{debug, warn};
6
7use crate::{error::CapturedError, http_client::HttpClient};
8
9static COMMON_PATHS: &[&str] = &[
12 "/api",
13 "/api/v1",
14 "/api/v2",
15 "/api/v3",
16 "/graphql",
17 "/graphiql",
18 "/playground",
19 "/swagger",
20 "/swagger.json",
21 "/swagger.yaml",
22 "/swagger-ui",
23 "/swagger-ui.html",
24 "/openapi",
25 "/openapi.json",
26 "/openapi.yaml",
27 "/api-docs",
28 "/api-docs.json",
29 "/docs",
30 "/redoc",
31 "/admin",
32 "/admin/api",
33 "/internal",
34 "/internal/api",
35 "/private",
36 "/debug",
37 "/actuator",
38 "/actuator/health",
39 "/actuator/env",
40 "/actuator/mappings",
41 "/actuator/beans",
42 "/actuator/metrics",
43 "/metrics",
44 "/health",
45 "/healthz",
46 "/readyz",
47 "/livez",
48 "/status",
49 "/.well-known/openid-configuration",
50 "/.well-known/oauth-authorization-server",
51 "/oauth/token",
52 "/oauth/authorize",
53 "/auth/token",
54 "/auth/login",
55 "/auth/refresh",
56 "/login",
57 "/logout",
58 "/register",
59 "/users",
60 "/user",
61 "/account",
62 "/accounts",
63 "/profile",
64 "/me",
65 "/config",
66 "/configuration",
67 "/settings",
68 "/env",
69 "/environment",
70 "/version",
71 "/info",
72 "/ping",
73 "/trace",
74 "/log",
75 "/logs",
76 "/debug/vars",
77 "/server-status",
78 "/server-info",
79 "/phpinfo.php",
80 "/.env",
81 "/.git/config",
82 "/wp-json/wp/v2",
83 "/wp-json",
84 "/jsonapi",
85 "/rest/v1",
86 "/rest/v2",
87 "/api/swagger.json",
88 "/api/openapi.json",
89 "/api/graphql",
90];
91
92pub struct CommonPathDiscovery<'a> {
93 client: &'a HttpClient,
94 base_url: &'a str,
95 concurrency: usize,
96 extra: Vec<String>,
98}
99
100impl<'a> CommonPathDiscovery<'a> {
101 pub fn new(
102 client: &'a HttpClient,
103 base_url: &'a str,
104 concurrency: usize,
105 extra: Vec<String>,
106 ) -> Self {
107 Self {
108 client,
109 base_url,
110 concurrency,
111 extra,
112 }
113 }
114
115 pub async fn run(&self) -> (HashSet<String>, Vec<CapturedError>) {
117 let base = self.base_url.trim_end_matches('/');
118
119 let mut all_paths: Vec<String> = COMMON_PATHS.iter().map(|p| (*p).to_string()).collect();
122 all_paths.extend(self.extra.iter().cloned());
123 all_paths.sort_unstable();
124 all_paths.dedup();
125 if all_paths.len() > 1 {
126 let mut rng = rand::thread_rng();
127 all_paths.shuffle(&mut rng);
128 }
129
130 let results = stream::iter(all_paths)
131 .map(|path| {
132 let url = format!("{base}{path}");
133 async move {
134 let result = self.client.head(&url).await;
135 (path, url, result)
136 }
137 })
138 .buffer_unordered(self.concurrency)
139 .collect::<Vec<_>>()
140 .await;
141
142 let mut found = HashSet::new();
143 let mut errors = Vec::new();
144
145 for (path, url, result) in results {
146 match result {
147 Ok(resp) => {
148 if resp.status != 404 && resp.status != 410 {
150 debug!("[common_paths] {} => {}", url, resp.status);
151 found.insert(path);
152 }
153 }
154 Err(e) => {
155 warn!("[common_paths] probe error: {}", e);
156 errors.push(e);
157 }
158 }
159 }
160
161 debug!("[common_paths] {} live paths found", found.len());
162 (found, errors)
163 }
164}