diff options
| author | Kyren223 <ulmliad223@gmail.com> | 2024-10-24 16:39:14 +0300 |
|---|---|---|
| committer | Kyren223 <ulmliad223@gmail.com> | 2024-10-24 16:39:14 +0300 |
| commit | 688668f6d34d4d962247bd5df60c3fca61185927 (patch) | |
| tree | 459a29a76bb3a8cd77cdeccf35d7c1689374d09d /internal/client/gateway/gateway.go | |
| parent | 6f83f199f8311bf7782da59bc2e07df38073e65a (diff) | |
feat: worked on client
Diffstat (limited to 'internal/client/gateway/gateway.go')
| -rw-r--r-- | internal/client/gateway/gateway.go | 215 |
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 +} |
