Peter Alcock 8 months ago
parent
commit
135900f980
100 changed files with 44358 additions and 0 deletions
  1. 11 0
      Dockerfile
  2. 4 0
      http-proxy-to-socks/.babelrc
  3. 14 0
      http-proxy-to-socks/.eslintrc
  4. 27 0
      http-proxy-to-socks/.github/workflows/test.yaml
  5. 3 0
      http-proxy-to-socks/.gitignore
  6. 4 0
      http-proxy-to-socks/.npmignore
  7. 5 0
      http-proxy-to-socks/.travis.yml
  8. 18 0
      http-proxy-to-socks/CHANGELOG.md
  9. 21 0
      http-proxy-to-socks/LICENSE.md
  10. 47 0
      http-proxy-to-socks/README.md
  11. 2 0
      http-proxy-to-socks/bin/hpts.js
  12. 6722 0
      http-proxy-to-socks/package-lock.json
  13. 50 0
      http-proxy-to-socks/package.json
  14. 26 0
      http-proxy-to-socks/src/__tests__/cli.spec.js
  15. 264 0
      http-proxy-to-socks/src/__tests__/proxy_server.spec.js
  16. 7 0
      http-proxy-to-socks/src/__tests__/server.spec.js
  17. 73 0
      http-proxy-to-socks/src/cli.js
  18. 68 0
      http-proxy-to-socks/src/logger.js
  19. 197 0
      http-proxy-to-socks/src/proxy_server.js
  20. 28 0
      http-proxy-to-socks/src/server.js
  21. 7 0
      polipo/.gitignore
  22. 7 0
      polipo/.hgignore
  23. 563 0
      polipo/CHANGES
  24. 19 0
      polipo/COPYING
  25. 72 0
      polipo/INSTALL
  26. 142 0
      polipo/Makefile
  27. 27 0
      polipo/README
  28. 38 0
      polipo/README.Windows
  29. 379 0
      polipo/atom.c
  30. 58 0
      polipo/atom.h
  31. 91 0
      polipo/auth.c
  32. 24 0
      polipo/auth.h
  33. 469 0
      polipo/chunk.c
  34. 51 0
      polipo/chunk.h
  35. 2155 0
      polipo/client.c
  36. 64 0
      polipo/client.h
  37. 869 0
      polipo/config.c
  38. 68 0
      polipo/config.h
  39. 165 0
      polipo/config.sample
  40. 149 0
      polipo/dirent_compat.c
  41. 50 0
      polipo/dirent_compat.h
  42. 2561 0
      polipo/diskcache.c
  43. 71 0
      polipo/diskcache.h
  44. 1756 0
      polipo/dns.c
  45. 48 0
      polipo/dns.h
  46. 834 0
      polipo/event.c
  47. 90 0
      polipo/event.h
  48. 833 0
      polipo/forbidden.c
  49. 50 0
      polipo/forbidden.h
  50. 18 0
      polipo/forbidden.sample
  51. 365 0
      polipo/fts_compat.c
  52. 69 0
      polipo/fts_compat.h
  53. 12 0
      polipo/ftsimport.c
  54. 6 0
      polipo/ftsimport.h
  55. 1094 0
      polipo/http.c
  56. 184 0
      polipo/http.h
  57. 1551 0
      polipo/http_parse.c
  58. 58 0
      polipo/http_parse.h
  59. 1171 0
      polipo/io.c
  60. 161 0
      polipo/io.h
  61. 746 0
      polipo/local.c
  62. 48 0
      polipo/local.h
  63. 13 0
      polipo/localindex.html
  64. 496 0
      polipo/log.c
  65. 140 0
      polipo/log.h
  66. 171 0
      polipo/main.c
  67. 322 0
      polipo/md5.c
  68. 60 0
      polipo/md5.h
  69. 15 0
      polipo/md5import.c
  70. 9 0
      polipo/md5import.h
  71. 505 0
      polipo/mingw.c
  72. 195 0
      polipo/mingw.h
  73. 1040 0
      polipo/object.c
  74. 197 0
      polipo/object.h
  75. 215 0
      polipo/parse_time.c
  76. 27 0
      polipo/parse_time.h
  77. 232 0
      polipo/polipo.h
  78. 93 0
      polipo/polipo.man
  79. 1964 0
      polipo/polipo.texi
  80. 2895 0
      polipo/server.c
  81. 113 0
      polipo/server.h
  82. 605 0
      polipo/socks.c
  83. 36 0
      polipo/socks.h
  84. 574 0
      polipo/tunnel.c
  85. 50 0
      polipo/tunnel.h
  86. 838 0
      polipo/util.c
  87. 107 0
      polipo/util.h
  88. 188 0
      privoxy/AUTHORS
  89. 2783 0
      privoxy/ChangeLog
  90. 1114 0
      privoxy/GNUmakefile.in
  91. 160 0
      privoxy/INSTALL
  92. 339 0
      privoxy/LICENSE
  93. 674 0
      privoxy/LICENSE.GPLv3
  94. 77 0
      privoxy/Makefile
  95. 323 0
      privoxy/README
  96. 489 0
      privoxy/TODO
  97. 273 0
      privoxy/acconfig.h
  98. 156 0
      privoxy/actionlist.h
  99. 1990 0
      privoxy/actions.c
  100. 96 0
      privoxy/actions.h

+ 11 - 0
Dockerfile

@@ -0,0 +1,11 @@
+FROM ubuntu:16.04
+RUN apt update
+ENV DEBIAN_INTERFACE=noninteractive
+RUN apt install -y privoxy polipo netcat tor haproxy nodejs npm gcc g++ make cmake autoconf automake autogen clang build-essential
+RUN apt install -y curl libcurl4-openssl-dev libpcap-dev masscan nmap wget unzip tcpdump squid3 dnsutils net-tools apt-transport-tor 
+RUN apt install -y apt-transport-https python python-dev python-pip python3 python3-dev python3-pip ruby ruby-dev golang ufw
+RUN pip install awscli
+RUN mkdir -p /app/proxy
+WORKDIR /app/proxy
+COPY . .
+RUN npm install http-to-socks-proxy

+ 4 - 0
http-proxy-to-socks/.babelrc

@@ -0,0 +1,4 @@
+{
+  "presets": [ "es2015" ],
+  "plugins": ["transform-async-to-generator"]
+}

+ 14 - 0
http-proxy-to-socks/.eslintrc

@@ -0,0 +1,14 @@
+{
+  "extends": "airbnb/base",
+  "globals": {
+    "expect": true,
+    "it": true,
+    "describe": true,
+    "beforeEach": true,
+    "jest": true
+  },
+  "rules": {
+    "no-underscore-dangle": 0,
+    "comma-dangle": 0
+  }
+}

+ 27 - 0
http-proxy-to-socks/.github/workflows/test.yaml

@@ -0,0 +1,27 @@
+name: Node.js CI
+
+on:
+  push:
+    branches: [ master ]
+  pull_request:
+    branches: [ master ]
+
+jobs:
+  test:
+    runs-on: ${{ matrix.platform }}
+
+    strategy:
+      fail-fast: true
+      matrix:
+        platform: [ubuntu-20.04]
+        node-version: [10.x, 12.x, 14.x, 16.x, 18.x]
+
+    steps:
+      - uses: actions/checkout@v3
+      - name: Use Node.js ${{ matrix.node-version }}
+        uses: actions/setup-node@v3
+        with:
+          node-version: ${{ matrix.node-version }}
+      - run: npm ci
+      - run: npm run build --if-present
+      - run: npm test

+ 3 - 0
http-proxy-to-socks/.gitignore

@@ -0,0 +1,3 @@
+node_modules
+test.json
+lib/

+ 4 - 0
http-proxy-to-socks/.npmignore

@@ -0,0 +1,4 @@
+node_modules
+test.json
+lib/**/__tests__
+src/

+ 5 - 0
http-proxy-to-socks/.travis.yml

@@ -0,0 +1,5 @@
+language: node_js
+node_js:
+  - "node"
+  - "10"
+  - "8"

+ 18 - 0
http-proxy-to-socks/CHANGELOG.md

@@ -0,0 +1,18 @@
+# CHANGELOG
+
+## 1.1.1
+- src: Use parsed url hostname instead of host which may contain also port
+
+## 1.1.0
+- src: Support setting local address of the http-service.
+- src: Format timestamp of logs.
+
+## 1.0.4
+- src: Support setting the `--level` option in `createServer()`.
+
+## 1.0.3
+- src: Bind `error` event on sockets to prevent servers from exiting in some node runtimes.
+
+## 1.0.2
+
+- Exports "main" fields in `package.json`.

+ 21 - 0
http-proxy-to-socks/LICENSE.md

@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2017-present Ouyang Yadong
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 47 - 0
http-proxy-to-socks/README.md

@@ -0,0 +1,47 @@
+# http-proxy-to-socks
+
+hpts(http-proxy-to-socks) is a nodejs tool to convert SOCKS proxy into http proxy.
+
+Many clients support setting up http proxy to speed up network requests and for sometimes only SOCKS proxy is available to you. SOCKS proxy supports TCP so that it's possible to convert those requests from http proxy into SOCKS protocol. In this way, you can still keep the goodness provided by your SOCKS proxy(e.g. encryption).
+
+## Setup
+
+```
+npm install -g http-proxy-to-socks
+```
+
+Make sure your nodejs version is greater than `4`.
+
+## Usage
+
+```
+hpts -s 127.0.0.1:1080 -p 8080
+```
+
+This will start a process listening on `8080` as a http proxy. It will convert http requests into socks requests and send them to port `1080`. Please make sure your socks service is available at the corresponding port.
+
+Other options:
+
+```
+Options:
+  -V, --version          output the version number
+  -s, --socks [socks]    specify your socks proxy host, default: 127.0.0.1:1080
+  -p, --port [port]      specify the listening port of http proxy server, default: 8080
+  -l, --host [host]      specify the listening host of http proxy server, default: 127.0.0.1
+  -c, --config [config]  read configs from file in json format
+  --level [level]        log level, vals: info, error
+  -h, --help             output usage information
+```
+
+You can specify a `json` config file with `-c`:
+
+```json
+{
+  "socks": "127.0.0.1:1080",
+  "port": 8080
+}
+```
+
+## TESTING
+
+npm run test

+ 2 - 0
http-proxy-to-socks/bin/hpts.js

@@ -0,0 +1,2 @@
+#! /usr/bin/env node
+require('../lib/cli').main();

+ 6722 - 0
http-proxy-to-socks/package-lock.json

@@ -0,0 +1,6722 @@
+{
+  "name": "http-proxy-to-socks",
+  "version": "1.1.0",
+  "lockfileVersion": 1,
+  "requires": true,
+  "dependencies": {
+    "abab": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/abab/-/abab-1.0.4.tgz",
+      "integrity": "sha1-X6rZwsB/YN12dw9xzwJbYqY8/U4=",
+      "dev": true
+    },
+    "acorn": {
+      "version": "5.7.3",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz",
+      "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==",
+      "dev": true
+    },
+    "acorn-globals": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-3.1.0.tgz",
+      "integrity": "sha1-/YJw9x+7SZawBPqIDuXUZXOnMb8=",
+      "dev": true,
+      "requires": {
+        "acorn": "^4.0.4"
+      },
+      "dependencies": {
+        "acorn": {
+          "version": "4.0.13",
+          "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz",
+          "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=",
+          "dev": true
+        }
+      }
+    },
+    "acorn-jsx": {
+      "version": "3.0.1",
+      "resolved": "http://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz",
+      "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=",
+      "dev": true,
+      "requires": {
+        "acorn": "^3.0.4"
+      },
+      "dependencies": {
+        "acorn": {
+          "version": "3.3.0",
+          "resolved": "http://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz",
+          "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=",
+          "dev": true
+        }
+      }
+    },
+    "ajv": {
+      "version": "4.11.8",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz",
+      "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=",
+      "dev": true,
+      "requires": {
+        "co": "^4.6.0",
+        "json-stable-stringify": "^1.0.1"
+      }
+    },
+    "ajv-keywords": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-1.5.1.tgz",
+      "integrity": "sha1-MU3QpLM2j609/NxU7eYXG4htrzw=",
+      "dev": true
+    },
+    "ansi-escapes": {
+      "version": "1.4.0",
+      "resolved": "http://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz",
+      "integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4=",
+      "dev": true
+    },
+    "ansi-regex": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+      "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
+      "dev": true
+    },
+    "ansi-styles": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+      "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+      "dev": true
+    },
+    "ansicolors": {
+      "version": "0.3.2",
+      "resolved": "https://registry.npmjs.org/ansicolors/-/ansicolors-0.3.2.tgz",
+      "integrity": "sha1-ZlWX3oap/+Oqm/vmyuXG6kJrSXk=",
+      "dev": true
+    },
+    "anymatch": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz",
+      "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "micromatch": "^2.1.5",
+        "normalize-path": "^2.0.0"
+      }
+    },
+    "append-transform": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-0.4.0.tgz",
+      "integrity": "sha1-126/jKlNJ24keja61EpLdKthGZE=",
+      "dev": true,
+      "requires": {
+        "default-require-extensions": "^1.0.0"
+      }
+    },
+    "argparse": {
+      "version": "1.0.10",
+      "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+      "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+      "dev": true,
+      "requires": {
+        "sprintf-js": "~1.0.2"
+      }
+    },
+    "arr-diff": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz",
+      "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=",
+      "dev": true,
+      "requires": {
+        "arr-flatten": "^1.0.1"
+      }
+    },
+    "arr-flatten": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz",
+      "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==",
+      "dev": true
+    },
+    "arr-union": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz",
+      "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=",
+      "dev": true,
+      "optional": true
+    },
+    "array-equal": {
+      "version": "1.0.0",
+      "resolved": "http://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz",
+      "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=",
+      "dev": true
+    },
+    "array-unique": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz",
+      "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=",
+      "dev": true
+    },
+    "arrify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz",
+      "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=",
+      "dev": true
+    },
+    "asn1": {
+      "version": "0.2.4",
+      "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
+      "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==",
+      "dev": true,
+      "requires": {
+        "safer-buffer": "~2.1.0"
+      }
+    },
+    "assert-plus": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
+      "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=",
+      "dev": true
+    },
+    "assign-symbols": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz",
+      "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=",
+      "dev": true,
+      "optional": true
+    },
+    "async": {
+      "version": "2.6.1",
+      "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz",
+      "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==",
+      "requires": {
+        "lodash": "^4.17.10"
+      }
+    },
+    "async-each": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz",
+      "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=",
+      "dev": true,
+      "optional": true
+    },
+    "asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=",
+      "dev": true
+    },
+    "atob": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
+      "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
+      "dev": true,
+      "optional": true
+    },
+    "aws-sign2": {
+      "version": "0.7.0",
+      "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
+      "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=",
+      "dev": true
+    },
+    "aws4": {
+      "version": "1.8.0",
+      "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz",
+      "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==",
+      "dev": true
+    },
+    "babel-cli": {
+      "version": "6.26.0",
+      "resolved": "http://registry.npm.taobao.org/babel-cli/download/babel-cli-6.26.0.tgz",
+      "integrity": "sha512-wau+BDtQfuSBGQ9PzzFL3REvR9Sxnd4LKwtcHAiPjhugA7K/80vpHXafj+O5bAqJOuSefjOx5ZJnNSR2J1Qw6Q==",
+      "dev": true,
+      "requires": {
+        "babel-core": "^6.26.0",
+        "babel-polyfill": "^6.26.0",
+        "babel-register": "^6.26.0",
+        "babel-runtime": "^6.26.0",
+        "chokidar": "^1.6.1",
+        "commander": "^2.11.0",
+        "convert-source-map": "^1.5.0",
+        "fs-readdir-recursive": "^1.0.0",
+        "glob": "^7.1.2",
+        "lodash": "^4.17.4",
+        "output-file-sync": "^1.1.2",
+        "path-is-absolute": "^1.0.1",
+        "slash": "^1.0.0",
+        "source-map": "^0.5.6",
+        "v8flags": "^2.1.1"
+      },
+      "dependencies": {
+        "babel-polyfill": {
+          "version": "6.26.0",
+          "resolved": "https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.26.0.tgz",
+          "integrity": "sha1-N5k3q8Z9eJWXCtxiHyhM2WbPIVM=",
+          "dev": true,
+          "requires": {
+            "babel-runtime": "^6.26.0",
+            "core-js": "^2.5.0",
+            "regenerator-runtime": "^0.10.5"
+          }
+        },
+        "commander": {
+          "version": "2.19.0",
+          "resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz",
+          "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==",
+          "dev": true
+        },
+        "regenerator-runtime": {
+          "version": "0.10.5",
+          "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz",
+          "integrity": "sha1-M2w+/BIgrc7dosn6tntaeVWjNlg=",
+          "dev": true
+        }
+      }
+    },
+    "babel-code-frame": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz",
+      "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=",
+      "dev": true,
+      "requires": {
+        "chalk": "^1.1.3",
+        "esutils": "^2.0.2",
+        "js-tokens": "^3.0.2"
+      },
+      "dependencies": {
+        "chalk": {
+          "version": "1.1.3",
+          "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+          "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^2.2.1",
+            "escape-string-regexp": "^1.0.2",
+            "has-ansi": "^2.0.0",
+            "strip-ansi": "^3.0.0",
+            "supports-color": "^2.0.0"
+          }
+        }
+      }
+    },
+    "babel-core": {
+      "version": "6.26.3",
+      "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.26.3.tgz",
+      "integrity": "sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA==",
+      "dev": true,
+      "requires": {
+        "babel-code-frame": "^6.26.0",
+        "babel-generator": "^6.26.0",
+        "babel-helpers": "^6.24.1",
+        "babel-messages": "^6.23.0",
+        "babel-register": "^6.26.0",
+        "babel-runtime": "^6.26.0",
+        "babel-template": "^6.26.0",
+        "babel-traverse": "^6.26.0",
+        "babel-types": "^6.26.0",
+        "babylon": "^6.18.0",
+        "convert-source-map": "^1.5.1",
+        "debug": "^2.6.9",
+        "json5": "^0.5.1",
+        "lodash": "^4.17.4",
+        "minimatch": "^3.0.4",
+        "path-is-absolute": "^1.0.1",
+        "private": "^0.1.8",
+        "slash": "^1.0.0",
+        "source-map": "^0.5.7"
+      }
+    },
+    "babel-generator": {
+      "version": "6.26.1",
+      "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz",
+      "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==",
+      "dev": true,
+      "requires": {
+        "babel-messages": "^6.23.0",
+        "babel-runtime": "^6.26.0",
+        "babel-types": "^6.26.0",
+        "detect-indent": "^4.0.0",
+        "jsesc": "^1.3.0",
+        "lodash": "^4.17.4",
+        "source-map": "^0.5.7",
+        "trim-right": "^1.0.1"
+      }
+    },
+    "babel-helper-call-delegate": {
+      "version": "6.24.1",
+      "resolved": "https://registry.npmjs.org/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz",
+      "integrity": "sha1-7Oaqzdx25Bw0YfiL/Fdb0Nqi340=",
+      "dev": true,
+      "requires": {
+        "babel-helper-hoist-variables": "^6.24.1",
+        "babel-runtime": "^6.22.0",
+        "babel-traverse": "^6.24.1",
+        "babel-types": "^6.24.1"
+      }
+    },
+    "babel-helper-define-map": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-helper-define-map/-/babel-helper-define-map-6.26.0.tgz",
+      "integrity": "sha1-pfVtq0GiX5fstJjH66ypgZ+Vvl8=",
+      "dev": true,
+      "requires": {
+        "babel-helper-function-name": "^6.24.1",
+        "babel-runtime": "^6.26.0",
+        "babel-types": "^6.26.0",
+        "lodash": "^4.17.4"
+      }
+    },
+    "babel-helper-function-name": {
+      "version": "6.24.1",
+      "resolved": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz",
+      "integrity": "sha1-00dbjAPtmCQqJbSDUasYOZ01gKk=",
+      "dev": true,
+      "requires": {
+        "babel-helper-get-function-arity": "^6.24.1",
+        "babel-runtime": "^6.22.0",
+        "babel-template": "^6.24.1",
+        "babel-traverse": "^6.24.1",
+        "babel-types": "^6.24.1"
+      }
+    },
+    "babel-helper-get-function-arity": {
+      "version": "6.24.1",
+      "resolved": "https://registry.npmjs.org/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz",
+      "integrity": "sha1-j3eCqpNAfEHTqlCQj4mwMbG2hT0=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "^6.22.0",
+        "babel-types": "^6.24.1"
+      }
+    },
+    "babel-helper-hoist-variables": {
+      "version": "6.24.1",
+      "resolved": "https://registry.npmjs.org/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz",
+      "integrity": "sha1-HssnaJydJVE+rbyZFKc/VAi+enY=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "^6.22.0",
+        "babel-types": "^6.24.1"
+      }
+    },
+    "babel-helper-optimise-call-expression": {
+      "version": "6.24.1",
+      "resolved": "https://registry.npmjs.org/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz",
+      "integrity": "sha1-96E0J7qfc/j0+pk8VKl4gtEkQlc=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "^6.22.0",
+        "babel-types": "^6.24.1"
+      }
+    },
+    "babel-helper-regex": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-helper-regex/-/babel-helper-regex-6.26.0.tgz",
+      "integrity": "sha1-MlxZ+QL4LyS3T6zu0DY5VPZJXnI=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "^6.26.0",
+        "babel-types": "^6.26.0",
+        "lodash": "^4.17.4"
+      }
+    },
+    "babel-helper-remap-async-to-generator": {
+      "version": "6.24.1",
+      "resolved": "https://registry.npmjs.org/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.24.1.tgz",
+      "integrity": "sha1-XsWBgnrXI/7N04HxySg5BnbkVRs=",
+      "dev": true,
+      "requires": {
+        "babel-helper-function-name": "^6.24.1",
+        "babel-runtime": "^6.22.0",
+        "babel-template": "^6.24.1",
+        "babel-traverse": "^6.24.1",
+        "babel-types": "^6.24.1"
+      }
+    },
+    "babel-helper-replace-supers": {
+      "version": "6.24.1",
+      "resolved": "https://registry.npmjs.org/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz",
+      "integrity": "sha1-v22/5Dk40XNpohPKiov3S2qQqxo=",
+      "dev": true,
+      "requires": {
+        "babel-helper-optimise-call-expression": "^6.24.1",
+        "babel-messages": "^6.23.0",
+        "babel-runtime": "^6.22.0",
+        "babel-template": "^6.24.1",
+        "babel-traverse": "^6.24.1",
+        "babel-types": "^6.24.1"
+      }
+    },
+    "babel-helpers": {
+      "version": "6.24.1",
+      "resolved": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.24.1.tgz",
+      "integrity": "sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "^6.22.0",
+        "babel-template": "^6.24.1"
+      }
+    },
+    "babel-jest": {
+      "version": "18.0.0",
+      "resolved": "http://registry.npm.taobao.org/babel-jest/download/babel-jest-18.0.0.tgz",
+      "integrity": "sha512-bUl0sQk2V73Xssp9C0NLX+nCIuxocWKVsPSI5qHb72ZovPxq6jCZqwfcCYkFGtQ7w20RCzguJpuNmwaZtjD0WA==",
+      "dev": true,
+      "requires": {
+        "babel-core": "^6.0.0",
+        "babel-plugin-istanbul": "^3.0.0",
+        "babel-preset-jest": "^18.0.0"
+      },
+      "dependencies": {
+        "babel-core": {
+          "version": "6.26.3",
+          "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.26.3.tgz",
+          "integrity": "sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA==",
+          "dev": true,
+          "requires": {
+            "babel-code-frame": "^6.26.0",
+            "babel-generator": "^6.26.0",
+            "babel-helpers": "^6.24.1",
+            "babel-messages": "^6.23.0",
+            "babel-register": "^6.26.0",
+            "babel-runtime": "^6.26.0",
+            "babel-template": "^6.26.0",
+            "babel-traverse": "^6.26.0",
+            "babel-types": "^6.26.0",
+            "babylon": "^6.18.0",
+            "convert-source-map": "^1.5.1",
+            "debug": "^2.6.9",
+            "json5": "^0.5.1",
+            "lodash": "^4.17.4",
+            "minimatch": "^3.0.4",
+            "path-is-absolute": "^1.0.1",
+            "private": "^0.1.8",
+            "slash": "^1.0.0",
+            "source-map": "^0.5.7"
+          }
+        }
+      }
+    },
+    "babel-messages": {
+      "version": "6.23.0",
+      "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz",
+      "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "^6.22.0"
+      }
+    },
+    "babel-plugin-check-es2015-constants": {
+      "version": "6.22.0",
+      "resolved": "https://registry.npmjs.org/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz",
+      "integrity": "sha1-NRV7EBQm/S/9PaP3XH0ekYNbv4o=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "^6.22.0"
+      }
+    },
+    "babel-plugin-istanbul": {
+      "version": "3.1.2",
+      "resolved": "http://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-3.1.2.tgz",
+      "integrity": "sha1-EdWr3hhCXsJLXWSMfgtdJc01SiI=",
+      "dev": true,
+      "requires": {
+        "find-up": "^1.1.2",
+        "istanbul-lib-instrument": "^1.4.2",
+        "object-assign": "^4.1.0",
+        "test-exclude": "^3.3.0"
+      }
+    },
+    "babel-plugin-jest-hoist": {
+      "version": "18.0.0",
+      "resolved": "http://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-18.0.0.tgz",
+      "integrity": "sha1-QVDnDsq1YObnNErchJSYBy004So=",
+      "dev": true
+    },
+    "babel-plugin-syntax-async-functions": {
+      "version": "6.13.0",
+      "resolved": "http://registry.npmjs.org/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz",
+      "integrity": "sha1-ytnK0RkbWtY0vzCuCHI5HgZHvpU=",
+      "dev": true
+    },
+    "babel-plugin-transform-async-to-generator": {
+      "version": "6.24.1",
+      "resolved": "http://registry.npm.taobao.org/babel-plugin-transform-async-to-generator/download/babel-plugin-transform-async-to-generator-6.24.1.tgz",
+      "integrity": "sha512-7BgYJujNCg0Ti3x0c/DL3tStvnKS6ktIYOmo9wginv/dfZOrbSZ+qG4IRRHMBOzZ5Awb1skTiAsQXg/+IWkZYw==",
+      "dev": true,
+      "requires": {
+        "babel-helper-remap-async-to-generator": "^6.24.1",
+        "babel-plugin-syntax-async-functions": "^6.8.0",
+        "babel-runtime": "^6.22.0"
+      },
+      "dependencies": {
+        "babel-runtime": {
+          "version": "6.26.0",
+          "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz",
+          "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=",
+          "dev": true,
+          "requires": {
+            "core-js": "^2.4.0",
+            "regenerator-runtime": "^0.11.0"
+          }
+        }
+      }
+    },
+    "babel-plugin-transform-es2015-arrow-functions": {
+      "version": "6.22.0",
+      "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz",
+      "integrity": "sha1-RSaSy3EdX3ncf4XkQM5BufJE0iE=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "^6.22.0"
+      }
+    },
+    "babel-plugin-transform-es2015-block-scoped-functions": {
+      "version": "6.22.0",
+      "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz",
+      "integrity": "sha1-u8UbSflk1wy42OC5ToICRs46YUE=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "^6.22.0"
+      }
+    },
+    "babel-plugin-transform-es2015-block-scoping": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz",
+      "integrity": "sha1-1w9SmcEwjQXBL0Y4E7CgnnOxiV8=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "^6.26.0",
+        "babel-template": "^6.26.0",
+        "babel-traverse": "^6.26.0",
+        "babel-types": "^6.26.0",
+        "lodash": "^4.17.4"
+      }
+    },
+    "babel-plugin-transform-es2015-classes": {
+      "version": "6.24.1",
+      "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz",
+      "integrity": "sha1-WkxYpQyclGHlZLSyo7+ryXolhNs=",
+      "dev": true,
+      "requires": {
+        "babel-helper-define-map": "^6.24.1",
+        "babel-helper-function-name": "^6.24.1",
+        "babel-helper-optimise-call-expression": "^6.24.1",
+        "babel-helper-replace-supers": "^6.24.1",
+        "babel-messages": "^6.23.0",
+        "babel-runtime": "^6.22.0",
+        "babel-template": "^6.24.1",
+        "babel-traverse": "^6.24.1",
+        "babel-types": "^6.24.1"
+      }
+    },
+    "babel-plugin-transform-es2015-computed-properties": {
+      "version": "6.24.1",
+      "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz",
+      "integrity": "sha1-b+Ko0WiV1WNPTNmZttNICjCBWbM=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "^6.22.0",
+        "babel-template": "^6.24.1"
+      }
+    },
+    "babel-plugin-transform-es2015-destructuring": {
+      "version": "6.23.0",
+      "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz",
+      "integrity": "sha1-mXux8auWf2gtKwh2/jWNYOdlxW0=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "^6.22.0"
+      }
+    },
+    "babel-plugin-transform-es2015-duplicate-keys": {
+      "version": "6.24.1",
+      "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz",
+      "integrity": "sha1-c+s9MQypaePvnskcU3QabxV2Qj4=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "^6.22.0",
+        "babel-types": "^6.24.1"
+      }
+    },
+    "babel-plugin-transform-es2015-for-of": {
+      "version": "6.23.0",
+      "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz",
+      "integrity": "sha1-9HyVsrYT3x0+zC/bdXNiPHUkhpE=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "^6.22.0"
+      }
+    },
+    "babel-plugin-transform-es2015-function-name": {
+      "version": "6.24.1",
+      "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz",
+      "integrity": "sha1-g0yJhTvDaxrw86TF26qU/Y6sqos=",
+      "dev": true,
+      "requires": {
+        "babel-helper-function-name": "^6.24.1",
+        "babel-runtime": "^6.22.0",
+        "babel-types": "^6.24.1"
+      }
+    },
+    "babel-plugin-transform-es2015-literals": {
+      "version": "6.22.0",
+      "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz",
+      "integrity": "sha1-T1SgLWzWbPkVKAAZox0xklN3yi4=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "^6.22.0"
+      }
+    },
+    "babel-plugin-transform-es2015-modules-amd": {
+      "version": "6.24.1",
+      "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz",
+      "integrity": "sha1-Oz5UAXI5hC1tGcMBHEvS8AoA0VQ=",
+      "dev": true,
+      "requires": {
+        "babel-plugin-transform-es2015-modules-commonjs": "^6.24.1",
+        "babel-runtime": "^6.22.0",
+        "babel-template": "^6.24.1"
+      }
+    },
+    "babel-plugin-transform-es2015-modules-commonjs": {
+      "version": "6.26.2",
+      "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.2.tgz",
+      "integrity": "sha512-CV9ROOHEdrjcwhIaJNBGMBCodN+1cfkwtM1SbUHmvyy35KGT7fohbpOxkE2uLz1o6odKK2Ck/tz47z+VqQfi9Q==",
+      "dev": true,
+      "requires": {
+        "babel-plugin-transform-strict-mode": "^6.24.1",
+        "babel-runtime": "^6.26.0",
+        "babel-template": "^6.26.0",
+        "babel-types": "^6.26.0"
+      }
+    },
+    "babel-plugin-transform-es2015-modules-systemjs": {
+      "version": "6.24.1",
+      "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz",
+      "integrity": "sha1-/4mhQrkRmpBhlfXxBuzzBdlAfSM=",
+      "dev": true,
+      "requires": {
+        "babel-helper-hoist-variables": "^6.24.1",
+        "babel-runtime": "^6.22.0",
+        "babel-template": "^6.24.1"
+      }
+    },
+    "babel-plugin-transform-es2015-modules-umd": {
+      "version": "6.24.1",
+      "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz",
+      "integrity": "sha1-rJl+YoXNGO1hdq22B9YCNErThGg=",
+      "dev": true,
+      "requires": {
+        "babel-plugin-transform-es2015-modules-amd": "^6.24.1",
+        "babel-runtime": "^6.22.0",
+        "babel-template": "^6.24.1"
+      }
+    },
+    "babel-plugin-transform-es2015-object-super": {
+      "version": "6.24.1",
+      "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz",
+      "integrity": "sha1-JM72muIcuDp/hgPa0CH1cusnj40=",
+      "dev": true,
+      "requires": {
+        "babel-helper-replace-supers": "^6.24.1",
+        "babel-runtime": "^6.22.0"
+      }
+    },
+    "babel-plugin-transform-es2015-parameters": {
+      "version": "6.24.1",
+      "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz",
+      "integrity": "sha1-V6w1GrScrxSpfNE7CfZv3wpiXys=",
+      "dev": true,
+      "requires": {
+        "babel-helper-call-delegate": "^6.24.1",
+        "babel-helper-get-function-arity": "^6.24.1",
+        "babel-runtime": "^6.22.0",
+        "babel-template": "^6.24.1",
+        "babel-traverse": "^6.24.1",
+        "babel-types": "^6.24.1"
+      }
+    },
+    "babel-plugin-transform-es2015-shorthand-properties": {
+      "version": "6.24.1",
+      "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz",
+      "integrity": "sha1-JPh11nIch2YbvZmkYi5R8U3jiqA=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "^6.22.0",
+        "babel-types": "^6.24.1"
+      }
+    },
+    "babel-plugin-transform-es2015-spread": {
+      "version": "6.22.0",
+      "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz",
+      "integrity": "sha1-1taKmfia7cRTbIGlQujdnxdG+NE=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "^6.22.0"
+      }
+    },
+    "babel-plugin-transform-es2015-sticky-regex": {
+      "version": "6.24.1",
+      "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz",
+      "integrity": "sha1-AMHNsaynERLN8M9hJsLta0V8zbw=",
+      "dev": true,
+      "requires": {
+        "babel-helper-regex": "^6.24.1",
+        "babel-runtime": "^6.22.0",
+        "babel-types": "^6.24.1"
+      }
+    },
+    "babel-plugin-transform-es2015-template-literals": {
+      "version": "6.22.0",
+      "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz",
+      "integrity": "sha1-qEs0UPfp+PH2g51taH2oS7EjbY0=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "^6.22.0"
+      }
+    },
+    "babel-plugin-transform-es2015-typeof-symbol": {
+      "version": "6.23.0",
+      "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz",
+      "integrity": "sha1-3sCfHN3/lLUqxz1QXITfWdzOs3I=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "^6.22.0"
+      }
+    },
+    "babel-plugin-transform-es2015-unicode-regex": {
+      "version": "6.24.1",
+      "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz",
+      "integrity": "sha1-04sS9C6nMj9yk4fxinxa4frrNek=",
+      "dev": true,
+      "requires": {
+        "babel-helper-regex": "^6.24.1",
+        "babel-runtime": "^6.22.0",
+        "regexpu-core": "^2.0.0"
+      }
+    },
+    "babel-plugin-transform-regenerator": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz",
+      "integrity": "sha1-4HA2lvveJ/Cj78rPi03KL3s6jy8=",
+      "dev": true,
+      "requires": {
+        "regenerator-transform": "^0.10.0"
+      }
+    },
+    "babel-plugin-transform-strict-mode": {
+      "version": "6.24.1",
+      "resolved": "https://registry.npmjs.org/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz",
+      "integrity": "sha1-1fr3qleKZbvlkc9e2uBKDGcCB1g=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "^6.22.0",
+        "babel-types": "^6.24.1"
+      }
+    },
+    "babel-polyfill": {
+      "version": "6.26.0",
+      "resolved": "http://registry.npm.taobao.org/babel-polyfill/download/babel-polyfill-6.26.0.tgz",
+      "integrity": "sha512-F2rZGQnAdaHWQ8YAoeRbukc7HS9QgdgeyJ0rQDd485v9opwuPvjpPFcOOT/WmkKTdgy9ESgSPXDcTNpzrGr6iQ==",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "^6.26.0",
+        "core-js": "^2.5.0",
+        "regenerator-runtime": "^0.10.5"
+      },
+      "dependencies": {
+        "babel-runtime": {
+          "version": "6.26.0",
+          "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz",
+          "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=",
+          "dev": true,
+          "requires": {
+            "core-js": "^2.4.0",
+            "regenerator-runtime": "^0.11.0"
+          },
+          "dependencies": {
+            "regenerator-runtime": {
+              "version": "0.11.1",
+              "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz",
+              "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==",
+              "dev": true
+            }
+          }
+        },
+        "core-js": {
+          "version": "2.5.7",
+          "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz",
+          "integrity": "sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw==",
+          "dev": true
+        },
+        "regenerator-runtime": {
+          "version": "0.10.5",
+          "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz",
+          "integrity": "sha1-M2w+/BIgrc7dosn6tntaeVWjNlg=",
+          "dev": true
+        }
+      }
+    },
+    "babel-preset-es2015": {
+      "version": "6.24.1",
+      "resolved": "http://registry.npm.taobao.org/babel-preset-es2015/download/babel-preset-es2015-6.24.1.tgz",
+      "integrity": "sha512-XfwUqG1Ry6R43m4Wfob+vHbIVBIqTg/TJY4Snku1iIzeH7mUnwHA8Vagmv+ZQbPwhS8HgsdQvy28Py3k5zpoFQ==",
+      "dev": true,
+      "requires": {
+        "babel-plugin-check-es2015-constants": "^6.22.0",
+        "babel-plugin-transform-es2015-arrow-functions": "^6.22.0",
+        "babel-plugin-transform-es2015-block-scoped-functions": "^6.22.0",
+        "babel-plugin-transform-es2015-block-scoping": "^6.24.1",
+        "babel-plugin-transform-es2015-classes": "^6.24.1",
+        "babel-plugin-transform-es2015-computed-properties": "^6.24.1",
+        "babel-plugin-transform-es2015-destructuring": "^6.22.0",
+        "babel-plugin-transform-es2015-duplicate-keys": "^6.24.1",
+        "babel-plugin-transform-es2015-for-of": "^6.22.0",
+        "babel-plugin-transform-es2015-function-name": "^6.24.1",
+        "babel-plugin-transform-es2015-literals": "^6.22.0",
+        "babel-plugin-transform-es2015-modules-amd": "^6.24.1",
+        "babel-plugin-transform-es2015-modules-commonjs": "^6.24.1",
+        "babel-plugin-transform-es2015-modules-systemjs": "^6.24.1",
+        "babel-plugin-transform-es2015-modules-umd": "^6.24.1",
+        "babel-plugin-transform-es2015-object-super": "^6.24.1",
+        "babel-plugin-transform-es2015-parameters": "^6.24.1",
+        "babel-plugin-transform-es2015-shorthand-properties": "^6.24.1",
+        "babel-plugin-transform-es2015-spread": "^6.22.0",
+        "babel-plugin-transform-es2015-sticky-regex": "^6.24.1",
+        "babel-plugin-transform-es2015-template-literals": "^6.22.0",
+        "babel-plugin-transform-es2015-typeof-symbol": "^6.22.0",
+        "babel-plugin-transform-es2015-unicode-regex": "^6.24.1",
+        "babel-plugin-transform-regenerator": "^6.24.1"
+      }
+    },
+    "babel-preset-jest": {
+      "version": "18.0.0",
+      "resolved": "http://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-18.0.0.tgz",
+      "integrity": "sha1-hPr4yj7GWrp9Xj9Zu67ZNaskBJ4=",
+      "dev": true,
+      "requires": {
+        "babel-plugin-jest-hoist": "^18.0.0"
+      }
+    },
+    "babel-register": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.26.0.tgz",
+      "integrity": "sha1-btAhFz4vy0htestFxgCahW9kcHE=",
+      "dev": true,
+      "requires": {
+        "babel-core": "^6.26.0",
+        "babel-runtime": "^6.26.0",
+        "core-js": "^2.5.0",
+        "home-or-tmp": "^2.0.0",
+        "lodash": "^4.17.4",
+        "mkdirp": "^0.5.1",
+        "source-map-support": "^0.4.15"
+      }
+    },
+    "babel-runtime": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz",
+      "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=",
+      "dev": true,
+      "requires": {
+        "core-js": "^2.4.0",
+        "regenerator-runtime": "^0.11.0"
+      }
+    },
+    "babel-template": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz",
+      "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "^6.26.0",
+        "babel-traverse": "^6.26.0",
+        "babel-types": "^6.26.0",
+        "babylon": "^6.18.0",
+        "lodash": "^4.17.4"
+      }
+    },
+    "babel-traverse": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz",
+      "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=",
+      "dev": true,
+      "requires": {
+        "babel-code-frame": "^6.26.0",
+        "babel-messages": "^6.23.0",
+        "babel-runtime": "^6.26.0",
+        "babel-types": "^6.26.0",
+        "babylon": "^6.18.0",
+        "debug": "^2.6.8",
+        "globals": "^9.18.0",
+        "invariant": "^2.2.2",
+        "lodash": "^4.17.4"
+      }
+    },
+    "babel-types": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz",
+      "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "^6.26.0",
+        "esutils": "^2.0.2",
+        "lodash": "^4.17.4",
+        "to-fast-properties": "^1.0.3"
+      }
+    },
+    "babylon": {
+      "version": "6.18.0",
+      "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz",
+      "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==",
+      "dev": true
+    },
+    "balanced-match": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
+      "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
+      "dev": true
+    },
+    "base": {
+      "version": "0.11.2",
+      "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz",
+      "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "cache-base": "^1.0.1",
+        "class-utils": "^0.3.5",
+        "component-emitter": "^1.2.1",
+        "define-property": "^1.0.0",
+        "isobject": "^3.0.1",
+        "mixin-deep": "^1.2.0",
+        "pascalcase": "^0.1.1"
+      },
+      "dependencies": {
+        "define-property": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
+          "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "is-descriptor": "^1.0.0"
+          }
+        },
+        "is-accessor-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+          "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-data-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+          "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-descriptor": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+          "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "is-accessor-descriptor": "^1.0.0",
+            "is-data-descriptor": "^1.0.0",
+            "kind-of": "^6.0.2"
+          }
+        },
+        "isobject": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
+          "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
+          "dev": true,
+          "optional": true
+        },
+        "kind-of": {
+          "version": "6.0.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
+          "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
+          "dev": true,
+          "optional": true
+        }
+      }
+    },
+    "bcrypt-pbkdf": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
+      "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=",
+      "dev": true,
+      "requires": {
+        "tweetnacl": "^0.14.3"
+      }
+    },
+    "binary-extensions": {
+      "version": "1.12.0",
+      "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.12.0.tgz",
+      "integrity": "sha512-DYWGk01lDcxeS/K9IHPGWfT8PsJmbXRtRd2Sx72Tnb8pcYZQFF1oSDb8hJtS1vhp212q1Rzi5dUf9+nq0o9UIg==",
+      "dev": true,
+      "optional": true
+    },
+    "brace-expansion": {
+      "version": "1.1.11",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+      "dev": true,
+      "requires": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "braces": {
+      "version": "1.8.5",
+      "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz",
+      "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=",
+      "dev": true,
+      "requires": {
+        "expand-range": "^1.8.1",
+        "preserve": "^0.2.0",
+        "repeat-element": "^1.1.2"
+      }
+    },
+    "browser-resolve": {
+      "version": "1.11.3",
+      "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.3.tgz",
+      "integrity": "sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ==",
+      "dev": true,
+      "requires": {
+        "resolve": "1.1.7"
+      },
+      "dependencies": {
+        "resolve": {
+          "version": "1.1.7",
+          "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz",
+          "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=",
+          "dev": true
+        }
+      }
+    },
+    "bser": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/bser/-/bser-1.0.2.tgz",
+      "integrity": "sha1-OBEWlwsqbe6lZG3RXdcnhES1YWk=",
+      "dev": true,
+      "requires": {
+        "node-int64": "^0.4.0"
+      }
+    },
+    "buffer-from": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
+      "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==",
+      "dev": true
+    },
+    "builtin-modules": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz",
+      "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=",
+      "dev": true
+    },
+    "cache-base": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz",
+      "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "collection-visit": "^1.0.0",
+        "component-emitter": "^1.2.1",
+        "get-value": "^2.0.6",
+        "has-value": "^1.0.0",
+        "isobject": "^3.0.1",
+        "set-value": "^2.0.0",
+        "to-object-path": "^0.3.0",
+        "union-value": "^1.0.0",
+        "unset-value": "^1.0.0"
+      },
+      "dependencies": {
+        "isobject": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
+          "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
+          "dev": true,
+          "optional": true
+        }
+      }
+    },
+    "caller-path": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz",
+      "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=",
+      "dev": true,
+      "requires": {
+        "callsites": "^0.2.0"
+      }
+    },
+    "callsites": {
+      "version": "0.2.0",
+      "resolved": "http://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz",
+      "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=",
+      "dev": true
+    },
+    "camelcase": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz",
+      "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=",
+      "dev": true
+    },
+    "cardinal": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/cardinal/-/cardinal-2.1.1.tgz",
+      "integrity": "sha1-fMEFXYItISlU0HsIXeolHMe8VQU=",
+      "dev": true,
+      "requires": {
+        "ansicolors": "~0.3.2",
+        "redeyed": "~2.1.0"
+      }
+    },
+    "caseless": {
+      "version": "0.12.0",
+      "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
+      "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=",
+      "dev": true
+    },
+    "chalk": {
+      "version": "2.4.1",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz",
+      "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==",
+      "requires": {
+        "ansi-styles": "^3.2.1",
+        "escape-string-regexp": "^1.0.5",
+        "supports-color": "^5.3.0"
+      },
+      "dependencies": {
+        "ansi-styles": {
+          "version": "3.2.1",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+          "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+          "requires": {
+            "color-convert": "^1.9.0"
+          }
+        },
+        "has-flag": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+          "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
+        },
+        "supports-color": {
+          "version": "5.5.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+          "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+          "requires": {
+            "has-flag": "^3.0.0"
+          }
+        }
+      }
+    },
+    "chokidar": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz",
+      "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "anymatch": "^1.3.0",
+        "async-each": "^1.0.0",
+        "fsevents": "^1.0.0",
+        "glob-parent": "^2.0.0",
+        "inherits": "^2.0.1",
+        "is-binary-path": "^1.0.0",
+        "is-glob": "^2.0.0",
+        "path-is-absolute": "^1.0.0",
+        "readdirp": "^2.0.0"
+      }
+    },
+    "ci-info": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz",
+      "integrity": "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==",
+      "dev": true
+    },
+    "circular-json": {
+      "version": "0.3.3",
+      "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz",
+      "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==",
+      "dev": true
+    },
+    "class-utils": {
+      "version": "0.3.6",
+      "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz",
+      "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "arr-union": "^3.1.0",
+        "define-property": "^0.2.5",
+        "isobject": "^3.0.0",
+        "static-extend": "^0.1.1"
+      },
+      "dependencies": {
+        "define-property": {
+          "version": "0.2.5",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+          "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "is-descriptor": "^0.1.0"
+          }
+        },
+        "isobject": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
+          "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
+          "dev": true,
+          "optional": true
+        }
+      }
+    },
+    "cli-cursor": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz",
+      "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=",
+      "dev": true,
+      "requires": {
+        "restore-cursor": "^1.0.1"
+      }
+    },
+    "cli-table": {
+      "version": "0.3.1",
+      "resolved": "https://registry.npmjs.org/cli-table/-/cli-table-0.3.1.tgz",
+      "integrity": "sha1-9TsFJmqLGguTSz0IIebi3FkUriM=",
+      "dev": true,
+      "requires": {
+        "colors": "1.0.3"
+      }
+    },
+    "cli-usage": {
+      "version": "0.1.8",
+      "resolved": "https://registry.npmjs.org/cli-usage/-/cli-usage-0.1.8.tgz",
+      "integrity": "sha512-EZJ+ty1TsqdnhZNt2QbI+ed3IUNHTH31blSOJLVph3oL4IExskPRyCDGJH7RuCBPy3QBmWgpbeUxXPhK0isXIw==",
+      "dev": true,
+      "requires": {
+        "marked": "^0.5.0",
+        "marked-terminal": "^3.0.0"
+      }
+    },
+    "cli-width": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz",
+      "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=",
+      "dev": true
+    },
+    "cliui": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz",
+      "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=",
+      "dev": true,
+      "requires": {
+        "string-width": "^1.0.1",
+        "strip-ansi": "^3.0.1",
+        "wrap-ansi": "^2.0.0"
+      }
+    },
+    "co": {
+      "version": "4.6.0",
+      "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
+      "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=",
+      "dev": true
+    },
+    "code-point-at": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
+      "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
+      "dev": true
+    },
+    "collection-visit": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz",
+      "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "map-visit": "^1.0.0",
+        "object-visit": "^1.0.0"
+      }
+    },
+    "color": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/color/-/color-3.0.0.tgz",
+      "integrity": "sha512-jCpd5+s0s0t7p3pHQKpnJ0TpQKKdleP71LWcA0aqiljpiuAkOSUFN/dyH8ZwF0hRmFlrIuRhufds1QyEP9EB+w==",
+      "requires": {
+        "color-convert": "^1.9.1",
+        "color-string": "^1.5.2"
+      }
+    },
+    "color-convert": {
+      "version": "1.9.3",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+      "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+      "requires": {
+        "color-name": "1.1.3"
+      }
+    },
+    "color-name": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+      "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
+    },
+    "color-string": {
+      "version": "1.5.3",
+      "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.3.tgz",
+      "integrity": "sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw==",
+      "requires": {
+        "color-name": "^1.0.0",
+        "simple-swizzle": "^0.2.2"
+      }
+    },
+    "colornames": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/colornames/-/colornames-1.1.1.tgz",
+      "integrity": "sha1-+IiQMGhcfE/54qVZ9Qd+t2qBb5Y="
+    },
+    "colors": {
+      "version": "1.0.3",
+      "resolved": "http://registry.npmjs.org/colors/-/colors-1.0.3.tgz",
+      "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=",
+      "dev": true
+    },
+    "colorspace": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.1.tgz",
+      "integrity": "sha512-pI3btWyiuz7Ken0BWh9Elzsmv2bM9AhA7psXib4anUXy/orfZ/E0MbQwhSOG/9L8hLlalqrU0UhOuqxW1YjmVw==",
+      "requires": {
+        "color": "3.0.x",
+        "text-hex": "1.0.x"
+      }
+    },
+    "combined-stream": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz",
+      "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==",
+      "dev": true,
+      "requires": {
+        "delayed-stream": "~1.0.0"
+      }
+    },
+    "commander": {
+      "version": "2.16.0",
+      "resolved": "http://registry.npm.taobao.org/commander/download/commander-2.16.0.tgz",
+      "integrity": "sha512-sVXqklSaotK9at437sFlFpyOcJonxe0yST/AG9DkQKUdIE6IqGIMv4SfAQSKaJbSdVEJYItASCrBiVQHq1HQew=="
+    },
+    "component-emitter": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
+      "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=",
+      "dev": true,
+      "optional": true
+    },
+    "concat-map": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+      "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
+      "dev": true
+    },
+    "concat-stream": {
+      "version": "1.6.2",
+      "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
+      "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
+      "dev": true,
+      "requires": {
+        "buffer-from": "^1.0.0",
+        "inherits": "^2.0.3",
+        "readable-stream": "^2.2.2",
+        "typedarray": "^0.0.6"
+      }
+    },
+    "contains-path": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz",
+      "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=",
+      "dev": true
+    },
+    "content-type-parser": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/content-type-parser/-/content-type-parser-1.0.2.tgz",
+      "integrity": "sha512-lM4l4CnMEwOLHAHr/P6MEZwZFPJFtAAKgL6pogbXmVZggIqXhdB6RbBtPOTsw2FcXwYhehRGERJmRrjOiIB8pQ==",
+      "dev": true
+    },
+    "convert-source-map": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz",
+      "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==",
+      "dev": true,
+      "requires": {
+        "safe-buffer": "~5.1.1"
+      }
+    },
+    "copy-descriptor": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz",
+      "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=",
+      "dev": true,
+      "optional": true
+    },
+    "core-js": {
+      "version": "2.5.7",
+      "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz",
+      "integrity": "sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw==",
+      "dev": true
+    },
+    "core-util-is": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
+      "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
+    },
+    "cssom": {
+      "version": "0.3.4",
+      "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.4.tgz",
+      "integrity": "sha512-+7prCSORpXNeR4/fUP3rL+TzqtiFfhMvTd7uEqMdgPvLPt4+uzFUeufx5RHjGTACCargg/DiEt/moMQmvnfkog==",
+      "dev": true
+    },
+    "cssstyle": {
+      "version": "0.2.37",
+      "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-0.2.37.tgz",
+      "integrity": "sha1-VBCXI0yyUTyDzu06zdwn/yeYfVQ=",
+      "dev": true,
+      "requires": {
+        "cssom": "0.3.x"
+      }
+    },
+    "d": {
+      "version": "1.0.0",
+      "resolved": "http://registry.npmjs.org/d/-/d-1.0.0.tgz",
+      "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=",
+      "dev": true,
+      "requires": {
+        "es5-ext": "^0.10.9"
+      }
+    },
+    "dashdash": {
+      "version": "1.14.1",
+      "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
+      "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
+      "dev": true,
+      "requires": {
+        "assert-plus": "^1.0.0"
+      }
+    },
+    "debug": {
+      "version": "2.6.9",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+      "dev": true,
+      "requires": {
+        "ms": "2.0.0"
+      }
+    },
+    "decamelize": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+      "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=",
+      "dev": true
+    },
+    "decode-uri-component": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
+      "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=",
+      "dev": true,
+      "optional": true
+    },
+    "deep-is": {
+      "version": "0.1.3",
+      "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz",
+      "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=",
+      "dev": true
+    },
+    "default-require-extensions": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-1.0.0.tgz",
+      "integrity": "sha1-836hXT4T/9m0N9M+GnW1+5eHTLg=",
+      "dev": true,
+      "requires": {
+        "strip-bom": "^2.0.0"
+      }
+    },
+    "define-property": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz",
+      "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "is-descriptor": "^1.0.2",
+        "isobject": "^3.0.1"
+      },
+      "dependencies": {
+        "is-accessor-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+          "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-data-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+          "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-descriptor": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+          "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "is-accessor-descriptor": "^1.0.0",
+            "is-data-descriptor": "^1.0.0",
+            "kind-of": "^6.0.2"
+          }
+        },
+        "isobject": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
+          "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
+          "dev": true,
+          "optional": true
+        },
+        "kind-of": {
+          "version": "6.0.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
+          "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
+          "dev": true,
+          "optional": true
+        }
+      }
+    },
+    "delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
+      "dev": true
+    },
+    "detect-indent": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz",
+      "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=",
+      "dev": true,
+      "requires": {
+        "repeating": "^2.0.0"
+      }
+    },
+    "diagnostics": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/diagnostics/-/diagnostics-1.1.1.tgz",
+      "integrity": "sha512-8wn1PmdunLJ9Tqbx+Fx/ZEuHfJf4NKSN2ZBj7SJC/OWRWha843+WsTjqMe1B5E3p28jqBlp+mJ2fPVxPyNgYKQ==",
+      "requires": {
+        "colorspace": "1.1.x",
+        "enabled": "1.0.x",
+        "kuler": "1.0.x"
+      }
+    },
+    "diff": {
+      "version": "3.5.0",
+      "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz",
+      "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==",
+      "dev": true
+    },
+    "doctrine": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
+      "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
+      "dev": true,
+      "requires": {
+        "esutils": "^2.0.2"
+      }
+    },
+    "ecc-jsbn": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
+      "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=",
+      "dev": true,
+      "requires": {
+        "jsbn": "~0.1.0",
+        "safer-buffer": "^2.1.0"
+      }
+    },
+    "enabled": {
+      "version": "1.0.2",
+      "resolved": "http://registry.npmjs.org/enabled/-/enabled-1.0.2.tgz",
+      "integrity": "sha1-ll9lE9LC0cX0ZStkouM5ZGf8L5M=",
+      "requires": {
+        "env-variable": "0.0.x"
+      }
+    },
+    "env-variable": {
+      "version": "0.0.5",
+      "resolved": "https://registry.npmjs.org/env-variable/-/env-variable-0.0.5.tgz",
+      "integrity": "sha512-zoB603vQReOFvTg5xMl9I1P2PnHsHQQKTEowsKKD7nseUfJq6UWzK+4YtlWUO1nhiQUxe6XMkk+JleSZD1NZFA=="
+    },
+    "errno": {
+      "version": "0.1.7",
+      "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz",
+      "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==",
+      "dev": true,
+      "requires": {
+        "prr": "~1.0.1"
+      }
+    },
+    "error-ex": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
+      "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
+      "dev": true,
+      "requires": {
+        "is-arrayish": "^0.2.1"
+      }
+    },
+    "es5-ext": {
+      "version": "0.10.46",
+      "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.46.tgz",
+      "integrity": "sha512-24XxRvJXNFwEMpJb3nOkiRJKRoupmjYmOPVlI65Qy2SrtxwOTB+g6ODjBKOtwEHbYrhWRty9xxOWLNdClT2djw==",
+      "dev": true,
+      "requires": {
+        "es6-iterator": "~2.0.3",
+        "es6-symbol": "~3.1.1",
+        "next-tick": "1"
+      }
+    },
+    "es6-iterator": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
+      "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=",
+      "dev": true,
+      "requires": {
+        "d": "1",
+        "es5-ext": "^0.10.35",
+        "es6-symbol": "^3.1.1"
+      }
+    },
+    "es6-map": {
+      "version": "0.1.5",
+      "resolved": "https://registry.npmjs.org/es6-map/-/es6-map-0.1.5.tgz",
+      "integrity": "sha1-kTbgUD3MBqMBaQ8LsU/042TpSfA=",
+      "dev": true,
+      "requires": {
+        "d": "1",
+        "es5-ext": "~0.10.14",
+        "es6-iterator": "~2.0.1",
+        "es6-set": "~0.1.5",
+        "es6-symbol": "~3.1.1",
+        "event-emitter": "~0.3.5"
+      }
+    },
+    "es6-set": {
+      "version": "0.1.5",
+      "resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.5.tgz",
+      "integrity": "sha1-0rPsXU2ADO2BjbU40ol02wpzzLE=",
+      "dev": true,
+      "requires": {
+        "d": "1",
+        "es5-ext": "~0.10.14",
+        "es6-iterator": "~2.0.1",
+        "es6-symbol": "3.1.1",
+        "event-emitter": "~0.3.5"
+      }
+    },
+    "es6-symbol": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz",
+      "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=",
+      "dev": true,
+      "requires": {
+        "d": "1",
+        "es5-ext": "~0.10.14"
+      }
+    },
+    "es6-weak-map": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.2.tgz",
+      "integrity": "sha1-XjqzIlH/0VOKH45f+hNXdy+S2W8=",
+      "dev": true,
+      "requires": {
+        "d": "1",
+        "es5-ext": "^0.10.14",
+        "es6-iterator": "^2.0.1",
+        "es6-symbol": "^3.1.1"
+      }
+    },
+    "escape-string-regexp": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+      "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
+    },
+    "escodegen": {
+      "version": "1.11.0",
+      "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.11.0.tgz",
+      "integrity": "sha512-IeMV45ReixHS53K/OmfKAIztN/igDHzTJUhZM3k1jMhIZWjk45SMwAtBsEXiJp3vSPmTcu6CXn7mDvFHRN66fw==",
+      "dev": true,
+      "requires": {
+        "esprima": "^3.1.3",
+        "estraverse": "^4.2.0",
+        "esutils": "^2.0.2",
+        "optionator": "^0.8.1",
+        "source-map": "~0.6.1"
+      },
+      "dependencies": {
+        "esprima": {
+          "version": "3.1.3",
+          "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz",
+          "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=",
+          "dev": true
+        },
+        "source-map": {
+          "version": "0.6.1",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+          "dev": true,
+          "optional": true
+        }
+      }
+    },
+    "escope": {
+      "version": "3.6.0",
+      "resolved": "https://registry.npmjs.org/escope/-/escope-3.6.0.tgz",
+      "integrity": "sha1-4Bl16BJ4GhY6ba392AOY3GTIicM=",
+      "dev": true,
+      "requires": {
+        "es6-map": "^0.1.3",
+        "es6-weak-map": "^2.0.1",
+        "esrecurse": "^4.1.0",
+        "estraverse": "^4.1.1"
+      }
+    },
+    "eslint": {
+      "version": "3.19.0",
+      "resolved": "http://registry.npm.taobao.org/eslint/download/eslint-3.19.0.tgz",
+      "integrity": "sha512-x6LJGXWCGB/4YOBhL48yeppZTo+YQUNC37N5qqCpC1b1kkNzydlQHQAtPuUSFoZSxgIadrysQoW2Hq602P+uEA==",
+      "dev": true,
+      "requires": {
+        "babel-code-frame": "^6.16.0",
+        "chalk": "^1.1.3",
+        "concat-stream": "^1.5.2",
+        "debug": "^2.1.1",
+        "doctrine": "^2.0.0",
+        "escope": "^3.6.0",
+        "espree": "^3.4.0",
+        "esquery": "^1.0.0",
+        "estraverse": "^4.2.0",
+        "esutils": "^2.0.2",
+        "file-entry-cache": "^2.0.0",
+        "glob": "^7.0.3",
+        "globals": "^9.14.0",
+        "ignore": "^3.2.0",
+        "imurmurhash": "^0.1.4",
+        "inquirer": "^0.12.0",
+        "is-my-json-valid": "^2.10.0",
+        "is-resolvable": "^1.0.0",
+        "js-yaml": "^3.5.1",
+        "json-stable-stringify": "^1.0.0",
+        "levn": "^0.3.0",
+        "lodash": "^4.0.0",
+        "mkdirp": "^0.5.0",
+        "natural-compare": "^1.4.0",
+        "optionator": "^0.8.2",
+        "path-is-inside": "^1.0.1",
+        "pluralize": "^1.2.1",
+        "progress": "^1.1.8",
+        "require-uncached": "^1.0.2",
+        "shelljs": "^0.7.5",
+        "strip-bom": "^3.0.0",
+        "strip-json-comments": "~2.0.1",
+        "table": "^3.7.8",
+        "text-table": "~0.2.0",
+        "user-home": "^2.0.0"
+      },
+      "dependencies": {
+        "babel-code-frame": {
+          "version": "6.26.0",
+          "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz",
+          "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=",
+          "dev": true,
+          "requires": {
+            "chalk": "^1.1.3",
+            "esutils": "^2.0.2",
+            "js-tokens": "^3.0.2"
+          }
+        },
+        "chalk": {
+          "version": "1.1.3",
+          "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+          "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^2.2.1",
+            "escape-string-regexp": "^1.0.2",
+            "has-ansi": "^2.0.0",
+            "strip-ansi": "^3.0.0",
+            "supports-color": "^2.0.0"
+          }
+        },
+        "debug": {
+          "version": "2.6.9",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+          "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "esutils": {
+          "version": "2.0.2",
+          "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz",
+          "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=",
+          "dev": true
+        },
+        "glob": {
+          "version": "7.1.3",
+          "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
+          "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
+          "dev": true,
+          "requires": {
+            "fs.realpath": "^1.0.0",
+            "inflight": "^1.0.4",
+            "inherits": "2",
+            "minimatch": "^3.0.4",
+            "once": "^1.3.0",
+            "path-is-absolute": "^1.0.0"
+          }
+        },
+        "globals": {
+          "version": "9.18.0",
+          "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz",
+          "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==",
+          "dev": true
+        },
+        "lodash": {
+          "version": "4.17.15",
+          "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
+          "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
+          "dev": true
+        },
+        "mkdirp": {
+          "version": "0.5.1",
+          "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
+          "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
+          "dev": true,
+          "requires": {
+            "minimist": "0.0.8"
+          }
+        },
+        "strip-bom": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
+          "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=",
+          "dev": true
+        },
+        "user-home": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/user-home/-/user-home-2.0.0.tgz",
+          "integrity": "sha1-nHC/2Babwdy/SGBODwS4tJzenp8=",
+          "dev": true,
+          "requires": {
+            "os-homedir": "^1.0.0"
+          }
+        }
+      }
+    },
+    "eslint-config-airbnb": {
+      "version": "14.1.0",
+      "resolved": "http://registry.npm.taobao.org/eslint-config-airbnb/download/eslint-config-airbnb-14.1.0.tgz",
+      "integrity": "sha512-rnlT7PFwtYw2ozyLlyk6qZXhvkgBtp3Jm5loqvN5qTy1UMwY6GmlUL6jTA4T2JVFNTFeOB7YYz/rayEX/p+GGQ==",
+      "dev": true,
+      "requires": {
+        "eslint-config-airbnb-base": "^11.1.0"
+      },
+      "dependencies": {
+        "eslint-config-airbnb-base": {
+          "version": "11.3.2",
+          "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-11.3.2.tgz",
+          "integrity": "sha512-/fhjt/VqzBA2SRsx7ErDtv6Ayf+XLw9LIOqmpBuHFCVwyJo2EtzGWMB9fYRFBoWWQLxmNmCpenNiH0RxyeS41w==",
+          "dev": true,
+          "requires": {
+            "eslint-restricted-globals": "^0.1.1"
+          }
+        }
+      }
+    },
+    "eslint-config-airbnb-base": {
+      "version": "11.3.2",
+      "resolved": "http://registry.npm.taobao.org/eslint-config-airbnb-base/download/eslint-config-airbnb-base-11.3.2.tgz",
+      "integrity": "sha512-/fhjt/VqzBA2SRsx7ErDtv6Ayf+XLw9LIOqmpBuHFCVwyJo2EtzGWMB9fYRFBoWWQLxmNmCpenNiH0RxyeS41w==",
+      "dev": true,
+      "requires": {
+        "eslint-restricted-globals": "^0.1.1"
+      },
+      "dependencies": {
+        "eslint-restricted-globals": {
+          "version": "0.1.1",
+          "resolved": "https://registry.npmjs.org/eslint-restricted-globals/-/eslint-restricted-globals-0.1.1.tgz",
+          "integrity": "sha1-NfDVy8ZMLj7WLpO0saevBbp+1Nc=",
+          "dev": true
+        }
+      }
+    },
+    "eslint-import-resolver-node": {
+      "version": "0.3.2",
+      "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz",
+      "integrity": "sha512-sfmTqJfPSizWu4aymbPr4Iidp5yKm8yDkHp+Ir3YiTHiiDfxh69mOUsmiqW6RZ9zRXFaF64GtYmN7e+8GHBv6Q==",
+      "dev": true,
+      "requires": {
+        "debug": "^2.6.9",
+        "resolve": "^1.5.0"
+      }
+    },
+    "eslint-module-utils": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.2.0.tgz",
+      "integrity": "sha1-snA2LNiLGkitMIl2zn+lTphBF0Y=",
+      "dev": true,
+      "requires": {
+        "debug": "^2.6.8",
+        "pkg-dir": "^1.0.0"
+      }
+    },
+    "eslint-plugin-import": {
+      "version": "2.13.0",
+      "resolved": "http://registry.npm.taobao.org/eslint-plugin-import/download/eslint-plugin-import-2.13.0.tgz",
+      "integrity": "sha512-t6hGKQDMIt9N8R7vLepsYXgDfeuhp6ZJSgtrLEDxonpSubyxUZHjhm6LsAaZX8q6GYVxkbT3kTsV9G5mBCFR6A==",
+      "dev": true,
+      "requires": {
+        "contains-path": "^0.1.0",
+        "debug": "^2.6.8",
+        "doctrine": "1.5.0",
+        "eslint-import-resolver-node": "^0.3.1",
+        "eslint-module-utils": "^2.2.0",
+        "has": "^1.0.1",
+        "lodash": "^4.17.4",
+        "minimatch": "^3.0.3",
+        "read-pkg-up": "^2.0.0",
+        "resolve": "^1.6.0"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "2.6.9",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+          "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "doctrine": {
+          "version": "1.5.0",
+          "resolved": "http://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz",
+          "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=",
+          "dev": true,
+          "requires": {
+            "esutils": "^2.0.2",
+            "isarray": "^1.0.0"
+          }
+        },
+        "find-up": {
+          "version": "2.1.0",
+          "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz",
+          "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=",
+          "dev": true,
+          "requires": {
+            "locate-path": "^2.0.0"
+          }
+        },
+        "load-json-file": {
+          "version": "2.0.0",
+          "resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz",
+          "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=",
+          "dev": true,
+          "requires": {
+            "graceful-fs": "^4.1.2",
+            "parse-json": "^2.2.0",
+            "pify": "^2.0.0",
+            "strip-bom": "^3.0.0"
+          }
+        },
+        "lodash": {
+          "version": "4.17.15",
+          "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
+          "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
+          "dev": true
+        },
+        "minimatch": {
+          "version": "3.0.4",
+          "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+          "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+          "dev": true,
+          "requires": {
+            "brace-expansion": "^1.1.7"
+          }
+        },
+        "path-type": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz",
+          "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=",
+          "dev": true,
+          "requires": {
+            "pify": "^2.0.0"
+          }
+        },
+        "read-pkg": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz",
+          "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=",
+          "dev": true,
+          "requires": {
+            "load-json-file": "^2.0.0",
+            "normalize-package-data": "^2.3.2",
+            "path-type": "^2.0.0"
+          }
+        },
+        "read-pkg-up": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz",
+          "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=",
+          "dev": true,
+          "requires": {
+            "find-up": "^2.0.0",
+            "read-pkg": "^2.0.0"
+          }
+        },
+        "resolve": {
+          "version": "1.8.1",
+          "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.8.1.tgz",
+          "integrity": "sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA==",
+          "dev": true,
+          "requires": {
+            "path-parse": "^1.0.5"
+          }
+        },
+        "strip-bom": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
+          "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=",
+          "dev": true
+        }
+      }
+    },
+    "eslint-restricted-globals": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/eslint-restricted-globals/-/eslint-restricted-globals-0.1.1.tgz",
+      "integrity": "sha1-NfDVy8ZMLj7WLpO0saevBbp+1Nc=",
+      "dev": true
+    },
+    "espree": {
+      "version": "3.5.4",
+      "resolved": "http://registry.npmjs.org/espree/-/espree-3.5.4.tgz",
+      "integrity": "sha512-yAcIQxtmMiB/jL32dzEp2enBeidsB7xWPLNiw3IIkpVds1P+h7qF9YwJq1yUNzp2OKXgAprs4F61ih66UsoD1A==",
+      "dev": true,
+      "requires": {
+        "acorn": "^5.5.0",
+        "acorn-jsx": "^3.0.0"
+      }
+    },
+    "esprima": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+      "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+      "dev": true
+    },
+    "esquery": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz",
+      "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==",
+      "dev": true,
+      "requires": {
+        "estraverse": "^4.0.0"
+      }
+    },
+    "esrecurse": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz",
+      "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==",
+      "dev": true,
+      "requires": {
+        "estraverse": "^4.1.0"
+      }
+    },
+    "estraverse": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz",
+      "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=",
+      "dev": true
+    },
+    "esutils": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz",
+      "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=",
+      "dev": true
+    },
+    "event-emitter": {
+      "version": "0.3.5",
+      "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz",
+      "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=",
+      "dev": true,
+      "requires": {
+        "d": "1",
+        "es5-ext": "~0.10.14"
+      }
+    },
+    "exec-sh": {
+      "version": "0.2.2",
+      "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.2.2.tgz",
+      "integrity": "sha512-FIUCJz1RbuS0FKTdaAafAByGS0CPvU3R0MeHxgtl+djzCc//F8HakL8GzmVNZanasTbTAY/3DRFA0KpVqj/eAw==",
+      "dev": true,
+      "requires": {
+        "merge": "^1.2.0"
+      }
+    },
+    "exit-hook": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz",
+      "integrity": "sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=",
+      "dev": true
+    },
+    "expand-brackets": {
+      "version": "0.1.5",
+      "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz",
+      "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=",
+      "dev": true,
+      "requires": {
+        "is-posix-bracket": "^0.1.0"
+      }
+    },
+    "expand-range": {
+      "version": "1.8.2",
+      "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz",
+      "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=",
+      "dev": true,
+      "requires": {
+        "fill-range": "^2.1.0"
+      }
+    },
+    "extend": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+      "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
+      "dev": true
+    },
+    "extend-shallow": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz",
+      "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "assign-symbols": "^1.0.0",
+        "is-extendable": "^1.0.1"
+      },
+      "dependencies": {
+        "is-extendable": {
+          "version": "1.0.1",
+          "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz",
+          "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "is-plain-object": "^2.0.4"
+          }
+        }
+      }
+    },
+    "extglob": {
+      "version": "0.3.2",
+      "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz",
+      "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=",
+      "dev": true,
+      "requires": {
+        "is-extglob": "^1.0.0"
+      }
+    },
+    "extsprintf": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
+      "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=",
+      "dev": true
+    },
+    "fast-deep-equal": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
+      "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=",
+      "dev": true
+    },
+    "fast-json-stable-stringify": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz",
+      "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=",
+      "dev": true
+    },
+    "fast-levenshtein": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+      "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
+      "dev": true
+    },
+    "fast-safe-stringify": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.6.tgz",
+      "integrity": "sha512-q8BZ89jjc+mz08rSxROs8VsrBBcn1SIw1kq9NjolL509tkABRk9io01RAjSaEv1Xb2uFLt8VtRiZbGp5H8iDtg=="
+    },
+    "fb-watchman": {
+      "version": "1.9.2",
+      "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-1.9.2.tgz",
+      "integrity": "sha1-okz0eCf4LTj7Waaa1wt247auc4M=",
+      "dev": true,
+      "requires": {
+        "bser": "1.0.2"
+      }
+    },
+    "fecha": {
+      "version": "2.3.3",
+      "resolved": "http://registry.npmjs.org/fecha/-/fecha-2.3.3.tgz",
+      "integrity": "sha512-lUGBnIamTAwk4znq5BcqsDaxSmZ9nDVJaij6NvRt/Tg4R69gERA+otPKbS86ROw9nxVMw2/mp1fnaiWqbs6Sdg=="
+    },
+    "figures": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz",
+      "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=",
+      "dev": true,
+      "requires": {
+        "escape-string-regexp": "^1.0.5",
+        "object-assign": "^4.1.0"
+      }
+    },
+    "file-entry-cache": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz",
+      "integrity": "sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=",
+      "dev": true,
+      "requires": {
+        "flat-cache": "^1.2.1",
+        "object-assign": "^4.0.1"
+      }
+    },
+    "filename-regex": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz",
+      "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=",
+      "dev": true
+    },
+    "fileset": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/fileset/-/fileset-2.0.3.tgz",
+      "integrity": "sha1-jnVIqW08wjJ+5eZ0FocjozO7oqA=",
+      "dev": true,
+      "requires": {
+        "glob": "^7.0.3",
+        "minimatch": "^3.0.3"
+      }
+    },
+    "fill-range": {
+      "version": "2.2.4",
+      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.4.tgz",
+      "integrity": "sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==",
+      "dev": true,
+      "requires": {
+        "is-number": "^2.1.0",
+        "isobject": "^2.0.0",
+        "randomatic": "^3.0.0",
+        "repeat-element": "^1.1.2",
+        "repeat-string": "^1.5.2"
+      }
+    },
+    "find-up": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz",
+      "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=",
+      "dev": true,
+      "requires": {
+        "path-exists": "^2.0.0",
+        "pinkie-promise": "^2.0.0"
+      }
+    },
+    "flat-cache": {
+      "version": "1.3.4",
+      "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.4.tgz",
+      "integrity": "sha512-VwyB3Lkgacfik2vhqR4uv2rvebqmDvFu4jlN/C1RzWoJEo8I7z4Q404oiqYCkq41mni8EzQnm95emU9seckwtg==",
+      "dev": true,
+      "requires": {
+        "circular-json": "^0.3.1",
+        "graceful-fs": "^4.1.2",
+        "rimraf": "~2.6.2",
+        "write": "^0.2.1"
+      }
+    },
+    "for-in": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
+      "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=",
+      "dev": true
+    },
+    "for-own": {
+      "version": "0.1.5",
+      "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz",
+      "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=",
+      "dev": true,
+      "requires": {
+        "for-in": "^1.0.1"
+      }
+    },
+    "forever-agent": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
+      "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=",
+      "dev": true
+    },
+    "form-data": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
+      "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
+      "dev": true,
+      "requires": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.6",
+        "mime-types": "^2.1.12"
+      }
+    },
+    "fragment-cache": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz",
+      "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "map-cache": "^0.2.2"
+      }
+    },
+    "fs-readdir-recursive": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz",
+      "integrity": "sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==",
+      "dev": true
+    },
+    "fs.realpath": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+      "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
+      "dev": true
+    },
+    "fsevents": {
+      "version": "1.2.9",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.9.tgz",
+      "integrity": "sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "nan": "^2.12.1",
+        "node-pre-gyp": "^0.12.0"
+      },
+      "dependencies": {
+        "abbrev": {
+          "version": "1.1.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "ansi-regex": {
+          "version": "2.1.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "aproba": {
+          "version": "1.2.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "are-we-there-yet": {
+          "version": "1.1.5",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "delegates": "^1.0.0",
+            "readable-stream": "^2.0.6"
+          }
+        },
+        "balanced-match": {
+          "version": "1.0.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "brace-expansion": {
+          "version": "1.1.11",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "balanced-match": "^1.0.0",
+            "concat-map": "0.0.1"
+          }
+        },
+        "chownr": {
+          "version": "1.1.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "code-point-at": {
+          "version": "1.1.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "concat-map": {
+          "version": "0.0.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "console-control-strings": {
+          "version": "1.1.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "core-util-is": {
+          "version": "1.0.2",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "debug": {
+          "version": "4.1.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "ms": "^2.1.1"
+          }
+        },
+        "deep-extend": {
+          "version": "0.6.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "delegates": {
+          "version": "1.0.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "detect-libc": {
+          "version": "1.0.3",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "fs-minipass": {
+          "version": "1.2.5",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "minipass": "^2.2.1"
+          }
+        },
+        "fs.realpath": {
+          "version": "1.0.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "gauge": {
+          "version": "2.7.4",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "aproba": "^1.0.3",
+            "console-control-strings": "^1.0.0",
+            "has-unicode": "^2.0.0",
+            "object-assign": "^4.1.0",
+            "signal-exit": "^3.0.0",
+            "string-width": "^1.0.1",
+            "strip-ansi": "^3.0.1",
+            "wide-align": "^1.1.0"
+          }
+        },
+        "glob": {
+          "version": "7.1.3",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "fs.realpath": "^1.0.0",
+            "inflight": "^1.0.4",
+            "inherits": "2",
+            "minimatch": "^3.0.4",
+            "once": "^1.3.0",
+            "path-is-absolute": "^1.0.0"
+          }
+        },
+        "has-unicode": {
+          "version": "2.0.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "iconv-lite": {
+          "version": "0.4.24",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "safer-buffer": ">= 2.1.2 < 3"
+          }
+        },
+        "ignore-walk": {
+          "version": "3.0.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "minimatch": "^3.0.4"
+          }
+        },
+        "inflight": {
+          "version": "1.0.6",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "once": "^1.3.0",
+            "wrappy": "1"
+          }
+        },
+        "inherits": {
+          "version": "2.0.3",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "ini": {
+          "version": "1.3.5",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "is-fullwidth-code-point": {
+          "version": "1.0.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "number-is-nan": "^1.0.0"
+          }
+        },
+        "isarray": {
+          "version": "1.0.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "minimatch": {
+          "version": "3.0.4",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "brace-expansion": "^1.1.7"
+          }
+        },
+        "minimist": {
+          "version": "0.0.8",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "minipass": {
+          "version": "2.3.5",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "safe-buffer": "^5.1.2",
+            "yallist": "^3.0.0"
+          }
+        },
+        "minizlib": {
+          "version": "1.2.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "minipass": "^2.2.1"
+          }
+        },
+        "mkdirp": {
+          "version": "0.5.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "minimist": "0.0.8"
+          }
+        },
+        "ms": {
+          "version": "2.1.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "needle": {
+          "version": "2.3.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "debug": "^4.1.0",
+            "iconv-lite": "^0.4.4",
+            "sax": "^1.2.4"
+          }
+        },
+        "node-pre-gyp": {
+          "version": "0.12.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "detect-libc": "^1.0.2",
+            "mkdirp": "^0.5.1",
+            "needle": "^2.2.1",
+            "nopt": "^4.0.1",
+            "npm-packlist": "^1.1.6",
+            "npmlog": "^4.0.2",
+            "rc": "^1.2.7",
+            "rimraf": "^2.6.1",
+            "semver": "^5.3.0",
+            "tar": "^4"
+          }
+        },
+        "nopt": {
+          "version": "4.0.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "abbrev": "1",
+            "osenv": "^0.1.4"
+          }
+        },
+        "npm-bundled": {
+          "version": "1.0.6",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "npm-packlist": {
+          "version": "1.4.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "ignore-walk": "^3.0.1",
+            "npm-bundled": "^1.0.1"
+          }
+        },
+        "npmlog": {
+          "version": "4.1.2",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "are-we-there-yet": "~1.1.2",
+            "console-control-strings": "~1.1.0",
+            "gauge": "~2.7.3",
+            "set-blocking": "~2.0.0"
+          }
+        },
+        "number-is-nan": {
+          "version": "1.0.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "object-assign": {
+          "version": "4.1.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "once": {
+          "version": "1.4.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "wrappy": "1"
+          }
+        },
+        "os-homedir": {
+          "version": "1.0.2",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "os-tmpdir": {
+          "version": "1.0.2",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "osenv": {
+          "version": "0.1.5",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "os-homedir": "^1.0.0",
+            "os-tmpdir": "^1.0.0"
+          }
+        },
+        "path-is-absolute": {
+          "version": "1.0.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "process-nextick-args": {
+          "version": "2.0.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "rc": {
+          "version": "1.2.8",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "deep-extend": "^0.6.0",
+            "ini": "~1.3.0",
+            "minimist": "^1.2.0",
+            "strip-json-comments": "~2.0.1"
+          },
+          "dependencies": {
+            "minimist": {
+              "version": "1.2.0",
+              "bundled": true,
+              "dev": true,
+              "optional": true
+            }
+          }
+        },
+        "readable-stream": {
+          "version": "2.3.6",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "core-util-is": "~1.0.0",
+            "inherits": "~2.0.3",
+            "isarray": "~1.0.0",
+            "process-nextick-args": "~2.0.0",
+            "safe-buffer": "~5.1.1",
+            "string_decoder": "~1.1.1",
+            "util-deprecate": "~1.0.1"
+          }
+        },
+        "rimraf": {
+          "version": "2.6.3",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "glob": "^7.1.3"
+          }
+        },
+        "safe-buffer": {
+          "version": "5.1.2",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "safer-buffer": {
+          "version": "2.1.2",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "sax": {
+          "version": "1.2.4",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "semver": {
+          "version": "5.7.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "set-blocking": {
+          "version": "2.0.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "signal-exit": {
+          "version": "3.0.2",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "string-width": {
+          "version": "1.0.2",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "code-point-at": "^1.0.0",
+            "is-fullwidth-code-point": "^1.0.0",
+            "strip-ansi": "^3.0.0"
+          }
+        },
+        "string_decoder": {
+          "version": "1.1.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "safe-buffer": "~5.1.0"
+          }
+        },
+        "strip-ansi": {
+          "version": "3.0.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "ansi-regex": "^2.0.0"
+          }
+        },
+        "strip-json-comments": {
+          "version": "2.0.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "tar": {
+          "version": "4.4.8",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "chownr": "^1.1.1",
+            "fs-minipass": "^1.2.5",
+            "minipass": "^2.3.4",
+            "minizlib": "^1.1.1",
+            "mkdirp": "^0.5.0",
+            "safe-buffer": "^5.1.2",
+            "yallist": "^3.0.2"
+          }
+        },
+        "util-deprecate": {
+          "version": "1.0.2",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "wide-align": {
+          "version": "1.1.3",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "string-width": "^1.0.2 || 2"
+          }
+        },
+        "wrappy": {
+          "version": "1.0.2",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "yallist": {
+          "version": "3.0.3",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        }
+      }
+    },
+    "function-bind": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+      "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
+      "dev": true
+    },
+    "generate-function": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
+      "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==",
+      "dev": true,
+      "requires": {
+        "is-property": "^1.0.2"
+      }
+    },
+    "generate-object-property": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz",
+      "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=",
+      "dev": true,
+      "requires": {
+        "is-property": "^1.0.0"
+      }
+    },
+    "get-caller-file": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz",
+      "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==",
+      "dev": true
+    },
+    "get-value": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz",
+      "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=",
+      "dev": true,
+      "optional": true
+    },
+    "getpass": {
+      "version": "0.1.7",
+      "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
+      "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=",
+      "dev": true,
+      "requires": {
+        "assert-plus": "^1.0.0"
+      }
+    },
+    "glob": {
+      "version": "7.1.3",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
+      "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
+      "dev": true,
+      "requires": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.0.4",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      }
+    },
+    "glob-base": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz",
+      "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=",
+      "dev": true,
+      "requires": {
+        "glob-parent": "^2.0.0",
+        "is-glob": "^2.0.0"
+      }
+    },
+    "glob-parent": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz",
+      "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=",
+      "dev": true,
+      "requires": {
+        "is-glob": "^2.0.0"
+      }
+    },
+    "globals": {
+      "version": "9.18.0",
+      "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz",
+      "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==",
+      "dev": true
+    },
+    "graceful-fs": {
+      "version": "4.1.15",
+      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz",
+      "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==",
+      "dev": true
+    },
+    "growly": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz",
+      "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=",
+      "dev": true
+    },
+    "handlebars": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.2.0.tgz",
+      "integrity": "sha512-Kb4xn5Qh1cxAKvQnzNWZ512DhABzyFNmsaJf3OAkWNa4NkaqWcNI8Tao8Tasi0/F4JD9oyG0YxuFyvyR57d+Gw==",
+      "dev": true,
+      "requires": {
+        "neo-async": "^2.6.0",
+        "optimist": "^0.6.1",
+        "source-map": "^0.6.1",
+        "uglify-js": "^3.1.4"
+      },
+      "dependencies": {
+        "source-map": {
+          "version": "0.6.1",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+          "dev": true
+        }
+      }
+    },
+    "har-schema": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
+      "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=",
+      "dev": true
+    },
+    "har-validator": {
+      "version": "5.1.3",
+      "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz",
+      "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==",
+      "dev": true,
+      "requires": {
+        "ajv": "^6.5.5",
+        "har-schema": "^2.0.0"
+      },
+      "dependencies": {
+        "ajv": {
+          "version": "6.5.5",
+          "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.5.5.tgz",
+          "integrity": "sha512-7q7gtRQDJSyuEHjuVgHoUa2VuemFiCMrfQc9Tc08XTAc4Zj/5U1buQJ0HU6i7fKjXU09SVgSmxa4sLvuvS8Iyg==",
+          "dev": true,
+          "requires": {
+            "fast-deep-equal": "^2.0.1",
+            "fast-json-stable-stringify": "^2.0.0",
+            "json-schema-traverse": "^0.4.1",
+            "uri-js": "^4.2.2"
+          }
+        }
+      }
+    },
+    "has": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+      "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+      "dev": true,
+      "requires": {
+        "function-bind": "^1.1.1"
+      }
+    },
+    "has-ansi": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
+      "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=",
+      "dev": true,
+      "requires": {
+        "ansi-regex": "^2.0.0"
+      }
+    },
+    "has-flag": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz",
+      "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=",
+      "dev": true
+    },
+    "has-value": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz",
+      "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "get-value": "^2.0.6",
+        "has-values": "^1.0.0",
+        "isobject": "^3.0.0"
+      },
+      "dependencies": {
+        "isobject": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
+          "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
+          "dev": true,
+          "optional": true
+        }
+      }
+    },
+    "has-values": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz",
+      "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "is-number": "^3.0.0",
+        "kind-of": "^4.0.0"
+      },
+      "dependencies": {
+        "is-number": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
+          "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "kind-of": "^3.0.2"
+          },
+          "dependencies": {
+            "kind-of": {
+              "version": "3.2.2",
+              "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+              "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+              "dev": true,
+              "optional": true,
+              "requires": {
+                "is-buffer": "^1.1.5"
+              }
+            }
+          }
+        },
+        "kind-of": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz",
+          "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "is-buffer": "^1.1.5"
+          }
+        }
+      }
+    },
+    "home-or-tmp": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz",
+      "integrity": "sha1-42w/LSyufXRqhX440Y1fMqeILbg=",
+      "dev": true,
+      "requires": {
+        "os-homedir": "^1.0.0",
+        "os-tmpdir": "^1.0.1"
+      }
+    },
+    "hosted-git-info": {
+      "version": "2.7.1",
+      "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz",
+      "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==",
+      "dev": true
+    },
+    "html-encoding-sniffer": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz",
+      "integrity": "sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==",
+      "dev": true,
+      "requires": {
+        "whatwg-encoding": "^1.0.1"
+      }
+    },
+    "http-signature": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
+      "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=",
+      "dev": true,
+      "requires": {
+        "assert-plus": "^1.0.0",
+        "jsprim": "^1.2.2",
+        "sshpk": "^1.7.0"
+      }
+    },
+    "iconv-lite": {
+      "version": "0.4.24",
+      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+      "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+      "dev": true,
+      "requires": {
+        "safer-buffer": ">= 2.1.2 < 3"
+      }
+    },
+    "ignore": {
+      "version": "3.3.10",
+      "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz",
+      "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==",
+      "dev": true
+    },
+    "imurmurhash": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+      "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=",
+      "dev": true
+    },
+    "inflight": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+      "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+      "dev": true,
+      "requires": {
+        "once": "^1.3.0",
+        "wrappy": "1"
+      }
+    },
+    "inherits": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+      "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
+    },
+    "inquirer": {
+      "version": "0.12.0",
+      "resolved": "http://registry.npmjs.org/inquirer/-/inquirer-0.12.0.tgz",
+      "integrity": "sha1-HvK/1jUE3wvHV4X/+MLEHfEvB34=",
+      "dev": true,
+      "requires": {
+        "ansi-escapes": "^1.1.0",
+        "ansi-regex": "^2.0.0",
+        "chalk": "^1.0.0",
+        "cli-cursor": "^1.0.1",
+        "cli-width": "^2.0.0",
+        "figures": "^1.3.5",
+        "lodash": "^4.3.0",
+        "readline2": "^1.0.1",
+        "run-async": "^0.1.0",
+        "rx-lite": "^3.1.2",
+        "string-width": "^1.0.1",
+        "strip-ansi": "^3.0.0",
+        "through": "^2.3.6"
+      },
+      "dependencies": {
+        "chalk": {
+          "version": "1.1.3",
+          "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+          "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^2.2.1",
+            "escape-string-regexp": "^1.0.2",
+            "has-ansi": "^2.0.0",
+            "strip-ansi": "^3.0.0",
+            "supports-color": "^2.0.0"
+          }
+        }
+      }
+    },
+    "interpret": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz",
+      "integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ=",
+      "dev": true
+    },
+    "invariant": {
+      "version": "2.2.4",
+      "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
+      "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
+      "dev": true,
+      "requires": {
+        "loose-envify": "^1.0.0"
+      }
+    },
+    "invert-kv": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz",
+      "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=",
+      "dev": true
+    },
+    "ip": {
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz",
+      "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo="
+    },
+    "is-accessor-descriptor": {
+      "version": "0.1.6",
+      "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz",
+      "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "kind-of": "^3.0.2"
+      }
+    },
+    "is-arrayish": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+      "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=",
+      "dev": true
+    },
+    "is-binary-path": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz",
+      "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "binary-extensions": "^1.0.0"
+      }
+    },
+    "is-buffer": {
+      "version": "1.1.6",
+      "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
+      "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==",
+      "dev": true
+    },
+    "is-builtin-module": {
+      "version": "1.0.0",
+      "resolved": "http://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz",
+      "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=",
+      "dev": true,
+      "requires": {
+        "builtin-modules": "^1.0.0"
+      }
+    },
+    "is-ci": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.2.1.tgz",
+      "integrity": "sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==",
+      "dev": true,
+      "requires": {
+        "ci-info": "^1.5.0"
+      }
+    },
+    "is-data-descriptor": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz",
+      "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "kind-of": "^3.0.2"
+      }
+    },
+    "is-descriptor": {
+      "version": "0.1.6",
+      "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz",
+      "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "is-accessor-descriptor": "^0.1.6",
+        "is-data-descriptor": "^0.1.4",
+        "kind-of": "^5.0.0"
+      },
+      "dependencies": {
+        "kind-of": {
+          "version": "5.1.0",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz",
+          "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==",
+          "dev": true,
+          "optional": true
+        }
+      }
+    },
+    "is-dotfile": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz",
+      "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=",
+      "dev": true
+    },
+    "is-equal-shallow": {
+      "version": "0.1.3",
+      "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz",
+      "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=",
+      "dev": true,
+      "requires": {
+        "is-primitive": "^2.0.0"
+      }
+    },
+    "is-extendable": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
+      "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=",
+      "dev": true
+    },
+    "is-extglob": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
+      "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=",
+      "dev": true
+    },
+    "is-finite": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz",
+      "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=",
+      "dev": true,
+      "requires": {
+        "number-is-nan": "^1.0.0"
+      }
+    },
+    "is-fullwidth-code-point": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
+      "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
+      "dev": true,
+      "requires": {
+        "number-is-nan": "^1.0.0"
+      }
+    },
+    "is-glob": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz",
+      "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=",
+      "dev": true,
+      "requires": {
+        "is-extglob": "^1.0.0"
+      }
+    },
+    "is-my-ip-valid": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz",
+      "integrity": "sha512-gmh/eWXROncUzRnIa1Ubrt5b8ep/MGSnfAUI3aRp+sqTCs1tv1Isl8d8F6JmkN3dXKc3ehZMrtiPN9eL03NuaQ==",
+      "dev": true
+    },
+    "is-my-json-valid": {
+      "version": "2.19.0",
+      "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.19.0.tgz",
+      "integrity": "sha512-mG0f/unGX1HZ5ep4uhRaPOS8EkAY8/j6mDRMJrutq4CqhoJWYp7qAlonIPy3TV7p3ju4TK9fo/PbnoksWmsp5Q==",
+      "dev": true,
+      "requires": {
+        "generate-function": "^2.0.0",
+        "generate-object-property": "^1.1.0",
+        "is-my-ip-valid": "^1.0.0",
+        "jsonpointer": "^4.0.0",
+        "xtend": "^4.0.0"
+      }
+    },
+    "is-number": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz",
+      "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=",
+      "dev": true,
+      "requires": {
+        "kind-of": "^3.0.2"
+      }
+    },
+    "is-plain-object": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
+      "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "isobject": "^3.0.1"
+      },
+      "dependencies": {
+        "isobject": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
+          "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
+          "dev": true,
+          "optional": true
+        }
+      }
+    },
+    "is-posix-bracket": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz",
+      "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=",
+      "dev": true
+    },
+    "is-primitive": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz",
+      "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=",
+      "dev": true
+    },
+    "is-property": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
+      "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=",
+      "dev": true
+    },
+    "is-resolvable": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz",
+      "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==",
+      "dev": true
+    },
+    "is-stream": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
+      "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ="
+    },
+    "is-typedarray": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
+      "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=",
+      "dev": true
+    },
+    "is-utf8": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz",
+      "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=",
+      "dev": true
+    },
+    "is-windows": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz",
+      "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==",
+      "dev": true,
+      "optional": true
+    },
+    "isarray": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+      "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
+    },
+    "isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+      "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
+      "dev": true
+    },
+    "isobject": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz",
+      "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=",
+      "dev": true,
+      "requires": {
+        "isarray": "1.0.0"
+      }
+    },
+    "isstream": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
+      "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=",
+      "dev": true
+    },
+    "istanbul-api": {
+      "version": "1.3.7",
+      "resolved": "https://registry.npmjs.org/istanbul-api/-/istanbul-api-1.3.7.tgz",
+      "integrity": "sha512-4/ApBnMVeEPG3EkSzcw25wDe4N66wxwn+KKn6b47vyek8Xb3NBAcg4xfuQbS7BqcZuTX4wxfD5lVagdggR3gyA==",
+      "dev": true,
+      "requires": {
+        "async": "^2.1.4",
+        "fileset": "^2.0.2",
+        "istanbul-lib-coverage": "^1.2.1",
+        "istanbul-lib-hook": "^1.2.2",
+        "istanbul-lib-instrument": "^1.10.2",
+        "istanbul-lib-report": "^1.1.5",
+        "istanbul-lib-source-maps": "^1.2.6",
+        "istanbul-reports": "^1.5.1",
+        "js-yaml": "^3.7.0",
+        "mkdirp": "^0.5.1",
+        "once": "^1.4.0"
+      },
+      "dependencies": {
+        "async": {
+          "version": "2.6.1",
+          "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz",
+          "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==",
+          "dev": true,
+          "requires": {
+            "lodash": "^4.17.10"
+          }
+        }
+      }
+    },
+    "istanbul-lib-coverage": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.1.tgz",
+      "integrity": "sha512-PzITeunAgyGbtY1ibVIUiV679EFChHjoMNRibEIobvmrCRaIgwLxNucOSimtNWUhEib/oO7QY2imD75JVgCJWQ==",
+      "dev": true
+    },
+    "istanbul-lib-hook": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-1.2.2.tgz",
+      "integrity": "sha512-/Jmq7Y1VeHnZEQ3TL10VHyb564mn6VrQXHchON9Jf/AEcmQ3ZIiyD1BVzNOKTZf/G3gE+kiGK6SmpF9y3qGPLw==",
+      "dev": true,
+      "requires": {
+        "append-transform": "^0.4.0"
+      }
+    },
+    "istanbul-lib-instrument": {
+      "version": "1.10.2",
+      "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-1.10.2.tgz",
+      "integrity": "sha512-aWHxfxDqvh/ZlxR8BBaEPVSWDPUkGD63VjGQn3jcw8jCp7sHEMKcrj4xfJn/ABzdMEHiQNyvDQhqm5o8+SQg7A==",
+      "dev": true,
+      "requires": {
+        "babel-generator": "^6.18.0",
+        "babel-template": "^6.16.0",
+        "babel-traverse": "^6.18.0",
+        "babel-types": "^6.18.0",
+        "babylon": "^6.18.0",
+        "istanbul-lib-coverage": "^1.2.1",
+        "semver": "^5.3.0"
+      }
+    },
+    "istanbul-lib-report": {
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-1.1.5.tgz",
+      "integrity": "sha512-UsYfRMoi6QO/doUshYNqcKJqVmFe9w51GZz8BS3WB0lYxAllQYklka2wP9+dGZeHYaWIdcXUx8JGdbqaoXRXzw==",
+      "dev": true,
+      "requires": {
+        "istanbul-lib-coverage": "^1.2.1",
+        "mkdirp": "^0.5.1",
+        "path-parse": "^1.0.5",
+        "supports-color": "^3.1.2"
+      },
+      "dependencies": {
+        "supports-color": {
+          "version": "3.2.3",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz",
+          "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=",
+          "dev": true,
+          "requires": {
+            "has-flag": "^1.0.0"
+          }
+        }
+      }
+    },
+    "istanbul-lib-source-maps": {
+      "version": "1.2.6",
+      "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.6.tgz",
+      "integrity": "sha512-TtbsY5GIHgbMsMiRw35YBHGpZ1DVFEO19vxxeiDMYaeOFOCzfnYVxvl6pOUIZR4dtPhAGpSMup8OyF8ubsaqEg==",
+      "dev": true,
+      "requires": {
+        "debug": "^3.1.0",
+        "istanbul-lib-coverage": "^1.2.1",
+        "mkdirp": "^0.5.1",
+        "rimraf": "^2.6.1",
+        "source-map": "^0.5.3"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.2.6",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
+          "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
+          "dev": true,
+          "requires": {
+            "ms": "^2.1.1"
+          }
+        },
+        "ms": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
+          "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==",
+          "dev": true
+        }
+      }
+    },
+    "istanbul-reports": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-1.5.1.tgz",
+      "integrity": "sha512-+cfoZ0UXzWjhAdzosCPP3AN8vvef8XDkWtTfgaN+7L3YTpNYITnCaEkceo5SEYy644VkHka/P1FvkWvrG/rrJw==",
+      "dev": true,
+      "requires": {
+        "handlebars": "^4.0.3"
+      }
+    },
+    "jest": {
+      "version": "18.1.0",
+      "resolved": "http://registry.npm.taobao.org/jest/download/jest-18.1.0.tgz",
+      "integrity": "sha512-pkxWTDzr81rRHCXUz6h44ZJIpmOBkn64eXlj64hDGsSzIL74Pl/CTx5M50LC/WdqtfLY6pVs9k0cgzT4GRw/8g==",
+      "dev": true,
+      "requires": {
+        "jest-cli": "^18.1.0"
+      },
+      "dependencies": {
+        "callsites": {
+          "version": "2.0.0",
+          "resolved": "http://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz",
+          "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=",
+          "dev": true
+        },
+        "jest-cli": {
+          "version": "18.1.0",
+          "resolved": "http://registry.npmjs.org/jest-cli/-/jest-cli-18.1.0.tgz",
+          "integrity": "sha1-Xq027K1CCBfCybqiqnV09jJXs9Y=",
+          "dev": true,
+          "requires": {
+            "ansi-escapes": "^1.4.0",
+            "callsites": "^2.0.0",
+            "chalk": "^1.1.1",
+            "graceful-fs": "^4.1.6",
+            "is-ci": "^1.0.9",
+            "istanbul-api": "^1.1.0-alpha.1",
+            "istanbul-lib-coverage": "^1.0.0",
+            "istanbul-lib-instrument": "^1.1.1",
+            "jest-changed-files": "^17.0.2",
+            "jest-config": "^18.1.0",
+            "jest-environment-jsdom": "^18.1.0",
+            "jest-file-exists": "^17.0.0",
+            "jest-haste-map": "^18.1.0",
+            "jest-jasmine2": "^18.1.0",
+            "jest-mock": "^18.0.0",
+            "jest-resolve": "^18.1.0",
+            "jest-resolve-dependencies": "^18.1.0",
+            "jest-runtime": "^18.1.0",
+            "jest-snapshot": "^18.1.0",
+            "jest-util": "^18.1.0",
+            "json-stable-stringify": "^1.0.0",
+            "node-notifier": "^4.6.1",
+            "sane": "~1.4.1",
+            "strip-ansi": "^3.0.1",
+            "throat": "^3.0.0",
+            "which": "^1.1.1",
+            "worker-farm": "^1.3.1",
+            "yargs": "^6.3.0"
+          },
+          "dependencies": {
+            "chalk": {
+              "version": "1.1.3",
+              "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+              "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+              "dev": true,
+              "requires": {
+                "ansi-styles": "^2.2.1",
+                "escape-string-regexp": "^1.0.2",
+                "has-ansi": "^2.0.0",
+                "strip-ansi": "^3.0.0",
+                "supports-color": "^2.0.0"
+              }
+            }
+          }
+        }
+      }
+    },
+    "jest-changed-files": {
+      "version": "17.0.2",
+      "resolved": "http://registry.npmjs.org/jest-changed-files/-/jest-changed-files-17.0.2.tgz",
+      "integrity": "sha1-9WV3WHNplvWQpRuH5ck2nZBLp7c=",
+      "dev": true
+    },
+    "jest-config": {
+      "version": "18.1.0",
+      "resolved": "http://registry.npmjs.org/jest-config/-/jest-config-18.1.0.tgz",
+      "integrity": "sha1-YRF0Cm1Iqrhv9anmqwuYvZk7b/Q=",
+      "dev": true,
+      "requires": {
+        "chalk": "^1.1.1",
+        "jest-environment-jsdom": "^18.1.0",
+        "jest-environment-node": "^18.1.0",
+        "jest-jasmine2": "^18.1.0",
+        "jest-mock": "^18.0.0",
+        "jest-resolve": "^18.1.0",
+        "jest-util": "^18.1.0",
+        "json-stable-stringify": "^1.0.0"
+      },
+      "dependencies": {
+        "chalk": {
+          "version": "1.1.3",
+          "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+          "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^2.2.1",
+            "escape-string-regexp": "^1.0.2",
+            "has-ansi": "^2.0.0",
+            "strip-ansi": "^3.0.0",
+            "supports-color": "^2.0.0"
+          }
+        }
+      }
+    },
+    "jest-diff": {
+      "version": "18.1.0",
+      "resolved": "http://registry.npmjs.org/jest-diff/-/jest-diff-18.1.0.tgz",
+      "integrity": "sha1-T/eedN2YjBORlbNl3GXYf2BvSAM=",
+      "dev": true,
+      "requires": {
+        "chalk": "^1.1.3",
+        "diff": "^3.0.0",
+        "jest-matcher-utils": "^18.1.0",
+        "pretty-format": "^18.1.0"
+      },
+      "dependencies": {
+        "chalk": {
+          "version": "1.1.3",
+          "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+          "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^2.2.1",
+            "escape-string-regexp": "^1.0.2",
+            "has-ansi": "^2.0.0",
+            "strip-ansi": "^3.0.0",
+            "supports-color": "^2.0.0"
+          }
+        }
+      }
+    },
+    "jest-environment-jsdom": {
+      "version": "18.1.0",
+      "resolved": "http://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-18.1.0.tgz",
+      "integrity": "sha1-GLQvDE6iuunzbKs2ObHo+MOE4k4=",
+      "dev": true,
+      "requires": {
+        "jest-mock": "^18.0.0",
+        "jest-util": "^18.1.0",
+        "jsdom": "^9.9.1"
+      }
+    },
+    "jest-environment-node": {
+      "version": "18.1.0",
+      "resolved": "http://registry.npmjs.org/jest-environment-node/-/jest-environment-node-18.1.0.tgz",
+      "integrity": "sha1-TWeXVyyN2pms9frmlutilFVHx3k=",
+      "dev": true,
+      "requires": {
+        "jest-mock": "^18.0.0",
+        "jest-util": "^18.1.0"
+      }
+    },
+    "jest-file-exists": {
+      "version": "17.0.0",
+      "resolved": "https://registry.npmjs.org/jest-file-exists/-/jest-file-exists-17.0.0.tgz",
+      "integrity": "sha1-f2Prc6HEOhP0Yb4mF2i0WvLN0Wk=",
+      "dev": true
+    },
+    "jest-haste-map": {
+      "version": "18.1.0",
+      "resolved": "http://registry.npmjs.org/jest-haste-map/-/jest-haste-map-18.1.0.tgz",
+      "integrity": "sha1-BoOcdLdwpAwaEGlohR340oHAg3U=",
+      "dev": true,
+      "requires": {
+        "fb-watchman": "^1.9.0",
+        "graceful-fs": "^4.1.6",
+        "micromatch": "^2.3.11",
+        "sane": "~1.4.1",
+        "worker-farm": "^1.3.1"
+      }
+    },
+    "jest-jasmine2": {
+      "version": "18.1.0",
+      "resolved": "http://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-18.1.0.tgz",
+      "integrity": "sha1-CU4QTCwYlwh2bHcmO7Kuy1hgqAs=",
+      "dev": true,
+      "requires": {
+        "graceful-fs": "^4.1.6",
+        "jest-matcher-utils": "^18.1.0",
+        "jest-matchers": "^18.1.0",
+        "jest-snapshot": "^18.1.0",
+        "jest-util": "^18.1.0"
+      }
+    },
+    "jest-matcher-utils": {
+      "version": "18.1.0",
+      "resolved": "http://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-18.1.0.tgz",
+      "integrity": "sha1-GsRlGVXuKmDO8ef8yYzf13PA+TI=",
+      "dev": true,
+      "requires": {
+        "chalk": "^1.1.3",
+        "pretty-format": "^18.1.0"
+      },
+      "dependencies": {
+        "chalk": {
+          "version": "1.1.3",
+          "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+          "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^2.2.1",
+            "escape-string-regexp": "^1.0.2",
+            "has-ansi": "^2.0.0",
+            "strip-ansi": "^3.0.0",
+            "supports-color": "^2.0.0"
+          }
+        }
+      }
+    },
+    "jest-matchers": {
+      "version": "18.1.0",
+      "resolved": "https://registry.npmjs.org/jest-matchers/-/jest-matchers-18.1.0.tgz",
+      "integrity": "sha1-A0FIS/h6H9C6wKTSyJnit3o/Hq0=",
+      "dev": true,
+      "requires": {
+        "jest-diff": "^18.1.0",
+        "jest-matcher-utils": "^18.1.0",
+        "jest-util": "^18.1.0",
+        "pretty-format": "^18.1.0"
+      }
+    },
+    "jest-mock": {
+      "version": "18.0.0",
+      "resolved": "http://registry.npmjs.org/jest-mock/-/jest-mock-18.0.0.tgz",
+      "integrity": "sha1-XCSIRuoz+lWLUm9TEqtKZ2XkibM=",
+      "dev": true
+    },
+    "jest-resolve": {
+      "version": "18.1.0",
+      "resolved": "http://registry.npmjs.org/jest-resolve/-/jest-resolve-18.1.0.tgz",
+      "integrity": "sha1-aACsy1NmWMkGzV4p3kErGrmsJJs=",
+      "dev": true,
+      "requires": {
+        "browser-resolve": "^1.11.2",
+        "jest-file-exists": "^17.0.0",
+        "jest-haste-map": "^18.1.0",
+        "resolve": "^1.2.0"
+      }
+    },
+    "jest-resolve-dependencies": {
+      "version": "18.1.0",
+      "resolved": "http://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-18.1.0.tgz",
+      "integrity": "sha1-gTT7XK9Zye2EL+AVKrAcUnEfG7s=",
+      "dev": true,
+      "requires": {
+        "jest-file-exists": "^17.0.0",
+        "jest-resolve": "^18.1.0"
+      }
+    },
+    "jest-runtime": {
+      "version": "18.1.0",
+      "resolved": "http://registry.npmjs.org/jest-runtime/-/jest-runtime-18.1.0.tgz",
+      "integrity": "sha1-Or/WhxdbIfw7haK4BkOZ6ZeFmSI=",
+      "dev": true,
+      "requires": {
+        "babel-core": "^6.0.0",
+        "babel-jest": "^18.0.0",
+        "babel-plugin-istanbul": "^3.0.0",
+        "chalk": "^1.1.3",
+        "graceful-fs": "^4.1.6",
+        "jest-config": "^18.1.0",
+        "jest-file-exists": "^17.0.0",
+        "jest-haste-map": "^18.1.0",
+        "jest-mock": "^18.0.0",
+        "jest-resolve": "^18.1.0",
+        "jest-snapshot": "^18.1.0",
+        "jest-util": "^18.1.0",
+        "json-stable-stringify": "^1.0.0",
+        "micromatch": "^2.3.11",
+        "yargs": "^6.3.0"
+      },
+      "dependencies": {
+        "chalk": {
+          "version": "1.1.3",
+          "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+          "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^2.2.1",
+            "escape-string-regexp": "^1.0.2",
+            "has-ansi": "^2.0.0",
+            "strip-ansi": "^3.0.0",
+            "supports-color": "^2.0.0"
+          }
+        }
+      }
+    },
+    "jest-snapshot": {
+      "version": "18.1.0",
+      "resolved": "http://registry.npmjs.org/jest-snapshot/-/jest-snapshot-18.1.0.tgz",
+      "integrity": "sha1-VbltLuY5ybznb4fyo/1Atxx6WRY=",
+      "dev": true,
+      "requires": {
+        "jest-diff": "^18.1.0",
+        "jest-file-exists": "^17.0.0",
+        "jest-matcher-utils": "^18.1.0",
+        "jest-util": "^18.1.0",
+        "natural-compare": "^1.4.0",
+        "pretty-format": "^18.1.0"
+      }
+    },
+    "jest-util": {
+      "version": "18.1.0",
+      "resolved": "http://registry.npmjs.org/jest-util/-/jest-util-18.1.0.tgz",
+      "integrity": "sha1-OpnDIRSrF/hL4JQ4JScAbm1L/Go=",
+      "dev": true,
+      "requires": {
+        "chalk": "^1.1.1",
+        "diff": "^3.0.0",
+        "graceful-fs": "^4.1.6",
+        "jest-file-exists": "^17.0.0",
+        "jest-mock": "^18.0.0",
+        "mkdirp": "^0.5.1"
+      },
+      "dependencies": {
+        "chalk": {
+          "version": "1.1.3",
+          "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+          "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^2.2.1",
+            "escape-string-regexp": "^1.0.2",
+            "has-ansi": "^2.0.0",
+            "strip-ansi": "^3.0.0",
+            "supports-color": "^2.0.0"
+          }
+        }
+      }
+    },
+    "js-tokens": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
+      "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=",
+      "dev": true
+    },
+    "js-yaml": {
+      "version": "3.13.1",
+      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
+      "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
+      "dev": true,
+      "requires": {
+        "argparse": "^1.0.7",
+        "esprima": "^4.0.0"
+      }
+    },
+    "jsbn": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
+      "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=",
+      "dev": true
+    },
+    "jsdom": {
+      "version": "9.12.0",
+      "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-9.12.0.tgz",
+      "integrity": "sha1-6MVG//ywbADUgzyoRBD+1/igl9Q=",
+      "dev": true,
+      "requires": {
+        "abab": "^1.0.3",
+        "acorn": "^4.0.4",
+        "acorn-globals": "^3.1.0",
+        "array-equal": "^1.0.0",
+        "content-type-parser": "^1.0.1",
+        "cssom": ">= 0.3.2 < 0.4.0",
+        "cssstyle": ">= 0.2.37 < 0.3.0",
+        "escodegen": "^1.6.1",
+        "html-encoding-sniffer": "^1.0.1",
+        "nwmatcher": ">= 1.3.9 < 2.0.0",
+        "parse5": "^1.5.1",
+        "request": "^2.79.0",
+        "sax": "^1.2.1",
+        "symbol-tree": "^3.2.1",
+        "tough-cookie": "^2.3.2",
+        "webidl-conversions": "^4.0.0",
+        "whatwg-encoding": "^1.0.1",
+        "whatwg-url": "^4.3.0",
+        "xml-name-validator": "^2.0.1"
+      },
+      "dependencies": {
+        "acorn": {
+          "version": "4.0.13",
+          "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz",
+          "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=",
+          "dev": true
+        }
+      }
+    },
+    "jsesc": {
+      "version": "1.3.0",
+      "resolved": "http://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz",
+      "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=",
+      "dev": true
+    },
+    "json-schema": {
+      "version": "0.2.3",
+      "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
+      "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=",
+      "dev": true
+    },
+    "json-schema-traverse": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+      "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+      "dev": true
+    },
+    "json-stable-stringify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz",
+      "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=",
+      "dev": true,
+      "requires": {
+        "jsonify": "~0.0.0"
+      }
+    },
+    "json-stringify-safe": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
+      "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=",
+      "dev": true
+    },
+    "json5": {
+      "version": "0.5.1",
+      "resolved": "http://registry.npmjs.org/json5/-/json5-0.5.1.tgz",
+      "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=",
+      "dev": true
+    },
+    "jsonify": {
+      "version": "0.0.0",
+      "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz",
+      "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=",
+      "dev": true
+    },
+    "jsonpointer": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz",
+      "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=",
+      "dev": true
+    },
+    "jsprim": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
+      "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=",
+      "dev": true,
+      "requires": {
+        "assert-plus": "1.0.0",
+        "extsprintf": "1.3.0",
+        "json-schema": "0.2.3",
+        "verror": "1.10.0"
+      }
+    },
+    "kind-of": {
+      "version": "3.2.2",
+      "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+      "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+      "dev": true,
+      "requires": {
+        "is-buffer": "^1.1.5"
+      }
+    },
+    "kuler": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/kuler/-/kuler-1.0.1.tgz",
+      "integrity": "sha512-J9nVUucG1p/skKul6DU3PUZrhs0LPulNaeUOox0IyXDi8S4CztTHs1gQphhuZmzXG7VOQSf6NJfKuzteQLv9gQ==",
+      "requires": {
+        "colornames": "^1.1.1"
+      }
+    },
+    "lcid": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz",
+      "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=",
+      "dev": true,
+      "requires": {
+        "invert-kv": "^1.0.0"
+      }
+    },
+    "levn": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
+      "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=",
+      "dev": true,
+      "requires": {
+        "prelude-ls": "~1.1.2",
+        "type-check": "~0.3.2"
+      }
+    },
+    "load-json-file": {
+      "version": "1.1.0",
+      "resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
+      "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=",
+      "dev": true,
+      "requires": {
+        "graceful-fs": "^4.1.2",
+        "parse-json": "^2.2.0",
+        "pify": "^2.0.0",
+        "pinkie-promise": "^2.0.0",
+        "strip-bom": "^2.0.0"
+      }
+    },
+    "locate-path": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz",
+      "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=",
+      "dev": true,
+      "requires": {
+        "p-locate": "^2.0.0",
+        "path-exists": "^3.0.0"
+      },
+      "dependencies": {
+        "path-exists": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
+          "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
+          "dev": true
+        }
+      }
+    },
+    "lodash": {
+      "version": "4.17.15",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
+      "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
+    },
+    "lodash._arraycopy": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/lodash._arraycopy/-/lodash._arraycopy-3.0.0.tgz",
+      "integrity": "sha1-due3wfH7klRzdIeKVi7Qaj5Q9uE=",
+      "dev": true
+    },
+    "lodash._arrayeach": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/lodash._arrayeach/-/lodash._arrayeach-3.0.0.tgz",
+      "integrity": "sha1-urFWsqkNPxu9XGU0AzSeXlkz754=",
+      "dev": true
+    },
+    "lodash._baseassign": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz",
+      "integrity": "sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=",
+      "dev": true,
+      "requires": {
+        "lodash._basecopy": "^3.0.0",
+        "lodash.keys": "^3.0.0"
+      }
+    },
+    "lodash._baseclone": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/lodash._baseclone/-/lodash._baseclone-3.3.0.tgz",
+      "integrity": "sha1-MDUZv2OT/n5C802LYw73eU41Qrc=",
+      "dev": true,
+      "requires": {
+        "lodash._arraycopy": "^3.0.0",
+        "lodash._arrayeach": "^3.0.0",
+        "lodash._baseassign": "^3.0.0",
+        "lodash._basefor": "^3.0.0",
+        "lodash.isarray": "^3.0.0",
+        "lodash.keys": "^3.0.0"
+      }
+    },
+    "lodash._basecopy": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz",
+      "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=",
+      "dev": true
+    },
+    "lodash._basefor": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/lodash._basefor/-/lodash._basefor-3.0.3.tgz",
+      "integrity": "sha1-dVC06SGO8J+tJDQ7YSAhx5tMIMI=",
+      "dev": true
+    },
+    "lodash._bindcallback": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz",
+      "integrity": "sha1-5THCdkTPi1epnhftlbNcdIeJOS4=",
+      "dev": true
+    },
+    "lodash._getnative": {
+      "version": "3.9.1",
+      "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz",
+      "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=",
+      "dev": true
+    },
+    "lodash.assign": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz",
+      "integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=",
+      "dev": true
+    },
+    "lodash.clonedeep": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-3.0.2.tgz",
+      "integrity": "sha1-oKHkDYKl6on/WxR7hETtY9koJ9s=",
+      "dev": true,
+      "requires": {
+        "lodash._baseclone": "^3.0.0",
+        "lodash._bindcallback": "^3.0.0"
+      }
+    },
+    "lodash.isarguments": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
+      "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=",
+      "dev": true
+    },
+    "lodash.isarray": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz",
+      "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=",
+      "dev": true
+    },
+    "lodash.keys": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz",
+      "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=",
+      "dev": true,
+      "requires": {
+        "lodash._getnative": "^3.0.0",
+        "lodash.isarguments": "^3.0.0",
+        "lodash.isarray": "^3.0.0"
+      }
+    },
+    "lodash.toarray": {
+      "version": "4.4.0",
+      "resolved": "https://registry.npmjs.org/lodash.toarray/-/lodash.toarray-4.4.0.tgz",
+      "integrity": "sha1-JMS/zWsvuji/0FlNsRedjptlZWE=",
+      "dev": true
+    },
+    "logform": {
+      "version": "1.10.0",
+      "resolved": "https://registry.npmjs.org/logform/-/logform-1.10.0.tgz",
+      "integrity": "sha512-em5ojIhU18fIMOw/333mD+ZLE2fis0EzXl1ZwHx4iQzmpQi6odNiY/t+ITNr33JZhT9/KEaH+UPIipr6a9EjWg==",
+      "requires": {
+        "colors": "^1.2.1",
+        "fast-safe-stringify": "^2.0.4",
+        "fecha": "^2.3.3",
+        "ms": "^2.1.1",
+        "triple-beam": "^1.2.0"
+      },
+      "dependencies": {
+        "colors": {
+          "version": "1.3.2",
+          "resolved": "https://registry.npmjs.org/colors/-/colors-1.3.2.tgz",
+          "integrity": "sha512-rhP0JSBGYvpcNQj4s5AdShMeE5ahMop96cTeDl/v9qQQm2fYClE2QXZRi8wLzc+GmXSxdIqqbOIAhyObEXDbfQ=="
+        },
+        "ms": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
+          "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
+        }
+      }
+    },
+    "loose-envify": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+      "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+      "dev": true,
+      "requires": {
+        "js-tokens": "^3.0.0 || ^4.0.0"
+      }
+    },
+    "makeerror": {
+      "version": "1.0.11",
+      "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz",
+      "integrity": "sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw=",
+      "dev": true,
+      "requires": {
+        "tmpl": "1.0.x"
+      }
+    },
+    "map-cache": {
+      "version": "0.2.2",
+      "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz",
+      "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=",
+      "dev": true,
+      "optional": true
+    },
+    "map-visit": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz",
+      "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "object-visit": "^1.0.0"
+      }
+    },
+    "marked": {
+      "version": "0.5.2",
+      "resolved": "https://registry.npmjs.org/marked/-/marked-0.5.2.tgz",
+      "integrity": "sha512-fdZvBa7/vSQIZCi4uuwo2N3q+7jJURpMVCcbaX0S1Mg65WZ5ilXvC67MviJAsdjqqgD+CEq4RKo5AYGgINkVAA==",
+      "dev": true
+    },
+    "marked-terminal": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/marked-terminal/-/marked-terminal-3.1.1.tgz",
+      "integrity": "sha512-7UBFww1rdx0w9HehLMCVYa8/AxXaiDigDfMsJcj82/wgLQG9cj+oiMAVlJpeWD57VFJY2OYY+bKeEVIjIlxi+w==",
+      "dev": true,
+      "requires": {
+        "cardinal": "^2.1.1",
+        "chalk": "^2.4.1",
+        "cli-table": "^0.3.1",
+        "lodash.assign": "^4.2.0",
+        "node-emoji": "^1.4.1"
+      },
+      "dependencies": {
+        "ansi-styles": {
+          "version": "3.2.1",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+          "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+          "dev": true,
+          "requires": {
+            "color-convert": "^1.9.0"
+          }
+        },
+        "chalk": {
+          "version": "2.4.1",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz",
+          "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^3.2.1",
+            "escape-string-regexp": "^1.0.5",
+            "supports-color": "^5.3.0"
+          }
+        },
+        "has-flag": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+          "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
+          "dev": true
+        },
+        "supports-color": {
+          "version": "5.5.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+          "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+          "dev": true,
+          "requires": {
+            "has-flag": "^3.0.0"
+          }
+        }
+      }
+    },
+    "math-random": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.1.tgz",
+      "integrity": "sha1-izqsWIuKZuSXXjzepn97sylgH6w=",
+      "dev": true
+    },
+    "merge": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/merge/-/merge-1.2.1.tgz",
+      "integrity": "sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ==",
+      "dev": true
+    },
+    "micromatch": {
+      "version": "2.3.11",
+      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz",
+      "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=",
+      "dev": true,
+      "requires": {
+        "arr-diff": "^2.0.0",
+        "array-unique": "^0.2.1",
+        "braces": "^1.8.2",
+        "expand-brackets": "^0.1.4",
+        "extglob": "^0.3.1",
+        "filename-regex": "^2.0.0",
+        "is-extglob": "^1.0.0",
+        "is-glob": "^2.0.1",
+        "kind-of": "^3.0.2",
+        "normalize-path": "^2.0.1",
+        "object.omit": "^2.0.0",
+        "parse-glob": "^3.0.4",
+        "regex-cache": "^0.4.2"
+      }
+    },
+    "mime-db": {
+      "version": "1.37.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz",
+      "integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==",
+      "dev": true
+    },
+    "mime-types": {
+      "version": "2.1.21",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz",
+      "integrity": "sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==",
+      "dev": true,
+      "requires": {
+        "mime-db": "~1.37.0"
+      }
+    },
+    "minimatch": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+      "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+      "dev": true,
+      "requires": {
+        "brace-expansion": "^1.1.7"
+      }
+    },
+    "minimist": {
+      "version": "0.0.8",
+      "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
+      "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
+      "dev": true
+    },
+    "mixin-deep": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz",
+      "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "for-in": "^1.0.2",
+        "is-extendable": "^1.0.1"
+      },
+      "dependencies": {
+        "is-extendable": {
+          "version": "1.0.1",
+          "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz",
+          "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "is-plain-object": "^2.0.4"
+          }
+        }
+      }
+    },
+    "mkdirp": {
+      "version": "0.5.1",
+      "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
+      "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
+      "dev": true,
+      "requires": {
+        "minimist": "0.0.8"
+      }
+    },
+    "ms": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+      "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+      "dev": true
+    },
+    "mute-stream": {
+      "version": "0.0.5",
+      "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.5.tgz",
+      "integrity": "sha1-j7+rsKmKJT0xhDMfno3rc3L6xsA=",
+      "dev": true
+    },
+    "nan": {
+      "version": "2.14.0",
+      "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz",
+      "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==",
+      "dev": true,
+      "optional": true
+    },
+    "nanomatch": {
+      "version": "1.2.13",
+      "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz",
+      "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "arr-diff": "^4.0.0",
+        "array-unique": "^0.3.2",
+        "define-property": "^2.0.2",
+        "extend-shallow": "^3.0.2",
+        "fragment-cache": "^0.2.1",
+        "is-windows": "^1.0.2",
+        "kind-of": "^6.0.2",
+        "object.pick": "^1.3.0",
+        "regex-not": "^1.0.0",
+        "snapdragon": "^0.8.1",
+        "to-regex": "^3.0.1"
+      },
+      "dependencies": {
+        "arr-diff": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
+          "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=",
+          "dev": true,
+          "optional": true
+        },
+        "array-unique": {
+          "version": "0.3.2",
+          "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz",
+          "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=",
+          "dev": true,
+          "optional": true
+        },
+        "kind-of": {
+          "version": "6.0.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
+          "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
+          "dev": true,
+          "optional": true
+        }
+      }
+    },
+    "natural-compare": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+      "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=",
+      "dev": true
+    },
+    "neo-async": {
+      "version": "2.6.1",
+      "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz",
+      "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==",
+      "dev": true
+    },
+    "next-tick": {
+      "version": "1.0.0",
+      "resolved": "http://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz",
+      "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=",
+      "dev": true
+    },
+    "node-emoji": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.8.1.tgz",
+      "integrity": "sha512-+ktMAh1Jwas+TnGodfCfjUbJKoANqPaJFN0z0iqh41eqD8dvguNzcitVSBSVK1pidz0AqGbLKcoVuVLRVZ/aVg==",
+      "dev": true,
+      "requires": {
+        "lodash.toarray": "^4.4.0"
+      }
+    },
+    "node-int64": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
+      "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=",
+      "dev": true
+    },
+    "node-notifier": {
+      "version": "4.6.1",
+      "resolved": "http://registry.npmjs.org/node-notifier/-/node-notifier-4.6.1.tgz",
+      "integrity": "sha1-BW0UJE89zBzq3+aK+c/wxUc6M/M=",
+      "dev": true,
+      "requires": {
+        "cli-usage": "^0.1.1",
+        "growly": "^1.2.0",
+        "lodash.clonedeep": "^3.0.0",
+        "minimist": "^1.1.1",
+        "semver": "^5.1.0",
+        "shellwords": "^0.1.0",
+        "which": "^1.0.5"
+      },
+      "dependencies": {
+        "minimist": {
+          "version": "1.2.0",
+          "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+          "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
+          "dev": true
+        }
+      }
+    },
+    "normalize-package-data": {
+      "version": "2.4.0",
+      "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz",
+      "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==",
+      "dev": true,
+      "requires": {
+        "hosted-git-info": "^2.1.4",
+        "is-builtin-module": "^1.0.0",
+        "semver": "2 || 3 || 4 || 5",
+        "validate-npm-package-license": "^3.0.1"
+      }
+    },
+    "normalize-path": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",
+      "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=",
+      "dev": true,
+      "requires": {
+        "remove-trailing-separator": "^1.0.1"
+      }
+    },
+    "number-is-nan": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
+      "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
+      "dev": true
+    },
+    "nwmatcher": {
+      "version": "1.4.4",
+      "resolved": "https://registry.npmjs.org/nwmatcher/-/nwmatcher-1.4.4.tgz",
+      "integrity": "sha512-3iuY4N5dhgMpCUrOVnuAdGrgxVqV2cJpM+XNccjR2DKOB1RUP0aA+wGXEiNziG/UKboFyGBIoKOaNlJxx8bciQ==",
+      "dev": true
+    },
+    "oauth-sign": {
+      "version": "0.9.0",
+      "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
+      "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==",
+      "dev": true
+    },
+    "object-assign": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+      "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
+      "dev": true
+    },
+    "object-copy": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz",
+      "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "copy-descriptor": "^0.1.0",
+        "define-property": "^0.2.5",
+        "kind-of": "^3.0.3"
+      },
+      "dependencies": {
+        "define-property": {
+          "version": "0.2.5",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+          "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "is-descriptor": "^0.1.0"
+          }
+        }
+      }
+    },
+    "object-visit": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz",
+      "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "isobject": "^3.0.0"
+      },
+      "dependencies": {
+        "isobject": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
+          "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
+          "dev": true,
+          "optional": true
+        }
+      }
+    },
+    "object.omit": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz",
+      "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=",
+      "dev": true,
+      "requires": {
+        "for-own": "^0.1.4",
+        "is-extendable": "^0.1.1"
+      }
+    },
+    "object.pick": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz",
+      "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "isobject": "^3.0.1"
+      },
+      "dependencies": {
+        "isobject": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
+          "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
+          "dev": true,
+          "optional": true
+        }
+      }
+    },
+    "once": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+      "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+      "dev": true,
+      "requires": {
+        "wrappy": "1"
+      }
+    },
+    "one-time": {
+      "version": "0.0.4",
+      "resolved": "https://registry.npmjs.org/one-time/-/one-time-0.0.4.tgz",
+      "integrity": "sha1-+M33eISCb+Tf+T46nMN7HkSAdC4="
+    },
+    "onetime": {
+      "version": "1.1.0",
+      "resolved": "http://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz",
+      "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=",
+      "dev": true
+    },
+    "optimist": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz",
+      "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=",
+      "dev": true,
+      "requires": {
+        "minimist": "~0.0.1",
+        "wordwrap": "~0.0.2"
+      },
+      "dependencies": {
+        "wordwrap": {
+          "version": "0.0.3",
+          "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz",
+          "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=",
+          "dev": true
+        }
+      }
+    },
+    "optionator": {
+      "version": "0.8.2",
+      "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz",
+      "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=",
+      "dev": true,
+      "requires": {
+        "deep-is": "~0.1.3",
+        "fast-levenshtein": "~2.0.4",
+        "levn": "~0.3.0",
+        "prelude-ls": "~1.1.2",
+        "type-check": "~0.3.2",
+        "wordwrap": "~1.0.0"
+      }
+    },
+    "os-homedir": {
+      "version": "1.0.2",
+      "resolved": "http://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
+      "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=",
+      "dev": true
+    },
+    "os-locale": {
+      "version": "1.4.0",
+      "resolved": "http://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz",
+      "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=",
+      "dev": true,
+      "requires": {
+        "lcid": "^1.0.0"
+      }
+    },
+    "os-tmpdir": {
+      "version": "1.0.2",
+      "resolved": "http://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
+      "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",
+      "dev": true
+    },
+    "output-file-sync": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/output-file-sync/-/output-file-sync-1.1.2.tgz",
+      "integrity": "sha1-0KM+7+YaIF+suQCS6CZZjVJFznY=",
+      "dev": true,
+      "requires": {
+        "graceful-fs": "^4.1.4",
+        "mkdirp": "^0.5.1",
+        "object-assign": "^4.1.0"
+      }
+    },
+    "p-limit": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz",
+      "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==",
+      "dev": true,
+      "requires": {
+        "p-try": "^1.0.0"
+      }
+    },
+    "p-locate": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz",
+      "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=",
+      "dev": true,
+      "requires": {
+        "p-limit": "^1.1.0"
+      }
+    },
+    "p-try": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz",
+      "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=",
+      "dev": true
+    },
+    "parse-glob": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz",
+      "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=",
+      "dev": true,
+      "requires": {
+        "glob-base": "^0.3.0",
+        "is-dotfile": "^1.0.0",
+        "is-extglob": "^1.0.0",
+        "is-glob": "^2.0.0"
+      }
+    },
+    "parse-json": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz",
+      "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=",
+      "dev": true,
+      "requires": {
+        "error-ex": "^1.2.0"
+      }
+    },
+    "parse5": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/parse5/-/parse5-1.5.1.tgz",
+      "integrity": "sha1-m387DeMr543CQBsXVzzK8Pb1nZQ=",
+      "dev": true
+    },
+    "pascalcase": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz",
+      "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=",
+      "dev": true,
+      "optional": true
+    },
+    "path-exists": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz",
+      "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=",
+      "dev": true,
+      "requires": {
+        "pinkie-promise": "^2.0.0"
+      }
+    },
+    "path-is-absolute": {
+      "version": "1.0.1",
+      "resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+      "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
+      "dev": true
+    },
+    "path-is-inside": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz",
+      "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=",
+      "dev": true
+    },
+    "path-parse": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
+      "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
+      "dev": true
+    },
+    "path-type": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz",
+      "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=",
+      "dev": true,
+      "requires": {
+        "graceful-fs": "^4.1.2",
+        "pify": "^2.0.0",
+        "pinkie-promise": "^2.0.0"
+      }
+    },
+    "performance-now": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
+      "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=",
+      "dev": true
+    },
+    "pify": {
+      "version": "2.3.0",
+      "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+      "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
+      "dev": true
+    },
+    "pinkie": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz",
+      "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=",
+      "dev": true
+    },
+    "pinkie-promise": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz",
+      "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=",
+      "dev": true,
+      "requires": {
+        "pinkie": "^2.0.0"
+      }
+    },
+    "pkg-dir": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-1.0.0.tgz",
+      "integrity": "sha1-ektQio1bstYp1EcFb/TpyTFM89Q=",
+      "dev": true,
+      "requires": {
+        "find-up": "^1.0.0"
+      }
+    },
+    "pluralize": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-1.2.1.tgz",
+      "integrity": "sha1-0aIUg/0iu0HlihL6NCGCMUCJfEU=",
+      "dev": true
+    },
+    "posix-character-classes": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz",
+      "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=",
+      "dev": true,
+      "optional": true
+    },
+    "prelude-ls": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
+      "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=",
+      "dev": true
+    },
+    "preserve": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz",
+      "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=",
+      "dev": true
+    },
+    "pretty-format": {
+      "version": "18.1.0",
+      "resolved": "http://registry.npmjs.org/pretty-format/-/pretty-format-18.1.0.tgz",
+      "integrity": "sha1-+2Wob3p/kZSWPu6RhlwbzxA54oQ=",
+      "dev": true,
+      "requires": {
+        "ansi-styles": "^2.2.1"
+      }
+    },
+    "private": {
+      "version": "0.1.8",
+      "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz",
+      "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==",
+      "dev": true
+    },
+    "process-nextick-args": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
+      "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw=="
+    },
+    "progress": {
+      "version": "1.1.8",
+      "resolved": "http://registry.npmjs.org/progress/-/progress-1.1.8.tgz",
+      "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=",
+      "dev": true
+    },
+    "prr": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
+      "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=",
+      "dev": true
+    },
+    "psl": {
+      "version": "1.1.29",
+      "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.29.tgz",
+      "integrity": "sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==",
+      "dev": true
+    },
+    "punycode": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
+      "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
+      "dev": true
+    },
+    "qs": {
+      "version": "6.5.2",
+      "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
+      "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
+      "dev": true
+    },
+    "randomatic": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.1.1.tgz",
+      "integrity": "sha512-TuDE5KxZ0J461RVjrJZCJc+J+zCkTb1MbH9AQUq68sMhOMcy9jLcb3BrZKgp9q9Ncltdg4QVqWrH02W2EFFVYw==",
+      "dev": true,
+      "requires": {
+        "is-number": "^4.0.0",
+        "kind-of": "^6.0.0",
+        "math-random": "^1.0.1"
+      },
+      "dependencies": {
+        "is-number": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz",
+          "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==",
+          "dev": true
+        },
+        "kind-of": {
+          "version": "6.0.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
+          "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
+          "dev": true
+        }
+      }
+    },
+    "read-pkg": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz",
+      "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=",
+      "dev": true,
+      "requires": {
+        "load-json-file": "^1.0.0",
+        "normalize-package-data": "^2.3.2",
+        "path-type": "^1.0.0"
+      }
+    },
+    "read-pkg-up": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz",
+      "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=",
+      "dev": true,
+      "requires": {
+        "find-up": "^1.0.0",
+        "read-pkg": "^1.0.0"
+      }
+    },
+    "readable-stream": {
+      "version": "2.3.6",
+      "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
+      "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
+      "requires": {
+        "core-util-is": "~1.0.0",
+        "inherits": "~2.0.3",
+        "isarray": "~1.0.0",
+        "process-nextick-args": "~2.0.0",
+        "safe-buffer": "~5.1.1",
+        "string_decoder": "~1.1.1",
+        "util-deprecate": "~1.0.1"
+      }
+    },
+    "readdirp": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz",
+      "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "graceful-fs": "^4.1.11",
+        "micromatch": "^3.1.10",
+        "readable-stream": "^2.0.2"
+      },
+      "dependencies": {
+        "arr-diff": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
+          "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=",
+          "dev": true,
+          "optional": true
+        },
+        "array-unique": {
+          "version": "0.3.2",
+          "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz",
+          "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=",
+          "dev": true,
+          "optional": true
+        },
+        "braces": {
+          "version": "2.3.2",
+          "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz",
+          "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "arr-flatten": "^1.1.0",
+            "array-unique": "^0.3.2",
+            "extend-shallow": "^2.0.1",
+            "fill-range": "^4.0.0",
+            "isobject": "^3.0.1",
+            "repeat-element": "^1.1.2",
+            "snapdragon": "^0.8.1",
+            "snapdragon-node": "^2.0.1",
+            "split-string": "^3.0.2",
+            "to-regex": "^3.0.1"
+          },
+          "dependencies": {
+            "extend-shallow": {
+              "version": "2.0.1",
+              "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+              "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+              "dev": true,
+              "optional": true,
+              "requires": {
+                "is-extendable": "^0.1.0"
+              }
+            }
+          }
+        },
+        "expand-brackets": {
+          "version": "2.1.4",
+          "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz",
+          "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "debug": "^2.3.3",
+            "define-property": "^0.2.5",
+            "extend-shallow": "^2.0.1",
+            "posix-character-classes": "^0.1.0",
+            "regex-not": "^1.0.0",
+            "snapdragon": "^0.8.1",
+            "to-regex": "^3.0.1"
+          },
+          "dependencies": {
+            "define-property": {
+              "version": "0.2.5",
+              "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+              "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+              "dev": true,
+              "optional": true,
+              "requires": {
+                "is-descriptor": "^0.1.0"
+              }
+            },
+            "extend-shallow": {
+              "version": "2.0.1",
+              "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+              "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+              "dev": true,
+              "optional": true,
+              "requires": {
+                "is-extendable": "^0.1.0"
+              }
+            },
+            "is-accessor-descriptor": {
+              "version": "0.1.6",
+              "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz",
+              "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=",
+              "dev": true,
+              "optional": true,
+              "requires": {
+                "kind-of": "^3.0.2"
+              },
+              "dependencies": {
+                "kind-of": {
+                  "version": "3.2.2",
+                  "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+                  "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+                  "dev": true,
+                  "optional": true,
+                  "requires": {
+                    "is-buffer": "^1.1.5"
+                  }
+                }
+              }
+            },
+            "is-data-descriptor": {
+              "version": "0.1.4",
+              "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz",
+              "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=",
+              "dev": true,
+              "optional": true,
+              "requires": {
+                "kind-of": "^3.0.2"
+              },
+              "dependencies": {
+                "kind-of": {
+                  "version": "3.2.2",
+                  "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+                  "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+                  "dev": true,
+                  "optional": true,
+                  "requires": {
+                    "is-buffer": "^1.1.5"
+                  }
+                }
+              }
+            },
+            "is-descriptor": {
+              "version": "0.1.6",
+              "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz",
+              "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==",
+              "dev": true,
+              "optional": true,
+              "requires": {
+                "is-accessor-descriptor": "^0.1.6",
+                "is-data-descriptor": "^0.1.4",
+                "kind-of": "^5.0.0"
+              }
+            },
+            "kind-of": {
+              "version": "5.1.0",
+              "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz",
+              "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==",
+              "dev": true,
+              "optional": true
+            }
+          }
+        },
+        "extglob": {
+          "version": "2.0.4",
+          "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz",
+          "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "array-unique": "^0.3.2",
+            "define-property": "^1.0.0",
+            "expand-brackets": "^2.1.4",
+            "extend-shallow": "^2.0.1",
+            "fragment-cache": "^0.2.1",
+            "regex-not": "^1.0.0",
+            "snapdragon": "^0.8.1",
+            "to-regex": "^3.0.1"
+          },
+          "dependencies": {
+            "define-property": {
+              "version": "1.0.0",
+              "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
+              "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
+              "dev": true,
+              "optional": true,
+              "requires": {
+                "is-descriptor": "^1.0.0"
+              }
+            },
+            "extend-shallow": {
+              "version": "2.0.1",
+              "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+              "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+              "dev": true,
+              "optional": true,
+              "requires": {
+                "is-extendable": "^0.1.0"
+              }
+            }
+          }
+        },
+        "fill-range": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
+          "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "extend-shallow": "^2.0.1",
+            "is-number": "^3.0.0",
+            "repeat-string": "^1.6.1",
+            "to-regex-range": "^2.1.0"
+          },
+          "dependencies": {
+            "extend-shallow": {
+              "version": "2.0.1",
+              "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+              "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+              "dev": true,
+              "optional": true,
+              "requires": {
+                "is-extendable": "^0.1.0"
+              }
+            }
+          }
+        },
+        "is-accessor-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+          "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-data-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+          "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-descriptor": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+          "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "is-accessor-descriptor": "^1.0.0",
+            "is-data-descriptor": "^1.0.0",
+            "kind-of": "^6.0.2"
+          }
+        },
+        "is-number": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
+          "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "kind-of": "^3.0.2"
+          },
+          "dependencies": {
+            "kind-of": {
+              "version": "3.2.2",
+              "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+              "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+              "dev": true,
+              "optional": true,
+              "requires": {
+                "is-buffer": "^1.1.5"
+              }
+            }
+          }
+        },
+        "isobject": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
+          "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
+          "dev": true,
+          "optional": true
+        },
+        "kind-of": {
+          "version": "6.0.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
+          "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
+          "dev": true,
+          "optional": true
+        },
+        "micromatch": {
+          "version": "3.1.10",
+          "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz",
+          "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "arr-diff": "^4.0.0",
+            "array-unique": "^0.3.2",
+            "braces": "^2.3.1",
+            "define-property": "^2.0.2",
+            "extend-shallow": "^3.0.2",
+            "extglob": "^2.0.4",
+            "fragment-cache": "^0.2.1",
+            "kind-of": "^6.0.2",
+            "nanomatch": "^1.2.9",
+            "object.pick": "^1.3.0",
+            "regex-not": "^1.0.0",
+            "snapdragon": "^0.8.1",
+            "to-regex": "^3.0.2"
+          }
+        }
+      }
+    },
+    "readline2": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/readline2/-/readline2-1.0.1.tgz",
+      "integrity": "sha1-QQWWCP/BVHV7cV2ZidGZ/783LjU=",
+      "dev": true,
+      "requires": {
+        "code-point-at": "^1.0.0",
+        "is-fullwidth-code-point": "^1.0.0",
+        "mute-stream": "0.0.5"
+      }
+    },
+    "rechoir": {
+      "version": "0.6.2",
+      "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz",
+      "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=",
+      "dev": true,
+      "requires": {
+        "resolve": "^1.1.6"
+      }
+    },
+    "redeyed": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/redeyed/-/redeyed-2.1.1.tgz",
+      "integrity": "sha1-iYS1gV2ZyyIEacme7v/jiRPmzAs=",
+      "dev": true,
+      "requires": {
+        "esprima": "~4.0.0"
+      }
+    },
+    "regenerate": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz",
+      "integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==",
+      "dev": true
+    },
+    "regenerator-runtime": {
+      "version": "0.11.1",
+      "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz",
+      "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==",
+      "dev": true
+    },
+    "regenerator-transform": {
+      "version": "0.10.1",
+      "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.10.1.tgz",
+      "integrity": "sha512-PJepbvDbuK1xgIgnau7Y90cwaAmO/LCLMI2mPvaXq2heGMR3aWW5/BQvYrhJ8jgmQjXewXvBjzfqKcVOmhjZ6Q==",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "^6.18.0",
+        "babel-types": "^6.19.0",
+        "private": "^0.1.6"
+      }
+    },
+    "regex-cache": {
+      "version": "0.4.4",
+      "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz",
+      "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==",
+      "dev": true,
+      "requires": {
+        "is-equal-shallow": "^0.1.3"
+      }
+    },
+    "regex-not": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz",
+      "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "extend-shallow": "^3.0.2",
+        "safe-regex": "^1.1.0"
+      }
+    },
+    "regexpu-core": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-2.0.0.tgz",
+      "integrity": "sha1-SdA4g3uNz4v6W5pCE5k45uoq4kA=",
+      "dev": true,
+      "requires": {
+        "regenerate": "^1.2.1",
+        "regjsgen": "^0.2.0",
+        "regjsparser": "^0.1.4"
+      }
+    },
+    "regjsgen": {
+      "version": "0.2.0",
+      "resolved": "http://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz",
+      "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=",
+      "dev": true
+    },
+    "regjsparser": {
+      "version": "0.1.5",
+      "resolved": "http://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz",
+      "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=",
+      "dev": true,
+      "requires": {
+        "jsesc": "~0.5.0"
+      },
+      "dependencies": {
+        "jsesc": {
+          "version": "0.5.0",
+          "resolved": "http://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz",
+          "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=",
+          "dev": true
+        }
+      }
+    },
+    "remove-trailing-separator": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
+      "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=",
+      "dev": true
+    },
+    "repeat-element": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz",
+      "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==",
+      "dev": true
+    },
+    "repeat-string": {
+      "version": "1.6.1",
+      "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz",
+      "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=",
+      "dev": true
+    },
+    "repeating": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz",
+      "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=",
+      "dev": true,
+      "requires": {
+        "is-finite": "^1.0.0"
+      }
+    },
+    "request": {
+      "version": "2.88.0",
+      "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz",
+      "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==",
+      "dev": true,
+      "requires": {
+        "aws-sign2": "~0.7.0",
+        "aws4": "^1.8.0",
+        "caseless": "~0.12.0",
+        "combined-stream": "~1.0.6",
+        "extend": "~3.0.2",
+        "forever-agent": "~0.6.1",
+        "form-data": "~2.3.2",
+        "har-validator": "~5.1.0",
+        "http-signature": "~1.2.0",
+        "is-typedarray": "~1.0.0",
+        "isstream": "~0.1.2",
+        "json-stringify-safe": "~5.0.1",
+        "mime-types": "~2.1.19",
+        "oauth-sign": "~0.9.0",
+        "performance-now": "^2.1.0",
+        "qs": "~6.5.2",
+        "safe-buffer": "^5.1.2",
+        "tough-cookie": "~2.4.3",
+        "tunnel-agent": "^0.6.0",
+        "uuid": "^3.3.2"
+      }
+    },
+    "require-directory": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+      "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=",
+      "dev": true
+    },
+    "require-main-filename": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz",
+      "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=",
+      "dev": true
+    },
+    "require-uncached": {
+      "version": "1.0.3",
+      "resolved": "http://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz",
+      "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=",
+      "dev": true,
+      "requires": {
+        "caller-path": "^0.1.0",
+        "resolve-from": "^1.0.0"
+      }
+    },
+    "resolve": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.8.1.tgz",
+      "integrity": "sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA==",
+      "dev": true,
+      "requires": {
+        "path-parse": "^1.0.5"
+      }
+    },
+    "resolve-from": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz",
+      "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=",
+      "dev": true
+    },
+    "resolve-url": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz",
+      "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=",
+      "dev": true,
+      "optional": true
+    },
+    "restore-cursor": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz",
+      "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=",
+      "dev": true,
+      "requires": {
+        "exit-hook": "^1.0.0",
+        "onetime": "^1.0.0"
+      }
+    },
+    "ret": {
+      "version": "0.1.15",
+      "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz",
+      "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==",
+      "dev": true,
+      "optional": true
+    },
+    "rimraf": {
+      "version": "2.6.2",
+      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz",
+      "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==",
+      "dev": true,
+      "requires": {
+        "glob": "^7.0.5"
+      }
+    },
+    "run-async": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/run-async/-/run-async-0.1.0.tgz",
+      "integrity": "sha1-yK1KXhEGYeQCp9IbUw4AnyX444k=",
+      "dev": true,
+      "requires": {
+        "once": "^1.3.0"
+      }
+    },
+    "rx-lite": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-3.1.2.tgz",
+      "integrity": "sha1-Gc5QLKVyZl87ZHsQk5+X/RYV8QI=",
+      "dev": true
+    },
+    "safe-buffer": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+      "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
+    },
+    "safe-regex": {
+      "version": "1.1.0",
+      "resolved": "http://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz",
+      "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "ret": "~0.1.10"
+      }
+    },
+    "safer-buffer": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+      "dev": true
+    },
+    "sane": {
+      "version": "1.4.1",
+      "resolved": "http://registry.npmjs.org/sane/-/sane-1.4.1.tgz",
+      "integrity": "sha1-iPdj10BA9fDCVrYWPbOZvxEKxxU=",
+      "dev": true,
+      "requires": {
+        "exec-sh": "^0.2.0",
+        "fb-watchman": "^1.8.0",
+        "minimatch": "^3.0.2",
+        "minimist": "^1.1.1",
+        "walker": "~1.0.5",
+        "watch": "~0.10.0"
+      },
+      "dependencies": {
+        "minimist": {
+          "version": "1.2.0",
+          "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+          "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
+          "dev": true
+        }
+      }
+    },
+    "sax": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
+      "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
+      "dev": true
+    },
+    "semver": {
+      "version": "5.6.0",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz",
+      "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==",
+      "dev": true
+    },
+    "set-blocking": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
+      "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
+      "dev": true
+    },
+    "set-value": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz",
+      "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "extend-shallow": "^2.0.1",
+        "is-extendable": "^0.1.1",
+        "is-plain-object": "^2.0.3",
+        "split-string": "^3.0.1"
+      },
+      "dependencies": {
+        "extend-shallow": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "is-extendable": "^0.1.0"
+          }
+        }
+      }
+    },
+    "shelljs": {
+      "version": "0.7.8",
+      "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.7.8.tgz",
+      "integrity": "sha1-3svPh0sNHl+3LhSxZKloMEjprLM=",
+      "dev": true,
+      "requires": {
+        "glob": "^7.0.0",
+        "interpret": "^1.0.0",
+        "rechoir": "^0.6.2"
+      }
+    },
+    "shellwords": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz",
+      "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==",
+      "dev": true
+    },
+    "simple-swizzle": {
+      "version": "0.2.2",
+      "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
+      "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=",
+      "requires": {
+        "is-arrayish": "^0.3.1"
+      },
+      "dependencies": {
+        "is-arrayish": {
+          "version": "0.3.2",
+          "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
+          "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="
+        }
+      }
+    },
+    "slash": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz",
+      "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=",
+      "dev": true
+    },
+    "slice-ansi": {
+      "version": "0.0.4",
+      "resolved": "http://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz",
+      "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=",
+      "dev": true
+    },
+    "smart-buffer": {
+      "version": "1.1.15",
+      "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-1.1.15.tgz",
+      "integrity": "sha1-fxFLW2X6s+KjWqd1uxLw0cZJvxY="
+    },
+    "snapdragon": {
+      "version": "0.8.2",
+      "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz",
+      "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "base": "^0.11.1",
+        "debug": "^2.2.0",
+        "define-property": "^0.2.5",
+        "extend-shallow": "^2.0.1",
+        "map-cache": "^0.2.2",
+        "source-map": "^0.5.6",
+        "source-map-resolve": "^0.5.0",
+        "use": "^3.1.0"
+      },
+      "dependencies": {
+        "define-property": {
+          "version": "0.2.5",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+          "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "is-descriptor": "^0.1.0"
+          }
+        },
+        "extend-shallow": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "is-extendable": "^0.1.0"
+          }
+        }
+      }
+    },
+    "snapdragon-node": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz",
+      "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "define-property": "^1.0.0",
+        "isobject": "^3.0.0",
+        "snapdragon-util": "^3.0.1"
+      },
+      "dependencies": {
+        "define-property": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
+          "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "is-descriptor": "^1.0.0"
+          }
+        },
+        "is-accessor-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+          "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-data-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+          "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "kind-of": "^6.0.0"
+          }
+        },
+        "is-descriptor": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+          "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "is-accessor-descriptor": "^1.0.0",
+            "is-data-descriptor": "^1.0.0",
+            "kind-of": "^6.0.2"
+          }
+        },
+        "isobject": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
+          "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
+          "dev": true,
+          "optional": true
+        },
+        "kind-of": {
+          "version": "6.0.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
+          "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
+          "dev": true,
+          "optional": true
+        }
+      }
+    },
+    "snapdragon-util": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz",
+      "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "kind-of": "^3.2.0"
+      }
+    },
+    "socks": {
+      "version": "1.1.10",
+      "resolved": "http://registry.npm.taobao.org/socks/download/socks-1.1.10.tgz",
+      "integrity": "sha512-ArX4vGPULWjKDKgUnW8YzfI2uXW7kzgkJuB0GnFBA/PfT3exrrOk+7Wk2oeb894Qf20u1PWv9LEgrO0Z82qAzA==",
+      "requires": {
+        "ip": "^1.1.4",
+        "smart-buffer": "^1.0.13"
+      }
+    },
+    "source-map": {
+      "version": "0.5.7",
+      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+      "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
+      "dev": true
+    },
+    "source-map-resolve": {
+      "version": "0.5.2",
+      "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz",
+      "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "atob": "^2.1.1",
+        "decode-uri-component": "^0.2.0",
+        "resolve-url": "^0.2.1",
+        "source-map-url": "^0.4.0",
+        "urix": "^0.1.0"
+      }
+    },
+    "source-map-support": {
+      "version": "0.4.18",
+      "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz",
+      "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==",
+      "dev": true,
+      "requires": {
+        "source-map": "^0.5.6"
+      }
+    },
+    "source-map-url": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz",
+      "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=",
+      "dev": true,
+      "optional": true
+    },
+    "spdx-correct": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.2.tgz",
+      "integrity": "sha512-q9hedtzyXHr5S0A1vEPoK/7l8NpfkFYTq6iCY+Pno2ZbdZR6WexZFtqeVGkGxW3TEJMN914Z55EnAGMmenlIQQ==",
+      "dev": true,
+      "requires": {
+        "spdx-expression-parse": "^3.0.0",
+        "spdx-license-ids": "^3.0.0"
+      }
+    },
+    "spdx-exceptions": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz",
+      "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==",
+      "dev": true
+    },
+    "spdx-expression-parse": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz",
+      "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==",
+      "dev": true,
+      "requires": {
+        "spdx-exceptions": "^2.1.0",
+        "spdx-license-ids": "^3.0.0"
+      }
+    },
+    "spdx-license-ids": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.2.tgz",
+      "integrity": "sha512-qky9CVt0lVIECkEsYbNILVnPvycuEBkXoMFLRWsREkomQLevYhtRKC+R91a5TOAQ3bCMjikRwhyaRqj1VYatYg==",
+      "dev": true
+    },
+    "split-string": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz",
+      "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "extend-shallow": "^3.0.0"
+      }
+    },
+    "sprintf-js": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+      "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
+      "dev": true
+    },
+    "sshpk": {
+      "version": "1.15.2",
+      "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.15.2.tgz",
+      "integrity": "sha512-Ra/OXQtuh0/enyl4ETZAfTaeksa6BXks5ZcjpSUNrjBr0DvrJKX+1fsKDPpT9TBXgHAFsa4510aNVgI8g/+SzA==",
+      "dev": true,
+      "requires": {
+        "asn1": "~0.2.3",
+        "assert-plus": "^1.0.0",
+        "bcrypt-pbkdf": "^1.0.0",
+        "dashdash": "^1.12.0",
+        "ecc-jsbn": "~0.1.1",
+        "getpass": "^0.1.1",
+        "jsbn": "~0.1.0",
+        "safer-buffer": "^2.0.2",
+        "tweetnacl": "~0.14.0"
+      }
+    },
+    "stack-trace": {
+      "version": "0.0.10",
+      "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",
+      "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA="
+    },
+    "static-extend": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz",
+      "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "define-property": "^0.2.5",
+        "object-copy": "^0.1.0"
+      },
+      "dependencies": {
+        "define-property": {
+          "version": "0.2.5",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+          "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "is-descriptor": "^0.1.0"
+          }
+        }
+      }
+    },
+    "string-width": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
+      "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
+      "dev": true,
+      "requires": {
+        "code-point-at": "^1.0.0",
+        "is-fullwidth-code-point": "^1.0.0",
+        "strip-ansi": "^3.0.0"
+      }
+    },
+    "string_decoder": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+      "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+      "requires": {
+        "safe-buffer": "~5.1.0"
+      }
+    },
+    "strip-ansi": {
+      "version": "3.0.1",
+      "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+      "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+      "dev": true,
+      "requires": {
+        "ansi-regex": "^2.0.0"
+      }
+    },
+    "strip-bom": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz",
+      "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=",
+      "dev": true,
+      "requires": {
+        "is-utf8": "^0.2.0"
+      }
+    },
+    "strip-json-comments": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+      "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=",
+      "dev": true
+    },
+    "supports-color": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+      "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+      "dev": true
+    },
+    "symbol-tree": {
+      "version": "3.2.2",
+      "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.2.tgz",
+      "integrity": "sha1-rifbOPZgp64uHDt9G8KQgZuFGeY=",
+      "dev": true
+    },
+    "table": {
+      "version": "3.8.3",
+      "resolved": "http://registry.npmjs.org/table/-/table-3.8.3.tgz",
+      "integrity": "sha1-K7xULw/amGGnVdOUf+/Ys/UThV8=",
+      "dev": true,
+      "requires": {
+        "ajv": "^4.7.0",
+        "ajv-keywords": "^1.0.0",
+        "chalk": "^1.1.1",
+        "lodash": "^4.0.0",
+        "slice-ansi": "0.0.4",
+        "string-width": "^2.0.0"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
+          "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
+          "dev": true
+        },
+        "chalk": {
+          "version": "1.1.3",
+          "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+          "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^2.2.1",
+            "escape-string-regexp": "^1.0.2",
+            "has-ansi": "^2.0.0",
+            "strip-ansi": "^3.0.0",
+            "supports-color": "^2.0.0"
+          },
+          "dependencies": {
+            "ansi-regex": {
+              "version": "2.1.1",
+              "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+              "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
+              "dev": true
+            },
+            "strip-ansi": {
+              "version": "3.0.1",
+              "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+              "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+              "dev": true,
+              "requires": {
+                "ansi-regex": "^2.0.0"
+              }
+            }
+          }
+        },
+        "is-fullwidth-code-point": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
+          "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
+          "dev": true
+        },
+        "string-width": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
+          "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
+          "dev": true,
+          "requires": {
+            "is-fullwidth-code-point": "^2.0.0",
+            "strip-ansi": "^4.0.0"
+          }
+        },
+        "strip-ansi": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+          "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^3.0.0"
+          }
+        }
+      }
+    },
+    "test-exclude": {
+      "version": "3.3.0",
+      "resolved": "http://registry.npmjs.org/test-exclude/-/test-exclude-3.3.0.tgz",
+      "integrity": "sha1-ehfKEjmYjJg2ewYhRW27fUvDiXc=",
+      "dev": true,
+      "requires": {
+        "arrify": "^1.0.1",
+        "micromatch": "^2.3.11",
+        "object-assign": "^4.1.0",
+        "read-pkg-up": "^1.0.1",
+        "require-main-filename": "^1.0.1"
+      }
+    },
+    "text-hex": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
+      "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="
+    },
+    "text-table": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+      "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=",
+      "dev": true
+    },
+    "throat": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/throat/-/throat-3.2.0.tgz",
+      "integrity": "sha512-/EY8VpvlqJ+sFtLPeOgc8Pl7kQVOWv0woD87KTXVHPIAE842FGT+rokxIhe8xIUP1cfgrkt0as0vDLjDiMtr8w==",
+      "dev": true
+    },
+    "through": {
+      "version": "2.3.8",
+      "resolved": "http://registry.npmjs.org/through/-/through-2.3.8.tgz",
+      "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=",
+      "dev": true
+    },
+    "tmpl": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz",
+      "integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=",
+      "dev": true
+    },
+    "to-fast-properties": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz",
+      "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=",
+      "dev": true
+    },
+    "to-object-path": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz",
+      "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "kind-of": "^3.0.2"
+      }
+    },
+    "to-regex": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz",
+      "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "define-property": "^2.0.2",
+        "extend-shallow": "^3.0.2",
+        "regex-not": "^1.0.2",
+        "safe-regex": "^1.1.0"
+      }
+    },
+    "to-regex-range": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz",
+      "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "is-number": "^3.0.0",
+        "repeat-string": "^1.6.1"
+      },
+      "dependencies": {
+        "is-number": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
+          "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "kind-of": "^3.0.2"
+          }
+        }
+      }
+    },
+    "tough-cookie": {
+      "version": "2.4.3",
+      "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",
+      "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==",
+      "dev": true,
+      "requires": {
+        "psl": "^1.1.24",
+        "punycode": "^1.4.1"
+      },
+      "dependencies": {
+        "punycode": {
+          "version": "1.4.1",
+          "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
+          "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=",
+          "dev": true
+        }
+      }
+    },
+    "tr46": {
+      "version": "0.0.3",
+      "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+      "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=",
+      "dev": true
+    },
+    "trim-right": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz",
+      "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=",
+      "dev": true
+    },
+    "triple-beam": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz",
+      "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw=="
+    },
+    "tunnel-agent": {
+      "version": "0.6.0",
+      "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
+      "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
+      "dev": true,
+      "requires": {
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "tweetnacl": {
+      "version": "0.14.5",
+      "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
+      "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=",
+      "dev": true
+    },
+    "type-check": {
+      "version": "0.3.2",
+      "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
+      "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=",
+      "dev": true,
+      "requires": {
+        "prelude-ls": "~1.1.2"
+      }
+    },
+    "typedarray": {
+      "version": "0.0.6",
+      "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
+      "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=",
+      "dev": true
+    },
+    "uglify-js": {
+      "version": "3.6.0",
+      "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.0.tgz",
+      "integrity": "sha512-W+jrUHJr3DXKhrsS7NUVxn3zqMOFn0hL/Ei6v0anCIMoKC93TjcflTagwIHLW7SfMFfiQuktQyFVCFHGUE0+yg==",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "commander": "~2.20.0",
+        "source-map": "~0.6.1"
+      },
+      "dependencies": {
+        "commander": {
+          "version": "2.20.0",
+          "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz",
+          "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==",
+          "dev": true,
+          "optional": true
+        },
+        "source-map": {
+          "version": "0.6.1",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+          "dev": true,
+          "optional": true
+        }
+      }
+    },
+    "union-value": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz",
+      "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "arr-union": "^3.1.0",
+        "get-value": "^2.0.6",
+        "is-extendable": "^0.1.1",
+        "set-value": "^2.0.1"
+      }
+    },
+    "unset-value": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz",
+      "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "has-value": "^0.3.1",
+        "isobject": "^3.0.0"
+      },
+      "dependencies": {
+        "has-value": {
+          "version": "0.3.1",
+          "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz",
+          "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "get-value": "^2.0.3",
+            "has-values": "^0.1.4",
+            "isobject": "^2.0.0"
+          },
+          "dependencies": {
+            "isobject": {
+              "version": "2.1.0",
+              "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz",
+              "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=",
+              "dev": true,
+              "optional": true,
+              "requires": {
+                "isarray": "1.0.0"
+              }
+            }
+          }
+        },
+        "has-values": {
+          "version": "0.1.4",
+          "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz",
+          "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=",
+          "dev": true,
+          "optional": true
+        },
+        "isobject": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
+          "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
+          "dev": true,
+          "optional": true
+        }
+      }
+    },
+    "uri-js": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
+      "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==",
+      "dev": true,
+      "requires": {
+        "punycode": "^2.1.0"
+      }
+    },
+    "urix": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz",
+      "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=",
+      "dev": true,
+      "optional": true
+    },
+    "use": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
+      "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==",
+      "dev": true,
+      "optional": true
+    },
+    "user-home": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/user-home/-/user-home-1.1.1.tgz",
+      "integrity": "sha1-K1viOjK2Onyd640PKNSFcko98ZA=",
+      "dev": true
+    },
+    "util-deprecate": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+      "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
+    },
+    "uuid": {
+      "version": "3.3.2",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
+      "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==",
+      "dev": true
+    },
+    "v8flags": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-2.1.1.tgz",
+      "integrity": "sha1-qrGh+jDUX4jdMhFIh1rALAtV5bQ=",
+      "dev": true,
+      "requires": {
+        "user-home": "^1.1.1"
+      }
+    },
+    "validate-npm-package-license": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
+      "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
+      "dev": true,
+      "requires": {
+        "spdx-correct": "^3.0.0",
+        "spdx-expression-parse": "^3.0.0"
+      }
+    },
+    "verror": {
+      "version": "1.10.0",
+      "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
+      "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
+      "dev": true,
+      "requires": {
+        "assert-plus": "^1.0.0",
+        "core-util-is": "1.0.2",
+        "extsprintf": "^1.2.0"
+      }
+    },
+    "walker": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz",
+      "integrity": "sha1-L3+bj9ENZ3JisYqITijRlhjgKPs=",
+      "dev": true,
+      "requires": {
+        "makeerror": "1.0.x"
+      }
+    },
+    "watch": {
+      "version": "0.10.0",
+      "resolved": "https://registry.npmjs.org/watch/-/watch-0.10.0.tgz",
+      "integrity": "sha1-d3mLLaD5kQ1ZXxrOWwwiWFIfIdw=",
+      "dev": true
+    },
+    "webidl-conversions": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz",
+      "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==",
+      "dev": true
+    },
+    "whatwg-encoding": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz",
+      "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==",
+      "dev": true,
+      "requires": {
+        "iconv-lite": "0.4.24"
+      }
+    },
+    "whatwg-url": {
+      "version": "4.8.0",
+      "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-4.8.0.tgz",
+      "integrity": "sha1-0pgaqRSMHgCkHFphMRZqtGg7vMA=",
+      "dev": true,
+      "requires": {
+        "tr46": "~0.0.3",
+        "webidl-conversions": "^3.0.0"
+      },
+      "dependencies": {
+        "webidl-conversions": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+          "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=",
+          "dev": true
+        }
+      }
+    },
+    "which": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
+      "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+      "dev": true,
+      "requires": {
+        "isexe": "^2.0.0"
+      }
+    },
+    "which-module": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz",
+      "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=",
+      "dev": true
+    },
+    "winston": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/winston/-/winston-3.1.0.tgz",
+      "integrity": "sha512-FsQfEE+8YIEeuZEYhHDk5cILo1HOcWkGwvoidLrDgPog0r4bser1lEIOco2dN9zpDJ1M88hfDgZvxe5z4xNcwg==",
+      "requires": {
+        "async": "^2.6.0",
+        "diagnostics": "^1.1.1",
+        "is-stream": "^1.1.0",
+        "logform": "^1.9.1",
+        "one-time": "0.0.4",
+        "readable-stream": "^2.3.6",
+        "stack-trace": "0.0.x",
+        "triple-beam": "^1.3.0",
+        "winston-transport": "^4.2.0"
+      }
+    },
+    "winston-transport": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.2.0.tgz",
+      "integrity": "sha512-0R1bvFqxSlK/ZKTH86nymOuKv/cT1PQBMuDdA7k7f0S9fM44dNH6bXnuxwXPrN8lefJgtZq08BKdyZ0DZIy/rg==",
+      "requires": {
+        "readable-stream": "^2.3.6",
+        "triple-beam": "^1.2.0"
+      }
+    },
+    "wordwrap": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
+      "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=",
+      "dev": true
+    },
+    "worker-farm": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.6.0.tgz",
+      "integrity": "sha512-6w+3tHbM87WnSWnENBUvA2pxJPLhQUg5LKwUQHq3r+XPhIM+Gh2R5ycbwPCyuGbNg+lPgdcnQUhuC02kJCvffQ==",
+      "dev": true,
+      "requires": {
+        "errno": "~0.1.7"
+      }
+    },
+    "wrap-ansi": {
+      "version": "2.1.0",
+      "resolved": "http://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
+      "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=",
+      "dev": true,
+      "requires": {
+        "string-width": "^1.0.1",
+        "strip-ansi": "^3.0.1"
+      }
+    },
+    "wrappy": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+      "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
+      "dev": true
+    },
+    "write": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz",
+      "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=",
+      "dev": true,
+      "requires": {
+        "mkdirp": "^0.5.1"
+      }
+    },
+    "xml-name-validator": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-2.0.1.tgz",
+      "integrity": "sha1-TYuPHszTQZqjYgYb7O9RXh5VljU=",
+      "dev": true
+    },
+    "xtend": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz",
+      "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=",
+      "dev": true
+    },
+    "y18n": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz",
+      "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=",
+      "dev": true
+    },
+    "yargs": {
+      "version": "6.6.0",
+      "resolved": "http://registry.npmjs.org/yargs/-/yargs-6.6.0.tgz",
+      "integrity": "sha1-eC7CHvQDNF+DCoCMo9UTr1YGUgg=",
+      "dev": true,
+      "requires": {
+        "camelcase": "^3.0.0",
+        "cliui": "^3.2.0",
+        "decamelize": "^1.1.1",
+        "get-caller-file": "^1.0.1",
+        "os-locale": "^1.4.0",
+        "read-pkg-up": "^1.0.1",
+        "require-directory": "^2.1.1",
+        "require-main-filename": "^1.0.1",
+        "set-blocking": "^2.0.0",
+        "string-width": "^1.0.2",
+        "which-module": "^1.0.0",
+        "y18n": "^3.2.1",
+        "yargs-parser": "^4.2.0"
+      }
+    },
+    "yargs-parser": {
+      "version": "4.2.1",
+      "resolved": "http://registry.npmjs.org/yargs-parser/-/yargs-parser-4.2.1.tgz",
+      "integrity": "sha1-KczqwNxPA8bIe0qfIX3RjJ90hxw=",
+      "dev": true,
+      "requires": {
+        "camelcase": "^3.0.0"
+      }
+    }
+  }
+}

+ 50 - 0
http-proxy-to-socks/package.json

@@ -0,0 +1,50 @@
+{
+  "name": "http-proxy-to-socks",
+  "version": "1.1.2",
+  "description": "hpts(http-proxy-to-socks) is a nodejs client to convert socks proxy into http proxy",
+  "main": "lib/server.js",
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/oyyd/http-proxy-to-socks"
+  },
+  "scripts": {
+    "build": "babel src --out-dir lib",
+    "dev": "jest --watch",
+    "test": "eslint src && jest",
+    "prepublish": "npm run test && npm run build"
+  },
+  "keywords": [
+    "socks",
+    "http proxy",
+    "converting proxy"
+  ],
+  "bin": {
+    "hpts": "./bin/hpts.js"
+  },
+  "author": "oyyd <oyydoibh@gmail.com>",
+  "license": "MIT",
+  "dependencies": {
+    "chalk": "^2.4.1",
+    "commander": "^2.9.0",
+    "socks": "^1.1.10",
+    "winston": "^3.1.0"
+  },
+  "devDependencies": {
+    "babel-cli": "^6.26.0",
+    "babel-jest": "^18.0.0",
+    "babel-plugin-transform-async-to-generator": "^6.22.0",
+    "babel-polyfill": "^6.22.0",
+    "babel-preset-es2015": "^6.18.0",
+    "eslint": "^3.13.1",
+    "eslint-config-airbnb": "^14.0.0",
+    "eslint-config-airbnb-base": "^11.0.1",
+    "eslint-plugin-import": "^2.2.0",
+    "jest": "^18.1.0"
+  },
+  "jest": {
+    "testPathDirs": [
+      "<rootDir>/src"
+    ],
+    "testRegex": "(\\.|/)(test|spec)\\.js$"
+  }
+}

+ 26 - 0
http-proxy-to-socks/src/__tests__/cli.spec.js

@@ -0,0 +1,26 @@
+const { getOptionsArgs } = require('../cli');
+
+describe('cli', () => {
+  describe('getOptionsArgs', () => {
+    it('should return an object with specified fields when provided', () => {
+      const options = {
+        socks: 'socks',
+        port: 'port',
+        level: 'level',
+        config: 'config',
+        FIELD_NOT_EXIST: 'FIELD_NOT_EXIST',
+      };
+
+      const res = getOptionsArgs(options);
+
+      Object.keys(options).forEach((name) => {
+        if (name === 'FIELD_NOT_EXIST') {
+          expect(res[name]).toBeFalsy();
+          return;
+        }
+
+        expect(res[name]).toBeTruthy();
+      });
+    });
+  });
+});

+ 264 - 0
http-proxy-to-socks/src/__tests__/proxy_server.spec.js

@@ -0,0 +1,264 @@
+const mockAgent = {};
+
+const mockOn = jest.fn();
+
+jest.mock('http', () => ({
+  request: jest.fn(() => ({
+    on: mockOn,
+  })),
+  Server: jest.fn(function Server() {
+    this.addListener = jest.fn();
+  }),
+}));
+
+jest.mock('socks', () => ({
+  createConnection: jest.fn(),
+  Agent: jest.fn(() => mockAgent),
+}));
+
+function last(array) {
+  return array[array.length - 1];
+}
+
+function getLastMockOn(event) {
+  return last(mockOn.mock.calls.filter(args => args[0] === event));
+}
+
+const http = require('http');
+const Socks = require('socks');
+const {
+  createServer,
+  getProxyObject,
+  parseProxyLine,
+  requestListener,
+  connectListener,
+} = require('../proxy_server');
+
+describe('proxy_server', () => {
+  const requestURL = 'https://google.com';
+  let getProxyInfo;
+  let request;
+  let socksRequest;
+  let response;
+  let socketRequest;
+  let socket;
+
+  beforeEach(() => {
+    getProxyInfo = jest.fn(() => ({
+      ipaddress: '127.0.0.1',
+      port: 8080,
+      type: 5,
+      authentication: { username: '', password: '' },
+    }));
+
+    request = {
+      on: jest.fn(),
+      pipe: jest.fn(),
+      url: requestURL,
+    };
+
+    socksRequest = {
+      on: jest.fn(),
+      pipe: jest.fn(),
+      url: requestURL.slice('https://'.length),
+    };
+
+    response = {
+      on: jest.fn(),
+      writeHead: jest.fn(),
+      end: jest.fn(),
+    };
+
+    socketRequest = {
+      on: jest.fn(),
+      write: jest.fn(),
+      pipe: jest.fn(),
+    };
+
+    socket = {
+      on: jest.fn(),
+      pipe: jest.fn(),
+      write: jest.fn(),
+      resume: jest.fn(),
+    };
+  });
+
+  describe('getProxyObject', () => {
+    it('should return a object with "ipaddress", "port", "type", "authentication" properties', () => {
+      const host = '127.0.0.1';
+      const port = '8080';
+      const res = getProxyObject(host, port);
+
+      expect(typeof res).toBe('object');
+      expect(res.ipaddress).toBe(host);
+      expect(res.port).toBe(parseInt(port, 10));
+      expect(res.type).toBe(5);
+      expect(typeof res.authentication).toBe('object');
+      expect(Object.hasOwnProperty.apply(res.authentication, ['username'])).toBeTruthy();
+      expect(Object.hasOwnProperty.apply(res.authentication, ['password'])).toBeTruthy();
+    });
+  });
+
+  describe('parseProxyLine', () => {
+    it('should return a object with "host" and "port" extracted from proxy string', () => {
+      const proxyLine = '127.0.0.1:1080';
+      const res = parseProxyLine(proxyLine);
+
+      expect(typeof res).toBe('object');
+      expect(res.ipaddress).toBe('127.0.0.1');
+      expect(res.port).toBe(1080);
+    });
+
+    it('should also contain "username" and "password" properties when it contains these info', () => {
+      const proxyLine = '127.0.0.1:1080:oyyd:password';
+      const res = parseProxyLine(proxyLine);
+
+      expect(typeof res).toBe('object');
+      expect(res.ipaddress).toBe('127.0.0.1');
+      expect(res.port).toBe(1080);
+      expect(typeof res.authentication).toBe('object');
+      expect(res.authentication.username).toBe('oyyd');
+      expect(res.authentication.password).toBe('password');
+    });
+
+    it('should throw error when the proxy string seems not good', () => {
+      let proxyLine = '127.0.0.1';
+      let error = null;
+
+      try {
+        error = parseProxyLine(proxyLine);
+      } catch (err) {
+        error = err;
+      }
+
+      expect(error instanceof Error).toBeTruthy();
+
+      proxyLine = '127.0.0.1:8080:oyyd';
+      error = null;
+
+      try {
+        error = parseProxyLine(proxyLine);
+      } catch (err) {
+        error = err;
+      }
+
+      expect(error instanceof Error).toBeTruthy();
+    });
+  });
+
+  describe('requestListener', () => {
+    it('should create an socks agent and take it as request agent', () => {
+      requestListener(getProxyInfo, request, response);
+
+      const lastCall = last(Socks.Agent.mock.calls);
+      const httpLastCall = last(http.request.mock.calls);
+
+      expect(requestURL.indexOf(lastCall[0].target.host) > -1).toBeTruthy();
+      expect(httpLastCall[0].agent === mockAgent).toBeTruthy();
+    });
+
+    it('should return 500 when error thrown', () => {
+      requestListener(getProxyInfo, request, response);
+
+      const onErrorArgs = getLastMockOn('error');
+
+      expect(onErrorArgs).toBeTruthy();
+
+      const error = new Error('500');
+
+      onErrorArgs[1](error);
+
+      expect(response.writeHead.mock.calls[0][0]).toBe(500);
+      expect(response.end.mock.calls[0][0].indexOf('error') > -1).toBeTruthy();
+    });
+
+    it('should pipe response when "response"', () => {
+      const proxyResponse = {
+        statusCode: 200,
+        headers: {},
+        pipe: jest.fn(),
+      };
+
+      requestListener(getProxyInfo, request, response);
+
+      const onResponseArgs = getLastMockOn('response');
+
+      expect(onResponseArgs).toBeTruthy();
+
+      onResponseArgs[1](proxyResponse);
+
+      expect(proxyResponse.pipe.mock.calls[0][0]).toBe(response);
+    });
+  });
+
+  describe('connectListener', () => {
+    it('should create socks connections', () => {
+      const head = '';
+      connectListener(getProxyInfo, socksRequest, socketRequest, head);
+
+      const lastCreateConnectionCall = last(Socks.createConnection.mock.calls);
+
+      expect(lastCreateConnectionCall[0].target.host).toBe('google.com');
+    });
+
+    it('should write 500 when error thrown', () => {
+      const head = '';
+      connectListener(getProxyInfo, socksRequest, socketRequest, head);
+
+      const lastCreateConnectionCall = last(Socks.createConnection.mock.calls);
+
+      const error = new Error('500');
+
+      lastCreateConnectionCall[1](error, socket);
+
+      expect(socketRequest.write.mock.calls[0][0].indexOf('500') > -1).toBeTruthy();
+      expect(socket.pipe.mock.calls.length === 0).toBeTruthy();
+    });
+
+    it('should pipe sockets when socket connected', () => {
+      const head = '';
+
+      connectListener(getProxyInfo, socksRequest, socketRequest, head);
+
+      const lastCreateConnectionCall = last(Socks.createConnection.mock.calls);
+
+      lastCreateConnectionCall[1](null, socket);
+
+      expect(socketRequest.pipe.mock.calls[0][0]).toBe(socket);
+      expect(socket.pipe.mock.calls[0][0]).toBe(socketRequest);
+      expect(socketRequest.write.mock.calls[0][0].indexOf('200') > -1).toBeTruthy();
+      expect(socket.write.mock.calls[0][0]).toBe(head);
+    });
+  });
+
+  describe('createServer', () => {
+    it('should push this.proxyList', () => {
+      const options = {
+        socks: '127.0.0.1:1080',
+      };
+
+      createServer(options);
+
+      const { proxyList } = http.Server.mock.instances[0];
+
+      expect(proxyList[0].ipaddress).toBe('127.0.0.1');
+      expect(proxyList[0].port).toBe(1080);
+    });
+
+    it('should listen both "request" and "connect" events', () => {
+      const options = {
+        proxy: '127.0.0.1:1080',
+      };
+
+      createServer(options);
+
+      const { addListener } = http.Server.mock.instances[0];
+
+      const onRequestArgs = addListener.mock.calls.filter(args => args[0] === 'request');
+      const onConnectArgs = addListener.mock.calls.filter(args => args[0] === 'connect');
+
+      expect(onRequestArgs.length > 0).toBeTruthy();
+      expect(onConnectArgs.length > 0).toBeTruthy();
+    });
+  });
+});

+ 7 - 0
http-proxy-to-socks/src/__tests__/server.spec.js

@@ -0,0 +1,7 @@
+const server = require('../server');
+
+describe('server', () => {
+  it('should export `createServer`', () => {
+    expect(typeof server.createServer).toBe('function');
+  });
+});

+ 73 - 0
http-proxy-to-socks/src/cli.js

@@ -0,0 +1,73 @@
+const { readFileSync } = require('fs');
+const { resolve } = require('path');
+const cli = require('commander');
+const { version } = require('../package.json');
+const { createServer } = require('./server');
+
+const optionNames = [
+  'socks',
+  'port',
+  'level',
+  'config',
+  'host',
+];
+
+function getFileConfig(filePath) {
+  const absFile = resolve(process.cwd(), filePath);
+
+  const content = readFileSync(absFile).toString('utf8');
+
+  let fileConfig = null;
+
+  try {
+    fileConfig = JSON.parse(content);
+  } catch (err) {
+    const error = new Error(`invalid json content: ${err.message}`);
+    error.code = err.code;
+    throw error;
+  }
+
+  return fileConfig;
+}
+
+function getOptionsArgs(args) {
+  const options = {};
+
+  optionNames.forEach((name) => {
+    if (Object.hasOwnProperty.apply(args, [name])) {
+      if (typeof args[name] !== 'string') {
+        throw new Error(`string "${name}" expected`);
+      }
+      options[name] = args[name];
+    }
+  });
+
+  return options;
+}
+
+function main() {
+  cli.version(version)
+    .option('-s, --socks [socks]', 'specify your socks proxy host, default: 127.0.0.1:1080')
+    .option('-p, --port [port]', 'specify the listening port of http proxy server, default: 8080')
+    .option('-l, --host [host]', 'specify the listening host of http proxy server, default: 127.0.0.1')
+    .option('-c, --config [config]', 'read configs from file in json format')
+    .option('--level [level]', 'log level, vals: info, error')
+    .parse(process.argv);
+
+  const options = getOptionsArgs(cli);
+
+  let fileConfig = null;
+
+  if (options.config) {
+    fileConfig = getFileConfig(options.config);
+  }
+
+  Object.assign(options, fileConfig);
+
+  createServer(options);
+}
+
+module.exports = {
+  getOptionsArgs,
+  main,
+};

+ 68 - 0
http-proxy-to-socks/src/logger.js

@@ -0,0 +1,68 @@
+const winston = require('winston');
+const chalk = require('chalk');
+
+const { format, createLogger: createWinstonLogger, transports } = winston;
+const { Console } = transports;
+const { combine, printf } = format;
+
+const DEFAULT_COMMON_OPTIONS = {
+  colorize: true,
+  timestamp: true,
+};
+
+function fillDigit(num) {
+  const str = String(num);
+  if (str.length === 1) {
+    return `0${str}`;
+  }
+
+  return str;
+}
+
+function getFormatedDate() {
+  const date = new Date();
+
+  return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`
+      + ` ${fillDigit(date.getHours())}:${fillDigit(date.getMinutes())}`
+      + `:${fillDigit(date.getSeconds())}`;
+}
+
+function createTransports() {
+  return [
+    new Console(Object.assign({}, DEFAULT_COMMON_OPTIONS)),
+  ];
+}
+
+function colorLevel(str) {
+  if (str === 'error') {
+    return chalk.red(str);
+  } else if (str === 'info') {
+    return chalk.green(str);
+  }
+
+  return str;
+}
+
+function createLogger(level = 'warn') {
+  return createWinstonLogger({
+    format: combine(printf(info =>
+        `${getFormatedDate()} - ${colorLevel(info.level)} ${info.message}`)),
+    level,
+    transports: createTransports(),
+  });
+}
+
+function changeLevel(logger, level) {
+  logger.configure({
+    level,
+    transports: createTransports(),
+  });
+}
+
+const logger = createLogger();
+
+module.exports = {
+  logger,
+  createLogger,
+  changeLevel,
+};

+ 197 - 0
http-proxy-to-socks/src/proxy_server.js

@@ -0,0 +1,197 @@
+// inspired by https://github.com/asluchevskiy/http-to-socks-proxy
+const util = require('util');
+const url = require('url');
+const http = require('http');
+const fs = require('fs');
+const Socks = require('socks');
+const { logger } = require('./logger');
+
+function randomElement(array) {
+  return array[Math.floor(Math.random() * array.length)];
+}
+
+function getProxyObject(host, port, login, password) {
+  return {
+    ipaddress: host,
+    port: parseInt(port, 10),
+    type: 5,
+    authentication: { username: login || '', password: password || '' },
+  };
+}
+
+function parseProxyLine(line) {
+  const proxyInfo = line.split(':');
+
+  if (proxyInfo.length !== 4 && proxyInfo.length !== 2) {
+    throw new Error(`Incorrect proxy line: ${line}`);
+  }
+
+  return getProxyObject.apply(this, proxyInfo);
+}
+
+function requestListener(getProxyInfo, request, response) {
+  logger.info(`request: ${request.url}`);
+
+  const proxy = getProxyInfo();
+  const ph = url.parse(request.url);
+
+  const socksAgent = new Socks.Agent({
+    proxy,
+    target: { host: ph.hostname, port: ph.port },
+  });
+
+  const options = {
+    port: ph.port,
+    hostname: ph.hostname,
+    method: request.method,
+    path: ph.path,
+    headers: request.headers,
+    agent: socksAgent,
+  };
+
+  const proxyRequest = http.request(options);
+
+  request.on('error', (err) => {
+    logger.error(`${err.message}`);
+    proxyRequest.destroy(err);
+  });
+
+  proxyRequest.on('error', (error) => {
+    logger.error(`${error.message} on proxy ${proxy.ipaddress}:${proxy.port}`);
+    response.writeHead(500);
+    response.end('Connection error\n');
+  });
+
+  proxyRequest.on('response', (proxyResponse) => {
+    proxyResponse.pipe(response);
+    response.writeHead(proxyResponse.statusCode, proxyResponse.headers);
+  });
+
+  request.pipe(proxyRequest);
+}
+
+function connectListener(getProxyInfo, request, socketRequest, head) {
+  logger.info(`connect: ${request.url}`);
+
+  const proxy = getProxyInfo();
+
+  const ph = url.parse(`http://${request.url}`);
+  const { hostname: host, port } = ph;
+
+  const options = {
+    proxy,
+    target: { host, port },
+    command: 'connect',
+  };
+
+  let socket;
+
+  socketRequest.on('error', (err) => {
+    logger.error(`${err.message}`);
+    if (socket) {
+      socket.destroy(err);
+    }
+  });
+
+  Socks.createConnection(options, (error, _socket) => {
+    socket = _socket;
+
+    if (error) {
+      // error in SocksSocket creation
+      logger.error(`${error.message} connection creating on ${proxy.ipaddress}:${proxy.port}`);
+      socketRequest.write(`HTTP/${request.httpVersion} 500 Connection error\r\n\r\n`);
+      return;
+    }
+
+    socket.on('error', (err) => {
+      logger.error(`${err.message}`);
+      socketRequest.destroy(err);
+    });
+
+    // tunneling to the host
+    socket.pipe(socketRequest);
+    socketRequest.pipe(socket);
+
+    socket.write(head);
+    socketRequest.write(`HTTP/${request.httpVersion} 200 Connection established\r\n\r\n`);
+    socket.resume();
+  });
+}
+
+function ProxyServer(options) {
+  // TODO: start point
+  http.Server.call(this, () => {});
+
+  this.proxyList = [];
+
+  if (options.socks) {
+    // stand alone proxy loging
+    this.loadProxy(options.socks);
+  } else if (options.socksList) {
+    // proxy list loading
+    this.loadProxyFile(options.socksList);
+    if (options.proxyListReloadTimeout) {
+      setInterval(
+        () => {
+          this.loadProxyFile(options.socksList);
+        },
+        options.proxyListReloadTimeout * 1000
+      );
+    }
+  }
+
+  this.addListener(
+    'request',
+    requestListener.bind(null, () => randomElement(this.proxyList))
+  );
+  this.addListener(
+    'connect',
+    connectListener.bind(null, () => randomElement(this.proxyList))
+  );
+}
+
+util.inherits(ProxyServer, http.Server);
+
+ProxyServer.prototype.loadProxy = function loadProxy(proxyLine) {
+  try {
+    this.proxyList.push(parseProxyLine(proxyLine));
+  } catch (ex) {
+    logger.error(ex.message);
+  }
+};
+
+ProxyServer.prototype.loadProxyFile = function loadProxyFile(fileName) {
+  const self = this;
+
+  logger.info(`Loading proxy list from file: ${fileName}`);
+
+  fs.readFile(fileName, (err, data) => {
+    if (err) {
+      logger.error(
+        `Impossible to read the proxy file : ${fileName} error : ${err.message}`
+      );
+      return;
+    }
+
+    const lines = data.toString().split('\n');
+    const proxyList = [];
+    for (let i = 0; i < lines.length; i += 1) {
+      if (!(lines[i] !== '' && lines[i].charAt(0) !== '#')) {
+        try {
+          proxyList.push(parseProxyLine(lines[i]));
+        } catch (ex) {
+          logger.error(ex.message);
+        }
+      }
+    }
+    self.proxyList = proxyList;
+  });
+};
+
+module.exports = {
+  createServer: options => new ProxyServer(options),
+  requestListener,
+  connectListener,
+  getProxyObject,
+  parseProxyLine,
+};

+ 28 - 0
http-proxy-to-socks/src/server.js

@@ -0,0 +1,28 @@
+const { logger, changeLevel } = require('./logger');
+const { createServer: createProxyServer } = require('./proxy_server');
+
+const DEFAULT_OPTIONS = {
+  host: '127.0.0.1',
+  socks: '127.0.0.1:1080',
+  proxyListReloadTimeout: 60,
+  port: 8080,
+};
+
+function createServer(opts) {
+  const options = Object.assign({}, DEFAULT_OPTIONS, opts);
+
+  if (typeof options.level === 'string') {
+    changeLevel(logger, options.level);
+  }
+
+  const { port, socks, host } = options;
+
+  // eslint-disable-next-line
+  console.log(`SOCKS: ${socks}\nhttp-proxy listening: ${host}:${port}`);
+
+  return createProxyServer(options).listen(port, host);
+}
+
+module.exports = {
+  createServer,
+};

+ 7 - 0
polipo/.gitignore

@@ -0,0 +1,7 @@
+polipo
+polipo\.exe
+core
+*.o
+polipo.html
+polipo.info
+html/

+ 7 - 0
polipo/.hgignore

@@ -0,0 +1,7 @@
+syntax: glob
+
+polipo
+polipo.info
+core
+html
+*.o

+ 563 - 0
polipo/CHANGES

@@ -0,0 +1,563 @@
+Polipo 1.1.2 (unreleased):
+
+  * Implemented support for SOCKS5 authenticatin (thanks to Amir Hossein
+    and Heinrich Hartmann).
+  * Implement support for the OPTIONS method (thanks to Jan Kraetzschmar).
+  * Implemented support for binding the server side to a specific address
+    (thanks to Sashka Nochkin).
+  * Add configuration variable dnsNameServerPort (thanks to Julio
+    Sangrador-Paton).
+  * Fix a bug that could prevent the on-disk cache from being written out.
+    Thanks to David Bonnes.
+  * Allow POST with no Content-Length.  Thanks to Jamie Jones.
+  * Windows build fixes.
+  * Added support for musl, removed support for Linux libc5.
+  * Dropped support for very old OpenBSD versions.
+  * Fixed the syntax of the generated Warning headers.
+
+14 May 2014: Polipo 1.1.1:
+
+  * Fix a typo that would cause a crash when connecting to a SOCKS
+    server identified by a DNS CNAME.  Thanks to soflare.
+  * Fix a typo that could cause an infinite loop when tunnelling.
+    Thanks to xnoreq for the extensive debugging session.
+  * Fix incorrect error handling that would cause a crash when sending
+    PUT or POST to the local server.
+  * Various improvements to the Windows port.  Thanks to xnoreq.
+
+14 April 2014: Polipo 1.1.0:
+
+  * Proper va_list handling for AMD64 and other RISC-like architectures
+    (Juliusz Chroboczek and Aleksandar Kuktin).
+  * Implemented work-around for broken Content-Range headers.
+  * Use 1024 instead of 32 as the length of the listen queue.
+  * Implemented the ability to control the permissions set on the log file.
+  * Implemented the ability to scrub private information from logs.
+  * Fixed a bug that could cause incorrect logging to syslog (thanks
+    to Sami Farin).
+  * Tweaked the portable version of mktime_gmt.  This will hopefully
+    fix the time issues on Windows.  Thanks to MaxWell and Greg.
+  * Changed chunk allocator to use larger arenas on 64-bit arches.
+  * Fixed a bug that could prevent saving objects on disk.  Thanks to
+    Ming Fu.
+  * Fixed a bug that could prevent parsing large headers.  Thanks to
+    Ming Fu.
+  * Fixed an issue that could cause crash messages to go into the disk
+    cache.
+  * Fixed a bug that could cause the default chunk memory to be incorrect
+    on FreeBSD machines.  Thanks to Frank Behrens.
+  * Fixed a number of bugs in the validation of Range requests.  Thanks to
+    Ken Brazier.
+  * Inhibited range requests for non-200 instances, as this breaks some
+    client software.  Thanks to Ken Brazier.
+  * Fix a integer overflow in processing client requests.
+  * Implement authentication on tunnelled connections.  Thanks to
+    Stephen Dolan.
+  * Implement ability to include Cache-Control: no-transform in all
+    requests.  Thanks to Nick Osborn.
+  * Fix a bug in the DNS code that would cause us to ignore CNAMES
+    after a DNS timeout.
+  * Fixed a crash that occurs when a server sends a malformed
+    Cache-Control: header (CVE-2009-3305). Thanks to Stefan Fritsch.
+  * Removed support for read-only caches.  Nobody ever used it.
+  * Changed the default value of diskCacheWriteoutOnClose to 64kB.
+  * Cleaned up naming of Windows support functions. Thanks to Honglei Jiang.
+  * Added support for forbidden tunnels (thanks to Richard Zidlicky).
+  * Fixed a bug that prevented parsing of extremely long literal IPv6
+    addresses (thanks to Jan Braun).
+  * Added support for DOS-style absolute pathnames.  Thanks to Gisle Vanem.
+  * Fixed a bug that could cause infinite revalidation loops.  Thanks to
+    David Rothlisberger.
+  * Fixed a bug that could provide incorrect caching information to clients.
+    Thanks to William Manley.
+  * Support MSVC 2010.  Thanks to Greg Hazel.
+  * Work-around buggy clients that send HTTP:// in uppercase.  Thanks
+    to David Rothlisberger.
+  * Save Cache-Control values to the disk cache.  Thanks to Ruben Alleres.
+  * Allow ~ in logFile.  Thanks to P.J. Hades.
+  * Fixed a bug that could cause infinite revalidation loops with servers
+    that perform sloppy packetisation (IIS, I'm looking at you).
+  * Fail expectations on the local interface.  This might or might not be
+    what CVE-2011-3596 is about, difficult to say since nobody is speaking
+    to me.
+
+31 January 2010: Polipo 1.0.4.1:
+
+  Cherry-picked fixes from 1.0.5.  Thanks to Julien Cristau.
+
+  * Fixed an integer overflow that may lead to a crash
+    (http://secunia.com/advisories/37607/). Discovered by Jeremy Brown.
+    (CVE-2009-4413)
+  * Fixed a crash that occurs when a server sends a malformed
+    Cache-Control: header (CVE-2009-3305). Patch from Stefan Fritsch.
+  * Prevent an infinite loop when a bodyless 204 or 1xx response is encountered
+  * Don't crash when we get an error while waiting for 100 continue status.
+
+8 January 2008: Polipo 1.0.4:
+
+  * Fixed the handling of tunnels with a parent proxy (thanks to
+    Richard Šputa).
+  * Fixed a bug that could cause connections to be shut down when
+    a server used the old (RFC 2068) semantics of ``100 Continue''
+    (thanks to Thomas Wiesel).
+  * Fixed a crash when a request URL was larger than 10 kB (thanks to
+    Fabian Keil).
+  * Fixed a possible failure to read client requests larger than one
+    chunk.
+
+6 October 2007: Polipo 1.0.3
+
+  * Changed the default for chunkMemory: it is now 24 MB or
+    one-quarter of physical memory, whichever is smaller.
+  * Support for logging to syslog (thanks to Nix).
+  * Made atom reference counts 32-bit longs; this should fix
+    problems that people were seeing when running Polipo with
+    humongous in-memory caches.
+  * Added Git, Jabber and CVS to default tunnelAllowedPorts.
+  * Fixed a bug that could cause URL matching to fail when using
+    anchored regular expressions (thanks to phuel).
+
+26 August 2007: Polipo 1.0.2:
+
+  * Fixed a crash that could happen with entities more than 2GB in
+    size.  Such entities are still not supported, but Polipo
+    should no longer crash.
+  * Minor HTTP compliance fixes, due to testing with Co-Advisor.
+  * Fixed a crash that would happen when a POST request was aborted
+    by the server.  Reported by Tero Pelander.
+  * Worked around a DNS-related bug that I'm unable to track down,
+    waiting for a proper fix.
+
+25 June 2007: Polipo 1.0.1:
+
+  * Made Polipo slightly more aggressive when speaking to
+    HTTP/1.0 servers (thanks to Fabian Keil for noticing that).
+  * Fixed a crash that would happen when a client used
+    Cache-Control: only-if-cached, and the object was not in cache.
+    (Reported by F. Zappa, A. Patala and V. Ghosal.)
+  * Fixed a descriptor leak when running under Windows.
+  * Made Polipo optionally drop connections after servicing
+    a number of connections (maxConnectionAge and maxConnectionRequests).
+
+6 March 2007: Polipo 1.0.0
+
+  * No changes since 0.9.99.2.
+
+7 February 2007: Polipo 0.9.99.2
+
+  * Fixed a buffer overflow in urlDirname (the 0.9 branch is not
+    vulnerable) (reported by J. P. Larocque).
+  * Implemented use of IPv6 temporary source addresses (Frank Behrens).
+  * Disabled use of unaligned range requests by default.  This is
+    controlled by the variable allowUnalignedRangeRequests (reported
+    by Chris Moore).
+  * Fixed descriptor leaks in SOCKS error handling (reported by
+    Roger Dingledine).
+  * Implemented maxSideBuffering.
+
+6 February 2007: Polipo 0.9.12
+
+  * Fixed incorrect caching of redirects (reported by Lawrence Lu).
+  * Fixed a possible hang when falling back to gethostbyname
+    (reported by Chris Moore).
+
+28 December 2006: Polipo 0.9.99.1
+
+  * Validation improvements and bug fixes.
+  * Don't use cached data when receiving the output from an HTTP/1.0 CGI.
+  * Allowed tunnelling of IMAP and POP traffic by default.
+  * Changed the disk cache expiry and indexing functions to use chunks.
+  * Made the disk cache unreadable by others by default.
+  * Fixed a bug that could cause stale data to be served after
+    a connection failure (reported by Hondza).
+  * Fixed computation of age and rtt for pipelined requests.
+  * Fixed incorrect cachability of redirects (reported by J.-P. Larocque).
+  * Fixed a bug that would cause uncachable objects to become
+    cachable after being reloaded from the on-disk cache (reported
+    by J.-P. Larocque).
+  * Implemented dontTrustVaryETag.
+
+7 December 2006: Polipo 0.9.11
+
+  * Fixed a crash that could happen when a network interface went down
+    while a DNS query was in progress (reported by Francesco Zappa).
+
+20 November 2006: Polipo 0.9.99.0:
+
+  * Implemented large buffers for headers larger than a chunk's worth.
+  * Made the HTTP parser lax by default (ignores unknown headers).
+  * Implemented the infrastructure for local POST requests and
+    implemented a real configuration interface (thanks to Theo Honohan).
+  * Made timeouts user-configurable and enforced an invariant between
+    timeouts.
+  * Made logging configurable at runtime (thanks to Frank Behrens).
+  * Implemented the infrastructure for asynchronous handling of 
+    forbidden URLs.
+  * Implemented the ability to redirect instead of returning an
+    error for forbidden URLs.
+  * Implemented support for Squid-style redirectors.
+  * Implemented User-configurable uncacheable URLs, analogous to
+    forbidden URLs (thanks to Joachim Haga).
+  * Implemented the ability to avoid caching pages with cookies
+    and redirects.
+  * Implemented maxPipelineTrain, which can be used to moderate
+  * Polipo's eagerness to pipeline.
+  * Unified parentHost and parentPort into parentProxy.
+  * Ported Polipo to native Windows (thanks to Dan Kennedy).
+  * Implemented disableVia.
+  * Implemented SOCKS support.
+  * Made disableVia and cacheIsShared to be true by default.
+  * Increased the default value of serverMaxSlots to 8.
+  * Made the disk cache code quote all characters except for a
+    small number of ``known safe'' ones.  This is an incompatible
+    change to the on-disk format.
+  * Changed HTTP parser to pass all Pragma headers to the next
+    hop; this should make some media players work through Polipo.
+  * Changed the connection scheduler to avoid pipelining when
+    there are idle connections to a given server.
+  * Made Polipo obey parentProxy when tunnelling (proxying https).
+  * Changed the default value of allowedPorts to be slightly more
+    permissive.
+  * Implemented tweakables for selectively disabling parts of the
+    configuration interface.  Indexing and listing known servers
+    are now disabled by default.
+  * Hide variables containing passwords.
+  * Fixed a bug that could cause incorrect validation when speaking
+    to an HTTP/1.0 server.
+  * Fixed a bug that could cause incorrect validation of Vary objects.
+  * Fixed a crash in the redirector code.
+  * Made disableVia the default, and changed the default value of idleTime.
+  * Made polipo delay serving of partial objects until after a
+    a successful validation.  This should fix Mozilla's prefetching.
+  * On 64-bit platforms, made CHUNK_SIZE default to 8kB.
+
+2 September 2006: Polipo 0.9.10:
+
+  * Fixed a crash when a client closes a connection at just the
+    wrong time.
+  * Fixed a crash that could happen when a server returned incorrect
+    headers and closed the connection at just the wrong time.
+  * Fixed restarting of connections on a server-side read error;
+    this should avoid the ``connection reset by peer'' problem.
+  * Corrected work-around for DNS servers that export both AAAA and CNAME.
+  * Fix incorrect error handling when overflowing the buffer when saving
+    an entity to disk.
+  * IPv6 tweaks for OpenBSD (thanks to Jun-ichiro itojun Hagino).
+  * Fixed incorrect error-handling on failure to parse a date.
+  * Fixed a deadlock when a tunnel is shut down and the buffer is
+    full.
+  * Fixed an incorrect use of va_start (guaranteed crash on AMD64).
+  * Fixed a possible race condition with a heavily pipelining client.
+  * Fixed a crash due to incorrect handling of write errors in POST.
+
+23 September 2005: Polipo 0.9.9:
+
+  * Fixed a bug that could cause objects to be incorrectly
+    determined to be dynamic (thanks to Joachim B. Haga).
+  * Fixed a bug that could cause the local web server to expose
+    files that are not under the local root (thanks to Wessel
+    Dankers).
+  * Fixed an off-by-one bug when parsing NL-terminated headers.
+  * Made Polipo forget about failures when finishing on the client side.
+  * Polipo now sends Host headers even when speaking to an upstream
+    proxy.  Some proxies take RFC 2616 literally, and require that
+    (thanks to Zoltan Ivanfi).
+  * Fixed some bugs in staleness computation, and implemented
+    server-side max-age directives (oops!) (thanks to Charley Chu).
+
+24 January 2005: Polipo 0.9.8:
+
+  * Backported the lax HTTP parser from the head branch.
+  * Fixed a race condition that could cause a crash if a single
+    object was being superseded twice at the same time.
+  * Fixed an incorrect test that caused Polipo to pipeline to all
+    HTTP/1.1 servers, even when they were determined as broken (thanks
+    to Daniel Koukola).
+  * Implemented maxPipelineTrain.
+  * Tweaked for uclibc (thanks to Detlef Riekenberg).
+
+27 December 2004: Polipo 0.9.7:
+
+  * Fixed a possible crash when tunnelling.
+  * Fixed spurious updates of object sizes when connection is dropped
+    by the client.
+  * Fixed parsing of URLs with explicit port number (thanks to
+    Frank Behrens).
+  * Fixed a possible crash when exiting POST in error.
+  * Fixed a protocol violation when an empty object is not superseded.
+
+31 October 2004: Polipo 0.9.6:
+  * Fixed a possible crash in ServeObject.
+  * Fixed two possible crashes when relaxTransparency is not false.
+  * Modified the config file parser to make it possible to have
+    backslashes in atoms.
+  * Fixed a violated invariant (leading to a crash) when superseding
+    objects.
+  * Fixed a possible crash in ClientFinish when a pipelined request
+    carries no object.
+  * Fixed a bug in handling of client-side Expect: 100-continue
+    (reported by Charley Chu).
+  * Fixed a scheduling bug that caused server-side requests to be
+    issued in the order opposite to a client-side pipeline (many
+    thanks to Joachim Haga).
+  * Abort when the config file couldn't be parsed (thanks to
+    Joachim Haga).
+  * Fixed error handling in POST and PUT requests, which could
+    cause a crash on an I/O error.
+
+17 June 2004: Polipo 0.9.5:
+  * Implemented upstream proxy authentication (HTTP Basic only).
+  * Fixed a possible crash when unable to schedule servicing a request.
+  * Fixed a possible crash when tunnelling (proxying https).
+  * Fixed signedness issues to make allowedClients work on PPC
+    (from Gergely Nagy).
+
+10 May 2004: Polipo 0.9.4:
+  * Fixed a bug (introduced in 0.9.3) that could cause a crash when
+    the download of an object was interrupted and then immediately restarted.
+  * Fixed a bug that could cause stale non-200 replies to be served.
+  * Fixed compilation on FreeBSD 5.2.1 (from Samuel Tardieu).
+  * Fixed definition of *_ROOT in diskcache.c
+
+6 April 2004: Polipo 0.9.3:
+  * Fix incorrect handling of EPIPE when tunnelling; this could cause
+    crashes if a peer closed a connection when we're writing.
+  * Fix a race condition that could cause ``error message lost in transit''
+    errors if a request was cancelled during connect.
+  * Check for exitFlag in workToDo: faster reaction to signals.
+
+28 March 2004: Polipo 0.9.2:
+  * Fixed a bug that could cause crashes when writing out small
+    objects (thanks to Frank Behrens).
+  * Made buffer allocation in httpParseHeaders dynamic.
+  * Fixed the declaration of pipelineAdditionalRequests.
+  * Fixed a bug that could cause empty directories to be missed
+    when expiring the disk cache.
+  * Switched the forbidden file to use extended regexps, the
+    previous usage was non-portable (thanks to Frank Behrens).
+
+9 March 2004: Polipo 0.9.1:
+  * Fixed a bug that could cause chunked encoding failures when
+    going from a 1.0 server to a 1.1 client.
+  * Fixed a bug that prevented comments after some config lines
+    (thanks to Tom Huckstep).
+  * Fixed a possible buffer overflow in dnsDecodeReply.
+  * Fixed portability to systems where rmdir returns EEXIST
+    instead of ENOTEMPTY.
+  * Fixed error handling on fork failures in fillSpecialObject.
+  * Fixed handling of EINTR in wait in specialRequestHandler.
+  * Fixed a bug that caused objects with no headers to fail.
+  * Fixed a minor memory leak in the config file parser.
+  * Minor build fixes for NetBSD.
+  * Added the 68020 and later to the list of architectures that
+    support unaligned access.
+
+18 February 2004: Polipo 0.9:
+  * Reworked the DNS code to parse ids even when a reply's qdcount
+    is 0.  No longer falls back to gethostbyname when DNS server
+    returns FormErr.
+  * Made the DNS code parse resolv.conf.
+
+11 Feburary 2004: Polipo 0.8.99.3:
+  * Minor changes to work around Cygwin mis-design.
+  * Fixed printing of n-state variables.
+  * Fixed proxyOffline handling.
+  * Fixed a bug that would cause errors to be reported with the
+    wrong content-type.
+  * Fixed a bug that would cause ``object vanished'' errors when
+    using HEAD for revalidation.
+  * Fixed a bug that could cause failed requests due to ``client
+    reset connection'' errors.
+
+24 January 2004: Polipo 0.8.99.2:
+  * Cleaned up authentication.
+  * Made authenticated replies cachable in one of the cases allowed
+    by RFC 2616.
+  * Fixed a bug that could, under some circumstances, cause a
+    password-protected object to be cached and returned to a
+    non-authorized client.
+  * Implemented 100-continue, controlled by the variable expectContinue.
+  * Implemented tristate, 4- and 5-state variables.  Split
+    proxyOffline into proxyOffline and relaxTransparency.  This is
+    an incompatible change to the config file format.
+  * Cleaned up the handling of allowed port ranges.  New
+    configuration variable allowedPorts (and new type intlist).
+  * Implemented tunnelling through the CONNECT method (https proxying).
+  * Will now read a request body on error (avoids a deadlock).
+  * Reworked the PUT/POST code to read the reply eagerly rather
+    than waiting for the write to finish (avoids writing the full
+    body on error and avoids the same deadlock as above).
+  * Made server addresses sticky: will now remember which of a
+    servers addresses worked last time, and use that address first.
+
+16 january 2004: Polipo 0.8.99.1:
+  * Fixed an expiry bug that caused DNS queries to be repeated on
+    each request.
+  * Added the variable ``preciseExpiry'' that prevents trusting
+    the mtime during expiry.
+
+14 January 2004: Polipo 0.8.99.0:
+  * Implemented IP address-based authentication.
+  * Implemented HTTP ``basic'' authentication.
+  * Implemented variable body offsets for the on-disk cache; this
+    makes the on-disk cache format incompatible with previous versions.
+  * Made the number of server slots configurable.
+  * Partially rewrote the disk cache code.
+  * Fixed a file descriptor leak in the early error handling code.
+  * Fixed a bug in the base64 encoder; this makes the on-disk cache
+    format incompatible with previous versions.
+  * Implemented proper reporting for 100-Continue replies
+    (100-Continue is not implemented yet).
+  * Made the number of server slots configurable at runtime.
+
+9 January 2004: Polipo 0.8.4:
+  * Log file is now line buffered.
+  * Will reopen the log file on SIGUSR1 and SIGUSR2.
+  * censoredHeaders now defaults to none, and censorReferer to 0.
+  * Fixed a memory allocation bug that could cause a crash.
+
+21 December 2003: Polipo 0.8.3:
+  * Fixed a potential buffer overflow on C89 systems in snnprintf.
+  * Fixed checking of Via headers.
+  * Added configurable log file.
+  * Added code to run as a daemon.
+  * Made the resolver grok names ending in a ``.''.
+  * Changed Makefile to fit Debian better.
+
+7 December 2003: Polipo 0.8.2:
+  * Implemented a version of fts for SVR4 systems.
+  * Implemented a version of mktime_gmt that doesn't use setenv.
+  * Fixed code used to determine FQDN.
+  * More unaligned access fixes.
+  * Disabled queryIPv6 when there is no IPv6 support in kernel.
+  * Enabled IPv6 support by default on FreeBSD and NetBSD.
+
+2 December 2003: Polipo 0.8.1:
+
+  * Fix a possible crash when doing a POST on a busy server.
+  * Fix a possible crash when socket(2) fails; implement switching
+    to a different address when socket(2) fails (e.g. when
+    accessing a double-stack host from an IPv4-only client).
+  * Fix a problem with servers stuck in the ``probing'' state.
+  * Work around a bug in Konqueror that strips question marks from
+    URLs.
+  * Fix incorrect error handling when dealing with connection
+    failures.
+  * Fix a compile problem in dns.c.
+  * Remove dependency on SSL, include MD5 code instead.
+  * Fix signedness of s_maxage.
+
+23 November 2003: Polipo 0.8:
+
+  * IPv6 support, on both the client and server side, including
+    DNS support and RFC 2732.
+  * Reworked the DNS code.
+  * Made it possible to compile without any particular resolver
+    and without the on-disk cache.
+  * Fixed a problem with the chunking encoder.
+  * Made the config file parser grok octal and hex values, allowed
+    colons and tildes in unquoted strings.
+  * Implemented tilde expansion in some config variables.
+  * Made Polipo slightly less eager to honour range requests for
+    dynamic instances.  Marked generated objects as dynamic. These
+    changes should solve some of the problems with PMM.
+  * Implemented the If-Range header (on both the client and server side).
+  * Implemented support for range requests smaller than one chunk
+    (and hence for pmmSize being smaller than CHUNK_SIZE).
+  * Fixed a bug that caused a deadlock (until a server timeout)
+    when doing a POST/PUT request with no free connection slots.
+  * Fixed a problem when diskCacheRoot didn't end in `/'.
+  * Fixed a refcounting problem that could cause Polipo to crash
+    on a DNS timeout.
+  * Fixed an alignment problem that could cause crashes on 
+    architectures that don't like unaligned memory accesses
+    (thanks to Rob Byrnes).
+  * Fixed a bug with the disk cache that caused spurious ``disk
+    entry changed behind our back'' warnings (and in principle
+    could cause data corruption, although that's very unlikely).
+  * Made opening connections slightly less aggressive -- Polipo
+    would sometimes open two connections where only one was needed.
+  * Modified client-side code to notice client shutdowns earlier
+    and notify the server side straight away.
+
+7 October 2003: Polipo 0.7
+
+  * Made the Request function a method of objects.
+  * Fixed a bug that could sometimes cause data corruption.
+  * Fixed a bug that could cause sending of incorrect data to
+    the client on a range request.
+  * Fixed POST and PUT requests.
+  * Fixed a bug that could sometimes cause a POST request to use a
+    stale connection.
+  * Included code to do poor man's multiplexing.
+  * Will now open multiple connections to non-persistent servers.
+  * Fixed a bug that could lead to idle connections dying without
+    being noticed.
+  * Fixed probing for pipelining.
+  * Actually use the new time function introduced in 0.5.
+  * Fixed a bug in strcasecmp_n.
+  * forbiddenFile can now be a directory.
+
+26 September 2003: Polipo 0.6
+
+  * Fixed precondition handling.
+  * Fixed a bug that could lead to lockups when revalidating an object.
+
+27 June 2003: Polipo 0.5
+
+  * Made the presence of a reader or writer explicit on the client side.
+  * Reworked closing client connections.
+  * Reworked reporting of server-side errors.
+  * Made buffer allocation lazy; idle clients and servers no longer
+    use up buffers.
+  * Reworked UTC time handling to use timegm(3) when available.
+
+12 March 2003: Polipo 0.4
+
+  * Implemented expiry of the on-disk cache.
+  * Implemented reliable aborting of connections; Polipo should no
+  * longer deadlock when a server falls into a black hole.
+  * Changed direct reads to be more aggressive by using readv in
+    three pieces.
+  * Changed serving of chunked data to be more eager about
+    serving a chunk's end marker.
+  * Implemented better reporting of DNS errors.
+  * Fixed a deadlock with pipelining on the client side.
+  * Removed most of the remaining copies when reading on the
+  * server side.
+  * Fixed a bug that caused some headers to disappear in transit.
+  * Fixed a possible livelock when reading chunked encoding.
+  * Fixed an overflow when pipelining on the server side.
+  * Fixed generation of indexes from the on-disk cache.
+  * Fixed a DNS crash when falling back on gethostbyname.
+
+1 March 2003: Polipo 0.3
+
+  * Implemented retrying of timed-out DNS requests.
+  * Implemented configuration mechanisms for case-insensitive atoms,
+    time values and atom lists; censoredHeaders can now be configured.
+  * No longer censors User-Agent.  Blame Beppe and Alain.
+  * Changed the handling of hop-by-hop HTTP headers to deal with multiple
+    Connection headers.
+  * Made client-side errors and successful revalidation no longer
+    close the connection.
+  * Fixed a bug that caused the allocation of an extraneous 2MB (!)
+    at startup.  Polipo can run in 100KB once again.
+  * Fixed a refcounting bug and some incorrect frees that could
+    lead to crashes when recovering from a server-side error.
+  * Fixed a bug with POST/PUT that could trigger a failed assertion.
+  * Made sure that POST/PUT don't get scheduled in multiple pieces.
+
+17 February 2003: Polipo 0.2
+
+  * Fixed an incorrect assertion that could cause crashes when the
+    server is fast.
+  * Fixed (hopefully) logic for 304 replies.
+  * Minor tweaks to scheduling that cause some speed increase when the
+    client is pipelining and the server is fast.
+  * Minor bug fixes and cleanups.
+  * Macro-ified do_log and friends.
+
+3 February 2003: Polipo 0.1
+
+  * Initial public release.

+ 19 - 0
polipo/COPYING

@@ -0,0 +1,19 @@
+Copyright (c) 2003-2008 by Juliusz Chroboczek
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.

+ 72 - 0
polipo/INSTALL

@@ -0,0 +1,72 @@
+Polipo installation instructions
+================================
+
+1. Building and running polipo
+------------------------------
+
+    $ make all
+    $ su -c 'make install'
+    $ man polipo
+    $ polipo &
+
+If you want Polipo to put itself into the background, you may replace
+the last line with:
+
+    $ polipo daemonise=true logFile="/var/log/polipo.log"
+
+On SVR4 systems (Solaris, HP/UX), you will need to use one of the
+following (whichever works):
+
+    $ make PLATFORM_DEFINES=-DSVR4 all
+    $ make PLATFORM_DEFINES=-DSVR4 LDLIBS='-lsocket -lnsl -lresolv' all
+
+You can also use Polipo without installing:
+
+    $ make
+    $ nroff -man polipo.man | more
+    $ ./polipo &
+
+For information about building on Windows, please see the file README.Windows.
+
+2. Configuring your user-agent
+------------------------------
+
+Once polipo is running, configure your user-agent (web browser) to use
+the proxy on `http://localhost:8123/'.  Depending on the user-agent,
+this is done either by setting the environment variable http_proxy,
+e.g.
+
+    $ http_proxy=http://localhost:8123; export http_proxy
+
+or by using the browser's ``preferences'' menu.
+
+3. Configuring polipo
+---------------------
+
+If you want to use an on-disk cache, you will need to create its root
+directory:
+
+    $ mkdir /var/cache/polipo/
+
+You should then arrange for cron to run the following on a regular
+basis:
+
+    killall -USR1 polipo
+    sleep 1
+    polipo -x
+    killall -USR2 polipo
+
+If you want to use a configuration file, you should put it in one of
+the locations `/etc/polipo/config' or `~/.polipo'; you can also use
+the `-c' flag to put it in a non-standard location.  See the file
+`config.sample' for an example.
+
+You might also want to create a forbidden URLs file, which you should
+put either in one of `/etc/polipo/forbidden' or `~/.polipo-forbidden';
+you can set the variable `forbiddenFile' in your config file if you
+want to put it in a non-standard location.  See `forbidden.sample' for
+an example.
+
+
+Juliusz Chroboczek
+<jch@pps.univ-paris-diderot.fr>

+ 142 - 0
polipo/Makefile

@@ -0,0 +1,142 @@
+PREFIX = /usr/local
+BINDIR = $(PREFIX)/bin
+MANDIR = $(PREFIX)/man
+INFODIR = $(PREFIX)/info
+LOCAL_ROOT = /usr/share/polipo/www
+DISK_CACHE_ROOT = /var/cache/polipo
+
+# To compile with Unix CC:
+
+# CDEBUGFLAGS=-O
+
+# To compile with GCC:
+
+# CC = gcc
+CDEBUGFLAGS = -Os -g -Wall -fno-strict-aliasing
+
+# To compile on a pure POSIX system:
+
+# CC = c89
+# CC = c99
+# CDEBUGFLAGS=-O
+
+# To compile with icc 7, you need -restrict.  (Their bug.)
+
+# CC=icc
+# CDEBUGFLAGS = -O -restrict
+
+# On System V (Solaris, HP/UX) you need the following:
+
+# PLATFORM_DEFINES = -DSVR4
+
+# On Solaris, you need the following:
+
+# LDLIBS = -lsocket -lnsl -lresolv
+
+# On mingw, you need
+
+# EXE=.exe
+# LDLIBS = -lws2_32
+
+FILE_DEFINES = -DLOCAL_ROOT=\"$(LOCAL_ROOT)/\" \
+               -DDISK_CACHE_ROOT=\"$(DISK_CACHE_ROOT)/\"
+
+# You may optionally also add any of the following to DEFINES:
+#
+#  -DNO_DISK_CACHE to compile out the on-disk cache and local web server;
+#  -DNO_IPv6 to avoid using the RFC 3493 API and stick to stock
+#      Berkeley sockets;
+#  -DHAVE_IPv6 to force the use of the RFC 3493 API on systems other
+#      than GNU/Linux and BSD (let me know if it works);
+#  -DNO_FANCY_RESOLVER to compile out the asynchronous name resolution
+#      code;
+#  -DNO_STANDARD_RESOLVER to compile out the code that falls back to
+#      gethostbyname/getaddrinfo when DNS requests fail;
+#  -DNO_TUNNEL to compile out the code that handles CONNECT requests;
+#  -DNO_SOCKS to compile out the SOCKS gateway code.
+#  -DNO_FORBIDDEN to compile out the all of the forbidden URL code
+#  -DNO_REDIRECTOR to compile out the Squid-style redirector code
+#  -DNO_SYSLOG to compile out logging to syslog
+
+DEFINES = $(FILE_DEFINES) $(PLATFORM_DEFINES)
+
+CFLAGS = $(MD5INCLUDES) $(CDEBUGFLAGS) $(DEFINES) $(EXTRA_DEFINES)
+
+SRCS = util.c event.c io.c chunk.c atom.c object.c log.c diskcache.c main.c \
+       config.c local.c http.c client.c server.c auth.c tunnel.c \
+       http_parse.c parse_time.c dns.c forbidden.c \
+       md5import.c md5.c ftsimport.c fts_compat.c socks.c mingw.c
+
+OBJS = util.o event.o io.o chunk.o atom.o object.o log.o diskcache.o main.o \
+       config.o local.o http.o client.o server.o auth.o tunnel.o \
+       http_parse.o parse_time.o dns.o forbidden.o \
+       md5import.o ftsimport.o socks.o mingw.o
+
+polipo$(EXE): $(OBJS)
+	$(CC) $(CFLAGS) $(LDFLAGS) -o polipo$(EXE) $(OBJS) $(MD5LIBS) $(LDLIBS)
+
+ftsimport.o: ftsimport.c fts_compat.c
+
+md5import.o: md5import.c md5.c
+
+.PHONY: all install install.binary install.man
+
+all: polipo$(EXE) polipo.info html/index.html localindex.html
+
+install: install.binary install.man
+
+install.binary: all
+	mkdir -p $(TARGET)$(BINDIR)
+	mkdir -p $(TARGET)$(LOCAL_ROOT)
+	mkdir -p $(TARGET)$(LOCAL_ROOT)/doc
+	rm -f $(TARGET)$(BINDIR)/polipo
+	cp -f polipo $(TARGET)$(BINDIR)/
+	cp -f html/* $(TARGET)$(LOCAL_ROOT)/doc
+	cp -f localindex.html $(TARGET)$(LOCAL_ROOT)/index.html
+
+install.man: all
+	mkdir -p $(TARGET)$(MANDIR)/man1
+	mkdir -p $(TARGET)$(INFODIR)
+	cp -f polipo.man $(TARGET)$(MANDIR)/man1/polipo.1
+	cp polipo.info $(TARGET)$(INFODIR)/
+	install-info --info-dir=$(TARGET)$(INFODIR) polipo.info
+
+
+polipo.info: polipo.texi
+	makeinfo polipo.texi
+
+html/index.html: polipo.texi
+	mkdir -p html
+	makeinfo --html -o html polipo.texi
+
+polipo.html: polipo.texi
+	makeinfo --html --no-split --no-headers -o polipo.html polipo.texi
+
+polipo.pdf: polipo.texi
+	texi2pdf polipo.texi
+
+polipo.ps.gz: polipo.ps
+	gzip -c polipo.ps > polipo.ps.gz
+
+polipo.ps: polipo.dvi
+	dvips -Pwww -o polipo.ps polipo.dvi
+
+polipo.dvi: polipo.texi
+	texi2dvi polipo.texi
+
+polipo.man.html: polipo.man
+	rman -f html polipo.man > polipo.man.html
+
+TAGS: $(SRCS)
+	etags $(SRCS)
+
+.PHONY: clean
+
+clean:
+	-rm -f polipo$(EXE) *.o *~ core TAGS gmon.out
+	-rm -f polipo.cp polipo.fn polipo.log polipo.vr
+	-rm -f polipo.cps polipo.info* polipo.pg polipo.toc polipo.vrs
+	-rm -f polipo.aux polipo.dvi polipo.ky polipo.ps polipo.tp
+	-rm -f polipo.dvi polipo.ps polipo.ps.gz polipo.pdf polipo.html
+	-rm -rf ./html/
+	-rm -f polipo.man.html

+ 27 - 0
polipo/README

@@ -0,0 +1,27 @@
+Polipo is no longer maintained
+==============================
+
+Polipo is no longer maintained.  Sorry.
+
+
+Original Polipo README
+======================
+
+Polipo is single-threaded, non blocking caching web proxy that has
+very modest resource needs.  See the file INSTALL for installation
+instructions.  See the texinfo manual (available as HTML after
+installation) for more information.
+
+Current information about Polipo can be found on the Polipo web page,
+
+    http://www.pps.univ-paris-diderot.fr/~jch/software/polipo/
+
+Further inquiries should be sent to the Polipo-users mailing list:
+
+    <polipo-users@lists.sourceforge.net>
+
+Please see the Polipo web page for subscription information.
+
+
+Juliusz Chroboczek
+<jch@pps.univ-paris-diderot.fr>

+ 38 - 0
polipo/README.Windows

@@ -0,0 +1,38 @@
+Building Polipo on Windows
+==========================
+
+There are two distinct ports of Polipo to Windows -- a port using the
+Cygwin emulation libraries, and an experimental native port using Mingw.
+
+The Cygwin port is identical to the Unix binary.  Build it just like
+you would build under Unix -- just type ``make all'' in the directory
+where you untarred the Polipo sources.
+
+In order to build the native port, cd to the Polipo directory, and do
+
+    make EXE=.exe LDLIBS=-lwsock32
+
+or, if you've got a regex library,
+
+    make EXE=.exe EXTRA_DEFINES=-DHAVE_REGEX LDLIBS="-lwsock32 -lregex"
+
+In order to cross-compile from a Unix system, you will probably need
+to point make at the right compiler:
+
+    make EXE=.exe CC=i586-mingw32msvc-gcc LDLIBS=-lwsock32
+
+The native port currently attempts to access files in locations that
+are typical for a Unix system; for example, it will attempt to read a
+configuration file /etc/polipo/config on the current drive.  You will
+probably need to point it at your config file with an explicit ``-c''
+command-line argument, and define at least the following configuration
+variables:
+
+    dnsNameServer
+    diskCacheRoot
+    forbiddenFile
+
+Help with solving this issue would be very much appreciated.
+
+
+Juliusz Chroboczek

+ 379 - 0
polipo/atom.c

@@ -0,0 +1,379 @@
+/*
+Copyright (c) 2003-2006 by Juliusz Chroboczek
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+#include "polipo.h"
+
+/* Atoms are interned, read-only reference-counted strings.
+
+   Interned means that equality of atoms is equivalent to structural
+   equality -- you don't need to strcmp, you just compare the AtomPtrs.
+   This property is used throughout Polipo, e.g. to speed up the HTTP
+   parser.
+
+   Polipo's atoms may contain NUL bytes -- you can use internAtomN to
+   store any random binary data within an atom.  However, Polipo always
+   terminates your data, so if you store textual data in an atom, you
+   may use the result of atomString as though it were a (read-only)
+   C string.
+
+*/
+
+static AtomPtr *atomHashTable;
+int used_atoms;
+
+void
+initAtoms()
+{
+    atomHashTable = calloc((1 << LOG2_ATOM_HASH_TABLE_SIZE),
+                           sizeof(AtomPtr));
+
+    if(atomHashTable == NULL) {
+        do_log(L_ERROR, "Couldn't allocate atom hash table.\n");
+        exit(1);
+    }
+    used_atoms = 0;
+}
+
+AtomPtr
+internAtomN(const char *string, int n)
+{
+    AtomPtr atom;
+    int h;
+
+    if(n < 0 || n >= (1 << (8 * sizeof(unsigned short))))
+        return NULL;
+
+    h = hash(0, string, n, LOG2_ATOM_HASH_TABLE_SIZE);
+    atom = atomHashTable[h];
+    while(atom) {
+        if(atom->length == n &&
+           (n == 0 || memcmp(atom->string, string, n) == 0))
+            break;
+        atom = atom->next;
+    }
+
+    if(!atom) {
+        atom = malloc(sizeof(AtomRec) - 1 + n + 1);
+        if(atom == NULL) {
+            return NULL;
+        }
+        atom->refcount = 0;
+        atom->length = n;
+        /* Atoms are used both for binary data and strings.  To make
+           their use as strings more convenient, atoms are always
+           NUL-terminated. */
+        memcpy(atom->string, string, n);
+        atom->string[n] = '\0';
+        atom->next = atomHashTable[h];
+        atomHashTable[h] = atom;
+        used_atoms++;
+    }
+    do_log(D_ATOM_REFCOUNT, "A 0x%lx %d++\n",
+           (unsigned long)atom, atom->refcount);
+    atom->refcount++;
+    return atom;
+}
+
+AtomPtr
+internAtom(const char *string)
+{
+    return internAtomN(string, strlen(string));
+}
+
+AtomPtr
+atomCat(AtomPtr atom, const char *string)
+{
+    char buf[128];
+    char *s = buf;
+    AtomPtr newAtom;
+    int n = strlen(string);
+    if(atom->length + n > 128) {
+        s = malloc(atom->length + n + 1);
+        if(s == NULL)
+            return NULL;
+    }
+    memcpy(s, atom->string, atom->length);
+    memcpy(s + atom->length, string, n);
+    newAtom = internAtomN(s, atom->length + n);
+    if(s != buf) free(s);
+    return newAtom;
+}
+
+int
+atomSplit(AtomPtr atom, char c, AtomPtr *return1, AtomPtr *return2)
+{
+    char *p;
+    AtomPtr atom1, atom2;
+    p = memchr(atom->string, c, atom->length);
+    if(p == NULL)
+        return 0;
+    atom1 = internAtomN(atom->string, p - atom->string);
+    if(atom1 == NULL)
+        return -ENOMEM;
+    atom2 = internAtomN(p + 1, atom->length - (p + 1 - atom->string));
+    if(atom2 == NULL) {
+        releaseAtom(atom1);
+        return -ENOMEM;
+    }
+    *return1 = atom1;
+    *return2 = atom2;
+    return 1;
+}
+
+AtomPtr
+internAtomLowerN(const char *string, int n)
+{
+    char *s;
+    char buf[100];
+    AtomPtr atom;
+
+    if(n < 0 || n >= 50000)
+        return NULL;
+
+    if(n < 100) {
+        s = buf;
+    } else {
+        s = malloc(n);
+        if(s == NULL)
+            return NULL;
+    }
+
+    lwrcpy(s, string, n);
+    atom = internAtomN(s, n);
+    if(s != buf) free(s);
+    return atom;
+}
+
+AtomPtr
+retainAtom(AtomPtr atom)
+{
+    if(atom == NULL)
+        return NULL;
+
+    do_log(D_ATOM_REFCOUNT, "A 0x%lx %d++\n",
+           (unsigned long)atom, atom->refcount);
+    assert(atom->refcount >= 1 && atom->refcount < LARGE_ATOM_REFCOUNT);
+    atom->refcount++;
+    return atom;
+}
+
+void
+releaseAtom(AtomPtr atom)
+{
+    if(atom == NULL)
+        return;
+
+    do_log(D_ATOM_REFCOUNT, "A 0x%lx %d--\n",
+           (unsigned long)atom, atom->refcount);
+    assert(atom->refcount >= 1 && atom->refcount < LARGE_ATOM_REFCOUNT);
+
+    atom->refcount--;
+
+    if(atom->refcount == 0) {
+        int h = hash(0, atom->string, atom->length, LOG2_ATOM_HASH_TABLE_SIZE);
+        assert(atomHashTable[h] != NULL);
+
+        if(atom == atomHashTable[h]) {
+            atomHashTable[h] = atom->next;
+            free(atom);
+        } else {
+            AtomPtr previous = atomHashTable[h];
+            while(previous->next) {
+                if(previous->next == atom)
+                    break;
+                previous = previous->next;
+            }
+            assert(previous->next != NULL);
+            previous->next = atom->next;
+            free(atom);
+        }
+        used_atoms--;
+    }
+}
+
+AtomPtr
+internAtomF(const char *format, ...)
+{
+    char *s;
+    char buf[150];
+    int n;
+    va_list args;
+    AtomPtr atom = NULL;
+
+    va_start(args, format);
+    n = vsnprintf(buf, 150, format, args);
+    va_end(args);
+    if(n >= 0 && n < 150) {
+        atom = internAtomN(buf, n);
+    } else {
+        va_start(args, format);
+        s = vsprintf_a(format, args);
+        va_end(args);
+        if(s != NULL) {
+            atom = internAtom(s);
+            free(s);
+        }
+    }
+    return atom;
+}
+
+static AtomPtr
+internAtomErrorV(int e, const char *f, va_list args)
+{
+    
+    char *es = pstrerror(e);
+    AtomPtr atom;
+    char *s1, *s2;
+    int n, rc;
+    va_list args_copy;
+
+    if(f) {
+        va_copy(args_copy, args);
+        s1 = vsprintf_a(f, args_copy);
+        va_end(args_copy);
+        if(s1 == NULL)
+            return NULL;
+        n = strlen(s1);
+    } else {
+        s1 = NULL;
+        n = 0;
+    }
+
+    s2 = malloc(n + 70);
+    if(s2 == NULL) {
+        free(s1);
+        return NULL;
+    }
+    if(s1) {
+        strcpy(s2, s1);
+        free(s1);
+    }
+
+    rc = snprintf(s2 + n, 69, f ? ": %s" : "%s", es);
+    if(rc < 0 || rc >= 69) {
+        free(s2);
+        return NULL;
+    }
+
+    atom = internAtomN(s2, n + rc);
+    free(s2);
+    return atom;
+}
+
+AtomPtr
+internAtomError(int e, const char *f, ...)
+{
+    AtomPtr atom;
+    va_list args;
+    va_start(args, f);
+    atom = internAtomErrorV(e, f, args);
+    va_end(args);
+    return atom;
+}
+
+char *
+atomString(AtomPtr atom)
+{
+    if(atom)
+        return atom->string;
+    else
+        return "(null)";
+}
+
+AtomListPtr
+makeAtomList(AtomPtr *atoms, int n)
+{
+    AtomListPtr list;
+    list = malloc(sizeof(AtomListRec));
+    if(list == NULL) return NULL;
+    list->length = 0;
+    list->size = 0;
+    list->list = NULL;
+    if(n > 0) {
+        int i;
+        list->list = malloc(n * sizeof(AtomPtr));
+        if(list->list == NULL) {
+            free(list);
+            return NULL;
+        }
+        list->size = n;
+        for(i = 0; i < n; i++)
+            list->list[i] = atoms[i];
+        list->length = n;
+    }
+    return list;
+}
+
+void
+destroyAtomList(AtomListPtr list)
+{
+    int i;
+    if(list->list) {
+        for(i = 0; i < list->length; i++)
+            releaseAtom(list->list[i]);
+        list->length = 0;
+        free(list->list);
+        list->list = NULL;
+        list->size = 0;
+    }
+    assert(list->size == 0);
+    free(list);
+}
+
+int
+atomListMember(AtomPtr atom, AtomListPtr list)
+{
+    int i;
+    for(i = 0; i < list->length; i++) {
+        if(atom == list->list[i])
+            return 1;
+    }
+    return 0;
+}
+
+void
+atomListCons(AtomPtr atom, AtomListPtr list)
+{
+    if(list->list == NULL) {
+        assert(list->size == 0);
+        list->list = malloc(5 * sizeof(AtomPtr));
+        if(list->list == NULL) {
+            do_log(L_ERROR, "Couldn't allocate AtomList\n");
+            return;
+        }
+        list->size = 5;
+    }
+    if(list->size <= list->length) {
+        AtomPtr *new_list;
+        int n = (2 * list->length + 1);
+        new_list = realloc(list->list, n * sizeof(AtomPtr));
+        if(new_list == NULL) {
+            do_log(L_ERROR, "Couldn't realloc AtomList\n");
+            return;
+        }
+        list->list = new_list;
+        list->size = n;
+    }
+    list->list[list->length] = atom;
+    list->length++;
+}
+

+ 58 - 0
polipo/atom.h

@@ -0,0 +1,58 @@
+/*
+Copyright (c) 2003-2006 by Juliusz Chroboczek
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+typedef struct _Atom {
+    unsigned int refcount;
+    struct _Atom *next;
+    unsigned short length;
+    char string[1];
+} AtomRec, *AtomPtr;
+
+typedef struct _AtomList {
+    int length;
+    int size;
+    AtomPtr *list;
+} AtomListRec, *AtomListPtr;
+
+#define LOG2_ATOM_HASH_TABLE_SIZE 10
+#define LARGE_ATOM_REFCOUNT 0xFFFFFF00U
+
+extern int used_atoms;
+
+void initAtoms(void);
+AtomPtr internAtom(const char *string);
+AtomPtr internAtomN(const char *string, int n);
+AtomPtr internAtomLowerN(const char *string, int n);
+AtomPtr atomCat(AtomPtr atom, const char *string);
+int atomSplit(AtomPtr atom, char c, AtomPtr *return1, AtomPtr *return2);
+AtomPtr retainAtom(AtomPtr atom);
+void releaseAtom(AtomPtr atom);
+AtomPtr internAtomError(int e, const char *f, ...)
+     ATTRIBUTE ((format (printf, 2, 3)));
+AtomPtr internAtomF(const char *format, ...)
+     ATTRIBUTE ((format (printf, 1, 2)));
+char *atomString(AtomPtr) ATTRIBUTE ((pure));
+AtomListPtr makeAtomList(AtomPtr *atoms, int n);
+void destroyAtomList(AtomListPtr list);
+int atomListMember(AtomPtr atom, AtomListPtr list)
+    ATTRIBUTE ((pure));
+void atomListCons(AtomPtr atom, AtomListPtr list);

+ 91 - 0
polipo/auth.c

@@ -0,0 +1,91 @@
+/*
+Copyright (c) 2004-2006 by Juliusz Chroboczek
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+#include "polipo.h"
+
+int
+buildClientAuthHeaders(AtomPtr url, char *word,
+                       AtomPtr *message_return, AtomPtr *headers_return)
+{
+    int code;
+    char *h;
+    AtomPtr message, headers;
+    if(urlIsLocal(url->string, url->length)) {
+        code = 401;
+        message = internAtomF("Server authentication %s", word);
+        h = "WWW-Authenticate";
+    } else {
+        code = 407;
+        message = internAtomF("Proxy authentication %s", word);
+        h = "Proxy-Authenticate";
+    }
+    headers = internAtomF("\r\n%s: Basic realm=\"%s\"",
+                          h, authRealm->string);
+    if(message_return)
+        *message_return = message;
+    else
+        releaseAtom(message);
+    *headers_return = headers;
+    return code;
+}
+
+int
+checkClientAuth(AtomPtr auth, AtomPtr url,
+                AtomPtr *message_return, AtomPtr *headers_return)
+{
+    int code = 0;
+    AtomPtr message = NULL, headers = NULL;
+
+    if(authRealm == NULL || authCredentials == NULL)
+        return 0;
+
+    if(auth == NULL)
+        code = buildClientAuthHeaders(url, "required", &message, &headers);
+    else if(auth->length >= 6 || lwrcmp(auth->string, "basic ", 6) == 0) {
+        if(b64cmp(auth->string + 6, auth->length - 6,
+                  authCredentials->string, authCredentials->length) == 0)
+            return 0;
+        code = buildClientAuthHeaders(url, "incorrect", &message, &headers);
+    } else {
+        code = buildClientAuthHeaders(url, NULL, NULL, &headers);
+        message = internAtom("Unexpected authentication scheme");
+    }
+
+    *message_return = message;
+    *headers_return = headers;
+    return code;
+}
+
+int
+buildServerAuthHeaders(char* buf, int n, int size, AtomPtr authCredentials)
+{
+    char authbuf[4 * 128 + 3];
+    int authlen;
+
+    if(authCredentials->length >= 3 * 128)
+        return -1;
+    authlen = b64cpy(authbuf, parentAuthCredentials->string,
+                     parentAuthCredentials->length, 0);
+    n = snnprintf(buf, n, size, "\r\nProxy-Authorization: Basic ");
+    n = snnprint_n(buf, n, size, authbuf, authlen);
+    return n;
+}

+ 24 - 0
polipo/auth.h

@@ -0,0 +1,24 @@
+/*
+Copyright (c) 2004-2006 by Juliusz Chroboczek
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+int checkClientAuth(AtomPtr, AtomPtr, AtomPtr*, AtomPtr*);
+int buildServerAuthHeaders(char*, int, int, AtomPtr);

+ 469 - 0
polipo/chunk.c

@@ -0,0 +1,469 @@
+/*
+Copyright (c) 2003-2006 by Juliusz Chroboczek
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+#include "polipo.h"
+
+#define MB (1024 * 1024)
+int chunkLowMark = 0, 
+    chunkCriticalMark = 0,
+    chunkHighMark = 0;
+
+void
+preinitChunks()
+{
+    CONFIG_VARIABLE(chunkLowMark, CONFIG_INT,
+                    "Low mark for chunk memory (0 = auto).");
+    CONFIG_VARIABLE(chunkCriticalMark, CONFIG_INT,
+                    "Critical mark for chunk memory (0 = auto).");
+    CONFIG_VARIABLE(chunkHighMark, CONFIG_INT,
+                    "High mark for chunk memory.");
+}
+
+static void
+initChunksCommon()
+{
+#define ROUND_CHUNKS(a) a = (((unsigned long)(a) + CHUNK_SIZE - 1) / CHUNK_SIZE) * CHUNK_SIZE;
+    int q;
+
+    if(CHUNK_SIZE != 1 << log2_ceil(CHUNK_SIZE)) {
+        do_log(L_ERROR, "CHUNK SIZE %d is not a power of two.\n", CHUNK_SIZE);
+        exit(1);
+    }
+
+    ROUND_CHUNKS(chunkHighMark);
+    ROUND_CHUNKS(chunkCriticalMark);
+    ROUND_CHUNKS(chunkLowMark);
+
+    if(chunkHighMark < 8 * CHUNK_SIZE) {
+        int mem = physicalMemory();
+        if(mem > 0)
+            chunkHighMark = mem / 4;
+        else
+            chunkHighMark = 24 * MB;
+        chunkHighMark = MIN(chunkHighMark, 24 * MB);
+        chunkHighMark = MAX(chunkHighMark, 8 * CHUNK_SIZE);
+    }
+
+    if(chunkHighMark < MB / 2)
+        fprintf(stderr,
+                "Warning: little chunk memory (%d bytes)\n", chunkHighMark);
+
+    q = 0;
+    if(chunkLowMark <= 0) q = 1;
+    if(chunkLowMark < 4 * CHUNK_SIZE ||
+       chunkLowMark > chunkHighMark - 4 * CHUNK_SIZE) {
+        chunkLowMark = MIN(chunkHighMark - 4 * CHUNK_SIZE,
+                           chunkHighMark * 3 / 4);
+        ROUND_CHUNKS(chunkLowMark);
+        if(!q) do_log(L_WARN, "Inconsistent chunkLowMark -- setting to %d.\n",
+                      chunkLowMark);
+    }
+
+    q = 0;
+    if(chunkCriticalMark <= 0) q = 1;
+    if(chunkCriticalMark >= chunkHighMark - 2 * CHUNK_SIZE ||
+       chunkCriticalMark <= chunkLowMark + 2 * CHUNK_SIZE) {
+        chunkCriticalMark =
+            MIN(chunkHighMark - 2 * CHUNK_SIZE,
+                chunkLowMark + (chunkHighMark - chunkLowMark) * 15 / 16);
+        ROUND_CHUNKS(chunkCriticalMark);
+        if(!q) do_log(L_WARN, "Inconsistent chunkCriticalMark -- "
+                      "setting to %d.\n", chunkCriticalMark);
+    }
+#undef ROUND_CHUNKS
+}
+
+
+int used_chunks = 0;
+
+static void
+maybe_free_chunks(int arenas, int force)
+{
+    if(force || used_chunks >= CHUNKS(chunkHighMark)) {
+        discardObjects(force, force);
+    }
+
+    if(arenas)
+        free_chunk_arenas();
+
+    if(used_chunks >= CHUNKS(chunkLowMark) && !objectExpiryScheduled) {
+        TimeEventHandlerPtr event;
+        event = scheduleTimeEvent(1, discardObjectsHandler, 0, NULL);
+        if(event)
+            objectExpiryScheduled = 1;
+    }
+}
+    
+
+
+#ifdef MALLOC_CHUNKS
+
+void
+initChunks(void)
+{
+    do_log(L_WARN, "Warning: using malloc(3) for chunk allocation.\n");
+    used_chunks = 0;
+    initChunksCommon();
+}
+
+void
+free_chunk_arenas()
+{
+    return;
+}
+
+void *
+get_chunk()
+{
+    void *chunk;
+
+    if(used_chunks > CHUNKS(chunkHighMark))
+        maybe_free_chunks(0, 0);
+    if(used_chunks > CHUNKS(chunkHighMark))
+        return NULL;
+    chunk = malloc(CHUNK_SIZE);
+    if(!chunk) {
+        maybe_free_chunks(1, 1);
+        chunk = malloc(CHUNK_SIZE);
+        if(!chunk)
+            return NULL;
+    }
+    used_chunks++;
+    return chunk;
+}
+
+void *
+maybe_get_chunk()
+{
+    void *chunk;
+    if(used_chunks > CHUNKS(chunkHighMark))
+        return NULL;
+    chunk = malloc(CHUNK_SIZE);
+    if(chunk)
+        used_chunks++;
+    return chunk;
+}
+
+void
+dispose_chunk(void *chunk)
+{
+    assert(chunk != NULL);
+    free(chunk);
+    used_chunks--;
+}
+
+void
+free_chunks()
+{
+    return;
+}
+
+int
+totalChunkArenaSize()
+{
+    return used_chunks * CHUNK_SIZE;
+}
+#else
+
+#ifdef WIN32 /*MINGW*/
+#define MAP_FAILED NULL
+#define getpagesize() (64 * 1024)
+static void *
+alloc_arena(size_t size)
+{
+    return VirtualAlloc(NULL, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
+}
+static int
+free_arena(void *addr, size_t size)
+{
+    int rc;
+    rc = VirtualFree(addr, size, MEM_RELEASE);
+    if(!rc)
+        rc = -1;
+    return rc;
+}
+#else
+#ifndef MAP_FAILED
+#define MAP_FAILED ((void*)((long int)-1))
+#endif
+static void *
+alloc_arena(size_t size)
+{
+    return mmap(NULL, size, PROT_READ | PROT_WRITE,
+                MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+}
+static int
+free_arena(void *addr, size_t size)
+{
+    return munmap(addr, size);
+}
+#endif
+
+/* Memory is organised into a number of chunks of ARENA_CHUNKS chunks
+   each.  Every arena is pointed at by a struct _ChunkArena. */
+/* If currentArena is not NULL, it points at the last arena used,
+   which gives very fast dispose/get sequences. */
+
+#define DEFINE_FFS(type, ffs_name) \
+int                           \
+ffs_name(type i)              \
+{                             \
+    int n;                    \
+    if(i == 0) return 0;      \
+    n = 1;                    \
+    while((i & 1) == 0) {     \
+        i >>= 1;              \
+        n++;                  \
+    }                         \
+    return n;                 \
+}
+
+#if defined(DEFAULT_ARENA_BITMAPS) + defined(LONG_ARENA_BITMAPS) + defined(LONG_LONG_ARENA_BITMAPS) > 1
+#error "Multiple sizes of arenas defined"
+#endif
+
+#if defined(DEFAULT_ARENA_BITMAPS) + defined(LONG_ARENA_BITMAPS) + defined(LONG_LONG_ARENA_BITMAPS) == 0
+#ifdef HAVE_FFSL
+/* This gives us 32-bit arena bitmaps on LP32, and 64-bit ones on LP64 */
+#define LONG_ARENA_BITMAPS
+#else
+#define DEFAULT_ARENA_BITMAPS
+#endif
+#endif
+
+#if defined(DEFAULT_ARENA_BITMAPS)
+
+#ifndef HAVE_FFS
+DEFINE_FFS(int, ffs)
+#endif
+typedef unsigned int ChunkBitmap;
+#define BITMAP_FFS(bitmap) (ffs(bitmap))
+
+#elif defined(LONG_ARENA_BITMAPS)
+
+#ifndef HAVE_FFSL
+DEFINE_FFS(long, ffsl)
+#endif
+typedef unsigned long ChunkBitmap;
+#define BITMAP_FFS(bitmap) (ffsl(bitmap))
+
+#elif defined(LONG_LONG_ARENA_BITMAPS)
+
+#ifndef HAVE_FFSLL
+DEFINE_FFS(long long, ffsll)
+#endif
+typedef unsigned long long ChunkBitmap;
+#define BITMAP_FFS(bitmap) (ffsll(bitmap))
+
+#else
+
+#error "You lose"
+
+#endif
+
+#define ARENA_CHUNKS ((unsigned)sizeof(ChunkBitmap) * 8)
+#define EMPTY_BITMAP (~(ChunkBitmap)0)
+#define BITMAP_BIT(i) (((ChunkBitmap)1) << (i))
+
+static int pagesize;
+typedef struct _ChunkArena {
+    ChunkBitmap bitmap;
+    char *chunks;
+} ChunkArenaRec, *ChunkArenaPtr;
+
+static ChunkArenaPtr chunkArenas, currentArena;
+static int numArenas;
+#define CHUNK_IN_ARENA(chunk, arena)                                    \
+    ((arena)->chunks &&                                                 \
+     (char*)(chunk) >= (arena)->chunks &&                               \
+     (char*)(chunk) < (arena)->chunks + (ARENA_CHUNKS * CHUNK_SIZE))
+
+#define CHUNK_ARENA_INDEX(chunk, arena)                                 \
+    ((unsigned)((unsigned long)(((char*)(chunk) - (arena)->chunks)) /   \
+                CHUNK_SIZE))
+
+void
+initChunks(void)
+{
+    int i;
+    used_chunks = 0;
+    initChunksCommon();
+    pagesize = getpagesize();
+    if((CHUNK_SIZE * ARENA_CHUNKS) % pagesize != 0) {
+        do_log(L_ERROR,
+               "The arena size %d (%d x %d) "
+               "is not a multiple of the page size %d.\n",
+                ARENA_CHUNKS * CHUNK_SIZE, ARENA_CHUNKS, CHUNK_SIZE, pagesize);
+        abort();
+    }
+    numArenas = 
+        (CHUNKS(chunkHighMark) + (ARENA_CHUNKS - 1)) / ARENA_CHUNKS;
+    chunkArenas = malloc(numArenas * sizeof(ChunkArenaRec));
+    if(chunkArenas == NULL) {
+        do_log(L_ERROR, "Couldn't allocate chunk arenas.\n");
+        exit (1);
+    }
+    for(i = 0; i < numArenas; i++) {
+        chunkArenas[i].bitmap = EMPTY_BITMAP;
+        chunkArenas[i].chunks = NULL;
+    }
+    currentArena = NULL;
+}
+
+static ChunkArenaPtr
+findArena()
+{
+    ChunkArenaPtr arena = NULL;
+    int i;
+
+    for(i = 0; i < numArenas; i++) {
+        arena = &(chunkArenas[i]);
+        if(arena->bitmap != 0)
+            break;
+        else
+            arena = NULL;
+    }
+
+    assert(arena != NULL);
+
+    if(!arena->chunks) {
+        void *p;
+        p = alloc_arena(CHUNK_SIZE * ARENA_CHUNKS);
+        if(p == MAP_FAILED) {
+            do_log_error(L_ERROR, errno, "Couldn't allocate chunk");
+            maybe_free_chunks(1, 1);
+            return NULL;
+        }
+        arena->chunks = p;
+    }
+    return arena;
+}
+
+void *
+get_chunk()
+{
+    unsigned i;
+    ChunkArenaPtr arena = NULL;
+
+    if(currentArena && currentArena->bitmap != 0) {
+        arena = currentArena;
+    } else {
+        if(used_chunks >= CHUNKS(chunkHighMark))
+            maybe_free_chunks(0, 0);
+
+        if(used_chunks >= CHUNKS(chunkHighMark))
+            return NULL;
+        
+        arena = findArena();
+        if(!arena)
+            return NULL;
+        currentArena = arena;
+    }
+    i = BITMAP_FFS(arena->bitmap) - 1;
+    arena->bitmap &= ~BITMAP_BIT(i);
+    used_chunks++;
+    return arena->chunks + CHUNK_SIZE * i;
+}
+
+void *
+maybe_get_chunk()
+{
+    unsigned i;
+    ChunkArenaPtr arena = NULL;
+
+    if(currentArena && currentArena->bitmap != 0) {
+        arena = currentArena;
+    } else {
+        if(used_chunks >= CHUNKS(chunkHighMark))
+            return NULL;
+
+        arena = findArena();
+        if(!arena)
+            return NULL;
+        currentArena = arena;
+    }
+    i = BITMAP_FFS(arena->bitmap) - 1;
+    arena->bitmap &= ~BITMAP_BIT(i);
+    used_chunks++;
+    return arena->chunks + CHUNK_SIZE * i;
+}
+
+void
+dispose_chunk(void *chunk)
+{
+    ChunkArenaPtr arena = NULL;
+    unsigned i;
+
+    assert(chunk != NULL);
+
+    if(currentArena && CHUNK_IN_ARENA(chunk, currentArena)) {
+        arena = currentArena;
+    } else {
+        for(i = 0; i < numArenas; i++) {
+            arena = &(chunkArenas[i]);
+            if(CHUNK_IN_ARENA(chunk, arena))
+                break;
+        }
+        assert(arena != NULL);
+        currentArena = arena;
+    }
+
+    i = CHUNK_ARENA_INDEX(chunk, arena);
+    arena->bitmap |= BITMAP_BIT(i);
+    used_chunks--;
+}
+
+void
+free_chunk_arenas()
+{
+    ChunkArenaPtr arena;
+    int i, rc;
+
+    for(i = 0; i < numArenas; i++) {
+        arena = &(chunkArenas[i]);
+        if(arena->bitmap == EMPTY_BITMAP && arena->chunks) {
+            rc = free_arena(arena->chunks, CHUNK_SIZE * ARENA_CHUNKS);
+            if(rc < 0) {
+                do_log_error(L_ERROR, errno, "Couldn't unmap memory");
+                continue;
+            }
+            arena->chunks = NULL;
+        }
+    }
+    if(currentArena && currentArena->chunks == NULL)
+        currentArena = NULL;
+}
+
+int
+totalChunkArenaSize()
+{
+    ChunkArenaPtr arena;
+    int i, size = 0;
+
+    for(i = 0; i < numArenas; i++) {
+        arena = &(chunkArenas[i]);
+        if(arena->chunks)
+            size += (CHUNK_SIZE * ARENA_CHUNKS);
+    }
+    return size;
+}
+#endif

+ 51 - 0
polipo/chunk.h

@@ -0,0 +1,51 @@
+/*
+Copyright (c) 2003-2006 by Juliusz Chroboczek
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+/* A larger chunk size gets you better I/O throughput, at the cost of
+   higer memory usage.  We assume you've got plenty of memory if you've
+   got 64-bit longs. */
+
+#ifndef CHUNK_SIZE
+#ifdef ULONG_MAX
+#if ULONG_MAX > 4294967295UL
+#define CHUNK_SIZE (8 * 1024)
+#else
+#define CHUNK_SIZE (4 * 1024)
+#endif
+#else
+#define CHUNK_SIZE (4 * 1024)
+#endif
+#endif
+
+#define CHUNKS(bytes) ((unsigned long)(bytes) / CHUNK_SIZE)
+
+extern int chunkLowMark, chunkHighMark, chunkCriticalMark;
+extern int used_chunks;
+
+void preinitChunks(void);
+void initChunks(void);
+void *get_chunk(void) ATTRIBUTE ((malloc));
+void *maybe_get_chunk(void) ATTRIBUTE ((malloc));
+
+void dispose_chunk(void *chunk);
+void free_chunk_arenas(void);
+int totalChunkArenaSize(void);

+ 2155 - 0
polipo/client.c

@@ -0,0 +1,2155 @@
+/*
+Copyright (c) 2003-2006 by Juliusz Chroboczek
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+#include "polipo.h"
+
+static int 
+httpAcceptAgain(TimeEventHandlerPtr event)
+{
+    FdEventHandlerPtr newevent;
+    int fd = *(int*)event->data;
+
+    newevent = schedule_accept(fd, httpAccept, NULL);
+    if(newevent == NULL) {
+        free_chunk_arenas();
+        newevent = schedule_accept(fd, httpAccept, NULL);
+        if(newevent == NULL) {
+            do_log(L_ERROR, "Couldn't schedule accept.\n");
+            polipoExit();
+        }
+    }
+    return 1;
+}
+
+int
+httpAccept(int fd, FdEventHandlerPtr event, AcceptRequestPtr request)
+{
+    int rc;
+    HTTPConnectionPtr connection;
+    TimeEventHandlerPtr timeout;
+
+    if(fd < 0) {
+        if(-fd == EINTR || -fd == EAGAIN || -fd == EWOULDBLOCK)
+            return 0;
+        do_log_error(L_ERROR, -fd, "Couldn't establish listening socket");
+        if(-fd == EMFILE || -fd == ENOMEM || -fd == ENOBUFS) {
+            TimeEventHandlerPtr again = NULL;
+            do_log(L_WARN, "Refusing client connections for one second.\n");
+            free_chunk_arenas();
+            again = scheduleTimeEvent(1, httpAcceptAgain, 
+                                      sizeof(request->fd), &request->fd);
+            if(!again) {
+                do_log(L_ERROR, "Couldn't schedule accept -- sleeping.\n");
+                sleep(1);
+                again = scheduleTimeEvent(1, httpAcceptAgain, 
+                                          sizeof(request->fd), &request->fd);
+                if(!again) {
+                    do_log(L_ERROR, "Couldn't schedule accept -- aborting.\n");
+                    polipoExit();
+                }
+            }
+            return 1;
+        } else {
+            polipoExit();
+            return 1;
+        }
+    }
+
+    if(allowedNets) {
+        if(netAddressMatch(fd, allowedNets) != 1) {
+            do_log(L_WARN, "Refusing connection from unauthorised net\n");
+            CLOSE(fd);
+            return 0;
+        }
+    }
+
+    rc = setNonblocking(fd, 1);
+    if(rc < 0) {
+        do_log_error(L_WARN, errno, "Couldn't set non blocking mode");
+        CLOSE(fd);
+        return 0;
+    }
+    rc = setNodelay(fd, 1);
+    if(rc < 0) 
+        do_log_error(L_WARN, errno, "Couldn't disable Nagle's algorithm");
+
+    connection = httpMakeConnection();
+
+    timeout = scheduleTimeEvent(clientTimeout, httpTimeoutHandler,
+                                sizeof(connection), &connection);
+    if(!timeout) {
+        CLOSE(fd);
+        free(connection);
+        return 0;
+    }
+
+    connection->fd = fd;
+    connection->timeout = timeout;
+
+    do_log(D_CLIENT_CONN, "Accepted client connection 0x%lx\n",
+           (unsigned long)connection);
+
+    connection->flags = CONN_READER;
+
+    do_stream_buf(IO_READ | IO_NOTNOW, connection->fd, 0,
+                  &connection->reqbuf, CHUNK_SIZE,
+                  httpClientHandler, connection);
+    return 0;
+}
+
+/* Abort a client connection.  It is only safe to abort the requests
+   if we know the connection is closed. */
+void
+httpClientAbort(HTTPConnectionPtr connection, int closed)
+{
+    HTTPRequestPtr request = connection->request;
+
+    pokeFdEvent(connection->fd, -EDOSHUTDOWN, POLLOUT);
+    if(closed) {
+        while(request) {
+            if(request->chandler) {
+                request->error_code = 500;
+                request->error_message = internAtom("Connection finishing");
+                abortConditionHandler(request->chandler);
+                request->chandler = NULL;
+            }
+            request = request->next;
+        }
+    }
+}
+
+/* s != 0 specifies that the connection must be shut down.  It is 1 in
+   order to linger the connection, 2 to close it straight away. */
+void
+httpClientFinish(HTTPConnectionPtr connection, int s)
+{
+    HTTPRequestPtr request = connection->request;
+
+    assert(!(request && request->request 
+             && request->request->request != request));
+
+    if(s == 0) {
+        if(!request || !(request->flags & REQUEST_PERSISTENT))
+            s = 1;
+    }
+
+    httpConnectionDestroyBuf(connection);
+
+    connection->flags &= ~CONN_WRITER;
+
+    if(connection->flags & CONN_SIDE_READER) {
+        /* We're in POST or PUT and the reader isn't done yet.
+           Wait for the read side to close the connection. */
+        assert(request && (connection->flags & CONN_READER));
+        if(s >= 2) {
+            pokeFdEvent(connection->fd, -EDOSHUTDOWN, POLLIN);
+        } else {
+            pokeFdEvent(connection->fd, -EDOGRACEFUL, POLLIN);
+        }
+        return;
+    }
+
+    if(connection->timeout) 
+        cancelTimeEvent(connection->timeout);
+    connection->timeout = NULL;
+
+    if(request) {
+        HTTPRequestPtr requestee;
+
+        requestee = request->request;
+        if(requestee) {
+            request->request = NULL;
+            requestee->request = NULL;
+        }
+        if(requestee)
+            httpServerClientReset(requestee);
+        if(request->chandler) {
+            request->error_code = 500;
+            request->error_message = internAtom("Connection finishing");
+            abortConditionHandler(request->chandler);
+            request->chandler = NULL;
+        }
+            
+        if(request->object) {
+            if(request->object->requestor == request)
+                request->object->requestor = NULL;
+            releaseObject(request->object);
+            request->object = NULL;
+        }
+        httpDequeueRequest(connection);
+        httpDestroyRequest(request);
+        request = NULL;
+
+    }
+
+    connection->len = -1;
+    connection->offset = 0;
+    connection->te = TE_IDENTITY;
+
+    if(!s) {
+        assert(connection->fd > 0);
+        connection->serviced++;
+        httpSetTimeout(connection, clientTimeout);
+        if(!(connection->flags & CONN_READER)) {
+            if(connection->reqlen == 0)
+                httpConnectionDestroyReqbuf(connection);
+            else if((connection->flags & CONN_BIGREQBUF) &&
+                    connection->reqlen < CHUNK_SIZE)
+                httpConnectionUnbigifyReqbuf(connection);
+            connection->flags |= CONN_READER;
+            httpSetTimeout(connection, clientTimeout);
+            do_stream_buf(IO_READ | IO_NOTNOW |
+                          (connection->reqlen ? IO_IMMEDIATE : 0),
+                          connection->fd, connection->reqlen,
+                          &connection->reqbuf,
+                          (connection->flags & CONN_BIGREQBUF) ?
+                          bigBufferSize : CHUNK_SIZE,
+                          httpClientHandler, connection);
+        }
+        /* The request has already been validated when it first got
+           into the queue */
+        if(connection->request) {
+            if(connection->request->object != NULL)
+                httpClientNoticeRequest(connection->request, 1);
+            else
+                assert(connection->flags & CONN_READER);
+        }
+        return;
+    }
+    
+    do_log(D_CLIENT_CONN, "Closing client connection 0x%lx\n",
+           (unsigned long)connection);
+
+    if(connection->flags & CONN_READER) {
+        httpSetTimeout(connection, 10);
+        if(connection->fd < 0) return;
+        if(s >= 2) {
+            pokeFdEvent(connection->fd, -EDOSHUTDOWN, POLLIN);
+        } else {
+            pokeFdEvent(connection->fd, -EDOGRACEFUL, POLLIN);
+        }
+        return;
+    }
+    while(1) {
+        HTTPRequestPtr requestee;
+        request = connection->request;
+        if(!request)
+            break;
+        requestee = request->request;
+        request->request = NULL;
+        if(requestee) {
+            requestee->request = NULL;
+            httpServerClientReset(requestee);
+        }
+        if(request->chandler)
+            abortConditionHandler(request->chandler);
+        request->chandler = NULL;
+        if(request->object && request->object->requestor == request)
+            request->object->requestor = NULL;
+        httpDequeueRequest(connection);
+        httpDestroyRequest(request);
+    }
+    httpConnectionDestroyReqbuf(connection);
+    if(connection->timeout)
+        cancelTimeEvent(connection->timeout);
+    connection->timeout = NULL;
+    if(connection->fd >= 0) {
+        if(s >= 2)
+            CLOSE(connection->fd);
+        else
+            lingeringClose(connection->fd);
+    }
+    connection->fd = -1;
+    free(connection);
+}
+
+/* Extremely baroque implementation of close: we need to synchronise
+   between the writer and the reader.  */
+
+static char client_shutdown_buffer[17];
+
+static int httpClientDelayedShutdownHandler(TimeEventHandlerPtr);
+
+static int
+httpClientDelayedShutdown(HTTPConnectionPtr connection)
+{
+    TimeEventHandlerPtr handler;
+
+    assert(connection->flags & CONN_READER);
+    handler = scheduleTimeEvent(1, httpClientDelayedShutdownHandler,
+                                sizeof(connection), &connection);
+    if(!handler) {
+        do_log(L_ERROR, 
+               "Couldn't schedule delayed shutdown -- freeing memory.");
+        free_chunk_arenas();
+        handler = scheduleTimeEvent(1, httpClientDelayedShutdownHandler,
+                                    sizeof(connection), &connection);
+        if(!handler) {
+            do_log(L_ERROR, 
+                   "Couldn't schedule delayed shutdown -- aborting.\n");
+            polipoExit();
+        }
+    }
+    return 1;
+}
+
+static int
+httpClientShutdownHandler(int status,
+                          FdEventHandlerPtr event, StreamRequestPtr request)
+{
+    HTTPConnectionPtr connection = request->data;
+
+    assert(connection->flags & CONN_READER);
+
+    if(!(connection->flags & CONN_WRITER)) {
+        connection->flags &= ~CONN_READER;
+        connection->reqlen = 0;
+        httpConnectionDestroyReqbuf(connection);
+        if(status && status != -EDOGRACEFUL)
+            httpClientFinish(connection, 2);
+        else
+            httpClientFinish(connection, 1);
+        return 1;
+    }
+
+    httpClientDelayedShutdown(connection);
+    return 1;
+}
+
+static int 
+httpClientDelayedShutdownHandler(TimeEventHandlerPtr event)
+{
+    HTTPConnectionPtr connection = *(HTTPConnectionPtr*)event->data;
+    assert(connection->flags & CONN_READER);
+
+    if(!(connection->flags & CONN_WRITER)) {
+        connection->flags &= ~CONN_READER;
+        connection->reqlen = 0;
+        httpConnectionDestroyReqbuf(connection);
+        httpClientFinish(connection, 1);
+        return 1;
+    }
+    do_stream(IO_READ | IO_NOTNOW, connection->fd, 
+              0, client_shutdown_buffer, 17, 
+              httpClientShutdownHandler, connection);
+    return 1;
+}
+
+int
+httpClientHandler(int status,
+                  FdEventHandlerPtr event, StreamRequestPtr request)
+{
+    HTTPConnectionPtr connection = request->data;
+    int i, body;
+    int bufsize = 
+        (connection->flags & CONN_BIGREQBUF) ? bigBufferSize : CHUNK_SIZE;
+
+    assert(connection->flags & CONN_READER);
+
+    /* There's no point trying to do something with this request if
+       the client has shut the connection down -- HTTP doesn't do
+       half-open connections. */
+    if(status != 0) {
+        connection->reqlen = 0;
+        httpConnectionDestroyReqbuf(connection);
+        if(!(connection->flags & CONN_WRITER)) {
+            connection->flags &= ~CONN_READER;
+            if(status > 0 || status == -ECONNRESET || status == -EDOSHUTDOWN)
+                httpClientFinish(connection, 2);
+            else
+                httpClientFinish(connection, 1);
+            return 1;
+        }
+        httpClientAbort(connection, status > 0 || status == -ECONNRESET);
+        connection->flags &= ~CONN_READER;
+        return 1;
+    }
+
+    i = findEndOfHeaders(connection->reqbuf, 0, request->offset, &body);
+    connection->reqlen = request->offset;
+
+    if(i >= 0) {
+        connection->reqbegin = i;
+        httpClientHandlerHeaders(event, request, connection);
+        return 1;
+    }
+
+    if(connection->reqlen >= bufsize) {
+        int rc = 0;
+        if(!(connection->flags & CONN_BIGREQBUF))
+            rc = httpConnectionBigifyReqbuf(connection);
+        if((connection->flags & CONN_BIGREQBUF) &&
+           connection->reqlen < bigBufferSize) {
+            do_stream(IO_READ, connection->fd, connection->reqlen,
+                      connection->reqbuf, bigBufferSize,
+                      httpClientHandler, connection);
+            return 1;
+        }
+        connection->reqlen = 0;
+        httpConnectionDestroyReqbuf(connection);
+        if(rc < 0) {
+            do_log(L_ERROR, "Couldn't allocate big buffer.\n");
+            httpClientNewError(connection, METHOD_UNKNOWN, 0, 400, 
+                               internAtom("Couldn't allocate big buffer"));
+        } else {
+            do_log(L_ERROR, "Couldn't find end of client's headers.\n");
+            httpClientNewError(connection, METHOD_UNKNOWN, 0, 400, 
+                               internAtom("Couldn't find end of headers"));
+        }
+        return 1;
+    }
+    httpSetTimeout(connection, clientTimeout);
+    return 0;
+}
+
+int
+httpClientRawErrorHeaders(HTTPConnectionPtr connection,
+                          int code, AtomPtr message,
+                          int close, AtomPtr headers)
+{
+    int fd = connection->fd;
+    int n;
+    char *url; int url_len;
+    char *etag;
+
+    assert(connection->flags & CONN_WRITER);
+    assert(code != 0);
+
+    if(close >= 0) {
+        if(connection->request)
+            close = 
+                close || !(connection->request->flags & REQUEST_PERSISTENT);
+        else
+            close = 1;
+    }
+    if(connection->request && connection->request->object) {
+        url = connection->request->object->key;
+        url_len = connection->request->object->key_size;
+        etag = connection->request->object->etag;
+    } else {
+        url = NULL;
+        url_len = 0;
+        etag = NULL;
+    }
+
+    if(connection->buf == NULL) {
+        connection->buf = get_chunk();
+        if(connection->buf == NULL) {
+            httpClientFinish(connection, 1);
+            return 1;
+        }
+    }
+
+    n = httpWriteErrorHeaders(connection->buf, CHUNK_SIZE, 0,
+                              connection->request &&
+                              connection->request->method != METHOD_HEAD,
+                              code, message, close > 0, headers,
+                              url, url_len, etag);
+    if(n <= 0) {
+        shutdown(connection->fd, 1);
+        if(close >= 0)
+            httpClientFinish(connection, 1);
+        return 1;
+    }
+
+    httpSetTimeout(connection, clientTimeout);
+    do_stream(IO_WRITE, fd, 0, connection->buf, n, 
+              close > 0 ? httpErrorStreamHandler :
+              close == 0 ? httpErrorNocloseStreamHandler :
+              httpErrorNofinishStreamHandler,
+              connection);
+
+    return 1;
+}
+
+int
+httpClientRawError(HTTPConnectionPtr connection, int code, AtomPtr message,
+                   int close)
+{
+    return httpClientRawErrorHeaders(connection, code, message, close, NULL);
+}
+
+int
+httpClientNoticeErrorHeaders(HTTPRequestPtr request, int code, AtomPtr message,
+                             AtomPtr headers)
+{
+    if(request->error_message)
+        releaseAtom(request->error_message);
+    if(request->error_headers)
+        releaseAtom(request->error_headers);
+    request->error_code = code;
+    request->error_message = message;
+    request->error_headers = headers;
+    httpClientNoticeRequest(request, 0);
+    return 1;
+}
+
+int
+httpClientNoticeError(HTTPRequestPtr request, int code, AtomPtr message)
+{
+    return httpClientNoticeErrorHeaders(request, code, message, NULL);
+}
+
+int
+httpClientError(HTTPRequestPtr request, int code, AtomPtr message)
+{
+    if(request->error_message)
+        releaseAtom(request->error_message);
+    request->error_code = code;
+    request->error_message = message;
+    if(request->chandler) {
+        abortConditionHandler(request->chandler);
+        request->chandler = NULL;
+    } else if(request->object)
+        notifyObject(request->object);
+    return 1;
+}
+
+/* This may be called from object handlers. */
+int
+httpClientLeanError(HTTPRequestPtr request, int code, AtomPtr message)
+{
+    if(request->error_message)
+        releaseAtom(request->error_message);
+    request->error_code = code;
+    request->error_message = message;
+    return 1;
+}
+
+
+int
+httpClientNewError(HTTPConnectionPtr connection, int method, int persist,
+                   int code, AtomPtr message)
+{
+    HTTPRequestPtr request;
+    request = httpMakeRequest();
+    if(request == NULL) {
+        do_log(L_ERROR, "Couldn't allocate error request.\n");
+        httpClientFinish(connection, 1);
+        return 1;
+    }
+    request->method = method;
+    if(persist)
+        request->flags |= REQUEST_PERSISTENT;
+    else
+        request->flags &= ~REQUEST_PERSISTENT;
+    request->error_code = code;
+    request->error_message = message;
+
+    httpQueueRequest(connection, request);
+    httpClientNoticeRequest(request, 0);
+    return 1;
+}
+        
+int
+httpErrorStreamHandler(int status,
+                       FdEventHandlerPtr event,
+                       StreamRequestPtr srequest)
+{
+    HTTPConnectionPtr connection = srequest->data;
+
+    if(status == 0 && !streamRequestDone(srequest))
+        return 0;
+
+    httpClientFinish(connection, 1);
+    return 1;
+}
+
+int
+httpErrorNocloseStreamHandler(int status,
+                              FdEventHandlerPtr event,
+                              StreamRequestPtr srequest)
+{
+    HTTPConnectionPtr connection = srequest->data;
+
+    if(status == 0 && !streamRequestDone(srequest))
+        return 0;
+
+    httpClientFinish(connection, 0);
+    return 1;
+}
+
+int
+httpErrorNofinishStreamHandler(int status,
+                               FdEventHandlerPtr event,
+                               StreamRequestPtr srequest)
+{
+    if(status == 0 && !streamRequestDone(srequest))
+        return 0;
+
+    return 1;
+}
+
+int
+httpClientHandlerHeaders(FdEventHandlerPtr event, StreamRequestPtr srequest,
+                         HTTPConnectionPtr connection)
+{
+    HTTPRequestPtr request;
+    int rc;
+    int method, version;
+    AtomPtr url = NULL;
+    int start;
+    int code;
+    AtomPtr message;
+
+    start = 0;
+    /* Work around clients working around NCSA lossage. */
+    if(connection->reqbuf[0] == '\n')
+        start = 1;
+    else if(connection->reqbuf[0] == '\r' && connection->reqbuf[1] == '\n')
+        start = 2;
+
+    httpSetTimeout(connection, -1);
+    rc = httpParseClientFirstLine(connection->reqbuf, start,
+                                  &method, &url, &version);
+    if(rc <= 0) {
+        do_log(L_ERROR, "Couldn't parse client's request line\n");
+        code = 400;
+        message =  internAtom("Error in request line");
+        goto fail;
+    }
+
+    do_log(D_CLIENT_REQ, "Client request: ");
+    do_log_n(D_CLIENT_REQ, connection->reqbuf, rc - 1);
+    do_log(D_CLIENT_REQ, "\n");
+
+    if(version != HTTP_10 && version != HTTP_11) {
+        do_log(L_ERROR, "Unknown client HTTP version\n");
+        code = 400;
+        message = internAtom("Error in first request line");
+        goto fail;
+    }
+
+    if(method == METHOD_UNKNOWN) {
+        code = 501;
+        message =  internAtom("Method not implemented");
+        goto fail;
+    }
+
+    request = httpMakeRequest();
+    if(request == NULL) {
+        do_log(L_ERROR, "Couldn't allocate client request.\n");
+        code = 500;
+        message = internAtom("Couldn't allocate client request");
+        goto fail;
+    }
+
+    if(connection->version != HTTP_UNKNOWN && version != connection->version) {
+        do_log(L_WARN, "Client version changed!\n");
+    }
+
+    connection->version = version;
+    request->flags = REQUEST_PERSISTENT;
+    request->method = method;
+    request->cache_control = no_cache_control;
+    httpQueueRequest(connection, request);
+    connection->reqbegin = rc;
+    return httpClientRequest(request, url);
+
+ fail:
+    if(url) releaseAtom(url);
+    shutdown(connection->fd, 0);
+    connection->reqlen = 0;
+    connection->reqbegin = 0;
+    httpConnectionDestroyReqbuf(connection);
+    connection->flags &= ~CONN_READER;
+    httpClientNewError(connection, METHOD_UNKNOWN, 0, code, message);
+    return 1;
+
+}
+
+static int
+httpClientRequestDelayed(TimeEventHandlerPtr event)
+{
+    HTTPRequestPtr request = *(HTTPRequestPtr*)event->data;
+    AtomPtr url;
+    url = internAtomN(request->object->key, request->object->key_size);
+    if(url == NULL) {
+        do_log(L_ERROR, "Couldn't allocate url.\n");
+        abortObject(request->object, 503, internAtom("Couldn't allocate url"));
+        return 1;
+    }
+    httpClientRequest(request, url);
+    return 1;
+}
+
+int
+delayedHttpClientRequest(HTTPRequestPtr request)
+{
+    TimeEventHandlerPtr event;
+    event = scheduleTimeEvent(-1, httpClientRequestDelayed,
+                              sizeof(request), &request);
+    if(!event)
+        return -1;
+    return 1;
+}
+
+int
+httpClientRequest(HTTPRequestPtr request, AtomPtr url)
+{
+    HTTPConnectionPtr connection = request->connection;
+    int i, rc;
+    int body_len, body_te;
+    AtomPtr headers;
+    CacheControlRec cache_control;
+    AtomPtr via, expect, auth;
+    HTTPConditionPtr condition;
+    HTTPRangeRec range;
+
+    assert(!request->chandler);
+    assert(connection->reqbuf);
+
+    i = httpParseHeaders(1, url,
+                         connection->reqbuf, connection->reqbegin, request,
+                         &headers, &body_len, 
+                         &cache_control, &condition, &body_te,
+                         NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
+                         &expect, &range, NULL, NULL, &via, &auth);
+    if(i < 0) {
+        releaseAtom(url);
+        do_log(L_ERROR, "Couldn't parse client headers.\n");
+        shutdown(connection->fd, 0);
+        request->flags &= ~REQUEST_PERSISTENT;
+        connection->flags &= ~CONN_READER;
+        httpClientNoticeError(request, 503,
+                              internAtom("Couldn't parse client headers"));
+        return 1;
+    }
+
+    connection->reqbegin = i;
+
+    if(body_len < 0) {
+        if(request->method == METHOD_GET || request->method == METHOD_HEAD ||
+           request->method == METHOD_POST || request->method == METHOD_OPTIONS ||
+           request->method == METHOD_DELETE)
+            body_len = 0;
+    }
+    connection->bodylen = body_len;
+    connection->reqte = body_te;
+
+    if(authRealm) {
+        AtomPtr message = NULL;
+        AtomPtr challenge = NULL;
+        int code = checkClientAuth(auth, url, &message, &challenge);
+        if(auth) {
+            releaseAtom(auth);
+            auth = NULL;
+        }
+        if(expect) {
+            releaseAtom(expect);
+            expect = NULL;
+        }
+        if(code) {
+            request->flags |= REQUEST_FORCE_ERROR;
+            httpClientDiscardBody(connection);
+            httpClientNoticeErrorHeaders(request, code, message, challenge);
+            return 1;
+        }
+    }
+
+    if(auth) {
+        releaseAtom(auth);
+        auth = NULL;
+    }
+
+    if(expect) {
+        if(expect == atom100Continue && REQUEST_SIDE(request)) {
+            request->flags |= REQUEST_WAIT_CONTINUE;
+        } else {
+            httpClientDiscardBody(connection);
+            httpClientNoticeError(request, 417,
+                                  internAtom("Expectation failed"));
+            releaseAtom(expect);
+            return 1;
+        }
+        releaseAtom(expect);
+    }
+
+    request->from = range.from < 0 ? 0 : range.from;
+    request->to = range.to;
+    request->cache_control = cache_control;
+    request->via = via;
+    request->headers = headers;
+    request->condition = condition;
+    request->object = NULL;
+
+    if(connection->serviced > 500)
+        request->flags &= ~REQUEST_PERSISTENT;
+
+    if(request->method == METHOD_CONNECT) {
+        if(connection->flags & CONN_WRITER) {
+            /* For now */
+            httpClientDiscardBody(connection);
+            httpClientNoticeError(request, 500,
+                                  internAtom("Pipelined CONNECT "
+                                             "not supported"));
+            return 1;
+        }
+        if(connection->flags & CONN_BIGREQBUF) {
+            /* For now */
+            httpClientDiscardBody(connection);
+            httpClientNoticeError(request, 500,
+                                  internAtom("CONNECT over big buffer "
+                                             "not supported"));
+            return 1;
+        }
+        connection->flags &= ~CONN_READER;
+        do_tunnel(connection->fd, connection->reqbuf, 
+                  connection->reqbegin, connection->reqlen, url);
+        connection->fd = -1;
+        connection->reqbuf = NULL;
+        connection->reqlen = 0;
+        connection->reqbegin = 0;
+        httpClientFinish(connection, 2);
+        return 1;
+    }
+
+    rc = urlForbidden(url, httpClientRequestContinue, request);
+    if(rc < 0) {
+        do_log(L_ERROR, "Couldn't schedule httpClientRequestContinue.\n");
+        httpClientDiscardBody(connection);
+        httpClientNoticeError(request, 500,
+                              internAtom("Couldn't schedule "
+                                         "httpClientRequestContinue"));
+        return 1;
+    }
+    return 1;
+}
+
+int
+httpClientRequestContinue(int forbidden_code, AtomPtr url,
+                          AtomPtr forbidden_message, AtomPtr forbidden_headers,
+                          void *closure)
+{
+    HTTPRequestPtr request = (HTTPRequestPtr)closure;
+    HTTPConnectionPtr connection = request->connection;
+    RequestFunction requestfn;
+    ObjectPtr object = NULL;
+
+    if(forbidden_code < 0) {
+        releaseAtom(url);
+        httpClientDiscardBody(connection);
+        httpClientNoticeError(request, 500, 
+                              internAtomError(-forbidden_code,
+                                              "Couldn't test for forbidden "
+                                              "URL"));
+        return 1;
+    }
+
+    if(forbidden_code) {
+        releaseAtom(url);
+        httpClientDiscardBody(connection);
+        httpClientNoticeErrorHeaders(request,
+                                     forbidden_code, forbidden_message,
+                                     forbidden_headers);
+        return 1;
+    }
+
+    requestfn = 
+        urlIsLocal(url->string, url->length) ? 
+        httpLocalRequest :
+        httpServerRequest;
+
+    if(request->method == METHOD_POST || request->method == METHOD_PUT ||
+       request->method == METHOD_OPTIONS || request->method == METHOD_DELETE) {
+        do {
+            object = findObject(OBJECT_HTTP, url->string, url->length);
+            if(object) {
+                privatiseObject(object, 0);
+                releaseObject(object);
+            }
+        } while(object);
+        request->object = makeObject(OBJECT_HTTP, url->string, url->length,
+                                     0, 0, requestfn, NULL);
+        if(request->object == NULL) {
+            httpClientDiscardBody(connection);
+            httpClientNoticeError(request, 503,
+                                  internAtom("Couldn't allocate object"));
+            return 1;
+        }
+        if(requestfn == httpLocalRequest)
+            request->object->flags |= OBJECT_LOCAL;
+        return httpClientSideRequest(request);
+    }
+
+    if(request->cache_control.flags & CACHE_AUTHORIZATION) {
+        do {
+            object = makeObject(OBJECT_HTTP, url->string, url->length, 0, 0,
+                                requestfn, NULL);
+            if(object && object->flags != OBJECT_INITIAL) {
+                if(!(object->cache_control & CACHE_PUBLIC)) {
+                    privatiseObject(object, 0);
+                    releaseObject(object);
+                    object = NULL;
+                } else
+                    break;
+            }
+        } while(object == NULL);
+        if(object)
+            object->flags |= OBJECT_LINEAR;
+    } else {
+        object = findObject(OBJECT_HTTP, url->string, url->length);
+        if(!object)
+            object = makeObject(OBJECT_HTTP, url->string, url->length, 1, 1,
+                                requestfn, NULL);
+    }
+    releaseAtom(url);
+    url = NULL;
+
+    if(!object) {
+        do_log(L_ERROR, "Couldn't allocate object.\n");
+        httpClientDiscardBody(connection);
+        httpClientNoticeError(request, 503,
+                              internAtom("Couldn't allocate object"));
+        return 1;
+    }
+
+    if(object->request == httpLocalRequest) {
+        object->flags |= OBJECT_LOCAL;
+    } else {
+        if(disableProxy) {
+            httpClientDiscardBody(connection);
+            httpClientNoticeError(request, 403,
+                                  internAtom("Proxying disabled"));
+            releaseObject(object);
+            return 1;
+        }
+
+        if(!checkVia(proxyName, request->via)) {
+            httpClientDiscardBody(connection);
+            httpClientNoticeError(request, 504, 
+                                  internAtom("Proxy loop detected"));
+            releaseObject(object);
+            return 1;
+        }
+    }
+
+    request->object = object;
+
+    httpClientDiscardBody(connection);
+    httpClientNoticeRequest(request, 0);
+    return 1;
+}
+
+static int httpClientDelayed(TimeEventHandlerPtr handler);
+
+int
+httpClientDiscardBody(HTTPConnectionPtr connection)
+{
+    TimeEventHandlerPtr handler;
+
+    assert(connection->reqoffset == 0);
+    assert(connection->flags & CONN_READER);
+
+    if(connection->reqte != TE_IDENTITY)
+        goto fail;
+
+    if(connection->bodylen < 0)
+        goto fail;
+
+    if(connection->bodylen < connection->reqlen - connection->reqbegin) {
+        connection->reqbegin += connection->bodylen;
+        connection->bodylen = 0;
+    } else {
+        connection->bodylen -= connection->reqlen - connection->reqbegin;
+        connection->reqbegin = 0;
+        connection->reqlen = 0;
+        httpConnectionDestroyReqbuf(connection);
+    }
+    connection->reqte = TE_UNKNOWN;
+
+    if(connection->bodylen > 0) {
+        httpSetTimeout(connection, clientTimeout);
+        do_stream_buf(IO_READ | IO_NOTNOW,
+                      connection->fd, connection->reqlen,
+                      &connection->reqbuf, CHUNK_SIZE,
+                      httpClientDiscardHandler, connection);
+        return 1;
+    }
+
+    if(connection->reqlen > connection->reqbegin) {
+        memmove(connection->reqbuf, connection->reqbuf + connection->reqbegin,
+                connection->reqlen - connection->reqbegin);
+        connection->reqlen -= connection->reqbegin;
+        connection->reqbegin = 0;
+    } else {
+        connection->reqlen = 0;
+        connection->reqbegin = 0;
+    }
+
+    httpSetTimeout(connection, clientTimeout);
+    /* We need to delay in order to make sure the previous request
+       gets queued on the server side.  IO_NOTNOW isn't strong enough
+       for that due to IO_IMMEDIATE. */
+    handler = scheduleTimeEvent(-1, httpClientDelayed,
+                                sizeof(connection), &connection);
+    if(handler == NULL) {
+        do_log(L_ERROR, "Couldn't schedule reading from client.");
+        goto fail;
+    }
+    return 1;
+
+ fail:
+    connection->reqlen = 0;
+    connection->reqbegin = 0;
+    connection->bodylen = 0;
+    connection->reqte = TE_UNKNOWN;
+    shutdown(connection->fd, 2);
+    handler = scheduleTimeEvent(-1, httpClientDelayed,
+                                sizeof(connection), &connection);
+    if(handler == NULL) {
+        do_log(L_ERROR, "Couldn't schedule reading from client.");
+        connection->flags &= ~CONN_READER;
+    }
+    return 1;
+}
+
+static int
+httpClientDelayed(TimeEventHandlerPtr event)
+{
+     HTTPConnectionPtr connection = *(HTTPConnectionPtr*)event->data;
+
+     /* IO_NOTNOW is unfortunate, but needed to avoid starvation if a
+        client is pipelining a lot of requests. */
+     if(connection->reqlen > 0) {
+         int bufsize;
+         if((connection->flags & CONN_BIGREQBUF) &&
+            connection->reqlen < CHUNK_SIZE)
+             httpConnectionUnbigifyReqbuf(connection);
+         /* Don't read new requests if buffer is big. */
+         bufsize = (connection->flags & CONN_BIGREQBUF) ?
+             connection->reqlen : CHUNK_SIZE;
+         do_stream(IO_READ | IO_IMMEDIATE | IO_NOTNOW,
+                   connection->fd, connection->reqlen,
+                   connection->reqbuf, bufsize,
+                   httpClientHandler, connection);
+     } else {
+         httpConnectionDestroyReqbuf(connection);
+         do_stream_buf(IO_READ | IO_NOTNOW,
+                       connection->fd, 0,
+                       &connection->reqbuf, CHUNK_SIZE,
+                       httpClientHandler, connection);
+     }
+     return 1;
+}
+
+int
+httpClientDiscardHandler(int status,
+                         FdEventHandlerPtr event, StreamRequestPtr request)
+{
+    HTTPConnectionPtr connection = request->data;
+
+    assert(connection->flags & CONN_READER);
+    if(status) {
+        if(status < 0 && status != -EPIPE && status != -ECONNRESET)
+            do_log_error(L_ERROR, -status, "Couldn't read from client");
+        connection->bodylen = -1;
+        return httpClientDiscardBody(connection);
+    }
+
+    assert(request->offset > connection->reqlen);
+    connection->reqlen = request->offset;
+
+    httpClientDiscardBody(connection);
+    return 1;
+}
+
+int
+httpClientNoticeRequest(HTTPRequestPtr request, int novalidate)
+{
+    HTTPConnectionPtr connection = request->connection;
+    ObjectPtr object = request->object;
+    int serveNow = (request == connection->request);
+    int validate = 0;
+    int conditional = 0;
+    int local, haveData;
+    int rc;
+
+    assert(!request->chandler);
+
+    if(request->error_code) {
+        if((request->flags & REQUEST_FORCE_ERROR) || REQUEST_SIDE(request) ||
+           request->object == NULL ||
+           (request->object->flags & OBJECT_LOCAL) ||
+           (request->object->flags & OBJECT_ABORTED) ||
+           (relaxTransparency < 1 && !proxyOffline)) {
+            if(serveNow) {
+                connection->flags |= CONN_WRITER;
+                return httpClientRawErrorHeaders(connection,
+                                                 request->error_code, 
+                                                 retainAtom(request->
+                                                            error_message),
+                                                 0, request->error_headers);
+            } else {
+                return 1;
+            }
+        }
+    }
+
+    if(REQUEST_SIDE(request)) {
+        assert(!(request->flags & REQUEST_REQUESTED));
+        if(serveNow) {
+            assert(!request->chandler);
+            request->chandler =
+                conditionWait(&request->object->condition, 
+                              httpClientGetHandler,
+                              sizeof(request), &request);
+            if(request->chandler == NULL) {
+                do_log(L_ERROR, "Couldn't register condition handler.\n");
+                connection->flags |= CONN_WRITER;
+                httpClientRawError(connection, 500,
+                                   internAtom("Couldn't register "
+                                              "condition handler"),
+                                   0);
+                return 1;
+            }
+            connection->flags |= CONN_WRITER;
+            rc = object->request(request->object,
+                                 request->method,
+                                 request->from, request->to, 
+                                 request,
+                                 request->object->request_closure);
+        }
+        return 1;
+    }
+
+    local = urlIsLocal(object->key, object->key_size);
+    objectFillFromDisk(object, request->from,
+                       request->method == METHOD_HEAD ? 0 : 1);
+
+    /* The spec doesn't strictly forbid 206 for non-200 instances, but doing
+       that breaks some client software. */
+    if(object->code && object->code != 200) {
+        request->from = 0;
+        request->to = -1;
+    }
+
+    if(request->condition && request->condition->ifrange) {
+        if(!object->etag || 
+           strcmp(object->etag, request->condition->ifrange) != 0) {
+            request->from = 0;
+            request->to = -1;
+        }
+    }
+
+    if(object->flags & OBJECT_DYNAMIC) {
+        request->from = 0;
+        request->to = -1;
+    }
+
+    if(request->method == METHOD_HEAD)
+        haveData = !(request->object->flags & OBJECT_INITIAL);
+    else
+        haveData = 
+            (request->object->length >= 0 && 
+             request->object->length <= request->from) ||
+            (objectHoleSize(request->object, request->from) == 0);
+
+    if(request->flags & REQUEST_REQUESTED)
+        validate = 0;
+    else if(novalidate || (!local && proxyOffline))
+        validate = 0;
+    else if(local)
+        validate = 
+            objectMustRevalidate(request->object, &request->cache_control);
+    else if(request->cache_control.flags & CACHE_ONLY_IF_CACHED)
+        validate = 0;
+    else if((request->object->flags & OBJECT_FAILED) &&
+            !(object->flags & OBJECT_INPROGRESS) &&
+            !relaxTransparency)
+        validate = 1;
+    else if(request->method != METHOD_HEAD &&
+            !objectHasData(object, request->from, request->to) &&
+            !(object->flags & OBJECT_INPROGRESS))
+        validate = 1;
+    else if(objectMustRevalidate((relaxTransparency <= 1 ? 
+                                  request->object : NULL),
+                                 &request->cache_control))
+        validate = 1;
+    else
+        validate = 0;
+
+    if(request->cache_control.flags & CACHE_ONLY_IF_CACHED) {
+        validate = 0;
+        if(!haveData) {
+            if(serveNow) {
+                connection->flags |= CONN_WRITER;
+                return httpClientRawError(connection, 504,
+                                          internAtom("Object not in cache"),
+                                          0);
+            } else
+                return 1;
+        }
+    }
+
+    if(!(request->object->flags & OBJECT_VALIDATING) &&
+       ((!validate && haveData) ||
+        (request->object->flags & OBJECT_FAILED))) {
+        if(serveNow) {
+            connection->flags |= CONN_WRITER;
+            lockChunk(request->object, request->from / CHUNK_SIZE);
+            return httpServeObject(connection);
+        } else {
+            return 1;
+        }
+    }
+
+    if((request->flags & REQUEST_REQUESTED) &&
+       !(request->object->flags & OBJECT_INPROGRESS)) {
+        /* This can happen either because the server side ran out of
+           memory, or because it is using HEAD validation.  We mark
+           the object to be fetched again. */
+        request->flags &= ~REQUEST_REQUESTED;
+    }
+
+    if(serveNow) {
+        connection->flags |= CONN_WRITER;
+        if(!local && proxyOffline)
+            return httpClientRawError(connection, 502, 
+                                      internAtom("Disconnected operation "
+                                                 "and object not in cache"),
+                                      0);
+        request->chandler =
+            conditionWait(&request->object->condition, httpClientGetHandler, 
+                          sizeof(request), &request);
+        if(request->chandler == NULL) {
+            do_log(L_ERROR, "Couldn't register condition handler.\n");
+            return httpClientRawError(connection, 503,
+                                      internAtom("Couldn't register "
+                                                 "condition handler"), 0);
+        }
+    }
+
+    if(request->object->flags & OBJECT_VALIDATING)
+        return 1;
+
+    conditional = (haveData && request->method == METHOD_GET);
+    if(!mindlesslyCacheVary && (request->object->cache_control & CACHE_VARY))
+        conditional = conditional && (request->object->etag != NULL);
+
+    conditional =
+        conditional && !(request->object->cache_control & CACHE_MISMATCH);
+
+    if(!(request->object->flags & OBJECT_INPROGRESS))
+        request->object->flags |= OBJECT_VALIDATING;
+    rc = request->object->request(request->object,
+                                  conditional ? METHOD_CONDITIONAL_GET : 
+                                  request->method,
+                                  request->from, request->to, request,
+                                  request->object->request_closure);
+    if(rc < 0) {
+        if(request->chandler)
+            unregisterConditionHandler(request->chandler);
+        request->chandler = NULL;
+        request->object->flags &= ~OBJECT_VALIDATING;
+        request->object->flags |= OBJECT_FAILED;
+        if(request->error_message)
+            releaseAtom(request->error_message);
+        request->error_code = 503;
+        request->error_message = internAtom("Couldn't schedule get");
+    }
+    return 1;
+}
+
+static int
+httpClientNoticeRequestDelayed(TimeEventHandlerPtr event)
+{
+    HTTPRequestPtr request = *(HTTPRequestPtr*)event->data;
+    httpClientNoticeRequest(request, 0);
+    return 1;
+}
+
+int
+delayedHttpClientNoticeRequest(HTTPRequestPtr request)
+{
+    TimeEventHandlerPtr event;
+    event = scheduleTimeEvent(-1, httpClientNoticeRequestDelayed,
+                              sizeof(request), &request);
+    if(!event)
+        return -1;
+    return 1;
+}
+
+int
+httpClientContinueDelayed(TimeEventHandlerPtr event)
+{
+    static char httpContinue[] = "HTTP/1.1 100 Continue\r\n\r\n";
+    HTTPConnectionPtr connection = *(HTTPConnectionPtr*)event->data;
+
+    do_stream(IO_WRITE, connection->fd, 0, httpContinue, 25,
+              httpErrorNofinishStreamHandler, connection);
+    return 1;
+}
+
+int
+delayedHttpClientContinue(HTTPConnectionPtr connection)
+{
+    TimeEventHandlerPtr event;
+    event = scheduleTimeEvent(-1, httpClientContinueDelayed,
+                              sizeof(connection), &connection);
+    if(!event)
+        return -1;
+    return 1;
+}
+
+int
+httpClientGetHandler(int status, ConditionHandlerPtr chandler)
+{
+    HTTPRequestPtr request = *(HTTPRequestPtr*)chandler->data;
+    HTTPConnectionPtr connection = request->connection;
+    ObjectPtr object = request->object;
+    int rc;
+
+    assert(request == connection->request);
+
+    if(request->request) {
+        assert(request->object->flags & OBJECT_INPROGRESS);
+        assert(!request->request->object ||
+               request->request->object == request->object);
+    }
+
+    if(status < 0) {
+        object->flags &= ~OBJECT_VALIDATING; /* for now */
+        if(request->request && request->request->request == request)
+            httpServerClientReset(request->request);
+        lockChunk(object, request->from / CHUNK_SIZE);
+        request->chandler = NULL;
+        rc = delayedHttpServeObject(connection);
+        if(rc < 0) {
+            unlockChunk(object, request->from / CHUNK_SIZE);
+            do_log(L_ERROR, "Couldn't schedule serving.\n");
+            abortObject(object, 503, internAtom("Couldn't schedule serving"));
+        }
+        return 1;
+    }
+
+    if(object->flags & OBJECT_VALIDATING)
+        return 0;
+
+    if(request->error_code) {
+        lockChunk(object, request->from / CHUNK_SIZE);
+        request->chandler = NULL;
+        rc = delayedHttpServeObject(connection);
+        if(rc < 0) {
+            unlockChunk(object, request->from / CHUNK_SIZE);
+            do_log(L_ERROR, "Couldn't schedule serving.\n");
+            abortObject(object, 503, internAtom("Couldn't schedule serving"));
+        }
+        return 1;
+    }
+
+    if(request->flags & REQUEST_WAIT_CONTINUE) {
+        if(request->request && 
+           !(request->request->flags & REQUEST_WAIT_CONTINUE)) {
+            request->flags &= ~REQUEST_WAIT_CONTINUE;
+            delayedHttpClientContinue(connection);
+        }
+        return 0;
+    }
+
+    /* See httpServerHandlerHeaders */
+    if((object->flags & OBJECT_SUPERSEDED) &&
+       /* Avoid superseding loops. */
+       !(request->flags & REQUEST_SUPERSEDED) &&
+       request->request && request->request->can_mutate) {
+        ObjectPtr new_object = retainObject(request->request->can_mutate);
+        if(object->requestor == request) {
+            if(new_object->requestor == NULL)
+                new_object->requestor = request;
+            object->requestor = NULL;
+            /* Avoid superseding the same request more than once. */
+            request->flags |= REQUEST_SUPERSEDED;
+        }
+        request->chandler = NULL;
+        releaseObject(object);
+        request->object = new_object;
+        request->request->object = new_object;
+        /* We're handling the wrong object now.  It's simpler to
+           rebuild the whole data structure from scratch rather than
+           trying to compensate. */
+        rc = delayedHttpClientNoticeRequest(request);
+        if(rc < 0) {
+            do_log(L_ERROR, "Couldn't schedule noticing of request.");
+            abortObject(object, 500,
+                        internAtom("Couldn't schedule "
+                                   "noticing of request"));
+            /* We're probably out of memory.  What can we do? */
+            shutdown(connection->fd, 1);
+        }
+        return 1;
+    }
+
+    if(object->requestor != request && !(object->flags & OBJECT_ABORTED)) {
+        /* Make sure we don't serve an object that is stale for us
+           unless we're the requestor. */
+        if((object->flags & (OBJECT_LINEAR | OBJECT_MUTATING)) ||
+           objectMustRevalidate(object, &request->cache_control)) {
+           if(object->flags & OBJECT_INPROGRESS)
+               return 0;
+           rc = delayedHttpClientNoticeRequest(request);
+           if(rc < 0) {
+               do_log(L_ERROR, "Couldn't schedule noticing of request.");
+               abortObject(object, 500,
+                           internAtom("Couldn't schedule "
+                                      "noticing of request"));
+           } else {
+               request->chandler = NULL;
+               return 1;
+           }
+        }
+    }
+
+    if(object->flags & (OBJECT_INITIAL | OBJECT_VALIDATING)) {
+        if(object->flags & (OBJECT_INPROGRESS | OBJECT_VALIDATING)) {
+            return 0;
+        } else if(object->flags & OBJECT_FAILED) {
+            if(request->error_code)
+                abortObject(object, 
+                            request->error_code, 
+                            retainAtom(request->error_message));
+            else {
+                abortObject(object, 500,
+                            internAtom("Error message lost in transit"));
+            }
+        } else {
+            /* The request was pruned by httpServerDiscardRequests */
+            if(chandler == request->chandler) {
+                int rc;
+                request->chandler = NULL;
+                rc = delayedHttpClientNoticeRequest(request);
+                if(rc < 0)
+                    abortObject(object, 500,
+                                internAtom("Couldn't allocate "
+                                           "delayed notice request"));
+                else
+                    return 1;
+            } else {
+                abortObject(object, 500,
+                            internAtom("Wrong request pruned -- "
+                                       "this shouldn't happen"));
+            }
+        }
+    }
+
+    if(request->object->flags & OBJECT_DYNAMIC) {
+        if(objectHoleSize(request->object, 0) == 0) {
+            request->from = 0;
+            request->to = -1;
+        } else {
+            /* We really should request again if that is not the case */
+        }
+    }
+
+    lockChunk(object, request->from / CHUNK_SIZE);
+    request->chandler = NULL;
+    rc = delayedHttpServeObject(connection);
+    if(rc < 0) {
+        unlockChunk(object, request->from / CHUNK_SIZE);
+        do_log(L_ERROR, "Couldn't schedule serving.\n");
+        abortObject(object, 503, internAtom("Couldn't schedule serving"));
+    }
+    return 1;
+}
+
+int
+httpClientSideRequest(HTTPRequestPtr request)
+{
+    HTTPConnectionPtr connection = request->connection;
+
+    if(request->from < 0 || request->to >= 0) {
+        httpClientNoticeError(request, 501,
+                              internAtom("Partial requests not implemented"));
+        httpClientDiscardBody(connection);
+        return 1;
+    }
+    if(connection->reqte != TE_IDENTITY) {
+        httpClientNoticeError(request, 501,
+                              internAtom("Chunked requests not implemented"));
+        httpClientDiscardBody(connection);
+        return 1;
+    }
+    if(connection->bodylen < 0) {
+        httpClientNoticeError(request, 502,
+                              internAtom("POST or PUT without "
+                                         "Content-Length"));
+        httpClientDiscardBody(connection);
+        return 1;
+    }
+    if(connection->reqlen < 0) {
+        httpClientNoticeError(request, 502,
+                              internAtom("Incomplete POST or PUT"));
+        httpClientDiscardBody(connection);
+        return 1;
+    }
+        
+    return httpClientNoticeRequest(request, 0);
+}
+
+int 
+httpClientSideHandler(int status,
+                      FdEventHandlerPtr event,
+                      StreamRequestPtr srequest)
+{
+    HTTPConnectionPtr connection = srequest->data;
+    HTTPRequestPtr request = connection->request;
+    HTTPRequestPtr requestee;
+    HTTPConnectionPtr server;
+    int push;
+    int code;
+    AtomPtr message = NULL;
+
+    assert(connection->flags & CONN_SIDE_READER);
+
+    if((request->object->flags & OBJECT_ABORTED) || 
+       !(request->object->flags & OBJECT_INPROGRESS)) {
+        code = request->object->code;
+        message = retainAtom(request->object->message);
+        goto fail;
+    }
+        
+    if(status < 0) {
+        do_log_error(L_ERROR, -status, "Reading from client");
+        code = 502;
+        message = internAtomError(-status, "Couldn't read from client");
+        goto fail;
+    }
+
+    requestee = request->request;
+    server = requestee->connection;
+
+    push = MIN(srequest->offset - connection->reqlen, 
+               connection->bodylen - connection->reqoffset);
+    if(push > 0) {
+        connection->reqlen += push;
+        httpServerDoSide(server);
+        return 1;
+    }
+
+    if(server->reqoffset >= connection->bodylen) {
+        connection->flags &= ~(CONN_READER | CONN_SIDE_READER);
+        return 1;
+    }
+
+    assert(status);
+    do_log(L_ERROR, "Incomplete client request.\n");
+    code = 502;
+    message = internAtom("Incomplete client request");
+
+ fail:
+    request->error_code = code;
+    if(request->error_message)
+        releaseAtom(request->error_message);
+    request->error_message = message;
+    if(request->error_headers)
+        releaseAtom(request->error_headers);
+    request->error_headers = NULL;
+
+    if(request->request) {
+        shutdown(request->request->connection->fd, 2);
+        pokeFdEvent(request->request->connection->fd, -ESHUTDOWN, POLLOUT);
+    }
+    notifyObject(request->object);
+    connection->flags &= ~CONN_SIDE_READER;
+    httpClientDiscardBody(connection);
+    return 1;
+}
+
+int 
+httpServeObject(HTTPConnectionPtr connection)
+{
+    HTTPRequestPtr request = connection->request;
+    ObjectPtr object = request->object;
+    int i = request->from / CHUNK_SIZE;
+    int j = request->from % CHUNK_SIZE;
+    int n, len, rc;
+    int bufsize = CHUNK_SIZE;
+    int condition_result;
+
+    object->atime = current_time.tv_sec;
+    objectMetadataChanged(object, 0);
+
+    httpSetTimeout(connection, -1);
+
+    if((request->error_code && relaxTransparency <= 0) ||
+       object->flags & OBJECT_INITIAL) {
+        object->flags &= ~OBJECT_FAILED;
+        unlockChunk(object, i);
+        if(request->error_code)
+            return httpClientRawError(connection,
+                                      request->error_code, 
+                                      retainAtom(request->error_message), 0);
+        else
+            return httpClientRawError(connection,
+                                      500, internAtom("Object vanished."), 0);
+    }
+
+    if(!(object->flags & OBJECT_INPROGRESS) && object->code == 0) {
+        if(object->flags & OBJECT_INITIAL) {
+            unlockChunk(object, i);
+            return httpClientRawError(connection, 503,
+                                      internAtom("Error message lost"), 0);
+                                      
+        } else {
+            unlockChunk(object, i);
+            do_log(L_ERROR, "Internal proxy error: object has no code.\n");
+            return httpClientRawError(connection, 500,
+                                      internAtom("Internal proxy error: "
+                                                 "object has no code"), 0);
+        }
+    }
+
+    condition_result = httpCondition(object, request->condition);
+
+    if(condition_result == CONDITION_FAILED) {
+        unlockChunk(object, i);
+        return httpClientRawError(connection, 412,
+                                  internAtom("Precondition failed"), 0);
+    } else if(condition_result == CONDITION_NOT_MODIFIED) {
+        unlockChunk(object, i);
+        return httpClientRawError(connection, 304,
+                                  internAtom("Not modified"), 0);
+    }
+
+    objectFillFromDisk(object, request->from,
+                       (request->method == METHOD_HEAD ||
+                        condition_result != CONDITION_MATCH) ? 0 : 1);
+
+    if(((object->flags & OBJECT_LINEAR) &&
+        (object->requestor != connection->request)) ||
+       ((object->flags & OBJECT_SUPERSEDED) &&
+        !(object->flags & OBJECT_LINEAR))) {
+        if(request->request) {
+            request->request->request = NULL;
+            request->request = NULL;
+            request->object->requestor = NULL;
+        }
+        object = makeObject(OBJECT_HTTP,
+                            object->key, object->key_size, 1, 0,
+                            object->request, NULL);
+        if(request->object->requestor == request)
+            request->object->requestor = NULL;
+        unlockChunk(request->object, i);
+        releaseObject(request->object);
+        request->object = NULL;
+        if(object == NULL) {
+            do_log(L_ERROR, "Couldn't allocate object.");
+            return httpClientRawError(connection, 501,
+                                      internAtom("Couldn't allocate object"), 
+                                      1);
+        }
+        if(urlIsLocal(object->key, object->key_size)) {
+            object->flags |= OBJECT_LOCAL;
+            object->request = httpLocalRequest;
+        }
+        request->object = object;
+        connection->flags &= ~CONN_WRITER;
+        return httpClientNoticeRequest(request, 1);
+    }
+
+    if(object->flags & OBJECT_ABORTED) {
+        unlockChunk(object, i);
+        return httpClientNoticeError(request, object->code, 
+                                     retainAtom(object->message));
+    }
+
+    if(connection->buf == NULL)
+        connection->buf = get_chunk();
+    if(connection->buf == NULL) {
+        unlockChunk(object, i);
+        do_log(L_ERROR, "Couldn't allocate client buffer.\n");
+        connection->flags &= ~CONN_WRITER;
+        httpClientFinish(connection, 1);
+        return 1;
+    }
+
+    if(object->length >= 0 && request->to >= object->length)
+        request->to = object->length;
+
+    if(request->from > 0 || request->to >= 0) {
+        if(request->method == METHOD_HEAD) {
+            request->to = request->from;
+        } else if(request->to < 0) {
+            if(object->length >= 0)
+                request->to = object->length;
+        }
+    }
+
+ again:
+
+    connection->len = 0;
+
+    if((request->from <= 0 && request->to < 0) || 
+       request->method == METHOD_HEAD) {
+        n = snnprintf(connection->buf, 0, bufsize,
+                      "HTTP/1.1 %d %s",
+                      object->code, atomString(object->message));
+    } else {
+        if((object->length >= 0 && request->from >= object->length) ||
+           (request->to >= 0 && request->from >= request->to)) {
+            unlockChunk(object, i);
+            return httpClientRawError(connection, 416,
+                                      internAtom("Requested range "
+                                                 "not satisfiable"),
+                                      0);
+        } else {
+            n = snnprintf(connection->buf, 0, bufsize,
+                          "HTTP/1.1 206 Partial content");
+        }
+    }
+
+    n = httpWriteObjectHeaders(connection->buf, n, bufsize,
+                               object, request->from, request->to);
+    if(n < 0)
+        goto fail;
+
+    if(request->method != METHOD_HEAD && 
+       condition_result != CONDITION_NOT_MODIFIED &&
+       request->to < 0 && object->length < 0) {
+        if(connection->version == HTTP_11) {
+            connection->te = TE_CHUNKED;
+            n = snnprintf(connection->buf, n, bufsize,
+                          "\r\nTransfer-Encoding: chunked");
+        } else {
+            request->flags &= ~REQUEST_PERSISTENT;
+        }
+    }
+        
+    if(object->age < current_time.tv_sec) {
+        n = snnprintf(connection->buf, n, bufsize,
+                      "\r\nAge: %d",
+                      (int)(current_time.tv_sec - object->age));
+    }
+    n = snnprintf(connection->buf, n, bufsize,
+                  "\r\nConnection: %s",
+                  (request->flags & REQUEST_PERSISTENT) ? 
+                  "keep-alive" : "close");
+
+    if(!(object->flags & OBJECT_LOCAL)) {
+        if((object->flags & OBJECT_FAILED) && !proxyOffline) {
+            n = snnprintf(connection->buf, n, bufsize,
+                          "\r\nWarning: 111 %s:%d \"Revalidation failed\"",
+                          proxyName->string, proxyPort);
+            if(request->error_code)
+                n = snnprintf(connection->buf, n, bufsize,
+                              " (%d %s)",
+                              request->error_code, 
+                              atomString(request->error_message));
+            object->flags &= ~OBJECT_FAILED;
+        } else if(proxyOffline && 
+                  objectMustRevalidate(object, &request->cache_control)) {
+            n = snnprintf(connection->buf, n, bufsize,
+                          "\r\nWarning: 112 %s:%d \"Disconnected operation\"",
+                          proxyName->string, proxyPort);
+        } else if(objectIsStale(object, &request->cache_control)) {
+            n = snnprintf(connection->buf, n, bufsize,
+                          "\r\nWarning: 110 %s:%d \"Object is stale\"",
+                          proxyName->string, proxyPort);
+        } else if(object->expires < 0 && object->max_age < 0 &&
+                  object->age < current_time.tv_sec - 24 * 3600) {
+            n = snnprintf(connection->buf, n, bufsize,
+                          "\r\nWarning: 113 %s:%d \"Heuristic expiration\"",
+                          proxyName->string, proxyPort);
+        }
+    }
+
+    n = snnprintf(connection->buf, n, bufsize, "\r\n\r\n");
+    
+    if(n < 0)
+        goto fail;
+    
+    connection->offset = request->from;
+
+    if(request->method == METHOD_HEAD || 
+       condition_result == CONDITION_NOT_MODIFIED ||
+       (object->flags & OBJECT_ABORTED)) {
+        len = 0;
+    } else {
+        if(i < object->numchunks) {
+            if(object->chunks[i].size <= j)
+                len = 0;
+            else
+                len = object->chunks[i].size - j;
+        } else {
+            len = 0;
+        }
+        if(request->to >= 0)
+            len = MIN(len, request->to - request->from);
+    }
+
+    connection->offset = request->from;
+    httpSetTimeout(connection, clientTimeout);
+    do_log(D_CLIENT_DATA, "Serving on 0x%lx for 0x%lx: offset %d len %d\n",
+           (unsigned long)connection, (unsigned long)object,
+           connection->offset, len);
+    do_stream_h(IO_WRITE |
+                (connection->te == TE_CHUNKED && len > 0 ? IO_CHUNKED : 0),
+                connection->fd, 0, 
+                connection->buf, n,
+                object->chunks[i].data + j, len,
+                httpServeObjectStreamHandler, connection);
+    return 1;
+
+ fail:
+    rc = 0;
+    connection->len = 0;
+    if(!(connection->flags & CONN_BIGBUF))
+        rc = httpConnectionBigify(connection);
+    if(rc > 0) {
+        bufsize = bigBufferSize;
+        goto again;
+    }
+    unlockChunk(object, i);
+    return httpClientRawError(connection, 500,
+                              rc == 0 ?
+                              internAtom("No space for headers") :
+                              internAtom("Couldn't allocate big buffer"), 0);
+}
+
+static int
+httpServeObjectDelayed(TimeEventHandlerPtr event)
+{
+    HTTPConnectionPtr connection = *(HTTPConnectionPtr*)event->data;
+    httpServeObject(connection);
+    return 1;
+}
+
+int
+delayedHttpServeObject(HTTPConnectionPtr connection)
+{
+    TimeEventHandlerPtr event;
+
+    assert(connection->request->object->chunks[connection->request->from / 
+                                               CHUNK_SIZE].locked > 0);
+
+    event = scheduleTimeEvent(-1, httpServeObjectDelayed,
+                              sizeof(connection), &connection);
+    if(!event) return -1;
+    return 1;
+}
+
+static int
+httpServeObjectFinishHandler(int status,
+                             FdEventHandlerPtr event,
+                             StreamRequestPtr srequest)
+{
+    HTTPConnectionPtr connection = srequest->data;
+    HTTPRequestPtr request = connection->request;
+
+    (void)request;
+    assert(!request->chandler);
+
+    if(status == 0 && !streamRequestDone(srequest))
+        return 0;
+
+    httpSetTimeout(connection, -1);
+
+    if(status < 0) {
+        do_log(L_ERROR, "Couldn't terminate chunked reply\n");
+        httpClientFinish(connection, 1);
+    } else {
+        httpClientFinish(connection, 0);
+    }
+    return 1;
+}
+
+int
+httpServeChunk(HTTPConnectionPtr connection)
+{
+    HTTPRequestPtr request = connection->request;
+    ObjectPtr object = request->object;
+    int i = connection->offset / CHUNK_SIZE;
+    int j = connection->offset - (i * CHUNK_SIZE);
+    int to, len, len2, end;
+    int rc;
+
+    /* This must be called with chunk i locked. */
+    assert(object->chunks[i].locked > 0);
+
+    if(object->flags & OBJECT_ABORTED)
+        goto fail;
+
+    if(object->length >= 0 && request->to >= 0)
+        to = MIN(request->to, object->length);
+    else if(object->length >= 0)
+        to = object->length;
+    else if(request->to >= 0)
+        to = request->to;
+    else
+        to = -1;
+
+    len = 0;
+    if(i < object->numchunks)
+        len = object->chunks[i].size - j;
+
+    if(request->method != METHOD_HEAD && 
+       len < CHUNK_SIZE && connection->offset + len < to) {
+        objectFillFromDisk(object, connection->offset + len, 2);
+        len = object->chunks[i].size - j;
+    }
+
+    if(to >= 0)
+        len = MIN(len, to - connection->offset);
+
+    if(len <= 0) {
+        if(to >= 0 && connection->offset >= to) {
+            if(request->chandler) {
+                unregisterConditionHandler(request->chandler);
+                request->chandler = NULL;
+            }
+            unlockChunk(object, i);
+            if(connection->te == TE_CHUNKED) {
+                httpSetTimeout(connection, clientTimeout);
+                do_stream(IO_WRITE | IO_CHUNKED | IO_END,
+                          connection->fd, 0, NULL, 0,
+                          httpServeObjectFinishHandler, connection);
+            } else {
+                httpClientFinish(connection,
+                                 !(object->length >= 0 &&
+                                   connection->offset >= object->length));
+            }
+            return 1;
+        } else {
+            if(!request->chandler) {
+                request->chandler =
+                    conditionWait(&object->condition, 
+                                  httpServeObjectHandler,
+                                  sizeof(connection), &connection);
+                if(!request->chandler) {
+                    do_log(L_ERROR, "Couldn't register condition handler\n");
+                    goto fail;
+                }
+            }
+            if(!(object->flags & OBJECT_INPROGRESS)) {
+                if(object->flags & OBJECT_SUPERSEDED) {
+                    goto fail;
+                }
+                if(REQUEST_SIDE(request)) goto fail;
+                rc = object->request(object, request->method,
+                                     connection->offset, -1, request,
+                                     object->request_closure);
+                if(rc <= 0) goto fail;
+            }
+            return 1;
+        }
+    } else {
+        /* len > 0 */
+        if(request->method != METHOD_HEAD)
+            objectFillFromDisk(object, (i + 1) * CHUNK_SIZE, 1);
+        if(request->chandler) {
+            unregisterConditionHandler(request->chandler);
+            request->chandler = NULL;
+        }
+        len2 = 0;
+        if(j + len == CHUNK_SIZE && object->numchunks > i + 1) {
+            len2 = object->chunks[i + 1].size;
+            if(to >= 0)
+                len2 = MIN(len2, to - (i + 1) * CHUNK_SIZE);
+        }
+        /* Lock early -- httpServerRequest may get_chunk */
+        if(len2 > 0)
+            lockChunk(object, i + 1);
+        if(object->length >= 0 && 
+           connection->offset + len + len2 == object->length)
+            end = 1;
+        else
+            end = 0;
+        /* Prefetch */
+        if(!(object->flags & OBJECT_INPROGRESS) && !REQUEST_SIDE(request)) {
+            if(object->chunks[i].size < CHUNK_SIZE &&
+               to >= 0 && connection->offset + len + 1 < to)
+                object->request(object, request->method,
+                                connection->offset + len, -1, request,
+                                object->request_closure);
+            else if(i + 1 < object->numchunks &&
+                    object->chunks[i + 1].size == 0 &&
+                    to >= 0 && (i + 1) * CHUNK_SIZE + 1 < to)
+                object->request(object, request->method,
+                                (i + 1) * CHUNK_SIZE, -1, request,
+                                object->request_closure);
+        }
+        if(len2 == 0) {
+            httpSetTimeout(connection, clientTimeout);
+            do_log(D_CLIENT_DATA, 
+                   "Serving on 0x%lx for 0x%lx: offset %d len %d\n",
+                   (unsigned long)connection, (unsigned long)object,
+                   connection->offset, len);
+            /* IO_NOTNOW in order to give other clients a chance to run. */
+            do_stream(IO_WRITE | IO_NOTNOW |
+                      (connection->te == TE_CHUNKED ? IO_CHUNKED : 0) |
+                      (end ? IO_END : 0),
+                      connection->fd, 0, 
+                      object->chunks[i].data + j, len,
+                      httpServeObjectStreamHandler, connection);
+        } else {
+            httpSetTimeout(connection, clientTimeout);
+            do_log(D_CLIENT_DATA, 
+                   "Serving on 0x%lx for 0x%lx: offset %d len %d + %d\n",
+                   (unsigned long)connection, (unsigned long)object,
+                   connection->offset, len, len2);
+            do_stream_2(IO_WRITE | IO_NOTNOW |
+                        (connection->te == TE_CHUNKED ? IO_CHUNKED : 0) |
+                        (end ? IO_END : 0),
+                        connection->fd, 0, 
+                        object->chunks[i].data + j, len,
+                        object->chunks[i + 1].data, len2,
+                        httpServeObjectStreamHandler2, connection);
+        }            
+        return 1;
+    }
+
+    abort();
+
+ fail:
+    unlockChunk(object, i);
+    if(request->chandler)
+        unregisterConditionHandler(request->chandler);
+    request->chandler = NULL;
+    httpClientFinish(connection, 1);
+    return 1;
+}
+
+static int
+httpServeChunkDelayed(TimeEventHandlerPtr event)
+{
+    HTTPConnectionPtr connection = *(HTTPConnectionPtr*)event->data;
+    httpServeChunk(connection);
+    return 1;
+}
+
+int
+delayedHttpServeChunk(HTTPConnectionPtr connection)
+{
+    TimeEventHandlerPtr event;
+    event = scheduleTimeEvent(-1, httpServeChunkDelayed,
+                              sizeof(connection), &connection);
+    if(!event) return -1;
+    return 1;
+}
+
+int
+httpServeObjectHandler(int status, ConditionHandlerPtr chandler)
+{
+    HTTPConnectionPtr connection = *(HTTPConnectionPtr*)chandler->data;
+    HTTPRequestPtr request = connection->request;
+    int rc;
+
+    if((request->object->flags & OBJECT_ABORTED) || status < 0) {
+        shutdown(connection->fd, 1);
+        httpSetTimeout(connection, 10);
+        /* httpServeChunk will take care of the error. */
+    }
+
+    httpSetTimeout(connection, -1);
+
+    request->chandler = NULL;
+    rc = delayedHttpServeChunk(connection);
+    if(rc < 0) {
+        do_log(L_ERROR, "Couldn't schedule serving.\n");
+        abortObject(request->object, 503, 
+                    internAtom("Couldn't schedule serving"));
+    }
+    return 1;
+}
+
+static int
+httpServeObjectStreamHandlerCommon(int kind, int status,
+                                   FdEventHandlerPtr event,
+                                   StreamRequestPtr srequest)
+{
+    HTTPConnectionPtr connection = srequest->data;
+    HTTPRequestPtr request = connection->request;
+    int condition_result = httpCondition(request->object, request->condition);
+    int i = connection->offset / CHUNK_SIZE;
+
+    assert(!request->chandler);
+
+    if(status == 0 && !streamRequestDone(srequest)) {
+        httpSetTimeout(connection, clientTimeout);
+        return 0;
+    }
+
+    httpSetTimeout(connection, -1);
+
+    unlockChunk(request->object, i);
+    if(kind == 2)
+        unlockChunk(request->object, i + 1);
+
+    if(status) {
+        if(status < 0) {
+            do_log_error(status == -ECONNRESET ? D_IO : L_ERROR, 
+                         -status, "Couldn't write to client");
+            if(status == -EIO || status == -ESHUTDOWN)
+                httpClientFinish(connection, 2);
+            else
+                httpClientFinish(connection, 1);
+        } else {
+            do_log(D_IO, "Couldn't write to client: short write.\n");
+            httpClientFinish(connection, 2);
+        }
+        return 1;
+    }
+
+    if(srequest->operation & IO_CHUNKED) {
+        assert(srequest->offset > 2);
+        connection->offset += srequest->offset - 2;
+    } else {
+        connection->offset += srequest->offset;
+    }
+    request->flags &= ~REQUEST_REQUESTED;
+
+    if(request->object->flags & OBJECT_ABORTED) {
+        httpClientFinish(connection, 1);
+        return 1;
+    }
+
+    if(connection->request->method == METHOD_HEAD ||
+       condition_result == CONDITION_NOT_MODIFIED) {
+        httpClientFinish(connection, 0);
+        return 1;
+    }
+
+    if(srequest->operation & IO_END)
+        httpClientFinish(connection, 0);
+    else {
+        httpConnectionDestroyBuf(connection);
+        lockChunk(connection->request->object,
+                  connection->offset / CHUNK_SIZE);
+        httpServeChunk(connection);
+    }
+    return 1;
+}
+
+int
+httpServeObjectStreamHandler(int status,
+                             FdEventHandlerPtr event,
+                             StreamRequestPtr srequest)
+{
+    return httpServeObjectStreamHandlerCommon(1, status, event, srequest);
+}
+
+int
+httpServeObjectStreamHandler2(int status,
+                              FdEventHandlerPtr event,
+                              StreamRequestPtr srequest)
+{
+    return httpServeObjectStreamHandlerCommon(2, status, event, srequest);
+}

+ 64 - 0
polipo/client.h

@@ -0,0 +1,64 @@
+/*
+Copyright (c) 2003-2006 by Juliusz Chroboczek
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+int httpAccept(int, FdEventHandlerPtr, AcceptRequestPtr);
+void httpClientFinish(HTTPConnectionPtr connection, int s);
+int httpClientHandler(int, FdEventHandlerPtr, StreamRequestPtr);
+int httpClientNoticeError(HTTPRequestPtr, int code, struct _Atom *message);
+int httpClientError(HTTPRequestPtr, int code, struct _Atom *message);
+int httpClientNewError(HTTPConnectionPtr, int method, int persist, 
+                       int code, struct _Atom *message);
+int httpClientRawError(HTTPConnectionPtr, int, struct _Atom*, int close);
+int httpErrorStreamHandler(int status,
+                           FdEventHandlerPtr event,
+                           StreamRequestPtr request);
+int httpErrorNocloseStreamHandler(int status,
+                                  FdEventHandlerPtr event,
+                                  StreamRequestPtr request);
+int httpErrorNofinishStreamHandler(int status,
+                                   FdEventHandlerPtr event,
+                                   StreamRequestPtr request);
+int httpClientRequest(HTTPRequestPtr request, AtomPtr url);
+int httpClientRequestContinue(int forbidden_code, AtomPtr url,
+                              AtomPtr forbidden_message,
+                              AtomPtr forbidden_headers,
+                              void *closure);
+int httpClientDiscardBody(HTTPConnectionPtr connection);
+int httpClientDiscardHandler(int, FdEventHandlerPtr, StreamRequestPtr);
+int httpClientGetHandler(int, ConditionHandlerPtr);
+int httpClientHandlerHeaders(FdEventHandlerPtr event, 
+                                StreamRequestPtr request,
+                                HTTPConnectionPtr connection);
+int httpClientNoticeRequest(HTTPRequestPtr request, int);
+int httpServeObject(HTTPConnectionPtr);
+int delayedHttpServeObject(HTTPConnectionPtr connection);
+int httpServeObjectStreamHandler(int status, 
+                                 FdEventHandlerPtr event,
+                                 StreamRequestPtr request);
+int httpServeObjectStreamHandler2(int status, 
+                                  FdEventHandlerPtr event,
+                                  StreamRequestPtr request);
+int httpServeObjectHandler(int, ConditionHandlerPtr);
+int httpClientSideRequest(HTTPRequestPtr request);
+int  httpClientSideHandler(int status,
+                           FdEventHandlerPtr event,
+                           StreamRequestPtr srequest);

+ 869 - 0
polipo/config.c

@@ -0,0 +1,869 @@
+/*
+Copyright (c) 2003-2006 by Juliusz Chroboczek
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+#include "polipo.h"
+
+ConfigVariablePtr configVariables = NULL;
+
+static ConfigVariablePtr
+findConfigVariable(AtomPtr name)
+{
+    ConfigVariablePtr var;
+    var = configVariables;
+    while(var != NULL) {
+        if(var->name == name)
+            break;
+        var = var->next;
+    }
+    return var;
+}
+
+void
+declareConfigVariable(AtomPtr name, int type, void *value, 
+                      int (*setter)(ConfigVariablePtr, void*), char *help)
+{
+    ConfigVariablePtr var, previous, next;
+
+    var = findConfigVariable(name);
+
+    if(var) {
+        do_log(L_ERROR, 
+               "Configuration variable %s declared multiple times.\n",
+               name->string);
+        if(var->type != type) {
+            exit(1);
+        }
+    }
+
+    var = malloc(sizeof(ConfigVariableRec));
+    if(var == NULL) {
+        do_log(L_ERROR, "Couldn't allocate config variable.\n");
+        exit(1);
+    }
+
+    var->name = retainAtom(name);
+    var->type = type;
+    switch(type) {
+    case CONFIG_INT: case CONFIG_OCTAL: case CONFIG_HEX: case CONFIG_TIME:
+    case CONFIG_BOOLEAN: case CONFIG_TRISTATE: case CONFIG_TETRASTATE:
+    case CONFIG_PENTASTATE:
+        var->value.i = value; break;
+    case CONFIG_FLOAT: var->value.f = value; break;
+    case CONFIG_ATOM: case CONFIG_ATOM_LOWER: case CONFIG_PASSWORD:
+        var->value.a = value; break;
+    case CONFIG_INT_LIST:
+        var->value.il = value; break;
+    case CONFIG_ATOM_LIST: case CONFIG_ATOM_LIST_LOWER: 
+        var->value.al = value; break;
+    default: abort();
+    }
+    var->setter = setter;
+    var->help = help;
+
+    previous = NULL;
+    next = configVariables;
+    while(next && strcmp(next->name->string, var->name->string) < 0) {
+        previous = next;
+        next = next->next;
+    }
+    if(next && strcmp(next->name->string, var->name->string) == 0) {
+        do_log(L_ERROR, "Variable %s declared multiple times.\n",
+               next->name->string);
+        abort();
+    }
+    if(previous == NULL) {
+        var->next = configVariables;
+        configVariables = var;
+    } else {
+        var->next = next;
+        previous->next = var;
+    }
+}
+
+static void
+printString(FILE *out, char *string, int html)
+{
+    if(html) {
+        char buf[512];
+        int i;
+        i = htmlString(buf, 0, 512, string, strlen(string));
+        if(i < 0) {
+            fprintf(out, "(overflow)");
+            return;
+        }
+        fwrite(buf, 1, i, out);
+    } else {
+        fprintf(out, "%s", string);
+    }
+}
+
+static void
+printVariable(FILE *out, ConfigVariablePtr var, int html, int parseable)
+{
+    int i;
+
+    switch(var->type) {
+    case CONFIG_INT: fprintf(out, "%d", *var->value.i); break;
+    case CONFIG_OCTAL: fprintf(out, "0%o", *var->value.i); break;
+    case CONFIG_HEX: fprintf(out, "0x%x", *var->value.i); break;
+    case CONFIG_TIME:
+        {
+            int v = *var->value.i;
+            if(v == 0) {
+                fprintf(out, "0s");
+            } else {
+                if(v >= 3600 * 24) fprintf(out, "%dd", v/(3600*24));
+                v = v % (3600 * 24);
+                if(v >= 3600) fprintf(out, "%dh", v / 3600);
+                v = v % 3600;
+                if(v >= 60) fprintf(out, "%dm", v / 60);
+                v = v % 60;
+                if(v > 0) fprintf(out, "%ds", v);
+            }
+        }
+        break;
+    case CONFIG_BOOLEAN:
+        switch(*var->value.i) {
+        case 0: fprintf(out, "false"); break;
+        case 1: fprintf(out, "true"); break;
+        default: fprintf(out, "???"); break;
+        }
+        break;
+    case CONFIG_TRISTATE:
+        switch(*var->value.i) {
+        case 0: fprintf(out, "false"); break;
+        case 1: fprintf(out, "maybe"); break;
+        case 2: fprintf(out, "true"); break;
+        default: fprintf(out, "???"); break;
+        }
+        break;
+    case CONFIG_TETRASTATE:
+        switch(*var->value.i) {
+        case 0: fprintf(out, "false"); break;
+        case 1: fprintf(out, "reluctantly"); break;
+        case 2: fprintf(out, "happily"); break;
+        case 3: fprintf(out, "true"); break;
+        default: fprintf(out, "???"); break;
+        }
+        break;
+    case CONFIG_PENTASTATE:
+        switch(*var->value.i) {
+        case 0: fprintf(out, "no"); break;
+        case 1: fprintf(out, "reluctantly"); break;
+        case 2: fprintf(out, "maybe"); break;
+        case 3: fprintf(out, "happily"); break;
+        case 4: fprintf(out, "true"); break;
+        default: fprintf(out, "???"); break;
+        }
+        break;
+    case CONFIG_FLOAT: fprintf(out, "%f", *var->value.f); break;
+    case CONFIG_ATOM: case CONFIG_ATOM_LOWER:
+        if(*var->value.a) {
+            if((*var->value.a)->length > 0) {
+                printString(out, (*var->value.a)->string, html);
+            } else {
+                if(!parseable)
+                    fprintf(out, "(empty)");
+            }
+        } else {
+            if(!parseable)
+                fprintf(out, "(none)");
+        }
+        break;
+    case CONFIG_PASSWORD:
+        if(!parseable)
+            fprintf(out, "(hidden)");
+        break;
+    case CONFIG_INT_LIST:
+        if((*var->value.il) == NULL) {
+            if(!parseable)
+                fprintf(out, "(not set)");
+        } else if((*var->value.il)->length == 0) {
+            if(!parseable)
+                fprintf(out, "(empty list)");
+        } else {
+            for(i = 0; i < (*var->value.il)->length; i++) {
+                int from = (*var->value.il)->ranges[i].from;
+                int to = (*var->value.il)->ranges[i].to;
+                assert(from <= to);
+                if(from == to)
+                    fprintf(out, "%d", from);
+                else
+                    fprintf(out, "%d-%d", from, to);
+                if(i < (*var->value.il)->length - 1)
+                    fprintf(out, ", ");
+            }
+        }
+        break;
+    case CONFIG_ATOM_LIST: case CONFIG_ATOM_LIST_LOWER:
+        if((*var->value.al) == NULL) {
+            if(!parseable)
+                fprintf(out, "(not set)");
+        } else if((*var->value.al)->length == 0) {
+            if(!parseable)
+                fprintf(out, "(empty list)");
+        } else {
+            for(i = 0; i < (*var->value.al)->length; i++) {
+                AtomPtr atom = (*var->value.al)->list[i];
+                if(atom) {
+                    if(atom->length > 0)
+                        printString(out, atom->string, html);
+                    else {
+                        if(!parseable)
+                            fprintf(out, "(empty)");
+                    }
+                } else {
+                    if(!parseable)
+                        fprintf(out, "(none)");
+                }
+                if(i < (*var->value.al)->length - 1)
+                    fprintf(out, ", ");
+            }
+        }
+        break;
+    default: abort();
+    }
+}
+
+static void
+printVariableForm(FILE *out, ConfigVariablePtr var)
+{
+    char *disabled = "";
+    int i;
+    
+    if(disableConfiguration || !var->setter) disabled = "disabled=true";
+
+    fprintf(out, "<form method=POST action=\"config?\">");
+  
+    switch(var->type) {
+    case CONFIG_INT: case CONFIG_OCTAL: case CONFIG_HEX:
+    case CONFIG_TIME: case CONFIG_FLOAT: case CONFIG_ATOM:
+    case CONFIG_ATOM_LOWER: case CONFIG_PASSWORD:
+    case CONFIG_INT_LIST: case CONFIG_ATOM_LIST: case CONFIG_ATOM_LIST_LOWER:
+        fprintf(out, "<input value=\"");
+        printVariable(out, var, 1, 1);
+        fprintf(out, "\"%s size=14 name=%s %s>\n",
+                var->type == CONFIG_PASSWORD ? " type=password" : "",
+                var->name->string, disabled);
+        break;
+
+    case CONFIG_BOOLEAN:
+        {
+            static char *states[] = {"false", "true"};
+            
+            fprintf(out, "<select name=%s %s>", var->name->string, disabled);
+            for(i=0; i < sizeof(states) / sizeof(states[0]); i++) {
+                if(*var->value.i == i) {
+                    fprintf(out, "<option selected>%s</option>", states[i]);
+                } else {
+                    fprintf(out, "<option>%s</option>", states[i]);
+                }
+            }
+            fprintf(out, "</select>");
+            if(var->setter)
+                fprintf(out, "<input type=\"submit\" value=\"set\"\n>");
+            break;
+        }
+    
+    case CONFIG_TRISTATE:
+        {
+            static char *states[] = {"false", "maybe", "true"};
+            
+            fprintf(out, "<select name=%s %s>", var->name->string, disabled);
+            for(i=0; i < sizeof(states) / sizeof(states[0]); i++) {
+                if(*var->value.i == i) {
+                    fprintf(out, "<option selected>%s</option>", states[i]);
+                } else {
+                    fprintf(out, "<option>%s</option>", states[i]);
+                }
+            }
+            fprintf(out, "</select>");
+            if(var->setter)
+                fprintf(out, "<input type=\"submit\" value=\"set\"\n>");
+            break;
+        }
+
+    case CONFIG_TETRASTATE:
+        {
+            static char *states[] =
+                {"false", "reluctantly", "happily", "true"};
+            
+            fprintf(out, "<select name=%s %s>", var->name->string, disabled);
+            for(i=0; i <sizeof(states) / sizeof(states[0]); i++) {
+                if(*var->value.i == i) {
+                    fprintf(out, "<option selected>%s</option>", states[i]);
+                } else {
+                    fprintf(out, "<option>%s</option>", states[i]);
+                }
+            }
+            fprintf(out, "</select>");
+            if(var->setter)
+                fprintf(out, "<input type=\"submit\" value=\"set\"\n>");
+            break;
+        }
+
+    case CONFIG_PENTASTATE:
+        {
+            static char *states[] =
+                {"no", "reluctantly", "maybe", "happily", "true"};
+
+            fprintf(out, "<select name=%s %s>", var->name->string, disabled);
+            for(i=0; i < sizeof(states) / sizeof(states[0]); i++) {
+                if(*var->value.i == i) {
+                    fprintf(out, "<option selected>%s</option>", states[i]);
+                } else {
+                    fprintf(out, "<option>%s</option>", states[i]);
+                }
+            }
+            fprintf(out, "</select>");
+            if(var->setter)
+                fprintf(out,"<input type=\"submit\" value=\"set\"\n>");
+            break;
+        }
+    default: abort();
+    }
+    fprintf(out, "</form>");
+}
+
+
+
+
+
+void
+printConfigVariables(FILE *out, int html)
+{
+    ConfigVariablePtr var;
+    int entryno = 0;
+
+#define PRINT_SEP() \
+    do {if(html) fprintf(out, "</td><td>"); else fprintf(out, " ");} while(0)
+
+    if(html) {
+        fprintf(out, "<table>\n");
+        fprintf(out, "<tbody>\n");
+    }
+
+    if(html) {
+        alternatingHttpStyle(out, "configlist");
+        fprintf(out,
+                "<table id=configlist>\n"
+                "<thead>\n"
+                "<tr><th>variable name</th>"
+                "<th>current value</th>"
+                "<th>new value</th>"
+                "<th>description</th>\n"
+                "</thead><tbody>\n"
+);
+    }
+
+    /* configFile is not a config variable, for obvious bootstrapping reasons.
+       CHUNK_SIZE is hardwired for now. */
+
+    fprintf(out,
+	    html ?
+	    "<tr class=\"even\"><td>configFile</td><td>%s</td><td></td><td>"
+	    "Configuration file.</td></tr>\n" :
+	    "configFile %s Configuration file.\n",
+	    configFile && configFile->length > 0 ?
+	    configFile->string : "(none)");
+    fprintf(out,
+	    html ?
+	    "<tr class=\"odd\"><td>CHUNK_SIZE</td><td>%d</td><td></td><td>"
+	    "Unit of chunk memory allocation.</td></tr>\n" :
+	    "CHUNK_SIZE %d Unit of chunk memory allocation.\n", CHUNK_SIZE);
+    
+    var = configVariables;
+    while(var != NULL) {
+      if(html) {
+          if(entryno % 2)
+              fprintf(out, "<tr class=odd>");
+          else
+              fprintf(out, "<tr class=even>");
+          fprintf(out, "<td>");
+      }
+
+      fprintf(out, "%s", var->name->string);
+
+      fprintf(out, html ? "<br/>" : " "); 
+      
+      fprintf(out, html ? "<i>" : "");    
+      
+      switch(var->type) {
+      case CONFIG_INT: case CONFIG_OCTAL: case CONFIG_HEX:
+	  fprintf(out, "integer"); break;
+      case CONFIG_TIME: fprintf(out, "time"); break;
+      case CONFIG_BOOLEAN: fprintf(out, "boolean"); break;
+      case CONFIG_TRISTATE: fprintf(out, "tristate"); break;
+      case CONFIG_TETRASTATE: fprintf(out, "4-state"); break;
+      case CONFIG_PENTASTATE: fprintf(out, "5-state"); break;
+      case CONFIG_FLOAT: fprintf(out, "float"); break;
+      case CONFIG_ATOM: case CONFIG_ATOM_LOWER: case CONFIG_PASSWORD:
+          fprintf(out, "atom"); break;
+      case CONFIG_INT_LIST: fprintf(out, "intlist"); break;
+      case CONFIG_ATOM_LIST: case CONFIG_ATOM_LIST_LOWER:
+	  fprintf(out, "list"); break;
+      default: abort();
+      }
+        
+      fprintf(out, html ? "</i>" : "");
+
+      PRINT_SEP();
+
+      printVariable(out, var, html, 0);
+
+      PRINT_SEP();
+	
+      if(html) {
+	printVariableForm(out, var);
+	PRINT_SEP();
+      }
+
+      fprintf(out, "%s", var->help?var->help:"");
+      if(html)
+	fprintf(out, "</td></tr>\n");
+      else
+	fprintf(out, "\n");
+
+      entryno++;
+      var = var->next;
+    }
+    if(html) {
+        fprintf(out, "</tbody>\n");
+        fprintf(out, "</table>\n");
+    }
+    return;
+#undef PRINT_SEP
+}
+
+static int
+skipWhitespace(char *buf, int i)
+{
+    while(buf[i] == ' ' || buf[i] == '\t' || buf[i] == '\r')
+        i++;
+    return i;
+}
+
+static int
+parseInt(char *buf, int offset, int *value_return)
+{
+    char *p;
+    int value;
+
+    value = strtol(buf + offset, &p, 0);
+    if(p <= buf + offset)
+        return -1;
+
+    *value_return = value;
+    return p - buf;
+}
+
+static struct config_state { char *name; int value; }
+states[] = 
+    { { "false", 0 }, 
+      { "no", 0 },
+      { "reluctantly", 1 },
+      { "seldom", 1 },
+      { "rarely", 1 },
+      { "lazily", 1 },
+      { "maybe", 2 },
+      { "perhaps", 2 },
+      { "happily", 3 },
+      { "often", 3 },
+      { "eagerly", 3 },
+      { "true", 4 },
+      { "yes", 4 } };
+
+static int
+parseState(char *buf, int offset, int kind)
+{
+    int i = offset;
+    int n;
+    int state = -1;
+
+    while(letter(buf[i]))
+        i++;
+    for(n = 0; n < sizeof(states) / sizeof(states[0]); n++) {
+        if(strlen(states[n].name) == i - offset &&
+           lwrcmp(buf + offset, states[n].name, i - offset) == 0) {
+            state = states[n].value;
+            break;
+        }
+    }
+    if(state < 0)
+        return -1;
+
+    switch(kind) {
+    case CONFIG_BOOLEAN:
+        if(state == 0) return 0;
+        else if(state == 4) return 1;
+        else return -1;
+        break;
+    case CONFIG_TRISTATE:
+        if(state == 0) return 0;
+        else if(state == 2) return 1;
+        else if(state == 4) return 2;
+        else return -1;
+        break;
+    case CONFIG_TETRASTATE:
+        if(state == 0) return 0;
+        else if(state == 1) return 1;
+        else if(state == 3) return 2;
+        else if(state == 4) return 3;
+        else return -1;
+        break;
+    case CONFIG_PENTASTATE:
+        return state;
+        break;
+    default:
+        abort();
+    }
+}
+
+static int
+parseAtom(char *buf, int offset, AtomPtr *value_return, int insensitive)
+{
+    int y0, i, j, k;
+    AtomPtr atom;
+    int escape = 0;
+    char *s;
+
+    i = offset;
+    if(buf[i] == '\"') {
+        i++;
+        y0 = i;
+        while(buf[i] != '\"' && buf[i] != '\n' && buf[i] != '\0') {
+            if(buf[i] == '\\' && buf[i + 1] != '\0') {
+                escape = 1;
+                i += 2;
+            } else
+                i++;
+        }
+        if(buf[i] != '\"')
+            return -1;
+        j = i + 1;
+    } else {
+        y0 = i;
+        while(letter(buf[i]) || digit(buf[i]) || 
+              buf[i] == '_' || buf[i] == '-' || buf[i] == '~' ||
+              buf[i] == '.' || buf[i] == ':' || buf[i] == '/')
+            i++;
+        j = i;
+    }
+
+    if(escape) {
+        s = malloc(i - y0);
+        if(s == NULL) return -1;
+        k = 0;
+        j = y0;
+        while(j < i) {
+            if(buf[j] == '\\' && j <= i - 2) {
+                s[k++] = buf[j + 1];
+                j += 2;
+            } else
+                s[k++] = buf[j++];
+        }
+        if(insensitive)
+            atom = internAtomLowerN(s, k);
+        else
+            atom = internAtomN(s, k);
+        free(s);
+        j++;
+    } else {
+        if(insensitive)
+            atom = internAtomLowerN(buf + y0, i - y0);
+        else
+            atom = internAtomN(buf + y0, i - y0);
+    }
+    *value_return = atom;
+    return j;
+}
+
+static int
+parseTime(char *line, int i, int *value_return)
+{
+    int v = 0, w;
+    while(1) {
+        if(!digit(line[i]))
+            break;
+        w = atoi(line + i);
+        while(digit(line[i])) i++;
+        switch(line[i]) {
+        case 'd': v += w * 24 * 3600; i++; break;
+        case 'h': v += w * 3600; i++; break;
+        case 'm': v += w * 60; i++; break;
+        case 's': v += w; i++; break;
+        default: v += w; goto done;
+        }
+    }
+ done:
+    *value_return = v;
+    return i;
+}
+
+int
+parseConfigLine(char *line, char *filename, int lineno, int set)
+{
+    int x0, x1;
+    int i, from, to;
+    AtomPtr name, value;
+    ConfigVariablePtr var;
+    int iv;
+    float fv;
+    AtomPtr av;
+    AtomListPtr alv;
+    IntListPtr ilv;
+
+    i = skipWhitespace(line, 0);
+    if(line[i] == '\n' || line[i] == '\0' || line[i] == '#')
+        return 0;
+
+    x0 = i;
+    while(letter(line[i]) || digit(line[i]))
+        i++;
+    x1 = i;
+
+    i = skipWhitespace(line, i);
+    if(line[i] != '=') {
+        goto syntax;
+    }
+    i++;
+    i = skipWhitespace(line, i);
+
+    name = internAtomN(line + x0, x1 - x0);
+    var = findConfigVariable(name);
+    releaseAtom(name);
+
+    if(set && var->setter == NULL)
+        return -2;
+ 
+    if(var == NULL) {
+        if(!set) {
+            do_log(L_ERROR, "%s:%d: unknown config variable ",
+                   filename, lineno);
+            do_log_n(L_ERROR, line + x0, x1 - x0);
+            do_log(L_ERROR, "\n");
+        }
+        return -1;
+    }
+    
+    i = skipWhitespace(line, i);
+    switch(var->type) {
+    case CONFIG_INT: case CONFIG_OCTAL: case CONFIG_HEX:
+        i = parseInt(line, i, &iv);
+        if(i < 0) goto syntax;
+        if(set)
+            var->setter(var, &iv);
+        else
+            *var->value.i = iv;
+    break;
+    case CONFIG_TIME:
+        i = parseTime(line, i, &iv);
+        if(i < 0) goto syntax;
+        i = skipWhitespace(line, i);
+        if(line[i] != '\n' && line[i] != '\0' && line[i] != '#')
+            goto syntax;
+        if(set)
+            var->setter(var, &iv);
+        else
+            *var->value.i = iv;
+        break;
+    case CONFIG_BOOLEAN:
+    case CONFIG_TRISTATE:
+    case CONFIG_TETRASTATE:
+    case CONFIG_PENTASTATE:
+        iv = parseState(line, i, var->type);
+        if(iv < 0)
+            goto syntax;
+        if(set)
+            var->setter(var, &iv);
+        else
+            *var->value.i = iv;
+        break;
+    case CONFIG_FLOAT: 
+        if(!digit(line[i]) && line[i] != '.')
+            goto syntax;
+        fv = atof(line + i);
+        if(set)
+            var->setter(var, &fv);
+        else
+            *var->value.f = fv;
+        break;
+    case CONFIG_ATOM: case CONFIG_ATOM_LOWER: case CONFIG_PASSWORD:
+        i = parseAtom(line, i, &av, (var->type == CONFIG_ATOM_LOWER));
+        if(i < 0) goto syntax;
+        if(!av) {
+            if(!set)
+                do_log(L_ERROR, "%s:%d: couldn't allocate atom.\n",
+                       filename, lineno);
+            return -1;
+        }
+        i = skipWhitespace(line, i);
+        if(line[i] != '\n' && line[i] != '\0' && line[i] != '#') {
+            releaseAtom(av);
+            goto syntax;
+        }
+        if(set)
+            var->setter(var, &av);
+        else {
+            if(*var->value.a) releaseAtom(*var->value.a);
+            *var->value.a = av;
+        }
+        break;
+    case CONFIG_INT_LIST:
+        ilv = makeIntList(0);
+        if(ilv == NULL) {
+            if(!set)
+                do_log(L_ERROR, "%s:%d: couldn't allocate int list.\n",
+                       filename, lineno);
+            return -1;
+        }
+        while(1) {
+            i = parseInt(line, i, &from);
+            if(i < 0) goto syntax;
+            to = from;
+            i = skipWhitespace(line, i);
+            if(line[i] == '-') {
+                i = skipWhitespace(line, i + 1);
+                i = parseInt(line, i, &to);
+                if(i < 0) {
+                    destroyIntList(ilv);
+                    goto syntax;
+                }
+                i = skipWhitespace(line, i);
+            }
+            intListCons(from, to, ilv);
+            if(line[i] == '\n' || line[i] == '\0' || line[i] == '#')
+                break;
+            if(line[i] != ',') {
+                destroyIntList(ilv);
+                goto syntax;
+            }
+            i = skipWhitespace(line, i + 1);
+        }
+        if(set)
+            var->setter(var, &ilv);
+        else {
+            if(*var->value.il) destroyIntList(*var->value.il);
+            *var->value.il = ilv;
+        }
+        break;
+    case CONFIG_ATOM_LIST: case CONFIG_ATOM_LIST_LOWER:
+        alv = makeAtomList(NULL, 0);
+        if(alv == NULL) {
+            if(!set)
+                do_log(L_ERROR, "%s:%d: couldn't allocate atom list.\n",
+                       filename, lineno);
+            return -1;
+        }
+        while(1) {
+            i = parseAtom(line, i, &value, 
+                          (var->type == CONFIG_ATOM_LIST_LOWER));
+            if(i < 0) goto syntax;
+            if(!value) {
+                if(!set)
+                    do_log(L_ERROR, "%s:%d: couldn't allocate atom.\n",
+                           filename, lineno);
+                return -1;
+            }
+            atomListCons(value, alv);
+            i = skipWhitespace(line, i);
+            if(line[i] == '\n' || line[i] == '\0' || line[i] == '#')
+                break;
+            if(line[i] != ',') {
+                destroyAtomList(alv);
+                goto syntax;
+            }
+            i = skipWhitespace(line, i + 1);
+        }
+        if(set)
+            var->setter(var, &alv);
+        else {
+            if(*var->value.al) destroyAtomList(*var->value.al);
+            *var->value.al = alv;
+        }
+        break;
+    default: abort();
+    }
+    return 1;
+
+ syntax:
+    if(!set)
+        do_log(L_ERROR, "%s:%d: parse error.\n", filename, lineno);
+    return -1;
+}
+
+int
+parseConfigFile(AtomPtr filename)
+{
+    char buf[512];
+    int lineno;
+    FILE *f;
+
+    if(!filename || filename->length == 0)
+        return 0;
+    f = fopen(filename->string, "r");
+    if(f == NULL) {
+        do_log_error(L_ERROR, errno, "Couldn't open config file %s",
+                     filename->string);
+        return -1;
+    }
+
+    lineno = 1;
+    while(1) {
+        char *s;
+        s = fgets(buf, 512, f);
+        if(s == NULL) {
+            fclose(f);
+            return 1;
+        }
+        parseConfigLine(buf, filename->string, lineno, 0);
+        lineno++;
+    }
+}
+
+int
+configIntSetter(ConfigVariablePtr var, void* value)
+{
+    assert(var->type <= CONFIG_PENTASTATE);
+    *var->value.i = *(int*)value;
+    return 1;
+}
+
+int
+configFloatSetter(ConfigVariablePtr var, void* value)
+{
+    assert(var->type == CONFIG_FLOAT);
+    *var->value.i = *(float*)value;
+    return 1;
+}
+
+
+int
+configAtomSetter(ConfigVariablePtr var, void* value)
+{
+    assert(var->type == CONFIG_ATOM || var->type == CONFIG_ATOM_LOWER ||
+           var->type == CONFIG_PASSWORD);
+    if(*var->value.a)
+        releaseAtom(*var->value.a);
+    *var->value.a = *(AtomPtr*)value;
+    return 1;
+}

+ 68 - 0
polipo/config.h

@@ -0,0 +1,68 @@
+/*
+Copyright (c) 2003-2006 by Juliusz Chroboczek
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+#define CONFIG_INT 0
+#define CONFIG_OCTAL 1
+#define CONFIG_HEX 2
+#define CONFIG_TIME 3
+#define CONFIG_BOOLEAN 4
+#define CONFIG_TRISTATE 5
+#define CONFIG_TETRASTATE 6
+#define CONFIG_PENTASTATE 7
+#define CONFIG_FLOAT 8
+#define CONFIG_ATOM 9
+#define CONFIG_ATOM_LOWER 10
+#define CONFIG_PASSWORD 11
+#define CONFIG_INT_LIST 12
+#define CONFIG_ATOM_LIST 13
+#define CONFIG_ATOM_LIST_LOWER 14
+
+typedef struct _ConfigVariable {
+    AtomPtr name;
+    int type;
+    union {
+        int *i;
+        float *f;
+        struct _Atom **a;
+        struct _AtomList **al;
+        struct _IntList **il;
+    } value;
+    int (*setter)(struct _ConfigVariable*, void*);
+    char *help;
+    struct _ConfigVariable *next;
+} ConfigVariableRec, *ConfigVariablePtr;
+
+#define CONFIG_VARIABLE(name, type, help) \
+    CONFIG_VARIABLE_SETTABLE(name, type, NULL, help)
+
+#define CONFIG_VARIABLE_SETTABLE(name, type, setter, help) \
+    declareConfigVariable(internAtom(#name), type, &name, setter, help)
+
+void declareConfigVariable(AtomPtr name, int type, void *value, 
+                           int (*setter)(ConfigVariablePtr, void*),
+                           char *help);
+void printConfigVariables(FILE *out, int html);
+int parseConfigLine(char *line, char *filename, int lineno, int set);
+int parseConfigFile(AtomPtr);
+int configIntSetter(ConfigVariablePtr, void*);
+int configFloatSetter(ConfigVariablePtr, void*);
+int configAtomSetter(ConfigVariablePtr, void*);

+ 165 - 0
polipo/config.sample

@@ -0,0 +1,165 @@
+# Sample configuration file for Polipo. -*-sh-*-
+
+# You should not need to use a configuration file; all configuration
+# variables have reasonable defaults.  If you want to use one, you
+# can copy this to /etc/polipo/config or to ~/.polipo and modify.
+
+# This file only contains some of the configuration variables; see the
+# list given by ``polipo -v'' and the manual for more.
+
+
+### Basic configuration
+### *******************
+
+# Uncomment one of these if you want to allow remote clients to
+# connect:
+
+# proxyAddress = "::0"        # both IPv4 and IPv6
+# proxyAddress = "0.0.0.0"    # IPv4 only
+
+# Uncomment this if you want your Polipo to bind to a specific IP:
+
+# proxyOutgoingAddress = "134.157.168.1"
+
+# If you do that, you'll want to restrict the set of hosts allowed to
+# connect:
+
+# allowedClients = 127.0.0.1, 134.157.168.57
+# allowedClients = 127.0.0.1, 134.157.168.0/24
+
+# Uncomment this if you want your Polipo to identify itself by
+# something else than the host name:
+
+# proxyName = "polipo.example.org"
+
+# Uncomment this if there's only one user using this instance of Polipo:
+
+# cacheIsShared = false
+
+# Uncomment this if you want to use a parent proxy:
+
+# parentProxy = "squid.example.org:3128"
+
+# Uncomment this if you want to use a parent SOCKS proxy:
+
+# socksParentProxy = "localhost:9050"
+# socksProxyType = socks5
+
+# Uncomment this if you want to scrub private information from the log:
+
+# scrubLogs = true
+
+
+### Memory
+### ******
+
+# Uncomment this if you want Polipo to use a ridiculously small amount
+# of memory (a hundred C-64 worth or so):
+
+# chunkHighMark = 819200
+# objectHighMark = 128
+
+# Uncomment this if you've got plenty of memory:
+
+# chunkHighMark = 50331648
+# objectHighMark = 16384
+
+
+### On-disk data
+### ************
+
+# Uncomment this if you want to disable the on-disk cache:
+
+# diskCacheRoot = ""
+
+# Uncomment this if you want to put the on-disk cache in a
+# non-standard location:
+
+# diskCacheRoot = "~/.polipo-cache/"
+
+# Uncomment this if you want to disable the local web server:
+
+# localDocumentRoot = ""
+
+# Uncomment this if you want to enable the pages under /polipo/index?
+# and /polipo/servers?.  This is a serious privacy leak if your proxy
+# is shared.
+
+# disableIndexing = false
+# disableServersList = false
+
+
+### Domain Name System
+### ******************
+
+# Uncomment this if you want to contact IPv4 hosts only (and make DNS
+# queries somewhat faster):
+
+# dnsQueryIPv6 = no
+
+# Uncomment this if you want Polipo to prefer IPv4 to IPv6 for
+# double-stack hosts:
+
+# dnsQueryIPv6 = reluctantly
+
+# Uncomment this to disable Polipo's DNS resolver and use the system's
+# default resolver instead.  If you do that, Polipo will freeze during
+# every DNS query:
+
+# dnsUseGethostbyname = yes
+
+
+### HTTP
+### ****
+
+# Uncomment this if you want to enable detection of proxy loops.
+# This will cause your hostname (or whatever you put into proxyName
+# above) to be included in every request:
+
+# disableVia=false
+
+# Uncomment this if you want to slightly reduce the amount of
+# information that you leak about yourself:
+
+# censoredHeaders = from, accept-language
+# censorReferer = maybe
+
+# Uncomment this if you're paranoid.  This will break a lot of sites,
+# though:
+
+# censoredHeaders = set-cookie, cookie, cookie2, from, accept-language
+# censorReferer = true
+
+# Uncomment this if you want to use Poor Man's Multiplexing; increase
+# the sizes if you're on a fast line.  They should each amount to a few
+# seconds' worth of transfer; if pmmSize is small, you'll want
+# pmmFirstSize to be larger.
+
+# Note that PMM is somewhat unreliable.
+
+# pmmFirstSize = 16384
+# pmmSize = 8192
+
+# Uncomment this if your user-agent does something reasonable with
+# Warning headers (most don't):
+
+# relaxTransparency = maybe
+
+# Uncomment this if you never want to revalidate instances for which
+# data is available (this is not a good idea):
+
+# relaxTransparency = yes
+
+# Uncomment this if you have no network:
+
+# proxyOffline = yes
+
+# Uncomment this if you want to avoid revalidating instances with a
+# Vary header (this is not a good idea):
+
+# mindlesslyCacheVary = true
+
+# Uncomment this if you want to add a no-transform directive to all
+# outgoing requests.
+
+# alwaysAddNoTransform = true

+ 149 - 0
polipo/dirent_compat.c

@@ -0,0 +1,149 @@
+/*
+
+    Implementation of POSIX directory browsing functions and types for Win32.
+
+    Author:  Kevlin Henney (kevlin@acm.org, kevlin@curbralan.com)
+    History: Created March 1997. Updated June 2003.
+    Rights:  See end of file.
+
+*/
+
+#ifdef WIN32
+
+#include "dirent_compat.h"
+#include <errno.h>
+#include <io.h> /* _findfirst and _findnext set errno iff they return -1 */
+#include <stdlib.h>
+#include <string.h>
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+struct DIR
+{
+    long                handle; /* -1 for failed rewind */
+    struct _finddata_t  info;
+    struct dirent       result; /* d_name null iff first time */
+    char                *name;  /* null-terminated char string */
+};
+
+DIR *opendir(const char *name)
+{
+    DIR *dir = 0;
+
+    if(name && name[0])
+    {
+        size_t base_length = strlen(name);
+        const char *all = /* search pattern must end with suitable wildcard */
+            strchr("/\\", name[base_length - 1]) ? "*" : "/*";
+
+        if((dir = (DIR *) malloc(sizeof *dir)) != 0 &&
+           (dir->name = (char *) malloc(base_length + strlen(all) + 1)) != 0)
+        {
+            strcat(strcpy(dir->name, name), all);
+
+            if((dir->handle = (long) _findfirst(dir->name, &dir->info)) != -1)
+            {
+                dir->result.d_name = 0;
+            }
+            else /* rollback */
+            {
+                free(dir->name);
+                free(dir);
+                dir = 0;
+            }
+        }
+        else /* rollback */
+        {
+            free(dir);
+            dir   = 0;
+            errno = ENOMEM;
+        }
+    }
+    else
+    {
+        errno = EINVAL;
+    }
+
+    return dir;
+}
+
+int closedir(DIR *dir)
+{
+    int result = -1;
+
+    if(dir)
+    {
+        if(dir->handle != -1)
+        {
+            result = _findclose(dir->handle);
+        }
+
+        free(dir->name);
+        free(dir);
+    }
+
+    if(result == -1) /* map all errors to EBADF */
+    {
+        errno = EBADF;
+    }
+
+    return result;
+}
+
+struct dirent *readdir(DIR *dir)
+{
+    struct dirent *result = 0;
+
+    if(dir && dir->handle != -1)
+    {
+        if(!dir->result.d_name || _findnext(dir->handle, &dir->info) != -1)
+        {
+            result         = &dir->result;
+            result->d_name = dir->info.name;
+        }
+    }
+    else
+    {
+        errno = EBADF;
+    }
+
+    return result;
+}
+
+void rewinddir(DIR *dir)
+{
+    if(dir && dir->handle != -1)
+    {
+        _findclose(dir->handle);
+        dir->handle = (long) _findfirst(dir->name, &dir->info);
+        dir->result.d_name = 0;
+    }
+    else
+    {
+        errno = EBADF;
+    }
+}
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
+
+/*
+
+    Copyright Kevlin Henney, 1997, 2003. All rights reserved.
+
+    Permission to use, copy, modify, and distribute this software and its
+    documentation for any purpose is hereby granted without fee, provided
+    that this copyright and permissions notice appear in all copies and
+    derivatives.
+    
+    This software is supplied "as is" without express or implied warranty.
+
+    But that said, if there are any problems please get in touch.
+
+*/

+ 50 - 0
polipo/dirent_compat.h

@@ -0,0 +1,50 @@
+#ifndef DIRENT_INCLUDED
+#define DIRENT_INCLUDED
+
+/*
+
+    Declaration of POSIX directory browsing functions and types for Win32.
+
+    Author:  Kevlin Henney (kevlin@acm.org, kevlin@curbralan.com)
+    History: Created March 1997. Updated June 2003.
+    Rights:  See end of file.
+    
+*/
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+typedef struct DIR DIR;
+
+struct dirent
+{
+    char *d_name;
+};
+
+DIR           *opendir(const char *);
+int           closedir(DIR *);
+struct dirent *readdir(DIR *);
+void          rewinddir(DIR *);
+
+/*
+
+    Copyright Kevlin Henney, 1997, 2003. All rights reserved.
+
+    Permission to use, copy, modify, and distribute this software and its
+    documentation for any purpose is hereby granted without fee, provided
+    that this copyright and permissions notice appear in all copies and
+    derivatives.
+    
+    This software is supplied "as is" without express or implied warranty.
+
+    But that said, if there are any problems please get in touch.
+
+*/
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif

+ 2561 - 0
polipo/diskcache.c

@@ -0,0 +1,2561 @@
+/*
+Copyright (c) 2003-2010 by Juliusz Chroboczek
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+#include "polipo.h"
+
+#ifndef NO_DISK_CACHE
+
+#include "md5import.h"
+
+int maxDiskEntries = 32;
+
+/* Because the functions in this file can be called during object
+   expiry, we cannot use get_chunk. */
+
+AtomPtr diskCacheRoot;
+AtomPtr localDocumentRoot;
+
+DiskCacheEntryPtr diskEntries = NULL, diskEntriesLast = NULL;
+int numDiskEntries = 0;
+int diskCacheDirectoryPermissions = 0700;
+int diskCacheFilePermissions = 0600;
+int diskCacheWriteoutOnClose = (64 * 1024);
+
+int maxDiskCacheEntrySize = -1;
+
+int diskCacheUnlinkTime = 32 * 24 * 60 * 60;
+int diskCacheTruncateTime = 4 * 24 * 60 * 60 + 12 * 60 * 60;
+int diskCacheTruncateSize =  1024 * 1024;
+int preciseExpiry = 0;
+
+static DiskCacheEntryRec negativeEntry = {
+    NULL, NULL,
+    -1, -1, -1, -1, 0, 0, NULL, NULL
+};
+
+#ifndef LOCAL_ROOT
+#define LOCAL_ROOT "/usr/share/polipo/www/"
+#endif
+
+#ifndef DISK_CACHE_ROOT
+#define DISK_CACHE_ROOT "/var/cache/polipo/"
+#endif
+
+static int maxDiskEntriesSetter(ConfigVariablePtr, void*);
+static int atomSetterFlush(ConfigVariablePtr, void*);
+static int reallyWriteoutToDisk(ObjectPtr object, int upto, int max);
+
+void 
+preinitDiskcache()
+{
+    diskCacheRoot = internAtom(DISK_CACHE_ROOT);
+    localDocumentRoot = internAtom(LOCAL_ROOT);
+
+    CONFIG_VARIABLE_SETTABLE(diskCacheDirectoryPermissions, CONFIG_OCTAL,
+                             configIntSetter,
+                             "Access rights for new directories.");
+    CONFIG_VARIABLE_SETTABLE(diskCacheFilePermissions, CONFIG_OCTAL,
+                             configIntSetter,
+                             "Access rights for new cache files.");
+    CONFIG_VARIABLE_SETTABLE(diskCacheWriteoutOnClose, CONFIG_INT,
+                             configIntSetter,
+                             "Number of bytes to write out eagerly.");
+    CONFIG_VARIABLE_SETTABLE(diskCacheRoot, CONFIG_ATOM, atomSetterFlush,
+                             "Root of the disk cache.");
+    CONFIG_VARIABLE_SETTABLE(localDocumentRoot, CONFIG_ATOM, atomSetterFlush,
+                             "Root of the local tree.");
+    CONFIG_VARIABLE_SETTABLE(maxDiskEntries, CONFIG_INT, maxDiskEntriesSetter,
+                    "File descriptors used by the on-disk cache.");
+    CONFIG_VARIABLE(diskCacheUnlinkTime, CONFIG_TIME,
+                    "Time after which on-disk objects are removed.");
+    CONFIG_VARIABLE(diskCacheTruncateTime, CONFIG_TIME,
+                    "Time after which on-disk objects are truncated.");
+    CONFIG_VARIABLE(diskCacheTruncateSize, CONFIG_INT, 
+                    "Size to which on-disk objects are truncated.");
+    CONFIG_VARIABLE(preciseExpiry, CONFIG_BOOLEAN,
+                    "Whether to consider all files for purging.");
+    CONFIG_VARIABLE_SETTABLE(maxDiskCacheEntrySize, CONFIG_INT,
+                             configIntSetter,
+                             "Maximum size of objects cached on disk.");
+}
+
+static int
+maxDiskEntriesSetter(ConfigVariablePtr var, void *value)
+{
+    int i;
+    assert(var->type == CONFIG_INT && var->value.i == &maxDiskEntries);
+    i = *(int*)value;
+    if(i < 0 || i > 1000000)
+        return -3;
+    maxDiskEntries = i;
+    while(numDiskEntries > maxDiskEntries)
+        destroyDiskEntry(diskEntriesLast->object, 0);
+    return 1;
+}
+
+static int
+atomSetterFlush(ConfigVariablePtr var, void *value)
+{
+    discardObjects(1, 0);
+    return configAtomSetter(var, value);
+}
+
+static int
+checkRoot(AtomPtr root)
+{
+    struct stat ss;
+    int rc;
+
+    if(!root || root->length == 0)
+        return 0;
+
+#ifdef WIN32  /* Require "x:/" or "x:\\" */
+    rc = isalpha(root->string[0]) && (root->string[1] == ':') &&
+         ((root->string[2] == '/') || (root->string[2] == '\\'));
+    if(!rc) {
+        return -2;
+    }
+#else
+    if(root->string[0] != '/') {
+        return -2;
+    }
+#endif
+
+    rc = stat(root->string, &ss);
+    if(rc < 0)
+        return -1;
+    else if(!S_ISDIR(ss.st_mode)) {
+        errno = ENOTDIR;
+        return -1;
+    }
+    return 1;
+}
+
+static AtomPtr
+maybeAddSlash(AtomPtr atom)
+{
+    AtomPtr newAtom = NULL;
+    if(!atom) return NULL;
+    if(atom->length > 0 && atom->string[atom->length - 1] != '/') {
+        newAtom = atomCat(atom, "/");
+        releaseAtom(atom);
+        return newAtom;
+    }
+    return atom;
+}
+
+void
+initDiskcache()
+{
+    int rc;
+
+    diskCacheRoot = expandTilde(maybeAddSlash(diskCacheRoot));
+    rc = checkRoot(diskCacheRoot);
+    if(rc <= 0) {
+        switch(rc) {
+        case 0: break;
+        case -1: do_log_error(L_WARN, errno, "Disabling disk cache"); break;
+        case -2: 
+            do_log(L_WARN, "Disabling disk cache: path %s is not absolute.\n",
+                   diskCacheRoot->string); 
+            break;
+        default: abort();
+        }
+        releaseAtom(diskCacheRoot);
+        diskCacheRoot = NULL;
+    }
+
+    localDocumentRoot = expandTilde(maybeAddSlash(localDocumentRoot));
+    rc = checkRoot(localDocumentRoot);
+    if(rc <= 0) {
+        switch(rc) {
+        case 0: break;
+        case -1: do_log_error(L_WARN, errno, "Disabling local tree"); break;
+        case -2: 
+            do_log(L_WARN, "Disabling local tree: path is not absolute.\n"); 
+            break;
+        default: abort();
+        }
+        releaseAtom(localDocumentRoot);
+        localDocumentRoot = NULL;
+    }
+}
+
+#ifdef DEBUG_DISK_CACHE
+#define CHECK_ENTRY(entry) check_entry((entry))
+static void
+check_entry(DiskCacheEntryPtr entry)
+{
+    if(entry && entry->fd < 0)
+        assert(entry == &negativeEntry);
+    if(entry && entry->fd >= 0) {
+        assert((!entry->previous) == (entry == diskEntries));
+        assert((!entry->next) == (entry == diskEntriesLast));
+        if(entry->size >= 0)
+            assert(entry->size + entry->body_offset >= entry->offset);
+        assert(entry->body_offset >= 0);
+        if(entry->offset >= 0) {
+            off_t offset;
+            offset = lseek(entry->fd, 0, SEEK_CUR);
+            assert(offset == entry->offset);
+        }
+        if(entry->size >= 0) {
+            int rc;
+            struct stat ss;
+            rc = fstat(entry->fd, &ss);
+            assert(rc >= 0);
+            assert(ss.st_size == entry->size + entry->body_offset);
+        }
+    }
+}
+#else
+#define CHECK_ENTRY(entry) do {} while(0)
+#endif
+
+int
+diskEntrySize(ObjectPtr object)
+{
+    struct stat buf;
+    int rc;
+    DiskCacheEntryPtr entry = object->disk_entry;
+
+    if(!entry || entry == &negativeEntry)
+        return -1;
+
+    if(entry->size >= 0)
+        return entry->size;
+
+    rc = fstat(entry->fd, &buf);
+    if(rc < 0) {
+        do_log_error(L_ERROR, errno, "Couldn't stat");
+        return -1;
+    }
+
+    if(buf.st_size <= entry->body_offset)
+        entry->size = 0;
+    else
+        entry->size =  buf.st_size - entry->body_offset;
+    CHECK_ENTRY(entry);
+    if(object->length >= 0 && entry->size == object->length)
+        object->flags |= OBJECT_DISK_ENTRY_COMPLETE;
+    return entry->size;
+}
+
+static int
+entrySeek(DiskCacheEntryPtr entry, off_t offset)
+{
+    off_t rc;
+
+    CHECK_ENTRY(entry);
+    assert(entry != &negativeEntry);
+    if(entry->offset == offset)
+        return 1;
+    if(offset > entry->body_offset) {
+        /* Avoid extending the file by mistake */
+        if(entry->size < 0)
+            diskEntrySize(entry->object);
+        if(entry->size < 0)
+            return -1;
+        if(entry->size + entry->body_offset < offset)
+            return -1;
+    }
+    rc = lseek(entry->fd, offset, SEEK_SET);
+    if(rc < 0) {
+        do_log_error(L_ERROR, errno, "Couldn't seek");
+        entry->offset = -1;
+        return -1;
+    }
+    entry->offset = offset;
+    return 1;
+}
+
+/* Given a local URL, constructs the filename where it can be found. */
+
+int
+localFilename(char *buf, int n, char *key, int len)
+{
+    int i, j;
+    if(len <= 0 || key[0] != '/') return -1;
+
+    if(urlIsSpecial(key, len)) return -1;
+
+    if(checkRoot(localDocumentRoot) <= 0)
+        return -1;
+
+    if(n <= localDocumentRoot->length)
+        return -1;
+
+    i = 0;
+    if(key[i] != '/')
+        return -1;
+
+    memcpy(buf, localDocumentRoot->string, localDocumentRoot->length);
+    j = localDocumentRoot->length;
+    if(buf[j - 1] == '/')
+        j--;
+
+    while(i < len) {
+        if(j >= n - 1)
+            return -1;
+        if(key[i] == '/' && i < len - 2)
+            if(key[i + 1] == '.' && 
+               (key[i + 2] == '.' || key[i + 2] == '/'))
+                return -1;
+        buf[j++] = key[i++];
+    }
+
+    if(buf[j - 1] == '/') {
+        if(j >= n - 11)
+            return -1;
+        memcpy(buf + j, "index.html", 10);
+        j += 10;
+    }
+
+    buf[j] = '\0';
+    return j;
+}
+
+static void
+md5(unsigned char *restrict key, int len, unsigned char *restrict dst)
+{
+    static MD5_CTX ctx;
+    MD5Init(&ctx);
+    MD5Update(&ctx, key, len);
+    MD5Final(&ctx);
+    memcpy(dst, ctx.digest, 16);
+}
+
+/* Check whether a character can be stored in a filename.  This is
+   needed since we want to support deficient file systems. */
+static int
+fssafe(char c)
+{
+    if(c <= 31 || c >= 127)
+        return 0;
+    if((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
+       (c >= '0' && c <= '9') ||  c == '.' || c == '-' || c == '_')
+        return 1;
+    return 0;
+}
+
+/* Given a URL, returns the directory name within which all files
+   starting with this URL can be found. */
+static int
+urlDirname(char *buf, int n, const char *url, int len)
+{
+    int i, j;
+    if(len < 8)
+        return -1;
+    if(lwrcmp(url, "http://", 7) != 0)
+        return -1;
+
+    if(checkRoot(diskCacheRoot) <= 0)
+        return -1;
+
+    if(n <= diskCacheRoot->length)
+        return -1;
+
+    memcpy(buf, diskCacheRoot->string, diskCacheRoot->length);
+    j = diskCacheRoot->length;
+
+    if(buf[j - 1] != '/')
+        buf[j++] = '/';
+
+    for(i = 7; i < len; i++) {
+        if(i >= len || url[i] == '/')
+            break;
+        if(url[i] == '.' && i != len - 1 && url[i + 1] == '.')
+            return -1;
+        if(url[i] == '%' || !fssafe(url[i])) {
+            if(j + 3 >= n) return -1;
+            buf[j++] = '%';
+            buf[j++] = i2h((url[i] & 0xF0) >> 4);
+            buf[j++] = i2h(url[i] & 0x0F);
+        } else {
+            buf[j++] = url[i]; if(j >= n) return -1;
+        }
+    }
+    buf[j++] = '/'; if(j >= n) return -1;
+    buf[j] = '\0';
+    return j;
+}
+
+/* Given a URL, returns the filename where the cached data can be
+   found. */
+static int
+urlFilename(char *restrict buf, int n, const char *url, int len)
+{
+    int j;
+    unsigned char md5buf[18];
+    j = urlDirname(buf, n, url, len);
+    if(j < 0 || j + 24 >= n)
+        return -1;
+    md5((unsigned char*)url, len, md5buf);
+    b64cpy(buf + j, (char*)md5buf, 16, 1);
+    buf[j + 24] = '\0';
+    return j + 24;
+}
+
+static char *
+dirnameUrl(char *url, int n, char *name, int len)
+{
+    int i, j, k, c1, c2;
+    k = diskCacheRoot->length;
+    if(len < k)
+        return NULL;
+    if(memcmp(name, diskCacheRoot->string, k) != 0)
+        return NULL;
+    if(n < 8)
+        return NULL;
+    memcpy(url, "http://", 7);
+    if(name[len - 1] == '/')
+        len --;
+    j = 7;
+    for(i = k; i < len; i++) {
+        if(name[i] == '%') {
+            if(i >= len - 2)
+                return NULL;
+            c1 = h2i(name[i + 1]);
+            c2 = h2i(name[i + 2]);
+            if(c1 < 0 || c2 < 0)
+                return NULL;
+            url[j++] = c1 * 16 + c2; if(j >= n) goto fail;
+            i += 2;             /* skip extra digits */
+        } else if(i < len - 1 && 
+                  name[i] == '.' && name[i + 1] == '/') {
+                return NULL;
+        } else if(i == len - 1 && name[i] == '.') {
+            return NULL;
+        } else {
+            url[j++] = name[i]; if(j >= n) goto fail;
+        }
+    }
+    url[j++] = '/'; if(j >= n) goto fail;
+    url[j] = '\0';
+    return url;
+
+ fail:
+    return NULL;
+}
+
+/* Create a file and all intermediate directories. */
+static int
+createFile(const char *name, int path_start)
+{
+    int fd;
+    char buf[1024];
+    int n;
+    int rc;
+
+    if(name[path_start] == '/')
+        path_start++;
+
+    if(path_start < 2 || name[path_start - 1] != '/' ) {
+        do_log(L_ERROR, "Incorrect name %s (%d).\n", name, path_start);
+        return -1;
+    }
+
+    fd = open(name, O_RDWR | O_CREAT | O_EXCL | O_BINARY,
+	      diskCacheFilePermissions);
+    if(fd >= 0)
+        return fd;
+    if(errno != ENOENT) {
+        do_log_error(L_ERROR, errno, "Couldn't create disk file %s", name);
+        return -1;
+    }
+    
+    n = path_start;
+    while(name[n] != '\0' && n < 1024) {
+        while(name[n] != '/' && name[n] != '\0' && n < 512)
+            n++;
+        if(name[n] != '/' || n >= 1024)
+            break;
+        memcpy(buf, name, n + 1);
+        buf[n + 1] = '\0';
+        rc = mkdir(buf, diskCacheDirectoryPermissions);
+        if(rc < 0 && errno != EEXIST) {
+            do_log_error(L_ERROR, errno, "Couldn't create directory %s", buf);
+            return -1;
+        }
+        n++;
+    }
+    fd = open(name, O_RDWR | O_CREAT | O_EXCL | O_BINARY,
+	      diskCacheFilePermissions);
+    if(fd < 0) {
+        do_log_error(L_ERROR, errno, "Couldn't create file %s", name);
+        return -1;
+    }
+
+    return fd;
+}
+
+static int
+chooseBodyOffset(int n, ObjectPtr object)
+{
+    int length = MAX(object->size, object->length);
+    int body_offset;
+
+    if(object->length >= 0 && object->length + n < 4096 - 4)
+        return -1;              /* no gap for small objects */
+
+    if(n <= 128)
+        body_offset = 256;
+    else if(n <= 192)
+        body_offset = 384;
+    else if(n <= 256)
+        body_offset = 512;
+    else if(n <= 384)
+        body_offset = 768;
+    else if(n <= 512)
+        body_offset = 1024;
+    else if(n <= 1024)
+        body_offset = 2048;
+    else if(n < 2048)
+        body_offset = 4096;
+    else
+        body_offset = ((n + 32 + 4095) / 4096 + 1) * 4096;
+
+    /* Tweak the gap so that we don't use up a full disk block for
+       a small tail */
+    if(object->length >= 0 && object->length < 64 * 1024) {
+        int last = (body_offset + object->length) % 4096;
+        int gap = body_offset - n - 32;
+        if(last < gap / 2)
+            body_offset -= last;
+    }
+
+    /* Rewriting large objects is expensive -- don't use small gaps.
+       This has the additional benefit of block-aligning large bodies. */
+    if(length >= 64 * 1024) {
+        int min_gap, min_offset;
+        if(length >= 512 * 1024)
+            min_gap = 4096;
+        else if(length >= 256 * 1024)
+            min_gap = 2048;
+        else
+            min_gap = 1024;
+
+        min_offset = ((n + 32 + min_gap - 1) / min_gap + 1) * min_gap;
+        body_offset = MAX(body_offset, min_offset);
+    }
+
+    return body_offset;
+}
+ 
+/* Assumes the file descriptor is at offset 0.  Returns -1 on failure,
+   otherwise the offset at which the file descriptor is left. */
+/* If chunk is not null, it should be the first chunk of the object,
+   and will be written out in the same operation if possible. */
+static int
+writeHeaders(int fd, int *body_offset_return,
+             ObjectPtr object, char *chunk, int chunk_len)
+{
+    int n, rc, error = -1;
+    int body_offset = *body_offset_return;
+    char *buf = NULL;
+    int buf_is_chunk = 0;
+    int bufsize = 0;
+
+    if(object->flags & OBJECT_LOCAL)
+        return -1;
+
+    if(body_offset > CHUNK_SIZE)
+        goto overflow;
+
+    /* get_chunk might trigger object expiry */
+    bufsize = CHUNK_SIZE;
+    buf_is_chunk = 1;
+    buf = maybe_get_chunk();
+    if(!buf) {
+        bufsize = 2048;
+        buf_is_chunk = 0;
+        buf = malloc(2048);
+        if(buf == NULL) {
+            do_log(L_ERROR, "Couldn't allocate buffer.\n");
+            return -1;
+        }
+    }
+
+ format_again:
+    n = snnprintf(buf, 0, bufsize, "HTTP/1.1 %3d %s",
+                  object->code, object->message->string);
+
+    n = httpWriteObjectHeaders(buf, n, bufsize, object, 0, -1);
+    if(n < 0)
+        goto overflow;
+
+    n = snnprintf(buf, n, bufsize, "\r\nX-Polipo-Location: ");
+    n = snnprint_n(buf, n, bufsize, object->key, object->key_size);
+
+    if(object->age >= 0 && object->age != object->date) {
+        n = snnprintf(buf, n, bufsize, "\r\nX-Polipo-Date: ");
+        n = format_time(buf, n, bufsize, object->age);
+    }
+
+    if(object->atime >= 0) {
+        n = snnprintf(buf, n, bufsize, "\r\nX-Polipo-Access: ");
+        n = format_time(buf, n, bufsize, object->atime);
+    }
+
+    if(n < 0)
+        goto overflow;
+
+    if(body_offset < 0)
+        body_offset = chooseBodyOffset(n, object);
+
+    if(body_offset > bufsize)
+        goto overflow;
+
+    if(body_offset > 0 && body_offset != n + 4)
+        n = snnprintf(buf, n, bufsize, "\r\nX-Polipo-Body-Offset: %d",
+                      body_offset);
+
+    n = snnprintf(buf, n, bufsize, "\r\n\r\n");
+    if(n < 0)
+        goto overflow;
+
+    if(body_offset < 0)
+        body_offset = n;
+    if(n > body_offset) {
+        error = -2;
+        goto fail;
+    }
+
+    if(n < body_offset)
+        memset(buf + n, 0, body_offset - n);
+
+ again:
+#ifdef HAVE_READV_WRITEV
+    if(chunk_len > 0) {
+        struct iovec iov[2];
+        iov[0].iov_base = buf;
+        iov[0].iov_len = body_offset;
+        iov[1].iov_base = chunk;
+        iov[1].iov_len = chunk_len;
+        rc = writev(fd, iov, 2);
+    } else
+#endif
+        rc = write(fd, buf, body_offset);
+
+    if(rc < 0 && errno == EINTR)
+        goto again;
+
+    if(rc < body_offset)
+        goto fail;
+    if(object->length >= 0 && 
+       rc - body_offset >= object->length)
+        object->flags |= OBJECT_DISK_ENTRY_COMPLETE;
+
+    *body_offset_return = body_offset;
+    if(buf_is_chunk)
+        dispose_chunk(buf);
+    else
+        free(buf);
+    return rc;
+
+ overflow:
+    if(bufsize < bigBufferSize) {
+        char *oldbuf = buf;
+        buf = malloc(bigBufferSize);
+        if(!buf) {
+            do_log(L_ERROR, "Couldn't allocate big buffer.\n");
+            goto fail;
+        }
+        bufsize = bigBufferSize;
+        if(oldbuf) {
+            if(buf_is_chunk)
+                dispose_chunk(oldbuf);
+            else
+                free(oldbuf);
+        }
+        buf_is_chunk = 0;
+        goto format_again;
+    }
+    /* fall through */
+
+ fail:
+    if(buf_is_chunk)
+        dispose_chunk(buf);
+    else
+        free(buf);
+    return error;
+}
+
+typedef struct _MimeEntry {
+    char *extension;
+    char *mime;
+} MimeEntryRec;
+
+static const MimeEntryRec mimeEntries[] = {
+    { "html", "text/html" },
+    { "htm", "text/html" },
+    { "text", "text/plain" },
+    { "txt", "text/plain" },
+    { "png", "image/png" },
+    { "gif", "image/gif" },
+    { "jpeg", "image/jpeg" },
+    { "jpg", "image/jpeg" },
+    { "ico", "image/x-icon" },
+    { "pdf", "application/pdf" },
+    { "ps", "application/postscript" },
+    { "tar", "application/x-tar" },
+    { "pac", "application/x-ns-proxy-autoconfig" },
+    { "css", "text/css" },
+    { "js",  "application/x-javascript" },
+    { "xml", "text/xml" },
+    { "swf", "application/x-shockwave-flash" },
+};
+
+static char*
+localObjectMimeType(ObjectPtr object, char **encoding_return)
+{
+    char *name = object->key;
+    int nlen = object->key_size;
+    int i;
+
+    assert(nlen >= 1);
+
+    if(name[nlen - 1] == '/') {
+        *encoding_return = NULL;
+        return "text/html";
+    }
+
+    if(nlen < 3) {
+        *encoding_return = NULL;
+        return "application/octet-stream";
+    }
+
+    if(memcmp(name + nlen - 3, ".gz", 3) == 0) {
+        *encoding_return = "x-gzip";
+        nlen -= 3;
+    } else if(memcmp(name + nlen - 2, ".Z", 2) == 0) {
+        *encoding_return = "x-compress";
+        nlen -= 2;
+    } else {
+        *encoding_return = NULL;
+    }
+
+    for(i = 0; i < sizeof(mimeEntries) / sizeof(mimeEntries[0]); i++) {
+        int len = strlen(mimeEntries[i].extension);
+        if(nlen > len && 
+           name[nlen - len - 1] == '.' &&
+           memcmp(name + nlen - len, mimeEntries[i].extension, len) == 0)
+            return mimeEntries[i].mime;
+    }
+
+    return "application/octet-stream";
+}
+
+/* Same interface as validateEntry -- see below */
+int
+validateLocalEntry(ObjectPtr object, int fd,
+                   int *body_offset_return, off_t *offset_return)
+{
+    struct stat ss;
+    char buf[512];
+    int n, rc;
+    char *encoding;
+
+    rc = fstat(fd, &ss);
+    if(rc < 0) {
+        do_log_error(L_ERROR, errno, "Couldn't stat");
+        return -1;
+    }
+
+    if(S_ISREG(ss.st_mode)) {
+        if(!(ss.st_mode & S_IROTH) ||
+           (object->length >= 0 && object->length != ss.st_size) ||
+           (object->last_modified >= 0 && 
+            object->last_modified != ss.st_mtime))
+            return -1;
+    } else {
+        notifyObject(object);
+        return -1;
+    }
+    
+    n = snnprintf(buf, 0, 512, "%lx-%lx-%lx",
+                  (unsigned long)ss.st_ino,
+                  (unsigned long)ss.st_size,
+                  (unsigned long)ss.st_mtime);
+    if(n >= 512)
+        n = -1;
+
+    if(n > 0 && object->etag) {
+        if(strlen(object->etag) != n ||
+           memcmp(object->etag, buf, n) != 0)
+            return -1;
+    }
+
+    if(!(object->flags & OBJECT_INITIAL)) {
+        if(!object->last_modified && !object->etag)
+            return -1;
+    }
+       
+    if(object->flags & OBJECT_INITIAL) {
+        object->length = ss.st_size;
+        object->last_modified = ss.st_mtime;
+        object->date = current_time.tv_sec;
+        object->age = current_time.tv_sec;
+        object->code = 200;
+        if(n > 0)
+            object->etag = strdup(buf); /* okay if fails */
+        object->message = internAtom("Okay");
+        n = snnprintf(buf, 0, 512,
+                      "\r\nServer: Polipo"
+                      "\r\nContent-Type: %s",
+                      localObjectMimeType(object, &encoding));
+        if(encoding != NULL)
+            n = snnprintf(buf, n, 512,
+                          "\r\nContent-Encoding: %s", encoding);
+        if(n < 0)
+            return -1;
+        object->headers = internAtomN(buf, n);
+        if(object->headers == NULL)
+            return -1;
+        object->flags &= ~OBJECT_INITIAL;
+    }
+
+    if(body_offset_return)
+        *body_offset_return = 0;
+    if(offset_return)
+        *offset_return = 0;
+    return 0;
+}
+
+/* Assumes fd is at offset 0.
+   Returns -1 if not valid, 1 if metadata should be written out, 0
+   otherwise. */
+int
+validateEntry(ObjectPtr object, int fd, 
+              int *body_offset_return, off_t *offset_return)
+{
+    char *buf;
+    int buf_is_chunk, bufsize;
+    int rc, n;
+    int dummy;
+    int code;
+    AtomPtr headers;
+    time_t date, last_modified, expires, polipo_age, polipo_access;
+    int length;
+    off_t offset = -1;
+    int body_offset;
+    char *etag;
+    AtomPtr via;
+    CacheControlRec cache_control;
+    char *location;
+    AtomPtr message;
+    int dirty = 0;
+
+    if(object->flags & OBJECT_LOCAL)
+        return validateLocalEntry(object, fd,
+                                  body_offset_return, offset_return);
+
+    if(!(object->flags & OBJECT_PUBLIC) && (object->flags & OBJECT_INITIAL))
+        return 0;
+
+    /* get_chunk might trigger object expiry */
+    bufsize = CHUNK_SIZE;
+    buf_is_chunk = 1;
+    buf = maybe_get_chunk();
+    if(!buf) {
+        bufsize = 2048;
+        buf_is_chunk = 0;
+        buf = malloc(2048);
+        if(buf == NULL) {
+            do_log(L_ERROR, "Couldn't allocate buffer.\n");
+            return -1;
+        }
+    }
+
+ again:
+    rc = read(fd, buf, bufsize);
+    if(rc < 0) {
+        if(errno == EINTR)
+            goto again;
+        do_log_error(L_ERROR, errno, "Couldn't read disk entry");
+        goto fail;
+    }
+    offset = rc;
+
+ parse_again:
+    n = findEndOfHeaders(buf, 0, rc, &dummy);
+    if(n < 0) {
+        char *oldbuf = buf;
+        if(bufsize < bigBufferSize) {
+            buf = malloc(bigBufferSize);
+            if(!buf) {
+                do_log(L_ERROR, "Couldn't allocate big buffer.\n");
+                goto fail;
+            }
+            bufsize = bigBufferSize;
+            memcpy(buf, oldbuf, offset);
+            if(buf_is_chunk)
+                dispose_chunk(oldbuf);
+            else
+                free(oldbuf);
+            buf_is_chunk = 0;
+        again2:
+            rc = read(fd, buf + offset, bufsize - offset);
+            if(rc < 0) {
+                if(errno == EINTR)
+                    goto again2;
+                do_log_error(L_ERROR, errno, "Couldn't read disk entry");
+                goto fail;
+            }
+            offset += rc;
+            goto parse_again;
+        }
+        do_log(L_ERROR, "Couldn't parse disk entry.\n");
+        goto fail;
+    }
+
+    rc = httpParseServerFirstLine(buf, &code, &dummy, &message);
+    if(rc < 0) {
+        do_log(L_ERROR, "Couldn't parse disk entry.\n");
+        goto fail;
+    }
+
+    if(object->code != 0 && object->code != code) {
+        releaseAtom(message);
+        goto fail;
+    }
+
+    rc = httpParseHeaders(0, NULL, buf, rc, NULL,
+                          &headers, &length, &cache_control, NULL, NULL,
+                          &date, &last_modified, &expires, &polipo_age,
+                          &polipo_access, &body_offset,
+                          NULL, &etag, NULL,
+                          NULL, NULL, &location, &via, NULL);
+    if(rc < 0) {
+        releaseAtom(message);
+        goto fail;
+    }
+    if(body_offset < 0)
+        body_offset = n;
+
+    if(!location || strlen(location) != object->key_size ||
+       memcmp(location, object->key, object->key_size) != 0) {
+        do_log(L_ERROR, "Inconsistent cache file for %s.\n", scrub(location));
+        goto invalid;
+    }
+
+    if(polipo_age < 0)
+        polipo_age = date;
+
+    if(polipo_age < 0) {
+        do_log(L_ERROR, "Undated disk entry for %s.\n", scrub(location));
+        goto invalid;
+    }
+
+    if(!(object->flags & OBJECT_INITIAL)) {
+        if((last_modified >= 0) != (object->last_modified >= 0))
+            goto invalid;
+
+        if((object->cache_control & CACHE_MISMATCH) ||
+           (cache_control.flags & CACHE_MISMATCH))
+            goto invalid;
+
+        if(last_modified >= 0 && object->last_modified >= 0 &&
+           last_modified != object->last_modified)
+            goto invalid;
+
+        if(length >= 0 && object->length >= 0)
+            if(length != object->length)
+                goto invalid;
+
+        if(!!etag != !!object->etag)
+            goto invalid;
+
+        if(etag && object->etag && strcmp(etag, object->etag) != 0)
+            goto invalid;
+
+        /* If we don't have a usable ETag, and either CACHE_VARY or we
+           don't have a last-modified date, we validate disk entries by
+           using their date. */
+        if(!(etag && object->etag) &&
+           (!(last_modified >= 0 && object->last_modified >= 0) ||
+            ((cache_control.flags & CACHE_VARY) ||
+             (object->cache_control & CACHE_VARY)))) {
+            if(date >= 0 && date != object->date)
+                goto invalid;
+            if(polipo_age >= 0 && polipo_age != object->age)
+                goto invalid;
+        }
+        if((object->cache_control & CACHE_VARY) && dontTrustVaryETag >= 1) {
+            /* Check content-type to work around mod_gzip bugs */
+            if(!httpHeaderMatch(atomContentType, object->headers, headers) ||
+               !httpHeaderMatch(atomContentEncoding, object->headers, headers))
+                goto invalid;
+        }
+    }
+
+    if(location)
+        free(location);
+
+    if(headers) {
+        if(!object->headers)
+            object->headers = headers;
+        else
+            releaseAtom(headers);
+    }
+
+    if(object->code == 0) {
+        object->code = code;
+        object->message = retainAtom(message);
+    }
+    if(object->date <= date)
+        object->date = date;
+    else 
+        dirty = 1;
+    if(object->last_modified < 0)
+        object->last_modified = last_modified;
+    if(object->expires < 0)
+        object->expires = expires;
+    else if(object->expires > expires)
+        dirty = 1;
+    if(object->age < 0)
+        object->age = polipo_age;
+    else if(object->age > polipo_age)
+        dirty = 1;
+    if(object->atime <= polipo_access)
+        object->atime = polipo_access;
+    else
+        dirty = 1;
+
+    object->cache_control |= cache_control.flags;
+    object->max_age = cache_control.max_age;
+    object->s_maxage = cache_control.s_maxage;
+
+    if(object->age < 0) object->age = object->date;
+    if(object->age < 0) object->age = 0; /* a long time ago */
+    if(object->length < 0) object->length = length;
+    if(!object->etag)
+        object->etag = etag;
+    else {
+        if(etag)
+            free(etag);
+    }
+    releaseAtom(message);
+
+    if(object->flags & OBJECT_INITIAL) object->via = via;
+    object->flags &= ~OBJECT_INITIAL;
+    if(offset > body_offset) {
+        /* We need to make sure we don't invoke object expiry recursively */
+        objectSetChunks(object, 1);
+        if(object->numchunks >= 1) {
+            if(object->chunks[0].data == NULL)
+                object->chunks[0].data = maybe_get_chunk();
+            if(object->chunks[0].data)
+                objectAddData(object, buf + body_offset,
+                              0, MIN(offset - body_offset, CHUNK_SIZE));
+        }
+    }
+
+    httpTweakCachability(object);
+
+    if(buf_is_chunk)
+        dispose_chunk(buf);
+    else
+        free(buf);
+    if(body_offset_return) *body_offset_return = body_offset;
+    if(offset_return) *offset_return = offset;
+    return dirty;
+
+ invalid:
+    releaseAtom(message);
+    if(etag) free(etag);
+    if(location) free(location);
+    if(via) releaseAtom(via);
+    /* fall through */
+
+ fail:
+    if(buf_is_chunk)
+        dispose_chunk(buf);
+    else
+        free(buf);
+    return -1;
+}
+
+void
+dirtyDiskEntry(ObjectPtr object)
+{
+    DiskCacheEntryPtr entry = object->disk_entry;
+    if(entry && entry != &negativeEntry) entry->metadataDirty = 1;
+}
+
+int
+revalidateDiskEntry(ObjectPtr object)
+{
+    DiskCacheEntryPtr entry = object->disk_entry;
+    int rc;
+    int body_offset;
+
+    if(!entry || entry == &negativeEntry)
+        return 1;
+
+    CHECK_ENTRY(entry);
+    rc = entrySeek(entry, 0);
+    if(rc < 0) return 0;
+
+    rc = validateEntry(object, entry->fd, &body_offset, &entry->offset);
+    if(rc < 0) {
+        destroyDiskEntry(object, 0);
+        return 0;
+    }
+    if(body_offset != entry->body_offset) {
+        do_log(L_WARN, "Inconsistent body offset (%d != %d).\n",
+               body_offset, entry->body_offset);
+        destroyDiskEntry(object, 0);
+        return 0;
+    }
+
+    entry->metadataDirty |= !!rc;
+    CHECK_ENTRY(entry);
+    return 1;
+}
+
+static DiskCacheEntryPtr
+makeDiskEntry(ObjectPtr object, int create)
+{
+    DiskCacheEntryPtr entry = NULL;
+    char buf[1024];
+    int fd = -1;
+    int negative = 0, size = -1, name_len = -1;
+    char *name = NULL;
+    off_t offset = -1;
+    int body_offset = -1;
+    int rc;
+    int local = (object->flags & OBJECT_LOCAL) != 0;
+    int dirty = 0;
+
+   if(local && create)
+       return NULL;
+
+    if(!local && !(object->flags & OBJECT_PUBLIC))
+        return NULL;
+
+    if(maxDiskCacheEntrySize >= 0) {
+        if(object->length > 0) {
+            if(object->length > maxDiskCacheEntrySize)
+                return NULL;
+        } else {
+            if(object->size > maxDiskCacheEntrySize)
+                return NULL;
+        }
+    }
+
+    if(object->disk_entry) {
+        entry = object->disk_entry;
+        CHECK_ENTRY(entry);
+        if(entry != &negativeEntry) {
+            /* We'll keep the entry -- put it at the front. */
+            if(entry != diskEntries && entry != &negativeEntry) {
+                entry->previous->next = entry->next;
+                if(entry->next)
+                    entry->next->previous = entry->previous;
+                else
+                    diskEntriesLast = entry->previous;
+                entry->next = diskEntries;
+                diskEntries->previous = entry;
+                entry->previous = NULL;
+                diskEntries = entry;
+            }
+            return entry;
+        } else {
+            if(entry == &negativeEntry) {
+                negative = 1;
+                if(!create) return NULL;
+                object->disk_entry = NULL;
+            }
+            entry = NULL;
+            destroyDiskEntry(object, 0);
+        }
+    }
+
+    if(numDiskEntries > maxDiskEntries)
+        destroyDiskEntry(diskEntriesLast->object, 0);
+
+    if(!local) {
+        if(diskCacheRoot == NULL || diskCacheRoot->length <= 0)
+            return NULL;
+        name_len = urlFilename(buf, 1024, object->key, object->key_size);
+        if(name_len < 0) return NULL;
+        if(!negative)
+            fd = open(buf, O_RDWR | O_BINARY);
+        if(fd >= 0) {
+            rc = validateEntry(object, fd, &body_offset, &offset);
+            if(rc >= 0) {
+                dirty = rc;
+            } else {
+                close(fd);
+                fd = -1;
+                rc = unlink(buf);
+                if(rc < 0 && errno != ENOENT) {
+                    do_log_error(L_WARN,  errno,
+                                 "Couldn't unlink stale disk entry %s", 
+                                 scrub(buf));
+                    /* But continue -- it's okay to have stale entries. */
+                }
+            }
+        }
+
+        if(fd < 0 && create && name_len > 0 && 
+           !(object->flags & OBJECT_INITIAL)) {
+            fd = createFile(buf, diskCacheRoot->length);
+            if(fd < 0)
+                return NULL;
+
+            if(fd >= 0) {
+                char *data = NULL;
+                int dsize = 0;
+                if(object->numchunks > 0) {
+                    data = object->chunks[0].data;
+                    dsize = object->chunks[0].size;
+                }
+                rc = writeHeaders(fd, &body_offset, object, data, dsize);
+                if(rc < 0) {
+                    do_log_error(L_ERROR, errno, "Couldn't write headers");
+                    rc = unlink(buf);
+                    if(rc < 0 && errno != ENOENT)
+                        do_log_error(L_ERROR, errno,
+                                     "Couldn't unlink truncated entry %s", 
+                                     scrub(buf));
+                    close(fd);
+                    return NULL;
+                }
+                assert(rc >= body_offset);
+                size = rc - body_offset;
+                offset = rc;
+                dirty = 0;
+            }
+        }
+    } else {
+        /* local */
+        if(localDocumentRoot == NULL || localDocumentRoot->length == 0)
+            return NULL;
+
+        name_len = 
+            localFilename(buf, 1024, object->key, object->key_size);
+        if(name_len < 0)
+            return NULL;
+        fd = open(buf, O_RDONLY | O_BINARY);
+        if(fd >= 0) {
+            if(validateEntry(object, fd, &body_offset, NULL) < 0) {
+                close(fd);
+                fd = -1;
+            }
+        }
+        offset = 0;
+    }
+
+    if(fd < 0) {
+        object->disk_entry = &negativeEntry;
+        return NULL;
+    }
+    assert(body_offset >= 0);
+
+    name = strdup_n(buf, name_len);
+    if(name == NULL) {
+        do_log(L_ERROR, "Couldn't allocate name.\n");
+        close(fd);
+        fd = -1;
+        return NULL;
+    }
+
+    entry = malloc(sizeof(DiskCacheEntryRec));
+    if(entry == NULL) {
+        do_log(L_ERROR, "Couldn't allocate entry.\n");
+        free(name);
+        close(fd);
+        return NULL;
+    }
+
+    entry->filename = name;
+    entry->object = object;
+    entry->fd = fd;
+    entry->body_offset = body_offset;
+    entry->local = local;
+    entry->offset = offset;
+    entry->size = size;
+    entry->metadataDirty = dirty;
+
+    entry->next = diskEntries;
+    if(diskEntries)
+        diskEntries->previous = entry;
+    diskEntries = entry;
+    if(diskEntriesLast == NULL)
+        diskEntriesLast = entry;
+    entry->previous = NULL;
+    numDiskEntries++;
+
+    object->disk_entry = entry;
+
+    CHECK_ENTRY(entry);
+    return entry;
+}
+
+/* Rewrite a disk cache entry, used when the body offset needs to change. */
+static int
+rewriteEntry(ObjectPtr object)
+{
+    int old_body_offset = object->disk_entry->body_offset;
+    int fd, rc, n;
+    DiskCacheEntryPtr entry;
+    char* buf;
+    int buf_is_chunk, bufsize;
+    int offset;
+
+    fd = dup(object->disk_entry->fd);
+    if(fd < 0) {
+        do_log_error(L_ERROR, errno, "Couldn't duplicate file descriptor");
+        return -1;
+    }
+
+    rc = destroyDiskEntry(object, 1);
+    if(rc < 0) {
+        close(fd);
+        return -1;
+    }
+    entry = makeDiskEntry(object, 1);
+    if(!entry) {
+        close(fd);
+        return -1;
+    }
+
+    offset = diskEntrySize(object);
+    if(offset < 0) {
+        close(fd);
+        return -1;
+    }
+
+    bufsize = CHUNK_SIZE;
+    buf_is_chunk = 1;
+    buf = maybe_get_chunk();
+    if(!buf) {
+        bufsize = 2048;
+        buf_is_chunk = 0;
+        buf = malloc(2048);
+        if(buf == NULL) {
+            do_log(L_ERROR, "Couldn't allocate buffer.\n");
+            close(fd);
+            return -1;
+        }
+    }
+
+    rc = lseek(fd, old_body_offset + offset, SEEK_SET);
+    if(rc < 0)
+        goto done;
+
+    while(1) {
+        CHECK_ENTRY(entry);
+        n = read(fd, buf, bufsize);
+        if(n < 0 && errno == EINTR)
+            continue;
+        if(n <= 0)
+            goto done;
+        rc = entrySeek(entry, entry->body_offset + offset);
+        if(rc < 0)
+            goto done;
+    write_again:
+        rc = write(entry->fd, buf, n);
+        if(rc >= 0) {
+            entry->offset += rc;
+            entry->size += rc;
+        } else if(errno == EINTR) {
+            goto write_again;
+        }
+        if(rc < n)
+            goto done;
+    }
+
+ done:
+    CHECK_ENTRY(entry);
+    if(object->length >= 0 && entry->size == object->length)
+        object->flags |= OBJECT_DISK_ENTRY_COMPLETE;
+    close(fd);
+    if(buf_is_chunk)
+        dispose_chunk(buf);
+    else
+        free(buf);
+    return 1;
+}
+            
+int
+destroyDiskEntry(ObjectPtr object, int d)
+{
+    DiskCacheEntryPtr entry = object->disk_entry;
+    int rc, urc = 1;
+
+    assert(!entry || !entry->local || !d);
+
+    if(d && !entry)
+        entry = makeDiskEntry(object, 0);
+
+    CHECK_ENTRY(entry);
+
+    if(!entry || entry == &negativeEntry) {
+        return 1;
+    }
+
+    assert(entry->object == object);
+
+    if(maxDiskCacheEntrySize >= 0 && object->size > maxDiskCacheEntrySize) {
+        /* See writeoutToDisk */
+        d = 1;
+    }
+
+    if(d) {
+        entry->object->flags &= ~OBJECT_DISK_ENTRY_COMPLETE;
+        if(entry->filename) {
+            urc = unlink(entry->filename);
+            if(urc < 0)
+                do_log_error(L_WARN, errno, 
+                             "Couldn't unlink %s", scrub(entry->filename));
+        }
+    } else {
+        if(entry && entry->metadataDirty)
+            writeoutMetadata(object);
+        makeDiskEntry(object, 0);
+        /* rewriteDiskEntry may change the disk entry */
+        entry = object->disk_entry;
+        if(entry == NULL || entry == &negativeEntry)
+            return 0;
+        if(diskCacheWriteoutOnClose > 0) {
+            reallyWriteoutToDisk(object, -1, diskCacheWriteoutOnClose);
+            entry = object->disk_entry;
+            if(entry == NULL || entry == &negativeEntry)
+                return 0;
+        }
+    }
+ again:
+    rc = close(entry->fd);
+    if(rc < 0 && errno == EINTR)
+        goto again;
+
+    entry->fd = -1;
+
+    if(entry->filename)
+        free(entry->filename);
+    entry->filename = NULL;
+
+    if(entry->previous)
+        entry->previous->next = entry->next;
+    else
+        diskEntries = entry->next;
+    if(entry->next)
+        entry->next->previous = entry->previous;
+    else
+        diskEntriesLast = entry->previous;
+
+    numDiskEntries--;
+    assert(numDiskEntries >= 0);
+
+    free(entry);
+    object->disk_entry = NULL;
+    if(urc < 0)
+        return -1;
+    else
+        return 1;
+}
+
+ObjectPtr 
+objectGetFromDisk(ObjectPtr object)
+{
+    DiskCacheEntryPtr entry = makeDiskEntry(object, 0);
+    if(!entry) return NULL;
+    return object;
+}
+
+
+int 
+objectFillFromDisk(ObjectPtr object, int offset, int chunks)
+{
+    DiskCacheEntryPtr entry;
+    int rc, result;
+    int i, j, k;
+    int complete;
+
+    if(object->type != OBJECT_HTTP)
+        return 0;
+
+    if(object->flags & OBJECT_LINEAR)
+        return 0;
+
+    if(object->length >= 0) {
+        chunks = MIN(chunks, 
+                     (object->length - offset + CHUNK_SIZE - 1) / CHUNK_SIZE);
+    }
+
+    rc = objectSetChunks(object, offset / CHUNK_SIZE + chunks);
+    if(rc < 0)
+        return 0;
+
+    complete = 1;
+    if(object->flags & OBJECT_INITIAL) {
+        complete = 0;
+    } else if((object->length < 0 || object->size < object->length) &&
+              object->size < (offset / CHUNK_SIZE + chunks) * CHUNK_SIZE) {
+        complete = 0;
+    } else {
+        for(k = 0; k < chunks; k++) {
+            int s;
+            i = offset / CHUNK_SIZE + k;
+            s = MIN(CHUNK_SIZE, object->size - i * CHUNK_SIZE);
+            if(object->chunks[i].size < s) {
+                complete = 0;
+                break;
+            }
+        }
+    }
+
+    if(complete)
+        return 1;
+
+    /* This has the side-effect of revalidating the entry, which is
+       what makes HEAD requests work. */
+    entry = makeDiskEntry(object, 0);
+    if(!entry)
+        return 0;
+                
+    for(k = 0; k < chunks; k++) {
+        i = offset / CHUNK_SIZE + k;
+        if(!object->chunks[i].data)
+            object->chunks[i].data = get_chunk();
+        if(!object->chunks[i].data) {
+            chunks = k;
+            break;
+        }
+        lockChunk(object, i);
+    }
+
+    result = 0;
+
+    for(k = 0; k < chunks; k++) {
+        int o;
+        i = offset / CHUNK_SIZE + k;
+        j = object->chunks[i].size;
+        o = i * CHUNK_SIZE + j;
+
+        if(object->chunks[i].size == CHUNK_SIZE)
+            continue;
+
+        if(entry->size >= 0 && entry->size <= o)
+            break;
+
+        if(entry->offset != entry->body_offset + o) {
+            rc = entrySeek(entry, entry->body_offset + o);
+            if(rc < 0) {
+                result = 0;
+                break;
+            }
+        }
+
+        CHECK_ENTRY(entry);
+        again:
+        rc = read(entry->fd, object->chunks[i].data + j, CHUNK_SIZE - j);
+        if(rc < 0) {
+            if(errno == EINTR)
+                goto again;
+            entry->offset = -1;
+            do_log_error(L_ERROR, errno, "Couldn't read");
+            break;
+        }
+
+        entry->offset += rc;
+        object->chunks[i].size += rc;
+        if(object->size < o + rc)
+            object->size = o + rc;
+
+        if(entry->object->length >= 0 && entry->size < 0 &&
+           entry->offset - entry->body_offset == entry->object->length)
+            entry->size = entry->object->length;
+            
+        if(rc < CHUNK_SIZE - j) {
+            /* Paranoia: the read may have been interrupted half-way. */
+            if(entry->size < 0) {
+                if(rc == 0 ||
+                   (entry->object->length >= 0 &&
+                    entry->object->length == 
+                    entry->offset - entry->body_offset))
+                    entry->size = entry->offset - entry->body_offset;
+                break;
+            } else if(entry->size != entry->offset - entry->body_offset) {
+                if(rc == 0 || 
+                   entry->size < entry->offset - entry->body_offset) {
+                    do_log(L_WARN,
+                           "Disk entry size changed behind our back: "
+                           "%ld -> %ld (%d).\n",
+                           (long)entry->size,
+                           (long)entry->offset - entry->body_offset,
+                           object->size);
+                    entry->size = -1;
+                }
+            }
+            break;
+        }
+
+        CHECK_ENTRY(entry);
+        result = 1;
+    }
+
+    CHECK_ENTRY(object->disk_entry);
+    for(k = 0; k < chunks; k++) {
+        i = offset / CHUNK_SIZE + k;
+        unlockChunk(object, i);
+    }
+
+    if(result > 0) {
+        notifyObject(object);
+        return 1;
+    } else {
+        return 0;
+    }
+}
+
+int 
+writeoutToDisk(ObjectPtr object, int upto, int max)
+{
+    if(maxDiskCacheEntrySize >= 0 && object->size > maxDiskCacheEntrySize) {
+        /* An object was created with an unknown length, and then grew
+           beyond maxDiskCacheEntrySize.  Destroy the disk entry. */
+        destroyDiskEntry(object, 1);
+        return 0;
+    }
+
+    return reallyWriteoutToDisk(object, upto, max);
+}
+        
+static int 
+reallyWriteoutToDisk(ObjectPtr object, int upto, int max)
+{
+    DiskCacheEntryPtr entry;
+    int rc;
+    int i, j;
+    int offset;
+    int bytes = 0;
+
+    if(upto < 0)
+        upto = object->size;
+
+    if((object->cache_control & CACHE_NO_STORE) || 
+       (object->flags & OBJECT_LOCAL))
+        return 0;
+
+    if((object->flags & OBJECT_DISK_ENTRY_COMPLETE) && !object->disk_entry)
+        return 0;
+
+    entry = makeDiskEntry(object, 1);
+    if(!entry) return 0;
+
+    assert(!entry->local);
+
+    if(object->flags & OBJECT_DISK_ENTRY_COMPLETE)
+        goto done;
+
+    diskEntrySize(object);
+    if(entry->size < 0)
+        return 0;
+
+    if(object->length >= 0 && entry->size >= object->length) {
+        object->flags |= OBJECT_DISK_ENTRY_COMPLETE;
+        goto done;
+    }
+
+    if(entry->size >= upto)
+        goto done;
+
+    offset = entry->size;
+
+    /* Avoid a seek in case we start writing at the beginning */
+    if(offset == 0 && entry->metadataDirty) {
+        writeoutMetadata(object);
+        /* rewriteDiskEntry may change the entry */
+        entry = makeDiskEntry(object, 0);
+        if(entry == NULL)
+            return 0;
+    }
+
+    rc = entrySeek(entry, offset + entry->body_offset);
+    if(rc < 0) return 0;
+
+    do {
+        if(max >= 0 && bytes >= max)
+            break;
+        CHECK_ENTRY(entry);
+        assert(entry->offset == offset + entry->body_offset);
+        i = offset / CHUNK_SIZE;
+        j = offset % CHUNK_SIZE;
+        if(i >= object->numchunks)
+            break;
+        if(object->chunks[i].size <= j)
+            break;
+    again:
+        rc = write(entry->fd, object->chunks[i].data + j,
+                   object->chunks[i].size - j);
+        if(rc < 0) {
+            if(errno == EINTR)
+                goto again;
+            do_log_error(L_ERROR, errno, "Couldn't write disk entry");
+            break;
+        }
+        entry->offset += rc;
+        offset += rc;
+        bytes += rc;
+        if(entry->size < offset)
+            entry->size = offset;
+    } while(j + rc >= CHUNK_SIZE);
+
+ done:
+    CHECK_ENTRY(entry);
+    if(entry->metadataDirty)
+        writeoutMetadata(object);
+
+    return bytes;
+}
+
+int 
+writeoutMetadata(ObjectPtr object)
+{
+    DiskCacheEntryPtr entry;
+    int rc;
+
+    if((object->cache_control & CACHE_NO_STORE) || 
+       (object->flags & OBJECT_LOCAL))
+        return 0;
+    
+    entry = makeDiskEntry(object, 0);
+    if(entry == NULL || entry == &negativeEntry)
+        goto fail;
+
+    assert(!entry->local);
+
+    rc = entrySeek(entry, 0);
+    if(rc < 0) goto fail;
+
+    rc = writeHeaders(entry->fd, &entry->body_offset, object, NULL, 0);
+    if(rc == -2) {
+        rc = rewriteEntry(object);
+        if(rc < 0) return 0;
+        return 1;
+    }
+    if(rc < 0) goto fail;
+    entry->offset = rc;
+    entry->metadataDirty = 0;
+    return 1;
+
+ fail:
+    /* We need this in order to avoid trying to write this entry out
+       multiple times. */
+    if(entry && entry != &negativeEntry)
+        entry->metadataDirty = 0;
+    return 0;
+}
+
+static void
+mergeDobjects(DiskObjectPtr dst, DiskObjectPtr src)
+{
+    if(dst->filename == NULL) {
+        dst->filename = src->filename;
+        dst->body_offset = src->body_offset;
+    } else
+        free(src->filename);
+    free(src->location);
+    if(dst->length < 0)
+        dst->length = src->length;
+    if(dst->size < 0)
+        dst->size = src->size;
+    if(dst->age < 0)
+        dst->age = src->age;
+    if(dst->date < 0)
+        dst->date = src->date;
+    if(dst->last_modified < 0)
+        dst->last_modified = src->last_modified;
+    free(src);
+}
+
+DiskObjectPtr
+readDiskObject(char *filename, struct stat *sb)
+{
+    int fd, rc, n, dummy, code;
+    int length, size;
+    time_t date, last_modified, age, atime, expires;
+    char *location = NULL, *fn = NULL;
+    DiskObjectPtr dobject;
+    char *buf;
+    int buf_is_chunk, bufsize;
+    int body_offset;
+    struct stat ss;
+
+    fd = -1;
+
+    if(sb == NULL) {
+        rc = stat(filename, &ss);
+        if(rc < 0) {
+            do_log_error(L_WARN, errno, "Couldn't stat %s", scrub(filename));
+            return NULL;
+        }
+        sb = &ss;
+    }
+
+    buf_is_chunk = 1;
+    bufsize = CHUNK_SIZE;
+    buf = get_chunk();
+    if(buf == NULL) {
+        do_log(L_ERROR, "Couldn't allocate buffer.\n");
+        return NULL;
+    }
+
+    if(S_ISREG(sb->st_mode)) {
+        fd = open(filename, O_RDONLY | O_BINARY);
+        if(fd < 0)
+            goto fail;
+    again:
+        rc = read(fd, buf, bufsize);
+        if(rc < 0)
+            goto fail;
+        
+        n = findEndOfHeaders(buf, 0, rc, &dummy);
+        if(n < 0) {
+            long lrc;
+            if(buf_is_chunk) {
+                dispose_chunk(buf);
+                buf_is_chunk = 0;
+                bufsize = bigBufferSize;
+                buf = malloc(bigBufferSize);
+                if(buf == NULL)
+                    goto fail2;
+                lrc = lseek(fd, 0, SEEK_SET);
+                if(lrc < 0)
+                    goto fail;
+                goto again;
+            }
+            goto fail;
+        }
+        
+        rc = httpParseServerFirstLine(buf, &code, &dummy, NULL);
+        if(rc < 0)
+            goto fail;
+
+        rc = httpParseHeaders(0, NULL, buf, rc, NULL,
+                              NULL, &length, NULL, NULL, NULL, 
+                              &date, &last_modified, &expires, &age,
+                              &atime, &body_offset, NULL,
+                              NULL, NULL, NULL, NULL, &location, NULL, NULL);
+        if(rc < 0 || location == NULL)
+            goto fail;
+        if(body_offset < 0)
+            body_offset = n;
+    
+        size = sb->st_size - body_offset;
+        if(size < 0)
+            size = 0;
+    } else if(S_ISDIR(sb->st_mode)) {
+        char *n;
+        n = dirnameUrl(buf, 512, (char*)filename, strlen(filename));
+        if(n == NULL)
+            goto fail;
+        location = strdup(n);
+        if(location == NULL)
+            goto fail;
+        length = -1;
+        size = -1;
+        body_offset = -1;
+        age = -1;
+        atime = -1;
+        date = -1;
+        last_modified = -1;
+        expires = -1;
+    } else {
+        goto fail;
+    }
+
+    dobject = malloc(sizeof(DiskObjectRec));
+    if(!dobject)
+        goto fail;
+    
+    fn = strdup(filename);
+    if(!fn)
+        goto fail;
+
+    if(buf_is_chunk)
+        dispose_chunk(buf);
+    else
+        free(buf);
+
+    dobject->location = location;
+    dobject->filename = fn;
+    dobject->length = length;
+    dobject->body_offset = body_offset;
+    dobject->size = size;
+    dobject->age = age;
+    dobject->access = atime;
+    dobject->date = date;
+    dobject->last_modified = last_modified;
+    dobject->expires = expires;
+    if(fd >= 0) close(fd);
+    return dobject;
+
+ fail:
+    if(buf_is_chunk)
+        dispose_chunk(buf);
+    else
+        free(buf);
+ fail2:
+    if(fd >= 0) close(fd);
+    if(location) free(location);
+    return NULL;
+}    
+    
+
+DiskObjectPtr
+processObject(DiskObjectPtr dobjects, char *filename, struct stat *sb)
+{
+    DiskObjectPtr dobject = NULL;
+    int c = 0;
+
+    dobject = readDiskObject((char*)filename, sb);
+    if(dobject == NULL)
+        return dobjects;
+
+    if(!dobjects ||
+       (c = strcmp(dobject->location, dobjects->location)) <= 0) {
+        if(dobjects && c == 0) {
+            mergeDobjects(dobjects, dobject);
+        } else {
+            dobject->next = dobjects;
+            dobjects = dobject;
+        }
+    } else {
+        DiskObjectPtr other = dobjects;
+        while(other->next) {
+            c = strcmp(dobject->location, other->next->location);
+            if(c < 0)
+                break;
+            other = other->next;
+        }
+        if(strcmp(dobject->location, other->location) == 0) {
+            mergeDobjects(other, dobject);
+        } else {
+            dobject->next = other->next;
+            other->next = dobject;
+        }
+    }
+    return dobjects;
+}
+
+/* Determine whether p is below root */
+static int
+filter(DiskObjectPtr p, const char *root, int n, int recursive)
+{
+    char *cp;
+    int m = strlen(p->location);
+    if(m < n)
+        return 0;
+    if(memcmp(root, p->location, n) != 0)
+        return 0;
+    if(recursive)
+        return 1;
+    if(m == 0 || p->location[m - 1] == '/')
+        return 1;
+    cp = strchr(p->location + n, '/');
+    if(cp && cp - p->location != m - 1)
+        return 0;
+    return 1;
+}
+
+/* Filter out all disk objects that are not under root */
+DiskObjectPtr
+filterDiskObjects(DiskObjectPtr from, const char *root, int recursive)
+{
+    int n = strlen(root);
+    DiskObjectPtr p, q;
+
+    while(from && !filter(from, root, n, recursive)) {
+        p = from;
+        from = p->next;
+        free(p->location);
+        free(p);
+    }
+
+    p = from;
+    while(p && p->next) {
+        if(!filter(p->next, root, n, recursive)) {
+            q = p->next;
+            p->next = q->next;
+            free(q->location);
+            free(q);
+        } else {
+            p = p->next;
+        }
+    }
+    return from;
+}
+
+DiskObjectPtr
+insertRoot(DiskObjectPtr from, const char *root)
+{
+    DiskObjectPtr p;
+
+    p = from;
+    while(p) {
+        if(strcmp(root, p->location) == 0)
+            return from;
+        p = p->next;
+    }
+
+    p = malloc(sizeof(DiskObjectRec));
+    if(!p) return from;
+    p->location = strdup(root);
+    if(p->location == NULL) {
+        free(p);
+        return from;
+    }
+    p->filename = NULL;
+    p->length = -1;
+    p->size = -1;
+    p->age = -1;
+    p->access = -1;
+    p->last_modified = -1;
+    p->expires = -1;
+    p->next = from;
+    return p;
+}
+
+/* Insert all missing directories in a sorted list of dobjects */
+DiskObjectPtr
+insertDirs(DiskObjectPtr from)
+{
+    DiskObjectPtr p, q, new;
+    int n, m;
+    char *cp;
+
+    p = NULL; q = from;
+    while(q) {
+        n = strlen(q->location);
+        if(n > 0 && q->location[n - 1] != '/') {
+            cp = strrchr(q->location, '/');
+            m = cp - q->location + 1;
+            if(cp && (!p || strlen(p->location) < m ||
+                      memcmp(p->location, q->location, m) != 0)) {
+                new = malloc(sizeof(DiskObjectRec));
+                if(!new) break;
+                new->location = strdup_n(q->location, m);
+                if(new->location == NULL) {
+                    free(new);
+                    break;
+                }
+                new->filename = NULL;
+                new->length = -1;
+                new->size = -1;
+                new->age = -1;
+                new->access = -1;
+                new->last_modified = -1;
+                new->expires = -1;
+                new->next = q;
+                if(p)
+                    p->next = new;
+                else
+                    from = new;
+            }
+        }
+        p = q;
+        q = q->next;
+    }
+    return from;
+}
+        
+void
+indexDiskObjects(FILE *out, const char *root, int recursive)
+{
+    int n, i, isdir;
+    DIR *dir;
+    struct dirent *dirent;
+    char buf[1024];
+    char *fts_argv[2];
+    FTS *fts;
+    FTSENT *fe;
+    DiskObjectPtr dobjects = NULL;
+    char *of = root[0] == '\0' ? "" : " of ";
+
+    fprintf(out, "<!DOCTYPE HTML PUBLIC "
+            "\"-//W3C//DTD HTML 4.01 Transitional//EN\" "
+            "\"http://www.w3.org/TR/html4/loose.dtd\">\n"
+            "<html><head>\n"
+            "<title>%s%s%s</title>\n"
+            "</head><body>\n"
+            "<h1>%s%s%s</h1>\n",
+            recursive ? "Recursive index" : "Index", of, root,
+            recursive ? "Recursive index" : "Index", of, root);
+
+    if(diskCacheRoot == NULL || diskCacheRoot->length <= 0) {
+        fprintf(out, "<p>No <tt>diskCacheRoot</tt>.</p>\n");
+        goto trailer;
+    }
+
+    if(diskCacheRoot->length >= 1024) {
+        fprintf(out,
+                "<p>The value of <tt>diskCacheRoot</tt> is "
+                "too long (%d).</p>\n",
+                diskCacheRoot->length);
+        goto trailer;
+    }
+
+    if(strlen(root) < 8) {
+        memcpy(buf, diskCacheRoot->string, diskCacheRoot->length);
+        buf[diskCacheRoot->length] = '\0';
+        n = diskCacheRoot->length;
+    } else {
+        n = urlDirname(buf, 1024, root, strlen(root));
+    }
+    if(n > 0) {
+        if(recursive) {
+            dir = NULL;
+            fts_argv[0] = buf;
+            fts_argv[1] = NULL;
+            fts = fts_open(fts_argv, FTS_LOGICAL, NULL);
+            if(fts) {
+                while(1) {
+                    fe = fts_read(fts);
+                    if(!fe) break;
+                    if(fe->fts_info != FTS_DP)
+                        dobjects =
+                            processObject(dobjects,
+                                          fe->fts_path,
+                                          fe->fts_info == FTS_NS ||
+                                          fe->fts_info == FTS_NSOK ?
+                                          fe->fts_statp : NULL);
+                }
+                fts_close(fts);
+            }
+        } else {
+            dir = opendir(buf);
+            if(dir) {
+                while(1) {
+                    dirent = readdir(dir);
+                    if(!dirent) break;
+                    if(n + strlen(dirent->d_name) < 1024) {
+                        strcpy(buf + n, dirent->d_name);
+                    } else {
+                        continue;
+                    }
+                    dobjects = processObject(dobjects, buf, NULL);
+                }
+                closedir(dir);
+            } else {
+                fprintf(out, "<p>Couldn't open directory: %s (%d).</p>\n",
+                        strerror(errno), errno);
+                goto trailer;
+            }
+        }
+    }
+
+    if(dobjects) {
+        int entryno;
+        dobjects = insertRoot(dobjects, root);
+        dobjects = insertDirs(dobjects);
+        dobjects = filterDiskObjects(dobjects, root, recursive);
+        buf[0] = '\0';
+        alternatingHttpStyle(out, "diskcachelist");
+        fprintf(out, "<table id=diskcachelist>\n");
+        fprintf(out, "<tbody>\n");
+        entryno = 0;
+        while(dobjects) {
+            DiskObjectPtr dobject = dobjects;
+            i = strlen(dobject->location);
+            isdir = (i == 0 || dobject->location[i - 1] == '/');
+            if(entryno % 2)
+                fprintf(out, "<tr class=odd>");
+            else
+                fprintf(out, "<tr class=even>");
+            if(dobject->size >= 0) {
+                fprintf(out, "<td><a href=\"%s\"><tt>",
+                        dobject->location);
+                htmlPrint(out,
+                          dobject->location, strlen(dobject->location));
+                fprintf(out, "</tt></a></td> ");
+                if(dobject->length >= 0) {
+                    if(dobject->size == dobject->length)
+                        fprintf(out, "<td>%d</td> ", dobject->length);
+                    else
+                        fprintf(out, "<td>%d/%d</td> ",
+                               dobject->size, dobject->length);
+                } else {
+                    /* Avoid a trigraph. */
+                    fprintf(out, "<td>%d/<em>??" "?</em></td> ", dobject->size);
+                }
+                if(dobject->last_modified >= 0) {
+                    struct tm *tm = gmtime(&dobject->last_modified);
+                    if(tm == NULL)
+                        n = -1;
+                    else
+                        n = strftime(buf, 1024, "%d.%m.%Y", tm);
+                } else
+                    n = -1;
+                if(n > 0) {
+                    buf[n] = '\0';
+                    fprintf(out, "<td>%s</td> ", buf);
+                } else {
+                    fprintf(out, "<td></td>");
+                }
+                
+                if(dobject->date >= 0) {
+                    struct tm *tm = gmtime(&dobject->date);
+                    if(tm == NULL)
+                        n = -1;
+                    else
+                        n = strftime(buf, 1024, "%d.%m.%Y", tm);
+                } else
+                    n = -1;
+                if(n > 0) {
+                    buf[n] = '\0';
+                    fprintf(out, "<td>%s</td>", buf);
+                } else {
+                    fprintf(out, "<td></td>");
+                }
+            } else {
+                fprintf(out, "<td><tt>");
+                htmlPrint(out, dobject->location,
+                          strlen(dobject->location));
+                fprintf(out, "</tt></td><td></td><td></td><td></td>");
+            }
+            if(isdir) {
+                fprintf(out, "<td><a href=\"/polipo/index?%s\">plain</a></td>"
+                        "<td><a href=\"/polipo/recursive-index?%s\">"
+                        "recursive</a></td>",
+                        dobject->location, dobject->location);
+            }
+            fprintf(out, "</tr>\n");
+            entryno++;
+            dobjects = dobject->next;
+            free(dobject->location);
+            free(dobject->filename);
+            free(dobject);
+        }
+        fprintf(out, "</tbody>\n");
+        fprintf(out, "</table>\n");
+    }
+
+ trailer:
+    fprintf(out, "<p><a href=\"/polipo/\">back</a></p>\n");
+    fprintf(out, "</body></html>\n");
+    return;
+}
+
+static int
+checkForZeroes(char *buf, int n)
+{
+    int i, j;
+    unsigned long *lbuf = (unsigned long *)buf;
+    assert(n % sizeof(unsigned long) == 0);
+
+    for(i = 0; i * sizeof(unsigned long) < n; i++) {
+        if(lbuf[i] != 0L)
+            return i * sizeof(unsigned long);
+    }
+    for(j = 0; i * sizeof(unsigned long) + j < n; j++) {
+        if(buf[i * sizeof(unsigned long) + j] != 0)
+            break;
+    }
+
+    return i * sizeof(unsigned long) + j;
+}
+
+static int
+copyFile(int from, char *filename, int n)
+{
+    char *buf;
+    int to, offset, nread, nzeroes, rc;
+
+    buf = malloc(CHUNK_SIZE);
+    if(buf == NULL)
+        return -1;
+
+    to = open(filename, O_RDWR | O_CREAT | O_EXCL | O_BINARY,
+	      diskCacheFilePermissions);
+    if(to < 0) {
+        free(buf);
+        return -1;
+    }
+
+    offset = 0;
+    while(offset < n) {
+        nread = read(from, buf, MIN(CHUNK_SIZE, n - offset));
+        if(nread <= 0)
+            break;
+        nzeroes = checkForZeroes(buf, nread & -8);
+        if(nzeroes > 0) {
+            /* I like holes */
+            rc = lseek(to, nzeroes, SEEK_CUR);
+            if(rc != offset + nzeroes) {
+                if(rc < 0)
+                    do_log_error(L_ERROR, errno, "Couldn't extend file");
+                else
+                    do_log(L_ERROR, 
+                           "Couldn't extend file: "
+                           "unexpected offset %d != %d + %d.\n",
+                           rc, offset, nread);
+                break;
+            }
+        }
+        if(nread > nzeroes) {
+            rc = write(to, buf + nzeroes, nread - nzeroes);
+            if(rc != nread - nzeroes) {
+                if(rc < 0)
+                    do_log_error(L_ERROR, errno, "Couldn't write");
+                else
+                    do_log(L_ERROR, "Short write.\n");
+                break;
+            }
+        }
+        offset += nread;
+    }
+    free(buf);
+    close(to);
+    if(offset <= 0)
+        unlink(filename);       /* something went wrong straight away */
+    return 1;
+}
+
+static long int
+expireFile(char *filename, struct stat *sb,
+           int *considered, int *unlinked, int *truncated)
+{
+    DiskObjectPtr dobject = NULL;
+    time_t t;
+    int fd, rc;
+    long int ret = sb->st_size;
+
+    if(!preciseExpiry) {
+        t = sb->st_mtime;
+        if(t > current_time.tv_sec + 1) {
+            do_log(L_WARN, "File %s has access time in the future.\n",
+                   filename);
+            t = current_time.tv_sec;
+        }
+        
+        if(t > current_time.tv_sec - diskCacheUnlinkTime &&
+           (sb->st_size < diskCacheTruncateSize ||
+            t > current_time.tv_sec - diskCacheTruncateTime))
+            return ret;
+    }
+    
+    (*considered)++;
+
+    dobject = readDiskObject(filename, sb);
+    if(!dobject) {
+        do_log(L_ERROR, "Incorrect disk entry %s -- removing.\n",
+               scrub(filename));
+        rc = unlink(filename);
+        if(rc < 0) {
+            do_log_error(L_ERROR, errno,
+                         "Couldn't unlink %s", scrub(filename));
+            return ret;
+        } else {
+            (*unlinked)++;
+            return 0;
+        }
+    }
+    
+    t = dobject->access;
+    if(t < 0) t = dobject->age;
+    if(t < 0) t = dobject->date;
+    
+    if(t > current_time.tv_sec)
+        do_log(L_WARN, 
+               "Disk entry %s (%s) has access time in the future.\n",
+               scrub(dobject->location), scrub(dobject->filename));
+    
+    if(t < current_time.tv_sec - diskCacheUnlinkTime) {
+        rc = unlink(dobject->filename);
+        if(rc < 0) {
+            do_log_error(L_ERROR, errno, "Couldn't unlink %s",
+                         scrub(filename));
+        } else {
+            (*unlinked)++;
+            ret = 0;
+        }
+    } else if(dobject->size > 
+              diskCacheTruncateSize + 4 * dobject->body_offset && 
+              t < current_time.tv_sec - diskCacheTruncateTime) {
+        /* We need to copy rather than simply truncate in place: the
+           latter would confuse a running polipo. */
+        fd = open(dobject->filename, O_RDONLY | O_BINARY, 0);
+        rc = unlink(dobject->filename);
+        if(rc < 0) {
+            do_log_error(L_ERROR, errno, "Couldn't unlink %s",
+                         scrub(filename));
+            close(fd);
+            fd = -1;
+        } else {
+            (*unlinked)++;
+            copyFile(fd, dobject->filename,
+                     dobject->body_offset + diskCacheTruncateSize);
+            close(fd);
+            (*unlinked)--;
+            (*truncated)++;
+            ret = sb->st_size - dobject->body_offset + diskCacheTruncateSize;
+        }
+    }
+    free(dobject->location);
+    free(dobject->filename);
+    free(dobject);
+    return ret;
+}
+    
+void
+expireDiskObjects()
+{
+    int rc;
+    char *fts_argv[2];
+    FTS *fts;
+    FTSENT *fe;
+    int files = 0, considered = 0, unlinked = 0, truncated = 0;
+    int dirs = 0, rmdirs = 0;
+    long left = 0, total = 0;
+
+    if(diskCacheRoot == NULL || 
+       diskCacheRoot->length <= 0 || diskCacheRoot->string[0] != '/')
+        return;
+
+    fts_argv[0] = diskCacheRoot->string;
+    fts_argv[1] = NULL;
+    fts = fts_open(fts_argv, FTS_LOGICAL, NULL);
+    if(fts == NULL) {
+        do_log_error(L_ERROR, errno, "Couldn't fts_open disk cache");
+    } else {
+        while(1) {
+            gettimeofday(&current_time, NULL);
+
+            fe = fts_read(fts);
+            if(!fe) break;
+
+            if(fe->fts_info == FTS_D)
+                continue;
+
+            if(fe->fts_info == FTS_DP || fe->fts_info == FTS_DC ||
+               fe->fts_info == FTS_DNR) {
+                if(fe->fts_accpath[0] == '/' &&
+                   strlen(fe->fts_accpath) <= diskCacheRoot->length)
+                    continue;
+                dirs++;
+                rc = rmdir(fe->fts_accpath);
+                if(rc >= 0)
+                    rmdirs++;
+                else if(errno != ENOTEMPTY && errno != EEXIST)
+                    do_log_error(L_ERROR, errno,
+                                 "Couldn't remove directory %s",
+                                 scrub(fe->fts_accpath));
+                continue;
+            } else if(fe->fts_info == FTS_NS) {
+                do_log_error(L_ERROR, fe->fts_errno, "Couldn't stat file %s",
+                             scrub(fe->fts_accpath));
+                continue;
+            } else if(fe->fts_info == FTS_ERR) {
+                do_log_error(L_ERROR, fe->fts_errno,
+                             "Couldn't fts_read disk cache");
+                break;
+            }
+
+            if(!S_ISREG(fe->fts_statp->st_mode)) {
+                do_log(L_ERROR, "Unexpected file %s type 0%o.\n", 
+                       fe->fts_accpath, (unsigned int)fe->fts_statp->st_mode);
+                continue;
+            }
+
+            files++;
+            left += expireFile(fe->fts_accpath, fe->fts_statp,
+                               &considered, &unlinked, &truncated);
+            total += fe->fts_statp->st_size;
+        }
+        fts_close(fts);
+    }
+
+    printf("Disk cache purged.\n");
+    printf("%d files, %d considered, %d removed, %d truncated "
+           "(%ldkB -> %ldkB).\n",
+           files, considered, unlinked, truncated, total/1024, left/1024);
+    printf("%d directories, %d removed.\n", dirs, rmdirs);
+    return;
+}
+
+#else
+
+void
+preinitDiskcache()
+{
+    return;
+}
+
+void
+initDiskcache()
+{
+    return;
+}
+
+int
+writeoutToDisk(ObjectPtr object, int upto, int max)
+{
+    return 0;
+}
+
+int
+destroyDiskEntry(ObjectPtr object, int d)
+{
+    return 0;
+}
+
+ObjectPtr
+objectGetFromDisk(ObjectPtr object)
+{
+    return NULL;
+}
+
+int
+objectFillFromDisk(ObjectPtr object, int offset, int chunks)
+{
+    return 0;
+}
+
+int
+revalidateDiskEntry(ObjectPtr object)
+{
+    return 0;
+}
+
+void
+dirtyDiskEntry(ObjectPtr object)
+{
+    return;
+}
+
+void
+expireDiskObjects()
+{
+    do_log(L_ERROR, "Disk cache not supported in this version.\n");
+}
+
+int
+diskEntrySize(ObjectPtr object)
+{
+    return -1;
+}
+#endif

+ 71 - 0
polipo/diskcache.h

@@ -0,0 +1,71 @@
+/*
+Copyright (c) 2003-2010 by Juliusz Chroboczek
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+extern int maxDiskEntries;
+
+extern AtomPtr diskCacheRoot;
+extern AtomPtr additionalDiskCacheRoot;
+
+typedef struct _DiskCacheEntry {
+    char *filename;
+    ObjectPtr object;
+    int fd;
+    off_t offset;
+    off_t size;
+    int body_offset;
+    short local;
+    short metadataDirty;
+    struct _DiskCacheEntry *next;
+    struct _DiskCacheEntry *previous;
+} *DiskCacheEntryPtr, DiskCacheEntryRec;
+
+typedef struct _DiskObject {
+    char *location;
+    char *filename;
+    int body_offset;
+    int length;
+    int size;
+    time_t age;
+    time_t access;
+    time_t date;
+    time_t last_modified;
+    time_t expires;
+    struct _DiskObject *next;
+} DiskObjectRec, *DiskObjectPtr;
+
+struct stat;
+
+extern int maxDiskCacheEntrySize;
+
+void preinitDiskcache(void);
+void initDiskcache(void);
+int destroyDiskEntry(ObjectPtr object, int);
+int diskEntrySize(ObjectPtr object);
+ObjectPtr objectGetFromDisk(ObjectPtr);
+int objectFillFromDisk(ObjectPtr object, int offset, int chunks);
+int writeoutMetadata(ObjectPtr object);
+int writeoutToDisk(ObjectPtr object, int upto, int max);
+void dirtyDiskEntry(ObjectPtr object);
+int revalidateDiskEntry(ObjectPtr object);
+DiskObjectPtr readDiskObject(char *filename, struct stat *sb);
+void indexDiskObjects(FILE *out, const char *root, int r);
+void expireDiskObjects(void);

+ 1756 - 0
polipo/dns.c

@@ -0,0 +1,1756 @@
+/*
+Copyright (c) 2003-2006 by Juliusz Chroboczek
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+#include "polipo.h"
+
+#ifndef NO_STANDARD_RESOLVER
+#ifndef NO_FANCY_RESOLVER
+int dnsUseGethostbyname = 1;
+#else 
+const int dnsUseGethostbyname = 3;
+#endif
+#else
+#ifndef NO_FANCY_RESOLVER
+const int dnsUseGethostbyname = 0;
+#else
+#error use no resolver at all?
+#endif
+#endif
+
+#ifndef NO_FANCY_RESOLVER
+AtomPtr dnsNameServer = NULL;
+int dnsMaxTimeout = 60;
+int dnsNameServerPort = 53;
+#endif
+
+#ifndef NO_STANDARD_RESOLVER
+int dnsGethostbynameTtl = 1200;
+#endif
+
+int dnsNegativeTtl = 120;
+
+#ifdef HAVE_IPv6
+int dnsQueryIPv6 = 2;
+#else
+const int dnsQueryIPv6 = 0;
+#endif
+
+typedef struct _DnsQuery {
+    unsigned id;
+    AtomPtr name;
+    ObjectPtr object;
+    AtomPtr inet4, inet6;
+    time_t ttl4, ttl6;
+    time_t time;
+    int timeout;
+    TimeEventHandlerPtr timeout_handler;
+    struct _DnsQuery *next;
+} DnsQueryRec, *DnsQueryPtr;
+
+union {
+    struct sockaddr sa;
+    struct sockaddr_in sin;
+#ifdef HAVE_IPv6
+    struct sockaddr_in6 sin6;
+#endif
+} nameserverAddress_storage;
+
+#ifndef NO_FANCY_RESOLVER
+static AtomPtr atomLocalhost, atomLocalhostDot;
+
+#define nameserverAddress nameserverAddress_storage.sa
+
+static DnsQueryPtr inFlightDnsQueries;
+static DnsQueryPtr inFlightDnsQueriesLast;
+#endif
+
+static int really_do_gethostbyname(AtomPtr name, ObjectPtr object);
+static int really_do_dns(AtomPtr name, ObjectPtr object);
+
+#ifndef NO_FANCY_RESOLVER
+static int stringToLabels(char *buf, int offset, int n, char *string);
+static int labelsToString(char *buf, int offset, int n, char *d, 
+                          int m, int *j_return);
+static int dnsBuildQuery(int id, char *buf, int offset, int n,
+                         AtomPtr name, int af);
+static int dnsReplyHandler(int abort, FdEventHandlerPtr event);
+static int dnsReplyId(char *buf, int offset, int n, int *id_return);
+static int dnsDecodeReply(char *buf, int offset, int n,
+                          int *id_return,
+                          AtomPtr *name_return, AtomPtr *value_return,
+                          int *af_return, unsigned *ttl_return);
+static int dnsHandler(int status, ConditionHandlerPtr chandler);
+static int dnsGethostbynameFallback(int id, AtomPtr message);
+static int sendQuery(DnsQueryPtr query);
+
+static int idSeed;
+#endif
+
+#if !defined(NO_FANCY_RESOLVER) && !defined(WIN32)
+static int
+parseResolvConf(char *filename)
+{
+    FILE *f;
+    char buf[512];
+    char *p, *q;
+    int n;
+    AtomPtr nameserver = NULL;
+
+    f = fopen(filename, "r");
+    if(f == NULL) {
+        do_log_error(L_ERROR, errno, "DNS: couldn't open %s", filename);
+        return 0;
+    }
+
+    while(1) {
+        p = fgets(buf, 512, f);
+        if(p == NULL)
+            break;
+
+        n = strlen(buf);
+        if(buf[n - 1] != '\n') {
+            int c;
+            do_log(L_WARN, "DNS: overly long line in %s -- skipping.\n",
+                   filename);
+            do {
+                c = fgetc(f);
+                if(c == EOF)
+                    break;
+            } while(c != '\n');
+            if(c == EOF)
+                break;
+        }
+        
+        while(*p == ' ' || *p == '\t')
+            p++;
+        if(strcasecmp_n("nameserver", p, 10) != 0)
+            continue;
+        p += 10;
+        while(*p == ' ' || *p == '\t')
+            p++;
+        q = p;
+        while(*q == '.' || *q == ':' || digit(*q) || letter(*q))
+            q++;
+        if(*q != ' ' && *q != '\t' && *q != '\r' && *q != '\n') {
+            do_log(L_WARN, "DNS: couldn't parse line in %s -- skipping.\n",
+                   filename);
+            continue;
+        }
+        nameserver = internAtomLowerN(p, q - p);
+        break;
+    }
+
+    fclose(f);
+    if(nameserver) {
+        dnsNameServer = nameserver;
+        return 1;
+    } else {
+        return 0;
+    }
+}
+#endif
+
+void
+preinitDns()
+{
+#ifdef HAVE_IPv6
+    int fd;
+#endif
+
+    assert(sizeof(struct in_addr) == 4);
+#ifdef HAVE_IPv6
+    assert(sizeof(struct in6_addr) == 16);
+#endif
+
+#ifndef NO_STANDARD_RESOLVER
+    CONFIG_VARIABLE(dnsGethostbynameTtl, CONFIG_TIME,
+                    "TTL for gethostbyname addresses.");
+#endif
+
+#ifdef HAVE_IPv6
+    fd = socket(PF_INET6, SOCK_STREAM, 0);
+    if(fd < 0) {
+        if(errno == EPROTONOSUPPORT || errno == EAFNOSUPPORT) {
+            dnsQueryIPv6 = 0;
+        } else {
+            do_log_error(L_WARN, errno, "DNS: couldn't create socket");
+        }
+    } else {
+        close(fd);
+    }
+#endif
+
+#ifndef NO_FANCY_RESOLVER
+#ifndef WIN32
+    parseResolvConf("/etc/resolv.conf");
+#endif
+    if(dnsNameServer == NULL || dnsNameServer->string[0] == '\0')
+        dnsNameServer = internAtom("127.0.0.1");
+    CONFIG_VARIABLE(dnsMaxTimeout, CONFIG_TIME,
+                    "Max timeout for DNS queries.");
+    CONFIG_VARIABLE(dnsNegativeTtl, CONFIG_TIME,
+                    "TTL for negative DNS replies with no TTL.");
+    CONFIG_VARIABLE(dnsNameServer, CONFIG_ATOM_LOWER,
+                    "The name server to use.");
+    CONFIG_VARIABLE(dnsNameServerPort, CONFIG_INT,
+                    "The name server port to use.");
+#ifndef NO_STANDARD_RESOLVER
+    CONFIG_VARIABLE(dnsUseGethostbyname, CONFIG_TETRASTATE,
+                    "Use the system resolver.");
+#endif
+#endif
+
+#ifdef HAVE_IPv6
+    CONFIG_VARIABLE(dnsQueryIPv6, CONFIG_TETRASTATE,
+                    "Query for IPv6 addresses.");
+#endif
+}
+
+void
+initDns()
+{
+#ifndef NO_FANCY_RESOLVER
+    int rc;
+    struct timeval t;
+    struct sockaddr_in *sin = (struct sockaddr_in*)&nameserverAddress;
+#ifdef HAVE_IPv6
+    struct sockaddr_in6 *sin6 = (struct sockaddr_in6*)&nameserverAddress;
+#endif
+
+    atomLocalhost = internAtom("localhost");
+    atomLocalhostDot = internAtom("localhost.");
+    inFlightDnsQueries = NULL;
+    inFlightDnsQueriesLast = NULL;
+
+    gettimeofday(&t, NULL);
+    idSeed = t.tv_usec & 0xFFFF;
+    sin->sin_family = AF_INET;
+    sin->sin_port = htons(dnsNameServerPort);
+    rc = inet_aton(dnsNameServer->string, &sin->sin_addr);
+#ifdef HAVE_IPv6
+    if(rc != 1) {
+        sin6->sin6_family = AF_INET6;
+        sin6->sin6_port = htons(dnsNameServerPort);
+        rc = inet_pton(AF_INET6, dnsNameServer->string, &sin6->sin6_addr);
+    }
+#endif
+    if(rc != 1) {
+        do_log(L_ERROR, "DNS: couldn't parse name server %s.\n",
+               dnsNameServer->string);
+        exit(1);
+    }
+#endif
+}
+
+int
+do_gethostbyname(char *origname,
+                 int count,
+                 int (*handler)(int, GethostbynameRequestPtr),
+                 void *data)
+{
+    ObjectPtr object;
+    int n = strlen(origname);
+    AtomPtr name;
+    GethostbynameRequestRec request;
+    int done, rc;
+
+    memset(&request, 0, sizeof(request));
+    request.name = NULL;
+    request.addr = NULL;
+    request.error_message = NULL;
+    request.count = count;
+    request.handler = handler;
+    request.data = data;
+
+    if(n <= 0 || n > 131) {
+        if(n <= 0) {
+            request.error_message = internAtom("empty name");
+            do_log(L_ERROR, "Empty DNS name.\n");
+            done = handler(-EINVAL, &request);
+        } else {
+            request.error_message = internAtom("name too long");
+            do_log(L_ERROR, "DNS name too long.\n");
+            done = handler(-ENAMETOOLONG, &request);
+        }
+        assert(done);
+        releaseAtom(request.error_message);
+        return 1;
+    }
+
+    if(origname[n - 1] == '.')
+        n--;
+
+    name = internAtomLowerN(origname, n);
+
+    if(name == NULL) {
+        request.error_message = internAtom("couldn't allocate name");
+        do_log(L_ERROR, "Couldn't allocate DNS name.\n");
+        done = handler(-ENOMEM, &request);
+        assert(done);
+        releaseAtom(request.error_message);
+        return 1;
+    }
+
+    request.name = name;
+    request.addr = NULL;
+    request.error_message = NULL;
+    request.count = count;
+    request.object = NULL;
+    request.handler = handler;
+    request.data = data;
+
+    object = findObject(OBJECT_DNS, name->string, name->length);
+    if(object == NULL || objectMustRevalidate(object, NULL)) {
+        if(object) {
+            privatiseObject(object, 0);
+            releaseObject(object);
+        }
+        object = makeObject(OBJECT_DNS, name->string, name->length, 1, 0,
+                            NULL, NULL);
+        if(object == NULL) {
+            request.error_message = internAtom("Couldn't allocate object");
+            do_log(L_ERROR, "Couldn't allocate DNS object.\n");
+            done = handler(-ENOMEM, &request);
+            assert(done);
+            releaseAtom(name);
+            releaseAtom(request.error_message);
+            return 1;
+        }
+    }
+
+    if((object->flags & (OBJECT_INITIAL | OBJECT_INPROGRESS)) ==
+       OBJECT_INITIAL) {
+        if(dnsUseGethostbyname >= 3)
+            rc = really_do_gethostbyname(name, object);
+        else
+            rc = really_do_dns(name, object);
+        if(rc < 0) {
+            assert(!(object->flags & (OBJECT_INITIAL | OBJECT_INPROGRESS)));
+            goto fail;
+        }
+    }
+
+    if(dnsUseGethostbyname >= 3)
+        assert(!(object->flags & OBJECT_INITIAL));
+
+#ifndef NO_FANCY_RESOLVER    
+    if(object->flags & OBJECT_INITIAL) {
+        ConditionHandlerPtr chandler;
+        assert(object->flags & OBJECT_INPROGRESS);
+        request.object = object;
+        chandler = conditionWait(&object->condition, dnsHandler,
+                                 sizeof(request), &request);
+        if(chandler == NULL)
+            goto fail;
+        return 1;
+    }
+#endif
+
+    if(object->headers && object->headers->length > 0) {
+        if(object->headers->string[0] == DNS_A)
+            assert(((object->headers->length - 1) % 
+                    sizeof(HostAddressRec)) == 0);
+        else
+            assert(object->headers->string[0] == DNS_CNAME);
+        request.addr = retainAtom(object->headers);
+    } else if(object->message) {
+        request.error_message = retainAtom(object->message);
+    }
+
+    releaseObject(object);
+
+    if(request.addr && request.addr->length > 0)
+        done = handler(1, &request);
+    else
+        done = handler(-EDNS_HOST_NOT_FOUND, &request);
+    assert(done);
+
+    releaseAtom(request.addr); request.addr = NULL;
+    releaseAtom(request.name); request.name = NULL;
+    releaseAtom(request.error_message); request.error_message = NULL;
+    return 1;
+
+ fail:
+    releaseNotifyObject(object);
+    done = handler(-errno, &request);
+    assert(done);
+    releaseAtom(name);
+    return 1;
+}
+
+static int
+dnsDelayedErrorNotifyHandler(TimeEventHandlerPtr event)
+{
+    int done;
+    GethostbynameRequestRec request =
+        *(GethostbynameRequestPtr)event->data;
+    done = request.handler(-EDNS_HOST_NOT_FOUND, &request);
+    assert(done);
+    releaseAtom(request.name); request.name = NULL;
+    releaseAtom(request.addr); request.addr = NULL;
+    releaseAtom(request.error_message); request.error_message = NULL;
+    return 1;
+}
+    
+static int
+dnsDelayedDoneNotifyHandler(TimeEventHandlerPtr event)
+{
+    int done;
+    GethostbynameRequestRec request = *(GethostbynameRequestPtr)event->data;
+    done = request.handler(1, &request);
+    assert(done);
+    releaseAtom(request.name); request.name = NULL;
+    releaseAtom(request.addr); request.addr = NULL;
+    releaseAtom(request.error_message); request.error_message = NULL;
+    return 1;
+}
+
+static int
+dnsDelayedNotify(int error, GethostbynameRequestPtr request)
+{
+    TimeEventHandlerPtr handler;
+
+    if(error)
+        handler = scheduleTimeEvent(0,
+                                    dnsDelayedErrorNotifyHandler,
+                                    sizeof(*request), request);
+    else
+        handler = scheduleTimeEvent(0,
+                                    dnsDelayedDoneNotifyHandler,
+                                    sizeof(*request), request);
+    if(handler == NULL) {
+        do_log(L_ERROR, "Couldn't schedule DNS notification.\n");
+        return -1;
+    }
+    return 1;
+}
+
+#ifdef HAVE_IPv6
+AtomPtr
+rfc2732(AtomPtr name)
+{
+    char buf[40]; /* 8*4 (hexdigits) + 7 (colons) + 1 ('\0') */
+    int rc;
+    AtomPtr a = NULL;
+
+    if(name->length < 40+2 && 
+       name->string[0] == '[' && name->string[name->length - 1] == ']') {
+        struct in6_addr in6a;
+        memcpy(buf, name->string + 1, name->length - 2);
+        buf[name->length - 2] = '\0';
+        rc = inet_pton(AF_INET6, buf, &in6a);
+        if(rc == 1) {
+            char s[1 + sizeof(HostAddressRec)];
+            memset(s, 0, sizeof(s));
+            s[0] = DNS_A;
+            s[1] = 6;
+            memcpy(s + 2, &in6a, 16);
+            a = internAtomN(s, 1 + sizeof(HostAddressRec));
+            if(a == NULL)
+                return NULL;
+        }
+    }
+    return a;
+}
+
+/* Used for sorting host addresses depending on the value of dnsQueryIPv6 */
+int
+compare_hostaddr(const void *av, const void *bv)
+{
+    const HostAddressRec *a = av, *b = bv;
+    int r;
+    if(a->af == 4) {
+        if(b->af == 4)
+            r = 0;
+        else
+            r = -1;
+    } else {
+        if(b->af == 6)
+            r = 0;
+        else
+            r = 1;
+    }
+    if(dnsQueryIPv6 >= 2)
+        return -r;
+    else
+        return r;
+}
+
+#ifndef NO_STANDARD_RESOLVER
+static int
+really_do_gethostbyname(AtomPtr name, ObjectPtr object)
+{
+    struct addrinfo *ai, *entry, hints;
+    int rc;
+    int error, i;
+    char buf[1024];
+    AtomPtr a;
+
+    a = rfc2732(name);
+    if(a) {
+        object->headers = a;
+        object->age = current_time.tv_sec;
+        object->expires = current_time.tv_sec + 240;
+        object->flags &= ~(OBJECT_INITIAL | OBJECT_INPROGRESS);
+        notifyObject(object);
+        return 0;
+    }
+
+    memset(&hints, 0, sizeof(hints));
+    hints.ai_protocol = IPPROTO_TCP;
+    if(dnsQueryIPv6 <= 0)
+        hints.ai_family = AF_INET;
+    else if(dnsQueryIPv6 >= 3)
+        hints.ai_family = AF_INET6;
+
+    rc = getaddrinfo(name->string, NULL, &hints, &ai);
+
+    switch(rc) {
+    case 0: error = 0; break;
+    case EAI_FAMILY:
+#ifdef EAI_ADDRFAMILY
+    case EAI_ADDRFAMILY:
+#endif
+    case EAI_SOCKTYPE:
+        error = EAFNOSUPPORT; break;
+    case EAI_BADFLAGS: error = EINVAL; break;
+    case EAI_SERVICE: error = EDNS_NO_RECOVERY; break;
+#ifdef EAI_NONAME
+    case EAI_NONAME:
+#endif
+#ifdef EAI_NODATA
+    case EAI_NODATA:
+#endif
+        error = EDNS_NO_ADDRESS; break;
+    case EAI_FAIL: error = EDNS_NO_RECOVERY; break;
+    case EAI_AGAIN: error = EDNS_TRY_AGAIN; break;
+#ifdef EAI_MEMORY
+    case EAI_MEMORY: error = ENOMEM; break;
+#endif
+    case EAI_SYSTEM: error = errno; break;
+    default: error = EUNKNOWN;
+    }
+
+    if(error == EDNS_NO_ADDRESS) {
+        object->headers = NULL;
+        object->age = current_time.tv_sec;
+        object->expires = current_time.tv_sec + dnsNegativeTtl;
+        object->flags &= ~(OBJECT_INITIAL | OBJECT_INPROGRESS);
+        notifyObject(object);
+        return 0;
+    } else if(error) {
+        do_log_error(L_ERROR, error, "Getaddrinfo failed");
+        object->flags &= ~OBJECT_INPROGRESS;
+        abortObject(object, 404,
+                    internAtomError(error, "Getaddrinfo failed"));
+        notifyObject(object);
+        return 0;
+    }
+
+    entry = ai;
+    buf[0] = DNS_A;
+    i = 0;
+    while(entry) {
+        HostAddressRec host;
+        int host_valid = 0;
+        if(entry->ai_family == AF_INET && entry->ai_protocol == IPPROTO_TCP) {
+            if(dnsQueryIPv6 < 3) {
+                host.af = 4;
+                memset(host.data, 0, sizeof(host.data));
+                memcpy(&host.data,
+                       &((struct sockaddr_in*)entry->ai_addr)->sin_addr, 
+                       4);
+                host_valid = 1;
+            }
+        } else if(entry->ai_family == AF_INET6 && 
+                  entry->ai_protocol == IPPROTO_TCP) {
+            if(dnsQueryIPv6 > 0) {
+                host.af = 6;
+                memset(&host.data, 0, sizeof(host.data));
+                memcpy(&host.data,
+                       &((struct sockaddr_in6*)entry->ai_addr)->sin6_addr, 
+                       16);
+                host_valid = 1;
+            }
+        }
+        if(host_valid) {
+            if(i >= 1024 / sizeof(HostAddressRec) - 2) {
+                do_log(L_ERROR, "Too many addresses for host %s\n", 
+                       name->string);
+                break;
+            }
+            memcpy(buf + 1 + i * sizeof(HostAddressRec), 
+                   &host, sizeof(HostAddressRec));
+            i++;
+        }
+        entry = entry->ai_next;
+    }
+    freeaddrinfo(ai);
+    if(i == 0) {
+        do_log(L_ERROR, "Getaddrinfo returned no useful addresses\n");
+        object->flags &= ~OBJECT_INPROGRESS;
+        abortObject(object, 404,
+                    internAtom("Getaddrinfo returned no useful addresses"));
+        notifyObject(object);
+        return 0;
+    }
+
+    if(1 <= dnsQueryIPv6 && dnsQueryIPv6 <= 2)
+        qsort(buf + 1, i, sizeof(HostAddressRec), compare_hostaddr);
+
+    a = internAtomN(buf, 1 + i * sizeof(HostAddressRec));
+    if(a == NULL) {
+        object->flags &= ~OBJECT_INPROGRESS;
+        abortObject(object, 501, internAtom("Couldn't allocate address"));
+        notifyObject(object);
+        return 0;
+    }
+    object->headers = a;
+    object->age = current_time.tv_sec;
+    object->expires = current_time.tv_sec + dnsGethostbynameTtl;
+    object->flags &= ~(OBJECT_INITIAL | OBJECT_INPROGRESS);
+    notifyObject(object);
+    return 0;
+}
+#endif
+    
+#else
+
+#ifndef NO_STANDARD_RESOLVER
+static int
+really_do_gethostbyname(AtomPtr name, ObjectPtr object)
+{
+    struct hostent *host;
+    char *s;
+    AtomPtr a;
+    int i, j;
+    int error;
+
+    host = gethostbyname(name->string);
+    if(host == NULL) {
+        switch(h_errno) {
+        case HOST_NOT_FOUND: error = EDNS_HOST_NOT_FOUND; break;
+#ifdef NO_ADDRESS
+        case NO_ADDRESS: error = EDNS_NO_ADDRESS; break;
+#endif
+        case NO_RECOVERY: error = EDNS_NO_RECOVERY; break;
+        case TRY_AGAIN: error = EDNS_TRY_AGAIN; break;
+        default: error = EUNKNOWN; break;
+        }
+        if(error == EDNS_HOST_NOT_FOUND) {
+            object->headers = NULL;
+            object->age = current_time.tv_sec;
+            object->expires = current_time.tv_sec + dnsNegativeTtl;
+            object->flags &= ~(OBJECT_INITIAL | OBJECT_INPROGRESS);
+            object->flags &= ~OBJECT_INPROGRESS;
+            notifyObject(object);
+            return 0;
+        } else {
+            do_log_error(L_ERROR, error, "Gethostbyname failed");
+            abortObject(object, 404, 
+                        internAtomError(error, "Gethostbyname failed"));
+            object->flags &= ~OBJECT_INPROGRESS;
+            notifyObject(object);
+            return 0;
+        }
+    }
+    if(host->h_addrtype != AF_INET) {
+        do_log(L_ERROR, "Address is not AF_INET.\n");
+        object->flags &= ~OBJECT_INPROGRESS;
+        abortObject(object, 404, internAtom("Address is not AF_INET"));
+        notifyObject(object);
+        return -1;
+    }
+    if(host->h_length != sizeof(struct in_addr)) {
+        do_log(L_ERROR, "Address size inconsistent.\n");
+        object->flags &= ~OBJECT_INPROGRESS;
+        abortObject(object, 404, internAtom("Address size inconsistent"));
+        notifyObject(object);
+        return 0;
+    }
+    i = 0;
+    while(host->h_addr_list[i] != NULL) i++;
+    s = malloc(1 + i * sizeof(HostAddressRec));
+    if(s == NULL) {
+        a = NULL;
+    } else {
+        memset(s, 0, 1 + i * sizeof(HostAddressRec));
+        s[0] = DNS_A;
+        for(j = 0; j < i; j++) {
+            s[j * sizeof(HostAddressRec) + 1] = 4;
+            memcpy(&s[j * sizeof(HostAddressRec) + 2], host->h_addr_list[j],
+                   sizeof(struct in_addr));
+        }
+        a = internAtomN(s, i * sizeof(HostAddressRec) + 1);
+        free(s);
+    }
+    if(!a) {
+        object->flags &= ~OBJECT_INPROGRESS;
+        abortObject(object, 501, internAtom("Couldn't allocate address"));
+        notifyObject(object);
+        return 0;
+    }
+    object->headers = a;
+    object->age = current_time.tv_sec;
+    object->expires = current_time.tv_sec + dnsGethostbynameTtl;
+    object->flags &= ~(OBJECT_INITIAL | OBJECT_INPROGRESS);
+    notifyObject(object);
+    return 0;
+
+}
+#endif
+
+#endif
+
+#ifdef NO_STANDARD_RESOLVER
+static int
+really_do_gethostbyname(AtomPtr name, ObjectPtr object)
+{
+    abort();
+}
+#endif
+
+#ifndef NO_FANCY_RESOLVER
+
+static int dnsSocket = -1;
+static FdEventHandlerPtr dnsSocketHandler = NULL;
+
+static int
+dnsHandler(int status, ConditionHandlerPtr chandler)
+{
+    GethostbynameRequestRec request = *(GethostbynameRequestPtr)chandler->data;
+    ObjectPtr object = request.object;
+
+    assert(!(object->flags & OBJECT_INPROGRESS));
+
+    if(object->headers) {
+        request.addr = retainAtom(object->headers);
+        dnsDelayedNotify(0, &request);
+    } else {
+        if(object->message)
+            request.error_message = retainAtom(object->message);
+        dnsDelayedNotify(1, &request);
+    }
+    releaseObject(object);
+    return 1;
+}
+
+static int
+queryInFlight(DnsQueryPtr query)
+{
+    DnsQueryPtr other;
+    other = inFlightDnsQueries;
+    while(other) {
+        if(other == query)
+            return 1;
+        other = other->next;
+    }
+    return 0;
+}
+
+static void
+removeQuery(DnsQueryPtr query)
+{
+    DnsQueryPtr previous;
+    if(query == inFlightDnsQueries) {
+        inFlightDnsQueries = query->next;
+        if(inFlightDnsQueries == NULL)
+            inFlightDnsQueriesLast = NULL;
+    } else {
+        previous = inFlightDnsQueries;
+        while(previous->next) {
+            if(previous->next == query)
+                break;
+            previous = previous->next;
+        }
+        assert(previous->next != NULL);
+        previous->next = query->next;
+        if(previous->next == NULL)
+            inFlightDnsQueriesLast = previous;
+    }
+}
+
+static void
+insertQuery(DnsQueryPtr query) 
+{
+    if(inFlightDnsQueriesLast)
+        inFlightDnsQueriesLast->next = query;
+    else
+        inFlightDnsQueries = query;
+    inFlightDnsQueriesLast = query;
+}
+
+static DnsQueryPtr
+findQuery(int id, AtomPtr name)
+{
+    DnsQueryPtr query;
+    query = inFlightDnsQueries;
+    while(query) {
+        if(query->id == id && (name == NULL || query->name == name))
+            return query;
+        query = query->next;
+    }
+    return NULL;
+}
+
+static int
+dnsTimeoutHandler(TimeEventHandlerPtr event)
+{
+    DnsQueryPtr query = *(DnsQueryPtr*)event->data;
+    ObjectPtr object = query->object;
+    int rc;
+
+    /* People are reporting that this does happen.  And I have no idea why. */
+    if(!queryInFlight(query)) {
+        do_log(L_ERROR, "BUG: timing out martian query (%s, flags: 0x%x).\n",
+               scrub(query->name->string), (unsigned)object->flags);
+        return 1;
+    }
+
+    query->timeout = MAX(10, query->timeout * 2);
+
+    if(query->timeout > dnsMaxTimeout) {
+        abortObject(object, 501, internAtom("Timeout"));
+        goto fail;
+    } else {
+        rc = sendQuery(query);
+        if(rc < 0) {
+            if(rc != -EWOULDBLOCK && rc != -EAGAIN && rc != -ENOBUFS) {
+                abortObject(object, 501,
+                            internAtomError(-rc,
+                                            "Couldn't send DNS query"));
+                goto fail;
+            }
+            /* else let it timeout */
+        }
+        query->timeout_handler =
+            scheduleTimeEvent(query->timeout, dnsTimeoutHandler,
+                              sizeof(query), &query);
+        if(query->timeout_handler == NULL) {
+            do_log(L_ERROR, "Couldn't schedule DNS timeout handler.\n");
+            abortObject(object, 501,
+                        internAtom("Couldn't schedule DNS timeout handler"));
+            goto fail;
+        }
+        return 1;
+    }
+
+ fail:
+    removeQuery(query);
+    object->flags &= ~OBJECT_INPROGRESS;
+    if(query->inet4) releaseAtom(query->inet4);
+    if(query->inet6) releaseAtom(query->inet6);
+    free(query);
+    releaseNotifyObject(object);
+    return 1;
+}
+
+static int
+establishDnsSocket()
+{
+    int rc;
+#ifdef HAVE_IPv6
+    int inet6 = (nameserverAddress.sa_family == AF_INET6);
+    int pf = inet6 ? PF_INET6 : PF_INET;
+    int sa_size = 
+        inet6 ? sizeof(struct sockaddr_in6) : sizeof(struct sockaddr_in);
+#else
+    int pf = PF_INET;
+    int sa_size = sizeof(struct sockaddr_in);
+#endif
+
+    if(dnsSocket < 0) {
+        assert(!dnsSocketHandler);
+        dnsSocket = socket(pf, SOCK_DGRAM, 0);
+        if(dnsSocket < 0) {
+            do_log_error(L_ERROR, errno, "Couldn't create DNS socket");
+            return -errno;
+        }
+
+        rc = connect(dnsSocket, &nameserverAddress, sa_size);
+        if(rc < 0) {
+            CLOSE(dnsSocket);
+            dnsSocket = -1;
+            do_log_error(L_ERROR, errno, "Couldn't create DNS \"connection\"");
+            return -errno;
+        }
+    }
+
+    if(!dnsSocketHandler) {
+        dnsSocketHandler = 
+            registerFdEvent(dnsSocket, POLLIN, dnsReplyHandler, 0, NULL);
+        if(dnsSocketHandler == NULL) {
+            do_log(L_ERROR, "Couldn't register DNS socket handler.\n");
+            CLOSE(dnsSocket);
+            dnsSocket = -1;
+            return -ENOMEM;
+        }
+    }
+
+    return 1;
+}
+
+static int
+sendQuery(DnsQueryPtr query)
+{
+    char buf[512];
+    int buflen;
+    int rc;
+    int af[2];
+    int i;
+
+    if(dnsSocket < 0)
+        return -1;
+
+    if(dnsQueryIPv6 <= 0) {
+        af[0] = 4; af[1] = 0;
+    } else if(dnsQueryIPv6 <= 2) {
+        af[0] = 4; af[1] = 6;
+    } else {
+        af[0] = 6; af[1] = 0;
+    }
+
+    for(i = 0; i < 2; i++) {
+        if(af[i] == 0)
+            continue;
+        if(af[i] == 4 && query->inet4)
+            continue;
+        else if(af[i] == 6 && query->inet6)
+            continue;
+
+        buflen = dnsBuildQuery(query->id, buf, 0, 512, query->name, af[i]);
+        if(buflen <= 0) {
+            do_log(L_ERROR, "Couldn't build DNS query.\n");
+            return buflen;
+        }
+
+        rc = send(dnsSocket, buf, buflen, 0);
+        if(rc < buflen) {
+            if(rc >= 0) {
+                do_log(L_ERROR, "Couldn't send DNS query: partial send.\n");
+                return -EAGAIN;
+            } else {
+                do_log_error(L_ERROR, errno, "Couldn't send DNS query");
+                return -errno;
+            }
+        }
+    }
+    return 1;
+}
+
+static int
+really_do_dns(AtomPtr name, ObjectPtr object)
+{
+    int rc;
+    DnsQueryPtr query;
+    AtomPtr message = NULL;
+    int id;
+    AtomPtr a = NULL;
+
+    if(a == NULL) {
+        if(name == atomLocalhost || name == atomLocalhostDot) {
+            char s[1 + sizeof(HostAddressRec)];
+            memset(s, 0, sizeof(s));
+            s[0] = DNS_A;
+            s[1] = 4;
+            s[2] = 127;
+            s[3] = 0;
+            s[4] = 0;
+            s[5] = 1;
+            a = internAtomN(s, 1 + sizeof(HostAddressRec));
+            if(a == NULL) {
+                abortObject(object, 501,
+                            internAtom("Couldn't allocate address"));
+                notifyObject(object);
+                errno = ENOMEM;
+                return -1;
+            }
+        }
+    }
+
+    if(a == NULL) {
+        struct in_addr ina;
+        rc = inet_aton(name->string, &ina);
+        if(rc == 1) {
+            char s[1 + sizeof(HostAddressRec)];
+            memset(s, 0, sizeof(s));
+            s[0] = DNS_A;
+            s[1] = 4;
+            memcpy(s + 2, &ina, 4);
+            a = internAtomN(s, 1 + sizeof(HostAddressRec));
+            if(a == NULL) {
+                abortObject(object, 501,
+                            internAtom("Couldn't allocate address"));
+                notifyObject(object);
+                errno = ENOMEM;
+                return -1;
+            }
+        }
+    }
+#ifdef HAVE_IPv6
+    if(a == NULL)
+        a = rfc2732(name);
+#endif
+
+    if(a) {
+        object->headers = a;
+        object->age = current_time.tv_sec;
+        object->expires = current_time.tv_sec + 240;
+        object->flags &= ~(OBJECT_INITIAL | OBJECT_INPROGRESS);
+        notifyObject(object);
+        return 0;
+    }
+
+    rc = establishDnsSocket();
+    if(rc < 0) {
+        do_log_error(L_ERROR, -rc, "Couldn't establish DNS socket.\n");
+        message = internAtomError(-rc, "Couldn't establish DNS socket");
+        goto fallback;
+    }
+
+    /* The id is used to speed up detecting replies to queries that
+       are no longer current -- see dnsReplyHandler. */
+    id = (idSeed++) & 0xFFFF;
+
+    query = malloc(sizeof(DnsQueryRec));
+    if(query == NULL) {
+        do_log(L_ERROR, "Couldn't allocate DNS query.\n");
+        message = internAtom("Couldn't allocate DNS query");
+        goto fallback;
+    }
+    query->id = id;
+    query->inet4 = NULL;
+    query->inet6 = NULL;
+    query->name = name;
+    query->time = current_time.tv_sec;
+    query->object = retainObject(object);
+    query->timeout = 4;
+    query->timeout_handler = NULL;
+    query->next = NULL;
+
+    query->timeout_handler = 
+        scheduleTimeEvent(query->timeout, dnsTimeoutHandler,
+                          sizeof(query), &query);
+    if(query->timeout_handler == NULL) {
+        do_log(L_ERROR, "Couldn't schedule DNS timeout handler.\n");
+        message = internAtom("Couldn't schedule DNS timeout handler");
+        goto free_fallback;
+    }
+    insertQuery(query);
+
+    object->flags |= OBJECT_INPROGRESS;
+    rc = sendQuery(query);
+    if(rc < 0) {
+        if(rc != -EWOULDBLOCK && rc != -EAGAIN && rc != -ENOBUFS) {
+            object->flags &= ~OBJECT_INPROGRESS;
+            message = internAtomError(-rc, "Couldn't send DNS query");
+            goto remove_fallback;
+        }
+        /* else let it timeout */
+    }
+    releaseAtom(message);
+    return 1;
+
+ remove_fallback:
+    removeQuery(query);
+ free_fallback:
+    releaseObject(query->object);
+    cancelTimeEvent(query->timeout_handler);
+    free(query);
+ fallback:
+    if(dnsUseGethostbyname >= 1) {
+        releaseAtom(message);
+        do_log(L_WARN, "Falling back on gethostbyname.\n");
+        return really_do_gethostbyname(name, object);
+    } else {
+        abortObject(object, 501, message);
+        notifyObject(object);
+        return 1;
+    }
+}
+
+static int
+dnsReplyHandler(int abort, FdEventHandlerPtr event)
+{
+    int fd = event->fd;
+    char buf[2048];
+    int len, rc;
+    ObjectPtr object;
+    unsigned ttl = 0;
+    AtomPtr name, value, message = NULL;
+    int id;
+    int af;
+    DnsQueryPtr query;
+    AtomPtr cname = NULL;
+
+    if(abort) {
+        dnsSocketHandler = NULL;
+        rc = establishDnsSocket();
+        if(rc < 0) {
+            do_log(L_ERROR, "Couldn't reestablish DNS socket.\n");
+            /* At this point, we should abort all in-flight
+               DNS requests.  Oh, well, they'll timeout anyway. */
+        }
+        return 1;
+    }
+
+    len = recv(fd, buf, 2048, 0);
+    if(len <= 0) {
+        if(errno == EINTR || errno == EAGAIN) return 0;
+        /* This is where we get ECONNREFUSED for an ICMP port unreachable */
+        do_log_error(L_ERROR, errno, "DNS: recv failed");
+        dnsGethostbynameFallback(-1, message);
+        return 0;
+    }
+
+    /* This could be a late reply to a query that timed out and was
+       resent, a reply to a query that timed out, or a reply to an
+       AAAA query when we already got a CNAME reply to the associated
+       A.  We filter such replies straight away, without trying to
+       parse them. */
+    rc = dnsReplyId(buf, 0, len, &id);
+    if(rc < 0) {
+        do_log(L_WARN, "Short DNS reply.\n");
+        return 0;
+    }
+    if(!findQuery(id, NULL)) {
+        return 0;
+    }
+
+    rc = dnsDecodeReply(buf, 0, len, &id, &name, &value, &af, &ttl);
+    if(rc < 0) {
+        assert(value == NULL);
+        /* We only want to fallback on gethostbyname if we received a
+           reply that we could not understand.  What about truncated
+           replies? */
+        if(rc < 0) {
+            do_log_error(L_WARN, -rc, "DNS");
+            if(dnsUseGethostbyname >= 2 ||
+               (dnsUseGethostbyname && 
+                (rc != -EDNS_HOST_NOT_FOUND && rc != -EDNS_NO_RECOVERY &&
+                 rc != -EDNS_FORMAT))) {
+                dnsGethostbynameFallback(id, message);
+                return 0;
+            } else {
+                message = internAtom(pstrerror(-rc));
+            }
+        } else {
+            assert(name != NULL && id >= 0 && af >= 0);
+        }
+    }
+
+    query = findQuery(id, name);
+    if(query == NULL) {
+        /* Duplicate id ? */
+        releaseAtom(value);
+        releaseAtom(name);
+        return 0;
+    }
+
+    /* We're going to use the information in this reply.  If it was an
+       error, construct an empty atom to distinguish it from information
+       we're still waiting for. */
+    if(value == NULL)
+        value = internAtom("");
+
+ again:
+    if(af == 4) {
+        if(query->inet4 == NULL) {
+            query->inet4 = value;
+            query->ttl4 = current_time.tv_sec + ttl;
+        } else
+            releaseAtom(value);
+    } else if(af == 6) {
+        if(query->inet6 == NULL) {
+            query->inet6 = value;
+            query->ttl6 = current_time.tv_sec + ttl;
+        } else
+            releaseAtom(value);
+    } else if(af == 0) {
+        /* Ignore errors in this case. */
+        if(query->inet4 && query->inet4->length == 0) {
+            releaseAtom(query->inet4);
+            query->inet4 = NULL;
+        }
+        if(query->inet6 && query->inet6->length == 0) {
+            releaseAtom(query->inet6);
+            query->inet6 = NULL;
+        }
+        if(query->inet4 || query->inet6) {
+            do_log(L_WARN, "Host %s has both %s and CNAME -- "
+                   "ignoring CNAME.\n", scrub(query->name->string),
+                   query->inet4 ? "A" : "AAAA");
+            releaseAtom(value);
+            value = internAtom("");
+            af = query->inet4 ? 4 : 6;
+            goto again;
+        } else {
+            cname = value;
+        }
+    }
+
+    if(rc >= 0 && !cname &&
+       ((dnsQueryIPv6 < 3 && query->inet4 == NULL) ||
+        (dnsQueryIPv6 > 0 && query->inet6 == NULL)))
+        return 0;
+
+    /* This query is complete */
+
+    cancelTimeEvent(query->timeout_handler);
+    object = query->object;
+
+    if(object->flags & OBJECT_INITIAL) {
+        assert(!object->headers);
+        if(cname) {
+            assert(query->inet4 == NULL && query->inet6 == NULL);
+            object->headers = cname;
+            object->expires = current_time.tv_sec + ttl;
+        } else if((!query->inet4 || query->inet4->length == 0) &&
+                  (!query->inet6 || query->inet6->length == 0)) {
+            releaseAtom(query->inet4);
+            releaseAtom(query->inet6);
+            object->expires = current_time.tv_sec + dnsNegativeTtl;
+            abortObject(object, 500, retainAtom(message));
+        } else if(!query->inet4 || query->inet4->length == 0) {
+            object->headers = query->inet6;
+            object->expires = query->ttl6;
+            releaseAtom(query->inet4);
+        } else if(!query->inet6 || query->inet6->length == 0) {
+            object->headers = query->inet4;
+            object->expires = query->ttl4;
+            releaseAtom(query->inet6);
+        } else {
+            /* need to merge results */
+            char buf[1024];
+            if(query->inet4->length + query->inet6->length > 1024) {
+                releaseAtom(query->inet4);
+                releaseAtom(query->inet6);
+                abortObject(object, 500, internAtom("DNS reply too long"));
+            } else {
+                if(dnsQueryIPv6 <= 1) {
+                    memcpy(buf, query->inet4->string, query->inet4->length);
+                    memcpy(buf + query->inet4->length,
+                           query->inet6->string + 1, query->inet6->length - 1);
+                } else {
+                    memcpy(buf, query->inet6->string, query->inet6->length);
+                    memcpy(buf + query->inet6->length,
+                           query->inet4->string + 1, query->inet4->length - 1);
+                }
+                object->headers =
+                    internAtomN(buf, 
+                                query->inet4->length + 
+                                query->inet6->length - 1);
+                if(object->headers == NULL)
+                    abortObject(object, 500, 
+                                internAtom("Couldn't allocate DNS atom"));
+            }
+            object->expires = MIN(query->ttl4, query->ttl6);
+        }
+        object->age = current_time.tv_sec;
+        object->flags &= ~(OBJECT_INITIAL | OBJECT_INPROGRESS);
+    } else {
+        do_log(L_WARN, "DNS object ex nihilo for %s.\n",
+               scrub(query->name->string));
+    }
+    
+    removeQuery(query);
+    free(query);
+
+    releaseAtom(name);
+    releaseAtom(message);
+    releaseNotifyObject(object);
+    return 0;
+}
+
+static int
+dnsGethostbynameFallback(int id, AtomPtr message)
+{
+    DnsQueryPtr query, previous;
+    ObjectPtr object;
+
+    if(inFlightDnsQueries == NULL) {
+        releaseAtom(message);
+        return 1;
+    }
+
+    query = NULL;
+    if(id < 0 || inFlightDnsQueries->id == id) {
+        previous = NULL;
+        query = inFlightDnsQueries;
+    } else {
+        previous = inFlightDnsQueries;
+        while(previous->next) {
+            if(previous->next->id == id) {
+                query = previous->next;
+                break;
+            }
+            previous = previous->next;
+        }
+        if(!query) {
+            previous = NULL;
+            query = inFlightDnsQueries;
+        }
+    }
+
+    if(previous == NULL) {
+        inFlightDnsQueries = query->next;
+        if(inFlightDnsQueries == NULL)
+            inFlightDnsQueriesLast = NULL;
+    } else {
+        previous->next = query->next;
+        if(query->next == NULL)
+            inFlightDnsQueriesLast = NULL;
+    }
+
+    object = makeObject(OBJECT_DNS, query->name->string, query->name->length,
+                        1, 0, NULL, NULL);
+    if(!object) {
+        do_log(L_ERROR, "Couldn't make DNS object.\n");
+        releaseAtom(query->name);
+        releaseAtom(message);
+        releaseObject(query->object);
+        cancelTimeEvent(query->timeout_handler);
+        free(query);
+        return -1;
+    }
+    if(dnsUseGethostbyname >= 1) {
+        releaseAtom(message);
+        do_log(L_WARN, "Falling back to using system resolver.\n");
+        really_do_gethostbyname(retainAtom(query->name), object);
+    } else {
+        releaseAtom(object->message);
+        object->message = message;
+        object->flags &= ~OBJECT_INPROGRESS;
+        releaseNotifyObject(object);
+    }
+    cancelTimeEvent(query->timeout_handler);
+    releaseAtom(query->name);
+    if(query->inet4) releaseAtom(query->inet4);
+    if(query->inet6) releaseAtom(query->inet6);
+    releaseObject(query->object);
+    free(query);
+    return 1;
+}
+
+static int
+stringToLabels(char *buf, int offset, int n, char *string)
+{
+    int i = offset;
+    int j = 0, k = 0;
+    while(1) {
+        while(string[k] != '.' && string[k] != '\0')
+            k++;
+        if(k >= j + 256) return -1;
+        buf[i] = (unsigned char)(k - j); i++; if(i >= n) return -1;
+        while(j < k) {
+            buf[i] = string[j]; i++; j++; if(i >= n) return -1;
+        }
+        if(string[j] == '\0') {
+            buf[i] = '\0';
+            i++; if(i >= n) return -1;
+            break;
+        }
+        j++; k++;
+    }
+
+    return i;
+}
+
+#ifdef UNALIGNED_ACCESS
+#define DO_NTOHS(_d, _s) _d = ntohs(*(unsigned short*)(_s));
+#define DO_NTOHL(_d, _s) _d = ntohl(*(unsigned*)(_s))
+#define DO_HTONS(_d, _s) *(unsigned short*)(_d) = htons(_s);
+#define DO_HTONL(_d, _s) *(unsigned*)(_d) = htonl(_s)
+#else
+#define DO_NTOHS(_d, _s) \
+    do { unsigned short _dd; \
+         memcpy(&(_dd), (_s), sizeof(unsigned short)); \
+         _d = ntohs(_dd); } while(0)
+#define DO_NTOHL(_d, _s) \
+    do { unsigned _dd; \
+         memcpy(&(_dd), (_s), sizeof(unsigned)); \
+         _d = ntohl(_dd); } while(0)
+#define DO_HTONS(_d, _s) \
+    do { unsigned short _dd; \
+         _dd = htons(_s); \
+         memcpy((_d), &(_dd), sizeof(unsigned short)); } while(0);
+#define DO_HTONL(_d, _s) \
+    do { unsigned _dd; \
+         _dd = htonl(_s); \
+         memcpy((_d), &(_dd), sizeof(unsigned)); } while(0);
+#endif
+
+static int
+labelsToString(char *buf, int offset, int n, char *d, int m, int *j_return)
+{
+    int i = offset, j, k;
+    int ll, rc;
+
+    j = 0;
+    while(1) {
+        if(i >= n) return -1;
+        ll = *(unsigned char*)&buf[i]; i++;
+        if(ll == 0) {
+            break;
+        }
+        if((ll & (3 << 6)) == (3 << 6)) {
+            /* RFC 1035, 4.1.4 */
+            int o;
+            if(i >= n) return -1;
+            o = (ll & ~(3 << 6)) << 8 | *(unsigned char*)&buf[i];
+            i++;
+            rc = labelsToString(buf, o, n, &d[j], m - j, &k);
+            if(rc < 0)
+                return -1;
+            j += k;
+            break;
+        } else if((ll & (3 << 6)) == 0) {
+            for(k = 0; k < ll; k++) {
+                if(i >= n || j >= m) return -1;
+                d[j++] = buf[i++];
+            }
+            if(i >= n) return -1;
+            if(buf[i] != '\0') {
+                if(j >= m) return -1;
+                d[j++] = '.';
+            }
+        } else {
+            return -1;
+        }
+    }
+    *j_return = j;
+    return i;
+}
+
+static int
+dnsBuildQuery(int id, char *buf, int offset, int n, AtomPtr name, int af)
+{
+    int i = offset;
+    int type;
+    switch(af) {
+    case 4: type = 1; break;
+    case 6: type = 28; break;
+    default: return -EINVAL;
+    }
+
+    if(i + 12 >= n) return -1;
+    DO_HTONS(&buf[i], id); i += 2;
+    DO_HTONS(&buf[i], 1<<8); i += 2;
+    DO_HTONS(&buf[i], 1); i += 2;
+    DO_HTONS(&buf[i], 0); i += 2;
+    DO_HTONS(&buf[i], 0); i += 2;
+    DO_HTONS(&buf[i], 0); i += 2;
+
+    i = stringToLabels(buf, i, n, name->string);
+    if(i < 0) return -ENAMETOOLONG;
+    
+    if(i + 4 >= n) return -ENAMETOOLONG;
+    DO_HTONS(&buf[i], type); i += 2;
+    DO_HTONS(&buf[i], 1); i += 2;
+    return i;
+}
+
+static int
+dnsReplyId(char *buf, int offset, int n, int *id_return)
+{
+    if(n - offset < 12)
+        return -1;
+    DO_NTOHS(*id_return, &buf[offset]);
+    return 1;
+}
+
+static int
+dnsDecodeReply(char *buf, int offset, int n, int *id_return,
+               AtomPtr *name_return, AtomPtr *value_return,
+               int *af_return, unsigned *ttl_return)
+{
+    int i = offset, j, m;
+    int id = -1, b23, qdcount, ancount, nscount, arcount, rdlength;
+    int class, type;
+    unsigned int ttl;
+    char b[2048];
+    int af = -1;
+    AtomPtr name = NULL, value;
+    char addresses[1024];
+    int addr_index = 0;
+    int error = EDNS_NO_ADDRESS;
+    unsigned final_ttl = 7 * 24 * 3600;
+    int dnserror;
+
+    if(n - i < 12) {
+        error = EDNS_INVALID;
+        goto fail;
+    }
+
+    DO_NTOHS(id, &buf[i]); i += 2;
+    DO_NTOHS(b23, &buf[i]); i += 2;
+    DO_NTOHS(qdcount, &buf[i]); i += 2;
+    DO_NTOHS(ancount, &buf[i]); i += 2;
+    DO_NTOHS(nscount, &buf[i]); i += 2;
+    DO_NTOHS(arcount, &buf[i]); i += 2;
+
+    do_log(D_DNS, 
+           "DNS id %d, b23 0x%x, qdcount %d, ancount %d, "
+           "nscount %d, arcount %d\n",
+           id, b23, qdcount, ancount, nscount, arcount);
+
+    if((b23 & (0xF870)) != 0x8000) {
+        do_log(L_ERROR, "Incorrect DNS reply (b23 = 0x%x).\n", b23);
+        error = EDNS_INVALID;
+        goto fail;
+    }
+
+    dnserror = b23 & 0xF;
+
+    if(b23 & 0x200) {
+        do_log(L_WARN, "Truncated DNS reply (b23 = 0x%x).\n", b23);
+    }
+
+    if(dnserror || qdcount != 1) {
+        if(!dnserror)
+            do_log(L_ERROR, 
+                   "Unexpected number %d of DNS questions.\n", qdcount);
+        if(dnserror == 1)
+            error = EDNS_FORMAT;
+        else if(dnserror == 2)
+            error = EDNS_NO_RECOVERY;
+        else if(dnserror == 3)
+            error = EDNS_HOST_NOT_FOUND;
+        else if(dnserror == 4 || dnserror == 5)
+            error = EDNS_REFUSED;
+        else if(dnserror == 0)
+            error = EDNS_INVALID;
+        else
+            error = EUNKNOWN;
+        goto fail;
+    }
+
+    /* We do this early, so that we can return the address family to
+       the caller in case of error. */
+    i = labelsToString(buf, i, n, b, 2048, &m);
+    if(i < 0) {
+        error = EDNS_FORMAT;
+        goto fail;
+    }
+    DO_NTOHS(type, &buf[i]); i += 2;
+    DO_NTOHS(class, &buf[i]); i += 2;
+
+    if(type == 1)
+        af = 4;
+    else if(type == 28)
+        af = 6;
+    else {
+        error = EDNS_FORMAT;
+        goto fail;
+    }
+
+    do_log(D_DNS, "DNS q: ");
+    do_log_n(D_DNS, b, m);
+    do_log(D_DNS, " (%d, %d)\n", type, class);
+    name = internAtomLowerN(b, m);
+    if(name == NULL) {
+        error = ENOMEM;
+        goto fail;
+    }
+
+    if(class != 1) {
+        error = EDNS_FORMAT;
+        goto fail;
+    }
+
+#define PARSE_ANSWER(kind, label) \
+do { \
+    i = labelsToString(buf, i, 1024, b, 2048, &m); \
+    if(i < 0) goto label; \
+    DO_NTOHS(type, &buf[i]); i += 2; if(i > 1024) goto label; \
+    DO_NTOHS(class, &buf[i]); i += 2; if(i > 1024) goto label; \
+    DO_NTOHL(ttl, &buf[i]); i += 4; if(i > 1024) goto label; \
+    DO_NTOHS(rdlength, &buf[i]); i += 2; if(i > 1024) goto label; \
+    do_log(D_DNS, "DNS " kind ": "); \
+    do_log_n(D_DNS, b, m); \
+    do_log(D_DNS, " (%d, %d): %d bytes, ttl %u\n", \
+           type, class, rdlength, ttl); \
+   } while(0)
+
+
+    for(j = 0; j < ancount; j++) {
+        PARSE_ANSWER("an", fail);
+        if(strcasecmp_n(name->string, b, m) == 0) {
+            if(class != 1) {
+                do_log(D_DNS, "DNS: %s: unknown class %d.\n", 
+                       name->string, class);
+                error = EDNS_UNSUPPORTED;
+                goto cont;
+            }
+            if(type == 1 || type == 28) {
+                if((type == 1 && rdlength != 4) ||
+                   (type == 28 && rdlength != 16)) {
+                    do_log(L_ERROR, 
+                           "DNS: %s: unexpected length %d of %s record.\n",
+                           scrub(name->string),
+                           rdlength, type == 1 ? "A" : "AAAA");
+                    error = EDNS_INVALID;
+                    if(rdlength <= 0 || rdlength >= 32)
+                        goto fail;
+                    goto cont;
+                }
+                if(af == 0) {
+                    do_log(L_WARN, "DNS: %s: host has both A and CNAME -- "
+                           "ignoring CNAME.\n", scrub(name->string));
+                    addr_index = 0;
+                    af = -1;
+                }
+                if(type == 1) {
+                    if(af < 0)
+                        af = 4;
+                    else if(af == 6) {
+                        do_log(L_WARN, "Unexpected AAAA reply.\n");
+                        goto cont;
+                    }
+                } else {
+                    if(af < 0)
+                        af = 6;
+                    else if(af == 4) {
+                        do_log(L_WARN, "Unexpected A reply.\n");
+                        goto cont;
+                    }
+                }
+
+                if(addr_index == 0) {
+                    addresses[0] = DNS_A;
+                    addr_index++;
+                } else {
+                    if(addr_index > 1000) {
+                        error = EDNS_INVALID;
+                        goto fail;
+                    }
+                }
+                assert(addresses[0] == DNS_A);
+                if(final_ttl > ttl)
+                    final_ttl = ttl;
+                memset(&addresses[addr_index], 0, sizeof(HostAddressRec));
+                if(type == 1) {
+                    addresses[addr_index] = 4;
+                    memcpy(addresses + addr_index + 1, buf + i, 4);
+                } else {
+                    addresses[addr_index] = 6;
+                    memcpy(addresses + addr_index + 1, buf + i, 16);
+                }
+                addr_index += sizeof(HostAddressRec);
+            } else if(type == 5) {
+                int j, k;
+                if(af != 0 && addr_index > 0) {
+                    do_log(L_WARN, "DNS: host has both CNAME and A -- "
+                           "ignoring CNAME.\n");
+                    goto cont;
+                }
+                af = 0;
+
+                if(addr_index != 0) {
+                    /* Only warn if the CNAMEs are not identical */
+                    char tmp[512]; int jj, kk;
+                    assert(addresses[0] == DNS_CNAME);
+                    jj = labelsToString(buf, i, n,
+                                        tmp, 512, &kk);
+                    if(jj < 0 ||
+                       kk != strlen(addresses + 1) ||
+                       memcmp(addresses + 1, tmp, kk) != 0) {
+                        do_log(L_WARN, "DNS: "
+                               "%s: host has multiple CNAMEs -- "
+                               "ignoring subsequent.\n",
+                               scrub(name->string));
+
+                    }
+                    goto cont;
+                }
+                addresses[0] = DNS_CNAME;
+                addr_index++;
+                j = labelsToString(buf, i, n,
+                                   addresses + 1, 1020, &k);
+                if(j < 0) {
+                    addr_index = 0;
+                    error = ENAMETOOLONG;
+                    continue;
+                }
+                addr_index = k + 1;
+            } else {
+                error = EDNS_NO_ADDRESS;
+                i += rdlength;
+                continue;
+            }
+
+        }
+    cont:
+        i += rdlength;
+    }
+
+#if (LOGGING_MAX & D_DNS)
+    for(j = 0; j < nscount; j++) {
+        PARSE_ANSWER("ns", nofail);
+        i += rdlength;
+    }
+
+    for(j = 0; j < arcount; j++) {
+        PARSE_ANSWER("ar", nofail);
+        i += rdlength;
+    }
+
+ nofail:
+#endif
+
+#undef PARSE_ANSWER
+
+    do_log(D_DNS, "DNS: %d bytes\n", addr_index);
+    if(af < 0)
+        goto fail;
+
+    value = internAtomN(addresses, addr_index);
+    if(value == NULL) {
+        error = ENOMEM;
+        goto fail;
+    }
+
+    assert(af >= 0);
+    *id_return = id;
+    *name_return = name;
+    *value_return = value;
+    *af_return = af;
+    *ttl_return = final_ttl;
+    return 1;
+
+ fail:
+    *id_return = id;
+    *name_return = name;
+    *value_return = NULL;
+    *af_return = af;
+    return -error;
+}
+
+#else
+
+static int
+really_do_dns(AtomPtr name, ObjectPtr object)
+{
+    abort();
+}
+
+#endif

+ 48 - 0
polipo/dns.h

@@ -0,0 +1,48 @@
+/*
+Copyright (c) 2003-2006 by Juliusz Chroboczek
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+extern char *nameServer;
+extern int useGethostbyname;
+
+#define DNS_A 0
+#define DNS_CNAME 1
+
+typedef struct _GethostbynameRequest {
+    AtomPtr name;
+    AtomPtr addr;
+    AtomPtr error_message;
+    int count;
+    ObjectPtr object;
+    int (*handler)(int, struct _GethostbynameRequest*);
+    void *data;
+} GethostbynameRequestRec, *GethostbynameRequestPtr;
+
+/* Note that this requires no alignment */
+typedef struct _HostAddress {
+    char af;                     /* 4 or 6 */
+    char data[16];
+} HostAddressRec, *HostAddressPtr;
+
+void preinitDns(void);
+void initDns(void);
+int do_gethostbyname(char *name, int count,
+                     int (*handler)(int, GethostbynameRequestPtr), void *data);

+ 834 - 0
polipo/event.c

@@ -0,0 +1,834 @@
+/*
+Copyright (c) 2003-2006 by Juliusz Chroboczek
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+#include "polipo.h"
+
+#ifdef HAVE_FORK
+static volatile sig_atomic_t exitFlag = 0;
+#else
+static int exitFlag = 0;
+#endif
+static int in_signalCondition = 0;
+
+static TimeEventHandlerPtr timeEventQueue;
+static TimeEventHandlerPtr timeEventQueueLast;
+
+struct timeval current_time;
+struct timeval null_time = {0,0};
+
+static int fdEventSize = 0;
+static int fdEventNum = 0;
+static struct pollfd *poll_fds = NULL;
+static FdEventHandlerPtr *fdEvents = NULL, *fdEventsLast = NULL;
+int diskIsClean = 1;
+
+static int fds_invalid = 0;
+
+static inline int
+timeval_cmp(struct timeval *t1, struct timeval *t2)
+{
+    if(t1->tv_sec < t2->tv_sec)
+        return -1;
+    else if(t1->tv_sec > t2->tv_sec)
+        return +1;
+    else if(t1->tv_usec < t2->tv_usec)
+        return -1;
+    else if(t1->tv_usec > t2->tv_usec)
+        return +1;
+    else
+        return 0;
+}
+
+static inline void
+timeval_minus(struct timeval *d,
+              const struct timeval *s1, const struct timeval *s2)
+{
+    if(s1->tv_usec >= s2->tv_usec) {
+        d->tv_usec = s1->tv_usec - s2->tv_usec;
+        d->tv_sec = s1->tv_sec - s2->tv_sec;
+    } else {
+        d->tv_usec = s1->tv_usec + 1000000 - s2->tv_usec;
+        d->tv_sec = s1->tv_sec - s2->tv_sec - 1;
+    }
+}
+
+int
+timeval_minus_usec(const struct timeval *s1, const struct timeval *s2)
+{
+    return (s1->tv_sec - s2->tv_sec) * 1000000 + s1->tv_usec - s2->tv_usec;
+}
+
+#ifdef HAVE_FORK
+static void
+sigexit(int signo)
+{
+    if(signo == SIGUSR1)
+        exitFlag = 1;
+    else if(signo == SIGUSR2)
+        exitFlag = 2;
+    else
+        exitFlag = 3;
+}
+#endif
+
+void
+initEvents()
+{
+#ifdef HAVE_FORK
+    struct sigaction sa;
+    sigset_t ss;
+
+    sigemptyset(&ss);
+    sa.sa_handler = SIG_IGN;
+    sa.sa_mask = ss;
+    sa.sa_flags = 0;
+    sigaction(SIGPIPE, &sa, NULL);
+
+    sigemptyset(&ss);
+    sa.sa_handler = sigexit;
+    sa.sa_mask = ss;
+    sa.sa_flags = 0;
+    sigaction(SIGTERM, &sa, NULL);
+
+    sigemptyset(&ss);
+    sa.sa_handler = sigexit;
+    sa.sa_mask = ss;
+    sa.sa_flags = 0;
+    sigaction(SIGHUP, &sa, NULL);
+
+    sigemptyset(&ss);
+    sa.sa_handler = sigexit;
+    sa.sa_mask = ss;
+    sa.sa_flags = 0;
+    sigaction(SIGINT, &sa, NULL);
+
+    sigemptyset(&ss);
+    sa.sa_handler = sigexit;
+    sa.sa_mask = ss;
+    sa.sa_flags = 0;
+    sigaction(SIGUSR1, &sa, NULL);
+
+    sigemptyset(&ss);
+    sa.sa_handler = sigexit;
+    sa.sa_mask = ss;
+    sa.sa_flags = 0;
+    sigaction(SIGUSR2, &sa, NULL);
+#endif
+
+    timeEventQueue = NULL;
+    timeEventQueueLast = NULL;
+    fdEventSize = 0;
+    fdEventNum = 0;
+    poll_fds = NULL;
+    fdEvents = NULL;
+    fdEventsLast = NULL;
+}
+
+void
+uninitEvents(void)
+{
+#ifdef HAVE_FORK
+    struct sigaction sa;
+    sigset_t ss;
+
+    sigemptyset(&ss);
+    sa.sa_handler = SIG_DFL;
+    sa.sa_mask = ss;
+    sa.sa_flags = 0;
+    sigaction(SIGTERM, &sa, NULL);
+
+    sigemptyset(&ss);
+    sa.sa_handler = SIG_DFL;
+    sa.sa_mask = ss;
+    sa.sa_flags = 0;
+    sigaction(SIGHUP, &sa, NULL);
+
+    sigemptyset(&ss);
+    sa.sa_handler = SIG_DFL;
+    sa.sa_mask = ss;
+    sa.sa_flags = 0;
+    sigaction(SIGINT, &sa, NULL);
+
+    sigemptyset(&ss);
+    sa.sa_handler = SIG_DFL;
+    sa.sa_mask = ss;
+    sa.sa_flags = 0;
+    sigaction(SIGUSR1, &sa, NULL);
+
+    sigemptyset(&ss);
+    sa.sa_handler = SIG_DFL;
+    sa.sa_mask = ss;
+    sa.sa_flags = 0;
+    sigaction(SIGUSR2, &sa, NULL);
+#endif
+}
+
+#ifdef HAVE_FORK
+void
+interestingSignals(sigset_t *ss)
+{
+    sigemptyset(ss);
+    sigaddset(ss, SIGTERM);
+    sigaddset(ss, SIGHUP);
+    sigaddset(ss, SIGINT);
+    sigaddset(ss, SIGUSR1);
+    sigaddset(ss, SIGUSR2);
+}
+#endif
+
+void
+timeToSleep(struct timeval *time)
+{
+    if(!timeEventQueue) {
+        time->tv_sec = ~0L;
+        time->tv_usec = ~0L;
+    } else {
+        *time = timeEventQueue->time;
+    }
+}
+
+static TimeEventHandlerPtr
+enqueueTimeEvent(TimeEventHandlerPtr event)
+{
+    TimeEventHandlerPtr otherevent;
+
+    /* We try to optimise two cases -- the event happens very soon, or
+       it happens after most of the other events. */
+    if(timeEventQueue == NULL ||
+       timeval_cmp(&event->time, &timeEventQueue->time) < 0) {
+        /* It's the first event */
+        event->next = timeEventQueue;
+        event->previous = NULL;
+        if(timeEventQueue) {
+            timeEventQueue->previous = event;
+        } else {
+            timeEventQueueLast = event;
+        }
+        timeEventQueue = event;
+    } else if(timeval_cmp(&event->time, &timeEventQueueLast->time) >= 0) {
+        /* It's the last one */
+        event->next = NULL;
+        event->previous = timeEventQueueLast;
+        timeEventQueueLast->next = event;
+        timeEventQueueLast = event;
+    } else {
+        /* Walk from the end */
+        otherevent = timeEventQueueLast;
+        while(otherevent->previous &&
+              timeval_cmp(&event->time, &otherevent->previous->time) < 0) {
+            otherevent = otherevent->previous;
+        }
+        event->next = otherevent;
+        event->previous = otherevent->previous;
+        if(otherevent->previous) {
+            otherevent->previous->next = event;
+        } else {
+            timeEventQueue = event;
+        }
+        otherevent->previous = event;
+    }
+    return event;
+}
+
+TimeEventHandlerPtr
+scheduleTimeEvent(int seconds,
+                  int (*handler)(TimeEventHandlerPtr), int dsize, void *data)
+{
+    struct timeval when;
+    TimeEventHandlerPtr event;
+
+    if(seconds >= 0) {
+        when = current_time;
+        when.tv_sec += seconds;
+    } else {
+        when.tv_sec = 0;
+        when.tv_usec = 0;
+    }
+
+    event = malloc(sizeof(TimeEventHandlerRec) - 1 + dsize);
+    if(event == NULL) {
+        do_log(L_ERROR, "Couldn't allocate time event handler -- "
+               "discarding all objects.\n");
+        exitFlag = 2;
+        return NULL;
+    }
+
+    event->time = when;
+    event->handler = handler;
+    /* Let the compiler optimise the common case */
+    if(dsize == sizeof(void*))
+        memcpy(event->data, data, sizeof(void*));
+    else if(dsize > 0)
+        memcpy(event->data, data, dsize);
+
+    return enqueueTimeEvent(event);
+}
+
+void
+cancelTimeEvent(TimeEventHandlerPtr event)
+{
+    if(event == timeEventQueue)
+        timeEventQueue = event->next;
+    if(event == timeEventQueueLast)
+        timeEventQueueLast = event->previous;
+    if(event->next)
+        event->next->previous = event->previous;
+    if(event->previous)
+        event->previous->next = event->next;
+    free(event);
+}
+
+int
+allocateFdEventNum(int fd)
+{
+    int i;
+    if(fdEventNum < fdEventSize) {
+        i = fdEventNum;
+        fdEventNum++;
+    } else {
+        struct pollfd *new_poll_fds;
+        FdEventHandlerPtr *new_fdEvents, *new_fdEventsLast;
+        int new_size = 3 * fdEventSize / 2 + 1;
+
+        new_poll_fds = realloc(poll_fds, new_size * sizeof(struct pollfd));
+        if(!new_poll_fds)
+            return -1;
+        new_fdEvents = realloc(fdEvents, new_size * sizeof(FdEventHandlerPtr));
+        if(!new_fdEvents)
+            return -1;
+        new_fdEventsLast = realloc(fdEventsLast, 
+                                   new_size * sizeof(FdEventHandlerPtr));
+        if(!new_fdEventsLast)
+            return -1;
+
+        poll_fds = new_poll_fds;
+        fdEvents = new_fdEvents;
+        fdEventsLast = new_fdEventsLast;
+        fdEventSize = new_size;
+        i = fdEventNum;
+        fdEventNum++;
+    }
+
+    poll_fds[i].fd = fd;
+    poll_fds[i].events = POLLERR | POLLHUP | POLLNVAL;
+    poll_fds[i].revents = 0;
+    fdEvents[i] = NULL;
+    fdEventsLast[i] = NULL;
+    fds_invalid = 1;
+    return i;
+}
+
+void
+deallocateFdEventNum(int i)
+{
+    if(i < fdEventNum - 1) {
+        memmove(&poll_fds[i], &poll_fds[i + 1], 
+                (fdEventNum - i - 1) * sizeof(struct pollfd));
+        memmove(&fdEvents[i], &fdEvents[i + 1],
+                (fdEventNum - i - 1) * sizeof(FdEventHandlerPtr));
+        memmove(&fdEventsLast[i], &fdEventsLast[i + 1],
+                (fdEventNum - i - 1) * sizeof(FdEventHandlerPtr));
+    }
+    fdEventNum--;
+    fds_invalid = 1;
+}
+
+FdEventHandlerPtr 
+makeFdEvent(int fd, int poll_events, 
+            int (*handler)(int, FdEventHandlerPtr), int dsize, void *data)
+{
+    FdEventHandlerPtr event;
+
+    event = malloc(sizeof(FdEventHandlerRec) - 1 + dsize);
+    if(event == NULL) {
+        do_log(L_ERROR, "Couldn't allocate fd event handler -- "
+               "discarding all objects.\n");
+        exitFlag = 2;
+        return NULL;
+    }
+    event->fd = fd;
+    event->poll_events = poll_events;
+    event->handler = handler;
+    /* Let the compiler optimise the common cases */
+    if(dsize == sizeof(void*))
+        memcpy(event->data, data, sizeof(void*));
+    else if(dsize == sizeof(StreamRequestRec))
+        memcpy(event->data, data, sizeof(StreamRequestRec));
+    else if(dsize > 0)
+        memcpy(event->data, data, dsize);
+    return event;
+}
+
+FdEventHandlerPtr
+registerFdEventHelper(FdEventHandlerPtr event)
+{
+    int i;
+    int fd = event->fd;
+
+    for(i = 0; i < fdEventNum; i++)
+        if(poll_fds[i].fd == fd)
+            break;
+
+    if(i >= fdEventNum)
+        i = allocateFdEventNum(fd);
+    if(i < 0) {
+        free(event);
+        return NULL;
+    }
+
+    event->next = NULL;
+    event->previous = fdEventsLast[i];
+    if(fdEvents[i] == NULL) {
+        fdEvents[i] = event;
+    } else {
+        fdEventsLast[i]->next = event;
+    }
+    fdEventsLast[i] = event;
+    poll_fds[i].events |= event->poll_events;
+
+    return event;
+}
+
+FdEventHandlerPtr 
+registerFdEvent(int fd, int poll_events, 
+                int (*handler)(int, FdEventHandlerPtr), int dsize, void *data)
+{
+    FdEventHandlerPtr event;
+
+    event = makeFdEvent(fd, poll_events, handler, dsize, data);
+    if(event == NULL)
+        return NULL;
+
+    return registerFdEventHelper(event);
+}
+
+static int
+recomputePollEvents(FdEventHandlerPtr event) 
+{
+    int pe = 0;
+    while(event) {
+        pe |= event->poll_events;
+        event = event->next;
+    }
+    return pe | POLLERR | POLLHUP | POLLNVAL;
+}
+
+static void
+unregisterFdEventI(FdEventHandlerPtr event, int i)
+{
+    assert(i < fdEventNum && poll_fds[i].fd == event->fd);
+
+    if(fdEvents[i] == event) {
+        assert(!event->previous);
+        fdEvents[i] = event->next;
+    } else {
+        event->previous->next = event->next;
+    }
+
+    if(fdEventsLast[i] == event) {
+        assert(!event->next);
+        fdEventsLast[i] = event->previous;
+    } else {
+        event->next->previous = event->previous;
+    }
+
+    free(event);
+
+    if(fdEvents[i] == NULL) {
+        deallocateFdEventNum(i);
+    } else {
+        poll_fds[i].events = recomputePollEvents(fdEvents[i]) | 
+            POLLERR | POLLHUP | POLLNVAL;
+    }
+}
+
+void 
+unregisterFdEvent(FdEventHandlerPtr event)
+{
+    int i;
+
+    for(i = 0; i < fdEventNum; i++) {
+        if(poll_fds[i].fd == event->fd) {
+            unregisterFdEventI(event, i);
+            return;
+        }
+    }
+    abort();
+}
+
+void
+runTimeEventQueue()
+{
+    TimeEventHandlerPtr event;
+    int done;
+
+    while(timeEventQueue && 
+          timeval_cmp(&timeEventQueue->time, &current_time) <= 0) {
+        event = timeEventQueue;
+        timeEventQueue = event->next;
+        if(timeEventQueue)
+            timeEventQueue->previous = NULL;
+        else
+            timeEventQueueLast = NULL;
+        done = event->handler(event);
+        assert(done);
+        free(event);
+    }
+}
+
+static FdEventHandlerPtr
+findEventHelper(int revents, FdEventHandlerPtr events)
+{
+    FdEventHandlerPtr event = events;
+    while(event) {
+        if(revents & event->poll_events)
+            return event;
+        event = event->next;
+    }
+    return NULL;
+}
+
+
+
+static FdEventHandlerPtr
+findEvent(int revents, FdEventHandlerPtr events)
+{
+    FdEventHandlerPtr event;
+
+    assert(!(revents & POLLNVAL));
+    
+    if((revents & POLLHUP) || (revents & POLLERR)) {
+        event = findEventHelper(POLLOUT, events);
+        if(event) return event;
+
+        event = findEventHelper(POLLIN, events);
+        if(event) return event;
+        return NULL;
+    }
+
+    if(revents & POLLOUT) {
+        event = findEventHelper(POLLOUT, events);
+        if(event) return event;
+    }
+
+    if(revents & POLLIN) {
+        event = findEventHelper(POLLIN, events);
+        if(event) return event;
+    }
+    return NULL;
+}
+
+typedef struct _FdEventHandlerPoke {
+    int fd;
+    int what;
+    int status;
+} FdEventHandlerPokeRec, *FdEventHandlerPokePtr;
+
+static int
+pokeFdEventHandler(TimeEventHandlerPtr tevent)
+{
+    FdEventHandlerPokePtr poke = (FdEventHandlerPokePtr)tevent->data;
+    int fd = poke->fd;
+    int what = poke->what;
+    int status = poke->status;
+    int done;
+    FdEventHandlerPtr event, next;
+    int i;
+
+    for(i = 0; i < fdEventNum; i++) {
+        if(poll_fds[i].fd == fd)
+            break;
+    }
+
+    if(i >= fdEventNum)
+        return 1;
+
+    event = fdEvents[i];
+    while(event) {
+        next = event->next;
+        if(event->poll_events & what) {
+            done = event->handler(status, event);
+            if(done) {
+                if(fds_invalid)
+                    unregisterFdEvent(event);
+                else
+                    unregisterFdEventI(event, i);
+            }
+            if(fds_invalid)
+                break;
+        }
+        event = next;
+    }
+    return 1;
+}
+
+void 
+pokeFdEvent(int fd, int status, int what)
+{
+    TimeEventHandlerPtr handler;
+    FdEventHandlerPokeRec poke;
+
+    poke.fd = fd;
+    poke.status = status;
+    poke.what = what;
+
+    handler = scheduleTimeEvent(0, pokeFdEventHandler,
+                                sizeof(poke), &poke);
+    if(!handler) {
+        do_log(L_ERROR, "Couldn't allocate handler.\n");
+    }
+}
+
+int
+workToDo()
+{
+    struct timeval sleep_time;
+    int rc;
+
+    if(exitFlag)
+        return 1;
+
+    timeToSleep(&sleep_time);
+    gettimeofday(&current_time, NULL);
+    if(timeval_cmp(&sleep_time, &current_time) <= 0)
+        return 1;
+    rc = poll(poll_fds, fdEventNum, 0);
+    if(rc < 0) {
+        do_log_error(L_ERROR, errno, "Couldn't poll");
+        return 1;
+    }
+    return(rc >= 1);
+}
+    
+void
+eventLoop()
+{
+    struct timeval sleep_time, timeout;
+    int rc, i, done, n;
+    FdEventHandlerPtr event;
+    int fd0;
+
+    gettimeofday(&current_time, NULL);
+
+    while(1) {
+    again:
+        if(exitFlag) {
+            if(exitFlag < 3)
+                reopenLog();
+            if(exitFlag >= 2) {
+                discardObjects(1, 0);
+                if(exitFlag >= 3)
+                    return;
+                free_chunk_arenas();
+            } else {
+                writeoutObjects(1);
+            }
+            initForbidden();
+            exitFlag = 0;
+        }
+
+        timeToSleep(&sleep_time);
+        if(sleep_time.tv_sec == -1) {
+            rc = poll(poll_fds, fdEventNum, 
+                      diskIsClean ? -1 : idleTime * 1000);
+        } else if(timeval_cmp(&sleep_time, &current_time) <= 0) {
+            runTimeEventQueue();
+            continue;
+        } else {
+            gettimeofday(&current_time, NULL);
+            if(timeval_cmp(&sleep_time, &current_time) <= 0) {
+                runTimeEventQueue();
+                continue;
+            } else {
+                int t;
+                timeval_minus(&timeout, &sleep_time, &current_time);
+                t = timeout.tv_sec * 1000 + (timeout.tv_usec + 999) / 1000;
+                rc = poll(poll_fds, fdEventNum,
+                          diskIsClean ? t : MIN(idleTime * 1000, t));
+            }
+        }
+
+        gettimeofday(&current_time, NULL);
+
+        if(rc < 0) {
+            if(errno == EINTR) {
+                continue;
+            } else if(errno == ENOMEM) {
+                free_chunk_arenas();
+                do_log(L_ERROR, 
+                       "Couldn't poll: out of memory.  "
+                       "Sleeping for one second.\n");
+                sleep(1);
+            } else {
+                do_log_error(L_ERROR, errno, "Couldn't poll");
+                exitFlag = 3;
+            }
+            continue;
+        }
+
+        if(rc == 0) {
+            if(!diskIsClean) {
+                timeToSleep(&sleep_time);
+                if(timeval_cmp(&sleep_time, &current_time) > 0)
+                    writeoutObjects(0);
+            }
+            continue;
+        }
+
+        /* Rather than tracking all changes to the in-memory cache, we
+           assume that something changed whenever we see any activity. */
+        diskIsClean = 0;
+
+        fd0 = 
+            (current_time.tv_usec ^ (current_time.tv_usec >> 16)) % fdEventNum;
+        n = rc;
+        for(i = 0; i < fdEventNum; i++) {
+            int j = (i + fd0) % fdEventNum;
+            if(n <= 0)
+                break;
+            if(poll_fds[j].revents) {
+                n--;
+                event = findEvent(poll_fds[j].revents, fdEvents[j]);
+                if(!event)
+                    continue;
+                done = event->handler(0, event);
+                if(done) {
+                    if(fds_invalid)
+                        unregisterFdEvent(event);
+                    else
+                        unregisterFdEventI(event, j);
+                }
+                if(fds_invalid) {
+                    fds_invalid = 0;
+                    goto again;
+                } 
+            }
+        }
+    }
+}
+
+void
+initCondition(ConditionPtr condition)
+{
+    condition->handlers = NULL;
+}
+
+ConditionPtr
+makeCondition(void)
+{
+    ConditionPtr condition;
+    condition = malloc(sizeof(ConditionRec));
+    if(condition == NULL)
+        return NULL;
+    initCondition(condition);
+    return condition;
+}
+
+ConditionHandlerPtr
+conditionWait(ConditionPtr condition,
+              int (*handler)(int, ConditionHandlerPtr),
+              int dsize, void *data)
+{
+    ConditionHandlerPtr chandler;
+
+    assert(!in_signalCondition);
+
+    chandler = malloc(sizeof(ConditionHandlerRec) - 1 + dsize);
+    if(!chandler)
+        return NULL;
+
+    chandler->condition = condition;
+    chandler->handler = handler;
+    /* Let the compiler optimise the common case */
+    if(dsize == sizeof(void*))
+        memcpy(chandler->data, data, sizeof(void*));
+    else if(dsize > 0)
+        memcpy(chandler->data, data, dsize);
+
+    if(condition->handlers)
+        condition->handlers->previous = chandler;
+    chandler->next = condition->handlers;
+    chandler->previous = NULL;
+    condition->handlers = chandler;
+    return chandler;
+}
+
+void
+unregisterConditionHandler(ConditionHandlerPtr handler)
+{
+    ConditionPtr condition = handler->condition;
+
+    assert(!in_signalCondition);
+
+    if(condition->handlers == handler)
+        condition->handlers = condition->handlers->next;
+    if(handler->next)
+        handler->next->previous = handler->previous;
+    if(handler->previous)
+        handler->previous->next = handler->next;
+
+    free(handler);
+}
+
+void 
+abortConditionHandler(ConditionHandlerPtr handler)
+{
+    int done;
+    done = handler->handler(-1, handler);
+    assert(done);
+    unregisterConditionHandler(handler);
+}
+
+void
+signalCondition(ConditionPtr condition)
+{
+    ConditionHandlerPtr handler;
+    int done;
+
+    assert(!in_signalCondition);
+    in_signalCondition++;
+
+    handler = condition->handlers;
+    while(handler) {
+        ConditionHandlerPtr next = handler->next;
+        done = handler->handler(0, handler);
+        if(done) {
+            if(handler == condition->handlers)
+                condition->handlers = next;
+            if(next)
+                next->previous = handler->previous;
+            if(handler->previous)
+                handler->previous->next = next;
+            else
+                condition->handlers = next;
+            free(handler);
+        }
+        handler = next;
+    }
+    in_signalCondition--;
+}
+
+void
+polipoExit()
+{
+    exitFlag = 3;
+}

+ 90 - 0
polipo/event.h

@@ -0,0 +1,90 @@
+/*
+Copyright (c) 2003-2006 by Juliusz Chroboczek
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+extern struct timeval current_time;
+extern struct timeval null_time;
+extern int diskIsClean;
+
+typedef struct _TimeEventHandler {
+    struct timeval time;
+    struct _TimeEventHandler *previous, *next;
+    int (*handler)(struct _TimeEventHandler*);
+    char data[1];
+} TimeEventHandlerRec, *TimeEventHandlerPtr;
+
+typedef struct _FdEventHandler {
+    short fd;
+    short poll_events;
+    struct _FdEventHandler *previous, *next;
+    int (*handler)(int, struct _FdEventHandler*);
+    char data[1];
+} FdEventHandlerRec, *FdEventHandlerPtr;
+
+typedef struct _ConditionHandler {
+    struct _Condition *condition;
+    struct _ConditionHandler *previous, *next;
+    int (*handler)(int, struct _ConditionHandler*);
+    char data[1];
+} ConditionHandlerRec, *ConditionHandlerPtr;
+
+typedef struct _Condition {
+    ConditionHandlerPtr handlers;
+} ConditionRec, *ConditionPtr;
+
+void initEvents(void);
+void uninitEvents(void);
+#ifdef HAVE_FORK
+void interestingSignals(sigset_t *ss);
+#endif
+
+TimeEventHandlerPtr scheduleTimeEvent(int seconds,
+                                      int (*handler)(TimeEventHandlerPtr),
+                                      int dsize, void *data);
+
+int timeval_minus_usec(const struct timeval *s1, const struct timeval *s2)
+     ATTRIBUTE((pure));
+void cancelTimeEvent(TimeEventHandlerPtr);
+int allocateFdEventNum(int fd);
+void deallocateFdEventNum(int i);
+void timeToSleep(struct timeval *);
+void runTimeEventQueue(void);
+FdEventHandlerPtr makeFdEvent(int fd, int poll_events, 
+                              int (*handler)(int, FdEventHandlerPtr), 
+                              int dsize, void *data);
+FdEventHandlerPtr registerFdEvent(int fd, int poll_events,
+                                  int (*handler)(int, FdEventHandlerPtr),
+                                  int dsize, void *data);
+FdEventHandlerPtr registerFdEventHelper(FdEventHandlerPtr event);
+void unregisterFdEvent(FdEventHandlerPtr event);
+void pokeFdEvent(int fd, int status, int what);
+int workToDo(void);
+void eventLoop(void);
+ConditionPtr makeCondition(void);
+void initCondition(ConditionPtr);
+void signalCondition(ConditionPtr condition);
+ConditionHandlerPtr 
+conditionWait(ConditionPtr condition,
+              int (*handler)(int, ConditionHandlerPtr),
+              int dsize, void *data);
+void unregisterConditionHandler(ConditionHandlerPtr);
+void abortConditionHandler(ConditionHandlerPtr);
+void polipoExit(void);

+ 833 - 0
polipo/forbidden.c

@@ -0,0 +1,833 @@
+/*
+Copyright (c) 2003-2010 by Juliusz Chroboczek
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+#include "polipo.h"
+
+#ifndef NO_FORBIDDEN
+
+#include <regex.h>
+#include <assert.h>
+
+typedef struct _Domain {
+    int length;
+    char domain[1];
+} DomainRec, *DomainPtr;
+
+AtomPtr forbiddenFile = NULL;
+AtomPtr forbiddenUrl = NULL;
+int forbiddenRedirectCode = 302;
+
+AtomPtr redirector = NULL;
+int redirectorRedirectCode = 302;
+
+DomainPtr *forbiddenDomains = NULL;
+regex_t *forbiddenRegex = NULL;
+
+AtomPtr uncachableFile = NULL;
+DomainPtr *uncachableDomains = NULL;
+regex_t *uncachableRegex = NULL;
+
+AtomPtr forbiddenTunnelsFile = NULL;
+DomainPtr *forbiddenTunnelsDomains = NULL;
+regex_t *forbiddenTunnelsRegex = NULL;
+
+
+/* these three are only used internally by {parse,read}DomainFile */
+/* to avoid having to pass it all as parameters */
+static DomainPtr *domains;
+static char *regexbuf;
+static int rlen, rsize, dlen, dsize;
+
+#ifndef NO_REDIRECTOR
+static pid_t redirector_pid = 0;
+static int redirector_read_fd = -1, redirector_write_fd = -1;
+#define REDIRECTOR_BUFFER_SIZE 1024
+static char *redirector_buffer = NULL;
+RedirectRequestPtr redirector_request_first = NULL,
+    redirector_request_last = NULL;
+#endif
+
+static int atomSetterForbidden(ConfigVariablePtr, void*);
+
+void
+preinitForbidden(void)
+{
+    CONFIG_VARIABLE_SETTABLE(forbiddenUrl, CONFIG_ATOM, configAtomSetter,
+                             "URL to which forbidden requests "
+                             "should be redirected.");
+    CONFIG_VARIABLE_SETTABLE(forbiddenRedirectCode, CONFIG_INT,
+                             configIntSetter,
+                             "Redirect code, 301 or 302.");
+    CONFIG_VARIABLE_SETTABLE(forbiddenFile, CONFIG_ATOM, atomSetterForbidden,
+                             "File specifying forbidden URLs.");
+#ifndef NO_REDIRECTOR
+    CONFIG_VARIABLE_SETTABLE(redirector, CONFIG_ATOM, atomSetterForbidden,
+                             "Squid-style redirector.");
+    CONFIG_VARIABLE_SETTABLE(redirectorRedirectCode, CONFIG_INT,
+                             configIntSetter,
+                             "Redirect code to use with redirector.");
+#endif
+    CONFIG_VARIABLE_SETTABLE(uncachableFile, CONFIG_ATOM, atomSetterForbidden,
+                             "File specifying uncachable URLs.");
+
+    CONFIG_VARIABLE_SETTABLE(forbiddenTunnelsFile, CONFIG_ATOM, atomSetterForbidden,
+                             "File specifying forbidden tunnels.");
+}
+
+static int
+atomSetterForbidden(ConfigVariablePtr var, void *value)
+{
+    initForbidden();
+    return configAtomSetter(var, value);
+}
+
+int
+readDomainFile(char *filename)
+{
+    FILE *in;
+    char buf[512];
+    char *rs;
+    int i, j, is_regex, start;
+
+    in = fopen(filename, "r");
+    if(in == NULL) {
+        if(errno != ENOENT)
+            do_log_error(L_ERROR, errno, "Couldn't open file %s", filename);
+        return -1;
+    }
+
+    while(1) {
+        rs = fgets(buf, 512, in);
+        if(rs == NULL)
+            break;
+        for(i = 0; i < 512; i++) {
+            if(buf[i] != ' ' && buf[i] != '\t')
+                break;
+        }
+        start = i;
+        for(i = start; i < 512; i++) {
+            if(buf[i] == '#' || buf[i] == '\r' || buf[i] == '\n')
+                break;
+        }
+        while(i > start) {
+            if(buf[i - 1] != ' ' && buf[i - 1] != '\t')
+                break;
+            i--;
+        }
+
+        if(i <= start)
+            continue;
+
+        /* The significant part of the line is now between start and i */
+
+        is_regex = 0;
+        for(j = start; j < i; j++) {
+            if(buf[j] == '\\' || buf[j] == '*' || buf[j] == '/') {
+                is_regex = 1;
+                break;
+            }
+        }
+
+        if(is_regex) {
+            while(rlen + i - start + 8 >= rsize) {
+                char *new_regexbuf;
+                new_regexbuf = realloc(regexbuf, rsize * 2 + 1);
+                if(new_regexbuf == NULL) {
+                    do_log(L_ERROR, "Couldn't reallocate regex.\n");
+                    fclose(in);
+                    return -1;
+                }
+                regexbuf = new_regexbuf;
+                rsize = rsize * 2 + 1;
+            }
+            if(rlen != 0)
+                rlen = snnprintf(regexbuf, rlen, rsize, "|");
+            rlen = snnprintf(regexbuf, rlen, rsize, "(");
+            rlen = snnprint_n(regexbuf, rlen, rsize, buf + start, i - start);
+            rlen = snnprintf(regexbuf, rlen, rsize, ")");
+        } else {
+            DomainPtr new_domain;
+            if(dlen >= dsize - 1) {
+                DomainPtr *new_domains;
+                new_domains = realloc(domains, (dsize * 2 + 1) *
+                                      sizeof(DomainPtr));
+                if(new_domains == NULL) {
+                    do_log(L_ERROR,
+                           "Couldn't reallocate domain list.\n");
+                    fclose(in);
+                    return -1;
+                }
+                domains = new_domains;
+                dsize = dsize * 2 + 1;
+            }
+            new_domain = malloc(sizeof(DomainRec) - 1 + i - start);
+            if(new_domain == NULL) {
+                do_log(L_ERROR, "Couldn't allocate domain.\n");
+                fclose(in);
+                return -1;
+            }
+            new_domain->length = i - start;
+            memcpy(new_domain->domain, buf + start, i - start);
+            domains[dlen++] = new_domain;
+        }
+    }
+    fclose(in);
+    return 1;
+}
+
+void
+parseDomainFile(AtomPtr file,
+                DomainPtr **domains_return, regex_t **regex_return)
+{
+    struct stat ss;
+    regex_t *regex;
+    int rc;
+
+    if(*domains_return) {
+        DomainPtr *domain = *domains_return;
+        while(*domain) {
+            free(*domain);
+            domain++;
+        }
+        free(*domains_return);
+        *domains_return = NULL;
+    }
+
+    if(*regex_return) {
+        regfree(*regex_return);
+        *regex_return = NULL;
+    }
+
+    if(!file || file->length == 0)
+        return;
+
+    domains = malloc(64 * sizeof(DomainPtr));
+    if(domains == NULL) {
+        do_log(L_ERROR, "Couldn't allocate domain list.\n");
+        return;
+    }
+    dlen = 0;
+    dsize = 64;
+
+    regexbuf = malloc(512);
+    if(regexbuf == NULL) {
+        do_log(L_ERROR, "Couldn't allocate regex.\n");
+        free(domains);
+        return;
+    }
+    rlen = 0;
+    rsize = 512;
+
+    rc = stat(file->string, &ss);
+    if(rc < 0) {
+        if(errno != ENOENT)
+            do_log_error(L_WARN, errno, "Couldn't stat file %s", file->string);
+    } else {
+        if(!S_ISDIR(ss.st_mode))
+            readDomainFile(file->string);
+        else {
+            char *fts_argv[2];
+            FTS *fts;
+            FTSENT *fe;
+            fts_argv[0] = file->string;
+            fts_argv[1] = NULL;
+            fts = fts_open(fts_argv, FTS_LOGICAL, NULL);
+            if(fts) {
+                while(1) {
+                    fe = fts_read(fts);
+                    if(!fe) break;
+                    if(fe->fts_info != FTS_D && fe->fts_info != FTS_DP &&
+                       fe->fts_info != FTS_DC && fe->fts_info != FTS_DNR)
+                        readDomainFile(fe->fts_accpath);
+                }
+                fts_close(fts);
+            } else {
+                do_log_error(L_ERROR, errno,
+                             "Couldn't scan directory %s", file->string);
+            }
+        }
+    }
+
+    if(dlen > 0) {
+        domains[dlen] = NULL;
+    } else {
+        free(domains);
+        domains = NULL;
+    }
+
+    if(rlen > 0) {
+        regex = malloc(sizeof(regex_t));
+        rc = regcomp(regex, regexbuf, REG_EXTENDED | REG_NOSUB);
+        if(rc != 0) {
+            char errbuf[100];
+            regerror(rc, regex, errbuf, 100);
+            do_log(L_ERROR, "Couldn't compile regex: %s.\n", errbuf);
+            free(regex);
+            regex = NULL;
+        }
+    } else {
+        regex = NULL;
+    }
+    free(regexbuf);
+
+    *domains_return = domains;
+    *regex_return = regex;
+
+    return;
+}
+
+void
+initForbidden(void)
+{
+    redirectorKill();
+
+    if(forbiddenFile)
+        forbiddenFile = expandTilde(forbiddenFile);
+
+    if(forbiddenFile == NULL) {
+        forbiddenFile = expandTilde(internAtom("~/.polipo-forbidden"));
+        if(forbiddenFile) {
+            if(access(forbiddenFile->string, F_OK) < 0) {
+                releaseAtom(forbiddenFile);
+                forbiddenFile = NULL;
+            }
+        }
+    }
+
+    if(forbiddenFile == NULL) {
+        if(access("/etc/polipo/forbidden", F_OK) >= 0)
+            forbiddenFile = internAtom("/etc/polipo/forbidden");
+    }
+
+    parseDomainFile(forbiddenFile, &forbiddenDomains, &forbiddenRegex);
+
+
+    if(uncachableFile)
+        uncachableFile = expandTilde(uncachableFile);
+
+    if(uncachableFile == NULL) {
+        uncachableFile = expandTilde(internAtom("~/.polipo-uncachable"));
+        if(uncachableFile) {
+            if(access(uncachableFile->string, F_OK) < 0) {
+                releaseAtom(uncachableFile);
+                uncachableFile = NULL;
+            }
+        }
+    }
+
+    if(uncachableFile == NULL) {
+        if(access("/etc/polipo/uncachable", F_OK) >= 0)
+            uncachableFile = internAtom("/etc/polipo/uncachable");
+    }
+
+    parseDomainFile(uncachableFile, &uncachableDomains, &uncachableRegex);
+
+    if(forbiddenTunnelsFile)
+        forbiddenTunnelsFile = expandTilde(forbiddenTunnelsFile);
+    
+    if(forbiddenTunnelsFile == NULL) {
+        forbiddenTunnelsFile = expandTilde(internAtom("~/.polipo-forbiddenTunnels"));
+        if(forbiddenTunnelsFile) {
+            if(access(forbiddenTunnelsFile->string, F_OK) < 0) {
+                releaseAtom(forbiddenTunnelsFile);
+                forbiddenTunnelsFile = NULL;
+            }
+        }
+    }
+    
+    if(forbiddenTunnelsFile == NULL) {
+        if(access("/etc/polipo/forbiddenTunnels", F_OK) >= 0)
+            forbiddenTunnelsFile = internAtom("/etc/polipo/forbiddenTunnels");
+    }
+    
+    parseDomainFile(forbiddenTunnelsFile, &forbiddenTunnelsDomains, &forbiddenTunnelsRegex);
+    
+    return;
+}
+
+int
+tunnelIsMatched(char *url, int lurl, char *hostname, int lhost)
+{
+    DomainPtr *domain, *domains;
+    
+    domains=forbiddenTunnelsDomains;
+    if (domains) {
+	domain = domains;
+	while(*domain) {
+	    if (lhost == (*domain)->length && 
+		memcmp(hostname, (*domain)->domain, lhost)==0)
+		return 1;
+	    domain++;
+	}
+    }
+
+    if(forbiddenTunnelsRegex) {
+	if(!regexec(forbiddenTunnelsRegex, url, 0, NULL, 0))
+	    return 1;
+    }
+    return 0;
+}
+
+int
+urlIsMatched(char *url, int length, DomainPtr *domains, regex_t *regex)
+{
+    /* This requires url to be NUL-terminated. */
+    assert(url[length] == '\0');
+
+    if(length < 8)
+        return 0;
+
+    if(lwrcmp(url, "http://", 7) != 0)
+        return 0;
+
+    if(domains) {
+        int i;
+        DomainPtr *domain;
+        for(i = 8; i < length; i++) {
+            if(url[i] == '/')
+                break;
+        }
+        domain = domains;
+        while(*domain) {
+            if((*domain)->length <= (i - 7) &&
+               (url[i - (*domain)->length - 1] == '.' ||
+                url[i - (*domain)->length - 1] == '/') &&
+               memcmp(url + i - (*domain)->length,
+                      (*domain)->domain,
+                      (*domain)->length) == 0)
+                return 1;
+            domain++;
+        }
+    }
+
+    if(regex)
+        return !regexec(regex, url, 0, NULL, 0);
+
+    return 0;
+}
+
+int
+urlIsUncachable(char *url, int length)
+{
+    return urlIsMatched(url, length, uncachableDomains, uncachableRegex);
+}
+
+int
+urlForbidden(AtomPtr url,
+             int (*handler)(int, AtomPtr, AtomPtr, AtomPtr, void*),
+             void *closure)
+{
+    int forbidden = urlIsMatched(url->string, url->length,
+                                 forbiddenDomains, forbiddenRegex);
+    int code = 0;
+    AtomPtr message = NULL, headers = NULL;
+
+
+    if(forbidden) {
+        message = internAtomF("Forbidden URL %s", url->string);
+        if(forbiddenUrl) {
+            code = forbiddenRedirectCode;
+            headers = internAtomF("\r\nLocation: %s", forbiddenUrl->string);
+        } else {
+            code = 403;
+        }
+    }
+
+#ifndef NO_REDIRECTOR
+    if(code == 0 && redirector) {
+        RedirectRequestPtr request;
+        request = malloc(sizeof(RedirectRequestRec));
+        if(request == NULL) {
+            do_log(L_ERROR, "Couldn't allocate redirect request.\n");
+            goto done;
+        }
+        request->url = url;
+        request->handler = handler;
+        request->data = closure;
+        if(redirector_request_first == NULL)
+            redirector_request_first = request;
+        else
+            redirector_request_last->next = request;
+        redirector_request_last = request;
+        request->next = NULL;
+        if(request == redirector_request_first)
+            redirectorTrigger();
+        return 1;
+    }
+
+#endif
+
+ done:
+    handler(code, url, message, headers, closure);
+    return 1;
+}
+
+#ifndef NO_REDIRECTOR
+static void
+logExitStatus(int status)
+{
+    if(WIFEXITED(status) && WEXITSTATUS(status) == 142)
+        /* See child code in runRedirector */
+        do_log(L_ERROR, "Couldn't start redirector.\n");
+    else {
+        char *reason =
+            WIFEXITED(status) ? "with status" :
+            WIFSIGNALED(status) ? "on signal" :
+            "with unknown status";
+        int value =
+            WIFEXITED(status) ? WEXITSTATUS(status) :
+            WIFSIGNALED(status) ? WTERMSIG(status) :
+            status;
+        do_log(L_ERROR,
+               "Redirector exited %s %d.\n", reason, value);
+    }
+}
+
+void
+redirectorKill(void)
+{
+    int rc, status, dead;
+
+    if(redirector_read_fd >= 0) {
+        rc = waitpid(redirector_pid, &status, WNOHANG);
+        dead = (rc > 0);
+        close(redirector_read_fd);
+        redirector_read_fd = -1;
+        close(redirector_write_fd);
+        redirector_write_fd = -1;
+        if(!dead) {
+            rc = kill(redirector_pid, SIGTERM);
+            if(rc < 0 && errno != ESRCH) {
+                do_log_error(L_ERROR, errno, "Couldn't kill redirector");
+                redirector_pid = -1;
+                return;
+            }
+            do {
+                rc = waitpid(redirector_pid, &status, 0);
+            } while(rc < 0 && errno == EINTR);
+            if(rc < 0)
+                do_log_error(L_ERROR, errno,
+                             "Couldn't wait for redirector's death");
+        } else
+            logExitStatus(status);
+        redirector_pid = -1;
+    }
+}
+
+static void
+redirectorDestroyRequest(RedirectRequestPtr request)
+{
+    assert(redirector_request_first == request);
+    redirector_request_first = request->next;
+    if(redirector_request_first == NULL)
+        redirector_request_last = NULL;
+    free(request);
+}
+
+void
+redirectorTrigger(void)
+{
+    RedirectRequestPtr request = redirector_request_first;
+    int rc;
+
+    if(!request)
+        return;
+
+    if(redirector_read_fd < 0) {
+        rc = runRedirector(&redirector_pid,
+                           &redirector_read_fd, &redirector_write_fd);
+        if(rc < 0) {
+            request->handler(rc, request->url, NULL, NULL, request->data);
+            redirectorDestroyRequest(request);
+            return;
+        }
+    }
+    do_stream_2(IO_WRITE, redirector_write_fd, 0,
+                request->url->string, request->url->length,
+                "\n", 1,
+                redirectorStreamHandler1, request);
+}
+
+int
+redirectorStreamHandler1(int status,
+                         FdEventHandlerPtr event,
+                         StreamRequestPtr srequest)
+{
+    RedirectRequestPtr request = (RedirectRequestPtr)srequest->data;
+
+    if(status) {
+        if(status >= 0)
+            status = -EPIPE;
+        do_log_error(L_ERROR, -status, "Write to redirector failed");
+        goto fail;
+    }
+
+    if(!streamRequestDone(srequest))
+        return 0;
+
+    do_stream(IO_READ, redirector_read_fd, 0,
+              redirector_buffer, REDIRECTOR_BUFFER_SIZE,
+              redirectorStreamHandler2, request);
+    return 1;
+
+ fail:
+    request->handler(status < 0 ? status : -EPIPE,
+                     request->url, NULL, NULL, request->data);
+    redirectorDestroyRequest(request);
+    redirectorKill();
+    return 1;
+}
+
+int
+redirectorStreamHandler2(int status,
+                         FdEventHandlerPtr event,
+                         StreamRequestPtr srequest)
+{
+    RedirectRequestPtr request = (RedirectRequestPtr)srequest->data;
+    char *c;
+    AtomPtr message;
+    AtomPtr headers;
+    int code;
+
+    if(status < 0) {
+        do_log_error(L_ERROR, -status, "Read from redirector failed");
+        request->handler(status, request->url, NULL, NULL, request->data);
+        goto kill;
+    }
+    c = memchr(redirector_buffer, '\n', srequest->offset);
+    if(!c) {
+        if(!status && srequest->offset < REDIRECTOR_BUFFER_SIZE)
+            return 0;
+        do_log(L_ERROR, "Redirector returned incomplete reply.\n");
+        request->handler(-EREDIRECTOR, request->url, NULL, NULL, request->data);
+        goto kill;
+    }
+    *c = '\0';
+
+    if(srequest->offset > c + 1 - redirector_buffer)
+        do_log(L_WARN, "Stray bytes in redirector output.\n");
+
+    if(c > redirector_buffer + 1 &&
+       (c - redirector_buffer != request->url->length ||
+        memcmp(redirector_buffer, request->url->string,
+               request->url->length) != 0)) {
+        code = redirectorRedirectCode;
+        message = internAtom("Redirected by external redirector");
+        if(message == NULL) {
+            request->handler(-ENOMEM, request->url, NULL, NULL, request->data);
+            goto kill;
+        }
+
+        headers = internAtomF("\r\nLocation: %s", redirector_buffer);
+        if(headers == NULL) {
+            releaseAtom(message);
+            request->handler(-ENOMEM, request->url, NULL, NULL, request->data);
+            goto kill;
+        }
+    } else {
+        code = 0;
+        message = NULL;
+        headers = NULL;
+    }
+    request->handler(code, request->url,
+                     message, headers, request->data);
+    goto cont;
+
+ cont:
+    redirectorDestroyRequest(request);
+    redirectorTrigger();
+    return 1;
+
+ kill:
+    redirectorKill();
+    goto cont;
+}
+
+int
+runRedirector(pid_t *pid_return, int *read_fd_return, int *write_fd_return)
+{
+    int rc, rc2, status;
+    pid_t pid;
+    int filedes1[2], filedes2[2];
+    sigset_t ss, old_mask;
+
+    assert(redirector);
+
+    if(redirector_buffer == NULL) {
+        redirector_buffer = malloc(REDIRECTOR_BUFFER_SIZE);
+        if(redirector_buffer == NULL)
+            return -errno;
+    }
+
+    rc = pipe(filedes1);
+    if(rc < 0) {
+        rc = -errno;
+        goto fail1;
+    }
+
+
+    rc = pipe(filedes2);
+    if(rc < 0) {
+        rc = -errno;
+        goto fail2;
+    }
+
+    fflush(stdout);
+    fflush(stderr);
+    flushLog();
+
+    interestingSignals(&ss);
+    do {
+        rc = sigprocmask(SIG_BLOCK, &ss, &old_mask);
+    } while (rc < 0 && errno == EINTR);
+    if(rc < 0) {
+        rc = -errno;
+        goto fail3;
+    }
+
+    pid = fork();
+    if(pid < 0) {
+        rc = -errno;
+        goto fail4;
+    }
+
+    if(pid > 0) {
+        do {
+            rc = sigprocmask(SIG_SETMASK, &old_mask, NULL);
+        } while(rc < 0 && errno == EINTR);
+
+        if(rc < 0) {
+            rc = -errno;
+            goto fail4;
+        }
+
+        rc = setNonblocking(filedes1[1], 1);
+        if(rc >= 0)
+            rc = setNonblocking(filedes2[0], 1);
+        if(rc < 0) {
+            rc = -errno;
+            goto fail4;
+        }
+
+        /* This is completely unnecesary -- if the redirector cannot be
+           started, redirectorStreamHandler1 will get EPIPE straight away --,
+           but it improves error messages somewhat. */
+        rc = waitpid(pid, &status, WNOHANG);
+        if(rc > 0) {
+            logExitStatus(status);
+            rc = -EREDIRECTOR;
+            goto fail4;
+        } else if(rc < 0) {
+            rc = -errno;
+            goto fail4;
+        }
+
+        *read_fd_return = filedes2[0];
+        *write_fd_return = filedes1[1];
+
+        *pid_return = pid;
+        /* This comes at the end so that the fail* labels can work */
+        close(filedes1[0]);
+        close(filedes2[1]);
+    } else {
+        close(filedes1[1]);
+        close(filedes2[0]);
+        uninitEvents();
+        do {
+            rc = sigprocmask(SIG_SETMASK, &old_mask, NULL);
+        } while (rc < 0 && errno == EINTR);
+        if(rc < 0)
+            exit(142);
+
+        if(filedes1[0] != 0)
+            dup2(filedes1[0], 0);
+        if(filedes2[1] != 1)
+            dup2(filedes2[1], 1);
+
+        execlp(redirector->string, redirector->string, (char*)NULL);
+        exit(142);
+        /* NOTREACHED */
+    }
+    return 1;
+
+ fail4:
+    do {
+        rc2 = sigprocmask(SIG_SETMASK, &old_mask, NULL);
+    } while(rc2 < 0 && errno == EINTR);
+ fail3:
+    close(filedes2[0]);
+    close(filedes2[1]);
+ fail2:
+    close(filedes1[0]);
+    close(filedes1[1]);
+ fail1:
+    free(redirector_buffer);
+    redirector_buffer = NULL;
+    return rc;
+}
+
+#else
+
+void
+redirectorKill(void)
+{
+    return;
+}
+
+#endif
+
+#else
+
+void
+preinitForbidden()
+{
+    return;
+}
+
+void
+initForbidden()
+{
+    return;
+}
+
+int
+tunnelIsMatched(char *url, int lurl, char *hostname, int lhost)
+{
+    return 0;
+}
+
+int
+urlIsUncachable(char *url, int length)
+{
+    return 0;
+}
+
+int
+urlForbidden(AtomPtr url,
+             int (*handler)(int, AtomPtr, AtomPtr, AtomPtr, void*),
+             void *closure)
+{
+    handler(0, url, NULL, NULL, closure);
+    return 1;
+}
+
+#endif

+ 50 - 0
polipo/forbidden.h

@@ -0,0 +1,50 @@
+/*
+Copyright (c) 2003-2006 by Juliusz Chroboczek
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+extern AtomPtr forbiddenUrl;
+extern int forbiddenRedirectCode;
+
+typedef struct _RedirectRequest {
+    AtomPtr url;
+    struct _RedirectRequest *next;
+    int (*handler)(int, AtomPtr, AtomPtr, AtomPtr, void*);
+    void *data;
+} RedirectRequestRec, *RedirectRequestPtr;
+
+void preinitForbidden(void);
+void initForbidden(void);
+int urlIsUncachable(char *url, int length);
+int urlForbidden(AtomPtr url,
+                 int (*handler)(int, AtomPtr, AtomPtr, AtomPtr, void*),
+                 void *closure);
+void redirectorKill(void);
+int redirectorStreamHandler1(int status,
+                             FdEventHandlerPtr event,
+                             StreamRequestPtr srequest);
+int redirectorStreamHandler2(int status,
+                             FdEventHandlerPtr event,
+                             StreamRequestPtr srequest);
+void redirectorTrigger(void);
+int 
+runRedirector(pid_t *pid_return, int *read_fd_return, int *write_fd_return);
+
+int tunnelIsMatched(char *url, int lurl, char *hostname, int lhost);

+ 18 - 0
polipo/forbidden.sample

@@ -0,0 +1,18 @@
+# Sample forbidden URLs file for polipo.  -*-sh-*-
+# Put this in /etc/polipo/forbidden or in ~/.polipo-forbidden.
+
+# Forbid all hosts belonging to a given domain name:
+
+#counter.com
+#hitbox.com
+#doubleclick.net
+#www.cashcount.com
+
+# Forbid all hosts contaning a string matching a given regex.  Note
+# that you need to quote dots, so that a regex is not misinterpreted
+# as a domain name.
+
+#^http://[^/]*counter\.com
+#/ads/
+#/phpAdsNew
+#counting\.php

+ 365 - 0
polipo/fts_compat.c

@@ -0,0 +1,365 @@
+/*
+Copyright (c) 2003-2006 by Juliusz Chroboczek
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+/* This file implements just enough of the fts family of functions
+   to make Polipo happy. */
+
+#include <stdlib.h>
+#include <errno.h>
+#include <sys/types.h>
+#ifndef _WIN32
+#include <unistd.h>
+#include <dirent.h>
+#else
+#include "dirent_compat.h"
+#endif
+#include <sys/stat.h>
+#include <errno.h>
+#include <string.h>
+
+#include "fts_compat.h"
+
+static char *
+getcwd_a()
+{
+    char buf[256];
+    char *ret;
+    ret = getcwd(buf, 256);
+    if(ret == NULL)
+        return NULL;
+    return strdup(buf);
+}
+
+static char *
+mkfilename(const char *path, char *filename)
+{
+    int n = strlen(path);
+    char *buf = malloc(n + 1 + strlen(filename) + 1);
+    if(buf == NULL)
+        return NULL;
+    memcpy(buf, path, n);
+    if(buf[n - 1] != '/')
+        buf[n++] = '/';
+    strcpy(buf + n, filename);
+    return buf;
+}
+
+static int
+split(const char *path, int *slash_return, int *dlen, int *blen)
+{
+    int len; int slash;
+    len = strlen(path);
+    while(len > 0 && path[len - 1] == '/')
+        len--;
+    if(len == 0)
+        return -1;
+    slash = len - 1;
+    while(slash >= 0 && path[slash] != '/')
+        slash--;
+
+    if(slash_return) *slash_return = slash;
+    if(dlen) *dlen = slash + 1;
+    if(blen) *blen = len - slash - 1;
+    return 1;
+}
+
+static char *
+basename_a(const char *path)
+{
+    int blen, slash;
+    char *b;
+    int rc;
+
+    rc = split(path, &slash, NULL, &blen);
+    if(rc < 0 || blen == 0)
+        return NULL;
+
+    b = malloc(blen + 1);
+    if(b == NULL)
+        return NULL;
+    memcpy(b, path + slash + 1, blen);
+    b[blen] = '\0';
+    return b;
+}
+
+static char *
+dirname_a(const char *path)
+{
+    int dlen;
+    int rc;
+    char *d;
+
+    rc = split(path, NULL, &dlen, NULL);
+    if(rc < 0)
+        return NULL;
+
+    d = malloc(dlen + 1);
+    if(d == NULL)
+        return NULL;
+    memcpy(d, path, dlen);
+    d[dlen] = '\0';
+    return d;
+}
+
+#if defined(__svr4__) || defined(SVR4)
+static int
+dirfd(DIR *dir)
+{
+    return dir->dd_fd;
+}
+#endif
+
+/*
+ * Make the directory identified by the argument the current directory.
+ */
+#ifdef WIN32 /*MINGW*/
+int
+change_to_dir(DIR *dir)
+{
+    errno = ENOSYS;
+    return -1;
+}
+#else
+int
+change_to_dir(DIR *dir)
+{
+    return fchdir(dirfd(dir));
+}
+#endif
+
+FTS*
+fts_open(char * const *path_argv, int options,
+         int (*compar)(const FTSENT **, const FTSENT **))
+{
+    FTS *fts;
+    DIR *dir;
+    char *cwd;
+    int rc;
+
+    if(options != FTS_LOGICAL || compar != NULL || path_argv[1] != NULL) {
+        errno = ENOSYS;
+        return NULL;
+    }
+
+    dir = opendir(path_argv[0]);
+    if(dir == NULL)
+        return NULL;
+
+    fts = calloc(sizeof(FTS), 1);
+    if(fts == NULL) {
+        int save = errno;
+        closedir(dir);
+        errno = save;
+        return NULL;
+    }
+
+    cwd = getcwd_a();
+    if(cwd == NULL) {
+        int save = errno;
+        free(fts);
+        closedir(dir);
+        errno = save;
+        return NULL;
+    }
+
+    rc = change_to_dir(dir);
+    if(rc < 0) {
+        int save = errno;
+        free(cwd);
+        free(fts);
+        closedir(dir);
+        errno = save;
+        return NULL;
+    }
+
+    fts->depth = 0;
+    fts->dir[0] = dir;
+    fts->cwd0 = cwd;
+    fts->cwd = strdup(path_argv[0]);
+    return fts;
+}
+
+int
+fts_close(FTS *fts)
+{
+    int save = 0;
+    int rc;
+
+    if(fts->ftsent.fts_path) {
+        free(fts->ftsent.fts_path);
+        fts->ftsent.fts_path = NULL;
+    }
+
+    if(fts->dname) {
+        free(fts->dname);
+        fts->dname = NULL;
+    }
+
+    rc = chdir(fts->cwd0);
+    if(rc < 0)
+        save = errno;
+
+    while(fts->depth >= 0) {
+        closedir(fts->dir[fts->depth]);
+        fts->depth--;
+    }
+
+    free(fts->cwd0);
+    if(fts->cwd) free(fts->cwd);
+    free(fts);
+
+    if(rc < 0) {
+        errno = save;
+        return -1;
+    }
+    return 0;
+}
+
+FTSENT *
+fts_read(FTS *fts)
+{
+    struct dirent *dirent;
+    int rc;
+    char *name;
+    char buf[1024];
+
+    if(fts->ftsent.fts_path) {
+        free(fts->ftsent.fts_path);
+        fts->ftsent.fts_path = NULL;
+    }
+
+    if(fts->dname) {
+        free(fts->dname);
+        fts->dname = NULL;
+    }
+
+ again:
+    dirent = readdir(fts->dir[fts->depth]);
+    if(dirent == NULL) {
+        char *newcwd = NULL;
+        closedir(fts->dir[fts->depth]);
+        fts->dir[fts->depth] = NULL;
+        fts->depth--;
+        if(fts->depth >= 0) {
+            fts->dname = basename_a(fts->cwd);
+            if(fts->dname == NULL)
+                goto error;
+            newcwd = dirname_a(fts->cwd);
+            if(newcwd == NULL)
+                goto error;
+        }
+        if(fts->cwd) free(fts->cwd);
+        fts->cwd = NULL;
+        if(fts->depth < 0)
+            return NULL;
+        rc = change_to_dir(fts->dir[fts->depth]);
+        if(rc < 0) {
+            free(newcwd);
+            goto error;
+        }
+        fts->cwd = newcwd;
+        name = fts->dname;
+        fts->ftsent.fts_info = FTS_DP;
+        goto done;
+    }
+
+    name = dirent->d_name;
+
+ again2:
+    rc = stat(name, &fts->ftstat);
+    if(rc < 0) {
+        fts->ftsent.fts_info = FTS_NS;
+        goto error2;
+    }
+
+    if(S_ISDIR(fts->ftstat.st_mode)) {
+        char *newcwd;
+        DIR *dir;
+
+        if(strcmp(name, ".") == 0 || strcmp(name, "..") == 0)
+            goto again;
+
+        if(fts->depth >= FTS_MAX_DEPTH) {
+            errno = ENFILE;
+            goto error;
+        }
+        dir = opendir(dirent->d_name);
+        if(dir == NULL) {
+            if(errno == EACCES) {
+                fts->ftsent.fts_info = FTS_DNR;
+                goto error2;
+            } else
+                goto error;
+        }
+        newcwd = mkfilename(fts->cwd, dirent->d_name);
+        rc = change_to_dir(dir);
+        if(rc < 0) {
+            free(newcwd);
+            goto error;
+        }
+        free(fts->cwd);
+        fts->cwd = newcwd;
+        fts->ftsent.fts_info = FTS_D;
+        fts->depth++;
+        fts->dir[fts->depth] = dir;
+        goto done;
+    } else if(S_ISREG(fts->ftstat.st_mode)) {
+        fts->ftsent.fts_info = FTS_F;
+        goto done;
+#ifdef S_ISLNK
+    } else if(S_ISLNK(fts->ftstat.st_mode)) {
+        int rc;
+        rc = readlink(name, buf, 1024);
+        if(rc < 0)
+            goto error;
+        if(rc >= 1023) {
+            errno = ENAMETOOLONG;
+            goto error;
+        }
+        buf[rc] = '\0';
+        name = buf;
+        if(access(buf, F_OK) >= 0)
+            goto again2;
+        fts->ftsent.fts_info = FTS_SLNONE;
+        goto done;
+#endif
+    } else {
+        fts->ftsent.fts_info = FTS_DEFAULT;
+        goto done;
+    }
+    
+ done:
+    if(fts->cwd == NULL)
+        fts->cwd = getcwd_a();
+    if(fts->cwd == NULL) goto error;
+    fts->ftsent.fts_path = mkfilename(fts->cwd, name);
+    if(fts->ftsent.fts_path == NULL) goto error;
+    fts->ftsent.fts_accpath = name;
+    fts->ftsent.fts_statp = &fts->ftstat;
+    return &fts->ftsent;
+
+ error:
+    fts->ftsent.fts_info = FTS_ERR;
+ error2:
+    fts->ftsent.fts_errno = errno;
+    return &fts->ftsent;
+}

+ 69 - 0
polipo/fts_compat.h

@@ -0,0 +1,69 @@
+/*
+Copyright (c) 2003-2006 by Juliusz Chroboczek
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+#ifndef _FTS_COMPAT_H
+#define _FTS_COMPAT_H
+
+#ifndef FTS_MAX_DEPTH
+#define FTS_MAX_DEPTH 4
+#endif
+
+#define FTS_LOGICAL 1
+
+#define FTS_F 1
+#define FTS_D 2
+#define FTS_DP 3
+#define FTS_DC 4
+#define FTS_NS 5
+#define FTS_NSOK 6
+#define FTS_DNR 7
+#define FTS_SLNONE 8
+#define FTS_DEFAULT 9
+#define FTS_ERR 10
+
+struct _FTSENT {
+    unsigned short fts_info;
+    char *fts_path;
+    char *fts_accpath;
+    struct stat *fts_statp;
+    int fts_errno;
+};
+
+typedef struct _FTSENT FTSENT;
+
+struct _FTS {
+    int depth;
+    DIR *dir[FTS_MAX_DEPTH];
+    char *cwd0, *cwd;
+    struct _FTSENT ftsent;
+    struct stat ftstat;
+    char *dname;
+};
+
+typedef struct _FTS FTS;
+
+FTS* fts_open(char * const *path_argv, int options,
+              int (*compar)(const FTSENT **, const FTSENT **));
+int fts_close(FTS *fts);
+FTSENT * fts_read(FTS *fts);
+
+#endif

+ 12 - 0
polipo/ftsimport.c

@@ -0,0 +1,12 @@
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE
+#endif
+
+#include "polipo.h"
+
+#ifndef HAVE_FTS
+#include "fts_compat.c"
+#else
+static int dummy ATTRIBUTE((unused));
+#endif
+

+ 6 - 0
polipo/ftsimport.h

@@ -0,0 +1,6 @@
+#ifdef HAVE_FTS
+#include <fts.h>
+#else
+#include "fts_compat.h"
+#endif
+

+ 1094 - 0
polipo/http.c

@@ -0,0 +1,1094 @@
+/*
+Copyright (c) 2003-2006 by Juliusz Chroboczek
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+#include "polipo.h"
+
+int disableProxy = 0;
+AtomPtr proxyName = NULL;
+int proxyPort = 8123;
+
+int clientTimeout = 120;
+int serverTimeout = 90;
+int serverIdleTimeout = 45;
+
+int bigBufferSize = (32 * 1024);
+
+AtomPtr displayName = NULL;
+
+AtomPtr authRealm = NULL;
+AtomPtr authCredentials = NULL;
+
+AtomPtr parentAuthCredentials = NULL;
+
+AtomListPtr allowedClients = NULL;
+NetAddressPtr allowedNets = NULL;
+
+IntListPtr allowedPorts = NULL;
+IntListPtr tunnelAllowedPorts = NULL;
+int expectContinue = 1;
+int dontTrustVaryETag = 1;
+
+AtomPtr atom100Continue;
+
+int disableVia = 1;
+
+/* 0 means that all failures lead to errors.  1 means that failures to
+   connect are reported in a Warning header when stale objects are
+   served.  2 means that only missing data is fetched from the net,
+   stale data is served without revalidation (browser-side
+   Cache-Control directives are still honoured).  3 means that no
+   connections are ever attempted. */
+
+int proxyOffline = 0;
+int relaxTransparency = 0;
+AtomPtr proxyAddress = NULL;
+
+static int timeoutSetter(ConfigVariablePtr var, void *value);
+
+void
+preinitHttp()
+{
+    proxyAddress = internAtom("127.0.0.1");
+    CONFIG_VARIABLE_SETTABLE(disableProxy, CONFIG_BOOLEAN, configIntSetter,
+                             "Whether to be a web server only.");
+    CONFIG_VARIABLE_SETTABLE(proxyOffline, CONFIG_BOOLEAN, configIntSetter,
+                             "Avoid contacting remote servers.");
+    CONFIG_VARIABLE_SETTABLE(relaxTransparency, CONFIG_TRISTATE, 
+                             configIntSetter,
+                             "Avoid contacting remote servers.");
+    CONFIG_VARIABLE(proxyPort, CONFIG_INT,
+                    "The TCP port on which the proxy listens.");
+    CONFIG_VARIABLE(proxyAddress, CONFIG_ATOM_LOWER,
+                    "The IP address on which the proxy listens.");
+    CONFIG_VARIABLE_SETTABLE(proxyName, CONFIG_ATOM_LOWER, configAtomSetter,
+                             "The name by which the proxy is known.");
+    CONFIG_VARIABLE_SETTABLE(clientTimeout, CONFIG_TIME, 
+                             timeoutSetter, "Client-side timeout.");
+    CONFIG_VARIABLE_SETTABLE(serverTimeout, CONFIG_TIME,
+                             timeoutSetter, "Server-side timeout.");
+    CONFIG_VARIABLE_SETTABLE(serverIdleTimeout, CONFIG_TIME,
+                             timeoutSetter, "Server-side idle timeout.");
+    CONFIG_VARIABLE(authRealm, CONFIG_ATOM,
+                    "Authentication realm.");
+    CONFIG_VARIABLE(displayName, CONFIG_ATOM,
+                    "Server name displayed on error pages.");
+    CONFIG_VARIABLE(authCredentials, CONFIG_PASSWORD,
+                    "username:password.");
+    CONFIG_VARIABLE(parentAuthCredentials, CONFIG_PASSWORD,
+                    "username:password.");
+    CONFIG_VARIABLE(allowedClients, CONFIG_ATOM_LIST_LOWER,
+                    "Networks from which clients are allowed to connect.");
+    CONFIG_VARIABLE(tunnelAllowedPorts, CONFIG_INT_LIST,
+                    "Ports to which tunnelled connections are allowed.");
+    CONFIG_VARIABLE(allowedPorts, CONFIG_INT_LIST,
+                    "Ports to which connections are allowed.");
+    CONFIG_VARIABLE(expectContinue, CONFIG_TRISTATE,
+                    "Send Expect-Continue to servers.");
+    CONFIG_VARIABLE(bigBufferSize, CONFIG_INT,
+                    "Size of big buffers (max size of headers).");
+    CONFIG_VARIABLE_SETTABLE(disableVia, CONFIG_BOOLEAN, configIntSetter,
+                             "Don't use Via headers.");
+    CONFIG_VARIABLE(dontTrustVaryETag, CONFIG_TRISTATE,
+                    "Whether to trust the ETag when there's Vary.");
+    preinitHttpParser();
+}
+
+static int
+timeoutSetter(ConfigVariablePtr var, void *value)
+{
+    configIntSetter(var, value);
+    if(clientTimeout <= serverTimeout)
+        clientTimeout = serverTimeout + 1;
+    return 1;
+}
+
+void
+initHttp()
+{
+    char *buf = NULL;
+    int namelen;
+    int n;
+    struct hostent *host;
+
+    initHttpParser();
+
+    atom100Continue = internAtom("100-continue");
+
+    if(clientTimeout <= serverTimeout) {
+        clientTimeout = serverTimeout + 1;
+        do_log(L_WARN, "Value of clientTimeout too small -- setting to %d.\n",
+               clientTimeout);
+    }
+
+    if(displayName == NULL)
+        displayName = internAtom("Polipo");
+
+    if(authCredentials != NULL && authRealm == NULL)
+        authRealm = internAtom("Polipo");
+
+    if(allowedClients) {
+        allowedNets = parseNetAddress(allowedClients);
+        if(allowedNets == NULL)
+            exit(1);
+    }
+
+    if(allowedPorts == NULL) {
+        allowedPorts = makeIntList(0);
+        if(allowedPorts == NULL) {
+            do_log(L_ERROR, "Couldn't allocate allowedPorts.\n");
+            exit(1);
+        }
+        intListCons(80, 100, allowedPorts);
+        intListCons(1024, 0xFFFF, allowedPorts);
+    }
+
+    if(tunnelAllowedPorts == NULL) {
+        tunnelAllowedPorts = makeIntList(0);
+        if(tunnelAllowedPorts == NULL) {
+            do_log(L_ERROR, "Couldn't allocate tunnelAllowedPorts.\n");
+            exit(1);
+        }
+        intListCons(22, 22, tunnelAllowedPorts);   /* ssh */
+        intListCons(80, 80, tunnelAllowedPorts);   /* HTTP */
+        intListCons(109, 110, tunnelAllowedPorts); /* POP 2 and 3*/
+        intListCons(143, 143, tunnelAllowedPorts); /* IMAP 2/4 */
+        intListCons(443, 443, tunnelAllowedPorts); /* HTTP/SSL */
+        intListCons(873, 873, tunnelAllowedPorts); /* rsync */
+        intListCons(993, 993, tunnelAllowedPorts); /* IMAP/SSL */
+        intListCons(995, 995, tunnelAllowedPorts); /* POP/SSL */
+        intListCons(2401, 2401, tunnelAllowedPorts); /* CVS */
+        intListCons(5222, 5223, tunnelAllowedPorts); /* Jabber */
+        intListCons(9418, 9418, tunnelAllowedPorts); /* Git */
+    }
+
+    if(proxyName)
+        return;
+
+    buf = get_chunk();
+    if(buf == NULL) {
+        do_log(L_ERROR, "Couldn't allocate chunk for host name.\n");
+        goto fail;
+    }
+
+    n = gethostname(buf, CHUNK_SIZE);
+    if(n != 0) {
+        do_log_error(L_WARN, errno, "Gethostname");
+        strcpy(buf, "polipo");
+        goto success;
+    }
+    /* gethostname doesn't necessarily NUL-terminate on overflow */
+    buf[CHUNK_SIZE - 1] = '\0';
+
+    if(strcmp(buf, "(none)") == 0 ||
+       strcmp(buf, "localhost") == 0 ||
+       strcmp(buf, "localhost.localdomain") == 0) {
+        do_log(L_WARN, "Couldn't determine host name -- using ``polipo''.\n");
+        strcpy(buf, "polipo");
+        goto success;
+    }
+
+    if(strchr(buf, '.') != NULL)
+        goto success;
+
+    host = gethostbyname(buf);
+    if(host == NULL) {
+        goto success;
+    }
+
+    if(host->h_addrtype != AF_INET)
+        goto success;
+
+    host = gethostbyaddr(host->h_addr_list[0], host->h_length,  AF_INET);
+
+    if(!host || !host->h_name || strcmp(host->h_name, "localhost") == 0 ||
+       strcmp(host->h_name, "localhost.localdomain") == 0)
+        goto success;
+
+    namelen = strlen(host->h_name);
+    if(namelen >= CHUNK_SIZE) {
+        do_log(L_ERROR, "Host name too long.\n");
+        goto success;
+    }
+
+    memcpy(buf, host->h_name, namelen + 1);
+
+ success:
+    proxyName = internAtom(buf);
+    if(proxyName == NULL) {
+        do_log(L_ERROR, "Couldn't allocate proxy name.\n");
+        goto fail;
+    }
+    dispose_chunk(buf);
+    return;
+
+ fail:
+    if(buf)
+        dispose_chunk(buf);
+    exit(1);
+    return;
+}
+
+int
+httpSetTimeout(HTTPConnectionPtr connection, int secs)
+{
+    TimeEventHandlerPtr new;
+
+    if(connection->timeout)
+        cancelTimeEvent(connection->timeout);
+    connection->timeout = NULL;
+
+    if(secs > 0) {
+        new = scheduleTimeEvent(secs, httpTimeoutHandler,
+                                sizeof(connection), &connection);
+        if(!new) {
+            do_log(L_ERROR, "Couldn't schedule timeout for connection 0x%lx\n",
+                   (unsigned long)connection);
+            return -1;
+        }
+    } else {
+        new = NULL;
+    }
+
+    connection->timeout = new;
+    return 1;
+}
+
+int 
+httpTimeoutHandler(TimeEventHandlerPtr event)
+{
+    HTTPConnectionPtr connection = *(HTTPConnectionPtr*)event->data;
+
+    if(connection->fd >= 0) {
+        int rc;
+        rc = shutdown(connection->fd, 2);
+        if(rc < 0 && errno != ENOTCONN)
+                do_log_error(L_ERROR, errno, "Timeout: shutdown failed");
+        pokeFdEvent(connection->fd, -EDOTIMEOUT, POLLIN | POLLOUT);
+    }
+    connection->timeout = NULL;
+    return 1;
+}
+
+int
+httpWriteObjectHeaders(char *buf, int offset, int len,
+                       ObjectPtr object, int from, int to)
+{
+    int n = offset;
+    CacheControlRec cache_control;
+
+    cache_control.flags = object->cache_control;
+    cache_control.max_age = object->max_age;
+    cache_control.s_maxage = object->s_maxage;
+    cache_control.max_stale = -1;
+    cache_control.min_fresh = -1;
+
+    if(from <= 0 && to < 0) {
+        if(object->length >= 0) {
+            n = snnprintf(buf, n, len,
+                          "\r\nContent-Length: %d", object->length);
+        }
+    } else {
+        if(to >= 0) {
+            n = snnprintf(buf, n, len,
+                          "\r\nContent-Length: %d", to - from);
+        }
+    }
+
+    if(from > 0 || to > 0) {
+        if(object->length >= 0) {
+            if(from >= to) {
+                n = snnprintf(buf, n, len,
+                              "\r\nContent-Range: bytes */%d",
+                              object->length);
+            } else {
+                n = snnprintf(buf, n, len,
+                              "\r\nContent-Range: bytes %d-%d/%d",
+                              from, to - 1, 
+                              object->length);
+            }
+        } else {
+            if(to >= 0) {
+                n = snnprintf(buf, n, len,
+                              "\r\nContent-Range: bytes %d-/*",
+                              from);
+            } else {
+                n = snnprintf(buf, n, len,
+                              "\r\nContent-Range: bytes %d-%d/*",
+                              from, to);
+            }
+        }
+    }
+        
+    if(object->etag) {
+        n = snnprintf(buf, n, len, "\r\nETag: \"%s\"", object->etag);
+    }
+    if((object->flags & OBJECT_LOCAL) || object->date >= 0) {
+        n = snnprintf(buf, n, len, "\r\nDate: ");
+        n = format_time(buf, n, len, 
+                        (object->flags & OBJECT_LOCAL) ?
+                        current_time.tv_sec : object->date);
+        if(n < 0)
+            goto fail;
+    }
+
+    if(object->last_modified >= 0) {
+        n = snnprintf(buf, n, len, "\r\nLast-Modified: ");
+        n = format_time(buf, n, len, object->last_modified);
+        if(n < 0)
+            goto fail;
+    }
+
+    if(object->expires >= 0) {
+        n = snnprintf(buf, n, len, "\r\nExpires: ");
+        n = format_time(buf, n, len, object->expires);
+        if(n < 0)
+            goto fail;
+    }
+
+    n = httpPrintCacheControl(buf, n, len,
+                              object->cache_control, &cache_control);
+    if(n < 0)
+        goto fail;
+
+    if(!disableVia && object->via)
+        n = snnprintf(buf, n, len, "\r\nVia: %s", object->via->string);
+
+    if(object->headers)
+        n = snnprint_n(buf, n, len, object->headers->string,
+                       object->headers->length);
+
+    if(n < len)
+        return n;
+    else
+        return -1;
+
+ fail:
+    return -1;
+}
+
+static int
+cachePrintSeparator(char *buf, int offset, int len,
+                    int subsequent)
+{
+    int n = offset;
+    if(subsequent)
+        n = snnprintf(buf, offset, len, ", ");
+    else
+        n = snnprintf(buf, offset, len, "\r\nCache-Control: ");
+    return n;
+}
+
+int
+httpPrintCacheControl(char *buf, int offset, int len,
+                      int flags, CacheControlPtr cache_control)
+{
+    int n = offset;
+    int sub = 0;
+
+#define PRINT_SEP() \
+    do {\
+        n = cachePrintSeparator(buf, n, len, sub); \
+        sub = 1; \
+    } while(0)
+
+    if(cache_control)
+        flags |= cache_control->flags;
+
+    if(flags & CACHE_NO) {
+        PRINT_SEP();
+        n = snnprintf(buf, n, len, "no-cache");
+    }
+    if(flags & CACHE_PUBLIC) {
+        PRINT_SEP();
+        n = snnprintf(buf, n, len, "public");
+    }
+    if(flags & CACHE_PRIVATE) {
+        PRINT_SEP();
+        n = snnprintf(buf, n, len, "private");
+    }
+    if(flags & CACHE_NO_STORE) {
+        PRINT_SEP();
+        n = snnprintf(buf, n, len, "no-store");
+    }
+    if(flags & CACHE_NO_TRANSFORM) {
+        PRINT_SEP();
+        n = snnprintf(buf, n, len, "no-transform");
+    }
+    if(flags & CACHE_MUST_REVALIDATE) {
+        PRINT_SEP();
+        n = snnprintf(buf, n, len, "must-revalidate");
+    }
+    if(flags & CACHE_PROXY_REVALIDATE) {
+        PRINT_SEP();
+        n = snnprintf(buf, n, len, "proxy-revalidate");
+    }
+    if(flags & CACHE_ONLY_IF_CACHED) {
+        PRINT_SEP();
+        n = snnprintf(buf, n, len, "only-if-cached");
+    }
+    if(cache_control) {
+        if(cache_control->max_age >= 0) {
+            PRINT_SEP();
+            n = snnprintf(buf, n, len, "max-age=%d",
+                          cache_control->max_age);
+        }
+        if(cache_control->s_maxage >= 0) {
+            PRINT_SEP();
+            n = snnprintf(buf, n, len, "s-maxage=%d", 
+                          cache_control->s_maxage);
+        }
+        if(cache_control->min_fresh > 0) {
+            PRINT_SEP();
+            n = snnprintf(buf, n, len, "min-fresh=%d",
+                          cache_control->min_fresh);
+        }
+        if(cache_control->max_stale > 0) {
+            PRINT_SEP();
+            n = snnprintf(buf, n, len, "max-stale=%d",
+                          cache_control->min_fresh);
+        }
+    }
+    return n;
+#undef PRINT_SEP
+}
+
+char *
+httpMessage(int code)
+{
+    switch(code) {
+    case 200:
+        return "Okay";
+    case 206:
+        return "Partial content";
+    case 300:
+        return "Multiple choices";
+    case 301:
+        return "Moved permanently";
+    case 302:
+        return "Found";
+    case 303:
+        return "See other";
+    case 304:
+        return "Not changed";
+    case 307:
+        return "Temporary redirect";
+    case 401:
+        return "Authentication Required";
+    case 403:
+        return "Forbidden";
+    case 404:
+        return "Not found";
+    case 405:
+        return "Method not allowed";
+    case 407:
+        return "Proxy authentication required";
+    default:
+        return "Unknown error code";
+    }
+}
+
+int
+htmlString(char *buf, int n, int len, char *s, int slen)
+{
+    int i = 0;
+    while(i < slen && n + 5 < len) {
+        switch(s[i]) {
+        case '&':
+            buf[n++] = '&'; buf[n++] = 'a'; buf[n++] = 'm'; buf[n++] = 'p';
+            buf[n++] = ';';
+            break;
+        case '<':
+            buf[n++] = '&'; buf[n++] = 'l'; buf[n++] = 't'; buf[n++] = ';';
+            break;
+        case '>':
+            buf[n++] = '&'; buf[n++] = 'g'; buf[n++] = 't'; buf[n++] = ';';
+            break;
+        case '"':
+            buf[n++] = '&'; buf[n++] = 'q'; buf[n++] = 'u'; buf[n++] = 'o';
+            buf[n++] = 't'; buf[n++] = ';';
+            break;
+        case '\0':
+            break;
+        default:
+            buf[n++] = s[i];
+        }
+        i++;
+    }
+    return n;
+}
+
+void
+htmlPrint(FILE *out, char *s, int slen)
+{
+    int i;
+    for(i = 0; i < slen; i++) {
+        switch(s[i]) {
+        case '&':
+            fputs("&amp;", out);
+            break;
+        case '<':
+            fputs("&lt;", out);
+            break;
+        case '>':
+            fputs("&gt;", out);
+            break;
+        default:
+            fputc(s[i], out);
+        }
+    }
+}
+
+HTTPConnectionPtr
+httpMakeConnection()
+{
+    HTTPConnectionPtr connection;
+    connection = malloc(sizeof(HTTPConnectionRec));
+    if(connection == NULL)
+        return NULL;
+    connection->flags = 0;
+    connection->fd = -1;
+    connection->buf = NULL;
+    connection->len = 0;
+    connection->offset = 0;
+    connection->request = NULL;
+    connection->request_last = NULL;
+    connection->serviced = 0;
+    connection->version = HTTP_UNKNOWN;
+    connection->time = current_time.tv_sec;
+    connection->timeout = NULL;
+    connection->te = TE_IDENTITY;
+    connection->reqbuf = NULL;
+    connection->reqlen = 0;
+    connection->reqbegin = 0;
+    connection->reqoffset = 0;
+    connection->bodylen = -1;
+    connection->reqte = TE_IDENTITY;
+    connection->chunk_remaining = 0;
+    connection->server = NULL;
+    connection->pipelined = 0;
+    connection->connecting = 0;
+    connection->server = NULL;
+    return connection;
+}
+
+void
+httpDestroyConnection(HTTPConnectionPtr connection)
+{
+    assert(connection->flags == 0);
+    httpConnectionDestroyBuf(connection);
+    assert(!connection->request);
+    assert(!connection->request_last);
+    httpConnectionDestroyReqbuf(connection);
+    assert(!connection->timeout);
+    assert(!connection->server);
+    free(connection);
+}
+
+void
+httpConnectionDestroyBuf(HTTPConnectionPtr connection)
+{
+    if(connection->buf) {
+        if(connection->flags & CONN_BIGBUF)
+            free(connection->buf);
+        else
+            dispose_chunk(connection->buf);
+    }
+    connection->flags &= ~CONN_BIGBUF;
+    connection->buf = NULL;
+}
+
+void
+httpConnectionDestroyReqbuf(HTTPConnectionPtr connection)
+{
+    if(connection->reqbuf) {
+        if(connection->flags & CONN_BIGREQBUF)
+            free(connection->reqbuf);
+        else
+            dispose_chunk(connection->reqbuf);
+    }
+    connection->flags &= ~CONN_BIGREQBUF;
+    connection->reqbuf = NULL;
+}
+
+HTTPRequestPtr 
+httpMakeRequest()
+{
+    HTTPRequestPtr request;
+    request = malloc(sizeof(HTTPRequestRec));
+    if(request == NULL)
+        return NULL;
+    request->flags = 0;
+    request->connection = NULL;
+    request->object = NULL;
+    request->method = METHOD_UNKNOWN;
+    request->from = 0;
+    request->to = -1;
+    request->cache_control = no_cache_control;
+    request->condition = NULL;
+    request->via = NULL;
+    request->chandler = NULL;
+    request->can_mutate = NULL;
+    request->error_code = 0;
+    request->error_message = NULL;
+    request->error_headers = NULL;
+    request->headers = NULL;
+    request->time0 = null_time;
+    request->time1 = null_time;
+    request->request = NULL;
+    request->next = NULL;
+    return request;
+}
+
+void
+httpDestroyRequest(HTTPRequestPtr request)
+{
+    if(request->object)
+        releaseObject(request->object);
+    if(request->condition)
+        httpDestroyCondition(request->condition);
+    releaseAtom(request->via);
+    assert(request->chandler == NULL);
+    releaseAtom(request->error_message);
+    releaseAtom(request->headers);
+    releaseAtom(request->error_headers);
+    assert(request->request == NULL);
+    assert(request->next == NULL);
+    free(request);
+}
+
+void
+httpQueueRequest(HTTPConnectionPtr connection, HTTPRequestPtr request)
+{
+    assert(request->next == NULL && request->connection == NULL);
+    request->connection = connection;
+    if(connection->request_last) {
+        assert(connection->request);
+        connection->request_last->next = request;
+        connection->request_last = request;
+    } else {
+        assert(!connection->request_last);
+        connection->request = request;
+        connection->request_last = request;
+    }
+}
+
+HTTPRequestPtr
+httpDequeueRequest(HTTPConnectionPtr connection)
+{
+    HTTPRequestPtr request = connection->request;
+    if(request) {
+        assert(connection->request_last);
+        connection->request = request->next;
+        if(!connection->request) connection->request_last = NULL;
+        request->next = NULL;
+    }
+    return request;
+}
+
+int
+httpConnectionBigify(HTTPConnectionPtr connection)
+{
+    char *bigbuf;
+    assert(!(connection->flags & CONN_BIGBUF));
+
+    if(bigBufferSize <= CHUNK_SIZE)
+        return 0;
+
+    bigbuf = malloc(bigBufferSize);
+    if(bigbuf == NULL)
+        return -1;
+    if(connection->len > 0)
+        memcpy(bigbuf, connection->buf, connection->len);
+    if(connection->buf)
+        dispose_chunk(connection->buf);
+    connection->buf = bigbuf;
+    connection->flags |= CONN_BIGBUF;
+    return 1;
+}
+
+int
+httpConnectionBigifyReqbuf(HTTPConnectionPtr connection)
+{
+    char *bigbuf;
+    assert(!(connection->flags & CONN_BIGREQBUF));
+
+    if(bigBufferSize <= CHUNK_SIZE)
+        return 0;
+
+    bigbuf = malloc(bigBufferSize);
+    if(bigbuf == NULL)
+        return -1;
+    if(connection->reqlen > 0)
+        memcpy(bigbuf, connection->reqbuf, connection->reqlen);
+    if(connection->reqbuf)
+        dispose_chunk(connection->reqbuf);
+    connection->reqbuf = bigbuf;
+    connection->flags |= CONN_BIGREQBUF;
+    return 1;
+}
+
+int
+httpConnectionUnbigify(HTTPConnectionPtr connection)
+{
+    char *buf;
+    assert(connection->flags & CONN_BIGBUF);
+    assert(connection->len < CHUNK_SIZE);
+
+    buf = get_chunk();
+    if(buf == NULL)
+        return -1;
+    if(connection->len > 0)
+        memcpy(buf, connection->buf, connection->len);
+    free(connection->buf);
+    connection->buf = buf;
+    connection->flags &= ~CONN_BIGBUF;
+    return 1;
+}
+
+int
+httpConnectionUnbigifyReqbuf(HTTPConnectionPtr connection)
+{
+    char *buf;
+    assert(connection->flags & CONN_BIGREQBUF);
+    assert(connection->reqlen < CHUNK_SIZE);
+
+    buf = get_chunk();
+    if(buf == NULL)
+        return -1;
+    if(connection->reqlen > 0)
+        memcpy(buf, connection->reqbuf, connection->reqlen);
+    free(connection->reqbuf);
+    connection->reqbuf = buf;
+    connection->flags &= ~CONN_BIGREQBUF;
+    return 1;
+}
+
+HTTPConditionPtr 
+httpMakeCondition()
+{
+    HTTPConditionPtr condition;
+    condition = malloc(sizeof(HTTPConditionRec));
+    if(condition == NULL)
+        return NULL;
+    condition->ims = -1;
+    condition->inms = -1;
+    condition->im = NULL;
+    condition->inm = NULL;
+    condition->ifrange = NULL;
+    return condition;
+}
+
+void
+httpDestroyCondition(HTTPConditionPtr condition)
+{
+    if(condition->inm)
+        free(condition->inm);
+    if(condition->im)
+        free(condition->im);
+    if(condition->ifrange)
+        free(condition->ifrange);
+    free(condition);
+}
+        
+int
+httpCondition(ObjectPtr object, HTTPConditionPtr condition)
+{
+    int rc = CONDITION_MATCH;
+
+    assert(!(object->flags & OBJECT_INITIAL));
+
+    if(!condition) return CONDITION_MATCH;
+
+    if(condition->ims >= 0) {
+        if(object->last_modified < 0 ||
+           condition->ims < object->last_modified)
+            return rc;
+        else
+            rc = CONDITION_NOT_MODIFIED;
+    }
+
+    if(condition->inms >= 0) {
+        if(object->last_modified < 0 || 
+           condition->inms >= object->last_modified)
+            return rc;
+        else
+            rc = CONDITION_FAILED;
+    }
+
+    if(condition->inm) {
+        if(!object->etag || strcmp(object->etag, condition->inm) != 0)
+            return rc;
+        else
+            rc = CONDITION_NOT_MODIFIED;
+    }
+
+    if(condition->im) {
+        if(!object->etag || strcmp(object->etag, condition->im) != 0)
+            rc = CONDITION_FAILED;
+        else
+            return rc;
+    }
+
+    return rc;
+}
+
+int
+httpWriteErrorHeaders(char *buf, int size, int offset, int do_body,
+                      int code, AtomPtr message, int close, AtomPtr headers,
+                      char *url, int url_len, char *etag)
+{
+    int n, m, i;
+    char *body;
+    char htmlMessage[100];
+    char timeStr[100];
+
+    assert(code != 0);
+
+    i = htmlString(htmlMessage, 0, 100, message->string, message->length);
+    if(i < 0)
+        strcpy(htmlMessage, "(Couldn't format message)");
+    else
+        htmlMessage[MIN(i, 99)] = '\0';
+
+    if(code != 304) {
+        body = get_chunk();
+        if(!body) {
+            do_log(L_ERROR, "Couldn't allocate body buffer.\n");
+            return -1;
+        }
+        m = snnprintf(body, 0, CHUNK_SIZE,
+                      "<!DOCTYPE HTML PUBLIC "
+                      "\"-//W3C//DTD HTML 4.01 Transitional//EN\" "
+                      "\"http://www.w3.org/TR/html4/loose.dtd\">"
+                      "\n<html><head>"
+                      "\n<title>Proxy %s: %3d %s.</title>"
+                      "\n</head><body>"
+                      "\n<h1>%3d %s</h1>"
+                      "\n<p>The following %s",
+                      code >= 400 ? "error" : "result",
+                      code, htmlMessage,
+                      code, htmlMessage,
+                      code >= 400 ? 
+                      "error occurred" :
+                      "status was returned");
+        if(url_len > 0) {
+            m = snnprintf(body, m, CHUNK_SIZE,
+                          " while trying to access <strong>");
+            m = htmlString(body, m, CHUNK_SIZE, url, url_len);
+            m = snnprintf(body, m, CHUNK_SIZE, "</strong>");
+        }
+
+        {
+            /* On BSD systems, tv_sec is a long. */
+            const time_t ct = current_time.tv_sec;
+                                             /*Mon, 24 Sep 2004 17:46:35 GMT*/
+            strftime(timeStr, sizeof(timeStr), "%a, %d %b %Y %H:%M:%S %Z",
+                     localtime(&ct));
+        }
+        
+        m = snnprintf(body, m, CHUNK_SIZE,
+                      ":<br><br>"
+                      "\n<strong>%3d %s</strong></p>"
+                      "\n<hr>Generated %s by %s on <em>%s:%d</em>."
+                      "\n</body></html>\r\n",
+                      code, htmlMessage,
+                      timeStr, displayName->string, proxyName->string, proxyPort);
+        if(m <= 0 || m >= CHUNK_SIZE) {
+            do_log(L_ERROR, "Couldn't write error body.\n");
+            dispose_chunk(body);
+            return -1;
+        }
+    } else {
+        body = NULL;
+        m = 0;
+    }
+
+    n = snnprintf(buf, 0, size,
+                 "HTTP/1.1 %3d %s"
+                 "\r\nConnection: %s"
+                 "\r\nDate: ",
+                  code, atomString(message),
+                  close ? "close" : "keep-alive");
+    n = format_time(buf, n, size, current_time.tv_sec);
+    if(code != 304) {
+        n = snnprintf(buf, n, size,
+                      "\r\nContent-Type: text/html"
+                      "\r\nContent-Length: %d", m);
+    } else {
+        if(etag)
+            n = snnprintf(buf, n, size, "\r\nETag: \"%s\"", etag);
+    }
+
+    if(code != 304 && code != 412) {
+        n = snnprintf(buf, n, size,
+                      "\r\nExpires: 0"
+                      "\r\nCache-Control: no-cache"
+                      "\r\nPragma: no-cache");
+    }
+
+    if(headers)
+        n = snnprint_n(buf, n, size,
+                      headers->string, headers->length);
+
+    n = snnprintf(buf, n, size, "\r\n\r\n");
+
+    if(n < 0 || n >= size) {
+        do_log(L_ERROR, "Couldn't write error.\n");
+        dispose_chunk(body);
+        return -1;
+    }
+
+    if(code != 304 && do_body) {
+        if(m > 0) memcpy(buf + n, body, m);
+        n += m;
+    }
+
+    if(body)
+        dispose_chunk(body);
+
+    return n;
+}
+
+AtomListPtr
+urlDecode(char *buf, int n)
+{
+    char mybuf[500];
+    int i, j = 0;
+    AtomListPtr list;
+    AtomPtr atom;
+
+    list = makeAtomList(NULL, 0);
+    if(list == NULL)
+        return NULL;
+
+    i = 0;
+    while(i < n) {
+        if(buf[i] == '%') {
+            int a, b;
+            if(i + 3 > n)
+                goto fail;
+            a = h2i(buf[i + 1]);
+            b = h2i(buf[i + 2]);
+            if(a < 0 || b < 0)
+                goto fail;
+            mybuf[j++] = (char)((a << 4) | b);
+            i += 3;
+            if(j >= 500) goto fail;
+        } else if(buf[i] == '&') {
+            atom = internAtomN(mybuf, j);
+            if(atom == NULL)
+                goto fail;
+            atomListCons(atom, list);
+            j = 0;
+            i++;
+        } else {
+            mybuf[j++] = buf[i++];
+            if(j >= 500) goto fail;
+        }
+    }
+
+    atom = internAtomN(mybuf, j);
+    if(atom == NULL)
+        goto fail;
+    atomListCons(atom, list);
+    return list;
+
+ fail:
+    destroyAtomList(list);
+    return NULL;
+}
+
+void
+httpTweakCachability(ObjectPtr object)
+{
+    int code = object->code;
+
+    if((object->cache_control & CACHE_AUTHORIZATION) &&
+       !(object->cache_control & CACHE_PUBLIC)) {
+        object->cache_control |= CACHE_NO_HIDDEN;
+        object->flags |= OBJECT_LINEAR;
+    }
+
+    /* This is not required by RFC 2616 -- but see RFC 3143 2.1.1.  We
+       manically avoid caching replies that we don't know how to
+       handle, even if Expires or Cache-Control says otherwise.  As to
+       known uncacheable replies, we obey Cache-Control and default to
+       allowing sharing but not caching. */
+    if(code != 200 && code != 206 && 
+       code != 300 && code != 301 && code != 302 && code != 303 &&
+       code != 304 && code != 307 &&
+       code != 403 && code != 404 && code != 405 && code != 416) {
+        object->cache_control |= (CACHE_NO_HIDDEN | CACHE_MISMATCH);
+        object->flags |= OBJECT_LINEAR;
+    } else if(code != 200 && code != 206 &&
+              code != 300 && code != 301 && code != 304 &&
+              code != 410) {
+        if(object->expires < 0 && !(object->cache_control & CACHE_PUBLIC)) {
+            object->cache_control |= CACHE_NO_HIDDEN;
+        }
+    } else if(dontCacheRedirects && (code == 301 || code == 302)) {
+        object->cache_control |= CACHE_NO_HIDDEN;
+    }
+
+    if(urlIsUncachable(object->key, object->key_size)) {
+        object->cache_control |= CACHE_NO_HIDDEN;
+    }
+
+    if((object->cache_control & CACHE_NO_STORE) != 0) {
+        object->cache_control |= CACHE_NO_HIDDEN;
+    }
+
+    if(object->cache_control & CACHE_VARY) {
+        if(!object->etag || dontTrustVaryETag >= 2) {
+            object->cache_control |= CACHE_MISMATCH;
+        }
+    }
+}
+
+int
+httpHeaderMatch(AtomPtr header, AtomPtr headers1, AtomPtr headers2)
+{
+    int rc1, b1, e1, rc2, b2, e2;
+
+    /* Short cut if both sets of headers are identical */
+    if(headers1 == headers2)
+        return 1;
+
+    rc1 = httpFindHeader(header, headers1->string, headers1->length,
+                         &b1, &e1);
+    rc2 = httpFindHeader(header, headers2->string, headers2->length,
+                         &b2, &e2);
+
+    if(rc1 == 0 && rc2 == 0)
+        return 1;
+
+    if(rc1 == 0 || rc2 == 0)
+        return 0;
+
+    if(e1 - b1 != e2 - b2)
+        return 0;
+
+    if(memcmp(headers1->string + b1, headers2->string + b2, e1 - b1) != 0)
+        return 0;
+
+    return 1;
+}

+ 184 - 0
polipo/http.h

@@ -0,0 +1,184 @@
+/*
+Copyright (c) 2003-2006 by Juliusz Chroboczek
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+typedef struct _HTTPCondition {
+    time_t ims;
+    time_t inms;
+    char *im;
+    char *inm;
+    char *ifrange;
+} HTTPConditionRec, *HTTPConditionPtr;
+
+typedef struct _HTTPRequest {
+    int flags;
+    struct _HTTPConnection *connection;
+    ObjectPtr object;
+    int method;
+    int from;
+    int to;
+    CacheControlRec cache_control;
+    HTTPConditionPtr condition;
+    AtomPtr via;
+    struct _ConditionHandler *chandler;
+    ObjectPtr can_mutate;
+    int error_code;
+    struct _Atom *error_message;
+    struct _Atom *error_headers;
+    AtomPtr headers;
+    struct timeval time0, time1;
+    struct _HTTPRequest *request;
+    struct _HTTPRequest *next;
+} HTTPRequestRec, *HTTPRequestPtr;
+
+/* request->flags */
+/* If not present, drop the connection after this request. */
+#define REQUEST_PERSISTENT 1
+/* This client-side request has already been requested on the server-side. */
+#define REQUEST_REQUESTED 2
+/* This request is waiting for continue from the server. */
+#define REQUEST_WAIT_CONTINUE 4
+/* Force an error unconditionally -- used for client auth failures. */
+#define REQUEST_FORCE_ERROR 8
+/* This server-side request was pipelined. */
+#define REQUEST_PIPELINED 16
+/* This client-side request has already switched objects once. */
+#define REQUEST_SUPERSEDED 32
+
+typedef struct _HTTPConnection {
+    int flags;
+    int fd;
+    char *buf;
+    int len;
+    int offset;
+    HTTPRequestPtr request;
+    HTTPRequestPtr request_last;
+    int serviced;
+    int version;
+    int time;
+    TimeEventHandlerPtr timeout;
+    int te;
+    char *reqbuf;
+    int reqlen;
+    int reqbegin;
+    int reqoffset;
+    int bodylen;
+    int reqte;
+    /* For server connections */
+    int chunk_remaining;
+    struct _HTTPServer *server;
+    int pipelined;
+    int connecting;
+} HTTPConnectionRec, *HTTPConnectionPtr;
+
+/* connection->flags */
+#define CONN_READER 1
+#define CONN_WRITER 2
+#define CONN_SIDE_READER 4
+#define CONN_BIGBUF 8
+#define CONN_BIGREQBUF 16
+
+/* request->method */
+#define METHOD_UNKNOWN -1
+#define METHOD_NONE -1
+#define METHOD_GET 0
+#define METHOD_HEAD 1
+#define METHOD_CONDITIONAL_GET 2
+#define METHOD_CONNECT 3
+#define METHOD_POST 4
+#define METHOD_PUT 5
+#define METHOD_OPTIONS 6
+#define METHOD_DELETE 7
+
+#define REQUEST_SIDE(request) ((request)->method >= METHOD_POST)
+
+/* server->version */
+#define HTTP_10 0
+#define HTTP_11 1
+#define HTTP_UNKNOWN -1
+
+/* connection->te */
+#define TE_IDENTITY 0
+#define TE_CHUNKED 1
+#define TE_UNKNOWN -1
+
+/* connection->connecting */
+#define CONNECTING_DNS 1
+#define CONNECTING_CONNECT 2
+#define CONNECTING_SOCKS 3
+
+/* the results of a conditional request.  200, 304 and 412. */
+#define CONDITION_MATCH 0
+#define CONDITION_NOT_MODIFIED 1
+#define CONDITION_FAILED 2
+
+extern int disableProxy;
+extern AtomPtr proxyName;
+extern int proxyPort;
+extern int clientTimeout, serverTimeout, serverIdleTimeout;
+extern int bigBufferSize;
+extern AtomPtr proxyAddress;
+extern int proxyOffline;
+extern int relaxTransparency;
+extern AtomPtr authRealm;
+extern AtomPtr authCredentials;
+extern AtomPtr parentAuthCredentials;
+extern AtomListPtr allowedClients;
+extern NetAddressPtr allowedNets;
+extern IntListPtr allowedPorts;
+extern IntListPtr tunnelAllowedPorts;
+extern int expectContinue;
+extern AtomPtr atom100Continue;
+extern int disableVia;
+extern int dontTrustVaryETag;
+
+void preinitHttp(void);
+void initHttp(void);
+
+int httpTimeoutHandler(TimeEventHandlerPtr);
+int httpSetTimeout(HTTPConnectionPtr connection, int secs);
+int httpWriteObjectHeaders(char *buf, int offset, int len, 
+                           ObjectPtr object, int from, int to);
+int httpPrintCacheControl(char*, int, int, int, CacheControlPtr);
+char *httpMessage(int) ATTRIBUTE((pure));
+int htmlString(char *buf, int n, int len, char *s, int slen);
+void htmlPrint(FILE *out, char *s, int slen);
+HTTPConnectionPtr httpMakeConnection(void);
+void httpDestroyConnection(HTTPConnectionPtr connection);
+void httpConnectionDestroyBuf(HTTPConnectionPtr connection);
+void httpConnectionDestroyReqbuf(HTTPConnectionPtr connection);
+HTTPRequestPtr httpMakeRequest(void);
+void httpDestroyRequest(HTTPRequestPtr request);
+void httpQueueRequest(HTTPConnectionPtr, HTTPRequestPtr);
+HTTPRequestPtr httpDequeueRequest(HTTPConnectionPtr connection);
+int httpConnectionBigify(HTTPConnectionPtr);
+int httpConnectionBigifyReqbuf(HTTPConnectionPtr);
+int httpConnectionUnbigify(HTTPConnectionPtr);
+int httpConnectionUnbigifyReqbuf(HTTPConnectionPtr);
+HTTPConditionPtr httpMakeCondition(void);
+void httpDestroyCondition(HTTPConditionPtr condition);
+int httpCondition(ObjectPtr, HTTPConditionPtr);
+int httpWriteErrorHeaders(char *buf, int size, int offset, int do_body,
+                          int code, AtomPtr message, int close, AtomPtr,
+                          char *url, int url_len, char *etag);
+AtomListPtr urlDecode(char*, int);
+void httpTweakCachability(ObjectPtr);
+int httpHeaderMatch(AtomPtr header, AtomPtr headers1, AtomPtr headers2);

+ 1551 - 0
polipo/http_parse.c

@@ -0,0 +1,1551 @@
+/*
+Copyright (c) 2003-2006 by Juliusz Chroboczek
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+#include "polipo.h"
+
+static int getNextWord(const char *buf, int i, int *x_return, int *y_return);
+static int getNextToken(const char *buf, int i, int *x_return, int *y_return);
+static int getNextTokenInList(const char *buf, int i, 
+                              int *x_return, int *y_return,
+                              int *z_return, int *t_return,
+                              int *end_return);
+
+static AtomPtr atomConnection, atomProxyConnection, atomContentLength,
+    atomHost, atomAcceptRange, atomTE,
+    atomReferer, atomProxyAuthenticate, atomProxyAuthorization,
+    atomKeepAlive, atomTrailer, atomDate, atomExpires,
+    atomIfModifiedSince, atomIfUnmodifiedSince, atomIfRange, atomLastModified,
+    atomIfMatch, atomIfNoneMatch, atomAge, atomTransferEncoding, 
+    atomETag, atomCacheControl, atomPragma, atomContentRange, atomRange,
+    atomVia, atomVary, atomExpect, atomAuthorization,
+    atomSetCookie, atomCookie, atomCookie2,
+    atomXPolipoDate, atomXPolipoAccess, atomXPolipoLocation, 
+    atomXPolipoBodyOffset;
+
+AtomPtr atomContentType, atomContentEncoding;
+
+int censorReferer = 0;
+int laxHttpParser = 1;
+
+static AtomListPtr censoredHeaders;
+
+void
+preinitHttpParser()
+{
+    CONFIG_VARIABLE_SETTABLE(censorReferer, CONFIG_TRISTATE, configIntSetter,
+                             "Censor referer headers.");
+    censoredHeaders = makeAtomList(NULL, 0);
+    if(censoredHeaders == NULL) {
+        do_log(L_ERROR, "Couldn't allocate censored atoms.\n");
+        exit(1);
+    }
+    CONFIG_VARIABLE(censoredHeaders, CONFIG_ATOM_LIST_LOWER,
+                    "Headers to censor.");
+    CONFIG_VARIABLE_SETTABLE(laxHttpParser, CONFIG_BOOLEAN, configIntSetter,
+                             "Ignore unknown HTTP headers.");
+}
+
+void
+initHttpParser()
+{
+#define A(name, value) name = internAtom(value); if(!name) goto fail;
+    /* These must be in lower-case */
+    A(atomConnection, "connection");
+    A(atomProxyConnection, "proxy-connection");
+    A(atomContentLength, "content-length");
+    A(atomHost, "host");
+    A(atomAcceptRange, "accept-range");
+    A(atomTE, "te");
+    A(atomReferer, "referer");
+    A(atomProxyAuthenticate, "proxy-authenticate");
+    A(atomProxyAuthorization, "proxy-authorization");
+    A(atomKeepAlive, "keep-alive");
+    A(atomTrailer, "trailer");
+    A(atomDate, "date");
+    A(atomExpires, "expires");
+    A(atomIfModifiedSince, "if-modified-since");
+    A(atomIfUnmodifiedSince, "if-unmodified-since");
+    A(atomIfRange, "if-range");
+    A(atomLastModified, "last-modified");
+    A(atomIfMatch, "if-match");
+    A(atomIfNoneMatch, "if-none-match");
+    A(atomAge, "age");
+    A(atomTransferEncoding, "transfer-encoding");
+    A(atomETag, "etag");
+    A(atomCacheControl, "cache-control");
+    A(atomPragma, "pragma");
+    A(atomContentRange, "content-range");
+    A(atomRange, "range");
+    A(atomVia, "via");
+    A(atomContentType, "content-type");
+    A(atomContentEncoding, "content-encoding");
+    A(atomVary, "vary");
+    A(atomExpect, "expect");
+    A(atomAuthorization, "authorization");
+    A(atomSetCookie, "set-cookie");
+    A(atomCookie, "cookie");
+    A(atomCookie2, "cookie2");
+    A(atomXPolipoDate, "x-polipo-date");
+    A(atomXPolipoAccess, "x-polipo-access");
+    A(atomXPolipoLocation, "x-polipo-location");
+    A(atomXPolipoBodyOffset, "x-polipo-body-offset");
+#undef A
+    return;
+
+ fail:
+    do_log(L_ERROR, "Couldn't allocate atom.\n");
+    exit(1);
+}
+
+static int
+getNextWord(const char *restrict buf, int i, int *x_return, int *y_return)
+{
+    int x, y;
+    while(buf[i] == ' ') i++;
+    if(buf[i] == '\n' || buf[i] == '\r') return -1;
+    x = i;
+    while(buf[i] > 32 && buf[i] < 127) i++;
+    y = i;
+
+    *x_return = x;
+    *y_return = y;
+
+    return 0;
+}
+
+static int
+skipComment(const char *restrict buf, int i)
+{
+    assert(buf[i] == '(');
+
+    i++;
+    while(1) {
+        if(buf[i] == '\\' && buf[i + 1] == ')') i+=2;
+        else if(buf[i] == ')') return i + 1;
+        else if(buf[i] == '\n') {
+            if(buf[i + 1] == ' ' || buf[i + 1] == '\t')
+                i += 2;
+            else
+                return -1;
+        } else if(buf[i] == '\r') {
+            if(buf[i + 1] != '\n') return -1;
+            if(buf[i + 2] == ' ' || buf[i + 2] == '\t')
+                i += 3;
+            else
+                return -1;
+        } else {
+            i++;
+        }
+    }
+    return i;
+}
+            
+
+static int
+skipWhitespace(const char *restrict buf, int i)
+{
+    while(1) {
+        if(buf[i] == ' ' || buf[i] == '\t')
+            i++;
+        else  if(buf[i] == '(') {
+            i = skipComment(buf, i);
+            if(i < 0) return -1;
+        } else if(buf[i] == '\n') {
+            if(buf[i + 1] == ' ' || buf[i + 1] == '\t')
+                i += 2;
+            else
+                return i;
+        } else if(buf[i] == '\r' && buf[i + 1] == '\n') {
+            if(buf[i + 2] == ' ' || buf[i + 2] == '\t')
+                i += 3;
+            else
+                return i;
+        } else
+            return i;
+    }
+}
+
+static int
+getNextToken(const char *restrict buf, int i, int *x_return, int *y_return)
+{
+    int x, y;
+ again:
+    while(buf[i] == ' ' || buf[i] == '\t')
+        i++;
+    if(buf[i] == '(') {
+        i++;
+        while(buf[i] != ')') {
+            if(buf[i] == '\n' || buf[i] == '\r')
+                return -1;
+            if(buf[i] == '\\' && buf[i + 1] != '\n' && buf[i + 1] != '\r')
+                buf += 2;
+            else
+                buf++;
+        }
+        goto again;
+    }
+    if(buf[i] == '\n') {
+        if(buf[i + 1] == ' ' || buf[i + 1] == '\t') {
+            i += 2;
+            goto again;
+        } else {
+            return -1;
+        }
+    }
+    if(buf[i] == '\r') {
+        if(buf[i + 1] == '\n' && (buf[i + 2] == ' ' || buf[i + 2] == '\t')) {
+            i += 3;
+            goto again;
+        } else {
+            return -1;
+        }
+    }
+    x = i;
+    while(buf[i] > 32 && buf[i] < 127) {
+        switch(buf[i]) {
+        case '(': case ')': case '<': case '>': case '@':
+        case ',': case ';': case ':': case '\\': case '/':
+        case '[': case ']': case '?': case '=':
+        case '{': case '}': case ' ': case '\t':
+            goto out;
+        default:
+            i++;
+        }
+    }
+ out:
+    y = i;
+
+    *x_return = x;
+    *y_return = y;
+
+    return y;
+}
+
+static int
+getNextETag(const char * restrict buf, int i, 
+            int *x_return, int *y_return, int *weak_return)
+{
+    int weak = 0;
+    int x, y;
+    while(buf[i] == ' ' || buf[i] == '\t')
+        i++;
+    if(buf[i] == 'W' && buf[i + 1] == '/') {
+        weak = 1;
+        i += 2;
+    }
+    if(buf[i] == '"')
+        i++;
+    else
+        return -1;
+
+    x = i;
+    while(buf[i] != '"') {
+        if(buf[i] == '\r' || buf[i] == '\n')
+            return -1;
+        i++;
+    }
+    y = i;
+    i++;
+
+    *x_return = x;
+    *y_return = y;
+    *weak_return = weak;
+    return i;
+}
+
+static int
+getNextTokenInList(const char *restrict buf, int i, 
+                   int *x_return, int *y_return,
+                   int *z_return, int *t_return,
+                   int *end_return)
+{
+    int j, x, y, z = -1, t = -1, end;
+    j = getNextToken(buf, i, &x, &y);
+    if(j < 0)
+        return -1;
+    while(buf[j] == ' ' || buf[j] == '\t')
+        j++;
+
+    if(buf[j] == '=') {
+        j++;
+        while(buf[j] == ' ' || buf[j] == '\t')
+            j++;
+        z = j;
+        while(buf[j] != ',' && buf[j] != '\n' && buf[j] != '\r')
+            j++;
+    }
+
+    if(buf[j] == '\n' || buf[j] == '\r') {
+        if(buf[j] == '\r') {
+            if(buf[j + 1] != '\n')
+                return -1;
+            j += 2;
+        } else
+            j++;
+        end = 1;
+        if(buf[j] == ' ' || buf[j] == '\t') {
+            while(buf[j] == ' ' || buf[j] == '\t')
+                j++;
+            end = 0;
+        }
+    } else if(buf[j] == ',') {
+        j++;
+        while(buf[j] == ' ' || buf[j] == '\t')
+            j++;
+        end = 0;
+    } else {
+        return -1;
+    }
+
+    *x_return = x;
+    *y_return = y;
+    if(z_return)
+        *z_return = z;
+    if(t_return)
+        *t_return = t;
+    *end_return = end;
+    return j;
+}
+
+static inline int
+token_compare(const char *buf, int start, int end, const char *s)
+{
+    return (strcasecmp_n(s, buf + start, end - start) == 0);
+}
+
+static int
+skipEol(const char *restrict buf, int i)
+{
+    while(buf[i] == ' ')
+        i++;
+    if(buf[i] == '\n')
+        return i + 1;
+    else if(buf[i] == '\r') {
+        if(buf[i + 1] == '\n')
+            return i + 2;
+        else
+            return -1;
+    } else {
+        return -1;
+    }
+}
+    
+static int
+skipToEol(const char *restrict buf, int i, int *start_return)
+{
+    while(buf[i] != '\n' && buf[i] != '\r')
+        i++;
+    if(buf[i] == '\n') {
+        *start_return = i;
+        return i + 1;
+    } else if(buf[i] == '\r') {
+        if(buf[i + 1] == '\n') {
+            *start_return = i;
+            return i + 2;
+        } else {
+            return -1;
+        }
+    }
+    return -1;
+}
+
+static int
+getHeaderValue(const char *restrict buf, int start, 
+               int *value_start_return, int *value_end_return)
+{
+    int i, j, k;
+
+    while(buf[start] == ' ' || buf[start] == '\t')
+        start++;
+    i = start;
+ again:
+    j = skipToEol(buf, i, &k);
+    if(j < 0)
+        return -1;
+    if(buf[j] == ' ' || buf[j] == '\t') {
+        i = j + 1;
+        goto again;
+    }
+    *value_start_return = start;
+    *value_end_return = k;
+    return j;
+}
+    
+int
+httpParseClientFirstLine(const char *restrict buf, int offset,
+                         int *method_return,
+                         AtomPtr *url_return,
+                         int *version_return)
+{
+    int i = 0;
+    int x, y;
+    int method;
+    AtomPtr url;
+    int version = HTTP_UNKNOWN;
+    int eol;
+
+    i = offset;
+    i = getNextWord(buf, i, &x, &y);
+    if(i < 0) return -1;
+    if(y == x + 3 && memcmp(buf + x, "GET", 3) == 0)
+        method = METHOD_GET;
+    else if(y == x + 4 && memcmp(buf + x, "HEAD", 4) == 0)
+        method = METHOD_HEAD;
+    else if(y == x + 4 && memcmp(buf + x, "POST", 4) == 0)
+        method = METHOD_POST;
+    else if(y == x + 3 && memcmp(buf + x, "PUT", 3) == 0)
+        method = METHOD_PUT;
+    else if(y == x + 7 && memcmp(buf + x, "CONNECT", 7) == 0)
+        method = METHOD_CONNECT;
+    else if(y == x + 7 && memcmp(buf + x, "OPTIONS", 7) == 0)
+        method = METHOD_OPTIONS;
+    else if(y == x + 6 && memcmp(buf + x, "DELETE", 6) == 0)
+        method = METHOD_DELETE;
+    else
+        method = METHOD_UNKNOWN;
+
+    i = getNextWord(buf, y + 1, &x, &y);
+    if(i < 0) return -1;
+
+    url = internAtomN(buf + x, y - x);
+
+    i = getNextWord(buf, y + 1, &x, &y);
+    if(i < 0) {
+        releaseAtom(url);
+        return -1;
+    }
+
+    if(y == x + 8) {
+        if(memcmp(buf + x, "HTTP/1.", 7) != 0)
+            version = HTTP_UNKNOWN;
+        else if(buf[x + 7] == '0')
+            version = HTTP_10;
+        else if(buf[x + 7] >= '1' && buf[x + 7] <= '9')
+            version = HTTP_11;
+        else
+            version = HTTP_UNKNOWN;
+    }
+
+    eol = skipEol(buf, y);
+    if(eol < 0) return -1;
+        
+    *method_return = method;
+    if(url_return)
+        *url_return = url;
+    else
+        releaseAtom(url);
+    *version_return = version;
+    return eol;
+}
+
+int
+httpParseServerFirstLine(const char *restrict buf, 
+                         int *status_return,
+                         int *version_return,
+                         AtomPtr *message_return)
+{
+    int i = 0;
+    int x, y, eol;
+    int status;
+    int version = HTTP_UNKNOWN;
+    
+    i = getNextWord(buf, 0, &x, &y);
+    if(i < 0)
+        return -1;
+    if(y == x + 8 && memcmp(buf + x, "HTTP/1.0", 8) == 0)
+        version = HTTP_10;
+    else if(y >= x + 8 && memcmp(buf + x, "HTTP/1.", 7) == 0)
+        version = HTTP_11;
+    else
+        version = HTTP_UNKNOWN;
+
+    i = getNextWord(buf, y + 1, &x, &y);
+    if(i < 0) return -1;
+    if(y == x + 3)
+        status = atol(buf + x);
+    else return -1;
+
+    i = skipToEol(buf, y, &eol);
+    if(i < 0) return -1;
+        
+    *status_return = status;
+    *version_return = version;
+    if(message_return) {
+        /* Netscape enterprise bug */
+        if(eol > y)
+            *message_return = internAtomN(buf + y + 1, eol - y - 1);
+        else
+            *message_return = internAtom("No message");
+    }
+    return i;
+}
+
+static int
+parseInt(const char *restrict buf, int start, int *val_return)
+{
+    int i = start, val = 0;
+    if(!digit(buf[i]))
+        return -1;
+    while(digit(buf[i])) {
+        val = val * 10 + (buf[i] - '0');
+        i++;
+    }
+    *val_return = val;
+    return i;
+}
+
+/* Returned *name_start_return is -1 at end of headers, -2 if the line
+   couldn't be parsed. */
+static int
+parseHeaderLine(const char *restrict buf, int start,
+                int *name_start_return, int *name_end_return,
+                int *value_start_return, int *value_end_return)
+{
+    int i;
+    int name_start, name_end, value_start, value_end;
+
+    if(buf[start] == '\n') {
+        *name_start_return = -1;
+        return start + 1;
+    }
+    if(buf[start] == '\r' && buf[start + 1] == '\n') {
+        *name_start_return = -1;
+        return start + 2;
+    }
+
+    i = getNextToken(buf, start, &name_start, &name_end);
+    if(i < 0 || buf[i] != ':')
+        goto syntax;
+    i++;
+    while(buf[i] == ' ' || buf[i] == '\t')
+        i++;
+
+    i = getHeaderValue(buf, i, &value_start, &value_end);
+    if(i < 0)
+        goto syntax;
+
+    *name_start_return = name_start;
+    *name_end_return = name_end;
+    *value_start_return = value_start;
+    *value_end_return = value_end;
+    return i;
+
+ syntax:
+    i = start;
+    while(1) {
+        if(buf[i] == '\n') {
+            i++;
+            break;
+        }
+        if(buf[i] == '\r' && buf[i + 1] == '\n') {
+            i += 2;
+            break;
+        }
+        i++;
+    }
+    *name_start_return = -2;
+    return i;
+}
+
+int
+findEndOfHeaders(const char *restrict buf, int from, int to, int *body_return) 
+{
+    int i = from;
+    int eol = 0;
+    while(i < to) {
+        if(buf[i] == '\n') {
+            if(eol) {
+                *body_return = i + 1;
+                return eol;
+            }
+            eol = i;
+            i++;
+        } else if(buf[i] == '\r') {
+            if(i < to - 1 && buf[i + 1] == '\n') {
+                if(eol) {
+                    *body_return = eol;
+                    return i + 2;
+                }
+                eol = i;
+                i += 2;
+            } else {
+                eol = 0;
+                i++;
+            }
+        } else {
+            eol = 0;
+            i++;
+        }
+    }
+    return -1;
+}
+
+static int
+parseContentRange(const char *restrict buf, int i, 
+                  int *from_return, int *to_return, int *full_len_return)
+{
+    int j;
+    int from, to, full_len;
+
+    i = skipWhitespace(buf, i);
+    if(i < 0) return -1;
+    if(!token_compare(buf, i, i + 5, "bytes")) {
+        do_log(L_WARN, "Incorrect Content-Range header -- chugging along.\n");
+    } else {
+        i += 5;
+    }
+    i = skipWhitespace(buf, i);
+    if(buf[i] == '*') {
+        from = 0;
+        to = -1;
+        i++;
+    } else {
+        i = parseInt(buf, i, &from);
+        if(i < 0) return -1;
+        if(buf[i] != '-') return -1;
+        i++;
+        i = parseInt(buf, i, &to);
+        if(i < 0) return -1;
+        to = to + 1;
+    }
+    if(buf[i] != '/')
+        return -1;
+    i++;
+    if(buf[i] == '*')
+        full_len = -1;
+    else {
+        i = parseInt(buf, i, &full_len);
+        if(i < 0) return -1;
+    }
+    j = skipEol(buf, i);
+    if(j < 0)
+        return -1;
+
+    *from_return = from;
+    *to_return = to;
+    *full_len_return = full_len;
+    return i;
+}
+
+static int
+parseRange(const char *restrict buf, int i, 
+           int *from_return, int *to_return)
+{
+    int j;
+    int from, to;
+
+    i = skipWhitespace(buf, i);
+    if(i < 0)
+        return -1;
+    if(!token_compare(buf, i, i + 6, "bytes="))
+        return -1;
+    i += 6;
+    i = skipWhitespace(buf, i);
+    if(buf[i] == '-') {
+        from = 0;
+    } else {
+        i = parseInt(buf, i, &from);
+        if(i < 0) return -1;
+    }
+    if(buf[i] != '-')
+        return -1;
+    i++;
+    j = parseInt(buf, i, &to);
+    if(j < 0) 
+        to = -1;
+    else {
+        to = to + 1;
+        i = j;
+    }
+    j = skipEol(buf, i);
+    if(j < 0) return -1;
+    *from_return = from;
+    *to_return = to;
+    return i;
+}
+
+static void
+parseCacheControl(const char *restrict buf, 
+                  int token_start, int token_end,
+                  int v_start, int v_end, int *age_return)
+{
+    if(v_start <= 0 || !digit(buf[v_start])) {
+        do_log(L_WARN, "Couldn't parse Cache-Control: ");
+        do_log_n(L_WARN, buf + token_start,
+                 (v_end >= 0 ? v_end : token_end) -
+                 token_start);
+        do_log(L_WARN, "\n");
+    } else
+        *age_return = atoi(buf + v_start);
+}
+
+static int
+urlSameHost(const char *url1, int len1, const char *url2, int len2)
+{
+    int i;
+    if(len1 < 7 || len2 < 7)
+        return 0;
+    if(memcmp(url1 + 4, "://", 3) != 0 || memcmp(url2 + 4, "://", 3) != 0)
+        return 0;
+
+    i = 7;
+    while(i < len1 && i < len2 && url1[i] != '/' && url2[i] != '/') {
+        if((url1[i] | 0x20) != (url2[i] | 0x20))
+            break;
+        i++;
+    }
+
+    if((i == len1 || url1[i] == '/') && ((i == len2 || url2[i] == '/')))
+        return 1;
+    return 0;
+}
+
+static char *
+resize_hbuf(char *hbuf, int *size, char *hbuf_small)
+{
+    int new_size = 2 * *size;
+    char *new_hbuf;
+
+    if(new_size <= *size)
+        goto fail;
+
+    if(hbuf == hbuf_small) {
+        new_hbuf = malloc(new_size);
+        if(new_hbuf == NULL) goto fail;
+        memcpy(new_hbuf, hbuf, *size);
+    } else {
+        new_hbuf = realloc(hbuf, new_size);
+        if(new_hbuf == NULL) goto fail;
+    }
+    *size = new_size;
+    return new_hbuf;
+
+ fail:
+    if(hbuf != hbuf_small)
+        free(hbuf);
+    *size = 0;
+    return NULL;
+}
+
+int
+httpParseHeaders(int client, AtomPtr url,
+                 const char *buf, int start, HTTPRequestPtr request,
+                 AtomPtr *headers_return,
+                 int *len_return, CacheControlPtr cache_control_return,
+                 HTTPConditionPtr *condition_return, int *te_return,
+                 time_t *date_return, time_t *last_modified_return,
+                 time_t *expires_return, time_t *polipo_age_return,
+                 time_t *polipo_access_return, int *polipo_body_offset_return,
+                 int *age_return, char **etag_return, AtomPtr *expect_return,
+                 HTTPRangePtr range_return, HTTPRangePtr content_range_return,
+                 char **location_return, AtomPtr *via_return,
+                 AtomPtr *auth_return)
+{
+    int local = url ? urlIsLocal(url->string, url->length) : 0;
+    char hbuf_small[512];
+    char *hbuf = hbuf_small;
+    int hbuf_size = 512, hbuf_length = 0;
+    int i, j,
+        name_start, name_end, value_start, value_end, 
+        token_start, token_end, end;
+    AtomPtr name = NULL;
+    time_t date = -1, last_modified = -1, expires = -1, polipo_age = -1,
+        polipo_access = -1, polipo_body_offset = -1;
+    int len = -1;
+    CacheControlRec cache_control;
+    char *endptr;
+    int te = TE_IDENTITY;
+    int age = -1;
+    char *etag = NULL, *ifrange = NULL;
+    int persistent = (!request || (request->connection->version != HTTP_10));
+    char *location = NULL;
+    AtomPtr via = NULL;
+    AtomPtr auth = NULL;
+    AtomPtr expect = NULL;
+    HTTPConditionPtr condition;
+    time_t ims = -1, inms = -1;
+    char *im = NULL, *inm = NULL;
+    AtomListPtr hopToHop = NULL;
+    HTTPRangeRec range = {-1, -1, -1}, content_range = {-1, -1, -1};
+    int haveCacheControl = 0;
+ 
+#define RESIZE_HBUF() \
+    do { \
+        hbuf = resize_hbuf(hbuf, &hbuf_size, hbuf_small); \
+        if(hbuf == NULL) \
+            goto fail; \
+    } while(0)
+
+    cache_control.flags = 0;
+    cache_control.max_age = -1;
+    cache_control.s_maxage = -1;
+    cache_control.min_fresh = -1;
+    cache_control.max_stale = -1;
+    
+    i = start;
+
+    while(1) {
+        i = parseHeaderLine(buf, i,
+                            &name_start, &name_end, &value_start, &value_end);
+        if(i < 0) {
+            do_log(L_ERROR, "Couldn't find end of header line.\n");
+            goto fail;
+        }
+
+        if(name_start == -1)
+            break;
+
+        if(name_start < 0)
+            continue;
+
+        name = internAtomLowerN(buf + name_start, name_end - name_start);
+
+        if(name == atomConnection) {
+            j = getNextTokenInList(buf, value_start, 
+                                   &token_start, &token_end, NULL, NULL,
+                                   &end);
+            while(1) {
+                if(j < 0) {
+                    do_log(L_ERROR, "Couldn't parse Connection: ");
+                    do_log_n(L_ERROR, buf + value_start, 
+                             value_end - value_start);
+                    do_log(L_ERROR, ".\n");
+                    goto fail;
+                }
+                if(token_compare(buf, token_start, token_end, "close")) {
+                    persistent = 0;
+                } else if(token_compare(buf, token_start, token_end, 
+                                        "keep-alive")) {
+                    persistent = 1;
+                } else {
+                    if(hopToHop == NULL)
+                        hopToHop = makeAtomList(NULL, 0);
+                    if(hopToHop == NULL) {
+                        do_log(L_ERROR, "Couldn't allocate atom list.\n");
+                        goto fail;
+                    }
+                    atomListCons(internAtomLowerN(buf + token_start,
+                                                  token_end - token_start),
+                                 hopToHop);
+                }
+                if(end)
+                    break;
+                j = getNextTokenInList(buf, j, 
+                                       &token_start, &token_end, NULL, NULL,
+                                       &end);
+            }
+        } else if(name == atomCacheControl)
+            haveCacheControl = 1;
+
+        releaseAtom(name);
+        name = NULL;
+    }
+    
+    i = start;
+
+    while(1) {
+        i = parseHeaderLine(buf, i, 
+                            &name_start, &name_end, &value_start, &value_end);
+        if(i < 0) {
+            do_log(L_ERROR, "Couldn't find end of header line.\n");
+            goto fail;
+        }
+
+        if(name_start == -1)
+            break;
+
+        if(name_start < 0) {
+            do_log(L_WARN, "Couldn't parse header line.\n");
+            if(laxHttpParser)
+                continue;
+            else
+                goto fail;
+        }
+
+        name = internAtomLowerN(buf + name_start, name_end - name_start);
+        
+        if(name == atomProxyConnection) {
+            j = getNextTokenInList(buf, value_start, 
+                                   &token_start, &token_end, NULL, NULL,
+                                   &end);
+            while(1) {
+                if(j < 0) {
+                    do_log(L_WARN, "Couldn't parse Proxy-Connection:");
+                    do_log_n(L_WARN, buf + value_start, 
+                             value_end - value_start);
+                    do_log(L_WARN, ".\n");
+                    persistent = 0;
+                    break;
+                }
+                if(token_compare(buf, token_start, token_end, "close")) {
+                    persistent = 0;
+                } else if(token_compare(buf, token_start, token_end, 
+                                        "keep-alive")) {
+                    persistent = 1;
+                }
+                if(end)
+                    break;
+                j = getNextTokenInList(buf, j, 
+                                       &token_start, &token_end, NULL, NULL,
+                                       &end);
+            }
+        } else if(name == atomContentLength) {
+            j = skipWhitespace(buf, value_start);
+            if(j < 0) {
+                do_log(L_WARN, "Couldn't parse Content-Length: \n");
+                do_log_n(L_WARN, buf + value_start, value_end - value_start);
+                do_log(L_WARN, ".\n");
+                len = -1;
+            } else {
+                errno = 0;
+                len = strtol(buf + value_start, &endptr, 10);
+                if(errno == ERANGE || endptr <= buf + value_start) {
+                    do_log(L_WARN, "Couldn't parse Content-Length: \n");
+                    do_log_n(L_WARN, buf + value_start, 
+                             value_end - value_start);
+                    do_log(L_WARN, ".\n");
+                    len = -1;
+                }
+            }
+        } else if((!local && name == atomProxyAuthorization) ||
+                  (local && name == atomAuthorization)) {
+            if(auth_return) {
+                auth = internAtomN(buf + value_start, value_end - value_start);
+                if(auth == NULL) {
+                    do_log(L_ERROR, "Couldn't allocate authorization.\n");
+                    goto fail;
+                }
+            }
+        } else if(name == atomReferer) {
+            int h;
+            if(censorReferer == 0 || 
+               (censorReferer == 1 && url != NULL &&
+                urlSameHost(url->string, url->length,
+                            buf + value_start, value_end - value_start))) {
+                while(hbuf_length > hbuf_size - 2)
+                    RESIZE_HBUF();
+                hbuf[hbuf_length++] = '\r';
+                hbuf[hbuf_length++] = '\n';
+                do {
+                    h = snnprint_n(hbuf, hbuf_length, hbuf_size,
+                                   buf + name_start, value_end - name_start);
+                    if(h < 0) RESIZE_HBUF();
+                } while(h < 0);
+                hbuf_length = h;
+            }
+        } else if(name == atomTrailer) {
+            do_log(L_ERROR, "Trailers present.\n");
+            goto fail;
+        } else if(name == atomDate || name == atomExpires ||
+                  name == atomIfModifiedSince || 
+                  name == atomIfUnmodifiedSince ||
+                  name == atomLastModified ||
+                  name == atomXPolipoDate || name == atomXPolipoAccess) {
+            time_t t;
+            j = parse_time(buf, value_start, value_end, &t);
+            if(j < 0) {
+                if(name != atomExpires) {
+                    do_log(L_WARN, "Couldn't parse %s: ", name->string);
+                    do_log_n(L_WARN, buf + value_start,
+                             value_end - value_start);
+                    do_log(L_WARN, "\n");
+                }
+                t = -1;
+            }
+            if(name == atomDate) {
+                if(t >= 0)
+                    date = t;
+            } else if(name == atomExpires) {
+                if(t >= 0)
+                    expires = t;
+                else
+                    expires = 0;
+            } else if(name == atomLastModified)
+                last_modified = t;
+            else if(name == atomIfModifiedSince)
+                ims = t;
+            else if(name == atomIfUnmodifiedSince)
+                inms = t;
+            else if(name == atomXPolipoDate)
+                polipo_age = t;
+            else if(name == atomXPolipoAccess)
+                polipo_access = t;
+        } else if(name == atomAge) {
+            j = skipWhitespace(buf, value_start);
+            if(j < 0) {
+                age = -1;
+            } else {
+                errno = 0;
+                age = strtol(buf + value_start, &endptr, 10);
+                if(errno == ERANGE || endptr <= buf + value_start)
+                    age = -1;
+            }
+            if(age < 0) {
+                do_log(L_WARN, "Couldn't parse age: \n");
+                do_log_n(L_WARN, buf + value_start, value_end - value_start);
+                do_log(L_WARN, " -- ignored.\n");
+            }
+        } else if(name == atomXPolipoBodyOffset) {
+            j = skipWhitespace(buf, value_start);
+            if(j < 0) {
+                do_log(L_ERROR, "Couldn't parse body offset.\n");
+                goto fail;
+            } else {
+                errno = 0;
+                polipo_body_offset = strtol(buf + value_start, &endptr, 10);
+                if(errno == ERANGE || endptr <= buf + value_start) {
+                    do_log(L_ERROR, "Couldn't parse body offset.\n");
+                    goto fail;
+                }
+            }
+        } else if(name == atomTransferEncoding) {
+            if(token_compare(buf, value_start, value_end, "identity"))
+                te = TE_IDENTITY;
+            else if(token_compare(buf, value_start, value_end, "chunked"))
+                te = TE_CHUNKED;
+            else
+                te = TE_UNKNOWN;
+        } else if(name == atomETag ||
+                  name == atomIfNoneMatch || name == atomIfMatch ||
+                  name == atomIfRange) {
+            int x, y;
+            int weak;
+            char *e;
+            j = getNextETag(buf, value_start, &x, &y, &weak);
+            if(j < 0) {
+                if(buf[value_start] != '\r' && buf[value_start] != '\n')
+                    do_log(L_ERROR, "Couldn't parse ETag.\n");
+            } else if(weak) {
+                do_log(L_WARN, "Server returned weak ETag -- ignored.\n");
+            } else {
+                e = strdup_n(buf + x, y - x);
+                if(e == NULL) goto fail;
+                if(name == atomETag) {
+                    if(!etag)
+                        etag = e;
+                    else
+                        free(e);
+                } else if(name == atomIfNoneMatch) {
+                    if(!inm)
+                        inm = e;
+                    else
+                        free(e);
+                } else if(name == atomIfMatch) {
+                    if(!im)
+                        im = e;
+                    else
+                        free(e);
+                } else if(name == atomIfRange) {
+                    if(!ifrange)
+                        ifrange = e;
+                    else
+                        free(e);
+                } else {
+                    abort();
+                }
+            }
+        } else if(name == atomCacheControl) {
+            int v_start, v_end;
+            j = getNextTokenInList(buf, value_start, 
+                                   &token_start, &token_end, 
+                                   &v_start, &v_end,
+                                   &end);
+            while(1) {
+                if(j < 0) {
+                    do_log(L_WARN, "Couldn't parse Cache-Control.\n");
+                    cache_control.flags |= CACHE_NO;
+                    break;
+                }
+                if(token_compare(buf, token_start, token_end, "no-cache")) {
+                    cache_control.flags |= CACHE_NO;
+                } else if(token_compare(buf, token_start, token_end,
+                                        "public")) {
+                    cache_control.flags |= CACHE_PUBLIC;
+                } else if(token_compare(buf, token_start, token_end, 
+                                        "private")) {
+                    cache_control.flags |= CACHE_PRIVATE;
+                } else if(token_compare(buf, token_start, token_end, 
+                                        "no-store")) {
+                    cache_control.flags |= CACHE_NO_STORE;
+                } else if(token_compare(buf, token_start, token_end, 
+                                        "no-transform")) {
+                    cache_control.flags |= CACHE_NO_TRANSFORM;
+                } else if(token_compare(buf, token_start, token_end,
+                                        "must-revalidate") ||
+                          token_compare(buf, token_start, token_end,
+                                        "must-validate")) { /* losers */
+                    cache_control.flags |= CACHE_MUST_REVALIDATE;
+                } else if(token_compare(buf, token_start, token_end, 
+                                        "proxy-revalidate")) {
+                    cache_control.flags |= CACHE_PROXY_REVALIDATE;
+                } else if(token_compare(buf, token_start, token_end,
+                                        "only-if-cached")) {
+                    cache_control.flags |= CACHE_ONLY_IF_CACHED;
+                } else if(token_compare(buf, token_start, token_end,
+                                        "max-age") ||
+                          token_compare(buf, token_start, token_end,
+                                        "maxage") || /* losers */
+                          token_compare(buf, token_start, token_end,
+                                        "s-maxage") ||
+                          token_compare(buf, token_start, token_end,
+                                        "min-fresh")) {
+                    parseCacheControl(buf, token_start, token_end,
+                                      v_start, v_end,
+                                      &cache_control.max_age);
+                } else if(token_compare(buf, token_start, token_end,
+                                        "max-stale")) {
+                    parseCacheControl(buf, token_start, token_end,
+                                      v_start, v_end,
+                                      &cache_control.max_stale);
+                } else {
+                    do_log(L_WARN, "Unsupported Cache-Control directive ");
+                    do_log_n(L_WARN, buf + token_start, 
+                             (v_end >= 0 ? v_end : token_end) - token_start);
+                    do_log(L_WARN, " -- ignored.\n");
+                }
+                if(end)
+                    break;
+                j = getNextTokenInList(buf, j, 
+                                       &token_start, &token_end,
+                                       &v_start, &v_end,
+                                       &end);
+            }
+        } else if(name == atomContentRange) {
+            if(!client) {
+                j = parseContentRange(buf, value_start, 
+                                      &content_range.from, &content_range.to, 
+                                      &content_range.full_length);
+                if(j < 0) {
+                    do_log(L_ERROR, "Couldn't parse Content-Range: ");
+                    do_log_n(L_ERROR, buf + value_start, 
+                             value_end - value_start);
+                    do_log(L_ERROR, "\n");
+                    goto fail;
+                }
+            } else {
+                do_log(L_ERROR, "Content-Range from client.\n");
+                goto fail;
+            }
+        } else if(name == atomRange) {
+            if(client) {
+                j = parseRange(buf, value_start, &range.from, &range.to);
+                if(j < 0) {
+                    do_log(L_WARN, "Couldn't parse Range -- ignored.\n");
+                    range.from = -1;
+                    range.to = -1;
+                }
+            } else {
+                do_log(L_WARN, "Range from server -- ignored\n");
+            }
+        } else if(name == atomXPolipoLocation) {
+            if(location_return) {
+                location = 
+                    strdup_n(buf + value_start, value_end - value_start);
+                if(location == NULL) {
+                    do_log(L_ERROR, "Couldn't allocate location.\n");
+                    goto fail;
+                }
+            }
+        } else if(name == atomVia) {
+            if(via_return) {
+                AtomPtr new_via, full_via;
+                new_via =
+                    internAtomN(buf + value_start, value_end - value_start);
+                if(new_via == NULL) {
+                    do_log(L_ERROR, "Couldn't allocate via.\n");
+                    goto fail;
+                }
+                if(via) {
+                    full_via =
+                        internAtomF("%s, %s", via->string, new_via->string);
+                    releaseAtom(new_via);
+                    if(full_via == NULL) {
+                        do_log(L_ERROR, "Couldn't allocate via");
+                        goto fail;
+                    }
+                    releaseAtom(via);
+                    via = full_via;
+                } else {
+                    via = new_via;
+                }
+            }
+        } else if(name == atomExpect) {
+            if(expect_return) {
+                expect = internAtomLowerN(buf + value_start, 
+                                          value_end - value_start);
+                if(expect == NULL) {
+                    do_log(L_ERROR, "Couldn't allocate expect.\n");
+                    goto fail;
+                }
+            }
+        } else {
+            if(!client && name == atomContentType) {
+                if(token_compare(buf, value_start, value_end,
+                                 "multipart/byteranges")) {
+                    do_log(L_ERROR, 
+                           "Server returned multipart/byteranges -- yuck!\n");
+                    goto fail;
+                }
+            } 
+            if(name == atomVary) {
+                if(!token_compare(buf, value_start, value_end, "host") &&
+                   !token_compare(buf, value_start, value_end, "*")) {
+                    /* What other vary headers should be ignored? */
+                    do_log(L_VARY, "Vary header present (");
+                    do_log_n(L_VARY,
+                             buf + value_start, value_end - value_start);
+                    do_log(L_VARY, ").\n");
+                }
+                cache_control.flags |= CACHE_VARY;
+            } else if(name == atomAuthorization) {
+                cache_control.flags |= CACHE_AUTHORIZATION;
+            } 
+
+            if(name == atomPragma) {
+                /* Pragma is only defined for the client, and the only
+                   standard value is no-cache (RFC 1945, 10.12).
+                   However, we honour a Pragma: no-cache for both the client
+                   and the server when there's no Cache-Control header.  In
+                   all cases, we pass the Pragma header to the next hop. */
+                if(!haveCacheControl) {
+                    j = getNextTokenInList(buf, value_start,
+                                           &token_start, &token_end, NULL, NULL,
+                                           &end);
+                    while(1) {
+                        if(j < 0) {
+                            do_log(L_WARN, "Couldn't parse Pragma.\n");
+                            cache_control.flags |= CACHE_NO;
+                            break;
+                        }
+                        if(token_compare(buf, token_start, token_end,
+                                         "no-cache"))
+                            cache_control.flags = CACHE_NO;
+                        if(end)
+                            break;
+                        j = getNextTokenInList(buf, j, &token_start, &token_end,
+                                               NULL, NULL, &end);
+                    }
+                }
+            }
+            if(!client &&
+               (name == atomSetCookie || 
+                name == atomCookie || name == atomCookie2))
+                cache_control.flags |= CACHE_COOKIE;
+
+            if(hbuf) {
+                if(name != atomConnection && name != atomHost &&
+                   name != atomAcceptRange && name != atomTE &&
+                   name != atomProxyAuthenticate &&
+                   name != atomKeepAlive &&
+                   (!hopToHop || !atomListMember(name, hopToHop)) &&
+                   !atomListMember(name, censoredHeaders)) {
+                    int h;
+                    while(hbuf_length > hbuf_size - 2)
+                        RESIZE_HBUF();
+                    hbuf[hbuf_length++] = '\r';
+                    hbuf[hbuf_length++] = '\n';
+                    do {
+                        h = snnprint_n(hbuf, hbuf_length, hbuf_size,
+                                       buf + name_start, 
+                                       value_end - name_start);
+                        if(h < 0) RESIZE_HBUF();
+                    } while(h < 0);
+                    hbuf_length = h;
+                }
+            }
+        }
+        releaseAtom(name);
+        name = NULL;
+    }
+
+    if(headers_return) {
+        AtomPtr pheaders = NULL; 
+        pheaders = internAtomN(hbuf, hbuf_length);
+        if(!pheaders)
+            goto fail;
+        *headers_return = pheaders;
+    }
+    if(hbuf != hbuf_small)
+        free(hbuf);
+    hbuf = NULL;
+    hbuf_size = 0;
+
+    if(request)
+        if(!persistent)
+            request->flags &= ~REQUEST_PERSISTENT;
+
+    if(te != TE_IDENTITY) len = -1;
+    if(len_return) *len_return = len;
+    if(cache_control_return) *cache_control_return = cache_control;
+    if(condition_return) {
+        if(ims >= 0 || inms >= 0 || im || inm || ifrange) {
+            condition = httpMakeCondition();
+            if(condition) {
+                condition->ims = ims;
+                condition->inms = inms;
+                condition->im = im;
+                condition->inm = inm;
+                condition->ifrange = ifrange;
+            } else {
+                do_log(L_ERROR, "Couldn't allocate condition.\n");
+                if(im) free(im);
+                if(inm) free(inm);
+            }
+        } else {
+            condition = NULL;
+        }
+        *condition_return = condition;
+    } else {
+        assert(!im && !inm);
+    }
+            
+    if(te_return) *te_return = te;
+    if(date_return) *date_return = date;
+    if(last_modified_return) *last_modified_return = last_modified;
+    if(expires_return) *expires_return = expires;
+    if(polipo_age_return) *polipo_age_return = polipo_age;
+    if(polipo_access_return) *polipo_access_return = polipo_access;
+    if(polipo_body_offset_return)
+        *polipo_body_offset_return = polipo_body_offset;
+    if(age_return) *age_return = age;
+    if(etag_return)
+        *etag_return = etag;
+    else {
+        if(etag) free(etag);
+    }
+    if(range_return) *range_return = range;
+    if(content_range_return) *content_range_return = content_range;
+    if(location_return) {
+        *location_return = location;
+    } else {
+        if(location)
+            free(location);
+    }
+    if(via_return)
+        *via_return = via;
+    else {
+        if(via)
+            releaseAtom(via);
+    }
+    if(expect_return)
+        *expect_return = expect;
+    else {
+        if(expect)
+            releaseAtom(expect);
+    }
+    if(auth_return)
+        *auth_return = auth;
+    else {
+        if(auth)
+            releaseAtom(auth);
+    }
+    if(hopToHop) destroyAtomList(hopToHop);
+    return i;
+
+ fail:
+    if(hbuf && hbuf != hbuf_small) free(hbuf);
+    if(name) releaseAtom(name);
+    if(etag) free(etag);
+    if(location) free(location);
+    if(via) releaseAtom(via);
+    if(expect) releaseAtom(expect);
+    if(auth) releaseAtom(auth);
+    if(hopToHop) destroyAtomList(hopToHop);
+        
+    return -1;
+#undef RESIZE_HBUF
+}
+
+int
+httpFindHeader(AtomPtr header, const char *headers, int hlen,
+               int *value_begin_return, int *value_end_return)
+{
+    int len = header->length;
+    int i = 0;
+
+    while(i + len + 1 < hlen) {
+        if(headers[i + len] == ':' &&
+           lwrcmp(headers + i, header->string, len) == 0) {
+            int j = i + len + 1, k;
+            while(j < hlen && headers[j] == ' ')
+                j++;
+            k = j;
+            while(k < hlen && headers[k] != '\n' && headers[k] != '\r')
+                k++;
+            *value_begin_return = j;
+            *value_end_return = k;
+            return 1;
+        } else {
+            while(i < hlen && headers[i] != '\n' && headers[i] != '\r')
+                i++;
+            i++;
+            if(i < hlen && headers[i] == '\n')
+                i++;
+        }
+    }
+    return 0;
+}
+
+int
+parseUrl(const char *url, int len,
+         int *x_return, int *y_return, int *port_return, int *z_return)
+{
+    int x, y, z, port = -1, i = 0;
+
+    if(len >= 7 && lwrcmp(url, "http://", 7) == 0) {
+        x = 7;
+        if(x < len && url[x] == '[') {
+            /* RFC 2732 */
+            for(i = x + 1; i < len; i++) {
+                if(url[i] == ']') {
+                    i++;
+                    break;
+                }
+                if((url[i] != ':') && !letter(url[i]) && !digit(url[i]))
+                    break;
+            }
+        } else {
+            for(i = x; i < len; i++)
+                if(url[i] == ':' || url[i] == '/')
+                    break;
+        }
+        y = i;
+
+        if(i < len && url[i] == ':') {
+            int j;
+            j = atoi_n(url, i + 1, len, &port);
+            if(j < 0) {
+                port = 80;
+            } else {
+                    i = j;
+            }
+        } else {
+            port = 80;
+        }
+    } else {
+        x = -1;
+        y = -1;
+    }
+
+    z = i;
+
+    *x_return = x;
+    *y_return = y;
+    *port_return = port;
+    *z_return = z;
+    return 0;
+}
+
+int
+urlIsLocal(const char *url, int len)
+{
+    return (len > 0 && url[0] == '/');
+}
+
+int
+urlIsSpecial(const char *url, int len)
+{
+    return (len >= 8 && memcmp(url, "/polipo/", 8) == 0);
+}
+
+int
+parseChunkSize(const char *restrict buf, int i, int end,
+               int *chunk_size_return)
+{
+    int v, d;
+    v = h2i(buf[i]);
+    if(v < 0)
+        return -1;
+
+    i++;
+
+    while(i < end) {
+        d = h2i(buf[i]);
+        if(d < 0)
+            break;
+        v = v * 16 + d;
+        i++;
+    }
+
+    while(i < end) {
+        if(buf[i] == ' ' || buf[i] == '\t')
+            i++;
+        else
+            break;
+    }
+
+    if(i >= end - 1)
+        return 0;
+
+    if(buf[i] != '\r' || buf[i + 1] != '\n')
+        return -1;
+
+    i += 2;
+
+    if(v == 0) {
+        if(i >= end - 1)
+            return 0;
+        if(buf[i] != '\r') {
+            do_log(L_ERROR, "Trailers present!\n");
+            return -1;
+        }
+        i++;
+        if(buf[i] != '\n')
+            return -1;
+        i++;
+    }
+
+    *chunk_size_return = v;
+    return i;
+}
+
+
+int
+checkVia(AtomPtr name, AtomPtr via)
+{
+    int i;
+    char *v;
+    if(via == NULL || via->length == 0)
+        return 1;
+
+    v = via->string;
+
+    i = 0;
+    while(i < via->length) {
+        while(v[i] == ' ' || v[i] == '\t' || v[i] == ',' ||
+              v[i] == '\r' || v[i] == '\n' ||
+              digit(v[i]) || v[i] == '.')
+            i++;
+        if(i + name->length > via->length)
+            break;
+        if(memcmp(v + i, name->string, name->length) == 0) {
+            char c = v[i + name->length];
+            if(c == '\0' || c == ' ' || c == '\t' || c == ',' ||
+               c == '\r' || c == '\n')
+                return 0;
+        }
+        i++;
+        while(letter(v[i]) || digit(v[i]) || v[i] == '.')
+            i++;
+    }
+    return 1;
+}

+ 58 - 0
polipo/http_parse.h

@@ -0,0 +1,58 @@
+/*
+Copyright (c) 2003-2006 by Juliusz Chroboczek
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+typedef struct HTTPRange {
+    int from;
+    int to;
+    int full_length;
+} HTTPRangeRec, *HTTPRangePtr;
+
+extern int censorReferer;
+extern AtomPtr atomContentType, atomContentEncoding;
+
+void preinitHttpParser(void);
+void initHttpParser(void);
+int httpParseClientFirstLine(const char *buf, int offset,
+                             int *method_return,
+                             AtomPtr *url_return,
+                             int *version_return);
+int httpParseServerFirstLine(const char *buf, 
+                             int *status_return,
+                             int *version_return,
+                             AtomPtr *message_return);
+
+int findEndOfHeaders(const char *buf, int from, int to, int *body_return);
+
+int httpParseHeaders(int, AtomPtr, const char *, int, HTTPRequestPtr,
+                     AtomPtr*, int*, CacheControlPtr, 
+                     HTTPConditionPtr *, int*,
+                     time_t*, time_t*, time_t*, time_t*, time_t*,
+                     int*, int*, char**, AtomPtr*,
+                     HTTPRangePtr, HTTPRangePtr, char**, AtomPtr*, AtomPtr*);
+int httpFindHeader(AtomPtr header, const char *headers, int hlen,
+                   int *value_begin_return, int *value_end_return);
+int parseUrl(const char *url, int len,
+             int *x_return, int *y_return, int *port_return, int *z_return);
+int urlIsLocal(const char *url, int len);
+int urlIsSpecial(const char *url, int len);
+int parseChunkSize(const char *buf, int i, int end, int *chunk_size_return);
+int checkVia(AtomPtr, AtomPtr);

+ 1171 - 0
polipo/io.c

@@ -0,0 +1,1171 @@
+/*
+Copyright (c) 2003-2006 by Juliusz Chroboczek
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+#include "polipo.h"
+
+#ifdef HAVE_IPv6
+#ifdef IPV6_PREFER_TEMPADDR
+#define HAVE_IPV6_PREFER_TEMPADDR 1
+#endif
+#endif
+
+#ifdef HAVE_IPV6_PREFER_TEMPADDR
+int useTemporarySourceAddress = 1;
+#endif
+
+AtomPtr proxyOutgoingAddress = NULL;
+
+void
+preinitIo()
+{
+#ifdef HAVE_IPV6_PREFER_TEMPADDR
+    CONFIG_VARIABLE_SETTABLE(useTemporarySourceAddress, CONFIG_TRISTATE,
+                             configIntSetter,
+                             "Prefer IPv6 temporary source address.");
+#endif
+
+    CONFIG_VARIABLE(proxyOutgoingAddress, CONFIG_ATOM_LOWER,
+                    "The IP address which the proxy connects from.");
+
+#ifdef HAVE_WINSOCK
+    /* Load the winsock dll */
+    WSADATA wsaData;
+    WORD wVersionRequested = MAKEWORD(2, 2);
+    int err = WSAStartup( wVersionRequested, &wsaData );
+    if (err != 0) {
+        do_log_error(L_ERROR, err, "Couldn't load winsock dll");
+        exit(-1);
+    }
+#endif
+    return;
+}
+
+void
+initIo()
+{
+    return;
+}
+
+FdEventHandlerPtr
+do_stream(int operation, int fd, int offset, char *buf, int len,
+          int (*handler)(int, FdEventHandlerPtr, StreamRequestPtr),
+          void *data)
+{
+    assert(len > offset || (operation & (IO_END | IO_IMMEDIATE)));
+    return schedule_stream(operation, fd, offset, 
+                           NULL, 0, buf, len, NULL, 0, NULL, 0, NULL,
+                           handler, data);
+}
+
+FdEventHandlerPtr
+do_stream_2(int operation, int fd, int offset, 
+            char *buf, int len, char *buf2, int len2,
+            int (*handler)(int, FdEventHandlerPtr, StreamRequestPtr),
+            void *data)
+{
+    assert(len + len2 > offset || (operation & (IO_END | IO_IMMEDIATE)));
+    return schedule_stream(operation, fd, offset,
+                           NULL, 0, buf, len, buf2, len2, NULL, 0, NULL,
+                           handler, data);
+}
+
+FdEventHandlerPtr
+do_stream_3(int operation, int fd, int offset, 
+            char *buf, int len, char *buf2, int len2, char *buf3, int len3,
+            int (*handler)(int, FdEventHandlerPtr, StreamRequestPtr),
+            void *data)
+{
+    assert(len + len2 > offset || (operation & (IO_END | IO_IMMEDIATE)));
+    return schedule_stream(operation, fd, offset,
+                           NULL, 0, buf, len, buf2, len2, buf3, len3, NULL,
+                           handler, data);
+}
+
+FdEventHandlerPtr
+do_stream_h(int operation, int fd, int offset,
+            char *header, int hlen, char *buf, int len,
+            int (*handler)(int, FdEventHandlerPtr, StreamRequestPtr),
+            void *data)
+{
+    assert(hlen + len > offset || (operation & (IO_END | IO_IMMEDIATE)));
+    return schedule_stream(operation, fd, offset, 
+                           header, hlen, buf, len, NULL, 0, NULL, 0, NULL,
+                           handler, data);
+}
+
+FdEventHandlerPtr
+do_stream_buf(int operation, int fd, int offset, char **buf_location, int len,
+              int (*handler)(int, FdEventHandlerPtr, StreamRequestPtr),
+              void *data)
+{
+    assert((len > offset || (operation & (IO_END | IO_IMMEDIATE)))
+           && len <= CHUNK_SIZE);
+    return schedule_stream(operation, fd, offset,
+                           NULL, 0, *buf_location, len, 
+                           NULL, 0, NULL, 0, buf_location,
+                           handler, data);
+}
+
+static int
+chunkHeaderLen(int i)
+{
+    if(i <= 0)
+        return 0;
+    if(i < 0x10)
+        return 3;
+    else if(i < 0x100)
+        return 4;
+    else if(i < 0x1000)
+        return 5;
+    else if(i < 0x10000)
+        return 6;
+    else
+        abort();
+}
+
+static int
+chunkHeader(char *buf, int buflen, int i)
+{
+    int n;
+    if(i <= 0)
+        return 0;
+    n = snprintf(buf, buflen, "%x\r\n", i);
+    return n;
+}
+
+
+FdEventHandlerPtr
+schedule_stream(int operation, int fd, int offset,
+                char *header, int hlen,
+                char *buf, int len, char *buf2, int len2, char *buf3, int len3,
+                char **buf_location,
+                int (*handler)(int, FdEventHandlerPtr, StreamRequestPtr),
+                void *data)
+{
+    StreamRequestRec request;
+    FdEventHandlerPtr event;
+    int done;
+
+    request.operation = operation;
+    request.fd = fd;
+    if(len3) {
+        assert(hlen == 0 && buf_location == NULL);
+        request.u.b.len3 = len3;
+        request.u.b.buf3 = buf3;
+        request.operation |= IO_BUF3;
+    } else if(buf_location) {
+        assert(hlen == 0);
+        request.u.l.buf_location = buf_location;
+        request.operation |= IO_BUF_LOCATION;
+    } else {
+        request.u.h.hlen = hlen;
+        request.u.h.header = header;
+    }
+    request.buf = buf;
+    request.len = len;
+    request.buf2 = buf2;
+    request.len2 = len2;
+    if((operation & IO_CHUNKED) || 
+       (!(request.operation & (IO_BUF3 | IO_BUF_LOCATION)) && hlen > 0)) {
+        assert(offset == 0);
+        request.offset = -hlen;
+        if(operation & IO_CHUNKED)
+            request.offset += -chunkHeaderLen(len + len2);
+    } else {
+        request.offset = offset;
+    }
+    request.handler = handler;
+    request.data = data;
+    event = makeFdEvent(fd, 
+                        (operation & IO_MASK) == IO_WRITE ?
+                        POLLOUT : POLLIN, 
+                        do_scheduled_stream, 
+                        sizeof(StreamRequestRec), &request);
+    if(!event) {
+        done = (*handler)(-ENOMEM, NULL, &request);
+        assert(done);
+        return NULL;
+    }
+
+    if(!(operation & IO_NOTNOW)) {
+        done = event->handler(0, event);
+        if(done) {
+            free(event);
+            return NULL;
+        }
+    } 
+
+    if(operation & IO_IMMEDIATE) {
+        assert(hlen == 0 && !(operation & IO_CHUNKED));
+        done = (*handler)(0, event, &request);
+        if(done) {
+            free(event);
+            return NULL;
+        }
+    }
+    event = registerFdEventHelper(event);
+    return event;
+}
+
+static const char *endChunkTrailer = "\r\n0\r\n\r\n";
+
+int
+do_scheduled_stream(int status, FdEventHandlerPtr event)
+{
+    StreamRequestPtr request = (StreamRequestPtr)&event->data;
+    int rc, done, i;
+    struct iovec iov[6];
+    int chunk_header_len;
+    char chunk_header[10];
+    int len12 = request->len + request->len2;
+    int len123 = 
+        request->len + request->len2 + 
+        ((request->operation & IO_BUF3) ? request->u.b.len3 : 0);
+
+    if(status) {
+        done = request->handler(status, event, request);
+        return done;
+    }
+
+    i = 0;
+
+    if(request->offset < 0) {
+        assert((request->operation & (IO_MASK | IO_BUF3 | IO_BUF_LOCATION)) == 
+               IO_WRITE);
+        if(request->operation & IO_CHUNKED) {
+            chunk_header_len = chunkHeaderLen(len123);
+        } else {
+            chunk_header_len = 0;
+        }
+
+        if(request->offset < -chunk_header_len) {
+            assert(request->offset >= -(request->u.h.hlen + chunk_header_len));
+            iov[i].iov_base = request->u.h.header;
+            iov[i].iov_len = -request->offset - chunk_header_len;
+            i++;
+        }
+
+        if(chunk_header_len > 0) {
+            chunkHeader(chunk_header, 10, len123);
+            if(request->offset < -chunk_header_len) {
+                iov[i].iov_base = chunk_header;
+                iov[i].iov_len = chunk_header_len;
+            } else {
+                iov[i].iov_base = chunk_header + 
+                    chunk_header_len + request->offset;
+                iov[i].iov_len = -request->offset;
+            }
+            i++;
+        }
+    }
+
+    if(request->len > 0) {
+        if(request->buf == NULL && 
+           (request->operation & IO_BUF_LOCATION)) {
+            assert(*request->u.l.buf_location == NULL);
+            request->buf = *request->u.l.buf_location = get_chunk();
+            if(request->buf == NULL) {
+                done = request->handler(-ENOMEM, event, request);
+                return done;
+            }
+        }
+        if(request->offset <= 0) {
+            iov[i].iov_base = request->buf;
+            iov[i].iov_len = request->len;
+            i++;
+        } else if(request->offset < request->len) {
+            iov[i].iov_base = request->buf + request->offset;
+            iov[i].iov_len = request->len - request->offset;
+            i++;
+        }
+    }
+
+    if(request->len2 > 0) {
+        if(request->offset <= request->len) {
+            iov[i].iov_base = request->buf2;
+            iov[i].iov_len = request->len2;
+            i++;
+        } else if(request->offset < request->len + request->len2) {
+            iov[i].iov_base = request->buf2 + request->offset - request->len;
+            iov[i].iov_len = request->len2 - request->offset + request->len;
+            i++;
+        }
+    }
+
+    if((request->operation & IO_BUF3) && request->u.b.len3 > 0) {
+        if(request->offset <= len12) {
+            iov[i].iov_base = request->u.b.buf3;
+            iov[i].iov_len = request->u.b.len3;
+            i++;
+        } else if(request->offset < len12 + request->u.b.len3) {
+            iov[i].iov_base = request->u.b.buf3 + request->offset - len12;
+            iov[i].iov_len = request->u.b.len3 - request->offset + len12;
+            i++;
+        }
+    }
+
+    if((request->operation & IO_CHUNKED)) {
+        int l;
+        const char *trailer;
+        if(request->operation & IO_END) {
+            if(len123 == 0) {
+                trailer = endChunkTrailer + 2;
+                l = 5;
+            } else {
+                trailer = endChunkTrailer;
+                l = 7;
+            }
+        } else {
+            trailer = endChunkTrailer;
+            l = 2;
+        }
+
+        if(request->offset <= len123) {
+            iov[i].iov_base = (char*)trailer;
+            iov[i].iov_len = l;
+            i++;
+        } else if(request->offset < len123 + l) {
+            iov[i].iov_base = 
+                (char*)endChunkTrailer + request->offset - len123;
+            iov[i].iov_len = l - request->offset + len123;
+            i++;
+        }
+    }
+
+    assert(i > 0);
+
+    if((request->operation & IO_MASK) == IO_WRITE) {
+        if(i > 1) 
+            rc = WRITEV(request->fd, iov, i);
+        else
+            rc = WRITE(request->fd, iov[0].iov_base, iov[0].iov_len);
+    } else {
+        if(i > 1) 
+            rc = READV(request->fd, iov, i);
+        else
+            rc = READ(request->fd, iov[0].iov_base, iov[0].iov_len);
+    }
+
+    if(rc > 0) {
+        request->offset += rc;
+        if(request->offset < 0) return 0;
+        done = request->handler(0, event, request);
+        return done;
+    } else if(rc == 0 || errno == EPIPE) {
+        done = request->handler(1, event, request);
+    } else if(errno == EAGAIN || errno == EINTR) {
+        return 0;
+    } else if(errno == EFAULT || errno == EBADF) {
+        abort();
+    } else {
+        done = request->handler(-errno, event, request);
+    }
+    assert(done);
+    return done;
+}
+
+int
+streamRequestDone(StreamRequestPtr request)
+{
+    int len123 = 
+        request->len + request->len2 +
+        ((request->operation & IO_BUF3) ? request->u.b.len3 : 0);
+
+    if(request->offset < 0)
+        return 0;
+    else if(request->offset < len123)
+        return 0;
+    else if(request->operation & IO_CHUNKED) {
+        if(request->operation & IO_END) {
+            if(request->offset < len123 + (len123 ? 7 : 5))
+                return 0;
+        } else {
+            if(request->offset < len123 + 2)
+                return 0;
+        }
+    }
+
+    return 1;
+}
+
+static int
+serverSocket_outgoingIP(int fd)
+{
+        int rc;
+        unsigned long int bind_addr_saddr = inet_addr (proxyOutgoingAddress->string);
+        struct sockaddr_in local_sockaddr_in[] = {{ 0 }};
+
+        local_sockaddr_in->sin_family = AF_INET;
+        local_sockaddr_in->sin_addr.s_addr = bind_addr_saddr;
+        local_sockaddr_in->sin_port = htons (0);
+
+        rc = bind(fd, (struct sockaddr *)local_sockaddr_in, sizeof (struct sockaddr));
+        if(rc != 0) {
+            do_log_error(L_WARN, errno, "Couldn't bind outgoing IP %s", proxyOutgoingAddress->string);
+        }
+
+        return rc;
+}
+
+static int
+serverSocket(int af)
+{
+    int fd, rc;
+    if(af == 4) {
+        fd = socket(PF_INET, SOCK_STREAM, 0);
+    } else if(af == 6) {
+#ifdef HAVE_IPv6
+        fd = socket(PF_INET6, SOCK_STREAM, 0);
+#else
+        fd = -1;
+        errno = EAFNOSUPPORT;
+#endif
+    } else {
+        abort();
+    }
+
+    if(fd >= 0) {
+        if(proxyOutgoingAddress != NULL) {
+            serverSocket_outgoingIP(fd);
+        }
+
+        rc = setNonblocking(fd, 1);
+        if(rc < 0) {
+            int errno_save = errno;
+            CLOSE(fd);
+            errno = errno_save;
+            return -1;
+        }
+#ifdef HAVE_IPV6_PREFER_TEMPADDR
+	if (af == 6 && useTemporarySourceAddress != 1) {
+            int value;
+            value = (useTemporarySourceAddress == 2) ? 1 : 0;
+            rc = setsockopt(fd, IPPROTO_IPV6, IPV6_PREFER_TEMPADDR,
+                            &value, sizeof(value));
+            if (rc < 0) {
+                /* no error, warning only */
+                do_log_error(L_WARN, errno, "Couldn't set IPV6CTL_USETEMPADDR");
+            }
+	}
+
+#endif
+    }
+    return fd;
+}
+
+FdEventHandlerPtr
+do_connect(AtomPtr addr, int index, int port,
+           int (*handler)(int, FdEventHandlerPtr, ConnectRequestPtr),
+           void *data)
+{
+    ConnectRequestRec request;
+    FdEventHandlerPtr event;
+    int done, fd, af;
+
+    assert(addr->length > 0 && addr->string[0] == DNS_A);
+    assert(addr->length % sizeof(HostAddressRec) == 1);
+
+    if(index >= (addr->length - 1)/ sizeof(HostAddressRec))
+        index = 0;
+
+    request.firstindex = index;
+    request.port = port;
+    request.handler = handler;
+    request.data = data;
+ again:
+    af = addr->string[1 + index * sizeof(HostAddressRec)];
+    fd = serverSocket(af);
+
+    request.fd = fd;
+    request.af = af;
+    request.addr = addr;
+    request.index = index;
+
+    if(fd < 0) {
+        int n = (addr->length - 1) / sizeof(HostAddressRec);
+        if(errno == EAFNOSUPPORT || errno == EPROTONOSUPPORT) {
+            if((index + 1) % n != request.firstindex) {
+                index = (index + 1) % n;
+                goto again;
+            }
+        }
+        do_log_error(L_ERROR, errno, "Couldn't create socket");
+        done = (*handler)(-errno, NULL, &request);
+        assert(done);
+        return NULL;
+    }
+
+    /* POLLIN is apparently needed on Windows */
+    event = registerFdEvent(fd, POLLIN | POLLOUT,
+                            do_scheduled_connect,
+                            sizeof(ConnectRequestRec), &request);
+    if(event == NULL) {
+        done = (*handler)(-ENOMEM, NULL, &request);
+        assert(done);
+        return NULL;
+    }
+
+    done = event->handler(0, event);
+    if(done) {
+        unregisterFdEvent(event);
+        return NULL;
+    }
+    return event;
+}
+
+int
+do_scheduled_connect(int status, FdEventHandlerPtr event)
+{
+    ConnectRequestPtr request = (ConnectRequestPtr)&event->data;
+    AtomPtr addr = request->addr;
+    int done;
+    int rc;
+    HostAddressPtr host;
+    struct sockaddr_in servaddr;
+#ifdef HAVE_IPv6
+    struct sockaddr_in6 servaddr6;
+#endif
+
+    assert(addr->length > 0 && addr->string[0] == DNS_A);
+    assert(addr->length % sizeof(HostAddressRec) == 1);
+    assert(request->index < (addr->length - 1) / sizeof(HostAddressRec));
+
+    if(status) {
+        done = request->handler(status, event, request);
+        if(done) {
+            releaseAtom(addr);
+            request->addr = NULL;
+            return 1;
+        }
+        return 0;
+    }
+
+ again:
+    host = (HostAddressPtr)&addr->string[1 + 
+                                         request->index * 
+                                         sizeof(HostAddressRec)];
+    if(host->af != request->af) {
+        int newfd;
+        /* Ouch.  Our socket has a different protocol than the host
+           address. */
+        CLOSE(request->fd);
+        newfd = serverSocket(host->af);
+        if(newfd < 0) {
+            if(errno == EAFNOSUPPORT || errno == EPROTONOSUPPORT) {
+                int n = request->addr->length / sizeof(HostAddressRec);
+                if((request->index + 1) % n != request->firstindex) {
+                    request->index = (request->index + 1) % n;
+                    goto again;
+                }
+            }
+            request->fd = -1;
+            done = request->handler(-errno, event, request);
+            assert(done);
+            return 1;
+        }
+        if(newfd != request->fd) {
+            request->fd = dup2(newfd, request->fd);
+            CLOSE(newfd);
+            if(request->fd < 0) {
+                done = request->handler(-errno, event, request);
+                assert(done);
+                return 1;
+            }
+        }
+        request->af = host->af;
+    }
+    switch(host->af) {
+    case 4:
+        memset(&servaddr, 0, sizeof(servaddr));
+        servaddr.sin_family = AF_INET;
+        servaddr.sin_port = htons(request->port);
+        memcpy(&servaddr.sin_addr, &host->data, sizeof(struct in_addr));
+        rc = connect(request->fd,
+                     (struct sockaddr*)&servaddr, sizeof(servaddr));
+        break;
+    case 6:
+#ifdef HAVE_IPv6
+        memset(&servaddr6, 0, sizeof(servaddr6));
+        servaddr6.sin6_family = AF_INET6;
+        servaddr6.sin6_port = htons(request->port);
+        memcpy(&servaddr6.sin6_addr, &host->data, sizeof(struct in6_addr));
+        rc = connect(request->fd,
+                     (struct sockaddr*)&servaddr6, sizeof(servaddr6));
+#else
+        rc = -1;
+        errno = EAFNOSUPPORT;
+#endif
+        break;
+    default:
+        abort();
+    }
+        
+    if(rc >= 0 || errno == EISCONN) {
+        done = request->handler(1, event, request);
+        assert(done);
+        releaseAtom(request->addr);
+        request->addr = NULL;
+        return 1;
+    }
+
+    if(errno == EINPROGRESS || errno == EINTR) {
+        return 0;
+    } else if(errno == EFAULT || errno == EBADF) {
+        abort();
+    } else {
+        int n = request->addr->length / sizeof(HostAddressRec);
+        if((request->index + 1) % n != request->firstindex) {
+            request->index = (request->index + 1) % n;
+            goto again;
+        }
+        done = request->handler(-errno, event, request);
+        assert(done);
+        releaseAtom(request->addr);
+        request->addr = NULL;
+        return 1;
+    }
+}
+
+FdEventHandlerPtr
+do_accept(int fd,
+          int (*handler)(int, FdEventHandlerPtr, AcceptRequestPtr),
+          void *data)
+{
+    FdEventHandlerPtr event;
+    int done;
+
+    event = schedule_accept(fd, handler, data);
+    if(event == NULL) {
+        done = (*handler)(-ENOMEM, NULL, NULL);
+        assert(done);
+    }
+
+    /* But don't invoke it now - this will delay accept if under load. */
+    return event;
+}
+
+FdEventHandlerPtr
+schedule_accept(int fd,
+                int (*handler)(int, FdEventHandlerPtr, AcceptRequestPtr),
+                void *data)
+{
+    FdEventHandlerPtr event;
+    AcceptRequestRec request;
+    int done;
+
+    request.fd = fd;
+    request.handler = handler;
+    request.data = data;
+    event = registerFdEvent(fd, POLLIN, 
+                            do_scheduled_accept, sizeof(request), &request);
+    if(!event) {
+        done = (*handler)(-ENOMEM, NULL, NULL);
+        assert(done);
+    }
+    return event;
+}
+
+int
+do_scheduled_accept(int status, FdEventHandlerPtr event)
+{
+    AcceptRequestPtr request = (AcceptRequestPtr)&event->data;
+    int rc, done;
+    socklen_t len;
+    struct sockaddr_in addr;
+
+    if(status) {
+        done = request->handler(status, event, request);
+        if(done) return done;
+    }
+
+    len = sizeof(struct sockaddr_in);
+
+    rc = accept(request->fd, (struct sockaddr*)&addr, &len);
+
+    if(rc >= 0)
+        done = request->handler(rc, event, request);
+    else
+        done = request->handler(-errno, event, request);
+    return done;
+}
+
+FdEventHandlerPtr
+create_listener(char *address, int port,
+                int (*handler)(int, FdEventHandlerPtr, AcceptRequestPtr),
+                void *data)
+{
+    int fd, rc;
+    int one = 1;
+    int done;
+    struct sockaddr_in addr;
+#ifdef HAVE_IPv6
+    int inet6 = 1;
+    struct sockaddr_in6 addr6;
+#else
+    int inet6 = 0;
+#endif
+
+    if(inet6 && address) {
+        struct in_addr buf;
+        rc = inet_aton(address, &buf);
+        if(rc == 1)
+            inet6 = 0;
+    }
+    fd = -1;
+    errno = EAFNOSUPPORT;
+
+#ifdef HAVE_IPv6
+    if(inet6) {
+        fd = socket(PF_INET6, SOCK_STREAM, 0);
+    }
+#endif
+
+    if(fd < 0 && (errno == EPROTONOSUPPORT || errno == EAFNOSUPPORT)) {
+        inet6 = 0;
+        fd = socket(PF_INET, SOCK_STREAM, 0);
+    }
+
+    if(fd < 0) {
+        done = (*handler)(-errno, NULL, NULL);
+        assert(done);
+        return NULL;
+    }
+
+#ifndef WIN32
+    /* on WIN32 SO_REUSEADDR allows two sockets bind to the same port */
+    rc = setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (char *)&one, sizeof(one));
+    if(rc < 0) do_log_error(L_WARN, errno, "Couldn't set SO_REUSEADDR");
+#endif
+
+    if(inet6) {
+#ifdef HAVE_IPv6
+        rc = setV6only(fd, 0);
+        if(rc < 0)
+            /* Reportedly OpenBSD returns an error for that.  So only
+               log it as a debugging message. */
+            do_log_error(D_CLIENT_CONN, errno, "Couldn't reset IPV6_V6ONLY");
+
+        memset(&addr6, 0, sizeof(addr6));
+        rc = inet_pton(AF_INET6, address, &addr6.sin6_addr);
+        if(rc != 1) {
+            done = (*handler)(rc == 0 ? -ESYNTAX : -errno, NULL, NULL);
+            assert(done);
+            return NULL;
+        }
+        addr6.sin6_family = AF_INET6;
+        addr6.sin6_port = htons(port);
+        rc = bind(fd, (struct sockaddr*)&addr6, sizeof(addr6));
+#else
+        rc = -1;
+        errno = EAFNOSUPPORT;
+#endif
+    } else {
+        memset(&addr, 0, sizeof(addr));
+        rc = inet_aton(address, &addr.sin_addr);
+        if(rc != 1) {
+            done = (*handler)(rc == 0 ? -ESYNTAX : -errno, NULL, NULL);
+            assert(done);
+            return NULL;
+        }
+        addr.sin_family = AF_INET;
+        addr.sin_port = htons(port);
+        rc = bind(fd, (struct sockaddr*)&addr, sizeof(addr));
+    }
+
+    if(rc < 0) {
+        do_log_error(L_ERROR, errno, "Couldn't bind");
+        CLOSE(fd);
+        done = (*handler)(-errno, NULL, NULL);
+        assert(done);
+        return NULL;
+    }
+
+    rc = setNonblocking(fd, 1);
+    if(rc < 0) {
+        do_log_error(L_ERROR, errno, "Couldn't set non blocking mode");
+        CLOSE(fd);
+        done = (*handler)(-errno, NULL, NULL);
+        assert(done);
+        return NULL;
+    }
+        
+    rc = listen(fd, 1024);
+    if(rc < 0) {
+        do_log_error(L_ERROR, errno, "Couldn't listen");
+        CLOSE(fd);
+        done = (*handler)(-errno, NULL, NULL);
+        assert(done);
+        return NULL;
+    }
+
+    do_log(L_INFO, "Established listening socket on port %d.\n", port);
+
+    return schedule_accept(fd, handler, data);
+}
+
+#ifndef SOL_TCP
+/* BSD */
+#define SOL_TCP IPPROTO_TCP
+#endif
+
+int
+setNonblocking(int fd, int nonblocking)
+{
+#ifdef WIN32 /*MINGW*/
+    return win32_setnonblocking(fd, nonblocking);
+#else
+    int rc;
+    rc = fcntl(fd, F_GETFL, 0);
+    if(rc < 0)
+        return -1;
+
+    rc = fcntl(fd, F_SETFL, nonblocking?(rc | O_NONBLOCK):(rc & ~O_NONBLOCK));
+    if(rc < 0)
+        return -1;
+
+    return 0;
+#endif
+}
+
+int
+setNodelay(int fd, int nodelay)
+{
+    int val = nodelay ? 1 : 0;
+    int rc;
+    rc = setsockopt(fd, SOL_TCP, TCP_NODELAY, (char *)&val, sizeof(val));
+    if(rc < 0)
+        return -1;
+    return 0;
+}
+
+#ifdef IPV6_V6ONLY
+int
+setV6only(int fd, int v6only)
+{
+    int val = v6only ? 1 : 0;
+    int rc;
+    rc = setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, (char *)&val, sizeof(val));
+    if(rc < 0)
+        return -1;
+    return 0;
+}
+#else
+int
+setV6only(int fd, int v6only)
+{
+    return 0;
+}
+#endif
+
+typedef struct _LingeringClose {
+    int fd;
+    FdEventHandlerPtr handler;
+    TimeEventHandlerPtr timeout;
+} LingeringCloseRec, *LingeringClosePtr;
+
+static int
+lingeringCloseTimeoutHandler(TimeEventHandlerPtr event)
+{
+    LingeringClosePtr l = *(LingeringClosePtr*)event->data;
+    assert(l->timeout == event);
+    l->timeout = NULL;
+    if(l->handler)
+        pokeFdEvent(l->fd, -ESHUTDOWN, POLLIN | POLLOUT);
+    else {
+        CLOSE(l->fd);
+        free(l);
+    }
+    return 1;
+}
+
+static int
+lingeringCloseHandler(int status, FdEventHandlerPtr event)
+{
+    LingeringClosePtr l = *(LingeringClosePtr*)event->data;
+    char buf[17];
+    int rc;
+
+    assert(l->handler == event);
+
+    l->handler = NULL;
+    if(status && status != -EDOGRACEFUL)
+        goto done;
+
+    rc = READ(l->fd, &buf, 17);
+    if(rc == 0 || (rc < 0 && errno != EAGAIN && errno != EINTR))
+        goto done;
+
+    /* The client is still sending data.  Ignore it in order to let
+       TCP's flow control do its work.  The timeout will close the
+       connection. */
+    return 1;
+
+ done:
+    if(l->timeout) {
+        cancelTimeEvent(l->timeout);
+        l->timeout = NULL;
+    }
+    CLOSE(l->fd);
+    free(l);
+    return 1;
+}
+
+int
+lingeringClose(int fd)
+{
+    int rc;
+    LingeringClosePtr l;
+
+    rc = shutdown(fd, 1);
+    if(rc < 0) {
+        if(errno != ENOTCONN) {
+            do_log_error(L_ERROR, errno, "Shutdown failed");
+        } else if(errno == EFAULT || errno == EBADF) {
+            abort();
+        }
+        CLOSE(fd);
+        return 1;
+    }
+
+    l = malloc(sizeof(LingeringCloseRec));
+    if(l == NULL)
+        goto fail;
+    l->fd = fd;
+    l->handler = NULL;
+    l->timeout = NULL;
+
+    l->timeout = scheduleTimeEvent(10, lingeringCloseTimeoutHandler,
+                                   sizeof(LingeringClosePtr), &l);
+    if(l->timeout == NULL) {
+        free(l);
+        goto fail;
+    }
+
+    l->handler = registerFdEvent(fd, POLLIN,
+                                 lingeringCloseHandler,
+                                 sizeof(LingeringClosePtr), &l);
+    if(l->handler == NULL) {
+        do_log(L_ERROR, "Couldn't schedule lingering close handler.\n");
+        /* But don't close -- the timeout will do its work. */
+    }
+    return 1;
+
+ fail:
+    do_log(L_ERROR, "Couldn't schedule lingering close.\n");
+    CLOSE(fd);
+    return 1;
+}
+
+NetAddressPtr
+parseNetAddress(AtomListPtr list)
+{
+    NetAddressPtr nl;
+    int i, rc, rc6;
+    char buf[100];
+    struct in_addr ina;
+#ifdef HAVE_IPv6
+    struct in6_addr ina6;
+#endif
+
+    nl = malloc((list->length + 1) * sizeof(NetAddressRec));
+    if(nl == NULL) {
+        do_log(L_ERROR, "Couldn't allocate network list.\n");
+        return NULL;
+    }
+
+    for(i = 0; i < list->length; i++) {
+        int prefix;
+        char *s = list->list[i]->string, *p;
+        int n = list->list[i]->length;
+        char *suffix;
+
+        while(*s == ' ' || *s == '\t') {
+            s++;
+            n--;
+        }
+
+        if(n >= 100) {
+            do_log(L_ERROR, "Network name too long.\n");
+            goto fail;
+        }
+        p = memchr(s, '/', n);
+        if(p) {
+            memcpy(buf, s, p - s);
+            buf[p - s] = '\0';
+            prefix = strtol(p + 1, &suffix, 10);
+        } else {
+            char *s1, *s2;
+            prefix = -1;
+            strcpy(buf, s);
+            s1 = strchr(s, ' ');
+            s2 = strchr(s, '\t');
+            if(s1 == NULL) suffix = s2;
+            else if(s2 == NULL) suffix = s1;
+            else if(s1 < s2) suffix = s1;
+            else suffix = s2;
+            if(suffix == NULL)
+                suffix = s + n;
+        }
+
+        if(!isWhitespace(suffix)) {
+            do_log(L_ERROR, "Couldn't parse network %s.\n", buf);
+            goto fail;
+        }
+
+        rc = 0; rc6 = 0;
+        rc = inet_aton(buf, &ina);
+#ifdef HAVE_IPv6
+        if(rc == 0) {
+            rc6 = inet_pton(AF_INET6, buf, &ina6);
+        }
+#endif
+        if(rc == 0 && rc6 == 0) {
+            do_log(L_ERROR, "Couldn't parse network %s.\n", buf);
+            goto fail;
+        }
+        nl[i].prefix = prefix;
+        if(rc) {
+            nl[i].af = 4;
+            memcpy(nl[i].data, &ina, 4);
+        } else {
+#ifdef HAVE_IPv6
+            nl[i].af = 6;
+            memcpy(nl[i].data, &ina6, 16);
+#else
+            abort();
+#endif
+        }
+    }
+    nl[i].af = 0;
+    return nl;
+
+ fail:
+    free(nl);
+    return NULL;
+}
+
+/* Returns 1 if the first n bits of a and b are equal */
+static int
+bitmatch(const unsigned char *a, const unsigned char *b, int n)
+{
+    if(n >= 8) {
+        if(memcmp(a, b, n / 8) != 0)
+            return 0;
+    }
+
+    if(n % 8 != 0) {
+        int mask = (~0) << (8 - n % 8);
+        if((a[n / 8] & mask) != (b[n / 8] & mask))
+            return 0;
+    }
+
+    return 1;
+}
+
+/* Returns 1 if the address in data is in list */
+static int
+match(int af, unsigned char *data, NetAddressPtr list)
+{
+    int i;
+#ifdef HAVE_IPv6
+    static const unsigned char v6mapped[] =
+        { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xFF, 0xFF };
+#endif
+
+    i = 0;
+    while(list[i].af != 0) {
+        if(af == 4 && list[i].af == 4) {
+            if(bitmatch(data, list[i].data,
+                        list[i].prefix >= 0 ? list[i].prefix : 32))
+                return 1;
+#ifdef HAVE_IPv6
+        } else if(af == 6 && list[i].af == 6) {
+            if(bitmatch(data, list[i].data,
+                        list[i].prefix >= 0 ? list[i].prefix : 128))
+                return 1;
+        } else if(af == 6 && list[i].af == 4) {
+            if(bitmatch(data, v6mapped, 96)) {
+                if(bitmatch(data + 12, list[i].data,
+                            list[i].prefix >= 0 ? list[i].prefix : 32))
+                    return 1;
+            }
+        } else if(af == 4 && list[i].af == 6) {
+            if(bitmatch(list[i].data, v6mapped, 96)) {
+                if(bitmatch(data, list[i].data + 12,
+                            list[i].prefix >= 96 ?
+                            list[i].prefix - 96 : 32))
+                    return 1;
+            }
+#endif
+        } else {
+            abort();
+        }
+        i++;
+    }
+    return 0;
+}
+
+int
+netAddressMatch(int fd, NetAddressPtr list)
+{
+    int rc;
+    socklen_t len;
+    struct sockaddr_in sain;
+#ifdef HAVE_IPv6
+    struct sockaddr_in6 sain6;
+#endif
+
+    len = sizeof(sain);
+    rc = getpeername(fd, (struct sockaddr*)&sain, &len);
+    if(rc < 0) {
+        do_log_error(L_ERROR, errno, "Couldn't get peer name");
+        return -1;
+    }
+
+    if(sain.sin_family == AF_INET) {
+        return match(4, (unsigned char*)&sain.sin_addr, list);
+#ifdef HAVE_IPv6
+    } else if(sain.sin_family == AF_INET6) {
+        len = sizeof(sain6);
+        rc = getpeername(fd, (struct sockaddr*)&sain6, &len);
+        if(rc < 0) {
+            do_log_error(L_ERROR, errno, "Couldn't get peer name");
+            return -1;
+        }
+        if(sain6.sin6_family != AF_INET6) {
+            do_log(L_ERROR, "Inconsistent peer name");
+            return -1;
+        }
+        return match(6, (unsigned char*)&sain6.sin6_addr, list);
+#endif
+    } else {
+        do_log(L_ERROR, "Unknown address family %d\n", sain.sin_family);
+        return -1;
+    }
+    return 0;
+}
+
+
+        
+        
+
+    

+ 161 - 0
polipo/io.h

@@ -0,0 +1,161 @@
+/*
+Copyright (c) 2003-2006 by Juliusz Chroboczek
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+/* request->operation */
+#define IO_READ 0
+#define IO_WRITE 1
+#define IO_MASK 0xFF
+/* Do not initiate operation now -- wait for the poll loop. */
+#define IO_NOTNOW 0x100
+/* Call the progress handler once if no data arrives immediately. */
+#define IO_IMMEDIATE 0x200
+/* Emit a chunk length before every write operation */
+#define IO_CHUNKED 0x400
+/* Emit a zero-length chunk at the end if chunked */
+#define IO_END 0x800
+
+/* Internal -- header is really buf3 */
+#define IO_BUF3 0x1000
+/* Internal -- header is really buf_location */
+#define IO_BUF_LOCATION 0x2000
+
+typedef struct _StreamRequest {
+    short operation;
+    short fd;
+    int offset;
+    int len;
+    int len2;
+    union {
+        struct {
+            int hlen;
+            char *header;
+        } h;
+        struct {
+            int len3;
+            char *buf3;
+        } b;
+        struct {
+            char **buf_location;
+        } l;
+    } u;
+    char *buf;
+    char *buf2;
+    int (*handler)(int, FdEventHandlerPtr, struct _StreamRequest*);
+    void *data;
+} StreamRequestRec, *StreamRequestPtr;
+
+typedef struct _ConnectRequest {
+    int fd;
+    int af;
+    struct _Atom *addr;
+    int firstindex;
+    int index;
+    int port;
+    int (*handler)(int, FdEventHandlerPtr, struct _ConnectRequest*);
+    void *data;
+} ConnectRequestRec, *ConnectRequestPtr;
+
+typedef struct _AcceptRequest {
+    int fd;
+    int (*handler)(int, FdEventHandlerPtr, struct _AcceptRequest*);
+    void *data;
+} AcceptRequestRec, *AcceptRequestPtr;
+
+void preinitIo();
+void initIo();
+
+FdEventHandlerPtr
+do_stream(int operation, int fd, int offset, char *buf, int len,
+          int (*handler)(int, FdEventHandlerPtr, StreamRequestPtr),
+          void *data);
+
+FdEventHandlerPtr
+do_stream_h(int operation, int fd, int offset, 
+            char *header, int hlen, char *buf, int len,
+            int (*handler)(int, FdEventHandlerPtr, StreamRequestPtr),
+            void *data);
+
+FdEventHandlerPtr
+do_stream_2(int operation, int fd, int offset, 
+            char *buf, int len, char *buf2, int len2,
+            int (*handler)(int, FdEventHandlerPtr, StreamRequestPtr),
+            void *data);
+
+FdEventHandlerPtr
+do_stream_3(int operation, int fd, int offset, 
+            char *buf, int len, char *buf2, int len2, char *buf3, int len3,
+            int (*handler)(int, FdEventHandlerPtr, StreamRequestPtr),
+            void *data);
+
+FdEventHandlerPtr
+do_stream_buf(int operation, int fd, int offset, char **buf_location, int len,
+              int (*handler)(int, FdEventHandlerPtr, StreamRequestPtr),
+              void *data);
+
+FdEventHandlerPtr
+schedule_stream(int operation, int fd, int offset,
+                char *header, int hlen,
+                char *buf, int len, char *buf2, int len2, char *buf3, int len3,
+                char **buf_location,
+                int (*handler)(int, FdEventHandlerPtr, StreamRequestPtr),
+                void *data);
+
+int do_scheduled_stream(int, FdEventHandlerPtr);
+int streamRequestDone(StreamRequestPtr);
+
+FdEventHandlerPtr
+do_connect(struct _Atom *addr, int index, int port,
+           int (*handler)(int, FdEventHandlerPtr, ConnectRequestPtr),
+           void *data);
+
+int do_scheduled_connect(int, FdEventHandlerPtr event);
+
+FdEventHandlerPtr
+do_accept(int fd,
+          int (*handler)(int, FdEventHandlerPtr, AcceptRequestPtr),
+          void* data);
+
+FdEventHandlerPtr 
+schedule_accept(int fd,
+                int (*handler)(int, FdEventHandlerPtr, AcceptRequestPtr),
+                void* data);
+
+int do_scheduled_accept(int, FdEventHandlerPtr event);
+
+FdEventHandlerPtr
+create_listener(char *address, int port,
+                int (*handler)(int, FdEventHandlerPtr, AcceptRequestPtr),
+                void *data);
+int setNonblocking(int fd, int nonblocking);
+int setNodelay(int fd, int nodelay);
+int setV6only(int fd, int v6only);
+int lingeringClose(int fd);
+
+typedef struct _NetAddress {
+    int prefix;
+    int af;
+    unsigned char data[16];
+} NetAddressRec, *NetAddressPtr;
+
+NetAddressPtr parseNetAddress(AtomListPtr list);
+int netAddressMatch(int fd, NetAddressPtr list) ATTRIBUTE ((pure));
+

+ 746 - 0
polipo/local.c

@@ -0,0 +1,746 @@
+/*
+Copyright (c) 2003-2006 by Juliusz Chroboczek
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+#include "polipo.h"
+
+int disableLocalInterface = 0;
+int disableConfiguration = 0;
+int disableIndexing = 1;
+int disableServersList = 1;
+
+AtomPtr atomInitForbidden;
+AtomPtr atomReopenLog;
+AtomPtr atomDiscardObjects;
+AtomPtr atomWriteoutObjects;
+AtomPtr atomFreeChunkArenas;
+
+void
+preinitLocal()
+{
+    atomInitForbidden = internAtom("init-forbidden");
+    atomReopenLog = internAtom("reopen-log");
+    atomDiscardObjects = internAtom("discard-objects");
+    atomWriteoutObjects = internAtom("writeout-objects");
+    atomFreeChunkArenas = internAtom("free-chunk-arenas");
+
+    /* These should not be settable for obvious reasons */
+    CONFIG_VARIABLE(disableLocalInterface, CONFIG_BOOLEAN,
+                    "Disable the local configuration pages.");
+    CONFIG_VARIABLE(disableConfiguration, CONFIG_BOOLEAN,
+                    "Disable reconfiguring Polipo at runtime.");
+    CONFIG_VARIABLE(disableIndexing, CONFIG_BOOLEAN,
+                    "Disable indexing of the local cache.");
+    CONFIG_VARIABLE(disableServersList, CONFIG_BOOLEAN,
+                    "Disable the list of known servers.");
+}
+
+static void fillSpecialObject(ObjectPtr, void (*)(FILE*, char*), void*);
+
+int
+httpLocalRequest(ObjectPtr object, int method, int from, int to,
+                 HTTPRequestPtr requestor, void *closure)
+{
+    if(object->requestor == NULL)
+        object->requestor = requestor;
+
+    if(!disableLocalInterface && urlIsSpecial(object->key, object->key_size))
+        return httpSpecialRequest(object, method, from, to, 
+                                  requestor, closure);
+
+    if(method >= METHOD_POST) {
+        abortObject(object, 405, internAtom("Method not allowed"));
+    } else if(object->flags & OBJECT_INITIAL) {
+        /* objectFillFromDisk already did the real work but we have to
+           make sure we don't get into an infinite loop. */
+        abortObject(object, 404, internAtom("Not found"));
+    }
+    object->age = current_time.tv_sec;
+    object->date = current_time.tv_sec;
+
+    object->flags &= ~OBJECT_VALIDATING;
+    notifyObject(object);
+    return 1;
+}
+
+void
+alternatingHttpStyle(FILE *out, char *id)
+{
+    fprintf(out,
+            "<style type=\"text/css\">\n"
+            "#%s tbody tr.even td { background-color: #eee; }\n"
+            "#%s tbody tr.odd  td { background-color: #fff; }\n"
+            "</style>\n", id, id);
+}
+
+static void
+printConfig(FILE *out, char *dummy)
+{
+    fprintf(out,
+            "<!DOCTYPE HTML PUBLIC "
+            "\"-//W3C//DTD HTML 4.01 Transitional//EN\" "
+            "\"http://www.w3.org/TR/html4/loose.dtd\">\n"
+            "<html><head>\n"
+            "<title>Polipo configuration</title>\n"
+            "</head><body>\n"
+            "<h1>Polipo configuration</h1>\n");
+    printConfigVariables(out, 1);
+    fprintf(out, "<p><a href=\"/polipo/\">back</a></p>");
+    fprintf(out, "</body></html>\n");
+}
+
+#ifndef NO_DISK_CACHE
+
+static void
+recursiveIndexDiskObjects(FILE *out, char *root)
+{
+    indexDiskObjects(out, root, 1);
+}
+
+static void
+plainIndexDiskObjects(FILE *out, char *root)
+{
+    indexDiskObjects(out, root, 0);
+}
+#endif
+
+static void
+serversList(FILE *out, char *dummy)
+{
+    listServers(out);
+}
+
+static int
+matchUrl(char *base, ObjectPtr object)
+{
+    int n = strlen(base);
+    if(object->key_size < n)
+        return 0;
+    if(memcmp(base, object->key, n) != 0)
+        return 0;
+    return (object->key_size == n) || (((char*)object->key)[n] == '?');
+}
+    
+int 
+httpSpecialRequest(ObjectPtr object, int method, int from, int to,
+                   HTTPRequestPtr requestor, void *closure)
+{
+    char buffer[1024];
+    int hlen;
+
+    if(method >= METHOD_POST) {
+        return httpSpecialSideRequest(object, method, from, to,
+                                      requestor, closure);
+    }
+
+    if(!(object->flags & OBJECT_INITIAL)) {
+        privatiseObject(object, 0);
+        supersedeObject(object);
+        object->flags &= ~(OBJECT_VALIDATING | OBJECT_INPROGRESS);
+        notifyObject(object);
+        return 1;
+    }
+
+    hlen = snnprintf(buffer, 0, 1024,
+                     "\r\nServer: polipo"
+                     "\r\nContent-Type: text/html");
+    object->date = current_time.tv_sec;
+    object->age = current_time.tv_sec;
+    object->headers = internAtomN(buffer, hlen);
+    object->code = 200;
+    object->message = internAtom("Okay");
+    object->flags &= ~OBJECT_INITIAL;
+    object->flags |= OBJECT_DYNAMIC;
+
+    if(object->key_size == 8 && memcmp(object->key, "/polipo/", 8) == 0) {
+        objectPrintf(object, 0,
+                     "<!DOCTYPE HTML PUBLIC "
+                     "\"-//W3C//DTD HTML 4.01 Transitional//EN\" "
+                     "\"http://www.w3.org/TR/html4/loose.dtd\">\n"
+                     "<html><head>\n"
+                     "<title>Polipo</title>\n"
+                     "</head><body>\n"
+                     "<h1>Polipo</h1>\n"
+                     "<p><a href=\"status?\">Status report</a>.</p>\n"
+                     "<p><a href=\"config?\">Current configuration</a>.</p>\n"
+                     "<p><a href=\"servers?\">Known servers</a>.</p>\n"
+#ifndef NO_DISK_CACHE
+                     "<p><a href=\"index?\">Disk cache index</a>.</p>\n"
+#endif
+                     "</body></html>\n");
+        object->length = object->size;
+    } else if(matchUrl("/polipo/status", object)) {
+        objectPrintf(object, 0,
+                     "<!DOCTYPE HTML PUBLIC "
+                     "\"-//W3C//DTD HTML 4.01 Transitional//EN\" "
+                     "\"http://www.w3.org/TR/html4/loose.dtd\">\n"
+                     "<html><head>\n"
+                     "<title>Polipo status report</title>\n"
+                     "</head><body>\n"
+                     "<h1>Polipo proxy on %s:%d: status report</h1>\n"
+                     "<p>The %s proxy on %s:%d is %s.</p>\n"
+                     "<p>There are %d public and %d private objects "
+                     "currently in memory using %d KB in %d chunks "
+                     "(%d KB allocated).</p>\n"
+                     "<p>There are %d atoms.</p>"
+                     "<p><form method=POST action=\"/polipo/status?\">"
+                     "<input type=submit name=\"init-forbidden\" "
+                     "value=\"Read forbidden file\"></form>\n"
+                     "<form method=POST action=\"/polipo/status?\">"
+                     "<input type=submit name=\"writeout-objects\" "
+                     "value=\"Write out in-memory cache\"></form>\n"
+                     "<form method=POST action=\"/polipo/status?\">"
+                     "<input type=submit name=\"discard-objects\" "
+                     "value=\"Discard in-memory cache\"></form>\n"
+                     "<form method=POST action=\"/polipo/status?\">"
+                     "<input type=submit name=\"reopen-log\" "
+                     "value=\"Reopen log file\"></form>\n"
+                     "<form method=POST action=\"/polipo/status?\">"
+                     "<input type=submit name=\"free-chunk-arenas\" "
+                     "value=\"Free chunk arenas\"></form></p>\n"
+                     "<p><a href=\"/polipo/\">back</a></p>"
+                     "</body></html>\n",
+                     proxyName->string, proxyPort,
+                     cacheIsShared ? "shared" : "private",
+                     proxyName->string, proxyPort,
+                     proxyOffline ? "off line" :
+                     (relaxTransparency ? 
+                      "on line (transparency relaxed)" :
+                      "on line"),
+                     publicObjectCount, privateObjectCount,
+                     used_chunks * CHUNK_SIZE / 1024, used_chunks,
+                     totalChunkArenaSize() / 1024,
+                     used_atoms);
+        object->expires = current_time.tv_sec;
+        object->length = object->size;
+    } else if(matchUrl("/polipo/config", object)) {
+        fillSpecialObject(object, printConfig, NULL);
+        object->expires = current_time.tv_sec + 5;
+#ifndef NO_DISK_CACHE
+    } else if(matchUrl("/polipo/index", object)) {
+        int len;
+        char *root;
+        if(disableIndexing) {
+            abortObject(object, 403, internAtom("Action not allowed"));
+            notifyObject(object);
+            return 1;
+        }
+        len = MAX(0, object->key_size - 14);
+        root = strdup_n((char*)object->key + 14, len);
+        if(root == NULL) {
+            abortObject(object, 503, internAtom("Couldn't allocate root"));
+            notifyObject(object);
+            return 1;
+        }
+        writeoutObjects(1);
+        fillSpecialObject(object, plainIndexDiskObjects, root);
+        free(root);
+        object->expires = current_time.tv_sec + 5;
+    } else if(matchUrl("/polipo/recursive-index", object)) {
+        int len;
+        char *root;
+        if(disableIndexing) {
+            abortObject(object, 403, internAtom("Action not allowed"));
+            notifyObject(object);
+            return 1;
+        }
+        len = MAX(0, object->key_size - 24);
+        root = strdup_n((char*)object->key + 24, len);
+        if(root == NULL) {
+            abortObject(object, 503, internAtom("Couldn't allocate root"));
+            notifyObject(object);
+            return 1;
+        }
+        writeoutObjects(1);
+        fillSpecialObject(object, recursiveIndexDiskObjects, root);
+        free(root);
+        object->expires = current_time.tv_sec + 20;
+#endif
+    } else if(matchUrl("/polipo/servers", object)) {
+        if(disableServersList) {
+            abortObject(object, 403, internAtom("Action not allowed"));
+            notifyObject(object);
+            return 1;
+        }
+        fillSpecialObject(object, serversList, NULL);
+        object->expires = current_time.tv_sec + 2;
+    } else {
+        abortObject(object, 404, internAtom("Not found"));
+    }
+
+    object->flags &= ~OBJECT_VALIDATING;
+    notifyObject(object);
+    return 1;
+}
+
+int 
+httpSpecialSideRequest(ObjectPtr object, int method, int from, int to,
+                       HTTPRequestPtr requestor, void *closure)
+{
+    HTTPConnectionPtr client = requestor->connection;
+
+    assert(client->request == requestor);
+
+    if(method != METHOD_POST) {
+        httpClientError(requestor, 405, internAtom("Method not allowed"));
+        requestor->connection->flags &= ~CONN_READER;
+        return 1;
+    }
+
+    if(requestor->flags & REQUEST_WAIT_CONTINUE) {
+        httpClientError(requestor, 417, internAtom("Expectation failed"));
+        requestor->connection->flags &= ~CONN_READER;
+        return 1;
+    }
+
+    return httpSpecialDoSide(requestor);
+}
+
+int
+httpSpecialDoSide(HTTPRequestPtr requestor)
+{
+    HTTPConnectionPtr client = requestor->connection;
+
+    if(client->reqlen - client->reqbegin >= client->bodylen) {
+        AtomPtr data;
+        data = internAtomN(client->reqbuf + client->reqbegin,
+                           client->reqlen - client->reqbegin);
+        client->reqbegin = 0;
+        client->reqlen = 0;
+        if(data == NULL) {
+            do_log(L_ERROR, "Couldn't allocate data.\n");
+            httpClientError(requestor, 500,
+                            internAtom("Couldn't allocate data"));
+            return 1;
+        }
+        httpSpecialDoSideFinish(data, requestor);
+        return 1;
+    }
+
+    if(client->reqlen - client->reqbegin >= CHUNK_SIZE) {
+        httpClientError(requestor, 500, internAtom("POST too large"));
+        return 1;
+    }
+
+    if(client->reqbegin > 0 && client->reqlen > client->reqbegin) {
+        memmove(client->reqbuf, client->reqbuf + client->reqbegin,
+                client->reqlen - client->reqbegin);
+    }
+    client->reqlen -= client->reqbegin;
+    client->reqbegin = 0;
+
+    do_stream(IO_READ | IO_NOTNOW, client->fd,
+              client->reqlen, client->reqbuf, CHUNK_SIZE,
+              httpSpecialClientSideHandler, client);
+    return 1;
+}
+
+int
+httpSpecialClientSideHandler(int status,
+                             FdEventHandlerPtr event,
+                             StreamRequestPtr srequest)
+{
+    HTTPConnectionPtr connection = srequest->data;
+    HTTPRequestPtr request = connection->request;
+    int push;
+
+    if((request->object->flags & OBJECT_ABORTED) || 
+       !(request->object->flags & OBJECT_INPROGRESS)) {
+        httpClientDiscardBody(connection);
+        httpClientError(request, 503, internAtom("Post aborted"));
+        return 1;
+    }
+        
+    if(status < 0) {
+        do_log_error(L_ERROR, -status, "Reading from client");
+        if(status == -EDOGRACEFUL)
+            httpClientFinish(connection, 1);
+        else
+            httpClientFinish(connection, 2);
+        return 1;
+    }
+
+    push = MIN(srequest->offset - connection->reqlen,
+               connection->bodylen - connection->reqoffset);
+    if(push > 0) {
+        connection->reqlen += push;
+        httpSpecialDoSide(request);
+    }
+
+    do_log(L_ERROR, "Incomplete client request.\n");
+    connection->flags &= ~CONN_READER;
+    httpClientRawError(connection, 502,
+                       internAtom("Incomplete client request"), 1);
+    return 1;
+}
+
+int
+httpSpecialDoSideFinish(AtomPtr data, HTTPRequestPtr requestor)
+{
+    ObjectPtr object = requestor->object;
+
+    if(matchUrl("/polipo/config", object)) {
+        AtomListPtr list = NULL;
+        int i, rc;
+
+        if(disableConfiguration) {
+            abortObject(object, 403, internAtom("Action not allowed"));
+            goto out;
+        }
+
+        list = urlDecode(data->string, data->length);
+        if(list == NULL) {
+            abortObject(object, 400,
+                        internAtom("Couldn't parse variable to set"));
+            goto out;
+        }
+        for(i = 0; i < list->length; i++) {
+            rc = parseConfigLine(list->list[i]->string, NULL, 0, 1);
+            if(rc < 0) {
+                abortObject(object, 400,
+                            rc == -1 ?
+                            internAtom("Couldn't parse variable to set") :
+                            internAtom("Variable is not settable"));
+                destroyAtomList(list);
+                goto out;
+            }
+        }
+        destroyAtomList(list);
+        object->date = current_time.tv_sec;
+        object->age = current_time.tv_sec;
+        object->headers = internAtom("\r\nLocation: /polipo/config?");
+        object->code = 303;
+        object->message = internAtom("Done");
+        object->flags &= ~OBJECT_INITIAL;
+        object->length = 0;
+    } else if(matchUrl("/polipo/status", object)) {
+        AtomListPtr list = NULL;
+        int i;
+
+        if(disableConfiguration) {
+            abortObject(object, 403, internAtom("Action not allowed"));
+            goto out;
+        }
+
+        list = urlDecode(data->string, data->length);
+        if(list == NULL) {
+            abortObject(object, 400,
+                        internAtom("Couldn't parse action"));
+            goto out;
+        }
+        for(i = 0; i < list->length; i++) {
+            char *equals = 
+                memchr(list->list[i]->string, '=', list->list[i]->length);
+            AtomPtr name = 
+                equals ? 
+                internAtomN(list->list[i]->string, 
+                            equals - list->list[i]->string) :
+                retainAtom(list->list[i]);
+            if(name == atomInitForbidden)
+                initForbidden();
+            else if(name == atomReopenLog)
+                reopenLog();
+            else if(name == atomDiscardObjects)
+                discardObjects(1, 0);
+            else if(name == atomWriteoutObjects)
+                writeoutObjects(1);
+            else if(name == atomFreeChunkArenas)
+                free_chunk_arenas();
+            else {
+                abortObject(object, 400, internAtomF("Unknown action %s",
+                                                     name->string));
+                releaseAtom(name);
+                destroyAtomList(list);
+                goto out;
+            }
+            releaseAtom(name);
+        }
+        destroyAtomList(list);
+        object->date = current_time.tv_sec;
+        object->age = current_time.tv_sec;
+        object->headers = internAtom("\r\nLocation: /polipo/status?");
+        object->code = 303;
+        object->message = internAtom("Done");
+        object->flags &= ~OBJECT_INITIAL;
+        object->length = 0;
+    } else {
+        abortObject(object, 405, internAtom("Method not allowed"));
+    }
+
+ out:
+    releaseAtom(data);
+    notifyObject(object);
+    requestor->connection->flags &= ~CONN_READER;
+    return 1;
+}
+
+#ifdef HAVE_FORK
+static void
+fillSpecialObject(ObjectPtr object, void (*fn)(FILE*, char*), void* closure)
+{
+    int rc;
+    int filedes[2];
+    pid_t pid;
+    sigset_t ss, old_mask;
+
+    if(object->flags & OBJECT_INPROGRESS)
+        return;
+
+    rc = pipe(filedes);
+    if(rc < 0) {
+        do_log_error(L_ERROR, errno, "Couldn't create pipe");
+        abortObject(object, 503,
+                    internAtomError(errno, "Couldn't create pipe"));
+        return;
+    }
+
+    fflush(stdout);
+    fflush(stderr);
+    flushLog();
+
+    /* Block signals that we handle specially until the child can
+       disable the handlers. */
+    interestingSignals(&ss);
+    /* I'm a little confused.  POSIX doesn't allow EINTR here, but I
+       think that both Linux and SVR4 do. */
+    do {
+        rc = sigprocmask(SIG_BLOCK, &ss, &old_mask);
+    } while (rc < 0 && errno == EINTR);
+    if(rc < 0) {
+        do_log_error(L_ERROR, errno, "Sigprocmask failed");
+        abortObject(object, 503, internAtomError(errno, "Sigprocmask failed"));
+        close(filedes[0]);
+        close(filedes[1]);
+        return;
+    }
+    
+    pid = fork();
+    if(pid < 0) {
+        do_log_error(L_ERROR, errno, "Couldn't fork");
+        abortObject(object, 503, internAtomError(errno, "Couldn't fork"));
+        close(filedes[0]);
+        close(filedes[1]);
+        do {
+            rc = sigprocmask(SIG_SETMASK, &old_mask, NULL);
+        } while (rc < 0 && errno == EINTR);
+        if(rc < 0) {
+            do_log_error(L_ERROR, errno, "Couldn't restore signal mask");
+            polipoExit();
+        }
+        return;
+    }
+
+    if(pid > 0) {
+        SpecialRequestPtr request;
+        close(filedes[1]);
+        do {
+            rc = sigprocmask(SIG_SETMASK, &old_mask, NULL);
+        } while (rc < 0 && errno == EINTR);
+        if(rc < 0) {
+            do_log_error(L_ERROR, errno, "Couldn't restore signal mask");
+            polipoExit();
+            return;
+        }
+
+        request = malloc(sizeof(SpecialRequestRec));
+        if(request == NULL) {
+            kill(pid, SIGTERM);
+            close(filedes[0]);
+            abortObject(object, 503,
+                        internAtom("Couldn't allocate request\n"));
+            notifyObject(object);
+            return;
+        } else {
+            request->buf = get_chunk();
+            if(request->buf == NULL) {
+                kill(pid, SIGTERM);
+                close(filedes[0]);
+                free(request);
+                abortObject(object, 503,
+                            internAtom("Couldn't allocate request\n"));
+                notifyObject(object);
+                return;
+            }
+        }
+        object->flags |= OBJECT_INPROGRESS;
+        retainObject(object);
+        request->object = object;
+        request->fd = filedes[0];
+        request->pid = pid;
+        request->offset = 0;
+        /* Under any sensible scheduler, the child will run before the
+           parent.  So no need for IO_NOTNOW. */
+        do_stream(IO_READ, filedes[0], 0, request->buf, CHUNK_SIZE,
+                  specialRequestHandler, request);
+    } else {
+        /* child */
+        close(filedes[0]);
+        uninitEvents();
+        do {
+            rc = sigprocmask(SIG_SETMASK, &old_mask, NULL);
+        } while (rc < 0 && errno == EINTR);
+        if(rc < 0)
+            exit(1);
+
+        if(filedes[1] != 1)
+            dup2(filedes[1], 1);
+
+        (*fn)(stdout, closure);
+        exit(0);
+    }
+}
+
+int
+specialRequestHandler(int status, 
+                      FdEventHandlerPtr event, StreamRequestPtr srequest)
+{
+    SpecialRequestPtr request = srequest->data;
+    int rc;
+    int killed = 0;
+
+    if(status < 0) {
+        kill(request->pid, SIGTERM);
+        killed = 1;
+        request->object->flags &= ~OBJECT_INPROGRESS;
+        abortObject(request->object, 502,
+                    internAtomError(-status, "Couldn't read from client"));
+        goto done;
+    }
+
+    if(srequest->offset > 0) {
+        rc = objectAddData(request->object, request->buf, 
+                           request->offset, srequest->offset);
+        if(rc < 0) {
+            kill(request->pid, SIGTERM);
+            killed = 1;
+            request->object->flags &= ~OBJECT_INPROGRESS;
+            abortObject(request->object, 503,
+                        internAtom("Couldn't add data to connection"));
+            goto done;
+        }
+        request->offset += srequest->offset;
+    }
+    if(status) {
+        request->object->flags &= ~OBJECT_INPROGRESS;
+        request->object->length = request->object->size;
+        goto done;
+    }
+
+    /* If we're the only person interested in this object, let's abort
+       it now. */
+    if(request->object->refcount <= 1) {
+        kill(request->pid, SIGTERM);
+        killed = 1;
+        request->object->flags &= ~OBJECT_INPROGRESS;
+        abortObject(request->object, 500, internAtom("Aborted"));
+        goto done;
+    }
+    notifyObject(request->object);
+    do_stream(IO_READ | IO_NOTNOW, request->fd, 0, request->buf, CHUNK_SIZE,
+              specialRequestHandler, request);
+    return 1;
+
+ done:
+    close(request->fd);
+    dispose_chunk(request->buf);
+    releaseNotifyObject(request->object);
+    /* That's a blocking wait.  It shouldn't block for long, as we've
+       either already killed the child, or else we got EOF from it. */
+    do {
+        rc = waitpid(request->pid, &status, 0);
+    } while(rc < 0 && errno == EINTR);
+    if(rc < 0) {
+        do_log(L_ERROR, "Wait for %d: %d\n", (int)request->pid, errno);
+    } else {
+        int normal = 
+            (WIFEXITED(status) && WEXITSTATUS(status) == 0) ||
+            (killed && WIFSIGNALED(status) && WTERMSIG(status) == SIGTERM);
+        char *reason =
+            WIFEXITED(status) ? "with status" : 
+            WIFSIGNALED(status) ? "on signal" :
+            "with unknown status";
+        int value =
+            WIFEXITED(status) ? WEXITSTATUS(status) :
+            WIFSIGNALED(status) ? WTERMSIG(status) :
+            status;
+        do_log(normal ? D_CHILD : L_ERROR, 
+               "Child %d exited %s %d.\n",
+               (int)request->pid, reason, value);
+    }
+    free(request);
+    return 1;
+}
+#else
+static void
+fillSpecialObject(ObjectPtr object, void (*fn)(FILE*, char*), void* closure)
+{
+    FILE *tmp = NULL;
+    char *buf = NULL;
+    int rc, len, offset;
+
+    if(object->flags & OBJECT_INPROGRESS)
+        return;
+
+    buf = get_chunk();
+    if(buf == NULL) {
+        abortObject(object, 503, internAtom("Couldn't allocate chunk"));
+        goto done;
+    }
+
+    tmp = tmpfile();
+    if(tmp == NULL) {
+        abortObject(object, 503, internAtom(pstrerror(errno)));
+        goto done;
+    }
+
+    (*fn)(tmp, closure);
+    fflush(tmp);
+
+    rewind(tmp);
+    offset = 0;
+    while(1) {
+        len = fread(buf, 1, CHUNK_SIZE, tmp);
+        if(len <= 0 && ferror(tmp)) {
+            abortObject(object, 503, internAtom(pstrerror(errno)));
+            goto done;
+        }
+        if(len <= 0)
+            break;
+
+        rc = objectAddData(object, buf, offset, len);
+        if(rc < 0) {
+            abortObject(object, 503, internAtom("Couldn't add data to object"));
+            goto done;
+        }
+
+        offset += len;
+    }
+
+    object->length = offset;
+
+ done:
+    if(buf)
+        dispose_chunk(buf);
+    if(tmp)
+        fclose(tmp);
+    notifyObject(object);
+}
+#endif

+ 48 - 0
polipo/local.h

@@ -0,0 +1,48 @@
+/*
+Copyright (c) 2003, 2004 by Juliusz Chroboczek
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+typedef struct _SpecialRequest {
+    ObjectPtr object;
+    int fd;
+    void *buf;
+    int offset;
+    pid_t pid;
+} SpecialRequestRec, *SpecialRequestPtr;
+
+extern int disableConfiguration;
+extern int disableIndexing;
+
+void preinitLocal(void);
+void alternatingHttpStyle(FILE *out, char *id);
+int httpLocalRequest(ObjectPtr object, int method, int from, int to,
+                     HTTPRequestPtr, void *);
+int httpSpecialRequest(ObjectPtr object, int method, int from, int to,
+                       HTTPRequestPtr, void*);
+int httpSpecialSideRequest(ObjectPtr object, int method, int from, int to,
+                           HTTPRequestPtr requestor, void *closure);
+int specialRequestHandler(int status, 
+                          FdEventHandlerPtr event, StreamRequestPtr request);
+int httpSpecialDoSide(HTTPRequestPtr requestor);
+int httpSpecialClientSideHandler(int status,
+                                 FdEventHandlerPtr event,
+                                 StreamRequestPtr srequest);
+int httpSpecialDoSideFinish(AtomPtr data, HTTPRequestPtr requestor);

+ 13 - 0
polipo/localindex.html

@@ -0,0 +1,13 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
+ "http://www.w3.org/TR/html4/loose.dtd">
+<html><head>
+<title>Welcome to Polipo</title>
+</head><body>
+<h1>Welcome to Polipo</h1>
+
+<p><a href="doc/">The Polipo manual</a>.
+
+<p><a href="polipo/">The configuration interface</a>.
+
+</body></html>
+

+ 496 - 0
polipo/log.c

@@ -0,0 +1,496 @@
+/*
+Copyright (c) 2003-2006 by Juliusz Chroboczek
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+#include "polipo.h"
+
+#ifdef HAVE_SYSLOG
+#include <syslog.h>
+#endif
+
+static int logLevel = LOGGING_DEFAULT;
+static int logSyslog = 0;
+static AtomPtr logFile = NULL;
+static FILE *logF;
+static int logFilePermissions = 0640;
+int scrubLogs = 0;
+
+#ifdef HAVE_SYSLOG
+static AtomPtr logFacility = NULL;
+static int facility;
+#endif
+
+#define STR(x) XSTR(x)
+#define XSTR(x) #x
+
+static void initSyslog(void);
+
+#ifdef HAVE_SYSLOG
+static char *syslogBuf;
+static int syslogBufSize;
+static int syslogBufLength;
+
+static int translateFacility(AtomPtr facility);
+static int translatePriority(int type);
+static void accumulateSyslogV(int type, const char *f, va_list args);
+static void accumulateSyslogN(int type, const char *s, int len);
+#endif
+
+void
+preinitLog()
+{
+    CONFIG_VARIABLE_SETTABLE(logLevel, CONFIG_HEX, configIntSetter,
+                             "Logging level (max = " STR(LOGGING_MAX) ").");
+    CONFIG_VARIABLE(logFile, CONFIG_ATOM, "Log file (stderr if empty and logSyslog is unset, /var/log/polipo if empty and daemonise is true).");
+    CONFIG_VARIABLE(logFilePermissions, CONFIG_OCTAL,
+                    "Access rights of the logFile.");
+    CONFIG_VARIABLE_SETTABLE(scrubLogs, CONFIG_BOOLEAN, configIntSetter,
+                             "If true, don't include URLs in logs.");
+
+#ifdef HAVE_SYSLOG
+    CONFIG_VARIABLE(logSyslog, CONFIG_BOOLEAN, "Log to syslog.");
+    CONFIG_VARIABLE(logFacility, CONFIG_ATOM, "Syslog facility to use.");
+    logFacility = internAtom("user");
+#endif
+
+    logF = stderr;
+}
+
+int
+loggingToStderr(void) {
+    return(logF == stderr);
+}
+
+static FILE *
+openLogFile(void)
+{
+    int fd;
+    FILE *f;
+
+    fd = open(logFile->string, O_WRONLY | O_CREAT | O_APPEND,
+              logFilePermissions);
+    if(fd < 0)
+        return NULL;
+
+    f = fdopen(fd, "a");
+    if(f == NULL) {
+        int saved_errno = errno;
+        close(fd);
+        errno = saved_errno;
+        return NULL;
+    }
+
+    setvbuf(f, NULL, _IOLBF, 0);
+    return f;
+}
+
+void
+initLog(void)
+{
+    if(daemonise && logFile == NULL && !logSyslog)
+        logFile = internAtom("/var/log/polipo");
+
+    if(logFile != NULL && logFile->length > 0) {
+        FILE *f;
+        logFile = expandTilde(logFile);
+        f = openLogFile();
+        if(f == NULL) {
+            do_log_error(L_ERROR, errno, "Couldn't open log file %s",
+                         logFile->string);
+            exit(1);
+        }
+        logF = f;
+    }
+
+    if(logSyslog) {
+        initSyslog();
+
+        if(logFile == NULL) {
+            logF = NULL;
+        }
+    }
+}
+
+#ifdef HAVE_SYSLOG
+static void
+initSyslog()
+{
+    if(logSyslog) {
+        facility = translateFacility(logFacility);
+        closelog();
+        openlog("polipo", LOG_PID, facility);
+
+        if(!syslogBuf) {
+            syslogBuf = strdup("");
+            syslogBufSize = 1;
+        }
+    }
+}
+
+/* Map a user-provided name to a syslog facility.
+
+   This is rendered quite ugly because POSIX hardly defines any, but we
+   should allow any the local system knows about. */
+
+static int
+translateFacility(AtomPtr facility)
+{
+    typedef struct
+    {
+        const char *name;
+        int facility;
+    } FacilitiesRec;
+
+    /* List of all known valid syslog facilities.
+
+       This list is terminated by a NULL facility name. */
+
+    FacilitiesRec facilities[] = {
+        /* These are all the facilities found in glibc 2.5. */
+#ifdef LOG_AUTH
+        { "auth", LOG_AUTH },
+#endif
+#ifdef LOG_AUTHPRIV
+        { "authpriv", LOG_AUTHPRIV },
+#endif
+#ifdef LOG_CRON
+        { "cron", LOG_CRON },
+#endif
+#ifdef LOG_DAEMON
+        { "daemon", LOG_DAEMON },
+#endif
+#ifdef LOG_FTP
+        { "ftp", LOG_FTP },
+#endif
+#ifdef LOG_KERN
+        { "kern", LOG_KERN },
+#endif
+#ifdef LOG_LPR
+        { "lpr", LOG_LPR },
+#endif
+#ifdef LOG_MAIL
+        { "mail", LOG_MAIL },
+#endif
+#ifdef LOG_NEWS
+        { "news", LOG_NEWS },
+#endif
+#ifdef LOG_SYSLOG
+        { "syslog", LOG_SYSLOG },
+#endif
+#ifdef LOG_uucp
+        { "uucp", LOG_UUCP },
+#endif
+        /* These are required by POSIX. */
+        { "user", LOG_USER },
+        { "local0", LOG_LOCAL0 },
+        { "local1", LOG_LOCAL1 },
+        { "local2", LOG_LOCAL2 },
+        { "local3", LOG_LOCAL3 },
+        { "local4", LOG_LOCAL4 },
+        { "local5", LOG_LOCAL5 },
+        { "local6", LOG_LOCAL6 },
+        { "local7", LOG_LOCAL7 },
+        { NULL, 0 }};
+
+    FacilitiesRec *current;
+
+    /* It would be more fitting to return LOG_DAEMON, but POSIX does not
+       guarantee the existence of that facility. */
+
+    if(!facility) {
+        return LOG_USER;
+    }
+
+    current = facilities;
+    while(current->name) {
+        if(!strcmp(current->name, atomString(facility))) {
+            return current->facility;
+        }
+        current++;
+    }
+
+    /* This will go to stderr because syslog is not yet initialized. */
+    do_log(L_ERROR, "Specified logFacility %s nonexistent on this system.",
+           atomString(facility));
+
+    return LOG_USER;
+}
+
+/* Translate a Polipo error type into a syslog priority. */
+
+static int
+translatePriority(int type)
+{
+    typedef struct
+    {
+        int type;
+        int priority;
+    } PrioritiesRec;
+
+    /* The list is terminated with a type of zero. */
+
+    PrioritiesRec priorities[] = {{ L_ERROR, LOG_ERR },
+                                  { L_WARN, LOG_WARNING },
+                                  { L_INFO, LOG_NOTICE },
+                                  { L_FORBIDDEN, LOG_DEBUG },
+                                  { L_UNCACHEABLE, LOG_DEBUG },
+                                  { L_SUPERSEDED, LOG_DEBUG },
+                                  { L_VARY, LOG_DEBUG },
+                                  { L_TUNNEL, LOG_NOTICE },
+                                  { 0, 0 }};
+    PrioritiesRec *current;
+
+    current = priorities;
+    while(current->type) {
+        if(current->type == type) {
+            return current->priority;
+        }
+        current++;
+    }
+
+    return LOG_DEBUG;
+}
+
+static int
+expandSyslog(int len)
+{
+    int newsize;
+    char *newbuf;
+
+    if(len < 0)
+        newsize = syslogBufSize * 2;
+    else
+        newsize = syslogBufLength + len + 1;
+
+    newbuf = realloc(syslogBuf, newsize);
+    if(!newbuf)
+        return -1;
+
+    syslogBuf = newbuf;
+    syslogBufSize = newsize;
+    return 1;
+}
+
+static void
+maybeFlushSyslog(int type)
+{
+    char *linefeed;
+    while(1) {
+        linefeed = memchr(syslogBuf, '\n', syslogBufLength);
+        if(linefeed == NULL)
+            break;
+        *linefeed = '\0';
+        syslog(translatePriority(type), "%s", syslogBuf);
+        linefeed++;
+        syslogBufLength -= (linefeed - syslogBuf);
+        if(syslogBufLength > 0)
+            memmove(syslogBuf, linefeed, syslogBufLength);
+    }
+}
+
+static void
+accumulateSyslogV(int type, const char *f, va_list args)
+{
+    int rc;
+    va_list args_copy;
+
+ again:
+    va_copy(args_copy, args);
+    rc = vsnprintf(syslogBuf + syslogBufLength,
+                   syslogBufSize - syslogBufLength,
+                   f, args_copy);
+    va_end(args_copy);
+
+    if(rc < 0 || rc >= syslogBufSize - syslogBufLength) {
+        rc = expandSyslog(rc);
+        if(rc < 0)
+            return;
+        goto again;
+    }
+
+    syslogBufLength += rc;
+
+    maybeFlushSyslog(type);
+}
+
+static void
+accumulateSyslogN(int type, const char *s, int len)
+{
+    while(syslogBufSize - syslogBufLength <= len)
+        expandSyslog(len);
+
+    memcpy(syslogBuf + syslogBufLength, s, len);
+    syslogBufLength += len;
+    syslogBuf[syslogBufLength] = '\0';
+
+    maybeFlushSyslog(type);
+}
+
+#else
+static void
+initSyslog()
+{
+    return;
+}
+#endif
+
+/* Flush any messages waiting to be logged. */
+void flushLog()
+{
+    if(logF)
+        fflush(logF);
+
+#ifdef HAVE_SYSLOG
+    /* There shouldn't really be anything here, but let's be paranoid.
+       We can't pick a good value for `type', so just invent one. */
+    if(logSyslog && syslogBuf[0] != '\0') {
+        accumulateSyslogN(L_INFO, "\n", 1);
+    }
+
+    assert(syslogBufLength == 0);
+#endif
+}
+
+void
+reopenLog()
+{
+    if(logFile && logFile->length > 0) {
+        FILE *f;
+        f = openLogFile();
+        if(f == NULL) {
+            do_log_error(L_ERROR, errno, "Couldn't reopen log file %s",
+                         logFile->string);
+            exit(1);
+        }
+        fclose(logF);
+        logF = f;
+    }
+
+    if(logSyslog)
+        initSyslog();
+}
+
+void
+really_do_log(int type, const char *f, ...)
+{
+    va_list args;
+
+    va_start(args, f);
+    if(type & LOGGING_MAX & logLevel)
+        really_do_log_v(type, f, args);
+    va_end(args);
+}
+
+void
+really_do_log_v(int type, const char *f, va_list args)
+{
+    va_list args_copy;
+
+    if(type & LOGGING_MAX & logLevel) {
+        if(logF)
+        {
+            va_copy(args_copy, args);
+            vfprintf(logF, f, args_copy);
+            va_end(args_copy);
+        }
+#ifdef HAVE_SYSLOG
+        if(logSyslog) {
+            va_copy(args_copy, args);
+            accumulateSyslogV(type, f, args_copy);
+            va_end(args_copy);
+        }
+#endif
+    }
+}
+
+void
+really_do_log_error(int type, int e, const char *f, ...)
+{
+    va_list args;
+    va_start(args, f);
+    if(type & LOGGING_MAX & logLevel)
+        really_do_log_error_v(type, e, f, args);
+    va_end(args);
+}
+
+void
+really_do_log_error_v(int type, int e, const char *f, va_list args)
+{
+    va_list args_copy;
+
+    if((type & LOGGING_MAX & logLevel) != 0) {
+        char *es = pstrerror(e);
+        if(es == NULL)
+            es = "Unknown error";
+
+        if(logF) {
+            va_copy(args_copy, args);
+            vfprintf(logF, f, args_copy);
+            fprintf(logF, ": %s\n", es);
+            va_end(args_copy);
+        }
+#ifdef HAVE_SYSLOG
+        if(logSyslog) {
+            char msg[256];
+            int n = 0;
+
+            va_copy(args_copy, args);
+            n = snnvprintf(msg, n, 256, f, args_copy);
+            va_end(args_copy);
+            n = snnprintf(msg, n, 256, ": ");
+            n = snnprint_n(msg, n, 256, es, strlen (es));
+            n = snnprintf(msg, n, 256, "\n");
+            /* Overflow? Vanishingly unlikely; truncate at 255. */
+            if(n < 0 || n > 256) {
+                n = 256;
+                msg[255] = '\0';
+            }
+            else
+                msg[n] = '\0';
+
+            accumulateSyslogN(type, msg, n);
+        }
+#endif
+    }
+}
+
+void
+really_do_log_n(int type, const char *s, int n)
+{
+    if((type & LOGGING_MAX & logLevel) != 0) {
+        if(logF) {
+            fwrite(s, n, 1, logF);
+        }
+#ifdef HAVE_SYSLOG
+        if(logSyslog)
+            accumulateSyslogN(type, s, n);
+#endif
+    }
+}
+
+const char *
+scrub(const char *message)
+{
+    if(scrubLogs)
+        return "(scrubbed)";
+    else
+        return message;
+}

+ 140 - 0
polipo/log.h

@@ -0,0 +1,140 @@
+/*
+Copyright (c) 2003-2006 by Juliusz Chroboczek
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+#define L_ERROR 0x1
+#define L_WARN 0x2
+#define L_INFO 0x4
+#define L_FORBIDDEN 0x8
+#define L_UNCACHEABLE 0x10
+#define L_SUPERSEDED 0x20
+#define L_VARY 0x40
+#define L_TUNNEL 0x80
+
+#define D_SERVER_CONN 0x100
+#define D_SERVER_REQ 0x200
+#define D_CLIENT_CONN 0x400
+#define D_CLIENT_REQ 0x800
+#define D_ATOM_REFCOUNT 0x1000
+#define D_REFCOUNT 0x2000
+#define D_LOCK 0x4000
+#define D_OBJECT 0x8000
+#define D_OBJECT_DATA 0x10000
+#define D_SERVER_OFFSET 0x20000
+#define D_CLIENT_DATA 0x40000
+#define D_DNS 0x80000
+#define D_CHILD 0x100000
+#define D_IO 0x200000
+
+#define LOGGING_DEFAULT (L_ERROR | L_WARN | L_INFO)
+#define LOGGING_MAX 0xFF
+
+extern int scrubLogs;
+
+void preinitLog(void);
+void initLog(void);
+void reopenLog(void);
+void flushLog(void);
+int loggingToStderr(void);
+
+void really_do_log(int type, const char *f, ...)
+    ATTRIBUTE ((format (printf, 2, 3)));
+void really_do_log_v(int type, const char *f, va_list args)
+    ATTRIBUTE ((format (printf, 2, 0)));
+void really_do_log_n(int type, const char *s, int n);
+void really_do_log_error(int type, int e, const char *f, ...)
+    ATTRIBUTE ((format (printf, 3, 4)));
+void really_do_log_error_v(int type, int e, const char *f, va_list args)
+    ATTRIBUTE ((format (printf, 3, 0)));
+const char *scrub(const char *message);
+
+#ifdef __GNUC__
+#define DO_BACKTRACE()                  \
+  do {                                  \
+    int n;                              \
+    void *buffer[10];                   \
+    n = backtrace(buffer, 5);           \
+    fflush(stderr);                     \
+    backtrace_symbols_fd(buffer, n, 2); \
+ } while(0)
+#else
+#define DO_BACKTRACE() /* */
+#endif
+
+/* These are macros because it's important that they should be
+   optimised away. */
+
+#if defined __STDC_VERSION__ && __STDC_VERSION__ >= 199901L
+
+#define do_log(_type, ...)                                           \
+    do {                                                             \
+        if((_type) & (LOGGING_MAX)) really_do_log((_type), __VA_ARGS__); \
+    } while(0)
+#define do_log_error(_type, _e, ...)                                 \
+    do {                                                             \
+        if((_type) & (LOGGING_MAX))                                  \
+            really_do_log_error((_type), (_e), __VA_ARGS__);         \
+    } while(0)
+
+#elif defined(__GNUC__)
+
+#define do_log(_type, _args...)                                \
+    do {                                                       \
+        if((_type) & (LOGGING_MAX)) really_do_log((_type), _args); \
+    } while(0)
+#define do_log_error(_type, _e, _args...)                      \
+    do {                                                       \
+        if((_type) & (LOGGING_MAX))                            \
+            really_do_log_error((_type), (_e), _args);         \
+    } while(0)
+
+#else
+
+/* No variadic macros -- let's hope inline works. */
+
+static inline void 
+do_log(int type, const char *f, ...)
+{
+    va_list args;
+
+    va_start(args, f);
+    if((type & (LOGGING_MAX)) != 0)
+        really_do_log_v(type, f, args);
+    va_end(args);
+}
+
+static inline void
+do_log_error(int type, int e, const char *f, ...)
+{
+    va_list args;
+
+    va_start(args, f);
+    if((type & (LOGGING_MAX)) != 0)
+        really_do_log_error_v(type, e, f, args);
+    va_end(args);
+}    
+
+#endif
+
+#define do_log_n(_type, _s, _n) \
+    do { \
+        if((_type) & (LOGGING_MAX)) really_do_log_n((_type), (_s), (_n)); \
+    } while(0)

+ 171 - 0
polipo/main.c

@@ -0,0 +1,171 @@
+/*
+Copyright (c) 2003-2006 by Juliusz Chroboczek
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+#include "polipo.h"
+
+AtomPtr configFile = NULL;
+AtomPtr pidFile = NULL;
+int daemonise = 0;
+
+static void
+usage(char *argv0)
+{
+    fprintf(stderr, 
+            "%s [ -h ] [ -v ] [ -x ] [ -c filename ] [ -- ] [ var=val... ]\n",
+            argv0);
+    fprintf(stderr, "  -h: display this message.\n");
+    fprintf(stderr, "  -v: display the list of configuration variables.\n");
+    fprintf(stderr, "  -x: perform expiry on the disk cache.\n");
+    fprintf(stderr, "  -c: specify the configuration file to use.\n");
+}
+
+int
+main(int argc, char **argv)
+{
+    FdEventHandlerPtr listener;
+    int i;
+    int rc;
+    int expire = 0, printConfig = 0;
+
+    initAtoms();
+    CONFIG_VARIABLE(daemonise, CONFIG_BOOLEAN, "Run as a daemon");
+    CONFIG_VARIABLE(pidFile, CONFIG_ATOM, "File with pid of running daemon.");
+
+    preinitChunks();
+    preinitLog();
+    preinitObject();
+    preinitIo();
+    preinitDns();
+    preinitServer();
+    preinitHttp();
+    preinitDiskcache();
+    preinitLocal();
+    preinitForbidden();
+    preinitSocks();
+
+    i = 1;
+    while(i < argc) {
+        if(argv[i][0] != '-')
+            break;
+        if(strcmp(argv[i], "--") == 0) {
+            i++;
+            break;
+        } else if(strcmp(argv[i], "-h") == 0) {
+            usage(argv[0]);
+            exit(0);
+        } else if(strcmp(argv[i], "-v") == 0) {
+            printConfig = 1;
+            i++;
+        } else if(strcmp(argv[i], "-x") == 0) {
+            expire = 1;
+            i++;
+        } else if(strcmp(argv[i], "-c") == 0) {
+            i++;
+            if(i >= argc) {
+                usage(argv[0]);
+                exit(1);
+            }
+            if(configFile)
+                releaseAtom(configFile);
+            configFile = internAtom(argv[i]);
+            i++;
+        } else {
+            usage(argv[0]);
+            exit(1);
+        }
+    }
+
+    if(configFile)
+        configFile = expandTilde(configFile);
+
+    if(configFile == NULL) {
+        configFile = expandTilde(internAtom("~/.polipo"));
+        if(configFile)
+            if(access(configFile->string, F_OK) < 0) {
+                releaseAtom(configFile);
+                configFile = NULL;
+            }
+    }
+
+    if(configFile == NULL) {
+        if(access("/etc/polipo/config", F_OK) >= 0)
+            configFile = internAtom("/etc/polipo/config");
+        if(configFile && access(configFile->string, F_OK) < 0) {
+            releaseAtom(configFile);
+            configFile = NULL;
+        }
+    }
+
+    rc = parseConfigFile(configFile);
+    if(rc < 0)
+        exit(1);
+
+    while(i < argc) {
+        rc = parseConfigLine(argv[i], "command line", 0, 0);
+        if(rc < 0)
+            exit(1);
+        i++;
+    }
+
+    initChunks();
+    initLog();
+    initObject();
+    if(!expire && !printConfig)
+        initEvents();
+    initIo();
+    initDns();
+    initHttp();
+    initServer();
+    initDiskcache();
+    initForbidden();
+    initSocks();
+
+    if(printConfig) {
+        printConfigVariables(stdout, 0);
+        exit(0);
+    }
+
+    if(expire) {
+        expireDiskObjects();
+        exit(0);
+    }
+
+    if(daemonise)
+        do_daemonise(loggingToStderr());
+
+    if(pidFile) {
+        pidFile = expandTilde(pidFile);
+        writePid(pidFile->string);
+    }
+
+    listener = create_listener(proxyAddress->string, 
+                               proxyPort, httpAccept, NULL);
+    if(!listener) {
+        if(pidFile) unlink(pidFile->string);
+        exit(1);
+    }
+
+    eventLoop();
+
+    if(pidFile) unlink(pidFile->string);
+    return 0;
+}

+ 322 - 0
polipo/md5.c

@@ -0,0 +1,322 @@
+/*
+ ***********************************************************************
+ ** md5.c -- the source code for MD5 routines                         **
+ ** RSA Data Security, Inc. MD5 Message-Digest Algorithm              **
+ ** Created: 2/17/90 RLR                                              **
+ ** Revised: 1/91 SRD,AJ,BSK,JT Reference C Version                   **
+ ** Revised (for MD5): RLR 4/27/91                                    **
+ **   -- G modified to have y&~z instead of y&z                       **
+ **   -- FF, GG, HH modified to add in last register done             **
+ **   -- Access pattern: round 2 works mod 5, round 3 works mod 3     **
+ **   -- distinct additive constant for each step                     **
+ **   -- round 4 added, working mod 7                                 **
+ ***********************************************************************
+ */
+
+/*
+ ***********************************************************************
+ ** Copyright (C) 1990, RSA Data Security, Inc. All rights reserved.  **
+ **                                                                   **
+ ** License to copy and use this software is granted provided that    **
+ ** it is identified as the "RSA Data Security, Inc. MD5 Message-     **
+ ** Digest Algorithm" in all material mentioning or referencing this  **
+ ** software or this function.                                        **
+ **                                                                   **
+ ** License is also granted to make and use derivative works          **
+ ** provided that such works are identified as "derived from the RSA  **
+ ** Data Security, Inc. MD5 Message-Digest Algorithm" in all          **
+ ** material mentioning or referencing the derived work.              **
+ **                                                                   **
+ ** RSA Data Security, Inc. makes no representations concerning       **
+ ** either the merchantability of this software or the suitability    **
+ ** of this software for any particular purpose.  It is provided "as  **
+ ** is" without express or implied warranty of any kind.              **
+ **                                                                   **
+ ** These notices must be retained in any copies of any part of this  **
+ ** documentation and/or software.                                    **
+ ***********************************************************************
+ */
+
+#include "md5.h"
+
+/*
+ ***********************************************************************
+ **  Message-digest routines:                                         **
+ **  To form the message digest for a message M                       **
+ **    (1) Initialize a context buffer mdContext using MD5Init        **
+ **    (2) Call MD5Update on mdContext and M                          **
+ **    (3) Call MD5Final on mdContext                                 **
+ **  The message digest is now in mdContext->digest[0...15]           **
+ ***********************************************************************
+ */
+
+/* forward declaration */
+static void Transform (UINT4 *, UINT4 *);
+
+#ifdef	__STDC__
+static const
+#else
+static
+#endif
+unsigned char PADDING[64] = {
+  0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
+};
+
+/* F, G, H and I are basic MD5 functions */
+#define F(x, y, z) (((x) & (y)) | ((~x) & (z)))
+#define G(x, y, z) (((x) & (z)) | ((y) & (~z)))
+#define H(x, y, z) ((x) ^ (y) ^ (z))
+#define I(x, y, z) ((y) ^ ((x) | (~z)))
+
+/* ROTATE_LEFT rotates x left n bits */
+#if	defined(FAST_MD5) && defined(__GNUC__) && defined(mc68000)
+/*
+ * If we're on a 68000 based CPU and using a GNU C compiler with
+ * inline assembly code, we can speed this up a bit.
+ */
+inline UINT4 ROTATE_LEFT(UINT4 x, int n)
+{   
+    asm("roll %2,%0" : "=d" (x) : "0" (x), "Ir" (n));
+    return x;
+}
+#else
+#define ROTATE_LEFT(x, n) (((x) << (n)) | ((x) >> (32-(n))))
+#endif
+
+
+/* FF, GG, HH, and II transformations for rounds 1, 2, 3, and 4 */
+/* Rotation is separate from addition to prevent recomputation */
+#define FF(a, b, c, d, x, s, ac) \
+  {(a) += F ((b), (c), (d)) + (x) + (UINT4)(ac); \
+   (a) = ROTATE_LEFT ((a), (s)); \
+   (a) += (b); \
+  }
+#define GG(a, b, c, d, x, s, ac) \
+  {(a) += G ((b), (c), (d)) + (x) + (UINT4)(ac); \
+   (a) = ROTATE_LEFT ((a), (s)); \
+   (a) += (b); \
+  }
+#define HH(a, b, c, d, x, s, ac) \
+  {(a) += H ((b), (c), (d)) + (x) + (UINT4)(ac); \
+   (a) = ROTATE_LEFT ((a), (s)); \
+   (a) += (b); \
+  }
+#define II(a, b, c, d, x, s, ac) \
+  {(a) += I ((b), (c), (d)) + (x) + (UINT4)(ac); \
+   (a) = ROTATE_LEFT ((a), (s)); \
+   (a) += (b); \
+  }
+
+/* The routine MD5Init initializes the message-digest context
+   mdContext. All fields are set to zero.
+ */
+void MD5Init (mdContext)
+MD5_CTX *mdContext;
+{
+  mdContext->i[0] = mdContext->i[1] = (UINT4)0;
+
+  /* Load magic initialization constants.
+   */
+  mdContext->buf[0] = (UINT4)0x67452301;
+  mdContext->buf[1] = (UINT4)0xefcdab89;
+  mdContext->buf[2] = (UINT4)0x98badcfe;
+  mdContext->buf[3] = (UINT4)0x10325476;
+}
+
+/* The routine MD5Update updates the message-digest context to
+   account for the presence of each of the characters inBuf[0..inLen-1]
+   in the message whose digest is being computed.
+ */
+void MD5Update (mdContext, inBuf, inLen)
+MD5_CTX *mdContext;
+unsigned const char *inBuf;
+unsigned int inLen;
+{
+  UINT4 in[16];
+  int mdi;
+  unsigned int i, ii;
+
+  /* compute number of bytes mod 64 */
+  mdi = (int)((mdContext->i[0] >> 3) & 0x3F);
+
+  /* update number of bits */
+  if ((mdContext->i[0] + ((UINT4)inLen << 3)) < mdContext->i[0])
+    mdContext->i[1]++;
+  mdContext->i[0] += ((UINT4)inLen << 3);
+  mdContext->i[1] += ((UINT4)inLen >> 29);
+
+  while (inLen--) {
+    /* add new character to buffer, increment mdi */
+    mdContext->in[mdi++] = *inBuf++;
+
+    /* transform if necessary */
+    if (mdi == 0x40) {
+      for (i = 0, ii = 0; i < 16; i++, ii += 4)
+        in[i] = (((UINT4)mdContext->in[ii+3]) << 24) |
+                (((UINT4)mdContext->in[ii+2]) << 16) |
+                (((UINT4)mdContext->in[ii+1]) << 8) |
+                ((UINT4)mdContext->in[ii]);
+      Transform (mdContext->buf, in);
+      mdi = 0;
+    }
+  }
+}
+
+/* The routine MD5Final terminates the message-digest computation and
+   ends with the desired message digest in mdContext->digest[0...15].
+ */
+
+void MD5Final (mdContext)
+MD5_CTX *mdContext;
+{
+  UINT4 in[16];
+  int mdi;
+  unsigned int i, ii;
+  unsigned int padLen;
+
+  /* save number of bits */
+  in[14] = mdContext->i[0];
+  in[15] = mdContext->i[1];
+
+  /* compute number of bytes mod 64 */
+  mdi = (int)((mdContext->i[0] >> 3) & 0x3F);
+
+  /* pad out to 56 mod 64 */
+  padLen = (mdi < 56) ? (56 - mdi) : (120 - mdi);
+  MD5Update (mdContext, PADDING, padLen);
+
+  /* append length in bits and transform */
+  for (i = 0, ii = 0; i < 14; i++, ii += 4)
+    in[i] = (((UINT4)mdContext->in[ii+3]) << 24) |
+            (((UINT4)mdContext->in[ii+2]) << 16) |
+            (((UINT4)mdContext->in[ii+1]) << 8) |
+            ((UINT4)mdContext->in[ii]);
+  Transform (mdContext->buf, in);
+
+  /* store buffer in digest */
+  for (i = 0, ii = 0; i < 4; i++, ii += 4) {
+    mdContext->digest[ii] = (unsigned char)(mdContext->buf[i] & 0xFF);
+    mdContext->digest[ii+1] =
+      (unsigned char)((mdContext->buf[i] >> 8) & 0xFF);
+    mdContext->digest[ii+2] =
+      (unsigned char)((mdContext->buf[i] >> 16) & 0xFF);
+    mdContext->digest[ii+3] =
+      (unsigned char)((mdContext->buf[i] >> 24) & 0xFF);
+  }
+}
+
+/* Basic MD5 step. Transforms buf based on in.
+ */
+static void Transform (buf, in)
+UINT4 *buf;
+UINT4 *in;
+{
+  UINT4 a = buf[0], b = buf[1], c = buf[2], d = buf[3];
+
+  /* Round 1 */
+#define S11 7
+#define S12 12
+#define S13 17
+#define S14 22
+
+  FF ( a, b, c, d, in[ 0], S11, 0xd76aa478); /* 1 */
+  FF ( d, a, b, c, in[ 1], S12, 0xe8c7b756); /* 2 */
+  FF ( c, d, a, b, in[ 2], S13, 0x242070db); /* 3 */
+  FF ( b, c, d, a, in[ 3], S14, 0xc1bdceee); /* 4 */
+  FF ( a, b, c, d, in[ 4], S11, 0xf57c0faf); /* 5 */
+  FF ( d, a, b, c, in[ 5], S12, 0x4787c62a); /* 6 */
+  FF ( c, d, a, b, in[ 6], S13, 0xa8304613); /* 7 */
+  FF ( b, c, d, a, in[ 7], S14, 0xfd469501); /* 8 */
+  FF ( a, b, c, d, in[ 8], S11, 0x698098d8); /* 9 */
+  FF ( d, a, b, c, in[ 9], S12, 0x8b44f7af); /* 10 */
+  FF ( c, d, a, b, in[10], S13, 0xffff5bb1); /* 11 */
+  FF ( b, c, d, a, in[11], S14, 0x895cd7be); /* 12 */
+  FF ( a, b, c, d, in[12], S11, 0x6b901122); /* 13 */
+  FF ( d, a, b, c, in[13], S12, 0xfd987193); /* 14 */
+  FF ( c, d, a, b, in[14], S13, 0xa679438e); /* 15 */
+  FF ( b, c, d, a, in[15], S14, 0x49b40821); /* 16 */
+
+  /* Round 2 */
+#define S21 5
+#define S22 9
+#define S23 14
+#define S24 20
+  GG ( a, b, c, d, in[ 1], S21, 0xf61e2562); /* 17 */
+  GG ( d, a, b, c, in[ 6], S22, 0xc040b340); /* 18 */
+  GG ( c, d, a, b, in[11], S23, 0x265e5a51); /* 19 */
+  GG ( b, c, d, a, in[ 0], S24, 0xe9b6c7aa); /* 20 */
+  GG ( a, b, c, d, in[ 5], S21, 0xd62f105d); /* 21 */
+  GG ( d, a, b, c, in[10], S22,  0x2441453); /* 22 */
+  GG ( c, d, a, b, in[15], S23, 0xd8a1e681); /* 23 */
+  GG ( b, c, d, a, in[ 4], S24, 0xe7d3fbc8); /* 24 */
+  GG ( a, b, c, d, in[ 9], S21, 0x21e1cde6); /* 25 */
+  GG ( d, a, b, c, in[14], S22, 0xc33707d6); /* 26 */
+  GG ( c, d, a, b, in[ 3], S23, 0xf4d50d87); /* 27 */
+  GG ( b, c, d, a, in[ 8], S24, 0x455a14ed); /* 28 */
+  GG ( a, b, c, d, in[13], S21, 0xa9e3e905); /* 29 */
+  GG ( d, a, b, c, in[ 2], S22, 0xfcefa3f8); /* 30 */
+  GG ( c, d, a, b, in[ 7], S23, 0x676f02d9); /* 31 */
+  GG ( b, c, d, a, in[12], S24, 0x8d2a4c8a); /* 32 */
+
+  /* Round 3 */
+#define S31 4
+#define S32 11
+#define S33 16
+#define S34 23
+  HH ( a, b, c, d, in[ 5], S31, 0xfffa3942); /* 33 */
+  HH ( d, a, b, c, in[ 8], S32, 0x8771f681); /* 34 */
+  HH ( c, d, a, b, in[11], S33, 0x6d9d6122); /* 35 */
+  HH ( b, c, d, a, in[14], S34, 0xfde5380c); /* 36 */
+  HH ( a, b, c, d, in[ 1], S31, 0xa4beea44); /* 37 */
+  HH ( d, a, b, c, in[ 4], S32, 0x4bdecfa9); /* 38 */
+  HH ( c, d, a, b, in[ 7], S33, 0xf6bb4b60); /* 39 */
+  HH ( b, c, d, a, in[10], S34, 0xbebfbc70); /* 40 */
+  HH ( a, b, c, d, in[13], S31, 0x289b7ec6); /* 41 */
+  HH ( d, a, b, c, in[ 0], S32, 0xeaa127fa); /* 42 */
+  HH ( c, d, a, b, in[ 3], S33, 0xd4ef3085); /* 43 */
+  HH ( b, c, d, a, in[ 6], S34,  0x4881d05); /* 44 */
+  HH ( a, b, c, d, in[ 9], S31, 0xd9d4d039); /* 45 */
+  HH ( d, a, b, c, in[12], S32, 0xe6db99e5); /* 46 */
+  HH ( c, d, a, b, in[15], S33, 0x1fa27cf8); /* 47 */
+  HH ( b, c, d, a, in[ 2], S34, 0xc4ac5665); /* 48 */
+
+  /* Round 4 */
+#define S41 6
+#define S42 10
+#define S43 15
+#define S44 21
+  II ( a, b, c, d, in[ 0], S41, 0xf4292244); /* 49 */
+  II ( d, a, b, c, in[ 7], S42, 0x432aff97); /* 50 */
+  II ( c, d, a, b, in[14], S43, 0xab9423a7); /* 51 */
+  II ( b, c, d, a, in[ 5], S44, 0xfc93a039); /* 52 */
+  II ( a, b, c, d, in[12], S41, 0x655b59c3); /* 53 */
+  II ( d, a, b, c, in[ 3], S42, 0x8f0ccc92); /* 54 */
+  II ( c, d, a, b, in[10], S43, 0xffeff47d); /* 55 */
+  II ( b, c, d, a, in[ 1], S44, 0x85845dd1); /* 56 */
+  II ( a, b, c, d, in[ 8], S41, 0x6fa87e4f); /* 57 */
+  II ( d, a, b, c, in[15], S42, 0xfe2ce6e0); /* 58 */
+  II ( c, d, a, b, in[ 6], S43, 0xa3014314); /* 59 */
+  II ( b, c, d, a, in[13], S44, 0x4e0811a1); /* 60 */
+  II ( a, b, c, d, in[ 4], S41, 0xf7537e82); /* 61 */
+  II ( d, a, b, c, in[11], S42, 0xbd3af235); /* 62 */
+  II ( c, d, a, b, in[ 2], S43, 0x2ad7d2bb); /* 63 */
+  II ( b, c, d, a, in[ 9], S44, 0xeb86d391); /* 64 */
+
+  buf[0] += a;
+  buf[1] += b;
+  buf[2] += c;
+  buf[3] += d;
+}
+
+/*
+ ***********************************************************************
+ ** End of md5.c                                                      **
+ ******************************** (cut) ********************************
+ */

+ 60 - 0
polipo/md5.h

@@ -0,0 +1,60 @@
+/*
+ ***********************************************************************
+ ** md5.h -- header file for implementation of MD5                    **
+ ** RSA Data Security, Inc. MD5 Message-Digest Algorithm              **
+ ** Created: 2/17/90 RLR                                              **
+ ** Revised: 12/27/90 SRD,AJ,BSK,JT Reference C version               **
+ ** Revised (for MD5): RLR 4/27/91                                    **
+ ***********************************************************************
+ */
+
+/*
+ ***********************************************************************
+ ** Copyright (C) 1990, RSA Data Security, Inc. All rights reserved.  **
+ **                                                                   **
+ ** License to copy and use this software is granted provided that    **
+ ** it is identified as the "RSA Data Security, Inc. MD5 Message-     **
+ ** Digest Algorithm" in all material mentioning or referencing this  **
+ ** software or this function.                                        **
+ **                                                                   **
+ ** License is also granted to make and use derivative works          **
+ ** provided that such works are identified as "derived from the RSA  **
+ ** Data Security, Inc. MD5 Message-Digest Algorithm" in all          **
+ ** material mentioning or referencing the derived work.              **
+ **                                                                   **
+ ** RSA Data Security, Inc. makes no representations concerning       **
+ ** either the merchantability of this software or the suitability    **
+ ** of this software for any particular purpose.  It is provided "as  **
+ ** is" without express or implied warranty of any kind.              **
+ **                                                                   **
+ ** These notices must be retained in any copies of any part of this  **
+ ** documentation and/or software.                                    **
+ ***********************************************************************
+ */
+
+#ifdef HAS_STDINT_H
+#include <stdint.h>
+#elif defined(HAS_INTTYPES_H)
+#include <inttypes.h>
+#endif
+
+/* typedef a 32-bit type */
+typedef uint32_t UINT4;
+
+/* Data structure for MD5 (Message-Digest) computation */
+typedef struct {
+  UINT4 i[2];                   /* number of _bits_ handled mod 2^64 */
+  UINT4 buf[4];                                    /* scratch buffer */
+  unsigned char in[64];                              /* input buffer */
+  unsigned char digest[16];     /* actual digest after MD5Final call */
+} MD5_CTX;
+
+void MD5Init (MD5_CTX *mdContext);
+void MD5Update (MD5_CTX *, unsigned const char *, unsigned int);
+void MD5Final (MD5_CTX *);
+
+/*
+ ***********************************************************************
+ ** End of md5.h                                                      **
+ ******************************** (cut) ********************************
+ */

+ 15 - 0
polipo/md5import.c

@@ -0,0 +1,15 @@
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE
+#endif
+
+#include <stdlib.h>
+
+#if defined __STDC_VERSION__ && __STDC_VERSION__ >= 199901L
+#define HAS_STDINT_H
+#else
+typedef unsigned int my_uint32_t;
+#undef uint32_t
+#define uint32_t my_uint32_t
+#endif
+#include "md5.c"
+#undef uint32_t

+ 9 - 0
polipo/md5import.h

@@ -0,0 +1,9 @@
+#if defined __STDC_VERSION__ && __STDC_VERSION__ >= 199901L
+#define HAS_STDINT_H
+#else
+typedef unsigned int my_uint32_t;
+#undef uint32_t
+#define uint32_t my_uint32_t
+#endif
+#include "md5.h"
+#undef uint32_t

+ 505 - 0
polipo/mingw.c

@@ -0,0 +1,505 @@
+/*
+Copyright (c) 2006 by Dan Kennedy.
+Copyright (c) 2006 by Juliusz Chroboczek.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+#include "polipo.h"
+
+#ifndef WIN32 /*MINGW*/
+
+static int dummy ATTRIBUTE((unused));
+
+#else
+
+#undef poll
+#undef socket
+#undef connect
+#undef accept
+#undef shutdown
+#undef getpeername
+#undef sleep
+#undef inet_aton
+#undef gettimeofday
+#undef stat
+
+/* Windows needs this header file for the implementation of inet_aton() */
+#include <ctype.h>
+
+/* _snprintf and friends have broken NULL termination semantics */
+int win32_snprintf(char* dest, size_t count, const char* format, ...)
+{
+    int r;
+    va_list args;
+    va_start(args, format);
+    r = _vsnprintf(dest, count, format, args);
+    va_end(args);
+    if (count > 0) {
+        dest[count-1] = '\0';
+    }
+    return r;
+}
+
+/* 
+ * Check whether "cp" is a valid ascii representation of an Internet address
+ * and convert to a binary address.  Returns 1 if the address is valid, 0 if
+ * not.  This replaces inet_addr, the return value from which cannot
+ * distinguish between failure and a local broadcast address.
+ *
+ * This implementation of the standard inet_aton() function was copied 
+ * (with trivial modifications) from the OpenBSD project.
+ */
+int
+win32_inet_aton(const char *cp, struct in_addr *addr)
+{
+    register unsigned int val;
+    register int base, n;
+    register char c;
+    unsigned int parts[4];
+    register unsigned int *pp = parts;
+
+    assert(sizeof(val) == 4);
+
+    c = *cp;
+    while(1) {
+        /*
+         * Collect number up to ``.''.
+         * Values are specified as for C:
+         * 0x=hex, 0=octal, isdigit=decimal.
+         */
+        if(!isdigit(c))
+            return (0);
+        val = 0; base = 10;
+        if(c == '0') {
+            c = *++cp;
+            if(c == 'x' || c == 'X')
+                base = 16, c = *++cp;
+            else
+                base = 8;
+        }
+        while(1) {
+            if(isascii(c) && isdigit(c)) {
+                val = (val * base) + (c - '0');
+                c = *++cp;
+            } else if(base == 16 && isascii(c) && isxdigit(c)) {
+                val = (val << 4) |
+                    (c + 10 - (islower(c) ? 'a' : 'A'));
+                c = *++cp;
+            } else
+                break;
+        }
+        if(c == '.') {
+            /*
+             * Internet format:
+             *    a.b.c.d
+             *    a.b.c    (with c treated as 16 bits)
+             *    a.b    (with b treated as 24 bits)
+             */
+            if(pp >= parts + 3)
+                return (0);
+            *pp++ = val;
+            c = *++cp;
+        } else
+            break;
+    }
+    /*
+     * Check for trailing characters.
+     */
+    if(c != '\0' && (!isascii(c) || !isspace(c)))
+        return (0);
+    /*
+     * Concoct the address according to
+     * the number of parts specified.
+     */
+    n = pp - parts + 1;
+    switch(n) {
+
+    case 0:
+        return (0);        /* initial nondigit */
+
+    case 1:                /* a -- 32 bits */
+        break;
+
+    case 2:                /* a.b -- 8.24 bits */
+        if((val > 0xffffff) || (parts[0] > 0xff))
+            return (0);
+        val |= parts[0] << 24;
+        break;
+
+    case 3:                /* a.b.c -- 8.8.16 bits */
+        if((val > 0xffff) || (parts[0] > 0xff) || (parts[1] > 0xff))
+            return (0);
+        val |= (parts[0] << 24) | (parts[1] << 16);
+        break;
+
+    case 4:                /* a.b.c.d -- 8.8.8.8 bits */
+        if((val > 0xff) || (parts[0] > 0xff) ||
+           (parts[1] > 0xff) || (parts[2] > 0xff))
+            return (0);
+        val |= (parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8);
+        break;
+    }
+    if(addr)
+        addr->s_addr = htonl(val);
+    return (1);
+}
+
+unsigned int
+win32_sleep(unsigned int seconds)
+{
+    Sleep(seconds * 1000);
+    return 0;
+}
+
+int
+win32_gettimeofday(struct timeval *tv, char *tz)
+{
+    const long long EPOCHFILETIME = (116444736000000000LL);
+    FILETIME        ft;
+    LARGE_INTEGER   li;
+    long long        t;
+
+    /* This implementation doesn't support the timezone parameter. That's Ok,
+     * as at present polipo always passed NULL as the second arg. We
+     * also need to make sure that we have at least 8 bytes of space to
+     * do the math in - otherwise there will be overflow errors.
+     */
+    assert(tz == NULL);
+    assert(sizeof(t) == 8);
+
+    if(tv) {
+        GetSystemTimeAsFileTime(&ft);
+        li.LowPart  = ft.dwLowDateTime;
+        li.HighPart = ft.dwHighDateTime;
+        t  = li.QuadPart;       /* In 100-nanosecond intervals */
+        t -= EPOCHFILETIME;     /* Offset to the Epoch time */
+        t /= 10;                /* In microseconds */
+        tv->tv_sec  = (long)(t / 1000000);
+        tv->tv_usec = (long)(t % 1000000);
+    }
+    return 0;
+}
+
+int win32_poll(struct pollfd *fds, unsigned int nfds, int timo)
+{
+    struct timeval timeout, *toptr;
+    fd_set ifds, ofds, efds, *ip, *op;
+    int i, rc;
+
+    /* Set up the file-descriptor sets in ifds, ofds and efds. */
+    FD_ZERO(&ifds);
+    FD_ZERO(&ofds);
+    FD_ZERO(&efds);
+    for (i = 0, op = ip = 0; i < nfds; ++i) {
+	fds[i].revents = 0;
+	if(fds[i].events & (POLLIN|POLLPRI)) {
+		ip = &ifds;
+		FD_SET(fds[i].fd, ip);
+	}
+	if(fds[i].events & POLLOUT) {
+		op = &ofds;
+		FD_SET(fds[i].fd, op);
+	}
+	FD_SET(fds[i].fd, &efds);
+    } 
+
+    /* Set up the timeval structure for the timeout parameter */
+    if(timo < 0) {
+	toptr = 0;
+    } else {
+	toptr = &timeout;
+	timeout.tv_sec = timo / 1000;
+	timeout.tv_usec = (timo - timeout.tv_sec * 1000) * 1000;
+    }
+
+#ifdef DEBUG_POLL
+    printf("Entering select() sec=%ld usec=%ld ip=%lx op=%lx\n",
+           (long)timeout.tv_sec, (long)timeout.tv_usec, (long)ip, (long)op);
+#endif
+    rc = select(0, ip, op, &efds, toptr);
+#ifdef DEBUG_POLL
+    printf("Exiting select rc=%d\n", rc);
+#endif
+
+    if(rc <= 0)
+	return rc;
+
+    if(rc > 0) {
+        for (i = 0; i < nfds; ++i) {
+            int fd = fds[i].fd;
+    	if(fds[i].events & (POLLIN|POLLPRI) && FD_ISSET(fd, &ifds))
+    		fds[i].revents |= POLLIN;
+    	if(fds[i].events & POLLOUT && FD_ISSET(fd, &ofds))
+    		fds[i].revents |= POLLOUT;
+    	if(FD_ISSET(fd, &efds))
+    		/* Some error was detected ... should be some way to know. */
+    		fds[i].revents |= POLLHUP;
+#ifdef DEBUG_POLL
+        printf("%d %d %d revent = %x\n", 
+                FD_ISSET(fd, &ifds), FD_ISSET(fd, &ofds), FD_ISSET(fd, &efds), 
+                fds[i].revents
+        );
+#endif
+        }
+    }
+    return rc;
+}
+
+int win32_close_socket(SOCKET fd) {
+    int rc;
+
+    rc = closesocket(fd);
+    return 0;
+}
+
+static void
+set_errno(int winsock_err)
+{
+    switch(winsock_err) {
+        case WSAEWOULDBLOCK:
+            errno = EAGAIN;
+            break;
+        default:
+            errno = winsock_err;
+            break;
+    }
+}
+
+int win32_write_socket(SOCKET fd, void *buf, int n)
+{
+    int rc = send(fd, buf, n, 0);
+    if(rc == SOCKET_ERROR) {
+        set_errno(WSAGetLastError());
+    }
+    return rc;
+}
+
+int win32_read_socket(SOCKET fd, void *buf, int n)
+{
+    int rc = recv(fd, buf, n, 0);
+    if(rc == SOCKET_ERROR) {
+        set_errno(WSAGetLastError());
+    }
+    return rc;
+}
+
+
+/*
+ * Set the "non-blocking" flag on socket fd to the value specified by
+ * the second argument (i.e. if the nonblocking argument is non-zero, the
+ * socket is set to non-blocking mode). Zero is returned if the operation
+ * is successful, other -1.
+ */
+int
+win32_setnonblocking(SOCKET fd, int nonblocking)
+{
+    int rc;
+
+    unsigned long mode = 1;
+    rc = ioctlsocket(fd, FIONBIO, &mode);
+    if(rc != 0) {
+        set_errno(WSAGetLastError());
+    }
+    return (rc == 0 ? 0 : -1);
+}
+
+/*
+ * A wrapper around the socket() function. The purpose of this wrapper
+ * is to ensure that the global errno symbol is set if an error occurs,
+ * even if we are using winsock.
+ */
+SOCKET
+win32_socket(int domain, int type, int protocol)
+{
+    SOCKET fd = socket(domain, type, protocol);
+    if(fd == INVALID_SOCKET) {
+        set_errno(WSAGetLastError());
+    }
+    return fd;
+}
+
+static void
+set_connect_errno(int winsock_err)
+{
+    switch(winsock_err) {
+        case WSAEINVAL:
+        case WSAEALREADY:
+        case WSAEWOULDBLOCK:
+            errno = EINPROGRESS;
+            break;
+        default:
+            errno = winsock_err;
+            break;
+    }
+}
+
+/*
+ * A wrapper around the connect() function. The purpose of this wrapper
+ * is to ensure that the global errno symbol is set if an error occurs,
+ * even if we are using winsock.
+ */
+int
+win32_connect(SOCKET fd, struct sockaddr *addr, socklen_t addr_len)
+{
+    int rc = connect(fd, addr, addr_len);
+    assert(rc == 0 || rc == SOCKET_ERROR);
+    if(rc == SOCKET_ERROR) {
+        set_connect_errno(WSAGetLastError());
+    }
+    return rc;
+}
+
+/*
+ * A wrapper around the accept() function. The purpose of this wrapper
+ * is to ensure that the global errno symbol is set if an error occurs,
+ * even if we are using winsock.
+ */
+SOCKET
+win32_accept(SOCKET fd, struct sockaddr *addr, socklen_t *addr_len)
+{
+    SOCKET newfd = accept(fd, addr, addr_len);
+    if(newfd == INVALID_SOCKET) {
+        set_errno(WSAGetLastError());
+        newfd = -1;
+    }
+    return newfd;
+}
+
+/*
+ * A wrapper around the shutdown() function. The purpose of this wrapper
+ * is to ensure that the global errno symbol is set if an error occurs,
+ * even if we are using winsock.
+ */
+int
+win32_shutdown(SOCKET fd, int mode)
+{
+    int rc = shutdown(fd, mode);
+    assert(rc == 0 || rc == SOCKET_ERROR);
+    if(rc == SOCKET_ERROR) {
+        set_errno(WSAGetLastError());
+    }
+    return rc;
+}
+
+/*
+ * A wrapper around the getpeername() function. The purpose of this wrapper
+ * is to ensure that the global errno symbol is set if an error occurs,
+ * even if we are using winsock.
+ */
+int
+win32_getpeername(SOCKET fd, struct sockaddr *name, socklen_t *namelen)
+{
+    int rc = getpeername(fd, name, namelen);
+    assert(rc == 0 || rc == SOCKET_ERROR);
+    if(rc == SOCKET_ERROR) {
+        set_errno(WSAGetLastError());
+    }
+    return rc;
+}
+
+/* Stat doesn't work on directories if the name ends in a slash. */
+
+int
+win32_stat(const char *filename, struct stat *ss)
+{
+    int len, rc, saved_errno;
+    char *noslash;
+
+    len = strlen(filename);
+    if(len <= 1 || filename[len - 1] != '/')
+        return stat(filename, ss);
+
+    noslash = malloc(len);
+    if(noslash == NULL)
+        return -1;
+
+    memcpy(noslash, filename, len - 1);
+    noslash[len - 1] = '\0';
+
+    rc = stat(noslash, ss);
+    saved_errno = errno;
+    free(noslash);
+    errno = saved_errno;
+    return rc;
+}
+#endif /* #ifdef WIN32 MINGW */
+
+#ifndef HAVE_READV_WRITEV
+
+int
+polipo_writev(int fd, const struct iovec *vector, int count)
+{
+    int rc;                     /* Return Code */
+    if(count == 1) {
+        rc = WRITE(fd, vector->iov_base, vector->iov_len);
+    } else {
+        int n = 0;              /* Total bytes to write */
+        char *buf = 0;          /* Buffer to copy to before writing */
+        int i;                  /* Counter var for looping over vector[] */
+        int offset = 0;        /* Offset for copying to buf */
+
+        /* Figure out the required buffer size */
+        for(i = 0; i < count; i++) {
+            n += vector[i].iov_len;
+        }
+
+        /* Allocate the buffer. If the allocation fails, bail out */
+        buf = malloc(n);
+        if(!buf) {
+            errno = ENOMEM;
+            return -1;
+        }
+
+        /* Copy the contents of the vector array to the buffer */
+        for(i = 0; i < count; i++) {
+            memcpy(&buf[offset], vector[i].iov_base, vector[i].iov_len);
+            offset += vector[i].iov_len;
+        }
+        assert(offset == n);
+
+        /* Write the entire buffer to the socket and free the allocation */
+        rc = WRITE(fd, buf, n);
+        free(buf);
+    }
+    return rc;
+}
+
+int
+polipo_readv(int fd, const struct iovec *vector, int count)
+{
+    int ret = 0;                     /* Return value */
+    int i;
+    for(i = 0; i < count; i++) {
+        int n = vector[i].iov_len;
+        int rc = READ(fd, vector[i].iov_base, n);
+        if(rc == n) {
+            ret += rc;
+        } else {
+            if(rc < 0) {
+                ret = (ret == 0 ? rc : ret);
+            } else {
+                ret += rc;
+            }
+            break;
+        }
+    }
+    return ret;
+}
+#endif

+ 195 - 0
polipo/mingw.h

@@ -0,0 +1,195 @@
+/*
+Copyright (c) 2006 by Dan Kennedy.
+Copyright (c) 2006 by Juliusz Chroboczek.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+/* 
+ * Polipo was originally designed to run on Unix-like systems. This
+ * header file (and it's accompanying implementation file mingw.c) contain
+ * code that allows polipo to run on Microsoft Windows too. 
+ *
+ * The target MS windows compiler is Mingw (MINimal Gnu for Windows). The
+ * code in this file probably get's us pretty close to MSVC also, but
+ * this has not been tested. To build polipo for Mingw, define the MINGW
+ * symbol. For Unix or Unix-like systems, leave it undefined.
+ */
+
+#ifdef WIN32 /*MINGW*/
+
+/* Unfortunately, there's no hiding it. */
+#define HAVE_WINSOCK 1
+
+#ifndef WINVER
+#define WINVER 0x0501
+#endif
+
+#ifndef _WIN32_WINNT
+#define _WIN32_WINNT 0x0501
+#endif
+
+#ifndef _WIN32_WINDOWS
+#define _WIN32_WINDOWS 0x0410
+#endif
+
+#ifndef _WIN32_IE
+#define _WIN32_IE 0x0700
+#endif
+
+/* At time of writing, a fair bit of stuff doesn't work under Mingw.
+ * Hopefully they will be fixed later (especially the disk-cache).
+ */
+#define NO_IPv6 1
+
+#define S_IROTH S_IREAD
+
+/* Pull in winsock2.h for (almost) berkeley sockets. */
+#include <winsock2.h>
+
+// here we smash the errno defines so we can smash the socket functions later to smash errno. yay!
+#ifdef ENOTCONN
+  #undef ENOTCONN
+#endif
+#ifdef EWOULDBLOCK
+  #undef EWOULDBLOCK
+#endif
+#ifdef ENOBUFS
+  #undef ENOBUFS
+#endif
+#ifdef ECONNRESET
+  #undef ECONNRESET
+#endif
+#ifdef EAFNOSUPPORT
+  #undef EAFNOSUPPORT
+#endif
+#ifdef EPROTONOSUPPORT
+  #undef EPROTONOSUPPORT
+#endif
+#ifdef EINPROGRESS
+  #undef EINPROGRESS
+#endif
+#ifdef EISCONN
+  #undef EISCONN
+#endif
+#define ENOTCONN        WSAENOTCONN
+#define EWOULDBLOCK     WSAEWOULDBLOCK
+#define ENOBUFS         WSAENOBUFS
+#define ECONNRESET      WSAECONNRESET
+#define ESHUTDOWN       WSAESHUTDOWN
+#define EAFNOSUPPORT    WSAEAFNOSUPPORT
+#define EPROTONOSUPPORT WSAEPROTONOSUPPORT
+#define EINPROGRESS     WSAEINPROGRESS
+#define EISCONN         WSAEISCONN
+
+
+#include <direct.h>
+#include <io.h>
+#include <process.h>
+
+/* winsock doesn't feature poll(), so there is a version implemented
+ * in terms of select() in mingw.c. The following definitions
+ * are copied from linux man pages. A poll() macro is defined to
+ * call the version in mingw.c.
+ */
+#define POLLIN      0x0001    /* There is data to read */
+#define POLLPRI     0x0002    /* There is urgent data to read */
+#define POLLOUT     0x0004    /* Writing now will not block */
+#define POLLERR     0x0008    /* Error condition */
+#define POLLHUP     0x0010    /* Hung up */
+#define POLLNVAL    0x0020    /* Invalid request: fd not open */
+struct pollfd {
+    SOCKET fd;        /* file descriptor */
+    short events;     /* requested events */
+    short revents;    /* returned events */
+};
+#define poll(x, y, z)        win32_poll(x, y, z)
+
+/* These wrappers do nothing special except set the global errno variable if
+ * an error occurs (winsock doesn't do this by default). They set errno
+ * to unix-like values (i.e. WSAEWOULDBLOCK is mapped to EAGAIN), so code
+ * outside of this file "shouldn't" have to worry about winsock specific error
+ * handling.
+ */
+#define socket(x, y, z)      win32_socket(x, y, z)
+#define connect(x, y, z)     win32_connect(x, y, z)
+#define accept(x, y, z)      win32_accept(x, y, z)
+#define shutdown(x, y)       win32_shutdown(x, y)
+#define getpeername(x, y, z) win32_getpeername(x, y, z)
+
+/* Wrapper macros to call misc. functions mingw is missing */
+#define sleep(x)             win32_sleep(x)
+#define inet_aton(x, y)      win32_inet_aton(x, y)
+#define gettimeofday(x, y)   win32_gettimeofday(x, y)
+#define stat(x, y)           win32_stat(x, y)
+#define snprintf             win32_snprintf
+
+#define mkdir(x, y) mkdir(x)
+#define getcwd _getcwd
+#define getpid _getpid
+#ifndef S_ISDIR
+#define S_ISDIR(mode)  (((mode) & S_IFMT) == S_IFDIR)
+#endif
+#ifndef S_ISREG
+#define S_ISREG(mode)  (((mode) & S_IFMT) == S_IFREG)
+#endif
+
+/* Winsock uses int instead of the usual socklen_t */
+typedef int socklen_t;
+
+/* Function prototypes for functions in mingw.c */
+unsigned int win32_sleep(unsigned int);
+int     win32_inet_aton(const char *, struct in_addr *);
+int     win32_gettimeofday(struct timeval *, char *);
+int     win32_poll(struct pollfd *, unsigned int, int);
+SOCKET  win32_socket(int, int, int);
+int     win32_connect(SOCKET, struct sockaddr*, socklen_t);
+SOCKET  win32_accept(SOCKET, struct sockaddr*, socklen_t *);
+int     win32_shutdown(SOCKET, int);
+int     win32_getpeername(SOCKET, struct sockaddr*, socklen_t *);
+int     win32_snprintf(char* dest, size_t count, const char* format, ...);
+
+/* Three socket specific macros */
+#define READ(x, y, z)  win32_read_socket(x, y, z)
+#define WRITE(x, y, z) win32_write_socket(x, y, z)
+#define CLOSE(x)       win32_close_socket(x)
+
+int win32_read_socket(SOCKET, void *, int);
+int win32_write_socket(SOCKET, void *, int);
+int win32_close_socket(SOCKET);
+
+int win32_setnonblocking(SOCKET, int);
+int win32_stat(const char*, struct stat*);
+#endif
+
+#ifndef HAVE_READV_WRITEV
+/*
+ * The HAVE_READV_WRITEV symbol should be defined if the system features
+ * the vector IO functions readv() and writev() and those functions may
+ * be legally used with sockets.
+ */
+struct iovec {
+    void *iov_base;   /* Starting address */
+    size_t iov_len;   /* Number of bytes */
+};
+#define WRITEV(x, y, z) polipo_writev(x, y, z)
+#define READV(x, y, z)  polipo_readv(x, y, z)
+int polipo_readv(int fd, const struct iovec *vector, int count);
+int polipo_writev(int fd, const struct iovec *vector, int count);
+#endif

+ 1040 - 0
polipo/object.c

@@ -0,0 +1,1040 @@
+/*
+Copyright (c) 2003-2006 by Juliusz Chroboczek
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+#include "polipo.h"
+
+int mindlesslyCacheVary = 0;
+int objectHashTableSize = 0;
+int log2ObjectHashTableSize;
+
+static ObjectPtr object_list = NULL;
+static ObjectPtr object_list_end = NULL;
+
+int objectExpiryScheduled;
+
+int publicObjectCount;
+int privateObjectCount;
+int cacheIsShared = 1;
+int publicObjectLowMark = 0, objectHighMark = 2048;
+
+static ObjectPtr *objectHashTable;
+int maxExpiresAge = (30 * 24 + 1) * 3600;
+int maxAge = (14 * 24 + 1) * 3600;
+float maxAgeFraction = 0.1;
+int maxNoModifiedAge = 23 * 60;
+int maxWriteoutWhenIdle = 64 * 1024;
+int maxObjectsWhenIdle = 32;
+int idleTime = 20;
+int dontCacheCookies = 0;
+
+void
+preinitObject()
+{
+    CONFIG_VARIABLE_SETTABLE(idleTime, CONFIG_TIME, configIntSetter,
+                             "Time to remain idle before writing out.");
+    CONFIG_VARIABLE_SETTABLE(maxWriteoutWhenIdle, CONFIG_INT, configIntSetter,
+                             "Amount of data to write at a time when idle.");
+    CONFIG_VARIABLE_SETTABLE(maxObjectsWhenIdle, CONFIG_INT, configIntSetter,
+                             "Number of objects to write at a time "
+                             "when idle.");
+    CONFIG_VARIABLE_SETTABLE(cacheIsShared, CONFIG_BOOLEAN, configIntSetter,
+                             "If false, ignore s-maxage and private.");
+    CONFIG_VARIABLE_SETTABLE(mindlesslyCacheVary, CONFIG_BOOLEAN,
+                             configIntSetter,
+                             "If true, mindlessly cache negotiated objects.");
+    CONFIG_VARIABLE(objectHashTableSize, CONFIG_INT,
+                    "Size of the object hash table (0 = auto).");
+    CONFIG_VARIABLE(objectHighMark, CONFIG_INT,
+                    "High object count mark.");
+    CONFIG_VARIABLE(publicObjectLowMark, CONFIG_INT,
+                    "Low object count mark (0 = auto).");
+    CONFIG_VARIABLE_SETTABLE(maxExpiresAge, CONFIG_TIME, configIntSetter,
+                             "Max age for objects with Expires header.");
+    CONFIG_VARIABLE_SETTABLE(maxAge, CONFIG_TIME, configIntSetter,
+                             "Max age for objects without Expires header.");
+    CONFIG_VARIABLE_SETTABLE(maxAgeFraction, CONFIG_FLOAT, configFloatSetter,
+                             "Fresh fraction of modification time.");
+    CONFIG_VARIABLE_SETTABLE(maxNoModifiedAge, CONFIG_TIME, configIntSetter,
+                             "Max age for objects without Last-modified.");
+    CONFIG_VARIABLE_SETTABLE(dontCacheCookies, CONFIG_BOOLEAN, configIntSetter,
+                             "Work around cachable cookies.");
+}
+
+void
+initObject()
+{
+    int q;
+    if(objectHighMark < 16) {
+        objectHighMark = 16;
+        do_log(L_WARN, "Impossibly low objectHighMark -- setting to %d.\n",
+               objectHighMark);
+    }
+
+    q = 0;
+    if(publicObjectLowMark == 0) q = 1;
+    if(publicObjectLowMark < 8 || publicObjectLowMark >= objectHighMark - 4) {
+        publicObjectLowMark = objectHighMark / 2;
+        if(!q)
+            do_log(L_WARN, "Impossible publicObjectLowMark value -- "
+                   "setting to %d.\n", publicObjectLowMark);
+    }
+
+    q = 1;
+    if(objectHashTableSize <= objectHighMark / 2 ||
+       objectHashTableSize > objectHighMark * 1024) {
+        if(objectHashTableSize != 0) q = 0;
+        objectHashTableSize = objectHighMark * 16;
+    }
+    log2ObjectHashTableSize = log2_ceil(objectHashTableSize);
+    objectHashTableSize = 1 << log2ObjectHashTableSize;
+    if(!q)
+        do_log(L_WARN, "Suspicious objectHashTableSize value -- "
+               "setting to %d.\n", objectHashTableSize);
+
+    object_list = NULL;
+    object_list_end = NULL;
+    publicObjectCount = 0;
+    privateObjectCount = 0;
+    objectHashTable = calloc(1 << log2ObjectHashTableSize,
+                             sizeof(ObjectPtr));
+    if(!objectHashTable) {
+        do_log(L_ERROR, "Couldn't allocate object hash table.\n");
+        exit(1);
+    }
+}
+
+ObjectPtr
+findObject(int type, const void *key, int key_size)
+{
+    int h;
+    ObjectPtr object;
+
+    if(key_size >= 50000)
+        return NULL;
+
+    h = hash(type, key, key_size, log2ObjectHashTableSize);
+    object = objectHashTable[h];
+    if(!object)
+        return NULL;
+    if(object->type != type || object->key_size != key_size ||
+       memcmp(object->key, key, key_size) != 0) {
+        return NULL;
+    }
+    if(object->next)
+        object->next->previous = object->previous;
+    if(object->previous)
+        object->previous->next = object->next;
+    if(object_list == object)
+        object_list = object->next;
+    if(object_list_end == object)
+        object_list_end = object->previous;
+    object->previous = NULL;
+    object->next = object_list;
+    if(object_list)
+        object_list->previous = object;
+    object_list = object;
+    if(!object_list_end)
+        object_list_end = object;
+    return retainObject(object);
+}
+
+ObjectPtr
+makeObject(int type, const void *key, int key_size, int public, int fromdisk,
+           RequestFunction request, void* request_closure)
+{
+    ObjectPtr object;
+    int h;
+
+    object = findObject(type, key, key_size);
+    if(object != NULL) {
+        if(public)
+            return object;
+        else
+            privatiseObject(object, 0);
+    }
+
+    if(publicObjectCount + privateObjectCount >= objectHighMark) {
+        if(!objectExpiryScheduled)
+            discardObjects(0, 0);
+        if(publicObjectCount + privateObjectCount >= objectHighMark) {
+            return NULL;
+        }
+    }
+
+    if(publicObjectCount >= publicObjectLowMark && 
+       !objectExpiryScheduled) {
+        TimeEventHandlerPtr event;
+        event = scheduleTimeEvent(-1, discardObjectsHandler, 0, NULL);
+        if(event)
+            objectExpiryScheduled = 1;
+        else
+            do_log(L_ERROR, "Couldn't schedule object expiry.\n");
+    }
+
+    object = malloc(sizeof(ObjectRec));
+    if(object == NULL)
+        return NULL;
+
+    object->type = type;
+    object->request = request;
+    object->request_closure = request_closure;
+    object->key = malloc(key_size + 1);
+    if(object->key == NULL) {
+        free(object);
+        return NULL;
+    }
+    memcpy(object->key, key, key_size);
+    /* In order to make it more convenient to use keys as strings,
+       they are NUL-terminated. */
+    object->key[key_size] = '\0';
+    object->key_size = key_size;
+    object->flags = (public?OBJECT_PUBLIC:0) | OBJECT_INITIAL;
+    if(public) {
+        h = hash(object->type, object->key, object->key_size, 
+                 log2ObjectHashTableSize);
+        if(objectHashTable[h]) {
+            writeoutToDisk(objectHashTable[h], objectHashTable[h]->size, -1);
+            privatiseObject(objectHashTable[h], 0);
+            assert(!objectHashTable[h]);
+        }
+        objectHashTable[h] = object;
+        object->next = object_list;
+        object->previous = NULL;
+        if(object_list)
+            object_list->previous = object;
+        object_list = object;
+        if(!object_list_end)
+            object_list_end = object;
+    } else {
+        object->next = NULL;
+        object->previous = NULL;
+    }
+    object->abort_data = NULL;
+    object->code = 0;
+    object->message = NULL;
+    initCondition(&object->condition);
+    object->headers = NULL;
+    object->via = NULL;
+    object->numchunks = 0;
+    object->chunks = NULL;
+    object->length = -1;
+    object->date = -1;
+    object->age = -1;
+    object->expires = -1;
+    object->last_modified = -1;
+    object->atime = -1;
+    object->etag = NULL;
+    object->cache_control = 0;
+    object->max_age = -1;
+    object->s_maxage = -1;
+    object->size = 0;
+    object->requestor = NULL;
+    object->disk_entry = NULL;
+    if(object->flags & OBJECT_PUBLIC)
+        publicObjectCount++;
+    else
+        privateObjectCount++;
+    object->refcount = 1;
+
+    if(public && fromdisk)
+        objectGetFromDisk(object);
+    return object;
+}
+
+void 
+objectMetadataChanged(ObjectPtr object, int revalidate)
+{
+    if(revalidate) {
+        revalidateDiskEntry(object);
+    } else {
+        object->flags &= ~OBJECT_DISK_ENTRY_COMPLETE;
+        dirtyDiskEntry(object);
+    }
+    return;
+}
+
+ObjectPtr
+retainObject(ObjectPtr object)
+{
+    do_log(D_REFCOUNT, "O 0x%lx %d++\n",
+           (unsigned long)object, object->refcount);
+    object->refcount++;
+    return object;
+}
+
+void
+releaseObject(ObjectPtr object)
+{
+    do_log(D_REFCOUNT, "O 0x%lx %d--\n",
+           (unsigned long)object, object->refcount);
+    object->refcount--;
+    if(object->refcount == 0) {
+        assert(!object->condition.handlers && 
+               !(object->flags & OBJECT_INPROGRESS));
+        if(!(object->flags & OBJECT_PUBLIC))
+            destroyObject(object);
+    }
+}
+
+void
+releaseNotifyObject(ObjectPtr object)
+{
+    do_log(D_REFCOUNT, "O 0x%lx %d--\n",
+           (unsigned long)object, object->refcount);
+    object->refcount--;
+    if(object->refcount > 0) {
+        notifyObject(object);
+    } else {
+        assert(!object->condition.handlers && 
+               !(object->flags & OBJECT_INPROGRESS));
+        if(!(object->flags & OBJECT_PUBLIC))
+            destroyObject(object);
+    }
+}
+
+void 
+lockChunk(ObjectPtr object, int i)
+{
+    do_log(D_LOCK, "Lock 0x%lx[%d]: ", (unsigned long)object, i);
+    assert(i >= 0);
+    if(i >= object->numchunks)
+        objectSetChunks(object, i + 1);
+    object->chunks[i].locked++;
+    do_log(D_LOCK, "%d\n", object->chunks[i].locked);
+}
+
+void 
+unlockChunk(ObjectPtr object, int i)
+{
+    do_log(D_LOCK, "Unlock 0x%lx[%d]: ", (unsigned long)object, i);
+    assert(i >= 0 && i < object->numchunks);
+    assert(object->chunks[i].locked > 0);
+    object->chunks[i].locked--;
+    do_log(D_LOCK, "%d\n", object->chunks[i].locked);
+}
+
+int
+objectSetChunks(ObjectPtr object, int numchunks)
+{
+    int n;
+
+    if(numchunks <= object->numchunks)
+        return 0;
+
+    if(object->length >= 0)
+        n = MAX(numchunks, (object->length + (CHUNK_SIZE - 1)) / CHUNK_SIZE);
+    else
+        n = MAX(numchunks, 
+                MAX(object->numchunks + 2, object->numchunks * 5 / 4));
+    
+    if(n == 0) {
+        assert(object->chunks == NULL);
+    } else if(object->numchunks == 0) {
+        object->chunks = calloc(n, sizeof(ChunkRec));
+        if(object->chunks == NULL) {
+            return -1;
+        }
+        object->numchunks = n;
+    } else {
+        ChunkPtr newchunks;
+        newchunks = realloc(object->chunks, n * sizeof(ChunkRec));
+        if(newchunks == NULL)
+            return -1;
+        memset(newchunks + object->numchunks, 0,
+               (n - object->numchunks) * sizeof(ChunkRec));
+        object->chunks = newchunks;
+        object->numchunks = n;
+    }
+    return 0;
+}
+
+ObjectPtr
+objectPartial(ObjectPtr object, int length, struct _Atom *headers)
+{
+    object->headers = headers;
+
+    if(length >= 0) {
+        if(object->size > length) {
+            abortObject(object, 502,
+                        internAtom("Inconsistent Content-Length"));
+            notifyObject(object);
+            return object;
+        }
+    }
+
+    if(length >= 0)
+        object->length = length;
+
+    object->flags &= ~OBJECT_INITIAL;
+    revalidateDiskEntry(object);
+    notifyObject(object);
+    return object;
+}
+
+static int
+objectAddChunk(ObjectPtr object, const char *data, int offset, int plen)
+{
+    int i = offset / CHUNK_SIZE;
+    int rc;
+
+    assert(offset % CHUNK_SIZE == 0);
+    assert(plen <= CHUNK_SIZE);
+
+    if(object->numchunks <= i) {
+        rc = objectSetChunks(object, i + 1);
+        if(rc < 0)
+            return -1;
+    }
+
+    lockChunk(object, i);
+
+    if(object->chunks[i].data == NULL) {
+        object->chunks[i].data = get_chunk();
+        if(object->chunks[i].data == NULL)
+            goto fail;
+    }
+
+    if(object->chunks[i].size >= plen) {
+        unlockChunk(object, i);
+        return 0;
+    }
+
+    if(object->size < offset + plen)
+        object->size = offset + plen;
+    object->chunks[i].size = plen;
+    memcpy(object->chunks[i].data, data, plen);
+    unlockChunk(object, i);
+    return 0;
+
+ fail:
+    unlockChunk(object, i);
+    return -1;
+}
+
+static int
+objectAddChunkEnd(ObjectPtr object, const char *data, int offset, int plen)
+{
+    int i = offset / CHUNK_SIZE;
+    int rc;
+
+    assert(offset % CHUNK_SIZE != 0 && 
+           offset % CHUNK_SIZE + plen <= CHUNK_SIZE);
+
+    if(object->numchunks <= i) {
+        rc = objectSetChunks(object, i + 1);
+        if(rc < 0)
+            return -1;
+    }
+
+    lockChunk(object, i);
+
+    if(object->chunks[i].data == NULL)
+        object->chunks[i].data = get_chunk();
+    if(object->chunks[i].data == NULL)
+        goto fail;
+
+    if(offset > object->size) {
+        goto fail;
+    }
+
+    if(object->chunks[i].size < offset % CHUNK_SIZE) {
+        goto fail;
+    }
+
+    if(object->size < offset + plen)
+        object->size = offset + plen;
+    object->chunks[i].size = offset % CHUNK_SIZE + plen;
+    memcpy(object->chunks[i].data + (offset % CHUNK_SIZE),
+           data, plen);
+
+    unlockChunk(object, i);
+    return 0;
+
+ fail:
+    unlockChunk(object, i);
+    return -1;
+}
+
+int
+objectAddData(ObjectPtr object, const char *data, int offset, int len)
+{
+    int rc;
+
+    do_log(D_OBJECT_DATA, "Adding data to 0x%lx (%d) at %d: %d bytes\n",
+           (unsigned long)object, object->length, offset, len);
+
+    if(len == 0)
+        return 1;
+
+    if(object->length >= 0) {
+        if(offset + len > object->length) {
+            do_log(L_ERROR, 
+                   "Inconsistent object length (%d, should be at least %d).\n",
+                   object->length, offset + len);
+            object->length = offset + len;
+        }
+    }
+            
+    object->flags &= ~OBJECT_FAILED;
+
+    if(offset + len >= object->numchunks * CHUNK_SIZE) {
+        rc = objectSetChunks(object, (offset + len - 1) / CHUNK_SIZE + 1);
+        if(rc < 0) {
+            return -1;
+        }
+    }
+
+    if(offset % CHUNK_SIZE != 0) {
+        int plen = CHUNK_SIZE - offset % CHUNK_SIZE;
+        if(plen >= len)
+            plen = len;
+        rc = objectAddChunkEnd(object, data, offset, plen);
+        if(rc < 0) {
+            return -1;
+        }            
+        offset += plen;
+        data += plen;
+        len -= plen;
+    }
+
+    while(len > 0) {
+        int plen = (len >= CHUNK_SIZE) ? CHUNK_SIZE : len;
+        rc = objectAddChunk(object, data, offset, plen);
+        if(rc < 0) {
+            return -1;
+        }
+        offset += plen;
+        data += plen;
+        len -= plen;
+    }
+
+    return 1;
+}
+
+void
+objectPrintf(ObjectPtr object, int offset, const char *format, ...)
+{
+    char *buf;
+    int rc;
+
+    va_list args;
+    va_start(args, format);
+    buf = vsprintf_a(format, args);
+    va_end(args);
+
+    if(buf == NULL) {
+        abortObject(object, 500, internAtom("Couldn't allocate string"));
+        return;
+    }
+
+    rc = objectAddData(object, buf, offset, strlen(buf));
+    free(buf);
+    if(rc < 0)
+        abortObject(object, 500, internAtom("Couldn't add data to object"));
+}
+
+int 
+objectHoleSize(ObjectPtr object, int offset)
+{
+    int size = 0, i;
+
+    if(offset < 0 || offset / CHUNK_SIZE >= object->numchunks)
+        return -1;
+
+    if(offset % CHUNK_SIZE != 0) {
+        if(object->chunks[offset / CHUNK_SIZE].size > offset % CHUNK_SIZE)
+            return 0;
+        else {
+            size += CHUNK_SIZE - offset % CHUNK_SIZE;
+            offset += CHUNK_SIZE - offset % CHUNK_SIZE;
+            if(offset < 0) {
+                /* Overflow */
+                return -1;
+            }
+        }
+    }
+
+    for(i = offset / CHUNK_SIZE; i < object->numchunks; i++) {
+        if(object->chunks[i].size == 0)
+            size += CHUNK_SIZE;
+        else
+            break;
+    }
+    if(i >= object->numchunks)
+        return -1;
+    return size;
+}
+
+
+/* Returns 2 if the data is wholly in memory, 1 if it's available on disk.
+   If the client request was a Range request, from & to specify the requested
+   range; otherwise 'from' is 0 and 'to' is -1. */
+int
+objectHasData(ObjectPtr object, int from, int to)
+{
+    int first, last, i, upto;
+
+    if(to < 0) {
+        if(object->length >= 0)
+            to = object->length;
+        else
+            return 0;
+    }
+
+    first = from / CHUNK_SIZE;
+    last = to / CHUNK_SIZE;
+
+    if(from >= to)
+        return 2;
+
+    if(to > object->size) {
+        upto = to;
+        goto disk;
+    }
+
+    if(last > object->numchunks ||
+       object->chunks[last].size > to % CHUNK_SIZE) {
+        upto = to;
+        goto disk;
+    }
+
+    for(i = last - 1; i >= first; i--) {
+        if(object->chunks[i].size < CHUNK_SIZE) {
+            upto = (i + 1) * CHUNK_SIZE;
+            goto disk;
+        }
+    }
+
+    return 2;
+
+ disk:
+    if(object->flags & OBJECT_DISK_ENTRY_COMPLETE)
+        return 1;
+
+    if(diskEntrySize(object) >= upto)
+        return 1;
+
+    return 0;
+}
+
+void
+destroyObject(ObjectPtr object)
+{
+    int i;
+
+    assert(object->refcount == 0 && !object->requestor);
+    assert(!object->condition.handlers && 
+           (object->flags & OBJECT_INPROGRESS) == 0);
+
+    if(object->disk_entry)
+        destroyDiskEntry(object, 0);
+
+    if(object->flags & OBJECT_PUBLIC) {
+        privatiseObject(object, 0);
+    } else {
+        object->type = -1;
+        if(object->message) releaseAtom(object->message);
+        if(object->key) free(object->key);
+        if(object->headers) releaseAtom(object->headers);
+        if(object->etag) free(object->etag);
+        if(object->via) releaseAtom(object->via);
+        for(i = 0; i < object->numchunks; i++) {
+            assert(!object->chunks[i].locked);
+            if(object->chunks[i].data)
+                dispose_chunk(object->chunks[i].data);
+            object->chunks[i].data = NULL;
+            object->chunks[i].size = 0;
+        }
+        if(object->chunks) free(object->chunks);
+        privateObjectCount--;
+        free(object);
+    }
+}
+
+void
+privatiseObject(ObjectPtr object, int linear) 
+{
+    int i, h;
+    if(!(object->flags & OBJECT_PUBLIC)) {
+        if(linear)
+            object->flags |= OBJECT_LINEAR;
+        return;
+    }
+
+    if(object->disk_entry)
+        destroyDiskEntry(object, 0);
+    object->flags &= ~OBJECT_PUBLIC;
+
+    for(i = 0; i < object->numchunks; i++) {
+        if(object->chunks[i].locked)
+            break;
+        if(object->chunks[i].data) {
+            object->chunks[i].size = 0;
+            dispose_chunk(object->chunks[i].data);
+            object->chunks[i].data = NULL;
+        }
+    }
+
+    h = hash(object->type, object->key, object->key_size, 
+             log2ObjectHashTableSize);
+    assert(objectHashTable[h] == object);
+    objectHashTable[h] = NULL;
+
+    if(object->previous)
+        object->previous->next = object->next;
+    if(object_list == object)
+        object_list = object->next;
+    if(object->next)
+        object->next->previous = object->previous;
+    if(object_list_end == object)
+        object_list_end = object->previous;
+    object->previous = NULL;
+    object->next = NULL;
+
+    publicObjectCount--;
+    privateObjectCount++;
+
+    if(object->refcount == 0)
+        destroyObject(object);
+    else {
+        if(linear)
+            object->flags |= OBJECT_LINEAR;
+    }
+}
+
+void
+abortObject(ObjectPtr object, int code, AtomPtr message)
+{
+    int i;
+
+    assert(code != 0);
+
+    object->flags &= ~(OBJECT_INITIAL | OBJECT_VALIDATING);
+    object->flags |= OBJECT_ABORTED;
+    object->code = code;
+    if(object->message) releaseAtom(object->message);
+    object->message = message;
+    object->length = 0;
+    object->date = object->age;
+    object->expires = object->age;
+    object->last_modified = -1;
+    if(object->etag) free(object->etag);
+    object->etag = NULL;
+    if(object->headers) releaseAtom(object->headers); 
+    object->headers = NULL;
+    object->size = 0;
+    for(i = 0; i < object->numchunks; i++) {
+        if(object->chunks[i].data) {
+            if(!object->chunks[i].locked) {
+                dispose_chunk(object->chunks[i].data);
+                object->chunks[i].data = NULL;
+                object->chunks[i].size = 0;
+            }
+        }
+    }
+    privatiseObject(object, 0);
+}
+
+void 
+supersedeObject(ObjectPtr object)
+{
+    object->flags |= OBJECT_SUPERSEDED;
+    destroyDiskEntry(object, 1);
+    privatiseObject(object, 0);
+    notifyObject(object);
+}
+
+void
+notifyObject(ObjectPtr object) 
+{
+    retainObject(object);
+    signalCondition(&object->condition);
+    releaseObject(object);
+}
+
+int
+discardObjectsHandler(TimeEventHandlerPtr event)
+{
+    return discardObjects(0, 0);
+}
+
+void
+writeoutObjects(int all)
+{
+    ObjectPtr object = object_list;
+    int bytes;
+    int objects;
+    int n;
+
+    if(diskIsClean) return;
+
+    objects = 0;
+    bytes = 0;
+    while(object) {
+        do {
+            if(!all) {
+                if(objects >= maxObjectsWhenIdle || 
+                   bytes >= maxWriteoutWhenIdle) {
+                    if(workToDo()) return;
+                    objects = 0;
+                    bytes = 0;
+                }
+            }
+            n = writeoutToDisk(object, -1, all ? -1 : maxWriteoutWhenIdle);
+            bytes += n;
+        } while(!all && n == maxWriteoutWhenIdle);
+        objects++;
+        object = object->next;
+    }
+    diskIsClean = 1;
+}
+
+int
+discardObjects(int all, int force)
+{
+    ObjectPtr object;
+    int i;
+    static int in_discardObjects = 0;
+    TimeEventHandlerPtr event;
+
+    if(in_discardObjects)
+        return 0;
+
+    in_discardObjects = 1;
+    
+    if(all || force || used_chunks >= CHUNKS(chunkHighMark) ||
+       publicObjectCount >= publicObjectLowMark ||
+       publicObjectCount + privateObjectCount >= objectHighMark) {
+        object = object_list_end;
+        while(object && 
+              (all || force || used_chunks >= CHUNKS(chunkLowMark))) {
+            if(force || ((object->flags & OBJECT_PUBLIC) &&
+                         object->numchunks > CHUNKS(chunkLowMark) / 4)) {
+                int j;
+                for(j = 0; j < object->numchunks; j++) {
+                    if(object->chunks[j].locked) {
+                        break;
+                    }
+                    if(object->chunks[j].size < CHUNK_SIZE) {
+                        continue;
+                    }
+                    writeoutToDisk(object, (j + 1) * CHUNK_SIZE, -1);
+                    dispose_chunk(object->chunks[j].data);
+                    object->chunks[j].data = NULL;
+                    object->chunks[j].size = 0;
+                }
+            }
+            object = object->previous;
+        }
+        
+        i = 0;
+        object = object_list_end;
+        while(object && 
+              (all || force ||
+               used_chunks - i > CHUNKS(chunkLowMark) ||
+               used_chunks > CHUNKS(chunkCriticalMark) ||
+               publicObjectCount > publicObjectLowMark)) {
+            ObjectPtr next_object = object->previous;
+            if(object->refcount == 0) {
+                i += object->numchunks;
+                writeoutToDisk(object, object->size, -1);
+                privatiseObject(object, 0);
+            } else if(all || force) {
+                writeoutToDisk(object, object->size, -1);
+                destroyDiskEntry(object, 0);
+            }
+            object = next_object;
+        }
+
+        object = object_list_end;
+        if(force || used_chunks > CHUNKS(chunkCriticalMark)) {
+            if(used_chunks > CHUNKS(chunkCriticalMark)) {
+                do_log(L_WARN, 
+                       "Short on chunk memory -- "
+                       "attempting to punch holes "
+                       "in the middle of objects.\n");
+            }
+            while(object && 
+                  (force || used_chunks > CHUNKS(chunkCriticalMark))) {
+                if(force || (object->flags & OBJECT_PUBLIC)) {
+                    int j;
+                    for(j = object->numchunks - 1; j >= 0; j--) {
+                        if(object->chunks[j].locked)
+                            continue;
+                        if(object->chunks[j].size < CHUNK_SIZE)
+                            continue;
+                        writeoutToDisk(object, (j + 1) * CHUNK_SIZE, -1);
+                        dispose_chunk(object->chunks[j].data);
+                        object->chunks[j].data = NULL;
+                        object->chunks[j].size = 0;
+                    }
+                }
+                object = object->previous;
+            }
+        }
+        event = scheduleTimeEvent(2, discardObjectsHandler, 0, NULL);
+        if(event) {
+            objectExpiryScheduled = 1;
+        } else {
+            objectExpiryScheduled = 0;
+            do_log(L_ERROR, "Couldn't schedule object expiry.\n");
+        }
+    } else {
+        objectExpiryScheduled = 0;
+    }
+
+    if(all) {
+        if(privateObjectCount + publicObjectCount != 0) {
+            do_log(L_WARN,
+                   "Discarded all objects, "
+                   "%d + %d objects left (%d chunks and %d atoms used).\n",
+                   publicObjectCount, privateObjectCount,
+                   used_chunks, used_atoms);
+        } else if(used_chunks != 0) {
+            do_log(L_WARN,
+                   "Discarded all objects, "
+                   "%d chunks and %d atoms left.\n",
+                   used_chunks, used_atoms);
+        }
+        diskIsClean = 1;
+    }
+
+    in_discardObjects = 0;
+    return 1;
+}
+
+CacheControlRec no_cache_control = {0, -1, -1, -1, -1};
+
+int
+objectIsStale(ObjectPtr object, CacheControlPtr cache_control)
+{
+    int stale = 0x7FFFFFFF;
+    int flags;
+    int max_age, s_maxage;
+    time_t date;
+
+    if(object->flags & OBJECT_INITIAL)
+        return 0;
+
+    if(object->date >= 0)
+        date = object->date;
+    else if(object->age >= 0)
+        date = object->age;
+    else
+        date = current_time.tv_sec;
+
+    if(cache_control == NULL)
+        cache_control = &no_cache_control;
+    flags = object->cache_control | cache_control->flags;
+
+    if(cache_control->max_age >= 0) {
+        if(object->max_age >= 0)
+            max_age = MIN(cache_control->max_age, object->max_age);
+        else
+            max_age = cache_control->max_age;
+    } else
+        max_age = object->max_age;
+
+    if(cache_control->s_maxage >= 0) {
+        if(object->s_maxage >= 0)
+            s_maxage = MIN(cache_control->s_maxage, object->s_maxage);
+        else
+            s_maxage = cache_control->s_maxage;
+    } else
+        s_maxage = object->s_maxage;
+    
+    if(max_age >= 0)
+        stale = MIN(stale, object->age + max_age);
+
+    if(cacheIsShared && s_maxage >= 0)
+        stale = MIN(stale, object->age + s_maxage);
+
+    if(object->expires >= 0 || object->max_age >= 0)
+        stale = MIN(stale, object->age + maxExpiresAge);
+    else
+        stale = MIN(stale, object->age + maxAge);
+
+    /* RFC 2616 14.9.3: server-side max-age overrides expires */
+
+    if(object->expires >= 0 && object->max_age < 0) {
+        /* This protects against clock skew */
+        stale = MIN(stale, object->age + object->expires - date);
+    }
+
+    if(object->expires < 0 && object->max_age < 0) {
+        /* No server-side information -- heuristic expiration */
+        if(object->last_modified >= 0)
+            /* Again, take care of clock skew */
+            stale = MIN(stale,
+                        object->age +
+                        (date - object->last_modified) * maxAgeFraction);
+        else
+            stale = MIN(stale, object->age + maxNoModifiedAge);
+    }
+
+    if(!(flags & CACHE_MUST_REVALIDATE) &&
+       !(cacheIsShared && (flags & CACHE_PROXY_REVALIDATE))) {
+        /* Client side can relax transparency */
+        if(cache_control->min_fresh >= 0) {
+            if(cache_control->max_stale >= 0)
+                stale = MIN(stale - cache_control->min_fresh,
+                            stale + cache_control->max_stale);
+            else
+                stale = stale - cache_control->min_fresh;
+        } else if(cache_control->max_stale >= 0) {
+            stale = stale + cache_control->max_stale;
+        }
+    }
+
+    return current_time.tv_sec > stale;
+}
+
+int
+objectMustRevalidate(ObjectPtr object, CacheControlPtr cache_control)
+{
+    int flags;
+
+    if(cache_control == NULL)
+        cache_control = &no_cache_control;
+    if(object)
+        flags = object->cache_control | cache_control->flags;
+    else
+        flags = cache_control->flags;
+    
+    if(flags & (CACHE_NO | CACHE_NO_HIDDEN | CACHE_NO_STORE))
+        return 1;
+
+    if(cacheIsShared && (flags & CACHE_PRIVATE))
+        return 1;
+
+    if(!mindlesslyCacheVary && (flags & CACHE_VARY))
+        return 1;
+
+    if(dontCacheCookies && (flags & CACHE_COOKIE))
+        return 1;
+
+    if(object)
+        return objectIsStale(object, cache_control);
+
+    return 0;
+}
+

+ 197 - 0
polipo/object.h

@@ -0,0 +1,197 @@
+/*
+Copyright (c) 2003-2006 by Juliusz Chroboczek
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+#undef MAX
+#undef MIN
+
+#define MAX(x,y) ((x)<=(y)?(y):(x))
+#define MIN(x,y) ((x)<=(y)?(x):(y))
+
+struct _HTTPRequest;
+
+#if defined(USHRT_MAX) && CHUNK_SIZE <= USHRT_MAX
+typedef unsigned short chunk_size_t;
+#else
+typedef unsigned int chunk_size_t;
+#endif
+
+typedef struct _Chunk {
+    short int locked;
+    chunk_size_t size;
+    char *data;
+} ChunkRec, *ChunkPtr;
+
+struct _Object;
+
+typedef int (*RequestFunction)(struct _Object *, int, int, int,
+                               struct _HTTPRequest*, void*);
+
+typedef struct _Object {
+    short refcount;
+    unsigned char type;
+    RequestFunction request;
+    void *request_closure;
+    char *key;
+    unsigned short key_size;
+    unsigned short flags;
+    unsigned short code;
+    void *abort_data;
+    struct _Atom *message;
+    int length;
+    time_t date;
+    time_t age;
+    time_t expires;
+    time_t last_modified;
+    time_t atime;
+    char *etag;
+    unsigned short cache_control;
+    int max_age;
+    int s_maxage;
+    struct _Atom *headers;
+    struct _Atom *via;
+    int size;
+    int numchunks;
+    ChunkPtr chunks;
+    void *requestor;
+    struct _Condition condition;
+    struct _DiskCacheEntry *disk_entry;
+    struct _Object *next, *previous;
+} ObjectRec, *ObjectPtr;
+
+typedef struct _CacheControl {
+    int flags;
+    int max_age;
+    int s_maxage;
+    int min_fresh;
+    int max_stale;
+} CacheControlRec, *CacheControlPtr;
+
+extern int cacheIsShared;
+extern int mindlesslyCacheVary;
+
+extern CacheControlRec no_cache_control;
+extern int objectExpiryScheduled;
+extern int publicObjectCount;
+extern int privateObjectCount;
+extern int idleTime;
+
+extern const time_t time_t_max;
+
+extern int publicObjectLowMark, objectHighMark;
+
+extern int log2ObjectHashTableSize;
+
+/* object->type */
+#define OBJECT_HTTP 1
+#define OBJECT_DNS 2
+
+/* object->flags */
+/* object is public */
+#define OBJECT_PUBLIC 1
+/* object hasn't got any headers yet */
+#define OBJECT_INITIAL 2
+/* a server connection is already taking care of the object */
+#define OBJECT_INPROGRESS 4
+/* the object has been superseded -- don't try to fetch it */
+#define OBJECT_SUPERSEDED 8
+/* the object is private and aditionally can only be used by its requestor */
+#define OBJECT_LINEAR 16
+/* the object is currently being validated */
+#define OBJECT_VALIDATING 32
+/* object has been aborted */
+#define OBJECT_ABORTED 64
+/* last object request was a failure */
+#define OBJECT_FAILED 128
+/* Object is a local file */
+#define OBJECT_LOCAL 256
+/* The object's data has been entirely written out to disk */
+#define OBJECT_DISK_ENTRY_COMPLETE 512
+/* The object is suspected to be dynamic -- don't PMM */
+#define OBJECT_DYNAMIC 1024
+/* Used for synchronisation between client and server. */
+#define OBJECT_MUTATING 2048
+
+/* object->cache_control and connection->cache_control */
+/* RFC 2616 14.9 */
+/* Non-standard: like no-cache, but kept internally */
+#define CACHE_NO_HIDDEN 1
+/* no-cache */
+#define CACHE_NO 2
+/* public */
+#define CACHE_PUBLIC 4
+/* private */
+#define CACHE_PRIVATE 8
+/* no-store */
+#define CACHE_NO_STORE 16
+/* no-transform */
+#define CACHE_NO_TRANSFORM 32
+/* must-revalidate */
+#define CACHE_MUST_REVALIDATE 64
+/* proxy-revalidate */
+#define CACHE_PROXY_REVALIDATE 128
+/* only-if-cached */
+#define CACHE_ONLY_IF_CACHED 256
+/* set if Vary header; treated as no-cache */
+#define CACHE_VARY 512
+/* set if Authorization header; treated specially */
+#define CACHE_AUTHORIZATION 1024
+/* set if cookie */
+#define CACHE_COOKIE 2048
+/* set if this object should never be combined with another resource */
+#define CACHE_MISMATCH 4096
+
+struct _HTTPRequest;
+
+void preinitObject(void);
+void initObject(void);
+ObjectPtr findObject(int type, const void *key, int key_size);
+ObjectPtr makeObject(int type, const void *key, int key_size,
+                     int public, int fromdisk,
+                     int (*request)(ObjectPtr, int, int, int, 
+                                    struct _HTTPRequest*, void*), void*);
+void objectMetadataChanged(ObjectPtr object, int dirty);
+ObjectPtr retainObject(ObjectPtr);
+void releaseObject(ObjectPtr);
+int objectSetChunks(ObjectPtr object, int numchunks);
+void lockChunk(ObjectPtr, int);
+void unlockChunk(ObjectPtr, int);
+void destroyObject(ObjectPtr object);
+void privatiseObject(ObjectPtr object, int linear);
+void abortObject(ObjectPtr object, int code, struct _Atom *message);
+void supersedeObject(ObjectPtr);
+void notifyObject(ObjectPtr);
+void releaseNotifyObject(ObjectPtr);
+ObjectPtr objectPartial(ObjectPtr object, int length, struct _Atom *headers);
+int objectHoleSize(ObjectPtr object, int offset)
+    ATTRIBUTE ((pure));
+int objectHasData(ObjectPtr object, int from, int to)
+    ATTRIBUTE ((pure));
+int objectAddData(ObjectPtr object, const char *data, int offset, int len);
+void objectPrintf(ObjectPtr object, int offset, const char *format, ...)
+     ATTRIBUTE ((format (printf, 3, 4)));
+int discardObjectsHandler(TimeEventHandlerPtr);
+void writeoutObjects(int);
+int discardObjects(int all, int force);
+int objectIsStale(ObjectPtr object, CacheControlPtr cache_control)
+    ATTRIBUTE ((pure));
+int objectMustRevalidate(ObjectPtr object, CacheControlPtr cache_control)
+    ATTRIBUTE ((pure));

+ 215 - 0
polipo/parse_time.c

@@ -0,0 +1,215 @@
+/*
+Copyright (c) 2003-2006 by Juliusz Chroboczek
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+#include "polipo.h"
+
+const time_t time_t_max = ((time_t)~(1U << 31));
+
+static inline int
+d2i(char c)
+{
+    if(c >= '0' && c <= '9')
+        return c - '0';
+    else
+        return -1;
+}
+
+static int
+parse_int(const char *buf, int i, int len, int *val_return)
+{
+    int val, d;
+
+    if(i >= len)
+        return -1;
+
+    val = d2i(buf[i]);
+    if(val < 0)
+        return -1;
+    else
+        i++;
+
+    while(i < len) {
+        d = d2i(buf[i]);
+        if(d < 0)
+            break;
+        val = val * 10 + d;
+        i++;
+    }
+    *val_return = val;
+    return i;
+}
+
+static const char month_names[12][3] = {
+    "jan", "feb", "mar", "apr", "may", "jun",
+    "jul", "aug", "sep", "oct", "nov", "dec",
+};
+
+static int
+skip_word(const char *buf, int i, int len)
+{
+    if(i >= len)
+        return -1;
+
+    if(!letter(buf[i]))
+       return -1;
+
+    while(i < len) {
+        if(!letter(buf[i]))
+            break;
+        i++;
+    }
+
+    return i;
+}
+
+static int
+parse_month(const char *buf, int i, int len, int *val_return)
+{
+    int j, k, l;
+    j = skip_word(buf, i, len);
+    if(j != i + 3)
+        return -1;
+    for(k = 0; k < 12; k++) {
+        for(l = 0; l < 3; l++) {
+            if(lwr(buf[i + l]) != month_names[k][l])
+                break;
+        }
+        if(l == 3)
+            break;
+    }
+    if(k >= 12)
+        return -1;
+    *val_return = k;
+    return j;
+}
+
+static int
+issep(char c)
+{
+    return c == ' ' || c == '\t' || c == ',' || c == ':' || c == '-';
+}
+
+int
+skip_separator(const char *buf, int i, int len)
+{
+    if(i >= len)
+        return -1;
+
+    if(issep(buf[i]))
+        i++;
+    else
+        return -1;
+
+    while(i < len) {
+        if(issep(buf[i]))
+            i++;
+        else
+            break;
+    }
+    return i;
+}
+
+int
+parse_time(const char *buf, int offset, int len, time_t *time_return)
+{
+    struct tm tm;
+    time_t t;
+    int i = offset;
+
+    i = skip_word(buf, i, len); if(i < 0) return -1;
+    i = skip_separator(buf, i, len); if(i < 0) return -1;
+
+    if(i >= len)
+        return -1;
+
+    if(d2i(buf[i]) >= 0) {
+        i = parse_int(buf, i, len, &tm.tm_mday); if(i < 0) return -1;
+        i = skip_separator(buf, i, len); if(i < 0) return -1;
+        i = parse_month(buf, i, len, &tm.tm_mon); if(i < 0) return -1;
+        i = skip_separator(buf, i, len); if(i < 0) return -1;
+        i = parse_int(buf, i, len, &tm.tm_year); if(i < 0) return -1;
+        if(tm.tm_year < 100)
+            tm.tm_year += 1900;
+        if(tm.tm_year < 1937)
+            tm.tm_year += 100;
+        if(tm.tm_year < 1937)
+            return -1;
+
+        i = skip_separator(buf, i, len); if(i < 0) return -1;
+        i = parse_int(buf, i, len, &tm.tm_hour); if(i < 0) return -1;
+        i = skip_separator(buf, i, len); if(i < 0) return -1;
+        i = parse_int(buf, i, len, &tm.tm_min); if(i < 0) return -1;
+        i = skip_separator(buf, i, len); if(i < 0) return -1;
+        i = parse_int(buf, i, len, &tm.tm_sec); if(i < 0) return -1;
+        i = skip_separator(buf, i, len); if(i < 0) return -1;
+        i = skip_word(buf, i, len); if(i < 0) return -1;
+    } else {                    /* funny American format */
+        i = parse_month(buf, i, len, &tm.tm_mon); if(i < 0) return -1;
+        i = skip_separator(buf, i, len); if(i < 0) return -1;
+        i = parse_int(buf, i, len, &tm.tm_mday); if(i < 0) return -1;
+        i = skip_separator(buf, i, len); if(i < 0) return -1;
+        i = parse_int(buf, i, len, &tm.tm_hour); if(i < 0) return -1;
+        i = skip_separator(buf, i, len); if(i < 0) return -1;
+        i = parse_int(buf, i, len, &tm.tm_min); if(i < 0) return -1;
+        i = skip_separator(buf, i, len); if(i < 0) return -1;
+        i = parse_int(buf, i, len, &tm.tm_sec); if(i < 0) return -1;
+        i = skip_separator(buf, i, len); if(i < 0) return -1;
+        i = parse_int(buf, i, len, &tm.tm_year); if(i < 0) return -1;
+        if(tm.tm_year < 100)
+            tm.tm_year += 1900;
+        if(tm.tm_year < 1937)
+            tm.tm_year += 100;
+        if(tm.tm_year < 1937 || tm.tm_year > 2040)
+            return -1;
+    }
+
+    if(tm.tm_year < 2038) {
+        tm.tm_year -= 1900;
+        tm.tm_isdst = -1;
+        t = mktime_gmt(&tm);
+        if(t == -1)
+            return -1;
+    } else {
+        t = time_t_max;
+    }
+
+    *time_return = t;
+    return i;
+}
+
+int
+format_time(char *buf, int i, int len, time_t t)
+{
+    struct tm *tm;
+    int rc;
+
+    if(i < 0 || i > len)
+        return -1;
+
+    tm = gmtime(&t);
+    if(tm == NULL)
+        return -1;
+    rc = strftime(buf + i, len - i, "%a, %d %b %Y %H:%M:%S GMT", tm);
+    if(rc <= 0)                 /* yes, that's <= */
+        return -1;
+    return i + rc;
+}

+ 27 - 0
polipo/parse_time.h

@@ -0,0 +1,27 @@
+/*
+Copyright (c) 2003-2006 by Juliusz Chroboczek
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+/* XXX */
+extern const time_t time_t_max;
+
+int parse_time(const char *buf, int i, int len, time_t *time_return);
+int format_time(char *buf, int i, int len, time_t t);

+ 232 - 0
polipo/polipo.h

@@ -0,0 +1,232 @@
+/*
+Copyright (c) 2003-2006 by Juliusz Chroboczek
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE
+#endif
+
+#ifndef WIN32
+#include <sys/param.h>
+#endif
+
+#include <limits.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <errno.h>
+#include <string.h>
+#include <assert.h>
+#ifndef _WIN32
+#include <unistd.h>
+#include <sys/time.h>
+#include <dirent.h>
+#else
+#include "dirent_compat.h"
+#endif
+#include <fcntl.h>
+#include <time.h>
+#include <sys/stat.h>
+#ifndef WIN32 /*MINGW*/
+#include <sys/mman.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <netinet/tcp.h>
+#include <arpa/inet.h>
+#include <netdb.h>
+#include <sys/types.h>
+#include <sys/uio.h>
+#include <poll.h>
+#include <sys/wait.h>
+#include <sys/ioctl.h>
+#include <signal.h>
+#endif
+
+#ifdef __MINGW32__
+#define MINGW
+#endif
+
+#ifndef MAP_ANONYMOUS
+#define MAP_ANONYMOUS MAP_ANON
+#endif
+
+#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L
+/* nothing */
+#elif defined(__GNUC__)
+#define inline __inline
+#if  (__GNUC__ >= 3)
+#define restrict __restrict
+#else
+#define restrict /**/
+#endif
+#else
+#define inline /**/
+#define restrict /**/
+#endif
+
+#if defined(__GNUC__) && (__GNUC__ >= 3)
+#define ATTRIBUTE(x) __attribute__(x)
+#else
+#define ATTRIBUTE(x) /**/
+#endif
+
+/* Musl doesn't have a specific test, so assume musl if Linux and neither
+   __GLIBC__ nor __UCLIBC__ */
+
+#if defined __linux
+#define HAVE_TM_GMTOFF
+#define HAVE_SETENV
+#define HAVE_ASPRINTF
+#define HAVE_MEMRCHR
+#ifdef __GLIBC__
+#define HAVE_FTS
+#endif
+#ifndef __UCLIBC__
+#define HAVE_FFSL
+#define HAVE_FFSLL
+#define HAVE_TIMEGM
+#endif
+#endif
+
+#ifdef BSD
+#define HAVE_TM_GMTOFF
+#define HAVE_FTS
+#define HAVE_SETENV
+#endif
+
+#ifdef __CYGWIN__
+#define HAVE_SETENV
+#define HAVE_ASPRINTF
+#endif
+
+#ifndef O_BINARY
+#define O_BINARY 0
+#endif
+
+#define HAVE_TZSET
+
+#if _POSIX_VERSION >= 200112L
+#define HAVE_SETENV
+#endif
+
+#ifndef NO_IPv6
+
+#ifdef __GLIBC__
+#if (__GLIBC__ > 2) || (__GLIBC__ == 2 && __GLIBC_MINOR__ >= 2)
+#define HAVE_IPv6
+#endif
+#endif
+
+#ifdef __FreeBSD__
+#define HAVE_ASPRINTF
+#if __FreeBSD_version >= 400000
+#define HAVE_IPv6
+#define HAVE_TIMEGM
+#endif
+#if __FreeBSD_version >= 503001
+#define HAVE_FFSL
+#endif
+#endif
+
+#ifdef __NetBSD__
+#if __NetBSD_Version__ >= 105000000
+#define HAVE_IPv6
+#endif
+#if __NetBSD_Version__ >= 200000000
+#define HAVE_TIMEGM
+#define HAVE_ASPRINTF
+#endif
+#endif
+
+#ifdef __OpenBSD__
+/* OpenBSD 2.3 and later */
+#define HAVE_IPv6
+#define HAVE_ASPRINTF
+#endif
+
+#ifdef __APPLE__
+#define HAVE_ASPRINTF
+#define HAVE_IPv6
+#define HAVE_TIMEGM
+#define HAVE_FFSL
+#endif
+
+#endif
+
+#if defined(i386) || defined(__mc68020__) || defined(__x86_64__)
+#define UNALIGNED_ACCESS
+#endif
+
+#ifndef WIN32 /*MINGW*/
+#define HAVE_FORK
+#ifndef NO_SYSLOG
+#define HAVE_SYSLOG
+#endif
+#define HAVE_READV_WRITEV
+#define HAVE_FFS
+#define READ(x, y, z) read(x, y, z)
+#define WRITE(x, y, z) write(x, y, z)
+#define CLOSE(x) close(x)
+#else
+#ifndef HAVE_REGEX
+#define NO_FORBIDDEN
+#endif
+#ifndef MINGW
+#define HAVE_MKGMTIME
+#endif
+#endif
+
+#ifdef HAVE_READV_WRITEV
+#define WRITEV(x, y, z) writev(x, y, z)
+#define READV(x, y, z)  readv(x, y, z)
+#endif
+
+#ifndef HAVE_FORK
+#define NO_REDIRECTOR
+#endif
+
+#include "mingw.h"
+
+#include "ftsimport.h"
+#include "atom.h"
+#include "util.h"
+#include "config.h"
+#include "event.h"
+#include "io.h"
+#include "socks.h"
+#include "chunk.h"
+#include "object.h"
+#include "dns.h"
+#include "http.h"
+#include "client.h"
+#include "local.h"
+#include "diskcache.h"
+#include "server.h"
+#include "http_parse.h"
+#include "parse_time.h"
+#include "forbidden.h"
+#include "log.h"
+#include "auth.h"
+#include "tunnel.h"
+
+extern AtomPtr configFile;
+extern int daemonise;
+extern AtomPtr pidFile;

+ 93 - 0
polipo/polipo.man

@@ -0,0 +1,93 @@
+.TH POLIPO 1
+.SH NAME
+polipo \- a caching web proxy
+.SH SYNOPSIS
+.B polipo
+[
+.B \-h
+|
+.B \-v
+|
+.B \-x
+] [
+.B \-c
+.I config
+] [
+.IB var=val
+]...
+.SH DESCRIPTION
+Polipo is a caching HTTP proxy.  It listens to requests for web pages
+from your browser and forwards them to web servers, and forwards the
+servers' replies to your browser.  In the process, it optimises and
+cleans up the network traffic.
+.PP
+By default, Polipo listens on TCP port 8123.  Please configure your
+web browser to use the proxy on 
+.B localhost 
+port 8123.
+.SH OPTIONS
+.TP
+.B \-h
+Display help and exit.
+.TP
+.B \-v
+Display the list of configuration variables and exit.
+.TP
+.B \-x
+Purge the on-disk cache and exit.
+.TP
+.B \-c
+Select an alternate configuration file.
+.TP
+.IB var = val
+Change the value of a configuration variable.
+.SH FILES
+.TP
+.B /etc/polipo/config
+The default location of Polipo's configuration file.
+.TP
+.B /etc/polipo/forbidden
+The default location of the list of forbidden URLs.
+.TP
+.B /var/cache/polipo/
+The default location of the on-disk cache.
+.TP
+.B /usr/share/polipo/www/
+The default root of the local web space.
+.SH SIGNALS
+.TP 
+.B SIGUSR1
+write out all in-memory objects to disk and reload the forbidden URLs file.
+.TP 
+.B SIGUSR2
+write out all in-memory objects to disk, discard all in-memory
+objects, and reload the forbidden URLs file.
+.TP 
+.B SIGTERM, SIGINT, SIGHUP
+write out all in-memory objects to disk and quit.
+.SH SECURITY
+The internal web server will follow symbolic links that point outside
+the local document tree.  You should run Polipo in a chroot jail if that
+is a problem.
+.PP
+There is no reason to run Polipo as root.
+.SH FULL DOCUMENTATION
+The full manual for Polipo is maintained in a texinfo file, and is
+normally available through a web server internal to Polipo.  Please
+make sure that Polipo is running, and point your favourite web browser
+at
+.IP
+http://localhost:8123/
+.PP
+Alternatively, you may type
+.IP
+$ info polipo
+.PP
+at a shell prompt, or
+.IP
+C-h i m polipo RET
+.PP
+in
+.BR Emacs .
+.SH AUTHOR
+Polipo was written by Juliusz Chroboczek.

+ 1964 - 0
polipo/polipo.texi

@@ -0,0 +1,1964 @@
+\input texinfo @c -*-texinfo-*-
+@c %**start of header
+@setfilename polipo.info
+@settitle The Polipo Manual
+@afourpaper
+@c %**end of header
+
+@dircategory Network Applications
+@direntry
+* Polipo: (polipo).                     The Polipo caching web proxy.
+@end direntry
+
+@copying
+Copyright @copyright{} 2003 -- 2014 by Juliusz Chroboczek.
+@end copying
+
+@titlepage
+@title The Polipo Manual
+@author Juliusz Chroboczek
+@page
+@vskip 0pt plus 1fill
+Polipo is a caching web proxy designed to be used as a personal
+cache or a cache shared among a few users.
+@vskip 0pt plus 1fill
+
+@insertcopying
+
+@end titlepage
+
+@contents
+
+@ifnottex
+@node Top, Background, (dir), (dir)
+@top Polipo
+
+Polipo is a caching web proxy designed to be used as a personal
+cache or a cache shared among a few users.
+
+@ifhtml
+The latest version of Polipo can be found on 
+@uref{http://www.pps.univ-paris-diderot.fr/~jch/software/polipo/,the Polipo web page}.
+@end ifhtml
+
+This manual was written by
+@uref{http://www.pps.univ-paris-diderot.fr/~jch/,,Juliusz Chroboczek}.
+
+@end ifnottex
+
+@menu
+* Background::                  Background information.
+* Running::                     Running Polipo
+* Network::                     Polipo and the network.
+* Caching::                     Caching.
+* Memory usage::                Limiting Polipo's memory usage.
+* Copying::                     Your rights and mine.
+* Variable index::              Variable index.
+* Concept index::               Concept index.
+@end menu
+
+@node Background, Running, Top, Top
+@chapter Background
+
+@menu
+* The web::                     The web and HTTP.
+* Proxies and caches::          Proxies and caches.
+* Latency and throughput::      Optimise latency, not throughput.
+* Network traffic::             Be nice to the net.
+* Partial instances::           Don't discard data.
+* POST and PUT::                Other requests
+@end menu
+
+@node The web, Proxies and caches, Background, Background
+@section The web and HTTP
+@cindex URL
+@cindex resource
+@cindex instance
+@cindex entity
+@cindex HTTP
+
+The web is a wide-scale decentralised distributed hypertext system,
+something that's obviously impossible to achieve reliably.
+
+The web is a collection of @dfn{resources} which are identified by
+@dfn{URLs}, strings starting with @code{http://}.  At any point in
+time, a resource has a certain value, which is called an
+@dfn{instance} of the resource.
+
+The fundamental protocol of the web is HTTP, a simple request/response
+protocol.  With HTTP, a client can make a request for a resource to a
+server, and the server replies with an @dfn{entity}, which is an
+on-the-wire representation of an instance or of a fragment thereof.
+
+@node Proxies and caches, Latency and throughput, The web, Background
+@section Proxies and caches
+@cindex proxy
+@cindex caching
+
+A proxy is a program that acts as both a client and a server.  It
+listens for client requests and forwards them to servers, and forwards
+the servers' replies to clients.
+
+An HTTP proxy can optimise web traffic away by @dfn{caching} server
+replies, storing them in memory in case they are needed again.  If a
+reply has been cached, a later client request may, under some
+conditions, be satisfied without going to the source again.
+
+In addition to taking the shortcuts made possible by caching, proxies
+can improve performance by generating better network traffic than the
+client applications would do.
+
+Proxies are also useful in ways unrelated to raw performance.  A proxy
+can be used to contact a server that is not directly accessible to the
+client, for example because there is a firewall in the way
+(@pxref{Parent proxies}), or because the client and the server use
+different lower layer protocols (for example IPv4 and IPv6).  Another
+common application of proxies is to modify the data sent to servers
+and returned to clients, for example by censoring headers that expose
+too much about the client's identity (@pxref{Censoring headers}) or
+removing advertisements from the data returned by the server
+(@pxref{Forbidden}).
+
+Polipo is a caching HTTP proxy that was originally designed as
+a @dfn{personal} proxy, i.e.@: a proxy that is used by a single user
+or a small group of users.  However, it has successfully been used by
+larger groups.
+
+@node Latency and throughput, Network traffic, Proxies and caches, Background
+@section Latency and throughput
+@cindex throughput
+@cindex latency
+
+Most network benchmarks consider @dfn{throughput}, or the average
+amount of data being pushed around per unit of time.  While important
+for batch applications (for example benchmarks), average throughput is
+mostly irrelevant when it comes to interactive web usage.  What is more
+important is a transaction's median @dfn{latency}, or whether the data
+starts to trickle down before the user gets annoyed.
+
+Typical web caches optimise for throughput --- for example, by
+consulting sibling caches before accessing a remote resource.  By
+doing so, they significantly add to the median latency, and therefore
+to the average user frustration.
+
+Polipo was designed to minimise latency.
+
+@node Network traffic, Partial instances, Latency and throughput, Background
+@section Network traffic
+
+The web was developed by people who were interested in text processing
+rather than in networking and, unsurprisingly enough, the first
+versions of the HTTP protocol did not make very good use of network
+resources.  The main problem in HTTP/0.9 and early versions of
+HTTP/1.0 was that a separate TCP connection (``virtual circuit'' for
+them telecom people) was created for every entity transferred.
+
+Opening multiple TCP connections has significant performance
+implications.  Obviously, connection setup and teardown require
+additional packet exchanges which increase network usage and, more
+importantly, latency.
+
+Less obviously, TCP is not optimised for that sort of usage.  TCP aims
+to avoid network @dfn{congestion}, a situation in which the network
+becomes unusable due to overly aggressive traffic patterns.  A correct
+TCP implementation will very carefully probe the network at the
+beginning of every connection, which means that a TCP connection is
+very slow during the first couple of kilobytes transferred, and only
+gets up to speed later.  Because most HTTP entities are small (in the
+1 to 10 kilobytes range), HTTP/0.9 uses TCP where it is most inefficient.
+
+@menu
+* Persistent connections::      Don't shut connections down.
+* Pipelining::                  Send a bunch of requests at once.
+* Poor Mans Multiplexing::      Split requests.
+@end menu
+
+@node Persistent connections, Pipelining, Network traffic, Network traffic
+@subsection Persistent connections
+@cindex persistent connection
+@cindex keep-alive connection
+
+Later HTTP versions allow the transfer of multiple entities on a
+single connection.  A connection that carries multiple entities is
+said to be @dfn{persistent} (or sometimes @dfn{keep-alive}).
+Unfortunately, persistent connections are an optional feature of HTTP,
+even in version 1.1.
+
+Polipo will attempt to use persistent connections on the server side,
+and will honour persistent connection requests from clients.
+
+@node Pipelining, Poor Mans Multiplexing, Persistent connections, Network traffic
+@subsection Pipelining
+@cindex Pipelining
+
+With persistent connections it becomes possible to @dfn{pipeline} or
+@dfn{stream} requests, i.e. to send multiple requests on a single
+connection without waiting for the replies to come back.  Because this
+technique gets the requests to the server faster, it reduces latency.
+Additionally, because multiple requests can often be sent in a single
+packet, pipelining reduces network traffic.
+
+Pipelining is a fairly common technique@footnote{The X11 protocol
+fundamentally relies on pipelining.  NNTP does support pipelining.
+SMTP doesn't, while ESMTP makes it an option.  FTP does support
+pipelining on the control connection.}, but it is not supported by
+HTTP/1.0.  HTTP/1.1 makes pipelining support compulsory in every
+server implementation that can use persistent connections, but there
+are a number of buggy servers that claim to implement HTTP/1.1 but
+don't support pipelining.
+
+Polipo carefully probes for pipelining support in a server and uses
+pipelining if it believes that it is reliable.  Polipo also deeply
+enjoys being pipelined at by a client@footnote{Other client-side
+implementations of HTTP that make use of pipelining include
+@uref{http://www.opera.com/,,Opera},
+@uref{http://www.mozilla.org,,Mozilla}, APT (the package downloader
+used by @uref{http://www.debian.org,,Debian} GNU/Linux) and LFTP.}.
+
+@node Poor Mans Multiplexing,  , Pipelining, Network traffic
+@subsection Poor Man's Multiplexing
+@cindex Poor Man's Multiplexing
+@cindex multiplexing
+
+A major weakness of the HTTP protocol is its inability to share a
+single connection between multiple simultaneous transactions --- to
+@dfn{multiplex} a number of transactions over a single connection.  In
+HTTP, a client can either request all instances sequentially, which
+significantly increases latency, or else open multiple concurrent
+connections, with all the problems that this implies
+(@pxref{Persistent connections}).
+
+Poor Man's Multiplexing (PMM) is a technique that simulates
+multiplexing by requesting an instance in multiple segments; because
+the segments are fetched in independent transactions, they can be
+interleaved with requests for other resources.
+
+Obviously, PMM only makes sense in the presence of persistent
+connections; additionally, it is only effective in the presence of
+pipelining (@pxref{Pipelining}).
+
+PMM poses a number of reliability issues.  If the resource being
+fetched is dynamic, it is quite possible that it will change between
+segments; thus, an implementation making use of PMM needs to be able
+to switch to full-resource retrieval when it detects a dynamic
+resource.
+
+Polipo supports PMM, but it is disabled it by default (@pxref{PMM}).
+
+@node Partial instances, POST and PUT, Network traffic, Background
+@section Caching partial instances
+@cindex partial instance
+@cindex range request
+
+A partial instance is an instance that is being cached but only part
+of which is available in the local cache.  There are three ways in
+which partial instances can arise: client applications requesting only
+part of an instance (Adobe's Acrobat Reader plugin is famous for
+that), a server dropping a connection mid-transfer (because it is
+short on resources, or, surprisingly often, because it is buggy), a
+client dropping a connection (usually because the user pressed
+@emph{stop}).
+
+When an instance is requested that is only partially cached, it is
+possible to request just the missing data by using a feature of HTTP
+known as a @dfn{range} request.  While support for range requests is
+optional, most servers honour them in case of static data (data that
+are stored on disk, rather then being generated on the fly e.g.@: by a
+CGI script).
+
+Caching partial instances has a number of positive effects.  Obviously,
+it reduces the amount of data transmitted as the available data
+needn't be fetched again.  Because it prevents partial data from being
+discarded, it makes it reasonable for a proxy to unconditionally abort
+a download when requested by the user, and therefore reduces network
+traffic.
+
+Polipo caches arbitrary partial instances in its in-memory cache.  It
+will only store the initial segment of a partial instance (from its
+beginning up to its first hole) in its on-disk cache, though.  In
+either case, it will attempt to use range requests to fetch the
+missing data.
+
+@node POST and PUT, , Partial instances, Background
+@section Other requests
+@cindex GET request
+@cindex HEAD request
+@cindex PUT request
+@cindex POST request
+@cindex OPTIONS request
+@cindex DELETE request
+@cindex PROPFIND request
+
+The previous sections pretend that there is only one kind of request
+in HTTP --- the @samp{GET} request.  In fact, there are some others.
+
+The @samp{HEAD} request method retrieves data about an resource.  Polipo
+does not normally use @samp{HEAD}, but will fall back to using it for
+validation it if finds that a given server fails to cooperate with its
+standard validation methods (@pxref{Cache transparency}).  Polipo will
+correctly reply to a client's @samp{HEAD} request.
+
+The @samp{POST} method is used to request that the server should do
+something rather than merely sending an entity; it is usually used
+with HTML forms that have an effect@footnote{HTML forms should use the
+@samp{GET} method when the form has no side-effect as this makes the
+results cacheable.}.  The @samp{PUT} method is used to replace an
+resource with a different instance; it is typically used by web
+publishing applications.
+
+@samp{POST}, @samp{PUT}, @samp{OPTIONS} and @samp{DELETE} requests are handled by
+Polipo pretty much like @samp{GET} and @samp{HEAD}; however, for various
+reasons, some precautions must be taken.  In particular, any cached data
+for the resource they refer to must be discarded, and they can never be
+pipelined.
+
+Finally, HTTP/1.1 includes a convenient backdoor with the
+@samp{CONNECT} method.  For more information, please see
+@ref{Tunnelling connections}.
+
+Polipo does not currently handle the more exotic methods such as
+@samp{PROPFIND}.
+
+@node Running, Network, Background, Top
+@chapter Running Polipo
+
+@menu
+* Polipo Invocation::           Starting Polipo.
+* Browser configuration::       Configuring your browser.
+* Stopping::                    Stopping and refreshing Polipo.
+* Local server::                The local web server and web interface.
+@end menu
+
+@node Polipo Invocation, Browser configuration, Running, Running
+@section Starting Polipo
+@cindex invocation
+
+By default, Polipo runs as a normal foreground job in a terminal in
+which it can log random ``How do you do?'' messages.  With the right
+configuration options, Polipo can run as a daemon.
+
+Polipo is run with the following command line:
+@example
+$ polipo [ -h ] [ -v ] [ -x ] [ -c @var{config} ] [ @var{var}=@var{val}... ]
+@end example
+All flags are optional.  The flag @option{-h} causes Polipo to print a
+short help message and to quit.  The flag @option{-v} causes Polipo to
+list all of its configuration variables and quit.  The flag
+@option{-x} causes Polipo to purge its on-disk cache and then quit
+(@pxref{Purging}).  The flag @option{-c} specifies the configuration
+file to use (by default @file{~/.polipo} or
+@file{/etc/polipo/config}).  Finally, Polipo's configuration can be
+changed on the command line by assigning values to given configuration
+variables.
+
+@menu
+* Configuring Polipo::          Plenty of options.
+* Daemon::                      Running in the background.
+* Logging::                     Funnelling status messages.
+@end menu
+
+@node Configuring Polipo, Daemon, Polipo Invocation, Polipo Invocation
+@subsection Configuration
+@cindex runtime configuration
+@cindex variable
+@cindex configuration variable
+@cindex configuration file
+
+There is a number of variables that you can tweak in order to
+configure Polipo, and they should all be described in this manual
+(@pxref{Variable index}).  You can display the complete, most
+up-to-date list of configuration variables by using the @option{-v}
+command line flag or by accessing the ``current configuration'' page
+of Polipo's web interface (@pxref{Web interface}).  Configuration
+variables can be set either on the command line or else in the
+configuration file given by the @option{-c} command-line flag.
+
+Configuration variables are typed, and @option{-v} will display their
+types.  The type can be of one of the following:
+@itemize @bullet
+@item
+@samp{integer} or @samp{float}: a numeric value;
+
+@item
+@samp{boolean}: a truth value, one of @samp{true} or @samp{false};
+
+@item
+@samp{tristate}: one of @samp{false}, @samp{maybe} or @samp{true};
+
+@item
+@samp{4-state}, one of @samp{false}, @samp{reluctantly},
+@samp{happily} or @samp{true};
+
+@item
+@samp{5-state}, one of @samp{false}, @samp{reluctantly}, @samp{maybe},
+@samp{happily} or @samp{true};
+
+@item
+@samp{atom}, a string written within double quotes @samp{"});
+
+@item
+@samp{list}, a comma-separated list of strings;
+
+@item
+@samp{intlist}, a comma-separated list of integers and ranges of
+integers (of the form `@var{n}--@var{m}').
+@end itemize
+
+The configuration file has a very simple syntax.  All blank lines are
+ignored, as are lines starting with a hash sign @samp{#}.  Other lines
+must be of the form
+@example
+@var{var} = @var{val}
+@end example
+where @var{var} is a variable to set and @var{val} is the value to set
+it to.
+
+It is possible to change the configuration of a running polipo by
+using the local configuration interface (@pxref{Web interface}).
+
+@node Daemon, Logging, Configuring Polipo, Polipo Invocation
+@subsection Running as a daemon
+@cindex daemon
+@cindex terminal
+@cindex pid
+@vindex daemonise
+@vindex pidFile
+
+If the configuration variable @code{daemonise} is set to true, Polipo
+will run as a daemon: it will fork and detach from its controlling
+terminal (if any).  The variable @code{daemonise} defaults to false.
+
+When Polipo is run as a daemon, it can be useful to get it to
+atomically write its @emph{pid} to a file.  If the variable
+@code{pidFile} is defined, it should be the name of a file where
+Polipo will write its @emph{pid}.  If the file already exists when it
+is started, Polipo will refuse to run.
+
+@node Logging,  , Daemon, Polipo Invocation
+@subsection Logging
+@cindex logging
+@vindex logLevel
+@vindex logFile
+@vindex logFilePermissions
+@vindex logSyslog
+@vindex logFacility
+@vindex scrubLogs
+
+When it encounters a difficulty, Polipo will print a friendly message.
+The location where these messages go is controlled by the
+configuration variables @code{logFile} and @code{logSyslog}.
+If @code{logSyslog} is @code{true}, error messages go to the system log
+facility given by @code{logFacility}.  If @code{logFile} is set, it is
+the name of a file where all output will accumulate.  If @code{logSyslog}
+is @code{false} and @code{logFile} is empty, messages go to the error
+output of the process (normally the terminal).
+
+The variable @code{logFile} defaults to empty if @code{daemonise} is
+false, and to @samp{/var/log/polipo} otherwise.  The variable
+@code{logSyslog} defaults to @code{false}, and @code{logFacility}
+defaults to @samp{user}.
+
+If @code{logFile} is set, then the variable @code{logFilePermissions}
+controls the Unix permissions with which the log file will be created if
+it doesn't exist.  It defaults to 0640.
+
+The amount of logging is controlled by the variable @code{logLevel}.
+Please see the file @samp{log.h} in the Polipo sources for the
+possible values of @code{logLevel}.
+
+Keeping extensive logs on your users browsing habits is probably
+a serere violation of their privacy.  If the variable @code{scrubLogs}
+is set, then Polipo will scrub most, if not all, private information
+from its logs.
+
+@node Browser configuration, Stopping, Polipo Invocation, Running
+@section Configuring your browser
+@cindex browser configuration
+@cindex user-agent configuration
+
+Telling your user-agent (web browser) to use Polipo is an operation
+that depends on the browser.  Many user-agents will transparently use
+Polipo if the environment variable @samp{http_proxy} points at it;
+e.g.@:
+@example
+$ export http_proxy=http://localhost:8123/
+@end example
+Netscape Navigator, Mozilla, Mozilla Firefox, KDE's Konqueror and
+probably other browsers require that you configure them manually
+through their @emph{Preferences} or @emph{Configure} menu.
+
+If your user-agent sports such options, tell it to use persistent
+connections when speaking to proxies, to speak HTTP/1.1 and to use
+HTTP/1.1 pipelining.
+
+@node Stopping, Local server, Browser configuration, Running
+@section Stopping Polipo and getting it to reload
+@cindex signals
+@cindex shutting down
+@cindex stopping
+
+Polipo will shut down cleanly if it receives @code{SIGHUP},
+@code{SIGTERM} or @code{SIGINT} signals; this will normally happen
+when a Polipo in the foreground receives a @code{^C} key press, when
+your system shuts down, or when you use the @code{kill} command with
+no flags.  Polipo will then write-out all its in-memory data to disk
+and quit.
+
+If Polipo receives the @code{SIGUSR1} signal, it will write out all
+the in-memory data to disk (but won't discard them), reopen the log
+file, and then reload the forbidden URLs file (@pxref{Forbidden}).
+
+Finally, if Polipo receives the @code{SIGUSR2} signal, it will write
+out all the in-memory data to disk and discard as much of the memory
+cache as possible.  It will then reopen the log file and reload the
+forbidden URLs file.
+
+@node Local server,  , Stopping, Running
+@section The local web server
+@vindex localDocumentRoot
+@vindex disableProxy
+@cindex web server
+@cindex local server
+
+Polipo includes a local web server, which is accessible on the same
+port as the one the proxy listens to.  Therefore, by default you can
+access Polipo's local web server as @samp{http://localhost:8123/}.
+
+The data for the local web server can be configured by setting
+@code{localDocumentRoot}, which defaults to
+@file{/usr/share/polipo/www/}.  Setting this variable to @samp{""}
+will disable the local server.
+
+Polipo assumes that the local web tree doesn't change behind its back.
+If you change any of the local files, you will need to notify Polipo
+by sending it a @code{SIGUSR2} signal (@pxref{Stopping}).
+
+If you use polipo as a publicly accessible web server, you might want
+to set the variable @code{disableProxy}, which will prevent it from
+acting as a web proxy.  (You will also want to set
+@code{disableLocalInterface} (@pxref{Web interface}), and perhaps run
+Polipo in a @emph{chroot} jail.)
+
+@menu
+* Web interface::               The web interface.
+@end menu
+
+@node Web interface,  , Local server, Local server
+@subsection The web interface
+@cindex runtime configuration
+@cindex web interface
+@vindex disableLocalInterface
+@vindex disableConfiguration
+@vindex disableServersList
+
+The subtree of the local web space rooted at
+@samp{http://localhost:8123/polipo/} is treated specially: URLs under
+this root do not correspond to on-disk files, but are generated by
+Polipo on-the-fly.  We call this subtree Polipo's @dfn{local web
+interface}.
+
+The page @samp{http://localhost:8123/polipo/config?} contains the
+values of all configuration variables, and allows setting most of them.
+
+The page @samp{http://localhost:8123/polipo/status?} provides a summary
+status report about the running Polipo, and allows performing a number
+of actions on the proxy, notably flushing the in-memory cache.
+
+The page @samp{http://localhost:8123/polipo/servers?} contains the list
+of known servers, and the statistics maintained about them
+(@pxref{Server statistics}).
+
+The pages starting with @samp{http://localhost:8123/polipo/index?}
+contain indices of the disk cache.  For example, the following page
+contains the index of the cached pages from the server of some random
+company:
+@example
+http://localhost:8123/polipo/index?http://www.microsoft.com/
+@end example
+The pages starting with
+@samp{http://localhost:8123/polipo/recursive-index?} contain recursive
+indices of various servers.  This functionality is disabled by
+default, and can be enabled by setting the variable
+@code{disableIndexing}.
+
+If you have multiple users, you will probably want to disable the
+local interface by setting the variable @code{disableLocalInterface}.
+You may also selectively control setting of variables, indexing and
+listing known servers by setting the variables
+@code{disableConfiguration}, @code{disableIndexing} and
+@code{disableServersList}.
+
+@node Network, Caching, Running, Top
+@chapter Polipo and the network
+
+@menu
+* Client connections::          Speaking to clients
+* Contacting servers::          Contacting servers.
+* HTTP tuning::                 Tuning at the HTTP level.
+* Offline browsing::            Browsing with poor connectivity.
+* Server statistics::           Polipo keeps statistics about servers.
+* Server-side behaviour::       Tuning the server-side behaviour.
+* PMM::                         Poor Man's Multiplexing.
+* Forbidden::                   You can forbid some URLs.
+* DNS::                         How Polipo finds hosts.
+* Parent proxies::              Fetching data from other proxies.
+* Tuning POST and PUT::         Tuning POST and PUT requests.
+* Tunnelling connections::      Tunnelling foreign protocols and https.
+@end menu
+
+@node Client connections, Contacting servers, Network, Network
+@section Client connections
+
+@vindex proxyAddress
+@vindex proxyPort
+@vindex proxyName
+@vindex displayName
+@cindex address
+@cindex port
+@cindex IPv6
+@cindex proxy loop
+@cindex loop
+@cindex proxy name
+@cindex via
+@cindex loopback address
+@cindex security
+
+There are three fundamental values that control how Polipo speaks to
+clients.  The variable @code{proxyAddress}, defines the IP address on
+which Polipo will listen; by default, its value is the @dfn{loopback
+address} @code{"127.0.0.1"}, meaning that Polipo will listen on the
+IPv4 loopback interface (the local host) only.  By setting this
+variable to a global IP address or to one of the special values
+@code{"::"} or @code{"0.0.0.0"}, it is possible to allow Polipo to
+serve remote clients.  This is likely to be a security hole unless you
+set @code{allowedClients} to a reasonable value (@pxref{Access control}).
+
+Note that the type of address that you specify for @code{proxyAddress}
+will determine whether Polipo listens to IPv4 or IPv6.  Currently, the
+only way to have Polipo listen to both protocols is to specify the
+IPv6 unspecified address (@code{"::"}) for @code{proxyAddress}.
+
+The variable @code{proxyPort}, by default 8123, defines the TCP port
+on which Polipo will listen.
+
+The variable @code{proxyName}, which defaults to the host name of the
+machine on which Polipo is running, defines the @dfn{name} of the
+proxy.  This can be an arbitrary string that should be unique among
+all instances of Polipo that you are running.  Polipo uses it in error
+messages and optionally for detecting proxy loops (by using the
+@samp{Via} HTTP header, @pxref{Censoring headers}). Finally, the
+@code{displayName} variable specifies the name used in user-visible
+error messages (default ``Polipo'').
+
+@menu
+* Access control::              Deciding who can connect.
+@end menu
+
+@node Access control,  , Client connections, Client connections
+@subsection Access control
+@vindex proxyAddress
+@vindex authCredentials
+@vindex authRealm
+@vindex allowedClients
+@cindex access control
+@cindex authentication
+@cindex loopback address
+@cindex security
+@cindex username
+@cindex password
+
+By making it possible to have Polipo listen on a non-routable address
+(for example the loopback address @samp{127.0.0.1}), the variable
+@code{proxyAddress} provides a very crude form of @dfn{access
+control}: the ability to decide which hosts are allowed to connect.
+
+A finer form of access control can be implemented by specifying
+explicitly a number of client addresses or ranges of addresses
+(networks) that a client is allowed to connect from.  This is done
+by setting the variable @code{allowedClients}.
+
+Every entry in @code{allowedClients} can be an IP address, for example
+@samp{134.157.168.57} or @samp{::1}.  It can also be a network
+address, i.e.@: an IP address and the number of bits in the network
+prefix, for example @samp{134.157.168.0/24} or
+@samp{2001:660:116::/48}.  Typical uses of @samp{allowedClients}
+variable include
+@example
+allowedClients = 127.0.0.1, ::1, 134.157.168.0/24, 2001:660:116::/48
+@end example
+or, for an IPv4-only version of Polipo,
+@example
+allowedClients = 127.0.0.1, 134.157.168.0/24
+@end example
+
+A different form of access control can be implemented by requiring
+each client to @dfn{authenticate}, i.e.@: to prove its identity before
+connecting.  Polipo currently only implements the most insecure form
+of authentication, @dfn{HTTP basic authentication}, which sends
+usernames and passwords in clear over the network.  HTTP basic
+authentication is required when the variable @code{authCredentials} is
+not null; its value should be of the form @samp{username:password}.
+
+Note that both IP-based authentication and HTTP basic authentication
+are insecure: the former is vulnerable to IP address spoofing, the
+latter to replay attacks.  If you need to access Polipo over the
+public Internet, the only secure option is to have it listen over the
+loopback interface only and use an ssh tunnel (@pxref{Parent
+proxies})@footnote{It is not quite clear to me whether HTTP digest
+authentication is worth implementing.  On the one hand, if implemented
+correctly, it appears to provide secure authentication; on the other
+hand, and unlike ssh or SSL, it doesn't make any attempt at ensuring
+privacy, and its optional integrity guarantees are impossible to
+implement without significantly impairing latency.}.
+
+@node Contacting servers, HTTP tuning, Client connections, Network
+@section Contacting servers
+
+@cindex multiple addresses
+@cindex IPv6
+@vindex useTemporarySourceAddress
+@vindex proxyOutgoingAddress
+
+A server can have multiple addresses, for example if it is
+@dfn{multihomed} (connected to multiple networks) or if it can speak
+both IPv4 and IPv6.  Polipo will try all of a hosts addresses in turn;
+once it has found one that works, it will stick to that address until
+it fails again.
+
+If your host has multiple IP addresses, you can specify an IP address
+to use for outgoing connections with the @code{proxyOutgoingAddress}
+variable.  If not specified (the default), it will be determined by
+the host OS.
+
+If connecting via IPv6 there is the possibility to use temporary
+source addresses to increase privacy (RFC@tie{}3041). The variable
+@code{useTemporarySourceAddress} controls the use of temporary
+addresses for outgoing connections; if set to @code{true}
+temporary addresses are preferred, if set to @code{false} static addresses
+are used and if set to @code{maybe} (the default) the operation
+system default is in effect. This setting is not available
+on all operation systems.
+
+@menu
+* Allowed ports::               Where the proxy is allowed to connect.
+@end menu
+
+@node Allowed ports,  , Contacting servers, Contacting servers
+@subsection Allowed ports
+
+@cindex Allowed ports
+@cindex Forbidden ports
+@cindex ports
+@vindex allowedPorts
+
+A TCP service is identified not only by the IP address of the machine
+it is running on, but also by a small integer, the TCP @dfn{port} it
+is @dfn{listening} on.  Normally, web servers listen on port 80, but
+it is not uncommon to have them listen on different ports; Polipo's
+internal web server, for example, listens on port 8123 by default.
+
+The variable @code{allowedPorts} contains the list of ports that
+Polipo will accept to connect to on behalf of clients; it defaults to
+@samp{80-100, 1024-65535}.  Set this variable to @samp{1-65535} if your
+clients (and the web pages they consult!) are fully trusted.  (The
+variable @code{allowedPorts} is not considered for tunnelled
+connections; @pxref{Tunnelling connections}).
+
+@node HTTP tuning, Offline browsing, Contacting servers, Network
+@section Tuning at the HTTP level
+@cindex HTTP
+@cindex headers
+
+@menu
+* Tuning the HTTP parser::      Tuning parsing of HTTP headers.
+* Censoring headers::           Censoring HTTP headers.
+* Intermediate proxies::        Adjusting intermediate proxy behaviour.
+@end menu
+
+@node Tuning the HTTP parser, Censoring headers, HTTP tuning, HTTP tuning
+@subsection Tuning the HTTP parser
+@vindex laxHttpParser
+@vindex bigBufferSize
+
+As a number of HTTP servers and CGI scripts serve incorrect HTTP
+headers, Polipo uses a @emph{lax} parser, meaning that incorrect HTTP
+headers will be ignored (a warning will be logged by default).  If the
+variable @code{laxHttpParser} is not set (it is set by default),
+Polipo will use a @emph{strict} parser, and refuse to serve an
+instance unless it could parse all the headers.
+
+When the amount of headers exceeds one chunk's worth (@pxref{Chunk
+memory}), Polipo will allocate a @dfn{big buffer} in order to store
+the headers.  The size of big buffers, and therefore the maximum
+amount of headers Polipo can parse, is specified by the variable
+@code{bigBufferSize} (32@dmn{kB} by default).
+
+@node Censoring headers, Intermediate proxies, Tuning the HTTP parser, HTTP tuning
+@subsection Censoring headers
+@cindex privacy
+@cindex anonymity
+@cindex Referer
+@cindex cookies
+@vindex censorReferer
+@vindex censoredHeaders
+@vindex proxyName
+@vindex disableVia
+
+Polipo offers the option to censor given HTTP headers in both client
+requests and server replies.  The main application of this feature is
+to very slightly improve the user's privacy by eliminating cookies and
+some content-negotiation headers.
+
+It is important to understand that these features merely make it
+slightly more difficult to gather statistics about the user's
+behaviour.  While they do not actually prevent such statistics from
+being collected, they might make it less cost-effective to do so.
+
+The general mechanism is controlled by the variable
+@code{censoredHeaders}, the value of which is a case-insensitive list
+of headers to unconditionally censor.  By default, it is empty, but
+I recommend that you set it to @samp{From, Accept-Language}.  Adding
+headers such as @samp{Set-Cookie}, @samp{Set-Cookie2}, @samp{Cookie},
+@samp{Cookie2} or @samp{User-Agent} to this list will probably break
+many web sites.
+
+The case of the @samp{Referer}@footnote{HTTP contains many mistakes
+and even one spelling error.} header is treated specially because many
+sites will refuse to serve pages when it is not provided.  If
+@code{censorReferer} is @code{false} (the default), @samp{Referer}
+headers are passed unchanged to the server.  If @code{censorReferer}
+is @code{maybe}, @samp{Referer} headers are passed to the server only
+when they refer to the same host as the resource being fetched.  If
+@code{censorReferer} is @code{true}, all @samp{Referer} headers are
+censored.  I recommend setting @code{censorReferer} to @code{maybe}.
+
+Another header that can have privacy implications is the @samp{Via}
+header, which is used to specify the chain of proxies through which
+a given request has passed.  Polipo will generate @samp{Via} headers
+if the variable @code{disableVia} is @code{false} (it is true by
+default).  If you choose to generate @samp{Via} headers, you may want
+to set the @code{proxyName} variable to some innocuous string
+(@pxref{Client connections}).
+
+@menu
+* Censor Accept-Language::      Why Accept-Language is evil.
+@end menu
+
+@node Censor Accept-Language,  , Censoring headers, Censoring headers
+@subsubsection Why censor Accept-Language
+@cindex negotiation
+@cindex content negotiation
+@cindex Accept-Language
+
+Recent versions of HTTP include a mechanism known as @dfn{content
+negotiation} which allows a user-agent and a server to negotiate the
+best representation (instance) for a given resource.  For example, a
+server that provides both PNG and GIF versions of an image will serve
+the PNG version to user-agents that support PNG, and the GIF version
+to Internet Explorer.
+
+Content negotiation requires that a client should send with every
+single request a number of headers specifying the user's cultural and
+technical preferences.  Most of these headers do not expose sensitive
+information (who cares whether your browser supports PNG?).  The
+@samp{Accept-Language} header, however, is meant to convey the user's
+linguistic preferences.  In some cases, this information is sufficient
+to pinpoint with great precision the user's origins and even his
+political or religious opinions; think, for example, of the
+implications of sending @samp{Accept-Language: yi} or @samp{ar_PS}.
+
+At any rate, @samp{Accept-Language} is not useful.  Its design is
+based on the assumption that language is merely another representation
+for the same information, and @samp{Accept-Language} simply carries a
+prioritised list of languages, which is not enough to usefully
+describe a literate user's preferences.  A typical French user, for
+example, will prefer an English-language original to a French
+(mis-)translation, while still wanting to see French language texts
+when they are original.  Such a situation cannot be described by the
+simple-minded @samp{Accept-Language} header.
+
+@node Intermediate proxies,  , Censoring headers, HTTP tuning
+@subsection Adjusting intermediate proxy behaviour
+@vindex alwaysAddNoTransform
+@cindex intermediate proxies
+
+Implementors of intermediate caches (proxies) have found it useful to
+convert the media type of certain entity bodies. A non-transparent
+proxy might, for example, convert between image formats in order to
+save cache space or to reduce the amount of traffic on a slow link.
+
+If @code{alwaysAddNoTransform} is true (it is false by default),
+Polipo will add a 'no-transform' cache control directive to all
+outgoing requests. This directive forbids (compliant) intermediate
+caches from responding with an object that was compressed or
+transformed in any way.
+
+@node Offline browsing, Server statistics, HTTP tuning, Network
+@section Offline browsing
+@vindex proxyOffline
+@cindex offline browsing
+@cindex browsing offline
+@cindex connectivity
+@cindex warning
+@cindex shift-click
+
+In an ideal world, all machines would have perfect connectivity to the
+network at all times and servers would never crash.  In the real
+world, it may be necessary to avoid hitting the network and have
+Polipo serve stale objects from its cache.
+
+Setting @code{proxyOffline} to @code{true} prevents Polipo from
+contacting remote servers, no matter what.  This setting is suitable
+when you have no network connection whatsoever.
+
+If @code{proxyOffline} is false, Polipo's caching behaviour is
+controlled by a number of variables documented in @ref{Tweaking validation}.
+
+@node Server statistics, Server-side behaviour, Offline browsing, Network
+@section Server statistics
+@vindex serverExpireTime
+@cindex server statistics
+@cindex round-trip time
+@cindex transfer rate
+
+In order to decide when to pipeline requests (@pxref{Pipelining}) and
+whether to perform Poor Man's Multiplexing 
+(@pxref{Poor Mans Multiplexing}), Polipo needs to keep statistics
+about servers.  These include the server's ability to handle
+persistent connections, the server's ability to handle pipelined
+requests, the round-trip time to the server, and the server's transfer
+rate.  The statistics are accessible from Polipo's web interface
+(@pxref{Web interface}). 
+
+The variable @samp{serverExpireTime} (default 1 day) specifies how
+long such information remains valid.  If a server has not been
+accessed for a time interval of at least @code{serverExpireTime},
+information about it will be discarded.
+
+As Polipo will eventually recover from incorrect information about a
+server, this value can be made fairly large.  The reason why it exists
+at all is to limit the amount of memory used up by information about
+servers.
+
+@node Server-side behaviour, PMM, Server statistics, Network
+@section Tweaking server-side behaviour
+@vindex serverSlots
+@vindex serverSlots1
+@vindex serverMaxSlots
+@vindex smallRequestTime
+@vindex replyUnpipelineTime
+@vindex replyUnpipelineSize
+@vindex maxPipelineTrain
+@vindex pipelineAdditionalRequests
+@vindex maxSideBuffering
+@cindex small request
+@cindex large request
+@cindex breaking pipelines
+
+The most important piece of information about a server is whether it
+supports persistent connections.  If this is the case, Polipo will
+open at most @code{serverSlots} connections to that server
+(@code{serverSlots1} if the server only implements HTTP/1.0), and
+attempt to pipeline; if not, Polipo will hit the server harder,
+opening up to @code{serverMaxSlots} connections.
+
+Another use of server information is to decide whether to pipeline
+additional requests on a connection that already has in-flight
+requests.  This is controlled by the variable
+@code{pipelineAdditionalRequests}; if it is @code{false}, no
+additional requests will be pipelined.  If it is @code{true},
+additional requests will be pipelined whenever possible.  If it is
+@code{maybe} (the default), additional requests will only be pipelined
+following @dfn{small} requests, where a small request one whose
+download is estimated to take no more than @code{smallRequestTime}
+(default 5@dmn{s}).
+
+Sometimes, a request has been pipelined after a request that prompts a
+very large reply from the server; when that happens, the pipeline
+needs be broken in order to reduce latency.  A reply is @dfn{large}
+and will cause a pipeline to be broken if either its size is at least
+@code{replyUnpipelineSize} (default one megabyte) or else the server's
+transfer rate is known and the body is expected to take at least
+@code{replyUnpipelineTime} to download (default 15@dmn{s}).
+
+The variable @code{maxPipelineTrain} defines the maximum number of
+requests that will be pipelined in a single write (default 10).
+Setting this variable to a very low value might (or might not) fix
+interaction with some unreliable servers that the normal heuristics
+are unable to detect.
+
+The variable @code{maxSideBuffering} specifies how much data will be
+buffered in a PUT or POST request; it defaults to 1500 bytes.  Setting
+this variable to 0 may cause some media players that abuse the HTTP
+protocol to work.
+
+@node PMM, Forbidden, Server-side behaviour, Network
+@section Poor Man's Multiplexing
+@cindex Poor Man's Multiplexing
+@cindex multiplexing
+@vindex pmmSize
+@vindex pmmFirstSize
+
+By default, Polipo does not use Poor Man's Multiplexing (@pxref{Poor
+Mans Multiplexing}).  If the variable @code{pmmSize} is set to a
+positive value, Polipo will use PMM when speaking to servers that are
+known to support pipelining.  It will request resources by segments of
+@code{pmmSize} bytes.  The first segment requested has a size of
+@code{pmmFirstSize}, which defaults to twice @code{pmmSize}.
+
+PMM is an intrinsically unreliable technique.  Polipo makes heroic
+efforts to make it at least usable, requesting that the server disable
+PMM when not useful (by using the @samp{If-Range} header) and
+disabling it on its own if a resource turns out to be dynamic.
+Notwithstanding these precautions, unless the server
+cooperates@footnote{More precisely, unless CGI scripts cooperate.},
+you will see failures when using PMM, which will usually result in
+blank pages and broken image icons; hitting @emph{Reload} on your
+browser will usually cause Polipo to notice that something went wrong
+and correct the problem.
+
+@node Forbidden, DNS, PMM, Network
+@section Forbidden and redirected URLs
+@cindex forbidden
+@cindex redirect
+@cindex web counter
+@cindex counter
+@cindex web bug
+@cindex bug
+@cindex advertisement
+@cindex web ad
+@cindex banner ad
+
+The web contains advertisements that a user-agent is supposed to
+download together with the requested pages.  Not only do
+advertisements pollute the user's brain, pushing them around takes
+time and uses up network bandwidth.
+
+Many so-called content providers also track user activities by using
+@dfn{web bugs}, tiny embedded images that cause a server to log where
+they are requested from.  Such images can be detected because they are
+usually uncacheable (@pxref{Cache transparency}) and therefore logged
+by Polipo by default.
+
+Polipo can be configured to prevent certain URLs from reaching the
+browser, either by returning a @emph{forbidden} error message to the
+user, or by @emph{redirecting} such URLs to some other URL.
+
+Some content providers attempt to subvert content filtering as well as 
+malware scans by tunnelling their questionable content as https or other 
+encrypted protocols. Other content providers are so clueless as to inject 
+content from external providers into supposedly safe webpages.
+Polipo has therefore the ability to selectively block tunneled connections 
+based on hostname and port information. 
+
+@menu
+* Internal forbidden list::     Specifying forbidden URLs.
+* External redirectors::        Using an external redirector.
+* Forbidden Tunnels::           Specifying hosts forbidden for tunnelling.
+@end menu
+
+@node Internal forbidden list, External redirectors, Forbidden, Forbidden
+@subsection Internal forbidden list
+@cindex forbidden
+@cindex redirect
+@vindex forbiddenFile
+@vindex forbiddenUrl
+@vindex forbiddenRedirectCode
+
+The file pointed at by the variable @code{forbiddenFile} (defaults to
+@file{~/.polipo-forbidden} or @file{/etc/polipo/forbidden}, whichever
+exists) specifies the set of URLs that should never be fetched.  If
+@code{forbiddenFile} is a directory, it will be recursively searched
+for files with forbidden URLs.
+
+Every line in a file listing forbidden URLs can either be a domain
+name --- a string that doesn't contain any of @samp{/}, @samp{*} or
+@samp{\} ---, or a POSIX extended regular expression.  Blank lines are
+ignored, as are those that start with a hash sign @samp{#}.
+
+By default, whenever it attempts to fetch a forbidden URL, the browser
+will receive a @emph{403 forbidden} error from Polipo.  Some users
+prefer to have the browser display a different page or an image.
+
+If @code{forbiddenUrl} is not null, it should represent a URL to which
+all forbidden URLs will be redirected.  The kind of redirection used
+is specified by @code{forbiddenRedirectCode}; if this is 302 (the
+default) the redirection will be marked as temporary, if 301 it will
+be a permanent one.
+
+@node External redirectors, Forbidden Tunnels, Internal forbidden list, Forbidden
+@subsection External redirectors
+@cindex forbidden
+@cindex redirect
+@cindex redirector
+@cindex Squid-style redirector
+@cindex Adzapper
+@vindex redirector
+@vindex redirectorRedirectCode
+
+Polipo can also use an external process (a @dfn{Squid-style
+redirector}) to determine which URLs should be redirected.  The name
+of the redirector binary is determined from the variable
+@code{redirector}, and the kind of redirection generated is specified
+by @code{redirectorRedirectCode}, which should be 302 (the default) or
+301.
+
+For example, to use Adzapper to redirect ads to an innocuous image, just set
+@example
+redirector = /usr/bin/adzapper
+@end example
+
+@node Forbidden Tunnels,  , External redirectors, Forbidden
+@subsection Forbidden Tunnels
+
+Polipo does by default allow tunnelled connections 
+(@pxref{Tunnelling connections}), however sometimes it is desirable to 
+block connections selectively. 
+
+Because polipo does only pass through tunnelled connections filtering is 
+possible based on hostname and port information only. Filtering based on 
+protocol specific types of information like pathname is not possible.
+
+Obviously the web browser (and other software) must be configured to use 
+polipo as tunneling proxy for this to work. The tunnelled traffic is neither
+touched nor inspected in any way by polipo, thus encryption, certification 
+and all other security and integrity guarantees implemented in the browser
+are not in any way affected.
+
+The file pointed at by the variable @code{forbiddenTunnelsFile} (defaults to
+@file{~/.polipo-forbiddenTunnels} or @file{/etc/polipo/forbiddenTunnels}, 
+whichever exists) specifies the set of tunnel specifications that should
+be blocked.
+
+Every line in a file listing forbidden Tunnels can either be a domain
+name --- a string that doesn't contain any of @samp{/}, @samp{*} or
+@samp{\} ---, or a POSIX extended regular expression.  Blank lines are
+ignored, as are those that start with a hash sign @samp{#}.
+
+Entries in the form of regular expressions will be matched against
+tunnel reqeusts of the form @code{hostname:portnumber}.
+
+Tunnelled and blocked connections will be logged if the configuration variable
+@code{logLevel} is set to a value such that @code{((logLevel & 0x80) !=0)} 
+
+Example @code{forbiddenTunnelsFile} :
+@example
+# simple case, exact match of hostnames
+www.massfuel.com
+
+# match hostname against regexp
+\.hitbox\.
+
+# match hostname and port against regexp
+# this will block tunnels to example.com but also  www.example.com
+# for ports in the range 600-999
+# Also watch for effects of 'tunnelAllowedPorts'
+example.com\:[6-9][0-9][0-9]
+
+# random examples
+\.liveperson\.
+\.atdmt\.com
+.*doubleclick\.net
+.*webtrekk\.de
+^count\..*
+.*\.offerstrategy\.com
+.*\.ivwbox\.de
+.*adwords.*
+.*\.sitestat\.com
+\.xiti\.com
+webtrekk\..*
+@end example
+
+@node DNS, Parent proxies, Forbidden, Network
+@section The domain name service
+@cindex DNS
+@cindex name server
+@cindex gethostbyname
+@cindex resolver
+@cindex IPv6
+@vindex dnsMaxTimeout
+@vindex dnsUseGethostbyname
+@vindex dnsNameServer
+@vindex dnsNameServerPort
+@vindex dnsNegativeTtl
+@vindex dnsGethostbynameTtl
+@vindex dnsQueryIPv6
+
+The low-level protocols beneath HTTP identify machines by IP
+addresses, sequences of four 8-bit integers such as
+@samp{199.232.41.10}@footnote{Or sequences of eight 16-bit integers if
+you are running IPv6.}.  HTTP, on the other hand, and most application 
+protocols, manipulate host names, strings such as @samp{www.polipo.org}.
+
+The @dfn{domain name service} (DNS) is a distributed database that
+maps host names to IP addresses.  When an application wants to make
+use of the DNS, it invokes a @dfn{resolver}, a local library or
+process that contacts remote name servers.
+
+Polipo usually tries to speak the DNS protocol itself rather than
+using the system resolver@footnote{The Unix interface to the resolver
+is provided by the @code{gethostbyname}(3) library call
+(@code{getaddrinfo}(3) on recent systems), which was designed at
+a time when a host lookup consisted in searching for one of five hosts
+in a @samp{HOSTS.TXT} file.  The @code{gethostbyname} call is
+@dfn{blocking}, meaning that all activity must cease while a host
+lookup is in progress.  When the call eventually returns, it doesn't
+provide a @dfn{time to live} (TTL) value to indicate how long the
+address may be cached.  For these reasons, @code{gethostbyname} is
+hardly useful for programs that need to contact more than a few hosts.
+(Recent systems replace @code{gethostbyname}(3) by
+@code{getaddrinfo}(3), which is reentrant.  While this removes one
+important problem that multi-threaded programs encounter, it doesn't
+solve any of the other issues with @code{gethostbyname}.)}.  Its
+precise behaviour is controlled by the value of
+@code{dnsUseGethostbyname}.  If @code{dnsUseGethostbyname} is
+@code{false}, Polipo never uses the system resolver.  If it is
+@code{reluctantly} (the default), Polipo tries to speak DNS and falls
+back to the system resolver if a name server could not be contacted.
+If it is @code{happily}, Polipo tries to speak DNS, and falls back to
+the system resolver if the host couldn't be found for any reason (this
+is not a good idea for shared proxies).  Finally, if
+@code{dnsUseGethostbyname} is @code{true}, Polipo never tries to speak
+DNS itself and uses the system resolver straight away (this is not
+recommended).
+
+If the internal DNS support is used, Polipo must be given a recursive
+name server to speak to.  By default, this information is taken from
+the @samp{/etc/resolv.conf} file at startup; however, if you wish to use
+a different name server, you may set the @code{dnsNameServer} and
+optionally @code{dnsNameServerPort} variables to an IP address and port
+number of a listening DNS server@footnote{While Polipo does its own
+caching of DNS data, I recommend that you run a local caching name server.
+I am very happy with @uref{http://www.thekelleys.org.uk/dnsmasq/doc.html,,@code{dnsmasq}}.}.
+
+When the reply to a DNS request is late to come, Polipo will retry
+multiple times using an exponentially increasing timeout.  The maximum
+timeout used before Polipo gives up is defined by @code{dnsMaxTimeout}
+(default 60@dmn{s}); the total time before Polipo gives up on a DNS
+query will be roughly twice @code{dnsMaxTimeout}.
+
+The variable @code{dnsNegativeTtl} specifies the time during which
+negative DNS information (information that a host @emph{doesn't}
+exist) will be cached; this defaults to 120@dmn{s}.  Increasing this
+value reduces both latency and network traffic but may cause a failed
+host not to be noticed when it comes back up.
+
+The variable @code{dnsQueryIPv6} specifies whether to query for IPv4
+or IPv6 addresses.  If @code{dnsQueryIPv6} is @code{false}, only IPv4
+addresses are queried.  If @code{dnsQueryIPv6} is @code{reluctantly},
+both types of addresses are queried, but IPv4 addresses are preferred.
+If @code{dnsQueryIPv6} is @code{happily} (the default), IPv6 addresses
+are preferred.  Finally, if @code{dnsQueryIPv6} is @code{true}, only
+IPv6 addresses are queried.
+
+If the system resolver is used, the value @code{dnsGethostbynameTtl}
+specifies the time during which a @code{gethostbyname} reply will be
+cached (default 5 minutes).
+
+@node Parent proxies, Tuning POST and PUT, DNS, Network
+@section Parent proxies
+
+Polipo will usually fetch instances directly from source servers as
+this configuration minimises latency.  In some cases, however, it may
+be useful to have Polipo fetch instances from a @dfn{parent} proxy.
+
+Polipo can use two protocols to speak to a parent proxy: HTTP and
+SOCKS.  When configured to use both HTTP and SOCKS proxying, Polipo
+will contact an HTTP proxy over SOCKS --- in other words, SOCKS is
+considered as being at a lower (sub)layer than HTTP.
+
+@menu
+* HTTP parent proxies::         Using an HTTP parent proxy.
+* SOCKS parent proxies::        Using a SOCKS4a parent proxy.
+@end menu
+
+@node HTTP parent proxies, SOCKS parent proxies, Parent proxies, Parent proxies
+@subsection HTTP parent proxies
+@vindex parentProxy
+@vindex parentAuthCredentials
+@cindex parent proxy
+@cindex upstream proxy
+@cindex firewall
+@cindex authentication
+
+The variable @code{parentProxy} specifies the hostname and port number
+of an HTTP parent proxy; it should have the form @samp{host:port}.
+
+If the parent proxy requires authorisation, the username and password
+should be specified in the variable @code{parentAuthCredentials} in
+the form @samp{username:password}.  Only @emph{Basic} authentication
+is supported, which is vulnerable to replay attacks.
+
+The main application of the parent proxy support is to cross
+firewalls.  Given a machine, say @code{trurl}, with unrestricted
+access to the web, the following evades a firewall by using an
+encrypted compressed @code{ssh} link:
+@example
+$ ssh -f -C -L 8124:localhost:8123 trurl polipo
+$ polipo parentProxy=localhost:8124
+@end example
+
+@node SOCKS parent proxies,  , HTTP parent proxies, Parent proxies
+@subsection SOCKS parent proxies
+@cindex SOCKS
+@vindex socksParentProxy
+@vindex socksAuthCredentials
+@vindex socksProxyType
+
+The variable @code{socksParentProxy} specifies the hostname and port
+number of a SOCKS parent proxy; it should have the form
+@samp{host:port}.  The variant of the SOCKS protocol being used is
+defined by @code{socksProxyType}, which can be either @samp{socks4a}
+or @samp{socks5}; the latter value specifies ``SOCKS5 with
+hostnames'', and is the default.
+
+The variable @code{socksAuthCredentials} can be used if your SOCKS
+proxy requires authentication.  For SOCKS4 and 4a, it is just
+a username; for SOCKS5 it is of the form @samp{username:password}.
+
+The main application of the SOCKS support is to use
+@uref{http://tor.eff.org,,Tor} to evade overly restrictive or
+misconfigured firewalls.  Assuming you have a Tor client running on
+the local host listening on the default port (9050), the following
+uses Tor for all outgoing HTTP traffic:
+@example
+$ polipo socksParentProxy=localhost:9050
+@end example
+
+@node Tuning POST and PUT, Tunnelling connections, Parent proxies, Network
+@section Tuning POST and PUT requests
+@cindex POST request
+@cindex PUT request
+@vindex expectContinue
+
+The main assumption behind the design of the HTTP protocol is that
+requests are idempotent: since a request can be repeated by a client,
+a server is allowed to drop a connection at any time.  This fact, more
+than anything else, explains the amazing scalability of the protocol.
+
+This assumption breaks down in the case of POST requests.  Indeed, a
+POST request usually causes some action to be performed (a page to be
+printed, a significant amount of money to be transferred from your
+bank account, or, in Florida, a vote to be registered), and such a
+request should not be repeated.
+
+The only solution to this problem is to reserve HTTP to idempotent
+activities, and use reliable protocols for action-effecting ones.
+Notwithstanding that, HTTP/1.1 makes a weak attempt at making POST
+requests slightly more reliable and efficient than they are in
+HTTP/1.0.
+
+When speaking to an HTTP/1.1 server, an HTTP client is allowed to
+request that the server check @emph{a priori} whether it intends to
+honour a POST request.  This is done by sending @dfn{an expectation},
+a specific header with the request, @samp{Expect: 100-continue}, and
+waiting for either an error message or a @samp{100 Continue} reply
+from the server.  If the latter arrives, the client is welcome to send
+the rest of the POST request@footnote{This, of course, is only part of
+the story.  Additionally, the server is not required to reply with
+@samp{100 Continue}, hence the client must implement a timeout.
+Furthermore, according to the obsolete RFC2068, the server is
+allowed to spontaneously send @samp{100 Continue}, so the client must
+be prepared to ignore such a reply at any time.}.
+
+Polipo's behaviour w.r.t.@: client expectations is controlled by the
+variable @code{expectContinue}.  If this variable is false, Polipo
+will never send an expectation to the server; if a client sends an
+expectation, Polipo will fail the expectation straight away, causing
+the client (if correctly implemented) to retry with no expectation.
+If @code{expectContinue} is @code{maybe} (the default), Polipo will
+behave in a standards-compliant manner: it will forward expectations
+to the server when allowed to do so, and fail client expectations
+otherwise.  Finally, if @code{expectContinue} is @code{true}, Polipo
+will always send expectations when it is reasonable to do so; this
+violates the relevant standards and will break some websites, but
+might decrease network traffic under some circumstances.
+
+@node Tunnelling connections,  , Tuning POST and PUT, Network
+@section Tunnelling connections
+@cindex tunnel
+@cindex tunnelling proxy
+@cindex https
+@cindex HTTP/SSL
+@cindex rsync
+@cindex CONNECT
+@vindex tunnelAllowedPorts
+
+Polipo is an HTTP proxy; it proxies HTTP traffic, and clients using
+other protocols should either establish a direct connection to the
+server or use an @emph{ad hoc} proxy.
+
+In many circumstances, however, it is not possible to establish
+a direct connection to the server, for example due to mis-configured
+firewalls or when trying to access the IPv4 Internet from an IPv6-only
+host.  In such situations, it is possible to have Polipo behave as
+a @emph{tunnelling} proxy --- a proxy that merely forwards traffic
+between the client and the server without understanding it.  Polipo
+enters tunnel mode when the client requests it by using the HTTP
+@samp{CONNECT} method.
+
+Most web browsers will use this technique for HTTP over SSL if
+configured to use Polipo as their `https proxy'.  More generally, the
+author has successfully used it to cross mis-configured firewalls
+using OpenSSH, rsync, Jabber, IRC, etc.
+
+The variable @code{tunnelAllowedPorts} specifies the set of ports that
+Polipo will accept to tunnel traffic to.  It defaults to allowing ssh,
+HTTP, https, rsync, IMAP, imaps, POP, pops, Jabber, CVS and Git traffic.
+
+It is possible to selectively block tunneled connections, 
+@pxref{Forbidden Tunnels}
+
+@node Caching, Memory usage, Network, Top
+@chapter Caching
+
+@menu
+* Cache transparency::          Fresh and stale data.
+* Memory cache::                The in-memory cache.
+* Disk cache::                  The on-disk cache.
+@end menu
+
+@node Cache transparency, Memory cache, Caching, Caching
+@section Cache transparency and validation
+@cindex transparent cache
+@cindex cache transparency
+@cindex out-of-date instances
+@cindex validation
+@cindex revalidation
+@cindex expire
+@cindex stale
+@cindex fresh
+
+If resources on a server change, it is possible for a cached instance
+to become out-of date.  Ideally, a cache would be perfectly
+@dfn{transparent}, meaning that it never serves an out-of-date
+instance; in a universe with a finite speed of signal propagation,
+however, this ideal is impossible to achieve.
+
+If a caching proxy decides that a cached instance is new enough to
+likely still be valid, it will directly serve the instance to the
+client; we then say that the cache decided that the instance is
+@dfn{fresh}.  When an instance is @dfn{stale} (not fresh), the cache
+will check with the upstream server whether the resource has changed;
+we say that the cached instance is being @dfn{revalidated}.
+
+In HTTP/1.1, responsibility for revalidation is shared between the
+client, the server and the proxy itself.  The client can override
+revalidation policy by using the @samp{Cache-Control}
+header@footnote{Or the obsolete @samp{Pragma} header.}; for example,
+some user-agents will request end-to-end revalidation in this way when
+the user shift-clicks on @emph{reload}.  The server may choose to
+specify revalidation policy by using the @samp{Expires} and
+@samp{Cache-Control} headers.  As to the proxy, it needs to choose a
+revalidation policy for instances with neither server- nor client-side
+cache control information.  Of course, nothing (except the HTTP/1.1
+spec, but that is easily ignored) prevents a proxy from overriding the
+client's and server's cache control directives.
+
+@menu
+* Tuning validation::           Tuning Polipo's validation behaviour.
+* Tweaking validation::         Further tweaking of validation.
+@end menu
+
+@node Tuning validation, Tweaking validation, Cache transparency, Cache transparency
+@subsection Tuning validation behaviour
+@cindex age
+@vindex maxAge
+@vindex maxAgeFraction
+@vindex maxExpiresAge
+@vindex maxNoModifiedAge
+
+Polipo's revalidation behaviour is controlled by a number of
+variables.  In the following, an resource's @dfn{age} is the time since
+it was last validated, either because it was fetched from the server
+or because it was revalidated.
+
+The policy defining when cached instances become stale in the absence
+of server-provided information is controlled by the variables
+@code{maxAge}, @code{maxAgeFraction}, @code{maxExpiresAge} and
+@code{maxNoModifiedAge}.  If an instance has an @samp{Expires} header,
+it becomes stale at the date given by that header, or when its age
+becomes larger than @code{maxExpiresAge}, whichever happens first.  If
+an instance has no @samp{Expires} header but has a @samp{LastModified}
+header, it becomes stale when its age reaches either
+@code{maxAgeFraction} of the time since it was last modified or else
+the absolute value @code{maxAge}, whichever happens first.  Finally,
+if an instance has neither @samp{Expires} nor @samp{Last-Modified}, it
+will become stale when its age reaches @code{maxNoModifiedAge}.
+
+@node Tweaking validation,  , Tuning validation, Cache transparency
+@subsection Further tweaking of validation behaviour
+@cindex uncachable
+@cindex vary
+@vindex cacheIsShared
+@vindex mindlesslyCacheVary
+@vindex uncachableFile
+@vindex dontCacheCookies
+@vindex dontCacheRedirects
+@vindex dontTrustVaryETag
+
+If @code{cacheIsShared} is false (it is true by default), Polipo will
+ignore the server-side @samp{Cache-Control} directives @samp{private},
+@samp{s-maxage} and @samp{proxy-must-revalidate}.  This is highly
+desirable behaviour when the proxy is used by just one user, but might
+break some sites if the proxy is shared.
+
+When connectivity is very poor, the variable @code{relaxTransparency}
+can be used to cause Polipo to serve stale instances under some
+circumstances.  If @code{relaxTransparency} is @code{false} (the
+default), all stale instances are validated (@pxref{Cache
+transparency}), and failures to connect are reported to the client.
+This is the default mode of operation of most other proxies, and the
+least likely to surprise the user.
+
+If @code{relaxTransparency} is @code{maybe}, all stale instances are
+still validated, but a failure to connect is only reported as an error
+if no data is available in the cache.  If a connection fails and stale
+data is available, it is served to the client with a suitable HTTP/1.1
+@samp{Warning} header.  Current user-agents do not provide visible
+indication of such warnings, however, and this setting will typically
+cause the browser to display stale data with no indication that
+anything went wrong.  It is useful when you are consulting a live web
+site but don't want to be bothered with failed revalidations.
+
+If @code{relaxTransparency} is @code{true}, missing data is fetched
+from remote servers, but stale data are unconditionally served with no
+validation.  Client-side @samp{Cache-Control} directives are still
+honoured, which means that you can force an end-to-end revalidation
+from the browser's interface (typically by shift-clicking on
+``reload'').  This setting is only useful if you have very bad network
+connectivity or are consulting a very slow web site or one that
+provides incorrect cache control information@footnote{This is for
+example the case of @code{www.microsoft.com}, and also of websites
+generated by a popular Free content management system written in
+Python.} and are willing to manually revalidate pages that you suspect
+are stale.
+
+If @code{mindlesslyCacheVary} is true, the presence of a @samp{Vary}
+header (which indicates that content-negotiation occurred,
+@pxref{Censor Accept-Language}) is ignored, and cached negotiated
+instances are mindlessly returned to the client.  If it is false (the
+default), negotiated instances are revalidated on every client
+request.
+
+Unfortunately, a number of servers (most notably some versions of
+Apache's @code{mod_deflate} module) send objects with a @samp{ETag}
+header that will confuse Polipo in the presence of a @samp{Vary}
+header.  Polipo will make a reasonable check for consistency if
+@samp{dontTrustVaryETag} is set to @samp{maybe} (the default); it will
+systematically ignore @samp{ETag} headers on objects with @samp{Vary}
+headers if it is set to @samp{true}.
+
+A number of websites incorrectly mark variable resources as cachable;
+such issues can be worked around in polipo by manually marking given
+categories of objects as uncachable.  If @code{dontCacheCookies} is
+true, all pages carrying HTTP cookies will be treated as uncachable.
+If @code{dontCacheRedirects} is true, all redirects (301 and 302) will
+be treated as uncachable.  Finally, if everything else fails, a list
+of uncachable URLs can be given in the file specified by
+@code{uncachableFile}, which has the same format as the
+@code{forbiddenFile} (@pxref{Internal forbidden list}).  If not
+specified, its location defaults to @samp{~/.polipo-uncachable} or
+@samp{/etc/polipo/uncachable}, whichever exists.
+
+@node Memory cache, Disk cache, Cache transparency, Caching
+@section The in-memory cache
+
+The in-memory cache consists of a list of HTTP and DNS objects
+maintained in least-recently used order.  An index to the in-memory
+cache is maintained as a (closed) hash table.
+
+When the in-memory cache grows beyond a certain size (controlled by a
+number of variables, @pxref{Memory usage}), or when a hash table
+collision occurs, resources are written out to disk.
+
+@node Disk cache,  , Memory cache, Caching
+@section The on-disk cache
+@cindex filesystem
+@cindex NFS
+@vindex diskCacheRoot
+@vindex maxDiskEntries
+@vindex diskCacheWriteoutOnClose
+@vindex diskCacheFilePermissions
+@vindex diskCacheDirectoryPermissions
+@vindex maxDiskCacheEntrySize
+
+The on-disk cache consists in a filesystem subtree rooted at
+a location defined by the variable @code{diskCacheRoot}, by default
+@code{"/var/cache/polipo/"}.  This directory should normally be
+writeable, readable and seekable by the user running Polipo.  While it
+is best to use a local filesystem for the on-disk cache, a NFSv3- or
+AFS-mounted filesystem should be safe in most implementations.  Do not
+use NFSv2, as it will cause cache corruption @footnote{Polipo assumes
+that @samp{open(O_CREAT | O_EXCL)} works reliably.}.
+
+If @code{diskCacheRoot} is an empty string, no disk cache is used.
+
+The value @code{maxDiskEntries} (32 by default) is the absolute
+maximum of file descriptors held open for on-disk objects.  When this
+limit is reached, Polipo will close descriptors on
+a least-recently-used basis.  This value should be set to be slightly
+larger than the number of resources that you expect to be live at
+a single time; defining the right notion of liveness is left as an
+exercise for the interested reader.
+
+The value @code{diskCacheWriteoutOnClose} (64@dmn{kB} by default) is
+the amount of data that Polipo will write out when closing a disk
+file.  Writing out data when closing a file can avoid subsequently
+reopening it, but causes unnecessary work if the instance is later
+superseded.
+
+The integers @code{diskCacheDirectoryPermissions} and
+@code{diskCacheFilePermissions} are the Unix filesystem permissions
+with which files and directories are created in the on-disk cache;
+they default to @samp{0700} and @samp{0600} respectively.
+
+The variable @code{maxDiskCacheEntrySize} specifies the maximum size,
+in bytes, of an instance that is stored in the on-disk cache.  If set
+to -1 (the default), all objects are stored in the on-disk cache,
+
+@menu
+* Asynchronous writing::        Writing out data when idle.
+* Purging::                     Purging the on-disk cache.
+* Disk format::                 Format of the on-disk cache.
+* Modifying the on-disk cache::
+@end menu
+
+@node Asynchronous writing, Purging, Disk cache, Disk cache
+@subsection Asynchronous writing
+@vindex idleTime
+@vindex maxObjectsWhenIdle
+@vindex maxWriteoutWhenIdle
+
+When Polipo runs out of memory (@pxref{Limiting memory usage}), it
+will start discarding instances from its memory cache.  If a disk
+cache has been configured, it will write out any instance that it
+discards.  Any memory allocation that prompted the purge must then
+wait for the write to complete.
+
+In order to avoid the latency hit that this causes, Polipo will
+preemptively write out instances to the disk cache whenever it is
+idle.  The integer @code{idleTime} specifies the time during which
+Polipo will remain idle before it starts writing out random objects to
+the on-disk cache; this value defaults to 20@dmn{s}.  You may want to
+decrease this value for a busy cache with little memory, or increase
+it if your cache is often idle and has a lot of memory.
+
+The value @code{maxObjectsWhenIdle} (default 32) specifies the maximum
+number of instances that an idle Polipo will write out without
+checking whether there's any new work to do.  The value
+@code{maxWriteoutWhenIdle} specifies the maximum amount of data
+(default 64@dmn{kB}) that Polipo will write out without checking for
+new activity.  Increasing these values will make asynchronous
+write-out slightly faster, at the cost of possibly increasing Polipo's
+latency in some rare circumstances.
+
+@node Purging, Disk format, Asynchronous writing, Disk cache
+@subsection Purging the on-disk cache
+@cindex purging
+@vindex diskCacheUnlinkTime
+@vindex diskCacheTruncateTime
+@vindex diskCacheTruncateSize
+@vindex preciseExpiry
+
+Polipo never removes a file in its on-disk cache, except when it finds
+that the instance that it represents has been superseded by a newer
+version.  In order to keep the on-disk cache from growing without
+bound, it is necessary to @dfn{purge} it once in a while.  Purging the
+cache typically consists in removing some files, truncating large
+files (@pxref{Partial instances}) or moving them to off-line storage.
+
+Polipo itself can be used to purge its on-disk cache; this is done by
+invoking Polipo with the @option{-x} flag.  This can safely be done
+when Polipo is running (@pxref{Modifying the on-disk cache}).
+
+For a purge to be effective, it is necessary to cause Polipo to
+write-out its in-memory cache to disk (@pxref{Stopping}).
+Additionally, Polipo will not necessarily notice the changed files
+until it attempts to access them; thus, you will want it to discard
+its in-memory cache after performing the purge.  The safe way to
+perform a purge is therefore:
+@example
+$ kill -USR1 @var{polipo-pid}
+$ sleep 1
+$ polipo -x
+$ kill -USR2 @var{polipo-pid}
+@end example
+
+The behaviour of the @option{-x} flag is controlled by three
+configuration variables.  The variable @code{diskCacheUnlinkTime}
+specifies the time during which an on-disk entry should remain unused
+before it is eligible for removal; it defaults to 32 days.  
+
+The variable @code{diskCacheTruncateTime} specifies the time for which
+an on-disk entry should remain unused before it is eligible for
+truncation; it defaults to 4 days and a half.  The variable
+@code{diskCacheTruncateSize} specifies the size at which files are
+truncated after they have not been accessed for
+@code{diskCacheTruncateTime}; it defaults to 1@dmn{MB}.
+
+Usually, Polipo uses a file's modification time in order to determine
+whether it is old enough to be expirable.  This heuristic can be
+disabled by setting the variable @code{preciseExpiry} to true.
+
+@node Disk format, Modifying the on-disk cache, Purging, Disk cache
+@subsection Format of the on-disk cache
+@vindex DISK_CACHE_BODY_OFFSET
+@cindex on-disk file
+@cindex on-disk cache
+
+The on-disk cache consists of a collection of files, one per instance.
+The format of an on-disk resource is similar to that of an HTTP
+message: it starts with an HTTP status line, followed by HTTP headers,
+followed by a blank line (@samp{\r\n\r\n}).  The blank line is
+optionally followed by a number of binary zeroes.  The body of the
+instance follows.
+
+The headers of an on-disk file have a few minor differences with HTTP
+messages.  Obviously, there is never a @samp{Transfer-Encoding} line.
+A few additional headers are used by Polipo for its internal
+bookkeeping:
+@itemize
+@item 
+@samp{X-Polipo-Location}: this is the URL of the resource stored in this
+file.  This is always present.
+
+@item
+@samp{X-Polipo-Date}: this is Polipo's estimation of the date at which
+this instance was last validated, and is used for generating the
+@samp{Age} header of HTTP messages.  This is optional, and only stored
+if different from the instance's date.
+
+@item
+@samp{X-Polipo-Access}: this is the date when the instance was last
+accessed by Polipo, and is used for cache purging (@pxref{Purging}).
+This is optional, and is absent if the instance was never accessed.
+
+@item
+@samp{X-Polipo-Body-Offset}: the presence of this line indicates that
+the blank line following the headers is followed by a number of zero
+bytes.  Its value is an integer, which indicates the offset since the
+beginning of the file at which the instance body actually starts.
+This line is optional, and if absent the body starts immediately after
+the blank line.
+
+@end itemize
+
+@node Modifying the on-disk cache,  , Disk format, Disk cache
+@subsection Modifying the on-disk cache
+@cindex on-disk cache
+
+It is safe to modify the on-disk cache while Polipo is running as long
+as no file is ever modified in place.  More precisely, the only safe
+operations are to unlink (remove, delete) files in the disk cache, or
+to atomically add new files to the cache (by performing an exclusive
+open, or by using one of the @samp{link} or @samp{rename} system
+calls).  It is @emph{not} safe to truncate a file in place.
+
+@node Memory usage, Copying, Caching, Top
+@chapter Memory usage
+@cindex memory
+
+Polipo uses two distinct pools of memory, the @dfn{chunk pool} and
+the @dfn{malloc pool}.
+
+@menu
+* Chunk memory::                Chunk memory.
+* Malloc memory::               Malloc memory.
+* Limiting memory usage::       Limiting Polipo's memory usage.
+@end menu
+
+@node Chunk memory, Malloc memory, Memory usage, Memory usage
+@section Chunk memory
+
+@vindex CHUNK_SIZE
+@vindex MALLOC_CHUNKS
+@cindex chunk
+@cindex memory
+
+Most of the memory used by Polipo is stored in chunks, fixed-size
+blocks of memory; the size of a chunk is defined by the compile-time
+constant @code{CHUNK_SIZE}, and defaults to 4096 bytes on 32-bit
+platforms, 8192 on 64-bit ones.  Chunks are used for storing object
+data (bodies of instances) and for temporary I/O buffers.  Increasing
+the chunk size increases performance somewhat, but at the cost of
+larger granularity of allocation and hence larger memory usage.
+
+By default, Polipo uses a hand-crafted memory allocator based on
+@code{mmap}(2) (@code{VirtualAlloc} under Windows) for allocating
+chunks; while this is very slightly faster than the stock memory
+allocator, its main benefit is that it limits memory fragmentation.
+It is possible to disable the chunk allocator, and use
+@code{malloc}(3) for all memory allocation, by defining
+@code{MALLOC_CHUNKS} at compile time; this is probably only useful for
+debugging.
+
+There is one assumption made about @code{CHUNK_SIZE}:
+@code{CHUNK_SIZE} multiplied by the number of bits in an
+@code{unsigned long} (actually in a @code{ChunkBitmap} --- see
+@file{chunk.c}) must be a multiple of the page size, which is 4096 on
+most systems (8192 on Alpha, and apparently 65536 on Windows).
+
+As all network I/O will be performed in units of one to two chunks,
+@code{CHUNK_SIZE} should be at least equal to your network interface's
+MTU (typically 1500 bytes).  Additionally, as much I/O will be done at
+@code{CHUNK_SIZE}-aligned addresses, @code{CHUNK_SIZE} should ideally
+be a multiple of the page size.
+
+In summary, 2048, 4096, 8192 and 16384 are good choices for
+@code{CHUNK_SIZE}.
+
+@node Malloc memory, Limiting memory usage, Chunk memory, Memory usage
+@section Malloc allocation
+@cindex malloc
+@cindex memory
+
+Polipo uses the standard @code{malloc}(3) memory allocator for
+allocating small data structures (up to 100 bytes), small strings and
+atoms (unique strings).
+
+@node Limiting memory usage,  , Malloc memory, Memory usage
+@section Limiting Polipo's memory usage
+@cindex limiting memory
+@cindex memory
+
+Polipo is designed to work well when given little memory, but will
+happily scale to larger configurations.  For that reason, you need to
+inform it of the amount of memory it can use.
+
+@menu
+* Limiting chunk usage::        Discard objects when low on chunks.
+* Limiting object usage::       Limit the number of objects.
+* OS usage limits::             Don't impose OS limits.
+@end menu
+
+@node Limiting chunk usage, Limiting object usage, Limiting memory usage, Limiting memory usage
+@subsection Limiting chunk usage
+
+@vindex chunkHighMark
+@vindex chunkCriticalMark
+@vindex chunkLowMark
+@vindex CHUNK_SIZE
+@cindex memory
+@cindex chunk
+
+You can limit Polipo's usage of chunk memory by setting
+@code{chunkHighMark} and @code{chunkLowMark}.
+
+The value @code{chunkHighMark} is the absolute maximum number of bytes
+of allocated chunk memory.  When this value is reached, Polipo will try
+to purge objects from its in-memory cache; if that fails to free memory,
+Polipo will start dropping connections.  This value defaults to
+24@dmn{MB} or one quarter of the machine's physical memory, whichever is
+less.
+
+When chunk usage falls back below @code{chunkLowMark}, Polipo will
+stop discarding in-memory objects.  The value
+@code{chunkCriticalMark}, which should be somewhere between
+@code{chunkLowMark} and @code{chunkHighMark}, specifies the value
+above which Polipo will make heroic efforts to free memory, including
+punching holes in the middle of instances, but without dropping
+connections.
+
+Unless set explicitly, both @code{chunkLowMark} and
+@code{chunkCriticalMark} are computed automatically from
+@code{chunkHighMark}.
+
+@node Limiting object usage, OS usage limits, Limiting chunk usage, Limiting memory usage
+@subsection Limiting object usage
+
+@vindex objectHighMark
+@vindex publicObjectLowMark
+@vindex objectHashTableSize
+
+Besides limiting chunk usage, it is possible to limit Polipo's memory
+usage by bounding the number of objects it keeps in memory at any given
+time.  This is done with @code{objectHighMark} and
+@code{publicObjectLowMark}.
+
+The value @code{objectHighMark} is the absolute maximum of objects
+held in memory (including resources and server addresses).  When the
+number of in-memory objects that haven't been superseded yet falls
+below @code{publicObjectLowMark}, Polipo will stop writing out objects
+to disk (superseded objects are discarded as soon as possible).
+
+On 32-bit architectures, every object costs 108 bytes of memory, plus
+storage for every globally unique header that is not handled specially
+(hopefully negligible), plus an overhead of one word (4 bytes) for
+every chunk of data in the object.
+
+You may also want to change @code{objectHashTableSize}.  This is the
+size of the hash table used for holding objects; it should be a power
+of two and defaults to eight times @code{objectHighMark}.  Increasing
+this value will reduce the number of objects being written out to disk
+due to hash table collisions.  Every hash table entry costs one word.
+
+@node OS usage limits,  , Limiting object usage, Limiting memory usage
+@subsection OS usage limits
+@cindex usage limit
+@cindex ulimit
+@cindex OOM killer
+
+Many operating systems permit limiting a process' memory usage by
+setting a @dfn{usage limit}; on most Unix-like systems, this is done
+with the @option{-v} option to the @command{ulimit} command.
+Typically, the effect is to cause calls to the @code{malloc} and
+@code{mmap} library functions to fail.
+
+Polipo will usually react gracefully to failures to allocate
+memory@footnote{There are exactly three places in the code where
+Polipo will give up and exit if out of memory; all three are extremely
+unlikely to happen in practice.}.  Nonetheless, you should avoid using
+OS limits to limit Polipo's memory usage: when it hits an OS limit,
+Polipo cannot allocate the memory needed to schedule recovery from the
+out-of-memory condition, and has no choice other than to drop a
+connection.
+
+Unfortunately, some operating system kernels (notably certain Linux
+releases) fail to fail an allocation if no usage limit is given;
+instead, they either crash when memory is exhausted, or else start
+killing random processes with no advance warning@footnote{How I wish
+for a @samp{SIGXMEM} signal.}.  On such systems, imposing an
+(unrealistically large) usage limit on Polipo is the safe thing to do.
+
+@node Copying, Variable index, Memory usage, Top
+@unnumbered Copying
+You are allowed to do anything you wish with Polipo as long as you
+don't deny my right to be recognised as its author and you don't blame
+me if anything goes wrong.
+
+More formally, Polipo is distributed under the following terms:
+
+@quotation
+Copyright @copyright{} 2003--2006 by Juliusz Chroboczek
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+@end quotation
+The last sentence is what happens when you allow lawyers to have it
+their way with a language.
+
+@node Variable index, Concept index, Copying, Top
+@unnumbered Variable index
+@printindex vr
+
+@node Concept index,  , Variable index, Top
+@unnumbered Concept index
+@printindex cp
+
+@bye

+ 2895 - 0
polipo/server.c

@@ -0,0 +1,2895 @@
+/*
+Copyright (c) 2003-2006 by Juliusz Chroboczek
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+#include "polipo.h"
+
+int serverExpireTime =  24 * 60 * 60;
+int smallRequestTime = 10;
+int replyUnpipelineTime = 20;
+int replyUnpipelineSize = 1024 * 1024;
+int pipelineAdditionalRequests = 1;
+int maxPipelineTrain = 10;
+AtomPtr parentProxy = NULL;
+AtomPtr parentHost = NULL;
+int parentPort = -1;
+int pmmFirstSize = 0, pmmSize = 0;
+int serverSlots = 2;
+int serverSlots1 = 4;
+int serverMaxSlots = 8;
+int dontCacheRedirects = 0;
+int maxSideBuffering = 1500;
+int maxConnectionAge = 1260;
+int maxConnectionRequests = 400;
+int alwaysAddNoTransform = 0;
+
+static HTTPServerPtr servers = 0;
+
+static int httpServerContinueConditionHandler(int, ConditionHandlerPtr);
+static int initParentProxy(void);
+static int parentProxySetter(ConfigVariablePtr var, void *value);
+static void httpServerDelayedFinish(HTTPConnectionPtr);
+static int allowUnalignedRangeRequests = 0;
+
+void
+preinitServer(void)
+{
+    CONFIG_VARIABLE_SETTABLE(parentProxy, CONFIG_ATOM_LOWER, parentProxySetter,
+                    "Parent proxy (host:port).");
+    CONFIG_VARIABLE(serverExpireTime, CONFIG_TIME,
+                    "Time during which server data is valid.");
+    CONFIG_VARIABLE_SETTABLE(smallRequestTime, CONFIG_TIME, configIntSetter,
+                             "Estimated time for a small request.");
+    CONFIG_VARIABLE_SETTABLE(replyUnpipelineTime, CONFIG_TIME, configIntSetter,
+                             "Estimated time for a pipeline break.");
+    CONFIG_VARIABLE_SETTABLE(replyUnpipelineSize, CONFIG_INT, configIntSetter,
+                    "Size for a pipeline break.");
+    CONFIG_VARIABLE_SETTABLE(pipelineAdditionalRequests, CONFIG_TRISTATE,
+                             configIntSetter,
+                             "Pipeline requests on an active connection.");
+    CONFIG_VARIABLE_SETTABLE(maxPipelineTrain, CONFIG_INT,
+                             configIntSetter,
+                             "Maximum number of requests "
+                             "pipelined at a time.");
+    CONFIG_VARIABLE(pmmFirstSize, CONFIG_INT,
+                    "The size of the first PMM chunk.");
+    CONFIG_VARIABLE(pmmSize, CONFIG_INT,
+                    "The size of a PMM chunk.");
+    CONFIG_VARIABLE(serverSlots, CONFIG_INT,
+                    "Maximum number of connections per server.");
+    CONFIG_VARIABLE(serverSlots1, CONFIG_INT,
+                    "Maximum number of connections per HTTP/1.0 server.");
+    CONFIG_VARIABLE(serverMaxSlots, CONFIG_INT,
+                    "Maximum number of connections per broken server.");
+    CONFIG_VARIABLE(dontCacheRedirects, CONFIG_BOOLEAN,
+                    "If true, don't cache redirects.");
+    CONFIG_VARIABLE_SETTABLE(allowUnalignedRangeRequests,
+                             CONFIG_BOOLEAN, configIntSetter,
+                             "Allow unaligned range requests (unreliable).");
+    CONFIG_VARIABLE_SETTABLE(maxSideBuffering,
+                             CONFIG_INT, configIntSetter,
+                             "Maximum buffering for PUT and POST requests.");
+    CONFIG_VARIABLE_SETTABLE(maxConnectionAge,
+                             CONFIG_TIME, configIntSetter,
+                             "Maximum age of a server-side connection.");
+    CONFIG_VARIABLE_SETTABLE(maxConnectionRequests,
+                             CONFIG_INT, configIntSetter,
+                             "Maximum number of requests on a server-side connection.");
+    CONFIG_VARIABLE(alwaysAddNoTransform, CONFIG_BOOLEAN,
+                    "If true, add a no-transform directive to all requests.");
+}
+
+static int
+parentProxySetter(ConfigVariablePtr var, void *value)
+{
+    configAtomSetter(var, value);
+    initParentProxy();
+    return 1;
+}
+
+static void
+discardServer(HTTPServerPtr server)
+{
+    HTTPServerPtr previous;
+    assert(!server->request);
+
+    if(server == servers)
+        servers = server->next;
+    else {
+        previous = servers;
+        while(previous->next != server)
+            previous = previous->next;
+        previous->next = server->next;
+    }
+
+    if(server->connection)
+        free(server->connection);
+    if(server->idleHandler)
+        free(server->idleHandler);
+    if(server->name)
+        free(server->name);
+
+    free(server);
+}
+
+static int
+httpServerIdle(HTTPServerPtr server)
+{
+    int i;
+    if(server->request) 
+        return 0;
+    for(i = 0; i < server->maxslots; i++)
+        if(server->connection[i])
+            return 0;
+    return 1;
+}
+
+static int
+expireServersHandler(TimeEventHandlerPtr event)
+{
+    HTTPServerPtr server, next;
+    TimeEventHandlerPtr e;
+    server = servers;
+    while(server) {
+        next = server->next;
+        if(httpServerIdle(server) &&
+           server->time + serverExpireTime < current_time.tv_sec)
+            discardServer(server);
+        server = next;
+    }
+    e = scheduleTimeEvent(serverExpireTime / 60 + 60, 
+                          expireServersHandler, 0, NULL);
+    if(!e) {
+        do_log(L_ERROR, "Couldn't schedule server expiry.\n");
+        polipoExit();
+    }
+    return 1;
+}
+
+static int
+roundSize(int size)
+{
+    return (size + CHUNK_SIZE - 1) / CHUNK_SIZE * CHUNK_SIZE;
+}
+
+static int
+initParentProxy()
+{
+    AtomPtr host, port_atom;
+    int rc, port;
+
+    if(parentHost) {
+        releaseAtom(parentHost);
+        parentHost = NULL;
+    }
+    if(parentPort >= 0)
+        parentPort = -1;
+
+    if(parentProxy != NULL && parentProxy->length == 0) {
+        releaseAtom(parentProxy);
+        parentProxy = NULL;
+    }
+
+    if(parentProxy == NULL)
+        return 1;
+
+    rc = atomSplit(parentProxy, ':', &host, &port_atom);
+    if(rc <= 0) {
+        do_log(L_ERROR, "Couldn't parse parentProxy.");
+        releaseAtom(parentProxy);
+        parentProxy = NULL;
+        return -1;
+    }
+
+    port = atoi(port_atom->string);
+    if(port <= 0 || port >= 0x10000) {
+        releaseAtom(host);
+        releaseAtom(port_atom);
+        do_log(L_ERROR, "Couldn't parse parentProxy.");
+        releaseAtom(parentProxy);
+        parentProxy = NULL;
+        return -1;
+    }
+
+    parentHost = host;
+    parentPort = port;
+    return 1;
+}
+
+void
+initServer(void)
+{
+    TimeEventHandlerPtr event;
+    servers = NULL;
+
+    if(pmmFirstSize || pmmSize) {
+        if(pmmSize == 0) pmmSize = pmmFirstSize;
+        if(pmmFirstSize == 0) pmmFirstSize = pmmSize;
+        pmmSize = roundSize(pmmSize);
+        pmmFirstSize = roundSize(pmmFirstSize);
+    }
+
+    if(serverMaxSlots < 1)
+        serverMaxSlots = 1;
+    if(serverSlots < 1)
+        serverSlots = 1;
+    if(serverSlots > serverMaxSlots)
+        serverSlots = serverMaxSlots;
+    if(serverSlots1 < serverSlots)
+        serverSlots1 = serverSlots;
+    if(serverSlots1 > serverMaxSlots)
+        serverSlots1 = serverMaxSlots;
+
+    initParentProxy();
+
+    event = scheduleTimeEvent(serverExpireTime / 60 + 60, expireServersHandler,
+                              0, NULL);
+    if(event == NULL) {
+        do_log(L_ERROR, "Couldn't schedule server expiry.\n");
+        exit(1);
+    }
+}
+
+static HTTPServerPtr
+getServer(char *name, int port, int proxy)
+{
+    HTTPServerPtr server;
+    int i;
+
+    server = servers;
+    while(server) {
+        if(strcmp(server->name, name) == 0 && server->port == port &&
+           server->isProxy == proxy) {
+            if(httpServerIdle(server) &&
+               server->time +  serverExpireTime < current_time.tv_sec) {
+                discardServer(server);
+                server = NULL;
+                break;
+            } else {
+                server->time = current_time.tv_sec;
+                return server;
+            }
+        }
+        server = server->next;
+    }
+    
+    server = malloc(sizeof(HTTPServerRec));
+    if(server == NULL) {
+        do_log(L_ERROR, "Couldn't allocate server.\n");
+        return NULL;
+    }
+
+    server->connection = malloc(serverMaxSlots * sizeof(HTTPConnectionPtr));
+    if(server->connection == NULL) {
+        do_log(L_ERROR, "Couldn't allocate server.\n");
+        free(server);
+        return NULL;
+    }
+
+    server->idleHandler = malloc(serverMaxSlots * sizeof(FdEventHandlerPtr));
+    if(server->connection == NULL) {
+        do_log(L_ERROR, "Couldn't allocate server.\n");
+        free(server->connection);
+        free(server);
+        return NULL;
+    }
+
+    server->maxslots = serverMaxSlots;
+
+    server->name = strdup(name);
+    if(server->name == NULL) {
+        do_log(L_ERROR, "Couldn't allocate server name.\n");
+        free(server);
+        return NULL;
+    }
+
+    server->port = port;
+    server->addrindex = 0;
+    server->isProxy = proxy;
+    server->version = HTTP_UNKNOWN;
+    server->persistent = 0;
+    server->pipeline = 0;
+    server->time = current_time.tv_sec;
+    server->rtt = -1;
+    server->rate = -1;
+    server->numslots = MIN(serverSlots, server->maxslots);
+    for(i = 0; i < server->maxslots; i++) {
+        server->connection[i] = NULL;
+        server->idleHandler[i] = NULL;
+    }
+    server->request = NULL;
+    server->request_last = NULL;
+    server->lies = 0;
+
+    server->next = servers;
+    servers = server;
+    return server;
+}
+
+int
+httpServerQueueRequest(HTTPServerPtr server, HTTPRequestPtr request)
+{
+    assert(request->request && request->request->request == request);
+    assert(request->connection == NULL);
+    if(server->request) {
+        server->request_last->next = request;
+        server->request_last = request;
+    } else {
+        server->request_last = request;
+        server->request = request;
+    }
+    return 1;
+}
+
+void
+httpServerAbort(HTTPConnectionPtr connection, int fail,
+                int code, AtomPtr message)
+{
+    HTTPRequestPtr request = connection->request;
+    if(request) {
+        if(request->request) {
+            httpClientError(request->request, code, retainAtom(message));
+        }
+        if(fail) {
+            request->object->flags |= OBJECT_FAILED;
+            if(request->object->flags & OBJECT_INITIAL)
+                abortObject(request->object, code, retainAtom(message));
+            notifyObject(request->object);
+        }
+    }
+    releaseAtom(message);
+    if(!connection->connecting)
+        httpServerFinish(connection, 1, 0);
+}
+
+void
+httpServerAbortRequest(HTTPRequestPtr request, int fail,
+                       int code, AtomPtr message)
+{
+    if(request->connection && request == request->connection->request) {
+        httpServerAbort(request->connection, fail, code, message);
+    } else {
+        HTTPRequestPtr requestor = request->request;
+        if(requestor) {
+            requestor->request = NULL;
+            request->request = NULL;
+            httpClientError(requestor, code, retainAtom(message));
+        }
+        if(fail) {
+            request->object->flags |= OBJECT_FAILED;
+            if(request->object->flags & OBJECT_INITIAL)
+                abortObject(request->object, code, retainAtom(message));
+            notifyObject(request->object);
+        }
+        releaseAtom(message);
+    }
+}
+
+void 
+httpServerClientReset(HTTPRequestPtr request)
+{
+    if(request->connection && 
+       request->connection->fd >= 0 &&
+       !request->connection->connecting &&
+       request->connection->request == request)
+        pokeFdEvent(request->connection->fd, -ECLIENTRESET, POLLIN | POLLOUT);
+}
+
+
+int
+httpMakeServerRequest(char *name, int port, ObjectPtr object, 
+                  int method, int from, int to, HTTPRequestPtr requestor)
+{
+    HTTPServerPtr server;
+    HTTPRequestPtr request;
+    int rc;
+
+    assert(!(object->flags & OBJECT_INPROGRESS));
+
+    if(parentHost) {
+        server = getServer(parentHost->string, parentPort, 1);
+    } else {
+        server = getServer(name, port, 0);
+    }
+    if(server == NULL) return -1;
+
+    object->flags |= OBJECT_INPROGRESS;
+    object->requestor = requestor;
+
+    request = httpMakeRequest();
+    if(!request) {
+        do_log(L_ERROR, "Couldn't allocate request.\n");
+        return -1;
+    }
+
+    /* Because we allocate objects in chunks, we cannot have data that
+       doesn't start at a chunk boundary. */
+    if(from % CHUNK_SIZE != 0) {
+        if(allowUnalignedRangeRequests) {
+            objectFillFromDisk(object, from / CHUNK_SIZE * CHUNK_SIZE, 1);
+            if(objectHoleSize(object, from - 1) != 0)
+                from = from / CHUNK_SIZE * CHUNK_SIZE;
+        } else {
+            from = from / CHUNK_SIZE * CHUNK_SIZE;
+        }
+    }
+
+    request->object = retainObject(object);
+    request->method = method;
+    if(method == METHOD_CONDITIONAL_GET) {
+        if(server->lies > 0)
+            request->method = METHOD_HEAD;
+    }
+    request->flags =
+        REQUEST_PERSISTENT |
+        (expectContinue ? (requestor->flags & REQUEST_WAIT_CONTINUE) : 0);
+    request->from = from;
+    request->to = to;
+    request->request = requestor;
+    requestor->request = request;
+    request->cache_control = requestor->cache_control;
+    request->time0 = null_time;
+    request->time1 = null_time;
+
+    rc = httpServerQueueRequest(server, request);
+    if(rc < 0) {
+        do_log(L_ERROR, "Couldn't queue request.\n");
+        request->request = NULL;
+        requestor->request = NULL;
+        object->flags &= ~(OBJECT_INPROGRESS | OBJECT_VALIDATING);
+        releaseNotifyObject(object);
+        httpDestroyRequest(request);
+        return 1;
+    }
+
+    if(request->flags & REQUEST_WAIT_CONTINUE) {
+        if(server->version == HTTP_10) {
+            httpServerAbortRequest(request, 1,
+                                   417, internAtom("Expectation failed"));
+            return 1;
+        }
+    } else if(expectContinue >= 2 && server->version == HTTP_11) {
+        if(request->method == METHOD_POST || request->method == METHOD_PUT ||
+           request->method == METHOD_OPTIONS || request->method == METHOD_DELETE)
+            request->flags |= REQUEST_WAIT_CONTINUE;
+    }
+        
+ again:
+    rc = httpServerTrigger(server);
+    if(rc < 0) {
+        /* We must be very short on memory.  If there are any requests
+           queued, we abort one and try again.  If there aren't, we
+           give up. */
+        do_log(L_ERROR, "Couldn't trigger server -- out of memory?\n");
+        if(server->request) {
+            httpServerAbortRequest(server->request, 1, 503,
+                                   internAtom("Couldn't trigger server"));
+            goto again;
+        }
+    }
+    return 1;
+}
+
+int
+httpServerConnection(HTTPServerPtr server)
+{
+    HTTPConnectionPtr connection;
+    int i;
+
+    connection = httpMakeConnection();
+    if(connection == NULL) {
+        do_log(L_ERROR, "Couldn't allocate server connection.\n");
+        return -1;
+    }
+    connection->server = server;
+
+    for(i = 0; i < server->numslots; i++) {
+        if(!server->connection[i]) {
+            server->connection[i] = connection;
+            break;
+        }
+    }
+    assert(i < server->numslots);
+    
+    connection->request = NULL;
+    connection->request_last = NULL;
+
+    do_log(D_SERVER_CONN, "C... %s:%d.\n",
+           scrub(connection->server->name), connection->server->port);
+    httpSetTimeout(connection, serverTimeout);
+    if(socksParentProxy) {
+        connection->connecting = CONNECTING_SOCKS;
+        do_socks_connect(server->name, connection->server->port,
+                         httpServerSocksHandler, connection);
+    } else {
+        connection->connecting = CONNECTING_DNS;
+        do_gethostbyname(server->name, 0,
+                         httpServerConnectionDnsHandler,
+                         connection);
+    }
+    return 1;
+}
+
+int
+httpServerConnectionDnsHandler(int status, GethostbynameRequestPtr request)
+{
+    HTTPConnectionPtr connection = request->data;
+
+    httpSetTimeout(connection, -1);
+
+    if(status <= 0) {
+        AtomPtr message;
+        message = internAtomF("Host %s lookup failed: %s",
+                              request->name ?
+                              request->name->string : "(unknown)",
+                              request->error_message ?
+                              request->error_message->string :
+                              pstrerror(-status));
+        do_log(L_ERROR, "Host %s lookup failed: %s (%d).\n", 
+               request->name ?
+               scrub(request->name->string) : "(unknown)",
+               request->error_message ?
+               request->error_message->string :
+               pstrerror(-status), -status);
+        connection->connecting = 0;
+        if(connection->server->request)
+            httpServerAbortRequest(connection->server->request, 1, 504,
+                                   retainAtom(message));
+        httpServerAbort(connection, 1, 502, message);
+        return 1;
+    }
+
+    if(request->addr->string[0] == DNS_CNAME) {
+        if(request->count > 10) {
+            AtomPtr message = internAtom("DNS CNAME loop");
+            do_log(L_ERROR, "DNS CNAME loop.\n");
+            connection->connecting = 0;
+            if(connection->server->request)
+                httpServerAbortRequest(connection->server->request, 1, 504,
+                                       retainAtom(message));
+            httpServerAbort(connection, 1, 504, message);
+            return 1;
+        }
+            
+        httpSetTimeout(connection, serverTimeout);
+        do_gethostbyname(request->addr->string + 1, request->count + 1,
+                         httpServerConnectionDnsHandler,
+                         connection);
+        return 1;
+    }
+
+    connection->connecting = CONNECTING_CONNECT;
+    httpSetTimeout(connection, serverTimeout);
+    do_connect(retainAtom(request->addr), connection->server->addrindex,
+               connection->server->port,
+               httpServerConnectionHandler, connection);
+    return 1;
+}
+
+int
+httpServerConnectionHandler(int status,
+                            FdEventHandlerPtr event,
+                            ConnectRequestPtr request)
+{
+    HTTPConnectionPtr connection = request->data;
+
+    assert(connection->fd < 0);
+    if(request->fd >= 0) {
+        int rc;
+        connection->fd = request->fd;
+        connection->server->addrindex = request->index;
+        rc = setNodelay(connection->fd, 1);
+        if(rc < 0)
+            do_log_error(L_WARN, errno, "Couldn't disable Nagle's algorithm");
+    }
+
+    return httpServerConnectionHandlerCommon(status, connection);
+}
+
+int
+httpServerSocksHandler(int status, SocksRequestPtr request)
+{
+    HTTPConnectionPtr connection = request->data;
+
+    assert(connection->fd < 0);
+    if(request->fd >= 0) {
+        connection->fd = request->fd;
+        connection->server->addrindex = 0;
+    }
+    return httpServerConnectionHandlerCommon(status, connection);
+}
+
+int
+httpServerConnectionHandlerCommon(int status, HTTPConnectionPtr connection)
+{
+    httpSetTimeout(connection, -1);
+
+    if(status < 0) {
+        AtomPtr message = 
+            internAtomError(-status, "Connect to %s:%d failed",
+                            connection->server->name,
+                            connection->server->port);
+        if(status != -ECLIENTRESET)
+            do_log_error(L_ERROR, -status, "Connect to %s:%d failed",
+                         scrub(connection->server->name),
+                         connection->server->port);
+        connection->connecting = 0;
+        if(connection->server->request)
+            httpServerAbortRequest(connection->server->request,
+                                   status != -ECLIENTRESET, 504, 
+                                   retainAtom(message));
+        httpServerAbort(connection, status != -ECLIENTRESET, 504, message);
+        return 1;
+    }
+
+    do_log(D_SERVER_CONN, "C    %s:%d.\n",
+           scrub(connection->server->name), connection->server->port);
+
+    connection->connecting = 0;
+    /* serverTrigger will take care of inserting any timeouts */
+    httpServerTrigger(connection->server);
+    return 1;
+}
+
+int
+httpServerIdleHandler(int a, FdEventHandlerPtr event)
+{
+    HTTPConnectionPtr connection = *(HTTPConnectionPtr*)event->data;
+    HTTPServerPtr server = connection->server;
+    int i;
+
+    assert(!connection->request);
+
+    do_log(D_SERVER_CONN, "Idle connection to %s:%d died.\n", 
+           scrub(connection->server->name), connection->server->port);
+
+    for(i = 0; i < server->maxslots; i++) {
+        if(connection == server->connection[i]) {
+            server->idleHandler[i] = NULL;
+            break;
+        }
+    }
+    assert(i < server->maxslots);
+
+    httpServerAbort(connection, 1, 504, internAtom("Timeout"));
+    return 1;
+}
+
+/* Discard aborted requests at the head of the queue. */
+static void
+httpServerDiscardRequests(HTTPServerPtr server)
+{
+    HTTPRequestPtr request;
+    while(server->request && !server->request->request) {
+        request = server->request;
+        server->request = request->next;
+        request->next = NULL;
+        if(server->request == NULL)
+            server->request_last = NULL;
+        request->object->flags &= ~(OBJECT_INPROGRESS | OBJECT_VALIDATING);
+        releaseNotifyObject(request->object);
+        request->object = NULL;
+        httpDestroyRequest(request);
+    }
+}
+
+static int
+pipelineIsSmall(HTTPConnectionPtr connection)
+{
+    HTTPRequestPtr request = connection->request;
+
+    if(pipelineAdditionalRequests <= 0)
+        return 0;
+    else if(pipelineAdditionalRequests >= 2)
+        return 1;
+
+    if(!request)
+        return 1;
+    if(request->next || !(request->flags & REQUEST_PERSISTENT))
+        return 0;
+    if(request->method == METHOD_HEAD || 
+       request->method == METHOD_CONDITIONAL_GET)
+        return 1;
+    if(request->to >= 0 && connection->server->rate > 0 &&
+       request->to - request->from < connection->server->rate * 
+       smallRequestTime)
+        return 1;
+    return 0;
+}
+
+static int
+numRequests(HTTPServerPtr server)
+{
+    int n = 0;
+    HTTPRequestPtr request = server->request;
+    while(request) {
+        n++;
+        request = request->next;
+    }
+    return n;
+}
+
+HTTPConnectionPtr
+httpServerGetConnection(HTTPServerPtr server, int *idle_return)
+{
+    int i, j;
+    int connecting = 0, empty = 0, idle = 0;
+
+    j = -1;
+    /* Try to find an idle connection */
+    for(i = 0; i < server->numslots; i++) {
+        if(server->connection[i]) {
+            if(!server->connection[i]->connecting) {
+                if(!server->connection[i]->request) {
+                    if(server->idleHandler[i])
+                        unregisterFdEvent(server->idleHandler[i]);
+                    server->idleHandler[i] = NULL;
+                    if(j < 0) j = i;
+                    idle++;
+                }
+            } else
+                connecting++;
+        } else
+            empty++;
+    }
+
+    if(j >= 0) {
+        *idle_return = idle;
+        return server->connection[j];
+    }
+
+    /* If there's an empty slot, schedule connection creation */
+    if(empty) {
+        /* Don't open a connection if there are already enough in
+           progress, except if the server doesn't do persistent
+           connections and there's only one in progress. */
+        if((connecting == 0 || (server->persistent <= 0 && connecting <= 1)) ||
+           connecting < numRequests(server)) {
+            httpServerConnection(server);
+        }
+    }
+
+    /* Find a connection that can accept additional requests */
+    if(server->version == HTTP_11 && server->pipeline >= 4) {
+        for(i = 0; i < serverSlots; i++) {
+            if(server->connection[i] && !server->connection[i]->connecting &&
+               pipelineIsSmall(server->connection[i])) {
+                if(server->idleHandler[i])
+                    unregisterFdEvent(server->idleHandler[i]);
+                server->idleHandler[i] = NULL;
+                *idle_return = 0;
+                return server->connection[i];
+            }
+        }
+    }
+    *idle_return = idle;
+    return NULL;
+}
+
+int
+httpServerTrigger(HTTPServerPtr server)
+{
+    HTTPConnectionPtr connection;
+    HTTPRequestPtr request;
+    int idle, n, i, rc, numidle;
+
+    while(server->request) {
+        httpServerDiscardRequests(server);
+
+        if(!server->request)
+            break;
+
+        if(REQUEST_SIDE(server->request)) {
+            rc = httpServerSideRequest(server);
+            /* If rc is 0, httpServerSideRequest didn't dequeue this
+               request.  Go through the scheduling loop again, come
+               back later. */
+            if(rc <= 0) break;
+            continue;
+        }
+        connection = httpServerGetConnection(server, &numidle);
+        if(!connection) break;
+
+        /* If server->pipeline <= 0, we don't do pipelining.  If
+           server->pipeline is 1, then we are ready to start probing
+           for pipelining on the server; we then send exactly two
+           requests in what is hopefully a single packet to check
+           whether the server has the nasty habit of discarding its
+           input buffers after each request.
+           If server->pipeline is 2 or 3, the pipelining probe is in
+           progress on this server, and we don't pipeline anything
+           until it succeeds.  When server->pipeline >= 4, pipelining
+           is believed to work on this server. */
+        if(server->version != HTTP_11 || server->pipeline <= 0 ||
+           server->pipeline == 2 || server->pipeline == 3) {
+            if(connection->pipelined == 0)
+                n = 1;
+            else
+                n = 0;
+        } else if(server->pipeline == 1) {
+            if(connection->pipelined == 0)
+                n = MIN(2, maxPipelineTrain);
+            else
+                n = 0;
+        } else {
+            n = maxPipelineTrain;
+        }
+
+        /* Don't pipeline if there are more idle connections */
+        if(numidle >= 2)
+            n = MIN(n, 1);
+    
+        idle = !connection->pipelined;
+        i = 0;
+        while(server->request && connection->pipelined < n) {
+            httpServerDiscardRequests(server);
+            if(!server->request) break;
+            request = server->request;
+            assert(request->request->request == request);
+            rc = httpWriteRequest(connection, request, -1);
+            if(rc < 0) {
+                if(i == 0)
+                    httpServerAbortRequest(request, rc != -ECLIENTRESET, 502,
+                                           internAtom("Couldn't "
+                                                      "write request"));
+                break;
+            }
+            do_log(D_SERVER_CONN, "W: ");
+            do_log_n(D_SERVER_CONN, 
+                     request->object->key, request->object->key_size);
+            do_log(D_SERVER_CONN, " (%d)\n", request->method);
+            if(connection->pipelined > 0)
+                request->flags |= REQUEST_PIPELINED;
+            request->time0 = current_time;
+            i++;
+            server->request = request->next;
+            request->next = NULL;
+            if(server->request == NULL)
+                server->request_last = NULL;
+            httpQueueRequest(connection, request);
+            connection->pipelined++;
+        }
+        if(server->persistent > 0 && server->pipeline == 1 && i >= 2)
+            server->pipeline = 2;
+
+        if(i > 0) httpServerSendRequest(connection);
+
+        if(idle && connection->pipelined > 0)
+            httpServerReply(connection, 0);
+
+        if(i == 0) break;
+    }
+
+    for(i = 0; i < server->maxslots; i++) {
+        if(server->connection[i] &&
+           !server->connection[i]->connecting &&
+           !server->connection[i]->request) {
+            /* Artificially age any fresh connections that aren't used
+               straight away; this is necessary for the logic for POST and 
+               the logic that determines whether a given request should be 
+               restarted. */
+            if(server->connection[i]->serviced == 0)
+                server->connection[i]->serviced = 1;
+            if(!server->idleHandler[i])
+                server->idleHandler[i] = 
+                    registerFdEvent(server->connection[i]->fd, POLLIN,
+                                    httpServerIdleHandler,
+                                    sizeof(HTTPConnectionPtr),
+                                    &server->connection[i]);
+            if(!server->idleHandler[i]) {
+                do_log(L_ERROR, "Couldn't register idle handler.\n");
+                httpServerFinish(server->connection[i], 1, 0);
+            }
+            httpSetTimeout(server->connection[i], serverIdleTimeout);
+        }
+    }
+
+    return 1;
+}
+
+int
+httpServerSideRequest(HTTPServerPtr server)
+{
+    HTTPRequestPtr request = server->request;
+    HTTPConnectionPtr connection;
+    HTTPRequestPtr requestor = request->request;
+    HTTPConnectionPtr client = requestor->connection;
+    int rc, i, freeslots, idle, connecting;
+
+    assert(REQUEST_SIDE(request));
+
+    connection = NULL;
+    freeslots = 0;
+    idle = -1;
+    connecting = 0;
+
+    /* Find a fresh connection */
+    for(i = 0; i < server->numslots; i++) {
+        if(!server->connection[i])
+            freeslots++;
+        else if(!server->connection[i]->connecting) {
+            if(!server->connection[i]->request) {
+                if(server->connection[i]->serviced == 0) {
+                    if(server->idleHandler[i])
+                        unregisterFdEvent(server->idleHandler[i]);
+                    server->idleHandler[i] = NULL;
+                    connection = server->connection[i];
+                    break;
+                } else {
+                    idle = i;
+                }
+            }
+        } else {
+            connecting++;
+        }
+    }
+
+    if(!connection) {
+        /* Make sure that a fresh connection will be established at some
+           point, then wait until httpServerTrigger calls us again. */
+        if(freeslots) {
+            httpServerConnection(server);
+        } else {
+            if(idle >= 0) {
+                /* Shutdown a random idle connection */
+                pokeFdEvent(server->connection[idle]->fd, 
+                            -EDOSHUTDOWN, POLLIN | POLLOUT);
+            }
+        }
+        return 0;
+    }
+
+    rc = httpWriteRequest(connection, request, client->bodylen);
+    if(rc < 0) {
+        do_log(L_ERROR, "Couldn't write POST or PUT request.\n");
+        httpServerAbortRequest(request, rc != -ECLIENTRESET, 502,
+                               internAtom("Couldn't write request"));
+        return 0;
+    }
+    server->request = request->next;
+    request->next = NULL;
+    if(server->request == NULL)
+        server->request_last = NULL;
+    httpQueueRequest(connection, request);
+    connection->pipelined = 1;
+    request->time0 = current_time;
+    connection->reqoffset = 0;
+    connection->bodylen = client->bodylen;
+    httpServerDoSide(connection);
+    return 1;
+}
+
+int 
+httpServerDoSide(HTTPConnectionPtr connection)
+{
+    HTTPRequestPtr request = connection->request;
+    HTTPRequestPtr requestor = request->request;
+    HTTPConnectionPtr client = requestor->connection;
+    int len = MIN(client->reqlen - client->reqbegin,
+                  connection->bodylen - connection->reqoffset);
+    int doflush = 
+        len > 0 &&
+        (len >= maxSideBuffering ||
+         client->reqbegin > 0 ||
+         (connection->reqoffset + client->reqlen - client->reqbegin) >=
+         connection->bodylen);
+    int done = connection->reqoffset >= connection->bodylen;
+
+    assert(connection->bodylen >= 0);
+
+    httpSetTimeout(connection, 60);
+
+    if(connection->reqlen > 0) {
+        /* Send the headers, but don't send any part of the body if
+           we're in wait_continue. */
+        do_stream_2(IO_WRITE,
+                    connection->fd, 0,
+                    connection->reqbuf, connection->reqlen,
+                    client->reqbuf + client->reqbegin, 
+                    (request->flags & REQUEST_WAIT_CONTINUE) ? 0 : len,
+                    httpServerSideHandler2, connection);
+        httpServerReply(connection, 0);
+    } else if(request->object->flags & OBJECT_ABORTED) {
+        if(connection->reqbuf)
+            dispose_chunk(connection->reqbuf);
+        connection->reqbuf = NULL;
+        connection->reqlen = 0;
+        pokeFdEvent(connection->fd, -ESHUTDOWN, POLLIN);
+        if(client->flags & CONN_READER) {
+            client->flags |= CONN_SIDE_READER;
+            do_stream(IO_READ | IO_IMMEDIATE | IO_NOTNOW,
+                      client->fd, 0, NULL, 0,
+                      httpClientSideHandler, client);
+        }
+    } else if(!(request->flags & REQUEST_WAIT_CONTINUE) && doflush) {
+        /* Make sure there's a reqbuf, as httpServerFinish uses
+           it to determine if there's a writer. */
+        if(connection->reqbuf == NULL)
+            connection->reqbuf = get_chunk();
+        assert(connection->reqbuf != NULL);
+        do_stream(IO_WRITE,
+                  connection->fd, 0,
+                  client->reqbuf + client->reqbegin, len,
+                  httpServerSideHandler, connection);
+    } else {
+        if(connection->reqbuf) {
+            httpConnectionDestroyReqbuf(connection);
+            connection->reqlen = 0;
+        }
+        if(request->flags & REQUEST_WAIT_CONTINUE) {
+            ConditionHandlerPtr chandler;
+            do_log(D_SERVER_CONN, "W... %s:%d.\n",
+                   scrub(connection->server->name), connection->server->port);
+            chandler = 
+                conditionWait(&request->object->condition,
+                              httpServerContinueConditionHandler,
+                              sizeof(connection), &connection);
+            if(chandler)
+                return 1;
+            else
+                do_log(L_ERROR, "Couldn't register condition handler.\n");
+            /* Fall through -- the client side will clean up. */
+        }
+        client->flags |= CONN_SIDE_READER;
+        do_stream(IO_READ | (done ? IO_IMMEDIATE : 0 ) | IO_NOTNOW,
+                  client->fd, client->reqlen,
+                  client->reqbuf, CHUNK_SIZE,
+                  httpClientSideHandler, client);
+    }
+    return 1;
+}
+
+static int
+httpClientDelayedDoSideHandler(TimeEventHandlerPtr event)
+{
+    HTTPConnectionPtr connection = *(HTTPConnectionPtr*)event->data;
+    httpServerDoSide(connection);
+    return 1;
+}
+
+static int
+httpServerDelayedDoSide(HTTPConnectionPtr connection)
+{
+    TimeEventHandlerPtr handler;
+    handler = scheduleTimeEvent(0, httpClientDelayedDoSideHandler,
+                                sizeof(connection), &connection);
+    if(!handler) {
+        do_log(L_ERROR, "Couldn't schedule DoSide -- freeing memory.\n");
+        free_chunk_arenas();
+        handler = scheduleTimeEvent(0, httpClientDelayedDoSideHandler,
+                                    sizeof(connection), &connection);
+        if(!handler)
+            do_log(L_ERROR, "Couldn't schedule DoSide.\n");
+        /* Somebody will hopefully end up timing out. */
+        return 1;
+    }
+    return 1;
+}
+
+static int
+httpServerSideHandlerCommon(int kind, int status,
+                            FdEventHandlerPtr event,
+                            StreamRequestPtr srequest)
+{
+    HTTPConnectionPtr connection = srequest->data;
+    HTTPRequestPtr request = connection->request;
+    HTTPRequestPtr requestor = request->request;
+    HTTPConnectionPtr client = requestor->connection;
+    int bodylen;
+
+    assert(request->object->flags & OBJECT_INPROGRESS);
+
+    if(status) {
+        do_log_error(L_ERROR, -status, "Couldn't write to server");
+        httpConnectionDestroyReqbuf(connection);
+        if(status != -ECLIENTRESET)
+            shutdown(connection->fd, 2);
+        abortObject(request->object, 502,
+                    internAtom("Couldn't write to server"));
+        /* Let the read side handle the error */
+        httpServerDoSide(connection);
+        return 1;
+    }
+
+    assert(srequest->offset > 0);
+
+    if(kind == 2) {
+        if(srequest->offset < connection->reqlen)
+            return 0;
+        bodylen = srequest->offset - connection->reqlen;
+        connection->reqlen = 0;
+        httpConnectionDestroyReqbuf(connection);
+    } else {
+        bodylen = srequest->offset;
+    }
+
+
+    assert(client->reqbegin + bodylen <= client->reqlen);
+
+    if(client->reqlen > client->reqbegin + bodylen)
+        memmove(client->reqbuf, client->reqbuf + client->reqbegin + bodylen,
+                client->reqlen - client->reqbegin - bodylen);
+    client->reqlen -= bodylen + client->reqbegin;
+    client->reqbegin = 0;
+    connection->reqoffset += bodylen;
+    httpServerDoSide(connection);
+    return 1;
+}
+
+int
+httpServerSideHandler(int status,
+                      FdEventHandlerPtr event,
+                      StreamRequestPtr srequest)
+{
+    return httpServerSideHandlerCommon(1, status, event, srequest);
+}
+
+int
+httpServerSideHandler2(int status,
+                       FdEventHandlerPtr event,
+                       StreamRequestPtr srequest)
+{
+    return httpServerSideHandlerCommon(2, status, event, srequest);
+}
+
+static int
+httpServerContinueConditionHandler(int status, ConditionHandlerPtr chandler)
+{
+    HTTPConnectionPtr connection = *(HTTPConnectionPtr*)chandler->data;
+
+    if(connection->request->flags & REQUEST_WAIT_CONTINUE)
+        return 0;
+    httpServerDelayedDoSide(connection);
+    return 1;
+}
+
+/* s is 0 to keep the connection alive, 1 to shutdown the connection */
+void
+httpServerFinish(HTTPConnectionPtr connection, int s, int offset)
+{
+    HTTPServerPtr server = connection->server;
+    HTTPRequestPtr request = connection->request;
+    int i;
+
+    if(request) {
+        assert(connection->pipelined >= 1);
+        assert((connection->pipelined > 1) == (request->next != NULL));
+    } else {
+        assert(connection->pipelined == 0);
+    }
+
+    if(!s && (!connection->request ||
+                  !(connection->request->flags & REQUEST_PERSISTENT)))
+        s = 1;
+
+    if(connection->serviced >= maxConnectionRequests ||
+       connection->time < current_time.tv_sec - maxConnectionAge)
+        s = 1;
+
+    if(connection->reqbuf) {
+        /* As most normal requests go out in a single packet, this is
+           extremely unlikely to happen.  As for POST/PUT requests,
+           they are not pipelined, so this can only happen if the
+           server sent an error reply early. */
+        assert(connection->fd >= 0);
+        shutdown(connection->fd, 1);
+        pokeFdEvent(connection->fd, -EDOSHUTDOWN, POLLOUT);
+        httpServerDelayedFinish(connection);
+        goto done;
+    }
+
+    if(request) {
+        /* Update statistics about the server */
+        int size = -1, d = -1, rtt = -1, rate = -1;
+        if(connection->offset > 0 && request->from >= 0)
+            size = connection->offset - request->from;
+        if(request->time1.tv_sec != null_time.tv_sec) {
+            d = timeval_minus_usec(&current_time, &request->time1);
+            if(!(request->flags & REQUEST_PIPELINED) &&
+               request->time0.tv_sec != null_time.tv_sec)
+                rtt = timeval_minus_usec(&request->time1, &request->time0);
+            if(size >= 8192 && d > 50000)
+                rate = ((double)size / (double)d) * 1000000.0 + 0.5;
+        }
+        request->time0 = null_time;
+        request->time1 = null_time;
+
+        if(rtt >= 0) {
+            if(server->rtt >= 0)
+                server->rtt = (3 * server->rtt + rtt + 2) / 4;
+            else
+                server->rtt = rtt;
+        }
+        if(rate >= 0) {
+            if(server->rate >= 0)
+                server->rate = (3 * server->rate + rate + 2) / 4;
+            else
+                server->rate = rate;
+        }
+
+        httpDequeueRequest(connection);
+        connection->pipelined--;
+        request->object->flags &= ~(OBJECT_INPROGRESS | OBJECT_VALIDATING);
+        if(request->request) {
+            request->request->request = NULL;
+            request->request = NULL;
+        }
+        releaseNotifyObject(request->object);
+        request->object = NULL;
+        httpDestroyRequest(request);
+    }
+
+    do_log(D_SERVER_CONN, "Done with server %s:%d connection (%d)\n",
+           scrub(connection->server->name), connection->server->port, s);
+
+    assert(offset <= connection->len);
+
+    if(!s) {
+        if(offset < connection->len) {
+            assert(connection->buf != NULL);
+            if(!connection->pipelined) {
+                do_log(L_WARN, 
+                       "Closing connection to %s:%d: "
+                       "%d stray bytes of data.\n",
+                       scrub(server->name), server->port,
+                       connection->len - offset);
+                s = 1;
+            } else {
+                memmove(connection->buf, connection->buf + offset,
+                        connection->len - offset);
+                connection->len = connection->len - offset;
+                if((connection->flags & CONN_BIGBUF) &&
+                   connection->len <= CHUNK_SIZE)
+                    httpConnectionUnbigify(connection);
+            }
+        } else {
+            connection->len = 0;
+        }
+    }
+
+    connection->server->time = current_time.tv_sec;
+    connection->serviced++;
+
+    if(s) {
+        if(connection->timeout)
+            cancelTimeEvent(connection->timeout);
+        connection->timeout = NULL;
+        httpConnectionDestroyBuf(connection);
+        if(connection->fd >= 0)
+            CLOSE(connection->fd);
+        connection->fd = -1;
+        server->persistent -= 1;
+        if(server->persistent < -5)
+            server->numslots = MIN(server->maxslots, serverMaxSlots);
+        if(connection->request) {
+            HTTPRequestPtr req;
+            do_log(D_SERVER_CONN, "Restarting pipeline to %s:%d.\n",
+                   scrub(server->name), server->port);
+            if(server->pipeline == 2)
+                server->pipeline -= 20;
+            else
+                server->pipeline -= 5;
+            req = connection->request;
+            while(req) {
+                req->connection = NULL;
+                req = req->next;
+            }
+            if(server->request)
+                connection->request_last->next = server->request;
+            else
+                server->request_last = connection->request_last;
+            server->request = connection->request;
+            connection->request = NULL;
+            connection->request_last = NULL;
+        }
+        /* Make sure we don't get confused into thinking a probe
+           is in progress. */
+        if(server->pipeline == 2 || server->pipeline == 3)
+            server->pipeline = 1;
+        for(i = 0; i < server->maxslots; i++)
+            if(connection == server->connection[i])
+                break;
+        assert(i < server->maxslots);
+        if(server->idleHandler[i])
+            unregisterFdEvent(server->idleHandler[i]);
+        server->idleHandler[i] = NULL;
+        server->connection[i] = NULL;
+        free(connection);
+    } else {
+        server->persistent += 1;
+        if(server->persistent > 0)
+            server->numslots = MIN(server->maxslots,
+                                   server->version == HTTP_10 ?
+                                   serverSlots1 : serverSlots);
+        httpSetTimeout(connection, serverTimeout);
+        /* See httpServerTrigger */
+        if(connection->pipelined ||
+           (server->version == HTTP_11 && server->pipeline <= 0) ||
+           (server->pipeline == 3)) {
+            server->pipeline++;
+        }
+        if(connection->pipelined) {
+            httpServerReply(connection, 1);
+        } else {
+            httpConnectionDestroyBuf(connection);
+        }
+    }
+
+ done:
+    httpServerTrigger(server);
+}
+
+static int
+httpServerDelayedFinishHandler(TimeEventHandlerPtr event)
+{
+    HTTPConnectionPtr connection = *(HTTPConnectionPtr*)event->data;
+    httpServerFinish(connection, 1, 0);
+    return 1;
+}
+
+static void
+httpServerDelayedFinish(HTTPConnectionPtr connection)
+{
+    TimeEventHandlerPtr handler;
+
+    handler = scheduleTimeEvent(1, httpServerDelayedFinishHandler,
+                                sizeof(connection), &connection);
+    if(!handler) {
+        do_log(L_ERROR,
+               "Couldn't schedule delayed finish -- freeing memory.");
+        free_chunk_arenas();
+        handler = scheduleTimeEvent(1, httpServerDelayedFinishHandler,
+                                    sizeof(connection), &connection);
+        if(!handler) {
+            do_log(L_ERROR,
+                   "Couldn't schedule delayed finish -- aborting.\n");
+            polipoExit();
+        }
+    }
+}
+
+void
+httpServerReply(HTTPConnectionPtr connection, int immediate)
+{
+    assert(connection->pipelined > 0);
+
+    if(connection->request->request == NULL) {
+        do_log(L_WARN, "Aborting pipeline on %s:%d.\n",
+               scrub(connection->server->name), connection->server->port);
+        httpServerFinish(connection, 1, 0);
+        return;
+    }
+
+    do_log(D_SERVER_CONN, "R: %s (%d)\n",
+           scrub(connection->request->object->key),
+           connection->request->method);
+
+    if(connection->len == 0)
+        httpConnectionDestroyBuf(connection);
+
+    httpSetTimeout(connection, serverTimeout);
+    do_stream_buf(IO_READ | (immediate ? IO_IMMEDIATE : 0) | IO_NOTNOW,
+                  connection->fd, connection->len,
+                  &connection->buf, CHUNK_SIZE,
+                  httpServerReplyHandler, connection);
+}
+
+int
+httpConnectionPipelined(HTTPConnectionPtr connection)
+{
+    HTTPRequestPtr request = connection->request;
+    int i = 0;
+    while(request) {
+        i++;
+        request = request->next;
+    }
+    return i;
+}
+
+void
+httpServerUnpipeline(HTTPRequestPtr request)
+{
+    HTTPConnectionPtr connection = request->connection;
+    HTTPServerPtr server = connection->server;
+
+    request->flags &= ~REQUEST_PERSISTENT;
+    if(request->next) {
+        HTTPRequestPtr req;
+        do_log(L_WARN,
+               "Restarting pipeline to %s:%d.\n", 
+               scrub(connection->server->name), connection->server->port);
+        req = request->next;
+        while(req) {
+            req->connection = NULL;
+            req = req->next;
+        }
+        if(server->request)
+            connection->request_last->next = server->request;
+        else
+            server->request_last = connection->request_last;
+        server->request = request->next;
+        request->next = NULL;
+        connection->request_last = request;
+    }
+    connection->pipelined = httpConnectionPipelined(connection);
+}
+
+void
+httpServerRestart(HTTPConnectionPtr connection)
+{
+    HTTPServerPtr server = connection->server;
+    HTTPRequestPtr request = connection->request;
+
+    if(request) {
+        HTTPRequestPtr req;
+        if(request->next)
+            do_log(L_WARN,
+                   "Restarting pipeline to %s:%d.\n", 
+                   scrub(connection->server->name), connection->server->port);
+        req = request;
+        while(req) {
+            req->connection = NULL;
+            req = req->next;
+        }
+        if(server->request)
+            connection->request_last->next = server->request;
+        else
+            server->request_last = connection->request_last;
+        server->request = request;
+        connection->request = NULL;
+        connection->request_last = NULL;
+    }
+    connection->pipelined = 0;
+    httpServerFinish(connection, 1, 0);
+}
+
+int
+httpServerRequest(ObjectPtr object, int method, int from, int to, 
+                  HTTPRequestPtr requestor, void *closure)
+{
+    int rc;
+    char name[132];
+    int port;
+    int x, y, z;
+
+    assert(from >= 0 && (to < 0 || to > from));
+    assert(closure == NULL);
+    assert(!(object->flags & OBJECT_LOCAL));
+    assert(object->type == OBJECT_HTTP);
+
+    if(object->flags & OBJECT_INPROGRESS)
+        return 1;
+
+    if(requestor->flags & REQUEST_REQUESTED)
+        return 0;
+
+    assert(requestor->request == NULL);
+
+    if(proxyOffline)
+        return -1;
+
+    rc = parseUrl(object->key, object->key_size, &x, &y, &port, &z);
+    
+    if(rc < 0 || x < 0 || y < 0 || y - x > 131) {
+        do_log(L_ERROR, "Couldn't parse URL %s\n", scrub(object->key));
+        abortObject(object, 400, internAtom("Couldn't parse URL"));
+        notifyObject(object);
+        return 1;
+    }
+
+    if(!intListMember(port, allowedPorts)) {
+        do_log(L_ERROR, "Attempted connection to port %d.\n", port);
+        abortObject(object, 403, internAtom("Forbidden port"));
+        notifyObject(object);
+        return 1;
+    }
+
+    memcpy(name, ((char*)object->key) + x, y - x);
+    name[y - x] = '\0';
+
+    requestor->flags |= REQUEST_REQUESTED;
+    rc = httpMakeServerRequest(name, port, object, method, from, to,
+                               requestor);
+                                   
+    if(rc < 0) {
+        abortObject(object, 
+                    503, internAtom("Couldn't schedule server request"));
+        notifyObject(object);
+        return 1;
+    }
+
+    return 1;
+}
+
+int
+httpWriteRequest(HTTPConnectionPtr connection, HTTPRequestPtr request,
+                 int bodylen)
+{
+    ObjectPtr object = request->object;
+    int from = request->from, to = request->to, method = request->method;
+    char *url = object->key, *m;
+    int url_size = object->key_size;
+    int x, y, port, z, location_size;
+    char *location;
+    int l, n, rc, bufsize;
+
+    assert(method != METHOD_NONE);
+
+    if(request->method == METHOD_GET || 
+       request->method == METHOD_CONDITIONAL_GET) {
+        if(to >= 0) {
+            assert(to >= from);
+            if(to == from) {
+                do_log(L_ERROR, "Requesting empty segment?\n");
+                return -1;
+            }
+        }
+
+        if(object->flags & OBJECT_DYNAMIC) {
+            from = 0;
+            to = -1;
+        } else {
+            objectFillFromDisk(object, from / CHUNK_SIZE * CHUNK_SIZE, 1);
+            l = objectHoleSize(request->object, from);
+            if(l > 0) {
+                if(to <= 0 || to > from + l)
+                    to = from + l;
+            }
+
+            if(pmmSize && connection->server->pipeline >= 4) {
+                if(from == 0)
+                    to = to < 0 ? pmmFirstSize : MIN(to, pmmFirstSize);
+                else
+                    to = to < 0 ? from + pmmSize : MIN(to, from + pmmSize);
+            }
+
+            if(from % CHUNK_SIZE != 0)
+                if(objectHoleSize(object, from - 1) != 0)
+                    from = from / CHUNK_SIZE * CHUNK_SIZE;
+        }
+    }
+
+    rc = parseUrl(url, url_size, &x, &y, &port, &z);
+
+    if(rc < 0 || x < 0 || y < 0) {
+        return -1;
+    }
+
+    if(connection->reqbuf == NULL) {
+        connection->reqbuf = get_chunk();
+        if(connection->reqbuf == NULL)
+            return -1;
+        connection->reqlen = 0;
+    }
+
+    if(method == METHOD_CONDITIONAL_GET &&
+       object->last_modified < 0 && object->etag == NULL)
+        method = request->method = METHOD_GET;
+
+ again:
+    bufsize = 
+        (connection->flags & CONN_BIGREQBUF) ? bigBufferSize : CHUNK_SIZE;
+    n = connection->reqlen;
+    switch(method) {
+    case METHOD_GET:
+    case METHOD_CONDITIONAL_GET: m = "GET"; break;
+    case METHOD_HEAD: m = "HEAD"; break;
+    case METHOD_POST: m = "POST"; break;
+    case METHOD_PUT: m = "PUT"; break;
+    case METHOD_OPTIONS: m = "OPTIONS"; break;
+    case METHOD_DELETE: m = "DELETE"; break;
+    default: abort();
+    }
+    n = snnprintf(connection->reqbuf, n, bufsize, "%s ", m);
+
+    if(connection->server->isProxy) {
+        n = snnprint_n(connection->reqbuf, n, bufsize,
+                       url, url_size);
+    } else {
+        if(url_size - z == 0) {
+            location = "/";
+            location_size = 1;
+        } else {
+            location = url + z;
+            location_size = url_size - z;
+        }
+        
+        n = snnprint_n(connection->reqbuf, n, bufsize, 
+                       location, location_size);
+    }
+    
+    do_log(D_SERVER_REQ, "Server request: ");
+    do_log_n(D_SERVER_REQ, url + x, y - x);
+    do_log(D_SERVER_REQ, ": ");
+    do_log_n(D_SERVER_REQ, connection->reqbuf, n);
+    do_log(D_SERVER_REQ, " (method %d from %d to %d, 0x%lx for 0x%lx)\n",
+           method, from, to,
+           (unsigned long)connection, (unsigned long)object);
+
+    n = snnprintf(connection->reqbuf, n, bufsize, " HTTP/1.1");
+
+    n = snnprintf(connection->reqbuf, n, bufsize, "\r\nHost: ");
+    n = snnprint_n(connection->reqbuf, n, bufsize, url + x, y - x);
+    if(port != 80)
+        n = snnprintf(connection->reqbuf, n, bufsize, ":%d", port);
+
+    if(connection->server->isProxy && parentAuthCredentials) {
+        n = buildServerAuthHeaders(connection->reqbuf, n, bufsize,
+                                   parentAuthCredentials);
+    }
+
+    if(bodylen >= 0)
+        n = snnprintf(connection->reqbuf, n, bufsize,
+                      "\r\nContent-Length: %d", bodylen);
+
+    if(request->flags & REQUEST_WAIT_CONTINUE)
+        n = snnprintf(connection->reqbuf, n, bufsize,
+                      "\r\nExpect: 100-continue");
+
+    if(method != METHOD_HEAD && (from > 0 || to >= 0)) {
+        if(to >= 0) {
+            n = snnprintf(connection->reqbuf, n, bufsize,
+                          "\r\nRange: bytes=%d-%d", from, to - 1);
+        } else {
+            n = snnprintf(connection->reqbuf, n, bufsize,
+                          "\r\nRange: bytes=%d-", from);
+        }
+    }
+
+    if(method == METHOD_GET && object->etag && (from > 0 || to >= 0)) {
+        if(request->request && request->request->request == request &&
+           request->request->from == 0 && request->request->to == -1 &&
+           pmmSize == 0 && pmmFirstSize == 0)
+            n = snnprintf(connection->reqbuf, n, bufsize,
+                          "\r\nIf-Range: \"%s\"", object->etag);
+    }
+
+    if(method == METHOD_CONDITIONAL_GET) {
+        if(object->last_modified >= 0) {
+            n = snnprintf(connection->reqbuf, n, bufsize,
+                          "\r\nIf-Modified-Since: ");
+            n = format_time(connection->reqbuf, n, bufsize,
+                            object->last_modified);
+        }
+        if(object->etag) {
+            n = snnprintf(connection->reqbuf, n, bufsize,
+                          "\r\nIf-None-Match: \"%s\"", object->etag);
+        }
+    }
+
+    n = httpPrintCacheControl(connection->reqbuf, n, bufsize,
+                              alwaysAddNoTransform ? CACHE_NO_TRANSFORM : 0,
+			      &request->cache_control);
+    if(n < 0)
+        goto fail;
+
+    if(request->request && request->request->headers) {
+        n = snnprint_n(connection->reqbuf, n, bufsize,
+                       request->request->headers->string, 
+                       request->request->headers->length);
+    }
+    if(!disableVia) {
+        if(request->request && request->request->via) {
+            n = snnprintf(connection->reqbuf, n, bufsize,
+                          "\r\nVia: %s, 1.1 %s",
+                          request->request->via->string, proxyName->string);
+        } else {
+            n = snnprintf(connection->reqbuf, n, bufsize,
+                          "\r\nVia: 1.1 %s",
+                          proxyName->string);
+        }
+    }
+
+    n = snnprintf(connection->reqbuf, n, bufsize,
+                  "\r\nConnection: %s\r\n\r\n",
+                  (request->flags & REQUEST_PERSISTENT) ? 
+                  "keep-alive" : "close");
+    if(n < 0 || n >= bufsize - 1)
+        goto fail;
+    connection->reqlen = n;
+    return n;
+
+ fail:
+    rc = 0;
+    if(!(connection->flags & CONN_BIGREQBUF))
+        rc = httpConnectionBigifyReqbuf(connection);
+    if(rc == 1)
+        goto again;
+    return -1;
+}
+
+int
+httpServerHandler(int status,
+                  FdEventHandlerPtr event,
+                  StreamRequestPtr srequest)
+{
+    HTTPConnectionPtr connection = srequest->data;
+
+    assert(connection->request->object->flags & OBJECT_INPROGRESS);
+
+    if(connection->reqlen == 0) {
+        do_log(D_SERVER_REQ, "Writing aborted on 0x%lx\n",
+               (unsigned long)connection);
+        goto fail;
+    }
+
+    if(status == 0 && !streamRequestDone(srequest)) {
+        httpSetTimeout(connection, serverTimeout);
+        return 0;
+    }
+
+    httpConnectionDestroyReqbuf(connection);
+
+    if(status) {
+        if(connection->serviced >= 1) {
+            httpServerRestart(connection);
+            return 1;
+        }
+        if(status < 0 && status != -ECONNRESET && status != -EPIPE)
+            do_log_error(L_ERROR, -status,
+                         "Couldn't send request to server");
+        goto fail;
+    }
+
+    return 1;
+
+ fail:
+    httpConnectionDestroyReqbuf(connection);
+    shutdown(connection->fd, 2);
+    pokeFdEvent(connection->fd, -EDOSHUTDOWN, POLLIN);
+    httpSetTimeout(connection, 60);
+    return 1;
+}
+
+int
+httpServerSendRequest(HTTPConnectionPtr connection)
+{
+    assert(connection->server);
+
+    if(connection->reqlen == 0) {
+        do_log(D_SERVER_REQ, 
+               "Writing aborted on 0x%lx\n", (unsigned long)connection);
+        httpConnectionDestroyReqbuf(connection);
+        shutdown(connection->fd, 2);
+        pokeFdEvent(connection->fd, -EDOSHUTDOWN, POLLIN | POLLOUT);
+        return -1;
+    }
+
+    httpSetTimeout(connection, serverTimeout);
+    do_stream(IO_WRITE, connection->fd, 0,
+              connection->reqbuf, connection->reqlen,
+              httpServerHandler, connection);
+    return 1;
+}
+
+int
+httpServerReplyHandler(int status,
+                       FdEventHandlerPtr event, 
+                       StreamRequestPtr srequest)
+{
+    HTTPConnectionPtr connection = srequest->data;
+    HTTPRequestPtr request = connection->request;
+    int i, body;
+    int bufsize = 
+        (connection->flags & CONN_BIGBUF) ? bigBufferSize : CHUNK_SIZE;
+
+    assert(request->object->flags & OBJECT_INPROGRESS);
+    if(status < 0) {
+        if(connection->serviced >= 1) {
+            httpServerRestart(connection);
+            return 1;
+        }
+        if(status != -ECLIENTRESET)
+            do_log_error(L_ERROR, -status, "Read from server failed");
+        httpServerAbort(connection, status != -ECLIENTRESET, 502, 
+                        internAtomError(-status, "Read from server failed"));
+        return 1;
+    }
+
+    i = findEndOfHeaders(connection->buf, 0, srequest->offset, &body);
+    connection->len = srequest->offset;
+
+    if(i >= 0) {
+        request->time1 = current_time;
+        return httpServerHandlerHeaders(status, event, srequest, connection);
+    }
+
+    if(status) {
+        if(connection->serviced >= 1) {
+            httpServerRestart(connection);
+            return 1;
+        }
+        if(status < 0) {
+            do_log(L_ERROR, 
+                   "Error reading server headers: %d\n", -status);
+            httpServerAbort(connection, status != -ECLIENTRESET, 502, 
+                            internAtomError(-status, 
+                                            "Error reading server headers"));
+        } else
+            httpServerAbort(connection, 1, 502, 
+                            internAtom("Server dropped connection"));
+        return 1;
+    }
+
+    if(connection->len >= bufsize) {
+        int rc = 0;
+        if(!(connection->flags & CONN_BIGBUF))
+            rc = httpConnectionBigify(connection);
+        if(rc == 0) {
+            do_log(L_ERROR, "Couldn't find end of server's headers.\n");
+            httpServerAbort(connection, 1, 502,
+                            internAtom("Couldn't find end "
+                                       "of server's headers"));
+            return 1;
+        } else if(rc < 0) {
+            do_log(L_ERROR, "Couldn't allocate big buffer.\n");
+            httpServerAbort(connection, 1, 500,
+                            internAtom("Couldn't allocate big buffer"));
+            return 1;
+        }
+        /* Can't just return 0 -- buf has moved. */
+        do_stream(IO_READ,
+                  connection->fd, connection->len,
+                  connection->buf, bigBufferSize,
+                  httpServerReplyHandler, connection);
+        return 1;
+    }
+
+    return 0;
+}
+
+int
+httpServerHandlerHeaders(int eof,
+                         FdEventHandlerPtr event,
+                         StreamRequestPtr srequest, 
+                         HTTPConnectionPtr connection)
+{
+    HTTPRequestPtr request = connection->request;
+    ObjectPtr object = request->object;
+    int rc;
+    int code, version;
+    int full_len;
+    AtomPtr headers;
+    int len;
+    int te;
+    CacheControlRec cache_control;
+    int age = -1;
+    time_t date, last_modified, expires;
+    struct timeval *init_time;
+    char *etag;
+    AtomPtr via, new_via;
+    int expect_body;
+    HTTPRangeRec content_range;
+    ObjectPtr new_object = NULL, old_object = NULL;
+    int supersede = 0;
+    AtomPtr message = NULL;
+    int suspectDynamic;
+    AtomPtr url = NULL;
+
+    assert(request->object->flags & OBJECT_INPROGRESS);
+    assert(eof >= 0);
+
+    httpSetTimeout(connection, -1);
+
+    if(request->flags & REQUEST_WAIT_CONTINUE) {
+        do_log(D_SERVER_CONN, "W   %s:%d.\n",
+               connection->server->name, connection->server->port);
+        request->flags &= ~REQUEST_WAIT_CONTINUE;
+    }
+
+    rc = httpParseServerFirstLine(connection->buf, &code, &version, &message);
+    if(rc <= 0) {
+        do_log(L_ERROR, "Couldn't parse server status line.\n");
+        httpServerAbort(connection, 1, 502,
+                        internAtom("Couldn't parse server status line"));
+        return 1;
+    }
+
+    do_log(D_SERVER_REQ, "Server status: ");
+    do_log_n(D_SERVER_REQ, connection->buf, 
+             connection->buf[rc - 1] == '\r' ? rc - 2 : rc - 2);
+    do_log(D_SERVER_REQ, " (0x%lx for 0x%lx)\n",
+           (unsigned long)connection, (unsigned long)object);
+
+    if(version != HTTP_10 && version != HTTP_11) {
+        do_log(L_ERROR, "Unknown server HTTP version\n");
+        httpServerAbort(connection, 1, 502,
+                        internAtom("Unknown server HTTP version"));
+        releaseAtom(message);
+        return 1;
+    } 
+
+    connection->version = version;
+    connection->server->version = version;
+    request->flags |= REQUEST_PERSISTENT;
+
+    url = internAtomN(object->key, object->key_size);    
+    rc = httpParseHeaders(0, url, connection->buf, rc, request,
+                          &headers, &len, &cache_control, NULL, &te,
+                          &date, &last_modified, &expires, NULL, NULL, NULL,
+                          &age, &etag, NULL, NULL, &content_range,
+                          NULL, &via, NULL);
+    if(rc < 0) {
+        do_log(L_ERROR, "Couldn't parse server headers\n");
+        releaseAtom(url);
+        releaseAtom(message);
+        httpServerAbort(connection, 1, 502, 
+                        internAtom("Couldn't parse server headers"));
+        return 1;
+    }
+
+    if(date < 0)
+        date = current_time.tv_sec;
+
+    if(code == 100) {
+        releaseAtom(url);
+        releaseAtom(message);
+        /* We've already reset wait_continue above, but we must still
+           ensure that the writer notices. */
+        notifyObject(request->object);
+        connection->len -= rc;
+        if(connection->len > 0)
+            memmove(connection->buf, connection->buf + rc, connection->len);
+        httpServerReply(connection, 1);
+        return 1;
+    }
+
+    if(code == 101) {
+        httpServerAbort(connection, 1, 501,
+                        internAtom("Upgrade not implemented"));
+        goto fail;
+    }
+
+    if(via && !checkVia(proxyName, via)) {
+        httpServerAbort(connection, 1, 504, internAtom("Proxy loop detected"));
+        goto fail;
+    }
+    full_len = content_range.full_length;
+
+    if(code == 206) {
+        if(content_range.from == -1 || content_range.to == -1) {
+            do_log(L_ERROR, "Partial content without range.\n");
+            httpServerAbort(connection, 1, 502,
+                            internAtom("Partial content without range"));
+            goto fail;
+        }
+        if(len >= 0 && len != content_range.to - content_range.from) {
+            do_log(L_ERROR, "Inconsistent partial content.\n");
+            httpServerAbort(connection, 1, 502,
+                            internAtom("Inconsistent partial content"));
+            goto fail;
+        }
+    } else if(code < 400 && 
+              (content_range.from >= 0 || content_range.to >= 0 || 
+               content_range.full_length >= 0)) {
+        do_log(L_WARN, "Range without partial content.\n");
+        /* Damn anakata. */
+        content_range.from = -1;
+        content_range.to = -1;
+        content_range.full_length = -1;
+    } else if(code != 304 && code != 412) {
+        full_len = len;
+    }
+
+    if(te != TE_IDENTITY && te != TE_CHUNKED) {
+        do_log(L_ERROR, "Unsupported transfer-encoding\n");
+        httpServerAbort(connection, 1, 502,
+                        internAtom("Unsupported transfer-encoding"));
+        goto fail;
+    }
+
+    if(code == 304) {
+        if(request->method != METHOD_CONDITIONAL_GET) {
+            do_log(L_ERROR, "Unexpected \"not changed\" reply from server\n");
+            httpServerAbort(connection, 1, 502,
+                            internAtom("Unexpected \"not changed\" "
+                                       "reply from server"));
+            goto fail;
+        }
+        if(object->etag && !etag) {
+            /* RFC 2616 10.3.5.  Violated by some front-end proxies. */
+            do_log(L_WARN, "\"Not changed\" reply with no ETag.\n");
+        } 
+    }
+
+    if(code == 412) {
+        if(request->method != METHOD_CONDITIONAL_GET ||
+           (!object->etag && !object->last_modified)) {
+            do_log(L_ERROR, 
+                   "Unexpected \"precondition failed\" reply from server.\n");
+            httpServerAbort(connection, 1, 502,
+                            internAtom("Unexpected \"precondition failed\" "
+                                       "reply from server"));
+            goto fail;
+        }
+    }
+
+    releaseAtom(url);
+
+    /* Okay, we're going to accept this reply. */
+
+    if((code == 200 || code == 206 || code == 304 || code == 412) &&
+       (cache_control.flags & (CACHE_NO | CACHE_NO_STORE) ||
+        cache_control.max_age == 0 ||
+        (cacheIsShared && cache_control.s_maxage == 0) ||
+        (expires >= 0 && expires <= object->age))) {
+        do_log(L_UNCACHEABLE, "Uncacheable object %s (%d)\n",
+               scrub(object->key), cache_control.flags);
+    }
+
+    if(request->time0.tv_sec != null_time.tv_sec)
+        init_time = &request->time0;
+    else
+        init_time = &current_time;
+    age = MIN(init_time->tv_sec - age, init_time->tv_sec);
+
+    if(request->method == METHOD_HEAD || 
+       code < 200 || code == 204 || code == 304)
+        expect_body = 0;
+    else if(te == TE_IDENTITY)
+        expect_body = (len != 0);
+    else
+        expect_body = 1;
+
+    connection->chunk_remaining = -1;
+    connection->te = te;
+
+    old_object = object;
+
+    connection->server->lies--;
+
+    if(object->cache_control & CACHE_MISMATCH)
+        supersede = 1;
+
+    if(code == 304 || code == 412) {
+        if((object->etag && etag && strcmp(object->etag, etag) != 0) ||
+           (object->last_modified >= 0 && last_modified >= 0 &&
+            object->last_modified != last_modified)) {
+            do_log(L_ERROR, "Inconsistent \"%s\" reply for %s\n",
+                   code == 304 ? "not changed":"precondition failed",
+                   scrub(object->key));
+            object->flags |= OBJECT_DYNAMIC;
+            supersede = 1;
+        }
+    } else if(!(object->flags & OBJECT_INITIAL)) {
+        if((object->last_modified < 0 || last_modified < 0) &&
+           (!object->etag || !etag))
+            supersede = 1;
+        else if(object->last_modified != last_modified)
+            supersede = 1;
+        else if(object->etag || etag) {
+            /* We need to be permissive here so as to deal with some
+               front-end proxies that discard ETags on partial
+               replies but not on full replies. */
+            if(etag && object->etag && strcmp(object->etag, etag) != 0)
+                supersede = 1;
+            else if(!object->etag)
+                supersede = 1;
+        }
+
+        if(!supersede && (object->cache_control & CACHE_VARY) &&
+           dontTrustVaryETag >= 1) {
+            /* Check content-type to work around mod_gzip bugs */
+            if(!httpHeaderMatch(atomContentType, object->headers, headers) ||
+               !httpHeaderMatch(atomContentEncoding, object->headers, headers))
+                supersede = 1;
+        }
+
+        if(full_len < 0 && te == TE_IDENTITY) {
+            /* It's an HTTP/1.0 CGI.  Be afraid. */
+            if(expect_body && content_range.from < 0 && content_range.to < 0)
+                supersede = 1;
+        }
+
+        if(!supersede && object->length >= 0 && full_len >= 0 &&
+                object->length != full_len) {
+            do_log(L_WARN, "Inconsistent length.\n");
+            supersede = 1;
+        }
+
+        if(!supersede &&
+           ((object->last_modified >= 0 && last_modified >= 0) ||
+            (object->etag && etag))) {
+            if(request->method == METHOD_CONDITIONAL_GET) {
+                do_log(L_WARN, "Server ignored conditional request.\n");
+                connection->server->lies += 10;
+                /* Drop the connection? */
+            }
+        }
+    } else if(code == 416) {
+        do_log(L_ERROR, "Unexpected \"range not satisfiable\" reply\n");
+        httpServerAbort(connection, 1, 502,
+                        internAtom("Unexpected \"range not satisfiable\" "
+                                   "reply"));
+        /* The object may be superseded.  Make sure the next request
+           won't be partial. */
+        abortObject(object, 502, 
+                    internAtom("Unexpected \"range not satisfiable\" reply"));
+        return 1;
+    }
+
+    if(object->flags & OBJECT_INITIAL)
+        supersede = 0;
+
+    if(supersede) {
+        do_log(L_SUPERSEDED,
+               "Superseding object %s (%d %d %d %s -> %d %d %d %s)\n",
+               scrub(old_object->key),
+               object->code, object->length, (int)object->last_modified,
+               object->etag ? object->etag : "(none)",
+               code, full_len, (int)last_modified,
+               etag ? etag : "(none)");
+        privatiseObject(old_object, 0);
+        new_object = makeObject(object->type, object->key, 
+                                object->key_size, 1, 0, 
+                                object->request, NULL);
+        if(new_object == NULL) {
+            do_log(L_ERROR, "Couldn't allocate object\n");
+            httpServerAbort(connection, 1, 500,
+                            internAtom("Couldn't allocate object"));
+            return 1;
+        }
+        if(urlIsLocal(new_object->key, new_object->key_size))
+            new_object->flags |= OBJECT_LOCAL;
+    } else {
+        new_object = object;
+    }
+
+    suspectDynamic =
+        (!etag && last_modified < 0) ||
+        (cache_control.flags &
+         (CACHE_NO_HIDDEN | CACHE_NO | CACHE_NO_STORE |
+          (cacheIsShared ? CACHE_PRIVATE : 0))) ||
+        (cache_control.max_age >= 0 && cache_control.max_age <= 2) ||
+        (cacheIsShared && 
+         cache_control.s_maxage >= 0 && cache_control.s_maxage <= 5) ||
+        (old_object->last_modified >= 0 && old_object->expires >= 0 && 
+         (old_object->expires - old_object->last_modified <= 1)) ||
+        (supersede && (old_object->date - date <= 5));
+
+    if(suspectDynamic)
+        new_object->flags |= OBJECT_DYNAMIC;
+    else if(!supersede)
+        new_object->flags &= ~OBJECT_DYNAMIC;
+    else if(old_object->flags & OBJECT_DYNAMIC)
+        new_object->flags |= OBJECT_DYNAMIC;
+
+    new_object->age = age;
+    new_object->cache_control |= cache_control.flags;
+    new_object->max_age = cache_control.max_age;
+    new_object->s_maxage = cache_control.s_maxage;
+    new_object->flags &= ~OBJECT_FAILED;
+
+    if(date >= 0)
+        new_object->date = date;
+    if(last_modified >= 0)
+        new_object->last_modified = last_modified;
+    if(expires >= 0)
+        new_object->expires = expires;
+    if(new_object->etag == NULL)
+        new_object->etag = etag;
+    else
+        free(etag);
+
+    switch(code) {
+    case 200:
+    case 300: case 301: case 302: case 303: case 307:
+    case 403: case 404: case 405: case 401:
+        if(new_object->message) releaseAtom(new_object->message);
+        new_object->code = code;
+        new_object->message = message;
+        break;
+    case 206: case 304: case 412:
+        if(new_object->code != 200 || !new_object->message) {
+            if(new_object->message) releaseAtom(new_object->message);
+            new_object->code = 200;
+            new_object->message = internAtom("OK");
+        }
+        releaseAtom(message);
+        break;
+    default:
+        if(new_object->message) releaseAtom(new_object->message);
+        new_object->code = code;
+        new_object->message = retainAtom(message);
+        break;
+    }
+
+    httpTweakCachability(new_object);
+
+    if(!via)
+        new_via = internAtomF("%s %s",
+                              version == HTTP_11 ? "1.1" : "1.0",
+                              proxyName->string);
+    else
+        new_via = internAtomF("%s, %s %s", via->string,
+                              version == HTTP_11 ? "1.1" : "1.0",
+                              proxyName->string);
+    if(new_via == NULL) {
+        do_log(L_ERROR, "Couldn't allocate Via.\n");
+    } else {
+        if(new_object->via) releaseAtom(new_object->via);
+        new_object->via = new_via;
+    }
+
+    if(new_object->flags & OBJECT_INITIAL) {
+        objectPartial(new_object, full_len, headers);
+    } else {
+        if(new_object->length < 0)
+            new_object->length = full_len;
+        /* XXX -- RFC 2616 13.5.3 */
+        releaseAtom(headers);
+    }
+
+    if(supersede) {
+        assert(new_object != old_object);
+        supersedeObject(old_object);
+    }
+
+    if(new_object != old_object) {
+        if(new_object->flags & OBJECT_INPROGRESS) {
+            /* Make sure we don't fetch this object two times at the
+               same time.  Just drop the connection. */
+            releaseObject(new_object);
+            httpServerFinish(connection, 1, 0);
+            return 1;
+        }
+        old_object->flags &= ~OBJECT_VALIDATING;
+        new_object->flags |= OBJECT_INPROGRESS;
+        /* Signal the client side to switch to the new object -- see
+           httpClientGetHandler.  If it doesn't, we'll give up on this
+           request below. */
+        new_object->flags |= OBJECT_MUTATING;
+        request->can_mutate = new_object;
+        notifyObject(old_object);
+        request->can_mutate = NULL;
+        new_object->flags &= ~OBJECT_MUTATING;
+        old_object->flags &= ~OBJECT_INPROGRESS;
+        if(request->object == old_object) {
+            if(request->request)
+                request->request->request = NULL;
+            request->request = NULL;
+            request->object = new_object;
+        } else {
+            assert(request->object == new_object);
+        }
+        releaseNotifyObject(old_object);
+        old_object = NULL;
+        object = new_object;
+    } else {
+        objectMetadataChanged(new_object, 0);
+    }
+
+    if(object->flags & OBJECT_VALIDATING) {
+        object->flags &= ~OBJECT_VALIDATING;
+        notifyObject(object);
+    }
+
+    if(!expect_body) {
+        httpServerFinish(connection, 0, rc);
+        return 1;
+    }
+
+    if(request->request == NULL) {
+        httpServerFinish(connection, 1, 0);
+        return 1;
+    }
+
+    if(code == 412) {
+        /* 412 replies contain a useless body.  For now, we
+           drop the connection. */
+        httpServerFinish(connection, 1, 0);
+        return 1;
+    }
+
+
+    if(request->flags & REQUEST_PERSISTENT) {
+        if(request->method != METHOD_HEAD && 
+           connection->te == TE_IDENTITY && len < 0) {
+            do_log(L_ERROR, "Persistent reply with no Content-Length\n");
+            /* That's potentially dangerous, as we could start reading
+               arbitrary data into the object.  Unfortunately, some
+               servers do that. */
+            request->flags &= ~REQUEST_PERSISTENT;
+        }
+    }
+
+    /* we're getting a body */
+    if(content_range.from > 0)
+        connection->offset = content_range.from;
+    else
+        connection->offset = 0;
+
+    if(content_range.to >= 0)
+        request->to = content_range.to;
+
+    do_log(D_SERVER_OFFSET, "0x%lx(0x%lx): offset = %d\n",
+           (unsigned long)connection, (unsigned long)object,
+           connection->offset);
+
+    if(connection->len > rc) {
+        rc = connectionAddData(connection, rc);
+        if(rc) {
+            if(rc < 0) {
+                if(rc == -2) {
+                    do_log(L_ERROR, "Couldn't parse chunk size.\n");
+                    httpServerAbort(connection, 1, 502,
+                                    internAtom("Couldn't parse chunk size"));
+                } else {
+                    do_log(L_ERROR, "Couldn't add data to connection.\n");
+                    httpServerAbort(connection, 1, 500, 
+                                    internAtom("Couldn't add data "
+                                               "to connection"));
+                }
+                return 1;
+            } else {
+                if(code != 206) {
+                    if(object->length < 0) {
+                        object->length = object->size;
+                        objectMetadataChanged(object, 0);
+                    } else if(object->length != object->size) {
+                        httpServerAbort(connection, 1, 500, 
+                                        internAtom("Inconsistent "
+                                                   "object size"));
+                        object->length = -1;
+                        return 1;
+                    }
+                }
+                httpServerFinish(connection, 0, 0);
+                return 1;
+            }
+        }
+    } else {
+        connection->len = 0;
+    }
+
+    if(eof) {
+        if(connection->te == TE_CHUNKED ||
+           (object->length >= 0 && 
+            connection->offset < object->length)) {
+            do_log(L_ERROR, "Server closed connection.\n");
+            httpServerAbort(connection, 1, 502,
+                            internAtom("Server closed connection"));
+            return 1;
+        } else {
+            if(code != 206 && eof > 0 && object->length < 0) {
+                object->length = object->size;
+                objectMetadataChanged(object, 0);
+            }
+            httpServerFinish(connection, 1, 0);
+            return 1;
+        }
+    } else {
+        return httpServerReadData(connection, 1);
+    }
+    return 0;
+
+ fail:
+    releaseAtom(url);
+    releaseAtom(message);
+    if(headers)
+        releaseAtom(headers);
+    if(etag)
+        free(etag);
+    if(via)
+        releaseAtom(via);
+    return 1;
+}
+
+int
+httpServerIndirectHandlerCommon(HTTPConnectionPtr connection, int eof)
+{
+    HTTPRequestPtr request = connection->request;
+
+    assert(eof >= 0);
+    assert(request->object->flags & OBJECT_INPROGRESS);
+
+    if(connection->len > 0) {
+        int rc;
+        rc = connectionAddData(connection, 0);
+        if(rc) {
+            if(rc < 0) {
+                if(rc == -2) {
+                    do_log(L_ERROR, "Couldn't parse chunk size.\n");
+                    httpServerAbort(connection, 1, 502,
+                                    internAtom("Couldn't parse chunk size"));
+                } else {
+                    do_log(L_ERROR, "Couldn't add data to connection.\n");
+                    httpServerAbort(connection, 1, 500,
+                                    internAtom("Couldn't add data "
+                                               "to connection"));
+                }
+                return 1;
+            } else {
+                if(request->to < 0) {
+                    if(request->object->length < 0) {
+                        request->object->length = request->object->size;
+                        objectMetadataChanged(request->object, 0);
+                    } else if(request->object->length != 
+                              request->object->size) {
+                        request->object->length = -1;
+                        httpServerAbort(connection, 1, 502,
+                                        internAtom("Inconsistent "
+                                                   "object size"));
+                        return 1;
+                    }
+                }
+                httpServerFinish(connection, 0, 0);
+            }
+            return 1;
+        }
+    }
+
+    if(eof && connection->len == 0) {
+        if(connection->te == TE_CHUNKED ||
+           (request->to >= 0 && connection->offset < request->to)) {
+            do_log(L_ERROR, "Server dropped connection.\n");
+            httpServerAbort(connection, 1, 502, 
+                            internAtom("Server dropped connection"));
+            return 1;
+        } else {
+            if(request->object->length < 0 && eof > 0 &&
+               (request->to < 0 || request->to > request->object->size)) {
+                request->object->length = request->object->size;
+                objectMetadataChanged(request->object, 0);
+            }
+            httpServerFinish(connection, 1, 0);
+            return 1;
+        }
+    } else {
+        return httpServerReadData(connection, 0);
+    }
+}
+
+int
+httpServerIndirectHandler(int status,
+                          FdEventHandlerPtr event, 
+                          StreamRequestPtr srequest)
+{
+    HTTPConnectionPtr connection = srequest->data;
+    assert(connection->request->object->flags & OBJECT_INPROGRESS);
+
+    httpSetTimeout(connection, -1);
+    if(status < 0) {
+        if(status != -ECLIENTRESET)
+            do_log_error(L_ERROR, -status, "Read from server failed");
+        httpServerAbort(connection, status != -ECLIENTRESET, 502,
+                        internAtomError(-status, "Read from server failed"));
+        return 1;
+    }
+
+    connection->len = srequest->offset;
+
+    return httpServerIndirectHandlerCommon(connection, status);
+}
+
+int
+httpServerReadData(HTTPConnectionPtr connection, int immediate)
+{
+    HTTPRequestPtr request = connection->request;
+    ObjectPtr object = request->object;
+    int to = -1;
+
+    assert(object->flags & OBJECT_INPROGRESS);
+
+    if(request->request == NULL) {
+        httpServerFinish(connection, 1, 0);
+        return 1;
+    }
+
+    if(request->to >= 0)
+        to = request->to;
+    else
+        to = object->length;
+
+    if(to >= 0 && to == connection->offset) {
+        httpServerFinish(connection, 0, 0);
+        return 1;
+    }
+
+    if(connection->len == 0 &&
+       ((connection->te == TE_IDENTITY && to > connection->offset) ||
+        (connection->te == TE_CHUNKED && connection->chunk_remaining > 0))) {
+        /* Read directly into the object */
+        int i = connection->offset / CHUNK_SIZE;
+        int j = connection->offset % CHUNK_SIZE;
+        int end, len, more;
+        /* See httpServerDirectHandlerCommon if you change this */
+        if(connection->te == TE_CHUNKED) {
+            len = connection->chunk_remaining;
+            /* The logic here is that we want more to just fit the
+               chunk header if we're doing a large read, but do a
+               large read if we would otherwise do a small one.  The
+               magic constant 2000 comes from the assumption that the
+               server uses chunks that have a size that are a power of
+               two (possibly including the chunk header), and that we
+               want a full ethernet packet to fit into our read. */
+            more = (len >= 2000 ? 20 : MIN(2048 - len, CHUNK_SIZE));
+        } else {
+            len = to - connection->offset;
+            /* We read more data only when there is a reasonable
+               chance of there being another reply coming. */
+            more = (connection->pipelined > 1) ? CHUNK_SIZE : 0;
+        }
+        end = len + connection->offset;
+
+        httpConnectionDestroyBuf(connection);
+
+        /* The order of allocation is important in case we run out of
+           memory. */
+        lockChunk(object, i);
+        if(object->chunks[i].data == NULL)
+            object->chunks[i].data = get_chunk();
+        if(object->chunks[i].data && object->chunks[i].size >= j) {
+            if(len + j > CHUNK_SIZE) {
+                lockChunk(object, i + 1);
+                if(object->chunks[i + 1].data == NULL)
+                    object->chunks[i + 1].data = get_chunk();
+                /* Unless we're grabbing all len of data, we do not
+                   want to do an indirect read immediately afterwards. */
+                if(more && len + j <= 2 * CHUNK_SIZE) {
+                    if(!connection->buf)
+                        connection->buf = get_chunk(); /* checked below */
+                }
+                if(object->chunks[i + 1].data) {
+                    do_stream_3(IO_READ | IO_NOTNOW, connection->fd, j,
+                                object->chunks[i].data, CHUNK_SIZE,
+                                object->chunks[i + 1].data,
+                                MIN(CHUNK_SIZE,
+                                    end - (i + 1) * CHUNK_SIZE),
+                                connection->buf, connection->buf ? more : 0,
+                                httpServerDirectHandler2, connection);
+                    return 1;
+                }
+                unlockChunk(object, i + 1);
+            }
+            if(more && len + j <= CHUNK_SIZE) {
+                if(!connection->buf)
+                    connection->buf = get_chunk();
+            }
+            do_stream_2(IO_READ | IO_NOTNOW, connection->fd, j,
+                        object->chunks[i].data,
+                        MIN(CHUNK_SIZE, end - i * CHUNK_SIZE),
+                        connection->buf, connection->buf ? more : 0,
+                        httpServerDirectHandler, connection);
+            return 1;
+        } else {
+            unlockChunk(object, i);
+        }
+    }
+       
+    if(connection->len == 0)
+        httpConnectionDestroyBuf(connection);
+
+    httpSetTimeout(connection, serverTimeout);
+    do_stream_buf(IO_READ | IO_NOTNOW |
+                  ((immediate && connection->len) ? IO_IMMEDIATE : 0),
+                  connection->fd, connection->len,
+                  &connection->buf,
+                  (connection->te == TE_CHUNKED ? 
+                   MIN(2048, CHUNK_SIZE) : CHUNK_SIZE),
+                  httpServerIndirectHandler, connection);
+    return 1;
+}
+
+int
+httpServerDirectHandlerCommon(int kind, int status,
+                              FdEventHandlerPtr event, 
+                              StreamRequestPtr srequest)
+{
+    HTTPConnectionPtr connection = srequest->data;
+    HTTPRequestPtr request = connection->request;
+    ObjectPtr object = request->object;
+    int i = connection->offset / CHUNK_SIZE;
+    int to, end, end1;
+
+    assert(request->object->flags & OBJECT_INPROGRESS);
+
+    httpSetTimeout(connection, -1);
+
+    if(status < 0) {
+        unlockChunk(object, i);
+        if(kind == 2) unlockChunk(object, i + 1);
+        if(status != -ECLIENTRESET)
+            do_log_error(L_ERROR, -status, "Read from server failed");
+        httpServerAbort(connection, status != -ECLIENTRESET, 502,
+                        internAtomError(-status, "Read from server failed"));
+        return 1;
+    }
+
+    /* We have incestuous knowledge of the decisions made in
+       httpServerReadData */
+    if(request->to >= 0)
+        to = request->to;
+    else
+        to = object->length;
+    if(connection->te == TE_CHUNKED)
+        end = connection->offset + connection->chunk_remaining;
+    else
+        end = to;
+    /* The amount of data actually read into the object */
+    end1 = MIN(end, i * CHUNK_SIZE + MIN(kind * CHUNK_SIZE, srequest->offset));
+
+    assert(end >= 0);
+    assert(end1 >= i * CHUNK_SIZE);
+    assert(end1 - 2 * CHUNK_SIZE <= i * CHUNK_SIZE);
+
+    object->chunks[i].size = 
+        MAX(object->chunks[i].size, MIN(end1 - i * CHUNK_SIZE, CHUNK_SIZE));
+    if(kind == 2 && end1 > (i + 1) * CHUNK_SIZE) {
+        object->chunks[i + 1].size =
+            MAX(object->chunks[i + 1].size, end1 - (i + 1) * CHUNK_SIZE);
+    }
+    if(connection->te == TE_CHUNKED) {
+        connection->chunk_remaining -= (end1 - connection->offset);
+        assert(connection->chunk_remaining >= 0);
+    }
+    connection->offset = end1;
+    object->size = MAX(object->size, end1);
+    unlockChunk(object, i);
+    if(kind == 2) unlockChunk(object, i + 1);
+
+    if(i * CHUNK_SIZE + srequest->offset > end1) {
+        connection->len = i * CHUNK_SIZE + srequest->offset - end1;
+        return httpServerIndirectHandlerCommon(connection, status);
+    } else {
+        notifyObject(object);
+        if(status) {
+            if(connection->te == TE_CHUNKED ||
+               (end >= 0 && connection->offset < end)) {
+                do_log(L_ERROR, "Server dropped connection.\n");
+                httpServerAbort(connection, 1, 502, 
+                                internAtom("Server dropped connection"));
+            } else
+                httpServerFinish(connection, 1, 0);
+            return 1;
+        } else {
+            return httpServerReadData(connection, 0);
+        }
+    }
+}
+
+int
+httpServerDirectHandler(int status,
+                        FdEventHandlerPtr event, 
+                        StreamRequestPtr srequest)
+{
+    return httpServerDirectHandlerCommon(1, status, event, srequest);
+}
+    
+int
+httpServerDirectHandler2(int status,
+                         FdEventHandlerPtr event, 
+                         StreamRequestPtr srequest)
+{
+    return httpServerDirectHandlerCommon(2, status, event, srequest);
+}
+
+/* Add the data accumulated in connection->buf into the object in
+   connection->request.  Returns 0 in the normal case, 1 if the TE is
+   self-terminating and we're done, -1 if there was a problem with
+   objectAddData, -2 if there was a problem with the data. */
+int
+connectionAddData(HTTPConnectionPtr connection, int skip)
+{
+    HTTPRequestPtr request = connection->request;
+    ObjectPtr object = request->object;
+    int rc;
+
+    if(connection->te == TE_IDENTITY) {
+        int len;
+        
+        len = connection->len - skip;
+        if(object->length >= 0) {
+            len = MIN(object->length - connection->offset, len);
+        }
+        if(request->to >= 0)
+            len = MIN(request->to - connection->offset, len);
+        if(len > 0) {
+            rc = objectAddData(object, connection->buf + skip,
+                               connection->offset, len);
+            if(rc < 0)
+                return -1;
+            connection->offset += len;
+            connection->len -= (len + skip);
+            do_log(D_SERVER_OFFSET, "0x%lx(0x%lx): offset = %d\n",
+                   (unsigned long)connection, (unsigned long)object,
+                   connection->offset);
+        }
+
+        if(connection->len > 0 && skip + len > 0) {
+            memmove(connection->buf,
+                    connection->buf + skip + len, connection->len);
+        }
+
+        if((object->length >= 0 && object->length <= connection->offset) ||
+           (request->to >= 0 && request->to <= connection->offset)) {
+            notifyObject(object);
+            return 1;
+        } else {
+            if(len > 0)
+                notifyObject(object);
+            return 0;
+        }
+    } else if(connection->te == TE_CHUNKED) {
+        int i = skip, j, size;
+        /* connection->chunk_remaining is 0 at the end of a chunk, -1
+           after the CR/LF pair ending a chunk, and -2 after we've
+           seen a chunk of length 0. */
+        if(connection->chunk_remaining > -2) {
+            while(1) {
+                if(connection->chunk_remaining <= 0) {
+                    if(connection->chunk_remaining == 0) {
+                        if(connection->len < i + 2)
+                            break;
+                        if(connection->buf[i] != '\r' ||
+                           connection->buf[i + 1] != '\n')
+                            return -1;
+                        i += 2;
+                        connection->chunk_remaining = -1;
+                    }
+                    if(connection->len < i + 2)
+                        break;
+                    j = parseChunkSize(connection->buf, i,
+                                       connection->len, &size);
+                    if(j < 0)
+                        return -2;
+                    if(j == 0)
+                        break;
+                    else
+                        i = j;
+                    if(size == 0) {
+                        connection->chunk_remaining = -2;
+                        break;
+                    } else {
+                        connection->chunk_remaining = size;
+                    }
+                } else {
+                    /* connection->chunk_remaining > 0 */
+                    size = MIN(connection->chunk_remaining,
+                               connection->len - i);
+                    if(size <= 0)
+                        break;
+                    rc = objectAddData(object, connection->buf + i,
+                                       connection->offset, size);
+                    connection->offset += size;
+                    if(rc < 0)
+                        return -1;
+                    i += size;
+                    connection->chunk_remaining -= size;
+                    do_log(D_SERVER_OFFSET, "0x%lx(0x%lx): offset = %d\n",
+                           (unsigned long)connection, 
+                           (unsigned long)object,
+                           connection->offset);
+                }
+            }
+        }
+        connection->len -= i;
+        if(connection->len > 0)
+            memmove(connection->buf, connection->buf + i, connection->len);
+        if(i > 0 || connection->chunk_remaining == -2)
+            notifyObject(object);
+        if(connection->chunk_remaining == -2)
+            return 1;
+        else
+            return 0;
+    } else {
+        abort();
+    }
+}
+
+void
+listServers(FILE *out)
+{
+    HTTPServerPtr server;
+    int i, n, m, entry;
+
+    fprintf(out, "<!DOCTYPE HTML PUBLIC "
+            "\"-//W3C//DTD HTML 4.01 Transitional//EN\" "
+            "\"http://www.w3.org/TR/html4/loose.dtd\">\n"
+            "<html><head>\n"
+            "\r\n<title>Known servers</title>\n"
+           "</head><body>\n"
+            "<h1>Known servers</h1>\n");
+
+    alternatingHttpStyle(out, "servers");
+    fprintf(out, "<table id=servers>\n");
+    fprintf(out, "<thead><tr><th>Server</th>"
+            "<th>Version</th>"
+            "<th>Persistent</th>"
+            "<th>Pipeline</th>"
+            "<th>Connections</th>"
+            "<th></th>"
+            "<th>rtt</th>"
+            "<th>rate</th>"
+            "</tr></thead>\n");
+    fprintf(out, "<tbody>\n");
+    server = servers;
+    entry = 0;
+    while(server) {
+        fprintf(out, "<tr class=\"%s\">", entry % 2 == 0 ? "even" : "odd");
+        if(server->port == 80)
+            fprintf(out, "<td>%s</td>", server->name);
+        else
+            fprintf(out, "<td>%s:%d</td>", server->name, server->port);
+
+        if(server->version == HTTP_11)
+            fprintf(out, "<td>1.1</td>");
+        else if(server->version == HTTP_10)
+            fprintf(out, "<td>1.0</td>");
+        else
+            fprintf(out, "<td>unknown</td>");
+
+        if(server->persistent < 0)
+            fprintf(out, "<td>no</td>");
+        else if(server->persistent > 0)
+            fprintf(out, "<td>yes</td>");
+        else
+            fprintf(out, "<td>unknown</td>");
+
+        if(server->version != HTTP_11 || server->persistent <= 0)
+            fprintf(out, "<td></td>");
+        else if(server->pipeline < 0)
+            fprintf(out, "<td>no</td>");
+        else if(server->pipeline >= 0 && server->pipeline <= 1)
+            fprintf(out, "<td>unknown</td>");
+        else if(server->pipeline == 2 || server->pipeline == 3)
+            fprintf(out, "<td>probing</td>");
+        else 
+            fprintf(out, "<td>yes</td>");
+
+        n = 0; m = 0;
+        for(i = 0; i < server->maxslots; i++)
+            if(server->connection[i] && !server->connection[i]->connecting) {
+                if(i < server->numslots)
+                    n++;
+                else
+                    m++;
+            }
+            
+        fprintf(out, "<td>%d/%d", n, server->numslots);
+        if(m)
+            fprintf(out, " + %d</td>", m);
+        else
+            fprintf(out, "</td>");
+
+        if(server->lies > 0)
+            fprintf(out, "<td>(%d lies)</td>", (server->lies + 9) / 10);
+        else
+            fprintf(out, "<td></td>");
+
+        if(server->rtt > 0)
+            fprintf(out, "<td>%.3f</td>", (double)server->rtt / 1000000.0);
+        else
+            fprintf(out, "<td></td>");
+        if(server->rate > 0)
+            fprintf(out, "<td>%d</td>", server->rate);
+        else
+            fprintf(out, "<td></td>");
+
+        fprintf(out, "</tr>\n");
+        server = server->next;
+        entry++;
+    }
+    fprintf(out, "</tbody>\n");
+    fprintf(out, "</table>\n");
+    fprintf(out, "<p><a href=\"/polipo/\">back</a></p>");
+    fprintf(out, "</body></html>\n");
+}

+ 113 - 0
polipo/server.h

@@ -0,0 +1,113 @@
+/*
+Copyright (c) 2003-2006 by Juliusz Chroboczek
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+extern int serverExpireTime, dontCacheRedirects;
+
+typedef struct _HTTPServer {
+    char *name;
+    int port;
+    int addrindex;
+    int isProxy;
+    int version;
+    int persistent;
+    int pipeline;
+    int lies;
+    int rtt;
+    int rate;
+    time_t time;
+    int numslots;
+    int maxslots;
+    HTTPConnectionPtr *connection;
+    FdEventHandlerPtr *idleHandler;
+    HTTPRequestPtr request, request_last;
+    struct _HTTPServer *next;
+} HTTPServerRec, *HTTPServerPtr;
+
+extern AtomPtr parentHost;
+extern int parentPort;
+
+void preinitServer(void);
+void initServer(void);
+
+void httpServerAbortHandler(ObjectPtr object);
+int httpMakeServerRequest(char *name, int port, ObjectPtr object, 
+                          int method, int from, int to,
+                          HTTPRequestPtr requestor);
+int httpServerQueueRequest(HTTPServerPtr server, HTTPRequestPtr request);
+int httpServerTrigger(HTTPServerPtr server);
+int httpServerSideRequest(HTTPServerPtr server);
+int  httpServerDoSide(HTTPConnectionPtr connection);
+int httpServerSideHandler(int status,
+                          FdEventHandlerPtr event,
+                          StreamRequestPtr srequest);
+int httpServerSideHandler2(int status,
+                           FdEventHandlerPtr event,
+                           StreamRequestPtr srequest);
+int httpServerConnectionDnsHandler(int status, 
+                                   GethostbynameRequestPtr request);
+int httpServerConnectionHandler(int status,
+                                FdEventHandlerPtr event,
+                                ConnectRequestPtr request);
+int httpServerSocksHandler(int status, SocksRequestPtr request);
+int httpServerConnectionHandlerCommon(int status,
+                                      HTTPConnectionPtr connection);
+void httpServerFinish(HTTPConnectionPtr connection, int s, int offset);
+
+void httpServerReply(HTTPConnectionPtr connection, int immediate);
+void httpServerAbort(HTTPConnectionPtr connection, int, int, struct _Atom *);
+void httpServerAbortRequest(HTTPRequestPtr request, int, int, struct _Atom *);
+void httpServerClientReset(HTTPRequestPtr request);
+void httpServerUnpipeline(HTTPRequestPtr request);
+int
+httpServerSendRequest(HTTPConnectionPtr connection);
+int
+httpServerHandler(int status, 
+                    FdEventHandlerPtr event,
+                    StreamRequestPtr request);
+int
+httpServerReplyHandler(int status,
+                       FdEventHandlerPtr event, 
+                       StreamRequestPtr request);
+int
+httpServerIndirectHandler(int status,
+                          FdEventHandlerPtr event, 
+                          StreamRequestPtr request);
+int
+httpServerDirectHandler(int status,
+                        FdEventHandlerPtr event, 
+                        StreamRequestPtr request);
+int
+httpServerDirectHandler2(int status,
+                         FdEventHandlerPtr event, 
+                         StreamRequestPtr request);
+int httpServerRequest(ObjectPtr object, int method, int from, int to,
+                      HTTPRequestPtr, void*);
+int httpServerHandlerHeaders(int eof,
+                             FdEventHandlerPtr event,
+                             StreamRequestPtr request, 
+                             HTTPConnectionPtr connection);
+int httpServerReadData(HTTPConnectionPtr, int);
+int connectionAddData(HTTPConnectionPtr connection, int skip);
+int 
+httpWriteRequest(HTTPConnectionPtr connection, HTTPRequestPtr request, int);
+
+void listServers(FILE*);

+ 605 - 0
polipo/socks.c

@@ -0,0 +1,605 @@
+/*
+Copyright (c) 2003-2006 by Juliusz Chroboczek
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+#include "polipo.h"
+
+#ifdef NO_SOCKS
+
+AtomPtr socksParentProxy = NULL;
+
+void
+preinitSocks()
+{
+    return;
+}
+
+void
+initSocks()
+{
+    return;
+}
+
+int
+do_socks_connect(char *name, int port,
+                 int (*handler)(int, SocksRequestPtr),
+                 void *data)
+{
+    SocksRequestRec request;
+    request.name = internAtomLowerN(name, strlen(name));
+    request.port = port;
+    request.handler = handler;
+    request.buf = NULL;
+    request.data = data;
+
+    handler(-ENOSYS, &request);
+    releaseAtom(request.name);
+    return 1;
+}
+
+#else
+
+int authed = -1;
+AtomPtr socksParentProxy = NULL;
+AtomPtr socksProxyHost = NULL;
+int socksProxyPort = -1;
+AtomPtr socksProxyAddress = NULL;
+int socksProxyAddressIndex = -1;
+AtomPtr socksUserName = NULL;
+AtomPtr socksPassWord = NULL;
+AtomPtr socksProxyType = NULL;
+AtomPtr aSocks4a, aSocks5;
+
+static int socksParentProxySetter(ConfigVariablePtr, void*);
+static int socksProxyTypeSetter(ConfigVariablePtr, void*);
+static int do_socks_connect_common(SocksRequestPtr);
+static int socksDnsHandler(int, GethostbynameRequestPtr);
+static int socksConnectHandler(int, FdEventHandlerPtr, ConnectRequestPtr);
+static int socksWriteHandler(int, FdEventHandlerPtr, StreamRequestPtr);
+static int socksReadHandler(int, FdEventHandlerPtr, StreamRequestPtr);
+static int socks5ReadHandler(int, FdEventHandlerPtr, StreamRequestPtr);
+static int socks5ReadHandlerAuth(int, FdEventHandlerPtr, StreamRequestPtr);
+static int socks5WriteHandler(int, FdEventHandlerPtr, StreamRequestPtr);
+static int socks5ReadHandler2(int, FdEventHandlerPtr, StreamRequestPtr);
+
+void
+preinitSocks()
+{
+    AtomPtr socksAuthCredentials = internAtom("");
+
+    aSocks4a = internAtom("socks4a");
+    aSocks5 = internAtom("socks5");
+    socksProxyType = retainAtom(aSocks5);
+    socksUserName = internAtom("");
+    socksPassWord = internAtom("");
+
+    CONFIG_VARIABLE_SETTABLE(socksParentProxy, CONFIG_ATOM_LOWER,
+                             socksParentProxySetter,
+                             "SOCKS parent proxy (host:port)");
+    CONFIG_VARIABLE_SETTABLE(socksAuthCredentials, CONFIG_PASSWORD,
+                             configAtomSetter,
+                             "SOCKS4a (or SOCKS5) credentials username:password");
+    CONFIG_VARIABLE_SETTABLE(socksProxyType, CONFIG_ATOM_LOWER,
+                             socksProxyTypeSetter,
+                             "One of socks4a or socks5");
+
+
+    // infer username and password from credentials
+    int rc = atomSplit(socksAuthCredentials, ':', &socksUserName, &socksPassWord);
+    if (rc < 0) {
+      do_log(L_ERROR, "Error splitting credentials");
+      exit(1);
+    } else if (rc == 0) {
+      // separator ':' not found
+      socksUserName = socksAuthCredentials;
+      releaseAtom(socksPassWord);
+      socksPassWord = NULL;
+    } else {
+      // split successfull: free memory
+      releaseAtom(socksAuthCredentials);
+    }
+}
+
+static int
+socksParentProxySetter(ConfigVariablePtr var, void *value)
+{
+    configAtomSetter(var, value);
+    initSocks();
+    return 1;
+}
+
+static int
+socksProxyTypeSetter(ConfigVariablePtr var, void *value)
+{
+    if(*var->value.a != aSocks4a && *var->value.a != aSocks5) {
+        do_log(L_ERROR, "Unknown socksProxyType %s\n", (*var->value.a)->string);
+        return -1;
+    }
+
+    return configAtomSetter(var, value);
+}
+
+void
+initSocks()
+{
+    int port = -1;
+    AtomPtr host = NULL, port_atom;
+    int rc;
+
+    if(socksParentProxy != NULL && socksParentProxy->length == 0) {
+        releaseAtom(socksParentProxy);
+        socksParentProxy = NULL;
+    }
+
+    if(socksParentProxy) {
+        rc = atomSplit(socksParentProxy, ':', &host, &port_atom);
+        if(rc <= 0) {
+            do_log(L_ERROR, "Couldn't parse socksParentProxy");
+            exit(1);
+        }
+        port = atoi(port_atom->string);
+        releaseAtom(port_atom);
+    }
+
+    if(socksProxyHost)
+        releaseAtom(socksProxyHost);
+    socksProxyHost = host;
+    socksProxyPort = port;
+    if(socksProxyAddress)
+        releaseAtom(socksProxyAddress);
+    socksProxyAddress = NULL;
+    socksProxyAddressIndex = -1;
+
+    if(socksProxyType != aSocks4a && socksProxyType != aSocks5) {
+        do_log(L_ERROR, "Unknown socksProxyType %s\n", socksProxyType->string);
+        exit(1);
+    }
+}
+
+static void
+destroySocksRequest(SocksRequestPtr request)
+{
+    releaseAtom(request->name);
+    if(request->buf)
+        free(request->buf);
+    free(request);
+}
+
+int
+do_socks_connect(char *name, int port,
+                 int (*handler)(int, SocksRequestPtr),
+                 void *data)
+{
+    SocksRequestPtr request = malloc(sizeof(SocksRequestRec));
+    SocksRequestRec request_nomem;
+    if(request == NULL)
+        goto nomem;
+
+    request->name = internAtomLowerN(name, strlen(name));
+    if(request->name == NULL) {
+        free(request);
+        goto nomem;
+    }
+
+    request->port = port;
+    request->fd = -1;
+    request->handler = handler;
+    request->buf = NULL;
+    request->data = data;
+
+    if(socksProxyAddress == NULL) {
+        do_gethostbyname(socksProxyHost->string, 0,
+                         socksDnsHandler,
+                         request);
+        return 1;
+    }
+
+    return do_socks_connect_common(request);
+
+ nomem:
+    request_nomem.name = internAtomLowerN(name, strlen(name));
+    request_nomem.port = port;
+    request_nomem.handler = handler;
+    request_nomem.buf = NULL;
+    request_nomem.data = data;
+
+    handler(-ENOMEM, &request_nomem);
+    releaseAtom(request_nomem.name);
+    return 1;
+}
+
+static int
+do_socks_connect_common(SocksRequestPtr request)
+{
+    assert(socksProxyAddressIndex >= 0);
+
+    do_connect(retainAtom(socksProxyAddress),
+               socksProxyAddressIndex,
+               socksProxyPort,
+               socksConnectHandler, request);
+    return 1;
+}
+
+static int
+socksDnsHandler(int status, GethostbynameRequestPtr grequest)
+{
+    SocksRequestPtr request = grequest->data;
+    if(status <= 0) {
+        request->handler(status, request);
+        destroySocksRequest(request);
+        return 1;
+    }
+
+    if(grequest->addr->string[0] == DNS_CNAME) {
+        if(grequest->count > 10) {
+            do_log(L_ERROR, "DNS CNAME loop.\n");
+            request->handler(-EDNS_CNAME_LOOP, request);
+            destroySocksRequest(request);
+            return 1;
+        }
+        do_gethostbyname(grequest->addr->string + 1, grequest->count + 1,
+                         socksDnsHandler, request);
+        return 1;
+    }
+
+
+    socksProxyAddress = retainAtom(grequest->addr);
+    socksProxyAddressIndex = 0;
+
+    do_socks_connect_common(request);
+    return 1;
+}
+
+static int
+socksConnectHandler(int status,
+                    FdEventHandlerPtr event,
+                    ConnectRequestPtr crequest)
+{
+    SocksRequestPtr request = crequest->data;
+    int rc;
+
+    if(status < 0) {
+        request->handler(status, request);
+        destroySocksRequest(request);
+        return 1;
+    }
+
+    assert(request->fd < 0);
+    request->fd = crequest->fd;
+    socksProxyAddressIndex = crequest->index;
+
+    rc = setNodelay(request->fd, 1);
+    if(rc < 0)
+        do_log_error(L_WARN, errno, "Couldn't disable Nagle's algorithm");
+
+    if(socksProxyType == aSocks4a) {
+        request->buf = malloc(8 +
+                              socksUserName->length + 1 +
+                              request->name->length + 1);
+        if(request->buf == NULL) {
+            CLOSE(request->fd);
+            request->fd = -1;
+            request->handler(-ENOMEM, request);
+            destroySocksRequest(request);
+            return 1;
+        }
+
+        request->buf[0] = 4;        /* VN */
+        request->buf[1] = 1;        /* CD = REQUEST */
+        request->buf[2] = (request->port >> 8) & 0xFF;
+        request->buf[3] = request->port & 0xFF;
+        request->buf[4] = request->buf[5] = request->buf[6] = 0;
+        request->buf[7] = 3;
+
+        memcpy(request->buf + 8, socksUserName->string, socksUserName->length);
+        request->buf[8 + socksUserName->length] = '\0';
+
+        memcpy(request->buf + 8 + socksUserName->length + 1,
+               request->name->string, request->name->length);
+        request->buf[8 + socksUserName->length + 1 + request->name->length] =
+            '\0';
+
+        do_stream(IO_WRITE, request->fd, 0, request->buf,
+                  8 + socksUserName->length + 1 + request->name->length + 1,
+                  socksWriteHandler, request);
+    } else if(socksProxyType == aSocks5) {
+        request->buf = malloc(8); /* 8 needed for the subsequent read */
+        if(request->buf == NULL) {
+            CLOSE(request->fd);
+            request->fd = -1;
+            request->handler(-ENOMEM, request);
+            destroySocksRequest(request);
+            return 1;
+        }
+        request->buf[0] = 5;             /* ver */
+        request->buf[1] = 1;             /* nmethods */
+	if (socksPassWord == NULL) {
+            request->buf[2] = 0;             /* no authentication required */
+	} else {
+            request->buf[2] = 2;             /* username/password */
+	}
+        do_stream(IO_WRITE, request->fd, 0, request->buf, 3,
+                  socksWriteHandler, request);
+    } else {
+        request->handler(-EUNKNOWN, request);
+    }
+    return 1;
+}
+
+static int
+socksWriteHandler(int status,
+                  FdEventHandlerPtr event,
+                  StreamRequestPtr srequest)
+{
+    SocksRequestPtr request = srequest->data;
+
+    if(status < 0)
+        goto error;
+
+    if(!streamRequestDone(srequest)) {
+        if(status) {
+            status = -ESOCKS_PROTOCOL;
+            goto error;
+        }
+        return 0;
+    }
+
+    int (*readHandler)(int, FdEventHandlerPtr, StreamRequestPtr) = NULL;
+    if (socksProxyType == aSocks4a){
+      readHandler = socksReadHandler;
+    } else if (socksPassWord == NULL) {
+      readHandler = socks5ReadHandler;
+    } else if (socksPassWord != NULL) {
+      readHandler = socks5ReadHandlerAuth;
+    }
+
+    do_stream(IO_READ | IO_NOTNOW, request->fd, 0, request->buf, 8, readHandler, request);
+    return 1;
+
+ error:
+    CLOSE(request->fd);
+    request->fd = -1;
+    request->handler(status, request);
+    destroySocksRequest(request);
+    return 1;
+}
+
+static int
+socksReadHandler(int status,
+                 FdEventHandlerPtr event,
+                 StreamRequestPtr srequest)
+{
+    SocksRequestPtr request = srequest->data;
+
+    if(status < 0)
+        goto error;
+
+    if(srequest->offset < 8) {
+        if(status) {
+            status = -ESOCKS_PROTOCOL;
+            goto error;
+        }
+        return 0;
+    }
+
+    if(request->buf[0] != 0 || request->buf[1] != 90) {
+        if(request->buf[1] >= 91 && request->buf[1] <= 93)
+            status = -(ESOCKS_PROTOCOL + request->buf[1] - 90);
+        else
+            status = -ESOCKS_PROTOCOL;
+        goto error;
+    }
+
+    request->handler(1, request);
+    destroySocksRequest(request);
+    return 1;
+
+ error:
+    CLOSE(request->fd);
+    request->fd = -1;
+    request->handler(status, request);
+    destroySocksRequest(request);
+    return 1;
+}
+
+static int
+socks5ReadHandlerAuth(int status,
+                  FdEventHandlerPtr event,
+                  StreamRequestPtr srequest)
+{
+    SocksRequestPtr request = srequest->data;
+
+    if(status < 0)
+        goto error;
+
+    if(srequest->offset < 2) {
+        if(status) {
+            status = -ESOCKS_PROTOCOL;
+            goto error;
+        }
+        return 0;
+    }
+
+    if(request->buf[0] != 5 || request->buf[1] != 2) {
+        status = -ESOCKS_PROTOCOL;
+        goto error;
+    }
+
+    free(request->buf);
+    request->buf = malloc(5 + socksUserName->length + socksPassWord->length);
+    if(request->buf == NULL) {
+        status = -ENOMEM;
+        goto error;
+    }
+
+    request->buf[0] = 1;	/* ver */
+    request->buf[1] = socksUserName->length;	/* username length */
+    memcpy(request->buf + 2, socksUserName->string, socksUserName->length);
+    request->buf[2 + socksUserName->length] = socksPassWord->length;	/* password length */
+    memcpy(request->buf + 3 + socksUserName->length, socksPassWord->string, socksPassWord->length);
+
+    do_stream(IO_WRITE, request->fd, 0,
+              request->buf, 3 + socksUserName->length + socksPassWord->length,
+              socks5WriteHandler, request);
+    return 1;
+
+ error:
+    CLOSE(request->fd);
+    request->fd = -1;
+    request->handler(status, request);
+    destroySocksRequest(request);
+    return 1;
+}
+
+
+static int
+socks5ReadHandler(int status,
+                  FdEventHandlerPtr event,
+                  StreamRequestPtr srequest)
+{
+    SocksRequestPtr request = srequest->data;
+
+    if(status < 0)
+        goto error;
+
+    if(srequest->offset < 2) {
+        if(status) {
+            status = -ESOCKS_PROTOCOL;
+            goto error;
+        }
+        return 0;
+    }
+
+    if(request->buf[1] != 0 ||
+       (socksPassWord != NULL && request->buf[0] != 1) || // user/pass: need ver 1
+       (socksPassWord == NULL && request->buf[0] != 5))   // no-auth:   need ver 5
+      {
+        status = -ESOCKS_PROTOCOL;
+        goto error;
+    }
+    authed = 1;
+    free(request->buf);
+    request->buf = malloc(5 + request->name->length + 2);
+    if(request->buf == NULL) {
+        status = -ENOMEM;
+        goto error;
+    }
+
+    request->buf[0] = 5;        /* ver */
+    request->buf[1] = 1;        /* cmd */
+    request->buf[2] = 0;        /* rsv */
+    request->buf[3] = 3;        /* atyp */
+    request->buf[4] = request->name->length;
+    memcpy(request->buf + 5, request->name->string, request->name->length);
+    request->buf[5 + request->name->length] = (request->port >> 8) & 0xFF;
+    request->buf[5 + request->name->length + 1] = request->port & 0xFF;
+
+    do_stream(IO_WRITE, request->fd, 0,
+              request->buf, 5 + request->name->length + 2,
+              socks5WriteHandler, request);
+    return 1;
+
+ error:
+    CLOSE(request->fd);
+    request->fd = -1;
+    request->handler(status, request);
+    destroySocksRequest(request);
+    return 1;
+}
+
+static int
+socks5WriteHandler(int status,
+                   FdEventHandlerPtr event,
+                   StreamRequestPtr srequest)
+{
+    SocksRequestPtr request = srequest->data;
+
+    if(status < 0)
+        goto error;
+
+    if(!streamRequestDone(srequest)) {
+        if(status) {
+            status = -ESOCKS_PROTOCOL;
+            goto error;
+        }
+        return 0;
+    }
+
+    do_stream(IO_READ | IO_NOTNOW, request->fd, 0, request->buf, 10,
+              ((socksPassWord != NULL) && (authed == -1) ? socks5ReadHandler : socks5ReadHandler2),
+	      request);
+    return 1;
+
+ error:
+    request->handler(status, request);
+    destroySocksRequest(request);
+    return 1;
+}
+
+static int
+socks5ReadHandler2(int status,
+                   FdEventHandlerPtr event,
+                   StreamRequestPtr srequest)
+{
+    SocksRequestPtr request = srequest->data;
+    authed = -1;
+    if(status < 0)
+        goto error;
+
+    if(srequest->offset < 4) {
+        if(status) {
+            status = -ESOCKS_PROTOCOL;
+            goto error;
+        }
+        return 0;
+    }
+
+    if(request->buf[0] != 5) {
+        status = -ESOCKS_PROTOCOL;
+        goto error;
+    }
+
+    if(request->buf[1] != 0) {
+        status = -(ESOCKS5_BASE + request->buf[1]);
+        goto error;
+    }
+
+    if(request->buf[3] != 1) {
+        status = -ESOCKS_PROTOCOL;
+        goto error;
+    }
+
+    if(srequest->offset < 10)
+        return 0;
+
+    request->handler(1, request);
+    destroySocksRequest(request);
+    return 1;
+
+ error:
+    CLOSE(request->fd);
+    request->fd = -1;
+    request->handler(status, request);
+    destroySocksRequest(request);
+    return 1;
+}
+
+#endif

+ 36 - 0
polipo/socks.h

@@ -0,0 +1,36 @@
+/*
+Copyright (c) 2003-2006 by Juliusz Chroboczek
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+extern AtomPtr socksParentProxy;
+
+typedef struct _SocksRequest {
+    AtomPtr name;
+    int port;
+    int fd;
+    int (*handler)(int, struct _SocksRequest*);
+    char *buf;
+    void *data;
+} SocksRequestRec, *SocksRequestPtr;
+
+void preinitSocks(void);
+void initSocks(void);
+int do_socks_connect(char*, int, int (*)(int, SocksRequestPtr), void*);

+ 574 - 0
polipo/tunnel.c

@@ -0,0 +1,574 @@
+/*
+Copyright (c) 2004-2006 by Juliusz Chroboczek
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+#include "polipo.h"
+
+#ifdef NO_TUNNEL
+
+void
+do_tunnel(int fd, char *buf, int offset, int len, AtomPtr url)
+{
+    int n;
+    assert(buf);
+
+    n = httpWriteErrorHeaders(buf, CHUNK_SIZE, 0, 1,
+                              501, internAtom("CONNECT not available "
+                                              "in this version."),
+                              1, NULL, url->string, url->length, NULL);
+    releaseAtom(url);
+    if(n >= 0) {
+        /* This is completely wrong.  The write is non-blocking, and we 
+           don't reschedule it if it fails.  But then, if the write
+           blocks, we'll simply drop the connection with no error message. */
+        write(fd, buf, n);
+    }
+    dispose_chunk(buf);
+    lingeringClose(fd);
+    return;
+}
+
+#else
+
+static void tunnelDispatch(TunnelPtr);
+static int tunnelRead1Handler(int, FdEventHandlerPtr, StreamRequestPtr);
+static int tunnelRead2Handler(int, FdEventHandlerPtr, StreamRequestPtr);
+static int tunnelWrite1Handler(int, FdEventHandlerPtr, StreamRequestPtr);
+static int tunnelWrite2Handler(int, FdEventHandlerPtr, StreamRequestPtr);
+static int tunnelDnsHandler(int, GethostbynameRequestPtr);
+static int tunnelConnectionHandler(int, FdEventHandlerPtr, ConnectRequestPtr);
+static int tunnelSocksHandler(int, SocksRequestPtr);
+static int tunnelHandlerCommon(int, TunnelPtr);
+static int tunnelError(TunnelPtr, int, AtomPtr);
+
+static int
+circularBufferFull(CircularBufferPtr buf)
+{
+    if(buf->head == buf->tail - 1)
+        return 1;
+    if(buf->head == CHUNK_SIZE - 1 && buf->tail == 0)
+        return 1;
+    return 0;
+}
+
+static int
+circularBufferEmpty(CircularBufferPtr buf)
+{
+     return buf->head == buf->tail;
+}
+
+static void
+logTunnel(TunnelPtr tunnel, int blocked)
+{
+    do_log(L_TUNNEL,"tunnel %s:%d %s\n", tunnel->hostname->string, tunnel->port,
+	   blocked ? "blocked" : "allowed");
+}
+
+static TunnelPtr
+makeTunnel(int fd, char *buf, int offset, int len)
+{
+    TunnelPtr tunnel;
+    assert(offset < CHUNK_SIZE);
+
+    tunnel = malloc(sizeof(TunnelRec));
+    if(tunnel == NULL)
+        return NULL;
+
+    tunnel->hostname = NULL;
+    tunnel->port = -1;
+    tunnel->flags = 0;
+    tunnel->fd1 = fd;
+    tunnel->fd2 = -1;
+    tunnel->buf1.buf = buf;
+    if(offset == len) {
+        tunnel->buf1.tail = 0;
+        tunnel->buf1.head = 0;
+    } else {
+        tunnel->buf1.tail = offset;
+        tunnel->buf1.head = len;
+    }
+    tunnel->buf2.buf = NULL;
+    tunnel->buf2.tail = 0;
+    tunnel->buf2.head = 0;
+    return tunnel;
+}
+
+static void
+destroyTunnel(TunnelPtr tunnel)
+{
+    assert(tunnel->fd1 < 0 && tunnel->fd2 < 0);
+    releaseAtom(tunnel->hostname);
+    if(tunnel->buf1.buf)
+        dispose_chunk(tunnel->buf1.buf);
+    if(tunnel->buf2.buf)
+        dispose_chunk(tunnel->buf2.buf);
+    free(tunnel);
+}
+
+void 
+do_tunnel(int fd, char *buf, int offset, int len, AtomPtr url)
+{
+    TunnelPtr tunnel;
+    int port;
+    char *p, *q;
+
+    tunnel = makeTunnel(fd, buf, offset, len);
+    if(tunnel == NULL) {
+        do_log(L_ERROR, "Couldn't allocate tunnel.\n");
+        releaseAtom(url);
+        dispose_chunk(buf);
+        CLOSE(fd);
+        return;
+    }
+
+    if(proxyOffline) {
+        do_log(L_INFO, "Attemted CONNECT when disconnected.\n");
+        releaseAtom(url);
+        tunnelError(tunnel, 502,
+                    internAtom("Cannot CONNECT when disconnected."));
+        return;
+    }
+
+    p = memrchr(url->string, ':', url->length);
+    q = NULL;
+    if(p)
+        port = strtol(p + 1, &q, 10);
+    if(!p || q != url->string + url->length) {
+        do_log(L_ERROR, "Couldn't parse CONNECT.\n");
+        releaseAtom(url);
+        tunnelError(tunnel, 400, internAtom("Couldn't parse CONNECT"));
+        return;
+    }
+    tunnel->hostname = internAtomLowerN(url->string, p - url->string);
+    if(tunnel->hostname == NULL) {
+        releaseAtom(url);
+        tunnelError(tunnel, 501, internAtom("Couldn't allocate hostname"));
+        return;
+    }
+
+    if(!intListMember(port, tunnelAllowedPorts)) {
+        releaseAtom(url);
+        tunnelError(tunnel, 403, internAtom("Forbidden port"));
+        return;
+    }
+    tunnel->port = port;
+    
+    if (tunnelIsMatched(url->string, url->length, 
+			tunnel->hostname->string, tunnel->hostname->length)) {
+        releaseAtom(url);
+        tunnelError(tunnel, 403, internAtom("Forbidden tunnel"));
+	logTunnel(tunnel,1);
+        return;
+    }
+    
+    logTunnel(tunnel,0);
+    
+    releaseAtom(url);
+
+    if(socksParentProxy)
+        do_socks_connect(parentHost ?
+                         parentHost->string : tunnel->hostname->string,
+                         parentHost ? parentPort : tunnel->port,
+                         tunnelSocksHandler, tunnel);
+    else
+        do_gethostbyname(parentHost ?
+                         parentHost->string : tunnel->hostname->string, 0,
+                         tunnelDnsHandler, tunnel);
+}
+
+static int
+tunnelDnsHandler(int status, GethostbynameRequestPtr request)
+{
+    TunnelPtr tunnel = request->data;
+
+    if(status <= 0) {
+        tunnelError(tunnel, 504,
+                    internAtomError(-status, 
+                                    "Host %s lookup failed",
+                                    atomString(tunnel->hostname)));
+        return 1;
+    }
+
+    if(request->addr->string[0] == DNS_CNAME) {
+        if(request->count > 10)
+            tunnelError(tunnel, 504, internAtom("CNAME loop"));
+        do_gethostbyname(request->addr->string + 1, request->count + 1,
+                         tunnelDnsHandler, tunnel);
+        return 1;
+    }
+
+    do_connect(retainAtom(request->addr), 0,
+               parentHost ? parentPort : tunnel->port,
+               tunnelConnectionHandler, tunnel);
+    return 1;
+}
+
+static int
+tunnelConnectionHandler(int status,
+                        FdEventHandlerPtr event,
+                        ConnectRequestPtr request)
+{
+    TunnelPtr tunnel = request->data;
+    int rc;
+
+    if(status < 0) {
+        tunnelError(tunnel, 504, internAtomError(-status, "Couldn't connect"));
+        return 1;
+    }
+
+    rc = setNodelay(request->fd, 1);
+    if(rc < 0)
+        do_log_error(L_WARN, errno, "Couldn't disable Nagle's algorithm");
+
+    return tunnelHandlerCommon(request->fd, tunnel);
+}
+
+static int
+tunnelSocksHandler(int status, SocksRequestPtr request)
+{
+    TunnelPtr tunnel = request->data;
+
+    if(status < 0) {
+        tunnelError(tunnel, 504, internAtomError(-status, "Couldn't connect"));
+        return 1;
+    }
+
+    return tunnelHandlerCommon(request->fd, tunnel);
+}
+
+static int
+tunnelHandlerParent(int fd, TunnelPtr tunnel)
+{
+    char *message;
+    int n;
+
+    if(tunnel->buf1.buf == NULL)
+        tunnel->buf1.buf = get_chunk();
+    if(tunnel->buf1.buf == NULL) {
+        message = "Couldn't allocate buffer";
+        goto fail;
+    }
+    if(tunnel->buf1.tail != tunnel->buf1.head) {
+        message = "Pipelined connect to parent proxy not implemented";
+        goto fail;
+    }
+
+    n = snnprintf(tunnel->buf1.buf, tunnel->buf1.tail, CHUNK_SIZE,
+                  "CONNECT %s:%d HTTP/1.1\r\nHost: %s",
+                  tunnel->hostname->string, tunnel->port, tunnel->hostname->string);
+    if (tunnel->port != 443)
+        n = snnprintf(tunnel->buf1.buf, n, CHUNK_SIZE, ":%d", tunnel->port);
+    if (parentAuthCredentials)
+        n = buildServerAuthHeaders(tunnel->buf1.buf, n, CHUNK_SIZE,
+                                   parentAuthCredentials);
+    n = snnprintf(tunnel->buf1.buf, n, CHUNK_SIZE, "\r\n\r\n");
+
+    if(n < 0) {
+        message = "Buffer overflow";
+        goto fail;
+    }
+    tunnel->buf1.head = n;
+    tunnelDispatch(tunnel);
+    return 1;
+
+ fail:
+    CLOSE(fd);
+    tunnel->fd2 = -1;
+    tunnelError(tunnel, 501, internAtom(message));
+    return 1;
+}
+
+static int
+tunnelHandlerCommon(int fd, TunnelPtr tunnel)
+{
+    const char *message = "HTTP/1.1 200 Tunnel established\r\n\r\n";
+
+    tunnel->fd2 = fd;
+
+    if(parentHost)
+        return tunnelHandlerParent(fd, tunnel);
+
+    if(tunnel->buf2.buf == NULL)
+        tunnel->buf2.buf = get_chunk();
+    if(tunnel->buf2.buf == NULL) {
+        CLOSE(fd);
+        tunnelError(tunnel, 501, internAtom("Couldn't allocate buffer"));
+        return 1;
+    }
+
+    memcpy(tunnel->buf2.buf, message, MIN(CHUNK_SIZE - 1, strlen(message)));
+    tunnel->buf2.head = MIN(CHUNK_SIZE - 1, strlen(message));
+
+    tunnelDispatch(tunnel);
+    return 1;
+}
+
+static void
+bufRead(int fd, CircularBufferPtr buf,
+        int (*handler)(int, FdEventHandlerPtr, StreamRequestPtr),
+        void *data)
+{
+    int tail;
+
+    if(buf->tail == 0)
+        tail = CHUNK_SIZE - 1;
+    else
+        tail = buf->tail - 1;
+
+    if(buf->head == 0)
+        do_stream_buf(IO_READ | IO_NOTNOW,
+                      fd, 0,
+                      &buf->buf, tail,
+                      handler, data);
+    else if(buf->tail > buf->head)
+        do_stream(IO_READ | IO_NOTNOW,
+                  fd, buf->head,
+                  buf->buf, tail,
+                  handler, data);
+    else 
+        do_stream_2(IO_READ | IO_NOTNOW,
+                    fd, buf->head,
+                    buf->buf, CHUNK_SIZE,
+                    buf->buf, tail,
+                    handler, data);
+}
+
+static void
+bufWrite(int fd, CircularBufferPtr buf,
+        int (*handler)(int, FdEventHandlerPtr, StreamRequestPtr),
+        void *data)
+{
+    if(buf->head > buf->tail)
+        do_stream(IO_WRITE,
+                  fd, buf->tail,
+                  buf->buf, buf->head,
+                  handler, data);
+    else
+        do_stream_2(IO_WRITE,
+                    fd, buf->tail,
+                    buf->buf, CHUNK_SIZE,
+                    buf->buf, buf->head,
+                    handler, data);
+}
+                    
+static void
+tunnelDispatch(TunnelPtr tunnel)
+{
+    if(circularBufferEmpty(&tunnel->buf1)) {
+        if(tunnel->buf1.buf && 
+           !(tunnel->flags & (TUNNEL_READER1 | TUNNEL_WRITER2))) {
+            dispose_chunk(tunnel->buf1.buf);
+            tunnel->buf1.buf = NULL;
+            tunnel->buf1.head = tunnel->buf1.tail = 0;
+        }
+    }
+
+    if(circularBufferEmpty(&tunnel->buf2)) {
+        if(tunnel->buf2.buf &&
+           !(tunnel->flags & (TUNNEL_READER2 | TUNNEL_WRITER1))) {
+            dispose_chunk(tunnel->buf2.buf);
+            tunnel->buf2.buf = NULL;
+            tunnel->buf2.head = tunnel->buf2.tail = 0;
+        }
+    }
+
+    if(tunnel->fd1 >= 0) {
+        if(!(tunnel->flags & (TUNNEL_READER1 | TUNNEL_EOF1)) && 
+           !circularBufferFull(&tunnel->buf1)) {
+            tunnel->flags |= TUNNEL_READER1;
+            bufRead(tunnel->fd1, &tunnel->buf1, tunnelRead1Handler, tunnel);
+        }
+        if(!(tunnel->flags & (TUNNEL_WRITER1 | TUNNEL_EPIPE1)) &&
+           !circularBufferEmpty(&tunnel->buf2)) {
+            tunnel->flags |= TUNNEL_WRITER1;
+            /* There's no IO_NOTNOW in bufWrite, so it might close the
+               file descriptor straight away.  Wait until we're
+               rescheduled. */
+            bufWrite(tunnel->fd1, &tunnel->buf2, tunnelWrite1Handler, tunnel);
+            return;
+        }
+        if(tunnel->fd2 < 0 || (tunnel->flags & TUNNEL_EOF2)) {
+            if(!(tunnel->flags & TUNNEL_EPIPE1))
+                shutdown(tunnel->fd1, 1);
+            tunnel->flags |= TUNNEL_EPIPE1;
+        } else if(tunnel->fd1 < 0 || (tunnel->flags & TUNNEL_EPIPE2)) {
+            if(!(tunnel->flags & TUNNEL_EOF1))
+                shutdown(tunnel->fd1, 0);
+            tunnel->flags |= TUNNEL_EOF1;
+        }
+        if((tunnel->flags & TUNNEL_EOF1) && (tunnel->flags & TUNNEL_EPIPE1)) {
+            if(!(tunnel->flags & (TUNNEL_READER1 | TUNNEL_WRITER1))) {
+                CLOSE(tunnel->fd1);
+                tunnel->fd1 = -1;
+            }
+        }
+    }
+
+    if(tunnel->fd2 >= 0) {
+        if(!(tunnel->flags & (TUNNEL_READER2 | TUNNEL_EOF2)) && 
+           !circularBufferFull(&tunnel->buf2)) {
+            tunnel->flags |= TUNNEL_READER2;
+            bufRead(tunnel->fd2, &tunnel->buf2, tunnelRead2Handler, tunnel);
+        }
+        if(!(tunnel->flags & (TUNNEL_WRITER2 | TUNNEL_EPIPE2)) &&
+           !circularBufferEmpty(&tunnel->buf1)) {
+            tunnel->flags |= TUNNEL_WRITER2;
+            bufWrite(tunnel->fd2, &tunnel->buf1, tunnelWrite2Handler, tunnel);
+            return;
+        }
+        if(tunnel->fd1 < 0 || (tunnel->flags & TUNNEL_EOF1)) {
+            if(!(tunnel->flags & TUNNEL_EPIPE2))
+                shutdown(tunnel->fd2, 1);
+            tunnel->flags |= TUNNEL_EPIPE2;
+        } else if(tunnel->fd1 < 0 || (tunnel->flags & TUNNEL_EPIPE1)) {
+            if(!(tunnel->flags & TUNNEL_EOF2))
+                shutdown(tunnel->fd2, 0);
+            tunnel->flags |= TUNNEL_EOF2;
+        }
+        if((tunnel->flags & TUNNEL_EOF2) && (tunnel->flags & TUNNEL_EPIPE2)) {
+            if(!(tunnel->flags & (TUNNEL_READER2 | TUNNEL_WRITER2))) {
+                CLOSE(tunnel->fd2);
+                tunnel->fd2 = -1;
+            }
+        }
+    }
+
+    if(tunnel->fd1 < 0 && tunnel->fd2 < 0)
+        destroyTunnel(tunnel);
+    else
+        assert(tunnel->flags & (TUNNEL_READER1 | TUNNEL_WRITER1 |
+                                TUNNEL_READER2 | TUNNEL_WRITER2));
+}
+
+static int
+tunnelRead1Handler(int status, 
+                   FdEventHandlerPtr event, StreamRequestPtr request)
+{
+    TunnelPtr tunnel = request->data;
+    if(status) {
+        if(status < 0 && status != -EPIPE && status != -ECONNRESET)
+            do_log_error(L_ERROR, -status, "Couldn't read from client");
+        tunnel->flags |= TUNNEL_EOF1;
+        goto done;
+    }
+    tunnel->buf1.head = request->offset % CHUNK_SIZE;
+ done:
+    /* Keep buffer empty to avoid a deadlock */
+    if((tunnel->flags & TUNNEL_EPIPE2))
+        tunnel->buf1.tail = tunnel->buf1.head;
+    tunnel->flags &= ~TUNNEL_READER1;
+    tunnelDispatch(tunnel);
+    return 1;
+}
+
+static int
+tunnelRead2Handler(int status, 
+                   FdEventHandlerPtr event, StreamRequestPtr request)
+{
+    TunnelPtr tunnel = request->data;
+    if(status) {
+        if(status < 0 && status != -EPIPE && status != -ECONNRESET)
+            do_log_error(L_ERROR, -status, "Couldn't read from server");
+        tunnel->flags |= TUNNEL_EOF2;
+        goto done;
+    }
+    tunnel->buf2.head = request->offset % CHUNK_SIZE;
+done:
+    /* Keep buffer empty to avoid a deadlock */
+    if((tunnel->flags & TUNNEL_EPIPE1))
+        tunnel->buf2.tail = tunnel->buf2.head;
+    tunnel->flags &= ~TUNNEL_READER2;
+    tunnelDispatch(tunnel);
+    return 1;
+}
+
+static int
+tunnelWrite1Handler(int status,
+                   FdEventHandlerPtr event, StreamRequestPtr request)
+{
+    TunnelPtr tunnel = request->data;
+    if(status || (tunnel->flags & TUNNEL_EPIPE1)) {
+        tunnel->flags |= TUNNEL_EPIPE1;
+        if(status < 0 && status != -EPIPE)
+            do_log_error(L_ERROR, -status, "Couldn't write to client");
+        /* Empty the buffer to avoid a deadlock */
+        tunnel->buf2.tail = tunnel->buf2.head;
+        goto done;
+    }
+    tunnel->buf2.tail = request->offset % CHUNK_SIZE;
+ done:
+    tunnel->flags &= ~TUNNEL_WRITER1;
+    tunnelDispatch(tunnel);
+    return 1;
+}
+        
+static int
+tunnelWrite2Handler(int status,
+                   FdEventHandlerPtr event, StreamRequestPtr request)
+{
+    TunnelPtr tunnel = request->data;
+    if(status || (tunnel->flags & TUNNEL_EPIPE2)) {
+        tunnel->flags |= TUNNEL_EPIPE2;
+        if(status < 0 && status != -EPIPE)
+            do_log_error(L_ERROR, -status, "Couldn't write to server");
+        /* Empty the buffer to avoid a deadlock */
+        tunnel->buf1.tail = tunnel->buf1.head;
+        goto done;
+    }
+    tunnel->buf1.tail = request->offset % CHUNK_SIZE;
+ done:
+    tunnel->flags &= ~TUNNEL_WRITER2;
+    tunnelDispatch(tunnel);
+    return 1;
+}
+        
+static int
+tunnelError(TunnelPtr tunnel, int code, AtomPtr message)
+{
+    int n;
+    if(tunnel->fd2 > 0) {
+        CLOSE(tunnel->fd2);
+        tunnel->fd2 = -1;
+    }
+
+    if(tunnel->buf2.buf == NULL)
+        tunnel->buf2.buf = get_chunk();
+    if(tunnel->buf2.buf == NULL)
+        goto fail;
+
+    n = httpWriteErrorHeaders(tunnel->buf2.buf, CHUNK_SIZE - 1, 0,
+                              1, code, message, 1, NULL,
+                              NULL, 0, NULL);
+
+    if(n <= 0) goto fail;
+
+    tunnel->buf2.head = n;
+
+    tunnelDispatch(tunnel);
+    return 1;
+
+ fail:
+    CLOSE(tunnel->fd1);
+    tunnel->fd1 = -1;
+    tunnelDispatch(tunnel);
+    return 1;
+}
+#endif

+ 50 - 0
polipo/tunnel.h

@@ -0,0 +1,50 @@
+/*
+Copyright (c) 2004-2006 by Juliusz Chroboczek
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+typedef struct _CircularBuffer {
+    int head;
+    int tail;
+    char *buf;
+} CircularBufferRec, *CircularBufferPtr;
+
+#define TUNNEL_READER1 1
+#define TUNNEL_WRITER1 2
+#define TUNNEL_EOF1 4
+#define TUNNEL_EPIPE1 8
+#define TUNNEL_READER2 16
+#define TUNNEL_WRITER2 32
+#define TUNNEL_EOF2 64
+#define TUNNEL_EPIPE2 128
+
+typedef struct _Tunnel {
+    AtomPtr hostname;
+    int port;
+    int flags;
+    int fd1;
+    CircularBufferRec buf1;
+    int fd2;
+    CircularBufferRec buf2;
+} TunnelRec, *TunnelPtr;
+
+void do_tunnel(int fd, char *buf, int offset, int len, AtomPtr url);
+
+void listTunnels(FILE *out);

+ 838 - 0
polipo/util.c

@@ -0,0 +1,838 @@
+/*
+Copyright (c) 2003-2007 by Juliusz Chroboczek
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+#include "polipo.h"
+
+/* Note that this is different from GNU's strndup(3). */
+char *
+strdup_n(const char *restrict buf, int n)
+{
+    char *s;
+    s = malloc(n + 1);
+    if(s == NULL)
+        return NULL;
+    memcpy(s, buf, n);
+    s[n] = '\0';
+    return s;
+}
+
+int
+snnprintf(char *restrict buf, int n, int len, const char *format, ...)
+{
+    va_list args;
+    int rc;
+    va_start(args, format);
+    rc = snnvprintf(buf, n, len, format, args);
+    va_end(args);
+    return rc;
+}
+
+int
+snnvprintf(char *restrict buf, int n, int len, const char *format, va_list args)
+{
+    int rc = -1;
+    va_list args_copy;
+
+    if(n < 0) return -2;
+    if(n < len) {
+        va_copy(args_copy, args);
+        rc = vsnprintf(buf + n, len - n, format, args_copy);
+        va_end(args_copy);
+    }
+    if(rc >= 0 && n + rc <= len)
+        return n + rc;
+    else
+        return -1;
+}
+
+int
+snnprint_n(char *restrict buf, int n, int len, const char *s, int slen)
+{
+    int i = 0;
+    if(n < 0) return -2;
+    while(i < slen && n < len)
+        buf[n++] = s[i++];
+    if(n < len)
+        return n;
+    else
+        return -1;
+}
+
+int
+letter(char c)
+{
+    if(c >= 'A' && c <= 'Z') return 1;
+    if(c >= 'a' && c <= 'z') return 1;
+    return 0;
+}
+
+int
+digit(char c)
+{
+    if(c >= '0' && c <= '9')
+        return 1;
+    return 0;
+}
+
+char
+lwr(char a)
+{
+    if(a >= 'A' && a <= 'Z')
+        return a | 0x20;
+    else
+        return a;
+}
+
+char *
+lwrcpy(char *restrict dst, const char *restrict src, int n)
+{
+    int i;
+    for(i = 0; i < n; i++)
+        dst[i] = lwr(src[i]);
+    return dst;
+}
+
+int
+lwrcmp(const char *as, const char *bs, int n)
+{
+    int i;
+    for(i = 0; i < n; i++) {
+        char a = lwr(as[i]), b = lwr(bs[i]);
+        if(a < b)
+            return -1;
+        else if(a > b)
+            return 1;
+    }
+    return 0;
+}
+
+int
+strcasecmp_n(const char *string, const char *buf, int n)
+{
+    int i;
+    i = 0;
+    while(string[i] != '\0' && i < n) {
+        char a = lwr(string[i]), b = lwr(buf[i]);
+        if(a < b)
+            return -1;
+        else if(a > b)
+            return 1;
+        i++;
+    }
+    if(string[i] == '\0' && i == n)
+        return 0;
+    else if(i == n)
+        return 1;
+    else
+        return -1;
+}
+
+int
+atoi_n(const char *restrict string, int n, int len, int *value_return)
+{
+    int i = n;
+    int val = 0;
+
+    if(i >= len || !digit(string[i]))
+        return -1;
+
+    while(i < len && digit(string[i])) {
+        val = val * 10 + (string[i] - '0');
+        i++;
+    }
+    *value_return = val;
+    return i;
+}
+
+int 
+isWhitespace(const char *string)
+{
+    while(*string != '\0') {
+        if(*string == ' ' || *string == '\t')
+            string++;
+        else
+            return 0;
+    }
+    return 1;
+}
+
+#ifndef HAVE_MEMRCHR
+void *
+memrchr(const void *s, int c, size_t n)
+{
+    const unsigned char *ss = s;
+    unsigned char cc = c;
+    size_t i;
+    for(i = 1; i <= n; i++)
+        if(ss[n - i] == cc)
+            return (void*)(ss + n - i);
+    return NULL;
+}
+#endif
+
+int
+h2i(char h) 
+{
+    if(h >= '0' && h <= '9')
+        return h - '0';
+    else if(h >= 'a' && h <= 'f')
+        return h - 'a' + 10;
+    else if(h >= 'A' && h <= 'F')
+        return h - 'A' + 10;
+    else
+        return -1;
+}
+    
+char
+i2h(int i)
+{
+    if(i < 0 || i >= 16)
+        return '?';
+    if(i < 10)
+        return i + '0';
+    else
+        return i - 10 + 'A';
+}
+
+/* floor(log2(x)) */
+int
+log2_floor(int x) 
+{
+    int i, j;
+
+    assert(x > 0);
+
+    i = 0;
+    j = 1;
+    while(2 * j <= x) {
+        i++;
+        j *= 2;
+    }
+    return i;
+}
+
+/* ceil(log2(x)) */
+int
+log2_ceil(int x) 
+{
+    int i, j;
+
+    assert(x > 0);
+
+    i = 0;
+    j = 1;
+    while(j < x) {
+        i++;
+        j *= 2;
+    }
+    return i;
+}
+
+#ifdef HAVE_ASPRINTF
+char *
+vsprintf_a(const char *f, va_list args)
+{
+    char *r;
+    int rc;
+    va_list args_copy;
+
+    va_copy(args_copy, args);
+    rc = vasprintf(&r, f, args_copy);
+    va_end(args_copy);
+    if(rc < 0)
+        return NULL;
+    return r;
+    
+}
+
+#else
+
+char*
+vsprintf_a(const char *f, va_list args)
+{
+    int n, size;
+    char buf[64];
+    char *string;
+    va_list args_copy;
+
+    va_copy(args_copy, args);
+    n = vsnprintf(buf, 64, f, args_copy);
+    va_end(args_copy);
+    if(n >= 0 && n < 64) {
+        return strdup_n(buf, n);
+    }
+    if(n >= 64)
+        size = n + 1;
+    else
+        size = 96;
+
+    while(1) {
+        string = malloc(size);
+        if(!string)
+            return NULL;
+        va_copy(args_copy, args);
+        n = vsnprintf(string, size, f, args_copy);
+        va_end(args_copy);
+        if(n >= 0 && n < size) {
+            return string;
+        } else if(n >= size)
+            size = n + 1;
+        else
+            size = size * 3 / 2;
+        free(string);
+        if(size > 16 * 1024)
+            return NULL;
+    }
+    /* NOTREACHED */
+}
+#endif
+
+char*
+sprintf_a(const char *f, ...)
+{
+    char *s;
+    va_list args;
+    va_start(args, f);
+    s = vsprintf_a(f, args);
+    va_end(args);
+    return s;
+}    
+
+unsigned int
+hash(unsigned int seed, const void *restrict key, int key_size,
+     unsigned int hash_size)
+{
+    int i;
+    unsigned int h;
+
+    h = seed;
+    for(i = 0; i < key_size; i++)
+        h = (h << 5) + (h >> (hash_size - 5)) +
+            ((unsigned char*)key)[i];
+    return h & ((1 << hash_size) - 1);
+}
+
+char *
+pstrerror(int e)
+{
+    char *s;
+    static char buf[20];
+
+    switch(e) {
+    case EDOSHUTDOWN: s = "Immediate shutdown requested"; break;
+    case EDOGRACEFUL: s = "Graceful shutdown requested"; break;
+    case EDOTIMEOUT: s = "Timeout"; break;
+    case ECLIENTRESET: s = "Connection reset by client"; break;
+    case ESYNTAX: s = "Incorrect syntax"; break;
+    case EREDIRECTOR: s = "Redirector error"; break;
+    case EDNS_HOST_NOT_FOUND: s = "Host not found"; break;
+    case EDNS_NO_ADDRESS: s = "No address"; break;
+    case EDNS_NO_RECOVERY: s = "Permanent name server failure"; break;
+    case EDNS_TRY_AGAIN: s = "Temporary name server failure"; break;
+    case EDNS_INVALID: s = "Invalid reply from name server"; break;
+    case EDNS_UNSUPPORTED: s = "Unsupported DNS reply"; break;
+    case EDNS_FORMAT: s = "Invalid DNS query"; break;
+    case EDNS_REFUSED: s = "DNS query refused by server"; break;
+    case EDNS_CNAME_LOOP: s = "DNS CNAME loop"; break;
+#ifndef NO_SOCKS
+    case ESOCKS_PROTOCOL: s = "SOCKS protocol error"; break;
+    case ESOCKS_REJECT_FAIL: s = "SOCKS request rejected or failed"; break;
+    case ESOCKS_REJECT_IDENTD: s = "SOCKS request rejected: "
+                                   "server couldn't connect to identd";
+        break;
+    case ESOCKS_REJECT_UID_MISMATCH: s = "SOCKS request rejected: "
+                                         "uid mismatch";
+        break;
+    case ESOCKS5_BASE: s = "SOCKS success"; break;
+    case ESOCKS5_BASE + 1: s = "General SOCKS server failure"; break;
+    case ESOCKS5_BASE + 2: s = "SOCKS connection not allowed"; break;
+    case ESOCKS5_BASE + 3: s = "SOCKS error: network unreachable"; break;
+    case ESOCKS5_BASE + 4: s = "SOCKS error: host unreachable"; break;
+    case ESOCKS5_BASE + 5: s = "SOCKS error: connection refused"; break;
+    case ESOCKS5_BASE + 6: s = "SOCKS error: TTL expired"; break;
+    case ESOCKS5_BASE + 7: s = "SOCKS command not supported"; break;
+    case ESOCKS5_BASE + 8: s = "SOCKS error: address type not supported";
+        break;
+#endif
+    case EUNKNOWN: s = "Unknown error"; break;
+    default: s = NULL; break;
+    }
+    if(!s) s = strerror(e);
+#ifdef WIN32 /*MINGW*/
+    if(!s) {
+        if(e >= WSABASEERR && e <= WSABASEERR + 2000) {
+            /* This should be okay, as long as the caller discards the
+               pointer before another error occurs. */
+            snprintf(buf, 20, "Winsock error %d", e);
+            s = buf;
+        }
+    }
+#endif
+    if(!s) {
+        snprintf(buf, 20, "Unknown error %d", e);
+        s = buf;
+    }
+    return s;
+}
+
+/* Like mktime(3), but UTC rather than local time */
+#if defined(HAVE_TIMEGM)
+time_t
+mktime_gmt(struct tm *tm)
+{
+    return timegm(tm);
+}
+#elif defined(HAVE_MKGMTIME)
+time_t
+mktime_gmt(struct tm *tm)
+{
+    return _mkgmtime(tm);
+}
+#elif defined(HAVE_TM_GMTOFF)
+time_t
+mktime_gmt(struct tm *tm)
+{
+    time_t t;
+    struct tm *ltm;
+
+    t = mktime(tm);
+    if(t < 0)
+        return -1;
+    ltm = localtime(&t);
+    if(ltm == NULL)
+        return -1;
+    return t + ltm->tm_gmtoff;
+}
+#elif defined(HAVE_TZSET)
+#ifdef HAVE_SETENV
+/* Taken from the Linux timegm(3) man page. */
+time_t
+mktime_gmt(struct tm *tm)
+{
+    time_t t;
+    char *tz;
+
+    tz = getenv("TZ");
+    setenv("TZ", "GMT", 1);
+    tzset();
+    t = mktime(tm);
+    if(tz)
+        setenv("TZ", tz, 1);
+    else
+        unsetenv("TZ");
+    tzset();
+    return t;
+}
+#else
+time_t
+mktime_gmt(struct tm *tm)
+{
+    time_t t;
+    char *tz;
+    static char *old_tz = NULL;
+
+    tz = getenv("TZ");
+    putenv("TZ=GMT");
+    tzset();
+    t = mktime(tm);
+    if(old_tz)
+        free(old_tz);
+    if(tz)
+        old_tz = sprintf_a("TZ=%s", tz);
+    else
+        old_tz = strdup("TZ");  /* XXX - non-portable? */
+    if(old_tz)
+        putenv(old_tz);
+    tzset();
+    return t;
+}
+#endif
+#else
+#error no mktime_gmt implementation on this platform
+#endif
+
+
+AtomPtr
+expandTilde(AtomPtr filename)
+{
+    char *buf;
+    char *home;
+    int len;
+    AtomPtr ret;
+
+    if(filename == NULL || filename->length < 1 ||
+       filename->string[0] != '~' || filename->string[1] != '/')
+        return filename;
+    
+    home = getenv("HOME");
+    if(home == NULL) {
+        return NULL;
+    }
+    len = strlen(home);
+    buf = malloc(len + 1 + 1 + filename->length - 2);
+    if(buf == NULL) {
+        do_log(L_ERROR, "Could not allocate buffer.\n");
+        return NULL;
+    }
+
+    memcpy(buf, home, len);
+    if(buf[len - 1] != '/')
+        buf[len++] = '/';
+    memcpy(buf + len, filename->string + 2, filename->length - 2);
+    len += filename->length - 2;
+    ret = internAtomN(buf, len);
+    free(buf);
+    if(ret != NULL)
+        releaseAtom(filename);
+    return ret;
+}
+
+#ifdef HAVE_FORK
+void
+do_daemonise(int noclose)
+{
+    int rc;
+
+    fflush(stdout);
+    fflush(stderr);
+
+    rc = fork();
+    if(rc < 0) {
+        do_log_error(L_ERROR, errno, "Couldn't fork");
+        exit(1);
+    }
+
+    if(rc > 0)
+        exit(0);
+
+    if(!noclose) {
+        int fd;
+        close(0);
+        close(1);
+        close(2);
+        /* Leaving the default file descriptors free is not a good
+           idea, as it will cause library functions such as abort to
+           thrash the on-disk cache. */
+        fd = open("/dev/null", O_RDONLY);
+        if(fd > 0) {
+            dup2(fd, 0);
+            close(fd);
+        }
+        fd = open("/dev/null", O_WRONLY);
+        if(fd >= 0) {
+            if(fd != 1)
+                dup2(fd, 1);
+            if(fd != 2)
+                dup2(fd, 2);
+            if(fd != 1 && fd != 2)
+                close(fd);
+        }
+    }
+    rc = setsid();
+    if(rc < 0) {
+        do_log_error(L_ERROR, errno, "Couldn't create new session");
+        exit(1);
+    }
+}
+
+#elif defined(WIN32)
+
+void
+do_daemonise(int noclose)
+{
+	do_log(L_INFO, "Detaching console");
+	FreeConsole();
+}
+
+#else
+
+void
+do_daemonise(int noclose)
+{
+    do_log(L_ERROR, "Cannot daemonise on this platform");
+    exit(1);
+}
+#endif
+
+
+void
+writePid(char *pidfile)
+{
+    int fd, n, rc;
+    char buf[16];
+
+    fd = open(pidfile, O_WRONLY | O_CREAT | O_EXCL, 0666);
+    if(fd < 0) {
+        do_log_error(L_ERROR, errno, 
+                     "Couldn't create pid file %s", pidfile);
+        exit(1);
+    }
+    n = snprintf(buf, 16, "%ld\n", (long)getpid());
+    if(n < 0 || n >= 16) {
+        close(fd);
+        unlink(pidfile);
+        do_log(L_ERROR, "Couldn't format pid.\n");
+        exit(1);
+    }
+    rc = write(fd, buf, n);
+    if(rc != n) {
+        close(fd);
+        unlink(pidfile);
+        do_log_error(L_ERROR, errno, "Couldn't write pid");
+        exit(1);
+    }
+
+    close(fd);
+    return;
+}
+
+static const char b64[64] =
+"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+
+/* "/" replaced with "-" */
+static const char b64fss[64] =
+"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-";
+
+int
+b64cpy(char *restrict dst, const char *restrict src, int n, int fss)
+{
+    const char *b = fss ? b64fss: b64;
+    int i, j;
+
+    j = 0;
+    for(i = 0; i < n; i += 3) {
+        unsigned char a0, a1, a2;
+        a0 = src[i];
+        a1 = i < n - 1 ? src[i + 1] : 0;
+        a2 = i < n - 2 ? src[i + 2] : 0;
+        dst[j++] = b[(a0 >> 2) & 0x3F];
+        dst[j++] = b[((a0 << 4) & 0x30) | ((a1 >> 4) & 0x0F)];
+        if(i < n - 1)
+            dst[j++] = b[((a1 << 2) & 0x3C) | ((a2 >> 6) & 0x03)];
+        else
+            dst[j++] = '=';
+        if(i < n - 2)
+            dst[j++] = b[a2 & 0x3F];
+        else
+            dst[j++] = '=';
+    }
+    return j;
+}
+
+int
+b64cmp(const char *restrict a, int an, const char *restrict b, int bn)
+{
+    char *buf;
+    int r;
+
+    if(an % 4 != 0)
+        return -1;
+    if((bn + 2) / 3 != an / 4)
+        return -1;
+    buf = malloc(an);
+    if(buf == NULL)
+        return -1;
+    b64cpy(buf, b, bn, 0);
+    r = memcmp(buf, a, an);
+    free(buf);
+    return r;
+}
+
+IntListPtr
+makeIntList(int size)
+{
+    IntListPtr list;
+    if(size <= 0)
+        size = 4;
+
+    list = malloc(sizeof(IntListRec));
+    if(list == NULL)
+        return NULL;
+
+    list->ranges = malloc(size * sizeof(IntRangeRec));
+    if(list->ranges == NULL) {
+        free(list);
+        return NULL;
+    }
+
+    list->length = 0;
+    list->size = size;
+    return list;
+}
+
+void
+destroyIntList(IntListPtr list)
+{
+    free(list->ranges);
+    free(list);
+}
+
+int
+intListMember(int n, IntListPtr list)
+{
+    int lo = 0, hi = list->length - 1;
+    int mid;
+    while(hi >= lo) {
+        mid = (hi + lo) / 2;
+        if(list->ranges[mid].from > n)
+            hi = mid - 1;
+        else if(list->ranges[mid].to < n)
+            lo = mid + 1;
+        else
+            return 1;
+    }
+    return 0;
+}
+
+static int
+deleteRange(IntListPtr list, int i)
+{
+    assert(list->length > i);
+    if(list->length > i + 1)
+        memmove(list->ranges + i, list->ranges + i + 1,
+                (list->length - i - 1) * sizeof(IntRangeRec));
+    list->length--;
+    return 1;
+}
+
+static int
+insertRange(int from, int to, IntListPtr list, int i)
+{
+    assert(i >= 0 && i <= list->length);
+    assert(i == 0 || list->ranges[i - 1].to < from - 1);
+    assert(i == list->length || list->ranges[i].from > to + 1);
+
+    if(list->length >= list->size) {
+        int newsize = list->size * 2 + 1;
+        IntRangePtr newranges = 
+            realloc(list->ranges, newsize * sizeof(IntRangeRec));
+        if(newranges == NULL)
+            return -1;
+        list->size = newsize;
+        list->ranges = newranges;
+    }
+
+    if(i < list->length)
+        memmove(list->ranges + i + 1, list->ranges + i,
+                list->length - i);
+    list->length++;
+    list->ranges[i].from = from;
+    list->ranges[i].to = to;
+    return 1;
+}
+
+static int
+maybeMergeRanges(IntListPtr list, int i)
+{
+    int rc;
+
+    while(i > 0 && list->ranges[i].from <= list->ranges[i - 1].to + 1) {
+            list->ranges[i - 1].from = 
+                MIN(list->ranges[i - 1].from, list->ranges[i].from);
+            list->ranges[i - 1].to =
+                MAX(list->ranges[i - 1].to, list->ranges[i].to);
+            rc = deleteRange(list, i);
+            if(rc < 0) return -1;
+            i--;
+    }
+
+    while(i < list->length - 1 && 
+          list->ranges[i].to >= list->ranges[i + 1].from - 1) {
+            list->ranges[i + 1].from = 
+                MIN(list->ranges[i + 1].from, list->ranges[i].from);
+            list->ranges[i - 1].to =
+                MAX(list->ranges[i + 1].to, list->ranges[i].to);
+            rc = deleteRange(list, i);
+            if(rc < 0) return -1;
+    }
+    return 1;
+}
+
+int
+intListCons(int from, int to, IntListPtr list)
+{
+    int i;
+
+    /* Don't bother with the dichotomy. */
+    for(i = 0; i < list->length; i++) {
+        if(list->ranges[i].to >= from - 1)
+            break;
+    }
+
+    if(i < list->length && 
+       (from >= list->ranges[i].from - 1 || to <= list->ranges[i].to + 1)) {
+        if(from <= list->ranges[i].from)
+            list->ranges[i].from = from;
+        if(to >= list->ranges[i].to)
+            list->ranges[i].to = to;
+        return maybeMergeRanges(list, i);
+    }
+    return insertRange(from, to, list, i);
+}
+
+/* Return the amount of physical memory on the box, -1 if unknown or
+   over two gigs. */
+#ifdef __linux
+
+#include <sys/sysinfo.h>
+int
+physicalMemory()
+{
+    int rc;
+    struct sysinfo info;
+
+    rc = sysinfo(&info);
+    if(rc < 0)
+        return -1;
+
+    if(info.totalram <= 0x7fffffff / info.mem_unit)
+        return (int)(info.totalram * info.mem_unit);
+
+    return -1;
+}
+
+#elif defined(__FreeBSD__)
+
+#include <sys/sysctl.h>
+int
+physicalMemory()
+{
+    unsigned long membytes;
+    size_t len;
+    int res;
+
+    len = sizeof(membytes);
+    res = sysctlbyname("hw.physmem", &membytes, &len, NULL, 0);
+    if (res || membytes > INT_MAX)
+        return -1;
+
+    return (int)membytes;
+}
+
+#else
+
+int
+physicalMemory()
+{
+    return -1;
+}
+#endif

+ 107 - 0
polipo/util.h

@@ -0,0 +1,107 @@
+/*
+Copyright (c) 2003-2006 by Juliusz Chroboczek
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+/* These are Polipo's error codes.  They need to be positive integers,
+   and must not collide with possible errno values.
+   Starting at 2^16 should be safe enough. */
+
+#define E0 (1 << 16)
+#define E1 (2 << 16)
+#define E2 (3 << 16)
+#define E3 (4 << 16)
+#define EUNKNOWN (E0)
+#define EDOSHUTDOWN (E0 + 1)
+#define EDOGRACEFUL (E0 + 2)
+#define EDOTIMEOUT (E0 + 3)
+#define ECLIENTRESET (E0 + 4)
+#define ESYNTAX (E0 + 5)
+#define EREDIRECTOR (E0 + 6)
+#define EDNS_HOST_NOT_FOUND (E1)
+#define EDNS_NO_ADDRESS (E1 + 1)
+#define EDNS_NO_RECOVERY (E1 + 2)
+#define EDNS_TRY_AGAIN (E1 + 3)
+#define EDNS_INVALID (E1 + 4)
+#define EDNS_UNSUPPORTED (E1 + 5)
+#define EDNS_FORMAT (E1 + 6)
+#define EDNS_REFUSED (E1 + 7)
+#define EDNS_CNAME_LOOP (E1 + 8)
+#define ESOCKS_PROTOCOL (E2)
+/* These correspond to SOCKS status codes 91 through 93 */
+#define ESOCKS_REJECT_FAIL (E2 + 1)
+#define ESOCKS_REJECT_IDENTD (E2 + 2)
+#define ESOCKS_REJECT_UID_MISMATCH (E2 + 3)
+/* (ESOCKS5_BASE + n) corresponds to SOCKS5 status code n (0 to 8) */
+#define ESOCKS5_BASE (E3)
+
+typedef struct _IntRange {
+    int from;
+    int to;
+} IntRangeRec, *IntRangePtr;
+
+typedef struct _IntList {
+    int length;
+    int size;
+    IntRangePtr ranges;
+} IntListRec, *IntListPtr;
+
+char *strdup_n(const char *restrict buf, int n) ATTRIBUTE ((malloc));
+int snnprintf(char *restrict buf, int n, int len, const char *format, ...)
+     ATTRIBUTE ((format (printf, 4, 5)));
+int snnvprintf(char *restrict buf, int n, int len, const char *format, va_list args)
+     ATTRIBUTE ((format (printf, 4, 0)));
+int snnprint_n(char *restrict buf, int n, int len, const char *s, int slen);
+int digit(char) ATTRIBUTE ((const));
+int letter(char) ATTRIBUTE ((const));
+char lwr(char) ATTRIBUTE ((const));
+char* lwrcpy(char *restrict dst, const char *restrict src, int n);
+int lwrcmp(const char *as, const char *bs, int n) ATTRIBUTE ((pure));
+int strcasecmp_n(const char *string, const char *buf, int n)
+     ATTRIBUTE ((pure));
+int atoi_n(const char *restrict string, int n, int len, int *value_return);
+int isWhitespace(const char *string) ATTRIBUTE((pure));
+#ifndef HAVE_MEMRCHR
+void *memrchr(const void *s, int c, size_t n) ATTRIBUTE ((pure));
+#endif
+int h2i(char h) ATTRIBUTE ((const));
+char i2h(int i) ATTRIBUTE ((const));
+int log2_floor(int x) ATTRIBUTE ((const));
+int log2_ceil(int x) ATTRIBUTE ((const));
+char* vsprintf_a(const char *f, va_list args)
+    ATTRIBUTE ((malloc, format (printf, 1, 0)));
+char* sprintf_a(const char *f, ...)
+    ATTRIBUTE ((malloc, format (printf, 1, 2)));
+unsigned int hash(unsigned seed, const void *restrict key, int key_size, 
+                  unsigned int hash_size)
+     ATTRIBUTE ((pure));
+char *pstrerror(int e);
+time_t mktime_gmt(struct tm *tm) ATTRIBUTE ((pure));
+AtomPtr expandTilde(AtomPtr filename);
+void do_daemonise(int noclose);
+void writePid(char *pidfile);
+int b64cpy(char *restrict dst, const char *restrict src, int n, int fss);
+int b64cmp(const char *restrict a, int an, const char *restrict b, int bn)
+    ATTRIBUTE ((pure));
+IntListPtr makeIntList(int size) ATTRIBUTE ((malloc));
+void destroyIntList(IntListPtr list);
+int intListMember(int n, IntListPtr list) ATTRIBUTE ((pure));
+int intListCons(int from, int to, IntListPtr list);
+int physicalMemory(void);

+ 188 - 0
privoxy/AUTHORS

@@ -0,0 +1,188 @@
+              Authors of Privoxy v2.9.x and 3.x
+===========================================================================
+
+Current Privoxy Team:
+
+ Fabian Keil, lead developer
+ David Schmidt
+ Lee Rian
+ Roland Rosenfeld
+ Ian Silvester
+
+Former Privoxy Team Members:
+
+ Johny Agotnes
+ Rodrigo Barbosa
+ Moritz Barsnick
+ Hal Burgiss
+ Ian Cummings
+ Brian Dessent
+ Jon Foster
+ Karsten Hopp
+ Alexander Lazic
+ Daniel Leite
+ Gábor Lipták
+ Adam Lock
+ Guy Laroche
+ Justin McMurtry
+ Mark Miller
+ Gerry Murphy
+ Andreas Oesterhelt
+ Haroon Rafique
+ Georg Sauthoff
+ Thomas Steudten
+ Jörg Strohmayer
+ Rodney Stromlund
+ Sviatoslav Sviridov
+ Sarantis Paskalis
+ Stefan Waldherr
+
+Thanks to the many people who have tested Privoxy, reported bugs, provided
+patches, made suggestions, donated or contributed in some other way. These
+include (in alphabetical order):
+
+ Rustam Abdullaev
+ Clint Adams
+ Maxim Antonov
+ Anatoly Arzhnikov
+ Ken Arromdee
+ Natxo Asenjo
+ Devin Bayer
+ Havard Berland
+ David Binderman
+ David Bo
+ Gergely Bor
+ Francois Botha
+ Reiner Buehl
+ Andrew J. Caines
+ Clifford Caoile
+ Edward Carrel
+ Pak Chan
+ Wan-Teh Chang
+ Sam Chen
+ Ramkumar Chinchani
+ Billy Crook
+ Frédéric Crozat
+ Matthew Daley
+ Michael T. Davis
+ Markus Dittrich
+ Mattes Dolak
+ Matthias Drochner
+ Peter E.
+ Florian Effenberger
+ Markus Elfring
+ Ryan Farmer
+ Matthew Fischer
+ Dean Gaudet
+ Stephen Gildea
+ John McGowan
+ Danny Goossen
+ Lizik Grelier
+ Daniel Griscom
+ Felix Gröbert
+ Bernard Guillot
+ Jeff H.
+ Tim H.
+ Aaron Hamid
+ Darel Henman
+ Magnus Holmgren
+ Eric M. Hopper
+ Ralf Horstmann
+ Ho+ Ho+ Ho+
+ Nedžad Hrnjica
+ Stefan Huehner
+ Basil Hussain
+ Peter Hyman
+ Derek Jennings
+ Andrew Jones
+ Julien Joubert
+ Ralf Jungblut
+ Petr Kadlec
+ Robert Klemme
+ Steven Kolins
+ Korda
+ Stefan Kurtz
+ Zeno Kugy
+ David Laight
+ Bert van Leeuwen
+ Don Libes
+ Paul Lieverse
+ Han Liu
+ Toby Lyward
+ Wil Mahan
+ Jindrich Makovicka
+ Raphael Marichez
+ Francois Marier
+ Angelina Matson
+ Jonathan McKenzie
+ David Mediavilla
+ Raphael Moll
+ J. Momberger
+ Mathew Murphy
+ Amuro Namie
+ Mark Nelson
+ Tobias Netzel
+ John Palkovic
+ Adam Piggott
+ Petr Písar
+ Dan Price
+ Roberto Ragusa
+ Félix Rauch
+ Kai Raven
+ Marvin Renich
+ Chris John Riley
+ Maynard Riley
+ Ivan Romanov
+ Andreas Rutkauskas
+ Sam
+ Bart Schelstraete
+ Gregory Seidman
+ Atman Sense
+ Chung-chieh Shan
+ Johan Sintorn
+ Benjamin C. Wiley Sittler
+ DRS David Soft
+ Simon South
+ Dan Stahlke
+ Oliver Stoeneberg
+ Václav Švec
+ Rick Sykes
+ Spinor S.
+ Peter Thoenen
+ Marc Thomas
+ Martin Thomas
+ Reuben Thomas
+ Guybrush Threepwood
+ Joel Verhagen
+ Bobby G. Vinyard
+ Jochen Voss
+ David Wagner
+ Glenn Washburn
+ Song Weijia
+ Jörg Weinmann
+ Darren Wiebe
+ Anduin Withers
+ withoutname
+ Eduard Wulff
+ Yang Xia
+ Jarry Xu
+ Oliver Yeoh
+ Yossi Zahn
+ Jamie Zawinski
+
+Privoxy is based in part on code originally developed by Junkbusters Corp. and
+Anonymous Coders.
+
+Privoxy heavily relies on Philip Hazel's PCRE.
+
+The code to filter compressed content makes use of zlib which is written by
+Jean-loup Gailly and Mark Adler.
+
+On systems that lack strptime(), Privoxy is using the one from the GNU C
+Library written by Ulrich Drepper.
+
+If we've missed you off this list, please let us know!
+
+ Privoxy team. https://www.privoxy.org/
+ <privoxy-devel@lists.privoxy.org>
+

+ 2783 - 0
privoxy/ChangeLog

@@ -0,0 +1,2783 @@
+--------------------------------------------------------------------
+ChangeLog for Privoxy
+--------------------------------------------------------------------
+*** Version 3.0.29 stable ***
+
+- Security/Reliability:
+  - Fixed memory leaks when a response is buffered and the buffer
+    limit is reached or Privoxy is running out of memory.
+    Commits bbd53f1010b and 4490d451f9b. OVE-20201118-0001.
+    Sponsored by: Robert Klemme
+  - Fixed a memory leak in the show-status CGI handler when
+    no action files are configured. Commit c62254a686.
+    OVE-20201118-0002.
+    Sponsored by: Robert Klemme
+  - Fixed a memory leak in the show-status CGI handler when
+    no filter files are configured. Commit 1b1370f7a8a.
+    OVE-20201118-0003.
+    Sponsored by: Robert Klemme
+  - Fixes a memory leak when client tags are active.
+    Commit 245e1cf32. OVE-20201118-0004.
+    Sponsored by: Robert Klemme
+  - Fixed a memory leak if multiple filters are executed
+    and the last one is skipped due to a pcre error.
+    Commit 5cfb7bc8fe. OVE-20201118-0005.
+  - Prevent an unlikely dereference of a NULL-pointer that
+    could result in a crash if accept-intercepted-requests
+    was enabled, Privoxy failed to get the request destination
+    from the Host header and a memory allocation failed.
+    Commit 7530132349. CID 267165. OVE-20201118-0006.
+  - Fixed memory leaks in the client-tags CGI handler when
+    client tags are configured and memory allocations fail.
+    Commit cf5640eb2a. CID 267168. OVE-20201118-0007.
+  - Fixed memory leaks in the show-status CGI handler when memory
+    allocations fail. Commit 064eac5fd0 and commit fdee85c0bf3.
+    CID 305233. OVE-20201118-0008.
+
+- General improvements:
+  - Added experimental https inspection support which allows to filter
+    https traffic. To enable it, install MbedTLS and configure with
+    --with-mbedtls, or install OpenSSL or LibreSSL and configure
+    with --with-openssl.
+    Afterwards configure the directives in section 7 of the
+    config file and enable the +https-inspection action.
+    Initial MbedTLS-based code contributed by Vaclav Svec,
+    initial OpenSSL support contributed by Maxim Antonov.
+    With help from Nedzad Hrnjica and Ho+ Ho+ Ho+.
+    Integration and improvements sponsored by Robert Klemme.
+  - pcrs: Request JIT compilation if it's supported and
+    the filter isn't dynamic. This can speed up filtering.
+  - Added support for Brotli decompression.
+    Sponsored by: Robert Klemme
+  - Added FEATURE_EXTENDED_STATISTICS to gather statistics for
+    block reasons and filter executions. To enable it, configure
+    with --enable-extended-statistics and visit
+    http://config.privoxy.org/show-status.
+    Sponsored by: Robert Klemme
+  - Use the IP_FREEBIND socket option, if defined. This allows
+    Privoxy to bind to not-yet assigned IP addresses which is
+    useful in failover environments.
+    Patch by Sam Varshavchik.
+  - Allow to use extended host patterns and vanilla host patterns
+    at the same time by prefixing extended host patterns with
+    "PCRE-HOST-PATTERN:". To enable this, configure with
+    --enable-pcre-host-patterns.
+    Sponsored by: Robert Klemme
+  - Added "Cross-origin resource sharing" (CORS) support.
+    This allows to access Privoxy's CGI interface via JavaScript from
+    another domain (white-listed with the new cors-allowed-origin directive).
+    Based on a patch by Nedzad Hrnjica.
+    Sponsored by: Robert Klemme.
+  - Add SOCKS5 username/password support.
+    Based on a patch by Sam, improved by Ivan Romanov.
+    Closes Patch#141 and solves TODO#105.
+  - Bump the maximum number of action and filter files
+    to 100 each.
+    Sponsored by: Robert Klemme
+  - Fixed handling of filters with "split-large-forms 1"
+    when using the CGI editor.
+    Reported by withoutname in #921.
+  - Better detect a mismatch of connection details when
+    figuring out whether or not a connection can be reused.
+  - Don't send a "Connection failure" message instead of the
+    "DNS failure" message.
+    Sponsored by: Robert Klemme
+  - Let LOG_LEVEL_REQUEST log all requests. Previously unencrypted
+    requests were only logged with LOG_LEVEL_REQUEST when they weren't
+    crunched (in which case they were logged with LOG_LEVEL_CRUNCH).
+    This was documented behaviour, but logging all requests seems more useful.
+  - Fixed locking around localtime() and gmtime().
+  - Removed OS/2 support. We haven't provided OS/2 packages in years,
+    it complicated the code and it depended on a fallback snprintf()
+    implementation which is GPLv2 only.
+  - Remove the fallback snprintf() implementation
+    Now that OS/2 support is gone we no longer need it.
+  - Fixed a bunch of format specifiers log messages.
+  - Added a missing apostrophe in the 'More Privoxy' menu.
+  - Explicitly prevent use of FEATURE_CONNECTION_SHARING
+    without FEATURE_CONNECTION_KEEP_ALIVE. It makes no sense
+    and does not compile anyway.
+    Sponsored by: Robert Klemme
+  - Fix build without FEATURE_CONNECTION_KEEP_ALIVE.
+    Sponsored by: Robert Klemme
+  - Downgrade the 'Graceful termination requested' message
+    to LOG_LEVEL_INFO as it isn't an error.
+    Sponsored by: Robert Klemme
+  - decompress_iob(): Downgrade the no-content message to LOG_LEVEL_RE_FILTER
+    While at it, fix a typo in a comment.
+    Sponsored by: Robert Klemme
+  - Fixed a couple of cppcheck warnings.
+  - Rename LOG_LEVEL_GPC to LOG_LEVEL_REQUEST.
+    Only the shadow knows what "GPC" is supposed to stand for.
+  - Remove SourceForge references in copyright headers.
+  - Upgrade a bunch of links to the homepage to https://.
+  - Add 'no-brotli-accepted' filter which prevents the
+    use of Brotli compression.
+  - Changed license for pcrs to GPLv2+ after getting the
+    permission from Andreas. This allows to redistribute
+    Privoxy under the GPLv3 which is required when linking
+    to future mbedTLS versions which are expected to be
+    licensed under the Apache 2.0 license only.
+  - Updated a bunch of tests that have to expect status code 403
+    now after r1.168/070e904afa5.
+  - Lowercase the host name in the request line.
+  - Only set SOURCE_DATE_EPOCH if it's not already set so
+    distributions can overwrite it through the environment.
+
+- Documentation changes:
+  - Explain that Privoxy has to be distributed under the
+    GPLv3 (or later) when linked with an MbedTLS version
+    that is licensed under the Apache 2.0 license.
+  - Import the GNU GPLv3 and include it the user manual.
+  - Clarify FEATURE_FORCE_LOAD's description. It allows to bypass
+    blocking not filtering and only does it if blocks aren't enforced.
+    Reported by: Robert Klemme
+  - FAQ: Remove Zwiebelfreunde e.V. from the list of fiduciary sponsors
+    As of 2021 they no longer handle donations for foreign organisations
+    due to lack of resources.
+  - FAQ: Remove an obsolete comment with a link to the long-gone PDF manual.
+  - FAQ: Add a link to the TODO list.
+  - FAQ: Change the sponsor amounts to USD slightly rounding the
+    converted amounts up to get simple numbers.
+    Receiving USD is apparently easier for SPI and SPI is
+    preferred by sponsors as they can send invoices.
+  - Advertise the client-tags CGI page in the user manual.
+  - Stop advertising the show-version CGI page which no longer exists.
+  - Add yet another reason why +prevent-compression may cause problems.
+  - Don't claim that contributors need ssh. It's only needed for committers.
+  - Replace obsolete CVS instructions with Git instructions.
+  - Remove an obsolete comment
+
+- Config file changes:
+  - Change the suggested default-server-timeout to 5 to match the
+    suggested keep-alive-timeout. Otherwise using the defaults would
+    result in Privoxy reducing the default-server-timeout and logging
+    an error message.
+    Sponsored by: Robert Klemme
+  - Update the 'debug 1' description.
+  - Add a missing 'client-specific-tag' directive.
+  - Comment out trusted-cgi-referer pointing to example.org.
+
+- Action file improvements:
+  - Block requests to /(.*/)?piwik\.php
+  - Block requests to .connectaserver.de/
+  - Block requests to pixel.inforsea.com/
+  - Block requests to t.vi-serve.com/
+  - Block requests to .ioam.de/
+  - Block requests to t.9gag.com/img.gif
+  - Block requests to .pixel.parsely.com/ as image
+  - Block requests to pixel.wp.com/
+  - Disable fast-redirects for .librarything.com/
+  - Disable fast-redirects for issue.freebsdfoundation.org/
+  - Disable fast-redirects for .twitter.com/.*origin=http
+  - Unblock belco24.de/
+  - Add fast-redirects exception for .wikipedia.org/
+  - Add fast-redirects exception for oss-fuzz.com/
+  - Disable fast-redirects for .consensu.org/delivery/pixel\.php
+    and block the requests as image instead
+  - Unblock .adbinstaller.com/
+    Reported by lvm in #942.
+  - Unblock .adbshell.com
+    Reported by lvm in #942.
+  - Unblock .tagesschau.de/
+  - Disable fast-redirects for collector.githubapp.com/
+    and block requests to it as image instead
+  - Unblock 'ada*.'
+  - Add fast-redirects{} exception for sourcepoint.vice.com/
+  - Unblock adaway.org/
+    Reported by DRS David Soft in AF#945.
+  - Change two block reasons that previously were the same.
+    Sponsored by: Robert Klemme
+  - Added a +delay-response{} test.
+  - Updated the location of the development version
+    of default.action.master.
+
+- Privoxy-Log-Parser:
+  - Added a --keep-date option to keep the date in highlighted messages.
+  - Highlight new log messages.
+  - Make gather_loglevel_clf_stats() more tolerant. While at it,
+    count all CLF messages as requests, even if the request is invalid.
+  - Only show HTTP version distribution if at least one version has been detected.
+  - Only show crunch statistics if crunches were detected.
+  - Warn if the request counts differ.
+  - Generate statistics if the log only contains LOG_LEVEL_CLF messages
+    so it can be used with vanilla webserver logs.
+    Previously Privoxy-specific "Request:" messages were required.
+  - Align the client-HTTP-version distribution like other distributions
+  - Bump version to 0.9.1
+  - Include status code distribution in the stats.
+  - Let the statistics include the size of the content Privoxy
+    transferred excluding HTTP headers.
+  - Get with the program and expect all requests to be logged with LOG_LEVEL_REQUEST.
+    It's no longer necessary to count both LOG_LEVEL_REQUEST and
+    LOG_LEVEL_CRUNCH messages to get the total number of requests.
+  - Leverage the LOG_LEVEL_CLF message to gather statistics that where
+    previously taken from LOG_LEVEL_HEADER lines. This results in less
+    confusing results if https inspection is enabled in which case there
+    are two LOG_LEVEL_HEADER lines with request lines.
+    Sponsored by: Robert Klemme
+  - Properly highlight the filter results message. Previously a brace got lost.
+  - Prefer the number of CLF lines to get the total number of requests
+    as it works with older Privoxy versions as well.
+
+- Privoxy-Regression-Test:
+  - Turn curl's globbing mode off so we can allow more characters in URLs.
+  - Allow '[' and ']' in URLs.
+  - Include the action file when complaining about missing Sticky Actions.
+  - Fix a sentence in the documentation.
+  - Bump version to 0.7.1
+
+- url-pattern-translator:
+  - Detect a couple of pattern prefixes case-insensitively.
+    Sponsored by: Robert Klemme
+  - Skip CLIENT-TAG patterns.
+    Sponsored by: Robert Klemme
+  - Skip patterns that have already been converted.
+    It should now be safe to "convert" a file multiple times.
+    Sponsored by: Robert Klemme
+  - Add the new 'PCRE-HOST-PATTERN:' prefix.
+    Sponsored by: Robert Klemme
+
+*** Version 3.0.28 stable ***
+
+- Bug fixes for regressions in 3.0.27:
+  - Fixed misplaced parentheses.
+    Reported by David Binderman.
+  - Changed two regression tests to depend on config directive
+    enable-remote-toggle instead of FEATURE_TOGGLE.
+
+*** Version 3.0.27 stable ***
+
+- General improvements:
+  - Add a receive-buffer-size directive which can be used to
+    set the size of the previously statically allocated buffer
+    in handle_established_connection().
+    Increasing the buffer size increases Privoxy's memory usage but
+    can lower the number of context switches and thereby reduce the
+    CPU usage and potentially increase the throughput.
+    This is mostly relevant for fast network connections and
+    large downloads that don't require filtering.
+    Sponsored by: Robert Klemme
+  - Add a listen-backlog directive which specifies the backlog
+    value passed to listen().
+    Sponsored by: Robert Klemme
+  - Add an enable-accept-filter directive which allows to
+    toggle accept filter support at run time when compiled
+    with FEATURE_ACCEPT_FILTER support.
+    It makes testing more convenient and now that it's
+    optional we can emit an error message if enabling
+    the accept filter fails.
+    Sponsored by: Robert Klemme
+  - Add a delay-response{} action.
+    This is useful to tar pit JavaScript requests that
+    are endlessly retried in case of blocks. It can also
+    be used to simulate a slow Internet connection.
+    Sponsored by: Robert Klemme
+  - Add a 'trusted-cgi-referrer' directive.
+    It allows to configure another page or site that can be used
+    to reach sensitive CGI resources.
+    Sponsored by: Robert Klemme
+  - Add a --fuzz mode which exposes Privoxy internals to input
+    from files or stdout.
+    Mainly tested with American Fuzzy Lop. For details see:
+    https://www.fabiankeil.de/talks/fuzzing-on-freebsd/
+    This work was partially funded with donations and done
+    as part of the Privoxy month in 2015.
+  - Consistently use the U(ngreedy) flag in the 'img-reorder' filter.
+  - listen_loop(): Reuse a single thread attribute object
+    The object doesn't change and creating a new one for
+    every thread is a waste of (CPU) time.
+    Sponsored by: Robert Klemme
+  - Free csp resources in the thread that belongs to the csp instead
+    of the main thread which has enough on its plate already.
+    Sponsored by: Robert Klemme
+  - Improve 'socket timeout reached' message.
+    Log the timeout that was triggered and downgrade the
+    log level to LOG_LEVEL_CONNECT to reduce the log noise
+    with common debug settings.
+    The timeout isn't necessary the result of an error and
+    usually merely indicates that Privoxy's socket timeout
+    is lower than the relevant timeouts used by client and
+    server.
+    Sponsored by: Robert Klemme
+  - Explicitly taint the server socket in case of CONNECT requests.
+    This doesn't fix any known problems, but makes
+    some log messages less confusing.
+  - Let write_pid_file() terminate if the pid file can't be opened.
+    Logging the issue at info level is unlikely to help.
+  - log_error(): Reduce the mutex-protected area by not using a
+    heap-allocated buffer that is shared between all threads.
+    This increases performance and reduces the latency with
+    verbose debug settings and multiple concurrent connections.
+    Sponsored by: Robert Klemme
+  - Let zalloc() use calloc() if it's available.
+    In some situations using calloc() can be faster than
+    malloc() + memset() and it should never be slower.
+    In the real world the impact of this change is not
+    expected to be noticeable.
+    Sponsored by: Robert Klemme
+  - Never use select() when poll() is available.
+    On most platforms select() is limited by FD_SETSIZE while
+    poll() is not. This was a scaling issue for multi-user setups.
+    Using poll() has no downside other than the usual risk
+    that code modifications may introduce new bugs that have
+    yet to be found and fixed.
+    At least in theory this commit could also reduce the latency
+    when there are lots of connections and select() would use
+    "bit fields in arrays of integers" to store file descriptors.
+    Another side effect is that Privoxy no longer has to stop
+    monitoring the client sockets when pipelined requests are
+    waiting but can't be read yet.
+    This code keeps the select()-based code behind ifdefs for
+    now but hopefully it can be removed soonish to make the
+    code more readable.
+    Sponsored by: Robert Klemme
+  - Add a 'reproducible-tarball-dist' target.
+    It's currently separate from the "tarball-dist" target
+    because it requires a tar implementation with mtree spec
+    support.
+    It's far from being perfect and does not enforce a
+    reproducible mode, but it's better than nothing.
+  - Use arc4random() if it's available.
+    While Privoxy doesn't need high quality pseudo-random numbers
+    there's no reason not to use them when we can and this silences
+    a warning emitted by code checkers that can't tell whether or not
+    the quality matters.
+  - Show the FEATURE_EXTERNAL_FILTERS status on the status page.
+    Better late than never. Previously a couple of tests weren't
+    executed as Privoxy-Regression-Test couldn't detect that the
+    FEATURE_EXTERNAL_FILTERS dependency was satisfied.
+  - Ditch FEATURE_IMAGE_DETECT_MSIE.
+    It's an obsolete workaround we inherited from Junkbuster
+    and was already disabled by default.
+    Users that feel the urge to work around issues with
+    image requests coming from an Internet Explorer version
+    from more than 15 years ago can still do this using tags.
+  - Consistently use strdup_or_die() instead of strdup() in
+    cases where allocation failures aren't expected.
+    Using strdup_or_die() allows to remove a couple of explicit
+    error checks which slightly reduces the size of the binary.
+  - Insert a refresh tag into the /client-tags CGI page when
+    serving it while a client-specific tag is temporarily enabled.
+    This makes it less likely that the user ends up
+    looking at tag state that is out of date.
+  - Use absolute URLs in the client-tag forms.
+    It's more consistent with the rest of the CGI page
+    URLs and makes it more convenient to copy the forms
+    to external pages.
+  - cgi_error_disabled(): Use status code 403 and an appropriate response line
+  - Use a dedicated CGI handler to deal with tag-toggle requests
+    As a result the /client-tags page is now safe to reach without
+    trusted Referer header which makes bookmarking or linking to
+    it more convenient.
+    Finally, refreshing the /client-tags page to show the
+    current state can no longer unintentionally repeat the
+    previous toggle request.
+  - Don't add a "Connection" header for CONNECT requests.
+    Explicitly sending "Connection: close" is not necessary and
+    apparently it causes problems with some forwarding proxies
+    that will close the connection prematurely.
+    Reported by Marc Thomas.
+  - Fix compiler warnings.
+
+- Bug fixes:
+  - rfc2553_connect_to(): Properly detect and log when poll()
+    reached the time out. Previously this was logged as:
+    Could not connect to [...]: No error: 0.
+    which isn't very helpful.
+    Sponsored by: Robert Klemme
+  - add_tag_for_client(): Set time_to_live properly.
+    Previously the time_to_live was always set for the first tag.
+    Attempts to temporarily enable a tag would result in enabling
+    it permanently unless no tag was enabled already.
+  - Revert r1.165 which didn't perform as advertised.
+    While the idea was to use "https:// when creating links
+    for the user manual on the website", the actual effect
+    was to use "https://" when Privoxy was supposed to serve
+    the user manual itself.
+    Reported by Yossi Zahn on Privoxy-devel@.
+  - socks5_connect(): Fail in case of unsupported address types.
+    Previously they would not be detected right away and
+    Privoxy would fail later on with an error message that
+    didn't make it obvious that the problem was socks-related.
+    So far, no such problems have actually been reported.
+  - socks5_connect(): Properly deal with socks replies that
+    contain IPv6 addresses.
+    Previously parts of the reply were left unread and
+    later on treated as invalid HTTP response data.
+    Fixes #904 reported by Danny Goossen who also provided
+    the initial version of this patch.
+
+- Action file improvements:
+  - Unblock 'msdn.microsoft.com/'.
+    It (presumably) isn't used to serve the kind of ads Privoxy should
+    block by default but happens to serve lots of pages with URLs that
+    are likely to result in false positives.
+    Reported by bugreporter1694 in AF#939.
+  - Disable gif deanimation for requests tagged with CSS-REQUEST.
+    The action will ignore content that isn't considered text
+    anyway and explicitly disabling it makes this more obvious
+    if "action" debugging (debug 65536) is enabled while
+    "gif deanimation" debugging (debug 256) isn't.
+  - Explicitly disable HTML filters for requests with CSS-REQUEST tag.
+    The filters are unlikely to break CSS files but executing
+    them without (intentionally) getting any hits is a waste of
+    cpu time and makes the log more noisy when running with
+    "debug 64".
+  - Unblock 'adventofcode.com/'.
+    Reported by Clint Adams in Debian bug #848211.
+    Fixes Roland's AF#937.
+  - Unblock 'adlibris.com'.
+    Reported by Wyrex in #935
+  - Unblock .golang.org/
+  - Add fast-redirects exception for '.youtube.com/.*origin=http'
+
+- Privoxy-Log-Parser:
+  - Don't gather host and resource statistics if they aren't requested.
+    While the performance impact seems negligible this significantly
+    reduces the memory usage if there are lots of requests.
+  - Bump version as the behaviour (slightly) changed.
+  - Count connection failures as well in statistics mode.
+    Sponsored by: Robert Klemme
+  - Count connection timeouts as well in statistics mode.
+    Sponsored by: Robert Klemme
+  - Fix an 'uninitialized value' warning when generating
+    statistics for a log file without response headers.
+    While privoxy-log-parser was supposed to detect this already,
+    the check was flawed and the message the user didn't see was
+    somewhat confusing anyway.
+    Now the message is less confusing, more helpful and actually printed.
+    Reported by: Robert Klemme
+
+- Documentation improvements:
+  - Refer to the git sources instead of CVS.
+  - Use GNU/Linux when referring to the OS instead of the kernel.
+  - Add FAQ entry for what to do if editing the config file is access denied.
+  - Add brief HTTP/2 FAQ.
+  - Add a small fuzzing section to the developer documentation.
+  - Add a client-header-tagger{client-ip-address} example.
+  - Stop suggesting that Privoxy is an anonymizing proxy.
+    The term could lead to Privoxy users overestimating
+    what it can do on its own (without Tor).
+  - Make it more obvious that SPI accepts Paypal, too.
+    Currently most donations are made through the Paypal account
+    managed by Zwiebelfreunde e.V. and a more even distribution
+    would be useful.
+  - Suggest to log applying actions as well when reproducing problems.
+  - Explicitly mention that Privoxy binaries are built by individuals
+    on their own systems. Buyer beware!
+  - Mention the release feed on the homepage.
+  - Remove a mysterious comment with a GNU FDL link as it isn't
+    useful and could confuse license scanners.
+    In May 2002 it was briefly claimed that "this document" was covered
+    by the GNU FDL. The commit message (r1.5) doesn't explain the motivation
+    or whether all copyright holders were actually asked and agreed to the
+    declared license change.
+    It's thus hard to tell whether or not the license change was legit,
+    but luckily two days later the "doc license" was "put" "back to GPL"
+    anyway (r1.6).
+    At the same time the offending comment with a link to the FDL
+    (not the GPL) was added for no obvious reason.
+    Now it's gone again.
+
+- Regression tests:
+  - Bump for-privoxy-version to 3.0.27 as we now rely on untrusted
+    CGI request being rejected with status code 403 (instead of 200).
+  - Update test for /send-stylesheet and add another one
+
+- Templates:
+  - Consistently use https:// when linking to the Privoxy website.
+  - Remove SourceForge references in Copyright header.
+  - Remove a couple of SourceForge references in a comment.
+    While at it, fix the grammar.
+  - Move the site-specific documentation block before the generic one.
+    While most Privoxy installations don't have a site-specific
+    documentation block, in cases were it exists it's likely to
+    be more relevant than the generic one.
+    Showing it first makes it less likely that users stop reading
+    before they reach it, especially on pages that don't fit on
+    the screen.
+
+- Build system improvements:
+  - Prefer openjade to jade. On some systems Jade produces
+    HTML with unescaped ampersands in URLs.
+  - Prefer OpenSP to SP to be consistent.
+  - Have Docbook generated HTML files be straight ASCII.
+    Dealing with a mixture of ISO-8859 and UTF-8 files is problematic.
+  - Echo the filename to stderr for 'make dok-tidy'.
+    Make it a bit easier to find errors in docbook generated HTML.
+  - Warn when still using select().
+  - Warn when compiling without calloc().
+  - Make it more obvious that the --with-fdsetsize configure switch
+    is pointless if poll() is available.
+  - Remove support for AmigaOS.
+  - Update windows build system to use supported software.
+    The cygwin gcc -mno-cygwin option is no longer supported, so
+    convert the windows build system to use the cygwin cross-compiler
+    to build "native" code.
+  - Add --enable-static-linking option for configure
+    does the same thing as LDFLAGS=-static; ./configure
+    but nicer than mixing evars and configure options.
+
+*** Version 3.0.26 stable ***
+
+- Bug fixes:
+  - Fixed crashes with "listen-addr :8118" (SF Bug #902).
+    The regression was introduced in 3.0.25 beta and reported
+    by Marvin Renich in Debian bug #834941.
+
+- General improvements:
+  - Log when privoxy is toggled on or off via cgi interface.
+  - Highlight the "Info: Now toggled " on/off log message
+    in the Windows log viewer.
+  - Highlight the loading actions/filter file log message
+    in the Windows log viewer.
+  - Mention client-specific tags on the toggle page as a
+    potentionally more appropriate alternative.
+
+- Documentation improvements:
+  - Update download section on the homepage.
+    The downloads are available from the website now.
+  - Add sponsor FAQ.
+  - Remove obsolete reference to mailing lists hosted at SourceForge.
+  - Update the "Before the Release" section of the developer manual.
+
+- Infrastructure improvements:
+  - Add perl script to generate an RSS feed for the packages
+    Submitted by "Unknown".
+
+- Build system improvements:
+  - strptime.h: fix a compiler warning about ambiguous else.
+  - configure.in: Check for Docbook goo on the BSDs as well.
+  - GNUMakefile.in: Let the dok-user target remove temporary files.
+
+*** Version 3.0.25 beta ***
+
+- Bug fixes:
+  - Always use the current toggle state for new requests.
+    Previously new requests on reused connections inherited
+    the toggle state from the previous request even though
+    the toggle state could have changed.
+    Reported by Robert Klemme.
+  - Fixed two buffer-overflows in the (deprecated) static
+    pcre code. These bugs are not considered security issues
+    as the input is trusted.
+    Found with afl-fuzz and ASAN.
+
+- General improvements:
+  - Added support for client-specific tags which allow Privoxy
+    admins to pre-define tags that are set for all requests from
+    clients that previously opted in through the CGI interface.
+    They are useful in multi-user setups where admins may
+    want to allow users to disable certain actions and filters
+    for themselves without affecting others.
+    In single-user setups they are useful to allow more fine-grained
+    toggling. For example to disable request blocking while still
+    crunching cookies, or to disable experimental filters only.
+    This is an experimental feature, the syntax and behaviour may
+    change in future versions.
+    Sponsored by Robert Klemme.
+  - Dynamic filters and taggers now support a $listen-address variable
+    which contains the address the request came in on.
+    For external filters the variable is called $PRIVOXY_LISTEN_ADDRESS.
+    Original patch contributed by pursievro.
+  - Add client-header-tagger 'listen-address'.
+  - Include the listen-address in the log message when logging new requests.
+    Patch contributed by pursievro.
+  - Turn invalid max-client-connections values into fatal errors.
+  - The show-status page now shows whether or not dates before 1970
+    and after 2038 are expected to be handled properly.
+    This is mainly useful for Privoxy-Regression-Test but could
+    also come handy when dealing with time-related support requests.
+  - On Mac OS X the thread id in log messages are more likely to
+    be unique now.
+  - When complaining about missing filters, the filter type is logged
+    as well.
+  - A couple of harmless coverity warnings were silenced
+    (CID #161202, CID #161203, CID #161211).
+
+- Action file improvements:
+  - Filtering is disabled for Range requests to let download resumption
+    and Windows updates work with the default configuration.
+  - Unblock ".ardmediathek.de/".
+    Reported by ThTomate in #932.
+
+- Documentation improvements:
+  - Add FAQ entry for crashes caused by memory limits.
+  - Remove obsolete FAQ entry about a bug in PHP 4.2.3.
+  - Mention the new mailing lists were appropriate.
+    As the archives have not been migrated, continue to
+    mention the archives at SF in the contacting section
+    for now.
+  - Note that the templates should be adjusted if Privoxy is
+    running as intercepting proxy without getting all requests.
+  - A bunch of links were converted to https://.
+  - Rephrase onion service paragraph to make it more obvious
+    that Tor is involved and that the whole website (and not
+    just the homepage) is available as onion service.
+  - Streamline the "More information" section on the homepage further
+    by additionally ditching the link to the 'See also' section
+    of the user manual. The section contains mostly links that are
+    directly reachable from the homepage already and the rest is
+    not significant enough to get a link from the homepage.
+  - Change the add-header{} example to set the DNT header
+    and use a complete section to make copy and pasting
+    more convenient.
+    Add a comment to make it obvious that adding the
+    header is not recommended for obvious reasons.
+    Using the DNT header as example was suggested by
+    Leo Wzukw.
+  - Streamline the support-and-service template
+    Instead of linking to the various support trackers
+    (whose URLs hopefully change soon), link to the
+    contact section of the user manual to increase the
+    chances that users actually read it.
+  - Add a FAQ entry for tainted sockets.
+  - More sections in the documentation have stable URLs now.
+  - FAQ: Explain why 'ping config.privoxy.org' is not expected
+    to reach a local Privoxy installation.
+  - Note that donations done through Zwiebelfreunde e.V. currently
+    can't be checked automatically.
+  - Updated section regarding starting Privoxy under OS X.
+  - Use dedicated start instructions for FreeBSD and ElectroBSD.
+  - Removed release instructions for AIX. They haven't been working
+    for years and unsurprisingly nobody seems to care.
+  - Removed obsolete reference to the solaris-dist target.
+  - Updated the release instructions for FreeBSD.
+  - Removed unfinished release instructions for Amiga OS and HP-UX 11.
+  - Added a pointer to the Cygwin Time Machine for getting the last release of
+    Cygwin version 1.5 to use for building Privoxy on Windows.
+  - Various typos have been fixed.
+
+- Infrastructure improvements:
+  - The website is no longer hosted at SourceForge and
+    can be reached through https now.
+  - The mailing lists at SourceForge have been deprecated,
+    you can subscribe to the new ones at: https://lists.privoxy.org/
+  - Migrating the remaining services from SourceForge is
+    work in progress (TODO list item #53).
+
+- Build system improvements:
+  - Add configure argument to optimistically redefine FD_SETSIZE
+    with the intent to change the maximum number of client
+    connections Privoxy can handle. Only works with some libcs.
+    Sponsored by Robert Klemme.
+  - Let the tarball-dist target skip files in ".git".
+  - Let the tarball-dist target work in cwds other than current.
+  - Make the 'clean' target faster when run from a git repository.
+  - Include tools in the generic distribution.
+  - Let the gen-dist target work in cwds other than current.
+  - Sort find output that is used for distribution tarballs
+    to get reproducible results.
+  - Don't add '-src' to the name of the tar ball generated by the
+    gen-dist target. The package isn't a source distribution but a
+    binary package.
+    While at it, use a variable for the name to reduce the chances
+    that the various references get out of sync and fix the gen-upload
+    target which was looking in the wrong directory.
+  - Add regression-tests.action to the files that are distributed.
+  - The gen-dist target which was broken since 2002 (r1.92) has been fixed.
+  - Remove genclspec.sh which has been obsolete since 2009.
+  - Remove obsolete reference to Redhat spec file.
+  - Remove the obsolete announce target which has been commented out years ago.
+  - Let rsync skip files if the checksums match.
+
+- Privoxy-Regression-Test:
+  - Add a "Default level offset" directive which can be used to
+    change the default level by a given value.
+    This directive affects all tests located after it until the end
+    of the file or a another "Default level offset" directive is reached.
+    The purpose of this directive is to make it more convenient to skip
+    similar tests in a given file without having to remove or disable
+    the tests completely.
+  - Let test level 17 depend on FEATURE_64_BIT_TIME_T
+    instead of FEATURE_PTHREAD which has no direct connection
+    to the time_t size.
+  - Fix indentation in perldoc examples.
+  - Don't overlook directives in the first line of the action file.
+  - Bump version to 0.7.
+  - Fix detection of the Privoxy version now that https://
+    is used for the website.
+
+*** Version 3.0.24 stable ***
+
+- Security fixes (denial of service):
+  - Prevent invalid reads in case of corrupt chunk-encoded content.
+    CVE-2016-1982. Bug discovered with afl-fuzz and AddressSanitizer.
+  - Remove empty Host headers in client requests.
+    Previously they would result in invalid reads. CVE-2016-1983.
+    Bug discovered with afl-fuzz and AddressSanitizer.
+
+- Bug fixes:
+  - When using socks5t, send the request body optimistically as well.
+    Previously the request body wasn't guaranteed to be sent at all
+    and the error message incorrectly blamed the server.
+    Fixes #1686 reported by Peter Müller and G4JC.
+  - Fixed buffer scaling in execute_external_filter() that could lead
+    to crashes. Submitted by Yang Xia in #892.
+  - Fixed crashes when executing external filters on platforms like
+    Mac OS X. Reported by Jonathan McKenzie on ijbswa-users@.
+  - Properly parse ACL directives with ports when compiled with HAVE_RFC2553.
+    Previously the port wasn't removed from the host and in case of
+    'permit-access 127.0.0.1 example.org:80' Privoxy would try (and fail)
+    to resolve "example.org:80" instead of example.org.
+    Reported by Pak Chan on ijbswa-users@.
+  - Check requests more carefully before serving them forcefully
+    when blocks aren't enforced. Privoxy always adds the force token
+    at the beginning of the path, but would previously accept it anywhere
+    in the request line. This could result in requests being served that
+    should be blocked. For example in case of pages that were loaded with
+    force and contained JavaScript to create additionally requests that
+    embed the origin URL (thus inheriting the force prefix).
+    The bug is not considered a security issue and the fix does not make
+    it harder for remote sites to intentionally circumvent blocks if
+    Privoxy isn't configured to enforce them.
+    Fixes #1695 reported by Korda.
+  - Normalize the request line in intercepted requests to make rewriting
+    the destination more convenient. Previously rewrites for intercepted
+    requests were expected to fail unless $hostport was being used, but
+    they failed "the wrong way" and would result in an out-of-memory
+    message (vanilla host patterns) or a crash (extended host patterns).
+    Reported by "Guybrush Threepwood" in #1694.
+  - Enable socket lingering for the correct socket.
+    Previously it was repeatedly enabled for the listen socket
+    instead of for the accepted socket. The bug was found by
+    code inspection and did not cause any (reported) issues.
+  - Detect and reject parameters for parameter-less actions.
+    Previously they were silently ignored.
+  - Fixed invalid reads in internal and outdated pcre code.
+    Found with afl-fuzz and AddressSanitizer.
+  - Prevent invalid read when loading invalid action files.
+    Found with afl-fuzz and AddressSanitizer.
+  - Windows build: Use the correct function to close the event handle.
+    It's unclear if this bug had a negative impact on Privoxy's behaviour.
+    Reported by Jarry Xu in #891.
+  - In case of invalid forward-socks5(t) directives, use the
+    correct directive name in the error messages. Previously they
+    referred to forward-socks4t failures.
+    Reported by Joel Verhagen in #889.
+
+- General improvements:
+  - Set NO_DELAY flag for the accepting socket. This significantly reduces
+    the latency if the operating system is not configured to set the flag
+    by default. Reported by Johan Sintorn in #894.
+  - Allow to build with mingw x86_64. Submitted by Rustam Abdullaev in #135.
+  - Introduce the new forwarding type 'forward-webserver'.
+    Currently it is only supported by the forward-override{} action and
+    there's no config directive with the same name. The forwarding type
+    is similar to 'forward', but the request line only contains the path
+    instead of the complete URL.
+  - The CGI editor no longer treats 'standard.action' special.
+    Nowadays the official "standards" are part of default.action
+    and there's no obvious reason to disallow editing them through
+    the cgi editor anyway (if the user decided that the lack of
+    authentication isn't an issue in her environment).
+  - Improved error messages when rejecting intercepted requests
+    with unknown destination.
+  - A couple of log messages now include the number of active threads.
+  - Removed non-standard Proxy-Agent headers in HTTP snipplets
+    to make testing more convenient.
+  - Include the error code for pcre errors Privoxy does not recognize.
+  - Config directives with numerical arguments are checked more carefully.
+  - Privoxy's malloc() wrapper has been changed to prevent zero-size
+    allocations which should only occur as the result of bugs.
+  - Various cosmetic changes.
+
+- Action file improvements:
+  - Unblock ".deutschlandradiokultur.de/".
+    Reported by u302320 in #924.
+  - Add two fast-redirect exceptions for "yandex.ru".
+  - Disable filter{banners-by-size} for ".plasmaservice.de/".
+  - Unblock "klikki.fi/adv/".
+  - Block requests for "resources.infolinks.com/".
+    Reported by "Black Rider" on ijbswa-users@.
+  - Block a bunch of criteo domains.
+    Reported by Black Rider.
+  - Block "abs.proxistore.com/abe/".
+    Reported by Black Rider.
+  - Disable filter{banners-by-size} for ".black-mosquito.org/".
+  - Disable fast-redirects for "disqus.com/".
+
+- Documentation improvements:
+  - FAQ: Explicitly point fingers at ASUS as an example of a
+    company that has been reported to force malware based on
+    Privoxy upon its customers.
+  - Correctly document the action type for a bunch of "multi-value"
+    actions that were incorrectly documented to be "parameterized".
+    Reported by Gregory Seidman on ijbswa-users@.
+  - Fixed the documented type of the forward-override{} action
+    which is obviously 'parameterized'.
+
+- Website improvements:
+  - Users who don't trust binaries served by SourceForge
+    can get them from a mirror. Migrating away from SourceForge
+    is planned for 2016 (TODO list item #53).
+  - The website is now available as onion service
+    (http://jvauzb4sb3bwlsnc.onion/).
+
+*** Version 3.0.23 stable ***
+
+- Bug fixes:
+  - Fixed a DoS issue in case of client requests with incorrect
+    chunk-encoded body. When compiled with assertions enabled
+    (the default) they could previously cause Privoxy to abort().
+    Reported by Matthew Daley. CVE-2015-1380.
+  - Fixed multiple segmentation faults and memory leaks in the
+    pcrs code. This fix also increases the chances that an invalid
+    pcrs command is rejected as such. Previously some invalid commands
+    would be loaded without error. Note that Privoxy's pcrs sources
+    (action and filter files) are considered trustworthy input and
+    should not be writable by untrusted third-parties. CVE-2015-1381.
+  - Fixed an 'invalid read' bug which could at least theoretically
+    cause Privoxy to crash. So far, no crashes have been observed.
+    CVE-2015-1382.
+  - Compiles with --disable-force again. Reported by Kai Raven.
+  - Client requests with body that can't be delivered no longer
+    cause pipelined requests behind them to be rejected as invalid.
+    Reported by Basil Hussain.
+
+- General improvements:
+  - If a pcrs command is rejected as invalid, Privoxy now logs
+    the cause of the problem as text. Previously the pcrs error
+    code was logged.
+  - The tests are less likely to cause false positives.
+
+- Action file improvements:
+  - '.sify.com/' is no longer blocked. Apparently it is not actually
+    a pure tracking site (anymore?). Reported by Andrew on ijbswa-users@.
+  - Unblock banners on .amnesty.de/ which aren't ads.
+
+- Documentation improvements:
+  - The 'Would you like to donate?' section now also contains
+    a "Paypal" address.
+  - The list of supported operating systems has been updated.
+  - The existence of the SF support and feature trackers has been
+    deemphasized because they have been broken for months.
+    Most of the time the mailing lists still work.
+  - The claim that default.action updates are sometimes released
+    on their own has been removed. It hasn't happened in years.
+  - Explicitly mention that Tor's port may deviate from the default
+    when using a bundle. Requested by Andrew on ijbswa-users@.
+
+*** Version 3.0.22 stable ***
+
+- Bug fixes:
+  - Fixed a memory leak when rejecting client connections due to
+    the socket limit being reached (CID 66382). This affected
+    Privoxy 3.0.21 when compiled with IPv6 support (on most
+    platforms this is the default). CVE-2015-1030.
+  - Fixed an immediate-use-after-free bug (CID 66394) and two
+    additional unconfirmed use-after-free complaints made by
+    Coverity scan (CID 66391, CID 66376). CVE-2015-1031.
+  - Actually show the FORCE_PREFIX value on the show-status page.
+  - Properly deal with Keep-Alive headers with timeout= parameters
+    If the timeout still can't be parsed, use the configured
+    timeout instead of preventing the client from keeping the
+    connection alive. Fixes #3615312/#870 reported by Bernard Guillot.
+  - Not using any filter files no longer results in warning messages
+    unless an action file is referencing header taggers or filters.
+    Reported by Stefan Kurtz in #3614835.
+  - Fixed a bug that prevented Privoxy from reusing some reusable
+    connections. Two bit masks with different purpose unintentionally
+    shared the same bit.
+  - A couple of additional bugs were discovered by Coverity Scan.
+    The fixes that are not expected to affect users are not explicitly
+    mentioned here, for details please have a look at the CVS logs.
+
+- General improvements:
+  - Introduced negative tag patterns NO-REQUEST-TAG and NO-RESPONSE-TAG.
+    They apply if no matching tag is found after parsing client or
+    server headers.
+  - Add support for external filters which allow to process the
+    response body with a script or program written in any language
+    the platform supports. External filters are enabled with
+    +external-filter{} after they have been defined in one of the
+    filter files with a header line starting with "EXTERNAL-FILTER:".
+    External filter support is experimental, not compiled by default
+    and known not to work on all platforms.
+  - Add support for the 'PATCH' method as defined in RFC5789.
+  - Reject requests with unsupported Expect header values.
+    Fixes a couple of Co-Advisor tests.
+  - Normalize the HTTP-version in forwarded requests and responses.
+    This is an explicit RFC 2616 MUST and RFC 7230 mandates that
+    intermediaries send their own HTTP-version in forwarded
+    messages.
+  - Server 'Keep-Alive' headers are no longer forwarded. From a user's
+    point of view it doesn't really matter, but RFC 2616 (obsolete)
+    mandates that the header is removed and this fixes a Co-Advisor
+    complaint.
+  - Change declared template file encoding to UTF-8. The templates
+    already used a subset of UTF-8 anyway and changing the declaration
+    allows to properly display UTF-8 characters used in the action files.
+    This change may require existing action files with ISO-8859-1
+    characters that aren't valid UTF-8 to be converted to UTF-8.
+    Requested by Sam Chen in #582.
+  - Do not pass rejected keep-alive timeouts to the server. It might
+    not have caused any problems (we know of), but doing the right
+    thing shouldn't hurt either.
+  - Let log_error() use its own buffer size #define to make changing
+    the log buffer size slightly less inconvenient.
+  - Turned single-threaded into a "proper" toggle directive with arguments.
+  - CGI templates no longer enforce new windows for some links.
+  - Remove an undocumented workaround ('HOST' header removal) for
+    an Apple iTunes bug that according to #729900 got fixed in 2003.
+
+- Action file improvements:
+  - The pattern 'promotions.' is no longer being blocked.
+    Reported by rakista in #3608540.
+  - Disable fast-redirects for .microsofttranslator.com/.
+  - Disable filter{banners-by-size} for .dgb-tagungszentren.de/.
+  - Add adn.speedtest.net as a site-specific unblocker.
+    Support request #3612908.
+  - Disable filter{banners-by-size} for creativecommons.org/.
+  - Block requests to data.gosquared.com/. Reported by cbug in #3613653.
+  - Unblock .conrad./newsletter/. Reported by David Bo in #3614238.
+  - Unblock .bundestag.de/.
+  - Unblock .rote-hilfe.de/.
+  - Disable fast-redirects for .facebook.com/plugins/like.php.
+  - Unblock Stackexchange popup URLs that aren't used to serve ads.
+    Reported by David Wagner in #3615179.
+  - Disable fast-redirects for creativecommons.org/.
+  - Unblock .stopwatchingus.info/.
+  - Block requests for .adcash.com/script/.
+    Reported by Tyrexionibus in #3615289.
+  - Disable HTML filters if the response was tagged as JavaScript.
+    Filtering JavaScript code with filters intended to deal with HTML
+    is usually a waste of time and, more importantly, may break stuff.
+  - Use a custom redirect{} for .washingtonpost.com/wp-apps/imrs\.php\?src=
+    Previously enabling the 'Advanced' settings (or manually enabling
+    +fast-redirects{}) prevented some images from being loaded properly.
+  - Unblock "adina*." Fixes #919 reported by Morton A. Goldberg.
+  - Block '/.*DigiAd'.
+  - Unblock 'adele*.'. Reported by Adele Lime in #1663.
+  - Disable banners-by-size for kggp.de/.
+
+- Filter file improvements & bug fixes:
+  - Decrease the chances that js-annoyances creates invalid JavaScript.
+    Submitted by John McGowan on ijbswa-users@.
+  - Let the msn filter hide 'related' ads again.
+  - Remove a stray '1' in the 'html-annoyances' filter.
+  - Prevent img-reorder from messing up img tags with empty src
+    attributes. Fixes #880 reported by Duncan.
+
+- Documentation improvements:
+  - Updated the 'Would you like to donate?' section.
+  - Note that invalid forward-override{} parameter syntax isn't
+    detected until the parameter is used.
+  - Add another +redirect{} example: a shortcut for illumos bugs.
+  - Make it more obvious that many operating systems support log
+    rotation out of the box.
+  - Fixed dead links. Reported by Mark Nelson in #3614557.
+  - Rephrased the 'Why is the configuration so complicated?' answer
+    to be slightly less condescending. Anonymously suggested in #3615122.
+  - Be more explicit about accept-intercepted-requests's lack of MITM support.
+  - Make 'demoronizer' FAQ entries more generic.
+  - Add an example hostname to the --pre-chroot-nslookup description.
+  - Add an example for a host pattern that matches an IP address.
+  - Rename the 'domain pattern' to 'host pattern' as it may
+    contain IP addresses as well.
+  - Recommend forward-socks5t when using Tor. It seems to work fine and
+    modifying the Tor configuration to profit from it hasn't been necessary
+    for a while now.
+  - Add another redirect{} example to stress that redirect loops can
+    and should be avoided.
+  - The usual spelling and grammar fixes. Parts of them were
+    reported by Reuben Thomas in #3615276.
+  - Mention the PCRS option letters T and D in the filter section.
+  - Clarify that handle-as-empty-doc-returns-ok is still useful
+    and will not be removed without replacement.
+  - Note that security issues shouldn't be reported using the bug tracker.
+  - Clarify what Privoxy does if both +block{} and +redirect{} apply.
+  - Removed the obsolete bookmarklets section.
+
+- Build system improvements:
+  - Let --with-group properly deal with secondary groups.
+    Patch submitted by Anatoly Arzhnikov in #3615187.
+  - Fix web-actions target.
+  - Add a web-faq target that only updates the FAQ on the webserver.
+  - Remove already-commented-out non-portable DOSFILTER alternatives.
+  - Remove the obsolete targets dok-put and dok-get.
+  - Add a sf-shell target.
+
+*** Version 3.0.21 stable ***
+
+- Bug fixes:
+  - On POSIX-like platforms, network sockets with file descriptor
+    values above FD_SETSIZE are properly rejected. Previously they
+    could cause memory corruption in configurations that allowed
+    the limit to be reached.
+  - Proxy authentication headers are removed unless the new directive
+    enable-proxy-authentication-forwarding is used. Forwarding the
+    headers potentially allows malicious sites to trick the user
+    into providing them with login information.
+    Reported by Chris John Riley.
+  - Compiles on OS/2 again now that unistd.h is only included
+    on platforms that have it.
+
+- General improvements:
+  - The show-status page shows the FEATURE_STRPTIME_SANITY_CHECKS status.
+  - A couple of assert()s that could theoretically dereference
+    NULL pointers in debug builds have been relocated.
+  - Added an LSB info block to the generic start script.
+    Based on a patch from Natxo Asenjo.
+  - The max-client-connections default has been changed to 128
+    which should be more than enough for most setups.
+
+- Action file improvements:
+  - Block rover.ebay./ar.*\&adtype= instead of "/.*\&adtype=" which
+    caused too man false positives.
+    Reported by u302320 in #360284, additional feedback from Adam Piggott.
+  - Unblock '.advrider.com/' and '/.*ADVrider'.
+    Anonymously reported in #3603636.
+  - Stop blocking '/js/slider\.js'.
+    Reported by Adam Piggott in #3606635 and _lvm in #2791160.
+
+- Filter file improvements:
+  - Added an iframes filter.
+
+- Documentation improvements:
+  - The whole GPLv2 text is included in the user manual now,
+    so Privoxy can serve it itself and the user can read it
+    without having to wade through GPLv3 ads first.
+  - Properly numbered and underlined a couple of section titles
+    in the config that where previously overlooked due to a flaw
+    in the conversion script. Reported by Ralf Jungblut.
+  - Improved the support instruction to hopefully make it harder to
+    unintentionally provide insufficient information when requesting
+    support. Previously it wasn't obvious that the information we need
+    in bug reports is usually also required in support requests.
+  - Removed documentation about packages that haven't been provided
+    in years.
+
+- Privoxy-Regression-Test:
+  - Only log the test number when not running in verbose mode
+    The position of the test is rarely relevant and it previously
+    wasn't exactly obvious which one of the numbers was useful to
+    repeat the test with --test-number.
+
+- GNUmakefile improvements:
+  - Factor generate-config-file out of config-file to make testing
+    more convenient.
+  - The clean target now also takes care of patch leftovers.
+
+*** Version 3.0.20 beta ***
+
+- Bug fixes:
+  - Client sockets are now properly shutdown and drained before being
+    closed. This fixes page truncation issues with clients that aggressively
+    pipeline data on platforms that otherwise discard already written data.
+    The issue mainly affected Opera users and was initially reported
+    by Kevin in #3464439, szotsaki provided additional information to track
+    down the cause.
+  - Fix latency calculation for shared connections (disabled by default).
+    It was broken since their introduction in 2009. The calculated latency
+    for most connections would be 0 in which case the timeout detection
+    failed to account for the real latency.
+  - Reject URLs with invalid port. Previously they were parsed incorrectly and
+    characters between the port number and the first slash were silently
+    dropped as shown by curl test 187.
+  - The default-server-timeout and socket-timeout directives accept 0 as
+    valid value.
+  - Fix a race condition on Windows that could cause Privoxy to become
+    unresponsive after toggling it on or off through the taskbar icon.
+    Reported by Tim H. in #3525694.
+  - Fix the compilation on Windows when configured without IPv6 support.
+  - Fix an assertion that could cause debug builds to abort() in case of
+    socks5 connection failures with "debug 2" enabled.
+  - Fix an assertion that could cause debug builds to abort() if a filter
+    contained nul bytes in the replacement text.
+
+- General improvements:
+  - Significantly improved keep-alive support for both client and server
+    connections.
+  - New debug log level 65536 which logs all actions that were applied to
+    the request.
+  - New directive client-header-order to forward client headers in a
+    different order than the one in which they arrived.
+  - New directive tolerate-pipelining to allow client-side pipelining.
+    If enabled (3.0.20 beta enables it by default), Privoxy will keep
+    pipelined client requests around to deal with them once the current
+    request has been served.
+  - New --config-test option to let Privoxy exit after checking whether or not
+    the configuration seems valid. The limitations noted in TODO #22 and #23
+    still apply. Based on a patch by Ramkumar Chinchani.
+  - New limit-cookie-lifetime{} action to let cookies expire before the end
+    of the session. Suggested by Rick Sykes in #1049575.
+  - Increase the hard-coded maximum number of actions and filter files from
+    10 to 30 (each). It doesn't significantly affect Privoxy's memory usage
+    and recompiling wasn't an option for all Privoxy users that reached the
+    limit.
+  - Add support for chunk-encoded client request bodies. Previously
+    chunk-encoded request bodies weren't guaranteed to be forwarded correctly,
+    so this can also be considered a bug fix although chunk-encoded request
+    bodies aren't commonly used in the real world.
+  - Add support for Tor's optimistic-data SOCKS extension, which can reduce the
+    latency for requests on newly created connections. Currently only the
+    headers are sent optimistically and only if the client request has already
+    been read completely which rules out requests with large bodies.
+  - After preventing the client from pipelining, don't signal keep-alive
+    intentions. When looking at the response headers alone, it previously
+    wasn't obvious from the client's perspective that no additional responses
+    should be expected.
+  - Stop considering client sockets tainted after receiving a request with body.
+    It hasn't been necessary for a while now and unnecessarily causes test
+    failures when using curl's test suite.
+  - Allow HTTP/1.0 clients to signal interest in keep-alive through the
+    Proxy-Connection header. While such client are rare in the real world, it
+    doesn't hurt and couple of curl tests rely on it.
+  - Only remove duplicated Content-Type headers when filters are enabled.
+    If they are not it doesn't cause ill effects and the user might not want it.
+    Downgrade the removal message to LOG_LEVEL_HEADER to clarify that it's not
+    an error in Privoxy and is unlikely to cause any problems in general.
+    Anonymously reported in #3599335.
+  - Set the socket option SO_LINGER for the client socket.
+  - Move several variable declarations to the beginning of their code block.
+    It's required when compiling with gcc 2.95 which is still used on some
+    platforms. Initial patch submitted by Simon South in #3564815.
+  - Optionally try to sanity-check strptime() results before trusting them.
+    Broken strptime() implementations have caused problems in the past and
+    the most recent offender seems to be FreeBSD's libc (standards/173421).
+  - When filtering is enabled, let Range headers pass if the range starts at
+    the beginning. This should work around (or at least reduce) the video
+    playback issues with various Apple clients as reported by Duc in #3426305.
+  - Do not confuse a client hanging up with a connection time out. If a client
+    closes its side of the connection without sending a request line, do not
+    send the CLIENT_CONNECTION_TIMEOUT_RESPONSE, but report the condition
+    properly.
+  - Allow closing curly braces as part of action values as long as they are
+    escaped.
+  - On Windows, the logfile is now written before showing the GUI error
+    message which blocks until the user acknowledges it.
+    Reported by Adriaan in #3593603.
+  - Remove an unreasonable parameter limit in the CGI interface. The new
+    parameter limit depends on the memory available and is currently unlikely
+    to be reachable, due to other limits in both Privoxy and common clients.
+    Reported by Andrew on ijbswa-users@.
+  - Decrease the chances of parse failures after requests with unsupported
+    methods were sent to the CGI interface.
+
+- Action file improvements:
+  - Remove the comment that indicated that updated default.action versions
+    are released on their own.
+  - Block 'optimize.indieclick.com/' and 'optimized-by.rubiconproject.com/'
+  - Unblock 'adjamblog.wordpress.com/' and 'adjamblog.files.wordpress.com/'.
+    Reported by Ryan Farmer in #3496116.
+  - Unblock '/.*Bugtracker'. Reported by pwhk in #3522341.
+  - Add test URLs for '.freebsd.org' and '.watson.org'.
+  - Unblock '.urbandictionary.com/popular'.
+  - Block '.adnxs.com/'.
+  - Block 'farm.plista.com/widgetdata.php'.
+  - Block 'rotation.linuxnewmedia.com/'.
+  - Block 'reklamy.sfd.pl/'. Reported by kacperdominik in #3399948.
+  - Block 'g.adspeed.net/'.
+  - Unblock 'websupport.wdc.com/'. Reported by Adam Piggot in #3577851.
+  - Block '/openx/www/delivery/'.
+  - Disable fast-redirects for '.googleapis.com/'.
+  - Block 'imp.double.net/'. Reported by David Bo in #3070411.
+  - Block 'gm-link.com/' which is used for email tracking.
+    Reported by David Bo in #1812733.
+  - Verify that requests to "bwp." are blocked. URL taken from #1736879
+    submitted by Francois Marier.
+  - Block '/.*bannerid='. Reported by Adam Piggott in #2975779.
+  - Block 'cltomedia.info/delivery/' and '.adexprt.com/'.
+    Anonymously reported in #2965254.
+  - Block 'de17a.com/'. Reported by David Bo in #3061472.
+  - Block 'oskar.tradera.com/'. Reported by David Bo in #3060596.
+  - Block '/scripts/webtrends\.js'. Reported by johnd16 in #3002729.
+  - Block requests for 'pool.*.adhese.com/'. Reported by johnd16 in #3002716.
+  - Update path pattern for Coremetrics and add tests.
+    Pattern and URLs submitted by Adam Piggott #3168443.
+  - Enable +fast-redirects{check-decoded-url} for 'tr.anp.se/'.
+    Reported by David Bo in #3268832.
+  - Unblock '.conrad.se/newsletter/banners/'. Reported by David Bo in #3413824.
+  - Block '.tynt.com/'. Reported by Dan Stahlke in #3421767.
+  - Unblock '.bbci.co.uk/radio/'. Reported by Adam Piggott in #3569603.
+  - Block requests to 'service.maxymiser.net/'.
+    Reported by johnd16 in #3118401 (with a previous URL).
+  - Disable fast-redirects for Google's "let's pretend your computer is
+    infected" page.
+  - Unblock '/.*download' to resolve actionsfile feedback #3498129.
+    Submitted by Steven Kolins (soundcloud.com not working).
+  - Unblock '.wlxrs.com/' which is required by hotmail.com.
+    Fixes #3413827 submitted by David Bo.
+  - Add two unblock patterns for popup radio and TV players.
+    Submitted by Adam Piggott in #3596089.
+
+- Filter file improvements & bug fixes:
+  - Add a referer tagger.
+  - Reduce the likelihood that the google filter messes up HTML-generating
+    JavaScript. Reported by Zeno Kugy in #3520260.
+
+- Documentation improvements:
+  - Revised all OS X sections due to new packaging module (OSXPackageBuilder).
+  - Update the list of supported operating systems to clarify that all Windows
+    versions after 95 are expected to work and note that the platform-specific
+    code for AmigaOS and QNX currently isn't maintained.
+  - Update 'Signals' section, the only explicitly handled signals are SIGINT,
+    SIGTERM and SIGHUP.
+  - Add Haiku to the list of operating systems on which Privoxy is known to
+    run.
+  - Add DragonFly to the list of BSDs on which Privoxy is known to run.
+  - Removed references to redhat-specific documentation set since it no longer
+    exists.
+  - Removed references to building PDFs since we no longer do so.
+  - Multiple listen-address directives are supported since 3.0.18, correct the
+    documentation to say so.
+  - Remove bogus section about long and short being preferable to int.
+  - Corrected some Internet JunkBuster references to Privoxy.
+  - Removed references to www.junkbusters.com since it is no longer
+    maintained. Reported by Angelina Matson.
+  - Various grammar and spelling corrections
+  - Add a client-header-tagger{} example for disabling filtering for range
+    requests.
+  - Correct a URL in the "Privoxy with Tor" FAQ.
+  - Spell 'refresh-tags' correctly. Reported by Don in #3571927.
+  - Sort manpage options alphabetically.
+  - Remove an incorrect sentence in the toggle section. The toggle state
+    doesn't affect whether or not the Windows version uses the tray icon.
+    Reported by Zeno Kugy in #3596395.
+  - Add new contributors since 3.0.19.
+
+- Log message improvements:
+  - When stopping to watch a client socket due to pipelining, additionally log
+    the socket number.
+  - Log the client socket and its condition before closing it. This makes it
+    more obvious that the socket actually gets closed and should help when
+    diagnosing problems like #3464439.
+  - In case of SOCKS5 failures, do not explicitly log the server's response.
+    It hasn't helped so far and the response can already be logged by enabling
+    "debug 32768" anyway. This reverts v1.81 and the follow-up bug fix v1.84.
+  - Relocate the connection-accepted message from listen_loop() to serve().
+    This way it's printed by the thread that is actually serving the
+    connection which is nice when grepping for thread ids in log files.
+
+- Code cleanups:
+  - Remove compatibility layer for versions prior to 3.0 since it has been
+    obsolete for more than 10 years now.
+  - Remove the ijb_isupper() and ijb_tolower() macros from parsers.c since
+    they aren't used in this file.
+  - Removed the 'Functions declared include:' comment sections since they tend
+    to be incomplete, incorrect and out of date and the benefit seems
+    questionable.
+  - Various comment grammar and comprehensibility improvements.
+  - Remove a pointless fflush() call in chat(). Flushing all streams pretty
+    much all the time for no obvious reason is ridiculous.
+  - Relocate ijb_isupper()'s definition to project.h and get the ijb_tolower()
+    definition from there, too.
+  - Relocate ijb_isdigit()'s definition to project.h.
+  - Rename ijb_foo macros to privoxy_foo.
+  - Add malloc_or_die() which will allow to simplify code paths where malloc()
+    failures don't need to be handled gracefully.
+  - Add strdup_or_die() which will allow to simplify code paths where strdup()
+    failures don't need to be handled gracefully.
+  - Replace strdup() calls with strdup_or_die() calls where it's safe and
+    simplifies the code.
+  - Fix white-space around parentheses.
+  - Add missing white-space behind if's and the following parentheses.
+  - Unwrap a memcpy() call in resolve_hostname_to_ip().
+  - Declare pcrs_get_delimiter()'s delimiters[] static const.
+  - Various optimisations to remove dead code and merge inefficient code
+    structures for improved clarity, performance or code compactness.
+  - Various data type corrections.
+  - Change visibility of several code segments when compiling without
+    FEATURE_CONNECTION_KEEP_ALIVE enabled for clarity.
+  - In pcrs_get_delimiter(), do not use delimiters outside the ASCII range.
+    Fixes a clang complaint.
+  - Fix an error message in get_last_url() nobody is supposed to see.
+    Reported by Matthew Fischer in #3507301.
+  - Fix a typo in the no-zlib-support complaint. Patch submitted by Matthew
+    Fischer in #3507304.
+  - Shorten ssplit()'s prototype by removing the last two arguments. We always
+    want to skip empty fields and ignore leading delimiters, so having
+    parameters for this only complicates the API.
+  - Use an enum for the type of the action value.
+  - Rename action_name's member takes_value to value_type as it isn't used as
+    boolean.
+  - Turn family mismatches in match_sockaddr() into fatal errors.
+  - Let enlist_unique_header() verify that the caller didn't pass a header
+    containing either \r or \n.
+  - Change the hashes used in load_config() to unsigned int. That's what
+    hash_string() actually returns and using a potentially larger type
+    is at best useless.
+  - Use privoxy_tolower() instead of vanilla tolower() with manual casting of
+    the argument.
+  - Catch ssplit() failures in parse_cgi_parameters().
+
+- Privoxy-Regression-Test:
+  - Add an 'Overwrite condition' directive to skip any matching tests before
+    it. As it has a global scope, using it is more convenient than clowning
+    around with the Ignore directive.
+  - Log to STDOUT instead of STDERR.
+  - Include the Privoxy version in the output.
+  - Various grammar and spelling corrections in documentation and code.
+  - Additional tests for range requests with filtering enabled.
+  - Tests with mostly invalid range request.
+  - Add a couple of hide-if-modified-since{} tests with different date formats.
+  - Cleaned up the format of the regression-tests.action file to match the
+    format of default.action.
+  - Remove the "Copyright" line from print_version(). When using --help, every
+    line of screen space matters and thus shouldn't be wasted on things the
+    user doesn't care about.
+
+- Privoxy-Log-Parser:
+  - Improve the --statistics performance by skipping sanity checks for input
+    that shouldn't affect the results anyway. Add a --strict-checks option
+    that enables some of the checks again, just in case anybody cares.
+  - The distribution of client requests per connection is included in
+    the --statistic output.
+  - The --accept-unknown-messages option has been removed and the behavior
+    is now the default.
+  - Accept and (mostly) highlight new log messages introduced with
+    Privoxy 3.0.20.
+
+- uagen:
+  - Bump generated Firefox version to 17.
+
+- GNUmakefile improvements:
+  - The dok-tidy target no longer taints documents with a tidy-mark
+  - Change RA_MODE from 0664 to 0644. Suggested by Markus Dittrich in
+    #3505445.
+  - Remove tidy's clean flag as it changes the scope of attributes.
+    Link-specific colors end up being applied to all text. Reported by Adam
+    Piggott in #3569551.
+  - Leave it up to the user whether or not smart tags are inserted.
+  - Let w3m itself do the line wrapping for the config file. It works better
+    than fmt as it can honour pre tags causing less unintentional line breaks.
+  - Ditch a pointless '-r' passed to rm to delete files.
+  - The config-file target now requires less manual intervention and updates
+    the original config.
+  - Change WDUMP to generate ASCII. Add WDUMP_UTF8 to allow UTF-8 in the
+    AUTHORS file so the names are right.
+  - Stop pretending that lynx and links are supported for the documentation.
+
+- configure improvements:
+  - On Haiku, do not pass -lpthread to the compiler. Haiku's pthreads
+    implementation is contained in its system library, libroot, so no
+    additional library needs to be searched.
+    Patch submitted by Simon South in #3564815.
+  - Additional Haiku-specific improvements. Disable checks intended for
+    multi-user systems as Haiku is presently single-user. Group Haiku-specific
+    settings in their own section, following the pattern for Solaris, OS/2 and
+    AmigaOS. Add additional library-related settings to remove the need for
+    providing configure with custom LDFLAGS.
+    Submitted by Simon South in #3574538.
+
+*** Version 3.0.19 Stable ***
+
+- Bug fixes:
+  - Prevent a segmentation fault when de-chunking buffered content.
+    It could be triggered by malicious web servers if Privoxy was
+    configured to filter the content and running on a platform
+    where SIZE_T_MAX isn't larger than UINT_MAX, which probably
+    includes most 32-bit systems. On those platforms, all Privoxy
+    versions before 3.0.19 appear to be affected.
+    To be on the safe side, this bug should be presumed to allow
+    code execution as proving that it doesn't seems unrealistic.
+  - Do not expect a response from the SOCKS4/4A server until it
+    got something to respond to. This regression was introduced
+    in 3.0.18 and prevented the SOCKS4/4A negotiation from working.
+    Reported by qqqqqw in #3459781.
+
+- General improvements:
+  - Fix an off-by-one in an error message about connect failures.
+  - Use a GNUMakefile variable for the webserver root directory and
+    update the path. Sourceforge changed it which broke various
+    web-related targets.
+  - Update the CODE_STATUS description.
+
+*** Version 3.0.18 Stable ***
+
+- Bug fixes:
+  - If a generated redirect URL contains characters RFC 3986 doesn't
+    permit, they are (re)encoded. Not doing this makes Privoxy versions
+    from 3.0.5 to 3.0.17 susceptible to HTTP response splitting (CWE-113)
+    attacks if the +fast-redirects{check-decoded-url} action is used.
+  - Fix a logic bug that could cause Privoxy to reuse a server
+    socket after it got tainted by a server-header-tagger-induced
+    block that was triggered before the whole server response had
+    been read. If keep-alive was enabled and the request following
+    the blocked one was to the same host and using the same forwarding
+    settings, Privoxy would send it on the tainted server socket.
+    While the server would simply treat it as a pipelined request,
+    Privoxy would later on fail to properly parse the server's
+    response as it would try to parse the unread data from the
+    first response as server headers for the second one.
+    Regression introduced in 3.0.17.
+  - When implying keep-alive in client_connection(), remember that
+    the client didn't. Fixes a regression introduced in 3.0.13 that
+    would cause Privoxy to wait for additional client requests after
+    receiving a HTTP/1.1 request with "Connection: close" set
+    and connection sharing enabled.
+    With clients which terminates the client connection after detecting
+    that the whole body has been received it doesn't really matter,
+    but with clients that don't the connection would be kept open until
+    it timed out.
+  - Fix a subtle race condition between prepare_csp_for_next_request()
+    and sweep(). A thread preparing itself for the next client request
+    could briefly appear to be inactive.
+    If all other threads were already using more recent files,
+    the thread could get its files swept away under its feet.
+    So far this has only been reproduced while stress testing in
+    valgrind while touching action files in a loop. It's unlikely
+    to have caused any actual problems in the real world.
+  - Disable filters if SDCH compression is used unless filtering is forced.
+    If SDCH was combined with a supported compression algorithm, Privoxy
+    previously could try to decompress it and ditch the Content-Encoding
+    header even though the SDCH compression wasn't dealt with.
+    Reported by zebul666 in #3225863.
+  - Make a copy of the --user value and only mess with that when splitting
+    user and group. On some operating systems modifying the value directly
+    is reflected in the output of ps and friends and can be misleading.
+    Reported by zepard in #3292710.
+  - If forwarded-connect-retries is set, only retry if Privoxy is actually
+    forwarding the request. Previously direct connections would be retried
+    as well.
+  - Fixed a small memory leak when retrying connections with IPv6
+    support enabled.
+  - Remove an incorrect assertion in compile_dynamic_pcrs_job_list()
+    It could be triggered by a pcrs job with an invalid pcre
+    pattern (for example one that contains a lone quantifier).
+  - If the --user argument user[.group] contains a dot, always bail out
+    if no group has been specified. Previously the intended, but undocumented
+    (and apparently untested), behaviour was to try interpreting the whole
+    argument as user name, but the detection was flawed and checked for '0'
+    instead of '\0', thus merely preventing group names beginning with a zero.
+  - In html_code_map[], use a numeric character reference instead of &apos;
+    which wasn't standardized before XHTML 1.0.
+  - Fix an invalid free when compiled with FEATURE_GRACEFUL_TERMINATION
+    and shut down through http://config.privoxy.org/die
+  - In get_actions(), fix the "temporary" backwards compatibility hack
+    to accept block actions without reason.
+    It also covered other actions that should be rejected as invalid.
+    Reported by Billy Crook.
+
+- General improvements:
+  - Privoxy can (re)compress buffered content before delivering
+    it to the client. Disabled by default as most users wouldn't
+    benefit from it.
+  - The +fast-redirects{check-decoded-url} action checks URL
+    segments separately. If there are other parameters behind
+    the redirect URL, this makes it unnecessary to cut them off
+    by additionally using a +redirect{} pcrs command.
+    Initial patch submitted by Jamie Zawinski in #3429848.
+  - When loading action sections, verify that the referenced filters
+    exist. Currently missing filters only result in an error message,
+    but eventually the severity will be upgraded to fatal.
+  - Allow to bind to multiple separate addresses.
+    Patch set submitted by Petr Pisar in #3354485.
+  - Set socket_error to errno if connecting fails in rfc2553_connect_to().
+    Previously rejected direct connections could be incorrectly reported
+    as DNS issues if Privoxy was compiled with IPv6 support.
+  - Adjust url_code_map[] so spaces are replaced with %20 instead of '+'
+    While '+' can be used by client's submitting form data, this is not
+    actually what Privoxy is using the lookups for. This is more of a
+    cosmetic issue and doesn't fix any known problems.
+  - When compiled without FEATURE_FAST_REDIRECTS, do not silently
+    ignore +fast-redirect{} directives
+  - Added a workaround for GNU libc's strptime() reporting negative
+    year values when the parsed year is only specified with two digits.
+    On affected systems cookies with such a date would not be turned
+    into session cookies by the +session-cookies-only action.
+    Reported by Vaeinoe in #3403560
+  - Fixed bind failures with certain GNU libc versions if no non-loopback
+    IP address has been configured on the system. This is mainly an issue
+    if the system is using DHCP and Privoxy is started before the network
+    is completely configured.
+    Reported by Raphael Marichez in #3349356.
+    Additional insight from Petr Pisar.
+  - Privoxy log messages now use the ISO 8601 date format %Y-%m-%d.
+    It's only slightly longer than the old format, but contains
+    the full date including the year and allows sorting by date
+    (when grepping in multiple log files) without hassle.
+  - In get_last_url(), do not bother trying to decode URLs that do
+    not contain at least one '%' sign. It reduces the log noise and
+    a number of unnecessary memory allocations.
+  - In case of SOCKS5 failures, dump the socks response in the log message.
+  - Simplify the signal setup in main().
+  - Streamline socks5_connect() slightly.
+  - In socks5_connect(), require a complete socks response from the server.
+    Previously Privoxy didn't care how much data the server response
+    contained as long as the first two bytes contained the expected
+    values. While at it, shrink the buffer size so Privoxy can't read
+    more than a whole socks response.
+  - In chat(), do not bother to generate a client request in case of
+    direct CONNECT requests. It will not be used anyway.
+  - Reduce server_last_modified()'s stack size.
+  - Shorten get_http_time() by using strftime().
+  - Constify the known_http_methods pointers in unknown_method().
+  - Constify the time_formats pointers in parse_header_time().
+  - Constify the formerly_valid_actions pointers in action_used_to_be_valid().
+  - Introduce a GNUMakefile MAN_PAGE variable that defaults to privoxy.1.
+    The Debian package uses section 8 for the man page and this
+    should simplify the patch.
+  - Deduplicate the INADDR_NONE definition for Solaris by moving it to jbsockets.h
+  - In block_url(), ditch the obsolete workaround for ancient Netscape versions
+    that supposedly couldn't properly deal with status code 403.
+  - Remove a useless NULL pointer check in load_trustfile().
+  - Remove two useless NULL pointer checks in load_one_re_filterfile().
+  - Change url_code_map[] from an array of pointers to an array of arrays
+    It removes an unnecessary layer of indirection and on 64bit system reduces
+    the size of the binary a bit.
+  - Fix various typos. Fixes taken from Debian's 29_typos.dpatch by Roland Rosenfeld.
+  - Add a dok-tidy GNUMakefile target to clean up the messy HTML
+    generated by the other dok targets.
+  - GNUisms in the GNUMakefile have been removed.
+  - Change the HTTP version in static responses to 1.1
+  - Synced config.sub and config.guess with upstream
+    2011-11-11/386c7218162c145f5f9e1ff7f558a3fbb66c37c5.
+  - Add a dedicated function to parse the values of toggles. Reduces duplicated
+    code in load_config() and provides better error handling. Invalid or missing
+    toggle values are now a fatal error instead of being silently ignored.
+  - Terminate HTML lines in static error messages with \n instead of \r\n.
+  - Simplify cgi_error_unknown() a bit.
+  - In LogPutString(), don't bother looking at pszText when not
+    actually logging anything.
+  - Change ssplit()'s fourth parameter from int to size_t.
+    Fixes a clang complaint.
+  - Add a warning that the statistics currently can't be trusted.
+    Mention Privoxy-Log-Parser's --statistics option as
+    an alternative for the time being.
+  - In rfc2553_connect_to(), start setting cgi->error_message on error.
+  - Change the expected status code returned for http://p.p/die depending
+    on whether or not FEATURE_GRACEFUL_TERMINATION is available.
+  - In cgi_die(), mark the client connection for closing.
+    If the client will fetch the style sheet through another connection
+    it gets the main thread out of the accept() state and should thus
+    trigger the actual shutdown.
+  - Add a proper CGI message for cgi_die().
+  - Don't enforce a logical line length limit in read_config_line().
+  - Slightly refactor server_last_modified() to remove useless gmtime*() calls.
+  - In get_content_type(), also recognize '.jpeg' as JPEG extension.
+  - Add '.png' to the list of recognized file extensions in get_content_type().
+  - In block_url(), consistently use the block reason "Request blocked by Privoxy"
+    In two places the reason was "Request for blocked URL" which hides the
+    fact that the request got blocked by Privoxy and isn't necessarily
+    correct as the block may be due to tags.
+  - In listen_loop(), reload the configuration files after accepting
+    a new connection instead of before.
+    Previously the first connection that arrived after a configuration
+    change would still be handled with the old configuration.
+  - In chat()'s receive-data loop, skip a client socket check if
+    the socket will be written to right away anyway. This can
+    increase the transfer speed for unfiltered content on fast
+    network connections.
+  - The socket timeout is used for SOCKS negotiations as well which
+    previously couldn't timeout.
+  - Don't keep the client connection alive if any configuration file
+    changed since the time the connection came in. This is closer to
+    Privoxy's behaviour before keep-alive support for client connection
+    has been added and also less confusing in general.
+  - Treat all Content-Type header values containing the pattern
+    'script' as a sign of text. Reported by pribog in #3134970.
+
+- Action file improvements:
+  - Moved the site-specific block pattern section below the one for the
+    generic patterns so for requests that are matched in both, the block
+    reason for the domain is shown which is usually more useful than showing
+    the one for the generic pattern.
+  - Remove -prevent-compression from the fragile alias. It's no longer
+    used anywhere by default and isn't known to break stuff anyway.
+  - Add a (disabled) section to block various Facebook tracking URLs.
+    Reported by Dan Stahlke in #3421764.
+  - Add a (disabled) section to rewrite and redirect click-tracking
+    URLs used on news.google.com.
+    Reported by Dan Stahlke in #3421755.
+  - Unblock linuxcounter.net/.
+    Reported by Dan Stahlke in #3422612.
+  - Block 'www91.intel.com/' which is used by Omniture.
+    Reported by Adam Piggott in #3167370.
+  - Disable the handle-as-empty-doc-returns-ok option and mark it as deprecated.
+    Reminded by tceverling in #2790091.
+  - Add ".ivwbox.de/" to the "Cross-site user tracking" section.
+    Reported by Nettozahler in #3172525.
+  - Unblock and fast-redirect ".awin1.com/.*=http://".
+    Reported by Adam Piggott in #3170921.
+  - Block "b.collective-media.net/".
+  - Widen the Debian popcon exception to "qa.debian.org/popcon".
+    Seen in Debian's 05_default_action.dpatch by Roland Rosenfeld.
+  - Block ".gemius.pl/" which only seems to be used for user tracking.
+    Reported by johnd16 in #3002731. Additional input from Lee and movax.
+  - Disable banners-by-size filters for '.thinkgeek.com/'.
+    The filter only seems to catch pictures of the inventory.
+  - Block requests for 'go.idmnet.bbelements.com/please/showit/'.
+    Reported by kacperdominik in #3372959.
+  - Unblock adainitiative.org/.
+  - Add a fast-redirects exception for '.googleusercontent.com/.*=cache'.
+  - Add a fast-redirects exception for webcache.googleusercontent.com/.
+  - Unblock http://adassier.wordpress.com/ and http://adassier.files.wordpress.com/.
+
+- Filter file improvements:
+  - Let the yahoo filter hide '.ads'.
+  - Let the msn filter hide overlay ads for Facebook 'likes' in search
+    results and elements with the id 's_notf_div'. They only seem to be
+    used to advertise site 'enhancements'.
+  - Let the js-events filter additionally disarm setInterval().
+    Suggested by dg1727 in #3423775.
+
+- Documentation improvements:
+  - Clarify the effect of compiling Privoxy with zlib support.
+    Suggested by dg1727 in #3423782.
+  - Point out that the SourceForge messaging system works like a black
+    hole and should thus not be used to contact individual developers.
+  - Mention some of the problems one can experience when not explicitly
+    configuring an IP addresses as listen address.
+  - Explicitly mention that hostnames can be used instead of IP addresses
+    for the listen-address, that only the first address returned will be
+    used and what happens if the address is invalid.
+    Requested by Calestyo in #3302213.
+
+- Log message improvements:
+  - If only the server connection is kept alive, do not pretend to
+    wait for a new client request.
+  - Remove a superfluous log message in forget_connection().
+  - In chat(), properly report missing server responses as such
+    instead of calling them empty.
+  - In forwarded_connect(), fix a log message nobody should ever see.
+  - Fix a log message in socks5_connect(), a failed write operation
+    was logged as failed read operation.
+  - Let load_one_actions_file() properly complain about a missing
+    '{' at the beginning of the file.
+    Simply stating that a line is invalid isn't particularly helpful.
+  - Do not claim to listen on a socket until Privoxy actually does.
+    Patch submitted by Petr Pisar #3354485
+  - Prevent a duplicated LOG_LEVEL_CLF message when sending out
+    the "no-server-data" response.
+  - Also log the client socket when dropping a connection.
+  - Include the destination host in the 'Request ... marked for
+    blocking. limit-connect{...} doesn't allow CONNECT ...' message
+    Patch submitted by Saperski in #3296250.
+  - Prevent a duplicated log message if none of the resolved IP
+    addresses were reachable.
+  - In connect_to(), do not pretend to retry if forwarded-connect-retries
+    is zero or unset.
+  - When a specified user or group can't be found, put the name in
+    single-quotes when logging it.
+  - In rfc2553_connect_to(), explain getnameinfo() errors better.
+  - Remove a useless log message in chat().
+  - When retrying to connect, also log the maximum number of connection
+    attempts.
+  - Rephrase a log message in compile_dynamic_pcrs_job_list().
+    Divide the error code and its meaning with a colon. Call the pcrs
+    job dynamic and not the filter. Filters may contain dynamic and
+    non-dynamic pcrs jobs at the same time. Only mention the name of
+    the filter or tagger, but don't claim it's a filter when it could
+    be a tagger.
+  - In a fatal error message in load_one_actions_file(), cover both
+    URL and TAG patterns.
+  - In pcrs_strerror(), properly report unknown positive error code
+    values as such. Previously they were handled like 0 (no error).
+  - In compile_dynamic_pcrs_job_list(), also log the actual error code as
+    pcrs_strerror() doesn't handle all errors reported by pcre.
+  - Don't bother trying to continue chatting if the client didn't ask for it.
+    Reduces log noise a bit.
+  - Make two fatal error message in load_one_actions_file() more descriptive.
+  - In cgi_send_user_manual(), log when rejecting a file name due to '/' or '..'.
+  - In load_file(), log a message if opening a file failed.
+    The CGI error message alone isn't too helpful.
+  - In connection_destination_matches(), improve two log messages
+    to help understand why the destinations don't match.
+  - Rephrase a log message in serve(). Client request arrival
+    should be differentiated from closed client connections now.
+  - In serve(), log if a client connection isn't reused due to a
+    configuration file change.
+  - Let mark_server_socket_tainted() always mark the server socket tainted,
+    just don't talk about it in cases where it has no effect. It doesn't change
+    Privoxy's behaviour, but makes understanding the log file easier.
+
+- configure:
+  - Added a --disable-ipv6-support switch for platforms where support
+    is detected but doesn't actually work.
+  - Do not check for the existence of strerror() and memmove() twice
+  - Remove a useless test for setpgrp(2). Privoxy doesn't need it and
+    it can cause problems when cross-compiling.
+  - Rename the --disable-acl-files switch to --disable-acl-support.
+    Since about 2001, ACL directives are specified in the standard
+    config file.
+  - Update the URL of the 'Removing outdated PCRE version after the
+    next stable release' posting. The old URL stopped working after
+    one of SF's recent site "optimizations". Reported by Han Liu.
+
+- Privoxy-Regression-Test:
+  - Added --shuffle-tests option to increase the chances of detection race conditions.
+  - Added a --local-test-file option that allows to use Privoxy-Regression-Test without Privoxy.
+  - Added tests for missing socks4 and socks4a forwarders.
+  - The --privoxy-address option now works with IPv6 addresses containing brackets, too.
+  - Perform limited sanity checks for parameters that are supposed to have numerical values.
+  - Added a --sleep-time option to specify a number of seconds to
+    sleep between tests, defaults to 0.
+  - Disable the range-requests tagger for tests that break if it's enabled.
+  - Log messages use the ISO 8601 date format %Y-%m-%d.
+  - Fix spelling in two error messages.
+  - In the --help output, include a list of supported tests and their default levels.
+  - Adjust the tests to properly deal with FEATURE_TOGGLE being disabled.
+
+- Privoxy-Log-Parser:
+  - Perform limited sanity checks for command line parameters that
+    are supposed to have numerical values.
+  - Implement a --unbreak-lines-only option to try to revert MUA breakage.
+  - Accept and highlight: Added header: Content-Encoding: deflate
+  - Accept and highlight: Compressed content from 29258 to 8630 bytes.
+  - Accept and highlight: Client request arrived in time on socket 21.
+  - Highlight: Didn't receive data in time: a.fsdn.com:443
+  - Accept log messages with ISO 8601 time stamps, too.
+
+- uagen:
+  - Bump generated Firefox version to 8.0.
+  - Only randomize the release date if the new --randomize-release-date
+    option is enabled. Firefox versions after 4 use a fixed date string
+    without meaning.
+
+*** Version 3.0.17 Stable ***
+
+- Fixed last-chunk-detection for responses where the body was small
+  enough to be read with the headers, causing Privoxy to wait for the
+  end of the content until the server closed the connection or the
+  request timed out. Reported by "Karsten" in #3028326.
+- Responses with status code 204 weren't properly detected as body-less
+  like RFC2616 mandates. Like the previous bug, this caused Privoxy to
+  wait for the end of the content until the server closed the connection
+  or the request timed out. Fixes #3022042 and #3025553, reported by a
+  user with no visible name. Most likely also fixes a bunch of other
+  AJAX-related problem reports that got closed in the past due to
+  insufficient information and lack of feedback.
+- Fixed an ACL bug that made it impossible to build a blacklist.
+  Usually the ACL directives are used in a whitelist, which worked
+  as expected, but blacklisting is still useful for public proxies
+  where one only needs to deny known abusers access.
+- Added LOG_LEVEL_RECEIVED to log the not-yet-parsed data read from the
+  network. This should make debugging various parsing issues a lot easier.
+- The IPv6 code is enabled by default on Windows versions that support it.
+  Patch submitted by oCameLo in #2942729.
+- In mingw32 versions, the user.filter file is reachable through the
+  GUI, just like default.filter is. Feature request 3040263.
+- Added the configure option --enable-large-file-support to set a few
+  defines that are required by platforms like GNU/Linux to support files
+  larger then 2GB. Mainly interesting for users without proper logfile
+  management.
+- Logging with "debug 16" no longer stops at the first nul byte which is
+  pretty useless. Non-printable characters are replaced with their hex value
+  so the result can't span multiple lines making parsing them harder then
+  necessary.
+- Privoxy logs when reading an action, filter or trust file.
+- Fixed incorrect regression test markup which caused a test in
+  3.0.16 to fail while Privoxy itself was working correctly.
+  While Privoxy accepts hide-referer, too, the action name is actually
+  hide-referrer which is also the name used one the final results page,
+  where the test expected the alias.
+
+- CGI interface improvements:
+  - In finish_http_response(), continue to add the 'Connection: close'
+    header if the client connection will not be kept alive.
+    Anonymously pointed out in #2987454.
+  - Apostrophes in block messages no longer cause parse errors
+    when the blocked page is viewed with JavaScript enabled.
+    Reported by dg1727 in #3062296.
+  - Fix a bunch of anchors that used underscores instead of dashes.
+  - Allow to keep the client connection alive after crunching the previous request.
+    Already opened server connections can be kept alive, too.
+  - In cgi_show_url_info(), don't forget to prefix URLs that only contain
+    http:// or https:// in the path. Fixes #2975765 reported by Adam Piggott.
+  - Show the 404 CGI page if cgi_send_user_manual() is called while
+    local user manual delivery is disabled.
+
+- Action file improvements:
+  - Enable user.filter by default. Suggested by David White in #3001830.
+  - Block .sitestat.com/. Reported by johnd16 in #3002725.
+  - Block .atemda.com/. Reported by johnd16 in #3002723.
+  - Block js.adlink.net/. Reported by johnd16 in #3002720.
+  - Block .analytics.yahoo.com/. Reported by johnd16 in #3002713.
+  - Block sb.scorecardresearch.com, too. Reported by dg1727 in #2992652.
+  - Fix problems noticed on Yahoo mail and news pages.
+  - Remove the too broad yahoo section, only keeping the
+    fast-redirects exception as discussed on ijbswa-devel@.
+  - Don't block adesklets.sourceforge.net. Reported in #2974204.
+  - Block chartbeat ping tracking. Reported in #2975895.
+  - Tag CSS and image requests with cautious and medium settings, too.
+  - Don't handle view.atdmt.com as image. It's used for click-throughs
+    so users should be able to "go there anyway".
+    Reported by Adam Piggott in #2975927.
+  - Also let the refresh-tags filter remove invalid refresh tags where
+    the 'url=' part is missing. Anonymously reported in #2986382.
+    While at it, update the description to mention the fact that only
+    refresh tags with refresh times above 9 seconds are covered.
+  - javascript needs to be blocked with +handle-as-empty-document to
+    work around Firefox bug 492459.  So move .js blockers from
+    +block{Might be a web-bug.} -handle-as-empty-document to
+    +block{Might be a web-bug.} +handle-as-empty-document.
+  - ijbswa-Feature Requests-3006719 - Block 160x578 Banners.
+  - Block another omniture tracking domain.
+  - Added a range-requests tagger.
+  - Added two sections to get Flickr's Ajax interface working with
+    default pre-settings. If you change the configuration to block
+    cookies by default, you'll need additional exceptions.
+    Reported by Mathias Homann in #3101419 and by Patrick on ijbswa-users@.
+
+- Documentation improvements:
+  - Explicitly mention how to match all URLs.
+  - Consistently recommend socks5 in the Tor FAQ entry and mention
+    its advantage compared to socks4a. Reported by David in #2960129.
+  - Slightly improve the explanation of why filtering may appear
+    slower than it is.
+  - Grammar fixes for the ACL section.
+  - Fixed a link to the 'intercepting' entry and add another one.
+  - Rename the 'Other' section to 'Mailing Lists' and reword it
+    to make it clear that nobody is forced to use the trackers
+  - Note that 'anonymously' posting on the trackers may not always
+    be possible.
+  - Suggest to enable debug 32768 when suspecting parsing problems.
+
+- Privoxy-Log-Parser improvements:
+  - Gather statistics for ressources, methods, and HTTP versions
+    used by the client.
+  - Also gather statistics for blocked and redirected requests.
+  - Provide the percentage of keep-alive offers the client accepted.
+  - Add a --url-statistics-threshold option.
+  - Add a --host-statistics-threshold option to also gather
+    statistics about how many request where made per host.
+  - Fix a bug in handle_loglevel_header() where a 'scan: ' got lost.
+  - Add a --shorten-thread-ids option to replace the thread id with
+    a decimal number.
+  - Accept and ignore: Looks like we got the last chunk together
+    with the server headers. We better stop reading.
+  - Accept and ignore: Continue hack in da house.
+  - Accept and higlight: Rejecting connection from 10.0.0.2.
+    Maximum number of connections reached.
+  - Accept and highlight: Loading actions file: /usr/local/etc/privoxy/default.action
+  - Accept and highlight: Loading filter file: /usr/local/etc/privoxy/default.filter
+  - Accept and highlight: Killed all-caps Host header line: HOST: bestproxydb.com
+  - Accept and highlight: Reducing expected bytes to 0. Marking
+    the server socket tainted after throwing 4 bytes away.
+  - Accept: Merged multiple header lines to: 'X-FORWARDED-PROTO: http X-HOST: 127.0.0.1'
+
+- Code cleanups:
+  - Remove the next member from the client_state struct. Only the main
+    thread needs access to all client states so give it its own struct.
+  - Garbage-collect request_contains_null_bytes().
+  - Ditch redundant code in unload_configfile().
+  - Ditch LogGetURLUnderCursor() which doesn't seem to be used anywhere.
+  - In write_socket(), remove the write-only variable write_len in
+    an ifdef __OS2__ block. Spotted by cppcheck.
+  - In connect_to(), don't declare the variable 'flags' on OS/2 where
+    it isn't used. Spotted by cppcheck.
+  - Limit the scope of various variables. Spotted by cppcheck.
+  - In add_to_iob(), turn an interestingly looking for loop into a
+    boring while loop.
+  - Code cleanup in preparation for external filters.
+  - In listen_loop(), mention the socket on which we accepted the
+    connection, not just the source IP address.
+  - In write_socket(), also log the socket we're writing to.
+  - In log_error(), assert that escaped characters get logged
+    completely or not at all.
+  - In log_error(), assert that ival and sval have reasonable values.
+    There's no reason not to abort() if they don't.
+  - Remove an incorrect cgi_error_unknown() call in a
+    cannot-happen-situation in send_crunch_response().
+  - Clean up white-space in http_response definition and
+    move the crunch_reason to the beginning.
+  - Turn http_response.reason into an enum and rename it
+    to http_response.crunch_reason.
+  - Silence a 'gcc (Debian 4.3.2-1.1) 4.3.2' warning on i686 GNU/Linux.
+  - Fix white-space in a log message in remove_chunked_transfer_coding().
+    While at it, add a note that the message doesn't seem to
+    be entirely correct and should be improved later on.
+
+- GNUmakefile improvements:
+  - Use $(SSH) instead of ssh, so one only needs to specify a username once.
+  - Removed references to the action feedback thingy that hasn't been
+    working for years.
+  - Consistently use shell.sourceforge.net instead of shell.sf.net so
+    one doesn't need to check server fingerprints twice.
+  - Removed GNUisms in the webserver and webactions targets so they
+    work with standard tar.
+
+*** Version 3.0.16 Stable ***
+
+- Added the config file option handle-as-empty-doc-returns-ok to
+  work around Firefox bug #492459, which causes Firefox to hang
+  if JavaScripts are blocked in certain situations. The option is
+  enabled in the default config file.
+- Added the config file option default-server-timeout to control the
+  assumed default server timeout. Since Privoxy no longer returns
+  an error message for connection resets on reused client connections,
+  assuming larger server timeout values appears to actually work
+  pretty well as long as connections aren't shared.
+- Added optional support for FreeBSD's accf_http(9). Use the
+  configure option --enable-accept-filter to enable it.
+- Added fancier Privoxy icons for win32. Contributed by Jeff H.
+- In daemon mode, fd 0, 1 and 2 are bound to /dev/null.
+- Resolve localhost using whatever address family the operating
+  system feels like. Previous betas would try to use IPv4 as this
+  is what most users expect, but this didn't work reliably on
+  GNU/Linux systems.
+- In the action lists on CGI pages, actions and their parameters are
+  no longer separated with a space. The action file parser doesn't
+  actually allow this and will throw an invalid syntax error if actions
+  and parameters in the action files are separated. Not adding the
+  spaces means copy and pasting CGI output into the action files works.
+- The default keep-alive timeout has been reduced to 5 seconds to work
+  around hangs in clients that treat the proxy like any other host and
+  stop allowing any new connections if the "maximum number of
+  connections per host" is reached.
+- Several webbug URLs that look like they are leading to images are now
+  blocked as image instead of empty documents. Doing the latter causes
+  WebKit-based clients to show a "missing image" icon which may mess up
+  the layout.
+- The no-such-domain template is used for DNS resolution
+  problems with FEATURE_IPV6_SUPPORT enabled. Previously the
+  connect-failed template was used. Reported by 'zebul666'.
+- Accepts quoted expiration dates even though RFC 2109 10.1.2
+  doesn't seem to allow them. Reported anonymously.
+- Don't try to forget connections if connection sharing is disabled.
+  This wasn't a real problem but caused an unnecessary log message.
+- The still undocumented --enable-extended-host-patterns configure
+  option has a better description.
+- Fixed an error message that would claim a write to the server
+  failed when actually writing to the client failed.
+- Log the crunch reason before trying to write to the client.
+  The log is easier to read that way.
+- Several log messages about client connections also mention
+  the socket number.
+- handle-as-empty-document no longer depends on the image blocking
+  code being enabled.
+- Privoxy-Log-Parser is roughly 40% faster in highlighting mode.
+- uagen, a Firefox User-Agent generator for Privoxy and Mozilla
+  browsers has been imported and is available in the tarball's
+  tools directory.
+- The scripts in the tools directory treat unknown parameters
+  as fatal errors.
+
+*** Version 3.0.15 beta ***
+
+- In case of missing server data, no error message is send to the
+  client if the request arrived on a reused connection. The client
+  is then supposed to silently retry the request without bothering
+  the user. This should significantly reduce the frequency of the
+  "No server or forwarder data received" error message many users
+  reported.
+- More reliable detection of prematurely closed client sockets
+  with keep-alive enabled.
+- FEATURE_CONNECTION_KEEP_ALIVE is decoupled from
+  FEATURE_CONNECTION_SHARING and now available on
+  all platforms.
+- Improved handling of POST requests on reused connections.
+  Should fix problems with stalled connections after submitting
+  form data with some browser configurations.
+- Fixed various latency calculation issues.
+- Allows the client to pass NTLM authentication requests to a
+  forwarding proxy. This was already assumed and hinted to work
+  in 3.0.13 beta but actually didn't. Now it's confirmed to work
+  with IE, Firefox and Chrome.
+  Thanks to Francois Botha and Wan-Teh Chang
+- Fixed a calculation problem if receiving the server headers
+  takes more than two reads, that could cause Privoxy to terminate
+  the connection prematurely. Reported by Oliver.
+- Compiles again on platforms such as OpenBSD and systems
+  using earlier glibc version that don't support AI_ADDRCONFIG.
+  Anonymously submitted in #2872591.
+- A bunch of MS VC project files and Suse and Redhat RPM spec
+  files have been removed as they were no longer maintained for
+  quite some time.
+- Overly long action lines are properly rejected with a proper
+  error message. Previously they would be either rejected as
+  invalid or cause a core dump through abort().
+- Already timed-out connections are no longer temporarily remembered.
+  They weren't reused anyway, but wasted a socket slot.
+- len refers to the number of bytes actually read which might
+  differ from the ones received. Adjust log messages accordingly.
+- The optional JavaScript on the CGI page uses encodeURIComponent()
+  instead of escape() which doesn't encode all characters that matter.
+  Anonymously reported in #2832722.
+- Fix gcc45 warnings in decompress_iob().
+- Various log message improvements.
+- Privoxy-Regression-Test supports redirect tests.
+- Privoxy-Log-Parser can gather some connection statistics.
+
+*** Version 3.0.14 beta ***
+
+- The latency is taken into account when evaluating whether or not to
+  reuse a connection. This should significantly reduce the number of
+  connections problems several users reported.
+- If the server doesn't specify how long the connection stays alive,
+  Privoxy errs on the safe side of caution and assumes it's only a second.
+- The error pages for connection timeouts or missing server data use a
+  Last-Modified date in the past. Retry attempts are detected and Privoxy
+  removes the If-Modified-Since header to prevent the server from responding
+  with status code 304 in which case the client would reuse the error message.
+- Setting keep-alive-timeout to 0 disables keep-alive support. Previously
+  Privoxy would claim to allow persistence but not reuse the connection.
+- Pipelined requests are less likely to be mistaken for the request
+  body of the previous request. Note that Privoxy still has no real
+  pipeline support and will either serialize pipelined requests or
+  drop them in which case the client has to resent them.
+- Fixed a crash on some Windows versions when header randomization
+  is enabled and the date couldn't be parsed.
+- Privoxy's keep-alive timeout for the current connection is reduced
+  to the one specified in the client's Keep-Alive header.
+- For HTTP/1.1 requests, Privoxy implies keep-alive support by not
+  setting any Connection header instead of using 'Connection: keep-alive'.
+- If the socket isn't reusable, Privoxy doesn't temporarily waste
+  a socket slot to remember the connection.
+- If keep-alive support is disabled but compiled in, the client's
+  Keep-Alive header is removed.
+- Fixed a bug on mingw32 where downloading large files failed if
+  keep-alive support was enabled.
+- Fixed a bug that (at least theoretically) could cause log
+  timestamps to be occasionally off by about a second.
+- No Proxy-Connection header if added if there already is one.
+- The configure script respects the $PATH variable when searching
+  for groups and id.
+
+*** Version 3.0.13 beta ***
+
+- Added IPv6 support. Thanks to Petr Pisar who not only provided
+  the initial patch but also helped a lot with the integration.
+- Added client-side keep-alive support.
+- The connection sharing code is only used if the connection-sharing
+  option is enabled.
+- The max-client-connections option has been added to restrict
+  the number of client connections below a value enforced by
+  the operating system.
+- Fixed a regression reintroduced in 3.0.12 that could cause
+  crashes on mingw32 if header date randomization was enabled.
+- Compressed content with extra fields couldn't be decompressed
+  and would get passed to the client unfiltered. This problem
+  has only be detected through statical analysis with clang as
+  nobody seems to be using extra fields anyway.
+- If the server resets the Connection after sending only the headers
+  Privoxy forwards what it got to the client. Previously Privoxy
+  would deliver an error message instead.
+- Error messages in case of connection timeouts use the right
+  HTTP status code.
+- If spawning a child to handle a request fails, the client
+  gets an error message and Privoxy continues to listen for
+  new requests right away.
+- The error messages in case of server-connection timeouts or
+  prematurely closed server connections are now template-based.
+- If zlib support isn't compiled in, Privoxy no longer tries to
+  filter compressed content unless explicitly asked to do so.
+- In case of connections that are denied based on ACL directives,
+  the memory used for the client IP is no longer leaked.
+- Fixed another small memory leak if the client request times out
+  while waiting for client headers other than the request line.
+- The client socket is kept open until the server socket has
+  been marked as unused. This should increase the chances that
+  the still-open connection will be reused for the client's next
+  request to the same destination. Note that this only matters
+  if connection-sharing is enabled.
+- A TODO list has been added to the source tarball to give potential
+  volunteers a better idea of what the current goals are. Donations
+  are still welcome too: https://www.privoxy.org/faq/general.html#DONATE
+
+*** Version 3.0.12 ***
+
+- The socket-timeout option now also works on platforms whose
+  select() implementation modifies the timeout structure.
+  Previously the timeout was triggered even if the connection
+  didn't stall. Reported by cyberpatrol.
+- The Connection: keep-alive code properly deals with files
+  larger than 2GB. Previously the connection was closed too
+  early.
+- The content length for files above 2GB is logged correctly.
+- The user-manual directive on the show-status page links to
+  the documentation location specified with the directive,
+  not to the Privoxy website.
+- When running in daemon mode, Privoxy doesn't log anything
+  to the console unless there are errors before the logfile
+  has been opened.
+- The show-status page prints warnings about invalid directives
+  on the same line as the directives themselves.
+- Fixed several justified (but harmless) compiler warnings,
+  mostly on 64 bit platforms.
+- The mingw32 version explicitly requests the default charset
+  to prevent display problems with some fonts available on more
+  recent Windows versions. Patch by Burberry.
+- The mingw32 version uses the Privoxy icon in the alt-tab
+  windows. Patch by Burberry.
+- The timestamp and the thread id is omitted in the "Fatal error"
+  message box on mingw32.
+- Fixed two related mingw32-only buffer overflows. Triggering
+  them required control over the configuration file, therefore
+  this isn't seen as a security issue.
+- In verbose mode, or if the new option --show-skipped-tests
+  is used, Privoxy-Regression-Test logs skipped tests and the
+  skip reason.
+
+*** Version 3.0.11 ***
+
+- On most platforms, outgoing connections can be kept alive and
+  reused if the server supports it. Whether or not this improves
+  things depends on the connection.
+- When dropping privileges, membership in supplementary groups
+  is given up as well. Not doing that can lead to Privoxy running
+  with more rights than necessary and violates the principle of
+  least privilege. Users of the --user option are advised to update.
+  Thanks to Matthias Drochner for reporting the problem,
+  providing the initial patch and testing the final version.
+- Passing invalid users or groups with the --user option
+  didn't lead to program exit. Regression introduced in 3.0.7.
+- The match all section has been moved from default.action
+  to a new file called match-all.action. As a result the
+  default.action no longer needs to be touched by the user
+  and can be safely overwritten by updates.
+- The standard.action file has been removed. Its content
+  is now part of the default.action file.
+- In some situations the logged content length was slightly too low.
+- Crunched requests are logged with their own log level.
+  If you used "debug 1" in the past, you'll probably want
+  to additionally enable "debug 1024", otherwise only passed
+  requests will be logged. If you only care about crunched
+  requests, simply replace "debug 1" with "debug 1024".
+- The crunch reason has been moved to the beginning of the
+  crunch message. For HTTP URLs, the protocol is logged as well.
+- Log messages are shortened by printing the thread id on its
+  own (as opposed to putting it inside the string "Privoxy()").
+- The config option socket-timeout has been added to control
+  the time Privoxy waits for data to arrive on a socket.
+- Support for remote toggling is controlled by the configure
+  option --disable-toggle only. In previous versions it also
+  depended on the action editor and thus configuring with the
+  --disable-editor option would disable remote toggling support
+  as well.
+- Requests with invalid HTTP versions are rejected.
+- The template symbol @date@ can be used to include a date(1)-like
+  time string. Initial patch submitted by Endre Szabo.
+- Responses from shoutcast servers are accepted again.
+  Problem reported and fix suggested by Stefan.
+- The hide-forwarded-for-headers action has been replaced with
+  the change-x-forwarded-for{} action which can also be used to
+  add X-Forwarded-For headers. The latter functionality already
+  existed in Privoxy versions prior to 3.0.7 but has been removed
+  as it was often used unintentionally (by not using the
+  hide-forwarded-for-headers action).
+- A "clear log" view option was added to the mingw32 version
+  to clear out all of the lines in the Privoxy log window.
+  Based on a patch submitted by T Ford.
+- The mingw32 version uses "critical sections" now, which prevents
+  log message corruption under load. As a side effect, the
+  "no thread-safe PRNG" warning could be removed as well.
+- The mingw32 version's task bar icon is crossed out and
+  the color changed to gray if Privoxy is toggled off.
+
+*** Version 3.0.10 ***
+
+- Ordinary configuration file changes no longer cause program
+  termination on OS/2 if the name of the logfile hasn't been
+  changed as well. This regression probably crept in with the
+  logging improvements in 3.0.7. Reported by Maynard.
+- The img-reorder filter is less likely to mess up JavaScript code in
+  img tags. Problem and solution reported by Glenn Washburn in #2014552.
+- The source tar ball now includes Privoxy-Log-Parser,
+  a syntax-highlighter for Privoxy logs. For fancy screenshots see:
+  http://www.fabiankeil.de/sourcecode/privoxy-log-parser/
+  Documentation is available through perldoc(1).
+
+*** Version 3.0.9 beta ***
+
+- Added SOCKS5 support (with address resolution done by
+  the SOCKS5 server). Patch provided by Eric M. Hopper.
+- The "blocked" CGI pages include a block reason that was
+  provided as argument to the last-applying block action.
+- If enable-edit-actions is disabled (the default since 3.0.7 beta)
+  the show-status page hides the edit buttons and explains why.
+  Previously the user would get the "this feature has been disabled"
+  message after using the edit button.
+- Forbidden CONNECT requests are treated like blocks by default.
+  The now-pointless treat-forbidden-connects-like-blocks action
+  has been removed.
+- Not enabling limit-connect now allows CONNECT requests to all ports.
+  In previous versions it would only allow CONNECT requests to port 443.
+  Use +limit-connect{443} if you think you need the old default behaviour.
+- The CGI editor gets turned off after three edit requests with invalid
+  file modification timestamps. This makes life harder for attackers
+  who can leverage browser bugs to send fake Referers and intend to
+  brute-force edit URLs.
+- Action settings for multiple patterns in the same section are
+  shared in memory. As a result these sections take up less space
+  (and are loaded slightly faster). Problem reported by Franz Schwartau.
+- Linear white space in HTTP headers will be normalized to single
+  spaces before parsing the header's content, headers split across
+  multiple lines get merged first. This should prevent problems like:
+   * letting the session-cookies-only action slip
+     some Cookies through unmodified,
+   * only suppressing the first line of a header,
+     thus creating an invalid one, and
+   * to incorrectly block headers with valid timestamps
+     that weren't properly recognized.
+  Headers that could trigger these problems are unlikely to appear
+  in "normal" web traffic, but could be intentionally generated to
+  fool some of Privoxy's header parsers.
+- Host information is gathered outside the main thread so it's less
+  likely to delay other incoming connections if the host is misconfigured.
+- New config option "hostname" to use a hostname other than
+  the one returned by the operating system. Useful to speed-up responses
+  for CGI requests on misconfigured systems. Requested by Max Khon.
+- The CGI editor supports the "disable all filters of this type"
+  directives "-client-header-filter", "-server-header-filter",
+  "-client-header-tagger" and "-server-header-tagger".
+- Fixed false-positives with the link-by-url filter and URLs that
+  contain the pattern "/jump/".
+- The less-download-windows filter no longer messes
+  "Content-Type: application/x-shockwave-flash" headers up.
+- In the show-url-info page's "Final results" section active and
+  inactive actions are listed separately. Patch provided by Lee.
+- The GNUmakefile supports the DESTDIR variable. Patch for
+  the install target submitted by Radoslaw Zielinski.
+- Embedding the content of configuration files in the show-status
+  page is significantly faster now. For a largish action file (1 MB)
+  a speedup of about 2450 times has been measured. This is mostly
+  interesting if you are using large action files or regularly use
+  Privoxy-Regression-Test while running Privoxy through Valgrind,
+  for stock configuration files it doesn't really matter.
+- If zlib support is unavailable and there are content
+  filters active but the prevent-compression action is disabled,
+  the show-url-info page includes a warning that compression
+  might prevent filtering.
+- The show-url-info page provides an OpenSearch Description that
+  allows to access the page through browser search plugins.
+- Custom client-header filters that rewrite the request line
+  incorrectly no longer cause Privoxy to crash. Reported by din_a4.
+- The obsolete kill-popups action has been removed as the
+  PCRS-based popup filters can do the same and are slightly
+  less unreliable.
+- The inspect-jpegs action has been removed.
+- The send-wafer and send-vanilla-wafer actions have been removed.
+  They weren't particular useful and their behaviour could be emulated
+  with add-header anyway.
+- Privoxy-Regression-Test has been significantly improved.
+- Most sections in the default.action file contain tests for
+  Privoxy-Regression-Test to verify that they are working as intended.
+- Parts of Privoxy have been refactored to increase maintainability.
+- Building with zlib (if available) is done by default.
+
+*** Version 3.0.8 ***
+
+- Fixed a small memory leak when listen-address only specifies the port.
+- The source tar balls now include Privoxy-Regression-Test which
+  (upon other things) can be used to automatically detect some
+  packaging problems. Packagers are welcome to give it a try.
+- Reverted a change in 3.0.7 that caused path patterns to be checked
+  even if the host pattern match already failed. While this doesn't
+  noticeable affect the performance, it makes it less likely to run
+  out of stack space with overly-complex path patterns the user might
+  have added.
+- Updated the msn, yahoo and google filters to work as advertised again.
+- The warning message shown by the show-status CGI page is easier to
+  understand. Previously it wasn't clear that the error message
+  is shown below the invalid directive. (Reported by Lee)
+- When regenerating Content-Disposition headers the more common
+  spelling is used for the name. Previously it was written without caps.
+- Less confusing log message if the content type isn't overwritten
+  because force-text-type wasn't used but the old type doesn't look
+  like content that would be filtered normally.
+- Better log messages if the user tries to execute filters that
+  don't exist.
+- Treat the non-standard Request-Range headers like standard range
+  headers and suppress them if content filtering is enabled.
+- Prevent the log messages for CONNECT requests to unacceptable
+  ports from printing the limit-connect argument as [null] if
+  limit-connect hasn't been explicitly enabled.
+- Don't disable the mingw32 log window if the logfile directive
+  isn't used. While it was an intentional change in 3.0.7 at least
+  one user perceived it as a regression and the same effect can
+  be achieved by disabling all debug directives.
+- Fixed two minor problems related to the win32 build process: a css
+  file was not being in the installer and the trustfile comment in the
+  config.txt referenced a nonexisting file
+- Minor documentation fixes.
+
+*** Version 3.0.7 beta ***
+
+- Added zlib support to filter content with gzip and deflate
+  encoding. (Patch provided by Wil Mahan)
+- Dedicated filters and actions are used for header filtering.
+  "filter-client-headers" and "filter-client-headers" are no longer
+  supported, use server-header-filter{} and client-header-filter{}
+  instead.
+- Tags can be used to change actions based on HTTP headers.
+- New server-header filter: less-download-windows.
+- New client-header taggers: css-requests, image-requests,
+  client-ip-address, http-method, allow-post, complete-url,
+  user-agent and privoxy-control.
+- New server-header taggers: content-type and privoxy-control.
+- The forward-override{} action allows to change the forwarding
+  settings through the action files, for example based on client
+  headers like the User-Agent, or the request origin.
+- Socks errors are no longer handled by the CGI page for
+  DNS resolution failures.
+- CGI pages use favicons to signal whether they are error
+  or control pages. This is useful if you rely heavily on
+  browser tabs.
+- The show-url-info CGI page shows the forwarding settings.
+- "Crunch!" log messages (used when Privoxy answers requests
+  by itself) now also contain the reason.
+- Allow to rewrite the request destination behind the client's back.
+- Fix socks requests on big-endian platforms. Patch provided by Song Weijia.
+- Fixes possible deadlocks and crashes on OpenBSD.
+  Patch provided by Ralf Horstmann.
+- The CGI action editor allows to edit actionfiles with previously
+  forbidden characters like dots.
+- New trust entries are saved with a comment that contains the
+  trusted referring URL (Suggested by Daniel Griscom).
+- Filter descriptions are HTML encoded automatically.
+- New config option "split-large-forms" to work
+  around a browser bug that caused IE6 and IE7 to ignore
+  the Submit button on the edit-actions-for-url CGI page.
+- New config option "allow-cgi-request-crunching" to allow
+  requests for Privoxy's CGI pages to be blocked, redirected
+  or (un)trusted like ordinary requests.
+- Empty filter files no longer interrupt the filtering process
+  prematurely and are correctly listed on the show-status CGI page.
+- New config option "accept-intercepted-requests" to combine
+  Privoxy with any packet filter to build an intercepting proxy
+  for HTTP/1.1 requests (and for HTTP/1.0 requests with Host header set).
+- fast-redirects{} catch redirects to https URLs as well.
+- redirect{s@foo@bar@} can be used to redirect to a rewritten
+  version of the original URL.
+- Trap unsupported gopher proxy requests.
+- Fixed a bug in the User Manual delivery on Windows
+  (mingw32 only). Images now show up correctly and HTML
+  pages are no longer padded with garbage data.
+- Fixed several minor memory leaks, most of them discovered with Valgrind.
+- Only unlink the pidfile if it's actually used.
+- Retries after connection problems with forced requests
+  aren't blocked again.
+- On Unix SIGABRT causes a core dump as expected and is no
+  longer treated as normal shutdown signal.
+- The "access denied" CGI page is more descriptive and
+  allows retries to circumvent the referrer check.
+- Updated PCRS to handle unexpected PCRE errors properly.
+  Fixed crashes that could occur if Privoxy was build
+  with external PCRE versions newer than Privoxy's internal
+  one. (Reported by Chung-chieh Shan)
+- Fixed crashes with null bytes in PCRS replacement strings
+  (Patch provided by Felix Gröbert).
+- Fixed crashes with header time randomization on mingw32.
+- The CGI style sheet is no longer delivered if the referring
+  page isn't a Privoxy CGI page. This prevents a JavaScript-based
+  Privoxy detection "attack". Note that detecting Privoxy is
+  still possible through other ways and Privoxy was never intended
+  to be invisible anyway.
+- Added support for AmigaOS 4, fixed build for AmigaOS 3.x.
+- The show-url-info CGI page displays a warning if Privoxy
+  is currently toggled off.
+- The show-status CGI page suppresses the edit button
+  for action files if Privoxy has no write access.
+- Most CGI error pages react properly to HEAD requests.
+- Requests with RFC 3253 HTTP methods (used by Subversion)
+  are accepted. (Patch provided by Petr Kadlec)
+- New config option "templdir" to change the location
+  of the CGI templates to make sure customized templates
+  aren't "updated".
+- Better handling of "HTTP/1.1 100 Continue" responses.
+- The background of the PNG pattern is transparent.
+- Fixed XML syntax errors caused by banners-by-size and banners-by-url.
+- Fixed crashes and possible action file corruptions
+  when lines containing hashes are written through the CGI editor.
+- Supports dynamic filters which can contain variables.
+- Supports tags to change the actions based on client or server headers.
+- Incorrect actions are logged before program termination.
+- The "actionsfile" syntax in the configuration file is consistent
+  with the rest of the configuration options and requires the
+  whole file name. This is an incompatible change, if you use
+  an old configuration file you might have to append ".action"
+  to your "actionsfile" directives.
+- With the configuration file option "enforce-blocks" the
+  "go there anyway" mechanism can be disabled without recompiling
+  Privoxy.
+- More precise error messages in case of incorrect acl syntax.
+- Logs a warning if filtering is enabled but impossible due
+  to lack of zlib support or use of the prevent-compression action.
+- Less noisy handling of Cookie:" and "Connection:" headers.
+- Improved error messages in case of connection problems.
+- Fix a command-line-parsing bug that was introduced before 3.0.5
+  beta and caused Privoxy to treat the last argument as configuration
+  file if no configuration file was specified.
+- Treat unknown command line options as fatal errors instead
+  of silently ignoring them.
+- Use string functions with length checks more often.
+- Don't log CONNECT requests twice.
+- Allow to log the source address for ACL-related connection drops.
+- Don't ignore applying filters if the server didn't
+  specify a Content-Type. Bug reported by Amuro Namie.
+- Rejected CONNECT requests are logged with log level info
+  (enabled by default) and the reason for the block.
+- New command line option "--pre-chroot-nslookup hostname" to
+  intialize the resolver library before chroot'ing. On some systems this
+  reduces the number of files that must be copied into the chroot tree.
+  (Patch provided by Stephen Gildea)
+- Fix a long-standing memory corruption bug that could cause
+  Privoxy to overwrite a single byte in memory it didn't explicitly
+  allocate (but that probably was allocated anyway due to bucket size).
+- Send template-based CGI pages as HTTP/1.1 unless the client
+  asked for HTTP/1.0.
+- Let the first line in connection established responses
+  end in \r\n as required by RFC1945. Reported by Bert van Leeuwen.
+- If no log file has been specified, disable logging instead of logging
+  to stderr.
+- Don't block stderr when in daemon mode.
+- Ignore missing zero-chunks when filtering chunk-encoded content.
+  Earlier Privoxy versions would buffer and then forward the content
+  unmodified which caused some browsers to simply show empty pages.
+- Fix double free in cgi_edit_actions_list(). Reported by Venustech AD-LAB.
+- The code to add X-Forwarded-For headers when the hide-forwarded-for-headers
+  action isn't being used has been removed.
+- Fixed trustfile feature which previously didn't work without FEATURE_TOGGLE.
+  Reported by Lee.
+- Minor code clean-ups, filter and action file updates.
+  (Some of them reported by Davide Alberani, Markus Elfring,
+   Stefan Huehner and Adam Piggott)
+
+*** Version 3.0.6 ***
+
+- New content filters: no-ping, google, msn, yahoo and blogspot.
+- New header filters:  x-httpd-php-to-html, html-to-xml, xml-to-html
+                       and hide-tor-exit-notation.
+- The special header "X-Filter: No" now disables header filtering as well.
+- Improved the filters img-reorder, js-annoyances, webbugs,
+  banners-by-size, banners-by-link and ie-exploits to make them
+  less likely to break anything.
+- Removed outdated URL patterns in default.action and added new ones.
+- Added redirection from http://p.p/user-manual to http://p.p/user-manual/
+- Changed webinterface default values for hide-user-agent, hide-referrer
+  and set-image-blocker.
+
+*** Version 3.0.5 beta ***
+
+- Windows version can be installed/started as a service.
+- Windows icon stays blue when Privoxy is idle, green when busy.
+- Integrated Fabian Keil's extensive patch.  See:
+  http://www.fabiankeil.de/sourcecode/privoxy/. Includes the
+  following new or significantly improved actions (among many
+  other improvements):
+
+     content-type-overwrite{}
+     crunch-client-header{string}
+     crunch-if-none-match
+     crunch-server-header{string}
+     fast-redirects{check-decoded-url}
+     filter-client-headers
+     filter-server-headers
+     force-text-mode
+     handle-as-empty-document
+     hide-accept-language{}
+     hide-content-disposition{}
+     hide-if-modified-since
+     hide-referrer{conditional-block}
+     overwrite-last-modified{}
+     redirect{URL}
+     treat-forbidden-connects-like-blocks
+
+- Standard-compliant clients are prevented from displaying cached
+  copies of Privoxy's error messages after the cause of the problem
+  has gone.
+- Improved DNS error handling.
+- Multiple filter files can now be specified in config.
+- Added jpeg filtering to defend against MS jpeg vulnerability MS04-028
+  with the new inspect-jpegs action.
+- Removed the "arbitrary" 1000 filter limit - addresses tracker #911950
+- Thanks to Jindrich Makovicka for a race condition fix for the log
+  file.  The race condition remains for non-pthread implementations.
+  Reference patch #1175720. Various other logging enhancements.
+- A pile of assorted bug fixes, memory leaks, enhancements, etc.
+- Moved Actions file reporting mechanism to SF tracker.
+- Two new options for config: enable-remote-http-toggle and
+  forwarded-connect-retries.
+- Trap unsupported FTP requests.
+- Let text/xml be filtered.
+- Numerous updates to default.action
+- Increase the compiled in limit of trusted referrers from 64 to 512
+  (for trustfile users).
+
+*** Version 3.0.3 ***
+
+- Fixed yet another two memory leaks. Process growth seems stopped now.
+- Further tightened security against malicious toggle-off links.
+- Excluded text/plain MIME types from filtering. This fixes a
+  couple of client-crashing, download corruption and
+  Privoxy performance issues, whose root cause lies in
+  web servers labelling content of unknown type as text/plain.
+- Assorted fixes for POSIX compliance, signal handling, graceful
+  termination, compiler warnings, OSX support, Win32 systray,
+  error logging, hostname wildcards, correct detection of NetBSD.
+- Workarounds for client (iTunes etc) and server (PHP < 4.2.3) bugs
+  including the notorious "blank page" problem.
+- Various filter improvements; most notably the unsolicited-popups
+  filter became less destructive
+- Major revamp of the actions file
+
+*** Version 3.0.2 ***
+
+- Fixed two memory leaks, one serious
+- Fixed bug in pcrs which could cause crashes with user-defined filters
+- Fixed bug in domain name matching
+- Assorted small fixes (Win32 menu, CGI URL editor, ..)
+- Added basic support for the OPTIONS and TRACE http methods
+- Added workaround for Bug in Mac OSX that made Privoxy crash occasionally
+- Refined the default action file through >400 items of user feedback
+- Filter changes:
+  - Assorted refinements, optimizations and fixes in the js-annoyances,
+    img-reorder, banners-by-size, banners-by-link, webbugs, refresh-tags,
+    html-annoyances, content-cookies and fun filters
+  - Replaced filter "popups" by choice between two modes:
+    - "unsolicited-popups" tries to catch only the unsolicited ones
+    - "all-popups" tries to kill them all (as before)
+  - New filter "tiny-textforms" Help those tiny or hard-wrap textareas.
+  - New filter "jumping-windows" that prevents windows from resizing
+    and moving themselves
+  - New filter "demoronizer" which fixes MS's abuse of std charsets
+    (common cases anyway).
+  - Replaced "nimda" with more general "ie-exploits" filter in which
+    all filters for exploits shall be collected
+- Improved cookie logging
+- Rewrote make install target. Added uninstall and install-strip
+  targets.
+- Fixed a potential (application-level, NOT OS-level!) security
+  problem involving remote toggling and action file manipulation
+  by mailicious websites.
+- Added ability to chroot (thanks to Sviatoslav Sviridov)
+- Added more action aliases for prehistoric action names
+- Add Slackware support to Makefile.
+
+*** Version 3.0  ***
+
+- Fixed Windows startmenu items, log window and tray icon menus.
+- Added warning for bogus install target
+- Added quicktime-kioskmode filter and improved frameset-borders
+- Updated default.action based on latest feedback
+- New PDF doc build process
+- Add a user contrib module to cvs:
+  http://cvs.sourceforge.net/cgi-bin/viewcvs.cgi/ijbswa/contrib/
+
+*** Version 2.9.18  ***
+
+- Added workaround for IE bug that broke CGI interface
+- Bugfix: String actions now reliably editable through CGI interface
+- Three filters fixed (again!)
+- Assorted small fixes and doc enhancements
+
+*** Version 2.9.16  ***
+
+- Major revamp of default.action to get rid of years of cruft.
+- Same for default.filter
+- Re-design and major improvements to the CGI editor interface.
+- Address spurious 'out of memory' error due to incorrect file permissions.
+- Impose buffer limits while reading client and server headers.
+- Better memory and CPU optimization.
+- Add Conectiva Linux package.
+- user-manual directive added to config for help links from within CGI
+  editor.
+- Multiple actions files can now be specified in config.
+- Actions files are changed to: default.action, standard.action, and
+  user.action. user.action is for personal/local configuration.
+- The usual many small and miscellaneous bug and security fixes.
+
+*** Version 2.9.14 beta ***
+
+- Fix Solaris compile problem (gateway.h and filters.h)
+- Makefile fixes for Solaris, FreeBSD (?)
+- Fix build failure where certain features were disabled.
+- 'blocked-compact' template is removed. Various CGI improvements,
+  including an adaptive 'blocked' template.
+- Various tweaks for actions file to get ready for stable 3.0
+- Included a 'Bookmarklet' and PHP scripts for reporting actions file
+  problems via web interface at privoxy.org. Accessed via internal CGIs.
+- Include cgi-style.css for templates.
+- #include mechansim for common text in templates
+- Various other minor fixes.
+
+*** Version 2.9.13 beta ***
+
+- *NEWS*: The project has been renamed to Privoxy! The new name is
+  reflected throughout (file locations, etc).
+- ijb.action is now default.action. re_filterfile is now
+  default.filter.
+- http://i.j.b/ is now http://p.p/
+- The 'logo' option for replacing ad iamges is removed now. 'Pattern'
+  (checkerboard) is now the default.
+- RPM spec file make over.
+
+
+*** Version 2.9.12 beta ***
+
+- **READ**: The default listening PORT is NOW 8118!!! Changed from
+  8000 due to conflict with NAS (Network Audio Server, whatever that
+  is.)
+- More CGI actions editor fixes and improvements.
+- Win32 command line fix ups.
+- re_filterfile now has modular sections that can be activated on a
+  per site basis. Some new goodies there too.
+- +filter now takes arguments to match FILTER sections in re_filterfile
+  for even more flexibility.
+- Added a new image blocker option: +image-blocker{pattern}, which
+  displays a checkerboard patthern and scales better than the logo.
+- PNG images will be used in place of GIF for JB built-in images
+  if configured with --enable-no-gif.
+- Clean up compiler warnings (mostly).
+- Improved handling of failed DNS lookups & diagnostics for failed bind
+  to listen socket
+- Made --no-daemon mode log to tty instead of logfile.
+- Various spec file and init script cleanups and improvements (Redhat and
+  SuSE).
+- CGI Editor works on OS/2 now.
+- Fix restart failure where sockets were in TIME_WAIT.
+- Fixes for actions cgi editor, make sure we have right file.
+- A --pidfile command line option now, in addition to --help,
+  --version, --no-daemon, --user and configfile. --no-daemon replaces
+  the former -d option and _DEBUG define. --user will drop privileges
+  to the specified user.
+- Signal handling cleanups (*nix).
+- CGI actions editor improvements and fixes.
+- Error handling improvements, especially out of memory.
+- Default re_filterfile fix that caused spurious IJB logos
+  (instead of 'blank').
+- configure.in threading fixes for Solaris.
+- Various other minor fixes.
+
+
+*** Version 2.9.11 beta Changes ***
+
+- Add "session" cookie concept where cookies exist for the life
+  of that browser session only (ie never goes to disk).
+- Checks for correct header length.
+- Fix user:pass@host.domain.com auth bug.
+- Better signal handling on *nix.
+- Fix CFLAGS hard-coded in configure.in
+- Fix threading bug re: gethostbyname() that caused random
+  URLs to fail in some cases.
+
+
+*** Version 2.9.11 Alpha Changes ***
+
+- A web-based editor for the actions file is included (go to http://i.j.b/).
+- Web-based toggle IJB on/off support.
+- Cookie handling has changed - the new +no-cookies-keep feature is now the
+default.
+- actionsfile is renamed to ijb.action.
+- junkbstr.txt is now config.txt on Win32.
+- Support for running IJB as a UNIX daemon process has improved.
+- Unix daemon now returns error code on failed start.
+- Timestamps in logfile and jarfile now.
+- Fix for the Netscape bug reintroduced in 2.9.9.
+- make should now abort if gmake (GNU make) not present.
+- Many other minor bugfixes
+- Start a ChangeLog :)
+
+
+
+*** Version 2.9.3 pre-Alpha Changes ***
+
+- Amiga support (completely untested by me - I don't have an Amiga)
+- "tinygif 3" support (redirects blocked images to a specified URL, so
+the browser doesn't have to load and cache many copies of the same
+image).
+- one case where there were both local and global "referrer" variables
+(yuck!) clarified by renaming the local one to "refer".
+- Fixed some places where close() was used instead of close_socket().
+Thanks to Jörg Strohmayer (joergs at users.sourceforge.net) for these.
+- Temporary hack to get FORCE_LOAD to work with IE.  I just lowercased the
+FORCE_LOAD_PREFIX.  Needs fixing properly.
+- Most URLs hardcoded into Junkbuster were changed to go through a script
+e.g. http://ijbswa.sourceforge.net/redirect.php?v=2.9.3&to=faq
+The only other URLs left are the GNU GPL:
+  http://www.fsf.org/copyleft/gpl.html
+and the home page:
+  http://ijbswa.sourceforge.net/
+... and various URLs which will be intercepted by Junkbuster anyway.
+TODO: Still need to do something with the URLs in Junkbuster Corp's
+copyright/trademark notice on the bottom of the show-proxy-args page.
+- PCRE or GNU Regex is now a #define option.
+
+
+*** Version 2.9.2 pre-Alpha Changes ***
+
+- Andreas applied the latest version of the FORCE patch.
+
+
+*** Version 2.9.1 pre-Alpha Changes ***
+
+- in parsers.c, fixed two #ifdef FORCE to #ifdef FORCE_LOAD
+(BTW: I think FORCE is precise enough, since loading remote
+data is the whole purpose of a proxy..)
+- Set the FORCE_PREFIX (back) to 'IJB-FORCE-LOAD-'. While 'noijb.'
+is more elegant and looks like a hostname in the URL, it doesn't
+make clear to the inexperienced user that the proxy is bypassed. It
+also has a higher name collision risk.
+- Filled in the function header templates for my functions in
+parsers.c (again). They obviously got lost in our current
+patch war ;-)
+- Cut the credit for the §-referrer-option from the config file,
+that Stefan had placed there.
+- Improved the re_filterfile
+
+
+*** Version 2.9.0 pre-Alpha Changes ***
+
+-  Now use PCRE, not GNU REGEX.  I have not yet had chance to check the
+syntax of the block/image/cookie file to ensure that they match what
+is expected - however they seem to work.
+-  Replaced "configure" script with one generated by "autoconf".  Also
+use a header "config.h" (was ijbconfig.h in my previous release) for
+the #defines.  "config.h" is now generated with "autoheader" from
+"acconfig.h" and "configure.in".  (Note that to install you do not
+need autoconf or autoheader - just run "./configure".)
+To see command-line options, run "./configure --help".
+This is my first ever autoconf script, so it has some rough edges
+(how PCRE is handled is the roughest).
+-  Error logging code replaced with new module errlog.c, based on the
+one from JunkbusterMT (but with the threading code removed).
+-  Most of Rodney's 0.21 and 0.21A patches applied. (Marked *).  I did not
+apply all of these, since I had already independently done conditional
+popup file, conditional image file, and integration of popup code.
+- ACL, Jar and trust files conditionally compiled.
+- New source file headers.
+- Various cosmetic changes.  (But I have not consistently ordered the
+config files - I think that's worthwhile, but it's 1am and I want to
+get this released!)
+- RCS tags on .h files.
+-  RCS tags are const char[] rather than const char *.  (Saves 4 bytes
+per tag ;-)
+- VC++ project files renamed to vc_junkbuster.*.
+- show-proxy-args now shows status of all conditionals, not just REGEX
+- Various functions moved around.  Most notably all the system-specific
+sockets code which was spread between jcc.c, bind.c, and connect.c,
+has been moved to "jbsockets.c".  The non-system-specific code from
+connect.c and socks4.c has been movet to "gateway.c".  Also, the
+config file loader and the global variables it writes to have been
+moved to "loadcfg.c".  (Maybe this should go into loaders.c?)
+And candidate for the "worst filename ever" award is "miscutil.c",
+which contains, well, miscellaneous utility functions like zalloc.
+(Suggestions for a better name for this file are welcome!)
+- Loaders now use a common function to read a line and skip comments,
+and this function also stores the proxy_args.
+- Added ./junkbuster --help     (Not for Win32 GUI)
+- Added ./junkbuster --version  (Not for Win32 GUI)
+- Win32 resources are now all marked as "U.S. English", rather than
+being a mix of "U.S. English", "U.K. English" and "Irish English".
+- Version number changes to 2.9.0
+
+
+----------------------------------------------------------------------
+Copyright   :  Written by and Copyright (C) 2001-2020 the
+               Privoxy team. https://www.privoxy.org/
+
+               Based on the Internet Junkbuster originally written
+               by and Copyright (C) 1997 Anonymous Coders and
+               Junkbusters Corporation.  http://www.junkbusters.com/
+
+               This program is free software; you can redistribute it
+               and/or modify it under the terms of the GNU General
+               Public License as published by the Free Software
+               Foundation; either version 2 of the License, or (at
+               your option) any later version.
+
+               This program is distributed in the hope that it will
+               be useful, but WITHOUT ANY WARRANTY; without even the
+               implied warranty of MERCHANTABILITY or FITNESS FOR A
+               PARTICULAR PURPOSE.  See the GNU General Public
+               License for more details.
+
+               The GNU General Public License should be included with
+               this file.  If not, you can view it at
+               http://www.gnu.org/copyleft/gpl.html
+               or write to the Free Software Foundation, Inc., 59
+               Temple Place - Suite 330, Boston, MA  02111-1307, USA.

+ 1114 - 0
privoxy/GNUmakefile.in

@@ -0,0 +1,1114 @@
+# Note:  GNUmakefile is built automatically from GNUmakefile.in
+#
+# Written by and Copyright (C) 2001-2018 members of the
+# Privoxy team. https://www.privoxy.org/
+#
+# Based on the Internet Junkbuster originally written
+# by and Copyright (C) 1997 Anonymous Coders and
+# Junkbusters Corporation.  http://www.junkbusters.com
+#
+# This program is free software; you can redistribute it
+# and/or modify it under the terms of the GNU General
+# Public License as published by the Free Software
+# Foundation; either version 2 of the License, or (at
+# your option) any later version.
+#
+# This program is distributed in the hope that it will
+# be useful, but WITHOUT ANY WARRANTY; without even the
+# implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.  See the GNU General Public
+# License for more details.
+#
+# The GNU General Public License should be included with
+# this file.  If not, you can view it at
+# http://www.gnu.org/copyleft/gpl.html
+# or write to the Free Software Foundation, Inc., 59
+# Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+#
+
+#############################################################################
+# Set make command correctly
+#############################################################################
+@SET_MAKE@
+
+#############################################################################
+# Version number (for RPM)
+#############################################################################
+
+VERSION_MAJOR = @VERSION_MAJOR@
+VERSION_MINOR = @VERSION_MINOR@
+VERSION_POINT = @VERSION_POINT@
+CODE_STATUS   = @CODE_STATUS@
+VERSION       = $(VERSION_MAJOR).$(VERSION_MINOR).$(VERSION_POINT)
+SNAPVERSION   = $(VERSION)-$(shell date "+%Y%m%d")
+
+SOURCE_DATE_EPOCH ?= @SOURCE_DATE_EPOCH@
+MTREE_SPEC_FILE = privoxy-$(VERSION)-$(CODE_STATUS).spec
+
+#############################################################################
+# "make install" directories and variables
+#############################################################################
+
+#User Group paras
+USER         = @USER@
+GROUP	   = @GROUP@
+
+datarootdir  = @datarootdir@
+prefix       = @prefix@
+exec_prefix  = @exec_prefix@
+CONF_BASE    = @sysconfdir@
+SBIN_DEST    = @sbindir@
+MAN_DIR      = @mandir@
+MAN_DEST     = $(MAN_DIR)/man1
+MAN_PAGE     = privoxy.1
+SHARE_DEST   = @datadir@
+DOC_DEST     = $(SHARE_DEST)/doc/privoxy
+VAR_DEST     = @localstatedir@
+LOGS_DEST    = $(VAR_DEST)/log/privoxy
+PIDS_DEST    = $(VAR_DEST)/run
+
+# if $prefix = /usr/local then the default CONFDEST change from
+# CONF_DEST = $(CONF_BASE) to CONF_DEST = $(CONF_BASE)/privoxy
+# by the target rule CONF_DEST
+#
+# also if the $prefix is /usr/local and there is no
+# $(SHARE_DEST)/doc, it checks for $prefix/doc and installs there
+# instead in this situation
+#
+# finally if $prefix=/usr/local and VAR_DEST=$prefix/var it
+# changes this to /var for storing the logs and pidfile
+
+# used in source dir only, the install goes to $share_dest/doc/privoxy
+DOK_WEB = doc/webserver/
+
+# Install usage should be compatible with install-sh.
+INSTALL    = @INSTALL@
+# Binaries
+BIN_MODE	 = 0755
+# Support files, docs, etc.
+RA_MODE   = 0644
+# Directory
+DIR_MODE   = 0755
+# Files daemon writes to.
+RWD_MODE   = 0660
+INSTALL_P  = -m $(BIN_MODE)
+INSTALL_T  = -m $(RA_MODE)
+INSTALL_D  = -m $(DIR_MODE) -d
+INSTALL_R  = -m $(RWD_MODE)
+
+# install options for superuser install
+#INSTALL_S  = -g @GROUP@ -o @USER@
+
+#############################################################################
+# Build tools
+#############################################################################
+
+PROGRAM    = privoxy@EXEEXT@
+CC         = @CC@
+ECHO       = echo
+GZIP_PROG  = gzip
+
+# id -u is not universal. FIXME: need to set from configure. Breaks on
+# Solaris.
+#ID         = id -u
+ID         = id
+LD         = @CC@
+RM         = rm -f
+CP         = cp -f
+RMDIR      = rmdir
+MKDIR      = ./mkinstalldirs
+STRIP_PROG = strip
+SED        = sed
+GREP       = grep
+CAT        = cat
+MV         = mv
+TAR        = tar
+LN         = ln
+TOUCH      = touch
+KILL       = kill
+CHMOD      = chmod
+CHOWN      = chown
+CHGRP      = chgrp
+GROUPS     = groups
+W3M_DUMP   = @W3M@ -I ISO-8859 -O ASCII -dump
+W3M_DUMP_UTF8 = @W3M@ -I ISO-8859 -O UTF-8 -dump
+# docbook output is ISO-8859 (which is a superset of ascii)
+JADECAT    = @JADECAT@
+JADEBIN    = @JADEBIN@
+NSGMLS     = @NSGMLS@
+DB         = $(JADEBIN) $(JADECAT) -ihtml -t sgml  -D.. -d ldp.dsl\#html
+DB_TXT     = $(JADEBIN) $(JADECAT) -ihtml -t sgml  -D.. -d ldp.dsl\#print
+# -d dsssl_spec
+#     This specifies that dsssl_spec is the system identifier of the DSSSL specification to be used.
+# ldp.dsl#html  : keep '&char;' strings as is
+# ldp.dsl#print : convert '&char;' strings to ISO-8859 equivalent
+# NOTE: '-d ldp.dsl\#whatever' _MUST_ be last and _MUST NOT_ have
+#       a trailing space so that '$(DB)-notoc' or '$(DB_TXT)-notoc'
+#       pulls in the correct dsl stylesheet
+DB2HTML    = @DB2HTML@
+MAN2HTML   = @MAN2HTML@
+G2H_CMD    = groff -mandoc -Thtml
+TARGET_OS  = @host@
+PERL       = perl
+DOC_DIR    = doc/source
+DOC_TMP    = $(DOC_DIR)/tmp
+DOC_STATUS = @DOC_STATUS@
+TIDY       = tidy -latin1 -q -modify -indent -wrap 120 --tidy-mark no --preserve-entities yes \
+ --mute MISSING_ATTRIBUTE --mute TRIM_EMPTY_ELEMENT
+# -latin1
+#     use ISO-8859-1 for both input and output
+#     docbook output is ISO-8859 and tidy assumes UTF-8
+# -q
+#     suppress nonessential output
+# -modify
+#     modify the original input file
+# --mute MISSING_ATTRIBUTE
+#     don't show <img> lacks "alt" attribute
+#             or <table> lacks "summary" attribute
+# --mute TRIM_EMPTY_ELEMENT
+#     don't show trimming empty <p>
+#
+RSYNC	   = rsync -av -c --chmod=D755,F644
+
+# Program to do LF->CRLF
+DOSFILTER  = $(PERL) -p -e 's/\n/\r\n/'
+CVSROOT    = :pserver:anonymous@ijbswa.cvs.sourceforge.net:/cvsroot/ijbswa
+#TMPDIR     := $(shell mktemp -d /tmp/$(PROGRAM).XXXXXX)
+# If your SF user name differs from your local one,
+# change this to "ssh -l sf-username"
+SSH	= ssh
+WWW_ROOT = /home/project-web/ijbswa
+# SourceForge login name used by the 'sf-shell' target (optional)
+SOURCE_FORGE_NAME = ''
+
+#############################################################################
+# Setup for make distribution for now.
+#############################################################################
+
+TAR_ARCH = /tmp/privoxy-$(VERSION).tar.gz
+GEN_DIST_TAR_NAME = privoxy-$(TARGET_OS)-$(VERSION)-$(CODE_STATUS).tar
+
+#############################################################################
+# We include these files in our distributions
+#############################################################################
+CONFIGS = 	config trust default.action match-all.action \
+		user.action default.filter user.filter \
+		regression-tests.action
+
+# take care that no CVS .cvsignore or other crappy files
+# are included here
+# and escape every '#' in the find. doh.
+CONFIG_FILES = $(CONFIGS) \
+		`find templates/ -type f | grep -v "CVS" | grep -v "\.\#" | grep -v ".*~" | grep -v ".cvsignore" | grep -v "TAGS" | sort`
+
+DOC_FILES = AUTHORS LICENSE LICENSE.GPLv3 README ChangeLog INSTALL \
+		`find doc/webserver/ -name "*.html" | grep -v "\(webserver\|team\)\/index\.html" | sort` \
+		`find doc/webserver/ -name "*.css" | sort` \
+                $(MAN_PAGE)
+
+#############################################################################
+# Filenames and libraries
+#############################################################################
+
+C_SRC  = actions.c cgi.c cgiedit.c cgisimple.c deanimate.c encode.c \
+         errlog.c filters.c gateway.c jbsockets.c jcc.c \
+         list.c loadcfg.c loaders.c miscutil.c parsers.c ssplit.c \
+         urlmatch.c
+
+C_OBJS = $(C_SRC:.c=.@OBJEXT@)
+C_HDRS = $(C_SRC:.c=.h) project.h actionlist.h
+
+CLIENT_TAG_SRC = @FEATURE_CLIENT_TAGS_ONLY@client-tags.c
+CLIENT_TAG_OBJS = @FEATURE_CLIENT_TAGS_ONLY@client-tags.@OBJEXT@
+
+FUZZ_SRC = @FUZZ_ONLY@fuzz.c
+FUZZ_OBJS = @FUZZ_ONLY@fuzz.@OBJEXT@
+
+W32_SRC   = @WIN_ONLY@w32log.c w32taskbar.c win32.c w32svrapi.c
+W32_FILES = @WIN_ONLY@w32.res
+W32_OBJS  = @WIN_ONLY@$(W32_SRC:.c=.@OBJEXT@) $(W32_FILES)
+W32_HDRS  = @WIN_ONLY@w32log.h w32taskbar.h win32.h w32res.h w32svrapi.h
+W32_LIB   = @WIN_ONLY@-lwsock32 -lcomctl32
+W32_INIS  = @WIN_ONLY@config.txt trust.txt
+
+SSL_SRC      = @FEATURE_HTTPS_INSPECTION_ONLY@ssl_common.c
+SSL_OBJS     = @FEATURE_HTTPS_INSPECTION_ONLY@$(SSL_SRC:.c=.o)
+SSL_HDRS     = @FEATURE_HTTPS_INSPECTION_ONLY@$(SSL_SRC:.c=.h) project.h
+
+MBEDTLS_SRC      = @FEATURE_HTTPS_INSPECTION_ONLY_MBEDTLS@ssl.c
+MBEDTLS_OBJS     = @FEATURE_HTTPS_INSPECTION_ONLY_MBEDTLS@$(MBEDTLS_SRC:.c=.o)
+MBEDTLS_HDRS     = @FEATURE_HTTPS_INSPECTION_ONLY_MBEDTLS@$(MBEDTLS_SRC:.c=.h)
+
+OPENSSL_SRC      = @FEATURE_HTTPS_INSPECTION_ONLY_OPENSSL@openssl.c
+OPENSSL_OBJS     = @FEATURE_HTTPS_INSPECTION_ONLY_OPENSSL@$(OPENSSL_SRC:.c=.o)
+OPENSSL_HDRS     = @FEATURE_HTTPS_INSPECTION_ONLY_OPENSSL@$(OPENSSL_SRC:.c=.h)
+
+PCRS_SRC     = @STATIC_PCRS_ONLY@pcrs.c
+PCRS_OBJS    = @STATIC_PCRS_ONLY@$(PCRS_SRC:.c=.@OBJEXT@)
+PCRS_HDRS    = @STATIC_PCRS_ONLY@$(PCRS_SRC:.c=.h)
+
+PCRE_SRC     = @STATIC_PCRE_ONLY@pcre/get.c pcre/maketables.c pcre/study.c pcre/pcre.c
+PCRE_OBJS    = @STATIC_PCRE_ONLY@$(PCRE_SRC:.c=.@OBJEXT@)
+PCRE_HDRS    = @STATIC_PCRE_ONLY@pcre/config.h pcre/chartables.c pcre/internal.h pcre/pcre.h
+
+# No REGEX (maybe because dynamically linked pcreposix):
+REGEX_SRC    =
+@STATIC_PCRE_ONLY@REGEX_SRC = pcre/pcreposix.c
+
+REGEX_OBJS   = $(REGEX_SRC:.c=.@OBJEXT@)
+REGEX_HDRS   = $(REGEX_SRC:.c=.h)
+
+# Dependencies introduced by #include "project.h".
+PROJECT_H_DEPS = project.h $(REGEX_HDRS) $(PCRS_HDRS) @STATIC_PCRE_ONLY@pcre/pcre.h
+
+# Socket libraries for platforms that need them explicitly defined
+SOCKET_LIB   = @SOCKET_LIB@
+
+# PThreads library, if needed.
+PTHREAD_LIB  = @PTHREAD_ONLY@@PTHREAD_LIB@
+
+SRCS         = $(C_SRC) $(CLIENT_TAG_SRC) $(FUZZ_SRC) $(W32_SRC)  $(PCRS_SRC)  $(PCRE_SRC)  $(REGEX_SRC) $(SSL_SRC) $(MBEDTLS_SRC) $(OPENSSL_SRC)
+OBJS         = $(C_OBJS) $(CLIENT_TAG_OBJS) $(FUZZ_OBJS) $(W32_OBJS) $(PCRS_OBJS) $(PCRE_OBJS) $(REGEX_OBJS) $(SSL_OBJS) $(MBEDTLS_OBJS) $(OPENSSL_OBJS)
+HDRS         = $(C_HDRS) $(W32_HDRS) $(PCRS_HDRS) $(PCRE_OBJS) $(REGEX_HDRS) $(SSL_HDRS) $(MBEDTLS_HDRS) $(OPENSSL_HDRS)
+LIBS         = @LIBS@ $(W32_LIB) $(SOCKET_LIB) $(PTHREAD_LIB)
+
+
+#############################################################################
+# Compiler switches
+#############################################################################
+
+# The flag "-mno-win32" can be used by Cygwin to emulate a un?x type build.
+# The flag "-mwindows -mno-cygwin" will cause Cygwin to use MingW32 for a
+# Win32 GUI build.
+# The flag "-pthread" is required if using Pthreads under Linux (and
+# possibly other OSs).
+SPECIAL_CFLAGS = @SPECIAL_CFLAGS@
+
+# Add your flags here
+OTHER_CFLAGS =
+
+CFLAGS = @CFLAGS@ @CPPFLAGS@ $(OTHER_CFLAGS) $(SPECIAL_CFLAGS) -Wall \
+         @STATIC_PCRE_ONLY@ -Ipcre
+
+LDFLAGS = @LDFLAGS@ $(DEBUG_CFLAGS) $(SPECIAL_CFLAGS)
+
+
+#############################################################################
+# Build section.
+#
+# There should NOT be any targets above this line.
+#############################################################################
+all: $(PROGRAM) default.action
+
+
+#############################################################################
+# Phony targets
+#############################################################################
+.PHONY: all inifiles \
+win-dist tarball-dist dok webserver clean clobber tags \
+install CONF_DEST LOG_DEST \
+PID_DEST check_doc install-strip uninstall GROUP_T
+
+#############################################################################
+# Define this explicitly because Solaris is broken!
+#############################################################################
+%.o: %.c
+	$(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@
+
+
+#############################################################################
+# Strip master copy comments from default.action:
+#############################################################################
+default.action: default.action.master
+	$(GREP) -v '^#MASTER#' default.action.master > $@
+#############################################################################
+# Win32 config files
+#############################################################################
+
+inifiles: $(W32_INIS)
+
+config.txt: config
+	$(SED) -e 's!\trustfile trust!trustfile trust.txt!' \
+	       -e 's!\logfile logfile!logfile privoxy.log!' \
+	       -e 's!#Win32-only: !!' \
+	       < $< | \
+	       $(DOSFILTER) > $@
+	# LF to CRLF in default.action
+	$(DOSFILTER) <default.action >default.action.txt && mv default.action.txt default.action
+	# LF to CRLF in default.filter
+	$(DOSFILTER) <default.filter >default.filter.txt && mv default.filter.txt default.filter
+
+trust.txt: trust
+	$(DOSFILTER) < $< > $@
+
+#############################################################################
+# Pre-dist check:
+#############################################################################
+dist-check:
+	@if [ -d CVS ]; then \
+           $(ECHO) "***************************************************"; \
+           $(ECHO) "***                                             ***"; \
+           $(ECHO) "***                  WARNING                    ***"; \
+           $(ECHO) "***                                             ***"; \
+           $(ECHO) "*** The presence of a CVS subdirectory suggests ***"; \
+           $(ECHO) "*** that you are trying to build a distribution ***"; \
+           $(ECHO) "*** package based on a checked out, not an      ***"; \
+           $(ECHO) "*** exported copy of the source tree. Please    ***"; \
+           $(ECHO) "*** see \"Releasing a new version\" in the        ***"; \
+           $(ECHO) "*** developer manual.                           ***"; \
+           $(ECHO) "***                                             ***"; \
+           $(ECHO) "***************************************************"; \
+           $(ECHO) "Type \"yes i am sure\" if you are sure that you"; \
+           $(ECHO) -n "really want to proceed: "; \
+           read answer; \
+           if [ "$$answer" != "yes i am sure" ]; then exit 1; fi \
+         fi;
+
+
+#############################################################################
+# create tar.gz from CVS:
+# This make-target is usually called through 'create-archive'. If you
+# run 'make create-snapshot' without setting SNAPVERSION, you'll get a
+# tar.gz with the current date in the name.
+# The main usage is to run it as follows (Red Hat example):
+# make SNAPVERSION=1.6x create-snapshot
+# This creates a tar.gz.
+#############################################################################
+create-snapshot:
+	@tag=`cvs -d $(CVSROOT) status Makefile | awk ' /Sticky Tag/ { print $$3 } '` 2> /dev/null; \
+	[ x"$$tag" = x"(none)" ] && tag=HEAD; \
+	echo "*** Creating package from $$tag!"; \
+	TMPDIR=$(shell mktemp -d /tmp/$(PROGRAM).XXXXXX); \
+	cd $$TMPDIR ; cvs -Q -d $(CVSROOT) export -r $$tag current || echo "Um... export aborted."; \
+	cd $$TMPDIR/current; \
+	$(TAR) --exclude ".cvsignore" --exclude "CVS" \
+		-czf /tmp/$(PROGRAM)-$(VERSION).tar.gz .; \
+	$(RM) -rf $$TMPDIR
+	@echo "Resulting file is /tmp/$(PROGRAM)-$(VERSION).tar.gz"
+
+
+#############################################################################
+# looks at the version of Makefile and exports a corresponding source-tree
+# example: if the Makefile has the sticky tag v_2_9_13, you'll get
+# privoxy-*-2.4.13.tar.gz.
+#############################################################################
+create-archive:
+	make SNAPVERSION=$(SNAPVERSION) create-snapshot
+
+#############################################################################
+# generic distribution
+#############################################################################
+gen-dist: dist-check
+	@$(ECHO) ""
+	@$(ECHO) "You have run autoconf && autoheader && ./configure right?"
+	@$(ECHO) ""
+	$(MAKE) $(PROGRAM)
+	$(STRIP_PROG) $(PROGRAM)
+	$(LN) -s `pwd` ../privoxy-$(VERSION)-$(CODE_STATUS)
+# add program
+	(cd .. && $(TAR) --exclude "PACKAGERS" -cvhf $(GEN_DIST_TAR_NAME) privoxy-$(VERSION)-$(CODE_STATUS)/$(PROGRAM))
+# add config files
+	for foo in $(CONFIG_FILES); do \
+		(cd .. && $(TAR) --exclude "PACKAGERS" -uvhf $(GEN_DIST_TAR_NAME) privoxy-$(VERSION)-$(CODE_STATUS)/$$foo;) \
+	done;
+# add documentation
+	for foo in $(DOC_FILES); do \
+		(cd .. && $(TAR) --exclude "PACKAGERS" -uvhf $(GEN_DIST_TAR_NAME) privoxy-$(VERSION)-$(CODE_STATUS)/$$foo;) \
+	done;
+# add tools
+	(cd .. && $(TAR) -uvhf $(GEN_DIST_TAR_NAME) privoxy-$(VERSION)-$(CODE_STATUS)/tools)
+# and zip the archive
+	$(RM) ../privoxy-$(VERSION)-$(CODE_STATUS)
+	$(GZIP_PROG) ../$(GEN_DIST_TAR_NAME)
+	@$(ECHO) Distribution with binary created.
+
+# anonymously ncftps the package to sourceforge
+gen-upload:
+	ncftpput -u anonymous -p ijbswa-developers@lists.sourceforge.net upload.sourceforge.net /incoming ../$(GEN_DIST_TAR_NAME).gz
+	@$(ECHO) -------------------------------------------------------
+	@$(ECHO) Now goto
+	@$(ECHO) https://sourceforge.net/project/admin/editpackages.php?group_id=11118
+	@$(ECHO) ... and release the files.
+	@$(ECHO) -------------------------------------------------------
+
+# use with care
+gen-clean:
+	$(RM) ../$(GEN_DIST_TAR_NAME)*
+
+#############################################################################
+# Tarball distribution: No CVS dirs, dotfiles, debian build dir,
+# (FIXME:) only parts of the static / generated docs mix in doc/webserver
+#############################################################################
+
+tarball-dist: dist-check clean clobber
+	$(LN) -s `pwd` ../privoxy-$(VERSION)-$(CODE_STATUS)
+
+	for i in `find . -type f -a -not \( -path "*/CVS*" -o -name ".*" \
+	-o -path "*/debian/*" -o -path "*/actions/*" -o -name "*.php" -o \
+	-name "PACKAGERS" -o -path "*.git/*" \) | sort`; do \
+	   files="$$files privoxy-$(VERSION)-$(CODE_STATUS)/$$i"; \
+	done &&  \
+	cd .. && $(TAR) -cvhf privoxy-$(VERSION)-$(CODE_STATUS)-src.tar $$files ; \
+
+# and zip the archive
+	$(RM) ../privoxy-$(VERSION)-$(CODE_STATUS)
+	$(GZIP_PROG) ../privoxy-$(VERSION)-$(CODE_STATUS)-src.tar
+	@$(ECHO) Tarball distribution created.
+
+# Create a mtree spec file that can be used to get a reproducible tar ball
+mtree-spec:
+	$(LN) -s `pwd` ../privoxy-$(VERSION)-$(CODE_STATUS)
+	$(ECHO) "#mtree 2.0" > $(MTREE_SPEC_FILE)
+	for i in `find . -type f -a -not \( -path "*/CVS*" -o -name ".*" \
+	-o -path "*/debian/*" -o -path "*/actions/*" -o -name "*.php" -o \
+	-name "PACKAGERS" -o -path "*.git/*" -o -name "*.spec" \) | env -i sort`; do \
+	   $(ECHO) "privoxy-$(VERSION)-$(CODE_STATUS)/$$i time=$(SOURCE_DATE_EPOCH) type=file uname=privoxy gname=privoxy mode=0555"; \
+	done >> $(MTREE_SPEC_FILE)
+	$(RM) ../privoxy-$(VERSION)-$(CODE_STATUS)
+
+# Create a reproducible tarball.
+# Requires a tar implementation with mtree support.
+reproducible-tarball-dist: dist-check clean clobber mtree-spec
+	$(LN) -s `pwd` ../privoxy-$(VERSION)-$(CODE_STATUS)
+
+	$(TAR) cvhf privoxy-$(VERSION)-$(CODE_STATUS)-src.tar -C .. @privoxy-$(VERSION)-$(CODE_STATUS)/$(MTREE_SPEC_FILE)
+	$(GZIP_PROG) -n privoxy-$(VERSION)-$(CODE_STATUS)-src.tar
+	$(RM) ../privoxy-$(VERSION)-$(CODE_STATUS) $(MTREE_SPEC_FILE)
+	@$(ECHO) Reproducible tarball distribution created.
+
+# anonymously ncftps the tarball to sourceforge
+tarball-upload:
+	ncftpput -u anonymous -p ijbswa-developers@lists.sourceforge.net upload.sourceforge.net /incoming ../privoxy-$(VERSION)-$(CODE_STATUS)-src.tar.gz
+	@$(ECHO) -------------------------------------------------------
+	@$(ECHO) Now goto
+	@$(ECHO) https://sourceforge.net/project/admin/editpackages.php?group_id=11118
+	@$(ECHO) ... and release the files.
+	@$(ECHO) -------------------------------------------------------
+
+tarball-clean:
+	$(RM) ../privoxy-$(VERSION)-$(CODE_STATUS)-src.tar.gz
+
+#############################################################################
+#
+# Documentation
+#
+# converts doc/source/*.sgml into html and man pages
+#
+#############################################################################
+
+# developer manual
+dok-devel:
+	$(RM) doc/webserver/developer-manual/*.html
+	$(RM) -r doc/source/developer-manual
+	mkdir -p doc/source/developer-manual
+	cd doc/source/developer-manual && $(DB) ../developer-manual.sgml && cd .. && cp developer-manual/*.html ../webserver/developer-manual/
+
+# user manual
+dok-user:
+	$(RM) doc/webserver/user-manual/*.html
+	$(RM) -r doc/source/user-manual/
+	mkdir -p doc/source/user-manual
+	cd doc/source/user-manual && $(DB) -iuser-man ../user-manual.sgml && cd .. && cp user-manual/*.html ../webserver/user-manual/
+	# FIXME: temp fix so same stylesheet gets in more than one place so it works
+	# for all doc set-ups, including the 'user manual' config option in local
+	# system where it MUST be in same directory as html.
+	$(PERL) -pi.bak -e 's/<\/head/\n<LINK REL=\"STYLESHEET\" TYPE=\"text\/css\" HREF=\"p_doc.css\">\n<\/head/i' doc/webserver/user-manual/*html && \
+	rm doc/webserver/user-manual/*html.bak
+
+# faq
+dok-faq:
+	$(RM) doc/webserver/faq/*.html
+	$(RM) -r doc/source/faq
+	mkdir -p doc/source/faq
+	cd doc/source/faq && $(DB) ../faq.sgml && cd .. && cp faq/*.html ../webserver/faq/
+
+# man page, one variation. Try to use the next target, just 'make man'.
+dok-man:
+	$(RM) doc/man/* doc/webserver/man-page/*.html
+	echo MAN2HTML is $(MAN2HTML)
+	@if [ $(MAN2HTML) != "false" ]; then \
+		$(ECHO) "<html><head><title>Privoxy Man page</title><link rel=\"stylesheet\" type=\"text/css\" href=\"../p_web.css\"></head><body><H2>NAME</H2>" > doc/webserver/man-page/privoxy-man-page.html; \
+		man ./$(MAN_PAGE) | $(MAN2HTML) -bare >> doc/webserver/man-page/privoxy-man-page.html; \
+		$(ECHO) "</body></html>" >> doc/webserver/man-page/privoxy-man-page.html; \
+	else \
+		$(MAKE) groff2html; \
+	fi;
+
+# Build man page from sgml. This requires the SGMLSpm perl module.
+# See CPAN, or your favorite perl repository. This is the preferred
+# target for man page generation!
+man: dok-release
+	mkdir -p doc/source/temp && cd doc/source/temp && $(RM) * ;\
+	$(NSGMLS) ../privoxy-man-page.sgml  | sgmlspl ../../../utils/docbook2man/docbook2man-spec.pl &&\
+	perl -pi.bak -e 's/ <URL:.*>//; s/\[ /\[/g' $(MAN_PAGE) ;\
+	perl -pi.bak -e "s/\[ /\[/g;s/á/\\\\['a]/g;s/é/\\\\['e]/g" $(MAN_PAGE); \
+	perl -pi.bak -e "s/ö/\\\\[:o]/g" $(MAN_PAGE); \
+	perl -pi.bak -e 's/([ {])-([a-z])/$$1\\-$$2/g' $(MAN_PAGE); \
+	perl -pi.bak -e 's/ --([a-z])/ \\-\\-$$1/g' $(MAN_PAGE); \
+	perl -pi.bak -e 's/\\fB--/\\fB\\-\\-/g' $(MAN_PAGE); \
+	$(DB) ../privoxy-man-page.sgml && $(MV) -f $(MAN_PAGE) ../../../$(MAN_PAGE)
+
+# For those with man2html ala RH7s.
+man2html:
+	mkdir -p doc/webserver/man-page
+	@if [ $(MAN2HTML) != "false" ]; then \
+		$(MAN2HTML) $(MAN_PAGE) |grep -v "^Content-type" > tmp.html; \
+		$(PERL) -pi.bak -e 's/<A .*Contents<\/A>//; s/<A .*man2html<\/A>/man2html/' tmp.html; \
+		$(PERL) -pi.bak -e 's/(<\/HEAD>)/<LINK REL=\"STYLESHEET\" TYPE=\"text\/css\" HREF=\"..\/p_doc.css\"><\/HEAD>/' tmp.html; \
+		$(PERL) -pi.bak -e 's/(<A.*),(">)/$$1$$2/g' tmp.html; \
+		$(PERL) -pi.bak -e 's,\.">,">,g' tmp.html; \
+		$(PERL) -pi.bak -e "s/\['a\]/\&aacute;/g;s/\['e\]/\&eacute;/g" tmp.html; \
+		$(SED) -e 's///g' tmp.html > doc/webserver/man-page/privoxy-man-page.html && $(RM) tmp.*; \
+	else \
+		$(MAKE) groff2html; \
+	fi;
+
+# Otherwise we get plain groff conversion.
+groff2html:
+	$(G2H_CMD) ./$(MAN_PAGE) | $(SED) -e 's@</head>@<link REL="STYLESHEET" TYPE="text/css" HREF="../p_doc.css"></head>@' > doc/webserver/man-page/privoxy-man-page.html
+
+
+# readme page and INSTALL file
+dok-readme: dok-release
+	cd doc/source && $(DB_TXT)-notoc -V nochunks readme.sgml > tmp.html &&\
+	$(W3M_DUMP) tmp.html > ../../README ;\
+	$(DB_TXT)-notoc -V nochunks install.sgml > tmp.html &&\
+	$(W3M_DUMP) tmp.html > ../../INSTALL ;\
+	$(RM) tmp.*
+
+# index.sgml is used to create both the Home Page, and a local index
+# for documentation, etc.
+#
+# index.html for webserver:
+dok-webserver:
+	cd doc/source/webserver && $(DB)-notoc -ip-homepage -V nochunks index.sgml > ../../webserver/index.html
+	$(PERL) -pi.bak -e 's/..\/p_doc.css/p_doc.css/;\
+	s/<\/HEAD/\n<meta name=\"description\" content=\"Privoxy helps users to protect their privacy.\"><\/HEAD/;\
+	s/\.\d\. //;\
+	s/__copy/&copy;/;\
+	s@(<SUB)@<p style="text-align: center">\1@; s@(</SUB)@\1></p@' \
+     doc/webserver/index.html && $(RM) doc/webserver/*.bak
+
+# privoxy-index.html for local documentation:
+dok-index:
+	cd doc/source/webserver && $(DB)-notoc -ip-index -V nochunks index.sgml > ../../webserver/privoxy-index.html
+	$(PERL) -pi.bak -e 's/..\/p_doc.css/p_doc.css/;\
+	s/<\/HEAD/\n<meta name=\"description\" content=\"Privoxy helps users to protect their privacy.\"><\/HEAD/;\
+	s/\.\d\. //;\
+	s/__copy/&copy;/' \
+     doc/webserver/privoxy-index.html && $(RM) doc/webserver/*.bak
+
+# Main documentation target.
+dok: dok-release dok-devel dok-user dok-faq dok-readme dok-webserver dok-authors dok-index
+	@$(ECHO) Documentation created.
+
+## Make AUTHORS file
+dok-authors:
+	cd doc/source && $(DB_TXT) -V nochunks authors.sgml > tmp.html && \
+	  $(W3M_DUMP_UTF8) tmp.html > ../../AUTHORS && $(RM) tmp.html
+
+# Set doc entities for VERSION and CODE_STATUS in sgml docs. Toggle content
+# exceptions accordingly. This needs to go before any doc building (doh).
+dok-release:
+	@$(ECHO) Setting doc version and status to $(VERSION), $(CODE_STATUS)
+	@$(PERL) -pi.bak -e 's/<!entity +p-version.*>/<!entity p-version "$(VERSION)">/;\
+     s/<!entity +p-status.*>/<!entity p-status "$(CODE_STATUS)">/' \
+     doc/source/*sgml doc/source/*/*sgml
+	$(RM) -r doc/source/*bak doc/source/*/*bak
+	@if [ $(CODE_STATUS) = "stable" ]; then \
+		$(ECHO) Setting docs to stable $(VERSION); \
+		$(PERL) -pi.bak -e 's/<!entity +% +p-stable.*>/<!entity % p-stable "INCLUDE">/;\
+			s/<!entity +% +p-not-stable.*>/<!entity % p-not-stable "IGNORE">/' \
+		doc/source/*sgml doc/source/*/*sgml; \
+	else \
+		$(ECHO) Setting docs to not stable $(VERSION); \
+		$(PERL) -pi.bak -e 's/<!entity +% +p-stable.*>/<!entity % p-stable "IGNORE">/; \
+			s/<!entity +% +p-not-stable.*>/<!entity % p-not-stable "INCLUDE">/' \
+		doc/source/*sgml doc/source/*/*sgml; \
+	fi;
+	$(RM) -r doc/source/*bak doc/source/*/*bak;
+
+# The main Privoxy config file, generated from sgml sources.
+# NOTE: This will require some hand editing.
+config-file: dok-release generate-config-file
+
+	$(RM) config.bak config.html
+	@$(ECHO)  "****************************************************"
+	@$(ECHO)  "The config file has been optimistically updated"
+	@$(ECHO)  "Now -- you may need to hand edit the results!"
+	@$(ECHO)  "In particular, check the Debug levels, the"
+	@$(ECHO)  "permit-access, forward & socks examples and the"
+	@$(ECHO)  "various user-manual examples, which all"
+	@$(ECHO)  "might have gotten hammered."
+	@$(ECHO)  "****************************************************"
+
+generate-config-file:
+	cd doc/source && $(DB_TXT)-notoc -iconfig-file -V nochunks config.sgml > ../../config.html
+	$(W3M_DUMP) -cols 67 config.html > config
+	$(PERL) -i.bak utils/prepare-configfile.pl config
+
+#############################################################################
+#
+# Webserver
+#
+# moves documentation to webserver
+#
+#############################################################################
+sf-shell:
+	@sf_name=$(SOURCE_FORGE_NAME); \
+	[ -n "$${sf_name}" ] || read -p "Enter SourceForge username: " sf_name || exit 1; \
+	echo "Opening shell for $${sf_name} ..."; \
+	ssh -t $${sf_name},ijbswa@shell.sourceforge.net create
+
+webserver: clean-editor-files
+	@$(ECHO) -------------------------------------------------------
+	@$(ECHO) You will need to "create" a SF shell first:
+	@$(ECHO)    ssh -t SF-USER-ID,ijbswa@shell.sourceforge.net create
+	@$(ECHO) Please make sure your documentation files are up to date.
+	@$(ECHO) Note that this command updates the home page and copys
+	@$(ECHO) all stuff to the webserver, it will not remove obsolete documents.
+	@$(ECHO) Note that a botched upload can result in the documentation
+	@$(ECHO) on the website becoming unreachable! Also the CSS files
+	@$(ECHO) currently seem to end up at the wrong place.
+	@$(ECHO) -------------------------------------------------------
+
+	@$(ECHO) Replacing the user-manual symlink
+	@$(SSH) shell.sourceforge.net "cd $(WWW_ROOT)/htdocs && rm user-manual \
+	 && mkdir -p $(VERSION)/user-manual && ln -s $(VERSION)/user-manual user-manual"
+
+	@$(ECHO) Uploading html
+	@cd doc/webserver; \
+          upload=`find . -type f -a -not \( -path "*/CVS*" -o -path "*/results*" \)`; \
+          $(TAR) cf - $$upload | $(SSH) shell.sourceforge.net 'cd $(WWW_ROOT)/htdocs/; tar xvm 2>&1 | grep -v timestamp'
+
+web-actions:
+	@$(ECHO) Updating the actions on the webserver ...
+	@$(RSYNC) doc/webserver/actions/*.php shell.sourceforge.net:$(WWW_ROOT)/htdocs/actions
+
+web-homepage:
+	@$(ECHO) "Updating the home page (index.html) only (be careful in case of version changes) ..."
+	@$(RSYNC) doc/webserver/index.html shell.sourceforge.net:$(WWW_ROOT)/htdocs/
+
+web-faq:
+	@$(ECHO) Updating the FAQ on the webserver ...
+	@$(RSYNC) doc/webserver/faq/*.html shell.sourceforge.net:$(WWW_ROOT)/htdocs/faq
+
+web-sponsors:
+	@$(ECHO) "Updating the sponsor page (index.html) only ..."
+	@$(RSYNC) doc/webserver/sponsors/index.html shell.sourceforge.net:$(WWW_ROOT)/htdocs/sponsors/
+
+web-user-manual:
+	@$(ECHO) Updating the user manual on the webserver (do not use in case of version changes) ...
+	@$(RSYNC) doc/webserver/user-manual/*.html shell.sourceforge.net:$(WWW_ROOT)/htdocs/user-manual/
+
+#############################################################################
+#
+# Try to clean up the generated HTML files.
+#
+# The files are such a mess that some of them require two tidy runs in a
+# row as the first one aborts prematurely. The vanilla tidy output renders
+# poorly because it contains a bit too much whitespace, so we additionally
+# run the files through perl to fix this again.
+#
+#############################################################################
+dok-tidy:
+	for html_file in `find doc/webserver -name "*.html"`; do \
+		$(ECHO) "------ begin processing $$html_file" >&2 ; \
+		$(TIDY) $$html_file || $(TIDY) $$html_file; \
+		$(PERL) -i'' -e 's@^\s*<br>\s*$$@@; s@ +$$@@;' -n -p $$html_file; \
+	done
+
+
+#############################################################################
+# Source file dependencies
+#############################################################################
+
+actions.@OBJEXT@:   actions.c   actions.h   config.h $(PROJECT_H_DEPS) errlog.h filters.h jcc.h list.h loaders.h miscutil.h actionlist.h ssplit.h
+cgi.@OBJEXT@:       cgi.c       cgi.h       config.h $(PROJECT_H_DEPS) cgiedit.h cgisimple.h jbsockets.h list.h pcrs.h encode.h ssplit.h jcc.h filters.h actions.h errlog.h miscutil.h
+cgiedit.@OBJEXT@:   cgiedit.c   cgiedit.h   config.h $(PROJECT_H_DEPS) cgi.h list.h pcrs.h encode.h ssplit.h jcc.h filters.h actionlist.h actions.h errlog.h miscutil.h
+cgisimple.@OBJEXT@: cgisimple.c cgisimple.h config.h $(PROJECT_H_DEPS) cgi.h list.h pcrs.h encode.h ssplit.h jcc.h filters.h actions.h errlog.h miscutil.h urlmatch.h
+deanimate.@OBJEXT@: deanimate.c deanimate.h config.h $(PROJECT_H_DEPS)
+encode.@OBJEXT@:    encode.c    encode.h    config.h
+errlog.@OBJEXT@:    errlog.c    errlog.h    config.h $(PROJECT_H_DEPS) @WIN_ONLY@w32log.h
+filters.@OBJEXT@:   filters.c   filters.h   config.h $(PROJECT_H_DEPS) errlog.h encode.h gateway.h jbsockets.h jcc.h loadcfg.h parsers.h ssplit.h cgi.h deanimate.h urlmatch.h @WIN_ONLY@win32.h
+gateway.@OBJEXT@:   gateway.c   gateway.h   config.h $(PROJECT_H_DEPS) errlog.h jbsockets.h jcc.h loadcfg.h
+jbsockets.@OBJEXT@: jbsockets.c jbsockets.h config.h $(PROJECT_H_DEPS) filters.h
+jcc.@OBJEXT@:       jcc.c       jcc.h       config.h $(PROJECT_H_DEPS) errlog.h filters.h gateway.h jbsockets.h loadcfg.h loaders.h miscutil.h parsers.h @WIN_ONLY@w32log.h win32.h w32svrapi.h cgi.h
+list.@OBJEXT@:      list.c      list.h      config.h $(PROJECT_H_DEPS) list.h miscutil.h
+loadcfg.@OBJEXT@:   loadcfg.c   loadcfg.h   config.h $(PROJECT_H_DEPS) errlog.h filters.h gateway.h jbsockets.h jcc.h loaders.h miscutil.h parsers.h @WIN_ONLY@w32log.h win32.h
+loaders.@OBJEXT@:   loaders.c   loaders.h   config.h $(PROJECT_H_DEPS) errlog.h encode.h filters.h gateway.h jcc.h loadcfg.h miscutil.h parsers.h ssplit.h
+miscutil.@OBJEXT@:  miscutil.c  miscutil.h  config.h
+parsers.@OBJEXT@:   parsers.c   parsers.h   config.h $(PROJECT_H_DEPS) errlog.h filters.h jbsockets.h jcc.h loadcfg.h loaders.h miscutil.h ssplit.h
+ssplit.@OBJEXT@:    ssplit.c    ssplit.h    config.h miscutil.h
+urlmatch.@OBJEXT@:  urlmatch.c  urlmatch.h  config.h $(PROJECT_H_DEPS) errlog.h miscutil.h ssplit.h
+client-tags.@OBJEXT@: client-tags.c client-tags.h config.h $(PROJECT_H_DEPS) errlog.h miscutil.h ssplit.h
+fuzz.@OBJEXT@: fuzz.c config.h $(PROJECT_H_DEPS) errlog.h miscutil.h ssplit.h
+ssl.@OBJEXT@: ssl.c ssl.h ssl_common.h config.h $(PROJECT_H_DEPS) encode.h errlog.h jcc.h miscutil.h
+openssl.@OBJEXT@: openssl.c ssl.h ssl_common.h config.h $(PROJECT_H_DEPS) encode.h errlog.h jcc.h miscutil.h
+ssl_common.@OBJEXT@: ssl_common.c ssl.h ssl_common.h config.h $(PROJECT_H_DEPS) errlog.h miscutil.h
+
+# GNU regex
+gnu_regex.@OBJEXT@: gnu_regex.c gnu_regex.h config.h
+
+# PCRS
+pcrs.@OBJEXT@: pcrs.c pcrs.h config.h @STATIC_PCRE_ONLY@pcre/pcre.h
+
+# PCRE
+pcre/get.@OBJEXT@:        pcre/get.c        pcre/config.h pcre/internal.h pcre/pcre.h
+pcre/maketables.@OBJEXT@: pcre/maketables.c pcre/config.h pcre/internal.h pcre/pcre.h
+pcre/pcre.@OBJEXT@:       pcre/pcre.c       pcre/config.h pcre/internal.h pcre/pcre.h pcre/chartables.c
+pcre/pcreposix.@OBJEXT@:  pcre/pcreposix.c  pcre/config.h pcre/internal.h pcre/pcre.h pcre/pcreposix.h
+pcre/study.@OBJEXT@:      pcre/study.c      pcre/config.h pcre/internal.h pcre/pcre.h
+
+# An auxiliary program makes the PCRE default character table source
+
+pcre/chartables.c:   pcre/dftables@EXEEXT@
+		pcre/dftables@EXEEXT@ >pcre/chartables.c
+
+pcre/dftables@EXEEXT@:       pcre/dftables.c pcre/maketables.c pcre/pcre.h pcre/internal.h pcre/config.h
+		$(CC) -o pcre/dftables@EXEEXT@ $(CFLAGS) pcre/dftables.c
+
+# Win32
+w32log.@OBJEXT@: w32log.c errlog.h config.h jcc.h loadcfg.h miscutil.h pcre/pcre.h pcre/pcreposix.h pcrs.h project.h w32log.h w32taskbar.h win32.h
+w32taskbar.@OBJEXT@: w32taskbar.c config.h w32log.h w32taskbar.h
+win32.@OBJEXT@: win32.c config.h jcc.h loadcfg.h pcre/pcre.h pcre/pcreposix.h pcrs.h project.h w32log.h win32.h w32svrapi.h
+
+w32.res: w32.rc w32res.h icons/radar-01.ico icons/radar-02.ico icons/radar-03.ico icons/radar-04.ico icons/radar-05.ico icons/radar-06.ico icons/radar-07.ico icons/radar-08.ico icons/idle.ico icons/privoxy.ico config.h
+	windres -F pe-i386 -D__MINGW32__=0.2 -O coff -i $< -o $@
+
+$(PROGRAM): $(OBJS) $(W32_FILES)
+	$(LD) $(LDFLAGS) -o $(PROGRAM) $(OBJS) $(LIBS)
+
+clean:
+	$(RM) a.out $(OBJS) $(W32_FILES) $(W32_INIS) $(PROGRAM) default.action \
+		config.base config.tmp \
+		`find . \( -name TAGS -o -name tags \) -a -not -path "./.git/*"` \
+		`find . -name "*.orig" -a -not -name rc.privoxy.orig -a -not -path "./.git/*"`
+
+clean-editor-files:
+	$(RM) `find . -name "*~"`
+	$(RM) `find . -name "#*#"` # Emacs backup files
+	$(RM) `find . -name ".\#*"`
+
+clobber: clean-editor-files
+	$(RM) GNUmakefile configure config.h.in config.h config.cache config.status config.log logfile \
+              privoxy.log core *.tar.gz *.tar privoxy-cl.spec doc/source/ldp.dsl config.new
+	$(RM) -r autom4te.cache
+
+#
+# FIXME: What is all this?
+#
+	$(RM) cscope.*  *.pdb *.lib *.exp
+
+distclean: clobber
+
+tags: $(SRCS) $(HDRS)
+	etags $(SRCS) $(HDRS)
+
+CONF_DEST:=$(shell if [ "$(prefix)" = "/usr/local" ] && [ "$(CONF_BASE)" = "$(prefix)/etc" ];then \
+		$(ECHO) "$(CONF_BASE)/privoxy";\
+	 else\
+		 $(ECHO) "$(CONF_BASE)";\
+	 fi)
+
+LOG_DEST:=$(shell if [ "$(prefix)" = "/usr/local" ] && [ "$(LOGS_DEST)" = "$(prefix)/var/log/privoxy" ];then \
+		$(ECHO) "/var/log/privoxy" ;\
+	 else\
+		 $(ECHO) "$(LOGS_DEST)";\
+	 fi)
+
+PID_DEST:=$(shell if [ "$(prefix)" = "/usr/local" ] && [ "$(PIDS_DEST)" = "$(prefix)/var/run" ];then \
+		$(ECHO) "/var/run" ;\
+	 else\
+		 $(ECHO) "$(PIDS_DEST)";\
+	 fi)
+
+check_doc:=$(shell if [ ! -d "$(SHARE_DEST)/doc" ] && [ "$(prefix)" = "/usr/local" ]  && [ -d "$(prefix)/doc" ];then \
+		$(ECHO) "1";\
+	 else\
+		 $(ECHO) "0";\
+	 fi)
+
+# If USER is specified but no GROUP, assume there is a GROUP of same name.
+GROUP_T:=$(shell if [ x$(GROUP) = x ] && [ x$(USER) != x ];then \
+		$(ECHO) "$(USER)" ;\
+	 else\
+		 $(ECHO) "$(GROUP)";\
+	 fi)
+
+install-strip:
+	$(MAKE) install STRIP=-s
+
+# FIXME: Test USER and GROUP on Slack to make sure this works as
+# intended.
+#
+# FIXME: id handling needs help, probably via configure, since 'id -u' is not
+# universally reliable (eg Solaris). Group handling could be better.
+# Perhaps the whole user/group validation should be done here, and simplified.
+PROGRAM_V = Privoxy $(VERSION) $(CODE_STATUS)
+install: CONF_DEST LOG_DEST PID_DEST check_doc GROUP_T
+	@# Quick test for valid USER.
+	@if [ -n "$(USER)" ]; then \
+		$(ID) $(USER) >/dev/null || exit 1;\
+	fi
+	@# Test for valid group. FIXME. USER does not have to belong to GROUP
+	@# for file ownership purposes.
+# 	if [ -n "$(GROUP_T)" ] && [ -n "$(USER)" ] && ! $(GROUPS) $(USER) | $(GREP) "\<$(GROUP_T)\>" >/dev/null; then \
+# 		$(ECHO) Group $(GROUP_T) for User $(USER) is invalid && exit 1 ;\
+# 	fi
+
+	@$(ECHO) "Creating directories, and preparing $(PROGRAM_V) installation"
+	$(CHMOD) $(DIR_MODE) $(MKDIR)
+	@$(MKDIR) $(DESTDIR)$(SBIN_DEST) $(DESTDIR)$(prefix) $(DESTDIR)$(CONF_DEST) \
+		$(DESTDIR)$(CONF_DEST)/templates $(DESTDIR)$(SHARE_DEST) \
+		$(DESTDIR)$(LOG_DEST) $(DESTDIR)$(PID_DEST)
+	@# Install the executable binary, strip if invoked as install-strip
+	@test -n "$(STRIP)" &&\
+	$(ECHO) Installing $(PROGRAM) stripped executable to $(SBIN_DEST) ||\
+	$(ECHO) Installing $(PROGRAM) executable to $(DESTDIR)$(SBIN_DEST)
+	$(INSTALL) $(INSTALL_P) $(STRIP) $(PROGRAM) $(DESTDIR)$(SBIN_DEST)
+
+	@# Install the DOCS and man page. install-sh only does one file at a time.
+	@# FIXME: only handles jpegs.
+	-@if [ $(check_doc) = 0 ]; then \
+		DOC=$(DOC_DEST) ;\
+	else \
+		DOC=$(prefix)/doc/privoxy ;\
+	fi;\
+	$(MKDIR) $(DESTDIR)$$DOC $(DESTDIR)$$DOC/user-manual $(DESTDIR)$$DOC/faq $(DESTDIR)$$DOC/developer-manual \
+	     $(DESTDIR)$$DOC/man-page $(DESTDIR)$$DOC/images $(DESTDIR)$(MAN_DEST) ;\
+	if [ -d "$(DOK_WEB)" ]; then \
+		$(ECHO) Installing FAQ, Manual, and other docs to $(DESTDIR)$$DOC;\
+          for i in user-manual developer-manual faq; do \
+               for ii in $(DOK_WEB)/$$i/*html; do \
+                    $(INSTALL) $(INSTALL_T) $$ii $(DESTDIR)$$DOC/$$i;\
+               done ;\
+          done ;\
+		for i in $(DOK_WEB)/user-manual/*jpg; do \
+               $(INSTALL) $(INSTALL_T) $$i $(DESTDIR)$$DOC/user-manual;\
+          done ;\
+		$(INSTALL) $(INSTALL_T) $(DOK_WEB)/man-page/*html $(DESTDIR)$$DOC/man-page;\
+		$(INSTALL) $(INSTALL_T) $(DOK_WEB)/privoxy-index.html $(DESTDIR)$$DOC/index.html;\
+		$(INSTALL) $(INSTALL_T) AUTHORS $(DESTDIR)$$DOC;\
+		$(INSTALL) $(INSTALL_T) LICENSE $(DESTDIR)$$DOC;\
+		$(INSTALL) $(INSTALL_T) LICENSE.GPLv3 $(DESTDIR)$$DOC;\
+		$(INSTALL) $(INSTALL_T) README $(DESTDIR)$$DOC;\
+		$(INSTALL) $(INSTALL_T) ChangeLog $(DESTDIR)$$DOC;\
+		$(INSTALL) $(INSTALL_T) $(DOK_WEB)/p_doc.css $(DESTDIR)$$DOC;\
+		$(INSTALL) $(INSTALL_T) $(DOK_WEB)/p_doc.css $(DESTDIR)$$DOC/user-manual;\
+	fi
+	@# Not all platforms support gzipped man pages.
+	@$(ECHO) Installing man page to $(DESTDIR)$(MAN_DEST)/$(MAN_PAGE)
+	-$(INSTALL) $(INSTALL_T) $(MAN_PAGE)  $(DESTDIR)$(MAN_DEST)/$(MAN_PAGE)
+
+	@# Change the config file default directories according to the configured ones
+	@$(ECHO) Rewriting config for this installation
+	@if [ -f config.base ] ; then \
+		$(CAT) config >config~ ;\
+		$(MV) config.base config ;\
+	fi
+	$(SED) 's+^confdir \.+confdir $(CONF_DEST)+' config | \
+	$(SED) 's+^logdir \.+logdir $(LOG_DEST)+' >config.tmp
+	-@if [ $(check_doc) = 0 ]; then \
+      $(SED) 's+^#\?user-manual .*+user-manual $(DOC_DEST)/user-manual/+' config.tmp >config.updated ;\
+	else \
+      $(SED) 's+^#\?user-manual .*+user-manual $(prefix)/doc/privoxy/user-manual/+' config.tmp >config.updated ;\
+	fi;\
+	$(MV) config config.base
+	$(MV) config.updated config
+
+	@# Install the config support files. Test for root install, and abort
+	@# if there is no privoxy user, and no other user was enabled during
+	@# configure. Later, install init script if appropriate.
+	@$(ECHO) Installing templates to $(DESTDIR)$(CONF_DEST)/templates
+	@for i in `find templates -type f`; do \
+		$(INSTALL) $(INSTALL_T) $$i $(DESTDIR)$(CONF_DEST)/templates ;\
+	done
+
+	@# FIXME: group/user validation is overly convoluted.
+	@# If superuser install ... we require a minimum of group ownership
+	@# of those files the daemon writes to, to be non-root owned.
+	@if [ "`$(ID) |sed 's/(.*//' |sed 's/.*=//'`" = "0" ] ;then\
+		if [ x$(USER) = x ] || [ $(USER) = root ]; then \
+			if [ x$(GROUP) = x ] || [ $(GROUP) = root ]; then \
+				if [ "`$(ID) privoxy`" ] && \
+					$(GROUPS) privoxy | $(SED) 's/^.*://' |$(GREP) "\<privoxy\>" >/dev/null; then \
+					$(ECHO) "Warning: Setting group owner to privoxy";\
+					GROUP_T=privoxy ;\
+				else \
+					$(ECHO)  "******************************************************************" ;\
+					$(ECHO)  " WARNING! WARNING! installing config files as root!" ;\
+					$(ECHO)  " It is strongly recommended to run $(PROGRAM) as a non-root user," ;\
+					$(ECHO)  " and to install the config files as that user and/or group!" ;\
+					$(ECHO)  " Please read INSTALL, and create a privoxy user and group!" ;\
+					$(ECHO)  "*******************************************************************" ;\
+					exit 1 ;\
+				fi ;\
+			else \
+				GROUP_T=$(GROUP) ;\
+			fi ;\
+			INSTALL_CONF="$(INSTALL_R) -g $$GROUP_T " ;\
+		else \
+			$(ECHO) "Superuser install, installing config files as $(USER):$(GROUP_T)" ;\
+			INSTALL_CONF="$(INSTALL_R) -o $(USER) -g $(GROUP_T)" ;\
+			GROUP_T=$(GROUP_T) ;\
+		fi ;\
+	else \
+		if [ ! "`id $(USER)`" = "`id`" ] ;then \
+			$(ECHO) "** WARNING ** current install user different from configured user!!" ;\
+			$(ECHO) "Edit may fail." ;\
+		fi ;\
+		INSTALL_CONF="$(INSTALL_R)" ;\
+	fi ;\
+	$(ECHO) Installing configuration files to $(DESTDIR)$(CONF_DEST);\
+	for i in $(CONFIGS); do \
+		if [ "$$i" = "default.action" ] || [ "$$i" = "default.filter" ] ; then \
+			$(RM) $(DESTDIR)$(CONF_DEST)/$$i ;\
+			$(ECHO) Installing fresh $$i;\
+			$(INSTALL) $$INSTALL_CONF $$i $(DESTDIR)$(CONF_DEST) || exit 1;\
+		elif [ -s "$(CONF_DEST)/$$i" ]; then \
+			$(ECHO) Installing $$i as $$i.new ;\
+			$(INSTALL) $$INSTALL_CONF $$i $(DESTDIR)$(CONF_DEST)/$$i.new || exit 1;\
+			NEW=1;\
+		else \
+			$(INSTALL) $$INSTALL_CONF $$i $(DESTDIR)$(CONF_DEST) || exit 1;\
+		fi ;\
+	done ;\
+	if [ -n "$$NEW" ]; then \
+		$(CHMOD) $(RWD_MODE) $(DESTDIR)$(CONF_DEST)/*.new || exit 1 ;\
+		$(ECHO) "Warning: Older config files are preserved. Check new versions for changes!" ;\
+	fi ;\
+	[ ! -f $(DESTDIR)$(LOG_DEST)/logfile ] && $(ECHO) Creating logfiles in $(DESTDIR)$(LOG_DEST) || \
+		$(ECHO) Checking logfiles in $(DESTDIR)$(LOG_DEST) ;\
+		$(TOUCH) $(DESTDIR)$(LOG_DEST)/logfile || exit 1 ;\
+	if [ x$$USER != x ]; then \
+		$(CHOWN) $$USER $(DESTDIR)$(LOG_DEST)/logfile || \
+		$(ECHO) "** WARNING ** current install user different from configured user. Logging may fail!!" ;\
+	fi ;\
+	if [ x$$GROUP_T != x ]; then \
+		$(CHGRP) $$GROUP_T $(DESTDIR)$(LOG_DEST)/logfile || \
+		$(ECHO) "** WARNING ** current install user different from configured user. Logging may fail!!" ;\
+	fi ;\
+	$(CHMOD) $(RWD_MODE) $(DESTDIR)$(LOG_DEST)/logfile || exit 1 ;\
+	if [ "$(prefix)" = "/usr/local" ] || [ "$(prefix)" = "/usr" ]; then \
+		if [ -f /etc/slackware-version ] && [ -d /etc/rc.d/ ] && [ -w /etc/rc.d/ ] ; then \
+               $(SED) 's+%PROGRAM%+$(PROGRAM)+' slackware/rc.privoxy.orig | \
+               $(SED) 's+%SBIN_DEST%+$(SBIN_DEST)+' | \
+               $(SED) 's+%CONF_DEST%+$(CONF_DEST)+' | \
+               $(SED) 's+%USER%+$(USER)+' | \
+               $(SED) 's+%GROUP%+$(GROUP_T)+' >slackware/rc.privoxy ;\
+			$(INSTALL) $(INSTALL_P) slackware/rc.privoxy $(DESTDIR)/etc/rc.d/ ;\
+               $(ECHO) "Installing for Slackware." ;\
+               $(ECHO) "Dont forget to add the rc.privoxy to rc.local if you want it started at every boot" ;\
+		elif [ -d $(DESTDIR)/etc/init.d ] && [ -w $(DESTDIR)/etc/init.d ] ; then \
+			$(ECHO) "Installing generic init script to $(DESTDIR)/etc/init.d/privoxy" ;\
+			$(ECHO) "Please check that the PATHs are correct, and edit if needed." ;\
+			$(INSTALL) $(INSTALL_P) privoxy-generic.init $(DESTDIR)/etc/init.d/privoxy ;\
+		fi ;\
+	else \
+		$(ECHO) "No init script installed, install it manually if needed" ;\
+	fi
+	$(RM) config.base config.tmp
+	@# mmmmm, good.
+	@$(ECHO) "$(PROGRAM_V) installation succeeded!"
+	@$(ECHO) "The Privoxy configuration files have been installed in $(DESTDIR)$(CONF_DEST)"
+
+# rmdir is used as a precaution since it will not remove non-empty
+# directories. RH init script creates lock file and pid file.
+uninstall: CONF_DEST LOG_DEST PID_DEST check_doc
+	@$(ECHO) Starting Privoxy uninstallation
+	@# KILL privoxy if running
+	@# XXX: the chkconfig line may need a DESTDIR prefix.
+	-@test -f $(DESTDIR)$(PID_DEST)/privoxy.pid && $(ECHO) Stopping $(PROGRAM) &&\
+         $(KILL) `$(CAT) $(DESTDIR)$(PID_DEST)/privoxy.pid` || :
+	-@test -f $(DESTDIR)/var/run/privoxy.pid && $(ECHO) Stopping $(PROGRAM) &&\
+          $(KILL) `$(CAT) $(DESTDIR)/var/run/privoxy.pid ` || :
+
+	@# Program binary
+	@$(ECHO) Removing $(PROGRAM) binary
+	$(RM) $(DESTDIR)$(SBIN_DEST)/$(PROGRAM) $(SBIN_DEST)/$(PROGRAM)~
+
+	@# config files and dir, and maybe old install backups
+	-@if [ -d $(DESTDIR)$(CONF_DEST) ]; then \
+		$(ECHO) Saving $(PROGRAM) config files to $(DESTDIR)/tmp/$(PROGRAM)-save ;\
+		$(MKDIR) $(DESTDIR)/tmp/$(PROGRAM)-save ;\
+		cd $(DESTDIR)$(CONF_DEST) ;\
+		for i in $(DESTDIR)$(CONFIGS); do \
+			[ -f $$i ] && $(CP) $$i $(DESTDIR)/tmp/$(PROGRAM)-save ;\
+		done ;\
+	fi
+	@$(ECHO) Removing $(PROGRAM) config files
+	-@for i in $(DESTDIR)$(CONFIGS); do \
+		test -f $(CONF_DEST)/$$i && $(ECHO) Removing $$i ;\
+		$(RM) $(DESTDIR)$(CONF_DEST)/$$i $(DESTDIR)$(CONF_DEST)/$$i~ $(DESTDIR)$(CONF_DEST)/$$i.new ;\
+	done
+	-@test -d $(DESTDIR)$(CONF_DEST)/templates && $(RM) -r $(DESTDIR)$(CONF_DEST)/templates &&\
+	$(ECHO) "Removing $(DESTDIR)$(CONF_DEST)/templates/*"
+
+	@# man page and docs
+	@$(ECHO) Removing $(PROGRAM) docs
+	-$(RM) $(DESTDIR)$(MAN_DEST)/$(MAN_PAGE)*
+	-$(RM) -r $(DESTDIR)$(DOC_DEST) || $(RM) -r $(DESTDIR)$(prefix)/doc/privoxy
+
+	@# Log and pidfile
+	@$(ECHO) Removing $(PROGRAM) logs
+	-$(RM) $(DESTDIR)$(LOG_DEST)/logfile $(DESTDIR)$(PID_DEST)/privoxy.pid
+
+	@# Final clean up of unused directories. Special handling of CONF and LOG
+     # destinations.
+	@$(ECHO) Removing $(PROGRAM) directories
+	@for i in $(DESTDIR)$(LOG_DEST) $(DESTDIR)$(CONF_DEST); do \
+		if test -d $$i; then \
+			$(ECHO) Removing $$i ;\
+			$(RMDIR) $$i || $(ECHO) "$$i is not empty, not removed" ;\
+		fi;\
+	done
+	@if [  ! "$(prefix)" = "/usr/local" ] ;then \
+		for i in $(DESTDIR)$(MAN_DEST) $(DESTDIR)$(MAN_DIR) $(DESTDIR)$(SHARE_DEST)/doc \
+                         $(DESTDIR)$(SHARE_DEST) $(DESTDIR)$(SBIN_DEST); do \
+			if test -d $$i; then \
+				$(ECHO) Removing $$i ;\
+				$(RMDIR) $$i || $(ECHO) "$$i is not empty, not removed" ;\
+			fi;\
+		done;\
+		if test $(LOG_DEST) != /var/log/privoxy && test -d $(DESTDIR)$(prefix)/var/log; then \
+			$(ECHO) Removing $(DESTDIR)$(prefix)/var/log ;\
+			$(RMDIR) $(DESTDIR)$(prefix)/var/log || $(ECHO) "$(DESTDIR)$(prefix)/var/log is not empty, not removed";\
+		fi ;\
+		if test $(PID_DEST) != /var/run && test -d $(DESTDIR)$(prefix)/var/run; then \
+			$(ECHO) Removing $(DESTDIR)$(prefix)/var/run ;\
+			$(RMDIR) $(DESTDIR)$(prefix)/var/run || $(ECHO) "$(DESTDIR)$(prefix)/var/run is not empty, not removed";\
+		fi ;\
+		if test $(prefix)/var != /var && test -d $(DESTDIR)$(prefix)/var; then \
+			$(ECHO) Removing $(DESTDIR)$(prefix)/var ;\
+			$(RMDIR) $(DESTDIR)$(prefix)/var || $(ECHO) "$(DESTDIR)$(prefix)/var is not empty, not removed" ;\
+		fi ;\
+		if test $(prefix) != / && test $(prefix) != /usr && test -d $(DESTDIR)$(prefix); then \
+			$(ECHO) Removing $(DESTDIR)$(prefix) ;\
+			$(ECHO) Removing installation directory $(DESTDIR)$(prefix) ;\
+			$(RMDIR) $(DESTDIR)$(prefix) || $(ECHO) "$(DESTDIR)$(prefix) is not empty, not removed" ;\
+		fi;\
+	fi
+
+	@# init scripts
+	@if [ "$(prefix)" = "/usr/local" ] || [ "$(prefix)" = "/usr" ]; then \
+		$(ECHO) Removing $(PROGRAM) init script ;\
+		if [ -f $(DESTDIR)/etc/slackware-version ] && \
+	                [ -d $(DESTDIR)/etc/rc.d/ ]  && [ -w $(DESTDIR)/etc/rc.d/ ] ; then \
+			$(RM) $(DESTDIR)/etc/rc.d/rc.privoxy ;\
+		elif [ -d $(DESTDIR)/etc/init.d ] && [ -w $(DESTDIR)/etc/init.d ] ; then \
+			$(RM) $(DESTDIR)/etc/init.d/privoxy ;\
+		else \
+			$(ECHO) "Unable to remove privoxy init script, not installed or permission denied" ;\
+		fi ;\
+	fi
+	@$(ECHO) Privoxy uninstalled, bye
+
+coffee:
+	 @perl	-e 'print pack "C*", (31,139,8,8,153,63,226,60,2,3,99,111,102,102,101,'	 \
+		-e '101,0,109,143,205,13,192,32,8,133,239,78,241,110,234,1,28,160,171,'  \
+		-e '152,208,53,26,117,247,22,165,73,137,125,9,1,62,126,2,128,169,5,243,' \
+		-e '143,13,139,49,164,65,100,149,152,102,73,141,88,73,178,116,205,100,'  \
+		-e '69,253,36,102,81,49,83,236,19,225,171,131,214,172,163,73,4,168,123,' \
+		-e '115,71,126,247,122,94,128,178,227,95,154,12,86,215,122,197,249,146,' \
+		-e '187,54,220,125,193,51,228,11,1,0,0);' | zcat

+ 160 - 0
privoxy/INSTALL

@@ -0,0 +1,160 @@
+/*********************************************************************
+ *
+ * File        :  $Source: /cvsroot/ijbswa/current/doc/source/install.sgml,v $
+ *
+ * Purpose     :  INSTALL file to help with installing from source.
+ *
+ * Copyright   :  Written by and Copyright (C) 2001-2009 the
+ *                Privoxy team. https://www.privoxy.org/
+ *
+ *                Based on the Internet Junkbuster originally written
+ *                by and Copyright (C) 1997 Anonymous Coders and
+ *                Junkbusters Corporation.  http://www.junkbusters.com
+ *
+ *                This program is free software; you can redistribute it
+ *                and/or modify it under the terms of the GNU General
+ *                Public License as published by the Free Software
+ *                Foundation; either version 2 of the License, or (at
+ *                your option) any later version.
+ *
+ *                This program is distributed in the hope that it will
+ *                be useful, but WITHOUT ANY WARRANTY; without even the
+ *                implied warranty of MERCHANTABILITY or FITNESS FOR A
+ *                PARTICULAR PURPOSE.  See the GNU General Public
+ *                License for more details.
+ *
+ *                The GNU General Public License should be included with
+ *                this file.  If not, you can view it at
+ *                http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+ *                or write to the Free Software Foundation, Inc.,
+ *                51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+ *                USA
+ *
+ *********************************************************************/
+
+-------------------------------------------------------------------------------
+
+To build Privoxy from source, autoconf, GNU make (gmake), and, of course, a C
+compiler like gcc are required.
+
+When building from a source tarball, first unpack the source:
+
+ tar xzvf privoxy-3.0.29-stable-src.tar.gz
+ cd privoxy-3.0.29-stable
+
+To build the development version, you can get the source code by doing:
+
+  cd <root-dir>
+  git clone https://www.privoxy.org/git/privoxy.git
+
+This will create a directory named <root-dir>/privoxy/, which will contain the
+source tree.
+
+Note that source code in Git is development quality, and may not be stable or
+well tested.
+
+It is strongly recommended to not run Privoxy as root. You should configure/
+install/run Privoxy as an unprivileged user, preferably by creating a "privoxy"
+user and group just for this purpose. See your local documentation for the
+correct command line to do add new users and groups (something like adduser,
+but the command syntax may vary from platform to platform).
+
+/etc/passwd might then look like:
+
+  privoxy:*:7777:7777:privoxy proxy:/no/home:/no/shell
+
+And then /etc/group, like:
+
+  privoxy:*:7777:
+
+Some binary packages may do this for you.
+
+Then, to build from either unpacked tarball or Git checkout:
+
+ autoheader
+ autoconf
+ ./configure      # (--help to see options)
+ make             # (the make from GNU, sometimes called gmake)
+ su               # Possibly required
+ make -n install  # (to see where all the files will go)
+ make -s install  # (to really install, -s to silence output)
+
+Using GNU make, you can have the first four steps automatically done for you by
+just typing:
+
+  make
+
+in the freshly downloaded or unpacked source directory.
+
+To build an executable with security enhanced features so that users cannot
+easily bypass the proxy (e.g. "Go There Anyway"), or alter their own
+configurations, configure like this:
+
+ ./configure  --disable-toggle  --disable-editor  --disable-force
+
+Note that all of these options can also be disabled through the configuration
+file.
+
+WARNING: If installing as root, the install will fail unless a non-root user or
+group is specified, or a privoxy user and group already exist on the system. If
+a non-root user is specified, and no group, then the installation will try to
+also use a group of the same name as "user". If a group is specified (and no
+user), then the support files will be installed as writable by that group, and
+owned by the user running the installation.
+
+configure accepts --with-user and --with-group options for setting user and
+group ownership of the configuration files (which need to be writable by the
+daemon). The specified user must already exist. When starting Privoxy, it must
+be run as this same user to insure write access to configuration and log files!
+
+Alternately, you can specify user and group on the make command line, but be
+sure both already exist:
+
+ make -s install  USER=privoxy GROUP=privoxy
+
+The default installation path for make install is /usr/local. This may of
+course be customized with the various ./configure path options. If you are
+doing an install to anywhere besides /usr/local, be sure to set the appropriate
+paths with the correct configure options (./configure --help). Non-privileged
+users must of course have write access permissions to wherever the target
+installation is going.
+
+If you do install to /usr/local, the install will use sysconfdir=$prefix/etc/
+privoxy by default. All other destinations, and the direct usage of
+--sysconfdir flag behave like normal, i.e. will not add the extra privoxy
+directory. This is for a safer install, as there may already exist another
+program that uses a file with the "config" name, and thus makes /usr/local/etc
+cleaner.
+
+If installing to /usr/local, the documentation will go by default to $prefix/
+share/doc. But if this directory doesn't exist, it will then try $prefix/doc
+and install there before creating a new $prefix/share/doc just for Privoxy.
+
+Again, if the installs goes to /usr/local, the localstatedir (ie: var/) will
+default to /var instead of $prefix/var so the logs will go to /var/log/privoxy
+/, and the pid file will be created in /var/run/privoxy.pid.
+
+make install will attempt to set the correct values in config (main
+configuration file). You should check this to make sure all values are correct.
+If appropriate, an init script will be installed, but it is up to the user to
+determine how and where to start Privoxy. The init script should be checked for
+correct paths and values, if anything other than a default install is done.
+
+If install finds previous versions of local configuration files, most of these
+will not be overwritten, and the new ones will be installed with a "new"
+extension. default.action and default.filter will be overwritten. You will then
+need to manually update the other installed configuration files as needed. The
+default template files will be overwritten. If you have customized, local
+templates, these should be stored safely in a separate directory and defined in
+config by the "templdir" directive. It is of course wise to always back-up any
+important configuration files "just in case". If a previous version of Privoxy
+is already running, you will have to restart it manually.
+
+For more detailed instructions on how to build Redhat RPMs, Windows
+self-extracting installers, building on platforms with special requirements
+etc, please consult the developer manual.
+
+The simplest command line to start Privoxy is $path/privoxy --user=privoxy
+$path/etc/privoxy/config. See privoxy --usage, or the man page, for other
+options, and configuration.
+

+ 339 - 0
privoxy/LICENSE

@@ -0,0 +1,339 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                    GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+                            NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License along
+    with this program; if not, write to the Free Software Foundation, Inc.,
+    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.

+ 674 - 0
privoxy/LICENSE.GPLv3

@@ -0,0 +1,674 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<https://www.gnu.org/licenses/>.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+<https://www.gnu.org/licenses/why-not-lgpl.html>.

+ 77 - 0
privoxy/Makefile

@@ -0,0 +1,77 @@
+# Written by and Copyright (C) 2001 the
+# Privoxy team. https://www.privoxy.org/
+#
+# Based on the Internet Junkbuster originally written
+# by and Copyright (C) 1997 Anonymous Coders and 
+# Junkbusters Corporation.  http://www.junkbusters.com
+#
+# This program is free software; you can redistribute it 
+# and/or modify it under the terms of the GNU General
+# Public License as published by the Free Software
+# Foundation; either version 2 of the License, or (at
+# your option) any later version.
+#
+# This program is distributed in the hope that it will
+# be useful, but WITHOUT ANY WARRANTY; without even the
+# implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.  See the GNU General Public
+# License for more details.
+#
+# The GNU General Public License should be included with
+# this file.  If not, you can view it at
+# http://www.gnu.org/copyleft/gpl.html
+# or write to the Free Software Foundation, Inc., 59
+# Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+#
+#############################################################################
+
+GNU_MAKE_CMD = gmake
+MAKE_CMD     = make
+
+error:
+	@if [ -f GNUmakefile ]; then \
+	    echo "***"; \
+	    echo "*** You are not using the GNU version of Make - maybe it's called gmake"; \
+	    echo "*** or it's in a different PATH? Please read INSTALL." ; \
+	    echo "***"; \
+	    exit 1; \
+	 elif test -n "$(HOST_ARCH)"  && test -z "$(MAKE_VERSION)" ; then \
+	    echo "***"; \
+	    echo "*** You are not using GNU Make on Solaris, please make sure you do" ; \
+	    echo "*** and re-run 'make' "; \
+	    echo "***"; \
+	    exit 1 ; \
+	 elif test -n "$(MACHINE_ARCH)"  && test -z "$(MAKE_VERSION)" ; then \
+	    echo "***"; \
+	    echo "*** You are not using GNU Make on FreeBSD, please make sure you do" ; \
+	    echo "*** and re-run 'make' "; \
+	    echo "***"; \
+	    exit 1 ; \
+	 else \
+	    echo "***"; \
+	    echo "*** To build this program, you must run"; \
+	    echo "*** autoheader && autoconf && ./configure and then run GNU make."; \
+	    echo "***"; \
+	    echo -n "*** Shall I do this for you now? (y/n) "; \
+	    read answer; \
+	    if [ "$$answer" = "y" ]; then \
+		autoheader && autoconf && ./configure || exit 1; \
+	  	if $(GNU_MAKE_CMD) -v |grep GNU >/dev/null 2>/dev/null; then \
+		   $(GNU_MAKE_CMD) ;\
+		elif $(MAKE_CMD) -v |grep GNU >/dev/null 2>/dev/null; then \
+		   $(MAKE_CMD) ;\
+		else \
+		   echo "Neither 'make' nor 'gmake' are GNU compatible!" ; \
+		   echo "Please read INSTALL." ; \
+		   exit 1 ; \
+		fi ;\
+	    fi; \
+	 fi
+
+.PHONY: error
+
+#############################################################################
+
+## Local Variables:
+## tab-width: 3
+## end:

+ 323 - 0
privoxy/README

@@ -0,0 +1,323 @@
+/*********************************************************************
+ *
+ * File        :  doc/source/readme.sgml
+ *
+ * Purpose     :  README file to give a short intro.
+ *
+ * Copyright   :  Written by and Copyright (C) 2001-2020 the
+ *                Privoxy team. https://www.privoxy.org/
+ *
+ *                Based on the Internet Junkbuster originally written
+ *                by and Copyright (C) 1997 Anonymous Coders and
+ *                Junkbusters Corporation.  http://www.junkbusters.com
+ *
+ *                This program is free software; you can redistribute it
+ *                and/or modify it under the terms of the GNU General
+ *                Public License as published by the Free Software
+ *                Foundation; either version 2 of the License, or (at
+ *                your option) any later version.
+ *
+ *                This program is distributed in the hope that it will
+ *                be useful, but WITHOUT ANY WARRANTY; without even the
+ *                implied warranty of MERCHANTABILITY or FITNESS FOR A
+ *                PARTICULAR PURPOSE.  See the GNU General Public
+ *                License for more details.
+ *
+ *                The GNU General Public License should be included with
+ *                this file.  If not, you can view it at
+ *                http://www.gnu.org/copyleft/gpl.html
+ *                or write to the Free Software Foundation, Inc.,
+ *                51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+ *                USA
+ *
+ *********************************************************************/
+
+This README is included with Privoxy 3.0.29. See https://www.privoxy.org/ for
+more information. The current code maturity level is "stable".
+
+-------------------------------------------------------------------------------
+
+Privoxy is a non-caching web proxy with advanced filtering capabilities for
+enhancing privacy, modifying web page data and HTTP headers, controlling
+access, and removing ads and other obnoxious Internet junk. Privoxy has a
+flexible configuration and can be customized to suit individual needs and
+tastes. It has application for both stand-alone systems and multi-user
+networks.
+
+Privoxy is Free Software and licensed under the GNU GPLv2 or later.
+
+Privoxy is an associated project of Software in the Public Interest (SPI).
+
+Helping hands and donations are welcome:
+
+  * https://www.privoxy.org/faq/general.html#PARTICIPATE
+
+  * https://www.privoxy.org/faq/general.html#DONATE
+
+-------------------------------------------------------------------------------
+
+1. CHANGES
+
+For a list of changes in this release, please have a look at the "ChangeLog",
+the "What's New" section or the "Upgrader's Notes" in the User Manual.
+
+-------------------------------------------------------------------------------
+
+2. INSTALL
+
+See the INSTALL file in this directory, for installing from raw source, and the
+User Manual, for all other installation types.
+
+-------------------------------------------------------------------------------
+
+3. RUN
+
+privoxy [--help] [--version] [--no-daemon] [--pidfile PIDFILE] [--user USER
+[.GROUP]] [--chroot] [--pre-chroot-nslookup HOSTNAME ][config_file]
+
+See the man page or User Manual for an explanation of each option, and other
+configuration and usage issues.
+
+If no config_file is specified on the command line, Privoxy will look for a
+file named 'config' in the current directory (except Win32 which will look for
+'config.txt'). If no config_file is found, Privoxy will fail to start.
+
+-------------------------------------------------------------------------------
+
+4. CONFIGURATION
+
+See: 'config', 'default.action', 'user.action', 'default.filter', and
+'user.filter'. 'user.action' and 'user.filter' are for personal and local
+configuration preferences. These are all well commented. Most of the magic is
+in '*.action' files. 'user.action' should be used for any actions
+customizations. On Unix-like systems, these files are typically installed in /
+etc/privoxy. On Windows, then wherever the executable itself is installed.
+There are many significant changes and advances from earlier versions. The User
+Manual has an explanation of all configuration options, and examples: https://
+www.privoxy.org/user-manual/.
+
+Be sure to set your browser(s) for HTTP/HTTPS Proxy at <IP>:<Port>, or whatever
+you specify in the config file under 'listen-address'. DEFAULT is
+127.0.0.1:8118. Note that Privoxy ONLY proxies HTTP (and HTTPS) traffic. Do not
+try it with FTP or other protocols for the simple reason it does not work.
+
+The actions list can be configured via the web interface accessed via http://
+p.p/, as well other options.
+
+-------------------------------------------------------------------------------
+
+5. DOCUMENTATION
+
+There should be documentation in the 'doc' subdirectory. In particular, see the
+User Manual there, the FAQ, and those interested in Privoxy development, should
+look at developer-manual.
+
+The source and configuration files are all well commented. The main
+configuration files are: 'config', 'default.action', and 'default.filter'.
+
+Included documentation may vary according to platform and packager. All
+documentation is posted on https://www.privoxy.org, in case you don't have it,
+or can't find it.
+
+-------------------------------------------------------------------------------
+
+6. CONTACTING THE DEVELOPERS, BUG REPORTING AND FEATURE REQUESTS
+
+We value your feedback. In fact, we rely on it to improve Privoxy and its
+configuration. However, please note the following hints, so we can provide you
+with the best support.
+
+-------------------------------------------------------------------------------
+
+6.1. Please provide sufficient information
+
+A lot of support requests don't contain enough information and can't be solved
+without a lot of back and forth which causes unnecessary delays. Reading this
+section should help to prevent that.
+
+Before contacting us to report a problem, please try to verify that it is a 
+Privoxy problem, and not a browser or site problem or documented behaviour that
+just happens to be different than what you expected. If unsure, try toggling
+off Privoxy, and see if the problem persists.
+
+If you are using your own custom configuration, please try the default
+configuration to see if the problem is configuration related. If you're having
+problems with a feature that is disabled by default, please ask around on the
+mailing list if others can reproduce the problem.
+
+If you aren't using the latest Privoxy version, the problem may have been found
+and fixed in the meantime. We would appreciate if you could take the time to
+upgrade to the latest version and verify that the problem still exists.
+
+Please be sure to provide the following information when reporting problems or
+requesting support:
+
+  * The exact Privoxy version you are using.
+
+  * The operating system and versions you run Privoxy on, e.g. Windows XP SP2.
+
+  * The name, platform, and version of the browser you were using (e.g. 
+    Internet Explorer v5.5 for Mac).
+
+  * The URL where the problem occurred, or some way for us to duplicate the
+    problem (e.g. http://somesite.example.com/?somethingelse=123).
+
+  * Whether your version of Privoxy is one supplied by the Privoxy developers
+    via SourceForge, or if you got your copy somewhere else.
+
+  * Whether you are using Privoxy together with another proxy such as Tor. If
+    so, please temporary disable the other proxy to see if the symptoms change.
+
+  * Whether you are using a personal firewall product. If so, does Privoxy work
+    without it?
+
+  * Any other pertinent information to help identify the problem such as config
+    or log file excerpts (yes, you should have log file entries for each action
+    taken). To get a meaningful logfile, please make sure that the logfile
+    directive is being used and the following debug options are enabled (all of
+    them):
+
+    debug     1 # Log the destination for each request Privoxy let through.
+                #   See also debug 1024.
+    debug     2 # show each connection status
+    debug     4 # show I/O status
+    debug     8 # show header parsing
+    debug   128 # debug redirects
+    debug   256 # debug GIF de-animation
+    debug   512 # Common Log Format
+    debug  1024 # Log the destination for requests Privoxy didn't let through,
+                #   and the reason why.
+    debug  4096 # Startup banner and warnings.
+    debug  8192 # Non-fatal errors
+    debug 65536 # Log applying actions
+
+    If you are having trouble with a filter, please additionally enable
+
+    debug    64 # debug regular expression filters
+
+    If you suspect that Privoxy interprets the request or the response
+    incorrectly, please enable
+
+    debug 32768 # log all data read from the network
+
+    It's easy for us to ignore log messages that aren't relevant but missing
+    log messages may make it impossible to investigate a problem. If you aren't
+    sure which of the debug directives are relevant, please just enable all of
+    them and let us worry about it.
+
+    Note that Privoxy log files may contain sensitive information so please
+    don't submit any logfiles you didn't read first. You can mask sensitive
+    information as long as it's clear that you removed something.
+
+You don't have to tell us your actual name when filing a problem report, but if
+you don't, please use a nickname so we can differentiate between your messages
+and the ones entered by other "anonymous" users that may respond to your
+request if they have the same problem or already found a solution. Note that
+due to spam the trackers may not always allow to post without being logged into
+SourceForge. If that's the case, you are still free to create a login that
+isn't directly linked to your name, though.
+
+Please also check the status of your request a few days after submitting it, as
+we may request additional information. If you use a SF id, you should
+automatically get a mail when someone responds to your request. Please don't
+bother to add an email address when using the tracker. If you prefer to
+communicate through email, just use one of the mailing lists directly.
+
+If you are new to reporting problems, you might be interested in How to Report
+Bugs Effectively.
+
+The appendix of the Privoxy User Manual also has helpful information on
+understanding actions, and action debugging.
+
+-------------------------------------------------------------------------------
+
+6.2. Get Support
+
+All users are welcome to discuss their issues on the users mailing list, where
+the developers also hang around.
+
+Please don't send private support requests to individual Privoxy developers,
+either use the mailing lists or the support trackers.
+
+If you have to contact a Privoxy developer directly for other reasons, please
+send a real mail and do not bother with SourceForge's messaging system. Answers
+to SourceForge messages are usually bounced by SourceForge's mail server in
+which case the developer wasted time writing a response you don't get. From
+your point of view it will look like your message has been completely ignored,
+so this is frustrating for all parties involved.
+
+Note that the Privoxy mailing lists are moderated. Posts from unsubscribed
+addresses have to be accepted manually by a moderator. This may cause a delay
+of several days and if you use a subject that doesn't clearly mention Privoxy
+or one of its features, your message may be accidentally discarded as spam.
+
+If you aren't subscribed, you should therefore spend a few seconds to come up
+with a proper subject. Additionally you should make it clear that you want to
+get CC'd. Otherwise some responses will be directed to the mailing list only,
+and you won't see them.
+
+-------------------------------------------------------------------------------
+
+6.3. Reporting Problems
+
+"Problems" for our purposes, come in two forms:
+
+  * Configuration issues, such as ads that slip through, or sites that don't
+    function properly due to one Privoxy "action" or another being turned "on".
+
+  * "Bugs" in the programming code that makes up Privoxy, such as that might
+    cause a crash. Documentation issues, for example spelling errors and
+    unclear descriptions, are bugs, too.
+
+-------------------------------------------------------------------------------
+
+6.3.1. Reporting Ads or Other Configuration Problems
+
+Please send feedback on ads that slipped through, innocent images that were
+blocked, sites that don't work properly, and other configuration related
+problem of default.action file, to https://sourceforge.net/tracker/?group_id=
+11118&atid=460288, the Actions File Tracker.
+
+-------------------------------------------------------------------------------
+
+6.3.2. Reporting Bugs
+
+Before reporting bugs, please make sure that the bug has not already been
+submitted and observe the additional hints at the top of the submit form. If
+already submitted, please feel free to add any info to the original report that
+might help to solve the issue.
+
+-------------------------------------------------------------------------------
+
+6.4. Reporting security problems
+
+If you discovered a security problem or merely suspect that a bug might be a
+security issue, please mail Fabian Keil <fk@fabiankeil.de> (OpenPGP
+fingerprint: 4F36 C17F 3816 9136 54A1 E850 6918 2291 8BA2 371C).
+
+Usually you should get a response within a day, otherwise it's likely that
+either your mail or the response didn't make it. If that happens, please mail
+to the developer list to request a status update.
+
+-------------------------------------------------------------------------------
+
+6.5. Mailing Lists
+
+If you prefer to communicate through email, instead of using a web interface,
+feel free to use one of the mailing lists. To discuss issues that haven't been
+completely diagnosed yet, please use the Privoxy users list. Technically
+interested users and people who wish to contribute to the project are always
+welcome on the developers list. You can find an overview of all Privoxy-related
+mailing lists, including list archives, at: https://lists.privoxy.org/mailman/
+listinfo. The lists hosted on privoxy.org have been created in 2016, the
+previously-used lists hosted at SourceForge are deprecated but the archives may
+still be useful: https://sourceforge.net/mail/?group_id=11118.
+
+-------------------------------------------------------------------------------
+
+6.6. SourceForge support trackers
+
+The SourceForge support trackers may be used as well, but have various
+technical problems that are unlikely to be fixed anytime soon. If you don't get
+a timely response, please try the mailing list as well.
+

+ 489 - 0
privoxy/TODO

@@ -0,0 +1,489 @@
+Some Privoxy-related tasks, sorted by the time they
+have been added, not by priority.
+
+The latest version should be available at:
+https://www.privoxy.org/gitweb/?p=privoxy.git;a=blob_plain;f=TODO;hb=HEAD
+
+There's work in progress to fund development on these items using
+donations. If you want to donate, please have a look at:
+https://www.privoxy.org/faq/general.html#DONATE
+
+1)  Add more regression tests. Filters should be tested automatically
+    (variables too). Could probably reuse large parts of Privoxy-Filter-Test.
+    Note that there is currently work in progress to leverage curl's
+    test suite, patches have been submitted upstream:
+    http://curl.haxx.se/mail/lib-2014-06/0070.html
+
+3)  Fix some more XXX: comments.
+
+6)  Remove actions that aren't needed anymore:
+
+    content-type-overwrite should probably stay as it's also
+    used by some of the CGI pages (XXX: name them).
+
+    crunch-client-header and crunch-server-header should probably
+    go, their only advantage is that their search strings can be
+    controlled through the CGI pages, other than that they only
+    have disadvantages.
+
+    crunch-if-none-match can be replaced with a header filter.
+
+    prevent-compression has a misleading name and could
+    be replaced with a header filter.
+
+7)  force-text-mode has a stupid name and should probably
+    be renamed to force-filter-mode.
+
+8)  handle-as-empty-document and handle-as-image should
+    be merged to something like handle-as{something} to
+    prevent them from being activated at the same time.
+
+10) There's a bug in the CGI editor that turns the
+    first section's "Insert new section below" into
+    a "Insert new section above" button.
+
+11) CGI templates should use semantically-correct HTML
+    and scale properly.
+
+    Work in progress.
+
+12) Support pipelining for outgoing connections.
+
+14) Allow to filter POST parameters.
+
+19) enable-forward-fallback. Syntax? Suggested by K.R.
+
+21) User Manual delivery doesn't accept multiple slashes. Should it?
+
+22) Verify action files properly (Including arguments) and
+    act accordingly (should probably intercept all requests
+    with a "Invalid option foo detected" CGI page).
+
+23) Do the same in case of syntax errors in the configuration file,
+    instead of just exiting or ignoring the problem.
+
+25) Handle multiple filters with the same name better. Reject them?
+
+26) Let show-url-info detect clearly invalid URLs.
+
+27) Make errno logging less thread-unsafe.
+    Verify that it's really an improvement.
+
+28) Don't take default ports in case of invalid forwarding ports.
+
+31) If a string action foo is disabled csp->action->string[ACTION_STRING_FOO]
+    doesn't necessarily contain NULL, but may contain the string of an
+    enabled foo action in an overruled section. Is it a bug? Does it matter?
+
+32) In case of forwarding failures with socks port == 9050,
+    show extra info about Tor (the whole FAQ entry?).
+
+36) Unload unused action files directly, even if they are
+    disabled without replacement.
+
+38) In the final results, explicitly list disabled multi actions
+    with their parameters. Not as trivial as it sounds.
+
+40) When running in daemon mode, Privoxy's working directory is '/'
+    which means it may not have permissions to dump core when necessary.
+    Figure out a way to solve this. Introduce a cwd config option?
+
+41) Change documentation framework to one that works cross-platform.
+    Evaluate WML and txt2tags.
+
+42) Add a DTrace USDT provider. Now that FreeBSD has userland DTrace
+    support there's no longer any reason not to.
+
+43) Write a tool to check URL patterns against URLs in the log file.
+    This could be included in Privoxy-Regression-Test.
+
+44) Privoxy-Log-Parser: Consider highlighting "Connection" in:
+    23:13:03.506 283b6100 Header: Replaced: 'Connection: Keep-Alive' with 'Connection: close'
+
+50) Investigate possible PCRS template speedup when searching
+    macros with strstr() before compiling pcrs commands.
+    Investigated, needs some restructuring but is probably worth it.
+
+51) Make user-manual directive more generic to allow serving the FAQ
+    and files from user-specified directories. Consider changing the
+    port for "same origin policy" issues.
+
+53) Find a more reliable hoster. Involves finding out what our
+    requirements are and which SF alternatives fulfil them.
+    It would probably also make sense to look into what other
+    projects did when migrating away from SF.
+
+    2014-05: Work in progress. Hosting wish list at the end
+             of this file. Looks like most of the other projects
+             that left SF had lower standards and moved to hosters
+             that don't come close to sattisfying the requirements.
+    2016-03: The website has been moved away from SF infrastructure
+             and is also available through https:// now.
+    2016-04: Server rent for a year has been sponsored by ChameleonJohn.
+    2016-04: The SF mailing lists have been deprecated, the new ones
+             are available at: https://lists.privoxy.org/
+
+    Interested donors: 1.
+
+58) Move more template strings from the code into the actual templates.
+
+59) Import the German template translation.
+
+60) Ask the Russian translators for input on how to make their
+    life easier.
+
+61) Consider (optionally?) skipping the hostname comparison when
+    checking if a connections that goes to a HTTP proxy can be reused.
+    Do all HTTP proxy support that? Is it worth it?
+
+63) Reject clearly too large requests earlier?
+
+64) Use proper copyright attribution. "Privoxy Developers"
+    is no legal entity.
+
+65) Polish Website. Probably involves ditching the Docbook
+    mess. There are already several threads in the mailinglist
+    archives about this. See also #41.
+
+66) Stop hard-coding the number of action and filter files.
+
+67) Clean up source code directory layout.
+
+68) Use standard make syntax so we don't depend on GNU make.
+
+69) Update autoconf setup (or move away from it).
+    Unfortunately the autoconf files can't be simply updated
+    due to license issues:
+    https://lists.privoxy.org/pipermail/privoxy-devel/2016-April/000008.html
+
+70) If the server connection is reset but the headers are
+    received, consider passing the mess to the client instead
+    of showing the connect-failed template. Relates to #2698674.
+
+74) Let Privoxy-Regression-Test optionally check that action
+    sections which disable actions actually are preceded by
+    sections that enable said actions.
+
+75) Create a tool that creates Privoxy action (and filter?) files
+    out of adblock files. Could be implemented as option for
+    url-pattern-translator.pl.
+
+76) Cache DNS responses. Note that this has been requested
+    several times by users, but is not a developer priority.
+    If you care about this, feel free to submit patches.
+
+77) Allow to configure the IP address used in outgoing connections.
+
+78) Allow to optionally use pcre's DFA algorithm.
+
+79) Evaluate pcre alternatives.
+
+82) Detect if the system time goes back in time let the user
+    know if it caused any connections to get closed.
+
+85) Once #51 is done, write a script that populates a directory with
+    various common third-party icons (stumbleupon.png, facebook.png ...)
+    and redirect requests for them to Privoxy.
+
+86) Add a server-body-tagger action. This is trivial as as all the
+    functionality required to do it already exists.
+
+87) Add a client-body-tagger action. This is less trivial as we currently
+    don't buffer client bodies. After 14) is implemented it would be
+    trivial, though.
+
+88) Investigate if there's a Perl module that Privoxy-Regression-Test
+    could optionally use to keep connections alive, preferably while
+    requiring less forks at the same time.
+
+89) When multiple block actions apply, consider showing all the block
+    reasons on the blocked page that haven't been overruled, not just
+    the last one.
+
+91) Add an optional limit for internal redirects. It would probably
+    be reasonable to default to a limit of one and showing an error
+    message if the request for the redirect URL would be redirected
+    again.
+
+92) The statistics currently aren't calculated correctly by Privoxy
+    as each thread is only counted as one request which is no longer
+    correct. This should be fixed, or the statistic code removed.
+    Privoxy-Log-Parser's provides more detailed statistics, anyway.
+
+93) Add a config directive to let Privoxy explicitly request either
+    IPv4 (or IPv6) addresses, even if the system supports both.
+    Could be useful as a workaround for misconfigured setups where the
+    libc returns IPv6 addresses even if there's no IPv6 connectivity.
+
+94) Add a config directive to let Privoxy prefer either IPv4 (or IPv6)
+    addresses, instead of trusting the libc to return them in an order
+    that makes sense. Like #93, this could be useful as a workaround
+    for misconfigured setups.
+
+96) Filters should be easier to look up. Currently get_filter() has to
+    go through all filters and skip the filter types the caller isn't
+    interested in.
+
+98) When showing action sections on the CGI pages, properly escape
+    line breaks so they can be copy&pasted into action files without
+    adjustments.
+
+99) Figure out a mechanism through which a user can easily enable
+    site-specific action sections that are too aggressive to be
+    enabled by default. This could be similar to the presettings
+    in default.action, but could also be just another action file
+    that isn't used by default.
+
+100) Create a cross-platform Privoxy control program and retire
+     the win32 GUI. Integrate support for Privoxy-Regression-Test,
+     Privoxy-Log-Parser, Privoxy-Filter-Test, uagen and similar tools.
+     Interested donors: 1.
+
+102) Add an include directive to split the config file into several parts.
+
+103) Potential performance improvement for large action files:
+     when figuring out which actions apply, check the action bit mask
+     before pattern matching and skip section that wouldn't modify the
+     actions already set. To increase the impact the sections would have
+     to be applied in reverse.
+
+104) The code to modify global_toggle_state should be factored out into
+     a separate function. Currently we mess with it in three different
+     files, but only in w32log.c the tray icon is explicitly set.
+     The logging is inconsistent as well. For details see #3525694.
+
+106) actionlist.h should be embedded in a way that causes less text
+     segment bloat.
+
+107) Support more pcrs variables, for example $destination-ip-address
+     and $source-ip-address.
+
+108) Allow to use a somewhat random string instead of PRIVOXY-FORCE.
+
+109) Let log_error() support the format specifier %S which should
+     work like %s but escape new lines like %N. This would be useful
+     to log the result of header filters which may inject new lines.
+
+110) Add a global-buffer-limit directive that roughly limits how
+     much malloc'ed memory Privoxy will use and can potentially
+     be smaller than (buffer-limit * max-client-connections).
+
+111) Reject requests if hosts and ports in request line and Host
+     header don't match (before filters have been applied).
+
+112) If a header filter is used to inject another header by inserting
+     a \r\n (undocumented feature), detect it and split the headers so
+     following header actions do not treat them as a single string.
+     Alternatively add another header injection mechanism.
+
+113) Log statistics upon receiving a certain signal (SIGINFO or SIGUSR1).
+
+114) Properly deal with status code 100. The current "Continue hack"
+     can cause problems for gpg when uploading keys through Privoxy.
+
+115) Add ICAP (RFC 3507) support. FR #3615158.
+
+116) Due to the use of sscanf(), Privoxy currently will fail to properly
+     parse chunks whose size can't be represented with 32 bit. This is
+     unlikely to cause problems in the real world, but should eventually
+     be fixed anyway. See also:
+     https://bugzilla.mozilla.org/show_bug.cgi?id=959100
+
+118) There should be "escaped" dynamic variables that are guaranteed
+     not to break filters.
+
+120) Add an option to limit pcre's recursion limit below the default.
+     On some platforms the recursion limit doesn't prevent pcre from
+     running out of stack space, causing the kernel to kill Privoxy
+     ungracefully.
+
+121) Add HTTP/2 support. As a first step, incoming HTTP/1.x requests
+     should be translated to outgoing HTTP/2 requests where possible
+     (and if desired by the user).
+     Interested donors: 1.
+
+122) Allow customized log messages.
+
+124) Add support for the "lightweight OS capability and sandbox framework"
+     Capsicum. http://www.cl.cam.ac.uk/research/security/capsicum/
+     Interested donors: 1.
+
+125) Allow clients to HTTPS-encrypt the proxy connection.
+     Interested donors: 1.
+
+126) Run the Co-Advisor HTTP compliance tests, evaluate the results,
+     fix the compliance issues that aren't by design and document
+     the rest.
+     Note that Privoxy developers qualify for free account upgrades:
+     http://coad.measurement-factory.com/details.html#pricing
+
+127) Add "real" CGI support (serve program output instead of forwarding
+     the request). The work is mostly done due to +external-filter{}.
+
+128) Add a config directive to control the stack limit.
+
+129) Completely implement RFC 7230 4.1 (Chunked Transfer Coding).
+     Currently Privoxy doesn't properly deal with trailers which
+     are rarely used in the real world but should be supported anyway.
+
+130) Move header_tagger() out of the parser structs and let it execute
+     taggers one-by-one against all headers so the header order has less
+     influence on the tagging result. As a bonus, dynamic taggers would
+     have to be compiled less often.
+
+131) The handle-as-empty-doc-returns-ok directive should be replaced with
+     an action so the behaviour can be enabled on a per-request basis.
+     Interested donors: 1.
+
+133) Consider allowing bitcoin donations.
+     Interested donors: 2.
+
+134) Track the total number of bytes written to and received from a socket.
+
+135) Add OpenBSM audit support.
+
+136) Make builds reproducible.
+
+137) Add a (preferably vector-based) logo.
+
+138) Bring back the scripts to provide actions file feedback.
+
+     Once upon a time (~2003) there were scripts on the webserver
+     to make reporting action file feedback more convenient for the
+     user and the actual reports more useful for the developers.
+     They have been unusable for years and have thus been disabled,
+     but making the reporting mechanism available again would be a
+     good idea.
+
+140) Toggling Privoxy off currently also disables stuff that
+     probably shouldn't be affected (such as actions like
+     forward-override). Investigate and fix or document.
+
+141) Port Privoxy to CloudABI, which, despite the name, is actually
+     rather neat. https://github.com/NuxiNL/cloudlibc
+
+142) Remove or update the "internal" pcre version.
+
+143) Add support for OpenBSD's pledge feature once it's stablelized.
+     This should be a lot less work then #124.
+
+146) Allow to save the internal client tag state to disk and
+     load it after restarts.
+
+147) Improve "Building from Source" section in the user manual.
+     A common problem seems to be that it's not obvious to non-technical
+     users how the listed dependencies can be installed on the commonly
+     used platforms. Adding a couple of examples should also be useful for
+     technical users (like Privoxy developers) who want to install or test
+     Privoxy on platforms they are not familiar with.
+
+148) Add a config directive to change the CGI_SITE_2_HOST
+     (default: config.privoxy.org).
+
+     If Privoxy is used as reverse proxy or intercepting proxy without
+     getting intercepted requests, error pages created from default templates
+     currently can result in client requests to config.privoxy.org on the
+     Internet which may not be desirable.
+
+150) Add blacklistd support.
+
+151) Let the dok-tidy target work cross-platform without introducing
+     a ton of white-space changes that hide the content changes.
+
+152) Fix CSS references in the website documentation.
+     For many pages p_doc.css is specified twice using different paths.
+     Usually at least one works, but not all of them do and the
+     duplicated requests are pointless even if they don't end up with
+     a 404.
+
+153) Catch SIGINT and use it to close the listen socket, serve
+     remaining connections and shut down. This would allow higher
+     uptime and make testing more convenient.
+
+154) Underline links in docs and cgi pages. More precisely,
+     don't mess with the browser defaults for link underlining.
+
+155) The sig_handler() shouldn't call log_error().
+     While it isn't known to cause actual problems in normal operation,
+     it's technically incorrect and causes crashes when running in
+     valgrind.
+
+156) Reject socks requests with an explicit error message similar
+     to the one used for ftp. Motivation:
+     https://lists.privoxy.org/pipermail/privoxy-users/2017-March/000195.html
+
+158) Use a single thread to wait for new requests on reused client connections.
+     Currently the thread that handles the first request on a connection
+     stays responsible for the client connect until it gets closed.
+     In case of lots of idle connections lots of waiting threads are used.
+     While it's conceivable that this ineffiency is irrelevant from a
+     performance point of view, using a single thread should reduce Privoxy's
+     memory footprint a bit which may be noticeable in case of multi-user setups
+     with hundreds of idle connections.
+
+161) Properly support requests with chunked transfer-encoding with https inspection.
+
+162) When https inspecting, delete generated keys and certificates if
+     the connection to the destination could not be established.
+     Makes silly DoS attacks slightly more complicated.
+
+163) Use subdirectories in the certificate-directory to lower the number
+     of files per directory.
+
+164) Evaluate switching from pcreposix(3) to pcre's native api
+     for URL matching which allows to compile the patterns once
+     at load-time.
+
+165) Add a max-connections-per-client directive.
+
+166) Figure out how to ship Windows binaries with external libraries
+     like pcre and MbedTLS. Required for #142. Somewhat related:
+     https://lists.privoxy.org/pipermail/privoxy-devel/2020-November/000400.html
+
+##########################################################################
+
+Hosting wish list (relevant for #53)
+
+What we need:
+
+- Bug tracker
+- Mailinglists (Mailman with public archives preferred)
+- Webspace (on a Unix-like OS that works with the webserver targets
+  in GNUMakefile)
+- Git repositories
+- Commit mails (preferably with unified diffs)
+
+(Unsorted) details to look at when evaluating hosters:
+
+1. Preferably no third-party ads and trackers.
+   External images, CSS and JavaScript may count as trackers
+   but texts like "supported by company XYZ" may be acceptable.
+
+2. JavaScript should be optional or not used at all.
+
+3. Services we don't need shouldn't be enabled anyway.
+   (We currently don't use Web forums, wikis, surveys etc.)
+
+4. It would be preferable if the hoster didn't have a bad track
+   record as far as user experience, security and privacy are
+   concerned and if the terms of service are "reasonable" and
+   haven't changed too often in the past. Updates in the past
+   should have been improvements and not regressions.
+
+5. It would be preferable if most of the server administration
+   is done by a trusted third-party (or at least not a lot of work
+   for us).
+
+6. The server(s) should be located in a country with laws we can
+   understand and follow (or at least not unintentionally violate).
+
+7. A server location in a country with some kind of due process
+   and strong data protection laws (at least on paper) would be
+   preferable.
+
+8. Given that Privoxy is a free software project it would be
+   preferable if the hoster would use free software where possible.
+
+9. Migrating away from the hoster in the future without losing
+   any important data should be possible without writing web
+   scrapers first.

+ 273 - 0
privoxy/acconfig.h

@@ -0,0 +1,273 @@
+#ifndef CONFIG_H_INCLUDED
+#define CONFIG_H_INCLUDED
+/*********************************************************************
+ *
+ * File        :  $Source: /cvsroot/ijbswa/current/acconfig.h,v $
+ *
+ * Purpose     :  This file should be the first thing included in every
+ *                .c file.  (Before even system headers).  It contains
+ *                #define statements for various features.  It was
+ *                introduced because the compile command line started
+ *                getting ludicrously long with feature defines.
+ *
+ * Copyright   :  Written by and Copyright (C) 2001-2014 the
+ *                Privoxy team. https://www.privoxy.org/
+ *
+ *                Based on the Internet Junkbuster originally written
+ *                by and Copyright (C) 1997 Anonymous Coders and
+ *                Junkbusters Corporation.  http://www.junkbusters.com
+ *
+ *                This program is free software; you can redistribute it
+ *                and/or modify it under the terms of the GNU General
+ *                Public License as published by the Free Software
+ *                Foundation; either version 2 of the License, or (at
+ *                your option) any later version.
+ *
+ *                This program is distributed in the hope that it will
+ *                be useful, but WITHOUT ANY WARRANTY; without even the
+ *                implied warranty of MERCHANTABILITY or FITNESS FOR A
+ *                PARTICULAR PURPOSE.  See the GNU General Public
+ *                License for more details.
+ *
+ *                The GNU General Public License should be included with
+ *                this file.  If not, you can view it at
+ *                http://www.gnu.org/copyleft/gpl.html
+ *                or write to the Free Software Foundation, Inc., 59
+ *                Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ *
+ *********************************************************************/
+
+@TOP@
+
+/*
+ * Version number - Major (X._._)
+ */
+#undef VERSION_MAJOR
+
+/*
+ * Version number - Minor (_.X._)
+ */
+#undef VERSION_MINOR
+
+/*
+ * Version number - Point (_._.X)
+ */
+#undef VERSION_POINT
+
+/*
+ * Version number, as a string
+ */
+#undef VERSION
+
+/*
+ * Status of the code: "alpha", "beta" or "stable".
+ */
+#undef CODE_STATUS
+
+/*
+ * Should pcre be statically built in instead of linkling with libpcre?
+ * (This is determined by configure depending on the availiability of
+ * libpcre and user preferences).
+ * Don't bother to change this here! Use configure instead.
+ */
+#undef FEATURE_DYNAMIC_PCRE
+
+/*
+ * Should pcrs be statically built in instead of linkling with libpcrs?
+ * (This is determined by configure depending on the availiability of
+ * libpcrs and user preferences).
+ * Don't bother to change this here! Use configure instead.
+ */
+#undef STATIC_PCRS
+
+/*
+ * Allows the use of an ACL to control access to the proxy by IP address.
+ */
+#undef FEATURE_ACL
+
+/*
+ * Allow Privoxy to use accf_http(9) if supported.
+ */
+#undef FEATURE_ACCEPT_FILTER
+
+/*
+ * Enables the web-based configuration (actionsfile) editor.  If you
+ * have a shared proxy, you might want to turn this off.
+ */
+#undef FEATURE_CGI_EDIT_ACTIONS
+
+/*
+ * Locally redirect remote script-redirect URLs
+ */
+#undef FEATURE_FAST_REDIRECTS
+
+/*
+ * Bypass filtering for 1 page only
+ */
+#undef FEATURE_FORCE_LOAD
+
+/*
+ * Allow blocking using images as well as HTML.
+ * If you do not define this then everything is blocked as HTML.
+ */
+#undef FEATURE_IMAGE_BLOCKING
+
+/*
+ * Use PNG instead of GIF for built-in images
+ */
+#undef FEATURE_NO_GIFS
+
+/*
+ * Allow to shutdown Privoxy through the webinterface.
+ */
+#undef FEATURE_GRACEFUL_TERMINATION
+
+/*
+ * Allow PCRE syntax in host patterns.
+ */
+#undef FEATURE_PCRE_HOST_PATTERNS
+
+/*
+ * Gather extended statistics.
+ */
+#undef FEATURE_EXTENDED_STATISTICS
+
+/*
+ * Allow filtering with scripts and programs.
+ */
+#undef FEATURE_EXTERNAL_FILTERS
+
+/*
+ * Keep connections alive if possible.
+ */
+#undef FEATURE_CONNECTION_KEEP_ALIVE
+
+/*
+ * Allow to share outgoing connections between incoming connections.
+ */
+#undef FEATURE_CONNECTION_SHARING
+
+/*
+ * Use POSIX threads instead of native threads.
+ */
+#undef FEATURE_PTHREAD
+
+/*
+ * Enables statistics function.
+ */
+#undef FEATURE_STATISTICS
+
+/*
+ * Enable strptime() sanity checks.
+ */
+#undef FEATURE_STRPTIME_SANITY_CHECKS
+
+/*
+ * Allow Privoxy to be "disabled" so it is just a normal non-blocking
+ * non-anonymizing proxy.  This is useful if you're trying to access a
+ * blocked or broken site - just change the setting in the config file,
+ * or use the handy "Disable" menu option in the Windows GUI.
+ */
+#undef FEATURE_TOGGLE
+
+/*
+ * Allows the use of trust files.
+ */
+#undef FEATURE_TRUST
+
+/*
+ * Defined on Solaris only.  Makes the system libraries thread safe.
+ */
+#undef _REENTRANT
+
+/*
+ * Defined on Solaris only.  Without this, many important functions are not
+ * defined in the system headers.
+ */
+#undef __EXTENSIONS__
+
+/*
+ * Defined always.
+ * FIXME: Don't know what it does or why we need it.
+ * (presumably something to do with MultiThreading?)
+ */
+#undef __MT__
+
+/* If the (nonstandard and thread-safe) function gethostbyname_r
+ * is available, select which signature to use
+ */
+#undef HAVE_GETHOSTBYNAME_R_6_ARGS
+#undef HAVE_GETHOSTBYNAME_R_5_ARGS
+#undef HAVE_GETHOSTBYNAME_R_3_ARGS
+
+/* If the (nonstandard and thread-safe) function gethostbyaddr_r
+ * is available, select which signature to use
+ */
+#undef HAVE_GETHOSTBYADDR_R_8_ARGS
+#undef HAVE_GETHOSTBYADDR_R_7_ARGS
+#undef HAVE_GETHOSTBYADDR_R_5_ARGS
+
+/* Defined if you have gmtime_r and localtime_r with a signature
+ * of (struct time *, struct tm *)
+ */
+#undef HAVE_GMTIME_R
+#undef HAVE_LOCALTIME_R
+
+/* Define to 'int' if <sys/socket.h> doesn't have it.
+ */
+#undef socklen_t
+
+/* Define if pcre.h must be included as <pcre/pcre.h>
+ */
+#undef PCRE_H_IN_SUBDIR
+
+/* Define if pcreposix.h must be included as <pcre/pcreposix.h>
+ */
+#undef PCREPOSIX_H_IN_SUBDIR
+
+@BOTTOM@
+
+/*
+ * Defined always.
+ * FIXME: Don't know what it does or why we need it.
+ * (presumably something to do with ANSI Standard C?)
+ */
+#ifndef __STDC__
+#define __STDC__ 1
+#endif /* ndef __STDC__ */
+
+/*
+ * Need to set up this define only for the Pthreads library for
+ * Win32, available from http://sources.redhat.com/pthreads-win32/
+ */
+#if defined(FEATURE_PTHREAD) && defined(_WIN32)
+#define __CLEANUP_C
+#endif /* defined(FEATURE_PTHREAD) && defined(_WIN32) */
+
+/*
+ * BEOS does not currently support POSIX threads.
+ * This *should* be detected by ./configure, but let's be sure.
+ */
+#if defined(FEATURE_PTHREAD) && defined(__BEOS__)
+#error BEOS does not support pthread - please run ./configure again with "--disable-pthread"
+
+#endif /* defined(FEATURE_PTHREAD) && defined(__BEOS__) */
+
+/*
+ * On OpenBSD and maybe also FreeBSD, gcc doesn't define the cpp
+ * symbol unix; it defines __unix__ and sometimes not even that:
+ */
+#if ( defined(__unix__) || defined(__NetBSD__) ) && !defined(unix)
+#define unix 1
+#endif
+
+/*
+ * It's too easy to accidentally use a Cygwin or MinGW32 version of config.h
+ * under VC++, and it usually gives many weird error messages.  Let's make
+ * the error messages understandable, by bailing out now.
+ */
+#ifdef _MSC_VER
+#error For MS VC++, please use vc_config_winthreads.h or vc_config_pthreads.h.  You can usually do this by selecting the "Build", "Clean" menu option.
+#endif /* def _MSC_VER */
+
+#endif /* CONFIG_H_INCLUDED */

+ 156 - 0
privoxy/actionlist.h

@@ -0,0 +1,156 @@
+/*********************************************************************
+ *
+ * File        :  $Source: /cvsroot/ijbswa/current/actionlist.h,v $
+ *
+ * Purpose     :  Master list of supported actions.
+ *                Not really a header, since it generates code.
+ *                This is included (3 times!) from actions.c
+ *                Each time, the following macros are defined to
+ *                suitable values beforehand:
+ *                    DEFINE_ACTION_MULTI()
+ *                    DEFINE_ACTION_STRING()
+ *                    DEFINE_ACTION_BOOL()
+ *                    DEFINE_ACTION_ALIAS
+ *
+ * Copyright   :  Written by and Copyright (C) 2001-2014 the
+ *                Privoxy team. https://www.privoxy.org/
+ *
+ *                Based on the Internet Junkbuster originally written
+ *                by and Copyright (C) 1997 Anonymous Coders and
+ *                Junkbusters Corporation.  http://www.junkbusters.com
+ *
+ *                This program is free software; you can redistribute it
+ *                and/or modify it under the terms of the GNU General
+ *                Public License as published by the Free Software
+ *                Foundation; either version 2 of the License, or (at
+ *                your option) any later version.
+ *
+ *                This program is distributed in the hope that it will
+ *                be useful, but WITHOUT ANY WARRANTY; without even the
+ *                implied warranty of MERCHANTABILITY or FITNESS FOR A
+ *                PARTICULAR PURPOSE.  See the GNU General Public
+ *                License for more details.
+ *
+ *                The GNU General Public License should be included with
+ *                this file.  If not, you can view it at
+ *                http://www.gnu.org/copyleft/gpl.html
+ *                or write to the Free Software Foundation, Inc., 59
+ *                Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ *
+ *********************************************************************/
+
+
+#if !(defined(DEFINE_ACTION_BOOL) && defined(DEFINE_ACTION_MULTI) && defined(DEFINE_ACTION_STRING))
+#error Please define lots of macros before including "actionlist.h".
+#endif /* !defined(all the DEFINE_ACTION_xxx macros) */
+
+#ifndef DEFINE_CGI_PARAM_RADIO
+#define DEFINE_CGI_PARAM_RADIO(name, bit, index, value, is_default)
+#define DEFINE_CGI_PARAM_CUSTOM(name, bit, index, default_val)
+#define DEFINE_CGI_PARAM_NO_RADIO(name, bit, index, default_val)
+#endif /* ndef DEFINE_CGI_PARAM_RADIO */
+
+DEFINE_ACTION_MULTI      ("add-header",                 ACTION_MULTI_ADD_HEADER)
+DEFINE_ACTION_STRING     ("block",                      ACTION_BLOCK, ACTION_STRING_BLOCK)
+DEFINE_CGI_PARAM_NO_RADIO("block",                      ACTION_BLOCK, ACTION_STRING_BLOCK, "No reason specified.")
+DEFINE_ACTION_STRING     ("change-x-forwarded-for",     ACTION_CHANGE_X_FORWARDED_FOR,  ACTION_STRING_CHANGE_X_FORWARDED_FOR)
+DEFINE_CGI_PARAM_RADIO   ("change-x-forwarded-for",     ACTION_CHANGE_X_FORWARDED_FOR,  ACTION_STRING_CHANGE_X_FORWARDED_FOR, "block", 0)
+DEFINE_CGI_PARAM_RADIO   ("change-x-forwarded-for",     ACTION_CHANGE_X_FORWARDED_FOR,  ACTION_STRING_CHANGE_X_FORWARDED_FOR, "add", 1)
+DEFINE_ACTION_MULTI      ("client-header-filter",       ACTION_MULTI_CLIENT_HEADER_FILTER)
+DEFINE_ACTION_MULTI      ("client-header-tagger",       ACTION_MULTI_CLIENT_HEADER_TAGGER)
+DEFINE_ACTION_STRING     ("content-type-overwrite",     ACTION_CONTENT_TYPE_OVERWRITE, ACTION_STRING_CONTENT_TYPE)
+DEFINE_CGI_PARAM_NO_RADIO("content-type-overwrite",     ACTION_CONTENT_TYPE_OVERWRITE, ACTION_STRING_CONTENT_TYPE,    "text/html")
+DEFINE_ACTION_STRING     ("crunch-client-header",       ACTION_CRUNCH_CLIENT_HEADER, ACTION_STRING_CLIENT_HEADER)
+DEFINE_CGI_PARAM_NO_RADIO("crunch-client-header",       ACTION_CRUNCH_CLIENT_HEADER, ACTION_STRING_CLIENT_HEADER,          "X-Whatever:")
+DEFINE_ACTION_BOOL       ("crunch-if-none-match",       ACTION_CRUNCH_IF_NONE_MATCH)
+DEFINE_ACTION_BOOL       ("crunch-incoming-cookies",    ACTION_CRUNCH_INCOMING_COOKIES)
+DEFINE_ACTION_BOOL       ("crunch-outgoing-cookies",    ACTION_CRUNCH_OUTGOING_COOKIES)
+DEFINE_ACTION_STRING     ("crunch-server-header",       ACTION_CRUNCH_SERVER_HEADER, ACTION_STRING_SERVER_HEADER)
+DEFINE_CGI_PARAM_NO_RADIO("crunch-server-header",       ACTION_CRUNCH_SERVER_HEADER, ACTION_STRING_SERVER_HEADER,          "X-Whatever:")
+DEFINE_ACTION_STRING     ("deanimate-gifs",             ACTION_DEANIMATE,       ACTION_STRING_DEANIMATE)
+DEFINE_CGI_PARAM_RADIO   ("deanimate-gifs",             ACTION_DEANIMATE,       ACTION_STRING_DEANIMATE,     "first", 0)
+DEFINE_ACTION_STRING     ("delay-response",             ACTION_DELAY_RESPONSE,  ACTION_STRING_DELAY_RESPONSE)
+DEFINE_CGI_PARAM_NO_RADIO("delay-response",             ACTION_DELAY_RESPONSE,  ACTION_STRING_DELAY_RESPONSE, "100")
+DEFINE_CGI_PARAM_RADIO   ("deanimate-gifs",             ACTION_DEANIMATE,       ACTION_STRING_DEANIMATE,     "last",  1)
+DEFINE_ACTION_BOOL       ("downgrade-http-version",     ACTION_DOWNGRADE)
+#ifdef FEATURE_EXTERNAL_FILTERS
+DEFINE_ACTION_MULTI      ("external-filter",            ACTION_MULTI_EXTERNAL_FILTER)
+#endif
+#ifdef FEATURE_FAST_REDIRECTS
+DEFINE_ACTION_STRING     ("fast-redirects",             ACTION_FAST_REDIRECTS,  ACTION_STRING_FAST_REDIRECTS)
+DEFINE_CGI_PARAM_RADIO   ("fast-redirects",             ACTION_FAST_REDIRECTS,  ACTION_STRING_FAST_REDIRECTS, "simple-check",  0)
+DEFINE_CGI_PARAM_RADIO   ("fast-redirects",             ACTION_FAST_REDIRECTS,  ACTION_STRING_FAST_REDIRECTS, "check-decoded-url",  1)
+#endif /* def FEATURE_FAST_REDIRECTS */
+DEFINE_ACTION_MULTI      ("filter",                     ACTION_MULTI_FILTER)
+DEFINE_ACTION_BOOL       ("force-text-mode",            ACTION_FORCE_TEXT_MODE)
+DEFINE_ACTION_STRING     ("forward-override",           ACTION_FORWARD_OVERRIDE, ACTION_STRING_FORWARD_OVERRIDE)
+DEFINE_CGI_PARAM_CUSTOM  ("forward-override",           ACTION_FORWARD_OVERRIDE, ACTION_STRING_FORWARD_OVERRIDE, "forward .")
+DEFINE_ACTION_BOOL       ("handle-as-empty-document",   ACTION_HANDLE_AS_EMPTY_DOCUMENT)
+DEFINE_ACTION_BOOL       ("handle-as-image",            ACTION_IMAGE)
+DEFINE_ACTION_STRING     ("hide-accept-language",       ACTION_HIDE_ACCEPT_LANGUAGE, ACTION_STRING_LANGUAGE)
+DEFINE_CGI_PARAM_RADIO   ("hide-accept-language",       ACTION_HIDE_ACCEPT_LANGUAGE, ACTION_STRING_LANGUAGE, "block", 0)
+DEFINE_CGI_PARAM_CUSTOM  ("hide-accept-language",       ACTION_HIDE_ACCEPT_LANGUAGE, ACTION_STRING_LANGUAGE, "de-de")
+DEFINE_ACTION_STRING     ("hide-content-disposition",   ACTION_HIDE_CONTENT_DISPOSITION, ACTION_STRING_CONTENT_DISPOSITION)
+DEFINE_CGI_PARAM_RADIO   ("hide-content-disposition",   ACTION_HIDE_CONTENT_DISPOSITION, ACTION_STRING_CONTENT_DISPOSITION,    "block", 0)
+DEFINE_CGI_PARAM_CUSTOM  ("hide-content-disposition",   ACTION_HIDE_CONTENT_DISPOSITION, ACTION_STRING_CONTENT_DISPOSITION,    "attachment; filename=WHATEVER.txt")
+DEFINE_ACTION_STRING     ("hide-from-header",           ACTION_HIDE_FROM,       ACTION_STRING_FROM)
+DEFINE_CGI_PARAM_RADIO   ("hide-from-header",           ACTION_HIDE_FROM,       ACTION_STRING_FROM,          "block", 1)
+DEFINE_CGI_PARAM_CUSTOM  ("hide-from-header",           ACTION_HIDE_FROM,       ACTION_STRING_FROM,          "spam_me_senseless@sittingduck.xyz")
+DEFINE_ACTION_STRING     ("hide-if-modified-since",     ACTION_HIDE_IF_MODIFIED_SINCE, ACTION_STRING_IF_MODIFIED_SINCE)
+DEFINE_CGI_PARAM_RADIO   ("hide-if-modified-since",     ACTION_HIDE_IF_MODIFIED_SINCE, ACTION_STRING_IF_MODIFIED_SINCE, "block", 0)
+DEFINE_CGI_PARAM_CUSTOM  ("hide-if-modified-since",     ACTION_HIDE_IF_MODIFIED_SINCE, ACTION_STRING_IF_MODIFIED_SINCE, "-1")
+DEFINE_ACTION_STRING     ("hide-referrer",              ACTION_HIDE_REFERER,    ACTION_STRING_REFERER)
+DEFINE_CGI_PARAM_RADIO   ("hide-referrer",              ACTION_HIDE_REFERER,    ACTION_STRING_REFERER,       "conditional-forge", 3)
+DEFINE_CGI_PARAM_RADIO   ("hide-referrer",              ACTION_HIDE_REFERER,    ACTION_STRING_REFERER,       "conditional-block", 2)
+DEFINE_CGI_PARAM_RADIO   ("hide-referrer",              ACTION_HIDE_REFERER,    ACTION_STRING_REFERER,       "forge", 1)
+DEFINE_CGI_PARAM_RADIO   ("hide-referrer",              ACTION_HIDE_REFERER,    ACTION_STRING_REFERER,       "block", 0)
+DEFINE_CGI_PARAM_CUSTOM  ("hide-referrer",              ACTION_HIDE_REFERER,    ACTION_STRING_REFERER,       "http://www.privoxy.org/")
+DEFINE_ACTION_STRING     ("hide-user-agent",            ACTION_HIDE_USER_AGENT, ACTION_STRING_USER_AGENT)
+DEFINE_CGI_PARAM_NO_RADIO("hide-user-agent",            ACTION_HIDE_USER_AGENT, ACTION_STRING_USER_AGENT,    "Privoxy " VERSION)
+#ifdef FEATURE_HTTPS_INSPECTION
+DEFINE_ACTION_BOOL       ("https-inspection",           ACTION_HTTPS_INSPECTION)
+DEFINE_ACTION_BOOL       ("ignore-certificate-errors",  ACTION_IGNORE_CERTIFICATE_ERRORS)
+#endif
+DEFINE_ACTION_STRING     ("limit-connect",              ACTION_LIMIT_CONNECT,   ACTION_STRING_LIMIT_CONNECT)
+DEFINE_CGI_PARAM_NO_RADIO("limit-connect",              ACTION_LIMIT_CONNECT,   ACTION_STRING_LIMIT_CONNECT,  "443")
+DEFINE_ACTION_STRING     ("limit-cookie-lifetime",      ACTION_LIMIT_COOKIE_LIFETIME, ACTION_STRING_LIMIT_COOKIE_LIFETIME)
+DEFINE_CGI_PARAM_CUSTOM  ("limit-cookie-lifetime",      ACTION_LIMIT_COOKIE_LIFETIME, ACTION_STRING_LIMIT_COOKIE_LIFETIME, "60")
+DEFINE_ACTION_STRING     ("overwrite-last-modified",    ACTION_OVERWRITE_LAST_MODIFIED, ACTION_STRING_LAST_MODIFIED)
+DEFINE_CGI_PARAM_RADIO   ("overwrite-last-modified",    ACTION_OVERWRITE_LAST_MODIFIED, ACTION_STRING_LAST_MODIFIED, "block", 0)
+DEFINE_CGI_PARAM_RADIO   ("overwrite-last-modified",    ACTION_OVERWRITE_LAST_MODIFIED, ACTION_STRING_LAST_MODIFIED, "reset-to-request-time", 1)
+DEFINE_CGI_PARAM_RADIO   ("overwrite-last-modified",    ACTION_OVERWRITE_LAST_MODIFIED, ACTION_STRING_LAST_MODIFIED, "randomize", 2)
+DEFINE_ACTION_BOOL       ("prevent-compression",        ACTION_NO_COMPRESSION)
+DEFINE_ACTION_STRING     ("redirect",                   ACTION_REDIRECT,        ACTION_STRING_REDIRECT)
+DEFINE_CGI_PARAM_NO_RADIO("redirect",                   ACTION_REDIRECT,        ACTION_STRING_REDIRECT,  "http://localhost/")
+DEFINE_ACTION_MULTI      ("server-header-filter",       ACTION_MULTI_SERVER_HEADER_FILTER)
+DEFINE_ACTION_MULTI      ("server-header-tagger",       ACTION_MULTI_SERVER_HEADER_TAGGER)
+DEFINE_ACTION_BOOL       ("session-cookies-only",       ACTION_SESSION_COOKIES_ONLY)
+DEFINE_ACTION_STRING     ("set-image-blocker",          ACTION_IMAGE_BLOCKER,   ACTION_STRING_IMAGE_BLOCKER)
+DEFINE_CGI_PARAM_RADIO   ("set-image-blocker",          ACTION_IMAGE_BLOCKER,   ACTION_STRING_IMAGE_BLOCKER, "pattern", 1)
+DEFINE_CGI_PARAM_RADIO   ("set-image-blocker",          ACTION_IMAGE_BLOCKER,   ACTION_STRING_IMAGE_BLOCKER, "blank", 0)
+DEFINE_CGI_PARAM_CUSTOM  ("set-image-blocker",          ACTION_IMAGE_BLOCKER,   ACTION_STRING_IMAGE_BLOCKER,  CGI_PREFIX "send-banner?type=pattern")
+
+#if DEFINE_ACTION_ALIAS
+
+/*
+ * Alternative spellings
+ */
+DEFINE_ACTION_STRING     ("hide-referer",   ACTION_HIDE_REFERER,    ACTION_STRING_REFERER)
+DEFINE_ACTION_BOOL       ("prevent-keeping-cookies", ACTION_SESSION_COOKIES_ONLY)
+
+/*
+ * Pre-3.0.7 (pseudo) compatibility
+ */
+DEFINE_ACTION_MULTI      ("filter-client-headers",       ACTION_MULTI_CLIENT_HEADER_FILTER)
+DEFINE_ACTION_MULTI      ("filter-server-headers",       ACTION_MULTI_SERVER_HEADER_FILTER)
+
+#endif /* if DEFINE_ACTION_ALIAS */
+
+#undef DEFINE_ACTION_MULTI
+#undef DEFINE_ACTION_STRING
+#undef DEFINE_ACTION_BOOL
+#undef DEFINE_ACTION_ALIAS
+#undef DEFINE_CGI_PARAM_CUSTOM
+#undef DEFINE_CGI_PARAM_RADIO
+#undef DEFINE_CGI_PARAM_NO_RADIO
+

+ 1990 - 0
privoxy/actions.c

@@ -0,0 +1,1990 @@
+/*********************************************************************
+ *
+ * File        :  $Source: /cvsroot/ijbswa/current/actions.c,v $
+ *
+ * Purpose     :  Declares functions to work with actions files
+ *
+ * Copyright   :  Written by and Copyright (C) 2001-2016 the
+ *                Privoxy team. https://www.privoxy.org/
+ *
+ *                Based on the Internet Junkbuster originally written
+ *                by and Copyright (C) 1997 Anonymous Coders and
+ *                Junkbusters Corporation.  http://www.junkbusters.com
+ *
+ *                This program is free software; you can redistribute it
+ *                and/or modify it under the terms of the GNU General
+ *                Public License as published by the Free Software
+ *                Foundation; either version 2 of the License, or (at
+ *                your option) any later version.
+ *
+ *                This program is distributed in the hope that it will
+ *                be useful, but WITHOUT ANY WARRANTY; without even the
+ *                implied warranty of MERCHANTABILITY or FITNESS FOR A
+ *                PARTICULAR PURPOSE.  See the GNU General Public
+ *                License for more details.
+ *
+ *                The GNU General Public License should be included with
+ *                this file.  If not, you can view it at
+ *                http://www.gnu.org/copyleft/gpl.html
+ *                or write to the Free Software Foundation, Inc., 59
+ *                Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ *
+ *********************************************************************/
+
+
+#include "config.h"
+
+#include <stdio.h>
+#include <string.h>
+#include <assert.h>
+#include <stdlib.h>
+
+#ifdef FEATURE_PTHREAD
+#include <pthread.h>
+#endif
+
+#include "project.h"
+#include "jcc.h"
+#include "list.h"
+#include "actions.h"
+#include "miscutil.h"
+#include "errlog.h"
+#include "loaders.h"
+#include "encode.h"
+#include "urlmatch.h"
+#include "cgi.h"
+#include "ssplit.h"
+#include "filters.h"
+
+/*
+ * We need the main list of options.
+ *
+ * First, we need a way to tell between boolean, string, and multi-string
+ * options.  For string and multistring options, we also need to be
+ * able to tell the difference between a "+" and a "-".  (For bools,
+ * the "+"/"-" information is encoded in "add" and "mask").  So we use
+ * an enumerated type (well, the preprocessor equivalent).  Here are
+ * the values:
+ */
+enum action_value_type {
+   AV_NONE       = 0, /* +opt -opt */
+   AV_ADD_STRING = 1, /* +stropt{string} */
+   AV_REM_STRING = 2, /* -stropt */
+   AV_ADD_MULTI  = 3, /* +multiopt{string} +multiopt{string2} */
+   AV_REM_MULTI  = 4  /* -multiopt{string} -multiopt          */
+};
+
+/*
+ * We need a structure to hold the name, flag changes,
+ * type, and string index.
+ */
+struct action_name
+{
+   const char * name;
+   unsigned long mask;                /* a bit set to "0" = remove action */
+   unsigned long add;                 /* a bit set to "1" = add action */
+   enum action_value_type value_type; /* an AV_... constant */
+   int index;                         /* index into strings[] or multi[] */
+};
+
+/*
+ * And with those building blocks in place, here's the array.
+ */
+static const struct action_name action_names[] =
+{
+   /*
+    * Well actually there's no data here - it's in actionlist.h
+    * This keeps it together to make it easy to change.
+    *
+    * Here's the macros used to format it:
+    */
+#define DEFINE_ACTION_MULTI(name,index)                   \
+   { "+" name, ACTION_MASK_ALL, 0, AV_ADD_MULTI, index }, \
+   { "-" name, ACTION_MASK_ALL, 0, AV_REM_MULTI, index },
+#define DEFINE_ACTION_STRING(name,flag,index)                 \
+   { "+" name, ACTION_MASK_ALL, flag, AV_ADD_STRING, index }, \
+   { "-" name, ~flag, 0, AV_REM_STRING, index },
+#define DEFINE_ACTION_BOOL(name,flag)   \
+   { "+" name, ACTION_MASK_ALL, flag }, \
+   { "-" name, ~flag, 0 },
+#define DEFINE_ACTION_ALIAS 1 /* Want aliases please */
+
+#include "actionlist.h"
+
+#undef DEFINE_ACTION_MULTI
+#undef DEFINE_ACTION_STRING
+#undef DEFINE_ACTION_BOOL
+#undef DEFINE_ACTION_ALIAS
+
+   { NULL, 0, 0 } /* End marker */
+};
+
+
+#ifndef FUZZ
+static
+#endif
+int load_one_actions_file(struct client_state *csp, int fileid);
+
+
+/*********************************************************************
+ *
+ * Function    :  merge_actions
+ *
+ * Description :  Merge two actions together.
+ *                Similar to "dest += src".
+ *
+ * Parameters  :
+ *          1  :  dest = Actions to modify.
+ *          2  :  src = Action to add.
+ *
+ * Returns     :  JB_ERR_OK or JB_ERR_MEMORY
+ *
+ *********************************************************************/
+jb_err merge_actions (struct action_spec *dest,
+                      const struct action_spec *src)
+{
+   int i;
+   jb_err err;
+
+   dest->mask &= src->mask;
+   dest->add  &= src->mask;
+   dest->add  |= src->add;
+
+   for (i = 0; i < ACTION_STRING_COUNT; i++)
+   {
+      char * str = src->string[i];
+      if (str)
+      {
+         freez(dest->string[i]);
+         dest->string[i] = strdup_or_die(str);
+      }
+   }
+
+   for (i = 0; i < ACTION_MULTI_COUNT; i++)
+   {
+      if (src->multi_remove_all[i])
+      {
+         /* Remove everything from dest */
+         list_remove_all(dest->multi_remove[i]);
+         dest->multi_remove_all[i] = 1;
+
+         err = list_duplicate(dest->multi_add[i], src->multi_add[i]);
+      }
+      else if (dest->multi_remove_all[i])
+      {
+         /*
+          * dest already removes everything, so we only need to worry
+          * about what we add.
+          */
+         list_remove_list(dest->multi_add[i], src->multi_remove[i]);
+         err = list_append_list_unique(dest->multi_add[i], src->multi_add[i]);
+      }
+      else
+      {
+         /* No "remove all"s to worry about. */
+         list_remove_list(dest->multi_add[i], src->multi_remove[i]);
+         err = list_append_list_unique(dest->multi_remove[i], src->multi_remove[i]);
+         if (!err) err = list_append_list_unique(dest->multi_add[i], src->multi_add[i]);
+      }
+
+      if (err)
+      {
+         return err;
+      }
+   }
+
+   return JB_ERR_OK;
+}
+
+
+/*********************************************************************
+ *
+ * Function    :  copy_action
+ *
+ * Description :  Copy an action_specs.
+ *                Similar to "dest = src".
+ *
+ * Parameters  :
+ *          1  :  dest = Destination of copy.
+ *          2  :  src = Source for copy.
+ *
+ * Returns     :  JB_ERR_OK or JB_ERR_MEMORY
+ *
+ *********************************************************************/
+jb_err copy_action (struct action_spec *dest,
+                    const struct action_spec *src)
+{
+   int i;
+   jb_err err = JB_ERR_OK;
+
+   free_action(dest);
+   memset(dest, '\0', sizeof(*dest));
+
+   dest->mask = src->mask;
+   dest->add  = src->add;
+
+   for (i = 0; i < ACTION_STRING_COUNT; i++)
+   {
+      char * str = src->string[i];
+      if (str)
+      {
+         str = strdup_or_die(str);
+         dest->string[i] = str;
+      }
+   }
+
+   for (i = 0; i < ACTION_MULTI_COUNT; i++)
+   {
+      dest->multi_remove_all[i] = src->multi_remove_all[i];
+      err = list_duplicate(dest->multi_remove[i], src->multi_remove[i]);
+      if (err)
+      {
+         return err;
+      }
+      err = list_duplicate(dest->multi_add[i],    src->multi_add[i]);
+      if (err)
+      {
+         return err;
+      }
+   }
+   return err;
+}
+
+/*********************************************************************
+ *
+ * Function    :  free_action_spec
+ *
+ * Description :  Frees an action_spec and the memory used by it.
+ *
+ * Parameters  :
+ *          1  :  src = Source to free.
+ *
+ * Returns     :  N/A
+ *
+ *********************************************************************/
+void free_action_spec(struct action_spec *src)
+{
+   free_action(src);
+   freez(src);
+}
+
+
+/*********************************************************************
+ *
+ * Function    :  free_action
+ *
+ * Description :  Destroy an action_spec.  Frees memory used by it,
+ *                except for the memory used by the struct action_spec
+ *                itself.
+ *
+ * Parameters  :
+ *          1  :  src = Source to free.
+ *
+ * Returns     :  N/A
+ *
+ *********************************************************************/
+void free_action (struct action_spec *src)
+{
+   int i;
+
+   if (src == NULL)
+   {
+      return;
+   }
+
+   for (i = 0; i < ACTION_STRING_COUNT; i++)
+   {
+      freez(src->string[i]);
+   }
+
+   for (i = 0; i < ACTION_MULTI_COUNT; i++)
+   {
+      destroy_list(src->multi_remove[i]);
+      destroy_list(src->multi_add[i]);
+   }
+
+   memset(src, '\0', sizeof(*src));
+}
+
+
+/*********************************************************************
+ *
+ * Function    :  get_action_token
+ *
+ * Description :  Parses a line for the first action.
+ *                Modifies its input array, doesn't allocate memory.
+ *                e.g. given:
+ *                *line="  +abc{def}  -ghi "
+ *                Returns:
+ *                *line="  -ghi "
+ *                *name="+abc"
+ *                *value="def"
+ *
+ * Parameters  :
+ *          1  :  line = [in] The line containing the action.
+ *                       [out] Start of next action on line, or
+ *                       NULL if we reached the end of line before
+ *                       we found an action.
+ *          2  :  name = [out] Start of action name, null
+ *                       terminated.  NULL on EOL
+ *          3  :  value = [out] Start of action value, null
+ *                        terminated.  NULL if none or EOL.
+ *
+ * Returns     :  JB_ERR_OK => Ok
+ *                JB_ERR_PARSE => Mismatched {} (line was trashed anyway)
+ *
+ *********************************************************************/
+jb_err get_action_token(char **line, char **name, char **value)
+{
+   char * str = *line;
+   char ch;
+
+   /* set default returns */
+   *line = NULL;
+   *name = NULL;
+   *value = NULL;
+
+   /* Eat any leading whitespace */
+   while ((*str == ' ') || (*str == '\t'))
+   {
+      str++;
+   }
+
+   if (*str == '\0')
+   {
+      return 0;
+   }
+
+   if (*str == '{')
+   {
+      /* null name, just value is prohibited */
+      return JB_ERR_PARSE;
+   }
+
+   *name = str;
+
+   /* parse option */
+   while (((ch = *str) != '\0') &&
+          (ch != ' ') && (ch != '\t') && (ch != '{'))
+   {
+      if (ch == '}')
+      {
+         /* error, '}' without '{' */
+         return JB_ERR_PARSE;
+      }
+      str++;
+   }
+   *str = '\0';
+
+   if (ch != '{')
+   {
+      /* no value */
+      if (ch == '\0')
+      {
+         /* EOL - be careful not to run off buffer */
+         *line = str;
+      }
+      else
+      {
+         /* More to parse next time. */
+         *line = str + 1;
+      }
+      return JB_ERR_OK;
+   }
+
+   str++;
+   *value = str;
+
+   /* The value ends with the first non-escaped closing curly brace */
+   while ((str = strchr(str, '}')) != NULL)
+   {
+      if (str[-1] == '\\')
+      {
+         /* Overwrite the '\' so the action doesn't see it. */
+         string_move(str-1, str);
+         continue;
+      }
+      break;
+   }
+   if (str == NULL)
+   {
+      /* error */
+      *value = NULL;
+      return JB_ERR_PARSE;
+   }
+
+   /* got value */
+   *str = '\0';
+   *line = str + 1;
+
+   chomp(*value);
+
+   return JB_ERR_OK;
+}
+
+/*********************************************************************
+ *
+ * Function    :  action_used_to_be_valid
+ *
+ * Description :  Checks if unrecognized actions were valid in earlier
+ *                releases.
+ *
+ * Parameters  :
+ *          1  :  action = The string containing the action to check.
+ *
+ * Returns     :  True if yes, otherwise false.
+ *
+ *********************************************************************/
+static int action_used_to_be_valid(const char *action)
+{
+   static const char * const formerly_valid_actions[] = {
+      "inspect-jpegs",
+      "kill-popups",
+      "send-vanilla-wafer",
+      "send-wafer",
+      "treat-forbidden-connects-like-blocks",
+      "vanilla-wafer",
+      "wafer"
+   };
+   unsigned int i;
+
+   for (i = 0; i < SZ(formerly_valid_actions); i++)
+   {
+      if (0 == strcmpic(action, formerly_valid_actions[i]))
+      {
+         return TRUE;
+      }
+   }
+
+   return FALSE;
+}
+
+/*********************************************************************
+ *
+ * Function    :  get_actions
+ *
+ * Description :  Parses a list of actions.
+ *
+ * Parameters  :
+ *          1  :  line = The string containing the actions.
+ *                       Will be written to by this function.
+ *          2  :  alias_list = Custom alias list, or NULL for none.
+ *          3  :  cur_action = Where to store the action.  Caller
+ *                             allocates memory.
+ *
+ * Returns     :  JB_ERR_OK => Ok
+ *                JB_ERR_PARSE => Parse error (line was trashed anyway)
+ *                nonzero => Out of memory (line was trashed anyway)
+ *
+ *********************************************************************/
+jb_err get_actions(char *line,
+                   struct action_alias * alias_list,
+                   struct action_spec *cur_action)
+{
+   jb_err err;
+   init_action(cur_action);
+   cur_action->mask = ACTION_MASK_ALL;
+
+   while (line)
+   {
+      char * option = NULL;
+      char * value = NULL;
+
+      err = get_action_token(&line, &option, &value);
+      if (err)
+      {
+         return err;
+      }
+
+      if (option)
+      {
+         /* handle option in 'option' */
+
+         /* Check for standard action name */
+         const struct action_name * action = action_names;
+
+         while ((action->name != NULL) && (0 != strcmpic(action->name, option)))
+         {
+            action++;
+         }
+         if (action->name != NULL)
+         {
+            /* Found it */
+            cur_action->mask &= action->mask;
+            cur_action->add  &= action->mask;
+            cur_action->add  |= action->add;
+
+            switch (action->value_type)
+            {
+            case AV_NONE:
+               if (value != NULL)
+               {
+                  log_error(LOG_LEVEL_ERROR,
+                     "Action %s does not take parameters but %s was given.",
+                     action->name, value);
+                  return JB_ERR_PARSE;
+               }
+               break;
+            case AV_ADD_STRING:
+               {
+                  /* add single string. */
+
+                  if ((value == NULL) || (*value == '\0'))
+                  {
+                     if (0 == strcmpic(action->name, "+block"))
+                     {
+                        /*
+                         * XXX: Temporary backwards compatibility hack.
+                         * XXX: should include line number.
+                         */
+                        value = "No reason specified.";
+                        log_error(LOG_LEVEL_ERROR,
+                           "block action without reason found. This may "
+                           "become a fatal error in future versions.");
+                     }
+                     else
+                     {
+                        return JB_ERR_PARSE;
+                     }
+                  }
+#ifdef FEATURE_EXTENDED_STATISTICS
+                  if (0 == strcmpic(action->name, "+block"))
+                  {
+                     register_block_reason_for_statistics(value);
+                  }
+#endif
+                  /* FIXME: should validate option string here */
+                  freez (cur_action->string[action->index]);
+                  cur_action->string[action->index] = strdup(value);
+                  if (NULL == cur_action->string[action->index])
+                  {
+                     return JB_ERR_MEMORY;
+                  }
+                  break;
+               }
+            case AV_REM_STRING:
+               {
+                  /* remove single string. */
+
+                  freez (cur_action->string[action->index]);
+                  break;
+               }
+            case AV_ADD_MULTI:
+               {
+                  /* append multi string. */
+
+                  struct list * remove_p = cur_action->multi_remove[action->index];
+                  struct list * add_p    = cur_action->multi_add[action->index];
+
+                  if ((value == NULL) || (*value == '\0'))
+                  {
+                     return JB_ERR_PARSE;
+                  }
+
+                  list_remove_item(remove_p, value);
+                  err = enlist_unique(add_p, value, 0);
+                  if (err)
+                  {
+                     return err;
+                  }
+                  break;
+               }
+            case AV_REM_MULTI:
+               {
+                  /* remove multi string. */
+
+                  struct list * remove_p = cur_action->multi_remove[action->index];
+                  struct list * add_p    = cur_action->multi_add[action->index];
+
+                  if ((value == NULL) || (*value == '\0')
+                     || ((*value == '*') && (value[1] == '\0')))
+                  {
+                     /*
+                      * no option, or option == "*".
+                      *
+                      * Remove *ALL*.
+                      */
+                     list_remove_all(remove_p);
+                     list_remove_all(add_p);
+                     cur_action->multi_remove_all[action->index] = 1;
+                  }
+                  else
+                  {
+                     /* Valid option - remove only 1 option */
+
+                     if (!cur_action->multi_remove_all[action->index])
+                     {
+                        /* there isn't a catch-all in the remove list already */
+                        err = enlist_unique(remove_p, value, 0);
+                        if (err)
+                        {
+                           return err;
+                        }
+                     }
+                     list_remove_item(add_p, value);
+                  }
+                  break;
+               }
+            default:
+               /* Shouldn't get here unless there's memory corruption. */
+               assert(0);
+               return JB_ERR_PARSE;
+            }
+         }
+         else
+         {
+            /* try user aliases. */
+            const struct action_alias * alias = alias_list;
+
+            while ((alias != NULL) && (0 != strcmpic(alias->name, option)))
+            {
+               alias = alias->next;
+            }
+            if (alias != NULL)
+            {
+               /* Found it */
+               merge_actions(cur_action, alias->action);
+            }
+            else if (((size_t)2 < strlen(option)) && action_used_to_be_valid(option+1))
+            {
+               log_error(LOG_LEVEL_ERROR, "Action '%s' is no longer valid "
+                  "in this Privoxy release. Ignored.", option+1);
+            }
+            else if (((size_t)2 < strlen(option)) && 0 == strcmpic(option+1, "hide-forwarded-for-headers"))
+            {
+               log_error(LOG_LEVEL_FATAL, "The action 'hide-forwarded-for-headers' "
+                  "is no longer valid in this Privoxy release. "
+                  "Use 'change-x-forwarded-for' instead.");
+            }
+            else
+            {
+               /* Bad action name */
+               /*
+                * XXX: This is a fatal error and Privoxy will later on exit
+                * in load_one_actions_file() because of an "invalid line".
+                *
+                * It would be preferable to name the offending option in that
+                * error message, but currently there is no way to do that and
+                * we have to live with two error messages for basically the
+                * same reason.
+                */
+               log_error(LOG_LEVEL_ERROR, "Unknown action or alias: %s", option);
+               return JB_ERR_PARSE;
+            }
+         }
+      }
+   }
+
+   return JB_ERR_OK;
+}
+
+
+/*********************************************************************
+ *
+ * Function    :  init_current_action
+ *
+ * Description :  Zero out an action.
+ *
+ * Parameters  :
+ *          1  :  dest = An uninitialized current_action_spec.
+ *
+ * Returns     :  N/A
+ *
+ *********************************************************************/
+void init_current_action (struct current_action_spec *dest)
+{
+   memset(dest, '\0', sizeof(*dest));
+
+   dest->flags = ACTION_MOST_COMPATIBLE;
+}
+
+
+/*********************************************************************
+ *
+ * Function    :  init_action
+ *
+ * Description :  Zero out an action.
+ *
+ * Parameters  :
+ *          1  :  dest = An uninitialized action_spec.
+ *
+ * Returns     :  N/A
+ *
+ *********************************************************************/
+void init_action (struct action_spec *dest)
+{
+   memset(dest, '\0', sizeof(*dest));
+}
+
+
+/*********************************************************************
+ *
+ * Function    :  merge_current_action
+ *
+ * Description :  Merge two actions together.
+ *                Similar to "dest += src".
+ *                Differences between this and merge_actions()
+ *                is that this one doesn't allocate memory for
+ *                strings (so "src" better be in memory for at least
+ *                as long as "dest" is, and you'd better free
+ *                "dest" using "free_current_action").
+ *                Also, there is no  mask or remove lists in dest.
+ *                (If we're applying it to a URL, we don't need them)
+ *
+ * Parameters  :
+ *          1  :  dest = Current actions, to modify.
+ *          2  :  src = Action to add.
+ *
+ * Returns  0  :  no error
+ *        !=0  :  error, probably JB_ERR_MEMORY.
+ *
+ *********************************************************************/
+jb_err merge_current_action (struct current_action_spec *dest,
+                             const struct action_spec *src)
+{
+   int i;
+   jb_err err = JB_ERR_OK;
+
+   dest->flags  &= src->mask;
+   dest->flags  |= src->add;
+
+   for (i = 0; i < ACTION_STRING_COUNT; i++)
+   {
+      char * str = src->string[i];
+      if (str)
+      {
+         str = strdup_or_die(str);
+         freez(dest->string[i]);
+         dest->string[i] = str;
+      }
+   }
+
+   for (i = 0; i < ACTION_MULTI_COUNT; i++)
+   {
+      if (src->multi_remove_all[i])
+      {
+         /* Remove everything from dest, then add src->multi_add */
+         err = list_duplicate(dest->multi[i], src->multi_add[i]);
+         if (err)
+         {
+            return err;
+         }
+      }
+      else
+      {
+         list_remove_list(dest->multi[i], src->multi_remove[i]);
+         err = list_append_list_unique(dest->multi[i], src->multi_add[i]);
+         if (err)
+         {
+            return err;
+         }
+      }
+   }
+   return err;
+}
+
+
+/*********************************************************************
+ *
+ * Function    :  update_action_bits_for_tag
+ *
+ * Description :  Updates the action bits based on the action sections
+ *                whose tag patterns match a provided tag.
+ *
+ * Parameters  :
+ *          1  :  csp = Current client state (buffers, headers, etc...)
+ *          2  :  tag = The tag on which the update should be based on
+ *
+ * Returns     :  0 if no tag matched, or
+ *                1 otherwise
+ *
+ *********************************************************************/
+int update_action_bits_for_tag(struct client_state *csp, const char *tag)
+{
+   struct file_list *fl;
+   struct url_actions *b;
+
+   int updated = 0;
+   int i;
+
+   assert(tag);
+   assert(list_contains_item(csp->tags, tag));
+
+   /* Run through all action files, */
+   for (i = 0; i < MAX_AF_FILES; i++)
+   {
+      if (((fl = csp->actions_list[i]) == NULL) || ((b = fl->f) == NULL))
+      {
+         /* Skip empty files */
+         continue;
+      }
+
+      /* and through all the action patterns, */
+      for (b = b->next; NULL != b; b = b->next)
+      {
+         /* skip everything but TAG patterns, */
+         if (!(b->url->flags & PATTERN_SPEC_TAG_PATTERN))
+         {
+            continue;
+         }
+
+         /* and check if one of the tag patterns matches the tag, */
+         if (0 == regexec(b->url->pattern.tag_regex, tag, 0, NULL, 0))
+         {
+            /* if it does, update the action bit map, */
+            if (merge_current_action(csp->action, b->action))
+            {
+               log_error(LOG_LEVEL_ERROR,
+                  "Out of memory while changing action bits");
+            }
+            /* and signal the change. */
+            updated = 1;
+         }
+      }
+   }
+
+   return updated;
+}
+
+
+/*********************************************************************
+ *
+ * Function    :  check_negative_tag_patterns
+ *
+ * Description :  Updates the action bits based on NO-*-TAG patterns.
+ *
+ * Parameters  :
+ *          1  :  csp = Current client state (buffers, headers, etc...)
+ *          2  :  flag = The tag pattern type
+ *
+ * Returns     :  JB_ERR_OK in case off success, or
+ *                JB_ERR_MEMORY on out-of-memory error.
+ *
+ *********************************************************************/
+jb_err check_negative_tag_patterns(struct client_state *csp, unsigned int flag)
+{
+   struct list_entry *tag;
+   struct file_list *fl;
+   struct url_actions *b = NULL;
+   int i;
+
+   for (i = 0; i < MAX_AF_FILES; i++)
+   {
+      fl = csp->actions_list[i];
+      if ((fl == NULL) || ((b = fl->f) == NULL))
+      {
+         continue;
+      }
+      for (b = b->next; NULL != b; b = b->next)
+      {
+         int tag_found = 0;
+         if (0 == (b->url->flags & flag))
+         {
+            continue;
+         }
+         for (tag = csp->tags->first; NULL != tag; tag = tag->next)
+         {
+            if (0 == regexec(b->url->pattern.tag_regex, tag->str, 0, NULL, 0))
+            {
+               /*
+                * The pattern matches at least one tag, thus the action
+                * section doesn't apply and we don't need to look at the
+                * other tags.
+                */
+               tag_found = 1;
+               break;
+            }
+         }
+         if (!tag_found)
+         {
+            /*
+             * The pattern doesn't match any tags,
+             * thus the action section applies.
+             */
+            if (merge_current_action(csp->action, b->action))
+            {
+               log_error(LOG_LEVEL_ERROR,
+                  "Out of memory while changing action bits");
+               return JB_ERR_MEMORY;
+            }
+            log_error(LOG_LEVEL_HEADER, "Updated action bits based on: %s",
+               b->url->spec);
+         }
+      }
+   }
+
+   return JB_ERR_OK;
+}
+
+
+/*********************************************************************
+ *
+ * Function    :  free_current_action
+ *
+ * Description :  Free memory used by a current_action_spec.
+ *                Does not free the current_action_spec itself.
+ *
+ * Parameters  :
+ *          1  :  src = Source to free.
+ *
+ * Returns     :  N/A
+ *
+ *********************************************************************/
+void free_current_action(struct current_action_spec *src)
+{
+   int i;
+
+   for (i = 0; i < ACTION_STRING_COUNT; i++)
+   {
+      freez(src->string[i]);
+   }
+
+   for (i = 0; i < ACTION_MULTI_COUNT; i++)
+   {
+      destroy_list(src->multi[i]);
+   }
+
+   memset(src, '\0', sizeof(*src));
+}
+
+
+static struct file_list *current_actions_file[MAX_AF_FILES]  = {
+   NULL, NULL, NULL, NULL, NULL,
+   NULL, NULL, NULL, NULL, NULL
+};
+
+
+#ifdef FEATURE_GRACEFUL_TERMINATION
+/*********************************************************************
+ *
+ * Function    :  unload_current_actions_file
+ *
+ * Description :  Unloads current actions file - reset to state at
+ *                beginning of program.
+ *
+ * Parameters  :  None
+ *
+ * Returns     :  N/A
+ *
+ *********************************************************************/
+void unload_current_actions_file(void)
+{
+   int i;
+
+   for (i = 0; i < MAX_AF_FILES; i++)
+   {
+      if (current_actions_file[i])
+      {
+         current_actions_file[i]->unloader = unload_actions_file;
+         current_actions_file[i] = NULL;
+      }
+   }
+}
+#endif /* FEATURE_GRACEFUL_TERMINATION */
+
+
+/*********************************************************************
+ *
+ * Function    :  unload_actions_file
+ *
+ * Description :  Unloads an actions module.
+ *
+ * Parameters  :
+ *          1  :  file_data = the data structure associated with the
+ *                            actions file.
+ *
+ * Returns     :  N/A
+ *
+ *********************************************************************/
+void unload_actions_file(void *file_data)
+{
+   struct url_actions * next;
+   struct url_actions * cur = (struct url_actions *)file_data;
+   while (cur != NULL)
+   {
+      next = cur->next;
+      free_pattern_spec(cur->url);
+      if ((next == NULL) || (next->action != cur->action))
+      {
+         /*
+          * As the action settings might be shared,
+          * we can only free them if the current
+          * url pattern is the last one, or if the
+          * next one is using different settings.
+          */
+         free_action_spec(cur->action);
+      }
+      freez(cur);
+      cur = next;
+   }
+}
+
+
+/*********************************************************************
+ *
+ * Function    :  free_alias_list
+ *
+ * Description :  Free memory used by a list of aliases.
+ *
+ * Parameters  :
+ *          1  :  alias_list = Linked list to free.
+ *
+ * Returns     :  N/A
+ *
+ *********************************************************************/
+void free_alias_list(struct action_alias *alias_list)
+{
+   while (alias_list != NULL)
+   {
+      struct action_alias * next = alias_list->next;
+      alias_list->next = NULL;
+      freez(alias_list->name);
+      free_action(alias_list->action);
+      free(alias_list);
+      alias_list = next;
+   }
+}
+
+
+/*********************************************************************
+ *
+ * Function    :  load_action_files
+ *
+ * Description :  Read and parse all the action files and add to files
+ *                list.
+ *
+ * Parameters  :
+ *          1  :  csp = Current client state (buffers, headers, etc...)
+ *
+ * Returns     :  0 => Ok, everything else is an error.
+ *
+ *********************************************************************/
+int load_action_files(struct client_state *csp)
+{
+   int i;
+   int result;
+
+   for (i = 0; i < MAX_AF_FILES; i++)
+   {
+      if (csp->config->actions_file[i])
+      {
+         result = load_one_actions_file(csp, i);
+         if (result)
+         {
+            return result;
+         }
+      }
+      else if (current_actions_file[i])
+      {
+         current_actions_file[i]->unloader = unload_actions_file;
+         current_actions_file[i] = NULL;
+      }
+   }
+
+   return 0;
+}
+
+
+/*********************************************************************
+ *
+ * Function    :  filter_type_to_string
+ *
+ * Description :  Converts a filter type enum into a string.
+ *
+ * Parameters  :
+ *          1  :  filter_type = filter_type as enum
+ *
+ * Returns     :  Pointer to static string.
+ *
+ *********************************************************************/
+static const char *filter_type_to_string(enum filter_type filter_type)
+{
+   switch (filter_type)
+   {
+   case FT_CONTENT_FILTER:
+      return "content filter";
+   case FT_CLIENT_HEADER_FILTER:
+      return "client-header filter";
+   case FT_SERVER_HEADER_FILTER:
+      return "server-header filter";
+   case FT_CLIENT_HEADER_TAGGER:
+      return "client-header tagger";
+   case FT_SERVER_HEADER_TAGGER:
+      return "server-header tagger";
+#ifdef FEATURE_EXTERNAL_FILTERS
+   case FT_EXTERNAL_CONTENT_FILTER:
+      return "external content filter";
+#endif
+   case FT_INVALID_FILTER:
+      return "invalid filter type";
+   }
+
+   return "unknown filter type";
+
+}
+
+/*********************************************************************
+ *
+ * Function    :  referenced_filters_are_missing
+ *
+ * Description :  Checks if any filters of a certain type referenced
+ *                in an action spec are missing.
+ *
+ * Parameters  :
+ *          1  :  csp = Current client state (buffers, headers, etc...)
+ *          2  :  cur_action = The action spec to check.
+ *          3  :  multi_index = The index where to look for the filter.
+ *          4  :  filter_type = The filter type the caller is interested in.
+ *
+ * Returns     :  0 => All referenced filters exist, everything else is an error.
+ *
+ *********************************************************************/
+static int referenced_filters_are_missing(const struct client_state *csp,
+   const struct action_spec *cur_action, int multi_index, enum filter_type filter_type)
+{
+   struct list_entry *filtername;
+
+   for (filtername = cur_action->multi_add[multi_index]->first;
+        filtername; filtername = filtername->next)
+   {
+      if (NULL == get_filter(csp, filtername->str, filter_type))
+      {
+         log_error(LOG_LEVEL_ERROR, "Missing %s '%s'",
+            filter_type_to_string(filter_type), filtername->str);
+         return 1;
+      }
+   }
+
+   return 0;
+
+}
+
+
+/*********************************************************************
+ *
+ * Function    :  action_spec_is_valid
+ *
+ * Description :  Should eventually figure out if an action spec
+ *                is valid, but currently only checks that the
+ *                referenced filters are accounted for.
+ *
+ * Parameters  :
+ *          1  :  csp = Current client state (buffers, headers, etc...)
+ *          2  :  cur_action = The action spec to check.
+ *
+ * Returns     :  0 => No problems detected, everything else is an error.
+ *
+ *********************************************************************/
+static int action_spec_is_valid(struct client_state *csp, const struct action_spec *cur_action)
+{
+   struct {
+      int multi_index;
+      enum filter_type filter_type;
+   } filter_map[] = {
+      {ACTION_MULTI_FILTER, FT_CONTENT_FILTER},
+      {ACTION_MULTI_CLIENT_HEADER_FILTER, FT_CLIENT_HEADER_FILTER},
+      {ACTION_MULTI_SERVER_HEADER_FILTER, FT_SERVER_HEADER_FILTER},
+      {ACTION_MULTI_CLIENT_HEADER_TAGGER, FT_CLIENT_HEADER_TAGGER},
+      {ACTION_MULTI_SERVER_HEADER_TAGGER, FT_SERVER_HEADER_TAGGER}
+   };
+   int errors = 0;
+   int i;
+
+   for (i = 0; i < SZ(filter_map); i++)
+   {
+      errors += referenced_filters_are_missing(csp, cur_action,
+         filter_map[i].multi_index, filter_map[i].filter_type);
+   }
+
+   return errors;
+
+}
+
+
+/*********************************************************************
+ *
+ * Function    :  load_one_actions_file
+ *
+ * Description :  Read and parse a action file and add to files
+ *                list.
+ *
+ * Parameters  :
+ *          1  :  csp = Current client state (buffers, headers, etc...)
+ *          2  :  fileid = File index to load.
+ *
+ * Returns     :  0 => Ok, everything else is an error.
+ *
+ *********************************************************************/
+#ifndef FUZZ
+static
+#endif
+int load_one_actions_file(struct client_state *csp, int fileid)
+{
+
+   /*
+    * Parser mode.
+    * Note: Keep these in the order they occur in the file, they are
+    * sometimes tested with <=
+    */
+   enum {
+      MODE_START_OF_FILE = 1,
+      MODE_SETTINGS      = 2,
+      MODE_DESCRIPTION   = 3,
+      MODE_ALIAS         = 4,
+      MODE_ACTIONS       = 5
+   } mode;
+
+   FILE *fp;
+   struct url_actions *last_perm;
+   struct url_actions *perm;
+   char  *buf;
+   struct file_list *fs;
+   struct action_spec * cur_action = NULL;
+   int cur_action_used = 0;
+   struct action_alias * alias_list = NULL;
+   unsigned long linenum = 0;
+   mode = MODE_START_OF_FILE;
+
+   if (!check_file_changed(current_actions_file[fileid], csp->config->actions_file[fileid], &fs))
+   {
+      /* No need to load */
+      csp->actions_list[fileid] = current_actions_file[fileid];
+      return 0;
+   }
+   if (!fs)
+   {
+      log_error(LOG_LEVEL_FATAL, "can't load actions file '%s': %E. "
+         "Note that beginning with Privoxy 3.0.7, actions files have to be specified "
+         "with their complete file names.", csp->config->actions_file[fileid]);
+      return 1; /* never get here */
+   }
+
+   fs->f = last_perm = zalloc_or_die(sizeof(*last_perm));
+
+   if ((fp = fopen(csp->config->actions_file[fileid], "r")) == NULL)
+   {
+      log_error(LOG_LEVEL_FATAL, "can't load actions file '%s': error opening file: %E",
+                csp->config->actions_file[fileid]);
+      return 1; /* never get here */
+   }
+
+   log_error(LOG_LEVEL_INFO, "Loading actions file: %s", csp->config->actions_file[fileid]);
+
+   while (read_config_line(fp, &linenum, &buf) != NULL)
+   {
+      if (*buf == '{')
+      {
+         /* It's a header block */
+         if (buf[1] == '{')
+         {
+            /* It's {{settings}} or {{alias}} */
+            size_t len = strlen(buf);
+            char * start = buf + 2;
+            char * end = buf + len - 1;
+            if ((len < (size_t)5) || (*end-- != '}') || (*end-- != '}'))
+            {
+               /* too short */
+               fclose(fp);
+               log_error(LOG_LEVEL_FATAL,
+                  "can't load actions file '%s': invalid line (%lu): %s",
+                  csp->config->actions_file[fileid], linenum, buf);
+               return 1; /* never get here */
+            }
+
+            /* Trim leading and trailing whitespace. */
+            end[1] = '\0';
+            chomp(start);
+
+            if (*start == '\0')
+            {
+               /* too short */
+               fclose(fp);
+               log_error(LOG_LEVEL_FATAL,
+                  "can't load actions file '%s': invalid line (%lu): {{ }}",
+                  csp->config->actions_file[fileid], linenum);
+               return 1; /* never get here */
+            }
+
+            /*
+             * An actionsfile can optionally contain the following blocks.
+             * They *MUST* be in this order, to simplify processing:
+             *
+             * {{settings}}
+             * name=value...
+             *
+             * {{description}}
+             * ...free text, format TBD, but no line may start with a '{'...
+             *
+             * {{alias}}
+             * name=actions...
+             *
+             * The actual actions must be *after* these special blocks.
+             * None of these special blocks may be repeated.
+             *
+             */
+            if (0 == strcmpic(start, "settings"))
+            {
+               /* it's a {{settings}} block */
+               if (mode >= MODE_SETTINGS)
+               {
+                  /* {{settings}} must be first thing in file and must only
+                   * appear once.
+                   */
+                  fclose(fp);
+                  log_error(LOG_LEVEL_FATAL,
+                     "can't load actions file '%s': line %lu: {{settings}} must only appear once, and it must be before anything else.",
+                     csp->config->actions_file[fileid], linenum);
+               }
+               mode = MODE_SETTINGS;
+            }
+            else if (0 == strcmpic(start, "description"))
+            {
+               /* it's a {{description}} block */
+               if (mode >= MODE_DESCRIPTION)
+               {
+                  /* {{description}} is a singleton and only {{settings}} may proceed it
+                   */
+                  fclose(fp);
+                  log_error(LOG_LEVEL_FATAL,
+                     "can't load actions file '%s': line %lu: {{description}} must only appear once, and only a {{settings}} block may be above it.",
+                     csp->config->actions_file[fileid], linenum);
+               }
+               mode = MODE_DESCRIPTION;
+            }
+            else if (0 == strcmpic(start, "alias"))
+            {
+               /* it's an {{alias}} block */
+               if (mode >= MODE_ALIAS)
+               {
+                  /* {{alias}} must be first thing in file, possibly after
+                   * {{settings}} and {{description}}
+                   *
+                   * {{alias}} must only appear once.
+                   *
+                   * Note that these are new restrictions introduced in
+                   * v2.9.10 in order to make actionsfile editing simpler.
+                   * (Otherwise, reordering actionsfile entries without
+                   * completely rewriting the file becomes non-trivial)
+                   */
+                  fclose(fp);
+                  log_error(LOG_LEVEL_FATAL,
+                     "can't load actions file '%s': line %lu: {{alias}} must only appear once, and it must be before all actions.",
+                     csp->config->actions_file[fileid], linenum);
+               }
+               mode = MODE_ALIAS;
+            }
+            else
+            {
+               /* invalid {{something}} block */
+               fclose(fp);
+               log_error(LOG_LEVEL_FATAL,
+                  "can't load actions file '%s': invalid line (%lu): {{%s}}",
+                  csp->config->actions_file[fileid], linenum, start);
+               return 1; /* never get here */
+            }
+         }
+         else
+         {
+            /* It's an actions block */
+
+            char *actions_buf;
+            char * end;
+
+            /* set mode */
+            mode = MODE_ACTIONS;
+
+            /* free old action */
+            if (cur_action)
+            {
+               if (!cur_action_used)
+               {
+                  free_action_spec(cur_action);
+               }
+               cur_action = NULL;
+            }
+            cur_action_used = 0;
+            cur_action = zalloc_or_die(sizeof(*cur_action));
+            init_action(cur_action);
+
+            /*
+             * Copy the buffer before messing with it as we may need the
+             * unmodified version in for the fatal error messages. Given
+             * that this is not a common event, we could instead simply
+             * read the line again.
+             *
+             * buf + 1 to skip the leading '{'
+             */
+            actions_buf = end = strdup_or_die(buf + 1);
+
+            /* check we have a trailing } and then trim it */
+            if (strlen(actions_buf))
+            {
+               end += strlen(actions_buf) - 1;
+            }
+            if (*end != '}')
+            {
+               /* No closing } */
+               fclose(fp);
+               freez(actions_buf);
+               log_error(LOG_LEVEL_FATAL, "can't load actions file '%s': "
+                  "Missing trailing '}' in action section starting at line (%lu): %s",
+                  csp->config->actions_file[fileid], linenum, buf);
+               return 1; /* never get here */
+            }
+            *end = '\0';
+
+            /* trim any whitespace immediately inside {} */
+            chomp(actions_buf);
+
+            if (get_actions(actions_buf, alias_list, cur_action))
+            {
+               /* error */
+               fclose(fp);
+               freez(actions_buf);
+               log_error(LOG_LEVEL_FATAL, "can't load actions file '%s': "
+                  "can't completely parse the action section starting at line (%lu): %s",
+                  csp->config->actions_file[fileid], linenum, buf);
+               return 1; /* never get here */
+            }
+
+            if (action_spec_is_valid(csp, cur_action))
+            {
+               log_error(LOG_LEVEL_ERROR, "Invalid action section in file '%s', "
+                  "starting at line %lu: %s",
+                  csp->config->actions_file[fileid], linenum, buf);
+            }
+
+            freez(actions_buf);
+         }
+      }
+      else if (mode == MODE_SETTINGS)
+      {
+         /*
+          * Part of the {{settings}} block.
+          * For now only serves to check if the file's minimum Privoxy
+          * version requirement is met, but we may want to read & check
+          * permissions when we go multi-user.
+          */
+         if (!strncmp(buf, "for-privoxy-version=", 20))
+         {
+            char *version_string, *fields[3];
+            int num_fields;
+
+            version_string = strdup_or_die(buf + 20);
+
+            num_fields = ssplit(version_string, ".", fields, SZ(fields));
+
+            if (num_fields < 1 || atoi(fields[0]) == 0)
+            {
+               log_error(LOG_LEVEL_ERROR,
+                 "While loading actions file '%s': invalid line (%lu): %s",
+                  csp->config->actions_file[fileid], linenum, buf);
+            }
+            else if (                  (atoi(fields[0]) > VERSION_MAJOR)
+               || ((num_fields > 1) && (atoi(fields[1]) > VERSION_MINOR))
+               || ((num_fields > 2) && (atoi(fields[2]) > VERSION_POINT)))
+            {
+               fclose(fp);
+               log_error(LOG_LEVEL_FATAL,
+                         "Actions file '%s', line %lu requires newer Privoxy version: %s",
+                         csp->config->actions_file[fileid], linenum, buf);
+               return 1; /* never get here */
+            }
+            free(version_string);
+         }
+      }
+      else if (mode == MODE_DESCRIPTION)
+      {
+         /*
+          * Part of the {{description}} block.
+          * Ignore for now.
+          */
+      }
+      else if (mode == MODE_ALIAS)
+      {
+         /*
+          * define an alias
+          */
+         char  actions_buf[BUFFER_SIZE];
+         struct action_alias * new_alias;
+
+         char * start = strchr(buf, '=');
+         char * end = start;
+
+         if ((start == NULL) || (start == buf))
+         {
+            log_error(LOG_LEVEL_FATAL,
+               "can't load actions file '%s': invalid alias line (%lu): %s",
+               csp->config->actions_file[fileid], linenum, buf);
+            return 1; /* never get here */
+         }
+
+         new_alias = zalloc_or_die(sizeof(*new_alias));
+
+         /* Eat any the whitespace before the '=' */
+         end--;
+         while ((*end == ' ') || (*end == '\t'))
+         {
+            /*
+             * we already know we must have at least 1 non-ws char
+             * at start of buf - no need to check
+             */
+            end--;
+         }
+         end[1] = '\0';
+
+         /* Eat any the whitespace after the '=' */
+         start++;
+         while ((*start == ' ') || (*start == '\t'))
+         {
+            start++;
+         }
+         if (*start == '\0')
+         {
+            log_error(LOG_LEVEL_FATAL,
+               "can't load actions file '%s': invalid alias line (%lu): %s",
+               csp->config->actions_file[fileid], linenum, buf);
+            return 1; /* never get here */
+         }
+
+         new_alias->name = strdup_or_die(buf);
+
+         strlcpy(actions_buf, start, sizeof(actions_buf));
+
+         if (get_actions(actions_buf, alias_list, new_alias->action))
+         {
+            /* error */
+            fclose(fp);
+            log_error(LOG_LEVEL_FATAL,
+               "can't load actions file '%s': invalid alias line (%lu): %s = %s",
+               csp->config->actions_file[fileid], linenum, buf, start);
+            return 1; /* never get here */
+         }
+
+         /* add to list */
+         new_alias->next = alias_list;
+         alias_list = new_alias;
+      }
+      else if (mode == MODE_ACTIONS)
+      {
+         /* it's an URL pattern */
+
+         /* allocate a new node */
+         perm = zalloc_or_die(sizeof(*perm));
+
+         perm->action = cur_action;
+         cur_action_used = 1;
+
+         /* Save the URL pattern */
+         if (create_pattern_spec(perm->url, buf))
+         {
+            fclose(fp);
+            log_error(LOG_LEVEL_FATAL,
+               "can't load actions file '%s': line %lu: cannot create URL or TAG pattern from: %s",
+               csp->config->actions_file[fileid], linenum, buf);
+            return 1; /* never get here */
+         }
+
+         /* add it to the list */
+         last_perm->next = perm;
+         last_perm = perm;
+      }
+      else if (mode == MODE_START_OF_FILE)
+      {
+         /* oops - please have a {} line as 1st line in file. */
+         fclose(fp);
+         log_error(LOG_LEVEL_FATAL,
+            "can't load actions file '%s': line %lu should begin with a '{': %s",
+            csp->config->actions_file[fileid], linenum, buf);
+         return 1; /* never get here */
+      }
+      else
+      {
+         /* How did we get here? This is impossible! */
+         fclose(fp);
+         log_error(LOG_LEVEL_FATAL,
+            "can't load actions file '%s': INTERNAL ERROR - mode = %d",
+            csp->config->actions_file[fileid], mode);
+         return 1; /* never get here */
+      }
+      freez(buf);
+   }
+
+   fclose(fp);
+
+   if (!cur_action_used)
+   {
+      free_action_spec(cur_action);
+   }
+   free_alias_list(alias_list);
+
+   /* the old one is now obsolete */
+   if (current_actions_file[fileid])
+   {
+      current_actions_file[fileid]->unloader = unload_actions_file;
+   }
+
+   fs->next    = files->next;
+   files->next = fs;
+   current_actions_file[fileid] = fs;
+
+   csp->actions_list[fileid] = fs;
+
+   return(0);
+
+}
+
+
+/*********************************************************************
+ *
+ * Function    :  actions_to_text
+ *
+ * Description :  Converts a actionsfile entry from the internal
+ *                structure into a text line.  The output is split
+ *                into one line for each action with line continuation.
+ *
+ * Parameters  :
+ *          1  :  action = The action to format.
+ *
+ * Returns     :  A string.  Caller must free it.
+ *                NULL on out-of-memory error.
+ *
+ *********************************************************************/
+char * actions_to_text(const struct action_spec *action)
+{
+   unsigned long mask = action->mask;
+   unsigned long add  = action->add;
+   char *result = strdup_or_die("");
+   struct list_entry * lst;
+
+   /* sanity - prevents "-feature +feature" */
+   mask |= add;
+
+
+#define DEFINE_ACTION_BOOL(__name, __bit)          \
+   if (!(mask & __bit))                            \
+   {                                               \
+      string_append(&result, " -" __name " \\\n"); \
+   }                                               \
+   else if (add & __bit)                           \
+   {                                               \
+      string_append(&result, " +" __name " \\\n"); \
+   }
+
+#define DEFINE_ACTION_STRING(__name, __bit, __index)   \
+   if (!(mask & __bit))                                \
+   {                                                   \
+      string_append(&result, " -" __name " \\\n");     \
+   }                                                   \
+   else if (add & __bit)                               \
+   {                                                   \
+      string_append(&result, " +" __name "{");         \
+      string_append(&result, action->string[__index]); \
+      string_append(&result, "} \\\n");                \
+   }
+
+#define DEFINE_ACTION_MULTI(__name, __index)         \
+   if (action->multi_remove_all[__index])            \
+   {                                                 \
+      string_append(&result, " -" __name " \\\n");   \
+   }                                                 \
+   else                                              \
+   {                                                 \
+      lst = action->multi_remove[__index]->first;    \
+      while (lst)                                    \
+      {                                              \
+         string_append(&result, " -" __name "{");    \
+         string_append(&result, lst->str);           \
+         string_append(&result, "} \\\n");           \
+         lst = lst->next;                            \
+      }                                              \
+   }                                                 \
+   lst = action->multi_add[__index]->first;          \
+   while (lst)                                       \
+   {                                                 \
+      string_append(&result, " +" __name "{");       \
+      string_append(&result, lst->str);              \
+      string_append(&result, "} \\\n");              \
+      lst = lst->next;                               \
+   }
+
+#define DEFINE_ACTION_ALIAS 0 /* No aliases for output */
+
+#include "actionlist.h"
+
+#undef DEFINE_ACTION_MULTI
+#undef DEFINE_ACTION_STRING
+#undef DEFINE_ACTION_BOOL
+#undef DEFINE_ACTION_ALIAS
+
+   return result;
+}
+
+
+/*********************************************************************
+ *
+ * Function    :  actions_to_html
+ *
+ * Description :  Converts a actionsfile entry from numeric form
+ *                ("mask" and "add") to a <br>-separated HTML string
+ *                in which each action is linked to its chapter in
+ *                the user manual.
+ *
+ * Parameters  :
+ *          1  :  csp    = Client state (for config)
+ *          2  :  action = Action spec to be converted
+ *
+ * Returns     :  A string.  Caller must free it.
+ *                NULL on out-of-memory error.
+ *
+ *********************************************************************/
+char * actions_to_html(const struct client_state *csp,
+                       const struct action_spec *action)
+{
+   unsigned long mask = action->mask;
+   unsigned long add  = action->add;
+   char *result = strdup_or_die("");
+   struct list_entry * lst;
+
+   /* sanity - prevents "-feature +feature" */
+   mask |= add;
+
+
+#define DEFINE_ACTION_BOOL(__name, __bit)       \
+   if (!(mask & __bit))                         \
+   {                                            \
+      string_append(&result, "\n<br>-");        \
+      string_join(&result, add_help_link(__name, csp->config)); \
+   }                                            \
+   else if (add & __bit)                        \
+   {                                            \
+      string_append(&result, "\n<br>+");        \
+      string_join(&result, add_help_link(__name, csp->config)); \
+   }
+
+#define DEFINE_ACTION_STRING(__name, __bit, __index) \
+   if (!(mask & __bit))                              \
+   {                                                 \
+      string_append(&result, "\n<br>-");             \
+      string_join(&result, add_help_link(__name, csp->config)); \
+   }                                                 \
+   else if (add & __bit)                             \
+   {                                                 \
+      string_append(&result, "\n<br>+");             \
+      string_join(&result, add_help_link(__name, csp->config)); \
+      string_append(&result, "{");                   \
+      string_join(&result, html_encode(action->string[__index])); \
+      string_append(&result, "}");                   \
+   }
+
+#define DEFINE_ACTION_MULTI(__name, __index)          \
+   if (action->multi_remove_all[__index])             \
+   {                                                  \
+      string_append(&result, "\n<br>-");              \
+      string_join(&result, add_help_link(__name, csp->config)); \
+   }                                                  \
+   else                                               \
+   {                                                  \
+      lst = action->multi_remove[__index]->first;     \
+      while (lst)                                     \
+      {                                               \
+         string_append(&result, "\n<br>-");           \
+         string_join(&result, add_help_link(__name, csp->config)); \
+         string_append(&result, "{");                 \
+         string_join(&result, html_encode(lst->str)); \
+         string_append(&result, "}");                 \
+         lst = lst->next;                             \
+      }                                               \
+   }                                                  \
+   lst = action->multi_add[__index]->first;           \
+   while (lst)                                        \
+   {                                                  \
+      string_append(&result, "\n<br>+");              \
+      string_join(&result, add_help_link(__name, csp->config)); \
+      string_append(&result, "{");                    \
+      string_join(&result, html_encode(lst->str));    \
+      string_append(&result, "}");                    \
+      lst = lst->next;                                \
+   }
+
+#define DEFINE_ACTION_ALIAS 0 /* No aliases for output */
+
+#include "actionlist.h"
+
+#undef DEFINE_ACTION_MULTI
+#undef DEFINE_ACTION_STRING
+#undef DEFINE_ACTION_BOOL
+#undef DEFINE_ACTION_ALIAS
+
+   /* trim leading <br> */
+   if (result && *result)
+   {
+      char * s = result;
+      result = strdup(result + 5);
+      free(s);
+   }
+
+   return result;
+}
+
+
+/*********************************************************************
+ *
+ * Function    :  current_actions_to_html
+ *
+ * Description :  Converts a current action spec to a <br> separated HTML
+ *                text in which each action is linked to its chapter in
+ *                the user manual.
+ *
+ * Parameters  :
+ *          1  :  csp    = Client state (for config)
+ *          2  :  action = Current action spec to be converted
+ *
+ * Returns     :  A string.  Caller must free it.
+ *                NULL on out-of-memory error.
+ *
+ *********************************************************************/
+char *current_action_to_html(const struct client_state *csp,
+                             const struct current_action_spec *action)
+{
+   unsigned long flags  = action->flags;
+   struct list_entry * lst;
+   char *result   = strdup_or_die("");
+   char *active   = strdup_or_die("");
+   char *inactive = strdup_or_die("");
+
+#define DEFINE_ACTION_BOOL(__name, __bit)  \
+   if (flags & __bit)                      \
+   {                                       \
+      string_append(&active, "\n<br>+");   \
+      string_join(&active, add_help_link(__name, csp->config)); \
+   }                                       \
+   else                                    \
+   {                                       \
+      string_append(&inactive, "\n<br>-"); \
+      string_join(&inactive, add_help_link(__name, csp->config)); \
+   }
+
+#define DEFINE_ACTION_STRING(__name, __bit, __index)   \
+   if (flags & __bit)                                  \
+   {                                                   \
+      string_append(&active, "\n<br>+");               \
+      string_join(&active, add_help_link(__name, csp->config)); \
+      string_append(&active, "{");                     \
+      string_join(&active, html_encode(action->string[__index])); \
+      string_append(&active, "}");                     \
+   }                                                   \
+   else                                                \
+   {                                                   \
+      string_append(&inactive, "\n<br>-");             \
+      string_join(&inactive, add_help_link(__name, csp->config)); \
+   }
+
+#define DEFINE_ACTION_MULTI(__name, __index)           \
+   lst = action->multi[__index]->first;                \
+   if (lst == NULL)                                    \
+   {                                                   \
+      string_append(&inactive, "\n<br>-");             \
+      string_join(&inactive, add_help_link(__name, csp->config)); \
+   }                                                   \
+   else                                                \
+   {                                                   \
+      while (lst)                                      \
+      {                                                \
+         string_append(&active, "\n<br>+");            \
+         string_join(&active, add_help_link(__name, csp->config)); \
+         string_append(&active, "{");                  \
+         string_join(&active, html_encode(lst->str));  \
+         string_append(&active, "}");                  \
+         lst = lst->next;                              \
+      }                                                \
+   }
+
+#define DEFINE_ACTION_ALIAS 0 /* No aliases for output */
+
+#include "actionlist.h"
+
+#undef DEFINE_ACTION_MULTI
+#undef DEFINE_ACTION_STRING
+#undef DEFINE_ACTION_BOOL
+#undef DEFINE_ACTION_ALIAS
+
+   if (active != NULL)
+   {
+      string_append(&result, active);
+      freez(active);
+   }
+   string_append(&result, "\n<br>");
+   if (inactive != NULL)
+   {
+      string_append(&result, inactive);
+      freez(inactive);
+   }
+   return result;
+}
+
+
+/*********************************************************************
+ *
+ * Function    :  action_to_line_of_text
+ *
+ * Description :  Converts a action spec to a single text line
+ *                listing the enabled actions.
+ *
+ * Parameters  :
+ *          1  :  action = Current action spec to be converted
+ *
+ * Returns     :  A string. Caller must free it.
+ *                Out-of-memory errors are fatal.
+ *
+ *********************************************************************/
+char *actions_to_line_of_text(const struct current_action_spec *action)
+{
+   char buffer[200];
+   struct list_entry *lst;
+   char *active;
+   const unsigned long flags = action->flags;
+
+   active = strdup_or_die("");
+
+#define DEFINE_ACTION_BOOL(__name, __bit)               \
+   if (flags & __bit)                                   \
+   {                                                    \
+      snprintf(buffer, sizeof(buffer), "+%s ", __name); \
+      string_append(&active, buffer);                   \
+   }                                                    \
+
+#define DEFINE_ACTION_STRING(__name, __bit, __index)    \
+   if (flags & __bit)                                   \
+   {                                                    \
+      snprintf(buffer, sizeof(buffer), "+%s{%s} ",      \
+         __name, action->string[__index]);              \
+      string_append(&active, buffer);                   \
+   }                                                    \
+
+#define DEFINE_ACTION_MULTI(__name, __index)            \
+   lst = action->multi[__index]->first;                 \
+   while (lst != NULL)                                  \
+   {                                                    \
+      snprintf(buffer, sizeof(buffer), "+%s{%s} ",      \
+         __name, lst->str);                             \
+      string_append(&active, buffer);                   \
+      lst = lst->next;                                  \
+   }                                                    \
+
+#define DEFINE_ACTION_ALIAS 0 /* No aliases for output */
+
+#include "actionlist.h"
+
+#undef DEFINE_ACTION_MULTI
+#undef DEFINE_ACTION_STRING
+#undef DEFINE_ACTION_BOOL
+#undef DEFINE_ACTION_ALIAS
+
+   if (active == NULL)
+   {
+      log_error(LOG_LEVEL_FATAL, "Out of memory in action_to_line_of_text()");
+   }
+
+   return active;
+}

+ 96 - 0
privoxy/actions.h

@@ -0,0 +1,96 @@
+#ifndef ACTIONS_H_INCLUDED
+#define ACTIONS_H_INCLUDED
+/*********************************************************************
+ *
+ * File        :  $Source: /cvsroot/ijbswa/current/actions.h,v $
+ *
+ * Purpose     :  Declares functions to work with actions files
+ *                Functions declared include: FIXME
+ *
+ * Copyright   :  Written by and Copyright (C) 2001-2007 members of the
+ *                Privoxy team. https://www.privoxy.org/
+ *
+ *                Based on the Internet Junkbuster originally written
+ *                by and Copyright (C) 1997 Anonymous Coders and
+ *                Junkbusters Corporation.  http://www.junkbusters.com
+ *
+ *                This program is free software; you can redistribute it
+ *                and/or modify it under the terms of the GNU General
+ *                Public License as published by the Free Software
+ *                Foundation; either version 2 of the License, or (at
+ *                your option) any later version.
+ *
+ *                This program is distributed in the hope that it will
+ *                be useful, but WITHOUT ANY WARRANTY; without even the
+ *                implied warranty of MERCHANTABILITY or FITNESS FOR A
+ *                PARTICULAR PURPOSE.  See the GNU General Public
+ *                License for more details.
+ *
+ *                The GNU General Public License should be included with
+ *                this file.  If not, you can view it at
+ *                http://www.gnu.org/copyleft/gpl.html
+ *                or write to the Free Software Foundation, Inc., 59
+ *                Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ *
+ *********************************************************************/
+
+
+struct action_spec;
+struct current_action_spec;
+struct client_state;
+
+
+
+/* This structure is used to hold user-defined aliases */
+struct action_alias
+{
+   const char * name;
+   struct action_spec action[1];
+   struct action_alias * next;
+};
+
+
+extern jb_err get_actions (char *line,
+                           struct action_alias * alias_list,
+                           struct action_spec *cur_action);
+extern void free_alias_list(struct action_alias *alias_list);
+
+extern void init_action(struct action_spec *dest);
+extern void free_action(struct action_spec *src);
+extern jb_err merge_actions (struct action_spec *dest,
+                             const struct action_spec *src);
+extern int update_action_bits_for_tag(struct client_state *csp, const char *tag);
+extern jb_err check_negative_tag_patterns(struct client_state *csp, unsigned int flag);
+extern jb_err copy_action (struct action_spec *dest,
+                           const struct action_spec *src);
+extern char * actions_to_text     (const struct action_spec *action);
+extern char * actions_to_html     (const struct client_state *csp,
+                                   const struct action_spec *action);
+extern void init_current_action     (struct current_action_spec *dest);
+extern void free_current_action     (struct current_action_spec *src);
+extern jb_err merge_current_action  (struct current_action_spec *dest,
+                                     const struct action_spec *src);
+extern char * current_action_to_html(const struct client_state *csp,
+                                     const struct current_action_spec *action);
+extern char * actions_to_line_of_text(const struct current_action_spec *action);
+
+extern jb_err get_action_token(char **line, char **name, char **value);
+extern void unload_actions_file(void *file_data);
+extern int load_action_files(struct client_state *csp);
+#ifdef FUZZ
+extern int load_one_actions_file(struct client_state *csp, int fileid);
+#endif
+
+#ifdef FEATURE_GRACEFUL_TERMINATION
+void unload_current_actions_file(void);
+#endif
+
+
+#endif /* ndef ACTIONS_H_INCLUDED */
+
+/*
+  Local Variables:
+  tab-width: 3
+  end:
+*/
+

Some files were not shown because too many files changed in this diff