perf:前端大改4

This commit is contained in:
qi4l
2026-05-01 10:25:04 +08:00
parent 02e817167e
commit 74123d1a09
4 changed files with 235 additions and 5 deletions
+20 -2
View File
@@ -1,13 +1,18 @@
import axios from 'axios' import axios from 'axios'
let authToken = null let authToken = localStorage.getItem('jyso_token')
export function setAuthToken(token) { export function setAuthToken(token) {
authToken = token authToken = token
if (token) {
localStorage.setItem('jyso_token', token)
} else {
localStorage.removeItem('jyso_token')
}
} }
export function getAuthToken() { export function getAuthToken() {
return authToken return authToken || localStorage.getItem('jyso_token')
} }
const api = axios.create({ const api = axios.create({
@@ -27,6 +32,7 @@ api.interceptors.response.use(
error => { error => {
if (error.response && error.response.status === 401) { if (error.response && error.response.status === 401) {
authToken = null authToken = null
localStorage.removeItem('jyso_token')
} }
return Promise.reject(error) return Promise.reject(error)
} }
@@ -72,4 +78,16 @@ export function getLogs(count) {
return api.get('/logs', { params: { 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 export default api
+81
View File
@@ -825,6 +825,87 @@ select {
.login-inner { padding: 36px 28px 32px; } .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) { @media (prefers-reduced-motion: reduce) {
*, *::before, *::after { *, *::before, *::after {
transition-duration: 0ms !important; transition-duration: 0ms !important;
+71 -3
View File
@@ -3,7 +3,8 @@ import { useNavigate } from 'react-router-dom'
import { useTheme } from '../context/ThemeContext' import { useTheme } from '../context/ThemeContext'
import { import {
getStatus, toggleServer, getGadgets, getStatus, toggleServer, getGadgets,
generatePayload as apiGenerate, updateConfig, setAuthToken, getLogs generatePayload as apiGenerate, updateConfig, setAuthToken, getLogs,
getFiles, downloadFile, deleteFile as apiDeleteFile
} from '../api' } from '../api'
export default function Dashboard() { export default function Dashboard() {
@@ -50,6 +51,8 @@ export default function Dashboard() {
const [logLines, setLogLines] = useState([]) const [logLines, setLogLines] = useState([])
const [logLoading, setLogLoading] = useState(false) const [logLoading, setLogLoading] = useState(false)
const logEndRef = useRef(null) 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'] const ROUTING_OPTIONS = ['Basic', 'ELProcessor', 'Groovy', 'jdbcBypass1', 'jdbcBypass2', 'ldap2rmi', 'SnakeYaml', 'XStream', 'MemoryXXE']
@@ -140,6 +143,7 @@ export default function Dashboard() {
if (res.data.success) { if (res.data.success) {
setPayloadResult(res.data.message || 'Payload generated successfully') setPayloadResult(res.data.message || 'Payload generated successfully')
setMsg({ success: 'Payload generated', error: '' }) setMsg({ success: 'Payload generated', error: '' })
if (res.data.saved) fetchFiles()
} else { } else {
setPayloadResult('Error: ' + (res.data.error || 'unknown')) setPayloadResult('Error: ' + (res.data.error || 'unknown'))
setMsg({ success: '', error: res.data.error || 'Generation failed' }) setMsg({ success: '', error: res.data.error || 'Generation failed' })
@@ -243,6 +247,38 @@ export default function Dashboard() {
useEffect(() => { loadStatus(); loadGadgets() }, []) 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(() => { useEffect(() => {
if (activeJndiTab !== 'logs') return if (activeJndiTab !== 'logs') return
fetchLogs() fetchLogs()
@@ -280,6 +316,9 @@ export default function Dashboard() {
</button> </button>
</div> </div>
<div className="header-right"> <div className="header-right">
<a className="wiki-btn" href="https://github.com/qi4L/JYso/wiki" target="_blank" rel="noopener noreferrer" title="Wiki">
Wiki
</a>
<button <button
className="theme-toggle" className="theme-toggle"
onClick={toggleTheme} onClick={toggleTheme}
@@ -633,8 +672,9 @@ export default function Dashboard() {
)} )}
{mode === 'gadget' && ( {mode === 'gadget' && (
<div className="glass-card section-enter"> <div style={{ display: 'flex', gap: 'var(--card-gap)', alignItems: 'flex-start' }}>
<div className="header" style={{ padding: 0, marginBottom: 16 }}> <div className="glass-card section-enter" style={{ flex: 3, marginBottom: 0 }}>
<div className="header" style={{ padding: 0, marginBottom: 16 }}>
<h2>Payload Generator</h2> <h2>Payload Generator</h2>
<button <button
className={`btn btn-secondary${showAdvanced ? ' active-tab' : ''}`} className={`btn btn-secondary${showAdvanced ? ' active-tab' : ''}`}
@@ -766,6 +806,34 @@ export default function Dashboard() {
</div> </div>
)} )}
</div> </div>
<div className="glass-card section-enter" style={{ flex: 2, marginBottom: 0 }}>
<div className="header" style={{ padding: 0, marginBottom: 16 }}>
<h2>File Manager</h2>
<button className="btn btn-secondary" style={{ padding: '4px 10px', fontSize: 11, width: 'auto' }}
onClick={fetchFiles} disabled={filesLoading}>
{filesLoading ? 'Loading...' : 'Refresh'}
</button>
</div>
<div className="file-list">
{files.length === 0 ? (
<div className="file-empty">No saved files</div>
) : (
files.map(f => (
<div key={f.name} className="file-row">
<span className="file-name" title={f.name}>{f.name}</span>
<span className="file-size">{f.size > 1024 ? (f.size / 1024).toFixed(1) + ' KB' : f.size + ' B'}</span>
<button className="file-btn" onClick={() => handleDownloadFile(f.name)} title="Download">
<svg width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M8 2v8M4 7l4 4 4-4M2 13h12" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/></svg>
</button>
<button className="file-btn file-btn-del" onClick={() => handleDeleteFile(f.name)} title="Delete">
<svg width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M3 5h10M6 5V3h4v2M5 5v8h6V5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/></svg>
</button>
</div>
))
)}
</div>
</div>
</div>
)} )}
</div> </div>
</div> </div>
@@ -52,6 +52,12 @@ public class JettyApiServlet extends HttpServlet {
handleStatus(resp); handleStatus(resp);
} else if ("/logs".equals(path) && "GET".equalsIgnoreCase(req.getMethod())) { } else if ("/logs".equals(path) && "GET".equalsIgnoreCase(req.getMethod())) {
handleLogs(req, resp); 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 { } else {
resp.setStatus(404); resp.setStatus(404);
resp.getWriter().write("{\"error\":\"Not found\"}"); resp.getWriter().write("{\"error\":\"Not found\"}");
@@ -96,6 +102,63 @@ public class JettyApiServlet extends HttpServlet {
resp.getWriter().write(JSON.toJSONString(result)); 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<Map<String, Object>> list = new ArrayList<>();
if (files != null) {
for (java.io.File f : files) {
Map<String, Object> 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<String, Object> 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 { private void handleServersStart(HttpServletRequest req, HttpServletResponse resp) throws IOException {
JSONObject json = readJson(req); JSONObject json = readJson(req);
Map<String, Object> result = new LinkedHashMap<>(); Map<String, Object> result = new LinkedHashMap<>();