summaryrefslogtreecommitdiff
path: root/internal/client/gateway/gateway.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/client/gateway/gateway.go')
-rw-r--r--internal/client/gateway/gateway.go215
1 files changed, 215 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
+}