summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorclsr <clsr@clsr.net>2017-08-18 13:46:10 +0200
committerclsr <clsr@clsr.net>2017-08-18 13:46:10 +0200
commitda77deba78c8a7447b4a38324d2422a5df293b26 (patch)
treebb94b8e18eaf27a86e9fc21c4342d9e0a605baf7
downloadcnp-go-da77deba78c8a7447b4a38324d2422a5df293b26.tar.gz
cnp-go-da77deba78c8a7447b4a38324d2422a5df293b26.zip
Initial commitv0.1.0
-rw-r--r--client.go39
-rw-r--r--cnp.go16
-rw-r--r--common.go141
-rw-r--r--common_test.go76
-rw-r--r--error.go205
-rw-r--r--header.go365
-rw-r--r--header_test.go200
-rw-r--r--message.go176
-rw-r--r--message_test.go204
-rw-r--r--request.go260
-rw-r--r--request_test.go341
-rw-r--r--response.go249
-rw-r--r--response_test.go317
-rw-r--r--server.go257
14 files changed, 2846 insertions, 0 deletions
diff --git a/client.go b/client.go
new file mode 100644
index 0000000..b208637
--- /dev/null
+++ b/client.go
@@ -0,0 +1,39 @@
+package cnp
+
+import (
+ "net"
+ "strconv"
+ "strings"
+)
+
+// TODO: make more modular and extensible like net/http
+
+// Send sends a CNP request to a server and returns the response.
+//
+// The TCP connection is made using net.Dial.
+func Send(r *Request) (*Response, error) {
+ host := r.Host()
+ if strings.LastIndexByte(host, ':') <= strings.LastIndexByte(host, ']') { // missing/default port
+ host = net.JoinHostPort(host, strconv.Itoa(DefaultPort))
+ }
+ conn, err := net.Dial("tcp", host)
+ if err != nil {
+ return nil, err
+ }
+ if err = r.Write(conn); err != nil {
+ return nil, err
+ }
+ if err = r.Close(); err != nil {
+ return nil, err
+ }
+ return ParseResponse(conn)
+}
+
+// Get sends a body-less request to a given URL.
+func Get(url string) (*Response, error) {
+ req, err := NewRequestURL(url, nil)
+ if err != nil {
+ return nil, err
+ }
+ return Send(req)
+}
diff --git a/cnp.go b/cnp.go
new file mode 100644
index 0000000..9ba8444
--- /dev/null
+++ b/cnp.go
@@ -0,0 +1,16 @@
+// Package cnp provides CNP client and server implementations.
+package cnp // import "contnet.org/lib/cnp-go"
+
+const (
+ // DefaultPort represents the default port for the cnp:// schema.
+ DefaultPort = 25454
+
+ // MaxHeaderLength is the maximum byte size of the header.
+ MaxHeaderLength = 1 * 1024 * 1024
+
+ // 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 = 3
+)
diff --git a/common.go b/common.go
new file mode 100644
index 0000000..e6173b0
--- /dev/null
+++ b/common.go
@@ -0,0 +1,141 @@
+package cnp
+
+import (
+ "bufio"
+ "bytes"
+ "io"
+ "mime"
+ "strconv"
+ "strings"
+ "time"
+)
+
+func readLimitedLine(br *bufio.Reader, length int) ([]byte, error) {
+ var buf bytes.Buffer
+
+ for {
+ data, err := br.ReadSlice('\n')
+ if len(data) > 0 {
+ buf.Write(data)
+ }
+ if buf.Len() > length {
+ return nil, ErrorTooLarge{"header exceeds maximum permitted size"}
+ }
+ if err == nil || err == io.EOF {
+ return buf.Bytes(), nil
+ }
+ if err != bufio.ErrBufferFull {
+ return nil, ErrorSyntax{"invalid header: missing line feed"}
+ }
+ }
+}
+
+func getInt(m *Message, param string) (int64, error) {
+ p := m.Param(param)
+ if p == "" {
+ return 0, nil
+ }
+ n, err := strconv.ParseUint(p, 10, 63)
+ if err != nil || (n != 0 && p[0] == '0') || (n == 0 && len(p) != 1) {
+ return int64(n), ErrorInvalid{"invalid parameter: " + param + " is not a valid integer"}
+ }
+ return int64(n), nil
+}
+
+func setInt(m *Message, param string, n int64) {
+ if n < 0 {
+ n = 0
+ }
+ m.SetParam(param, strconv.FormatInt(n, 10))
+}
+
+func getFilename(m *Message, param string) (string, error) {
+ name := m.Param(param)
+ if strings.ContainsAny(name, "/\x00") {
+ return name, ErrorInvalid{"invalid parameter: " + param + " contains invalid characters"}
+ }
+ return name, nil
+}
+
+func setFilename(m *Message, param, name string) error {
+ if strings.ContainsAny(name, "/\x00") {
+ return ErrorInvalid{"invalid parameter: " + param + " contains invalid characters"}
+ }
+ m.SetParam(param, name)
+ return nil
+}
+
+func getType(m *Message, param string) (typ string, err error) {
+ t := m.Param(param)
+ if t != "" {
+ var params map[string]string
+ typ, params, err = mime.ParseMediaType(t)
+ if err != nil || !validMimeType(t) || len(params) > 0 { // may not contain params
+ err = ErrorInvalid{"invalid parameter: " + param + " is not a valid mime type"}
+ }
+ }
+ if typ == "" {
+ typ = "application/octet-stream"
+ }
+ return
+}
+
+func setType(m *Message, param, typ string) error {
+ if typ == "" {
+ m.SetParam(param, "")
+ return nil
+ }
+
+ /*ss := strings.Split(typ, "/")
+ if len(ss) != 2 || len(ss[0]) == 0 || strings.ContainsAny(typ, "\x00 \n\t\r;,") {
+ return ErrorInvalid{…}
+ }*/
+
+ t := mime.FormatMediaType(typ, nil)
+ if t == "" || !validMimeType(typ) {
+ return ErrorInvalid{"invalid parameter: " + param + " is not a valid mime type"}
+ }
+ m.SetParam(param, t)
+
+ return nil
+}
+
+func validMimeType(typ string) bool {
+ ss := strings.Split(typ, "/")
+ if len(ss) != 2 || ss[0] == "" || ss[1] == "" {
+ return false
+ }
+ for _, r := range typ {
+ /*switch r {
+ case '(', ')', '<', '>', '@', ',', ';', ':', '\\', '"', '[', ']', '?', '=':
+ // tspecials except /
+ return false
+ }*/ // handled by mime.ParseMediaType
+ if r <= ' ' || r >= '\x7f' {
+ // control codes, whitespace, null
+ return false
+ }
+ }
+ return true
+}
+
+func getTime(m *Message, param string) (time.Time, error) {
+ t := m.Param(param)
+ var z time.Time
+ if t == "" {
+ return z, nil
+ }
+ ts, err := time.Parse(time.RFC3339, t)
+ if err != nil || !strings.HasSuffix(t, "Z") {
+ return z, ErrorInvalid{"invalid parameter: " + param + " is not a valid RFC3339 timestamp"}
+ }
+ return ts, nil
+}
+
+func setTime(m *Message, param string, t time.Time) {
+ if t.IsZero() {
+ m.SetParam(param, "")
+ } else {
+ m.SetParam(param, t.UTC().Format(time.RFC3339))
+ }
+}
diff --git a/common_test.go b/common_test.go
new file mode 100644
index 0000000..8e8c673
--- /dev/null
+++ b/common_test.go
@@ -0,0 +1,76 @@
+package cnp
+
+import (
+ "bytes"
+ "io"
+ "io/ioutil"
+ "reflect"
+ "strings"
+)
+
+func headerEqual(a, b Header) bool {
+ return a.Intent == b.Intent && a.VersionMajor == b.VersionMajor && a.VersionMinor == b.VersionMinor && paramEqual(a.Parameters, b.Parameters)
+}
+
+func paramEqual(a, b Parameters) bool {
+ if len(a) != len(b) {
+ return false
+ }
+ for k := range a {
+ if a[k] != b[k] {
+ return false
+ }
+ }
+ return true
+}
+
+func errorEqual(a, b error) bool {
+ if a == nil && b == nil {
+ return true
+ }
+ return reflect.TypeOf(a) == reflect.TypeOf(b)
+}
+
+func msgEqual(a, b *Message) bool {
+ return headerEqual(a.Header, b.Header) && bodyEqual(a.Body, b.Body)
+}
+
+func bodyEqual(a, b io.Reader) bool {
+ if a == nil && b == nil {
+ return true
+ }
+ if a == nil {
+ a = strings.NewReader("")
+ }
+ if b == nil {
+ b = strings.NewReader("")
+ }
+ ba, err := ioutil.ReadAll(a)
+ if err != nil {
+ panic(err)
+ }
+ if s, ok := a.(io.Seeker); ok {
+ _, _ = s.Seek(0, io.SeekStart)
+ }
+ bb, err := ioutil.ReadAll(b)
+ if err != nil {
+ panic(err)
+ }
+ if s, ok := b.(io.Seeker); ok {
+ _, _ = s.Seek(0, io.SeekStart)
+ }
+ return bytes.Equal(ba, bb)
+}
+
+type testStringReader struct {
+ s string
+}
+
+func (r *testStringReader) Read(b []byte) (n int, err error) {
+ if r.s == "" {
+ return 0, io.EOF
+ }
+ n = copy(b, r.s)
+ r.s = r.s[n:]
+ return
+}
diff --git a/error.go b/error.go
new file mode 100644
index 0000000..b6270b9
--- /dev/null
+++ b/error.go
@@ -0,0 +1,205 @@
+package cnp
+
+// Error represents an error as used in a CNP error response.
+type Error interface {
+ // CNPError returns the value of the "error" parameter in a CNP error
+ // response.
+ CNPError() string
+
+ Error() string
+}
+
+const (
+ // ReasonSyntax represents the "syntax" reason parameter value.
+ ReasonSyntax = "syntax"
+ // ReasonVersion represents the "version" reason parameter value.
+ ReasonVersion = "version"
+ // ReasonInvalid represents the "invalid" reason parameter value.
+ ReasonInvalid = "invalid"
+ // ReasonNotSupported represents the "not_supported" reason parameter value.
+ ReasonNotSupported = "not_supported"
+ // ReasonTooLarge represents the "too_large" reason parameter value.
+ ReasonTooLarge = "too_large"
+ // ReasonNotFound represents the "not_found" reason parameter value.
+ ReasonNotFound = "not_found"
+ // ReasonDenied represents the "denied" reason parameter value.
+ ReasonDenied = "denied"
+ // ReasonRejected represents the "rejected" reason parameter value.
+ ReasonRejected = "rejected"
+ // ReasonServerError represents the "server_error" reason parameter value.
+ ReasonServerError = "server_error"
+)
+
+// NewError returns a new Error based on a reason parameter value.
+//
+// If the reason is blank, nil is returned.
+// If the reason is unknown, ErrorServerError is returned.
+func NewError(reason string) Error {
+ switch reason {
+ case ReasonSyntax:
+ return ErrorSyntax{"Invalid CNP Message Syntax"}
+ case ReasonVersion:
+ return ErrorVersion{"Unsupported CNP Protocol Version"}
+ case ReasonInvalid:
+ return ErrorInvalid{"Invalid CNP Message"}
+ case ReasonNotSupported:
+ return ErrorNotSupported{"CNP Feature Not Supported"}
+ case ReasonTooLarge:
+ return ErrorTooLarge{"CNP Message Too Large"}
+ case ReasonNotFound:
+ return ErrorNotFound{"Not Found"}
+ case ReasonDenied:
+ return ErrorDenied{"Denied"}
+ case ReasonRejected:
+ return ErrorRejected{"Rejected"}
+ case "":
+ return nil
+ default:
+ fallthrough
+ case ReasonServerError:
+ return ErrorServerError{"Internal Server Error"}
+ }
+}
+
+// ErrorSyntax represents the CNP "syntax" error reason.
+type ErrorSyntax struct {
+ Reason string
+}
+
+// CNPError on ErrorSyntax returns the error parameter value "syntax".
+func (e ErrorSyntax) CNPError() string {
+ return ReasonSyntax
+}
+
+func (e ErrorSyntax) Error() string {
+ return "CNP syntax error: " + e.Reason
+}
+
+// ErrorVersion represents the CNP "version" error reason.
+type ErrorVersion struct {
+ Reason string
+}
+
+// CNPError on ErrorVersion returns the error parameter value "version".
+func (e ErrorVersion) CNPError() string {
+ return ReasonVersion
+}
+
+func (e ErrorVersion) Error() string {
+ return "Unsupported CNP version: " + e.Reason
+}
+
+// ErrorInvalid represents the CNP "invalid" error reason.
+type ErrorInvalid struct {
+ Reason string
+}
+
+// CNPError on ErrorInvalid returns the error parameter value "invalid".
+func (e ErrorInvalid) CNPError() string {
+ return ReasonInvalid
+}
+
+func (e ErrorInvalid) Error() string {
+ return "Invalid CNP message: " + e.Reason
+}
+
+// ErrorNotSupported represents the CNP "not_supported" error reason.
+type ErrorNotSupported struct {
+ Reason string
+}
+
+// CNPError on ErrorNotSupported returns the error parameter value
+// "not_supported".
+func (e ErrorNotSupported) CNPError() string {
+ return ReasonNotSupported
+}
+
+func (e ErrorNotSupported) Error() string {
+ return "Requested CNP feature is not supported: " + e.Reason
+}
+
+// ErrorTooLarge represents the CNP "too_large" error reason.
+type ErrorTooLarge struct {
+ Reason string
+}
+
+// CNPError on ErrorTooLarge returns the error parameter value "too_large".
+func (e ErrorTooLarge) CNPError() string {
+ return ReasonTooLarge
+}
+
+func (e ErrorTooLarge) Error() string {
+ return "CNP message is too large: " + e.Reason
+}
+
+// ErrorNotFound represents the CNP "not_found" error reason.
+type ErrorNotFound struct {
+ Reason string
+}
+
+// CNPError on ErrorNotFound returns the error parameter value "not_found".
+func (e ErrorNotFound) CNPError() string {
+ return ReasonNotFound
+}
+
+func (e ErrorNotFound) Error() string {
+ return "Requested path was not found: " + e.Reason
+}
+
+// ErrorDenied represents the CNP "denied" error reason.
+type ErrorDenied struct {
+ Reason string
+}
+
+// CNPError on ErrorDenied returns the error parameter value "denied".
+func (e ErrorDenied) CNPError() string {
+ return ReasonDenied
+}
+
+func (e ErrorDenied) Error() string {
+ return "Server denied access: " + e.Reason
+}
+
+// ErrorRejected represents the CNP "rejected" error reason.
+type ErrorRejected struct {
+ Reason string
+}
+
+// CNPError on ErrorRejected returns the error parameter value "rejected".
+func (e ErrorRejected) CNPError() string {
+ return ReasonRejected
+}
+
+func (e ErrorRejected) Error() string {
+ return "Server rejected request: " + e.Reason
+}
+
+// ErrorServerError represents the CNP "server_error" error reason.
+type ErrorServerError struct {
+ Reason string
+}
+
+// CNPError on ErrorServerError returns the error parameter value
+// "server_error".
+func (e ErrorServerError) CNPError() string {
+ return ReasonServerError
+}
+
+func (e ErrorServerError) Error() string {
+ return "Internal server error: " + e.Reason
+}
+
+// ErrorURL is a non-CNPError that represents an invalid CNP URL.
+type ErrorURL struct {
+ // Err represents the error reason.
+ Err error
+ // URL is the URL that triggered the error.
+ URL string
+}
+
+func (e ErrorURL) Error() string {
+ if e.Err == nil {
+ return "CNP URL error"
+ }
+ return e.Err.Error()
+}
diff --git a/header.go b/header.go
new file mode 100644
index 0000000..e185dca
--- /dev/null
+++ b/header.go
@@ -0,0 +1,365 @@
+package cnp
+
+import (
+ "bufio"
+ "bytes"
+ "io"
+ "sort"
+ "strconv"
+)
+
+const (
+ // IntentOK represents the "ok" response intent.
+ IntentOK = "ok"
+ // IntentNotModified represents the "not_modified" response intent.
+ IntentNotModified = "not_modified"
+ // IntentError represents the "error" response intent.
+ IntentError = "error"
+ // IntentRedirect represents the "redirect" response intent.
+ IntentRedirect = "redirect"
+)
+
+// Header represents a CNP message header
+type Header struct {
+ // VersionMajor is the major CNP version given in the header.
+ VersionMajor int
+ // VersionMinor is the minor CNP version given in the header.
+ VersionMinor int
+ // Intent is the intent string of the message.
+ Intent string
+ // Parameters is a decoded map of the message parameters.
+ Parameters Parameters
+}
+
+// NewHeader creates a new CNP header from an intent and optional parameter
+// map.
+func NewHeader(intent string, params Parameters) Header {
+ if params == nil {
+ params = make(Parameters)
+ }
+ return Header{
+ VersionMajor: VersionMajor,
+ VersionMinor: VersionMinor,
+ Intent: intent,
+ Parameters: params,
+ }
+}
+
+// ParseHeader parses a CNP header from a bytestring.
+// The line parameter must be a single line that ends with a line feed.
+func ParseHeader(line []byte) (h Header, err error) {
+ if bytes.IndexByte(line, '\x00') >= 0 {
+ err = ErrorSyntax{"invalid header: NUL byte"}
+ return
+ }
+
+ if !bytes.HasPrefix(line, []byte("cnp/")) {
+ err = ErrorSyntax{"invalid header: no version string"}
+ return
+ }
+ line = line[len("cnp/"):]
+
+ var o int
+
+ h.VersionMajor, o, err = parseHeaderNumber(line)
+ if err != nil {
+ return
+ }
+ line = line[o:]
+
+ if len(line) == 0 || line[0] != '.' {
+ err = ErrorSyntax{"invalid header: invalid version number tuple"}
+ return
+ }
+ line = line[1:]
+ h.VersionMinor, o, err = parseHeaderNumber(line)
+ if err != nil {
+ return
+ }
+ line = line[o:]
+
+ if len(line) == 0 || line[0] != ' ' {
+ err = ErrorSyntax{"invalid header: expected a space"}
+ return
+ }
+ line = line[1:]
+
+ h.Intent, o, err = parseHeaderIntent(line)
+ if err != nil {
+ return
+ }
+ line = line[o:]
+
+ h.Parameters, o, err = parseHeaderParameters(line)
+ if err != nil {
+ return
+ }
+ line = line[o:]
+
+ if len(line) != 1 || line[0] != '\n' {
+ err = ErrorSyntax{"invalid header: expected line feed and end"}
+ return
+ }
+
+ return
+}
+
+func parseHeaderNumber(line []byte) (n int, length int, err error) {
+ i := 0
+ for ; i < len(line); i++ {
+ b := line[i]
+ if '0' <= b && b <= '9' {
+ } else {
+ break
+ }
+ }
+ n, err = strconv.Atoi(string(line[:i]))
+ if err != nil || (i > 1 && line[0] == '0') {
+ err = ErrorSyntax{"invalid header: invalid version number format"}
+ return
+ }
+ length = i
+ return
+}
+
+func parseHeaderIntent(line []byte) (intent string, length int, err error) {
+ p := bytes.IndexAny(line, " \n=\x00")
+ if p < 0 {
+ p = len(line) - 1
+ }
+ if p == 0 {
+ err = ErrorSyntax{"invalid header: empty intent"}
+ return
+ }
+ intent, err = Unescape(line[:p])
+ length = p
+ return
+}
+
+func parseHeaderParameters(line []byte) (params Parameters, length int, err error) {
+ params = make(Parameters)
+
+ for len(line) > 1 {
+ if line[0] == '\n' {
+ break
+ } else if line[0] != ' ' {
+ err = ErrorSyntax{"invalid header: expected a space"}
+ return
+ }
+ line = line[1:]
+ length++
+
+ var k, v string
+ var o int
+ k, v, o, err = parseHeaderParameter(line)
+ if err != nil {
+ return
+ }
+ if _, ok := params[k]; ok {
+ err = ErrorSyntax{"invalid header: duplicate parameter"}
+ return
+ }
+ params[k] = v
+ line = line[o:]
+ length += o
+ }
+
+ return
+}
+
+func parseHeaderParameter(line []byte) (key, value string, length int, err error) {
+ var o int
+ key, o, err = parseHeaderParamString(line)
+ if err != nil {
+ return
+ }
+ line = line[o:]
+ length += o
+
+ if len(line) == 0 || line[0] != '=' {
+ err = ErrorSyntax{"invalid header: expected an equals sign"}
+ return
+ }
+ line = line[1:]
+ length++
+
+ value, o, err = parseHeaderParamString(line)
+ if err != nil {
+ return
+ }
+ length += o
+
+ return
+}
+
+func parseHeaderParamString(line []byte) (s string, length int, err error) {
+ for ; length < len(line); length++ {
+ b := line[length]
+ if b == '\n' || b == ' ' || b == '=' {
+ break
+ }
+ }
+ s, err = Unescape(line[:length])
+ return
+}
+
+// Version returns the message's CNP version as a "cnp/X.Y" string.
+func (h Header) Version() string {
+ return "cnp/" + strconv.Itoa(h.VersionMajor) + "." + strconv.Itoa(h.VersionMinor)
+}
+
+// Write writes the CNP message header line in the wire format.
+// The written line ends with a line feed.
+func (h Header) Write(w io.Writer) (err error) {
+ bw := bufio.NewWriter(w)
+ _, err = bw.WriteString(h.Version())
+ if err != nil {
+ return
+ }
+ err = bw.WriteByte(' ')
+ if err != nil {
+ return
+ }
+ _, err = bw.Write(Escape(h.Intent))
+ if err != nil {
+ return
+ }
+ err = h.Parameters.Write(bw)
+ if err != nil {
+ return
+ }
+ err = bw.WriteByte('\n')
+ if err != nil {
+ return
+ }
+ return bw.Flush()
+}
+
+func (h Header) String() string {
+ var buf bytes.Buffer
+ _ = h.Write(&buf)
+ return buf.String()
+}
+
+// Parameters represents CNP message parameter key=value pairs.
+type Parameters map[string]string
+
+// Write writes the parameters encoded for inclusion in the wire format.
+// Includes a leading space.
+func (p Parameters) Write(w io.Writer) (err error) {
+ bw := bufio.NewWriter(w)
+ keys := []string{}
+ for k := range p {
+ keys = append(keys, k)
+ }
+ sort.Strings(keys)
+ for _, k := range keys {
+ err = bw.WriteByte(' ')
+ if err != nil {
+ return
+ }
+ _, err = bw.Write(Escape(k))
+ if err != nil {
+ return
+ }
+ err = bw.WriteByte('=')
+ if err != nil {
+ return
+ }
+ _, err = bw.Write(Escape(p[k]))
+ if err != nil {
+ return
+ }
+ }
+ return
+}
+
+// Escape CNP-escapes the bytestring s.
+func Escape(s string) []byte {
+ el := escapeLength(s)
+ if el == len(s) {
+ return []byte(s)
+ }
+ bs := make([]byte, el)
+ bi := 0
+ for i := 0; i < len(s); i++ {
+ switch s[i] {
+ case '\x00':
+ bs[bi] = '\\'
+ bs[bi+1] = '0'
+ bi += 2
+
+ case '\n':
+ bs[bi] = '\\'
+ bs[bi+1] = 'n'
+ bi += 2
+
+ case ' ':
+ bs[bi] = '\\'
+ bs[bi+1] = '_'
+ bi += 2
+
+ case '=':
+ bs[bi] = '\\'
+ bs[bi+1] = '-'
+ bi += 2
+
+ case '\\':
+ bs[bi] = '\\'
+ bs[bi+1] = '\\'
+ bi += 2
+
+ default:
+ bs[bi] = s[i]
+ bi++
+ }
+ }
+ return bs
+}
+
+func escapeLength(s string) (l int) {
+ for i := 0; i < len(s); i++ {
+ switch s[i] {
+ case '\x00', '\n', ' ', '=', '\\':
+ l += 2
+ default:
+ l++
+ }
+ }
+ return
+}
+
+// Unescape unescapes the bs from wire format into a bytestring.
+func Unescape(bs []byte) (string, error) {
+ buf := make([]byte, len(bs))
+ bi := 0
+
+ for i := 0; i < len(bs); i++ {
+ switch bs[i] {
+ case '\\':
+ i++
+ if i >= len(bs) {
+ return string(buf[:bi]), ErrorSyntax{"invalid escape sequence: unexpected end of string"}
+ }
+ switch bs[i] {
+ case '0':
+ buf[bi] = '\x00'
+ case 'n':
+ buf[bi] = '\n'
+ case '_':
+ buf[bi] = ' '
+ case '-':
+ buf[bi] = '='
+ case '\\':
+ buf[bi] = '\\'
+ default:
+ return string(buf[:bi]), ErrorSyntax{"invalid escape sequence: undefined sequence"}
+ }
+ default:
+ buf[bi] = bs[i]
+ }
+ bi++
+ }
+
+ return string(buf[:bi]), nil
+}
diff --git a/header_test.go b/header_test.go
new file mode 100644
index 0000000..b035078
--- /dev/null
+++ b/header_test.go
@@ -0,0 +1,200 @@
+package cnp
+
+import "testing"
+
+var escapes = map[string]string{
+ "": ``,
+ "ContNet": `ContNet`,
+ " ": `\_`,
+ "=": `\-`,
+ "\n": `\n`,
+ "\x00": `\0`,
+ "\\": `\\`,
+ "a\nb c=d\x00e\\f": `a\nb\_c\-d\0e\\f`,
+ "\n\n\n": `\n\n\n`,
+ "===": `\-\-\-`,
+ " ": `\_\_\_`,
+ "\x00\x00\x00": `\0\0\0`,
+ "\\\\\\": `\\\\\\`,
+ " =\n\x00\\": `\_\-\n\0\\`,
+ "\b5Ὂg̀9! ℃ᾭG": "\b5Ὂg̀9!\\_℃ᾭG",
+ "\xff\x00\xee\xaa\xee": "\xff\\0\xee\xaa\xee",
+ "\x00\x10\x20\x30\x40": "\\0\x10\\_\x30\x40",
+ "\x10\x50\x90\xe0": "\x10\x50\x90\xe0",
+ "Hello, 世界": `Hello,\_世界`,
+ "\xed\x9f\xbf": "\xed\x9f\xbf",
+ "\xee\x80\x80": "\xee\x80\x80",
+ "\xef\xbf\xbd": "\xef\xbf\xbd",
+ "\x80\x80\x80\x80": "\x80\x80\x80\x80",
+}
+
+func TestEscape(t *testing.T) {
+ for k, v := range escapes {
+ e := string(Escape(k))
+ if e != v {
+ t.Errorf("Escape(%q) -> %q, expected %q", k, e, v)
+ }
+ }
+ for i := 0; i <= 255; i++ {
+ switch i {
+ case '\x00', ' ', '=', '\n', '\\':
+ continue
+ default:
+ s := string([]byte{byte(i)})
+ b := Escape(s)
+ if s != string(b) {
+ t.Errorf("Escape(%q) -> %q, expected %q", s, b, s)
+ }
+ }
+ }
+}
+
+func TestUnEscape(t *testing.T) {
+ for k, v := range escapes {
+ u, err := Unescape([]byte(v))
+ if err != nil {
+ t.Errorf("Unescape(%q): error: %s", v, err)
+ } else if u != k {
+ t.Errorf("Unescape(%q) -> %q, expected %q", v, u, k)
+ }
+ }
+ for i := 0; i <= 255; i++ {
+ switch i {
+ case '\x00', ' ', '=', '\n', '\\':
+ continue
+ default:
+ b := []byte{byte(i)}
+ s, err := Unescape(b)
+ if err != nil {
+ t.Errorf("Unescape(%q): error: %s", b, err)
+ } else if string(b) != s {
+ t.Errorf("Escape(%q) -> %q, expected %q", b, s, b)
+ }
+ }
+ }
+}
+
+var headers = map[string]struct {
+ h Header
+ e error
+}{
+ // invalid version
+ "cnp/0.3ok\n": {e: ErrorSyntax{}},
+ "cwp/0.3 ok\n": {e: ErrorSyntax{}},
+ "cnp/0.03 ok\n": {e: ErrorSyntax{}},
+ "cnp/00.3 ok\n": {e: ErrorSyntax{}},
+ "cnp/0..3 ok\n": {e: ErrorSyntax{}},
+ "cnp/.3 ok\n": {e: ErrorSyntax{}},
+ "cnp/0. ok\n": {e: ErrorSyntax{}},
+ "cnp/. ok\n": {e: ErrorSyntax{}},
+ "cnp/0,3 ok\n": {e: ErrorSyntax{}},
+ "/0.3 ok\n": {e: ErrorSyntax{}},
+ "0.3 ok\n": {e: ErrorSyntax{}},
+ "cnp/ ok\n": {e: ErrorSyntax{}},
+ "cnp ok\n": {e: ErrorSyntax{}},
+ "cnp.0.3 ok\n": {e: ErrorSyntax{}},
+ "cnp/03 ok\n": {e: ErrorSyntax{}},
+ "cnp/3 ok\n": {e: ErrorSyntax{}},
+ "cnp/0 ok\n": {e: ErrorSyntax{}},
+ "cnp/0 3 ok\n": {e: ErrorSyntax{}},
+ "cnp/0/3 ok\n": {e: ErrorSyntax{}},
+
+ // missing/invalid intent
+ "cnp/0.3\n": {e: ErrorSyntax{}},
+ "cnp/0.3 \n": {e: ErrorSyntax{}},
+ "cnp/0.3 o\x00k\n": {e: ErrorSyntax{}},
+ "cnp/0.3 foo=bar\n": {e: ErrorSyntax{}},
+
+ // missing/invalid line end
+ "cnp/0.3 ok \n": {e: ErrorSyntax{}},
+ "cnp/0.3 ok\n\n": {e: ErrorSyntax{}},
+ "cnp/0.3 ok": {e: ErrorSyntax{}},
+ "cnp/0.3 ok ": {e: ErrorSyntax{}},
+ "cnp/0.3 ok foo=bar \n": {e: ErrorSyntax{}},
+ "cnp/0.3 ok foo=bar": {e: ErrorSyntax{}},
+ "cnp/0.3 ok = =\n": {e: ErrorSyntax{}},
+
+ // spaces
+ "cnp/0.3 ok foo=bar\n": {e: ErrorSyntax{}},
+ "cnp/0.3 ok\n": {e: ErrorSyntax{}},
+ "cnp/0.3\tok\n": {e: ErrorSyntax{}},
+
+ // invalid params
+ "cnp/0.3 ok foo==bar\n": {e: ErrorSyntax{}},
+ "cnp/0.3 ok foo=bar=baz\n": {e: ErrorSyntax{}},
+ "cnp/0.3 ok foo=bar baz=quux \n": {e: ErrorSyntax{}},
+ "cnp/0.3 ok foo\\-bar\n": {e: ErrorSyntax{}},
+
+ // invalid escape sequences
+ "cnp/0.3 o\\k\n": {e: ErrorSyntax{}},
+ "cnp/0.3 ok qwe=\\\n": {e: ErrorSyntax{}},
+ "cnp/0.3 ok fo\\o=bar\n": {e: ErrorSyntax{}},
+
+ // valid
+ "cnp/0.0 ok\n": {h: Header{0, 0, "ok", nil}},
+ "cnp/0.3 ok\n": {h: Header{0, 3, "ok", nil}},
+ "cnp/1.0 ok\n": {h: Header{1, 0, "ok", nil}},
+ "cnp/123456.987654 ok\n": {h: Header{123456, 987654, "ok", nil}},
+ "cnp/0.1 ok\n": {h: Header{0, 1, "ok", nil}},
+ "cnp/0.3 ok\r\n": {h: Header{0, 3, "ok\r", nil}},
+ "cnp/0.3 foo\\nbar\n": {h: Header{0, 3, "foo\nbar", nil}},
+ "cnp/0.3 \\-\\_\\n\\0\\\\\n": {h: Header{0, 3, "= \n\x00\\", nil}},
+
+ // valid with params
+ "cnp/0.3 ok type=text/plain\n": {h: Header{0, 3, "ok", Parameters{"type": "text/plain"}}},
+ "cnp/0.3 ok baz=quux foo=bar qwe=asd\n": {h: Header{0, 3, "ok", Parameters{"foo": "bar", "baz": "quux", "qwe": "asd"}}},
+ "cnp/0.3 ok = \\_=\\0 \\-=\\n\r\n": {h: Header{0, 3, "ok", Parameters{"": "", "=": "\n\r", " ": "\x00"}}},
+}
+
+func TestHeaderParse(t *testing.T) {
+ for raw, tst := range headers {
+ hdr, err := ParseHeader([]byte(raw))
+ if err != nil || tst.e != nil {
+ if !errorEqual(err, tst.e) {
+ t.Errorf("ParseHeader(%q): expected error %+v, got %+v (%v)", raw, tst.e, err, err)
+ }
+ } else if !headerEqual(tst.h, hdr) {
+ t.Errorf("ParseHeader(%q): expected %+v, got %+v", raw, tst.h, hdr)
+ }
+ }
+}
+
+func TestHeaderCompose(t *testing.T) {
+ for raw, tst := range headers {
+ if tst.e != nil {
+ continue
+ }
+ // Parameters.Write currently sorts parameter keys
+ /*if len(tst.h.Parameters) > 1 {
+ continue // can't depend on parameter order
+ }*/
+ str := tst.h.String()
+ if raw != str {
+ t.Errorf("%+v.String(): expected %q, got %q", tst.h, raw, str)
+ }
+ }
+}
+
+func TestNewHeader(t *testing.T) {
+ raw := "cnp/0.3 ok baz=quux foo=bar qwe=asd\n"
+ hdr := Header{0, 3, "ok", Parameters{"foo": "bar", "baz": "quux", "qwe": "asd"}}
+ h := NewHeader("ok", Parameters{"foo": "bar", "baz": "quux", "qwe": "asd"})
+ if !headerEqual(hdr, h) {
+ t.Errorf("%+v: expected %+v", h, hdr)
+ }
+ s := h.String()
+ if raw != s {
+ t.Errorf("%q: expected %q", h, hdr)
+ }
+
+ raw = "cnp/0.3 ok\n"
+ hdr = Header{0, 3, "ok", nil}
+ h = NewHeader("ok", nil)
+ if !headerEqual(hdr, h) {
+ t.Errorf("%+v: expected %+v", h, hdr)
+ }
+ s = h.String()
+ if raw != s {
+ t.Errorf("%q: expected %q", h, hdr)
+ }
+}
diff --git a/message.go b/message.go
new file mode 100644
index 0000000..eb3d2b9
--- /dev/null
+++ b/message.go
@@ -0,0 +1,176 @@
+package cnp
+
+import (
+ "bufio"
+ "bytes"
+ "io"
+ "io/ioutil"
+ "strings"
+)
+
+// Message represents a CNP message.
+type Message struct {
+ Header Header
+ Body io.Reader
+ closer io.Closer
+}
+
+// NewMessage creates a new CNP message.
+//
+// This method also calls Message.TryComputeLength().
+func NewMessage(intent string, body io.Reader) *Message {
+ msg := &Message{
+ Header: NewHeader(intent, nil),
+ Body: body,
+ }
+
+ msg.TryComputeLength()
+
+ return msg
+}
+
+// ParseMessage parses a CNP message.
+//
+// The message's Body field is set to a bufio.Reader wrapping r. If r is a
+// io.Closer, it is also stored separately for usage with Message.Close().
+func ParseMessage(r io.Reader) (*Message, error) {
+ br := bufio.NewReader(r)
+
+ line, err := readLimitedLine(br, MaxHeaderLength)
+ if err != nil {
+ return nil, err
+ }
+
+ h, err := ParseHeader(line)
+ if err != nil {
+ return nil, err
+ }
+
+ msg := &Message{
+ Header: h,
+ Body: br,
+ }
+
+ if rc, ok := r.(io.Closer); ok {
+ msg.closer = rc
+ }
+
+ return msg, nil
+}
+
+// Close attempts to close the message body reader.
+//
+// If the message's Body field is an io.Closer, its Close() method is called.
+// If Body is not an io.Closer and the message was created with ParseMessage
+// provided with an io.Closer, the Close() method on the original reader will
+// be called.
+// Otherwise, this function does nothing.
+func (msg *Message) Close() error {
+ if rc, ok := msg.Body.(io.Closer); ok {
+ return rc.Close()
+ }
+ if msg.closer != nil {
+ return msg.closer.Close()
+ }
+ return nil
+}
+
+// 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.
+func (msg *Message) ComputeLength() error {
+ if !msg.TryComputeLength() {
+ buf, err := ioutil.ReadAll(msg.Body)
+ if len(buf) > 0 {
+ msg.Body = bytes.NewReader(buf)
+ msg.SetLength(int64(len(buf)))
+ }
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// 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.
+func (msg *Message) TryComputeLength() bool {
+ switch v := msg.Body.(type) {
+ case *bytes.Buffer:
+ msg.SetLength(int64(v.Len()))
+ case *bytes.Reader:
+ msg.SetLength(int64(v.Len()))
+ case *strings.Reader:
+ msg.SetLength(int64(v.Len()))
+ case nil:
+ msg.SetLength(0)
+ default:
+ return false
+ }
+ return true
+}
+
+// SetLength sets the length header parameter to n.
+func (msg *Message) SetLength(n int64) {
+ if n == 0 {
+ msg.SetParam("length", "")
+ } else {
+ setInt(msg, "length", n)
+ }
+}
+
+// Length gets the length header parameter (or 0 if it's not set or invalid).
+func (msg *Message) Length() int64 {
+ n, err := getInt(msg, "length")
+ if err != nil {
+ return 0
+ }
+ return n
+}
+
+// Param retrieves a header parameter.
+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.
+func (msg *Message) SetParam(key, value string) {
+ if len(value) == 0 {
+ delete(msg.Header.Parameters, key)
+ } else {
+ msg.Header.Parameters[key] = value
+ }
+}
+
+// Intent retrieves the message header intent.
+func (msg *Message) Intent() string {
+ return msg.Header.Intent
+}
+
+// SetIntent sets the message header intent.
+func (msg *Message) SetIntent(s string) {
+ msg.Header.Intent = s
+}
+
+// Validate validates the message header parameter value format (length).
+func (msg *Message) Validate() error {
+ _, err := getInt(msg, "length")
+ return err
+}
+
+// Write writes the message header and body to w.
+func (msg *Message) Write(w io.Writer) error {
+ if err := msg.Header.Write(w); err != nil {
+ return err
+ }
+ if msg.Body != nil {
+ if _, err := io.Copy(w, msg.Body); err != nil {
+ return err
+ }
+ }
+ return nil
+}
diff --git a/message_test.go b/message_test.go
new file mode 100644
index 0000000..6955fc3
--- /dev/null
+++ b/message_test.go
@@ -0,0 +1,204 @@
+package cnp
+
+import (
+ "bytes"
+ "io"
+ "strconv"
+ "strings"
+ "testing"
+)
+
+var (
+ messageTests = []messageTest{
+ {
+ "qweasd", "text/plain", nil, nil,
+ "cnp/0.3 test1 length=6 type=text/plain\nqweasd",
+ &Message{
+ Header: Header{
+ VersionMajor: 0,
+ VersionMinor: 3,
+ Intent: "test1",
+ Parameters: Parameters{
+ "type": "text/plain",
+ "length": "6",
+ },
+ },
+ Body: strings.NewReader("qweasd"),
+ },
+ },
+
+ {
+ "qweasd", "text", nil, ErrorInvalid{},
+ "cnp/0.3 test2 length=w type=text/plain\nqweasd",
+ &Message{
+ Header: Header{
+ VersionMajor: 0,
+ VersionMinor: 3,
+ Intent: "test2",
+ Parameters: Parameters{
+ "type": "text/plain",
+ "length": "w",
+ },
+ },
+ Body: strings.NewReader("qweasd"),
+ },
+ },
+
+ {
+ "", "text/plain", ErrorSyntax{}, nil,
+ "cnp/0.3 test3 type=text/plain",
+ nil,
+ },
+
+ {
+ "", "text/plain", nil, nil,
+ "cnp/0.3 test4 type=text/plain\n",
+ &Message{
+ Header: Header{
+ VersionMajor: 0,
+ VersionMinor: 3,
+ Intent: "test4",
+ Parameters: Parameters{
+ "type": "text/plain",
+ },
+ },
+ Body: nil,
+ },
+ },
+ }
+)
+
+type messageTest struct {
+ b, t string
+ p, v error
+ s string
+ m *Message
+}
+
+func (tst *messageTest) Reset() {
+ if tst.m != nil && tst.b != "" {
+ tst.m.Body = strings.NewReader(tst.b)
+ }
+}
+
+func TestParse(t *testing.T) {
+ for _, tst := range messageTests {
+ tst.Reset()
+ msg, err := ParseMessage(strings.NewReader(tst.s))
+ if tst.p != nil || err != nil {
+ if !errorEqual(tst.p, err) {
+ t.Errorf("ParseMessage(%q): expected error %+v, got %+v (%v)", tst.s, tst.p, err, err)
+ continue
+ }
+ } else if !msgEqual(msg, tst.m) {
+ t.Errorf("\nexpected: %+v\ngot: %+v", tst.m, msg)
+ } else if l := msg.Length(); tst.v == nil && l != int64(len(tst.b)) {
+ t.Errorf("%+v: expected length %d, got %d", tst.m, len(tst.b), l)
+ } else if err = msg.Validate(); !errorEqual(err, tst.v) {
+ t.Errorf("%+v: expected validation error %+v, got %+v (%s)", msg, tst.v, err, err)
+ }
+ }
+}
+
+func TestNew(t *testing.T) {
+ for i, tst := range messageTests {
+ if tst.v != nil || tst.p != nil {
+ continue // skip invalid messages
+ }
+ tst.Reset()
+ var r io.Reader
+ if tst.b != "" {
+ r = strings.NewReader(tst.b)
+ }
+ msg := NewMessage("test"+strconv.Itoa(i+1), r)
+ msg.SetParam("type", tst.t)
+ if !msgEqual(msg, tst.m) {
+ t.Errorf("\nexpected: %+v\ngot: %+v", tst.m, msg)
+ }
+ }
+}
+
+func TestWrite(t *testing.T) {
+ for _, tst := range messageTests {
+ if tst.p != nil {
+ continue // skip invalid messages
+ }
+ tst.Reset()
+ var buf bytes.Buffer
+ err := tst.m.Write(&buf)
+ if err != nil {
+ t.Errorf("%+v: error writing message body: %s", tst.m, err)
+ }
+ if buf.String() != tst.s {
+ t.Errorf("%+v:\nexpected: %q\ngot: %q", tst.m, tst.s, buf.String())
+ }
+ }
+}
+
+func TestComputeLength(t *testing.T) {
+ s := "qweasd"
+ r := &testStringReader{"qweasd"}
+ msg := NewMessage("ok", r)
+ if _, ok := msg.Header.Parameters["length"]; msg.Length() != 0 || msg.Param("length") != "" || ok {
+ t.Fatalf("%+v: expected no length parameter", msg)
+ }
+ if err := msg.ComputeLength(); err != nil {
+ t.Fatalf("%+v.ComputeLength(): error: %s", msg, err)
+ }
+ if l := msg.Length(); l != int64(len(s)) {
+ t.Fatalf("%+v: invalid length %d, expected %d\n", msg, l, len(s))
+ }
+ if _, ok := msg.Body.(*testStringReader); r.s != "" || ok || r == msg.Body {
+ t.Fatalf("%+v: did not buffer body correctly", msg)
+ }
+ if !bodyEqual(msg.Body, strings.NewReader(s)) {
+ t.Fatalf("%+v: incorrect buffered body", msg)
+ }
+}
+
+func TestParseTooLarge(t *testing.T) {
+ s := "cnp/0.3 ok text="
+ str := s + strings.Repeat(".", MaxHeaderLength-len(s)) + "\n"
+ _, err := ParseMessage(strings.NewReader(str))
+ if _, ok := err.(ErrorTooLarge); !ok {
+ t.Errorf("\nexpected: ErrorTooLarge\ngot: %+v", err)
+ }
+}
+
+type noopCloser struct {
+ r io.Reader
+ closed bool
+}
+
+func (n *noopCloser) Close() error {
+ n.closed = true
+ return nil
+}
+
+func (n *noopCloser) Read(b []byte) (int, error) {
+ return n.r.Read(b)
+}
+
+func TestClose(t *testing.T) {
+ var r io.Reader = strings.NewReader("cnp/0.3 ok\nqweasd")
+ if msg, err := ParseMessage(r); err != nil {
+ t.Errorf("ParseMessage error: %v", err)
+ } else if err = msg.Close(); err != nil {
+ t.Errorf("Error closing message: %v", err)
+ }
+ r = &noopCloser{r: strings.NewReader("cnp/0.3 ok\nqweasd")}
+ if msg, err := ParseMessage(r); err != nil {
+ t.Errorf("ParseMessage error: %v", err)
+ } else if err = msg.Close(); err != nil {
+ t.Errorf("Error closing message: %v", err)
+ } else if !r.(*noopCloser).closed {
+ t.Errorf("Reader was not closed")
+ }
+ r = &noopCloser{r: strings.NewReader("qweasd")}
+ msg := NewMessage("ok", r)
+ if err := msg.Close(); err != nil {
+ t.Errorf("Error closing message: %v", err)
+ } else if !r.(*noopCloser).closed {
+ t.Errorf("Reader was not closed")
+ }
+}
diff --git a/request.go b/request.go
new file mode 100644
index 0000000..8c5a34e
--- /dev/null
+++ b/request.go
@@ -0,0 +1,260 @@
+package cnp
+
+import (
+ "bytes"
+ "errors"
+ "io"
+ "net/url"
+ "path"
+ "strconv"
+ "strings"
+ "time"
+)
+
+// Request represents a CNP request message.
+type Request struct {
+ Message
+}
+
+// NewRequest creates a new Request from a host, path and optional body data.
+func NewRequest(host, pth string, body []byte) (*Request, error) {
+ var r io.Reader
+ if body != nil {
+ r = bytes.NewReader(body)
+ }
+ req := &Request{*NewMessage("/", r)}
+ if err := req.SetHostPath(host, pth); err != nil {
+ return nil, err
+ }
+ return req, nil
+}
+
+// NewRequestURL creates a new Request from a URL and body data.
+func NewRequestURL(urlStr string, body []byte) (*Request, error) {
+ // XXX: handle //example.com/path URLs
+ if strings.HasPrefix(urlStr, "//") {
+ urlStr = "cnp:" + urlStr
+ }
+
+ u, err := url.ParseRequestURI(urlStr)
+ if err != nil {
+ return nil, ErrorURL{err, urlStr}
+ }
+
+ if u.Scheme != "cnp" && u.Scheme != "" {
+ return nil, ErrorURL{errors.New("NewRequestURL: URL is not a cnp:// URL"), urlStr}
+ }
+ if u.Opaque != "" {
+ return nil, ErrorURL{errors.New("NewRequestURL: CNP URL may not contain opaque data"), urlStr}
+ }
+ if u.User != nil {
+ return nil, ErrorURL{errors.New("NewRequestURL: CNP URL cannot may not contain userinfo"), urlStr}
+ }
+
+ host := u.Hostname()
+ if strings.ContainsRune(host, ':') { // IPv6
+ host = "[" + host + "]"
+ }
+ port := DefaultPort
+ if sp := u.Port(); sp != "" {
+ port, err = strconv.Atoi(sp)
+ if err != nil {
+ return nil, ErrorURL{err, urlStr}
+ }
+ }
+ if port != DefaultPort {
+ host = host + ":" + strconv.Itoa(port)
+ }
+
+ pth := u.Path
+ if pth == "" {
+ pth = "/"
+ }
+ /*if u.RawQuery != "" {
+ q, err := url.QueryUnescape(u.RawQuery)
+ if err != nil {
+ return nil, ErrorURL{err, urlStr}
+ }
+ pth = pth + "?" + q
+ }*/
+
+ return NewRequest(host, pth, body)
+}
+
+// ParseRequest parses a request message.
+func ParseRequest(r io.Reader) (*Request, error) {
+ msg, err := ParseMessage(r)
+ if err != nil {
+ return nil, err
+ }
+
+ if err = validateRequestIntent(msg.Intent()); err != nil {
+ return nil, err
+ }
+
+ return &Request{*msg}, nil
+}
+
+// SetHost sets the host part of the request intent, leaving path unchanged.
+func (r *Request) SetHost(host string) error {
+ return r.SetHostPath(host, r.Path())
+}
+
+// SetPath sets the path part of the request intent, leaving host unchanged.
+func (r *Request) SetPath(pth string) error {
+ return r.SetHostPath(r.Host(), pth)
+}
+
+// SetHostPath sets the request intent.
+func (r *Request) SetHostPath(host, pth string) error {
+ if len(pth) < 1 || pth[0] != '/' {
+ return ErrorInvalid{"invalid request: invalid path"}
+ }
+ if strings.ContainsRune(host, '/') {
+ return ErrorInvalid{"invalid request: invalid host"}
+ }
+ r.SetIntent(host + Clean(pth))
+ return nil
+}
+
+// Host returns the host part of the request intent.
+func (r *Request) Host() string {
+ host, _ := r.HostPath()
+ return host
+}
+
+// Path returns the path part of the request intent.
+func (r *Request) Path() string {
+ _, pth := r.HostPath()
+ return pth
+}
+
+// HostPath returns the host and path parts of the request intent.
+func (r *Request) HostPath() (host string, pth string) {
+ ss := strings.SplitN(r.Intent(), "/", 2)
+ if len(ss) != 2 {
+ return "", "/"
+ }
+ host = ss[0]
+ pth = "/" + ss[1]
+ return
+}
+
+// URL returns a cnp:// URL based on this request's intent.
+func (r *Request) URL() *url.URL {
+ var u url.URL
+ u.Scheme = "cnp"
+ u.Host = r.Host()
+ u.Path = r.Path()
+ return &u
+}
+
+// Name retrieves the name request parameter.
+//
+// If the name request parameter is not a valid filename, an empty string is
+// returned.
+func (r *Request) Name() string {
+ name, err := getFilename(&r.Message, "name")
+ if err != nil {
+ return ""
+ }
+ return name
+}
+
+// SetName sets the name request parameter.
+//
+// An error is raised 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)
+}
+
+// Type retrieves the type request parameter.
+//
+// If the type request parameter is invalid or empty, the default value
+// "application/octet-stream" is returned.
+func (r *Request) Type() string {
+ typ, _ := getType(&r.Message, "type")
+ return typ
+}
+
+// SetType sets the type request parameter.
+//
+// An error is raised if typ is not a valid format for a MIME type.
+func (r *Request) SetType(typ string) error {
+ return setType(&r.Message, "type", typ)
+}
+
+// IfModified retrieves the if_modified request parameter.
+//
+// If the parameter isn't a valid RFC3339 timestamp, a zero time.Time is
+// returned.
+func (r *Request) IfModified() time.Time {
+ t, err := getTime(&r.Message, "if_modified")
+ if err != nil {
+ return time.Time{}
+ }
+ return t
+}
+
+// SetIfModified sets the if_modified request parameter.
+//
+// If t is the zero time value, the if_modified parameter is unset.
+func (r *Request) SetIfModified(t time.Time) {
+ setTime(&r.Message, "if_modified", t)
+}
+
+// Validate validates the request header intent and parameter value format
+// (length, name, type, if_modified)
+func (r *Request) Validate() error {
+ if err := validateRequestIntent(r.Intent()); err != nil {
+ return err
+ }
+ if err := r.Message.Validate(); err != nil {
+ return err
+ }
+ if _, err := getFilename(&r.Message, "name"); err != nil {
+ return err
+ }
+ if _, err := getType(&r.Message, "type"); err != nil {
+ return err
+ }
+ if _, err := getTime(&r.Message, "if_modified"); err != nil {
+ return err
+ }
+ return nil
+}
+
+func validateRequestIntent(intent string) error {
+ ss := strings.SplitN(intent, "/", 2)
+ if len(ss) != 2 {
+ return ErrorInvalid{"invalid request: invalid intent"}
+ }
+ host, pth := ss[0], ss[1]
+
+ if strings.ContainsAny(host, "\x00 ") || strings.ContainsRune(pth, '\x00') {
+ return ErrorInvalid{"invalid request: invalid intent"}
+ }
+
+ return nil
+}
+
+// Write ensures that the request's length parameter is set if it has body and
+// then writes it to w.
+func (r *Request) Write(w io.Writer) error {
+ if _, ok := r.Header.Parameters["length"]; !ok {
+ if err := r.ComputeLength(); err != nil {
+ return err
+ }
+ }
+ return r.Message.Write(w)
+}
+
+// Clean cleans a CNP intent path.
+func Clean(s string) string {
+ c := path.Clean(s)
+ if len(s) > 0 && len(c) > 0 && s[len(s)-1] == '/' && c[len(c)-1] != '/' {
+ return c + "/"
+ }
+ return c
+}
diff --git a/request_test.go b/request_test.go
new file mode 100644
index 0000000..bb4aa9b
--- /dev/null
+++ b/request_test.go
@@ -0,0 +1,341 @@
+package cnp
+
+import (
+ "bytes"
+ "strconv"
+ "strings"
+ "testing"
+ "time"
+)
+
+type requestTest struct {
+ h, s string
+ p Parameters
+ e, v error
+}
+
+var requestTests = []requestTest{
+ // invalid intent
+ {"", "", nil, ErrorInvalid{}, nil},
+ {"", "foo/bar", nil, ErrorInvalid{}, nil},
+ {"foo/", "bar", nil, ErrorInvalid{}, nil},
+ {"foo/", "/bar", nil, ErrorInvalid{}, nil},
+
+ // invalid request params
+ {"", "/", Parameters{"length": "w"}, nil, ErrorInvalid{}},
+ {"", "/", Parameters{"length": "-1"}, nil, ErrorInvalid{}},
+ {"", "/", Parameters{"length": "03"}, nil, ErrorInvalid{}},
+ {"", "/", Parameters{"name": "foo/bar"}, nil, ErrorInvalid{}},
+ {"", "/", Parameters{"name": "foo\x00bar"}, nil, ErrorInvalid{}},
+ {"", "/", Parameters{"type": "foo"}, nil, ErrorInvalid{}},
+ {"", "/", Parameters{"type": "\x00"}, nil, ErrorInvalid{}},
+ {"", "/", Parameters{"type": "text/plain\x00"}, nil, ErrorInvalid{}},
+ {"", "/", Parameters{"type": "foo/bar "}, nil, ErrorInvalid{}},
+ {"", "/", Parameters{"type": " foo/bar"}, nil, ErrorInvalid{}},
+ {"", "/", Parameters{"type": "foo /bar"}, nil, ErrorInvalid{}},
+ {"", "/", Parameters{"type": "foo/ bar"}, nil, ErrorInvalid{}},
+ {"", "/", Parameters{"type": "foo/bar\n"}, nil, ErrorInvalid{}},
+ {"", "/", Parameters{"type": "foo/b(r"}, nil, ErrorInvalid{}},
+ {"", "/", Parameters{"if_modified": "0"}, nil, ErrorInvalid{}},
+ {"", "/", Parameters{"if_modified": "now"}, nil, ErrorInvalid{}},
+ {"", "/", Parameters{"if_modified": "today"}, nil, ErrorInvalid{}},
+ {"", "/", Parameters{"if_modified": "Thu Jan 1 00:00:00 UTC 1970"}, nil, ErrorInvalid{}},
+ {"", "/", Parameters{"if_modified": "1970-01-01 00:00:00+00:00"}, nil, ErrorInvalid{}},
+ {"", "/", Parameters{"if_modified": "1970-01-01 00:00:00 UTC"}, nil, ErrorInvalid{}},
+ {"", "/", Parameters{"if_modified": "1970-01-01 00:00:00+0000"}, nil, ErrorInvalid{}},
+ {"", "/", Parameters{"if_modified": "1970-01-01 00:00:00+00"}, nil, ErrorInvalid{}},
+ {"", "/", Parameters{"if_modified": "1970-01-01T00:00:00+00:00"}, nil, ErrorInvalid{}},
+ {"", "/", Parameters{"if_modified": "1970-01-01T00:00:00 UTC"}, nil, ErrorInvalid{}},
+ {"", "/", Parameters{"if_modified": "1970-01-01T00:00:00+0000"}, nil, ErrorInvalid{}},
+ {"", "/", Parameters{"if_modified": "1970-01-01T00:00:00+00"}, nil, ErrorInvalid{}},
+ {"", "/", Parameters{"if_modified": "0000-00-01T00:00:00Z"}, nil, ErrorInvalid{}},
+ {"", "/", Parameters{"if_modified": "0000-01-00T00:00:00Z"}, nil, ErrorInvalid{}},
+ {"", "/", Parameters{"if_modified": "0000-01-01T24:00:00Z"}, nil, ErrorInvalid{}},
+ {"", "/", Parameters{"if_modified": "0000-01-01T00:60:00Z"}, nil, ErrorInvalid{}},
+ {"", "/", Parameters{"if_modified": "0000-01-01T00:00:60Z"}, nil, ErrorInvalid{}},
+ {"", "/", Parameters{"if_modified": "0000-11-31T00:00:00Z"}, nil, ErrorInvalid{}},
+ {"", "/", Parameters{"if_modified": "0001-02-29T00:00:00Z"}, nil, ErrorInvalid{}},
+ {"", "/", Parameters{"if_modified": "0002-02-29T00:00:00Z"}, nil, ErrorInvalid{}},
+ {"", "/", Parameters{"if_modified": "0003-02-29T00:00:00Z"}, nil, ErrorInvalid{}},
+ {"", "/", Parameters{"if_modified": "0005-02-29T00:00:00Z"}, nil, ErrorInvalid{}},
+ {"", "/", Parameters{"if_modified": "0100-02-29T00:00:00Z"}, nil, ErrorInvalid{}},
+ {"", "/", Parameters{"if_modified": "1000-02-29T00:00:00Z"}, nil, ErrorInvalid{}},
+ {"", "/", Parameters{"if_modified": "123-01-01T00:00:00Z"}, nil, ErrorInvalid{}},
+ {"", "/", 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{}},
+
+ // valid simple requests
+ {"", "/", nil, nil, nil},
+ {"", "/foo/bar", nil, nil, nil},
+ {"cnp.example.com", "/", nil, nil, nil},
+ {"foo", "/bar", nil, nil, nil},
+ {"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": "0"}, nil, nil},
+ {"", "/", Parameters{"length": "1"}, nil, nil},
+ {"", "/", Parameters{"length": "12345670089000000"}, nil, nil},
+ {"", "/", Parameters{"name": "foobar"}, nil, nil},
+ {"", "/", Parameters{"name": "x"}, nil, nil},
+ {"", "/", Parameters{"name": "..-~!foo bar\nbaz\rquux=qwe\\asd"}, nil, nil},
+ {"", "/", Parameters{"name": strings.Repeat("w", 1024*8)}, nil, nil},
+ {"", "/", Parameters{"type": "qwe/asd"}, nil, nil},
+ {"", "/", Parameters{"type": "qwe+asd/foo"}, nil, nil},
+ {"", "/", Parameters{"type": "qwe-asd/foo"}, nil, nil},
+ {"", "/", Parameters{"if_modified": "1970-01-01T00:00:00Z"}, nil, nil},
+ {"", "/", Parameters{"if_modified": "0000-01-01T00:00:00Z"}, nil, nil},
+ {"", "/", Parameters{"if_modified": "9999-12-31T23:59:59Z"}, nil, nil},
+ {"", "/", 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},
+}
+
+func TestNewRequest(t *testing.T) {
+ for _, tst := range requestTests {
+ req, err := NewRequest(tst.h, tst.s, []byte{})
+ if !errorEqual(tst.e, err) {
+ t.Errorf("NewRequest(%q, %q): expected error %+v, got %+v (%v)", tst.h, tst.s, tst.e, err, err)
+ continue
+ }
+ if tst.e == nil {
+ if tst.h != req.Host() {
+ t.Errorf("NewRequest(%q: %q): got unexpected host %q", tst.h, tst.s, req.Host())
+ } else if Clean(tst.s) != req.Path() {
+ t.Errorf("NewRequest(%q: %q): got unexpected path %q", tst.h, tst.s, req.Path())
+ }
+ }
+ }
+}
+
+type requestURLTest struct {
+ u string
+ h, p string
+ e error
+}
+
+var requestURLTests = []requestURLTest{
+ // invalid
+ {"cnp:example.com/path/to/file", "", "", ErrorURL{}},
+ {"http://example.com/path/to/file", "", "", ErrorURL{}},
+ {"cnp://foo@bar:example.com/path/to/file", "", "", ErrorURL{}},
+ {"cnp://foo:example.com/path/to/file", "", "", ErrorURL{}},
+ {"", "", "/", ErrorURL{}},
+ {"cnp://example.com/%5", "", "", ErrorURL{}},
+ //{"cnp://example.com/?%5", "", "", ErrorURL{}},
+
+ // valid
+ {"cnp://example.com/path/to/file", "example.com", "/path/to/file", nil},
+ {"cnp://2130706433/", "2130706433", "/", nil},
+ //{"cnp://02130706433/", "2130706433", "/", nil},
+ {"cnp://127.0.0.1/", "127.0.0.1", "/", nil},
+ //{"cnp://0127.0.00.01/", "127.0.0.1", "/", nil},
+ {"cnp://localhost", "localhost", "/", nil},
+ {"cnp://[::1]/foo%20bar", "[::1]", "/foo bar", nil},
+ {"cnp://[2001:db8::7334]/foo%0abar", "[2001:db8::7334]", "/foo\nbar", nil},
+ //{"cnp://[2001:0db8:0000::7334]/foo%0abar", "[2001:db8::7334]", "/foo\nbar", nil},
+ //{"cnp://example.com/qwe%20asd?foo=bar&baz=qu%20ux", "example.com", "/qwe asd?foo=bar&baz=qu ux", nil},
+ {"cnp://example.com/qwe%20asd?foo=bar&baz=qu%20ux", "example.com", "/qwe asd", nil},
+ {"cnp://localhost:25454", "localhost", "/", nil},
+ {"cnp://localhost:12345/foo/bar", "localhost:12345", "/foo/bar", nil},
+ {"cnp://localhost:25454/foo/bar", "localhost", "/foo/bar", nil},
+ {"cnp://2130706433:12345/foo/bar", "2130706433:12345", "/foo/bar", nil},
+ {"cnp://2130706433:25454/foo/bar", "2130706433", "/foo/bar", nil},
+ {"cnp://127.0.0.1:12345/foo/bar", "127.0.0.1:12345", "/foo/bar", nil},
+ {"cnp://127.0.0.1:25454/foo/bar", "127.0.0.1", "/foo/bar", nil},
+ {"cnp://[::1]:12345/foo/bar", "[::1]:12345", "/foo/bar", nil},
+ {"cnp://[::1]:25454/foo/bar", "[::1]", "/foo/bar", nil},
+ {"//example.com/path/", "example.com", "/path/", nil},
+ {"/example.com/path/", "", "/example.com/path/", nil},
+ {"/foo/bar", "", "/foo/bar", nil},
+ {"/", "", "/", nil},
+ {"cnp://example.com/ ™☺)\n", "example.com", "/ ™☺)\n", nil},
+ {"cnp://œ¤å₥¶ḹə.©°ɱ/baz/../foo/.//bar////.//../bar//", "œ¤å₥¶ḹə.©°ɱ", "/foo/bar/", nil},
+ {"cnp://foo/bar/", "foo", "/bar/", nil},
+ {"cnp:///foo/bar/", "", "/foo/bar/", nil},
+ {"cnp:////////foo/bar/", "", "/foo/bar/", nil},
+}
+
+func TestNewRequestURL(t *testing.T) {
+ for _, tst := range requestURLTests {
+ req, err := NewRequestURL(tst.u, nil)
+ if !errorEqual(tst.e, err) {
+ t.Errorf("NewRequestURL(%q, nil): expected error %+v, got %+v (%v)", tst.u, tst.e, err, err)
+ continue
+ }
+ if tst.e == nil {
+ if req.Host() != tst.h {
+ t.Errorf("NewRequestURL(%q, nil): expected host %q, got %q", tst.u, tst.h, req.Host())
+ } else if req.Path() != tst.p {
+ t.Errorf("NewRequestURL(%q, nil): expected path %q, got %q", tst.u, tst.p, req.Path())
+ }
+ }
+ }
+}
+
+func TestWriteParseRequest(t *testing.T) {
+ for _, tst := range requestTests {
+ if tst.e != nil {
+ continue // skip syntax errors but not invalid params
+ }
+ req := &Request{Message{Header{0, 3, "/", Parameters{}}, nil, nil}}
+ if err := req.SetHost(tst.h); err != nil {
+ t.Errorf("SetHost(%q) error: %v", tst.h, err)
+ continue
+ }
+ if err := req.SetPath(tst.s); err != nil {
+ t.Errorf("SetPath(%q) error: %v", tst.s, err)
+ continue
+ }
+ for k, v := range tst.p {
+ req.SetParam(k, v)
+ }
+
+ var buf bytes.Buffer
+ if err := req.Write(&buf); err != nil {
+ t.Errorf("%+v.Write: error %v", req, err)
+ continue
+ }
+
+ s := buf.String()
+ req2, err := ParseRequest(strings.NewReader(s))
+ if err != nil {
+ t.Errorf("ParseRequest(%q) error: %v", s, err)
+ continue
+ }
+ if !msgEqual(&req.Message, &req2.Message) {
+ t.Errorf("Write/Parse: expected %+v, got %+v", req, req2)
+ continue
+ }
+ }
+}
+
+func TestRequestValidate(t *testing.T) {
+ for _, tst := range requestTests {
+ if tst.e != nil {
+ continue
+ }
+ req := &Request{Message{Header{Intent: tst.h + tst.s, Parameters: tst.p}, nil, nil}}
+ if err := req.Validate(); !errorEqual(tst.v, err) {
+ t.Errorf("%+v.Validate(): expected error %+v, got %+v (%v)", req, tst.v, err, err)
+ }
+ }
+}
+
+func TestRequestGetSet(t *testing.T) {
+ e := func(k, v string, expect, err error) {
+ if !errorEqual(err, expect) {
+ t.Errorf("setting param %q to %q: expected %+v, got %+v (%v)", k, v, expect, err, err)
+ }
+ }
+ c := func(k, v string, expect error, p string) {
+ if expect == nil && v != p {
+ t.Errorf("getting param %q: expected %q, got %q", k, v, p)
+ }
+ }
+
+ for _, tst := range requestTests {
+ if tst.e != nil {
+ continue
+ }
+ req, _ := NewRequest(tst.h, tst.s, nil)
+ for k, v := range tst.p {
+ switch k {
+ case "length":
+ n, _ := strconv.ParseUint(v, 10, 63)
+ req.SetLength(int64(n))
+ s := strconv.FormatInt(req.Length(), 10)
+ if s == "0" && v == "" {
+ s = ""
+ }
+ c(k, v, tst.v, s)
+ case "name":
+ e(k, v, tst.v, req.SetName(v))
+ c(k, v, tst.v, req.Name())
+ case "type":
+ e(k, v, tst.v, req.SetType(v))
+ t := req.Type()
+ if t == "application/octet-stream" && v == "" {
+ t = ""
+ }
+ c(k, v, tst.v, t)
+ case "if_modified":
+ if tst.v != nil {
+ continue // invalid time format, can't even parse
+ }
+ var tm time.Time
+ if v != "" {
+ tm, _ = time.Parse(time.RFC3339, v)
+ }
+ req.SetIfModified(tm)
+ tm2 := ""
+ if !req.IfModified().IsZero() {
+ tm2 = req.IfModified().Format(time.RFC3339)
+ }
+ c(k, v, tst.v, tm2)
+ default:
+ req.SetParam(k, v)
+ c(k, v, tst.v, req.Param(k))
+ }
+ }
+ }
+}
+
+var urlTests = map[string]string{
+ "œ¤å₥¶ḹə.©°ɱ/foo/bar": "cnp://%C5%93%C2%A4%C3%A5%E2%82%A5%C2%B6%E1%B8%B9%C9%99.%C2%A9%C2%B0%C9%B1/foo/bar",
+ "2130706433/": "cnp://2130706433/",
+ "example.com/path/to/file/": "cnp://example.com/path/to/file/",
+ "[::1]/foo bar": "cnp://[::1]/foo%20bar",
+ "[2001:db8::7334]/foo\nbar": "cnp://[2001:db8::7334]/foo%0Abar",
+ "example.com/qwe asd": "cnp://example.com/qwe%20asd",
+ "example.com/ ™☺)\n": "cnp://example.com/%20%E2%84%A2%E2%98%BA%29%0A",
+}
+
+func TestRequestURL(t *testing.T) {
+ for k, v := range urlTests {
+ t.Run(v, func(t *testing.T) {
+ req, err := NewRequestURL(v, nil)
+ if err != nil {
+ t.Fatalf("NewRequestURL(%q): error: %v", v, err)
+ }
+ if req.Intent() != k {
+ t.Fatalf("NewRequestURL(%q).Intent(): expected %q, got %q", v, k, req.Intent())
+ }
+ if req.URL().String() != v {
+ t.Fatalf("NewRequestURL(%q).URL(): expected %q, got %q", v, v, req.URL())
+ }
+ })
+ }
+}
+
+var cleanTests = map[string]string{
+ "/../.././//foo/bar/..": "/foo",
+ "//foo/bar/../": "/foo/",
+ "/": "/",
+ "//": "/",
+ "/..": "/",
+ "/../..": "/",
+ "/../": "/",
+ "/.": "/",
+ "/./": "/",
+ "/./.": "/",
+ "/././": "/",
+ "/./../": "/",
+ "/.././": "/",
+ "/foo/bar/../baz": "/foo/baz",
+ "/foo/bar/../baz/": "/foo/baz/",
+ "/foo/../foo/bar/../bar/baz/quux/../": "/foo/bar/baz/",
+ "/foo/../foo/bar/../bar/baz/quux/..": "/foo/bar/baz",
+}
+
+func TestClean(t *testing.T) {
+ for k, v := range cleanTests {
+ t.Run(k, func(t *testing.T) {
+ c := Clean(k)
+ if c != v {
+ t.Fatalf("Clean(%q): expected %q, got %q", k, v, c)
+ }
+ })
+ }
+}
diff --git a/response.go b/response.go
new file mode 100644
index 0000000..bf23f20
--- /dev/null
+++ b/response.go
@@ -0,0 +1,249 @@
+package cnp
+
+import (
+ "bytes"
+ "io"
+ "strings"
+ "time"
+)
+
+// Response represents a CNP response message.
+type Response struct {
+ Message
+}
+
+// NewResponse creates a new Response from a response intent and optional body
+// data.
+func NewResponse(intent string, body []byte) (resp *Response, err error) {
+ var r io.Reader
+ if body != nil {
+ r = bytes.NewReader(body)
+ }
+ resp = &Response{*NewMessage("", r)}
+ err = resp.SetResponseIntent(intent)
+ return
+}
+
+var responseIntents = map[string]bool{
+ IntentOK: true,
+ IntentNotModified: true,
+ IntentRedirect: true,
+ IntentError: true,
+}
+
+// ParseResponse parses a response message.
+func ParseResponse(r io.Reader) (*Response, error) {
+ msg, err := ParseMessage(r)
+ if msg == nil {
+ return nil, err
+ }
+ return &Response{*msg}, err
+}
+
+// ResponseIntent retrieves the response intent.
+// If the intent is unknown, the "error" intent is returned.
+func (r *Response) ResponseIntent() string {
+ s := r.Intent()
+ if !responseIntents[s] {
+ return IntentError
+ }
+ return s
+}
+
+// SetResponseIntent sets the response intent.
+// If the provided intent is not a known response intent, an error is returned
+// and the intent is set to "error".
+func (r *Response) SetResponseIntent(intent string) error {
+ if !responseIntents[intent] {
+ r.SetIntent(IntentError)
+ return ErrorInvalid{"invalid response: unknown response intent"}
+ }
+ r.SetIntent(intent)
+ return nil
+}
+
+// Name retrieves the name response parameter.
+//
+// If the name response parameter is not a valid filename, an empty string is
+// returned.
+func (r *Response) Name() string {
+ name, err := getFilename(&r.Message, "name")
+ if err != nil {
+ return ""
+ }
+ return name
+}
+
+// SetName sets the name response parameter.
+//
+// An error is raised 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)
+}
+
+// Type retrieves the type response parameter.
+//
+// If the type response parameter is invalid or empty, the default value
+// "application/octet-stream" is returned.
+func (r *Response) Type() string {
+ typ, _ := getType(&r.Message, "type")
+ return typ
+}
+
+// SetType sets the type response parameter.
+//
+// An error is raised if typ is not a valid format for a MIME type.
+func (r *Response) SetType(typ string) error {
+ return setType(&r.Message, "type", typ)
+}
+
+// Time retrieves the time response parameter.
+//
+// If the parameter isn't a valid RFC3339 timestamp, a zero time.Time is
+// returned.
+func (r *Response) Time() time.Time {
+ t, err := getTime(&r.Message, "time")
+ if err != nil {
+ return time.Time{}
+ }
+ return t
+}
+
+// SetTime sets the time response parameter.
+//
+// If t is the zero time value, the time parameter is unset.
+func (r *Response) SetTime(t time.Time) {
+ setTime(&r.Message, "time", t)
+}
+
+// Modified retrieves the modified Response parameter.
+//
+// If the parameter isn't a valid RFC3339 timestamp, a zero time.Time is
+// returned.
+func (r *Response) Modified() time.Time {
+ t, err := getTime(&r.Message, "modified")
+ if err != nil {
+ return time.Time{}
+ }
+ return t
+}
+
+// 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.
+func (r *Response) SetModified(t time.Time) {
+ setTime(&r.Message, "modified", t)
+ if r.Time().IsZero() {
+ r.SetTime(time.Now())
+ }
+}
+
+// Location retrieves the host and path from the location response
+// parameter.
+//
+// If the location parameter is empty, it returns empty host and path. If the
+// location parameter is invalid, an error is returned.
+func (r *Response) Location() (host, path string, err error) {
+ l := r.Param("location")
+ if l == "" {
+ return "", "", nil
+ }
+ if err := validateRequestIntent(l); err != nil {
+ return "", "", err
+ }
+ ss := strings.SplitN(l, "/", 2)
+ return ss[0], "/" + ss[1], nil
+}
+
+// SetLocation sets the location response parameter to host and path.
+//
+// If the host or path are invalid
+func (r *Response) SetLocation(host, path string) error {
+ if strings.ContainsRune(host, '/') {
+ return ErrorInvalid{"invalid response: invalid location parameter"}
+ }
+ l := host + path
+ if err := validateRequestIntent(l); err != nil {
+ return ErrorInvalid{"invalid response: invalid location parameter"}
+ }
+ r.SetParam("location", l)
+ return nil
+}
+
+var responseErrorReasons = map[string]bool{
+ ReasonSyntax: true,
+ ReasonVersion: true,
+ ReasonInvalid: true,
+ ReasonNotSupported: true,
+ ReasonTooLarge: true,
+ ReasonNotFound: true,
+ ReasonDenied: true,
+ ReasonRejected: true,
+ ReasonServerError: true,
+ "": true,
+}
+
+// Reason retrieves the reason response parameter.
+//
+// If the reason is nonempty and unknown, "server_error" is returned.
+func (r *Response) Reason() string {
+ reason := r.Param("reason")
+ if _, ok := responseErrorReasons[reason]; ok {
+ return reason
+ }
+ return ReasonServerError
+}
+
+// SetReason sets the reason response parameter.
+//
+// If the reason is nonempty and unknown, it sets "server_error" instead.
+func (r *Response) SetReason(reason string) error {
+ if _, ok := responseErrorReasons[reason]; !ok {
+ r.SetParam("reason", ReasonServerError)
+ return ErrorInvalid{"invalid response: unknown error reason"}
+ }
+ r.SetParam("reason", reason)
+ return nil
+}
+
+// Validate validates the response intent and header parameter value format
+// (length, name, type, time, modified, location, reason)
+func (r *Response) Validate() error {
+ if !responseIntents[r.Intent()] {
+ return ErrorInvalid{"invalid response: unknown response intent"}
+ }
+ if err := r.Message.Validate(); err != nil {
+ return err
+ }
+ if _, err := getFilename(&r.Message, "name"); err != nil {
+ return err
+ }
+ if _, err := getType(&r.Message, "type"); err != nil {
+ return err
+ }
+ if _, err := getTime(&r.Message, "time"); err != nil {
+ return err
+ }
+ if _, err := getTime(&r.Message, "modified"); err != nil {
+ return err
+ }
+ if h, p, err := r.Location(); err != nil {
+ return err
+ } else if r.ResponseIntent() == IntentRedirect && h == "" && p == "" {
+ return ErrorInvalid{"invalid response: redirect response needs location parameter"}
+ }
+ if !responseErrorReasons[r.Param("reason")] {
+ return ErrorInvalid{"invalid response: unknown error reason"}
+ }
+ return nil
+}
+
+// Write writes the response to w.
+func (r *Response) Write(w io.Writer) error {
+ if _, ok := r.Header.Parameters["length"]; !ok {
+ r.TryComputeLength()
+ }
+ return r.Message.Write(w)
+}
diff --git a/response_test.go b/response_test.go
new file mode 100644
index 0000000..8033335
--- /dev/null
+++ b/response_test.go
@@ -0,0 +1,317 @@
+package cnp
+
+import (
+ "bytes"
+ "strconv"
+ "strings"
+ "testing"
+ "time"
+)
+
+var (
+ responseTests = []responseTest{
+ // invalid intent
+ {"", nil, ErrorInvalid{}, ErrorInvalid{}},
+ {"foo", nil, ErrorInvalid{}, ErrorInvalid{}},
+ {"example.com/path", nil, ErrorInvalid{}, ErrorInvalid{}},
+ {"errors", nil, ErrorInvalid{}, ErrorInvalid{}},
+ {" ok", nil, ErrorInvalid{}, ErrorInvalid{}},
+ {"ok ", nil, ErrorInvalid{}, ErrorInvalid{}},
+ {"оk", nil, ErrorInvalid{}, ErrorInvalid{}},
+
+ // invalid response params
+ {"ok", Parameters{"length": "w"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"length": "-1"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"length": "03"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"name": "/.."}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"name": "/"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"name": "foo/bar"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"name": "foo/bar"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"name": "foo\x00bar"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"type": "foo"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"type": "\x00"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"type": "text/plain\x00"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"type": "foo/bar "}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"type": " foo/bar"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"type": "foo /bar"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"type": "foo/ bar"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"type": "foo/bar\n"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"type": "foo/b(r"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"time": "0"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"time": "now"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"time": "today"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"time": "Thu Jan 1 00:00:00 UTC 1970"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"time": "1970-01-01 00:00:00+00:00"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"time": "1970-01-01 00:00:00 UTC"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"time": "1970-01-01 00:00:00+0000"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"time": "1970-01-01 00:00:00+00"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"time": "1970-01-01T00:00:00+00:00"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"time": "1970-01-01T00:00:00 UTC"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"time": "1970-01-01T00:00:00+0000"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"time": "1970-01-01T00:00:00+00"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"time": "0000-00-01T00:00:00Z"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"time": "0000-01-00T00:00:00Z"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"time": "0000-01-01T24:00:00Z"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"time": "0000-01-01T00:60:00Z"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"time": "0000-01-01T00:00:60Z"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"time": "0000-11-31T00:00:00Z"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"time": "0001-02-29T00:00:00Z"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"time": "0002-02-29T00:00:00Z"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"time": "0003-02-29T00:00:00Z"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"time": "0005-02-29T00:00:00Z"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"time": "0100-02-29T00:00:00Z"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"time": "1000-02-29T00:00:00Z"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"time": "123-01-01T00:00:00Z"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"time": "12345-01-01T00:00:00Z"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"time": "-5-01-01T00:00:00Z"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"time": "-2005-01-01T00:00:00Z"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"modified": "0"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"modified": "now"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"modified": "today"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"modified": "Thu Jan 1 00:00:00 UTC 1970"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"modified": "1970-01-01 00:00:00+00:00"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"modified": "1970-01-01 00:00:00 UTC"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"modified": "1970-01-01 00:00:00+0000"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"modified": "1970-01-01 00:00:00+00"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"modified": "1970-01-01T00:00:00+00:00"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"modified": "1970-01-01T00:00:00 UTC"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"modified": "1970-01-01T00:00:00+0000"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"modified": "1970-01-01T00:00:00+00"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"modified": "0000-00-01T00:00:00Z"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"modified": "0000-01-00T00:00:00Z"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"modified": "0000-01-01T24:00:00Z"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"modified": "0000-01-01T00:60:00Z"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"modified": "0000-01-01T00:00:60Z"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"modified": "0000-11-31T00:00:00Z"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"modified": "0001-02-29T00:00:00Z"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"modified": "0002-02-29T00:00:00Z"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"modified": "0003-02-29T00:00:00Z"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"modified": "0005-02-29T00:00:00Z"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"modified": "0100-02-29T00:00:00Z"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"modified": "1000-02-29T00:00:00Z"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"modified": "123-01-01T00:00:00Z"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"modified": "12345-01-01T00:00:00Z"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"modified": "-5-01-01T00:00:00Z"}, nil, ErrorInvalid{}},
+ {"ok", Parameters{"modified": "-2005-01-01T00:00:00Z"}, nil, ErrorInvalid{}},
+ {"redirect", Parameters{"location": "foo"}, nil, ErrorInvalid{}},
+ {"redirect", Parameters{"location": "foo bar/baz quux"}, nil, ErrorInvalid{}},
+ {"redirect", Parameters{"location": "/foo\x00bar"}, nil, ErrorInvalid{}},
+ {"error", Parameters{"reason": "ok"}, nil, ErrorInvalid{}},
+ {"error", Parameters{"reason": "not supported"}, nil, ErrorInvalid{}},
+ {"error", Parameters{"reason": "syntax\n"}, nil, ErrorInvalid{}},
+ {"error", Parameters{"reason": " server_error"}, nil, ErrorInvalid{}},
+ {"error", Parameters{"reason": "invalid "}, nil, ErrorInvalid{}},
+
+ // invalid: redirect *requires* the location parameter
+ {"redirect", nil, nil, ErrorInvalid{}},
+ {"redirect", Parameters{"location": ""}, nil, ErrorInvalid{}},
+
+ // valid simple responses
+ {"ok", nil, nil, nil},
+ {"not_modified", nil, nil, nil},
+ {"error", nil, nil, nil},
+
+ // valid responses with parameters
+ {"ok", Parameters{"length": "", "name": "", "type": "", "time": "", "reason": "", "location": "", "modified": "", "": "", "q\x00we": "=a s\nd"}, nil, nil},
+
+ {"ok", Parameters{"length": "0"}, nil, nil},
+ {"ok", Parameters{"length": "1"}, nil, nil},
+ {"ok", Parameters{"length": "12345670089000000"}, nil, nil},
+ {"ok", Parameters{"length": "12345678900"}, nil, nil},
+
+ {"ok", Parameters{"name": "foobar"}, nil, nil},
+ {"ok", Parameters{"name": "foo bar"}, nil, nil},
+ {"ok", Parameters{"name": "foo=bar\nbaz\rquux"}, nil, nil},
+ {"ok", Parameters{"name": "..-~!foo bar\nbaz\rquux=qwe\\asd"}, nil, nil},
+ {"ok", Parameters{"name": strings.Repeat("w", 1024*8)}, nil, nil},
+ {"ok", Parameters{"name": " "}, nil, nil},
+ {"ok", Parameters{"name": ".."}, nil, nil},
+ {"ok", Parameters{"name": "."}, nil, nil},
+
+ {"ok", Parameters{"type": "foo/bar"}, nil, nil},
+ {"ok", Parameters{"type": "application/octet-stream"}, nil, nil},
+ {"ok", Parameters{"type": "x-test/x-testing"}, nil, nil},
+ {"ok", Parameters{"type": "application/vnd.testing.test-test.5+xml"}, nil, nil},
+
+ {"ok", Parameters{"modified": "1970-01-01T00:00:00Z"}, nil, nil},
+ {"ok", Parameters{"modified": "0000-01-01T00:00:00Z"}, nil, nil},
+ {"ok", Parameters{"modified": "9999-12-31T23:59:59Z"}, nil, nil},
+ {"ok", Parameters{"modified": "0123-05-06T07:08:09Z"}, nil, nil},
+ {"ok", Parameters{"modified": "0000-02-29T00:00:00Z"}, nil, nil},
+ {"ok", Parameters{"modified": "2000-02-29T00:00:00Z"}, nil, nil},
+
+ {"ok", Parameters{"time": "1970-01-01T00:00:00Z"}, nil, nil},
+ {"ok", Parameters{"time": "0000-01-01T00:00:00Z"}, nil, nil},
+ {"ok", Parameters{"time": "9999-12-31T23:59:59Z"}, nil, nil},
+ {"ok", Parameters{"time": "0123-05-06T07:08:09Z"}, nil, nil},
+ {"ok", Parameters{"time": "0000-02-29T00:00:00Z"}, nil, nil},
+ {"ok", Parameters{"time": "2000-02-29T00:00:00Z"}, nil, nil},
+
+ {"error", Parameters{"reason": "syntax"}, nil, nil},
+ {"error", Parameters{"reason": "version"}, nil, nil},
+ {"error", Parameters{"reason": "invalid"}, nil, nil},
+ {"error", Parameters{"reason": "not_supported"}, nil, nil},
+ {"error", Parameters{"reason": "too_large"}, nil, nil},
+ {"error", Parameters{"reason": "not_found"}, nil, nil},
+ {"error", Parameters{"reason": "denied"}, nil, nil},
+ {"error", Parameters{"reason": "rejected"}, nil, nil},
+ {"error", Parameters{"reason": "server_error"}, nil, nil},
+
+ {"redirect", Parameters{"location": "/"}, nil, nil},
+ {"redirect", Parameters{"location": "foo/bar"}, nil, nil},
+ {"redirect", Parameters{"location": "foo/"}, nil, nil},
+ {"redirect", Parameters{"location": "/bar"}, nil, nil},
+ {"redirect", Parameters{"location": "[::1]:12345/ foo\n\x01\xff/"}, nil, nil},
+ {"redirect", Parameters{"location": "/../../////././.."}, nil, nil},
+ }
+)
+
+type responseTest struct {
+ i string
+ p Parameters
+ e, v error
+}
+
+func TestNewResponse(t *testing.T) {
+ for _, tst := range responseTests {
+ resp, err := NewResponse(tst.i, []byte{})
+ if !errorEqual(tst.e, err) {
+ t.Errorf("NewResponse(%q): expected error %+v, got %+v (%v)", tst.i, tst.e, err, err)
+ continue
+ }
+ if tst.e == nil {
+ if tst.i != resp.ResponseIntent() {
+ t.Errorf("NewResponse(%q): got unexpected intent %q", tst.i, resp.ResponseIntent())
+ }
+ }
+ }
+}
+
+func TestWriteParseResponse(t *testing.T) {
+ for _, tst := range responseTests {
+ if tst.e != nil {
+ continue // skip invalid intents
+ }
+ resp := &Response{Message{Header{0, 3, "ok", Parameters{}}, nil, nil}}
+ if err := resp.SetResponseIntent(tst.i); err != nil {
+ t.Errorf("SetResponseIntent(%q) error: %v", tst.i, err)
+ continue
+ }
+ for k, v := range tst.p {
+ resp.SetParam(k, v)
+ }
+
+ var buf bytes.Buffer
+ if err := resp.Write(&buf); err != nil {
+ t.Errorf("%+v.Write: error %v", resp, err)
+ continue
+ }
+
+ s := buf.String()
+ resp2, err := ParseResponse(strings.NewReader(s))
+ if err != nil {
+ t.Errorf("ParseResponse(%q) error: %v", s, err)
+ continue
+ }
+ if !msgEqual(&resp.Message, &resp2.Message) {
+ t.Errorf("Write/Parse: expected %+v, got %+v", resp, resp2)
+ continue
+ }
+ }
+}
+
+func TestResponseValidate(t *testing.T) {
+ for _, tst := range responseTests {
+ resp := &Response{Message{Header{Intent: tst.i, Parameters: tst.p}, nil, nil}}
+ if err := resp.Validate(); !errorEqual(tst.v, err) {
+ t.Errorf("%+v.Validate(): expected error %+v, got %+v (%v)", resp, tst.v, err, err)
+ }
+ }
+}
+
+func TestResponseGetSet(t *testing.T) {
+ e := func(k, v string, expect, err error) {
+ if !errorEqual(err, expect) {
+ t.Errorf("setting param %q to %q: expected %+v, got %+v (%v)", k, v, expect, err, err)
+ }
+ }
+ c := func(k, v string, expect error, p string) {
+ if expect == nil && v != p {
+ t.Errorf("getting param %q: expected %q, got %q", k, v, p)
+ }
+ }
+
+ for _, tst := range responseTests {
+ resp, _ := NewResponse("ok", nil)
+ if err := resp.SetResponseIntent(tst.i); !errorEqual(err, tst.e) {
+ t.Errorf("setting response intent to %q: expected %+v, got %+v (%v)", tst.i, tst.e, err, err)
+ continue
+ }
+ if tst.e == nil && resp.ResponseIntent() != tst.i {
+ t.Errorf("getting response intent: expected %q, got %q", tst.i, resp.ResponseIntent())
+ continue
+ }
+ for k, v := range tst.p {
+ switch k {
+ case "length":
+ n, _ := strconv.ParseUint(v, 10, 63)
+ resp.SetLength(int64(n))
+ s := strconv.FormatInt(resp.Length(), 10)
+ if s == "0" && v == "" {
+ s = ""
+ }
+ c(k, v, tst.v, s)
+ case "name":
+ e(k, v, tst.v, resp.SetName(v))
+ c(k, v, tst.v, resp.Name())
+ case "type":
+ e(k, v, tst.v, resp.SetType(v))
+ t := resp.Type()
+ if t == "application/octet-stream" && v == "" {
+ t = ""
+ }
+ c(k, v, tst.v, t)
+ case "time":
+ var tm time.Time
+ if v != "" {
+ tm, _ = time.Parse(time.RFC3339, v)
+ }
+ resp.SetTime(tm)
+ tm2 := ""
+ if !resp.Time().IsZero() {
+ tm2 = resp.Time().Format(time.RFC3339)
+ }
+ c(k, v, tst.v, tm2)
+ case "modified":
+ var tm time.Time
+ if v != "" {
+ tm, _ = time.Parse(time.RFC3339, v)
+ }
+ resp.SetModified(tm)
+ tm2 := ""
+ if !resp.Modified().IsZero() {
+ tm2 = resp.Modified().Format(time.RFC3339)
+ }
+ c(k, v, tst.v, tm2)
+ case "location":
+ ss := strings.SplitN(v, "/", 2)
+ if len(ss) != 2 {
+ continue // invalid location
+ }
+ e(k, v, tst.v, resp.SetLocation(ss[0], "/"+ss[1]))
+ host, path, err := resp.Location()
+ if err != nil {
+ t.Errorf("getting parameter location: error %v", err)
+ } else {
+ c(k, v, tst.v, host+path)
+ }
+ case "reason":
+ e(k, v, tst.v, resp.SetReason(v))
+ c(k, v, tst.v, resp.Reason())
+ default:
+ resp.SetParam(k, v)
+ c(k, v, tst.v, resp.Param(k))
+ }
+ }
+ }
+}
diff --git a/server.go b/server.go
new file mode 100644
index 0000000..3413923
--- /dev/null
+++ b/server.go
@@ -0,0 +1,257 @@
+package cnp
+
+import (
+ "bytes"
+ "errors"
+ "io"
+ "log"
+ "net"
+ "os"
+ "strconv"
+ "strings"
+ "time"
+)
+
+// TODO: make more modular and extensible like net/http
+
+// Server represents a CNP server.
+type Server struct {
+ // LogAccess is called for every finished response if it's non-nil.
+ LogAccess func(resp ResponseWriter, req *Request, respIntent string, respBytes int64)
+
+ // LogError is called when an error happens if it's non-nil.
+ LogError func(err interface{})
+
+ // AccessLog is used to log finished responses if LogAccess is nil.
+ AccessLogger *log.Logger
+
+ // ErrorLogger is used to log errors if LogError is nil.
+ ErrorLogger *log.Logger
+
+ // Address is the host:port that the server listens on.
+ Address string
+
+ // Handler is used to handle received requests.
+ Handler Handler
+
+ // Validate enables request parameter value validation; invalid requests
+ // are responded 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".
+func NewServer() *Server {
+ return &Server{
+ AccessLogger: log.New(os.Stdout, "", 0),
+ ErrorLogger: log.New(os.Stderr, "error: ", log.LstdFlags),
+ Validate: true,
+ Address: "localhost",
+ }
+}
+
+// ListenAndServe uses net.Listen to listen on TCP srv.Address for new
+// requests, then calls srv.Serve.
+func (srv *Server) ListenAndServe() error {
+ addr := srv.Address
+ if strings.LastIndexByte(addr, ':') <= strings.LastIndexByte(addr, ']') ||
+ strings.Count(addr, ":") > 1 && !strings.HasPrefix(addr, "[") { // missing/default port
+ addr = net.JoinHostPort(addr, strconv.Itoa(DefaultPort))
+ }
+ l, err := net.Listen("tcp", addr)
+ if err != nil {
+ return err
+ }
+ return srv.Serve(l)
+}
+
+// Serve listens on l for new connections and dispatches HandleConn goroutines.
+func (srv *Server) Serve(l net.Listener) error {
+ defer l.Close()
+
+ for {
+ conn, err := l.Accept()
+ if err != nil {
+ return err
+ }
+ go srv.HandleConn(conn)
+ }
+}
+
+func (srv *Server) sendError(conn net.Conn, req *Request, err Error) {
+ er, _ := NewResponse(IntentError, []byte(err.Error()+"\n"))
+ er.SetParam("reason", err.CNPError())
+ er.SetParam("type", "text/plain")
+ var buf bytes.Buffer
+ er.Write(&buf)
+ er.SetLength(int64(buf.Len()))
+ l, e2 := io.Copy(conn, &buf)
+ if e2 != nil {
+ srv.logError(e2)
+ }
+ srv.logAccess(&responseWriter{addr: conn.RemoteAddr()}, req, er.Intent(), l)
+}
+
+// HandleConn reads a CNP request from conn and runs a handler to respond.
+func (srv *Server) HandleConn(conn net.Conn) {
+ var rw *responseWriter
+ var req *Request
+
+ defer func() {
+ /*_, err := io.Copy(ioutil.Discard, req.Body)
+ if err != nil {
+ srv.ErrorLog.Print(err)
+ }*/
+
+ if rw != nil && rw.headerWritten {
+ srv.logAccess(rw, req, rw.resp.Header.Intent, rw.n)
+ }
+
+ if rec := recover(); rec != nil {
+ srv.logError(rec)
+ if err, ok := rec.(Error); ok && rw != nil && !rw.headerWritten {
+ srv.sendError(conn, req, err)
+ }
+ }
+
+ if req != nil {
+ req.Close()
+ }
+ }()
+
+ req, err := ParseRequest(conn)
+ req.Body = io.LimitReader(req.Body, req.Length())
+ if err != nil {
+ if e, ok := err.(Error); ok {
+ resp, _ := NewResponse(IntentError, nil)
+ resp.SetParam("reason", e.CNPError())
+ resp.Write(conn)
+ return
+ }
+ panic(err)
+ }
+
+ if srv.Validate {
+ err = req.Validate()
+ if err != nil {
+ srv.sendError(conn, req, err.(Error))
+ return
+ }
+ }
+
+ if srv.Handler != nil {
+ resp, _ := NewResponse(IntentOK, nil)
+ rw = &responseWriter{
+ w: conn,
+ resp: resp,
+ addr: conn.RemoteAddr(),
+ }
+ srv.Handler.ServeCNP(rw, req)
+ if !rw.headerWritten {
+ rw.WriteHeader()
+ }
+ }
+}
+
+func (srv *Server) logAccess(resp ResponseWriter, req *Request, respIntent string, respBytes int64) {
+ if srv.LogAccess != nil {
+ srv.LogAccess(resp, req, respIntent, respBytes)
+ } else if srv.AccessLogger != nil {
+ srv.AccessLogger.Printf("%s - - %s %q %s %d",
+ resp.RemoteAddr(),
+ time.Now().Format("[02/Jan/2006:03:04:05 -0700]"),
+ req.Header.Version()+" "+string(Escape(req.Host()))+string(Escape(req.Path())),
+ respIntent,
+ respBytes,
+ )
+ }
+}
+
+func (srv *Server) logError(err interface{}) {
+ if srv.LogError != nil {
+ srv.LogError(err)
+ } else if srv.ErrorLogger != nil {
+ srv.ErrorLogger.Println(err)
+ }
+}
+
+// Handler handles CNP requests accepted by the server.
+type Handler interface {
+ // ServeCNP responds to a CNP request.
+ //
+ // This function must be safe for concurrent use.
+ ServeCNP(resp ResponseWriter, req *Request)
+}
+
+// HandlerFunc allows using raw functions as handlers.
+type HandlerFunc func(resp ResponseWriter, req *Request)
+
+// ServeCNP calls h(resp, req).
+func (h HandlerFunc) ServeCNP(resp ResponseWriter, req *Request) {
+ h(resp, req)
+}
+
+// ResponseWriter is used by a CNP server to write responses to CNP requests.
+type ResponseWriter interface {
+ // Response returns a Response object whose header will be written to the
+ // socket. The body is nil and should be ignored.
+ Response() *Response
+
+ // RemoteAddr returns the network address of the client.
+ RemoteAddr() net.Addr
+
+ // WriteHeader sends a CNP header with an intent.
+ WriteHeader() error
+
+ // Write sends data in the CNP response body.
+ //
+ // If WriteHeader has not been called yet, it also calls WriteHeader("ok").
+ Write(data []byte) (int, error)
+}
+
+type responseWriter struct {
+ w io.Writer
+ addr net.Addr
+ resp *Response
+ srv *Server
+ n int64
+ headerWritten bool
+}
+
+func (r *responseWriter) Response() *Response {
+ return r.resp
+}
+
+func (r *responseWriter) RemoteAddr() net.Addr {
+ return r.addr
+}
+
+func (r *responseWriter) WriteHeader() error {
+ if r.headerWritten {
+ err := errors.New("multiple WriteHeader calls")
+ r.srv.logError(err)
+ return err
+ }
+ r.headerWritten = true
+ return r.resp.Header.Write(r)
+}
+
+func (r *responseWriter) Write(data []byte) (int, error) {
+ if !r.headerWritten {
+ r.WriteHeader()
+ }
+ n, err := r.w.Write(data)
+ r.n += int64(n)
+ return n, err
+}
+
+// ListenAndServe creates a new Server with a listen address and a handler and
+// calls its ListenAndServe method.
+func ListenAndServe(addr string, handler Handler) error {
+ srv := NewServer()
+ srv.Address = addr
+ srv.Handler = handler
+ return srv.ListenAndServe()
+}