Compare commits

...

4 Commits

10 changed files with 368 additions and 102 deletions

2
.gitignore vendored
View File

@@ -184,3 +184,5 @@ go.work.sum
lstWrapper/Program_working_node_ws.txt
lstWrapper/web.config.txt

View File

@@ -4,8 +4,8 @@ meta {
seq: 1
}
post {
url: http://localhost:8080/build
get {
url: http://localhost:8080/check
body: none
auth: inherit
}

View File

@@ -66,6 +66,11 @@ const main = async () => {
if (process.env.NODE_ENV?.trim() !== "production") {
app.use(morgan("tiny"));
basePath = "/lst";
app.use(
basePath + "/test",
express.static(join(__dirname, "../../controller"))
);
}
// docs and api stuff
@@ -78,6 +83,11 @@ const main = async () => {
express.static(join(__dirname, "../../frontend/dist"))
);
app.use(
basePath + "/test",
express.static(join(__dirname, "../../frontend/dist"))
);
// register app
setupRoutes(app, basePath);

View File

@@ -13,6 +13,17 @@ import (
func registerBuildChannel(server *socketio.Server) {
// Example: When clients join "build" namespace or room
server.OnEvent("/", "subscribe:build", func(s socketio.Conn) {
host, err := os.Hostname()
if err != nil {
server.BroadcastToRoom("/", "build", "buildlogs", "Could not retrieve hostname")
return
}
if strings.Contains(host, "VMS") || strings.Contains(host, "vms") {
server.BroadcastToRoom("/", "build", "buildlogs", "You are not allowed to run the build on a production server")
return
}
s.Join("build")
s.Emit("buildlogs", "👋 Connected to build channel") // this is where all the messages are actually sent to
@@ -23,16 +34,6 @@ func registerBuildChannel(server *socketio.Server) {
fmt.Println("🔨 Build triggered:", target)
go func() {
host, err := os.Hostname()
if err != nil {
server.BroadcastToRoom("/", "build", "buildlogs", "Could not retrieve hostname")
return
}
if strings.Contains(host, "VMS") || strings.Contains(host, "vms") {
server.BroadcastToRoom("/", "build", "buildlogs", "You are not allowed to run the build on a production server")
return
}
server.BroadcastToRoom("/", "build", "buildlogs", "🔨 Starting build: Old App")
if err := runNpmV2Build(server); err != nil {

View File

@@ -58,13 +58,23 @@
<button type="button" id="btnCopyLatest">Send</button>
</form>
</div>
<button id="btnUpdateServer">Server Update</button>
</div>
<div id="logWindow"></div>
<script>
// ✅ Define socket in global scope
const socket = io("http://localhost:8000");
// const socket = io("https://usmcd1vms036.alpla.net", {
// path: "/lst/socket.io/",
// transports: ["polling"],
// });
const socket = io("http://localhost:8080", {
path: "/socket.io/",
transports: ["polling"],
});
// log window reference
const logWindow = document.getElementById("logWindow");
@@ -140,7 +150,10 @@
if (!fromMyInput) return;
// Emit to backend (adjust event name as required)
socket.emit("update", fromMyInput);
socket.emit("update", {
action: "copy",
target: fromMyInput,
});
// You can define your own logMessage function
logMessage("info", `Copying to ${fromMyInput}`);
@@ -151,6 +164,14 @@
};
};
document.getElementById("btnUpdateServer").onclick = () => {
socket.emit("update", {
action: "update",
target: "usmcd1vms036",
}); // "frontend" = payload target
logMessage("update", "Update The server");
};
socket.on("logs", (msg) => logMessage("logs", msg));
socket.on("errors", (msg) => logMessage("errors", msg));
socket.on("buildlogs", (msg) => logMessage("build", msg));

View File

@@ -0,0 +1,68 @@
package router
import (
"fmt"
"net/http"
"os"
"time"
"github.com/gin-gonic/gin"
)
type UpdatePayload struct {
Action string `json:"action"`
Target string `json:"target"`
}
func Setup(basePath string) *gin.Engine {
r := gin.Default()
if os.Getenv("NODE") == "production" {
gin.SetMode(gin.ReleaseMode)
}
r.GET(basePath+"/check", func(c *gin.Context) {
c.JSON(http.StatusAccepted, gin.H{"check": "good"})
})
//logger.RegisterLoggerRoutes(r, basePath, db)
r.POST(basePath+"/update", func(c *gin.Context) {
var payload UpdatePayload
if err := c.BindJSON(&payload); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.Writer.Header().Set("Content-Type", "text/event-stream")
c.Writer.Header().Set("Cache-Control", "no-cache")
c.Writer.Header().Set("Connection", "keep-alive")
flusher, ok := c.Writer.(http.Flusher)
if !ok {
c.String(http.StatusInternalServerError, "Streaming not supported")
return
}
steps := []string{"🚀 Starting...", "📂 Copying...", "🔧 Migrating...", "✅ Done!"}
for _, step := range steps {
fmt.Fprintf(c.Writer, "event: log\ndata: %s\n\n", step)
flusher.Flush() // 🔑 actually push chunk
time.Sleep(1 * time.Second) // simulate work
}
})
r.Any(basePath+"/", func(c *gin.Context) { errorApiLoc(c) })
return r
}
func errorApiLoc(c *gin.Context) {
// log.Error("Api endpoint hit that dose not exist", "system", map[string]interface{}{
// "endpoint": c.Request.URL.Path,
// "client_ip": c.ClientIP(),
// "user_agent": c.Request.UserAgent(),
// })
c.JSON(http.StatusBadRequest, gin.H{"message": "looks like you have encountered a route that dose not exist"})
}

View File

@@ -4,19 +4,33 @@ import (
"fmt"
"log"
"net/http"
"os"
"time"
"github.com/gin-gonic/gin"
socketio "github.com/googollee/go-socket.io"
"github.com/joho/godotenv"
router "lst.net/internal/route_handler"
)
func main() {
err := godotenv.Load("../.env")
if err != nil {
//log := logger.New()
//log.Info("Warning: .env file not found (ok in Docker/production)", "system", map[string]interface{}{})
fmt.Println("Warning: .env file not found")
}
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
// gin stuff
basePath := "/api/controller"
if os.Getenv("NODE_ENV") != "production" {
basePath = "/lst/api/controller"
}
r := router.Setup(basePath) // returns *gin.Engine
server := socketio.NewServer(nil)
server.OnConnect("/", func(s socketio.Conn) error {
@@ -91,12 +105,16 @@ func main() {
go server.Serve()
defer server.Close()
// Enable CORS wrapper for Socket.IO route
http.Handle("/socket.io/", withCORS(server))
http.Handle("/", http.FileServer(http.Dir("./static")))
// mount socket.io on /socket.io/*
r.Any("/socket.io/*any", gin.WrapH(withCORS(server)))
fmt.Println("🚀 Socket.IO server running on :8000")
log.Fatal(http.ListenAndServe(":8000", nil))
// mount a static dir (like http.FileServer)
//r.Static("/", "./static")
fmt.Println("🚀 Server running on :" + port)
if err := r.Run(":" + port); err != nil {
log.Fatal("Server failed:", err)
}
}
// Reuse your proper CORS handler
@@ -104,12 +122,15 @@ func withCORS(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
if origin != "" {
// 🔑 Echo the request Origin, not "*"
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Vary", "Origin")
}
w.Header().Set("Access-Control-Allow-Methods", "GET,POST,OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
w.Header().Set("Access-Control-Allow-Credentials", "true")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return

View File

@@ -1,11 +1,24 @@
package main
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"strings"
socketio "github.com/googollee/go-socket.io"
)
type UpdatePayload struct {
Action string `json:"action"`
Target string `json:"target"`
}
func registerUpdateChannel(server *socketio.Server) {
server.OnEvent("/", "subscribe:update", func(s socketio.Conn) {
@@ -15,21 +28,91 @@ func registerUpdateChannel(server *socketio.Server) {
})
// copy files based on the target passed over
server.OnEvent("/", "update", func(s socketio.Conn, target string) {
server.BroadcastToRoom("/", "update", "updateLogs",
fmt.Sprintf("🚀 Copying latest build to %v", target))
copyBuild(server, target)
server.OnEvent("/", "update", func(s socketio.Conn, payload UpdatePayload) {
switch strings.ToLower(payload.Action) {
case "copy":
server.BroadcastToRoom("/", "update", "updateLogs",
fmt.Sprintf("🚀 Copying latest build to %v", payload.Target))
copyLatestBuild(server, payload.Target)
case "update":
updateServer(server, payload.Target)
default:
server.BroadcastToRoom("/", "update", "updateLogs",
fmt.Sprintf("❓ Unknown action: %s", payload.Action))
}
})
// update the server
// server.OnEvent("/", "update", func(s socketio.Conn, target string) {
// msg := fmt.Sprintf("🔧 Running updateServer on: %s", target)
// server.BroadcastToRoom("/update", "update", "updateLogs", msg)
// })
server.OnEvent("/", "unsubscribe:update", func(s socketio.Conn) {
s.Leave("update")
s.Emit("updateLogs", "👋 Unsubscribed from update logs")
})
}
func updateServer(server *socketio.Server, target string) {
host, err := os.Hostname()
if err != nil {
server.BroadcastToRoom("/", "update", "updateLogs", "Could not retrieve hostname")
return
}
if strings.Contains(host, "VMS") || strings.Contains(host, "vms") {
server.BroadcastToRoom("/", "update", "updateLogs", "Your are about to check for a new build and then update the server.")
return
}
if target == "" {
server.BroadcastToRoom("/", "update", "updateLogs", "You seem to be on a dev server or not an actual production server, you MUST pass a server over. I.E. USMCD1VMS036")
return
} else {
server.BroadcastToRoom("/", "update", "updateLogs", fmt.Sprintf("Running the update on: %v", target))
go triggerRemoteUpdate(server, target, UpdatePayload{Action: "update", Target: target})
}
}
func copyLatestBuild(server *socketio.Server, target string) {
server.BroadcastToRoom("/", "update", "updateLogs",
fmt.Sprintf("🚀 Copying latest build to %v", target))
copyBuild(server, target)
}
func triggerRemoteUpdate(server *socketio.Server, remoteURL string, payload UpdatePayload) {
basePath := "/api/controller"
if os.Getenv("NODE_ENV") != "production" {
basePath = "/lst/api/controller"
}
// send POST request with JSON, expect SSE / streaming text back
body, _ := json.Marshal(payload)
url := fmt.Sprintf("https://%v.alpla.net%v/update", remoteURL, basePath)
//url := fmt.Sprintf("http://localhost:8080%v/update", basePath)
fmt.Println(url)
resp, err := http.Post(url, "application/json", bytes.NewBuffer(body))
if err != nil {
log.Println("❌ Cannot connect remote:", err)
return
}
defer resp.Body.Close()
decoder := bufio.NewReader(resp.Body)
for {
line, err := decoder.ReadString('\n')
if err != nil {
if err != io.EOF {
log.Println("❌ Error reading stream:", err)
}
break
}
//fmt.Println(line)
parsed := strings.TrimSpace(line)
if parsed != "" {
log.Println("📡 Remote log:", parsed)
server.BroadcastToRoom("/", "update", "updateLogs", parsed)
}
}
}

View File

@@ -1,26 +1,32 @@
using System;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.Extensions.DependencyInjection;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient("NodeApp", client =>
{
client.BaseAddress = new Uri("http://localhost:4000");
});
// Register HttpClient so we can proxy HTTP traffic
builder.Services.AddHttpClient();
var app = builder.Build();
// Enable WebSocket support
app.UseWebSockets();
// Logging method
// Forwarded headers (important if behind IIS or another proxy)
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto,
ForwardLimit = 2
});
// Simple file logger
void LogToFile(string message)
{
try
@@ -28,44 +34,91 @@ void LogToFile(string message)
string logDir = Path.Combine(AppContext.BaseDirectory, "logs");
Directory.CreateDirectory(logDir);
string logFilePath = Path.Combine(logDir, "proxy_log.txt");
File.AppendAllText(logFilePath, $"{DateTime.UtcNow}: {message}{Environment.NewLine}");
File.AppendAllText(logFilePath, $"{DateTime.UtcNow:u}: {message}{Environment.NewLine}");
}
catch (Exception ex)
{
// Handle potential errors writing to log file
Console.WriteLine($"Logging error: {ex.Message}");
}
}
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto,
// Increase the limit if you have multiple proxies
ForwardLimit = 2
});
// Middleware to handle WebSocket requests
app.Use(async (context, next) =>
{
if (context.WebSockets.IsWebSocketRequest && context.Request.Path.StartsWithSegments("/ws"))
if (context.Request.Headers.ContainsKey("Origin"))
{
// LogToFile($"WebSocket request received for path: {context.Request.Path}");
var origin = context.Request.Headers["Origin"].ToString();
context.Response.Headers["Access-Control-Allow-Origin"] = origin;
context.Response.Headers["Vary"] = "Origin";
context.Response.Headers["Access-Control-Allow-Credentials"] = "true";
context.Response.Headers["Access-Control-Allow-Methods"] = "GET,POST,PUT,PATCH,DELETE,OPTIONS";
context.Response.Headers["Access-Control-Allow-Headers"] = "Content-Type, Authorization, X-Requested-With";
}
if (string.Equals(context.Request.Method, "OPTIONS", StringComparison.OrdinalIgnoreCase))
{
context.Response.StatusCode = StatusCodes.Status204NoContent;
return;
}
await next();
});
// Single terminal middleware (Run instead of Use → no ambiguity)
app.Run(async (HttpContext context) =>
{
var rawPath = context.Request.Path.Value ?? "";
string backendHttpBase;
string backendWsBase;
if (rawPath.StartsWith("/socket.io", StringComparison.OrdinalIgnoreCase) ||
rawPath.StartsWith("/lst/socket.io", StringComparison.OrdinalIgnoreCase))
{
backendHttpBase = "http://localhost:8080";
backendWsBase = "ws://localhost:8080";
}
else if (rawPath.StartsWith("/lst/api/controller", StringComparison.OrdinalIgnoreCase) ||
rawPath.StartsWith("/api/controller", StringComparison.OrdinalIgnoreCase))
{
backendHttpBase = "http://localhost:8080";
backendWsBase = "ws://localhost:8080";
// Now strip only once
// var newPath = rawPath.Substring("/lst".Length);
// context.Request.Path = new PathString(newPath);
}
else
{
backendHttpBase = "http://localhost:4000";
backendWsBase = "ws://localhost:4000";
}
// Handle WebSocket requests
if (context.WebSockets.IsWebSocketRequest)
{
try
{
var backendUri = new UriBuilder("ws", "localhost", 4000)
var backendUri = new UriBuilder(backendWsBase)
{
Path = context.Request.Path,
Query = context.Request.QueryString.ToString()
}.Uri;
using var backendSocket = new ClientWebSocket();
// Forward incoming headers
foreach (var header in context.Request.Headers)
{
try { backendSocket.Options.SetRequestHeader(header.Key, header.Value); }
catch { /* ignore headers WS client doesn't like */ }
}
await backendSocket.ConnectAsync(backendUri, context.RequestAborted);
using var frontendSocket = await context.WebSockets.AcceptWebSocketAsync();
var cts = new CancellationTokenSource();
// WebSocket forwarding tasks
var forwardToBackend = ForwardWebSocketAsync(frontendSocket, backendSocket, cts.Token);
var forwardToBackend = ForwardWebSocketAsync(frontendSocket, backendSocket, cts.Token);
var forwardToFrontend = ForwardWebSocketAsync(backendSocket, frontendSocket, cts.Token);
await Task.WhenAny(forwardToBackend, forwardToFrontend);
@@ -73,33 +126,22 @@ app.Use(async (context, next) =>
}
catch (Exception ex)
{
//LogToFile($"WebSocket proxy error: {ex.Message}");
LogToFile($"WebSocket proxy error: {ex.Message}");
context.Response.StatusCode = (int)HttpStatusCode.BadGateway;
await context.Response.WriteAsync($"WebSocket proxy error: {ex.Message}");
}
}
else
{
await next();
}
});
// Middleware to handle HTTP requests
app.Use(async (context, next) =>
{
if (context.WebSockets.IsWebSocketRequest)
{
await next();
return;
}
var client = context.RequestServices.GetRequiredService<IHttpClientFactory>().CreateClient("NodeApp");
// Otherwise: normal HTTP request
var client = context.RequestServices.GetRequiredService<IHttpClientFactory>().CreateClient();
var targetUri = backendHttpBase + context.Request.Path + context.Request.QueryString;
try
{
var request = new HttpRequestMessage(new HttpMethod(context.Request.Method),
context.Request.Path + context.Request.QueryString);
var request = new HttpRequestMessage(new HttpMethod(context.Request.Method), targetUri);
// Copy headers
foreach (var header in context.Request.Headers)
{
if (!request.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray()))
@@ -114,21 +156,45 @@ app.Use(async (context, next) =>
request.Content = new StreamContent(context.Request.Body);
}
var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, context.RequestAborted);
var response = await client.SendAsync(request,
HttpCompletionOption.ResponseHeadersRead,
context.RequestAborted);
context.Response.StatusCode = (int)response.StatusCode;
// Copy backend headers
foreach (var header in response.Headers)
{
context.Response.Headers[header.Key] = header.Value.ToArray();
}
foreach (var header in response.Content.Headers)
{
context.Response.Headers[header.Key] = header.Value.ToArray();
context.Response.Headers.Remove("transfer-encoding");
// ✅ NOW inject/override CORS
if (context.Request.Headers.ContainsKey("Origin"))
{
var origin = context.Request.Headers["Origin"].ToString();
context.Response.Headers["Access-Control-Allow-Origin"] = origin;
context.Response.Headers["Vary"] = "Origin";
context.Response.Headers["Access-Control-Allow-Credentials"] = "true";
context.Response.Headers["Access-Control-Allow-Methods"] =
"GET,POST,PUT,PATCH,DELETE,OPTIONS";
context.Response.Headers["Access-Control-Allow-Headers"] =
"Content-Type, Authorization, X-Requested-With";
}
context.Response.Headers.Remove("transfer-encoding");
await response.Content.CopyToAsync(context.Response.Body);
//await response.Content.CopyToAsync(context.Response.Body);
// changes to manage the SSE so we get it all at once
var stream = await response.Content.ReadAsStreamAsync(context.RequestAborted);
var buffer = new byte[8192];
int bytesRead;
while ((bytesRead = await stream.ReadAsync(buffer.AsMemory(0, buffer.Length), context.RequestAborted)) > 0)
{
await context.Response.Body.WriteAsync(buffer.AsMemory(0, bytesRead), context.RequestAborted);
// 🔑 Force flush so chunks go straight to client
await context.Response.Body.FlushAsync(context.RequestAborted);
}
}
catch (HttpRequestException ex)
{
@@ -138,9 +204,10 @@ app.Use(async (context, next) =>
}
});
async Task ForwardWebSocketAsync(WebSocket source, WebSocket destination, CancellationToken cancellationToken)
// Helper to forward WS frames in both directions
static async Task ForwardWebSocketAsync(WebSocket source, WebSocket destination, CancellationToken cancellationToken)
{
var buffer = new byte[4 * 1024];
var buffer = new byte[8192];
try
{
while (source.State == WebSocketState.Open &&
@@ -153,13 +220,22 @@ async Task ForwardWebSocketAsync(WebSocket source, WebSocket destination, Cancel
await destination.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "Closing", cancellationToken);
break;
}
await destination.SendAsync(new ArraySegment<byte>(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, cancellationToken);
await destination.SendAsync(
new ArraySegment<byte>(buffer, 0, result.Count),
result.MessageType,
result.EndOfMessage,
cancellationToken);
}
}
catch (WebSocketException ex)
{
LogToFile($"WebSocket forwarding error: {ex.Message}");
await destination.CloseOutputAsync(WebSocketCloseStatus.InternalServerError, "Error", cancellationToken);
Console.WriteLine($"WebSocket forwarding error: {ex.Message}");
try
{
await destination.CloseOutputAsync(WebSocketCloseStatus.InternalServerError, "Error", cancellationToken);
}
catch { }
}
}

View File

@@ -1,25 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.webServer>
<!-- Enable WebSockets (may require unlocking at host level) -->
<!-- Enable WebSockets -->
<webSocket enabled="true" receiveBufferLimit="4194304" pingInterval="00:01:00" />
<rewrite>
<rules>
<rule name="Proxy to Wrapper" stopProcessing="true">
<match url="^lst/(.*)" />
<conditions>
<add input="{HTTP_UPGRADE}" pattern="^WebSocket$" negate="true" />
</conditions>
<action type="Rewrite" url="http://localhost:4000/{R:1}" />
<serverVariables>
<set name="HTTP_X_FORWARDED_FOR" value="{REMOTE_ADDR}" />
<set name="HTTP_X_REAL_IP" value="{REMOTE_ADDR}" />
</serverVariables>
</rule>
</rules>
</rewrite>
<staticContent>
<remove fileExtension=".js" />
<mimeMap fileExtension=".js" mimeType="application/javascript" />
@@ -38,7 +22,7 @@
<aspNetCore processPath="dotnet"
arguments=".\lstWrapper.dll"
stdoutLogEnabled="false"
stdoutLogEnabled="true"
stdoutLogFile=".\logs\stdout"
hostingModel="inprocess" />
</system.webServer>