412 lines
13 KiB
Go
412 lines
13 KiB
Go
|
// 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)
|
|||
|
}
|
|||
|
|