From c3ad5349590b10e26d07ef9d538333c373fcfac5 Mon Sep 17 00:00:00 2001 From: Nikita Osokin Date: Wed, 8 Jan 2025 17:11:26 +0500 Subject: [PATCH] =?UTF-8?q?=D0=98=D0=B7=D0=BD=D0=B0=D1=87=D0=B0=D0=BB?= =?UTF-8?q?=D1=8C=D0=BD=D1=8B=D0=B9=20=D0=BA=D0=BE=D0=BC=D0=BC=D0=B8=D1=82?= =?UTF-8?q?.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + 404.html | 33 +++++++ COPYRIGHT | 19 ++++ chpasswd.c | 267 +++++++++++++++++++++++++++++++++++++++++++++++++++ index.html | 135 ++++++++++++++++++++++++++ webserver.go | 162 +++++++++++++++++++++++++++++++ 6 files changed, 618 insertions(+) create mode 100644 .gitignore create mode 100644 404.html create mode 100644 COPYRIGHT create mode 100644 chpasswd.c create mode 100644 index.html create mode 100644 webserver.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9948418 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +chpasswd +webserver diff --git a/404.html b/404.html new file mode 100644 index 0000000..576a15f --- /dev/null +++ b/404.html @@ -0,0 +1,33 @@ + + + + + + + + +

