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 }