// // Copyright (c) 2006, Brian Frank and Andy Frank // Licensed under the Academic Free License version 3.0 // // History: // 15 Mar 06 Andy Frank Creation // using inet ** ** WebReq encapsulates a web request. ** ** See [pod doc]`pod-doc#webReq`. ** abstract class WebReq { ** ** The HTTP request method in uppercase. Example: GET, POST, PUT. ** abstract Str method() ** ** Return if the method is GET ** abstract Bool isGet() ** ** Return if the method is POST ** abstract Bool isPost() ** ** The HTTP version of the request. ** abstract Version version() ** ** Get the IP host address of the client socket making this request. ** abstract IpAddr remoteAddr() ** ** Get the IP port of the client socket making this request. ** abstract Int remotePort() ** ** The request URI including the query string relative to ** this authority. Also see `absUri`, `modBase`, and `modRel`. ** ** Examples: ** /a/b/c ** /a?q=bar ** abstract Uri uri() ** ** The absolute request URI including the full authority ** and the query string. Also see `uri`, `modBase`, and `modRel`. ** This method is equivalent to: ** "http://" + headers["Host"] + uri ** ** Examples: ** http://www.foo.com/a/b/c ** http://www.foo.com/a?q=bar ** virtual once Uri absUri() { host := headers["Host"] if (host == null) throw Err("Missing Host header") return `http://${host}/` + uri } ** ** Get the WebMod which is currently responsible ** for processing this request. ** abstract WebMod mod ** ** Base URI of the current WebMod. This Uri always ends in a slash. ** This is the URI used to route to the WebMod itself. The remainder ** of `uri` is stored in `modRel` so that the following always ** holds true (with exception of a trailing slash): ** modBase + modRel == uri ** ** For example if the current WebMod is mounted as '/mod' then: ** uri modBase modRel ** ---------- ------- ------- ** `/mod` `/mod/` `` ** `/mod/` `/mod/` `` ** `/mod?q` `/mod/` `?q` ** `/mod/a` `/mod/` `a` ** `/mod/a/b` `/mod/` `a/b` ** Uri modBase := `/` { set { if (!it.isDir) throw ArgErr("modBase must end in slash"); if (it.path.size > uri.path.size) throw ArgErr("modBase ($it) is not slice of uri ($uri)"); &modBase = it modRelVal = uri.relTo(&modBase)//[it.path.size..-1] } } ** ** WebMod relative part of the URI - see `modBase`. ** Uri modRel() { modRelVal ?: uri } private Uri? modRelVal ** ** Map of HTTP request headers. The headers map is readonly ** and case insensitive (see `sys::Map.caseInsensitive`). ** ** Examples: ** req.headers["Accept-Language"] ** abstract Str:Str headers() ** ** Get the accepted locales for this request based on the ** "Accept-Language" HTTP header. List is sorted by preference, where ** 'locales.first' is best, and 'locales.last' is worst. This list is ** guarenteed to contain Locale("en"). ** virtual once Locale[] locales() { list := Locale[,] hval := headers["Accept-Language"] if (hval != null) { WebUtil.parseList(hval).each |val| { try { colon := val.index(";q=") qual := colon==null ? 1f : val[colon+3..-1].toFloat lang := colon==null ? val : val[0..<colon] loc := Locale.fromStr(lang, false) if (qual > 0f && loc != null && !list.contains(loc)) list.add(loc) } catch (Err err) { err.trace } } } // make sure we always have 'en' en := Locale("en") if (!list.contains(en)) list.add(en) return list } ** ** Map of cookie values keyed by cookie name. The ** cookies map is readonly and case insensitive. ** virtual once Str:Str cookies() { try return MimeType.parseParams(headers.get("Cookie", "")).ro catch (Err e) e.trace return Str:Str[:].ro } ** ** Get the session associated with this browser "connection". ** The session must be accessed the first time before the ** response is committed. ** abstract WebSession session() ** ** Get the key/value pairs of the form data. If the request ** content type is "application/x-www-form-urlencoded", then the ** first time this method is called the request content is read ** and parsed using `sys::Uri.decodeQuery`. If the content ** type is not "application/x-www-form-urlencoded" this method ** returns null. ** virtual once [Str:Str]? form() { ct := headers.get("Content-Type", "").lower if (ct.startsWith("application/x-www-form-urlencoded")) { len := headers["Content-Length"] if (len == null) throw IOErr("Missing Content-Length header") return Uri.decodeQuery(in.readLine(len.toInt)) } return null } ** ** Get the stream to read request body. See `WebUtil.makeContentInStream` ** to check under which conditions request content is available. ** If request content is not available, then throw an exception. ** ** If the client specified the "Expect: 100-continue" header, then the first ** access of the request input stream will automatically send the client ** a [100 Continue]`pod-doc#expectContinue` response. ** abstract InStream in() ** ** Access to socket options for this request. ** abstract SocketOptions socketOptions() ** ** Access to underlying socket - internal use only! ** @NoDoc abstract TcpSocket socket() ** ** Stash allows you to stash objects on the WebReq object ** in order to pass data b/w Weblets while processing ** this request. ** virtual Str:Obj? stash() { stashRef } private Str:Obj? stashRef := Str:Obj?["web.startTime":Duration.now] ** ** Given a web request: ** 1. check that the content-type is form-data ** 2. get the boundary string ** 3. invoke callback for each part (see `WebUtil.parseMultiPart`) ** ** For each part in the stream call the given callback function with ** the part's form name, headers, and an input stream used to read the ** part's body. ** Void parseMultiPartForm(|Str formName, InStream in, [Str:Str] headers| cb) { mime := MimeType(this.headers["Content-Type"]) if (mime.subType != "form-data") throw Err("Invalid content-type: $mime") boundary := mime.params["boundary"] ?: throw Err("Missing boundary param: $mime") WebUtil.parseMultiPart(this.in, boundary) |partHeaders, partIn| { cd := partHeaders["Content-Disposition"] ?: throw Err("Multi-part missing Content-Disposition") semi := cd.index(";") ?: throw Err("Expected semicolon; Content-Disposition: $cd") params := MimeType.parseParams(cd[cd.index(";")+1..-1]) formName := params["name"] ?: throw Err("Expected name param; Content-Disposition: $cd") cb(formName, partIn, partHeaders) try { partIn.skip(Int.maxVal) } catch {} // drain stream } } }