package main import ( "flag" "fmt" "io/ioutil" "log" "net" "net/http" "os" "strings" "time" "github.com/nats-io/nats.go" ) // printHelp outputs the usage information. func printHelp() { fmt.Println("Usage: HTTP-to-NATS proxy server") fmt.Println("\nThe following environment variables are supported:") fmt.Println(" NATS_URL - NATS connection URL (default: nats://127.0.0.1:4222)") fmt.Println(" NATS_USER - NATS username for authentication (optional)") fmt.Println(" NATS_PASSWORD - NATS password for authentication (optional)") fmt.Println(" NATS_TOKEN - NATS token for authentication (optional)") fmt.Println(" NATS_NKEY - NATS NKEY for authentication (optional)") fmt.Println(" NATS_NKEY_SEED - NATS NKEY seed for authentication (optional)") fmt.Println(" NATS_CREDS_FILE - Path to NATS credentials file (optional)") fmt.Println(" NATS_INBOX_PREFIX - Subject prefix for NATS messages (default: _INBOX)") fmt.Println(" HTTP_PORT - HTTP port to listen on (default: 8080)") } func main() { helpFlag := flag.Bool("help", false, "Display help information about available environment variables") flag.Parse() if *helpFlag { printHelp() os.Exit(0) } // Read NATS connection info from environment variables natsURL := os.Getenv("NATS_URL") if natsURL == "" { natsURL = nats.DefaultURL // defaults to "nats://127.0.0.1:4222" } natsUser := os.Getenv("NATS_USER") natsPassword := os.Getenv("NATS_PASSWORD") natsToken := os.Getenv("NATS_TOKEN") natsNkey := os.Getenv("NATS_NKEY") natsNkeySeed := os.Getenv("NATS_NKEY_SEED") natsCredsFile := os.Getenv("NATS_CREDS_FILE") natsInboxPrefix := os.Getenv("NATS_INBOX_PREFIX") // Read HTTP port from environment variables httpPort := os.Getenv("HTTP_PORT") if httpPort == "" { httpPort = "8080" } // Set up NATS connection options opts := []nats.Option{nats.Name("Web proxy")} if natsUser != "" && natsPassword != "" { opts = append(opts, nats.UserInfo(natsUser, natsPassword)) } else if natsToken != "" { opts = append(opts, nats.Token(natsToken)) } else if natsNkey != "" && natsNkeySeed != "" { log.Fatalln("NKEY connection not supported") } else if natsCredsFile != "" { opts = append(opts, nats.UserCredentials(natsCredsFile)) } if natsInboxPrefix != "" { opts = append(opts, nats.CustomInboxPrefix(natsInboxPrefix)) } nc, err := nats.Connect(natsURL, opts...) if err != nil { log.Fatal("Error connecting to NATS:", err) } defer nc.Close() // HTTP handler to proxy all requests to NATS http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { // Read the entire request body (if any) body, err := ioutil.ReadAll(r.Body) if err != nil { http.Error(w, "Error reading request body", http.StatusInternalServerError) return } defer r.Body.Close() // Create the NATS subject. // Remove the leading slash from the path and replace remaining slashes with dots. // The subject is prefixed with "http...", where is lower-case. // Extract host and reverse domain components host := r.Host // Remove port if present if h, _, err := net.SplitHostPort(host); err == nil { host = h } // Use "_" instead of "." in the domain to make it a single token. domainParts := strings.ReplaceAll(host, ".", "_") // Process path component path := strings.TrimPrefix(r.URL.Path, "/") // Replace all "." with "_" and then all "/" with ".". subjectPath := strings.ReplaceAll(strings.ReplaceAll(path, ".", "_"), "/", ".") // Build final subject subjectBase := "http" subjectParts := []string{subjectBase, domainParts, strings.ToLower(r.Method)} if subjectPath != "" { subjectParts = append(subjectParts, subjectPath) } subject := strings.Join(subjectParts, ".") log.Println("Forwarding HTTP", r.Method, "request on", r.URL.Path, "to NATS subject:", subject) // Create a new NATS message with the HTTP request body msg := nats.Msg{ Subject: subject, Data: body, Header: nats.Header{}, } // Copy over all the HTTP request headers to the NATS message headers for key, values := range r.Header { for _, value := range values { msg.Header.Add(key, value) } } // Add additional HTTP meta-data as headers msg.Header.Set("X-HTTP-Method", r.Method) msg.Header.Set("X-HTTP-Path", r.URL.Path) msg.Header.Set("X-HTTP-Query", r.URL.RawQuery) msg.Header.Set("X-HTTP-Host", r.Host) msg.Header.Set("X-Remote-Addr", r.RemoteAddr) // Send the NATS request and wait synchronously for a reply (timeout: 30 seconds) reply, err := nc.RequestMsg(&msg, 30*time.Second) if err != nil { log.Println("NATS request error:", err) // Handle specific NATS error cases if err == nats.ErrNoResponders || strings.Contains(err.Error(), "no responders") { http.Error(w, "No service available to handle request", http.StatusNotFound) } else { http.Error(w, "Error processing request", http.StatusInternalServerError) } return } // Set any response headers from the NATS reply on the HTTP response for key, values := range reply.Header { for _, value := range values { w.Header().Add(key, value) } } // Write the reply body (from NATS) back to the HTTP client w.Write(reply.Data) }) // Start the HTTP server fmt.Println("Server is running on http://localhost:8080") log.Fatal(http.ListenAndServe(":8080", nil)) }