// citrusNikOs' journal -- минималистичный сервер для веб-журнала с расписанием. // // Copyright (c) 2023 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 ( "fmt" "io" "os" "sync" "net/http" "time" "strconv" ) var schedFileRWMutex sync.RWMutex var classesRWMutexes map[string]*sync.RWMutex var classesNames []string func isValidClass(class string) bool { for i := 0; i < len(classesNames); i++ { if classesNames[i] == class { return true } } return false } func getCurrentDay() uint64 { return uint64(time.Now().Unix()) / 86400 } func isValidDay(day string) bool { if len(day) == 0 || (day[0] == '0' && len(day) != 1) { return false } for i := 0; i < len(day); i++ { if day[i] < '0' || day[i] > '9' { return false } } return true } func read8BytesBE(reader io.Reader, file *os.File) (uint64, error) { var result uint64 var err error var count int buffer := make([]byte, 8) count, err = reader.Read(buffer) if count != 8 { return 0, err } for i := 0; i < 8; i++ { result <<= 8 result |= uint64(buffer[i]) } _, err = (*file).Write(buffer) if err != nil { return 0, err } return result, nil } func read8BytesBENoCopy(reader io.Reader) (uint64, error) { var result uint64 var err error var count int buffer := make([]byte, 8) count, err = reader.Read(buffer) if count != 8 { return 0, err } for i := 0; i < 8; i++ { result <<= 8 result |= uint64(buffer[i]) } return result, nil } func writeUint64BE(writer io.Writer, num uint64) error { buffer := make([]byte, 8) bufferI := 0 for i := 56; i >= 0; i -= 8 { buffer[bufferI] = uint8((num >> i) & 0xFF) bufferI++ } _, err := writer.Write(buffer) if err != nil { return err } return nil } func respondWithFile(writer http.ResponseWriter, filename string) { file, err := os.Open(filename) if err != nil { writer.WriteHeader(http.StatusInternalServerError) return } stat, err := file.Stat() if err != nil { writer.WriteHeader(http.StatusInternalServerError) file.Close() return } buffer := make([]byte, stat.Size()) _, err = file.Read(buffer); if err != nil { writer.WriteHeader(http.StatusInternalServerError) file.Close() return } writer.Write(buffer) file.Close() } func handleRootAnd404(writer http.ResponseWriter, request *http.Request) { if request.URL.Path != "/" { writer.WriteHeader(http.StatusNotFound) respondWithFile(writer, "404.html") } else { respondWithFile(writer, "index.html") } } func handleGetClassesList(writer http.ResponseWriter, request *http.Request) { err := writeUint64BE(writer, uint64(len(classesNames))) if err != nil { writer.WriteHeader(http.StatusInternalServerError) } for i := 0; i < len(classesNames); i++ { err = writeUint64BE(writer, uint64(len(classesNames[i]))) if err != nil { writer.WriteHeader(http.StatusInternalServerError) } _, err = writer.Write([]uint8(classesNames[i])) if err != nil { writer.WriteHeader(http.StatusInternalServerError) } } } func handleEditPage(writer http.ResponseWriter, request *http.Request) { if request.Method == http.MethodPost { queryValues := request.URL.Query() if len(queryValues["c"]) == 0 || !isValidClass(queryValues["c"][0]) { writer.WriteHeader(http.StatusBadRequest) return } var day string if len(queryValues["d"]) == 0 || !isValidDay(queryValues["d"][0]) { day = strconv.FormatUint(getCurrentDay(), 10) } else { day = queryValues["d"][0] } passwordLen, err := read8BytesBENoCopy(request.Body); if err != nil { writer.WriteHeader(http.StatusBadRequest) return } passwordFile, err := os.Open("classes/" + queryValues["c"][0] + "/password") if err != nil { writer.WriteHeader(http.StatusInternalServerError) return } passwordFileInfo, err := passwordFile.Stat() if err != nil { passwordFile.Close() writer.WriteHeader(http.StatusInternalServerError) return } if uint64(passwordFileInfo.Size() - 1) != passwordLen { passwordFile.Close() writer.WriteHeader(http.StatusUnauthorized) return } userPassword := make([]byte, passwordLen) passwordCount, err := request.Body.Read(userPassword) if uint64(passwordCount) != passwordLen || err != nil { passwordFile.Close() writer.WriteHeader(http.StatusBadRequest) return } password := make([]byte, passwordLen) passwordCount, err = passwordFile.Read(password) if uint64(passwordCount) != passwordLen || err != nil { passwordFile.Close() writer.WriteHeader(http.StatusInternalServerError) return } for i := uint64(0); i < passwordLen; i++ { if password[i] != userPassword[i] { passwordFile.Close() writer.WriteHeader(http.StatusUnauthorized) return } } tmpSchedFile, err := os.CreateTemp("", "sched") if err != nil { writer.WriteHeader(http.StatusBadRequest) return } layoutID, err := read8BytesBE(request.Body, tmpSchedFile) if err != nil || layoutID != 0 { writer.WriteHeader(http.StatusBadRequest) os.Remove(tmpSchedFile.Name()) return } lessonsCount, err := read8BytesBE(request.Body, tmpSchedFile) if err != nil || lessonsCount > 7 { writer.WriteHeader(http.StatusBadRequest) os.Remove((*tmpSchedFile).Name()) return } buffer := make([]byte, 1) lastNumber := uint64(0) isFirst := true for i := uint64(0); i < lessonsCount; i++ { number, err := read8BytesBE(request.Body, tmpSchedFile) if err != nil { writer.WriteHeader(http.StatusBadRequest) os.Remove((*tmpSchedFile).Name()) return } if !isFirst && number <= lastNumber { writer.WriteHeader(http.StatusBadRequest) os.Remove((*tmpSchedFile).Name()) return } isFirst = false lastNumber = number nameLen, err := read8BytesBE(request.Body, tmpSchedFile) if err != nil { writer.WriteHeader(http.StatusBadRequest) os.Remove((*tmpSchedFile).Name()) return } for j := uint64(0); j < nameLen; j++ { beenRead, _ := request.Body.Read(buffer) _, err := (*tmpSchedFile).Write(buffer) if beenRead == 0 || err != nil { writer.WriteHeader(http.StatusBadRequest) os.Remove((*tmpSchedFile).Name()) return } } descrLen, err := read8BytesBE(request.Body, tmpSchedFile) if err != nil { writer.WriteHeader(http.StatusBadRequest) os.Remove((*tmpSchedFile).Name()) return } for j := uint64(0); j < descrLen; j++ { beenRead, _ := request.Body.Read(buffer) _, err := (*tmpSchedFile).Write(buffer) if beenRead == 0 || err != nil { writer.WriteHeader(http.StatusBadRequest) os.Remove((*tmpSchedFile).Name()) return } } } beenRead, err := request.Body.Read(buffer) if beenRead != 0 { writer.WriteHeader(http.StatusBadRequest) os.Remove((*tmpSchedFile).Name()) return } classesRWMutexes[queryValues["c"][0]].Lock() schedFileName := "classes/" + queryValues["c"][0] + "/" + day os.Remove(schedFileName) os.Rename((*tmpSchedFile).Name(), schedFileName) classesRWMutexes[queryValues["c"][0]].Unlock() } else { respondWithFile(writer, "editpage.html") } } /* TODO: в эту и похожие функции нужно добавть возможность отправить статус 500 */ func handleSchedule(writer http.ResponseWriter, request *http.Request) { respondWithFile(writer, "schedule.html") } /* TODO: возможно стоит отправлять что-то кроме просто ошибки 500 */ func handleScheduleGet(writer http.ResponseWriter, request *http.Request) { queryValues := request.URL.Query() if len(queryValues["c"]) == 0 || !isValidClass(queryValues["c"][0]) { writer.WriteHeader(http.StatusBadRequest) return } var day string if len(queryValues["d"]) == 0 || !isValidDay(queryValues["d"][0]) { day = strconv.FormatUint(getCurrentDay(), 10) } else { day = queryValues["d"][0] } classesRWMutexes[queryValues["c"][0]].RLock() defer classesRWMutexes[queryValues["c"][0]].RUnlock() i := 0 var schedFile *os.File var err error schedFileName := "classes/" + queryValues["c"][0] + "/" + day for schedFile, err = os.Open(schedFileName); err != nil; schedFile, err = os.Open(schedFileName) { if i == 1000 { return } i++ } _, err = io.Copy(writer, schedFile) if err != nil { writer.WriteHeader(http.StatusInternalServerError) } schedFile.Close() } func main() { classesRWMutexes = make(map[string]*sync.RWMutex) var classesDir *os.File var classes []os.DirEntry var err error classesDir, err = os.Open("classes") if err != nil { fmt.Printf("[!!!]Не удалось открыть директорию classes.\n") return } classes, err = classesDir.ReadDir(0) classesDir.Close() if err != nil { fmt.Printf("[!!!]Не удалось прочитать содержимое директории classes.\n") return } classesNames = make([]string, len(classes)) for i := 0; i < len(classes); i++ { if !classes[i].IsDir() { fmt.Printf("[!!!]Директория classes должна содержать только директории.\n") return } classesNames[i] = classes[i].Name() classesRWMutexes[classes[i].Name()] = new(sync.RWMutex) } classes = nil http.HandleFunc("/", handleRootAnd404) http.HandleFunc("/getClassesList", handleGetClassesList) http.HandleFunc("/schedule", handleSchedule) http.HandleFunc("/scheduleGet", handleScheduleGet) http.HandleFunc("/editpage", handleEditPage) http.ListenAndServe(":62314", nil) }