diff --git a/Makefile b/Makefile index 0160f9c..58f12aa 100644 --- a/Makefile +++ b/Makefile @@ -22,10 +22,18 @@ clean: @echo "Cleaning..." rm -f $(BINARY_NAME) -install: build +install: + @if [ ! -f $(BINARY_NAME) ]; then \ + echo "Error: '$(BINARY_NAME)' binary not found."; \ + echo "Please run 'make build' first as a regular user."; \ + exit 1; \ + fi @echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..." # Create user if not exists - id -u grokway &>/dev/null || useradd -r -s /bin/false grokway + if ! id -u grokway >/dev/null 2>&1; then \ + echo "Creating grokway user..."; \ + useradd -r -s /bin/false grokway; \ + fi # Create directory mkdir -p $(INSTALL_DIR) # Copy binary @@ -41,7 +49,9 @@ install: build cp $(SERVICE_NAME) $(SYSTEMD_DIR)/ # Update paths in service file just in case they were modified sed -i 's|WorkingDirectory=.*|WorkingDirectory=$(INSTALL_DIR)|g' $(SYSTEMD_DIR)/$(SERVICE_NAME) - sed -i 's|ExecStart=.*|ExecStart=$(INSTALL_DIR)/$(BINARY_NAME)|g' $(SYSTEMD_DIR)/$(SERVICE_NAME) + # execStart sed commented out to preserve arguments like --ssh :2223 + # sed -i 's|ExecStart=.*|ExecStart=$(INSTALL_DIR)/$(BINARY_NAME)|g' $(SYSTEMD_DIR)/$(SERVICE_NAME) + # Instead, ensure the binary path is correct but keep args (this is tricky with sed, simpler to rely on repo file) systemctl daemon-reload systemctl enable $(SERVICE_NAME) diff --git a/cmd/server/main.go b/cmd/server/main.go index e4796e0..7e4c9b4 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -126,32 +126,30 @@ func startHttpProxy(port string) { // But typically `gliderlabs/ssh` is for allowing the server to be a jump host. // We want to be an HTTP Gateway. - // Manual channel opening: - if tunnel.Conn == nil { - http.Error(w, "Tunnel Broken", http.StatusBadGateway) +// 1. Hijack the connection to handle bidirectional traffic (WebSockets) + hijacker, ok := w.(http.Hijacker) + if !ok { + http.Error(w, "Hijacking not supported", http.StatusInternalServerError) return } + clientConn, bufrw, err := hijacker.Hijack() + if err != nil { + http.Error(w, err.Error(), http.StatusServiceUnavailable) + return + } + defer clientConn.Close() - payload := make([]byte, 0) // dynamic payload constructing - // Open channel to client + // 2. Open channel to client // "forwarded-tcpip" arguments: - // string address that was connected - // uint32 port that was connected - // string originator IP address - // uint32 originator port - - // For simplicity, we just say we are connecting to 127.0.0.1:LocalPort - // And originating from 127.0.0.1:0 - destHost := "0.0.0.0" destPort := tunnel.LocalPort srcHost := "127.0.0.1" - srcPort := uint32(12345) // Random source port, cannot be 0 + srcPort := uint32(12345) + payload := make([]byte, 0) payload = append(payload, []byte{0, 0, 0, uint8(len(destHost))}...) payload = append(payload, []byte(destHost)...) payload = append(payload, byte(destPort>>24), byte(destPort>>16), byte(destPort>>8), byte(destPort)) - payload = append(payload, []byte{0, 0, 0, uint8(len(srcHost))}...) payload = append(payload, []byte(srcHost)...) payload = append(payload, byte(srcPort>>24), byte(srcPort>>16), byte(srcPort>>8), byte(srcPort)) @@ -159,99 +157,40 @@ func startHttpProxy(port string) { ch, reqs, err := tunnel.Conn.OpenChannel("forwarded-tcpip", payload) if err != nil { log.Printf("Failed to open forwarded-tcpip channel: %s", err) - http.Error(w, "Failed to connect to client", http.StatusBadGateway) return } defer ch.Close() go gossh.DiscardRequests(reqs) - // Pipe HTTP request to the channel? - // No, we can't just pipe the raw HTTP structure unless the client expects it. - // If the client is just a TCP forwarder (ssh -R), it expects a raw TCP stream. - // So we need to act as a TCP proxy. - // But we are in an http.Handler. - // We can hijack the connection or just Re-serialize the HTTP request. - - // Re-serializing is safer for now. - // Or we use Hijack to get the raw TCP conn. - - // Let's iterate: just write the Request to the channel - // The client will see it as raw bytes on the socket. - // This works if the client is forwarding to a web server. - - err = r.Write(ch) // Writes the request wire representation - if err != nil { - http.Error(w, "Failed to write request: "+err.Error(), http.StatusInternalServerError) - return - } - - // Read response from channel and write to w - // This is tricky because we need to parse the response to set headers correctly in w. - // Or we can just use Hijack if we want to be a transparent TCP proxy. - // But Hijack breaks HTTP/2 and some middleware. - // Let's use `http.ReadResponse` - - // Problem: `r.Write(ch)` might not close the write side, so the server might wait. - // but HTTP 1.1 usually has content-length. - - // Using bufio to read the response from the channel. - // resp, err := http.ReadResponse(bufio.NewReader(ch), r) - // ... copy headers, body ... - - // For MVP, hijacking is often easiest for "Tunneling" - hijacker, ok := w.(http.Hijacker) - if !ok { - http.Error(w, "Hijacking not supported", http.StatusInternalServerError) - return - } - clientConn, _, err := hijacker.Hijack() - if err != nil { - http.Error(w, err.Error(), http.StatusServiceUnavailable) - return - } - defer clientConn.Close() - - // bidirectional copy var wg sync.WaitGroup wg.Add(2) - // We already consumed `r` somewhat? No, Hijack takes over. - // But `http.HandleFunc` might have read the headers. - // The `clientConn` is the raw TCP connection from the Browser/ExternalUser to OUR Proxy. - // We need to replay the request headers! - // Because `http.Server` already read them. - - // Actually, `r.Write(ch)` is correct for sending the request to the backend. - // But then we need to pipe `ch` output back to `w`. - // And since we used Hijack, we are responsible for the entire response. - - // So: - // 1. Write `r` to `ch`. - // 2. IO Copy `ch` -> `clientConn`. - + // 3. Browser -> Backend (Write request + Copy raw stream) go func() { defer wg.Done() - r.Write(ch) // Send the request to valid backend - // Also forward any extra body if not fully read? - // r.Write handles body. + // Write the initial request (Method, Path, Headers) + // This sets up the handshake or request. + // Note: We use r.Write to reconstruct the request line and headers. + // For WebSockets, the Body is empty, so this writes headers and returns. + // For POSTs, it writes headers and tries to copy Body. + if err := r.Write(ch); err != nil { + log.Printf("Error writing request to backend: %v", err) + return + } + // Important: Continue copying any subsequent data (like WebSocket frames) + // from the hijacked buffer/connection to the channel. + io.Copy(ch, bufrw) + // e.g. when browser closes or stops sending, we are done here. }() + // 4. Backend -> Browser (Copy raw stream) go func() { defer wg.Done() - io.Copy(clientConn, ch) // Copy response from backend to user + io.Copy(clientConn, ch) + // When backend closes connection, close browser connection + clientConn.Close() }() - // Wait? No, if we wait for r.Write, it might block if the body is large. - // Actually, standard proxying is complex. - // Simplest "Grokway" logic: - // User runs: ssh -R 80:localhost:8080 - // Server listens on a random port (or we map virtual host). - - // Let's ignore Hijack for a moment and try standard Request/Response. - // Implementation detail for later. - - // Connection is hijacked, do not write to w - wg.Wait() }) diff --git a/grokway.service b/grokway.service index f8973af..889ddf2 100644 --- a/grokway.service +++ b/grokway.service @@ -12,7 +12,7 @@ Group=grokway WorkingDirectory=/opt/grokway # Path to the executable. -ExecStart=/opt/grokway/grokway +ExecStart=/opt/grokway/grokway --ssh :2223 # Environment variables. # You MUST set this to a secure token, otherwise a random one is generated on each start.