Skip to content

Commit edf1bc7

Browse files
author
mirkobrombin
committed
feat: add NodeJSPlugin for executing Node.js applications
1 parent a767d31 commit edf1bc7

File tree

6 files changed

+327
-1
lines changed

6 files changed

+327
-1
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
private.key
22
certificate.crt
3-
test.sh
3+
test.sh
4+
public/node_test/app/node_modules
5+
public/node_test/app/pnpm-lock.yaml

cmd/goup/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ func main() {
1313
pluginManager.Register(&plugins.CustomHeaderPlugin{})
1414
pluginManager.Register(&plugins.PHPPlugin{})
1515
pluginManager.Register(&plugins.AuthPlugin{})
16+
pluginManager.Register(&plugins.NodeJSPlugin{})
1617

1718
cli.Execute()
1819
}

plugins/nodejs.go

Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
package plugins
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"net/http"
7+
"os"
8+
"os/exec"
9+
"path/filepath"
10+
"strings"
11+
"sync"
12+
13+
"github.com/mirkobrombin/goup/internal/config"
14+
"github.com/mirkobrombin/goup/internal/server/middleware"
15+
log "github.com/sirupsen/logrus"
16+
)
17+
18+
// NodeJSPlugin handles the execution of a Node.js application.
19+
type NodeJSPlugin struct {
20+
mu sync.Mutex
21+
process *os.Process
22+
}
23+
24+
// Name returns the name of the plugin.
25+
func (p *NodeJSPlugin) Name() string {
26+
return "NodeJSPlugin"
27+
}
28+
29+
// Init registers any global middleware (none for NodeJSPlugin).
30+
func (p *NodeJSPlugin) Init(mwManager *middleware.MiddlewareManager) error {
31+
return nil
32+
}
33+
34+
// InitForSite initializes the plugin for a specific site.
35+
func (p *NodeJSPlugin) InitForSite(mwManager *middleware.MiddlewareManager, logger *log.Logger, conf config.SiteConfig) error {
36+
mwManager.Use(p.nodeMiddleware(logger, conf))
37+
return nil
38+
}
39+
40+
// NodeJSPluginConfig represents the configuration for the NodeJSPlugin.
41+
type NodeJSPluginConfig struct {
42+
Enable bool `json:"enable"`
43+
Port string `json:"port"`
44+
RootDir string `json:"root_dir"`
45+
Entry string `json:"entry"`
46+
InstallDeps bool `json:"install_deps"`
47+
NodePath string `json:"node_path"`
48+
PackageManager string `json:"package_manager"`
49+
ProxyPaths []string `json:"proxy_paths"`
50+
}
51+
52+
// nodeMiddleware intercepts requests and forwards API calls to Node.js.
53+
func (p *NodeJSPlugin) nodeMiddleware(logger *log.Logger, conf config.SiteConfig) middleware.MiddlewareFunc {
54+
return func(next http.Handler) http.Handler {
55+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
56+
// Retrieve site-specific plugin configuration.
57+
pluginConfigRaw, ok := conf.PluginConfigs[p.Name()]
58+
if !ok {
59+
logger.Warnf("Plugin config not found for host: %s", r.Host)
60+
next.ServeHTTP(w, r)
61+
return
62+
}
63+
64+
// Parse configuration.
65+
pluginConfig := NodeJSPluginConfig{}
66+
if rawMap, ok := pluginConfigRaw.(map[string]interface{}); ok {
67+
if enable, ok := rawMap["enable"].(bool); ok {
68+
pluginConfig.Enable = enable
69+
}
70+
if port, ok := rawMap["port"].(string); ok {
71+
pluginConfig.Port = port
72+
}
73+
if rootDir, ok := rawMap["root_dir"].(string); ok {
74+
pluginConfig.RootDir = rootDir
75+
}
76+
if entry, ok := rawMap["entry"].(string); ok {
77+
pluginConfig.Entry = entry
78+
}
79+
if installDeps, ok := rawMap["install_deps"].(bool); ok {
80+
pluginConfig.InstallDeps = installDeps
81+
}
82+
if nodePath, ok := rawMap["node_path"].(string); ok {
83+
pluginConfig.NodePath = nodePath
84+
}
85+
if packageManager, ok := rawMap["package_manager"].(string); ok {
86+
pluginConfig.PackageManager = packageManager
87+
}
88+
if proxyPaths, ok := rawMap["proxy_paths"].([]interface{}); ok {
89+
for _, path := range proxyPaths {
90+
if pathStr, ok := path.(string); ok {
91+
pluginConfig.ProxyPaths = append(pluginConfig.ProxyPaths, pathStr)
92+
}
93+
}
94+
}
95+
}
96+
97+
if !pluginConfig.Enable {
98+
logger.Infof("NodeJS Plugin is disabled for host: %s", r.Host)
99+
next.ServeHTTP(w, r)
100+
return
101+
}
102+
103+
// Start Node.js if it is not already running.
104+
p.ensureNodeServerRunning(pluginConfig, logger)
105+
106+
// Check if the request should be forwarded to Node.js.
107+
for _, proxyPath := range pluginConfig.ProxyPaths {
108+
if strings.HasPrefix(r.URL.Path, proxyPath) {
109+
p.proxyToNode(w, r, pluginConfig, logger)
110+
return
111+
}
112+
}
113+
114+
// If the request does not match a Node.js route, let GoUp handle static files.
115+
next.ServeHTTP(w, r)
116+
})
117+
}
118+
}
119+
120+
// ensureNodeServerRunning starts Node.js if it is not already running.
121+
func (p *NodeJSPlugin) ensureNodeServerRunning(config NodeJSPluginConfig, logger *log.Logger) {
122+
p.mu.Lock()
123+
defer p.mu.Unlock()
124+
125+
// If the process is already running, do nothing.
126+
if p.process != nil {
127+
logger.Infof("Node.js server is already running (PID: %d)", p.process.Pid)
128+
return
129+
}
130+
131+
logger.Infof("Starting Node.js server...")
132+
133+
// Install dependencies if required.
134+
if config.InstallDeps {
135+
p.installDependencies(config, logger)
136+
}
137+
138+
// Start the Node.js server.
139+
entryPath := filepath.Join(config.RootDir, config.Entry)
140+
141+
nodePath := config.NodePath
142+
if nodePath == "" {
143+
nodePath = "node"
144+
}
145+
146+
cmd := exec.Command(nodePath, entryPath)
147+
cmd.Dir = config.RootDir
148+
cmd.Stdout = os.Stdout
149+
cmd.Stderr = os.Stderr
150+
151+
if err := cmd.Start(); err != nil {
152+
logger.Errorf("Failed to start Node.js server: %v", err)
153+
return
154+
}
155+
156+
p.process = cmd.Process
157+
logger.Infof("Started Node.js server (PID: %d) on port %s", p.process.Pid, config.Port)
158+
}
159+
160+
// proxyToNode forwards the original request to Node.js and returns the response.
161+
func (p *NodeJSPlugin) proxyToNode(w http.ResponseWriter, r *http.Request, config NodeJSPluginConfig, logger *log.Logger) {
162+
// Construct the URL for forwarding the request to Node.js.
163+
nodeURL := fmt.Sprintf("http://localhost:%s%s", config.Port, r.URL.Path)
164+
165+
// Create a new HTTP request forwarding the original request data.
166+
bodyReader, err := io.ReadAll(r.Body)
167+
if err != nil {
168+
logger.Errorf("Failed to read request body: %v", err)
169+
http.Error(w, "Failed to read request body", http.StatusInternalServerError)
170+
return
171+
}
172+
defer r.Body.Close()
173+
174+
req, err := http.NewRequest(r.Method, nodeURL, strings.NewReader(string(bodyReader)))
175+
if err != nil {
176+
logger.Errorf("Failed to create request for Node.js: %v", err)
177+
http.Error(w, "Failed to create request", http.StatusInternalServerError)
178+
return
179+
}
180+
181+
// Copy headers from the original request.
182+
for key, values := range r.Header {
183+
for _, value := range values {
184+
req.Header.Add(key, value)
185+
}
186+
}
187+
188+
client := &http.Client{}
189+
resp, err := client.Do(req)
190+
if err != nil {
191+
logger.Errorf("Failed to connect to Node.js backend: %v", err)
192+
http.Error(w, "Node.js backend unavailable", http.StatusBadGateway)
193+
return
194+
}
195+
defer resp.Body.Close()
196+
197+
// Copy response headers.
198+
for key, values := range resp.Header {
199+
for _, value := range values {
200+
w.Header().Add(key, value)
201+
}
202+
}
203+
204+
// Write the status code and response body.
205+
w.WriteHeader(resp.StatusCode)
206+
body, err := io.ReadAll(resp.Body)
207+
if err != nil {
208+
logger.Errorf("Failed to read response body from Node.js: %v", err)
209+
http.Error(w, "Failed to read response from Node.js", http.StatusInternalServerError)
210+
return
211+
}
212+
213+
w.Write(body)
214+
}
215+
216+
// startNodeServer ensures the Node.js server is running.
217+
func (p *NodeJSPlugin) startNodeServer(config NodeJSPluginConfig, logger *log.Logger) {
218+
logger.Infof("Starting Node.js server...")
219+
p.mu.Lock()
220+
defer p.mu.Unlock()
221+
222+
// Install dependencies if needed.
223+
if config.InstallDeps {
224+
p.installDependencies(config, logger)
225+
}
226+
227+
// Start the Node.js server.
228+
entryPath := filepath.Join(config.RootDir, config.Entry)
229+
cmd := exec.Command(config.NodePath, entryPath)
230+
cmd.Dir = config.RootDir
231+
cmd.Stdout = os.Stdout
232+
cmd.Stderr = os.Stderr
233+
234+
if err := cmd.Start(); err != nil {
235+
logger.Errorf("Failed to start Node.js server: %v", err)
236+
return
237+
}
238+
239+
p.process = cmd.Process
240+
logger.Infof("Started Node.js server (PID: %d) on port %s", p.process.Pid, config.Port)
241+
}
242+
243+
// installDependencies installs dependencies using the configured package manager.
244+
func (p *NodeJSPlugin) installDependencies(config NodeJSPluginConfig, logger *log.Logger) {
245+
nodeModulesPath := filepath.Join(config.RootDir, "node_modules")
246+
if _, err := os.Stat(nodeModulesPath); os.IsNotExist(err) {
247+
logger.Infof("node_modules not found, installing dependencies in %s", config.RootDir)
248+
249+
packageManager := config.PackageManager
250+
if packageManager == "" {
251+
packageManager = "npm"
252+
}
253+
254+
logger.Infof("Using package manager: %s", packageManager)
255+
cmd := exec.Command(packageManager, "install")
256+
cmd.Dir = config.RootDir
257+
cmd.Stdout = os.Stdout
258+
cmd.Stderr = os.Stderr
259+
if err := cmd.Run(); err != nil {
260+
logger.Errorf("Failed to install dependencies using %s: %v", packageManager, err)
261+
} else {
262+
logger.Infof("Dependencies installed successfully using %s", packageManager)
263+
}
264+
}
265+
}

