diff --git a/src/main/frontend/src/api/index.js b/src/main/frontend/src/api/index.js index 00e2dda..66db25a 100644 --- a/src/main/frontend/src/api/index.js +++ b/src/main/frontend/src/api/index.js @@ -1,13 +1,18 @@ import axios from 'axios' -let authToken = null +let authToken = localStorage.getItem('jyso_token') export function setAuthToken(token) { authToken = token + if (token) { + localStorage.setItem('jyso_token', token) + } else { + localStorage.removeItem('jyso_token') + } } export function getAuthToken() { - return authToken + return authToken || localStorage.getItem('jyso_token') } const api = axios.create({ @@ -27,6 +32,7 @@ api.interceptors.response.use( error => { if (error.response && error.response.status === 401) { authToken = null + localStorage.removeItem('jyso_token') } return Promise.reject(error) } @@ -72,4 +78,16 @@ export function getLogs(count) { return api.get('/logs', { params: { count } }) } +export function getFiles() { + return api.get('/files') +} + +export function downloadFile(name) { + return api.get('/files/download', { params: { name }, responseType: 'blob' }) +} + +export function deleteFile(name) { + return api.post('/files/delete', { name }) +} + export default api diff --git a/src/main/frontend/src/index.css b/src/main/frontend/src/index.css index d66e5b6..b55bf85 100644 --- a/src/main/frontend/src/index.css +++ b/src/main/frontend/src/index.css @@ -825,6 +825,87 @@ select { .login-inner { padding: 36px 28px 32px; } } +.wiki-btn { + display: inline-flex; + align-items: center; + padding: 6px 14px; + border-radius: var(--btn-radius); + background: var(--bg-input); + border: 1px solid var(--border-color); + color: var(--text-secondary); + font-size: 12px; + font-weight: 600; + text-decoration: none; + cursor: pointer; + transition: all var(--transition-fast); + text-transform: uppercase; + letter-spacing: 0.3px; +} +.wiki-btn:hover { + background: var(--accent-glow); + border-color: var(--accent); + color: var(--accent); +} + +.file-list { + max-height: 360px; + overflow-y: auto; +} +.file-row { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 10px; + border-bottom: 1px solid var(--border-color); + font-size: 12px; + transition: background var(--transition-fast); +} +.file-row:last-child { border-bottom: none; } +.file-row:hover { background: var(--bg-primary); } +.file-name { + flex: 1; + font-family: var(--font-mono); + color: var(--text-primary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.file-size { + font-size: 10px; + color: var(--text-muted); + flex-shrink: 0; + min-width: 45px; + text-align: right; +} +.file-btn { + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + border: none; + border-radius: 6px; + background: transparent; + color: var(--text-muted); + cursor: pointer; + transition: all var(--transition-fast); +} +.file-btn:hover { + background: var(--accent-glow); + color: var(--accent); +} +.file-btn-del:hover { + background: var(--danger-bg); + color: var(--danger); +} +.file-empty { + text-align: center; + padding: 28px 0; + color: var(--text-muted); + font-size: 13px; + font-family: 'Inter', sans-serif; +} + @media (prefers-reduced-motion: reduce) { *, *::before, *::after { transition-duration: 0ms !important; diff --git a/src/main/frontend/src/pages/Dashboard.jsx b/src/main/frontend/src/pages/Dashboard.jsx index 2b85852..1158c7d 100644 --- a/src/main/frontend/src/pages/Dashboard.jsx +++ b/src/main/frontend/src/pages/Dashboard.jsx @@ -3,7 +3,8 @@ import { useNavigate } from 'react-router-dom' import { useTheme } from '../context/ThemeContext' import { getStatus, toggleServer, getGadgets, - generatePayload as apiGenerate, updateConfig, setAuthToken, getLogs + generatePayload as apiGenerate, updateConfig, setAuthToken, getLogs, + getFiles, downloadFile, deleteFile as apiDeleteFile } from '../api' export default function Dashboard() { @@ -50,6 +51,8 @@ export default function Dashboard() { const [logLines, setLogLines] = useState([]) const [logLoading, setLogLoading] = useState(false) const logEndRef = useRef(null) + const [files, setFiles] = useState([]) + const [filesLoading, setFilesLoading] = useState(false) const ROUTING_OPTIONS = ['Basic', 'ELProcessor', 'Groovy', 'jdbcBypass1', 'jdbcBypass2', 'ldap2rmi', 'SnakeYaml', 'XStream', 'MemoryXXE'] @@ -140,6 +143,7 @@ export default function Dashboard() { if (res.data.success) { setPayloadResult(res.data.message || 'Payload generated successfully') setMsg({ success: 'Payload generated', error: '' }) + if (res.data.saved) fetchFiles() } else { setPayloadResult('Error: ' + (res.data.error || 'unknown')) setMsg({ success: '', error: res.data.error || 'Generation failed' }) @@ -243,6 +247,38 @@ export default function Dashboard() { useEffect(() => { loadStatus(); loadGadgets() }, []) + useEffect(() => { + if (mode !== 'gadget') return + fetchFiles() + }, [mode]) + + async function fetchFiles() { + try { + const res = await getFiles() + setFiles(res.data) + } catch { /* ignore */ } + } + + async function handleDownloadFile(name) { + try { + const res = await downloadFile(name) + const url = window.URL.createObjectURL(new Blob([res.data])) + const a = document.createElement('a') + a.href = url + a.download = name + a.click() + window.URL.revokeObjectURL(url) + } catch { /* ignore */ } + } + + async function handleDeleteFile(name) { + if (!confirm(`Delete ${name}?`)) return + try { + await apiDeleteFile(name) + fetchFiles() + } catch { /* ignore */ } + } + useEffect(() => { if (activeJndiTab !== 'logs') return fetchLogs() @@ -280,6 +316,9 @@ export default function Dashboard() {
+ + Wiki +
+
+
+

File Manager

+ +
+
+ {files.length === 0 ? ( +
No saved files
+ ) : ( + files.map(f => ( +
+ {f.name} + {f.size > 1024 ? (f.size / 1024).toFixed(1) + ' KB' : f.size + ' B'} + + +
+ )) + )} +
+
+ )} diff --git a/src/main/java/com/qi4l/JYso/web/JettyApiServlet.java b/src/main/java/com/qi4l/JYso/web/JettyApiServlet.java index 41f99b4..abbf316 100644 --- a/src/main/java/com/qi4l/JYso/web/JettyApiServlet.java +++ b/src/main/java/com/qi4l/JYso/web/JettyApiServlet.java @@ -52,6 +52,12 @@ public class JettyApiServlet extends HttpServlet { handleStatus(resp); } else if ("/logs".equals(path) && "GET".equalsIgnoreCase(req.getMethod())) { handleLogs(req, resp); + } else if ("/files".equals(path) && "GET".equalsIgnoreCase(req.getMethod())) { + handleFileList(resp); + } else if ("/files/download".equals(path) && "GET".equalsIgnoreCase(req.getMethod())) { + handleFileDownload(req, resp); + } else if ("/files/delete".equals(path) && "POST".equalsIgnoreCase(req.getMethod())) { + handleFileDelete(req, resp); } else { resp.setStatus(404); resp.getWriter().write("{\"error\":\"Not found\"}"); @@ -96,6 +102,63 @@ public class JettyApiServlet extends HttpServlet { resp.getWriter().write(JSON.toJSONString(result)); } + private void handleFileList(HttpServletResponse resp) throws IOException { + java.io.File dir = new java.io.File("."); + java.io.File[] files = dir.listFiles(f -> f.isFile() && f.getName().endsWith(".ser")); + List> list = new ArrayList<>(); + if (files != null) { + for (java.io.File f : files) { + Map item = new LinkedHashMap<>(); + item.put("name", f.getName()); + item.put("size", f.length()); + item.put("modified", f.lastModified()); + list.add(item); + } + } + list.sort((a, b) -> Long.compare((long) b.get("modified"), (long) a.get("modified"))); + resp.getWriter().write(JSON.toJSONString(list)); + } + + private void handleFileDownload(HttpServletRequest req, HttpServletResponse resp) throws IOException { + String name = req.getParameter("name"); + if (name == null || name.isEmpty()) { + resp.setStatus(400); + resp.getWriter().write("{\"error\":\"name required\"}"); + return; + } + Path filePath = Paths.get(name); + if (!Files.exists(filePath)) { + resp.setStatus(404); + resp.getWriter().write("{\"error\":\"file not found\"}"); + return; + } + resp.setContentType("application/octet-stream"); + resp.setHeader("Content-Disposition", "attachment; filename=\"" + name + "\""); + resp.setContentLengthLong(Files.size(filePath)); + Files.copy(filePath, resp.getOutputStream()); + } + + private void handleFileDelete(HttpServletRequest req, HttpServletResponse resp) throws IOException { + JSONObject json = readJson(req); + String name = json.getString("name"); + Map result = new LinkedHashMap<>(); + if (name == null || name.isEmpty()) { + result.put("success", false); + result.put("error", "name required"); + resp.getWriter().write(JSON.toJSONString(result)); + return; + } + Path filePath = Paths.get(name); + if (!Files.exists(filePath)) { + result.put("success", false); + result.put("error", "file not found"); + } else { + Files.delete(filePath); + result.put("success", true); + } + resp.getWriter().write(JSON.toJSONString(result)); + } + private void handleServersStart(HttpServletRequest req, HttpServletResponse resp) throws IOException { JSONObject json = readJson(req); Map result = new LinkedHashMap<>();