Skip to content

Commit 434a433

Browse files
author
mirkobrombin
committed
chore: plugin system for custom middlewares
1 parent e89d760 commit 434a433

File tree

11 files changed

+263
-44
lines changed

11 files changed

+263
-44
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ Each site configuration is represented by a JSON file and meets the following st
121121
"port": 8080,
122122
"root_directory": "/path/to/root",
123123
"custom_headers": {
124-
"X-Custom-Header": "Value"
124+
"X-Domain-Name": "example.com"
125125
},
126126
"proxy_pass": "http://localhost:3000",
127127
"ssl": {

cmd/goup/main.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,15 @@ package main
22

33
import (
44
"github.com/mirkobrombin/goup/internal/cli"
5+
"github.com/mirkobrombin/goup/internal/plugin"
6+
"github.com/mirkobrombin/goup/plugins"
57
)
68

79
func main() {
10+
pluginManager := plugin.GetPluginManagerInstance()
11+
12+
// Register your plugins here:
13+
pluginManager.Register(&plugins.CustomHeaderPlugin{})
14+
815
cli.Execute()
916
}

internal/cli/cli.go

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"strings"
88

99
"github.com/mirkobrombin/goup/internal/config"
10+
"github.com/mirkobrombin/goup/internal/plugin"
1011
"github.com/mirkobrombin/goup/internal/server"
1112
"github.com/spf13/cobra"
1213
)
@@ -33,8 +34,8 @@ func init() {
3334
rootCmd.AddCommand(startCmd)
3435
rootCmd.AddCommand(validateCmd)
3536
rootCmd.AddCommand(listCmd)
37+
rootCmd.AddCommand(pluginsCmd)
3638

37-
// Add the --tui flag to the start command
3839
startCmd.Flags().BoolVarP(&tuiMode, "tui", "t", false, "Enable TUI mode")
3940
}
4041

@@ -203,3 +204,22 @@ func list(cmd *cobra.Command, args []string) {
203204
fmt.Printf("- %s (port %d)\n", conf.Domain, conf.Port)
204205
}
205206
}
207+
208+
var pluginsCmd = &cobra.Command{
209+
Use: "plugins",
210+
Short: "List all registered plugins",
211+
Run: func(cmd *cobra.Command, args []string) {
212+
pluginManager := plugin.GetPluginManagerInstance()
213+
plugins := pluginManager.GetRegisteredPlugins()
214+
215+
if len(plugins) == 0 {
216+
fmt.Println("No plugins registered.")
217+
return
218+
}
219+
220+
fmt.Println("Registered plugins:")
221+
for _, name := range plugins {
222+
fmt.Printf("- %s\n", name)
223+
}
224+
},
225+
}

internal/plugin/plugin.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package plugin
2+
3+
import (
4+
"sync"
5+
6+
"github.com/mirkobrombin/goup/internal/server/middleware"
7+
)
8+
9+
// Plugin defines the interface for GoUP plugins.
10+
type Plugin interface {
11+
Name() string
12+
Init(mwManager *middleware.MiddlewareManager) error
13+
}
14+
15+
// PluginManager manages loading and initialization of plugins.
16+
type PluginManager struct {
17+
plugins []Plugin
18+
mu sync.Mutex
19+
}
20+
21+
// Singleton PluginManagerInstance.
22+
var pluginManagerInstance *PluginManager
23+
var once sync.Once
24+
25+
// GetPluginManagerInstance returns the singleton instance of PluginManager.
26+
func GetPluginManagerInstance() *PluginManager {
27+
once.Do(func() {
28+
pluginManagerInstance = &PluginManager{
29+
plugins: []Plugin{},
30+
}
31+
})
32+
return pluginManagerInstance
33+
}
34+
35+
// Register registers a new plugin.
36+
func (pm *PluginManager) Register(plugin Plugin) {
37+
pm.mu.Lock()
38+
defer pm.mu.Unlock()
39+
pm.plugins = append(pm.plugins, plugin)
40+
}
41+
42+
// InitPlugins initializes all registered plugins.
43+
func (pm *PluginManager) InitPlugins(mwManager *middleware.MiddlewareManager) error {
44+
pm.mu.Lock()
45+
defer pm.mu.Unlock()
46+
47+
for _, plugin := range pm.plugins {
48+
if err := plugin.Init(mwManager); err != nil {
49+
return err
50+
}
51+
}
52+
return nil
53+
}
54+
55+
// GetRegisteredPlugins returns the registered plugins.
56+
func (pm *PluginManager) GetRegisteredPlugins() []string {
57+
pm.mu.Lock()
58+
defer pm.mu.Unlock()
59+
60+
names := make([]string, len(pm.plugins))
61+
for i, plugin := range pm.plugins {
62+
names[i] = plugin.Name()
63+
}
64+
return names
65+
}

