summaryrefslogtreecommitdiffstats
path: root/request.go
diff options
context:
space:
mode:
Diffstat (limited to 'request.go')
-rw-r--r--request.go260
1 files changed, 260 insertions, 0 deletions
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
+}