108 lines
3.8 KiB
Go
108 lines
3.8 KiB
Go
package adminapi
|
|
|
|
import (
|
|
"io/fs"
|
|
"strings"
|
|
|
|
"github.com/iceBear67/simplepages/api"
|
|
"github.com/iceBear67/simplepages/internal/config"
|
|
)
|
|
|
|
// Field length caps. They exist to stop a client filling the database with a
|
|
// megabyte of display name, not to express any semantic limit.
|
|
const (
|
|
maxDisplayNameLen = 200
|
|
maxKeyNameLen = 200
|
|
maxSitePathLen = 1024
|
|
maxCacheControlLen = 256
|
|
)
|
|
|
|
// checkProjectName validates a name before it becomes a row and, later, a
|
|
// "~name" entry inside $WEBROOT. The pattern is the only thing standing between
|
|
// a project name and the filesystem, so this is a hard reject rather than a
|
|
// normalisation.
|
|
func checkProjectName(name string) error {
|
|
if name == "" {
|
|
return api.Errorf(api.CodeInvalidProjectName, "project name is required")
|
|
}
|
|
if !config.ProjectNamePattern.MatchString(name) {
|
|
return api.Errorf(api.CodeInvalidProjectName,
|
|
"project name must match %s", config.ProjectNamePattern.String())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// checkText rejects control characters in free-text fields.
|
|
//
|
|
// These strings are printed to operator terminals by "pages project list" and
|
|
// written into structured logs. An embedded ESC would let whoever set the field
|
|
// move the cursor, recolour the output or rewrite the line the operator is
|
|
// reading; a newline would forge a second log record.
|
|
func checkText(field, v string, max int) error {
|
|
if len(v) > max {
|
|
return api.Errorf(api.CodeBadRequest, "%s must be at most %d bytes", field, max)
|
|
}
|
|
for i := 0; i < len(v); i++ {
|
|
if c := v[i]; c < 0x20 || c == 0x7f {
|
|
return api.Errorf(api.CodeBadRequest, "%s must not contain control characters", field)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// checkSitePath validates a path that names a document inside a deployment,
|
|
// such as index_file or not_found_file.
|
|
//
|
|
// It is checked here as well as at serve time because a value that cannot
|
|
// possibly resolve is a configuration mistake worth reporting at the moment it
|
|
// is made, rather than as a silent 404 on every request afterwards.
|
|
func checkSitePath(field, v string) error {
|
|
if v == "" {
|
|
return api.Errorf(api.CodeInvalidPath, "%s must not be empty", field)
|
|
}
|
|
if len(v) > maxSitePathLen {
|
|
return api.Errorf(api.CodeInvalidPath, "%s must be at most %d bytes", field, maxSitePathLen)
|
|
}
|
|
// fs.ValidPath rejects "..", absolute paths, empty segments and a trailing
|
|
// slash. It accepts ".", which is a directory rather than a document.
|
|
if !fs.ValidPath(v) || v == "." {
|
|
return api.Errorf(api.CodeInvalidPath,
|
|
"%s must be a relative slash-separated path with no . or .. segments", field)
|
|
}
|
|
if strings.ContainsRune(v, '\\') {
|
|
return api.Errorf(api.CodeInvalidPath, "%s must use forward slashes", field)
|
|
}
|
|
for i := 0; i < len(v); i++ {
|
|
if c := v[i]; c < 0x20 || c == 0x7f {
|
|
return api.Errorf(api.CodeInvalidPath, "%s must not contain control characters", field)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// checkHeaderValue validates a string that is emitted verbatim as a response
|
|
// header value.
|
|
//
|
|
// A CR or LF here would be response splitting: the project could append headers
|
|
// of its own choosing to every response it serves, and under path routing those
|
|
// responses share an origin with every other project. net/http replaces newlines
|
|
// with spaces on write, so this is defence in depth — but it is the layer that
|
|
// keeps the bad value out of the database in the first place, and it is the one
|
|
// that tells the operator they typed something wrong.
|
|
func checkHeaderValue(field, v string) error {
|
|
if len(v) > maxCacheControlLen {
|
|
return api.Errorf(api.CodeBadRequest, "%s must be at most %d bytes", field, maxCacheControlLen)
|
|
}
|
|
for i := 0; i < len(v); i++ {
|
|
c := v[i]
|
|
if c == '\t' {
|
|
continue
|
|
}
|
|
if c < 0x20 || c > 0x7e {
|
|
return api.Errorf(api.CodeBadRequest,
|
|
"%s must contain only printable ASCII (no newlines)", field)
|
|
}
|
|
}
|
|
return nil
|
|
}
|