// Веб-сервер для смены пароля на почтовый ящик @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 == 1) { 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) }