Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@ import (
"html/template"
"io"
"net/http"
urlpkg "net/url"
"os"
"path"
"path/filepath"
"slices"
"strings"
)

type Ctx struct {
Expand All @@ -17,6 +20,17 @@ type Ctx struct {
Meta
}

func NewContext(r *http.Request, w http.ResponseWriter) Ctx {
return Ctx{
Request: r,
Response: Response{w},
Meta: Meta{
Query: make(map[string]string),
Path: make(map[string]string),
},
}
}

// Send an HTML template t file to the client. If template not in template dir then will return error.
func (c *Ctx) Template(t string, data any) (err error) {
templatePaths := engine.Config.TemplatePaths
Expand Down Expand Up @@ -123,3 +137,58 @@ func (c *Ctx) JSON(j any) (err error) {
_, err = c.Response.Write(v)
return
}

func (c *Ctx) Redirect(url string, code int) {
if u, err := urlpkg.Parse(url); err == nil {
// If url was relative, make its path absolute by
// combining with request path.
// The client would probably do this for us,
// but doing it ourselves is more reliable.
// See RFC 7231, section 7.1.2
if u.Scheme == "" && u.Host == "" {
oldpath := c.Request.URL.Path
if oldpath == "" { // should not happen, but avoid a crash if it does
oldpath = "/"
}

// no leading http://server
if url == "" || url[0] != '/' {
// make relative path absolute
olddir, _ := path.Split(oldpath)
url = olddir + url
}

var query string
if i := strings.Index(url, "?"); i != -1 {
url, query = url[:i], url[i:]
}

// clean up but preserve trailing slash
trailing := strings.HasSuffix(url, "/")
url = path.Clean(url)
if trailing && !strings.HasSuffix(url, "/") {
url += "/"
}
url += query
}
}

h := c.Response.Header()

// RFC 7231 notes that a short HTML body is usually included in
// the response because older user agents may not understand 301/307.
// Do it only if the request didn't already have a Content-Type headec.Request.
_, hadCT := h["Content-Type"]

h.Set("Location", hexEscapeNonASCII(url))
if !hadCT && (c.Request.Method == "GET" || c.Request.Method == "HEAD") {
h.Set("Content-Type", "text/html; charset=utf-8")
}
c.Response.WriteHeader(code)

// Shouldn't send the body for POST or HEAD; that leaves GET.
if !hadCT && c.Request.Method == "GET" {
body := "<a href=\"" + htmlEscape(url) + "\">" + http.StatusText(code) + "</a>.\n"
fmt.Fprintln(c.Response, body)
}
}
33 changes: 33 additions & 0 deletions examples/redirect/redirect_http.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package main

import (
"net/http"
"net/url"

Goster "github.com/dpouris/goster"
)

const HOST = "https://test.com"

func main() {
g := Goster.NewServer()

g.Get("*path", func(ctx *Goster.Ctx) error {
pathname, _ := ctx.Path.Get("path")
status := http.StatusMovedPermanently
if len(pathname) > 1 {
status = http.StatusTemporaryRedirect
}
location, _ := url.JoinPath(HOST, pathname)
ctx.Redirect(location, status)
return nil
})

g.UseGlobal(func(ctx *Goster.Ctx) error {
urlPath, _ := ctx.Path.Get("path")
println("Path: " + urlPath)
return nil
})

g.Start(":8001")
}
39 changes: 13 additions & 26 deletions goster.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,6 @@ type Goster struct {
Logs []string // Logs stores logs for future reference.
}

// Route represents an HTTP route with a type and a handler function.
type Route struct {
Type string // Type specifies the type of the route (e.g., "static", "dynamic").
Handler RequestHandler // Handler is the function that handles the route.
}

// RequestHandler is a type for functions that handle HTTP requests within a given context.
type RequestHandler func(ctx *Ctx) error

Expand All @@ -31,7 +25,7 @@ func NewServer() *Goster {
return g
}

// UseGlobal adds middleware handlers that will be applied to every single request.
// UseGlobal adds middleware handlers that will be applied to every single incoming request.
func (g *Goster) UseGlobal(m ...RequestHandler) {
g.Middleware["*"] = append(g.Middleware["*"], m...)
}
Expand Down Expand Up @@ -94,34 +88,20 @@ func (g *Goster) StartTLS(addr string, certFile string, keyFile string) {
// ServeHTTP is the handler for incoming HTTP requests to the server.
// It parses the request, manages routing, and is required to implement the http.Handler interface.
func (g *Goster) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := Ctx{
Request: r,
Response: Response{w},
Meta: Meta{
Query: make(map[string]string),
Path: make(map[string]string),
},
}
ctx := NewContext(r, w)
// Parse the URL and extract query parameters into the Meta struct
urlPath := ctx.Request.URL.EscapedPath()
method := ctx.Request.Method
DefaultHeader(&ctx)

// Validate the route based on the HTTP method and URL
status := g.validateRoute(method, urlPath)
if status != http.StatusOK {
ctx.Response.WriteHeader(status)
return
}

// Construct a normal route from URL path if it matches a specific dynamic route
// Construct a static route from URL path if it matches a specific dynamic or wildcard route
for routePath, route := range g.Routes[method] {
if route.Type != "dynamic" {
if route.Type == Static {
continue
}

if matchesDynamicRoute(urlPath, routePath) {
ctx.Meta.ParseDynamicPath(urlPath, routePath)
if urlMatchesRoute(urlPath, routePath) {
ctx.Meta.ParsePath(urlPath, routePath)
err := g.Routes.New(method, urlPath, route.Handler)
if err != nil {
_ = fmt.Errorf("route %s is duplicate", urlPath) // TODO: it is duplicate, handle
Expand All @@ -130,6 +110,13 @@ func (g *Goster) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
}

// Validate the route based on the HTTP method and URL
status := g.validateRoute(method, urlPath)
if status != http.StatusOK {
ctx.Response.WriteHeader(status)
return
}

// Parses query params if any and adds them to query map
ctx.Meta.ParseQueryParams(r.URL.String())

Expand Down
2 changes: 1 addition & 1 deletion goster_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ func TestIsDynamicRoute(t *testing.T) {

failedCases := make(map[int]IsDynamicRouteCase, 0)
for i, c := range testCases {
if matchesDynamicRoute(c.url, c.dynamicPath) != c.expectedResult {
if urlMatchesRoute(c.url, c.dynamicPath) != c.expectedResult {
failedCases[i] = c
} else {
t.Logf("PASSED [%d] - %s\n", i, c.name)
Expand Down
16 changes: 6 additions & 10 deletions meta.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ type Meta struct {
Path Path
}

type DynamicPath struct {
path string
value string
type PathValues struct {
Key string
Value string
}

type Params map[string]string
Expand Down Expand Up @@ -69,16 +69,12 @@ func (m *Meta) ParseQueryParams(url string) {
}
}

func (m *Meta) ParseDynamicPath(url, urlPath string) {
func (m *Meta) ParsePath(url, urlPath string) {
cleanPath(&url)
cleanPath(&urlPath)
dynamicPaths, isDynamic := matchDynamicPath(url, urlPath)

if !isDynamic {
return
}
dynamicPaths := constructElements(url, urlPath)

for _, dynamicPath := range dynamicPaths {
m.Path[dynamicPath.path] = dynamicPath.value
m.Path[dynamicPath.Key] = dynamicPath.Value
}
}
Loading
Loading