Browse Source
- Define Transport interface (Name, Start, Stop, Addresses) at pkg/interfaces/transport/ - Add Transport Manager with ordered startup/shutdown at pkg/transport/ - Create TCP transport (pkg/transport/tcp/) wrapping plain http.Server - Create TLS transport (pkg/transport/tls/) with ACME + manual certs - Create Tor transport (pkg/transport/tor/) wrapping existing pkg/tor service - Replace ~220 lines of inline transport code in app/main.go with manager - Simplify shutdown handler to single transportMgr.StopAll() call - Use transportMgr.Addresses() for NIP-11 relay info (replaces torService) Files modified: - pkg/interfaces/transport/transport.go: New transport interface - pkg/transport/manager.go: New transport manager - pkg/transport/tcp/tcp.go: New TCP transport - pkg/transport/tls/tls.go: New TLS/ACME transport (moved from app/tls.go) - pkg/transport/tor/tor.go: New Tor transport wrapper - app/main.go: Use transport manager for all networking - app/server.go: Replace torService field with transportMgr - app/handle-relayinfo.go: Use transportMgr.Addresses() - app/tls.go: Deleted (moved to pkg/transport/tls/) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>imwald-v0.58.10
9 changed files with 557 additions and 258 deletions
@ -1,132 +0,0 @@ |
|||||||
package app |
|
||||||
|
|
||||||
import ( |
|
||||||
"crypto/tls" |
|
||||||
"crypto/x509" |
|
||||||
"fmt" |
|
||||||
"strings" |
|
||||||
"sync" |
|
||||||
|
|
||||||
"golang.org/x/crypto/acme/autocert" |
|
||||||
"lol.mleku.dev/chk" |
|
||||||
"lol.mleku.dev/log" |
|
||||||
) |
|
||||||
|
|
||||||
// TLSConfig returns a TLS configuration that works with LetsEncrypt automatic SSL cert issuer
|
|
||||||
// as well as any provided certificate files from providers.
|
|
||||||
//
|
|
||||||
// The certs are provided in the form of paths where .pem and .key files exist
|
|
||||||
func TLSConfig(m *autocert.Manager, certs ...string) (tc *tls.Config) { |
|
||||||
certMap := make(map[string]*tls.Certificate) |
|
||||||
var mx sync.Mutex |
|
||||||
|
|
||||||
for _, certPath := range certs { |
|
||||||
if certPath == "" { |
|
||||||
continue |
|
||||||
} |
|
||||||
|
|
||||||
var err error |
|
||||||
var c tls.Certificate |
|
||||||
|
|
||||||
// Load certificate and key files
|
|
||||||
if c, err = tls.LoadX509KeyPair( |
|
||||||
certPath+".pem", certPath+".key", |
|
||||||
); chk.E(err) { |
|
||||||
log.E.F("failed to load certificate from %s: %v", certPath, err) |
|
||||||
continue |
|
||||||
} |
|
||||||
|
|
||||||
// Extract domain names from certificate
|
|
||||||
if len(c.Certificate) > 0 { |
|
||||||
if x509Cert, err := x509.ParseCertificate(c.Certificate[0]); err == nil { |
|
||||||
// Use the common name as the primary domain
|
|
||||||
if x509Cert.Subject.CommonName != "" { |
|
||||||
certMap[x509Cert.Subject.CommonName] = &c |
|
||||||
log.I.F("loaded certificate for domain: %s", x509Cert.Subject.CommonName) |
|
||||||
} |
|
||||||
// Also add any subject alternative names
|
|
||||||
for _, san := range x509Cert.DNSNames { |
|
||||||
if san != "" { |
|
||||||
certMap[san] = &c |
|
||||||
log.I.F("loaded certificate for SAN domain: %s", san) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if m == nil { |
|
||||||
// Create a basic TLS config without autocert
|
|
||||||
tc = &tls.Config{ |
|
||||||
GetCertificate: func(helo *tls.ClientHelloInfo) (*tls.Certificate, error) { |
|
||||||
mx.Lock() |
|
||||||
defer mx.Unlock() |
|
||||||
|
|
||||||
// Check for exact match first
|
|
||||||
if cert, exists := certMap[helo.ServerName]; exists { |
|
||||||
return cert, nil |
|
||||||
} |
|
||||||
|
|
||||||
// Check for wildcard matches
|
|
||||||
for domain, cert := range certMap { |
|
||||||
if strings.HasPrefix(domain, "*.") { |
|
||||||
baseDomain := domain[2:] // Remove "*."
|
|
||||||
if strings.HasSuffix(helo.ServerName, baseDomain) { |
|
||||||
return cert, nil |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return nil, fmt.Errorf("no certificate found for %s", helo.ServerName) |
|
||||||
}, |
|
||||||
} |
|
||||||
} else { |
|
||||||
tc = m.TLSConfig() |
|
||||||
tc.GetCertificate = func(helo *tls.ClientHelloInfo) (*tls.Certificate, error) { |
|
||||||
mx.Lock() |
|
||||||
|
|
||||||
// Check for exact match first
|
|
||||||
if cert, exists := certMap[helo.ServerName]; exists { |
|
||||||
mx.Unlock() |
|
||||||
return cert, nil |
|
||||||
} |
|
||||||
|
|
||||||
// Check for wildcard matches
|
|
||||||
for domain, cert := range certMap { |
|
||||||
if strings.HasPrefix(domain, "*.") { |
|
||||||
baseDomain := domain[2:] // Remove "*."
|
|
||||||
if strings.HasSuffix(helo.ServerName, baseDomain) { |
|
||||||
mx.Unlock() |
|
||||||
return cert, nil |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
mx.Unlock() |
|
||||||
|
|
||||||
// Fall back to autocert for domains not in our certificate map
|
|
||||||
return m.GetCertificate(helo) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return tc |
|
||||||
} |
|
||||||
|
|
||||||
// ValidateTLSConfig checks if the TLS configuration is valid
|
|
||||||
func ValidateTLSConfig(domains []string, certs []string) (err error) { |
|
||||||
if len(domains) == 0 { |
|
||||||
return fmt.Errorf("no TLS domains specified") |
|
||||||
} |
|
||||||
|
|
||||||
// Validate domain names
|
|
||||||
for _, domain := range domains { |
|
||||||
if domain == "" { |
|
||||||
continue |
|
||||||
} |
|
||||||
if strings.Contains(domain, " ") || strings.Contains(domain, "\t") { |
|
||||||
return fmt.Errorf("invalid domain name: %s", domain) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return nil |
|
||||||
} |
|
||||||
@ -0,0 +1,16 @@ |
|||||||
|
// Package transport defines the interface for pluggable network transports.
|
||||||
|
package transport |
||||||
|
|
||||||
|
import "context" |
||||||
|
|
||||||
|
// Transport represents a network transport that serves the relay.
|
||||||
|
type Transport interface { |
||||||
|
// Name returns the transport identifier (e.g., "tcp", "tls", "tor").
|
||||||
|
Name() string |
||||||
|
// Start begins accepting connections through this transport.
|
||||||
|
Start(ctx context.Context) error |
||||||
|
// Stop gracefully shuts down the transport.
|
||||||
|
Stop(ctx context.Context) error |
||||||
|
// Addresses returns the addresses this transport is reachable on.
|
||||||
|
Addresses() []string |
||||||
|
} |
||||||
@ -0,0 +1,86 @@ |
|||||||
|
// Package transport provides a manager for pluggable network transports.
|
||||||
|
package transport |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"fmt" |
||||||
|
"sync" |
||||||
|
|
||||||
|
"lol.mleku.dev/log" |
||||||
|
|
||||||
|
iface "next.orly.dev/pkg/interfaces/transport" |
||||||
|
) |
||||||
|
|
||||||
|
// Manager manages multiple transports and coordinates their lifecycle.
|
||||||
|
type Manager struct { |
||||||
|
mu sync.RWMutex |
||||||
|
transports []iface.Transport |
||||||
|
} |
||||||
|
|
||||||
|
// NewManager creates a new transport manager.
|
||||||
|
func NewManager() *Manager { |
||||||
|
return &Manager{} |
||||||
|
} |
||||||
|
|
||||||
|
// Add registers a transport with the manager.
|
||||||
|
func (m *Manager) Add(t iface.Transport) { |
||||||
|
m.mu.Lock() |
||||||
|
defer m.mu.Unlock() |
||||||
|
m.transports = append(m.transports, t) |
||||||
|
} |
||||||
|
|
||||||
|
// StartAll starts all registered transports in order.
|
||||||
|
// If any transport fails to start, previously started transports are stopped.
|
||||||
|
func (m *Manager) StartAll(ctx context.Context) error { |
||||||
|
m.mu.RLock() |
||||||
|
defer m.mu.RUnlock() |
||||||
|
|
||||||
|
for i, t := range m.transports { |
||||||
|
log.I.F("starting transport: %s", t.Name()) |
||||||
|
if err := t.Start(ctx); err != nil { |
||||||
|
// Stop previously started transports in reverse order
|
||||||
|
for j := i - 1; j >= 0; j-- { |
||||||
|
if stopErr := m.transports[j].Stop(ctx); stopErr != nil { |
||||||
|
log.E.F("failed to stop transport %s during rollback: %v", |
||||||
|
m.transports[j].Name(), stopErr) |
||||||
|
} |
||||||
|
} |
||||||
|
return fmt.Errorf("transport %s failed to start: %w", t.Name(), err) |
||||||
|
} |
||||||
|
log.I.F("transport started: %s", t.Name()) |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// StopAll stops all transports in reverse order.
|
||||||
|
func (m *Manager) StopAll(ctx context.Context) error { |
||||||
|
m.mu.RLock() |
||||||
|
defer m.mu.RUnlock() |
||||||
|
|
||||||
|
var firstErr error |
||||||
|
for i := len(m.transports) - 1; i >= 0; i-- { |
||||||
|
t := m.transports[i] |
||||||
|
log.I.F("stopping transport: %s", t.Name()) |
||||||
|
if err := t.Stop(ctx); err != nil { |
||||||
|
log.E.F("failed to stop transport %s: %v", t.Name(), err) |
||||||
|
if firstErr == nil { |
||||||
|
firstErr = err |
||||||
|
} |
||||||
|
} else { |
||||||
|
log.I.F("transport stopped: %s", t.Name()) |
||||||
|
} |
||||||
|
} |
||||||
|
return firstErr |
||||||
|
} |
||||||
|
|
||||||
|
// Addresses returns all addresses from all transports.
|
||||||
|
func (m *Manager) Addresses() []string { |
||||||
|
m.mu.RLock() |
||||||
|
defer m.mu.RUnlock() |
||||||
|
|
||||||
|
var addrs []string |
||||||
|
for _, t := range m.transports { |
||||||
|
addrs = append(addrs, t.Addresses()...) |
||||||
|
} |
||||||
|
return addrs |
||||||
|
} |
||||||
@ -0,0 +1,78 @@ |
|||||||
|
// Package tcp provides a plain HTTP transport for the relay.
|
||||||
|
package tcp |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"fmt" |
||||||
|
"net/http" |
||||||
|
"sync" |
||||||
|
|
||||||
|
"lol.mleku.dev/log" |
||||||
|
) |
||||||
|
|
||||||
|
// Config holds TCP transport configuration.
|
||||||
|
type Config struct { |
||||||
|
// Addr is the listen address (e.g., "0.0.0.0:3334").
|
||||||
|
Addr string |
||||||
|
// Handler is the HTTP handler to serve.
|
||||||
|
Handler http.Handler |
||||||
|
} |
||||||
|
|
||||||
|
// Transport serves HTTP over plain TCP.
|
||||||
|
type Transport struct { |
||||||
|
cfg *Config |
||||||
|
server *http.Server |
||||||
|
mu sync.Mutex |
||||||
|
} |
||||||
|
|
||||||
|
// New creates a new TCP transport.
|
||||||
|
func New(cfg *Config) *Transport { |
||||||
|
return &Transport{cfg: cfg} |
||||||
|
} |
||||||
|
|
||||||
|
func (t *Transport) Name() string { return "tcp" } |
||||||
|
|
||||||
|
func (t *Transport) Start(ctx context.Context) error { |
||||||
|
t.mu.Lock() |
||||||
|
defer t.mu.Unlock() |
||||||
|
|
||||||
|
t.server = &http.Server{ |
||||||
|
Addr: t.cfg.Addr, |
||||||
|
Handler: t.cfg.Handler, |
||||||
|
} |
||||||
|
|
||||||
|
log.I.F("starting listener on http://%s", t.cfg.Addr) |
||||||
|
|
||||||
|
errCh := make(chan error, 1) |
||||||
|
go func() { |
||||||
|
if err := t.server.ListenAndServe(); err != nil && err != http.ErrServerClosed { |
||||||
|
errCh <- err |
||||||
|
} |
||||||
|
close(errCh) |
||||||
|
}() |
||||||
|
|
||||||
|
// Give the server a moment to fail on bind errors
|
||||||
|
select { |
||||||
|
case err := <-errCh: |
||||||
|
if err != nil { |
||||||
|
return fmt.Errorf("tcp listen on %s: %w", t.cfg.Addr, err) |
||||||
|
} |
||||||
|
default: |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (t *Transport) Stop(ctx context.Context) error { |
||||||
|
t.mu.Lock() |
||||||
|
defer t.mu.Unlock() |
||||||
|
|
||||||
|
if t.server == nil { |
||||||
|
return nil |
||||||
|
} |
||||||
|
return t.server.Shutdown(ctx) |
||||||
|
} |
||||||
|
|
||||||
|
func (t *Transport) Addresses() []string { |
||||||
|
return []string{"ws://" + t.cfg.Addr + "/"} |
||||||
|
} |
||||||
@ -0,0 +1,247 @@ |
|||||||
|
// Package tls provides a TLS/ACME transport for the relay.
|
||||||
|
package tls |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"crypto/tls" |
||||||
|
"crypto/x509" |
||||||
|
"fmt" |
||||||
|
"net/http" |
||||||
|
"os" |
||||||
|
"path/filepath" |
||||||
|
"strings" |
||||||
|
"sync" |
||||||
|
|
||||||
|
"golang.org/x/crypto/acme/autocert" |
||||||
|
"lol.mleku.dev/chk" |
||||||
|
"lol.mleku.dev/log" |
||||||
|
) |
||||||
|
|
||||||
|
// Config holds TLS transport configuration.
|
||||||
|
type Config struct { |
||||||
|
// Domains is the list of domains for ACME auto-cert.
|
||||||
|
Domains []string |
||||||
|
// Certs is a list of manual certificate paths (without extension).
|
||||||
|
// For each path, .pem and .key files are loaded.
|
||||||
|
Certs []string |
||||||
|
// DataDir is the base data directory for the autocert cache.
|
||||||
|
DataDir string |
||||||
|
// Handler is the HTTP handler to serve.
|
||||||
|
Handler http.Handler |
||||||
|
} |
||||||
|
|
||||||
|
// Transport serves HTTPS with automatic or manual TLS certificates.
|
||||||
|
// It runs two servers: HTTPS on :443 and HTTP on :80 for ACME challenges.
|
||||||
|
type Transport struct { |
||||||
|
cfg *Config |
||||||
|
tlsServer *http.Server |
||||||
|
httpServer *http.Server |
||||||
|
mu sync.Mutex |
||||||
|
} |
||||||
|
|
||||||
|
// New creates a new TLS transport.
|
||||||
|
func New(cfg *Config) *Transport { |
||||||
|
return &Transport{cfg: cfg} |
||||||
|
} |
||||||
|
|
||||||
|
func (t *Transport) Name() string { return "tls" } |
||||||
|
|
||||||
|
func (t *Transport) Start(ctx context.Context) error { |
||||||
|
t.mu.Lock() |
||||||
|
defer t.mu.Unlock() |
||||||
|
|
||||||
|
if err := ValidateConfig(t.cfg.Domains, t.cfg.Certs); err != nil { |
||||||
|
return fmt.Errorf("invalid TLS configuration: %w", err) |
||||||
|
} |
||||||
|
|
||||||
|
// Create cache directory for autocert
|
||||||
|
cacheDir := filepath.Join(t.cfg.DataDir, "autocert") |
||||||
|
if err := os.MkdirAll(cacheDir, 0700); err != nil { |
||||||
|
return fmt.Errorf("failed to create autocert cache directory: %w", err) |
||||||
|
} |
||||||
|
|
||||||
|
// Set up autocert manager
|
||||||
|
m := &autocert.Manager{ |
||||||
|
Prompt: autocert.AcceptTOS, |
||||||
|
Cache: autocert.DirCache(cacheDir), |
||||||
|
HostPolicy: autocert.HostWhitelist(t.cfg.Domains...), |
||||||
|
} |
||||||
|
|
||||||
|
// Create TLS server on port 443
|
||||||
|
t.tlsServer = &http.Server{ |
||||||
|
Addr: ":443", |
||||||
|
Handler: t.cfg.Handler, |
||||||
|
TLSConfig: tlsConfig(m, t.cfg.Certs...), |
||||||
|
} |
||||||
|
|
||||||
|
// Create HTTP server for ACME challenges and redirects on port 80
|
||||||
|
t.httpServer = &http.Server{ |
||||||
|
Addr: ":80", |
||||||
|
Handler: m.HTTPHandler(nil), |
||||||
|
} |
||||||
|
|
||||||
|
log.I.F("TLS enabled for domains: %v", t.cfg.Domains) |
||||||
|
|
||||||
|
// Start TLS server
|
||||||
|
go func() { |
||||||
|
log.I.F("starting TLS listener on https://:443") |
||||||
|
if err := t.tlsServer.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed { |
||||||
|
log.E.F("TLS server error: %v", err) |
||||||
|
} |
||||||
|
}() |
||||||
|
|
||||||
|
// Start HTTP server for ACME challenges
|
||||||
|
go func() { |
||||||
|
log.I.F("starting HTTP listener on http://:80 for ACME challenges") |
||||||
|
if err := t.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { |
||||||
|
log.E.F("HTTP server error: %v", err) |
||||||
|
} |
||||||
|
}() |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (t *Transport) Stop(ctx context.Context) error { |
||||||
|
t.mu.Lock() |
||||||
|
defer t.mu.Unlock() |
||||||
|
|
||||||
|
var firstErr error |
||||||
|
|
||||||
|
if t.tlsServer != nil { |
||||||
|
if err := t.tlsServer.Shutdown(ctx); err != nil { |
||||||
|
log.E.F("TLS server shutdown error: %v", err) |
||||||
|
firstErr = err |
||||||
|
} else { |
||||||
|
log.I.F("TLS server shutdown completed") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if t.httpServer != nil { |
||||||
|
if err := t.httpServer.Shutdown(ctx); err != nil { |
||||||
|
log.E.F("HTTP server shutdown error: %v", err) |
||||||
|
if firstErr == nil { |
||||||
|
firstErr = err |
||||||
|
} |
||||||
|
} else { |
||||||
|
log.I.F("HTTP server shutdown completed") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return firstErr |
||||||
|
} |
||||||
|
|
||||||
|
func (t *Transport) Addresses() []string { |
||||||
|
var addrs []string |
||||||
|
for _, domain := range t.cfg.Domains { |
||||||
|
addrs = append(addrs, "wss://"+domain+"/") |
||||||
|
} |
||||||
|
return addrs |
||||||
|
} |
||||||
|
|
||||||
|
// ValidateConfig checks if the TLS configuration is valid.
|
||||||
|
func ValidateConfig(domains []string, certs []string) error { |
||||||
|
if len(domains) == 0 { |
||||||
|
return fmt.Errorf("no TLS domains specified") |
||||||
|
} |
||||||
|
|
||||||
|
for _, domain := range domains { |
||||||
|
if domain == "" { |
||||||
|
continue |
||||||
|
} |
||||||
|
if strings.Contains(domain, " ") || strings.Contains(domain, "\t") { |
||||||
|
return fmt.Errorf("invalid domain name: %s", domain) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// tlsConfig returns a TLS configuration that works with LetsEncrypt automatic
|
||||||
|
// SSL cert issuer as well as any provided certificate files.
|
||||||
|
//
|
||||||
|
// Certs are provided as paths where .pem and .key files exist.
|
||||||
|
func tlsConfig(m *autocert.Manager, certs ...string) *tls.Config { |
||||||
|
certMap := make(map[string]*tls.Certificate) |
||||||
|
var mx sync.Mutex |
||||||
|
|
||||||
|
for _, certPath := range certs { |
||||||
|
if certPath == "" { |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
var err error |
||||||
|
var c tls.Certificate |
||||||
|
|
||||||
|
if c, err = tls.LoadX509KeyPair( |
||||||
|
certPath+".pem", certPath+".key", |
||||||
|
); chk.E(err) { |
||||||
|
log.E.F("failed to load certificate from %s: %v", certPath, err) |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
if len(c.Certificate) > 0 { |
||||||
|
if x509Cert, err := x509.ParseCertificate(c.Certificate[0]); err == nil { |
||||||
|
if x509Cert.Subject.CommonName != "" { |
||||||
|
certMap[x509Cert.Subject.CommonName] = &c |
||||||
|
log.I.F("loaded certificate for domain: %s", x509Cert.Subject.CommonName) |
||||||
|
} |
||||||
|
for _, san := range x509Cert.DNSNames { |
||||||
|
if san != "" { |
||||||
|
certMap[san] = &c |
||||||
|
log.I.F("loaded certificate for SAN domain: %s", san) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if m == nil { |
||||||
|
return &tls.Config{ |
||||||
|
GetCertificate: func(helo *tls.ClientHelloInfo) (*tls.Certificate, error) { |
||||||
|
mx.Lock() |
||||||
|
defer mx.Unlock() |
||||||
|
|
||||||
|
if cert, exists := certMap[helo.ServerName]; exists { |
||||||
|
return cert, nil |
||||||
|
} |
||||||
|
|
||||||
|
for domain, cert := range certMap { |
||||||
|
if strings.HasPrefix(domain, "*.") { |
||||||
|
baseDomain := domain[2:] |
||||||
|
if strings.HasSuffix(helo.ServerName, baseDomain) { |
||||||
|
return cert, nil |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return nil, fmt.Errorf("no certificate found for %s", helo.ServerName) |
||||||
|
}, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
tc := m.TLSConfig() |
||||||
|
tc.GetCertificate = func(helo *tls.ClientHelloInfo) (*tls.Certificate, error) { |
||||||
|
mx.Lock() |
||||||
|
|
||||||
|
if cert, exists := certMap[helo.ServerName]; exists { |
||||||
|
mx.Unlock() |
||||||
|
return cert, nil |
||||||
|
} |
||||||
|
|
||||||
|
for domain, cert := range certMap { |
||||||
|
if strings.HasPrefix(domain, "*.") { |
||||||
|
baseDomain := domain[2:] |
||||||
|
if strings.HasSuffix(helo.ServerName, baseDomain) { |
||||||
|
mx.Unlock() |
||||||
|
return cert, nil |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
mx.Unlock() |
||||||
|
|
||||||
|
return m.GetCertificate(helo) |
||||||
|
} |
||||||
|
|
||||||
|
return tc |
||||||
|
} |
||||||
@ -0,0 +1,91 @@ |
|||||||
|
// Package tor provides a Tor hidden service transport for the relay.
|
||||||
|
// It wraps the existing pkg/tor service as a pluggable transport.
|
||||||
|
package tor |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"net/http" |
||||||
|
|
||||||
|
"lol.mleku.dev/log" |
||||||
|
|
||||||
|
torservice "next.orly.dev/pkg/tor" |
||||||
|
) |
||||||
|
|
||||||
|
// Config holds Tor transport configuration.
|
||||||
|
type Config struct { |
||||||
|
// Port is the internal port for the hidden service.
|
||||||
|
Port int |
||||||
|
// DataDir is the directory for Tor data (torrc, keys, hostname, etc.).
|
||||||
|
DataDir string |
||||||
|
// Binary is the path to the tor executable.
|
||||||
|
Binary string |
||||||
|
// SOCKSPort is the port for outbound SOCKS connections (0 = disabled).
|
||||||
|
SOCKSPort int |
||||||
|
// Handler is the HTTP handler to serve.
|
||||||
|
Handler http.Handler |
||||||
|
} |
||||||
|
|
||||||
|
// Transport serves the relay as a Tor hidden service.
|
||||||
|
type Transport struct { |
||||||
|
cfg *Config |
||||||
|
service *torservice.Service |
||||||
|
} |
||||||
|
|
||||||
|
// New creates a new Tor transport.
|
||||||
|
func New(cfg *Config) *Transport { |
||||||
|
return &Transport{cfg: cfg} |
||||||
|
} |
||||||
|
|
||||||
|
func (t *Transport) Name() string { return "tor" } |
||||||
|
|
||||||
|
func (t *Transport) Start(ctx context.Context) error { |
||||||
|
svcCfg := &torservice.Config{ |
||||||
|
Port: t.cfg.Port, |
||||||
|
DataDir: t.cfg.DataDir, |
||||||
|
Binary: t.cfg.Binary, |
||||||
|
SOCKSPort: t.cfg.SOCKSPort, |
||||||
|
Handler: t.cfg.Handler, |
||||||
|
} |
||||||
|
|
||||||
|
var err error |
||||||
|
t.service, err = torservice.New(svcCfg) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
if err = t.service.Start(); err != nil { |
||||||
|
t.service = nil |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
if addr := t.service.OnionWSAddress(); addr != "" { |
||||||
|
log.I.F("Tor hidden service listening on port %d, address: %s", t.cfg.Port, addr) |
||||||
|
} else { |
||||||
|
log.I.F("Tor hidden service listening on port %d (waiting for .onion address)", t.cfg.Port) |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (t *Transport) Stop(ctx context.Context) error { |
||||||
|
if t.service == nil { |
||||||
|
return nil |
||||||
|
} |
||||||
|
return t.service.Stop() |
||||||
|
} |
||||||
|
|
||||||
|
func (t *Transport) Addresses() []string { |
||||||
|
if t.service == nil { |
||||||
|
return nil |
||||||
|
} |
||||||
|
if addr := t.service.OnionWSAddress(); addr != "" { |
||||||
|
return []string{addr} |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Service returns the underlying Tor service for access to Tor-specific
|
||||||
|
// functionality (e.g., OnionAddress, DataDir).
|
||||||
|
func (t *Transport) Service() *torservice.Service { |
||||||
|
return t.service |
||||||
|
} |
||||||
Loading…
Reference in new issue