internal/plugin/plugin_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package plugin
2+
3+
import (
4+
"net/http"
5+
"net/http/httptest"
6+
"testing"
7+
8+
"github.com/mirkobrombin/goup/internal/server/middleware"
9+
"github.com/mirkobrombin/goup/plugins"
10+
)
11+
12+
func TestPluginManager(t *testing.T) {
13+
pluginManager := GetPluginManagerInstance()
14+
15+
pluginManager.Register(&plugins.CustomHeaderPlugin{})
16+
17+
mwManager := middleware.NewMiddlewareManager()
18+
err := pluginManager.InitPlugins(mwManager)
19+
if err != nil {
20+
t.Fatalf("Failed to initialize plugin: %v", err)
21+
}
22+
23+
handler := mwManager.Apply(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
24+
w.WriteHeader(http.StatusOK)
25+
}))
26+
27+
req := httptest.NewRequest("GET", "http://example.com", nil)
28+
rec := httptest.NewRecorder()
29+
30+
handler.ServeHTTP(rec, req)
31+
32+
if rec.Header().Get("X-GoUP-Header") != "GoUP" {
33+
t.Errorf("Expected X-GoUP-Header to be 'GoUP', got '%s'", rec.Header().Get("X-GoUP-Header"))
34+
}
35+
}

internal/server/handler.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ func createHandler(conf config.SiteConfig, logger *log.Logger, identifier string
1818
var handler http.Handler
1919

2020
if conf.ProxyPass != "" {
21-
// Set up reverse proxy handler if ProxyPass is set
21+
// Set up reverse proxy handler if ProxyPass is set.
2222
proxyURL, err := url.Parse(conf.ProxyPass)
2323
if err != nil {
2424
return nil, fmt.Errorf("invalid proxy URL: %v", err)
@@ -29,17 +29,21 @@ func createHandler(conf config.SiteConfig, logger *log.Logger, identifier string
2929
proxy.ServeHTTP(w, r)
3030
})
3131
} else {
32-
// Serve static files
32+
// Serve static files from the root directory.
3333
fs := http.FileServer(http.Dir(conf.RootDirectory))
3434
handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
3535
addCustomHeaders(w, conf.CustomHeaders)
3636
fs.ServeHTTP(w, r)
3737
})
3838
}
3939

40-
// Wrap with logging and timeout middleware
40+
// Set up site-specific middleware.
41+
mwManager := middleware.NewMiddlewareManager()
4142
timeout := time.Duration(conf.RequestTimeout) * time.Second
42-
handler = middleware.TimeoutMiddleware(timeout, middleware.LoggingMiddleware(logger, conf.Domain, identifier, handler))
43+
mwManager.Use(middleware.TimeoutMiddleware(timeout))
44+
mwManager.Use(middleware.LoggingMiddleware(logger, conf.Domain, identifier))
45+
46+
handler = mwManager.Apply(handler)
4347

4448
return handler, nil
4549
}
@@ -50,7 +54,7 @@ func addCustomHeaders(w http.ResponseWriter, headers map[string]string) {
5054
w.Header().Set(key, value)
5155
}
5256

