Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
263 changes: 263 additions & 0 deletions packages/go-plugin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
# Outray Go Client

Expose your localhost Go server to the internet using [Outray](https://outray.dev).

## Installation

```bash
go get github.com/outray-tunnel/outray-go
```

## Quick Start

### Basic Usage

```go
package main

import (
"fmt"
"log"
"net/http"

outray "github.com/outray-tunnel/outray-go"
)

func main() {
// Start your local server
go func() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello from Go!")
})
http.ListenAndServe(":8080", nil)
}()

// Create tunnel
client := outray.New(outray.Options{
LocalPort: 8080,
OnTunnelReady: func(url string, port int) {
fmt.Printf("🚀 Tunnel ready at: %s\n", url)
},
})

if err := client.Start(); err != nil {
log.Fatal(err)
}
defer client.Stop()

// Keep running
select {}
}
```

### With Authentication

```go
client := outray.New(outray.Options{
LocalPort: 8080,
APIKey: "your-api-key", // or set OUTRAY_API_KEY env var
Subdomain: "my-go-app", // or set OUTRAY_SUBDOMAIN env var
OnTunnelReady: func(url string, port int) {
fmt.Printf("Tunnel ready at: %s\n", url)
},
OnError: func(err error, code string) {
fmt.Printf("Error [%s]: %v\n", code, err)
},
})
```

### With Gin

```go
package main

import (
"fmt"
"log"

"github.com/gin-gonic/gin"
outray "github.com/outray-tunnel/outray-go"
)

func main() {
r := gin.Default()

r.GET("/", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "Hello from Gin!"})
})

// Start tunnel
client := outray.New(outray.Options{
LocalPort: 8080,
OnTunnelReady: func(url string, port int) {
fmt.Printf("🚀 Tunnel: %s\n", url)
},
})

if err := client.Start(); err != nil {
log.Fatal(err)
}
defer client.Stop()

// Start Gin server
r.Run(":8080")
}
```

### With Echo

```go
package main

import (
"fmt"
"log"
"net/http"

"github.com/labstack/echo/v4"
outray "github.com/outray-tunnel/outray-go"
)

func main() {
e := echo.New()

e.GET("/", func(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{"message": "Hello from Echo!"})
})

// Start tunnel
client := outray.New(outray.Options{
LocalPort: 8080,
OnTunnelReady: func(url string, port int) {
fmt.Printf("🚀 Tunnel: %s\n", url)
},
})

if err := client.Start(); err != nil {
log.Fatal(err)
}
defer client.Stop()

// Start Echo server
e.Start(":8080")
}
```

### With Fiber

```go
package main

import (
"fmt"
"log"

"github.com/gofiber/fiber/v2"
outray "github.com/outray-tunnel/outray-go"
)

func main() {
app := fiber.New()

app.Get("/", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{"message": "Hello from Fiber!"})
})

// Start tunnel
client := outray.New(outray.Options{
LocalPort: 3000,
OnTunnelReady: func(url string, port int) {
fmt.Printf("🚀 Tunnel: %s\n", url)
},
})

if err := client.Start(); err != nil {
log.Fatal(err)
}
defer client.Stop()

// Start Fiber server
app.Listen(":3000")
}
```

## Options

| Option | Type | Description |
| ---------------- | ---------------------------------------- | ---------------------------------------------------------------- |
| `LocalPort` | `int` | **Required.** The local port to tunnel |
| `ServerURL` | `string` | Outray server URL (default: `wss://api.outray.dev/`) |
| `APIKey` | `string` | API key for authentication (or use `OUTRAY_API_KEY` env var) |
| `Subdomain` | `string` | Request a specific subdomain (or use `OUTRAY_SUBDOMAIN` env var) |
| `CustomDomain` | `string` | Use a custom domain instead of subdomain |
| `Protocol` | `Protocol` | Tunnel protocol: `ProtocolHTTP`, `ProtocolTCP`, `ProtocolUDP` |
| `RemotePort` | `int` | Port to expose on server (for TCP/UDP tunnels) |
| `OnTunnelReady` | `func(url string, port int)` | Called when tunnel is established |
| `OnRequest` | `func(info RequestInfo)` | Called for each proxied request |
| `OnError` | `func(err error, code string)` | Called when an error occurs |
| `OnReconnecting` | `func(attempt int, delay time.Duration)` | Called when reconnecting |
| `OnClose` | `func(reason string)` | Called when tunnel is closed |
| `Silent` | `bool` | Suppress log output |
| `Logger` | `*log.Logger` | Custom logger |

## TCP Tunnels

```go
client := outray.New(outray.Options{
LocalPort: 5432, // PostgreSQL
Protocol: outray.ProtocolTCP,
RemotePort: 5432, // Optional: request specific port
OnTunnelReady: func(url string, port int) {
fmt.Printf("TCP tunnel ready on port %d\n", port)
},
})
```

## UDP Tunnels

```go
client := outray.New(outray.Options{
LocalPort: 53, // DNS
Protocol: outray.ProtocolUDP,
OnTunnelReady: func(url string, port int) {
fmt.Printf("UDP tunnel ready on port %d\n", port)
},
})
```

## Request Logging

```go
client := outray.New(outray.Options{
LocalPort: 8080,
OnRequest: func(info outray.RequestInfo) {
fmt.Printf("%s %s -> %d (%v)\n",
info.Method,
info.Path,
info.StatusCode,
info.Duration,
)
},
})
```

## Environment Variables

| Variable | Description |
| ------------------- | -------------------------- |
| `OUTRAY_API_KEY` | API key for authentication |
| `OUTRAY_SUBDOMAIN` | Requested subdomain |
| `OUTRAY_SERVER_URL` | Custom server URL |

## Error Codes

| Code | Description |
| -------------------- | ------------------------------------- |
| `AUTH_FAILED` | Invalid API key |
| `LIMIT_EXCEEDED` | Tunnel limit exceeded for your plan |
| `SUBDOMAIN_IN_USE` | Requested subdomain is already in use |
| `BANDWIDTH_EXCEEDED` | Monthly bandwidth limit exceeded |

## License

MIT
Binary file added packages/go-plugin/example/example
Binary file not shown.
12 changes: 12 additions & 0 deletions packages/go-plugin/example/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module example

go 1.21

require github.com/outray-tunnel/outray-go v0.0.0

require (
github.com/gorilla/websocket v1.5.1 // indirect
golang.org/x/net v0.17.0 // indirect
)

replace github.com/outray-tunnel/outray-go => ../
4 changes: 4 additions & 0 deletions packages/go-plugin/example/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
83 changes: 83 additions & 0 deletions packages/go-plugin/example/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package main

import (
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"

outray "github.com/outray-tunnel/outray-go"
)

func main() {
// Start a simple HTTP server
go func() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"message": "Hello from Go!", "path": "%s", "method": "%s"}`, r.URL.Path, r.Method)
})

http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"status": "ok", "timestamp": "%s"}`, time.Now().Format(time.RFC3339))
})

