From 1f6742762c2bcf6f0b7b0a64169aba327e4d1092 Mon Sep 17 00:00:00 2001 From: uan Date: Sun, 3 Aug 2025 15:04:16 +0200 Subject: [PATCH] divided in different files --- logger.go | 62 +++++++++++++ main.go | 242 +++------------------------------------------------ utils.go | 21 +++++ webserver.go | 172 ++++++++++++++++++++++++++++++++++++ 4 files changed, 265 insertions(+), 232 deletions(-) create mode 100644 logger.go create mode 100644 utils.go create mode 100644 webserver.go diff --git a/logger.go b/logger.go new file mode 100644 index 0000000..354df40 --- /dev/null +++ b/logger.go @@ -0,0 +1,62 @@ +package main + +import ( + "fmt" + "log" + "os" +) + +const ( + FLAGS_LOG_OPEN int = os.O_APPEND | os.O_WRONLY | os.O_CREATE + FLAGS_CONFIG_OPEN int = os.O_RDONLY | os.O_CREATE + PERMS_LOG_OPEN os.FileMode = os.ModeType | os.ModePerm + PERMS_CONFIG_OPEN os.FileMode = os.ModeType | os.ModePerm + PERMS_MKDIR os.FileMode = os.ModeDir | os.ModePerm +) + +type Logger struct { + logFolder string +} + +func NewLogger(logFolder_ string) *Logger { + return &Logger{ + logFolder: logFolder_, + } +} + +// returns error != nil +func (l *Logger) logIfError(err error) bool { + if err != nil { + l.logError(err.Error()) + return true + } + return false +} + +func (l *Logger) logAccess( + remoteAddr, identifier, authuser, timestamp, request, + status, bytesSent, referer, user_agent string, +) { + out := fmt.Sprintf("%v %v %v [%v] \"%v\" %v %v \"%v\" \"%v\"\n", + remoteAddr, identifier, authuser, timestamp, request, status, bytesSent, referer, user_agent, + ) + os.Mkdir(l.logFolder, os.ModeDir|os.ModePerm) + file, err := os.OpenFile(ensureSlashSuffix(l.logFolder)+FILENAME_ACCESSLOG, FLAGS_LOG_OPEN, PERMS_LOG_OPEN) + + if err != nil { + log.Println("couldn't open log access file at", ensureSlashSuffix(l.logFolder)+FILENAME_ACCESSLOG) + } + defer file.Close() + file.WriteString(out) +} + +func (l *Logger) logError(str string) { + os.Mkdir(l.logFolder, PERMS_MKDIR) + file, err := os.OpenFile(ensureSlashSuffix(l.logFolder)+FILENAME_ERRORLOG, FLAGS_LOG_OPEN, PERMS_LOG_OPEN) + + if err != nil { + log.Println("couldn't open log error file at", ensureSlashSuffix(l.logFolder)+FILENAME_ERRORLOG) + } + defer file.Close() + file.WriteString(str + "\n") +} diff --git a/main.go b/main.go index cfa30bf..f5df94b 100644 --- a/main.go +++ b/main.go @@ -1,48 +1,26 @@ package main import ( - "errors" "fmt" - "log" - "net/http" "os" - "slices" - "strconv" - "strings" - "time" "github.com/akamensky/argparse" ) const ( - PATH_ACCESSLOG string = "access.log" - PATH_ERRORLOG string = "error.log" - PATH_IPFILTER string = "ipfilter.conf" - PATH_USERAGENTFILTER string = "useragentfilter.conf" - FLAGS_LOG_OPEN int = os.O_APPEND | os.O_WRONLY | os.O_CREATE - FLAGS_CONFIG_OPEN int = os.O_RDONLY | os.O_CREATE - PERMS_LOG_OPEN os.FileMode = os.ModeType | os.ModePerm - PERMS_CONFIG_OPEN os.FileMode = os.ModeType | os.ModePerm - PERMS_MKDIR os.FileMode = os.ModeDir | os.ModePerm - FILTER_MODE_WHITELIST = 0 - FILTER_MODE_BLACKLIST = 1 -) - -var ( - logFolder string = "" - configFolder string = "" - ipFilter []string = make([]string, 0) - ipFilterMode = FILTER_MODE_WHITELIST - userAgentFilter []string = make([]string, 0) - userAgentFilterMode = FILTER_MODE_WHITELIST + FILENAME_ACCESSLOG string = "access.log" + FILENAME_ERRORLOG string = "error.log" + FILENAME_IPFILTER string = "ipfilter.conf" + FILENAME_USERAGENTFILTER string = "useragentfilter.conf" ) func main() { parser := argparse.NewParser("miniws", "") - port := parser.String("p", "port", &argparse.Options{Default: "8040"}) - _logFolder := parser.String("l", "logs-folder", &argparse.Options{Default: "logs"}) - _configFolder := parser.String("c", "config-folder", &argparse.Options{Default: "config"}) + + port := parser.Int("p", "port", &argparse.Options{Default: 8040}) + logFolder := parser.String("l", "logs-folder", &argparse.Options{Default: "logs"}) + configFolder := parser.String("c", "config-folder", &argparse.Options{Default: "config"}) err := parser.Parse(os.Args) if err != nil { @@ -52,207 +30,7 @@ func main() { return } - logFolder = *_logFolder - configFolder = *_configFolder - - parseIpFilter() - parseUserAgentFilter() - http.HandleFunc("/", get) - log.Println("Server started on port " + *port) - http.ListenAndServe(":"+*port, nil) -} - -func get(writer http.ResponseWriter, req *http.Request) { - - respStatusCode := http.StatusOK - - if !isIpValid(req.RemoteAddr) || !isUserAgentValid(req.UserAgent()) { - writer.WriteHeader(http.StatusForbidden) - return - } - - fetchedData, fetchErr := fetchFileContents(req.URL.Path) - - sentBytes := 0 - - if logIfError(fetchErr) { - respStatusCode = http.StatusNotFound - writer.WriteHeader(respStatusCode) - } else { - sentBytesCount, _ := writer.Write(fetchedData) - sentBytes = sentBytesCount - } - - logAccess( - strings.Split(req.RemoteAddr, ":")[0], //remote address - "-", //identifier (can't get) - getOrDash(req.URL.User.Username()), //username - time.Now().Format("02/Jan/2006:15:04:05 -0700"), //timestamp - req.Method+" "+req.URL.Path+" "+getHttpVersionString(req.ProtoMajor, req.ProtoMinor), //HTTP version - strconv.Itoa(respStatusCode), //response code - strconv.Itoa(sentBytes), //# of sent bytes - req.Referer(), //Referer - req.UserAgent(), //User Agent - ) -} - -func fetchFileContents(filepath string) ([]byte, error) { - if filepath == "/" { - filepath = "." - } else { - filepath_relative, _ := strings.CutPrefix(filepath, "/") - filepath = filepath_relative - } - fileinfo, err := os.Stat(filepath) - if err != nil { - return nil, err - } - if fileinfo.IsDir() { - filepath += "/index.html" - } - return os.ReadFile(filepath) + webserver := NewWebServer(*port, *logFolder, *configFolder) + webserver.Run() } - -// returns error != nil -func logIfError(err error) bool { - if err != nil { - logError(err.Error()) - return true - } - return false -} - -func logAccess( - remoteAddr, identifier, authuser, timestamp, request, - status, bytesSent, referer, user_agent string, -) { - out := fmt.Sprintf("%v %v %v [%v] \"%v\" %v %v \"%v\" \"%v\"\n", - remoteAddr, identifier, authuser, timestamp, request, status, bytesSent, referer, user_agent, - ) - os.Mkdir(logFolder, os.ModeDir|os.ModePerm) - file, err := os.OpenFile(ensureSlashSuffix(logFolder)+PATH_ACCESSLOG, FLAGS_LOG_OPEN, PERMS_LOG_OPEN) - - if err != nil { - log.Println("couldn't open log access file at", ensureSlashSuffix(logFolder)+PATH_ACCESSLOG) - } - defer file.Close() - file.WriteString(out) -} - -func logError(str string) { - os.Mkdir(logFolder, PERMS_MKDIR) - file, err := os.OpenFile(ensureSlashSuffix(logFolder)+PATH_ERRORLOG, FLAGS_LOG_OPEN, PERMS_LOG_OPEN) - - if err != nil { - log.Println("couldn't open log error file at", ensureSlashSuffix(logFolder)+PATH_ERRORLOG) - } - defer file.Close() - file.WriteString(str + "\n") -} - -func getHttpVersionString(major, minor int) string { - return "HTTP/" + strconv.Itoa(major) + "." + strconv.Itoa(minor) -} - -func getOrDash(str string) string { - if str == "" { - return "-" - } - return str -} - -func ensureSlashSuffix(str string) string { - return strings.TrimSuffix(str, "/") + "/" -} - -func isIpValid(ip string) bool { - ip = strings.Split(ip, ":")[0] // remove port - switch ipFilterMode { - case FILTER_MODE_WHITELIST: - return slices.Contains(ipFilter, ip) - case FILTER_MODE_BLACKLIST: - return !slices.Contains(ipFilter, ip) - default: - return false - } -} - -func isUserAgentValid(userAgent string) bool { - switch userAgentFilterMode { - case FILTER_MODE_WHITELIST: - return slices.Contains(userAgentFilter, userAgent) - case FILTER_MODE_BLACKLIST: - return !slices.Contains(userAgentFilter, userAgent) - default: - return false - } -} - -func parseIpFilter() { - os.Mkdir(configFolder, PERMS_MKDIR) - fileinfo, err := os.Stat(ensureSlashSuffix(configFolder) + PATH_IPFILTER) - - if errors.Is(err, os.ErrNotExist) { - os.Create(ensureSlashSuffix(configFolder) + PATH_IPFILTER) - } - if fileinfo.Size() == 0 { // empty config - ipFilterMode = FILTER_MODE_BLACKLIST - ipFilter = make([]string, 0) - return - } - ipFilterContent, err := os.ReadFile(ensureSlashSuffix(configFolder) + PATH_IPFILTER) - - if logIfError(err) { - return - } - - ipFilterLines := strings.Split(string(ipFilterContent), "\n") - - _ipFilterMode := ipFilterLines[0] - - switch _ipFilterMode { - case "allow": - ipFilterMode = FILTER_MODE_WHITELIST - case "deny": - ipFilterMode = FILTER_MODE_BLACKLIST - default: - logError("invalid ip filter mode, use allow|deny") - } - - ipFilter = ipFilterLines[1:] - -} - -func parseUserAgentFilter() { - os.Mkdir(configFolder, PERMS_MKDIR) - fileinfo, err := os.Stat(ensureSlashSuffix(configFolder) + PATH_USERAGENTFILTER) - if errors.Is(err, os.ErrNotExist) { - os.Create(ensureSlashSuffix(configFolder) + PATH_USERAGENTFILTER) - } - if fileinfo.Size() == 0 { // empty config - userAgentFilterMode = FILTER_MODE_BLACKLIST - userAgentFilter = make([]string, 0) - return - } - - userAgentFilterContent, err := os.ReadFile(ensureSlashSuffix(configFolder) + PATH_USERAGENTFILTER) - - if logIfError(err) { - return - } - - userAgentFilterLines := strings.Split(string(userAgentFilterContent), "\n") - _userAgentFilterMode := userAgentFilterLines[0] - - println(len(userAgentFilterLines)) - switch _userAgentFilterMode { - case "allow": - userAgentFilterMode = FILTER_MODE_WHITELIST - case "deny": - userAgentFilterMode = FILTER_MODE_BLACKLIST - default: - logError("invalid userAgent filter mode, use allow|deny") - } - userAgentFilter = userAgentFilterLines[1:] -} diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..6a10ef0 --- /dev/null +++ b/utils.go @@ -0,0 +1,21 @@ +package main + +import ( + "strconv" + "strings" +) + +func getHttpVersionString(major, minor int) string { + return "HTTP/" + strconv.Itoa(major) + "." + strconv.Itoa(minor) +} + +func getOrDash(str string) string { + if str == "" { + return "-" + } + return str +} + +func ensureSlashSuffix(str string) string { + return strings.TrimSuffix(str, "/") + "/" +} diff --git a/webserver.go b/webserver.go new file mode 100644 index 0000000..f081391 --- /dev/null +++ b/webserver.go @@ -0,0 +1,172 @@ +package main + +import ( + "errors" + "log" + "net/http" + "os" + "slices" + "strconv" + "strings" + "time" +) + +const ( + FILTER_MODE_WHITELIST FilterMode = 0 + FILTER_MODE_BLACKLIST FilterMode = 1 +) + +type FilterMode int + +type WebServer struct { + logger *Logger + port int + configFolder string + ipFilter []string + userAgentFilter []string + ipFilterMode FilterMode + userAgentFilterMode FilterMode +} + +func NewWebServer(port_ int, logFolder_, configFolder_ string) *WebServer { + + return &WebServer{ + logger: NewLogger(logFolder_), + port: port_, + configFolder: configFolder_, + ipFilter: make([]string, 0), + userAgentFilter: make([]string, 0), + ipFilterMode: FILTER_MODE_BLACKLIST, + userAgentFilterMode: FILTER_MODE_BLACKLIST, + } +} + +func (ws *WebServer) Run() { + + ws.ipFilterMode, ws.ipFilter = ws.parseFilterPanics(FILENAME_IPFILTER) + ws.userAgentFilterMode, ws.userAgentFilter = ws.parseFilterPanics(FILENAME_USERAGENTFILTER) + + http.HandleFunc("/", ws.get) + log.Println("Server started on port " + strconv.Itoa(ws.port)) + http.ListenAndServe(":"+strconv.Itoa(ws.port), nil) +} + +func (ws *WebServer) parseFilterPanics(filename string) (FilterMode, []string) { + + filterMode := FILTER_MODE_BLACKLIST + filter := make([]string, 0) + + os.Mkdir(ws.configFolder, PERMS_MKDIR) + fileinfo, err := os.Stat(ensureSlashSuffix(ws.configFolder) + filename) + + if errors.Is(err, os.ErrNotExist) { + os.Create(ensureSlashSuffix(ws.configFolder) + filename) + fileinfo, err = os.Stat(ensureSlashSuffix(ws.configFolder) + filename) + } + + if err != nil { + panic("Error opening " + filename + ": " + err.Error()) + } + + if fileinfo.Size() == 0 { // empty config + return filterMode, filter + } + + filterContent, err := os.ReadFile(ensureSlashSuffix(ws.configFolder) + filename) + + if ws.logger.logIfError(err) { + panic("Error reading " + filename + ": " + err.Error()) + } + + filterLines := strings.Split(string(filterContent), "\n") + readFilterMode := filterLines[0] + + switch readFilterMode { + case "allow": + filterMode = FILTER_MODE_WHITELIST + case "deny": + filterMode = FILTER_MODE_BLACKLIST + default: + panic("invalid filter mode for " + filename + ": use allow|deny") + } + + filter = filterLines[1:] + + return filterMode, filter + +} + +func (ws *WebServer) isIpValid(ip string) bool { + ip = strings.Split(ip, ":")[0] // remove port + switch ws.ipFilterMode { + case FILTER_MODE_WHITELIST: + return slices.Contains(ws.ipFilter, ip) + case FILTER_MODE_BLACKLIST: + return !slices.Contains(ws.ipFilter, ip) + default: + return false + } +} + +func (ws *WebServer) isUserAgentValid(userAgent string) bool { + switch ws.userAgentFilterMode { + case FILTER_MODE_WHITELIST: + return slices.Contains(ws.userAgentFilter, userAgent) + case FILTER_MODE_BLACKLIST: + return !slices.Contains(ws.userAgentFilter, userAgent) + default: + return false + } +} + +func (ws *WebServer) fetchFileContents(filepath string) ([]byte, error) { + if filepath == "/" { + filepath = "." + } else { + filepath_relative, _ := strings.CutPrefix(filepath, "/") + filepath = filepath_relative + } + fileinfo, err := os.Stat(filepath) + if err != nil { + return nil, err + } + if fileinfo.IsDir() { + filepath += "/index.html" + } + return os.ReadFile(filepath) + +} + +func (ws *WebServer) get(writer http.ResponseWriter, req *http.Request) { + + respStatusCode := http.StatusOK + + if !ws.isIpValid(req.RemoteAddr) || !ws.isUserAgentValid(req.UserAgent()) { + writer.WriteHeader(http.StatusForbidden) + return + } + + fetchedData, fetchErr := ws.fetchFileContents(req.URL.Path) + + sentBytes := 0 + + if ws.logger.logIfError(fetchErr) { + respStatusCode = http.StatusNotFound + writer.WriteHeader(respStatusCode) + } else { + sentBytesCount, _ := writer.Write(fetchedData) + sentBytes = sentBytesCount + } + + ws.logger.logAccess( + strings.Split(req.RemoteAddr, ":")[0], //remote address + "-", //identifier (can't get) + getOrDash(req.URL.User.Username()), //username + time.Now().Format("02/Jan/2006:15:04:05 -0700"), //timestamp + req.Method+" "+req.URL.Path+" "+getHttpVersionString(req.ProtoMajor, req.ProtoMinor), //HTTP version + strconv.Itoa(respStatusCode), //response code + strconv.Itoa(sentBytes), //# of sent bytes + req.Referer(), //Referer + req.UserAgent(), //User Agent + ) +}