summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--cnp.go2
-rw-r--r--common.go24
-rw-r--r--header.go44
-rw-r--r--message.go15
-rw-r--r--request.go30
-rw-r--r--request_test.go24
-rw-r--r--response.go37
-rw-r--r--response_test.go23
-rw-r--r--server.go6
9 files changed, 157 insertions, 48 deletions
diff --git a/cnp.go b/cnp.go
index 9ba8444..d5a3210 100644
--- a/cnp.go
+++ b/cnp.go
@@ -11,6 +11,6 @@ const (
// VersionMajor is the major CNP version (X in cnp/X.Y).
VersionMajor = 0
- // VersionMinor is the major CNP version (Y in cnp/X.Y).
+ // VersionMinor is the minor CNP version (Y in cnp/X.Y).
VersionMinor = 3
)
diff --git a/common.go b/common.go
index e6173b0..310da86 100644
--- a/common.go
+++ b/common.go
@@ -139,3 +139,27 @@ func setTime(m *Message, param string, t time.Time) {
m.SetParam(param, t.UTC().Format(time.RFC3339))
}
}
+
+func getSelect(m *Message, param string) (string, string, error) {
+ s := m.Param(param)
+ if s == "" {
+ return "", "", nil
+ }
+ ss := strings.SplitN(s, ":", 2)
+ if len(ss) != 2 || ss[0] == "" {
+ return "", "", ErrorInvalid{"invalid parameter: " + param + " is not a valid selector"}
+ }
+ return ss[0], ss[1], nil
+}
+
+func setSelect(m *Message, param string, selector, query string) error {
+ if strings.ContainsRune(selector, ':') {
+ return ErrorInvalid{"invalid parameter: " + param + " is not a valid selector name"}
+ }
+ if selector == "" {
+ m.SetParam(param, "")
+ } else {
+ m.SetParam(param, selector+":"+query)
+ }
+ return nil
+}
diff --git a/header.go b/header.go
index e185dca..cb16db5 100644
--- a/header.go
+++ b/header.go
@@ -245,7 +245,7 @@ func (h Header) String() string {
type Parameters map[string]string
// Write writes the parameters encoded for inclusion in the wire format.
-// Includes a leading space.
+// Includes a leading space if p is nonempty.
func (p Parameters) Write(w io.Writer) (err error) {
bw := bufio.NewWriter(w)
keys := []string{}
@@ -280,41 +280,41 @@ func Escape(s string) []byte {
if el == len(s) {
return []byte(s)
}
- bs := make([]byte, el)
+ data := make([]byte, el)
bi := 0
for i := 0; i < len(s); i++ {
switch s[i] {
case '\x00':
- bs[bi] = '\\'
- bs[bi+1] = '0'
+ data[bi] = '\\'
+ data[bi+1] = '0'
bi += 2
case '\n':
- bs[bi] = '\\'
- bs[bi+1] = 'n'
+ data[bi] = '\\'
+ data[bi+1] = 'n'
bi += 2
case ' ':
- bs[bi] = '\\'
- bs[bi+1] = '_'
+ data[bi] = '\\'
+ data[bi+1] = '_'
bi += 2
case '=':
- bs[bi] = '\\'
- bs[bi+1] = '-'
+ data[bi] = '\\'
+ data[bi+1] = '-'
bi += 2
case '\\':
- bs[bi] = '\\'
- bs[bi+1] = '\\'
+ data[bi] = '\\'
+ data[bi+1] = '\\'
bi += 2
default:
- bs[bi] = s[i]
+ data[bi] = s[i]
bi++
}
}
- return bs
+ return data
}
func escapeLength(s string) (l int) {
@@ -329,19 +329,19 @@ func escapeLength(s string) (l int) {
return
}
-// Unescape unescapes the bs from wire format into a bytestring.
-func Unescape(bs []byte) (string, error) {
- buf := make([]byte, len(bs))
+// Unescape unescapes data from wire format into a bytestring.
+func Unescape(data []byte) (string, error) {
+ buf := make([]byte, len(data))
bi := 0
- for i := 0; i < len(bs); i++ {
- switch bs[i] {
+ for i := 0; i < len(data); i++ {
+ switch data[i] {
case '\\':
i++
- if i >= len(bs) {
+ if i >= len(data) {
return string(buf[:bi]), ErrorSyntax{"invalid escape sequence: unexpected end of string"}
}
- switch bs[i] {
+ switch data[i] {
case '0':
buf[bi] = '\x00'
case 'n':
@@ -356,7 +356,7 @@ func Unescape(bs []byte) (string, error) {
return string(buf[:bi]), ErrorSyntax{"invalid escape sequence: undefined sequence"}
}
default:
- buf[bi] = bs[i]
+ buf[bi] = data[i]
}
bi++
}
diff --git a/message.go b/message.go
index eb3d2b9..fdb16b3 100644
--- a/message.go
+++ b/message.go
@@ -31,7 +31,7 @@ func NewMessage(intent string, body io.Reader) *Message {
// ParseMessage parses a CNP message.
//
-// The message's Body field is set to a bufio.Reader wrapping r. If r is a
+// The message's Body field is set to a bufio.Reader wrapping r. If r is an
// io.Closer, it is also stored separately for usage with Message.Close().
func ParseMessage(r io.Reader) (*Message, error) {
br := bufio.NewReader(r)
@@ -77,7 +77,7 @@ func (msg *Message) Close() error {
// ComputeLength sets the length header parameter based on the message body.
// First, msg.TryComputeLength() is attempted; if that fails, the request is
-// fully read into a buffer and msg.Body is set to a bytes.Reader.
+// fully read into a bytes.Buffer and msg.Body is set to it.
func (msg *Message) ComputeLength() error {
if !msg.TryComputeLength() {
buf, err := ioutil.ReadAll(msg.Body)
@@ -94,9 +94,10 @@ func (msg *Message) ComputeLength() error {
}
// TryComputeLength sets the length header parameter to the length of the
-// message body if it's one of *bytes.Buffer, *bytes.Reader or *strings.Reader
-// and returns true. If msg.Body is nil, the length parameter is unset and the
-// function returns true. Otherwise, false is returned.
+// message body if the body's type is one of *bytes.Buffer, *bytes.Reader or
+// *strings.Reader and returns true. If msg.Body is nil, the length parameter
+// is unset and the function returns true. Otherwise, false is returned and the
+// length parameter remains unchanged.
func (msg *Message) TryComputeLength() bool {
switch v := msg.Body.(type) {
case *bytes.Buffer:
@@ -131,13 +132,13 @@ func (msg *Message) Length() int64 {
return n
}
-// Param retrieves a header parameter.
+// Param retrieves a header parameter. It performs no value validation.
func (msg *Message) Param(key string) string {
return msg.Header.Parameters[key]
}
// SetParam sets a header parameter. If the value is empty, the parameter is
-// unset.
+// unset. It performs no value validation.
func (msg *Message) SetParam(key, value string) {
if len(value) == 0 {
delete(msg.Header.Parameters, key)
diff --git a/request.go b/request.go
index 8c5a34e..0fb27a2 100644
--- a/request.go
+++ b/request.go
@@ -29,7 +29,7 @@ func NewRequest(host, pth string, body []byte) (*Request, error) {
return req, nil
}
-// NewRequestURL creates a new Request from a URL and body data.
+// NewRequestURL creates a new Request from a URL and optional body data.
func NewRequestURL(urlStr string, body []byte) (*Request, error) {
// XXX: handle //example.com/path URLs
if strings.HasPrefix(urlStr, "//") {
@@ -163,7 +163,7 @@ func (r *Request) Name() string {
// SetName sets the name request parameter.
//
-// An error is raised if the name includes characters not valid in a filename
+// Returns an error if the name includes characters not valid in a filename
// (slash, null byte).
func (r *Request) SetName(name string) error {
return setFilename(&r.Message, "name", name)
@@ -180,7 +180,7 @@ func (r *Request) Type() string {
// SetType sets the type request parameter.
//
-// An error is raised if typ is not a valid format for a MIME type.
+// Returns an error if typ is not a valid format for a MIME type.
func (r *Request) SetType(typ string) error {
return setType(&r.Message, "type", typ)
}
@@ -204,8 +204,23 @@ func (r *Request) SetIfModified(t time.Time) {
setTime(&r.Message, "if_modified", t)
}
+// Select retrieves the select request parameter.
+//
+// If the parameter isn't a valid selector, empty strings are returned.
+func (r *Request) Select() (selector, query string) {
+ selector, query, _ = getSelect(&r.Message, "select")
+ return
+}
+
+// SetSelect sets the select request parameter.
+//
+// If the selector name is empty, the select parameter is unset.
+func (r *Request) SetSelect(selector, query string) error {
+ return setSelect(&r.Message, "select", selector, query)
+}
+
// Validate validates the request header intent and parameter value format
-// (length, name, type, if_modified)
+// (length, name, type, if_modified, select)
func (r *Request) Validate() error {
if err := validateRequestIntent(r.Intent()); err != nil {
return err
@@ -222,6 +237,9 @@ func (r *Request) Validate() error {
if _, err := getTime(&r.Message, "if_modified"); err != nil {
return err
}
+ if _, _, err := getSelect(&r.Message, "select"); err != nil {
+ return err
+ }
return nil
}
@@ -250,7 +268,9 @@ func (r *Request) Write(w io.Writer) error {
return r.Message.Write(w)
}
-// Clean cleans a CNP intent path.
+// Clean cleans a CNP request intent path.
+//
+// This works the same as path.Clean(), but preserves a trailing slash.
func Clean(s string) string {
c := path.Clean(s)
if len(s) > 0 && len(c) > 0 && s[len(s)-1] == '/' && c[len(c)-1] != '/' {
diff --git a/request_test.go b/request_test.go
index bb4aa9b..23b8ac0 100644
--- a/request_test.go
+++ b/request_test.go
@@ -64,6 +64,10 @@ var requestTests = []requestTest{
{"", "/", Parameters{"if_modified": "12345-01-01T00:00:00Z"}, nil, ErrorInvalid{}},
{"", "/", Parameters{"if_modified": "-5-01-01T00:00:00Z"}, nil, ErrorInvalid{}},
{"", "/", Parameters{"if_modified": "-2005-01-01T00:00:00Z"}, nil, ErrorInvalid{}},
+ {"", "/", Parameters{"select": "w"}, nil, ErrorInvalid{}},
+ {"", "/", Parameters{"select": " "}, nil, ErrorInvalid{}},
+ {"", "/", Parameters{"select": ":"}, nil, ErrorInvalid{}},
+ {"", "/", Parameters{"select": ":foobar"}, nil, ErrorInvalid{}},
// valid simple requests
{"", "/", nil, nil, nil},
@@ -73,7 +77,7 @@ var requestTests = []requestTest{
{"example.com", "/ f=#\\oo///.././.../~/\x01/\xff/ba\nr", nil, nil, nil},
// valid request params
- {"", "/", Parameters{"length": "", "name": "", "type": "", "if_modified": "", "": "", "q\x00we": "=a s\nd"}, nil, nil},
+ {"", "/", Parameters{"length": "", "name": "", "type": "", "if_modified": "", "select": "", "": "", "q\x00we": "=a s\nd"}, nil, nil},
{"", "/", Parameters{"length": "0"}, nil, nil},
{"", "/", Parameters{"length": "1"}, nil, nil},
{"", "/", Parameters{"length": "12345670089000000"}, nil, nil},
@@ -90,6 +94,10 @@ var requestTests = []requestTest{
{"", "/", Parameters{"if_modified": "0123-05-06T07:08:09Z"}, nil, nil},
{"", "/", Parameters{"if_modified": "0000-02-29T00:00:00Z"}, nil, nil},
{"", "/", Parameters{"if_modified": "2000-02-29T00:00:00Z"}, nil, nil},
+ {"", "/", Parameters{"select": "\x00:\x00"}, nil, nil},
+ {"", "/", Parameters{"select": "foo:bar:baz"}, nil, nil},
+ {"", "/", Parameters{"select": "byte:5-"}, nil, nil},
+ {"", "/", Parameters{"select": "cnm:#/foo/bar"}, nil, nil},
}
func TestNewRequest(t *testing.T) {
@@ -274,6 +282,20 @@ func TestRequestGetSet(t *testing.T) {
tm2 = req.IfModified().Format(time.RFC3339)
}
c(k, v, tst.v, tm2)
+ case "select":
+ if tst.v != nil {
+ continue
+ }
+ sel := ""
+ ss := strings.SplitN(v, ":", 2)
+ if len(ss) == 2 {
+ req.SetSelect(ss[0], ss[1])
+ a, b := req.Select()
+ if a != "" {
+ sel = a + ":" + b
+ }
+ }
+ c(k, v, tst.v, sel)
default:
req.SetParam(k, v)
c(k, v, tst.v, req.Param(k))
diff --git a/response.go b/response.go
index bf23f20..3ecb961 100644
--- a/response.go
+++ b/response.go
@@ -76,7 +76,7 @@ func (r *Response) Name() string {
// SetName sets the name response parameter.
//
-// An error is raised if the name includes characters not valid in a filename
+// Returns an error if the name includes characters not valid in a filename
// (slash, null byte).
func (r *Response) SetName(name string) error {
return setFilename(&r.Message, "name", name)
@@ -93,7 +93,7 @@ func (r *Response) Type() string {
// SetType sets the type response parameter.
//
-// An error is raised if typ is not a valid format for a MIME type.
+// Returns an error if typ is not a valid format for a MIME type.
func (r *Response) SetType(typ string) error {
return setType(&r.Message, "type", typ)
}
@@ -131,11 +131,11 @@ func (r *Response) Modified() time.Time {
// SetModified sets the modified response parameter.
//
-// If the time response parameter is empty, it's set to the current time.
-// If t is the zero time value, the modified parameter is unset.
+// If t is the zero time value, the modified parameter is unset. Otherwise, if
+// the time response parameter is empty, it's set to the current time.
func (r *Response) SetModified(t time.Time) {
setTime(&r.Message, "modified", t)
- if r.Time().IsZero() {
+ if !t.IsZero() && r.Time().IsZero() {
r.SetTime(time.Now())
}
}
@@ -159,14 +159,15 @@ func (r *Response) Location() (host, path string, err error) {
// SetLocation sets the location response parameter to host and path.
//
-// If the host or path are invalid
+// Returns an error if the host or path are invalid.
func (r *Response) SetLocation(host, path string) error {
+ err := ErrorInvalid{"invalid response: invalid location parameter"}
if strings.ContainsRune(host, '/') {
- return ErrorInvalid{"invalid response: invalid location parameter"}
+ return err
}
l := host + path
if err := validateRequestIntent(l); err != nil {
- return ErrorInvalid{"invalid response: invalid location parameter"}
+ return err
}
r.SetParam("location", l)
return nil
@@ -208,8 +209,23 @@ func (r *Response) SetReason(reason string) error {
return nil
}
+// Select retrieves the select response parameter.
+//
+// If the parameter isn't a valid selector, empty strings are returned.
+func (r *Response) Select() (selector, query string) {
+ selector, query, _ = getSelect(&r.Message, "select")
+ return
+}
+
+// SetSelect sets the select response parameter.
+//
+// If the selector name is empty, the select parameter is unset.
+func (r *Response) SetSelect(selector, query string) error {
+ return setSelect(&r.Message, "select", selector, query)
+}
+
// Validate validates the response intent and header parameter value format
-// (length, name, type, time, modified, location, reason)
+// (length, name, type, time, modified, location, reason, select)
func (r *Response) Validate() error {
if !responseIntents[r.Intent()] {
return ErrorInvalid{"invalid response: unknown response intent"}
@@ -237,6 +253,9 @@ func (r *Response) Validate() error {
if !responseErrorReasons[r.Param("reason")] {
return ErrorInvalid{"invalid response: unknown error reason"}
}
+ if _, _, err := getSelect(&r.Message, "select"); err != nil {
+ return err
+ }
return nil
}
diff --git a/response_test.go b/response_test.go
index 8033335..97e810b 100644
--- a/response_test.go
+++ b/response_test.go
@@ -101,6 +101,10 @@ var (
{"error", Parameters{"reason": "syntax\n"}, nil, ErrorInvalid{}},
{"error", Parameters{"reason": " server_error"}, nil, ErrorInvalid{}},
{"error", Parameters{"reason": "invalid "}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"select": "w"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"select": " "}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"select": ":"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"select": ":foobar"}, nil, ErrorInvalid{}},
// invalid: redirect *requires* the location parameter
{"redirect", nil, nil, ErrorInvalid{}},
@@ -163,6 +167,11 @@ var (
{"redirect", Parameters{"location": "/bar"}, nil, nil},
{"redirect", Parameters{"location": "[::1]:12345/ foo\n\x01\xff/"}, nil, nil},
{"redirect", Parameters{"location": "/../../////././.."}, nil, nil},
+
+ {"ok", Parameters{"select": "\x00:\x00"}, nil, nil},
+ {"ok", Parameters{"select": "foo:bar:baz"}, nil, nil},
+ {"ok", Parameters{"select": "byte:5-"}, nil, nil},
+ {"ok", Parameters{"select": "cnm:#/foo/bar"}, nil, nil},
}
)
@@ -308,6 +317,20 @@ func TestResponseGetSet(t *testing.T) {
case "reason":
e(k, v, tst.v, resp.SetReason(v))
c(k, v, tst.v, resp.Reason())
+ case "select":
+ if tst.v != nil {
+ continue
+ }
+ sel := ""
+ ss := strings.SplitN(v, ":", 2)
+ if len(ss) == 2 {
+ resp.SetSelect(ss[0], ss[1])
+ a, b := resp.Select()
+ if a != "" {
+ sel = a + ":" + b
+ }
+ }
+ c(k, v, tst.v, sel)
default:
resp.SetParam(k, v)
c(k, v, tst.v, resp.Param(k))
diff --git a/server.go b/server.go
index 4320f41..6d23ddd 100644
--- a/server.go
+++ b/server.go
@@ -35,14 +35,14 @@ type Server struct {
Handler Handler
// Validate enables request parameter value validation; invalid requests
- // are responded with errors.
+ // are responded to with errors.
Validate bool
sock net.Conn
}
-// NewServer creates a new Server with default access and errors logs and sets
-// the listen address to "localhost".
+// NewServer creates a new Server with default access and errors loggers,
+// validation enabled and the listen address set to "localhost".
func NewServer() *Server {
return &Server{
AccessLogger: log.New(os.Stdout, "", 0),