├── MAKEFILE ├── README.md ├── go.mod ├── go.sum ├── day2_reader_writer └── todo_over_quic │ ├── go.mod │ ├── main.go │ ├── protocol.go │ ├── server_main.go │ ├── go.sum │ ├── cli.go │ ├── server.go │ ├── storage.go │ ├── client.go │ └── handlers.go ├── day3 └── leetcode_like_code_runner │ ├── two_sum.py │ ├── main.go │ ├── go.mod │ ├── protocol.go │ ├── cli.go │ ├── client.go │ ├── server.go │ ├── runner.go │ └── go.sum └── day1_tcp_udp ├── tcp_multiclient_chat_n_file ├── cmd │ └── server │ │ └── main.go ├── protocol.go ├── server │ └── server.go └── client │ └── client.go ├── udp_echo ├── client │ └── client.go ├── server │ └── server.go └── udp_test.go └── tcp_echo ├── client └── client.go ├── tcp_test.go └── server └── server.go /MAKEFILE: -------------------------------------------------------------------------------- 1 | run-drill-1: 2 | go run ./day1_tcp_udp/drill1_tcp_echo -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bloodsport 2 | 8-day hardcore networking + messaging + observability drills in Go. 3 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/pixperk/bloodsport 2 | 3 | go 1.23.6 4 | 5 | require github.com/docker/docker v28.4.0+incompatible // indirect 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/docker/docker v28.4.0+incompatible h1:KVC7bz5zJY/4AZe/78BIvCnPsLaC9T/zh72xnlrTTOk= 2 | github.com/docker/docker v28.4.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 3 | -------------------------------------------------------------------------------- /day2_reader_writer/todo_over_quic/go.mod: -------------------------------------------------------------------------------- 1 | module todo_over_quic 2 | 3 | go 1.23 4 | 5 | toolchain go1.23.6 6 | 7 | require ( 8 | github.com/google/uuid v1.6.0 9 | github.com/quic-go/quic-go v0.54.0 10 | ) 11 | 12 | require ( 13 | go.uber.org/mock v0.5.0 // indirect 14 | golang.org/x/crypto v0.26.0 // indirect 15 | golang.org/x/mod v0.18.0 // indirect 16 | golang.org/x/net v0.28.0 // indirect 17 | golang.org/x/sync v0.8.0 // indirect 18 | golang.org/x/sys v0.23.0 // indirect 19 | golang.org/x/tools v0.22.0 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /day3/leetcode_like_code_runner/two_sum.py: -------------------------------------------------------------------------------- 1 | def two_sum(nums, target): 2 | seen = {} # value -> index 3 | for i, x in enumerate(nums): 4 | need = target - x 5 | if need in seen: 6 | return (seen[need], i) 7 | seen[x] = i 8 | return None # if no solution found 9 | 10 | # Read input 11 | line1 = input().split(',') 12 | nums = [int(x) for x in line1] 13 | target = int(input()) 14 | result = two_sum(nums, target) 15 | 16 | # Print the result in the expected format 17 | if result: 18 | print(f"{result[0]},{result[1]}") 19 | else: 20 | print("No solution found") 21 | -------------------------------------------------------------------------------- /day2_reader_writer/todo_over_quic/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | ) 8 | 9 | func main() { 10 | if len(os.Args) < 2 { 11 | fmt.Println("Usage:") 12 | fmt.Println(" go run . server [options] - Start the server") 13 | fmt.Println(" go run . client [options] - Start the client") 14 | os.Exit(1) 15 | } 16 | 17 | mode := os.Args[1] 18 | os.Args = append(os.Args[:1], os.Args[2:]...) 19 | flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) 20 | 21 | switch mode { 22 | case "server": 23 | serverMain() 24 | case "client": 25 | clientMain() 26 | default: 27 | fmt.Printf("Unknown mode: %s\n", mode) 28 | fmt.Println("Use 'server' or 'client'") 29 | os.Exit(1) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /day1_tcp_udp/tcp_multiclient_chat_n_file/cmd/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | 10 | "github.com/pixperk/bloodsport/day1_tcp_udp/tcp_multiclient_chat_n_file/server" 11 | ) 12 | 13 | func main() { 14 | srv := server.NewServer(":8080") 15 | 16 | ctx, cancel := context.WithCancel(context.Background()) 17 | defer cancel() 18 | 19 | sigChan := make(chan os.Signal, 1) 20 | signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) 21 | 22 | go func() { 23 | <-sigChan 24 | fmt.Println("\nShutdown signal received...") 25 | cancel() 26 | os.Exit(0) 27 | }() 28 | 29 | fmt.Println("Starting TCP chat server on :8080...") 30 | if err := srv.Start(ctx); err != nil { 31 | fmt.Printf("Server error: %v\n", err) 32 | os.Exit(1) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /day2_reader_writer/todo_over_quic/protocol.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | const ( 4 | Ping = iota 5 | Pong 6 | Error 7 | CreateTodo 8 | ReadTodo 9 | UpdateTodo 10 | DeleteTodo 11 | ListTodos 12 | UploadFile 13 | UploadTodos 14 | ) 15 | 16 | type Message struct { 17 | Type int `json:"type"` 18 | ReqId string `json:"req_id"` 19 | Payload any `json:"payload"` 20 | Error string `json:"error,omitempty"` 21 | } 22 | 23 | type Todo struct { 24 | Id int `json:"id"` 25 | Title string `json:"title"` 26 | Done bool `json:"done"` 27 | Files []*File `json:"files,omitempty"` 28 | } 29 | 30 | type File struct { 31 | Path string `json:"path"` 32 | Data []byte `json:"data,omitempty"` 33 | } 34 | 35 | type CreateTodoRequest struct { 36 | Title string `json:"title"` 37 | } 38 | type UpdateTodoRequest struct { 39 | Id int `json:"id"` 40 | Title string `json:"title"` 41 | Done bool `json:"done"` 42 | } 43 | 44 | type ReadTodoRequest struct { 45 | Id int `json:"id"` 46 | } 47 | 48 | type DeleteTodoRequest struct { 49 | Id int `json:"id"` 50 | } 51 | -------------------------------------------------------------------------------- /day2_reader_writer/todo_over_quic/server_main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | ) 10 | 11 | func serverMain() { 12 | var ( 13 | addr = flag.String("addr", ":8443", "Server address") 14 | dataDir = flag.String("data", "./data", "Data directory") 15 | ) 16 | flag.Parse() 17 | 18 | storage := NewFileStorage(*dataDir) 19 | if err := storage.EnsureDataDir(); err != nil { 20 | log.Fatal("Failed to create data directory:", err) 21 | } 22 | 23 | if file, err := os.Open(storage.todosFile); err == nil { 24 | storage.Load(file) 25 | file.Close() 26 | log.Println("Loaded existing todos") 27 | } 28 | 29 | server := NewTodoServer(storage) 30 | 31 | go func() { 32 | c := make(chan os.Signal, 1) 33 | signal.Notify(c, os.Interrupt, syscall.SIGTERM) 34 | <-c 35 | 36 | log.Println("Saving todos before shutdown...") 37 | if file, err := os.Create(storage.todosFile); err == nil { 38 | storage.Save(file) 39 | file.Close() 40 | } 41 | os.Exit(0) 42 | }() 43 | 44 | log.Fatal(server.Start(*addr)) 45 | } 46 | -------------------------------------------------------------------------------- /day3/leetcode_like_code_runner/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "os" 8 | "os/signal" 9 | "syscall" 10 | ) 11 | 12 | func main() { 13 | if len(os.Args) < 2 { 14 | fmt.Println("Usage:") 15 | fmt.Println(" go run . server - Start the judge server") 16 | fmt.Println(" go run . client - Start the client") 17 | os.Exit(1) 18 | } 19 | 20 | mode := os.Args[1] 21 | os.Args = append(os.Args[:1], os.Args[2:]...) 22 | flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) 23 | 24 | switch mode { 25 | case "server": 26 | judgeServerMain() 27 | case "client": 28 | codeJudgeMain() 29 | default: 30 | fmt.Printf("Unknown mode: %s\n", mode) 31 | fmt.Println("Use 'server' or 'client'") 32 | os.Exit(1) 33 | } 34 | } 35 | 36 | func judgeServerMain() { 37 | var addr = flag.String("addr", ":8443", "Server address") 38 | flag.Parse() 39 | 40 | server, err := NewJudgeServer() 41 | if err != nil { 42 | log.Fatal("Failed to create server:", err) 43 | } 44 | 45 | // Handle graceful shutdown 46 | go func() { 47 | c := make(chan os.Signal, 1) 48 | signal.Notify(c, os.Interrupt, syscall.SIGTERM) 49 | <-c 50 | 51 | log.Println("Shutting down judge server...") 52 | os.Exit(0) 53 | }() 54 | 55 | log.Printf("Starting Code Judge server on %s", *addr) 56 | log.Fatal(server.Start(*addr)) 57 | } 58 | -------------------------------------------------------------------------------- /day1_tcp_udp/udp_echo/client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "net" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | type UDPClient struct { 10 | SrvAddr string 11 | conn *net.UDPConn 12 | timeout time.Duration 13 | mu sync.Mutex 14 | } 15 | 16 | func NewUDPClient(addr string, timeout time.Duration) (*UDPClient, error) { 17 | 18 | udpAddr, err := net.ResolveUDPAddr("udp", addr) 19 | if err != nil { 20 | return nil, err 21 | } 22 | 23 | conn, err := net.DialUDP("udp", nil, udpAddr) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | return &UDPClient{ 29 | SrvAddr: addr, 30 | conn: conn, 31 | timeout: timeout, 32 | }, nil 33 | } 34 | 35 | func (c *UDPClient) Connect() error { 36 | c.mu.Lock() 37 | defer c.mu.Unlock() 38 | 39 | srvAddr, err := net.ResolveUDPAddr("udp", c.SrvAddr) 40 | if err != nil { 41 | return err 42 | } 43 | 44 | conn, err := net.DialUDP("udp", nil, srvAddr) 45 | if err != nil { 46 | return err 47 | } 48 | c.conn = conn 49 | return nil 50 | } 51 | 52 | func (c *UDPClient) SendMessage(msg string) error { 53 | _, err := c.conn.Write([]byte(msg)) 54 | return err 55 | } 56 | 57 | func (c *UDPClient) ReceiveMessage() (string, error) { 58 | // Set read timeout 59 | c.conn.SetReadDeadline(time.Now().Add(c.timeout)) 60 | 61 | buffer := make([]byte, 1024) 62 | n, err := c.conn.Read(buffer) 63 | if err != nil { 64 | return "", err 65 | } 66 | 67 | return string(buffer[:n]), nil 68 | } 69 | -------------------------------------------------------------------------------- /day1_tcp_udp/tcp_echo/client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "net" 7 | "strings" 8 | "sync" 9 | "time" 10 | ) 11 | 12 | type TCPClient struct { 13 | SrvAddr string 14 | conn net.Conn 15 | reader *bufio.Reader 16 | timeout time.Duration 17 | mu sync.Mutex 18 | connected bool 19 | } 20 | 21 | func NewTCPClient(addr string) *TCPClient { 22 | return &TCPClient{ 23 | SrvAddr: addr, 24 | timeout: 30 * time.Second, 25 | } 26 | } 27 | 28 | func (c *TCPClient) Connect() error { 29 | c.mu.Lock() 30 | defer c.mu.Unlock() 31 | if c.connected { 32 | return nil 33 | } 34 | conn, err := net.DialTimeout("tcp", c.SrvAddr, c.timeout) 35 | if err != nil { 36 | return fmt.Errorf("failed to connect to server: %w", err) 37 | } 38 | c.conn = conn 39 | c.reader = bufio.NewReader(conn) 40 | c.connected = true 41 | 42 | return nil 43 | } 44 | 45 | func (c *TCPClient) SendLine(line string) error { 46 | if !c.connected { 47 | return fmt.Errorf("not connected to server") 48 | } 49 | 50 | if !strings.HasSuffix(line, "\n") { 51 | line += "\n" 52 | } 53 | 54 | _, err := c.conn.Write([]byte(line)) 55 | return err 56 | } 57 | 58 | func (c *TCPClient) Close() error { 59 | c.mu.Lock() 60 | defer c.mu.Unlock() 61 | if !c.connected { 62 | return fmt.Errorf("not connected to server") 63 | } 64 | err := c.conn.Close() 65 | if err != nil { 66 | return fmt.Errorf("failed to close connection: %w", err) 67 | } 68 | c.connected = false 69 | c.conn = nil 70 | c.reader = nil 71 | return nil 72 | } 73 | -------------------------------------------------------------------------------- /day1_tcp_udp/tcp_echo/tcp_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "testing" 8 | "time" 9 | 10 | "github.com/pixperk/bloodsport/day1_tcp_udp/tcp_echo/client" 11 | "github.com/pixperk/bloodsport/day1_tcp_udp/tcp_echo/server" 12 | ) 13 | 14 | func getFreePort() int { 15 | addr, _ := net.ResolveTCPAddr("tcp", "localhost:0") 16 | l, _ := net.ListenTCP("tcp", addr) 17 | defer l.Close() 18 | return l.Addr().(*net.TCPAddr).Port 19 | } 20 | 21 | func TestTCPMultipleClients(t *testing.T) { 22 | 23 | port := getFreePort() 24 | addr := fmt.Sprintf("localhost:%d", port) 25 | 26 | ctx, cancel := context.WithCancel(context.Background()) 27 | defer cancel() 28 | 29 | s := server.NewTCPServer(addr) 30 | go s.StartWithContext(ctx) 31 | 32 | time.Sleep(100 * time.Millisecond) 33 | 34 | numClients := 5 35 | errors := make(chan error, numClients) 36 | 37 | for i := range numClients { 38 | go func(clientID int) { 39 | 40 | c := client.NewTCPClient(addr) 41 | err := c.Connect() 42 | if err != nil { 43 | errors <- fmt.Errorf("client %d failed to connect: %v", clientID, err) 44 | return 45 | } 46 | defer c.Close() 47 | 48 | message := fmt.Sprintf("Hello from client %d", clientID) 49 | err = c.SendLine(message) 50 | if err != nil { 51 | errors <- fmt.Errorf("client %d failed to send message: %v", clientID, err) 52 | return 53 | } 54 | errors <- nil 55 | }(i) 56 | } 57 | 58 | for range numClients { 59 | err := <-errors 60 | if err != nil { 61 | t.Errorf("Client error: %v", err) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /day2_reader_writer/todo_over_quic/go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 2 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 3 | github.com/quic-go/quic-go v0.47.0/go.mod h1:3bCapYsJvXGZcipOHuu7plYtaV6tnF+z7wIFsU0WK9E= 4 | github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg= 5 | github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= 6 | go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= 7 | go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= 8 | golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= 9 | golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= 10 | golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= 11 | golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 12 | golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= 13 | golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= 14 | golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= 15 | golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 16 | golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= 17 | golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 18 | golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= 19 | golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= 20 | -------------------------------------------------------------------------------- /day1_tcp_udp/tcp_multiclient_chat_n_file/protocol.go: -------------------------------------------------------------------------------- 1 | package protocol 2 | 3 | import "io" 4 | 5 | type MessageType int 6 | 7 | const ( 8 | TypeInitAck MessageType = iota 9 | TypeChat 10 | TypeFile 11 | TypeFileData 12 | ) 13 | 14 | // Main message wrapper - this is what gets sent over the network 15 | type Message struct { 16 | Type MessageType `json:"type"` 17 | 18 | // Only one of these will be populated based on Type 19 | InitAck *InitAck `json:"init_ack,omitempty"` 20 | Chat *Chat `json:"chat,omitempty"` 21 | File *File `json:"file,omitempty"` 22 | FileData *FileData `json:"file_data,omitempty"` 23 | } 24 | 25 | type InitAck struct { //Client sends ID and Name 26 | ID string `json:"id"` 27 | Name string `json:"name"` 28 | } 29 | 30 | type Chat struct { 31 | FromID string `json:"from_id"` // Who sent this message 32 | ToID string `json:"to_id,omitempty"` // Empty = broadcast, otherwise DM 33 | Message string `json:"message"` 34 | } 35 | 36 | type File struct { 37 | FromID string `json:"from_id"` // Who sent this file 38 | ToID string `json:"to_id,omitempty"` // Empty = broadcast, otherwise DM 39 | Name string `json:"name"` 40 | Size int64 `json:"size"` 41 | BufferSize int64 `json:"buffer_size"` 42 | Reader io.Reader 43 | } 44 | 45 | type FileData struct { 46 | FromID string `json:"from_id"` // Who sent this file chunk 47 | ToID string `json:"to_id,omitempty"` // Empty = broadcast, otherwise DM 48 | FileName string `json:"file_name"` 49 | Data string `json:"data"` 50 | ChunkNum int `json:"chunk_num"` 51 | IsLast bool `json:"is_last"` //Is this the last chunk 52 | } 53 | -------------------------------------------------------------------------------- /day1_tcp_udp/udp_echo/server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "math/rand/v2" 7 | "net" 8 | "sync" 9 | ) 10 | 11 | type UDPServer struct { 12 | ListenAddr string 13 | conn *net.UDPConn 14 | 15 | mu sync.RWMutex 16 | running bool 17 | packetLoss float64 // 0.0 to 1.0 (0% to 100% loss) 18 | } 19 | 20 | func NewUDPServer(addr string) *UDPServer { 21 | return &UDPServer{ 22 | ListenAddr: addr, 23 | } 24 | } 25 | 26 | func (s *UDPServer) Start(ctx context.Context) error { 27 | addr, err := net.ResolveUDPAddr("udp", s.ListenAddr) 28 | if err != nil { 29 | return err 30 | } 31 | 32 | conn, err := net.ListenUDP("udp", addr) 33 | if err != nil { 34 | return err 35 | } 36 | 37 | s.mu.Lock() 38 | s.conn = conn 39 | s.running = true 40 | s.mu.Unlock() 41 | 42 | //single goroutine reads all packets 43 | return s.listen(ctx) 44 | } 45 | 46 | func (s *UDPServer) listen(ctx context.Context) error { 47 | buf := make([]byte, 1024) 48 | for { 49 | select { 50 | case <-ctx.Done(): 51 | return nil 52 | default: 53 | } 54 | 55 | n, clientAddr, err := s.conn.ReadFromUDP(buf) 56 | if err != nil { 57 | return err 58 | } 59 | 60 | go s.handlePacket(buf[:n], clientAddr) 61 | } 62 | } 63 | 64 | func (s *UDPServer) simulatePacketLoss() bool { 65 | s.mu.RLock() 66 | defer s.mu.RUnlock() 67 | 68 | if s.packetLoss <= 0.0 { 69 | return false 70 | } 71 | 72 | loss := rand.Float64() 73 | return loss < s.packetLoss 74 | } 75 | 76 | func (s *UDPServer) handlePacket(data []byte, clientAddr *net.UDPAddr) { 77 | if s.simulatePacketLoss() { 78 | fmt.Printf("Simulated packet loss from %v\n", clientAddr) 79 | return 80 | } 81 | 82 | response := "ECHO : " + string(data) 83 | s.conn.WriteToUDP([]byte(response), clientAddr) 84 | } 85 | -------------------------------------------------------------------------------- /day3/leetcode_like_code_runner/go.mod: -------------------------------------------------------------------------------- 1 | module code_runner 2 | 3 | go 1.23.6 4 | 5 | require ( 6 | github.com/docker/docker v28.4.0+incompatible 7 | github.com/google/uuid v1.6.0 8 | github.com/quic-go/quic-go v0.54.0 9 | ) 10 | 11 | require ( 12 | github.com/Microsoft/go-winio v0.6.2 // indirect 13 | github.com/containerd/errdefs v1.0.0 // indirect 14 | github.com/containerd/errdefs/pkg v0.3.0 // indirect 15 | github.com/containerd/log v0.1.0 // indirect 16 | github.com/distribution/reference v0.6.0 // indirect 17 | github.com/docker/go-connections v0.6.0 // indirect 18 | github.com/docker/go-units v0.5.0 // indirect 19 | github.com/felixge/httpsnoop v1.0.4 // indirect 20 | github.com/go-logr/logr v1.4.3 // indirect 21 | github.com/go-logr/stdr v1.2.2 // indirect 22 | github.com/moby/docker-image-spec v1.3.1 // indirect 23 | github.com/moby/sys/atomicwriter v0.1.0 // indirect 24 | github.com/moby/term v0.5.2 // indirect 25 | github.com/morikuni/aec v1.0.0 // indirect 26 | github.com/opencontainers/go-digest v1.0.0 // indirect 27 | github.com/opencontainers/image-spec v1.1.1 // indirect 28 | github.com/pkg/errors v0.9.1 // indirect 29 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 30 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect 31 | go.opentelemetry.io/otel v1.38.0 // indirect 32 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect 33 | go.opentelemetry.io/otel/metric v1.38.0 // indirect 34 | go.opentelemetry.io/otel/trace v1.38.0 // indirect 35 | go.uber.org/mock v0.5.0 // indirect 36 | golang.org/x/crypto v0.41.0 // indirect 37 | golang.org/x/mod v0.18.0 // indirect 38 | golang.org/x/net v0.43.0 // indirect 39 | golang.org/x/sync v0.8.0 // indirect 40 | golang.org/x/sys v0.35.0 // indirect 41 | golang.org/x/time v0.12.0 // indirect 42 | golang.org/x/tools v0.22.0 // indirect 43 | gotest.tools/v3 v3.5.2 // indirect 44 | ) 45 | -------------------------------------------------------------------------------- /day1_tcp_udp/udp_echo/udp_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "testing" 7 | "time" 8 | 9 | "github.com/pixperk/bloodsport/day1_tcp_udp/udp_echo/client" 10 | "github.com/pixperk/bloodsport/day1_tcp_udp/udp_echo/server" 11 | ) 12 | 13 | func TestUDPBasicEchoWithoutPacketLoss(t *testing.T) { 14 | 15 | srv := server.NewUDPServer("localhost:9999") 16 | 17 | ctx, cancel := context.WithCancel(context.Background()) 18 | defer cancel() 19 | 20 | go srv.Start(ctx) 21 | time.Sleep(200 * time.Millisecond) // Give more time to start 22 | 23 | cli, err := client.NewUDPClient("localhost:9999", 3*time.Second) 24 | if err != nil { 25 | t.Fatalf("Failed to create client: %v", err) 26 | } 27 | 28 | // Test single message 29 | testMsg := "Hello UDP Server" 30 | 31 | if err := cli.SendMessage(testMsg); err != nil { 32 | t.Fatalf("Failed to send message: %v", err) 33 | } 34 | 35 | response, err := cli.ReceiveMessage() 36 | if err != nil { 37 | t.Fatalf("Failed to receive message: %v", err) 38 | } 39 | 40 | t.Logf("Received response: %q", response) 41 | 42 | expected := "ECHO : " + testMsg 43 | if response != expected { 44 | t.Errorf("Expected %q, got %q", expected, response) 45 | } 46 | } 47 | 48 | func TestUDPManualConnection(t *testing.T) { 49 | 50 | srv := server.NewUDPServer("127.0.0.1:9997") 51 | 52 | ctx, cancel := context.WithCancel(context.Background()) 53 | defer cancel() 54 | 55 | go srv.Start(ctx) 56 | time.Sleep(200 * time.Millisecond) 57 | 58 | conn, err := net.Dial("udp", "127.0.0.1:9997") 59 | if err != nil { 60 | t.Fatalf("Failed to dial UDP: %v", err) 61 | } 62 | defer conn.Close() 63 | 64 | // Send message 65 | message := "manual test" 66 | _, err = conn.Write([]byte(message)) 67 | if err != nil { 68 | t.Fatalf("Failed to send: %v", err) 69 | } 70 | 71 | conn.SetReadDeadline(time.Now().Add(3 * time.Second)) 72 | buffer := make([]byte, 1024) 73 | n, err := conn.Read(buffer) 74 | if err != nil { 75 | t.Fatalf("Failed to read: %v", err) 76 | } 77 | 78 | response := string(buffer[:n]) 79 | expected := "ECHO : " + message 80 | if response != expected { 81 | t.Errorf("Expected %q, got %q", expected, response) 82 | } else { 83 | t.Logf("Manual UDP test passed: %q", response) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /day1_tcp_udp/tcp_echo/server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "fmt" 7 | "io" 8 | "net" 9 | "sync" 10 | ) 11 | 12 | type TCPServer struct { 13 | ListenAddr string 14 | mu sync.RWMutex 15 | connections map[net.Conn]bool 16 | } 17 | 18 | func NewTCPServer(addr string) *TCPServer { 19 | return &TCPServer{ 20 | ListenAddr: addr, 21 | connections: make(map[net.Conn]bool), 22 | } 23 | } 24 | 25 | func (s *TCPServer) Start() error { 26 | return s.StartWithContext(context.Background()) 27 | } 28 | 29 | func (s *TCPServer) StartWithContext(ctx context.Context) error { 30 | lis, err := net.Listen("tcp", s.ListenAddr) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | defer lis.Close() 36 | 37 | fmt.Printf("listening on %v\n", s.ListenAddr) 38 | 39 | s.acceptConns(ctx, lis) 40 | return nil 41 | } 42 | 43 | func (s *TCPServer) acceptConns(ctx context.Context, lis net.Listener) { 44 | for { 45 | select { 46 | case <-ctx.Done(): 47 | fmt.Println("Server shutting down...") 48 | s.closeAllConnections() 49 | return 50 | default: 51 | } 52 | 53 | conn, err := lis.Accept() 54 | if err != nil { 55 | fmt.Printf("accept error: %v\n", err) 56 | continue 57 | } 58 | 59 | s.addConnection(conn) 60 | go s.handleConn(conn) 61 | } 62 | } 63 | 64 | func (s *TCPServer) handleConn(conn net.Conn) { 65 | defer func() { 66 | s.removeConnection(conn) 67 | conn.Close() 68 | }() 69 | 70 | reader := bufio.NewReader(conn) 71 | for { 72 | data, err := reader.ReadString('\n') 73 | if err == io.EOF { 74 | return 75 | } else if err != nil { 76 | fmt.Println("read error:", err) 77 | return 78 | } 79 | s.handleData(conn, data) 80 | } 81 | } 82 | 83 | func (s *TCPServer) handleData(conn net.Conn, data string) { 84 | _, err := conn.Write([]byte("Echo: " + data)) 85 | if err != nil { 86 | fmt.Println("write error:", err) 87 | } 88 | } 89 | 90 | func (s *TCPServer) addConnection(conn net.Conn) { 91 | s.mu.Lock() 92 | defer s.mu.Unlock() 93 | s.connections[conn] = true 94 | } 95 | 96 | func (s *TCPServer) removeConnection(conn net.Conn) { 97 | s.mu.Lock() 98 | defer s.mu.Unlock() 99 | delete(s.connections, conn) 100 | } 101 | 102 | func (s *TCPServer) closeAllConnections() { 103 | s.mu.Lock() 104 | defer s.mu.Unlock() 105 | for conn := range s.connections { 106 | conn.Close() 107 | } 108 | s.connections = make(map[net.Conn]bool) 109 | } 110 | -------------------------------------------------------------------------------- /day3/leetcode_like_code_runner/protocol.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | "time" 7 | ) 8 | 9 | type MessageType int 10 | 11 | const ( 12 | SubmitCode MessageType = iota 13 | ExecutionResult 14 | GetProblem 15 | ListProblems 16 | Error 17 | Ping 18 | Pong 19 | ) 20 | 21 | type Message struct { 22 | Type MessageType `json:"type"` 23 | ReqId string `json:"req_id"` 24 | Payload interface{} `json:"payload"` 25 | Error string `json:"error,omitempty"` 26 | } 27 | 28 | type CodeSubmission struct { 29 | ProblemID string `json:"problem_id"` 30 | Language string `json:"language"` 31 | Code string `json:"code"` 32 | } 33 | 34 | type CodeExecutionResult struct { 35 | Status string `json:"status"` 36 | PassedTests int `json:"passed_tests"` 37 | TotalTests int `json:"total_tests"` 38 | ExecutionTime time.Duration `json:"execution_time_ms"` 39 | MemoryUsage int64 `json:"memory_usage"` 40 | CompileError string `json:"compile_error,omitempty"` 41 | TestResults []TestResult `json:"test_results"` 42 | } 43 | 44 | type TestResult struct { 45 | Input string `json:"input"` 46 | Expected string `json:"expected"` 47 | Actual string `json:"actual"` 48 | Status string `json:"status"` 49 | ExecutionTime time.Duration `json:"execution_time_ms"` 50 | } 51 | 52 | type Problem struct { 53 | ID string `json:"id"` 54 | Title string `json:"title"` 55 | Description string `json:"description"` 56 | TestCases []TestCase `json:"test_cases"` 57 | Template string `json:"template"` 58 | } 59 | 60 | type TestCase struct { 61 | Input string `json:"input"` 62 | Expected string `json:"expected"` 63 | } 64 | 65 | func getPayloadBytes(payload interface{}) ([]byte, error) { 66 | switch p := payload.(type) { 67 | case []byte: 68 | return p, nil 69 | case string: 70 | // Try base64 decode first (for JSON marshaled []byte) 71 | if decoded, err := base64.StdEncoding.DecodeString(p); err == nil { 72 | return decoded, nil 73 | } 74 | // If not base64, treat as regular string 75 | return []byte(p), nil 76 | default: 77 | return json.Marshal(p) 78 | } 79 | } 80 | 81 | func getClientPayloadBytes(payload interface{}) ([]byte, error) { 82 | switch p := payload.(type) { 83 | case []byte: 84 | return p, nil 85 | case string: 86 | // Try base64 decode first 87 | if decoded, err := base64.StdEncoding.DecodeString(p); err == nil { 88 | return decoded, nil 89 | } 90 | return []byte(p), nil 91 | default: 92 | return json.Marshal(p) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /day3/leetcode_like_code_runner/cli.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "log" 7 | "os" 8 | "strings" 9 | ) 10 | 11 | func codeJudgeMain() { 12 | client := NewCodeJudgeClient() 13 | if err := client.Connect("localhost:8443"); err != nil { 14 | log.Fatal("Failed to connect:", err) 15 | } 16 | defer client.Close() 17 | 18 | fmt.Println("Code Judge - LeetCode Style Runner") 19 | fmt.Println("Commands:") 20 | fmt.Println(" ping - Test connection") 21 | fmt.Println(" problems - List all problems") 22 | fmt.Println(" get - Get problem details") 23 | fmt.Println(" submit - Submit solution") 24 | fmt.Println(" quit - Exit") 25 | 26 | scanner := bufio.NewScanner(os.Stdin) 27 | for { 28 | fmt.Print("judge> ") 29 | if !scanner.Scan() { 30 | break 31 | } 32 | 33 | parts := strings.Fields(scanner.Text()) 34 | if len(parts) == 0 { 35 | continue 36 | } 37 | 38 | switch parts[0] { 39 | case "ping": 40 | if err := client.Ping(); err != nil { 41 | fmt.Printf("Ping failed: %v\n", err) 42 | } else { 43 | fmt.Println("Pong!") 44 | } 45 | 46 | case "problems": 47 | problems, err := client.ListProblems() 48 | if err != nil { 49 | fmt.Printf("Error: %v\n", err) 50 | continue 51 | } 52 | 53 | fmt.Printf("Available problems:\n") 54 | for _, problem := range problems { 55 | fmt.Printf(" %s: %s\n", problem.ID, problem.Title) 56 | } 57 | 58 | case "get": 59 | if len(parts) != 2 { 60 | fmt.Println("Usage: get ") 61 | continue 62 | } 63 | 64 | problem, err := client.GetProblem(parts[1]) 65 | if err != nil { 66 | fmt.Printf("Error: %v\n", err) 67 | continue 68 | } 69 | 70 | fmt.Printf("\nProblem: %s\n", problem.Title) 71 | fmt.Printf("Description: %s\n", problem.Description) 72 | fmt.Printf("Test cases: %d\n", len(problem.TestCases)) 73 | fmt.Printf("\nTemplate:\n%s\n", problem.Template) 74 | 75 | case "submit": 76 | if len(parts) != 4 { 77 | fmt.Println("Usage: submit ") 78 | fmt.Println("Supported languages: python, java, cpp") 79 | continue 80 | } 81 | 82 | code, err := client.UploadFile(parts[3]) 83 | if err != nil { 84 | fmt.Printf("Error reading file: %v\n", err) 85 | continue 86 | } 87 | 88 | submission := CodeSubmission{ 89 | ProblemID: parts[1], 90 | Language: parts[2], 91 | Code: code, 92 | } 93 | 94 | fmt.Printf("Submitting solution for %s...\n", parts[1]) 95 | 96 | result, err := client.SubmitCode(submission) 97 | if err != nil { 98 | fmt.Printf("Error: %v\n", err) 99 | continue 100 | } 101 | 102 | fmt.Printf("\n" + strings.Repeat("=", 50) + "\n") 103 | fmt.Printf("RESULT: %s\n", result.Status) 104 | fmt.Printf("Passed: %d/%d tests\n", result.PassedTests, result.TotalTests) 105 | 106 | if result.CompileError != "" { 107 | fmt.Printf("\nCompile Error:\n%s\n", result.CompileError) 108 | } 109 | 110 | fmt.Printf("\nTest Results:\n") 111 | for i, testResult := range result.TestResults { 112 | status := testResult.Status 113 | if status == "ACCEPTED" { 114 | status = "✓ " + status 115 | } else { 116 | status = "✗ " + status 117 | } 118 | 119 | fmt.Printf("Test %d: %s (%.2fms)\n", 120 | i+1, status, 121 | float64(testResult.ExecutionTime.Nanoseconds())/1e6) 122 | 123 | if testResult.Status != "ACCEPTED" { 124 | fmt.Printf(" Input: %s\n", testResult.Input) 125 | fmt.Printf(" Expected: %s\n", testResult.Expected) 126 | fmt.Printf(" Actual: %s\n", testResult.Actual) 127 | } 128 | } 129 | fmt.Println(strings.Repeat("=", 50)) 130 | 131 | case "quit": 132 | return 133 | 134 | default: 135 | fmt.Println("Unknown command. Type 'quit' to exit.") 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /day3/leetcode_like_code_runner/client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "os" 10 | 11 | "github.com/google/uuid" 12 | "github.com/quic-go/quic-go" 13 | ) 14 | 15 | type CodeJudgeClient struct { 16 | conn *quic.Conn 17 | } 18 | 19 | func NewCodeJudgeClient() *CodeJudgeClient { 20 | return &CodeJudgeClient{} 21 | } 22 | 23 | func (c *CodeJudgeClient) Connect(addr string) error { 24 | tlsConfig := &tls.Config{ 25 | InsecureSkipVerify: true, 26 | NextProtos: []string{"code-judge"}, 27 | } 28 | 29 | conn, err := quic.DialAddr(context.Background(), addr, tlsConfig, nil) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | c.conn = conn 35 | return nil 36 | } 37 | 38 | func (c *CodeJudgeClient) Close() error { 39 | if c.conn != nil { 40 | return c.conn.CloseWithError(0, "Client closing") 41 | } 42 | return nil 43 | } 44 | 45 | func (c *CodeJudgeClient) sendRequest(msg *Message) (*Message, error) { 46 | stream, err := c.conn.OpenStreamSync(context.Background()) 47 | if err != nil { 48 | return nil, err 49 | } 50 | defer stream.Close() 51 | 52 | msg.ReqId = uuid.New().String() 53 | 54 | encoder := json.NewEncoder(stream) 55 | if err := encoder.Encode(msg); err != nil { 56 | return nil, err 57 | } 58 | 59 | decoder := json.NewDecoder(stream) 60 | var response Message 61 | if err := decoder.Decode(&response); err != nil { 62 | return nil, err 63 | } 64 | 65 | return &response, nil 66 | } 67 | 68 | func (c *CodeJudgeClient) Ping() error { 69 | msg := &Message{Type: Ping} 70 | response, err := c.sendRequest(msg) 71 | if err != nil { 72 | return err 73 | } 74 | 75 | if response.Type != Pong { 76 | return fmt.Errorf("unexpected response type: %d", response.Type) 77 | } 78 | 79 | return nil 80 | } 81 | 82 | func (c *CodeJudgeClient) SubmitCode(submission CodeSubmission) (*CodeExecutionResult, error) { 83 | payload, _ := json.Marshal(submission) 84 | 85 | msg := &Message{ 86 | Type: SubmitCode, 87 | Payload: payload, 88 | } 89 | 90 | response, err := c.sendRequest(msg) 91 | if err != nil { 92 | return nil, err 93 | } 94 | 95 | if response.Type == Error { 96 | return nil, fmt.Errorf(response.Error) 97 | } 98 | 99 | var result CodeExecutionResult 100 | payloadBytes, err := getClientPayloadBytes(response.Payload) 101 | if err != nil { 102 | return nil, err 103 | } 104 | 105 | if err := json.Unmarshal(payloadBytes, &result); err != nil { 106 | return nil, err 107 | } 108 | 109 | return &result, nil 110 | } 111 | 112 | func (c *CodeJudgeClient) GetProblem(problemID string) (*Problem, error) { 113 | req := map[string]string{"problem_id": problemID} 114 | payload, _ := json.Marshal(req) 115 | 116 | msg := &Message{ 117 | Type: GetProblem, 118 | Payload: payload, 119 | } 120 | 121 | response, err := c.sendRequest(msg) 122 | if err != nil { 123 | return nil, err 124 | } 125 | 126 | if response.Type == Error { 127 | return nil, fmt.Errorf(response.Error) 128 | } 129 | 130 | var problem Problem 131 | payloadBytes, err := getClientPayloadBytes(response.Payload) 132 | if err != nil { 133 | return nil, err 134 | } 135 | 136 | if err := json.Unmarshal(payloadBytes, &problem); err != nil { 137 | return nil, err 138 | } 139 | 140 | return &problem, nil 141 | } 142 | 143 | func (c *CodeJudgeClient) ListProblems() ([]Problem, error) { 144 | msg := &Message{Type: ListProblems} 145 | 146 | response, err := c.sendRequest(msg) 147 | if err != nil { 148 | return nil, err 149 | } 150 | 151 | if response.Type == Error { 152 | return nil, fmt.Errorf(response.Error) 153 | } 154 | 155 | var problems []Problem 156 | payloadBytes, err := getClientPayloadBytes(response.Payload) 157 | if err != nil { 158 | return nil, err 159 | } 160 | 161 | if err := json.Unmarshal(payloadBytes, &problems); err != nil { 162 | return nil, err 163 | } 164 | 165 | return problems, nil 166 | } 167 | 168 | func (c *CodeJudgeClient) UploadFile(filePath string) (string, error) { 169 | file, err := os.Open(filePath) 170 | if err != nil { 171 | return "", err 172 | } 173 | defer file.Close() 174 | 175 | content, err := io.ReadAll(file) 176 | if err != nil { 177 | return "", err 178 | } 179 | 180 | return string(content), nil 181 | } 182 | -------------------------------------------------------------------------------- /day2_reader_writer/todo_over_quic/cli.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "flag" 6 | "fmt" 7 | "log" 8 | "os" 9 | "strconv" 10 | "strings" 11 | ) 12 | 13 | func clientMain() { 14 | var addr = flag.String("addr", "localhost:8443", "Server address") 15 | flag.Parse() 16 | 17 | client := NewTodoClient() 18 | if err := client.Connect(*addr); err != nil { 19 | log.Fatal("Failed to connect:", err) 20 | } 21 | defer client.Close() 22 | 23 | fmt.Println("Connected to QUIC todo server!") 24 | fmt.Println("Commands:") 25 | fmt.Println(" ping - Test connection") 26 | fmt.Println(" create - Create a new todo") 27 | fmt.Println(" read <id> - Read a todo by ID") 28 | fmt.Println(" update <id> <title> <done> - Update todo (done: true/false)") 29 | fmt.Println(" delete <id> - Delete a todo") 30 | fmt.Println(" list - List all todos") 31 | fmt.Println(" upload <todo_id> <file_path> - Upload file to todo") 32 | fmt.Println(" quit - Exit") 33 | 34 | scanner := bufio.NewScanner(os.Stdin) 35 | for { 36 | fmt.Print("> ") 37 | if !scanner.Scan() { 38 | break 39 | } 40 | 41 | line := strings.TrimSpace(scanner.Text()) 42 | if line == "" { 43 | continue 44 | } 45 | 46 | parts := strings.Fields(line) 47 | cmd := parts[0] 48 | 49 | switch cmd { 50 | case "ping": 51 | if err := client.Ping(); err != nil { 52 | fmt.Printf("Ping failed: %v\n", err) 53 | } else { 54 | fmt.Println("Pong!") 55 | } 56 | 57 | case "create": 58 | if len(parts) < 2 { 59 | fmt.Println("Usage: create <title>") 60 | continue 61 | } 62 | title := strings.Join(parts[1:], " ") 63 | _, err := client.CreateTodo(title) 64 | if err != nil { 65 | fmt.Printf("Error: %v\n", err) 66 | } else { 67 | fmt.Printf("Created todo: %s\n", title) 68 | } 69 | 70 | case "read": 71 | if len(parts) != 2 { 72 | fmt.Println("Usage: read <id>") 73 | continue 74 | } 75 | id, err := strconv.Atoi(parts[1]) 76 | if err != nil { 77 | fmt.Println("Invalid todo ID") 78 | continue 79 | } 80 | todo, err := client.ReadTodo(id) 81 | if err != nil { 82 | fmt.Printf("Error: %v\n", err) 83 | } else { 84 | status := "incomplete" 85 | if todo.Done { 86 | status = "complete" 87 | } 88 | fmt.Printf("Todo %d: %s [%s]\n", todo.Id, todo.Title, status) 89 | if len(todo.Files) > 0 { 90 | fmt.Printf("Files: %d attached\n", len(todo.Files)) 91 | } 92 | } 93 | 94 | case "update": 95 | if len(parts) < 4 { 96 | fmt.Println("Usage: update <id> <title> <done>") 97 | continue 98 | } 99 | id, err := strconv.Atoi(parts[1]) 100 | if err != nil { 101 | fmt.Println("Invalid todo ID") 102 | continue 103 | } 104 | title := parts[2] 105 | done, err := strconv.ParseBool(parts[3]) 106 | if err != nil { 107 | fmt.Println("Invalid done value (use true/false)") 108 | continue 109 | } 110 | if err := client.UpdateTodo(id, title, done); err != nil { 111 | fmt.Printf("Error: %v\n", err) 112 | } else { 113 | fmt.Printf("Updated todo %d\n", id) 114 | } 115 | 116 | case "delete": 117 | if len(parts) != 2 { 118 | fmt.Println("Usage: delete <id>") 119 | continue 120 | } 121 | id, err := strconv.Atoi(parts[1]) 122 | if err != nil { 123 | fmt.Println("Invalid todo ID") 124 | continue 125 | } 126 | if err := client.DeleteTodo(id); err != nil { 127 | fmt.Printf("Error: %v\n", err) 128 | } else { 129 | fmt.Printf("Deleted todo %d\n", id) 130 | } 131 | 132 | case "list": 133 | todos, err := client.ListTodos() 134 | if err != nil { 135 | fmt.Printf("Error: %v\n", err) 136 | } else { 137 | if len(todos) == 0 { 138 | fmt.Println("No todos found") 139 | } else { 140 | fmt.Printf("Found %d todos:\n", len(todos)) 141 | for _, todo := range todos { 142 | status := "incomplete" 143 | if todo.Done { 144 | status = "complete" 145 | } 146 | fmt.Printf(" %d: %s [%s]\n", todo.Id, todo.Title, status) 147 | } 148 | } 149 | } 150 | 151 | case "upload": 152 | if len(parts) != 3 { 153 | fmt.Println("Usage: upload <todo_id> <file_path>") 154 | continue 155 | } 156 | todoID, err := strconv.Atoi(parts[1]) 157 | if err != nil { 158 | fmt.Println("Invalid todo ID") 159 | continue 160 | } 161 | if err := client.UploadFile(todoID, parts[2]); err != nil { 162 | fmt.Printf("Upload failed: %v\n", err) 163 | } else { 164 | fmt.Println("File uploaded successfully!") 165 | } 166 | 167 | case "quit": 168 | return 169 | 170 | default: 171 | fmt.Println("Unknown command. Type 'quit' to exit.") 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /day2_reader_writer/todo_over_quic/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "crypto/rand" 6 | "crypto/rsa" 7 | "crypto/tls" 8 | "crypto/x509" 9 | "crypto/x509/pkix" 10 | "encoding/json" 11 | "encoding/pem" 12 | "log" 13 | "math/big" 14 | "net" 15 | "sync" 16 | "time" 17 | 18 | "github.com/google/uuid" 19 | "github.com/quic-go/quic-go" 20 | ) 21 | 22 | type TodoServer struct { 23 | listener *quic.Listener 24 | storage TodoStorage 25 | 26 | mu sync.RWMutex 27 | clients map[string]*quic.Conn 28 | } 29 | 30 | func NewTodoServer(storage TodoStorage) *TodoServer { 31 | return &TodoServer{ 32 | storage: storage, 33 | clients: make(map[string]*quic.Conn), 34 | } 35 | } 36 | 37 | func (s *TodoServer) Start(addr string) error { 38 | tlsConfig, err := generateTLSConfig() 39 | if err != nil { 40 | return err 41 | } 42 | 43 | quicConfig := &quic.Config{ 44 | MaxStreamReceiveWindow: 1024 * 1024, //1mb per stream 45 | MaxConnectionReceiveWindow: 4 * 1024 * 1024, //4mb per connection 46 | KeepAlivePeriod: 30 * time.Second, 47 | MaxIdleTimeout: 5 * time.Minute, 48 | } 49 | 50 | lis, err := quic.ListenAddr(addr, tlsConfig, quicConfig) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | s.listener = lis 56 | log.Printf("quic todo server listening on %s\n", addr) 57 | 58 | s.acceptConns() 59 | 60 | return nil 61 | 62 | } 63 | 64 | func (s *TodoServer) acceptConns() { 65 | for { 66 | conn, err := s.listener.Accept(context.Background()) 67 | if err != nil { 68 | log.Printf("failed to accept connection: %v\n", err) 69 | continue 70 | } 71 | 72 | log.Printf("new client connected: %s\n", conn.RemoteAddr().String()) 73 | go s.handleConn(conn) 74 | } 75 | } 76 | 77 | func (s *TodoServer) handleConn(conn *quic.Conn) { 78 | clientId := uuid.New().String() 79 | 80 | s.mu.Lock() 81 | s.clients[clientId] = conn 82 | s.mu.Unlock() 83 | 84 | for { 85 | stream, err := conn.AcceptStream(context.Background()) 86 | if err != nil { 87 | log.Printf("failed to accept stream: %v\n", err) 88 | return 89 | } 90 | 91 | go s.handleStream(clientId, stream) 92 | } 93 | 94 | } 95 | 96 | func (s *TodoServer) handleStream(clientId string, stream *quic.Stream) { 97 | defer stream.Close() 98 | 99 | decoder := json.NewDecoder(stream) 100 | encoder := json.NewEncoder(stream) 101 | 102 | var msg Message 103 | if err := decoder.Decode(&msg); err != nil { 104 | log.Printf("failed to decode message from client %s: %v\n", clientId, err) 105 | return 106 | } 107 | 108 | log.Printf("received message from client %s: %+v\n", clientId, msg) 109 | 110 | resp := s.processMessage(&msg, stream) 111 | 112 | if err := encoder.Encode(resp); err != nil { 113 | log.Printf("Failed to encode response: %v", err) 114 | } 115 | } 116 | 117 | func (s *TodoServer) processMessage(msg *Message, stream *quic.Stream) *Message { 118 | switch msg.Type { 119 | case Ping: 120 | return &Message{Type: Pong, ReqId: msg.ReqId, Payload: "PONG"} 121 | 122 | case CreateTodo: 123 | return s.handleCreateTodo(msg) 124 | 125 | case ReadTodo: 126 | return s.handleReadTodo(msg) 127 | 128 | case UpdateTodo: 129 | return s.handleUpdateTodo(msg) 130 | 131 | case DeleteTodo: 132 | return s.handleDeleteTodo(msg) 133 | 134 | case ListTodos: 135 | return s.handleListTodos(msg) 136 | 137 | case UploadFile: 138 | return s.handleFileUpload(msg, stream) 139 | 140 | case UploadTodos: 141 | return s.handleUploadTodos(msg, stream) 142 | 143 | default: 144 | return &Message{Type: Error, ReqId: msg.ReqId, Payload: "Unknown message type"} 145 | 146 | } 147 | } 148 | 149 | // generate fake ssl cert 150 | func generateTLSConfig() (*tls.Config, error) { 151 | key, err := rsa.GenerateKey(rand.Reader, 2048) 152 | if err != nil { 153 | return nil, err 154 | } 155 | 156 | template := x509.Certificate{ 157 | SerialNumber: big.NewInt(1), 158 | Subject: pkix.Name{ 159 | Organization: []string{"Todo App"}, 160 | Country: []string{"US"}, 161 | Province: []string{""}, 162 | Locality: []string{"San Francisco"}, 163 | StreetAddress: []string{""}, 164 | PostalCode: []string{""}, 165 | }, 166 | NotBefore: time.Now(), 167 | NotAfter: time.Now().Add(365 * 24 * time.Hour), 168 | KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, 169 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, 170 | IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1)}, 171 | } 172 | 173 | certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key) 174 | if err != nil { 175 | return nil, err 176 | } 177 | 178 | keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}) 179 | certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) 180 | 181 | tlsCert, err := tls.X509KeyPair(certPEM, keyPEM) 182 | if err != nil { 183 | return nil, err 184 | } 185 | 186 | return &tls.Config{ 187 | Certificates: []tls.Certificate{tlsCert}, 188 | NextProtos: []string{"todo-quic"}, 189 | }, nil 190 | } 191 | -------------------------------------------------------------------------------- /day2_reader_writer/todo_over_quic/storage.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "os" 9 | "path/filepath" 10 | "sync" 11 | ) 12 | 13 | type TodoStorage interface { 14 | Save(w io.Writer) error 15 | Load(r io.Reader) error 16 | Create(todo *Todo) error 17 | Read(id int) (*Todo, error) 18 | Update(id int, updates map[string]interface{}) error 19 | Delete(id int) error 20 | List() ([]*Todo, error) 21 | SaveFile(todoID int, fileName string, r io.Reader, size int64) error 22 | LoadFile(todoID int, fileName string, w io.Writer) error 23 | } 24 | 25 | type FileStorage struct { 26 | mu sync.RWMutex 27 | todos map[int]*Todo 28 | nextID int 29 | dataDir string 30 | todosFile string 31 | } 32 | 33 | func NewFileStorage(dataDir string) *FileStorage { 34 | return &FileStorage{ 35 | todos: make(map[int]*Todo), 36 | nextID: 1, 37 | dataDir: dataDir, 38 | todosFile: filepath.Join(dataDir, "todos.json"), 39 | } 40 | } 41 | 42 | func (fs *FileStorage) EnsureDataDir() error { 43 | return os.MkdirAll(fs.dataDir, 0755) 44 | } 45 | 46 | func (fs *FileStorage) Save(w io.Writer) error { 47 | fs.mu.RLock() 48 | defer fs.mu.RUnlock() 49 | 50 | bw := bufio.NewWriter(w) 51 | defer bw.Flush() 52 | 53 | encoder := json.NewEncoder(bw) 54 | encoder.SetIndent("", " ") 55 | 56 | data := struct { 57 | NextID int `json:"next_id"` 58 | Todos map[int]*Todo `json:"todos"` 59 | }{ 60 | NextID: fs.nextID, 61 | Todos: fs.todos, 62 | } 63 | 64 | return encoder.Encode(data) 65 | } 66 | 67 | func (fs *FileStorage) Load(r io.Reader) error { 68 | fs.mu.Lock() 69 | defer fs.mu.Unlock() 70 | 71 | br := bufio.NewReader(r) 72 | decoder := json.NewDecoder(br) 73 | 74 | var data struct { 75 | NextID int `json:"next_id"` 76 | Todos map[int]*Todo `json:"todos"` 77 | } 78 | 79 | if err := decoder.Decode(&data); err != nil { 80 | return err 81 | } 82 | 83 | fs.nextID = data.NextID 84 | fs.todos = data.Todos 85 | return nil 86 | } 87 | 88 | func (fs *FileStorage) Create(todo *Todo) error { 89 | fs.mu.Lock() 90 | defer fs.mu.Unlock() 91 | 92 | todo.Id = fs.nextID 93 | fs.nextID++ 94 | if todo.Files == nil { 95 | todo.Files = make([]*File, 0) 96 | } 97 | fs.todos[todo.Id] = todo 98 | return nil 99 | } 100 | 101 | func (fs *FileStorage) Read(id int) (*Todo, error) { 102 | fs.mu.RLock() 103 | defer fs.mu.RUnlock() 104 | 105 | todo, exists := fs.todos[id] 106 | if !exists { 107 | return nil, fmt.Errorf("todo %d not found", id) 108 | } 109 | return todo, nil 110 | } 111 | 112 | func (fs *FileStorage) Update(id int, updates map[string]interface{}) error { 113 | fs.mu.Lock() 114 | defer fs.mu.Unlock() 115 | 116 | todo, exists := fs.todos[id] 117 | if !exists { 118 | return fmt.Errorf("todo %d not found", id) 119 | } 120 | 121 | if title, ok := updates["title"].(string); ok && title != "" { 122 | todo.Title = title 123 | } 124 | if done, ok := updates["done"].(bool); ok { 125 | todo.Done = done 126 | } 127 | 128 | return nil 129 | } 130 | 131 | func (fs *FileStorage) Delete(id int) error { 132 | fs.mu.Lock() 133 | defer fs.mu.Unlock() 134 | 135 | if _, exists := fs.todos[id]; !exists { 136 | return fmt.Errorf("todo %d not found", id) 137 | } 138 | 139 | delete(fs.todos, id) 140 | return nil 141 | } 142 | 143 | func (fs *FileStorage) List() ([]*Todo, error) { 144 | fs.mu.RLock() 145 | defer fs.mu.RUnlock() 146 | 147 | todos := make([]*Todo, 0, len(fs.todos)) 148 | for _, todo := range fs.todos { 149 | todos = append(todos, todo) 150 | } 151 | return todos, nil 152 | } 153 | 154 | func (fs *FileStorage) SaveFile(todoID int, fileName string, r io.Reader, size int64) error { 155 | fs.mu.Lock() 156 | defer fs.mu.Unlock() 157 | 158 | todo, exists := fs.todos[todoID] 159 | if !exists { 160 | return fmt.Errorf("todo %d not found", todoID) 161 | } 162 | 163 | fileDir := filepath.Join(fs.dataDir, "files", fmt.Sprintf("todo_%d", todoID)) 164 | os.MkdirAll(fileDir, 0755) 165 | 166 | filePath := filepath.Join(fileDir, fileName) 167 | file, err := os.Create(filePath) 168 | if err != nil { 169 | return err 170 | } 171 | defer file.Close() 172 | 173 | bw := bufio.NewWriter(file) 174 | defer bw.Flush() 175 | 176 | lr := io.LimitReader(r, size) 177 | written, err := io.Copy(bw, lr) 178 | if err != nil { 179 | return err 180 | } 181 | 182 | fileRef := &File{ 183 | Path: fileName, 184 | Data: nil, 185 | } 186 | 187 | todo.Files = append(todo.Files, fileRef) 188 | 189 | fmt.Printf("Saved file %s (%d bytes) for todo %d\n", fileName, written, todoID) 190 | return nil 191 | } 192 | 193 | func (fs *FileStorage) LoadFile(todoID int, fileName string, w io.Writer) error { 194 | fs.mu.RLock() 195 | defer fs.mu.RUnlock() 196 | 197 | fileDir := filepath.Join(fs.dataDir, "files", fmt.Sprintf("todo_%d", todoID)) 198 | filePath := filepath.Join(fileDir, fileName) 199 | 200 | file, err := os.Open(filePath) 201 | if err != nil { 202 | return err 203 | } 204 | defer file.Close() 205 | 206 | br := bufio.NewReader(file) 207 | _, err = io.Copy(w, br) 208 | return err 209 | } 210 | -------------------------------------------------------------------------------- /day2_reader_writer/todo_over_quic/client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "encoding/base64" 7 | "encoding/json" 8 | "fmt" 9 | "io" 10 | "os" 11 | 12 | "github.com/google/uuid" 13 | "github.com/quic-go/quic-go" 14 | ) 15 | 16 | func getClientPayloadBytes(payload any) ([]byte, error) { 17 | switch p := payload.(type) { 18 | case []byte: 19 | return p, nil 20 | case string: 21 | // Try to base64 decode first (this is what JSON does with []byte fields) 22 | if decoded, err := base64.StdEncoding.DecodeString(p); err == nil { 23 | return decoded, nil 24 | } 25 | // If base64 decode fails, treat as regular string 26 | return []byte(p), nil 27 | default: 28 | return json.Marshal(p) 29 | } 30 | } 31 | 32 | type TodoClient struct { 33 | conn *quic.Conn 34 | } 35 | 36 | func NewTodoClient() *TodoClient { 37 | return &TodoClient{} 38 | } 39 | 40 | func (c *TodoClient) Connect(addr string) error { 41 | tlsConfig := &tls.Config{ 42 | InsecureSkipVerify: true, 43 | NextProtos: []string{"todo-quic"}, 44 | } 45 | 46 | conn, err := quic.DialAddr(context.Background(), addr, tlsConfig, nil) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | c.conn = conn 52 | return nil 53 | } 54 | 55 | func (c *TodoClient) Close() error { 56 | if c.conn != nil { 57 | return c.conn.CloseWithError(0, "Client closing") 58 | } 59 | return nil 60 | } 61 | 62 | func (c *TodoClient) sendRequest(msg *Message) (*Message, error) { 63 | stream, err := c.conn.OpenStreamSync(context.Background()) 64 | if err != nil { 65 | return nil, err 66 | } 67 | defer stream.Close() 68 | 69 | msg.ReqId = uuid.New().String() 70 | 71 | encoder := json.NewEncoder(stream) 72 | if err := encoder.Encode(msg); err != nil { 73 | return nil, err 74 | } 75 | 76 | decoder := json.NewDecoder(stream) 77 | var response Message 78 | if err := decoder.Decode(&response); err != nil { 79 | return nil, err 80 | } 81 | 82 | return &response, nil 83 | } 84 | 85 | func (c *TodoClient) Ping() error { 86 | msg := &Message{Type: Ping} 87 | response, err := c.sendRequest(msg) 88 | if err != nil { 89 | return err 90 | } 91 | 92 | if response.Type != Pong { 93 | return fmt.Errorf("unexpected response type: %d", response.Type) 94 | } 95 | 96 | return nil 97 | } 98 | 99 | func (c *TodoClient) CreateTodo(title string) (*Todo, error) { 100 | req := CreateTodoRequest{Title: title} 101 | payload, _ := json.Marshal(req) 102 | 103 | msg := &Message{ 104 | Type: CreateTodo, 105 | Payload: payload, 106 | } 107 | 108 | response, err := c.sendRequest(msg) 109 | if err != nil { 110 | return nil, err 111 | } 112 | 113 | if response.Type == Error { 114 | return nil, fmt.Errorf(response.Error) 115 | } 116 | 117 | return nil, nil 118 | } 119 | 120 | func (c *TodoClient) ReadTodo(id int) (*Todo, error) { 121 | req := ReadTodoRequest{Id: id} 122 | payload, _ := json.Marshal(req) 123 | 124 | msg := &Message{ 125 | Type: ReadTodo, 126 | Payload: payload, 127 | } 128 | 129 | response, err := c.sendRequest(msg) 130 | if err != nil { 131 | return nil, err 132 | } 133 | 134 | if response.Type == Error { 135 | return nil, fmt.Errorf(response.Error) 136 | } 137 | 138 | var todo Todo 139 | payloadBytes, err := getClientPayloadBytes(response.Payload) 140 | if err != nil { 141 | return nil, err 142 | } 143 | 144 | if err := json.Unmarshal(payloadBytes, &todo); err != nil { 145 | return nil, err 146 | } 147 | 148 | return &todo, nil 149 | } 150 | 151 | func (c *TodoClient) UpdateTodo(id int, title string, done bool) error { 152 | req := UpdateTodoRequest{ 153 | Id: id, 154 | Title: title, 155 | Done: done, 156 | } 157 | payload, _ := json.Marshal(req) 158 | 159 | msg := &Message{ 160 | Type: UpdateTodo, 161 | Payload: payload, 162 | } 163 | 164 | response, err := c.sendRequest(msg) 165 | if err != nil { 166 | return err 167 | } 168 | 169 | if response.Type == Error { 170 | return fmt.Errorf(response.Error) 171 | } 172 | 173 | return nil 174 | } 175 | 176 | func (c *TodoClient) DeleteTodo(id int) error { 177 | req := DeleteTodoRequest{Id: id} 178 | payload, _ := json.Marshal(req) 179 | 180 | msg := &Message{ 181 | Type: DeleteTodo, 182 | Payload: payload, 183 | } 184 | 185 | response, err := c.sendRequest(msg) 186 | if err != nil { 187 | return err 188 | } 189 | 190 | if response.Type == Error { 191 | return fmt.Errorf(response.Error) 192 | } 193 | 194 | return nil 195 | } 196 | 197 | func (c *TodoClient) ListTodos() ([]*Todo, error) { 198 | msg := &Message{Type: ListTodos} 199 | 200 | response, err := c.sendRequest(msg) 201 | if err != nil { 202 | return nil, err 203 | } 204 | 205 | if response.Type == Error { 206 | return nil, fmt.Errorf(response.Error) 207 | } 208 | 209 | var listResp ListTodosResponse 210 | payloadBytes, err := getClientPayloadBytes(response.Payload) 211 | if err != nil { 212 | return nil, err 213 | } 214 | 215 | if err := json.Unmarshal(payloadBytes, &listResp); err != nil { 216 | return nil, err 217 | } 218 | 219 | return listResp.Todos, nil 220 | } 221 | 222 | func (c *TodoClient) UploadFile(todoID int, filePath string) error { 223 | file, err := os.Open(filePath) 224 | if err != nil { 225 | return err 226 | } 227 | defer file.Close() 228 | 229 | fileInfo, err := file.Stat() 230 | if err != nil { 231 | return err 232 | } 233 | 234 | stream, err := c.conn.OpenStreamSync(context.Background()) 235 | if err != nil { 236 | return err 237 | } 238 | defer stream.Close() 239 | 240 | req := FileUploadRequest{ 241 | TodoID: todoID, 242 | FileName: fileInfo.Name(), 243 | FileSize: fileInfo.Size(), 244 | } 245 | 246 | msg := &Message{ 247 | Type: UploadFile, 248 | ReqId: uuid.New().String(), 249 | Payload: mustMarshal(req), 250 | } 251 | 252 | encoder := json.NewEncoder(stream) 253 | if err := encoder.Encode(msg); err != nil { 254 | return err 255 | } 256 | 257 | decoder := json.NewDecoder(stream) 258 | var response Message 259 | if err := decoder.Decode(&response); err != nil { 260 | return err 261 | } 262 | 263 | if response.Type == Error { 264 | return fmt.Errorf(response.Error) 265 | } 266 | 267 | _, err = io.Copy(stream, file) 268 | return err 269 | } 270 | 271 | func mustMarshal(v interface{}) []byte { 272 | data, _ := json.Marshal(v) 273 | return data 274 | } 275 | -------------------------------------------------------------------------------- /day2_reader_writer/todo_over_quic/handlers.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | "log" 7 | 8 | "github.com/quic-go/quic-go" 9 | ) 10 | 11 | func getPayloadBytes(payload any) ([]byte, error) { 12 | switch p := payload.(type) { 13 | case []byte: 14 | return p, nil 15 | case string: 16 | // Try to base64 decode first (this is what JSON does with []byte fields) 17 | if decoded, err := base64.StdEncoding.DecodeString(p); err == nil { 18 | return decoded, nil 19 | } 20 | // If base64 decode fails, treat as regular string 21 | return []byte(p), nil 22 | default: 23 | return json.Marshal(p) 24 | } 25 | } 26 | 27 | type Response struct { 28 | Ok bool `json:"ok"` 29 | Error string `json:"error,omitempty"` 30 | } 31 | 32 | type ListTodosResponse struct { 33 | Todos []*Todo `json:"todos"` 34 | } 35 | 36 | type FileUploadRequest struct { 37 | TodoID int `json:"todo_id"` 38 | FileName string `json:"file_name"` 39 | FileSize int64 `json:"file_size"` 40 | } 41 | 42 | type FileUploadResponse struct { 43 | Success bool `json:"success"` 44 | Message string `json:"message,omitempty"` 45 | } 46 | 47 | func (s *TodoServer) handleCreateTodo(msg *Message) *Message { 48 | var req CreateTodoRequest 49 | 50 | payloadBytes, err := getPayloadBytes(msg.Payload) 51 | if err != nil { 52 | return &Message{ 53 | Type: Error, 54 | ReqId: msg.ReqId, 55 | Error: "Invalid payload format", 56 | } 57 | } 58 | 59 | if err := json.Unmarshal(payloadBytes, &req); err != nil { 60 | return &Message{ 61 | Type: Error, 62 | ReqId: msg.ReqId, 63 | Error: "Invalid request payload", 64 | } 65 | } 66 | 67 | todo := &Todo{ 68 | Title: req.Title, 69 | Done: false, 70 | Files: make([]*File, 0), 71 | } 72 | 73 | if err := s.storage.Create(todo); err != nil { 74 | return &Message{ 75 | Type: Error, 76 | ReqId: msg.ReqId, 77 | Error: err.Error(), 78 | } 79 | } 80 | 81 | response := Response{ 82 | Ok: true, 83 | } 84 | payload, _ := json.Marshal(response) 85 | 86 | return &Message{ 87 | Type: CreateTodo, 88 | ReqId: msg.ReqId, 89 | Payload: payload, 90 | } 91 | } 92 | 93 | func (s *TodoServer) handleReadTodo(msg *Message) *Message { 94 | var req ReadTodoRequest 95 | 96 | payloadBytes, err := getPayloadBytes(msg.Payload) 97 | if err != nil { 98 | return &Message{ 99 | Type: Error, 100 | ReqId: msg.ReqId, 101 | Error: "Invalid payload format", 102 | } 103 | } 104 | 105 | if err := json.Unmarshal(payloadBytes, &req); err != nil { 106 | return &Message{ 107 | Type: Error, 108 | ReqId: msg.ReqId, 109 | Error: "Invalid request payload", 110 | } 111 | } 112 | 113 | todo, err := s.storage.Read(req.Id) 114 | if err != nil { 115 | return &Message{ 116 | Type: Error, 117 | ReqId: msg.ReqId, 118 | Error: err.Error(), 119 | } 120 | } 121 | 122 | payload, _ := json.Marshal(todo) 123 | 124 | return &Message{ 125 | Type: ReadTodo, 126 | ReqId: msg.ReqId, 127 | Payload: payload, 128 | } 129 | } 130 | 131 | func (s *TodoServer) handleUpdateTodo(msg *Message) *Message { 132 | var req UpdateTodoRequest 133 | 134 | payloadBytes, err := getPayloadBytes(msg.Payload) 135 | if err != nil { 136 | return &Message{ 137 | Type: Error, 138 | ReqId: msg.ReqId, 139 | Error: "Invalid payload format", 140 | } 141 | } 142 | 143 | if err := json.Unmarshal(payloadBytes, &req); err != nil { 144 | return &Message{ 145 | Type: Error, 146 | ReqId: msg.ReqId, 147 | Error: "Invalid request payload", 148 | } 149 | } 150 | 151 | updates := make(map[string]interface{}) 152 | if req.Title != "" { 153 | updates["title"] = req.Title 154 | } 155 | updates["done"] = req.Done 156 | 157 | if err := s.storage.Update(req.Id, updates); err != nil { 158 | return &Message{ 159 | Type: Error, 160 | ReqId: msg.ReqId, 161 | Error: err.Error(), 162 | } 163 | } 164 | 165 | payload := Response{ 166 | Ok: true, 167 | } 168 | payloadData, _ := json.Marshal(payload) 169 | 170 | return &Message{ 171 | Type: UpdateTodo, 172 | ReqId: msg.ReqId, 173 | Payload: payloadData, 174 | } 175 | } 176 | 177 | func (s *TodoServer) handleDeleteTodo(msg *Message) *Message { 178 | var req DeleteTodoRequest 179 | 180 | payloadBytes, err := getPayloadBytes(msg.Payload) 181 | if err != nil { 182 | return &Message{ 183 | Type: Error, 184 | ReqId: msg.ReqId, 185 | Error: "Invalid payload format", 186 | } 187 | } 188 | 189 | if err := json.Unmarshal(payloadBytes, &req); err != nil { 190 | return &Message{ 191 | Type: Error, 192 | ReqId: msg.ReqId, 193 | Error: "Invalid request payload", 194 | } 195 | } 196 | 197 | if err := s.storage.Delete(req.Id); err != nil { 198 | return &Message{ 199 | Type: Error, 200 | ReqId: msg.ReqId, 201 | Error: err.Error(), 202 | } 203 | } 204 | 205 | response := Response{ 206 | Ok: true, 207 | } 208 | payload, _ := json.Marshal(response) 209 | 210 | return &Message{ 211 | Type: DeleteTodo, 212 | ReqId: msg.ReqId, 213 | Payload: payload, 214 | } 215 | } 216 | 217 | func (s *TodoServer) handleListTodos(msg *Message) *Message { 218 | todos, err := s.storage.List() 219 | if err != nil { 220 | return &Message{ 221 | Type: Error, 222 | ReqId: msg.ReqId, 223 | Error: err.Error(), 224 | } 225 | } 226 | 227 | response := ListTodosResponse{ 228 | Todos: todos, 229 | } 230 | payload, _ := json.Marshal(response) 231 | 232 | return &Message{ 233 | Type: ListTodos, 234 | ReqId: msg.ReqId, 235 | Payload: payload, 236 | } 237 | } 238 | 239 | func (s *TodoServer) handleFileUpload(msg *Message, stream *quic.Stream) *Message { 240 | var req FileUploadRequest 241 | 242 | payloadBytes, err := getPayloadBytes(msg.Payload) 243 | if err != nil { 244 | return &Message{ 245 | Type: Error, 246 | ReqId: msg.ReqId, 247 | Error: "Invalid payload format", 248 | } 249 | } 250 | 251 | if err := json.Unmarshal(payloadBytes, &req); err != nil { 252 | return &Message{ 253 | Type: Error, 254 | ReqId: msg.ReqId, 255 | Error: "Invalid file upload request", 256 | } 257 | } 258 | 259 | go func() { 260 | err := s.storage.SaveFile(req.TodoID, req.FileName, stream, req.FileSize) 261 | if err != nil { 262 | log.Printf("File upload failed: %v", err) 263 | } 264 | }() 265 | 266 | response := FileUploadResponse{ 267 | Success: true, 268 | Message: "Upload initiated", 269 | } 270 | payload, _ := json.Marshal(response) 271 | 272 | return &Message{ 273 | Type: UploadFile, 274 | ReqId: msg.ReqId, 275 | Payload: payload, 276 | } 277 | } 278 | 279 | func (s *TodoServer) handleUploadTodos(msg *Message, stream *quic.Stream) *Message { 280 | err := s.storage.Save(stream) 281 | if err != nil { 282 | return &Message{ 283 | Type: Error, 284 | ReqId: msg.ReqId, 285 | Error: err.Error(), 286 | } 287 | } 288 | 289 | response := Response{ 290 | Ok: true, 291 | } 292 | payload, _ := json.Marshal(response) 293 | 294 | return &Message{ 295 | Type: UploadTodos, 296 | ReqId: msg.ReqId, 297 | Payload: payload, 298 | } 299 | } 300 | -------------------------------------------------------------------------------- /day1_tcp_udp/tcp_multiclient_chat_n_file/server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "net" 9 | "sync" 10 | 11 | protocol "github.com/pixperk/bloodsport/day1_tcp_udp/tcp_multiclient_chat_n_file" 12 | ) 13 | 14 | type Server struct { 15 | ListenAddr string 16 | 17 | mu sync.RWMutex 18 | clients map[*Client]bool 19 | } 20 | 21 | type Client struct { 22 | Conn net.Conn 23 | ID string //can also serve as file prefix 24 | Name string 25 | } 26 | 27 | func NewServer(listenAddr string) *Server { 28 | return &Server{ 29 | ListenAddr: listenAddr, 30 | clients: make(map[*Client]bool), 31 | } 32 | } 33 | 34 | func (s *Server) Start(ctx context.Context) error { 35 | lis, err := net.Listen("tcp", s.ListenAddr) 36 | if err != nil { 37 | return fmt.Errorf("failed to listen on %s: %w", s.ListenAddr, err) 38 | } 39 | 40 | defer lis.Close() 41 | 42 | fmt.Printf("chat and file transfer server listening on %s\n", s.ListenAddr) 43 | 44 | s.acceptConns(ctx, lis) 45 | 46 | return nil 47 | } 48 | 49 | func (s *Server) Close() { 50 | s.mu.Lock() 51 | defer s.mu.Unlock() 52 | 53 | clients := make([]*Client, 0, len(s.clients)) 54 | for c := range s.clients { 55 | clients = append(clients, c) 56 | } 57 | 58 | s.mu.Unlock() 59 | 60 | for _, c := range clients { 61 | c.Conn.Close() 62 | s.removeClient(c) // This will acquire its own lock 63 | } 64 | } 65 | 66 | func (s *Server) acceptConns(ctx context.Context, lis net.Listener) { 67 | for { 68 | select { 69 | case <-ctx.Done(): 70 | fmt.Println("Server shutting down...") 71 | s.Close() 72 | return 73 | default: 74 | } 75 | conn, err := lis.Accept() 76 | if err != nil { 77 | fmt.Printf("accept error: %v\n", err) 78 | continue 79 | } 80 | 81 | go s.handleNewConnection(conn) 82 | } 83 | } 84 | 85 | func (s *Server) handleNewConnection(conn net.Conn) { 86 | //Unique ID and name are generated on the client side 87 | client := &Client{ 88 | Conn: conn, 89 | } 90 | 91 | defer func() { 92 | conn.Close() 93 | s.removeClient(client) 94 | }() 95 | 96 | decoder := json.NewDecoder(conn) 97 | 98 | for { 99 | var msg protocol.Message 100 | 101 | if err := decoder.Decode(&msg); err != nil { 102 | if err == io.EOF { 103 | fmt.Printf("Client %s disconnected\n", client.ID) 104 | return 105 | } 106 | fmt.Println("failed to decode message:", err) 107 | continue 108 | } 109 | s.handleMessage(client, &msg) 110 | } 111 | } 112 | 113 | func (s *Server) handleMessage(client *Client, msg *protocol.Message) { 114 | switch msg.Type { 115 | case protocol.TypeInitAck: 116 | if msg.InitAck != nil { 117 | s.handleInitAck(client, msg.InitAck) 118 | } 119 | case protocol.TypeChat: 120 | if msg.Chat != nil { 121 | s.handleChat(client, msg.Chat) 122 | } 123 | case protocol.TypeFile: 124 | if msg.File != nil { 125 | s.handleFile(client, msg.File) 126 | } 127 | case protocol.TypeFileData: 128 | if msg.FileData != nil { 129 | s.handleFileData(client, msg.FileData) 130 | } 131 | default: 132 | fmt.Printf("Unknown message type %d from client %s\n", msg.Type, client.ID) 133 | } 134 | } 135 | 136 | func (s *Server) handleInitAck(client *Client, initAck *protocol.InitAck) { 137 | client.ID = initAck.ID 138 | client.Name = initAck.Name 139 | 140 | s.addClient(client) 141 | 142 | fmt.Printf("Client registered: ID=%s, Name=%s\n", client.ID, client.Name) 143 | 144 | ackMsg := &protocol.Message{ 145 | Type: protocol.TypeInitAck, 146 | InitAck: &protocol.InitAck{ 147 | ID: client.ID, 148 | Name: client.Name, 149 | }, 150 | } 151 | 152 | s.broadcastToAll(ackMsg) 153 | } 154 | 155 | func (s *Server) handleChat(client *Client, chat *protocol.Chat) { 156 | if chat.FromID != client.ID { 157 | fmt.Printf("Mismatched FromID in chat message: expected %s, got %s\n", client.ID, chat.FromID) 158 | return 159 | } 160 | 161 | chatMsg := &protocol.Message{ 162 | Type: protocol.TypeChat, 163 | Chat: chat, 164 | } 165 | 166 | if chat.ToID == "" { 167 | // Broadcast message 168 | s.broadcastToAll(chatMsg) 169 | } else { 170 | // Direct message 171 | receiver, ok := s.getClientByID(chat.ToID) 172 | if !ok { 173 | fmt.Printf("Unknown recipient ID %s\n", chat.ToID) 174 | return 175 | } 176 | s.sendTo(receiver, chatMsg) 177 | } 178 | } 179 | 180 | // this just plays with the metadata 181 | func (s *Server) handleFile(client *Client, file *protocol.File) { 182 | fmt.Printf("File transfer initiated by %s: %s (%d bytes)\n", client.ID, file.Name, file.Size) 183 | 184 | fileMsg := &protocol.Message{ 185 | Type: protocol.TypeFile, 186 | File: file, 187 | } 188 | 189 | if file.ToID == "" { 190 | s.broadcastToAll(fileMsg) 191 | } else { 192 | receiver, ok := s.getClientByID(file.ToID) 193 | if !ok { 194 | fmt.Printf("Unknown recipient for file: %s\n", file.ToID) 195 | return 196 | } 197 | s.sendTo(receiver, fileMsg) 198 | } 199 | } 200 | 201 | func (s *Server) handleFileData(client *Client, fileData *protocol.FileData) { 202 | // Forward file data chunk to recipients 203 | dataMsg := &protocol.Message{ 204 | Type: protocol.TypeFileData, 205 | FileData: fileData, 206 | } 207 | 208 | if fileData.ToID == "" { 209 | s.broadcastToAll(dataMsg) 210 | } else { 211 | receiver, ok := s.getClientByID(fileData.ToID) 212 | if !ok { 213 | fmt.Printf("Unknown recipient for file data: %s\n", fileData.ToID) 214 | return 215 | } 216 | s.sendTo(receiver, dataMsg) 217 | } 218 | 219 | if fileData.IsLast { 220 | fmt.Printf("File transfer complete: %s from %s\n", fileData.FileName, client.ID) 221 | } 222 | } 223 | 224 | func (s *Server) getClientByID(id string) (*Client, bool) { 225 | s.mu.RLock() 226 | defer s.mu.RUnlock() 227 | 228 | for client := range s.clients { 229 | if client.ID == id { 230 | return client, true 231 | } 232 | } 233 | return nil, false 234 | } 235 | 236 | func (s *Server) sendTo(client *Client, msg *protocol.Message) { 237 | s.mu.RLock() 238 | defer s.mu.RUnlock() 239 | 240 | if err := json.NewEncoder(client.Conn).Encode(msg); err != nil { 241 | fmt.Printf("failed to send message to client %s: %v\n", client.ID, err) 242 | } 243 | } 244 | 245 | func (s *Server) broadcastToAll(msg *protocol.Message) { 246 | s.mu.RLock() 247 | defer s.mu.RUnlock() 248 | 249 | for client := range s.clients { 250 | if msg.Type == protocol.TypeChat && msg.Chat != nil && client.ID == msg.Chat.FromID { 251 | continue // Skip sender 252 | } 253 | if msg.Type == protocol.TypeFile && msg.File != nil && client.ID == msg.File.FromID { 254 | continue // Skip sender for file transfers 255 | } 256 | if msg.Type == protocol.TypeFileData && msg.FileData != nil && client.ID == msg.FileData.FromID { 257 | continue // Skip sender for file data chunks 258 | } 259 | if err := json.NewEncoder(client.Conn).Encode(msg); err != nil { 260 | fmt.Printf("failed to send message to client %s: %v\n", client.ID, err) 261 | } 262 | } 263 | } 264 | 265 | func (s *Server) addClient(c *Client) { 266 | s.mu.Lock() 267 | defer s.mu.Unlock() 268 | s.clients[c] = true 269 | } 270 | 271 | func (s *Server) removeClient(c *Client) { 272 | delete(s.clients, c) 273 | } 274 | -------------------------------------------------------------------------------- /day3/leetcode_like_code_runner/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "crypto/rand" 6 | "crypto/rsa" 7 | "crypto/tls" 8 | "crypto/x509" 9 | "crypto/x509/pkix" 10 | "encoding/json" 11 | "encoding/pem" 12 | "log" 13 | "math/big" 14 | "net" 15 | "sync" 16 | "time" 17 | 18 | "github.com/google/uuid" 19 | "github.com/quic-go/quic-go" 20 | ) 21 | 22 | type JudgeServer struct { 23 | listener *quic.Listener 24 | codeRunner *CodeRunner 25 | problems map[string]Problem 26 | 27 | mu sync.RWMutex 28 | clients map[string]*quic.Conn 29 | } 30 | 31 | func NewJudgeServer() (*JudgeServer, error) { 32 | runner, err := NewCodeRunner() 33 | if err != nil { 34 | return nil, err 35 | } 36 | 37 | problems := map[string]Problem{ 38 | "two-sum": { 39 | ID: "two-sum", 40 | Title: "Two Sum", 41 | Description: "Given an array of integers nums and an integer target, return indices of the two numbers such that they add up to target.", 42 | TestCases: []TestCase{ 43 | {Input: "2,7,11,15\n9", Expected: "0,1"}, 44 | {Input: "3,2,4\n6", Expected: "1,2"}, 45 | {Input: "3,3\n6", Expected: "0,1"}, 46 | }, 47 | Template: `def two_sum(nums, target): 48 | # Your code here 49 | pass 50 | 51 | # Read input 52 | line1 = input().split(',') 53 | nums = [int(x) for x in line1] 54 | target = int(input()) 55 | 56 | # Call function and print result 57 | result = two_sum(nums, target) 58 | print(f"{result[0]},{result[1]}")`, 59 | }, 60 | "reverse-integer": { 61 | ID: "reverse-integer", 62 | Title: "Reverse Integer", 63 | Description: "Given a signed 32-bit integer x, return x with its digits reversed.", 64 | TestCases: []TestCase{ 65 | {Input: "123", Expected: "321"}, 66 | {Input: "-123", Expected: "-321"}, 67 | {Input: "120", Expected: "21"}, 68 | }, 69 | Template: `def reverse(x): 70 | # Your code here 71 | pass 72 | 73 | # Read input 74 | x = int(input()) 75 | 76 | # Call function and print result 77 | result = reverse(x) 78 | print(result)`, 79 | }, 80 | } 81 | 82 | return &JudgeServer{ 83 | codeRunner: runner, 84 | problems: problems, 85 | clients: make(map[string]*quic.Conn), 86 | }, nil 87 | } 88 | 89 | func (js *JudgeServer) Start(addr string) error { 90 | tlsConfig, err := generateTLSConfig() 91 | if err != nil { 92 | return err 93 | } 94 | 95 | quicConfig := &quic.Config{ 96 | MaxStreamReceiveWindow: 1024 * 1024, 97 | MaxConnectionReceiveWindow: 4 * 1024 * 1024, 98 | KeepAlivePeriod: 30 * time.Second, 99 | MaxIdleTimeout: 5 * time.Minute, 100 | } 101 | 102 | lis, err := quic.ListenAddr(addr, tlsConfig, quicConfig) 103 | if err != nil { 104 | return err 105 | } 106 | 107 | js.listener = lis 108 | log.Printf("Code Judge server listening on %s", addr) 109 | 110 | js.acceptConns() 111 | return nil 112 | } 113 | 114 | func (js *JudgeServer) acceptConns() { 115 | for { 116 | conn, err := js.listener.Accept(context.Background()) 117 | if err != nil { 118 | log.Printf("Failed to accept connection: %v", err) 119 | continue 120 | } 121 | 122 | log.Printf("New client connected: %s", conn.RemoteAddr().String()) 123 | go js.handleConn(conn) 124 | } 125 | } 126 | 127 | func (js *JudgeServer) handleConn(conn *quic.Conn) { 128 | clientId := uuid.New().String() 129 | 130 | js.mu.Lock() 131 | js.clients[clientId] = conn 132 | js.mu.Unlock() 133 | 134 | defer func() { 135 | js.mu.Lock() 136 | delete(js.clients, clientId) 137 | js.mu.Unlock() 138 | }() 139 | 140 | for { 141 | stream, err := conn.AcceptStream(context.Background()) 142 | if err != nil { 143 | log.Printf("Failed to accept stream: %v", err) 144 | return 145 | } 146 | 147 | go js.handleStream(clientId, stream) 148 | } 149 | } 150 | 151 | func (js *JudgeServer) handleStream(clientId string, stream *quic.Stream) { 152 | defer stream.Close() 153 | 154 | decoder := json.NewDecoder(stream) 155 | encoder := json.NewEncoder(stream) 156 | 157 | var msg Message 158 | if err := decoder.Decode(&msg); err != nil { 159 | log.Printf("Failed to decode message from client %s: %v", clientId, err) 160 | return 161 | } 162 | 163 | log.Printf("Received message from client %s: type=%d", clientId, msg.Type) 164 | 165 | resp := js.processMessage(&msg, stream) 166 | 167 | if err := encoder.Encode(resp); err != nil { 168 | log.Printf("Failed to encode response: %v", err) 169 | } 170 | } 171 | 172 | func (js *JudgeServer) processMessage(msg *Message, stream *quic.Stream) *Message { 173 | switch msg.Type { 174 | case Ping: 175 | return &Message{Type: Pong, ReqId: msg.ReqId, Payload: "PONG"} 176 | 177 | case SubmitCode: 178 | return js.handleCodeSubmission(msg) 179 | 180 | case GetProblem: 181 | return js.handleGetProblem(msg) 182 | 183 | case ListProblems: 184 | return js.handleListProblems(msg) 185 | 186 | default: 187 | return &Message{Type: Error, ReqId: msg.ReqId, Error: "Unknown message type"} 188 | } 189 | } 190 | 191 | func (js *JudgeServer) handleCodeSubmission(msg *Message) *Message { 192 | var submission CodeSubmission 193 | 194 | payloadBytes, err := getPayloadBytes(msg.Payload) 195 | if err != nil { 196 | return &Message{ 197 | Type: Error, 198 | ReqId: msg.ReqId, 199 | Error: "Invalid payload format", 200 | } 201 | } 202 | 203 | if err := json.Unmarshal(payloadBytes, &submission); err != nil { 204 | return &Message{ 205 | Type: Error, 206 | ReqId: msg.ReqId, 207 | Error: "Invalid submission format", 208 | } 209 | } 210 | 211 | problem, exists := js.problems[submission.ProblemID] 212 | if !exists { 213 | return &Message{ 214 | Type: Error, 215 | ReqId: msg.ReqId, 216 | Error: "Problem not found", 217 | } 218 | } 219 | 220 | log.Printf("Executing code for problem %s in %s", submission.ProblemID, submission.Language) 221 | 222 | result, err := js.codeRunner.ExecuteCode(submission, problem) 223 | if err != nil { 224 | return &Message{ 225 | Type: Error, 226 | ReqId: msg.ReqId, 227 | Error: err.Error(), 228 | } 229 | } 230 | 231 | payload, _ := json.Marshal(result) 232 | 233 | return &Message{ 234 | Type: ExecutionResult, 235 | ReqId: msg.ReqId, 236 | Payload: payload, 237 | } 238 | } 239 | 240 | func (js *JudgeServer) handleGetProblem(msg *Message) *Message { 241 | var req struct { 242 | ProblemID string `json:"problem_id"` 243 | } 244 | 245 | payloadBytes, err := getPayloadBytes(msg.Payload) 246 | if err != nil { 247 | return &Message{ 248 | Type: Error, 249 | ReqId: msg.ReqId, 250 | Error: "Invalid payload format", 251 | } 252 | } 253 | 254 | if err := json.Unmarshal(payloadBytes, &req); err != nil { 255 | return &Message{ 256 | Type: Error, 257 | ReqId: msg.ReqId, 258 | Error: "Invalid request format", 259 | } 260 | } 261 | 262 | problem, exists := js.problems[req.ProblemID] 263 | if !exists { 264 | return &Message{ 265 | Type: Error, 266 | ReqId: msg.ReqId, 267 | Error: "Problem not found", 268 | } 269 | } 270 | 271 | payload, _ := json.Marshal(problem) 272 | 273 | return &Message{ 274 | Type: GetProblem, 275 | ReqId: msg.ReqId, 276 | Payload: payload, 277 | } 278 | } 279 | 280 | func (js *JudgeServer) handleListProblems(msg *Message) *Message { 281 | problems := make([]Problem, 0, len(js.problems)) 282 | for _, problem := range js.problems { 283 | problems = append(problems, problem) 284 | } 285 | 286 | payload, _ := json.Marshal(problems) 287 | 288 | return &Message{ 289 | Type: ListProblems, 290 | ReqId: msg.ReqId, 291 | Payload: payload, 292 | } 293 | } 294 | 295 | func generateTLSConfig() (*tls.Config, error) { 296 | key, err := rsa.GenerateKey(rand.Reader, 2048) 297 | if err != nil { 298 | return nil, err 299 | } 300 | 301 | template := x509.Certificate{ 302 | SerialNumber: big.NewInt(1), 303 | Subject: pkix.Name{ 304 | Organization: []string{"Code Judge"}, 305 | Country: []string{"US"}, 306 | Province: []string{""}, 307 | Locality: []string{"San Francisco"}, 308 | StreetAddress: []string{""}, 309 | PostalCode: []string{""}, 310 | }, 311 | NotBefore: time.Now(), 312 | NotAfter: time.Now().Add(365 * 24 * time.Hour), 313 | KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, 314 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, 315 | IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1)}, 316 | } 317 | 318 | certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key) 319 | if err != nil { 320 | return nil, err 321 | } 322 | 323 | keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}) 324 | certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) 325 | 326 | tlsCert, err := tls.X509KeyPair(certPEM, keyPEM) 327 | if err != nil { 328 | return nil, err 329 | } 330 | 331 | return &tls.Config{ 332 | Certificates: []tls.Certificate{tlsCert}, 333 | NextProtos: []string{"code-judge"}, 334 | }, nil 335 | } 336 | -------------------------------------------------------------------------------- /day3/leetcode_like_code_runner/runner.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "archive/tar" 5 | "bytes" 6 | "context" 7 | "fmt" 8 | "io" 9 | "strings" 10 | "time" 11 | 12 | "github.com/docker/docker/api/types/container" 13 | "github.com/docker/docker/client" 14 | "github.com/docker/docker/pkg/stdcopy" 15 | ) 16 | 17 | type CodeRunner struct { 18 | client *client.Client 19 | languages map[string]LangConfig 20 | } 21 | 22 | type LangConfig struct { 23 | Image string `json:"image"` 24 | CompileCmd []string `json:"compile_cmd"` 25 | RunCmd []string `json:"run_cmd"` 26 | Extension string `json:"extension"` 27 | Timeout time.Duration `json:"timeout"` 28 | } 29 | 30 | func NewCodeRunner() (*CodeRunner, error) { 31 | cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | lang := map[string]LangConfig{ 37 | "python": { 38 | Image: "python:3.9-slim", 39 | RunCmd: []string{"python", "/app/solution.py"}, 40 | Extension: "py", 41 | Timeout: 5 * time.Second, 42 | }, 43 | "java": { 44 | Image: "openjdk:11-jre-slim", 45 | CompileCmd: []string{"javac", "/app/Solution.java"}, 46 | RunCmd: []string{"java", "-cp", "/app", "Solution"}, 47 | Extension: "java", 48 | Timeout: 10 * time.Second, 49 | }, 50 | "cpp": { 51 | Image: "gcc:9", 52 | CompileCmd: []string{"g++", "/app/solution.cpp", "-o", "/app/solution"}, 53 | RunCmd: []string{"/app/solution"}, 54 | Extension: "cpp", 55 | Timeout: 10 * time.Second, 56 | }, 57 | } 58 | 59 | return &CodeRunner{ 60 | client: cli, 61 | languages: lang, 62 | }, nil 63 | } 64 | 65 | func (r *CodeRunner) ExecuteCode(submission CodeSubmission, problem Problem) (*CodeExecutionResult, error) { 66 | langConfig, exists := r.languages[submission.Language] 67 | if !exists { 68 | return &CodeExecutionResult{ 69 | Status: "LANGUAGE_NOT_SUPPORTED", 70 | }, nil 71 | } 72 | 73 | contId, err := r.createContainer(langConfig) 74 | if err != nil { 75 | return nil, err 76 | } 77 | defer r.cleanup(contId) 78 | 79 | if err := r.copyCodeToContainer(contId, submission.Code, langConfig.Extension); err != nil { 80 | return nil, err 81 | } 82 | 83 | if langConfig.CompileCmd != nil { 84 | if err := r.compile(contId, langConfig); err != nil { 85 | return &CodeExecutionResult{ 86 | Status: "COMPILE_ERROR", 87 | CompileError: err.Error(), 88 | }, nil 89 | } 90 | } 91 | 92 | results := make([]TestResult, len(problem.TestCases)) 93 | passed := 0 94 | 95 | for i, testCase := range problem.TestCases { 96 | res := r.runTestCase(contId, langConfig, testCase) 97 | results[i] = res 98 | if res.Status == "ACCEPTED" { 99 | passed++ 100 | } 101 | } 102 | 103 | status := "WRONG_ANSWER" 104 | if passed == len(problem.TestCases) { 105 | status = "ACCEPTED" 106 | } 107 | 108 | return &CodeExecutionResult{ 109 | Status: status, 110 | PassedTests: passed, 111 | TotalTests: len(problem.TestCases), 112 | TestResults: results, 113 | }, nil 114 | } 115 | 116 | func (r *CodeRunner) createContainer(conf LangConfig) (string, error) { 117 | ctx := context.Background() 118 | 119 | contConf := &container.Config{ 120 | Image: conf.Image, 121 | Cmd: []string{"sleep", "300"}, //keep container alive 122 | WorkingDir: "/app", 123 | } 124 | 125 | hostConfig := &container.HostConfig{ 126 | Resources: container.Resources{ 127 | Memory: 128 * 1024 * 1024, // 128MB 128 | NanoCPUs: 500000000, // 0.5 CPU 129 | }, 130 | NetworkMode: "none", //disable networking 131 | } 132 | 133 | resp, err := r.client.ContainerCreate(ctx, contConf, hostConfig, nil, nil, "") 134 | if err != nil { 135 | return "", err 136 | } 137 | 138 | // Start the container 139 | if err := r.client.ContainerStart(ctx, resp.ID, container.StartOptions{}); err != nil { 140 | return "", err 141 | } 142 | 143 | return resp.ID, nil 144 | } 145 | 146 | func (r *CodeRunner) copyCodeToContainer(containerId, code, extension string) error { 147 | filename := fmt.Sprintf("solution.%s", extension) 148 | if extension == "java" { 149 | filename = "Solution.java" //java needs class name 150 | } 151 | 152 | //tar archive with code file 153 | tarContent := createTarWithFile(filename, code) 154 | return r.client.CopyToContainer(context.Background(), containerId, "/app", tarContent, container.CopyToContainerOptions{}) 155 | } 156 | 157 | func (r *CodeRunner) compile(containerID string, conf LangConfig) error { 158 | ctx, cancel := context.WithTimeout(context.Background(), conf.Timeout) 159 | defer cancel() 160 | 161 | execConf := container.ExecOptions{ 162 | Cmd: conf.CompileCmd, 163 | AttachStdout: true, 164 | AttachStderr: true, 165 | } 166 | 167 | execResp, err := r.client.ContainerExecCreate(ctx, containerID, execConf) 168 | if err != nil { 169 | return err 170 | } 171 | 172 | hijackedResp, err := r.client.ContainerExecAttach(ctx, execResp.ID, container.ExecStartOptions{}) 173 | if err != nil { 174 | return err 175 | } 176 | defer hijackedResp.Close() 177 | 178 | var stdoutBuf, stderrBuf bytes.Buffer 179 | _, err = stdcopy.StdCopy(&stdoutBuf, &stderrBuf, hijackedResp.Reader) 180 | if err != nil { 181 | return err 182 | } 183 | 184 | inspectResp, err := r.client.ContainerExecInspect(ctx, execResp.ID) 185 | if err != nil { 186 | return err 187 | } 188 | 189 | if inspectResp.ExitCode != 0 { 190 | // Use stderr if available, otherwise stdout 191 | errorOutput := stderrBuf.String() 192 | if errorOutput == "" { 193 | errorOutput = stdoutBuf.String() 194 | } 195 | return fmt.Errorf("compilation failed: %s", errorOutput) 196 | } 197 | 198 | return nil 199 | } 200 | 201 | func (cr *CodeRunner) runTestCase(containerID string, config LangConfig, testCase TestCase) TestResult { 202 | ctx, cancel := context.WithTimeout(context.Background(), config.Timeout) 203 | defer cancel() 204 | 205 | startTime := time.Now() 206 | 207 | // prepare execution with input 208 | execConfig := container.ExecOptions{ 209 | Cmd: config.RunCmd, 210 | AttachStdin: true, 211 | AttachStdout: true, 212 | AttachStderr: true, 213 | } 214 | 215 | execResp, err := cr.client.ContainerExecCreate(ctx, containerID, execConfig) 216 | if err != nil { 217 | return TestResult{ 218 | Input: testCase.Input, 219 | Expected: testCase.Expected, 220 | Status: "SYSTEM_ERROR", 221 | ExecutionTime: time.Since(startTime), 222 | } 223 | } 224 | 225 | hijackedResp, err := cr.client.ContainerExecAttach(ctx, execResp.ID, container.ExecStartOptions{}) 226 | if err != nil { 227 | return TestResult{ 228 | Input: testCase.Input, 229 | Expected: testCase.Expected, 230 | Status: "SYSTEM_ERROR", 231 | ExecutionTime: time.Since(startTime), 232 | } 233 | } 234 | defer hijackedResp.Close() 235 | 236 | // Send input 237 | inputWithNewline := testCase.Input + "\n" 238 | hijackedResp.Conn.Write([]byte(inputWithNewline)) 239 | hijackedResp.CloseWrite() 240 | 241 | // Read output 242 | var stdoutBuf, stderrBuf bytes.Buffer 243 | _, err = stdcopy.StdCopy(&stdoutBuf, &stderrBuf, hijackedResp.Reader) 244 | executionTime := time.Since(startTime) 245 | 246 | if ctx.Err() == context.DeadlineExceeded { 247 | return TestResult{ 248 | Input: testCase.Input, 249 | Expected: testCase.Expected, 250 | Status: "TIME_LIMIT_EXCEEDED", 251 | ExecutionTime: executionTime, 252 | } 253 | } 254 | 255 | if err != nil { 256 | return TestResult{ 257 | Input: testCase.Input, 258 | Expected: testCase.Expected, 259 | Status: "RUNTIME_ERROR", 260 | ExecutionTime: executionTime, 261 | } 262 | } 263 | 264 | actual := strings.TrimSpace(stdoutBuf.String()) 265 | expected := strings.TrimSpace(testCase.Expected) 266 | 267 | // Debug logging 268 | fmt.Printf("DEBUG - Stdout: %q\n", stdoutBuf.String()) 269 | fmt.Printf("DEBUG - Stderr: %q\n", stderrBuf.String()) 270 | fmt.Printf("DEBUG - Actual after trim: %q\n", actual) 271 | fmt.Printf("DEBUG - Expected after trim: %q\n", expected) 272 | fmt.Printf("DEBUG - Equal? %v\n", actual == expected) 273 | 274 | status := "WRONG_ANSWER" 275 | if actual == expected { 276 | status = "ACCEPTED" 277 | } 278 | 279 | return TestResult{ 280 | Input: testCase.Input, 281 | Expected: expected, 282 | Actual: actual, 283 | Status: status, 284 | ExecutionTime: executionTime, 285 | } 286 | } 287 | 288 | func (cr *CodeRunner) cleanup(containerID string) { 289 | ctx := context.Background() 290 | cr.client.ContainerKill(ctx, containerID, "SIGKILL") 291 | cr.client.ContainerRemove(ctx, containerID, container.RemoveOptions{Force: true}) 292 | } 293 | 294 | func createTarWithFile(filename, content string) io.Reader { 295 | var buf bytes.Buffer 296 | tw := tar.NewWriter(&buf) 297 | defer tw.Close() 298 | 299 | header := &tar.Header{ 300 | Name: filename, 301 | Mode: 0644, 302 | Size: int64(len(content)), 303 | } 304 | 305 | if err := tw.WriteHeader(header); err != nil { 306 | return strings.NewReader("") 307 | } 308 | 309 | if _, err := tw.Write([]byte(content)); err != nil { 310 | return strings.NewReader("") 311 | } 312 | 313 | return &buf 314 | } 315 | -------------------------------------------------------------------------------- /day3/leetcode_like_code_runner/go.sum: -------------------------------------------------------------------------------- 1 | github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= 2 | github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= 3 | github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= 4 | github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= 5 | github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= 6 | github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= 7 | github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= 8 | github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= 9 | github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= 10 | github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= 11 | github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= 12 | github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= 13 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 14 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 15 | github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= 16 | github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= 17 | github.com/docker/docker v28.4.0+incompatible h1:KVC7bz5zJY/4AZe/78BIvCnPsLaC9T/zh72xnlrTTOk= 18 | github.com/docker/docker v28.4.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 19 | github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= 20 | github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= 21 | github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= 22 | github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 23 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 24 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 25 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 26 | github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= 27 | github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 28 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 29 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 30 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 31 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 32 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 33 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 34 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= 35 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= 36 | github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= 37 | github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= 38 | github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= 39 | github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= 40 | github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= 41 | github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= 42 | github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= 43 | github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= 44 | github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= 45 | github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= 46 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= 47 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 48 | github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= 49 | github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= 50 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 51 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 52 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 53 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 54 | github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg= 55 | github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= 56 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 57 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 58 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 59 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 60 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 61 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 62 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= 63 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= 64 | go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= 65 | go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= 66 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= 67 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= 68 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4= 69 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4= 70 | go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= 71 | go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= 72 | go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= 73 | go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= 74 | go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= 75 | go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= 76 | go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= 77 | go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= 78 | go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= 79 | go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= 80 | go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= 81 | go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= 82 | golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= 83 | golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= 84 | golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= 85 | golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 86 | golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= 87 | golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= 88 | golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= 89 | golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 90 | golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= 91 | golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 92 | golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= 93 | golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= 94 | golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= 95 | golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= 96 | golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= 97 | golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= 98 | google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY= 99 | google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE= 100 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE= 101 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc= 102 | google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4= 103 | google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= 104 | google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= 105 | google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= 106 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 107 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 108 | gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= 109 | gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= 110 | -------------------------------------------------------------------------------- /day1_tcp_udp/tcp_multiclient_chat_n_file/client/client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "encoding/base64" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "net" 10 | "os" 11 | "path/filepath" 12 | "strings" 13 | 14 | protocol "github.com/pixperk/bloodsport/day1_tcp_udp/tcp_multiclient_chat_n_file" 15 | ) 16 | 17 | type Client struct { 18 | conn net.Conn 19 | id string 20 | name string 21 | reader *bufio.Reader 22 | activeFileTransfer map[string]*FileTransfer // key: fromID_fileName 23 | } 24 | 25 | type FileTransfer struct { 26 | File *protocol.File 27 | Data []byte 28 | Received int64 29 | } 30 | 31 | func main() { 32 | if len(os.Args) < 2 { 33 | fmt.Println("Usage: go run client.go <client_name>") 34 | os.Exit(1) 35 | } 36 | 37 | clientName := os.Args[1] 38 | client := &Client{ 39 | id: fmt.Sprintf("client_%s_%d", clientName, os.Getpid()), 40 | name: clientName, 41 | reader: bufio.NewReader(os.Stdin), 42 | activeFileTransfer: make(map[string]*FileTransfer), 43 | } 44 | 45 | if err := client.connect("localhost:8080"); err != nil { 46 | fmt.Printf("Failed to connect: %v\n", err) 47 | os.Exit(1) 48 | } 49 | 50 | defer client.conn.Close() 51 | 52 | if err := client.sendInitAck(); err != nil { 53 | fmt.Printf("Failed to send init ack: %v\n", err) 54 | return 55 | } 56 | 57 | go client.receiveMessages() 58 | 59 | client.interactiveMode() 60 | } 61 | 62 | func (c *Client) connect(addr string) error { 63 | conn, err := net.Dial("tcp", addr) 64 | if err != nil { 65 | return err 66 | } 67 | c.conn = conn 68 | fmt.Printf("Connected to server as %s (ID: %s)\n", c.name, c.id) 69 | return nil 70 | } 71 | 72 | func (c *Client) sendInitAck() error { 73 | msg := &protocol.Message{ 74 | Type: protocol.TypeInitAck, 75 | InitAck: &protocol.InitAck{ 76 | ID: c.id, 77 | Name: c.name, 78 | }, 79 | } 80 | return json.NewEncoder(c.conn).Encode(msg) 81 | } 82 | 83 | func (c *Client) receiveMessages() { 84 | decoder := json.NewDecoder(c.conn) 85 | for { 86 | var msg protocol.Message 87 | if err := decoder.Decode(&msg); err != nil { 88 | fmt.Printf("\nConnection lost: %v\n", err) 89 | return 90 | } 91 | 92 | switch msg.Type { 93 | case protocol.TypeInitAck: 94 | if msg.InitAck != nil && msg.InitAck.ID != c.id { 95 | fmt.Printf("\n[SYSTEM] %s joined the chat\n", msg.InitAck.Name) 96 | fmt.Print("> ") 97 | } 98 | case protocol.TypeChat: 99 | if msg.Chat != nil { 100 | if msg.Chat.ToID == "" { 101 | fmt.Printf("\n[BROADCAST] %s: %s\n", msg.Chat.FromID, msg.Chat.Message) 102 | } else { 103 | fmt.Printf("\n[DM] %s: %s\n", msg.Chat.FromID, msg.Chat.Message) 104 | } 105 | fmt.Print("> ") 106 | } 107 | case protocol.TypeFile: 108 | if msg.File != nil { 109 | c.startFileReceive(msg.File) 110 | } 111 | case protocol.TypeFileData: 112 | if msg.FileData != nil { 113 | c.receiveFileChunk(msg.FileData) 114 | } 115 | } 116 | } 117 | } 118 | 119 | func (c *Client) interactiveMode() { 120 | fmt.Println("\nChat Commands:") 121 | fmt.Println(" /dm <user_id> <message> - Send direct message") 122 | fmt.Println(" /file <path> - Broadcast file to all") 123 | fmt.Println(" /sendfile <user_id> <path> - Send file to specific user") 124 | fmt.Println(" /quit - Exit") 125 | fmt.Println(" <message> - Broadcast message to all") 126 | fmt.Print("> ") 127 | 128 | for { 129 | input, err := c.reader.ReadString('\n') 130 | if err != nil { 131 | fmt.Printf("Error reading input: %v\n", err) 132 | break 133 | } 134 | 135 | input = strings.TrimSpace(input) 136 | if input == "" { 137 | fmt.Print("> ") 138 | continue 139 | } 140 | 141 | if input == "/quit" { 142 | fmt.Println("Goodbye!") 143 | break 144 | } 145 | 146 | if strings.HasPrefix(input, "/dm ") { 147 | c.handleDirectMessage(input) 148 | } else if strings.HasPrefix(input, "/file ") { 149 | c.handleFileCommand(input) 150 | } else if strings.HasPrefix(input, "/sendfile ") { 151 | c.handleSendFileCommand(input) 152 | } else { 153 | c.sendBroadcastMessage(input) 154 | } 155 | fmt.Print("> ") 156 | } 157 | } 158 | 159 | func (c *Client) handleDirectMessage(input string) { 160 | parts := strings.SplitN(input[4:], " ", 2) // Remove "/dm " 161 | if len(parts) < 2 { 162 | fmt.Println("Usage: /dm <user_id> <message>") 163 | return 164 | } 165 | 166 | toID := parts[0] 167 | message := parts[1] 168 | 169 | c.sendChatMessage(toID, message) 170 | } 171 | 172 | func (c *Client) sendBroadcastMessage(message string) { 173 | c.sendChatMessage("", message) 174 | } 175 | 176 | func (c *Client) sendChatMessage(toID, message string) { 177 | msg := &protocol.Message{ 178 | Type: protocol.TypeChat, 179 | Chat: &protocol.Chat{ 180 | FromID: c.id, 181 | ToID: toID, 182 | Message: message, 183 | }, 184 | } 185 | 186 | if err := json.NewEncoder(c.conn).Encode(msg); err != nil { 187 | fmt.Printf("Failed to send message: %v\n", err) 188 | } 189 | } 190 | 191 | // File transfer methods 192 | func (c *Client) handleFileCommand(input string) { 193 | filePath := strings.TrimSpace(input[6:]) // Remove "/file " 194 | if filePath == "" { 195 | fmt.Println("Usage: /file <path>") 196 | return 197 | } 198 | c.sendFile(filePath, "") // Empty toID = broadcast to all 199 | } 200 | 201 | func (c *Client) handleSendFileCommand(input string) { 202 | parts := strings.SplitN(input[10:], " ", 2) // Remove "/sendfile " 203 | if len(parts) < 2 { 204 | fmt.Println("Usage: /sendfile <user_id> <path>") 205 | return 206 | } 207 | 208 | toID := parts[0] 209 | filePath := parts[1] 210 | c.sendFile(filePath, toID) 211 | } 212 | 213 | func (c *Client) sendFile(filePath, toID string) { 214 | // Check if file exists and get info 215 | fileInfo, err := os.Stat(filePath) 216 | if err != nil { 217 | fmt.Printf("Error accessing file %s: %v\n", filePath, err) 218 | return 219 | } 220 | 221 | fileName := filepath.Base(filePath) 222 | fileSize := fileInfo.Size() 223 | chunkSize := int64(1024) // 1KB chunks 224 | 225 | fmt.Printf("Sending file: %s (%d bytes) to %s\n", fileName, fileSize, 226 | func() string { 227 | if toID == "" { 228 | return "all users" 229 | } 230 | return toID 231 | }()) 232 | 233 | // Send file metadata first 234 | fileMsg := &protocol.Message{ 235 | Type: protocol.TypeFile, 236 | File: &protocol.File{ 237 | FromID: c.id, 238 | ToID: toID, 239 | Name: fileName, 240 | Size: fileSize, 241 | BufferSize: chunkSize, 242 | }, 243 | } 244 | 245 | if err := json.NewEncoder(c.conn).Encode(fileMsg); err != nil { 246 | fmt.Printf("Failed to send file metadata: %v\n", err) 247 | return 248 | } 249 | 250 | // Open and read file in chunks 251 | file, err := os.Open(filePath) 252 | if err != nil { 253 | fmt.Printf("Failed to open file: %v\n", err) 254 | return 255 | } 256 | defer file.Close() 257 | 258 | buffer := make([]byte, chunkSize) 259 | chunkNum := 0 260 | var totalSent int64 261 | 262 | for { 263 | n, err := file.Read(buffer) 264 | if err == io.EOF { 265 | break 266 | } 267 | if err != nil { 268 | fmt.Printf("Error reading file: %v\n", err) 269 | return 270 | } 271 | 272 | // Encode chunk as base64 and send as FileData message 273 | encodedData := base64.StdEncoding.EncodeToString(buffer[:n]) 274 | 275 | dataMsg := &protocol.Message{ 276 | Type: protocol.TypeFileData, 277 | FileData: &protocol.FileData{ 278 | FromID: c.id, 279 | ToID: toID, 280 | FileName: fileName, 281 | Data: encodedData, 282 | ChunkNum: chunkNum, 283 | IsLast: false, 284 | }, 285 | } 286 | 287 | if err := json.NewEncoder(c.conn).Encode(dataMsg); err != nil { 288 | fmt.Printf("Error sending file chunk: %v\n", err) 289 | return 290 | } 291 | 292 | totalSent += int64(n) 293 | chunkNum++ 294 | } 295 | 296 | // Send final chunk marker 297 | finalMsg := &protocol.Message{ 298 | Type: protocol.TypeFileData, 299 | FileData: &protocol.FileData{ 300 | FromID: c.id, 301 | ToID: toID, 302 | FileName: fileName, 303 | Data: "", 304 | ChunkNum: chunkNum, 305 | IsLast: true, 306 | }, 307 | } 308 | 309 | if err := json.NewEncoder(c.conn).Encode(finalMsg); err != nil { 310 | fmt.Printf("Error sending final file marker: %v\n", err) 311 | return 312 | } 313 | 314 | fmt.Printf("File sent successfully: %d bytes\n", totalSent) 315 | } 316 | 317 | func (c *Client) startFileReceive(file *protocol.File) { 318 | key := fmt.Sprintf("%s_%s", file.FromID, file.Name) 319 | 320 | if file.ToID == "" { 321 | fmt.Printf("\n[FILE BROADCAST] %s is sending: %s (%d bytes)\n", file.FromID, file.Name, file.Size) 322 | } else { 323 | fmt.Printf("\n[FILE DM] %s is sending: %s (%d bytes)\n", file.FromID, file.Name, file.Size) 324 | } 325 | 326 | c.activeFileTransfer[key] = &FileTransfer{ 327 | File: file, 328 | Data: make([]byte, 0, file.Size), 329 | Received: 0, 330 | } 331 | } 332 | 333 | func (c *Client) receiveFileChunk(fileData *protocol.FileData) { 334 | key := fmt.Sprintf("%s_%s", fileData.FromID, fileData.FileName) 335 | 336 | transfer, exists := c.activeFileTransfer[key] 337 | if !exists { 338 | fmt.Printf("Received file chunk for unknown transfer: %s\n", key) 339 | return 340 | } 341 | 342 | if !fileData.IsLast && fileData.Data != "" { 343 | chunkData, err := base64.StdEncoding.DecodeString(fileData.Data) 344 | if err != nil { 345 | fmt.Printf("Error decoding file chunk: %v\n", err) 346 | return 347 | } 348 | 349 | transfer.Data = append(transfer.Data, chunkData...) 350 | transfer.Received += int64(len(chunkData)) 351 | } 352 | 353 | if fileData.IsLast { 354 | // File transfer complete, save to disk 355 | if err := os.MkdirAll("received_files", 0755); err != nil { 356 | fmt.Printf("Failed to create received_files directory: %v\n", err) 357 | return 358 | } 359 | 360 | outputPath := filepath.Join("received_files", fmt.Sprintf("%s_%s", fileData.FromID, fileData.FileName)) 361 | 362 | if err := os.WriteFile(outputPath, transfer.Data, 0644); err != nil { 363 | fmt.Printf("Failed to save file: %v\n", err) 364 | return 365 | } 366 | 367 | fmt.Printf("File received: %s (%d bytes) -> %s\n", fileData.FileName, transfer.Received, outputPath) 368 | fmt.Print("> ") 369 | 370 | // Clean up 371 | delete(c.activeFileTransfer, key) 372 | } 373 | } 374 | --------------------------------------------------------------------------------