log.Println("Starting local server on :8080...")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}()

// Wait for server to start
time.Sleep(100 * time.Millisecond)

// Create and start the tunnel
client := outray.New(outray.Options{
LocalPort: 8080,
// Subdomain: "my-go-app", // Uncomment to request specific subdomain
// APIKey: "your-api-key", // Or set OUTRAY_API_KEY env var

OnTunnelReady: func(url string, port int) {
fmt.Printf("\n🚀 Tunnel ready!\n")
fmt.Printf(" Public URL: %s\n", url)
fmt.Printf(" Local: http://localhost:8080\n\n")
},

OnRequest: func(info outray.RequestInfo) {
statusEmoji := "✅"
if info.StatusCode >= 400 {
statusEmoji = "❌"
}
fmt.Printf("%s %s %s -> %d (%v)\n",
statusEmoji,
info.Method,
info.Path,
info.StatusCode,
info.Duration.Round(time.Millisecond),
)
},

OnError: func(err error, code string) {
fmt.Printf("❌ Error [%s]: %v\n", code, err)
},

OnReconnecting: func(attempt int, delay time.Duration) {
fmt.Printf("🔄 Reconnecting (attempt %d) in %v...\n", attempt, delay)
},
})

if err := client.Start(); err != nil {
log.Fatalf("Failed to start tunnel: %v", err)
}
defer client.Stop()

// Wait for interrupt signal
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
<-sigChan

fmt.Println("\n\nShutting down...")
}
7 changes: 7 additions & 0 deletions packages/go-plugin/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module github.com/outray-tunnel/outray-go

go 1.21

require github.com/gorilla/websocket v1.5.1

require golang.org/x/net v0.17.0 // indirect
4 changes: 4 additions & 0 deletions packages/go-plugin/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
Loading