diff options
| -rw-r--r-- | cnp.go | 2 | ||||
| -rw-r--r-- | common.go | 24 | ||||
| -rw-r--r-- | header.go | 44 | ||||
| -rw-r--r-- | message.go | 15 | ||||
| -rw-r--r-- | request.go | 30 | ||||
| -rw-r--r-- | request_test.go | 24 | ||||
| -rw-r--r-- | response.go | 37 | ||||
| -rw-r--r-- | response_test.go | 23 | ||||
| -rw-r--r-- | server.go | 6 | 
9 files changed, 157 insertions, 48 deletions
| @@ -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  ) @@ -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 +} @@ -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++  	} @@ -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) @@ -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)) @@ -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), |