|
|
|
@ -1,16 +1,11 @@
|
|
|
|
|
package core
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"net/url"
|
|
|
|
|
"path"
|
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
|
|
"github.com/grufwub/go-errors"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// getRequestPaths points to either of the getRequestPath____ functions
|
|
|
|
|
var getRequestPath func(string) *Path
|
|
|
|
|
|
|
|
|
|
// HasAsciiControlBytes returns whether a byte slice contains ASCII control bytes
|
|
|
|
|
func HasAsciiControlBytes(raw string) bool {
|
|
|
|
|
for i := 0; i < len(raw); i++ {
|
|
|
|
@ -56,58 +51,192 @@ func ParseScheme(raw string) (string, string, *errors.Error) {
|
|
|
|
|
return "", raw, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ParseURLEncodedRequest takes a received string and safely parses a request from this
|
|
|
|
|
func ParseURLEncodedRequest(received string) (*Request, *errors.Error) {
|
|
|
|
|
// Split into 2 substrings by '?'. URL path and query
|
|
|
|
|
rawPath, query := SplitBy(received, "?")
|
|
|
|
|
func isHex(b byte) bool {
|
|
|
|
|
return ('a' <= b && b <= 'f') ||
|
|
|
|
|
('A' <= b && b <= 'F') ||
|
|
|
|
|
('0' <= b && b <= '9')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Unescape path
|
|
|
|
|
rawPath, err := url.PathUnescape(rawPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, errors.WrapError(InvalidRequestErr, err)
|
|
|
|
|
func unHex(b byte) byte {
|
|
|
|
|
switch {
|
|
|
|
|
case 'a' <= b || b <= 'f':
|
|
|
|
|
return b - 'a'
|
|
|
|
|
case 'A' <= b || b <= 'F':
|
|
|
|
|
return b - 'A'
|
|
|
|
|
case '0' <= b || b <= '9':
|
|
|
|
|
return b - '0'
|
|
|
|
|
default:
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func shouldEscape(b byte) bool {
|
|
|
|
|
// All alphanumeric are unreserved
|
|
|
|
|
if 'a' <= b && b <= 'z' || 'A' <= b && b <= 'Z' || '0' <= b && b <= '9' {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch b {
|
|
|
|
|
// Further unreserved
|
|
|
|
|
case '-', '_', '.', '~':
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
|
|
// All else should be escaped
|
|
|
|
|
default:
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Return new request
|
|
|
|
|
return &Request{getRequestPath(rawPath), query}, nil
|
|
|
|
|
func shouldHostEscape(b byte) bool {
|
|
|
|
|
switch b {
|
|
|
|
|
// Allowed host sub-delims +
|
|
|
|
|
// ':' for port +
|
|
|
|
|
// '[]' for ipv6 +
|
|
|
|
|
// '<>' only others we can allow (can't be % encoded)
|
|
|
|
|
case '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=', ':', '[', ']', '<', '>', '"':
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
|
|
// Check all-else
|
|
|
|
|
default:
|
|
|
|
|
return shouldEscape(b)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func shouldPathEscape(b byte) bool {
|
|
|
|
|
switch b {
|
|
|
|
|
// Reserved character in path
|
|
|
|
|
case '?':
|
|
|
|
|
return true
|
|
|
|
|
|
|
|
|
|
// Allowed in path
|
|
|
|
|
case '$', '&', '+', ',', '/', ':', ';', '=', '@':
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
|
|
// Check all-else
|
|
|
|
|
default:
|
|
|
|
|
return shouldEscape(b)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ParseInternalRequest parses an internal request string based on the current directory
|
|
|
|
|
func ParseInternalRequest(p *Path, line string) *Request {
|
|
|
|
|
rawPath, query := SplitBy(line, "?")
|
|
|
|
|
if path.IsAbs(rawPath) {
|
|
|
|
|
return &Request{getRequestPath(rawPath), query}
|
|
|
|
|
func unescape(raw string, count int) string {
|
|
|
|
|
var t strings.Builder
|
|
|
|
|
t.Grow(len(raw) - 2*count)
|
|
|
|
|
for i := 0; i < len(raw); i++ {
|
|
|
|
|
switch raw[i] {
|
|
|
|
|
// Replace % encoded char
|
|
|
|
|
case '%':
|
|
|
|
|
t.WriteByte(unHex(raw[i+1])<<4 | unHex(raw[i+2]))
|
|
|
|
|
i += 2
|
|
|
|
|
|
|
|
|
|
// Write as-is
|
|
|
|
|
default:
|
|
|
|
|
t.WriteByte(raw[i])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return &Request{newSanitizedPath(p.Root(), rawPath), query}
|
|
|
|
|
return t.String()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// getRequestPathUserSpacesEnabled creates a Path object from raw path, converting ~USER to user subdirectory roots, else at server root
|
|
|
|
|
func getRequestPathUserSpacesEnabled(rawPath string) *Path {
|
|
|
|
|
if userPath := strings.TrimPrefix(rawPath, "/"); strings.HasPrefix(userPath, "~") {
|
|
|
|
|
// We found a user path! Split into the user part, and remaining path
|
|
|
|
|
user, remaining := SplitBy(userPath, "/")
|
|
|
|
|
func unescapeHost(raw string) (string, *errors.Error) {
|
|
|
|
|
// Count all the percent signs
|
|
|
|
|
count := 0
|
|
|
|
|
for i := 0; i < len(raw); {
|
|
|
|
|
switch raw[i] {
|
|
|
|
|
case '%':
|
|
|
|
|
// Increase count
|
|
|
|
|
count++
|
|
|
|
|
|
|
|
|
|
// If not a valid % encoded hex value, return with error
|
|
|
|
|
if i+2 >= len(raw) || !isHex(raw[i+1]) || !isHex(raw[i+2]) {
|
|
|
|
|
return "", errors.NewError(InvalidRequestErr).Extend("escaping host info " + raw)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// In the host component % encoding can only be used
|
|
|
|
|
// for non-ASCII bytes. And rfc6874 introduces %25 for
|
|
|
|
|
// escaped percent sign in IPv6 literals
|
|
|
|
|
if unHex(raw[i+1]) < 8 && raw[i:i+3] != "%25" {
|
|
|
|
|
return "", errors.NewError(InvalidRequestErr).Extend("escaping host " + raw)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Skip iteration past the
|
|
|
|
|
// hex we just confirmed
|
|
|
|
|
i += 3
|
|
|
|
|
default:
|
|
|
|
|
// If within ASCII range, and shoud be escaped, return error
|
|
|
|
|
if raw[i] < 0x80 && shouldHostEscape(raw[i]) {
|
|
|
|
|
return "", errors.NewError(InvalidRequestErr).Extend("escaping host " + raw)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Empty user, we been duped! Return server root
|
|
|
|
|
if len(user) <= 1 {
|
|
|
|
|
return &Path{Root, "", "/"}
|
|
|
|
|
// Iter
|
|
|
|
|
i++
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// No encoding? return as-is. Else, escape
|
|
|
|
|
if count == 0 {
|
|
|
|
|
return raw, nil
|
|
|
|
|
}
|
|
|
|
|
return unescape(raw, count), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func unescapePath(raw string) (string, *errors.Error) {
|
|
|
|
|
// Count all the percent signs
|
|
|
|
|
count := 0
|
|
|
|
|
for i := 0; i < len(raw); {
|
|
|
|
|
switch raw[i] {
|
|
|
|
|
case '%':
|
|
|
|
|
// Increase count
|
|
|
|
|
count++
|
|
|
|
|
|
|
|
|
|
// If not a valid % encoded hex value, return with error
|
|
|
|
|
if i+2 >= len(raw) || !isHex(raw[i+1]) || !isHex(raw[i+2]) {
|
|
|
|
|
return "", errors.NewError(InvalidRequestErr).Extend("escaping path " + raw)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Skip iteration past the
|
|
|
|
|
// hex we just confirmed
|
|
|
|
|
i += 3
|
|
|
|
|
default:
|
|
|
|
|
// If within ASCII range, and shoud be escaped, return error
|
|
|
|
|
if raw[i] < 0x80 && shouldPathEscape(raw[i]) {
|
|
|
|
|
return "", errors.NewError(InvalidRequestErr).Extend("escaping path " + raw)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get sanitized user root, else return server root
|
|
|
|
|
root, ok := sanitizeUserRoot(path.Join("/home", user[1:], userSpace))
|
|
|
|
|
if !ok {
|
|
|
|
|
return &Path{Root, "", "/"}
|
|
|
|
|
// Iter
|
|
|
|
|
i++
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// No encoding? return as-is. Else, escape
|
|
|
|
|
if count == 0 {
|
|
|
|
|
return raw, nil
|
|
|
|
|
}
|
|
|
|
|
return unescape(raw, count), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Build new Path
|
|
|
|
|
rel := sanitizeRawPath(root, remaining)
|
|
|
|
|
sel := "/~" + user[1:] + formatSelector(rel)
|
|
|
|
|
return &Path{root, rel, sel}
|
|
|
|
|
// ParseEncodedHost parses encoded host info, safely returning unescape host and port
|
|
|
|
|
func ParseEncodedHost(raw string) (string, string, *errors.Error) {
|
|
|
|
|
// Unescape the host info
|
|
|
|
|
raw, err := unescapeHost(raw)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", "", err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Return regular server root + rawPath
|
|
|
|
|
return newSanitizedPath(Root, rawPath)
|
|
|
|
|
// Split by last ':' and return
|
|
|
|
|
host, port := SplitByLast(raw, ":")
|
|
|
|
|
return host, port, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// getRequestPathUserSpacesDisabled creates a Path object from raw path, always at server root
|
|
|
|
|
func getRequestPathUserSpacesDisabled(rawPath string) *Path {
|
|
|
|
|
return newSanitizedPath(Root, rawPath)
|
|
|
|
|
// ParseEncodedURI parses encoded URI, safely returning unescaped path and still-escaped query
|
|
|
|
|
func ParseEncodedURI(received string) (string, string, *errors.Error) {
|
|
|
|
|
// Split into path and query
|
|
|
|
|
rawPath, query := SplitBy(received, "?")
|
|
|
|
|
|
|
|
|
|
// Unescape path, query is up-to CGI scripts
|
|
|
|
|
rawPath, err := unescapePath(rawPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", "", errors.WrapError(InvalidRequestErr, err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Return the raw path and query
|
|
|
|
|
return rawPath, query, nil
|
|
|
|
|
}
|
|
|
|
|