commit f8b8e8c02bb7058101273f5c1977266e23ca9ef9 Author: Nikita Osokin Date: Thu Nov 9 23:11:16 2023 +0500 Первый коммит. diff --git a/404.html b/404.html new file mode 100644 index 0000000..f683e66 --- /dev/null +++ b/404.html @@ -0,0 +1,34 @@ + + + + + + + + +

:(

+

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

+ + + diff --git a/COPYRIGHT b/COPYRIGHT new file mode 100644 index 0000000..3e8dfa4 --- /dev/null +++ b/COPYRIGHT @@ -0,0 +1,20 @@ +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. diff --git a/editpage.html b/editpage.html new file mode 100644 index 0000000..77bec62 --- /dev/null +++ b/editpage.html @@ -0,0 +1,247 @@ + + + + + + + + + +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ 1. + + + + + + +
+ 2. + + + + + + +
+ 3. + + + + + + +
+ 4. + + + + + + +
+ 5. + + + + + + +
+ 6. + + + + + + +
+ 7. + + + + + + +
+ +

+ + + + + + + diff --git a/index.html b/index.html new file mode 100644 index 0000000..fca4595 --- /dev/null +++ b/index.html @@ -0,0 +1,123 @@ + + + + + + + + +

Добро пожаловать!

+

Здравствуй, клиент!

+ +

Список классов:

+ + + + +
+ + + + + diff --git a/main.go b/main.go new file mode 100644 index 0000000..829dea5 --- /dev/null +++ b/main.go @@ -0,0 +1,411 @@ +// 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, request *http.Request, filename string) { + file, err := os.Open(filename) + if err != nil { + fmt.Println(" !!! Файл", filename, "не найден!") + return + } + + stat, err := file.Stat() + if err != nil { + fmt.Println(" !!! Не удалось получить информацию о файле", filename + "!") + return + } + buffer := make([]byte, stat.Size()) + + count, err := file.Read(buffer); + if err != nil { + fmt.Println(" !!! Не удалось прочитать содержимое файла", filename + "!") + return + } + if int64(count) != stat.Size() { + fmt.Println(" *** Файл", filename, "был прочитан не полностью.") + } + + writer.Write(buffer) +} + +func handleRootAnd404(writer http.ResponseWriter, request *http.Request) { + if request.URL.Path != "/" { + writer.WriteHeader(http.StatusNotFound) + respondWithFile(writer, request, "404.html") + } else { + respondWithFile(writer, request, "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, request, "editpage.html") + } +} +/* TODO: в эту и похожие функции нужно добавть возможность отправить статус 500 */ +func handleSchedule(writer http.ResponseWriter, request *http.Request) { + respondWithFile(writer, request, "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) + 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) + } + + http.HandleFunc("/", handleRootAnd404) + http.HandleFunc("/getClassesList", handleGetClassesList) + http.HandleFunc("/schedule", handleSchedule) + http.HandleFunc("/scheduleGet", handleScheduleGet) + http.HandleFunc("/editpage", handleEditPage) + http.ListenAndServe(":62314", nil) +} + diff --git a/schedule.html b/schedule.html new file mode 100644 index 0000000..3387a17 --- /dev/null +++ b/schedule.html @@ -0,0 +1,126 @@ + + + + + + + + + + +

+ + + + +
+ + + + +