diff --git a/hsp/client/client.go b/hsp/client/client.go new file mode 100644 index 0000000..61b2bdf --- /dev/null +++ b/hsp/client/client.go @@ -0,0 +1,28 @@ +package client + +import ( + "net" + + "github.com/LandaMm/hsp-go/hsp" +) + +type Client struct { + Duplex *hsp.PacketDuplex +} + +func NewClient() *Client { + return &Client{ + Duplex: nil, + } +} + +func (c *Client) SendRequest(req *hsp.Request, address string) (*hsp.Response, error) { + // TODO: Parse pathname + conn, err := net.Dial("tcp", address) + if err != nil { + return nil, err + } + + c.Duplex = hsp.NewPacketDuplex(conn) +} + diff --git a/hsp/constants.go b/hsp/constants.go new file mode 100644 index 0000000..2fedba4 --- /dev/null +++ b/hsp/constants.go @@ -0,0 +1,39 @@ +package hsp + +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/hsp/packet.go b/hsp/packet.go new file mode 100644 index 0000000..0e902e0 --- /dev/null +++ b/hsp/packet.go @@ -0,0 +1,188 @@ +package hsp + +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/hsp/request.go b/hsp/request.go new file mode 100644 index 0000000..1ef3b87 --- /dev/null +++ b/hsp/request.go @@ -0,0 +1,89 @@ +package hsp + +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/hsp/response.go b/hsp/response.go new file mode 100644 index 0000000..4e539ee --- /dev/null +++ b/hsp/response.go @@ -0,0 +1,90 @@ +package hsp + +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/hsp/server/router.go b/hsp/server/router.go new file mode 100644 index 0000000..35989f6 --- /dev/null +++ b/hsp/server/router.go @@ -0,0 +1,54 @@ +package server + +import ( + "errors" + "log" + "net" + + "github.com/LandaMm/hsp-go/hsp" +) + +type RouteHandler func(req *hsp.Request) *hsp.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 := hsp.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 := hsp.NewPacketRequest(conn.RemoteAddr(), packet) + res := handler(req) + _, err := dupl.WritePacket(res.ToPacket()) + return err + } + } + return errors.New("Not Found") +} + diff --git a/hsp/server/server.go b/hsp/server/server.go new file mode 100644 index 0000000..fffe599 --- /dev/null +++ b/hsp/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 +} + +