diff --git a/http/ant_matcher.go b/http/ant_matcher.go new file mode 100644 index 0000000..d6b4708 --- /dev/null +++ b/http/ant_matcher.go @@ -0,0 +1,109 @@ +// Copyright 2025 Codnect +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package http + +import ( + "regexp" + "strings" +) + +type antRoute struct { + method string + pattern string + matcher *regexp.Regexp + placeholders []string + handler Handler + middleware []Middleware +} + +// newAntRoute builds an ant-style route with placeholders and wildcard support. +func newAntRoute(method, pattern string, handler Handler, middleware []Middleware) (*antRoute, error) { + regex, placeholders, err := compileAntPattern(pattern) + if err != nil { + return nil, err + } + + return &antRoute{ + method: strings.ToUpper(method), + pattern: pattern, + matcher: regex, + placeholders: placeholders, + handler: handler, + middleware: middleware, + }, nil +} + +func (r *antRoute) match(method, path string) (map[string]string, bool) { + if r.method != method { + return nil, false + } + + matches := r.matcher.FindStringSubmatch(path) + if len(matches) == 0 { + return nil, false + } + + params := make(map[string]string) + for i, name := range r.placeholders { + params[name] = matches[i+1] + } + return params, true +} + +// compileAntPattern compiles an ant-style pattern into a regular expression. +func compileAntPattern(pattern string) (*regexp.Regexp, []string, error) { + var builder strings.Builder + builder.WriteString("^") + + placeholders := make([]string, 0) + for i := 0; i < len(pattern); i++ { + ch := pattern[i] + + switch ch { + case '*': + if i+1 < len(pattern) && pattern[i+1] == '*' { + builder.WriteString("(.*)") + i++ + } else { + builder.WriteString("([^/]*)") + } + case '?': + builder.WriteString("([^/])") + case '{': + end := strings.IndexByte(pattern[i:], '}') + if end == -1 { + return nil, nil, ErrInvalidPattern + } + + placeholder := pattern[i+1 : i+end] + placeholders = append(placeholders, placeholder) + builder.WriteString("([^/]+)") + i += end + default: + if strings.ContainsRune(".+()^$|[]\\", rune(ch)) { + builder.WriteString("\\") + } + builder.WriteByte(ch) + } + } + + builder.WriteString("$") + regex, err := regexp.Compile(builder.String()) + if err != nil { + return nil, nil, err + } + + return regex, placeholders, nil +} diff --git a/http/context.go b/http/context.go new file mode 100644 index 0000000..0774de5 --- /dev/null +++ b/http/context.go @@ -0,0 +1,67 @@ +// Copyright 2025 Codnect +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package http + +import "io" + +// Handler represents an HTTP handler function. +type Handler func(Context) + +// Middleware represents an HTTP middleware component. +type Middleware = Handler + +// Context represents the request-scoped context passed to handlers and middleware. +type Context interface { + // Request returns the incoming HTTP request abstraction. + Request() Request + // Response returns the outgoing HTTP response abstraction. + Response() Response + // Security returns the active SecurityContext. + Security() SecurityContext + // SetSecurity replaces the current security context. + SetSecurity(SecurityContext) + // Next executes the next middleware or handler in the chain. + Next() + // Params returns the matched route parameters. + Params() map[string]string + // Param retrieves a specific route parameter by name. + Param(string) string + // Set stores a value in the context. + Set(string, any) + // Get retrieves a stored value and a boolean indicating presence. + Get(string) (any, bool) +} + +// Request is an abstract HTTP request representation. +type Request interface { + // Method returns the HTTP method of the request. + Method() string + // Path returns the request path. + Path() string + // Header returns the first value for the named header. + Header(string) string + // Body returns the raw request body reader. + Body() io.ReadCloser +} + +// Response is an abstract HTTP response representation. +type Response interface { + // Status writes the HTTP status code. + Status(int) + // Header sets a header value on the response. + Header(string, string) + // Write writes the response body bytes. + Write([]byte) (int, error) +} diff --git a/http/radix_tree.go b/http/radix_tree.go new file mode 100644 index 0000000..9a5ce1c --- /dev/null +++ b/http/radix_tree.go @@ -0,0 +1,126 @@ +// Copyright 2025 Codnect +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package http + +import "strings" + +type radixNode struct { + segment string + children map[string]*radixNode + paramChild *radixNode + wildcard *radixNode + paramName string + handlers map[string]routeEntry +} + +type routeEntry struct { + handler Handler + middleware []Middleware +} + +func newRadixNode(segment string) *radixNode { + return &radixNode{ + segment: segment, + children: make(map[string]*radixNode), + handlers: make(map[string]routeEntry), + } +} + +func (n *radixNode) addRoute(method, pattern string, handler Handler, middleware []Middleware) { + segments := splitPath(pattern) + current := n + + for _, segment := range segments { + switch { + case strings.HasPrefix(segment, ":"): + if current.paramChild == nil { + current.paramChild = newRadixNode(segment) + current.paramChild.paramName = strings.TrimPrefix(segment, ":") + } + current = current.paramChild + case segment == "**": + if current.wildcard == nil { + current.wildcard = newRadixNode(segment) + } + current = current.wildcard + default: + child, ok := current.children[segment] + if !ok { + child = newRadixNode(segment) + current.children[segment] = child + } + current = child + } + } + + current.handlers[strings.ToUpper(method)] = routeEntry{handler: handler, middleware: middleware} +} + +func (n *radixNode) match(method, path string) (map[string]string, routeEntry, bool) { + segments := splitPath(path) + params := make(map[string]string) + if entry, ok := n.matchSegments(strings.ToUpper(method), segments, params); ok { + return params, entry, true + } + return nil, routeEntry{}, false +} + +func (n *radixNode) matchSegments(method string, segments []string, params map[string]string) (routeEntry, bool) { + if len(segments) == 0 { + if entry, ok := n.handlers[method]; ok { + return entry, true + } + if n.wildcard != nil { + if entry, ok := n.wildcard.handlers[method]; ok { + return entry, true + } + } + return routeEntry{}, false + } + + segment := segments[0] + + if child, ok := n.children[segment]; ok { + if entry, ok := child.matchSegments(method, segments[1:], params); ok { + return entry, true + } + } + + if n.paramChild != nil { + params[n.paramChild.paramName] = segment + if entry, ok := n.paramChild.matchSegments(method, segments[1:], params); ok { + return entry, true + } + delete(params, n.paramChild.paramName) + } + + if n.wildcard != nil { + params["*wildcard"] = strings.Join(segments, "/") + if entry, ok := n.wildcard.matchSegments(method, nil, params); ok { + return entry, true + } + delete(params, "*wildcard") + } + + return routeEntry{}, false +} + +func splitPath(path string) []string { + cleaned := strings.Trim(path, "/") + if cleaned == "" { + return nil + } + return strings.Split(cleaned, "/") +} diff --git a/http/router.go b/http/router.go new file mode 100644 index 0000000..93e8a7b --- /dev/null +++ b/http/router.go @@ -0,0 +1,81 @@ +// Copyright 2025 Codnect +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package http + +import ( + "errors" + "strings" +) + +// ErrInvalidPattern indicates the provided pattern is malformed. +var ErrInvalidPattern = errors.New("invalid route pattern") + +// Router routes HTTP requests using radix-tree and ant-style pattern matching. +type Router struct { + root *radixNode + antRoutes []*antRoute + notFound Handler +} + +// NewRouter creates a new Router instance. +func NewRouter() *Router { + return &Router{ + root: newRadixNode("/"), + notFound: func(ctx Context) {}, + } +} + +// Handle registers a route for the given method and pattern. +func (r *Router) Handle(method, pattern string, handler Handler, middleware ...Middleware) error { + pattern = strings.TrimSpace(pattern) + if pattern == "" { + return ErrInvalidPattern + } + + if strings.ContainsAny(pattern, "*?{") { + route, err := newAntRoute(method, pattern, handler, middleware) + if err != nil { + return err + } + r.antRoutes = append(r.antRoutes, route) + return nil + } + + r.root.addRoute(method, pattern, handler, middleware) + return nil +} + +// NotFound sets the handler executed when no routes match. +func (r *Router) NotFound(handler Handler) { + if handler != nil { + r.notFound = handler + } +} + +// Resolve resolves the incoming method and path into a handler and middleware chain. +func (r *Router) Resolve(method, path string) (Handler, []Middleware, map[string]string) { + params, entry, ok := r.root.match(strings.ToUpper(method), path) + if ok { + return entry.handler, entry.middleware, params + } + + for _, route := range r.antRoutes { + if params, matched := route.match(strings.ToUpper(method), path); matched { + return route.handler, route.middleware, params + } + } + + return r.notFound, nil, map[string]string{} +} diff --git a/http/security.go b/http/security.go new file mode 100644 index 0000000..b9d0fd1 --- /dev/null +++ b/http/security.go @@ -0,0 +1,82 @@ +// Copyright 2025 Codnect +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package http + +// SecurityContext represents authorization and authentication data associated with the request. +type SecurityContext interface { + // Principal returns the authenticated principal or nil when unauthenticated. + Principal() any + // IsAuthenticated returns true when the principal is authenticated. + IsAuthenticated() bool + // HasAuthority checks for a granted authority/role. + HasAuthority(string) bool + // Attributes exposes additional security attributes. + Attributes() map[string]any +} + +// SimpleSecurityContext provides a minimal SecurityContext implementation. +type SimpleSecurityContext struct { + principal any + authenticated bool + authorities []string + attributes map[string]any +} + +// NewUnauthenticatedSecurityContext creates an unauthenticated security context. +func NewUnauthenticatedSecurityContext() *SimpleSecurityContext { + return &SimpleSecurityContext{ + principal: nil, + authenticated: false, + attributes: make(map[string]any), + } +} + +// NewSecurityContext creates an authenticated security context with the given principal and authorities. +func NewSecurityContext(principal any, authorities ...string) *SimpleSecurityContext { + return &SimpleSecurityContext{ + principal: principal, + authenticated: true, + authorities: authorities, + attributes: make(map[string]any), + } +} + +// Principal returns the associated principal. +func (c *SimpleSecurityContext) Principal() any { + return c.principal +} + +// IsAuthenticated returns true if the principal is authenticated. +func (c *SimpleSecurityContext) IsAuthenticated() bool { + return c.authenticated +} + +// HasAuthority checks if the requested authority is present. +func (c *SimpleSecurityContext) HasAuthority(authority string) bool { + for _, granted := range c.authorities { + if granted == authority { + return true + } + } + return false +} + +// Attributes returns additional security attributes. +func (c *SimpleSecurityContext) Attributes() map[string]any { + if c.attributes == nil { + c.attributes = make(map[string]any) + } + return c.attributes +} diff --git a/http/server.go b/http/server.go new file mode 100644 index 0000000..5df9eed --- /dev/null +++ b/http/server.go @@ -0,0 +1,72 @@ +// Copyright 2025 Codnect +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package http + +import ( + stdhttp "net/http" + "strings" +) + +// Server is the default HTTP server implementation using the standard net/http stack. +type Server struct { + router *Router + middleware []Middleware +} + +// NewServer creates a Server with a router ready to register handlers. +func NewServer() *Server { + return &Server{ + router: NewRouter(), + } +} + +// Router exposes the underlying router to allow advanced configuration. +func (s *Server) Router() *Router { + return s.router +} + +// Use registers middleware executed for every request. +func (s *Server) Use(middleware ...Middleware) { + s.middleware = append(s.middleware, middleware...) +} + +// Handle registers a handler for the given method and pattern. +func (s *Server) Handle(method, pattern string, handler Handler, middleware ...Middleware) error { + return s.router.Handle(method, pattern, handler, middleware...) +} + +// ServeHTTP satisfies stdlib's http.Handler and dispatches requests through the router. +func (s *Server) ServeHTTP(w stdhttp.ResponseWriter, r *stdhttp.Request) { + method := strings.ToUpper(r.Method) + path := r.URL.Path + + handler, middleware, params := s.router.Resolve(method, path) + request := &standardRequest{req: r} + response := &standardResponse{writer: w} + + security := NewUnauthenticatedSecurityContext() + ctx := newStandardContext(request, response, security, params, append(s.middleware, middleware...), handler) + ctx.Next() +} + +// ListenAndServe starts the HTTP server on the given address. +func (s *Server) ListenAndServe(addr string) error { + return stdhttp.ListenAndServe(addr, s) +} + +// ListenAndServeTLS starts the HTTP server with TLS configuration. +func (s *Server) ListenAndServeTLS(addr, certFile, keyFile string) error { + return stdhttp.ListenAndServeTLS(addr, certFile, keyFile, s) +} diff --git a/http/standard_context.go b/http/standard_context.go new file mode 100644 index 0000000..be654dc --- /dev/null +++ b/http/standard_context.go @@ -0,0 +1,136 @@ +// Copyright 2025 Codnect +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package http + +import ( + "io" + + stdhttp "net/http" +) + +type standardContext struct { + request Request + response Response + security SecurityContext + params map[string]string + storage map[string]any + handlers []Handler + index int +} + +func newStandardContext(req Request, res Response, sec SecurityContext, params map[string]string, middleware []Middleware, handler Handler) *standardContext { + handlers := make([]Handler, 0, len(middleware)+1) + handlers = append(handlers, middleware...) + handlers = append(handlers, handler) + + return &standardContext{ + request: req, + response: res, + security: sec, + params: params, + storage: make(map[string]any), + handlers: handlers, + index: -1, + } +} + +func (c *standardContext) Request() Request { + return c.request +} + +func (c *standardContext) Response() Response { + return c.response +} + +func (c *standardContext) Security() SecurityContext { + return c.security +} + +func (c *standardContext) SetSecurity(sec SecurityContext) { + if sec != nil { + c.security = sec + } +} + +func (c *standardContext) Next() { + c.index++ + if c.index < len(c.handlers) { + c.handlers[c.index](c) + } +} + +func (c *standardContext) Params() map[string]string { + clone := make(map[string]string) + for k, v := range c.params { + clone[k] = v + } + return clone +} + +func (c *standardContext) Param(name string) string { + return c.params[name] +} + +func (c *standardContext) Set(key string, value any) { + c.storage[key] = value +} + +func (c *standardContext) Get(key string) (any, bool) { + val, ok := c.storage[key] + return val, ok +} + +// standardRequest adapts net/http.Request to the Request interface. +type standardRequest struct { + req *stdhttp.Request +} + +func (r *standardRequest) Method() string { + return r.req.Method +} + +func (r *standardRequest) Path() string { + return r.req.URL.Path +} + +func (r *standardRequest) Header(name string) string { + return r.req.Header.Get(name) +} + +func (r *standardRequest) Body() io.ReadCloser { + return r.req.Body +} + +// standardResponse adapts http.ResponseWriter to the Response interface. +type standardResponse struct { + writer stdhttp.ResponseWriter + wroteHeader bool +} + +func (r *standardResponse) Status(code int) { + r.writer.WriteHeader(code) + r.wroteHeader = true +} + +func (r *standardResponse) Header(name, value string) { + r.writer.Header().Set(name, value) +} + +func (r *standardResponse) Write(body []byte) (int, error) { + if !r.wroteHeader { + r.Status(stdhttp.StatusOK) + } + return r.writer.Write(body) +}