public/node_test/app/package.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"name": "nodejs-plugin-test",
3+
"version": "1.0.0",
4+
"description": "A simple Node.js app to test the NodeJSPlugin in GoUp",
5+
"main": "server.js",
6+
"scripts": {
7+
"start": "node server.js"
8+
},
9+
"dependencies": {
10+
"express": "^4.18.2"
11+
}
12+
}

public/node_test/app/server.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
const express = require("express");
2+
3+
const app = express();
4+
const PORT = process.env.PORT || 3000;
5+
6+
app.use((req, res, next) => {
7+
console.log(`[NodeJSPlugin] Request: ${req.method} ${req.url}`);
8+
next();
9+
});
10+
11+
app.get("/api/test", (req, res) => {
12+
res.json({ message: "Hello from NodeJSPlugin!", timestamp: Date.now() });
13+
});
14+
15+
app.listen(PORT, () => {
16+
console.log(`NodeJS server running on http://localhost:${PORT}`);
17+
});

public/node_test/static/index.html

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
4+
<head>
5+
<meta charset="UTF-8">
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
7+
<title>GoUp + Node.js Test</title>
8+
</head>
9+
10+
<body>
11+
<h1>GoUp + Node.js Plugin Test</h1>
12+
<button onclick="testAPI()">Test Node API</button>
13+
<p id="response"></p>
14+
15+
<script>
16+
function testAPI() {
17+
fetch('/api/test')
18+
.then(response => response.json())
19+
.then(data => {
20+
document.getElementById('response').innerText = JSON.stringify(data);
21+
})
22+
.catch(error => {
23+
document.getElementById('response').innerText = 'Error: ' + error;
24+
});
25+
}
26+
</script>
27+
</body>
28+
29+
</html>

0 commit comments

Comments
 (0)