Compare commits

..

2 Commits

Author SHA1 Message Date
df0bf9bed7 feat: optimize mime type 2026-02-15 18:49:22 +01:00
f85afce949 feat: basic file handling 2026-02-15 16:31:49 +01:00
7 changed files with 243 additions and 4 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
./htdocs
htdocs

16
config/config.go Normal file
View File

@@ -0,0 +1,16 @@
package config
type Config struct {
NetInterface string
Htdocs string
Profile bool
}
func GetConfig() *Config {
//TODO: Load config from file
return &Config{
NetInterface: "127.0.0.1:80",
Htdocs: "./htdocs",
Profile: false,
}
}

6
go.mod
View File

@@ -1,3 +1,9 @@
module tilok.dev/go-http-server
go 1.26.0
require (
github.com/felixge/fgprof v0.9.3 // indirect
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd // indirect
github.com/pkg/profile v1.7.0 // indirect
)

21
go.sum Normal file
View File

@@ -0,0 +1,21 @@
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y=
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA=
github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

114
http_handle.go Normal file
View File

@@ -0,0 +1,114 @@
package main
import (
"bufio"
"errors"
"fmt"
"io"
"io/fs"
"log/slog"
"net"
"os"
"path"
"strings"
"tilok.dev/go-http-server/config"
rh "tilok.dev/go-http-server/response_helper"
)
func HandleHTTPRequest(headers Headers, conn net.Conn) {
if strings.Contains(headers.Uri, "..") {
conn.Write([]byte("HTTP/1.1 403 Fuck you\r\n\r\n"))
conn.Close()
return
}
config := config.GetConfig()
_, err := os.Stat(config.Htdocs)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
CreateDefaultHtdocs(config.Htdocs)
} else {
slog.Error("Failed to stat directory", "error", err)
panic("Failed to stat directory")
}
}
desiredPath := path.Join(config.Htdocs, headers.Uri)
if headers.Uri == "/" {
desiredPath = path.Join(config.Htdocs, "index.html")
}
file, err := os.Open(desiredPath)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
rh.RespondWithStatusCode(404, conn)
return
}
if errors.Is(err, fs.ErrPermission) {
rh.RespondWithStatusCode(403, conn)
return
}
slog.Error("Unhandled error case", "err", err)
rh.RespondWithStatusCode(500, conn)
return
}
defer file.Close()
sendFile(file, conn)
conn.Close()
}
func sendFile(file *os.File, conn net.Conn) {
reader := bufio.NewReader(file)
defer file.Close()
fileStat, err := file.Stat()
if err != nil {
slog.Error("Failed to stat file", "error", err)
rh.RespondWithStatusCode(500, conn)
return
}
parts := strings.Split(fileStat.Name(), ".")
ext := "." + parts[len(parts)-1]
mime_type := rh.ExtensionToMimetype[ext]
length := fileStat.Size()
if mime_type == "" {
mime_type = "application/octet-stream"
}
header := fmt.Sprintf("HTTP/1.1 200 OK\r\nContent-Type: %s\r\nContent-Length: %d\r\nServer: Tilo's Go HTTP Server\r\n\r\n", mime_type, length)
conn.Write([]byte(header))
buf := make([]byte, 4096)
for {
n, err := reader.Read(buf)
if err != nil {
if err == io.EOF {
break
}
slog.Error("Failed to read file", "error", err)
rh.RespondWithStatusCode(500, conn)
return
}
conn.Write(buf[:n])
}
}
func CreateDefaultHtdocs(dirPath string) error {
err := os.MkdirAll(dirPath, 0755)
if err != nil {
return err
}
err = os.WriteFile(path.Join(dirPath, "index.html"), []byte("<DOCTYPE html><html><head><title>Hello World</title></head><body><h1>Hello World</h1></body></html>"), 0644)
if err != nil {
return err
}
return nil
}

21
main.go
View File

