Files
seanime-docker/seanime-2.9.10/internal/nakama/connect.go
2025-09-20 14:08:38 +01:00

215 lines
5.7 KiB
Go

package nakama
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"fmt"
"io"
"github.com/huin/goupnp/dcps/internetgateway1"
"github.com/huin/goupnp/dcps/internetgateway2"
)
type UPnPClient interface {
GetExternalIPAddress() (string, error)
AddPortMapping(string, uint16, string, uint16, string, bool, string, uint32) error
DeletePortMapping(string, uint16, string) error
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Port forwarding
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
func EnablePortForwarding(port int) (string, error) {
return enablePortForwarding(port)
}
// enablePortForwarding enables port forwarding for a given port and returns the address.
func enablePortForwarding(port int) (string, error) {
// Try IGDv2 first, then fallback to IGDv1
ip, err := addPortMappingIGD(func() ([]UPnPClient, error) {
clients, _, err := internetgateway2.NewWANIPConnection1Clients()
if err != nil {
return nil, err
}
upnpClients := make([]UPnPClient, len(clients))
for i, client := range clients {
upnpClients[i] = client
}
return upnpClients, nil
}, port)
if err != nil {
ip, err = addPortMappingIGD(func() ([]UPnPClient, error) {
clients, _, err := internetgateway1.NewWANIPConnection1Clients()
if err != nil {
return nil, err
}
upnpClients := make([]UPnPClient, len(clients))
for i, client := range clients {
upnpClients[i] = client
}
return upnpClients, nil
}, port)
if err != nil {
return "", fmt.Errorf("failed to add port mapping: %w", err)
}
}
return fmt.Sprintf("http://%s:%d", ip, port), nil
}
func disablePortForwarding(port int) error {
// Try to remove port mapping from both IGDv2 and IGDv1
err1 := removePortMappingIGD(func() ([]UPnPClient, error) {
clients, _, err := internetgateway2.NewWANIPConnection1Clients()
if err != nil {
return nil, err
}
upnpClients := make([]UPnPClient, len(clients))
for i, client := range clients {
upnpClients[i] = client
}
return upnpClients, nil
}, port)
err2 := removePortMappingIGD(func() ([]UPnPClient, error) {
clients, _, err := internetgateway1.NewWANIPConnection1Clients()
if err != nil {
return nil, err
}
upnpClients := make([]UPnPClient, len(clients))
for i, client := range clients {
upnpClients[i] = client
}
return upnpClients, nil
}, port)
// Return error only if both failed
if err1 != nil && err2 != nil {
return fmt.Errorf("failed to remove port mapping from IGDv2: %v, IGDv1: %v", err1, err2)
}
return nil
}
// addPortMappingIGD adds a port mapping using the provided client factory and returns the external IP
func addPortMappingIGD(clientFactory func() ([]UPnPClient, error), port int) (string, error) {
clients, err := clientFactory()
if err != nil {
return "", err
}
for _, client := range clients {
// Get external IP address
externalIP, err := client.GetExternalIPAddress()
if err != nil {
continue // Try next client
}
// Add port mapping
err = client.AddPortMapping(
"", // NewRemoteHost (empty for any)
uint16(port), // NewExternalPort
"TCP", // NewProtocol
uint16(port), // NewInternalPort
"127.0.0.1", // NewInternalClient (localhost)
true, // NewEnabled
"Seanime Nakama", // NewPortMappingDescription
uint32(3600), // NewLeaseDuration (1 hour)
)
if err != nil {
continue // Try next client
}
return externalIP, nil // Success
}
return "", fmt.Errorf("no working UPnP clients found")
}
// removePortMappingIGD removes a port mapping using the provided client factory
func removePortMappingIGD(clientFactory func() ([]UPnPClient, error), port int) error {
clients, err := clientFactory()
if err != nil {
return err
}
for _, client := range clients {
err = client.DeletePortMapping(
"", // NewRemoteHost (empty for any)
uint16(port), // NewExternalPort
"TCP", // NewProtocol
)
if err != nil {
continue // Try next client
}
return nil // Success
}
return fmt.Errorf("no working UPnP clients found")
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Join code (shelved)
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
func EncryptJoinCode(ip string, port int, password string) (string, error) {
plainText := fmt.Sprintf("%s:%d", ip, port)
// Derive 256-bit key from password
key := sha256.Sum256([]byte(password))
block, err := aes.NewCipher(key[:])
if err != nil {
return "", err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
nonce := make([]byte, gcm.NonceSize())
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
return "", err
}
ciphertext := gcm.Seal(nonce, nonce, []byte(plainText), nil)
return base64.RawURLEncoding.EncodeToString(ciphertext), nil
}
func DecryptJoinCode(code, password string) (string, error) {
data, err := base64.RawURLEncoding.DecodeString(code)
if err != nil {
return "", err
}
key := sha256.Sum256([]byte(password))
block, err := aes.NewCipher(key[:])
if err != nil {
return "", err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
nonceSize := gcm.NonceSize()
if len(data) < nonceSize {
return "", fmt.Errorf("ciphertext too short")
}
nonce, ciphertext := data[:nonceSize], data[nonceSize:]
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return "", err
}
return string(plaintext), nil
}