From 038817d24de372a5869a1726003f114257879efc Mon Sep 17 00:00:00 2001 From: Damien Guihal Date: Mon, 6 Jan 2025 12:07:39 +0100 Subject: [PATCH 01/12] Be more flexible on action versions --- .github/workflows/docker-build.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 266e9ba..8de4fcc 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -35,7 +35,7 @@ jobs: working-directory: web - name: Log in to the Container registry - uses: docker/login-action@v1.10.0 + uses: docker/login-action@v1 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -43,16 +43,15 @@ jobs: - name: Extract metadata (tags, labels) for Docker id: meta - uses: docker/metadata-action@v5.5.1 + uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | # set latest tag for master branch type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }} - - name: Build and push Docker image - uses: docker/build-push-action@v6.0.0 + uses: docker/build-push-action@v6 with: context: . push: true From 0ae42047ca3209cce77e99dbfc9c002cb054c11a Mon Sep 17 00:00:00 2001 From: Damien Guihal Date: Mon, 6 Jan 2025 12:23:38 +0100 Subject: [PATCH 02/12] fix tags guessing --- .github/workflows/docker-build.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 8de4fcc..abc4ef2 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -46,9 +46,6 @@ jobs: uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - tags: | - # set latest tag for master branch - type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }} - name: Build and push Docker image uses: docker/build-push-action@v6 From aed38c8d5531e9b991e06a09f9145e25b03896a1 Mon Sep 17 00:00:00 2001 From: Damien Guihal Date: Fri, 10 Jan 2025 15:53:08 +0100 Subject: [PATCH 03/12] Add XML header to XML backend --- backendhandler.go | 11 +++++++++-- goboard.go | 4 ++-- web/package-lock.json | 4 ++-- web/package.json | 2 +- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/backendhandler.go b/backendhandler.go index 1878e5b..5b5e1f7 100644 --- a/backendhandler.go +++ b/backendhandler.go @@ -114,7 +114,8 @@ func (b *BackendHandler) getBackend(w http.ResponseWriter, r *http.Request) { } // TODO : Manage returning an original posted data for a specific id as text -// Maybe consider allowing this only for admins (not sure it is relevant) +// +// Maybe consider allowing this only for admins (not sure it is relevant) func (b *BackendHandler) getPost(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) @@ -258,6 +259,10 @@ func guessFormat(formatAttr string, acceptHeader string) (format string) { func postsToXML(posts []goboardbackend.Post, backendLocation string) []byte { var b = goboardbackend.Board{} + var outputBuffer bytes.Buffer + + outputBuffer.WriteString("\n") + if (len(backendLocation)) > 0 { b.Site = "http://" + backendLocation } else { @@ -279,7 +284,9 @@ func postsToXML(posts []goboardbackend.Post, backendLocation string) []byte { if err != nil { return []byte(err.Error()) } - return s + outputBuffer.Write(s) + + return outputBuffer.Bytes() } func postsToJSON(posts []goboardbackend.Post) []byte { diff --git a/goboard.go b/goboard.go index 93a9708..e639a83 100644 --- a/goboard.go +++ b/goboard.go @@ -18,7 +18,7 @@ import ( "gopkg.in/yaml.v2" ) -const goBoardVer = 0.03 +const goBoardVer = "0.0.4" // Config holds the configuration of the process type Config struct { @@ -188,7 +188,7 @@ func main() { } } - fmt.Println("GoBoard version ", goBoardVer, " starting on port", config.ListenPort) + fmt.Println("GoBoard version", goBoardVer, "starting on port", config.ListenPort) var handler http.Handler if fiAccessLog != nil { diff --git a/web/package-lock.json b/web/package-lock.json index 56cbe26..7c28b6d 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "goboard-ui", - "version": "0.0.3", + "version": "0.0.4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "goboard-ui", - "version": "0.0.3", + "version": "0.0.4", "hasInstallScript": true, "devDependencies": { "bower": "^1.8.14", diff --git a/web/package.json b/web/package.json index 1ecdf9e..8127ca6 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "goboard-ui", - "version": "0.0.3", + "version": "0.0.4", "private": true, "devDependencies": { "bower": "^1.8.14", From 3b967e8110651f90fc8228c93a70eeefb189dbe3 Mon Sep 17 00:00:00 2001 From: Damien Guihal <4045744+dguihal@users.noreply.github.com> Date: Wed, 19 Feb 2025 10:02:18 +0100 Subject: [PATCH 04/12] Update codeql-analysis.yml --- .github/workflows/codeql-analysis.yml | 78 ++++++++++++++++++--------- 1 file changed, 53 insertions(+), 25 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 40b4d4d..0452aa3 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -9,14 +9,13 @@ # the `language` matrix defined below to confirm you have the correct set of # supported CodeQL languages. # -name: "CodeQL" +name: "CodeQL Advanced" on: push: - branches: [ master ] + branches: [ "master" ] pull_request: - # The branches below must be a subset of the branches above - branches: [ master ] + branches: [ "master" ] schedule: - cron: '24 13 * * 2' @@ -24,49 +23,78 @@ permissions: read-all jobs: analyze: - name: Analyze - runs-on: ubuntu-latest + name: Analyze (${{ matrix.language }}) + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners (GitHub.com only) + # Consider using larger runners or machines with greater resources for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories actions: read contents: read - security-events: write strategy: fail-fast: false matrix: - language: [ 'go', 'javascript' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] - # Learn more about CodeQL language support at https://git.io/codeql-language-support - + language: [ 'go', 'javascript-typescript' ] + build-mode: autobuild + # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' + # Use `c-cpp` to analyze code written in C, C++ or both + # Use 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, + # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. + # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how + # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages steps: - name: Checkout repository uses: actions/checkout@v4 + # Add any setup steps before running the `github/codeql-action/init` action. + # This includes steps like installing compilers or runtimes (`actions/setup-node` + # or others). This is typically only required for manual builds. + # - name: Setup runtime (example) + # uses: actions/setup-example@v1 + # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. - # queries: ./path/to/local/query, your-org/your-repo/queries@main - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v3 + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + # If the analyze step fails for one of the languages you are analyzing with + # "We were unable to automatically build your code", modify the matrix above + # to set the build mode to "manual" for that language. Then modify this step + # to build your code. # ℹ️ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl - - # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language - - #- run: | - # make bootstrap - # make release + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + - if: matrix.build-mode == 'manual' + shell: bash + run: | + echo 'If you are using a "manual" build mode for one or more of the' \ + 'languages you are analyzing, replace this with the commands to build' \ + 'your code, for example:' + echo ' make bootstrap' + echo ' make release' + exit 1 - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" + upload: "never" From 0d7c7f4c1075a33e7dfc0c4564a7df75bcf75b7b Mon Sep 17 00:00:00 2001 From: Damien Guihal <4045744+dguihal@users.noreply.github.com> Date: Wed, 19 Feb 2025 10:09:19 +0100 Subject: [PATCH 05/12] Update linter.yml --- .github/workflows/linter.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index b0ba938..cfce489 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -63,7 +63,7 @@ jobs: # Run Linter against code base # ################################ - name: Lint Code Base - uses: github/super-linter@v6 + uses: super-linter/super-linter/slim@v7 env: DEFAULT_BRANCH: master GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From d9fee364b6521805e17791e009e124d8660ff21e Mon Sep 17 00:00:00 2001 From: Damien Guihal <4045744+dguihal@users.noreply.github.com> Date: Wed, 19 Feb 2025 10:17:16 +0100 Subject: [PATCH 06/12] Update Dockerfile --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 6592490..fabf558 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ ## ## Build ## -FROM golang:1.22-alpine AS build +FROM golang:1.23-alpine AS build WORKDIR /goboard From 56f14ab371d698e8f26f2933ea4885636db4c94a Mon Sep 17 00:00:00 2001 From: Damien Guihal <4045744+dguihal@users.noreply.github.com> Date: Wed, 19 Feb 2025 10:17:46 +0100 Subject: [PATCH 07/12] Update go.mod --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 7039f0a..3d441f9 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/dguihal/goboard -go 1.22 +go 1.23 toolchain go1.23.6 require ( From ade7f9b950b3afe54ae597be4382bcd00105d22b Mon Sep 17 00:00:00 2001 From: Damien Guihal Date: Tue, 13 May 2025 11:19:24 +0200 Subject: [PATCH 08/12] Refactor code --- goboard.go | 40 +++++++------------ .../admin/adminhandler.go | 17 ++++---- .../backend/backendhandler.go | 21 +++++----- handlers/goboardhandler.go | 24 +++++++++++ .../template/templatehandler.go | 10 +++-- .../user/userhandler.go | 19 ++++----- handlers/user/userhandler_test.go | 32 +++++++++++++++ 7 files changed, 106 insertions(+), 57 deletions(-) rename adminhandler.go => handlers/admin/adminhandler.go (85%) rename backendhandler.go => handlers/backend/backendhandler.go (90%) create mode 100644 handlers/goboardhandler.go rename templatehandler.go => handlers/template/templatehandler.go (86%) rename userhandler.go => handlers/user/userhandler.go (86%) create mode 100644 handlers/user/userhandler_test.go diff --git a/goboard.go b/goboard.go index e639a83..085763a 100644 --- a/goboard.go +++ b/goboard.go @@ -16,6 +16,11 @@ import ( "github.com/gorilla/mux" bolt "go.etcd.io/bbolt" "gopkg.in/yaml.v2" + + admin "github.com/dguihal/goboard/handlers/admin" + backend "github.com/dguihal/goboard/handlers/backend" + template "github.com/dguihal/goboard/handlers/template" + user "github.com/dguihal/goboard/handlers/user" ) const goBoardVer = "0.0.4" @@ -35,23 +40,6 @@ type Config struct { AdminToken string `yaml:"AdminToken"` } -// RESTEndpointHandler defines a handler function for a REST Endpoint -type RESTEndpointHandler func(http.ResponseWriter, *http.Request) - -// SupportedOp Defines a REST endpoint with its path, method and endpoint -type SupportedOp struct { - PathBase string - RestPath string - Method string - handler RESTEndpointHandler -} - -// GoBoardHandler Base Class for endpoint handlers -type GoBoardHandler struct { - Db *bolt.DB - supportedOps []SupportedOp -} - // Command line arguments management var configFilePath string var showHelp bool @@ -130,27 +118,27 @@ func main() { r := mainRouter // Backend operations - backendHandler := NewBackendHandler(config.MaxHistorySize, config.BackendTimeZone) + backendHandler := backend.NewBackendHandler(config.MaxHistorySize, config.BackendTimeZone) backendHandler.Db = db - for _, op := range backendHandler.supportedOps { + for _, op := range backendHandler.SupportedOps { r.Handle(op.RestPath, backendHandler).Methods(op.Method) } // User operations - userHandler := NewUserHandler(config.CookieDuration) + userHandler := user.NewUserHandler(config.CookieDuration) userHandler.Db = db - for _, op := range userHandler.supportedOps { + for _, op := range userHandler.SupportedOps { r.Handle(op.RestPath, userHandler).Methods(op.Method) } // Admin operations - adminHandler := NewAdminHandler(config.AdminToken) + adminHandler := admin.NewAdminHandler(config.AdminToken) adminHandler.Db = db - for _, op := range adminHandler.supportedOps { + for _, op := range adminHandler.SupportedOps { r.Handle(op.RestPath, adminHandler).Methods(op.Method) } - templateHandler := NewTemplateHandler() + templateHandler := template.NewTemplateHandler() // Swagger operations if len(config.SwaggerPath) > 0 { @@ -164,7 +152,7 @@ func main() { } else { templateHandler.SetSwaggerBaseDir(realPath) swaggerOp := templateHandler.GetSwaggerOp() - r.HandleFunc(swaggerOp.RestPath, swaggerOp.handler).Methods(swaggerOp.Method) + r.HandleFunc(swaggerOp.RestPath, swaggerOp.Handler).Methods(swaggerOp.Method) r.PathPrefix("/swagger/").Handler(http.StripPrefix("/swagger/", http.FileServer(http.Dir(realPath)))) r.Handle("/swagger", http.RedirectHandler("/swagger/", http.StatusMovedPermanently)) @@ -181,7 +169,7 @@ func main() { } else if _, err := os.Stat(strings.Join([]string{realPath, "/index.html"}, "")); os.IsNotExist(err) { log.Println(strings.Join([]string{realPath, "/index.html"}, ""), "Not found: Disabling webui capabilities") } else { - templateHandler.setWebUIBaseDir(realPath) + templateHandler.SetWebUIBaseDir(realPath) r.PathPrefix("/webui/").Handler(http.StripPrefix("/webui/", http.FileServer(http.Dir(realPath)))) r.Handle("/webui", http.RedirectHandler("/webui/", http.StatusMovedPermanently)) r.Handle("/", http.RedirectHandler("/webui/", http.StatusMovedPermanently)) diff --git a/adminhandler.go b/handlers/admin/adminhandler.go similarity index 85% rename from adminhandler.go rename to handlers/admin/adminhandler.go index 615140e..31c81a3 100644 --- a/adminhandler.go +++ b/handlers/admin/adminhandler.go @@ -1,4 +1,4 @@ -package main +package admin import ( "encoding/json" @@ -8,6 +8,7 @@ import ( "strconv" "strings" + "github.com/dguihal/goboard/handlers" goboardbackend "github.com/dguihal/goboard/internal/backend" goboardcookie "github.com/dguihal/goboard/internal/cookie" goboarduser "github.com/dguihal/goboard/internal/user" @@ -19,7 +20,7 @@ const tokenWarnLen int = 12 // AdminHandler represents the handler of admin URLs type AdminHandler struct { - GoBoardHandler + handlers.GoBoardHandler adminToken string } @@ -28,10 +29,10 @@ type AdminHandler struct { func NewAdminHandler(adminToken string) (a *AdminHandler) { a = &AdminHandler{} - a.supportedOps = []SupportedOp{ - {"/admin/user/", "/admin/user/{login}", "DELETE", a.deleteUser}, // Delete a user - {"/admin/user/", "/admin/user/{login}", "GET", a.getUser}, // Get a user info - {"/admin/post/", "/admin/post/{id}", "DELETE", a.deletePost}, // Delete a post + a.SupportedOps = []handlers.SupportedOp{ + {PathBase: "/admin/user/", RestPath: "/admin/user/{login}", Method: "DELETE", Handler: a.deleteUser}, // Delete a user + {PathBase: "/admin/user/", RestPath: "/admin/user/{login}", Method: "GET", Handler: a.getUser}, // Get a user info + {PathBase: "/admin/post/", RestPath: "/admin/post/{id}", Method: "DELETE", Handler: a.deletePost}, // Delete a post } if len(adminToken) <= tokenMinLen { @@ -51,10 +52,10 @@ func (a *AdminHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - for _, op := range a.supportedOps { + for _, op := range a.SupportedOps { if r.Method == op.Method && strings.HasPrefix(r.URL.Path, op.PathBase) { // Call specific handling method - op.handler(w, r) + op.Handler(w, r) return } } diff --git a/backendhandler.go b/handlers/backend/backendhandler.go similarity index 90% rename from backendhandler.go rename to handlers/backend/backendhandler.go index 5b5e1f7..aa31fab 100644 --- a/backendhandler.go +++ b/handlers/backend/backendhandler.go @@ -1,4 +1,4 @@ -package main +package backend import ( "bytes" @@ -10,6 +10,7 @@ import ( "strings" "time" + "github.com/dguihal/goboard/handlers" goboardbackend "github.com/dguihal/goboard/internal/backend" goboardcookie "github.com/dguihal/goboard/internal/cookie" "github.com/gorilla/mux" @@ -31,7 +32,7 @@ var knownHeaders = map[string]string{ // BackendHandler represents the handler of backend URLs type BackendHandler struct { - GoBoardHandler + handlers.GoBoardHandler historySize int } @@ -40,12 +41,12 @@ type BackendHandler struct { func NewBackendHandler(historySize int, frontLocation string) (b *BackendHandler) { b = &BackendHandler{} - b.supportedOps = []SupportedOp{ - {"/backend", "/backend", "GET", b.getBackend}, // Get backend (in xml) - {"/backend", "/backend/{format}", "GET", b.getBackend}, // Get backend (in specific format) - {"/post", "/post", "POST", b.post}, // Post new message - {"/post/", "/post/{id}", "GET", b.getPost}, // Get a specific message (in xml) - {"/post/", "/post/{id}/{format}", "GET", b.getPost}, // Get a specific message (in specific format) + b.SupportedOps = []handlers.SupportedOp{ + {PathBase: "/backend", RestPath: "/backend", Method: "GET", Handler: b.getBackend}, // Get backend (in xml) + {PathBase: "/backend", RestPath: "/backend/{format}", Method: "GET", Handler: b.getBackend}, // Get backend (in specific format) + {PathBase: "/post", RestPath: "/post", Method: "POST", Handler: b.post}, // Post new message + {PathBase: "/post/", RestPath: "/post/{id}", Method: "GET", Handler: b.getPost}, // Get a specific message (in xml) + {PathBase: "/post/", RestPath: "/post/{id}/{format}", Method: "GET", Handler: b.getPost}, // Get a specific message (in specific format) } if location, err := time.LoadLocation(frontLocation); err == nil { @@ -61,10 +62,10 @@ func NewBackendHandler(historySize int, frontLocation string) (b *BackendHandler func (b *BackendHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - for _, op := range b.supportedOps { + for _, op := range b.SupportedOps { if r.Method == op.Method && strings.HasPrefix(r.URL.Path, op.PathBase) { // Call specific handling method - op.handler(w, r) + op.Handler(w, r) return } } diff --git a/handlers/goboardhandler.go b/handlers/goboardhandler.go new file mode 100644 index 0000000..77b9d23 --- /dev/null +++ b/handlers/goboardhandler.go @@ -0,0 +1,24 @@ +package handlers + +import ( + "net/http" + + bolt "go.etcd.io/bbolt" +) + +// RESTEndpointHandler defines a handler function for a REST Endpoint +type RESTEndpointHandler func(http.ResponseWriter, *http.Request) + +// SupportedOp Defines a REST endpoint with its path, method and endpoint +type SupportedOp struct { + PathBase string + RestPath string + Method string + Handler RESTEndpointHandler +} + +// GoBoardHandler Base Class for endpoint handlers +type GoBoardHandler struct { + Db *bolt.DB + SupportedOps []SupportedOp +} diff --git a/templatehandler.go b/handlers/template/templatehandler.go similarity index 86% rename from templatehandler.go rename to handlers/template/templatehandler.go index d44b9f2..410a3f2 100644 --- a/templatehandler.go +++ b/handlers/template/templatehandler.go @@ -1,10 +1,12 @@ -package main +package template import ( "bytes" "html/template" "net/http" "os" + + "github.com/dguihal/goboard/handlers" ) // TemplateHandler represents the handler of swagger URLs @@ -33,12 +35,12 @@ func (s *TemplateHandler) SetSwaggerBaseDir(swaggerBaseDir string) { } // GetSwaggerOp GET swagger content -func (s *TemplateHandler) GetSwaggerOp() SupportedOp { - return SupportedOp{"/swagger/swagger.yaml", "/swagger/swagger.yaml", "GET", s.ServeSwagger} +func (s *TemplateHandler) GetSwaggerOp() handlers.SupportedOp { + return handlers.SupportedOp{PathBase: "/swagger/swagger.yaml", RestPath: "/swagger/swagger.yaml", Method: "GET", Handler: s.ServeSwagger} } // SetSwaggerBaseDir configures webui base dir -func (s *TemplateHandler) setWebUIBaseDir(webuiBaseDir string) { +func (s *TemplateHandler) SetWebUIBaseDir(webuiBaseDir string) { s.webuiBaseDir = http.Dir(webuiBaseDir) s.webuiBaseDirSet = true diff --git a/userhandler.go b/handlers/user/userhandler.go similarity index 86% rename from userhandler.go rename to handlers/user/userhandler.go index f99ab63..09a5fb9 100644 --- a/userhandler.go +++ b/handlers/user/userhandler.go @@ -1,4 +1,4 @@ -package main +package user import ( "encoding/json" @@ -7,13 +7,14 @@ import ( "os" "strings" + "github.com/dguihal/goboard/handlers" goboardcookie "github.com/dguihal/goboard/internal/cookie" goboarduser "github.com/dguihal/goboard/internal/user" ) // UserHandler represents the handler of user URLs type UserHandler struct { - GoBoardHandler + handlers.GoBoardHandler cookieDurationD int logger *log.Logger @@ -25,11 +26,11 @@ func NewUserHandler(cookieDuration int) (u *UserHandler) { u.logger = log.New(os.Stdout, "", log.Ldate|log.Ltime|log.Lshortfile) - u.supportedOps = []SupportedOp{ - {"/user/add", "/user/add", "POST", u.addUser}, // Add a user - {"/user/login", "/user/login", "POST", u.authUser}, // Authenticate a user - {"/user/logout", "/user/logout", "GET", u.unAuthUser}, // Unauthenticate a user - {"/user/whoami", "/user/whoami", "GET", u.whoAmI}, // Get self account infos + u.SupportedOps = []handlers.SupportedOp{ + {PathBase: "/user/add", RestPath: "/user/add", Method: "POST", Handler: u.addUser}, // Add a user + {PathBase: "/user/login", RestPath: "/user/login", Method: "POST", Handler: u.authUser}, // Authenticate a user + {PathBase: "/user/logout", RestPath: "/user/logout", Method: "GET", Handler: u.unAuthUser}, // Unauthenticate a user + {PathBase: "/user/whoami", RestPath: "/user/whoami", Method: "GET", Handler: u.whoAmI}, // Get self account infos } u.cookieDurationD = cookieDuration @@ -39,10 +40,10 @@ func NewUserHandler(cookieDuration int) (u *UserHandler) { func (u *UserHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - for _, op := range u.supportedOps { + for _, op := range u.SupportedOps { if r.Method == op.Method && strings.HasPrefix(r.URL.Path, op.PathBase) { // Call specific handling method - op.handler(w, r) + op.Handler(w, r) return } } diff --git a/handlers/user/userhandler_test.go b/handlers/user/userhandler_test.go new file mode 100644 index 0000000..71dd9ec --- /dev/null +++ b/handlers/user/userhandler_test.go @@ -0,0 +1,32 @@ +package user + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +// Exemple de test pour le handler UserHandler +func TestUserHandler(t *testing.T) { + // Crée une requête HTTP GET vers "/user" + req := httptest.NewRequest(http.MethodGet, "/user/whoami", nil) + + // Crée un enregistreur de réponse pour capturer la réponse du handler + rr := httptest.NewRecorder() + + userHandler := NewUserHandler(30) + + // Appelle le handler avec la requête et l'enregistreur de réponse + userHandler.ServeHTTP(rr, req) + + // Vérifie que le code de statut HTTP est 403 Forbidden + if status := rr.Code; status != http.StatusForbidden { + t.Errorf("Code de statut incorrect : obtenu %v, attendu %v", status, http.StatusForbidden) + } + + // Vérifie que le corps de la réponse contient le texte attendu + expected := "You need to be authenticated" + if rr.Body.String() != expected { + t.Errorf("Corps de la réponse incorrect : obtenu %v, attendu %v", rr.Body.String(), expected) + } +} From 4e62ff43dfa8db532c4c7a330b71c9068d1d316d Mon Sep 17 00:00:00 2001 From: Damien Guihal Date: Tue, 13 May 2025 14:09:45 +0200 Subject: [PATCH 09/12] Refactor code --- goboard.go | 8 +-- handlers/goboardhandler.go | 4 +- handlers/user/userhandler.go | 4 +- handlers/user/userhandler_test.go | 113 +++++++++++++++++++++++++++--- internal/backend/backend.go | 18 ++--- internal/cookie/cookie.go | 22 +++--- internal/user/user.go | 18 ++--- 7 files changed, 140 insertions(+), 47 deletions(-) diff --git a/goboard.go b/goboard.go index 085763a..a1b824a 100644 --- a/goboard.go +++ b/goboard.go @@ -14,7 +14,7 @@ import ( "github.com/gorilla/handlers" "github.com/gorilla/mux" - bolt "go.etcd.io/bbolt" + "go.etcd.io/bbolt" "gopkg.in/yaml.v2" admin "github.com/dguihal/goboard/handlers/admin" @@ -108,7 +108,7 @@ func main() { } // Open database - db, err := bolt.Open(config.GoBoardDBFile, config.GoBoardDBFileMode, &bolt.Options{Timeout: 1 * time.Second}) + db, err := bbolt.Open(config.GoBoardDBFile, config.GoBoardDBFileMode, &bbolt.Options{Timeout: 1 * time.Second}) if err != nil { log.Fatalf("error: %v", err) } @@ -216,7 +216,7 @@ func main() { //https://github.com/golang/go/wiki/LearnServerProgramming //https://astaxie.gitbooks.io/build-web-application-with-golang/ -//https://github.com/boltdb/bolt : Backend -//https://github.com/skyec/boltdb-server/blob/master/server.go +//https://github.com/etcd-io/bbolt : Backend +//https://github.com/skyec/boltdb-server : Example //https://blog.golang.org/error-handling-and-go diff --git a/handlers/goboardhandler.go b/handlers/goboardhandler.go index 77b9d23..e4747b9 100644 --- a/handlers/goboardhandler.go +++ b/handlers/goboardhandler.go @@ -3,7 +3,7 @@ package handlers import ( "net/http" - bolt "go.etcd.io/bbolt" + "go.etcd.io/bbolt" ) // RESTEndpointHandler defines a handler function for a REST Endpoint @@ -19,6 +19,6 @@ type SupportedOp struct { // GoBoardHandler Base Class for endpoint handlers type GoBoardHandler struct { - Db *bolt.DB + Db *bbolt.DB SupportedOps []SupportedOp } diff --git a/handlers/user/userhandler.go b/handlers/user/userhandler.go index 09a5fb9..b5ea0f1 100644 --- a/handlers/user/userhandler.go +++ b/handlers/user/userhandler.go @@ -71,7 +71,7 @@ func (u *UserHandler) addUser(w http.ResponseWriter, r *http.Request) { // User created : Send him a cookie if cookie, err := goboardcookie.ForUser(u.Db, login, u.cookieDurationD); err == nil { http.SetCookie(w, &cookie) - w.WriteHeader(http.StatusOK) + w.WriteHeader(http.StatusCreated) return } } else { @@ -130,8 +130,6 @@ func (u *UserHandler) authUser(w http.ResponseWriter, r *http.Request) { } } - u.logger.Println(cookie) - u.logger.Println(err) if err == nil { http.SetCookie(w, &cookie) w.WriteHeader(http.StatusOK) diff --git a/handlers/user/userhandler_test.go b/handlers/user/userhandler_test.go index 71dd9ec..0a6ac48 100644 --- a/handlers/user/userhandler_test.go +++ b/handlers/user/userhandler_test.go @@ -3,30 +3,125 @@ package user import ( "net/http" "net/http/httptest" + "net/url" + "os" + "strings" "testing" + + "go.etcd.io/bbolt" ) -// Exemple de test pour le handler UserHandler -func TestUserHandler(t *testing.T) { - // Crée une requête HTTP GET vers "/user" +type UserTestEnv struct { + DB *bbolt.DB +} + +// setupTestEnv create temporary test env +func setupTestEnv(t *testing.T) (*UserTestEnv, func()) { + tmpFile := "test_auth.db" + db, err := bbolt.Open(tmpFile, 0600, nil) + if err != nil { + t.Fatalf("Erreur d'ouverture BoltDB : %v", err) + } + + // Initialise les buckets nécessaires + err = db.Update(func(tx *bbolt.Tx) error { + _, err := tx.CreateBucketIfNotExists([]byte("users")) + return err + }) + if err != nil { + t.Fatalf("Erreur d'init bucket : %v", err) + } + + env := &UserTestEnv{DB: db} + + // Fonction de nettoyage + cleanup := func() { + db.Close() + os.Remove(tmpFile) + } + + return env, cleanup +} + +func TestUserHandlerWhoami403(t *testing.T) { + + env, cleanup := setupTestEnv(t) + defer cleanup() + req := httptest.NewRequest(http.MethodGet, "/user/whoami", nil) - // Crée un enregistreur de réponse pour capturer la réponse du handler + // Response recorder to capture handler's response rr := httptest.NewRecorder() userHandler := NewUserHandler(30) + userHandler.Db = env.DB - // Appelle le handler avec la requête et l'enregistreur de réponse userHandler.ServeHTTP(rr, req) - // Vérifie que le code de statut HTTP est 403 Forbidden if status := rr.Code; status != http.StatusForbidden { - t.Errorf("Code de statut incorrect : obtenu %v, attendu %v", status, http.StatusForbidden) + t.Errorf("Wrong response status code: got %v, expected %v", status, http.StatusForbidden) } - // Vérifie que le corps de la réponse contient le texte attendu expected := "You need to be authenticated" if rr.Body.String() != expected { - t.Errorf("Corps de la réponse incorrect : obtenu %v, attendu %v", rr.Body.String(), expected) + t.Errorf("Wrong response body: got %v, expected %v", rr.Body.String(), expected) + } +} + +func TestUserHandlerRegisterLogin(t *testing.T) { + + env, cleanup := setupTestEnv(t) + defer cleanup() + + // Step 1: Create user + form := url.Values{} + form.Add("login", "alice") + form.Add("password", "secure123") + + userAddReq := httptest.NewRequest(http.MethodPost, "/user/add", strings.NewReader(form.Encode())) + userAddReq.Header.Set("Content-Type", "application/x-www-form-urlencoded") + userAddRes := httptest.NewRecorder() + + userHandler := NewUserHandler(30) + userHandler.Db = env.DB + + userHandler.ServeHTTP(userAddRes, userAddReq) + + if userAddRes.Code != http.StatusCreated { + t.Fatalf("User creation failed: %d - %s", userAddRes.Code, userAddRes.Body.String()) + } + + // Step 2: Authenticate user + loginReq := httptest.NewRequest(http.MethodPost, "/user/login", strings.NewReader(form.Encode())) + loginReq.Header.Set("Content-Type", "application/x-www-form-urlencoded") + loginRes := httptest.NewRecorder() + + userHandler.ServeHTTP(loginRes, loginReq) + + if loginRes.Code != http.StatusOK { + t.Fatalf("Authentification failed: %d - %s", loginRes.Code, loginRes.Body.String()) } + + // Check cookie in response + cookies := loginRes.Result().Cookies() + found := false + + if len(cookies) == 0 { + t.Fatalf("No cookies returned") + } + + for _, cookie := range cookies { + if cookie.Name == "goboard_id" { + found = true + if cookie.Value == "" { + t.Errorf("Cookie 'goboard_id' present but empty") + } + break + } + } + + if !found { + t.Errorf("Cookie 'goboard_id' not found") + } + } diff --git a/internal/backend/backend.go b/internal/backend/backend.go index 4600915..9c2f1c0 100644 --- a/internal/backend/backend.go +++ b/internal/backend/backend.go @@ -6,7 +6,7 @@ import ( "time" goboardutils "github.com/dguihal/goboard/internal/utils" - bolt "go.etcd.io/bbolt" + "go.etcd.io/bbolt" ) const backendBucketName string = "Backend" @@ -47,9 +47,9 @@ type Board struct { } // DeletePost is a method for deleting a post from the history -func DeletePost(db *bolt.DB, id uint64) (err error) { +func DeletePost(db *bbolt.DB, id uint64) (err error) { - err = db.Update(func(tx *bolt.Tx) error { + err = db.Update(func(tx *bbolt.Tx) error { b, err := tx.CreateBucketIfNotExists([]byte(backendBucketName)) if err != nil { return err @@ -61,11 +61,11 @@ func DeletePost(db *bolt.DB, id uint64) (err error) { } // GetBackend returns the last posts from the history -func GetBackend(db *bolt.DB, historySize int, last uint64) (posts []Post, err error) { +func GetBackend(db *bbolt.DB, historySize int, last uint64) (posts []Post, err error) { posts = make([]Post, historySize) - err = db.View(func(tx *bolt.Tx) error { + err = db.View(func(tx *bbolt.Tx) error { b := tx.Bucket([]byte(backendBucketName)) if b == nil { @@ -95,11 +95,11 @@ func GetBackend(db *bolt.DB, historySize int, last uint64) (posts []Post, err er } // GetPost returns a post from its id -func GetPost(db *bolt.DB, id uint64) (post Post, err error) { +func GetPost(db *bbolt.DB, id uint64) (post Post, err error) { post = Post{} - err = db.View(func(tx *bolt.Tx) error { + err = db.View(func(tx *bbolt.Tx) error { b := tx.Bucket([]byte(backendBucketName)) if b == nil { @@ -120,9 +120,9 @@ func GetPost(db *bolt.DB, id uint64) (post Post, err error) { } // PostMessage adds a new message to the history -func PostMessage(db *bolt.DB, post Post) (postID uint64, err error) { +func PostMessage(db *bbolt.DB, post Post) (postID uint64, err error) { - err = db.Update(func(tx *bolt.Tx) error { + err = db.Update(func(tx *bbolt.Tx) error { b, err := tx.CreateBucketIfNotExists([]byte(backendBucketName)) if err != nil { return err diff --git a/internal/cookie/cookie.go b/internal/cookie/cookie.go index 8bdfb93..b4f17d8 100644 --- a/internal/cookie/cookie.go +++ b/internal/cookie/cookie.go @@ -8,7 +8,7 @@ import ( "time" "github.com/dchest/uniuri" - bolt "go.etcd.io/bbolt" + "go.etcd.io/bbolt" ) const goboardCookieName string = "goboard_id" @@ -36,7 +36,7 @@ type UserCookieError struct { func (e *UserCookieError) Error() string { return e.error.Error() } // ForUser returns a valid cookie (already existing or new) for a user -func ForUser(db *bolt.DB, login string, cookieDurationD int) (cookie http.Cookie, err error) { +func ForUser(db *bbolt.DB, login string, cookieDurationD int) (cookie http.Cookie, err error) { if cookie, err = fetchCookieForUser(db, login); err != nil { if ucerr, ok := err.(*UserCookieError); ok { @@ -51,7 +51,7 @@ func ForUser(db *bolt.DB, login string, cookieDurationD int) (cookie http.Cookie } // createAndStoreCookie creates a new cookie and stores it in database -func createAndStoreCookie(db *bolt.DB, login string, cookieDurationD int) (cookie http.Cookie, err error) { +func createAndStoreCookie(db *bbolt.DB, login string, cookieDurationD int) (cookie http.Cookie, err error) { expiration := time.Now().Add(time.Duration(cookieDurationD) * 24 * time.Hour) cookie = http.Cookie{ @@ -72,7 +72,7 @@ func createAndStoreCookie(db *bolt.DB, login string, cookieDurationD int) (cooki return cookie, ucerr } - err = db.Update(func(tx *bolt.Tx) error { + err = db.Update(func(tx *bbolt.Tx) error { b, err := tx.CreateBucketIfNotExists([]byte(usersCookieBucketName)) if err != nil { ucerr := &UserCookieError{error: err, ErrCode: DatabaseError} @@ -96,9 +96,9 @@ func createAndStoreCookie(db *bolt.DB, login string, cookieDurationD int) (cooki } // DeleteCookiesForUser delete stored cookies for user -func DeleteCookiesForUser(db *bolt.DB, login string) (err error) { +func DeleteCookiesForUser(db *bbolt.DB, login string) (err error) { - err = db.Batch(func(tx *bolt.Tx) error { + err = db.Batch(func(tx *bbolt.Tx) error { b, err := tx.CreateBucketIfNotExists([]byte(usersCookieBucketName)) if err != nil { uerr := &UserCookieError{error: err, ErrCode: DatabaseError} @@ -126,11 +126,11 @@ func DeleteCookiesForUser(db *bolt.DB, login string) (err error) { } // fetchCookieForUser retreive a valid stored cookie for a user (if any in database) -func fetchCookieForUser(db *bolt.DB, login string) (cookie http.Cookie, err error) { +func fetchCookieForUser(db *bbolt.DB, login string) (cookie http.Cookie, err error) { cookie = http.Cookie{} - err = db.Update(func(tx *bolt.Tx) error { + err = db.Update(func(tx *bbolt.Tx) error { // Find if non expired cookie already exists b, err := tx.CreateBucketIfNotExists([]byte(usersCookieBucketName)) if err != nil { @@ -174,7 +174,7 @@ func fetchCookieForUser(db *bolt.DB, login string) (cookie http.Cookie, err erro } // LoginForCookie get the user associated with a cookie -func LoginForCookie(db *bolt.DB, cookie *http.Cookie) (login string, err error) { +func LoginForCookie(db *bbolt.DB, cookie *http.Cookie) (login string, err error) { var uc = UserCookie{} login = "" @@ -182,7 +182,7 @@ func LoginForCookie(db *bolt.DB, cookie *http.Cookie) (login string, err error) return } - err = db.View(func(tx *bolt.Tx) error { + err = db.View(func(tx *bbolt.Tx) error { b := tx.Bucket([]byte(usersCookieBucketName)) @@ -201,7 +201,7 @@ func LoginForCookie(db *bolt.DB, cookie *http.Cookie) (login string, err error) login = uc.Login if err == nil && uc.Cookie.Expires.Before(time.Now()) { - err = db.Update(func(tx *bolt.Tx) error { + err = db.Update(func(tx *bbolt.Tx) error { b := tx.Bucket([]byte(usersCookieBucketName)) if b == nil { return nil diff --git a/internal/user/user.go b/internal/user/user.go index dd30b39..81dfb83 100644 --- a/internal/user/user.go +++ b/internal/user/user.go @@ -6,7 +6,7 @@ import ( "fmt" "time" - bolt "go.etcd.io/bbolt" + "go.etcd.io/bbolt" "golang.org/x/crypto/bcrypt" ) @@ -33,9 +33,9 @@ type User struct { HashedPassword []byte `json:"HashedPassword,omitempty"` } -func AddUser(db *bolt.DB, login string, password string) (uerr error) { +func AddUser(db *bbolt.DB, login string, password string) (uerr error) { - db.Batch(func(tx *bolt.Tx) error { + db.Batch(func(tx *bbolt.Tx) error { b, err := tx.CreateBucketIfNotExists([]byte(usersBucketName)) if err != nil { uerr = &Error{error: err, ErrCode: DatabaseError} @@ -75,9 +75,9 @@ func AddUser(db *bolt.DB, login string, password string) (uerr error) { return } -func AuthUser(db *bolt.DB, login string, password string) (uerr error) { +func AuthUser(db *bbolt.DB, login string, password string) (uerr error) { - db.View(func(tx *bolt.Tx) error { + db.View(func(tx *bbolt.Tx) error { b := tx.Bucket([]byte(usersBucketName)) var v []byte @@ -107,9 +107,9 @@ func AuthUser(db *bolt.DB, login string, password string) (uerr error) { return } -func DeleteUser(db *bolt.DB, login string) (uerr error) { +func DeleteUser(db *bbolt.DB, login string) (uerr error) { - db.Batch(func(tx *bolt.Tx) error { + db.Batch(func(tx *bbolt.Tx) error { b, err := tx.CreateBucketIfNotExists([]byte(usersBucketName)) if err != nil { uerr = &Error{error: err, ErrCode: DatabaseError} @@ -134,9 +134,9 @@ func DeleteUser(db *bolt.DB, login string) (uerr error) { return } -func GetUser(db *bolt.DB, login string) (user User, uerr error) { +func GetUser(db *bbolt.DB, login string) (user User, uerr error) { - db.View(func(tx *bolt.Tx) error { + db.View(func(tx *bbolt.Tx) error { b := tx.Bucket([]byte(usersBucketName)) var v []byte From 2db938bacf4fd49d7d492b399a1800ac45af9f44 Mon Sep 17 00:00:00 2001 From: Damien Guihal Date: Tue, 13 May 2025 15:20:19 +0200 Subject: [PATCH 10/12] Fix docker build --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index 4869f54..d8a611c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,6 +13,7 @@ RUN go mod download COPY *.go ./ COPY internal ./internal/ +COPY handlers ./handlers/ RUN go build -o /goboard From ce1baf01f3b688c0733674bfab51a17afee7af31 Mon Sep 17 00:00:00 2001 From: Damien Guihal Date: Wed, 6 Aug 2025 17:41:59 +0200 Subject: [PATCH 11/12] Code cleanup --- internal/backend/backendsanitizer.go | 8 ++++---- internal/user/user.go | 9 ++++----- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/internal/backend/backendsanitizer.go b/internal/backend/backendsanitizer.go index 56ea612..f0cd1d9 100644 --- a/internal/backend/backendsanitizer.go +++ b/internal/backend/backendsanitizer.go @@ -84,10 +84,10 @@ L: for { tt := z.Next() - switch { - case tt == html.ErrorToken: + switch tt { + case html.ErrorToken: break L - case tt == html.StartTagToken: + case html.StartTagToken: tn, hasAttrs := z.TagName() tnStr := string(tn) @@ -122,7 +122,7 @@ L: txt: sanitizeChars(string(z.Raw())), tokenType: html.TextToken}) } - case tt == html.EndTagToken: + case html.EndTagToken: tn, _ := z.TagName() tnStr := string(tn) diff --git a/internal/user/user.go b/internal/user/user.go index 81dfb83..8b4fec3 100644 --- a/internal/user/user.go +++ b/internal/user/user.go @@ -121,11 +121,10 @@ func DeleteUser(db *bbolt.DB, login string) (uerr error) { if v == nil { // User does not exists uerr = &Error{error: fmt.Errorf("User does not exists"), ErrCode: UserDoesNotExistsError} return uerr - } else { - if err = b.Delete([]byte(login)); err != nil { - uerr = &Error{error: err, ErrCode: DatabaseError} - return err - } + } + if err = b.Delete([]byte(login)); err != nil { + uerr = &Error{error: err, ErrCode: DatabaseError} + return err } return nil From b0d79ddad206fc185f15536c6106df2f9b86bc2f Mon Sep 17 00:00:00 2001 From: Damien Guihal Date: Wed, 6 Aug 2025 17:42:19 +0200 Subject: [PATCH 12/12] Code cleanup --- handlers/user/userhandler.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/handlers/user/userhandler.go b/handlers/user/userhandler.go index b5ea0f1..b50fc09 100644 --- a/handlers/user/userhandler.go +++ b/handlers/user/userhandler.go @@ -52,13 +52,14 @@ func (u *UserHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (u *UserHandler) addUser(w http.ResponseWriter, r *http.Request) { r.ParseForm() - var login, passwd string = "", "" + var login string if login = r.FormValue("login"); len(login) == 0 { w.WriteHeader(http.StatusBadRequest) w.Write([]byte("Login can't be empty")) return } + var passwd string if passwd = r.FormValue("password"); len(passwd) == 0 { w.WriteHeader(http.StatusBadRequest) w.Write([]byte("Password can't be empty")) @@ -69,7 +70,8 @@ func (u *UserHandler) addUser(w http.ResponseWriter, r *http.Request) { if err == nil { // User created : Send him a cookie - if cookie, err := goboardcookie.ForUser(u.Db, login, u.cookieDurationD); err == nil { + var cookie http.Cookie + if cookie, err = goboardcookie.ForUser(u.Db, login, u.cookieDurationD); err == nil { http.SetCookie(w, &cookie) w.WriteHeader(http.StatusCreated) return @@ -90,13 +92,14 @@ func (u *UserHandler) addUser(w http.ResponseWriter, r *http.Request) { func (u *UserHandler) authUser(w http.ResponseWriter, r *http.Request) { r.ParseForm() - var login, passwd string = "", "" + var login string if login = r.FormValue("login"); len(login) == 0 { w.WriteHeader(http.StatusBadRequest) w.Write([]byte("Login can't be empty")) return } + var passwd string if passwd = r.FormValue("password"); len(passwd) == 0 { w.WriteHeader(http.StatusBadRequest) w.Write([]byte("Password can't be empty"))