feat: init project + server prot is ready

This commit is contained in:
2025-04-15 18:08:52 +02:00
commit 9c55ea257d
10 changed files with 624 additions and 0 deletions

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module github.com/LandaMm/hsp-go
go 1.24.1

BIN
hsp-go Executable file

Binary file not shown.

74
main.go Normal file
View File

@ -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")
}
}

2
received.bin Normal file
View File

@ -0,0 +1,2 @@
Hello, everyone!
I'm a txt file

40
server/constants.go Normal file
View File

@ -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
}

188
server/packet.go Normal file
View File

@ -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
}

89
server/request.go Normal file
View File

@ -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
}

90
server/response.go Normal file
View File

@ -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
}

52
server/router.go Normal file
View File

@ -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")
}

86
server/server.go Normal file
View File

@ -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
}