From 77e4a2b6d11d33d06e0b16c3536bf61d547948e9 Mon Sep 17 00:00:00 2001 From: jbergner Date: Sat, 12 Jul 2025 23:09:54 +0200 Subject: [PATCH] Init and optimizations --- go.mod | 5 + go.sum | 2 + main.go | 221 ++++++++++++++++++++ racc_common/racc_common.go | 22 ++ racc_filebrowser/racc_filebrowser.go | 150 ++++++++++++++ racc_httpstatus/racc_httpstatus.go | 117 +++++++++++ racc_logging/racc_logging.go | 58 ++++++ racc_middleware/racc_middleware.go | 15 ++ racc_session/racc_session.go | 101 ++++++++++ racc_sysauth/racc_sysauth.go | 289 +++++++++++++++++++++++++++ racc_template/racc_template.go | 81 ++++++++ 11 files changed, 1061 insertions(+) create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 racc_common/racc_common.go create mode 100644 racc_filebrowser/racc_filebrowser.go create mode 100644 racc_httpstatus/racc_httpstatus.go create mode 100644 racc_logging/racc_logging.go create mode 100644 racc_middleware/racc_middleware.go create mode 100644 racc_session/racc_session.go create mode 100644 racc_sysauth/racc_sysauth.go create mode 100644 racc_template/racc_template.go diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..61e67cd --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module git.send.nrw/sendnrw/raccws + +go 1.24.4 + +require github.com/google/uuid v1.6.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7790d7c --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/main.go b/main.go new file mode 100644 index 0000000..c65f3ae --- /dev/null +++ b/main.go @@ -0,0 +1,221 @@ +package main + +import ( + "encoding/json" + "fmt" + "net/http" + "os" + "os/signal" + "syscall" + + "git.send.nrw/sendnrw/raccws/racc_common" + "git.send.nrw/sendnrw/raccws/racc_filebrowser" + "git.send.nrw/sendnrw/raccws/racc_httpstatus" + "git.send.nrw/sendnrw/raccws/racc_logging" + "git.send.nrw/sendnrw/raccws/racc_middleware" + "git.send.nrw/sendnrw/raccws/racc_session" + "git.send.nrw/sendnrw/raccws/racc_sysauth" + "git.send.nrw/sendnrw/raccws/racc_template" +) + +var DefaultAppPath string = "/" +var DefaultLogNumber int = 500 + +var CORE_SESSIONFILE string = "serversessions.json" +var CORE_SERVERLOG string = "serverlog.json" +var CORE_RELPATH string = "./" + +func DefaultRoute(w http.ResponseWriter, r *http.Request) { + //fmt.Println(r.URL.Path) + if r.URL.Path == "/" { + http.Redirect(w, r, DefaultAppPath, http.StatusMovedPermanently) + } + fileInfo, err := os.Stat(CORE_RELPATH + r.URL.Path) + if os.IsNotExist(err) { + racc_httpstatus.Default404(w, r) + return + } + if !fileInfo.IsDir() { + http.ServeFile(w, r, CORE_RELPATH+r.URL.Path) + return + } else { + racc_httpstatus.Default404(w, r) + return + } + +} + +func prepareExit() { + fmt.Println("Running exit tasks...") + // Hier können Sie Ihre Aufräumarbeiten ausführen + SaveFile(CORE_SESSIONFILE, &racc_session.SessionStore) + SaveFile(CORE_SERVERLOG, racc_logging.GetLastXitems(DefaultLogNumber)) + fmt.Println("Exit completed.") +} + +func main() { + /* */ + //app_adinfo.MakeDefault() + /* */ + /*server_port := flag.String("port", "8080", "Port to be used for http server") + server_mode := flag.String("mode", "http", "Security mode the server should run (http/https)") + server_tls_key := flag.String("key", "private.key", "Private key file") + server_tls_cert := flag.String("cert", "public.crt", "Public cert file") + flag.Parse()*/ + + HTTP_PORT := racc_common.GetENV("HTTP_PORT", "8080") + HTTP_TLS := racc_common.GetENV("HTTP_TLS", "0") + HTTP_TLS_PRIVATEKEY := racc_common.GetENV("HTTP_TLS_PRIVATEKEY", "") + HTTP_TLS_CERTIFICATE := racc_common.GetENV("HTTP_TLS_CERTIFICATE", "") + + CORE_SESSIONFILE = racc_common.GetENV("CORE_SESSIONFILE", "serversessions.json") + CORE_SERVERLOG = racc_common.GetENV("CORE_SERVERLOG", "serverlog.json") + CORE_RELPATH = racc_common.GetENV("CORE_RELPATH", "./") + + /* Print actual settings */ + fmt.Println("----------") + fmt.Println("Service configured with settings:") + fmt.Println("----------") + fmt.Println("HTTP-PORT: "+HTTP_PORT, "HTTP_TLS: "+HTTP_TLS, "HTTP_TLS_PRIVATEKEY: "+HTTP_TLS_PRIVATEKEY, "HTTP_TLS_CERTIFICATE: "+HTTP_TLS_CERTIFICATE) + fmt.Println("CORE_SESSIONFILE: "+CORE_SESSIONFILE, "CORE_SERVERLOG: "+CORE_SERVERLOG, "CORE_RELPATH: "+CORE_RELPATH) + fmt.Println("----------") + + // Signal-Kanal einrichten + stop := make(chan os.Signal, 1) + signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM) + + // Goroutine, die auf Signale wartet + go func() { + <-stop + fmt.Println("Received stop signal") + prepareExit() + os.Exit(0) + }() + + // Hauptprogramm + fmt.Println("Program is running. Press Ctrl+C to exit.") + + /* Load Infrastructure */ + /* Load Server-Log */ + LoadFile(CORE_SERVERLOG, &racc_logging.LogBook) + /* Load Session-Data */ + LoadFile(CORE_SESSIONFILE, &racc_session.SessionStore) + + /* Router-Definition */ + mainRouter := http.NewServeMux() + defaultRouter := http.NewServeMux() + apiRouter := http.NewServeMux() + portalRouter := http.NewServeMux() + //adminRouter := http.NewServeMux() + userRouter := http.NewServeMux() + + /* Main-Router-Middleware */ + mainMiddlewareStack := racc_middleware.CreateStack( + racc_session.SessionMiddleware, + ) + + /* Admin-Router-Middleware */ + defaultMiddlewareStack := racc_middleware.CreateStack(racc_logging.Logging) + apiMiddlewareStack := racc_middleware.CreateStack(racc_logging.Logging, racc_sysauth.BearerAuthMiddleware("auth_bearer.json")) + portalMiddlewareStack := racc_middleware.CreateStack(racc_logging.Logging, racc_template.RenderApplicationTemplate("html-templates/application_1.html", nil)) + //adminMiddlewareStack := racc_middleware.CreateStack(racc_logging.Logging, racc_sysauth.BasicAuthMiddleware("auth_basic_admin.json")) + userMiddlewareStack := racc_middleware.CreateStack(racc_logging.Logging) + //appDemoMiddlewareStack := racc_middleware.CreateStack(racc_logging.Logging) + appFileMiddlewareStack := racc_middleware.CreateStack(racc_logging.Logging) + //appDynDNSMiddlewareStack := racc_middleware.CreateStack(racc_logging.Logging) + + /* Main-Router-Routen */ + mainRouter.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static")))) + mainRouter.Handle("/api/", apiMiddlewareStack(http.StripPrefix("/api", apiRouter))) + mainRouter.Handle("/portal/", portalMiddlewareStack(http.StripPrefix("/portal", portalRouter))) + //mainRouter.Handle("/admin/", adminMiddlewareStack(http.StripPrefix("/admin", app_admin.Main()))) + mainRouter.Handle("/user/", userMiddlewareStack(http.StripPrefix("/user", userRouter))) + //mainRouter.Handle("/demo/", appDemoMiddlewareStack(http.StripPrefix("/demo", app_demo.Main()))) + mainRouter.Handle("/files/", appFileMiddlewareStack(http.StripPrefix("/files", racc_filebrowser.Router()))) + //mainRouter.Handle("/dns/", appDynDNSMiddlewareStack(http.StripPrefix("/dns", app_dyndns.Main()))) + + /* Default-Route (Should stay here forever) */ + mainRouter.Handle("/", defaultMiddlewareStack(http.StripPrefix("", defaultRouter))) + defaultRouter.HandleFunc("/", DefaultRoute) + + /* Sub-Router-Routen */ + //adminRouter.HandleFunc("/sessions", app_admin.adminSessionHandler) + apiRouter.HandleFunc("/", HttpSaveFile(CORE_SERVERLOG, &racc_logging.LogBook)) + /* */ + server := http.Server{ + Addr: ":" + HTTP_PORT, + Handler: mainMiddlewareStack(mainRouter), + } + + /* */ + fmt.Println("Server listening on port :" + HTTP_PORT) + if HTTP_TLS == "0" { + fmt.Println("Protocol is http (insecure)") + StopServer(server.ListenAndServe()) + } + if HTTP_TLS == "1" { + fmt.Println("Protocol is https (secure)") + StopServer(server.ListenAndServeTLS(HTTP_TLS_CERTIFICATE, HTTP_TLS_PRIVATEKEY)) + } + +} + +func StopServer(e error) { + fmt.Println("Stopping server...") + prepareExit() + fmt.Println("Server stopped!") +} + +func HttpSaveFile(xfile string, a any) racc_httpstatus.App { + return func(next_w http.ResponseWriter, next_r *http.Request) { + file, _ := os.Create(xfile) + encoder := json.NewEncoder(file) + err := encoder.Encode(a) + if err != nil { + fmt.Println("Error encoding JSON:", err) + } + file.Close() + } +} + +func SaveFile(xfile string, a any) { + file, _ := os.Create(xfile) + encoder := json.NewEncoder(file) + err := encoder.Encode(a) + if err != nil { + fmt.Println("Error encoding JSON:", err) + } + file.Close() + fmt.Println("Saved content to: ", xfile) +} + +func HttpLoadFile(xfile string, a any) racc_httpstatus.App { + return func(next_w http.ResponseWriter, next_r *http.Request) { + if _, err := os.Stat(xfile); os.IsNotExist(err) { + fmt.Println("Speicherdatei nicht gefunden, neue Map wird erstellt") + } else { + file, _ := os.Open(xfile) + decoder := json.NewDecoder(file) + err := decoder.Decode(a) + if err != nil { + fmt.Println("Fehler in JSON-Datei:", err) + } + file.Close() + } + } +} + +func LoadFile(xfile string, a any) { + if _, err := os.Stat(xfile); os.IsNotExist(err) { + fmt.Println("Speicherdatei nicht gefunden, neue Map wird erstellt") + } else { + file, _ := os.Open(xfile) + decoder := json.NewDecoder(file) + err := decoder.Decode(a) + if err != nil { + fmt.Println("Fehler in JSON-Datei:", err) + } + file.Close() + } + fmt.Println("Loaded content from: ", xfile) +} diff --git a/racc_common/racc_common.go b/racc_common/racc_common.go new file mode 100644 index 0000000..06b3c41 --- /dev/null +++ b/racc_common/racc_common.go @@ -0,0 +1,22 @@ +package racc_common + +import ( + "os" + "strconv" + "strings" +) + +func GetENV(k, d string) string { + if v := os.Getenv(k); v != "" { + return v + } + return d +} + +func Enabled(k string, def bool) bool { + b, err := strconv.ParseBool(strings.ToLower(os.Getenv(k))) + if err != nil { + return def + } + return b +} diff --git a/racc_filebrowser/racc_filebrowser.go b/racc_filebrowser/racc_filebrowser.go new file mode 100644 index 0000000..616b737 --- /dev/null +++ b/racc_filebrowser/racc_filebrowser.go @@ -0,0 +1,150 @@ +package racc_filebrowser + +import ( + "errors" + "html/template" + "net/http" + "os" + "path" + "path/filepath" + "sort" + "strings" +) + +const ( + appID = "files" + baseURI = "/files" // URL-Prefix (immer Slash) + root = "./" + appID // lokaler Wurzel­ordner +) + +// ---------- Helper-Typen ---------- + +type fileInfo struct { + Name string // nur Dateiname + WebPath string // URL-Pfad (Slash-Separators) + LocalPath string // voller lokaler Pfad (OS-Separators) + IsDir bool +} + +type pageData struct { + Path string // angezeigter Ordner (URL-Pfad ab /files) + Query string // Suchbegriff + Dirs []fileInfo // nur Ordner + Files []fileInfo // nur Dateien +} + +// ---------- Öffentliche API ---------- + +// Router liefert eine fertige Sub-Mux, die du im Haupt­server mounten kannst. +func Router() *http.ServeMux { + m := http.NewServeMux() + m.HandleFunc("/", handle) + return m +} + +// ---------- HTTP-Handler ---------- + +func handle(w http.ResponseWriter, r *http.Request) { + // Zielpfad aus URL zusammensetzen und absichern + reqPath := strings.TrimPrefix(r.URL.Path, baseURI) // »/foo« → »foo« + local, err := safeJoin(root, reqPath) // verhindert ..-Traversal + if err != nil { + http.Error(w, "invalid path", http.StatusBadRequest) + return + } + query := r.URL.Query().Get("q") + + // Datei-Info ermitteln + info, err := os.Stat(local) + if errors.Is(err, os.ErrNotExist) { + http.NotFound(w, r) + return + } else if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Datei direkt ausliefern + if !info.IsDir() && query == "" { + http.ServeFile(w, r, local) + return + } + + // Verzeichnis auflisten + dirs, files, err := listDir(local, query) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + pd := pageData{ + Path: path.Clean("/" + reqPath), // immer URL-Slash + Query: query, + Dirs: dirs, + Files: files, + } + renderTemplate(w, pd) +} + +// ---------- Pfad-Utilities ---------- + +// safeJoin verknüpft root und req so, dass das Ergebnis IMMER unter root bleibt. +func safeJoin(root, req string) (string, error) { + clean := filepath.Clean("/" + req) // Normieren + local := filepath.Join(root, clean) // OS-Pfad + if !strings.HasPrefix(local, filepath.Clean(root)) { + return "", errors.New("path escape") + } + return local, nil +} + +// buildWebPath konvertiert einen lokalen Pfad zurück in einen URL-Pfad. +func buildWebPath(local string) string { + rel, _ := filepath.Rel(root, local) // garantiert unter root + return path.Join(baseURI, filepath.ToSlash(rel)) +} + +// ---------- Verzeichnis lesen ---------- + +func listDir(folder, query string) (dirs, files []fileInfo, err error) { + entries, err := os.ReadDir(folder) + if err != nil { + return nil, nil, err + } + + for _, e := range entries { + name := e.Name() + if query != "" && !strings.Contains(strings.ToLower(name), strings.ToLower(query)) { + continue + } + + local := filepath.Join(folder, name) + item := fileInfo{ + Name: name, + LocalPath: local, + WebPath: buildWebPath(local), + IsDir: e.IsDir(), + } + if item.IsDir { + dirs = append(dirs, item) + } else { + files = append(files, item) + } + } + + // alphabetisch sortieren + sort.Slice(dirs, func(i, j int) bool { return dirs[i].Name < dirs[j].Name }) + sort.Slice(files, func(i, j int) bool { return files[i].Name < files[j].Name }) + return +} + +// ---------- Template-Renderer ---------- + +var tmpl = template.Must(template.ParseFiles("./html-templates/filebrowser.html")) + +func renderTemplate(w http.ResponseWriter, pd pageData) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + if err := tmpl.Execute(w, pd); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} diff --git a/racc_httpstatus/racc_httpstatus.go b/racc_httpstatus/racc_httpstatus.go new file mode 100644 index 0000000..27297d4 --- /dev/null +++ b/racc_httpstatus/racc_httpstatus.go @@ -0,0 +1,117 @@ +package racc_httpstatus + +import ( + "net/http" + "strconv" + + "git.send.nrw/sendnrw/raccws/racc_logging" + "git.send.nrw/sendnrw/raccws/racc_template" +) + +type App func(http.ResponseWriter, *http.Request) + +var Default400 = CreateStatusPage("defaultErrors.html", "./html-templates", 400, struct { + ErrorCode string + ErrorName string + ErrorMSG string +}{ErrorCode: "400", ErrorName: "Bad Request", ErrorMSG: "The 400 (Bad Request) status code indicates that the server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing)."}) + +var Default401 = CreateStatusPage("defaultErrors.html", "./html-templates", 401, struct { + ErrorCode string + ErrorName string + ErrorMSG string +}{ErrorCode: "401", ErrorName: "Unauthorized", ErrorMSG: "The 401 (Unauthorized) status code indicates that the request has not been applied because it lacks valid authentication credentials for the target resource. The server generating a 401 response MUST send a WWW-Authenticate header field containing at least one challenge applicable to the target resource."}) + +var Default402 = CreateStatusPage("defaultErrors.html", "./html-templates", 402, struct { + ErrorCode string + ErrorName string + ErrorMSG string +}{ErrorCode: "402", ErrorName: "Payment Required", ErrorMSG: "The 402 (Payment Required) status code is reserved for future use."}) + +var Default403 = CreateStatusPage("defaultErrors.html", "./html-templates", 403, struct { + ErrorCode string + ErrorName string + ErrorMSG string +}{ErrorCode: "403", ErrorName: "Forbidden", ErrorMSG: "The 403 (Forbidden) status code indicates that the server understood the request but refuses to fulfill it. A server that wishes to make public why the request has been forbidden can describe that reason in the response content (if any)."}) + +var Default404 = CreateStatusPage("defaultErrors.html", "./html-templates", 404, struct { + ErrorCode string + ErrorName string + ErrorMSG string +}{ErrorCode: "404", ErrorName: "Not Found", ErrorMSG: "The 404 (Not Found) status code indicates that the origin server did not find a current representation for the target resource or is not willing to disclose that one exists. A 404 status code does not indicate whether this lack of representation is temporary or permanent; the 410 (Gone) status code is preferred over 404 if the origin server knows, presumably through some configurable means, that the condition is likely to be permanent."}) + +var Default405 = CreateStatusPage("defaultErrors.html", "./html-templates", 405, struct { + ErrorCode string + ErrorName string + ErrorMSG string +}{ErrorCode: "405", ErrorName: "Method Not Allowed", ErrorMSG: "The 405 (Method Not Allowed) status code indicates that the method received in the request-line is known by the origin server but not supported by the target resource. The origin server MUST generate an Allow header field in a 405 response containing a list of the target resource's currently supported methods."}) + +var Default406 = CreateStatusPage("defaultErrors.html", "./html-templates", 406, struct { + ErrorCode string + ErrorName string + ErrorMSG string +}{ErrorCode: "406", ErrorName: "Not Acceptable", ErrorMSG: "The 406 (Not Acceptable) status code indicates that the target resource does not have a current representation that would be acceptable to the user agent, according to the proactive negotiation header fields received in the request, and the server is unwilling to supply a default representation."}) + +var Default407 = CreateStatusPage("defaultErrors.html", "./html-templates", 407, struct { + ErrorCode string + ErrorName string + ErrorMSG string +}{ErrorCode: "407", ErrorName: "Proxy Authentication Required", ErrorMSG: "The 407 (Proxy Authentication Required) status code is similar to 401 (Unauthorized), but it indicates that the client needs to authenticate itself in order to use a proxy for this request. The proxy MUST send a Proxy-Authenticate header field containing a challenge applicable to that proxy for the request. The client MAY repeat the request with a new or replaced Proxy-Authorization header field."}) + +var Default408 = CreateStatusPage("defaultErrors.html", "./html-templates", 408, struct { + ErrorCode string + ErrorName string + ErrorMSG string +}{ErrorCode: "408", ErrorName: "Request Timeout", ErrorMSG: "The 408 (Request Timeout) status code indicates that the server did not receive a complete request message within the time that it was prepared to wait."}) + +var Default409 = CreateStatusPage("defaultErrors.html", "./html-templates", 409, struct { + ErrorCode string + ErrorName string + ErrorMSG string +}{ErrorCode: "409", ErrorName: "Conflict", ErrorMSG: "The 409 (Conflict) status code indicates that the request could not be completed due to a conflict with the current state of the target resource. This code is used in situations where the user might be able to resolve the conflict and resubmit the request. The server SHOULD generate content that includes enough information for a user to recognize the source of the conflict. Conflicts are most likely to occur in response to a PUT request. For example, if versioning were being used and the representation being PUT included changes to a resource that conflict with those made by an earlier (third-party) request, the origin server might use a 409 response to indicate that it can't complete the request. In this case, the response representation would likely contain information useful for merging the differences based on the revision history."}) + +var Default500 = CreateStatusPage("defaultErrors.html", "./html-templates", 500, struct { + ErrorCode string + ErrorName string + ErrorMSG string +}{ErrorCode: "500", ErrorName: "Internal Server Error", ErrorMSG: "The 500 (Internal Server Error) status code indicates that the server encountered an unexpected condition that prevented it from fulfilling the request."}) + +var Default501 = CreateStatusPage("defaultErrors.html", "./html-templates", 501, struct { + ErrorCode string + ErrorName string + ErrorMSG string +}{ErrorCode: "501", ErrorName: "Not Implemented", ErrorMSG: "The 501 (Not Implemented) status code indicates that the server does not support the functionality required to fulfill the request. This is the appropriate response when the server does not recognize the request method and is not capable of supporting it for any resource."}) + +var Default502 = CreateStatusPage("defaultErrors.html", "./html-templates", 502, struct { + ErrorCode string + ErrorName string + ErrorMSG string +}{ErrorCode: "502", ErrorName: "Bad Gateway", ErrorMSG: "The 502 (Bad Gateway) status code indicates that the server, while acting as a gateway or proxy, received an invalid response from an inbound server it accessed while attempting to fulfill the request."}) + +var Default503 = CreateStatusPage("defaultErrors.html", "./html-templates", 503, struct { + ErrorCode string + ErrorName string + ErrorMSG string +}{ErrorCode: "503", ErrorName: "Service Unavailable", ErrorMSG: "The 503 (Service Unavailable) status code indicates that the server is currently unable to handle the request due to a temporary overload or scheduled maintenance, which will likely be alleviated after some delay. The server MAY send a Retry-After header field to suggest an appropriate amount of time for the client to wait before retrying the request."}) + +var Default504 = CreateStatusPage("defaultErrors.html", "./html-templates", 504, struct { + ErrorCode string + ErrorName string + ErrorMSG string +}{ErrorCode: "504", ErrorName: "Gateway Timeout", ErrorMSG: "The 504 (Gateway Timeout) status code indicates that the server, while acting as a gateway or proxy, did not receive a timely response from an upstream server it needed to access in order to complete the request."}) + +func CreateStatusPage(tplname, tpldir string, statuscode int, data interface{}) App { + return func(next_w http.ResponseWriter, next_r *http.Request) { + sc := strconv.Itoa(statuscode) + racc_logging.AppendToLogMainFunction(next_w, next_r, sc) + racc_template.ProcessTemplate(next_w, tplname, tpldir, statuscode, data) + } + +} + +func CreateAppResponse(statuscode int, statusmsg string) App { + return func(next_w http.ResponseWriter, next_r *http.Request) { + next_w.WriteHeader(statuscode) + next_w.Write([]byte(statusmsg)) + } +} diff --git a/racc_logging/racc_logging.go b/racc_logging/racc_logging.go new file mode 100644 index 0000000..dbcc29d --- /dev/null +++ b/racc_logging/racc_logging.go @@ -0,0 +1,58 @@ +package racc_logging + +import ( + "fmt" + "net/http" + "strings" + "time" + + "git.send.nrw/sendnrw/raccws/racc_session" +) + +type LogItem struct { + Module string + URL string + Time string + Session string + RemoteIP string + RemotePort string + Method string + Status string +} + +var LogBook []LogItem + +func GetLastXitems(amount int) []LogItem { + var temp []LogItem + startIndex := len(LogBook) - amount + // Sicherstellen, dass startIndex nicht negativ wird + if startIndex < 0 { + startIndex = 0 + } + for i := startIndex; i < len(LogBook); i++ { + temp = append(temp, LogBook[i]) + } + return temp +} + +func Logging(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + AppendToLogMainFunction(w, r, "") + next.ServeHTTP(w, r) + }) +} + +func AppendToLogMainFunction(w http.ResponseWriter, r *http.Request, status string) { + start := time.Now() + sessionID := r.Context().Value(racc_session.SessionKey).(string) + meth := r.Method + IP_ONLY := strings.Split(r.RemoteAddr, ":")[0] + PORT_ONLY := strings.Split(r.RemoteAddr, ":")[1] + li := LogItem{Module: "http", URL: r.URL.Path, Time: start.Format(time.DateTime), Session: sessionID, RemoteIP: IP_ONLY, RemotePort: PORT_ONLY, Status: status, Method: meth} + WriteLog(li) +} + +func WriteLog(logitem LogItem) { + LogBook = append(LogBook, logitem) + fmt.Println(logitem) +} diff --git a/racc_middleware/racc_middleware.go b/racc_middleware/racc_middleware.go new file mode 100644 index 0000000..088fd0c --- /dev/null +++ b/racc_middleware/racc_middleware.go @@ -0,0 +1,15 @@ +package racc_middleware + +import "net/http" + +type Middleware func(http.Handler) http.Handler + +func CreateStack(xs ...Middleware) Middleware { + return func(next http.Handler) http.Handler { + for i := len(xs) - 1; i >= 0; i-- { + x := xs[i] + next = x(next) + } + return next + } +} diff --git a/racc_session/racc_session.go b/racc_session/racc_session.go new file mode 100644 index 0000000..519101e --- /dev/null +++ b/racc_session/racc_session.go @@ -0,0 +1,101 @@ +package racc_session + +import ( + "context" + "net/http" + "time" + + "github.com/google/uuid" +) + +type contextKey string + +const SessionKey contextKey = "session_id" + +/* Default Session Expiration */ +var SessionDuration time.Duration = 24 * 60 * time.Minute + +// SessionMiddleware überprüft, ob ein Session-Cookie vorhanden ist, +// und erstellt bei Bedarf eine neue SessionID. +func SessionMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + cookie, err := r.Cookie("session_id") + if err != nil || cookie.Value == "" { + // Erstelle eine neue SessionID + sessionID := uuid.New().String() + // Erstelle Time to Expire + ///now := time.Now() + // Ablaufzeit in 60 Minuten + ///expiration := now.Local().Add(SessionDuration) + // Speichere die SessionID in einem Cookie + http.SetCookie(w, &http.Cookie{ + Name: "session_id", + Value: sessionID, + Path: "/", + ///Expires: expiration, + // Optional: Setze weitere Cookie-Attribute wie Secure, HttpOnly, etc. + }) + // Setze die SessionID im Kontext des Requests + r = r.WithContext(context.WithValue(r.Context(), SessionKey, sessionID)) + } else { + // Setze die vorhandene SessionID im Kontext des Requests + r = r.WithContext(context.WithValue(r.Context(), SessionKey, cookie.Value)) + } + next.ServeHTTP(w, r) + }) +} + +type SessionItem struct { + Key string + Value string +} + +var SessionStore = map[string][]SessionItem{} + +func AddOrUpdateSessionItem(sessionID string, item SessionItem) { + if items, exists := SessionStore[sessionID]; exists { + for i, existingItem := range items { + if existingItem.Key == item.Key { + SessionStore[sessionID][i].Value = item.Value + return + } + } + SessionStore[sessionID] = append(items, item) + } else { + SessionStore[sessionID] = []SessionItem{item} + } + /*jsonstore, err := json.Marshal(SessionStore) + if err != nil { + panic("LOL") + } + fmt.Println(string(jsonstore))*/ +} + +func RemoveSessionItem(sessionID string, keyToRemove string) { + if items, exists := SessionStore[sessionID]; exists { + updatedItems := []SessionItem{} + for _, item := range items { + if item.Key != keyToRemove { + updatedItems = append(updatedItems, item) + } + } + if len(updatedItems) == 0 { + delete(SessionStore, sessionID) + } else { + SessionStore[sessionID] = updatedItems + } + } +} + +func GetSessionItem(sessionID string, keyToInspect string) string { + if items, exists := SessionStore[sessionID]; exists { + for i, existingItem := range items { + if existingItem.Key == keyToInspect { + return SessionStore[sessionID][i].Value + } + } + return "" + } else { + return "" + } +} diff --git a/racc_sysauth/racc_sysauth.go b/racc_sysauth/racc_sysauth.go new file mode 100644 index 0000000..6842633 --- /dev/null +++ b/racc_sysauth/racc_sysauth.go @@ -0,0 +1,289 @@ +package racc_sysauth + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "net/http" + "os" + "strings" + + "git.send.nrw/sendnrw/raccws/racc_session" +) + +type Credential struct { + Username string `json:"username"` + Password string `json:"password"` +} + +// BearerToken repräsentiert ein gültiges Token +type BearerToken string + +// MultiAuthMiddleware ist eine Middleware, die Basic Auth, Bearer Auth und Active Directory Auth unterstützt. +func MultiAuthMiddleware(basicCredFilePath, bearerTokenFilePath, adServer, adBaseDN, adUserDN, adPassword string) func(http.Handler) http.Handler { + basicCredentials, err := loadBasicCredentials(basicCredFilePath) + if err != nil { + fmt.Println("Error loading basic credentials:", err) + return nil + } + bearerTokens, err := loadBearerTokens(bearerTokenFilePath) + if err != nil { + fmt.Println("Error loading bearer tokens:", err) + return nil + } + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + authHeader := r.Header.Get("Authorization") + z0, _ := validateAuth(authHeader, basicCredentials, bearerTokens, adServer, adBaseDN, adUserDN, adPassword) + if !z0 { + w.Header().Set("WWW-Authenticate", `Basic realm="Please provide credentials"`) + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + next.ServeHTTP(w, r) + }) + } +} + +// ADAuthMiddleware ist eine Middleware, die Active Directory-Authentifizierung durchführt. +/*func ADAuthMiddleware(adServer, adBaseDN, adUserDN, adPassword string) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + authHeader := r.Header.Get("Authorization") + if authHeader == "" || !strings.HasPrefix(authHeader, "Basic ") { + w.Header().Set("WWW-Authenticate", `Basic realm="Please provide credentials"`) + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + encodedCredentials := strings.TrimPrefix(authHeader, "Basic ") + decodedCredentials, err := base64.StdEncoding.DecodeString(encodedCredentials) + if err != nil { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + parts := strings.SplitN(string(decodedCredentials), ":", 2) + if len(parts) != 2 { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + username := parts[0] + password := parts[1] + if !validateADCredentials(adServer, adBaseDN, adUserDN, adPassword, username, password) { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + next.ServeHTTP(w, r) + }) + } +}*/ + +// BasicAuthMiddleware ist eine Middleware, die Basic-Authentifizierung erfordert. +func BasicAuthMiddleware(credFilePath string) func(http.Handler) http.Handler { + credentials, err := loadCredentials(credFilePath) + if err != nil { + fmt.Println("Error loading credentials:", err) + return nil + } + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + authHeader := r.Header.Get("Authorization") + z0, z1 := validateBasicAuth(authHeader, credentials) + if !z0 { + w.Header().Set("WWW-Authenticate", `Basic realm="Please provide credentials"`) + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } else { + sessionID := r.Context().Value(racc_session.SessionKey).(string) + authMethod := racc_session.SessionItem{Key: "authmethod", Value: "Basic"} + racc_session.AddOrUpdateSessionItem(sessionID, authMethod) + authUser := racc_session.SessionItem{Key: "authuser", Value: z1} + racc_session.AddOrUpdateSessionItem(sessionID, authUser) + } + next.ServeHTTP(w, r) + }) + } +} + +// BearerAuthMiddleware ist eine Middleware, die Bearer-Authentifizierung erfordert. +func BearerAuthMiddleware(tokenFilePath string) func(http.Handler) http.Handler { + tokens, err := loadBearerTokens(tokenFilePath) + if err != nil { + fmt.Println("Error loading tokens:", err) + return nil + } + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + authHeader := r.Header.Get("Authorization") + if !validateBearerToken(authHeader, tokens) { + w.Header().Set("WWW-Authenticate", `Bearer realm="Please provide a valid token"`) + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + next.ServeHTTP(w, r) + }) + } +} + +// loadBasicCredentials lädt die Anmeldeinformationen für Basic Auth aus einer JSON-Datei +func loadBasicCredentials(filePath string) ([]Credential, error) { + file, err := os.Open(filePath) + if err != nil { + return nil, err + } + defer file.Close() + var credentials []Credential + decoder := json.NewDecoder(file) + if err := decoder.Decode(&credentials); err != nil { + return nil, err + } + return credentials, nil +} + +// loadBearerTokens lädt die gültigen Bearer-Tokens aus einer JSON-Datei +func loadBearerTokens(filePath string) ([]BearerToken, error) { + file, err := os.Open(filePath) + if err != nil { + return nil, err + } + defer file.Close() + var tokens []BearerToken + decoder := json.NewDecoder(file) + if err := decoder.Decode(&tokens); err != nil { + return nil, err + } + return tokens, nil +} + +// validateAuth überprüft, ob der Authorization-Header entweder Basic Auth, Bearer Token oder Active Directory enthält +func validateAuth(authHeader string, basicCredentials []Credential, bearerTokens []BearerToken, adServer, adBaseDN, adUserDN, adPassword string) (bool, string) { + if authHeader == "" { + return false, "" + } + if strings.HasPrefix(authHeader, "Basic ") { + return validateBasicAuth(authHeader, basicCredentials) + } else if strings.HasPrefix(authHeader, "Bearer ") { + return validateBearerToken(authHeader, bearerTokens), "" + } else if strings.HasPrefix(authHeader, "AD ") { + /*return validateADAuth(authHeader, adServer, adBaseDN, adUserDN, adPassword), ""*/ + } + return false, "" +} + +// validateBasicAuth überprüft, ob der Authorization-Header gültige Basic-Authentifizierungsdaten enthält +func validateBasicAuth(authHeader string, credentials []Credential) (bool, string) { + encodedCredentials := strings.TrimPrefix(authHeader, "Basic ") + decodedCredentials, err := base64.StdEncoding.DecodeString(encodedCredentials) + if err != nil { + return false, "" + } + parts := strings.SplitN(string(decodedCredentials), ":", 2) + if len(parts) != 2 { + return false, "" + } + username := parts[0] + password := parts[1] + for _, cred := range credentials { + if username == cred.Username && password == cred.Password { + return true, cred.Username + } + } + return false, "" +} + +// validateBearerToken überprüft, ob der Authorization-Header ein gültiges Bearer-Token enthält +func validateBearerToken(authHeader string, validTokens []BearerToken) bool { + token := strings.TrimPrefix(authHeader, "Bearer ") + for _, validToken := range validTokens { + if BearerToken(token) == validToken { + return true + } + } + return false +} + +// validateADAuth überprüft die Benutzeranmeldeinformationen gegen Active Directory +/*func validateADAuth(authHeader, adServer, adBaseDN, adUserDN, adPassword string) bool { + if !strings.HasPrefix(authHeader, "AD ") { + return false + } + + encodedCredentials := strings.TrimPrefix(authHeader, "AD ") + decodedCredentials, err := base64.StdEncoding.DecodeString(encodedCredentials) + if err != nil { + return false + } + + parts := strings.SplitN(string(decodedCredentials), ":", 2) + if len(parts) != 2 { + return false + } + + username := parts[0] + password := parts[1] + + return validateADCredentials(adServer, adBaseDN, adUserDN, adPassword, username, password) +}*/ + +// validateADCredentials überprüft die Benutzeranmeldeinformationen gegen Active Directory +/*func validateADCredentials(adServer, adBaseDN, adUserDN, adPassword, username, password string) bool { + l, err := ldap.DialURL(adServer) + if err != nil { + fmt.Println("Failed to connect to AD server:", err) + return false + } + defer l.Close() + + err = l.Bind(adUserDN, adPassword) + if err != nil { + fmt.Println("Failed to bind to AD server:", err) + return false + } + + searchRequest := ldap.NewSearchRequest( + adBaseDN, + ldap.ScopeWholeSubtree, + ldap.NeverDerefAliases, + 0, + 0, + false, + fmt.Sprintf("(sAMAccountName=%s)", username), + []string{"dn"}, + nil, + ) + + sr, err := l.Search(searchRequest) + if err != nil { + fmt.Println("Failed to search user in AD:", err) + return false + } + + if len(sr.Entries) != 1 { + fmt.Println("User not found or multiple users found") + return false + } + + userDN := sr.Entries[0].DN + err = l.Bind(userDN, password) + if err != nil { + fmt.Println("Failed to authenticate user:", err) + return false + } + + return true +}*/ + +func loadCredentials(filePath string) ([]Credential, error) { + file, err := os.Open(filePath) + if err != nil { + return nil, err + } + defer file.Close() + + var credentials []Credential + decoder := json.NewDecoder(file) + if err := decoder.Decode(&credentials); err != nil { + return nil, err + } + + return credentials, nil +} diff --git a/racc_template/racc_template.go b/racc_template/racc_template.go new file mode 100644 index 0000000..6af05af --- /dev/null +++ b/racc_template/racc_template.go @@ -0,0 +1,81 @@ +package racc_template + +import ( + "fmt" + "html/template" + "log" + "net/http" + "os" + "path/filepath" +) + +var templates = template.Must(template.ParseFiles("./html-templates/index.html")) + +func WriteTemplate(title, description, templatedir string, status int) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + initTemplates(templatedir) + w.WriteHeader(status) + //fmt.Println("Status WriteTemplate ", status) + renderTemplate(w, "html-templates/application_mainframe.html", nil) + }) + } +} + +func RenderApplicationTemplate(tmpl string, data interface{}) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + a, b := template.ParseFiles("html-templates/application_mainframe.html", tmpl) + fmt.Println(b) + tmpls := template.Must(a, b) + c := tmpls.ExecuteTemplate(w, "application_mainframe.html", data) + fmt.Println(c) + }) + } +} + +func RenderApplicationTemplateHandler(tmpl string, data interface{}) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + a, b := template.ParseFiles("html-templates/application_mainframe.html", tmpl) + fmt.Println(b) + tmpls := template.Must(a, b) + c := tmpls.ExecuteTemplate(w, "application_mainframe.html", data) + fmt.Println(c) + }) +} + +func ProcessTemplate(w http.ResponseWriter, templatename, templatedir string, status int, data interface{}) { + initTemplates(templatedir) + w.WriteHeader(status) + //fmt.Println("Status ProcessTemplate ", status) + renderTemplate(w, templatename, data) +} + +func initTemplates(directory string) { + + files, err := os.ReadDir(directory) + if err != nil { + log.Fatal(err) + } + + var filePaths []string + for _, file := range files { + if !file.IsDir() { + filePaths = append(filePaths, filepath.Join(directory, file.Name())) + } + } + fmt.Println(filePaths) + tmpl, err := template.ParseFiles(filePaths...) + if err != nil { + log.Fatal(err) + } + + templates = template.Must(tmpl, err) +} + +func renderTemplate(w http.ResponseWriter, tmpl string, data interface{}) { + err := templates.ExecuteTemplate(w, tmpl, data) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +}