mirror of
https://github.com/danielmiessler/Fabric.git
synced 2026-01-10 23:08:06 -05:00
Add initial set of utility plugins for the template system: - datetime: Date/time formatting and manipulation - fetch: HTTP content retrieval and processing - file: File system operations and content handling - sys: System information and environment access - text: String manipulation and formatting operations Each plugin includes: - Implementation with comprehensive test coverage - Markdown documentation of capabilities - Integration with template package This builds on the template system to provide practical utility functions while maintaining a focused scope for the initial plugin release.
134 lines
4.3 KiB
Go
134 lines
4.3 KiB
Go
// Package template provides URL fetching operations for the template system.
|
|
// Security Note: This plugin makes outbound HTTP requests. Use with caution
|
|
// and consider implementing URL allowlists in production.
|
|
package template
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"mime"
|
|
"net/http"
|
|
"strings"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
const (
|
|
// MaxContentSize limits response size to 1MB to prevent memory issues
|
|
MaxContentSize = 1024 * 1024
|
|
|
|
// UserAgent identifies the client in HTTP requests
|
|
UserAgent = "Fabric-Fetch/1.0"
|
|
)
|
|
|
|
// FetchPlugin provides HTTP fetching capabilities with safety constraints:
|
|
// - Only text content types allowed
|
|
// - Size limited to MaxContentSize
|
|
// - UTF-8 validation
|
|
// - Null byte checking
|
|
type FetchPlugin struct{}
|
|
|
|
// Apply executes fetch operations:
|
|
// - get:URL: Fetches content from URL, returns text content
|
|
func (p *FetchPlugin) Apply(operation string, value string) (string, error) {
|
|
debugf("Fetch: operation=%q value=%q", operation, value)
|
|
|
|
switch operation {
|
|
case "get":
|
|
return p.fetch(value)
|
|
default:
|
|
return "", fmt.Errorf("fetch: unknown operation %q (supported: get)", operation)
|
|
}
|
|
}
|
|
|
|
// isTextContent checks if the content type is text-based
|
|
func (p *FetchPlugin) isTextContent(contentType string) bool {
|
|
debugf("Fetch: checking content type %q", contentType)
|
|
|
|
mediaType, _, err := mime.ParseMediaType(contentType)
|
|
if err != nil {
|
|
debugf("Fetch: error parsing media type: %v", err)
|
|
return false
|
|
}
|
|
|
|
isText := strings.HasPrefix(mediaType, "text/") ||
|
|
mediaType == "application/json" ||
|
|
mediaType == "application/xml" ||
|
|
mediaType == "application/yaml" ||
|
|
mediaType == "application/x-yaml" ||
|
|
strings.HasSuffix(mediaType, "+json") ||
|
|
strings.HasSuffix(mediaType, "+xml") ||
|
|
strings.HasSuffix(mediaType, "+yaml")
|
|
|
|
debugf("Fetch: content type %q is text: %v", mediaType, isText)
|
|
return isText
|
|
}
|
|
|
|
// validateTextContent ensures content is valid UTF-8 without null bytes
|
|
func (p *FetchPlugin) validateTextContent(content []byte) error {
|
|
debugf("Fetch: validating content length=%d bytes", len(content))
|
|
|
|
if !utf8.Valid(content) {
|
|
return fmt.Errorf("fetch: content is not valid UTF-8 text")
|
|
}
|
|
|
|
if bytes.Contains(content, []byte{0}) {
|
|
return fmt.Errorf("fetch: content contains null bytes")
|
|
}
|
|
|
|
debugf("Fetch: content validation successful")
|
|
return nil
|
|
}
|
|
|
|
// fetch retrieves content from a URL with safety checks
|
|
func (p *FetchPlugin) fetch(urlStr string) (string, error) {
|
|
debugf("Fetch: requesting URL %q", urlStr)
|
|
|
|
client := &http.Client{}
|
|
req, err := http.NewRequest("GET", urlStr, nil)
|
|
if err != nil {
|
|
return "", fmt.Errorf("fetch: error creating request: %v", err)
|
|
}
|
|
req.Header.Set("User-Agent", UserAgent)
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return "", fmt.Errorf("fetch: error fetching URL: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
debugf("Fetch: got response status=%q", resp.Status)
|
|
if resp.StatusCode != http.StatusOK {
|
|
return "", fmt.Errorf("fetch: HTTP error: %d - %s", resp.StatusCode, resp.Status)
|
|
}
|
|
|
|
if contentLength := resp.ContentLength; contentLength > MaxContentSize {
|
|
return "", fmt.Errorf("fetch: content too large: %d bytes (max %d bytes)",
|
|
contentLength, MaxContentSize)
|
|
}
|
|
|
|
contentType := resp.Header.Get("Content-Type")
|
|
debugf("Fetch: content-type=%q", contentType)
|
|
if !p.isTextContent(contentType) {
|
|
return "", fmt.Errorf("fetch: unsupported content type %q - only text content allowed",
|
|
contentType)
|
|
}
|
|
|
|
debugf("Fetch: reading response body")
|
|
limitReader := io.LimitReader(resp.Body, MaxContentSize+1)
|
|
content, err := io.ReadAll(limitReader)
|
|
if err != nil {
|
|
return "", fmt.Errorf("fetch: error reading response: %v", err)
|
|
}
|
|
|
|
if len(content) > MaxContentSize {
|
|
return "", fmt.Errorf("fetch: content too large: exceeds %d bytes", MaxContentSize)
|
|
}
|
|
|
|
if err := p.validateTextContent(content); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
debugf("Fetch: operation completed successfully, read %d bytes", len(content))
|
|
return string(content), nil
|
|
} |