diff options
Diffstat (limited to 'internal/client/gateway')
| -rw-r--r-- | internal/client/gateway/gateway.go | 215 | ||||
| -rw-r--r-- | internal/client/gateway/server.crt | 21 |
2 files changed, 236 insertions, 0 deletions
diff --git a/internal/client/gateway/gateway.go b/internal/client/gateway/gateway.go new file mode 100644 index 0000000..2798950 --- /dev/null +++ b/internal/client/gateway/gateway.go @@ -0,0 +1,215 @@ +package gateway + +import ( + "context" + "crypto/ed25519" + "crypto/tls" + "crypto/x509" + _ "embed" + "errors" + "io" + "log" + "net" + "os" + "sync" + "time" + + tea "github.com/charmbracelet/bubbletea" + + "github.com/kyren223/eko/internal/packet" + "github.com/kyren223/eko/pkg/assert" +) + +//go:embed server.crt +var certPEM []byte + +var ( + tlsConfig *tls.Config + + asyncResponses []chan packet.Payload + responsesMu sync.Mutex + + connection net.Conn + connMu sync.Mutex +) + +func init() { + certPool := x509.NewCertPool() + if !certPool.AppendCertsFromPEM(certPEM) { + log.Fatalln("failed to append server certificate") + } + + tlsConfig = &tls.Config{ + RootCAs: certPool, + ServerName: "localhost", + } +} + +func Connect(ctx context.Context, program *tea.Program, privKey ed25519.PrivateKey) { + conn, err := tls.Dial("tcp4", ":7223", tlsConfig) + if err != nil { + assert.NoError(err, "TODO handle error") + } + log.Println("established connection with server") + + if err := handleAuth(ctx, conn, privKey); err != nil { + assert.NoError(err, "TODO handle error") + } + log.Println("successfully authenticated with server") + + framer := packet.NewFramer(ctx) + + go func() { + connection = conn + handleConnection(ctx, conn, framer) + close(framer.Out) + conn.Close() + connection = nil + }() + + go handlePacketStream(framer, program) +} + +func handleAuth(ctx context.Context, conn net.Conn, privKey ed25519.PrivateKey) error { + const nonceSize = 32 + challengeRequest := make([]byte, 1+nonceSize) + + err := conn.SetDeadline(time.Now().Add(10 * time.Second)) + assert.NoError(err, "setting deadline should not error") + defer func() { + err := conn.SetDeadline(time.Time{}) + assert.NoError(err, "unsetting deadline should not error") + }() + bytesRead := 0 + for bytesRead < 1+nonceSize { + n, err := conn.Read(challengeRequest[bytesRead:]) + if err != nil { + return err + } + bytesRead += n + } + + assert.Assert(challengeRequest[0] == packet.VERSION, "client should always have the same version as the server") + + challengeResponse := make([]byte, 1+ed25519.PublicKeySize+ed25519.SignatureSize) + challengeResponse[0] = packet.VERSION + copy(challengeResponse[1:1+ed25519.PublicKeySize], privKey.Public().(ed25519.PublicKey)) + signedNonce := ed25519.Sign(privKey, challengeRequest[1:]) + n := copy(challengeResponse[1+ed25519.PublicKeySize:], signedNonce) + assert.Assert(n == ed25519.SignatureSize, "copy should've copied the entire signature exactly") + + _, err = conn.Write(challengeResponse) + if err != nil { + return err + } + + return nil +} + +func handleConnection(ctx context.Context, conn net.Conn, framer packet.PacketFramer) { + buffer := make([]byte, 512) + for { + err := conn.SetReadDeadline(time.Now().Add(time.Second)) + assert.NoError(err, "setting a read deadline should not error") + n, err := conn.Read(buffer) + deadlineExceeded := errors.Is(err, os.ErrDeadlineExceeded) + if err != nil && !deadlineExceeded { + if !errors.Is(err, io.EOF) { + log.Println("read error:", err) + } + break + } + + if ctx.Err() != nil { + log.Println("context error:", ctx.Err()) + break + } + + err = framer.Push(ctx, buffer[:n]) + if ctx.Err() != nil { + log.Println("context error:", ctx.Err()) + break + } + assert.NoError(err, "packets from server should always be correct") + } +} + +func handlePacketStream(framer packet.PacketFramer, program *tea.Program) { + for { + pkt, ok := <-framer.Out + if !ok { + break + } + + payload, err := pkt.DecodedPayload() + assert.NoError(err, "server should always provide a decodeable packet") + + if pkt.Type().IsPush() { + log.Println("received streamed packet:", payload) + program.Send(payload) + continue + } + + responsesMu.Lock() + assert.Assert(len(asyncResponses) != 0, "there must always be at least 1 response waiting") + responseChan := asyncResponses[0] + copy(asyncResponses, asyncResponses[1:]) + asyncResponses = asyncResponses[:len(asyncResponses)-1] + responsesMu.Unlock() + + go func() { + responseChan <- payload + }() + } +} + +func conn() net.Conn { + return connection +} + +func Send(request packet.Payload) <-chan packet.Payload { + responseChan := make(chan packet.Payload) + go func() { + pkt := packet.NewPacket(packet.NewMsgPackEncoder(request)) + + conn := conn() + if conn == nil { + log.Println("request send error:", "connection is closed") + close(responseChan) + return + } + + connMu.Lock() + errDeadline := conn.SetWriteDeadline(time.Now().Add(5 * time.Second)) + assert.NoError(errDeadline, "setting a write deadline should not error") + _, err := pkt.Into(conn) + errDeadline = conn.SetWriteDeadline(time.Time{}) + assert.NoError(errDeadline, "setting a write deadline should not error") + connMu.Unlock() + if err != nil { + log.Println("request send error:", err) + close(responseChan) + return + } + + responsesMu.Lock() + asyncResponses = append(asyncResponses, responseChan) + responsesMu.Unlock() + + time.Sleep(5 * time.Second) + responsesMu.Lock() + index := -1 + for i, ch := range asyncResponses { + if ch == responseChan { + index = i + } + } + if index != -1 { + copy(asyncResponses[index:], asyncResponses[index+1:]) + asyncResponses = asyncResponses[:len(asyncResponses)-1] + close(responseChan) + } + responsesMu.Unlock() + }() + return responseChan +} diff --git a/internal/client/gateway/server.crt b/internal/client/gateway/server.crt new file mode 100644 index 0000000..caf2384 --- /dev/null +++ b/internal/client/gateway/server.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDbTCCAlWgAwIBAgIUZyvzq7LOxKqTttTRjChoJWOO4pYwDQYJKoZIhvcNAQEL +BQAwOjEhMB8GCSqGSIb3DQEJARYSa3lyZW4yMjNAcHJvdG9uLm1lMRUwEwYDVQQD +DAxreXJlbjIyMy5kZXYwHhcNMjQxMDA5MjE1NzQzWhcNMjUxMDA5MjE1NzQzWjA6 +MSEwHwYJKoZIhvcNAQkBFhJreXJlbjIyM0Bwcm90b24ubWUxFTATBgNVBAMMDGt5 +cmVuMjIzLmRldjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK7hd+zT +kqrn/8EhLEO0uMKKHgfoyczYWTlA9uPFADOsjdzXRLuR/Y3rK0PBE4u55xcjYZSf +mzJmVHuv1rEFOt634YOoE2UwJd9V2M0p+cD716XIEDNPfVCUe77FoZoYaH1h8QF5 +Mrx2eDH5JZt690F05O39zYzbb7+RlChWlt1kBcmLEZ1GKJeXznbL6lLMh20deYX9 +7oemqYMqP9DFbFeHkubeZ20yQvKW9cOWae9M+IhE9dAa8fm5WdfiDoTdAHfbIawx +r1OB4YqfXlXler9wAHfHWeCS0KgZCTdghF1h6wtYlwyQZcUuv+dHN7SP7zVo8pOD +b7NUqjFAMGlNgf0CAwEAAaNrMGkwCQYDVR0TBAIwADALBgNVHQ8EBAMCBaAwMAYD +VR0RBCkwJ4IJbG9jYWxob3N0ggsqLmxvY2FsaG9zdIINZWtvLmxvY2FsaG9zdDAd +BgNVHQ4EFgQUf32SXO976zgO0K/wlgWdyT3EPzcwDQYJKoZIhvcNAQELBQADggEB +AHVMGCkaZv5eIOQwevfrsEJQo3dNG34om8wBVGS5iQyho0VJZpKZSiQ16yv4x2kc +UICfVEFcfO/7/hRlA5yLWE/wpeqCgTSgtQ74gvc8D6H26wCznSPj9MIRWxYhSmPM +YO+7UKqyvFoaKiW4OkqJvCRzrpwr/lbXcGpD47UqT5gRvjJ91ULCHIUt8qDUS6+8 +mEGJAe/xFkiJ6zT0bThlqMaCA4v5g9tHGXzooIZ+YSgTvlWhAM6mVwt34l2rDSOw +4YNGUJXKCoGpy8U0NteIOOs6HhaslJpKe1mSSxmMQcgBcaf6yBT08mYfQPSsaeOk +OSoncuVBnT64liAtShpsgTc= +-----END CERTIFICATE----- |
