From 9ca364d8753a5f2c7529c5b3dd7178bfd51effc6 Mon Sep 17 00:00:00 2001 From: clsr Date: Fri, 25 Aug 2017 17:04:42 +0200 Subject: Add support for the draft/cnp-select "select" request/response parameter --- cnp.go | 2 +- common.go | 24 ++++++++++++++++++++++++ header.go | 44 ++++++++++++++++++++++---------------------- message.go | 15 ++++++++------- request.go | 30 +++++++++++++++++++++++++----- request_test.go | 24 +++++++++++++++++++++++- response.go | 37 ++++++++++++++++++++++++++++--------- response_test.go | 23 +++++++++++++++++++++++ server.go | 6 +++--- 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), -- cgit