From 8eafe19a1d696d36af9f94e8eda2ed725123af22 Mon Sep 17 00:00:00 2001 From: RomkaZVO Date: Wed, 29 Jul 2026 00:34:23 +0800 Subject: [PATCH] =?UTF-8?q?=D0=BD=D0=B0=D0=B2=D0=B5=D0=BB=20=D0=BF=D0=BE?= =?UTF-8?q?=D1=80=D1=8F=D0=B4=20=D0=B2=20=D1=81=D1=82=D1=80=D1=83=D0=BA?= =?UTF-8?q?=D1=82=D1=83=D1=80=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- addition/busybox_nginx_reverse.txt | 64 ++ www/403.html | 61 ++ www/404.html | 60 ++ www/cgi-bin/.htpasswd | 1 + www/cgi-bin/Makefile | 25 + www/cgi-bin/config.ini | 30 + www/cgi-bin/src/auth.c | 261 ++++++ www/cgi-bin/src/auth.h | 14 + www/cgi-bin/src/check_token.c | 53 ++ www/cgi-bin/src/config.c | 139 +++ www/cgi-bin/src/config.h | 32 + www/cgi-bin/src/fs.c | 179 ++++ www/cgi-bin/src/fs.h | 38 + www/cgi-bin/src/main.c | 245 +++++ www/cgi-bin/src/render.c | 657 +++++++++++++ www/cgi-bin/src/render.h | 18 + www/cgi-bin/src/status.c | 97 ++ www/cgi-bin/src/style.c | 36 + www/cgi-bin/src/utils.c | 98 ++ www/cgi-bin/src/utils.h | 13 + www/cgi-bin/style.css | 1387 ++++++++++++++++++++++++++++ www/cgi-bin/template.html | 546 +++++++++++ 23 files changed, 4055 insertions(+), 1 deletion(-) create mode 100644 addition/busybox_nginx_reverse.txt create mode 100644 www/403.html create mode 100644 www/404.html create mode 100644 www/cgi-bin/.htpasswd create mode 100644 www/cgi-bin/Makefile create mode 100755 www/cgi-bin/config.ini create mode 100644 www/cgi-bin/src/auth.c create mode 100644 www/cgi-bin/src/auth.h create mode 100644 www/cgi-bin/src/check_token.c create mode 100644 www/cgi-bin/src/config.c create mode 100644 www/cgi-bin/src/config.h create mode 100644 www/cgi-bin/src/fs.c create mode 100644 www/cgi-bin/src/fs.h create mode 100644 www/cgi-bin/src/main.c create mode 100644 www/cgi-bin/src/render.c create mode 100644 www/cgi-bin/src/render.h create mode 100644 www/cgi-bin/src/status.c create mode 100644 www/cgi-bin/src/style.c create mode 100644 www/cgi-bin/src/utils.c create mode 100644 www/cgi-bin/src/utils.h create mode 100755 www/cgi-bin/style.css create mode 100644 www/cgi-bin/template.html diff --git a/README.md b/README.md index 408cfb5..0e239ef 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Trashbox — Минималистичный CGI-файловый сервер -Легковесный файлообменник на чистом C для Linux/Debian. Работает через связку **Nginx (Reverse Proxy)** + **BusyBox `httpd` (CGI Backend)**. +Легковесный файлообменник на чистом C для GNU/Linux. Работает через связку **Nginx (Reverse Proxy)** + **BusyBox `httpd` (CGI Backend)**. > **Примечания:**
1) Nginx опционален, нужен только если необходимен Reverse Proxy, например, для SSL.
> 2) Зачем? Хотелось что-то легковесного, но сам я не погромист, потому был применён вайбкод подход, делал вместе с DeepSeek. Вдохновленно [этим](https://github.com/metalx1000/Directory-Index-for-httpd) проектом, но там есть нюанс, сурсы закрыты, а что-то такое от рута пускать такое себе.
> 3) [Здесь](https://home.mashup.su) можно потыкать и посмотреть как работает.
diff --git a/addition/busybox_nginx_reverse.txt b/addition/busybox_nginx_reverse.txt new file mode 100644 index 0000000..831fa4c --- /dev/null +++ b/addition/busybox_nginx_reverse.txt @@ -0,0 +1,64 @@ +server { + listen 443 ssl; + http2 on; + server_name domain.com www.domain.com; + ssl_certificate /etc/letsencrypt/live/domain.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/domain.com/privkey.pem; + + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384; + + root /home/romkazvo/www; + + error_page 404 =404 /404.html; + location = /404.html { + root /home/romkazvo/www; + allow all; + } + + error_page 403 =403 /403.html; + location = /403.html { + root /home/romkazvo/www; + allow all; + } + + location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|webp)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + try_files $uri =404; + } + + location /cgi-bin/ { + proxy_pass http://127.0.0.1:8050; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_http_version 1.1; + proxy_buffering off; + proxy_cache off; + proxy_read_timeout 300s; + proxy_send_timeout 300s; + proxy_intercept_errors on; + } + + location / { + proxy_pass http://127.0.0.1:8050; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_http_version 1.1; + proxy_buffering off; + proxy_cache off; + proxy_read_timeout 300s; + proxy_send_timeout 300s; + proxy_intercept_errors on; + } +} + +server { + listen 80; + server_name domain.com www.domain.com; + return 301 https://$server_name$request_uri; +} diff --git a/www/403.html b/www/403.html new file mode 100644 index 0000000..1ef3cf5 --- /dev/null +++ b/www/403.html @@ -0,0 +1,61 @@ + + + + + + Trashbox - 403 Доступ запрещён + + + + +
+
403
+

Доступ запрещён

+

Эй, дружок-пирожок, сюда нельзя

+ Вернуться на главную +
+ + + diff --git a/www/404.html b/www/404.html new file mode 100644 index 0000000..dabe65f --- /dev/null +++ b/www/404.html @@ -0,0 +1,60 @@ + + + + + + Trashbox - 404 Страница не найдена + + + + +
+
404
+

Страница не найдена

+

Эй, дружок-пирожок, тобой выбрана неправильная дверь

+ Вернуться на главную +
+ + diff --git a/www/cgi-bin/.htpasswd b/www/cgi-bin/.htpasswd new file mode 100644 index 0000000..a56d9b7 --- /dev/null +++ b/www/cgi-bin/.htpasswd @@ -0,0 +1 @@ +test folder:123456 diff --git a/www/cgi-bin/Makefile b/www/cgi-bin/Makefile new file mode 100644 index 0000000..e22f56c --- /dev/null +++ b/www/cgi-bin/Makefile @@ -0,0 +1,25 @@ +CC = gcc +CFLAGS = -Wall -Wextra -O2 -I./src +TARGETS = index.cgi style.cgi status.cgi + +SRCS_INDEX = src/main.c src/config.c src/auth.c src/fs.c src/render.c src/utils.c +OBJS_INDEX = $(SRCS_INDEX:.c=.o) + +all: $(TARGETS) + +index.cgi: $(OBJS_INDEX) + $(CC) $(CFLAGS) -o $@ $^ + +style.cgi: src/style.c + $(CC) $(CFLAGS) -o $@ $< + +status.cgi: src/status.c + $(CC) $(CFLAGS) -o $@ $< + +clean: + rm -f $(OBJS_INDEX) $(TARGETS) + +install: all + chmod +x $(TARGETS) + +.PHONY: all clean install diff --git a/www/cgi-bin/config.ini b/www/cgi-bin/config.ini new file mode 100755 index 0000000..b1086b2 --- /dev/null +++ b/www/cgi-bin/config.ini @@ -0,0 +1,30 @@ +# Trashbox CGI config + +[paths] +base_path = /home/romkazvo/www +template_path = /home/romkazvo/www/cgi-bin/template.html +passwd_file = /home/romkazvo/www/cgi-bin/.htpasswd + +[limits] +max_entries = 1000 + +[hidden] +dirs = cgi-bin,.git,.svn +files = .htpasswd,.htaccess,.gitignore, robots.txt, 404.html, 403.html + +[preview] +image = jpg,jpeg,png,gif,webp,bmp,svg,ico +text = txt,md,html,htm,css,js,json,xml,csv +audio = mp3,wav,ogg,flac,m4a,aac +video = mp4,webm,ogv,mov,avi,mkv +pdf = pdf + +[security] +cookie_name = folder_auth +cookie_lifetime = 3600 +max_attempts = 5 +block_time = 900 + +[logging] +enable = 1 +log_file = /home/romkazvo/www/cgi-bin/logs/access.log diff --git a/www/cgi-bin/src/auth.c b/www/cgi-bin/src/auth.c new file mode 100644 index 0000000..eba8a51 --- /dev/null +++ b/www/cgi-bin/src/auth.c @@ -0,0 +1,261 @@ +#include +#include +#include +#include +#include +#include +#include +#include "auth.h" +#include "config.h" +#include "utils.h" + +#define TOKEN_DIR "/tmp/trashbox_tokens" +#define TOKEN_SALT "TrashBoxSecretSalt2024" + +// ============================================ +// ПРОВЕРКА ПАРОЛЯ +// ============================================ + +int check_folder_password(const char *folder_name, const char *password) { + if (!password || !folder_name || password[0] == '\0') return 0; + + FILE *f = fopen(g_config.passwd_file, "r"); + if (!f) { + fprintf(stderr, "DEBUG: cannot open passwd file: %s\n", g_config.passwd_file); + return 0; + } + + char line[512]; + while (fgets(line, sizeof(line), f)) { + line[strcspn(line, "\r\n")] = 0; + char *colon = strchr(line, ':'); + if (!colon) continue; + *colon = '\0'; + char *folder = line; + char *pass = colon + 1; + const char *last_slash = strrchr(folder_name, '/'); + const char *base_name = last_slash ? last_slash + 1 : folder_name; + + if (strcmp(folder, folder_name) == 0 || strcmp(folder, base_name) == 0) { + fclose(f); + return strcmp(pass, password) == 0; + } + } + fclose(f); + return 0; +} + +int is_folder_protected(const char *folder_path) { + if (!folder_path || folder_path[0] == '\0') return 0; + + FILE *f = fopen(g_config.passwd_file, "r"); + if (!f) return 0; + + char line[512]; + while (fgets(line, sizeof(line), f)) { + line[strcspn(line, "\r\n")] = 0; + char *colon = strchr(line, ':'); + if (!colon) continue; + *colon = '\0'; + const char *last_slash = strrchr(folder_path, '/'); + const char *folder_name = last_slash ? last_slash + 1 : folder_path; + if (strcmp(line, folder_path) == 0 || strcmp(line, folder_name) == 0) { + fclose(f); + return 1; + } + } + fclose(f); + return 0; +} + +// ============================================ +// ЛИМИТ ПОПЫТОК +// ============================================ + +void get_attempts_file(char *buf, size_t size, const char *folder_name, const char *ip) { + char safe_folder[256]; + strncpy(safe_folder, folder_name, sizeof(safe_folder) - 1); + safe_folder[sizeof(safe_folder) - 1] = '\0'; + for (char *p = safe_folder; *p; p++) { + if (*p == '/' || *p == '\\' || *p == '.') *p = '_'; + } + snprintf(buf, size, "/tmp/trashbox_attempts_%s_%s", safe_folder, ip); +} + +int check_attempts(const char *folder_name, const char *ip) { + if (!folder_name || !ip) return 0; + + char attempts_file[256]; + get_attempts_file(attempts_file, sizeof(attempts_file), folder_name, ip); + + FILE *f = fopen(attempts_file, "r"); + if (!f) return 0; + + int attempts; + time_t first_attempt_time; + + if (fscanf(f, "%d %ld", &attempts, &first_attempt_time) != 2) { + fclose(f); + return 0; + } + fclose(f); + + time_t now = time(NULL); + if (now - first_attempt_time > g_config.block_time) { + unlink(attempts_file); + return 0; + } + + return attempts >= g_config.max_attempts; +} + +void add_attempt(const char *folder_name, const char *ip) { + if (!folder_name || !ip) return; + + char attempts_file[256]; + get_attempts_file(attempts_file, sizeof(attempts_file), folder_name, ip); + + int attempts = 0; + time_t first_attempt_time = time(NULL); + + FILE *f = fopen(attempts_file, "r"); + if (f) { + fscanf(f, "%d %ld", &attempts, &first_attempt_time); + fclose(f); + attempts++; + } else { + attempts = 1; + } + + f = fopen(attempts_file, "w"); + if (f) { + fprintf(f, "%d %ld\n", attempts, first_attempt_time); + fclose(f); + } +} + +void clear_attempts(const char *folder_name, const char *ip) { + if (!folder_name || !ip) return; + + char attempts_file[256]; + get_attempts_file(attempts_file, sizeof(attempts_file), folder_name, ip); + unlink(attempts_file); +} + +// ============================================ +// ТОКЕНЫ +// ============================================ + +char* generate_token(const char *path) { + // ============================================ + // ФИНАЛЬНЫЙ ФИКС: очищаем путь от любых нежелательных символов + // ============================================ + char clean_path[1024]; + strncpy(clean_path, path, sizeof(clean_path) - 1); + clean_path[sizeof(clean_path) - 1] = '\0'; + + // Удаляем всё после '%' и другие нежелательные символы + char *p = clean_path; + while (*p) { + if (*p == '%' || *p == '\n' || *p == '\r') { + *p = '\0'; + break; + } + p++; + } + + // Если путь пустой — используем "default" + if (clean_path[0] == '\0') { + strcpy(clean_path, "default"); + } + + mkdir(TOKEN_DIR, 0700); + + time_t now = time(NULL); + char input[1024]; + snprintf(input, sizeof(input), "%ld_%s_%s", now, clean_path, TOKEN_SALT); + + unsigned long hash = 0; + for (int i = 0; input[i]; i++) { + hash = hash * 31 + input[i]; + } + + char token[256]; + snprintf(token, sizeof(token), "%lx_%ld", hash, now); + + char token_file[512]; + snprintf(token_file, sizeof(token_file), "%s/%s", TOKEN_DIR, token); + + FILE *f = fopen(token_file, "w"); + if (!f) return NULL; + fprintf(f, "%s", clean_path); + fclose(f); + + return strdup(token); +} + +int check_token(const char *token, char *path, size_t path_size) { + if (!token || !token[0]) return 0; + + char token_file[512]; + snprintf(token_file, sizeof(token_file), "%s/%s", TOKEN_DIR, token); + + FILE *f = fopen(token_file, "r"); + if (!f) return 0; + + char stored_path[1024]; + if (fgets(stored_path, sizeof(stored_path), f) == NULL) { + fclose(f); + return 0; + } + fclose(f); + + stored_path[strcspn(stored_path, "\n")] = '\0'; + + // Обрезаем % в конце + char *pp = stored_path; + while (*pp) { + if (*pp == '%' || *pp == '\n' || *pp == '\r') { + *pp = '\0'; + break; + } + pp++; + } + + struct stat st; + if (stat(token_file, &st) != 0) return 0; + + time_t now = time(NULL); + if (now - st.st_mtime > 3600) { + unlink(token_file); + return 0; + } + + strncpy(path, stored_path, path_size - 1); + path[path_size - 1] = '\0'; + + return 1; +} + +void cleanup_old_tokens(void) { + DIR *dir = opendir(TOKEN_DIR); + if (!dir) return; + + struct dirent *entry; + time_t now = time(NULL); + + while ((entry = readdir(dir)) != NULL) { + if (entry->d_name[0] == '.') continue; + + char token_file[512]; + snprintf(token_file, sizeof(token_file), "%s/%s", TOKEN_DIR, entry->d_name); + + struct stat st; + if (stat(token_file, &st) == 0) { + if (now - st.st_mtime > 3600) { + unlink(token_file); + } + } + } + closedir(dir); +} \ No newline at end of file diff --git a/www/cgi-bin/src/auth.h b/www/cgi-bin/src/auth.h new file mode 100644 index 0000000..c5a1b91 --- /dev/null +++ b/www/cgi-bin/src/auth.h @@ -0,0 +1,14 @@ +#ifndef AUTH_H +#define AUTH_H + +int is_folder_protected(const char *folder_path); +int check_folder_password(const char *folder_name, const char *password); +int check_attempts(const char *folder_name, const char *ip); +void add_attempt(const char *folder_name, const char *ip); +void clear_attempts(const char *folder_name, const char *ip); + +char* generate_token(const char *path); +int check_token(const char *token, char *path, size_t path_size); +void cleanup_old_tokens(void); + +#endif diff --git a/www/cgi-bin/src/check_token.c b/www/cgi-bin/src/check_token.c new file mode 100644 index 0000000..5522524 --- /dev/null +++ b/www/cgi-bin/src/check_token.c @@ -0,0 +1,53 @@ +#include +#include +#include +#include +#include +#include + +#define TOKEN_DIR "/tmp/trashbox_tokens" + +int main() { + char *query_string = getenv("QUERY_STRING"); + if (!query_string) { + printf("Content-type: text/plain\n\ninvalid"); + return 0; + } + + char *token_start = strstr(query_string, "token="); + if (!token_start) { + printf("Content-type: text/plain\n\ninvalid"); + return 0; + } + + token_start += 6; + char *token_end = strchr(token_start, '&'); + int token_len = token_end ? (int)(token_end - token_start) : (int)strlen(token_start); + if (token_len <= 0 || token_len >= 256) { + printf("Content-type: text/plain\n\ninvalid"); + return 0; + } + + char token[256]; + strncpy(token, token_start, token_len); + token[token_len] = '\0'; + + char token_file[512]; + snprintf(token_file, sizeof(token_file), "%s/%s", TOKEN_DIR, token); + + struct stat st; + if (stat(token_file, &st) != 0) { + printf("Content-type: text/plain\n\ninvalid"); + return 0; + } + + time_t now = time(NULL); + if (now - st.st_mtime > 3600) { + unlink(token_file); + printf("Content-type: text/plain\n\ninvalid"); + return 0; + } + + printf("Content-type: text/plain\n\nvalid"); + return 0; +} diff --git a/www/cgi-bin/src/config.c b/www/cgi-bin/src/config.c new file mode 100644 index 0000000..97f6861 --- /dev/null +++ b/www/cgi-bin/src/config.c @@ -0,0 +1,139 @@ +#include +#include +#include +#include "config.h" + +config_t g_config; + +int load_config(const char *path) { + FILE *f = fopen(path, "r"); + if (!f) return -1; + + char line[512]; + char current_section[64] = ""; + + while (fgets(line, sizeof(line), f)) { + char *p = line; + while (*p == ' ' || *p == '\t') p++; + if (*p == '\0' || *p == '#' || *p == ';') continue; + + if (*p == '[') { + p++; + char *end = strchr(p, ']'); + if (end) { + *end = '\0'; + strncpy(current_section, p, sizeof(current_section) - 1); + current_section[sizeof(current_section) - 1] = '\0'; + } + continue; + } + + char *eq = strchr(p, '='); + if (!eq) continue; + *eq = '\0'; + char *key = p; + char *value = eq + 1; + + char *end_key = key + strlen(key) - 1; + while (end_key > key && (*end_key == ' ' || *end_key == '\t')) { + *end_key = '\0'; + end_key--; + } + + while (*value == ' ' || *value == '\t') value++; + char *end_val = value + strlen(value) - 1; + while (end_val > value && (*end_val == ' ' || *end_val == '\t' || *end_val == '\r' || *end_val == '\n')) { + *end_val = '\0'; + end_val--; + } + + if (strcmp(current_section, "paths") == 0) { + if (strcmp(key, "base_path") == 0) strncpy(g_config.base_path, value, sizeof(g_config.base_path) - 1); + else if (strcmp(key, "template_path") == 0) strncpy(g_config.template_path, value, sizeof(g_config.template_path) - 1); + else if (strcmp(key, "passwd_file") == 0) strncpy(g_config.passwd_file, value, sizeof(g_config.passwd_file) - 1); + } + else if (strcmp(current_section, "limits") == 0) { + if (strcmp(key, "max_entries") == 0) g_config.max_entries = atoi(value); + } + else if (strcmp(current_section, "hidden") == 0) { + if (strcmp(key, "dirs") == 0) { + char *token = strtok(value, ","); + g_config.hidden_dirs_count = 0; + while (token && g_config.hidden_dirs_count < 10) { + while (*token == ' ') token++; + strncpy(g_config.hidden_dirs[g_config.hidden_dirs_count], token, 255); + g_config.hidden_dirs[g_config.hidden_dirs_count][255] = '\0'; + g_config.hidden_dirs_count++; + token = strtok(NULL, ","); + } + } + else if (strcmp(key, "files") == 0) { + char *token = strtok(value, ","); + g_config.hidden_files_count = 0; + while (token && g_config.hidden_files_count < 10) { + while (*token == ' ') token++; + strncpy(g_config.hidden_files[g_config.hidden_files_count], token, 255); + g_config.hidden_files[g_config.hidden_files_count][255] = '\0'; + g_config.hidden_files_count++; + token = strtok(NULL, ","); + } + } + } + else if (strcmp(current_section, "preview") == 0) { + if (strcmp(key, "image") == 0) strncpy(g_config.preview_image, value, sizeof(g_config.preview_image) - 1); + else if (strcmp(key, "text") == 0) strncpy(g_config.preview_text, value, sizeof(g_config.preview_text) - 1); + else if (strcmp(key, "audio") == 0) strncpy(g_config.preview_audio, value, sizeof(g_config.preview_audio) - 1); + else if (strcmp(key, "video") == 0) strncpy(g_config.preview_video, value, sizeof(g_config.preview_video) - 1); + else if (strcmp(key, "pdf") == 0) strncpy(g_config.preview_pdf, value, sizeof(g_config.preview_pdf) - 1); + } + else if (strcmp(current_section, "security") == 0) { + if (strcmp(key, "max_attempts") == 0) g_config.max_attempts = atoi(value); + else if (strcmp(key, "block_time") == 0) g_config.block_time = atoi(value); + } + else if (strcmp(current_section, "logging") == 0) { + if (strcmp(key, "enable") == 0) { + g_config.enable_access_log = atoi(value); + } else if (strcmp(key, "log_file") == 0) { + strncpy(g_config.log_file, value, sizeof(g_config.log_file) - 1); + g_config.log_file[sizeof(g_config.log_file) - 1] = '\0'; + } + } + } + + fclose(f); + return 0; +} + +void set_default_config(void) { + strcpy(g_config.base_path, "/home/romkazvo/www"); + strcpy(g_config.template_path, "/home/romkazvo/www/cgi-bin/template.html"); + strcpy(g_config.passwd_file, "/home/romkazvo/www/cgi-bin/.htpasswd"); + g_config.max_entries = 1000; + g_config.hidden_dirs_count = 0; + g_config.hidden_files_count = 0; + strcpy(g_config.preview_image, "jpg,jpeg,png,gif,webp,bmp,svg,ico"); + strcpy(g_config.preview_text, "txt,md,html,htm,css,js,json,xml,csv"); + strcpy(g_config.preview_audio, "mp3,wav,ogg,flac,m4a,aac"); + strcpy(g_config.preview_video, "mp4,webm,ogv,mov,avi,mkv"); + strcpy(g_config.preview_pdf, "pdf"); + + g_config.max_attempts = 5; + g_config.block_time = 900; + + strcpy(g_config.log_file, "/home/romkazvo/www/cgi-bin/logs/access.log"); + g_config.enable_access_log = 1; +} + +int is_string_in_list(const char *str, const char *list) { + if (!list || !*list) return 0; + char temp[512]; + strncpy(temp, list, sizeof(temp) - 1); + temp[sizeof(temp) - 1] = '\0'; + char *token = strtok(temp, ","); + while (token) { + while (*token == ' ') token++; + if (strcasecmp(token, str) == 0) return 1; + token = strtok(NULL, ","); + } + return 0; +} diff --git a/www/cgi-bin/src/config.h b/www/cgi-bin/src/config.h new file mode 100644 index 0000000..3a07b9a --- /dev/null +++ b/www/cgi-bin/src/config.h @@ -0,0 +1,32 @@ +#ifndef CONFIG_H +#define CONFIG_H + +typedef struct { + char base_path[1024]; + char template_path[1024]; + char passwd_file[1024]; + int max_entries; + char hidden_dirs[10][256]; + int hidden_dirs_count; + char hidden_files[10][256]; + int hidden_files_count; + char preview_image[512]; + char preview_text[512]; + char preview_audio[512]; + char preview_video[512]; + char preview_pdf[512]; + + int max_attempts; + int block_time; + + char log_file[1024]; + int enable_access_log; +} config_t; + +extern config_t g_config; + +int load_config(const char *path); +void set_default_config(void); +int is_string_in_list(const char *str, const char *list); + +#endif diff --git a/www/cgi-bin/src/fs.c b/www/cgi-bin/src/fs.c new file mode 100644 index 0000000..0971555 --- /dev/null +++ b/www/cgi-bin/src/fs.c @@ -0,0 +1,179 @@ +#include +#include +#include +#include +#include +#include +#include "fs.h" +#include "config.h" +#include "auth.h" +#include "utils.h" + +query_params_t g_params; + +int is_safe_path(const char *path) { + if (!path) return 0; + if (strstr(path, "..")) return 0; + if (strstr(path, "./")) return 0; + if (path[0] == '/') return 0; + if (strstr(path, "//")) return 0; + return 1; +} + +void safe_path_join(char *result, size_t result_size, const char *base, const char *path) { + if (!is_safe_path(path)) { + snprintf(result, result_size, "%s", base); + return; + } + snprintf(result, result_size, "%s/%s", base, path); +} + +int count_files_in_dir(const char *path) { + DIR *dir = opendir(path); + if (!dir) return 0; + struct dirent *entry; + int count = 0; + while ((entry = readdir(dir)) != NULL) { + if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue; + if (entry->d_name[0] == '.') continue; + int hidden = 0; + for (int i = 0; i < g_config.hidden_files_count; i++) { + if (strcmp(entry->d_name, g_config.hidden_files[i]) == 0) { hidden = 1; break; } + } + if (hidden) continue; + count++; + } + closedir(dir); + return count; +} + +long long get_dir_size(const char *path) { + DIR *dir = opendir(path); + if (!dir) return 0; + struct dirent *entry; + struct stat statbuf; + char fullpath[1024]; + long long size = 0; + while ((entry = readdir(dir)) != NULL) { + if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue; + snprintf(fullpath, sizeof(fullpath), "%s/%s", path, entry->d_name); + if (stat(fullpath, &statbuf) == 0) { + if (S_ISDIR(statbuf.st_mode)) size += get_dir_size(fullpath); + else if (S_ISREG(statbuf.st_mode)) size += statbuf.st_size; + } + } + closedir(dir); + return size; +} + +const char* get_file_icon(const char* filename) { + const char *ext = strrchr(filename, '.'); + if (!ext) return "📄"; + ext++; + if (is_string_in_list(ext, g_config.preview_image)) return "🖼️"; + if (is_string_in_list(ext, g_config.preview_audio)) return "🎵"; + if (is_string_in_list(ext, g_config.preview_video)) return "🎬"; + if (strcasecmp(ext, "zip") == 0 || strcasecmp(ext, "rar") == 0 || + strcasecmp(ext, "7z") == 0 || strcasecmp(ext, "tar") == 0 || + strcasecmp(ext, "gz") == 0) return "📦"; + if (strcasecmp(ext, "pdf") == 0) return "📕"; + if (strcasecmp(ext, "doc") == 0 || strcasecmp(ext, "docx") == 0) return "📘"; + if (strcasecmp(ext, "xls") == 0 || strcasecmp(ext, "xlsx") == 0) return "📗"; + if (strcasecmp(ext, "txt") == 0) return "📝"; + return "📄"; +} + +int is_previewable(const char* filename) { + const char *ext = strrchr(filename, '.'); + if (!ext) return 0; + ext++; + return (is_string_in_list(ext, g_config.preview_image) || + is_string_in_list(ext, g_config.preview_text) || + is_string_in_list(ext, g_config.preview_audio) || + is_string_in_list(ext, g_config.preview_video) || + is_string_in_list(ext, g_config.preview_pdf)); +} + +void search_recursive(const char *base_path, const char *display_path, const char *search_term, entry_t *results, int *count, int max_results) { + DIR *dir = opendir(base_path); + if (!dir) return; + struct dirent *entry; + struct stat file_stat; + char full_path[1024]; + char display_subpath[1024]; + while ((entry = readdir(dir)) != NULL && *count < max_results) { + if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue; + if (entry->d_name[0] == '.') continue; + int hidden = 0; + for (int i = 0; i < g_config.hidden_dirs_count; i++) { + if (strcmp(entry->d_name, g_config.hidden_dirs[i]) == 0) { hidden = 1; break; } + } + for (int i = 0; i < g_config.hidden_files_count; i++) { + if (strcmp(entry->d_name, g_config.hidden_files[i]) == 0) { hidden = 1; break; } + } + if (hidden) continue; + snprintf(full_path, sizeof(full_path), "%s/%s", base_path, entry->d_name); + if (display_path[0] == '\0') { + snprintf(display_subpath, sizeof(display_subpath), "%s", entry->d_name); + } else { + snprintf(display_subpath, sizeof(display_subpath), "%s/%s", display_path, entry->d_name); + } + if (is_folder_protected(display_subpath) || is_folder_protected(entry->d_name)) { + continue; + } + if (stat(full_path, &file_stat) == 0) { + if (my_strcasestr(entry->d_name, search_term) != NULL) { + entry_t *result = &results[*count]; + snprintf(result->name, sizeof(result->name), "%s", entry->d_name); + if (S_ISDIR(file_stat.st_mode)) { + result->is_dir = 1; + result->size = 0; + result->dir_size = get_dir_size(full_path); + result->file_count = count_files_in_dir(full_path); + result->mtime = file_stat.st_mtime; + strcpy(result->icon, "📁"); + result->is_protected = 0; + url_encode(display_subpath, result->encoded_path, sizeof(result->encoded_path)); + snprintf(result->file_url, sizeof(result->file_url), "%s", display_subpath); + (*count)++; + } else if (S_ISREG(file_stat.st_mode)) { + result->is_dir = 0; + result->size = file_stat.st_size; + result->dir_size = 0; + result->file_count = 0; + result->mtime = file_stat.st_mtime; + strcpy(result->icon, get_file_icon(entry->d_name)); + result->is_protected = 0; + url_encode(display_subpath, result->encoded_path, sizeof(result->encoded_path)); + snprintf(result->file_url, sizeof(result->file_url), "%s", display_subpath); + (*count)++; + } + } + if (S_ISDIR(file_stat.st_mode)) { + search_recursive(full_path, display_subpath, search_term, results, count, max_results); + } + } + } + closedir(dir); +} + +int compare_entries_sorted(const void *a, const void *b) { + const entry_t *entryA = (const entry_t *)a; + const entry_t *entryB = (const entry_t *)b; + + if (entryA->is_dir && !entryB->is_dir) return -1; + if (!entryA->is_dir && entryB->is_dir) return 1; + + int result = 0; + if (strcmp(g_params.sort_by, "size") == 0) { + long long sizeA = entryA->is_dir ? entryA->dir_size : entryA->size; + long long sizeB = entryB->is_dir ? entryB->dir_size : entryB->size; + result = (sizeA > sizeB) - (sizeA < sizeB); + } else if (strcmp(g_params.sort_by, "date") == 0) { + result = (entryA->mtime > entryB->mtime) - (entryA->mtime < entryB->mtime); + } else { + result = strcasecmp(entryA->name, entryB->name); + } + + return result * g_params.sort_order; +} diff --git a/www/cgi-bin/src/fs.h b/www/cgi-bin/src/fs.h new file mode 100644 index 0000000..1fb901c --- /dev/null +++ b/www/cgi-bin/src/fs.h @@ -0,0 +1,38 @@ +#ifndef FS_H +#define FS_H + +#include + +typedef struct { + char name[256]; + int is_dir; + long size; + char icon[8]; + char encoded_path[1024]; + char file_url[2048]; + int is_protected; + time_t mtime; + int file_count; + long long dir_size; +} entry_t; + +typedef struct { + char sort_by[16]; + int sort_order; + char search[256]; + int recursive; + char view[16]; +} query_params_t; + +extern query_params_t g_params; + +int is_safe_path(const char *path); +void safe_path_join(char *result, size_t result_size, const char *base, const char *path); +int count_files_in_dir(const char *path); +long long get_dir_size(const char *path); +const char* get_file_icon(const char* filename); +int is_previewable(const char* filename); +void search_recursive(const char *base_path, const char *display_path, const char *search_term, entry_t *results, int *count, int max_results); +int compare_entries_sorted(const void *a, const void *b); + +#endif diff --git a/www/cgi-bin/src/main.c b/www/cgi-bin/src/main.c new file mode 100644 index 0000000..f90d953 --- /dev/null +++ b/www/cgi-bin/src/main.c @@ -0,0 +1,245 @@ +#include +#include +#include +#include +#include +#include "config.h" +#include "auth.h" +#include "fs.h" +#include "render.h" +#include "utils.h" + +int main() { + setlocale(LC_ALL, "en_US.UTF-8"); + setlocale(LC_CTYPE, "en_US.UTF-8"); + + if (load_config("/home/romkazvo/www/cgi-bin/config.ini") != 0) { + set_default_config(); + } + + strcpy(g_params.sort_by, "name"); + g_params.sort_order = 1; + g_params.search[0] = '\0'; + g_params.recursive = 0; + strcpy(g_params.view, "list"); + + char *method = getenv("REQUEST_METHOD"); + char *query_string = getenv("QUERY_STRING"); + char *remote_addr = getenv("REMOTE_ADDR"); + if (!remote_addr) remote_addr = "unknown"; + + char display_path[1024] = ""; + char safe_display_path[1024] = ""; + char base_path[1024]; + strcpy(base_path, g_config.base_path); + + // ============================================ + // ОБРАБОТКА POST (пароль через форму) + // ============================================ + if (method && strcmp(method, "POST") == 0) { + char *content_length_str = getenv("CONTENT_LENGTH"); + if (content_length_str) { + int content_length = atoi(content_length_str); + if (content_length > 0 && content_length < 65536) { + char *post_data = malloc(content_length + 1); + if (post_data) { + int read_len = fread(post_data, 1, content_length, stdin); + post_data[read_len] = '\0'; + + char post_path[1024] = ""; + char post_password[256] = ""; + + char *p = post_data; + while (*p) { + if (strncmp(p, "path=", 5) == 0) { + p += 5; + char *end = strchr(p, '&'); + int len = end ? (int)(end - p) : (int)strlen(p); + if (len > 0 && len < (int)sizeof(post_path) - 1) { + char encoded[1024]; + strncpy(encoded, p, len); + encoded[len] = '\0'; + url_decode_enhanced(encoded, post_path, sizeof(post_path)); + } + p += len; + if (*p == '&') p++; + } else if (strncmp(p, "password=", 9) == 0) { + p += 9; + char *end = strchr(p, '&'); + int len = end ? (int)(end - p) : (int)strlen(p); + if (len > 0 && len < (int)sizeof(post_password) - 1) { + char encoded[256]; + strncpy(encoded, p, len); + encoded[len] = '\0'; + url_decode_enhanced(encoded, post_password, sizeof(post_password)); + } + p += len; + if (*p == '&') p++; + } else { + p++; + } + } + + free(post_data); + + fprintf(stderr, "DEBUG POST: path='%s', password='%s'\n", post_path, post_password); + + if (post_path[0] && post_password[0]) { + if (check_attempts(post_path, remote_addr)) { + char encoded_path[1024]; + url_encode(post_path, encoded_path, sizeof(encoded_path)); + printf("Status: 302 Found\r\n"); + printf("Location: /cgi-bin/index.cgi?path=%s&error=blocked\r\n\r\n", encoded_path); + return 0; + } + + const char *last_slash = strrchr(post_path, '/'); + const char *folder_name = last_slash ? last_slash + 1 : post_path; + + fprintf(stderr, "DEBUG: checking folder='%s'\n", folder_name); + + if (check_folder_password(folder_name, post_password)) { + fprintf(stderr, "DEBUG: password OK!\n"); + clear_attempts(post_path, remote_addr); + + char encoded_path[1024]; + url_encode(post_path, encoded_path, sizeof(encoded_path)); + + char clean_path[1024]; + strncpy(clean_path, post_path, sizeof(clean_path) - 1); + clean_path[sizeof(clean_path) - 1] = '\0'; + + char *pp = clean_path; + while (*pp) { + if (*pp == '%') { + *pp = '\0'; + break; + } + pp++; + } + + fprintf(stderr, "DEBUG: clean_path before token = '%s'\n", clean_path); + + char *token = generate_token(clean_path); + if (token) { + fprintf(stderr, "DEBUG: token generated: %s\n", token); + // ============================================ + // РЕДИРЕКТ С ТОКЕНОМ + // ============================================ + printf("Status: 302 Found\r\n"); + printf("Location: /cgi-bin/index.cgi?path=%s&token=%s\r\n", encoded_path, token); + printf("\r\n"); + free(token); + return 0; + } else { + fprintf(stderr, "DEBUG: token generation failed\n"); + printf("Status: 302 Found\r\n"); + printf("Location: /cgi-bin/index.cgi?path=%s\r\n", encoded_path); + printf("\r\n"); + return 0; + } + } else { + fprintf(stderr, "DEBUG: password FAILED!\n"); + add_attempt(post_path, remote_addr); + + char encoded_path[1024]; + url_encode(post_path, encoded_path, sizeof(encoded_path)); + + printf("Status: 302 Found\r\n"); + printf("Location: /cgi-bin/index.cgi?path=%s&error=1\r\n", encoded_path); + printf("\r\n"); + return 0; + } + } else { + fprintf(stderr, "DEBUG: path or password empty!\n"); + } + } + } + } + } + + // ============================================ + // ОБРАБОТКА GET + // ============================================ + if (query_string) { + char *path_start = strstr(query_string, "path="); + if (path_start) { + path_start += 5; + char *path_end = strchr(path_start, '&'); + int path_len = path_end ? (int)(path_end - path_start) : (int)strlen(path_start); + if (path_len > 0 && path_len < (int)sizeof(display_path) - 1) { + char encoded_path[1024]; + strncpy(encoded_path, path_start, path_len); + encoded_path[path_len] = '\0'; + url_decode_enhanced(encoded_path, display_path, sizeof(display_path)); + if (!is_safe_path(display_path)) { + display_path[0] = '\0'; + strcpy(base_path, g_config.base_path); + } else { + strncpy(safe_display_path, display_path, sizeof(safe_display_path) - 1); + safe_display_path[sizeof(safe_display_path) - 1] = '\0'; + safe_path_join(base_path, sizeof(base_path), g_config.base_path, safe_display_path); + } + } + } + + char *sort_start = strstr(query_string, "sort="); + if (sort_start) { + sort_start += 5; + char *sort_end = strchr(sort_start, '&'); + int sort_len = sort_end ? (int)(sort_end - sort_start) : (int)strlen(sort_start); + if (sort_len > 0 && sort_len < (int)sizeof(g_params.sort_by)) { + strncpy(g_params.sort_by, sort_start, sort_len); + g_params.sort_by[sort_len] = '\0'; + } + } + + char *order_start = strstr(query_string, "order="); + if (order_start) { + order_start += 6; + if (*order_start == '1') { + g_params.sort_order = 1; + } else if (*order_start == '-') { + g_params.sort_order = -1; + } + } + + char *view_start = strstr(query_string, "view="); + if (view_start) { + view_start += 5; + char *view_end = strchr(view_start, '&'); + int view_len = view_end ? (int)(view_end - view_start) : (int)strlen(view_start); + if (view_len > 0 && view_len < (int)sizeof(g_params.view) - 1) { + strncpy(g_params.view, view_start, view_len); + g_params.view[view_len] = '\0'; + } + } + + char *search_start = strstr(query_string, "search="); + if (search_start) { + search_start += 7; + char *search_end = strchr(search_start, '&'); + int search_len = search_end ? (int)(search_end - search_start) : (int)strlen(search_start); + if (search_len > 0 && search_len < (int)sizeof(g_params.search) - 1) { + char encoded_search[256]; + strncpy(encoded_search, search_start, search_len); + encoded_search[search_len] = '\0'; + url_decode_enhanced(encoded_search, g_params.search, sizeof(g_params.search)); + if (g_params.search[0] != '\0') { + g_params.recursive = 1; + } + } + } + } + + static time_t last_cleanup = 0; + time_t now = time(NULL); + if (now - last_cleanup > 3600) { + cleanup_old_tokens(); + last_cleanup = now; + } + + printf("Content-type: text/html; charset=utf-8\n\n"); + print_template(base_path, display_path); + return 0; +} \ No newline at end of file diff --git a/www/cgi-bin/src/render.c b/www/cgi-bin/src/render.c new file mode 100644 index 0000000..644576f --- /dev/null +++ b/www/cgi-bin/src/render.c @@ -0,0 +1,657 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include "render.h" +#include "config.h" +#include "auth.h" +#include "fs.h" +#include "utils.h" + +extern query_params_t g_params; + +void buffer_init(buffer_t *buf) { + buf->capacity = 65536; + buf->data = malloc(buf->capacity); + buf->size = 0; +} + +void buffer_append(buffer_t *buf, const char *str) { + size_t len = strlen(str); + if (buf->size + len >= buf->capacity) { + buf->capacity *= 2; + buf->data = realloc(buf->data, buf->capacity); + } + memcpy(buf->data + buf->size, str, len); + buf->size += len; +} + +void buffer_free(buffer_t *buf) { free(buf->data); } + +void get_token_part(char *buf, size_t size) { + buf[0] = '\0'; + char *query_string = getenv("QUERY_STRING"); + if (!query_string) return; + + char *token_start = strstr(query_string, "token="); + if (!token_start) return; + + token_start += 6; + char *token_end = strchr(token_start, '&'); + int token_len = token_end ? (int)(token_end - token_start) : (int)strlen(token_start); + if (token_len > 0 && token_len < (int)size - 1) { + strncpy(buf, token_start, token_len); + buf[token_len] = '\0'; + } +} + +void print_breadcrumb(buffer_t *buf, const char *display_path) { + char token_part[256] = ""; + get_token_part(token_part, sizeof(token_part)); + + buffer_append(buf, "
\n"); + buffer_append(buf, " 🏠 Главная"); + + if (display_path && display_path[0]) { + char temp_path[1024] = ""; + char encoded[2048]; + char *path_copy = strdup(display_path); + char *token = strtok(path_copy, "/"); + while (token) { + buffer_append(buf, " / "); + if (temp_path[0]) strcat(temp_path, "/"); + strcat(temp_path, token); + url_encode(temp_path, encoded, sizeof(encoded)); + char safe_token[512]; + html_escape(token, safe_token, sizeof(safe_token)); + + buffer_append(buf, ""); + buffer_append(buf, safe_token); + buffer_append(buf, ""); + + token = strtok(NULL, "/"); + } + free(path_copy); + } + buffer_append(buf, "\n
\n"); +} + +void print_password_form(buffer_t *buf, const char *folder_name) { + char *remote_addr = getenv("REMOTE_ADDR"); + if (!remote_addr) remote_addr = "unknown"; + + if (check_attempts(folder_name, remote_addr)) { + buffer_append(buf, "
\n"); + buffer_append(buf, "
\n"); + buffer_append(buf, "
🚫
\n"); + buffer_append(buf, "

Доступ заблокирован

\n"); + buffer_append(buf, "

Слишком много неудачных попыток. Подождите 15 минут.

\n"); + buffer_append(buf, " ← Вернуться на главную\n"); + buffer_append(buf, "
\n"); + buffer_append(buf, "
\n"); + return; + } + + char encoded_path[1024]; + url_encode(folder_name, encoded_path, sizeof(encoded_path)); + + buffer_append(buf, "
\n"); + buffer_append(buf, "
\n"); + buffer_append(buf, "
🔒
\n"); + buffer_append(buf, "

Папка защищена паролем

\n"); + buffer_append(buf, "

Введите пароль для доступа к папке "); + char safe_name[512]; + html_escape(folder_name, safe_name, sizeof(safe_name)); + buffer_append(buf, safe_name); + buffer_append(buf, "

\n"); + buffer_append(buf, "
\n"); + buffer_append(buf, " \n"); + buffer_append(buf, " \n"); + buffer_append(buf, " \n"); + buffer_append(buf, "
\n"); + buffer_append(buf, "

"); + + char *query_string = getenv("QUERY_STRING"); + if (query_string && strstr(query_string, "error=1")) { + buffer_append(buf, "❌ Неверный пароль! Попробуйте снова."); + } + buffer_append(buf, "

\n"); + buffer_append(buf, " ← Вернуться на главную\n"); + buffer_append(buf, "
\n"); + buffer_append(buf, "
\n"); +} + +void print_content(buffer_t *buf, const char *base_path, const char *display_path) { + fprintf(stderr, "DEBUG: print_content START, display_path='%s'\n", display_path ? display_path : "(null)"); + + // ============================================ + // ОБЪЯВЛЯЕМ entries В НАЧАЛЕ ФУНКЦИИ + // ============================================ + DIR *dir; + struct dirent *entry; + struct stat file_stat; + char full_path[1024]; + entry_t *entries = NULL; + int entry_count = 0, dir_count = 0, file_count = 0; + long long total_size = 0; + int is_recursive_search = 0; + char *remote_addr = getenv("REMOTE_ADDR"); + if (!remote_addr) remote_addr = "unknown"; + + char *query_string = getenv("QUERY_STRING"); + int is_root = 0; + if (!query_string || !strstr(query_string, "path=")) { + fprintf(stderr, "DEBUG: no path=, rendering root\n"); + is_root = 1; + } + + char token_part[256] = ""; + get_token_part(token_part, sizeof(token_part)); + fprintf(stderr, "DEBUG: token_part='%s'\n", token_part); + + int has_valid_token = 0; + + if (!is_root && query_string) { + char *token_start = strstr(query_string, "token="); + if (token_start) { + token_start += 6; + char *token_end = strchr(token_start, '&'); + int token_len = token_end ? (int)(token_end - token_start) : (int)strlen(token_start); + if (token_len > 0 && token_len < 256) { + char token[256]; + strncpy(token, token_start, token_len); + token[token_len] = '\0'; + + char token_path[1024]; + if (check_token(token, token_path, sizeof(token_path))) { + if (strcmp(token_path, display_path) == 0) { + has_valid_token = 1; + fprintf(stderr, "DEBUG: valid token for '%s'\n", display_path); + } + } + } + } + } + + if (!is_root && !has_valid_token && display_path && display_path[0]) { + char *last_slash = strrchr(display_path, '/'); + char *folder_name = last_slash ? last_slash + 1 : (char*)display_path; + + if (query_string && strstr(query_string, "error=1")) { + fprintf(stderr, "DEBUG: error=1, showing password form\n"); + print_password_form(buf, folder_name); + return; + } + + if (is_folder_protected(folder_name)) { + fprintf(stderr, "DEBUG: no valid token, showing password form\n"); + // Добавляем скрипт очистки localStorage + buffer_append(buf, "\n"); + print_password_form(buf, folder_name); + return; + } + } + + // ============================================ + // ВЫДЕЛЯЕМ ПАМЯТЬ ДЛЯ entries + // ============================================ + entries = malloc(sizeof(entry_t) * g_config.max_entries); + if (!entries) { + buffer_append(buf, "
Ошибка выделения памяти
"); + return; + } + + if (g_params.search[0] != '\0') { + int result_count = 0; + search_recursive(g_config.base_path, "", g_params.search, entries, &result_count, g_config.max_entries); + if (result_count == 0) { + buffer_append(buf, "
\n"); + buffer_append(buf, "
🔍
\n"); + buffer_append(buf, "

Ничего не найдено

\n"); + buffer_append(buf, "

По запросу \""); + char safe_search[512]; + html_escape(g_params.search, safe_search, sizeof(safe_search)); + buffer_append(buf, safe_search); + buffer_append(buf, "\" ничего не найдено

\n"); + buffer_append(buf, "
\n"); + free(entries); + return; + } + entry_count = result_count; + is_recursive_search = 1; + for (int i = 0; i < entry_count; i++) { + if (entries[i].is_dir) { + dir_count++; + total_size += entries[i].dir_size; + } else { + file_count++; + total_size += entries[i].size; + } + } + } else { + dir = opendir(base_path); + if (!dir) { + char alt_path[1024]; + snprintf(alt_path, sizeof(alt_path), "%s", g_config.base_path); + if (display_path[0]) { + char *encoded = strdup(display_path); + url_decode_enhanced(encoded, alt_path + strlen(alt_path), sizeof(alt_path) - strlen(alt_path)); + free(encoded); + } + dir = opendir(alt_path); + if (!dir) { + buffer_append(buf, "
\n"); + buffer_append(buf, "
📁
\n"); + char *display_base = strstr(base_path, "/www/"); + if (!display_base) display_base = (char*)base_path; + buffer_append(buf, "

Ошибка открытия директории: "); + buffer_append(buf, display_base); + buffer_append(buf, "

\n"); + buffer_append(buf, "

Папка не найдена

\n"); + buffer_append(buf, "

← Вернуться на главную

\n"); + buffer_append(buf, "
\n"); + free(entries); + return; + } + } + while ((entry = readdir(dir)) != NULL && entry_count < g_config.max_entries) { + if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue; + int hidden = 0; + for (int i = 0; i < g_config.hidden_dirs_count; i++) { + if (strcmp(entry->d_name, g_config.hidden_dirs[i]) == 0) { hidden = 1; break; } + } + for (int i = 0; i < g_config.hidden_files_count; i++) { + if (strcmp(entry->d_name, g_config.hidden_files[i]) == 0) { hidden = 1; break; } + } + if (entry->d_name[0] == '.') hidden = 1; + if (hidden) continue; + snprintf(full_path, sizeof(full_path), "%s/%s", base_path, entry->d_name); + if (stat(full_path, &file_stat) == 0) { + snprintf(entries[entry_count].name, sizeof(entries[0].name), "%s", entry->d_name); + if (S_ISDIR(file_stat.st_mode)) { + entries[entry_count].is_dir = 1; + entries[entry_count].size = 0; + entries[entry_count].mtime = file_stat.st_mtime; + entries[entry_count].file_count = count_files_in_dir(full_path); + entries[entry_count].dir_size = get_dir_size(full_path); + total_size += entries[entry_count].dir_size; + if (is_folder_protected(entry->d_name)) { + strcpy(entries[entry_count].icon, "🔒"); + entries[entry_count].is_protected = 1; + } else { + strcpy(entries[entry_count].icon, "📁"); + entries[entry_count].is_protected = 0; + } + char new_path[2048]; + snprintf(new_path, sizeof(new_path), "%s%s%s", display_path, display_path[0] ? "/" : "", entries[entry_count].name); + url_encode(new_path, entries[entry_count].encoded_path, sizeof(entries[0].encoded_path)); + dir_count++; + } else if (S_ISREG(file_stat.st_mode)) { + entries[entry_count].is_dir = 0; + entries[entry_count].size = file_stat.st_size; + entries[entry_count].mtime = file_stat.st_mtime; + entries[entry_count].file_count = 0; + entries[entry_count].dir_size = 0; + strcpy(entries[entry_count].icon, get_file_icon(entry->d_name)); + entries[entry_count].is_protected = 0; + char encoded_filename[1024]; + url_encode(entry->d_name, encoded_filename, sizeof(encoded_filename)); + char temp_url[2048]; + snprintf(temp_url, sizeof(temp_url), "%s%s%s", display_path, display_path[0] ? "/" : "", encoded_filename); + snprintf(entries[entry_count].file_url, sizeof(entries[0].file_url), "%s", temp_url); + total_size += file_stat.st_size; + file_count++; + } else { + continue; + } + entry_count++; + } + } + closedir(dir); + if (entry_count == 0) { + buffer_append(buf, "
\n"); + buffer_append(buf, "
📄
\n"); + buffer_append(buf, "

Здесь пусто

\n"); + buffer_append(buf, "

В этой директории нет файлов или папок

\n"); + buffer_append(buf, "
\n"); + free(entries); + return; + } + } + + qsort(entries, entry_count, sizeof(entry_t), compare_entries_sorted); + + char encoded_path[1024]; + url_encode(display_path, encoded_path, sizeof(encoded_path)); + char safe_search[512]; + html_escape(g_params.search, safe_search, sizeof(safe_search)); + + int next_order = g_params.sort_order * -1; + char order_str[4]; + snprintf(order_str, sizeof(order_str), "%d", next_order); + + char active_name[64] = ""; + char active_size[64] = ""; + char active_date[64] = ""; + if (strcmp(g_params.sort_by, "name") == 0) { strcpy(active_name, " active"); } + else if (strcmp(g_params.sort_by, "size") == 0) { strcpy(active_size, " active"); } + else if (strcmp(g_params.sort_by, "date") == 0) { strcpy(active_date, " active"); } + + char arrow[8] = ""; + if (g_params.sort_order == 1) strcpy(arrow, "🔽"); + else strcpy(arrow, "🔼"); + + char link_with_token[512]; + if (token_part[0]) { + snprintf(link_with_token, sizeof(link_with_token), "&%s", token_part); + } else { + link_with_token[0] = '\0'; + } + + buffer_append(buf, "
\n"); + buffer_append(buf, "
\n"); + buffer_append(buf, "
\n"); + buffer_append(buf, " \n"); + if (token_part[0]) { + buffer_append(buf, " \n"); + } + buffer_append(buf, " \n"); + buffer_append(buf, " \n"); + buffer_append(buf, "
\n"); + buffer_append(buf, "
\n"); + buffer_append(buf, " \n"); + buffer_append(buf, "
\n"); + + if (is_recursive_search) { + buffer_append(buf, "
"); + buffer_append(buf, "🔍 Результаты поиска по всему сайту: "); + char tmp[32]; + snprintf(tmp, sizeof(tmp), "%d", entry_count); + buffer_append(buf, tmp); + buffer_append(buf, " файлов/папок найдено
\n"); + } + + buffer_append(buf, "\n"); + if (is_recursive_search) { + char stats[256]; + snprintf(stats, sizeof(stats), + "
\n Найдено: папок %d | файлов %d\n
\n", + dir_count, file_count); + buffer_append(buf, stats); + } else { + char total_size_str[32]; + format_size(total_size, total_size_str); + char stats[256]; + snprintf(stats, sizeof(stats), + "
\n Папки: %d | Файлы: %d | Общий размер: %s\n
\n", + dir_count, file_count, total_size_str); + buffer_append(buf, stats); + } + free(entries); + fprintf(stderr, "DEBUG: print_content END\n"); +} + +void print_template(const char *base_path, const char *display_path) { + FILE *file = fopen(g_config.template_path, "r"); + if (!file) { + fprintf(stderr, "DEBUG: cannot open template: %s\n", g_config.template_path); + printf("Error: Cannot read template\n"); + return; + } + fprintf(stderr, "DEBUG: template opened successfully\n"); + + buffer_t output; + buffer_init(&output); + char line[4096]; + while (fgets(line, sizeof(line), file)) { + if (strstr(line, "")) { + print_breadcrumb(&output, display_path); + } else if (strstr(line, "")) { + print_content(&output, base_path, display_path); + } else { + buffer_append(&output, line); + } + } + fclose(file); + + fprintf(stderr, "DEBUG: output size = %zu bytes\n", output.size); + if (output.size == 0) { + fprintf(stderr, "DEBUG: WARNING! output is empty!\n"); + buffer_free(&output); + return; + } + fwrite(output.data, 1, output.size, stdout); + fflush(stdout); + fprintf(stderr, "DEBUG: fwrite done, flushed\n"); + + buffer_free(&output); +} diff --git a/www/cgi-bin/src/render.h b/www/cgi-bin/src/render.h new file mode 100644 index 0000000..0b38b6b --- /dev/null +++ b/www/cgi-bin/src/render.h @@ -0,0 +1,18 @@ +#ifndef RENDER_H +#define RENDER_H + +#include "fs.h" + +typedef struct { + char *data; + size_t size; + size_t capacity; +} buffer_t; + +void buffer_init(buffer_t *buf); +void buffer_append(buffer_t *buf, const char *str); +void buffer_free(buffer_t *buf); +void print_template(const char *base_path, const char *display_path); +void render_folder(buffer_t *buf, const char *base_path, const char *display_path); + +#endif diff --git a/www/cgi-bin/src/status.c b/www/cgi-bin/src/status.c new file mode 100644 index 0000000..42a82ba --- /dev/null +++ b/www/cgi-bin/src/status.c @@ -0,0 +1,97 @@ +// /home/romkazvo/www/cgi-bin/src/status.c +#include +#include +#include +#include + +long get_nginx_memory() { + long total_kb = 0; + char command[] = "ps -o rss= -C nginx 2>/dev/null"; + + FILE *ps = popen(command, "r"); + if (!ps) return 0; + + char line[64]; + while (fgets(line, sizeof(line), ps)) { + long kb; + if (sscanf(line, "%ld", &kb) == 1) { + total_kb += kb; + } + } + pclose(ps); + + return total_kb; +} + +int get_process_memory(const char *process_name, long *memory_kb) { + char command[256]; + snprintf(command, sizeof(command), "ps -o rss= -C %s 2>/dev/null | head -1", process_name); + + FILE *ps = popen(command, "r"); + if (!ps) return 0; + + int result = fscanf(ps, "%ld", memory_kb); + pclose(ps); + + return result == 1; +} + +float get_cpu_load() { + FILE *fp = fopen("/proc/loadavg", "r"); + if (!fp) return 0.0f; + + float load; + fscanf(fp, "%f", &load); + fclose(fp); + + return load; +} + +void get_memory_usage(long *used_mb, long *total_mb) { + FILE *fp = fopen("/proc/meminfo", "r"); + if (!fp) { + *used_mb = 0; + *total_mb = 0; + return; + } + + char line[128]; + long total = 0, available = 0; + + while (fgets(line, sizeof(line), fp)) { + if (sscanf(line, "MemTotal: %ld kB", &total) == 1) { + // сохраняем + } else if (sscanf(line, "MemAvailable: %ld kB", &available) == 1) { + break; + } + } + fclose(fp); + + *total_mb = total / 1024; + *used_mb = (total - available) / 1024; +} + +int main() { + printf("Content-type: text/plain; charset=utf-8\n\n"); + + long busybox_kb = 0, nginx_kb = 0; + get_process_memory("busybox", &busybox_kb); + nginx_kb = get_nginx_memory(); + + double busybox_mb = busybox_kb / 1024.0; + double nginx_mb = nginx_kb / 1024.0; + double total_mb = busybox_mb + nginx_mb; + + float cpu_load = get_cpu_load(); + + long used_mb = 0, total_sys_mb = 0; + get_memory_usage(&used_mb, &total_sys_mb); + + // Две строки + printf("BusyBox: %.1f MB | Nginx: %.1f MB | Всего: %.1f MB\n", + busybox_mb, nginx_mb, total_mb); + printf("CPU Load: %.2f | Память: %ld / %ld MB (%.0f%%)", + cpu_load, used_mb, total_sys_mb, total_sys_mb > 0 ? (float)used_mb / total_sys_mb * 100 : 0); + + return 0; +} diff --git a/www/cgi-bin/src/style.c b/www/cgi-bin/src/style.c new file mode 100644 index 0000000..818e8c1 --- /dev/null +++ b/www/cgi-bin/src/style.c @@ -0,0 +1,36 @@ +#include +#include +#include +#include + +int main() { + char css_path[1024] = "/home/romkazvo/www/cgi-bin/style.css"; + struct stat st; + + if (stat(css_path, &st) != 0) { + printf("Status: 404 Not Found\n"); + printf("Content-type: text/plain\n\n"); + printf("CSS file not found\n"); + return 1; + } + + printf("Content-type: text/css\n"); + printf("Cache-Control: public, max-age=3600\n\n"); + + FILE *css_file = fopen(css_path, "r"); + if (!css_file) { + printf("Status: 500 Internal Server Error\n"); + printf("Content-type: text/plain\n\n"); + printf("Cannot open CSS file\n"); + return 1; + } + + char buffer[4096]; + size_t bytes_read; + while ((bytes_read = fread(buffer, 1, sizeof(buffer), css_file)) > 0) { + fwrite(buffer, 1, bytes_read, stdout); + } + + fclose(css_file); + return 0; +} diff --git a/www/cgi-bin/src/utils.c b/www/cgi-bin/src/utils.c new file mode 100644 index 0000000..b7caffe --- /dev/null +++ b/www/cgi-bin/src/utils.c @@ -0,0 +1,98 @@ +#include +#include +#include +#include +#include +#include "utils.h" + +char* my_strcasestr(const char *haystack, const char *needle) { + if (!haystack || !needle || *needle == '\0') return (char*)haystack; + size_t needle_len = strlen(needle); + while (*haystack) { + if (strncasecmp(haystack, needle, needle_len) == 0) return (char*)haystack; + haystack++; + } + return NULL; +} + +void html_escape(const char *src, char *dst, size_t dst_size) { + if (!src || !dst || dst_size == 0) return; + char *p = dst; + size_t i = 0; + while (*src && i < dst_size - 6) { + switch (*src) { + case '<': strcpy(p, "<"); p += 4; i += 4; break; + case '>': strcpy(p, ">"); p += 4; i += 4; break; + case '"': strcpy(p, """); p += 6; i += 6; break; + case '&': strcpy(p, "&"); p += 5; i += 5; break; + default: *p++ = *src; i++; break; + } + src++; + } + *p = '\0'; +} + +void format_size(long long size, char* buffer) { + if (size < 1024) snprintf(buffer, 32, "%lld B", size); + else if (size < 1024 * 1024) snprintf(buffer, 32, "%.1f KB", size / 1024.0); + else if (size < 1024 * 1024 * 1024) snprintf(buffer, 32, "%.1f MB", size / (1024.0 * 1024.0)); + else snprintf(buffer, 32, "%.1f GB", size / (1024.0 * 1024.0 * 1024.0)); +} + +void format_date(time_t mtime, char* buffer, size_t size) { + struct tm *tm_info = localtime(&mtime); + strftime(buffer, size, "%d.%m.%Y", tm_info); +} + +void url_encode(const char *src, char *dst, size_t dst_size) { + static const char *hex = "0123456789ABCDEF"; + char *p = dst; + size_t i = 0; + while (*src && i < dst_size - 3) { + unsigned char c = (unsigned char)*src; + if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || strchr("-_.~", c)) { + *p++ = c; i++; + } else if (c == ' ') { + *p++ = '%'; *p++ = '2'; *p++ = '0'; i += 3; + } else { + *p++ = '%'; *p++ = hex[(c >> 4) & 0xF]; *p++ = hex[c & 0xF]; i += 3; + } + src++; + } + *p = '\0'; +} + +void url_decode_enhanced(const char *src, char *dst, size_t dst_size) { + char *p = dst; + size_t decoded_len = 0; + while (*src && decoded_len < dst_size - 1) { + if (*src == '%') { + if (src[1] && src[2] && isxdigit(src[1]) && isxdigit(src[2])) { + char hex[3] = {src[1], src[2], '\0'}; + unsigned char c = (unsigned char)strtol(hex, NULL, 16); + if (c >= 0x80 || c >= 0x20 || c == 0x0A || c == 0x0D) { + *p++ = c; + decoded_len++; + } else { + *p++ = '_'; + decoded_len++; + } + src += 3; + } else { + // ============================================ + // ФИКС: если % не валидный — просто пропускаем его + // ============================================ + src++; + } + } else if (*src == '+') { + *p++ = ' '; + decoded_len++; + src++; + } else { + *p++ = *src++; + decoded_len++; + } + } + *p = '\0'; +} \ No newline at end of file diff --git a/www/cgi-bin/src/utils.h b/www/cgi-bin/src/utils.h new file mode 100644 index 0000000..0301302 --- /dev/null +++ b/www/cgi-bin/src/utils.h @@ -0,0 +1,13 @@ +#ifndef UTILS_H +#define UTILS_H + +#include + +void url_encode(const char *src, char *dst, size_t dst_size); +void url_decode_enhanced(const char *src, char *dst, size_t dst_size); +void html_escape(const char *src, char *dst, size_t dst_size); +char* my_strcasestr(const char *haystack, const char *needle); +void format_size(long long size, char* buffer); +void format_date(time_t mtime, char* buffer, size_t size); + +#endif diff --git a/www/cgi-bin/style.css b/www/cgi-bin/style.css new file mode 100755 index 0000000..98a550f --- /dev/null +++ b/www/cgi-bin/style.css @@ -0,0 +1,1387 @@ +/* ============================================ + БАЗА + ============================================ */ +:root { + --bg-primary: #f8f9fa; + --bg-secondary: #ffffff; + --bg-header: linear-gradient(135deg, #2d3436, #636e72); + --text-primary: #2d3436; + --text-secondary: #636e72; + --text-muted: #b2bec3; + --border-color: #dfe6e9; + --border-light: #f1f2f6; + --accent-blue: #0984e3; + --accent-orange: #e17055; + --accent-green: #00b894; + --hover-bg: #f1f2f6; + --shadow: 0 4px 12px rgba(0,0,0,0.04); + --radius: 12px; +} + +.dark-theme { + --bg-primary: #1e1e1e; + --bg-secondary: #2d2d2d; + --bg-header: linear-gradient(135deg, #1e1e1e, #2d2d2d); + --text-primary: #e0e0e0; + --text-secondary: #b0b0b0; + --text-muted: #6c6c6c; + --border-color: #3d3d3d; + --border-light: #333333; + --accent-blue: #74b9ff; + --accent-orange: #fdcb6e; + --accent-green: #55efc4; + --hover-bg: #3d3d3d; + --shadow: 0 4px 12px rgba(0,0,0,0.2); + --radius: 12px; +} + +/* ============================================ + ТЕЛО + ============================================ */ +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + background: var(--bg-primary); + color: var(--text-primary); + line-height: 1.6; + min-height: 100vh; + transition: background 0.25s ease, color 0.25s ease; +} + +body.loaded * { + transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease; +} + +/* ============================================ + КОНТЕЙНЕР + ============================================ */ +.container { + max-width: 100%; + margin: 0 auto; + background: var(--bg-secondary); + min-width: 320px; + min-height: 100vh; + box-shadow: var(--shadow); + transition: background 0.25s ease, box-shadow 0.25s ease; +} + +@media (min-width: 768px) { + .container { + max-width: 1100px; + margin: 16px auto; + min-height: auto; + border-radius: var(--radius); + overflow: hidden; + box-shadow: var(--shadow); + } +} + +/* ============================================ + ШАПКА + ============================================ */ +.header { + background: var(--bg-header); + color: white; + padding: 2rem 1.5rem; + text-align: center; + position: relative; +} + +.header h1 { + font-size: 2rem; + font-weight: 700; + margin: 0; + letter-spacing: -0.5px; +} + +.header .subtitle { + opacity: 0.85; + font-size: 1.05rem; + margin-top: 0.25rem; + font-weight: 400; +} + +/* ============================================ + ОПИСАНИЕ (НЕ УВЕЛИЧИВАЕМ) + ============================================ */ +.site-description { + text-align: center; + padding: 0.75rem 1.5rem; + font-size: 1rem; + color: var(--text-secondary); + border-bottom: 1px solid var(--border-color); + background: var(--bg-primary); + font-weight: 400; + letter-spacing: 0.2px; +} + +/* ============================================ + ХЛЕБНЫЕ КРОШКИ + ============================================ */ +.breadcrumb { + background: var(--bg-primary); + padding: 0.6rem 1.25rem; + border-bottom: 1px solid var(--border-color); + font-size: 0.85rem; + overflow-x: auto; + white-space: nowrap; + -webkit-overflow-scrolling: touch; + color: var(--text-secondary); + display: flex; + align-items: center; + gap: 0.3rem; + flex-wrap: nowrap; +} + +.breadcrumb a { + color: var(--accent-blue); + text-decoration: none; + font-weight: 500; + transition: color 0.15s ease; + padding: 0.1rem 0.2rem; +} + +.breadcrumb a:hover { + color: var(--accent-orange); + text-decoration: underline; +} + +.breadcrumb .separator { + color: var(--text-muted); + user-select: none; + font-weight: 300; + margin: 0 0.1rem; +} + +/* ============================================ + ТУЛБАР + ============================================ */ +.toolbar { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.5rem 1rem; + padding: 0.6rem 1.25rem; + background: var(--bg-secondary); + border-bottom: 1px solid var(--border-light); + transition: background 0.2s ease; +} + +/* Поиск */ +.search-box { + flex: 1 1 220px; + min-width: 150px; +} + +.search-box form { + display: flex; + gap: 0.4rem; + align-items: center; +} + +.search-box input[type="text"] { + flex: 1; + padding: 0.5rem 1rem; + border: 1.5px solid var(--border-color); + border-radius: 50px; + background: var(--bg-primary); + color: var(--text-primary); + font-size: 0.85rem; + transition: border-color 0.2s ease, box-shadow 0.2s ease; + outline: none; + min-width: 80px; + height: 38px; + box-sizing: border-box; +} + +.search-box input[type="text"]:focus { + border-color: var(--accent-blue); + box-shadow: 0 0 0 3px rgba(9, 132, 227, 0.12); +} + +.search-box button { + padding: 0 1.2rem; + background: var(--accent-blue); + color: white; + border: none; + border-radius: 50px; + cursor: pointer; + font-weight: 600; + font-size: 0.85rem; + transition: background 0.2s ease, transform 0.15s ease; + white-space: nowrap; + height: 38px; + display: inline-flex; + align-items: center; + justify-content: center; + box-sizing: border-box; +} + +.search-box button:hover { + background: var(--accent-orange); + transform: scale(1.02); +} + +/* Группы настроек */ +.settings-group { + display: flex; + align-items: center; + gap: 0.25rem; + flex-wrap: wrap; +} + +.settings-label { + color: var(--text-muted); + font-weight: 500; + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.3px; + margin-right: 0.15rem; +} + +/* Разделитель между группами */ +.settings-divider { + width: 1px; + height: 30px; + background: var(--border-color); + margin: 0 0.25rem; +} + +@media (max-width: 768px) { + .settings-divider { + display: none; + } +} + +/* Кнопки вида и сортировки */ +.view-btn, +.sort-btn { + padding: 0 0.9rem; + background: transparent; + border: 1.5px solid var(--border-color); + border-radius: 50px; + color: var(--text-secondary); + font-size: 0.78rem; + cursor: pointer; + transition: all 0.15s ease; + font-weight: 500; + text-decoration: none; + display: inline-flex; + align-items: center; + justify-content: center; + height: 38px; + line-height: 1; + gap: 0.2rem; + box-sizing: border-box; + white-space: nowrap; +} + +.view-btn:hover, +.sort-btn:hover { + background: var(--hover-bg); + border-color: var(--accent-blue); + color: var(--text-primary); +} + +.view-btn.active, +.sort-btn.active { + background: var(--accent-blue); + color: white !important; + border-color: var(--accent-blue); +} + +.view-btn .icon, +.sort-btn .icon { + font-size: 0.85rem; + line-height: 1; +} + +/* Скрываем старый settings-row */ +.settings-row { + display: none !important; +} + +/* ============================================ + СПИСОК ФАЙЛОВ + ============================================ */ +ul { + list-style: none; + padding: 0; + margin: 0; +} + +li { + padding: 0.6rem 1.25rem; + border-bottom: 1px solid var(--border-light); + display: flex; + align-items: center; + flex-wrap: nowrap; + transition: background 0.15s ease; +} + +li:hover { + background: var(--hover-bg); +} + +li:last-child { + border-bottom: none; +} + +li.section-title { + color: var(--text-secondary); + font-size: 0.8rem; + font-weight: 600; + padding: 0.5rem 1.25rem; + border-bottom: 2px solid var(--border-color); + background: var(--bg-primary); + text-transform: uppercase; + letter-spacing: 0.5px; + margin-top: 0.5rem; +} + +li.section-title:first-of-type { + margin-top: 0; +} + +/* ============================================ + ФАЙЛОВАЯ СТРОКА — ФИНАЛЬНЫЙ ФИКС + ============================================ */ +.file-row { + display: flex; + align-items: center; + justify-content: space-between; + flex-grow: 1; + min-width: 0; + gap: 0.75rem; + flex-wrap: nowrap; + width: 100%; +} + +.file-link, +.dir-link { + display: flex; + align-items: center; + flex: 1 1 auto; + min-width: 0; + gap: 0.6rem; + overflow: hidden; + text-decoration: none; + color: var(--text-primary); +} + +.file-link .file-name { + font-weight: 500; +} + +.dir-link .file-name { + font-weight: 600; +} + +.file-link:hover .file-name { + color: var(--accent-blue); +} + +.dir-link:hover .file-name { + color: var(--accent-orange); +} + +.file-icon { + font-size: 1.3rem; + flex-shrink: 0; + width: 28px; + min-width: 28px; + max-width: 28px; + text-align: center; + line-height: 1; +} + +.file-name { + flex: 1 1 auto; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 0.92rem; + transition: color 0.15s ease; +} + +.file-controls { + display: flex; + align-items: center; + gap: 0.3rem; + flex-shrink: 0; + flex-wrap: nowrap; + margin-left: auto; +} + +.file-meta { + color: var(--text-muted); + font-size: 0.75rem; + font-weight: 500; + white-space: nowrap; + text-align: right; + font-variant-numeric: tabular-nums; + flex-shrink: 0; +} + +.file-meta.date { + min-width: 80px; + width: 80px; +} + +.file-meta.size { + min-width: 60px; + width: 60px; + text-align: right; + flex-shrink: 0; +} + +/* Кнопки — фиксированный размер */ +.preview-btn, +.copy-btn, +.download-btn { + background: transparent; + border: 1px solid var(--border-color); + border-radius: 6px; + width: 32px; + height: 32px; + min-width: 32px; + min-height: 32px; + max-width: 32px; + max-height: 32px; + cursor: pointer; + font-size: 0.8rem; + display: inline-flex; + align-items: center; + justify-content: center; + transition: all 0.15s ease; + opacity: 0.6; + color: var(--text-secondary); + padding: 0; + box-sizing: border-box; + flex-shrink: 0; +} + +.preview-btn:hover, +.copy-btn:hover, +.download-btn:hover { + opacity: 1; + transform: scale(1.05); +} + +.preview-btn:hover { + background: var(--accent-blue); + color: white; + border-color: var(--accent-blue); +} + +.copy-btn:hover { + background: var(--accent-blue); + color: white; + border-color: var(--accent-blue); +} + +.download-btn:hover { + background: var(--accent-green); + color: white; + border-color: var(--accent-green); +} + +/* ============================================ + СТАТИСТИКА + ============================================ */ +.stats { + text-align: center; + color: var(--text-secondary); + margin-top: 2rem; + padding: 1rem 1.25rem; + border-top: 1px solid var(--border-color); + font-size: 0.9rem; + background: var(--bg-primary); +} + +.empty-state { + text-align: center; + padding: 3rem 1rem; + color: var(--text-secondary); +} + +.empty-state .icon { + font-size: 3rem; + margin-bottom: 1rem; + opacity: 0.5; +} + +/* ============================================ + ПАРОЛЬНАЯ ФОРМА + ============================================ */ +.password-form-container { + display: flex; + justify-content: center; + align-items: center; + min-height: 400px; + padding: 2rem; +} + +.password-form { + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 12px; + padding: 2rem; + max-width: 400px; + width: 100%; + text-align: center; + box-shadow: 0 4px 20px rgba(0,0,0,0.1); +} + +.password-form .lock-icon { + font-size: 3rem; + margin-bottom: 1rem; + display: block; +} + +.password-form h2 { + font-size: 1.3rem; + margin-bottom: 0.5rem; + color: var(--text-primary); +} + +.password-form p { + color: var(--text-secondary); + margin-bottom: 1.5rem; + font-size: 0.95rem; +} + +.password-form p strong { + color: var(--text-primary); + word-break: break-all; +} + +.password-form form { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.password-form input[type="password"] { + padding: 0.75rem 1rem; + border: 2px solid var(--border-color); + border-radius: 8px; + font-size: 1rem; + background: var(--bg-primary); + color: var(--text-primary); + transition: border-color 0.2s ease; + width: 100%; + box-sizing: border-box; +} + +.password-form input[type="password"]:focus { + outline: none; + border-color: var(--accent-blue); +} + +.password-form button { + padding: 0.75rem; + background: var(--accent-blue); + color: white; + border: none; + border-radius: 8px; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: background 0.2s ease; +} + +.password-form button:hover { + background: var(--accent-green); +} + +.password-form .error-msg { + color: #dc3545; + font-size: 0.9rem; + min-height: 1.5rem; + margin: 0.25rem 0; +} + +.password-form .back-link { + display: inline-block; + margin-top: 0.75rem; + color: var(--text-secondary); + text-decoration: none; + font-size: 0.9rem; +} + +.password-form .back-link:hover { + color: var(--accent-blue); + text-decoration: underline; +} + +.protected-folder { + position: relative; +} + +/* ============================================ + МОДАЛКА + ============================================ */ +.preview-modal { + display: none; + position: fixed; + z-index: 2000; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.7); + backdrop-filter: blur(5px); + -webkit-backdrop-filter: blur(5px); + overflow: auto; +} + +.preview-modal.open { + display: flex !important; + align-items: center; + justify-content: center; +} + +.preview-content { + background: var(--bg-secondary); + border-radius: 12px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); + display: flex; + flex-direction: column; + overflow: hidden; + max-width: 90vw; + max-height: 90vh; + width: auto; + height: auto; + min-width: 320px; + min-height: 200px; +} + +.preview-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.75rem 1rem; + border-bottom: 1px solid var(--border-color); + background: var(--bg-primary); + flex-shrink: 0; + gap: 1rem; +} + +.preview-title { + font-weight: 600; + color: var(--text-primary); + margin: 0; + flex-grow: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 0.95rem; +} + +.close-preview { + background: transparent; + color: var(--text-secondary); + border: none; + border-radius: 6px; + width: 34px; + height: 34px; + cursor: pointer; + font-size: 1.4rem; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.15s ease; +} + +.close-preview:hover { + background: #dc3545; + color: white; +} + +.preview-iframe { + flex-grow: 1; + border: none; + background: var(--bg-secondary); + min-height: 0; + width: 100%; + height: 100%; +} + +.preview-image-container { + display: flex; + align-items: center; + justify-content: center; + flex-grow: 1; + padding: 1rem; + background: var(--bg-primary); + overflow: hidden; + width: 100%; + height: 100%; +} + +.preview-image { + max-width: 100%; + max-height: 100%; + width: auto; + height: auto; + object-fit: contain; + border-radius: 4px; + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1); + display: block; +} + +/* Форма пароля в модалке */ +#passwordFormContainer { + display: flex; + justify-content: center; + align-items: center; + padding: 2rem; + flex-grow: 1; + min-height: 300px; +} + +#passwordFormContainer > div { + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 12px; + padding: 2rem; + max-width: 400px; + width: 100%; + text-align: center; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); +} + +/* ============================================ + ФУТЕР — НЕ УВЕЛИЧИВАЕМ + ============================================ */ +.footer { + background: var(--bg-primary); + border-top: 1px solid var(--border-color); + padding: 1.5rem 1rem; + text-align: center; + color: var(--text-secondary); + font-size: 0.85rem; +} + +.footer a { + color: var(--accent-blue); + text-decoration: none; + font-weight: 500; + transition: opacity 0.15s ease; +} + +.footer a:hover { + opacity: 0.7; +} + +/* ============================================ + ТЕМА И СКРОЛЛ + ============================================ */ +.theme-toggle { + position: fixed; + top: 12px; + right: 12px; + background: rgba(255,255,255,0.15); + border: 1px solid rgba(255,255,255,0.2); + border-radius: 50%; + width: 48px; + height: 48px; + font-size: 1.3rem; + cursor: pointer; + z-index: 1000; + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s ease; + color: white; +} + +.dark-theme .theme-toggle { + background: rgba(0,0,0,0.3); + border-color: rgba(255,255,255,0.1); + color: var(--text-primary); +} + +.theme-toggle:hover { + transform: scale(1.1); + background: rgba(255,255,255,0.25); +} + +.dark-theme .theme-toggle:hover { + background: rgba(0,0,0,0.4); +} + +.scroll-top { + position: fixed; + bottom: 20px; + right: 20px; + width: 48px; + height: 48px; + background: var(--accent-blue); + color: white; + border: none; + border-radius: 50%; + font-size: 1.3rem; + cursor: pointer; + opacity: 0; + visibility: hidden; + transition: all 0.3s ease; + z-index: 1000; + box-shadow: 0 2px 12px rgba(0,0,0,0.2); + display: flex; + align-items: center; + justify-content: center; +} + +.scroll-top.visible { + opacity: 1; + visibility: visible; +} + +.scroll-top:hover { + background: var(--accent-green); + transform: translateY(-2px); + box-shadow: 0 4px 20px rgba(0,0,0,0.3); +} + +/* ============================================ + УВЕДОМЛЕНИЕ + ============================================ */ +#copyNotification { + position: fixed; + bottom: 80px; + left: 50%; + transform: translateX(-50%); + background: var(--accent-green); + color: white; + padding: 12px 24px; + border-radius: 8px; + font-weight: 600; + box-shadow: 0 4px 12px rgba(0,0,0,0.3); + z-index: 3000; + transition: opacity 0.3s; + opacity: 1; +} + +/* ============================================ + АДАПТИВ — ПЛАНШЕТЫ + ============================================ */ +@media (max-width: 768px) { + .header { + padding: 1.25rem 1rem; + } + .header h1 { + font-size: 1.5rem; + } + + .toolbar { + flex-direction: column; + align-items: stretch; + gap: 0.4rem; + padding: 0.6rem 1rem; + } + + .search-box { + flex: 1 1 100%; + min-width: 0; + } + + .search-box form { + flex-wrap: nowrap; + } + + .search-box input[type="text"] { + min-width: 60px; + font-size: 0.8rem; + padding: 0.4rem 0.8rem; + height: 34px; + } + + .search-box button { + font-size: 0.8rem; + padding: 0 0.9rem; + height: 34px; + } + + .settings-group { + flex-wrap: wrap; + justify-content: center; + min-height: 34px; + align-items: center; + } + + .view-btn, + .sort-btn { + font-size: 0.75rem; + padding: 0 0.7rem; + height: 34px; + } + + .settings-divider { + height: 26px; + } + + .file-controls { + gap: 0.2rem; + } + + .preview-btn, + .copy-btn, + .download-btn { + width: 28px; + height: 28px; + min-width: 28px; + min-height: 28px; + max-width: 28px; + max-height: 28px; + font-size: 0.7rem; + } + + .file-icon { + font-size: 1.2rem; + width: 24px; + } + + .file-name { + font-size: 0.85rem; + } + + li { + padding: 0.5rem 0.75rem; + } + + .file-meta.date { + min-width: 60px; + width: 60px; + } + + .file-meta.size { + min-width: 50px; + width: 50px; + } + + .breadcrumb { + padding: 0.5rem 0.75rem; + font-size: 0.8rem; + } + + .preview-content { + max-width: 96vw; + max-height: 92vh; + min-width: auto; + border-radius: 8px; + } + + #passwordFormContainer { + padding: 1.5rem; + min-height: 250px; + } + + #passwordFormContainer > div { + padding: 1.5rem; + } +} + +/* ============================================ + АДАПТИВ — МОБИЛКИ (ВСЁ УВЕЛИЧЕНО) + ============================================ */ +@media (max-width: 480px) { + .header { + padding: 1.25rem 0.75rem; + } + .header h1 { + font-size: 1.6rem; + } + .header .subtitle { + font-size: 1rem; + } + + .toolbar { + flex-direction: column; + align-items: stretch; + gap: 0.6rem; + padding: 0.75rem 0.75rem; + } + + .search-box { + width: 100%; + min-width: 0; + } + + .search-box form { + flex-wrap: nowrap; + width: 100%; + gap: 0.5rem; + } + + .search-box input[type="text"] { + font-size: 0.9rem; + padding: 0.6rem 0.8rem; + min-width: 60px; + height: 44px; + flex: 1; + } + + .search-box button { + font-size: 0.9rem; + padding: 0 1rem; + height: 44px; + flex-shrink: 0; + } + + .settings-group { + gap: 0.3rem; + flex-wrap: wrap; + justify-content: center; + min-height: 40px; + align-items: center; + } + + .settings-label { + font-size: 0.7rem; + flex-shrink: 0; + } + + .view-btn, + .sort-btn { + font-size: 0.8rem; + padding: 0 0.8rem; + height: 38px; + border-width: 1.5px; + flex-shrink: 0; + } + + .view-btn .icon, + .sort-btn .icon { + font-size: 0.85rem; + } + + /* ===== ФАЙЛОВАЯ СТРОКА — ФИКС ===== */ + .file-row { + gap: 0.3rem !important; + flex-wrap: nowrap !important; + } + + .file-link, + .dir-link { + gap: 0.3rem !important; + flex: 1 1 auto !important; + min-width: 0 !important; + } + + .file-icon { + font-size: 1.4rem !important; + width: 32px !important; + min-width: 32px !important; + flex-shrink: 0 !important; + } + + .file-name { + font-size: 0.95rem !important; + flex: 1 1 auto !important; + min-width: 0 !important; + overflow: hidden !important; + text-overflow: ellipsis !important; + white-space: nowrap !important; + } + + .file-controls { + gap: 0.3rem !important; + flex-shrink: 0 !important; + margin-left: auto !important; + } + + .file-meta.size { + font-size: 0.75rem !important; + min-width: 50px !important; + width: auto !important; + flex-shrink: 0 !important; + text-align: right !important; + } + + .file-meta.date { + display: none !important; + } + + .preview-btn, + .copy-btn, + .download-btn { + width: 38px !important; + height: 38px !important; + min-width: 38px !important; + min-height: 38px !important; + max-width: 38px !important; + max-height: 38px !important; + font-size: 0.9rem !important; + flex-shrink: 0 !important; + padding: 0 !important; + border-width: 1.5px !important; + margin: 0 !important; + border-radius: 8px !important; + } + + li { + padding: 0.6rem 0.6rem !important; + min-height: 56px !important; + } + + li.section-title { + font-size: 0.85rem; + padding: 0.5rem 0.6rem; + } + + /* ===== ХЛЕБНЫЕ КРОШКИ ===== */ + .breadcrumb { + padding: 0.6rem 0.75rem; + font-size: 0.9rem; + gap: 0.3rem; + } + .breadcrumb a { + padding: 0.2rem 0.3rem; + } + + /* ===== СТАТИСТИКА ===== */ + .stats { + font-size: 0.85rem; + padding: 0.75rem 0.6rem; + } + + /* ===== ТЕМА И СКРОЛЛ ===== */ + .theme-toggle { + top: 8px; + right: 8px; + width: 48px; + height: 48px; + font-size: 1.3rem; + } + + .scroll-top { + bottom: 20px; + right: 20px; + width: 48px; + height: 48px; + font-size: 1.3rem; + } + + /* ===== ФУТЕР (НЕ УВЕЛИЧИВАЕМ) ===== */ + .footer { + padding: 1rem 0.75rem; + font-size: 0.8rem; + } + + /* ===== МОДАЛКА ===== */ + .preview-content { + max-width: 98vw; + max-height: 95vh; + border-radius: 8px; + min-height: 150px; + } + + .preview-header { + padding: 0.6rem 0.8rem; + } + + .preview-title { + font-size: 0.9rem; + } + + .close-preview { + width: 34px; + height: 34px; + font-size: 1.2rem; + } + + .preview-image-container { + padding: 0.5rem; + } + + #passwordFormContainer { + padding: 1rem; + min-height: 200px; + } + + #passwordFormContainer > div { + padding: 1.5rem 1rem; + border-radius: 8px; + } + + /* ===== ФОРМА ПАРОЛЯ ===== */ + .password-form { + padding: 1.5rem 1rem; + } + .password-form .lock-icon { + font-size: 2.5rem; + } + .password-form h2 { + font-size: 1.2rem; + } + .password-form p { + font-size: 0.9rem; + } + .password-form input[type="password"] { + font-size: 1rem; + padding: 0.7rem 0.8rem; + height: 48px; + } + .password-form button { + font-size: 1rem; + padding: 0.7rem; + height: 48px; + } +} + +/* ============================================ + АДАПТИВ — ОЧЕНЬ МАЛЕНЬКИЕ ЭКРАНЫ + ============================================ */ +@media (max-width: 360px) { + .header h1 { + font-size: 1.3rem; + } + + .search-box input[type="text"] { + font-size: 0.8rem; + height: 40px; + padding: 0.4rem 0.6rem; + } + .search-box button { + font-size: 0.8rem; + height: 40px; + padding: 0 0.7rem; + } + + .view-btn, + .sort-btn { + font-size: 0.7rem; + padding: 0 0.5rem; + height: 34px; + } + + .file-icon { + font-size: 1.2rem !important; + width: 28px !important; + min-width: 28px !important; + } + + .file-name { + font-size: 0.85rem !important; + } + + .preview-btn, + .copy-btn, + .download-btn { + width: 34px !important; + height: 34px !important; + min-width: 34px !important; + min-height: 34px !important; + max-width: 34px !important; + max-height: 34px !important; + font-size: 0.8rem !important; + border-radius: 6px !important; + } + + .file-meta.size { + font-size: 0.65rem !important; + min-width: 40px !important; + } + + li { + padding: 0.4rem 0.4rem !important; + min-height: 48px !important; + } + + .file-row { + gap: 0.3rem !important; + } + + .theme-toggle { + width: 40px; + height: 40px; + font-size: 1.1rem; + top: 6px; + right: 6px; + } + + .scroll-top { + width: 40px; + height: 40px; + font-size: 1.1rem; + bottom: 16px; + right: 16px; + } + + .password-form input[type="password"] { + height: 42px; + font-size: 0.9rem; + } + .password-form button { + height: 42px; + font-size: 0.9rem; + } +} + +/* ===== ФАЙЛЫ — МОБИЛКИ ===== */ +.file-row { + gap: 0.3rem !important; +} + +.file-link, +.dir-link { + gap: 0.3rem !important; + flex: 1 1 auto !important; +} + +.file-icon { + font-size: 1.4rem !important; + width: 32px !important; + min-width: 32px !important; + max-width: 32px !important; + flex-shrink: 0 !important; +} + +.file-name { + font-size: 0.95rem !important; + flex: 1 1 auto !important; + min-width: 0 !important; +} + +.file-link .file-name { + font-weight: 500 !important; +} + +.dir-link .file-name { + font-weight: 600 !important; +} + +.file-controls { + gap: 0.3rem !important; + flex-shrink: 0 !important; + margin-left: auto !important; +} + +.file-meta.size { + font-size: 0.75rem !important; + min-width: 50px !important; + width: auto !important; + flex-shrink: 0 !important; +} + +.file-meta.date { + display: none !important; +} + +.preview-btn, +.copy-btn, +.download-btn { + width: 38px !important; + height: 38px !important; + min-width: 38px !important; + min-height: 38px !important; + max-width: 38px !important; + max-height: 38px !important; + font-size: 0.9rem !important; + flex-shrink: 0 !important; +} \ No newline at end of file diff --git a/www/cgi-bin/template.html b/www/cgi-bin/template.html new file mode 100644 index 0000000..8d225ae --- /dev/null +++ b/www/cgi-bin/template.html @@ -0,0 +1,546 @@ + + + + + + Trashbox - личная помоечка + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ Trashbox +
+

Trashbox

+
+ Склад всякой всячины +
+
+
+
+
+ Личный уголок для обмена файлами. Заливаю сюда то, чем хочу поделиться, без всяких лимитов и сторонних сервисов. +
+
+ + +
+
+ +
+ + +
+ Загрузка статистики... +
+ + +
+ + + +
+
+
+

Предпросмотр файла

+ +
+ +
+
+ + + + \ No newline at end of file