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)
|
||
}
|
||
|