:(

+

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

+ + + diff --git a/COPYRIGHT b/COPYRIGHT new file mode 100644 index 0000000..c11dfb8 --- /dev/null +++ b/COPYRIGHT @@ -0,0 +1,19 @@ +Copyright (c) 2025 Nikita Osokin. + +Redistribution and use in source and binary forms, with or without modification, are permitted +provided that the following conditions are met: +1. Redistributions of source code must retain the above copyright notice, this list of + conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, this list of + conditions and the following disclaimer in the documentation and/or other materials provided + with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/chpasswd.c b/chpasswd.c new file mode 100644 index 0000000..c0208dd --- /dev/null +++ b/chpasswd.c @@ -0,0 +1,267 @@ +/* Низкоуровневая программа для смены пароля на почтовый ящик @sch9.ru + * + * Copyright (c) 2025 Nikita Osokin. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted + * provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define INTERNAL_ERROR -1 +#define ILLEGAL_USERNAME -2 +#define USER_NOT_FOUND -3 +#define INCORRECT_OLD_PASSWORD -4 +#define PASSWORD_MISMATCH -5 +#define OLD_PATH_BASE 20 +#define BUF_LEN 64 * 1024 + +#define walk_to_lf(x) for(iter = (x); *iter != '\n' && *iter != '\0'; iter++) + +char creds_old_path[] = "/etc/mail/creds.old/XXXXXX"; +char buf[BUF_LEN]; + +int open_creds_old(size_t i) { + if(i == OLD_PATH_BASE + 6) { + return open(creds_old_path, O_CREAT | O_EXCL | O_WRONLY, 0640); + } + + for(char c = '0'; c <= '9'; c++) { + creds_old_path[i] = c; + + int fd = open_creds_old(i + 1); + if(fd != -1) { + return fd; + } + } + + return -1; +} + +void copy_to(FILE * restrict src, FILE * restrict dest, const char * destPath) { + size_t readen /* неправильные глаголы... */, written; + + while(!feof(src)) { + readen = fread(buf, 1, BUF_LEN, src); + written = fwrite(buf, 1, readen, dest); + if(written != readen) { + fclose(src); + fclose(dest); + remove(destPath); + + exit(INTERNAL_ERROR); + } + } +} + +void backup(FILE * restrict creds) { + int creds_old_fd = open_creds_old(OLD_PATH_BASE); + if(creds_old_fd == -1) { + fclose(creds); + + exit(INTERNAL_ERROR); + } + FILE * creds_old = fdopen(creds_old_fd, "w"); + if(creds_old == NULL) { + fclose(creds); + close(creds_old_fd); + remove(creds_old_path); + + exit(INTERNAL_ERROR); + } + + copy_to(creds, creds_old, creds_old_path); + + fclose(creds_old); + rewind(creds); +} + +char * found_user_line( + char ** restrict line, + size_t * restrict n, + FILE * restrict creds, + char * restrict username, + FILE * restrict creds_new +) { + if(getline(line, n, creds) == -1) { + fclose(creds); + fclose(creds_new); + remove("/etc/mail/creds.new"); + + exit(USER_NOT_FOUND); + } + + char * line_iter = *line; + char * username_iter = username; + + while(*line_iter != '\0' && *line_iter != ':' && *username_iter != '\0') { + if(*line_iter != *username_iter) { + fputs(*line, creds_new); + + return NULL; + } + line_iter++; + username_iter++; + } + if(*line_iter != ':' || *username_iter != '\0') { + fputs(*line, creds_new); + + return NULL; + } + + return line_iter + 1; +} + +int main(int argc, char ** argv) { + char username[2049]; + char old_password[2049]; + char password[2049]; + char password_repeat[2049]; + fgets(username, 2049, stdin); + fgets(old_password, 2049, stdin); + fgets(password, 2049, stdin); + fgets(password_repeat, 2049, stdin); + + // Очень неэффективный, но надёжный способ убрать '\n' в концах строк + char * iter; + + walk_to_lf(username) { + if(*iter == ':' || !isprint((unsigned char)*iter)) { + return ILLEGAL_USERNAME; + } else { + *iter = tolower((unsigned char)*iter); + } + } + *iter = '\0'; + + walk_to_lf(old_password); + *iter = '\0'; + + walk_to_lf(password); + *iter = '\0'; + size_t password_len = (size_t)(iter - password); + + walk_to_lf(password_repeat); + *iter = '\0'; + if(strcmp(password, password_repeat)) { + return PASSWORD_MISMATCH; + } + + + FILE * creds = fopen("/etc/mail/creds", "r"); + if(creds == NULL) { + return INTERNAL_ERROR; + } + + backup(creds); + + + // Формирование нового файла creds + FILE * creds_new = fopen("/etc/mail/creds.new", "w"); + if(creds_new == NULL) { + fclose(creds); + + return INTERNAL_ERROR; + } + + char * line = NULL; + size_t n; + char * hash_begin, * hash_iter; + + while((hash_begin = found_user_line(&line, &n, creds, username, creds_new)) == NULL); + fputs(username, creds_new); + fputc(':', creds_new); + + for(hash_iter = hash_begin; *hash_iter != ':'; hash_iter++); + *hash_iter = '\0'; // Теперь мы ограничили часть строки, которая содержит хеш старого пароля + + char * enc = crypt(old_password, hash_begin); + if(strcmp(enc, hash_begin)) { + fclose(creds); + fclose(creds_new); + remove("/etc/mail/creds.new"); + + return INCORRECT_OLD_PASSWORD; + } + + + int inpipe[2], outpipe[2], status; + size_t new_hash_len; + pipe(inpipe); + pipe(outpipe); + + pid_t pid = fork(); + if(pid == -1) { + fclose(creds); + fclose(creds_new); + remove("/etc/mail/creds.new"); + + return INTERNAL_ERROR; + } else if(pid == 0) { + dup2(inpipe[0], 0); + close(inpipe[0]); + close(inpipe[1]); + dup2(outpipe[1], 1); + close(outpipe[0]); + close(outpipe[1]); + + execl("/sbin/smtpctl", "smtpctl", "encrypt", (char *)NULL); + return INTERNAL_ERROR; + } + close(inpipe[0]); + close(outpipe[1]); + + write(inpipe[1], password, password_len); + close(inpipe[1]); + + wait(&status); + if(status) { + fclose(creds); + fclose(creds_new); + remove("/etc/mail/creds.new"); + + return INTERNAL_ERROR; + } + + new_hash_len = read(outpipe[0], buf, 64 * 1024); + close(outpipe[0]); + // Основывается на том, что последний символ вывода smtpctl encrypt - '\n' + buf[new_hash_len - 1] = '\0'; + fputs(buf, creds_new); + + + fputc(':', creds_new); + fputs(hash_iter + 1, creds_new); + copy_to(creds, creds_new, "/etc/mail/creds.new"); + + fclose(creds_new); + fclose(creds); + remove("/etc/mail/creds"); + rename("/etc/mail/creds.new", "/etc/mail/creds"); + + return 0; +} + diff --git a/index.html b/index.html new file mode 100644 index 0000000..b67d27c --- /dev/null +++ b/index.html @@ -0,0 +1,135 @@ + + + + + + + + + +
+

Смена пароля на почтовый ящик @sch9.ru

+ + + + + +
+ + + + + diff --git a/webserver.go b/webserver.go new file mode 100644 index 0000000..876b6f5 --- /dev/null +++ b/webserver.go @@ -0,0 +1,162 @@ +// Веб-сервер для смены пароля на почтовый ящик @sch9.ru +// +// Copyright (c) 2025 Nikita Osokin. +// +// Redistribution and use in source and binary forms, with or without modification, are permitted +// provided that the following conditions are met: +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of +// conditions and the following disclaimer in the documentation and/or other materials provided +// with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR +// IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +package main + +import ( + "io" + "log" + "net/http" + "os" + "os/exec" + "strconv" + "strings" + "sync" +) + +var status404_text []byte +var index_text []byte +var passwd_file_mutex sync.Mutex + +func validate_chpasswd_input(reader io.Reader) ([]byte, bool) { + buf := make([]byte, 2049 * 4) + l, _ := reader.Read(buf) + + eof_check := make([]byte, 1) + n, err := reader.Read(eof_check) + if n != 0 || err != io.EOF { + return nil, false + } + + i := 0 + lines_found := 0 + line_start := 0 + for i < l { + if buf[i] == '\n' { + if(i + 1 - line_start > 2049 || i + 1 - line_start == 0) { + return nil, false + } + + line_start = i + 1 + lines_found++ + if(lines_found == 4) { + break + } + } + + i++ + } + + if lines_found != 4 || i + 1 != l { + return nil, false + } + return buf, true +} + +func handle_request(writer http.ResponseWriter, request *http.Request) { + if request.URL.Path != "/" { + writer.WriteHeader(http.StatusNotFound) + writer.Write(status404_text) + } else { + if request.Method == http.MethodGet { + writer.Write(index_text) + } else if request.Method == http.MethodPost { + passwd_file_mutex.Lock() + + lines, could_read := validate_chpasswd_input(request.Body) + if !could_read { + writer.WriteHeader(http.StatusBadRequest) + writer.Write([]byte("250")) // -6, INVALID_INPUT + + passwd_file_mutex.Unlock() + return + } + + chpasswd := exec.Command("./chpasswd", "./chpasswd") + chpasswd.Stdin = strings.NewReader(string(lines)) + chpasswd_res := chpasswd.Run() + if chpasswd_res != nil { + writer.WriteHeader(http.StatusBadRequest) + writer.Write([]byte(strconv.Itoa(chpasswd_res.(*exec.ExitError).ExitCode()))) + + passwd_file_mutex.Unlock() + return + } + + update := exec.Command("/sbin/smtpctl", "update", "table", "creds") + update_res := update.Run() + if update_res != nil { + writer.WriteHeader(http.StatusBadRequest) + writer.Write([]byte("255")) // -1, INTERNAL_ERROR + + passwd_file_mutex.Unlock() + return + } + + writer.Write([]byte("0")) + + passwd_file_mutex.Unlock() + return + } else { + writer.WriteHeader(http.StatusNotImplemented) + } + } +} + +func handle_favicon(writer http.ResponseWriter, request *http.Request) { + writer.WriteHeader(http.StatusNotFound) +} + +func get_file_text(name string) []byte { + var err error + + file, err := os.Open(name) + if err != nil { + log.Fatal(err) + } + + file_stat, err := file.Stat() + if err != nil { + file.Close() + log.Fatal(err) + } + text := make([]byte, file_stat.Size()) + + _, err = file.Read(text); + if err != nil { + file.Close() + log.Fatal(err) + } + + file.Close() + return text +} + +func main() { + index_text = get_file_text("index.html") + status404_text = get_file_text("404.html") + + http.HandleFunc("/favicon.ico", handle_favicon) + http.HandleFunc("/", handle_request) + http.ListenAndServe(":62272", nil) +} +