commit 9c55ea257dc2c9014f5e82736b5337885309bbdd Author: LandaMm Date: Tue Apr 15 18:08:52 2025 +0200 feat: init project + server prot is ready diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..112b205 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/LandaMm/hsp-go + +go 1.24.1 diff --git a/hsp-go b/hsp-go new file mode 100755 index 0000000..2f47dec Binary files /dev/null and b/hsp-go differ diff --git a/main.go b/main.go new file mode 100644 index 0000000..52f1dc1 --- /dev/null +++ b/main.go @@ -0,0 +1,74 @@ +package main + +import ( + "fmt" + "log" + "net" + "os" + "os/signal" + "syscall" + + "github.com/LandaMm/hsp-go/server" +) + +func FileUploadRoute(req *server.Request) *server.Response { + log.Println("[MAIN] File Upload request:", req) + bytes, err := req.ExtractBytes() + if err != nil { + return server.NewStatusResponse(server.STATUS_INTERNALERR) + } + + filename := "received.bin" + err = os.WriteFile(filename, bytes, 0644) + if err != nil { + log.Fatalln("Failed to write packet payload into a file:", err) + return server.NewStatusResponse(server.STATUS_INTERNALERR) + } + + log.Println("Received new request from client:", req.Conn().RemoteAddr().String()) + + res := server.NewTextResponse("Hello, World!") + res.AddHeader("filename", filename) + + return res +} + +func main() { + srv := server.NewServer("localhost:3000") + fmt.Printf("Server created on address: %s\n", srv.Addr) + + handler := make(chan net.Conn, 1) + + router := server.NewRouter() + + router.AddRoute("/file-upload", FileUploadRoute) + + srv.SetListener(handler) + + go func() { + for { + conn := <-handler + if err := router.Handle(conn); err != nil { + log.Println("[MAIN] Error handling connection:", err.Error()) + } + } + }() + + sigs := make(chan os.Signal, 1) + + go func() { + s := <-sigs + if s == syscall.SIGINT || s == syscall.SIGTERM { + log.Println("Gracefully shutting down the server") + if err := srv.Stop(); err != nil { + log.Fatalln("Failed to close the server:", err) + } + } + }() + + signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) + + if err := srv.Start(); err != nil { + log.Fatalln("Failed to start server") + } +} diff --git a/received.bin b/received.bin new file mode 100644 index 0000000..d7a7acf --- /dev/null +++ b/received.bin @@ -0,0 +1,2 @@ +Hello, everyone! +I'm a txt file diff --git a/server/constants.go b/server/constants.go new file mode 100644 index 0000000..89c4853 --- /dev/null +++ b/server/constants.go @@ -0,0 +1,40 @@ + +package server + +const ( + H_STATUS = "status" + H_DATA_FORMAT = "data-format" + H_ROUTE = "route" +) + +const ( + DF_BYTES = "bytes" + DF_TEXT = "text" + DF_JSON = "json" +) + +const ( + E_UTF8 = "utf-8" +) + +const ( + STATUS_SUCCESS = 0 + STATUS_NOTFOUND = 69 + STATUS_INTERNALERR = 129 +) + +var DATA_FORMATS map[string]string = map[string]string{ + "bytes": DF_BYTES, + "text": DF_TEXT, + "json": DF_JSON, +} + +var ENCODINGS map[string]string = map[string]string{ + "utf-8": E_UTF8, +} + +type DataFormat struct { + Format string + Encoding string +} + diff --git a/server/packet.go b/server/packet.go new file mode 100644 index 0000000..50ecd1e --- /dev/null +++ b/server/packet.go @@ -0,0 +1,188 @@ +package server + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "io" + "net" +) + +const ( + Magic uint32 = 0xDEADBEEF +) + +const ( + PacketVersion int = 1 +) + +type RawPacket struct { + Magic uint32 + Version uint8 + Flags uint8 + HeaderSize uint16 + PayloadSize uint32 + Header []byte + Payload []byte +} + +type Packet struct { + Version int + Flags int + Headers map[string]string + Payload []byte +} + +type PacketDuplex struct { + conn net.Conn +} + +func BuildPacket(headers map[string]string, payload []byte) *Packet { + return &Packet{ + Version: PacketVersion, + Flags: 0, // TODO: + Headers: headers, + Payload: payload, + } +} + +func ParseHeaders(rawHeaders []byte, headers *map[string]string) error { + i := 0 + for i < len(rawHeaders) { + if rawHeaders[i] == '\n' { + break + } + var key string + for rawHeaders[i] != ':' { + if rawHeaders[i] != ' ' { + key += string(rawHeaders[i]) + } + i++ + } + i++ + var value string + for rawHeaders[i] != '\n' { + if rawHeaders[i] != ' ' { + value += string(rawHeaders[i]) + } + i++ + } + i++ + (*headers)[key] = value + } + return nil +} + +func SerializeHeaders(headers *map[string]string) []byte { + buf := new(bytes.Buffer) + for k, v := range(*headers) { + fmt.Fprintf(buf, "%s:%s\n", k, v) + } + fmt.Fprintf(buf, "\n") + return buf.Bytes() +} + +func NewPacketDuplex(conn net.Conn) *PacketDuplex { + return &PacketDuplex{ + conn, + } +} + +func (r *PacketDuplex) ReadPacket() (*Packet, error) { + rpkt := &RawPacket{} + + err := binary.Read(r.conn, binary.BigEndian, &rpkt.Magic) + if err != nil { + return nil, err + } + + if rpkt.Magic != Magic { + return nil, errors.New("Magic bytes are invalid") + } + + err = binary.Read(r.conn, binary.BigEndian, &rpkt.Version) + if err != nil { + return nil, err + } + + err = binary.Read(r.conn, binary.BigEndian, &rpkt.Flags) + if err != nil { + return nil, err + } + + err = binary.Read(r.conn, binary.BigEndian, &rpkt.HeaderSize) + if err != nil { + return nil, err + } + + err = binary.Read(r.conn, binary.BigEndian, &rpkt.PayloadSize) + if err != nil { + return nil, err + } + + rpkt.Header = make([]byte, rpkt.HeaderSize) + if _, err := io.ReadFull(r.conn, rpkt.Header); err != nil { + return nil, err + } + + rpkt.Payload = make([]byte, rpkt.PayloadSize) + if _, err := io.ReadFull(r.conn, rpkt.Payload); err != nil { + return nil, err + } + + pkt := &Packet{ + Version: int(rpkt.Version), + Flags: int(rpkt.Flags), + Headers: make(map[string]string), + Payload: rpkt.Payload, + } + + ParseHeaders(rpkt.Header, &pkt.Headers) + + return pkt, nil +} + +func (r *PacketDuplex) WritePacket(packet *Packet) (int, error) { + buf := new(bytes.Buffer) + + if err := binary.Write(buf, binary.BigEndian, Magic); err != nil { + return 0, errors.New(fmt.Sprintf("Failed to write magic into packet: %s", err.Error())) + } + + if err := binary.Write(buf, binary.BigEndian, uint8(packet.Version)); err != nil { + return 0, errors.New(fmt.Sprintf("Failed to write version into packet: %s", err.Error())) + } + + if err := binary.Write(buf, binary.BigEndian, uint8(packet.Flags)); err != nil { + return 0, errors.New(fmt.Sprintf("Failed to write flags into packet: %s", err.Error())) + } + + rawHeaders := SerializeHeaders(&packet.Headers) + headerSize := len(rawHeaders) + payloadSize := len(packet.Payload) + + if err := binary.Write(buf, binary.BigEndian, uint16(headerSize)); err != nil { + return 0, errors.New(fmt.Sprintf("Failed to write header size into packet: %s", err.Error())) + } + + if err := binary.Write(buf, binary.BigEndian, uint32(payloadSize)); err != nil { + return 0, errors.New(fmt.Sprintf("Failed to write payload size into packet: %s", err.Error())) + } + + if _, err := buf.Write(rawHeaders[:headerSize]); err != nil { + return 0, errors.New(fmt.Sprintf("Failed to write raw headers: %s", err.Error())) + } + + if _, err := buf.Write(packet.Payload[:payloadSize]); err != nil { + return 0, errors.New(fmt.Sprintf("Failed to write payload: %s", err.Error())) + } + + n, err := r.conn.Write(buf.Bytes()) + if err != nil { + return 0, errors.New(fmt.Sprintf("Failed to send packet over connection: %s", err.Error())) + } + + return n, nil +} + diff --git a/server/request.go b/server/request.go new file mode 100644 index 0000000..9858443 --- /dev/null +++ b/server/request.go @@ -0,0 +1,89 @@ +package server + +import ( + "errors" + "fmt" + "net" + "slices" + "strings" +) + +type Request struct { + conn net.Conn + packet *Packet +} + +func NewRequest(conn net.Conn, packet *Packet) *Request { + return &Request{ + conn, packet, + } +} + +func (req *Request) Conn() net.Conn { + return req.conn +} + +func (req *Request) GetHeader(key string) (string, bool) { + value, ok := req.packet.Headers[key] + return value, ok +} + +func (req *Request) GetDataFormat() (*DataFormat, error) { + // TODO: use predefined header names + format, ok := req.packet.Headers["data-format"] + if !ok { + return nil, errors.New("Data format header is not provided in request") + } + + parts := strings.Split(format, ":") + if len(parts) != 2 { + if format == "bytes" { + return &DataFormat{ + Format: DF_BYTES, + }, nil + } + return nil, errors.New("Invalid data format header") + } + + f, ok := DATA_FORMATS[parts[0]] + if !ok { + return nil, errors.New(fmt.Sprintf("Unknown data format: %s", parts[0])) + } + + encoding, ok := ENCODINGS[parts[1]] + if !ok { + return nil, errors.New(fmt.Sprintf("Unknown payload encoding: %s", parts[1])) + } + + return &DataFormat{ + Format: f, + Encoding: encoding, + }, nil +} + +func (req *Request) ExtractText() (string, error) { + df, err := req.GetDataFormat() + if err != nil { + return "", err + } + + if !slices.Contains([]string{DF_TEXT, DF_JSON}, df.Format) { + return "", errors.New(fmt.Sprintf("Data format '%s' cannot be extracted as text", df.Format)) + } + + return string(req.packet.Payload), nil +} + +func (req *Request) ExtractBytes() ([]byte, error) { + df, err := req.GetDataFormat() + if err != nil { + return nil, err + } + + if df.Format != "bytes" { + return nil, errors.New(fmt.Sprintf("Data format '%s' is invalid for extracting bytes", df.Format)) + } + + return req.packet.Payload, nil +} + diff --git a/server/response.go b/server/response.go new file mode 100644 index 0000000..d40ce10 --- /dev/null +++ b/server/response.go @@ -0,0 +1,90 @@ +package server + +import ( + "bytes" + "encoding/json" + "fmt" + "log" + "maps" + "strconv" +) + +type Response struct { + StatusCode int + Format DataFormat + Headers map[string]string + Payload []byte +} + +func NewStatusResponse(status int) *Response { + return &Response{ + StatusCode: status, + Headers: make(map[string]string), + Format: DataFormat{ + Format: DF_BYTES, + Encoding: "", + }, + Payload: make([]byte, 0), + } +} + +func NewTextResponse(text string) *Response { + return &Response{ + StatusCode: STATUS_SUCCESS, + Headers: make(map[string]string), + Format: DataFormat{ + Format: DF_TEXT, + Encoding: E_UTF8, + }, + Payload: []byte(text), + } +} + +func NewJsonResponse(data map[string]string) (*Response, error) { + jsonBytes, err := json.Marshal(data) + if err != nil { + return nil, err + } + + return &Response{ + StatusCode: STATUS_SUCCESS, + Headers: make(map[string]string), + Format: DataFormat{ + Format: DF_JSON, + Encoding: E_UTF8, + }, + Payload: jsonBytes, + }, nil +} + +func (res *Response) ToPacket() *Packet { + headers := make(map[string]string) + + maps.Copy(headers, res.Headers) + + headers[H_DATA_FORMAT] = fmt.Sprintf("%s:%s", res.Format.Format, res.Format.Encoding) + headers[H_STATUS] = strconv.Itoa(res.StatusCode) + + return BuildPacket(headers, res.Payload) +} + +func (res *Response) AddHeader(key, value string) { + if _, ok := res.Headers[key]; ok { + log.Printf("WARN: Rewriting already existing header: '%s'\n", key) + } + res.Headers[key] = value +} + +func (res *Response) Write(p []byte) (int, error) { + buf := new(bytes.Buffer) + + n, err := buf.Write(p) + if err != nil { + return n, err + } + + res.Payload = buf.Bytes() + + return n, err +} + diff --git a/server/router.go b/server/router.go new file mode 100644 index 0000000..1ba68fc --- /dev/null +++ b/server/router.go @@ -0,0 +1,52 @@ +package server + +import ( + "errors" + "log" + "net" +) + +type RouteHandler func(req *Request) *Response + +type Router struct { + Routes map[string]RouteHandler +} + +func NewRouter() *Router { + return &Router{ + Routes: make(map[string]RouteHandler), + } +} + +func (r *Router) AddRoute(pathname string, handler RouteHandler) { + if _, ok := r.Routes[pathname]; ok { + log.Printf("WARN: Rewriting existing route '%s'\n", pathname) + } + r.Routes[pathname] = handler +} + +func (r *Router) Handle(conn net.Conn) error { + defer conn.Close() + + log.Printf("Got new connection from %s\n", conn.RemoteAddr().String()) + + dupl := NewPacketDuplex(conn) + + // TODO: Ability to keep connection alive + packet, err := dupl.ReadPacket() + if err != nil { + return err + } + + if route, ok := packet.Headers["route"]; ok { + log.Printf("[ROUTER] New connection to '%s'", route) + if handler, ok := r.Routes[route]; ok { + req := NewRequest(conn, packet) + res := handler(req) + _, err := dupl.WritePacket(res.ToPacket()) + return err + } + } + return errors.New("Not Found") +} + diff --git a/server/server.go b/server/server.go new file mode 100644 index 0000000..fffe599 --- /dev/null +++ b/server/server.go @@ -0,0 +1,86 @@ +package server + +import ( + "log" + "net" + "sync" +) + +type Server struct { + Addr string + Running bool + ConnChan chan net.Conn + listener net.Listener + mu sync.Mutex +} + +func NewServer(addr string) *Server { + return &Server{ + Addr: addr, + Running: false, + } +} + +func (s *Server) SetListener(ln chan net.Conn) { + s.ConnChan = ln +} + +func (s *Server) Start() error { + ln, err := net.Listen("tcp", ":3000") + if err != nil { + return err + } + + s.mu.Lock() + s.listener = ln + s.Running = true + s.mu.Unlock() + + for s.IsRunning() { + log.Println("DEBUG:", "Waiting for new connection to accept") + conn, err := ln.Accept() + if err != nil { + if !s.IsRunning() { + break; + } + return err + } + + if s.ConnChan != nil { + s.ConnChan <- conn + } + } + + log.Println("DEBUG:", "Finished listening for connections") + + s.mu.Lock() + s.Running = false + s.listener = nil + s.mu.Unlock() + + return nil +} + + +func (s *Server) Stop() error { + s.mu.Lock() + defer s.mu.Unlock() + + s.Running = false + if s.listener != nil { + return s.listener.Close() + } + if s.ConnChan != nil { + close(s.ConnChan) + } + + return nil +} + +func (s *Server) IsRunning() bool { + s.mu.Lock() + defer s.mu.Unlock() + return s.Running +} + +