mirror of
https://github.com/qi4L/JYso.git
synced 2026-09-21 22:40:43 +08:00
perf:前端大改4
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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<>();
|
||||||
|
|||||||
Reference in New Issue
Block a user