@@ -4,14 +4,21 @@ import (
"bufio"
"log/slog"
"net"
"time"
"github.com/pkg/profile"
"tilok.dev/go-http-server/config"
rh "tilok.dev/go-http-server/response_helper"
)
func main() {
//TODO: Make interface configurable
interf := "127.0.0.1:8080"
listener, err := net.Listen("tcp", interf)
conf := config.GetConfig()
if conf.Profile {
defer profile.Start().Stop()
}
listener, err := net.Listen("tcp", conf.NetInterface)
if err != nil {
slog.Error("Could not create listener", "err", err.Error())
}
@@ -30,6 +37,11 @@ func main() {
}
func handleConnection(conn net.Conn) {
startTime := time.Now()
defer func() {
slog.Info("Request handled", "duration", time.Since(startTime).String())
}()
defer conn.Close()
reader := bufio.NewReader(conn)
headers := readHeaders(reader)
@@ -39,5 +51,6 @@ func handleConnection(conn net.Conn) {
}
slog.Info("Received request", "method", headers.Method, "uri", headers.Uri, "proto", headers.Proto, "client", conn.RemoteAddr().String())
headers.debugPrintHeaders()
HandleHTTPRequest(*headers, conn)
}

View File

@@ -12,3 +12,70 @@ func RespondWithStatusCode(statusCode int, conn net.Conn) {
conn.Write([]byte("Content-Length: 0\r\n"))
conn.Write([]byte("\r\n"))
}
var ExtensionToMimetype = map[string]string{
".aac": "audio/aac",
".avi": "video/x-msvideo",
".avif": "image/avif",
".bin": "application/octet-stream",
".bmp": "image/bmp",
".bz": "application/x-bzip",
".bz2": "application/x-bzip2",
".css": "text/css; charset=utf-8",
".csv": "text/csv; charset=utf-8",
".doc": "application/msword",
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
".eot": "application/vnd.ms-fontobject",
".epub": "application/epub+zip",
".gif": "image/gif",
".gz": "application/gzip",
".htm": "text/html; charset=utf-8",
".html": "text/html; charset=utf-8",
".ico": "image/x-icon",
".jar": "application/java-archive",
".jpeg": "image/jpeg",
".jpg": "image/jpeg",
".js": "text/javascript; charset=utf-8",
".json": "application/json; charset=utf-8",
".map": "application/json; charset=utf-8",
".md": "text/markdown; charset=utf-8",
".mjs": "text/javascript; charset=utf-8",
".mp3": "audio/mpeg",
".mp4": "video/mp4",
".mpeg": "video/mpeg",
".mpg": "video/mpeg",
".odp": "application/vnd.oasis.opendocument.presentation",
".ods": "application/vnd.oasis.opendocument.spreadsheet",
".odt": "application/vnd.oasis.opendocument.text",
".oga": "audio/ogg",
".ogg": "audio/ogg",
".ogv": "video/ogg",
".otf": "font/otf",
".pdf": "application/pdf",
".png": "image/png",
".ppt": "application/vnd.ms-powerpoint",
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
".rar": "application/vnd.rar",
".rtf": "application/rtf",
".svg": "image/svg+xml",
".tar": "application/x-tar",
".tif": "image/tiff",
".tiff": "image/tiff",
".ts": "video/mp2t",
".ttf": "font/ttf",
".txt": "text/plain; charset=utf-8",
".wav": "audio/wav",
".weba": "audio/webm",
".webm": "video/webm",
".webp": "image/webp",
".woff": "font/woff",
".woff2": "font/woff2",
".xhtml": "application/xhtml+xml; charset=utf-8",
".xls": "application/vnd.ms-excel",
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
".xml": "application/xml; charset=utf-8",
".yaml": "application/yaml; charset=utf-8",
".yml": "application/yaml; charset=utf-8",
".zip": "application/zip",
".7z": "application/x-7z-compressed",
}