From da77deba78c8a7447b4a38324d2422a5df293b26 Mon Sep 17 00:00:00 2001 From: clsr Date: Fri, 18 Aug 2017 13:46:10 +0200 Subject: Initial commit --- request.go | 260 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 260 insertions(+) create mode 100644 request.go (limited to 'request.go') 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 +} -- cgit