Files
go-sam-go/stream_test.go
2025-10-06 20:24:49 -04:00

408 lines
10 KiB
Go

package sam3
import (
"fmt"
"net"
"strings"
"testing"
"time"
"github.com/go-i2p/i2pkeys"
)
/*
* This file contains tests and examples for the stream session functionality of the sam3 package.
* It was copied directly from the sam3 package and modified to fit the current context.
* The tests cover creating stream sessions, dialing I2P addresses, and establishing
* server-client communication over I2P streams. Examples demonstrate basic usage of
* the sam3 library for connecting to I2P and creating stream sessions.
*
* Note: These tests require a running I2P router with SAM bridge enabled.
*/
func Test_StreamingDial(t *testing.T) {
if testing.Short() {
return
}
fmt.Println("Test_StreamingDial")
// Set up a local test listener instead of using external site
testListener := SetupTestListenerWithHTTP(t, generateUniqueSessionID("streaming_dial_listener"))
defer testListener.Close()
sam, err := NewSAM(SAMDefaultAddr(""))
if err != nil {
fmt.Println(err.Error())
t.Fail()
return
}
defer sam.Close()
keys, err := sam.NewKeys()
if err != nil {
fmt.Println(err.Error())
t.Fail()
return
}
fmt.Println("\tBuilding tunnel")
ss, err := sam.NewStreamSession("streamTun", keys, []string{"inbound.length=1", "outbound.length=1", "inbound.lengthVariance=0", "outbound.lengthVariance=0", "inbound.quantity=1", "outbound.quantity=1"})
if err != nil {
fmt.Println(err.Error())
t.Fail()
return
}
fmt.Println("\tNotice: Using local test listener instead of external I2P site for improved test stability.")
fmt.Printf("\tDialing test listener (%s)\n", testListener.AddrString())
conn, err := ss.DialI2P(testListener.Addr())
if err != nil {
fmt.Println(err.Error())
t.Fail()
return
}
defer conn.Close()
fmt.Println("\tSending HTTP GET /")
if _, err := conn.Write([]byte("GET /\n")); err != nil {
fmt.Println(err.Error())
t.Fail()
return
}
buf := make([]byte, 4096)
n, err := conn.Read(buf)
if !strings.Contains(strings.ToLower(string(buf[:n])), "http") && !strings.Contains(strings.ToLower(string(buf[:n])), "html") {
fmt.Printf("\tProbably failed to StreamSession.DialI2P(test listener)? It replied %d bytes, but nothing that looked like http/html", n)
} else {
fmt.Println("\tRead HTTP/HTML from test listener")
}
}
func Test_StreamingServerClient(t *testing.T) {
if testing.Short() {
return
}
fmt.Println("Test_StreamingServerClient")
sam, err := NewSAM(SAMDefaultAddr(""))
if err != nil {
t.Fail()
return
}
defer sam.Close()
keys, err := sam.NewKeys()
if err != nil {
t.Fail()
return
}
fmt.Println("\tServer: Creating tunnel")
ss, err := sam.NewStreamSession("serverTun", keys, []string{"inbound.length=1", "outbound.length=1", "inbound.lengthVariance=0", "outbound.lengthVariance=0", "inbound.quantity=1", "outbound.quantity=1"})
if err != nil {
return
}
c, w := make(chan bool), make(chan bool)
go func(c, w chan (bool)) {
if !(<-w) {
return
}
sam2, err := NewSAM(SAMDefaultAddr(""))
if err != nil {
c <- false
return
}
defer sam2.Close()
keys, err := sam2.NewKeys()
if err != nil {
c <- false
return
}
fmt.Println("\tClient: Creating tunnel")
ss2, err := sam2.NewStreamSession("clientTun", keys, []string{"inbound.length=1", "outbound.length=1", "inbound.lengthVariance=0", "outbound.lengthVariance=0", "inbound.quantity=1", "outbound.quantity=1"})
if err != nil {
c <- false
return
}
fmt.Println("\tClient: Connecting to server")
conn, err := ss2.DialI2P(ss.Addr())
if err != nil {
c <- false
return
}
fmt.Println("\tClient: Connected to tunnel")
defer conn.Close()
_, err = conn.Write([]byte("Hello world <3 <3 <3 <3 <3 <3"))
if err != nil {
c <- false
return
}
c <- true
}(c, w)
l, err := ss.Listen()
if err != nil {
fmt.Println("ss.Listen(): " + err.Error())
t.Fail()
w <- false
return
}
defer l.Close()
w <- true
fmt.Println("\tServer: Accept()ing on tunnel")
conn, err := l.Accept()
if err != nil {
t.Fail()
fmt.Println("Failed to Accept(): " + err.Error())
return
}
defer conn.Close()
buf := make([]byte, 512)
n, err := conn.Read(buf)
fmt.Printf("\tClient exited successfully: %t\n", <-c)
fmt.Println("\tServer: received from Client: " + string(buf[:n]))
}
func ExampleStreamSession() {
// Creates a new StreamingSession with a local server and client.
// Demonstrates how a SAMConn behaves just like a normal net.Conn.
//
// Requirements: This example requires a running I2P router with SAM bridge enabled.
const samBridge = "127.0.0.1:7656"
// Create server session first
server_sam, err := NewSAM(samBridge)
if err != nil {
fmt.Printf("Failed to connect to I2P SAM bridge: %v", err)
return
}
defer server_sam.Close()
server_keys, err := server_sam.NewKeys()
if err != nil {
fmt.Printf("Failed to generate server I2P keys: %v", err)
return
}
server_session, err := server_sam.NewStreamSession("stream_server", server_keys, Options_Small)
if err != nil {
fmt.Printf("Failed to create server stream session: %v", err)
return
}
defer server_session.Close()
// Synchronization channels
serverReady := make(chan bool)
clientDone := make(chan bool)
// Server goroutine - listens and responds with HTTP-like content
go func() {
listener, err := server_session.Listen()
if err != nil {
fmt.Printf("Server failed to listen: %v", err)
serverReady <- false
return
}
defer listener.Close()
serverReady <- true
conn, err := listener.Accept()
if err != nil {
return
}
defer conn.Close()
// Read client request
buf := make([]byte, 256)
_, _ = conn.Read(buf)
// Send HTTP-like response
response := "HTTP/1.0 200 OK\r\nContent-Type: text/html\r\n\r\n<html><body>Test</body></html>"
conn.Write([]byte(response))
}()
// Wait for server to be ready
if !<-serverReady {
return
}
// Client operations in separate goroutine
go func() {
client_sam, err := NewSAM(samBridge)
if err != nil {
fmt.Printf("Client failed to connect to I2P: %v", err)
clientDone <- false
return
}
defer client_sam.Close()
client_keys, err := client_sam.NewKeys()
if err != nil {
fmt.Printf("Client failed to generate keys: %v", err)
clientDone <- false
return
}
client_session, err := client_sam.NewStreamSession("stream_client", client_keys, Options_Small)
if err != nil {
fmt.Printf("Client failed to create session: %v", err)
clientDone <- false
return
}
defer client_session.Close()
// I2P tunnel establishment can take 30-120 seconds, so we retry dialing
var conn net.Conn
var dialErr error
for attempt := 0; attempt < 6; attempt++ {
conn, dialErr = client_session.DialI2P(server_keys.Addr())
if dialErr == nil {
break
}
if attempt < 5 {
// Exponential backoff: 15s, 30s, 45s, 60s, 75s
sleepTime := time.Duration(15*(attempt+1)) * time.Second
fmt.Printf("Dial attempt %d failed: %v. Retrying in %v...\n", attempt+1, dialErr, sleepTime)
time.Sleep(sleepTime)
}
}
if dialErr != nil {
fmt.Printf("Client failed to dial server after retries: %v", dialErr)
clientDone <- false
return
}
defer conn.Close()
fmt.Println("Sending HTTP GET /")
if _, err := conn.Write([]byte("GET /\n")); err != nil {
fmt.Printf("Failed to write to connection: %v", err)
clientDone <- false
return
}
buf := make([]byte, 4096)
n, err := conn.Read(buf)
if err != nil {
fmt.Printf("Failed to read from connection: %v", err)
clientDone <- false
return
}
response := string(buf[:n])
if strings.Contains(strings.ToLower(response), "http") || strings.Contains(strings.ToLower(response), "html") {
fmt.Println("Read HTTP/HTML from test destination")
} else {
fmt.Printf("Failed to get HTTP/HTML response (got %d bytes)", n)
}
clientDone <- true
}()
// Wait for client to complete
<-clientDone
// Output:
// Sending HTTP GET /
// Read HTTP/HTML from test destination
}
func ExampleStreamListener() {
// One server Accept()ing on a StreamListener, and one client that Dials
// through I2P to the server. Server writes "Hello world!" through a SAMConn
// (which implements net.Conn) and the client prints the message.
//
// Requirements: This example requires a running I2P router with SAM bridge enabled.
const samBridge = "127.0.0.1:7656"
server_sam, err := NewSAM(samBridge)
if err != nil {
fmt.Printf("Failed to connect to I2P SAM bridge: %v", err)
return
}
defer server_sam.Close()
keys, err := server_sam.NewKeys()
if err != nil {
fmt.Printf("Failed to generate I2P keys: %v", err)
return
}
// Create server session BEFORE starting client goroutine
server_session, err := server_sam.NewStreamSession("server_example", keys, Options_Small)
if err != nil {
fmt.Printf("Failed to create server session: %v", err)
return
}
// Channels for synchronization and results
quit := make(chan bool)
serverReady := make(chan bool)
// Client connecting to the server - waits for server readiness signal
go func(server i2pkeys.I2PAddr) {
// Wait for server to be fully ready (session + listener created)
if !<-serverReady {
quit <- false
return
}
client_sam, err := NewSAM(samBridge)
if err != nil {
fmt.Printf("Client failed to connect to I2P: %v", err)
quit <- false
return
}
defer client_sam.Close()
keys, err := client_sam.NewKeys()
if err != nil {
fmt.Printf("Client failed to generate keys: %v", err)
quit <- false
return
}
client_session, err := client_sam.NewStreamSession("client_example", keys, Options_Small)
if err != nil {
fmt.Printf("Client failed to create session: %v", err)
quit <- false
return
}
client_conn, err := client_session.DialI2P(server)
if err != nil {
fmt.Printf("Client failed to dial server: %v", err)
quit <- false
return
}
buf := make([]byte, 256)
n, err := client_conn.Read(buf)
if err != nil {
fmt.Printf("Client failed to read: %v", err)
quit <- false
return
}
fmt.Println(string(buf[:n]))
quit <- true
}(keys.Addr()) // end of client - pass server address
// Create listener and signal client can proceed
l, err := server_session.Listen()
if err != nil {
fmt.Printf("Failed to listen: %v", err)
serverReady <- false // Signal failure to client
return
}
defer l.Close()
serverReady <- true // Signal success to client
conn, err := l.Accept()
if err != nil {
fmt.Printf("Failed to accept connection: %v", err)
return
}
_, err = conn.Write([]byte("Hello world!"))
if err != nil {
fmt.Printf("Failed to write to client: %v", err)
return
}
success := <-quit // waits for client to complete
if !success {
fmt.Printf("Client operation failed")
return
}
// Output:
// Hello world!
}