RFC6265 用于 Node.js 的 Cookie 与 CookieJar
var tough = require('tough-cookie'); var Cookie = tough.Cookie; var cookie = Cookie.parse(header); cookie.value = 'somethingdifferent'; header = cookie.toString();
var cookiejar = new tough.CookieJar(); cookiejar.setCookie(cookie, 'http://currentdomain.example.com/path', cb); // ... cookiejar.getCookies('http://example.com/otherpath',function(err,cookies) { res.headers['cookie'] = cookies.join('; '); });
# 安装
这 _太_ 容易了!
`npm install tough-cookie`
为什么叫这个名字? NPM 模块 `cookie`、`cookies` 和 `cookiejar` 都已被占用。
## 版本支持
对 node.js 版本的支持将遵循 [request](https://www.npmjs.com/package/request) 模块的支持策略。
# API
## tough
`require('tough-cookie')` 返回的模块上的函数。 所有这些函数都可以作为纯函数使用,无需"绑定"。
**注意**:在 1.0.x 之前,其中几个函数接受 `strict` 参数。由于该参数已不再必要,因此已从 API 中移除。
### `parseDate(string)`
将 cookie 日期字符串解析为 `Date` 对象。 解析遵循 RFC6265 第 5.1.1 节,而不是 `Date.parse()`。
### `formatDate(date)`
将 Date 格式化为 RFC1123 字符串(RFC6265 推荐的格式)。
### `canonicalDomain(str)`
将域名转换为规范域名。 规范域名是去除首尾空白、转换为小写、去除前导点号并可选用 punycode 编码的域名(RFC6265 第 5.1.2 节)。 在大多数情况下,此函数是幂等的(可以对其输出再次运行而不会产生不良影响)。
### `domainMatch(str,domStr[,canonicalize=true])`
回答"这个真实域名是否匹配 cookie 中的域名?"。 `str` 是"当前"域名,`domStr` 是"cookie"域名。 匹配遵循 RFC6265 第 5.1.3 节,但可以把它理解为"后缀匹配"。
`canonicalize` 参数决定是否将另外两个参数交给 `canonicalDomain` 处理。
### `defaultPath(path)`
给定当前的请求/响应路径,返回适合存储在 cookie 中的 Path。 这基本上就是路径中"文件"的"目录",但这由 RFC 第 5.1.4 节规定。
`path` 参数必须 _只是_ URI 的路径名部分(即排除主机名、查询、片段等)。 这是 node 的 `uri.parse()` 输出中的 `.pathname` 属性。
### `pathMatch(reqPath,cookiePath)`
按照 RFC6265 第 5.1.4 节,回答"请求路径是否路径匹配给定的 cookie 路径?"。 返回布尔值。
这本质上是一种前缀匹配,其中 `cookiePath` 是 `reqPath` 的前缀。
### `parse(cookieString[, options])`
`Cookie.parse(cookieString[, options])` 的别名
### `fromJSON(string)`
`Cookie.fromJSON(string)` 的别名
### `getPublicSuffix(hostname)`
返回此主机名的公共后缀。 公共后缀是可以设置 cookie 的最短域名。 如果无法为该主机名设置 cookie,则返回 `null`。
例如:`www.example.com` 和 `www.subdomain.example.com` 的公共后缀都是 `example.com`。
更多信息,请参阅 http://publicsuffix.org/。 此模块的列表源自该网站。此调用目前是对 [`psl`](https://www.npmjs.com/package/psl) 的 [get() 方法](https://www.npmjs.com/package/psl#pslgetdomain) 的封装。
### `cookieCompare(a,b)`
与 `.sort()` 配合使用,将 cookie 列表按 RFC 建议的顺序(第 5.4 节第 2 步)排序。排序算法按优先级顺序如下:
* `.path` 最长
* `.creation` 最早(精度为 1ms,与 `Date` 相同)
* `.creationIndex` 最小(以突破 1ms 精度限制)``` javascript
var cookies = [ /* unsorted array of Cookie objects */ ];
cookies = cookies.sort(cookieCompare);
注意:由于 JavaScript 的 Date 精度被限制为 1 毫秒,因此同一个毫秒内完全可能出现多个 cookie。当使用 .setCookie() 的 now 选项时尤其如此。.creationIndex 属性是一个进程级全局计数器,在通过 new Cookie() 构造时分配。这保留了 RFC 排序的精神:较早的 cookie 排在前面。这对于 MemoryCookieStore 来说效果很好,因为 Set-Cookie 头是按顺序解析的,但对于分布式系统可能就不那么好了。精密的 Store 可能希望将此设置为某种其他的 逻辑时钟,以便如果 cookie A 和 B 在同一毫秒内创建,但 cookie A 先于 cookie B 创建,则 A.creationIndex < B.creationIndex。如果你想更改全局计数器(你大概 不应该 这么做),它存储在 Cookie.cookiesCreated 中。
permuteDomain(domain)生成一个列表,包含所有可能通过 domainMatch() 匹配该参数的域。对于实现 cookie 存储可能很有用。
permutePath(path)生成一个列表,包含所有可能通过 pathMatch() 匹配该参数的路径。对于实现 cookie 存储可能很有用。
通过 tough.Cookie 导出。
Cookie.parse(cookieString[, options])将单个 Cookie 或 Set-Cookie HTTP 头解析为 Cookie 对象。如果字符串无法解析,则返回 undefined。
options 参数不是必需的,目前只有一个属性:
true,则启用对无键 cookie 的解析(例如 =abc 和 =),这些 cookie 不符合 RFC 规范。如果 options 不是对象,则会被忽略,这意味着你可以将其与 Array#map 一起使用。
以下是在 node HTTP/HTTPS 响应中处理 Set-Cookie 头的方法:``` javascript if (res.headers['set-cookie'] instanceof Array) cookies = res.headers['set-cookie'].map(Cookie.parse); else cookies = [Cookie.parse(res.headers['set-cookie'])];
_注意:_ 在 2.3.3 版本中,tough-cookie 限制了 `=` 之前的空格数为 256 个字符。此限制之后已被移除。
请参阅 [Issue 92](https://github.com/salesforce/tough-cookie/issues/92)
### 属性
Cookie 对象的属性:
* _key_ - 字符串 - cookie 的名称或键(默认值为 "")
* _value_ - 字符串 - cookie 的值(默认值为 "")
* _expires_ - `Date` - 若设置,则为 cookie 的 `Expires=` 属性(默认字符串为 `"Infinity"`)。参见 `setExpires()`
* _maxAge_ - 秒 - 若设置,则为 cookie 的 `Max-Age=` 属性(以_秒_为单位)。也可分别设置为字符串 `"Infinity"` 和 `"-Infinity"`,表示永不过期和立即过期。参见 `setMaxAge()`
* _domain_ - 字符串 - cookie 的 `Domain=` 属性
* _path_ - 字符串 - cookie 的 `Path=` 属性
* _secure_ - 布尔值 - `Secure` cookie 标志
* _httpOnly_ - 布尔值 - `HttpOnly` cookie 标志
* _extensions_ - `Array` - 任何无法识别的 cookie 属性,以字符串形式表示(即使其中包含等号)
* _creation_ - `Date` - 此 cookie 被构造的时间
* _creationIndex_ - 数值 - 在构造时设置,用于提供更高的排序精度(完整说明请参阅 `cookieCompare(a,b)`)
在 cookie 经过 `CookieJar.setCookie()` 处理后,它还将具有以下附加属性:
* _hostOnly_ - 布尔值 - 是否为仅主机(host-only)cookie(即未设置 Domain 字段,而是隐含存在)
* _pathIsDefault_ - 布尔值 - 若为 true,则表示 cookie 上没有 Path 字段,并且使用 `defaultPath()` 推导出了该字段。
* _creation_ - `Date` - 从构造时间**修改**为 cookie 被添加到 jar 的时间
* _lastAccessed_ - `Date` - cookie 最后一次被访问的时间。将来实现时会用于 cookie 清理。使用 `cookiejar.getCookies(...)` 将更新此属性。
### `Cookie([{properties}])`
接收一个选项对象,该对象可以包含上述任何 Cookie 属性;未指定的属性将使用默认值。
### `.toString()`
编码为 Set-Cookie 头部的值。Expires cookie 字段使用 `formatDate()` 设置,但如果 `.expires` 为 `Infinity`,则完全省略该字段。
### `.cookieString()`
编码为 Cookie 头部的值(即 `.key` 和 `.value` 属性以 '=' 连接)。
### `.setExpires(String)`
根据通过 `parseDate()` 传入的日期字符串设置过期时间。如果 parseDate 返回 `null`(即无法解析该日期字符串),则 `.expires` 会被设置为字符串 `"Infinity"`。
### `.setMaxAge(number)`
以秒为单位设置 maxAge。将 `-Infinity` 强制转换为 `"-Infinity"`,将 `Infinity` 强制转换为 `"Infinity"`,以便 JSON 序列化正确。
### `.expiryTime([now=Date.now()])`
### `.expiryDate([now=Date.now()])`
expiryTime() 计算此 cookie 过期的绝对 Unix 纪元毫秒数。expiryDate() 的工作方式类似,区别在于它返回一个 `Date` 对象。请注意,在这两种情况下,`now` 参数都应为毫秒。
Max-Age 优先于 Expires(按照 RFC 规定)。`.creation` 属性 -- 或默认情况下的 `now` 参数 -- 用于偏移 `.maxAge` 属性。
如果设置了 Expires(`.expires`),则返回该值。
否则,`expiryTime()` 返回 `Infinity`,`expiryDate()` 返回 "Tue, 19 Jan 2038 03:14:07 GMT" 对应的 `Date` 对象(这是 32 位 `time_t` 能表示的最新日期;也是大多数用户代理的常见上限)。
### `.TTL([now=Date.now()])`
计算相对于 `now`(毫秒)的 TTL。与 `expiryTime`/`expiryDate` 相同的优先级规则生效。
对于没有显式过期时间的 cookie,返回数值 `Infinity`;如果 cookie 已过期,则返回 `0`。否则返回以毫秒为单位的生存时间。
### `.canonicalizedDomain()`
### `.cdomain()`
返回规范化后的 `.domain` 字段。如果域包含任何非 ASCII 字符,则会进行小写化并进行 punycode(RFC3490)编码。
### `.toJSON()`
为了便于使用 `JSON.serialize(cookie)`,返回一个可被 JSON 序列化的普通 `Object`。
所有 `Date` 属性(即 `.expires`、`.creation` 和 `.lastAccessed`)均以 ISO 格式(`.toISOString()`)导出。
**注意**:自定义的 `Cookie` 属性将被丢弃。在 tough-cookie 1.x 中,由于没有显式定义 `.toJSON` 方法,所有可枚举属性都会被捕获。如果你希望某个属性被序列化,请将该属性名称添加到 `Cookie.serializableProperties` 数组中。
### `Cookie.fromJSON(strOrObj)`
与 `cookie.toJSON()` 相反。如果传入的是字符串,将先对其进行 `JSON.parse()`。
所有 `Date` 属性(即 `.expires`、`.creation` 和 `.lastAccessed`)都通过 `Date.parse()` 解析,而不是 tough-cookie 的 `parseDate`,因为在当前这一层处理的是 JavaScript/JSON 风格的时间戳。
如果 JSON 解析出错,则返回 `null`。
### `.clone()`
深克隆此 cookie,实现方式完全等同于 `Cookie.fromJSON(cookie.toJSON())`。
### `.validate()`
状态:*进行中*。可用于部分场景,但远非全面。
验证 cookie 属性在语义上的正确性。可用于对生成的任何 Set-Cookie 头部进行 "lint" 检查。目前它返回布尔值,但最终可能会返回原因字符串——你可以使用以下构造来为未来做准备:``` javascript
if (cookie.validate() === true) {
// it's tasty
} else {
// yuck!
}
通过 tough.CookieJar 导出。
CookieJar([store],[options])只需使用 new CookieJar()。 如果您希望使用自定义存储,请将其传递给构造函数,否则将创建并使用 MemoryCookieStore。
options 对象可以省略,并且可以具有以下属性:
true - 拒绝域名类似 "com" 和 "co.uk" 的 Cookiefalse - 接受像 bar 和 =bar 这样的畸形 Cookie,它们隐含一个空名称。
这不符合标准,但在网络上有时会用到,并且(大多数)浏览器会接受。由于该模块最终希望支持数据库/远程等 CookieJar,因此 CookieJar 方法采用延续传递风格。
.setCookie(cookieOrString, currentUrl, [{options},] cb(err,cookie))尝试在 Cookie jar 中设置 Cookie。如果操作失败,错误将传递给回调 cb,否则该 Cookie 会被传递出去。Cookie 的 .creation、.lastAccessed 和 .hostOnly 属性将得到更新。
options 对象可以省略,并且可以具有以下属性:
true - 指示这是 HTTP 还是非 HTTP API。影响 HttpOnly Cookie。https: 或 wss: 开头,则默认为 true,否则为 false。new Date() - 用于 Cookie 的创建/访问时间false - 静默忽略解析错误和无效域名等问题。此选项不会忽略 Store 错误。根据 RFC,如果 Cookie 字符串中没有“Domain=”参数(或 Cookie 对象上的 .domain 为 null),则设置 .hostOnly 属性。在这种情况下,.domain 属性被设置为 currentUrl 的完全限定主机名。匹配此 Cookie 需要精确的主机名匹配(而不是通常的 domainMatch)。
.setCookieSync(cookieOrString, currentUrl, [{options}])setCookie 的同步版本;仅适用于同步存储(例如默认的 MemoryCookieStore)。
.getCookies(currentUrl, [{options},] cb(err,cookies))获取当前 url 的 Cookie 标头中可发送的 Cookie 列表。
如果遇到错误,该错误将以 err 形式传递给回调,否则将传递一个 Cookie 对象的 Array。除非给出 {sort:false} 选项,否则该数组会使用 cookieCompare() 进行排序。
options 对象可以省略,并且可以具有以下属性:
true - 指示这是 HTTP 还是非 HTTP API。影响 HttpOnly Cookie。https: 或 wss: 开头,则默认为 true,否则为 false。new Date() - 用于 Cookie 的创建/访问时间true - 对 Cookie 执行过期时间检查,并从存储中异步移除已过期的 Cookie。使用 false 将返回已过期的 Cookie,并且不会将它们从存储中移除(这对于重放 Set-Cookie 标头可能很有用)。false - 如果为 true,则不要按路径限定 Cookie 的作用域。默认使用符合 RFC 的路径作用域。注意:底层存储可能不支持此选项(默认的 MemoryCookieStore 支持它)。返回的 Cookie 的 .lastAccessed 属性将已被更新。
.getCookiesSync(currentUrl, [{options}])getCookies 的同步版本;仅适用于同步存储(例如默认的 MemoryCookieStore)。
.getCookieString(...)接受与 .getCookies() 相同的选项,但向回调传递适合 Cookie 标头的字符串而不是数组。只需通过 .cookieString() 映射 Cookie 数组即可。
.getCookieStringSync(...)getCookieString 的同步版本;仅适用于同步存储(例如默认的 MemoryCookieStore)。
.getSetCookieStrings(...)返回适合 Set-Cookie 标头的字符串数组。接受与 .getCookies() 相同的选项。只需通过 .toString() 映射 Cookie 数组即可。
.getSetCookieStringsSync(...)getSetCookieStrings 的同步版本;仅适用于同步存储(例如默认的 MemoryCookieStore)。
.serialize(cb(err,serializedObject))如果底层存储支持 .getAllCookies,则序列化 Jar。
注意:自定义的 Cookie 属性将被丢弃。如果希望某个属性被序列化,请将该属性名添加到 Cookie.serializableProperties 数组中。
参见 [序列化格式]。
.serializeSync().serialize 的同步版本。
.toJSON().serializeSync() 的别名,方便使用 JSON.stringify(cookiejar)。
CookieJar.deserialize(serialized, [store], cb(err,object))将创建一个新的 Jar,并将序列化的 Cookie 添加到底层存储中。每个 Cookie 都会按照它们在序列化中出现的顺序,通过 store.putCookie 添加。
store 参数是可选的,但应该是 Store 的实例。默认情况下,会创建一个新的 MemoryCookieStore 实例。
为方便起见,如果 serialized 是字符串,则首先通过 JSON.parse 解析。如果解析抛出错误,则该错误会传递给回调。
CookieJar.deserializeSync(serialized, [store]).deserialize 的同步版本。注意 store 必须是同步的,此功能才能正常工作。
CookieJar.fromJSON(string).deserializeSync 的别名,以与 Cookie.fromJSON() 保持一致。
.clone([store,]cb(err,newJar))生成此 jar 的深拷贝。对原始 jar 的修改不会影响克隆,反之亦然。
store 参数是可选的,但应该是 Store 的实例。默认情况下,会创建一个新的 MemoryCookieStore 实例。只要源实现 .getAllCookies() 且目标实现 .putCookie(),就支持在不同 store 类型之间进行转移。
.cloneSync([store]).clone 的同步版本,返回一个新的 CookieJar 实例。
store 参数是可选的,但如果指定,则必须是 同步 的 Store 实例。如果未传递,则使用新的 MemoryCookieStore 实例。
source 和 destination 都必须是同步的 Store。如果其中一个或两个存储都是异步的,请改用 .clone。请记住,MemoryCookieStore 同时支持同步和异步 API 调用。
.removeAllCookies(cb(err))从 jar 中移除所有 Cookie。
这是 tough-cookie 2.5 版本新增的向后兼容功能,因此并非所有 Store 都会高效地实现它。对于未实现 removeAllCookies 的 Store,回退方案是在 getAllCookies 之后调用 removeCookie。如果 getAllCookies 失败或未在 Store 中实现,则返回该错误。如果一次或多次 removeCookie 调用失败,则仅返回第一个错误。
.removeAllCookiesSync().removeAllCookies() 的同步版本。
CookieJar 存储的基类。可通过 tough.Store 获取。
每个 CookieJar 实例的存储模型都可以替换为自定义实现。默认的是 MemoryCookieStore,可在 lib/memstore.js 文件中找到。该 API 使用延续传递风格以支持异步存储。
Store 应继承自基类 Store,可通过 require('tough-cookie').Store 获取。
Store 默认是异步的,但如果将 store.synchronous 设置为 true,则可以使用包含该 store 的 CookieJar 上的 *Sync 方法(然而,延续传递风格
所有 domain 参数在调用之前都将已经过标准化。
Cookie 存储必须包含以下所有方法。
store.findCookie(domain, path, key, cb(err,cookie))检索具有给定 domain、path 和 key(又名名称)的 Cookie。RFC 规定,此类 Cookie 在存储中应恰好存在一个。如果存储使用了版本控制,这意味着应返回最新/最新的此类 Cookie。
回调接收一个错误和结果 Cookie 对象。如果未找到 Cookie,则必须传递 null(即不是错误)。
store.findCookies(domain, path, cb(err,cookies))定位与给定 domain 和 path 匹配的 Cookie。这通常在上述 cookiejar.getCookies() 的上下文中调用。
如果未找到 Cookie,则必须向回调传递一个空数组。
得到的列表将根据 RFC(域匹配、路径匹配、仅 HTTP 标志、安全标志、过期等)检查其是否适用于当前请求,因此在实现此方法时可以使用乐观搜索算法。但是,所使用的搜索算法应尝试查找与 domain 匹配 domainMatch() 且与 path 匹配 pathMatch() 的 Cookie,以减少需要执行的检查量。
自版本 0.9.12 起,上述 cookiejar.getCookies() 的 allPaths 选项将使此处的 path 为 null。如果 path 为 null,则不得执行路径匹配(即仅进行域匹配)。
store.putCookie(cookie, cb(err))向存储中添加新的 Cookie。实现应替换任何具有相同 .domain、.path 和 .key 属性的现有 Cookie —— 根据实现的性质,在调用 fetchCookie 和 putCookie 之间可能会发生重复的 putCookie。
cookie 对象不得被修改;调用者已经更新了 .creation 和 .lastAccessed 属性。
如果无法存储该 Cookie,则传递错误。
store.updateCookie(oldCookie, newCookie, cb(err))更新现有 Cookie。实现必须更新具有相同 domain、.path 和 .key 的 Cookie 的 .value。实现应检查存储中的旧值是否与 oldCookie 等价 - 如何解决冲突取决于存储。
两个对象之间的 .lastAccessed 属性总是不同的(在 JavaScript 时钟所能达到的精度范围内)。.creation 和 .creationIndex 都保证相同。存储可以忽略或延迟 .lastAccessed 的更改,但这会影响 Cookie 被选择自动删除的方式(例如,最近最少使用,这由存储自行实现)。
存储可能希望优化更改存储中 Cookie 的 .value,而不是存储新 Cookie。如果实现未定义此方法,则会在 store 对象上添加一个调用 putCookie(newCookie,cb) 的存根。
newCookie 和 oldCookie 对象不得被修改。
如果无法存储 newCookie,则传递错误。
store.removeCookie(domain, path, key, cb(err))从存储中移除 Cookie(参见上面关于唯一性约束的 findCookie 说明)。
如果 Cookie 不存在,实现不得传递错误;只有在移除现有 Cookie 失败时才传递错误。
store.removeCookies(domain, path, cb(err))从存储中移除匹配的 Cookie。path 参数是可选的,如果缺少,则意味着应移除该域中的所有路径。
仅当移除任何现有 Cookie 失败时才传递错误。
store.removeAllCookies(cb(err))可选。从存储中移除所有 Cookie。
如果无法移除一个或多个 Cookie,则传递错误。
注意:这是 tough-cookie 2.5 版本新增的方法,因此并非所有 Store 都会实现它,而且有些存储可能选择不实现它。
store.getAllCookies(cb(err, cookies))可选。在 jar.serialize() 期间生成所有 Cookie 的 Array。数组中的项可以是真正的 Cookie 对象,也可以是具有 [序列化格式] 数据结构的通用 Object。
Cookie 应按照创建顺序返回,以通过 compareCookies() 保持排序。供参考,MemoryCookieStore 会按 .creationIndex 排序,因为它在内部使用真正的 Cookie 对象。如果您不按创建顺序返回 Cookie,它们仍会按创建时间排序,但这只有 1ms 的精度。有关更多详细信息,请参阅 compareCookies。
如果检索失败,则传递错误。
注意:由于技术限制,并非所有 Store 都能实现此方法,因此它是可选的。
继承自 Store。
一个纯内存的 CookieJar 同步存储实现,默认使用。尽管是同步实现,它可以与 CookieJar API 的同步和异步形式一起使用。支持序列化、getAllCookies 和 removeAllCookies。
以下是一些由社区编写和维护的 Store 实现。它们不是官方实现,我们也不为其背书,但您可能有兴趣看一下:
db-cookie-store: SQL,包括基于 SQLite 的数据库file-cookie-store: 磁盘上的 Netscape cookie 文件格式redis-cookie-store: Redistough-cookie-filestore: 磁盘上的 JSONtough-cookie-web-storage-store: DOM localStorage 和 sessionStorage注意:如果您希望自定义 Cookie 属性被序列化,请将属性名称添加到 Cookie.serializableProperties。```js
{
// The version of tough-cookie that serialized this jar.
version: '[email protected]',
// add the store type, to make humans happy:
storeType: 'MemoryCookieStore',
// CookieJar configuration:
rejectPublicSuffixes: true,
// ... future items go here
// Gets filled from jar.store.getAllCookies():
cookies: [
{
key: 'string',
value: 'string',
// ...
/* other Cookie.serializableProperties go here */
}
]
}
# 版权与许可证
BSD-3-Clause:```text
Copyright (c) 2015, Salesforce.com, Inc.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of Salesforce.com nor the names of its contributors may
be used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.