53-
// Expose the custom headers to the client. Should be safe (?)
57+
// Expose custom headers to the client.
5458
exposeHeaders := []string{}
5559
for key := range headers {
5660
exposeHeaders = append(exposeHeaders, key)
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package middleware
2+
3+
import "net/http"
4+
5+
// MiddlewareFunc represents the type for middleware functions.
6+
type MiddlewareFunc func(http.Handler) http.Handler
7+
8+
// MiddlewareManager manages a chain of middleware.
9+
type MiddlewareManager struct {
10+
middleware []MiddlewareFunc
11+
}
12+
13+
// NewMiddlewareManager creates a new instance of MiddlewareManager.
14+
func NewMiddlewareManager() *MiddlewareManager {
15+
return &MiddlewareManager{
16+
middleware: []MiddlewareFunc{},
17+
}
18+
}
19+
20+
// Use adds a middleware function to the chain.
21+
func (m *MiddlewareManager) Use(mw MiddlewareFunc) {
22+
m.middleware = append(m.middleware, mw)
23+
}
24+
25+
// Apply applies all registered middleware to a handler.
26+
func (m *MiddlewareManager) Apply(handler http.Handler) http.Handler {
27+
for i := len(m.middleware) - 1; i >= 0; i-- {
28+
handler = m.middleware[i](handler)
29+
}
30+
return handler
31+
}

internal/server/middleware/middleware.go

Lines changed: 32 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,44 +9,49 @@ import (
99
)
1010

1111
// LoggingMiddleware logs HTTP requests.
12-
func LoggingMiddleware(logger *log.Logger, domain string, identifier string, next http.Handler) http.Handler {
13-
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
14-
start := time.Now()
15-
16-
rw := &responseWriter{ResponseWriter: w, statusCode: 200}
17-
18-
next.ServeHTTP(rw, r)
19-
20-
duration := time.Since(start)
21-
22-
entry := logger.WithFields(log.Fields{
23-
"domain": domain,
24-
"identifier": identifier,
25-
"method": r.Method,
26-
"url": r.URL.String(),
27-
"remote_addr": r.RemoteAddr,
28-
"status_code": rw.statusCode,
29-
"duration": duration.Seconds(),
12+
func LoggingMiddleware(logger *log.Logger, domain string, identifier string) MiddlewareFunc {
13+
return func(next http.Handler) http.Handler {
14+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
15+
start := time.Now()
16+
17+
rw := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
18+
19+
next.ServeHTTP(rw, r)
20+
21+
duration := time.Since(start)
22+
23+
entry := logger.WithFields(log.Fields{
24+
"domain": domain,
25+
"identifier": identifier,
26+
"method": r.Method,
27+
"url": r.URL.String(),
28+
"remote_addr": r.RemoteAddr,
29+
"status_code": rw.statusCode,
30+
"duration": duration.Seconds(),
31+
})
32+
entry.Info("Handled request")
33+
34+
if tui.IsEnabled() {
35+
tui.UpdateLog(identifier, entry)
36+
}
3037
})
31-
entry.Info("Handled request")
32-
33-
if tui.IsEnabled() {
34-
tui.UpdateLog(identifier, entry)
35-
}
36-
})
38+
}
3739
}
3840

3941
// TimeoutMiddleware applies a timeout to HTTP requests.
40-
func TimeoutMiddleware(timeout time.Duration, next http.Handler) http.Handler {
41-
return http.TimeoutHandler(next, timeout, "Request timed out")
42+
func TimeoutMiddleware(timeout time.Duration) MiddlewareFunc {
43+
return func(next http.Handler) http.Handler {
44+
return http.TimeoutHandler(next, timeout, "Request timed out")
45+
}
4246
}
4347

44-
// responseWriter is a wrapper for http.ResponseWriter that captures the status code.
48+
// responseWriter wraps http.ResponseWriter to capture the status code.
4549
type responseWriter struct {
4650
http.ResponseWriter
4751
statusCode int
4852
}
4953

54+
// WriteHeader sets the HTTP status code.
5055
func (rw *responseWriter) WriteHeader(code int) {
5156
rw.statusCode = code
5257
rw.ResponseWriter.WriteHeader(code)

internal/server/middleware/middleware_test.go

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,25 +11,29 @@ import (
1111

1212
func TestLoggingMiddleware(t *testing.T) {
1313
logger := log.New()
14-
logger.Out = httptest.NewRecorder() // Discard output on purpose
14+
logger.Out = httptest.NewRecorder() // Discard output for testing
1515

1616
domain := "example.com"
1717
identifier := "test"
1818

1919
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
20-
w.WriteHeader(200)
20+
w.WriteHeader(http.StatusOK)
2121
w.Write([]byte("OK"))
2222
})
2323

24-
middlewareHandler := LoggingMiddleware(logger, domain, identifier, handler)
24+
// Get the middleware function
25+
loggingMiddleware := LoggingMiddleware(logger, domain, identifier)
26+
27+
// Apply middleware to the handler
28+
middlewareHandler := loggingMiddleware(handler)
2529

2630
req := httptest.NewRequest("GET", "http://example.com/test", nil)
2731
w := httptest.NewRecorder()
2832

2933
middlewareHandler.ServeHTTP(w, req)
3034

31-
if w.Code != 200 {
32-
t.Errorf("Expected status code 200, got %d", w.Code)
35+
if w.Code != http.StatusOK {
36+
t.Errorf("Expected status code %d, got %d", http.StatusOK, w.Code)
3337
}
3438
}
3539

@@ -41,7 +45,11 @@ func TestTimeoutMiddleware(t *testing.T) {
4145
w.Write([]byte("This should timeout"))
4246
})
4347

44-
middlewareHandler := TimeoutMiddleware(timeout, handler)
48+
// Get the middleware function
49+
timeoutMiddleware := TimeoutMiddleware(timeout)
50+
51+
// Apply middleware to the handler
52+
middlewareHandler := timeoutMiddleware(handler)
4553

4654
req := httptest.NewRequest("GET", "http://example.com/test", nil)
4755
w := httptest.NewRecorder()

0 commit comments

Comments
 (0)