mirror of
https://github.com/fnproject/fn.git
synced 2022-10-28 21:29:17 +03:00
Bunch of cleanup and reorg
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -14,3 +14,5 @@ vendor/
|
||||
/functions
|
||||
|
||||
private.sh
|
||||
.env
|
||||
*.pem
|
||||
|
||||
@@ -13,24 +13,9 @@ glide install
|
||||
Test it, the iron token and project id are for cache.
|
||||
|
||||
```sh
|
||||
docker run -e "IRON_TOKEN=GP8cqlKSrcpmqeR8x9WKD4qSAss" -e "IRON_PROJECT_ID=4fd2729368a0197d1102056b" -e "CLOUDFLARE_EMAIL=treeder@gmail.com" -e "CLOUDFLARE_API_KEY=x" --rm -it --privileged -p 8080:8080 iron/functions
|
||||
docker run --env-file .env --rm -it --privileged -p 8080:8080 iron/functions
|
||||
```
|
||||
|
||||
Push it:
|
||||
|
||||
```sh
|
||||
docker push iron/functions
|
||||
```
|
||||
|
||||
Get it on a server and point router.iron.computer (on cloudflare) to the machine.
|
||||
|
||||
After deploying, run it with:
|
||||
|
||||
```sh
|
||||
docker run -e --name functions -it --privileged -d -p 80:80 "IRON_TOKEN=GP8cqlKSrcpmqeR8x9WKD4qSAss" -e "IRON_PROJECT_ID=4fd2729368a0197d1102056b" -e PORT=80 iron/functions
|
||||
```
|
||||
|
||||
|
||||
## Releasing
|
||||
|
||||
```sh
|
||||
|
||||
16
README.md
16
README.md
@@ -1,15 +1,25 @@
|
||||
Note: currently running at: http://gateway.iron.computer:8080/
|
||||
|
||||
# MicroServices Gateway / API Gateway
|
||||
# IronFunctions
|
||||
|
||||
First, let's fire up an IronFunctions instance. Copy the [example.env](example.env) file into a file named `.env` and fill in the missing values.
|
||||
|
||||
Then start your functions instance:
|
||||
|
||||
```
|
||||
docker run --env-file .env --rm -it --privileged -p 8080:8080 iron/functions
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
First things first, create an app/service:
|
||||
|
||||
TOOD: App or service??
|
||||
|
||||
```sh
|
||||
iron create app
|
||||
iron create app APP_NAME
|
||||
# OR
|
||||
curl -H "Content-Type: application/json" -X POST -d '{"name":"myapp"}' http://localhost:8080/api/v1/apps
|
||||
curl -H "Content-Type: application/json" -X POST -d '{"name":"APP_NAME"}' http://localhost:8080/api/v1/apps
|
||||
```
|
||||
|
||||
Now add routes to the app. First we'll add a route to the output of a docker container:
|
||||
|
||||
@@ -1,83 +1,29 @@
|
||||
/*
|
||||
|
||||
For keeping a minimum running, perhaps when doing a routing table update, if destination hosts are all
|
||||
expired or about to expire we start more.
|
||||
|
||||
*/
|
||||
|
||||
package main
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/iron-io/go/common"
|
||||
"github.com/iron-io/iron_go/cache"
|
||||
)
|
||||
|
||||
var config struct {
|
||||
CloudFlare struct {
|
||||
Email string `json:"email"`
|
||||
AuthKey string `json:"auth_key"`
|
||||
} `json:"cloudflare"`
|
||||
Cache struct {
|
||||
Host string `json:"host"`
|
||||
Token string `json:"token"`
|
||||
ProjectId string `json:"project_id"`
|
||||
}
|
||||
Iron struct {
|
||||
Token string `json:"token"`
|
||||
ProjectId string `json:"project_id"`
|
||||
SuperToken string `json:"super_token"`
|
||||
WorkerHost string `json:"worker_host"`
|
||||
AuthHost string `json:"auth_host"`
|
||||
} `json:"iron"`
|
||||
Logging struct {
|
||||
To string `json:"to"`
|
||||
Level string `json:"level"`
|
||||
Prefix string `json:"prefix"`
|
||||
}
|
||||
}
|
||||
|
||||
//var routingTable = map[string]*Route{}
|
||||
var icache = cache.New("routing-table")
|
||||
var config *Config
|
||||
|
||||
var (
|
||||
ironAuth common.Auther
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
func New(conf *Config) *Api {
|
||||
config = conf
|
||||
api := &Api{}
|
||||
return api
|
||||
}
|
||||
|
||||
func main() {
|
||||
type Api struct {
|
||||
}
|
||||
|
||||
var configFile string
|
||||
var env string
|
||||
flag.StringVar(&configFile, "c", "", "Config file name")
|
||||
// when this was e, it was erroring out.
|
||||
flag.StringVar(&env, "e", "development", "environment")
|
||||
func (api *Api) Start() {
|
||||
|
||||
flag.Parse() // Scans the arg list and sets up flags
|
||||
|
||||
// Deployer is now passing -c in since we're using upstart and it doesn't know what directory to run in
|
||||
if configFile == "" {
|
||||
configFile = "config_" + env + ".json"
|
||||
}
|
||||
|
||||
// common.LoadConfigFile(configFile, &config)
|
||||
// common.SetLogging(common.LoggingConfig{To: config.Logging.To, Level: config.Logging.Level, Prefix: config.Logging.Prefix})
|
||||
|
||||
// TODO: validate inputs, iron tokens, cloudflare stuff, etc
|
||||
config.CloudFlare.Email = os.Getenv("CLOUDFLARE_EMAIL")
|
||||
config.CloudFlare.AuthKey = os.Getenv("CLOUDFLARE_API_KEY")
|
||||
|
||||
log.Println("config:", config)
|
||||
log.Infoln("Starting up router version", Version)
|
||||
|
||||
r := mux.NewRouter()
|
||||
@@ -165,7 +111,7 @@ func NewRoute(w http.ResponseWriter, req *http.Request) {
|
||||
vars := mux.Vars(req)
|
||||
projectId := vars["project_id"]
|
||||
appName := vars["app_name"]
|
||||
log.Infoln("project_id:", projectId, "app_name", appName)
|
||||
log.Infoln("project_id: ", projectId, "app: ", appName)
|
||||
|
||||
route := &Route3{}
|
||||
if !ReadJSON(w, req, &route) {
|
||||
31
api/config.go
Normal file
31
api/config.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package api
|
||||
|
||||
type Config struct {
|
||||
CloudFlare struct {
|
||||
Email string `json:"email"`
|
||||
ApiKey string `json:"api_key"`
|
||||
ZoneId string `json:"zone_id"`
|
||||
} `json:"cloudflare"`
|
||||
Cache struct {
|
||||
Host string `json:"host"`
|
||||
Token string `json:"token"`
|
||||
ProjectId string `json:"project_id"`
|
||||
}
|
||||
Iron struct {
|
||||
Token string `json:"token"`
|
||||
ProjectId string `json:"project_id"`
|
||||
SuperToken string `json:"super_token"`
|
||||
WorkerHost string `json:"worker_host"`
|
||||
AuthHost string `json:"auth_host"`
|
||||
} `json:"iron"`
|
||||
Logging struct {
|
||||
To string `json:"to"`
|
||||
Level string `json:"level"`
|
||||
Prefix string `json:"prefix"`
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Config) Validate() error {
|
||||
// TODO:
|
||||
return nil
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -25,22 +25,22 @@ the routing table. Backend runners know this too.
|
||||
This will also create a dns entry for the worker in iron.computer.
|
||||
*/
|
||||
func registerHost(w http.ResponseWriter, r *http.Request, app *App) bool {
|
||||
// Give it an iron.computer entry with format:
|
||||
// WORKER_NAME.PROJECT_ID.iron.computer.
|
||||
dnsHost := fmt.Sprintf("%v.%v.iron.computer", app.Name, 123)
|
||||
// Give it an iron.computer entry with format: APP_NAME.PROJECT_ID.ironfunctions.com
|
||||
dnsHost := fmt.Sprintf("%v.%v.ironfunctions.com", app.Name, 123)
|
||||
app.Dns = dnsHost
|
||||
log.Info("registering dns", "dnsname", dnsHost)
|
||||
|
||||
if app.CloudFlareId == "" {
|
||||
// Tad hacky, but their go lib is pretty confusing.
|
||||
cfMethod := "POST"
|
||||
cfUrl := "https://api.cloudflare.com/client/v4/zones/29a42a6c6b9b2ed4b843b78d65b8af89/dns_records"
|
||||
cfUrl := fmt.Sprintf("https://api.cloudflare.com/client/v4/zones/%v/dns_records", config.CloudFlare.ZoneId)
|
||||
if app.CloudFlareId != "" {
|
||||
// Have this here in case we need to support updating the entry. If we do this, is how:
|
||||
cfMethod = "PUT"
|
||||
cfUrl = cfUrl + "/" + app.CloudFlareId
|
||||
}
|
||||
cfbody := "{\"type\":\"CNAME\",\"name\":\"" + dnsHost + "\",\"content\":\"router.iron.computer\",\"ttl\":120}"
|
||||
log.Info("registering dns: ", "dnsname: ", dnsHost, " url: ", cfUrl)
|
||||
|
||||
cfbody := "{\"type\":\"CNAME\",\"name\":\"" + dnsHost + "\",\"content\":\"api.ironfunctions.com\",\"ttl\":120}"
|
||||
client := &http.Client{} // todo: is default client fine?
|
||||
req, err := http.NewRequest(
|
||||
cfMethod,
|
||||
@@ -48,7 +48,7 @@ func registerHost(w http.ResponseWriter, r *http.Request, app *App) bool {
|
||||
strings.NewReader(cfbody),
|
||||
)
|
||||
req.Header.Set("X-Auth-Email", config.CloudFlare.Email)
|
||||
req.Header.Set("X-Auth-Key", config.CloudFlare.AuthKey)
|
||||
req.Header.Set("X-Auth-Key", config.CloudFlare.ApiKey)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package api
|
||||
|
||||
type Route struct {
|
||||
// TODO: Change destinations to a simple cache so it can expire entries after 55 minutes (the one we use in common?)
|
||||
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package api
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
3
api/version.go
Normal file
3
api/version.go
Normal file
@@ -0,0 +1,3 @@
|
||||
package api
|
||||
|
||||
const Version = "0.0.26"
|
||||
22
client.rb
22
client.rb
@@ -1,22 +0,0 @@
|
||||
require 'rest'
|
||||
|
||||
Rest.logger.level = Logger::DEBUG
|
||||
rest = Rest::Client.new
|
||||
|
||||
base_url = "http://routertest.irondns.info/"
|
||||
#"http://localhost:8080/"
|
||||
|
||||
response = rest.get(base_url)
|
||||
puts "body:"
|
||||
puts response.body
|
||||
puts "\n\n"
|
||||
|
||||
|
||||
# test post
|
||||
begin
|
||||
r = rest.post("#{base_url}somepost", :form_data=>{:x=>1, :y=>"a"})
|
||||
rescue Rest::HttpError => ex
|
||||
p ex
|
||||
end
|
||||
p r
|
||||
p r.body
|
||||
9
example.env
Normal file
9
example.env
Normal file
@@ -0,0 +1,9 @@
|
||||
# For IronCache
|
||||
IRON_TOKEN=X
|
||||
IRON_PROJECT_ID=X
|
||||
|
||||
# For CloudFlare dns support
|
||||
CLOUDFLARE_EMAIL=you@example.com
|
||||
CLOUDFLARE_API_KEY=X
|
||||
# See comments here to get zone id https://blog.cloudflare.com/cloudflare-tips-frequently-used-cloudflare-ap/#comment-2412200222
|
||||
CLOUDFLARE_ZONE_ID=y
|
||||
@@ -1,4 +0,0 @@
|
||||
---
|
||||
BUNDLE_PATH: bundle
|
||||
BUNDLE_CLEAN: true
|
||||
BUNDLE_DISABLE_SHARED_GEMS: '1'
|
||||
@@ -1,8 +0,0 @@
|
||||
FROM treeder/ruby:2.2.2
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ADD . /app
|
||||
|
||||
ENTRYPOINT ["ruby", "app.rb"]
|
||||
# CMD ["/bin/bash"] FROM ubuntu
|
||||
@@ -1,3 +0,0 @@
|
||||
source 'https://rubygems.org'
|
||||
|
||||
gem 'sinatra'
|
||||
@@ -1,20 +0,0 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
rack (1.6.4)
|
||||
rack-protection (1.5.3)
|
||||
rack
|
||||
sinatra (1.4.6)
|
||||
rack (~> 1.4)
|
||||
rack-protection (~> 1.4)
|
||||
tilt (>= 1.3, < 3)
|
||||
tilt (2.0.1)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
sinatra
|
||||
|
||||
BUNDLED WITH
|
||||
1.10.5
|
||||
@@ -1,19 +0,0 @@
|
||||
|
||||
|
||||
```
|
||||
docker run --rm -p 8080:8080 treeder/hello-sinatra
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Building
|
||||
|
||||
Update gems and vendor them
|
||||
```
|
||||
dj run treeder/ruby:2.2.2 bundle update
|
||||
dj run treeder/ruby:2.2.2 bundle install --standalone --clean
|
||||
```
|
||||
|
||||
```
|
||||
docker build -t treeder/hello-sinatra:latest .
|
||||
```
|
||||
@@ -1,21 +0,0 @@
|
||||
require_relative 'bundle/bundler/setup'
|
||||
require 'sinatra'
|
||||
|
||||
# Now we start the actual worker
|
||||
##################################################################3
|
||||
|
||||
port = ENV['PORT'] || 8080
|
||||
puts "STARTING SINATRA on port #{port}"
|
||||
my_app = Sinatra.new do
|
||||
set :port, port
|
||||
set :bind, '0.0.0.0'
|
||||
post('/somepost') do
|
||||
puts "in somepost"
|
||||
p params
|
||||
end
|
||||
get('/ping') { "pong" }
|
||||
get('/') { "hi!" }
|
||||
|
||||
# get('/*') { "you passed in #{params[:splat].inspect}" }
|
||||
end
|
||||
my_app.run!
|
||||
@@ -1,9 +0,0 @@
|
||||
require 'rbconfig'
|
||||
# ruby 1.8.7 doesn't define RUBY_ENGINE
|
||||
ruby_engine = defined?(RUBY_ENGINE) ? RUBY_ENGINE : 'ruby'
|
||||
ruby_version = RbConfig::CONFIG["ruby_version"]
|
||||
path = File.expand_path('..', __FILE__)
|
||||
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/rack-1.6.4/lib"
|
||||
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/rack-protection-1.5.3/lib"
|
||||
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/tilt-2.0.1/lib"
|
||||
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/sinatra-1.4.6/lib"
|
||||
@@ -1,23 +0,0 @@
|
||||
#!/usr/bin/env ruby
|
||||
#
|
||||
# This file was generated by RubyGems.
|
||||
#
|
||||
# The application 'rack' is installed as part of a gem, and
|
||||
# this file is here to facilitate running it.
|
||||
#
|
||||
|
||||
require 'rubygems'
|
||||
|
||||
version = ">= 0"
|
||||
|
||||
if ARGV.first
|
||||
str = ARGV.first
|
||||
str = str.dup.force_encoding("BINARY") if str.respond_to? :force_encoding
|
||||
if str =~ /\A_(.*)_\z/ and Gem::Version.correct?($1) then
|
||||
version = $1
|
||||
ARGV.shift
|
||||
end
|
||||
end
|
||||
|
||||
gem 'rack', version
|
||||
load Gem.bin_path('rack', 'rackup', version)
|
||||
@@ -1,23 +0,0 @@
|
||||
#!/usr/bin/env ruby
|
||||
#
|
||||
# This file was generated by RubyGems.
|
||||
#
|
||||
# The application 'tilt' is installed as part of a gem, and
|
||||
# this file is here to facilitate running it.
|
||||
#
|
||||
|
||||
require 'rubygems'
|
||||
|
||||
version = ">= 0"
|
||||
|
||||
if ARGV.first
|
||||
str = ARGV.first
|
||||
str = str.dup.force_encoding("BINARY") if str.respond_to? :force_encoding
|
||||
if str =~ /\A_(.*)_\z/ and Gem::Version.correct?($1) then
|
||||
version = $1
|
||||
ARGV.shift
|
||||
end
|
||||
end
|
||||
|
||||
gem 'tilt', version
|
||||
load Gem.bin_path('tilt', 'tilt', version)
|
||||
BIN
hello-sinatra/bundle/ruby/2.2.0/cache/rack-1.6.4.gem
vendored
BIN
hello-sinatra/bundle/ruby/2.2.0/cache/rack-1.6.4.gem
vendored
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
hello-sinatra/bundle/ruby/2.2.0/cache/tilt-2.0.1.gem
vendored
BIN
hello-sinatra/bundle/ruby/2.2.0/cache/tilt-2.0.1.gem
vendored
Binary file not shown.
@@ -1,18 +0,0 @@
|
||||
Copyright (c) 2007-2015 Christian Neukirchen <purl.org/net/chneukirchen>
|
||||
|
||||
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 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.
|
||||
@@ -1,355 +0,0 @@
|
||||
Fri Jun 19 07:14:50 2015 Matthew Draper <matthew@trebex.net>
|
||||
|
||||
* Work around a Rails incompatibility in our private API
|
||||
|
||||
Fri Jun 12 11:37:41 2015 Aaron Patterson <tenderlove@ruby-lang.org>
|
||||
|
||||
* Prevent extremely deep parameters from being parsed. CVE-2015-3225
|
||||
|
||||
### December 18th, Thirty sixth public release 1.6.0
|
||||
|
||||
### February 7th, Thirty fifth public release 1.5.2
|
||||
- Fix CVE-2013-0263, timing attack against Rack::Session::Cookie
|
||||
- Fix CVE-2013-0262, symlink path traversal in Rack::File
|
||||
- Add various methods to Session for enhanced Rails compatibility
|
||||
- Request#trusted_proxy? now only matches whole stirngs
|
||||
- Add JSON cookie coder, to be default in Rack 1.6+ due to security concerns
|
||||
- URLMap host matching in environments that don't set the Host header fixed
|
||||
- Fix a race condition that could result in overwritten pidfiles
|
||||
- Various documentation additions
|
||||
|
||||
### February 7th, Thirty fifth public release 1.4.5
|
||||
- Fix CVE-2013-0263, timing attack against Rack::Session::Cookie
|
||||
- Fix CVE-2013-0262, symlink path traversal in Rack::File
|
||||
|
||||
### February 7th, Thirty fifth public release 1.1.6, 1.2.8, 1.3.10
|
||||
- Fix CVE-2013-0263, timing attack against Rack::Session::Cookie
|
||||
|
||||
### January 28th, 2013: Thirty fourth public release 1.5.1
|
||||
- Rack::Lint check_hijack now conforms to other parts of SPEC
|
||||
- Added hash-like methods to Abstract::ID::SessionHash for compatibility
|
||||
- Various documentation corrections
|
||||
|
||||
### January 21st, 2013: Thirty third public release 1.5.0
|
||||
- Introduced hijack SPEC, for before-response and after-response hijacking
|
||||
- SessionHash is no longer a Hash subclass
|
||||
- Rack::File cache_control parameter is removed, in place of headers options
|
||||
- Rack::Auth::AbstractRequest#scheme now yields strings, not symbols
|
||||
- Rack::Utils cookie functions now format expires in RFC 2822 format
|
||||
- Rack::File now has a default mime type
|
||||
- rackup -b 'run Rack::File.new(".")', option provides command line configs
|
||||
- Rack::Deflater will no longer double encode bodies
|
||||
- Rack::Mime#match? provides convenience for Accept header matching
|
||||
- Rack::Utils#q_values provides splitting for Accept headers
|
||||
- Rack::Utils#best_q_match provides a helper for Accept headers
|
||||
- Rack::Handler.pick provides convenience for finding available servers
|
||||
- Puma added to the list of default servers (preferred over Webrick)
|
||||
- Various middleware now correctly close body when replacing it
|
||||
- Rack::Request#params is no longer persistent with only GET params
|
||||
- Rack::Request#update_param and #delete_param provide persistent operations
|
||||
- Rack::Request#trusted_proxy? now returns true for local unix sockets
|
||||
- Rack::Response no longer forces Content-Types
|
||||
- Rack::Sendfile provides local mapping configuration options
|
||||
- Rack::Utils#rfc2109 provides old netscape style time output
|
||||
- Updated HTTP status codes
|
||||
- Ruby 1.8.6 likely no longer passes tests, and is no longer fully supported
|
||||
|
||||
### January 13th, 2013: Thirty second public release 1.4.4, 1.3.9, 1.2.7, 1.1.5
|
||||
- [SEC] Rack::Auth::AbstractRequest no longer symbolizes arbitrary strings
|
||||
- Fixed erroneous test case in the 1.3.x series
|
||||
|
||||
### January 7th, 2013: Thirty first public release 1.4.3
|
||||
- Security: Prevent unbounded reads in large multipart boundaries
|
||||
|
||||
### January 7th, 2013: Thirtieth public release 1.3.8
|
||||
- Security: Prevent unbounded reads in large multipart boundaries
|
||||
|
||||
### January 6th, 2013: Twenty ninth public release 1.4.2
|
||||
- Add warnings when users do not provide a session secret
|
||||
- Fix parsing performance for unquoted filenames
|
||||
- Updated URI backports
|
||||
- Fix URI backport version matching, and silence constant warnings
|
||||
- Correct parameter parsing with empty values
|
||||
- Correct rackup '-I' flag, to allow multiple uses
|
||||
- Correct rackup pidfile handling
|
||||
- Report rackup line numbers correctly
|
||||
- Fix request loops caused by non-stale nonces with time limits
|
||||
- Fix reloader on Windows
|
||||
- Prevent infinite recursions from Response#to_ary
|
||||
- Various middleware better conforms to the body close specification
|
||||
- Updated language for the body close specification
|
||||
- Additional notes regarding ECMA escape compatibility issues
|
||||
- Fix the parsing of multiple ranges in range headers
|
||||
- Prevent errors from empty parameter keys
|
||||
- Added PATCH verb to Rack::Request
|
||||
- Various documentation updates
|
||||
- Fix session merge semantics (fixes rack-test)
|
||||
- Rack::Static :index can now handle multiple directories
|
||||
- All tests now utilize Rack::Lint (special thanks to Lars Gierth)
|
||||
- Rack::File cache_control parameter is now deprecated, and removed by 1.5
|
||||
- Correct Rack::Directory script name escaping
|
||||
- Rack::Static supports header rules for sophisticated configurations
|
||||
- Multipart parsing now works without a Content-Length header
|
||||
- New logos courtesy of Zachary Scott!
|
||||
- Rack::BodyProxy now explicitly defines #each, useful for C extensions
|
||||
- Cookies that are not URI escaped no longer cause exceptions
|
||||
|
||||
### January 6th, 2013: Twenty eighth public release 1.3.7
|
||||
- Add warnings when users do not provide a session secret
|
||||
- Fix parsing performance for unquoted filenames
|
||||
- Updated URI backports
|
||||
- Fix URI backport version matching, and silence constant warnings
|
||||
- Correct parameter parsing with empty values
|
||||
- Correct rackup '-I' flag, to allow multiple uses
|
||||
- Correct rackup pidfile handling
|
||||
- Report rackup line numbers correctly
|
||||
- Fix request loops caused by non-stale nonces with time limits
|
||||
- Fix reloader on Windows
|
||||
- Prevent infinite recursions from Response#to_ary
|
||||
- Various middleware better conforms to the body close specification
|
||||
- Updated language for the body close specification
|
||||
- Additional notes regarding ECMA escape compatibility issues
|
||||
- Fix the parsing of multiple ranges in range headers
|
||||
|
||||
### January 6th, 2013: Twenty seventh public release 1.2.6
|
||||
- Add warnings when users do not provide a session secret
|
||||
- Fix parsing performance for unquoted filenames
|
||||
|
||||
### January 6th, 2013: Twenty sixth public release 1.1.4
|
||||
- Add warnings when users do not provide a session secret
|
||||
|
||||
### January 22nd, 2012: Twenty fifth public release 1.4.1
|
||||
- Alter the keyspace limit calculations to reduce issues with nested params
|
||||
- Add a workaround for multipart parsing where files contain unescaped "%"
|
||||
- Added Rack::Response::Helpers#method_not_allowed? (code 405)
|
||||
- Rack::File now returns 404 for illegal directory traversals
|
||||
- Rack::File now returns 405 for illegal methods (non HEAD/GET)
|
||||
- Rack::Cascade now catches 405 by default, as well as 404
|
||||
- Cookies missing '--' no longer cause an exception to be raised
|
||||
- Various style changes and documentation spelling errors
|
||||
- Rack::BodyProxy always ensures to execute its block
|
||||
- Additional test coverage around cookies and secrets
|
||||
- Rack::Session::Cookie can now be supplied either secret or old_secret
|
||||
- Tests are no longer dependent on set order
|
||||
- Rack::Static no longer defaults to serving index files
|
||||
- Rack.release was fixed
|
||||
|
||||
### December 28th, 2011: Twenty fourth public release 1.4.0
|
||||
- Ruby 1.8.6 support has officially been dropped. Not all tests pass.
|
||||
- Raise sane error messages for broken config.ru
|
||||
- Allow combining run and map in a config.ru
|
||||
- Rack::ContentType will not set Content-Type for responses without a body
|
||||
- Status code 205 does not send a response body
|
||||
- Rack::Response::Helpers will not rely on instance variables
|
||||
- Rack::Utils.build_query no longer outputs '=' for nil query values
|
||||
- Various mime types added
|
||||
- Rack::MockRequest now supports HEAD
|
||||
- Rack::Directory now supports files that contain RFC3986 reserved chars
|
||||
- Rack::File now only supports GET and HEAD requests
|
||||
- Rack::Server#start now passes the block to Rack::Handler::<h>#run
|
||||
- Rack::Static now supports an index option
|
||||
- Added the Teapot status code
|
||||
- rackup now defaults to Thin instead of Mongrel (if installed)
|
||||
- Support added for HTTP_X_FORWARDED_SCHEME
|
||||
- Numerous bug fixes, including many fixes for new and alternate rubies
|
||||
|
||||
### December 28th, 2011: Twenty first public release: 1.1.3.
|
||||
- Security fix. http://www.ocert.org/advisories/ocert-2011-003.html
|
||||
Further information here: http://jruby.org/2011/12/27/jruby-1-6-5-1
|
||||
|
||||
### October 17, 2011: Twentieth public release 1.3.5
|
||||
- Fix annoying warnings caused by the backport in 1.3.4
|
||||
|
||||
### October 1, 2011: Nineteenth public release 1.3.4
|
||||
- Backport security fix from 1.9.3, also fixes some roundtrip issues in URI
|
||||
- Small documentation update
|
||||
- Fix an issue where BodyProxy could cause an infinite recursion
|
||||
- Add some supporting files for travis-ci
|
||||
|
||||
### September 16, 2011: Eighteenth public release 1.2.4
|
||||
- Fix a bug with MRI regex engine to prevent XSS by malformed unicode
|
||||
|
||||
### September 16, 2011: Seventeenth public release 1.3.3
|
||||
- Fix bug with broken query parameters in Rack::ShowExceptions
|
||||
- Rack::Request#cookies no longer swallows exceptions on broken input
|
||||
- Prevents XSS attacks enabled by bug in Ruby 1.8's regexp engine
|
||||
- Rack::ConditionalGet handles broken If-Modified-Since helpers
|
||||
|
||||
### July 16, 2011: Sixteenth public release 1.3.2
|
||||
- Fix for Rails and rack-test, Rack::Utils#escape calls to_s
|
||||
|
||||
### July 13, 2011: Fifteenth public release 1.3.1
|
||||
- Fix 1.9.1 support
|
||||
- Fix JRuby support
|
||||
- Properly handle $KCODE in Rack::Utils.escape
|
||||
- Make method_missing/respond_to behavior consistent for Rack::Lock,
|
||||
Rack::Auth::Digest::Request and Rack::Multipart::UploadedFile
|
||||
- Reenable passing rack.session to session middleware
|
||||
- Rack::CommonLogger handles streaming responses correctly
|
||||
- Rack::MockResponse calls close on the body object
|
||||
- Fix a DOS vector from MRI stdlib backport
|
||||
|
||||
### May 22nd, 2011: Fourteenth public release 1.2.3
|
||||
- Pulled in relevant bug fixes from 1.3
|
||||
- Fixed 1.8.6 support
|
||||
|
||||
### May 22nd, 2011: Thirteenth public release 1.3.0
|
||||
- Various performance optimizations
|
||||
- Various multipart fixes
|
||||
- Various multipart refactors
|
||||
- Infinite loop fix for multipart
|
||||
- Test coverage for Rack::Server returns
|
||||
- Allow files with '..', but not path components that are '..'
|
||||
- rackup accepts handler-specific options on the command line
|
||||
- Request#params no longer merges POST into GET (but returns the same)
|
||||
- Use URI.encode_www_form_component instead. Use core methods for escaping.
|
||||
- Allow multi-line comments in the config file
|
||||
- Bug L#94 reported by Nikolai Lugovoi, query parameter unescaping.
|
||||
- Rack::Response now deletes Content-Length when appropriate
|
||||
- Rack::Deflater now supports streaming
|
||||
- Improved Rack::Handler loading and searching
|
||||
- Support for the PATCH verb
|
||||
- env['rack.session.options'] now contains session options
|
||||
- Cookies respect renew
|
||||
- Session middleware uses SecureRandom.hex
|
||||
|
||||
### March 13th, 2011: Twelfth public release 1.2.2/1.1.2.
|
||||
- Security fix in Rack::Auth::Digest::MD5: when authenticator
|
||||
returned nil, permission was granted on empty password.
|
||||
|
||||
### June 15th, 2010: Eleventh public release 1.2.1.
|
||||
- Make CGI handler rewindable
|
||||
- Rename spec/ to test/ to not conflict with SPEC on lesser
|
||||
operating systems
|
||||
|
||||
### June 13th, 2010: Tenth public release 1.2.0.
|
||||
- Removed Camping adapter: Camping 2.0 supports Rack as-is
|
||||
- Removed parsing of quoted values
|
||||
- Add Request.trace? and Request.options?
|
||||
- Add mime-type for .webm and .htc
|
||||
- Fix HTTP_X_FORWARDED_FOR
|
||||
- Various multipart fixes
|
||||
- Switch test suite to bacon
|
||||
|
||||
### January 3rd, 2010: Ninth public release 1.1.0.
|
||||
- Moved Auth::OpenID to rack-contrib.
|
||||
- SPEC change that relaxes Lint slightly to allow subclasses of the
|
||||
required types
|
||||
- SPEC change to document rack.input binary mode in greator detail
|
||||
- SPEC define optional rack.logger specification
|
||||
- File servers support X-Cascade header
|
||||
- Imported Config middleware
|
||||
- Imported ETag middleware
|
||||
- Imported Runtime middleware
|
||||
- Imported Sendfile middleware
|
||||
- New Logger and NullLogger middlewares
|
||||
- Added mime type for .ogv and .manifest.
|
||||
- Don't squeeze PATH_INFO slashes
|
||||
- Use Content-Type to determine POST params parsing
|
||||
- Update Rack::Utils::HTTP_STATUS_CODES hash
|
||||
- Add status code lookup utility
|
||||
- Response should call #to_i on the status
|
||||
- Add Request#user_agent
|
||||
- Request#host knows about forwared host
|
||||
- Return an empty string for Request#host if HTTP_HOST and
|
||||
SERVER_NAME are both missing
|
||||
- Allow MockRequest to accept hash params
|
||||
- Optimizations to HeaderHash
|
||||
- Refactored rackup into Rack::Server
|
||||
- Added Utils.build_nested_query to complement Utils.parse_nested_query
|
||||
- Added Utils::Multipart.build_multipart to complement
|
||||
Utils::Multipart.parse_multipart
|
||||
- Extracted set and delete cookie helpers into Utils so they can be
|
||||
used outside Response
|
||||
- Extract parse_query and parse_multipart in Request so subclasses
|
||||
can change their behavior
|
||||
- Enforce binary encoding in RewindableInput
|
||||
- Set correct external_encoding for handlers that don't use RewindableInput
|
||||
|
||||
### October 18th, 2009: Eighth public release 1.0.1.
|
||||
- Bump remainder of rack.versions.
|
||||
- Support the pure Ruby FCGI implementation.
|
||||
- Fix for form names containing "=": split first then unescape components
|
||||
- Fixes the handling of the filename parameter with semicolons in names.
|
||||
- Add anchor to nested params parsing regexp to prevent stack overflows
|
||||
- Use more compatible gzip write api instead of "<<".
|
||||
- Make sure that Reloader doesn't break when executed via ruby -e
|
||||
- Make sure WEBrick respects the :Host option
|
||||
- Many Ruby 1.9 fixes.
|
||||
|
||||
### April 25th, 2009: Seventh public release 1.0.0.
|
||||
- SPEC change: Rack::VERSION has been pushed to [1,0].
|
||||
- SPEC change: header values must be Strings now, split on "\n".
|
||||
- SPEC change: Content-Length can be missing, in this case chunked transfer
|
||||
encoding is used.
|
||||
- SPEC change: rack.input must be rewindable and support reading into
|
||||
a buffer, wrap with Rack::RewindableInput if it isn't.
|
||||
- SPEC change: rack.session is now specified.
|
||||
- SPEC change: Bodies can now additionally respond to #to_path with
|
||||
a filename to be served.
|
||||
- NOTE: String bodies break in 1.9, use an Array consisting of a
|
||||
single String instead.
|
||||
- New middleware Rack::Lock.
|
||||
- New middleware Rack::ContentType.
|
||||
- Rack::Reloader has been rewritten.
|
||||
- Major update to Rack::Auth::OpenID.
|
||||
- Support for nested parameter parsing in Rack::Response.
|
||||
- Support for redirects in Rack::Response.
|
||||
- HttpOnly cookie support in Rack::Response.
|
||||
- The Rakefile has been rewritten.
|
||||
- Many bugfixes and small improvements.
|
||||
|
||||
### January 9th, 2009: Sixth public release 0.9.1.
|
||||
- Fix directory traversal exploits in Rack::File and Rack::Directory.
|
||||
|
||||
### January 6th, 2009: Fifth public release 0.9.
|
||||
- Rack is now managed by the Rack Core Team.
|
||||
- Rack::Lint is stricter and follows the HTTP RFCs more closely.
|
||||
- Added ConditionalGet middleware.
|
||||
- Added ContentLength middleware.
|
||||
- Added Deflater middleware.
|
||||
- Added Head middleware.
|
||||
- Added MethodOverride middleware.
|
||||
- Rack::Mime now provides popular MIME-types and their extension.
|
||||
- Mongrel Header now streams.
|
||||
- Added Thin handler.
|
||||
- Official support for swiftiplied Mongrel.
|
||||
- Secure cookies.
|
||||
- Made HeaderHash case-preserving.
|
||||
- Many bugfixes and small improvements.
|
||||
|
||||
### August 21st, 2008: Fourth public release 0.4.
|
||||
- New middleware, Rack::Deflater, by Christoffer Sawicki.
|
||||
- OpenID authentication now needs ruby-openid 2.
|
||||
- New Memcache sessions, by blink.
|
||||
- Explicit EventedMongrel handler, by Joshua Peek <josh@joshpeek.com>
|
||||
- Rack::Reloader is not loaded in rackup development mode.
|
||||
- rackup can daemonize with -D.
|
||||
- Many bugfixes, especially for pool sessions, URLMap, thread safety
|
||||
and tempfile handling.
|
||||
- Improved tests.
|
||||
- Rack moved to Git.
|
||||
|
||||
### February 26th, 2008: Third public release 0.3.
|
||||
- LiteSpeed handler, by Adrian Madrid.
|
||||
- SCGI handler, by Jeremy Evans.
|
||||
- Pool sessions, by blink.
|
||||
- OpenID authentication, by blink.
|
||||
- :Port and :File options for opening FastCGI sockets, by blink.
|
||||
- Last-Modified HTTP header for Rack::File, by blink.
|
||||
- Rack::Builder#use now accepts blocks, by Corey Jewett.
|
||||
(See example/protectedlobster.ru)
|
||||
- HTTP status 201 can contain a Content-Type and a body now.
|
||||
- Many bugfixes, especially related to Cookie handling.
|
||||
|
||||
### May 16th, 2007: Second public release 0.2.
|
||||
- HTTP Basic authentication.
|
||||
- Cookie Sessions.
|
||||
- Static file handler.
|
||||
- Improved Rack::Request.
|
||||
- Improved Rack::Response.
|
||||
- Added Rack::ShowStatus, for better default error messages.
|
||||
- Bug fixes in the Camping adapter.
|
||||
- Removed Rails adapter, was too alpha.
|
||||
|
||||
### March 3rd, 2007: First public release 0.1.
|
||||
@@ -1,44 +0,0 @@
|
||||
= Known issues with Rack and ECMA-262
|
||||
|
||||
* Many users expect the escape() function defined in ECMA-262 to be compatible
|
||||
with URI. Confusion is especially strong because the documentation for the
|
||||
escape function includes a reference to the URI specifications. ECMA-262
|
||||
escape is not however a URI escape function, it is a javascript escape
|
||||
function, and is not fully compatible. Most notably, for characters outside of
|
||||
the BMP. Users should use the more correct encodeURI functions.
|
||||
|
||||
= Known issues with Rack and Web servers
|
||||
|
||||
* Lighttpd sets wrong SCRIPT_NAME and PATH_INFO if you mount your
|
||||
FastCGI app at "/". This can be fixed by using this middleware:
|
||||
|
||||
class LighttpdScriptNameFix
|
||||
def initialize(app)
|
||||
@app = app
|
||||
end
|
||||
|
||||
def call(env)
|
||||
env["PATH_INFO"] = env["SCRIPT_NAME"].to_s + env["PATH_INFO"].to_s
|
||||
env["SCRIPT_NAME"] = ""
|
||||
@app.call(env)
|
||||
end
|
||||
end
|
||||
|
||||
Of course, use this only when your app runs at "/".
|
||||
|
||||
Since lighttpd 1.4.23, you also can use the "fix-root-scriptname" flag
|
||||
in fastcgi.server.
|
||||
|
||||
= Known conflicts regarding parameter parsing
|
||||
|
||||
* Many users have differing opinions about parameter parsing. The current
|
||||
parameter parsers in Rack are based on a combination of the HTTP and CGI
|
||||
specs, and are intended to round-trip encoding and decoding. There are some
|
||||
choices that may be viewed as deficiencies, specifically:
|
||||
- Rack does not create implicit arrays for multiple instances of a parameter
|
||||
- Rack returns nil when a value is not given
|
||||
- Rack does not support multi-type keys in parameters
|
||||
These issues or choices, will not be fixed before 2.0, if at all. They are
|
||||
very major breaking changes. Users are free to write alternative parameter
|
||||
parsers, and their own Request and Response wrappers. Moreover, users are
|
||||
encouraged to do so.
|
||||
@@ -1,312 +0,0 @@
|
||||
= Rack, a modular Ruby webserver interface {<img src="https://secure.travis-ci.org/rack/rack.svg" alt="Build Status" />}[http://travis-ci.org/rack/rack] {<img src="https://gemnasium.com/rack/rack.svg" alt="Dependency Status" />}[https://gemnasium.com/rack/rack]
|
||||
|
||||
Rack provides a minimal, modular and adaptable interface for developing
|
||||
web applications in Ruby. By wrapping HTTP requests and responses in
|
||||
the simplest way possible, it unifies and distills the API for web
|
||||
servers, web frameworks, and software in between (the so-called
|
||||
middleware) into a single method call.
|
||||
|
||||
The exact details of this are described in the Rack specification,
|
||||
which all Rack applications should conform to.
|
||||
|
||||
== Supported web servers
|
||||
|
||||
The included *handlers* connect all kinds of web servers to Rack:
|
||||
* Mongrel
|
||||
* EventedMongrel
|
||||
* SwiftipliedMongrel
|
||||
* WEBrick
|
||||
* FCGI
|
||||
* CGI
|
||||
* SCGI
|
||||
* LiteSpeed
|
||||
* Thin
|
||||
|
||||
These web servers include Rack handlers in their distributions:
|
||||
* Ebb
|
||||
* Fuzed
|
||||
* Glassfish v3
|
||||
* Phusion Passenger (which is mod_rack for Apache and for nginx)
|
||||
* Puma
|
||||
* Rainbows!
|
||||
* Reel
|
||||
* Unicorn
|
||||
* unixrack
|
||||
* uWSGI
|
||||
* yahns
|
||||
* Zbatery
|
||||
|
||||
Any valid Rack app will run the same on all these handlers, without
|
||||
changing anything.
|
||||
|
||||
== Supported web frameworks
|
||||
|
||||
These frameworks include Rack adapters in their distributions:
|
||||
* Camping
|
||||
* Coset
|
||||
* Espresso
|
||||
* Halcyon
|
||||
* Mack
|
||||
* Maveric
|
||||
* Merb
|
||||
* Racktools::SimpleApplication
|
||||
* Ramaze
|
||||
* Ruby on Rails
|
||||
* Rum
|
||||
* Sinatra
|
||||
* Sin
|
||||
* Vintage
|
||||
* Waves
|
||||
* Wee
|
||||
* ... and many others.
|
||||
|
||||
== Available middleware
|
||||
|
||||
Between the server and the framework, Rack can be customized to your
|
||||
applications needs using middleware, for example:
|
||||
* Rack::URLMap, to route to multiple applications inside the same process.
|
||||
* Rack::CommonLogger, for creating Apache-style logfiles.
|
||||
* Rack::ShowException, for catching unhandled exceptions and
|
||||
presenting them in a nice and helpful way with clickable backtrace.
|
||||
* Rack::File, for serving static files.
|
||||
* ...many others!
|
||||
|
||||
All these components use the same interface, which is described in
|
||||
detail in the Rack specification. These optional components can be
|
||||
used in any way you wish.
|
||||
|
||||
== Convenience
|
||||
|
||||
If you want to develop outside of existing frameworks, implement your
|
||||
own ones, or develop middleware, Rack provides many helpers to create
|
||||
Rack applications quickly and without doing the same web stuff all
|
||||
over:
|
||||
* Rack::Request, which also provides query string parsing and
|
||||
multipart handling.
|
||||
* Rack::Response, for convenient generation of HTTP replies and
|
||||
cookie handling.
|
||||
* Rack::MockRequest and Rack::MockResponse for efficient and quick
|
||||
testing of Rack application without real HTTP round-trips.
|
||||
|
||||
== rack-contrib
|
||||
|
||||
The plethora of useful middleware created the need for a project that
|
||||
collects fresh Rack middleware. rack-contrib includes a variety of
|
||||
add-on components for Rack and it is easy to contribute new modules.
|
||||
|
||||
* https://github.com/rack/rack-contrib
|
||||
|
||||
== rackup
|
||||
|
||||
rackup is a useful tool for running Rack applications, which uses the
|
||||
Rack::Builder DSL to configure middleware and build up applications
|
||||
easily.
|
||||
|
||||
rackup automatically figures out the environment it is run in, and
|
||||
runs your application as FastCGI, CGI, or standalone with Mongrel or
|
||||
WEBrick---all from the same configuration.
|
||||
|
||||
== Quick start
|
||||
|
||||
Try the lobster!
|
||||
|
||||
Either with the embedded WEBrick starter:
|
||||
|
||||
ruby -Ilib lib/rack/lobster.rb
|
||||
|
||||
Or with rackup:
|
||||
|
||||
bin/rackup -Ilib example/lobster.ru
|
||||
|
||||
By default, the lobster is found at http://localhost:9292.
|
||||
|
||||
== Installing with RubyGems
|
||||
|
||||
A Gem of Rack is available at rubygems.org. You can install it with:
|
||||
|
||||
gem install rack
|
||||
|
||||
I also provide a local mirror of the gems (and development snapshots)
|
||||
at my site:
|
||||
|
||||
gem install rack --source http://chneukirchen.org/releases/gems/
|
||||
|
||||
== Running the tests
|
||||
|
||||
Testing Rack requires the bacon testing framework:
|
||||
|
||||
bundle install --without extra # to be able to run the fast tests
|
||||
|
||||
Or:
|
||||
|
||||
bundle install # this assumes that you have installed native extensions!
|
||||
|
||||
There are two rake-based test tasks:
|
||||
|
||||
rake test tests all the fast tests (no Handlers or Adapters)
|
||||
rake fulltest runs all the tests
|
||||
|
||||
The fast testsuite has no dependencies outside of the core Ruby
|
||||
installation and bacon.
|
||||
|
||||
To run the test suite completely, you need:
|
||||
|
||||
* fcgi
|
||||
* memcache-client
|
||||
* mongrel
|
||||
* thin
|
||||
|
||||
The full set of tests test FCGI access with lighttpd (on port
|
||||
9203) so you will need lighttpd installed as well as the FCGI
|
||||
libraries and the fcgi gem:
|
||||
|
||||
Download and install lighttpd:
|
||||
|
||||
http://www.lighttpd.net/download
|
||||
|
||||
Installing the FCGI libraries:
|
||||
|
||||
curl -O http://www.fastcgi.com/dist/fcgi-2.4.0.tar.gz
|
||||
tar xzvf fcgi-2.4.0.tar.gz
|
||||
cd fcgi-2.4.0
|
||||
./configure --prefix=/usr/local
|
||||
make
|
||||
sudo make install
|
||||
cd ..
|
||||
|
||||
Installing the Ruby fcgi gem:
|
||||
|
||||
gem install fcgi
|
||||
|
||||
Furthermore, to test Memcache sessions, you need memcached (will be
|
||||
run on port 11211) and memcache-client installed.
|
||||
|
||||
== Configuration
|
||||
|
||||
Several parameters can be modified on Rack::Utils to configure Rack behaviour.
|
||||
|
||||
e.g:
|
||||
|
||||
Rack::Utils.key_space_limit = 128
|
||||
|
||||
=== key_space_limit
|
||||
|
||||
The default number of bytes to allow a single parameter key to take up.
|
||||
This helps prevent a rogue client from flooding a Request.
|
||||
|
||||
Default to 65536 characters (4 kiB in worst case).
|
||||
|
||||
=== multipart_part_limit
|
||||
|
||||
The maximum number of parts a request can contain.
|
||||
Accepting too many part can lead to the server running out of file handles.
|
||||
|
||||
The default is 128, which means that a single request can't upload more than 128 files at once.
|
||||
|
||||
Set to 0 for no limit.
|
||||
|
||||
Can also be set via the RACK_MULTIPART_PART_LIMIT environment variable.
|
||||
|
||||
== History
|
||||
|
||||
See <https://github.com/rack/HISTORY.md>.
|
||||
|
||||
== Contact
|
||||
|
||||
Please post bugs, suggestions and patches to
|
||||
the bug tracker at <https://github.com/rack/rack/issues>.
|
||||
|
||||
Please post security related bugs and suggestions to the core team at
|
||||
<https://groups.google.com/group/rack-core> or rack-core@googlegroups.com. This
|
||||
list is not public. Due to wide usage of the library, it is strongly preferred
|
||||
that we manage timing in order to provide viable patches at the time of
|
||||
disclosure. Your assistance in this matter is greatly appreciated.
|
||||
|
||||
Mailing list archives are available at
|
||||
<https://groups.google.com/group/rack-devel>.
|
||||
|
||||
Git repository (send Git patches to the mailing list):
|
||||
* https://github.com/rack/rack
|
||||
* http://git.vuxu.org/cgi-bin/gitweb.cgi?p=rack-github.git
|
||||
|
||||
You are also welcome to join the #rack channel on irc.freenode.net.
|
||||
|
||||
== Thanks
|
||||
|
||||
The Rack Core Team, consisting of
|
||||
|
||||
* Christian Neukirchen (chneukirchen)
|
||||
* James Tucker (raggi)
|
||||
* Josh Peek (josh)
|
||||
* José Valim (josevalim)
|
||||
* Michael Fellinger (manveru)
|
||||
* Aaron Patterson (tenderlove)
|
||||
* Santiago Pastorino (spastorino)
|
||||
* Konstantin Haase (rkh)
|
||||
|
||||
and the Rack Alumnis
|
||||
|
||||
* Ryan Tomayko (rtomayko)
|
||||
* Scytrin dai Kinthra (scytrin)
|
||||
|
||||
would like to thank:
|
||||
|
||||
* Adrian Madrid, for the LiteSpeed handler.
|
||||
* Christoffer Sawicki, for the first Rails adapter and Rack::Deflater.
|
||||
* Tim Fletcher, for the HTTP authentication code.
|
||||
* Luc Heinrich for the Cookie sessions, the static file handler and bugfixes.
|
||||
* Armin Ronacher, for the logo and racktools.
|
||||
* Alex Beregszaszi, Alexander Kahn, Anil Wadghule, Aredridel, Ben
|
||||
Alpert, Dan Kubb, Daniel Roethlisberger, Matt Todd, Tom Robinson,
|
||||
Phil Hagelberg, S. Brent Faulkner, Bosko Milekic, Daniel Rodríguez
|
||||
Troitiño, Genki Takiuchi, Geoffrey Grosenbach, Julien Sanchez, Kamal
|
||||
Fariz Mahyuddin, Masayoshi Takahashi, Patrick Aljordm, Mig, Kazuhiro
|
||||
Nishiyama, Jon Bardin, Konstantin Haase, Larry Siden, Matias
|
||||
Korhonen, Sam Ruby, Simon Chiang, Tim Connor, Timur Batyrshin, and
|
||||
Zach Brock for bug fixing and other improvements.
|
||||
* Eric Wong, Hongli Lai, Jeremy Kemper for their continuous support
|
||||
and API improvements.
|
||||
* Yehuda Katz and Carl Lerche for refactoring rackup.
|
||||
* Brian Candler, for Rack::ContentType.
|
||||
* Graham Batty, for improved handler loading.
|
||||
* Stephen Bannasch, for bug reports and documentation.
|
||||
* Gary Wright, for proposing a better Rack::Response interface.
|
||||
* Jonathan Buch, for improvements regarding Rack::Response.
|
||||
* Armin Röhrl, for tracking down bugs in the Cookie generator.
|
||||
* Alexander Kellett for testing the Gem and reviewing the announcement.
|
||||
* Marcus Rückert, for help with configuring and debugging lighttpd.
|
||||
* The WSGI team for the well-done and documented work they've done and
|
||||
Rack builds up on.
|
||||
* All bug reporters and patch contributors not mentioned above.
|
||||
|
||||
== Copyright
|
||||
|
||||
Copyright (C) 2007, 2008, 2009, 2010 Christian Neukirchen <http://purl.org/net/chneukirchen>
|
||||
|
||||
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 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.
|
||||
|
||||
== Links
|
||||
|
||||
Rack:: <http://rack.github.io/>
|
||||
Official Rack repositories:: <https://github.com/rack>
|
||||
Rack Bug Tracking:: <https://github.com/rack/rack/issues>
|
||||
rack-devel mailing list:: <https://groups.google.com/group/rack-devel>
|
||||
Rack's Rubyforge project:: <http://rubyforge.org/projects/rack>
|
||||
|
||||
Christian Neukirchen:: <http://chneukirchen.org/>
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
# Rakefile for Rack. -*-ruby-*-
|
||||
|
||||
desc "Run all the tests"
|
||||
task :default => [:test]
|
||||
|
||||
desc "Install gem dependencies"
|
||||
task :deps do
|
||||
require 'rubygems'
|
||||
spec = Gem::Specification.load('rack.gemspec')
|
||||
spec.dependencies.each do |dep|
|
||||
reqs = dep.requirements_list
|
||||
reqs = (["-v"] * reqs.size).zip(reqs).flatten
|
||||
# Use system over sh, because we want to ignore errors!
|
||||
system Gem.ruby, "-S", "gem", "install", '--conservative', dep.name, *reqs
|
||||
end
|
||||
end
|
||||
|
||||
desc "Make an archive as .tar.gz"
|
||||
task :dist => %w[chmod ChangeLog SPEC rdoc] do
|
||||
sh "git archive --format=tar --prefix=#{release}/ HEAD^{tree} >#{release}.tar"
|
||||
sh "pax -waf #{release}.tar -s ':^:#{release}/:' SPEC ChangeLog doc rack.gemspec"
|
||||
sh "gzip -f -9 #{release}.tar"
|
||||
end
|
||||
|
||||
desc "Make an official release"
|
||||
task :officialrelease do
|
||||
puts "Official build for #{release}..."
|
||||
sh "rm -rf stage"
|
||||
sh "git clone --shared . stage"
|
||||
sh "cd stage && rake officialrelease_really"
|
||||
sh "mv stage/#{release}.tar.gz stage/#{release}.gem ."
|
||||
end
|
||||
|
||||
task :officialrelease_really => %w[SPEC dist gem] do
|
||||
sh "shasum #{release}.tar.gz #{release}.gem"
|
||||
end
|
||||
|
||||
def release
|
||||
"rack-#{File.read("rack.gemspec")[/s.version *= *"(.*?)"/, 1]}"
|
||||
end
|
||||
|
||||
desc "Make binaries executable"
|
||||
task :chmod do
|
||||
Dir["bin/*"].each { |binary| File.chmod(0755, binary) }
|
||||
Dir["test/cgi/test*"].each { |binary| File.chmod(0755, binary) }
|
||||
end
|
||||
|
||||
desc "Generate a ChangeLog"
|
||||
task :changelog => %w[ChangeLog]
|
||||
|
||||
file '.git/index'
|
||||
file "ChangeLog" => '.git/index' do
|
||||
File.open("ChangeLog", "w") { |out|
|
||||
log = `git log -z`
|
||||
log.force_encoding(Encoding::BINARY) if log.respond_to?(:force_encoding)
|
||||
log.split("\0").map { |chunk|
|
||||
author = chunk[/Author: (.*)/, 1].strip
|
||||
date = chunk[/Date: (.*)/, 1].strip
|
||||
desc, detail = $'.strip.split("\n", 2)
|
||||
detail ||= ""
|
||||
detail = detail.gsub(/.*darcs-hash:.*/, '')
|
||||
detail.rstrip!
|
||||
out.puts "#{date} #{author}"
|
||||
out.puts " * #{desc.strip}"
|
||||
out.puts detail unless detail.empty?
|
||||
out.puts
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
file 'lib/rack/lint.rb'
|
||||
desc "Generate Rack Specification"
|
||||
file "SPEC" => 'lib/rack/lint.rb' do
|
||||
File.open("SPEC", "wb") { |file|
|
||||
IO.foreach("lib/rack/lint.rb") { |line|
|
||||
if line =~ /## (.*)/
|
||||
file.puts $1
|
||||
end
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
desc "Run all the fast + platform agnostic tests"
|
||||
task :test => 'SPEC' do
|
||||
opts = ENV['TEST'] || '-a'
|
||||
specopts = ENV['TESTOPTS'] ||
|
||||
"-q -t '^(?!Rack::Adapter|Rack::Session::Memcache|Rack::Server|Rack::Handler)'"
|
||||
|
||||
sh "bacon -w -I./lib:./test #{opts} #{specopts}"
|
||||
end
|
||||
|
||||
desc "Run all the tests we run on CI"
|
||||
task :ci => :fulltest
|
||||
|
||||
desc "Run all the tests"
|
||||
task :fulltest => %w[SPEC chmod] do
|
||||
opts = ENV['TEST'] || '-a'
|
||||
specopts = ENV['TESTOPTS'] || '-q'
|
||||
sh "bacon -r./test/gemloader -I./lib:./test -w #{opts} #{specopts}"
|
||||
end
|
||||
|
||||
task :gem => ["SPEC"] do
|
||||
sh "gem build rack.gemspec"
|
||||
end
|
||||
|
||||
task :doc => :rdoc
|
||||
desc "Generate RDoc documentation"
|
||||
task :rdoc => %w[ChangeLog SPEC] do
|
||||
sh(*%w{rdoc --line-numbers --main README.rdoc
|
||||
--title 'Rack\ Documentation' --charset utf-8 -U -o doc} +
|
||||
%w{README.rdoc KNOWN-ISSUES SPEC ChangeLog} +
|
||||
`git ls-files lib/\*\*/\*.rb`.strip.split)
|
||||
cp "contrib/rdoc.css", "doc/rdoc.css"
|
||||
end
|
||||
|
||||
task :pushdoc => %w[rdoc] do
|
||||
sh "rsync -avz doc/ rack.rubyforge.org:/var/www/gforge-projects/rack/doc/"
|
||||
end
|
||||
|
||||
task :pushsite => %w[pushdoc] do
|
||||
sh "cd site && git gc"
|
||||
sh "rsync -avz site/ rack.rubyforge.org:/var/www/gforge-projects/rack/"
|
||||
sh "cd site && git push"
|
||||
end
|
||||
@@ -1,264 +0,0 @@
|
||||
This specification aims to formalize the Rack protocol. You
|
||||
can (and should) use Rack::Lint to enforce it.
|
||||
When you develop middleware, be sure to add a Lint before and
|
||||
after to catch all mistakes.
|
||||
= Rack applications
|
||||
A Rack application is a Ruby object (not a class) that
|
||||
responds to +call+.
|
||||
It takes exactly one argument, the *environment*
|
||||
and returns an Array of exactly three values:
|
||||
The *status*,
|
||||
the *headers*,
|
||||
and the *body*.
|
||||
== The Environment
|
||||
The environment must be an instance of Hash that includes
|
||||
CGI-like headers. The application is free to modify the
|
||||
environment.
|
||||
The environment is required to include these variables
|
||||
(adopted from PEP333), except when they'd be empty, but see
|
||||
below.
|
||||
<tt>REQUEST_METHOD</tt>:: The HTTP request method, such as
|
||||
"GET" or "POST". This cannot ever
|
||||
be an empty string, and so is
|
||||
always required.
|
||||
<tt>SCRIPT_NAME</tt>:: The initial portion of the request
|
||||
URL's "path" that corresponds to the
|
||||
application object, so that the
|
||||
application knows its virtual
|
||||
"location". This may be an empty
|
||||
string, if the application corresponds
|
||||
to the "root" of the server.
|
||||
<tt>PATH_INFO</tt>:: The remainder of the request URL's
|
||||
"path", designating the virtual
|
||||
"location" of the request's target
|
||||
within the application. This may be an
|
||||
empty string, if the request URL targets
|
||||
the application root and does not have a
|
||||
trailing slash. This value may be
|
||||
percent-encoded when I originating from
|
||||
a URL.
|
||||
<tt>QUERY_STRING</tt>:: The portion of the request URL that
|
||||
follows the <tt>?</tt>, if any. May be
|
||||
empty, but is always required!
|
||||
<tt>SERVER_NAME</tt>, <tt>SERVER_PORT</tt>::
|
||||
When combined with <tt>SCRIPT_NAME</tt> and
|
||||
<tt>PATH_INFO</tt>, these variables can be
|
||||
used to complete the URL. Note, however,
|
||||
that <tt>HTTP_HOST</tt>, if present,
|
||||
should be used in preference to
|
||||
<tt>SERVER_NAME</tt> for reconstructing
|
||||
the request URL.
|
||||
<tt>SERVER_NAME</tt> and <tt>SERVER_PORT</tt>
|
||||
can never be empty strings, and so
|
||||
are always required.
|
||||
<tt>HTTP_</tt> Variables:: Variables corresponding to the
|
||||
client-supplied HTTP request
|
||||
headers (i.e., variables whose
|
||||
names begin with <tt>HTTP_</tt>). The
|
||||
presence or absence of these
|
||||
variables should correspond with
|
||||
the presence or absence of the
|
||||
appropriate HTTP header in the
|
||||
request. See
|
||||
<a href="https://tools.ietf.org/html/rfc3875#section-4.1.18">
|
||||
RFC3875 section 4.1.18</a> for
|
||||
specific behavior.
|
||||
In addition to this, the Rack environment must include these
|
||||
Rack-specific variables:
|
||||
<tt>rack.version</tt>:: The Array representing this version of Rack
|
||||
See Rack::VERSION, that corresponds to
|
||||
the version of this SPEC.
|
||||
<tt>rack.url_scheme</tt>:: +http+ or +https+, depending on the
|
||||
request URL.
|
||||
<tt>rack.input</tt>:: See below, the input stream.
|
||||
<tt>rack.errors</tt>:: See below, the error stream.
|
||||
<tt>rack.multithread</tt>:: true if the application object may be
|
||||
simultaneously invoked by another thread
|
||||
in the same process, false otherwise.
|
||||
<tt>rack.multiprocess</tt>:: true if an equivalent application object
|
||||
may be simultaneously invoked by another
|
||||
process, false otherwise.
|
||||
<tt>rack.run_once</tt>:: true if the server expects
|
||||
(but does not guarantee!) that the
|
||||
application will only be invoked this one
|
||||
time during the life of its containing
|
||||
process. Normally, this will only be true
|
||||
for a server based on CGI
|
||||
(or something similar).
|
||||
<tt>rack.hijack?</tt>:: present and true if the server supports
|
||||
connection hijacking. See below, hijacking.
|
||||
<tt>rack.hijack</tt>:: an object responding to #call that must be
|
||||
called at least once before using
|
||||
rack.hijack_io.
|
||||
It is recommended #call return rack.hijack_io
|
||||
as well as setting it in env if necessary.
|
||||
<tt>rack.hijack_io</tt>:: if rack.hijack? is true, and rack.hijack
|
||||
has received #call, this will contain
|
||||
an object resembling an IO. See hijacking.
|
||||
Additional environment specifications have approved to
|
||||
standardized middleware APIs. None of these are required to
|
||||
be implemented by the server.
|
||||
<tt>rack.session</tt>:: A hash like interface for storing
|
||||
request session data.
|
||||
The store must implement:
|
||||
store(key, value) (aliased as []=);
|
||||
fetch(key, default = nil) (aliased as []);
|
||||
delete(key);
|
||||
clear;
|
||||
<tt>rack.logger</tt>:: A common object interface for logging messages.
|
||||
The object must implement:
|
||||
info(message, &block)
|
||||
debug(message, &block)
|
||||
warn(message, &block)
|
||||
error(message, &block)
|
||||
fatal(message, &block)
|
||||
<tt>rack.multipart.buffer_size</tt>:: An Integer hint to the multipart parser as to what chunk size to use for reads and writes.
|
||||
<tt>rack.multipart.tempfile_factory</tt>:: An object responding to #call with two arguments, the filename and content_type given for the multipart form field, and returning an IO-like object that responds to #<< and optionally #rewind. This factory will be used to instantiate the tempfile for each multipart form file upload field, rather than the default class of Tempfile.
|
||||
The server or the application can store their own data in the
|
||||
environment, too. The keys must contain at least one dot,
|
||||
and should be prefixed uniquely. The prefix <tt>rack.</tt>
|
||||
is reserved for use with the Rack core distribution and other
|
||||
accepted specifications and must not be used otherwise.
|
||||
The environment must not contain the keys
|
||||
<tt>HTTP_CONTENT_TYPE</tt> or <tt>HTTP_CONTENT_LENGTH</tt>
|
||||
(use the versions without <tt>HTTP_</tt>).
|
||||
The CGI keys (named without a period) must have String values.
|
||||
There are the following restrictions:
|
||||
* <tt>rack.version</tt> must be an array of Integers.
|
||||
* <tt>rack.url_scheme</tt> must either be +http+ or +https+.
|
||||
* There must be a valid input stream in <tt>rack.input</tt>.
|
||||
* There must be a valid error stream in <tt>rack.errors</tt>.
|
||||
* There may be a valid hijack stream in <tt>rack.hijack_io</tt>
|
||||
* The <tt>REQUEST_METHOD</tt> must be a valid token.
|
||||
* The <tt>SCRIPT_NAME</tt>, if non-empty, must start with <tt>/</tt>
|
||||
* The <tt>PATH_INFO</tt>, if non-empty, must start with <tt>/</tt>
|
||||
* The <tt>CONTENT_LENGTH</tt>, if given, must consist of digits only.
|
||||
* One of <tt>SCRIPT_NAME</tt> or <tt>PATH_INFO</tt> must be
|
||||
set. <tt>PATH_INFO</tt> should be <tt>/</tt> if
|
||||
<tt>SCRIPT_NAME</tt> is empty.
|
||||
<tt>SCRIPT_NAME</tt> never should be <tt>/</tt>, but instead be empty.
|
||||
=== The Input Stream
|
||||
The input stream is an IO-like object which contains the raw HTTP
|
||||
POST data.
|
||||
When applicable, its external encoding must be "ASCII-8BIT" and it
|
||||
must be opened in binary mode, for Ruby 1.9 compatibility.
|
||||
The input stream must respond to +gets+, +each+, +read+ and +rewind+.
|
||||
* +gets+ must be called without arguments and return a string,
|
||||
or +nil+ on EOF.
|
||||
* +read+ behaves like IO#read.
|
||||
Its signature is <tt>read([length, [buffer]])</tt>.
|
||||
If given, +length+ must be a non-negative Integer (>= 0) or +nil+,
|
||||
and +buffer+ must be a String and may not be nil.
|
||||
If +length+ is given and not nil, then this method reads at most
|
||||
+length+ bytes from the input stream.
|
||||
If +length+ is not given or nil, then this method reads
|
||||
all data until EOF.
|
||||
When EOF is reached, this method returns nil if +length+ is given
|
||||
and not nil, or "" if +length+ is not given or is nil.
|
||||
If +buffer+ is given, then the read data will be placed
|
||||
into +buffer+ instead of a newly created String object.
|
||||
* +each+ must be called without arguments and only yield Strings.
|
||||
* +rewind+ must be called without arguments. It rewinds the input
|
||||
stream back to the beginning. It must not raise Errno::ESPIPE:
|
||||
that is, it may not be a pipe or a socket. Therefore, handler
|
||||
developers must buffer the input data into some rewindable object
|
||||
if the underlying input stream is not rewindable.
|
||||
* +close+ must never be called on the input stream.
|
||||
=== The Error Stream
|
||||
The error stream must respond to +puts+, +write+ and +flush+.
|
||||
* +puts+ must be called with a single argument that responds to +to_s+.
|
||||
* +write+ must be called with a single argument that is a String.
|
||||
* +flush+ must be called without arguments and must be called
|
||||
in order to make the error appear for sure.
|
||||
* +close+ must never be called on the error stream.
|
||||
=== Hijacking
|
||||
==== Request (before status)
|
||||
If rack.hijack? is true then rack.hijack must respond to #call.
|
||||
rack.hijack must return the io that will also be assigned (or is
|
||||
already present, in rack.hijack_io.
|
||||
rack.hijack_io must respond to:
|
||||
<tt>read, write, read_nonblock, write_nonblock, flush, close,
|
||||
close_read, close_write, closed?</tt>
|
||||
The semantics of these IO methods must be a best effort match to
|
||||
those of a normal ruby IO or Socket object, using standard
|
||||
arguments and raising standard exceptions. Servers are encouraged
|
||||
to simply pass on real IO objects, although it is recognized that
|
||||
this approach is not directly compatible with SPDY and HTTP 2.0.
|
||||
IO provided in rack.hijack_io should preference the
|
||||
IO::WaitReadable and IO::WaitWritable APIs wherever supported.
|
||||
There is a deliberate lack of full specification around
|
||||
rack.hijack_io, as semantics will change from server to server.
|
||||
Users are encouraged to utilize this API with a knowledge of their
|
||||
server choice, and servers may extend the functionality of
|
||||
hijack_io to provide additional features to users. The purpose of
|
||||
rack.hijack is for Rack to "get out of the way", as such, Rack only
|
||||
provides the minimum of specification and support.
|
||||
If rack.hijack? is false, then rack.hijack should not be set.
|
||||
If rack.hijack? is false, then rack.hijack_io should not be set.
|
||||
==== Response (after headers)
|
||||
It is also possible to hijack a response after the status and headers
|
||||
have been sent.
|
||||
In order to do this, an application may set the special header
|
||||
<tt>rack.hijack</tt> to an object that responds to <tt>call</tt>
|
||||
accepting an argument that conforms to the <tt>rack.hijack_io</tt>
|
||||
protocol.
|
||||
After the headers have been sent, and this hijack callback has been
|
||||
called, the application is now responsible for the remaining lifecycle
|
||||
of the IO. The application is also responsible for maintaining HTTP
|
||||
semantics. Of specific note, in almost all cases in the current SPEC,
|
||||
applications will have wanted to specify the header Connection:close in
|
||||
HTTP/1.1, and not Connection:keep-alive, as there is no protocol for
|
||||
returning hijacked sockets to the web server. For that purpose, use the
|
||||
body streaming API instead (progressively yielding strings via each).
|
||||
Servers must ignore the <tt>body</tt> part of the response tuple when
|
||||
the <tt>rack.hijack</tt> response API is in use.
|
||||
The special response header <tt>rack.hijack</tt> must only be set
|
||||
if the request env has <tt>rack.hijack?</tt> <tt>true</tt>.
|
||||
==== Conventions
|
||||
* Middleware should not use hijack unless it is handling the whole
|
||||
response.
|
||||
* Middleware may wrap the IO object for the response pattern.
|
||||
* Middleware should not wrap the IO object for the request pattern. The
|
||||
request pattern is intended to provide the hijacker with "raw tcp".
|
||||
== The Response
|
||||
=== The Status
|
||||
This is an HTTP status. When parsed as integer (+to_i+), it must be
|
||||
greater than or equal to 100.
|
||||
=== The Headers
|
||||
The header must respond to +each+, and yield values of key and value.
|
||||
Special headers starting "rack." are for communicating with the
|
||||
server, and must not be sent back to the client.
|
||||
The header keys must be Strings.
|
||||
The header must not contain a +Status+ key.
|
||||
The header must conform to RFC7230 token specification, i.e. cannot
|
||||
contain non-printable ASCII, DQUOTE or "(),/:;<=>?@[\]{}".
|
||||
The values of the header must be Strings,
|
||||
consisting of lines (for multiple header values, e.g. multiple
|
||||
<tt>Set-Cookie</tt> values) separated by "\\n".
|
||||
The lines must not contain characters below 037.
|
||||
=== The Content-Type
|
||||
There must not be a <tt>Content-Type</tt>, when the +Status+ is 1xx,
|
||||
204, 205 or 304.
|
||||
=== The Content-Length
|
||||
There must not be a <tt>Content-Length</tt> header when the
|
||||
+Status+ is 1xx, 204, 205 or 304.
|
||||
=== The Body
|
||||
The Body must respond to +each+
|
||||
and must only yield String values.
|
||||
The Body itself should not be an instance of String, as this will
|
||||
break in Ruby 1.9.
|
||||
If the Body responds to +close+, it will be called after iteration. If
|
||||
the body is replaced by a middleware after action, the original body
|
||||
must be closed first, if it responds to close.
|
||||
If the Body responds to +to_path+, it must return a String
|
||||
identifying the location of a file whose contents are identical
|
||||
to that produced by calling +each+; this may be used by the
|
||||
server as an alternative, possibly more efficient way to
|
||||
transport the response.
|
||||
The Body commonly is an Array of Strings, the application
|
||||
instance itself, or a File-like object.
|
||||
== Thanks
|
||||
Some parts of this specification are adopted from PEP333: Python
|
||||
Web Server Gateway Interface
|
||||
v1.0 (http://www.python.org/dev/peps/pep-0333/). I'd like to thank
|
||||
everyone involved in that effort.
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/usr/bin/env ruby
|
||||
|
||||
require "rack"
|
||||
Rack::Server.start
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 23 KiB |
@@ -1,150 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="744.09448819"
|
||||
height="1052.3622047"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.48.3.1 r9886"
|
||||
sodipodi:docname="rack.svg">
|
||||
<defs
|
||||
id="defs4">
|
||||
<linearGradient
|
||||
id="linearGradient3837"
|
||||
osb:paint="solid">
|
||||
<stop
|
||||
style="stop-color:#000000;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop3839" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.98994949"
|
||||
inkscape:cx="230.49849"
|
||||
inkscape:cy="656.46253"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
showguides="false"
|
||||
inkscape:guide-bbox="true"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1056"
|
||||
inkscape:window-x="1920"
|
||||
inkscape:window-y="24"
|
||||
inkscape:window-maximized="1">
|
||||
<sodipodi:guide
|
||||
orientation="1,0"
|
||||
position="645.99255,757.10933"
|
||||
id="guide2995" />
|
||||
<sodipodi:guide
|
||||
orientation="1,0"
|
||||
position="488.40876,686.90373"
|
||||
id="guide2997" />
|
||||
<sodipodi:guide
|
||||
orientation="1,0"
|
||||
position="176.7767,748.52304"
|
||||
id="guide2999" />
|
||||
<sodipodi:guide
|
||||
orientation="1,0"
|
||||
position="355.71429,782.85714"
|
||||
id="guide3005" />
|
||||
<sodipodi:guide
|
||||
orientation="0,1"
|
||||
position="527.14286,642.85714"
|
||||
id="guide3007" />
|
||||
<sodipodi:guide
|
||||
orientation="0,1"
|
||||
position="431.42857,507.85714"
|
||||
id="guide3009" />
|
||||
<sodipodi:guide
|
||||
orientation="0,1"
|
||||
position="488.40876,783.57143"
|
||||
id="guide3011" />
|
||||
<sodipodi:guide
|
||||
orientation="0,1"
|
||||
position="505,372.85714"
|
||||
id="guide3013" />
|
||||
</sodipodi:namedview>
|
||||
<metadata
|
||||
id="metadata7">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<path
|
||||
style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:10;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans;stroke-opacity:1;stroke-miterlimit:30;stroke-dasharray:none;stroke-linecap:round;stroke-linejoin:round"
|
||||
d="m 176.28125,201.03125 0,0.625 0,395.125 0,0.375 0.34375,0.0937 312.34375,91.6875 0.625,0.1875 0,-0.65625 0.125,-419.09375 0,-0.40625 -0.40625,-0.0937 -312.4375,-67.71875 -0.59375,-0.125 z m 1,1.21875 311.4375,67.5 -0.125,418.0625 -311.3125,-91.375 0,-394.1875 z"
|
||||
id="path2985"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:10;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans;stroke-opacity:1;stroke-miterlimit:30;stroke-dasharray:none;stroke-linecap:round;stroke-linejoin:round"
|
||||
d="m 647.21875,206.59375 -0.6875,0.28125 -157.59375,62.21875 -0.3125,0.125 0,0.34375 0.1875,419.125 0,0.75 0.6875,-0.28125 156.0625,-63.1875 0.3125,-0.125 0,-0.34375 1.34375,-418.15625 0,-0.75 z m -1,1.4375 -1.34375,417.125 -155.0625,62.78125 -0.1875,-418.03125 156.59375,-61.875 z"
|
||||
id="path2993"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:10;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans;stroke-opacity:1;stroke-miterlimit:30;stroke-dasharray:none;stroke-linecap:round;stroke-linejoin:round"
|
||||
d="m 355.6875,137.40625 -0.15625,0.0625 L 176.96875,201.0625 177.3125,202 355.75,138.4375 646.78125,207.53125 647,206.5625 l -291.15625,-69.125 -0.15625,-0.0312 z"
|
||||
id="path3003"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:10;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans;stroke-opacity:1;stroke-miterlimit:30;stroke-dasharray:none;stroke-linecap:round;stroke-linejoin:round"
|
||||
d="m 355.71875,277.53125 -0.125,0.0312 -178.9375,47.96875 -1.8125,0.46875 1.8125,0.5 311.625,83.5 0.15625,0.0312 0.125,-0.0625 157.59375,-53.65625 1.5625,-0.53125 -1.59375,-0.4375 -290.28125,-77.78125 -0.125,-0.0312 z m 0,1.03125 L 644.3125,355.90625 488.375,409 178.71875,326 l 177,-47.4375 z"
|
||||
id="path3015"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:10;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans;stroke-opacity:1;stroke-miterlimit:30;stroke-dasharray:none;stroke-linecap:round;stroke-linejoin:round"
|
||||
d="m 355.21875,240.9375 0,37.125 1,0 0,-37.125 -1,0 z"
|
||||
id="path3017"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:10;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans;stroke-opacity:1;stroke-miterlimit:30;stroke-dasharray:none;stroke-linecap:round;stroke-linejoin:round"
|
||||
d="m 176.28125,202.65625 0,393.28125 1,0 0,-393.28125 -1,0 z"
|
||||
id="path3019"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:10;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans;stroke-opacity:1;stroke-miterlimit:30;stroke-dasharray:none;stroke-linecap:round;stroke-linejoin:round"
|
||||
d="m 355.71875,409 -0.125,0.0312 L 177,455.8125 l -1.78125,0.46875 1.78125,0.5 L 488.28125,545 l 0.15625,0.0312 0.125,-0.0625 156.71875,-56.25 1.46875,-0.53125 -1.53125,-0.40625 -289.375,-78.75 -0.125,-0.0312 z m 0,1.03125 287.6875,78.28125 L 488.375,544 179.03125,456.3125 355.71875,410.03125 z"
|
||||
id="path3021"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:10;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans;stroke-opacity:1;stroke-miterlimit:30;stroke-dasharray:none;stroke-linecap:round;stroke-linejoin:round"
|
||||
d="m 355.71875,544 -0.15625,0.0312 -178.5625,52.3125 0.28125,0.96875 178.4375,-52.28125 289.59375,80.25 0.28125,-0.96875 -289.75,-80.28125 -0.125,-0.0312 z"
|
||||
id="path3023"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:10;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans;stroke-opacity:1;stroke-miterlimit:30;stroke-dasharray:none;stroke-linecap:round;stroke-linejoin:round"
|
||||
d="m 355.21875,374.34375 0,35.15625 1,0 0,-35.15625 -1,0 z"
|
||||
id="path3025"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:10;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans;stroke-opacity:1;stroke-miterlimit:30;stroke-dasharray:none;stroke-linecap:round;stroke-linejoin:round"
|
||||
d="m 355.1875,507.03125 0,37.4375 1.03125,0 0,-37.4375 -1.03125,0 z"
|
||||
id="path3027"
|
||||
inkscape:connector-curvature="0" />
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 12 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 35 KiB |
@@ -1,412 +0,0 @@
|
||||
/* Forked from the Darkfish templates rdoc.css file, much hacked, probably
|
||||
* imperfect */
|
||||
|
||||
html { max-width: 960px; margin: 0 auto; }
|
||||
body {
|
||||
font: 14px "Helvetica Neue", Helvetica, Tahoma, sans-serif;
|
||||
}
|
||||
body.file-popup {
|
||||
font-size: 90%;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #4183C4;
|
||||
}
|
||||
|
||||
:link,
|
||||
:visited {
|
||||
color: #4183C4;
|
||||
text-decoration: none;
|
||||
}
|
||||
:link:hover,
|
||||
:visited:hover {
|
||||
border-bottom: 1px dotted #4183C4;
|
||||
}
|
||||
|
||||
pre, pre.description {
|
||||
font: 12px Monaco,"Courier New","DejaVu Sans Mono","Bitstream Vera Sans Mono",monospace;
|
||||
padding: 1em;
|
||||
overflow: auto;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* @group Generic Classes */
|
||||
|
||||
.initially-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#search-field {
|
||||
width: 98%;
|
||||
}
|
||||
|
||||
.missing-docs {
|
||||
font-size: 120%;
|
||||
background: white url(images/wrench_orange.png) no-repeat 4px center;
|
||||
color: #ccc;
|
||||
line-height: 2em;
|
||||
border: 1px solid #d00;
|
||||
opacity: 1;
|
||||
text-indent: 24px;
|
||||
letter-spacing: 3px;
|
||||
font-weight: bold;
|
||||
-webkit-border-radius: 5px;
|
||||
-moz-border-radius: 5px;
|
||||
}
|
||||
|
||||
.target-section {
|
||||
border: 2px solid #dcce90;
|
||||
border-left-width: 8px;
|
||||
background: #fff3c2;
|
||||
}
|
||||
|
||||
/* @end */
|
||||
|
||||
/* @group Index Page, Standalone file pages */
|
||||
.indexpage ul {
|
||||
line-height: 160%;
|
||||
list-style: none;
|
||||
}
|
||||
.indexpage ul :link,
|
||||
.indexpage ul :visited {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.indexpage li {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.indexpage ul > li {
|
||||
background: url(images/bullet_black.png) no-repeat left 4px;
|
||||
}
|
||||
.indexpage li.method {
|
||||
background: url(images/plugin.png) no-repeat left 4px;
|
||||
}
|
||||
.indexpage li.module {
|
||||
background: url(images/package.png) no-repeat left 4px;
|
||||
}
|
||||
.indexpage li.class {
|
||||
background: url(images/ruby.png) no-repeat left 4px;
|
||||
}
|
||||
.indexpage li.file {
|
||||
background: url(images/page_white_text.png) no-repeat left 4px;
|
||||
}
|
||||
.indexpage li li {
|
||||
background: url(images/tag_blue.png) no-repeat left 4px;
|
||||
}
|
||||
.indexpage li .toc-toggle {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: url(images/add.png) no-repeat;
|
||||
}
|
||||
|
||||
.indexpage li .toc-toggle.open {
|
||||
background: url(images/delete.png) no-repeat;
|
||||
}
|
||||
|
||||
/* @end */
|
||||
|
||||
/* @group Top-Level Structure */
|
||||
|
||||
.project-section, #file-metadata, #class-metadata {
|
||||
display: block;
|
||||
background: #f5f5f5;
|
||||
margin-bottom: 1em;
|
||||
padding: 0.5em;
|
||||
}
|
||||
.project-section h3, #file-metadata h3, #class-metadata h3 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#metadata {
|
||||
float: left;
|
||||
width: 280px;
|
||||
}
|
||||
|
||||
#documentation {
|
||||
margin: 2em 1em 2em 300px;
|
||||
}
|
||||
|
||||
#validator-badges {
|
||||
clear: both;
|
||||
margin: 1em 1em 2em;
|
||||
font-size: smaller;
|
||||
}
|
||||
|
||||
/* @end */
|
||||
|
||||
/* @group Metadata Section */
|
||||
|
||||
#metadata ul,
|
||||
#metadata dl,
|
||||
#metadata p {
|
||||
padding: 0px;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
dl.svninfo {
|
||||
color: #666;
|
||||
margin: 0;
|
||||
}
|
||||
dl.svninfo dt {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
ul.link-list li {
|
||||
white-space: nowrap;
|
||||
}
|
||||
ul.link-list .type {
|
||||
font-size: 8px;
|
||||
text-transform: uppercase;
|
||||
color: white;
|
||||
background: #969696;
|
||||
}
|
||||
|
||||
/* @end */
|
||||
|
||||
/* @group Documentation Section */
|
||||
|
||||
.note-list {
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.label-list {
|
||||
margin: 8px 1.5em;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
.description .label-list {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.note-list dt {
|
||||
font-weight: bold;
|
||||
}
|
||||
.note-list dd {
|
||||
}
|
||||
|
||||
.label-list dt {
|
||||
font-weight: bold;
|
||||
background: #ddd;
|
||||
}
|
||||
.label-list dd {
|
||||
}
|
||||
.label-list dd + dt,
|
||||
.note-list dd + dt {
|
||||
margin-top: 0.7em;
|
||||
}
|
||||
|
||||
#documentation .section {
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
#documentation h2.section-header {
|
||||
color: #333;
|
||||
font-size: 175%;
|
||||
}
|
||||
|
||||
.documentation-section-title {
|
||||
position: relative;
|
||||
}
|
||||
.documentation-section-title .section-click-top {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 12px;
|
||||
font-size: 10px;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.documentation-section-title:hover .section-click-top {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
#documentation h3.section-header {
|
||||
color: #333;
|
||||
font-size: 150%;
|
||||
}
|
||||
|
||||
#constants-list > dl,
|
||||
#attributes-list > dl {
|
||||
margin: 1em 0 2em;
|
||||
border: 0;
|
||||
}
|
||||
#constants-list > dl dt,
|
||||
#attributes-list > dl dt {
|
||||
font-weight: bold;
|
||||
font-family: Monaco, "Andale Mono";
|
||||
background: inherit;
|
||||
}
|
||||
#constants-list > dl dt a,
|
||||
#attributes-list > dl dt a {
|
||||
color: inherit;
|
||||
}
|
||||
#constants-list > dl dd,
|
||||
#attributes-list > dl dd {
|
||||
margin: 0 0 1em 0;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.documentation-section h2 {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.documentation-section h2 a {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 10px;
|
||||
font-size: 12px;
|
||||
color: #9b9877;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.documentation-section h2:hover a {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
/* @group Method Details */
|
||||
|
||||
#documentation .method-source-code {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#documentation .method-detail {
|
||||
margin: 0.2em 0.2em;
|
||||
padding: 0.5em;
|
||||
}
|
||||
#documentation .method-detail:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
#documentation .method-heading {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
font-size: 125%;
|
||||
line-height: 125%;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
background: url(images/brick.png) no-repeat left bottom;
|
||||
padding-left: 20px;
|
||||
}
|
||||
#documentation .method-heading :link,
|
||||
#documentation .method-heading :visited {
|
||||
color: inherit;
|
||||
}
|
||||
#documentation .method-click-advice {
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
font-size: 10px;
|
||||
color: #aaa;
|
||||
visibility: hidden;
|
||||
background: url(images/zoom.png) no-repeat right 5px;
|
||||
padding-right: 20px;
|
||||
overflow: show;
|
||||
}
|
||||
#documentation .method-heading:hover .method-click-advice {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
#documentation .method-alias .method-heading {
|
||||
color: #666;
|
||||
background: url(images/brick_link.png) no-repeat left bottom;
|
||||
}
|
||||
|
||||
#documentation .method-description,
|
||||
#documentation .aliases {
|
||||
margin: 0 20px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
#documentation .method-description p,
|
||||
#documentation .aliases p {
|
||||
line-height: 1.2em;
|
||||
}
|
||||
|
||||
#documentation .aliases {
|
||||
font-style: italic;
|
||||
cursor: default;
|
||||
}
|
||||
#documentation .method-description p {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
#documentation .method-description ul {
|
||||
margin-left: 1.5em;
|
||||
}
|
||||
|
||||
#documentation .attribute-method-heading {
|
||||
background: url(images/tag_green.png) no-repeat left bottom;
|
||||
}
|
||||
#documentation #attribute-method-details .method-detail:hover {
|
||||
background-color: transparent;
|
||||
cursor: default;
|
||||
}
|
||||
#documentation .attribute-access-type {
|
||||
font-size: 60%;
|
||||
text-transform: uppercase;
|
||||
vertical-align: super;
|
||||
}
|
||||
|
||||
.method-section .method-source-code {
|
||||
background: white;
|
||||
}
|
||||
|
||||
/* @group Source Code */
|
||||
|
||||
.ruby-constant .ruby-keyword .ruby-ivar .ruby-operator .ruby-identifier
|
||||
.ruby-node .ruby-comment .ruby-regexp .ruby-value {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* Thanks GitHub!!! */
|
||||
.ruby-constant { color: #458; font-weight: bold; }
|
||||
.ruby-keyword { color: black; font-weight: bold; }
|
||||
.ruby-ivar { color: teal; }
|
||||
.ruby-operator { color: #000; }
|
||||
.ruby-identifier { color: black; }
|
||||
.ruby-node { color: red; }
|
||||
.ruby-comment { color: #998; font-weight: bold; }
|
||||
.ruby-regexp { color: #009926; }
|
||||
.ruby-value { color: #099; }
|
||||
.ruby-string { color: red; }
|
||||
|
||||
/* @group search results */
|
||||
|
||||
#search-section .section-header {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#search-results {
|
||||
width: 100%;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#search-results h1 {
|
||||
font-size: 1em;
|
||||
font-weight: normal;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
#search-results .current {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
#search-results li {
|
||||
list-style: none;
|
||||
line-height: 1em;
|
||||
padding: 0.5em;
|
||||
border-bottom: 1px solid black;
|
||||
}
|
||||
|
||||
#search-results .search-namespace {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#search-results li em {
|
||||
background: yellow;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
#search-results pre {
|
||||
margin: 0.5em;
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
require 'rack/lobster'
|
||||
|
||||
use Rack::ShowExceptions
|
||||
run Rack::Lobster.new
|
||||
@@ -1,14 +0,0 @@
|
||||
require 'rack'
|
||||
require 'rack/lobster'
|
||||
|
||||
lobster = Rack::Lobster.new
|
||||
|
||||
protected_lobster = Rack::Auth::Basic.new(lobster) do |username, password|
|
||||
'secret' == password
|
||||
end
|
||||
|
||||
protected_lobster.realm = 'Lobster 2.0'
|
||||
|
||||
pretty_protected_lobster = Rack::ShowStatus.new(Rack::ShowExceptions.new(protected_lobster))
|
||||
|
||||
Rack::Server.start :app => pretty_protected_lobster, :Port => 9292
|
||||
@@ -1,8 +0,0 @@
|
||||
require 'rack/lobster'
|
||||
|
||||
use Rack::ShowExceptions
|
||||
use Rack::Auth::Basic, "Lobster 2.0" do |username, password|
|
||||
'secret' == password
|
||||
end
|
||||
|
||||
run Rack::Lobster.new
|
||||
@@ -1,98 +0,0 @@
|
||||
# Copyright (C) 2007, 2008, 2009, 2010 Christian Neukirchen <purl.org/net/chneukirchen>
|
||||
#
|
||||
# Rack is freely distributable under the terms of an MIT-style license.
|
||||
# See COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
# The Rack main module, serving as a namespace for all core Rack
|
||||
# modules and classes.
|
||||
#
|
||||
# All modules meant for use in your application are <tt>autoload</tt>ed here,
|
||||
# so it should be enough just to <tt>require rack.rb</tt> in your code.
|
||||
|
||||
module Rack
|
||||
# The Rack protocol version number implemented.
|
||||
VERSION = [1,3]
|
||||
|
||||
# Return the Rack protocol version as a dotted string.
|
||||
def self.version
|
||||
VERSION.join(".")
|
||||
end
|
||||
|
||||
# Return the Rack release as a dotted string.
|
||||
def self.release
|
||||
"1.6.4"
|
||||
end
|
||||
PATH_INFO = 'PATH_INFO'.freeze
|
||||
REQUEST_METHOD = 'REQUEST_METHOD'.freeze
|
||||
SCRIPT_NAME = 'SCRIPT_NAME'.freeze
|
||||
QUERY_STRING = 'QUERY_STRING'.freeze
|
||||
CACHE_CONTROL = 'Cache-Control'.freeze
|
||||
CONTENT_LENGTH = 'Content-Length'.freeze
|
||||
CONTENT_TYPE = 'Content-Type'.freeze
|
||||
|
||||
GET = 'GET'.freeze
|
||||
HEAD = 'HEAD'.freeze
|
||||
|
||||
autoload :Builder, "rack/builder"
|
||||
autoload :BodyProxy, "rack/body_proxy"
|
||||
autoload :Cascade, "rack/cascade"
|
||||
autoload :Chunked, "rack/chunked"
|
||||
autoload :CommonLogger, "rack/commonlogger"
|
||||
autoload :ConditionalGet, "rack/conditionalget"
|
||||
autoload :Config, "rack/config"
|
||||
autoload :ContentLength, "rack/content_length"
|
||||
autoload :ContentType, "rack/content_type"
|
||||
autoload :ETag, "rack/etag"
|
||||
autoload :File, "rack/file"
|
||||
autoload :Deflater, "rack/deflater"
|
||||
autoload :Directory, "rack/directory"
|
||||
autoload :ForwardRequest, "rack/recursive"
|
||||
autoload :Handler, "rack/handler"
|
||||
autoload :Head, "rack/head"
|
||||
autoload :Lint, "rack/lint"
|
||||
autoload :Lock, "rack/lock"
|
||||
autoload :Logger, "rack/logger"
|
||||
autoload :MethodOverride, "rack/methodoverride"
|
||||
autoload :Mime, "rack/mime"
|
||||
autoload :NullLogger, "rack/nulllogger"
|
||||
autoload :Recursive, "rack/recursive"
|
||||
autoload :Reloader, "rack/reloader"
|
||||
autoload :Runtime, "rack/runtime"
|
||||
autoload :Sendfile, "rack/sendfile"
|
||||
autoload :Server, "rack/server"
|
||||
autoload :ShowExceptions, "rack/showexceptions"
|
||||
autoload :ShowStatus, "rack/showstatus"
|
||||
autoload :Static, "rack/static"
|
||||
autoload :TempfileReaper, "rack/tempfile_reaper"
|
||||
autoload :URLMap, "rack/urlmap"
|
||||
autoload :Utils, "rack/utils"
|
||||
autoload :Multipart, "rack/multipart"
|
||||
|
||||
autoload :MockRequest, "rack/mock"
|
||||
autoload :MockResponse, "rack/mock"
|
||||
|
||||
autoload :Request, "rack/request"
|
||||
autoload :Response, "rack/response"
|
||||
|
||||
module Auth
|
||||
autoload :Basic, "rack/auth/basic"
|
||||
autoload :AbstractRequest, "rack/auth/abstract/request"
|
||||
autoload :AbstractHandler, "rack/auth/abstract/handler"
|
||||
module Digest
|
||||
autoload :MD5, "rack/auth/digest/md5"
|
||||
autoload :Nonce, "rack/auth/digest/nonce"
|
||||
autoload :Params, "rack/auth/digest/params"
|
||||
autoload :Request, "rack/auth/digest/request"
|
||||
end
|
||||
end
|
||||
|
||||
module Session
|
||||
autoload :Cookie, "rack/session/cookie"
|
||||
autoload :Pool, "rack/session/pool"
|
||||
autoload :Memcache, "rack/session/memcache"
|
||||
end
|
||||
|
||||
module Utils
|
||||
autoload :OkJson, "rack/utils/okjson"
|
||||
end
|
||||
end
|
||||
@@ -1,37 +0,0 @@
|
||||
module Rack
|
||||
module Auth
|
||||
# Rack::Auth::AbstractHandler implements common authentication functionality.
|
||||
#
|
||||
# +realm+ should be set for all handlers.
|
||||
|
||||
class AbstractHandler
|
||||
|
||||
attr_accessor :realm
|
||||
|
||||
def initialize(app, realm=nil, &authenticator)
|
||||
@app, @realm, @authenticator = app, realm, authenticator
|
||||
end
|
||||
|
||||
|
||||
private
|
||||
|
||||
def unauthorized(www_authenticate = challenge)
|
||||
return [ 401,
|
||||
{ CONTENT_TYPE => 'text/plain',
|
||||
CONTENT_LENGTH => '0',
|
||||
'WWW-Authenticate' => www_authenticate.to_s },
|
||||
[]
|
||||
]
|
||||
end
|
||||
|
||||
def bad_request
|
||||
return [ 400,
|
||||
{ CONTENT_TYPE => 'text/plain',
|
||||
CONTENT_LENGTH => '0' },
|
||||
[]
|
||||
]
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,43 +0,0 @@
|
||||
require 'rack/request'
|
||||
|
||||
module Rack
|
||||
module Auth
|
||||
class AbstractRequest
|
||||
|
||||
def initialize(env)
|
||||
@env = env
|
||||
end
|
||||
|
||||
def request
|
||||
@request ||= Request.new(@env)
|
||||
end
|
||||
|
||||
def provided?
|
||||
!authorization_key.nil?
|
||||
end
|
||||
|
||||
def parts
|
||||
@parts ||= @env[authorization_key].split(' ', 2)
|
||||
end
|
||||
|
||||
def scheme
|
||||
@scheme ||= parts.first && parts.first.downcase
|
||||
end
|
||||
|
||||
def params
|
||||
@params ||= parts.last
|
||||
end
|
||||
|
||||
|
||||
private
|
||||
|
||||
AUTHORIZATION_KEYS = ['HTTP_AUTHORIZATION', 'X-HTTP_AUTHORIZATION', 'X_HTTP_AUTHORIZATION']
|
||||
|
||||
def authorization_key
|
||||
@authorization_key ||= AUTHORIZATION_KEYS.detect { |key| @env.has_key?(key) }
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,58 +0,0 @@
|
||||
require 'rack/auth/abstract/handler'
|
||||
require 'rack/auth/abstract/request'
|
||||
|
||||
module Rack
|
||||
module Auth
|
||||
# Rack::Auth::Basic implements HTTP Basic Authentication, as per RFC 2617.
|
||||
#
|
||||
# Initialize with the Rack application that you want protecting,
|
||||
# and a block that checks if a username and password pair are valid.
|
||||
#
|
||||
# See also: <tt>example/protectedlobster.rb</tt>
|
||||
|
||||
class Basic < AbstractHandler
|
||||
|
||||
def call(env)
|
||||
auth = Basic::Request.new(env)
|
||||
|
||||
return unauthorized unless auth.provided?
|
||||
|
||||
return bad_request unless auth.basic?
|
||||
|
||||
if valid?(auth)
|
||||
env['REMOTE_USER'] = auth.username
|
||||
|
||||
return @app.call(env)
|
||||
end
|
||||
|
||||
unauthorized
|
||||
end
|
||||
|
||||
|
||||
private
|
||||
|
||||
def challenge
|
||||
'Basic realm="%s"' % realm
|
||||
end
|
||||
|
||||
def valid?(auth)
|
||||
@authenticator.call(*auth.credentials)
|
||||
end
|
||||
|
||||
class Request < Auth::AbstractRequest
|
||||
def basic?
|
||||
"basic" == scheme
|
||||
end
|
||||
|
||||
def credentials
|
||||
@credentials ||= params.unpack("m*").first.split(/:/, 2)
|
||||
end
|
||||
|
||||
def username
|
||||
credentials.first
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,129 +0,0 @@
|
||||
require 'rack/auth/abstract/handler'
|
||||
require 'rack/auth/digest/request'
|
||||
require 'rack/auth/digest/params'
|
||||
require 'rack/auth/digest/nonce'
|
||||
require 'digest/md5'
|
||||
|
||||
module Rack
|
||||
module Auth
|
||||
module Digest
|
||||
# Rack::Auth::Digest::MD5 implements the MD5 algorithm version of
|
||||
# HTTP Digest Authentication, as per RFC 2617.
|
||||
#
|
||||
# Initialize with the [Rack] application that you want protecting,
|
||||
# and a block that looks up a plaintext password for a given username.
|
||||
#
|
||||
# +opaque+ needs to be set to a constant base64/hexadecimal string.
|
||||
#
|
||||
class MD5 < AbstractHandler
|
||||
|
||||
attr_accessor :opaque
|
||||
|
||||
attr_writer :passwords_hashed
|
||||
|
||||
def initialize(app, realm=nil, opaque=nil, &authenticator)
|
||||
@passwords_hashed = nil
|
||||
if opaque.nil? and realm.respond_to? :values_at
|
||||
realm, opaque, @passwords_hashed = realm.values_at :realm, :opaque, :passwords_hashed
|
||||
end
|
||||
super(app, realm, &authenticator)
|
||||
@opaque = opaque
|
||||
end
|
||||
|
||||
def passwords_hashed?
|
||||
!!@passwords_hashed
|
||||
end
|
||||
|
||||
def call(env)
|
||||
auth = Request.new(env)
|
||||
|
||||
unless auth.provided?
|
||||
return unauthorized
|
||||
end
|
||||
|
||||
if !auth.digest? || !auth.correct_uri? || !valid_qop?(auth)
|
||||
return bad_request
|
||||
end
|
||||
|
||||
if valid?(auth)
|
||||
if auth.nonce.stale?
|
||||
return unauthorized(challenge(:stale => true))
|
||||
else
|
||||
env['REMOTE_USER'] = auth.username
|
||||
|
||||
return @app.call(env)
|
||||
end
|
||||
end
|
||||
|
||||
unauthorized
|
||||
end
|
||||
|
||||
|
||||
private
|
||||
|
||||
QOP = 'auth'.freeze
|
||||
|
||||
def params(hash = {})
|
||||
Params.new do |params|
|
||||
params['realm'] = realm
|
||||
params['nonce'] = Nonce.new.to_s
|
||||
params['opaque'] = H(opaque)
|
||||
params['qop'] = QOP
|
||||
|
||||
hash.each { |k, v| params[k] = v }
|
||||
end
|
||||
end
|
||||
|
||||
def challenge(hash = {})
|
||||
"Digest #{params(hash)}"
|
||||
end
|
||||
|
||||
def valid?(auth)
|
||||
valid_opaque?(auth) && valid_nonce?(auth) && valid_digest?(auth)
|
||||
end
|
||||
|
||||
def valid_qop?(auth)
|
||||
QOP == auth.qop
|
||||
end
|
||||
|
||||
def valid_opaque?(auth)
|
||||
H(opaque) == auth.opaque
|
||||
end
|
||||
|
||||
def valid_nonce?(auth)
|
||||
auth.nonce.valid?
|
||||
end
|
||||
|
||||
def valid_digest?(auth)
|
||||
pw = @authenticator.call(auth.username)
|
||||
pw && Rack::Utils.secure_compare(digest(auth, pw), auth.response)
|
||||
end
|
||||
|
||||
def md5(data)
|
||||
::Digest::MD5.hexdigest(data)
|
||||
end
|
||||
|
||||
alias :H :md5
|
||||
|
||||
def KD(secret, data)
|
||||
H([secret, data] * ':')
|
||||
end
|
||||
|
||||
def A1(auth, password)
|
||||
[ auth.username, auth.realm, password ] * ':'
|
||||
end
|
||||
|
||||
def A2(auth)
|
||||
[ auth.method, auth.uri ] * ':'
|
||||
end
|
||||
|
||||
def digest(auth, password)
|
||||
password_hash = passwords_hashed? ? password : H(A1(auth, password))
|
||||
|
||||
KD(password_hash, [ auth.nonce, auth.nc, auth.cnonce, QOP, H(A2(auth)) ] * ':')
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,51 +0,0 @@
|
||||
require 'digest/md5'
|
||||
|
||||
module Rack
|
||||
module Auth
|
||||
module Digest
|
||||
# Rack::Auth::Digest::Nonce is the default nonce generator for the
|
||||
# Rack::Auth::Digest::MD5 authentication handler.
|
||||
#
|
||||
# +private_key+ needs to set to a constant string.
|
||||
#
|
||||
# +time_limit+ can be optionally set to an integer (number of seconds),
|
||||
# to limit the validity of the generated nonces.
|
||||
|
||||
class Nonce
|
||||
|
||||
class << self
|
||||
attr_accessor :private_key, :time_limit
|
||||
end
|
||||
|
||||
def self.parse(string)
|
||||
new(*string.unpack("m*").first.split(' ', 2))
|
||||
end
|
||||
|
||||
def initialize(timestamp = Time.now, given_digest = nil)
|
||||
@timestamp, @given_digest = timestamp.to_i, given_digest
|
||||
end
|
||||
|
||||
def to_s
|
||||
[([ @timestamp, digest ] * ' ')].pack("m*").strip
|
||||
end
|
||||
|
||||
def digest
|
||||
::Digest::MD5.hexdigest([ @timestamp, self.class.private_key ] * ':')
|
||||
end
|
||||
|
||||
def valid?
|
||||
digest == @given_digest
|
||||
end
|
||||
|
||||
def stale?
|
||||
!self.class.time_limit.nil? && (Time.now.to_i - @timestamp) > self.class.time_limit
|
||||
end
|
||||
|
||||
def fresh?
|
||||
!stale?
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,53 +0,0 @@
|
||||
module Rack
|
||||
module Auth
|
||||
module Digest
|
||||
class Params < Hash
|
||||
|
||||
def self.parse(str)
|
||||
Params[*split_header_value(str).map do |param|
|
||||
k, v = param.split('=', 2)
|
||||
[k, dequote(v)]
|
||||
end.flatten]
|
||||
end
|
||||
|
||||
def self.dequote(str) # From WEBrick::HTTPUtils
|
||||
ret = (/\A"(.*)"\Z/ =~ str) ? $1 : str.dup
|
||||
ret.gsub!(/\\(.)/, "\\1")
|
||||
ret
|
||||
end
|
||||
|
||||
def self.split_header_value(str)
|
||||
str.scan( /(\w+\=(?:"[^\"]+"|[^,]+))/n ).collect{ |v| v[0] }
|
||||
end
|
||||
|
||||
def initialize
|
||||
super()
|
||||
|
||||
yield self if block_given?
|
||||
end
|
||||
|
||||
def [](k)
|
||||
super k.to_s
|
||||
end
|
||||
|
||||
def []=(k, v)
|
||||
super k.to_s, v.to_s
|
||||
end
|
||||
|
||||
UNQUOTED = ['nc', 'stale']
|
||||
|
||||
def to_s
|
||||
map do |k, v|
|
||||
"#{k}=" + (UNQUOTED.include?(k) ? v.to_s : quote(v))
|
||||
end.join(', ')
|
||||
end
|
||||
|
||||
def quote(str) # From WEBrick::HTTPUtils
|
||||
'"' << str.gsub(/[\\\"]/o, "\\\1") << '"'
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
require 'rack/auth/abstract/request'
|
||||
require 'rack/auth/digest/params'
|
||||
require 'rack/auth/digest/nonce'
|
||||
|
||||
module Rack
|
||||
module Auth
|
||||
module Digest
|
||||
class Request < Auth::AbstractRequest
|
||||
def method
|
||||
@env['rack.methodoverride.original_method'] || @env[REQUEST_METHOD]
|
||||
end
|
||||
|
||||
def digest?
|
||||
"digest" == scheme
|
||||
end
|
||||
|
||||
def correct_uri?
|
||||
request.fullpath == uri
|
||||
end
|
||||
|
||||
def nonce
|
||||
@nonce ||= Nonce.parse(params['nonce'])
|
||||
end
|
||||
|
||||
def params
|
||||
@params ||= Params.parse(parts.last)
|
||||
end
|
||||
|
||||
def respond_to?(sym, *)
|
||||
super or params.has_key? sym.to_s
|
||||
end
|
||||
|
||||
def method_missing(sym, *args)
|
||||
return super unless params.has_key?(key = sym.to_s)
|
||||
return params[key] if args.size == 0
|
||||
raise ArgumentError, "wrong number of arguments (#{args.size} for 0)"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,56 +0,0 @@
|
||||
# :stopdoc:
|
||||
|
||||
# Stolen from ruby core's uri/common.rb, with modifications to support 1.8.x
|
||||
#
|
||||
# https://github.com/ruby/ruby/blob/trunk/lib/uri/common.rb
|
||||
#
|
||||
#
|
||||
|
||||
module URI
|
||||
TBLENCWWWCOMP_ = {} # :nodoc:
|
||||
256.times do |i|
|
||||
TBLENCWWWCOMP_[i.chr] = '%%%02X' % i
|
||||
end
|
||||
TBLENCWWWCOMP_[' '] = '+'
|
||||
TBLENCWWWCOMP_.freeze
|
||||
TBLDECWWWCOMP_ = {} # :nodoc:
|
||||
256.times do |i|
|
||||
h, l = i>>4, i&15
|
||||
TBLDECWWWCOMP_['%%%X%X' % [h, l]] = i.chr
|
||||
TBLDECWWWCOMP_['%%%x%X' % [h, l]] = i.chr
|
||||
TBLDECWWWCOMP_['%%%X%x' % [h, l]] = i.chr
|
||||
TBLDECWWWCOMP_['%%%x%x' % [h, l]] = i.chr
|
||||
end
|
||||
TBLDECWWWCOMP_['+'] = ' '
|
||||
TBLDECWWWCOMP_.freeze
|
||||
|
||||
# Encode given +s+ to URL-encoded form data.
|
||||
#
|
||||
# This method doesn't convert *, -, ., 0-9, A-Z, _, a-z, but does convert SP
|
||||
# (ASCII space) to + and converts others to %XX.
|
||||
#
|
||||
# This is an implementation of
|
||||
# http://www.w3.org/TR/html5/forms.html#url-encoded-form-data
|
||||
#
|
||||
# See URI.decode_www_form_component, URI.encode_www_form
|
||||
def self.encode_www_form_component(s)
|
||||
str = s.to_s
|
||||
if RUBY_VERSION < "1.9" && $KCODE =~ /u/i
|
||||
str.gsub(/([^ a-zA-Z0-9_.-]+)/) do
|
||||
'%' + $1.unpack('H2' * Rack::Utils.bytesize($1)).join('%').upcase
|
||||
end.tr(' ', '+')
|
||||
else
|
||||
str.gsub(/[^*\-.0-9A-Z_a-z]/) {|m| TBLENCWWWCOMP_[m]}
|
||||
end
|
||||
end
|
||||
|
||||
# Decode given +str+ of URL-encoded form data.
|
||||
#
|
||||
# This decodes + to SP.
|
||||
#
|
||||
# See URI.encode_www_form_component, URI.decode_www_form
|
||||
def self.decode_www_form_component(str, enc=nil)
|
||||
raise ArgumentError, "invalid %-encoding (#{str})" unless /\A(?:%[0-9a-fA-F]{2}|[^%])*\z/ =~ str
|
||||
str.gsub(/\+|%[0-9a-fA-F]{2}/) {|m| TBLDECWWWCOMP_[m]}
|
||||
end
|
||||
end
|
||||
@@ -1,52 +0,0 @@
|
||||
# :stopdoc:
|
||||
|
||||
# Stolen from ruby core's uri/common.rb @32618ba to fix DoS issues in 1.9.2
|
||||
#
|
||||
# https://github.com/ruby/ruby/blob/32618ba7438a2247042bba9b5d85b5d49070f5e5/lib/uri/common.rb
|
||||
#
|
||||
# Issue:
|
||||
# http://redmine.ruby-lang.org/issues/5149
|
||||
#
|
||||
# Relevant Fixes:
|
||||
# https://github.com/ruby/ruby/commit/b5f91deee04aa6ccbe07c23c8222b937c22a799b
|
||||
# https://github.com/ruby/ruby/commit/93177c1e5c3906abf14472ae0b905d8b5c72ce1b
|
||||
#
|
||||
# This should probably be removed once there is a Ruby 1.9.2 patch level that
|
||||
# includes this fix.
|
||||
|
||||
require 'uri/common'
|
||||
|
||||
module URI
|
||||
TBLDECWWWCOMP_ = {} unless const_defined?(:TBLDECWWWCOMP_) #:nodoc:
|
||||
if TBLDECWWWCOMP_.empty?
|
||||
256.times do |i|
|
||||
h, l = i>>4, i&15
|
||||
TBLDECWWWCOMP_['%%%X%X' % [h, l]] = i.chr
|
||||
TBLDECWWWCOMP_['%%%x%X' % [h, l]] = i.chr
|
||||
TBLDECWWWCOMP_['%%%X%x' % [h, l]] = i.chr
|
||||
TBLDECWWWCOMP_['%%%x%x' % [h, l]] = i.chr
|
||||
end
|
||||
TBLDECWWWCOMP_['+'] = ' '
|
||||
TBLDECWWWCOMP_.freeze
|
||||
end
|
||||
|
||||
def self.decode_www_form(str, enc=Encoding::UTF_8)
|
||||
return [] if str.empty?
|
||||
unless /\A#{WFKV_}=#{WFKV_}(?:[;&]#{WFKV_}=#{WFKV_})*\z/o =~ str
|
||||
raise ArgumentError, "invalid data of application/x-www-form-urlencoded (#{str})"
|
||||
end
|
||||
ary = []
|
||||
$&.scan(/([^=;&]+)=([^;&]*)/) do
|
||||
ary << [decode_www_form_component($1, enc), decode_www_form_component($2, enc)]
|
||||
end
|
||||
ary
|
||||
end
|
||||
|
||||
def self.decode_www_form_component(str, enc=Encoding::UTF_8)
|
||||
raise ArgumentError, "invalid %-encoding (#{str})" unless /\A[^%]*(?:%\h\h[^%]*)*\z/ =~ str
|
||||
str.gsub(/\+|%\h\h/, TBLDECWWWCOMP_).force_encoding(enc)
|
||||
end
|
||||
|
||||
remove_const :WFKV_ if const_defined?(:WFKV_)
|
||||
WFKV_ = '(?:[^%#=;&]*(?:%\h\h[^%#=;&]*)*)' # :nodoc:
|
||||
end
|
||||
@@ -1,29 +0,0 @@
|
||||
# :stopdoc:
|
||||
|
||||
require 'uri/common'
|
||||
|
||||
# Issue:
|
||||
# http://bugs.ruby-lang.org/issues/5925
|
||||
#
|
||||
# Relevant commit:
|
||||
# https://github.com/ruby/ruby/commit/edb7cdf1eabaff78dfa5ffedfbc2e91b29fa9ca1
|
||||
|
||||
module URI
|
||||
256.times do |i|
|
||||
TBLENCWWWCOMP_[i.chr] = '%%%02X' % i
|
||||
end
|
||||
TBLENCWWWCOMP_[' '] = '+'
|
||||
TBLENCWWWCOMP_.freeze
|
||||
|
||||
256.times do |i|
|
||||
h, l = i>>4, i&15
|
||||
TBLDECWWWCOMP_['%%%X%X' % [h, l]] = i.chr
|
||||
TBLDECWWWCOMP_['%%%x%X' % [h, l]] = i.chr
|
||||
TBLDECWWWCOMP_['%%%X%x' % [h, l]] = i.chr
|
||||
TBLDECWWWCOMP_['%%%x%x' % [h, l]] = i.chr
|
||||
end
|
||||
TBLDECWWWCOMP_['+'] = ' '
|
||||
TBLDECWWWCOMP_.freeze
|
||||
end
|
||||
|
||||
# :startdoc:
|
||||
@@ -1,39 +0,0 @@
|
||||
module Rack
|
||||
class BodyProxy
|
||||
def initialize(body, &block)
|
||||
@body, @block, @closed = body, block, false
|
||||
end
|
||||
|
||||
def respond_to?(*args)
|
||||
return false if args.first.to_s =~ /^to_ary$/
|
||||
super or @body.respond_to?(*args)
|
||||
end
|
||||
|
||||
def close
|
||||
return if @closed
|
||||
@closed = true
|
||||
begin
|
||||
@body.close if @body.respond_to? :close
|
||||
ensure
|
||||
@block.call
|
||||
end
|
||||
end
|
||||
|
||||
def closed?
|
||||
@closed
|
||||
end
|
||||
|
||||
# N.B. This method is a special case to address the bug described by #434.
|
||||
# We are applying this special case for #each only. Future bugs of this
|
||||
# class will be handled by requesting users to patch their ruby
|
||||
# implementation, to save adding too many methods in this class.
|
||||
def each(*args, &block)
|
||||
@body.each(*args, &block)
|
||||
end
|
||||
|
||||
def method_missing(*args, &block)
|
||||
super if args.first.to_s =~ /^to_ary$/
|
||||
@body.__send__(*args, &block)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,164 +0,0 @@
|
||||
module Rack
|
||||
# Rack::Builder implements a small DSL to iteratively construct Rack
|
||||
# applications.
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# require 'rack/lobster'
|
||||
# app = Rack::Builder.new do
|
||||
# use Rack::CommonLogger
|
||||
# use Rack::ShowExceptions
|
||||
# map "/lobster" do
|
||||
# use Rack::Lint
|
||||
# run Rack::Lobster.new
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# run app
|
||||
#
|
||||
# Or
|
||||
#
|
||||
# app = Rack::Builder.app do
|
||||
# use Rack::CommonLogger
|
||||
# run lambda { |env| [200, {'Content-Type' => 'text/plain'}, ['OK']] }
|
||||
# end
|
||||
#
|
||||
# run app
|
||||
#
|
||||
# +use+ adds middleware to the stack, +run+ dispatches to an application.
|
||||
# You can use +map+ to construct a Rack::URLMap in a convenient way.
|
||||
|
||||
class Builder
|
||||
def self.parse_file(config, opts = Server::Options.new)
|
||||
options = {}
|
||||
if config =~ /\.ru$/
|
||||
cfgfile = ::File.read(config)
|
||||
if cfgfile[/^#\\(.*)/] && opts
|
||||
options = opts.parse! $1.split(/\s+/)
|
||||
end
|
||||
cfgfile.sub!(/^__END__\n.*\Z/m, '')
|
||||
app = new_from_string cfgfile, config
|
||||
else
|
||||
require config
|
||||
app = Object.const_get(::File.basename(config, '.rb').capitalize)
|
||||
end
|
||||
return app, options
|
||||
end
|
||||
|
||||
def self.new_from_string(builder_script, file="(rackup)")
|
||||
eval "Rack::Builder.new {\n" + builder_script + "\n}.to_app",
|
||||
TOPLEVEL_BINDING, file, 0
|
||||
end
|
||||
|
||||
def initialize(default_app = nil,&block)
|
||||
@use, @map, @run, @warmup = [], nil, default_app, nil
|
||||
instance_eval(&block) if block_given?
|
||||
end
|
||||
|
||||
def self.app(default_app = nil, &block)
|
||||
self.new(default_app, &block).to_app
|
||||
end
|
||||
|
||||
# Specifies middleware to use in a stack.
|
||||
#
|
||||
# class Middleware
|
||||
# def initialize(app)
|
||||
# @app = app
|
||||
# end
|
||||
#
|
||||
# def call(env)
|
||||
# env["rack.some_header"] = "setting an example"
|
||||
# @app.call(env)
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# use Middleware
|
||||
# run lambda { |env| [200, { "Content-Type" => "text/plain" }, ["OK"]] }
|
||||
#
|
||||
# All requests through to this application will first be processed by the middleware class.
|
||||
# The +call+ method in this example sets an additional environment key which then can be
|
||||
# referenced in the application if required.
|
||||
def use(middleware, *args, &block)
|
||||
if @map
|
||||
mapping, @map = @map, nil
|
||||
@use << proc { |app| generate_map app, mapping }
|
||||
end
|
||||
@use << proc { |app| middleware.new(app, *args, &block) }
|
||||
end
|
||||
|
||||
# Takes an argument that is an object that responds to #call and returns a Rack response.
|
||||
# The simplest form of this is a lambda object:
|
||||
#
|
||||
# run lambda { |env| [200, { "Content-Type" => "text/plain" }, ["OK"]] }
|
||||
#
|
||||
# However this could also be a class:
|
||||
#
|
||||
# class Heartbeat
|
||||
# def self.call(env)
|
||||
# [200, { "Content-Type" => "text/plain" }, ["OK"]]
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# run Heartbeat
|
||||
def run(app)
|
||||
@run = app
|
||||
end
|
||||
|
||||
# Takes a lambda or block that is used to warm-up the application.
|
||||
#
|
||||
# warmup do |app|
|
||||
# client = Rack::MockRequest.new(app)
|
||||
# client.get('/')
|
||||
# end
|
||||
#
|
||||
# use SomeMiddleware
|
||||
# run MyApp
|
||||
def warmup(prc=nil, &block)
|
||||
@warmup = prc || block
|
||||
end
|
||||
|
||||
# Creates a route within the application.
|
||||
#
|
||||
# Rack::Builder.app do
|
||||
# map '/' do
|
||||
# run Heartbeat
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# The +use+ method can also be used here to specify middleware to run under a specific path:
|
||||
#
|
||||
# Rack::Builder.app do
|
||||
# map '/' do
|
||||
# use Middleware
|
||||
# run Heartbeat
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# This example includes a piece of middleware which will run before requests hit +Heartbeat+.
|
||||
#
|
||||
def map(path, &block)
|
||||
@map ||= {}
|
||||
@map[path] = block
|
||||
end
|
||||
|
||||
def to_app
|
||||
app = @map ? generate_map(@run, @map) : @run
|
||||
fail "missing run or map statement" unless app
|
||||
app = @use.reverse.inject(app) { |a,e| e[a] }
|
||||
@warmup.call(app) if @warmup
|
||||
app
|
||||
end
|
||||
|
||||
def call(env)
|
||||
to_app.call(env)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def generate_map(default_app, mapping)
|
||||
mapped = default_app ? {'/' => default_app} : {}
|
||||
mapping.each { |r,b| mapped[r] = self.class.new(default_app, &b).to_app }
|
||||
URLMap.new(mapped)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,52 +0,0 @@
|
||||
module Rack
|
||||
# Rack::Cascade tries a request on several apps, and returns the
|
||||
# first response that is not 404 or 405 (or in a list of configurable
|
||||
# status codes).
|
||||
|
||||
class Cascade
|
||||
NotFound = [404, {CONTENT_TYPE => "text/plain"}, []]
|
||||
|
||||
attr_reader :apps
|
||||
|
||||
def initialize(apps, catch=[404, 405])
|
||||
@apps = []; @has_app = {}
|
||||
apps.each { |app| add app }
|
||||
|
||||
@catch = {}
|
||||
[*catch].each { |status| @catch[status] = true }
|
||||
end
|
||||
|
||||
def call(env)
|
||||
result = NotFound
|
||||
|
||||
last_body = nil
|
||||
|
||||
@apps.each do |app|
|
||||
# The SPEC says that the body must be closed after it has been iterated
|
||||
# by the server, or if it is replaced by a middleware action. Cascade
|
||||
# replaces the body each time a cascade happens. It is assumed that nil
|
||||
# does not respond to close, otherwise the previous application body
|
||||
# will be closed. The final application body will not be closed, as it
|
||||
# will be passed to the server as a result.
|
||||
last_body.close if last_body.respond_to? :close
|
||||
|
||||
result = app.call(env)
|
||||
last_body = result[2]
|
||||
break unless @catch.include?(result[0].to_i)
|
||||
end
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
def add(app)
|
||||
@has_app[app] = true
|
||||
@apps << app
|
||||
end
|
||||
|
||||
def include?(app)
|
||||
@has_app.include? app
|
||||
end
|
||||
|
||||
alias_method :<<, :add
|
||||
end
|
||||
end
|
||||
@@ -1,69 +0,0 @@
|
||||
require 'rack/utils'
|
||||
|
||||
module Rack
|
||||
|
||||
# Middleware that applies chunked transfer encoding to response bodies
|
||||
# when the response does not include a Content-Length header.
|
||||
class Chunked
|
||||
include Rack::Utils
|
||||
|
||||
# A body wrapper that emits chunked responses
|
||||
class Body
|
||||
TERM = "\r\n"
|
||||
TAIL = "0#{TERM}#{TERM}"
|
||||
|
||||
include Rack::Utils
|
||||
|
||||
def initialize(body)
|
||||
@body = body
|
||||
end
|
||||
|
||||
def each
|
||||
term = TERM
|
||||
@body.each do |chunk|
|
||||
size = bytesize(chunk)
|
||||
next if size == 0
|
||||
|
||||
chunk = chunk.dup.force_encoding(Encoding::BINARY) if chunk.respond_to?(:force_encoding)
|
||||
yield [size.to_s(16), term, chunk, term].join
|
||||
end
|
||||
yield TAIL
|
||||
end
|
||||
|
||||
def close
|
||||
@body.close if @body.respond_to?(:close)
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(app)
|
||||
@app = app
|
||||
end
|
||||
|
||||
# pre-HTTP/1.0 (informally "HTTP/0.9") HTTP requests did not have
|
||||
# a version (nor response headers)
|
||||
def chunkable_version?(ver)
|
||||
case ver
|
||||
when "HTTP/1.0", nil, "HTTP/0.9"
|
||||
false
|
||||
else
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
def call(env)
|
||||
status, headers, body = @app.call(env)
|
||||
headers = HeaderHash.new(headers)
|
||||
|
||||
if ! chunkable_version?(env['HTTP_VERSION']) ||
|
||||
STATUS_WITH_NO_ENTITY_BODY.include?(status) ||
|
||||
headers[CONTENT_LENGTH] ||
|
||||
headers['Transfer-Encoding']
|
||||
[status, headers, body]
|
||||
else
|
||||
headers.delete(CONTENT_LENGTH)
|
||||
headers['Transfer-Encoding'] = 'chunked'
|
||||
[status, headers, Body.new(body)]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,72 +0,0 @@
|
||||
require 'rack/body_proxy'
|
||||
|
||||
module Rack
|
||||
# Rack::CommonLogger forwards every request to the given +app+, and
|
||||
# logs a line in the
|
||||
# {Apache common log format}[http://httpd.apache.org/docs/1.3/logs.html#common]
|
||||
# to the +logger+.
|
||||
#
|
||||
# If +logger+ is nil, CommonLogger will fall back +rack.errors+, which is
|
||||
# an instance of Rack::NullLogger.
|
||||
#
|
||||
# +logger+ can be any class, including the standard library Logger, and is
|
||||
# expected to have either +write+ or +<<+ method, which accepts the CommonLogger::FORMAT.
|
||||
# According to the SPEC, the error stream must also respond to +puts+
|
||||
# (which takes a single argument that responds to +to_s+), and +flush+
|
||||
# (which is called without arguments in order to make the error appear for
|
||||
# sure)
|
||||
class CommonLogger
|
||||
# Common Log Format: http://httpd.apache.org/docs/1.3/logs.html#common
|
||||
#
|
||||
# lilith.local - - [07/Aug/2006 23:58:02 -0400] "GET / HTTP/1.1" 500 -
|
||||
#
|
||||
# %{%s - %s [%s] "%s %s%s %s" %d %s\n} %
|
||||
FORMAT = %{%s - %s [%s] "%s %s%s %s" %d %s %0.4f\n}
|
||||
|
||||
def initialize(app, logger=nil)
|
||||
@app = app
|
||||
@logger = logger
|
||||
end
|
||||
|
||||
def call(env)
|
||||
began_at = Time.now
|
||||
status, header, body = @app.call(env)
|
||||
header = Utils::HeaderHash.new(header)
|
||||
body = BodyProxy.new(body) { log(env, status, header, began_at) }
|
||||
[status, header, body]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def log(env, status, header, began_at)
|
||||
now = Time.now
|
||||
length = extract_content_length(header)
|
||||
|
||||
msg = FORMAT % [
|
||||
env['HTTP_X_FORWARDED_FOR'] || env["REMOTE_ADDR"] || "-",
|
||||
env["REMOTE_USER"] || "-",
|
||||
now.strftime("%d/%b/%Y:%H:%M:%S %z"),
|
||||
env[REQUEST_METHOD],
|
||||
env[PATH_INFO],
|
||||
env[QUERY_STRING].empty? ? "" : "?"+env[QUERY_STRING],
|
||||
env["HTTP_VERSION"],
|
||||
status.to_s[0..3],
|
||||
length,
|
||||
now - began_at ]
|
||||
|
||||
logger = @logger || env['rack.errors']
|
||||
# Standard library logger doesn't support write but it supports << which actually
|
||||
# calls to write on the log device without formatting
|
||||
if logger.respond_to?(:write)
|
||||
logger.write(msg)
|
||||
else
|
||||
logger << msg
|
||||
end
|
||||
end
|
||||
|
||||
def extract_content_length(headers)
|
||||
value = headers[CONTENT_LENGTH] or return '-'
|
||||
value.to_s == '0' ? '-' : value
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,79 +0,0 @@
|
||||
require 'rack/utils'
|
||||
|
||||
module Rack
|
||||
|
||||
# Middleware that enables conditional GET using If-None-Match and
|
||||
# If-Modified-Since. The application should set either or both of the
|
||||
# Last-Modified or Etag response headers according to RFC 2616. When
|
||||
# either of the conditions is met, the response body is set to be zero
|
||||
# length and the response status is set to 304 Not Modified.
|
||||
#
|
||||
# Applications that defer response body generation until the body's each
|
||||
# message is received will avoid response body generation completely when
|
||||
# a conditional GET matches.
|
||||
#
|
||||
# Adapted from Michael Klishin's Merb implementation:
|
||||
# https://github.com/wycats/merb/blob/master/merb-core/lib/merb-core/rack/middleware/conditional_get.rb
|
||||
class ConditionalGet
|
||||
def initialize(app)
|
||||
@app = app
|
||||
end
|
||||
|
||||
def call(env)
|
||||
case env[REQUEST_METHOD]
|
||||
when "GET", "HEAD"
|
||||
status, headers, body = @app.call(env)
|
||||
headers = Utils::HeaderHash.new(headers)
|
||||
if status == 200 && fresh?(env, headers)
|
||||
status = 304
|
||||
headers.delete(CONTENT_TYPE)
|
||||
headers.delete(CONTENT_LENGTH)
|
||||
original_body = body
|
||||
body = Rack::BodyProxy.new([]) do
|
||||
original_body.close if original_body.respond_to?(:close)
|
||||
end
|
||||
end
|
||||
[status, headers, body]
|
||||
else
|
||||
@app.call(env)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fresh?(env, headers)
|
||||
modified_since = env['HTTP_IF_MODIFIED_SINCE']
|
||||
none_match = env['HTTP_IF_NONE_MATCH']
|
||||
|
||||
return false unless modified_since || none_match
|
||||
|
||||
success = true
|
||||
success &&= modified_since?(to_rfc2822(modified_since), headers) if modified_since
|
||||
success &&= etag_matches?(none_match, headers) if none_match
|
||||
success
|
||||
end
|
||||
|
||||
def etag_matches?(none_match, headers)
|
||||
etag = headers['ETag'] and etag == none_match
|
||||
end
|
||||
|
||||
def modified_since?(modified_since, headers)
|
||||
last_modified = to_rfc2822(headers['Last-Modified']) and
|
||||
modified_since and
|
||||
modified_since >= last_modified
|
||||
end
|
||||
|
||||
def to_rfc2822(since)
|
||||
# shortest possible valid date is the obsolete: 1 Nov 97 09:55 A
|
||||
# anything shorter is invalid, this avoids exceptions for common cases
|
||||
# most common being the empty string
|
||||
if since && since.length >= 16
|
||||
# NOTE: there is no trivial way to write this in a non execption way
|
||||
# _rfc2822 returns a hash but is not that usable
|
||||
Time.rfc2822(since) rescue nil
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,20 +0,0 @@
|
||||
module Rack
|
||||
# Rack::Config modifies the environment using the block given during
|
||||
# initialization.
|
||||
#
|
||||
# Example:
|
||||
# use Rack::Config do |env|
|
||||
# env['my-key'] = 'some-value'
|
||||
# end
|
||||
class Config
|
||||
def initialize(app, &block)
|
||||
@app = app
|
||||
@block = block
|
||||
end
|
||||
|
||||
def call(env)
|
||||
@block.call(env)
|
||||
@app.call(env)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,37 +0,0 @@
|
||||
require 'rack/utils'
|
||||
require 'rack/body_proxy'
|
||||
|
||||
module Rack
|
||||
|
||||
# Sets the Content-Length header on responses with fixed-length bodies.
|
||||
class ContentLength
|
||||
include Rack::Utils
|
||||
|
||||
def initialize(app)
|
||||
@app = app
|
||||
end
|
||||
|
||||
def call(env)
|
||||
status, headers, body = @app.call(env)
|
||||
headers = HeaderHash.new(headers)
|
||||
|
||||
if !STATUS_WITH_NO_ENTITY_BODY.include?(status.to_i) &&
|
||||
!headers[CONTENT_LENGTH] &&
|
||||
!headers['Transfer-Encoding'] &&
|
||||
body.respond_to?(:to_ary)
|
||||
|
||||
obody = body
|
||||
body, length = [], 0
|
||||
obody.each { |part| body << part; length += bytesize(part) }
|
||||
|
||||
body = BodyProxy.new(body) do
|
||||
obody.close if obody.respond_to?(:close)
|
||||
end
|
||||
|
||||
headers[CONTENT_LENGTH] = length.to_s
|
||||
end
|
||||
|
||||
[status, headers, body]
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,29 +0,0 @@
|
||||
require 'rack/utils'
|
||||
|
||||
module Rack
|
||||
|
||||
# Sets the Content-Type header on responses which don't have one.
|
||||
#
|
||||
# Builder Usage:
|
||||
# use Rack::ContentType, "text/plain"
|
||||
#
|
||||
# When no content type argument is provided, "text/html" is assumed.
|
||||
class ContentType
|
||||
include Rack::Utils
|
||||
|
||||
def initialize(app, content_type = "text/html")
|
||||
@app, @content_type = app, content_type
|
||||
end
|
||||
|
||||
def call(env)
|
||||
status, headers, body = @app.call(env)
|
||||
headers = Utils::HeaderHash.new(headers)
|
||||
|
||||
unless STATUS_WITH_NO_ENTITY_BODY.include?(status)
|
||||
headers[CONTENT_TYPE] ||= @content_type
|
||||
end
|
||||
|
||||
[status, headers, body]
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,154 +0,0 @@
|
||||
require "zlib"
|
||||
require "time" # for Time.httpdate
|
||||
require 'rack/utils'
|
||||
|
||||
module Rack
|
||||
# This middleware enables compression of http responses.
|
||||
#
|
||||
# Currently supported compression algorithms:
|
||||
#
|
||||
# * gzip
|
||||
# * deflate
|
||||
# * identity (no transformation)
|
||||
#
|
||||
# The middleware automatically detects when compression is supported
|
||||
# and allowed. For example no transformation is made when a cache
|
||||
# directive of 'no-transform' is present, or when the response status
|
||||
# code is one that doesn't allow an entity body.
|
||||
class Deflater
|
||||
##
|
||||
# Creates Rack::Deflater middleware.
|
||||
#
|
||||
# [app] rack app instance
|
||||
# [options] hash of deflater options, i.e.
|
||||
# 'if' - a lambda enabling / disabling deflation based on returned boolean value
|
||||
# e.g use Rack::Deflater, :if => lambda { |env, status, headers, body| body.length > 512 }
|
||||
# 'include' - a list of content types that should be compressed
|
||||
def initialize(app, options = {})
|
||||
@app = app
|
||||
|
||||
@condition = options[:if]
|
||||
@compressible_types = options[:include]
|
||||
end
|
||||
|
||||
def call(env)
|
||||
status, headers, body = @app.call(env)
|
||||
headers = Utils::HeaderHash.new(headers)
|
||||
|
||||
unless should_deflate?(env, status, headers, body)
|
||||
return [status, headers, body]
|
||||
end
|
||||
|
||||
request = Request.new(env)
|
||||
|
||||
encoding = Utils.select_best_encoding(%w(gzip deflate identity),
|
||||
request.accept_encoding)
|
||||
|
||||
# Set the Vary HTTP header.
|
||||
vary = headers["Vary"].to_s.split(",").map { |v| v.strip }
|
||||
unless vary.include?("*") || vary.include?("Accept-Encoding")
|
||||
headers["Vary"] = vary.push("Accept-Encoding").join(",")
|
||||
end
|
||||
|
||||
case encoding
|
||||
when "gzip"
|
||||
headers['Content-Encoding'] = "gzip"
|
||||
headers.delete(CONTENT_LENGTH)
|
||||
mtime = headers.key?("Last-Modified") ?
|
||||
Time.httpdate(headers["Last-Modified"]) : Time.now
|
||||
[status, headers, GzipStream.new(body, mtime)]
|
||||
when "deflate"
|
||||
headers['Content-Encoding'] = "deflate"
|
||||
headers.delete(CONTENT_LENGTH)
|
||||
[status, headers, DeflateStream.new(body)]
|
||||
when "identity"
|
||||
[status, headers, body]
|
||||
when nil
|
||||
message = "An acceptable encoding for the requested resource #{request.fullpath} could not be found."
|
||||
bp = Rack::BodyProxy.new([message]) { body.close if body.respond_to?(:close) }
|
||||
[406, {CONTENT_TYPE => "text/plain", CONTENT_LENGTH => message.length.to_s}, bp]
|
||||
end
|
||||
end
|
||||
|
||||
class GzipStream
|
||||
def initialize(body, mtime)
|
||||
@body = body
|
||||
@mtime = mtime
|
||||
@closed = false
|
||||
end
|
||||
|
||||
def each(&block)
|
||||
@writer = block
|
||||
gzip =::Zlib::GzipWriter.new(self)
|
||||
gzip.mtime = @mtime
|
||||
@body.each { |part|
|
||||
gzip.write(part)
|
||||
gzip.flush
|
||||
}
|
||||
ensure
|
||||
gzip.close
|
||||
@writer = nil
|
||||
end
|
||||
|
||||
def write(data)
|
||||
@writer.call(data)
|
||||
end
|
||||
|
||||
def close
|
||||
return if @closed
|
||||
@closed = true
|
||||
@body.close if @body.respond_to?(:close)
|
||||
end
|
||||
end
|
||||
|
||||
class DeflateStream
|
||||
DEFLATE_ARGS = [
|
||||
Zlib::DEFAULT_COMPRESSION,
|
||||
# drop the zlib header which causes both Safari and IE to choke
|
||||
-Zlib::MAX_WBITS,
|
||||
Zlib::DEF_MEM_LEVEL,
|
||||
Zlib::DEFAULT_STRATEGY
|
||||
]
|
||||
|
||||
def initialize(body)
|
||||
@body = body
|
||||
@closed = false
|
||||
end
|
||||
|
||||
def each
|
||||
deflator = ::Zlib::Deflate.new(*DEFLATE_ARGS)
|
||||
@body.each { |part| yield deflator.deflate(part, Zlib::SYNC_FLUSH) }
|
||||
yield deflator.finish
|
||||
nil
|
||||
ensure
|
||||
deflator.close
|
||||
end
|
||||
|
||||
def close
|
||||
return if @closed
|
||||
@closed = true
|
||||
@body.close if @body.respond_to?(:close)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def should_deflate?(env, status, headers, body)
|
||||
# Skip compressing empty entity body responses and responses with
|
||||
# no-transform set.
|
||||
if Utils::STATUS_WITH_NO_ENTITY_BODY.include?(status) ||
|
||||
headers[CACHE_CONTROL].to_s =~ /\bno-transform\b/ ||
|
||||
(headers['Content-Encoding'] && headers['Content-Encoding'] !~ /\bidentity\b/)
|
||||
return false
|
||||
end
|
||||
|
||||
# Skip if @compressible_types are given and does not include request's content type
|
||||
return false if @compressible_types && !(headers.has_key?('Content-Type') && @compressible_types.include?(headers['Content-Type'][/[^;]*/]))
|
||||
|
||||
# Skip if @condition lambda is given and evaluates to false
|
||||
return false if @condition && !@condition.call(env, status, headers, body)
|
||||
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,167 +0,0 @@
|
||||
require 'time'
|
||||
require 'rack/utils'
|
||||
require 'rack/mime'
|
||||
|
||||
module Rack
|
||||
# Rack::Directory serves entries below the +root+ given, according to the
|
||||
# path info of the Rack request. If a directory is found, the file's contents
|
||||
# will be presented in an html based index. If a file is found, the env will
|
||||
# be passed to the specified +app+.
|
||||
#
|
||||
# If +app+ is not specified, a Rack::File of the same +root+ will be used.
|
||||
|
||||
class Directory
|
||||
DIR_FILE = "<tr><td class='name'><a href='%s'>%s</a></td><td class='size'>%s</td><td class='type'>%s</td><td class='mtime'>%s</td></tr>"
|
||||
DIR_PAGE = <<-PAGE
|
||||
<html><head>
|
||||
<title>%s</title>
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
|
||||
<style type='text/css'>
|
||||
table { width:100%%; }
|
||||
.name { text-align:left; }
|
||||
.size, .mtime { text-align:right; }
|
||||
.type { width:11em; }
|
||||
.mtime { width:15em; }
|
||||
</style>
|
||||
</head><body>
|
||||
<h1>%s</h1>
|
||||
<hr />
|
||||
<table>
|
||||
<tr>
|
||||
<th class='name'>Name</th>
|
||||
<th class='size'>Size</th>
|
||||
<th class='type'>Type</th>
|
||||
<th class='mtime'>Last Modified</th>
|
||||
</tr>
|
||||
%s
|
||||
</table>
|
||||
<hr />
|
||||
</body></html>
|
||||
PAGE
|
||||
|
||||
attr_reader :files
|
||||
attr_accessor :root, :path
|
||||
|
||||
def initialize(root, app=nil)
|
||||
@root = F.expand_path(root)
|
||||
@app = app || Rack::File.new(@root)
|
||||
end
|
||||
|
||||
def call(env)
|
||||
dup._call(env)
|
||||
end
|
||||
|
||||
F = ::File
|
||||
|
||||
def _call(env)
|
||||
@env = env
|
||||
@script_name = env[SCRIPT_NAME]
|
||||
@path_info = Utils.unescape(env[PATH_INFO])
|
||||
|
||||
if forbidden = check_forbidden
|
||||
forbidden
|
||||
else
|
||||
@path = F.join(@root, @path_info)
|
||||
list_path
|
||||
end
|
||||
end
|
||||
|
||||
def check_forbidden
|
||||
return unless @path_info.include? ".."
|
||||
|
||||
body = "Forbidden\n"
|
||||
size = Rack::Utils.bytesize(body)
|
||||
return [403, {"Content-Type" => "text/plain",
|
||||
CONTENT_LENGTH => size.to_s,
|
||||
"X-Cascade" => "pass"}, [body]]
|
||||
end
|
||||
|
||||
def list_directory
|
||||
@files = [['../','Parent Directory','','','']]
|
||||
glob = F.join(@path, '*')
|
||||
|
||||
url_head = (@script_name.split('/') + @path_info.split('/')).map do |part|
|
||||
Rack::Utils.escape part
|
||||
end
|
||||
|
||||
Dir[glob].sort.each do |node|
|
||||
stat = stat(node)
|
||||
next unless stat
|
||||
basename = F.basename(node)
|
||||
ext = F.extname(node)
|
||||
|
||||
url = F.join(*url_head + [Rack::Utils.escape(basename)])
|
||||
size = stat.size
|
||||
type = stat.directory? ? 'directory' : Mime.mime_type(ext)
|
||||
size = stat.directory? ? '-' : filesize_format(size)
|
||||
mtime = stat.mtime.httpdate
|
||||
url << '/' if stat.directory?
|
||||
basename << '/' if stat.directory?
|
||||
|
||||
@files << [ url, basename, size, type, mtime ]
|
||||
end
|
||||
|
||||
return [ 200, { CONTENT_TYPE =>'text/html; charset=utf-8'}, self ]
|
||||
end
|
||||
|
||||
def stat(node, max = 10)
|
||||
F.stat(node)
|
||||
rescue Errno::ENOENT, Errno::ELOOP
|
||||
return nil
|
||||
end
|
||||
|
||||
# TODO: add correct response if not readable, not sure if 404 is the best
|
||||
# option
|
||||
def list_path
|
||||
@stat = F.stat(@path)
|
||||
|
||||
if @stat.readable?
|
||||
return @app.call(@env) if @stat.file?
|
||||
return list_directory if @stat.directory?
|
||||
else
|
||||
raise Errno::ENOENT, 'No such file or directory'
|
||||
end
|
||||
|
||||
rescue Errno::ENOENT, Errno::ELOOP
|
||||
return entity_not_found
|
||||
end
|
||||
|
||||
def entity_not_found
|
||||
body = "Entity not found: #{@path_info}\n"
|
||||
size = Rack::Utils.bytesize(body)
|
||||
return [404, {"Content-Type" => "text/plain",
|
||||
CONTENT_LENGTH => size.to_s,
|
||||
"X-Cascade" => "pass"}, [body]]
|
||||
end
|
||||
|
||||
def each
|
||||
show_path = Rack::Utils.escape_html(@path.sub(/^#{@root}/,''))
|
||||
files = @files.map{|f| DIR_FILE % DIR_FILE_escape(*f) }*"\n"
|
||||
page = DIR_PAGE % [ show_path, show_path , files ]
|
||||
page.each_line{|l| yield l }
|
||||
end
|
||||
|
||||
# Stolen from Ramaze
|
||||
|
||||
FILESIZE_FORMAT = [
|
||||
['%.1fT', 1 << 40],
|
||||
['%.1fG', 1 << 30],
|
||||
['%.1fM', 1 << 20],
|
||||
['%.1fK', 1 << 10],
|
||||
]
|
||||
|
||||
def filesize_format(int)
|
||||
FILESIZE_FORMAT.each do |format, size|
|
||||
return format % (int.to_f / size) if int >= size
|
||||
end
|
||||
|
||||
int.to_s + 'B'
|
||||
end
|
||||
|
||||
private
|
||||
# Assumes url is already escaped.
|
||||
def DIR_FILE_escape url, *html
|
||||
[url, *html.map { |e| Utils.escape_html(e) }]
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,73 +0,0 @@
|
||||
require 'digest/md5'
|
||||
|
||||
module Rack
|
||||
# Automatically sets the ETag header on all String bodies.
|
||||
#
|
||||
# The ETag header is skipped if ETag or Last-Modified headers are sent or if
|
||||
# a sendfile body (body.responds_to :to_path) is given (since such cases
|
||||
# should be handled by apache/nginx).
|
||||
#
|
||||
# On initialization, you can pass two parameters: a Cache-Control directive
|
||||
# used when Etag is absent and a directive when it is present. The first
|
||||
# defaults to nil, while the second defaults to "max-age=0, private, must-revalidate"
|
||||
class ETag
|
||||
ETAG_STRING = 'ETag'.freeze
|
||||
DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate".freeze
|
||||
|
||||
def initialize(app, no_cache_control = nil, cache_control = DEFAULT_CACHE_CONTROL)
|
||||
@app = app
|
||||
@cache_control = cache_control
|
||||
@no_cache_control = no_cache_control
|
||||
end
|
||||
|
||||
def call(env)
|
||||
status, headers, body = @app.call(env)
|
||||
|
||||
if etag_status?(status) && etag_body?(body) && !skip_caching?(headers)
|
||||
original_body = body
|
||||
digest, new_body = digest_body(body)
|
||||
body = Rack::BodyProxy.new(new_body) do
|
||||
original_body.close if original_body.respond_to?(:close)
|
||||
end
|
||||
headers[ETAG_STRING] = %(W/"#{digest}") if digest
|
||||
end
|
||||
|
||||
unless headers[CACHE_CONTROL]
|
||||
if digest
|
||||
headers[CACHE_CONTROL] = @cache_control if @cache_control
|
||||
else
|
||||
headers[CACHE_CONTROL] = @no_cache_control if @no_cache_control
|
||||
end
|
||||
end
|
||||
|
||||
[status, headers, body]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def etag_status?(status)
|
||||
status == 200 || status == 201
|
||||
end
|
||||
|
||||
def etag_body?(body)
|
||||
!body.respond_to?(:to_path)
|
||||
end
|
||||
|
||||
def skip_caching?(headers)
|
||||
(headers[CACHE_CONTROL] && headers[CACHE_CONTROL].include?('no-cache')) ||
|
||||
headers.key?(ETAG_STRING) || headers.key?('Last-Modified')
|
||||
end
|
||||
|
||||
def digest_body(body)
|
||||
parts = []
|
||||
digest = nil
|
||||
|
||||
body.each do |part|
|
||||
parts << part
|
||||
(digest ||= Digest::MD5.new) << part unless part.empty?
|
||||
end
|
||||
|
||||
[digest && digest.hexdigest, parts]
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,152 +0,0 @@
|
||||
require 'time'
|
||||
require 'rack/utils'
|
||||
require 'rack/mime'
|
||||
|
||||
module Rack
|
||||
# Rack::File serves files below the +root+ directory given, according to the
|
||||
# path info of the Rack request.
|
||||
# e.g. when Rack::File.new("/etc") is used, you can access 'passwd' file
|
||||
# as http://localhost:9292/passwd
|
||||
#
|
||||
# Handlers can detect if bodies are a Rack::File, and use mechanisms
|
||||
# like sendfile on the +path+.
|
||||
|
||||
class File
|
||||
ALLOWED_VERBS = %w[GET HEAD OPTIONS]
|
||||
ALLOW_HEADER = ALLOWED_VERBS.join(', ')
|
||||
|
||||
attr_accessor :root
|
||||
attr_accessor :path
|
||||
attr_accessor :cache_control
|
||||
|
||||
alias :to_path :path
|
||||
|
||||
def initialize(root, headers={}, default_mime = 'text/plain')
|
||||
@root = root
|
||||
@headers = headers
|
||||
@default_mime = default_mime
|
||||
end
|
||||
|
||||
def call(env)
|
||||
dup._call(env)
|
||||
end
|
||||
|
||||
F = ::File
|
||||
|
||||
def _call(env)
|
||||
unless ALLOWED_VERBS.include? env[REQUEST_METHOD]
|
||||
return fail(405, "Method Not Allowed", {'Allow' => ALLOW_HEADER})
|
||||
end
|
||||
|
||||
path_info = Utils.unescape(env[PATH_INFO])
|
||||
clean_path_info = Utils.clean_path_info(path_info)
|
||||
|
||||
@path = F.join(@root, clean_path_info)
|
||||
|
||||
available = begin
|
||||
F.file?(@path) && F.readable?(@path)
|
||||
rescue SystemCallError
|
||||
false
|
||||
end
|
||||
|
||||
if available
|
||||
serving(env)
|
||||
else
|
||||
fail(404, "File not found: #{path_info}")
|
||||
end
|
||||
end
|
||||
|
||||
def serving(env)
|
||||
if env["REQUEST_METHOD"] == "OPTIONS"
|
||||
return [200, {'Allow' => ALLOW_HEADER, CONTENT_LENGTH => '0'}, []]
|
||||
end
|
||||
last_modified = F.mtime(@path).httpdate
|
||||
return [304, {}, []] if env['HTTP_IF_MODIFIED_SINCE'] == last_modified
|
||||
|
||||
headers = { "Last-Modified" => last_modified }
|
||||
headers[CONTENT_TYPE] = mime_type if mime_type
|
||||
|
||||
# Set custom headers
|
||||
@headers.each { |field, content| headers[field] = content } if @headers
|
||||
|
||||
response = [ 200, headers, env[REQUEST_METHOD] == "HEAD" ? [] : self ]
|
||||
|
||||
size = filesize
|
||||
|
||||
ranges = Rack::Utils.byte_ranges(env, size)
|
||||
if ranges.nil? || ranges.length > 1
|
||||
# No ranges, or multiple ranges (which we don't support):
|
||||
# TODO: Support multiple byte-ranges
|
||||
response[0] = 200
|
||||
@range = 0..size-1
|
||||
elsif ranges.empty?
|
||||
# Unsatisfiable. Return error, and file size:
|
||||
response = fail(416, "Byte range unsatisfiable")
|
||||
response[1]["Content-Range"] = "bytes */#{size}"
|
||||
return response
|
||||
else
|
||||
# Partial content:
|
||||
@range = ranges[0]
|
||||
response[0] = 206
|
||||
response[1]["Content-Range"] = "bytes #{@range.begin}-#{@range.end}/#{size}"
|
||||
size = @range.end - @range.begin + 1
|
||||
end
|
||||
|
||||
response[2] = [response_body] unless response_body.nil?
|
||||
|
||||
response[1][CONTENT_LENGTH] = size.to_s
|
||||
response
|
||||
end
|
||||
|
||||
def each
|
||||
F.open(@path, "rb") do |file|
|
||||
file.seek(@range.begin)
|
||||
remaining_len = @range.end-@range.begin+1
|
||||
while remaining_len > 0
|
||||
part = file.read([8192, remaining_len].min)
|
||||
break unless part
|
||||
remaining_len -= part.length
|
||||
|
||||
yield part
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fail(status, body, headers = {})
|
||||
body += "\n"
|
||||
[
|
||||
status,
|
||||
{
|
||||
CONTENT_TYPE => "text/plain",
|
||||
CONTENT_LENGTH => body.size.to_s,
|
||||
"X-Cascade" => "pass"
|
||||
}.merge!(headers),
|
||||
[body]
|
||||
]
|
||||
end
|
||||
|
||||
# The MIME type for the contents of the file located at @path
|
||||
def mime_type
|
||||
Mime.mime_type(F.extname(@path), @default_mime)
|
||||
end
|
||||
|
||||
def filesize
|
||||
# If response_body is present, use its size.
|
||||
return Rack::Utils.bytesize(response_body) if response_body
|
||||
|
||||
# We check via File::size? whether this file provides size info
|
||||
# via stat (e.g. /proc files often don't), otherwise we have to
|
||||
# figure it out by reading the whole file into memory.
|
||||
F.size?(@path) || Utils.bytesize(F.read(@path))
|
||||
end
|
||||
|
||||
# By default, the response body for file requests is nil.
|
||||
# In this case, the response body will be generated later
|
||||
# from the file at @path
|
||||
def response_body
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,109 +0,0 @@
|
||||
module Rack
|
||||
# *Handlers* connect web servers with Rack.
|
||||
#
|
||||
# Rack includes Handlers for Thin, WEBrick, FastCGI, CGI, SCGI
|
||||
# and LiteSpeed.
|
||||
#
|
||||
# Handlers usually are activated by calling <tt>MyHandler.run(myapp)</tt>.
|
||||
# A second optional hash can be passed to include server-specific
|
||||
# configuration.
|
||||
module Handler
|
||||
def self.get(server)
|
||||
return unless server
|
||||
server = server.to_s
|
||||
|
||||
unless @handlers.include? server
|
||||
load_error = try_require('rack/handler', server)
|
||||
end
|
||||
|
||||
if klass = @handlers[server]
|
||||
klass.split("::").inject(Object) { |o, x| o.const_get(x) }
|
||||
else
|
||||
const_get(server, false)
|
||||
end
|
||||
|
||||
rescue NameError => name_error
|
||||
raise load_error || name_error
|
||||
end
|
||||
|
||||
# Select first available Rack handler given an `Array` of server names.
|
||||
# Raises `LoadError` if no handler was found.
|
||||
#
|
||||
# > pick ['thin', 'webrick']
|
||||
# => Rack::Handler::WEBrick
|
||||
def self.pick(server_names)
|
||||
server_names = Array(server_names)
|
||||
server_names.each do |server_name|
|
||||
begin
|
||||
return get(server_name.to_s)
|
||||
rescue LoadError, NameError
|
||||
end
|
||||
end
|
||||
|
||||
raise LoadError, "Couldn't find handler for: #{server_names.join(', ')}."
|
||||
end
|
||||
|
||||
def self.default(options = {})
|
||||
# Guess.
|
||||
if ENV.include?("PHP_FCGI_CHILDREN")
|
||||
# We already speak FastCGI
|
||||
options.delete :File
|
||||
options.delete :Port
|
||||
|
||||
Rack::Handler::FastCGI
|
||||
elsif ENV.include?(REQUEST_METHOD)
|
||||
Rack::Handler::CGI
|
||||
elsif ENV.include?("RACK_HANDLER")
|
||||
self.get(ENV["RACK_HANDLER"])
|
||||
else
|
||||
pick ['thin', 'puma', 'webrick']
|
||||
end
|
||||
end
|
||||
|
||||
# Transforms server-name constants to their canonical form as filenames,
|
||||
# then tries to require them but silences the LoadError if not found
|
||||
#
|
||||
# Naming convention:
|
||||
#
|
||||
# Foo # => 'foo'
|
||||
# FooBar # => 'foo_bar.rb'
|
||||
# FooBAR # => 'foobar.rb'
|
||||
# FOObar # => 'foobar.rb'
|
||||
# FOOBAR # => 'foobar.rb'
|
||||
# FooBarBaz # => 'foo_bar_baz.rb'
|
||||
def self.try_require(prefix, const_name)
|
||||
file = const_name.gsub(/^[A-Z]+/) { |pre| pre.downcase }.
|
||||
gsub(/[A-Z]+[^A-Z]/, '_\&').downcase
|
||||
|
||||
require(::File.join(prefix, file))
|
||||
nil
|
||||
rescue LoadError => error
|
||||
error
|
||||
end
|
||||
|
||||
def self.register(server, klass)
|
||||
@handlers ||= {}
|
||||
@handlers[server.to_s] = klass.to_s
|
||||
end
|
||||
|
||||
autoload :CGI, "rack/handler/cgi"
|
||||
autoload :FastCGI, "rack/handler/fastcgi"
|
||||
autoload :Mongrel, "rack/handler/mongrel"
|
||||
autoload :EventedMongrel, "rack/handler/evented_mongrel"
|
||||
autoload :SwiftipliedMongrel, "rack/handler/swiftiplied_mongrel"
|
||||
autoload :WEBrick, "rack/handler/webrick"
|
||||
autoload :LSWS, "rack/handler/lsws"
|
||||
autoload :SCGI, "rack/handler/scgi"
|
||||
autoload :Thin, "rack/handler/thin"
|
||||
|
||||
register 'cgi', 'Rack::Handler::CGI'
|
||||
register 'fastcgi', 'Rack::Handler::FastCGI'
|
||||
register 'mongrel', 'Rack::Handler::Mongrel'
|
||||
register 'emongrel', 'Rack::Handler::EventedMongrel'
|
||||
register 'smongrel', 'Rack::Handler::SwiftipliedMongrel'
|
||||
register 'webrick', 'Rack::Handler::WEBrick'
|
||||
register 'lsws', 'Rack::Handler::LSWS'
|
||||
register 'scgi', 'Rack::Handler::SCGI'
|
||||
register 'thin', 'Rack::Handler::Thin'
|
||||
end
|
||||
end
|
||||
@@ -1,61 +0,0 @@
|
||||
require 'rack/content_length'
|
||||
require 'rack/rewindable_input'
|
||||
|
||||
module Rack
|
||||
module Handler
|
||||
class CGI
|
||||
def self.run(app, options=nil)
|
||||
$stdin.binmode
|
||||
serve app
|
||||
end
|
||||
|
||||
def self.serve(app)
|
||||
env = ENV.to_hash
|
||||
env.delete "HTTP_CONTENT_LENGTH"
|
||||
|
||||
env["SCRIPT_NAME"] = "" if env["SCRIPT_NAME"] == "/"
|
||||
|
||||
env.update({"rack.version" => Rack::VERSION,
|
||||
"rack.input" => Rack::RewindableInput.new($stdin),
|
||||
"rack.errors" => $stderr,
|
||||
|
||||
"rack.multithread" => false,
|
||||
"rack.multiprocess" => true,
|
||||
"rack.run_once" => true,
|
||||
|
||||
"rack.url_scheme" => ["yes", "on", "1"].include?(ENV["HTTPS"]) ? "https" : "http"
|
||||
})
|
||||
|
||||
env[QUERY_STRING] ||= ""
|
||||
env["HTTP_VERSION"] ||= env["SERVER_PROTOCOL"]
|
||||
env["REQUEST_PATH"] ||= "/"
|
||||
|
||||
status, headers, body = app.call(env)
|
||||
begin
|
||||
send_headers status, headers
|
||||
send_body body
|
||||
ensure
|
||||
body.close if body.respond_to? :close
|
||||
end
|
||||
end
|
||||
|
||||
def self.send_headers(status, headers)
|
||||
$stdout.print "Status: #{status}\r\n"
|
||||
headers.each { |k, vs|
|
||||
vs.split("\n").each { |v|
|
||||
$stdout.print "#{k}: #{v}\r\n"
|
||||
}
|
||||
}
|
||||
$stdout.print "\r\n"
|
||||
$stdout.flush
|
||||
end
|
||||
|
||||
def self.send_body(body)
|
||||
body.each { |part|
|
||||
$stdout.print part
|
||||
$stdout.flush
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,8 +0,0 @@
|
||||
require 'swiftcore/evented_mongrel'
|
||||
|
||||
module Rack
|
||||
module Handler
|
||||
class EventedMongrel < Handler::Mongrel
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,101 +0,0 @@
|
||||
require 'fcgi'
|
||||
require 'socket'
|
||||
require 'rack/content_length'
|
||||
require 'rack/rewindable_input'
|
||||
|
||||
if defined? FCGI::Stream
|
||||
class FCGI::Stream
|
||||
alias _rack_read_without_buffer read
|
||||
|
||||
def read(n, buffer=nil)
|
||||
buf = _rack_read_without_buffer n
|
||||
buffer.replace(buf.to_s) if buffer
|
||||
buf
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module Rack
|
||||
module Handler
|
||||
class FastCGI
|
||||
def self.run(app, options={})
|
||||
if options[:File]
|
||||
STDIN.reopen(UNIXServer.new(options[:File]))
|
||||
elsif options[:Port]
|
||||
STDIN.reopen(TCPServer.new(options[:Host], options[:Port]))
|
||||
end
|
||||
FCGI.each { |request|
|
||||
serve request, app
|
||||
}
|
||||
end
|
||||
|
||||
def self.valid_options
|
||||
environment = ENV['RACK_ENV'] || 'development'
|
||||
default_host = environment == 'development' ? 'localhost' : '0.0.0.0'
|
||||
|
||||
{
|
||||
"Host=HOST" => "Hostname to listen on (default: #{default_host})",
|
||||
"Port=PORT" => "Port to listen on (default: 8080)",
|
||||
"File=PATH" => "Creates a Domain socket at PATH instead of a TCP socket. Ignores Host and Port if set.",
|
||||
}
|
||||
end
|
||||
|
||||
def self.serve(request, app)
|
||||
env = request.env
|
||||
env.delete "HTTP_CONTENT_LENGTH"
|
||||
|
||||
env["SCRIPT_NAME"] = "" if env["SCRIPT_NAME"] == "/"
|
||||
|
||||
rack_input = RewindableInput.new(request.in)
|
||||
|
||||
env.update({"rack.version" => Rack::VERSION,
|
||||
"rack.input" => rack_input,
|
||||
"rack.errors" => request.err,
|
||||
|
||||
"rack.multithread" => false,
|
||||
"rack.multiprocess" => true,
|
||||
"rack.run_once" => false,
|
||||
|
||||
"rack.url_scheme" => ["yes", "on", "1"].include?(env["HTTPS"]) ? "https" : "http"
|
||||
})
|
||||
|
||||
env[QUERY_STRING] ||= ""
|
||||
env["HTTP_VERSION"] ||= env["SERVER_PROTOCOL"]
|
||||
env["REQUEST_PATH"] ||= "/"
|
||||
env.delete "CONTENT_TYPE" if env["CONTENT_TYPE"] == ""
|
||||
env.delete "CONTENT_LENGTH" if env["CONTENT_LENGTH"] == ""
|
||||
|
||||
begin
|
||||
status, headers, body = app.call(env)
|
||||
begin
|
||||
send_headers request.out, status, headers
|
||||
send_body request.out, body
|
||||
ensure
|
||||
body.close if body.respond_to? :close
|
||||
end
|
||||
ensure
|
||||
rack_input.close
|
||||
request.finish
|
||||
end
|
||||
end
|
||||
|
||||
def self.send_headers(out, status, headers)
|
||||
out.print "Status: #{status}\r\n"
|
||||
headers.each { |k, vs|
|
||||
vs.split("\n").each { |v|
|
||||
out.print "#{k}: #{v}\r\n"
|
||||
}
|
||||
}
|
||||
out.print "\r\n"
|
||||
out.flush
|
||||
end
|
||||
|
||||
def self.send_body(out, body)
|
||||
body.each { |part|
|
||||
out.print part
|
||||
out.flush
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,61 +0,0 @@
|
||||
require 'lsapi'
|
||||
require 'rack/content_length'
|
||||
require 'rack/rewindable_input'
|
||||
|
||||
module Rack
|
||||
module Handler
|
||||
class LSWS
|
||||
def self.run(app, options=nil)
|
||||
while LSAPI.accept != nil
|
||||
serve app
|
||||
end
|
||||
end
|
||||
def self.serve(app)
|
||||
env = ENV.to_hash
|
||||
env.delete "HTTP_CONTENT_LENGTH"
|
||||
env["SCRIPT_NAME"] = "" if env["SCRIPT_NAME"] == "/"
|
||||
|
||||
rack_input = RewindableInput.new($stdin.read.to_s)
|
||||
|
||||
env.update(
|
||||
"rack.version" => Rack::VERSION,
|
||||
"rack.input" => rack_input,
|
||||
"rack.errors" => $stderr,
|
||||
"rack.multithread" => false,
|
||||
"rack.multiprocess" => true,
|
||||
"rack.run_once" => false,
|
||||
"rack.url_scheme" => ["yes", "on", "1"].include?(ENV["HTTPS"]) ? "https" : "http"
|
||||
)
|
||||
|
||||
env[QUERY_STRING] ||= ""
|
||||
env["HTTP_VERSION"] ||= env["SERVER_PROTOCOL"]
|
||||
env["REQUEST_PATH"] ||= "/"
|
||||
status, headers, body = app.call(env)
|
||||
begin
|
||||
send_headers status, headers
|
||||
send_body body
|
||||
ensure
|
||||
body.close if body.respond_to? :close
|
||||
end
|
||||
ensure
|
||||
rack_input.close
|
||||
end
|
||||
def self.send_headers(status, headers)
|
||||
print "Status: #{status}\r\n"
|
||||
headers.each { |k, vs|
|
||||
vs.split("\n").each { |v|
|
||||
print "#{k}: #{v}\r\n"
|
||||
}
|
||||
}
|
||||
print "\r\n"
|
||||
STDOUT.flush
|
||||
end
|
||||
def self.send_body(body)
|
||||
body.each { |part|
|
||||
print part
|
||||
STDOUT.flush
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,106 +0,0 @@
|
||||
require 'mongrel'
|
||||
require 'stringio'
|
||||
require 'rack/content_length'
|
||||
require 'rack/chunked'
|
||||
|
||||
module Rack
|
||||
module Handler
|
||||
class Mongrel < ::Mongrel::HttpHandler
|
||||
def self.run(app, options={})
|
||||
environment = ENV['RACK_ENV'] || 'development'
|
||||
default_host = environment == 'development' ? 'localhost' : '0.0.0.0'
|
||||
|
||||
server = ::Mongrel::HttpServer.new(
|
||||
options[:Host] || default_host,
|
||||
options[:Port] || 8080,
|
||||
options[:num_processors] || 950,
|
||||
options[:throttle] || 0,
|
||||
options[:timeout] || 60)
|
||||
# Acts like Rack::URLMap, utilizing Mongrel's own path finding methods.
|
||||
# Use is similar to #run, replacing the app argument with a hash of
|
||||
# { path=>app, ... } or an instance of Rack::URLMap.
|
||||
if options[:map]
|
||||
if app.is_a? Hash
|
||||
app.each do |path, appl|
|
||||
path = '/'+path unless path[0] == ?/
|
||||
server.register(path, Rack::Handler::Mongrel.new(appl))
|
||||
end
|
||||
elsif app.is_a? URLMap
|
||||
app.instance_variable_get(:@mapping).each do |(host, path, appl)|
|
||||
next if !host.nil? && !options[:Host].nil? && options[:Host] != host
|
||||
path = '/'+path unless path[0] == ?/
|
||||
server.register(path, Rack::Handler::Mongrel.new(appl))
|
||||
end
|
||||
else
|
||||
raise ArgumentError, "first argument should be a Hash or URLMap"
|
||||
end
|
||||
else
|
||||
server.register('/', Rack::Handler::Mongrel.new(app))
|
||||
end
|
||||
yield server if block_given?
|
||||
server.run.join
|
||||
end
|
||||
|
||||
def self.valid_options
|
||||
environment = ENV['RACK_ENV'] || 'development'
|
||||
default_host = environment == 'development' ? 'localhost' : '0.0.0.0'
|
||||
|
||||
{
|
||||
"Host=HOST" => "Hostname to listen on (default: #{default_host})",
|
||||
"Port=PORT" => "Port to listen on (default: 8080)",
|
||||
"Processors=N" => "Number of concurrent processors to accept (default: 950)",
|
||||
"Timeout=N" => "Time before a request is dropped for inactivity (default: 60)",
|
||||
"Throttle=N" => "Throttle time between socket.accept calls in hundredths of a second (default: 0)",
|
||||
}
|
||||
end
|
||||
|
||||
def initialize(app)
|
||||
@app = app
|
||||
end
|
||||
|
||||
def process(request, response)
|
||||
env = Hash[request.params]
|
||||
env.delete "HTTP_CONTENT_TYPE"
|
||||
env.delete "HTTP_CONTENT_LENGTH"
|
||||
|
||||
env["SCRIPT_NAME"] = "" if env["SCRIPT_NAME"] == "/"
|
||||
|
||||
rack_input = request.body || StringIO.new('')
|
||||
rack_input.set_encoding(Encoding::BINARY) if rack_input.respond_to?(:set_encoding)
|
||||
|
||||
env.update({"rack.version" => Rack::VERSION,
|
||||
"rack.input" => rack_input,
|
||||
"rack.errors" => $stderr,
|
||||
|
||||
"rack.multithread" => true,
|
||||
"rack.multiprocess" => false, # ???
|
||||
"rack.run_once" => false,
|
||||
|
||||
"rack.url_scheme" => ["yes", "on", "1"].include?(env["HTTPS"]) ? "https" : "http"
|
||||
})
|
||||
env[QUERY_STRING] ||= ""
|
||||
|
||||
status, headers, body = @app.call(env)
|
||||
|
||||
begin
|
||||
response.status = status.to_i
|
||||
response.send_status(nil)
|
||||
|
||||
headers.each { |k, vs|
|
||||
vs.split("\n").each { |v|
|
||||
response.header[k] = v
|
||||
}
|
||||
}
|
||||
response.send_header
|
||||
|
||||
body.each { |part|
|
||||
response.write part
|
||||
response.socket.flush
|
||||
}
|
||||
ensure
|
||||
body.close if body.respond_to? :close
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,70 +0,0 @@
|
||||
require 'scgi'
|
||||
require 'stringio'
|
||||
require 'rack/content_length'
|
||||
require 'rack/chunked'
|
||||
|
||||
module Rack
|
||||
module Handler
|
||||
class SCGI < ::SCGI::Processor
|
||||
attr_accessor :app
|
||||
|
||||
def self.run(app, options=nil)
|
||||
options[:Socket] = UNIXServer.new(options[:File]) if options[:File]
|
||||
new(options.merge(:app=>app,
|
||||
:host=>options[:Host],
|
||||
:port=>options[:Port],
|
||||
:socket=>options[:Socket])).listen
|
||||
end
|
||||
|
||||
def self.valid_options
|
||||
environment = ENV['RACK_ENV'] || 'development'
|
||||
default_host = environment == 'development' ? 'localhost' : '0.0.0.0'
|
||||
|
||||
{
|
||||
"Host=HOST" => "Hostname to listen on (default: #{default_host})",
|
||||
"Port=PORT" => "Port to listen on (default: 8080)",
|
||||
}
|
||||
end
|
||||
|
||||
def initialize(settings = {})
|
||||
@app = settings[:app]
|
||||
super(settings)
|
||||
end
|
||||
|
||||
def process_request(request, input_body, socket)
|
||||
env = Hash[request]
|
||||
env.delete "HTTP_CONTENT_TYPE"
|
||||
env.delete "HTTP_CONTENT_LENGTH"
|
||||
env["REQUEST_PATH"], env[QUERY_STRING] = env["REQUEST_URI"].split('?', 2)
|
||||
env["HTTP_VERSION"] ||= env["SERVER_PROTOCOL"]
|
||||
env[PATH_INFO] = env["REQUEST_PATH"]
|
||||
env["QUERY_STRING"] ||= ""
|
||||
env["SCRIPT_NAME"] = ""
|
||||
|
||||
rack_input = StringIO.new(input_body)
|
||||
rack_input.set_encoding(Encoding::BINARY) if rack_input.respond_to?(:set_encoding)
|
||||
|
||||
env.update({"rack.version" => Rack::VERSION,
|
||||
"rack.input" => rack_input,
|
||||
"rack.errors" => $stderr,
|
||||
"rack.multithread" => true,
|
||||
"rack.multiprocess" => true,
|
||||
"rack.run_once" => false,
|
||||
|
||||
"rack.url_scheme" => ["yes", "on", "1"].include?(env["HTTPS"]) ? "https" : "http"
|
||||
})
|
||||
status, headers, body = app.call(env)
|
||||
begin
|
||||
socket.write("Status: #{status}\r\n")
|
||||
headers.each do |k, vs|
|
||||
vs.split("\n").each { |v| socket.write("#{k}: #{v}\r\n")}
|
||||
end
|
||||
socket.write("\r\n")
|
||||
body.each {|s| socket.write(s)}
|
||||
ensure
|
||||
body.close if body.respond_to? :close
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,8 +0,0 @@
|
||||
require 'swiftcore/swiftiplied_mongrel'
|
||||
|
||||
module Rack
|
||||
module Handler
|
||||
class SwiftipliedMongrel < Handler::Mongrel
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,33 +0,0 @@
|
||||
require "thin"
|
||||
require "rack/content_length"
|
||||
require "rack/chunked"
|
||||
|
||||
module Rack
|
||||
module Handler
|
||||
class Thin
|
||||
def self.run(app, options={})
|
||||
environment = ENV['RACK_ENV'] || 'development'
|
||||
default_host = environment == 'development' ? 'localhost' : '0.0.0.0'
|
||||
|
||||
host = options.delete(:Host) || default_host
|
||||
port = options.delete(:Port) || 8080
|
||||
args = [host, port, app, options]
|
||||
# Thin versions below 0.8.0 do not support additional options
|
||||
args.pop if ::Thin::VERSION::MAJOR < 1 && ::Thin::VERSION::MINOR < 8
|
||||
server = ::Thin::Server.new(*args)
|
||||
yield server if block_given?
|
||||
server.start
|
||||
end
|
||||
|
||||
def self.valid_options
|
||||
environment = ENV['RACK_ENV'] || 'development'
|
||||
default_host = environment == 'development' ? 'localhost' : '0.0.0.0'
|
||||
|
||||
{
|
||||
"Host=HOST" => "Hostname to listen on (default: #{default_host})",
|
||||
"Port=PORT" => "Port to listen on (default: 8080)",
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,122 +0,0 @@
|
||||
require 'webrick'
|
||||
require 'stringio'
|
||||
require 'rack/content_length'
|
||||
|
||||
# This monkey patch allows for applications to perform their own chunking
|
||||
# through WEBrick::HTTPResponse iff rack is set to true.
|
||||
class WEBrick::HTTPResponse
|
||||
attr_accessor :rack
|
||||
|
||||
alias _rack_setup_header setup_header
|
||||
def setup_header
|
||||
app_chunking = rack && @header['transfer-encoding'] == 'chunked'
|
||||
|
||||
@chunked = app_chunking if app_chunking
|
||||
|
||||
_rack_setup_header
|
||||
|
||||
@chunked = false if app_chunking
|
||||
end
|
||||
end
|
||||
|
||||
module Rack
|
||||
module Handler
|
||||
class WEBrick < ::WEBrick::HTTPServlet::AbstractServlet
|
||||
def self.run(app, options={})
|
||||
environment = ENV['RACK_ENV'] || 'development'
|
||||
default_host = environment == 'development' ? 'localhost' : '0.0.0.0'
|
||||
|
||||
options[:BindAddress] = options.delete(:Host) || default_host
|
||||
options[:Port] ||= 8080
|
||||
@server = ::WEBrick::HTTPServer.new(options)
|
||||
@server.mount "/", Rack::Handler::WEBrick, app
|
||||
yield @server if block_given?
|
||||
@server.start
|
||||
end
|
||||
|
||||
def self.valid_options
|
||||
environment = ENV['RACK_ENV'] || 'development'
|
||||
default_host = environment == 'development' ? 'localhost' : '0.0.0.0'
|
||||
|
||||
{
|
||||
"Host=HOST" => "Hostname to listen on (default: #{default_host})",
|
||||
"Port=PORT" => "Port to listen on (default: 8080)",
|
||||
}
|
||||
end
|
||||
|
||||
def self.shutdown
|
||||
@server.shutdown
|
||||
@server = nil
|
||||
end
|
||||
|
||||
def initialize(server, app)
|
||||
super server
|
||||
@app = app
|
||||
end
|
||||
|
||||
def service(req, res)
|
||||
res.rack = true
|
||||
env = req.meta_vars
|
||||
env.delete_if { |k, v| v.nil? }
|
||||
|
||||
rack_input = StringIO.new(req.body.to_s)
|
||||
rack_input.set_encoding(Encoding::BINARY) if rack_input.respond_to?(:set_encoding)
|
||||
|
||||
env.update({"rack.version" => Rack::VERSION,
|
||||
"rack.input" => rack_input,
|
||||
"rack.errors" => $stderr,
|
||||
|
||||
"rack.multithread" => true,
|
||||
"rack.multiprocess" => false,
|
||||
"rack.run_once" => false,
|
||||
|
||||
"rack.url_scheme" => ["yes", "on", "1"].include?(env["HTTPS"]) ? "https" : "http",
|
||||
|
||||
"rack.hijack?" => true,
|
||||
"rack.hijack" => lambda { raise NotImplementedError, "only partial hijack is supported."},
|
||||
"rack.hijack_io" => nil,
|
||||
})
|
||||
|
||||
env["HTTP_VERSION"] ||= env["SERVER_PROTOCOL"]
|
||||
env[QUERY_STRING] ||= ""
|
||||
unless env[PATH_INFO] == ""
|
||||
path, n = req.request_uri.path, env["SCRIPT_NAME"].length
|
||||
env[PATH_INFO] = path[n, path.length-n]
|
||||
end
|
||||
env["REQUEST_PATH"] ||= [env["SCRIPT_NAME"], env[PATH_INFO]].join
|
||||
|
||||
status, headers, body = @app.call(env)
|
||||
begin
|
||||
res.status = status.to_i
|
||||
headers.each { |k, vs|
|
||||
next if k.downcase == "rack.hijack"
|
||||
|
||||
if k.downcase == "set-cookie"
|
||||
res.cookies.concat vs.split("\n")
|
||||
else
|
||||
# Since WEBrick won't accept repeated headers,
|
||||
# merge the values per RFC 1945 section 4.2.
|
||||
res[k] = vs.split("\n").join(", ")
|
||||
end
|
||||
}
|
||||
|
||||
io_lambda = headers["rack.hijack"]
|
||||
if io_lambda
|
||||
rd, wr = IO.pipe
|
||||
res.body = rd
|
||||
res.chunked = true
|
||||
io_lambda.call wr
|
||||
elsif body.respond_to?(:to_path)
|
||||
res.body = ::File.open(body.to_path, 'rb')
|
||||
else
|
||||
body.each { |part|
|
||||
res.body << part
|
||||
}
|
||||
end
|
||||
ensure
|
||||
body.close if body.respond_to? :close
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,27 +0,0 @@
|
||||
require 'rack/body_proxy'
|
||||
|
||||
module Rack
|
||||
|
||||
class Head
|
||||
# Rack::Head returns an empty body for all HEAD requests. It leaves
|
||||
# all other requests unchanged.
|
||||
def initialize(app)
|
||||
@app = app
|
||||
end
|
||||
|
||||
def call(env)
|
||||
status, headers, body = @app.call(env)
|
||||
|
||||
if env[REQUEST_METHOD] == HEAD
|
||||
[
|
||||
status, headers, Rack::BodyProxy.new([]) do
|
||||
body.close if body.respond_to? :close
|
||||
end
|
||||
]
|
||||
else
|
||||
[status, headers, body]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
@@ -1,760 +0,0 @@
|
||||
require 'rack/utils'
|
||||
require 'forwardable'
|
||||
|
||||
module Rack
|
||||
# Rack::Lint validates your application and the requests and
|
||||
# responses according to the Rack spec.
|
||||
|
||||
class Lint
|
||||
def initialize(app)
|
||||
@app = app
|
||||
@content_length = nil
|
||||
end
|
||||
|
||||
# :stopdoc:
|
||||
|
||||
class LintError < RuntimeError; end
|
||||
module Assertion
|
||||
def assert(message, &block)
|
||||
unless block.call
|
||||
raise LintError, message
|
||||
end
|
||||
end
|
||||
end
|
||||
include Assertion
|
||||
|
||||
## This specification aims to formalize the Rack protocol. You
|
||||
## can (and should) use Rack::Lint to enforce it.
|
||||
##
|
||||
## When you develop middleware, be sure to add a Lint before and
|
||||
## after to catch all mistakes.
|
||||
|
||||
## = Rack applications
|
||||
|
||||
## A Rack application is a Ruby object (not a class) that
|
||||
## responds to +call+.
|
||||
def call(env=nil)
|
||||
dup._call(env)
|
||||
end
|
||||
|
||||
def _call(env)
|
||||
## It takes exactly one argument, the *environment*
|
||||
assert("No env given") { env }
|
||||
check_env env
|
||||
|
||||
env['rack.input'] = InputWrapper.new(env['rack.input'])
|
||||
env['rack.errors'] = ErrorWrapper.new(env['rack.errors'])
|
||||
|
||||
## and returns an Array of exactly three values:
|
||||
status, headers, @body = @app.call(env)
|
||||
## The *status*,
|
||||
check_status status
|
||||
## the *headers*,
|
||||
check_headers headers
|
||||
|
||||
check_hijack_response headers, env
|
||||
|
||||
## and the *body*.
|
||||
check_content_type status, headers
|
||||
check_content_length status, headers
|
||||
@head_request = env[REQUEST_METHOD] == "HEAD"
|
||||
[status, headers, self]
|
||||
end
|
||||
|
||||
## == The Environment
|
||||
def check_env(env)
|
||||
## The environment must be an instance of Hash that includes
|
||||
## CGI-like headers. The application is free to modify the
|
||||
## environment.
|
||||
assert("env #{env.inspect} is not a Hash, but #{env.class}") {
|
||||
env.kind_of? Hash
|
||||
}
|
||||
|
||||
##
|
||||
## The environment is required to include these variables
|
||||
## (adopted from PEP333), except when they'd be empty, but see
|
||||
## below.
|
||||
|
||||
## <tt>REQUEST_METHOD</tt>:: The HTTP request method, such as
|
||||
## "GET" or "POST". This cannot ever
|
||||
## be an empty string, and so is
|
||||
## always required.
|
||||
|
||||
## <tt>SCRIPT_NAME</tt>:: The initial portion of the request
|
||||
## URL's "path" that corresponds to the
|
||||
## application object, so that the
|
||||
## application knows its virtual
|
||||
## "location". This may be an empty
|
||||
## string, if the application corresponds
|
||||
## to the "root" of the server.
|
||||
|
||||
## <tt>PATH_INFO</tt>:: The remainder of the request URL's
|
||||
## "path", designating the virtual
|
||||
## "location" of the request's target
|
||||
## within the application. This may be an
|
||||
## empty string, if the request URL targets
|
||||
## the application root and does not have a
|
||||
## trailing slash. This value may be
|
||||
## percent-encoded when I originating from
|
||||
## a URL.
|
||||
|
||||
## <tt>QUERY_STRING</tt>:: The portion of the request URL that
|
||||
## follows the <tt>?</tt>, if any. May be
|
||||
## empty, but is always required!
|
||||
|
||||
## <tt>SERVER_NAME</tt>, <tt>SERVER_PORT</tt>::
|
||||
## When combined with <tt>SCRIPT_NAME</tt> and
|
||||
## <tt>PATH_INFO</tt>, these variables can be
|
||||
## used to complete the URL. Note, however,
|
||||
## that <tt>HTTP_HOST</tt>, if present,
|
||||
## should be used in preference to
|
||||
## <tt>SERVER_NAME</tt> for reconstructing
|
||||
## the request URL.
|
||||
## <tt>SERVER_NAME</tt> and <tt>SERVER_PORT</tt>
|
||||
## can never be empty strings, and so
|
||||
## are always required.
|
||||
|
||||
## <tt>HTTP_</tt> Variables:: Variables corresponding to the
|
||||
## client-supplied HTTP request
|
||||
## headers (i.e., variables whose
|
||||
## names begin with <tt>HTTP_</tt>). The
|
||||
## presence or absence of these
|
||||
## variables should correspond with
|
||||
## the presence or absence of the
|
||||
## appropriate HTTP header in the
|
||||
## request. See
|
||||
## <a href="https://tools.ietf.org/html/rfc3875#section-4.1.18">
|
||||
## RFC3875 section 4.1.18</a> for
|
||||
## specific behavior.
|
||||
|
||||
## In addition to this, the Rack environment must include these
|
||||
## Rack-specific variables:
|
||||
|
||||
## <tt>rack.version</tt>:: The Array representing this version of Rack
|
||||
## See Rack::VERSION, that corresponds to
|
||||
## the version of this SPEC.
|
||||
|
||||
## <tt>rack.url_scheme</tt>:: +http+ or +https+, depending on the
|
||||
## request URL.
|
||||
|
||||
## <tt>rack.input</tt>:: See below, the input stream.
|
||||
|
||||
## <tt>rack.errors</tt>:: See below, the error stream.
|
||||
|
||||
## <tt>rack.multithread</tt>:: true if the application object may be
|
||||
## simultaneously invoked by another thread
|
||||
## in the same process, false otherwise.
|
||||
|
||||
## <tt>rack.multiprocess</tt>:: true if an equivalent application object
|
||||
## may be simultaneously invoked by another
|
||||
## process, false otherwise.
|
||||
|
||||
## <tt>rack.run_once</tt>:: true if the server expects
|
||||
## (but does not guarantee!) that the
|
||||
## application will only be invoked this one
|
||||
## time during the life of its containing
|
||||
## process. Normally, this will only be true
|
||||
## for a server based on CGI
|
||||
## (or something similar).
|
||||
|
||||
## <tt>rack.hijack?</tt>:: present and true if the server supports
|
||||
## connection hijacking. See below, hijacking.
|
||||
|
||||
## <tt>rack.hijack</tt>:: an object responding to #call that must be
|
||||
## called at least once before using
|
||||
## rack.hijack_io.
|
||||
## It is recommended #call return rack.hijack_io
|
||||
## as well as setting it in env if necessary.
|
||||
|
||||
## <tt>rack.hijack_io</tt>:: if rack.hijack? is true, and rack.hijack
|
||||
## has received #call, this will contain
|
||||
## an object resembling an IO. See hijacking.
|
||||
|
||||
## Additional environment specifications have approved to
|
||||
## standardized middleware APIs. None of these are required to
|
||||
## be implemented by the server.
|
||||
|
||||
## <tt>rack.session</tt>:: A hash like interface for storing
|
||||
## request session data.
|
||||
## The store must implement:
|
||||
if session = env['rack.session']
|
||||
## store(key, value) (aliased as []=);
|
||||
assert("session #{session.inspect} must respond to store and []=") {
|
||||
session.respond_to?(:store) && session.respond_to?(:[]=)
|
||||
}
|
||||
|
||||
## fetch(key, default = nil) (aliased as []);
|
||||
assert("session #{session.inspect} must respond to fetch and []") {
|
||||
session.respond_to?(:fetch) && session.respond_to?(:[])
|
||||
}
|
||||
|
||||
## delete(key);
|
||||
assert("session #{session.inspect} must respond to delete") {
|
||||
session.respond_to?(:delete)
|
||||
}
|
||||
|
||||
## clear;
|
||||
assert("session #{session.inspect} must respond to clear") {
|
||||
session.respond_to?(:clear)
|
||||
}
|
||||
end
|
||||
|
||||
## <tt>rack.logger</tt>:: A common object interface for logging messages.
|
||||
## The object must implement:
|
||||
if logger = env['rack.logger']
|
||||
## info(message, &block)
|
||||
assert("logger #{logger.inspect} must respond to info") {
|
||||
logger.respond_to?(:info)
|
||||
}
|
||||
|
||||
## debug(message, &block)
|
||||
assert("logger #{logger.inspect} must respond to debug") {
|
||||
logger.respond_to?(:debug)
|
||||
}
|
||||
|
||||
## warn(message, &block)
|
||||
assert("logger #{logger.inspect} must respond to warn") {
|
||||
logger.respond_to?(:warn)
|
||||
}
|
||||
|
||||
## error(message, &block)
|
||||
assert("logger #{logger.inspect} must respond to error") {
|
||||
logger.respond_to?(:error)
|
||||
}
|
||||
|
||||
## fatal(message, &block)
|
||||
assert("logger #{logger.inspect} must respond to fatal") {
|
||||
logger.respond_to?(:fatal)
|
||||
}
|
||||
end
|
||||
|
||||
## <tt>rack.multipart.buffer_size</tt>:: An Integer hint to the multipart parser as to what chunk size to use for reads and writes.
|
||||
if bufsize = env['rack.multipart.buffer_size']
|
||||
assert("rack.multipart.buffer_size must be an Integer > 0 if specified") {
|
||||
bufsize.is_a?(Integer) && bufsize > 0
|
||||
}
|
||||
end
|
||||
|
||||
## <tt>rack.multipart.tempfile_factory</tt>:: An object responding to #call with two arguments, the filename and content_type given for the multipart form field, and returning an IO-like object that responds to #<< and optionally #rewind. This factory will be used to instantiate the tempfile for each multipart form file upload field, rather than the default class of Tempfile.
|
||||
if tempfile_factory = env['rack.multipart.tempfile_factory']
|
||||
assert("rack.multipart.tempfile_factory must respond to #call") { tempfile_factory.respond_to?(:call) }
|
||||
env['rack.multipart.tempfile_factory'] = lambda do |filename, content_type|
|
||||
io = tempfile_factory.call(filename, content_type)
|
||||
assert("rack.multipart.tempfile_factory return value must respond to #<<") { io.respond_to?(:<<) }
|
||||
io
|
||||
end
|
||||
end
|
||||
|
||||
## The server or the application can store their own data in the
|
||||
## environment, too. The keys must contain at least one dot,
|
||||
## and should be prefixed uniquely. The prefix <tt>rack.</tt>
|
||||
## is reserved for use with the Rack core distribution and other
|
||||
## accepted specifications and must not be used otherwise.
|
||||
##
|
||||
|
||||
%w[REQUEST_METHOD SERVER_NAME SERVER_PORT
|
||||
QUERY_STRING
|
||||
rack.version rack.input rack.errors
|
||||
rack.multithread rack.multiprocess rack.run_once].each { |header|
|
||||
assert("env missing required key #{header}") { env.include? header }
|
||||
}
|
||||
|
||||
## The environment must not contain the keys
|
||||
## <tt>HTTP_CONTENT_TYPE</tt> or <tt>HTTP_CONTENT_LENGTH</tt>
|
||||
## (use the versions without <tt>HTTP_</tt>).
|
||||
%w[HTTP_CONTENT_TYPE HTTP_CONTENT_LENGTH].each { |header|
|
||||
assert("env contains #{header}, must use #{header[5,-1]}") {
|
||||
not env.include? header
|
||||
}
|
||||
}
|
||||
|
||||
## The CGI keys (named without a period) must have String values.
|
||||
env.each { |key, value|
|
||||
next if key.include? "." # Skip extensions
|
||||
assert("env variable #{key} has non-string value #{value.inspect}") {
|
||||
value.kind_of? String
|
||||
}
|
||||
}
|
||||
|
||||
## There are the following restrictions:
|
||||
|
||||
## * <tt>rack.version</tt> must be an array of Integers.
|
||||
assert("rack.version must be an Array, was #{env["rack.version"].class}") {
|
||||
env["rack.version"].kind_of? Array
|
||||
}
|
||||
## * <tt>rack.url_scheme</tt> must either be +http+ or +https+.
|
||||
assert("rack.url_scheme unknown: #{env["rack.url_scheme"].inspect}") {
|
||||
%w[http https].include? env["rack.url_scheme"]
|
||||
}
|
||||
|
||||
## * There must be a valid input stream in <tt>rack.input</tt>.
|
||||
check_input env["rack.input"]
|
||||
## * There must be a valid error stream in <tt>rack.errors</tt>.
|
||||
check_error env["rack.errors"]
|
||||
## * There may be a valid hijack stream in <tt>rack.hijack_io</tt>
|
||||
check_hijack env
|
||||
|
||||
## * The <tt>REQUEST_METHOD</tt> must be a valid token.
|
||||
assert("REQUEST_METHOD unknown: #{env[REQUEST_METHOD]}") {
|
||||
env["REQUEST_METHOD"] =~ /\A[0-9A-Za-z!\#$%&'*+.^_`|~-]+\z/
|
||||
}
|
||||
|
||||
## * The <tt>SCRIPT_NAME</tt>, if non-empty, must start with <tt>/</tt>
|
||||
assert("SCRIPT_NAME must start with /") {
|
||||
!env.include?("SCRIPT_NAME") ||
|
||||
env["SCRIPT_NAME"] == "" ||
|
||||
env["SCRIPT_NAME"] =~ /\A\//
|
||||
}
|
||||
## * The <tt>PATH_INFO</tt>, if non-empty, must start with <tt>/</tt>
|
||||
assert("PATH_INFO must start with /") {
|
||||
!env.include?("PATH_INFO") ||
|
||||
env["PATH_INFO"] == "" ||
|
||||
env["PATH_INFO"] =~ /\A\//
|
||||
}
|
||||
## * The <tt>CONTENT_LENGTH</tt>, if given, must consist of digits only.
|
||||
assert("Invalid CONTENT_LENGTH: #{env["CONTENT_LENGTH"]}") {
|
||||
!env.include?("CONTENT_LENGTH") || env["CONTENT_LENGTH"] =~ /\A\d+\z/
|
||||
}
|
||||
|
||||
## * One of <tt>SCRIPT_NAME</tt> or <tt>PATH_INFO</tt> must be
|
||||
## set. <tt>PATH_INFO</tt> should be <tt>/</tt> if
|
||||
## <tt>SCRIPT_NAME</tt> is empty.
|
||||
assert("One of SCRIPT_NAME or PATH_INFO must be set (make PATH_INFO '/' if SCRIPT_NAME is empty)") {
|
||||
env["SCRIPT_NAME"] || env["PATH_INFO"]
|
||||
}
|
||||
## <tt>SCRIPT_NAME</tt> never should be <tt>/</tt>, but instead be empty.
|
||||
assert("SCRIPT_NAME cannot be '/', make it '' and PATH_INFO '/'") {
|
||||
env["SCRIPT_NAME"] != "/"
|
||||
}
|
||||
end
|
||||
|
||||
## === The Input Stream
|
||||
##
|
||||
## The input stream is an IO-like object which contains the raw HTTP
|
||||
## POST data.
|
||||
def check_input(input)
|
||||
## When applicable, its external encoding must be "ASCII-8BIT" and it
|
||||
## must be opened in binary mode, for Ruby 1.9 compatibility.
|
||||
assert("rack.input #{input} does not have ASCII-8BIT as its external encoding") {
|
||||
input.external_encoding.name == "ASCII-8BIT"
|
||||
} if input.respond_to?(:external_encoding)
|
||||
assert("rack.input #{input} is not opened in binary mode") {
|
||||
input.binmode?
|
||||
} if input.respond_to?(:binmode?)
|
||||
|
||||
## The input stream must respond to +gets+, +each+, +read+ and +rewind+.
|
||||
[:gets, :each, :read, :rewind].each { |method|
|
||||
assert("rack.input #{input} does not respond to ##{method}") {
|
||||
input.respond_to? method
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
class InputWrapper
|
||||
include Assertion
|
||||
|
||||
def initialize(input)
|
||||
@input = input
|
||||
end
|
||||
|
||||
## * +gets+ must be called without arguments and return a string,
|
||||
## or +nil+ on EOF.
|
||||
def gets(*args)
|
||||
assert("rack.input#gets called with arguments") { args.size == 0 }
|
||||
v = @input.gets
|
||||
assert("rack.input#gets didn't return a String") {
|
||||
v.nil? or v.kind_of? String
|
||||
}
|
||||
v
|
||||
end
|
||||
|
||||
## * +read+ behaves like IO#read.
|
||||
## Its signature is <tt>read([length, [buffer]])</tt>.
|
||||
##
|
||||
## If given, +length+ must be a non-negative Integer (>= 0) or +nil+,
|
||||
## and +buffer+ must be a String and may not be nil.
|
||||
##
|
||||
## If +length+ is given and not nil, then this method reads at most
|
||||
## +length+ bytes from the input stream.
|
||||
##
|
||||
## If +length+ is not given or nil, then this method reads
|
||||
## all data until EOF.
|
||||
##
|
||||
## When EOF is reached, this method returns nil if +length+ is given
|
||||
## and not nil, or "" if +length+ is not given or is nil.
|
||||
##
|
||||
## If +buffer+ is given, then the read data will be placed
|
||||
## into +buffer+ instead of a newly created String object.
|
||||
def read(*args)
|
||||
assert("rack.input#read called with too many arguments") {
|
||||
args.size <= 2
|
||||
}
|
||||
if args.size >= 1
|
||||
assert("rack.input#read called with non-integer and non-nil length") {
|
||||
args.first.kind_of?(Integer) || args.first.nil?
|
||||
}
|
||||
assert("rack.input#read called with a negative length") {
|
||||
args.first.nil? || args.first >= 0
|
||||
}
|
||||
end
|
||||
if args.size >= 2
|
||||
assert("rack.input#read called with non-String buffer") {
|
||||
args[1].kind_of?(String)
|
||||
}
|
||||
end
|
||||
|
||||
v = @input.read(*args)
|
||||
|
||||
assert("rack.input#read didn't return nil or a String") {
|
||||
v.nil? or v.kind_of? String
|
||||
}
|
||||
if args[0].nil?
|
||||
assert("rack.input#read(nil) returned nil on EOF") {
|
||||
!v.nil?
|
||||
}
|
||||
end
|
||||
|
||||
v
|
||||
end
|
||||
|
||||
## * +each+ must be called without arguments and only yield Strings.
|
||||
def each(*args)
|
||||
assert("rack.input#each called with arguments") { args.size == 0 }
|
||||
@input.each { |line|
|
||||
assert("rack.input#each didn't yield a String") {
|
||||
line.kind_of? String
|
||||
}
|
||||
yield line
|
||||
}
|
||||
end
|
||||
|
||||
## * +rewind+ must be called without arguments. It rewinds the input
|
||||
## stream back to the beginning. It must not raise Errno::ESPIPE:
|
||||
## that is, it may not be a pipe or a socket. Therefore, handler
|
||||
## developers must buffer the input data into some rewindable object
|
||||
## if the underlying input stream is not rewindable.
|
||||
def rewind(*args)
|
||||
assert("rack.input#rewind called with arguments") { args.size == 0 }
|
||||
assert("rack.input#rewind raised Errno::ESPIPE") {
|
||||
begin
|
||||
@input.rewind
|
||||
true
|
||||
rescue Errno::ESPIPE
|
||||
false
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
## * +close+ must never be called on the input stream.
|
||||
def close(*args)
|
||||
assert("rack.input#close must not be called") { false }
|
||||
end
|
||||
end
|
||||
|
||||
## === The Error Stream
|
||||
def check_error(error)
|
||||
## The error stream must respond to +puts+, +write+ and +flush+.
|
||||
[:puts, :write, :flush].each { |method|
|
||||
assert("rack.error #{error} does not respond to ##{method}") {
|
||||
error.respond_to? method
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
class ErrorWrapper
|
||||
include Assertion
|
||||
|
||||
def initialize(error)
|
||||
@error = error
|
||||
end
|
||||
|
||||
## * +puts+ must be called with a single argument that responds to +to_s+.
|
||||
def puts(str)
|
||||
@error.puts str
|
||||
end
|
||||
|
||||
## * +write+ must be called with a single argument that is a String.
|
||||
def write(str)
|
||||
assert("rack.errors#write not called with a String") { str.kind_of? String }
|
||||
@error.write str
|
||||
end
|
||||
|
||||
## * +flush+ must be called without arguments and must be called
|
||||
## in order to make the error appear for sure.
|
||||
def flush
|
||||
@error.flush
|
||||
end
|
||||
|
||||
## * +close+ must never be called on the error stream.
|
||||
def close(*args)
|
||||
assert("rack.errors#close must not be called") { false }
|
||||
end
|
||||
end
|
||||
|
||||
class HijackWrapper
|
||||
include Assertion
|
||||
extend Forwardable
|
||||
|
||||
REQUIRED_METHODS = [
|
||||
:read, :write, :read_nonblock, :write_nonblock, :flush, :close,
|
||||
:close_read, :close_write, :closed?
|
||||
]
|
||||
|
||||
def_delegators :@io, *REQUIRED_METHODS
|
||||
|
||||
def initialize(io)
|
||||
@io = io
|
||||
REQUIRED_METHODS.each do |meth|
|
||||
assert("rack.hijack_io must respond to #{meth}") { io.respond_to? meth }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
## === Hijacking
|
||||
#
|
||||
# AUTHORS: n.b. The trailing whitespace between paragraphs is important and
|
||||
# should not be removed. The whitespace creates paragraphs in the RDoc
|
||||
# output.
|
||||
#
|
||||
## ==== Request (before status)
|
||||
def check_hijack(env)
|
||||
if env['rack.hijack?']
|
||||
## If rack.hijack? is true then rack.hijack must respond to #call.
|
||||
original_hijack = env['rack.hijack']
|
||||
assert("rack.hijack must respond to call") { original_hijack.respond_to?(:call) }
|
||||
env['rack.hijack'] = proc do
|
||||
## rack.hijack must return the io that will also be assigned (or is
|
||||
## already present, in rack.hijack_io.
|
||||
io = original_hijack.call
|
||||
HijackWrapper.new(io)
|
||||
##
|
||||
## rack.hijack_io must respond to:
|
||||
## <tt>read, write, read_nonblock, write_nonblock, flush, close,
|
||||
## close_read, close_write, closed?</tt>
|
||||
##
|
||||
## The semantics of these IO methods must be a best effort match to
|
||||
## those of a normal ruby IO or Socket object, using standard
|
||||
## arguments and raising standard exceptions. Servers are encouraged
|
||||
## to simply pass on real IO objects, although it is recognized that
|
||||
## this approach is not directly compatible with SPDY and HTTP 2.0.
|
||||
##
|
||||
## IO provided in rack.hijack_io should preference the
|
||||
## IO::WaitReadable and IO::WaitWritable APIs wherever supported.
|
||||
##
|
||||
## There is a deliberate lack of full specification around
|
||||
## rack.hijack_io, as semantics will change from server to server.
|
||||
## Users are encouraged to utilize this API with a knowledge of their
|
||||
## server choice, and servers may extend the functionality of
|
||||
## hijack_io to provide additional features to users. The purpose of
|
||||
## rack.hijack is for Rack to "get out of the way", as such, Rack only
|
||||
## provides the minimum of specification and support.
|
||||
env['rack.hijack_io'] = HijackWrapper.new(env['rack.hijack_io'])
|
||||
io
|
||||
end
|
||||
else
|
||||
##
|
||||
## If rack.hijack? is false, then rack.hijack should not be set.
|
||||
assert("rack.hijack? is false, but rack.hijack is present") { env['rack.hijack'].nil? }
|
||||
##
|
||||
## If rack.hijack? is false, then rack.hijack_io should not be set.
|
||||
assert("rack.hijack? is false, but rack.hijack_io is present") { env['rack.hijack_io'].nil? }
|
||||
end
|
||||
end
|
||||
|
||||
## ==== Response (after headers)
|
||||
## It is also possible to hijack a response after the status and headers
|
||||
## have been sent.
|
||||
def check_hijack_response(headers, env)
|
||||
|
||||
# this check uses headers like a hash, but the spec only requires
|
||||
# headers respond to #each
|
||||
headers = Rack::Utils::HeaderHash.new(headers)
|
||||
|
||||
## In order to do this, an application may set the special header
|
||||
## <tt>rack.hijack</tt> to an object that responds to <tt>call</tt>
|
||||
## accepting an argument that conforms to the <tt>rack.hijack_io</tt>
|
||||
## protocol.
|
||||
##
|
||||
## After the headers have been sent, and this hijack callback has been
|
||||
## called, the application is now responsible for the remaining lifecycle
|
||||
## of the IO. The application is also responsible for maintaining HTTP
|
||||
## semantics. Of specific note, in almost all cases in the current SPEC,
|
||||
## applications will have wanted to specify the header Connection:close in
|
||||
## HTTP/1.1, and not Connection:keep-alive, as there is no protocol for
|
||||
## returning hijacked sockets to the web server. For that purpose, use the
|
||||
## body streaming API instead (progressively yielding strings via each).
|
||||
##
|
||||
## Servers must ignore the <tt>body</tt> part of the response tuple when
|
||||
## the <tt>rack.hijack</tt> response API is in use.
|
||||
|
||||
if env['rack.hijack?'] && headers['rack.hijack']
|
||||
assert('rack.hijack header must respond to #call') {
|
||||
headers['rack.hijack'].respond_to? :call
|
||||
}
|
||||
original_hijack = headers['rack.hijack']
|
||||
headers['rack.hijack'] = proc do |io|
|
||||
original_hijack.call HijackWrapper.new(io)
|
||||
end
|
||||
else
|
||||
##
|
||||
## The special response header <tt>rack.hijack</tt> must only be set
|
||||
## if the request env has <tt>rack.hijack?</tt> <tt>true</tt>.
|
||||
assert('rack.hijack header must not be present if server does not support hijacking') {
|
||||
headers['rack.hijack'].nil?
|
||||
}
|
||||
end
|
||||
end
|
||||
## ==== Conventions
|
||||
## * Middleware should not use hijack unless it is handling the whole
|
||||
## response.
|
||||
## * Middleware may wrap the IO object for the response pattern.
|
||||
## * Middleware should not wrap the IO object for the request pattern. The
|
||||
## request pattern is intended to provide the hijacker with "raw tcp".
|
||||
|
||||
## == The Response
|
||||
|
||||
## === The Status
|
||||
def check_status(status)
|
||||
## This is an HTTP status. When parsed as integer (+to_i+), it must be
|
||||
## greater than or equal to 100.
|
||||
assert("Status must be >=100 seen as integer") { status.to_i >= 100 }
|
||||
end
|
||||
|
||||
## === The Headers
|
||||
def check_headers(header)
|
||||
## The header must respond to +each+, and yield values of key and value.
|
||||
assert("headers object should respond to #each, but doesn't (got #{header.class} as headers)") {
|
||||
header.respond_to? :each
|
||||
}
|
||||
header.each { |key, value|
|
||||
## Special headers starting "rack." are for communicating with the
|
||||
## server, and must not be sent back to the client.
|
||||
next if key =~ /^rack\..+$/
|
||||
|
||||
## The header keys must be Strings.
|
||||
assert("header key must be a string, was #{key.class}") {
|
||||
key.kind_of? String
|
||||
}
|
||||
## The header must not contain a +Status+ key.
|
||||
assert("header must not contain Status") { key.downcase != "status" }
|
||||
## The header must conform to RFC7230 token specification, i.e. cannot
|
||||
## contain non-printable ASCII, DQUOTE or "(),/:;<=>?@[\]{}".
|
||||
assert("invalid header name: #{key}") { key !~ /[\(\),\/:;<=>\?@\[\\\]{}[:cntrl:]]/ }
|
||||
|
||||
## The values of the header must be Strings,
|
||||
assert("a header value must be a String, but the value of " +
|
||||
"'#{key}' is a #{value.class}") { value.kind_of? String }
|
||||
## consisting of lines (for multiple header values, e.g. multiple
|
||||
## <tt>Set-Cookie</tt> values) separated by "\\n".
|
||||
value.split("\n").each { |item|
|
||||
## The lines must not contain characters below 037.
|
||||
assert("invalid header value #{key}: #{item.inspect}") {
|
||||
item !~ /[\000-\037]/
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
## === The Content-Type
|
||||
def check_content_type(status, headers)
|
||||
headers.each { |key, value|
|
||||
## There must not be a <tt>Content-Type</tt>, when the +Status+ is 1xx,
|
||||
## 204, 205 or 304.
|
||||
if key.downcase == "content-type"
|
||||
assert("Content-Type header found in #{status} response, not allowed") {
|
||||
not Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include? status.to_i
|
||||
}
|
||||
return
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
## === The Content-Length
|
||||
def check_content_length(status, headers)
|
||||
headers.each { |key, value|
|
||||
if key.downcase == 'content-length'
|
||||
## There must not be a <tt>Content-Length</tt> header when the
|
||||
## +Status+ is 1xx, 204, 205 or 304.
|
||||
assert("Content-Length header found in #{status} response, not allowed") {
|
||||
not Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include? status.to_i
|
||||
}
|
||||
@content_length = value
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
def verify_content_length(bytes)
|
||||
if @head_request
|
||||
assert("Response body was given for HEAD request, but should be empty") {
|
||||
bytes == 0
|
||||
}
|
||||
elsif @content_length
|
||||
assert("Content-Length header was #{@content_length}, but should be #{bytes}") {
|
||||
@content_length == bytes.to_s
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
## === The Body
|
||||
def each
|
||||
@closed = false
|
||||
bytes = 0
|
||||
|
||||
## The Body must respond to +each+
|
||||
assert("Response body must respond to each") do
|
||||
@body.respond_to?(:each)
|
||||
end
|
||||
|
||||
@body.each { |part|
|
||||
## and must only yield String values.
|
||||
assert("Body yielded non-string value #{part.inspect}") {
|
||||
part.kind_of? String
|
||||
}
|
||||
bytes += Rack::Utils.bytesize(part)
|
||||
yield part
|
||||
}
|
||||
verify_content_length(bytes)
|
||||
|
||||
##
|
||||
## The Body itself should not be an instance of String, as this will
|
||||
## break in Ruby 1.9.
|
||||
##
|
||||
## If the Body responds to +close+, it will be called after iteration. If
|
||||
## the body is replaced by a middleware after action, the original body
|
||||
## must be closed first, if it responds to close.
|
||||
# XXX howto: assert("Body has not been closed") { @closed }
|
||||
|
||||
|
||||
##
|
||||
## If the Body responds to +to_path+, it must return a String
|
||||
## identifying the location of a file whose contents are identical
|
||||
## to that produced by calling +each+; this may be used by the
|
||||
## server as an alternative, possibly more efficient way to
|
||||
## transport the response.
|
||||
|
||||
if @body.respond_to?(:to_path)
|
||||
assert("The file identified by body.to_path does not exist") {
|
||||
::File.exist? @body.to_path
|
||||
}
|
||||
end
|
||||
|
||||
##
|
||||
## The Body commonly is an Array of Strings, the application
|
||||
## instance itself, or a File-like object.
|
||||
end
|
||||
|
||||
def close
|
||||
@closed = true
|
||||
@body.close if @body.respond_to?(:close)
|
||||
end
|
||||
|
||||
# :startdoc:
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
## == Thanks
|
||||
## Some parts of this specification are adopted from PEP333: Python
|
||||
## Web Server Gateway Interface
|
||||
## v1.0 (http://www.python.org/dev/peps/pep-0333/). I'd like to thank
|
||||
## everyone involved in that effort.
|
||||
@@ -1,70 +0,0 @@
|
||||
require 'zlib'
|
||||
|
||||
require 'rack/request'
|
||||
require 'rack/response'
|
||||
|
||||
module Rack
|
||||
# Paste has a Pony, Rack has a Lobster!
|
||||
class Lobster
|
||||
LobsterString = Zlib::Inflate.inflate("eJx9kEEOwyAMBO99xd7MAcytUhPlJyj2
|
||||
P6jy9i4k9EQyGAnBarEXeCBqSkntNXsi/ZCvC48zGQoZKikGrFMZvgS5ZHd+aGWVuWwhVF0
|
||||
t1drVmiR42HcWNz5w3QanT+2gIvTVCiE1lm1Y0eU4JGmIIbaKwextKn8rvW+p5PIwFl8ZWJ
|
||||
I8jyiTlhTcYXkekJAzTyYN6E08A+dk8voBkAVTJQ==".delete("\n ").unpack("m*")[0])
|
||||
|
||||
LambdaLobster = lambda { |env|
|
||||
if env[QUERY_STRING].include?("flip")
|
||||
lobster = LobsterString.split("\n").
|
||||
map { |line| line.ljust(42).reverse }.
|
||||
join("\n")
|
||||
href = "?"
|
||||
else
|
||||
lobster = LobsterString
|
||||
href = "?flip"
|
||||
end
|
||||
|
||||
content = ["<title>Lobstericious!</title>",
|
||||
"<pre>", lobster, "</pre>",
|
||||
"<a href='#{href}'>flip!</a>"]
|
||||
length = content.inject(0) { |a,e| a+e.size }.to_s
|
||||
[200, {CONTENT_TYPE => "text/html", CONTENT_LENGTH => length}, content]
|
||||
}
|
||||
|
||||
def call(env)
|
||||
req = Request.new(env)
|
||||
if req.GET["flip"] == "left"
|
||||
lobster = LobsterString.split("\n").map do |line|
|
||||
line.ljust(42).reverse.
|
||||
gsub('\\', 'TEMP').
|
||||
gsub('/', '\\').
|
||||
gsub('TEMP', '/').
|
||||
gsub('{','}').
|
||||
gsub('(',')')
|
||||
end.join("\n")
|
||||
href = "?flip=right"
|
||||
elsif req.GET["flip"] == "crash"
|
||||
raise "Lobster crashed"
|
||||
else
|
||||
lobster = LobsterString
|
||||
href = "?flip=left"
|
||||
end
|
||||
|
||||
res = Response.new
|
||||
res.write "<title>Lobstericious!</title>"
|
||||
res.write "<pre>"
|
||||
res.write lobster
|
||||
res.write "</pre>"
|
||||
res.write "<p><a href='#{href}'>flip!</a></p>"
|
||||
res.write "<p><a href='?flip=crash'>crash!</a></p>"
|
||||
res.finish
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
if $0 == __FILE__
|
||||
require 'rack'
|
||||
require 'rack/showexceptions'
|
||||
Rack::Server.start(
|
||||
:app => Rack::ShowExceptions.new(Rack::Lint.new(Rack::Lobster.new)), :Port => 9292
|
||||
)
|
||||
end
|
||||
@@ -1,26 +0,0 @@
|
||||
require 'thread'
|
||||
require 'rack/body_proxy'
|
||||
|
||||
module Rack
|
||||
# Rack::Lock locks every request inside a mutex, so that every request
|
||||
# will effectively be executed synchronously.
|
||||
class Lock
|
||||
FLAG = 'rack.multithread'.freeze
|
||||
|
||||
def initialize(app, mutex = Mutex.new)
|
||||
@app, @mutex = app, mutex
|
||||
end
|
||||
|
||||
def call(env)
|
||||
old, env[FLAG] = env[FLAG], false
|
||||
@mutex.lock
|
||||
response = @app.call(env)
|
||||
body = BodyProxy.new(response[2]) { @mutex.unlock }
|
||||
response[2] = body
|
||||
response
|
||||
ensure
|
||||
@mutex.unlock unless body
|
||||
env[FLAG] = old
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,18 +0,0 @@
|
||||
require 'logger'
|
||||
|
||||
module Rack
|
||||
# Sets up rack.logger to write to rack.errors stream
|
||||
class Logger
|
||||
def initialize(app, level = ::Logger::INFO)
|
||||
@app, @level = app, level
|
||||
end
|
||||
|
||||
def call(env)
|
||||
logger = ::Logger.new(env['rack.errors'])
|
||||
logger.level = @level
|
||||
|
||||
env['rack.logger'] = logger
|
||||
@app.call(env)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,43 +0,0 @@
|
||||
module Rack
|
||||
class MethodOverride
|
||||
HTTP_METHODS = %w(GET HEAD PUT POST DELETE OPTIONS PATCH LINK UNLINK)
|
||||
|
||||
METHOD_OVERRIDE_PARAM_KEY = "_method".freeze
|
||||
HTTP_METHOD_OVERRIDE_HEADER = "HTTP_X_HTTP_METHOD_OVERRIDE".freeze
|
||||
ALLOWED_METHODS = ["POST"]
|
||||
|
||||
def initialize(app)
|
||||
@app = app
|
||||
end
|
||||
|
||||
def call(env)
|
||||
if allowed_methods.include?(env[REQUEST_METHOD])
|
||||
method = method_override(env)
|
||||
if HTTP_METHODS.include?(method)
|
||||
env["rack.methodoverride.original_method"] = env[REQUEST_METHOD]
|
||||
env[REQUEST_METHOD] = method
|
||||
end
|
||||
end
|
||||
|
||||
@app.call(env)
|
||||
end
|
||||
|
||||
def method_override(env)
|
||||
req = Request.new(env)
|
||||
method = method_override_param(req) ||
|
||||
env[HTTP_METHOD_OVERRIDE_HEADER]
|
||||
method.to_s.upcase
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def allowed_methods
|
||||
ALLOWED_METHODS
|
||||
end
|
||||
|
||||
def method_override_param(req)
|
||||
req.POST[METHOD_OVERRIDE_PARAM_KEY]
|
||||
rescue Utils::InvalidParameterError, Utils::ParameterTypeError
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,664 +0,0 @@
|
||||
module Rack
|
||||
module Mime
|
||||
# Returns String with mime type if found, otherwise use +fallback+.
|
||||
# +ext+ should be filename extension in the '.ext' format that
|
||||
# File.extname(file) returns.
|
||||
# +fallback+ may be any object
|
||||
#
|
||||
# Also see the documentation for MIME_TYPES
|
||||
#
|
||||
# Usage:
|
||||
# Rack::Mime.mime_type('.foo')
|
||||
#
|
||||
# This is a shortcut for:
|
||||
# Rack::Mime::MIME_TYPES.fetch('.foo', 'application/octet-stream')
|
||||
|
||||
def mime_type(ext, fallback='application/octet-stream')
|
||||
MIME_TYPES.fetch(ext.to_s.downcase, fallback)
|
||||
end
|
||||
module_function :mime_type
|
||||
|
||||
# Returns true if the given value is a mime match for the given mime match
|
||||
# specification, false otherwise.
|
||||
#
|
||||
# Rack::Mime.match?('text/html', 'text/*') => true
|
||||
# Rack::Mime.match?('text/plain', '*') => true
|
||||
# Rack::Mime.match?('text/html', 'application/json') => false
|
||||
|
||||
def match?(value, matcher)
|
||||
v1, v2 = value.split('/', 2)
|
||||
m1, m2 = matcher.split('/', 2)
|
||||
|
||||
(m1 == '*' || v1 == m1) && (m2.nil? || m2 == '*' || m2 == v2)
|
||||
end
|
||||
module_function :match?
|
||||
|
||||
# List of most common mime-types, selected various sources
|
||||
# according to their usefulness in a webserving scope for Ruby
|
||||
# users.
|
||||
#
|
||||
# To amend this list with your local mime.types list you can use:
|
||||
#
|
||||
# require 'webrick/httputils'
|
||||
# list = WEBrick::HTTPUtils.load_mime_types('/etc/mime.types')
|
||||
# Rack::Mime::MIME_TYPES.merge!(list)
|
||||
#
|
||||
# N.B. On Ubuntu the mime.types file does not include the leading period, so
|
||||
# users may need to modify the data before merging into the hash.
|
||||
#
|
||||
# To add the list mongrel provides, use:
|
||||
#
|
||||
# require 'mongrel/handlers'
|
||||
# Rack::Mime::MIME_TYPES.merge!(Mongrel::DirHandler::MIME_TYPES)
|
||||
|
||||
MIME_TYPES = {
|
||||
".123" => "application/vnd.lotus-1-2-3",
|
||||
".3dml" => "text/vnd.in3d.3dml",
|
||||
".3g2" => "video/3gpp2",
|
||||
".3gp" => "video/3gpp",
|
||||
".a" => "application/octet-stream",
|
||||
".acc" => "application/vnd.americandynamics.acc",
|
||||
".ace" => "application/x-ace-compressed",
|
||||
".acu" => "application/vnd.acucobol",
|
||||
".aep" => "application/vnd.audiograph",
|
||||
".afp" => "application/vnd.ibm.modcap",
|
||||
".ai" => "application/postscript",
|
||||
".aif" => "audio/x-aiff",
|
||||
".aiff" => "audio/x-aiff",
|
||||
".ami" => "application/vnd.amiga.ami",
|
||||
".appcache" => "text/cache-manifest",
|
||||
".apr" => "application/vnd.lotus-approach",
|
||||
".asc" => "application/pgp-signature",
|
||||
".asf" => "video/x-ms-asf",
|
||||
".asm" => "text/x-asm",
|
||||
".aso" => "application/vnd.accpac.simply.aso",
|
||||
".asx" => "video/x-ms-asf",
|
||||
".atc" => "application/vnd.acucorp",
|
||||
".atom" => "application/atom+xml",
|
||||
".atomcat" => "application/atomcat+xml",
|
||||
".atomsvc" => "application/atomsvc+xml",
|
||||
".atx" => "application/vnd.antix.game-component",
|
||||
".au" => "audio/basic",
|
||||
".avi" => "video/x-msvideo",
|
||||
".bat" => "application/x-msdownload",
|
||||
".bcpio" => "application/x-bcpio",
|
||||
".bdm" => "application/vnd.syncml.dm+wbxml",
|
||||
".bh2" => "application/vnd.fujitsu.oasysprs",
|
||||
".bin" => "application/octet-stream",
|
||||
".bmi" => "application/vnd.bmi",
|
||||
".bmp" => "image/bmp",
|
||||
".box" => "application/vnd.previewsystems.box",
|
||||
".btif" => "image/prs.btif",
|
||||
".bz" => "application/x-bzip",
|
||||
".bz2" => "application/x-bzip2",
|
||||
".c" => "text/x-c",
|
||||
".c4g" => "application/vnd.clonk.c4group",
|
||||
".cab" => "application/vnd.ms-cab-compressed",
|
||||
".cc" => "text/x-c",
|
||||
".ccxml" => "application/ccxml+xml",
|
||||
".cdbcmsg" => "application/vnd.contact.cmsg",
|
||||
".cdkey" => "application/vnd.mediastation.cdkey",
|
||||
".cdx" => "chemical/x-cdx",
|
||||
".cdxml" => "application/vnd.chemdraw+xml",
|
||||
".cdy" => "application/vnd.cinderella",
|
||||
".cer" => "application/pkix-cert",
|
||||
".cgm" => "image/cgm",
|
||||
".chat" => "application/x-chat",
|
||||
".chm" => "application/vnd.ms-htmlhelp",
|
||||
".chrt" => "application/vnd.kde.kchart",
|
||||
".cif" => "chemical/x-cif",
|
||||
".cii" => "application/vnd.anser-web-certificate-issue-initiation",
|
||||
".cil" => "application/vnd.ms-artgalry",
|
||||
".cla" => "application/vnd.claymore",
|
||||
".class" => "application/octet-stream",
|
||||
".clkk" => "application/vnd.crick.clicker.keyboard",
|
||||
".clkp" => "application/vnd.crick.clicker.palette",
|
||||
".clkt" => "application/vnd.crick.clicker.template",
|
||||
".clkw" => "application/vnd.crick.clicker.wordbank",
|
||||
".clkx" => "application/vnd.crick.clicker",
|
||||
".clp" => "application/x-msclip",
|
||||
".cmc" => "application/vnd.cosmocaller",
|
||||
".cmdf" => "chemical/x-cmdf",
|
||||
".cml" => "chemical/x-cml",
|
||||
".cmp" => "application/vnd.yellowriver-custom-menu",
|
||||
".cmx" => "image/x-cmx",
|
||||
".com" => "application/x-msdownload",
|
||||
".conf" => "text/plain",
|
||||
".cpio" => "application/x-cpio",
|
||||
".cpp" => "text/x-c",
|
||||
".cpt" => "application/mac-compactpro",
|
||||
".crd" => "application/x-mscardfile",
|
||||
".crl" => "application/pkix-crl",
|
||||
".crt" => "application/x-x509-ca-cert",
|
||||
".csh" => "application/x-csh",
|
||||
".csml" => "chemical/x-csml",
|
||||
".csp" => "application/vnd.commonspace",
|
||||
".css" => "text/css",
|
||||
".csv" => "text/csv",
|
||||
".curl" => "application/vnd.curl",
|
||||
".cww" => "application/prs.cww",
|
||||
".cxx" => "text/x-c",
|
||||
".daf" => "application/vnd.mobius.daf",
|
||||
".davmount" => "application/davmount+xml",
|
||||
".dcr" => "application/x-director",
|
||||
".dd2" => "application/vnd.oma.dd2+xml",
|
||||
".ddd" => "application/vnd.fujixerox.ddd",
|
||||
".deb" => "application/x-debian-package",
|
||||
".der" => "application/x-x509-ca-cert",
|
||||
".dfac" => "application/vnd.dreamfactory",
|
||||
".diff" => "text/x-diff",
|
||||
".dis" => "application/vnd.mobius.dis",
|
||||
".djv" => "image/vnd.djvu",
|
||||
".djvu" => "image/vnd.djvu",
|
||||
".dll" => "application/x-msdownload",
|
||||
".dmg" => "application/octet-stream",
|
||||
".dna" => "application/vnd.dna",
|
||||
".doc" => "application/msword",
|
||||
".docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
".dot" => "application/msword",
|
||||
".dp" => "application/vnd.osgi.dp",
|
||||
".dpg" => "application/vnd.dpgraph",
|
||||
".dsc" => "text/prs.lines.tag",
|
||||
".dtd" => "application/xml-dtd",
|
||||
".dts" => "audio/vnd.dts",
|
||||
".dtshd" => "audio/vnd.dts.hd",
|
||||
".dv" => "video/x-dv",
|
||||
".dvi" => "application/x-dvi",
|
||||
".dwf" => "model/vnd.dwf",
|
||||
".dwg" => "image/vnd.dwg",
|
||||
".dxf" => "image/vnd.dxf",
|
||||
".dxp" => "application/vnd.spotfire.dxp",
|
||||
".ear" => "application/java-archive",
|
||||
".ecelp4800" => "audio/vnd.nuera.ecelp4800",
|
||||
".ecelp7470" => "audio/vnd.nuera.ecelp7470",
|
||||
".ecelp9600" => "audio/vnd.nuera.ecelp9600",
|
||||
".ecma" => "application/ecmascript",
|
||||
".edm" => "application/vnd.novadigm.edm",
|
||||
".edx" => "application/vnd.novadigm.edx",
|
||||
".efif" => "application/vnd.picsel",
|
||||
".ei6" => "application/vnd.pg.osasli",
|
||||
".eml" => "message/rfc822",
|
||||
".eol" => "audio/vnd.digital-winds",
|
||||
".eot" => "application/vnd.ms-fontobject",
|
||||
".eps" => "application/postscript",
|
||||
".es3" => "application/vnd.eszigno3+xml",
|
||||
".esf" => "application/vnd.epson.esf",
|
||||
".etx" => "text/x-setext",
|
||||
".exe" => "application/x-msdownload",
|
||||
".ext" => "application/vnd.novadigm.ext",
|
||||
".ez" => "application/andrew-inset",
|
||||
".ez2" => "application/vnd.ezpix-album",
|
||||
".ez3" => "application/vnd.ezpix-package",
|
||||
".f" => "text/x-fortran",
|
||||
".f77" => "text/x-fortran",
|
||||
".f90" => "text/x-fortran",
|
||||
".fbs" => "image/vnd.fastbidsheet",
|
||||
".fdf" => "application/vnd.fdf",
|
||||
".fe_launch" => "application/vnd.denovo.fcselayout-link",
|
||||
".fg5" => "application/vnd.fujitsu.oasysgp",
|
||||
".fli" => "video/x-fli",
|
||||
".flo" => "application/vnd.micrografx.flo",
|
||||
".flv" => "video/x-flv",
|
||||
".flw" => "application/vnd.kde.kivio",
|
||||
".flx" => "text/vnd.fmi.flexstor",
|
||||
".fly" => "text/vnd.fly",
|
||||
".fm" => "application/vnd.framemaker",
|
||||
".fnc" => "application/vnd.frogans.fnc",
|
||||
".for" => "text/x-fortran",
|
||||
".fpx" => "image/vnd.fpx",
|
||||
".fsc" => "application/vnd.fsc.weblaunch",
|
||||
".fst" => "image/vnd.fst",
|
||||
".ftc" => "application/vnd.fluxtime.clip",
|
||||
".fti" => "application/vnd.anser-web-funds-transfer-initiation",
|
||||
".fvt" => "video/vnd.fvt",
|
||||
".fzs" => "application/vnd.fuzzysheet",
|
||||
".g3" => "image/g3fax",
|
||||
".gac" => "application/vnd.groove-account",
|
||||
".gdl" => "model/vnd.gdl",
|
||||
".gem" => "application/octet-stream",
|
||||
".gemspec" => "text/x-script.ruby",
|
||||
".ghf" => "application/vnd.groove-help",
|
||||
".gif" => "image/gif",
|
||||
".gim" => "application/vnd.groove-identity-message",
|
||||
".gmx" => "application/vnd.gmx",
|
||||
".gph" => "application/vnd.flographit",
|
||||
".gqf" => "application/vnd.grafeq",
|
||||
".gram" => "application/srgs",
|
||||
".grv" => "application/vnd.groove-injector",
|
||||
".grxml" => "application/srgs+xml",
|
||||
".gtar" => "application/x-gtar",
|
||||
".gtm" => "application/vnd.groove-tool-message",
|
||||
".gtw" => "model/vnd.gtw",
|
||||
".gv" => "text/vnd.graphviz",
|
||||
".gz" => "application/x-gzip",
|
||||
".h" => "text/x-c",
|
||||
".h261" => "video/h261",
|
||||
".h263" => "video/h263",
|
||||
".h264" => "video/h264",
|
||||
".hbci" => "application/vnd.hbci",
|
||||
".hdf" => "application/x-hdf",
|
||||
".hh" => "text/x-c",
|
||||
".hlp" => "application/winhlp",
|
||||
".hpgl" => "application/vnd.hp-hpgl",
|
||||
".hpid" => "application/vnd.hp-hpid",
|
||||
".hps" => "application/vnd.hp-hps",
|
||||
".hqx" => "application/mac-binhex40",
|
||||
".htc" => "text/x-component",
|
||||
".htke" => "application/vnd.kenameaapp",
|
||||
".htm" => "text/html",
|
||||
".html" => "text/html",
|
||||
".hvd" => "application/vnd.yamaha.hv-dic",
|
||||
".hvp" => "application/vnd.yamaha.hv-voice",
|
||||
".hvs" => "application/vnd.yamaha.hv-script",
|
||||
".icc" => "application/vnd.iccprofile",
|
||||
".ice" => "x-conference/x-cooltalk",
|
||||
".ico" => "image/vnd.microsoft.icon",
|
||||
".ics" => "text/calendar",
|
||||
".ief" => "image/ief",
|
||||
".ifb" => "text/calendar",
|
||||
".ifm" => "application/vnd.shana.informed.formdata",
|
||||
".igl" => "application/vnd.igloader",
|
||||
".igs" => "model/iges",
|
||||
".igx" => "application/vnd.micrografx.igx",
|
||||
".iif" => "application/vnd.shana.informed.interchange",
|
||||
".imp" => "application/vnd.accpac.simply.imp",
|
||||
".ims" => "application/vnd.ms-ims",
|
||||
".ipk" => "application/vnd.shana.informed.package",
|
||||
".irm" => "application/vnd.ibm.rights-management",
|
||||
".irp" => "application/vnd.irepository.package+xml",
|
||||
".iso" => "application/octet-stream",
|
||||
".itp" => "application/vnd.shana.informed.formtemplate",
|
||||
".ivp" => "application/vnd.immervision-ivp",
|
||||
".ivu" => "application/vnd.immervision-ivu",
|
||||
".jad" => "text/vnd.sun.j2me.app-descriptor",
|
||||
".jam" => "application/vnd.jam",
|
||||
".jar" => "application/java-archive",
|
||||
".java" => "text/x-java-source",
|
||||
".jisp" => "application/vnd.jisp",
|
||||
".jlt" => "application/vnd.hp-jlyt",
|
||||
".jnlp" => "application/x-java-jnlp-file",
|
||||
".joda" => "application/vnd.joost.joda-archive",
|
||||
".jp2" => "image/jp2",
|
||||
".jpeg" => "image/jpeg",
|
||||
".jpg" => "image/jpeg",
|
||||
".jpgv" => "video/jpeg",
|
||||
".jpm" => "video/jpm",
|
||||
".js" => "application/javascript",
|
||||
".json" => "application/json",
|
||||
".karbon" => "application/vnd.kde.karbon",
|
||||
".kfo" => "application/vnd.kde.kformula",
|
||||
".kia" => "application/vnd.kidspiration",
|
||||
".kml" => "application/vnd.google-earth.kml+xml",
|
||||
".kmz" => "application/vnd.google-earth.kmz",
|
||||
".kne" => "application/vnd.kinar",
|
||||
".kon" => "application/vnd.kde.kontour",
|
||||
".kpr" => "application/vnd.kde.kpresenter",
|
||||
".ksp" => "application/vnd.kde.kspread",
|
||||
".ktz" => "application/vnd.kahootz",
|
||||
".kwd" => "application/vnd.kde.kword",
|
||||
".latex" => "application/x-latex",
|
||||
".lbd" => "application/vnd.llamagraphics.life-balance.desktop",
|
||||
".lbe" => "application/vnd.llamagraphics.life-balance.exchange+xml",
|
||||
".les" => "application/vnd.hhe.lesson-player",
|
||||
".link66" => "application/vnd.route66.link66+xml",
|
||||
".log" => "text/plain",
|
||||
".lostxml" => "application/lost+xml",
|
||||
".lrm" => "application/vnd.ms-lrm",
|
||||
".ltf" => "application/vnd.frogans.ltf",
|
||||
".lvp" => "audio/vnd.lucent.voice",
|
||||
".lwp" => "application/vnd.lotus-wordpro",
|
||||
".m3u" => "audio/x-mpegurl",
|
||||
".m4a" => "audio/mp4a-latm",
|
||||
".m4v" => "video/mp4",
|
||||
".ma" => "application/mathematica",
|
||||
".mag" => "application/vnd.ecowin.chart",
|
||||
".man" => "text/troff",
|
||||
".manifest" => "text/cache-manifest",
|
||||
".mathml" => "application/mathml+xml",
|
||||
".mbk" => "application/vnd.mobius.mbk",
|
||||
".mbox" => "application/mbox",
|
||||
".mc1" => "application/vnd.medcalcdata",
|
||||
".mcd" => "application/vnd.mcd",
|
||||
".mdb" => "application/x-msaccess",
|
||||
".mdi" => "image/vnd.ms-modi",
|
||||
".mdoc" => "text/troff",
|
||||
".me" => "text/troff",
|
||||
".mfm" => "application/vnd.mfmp",
|
||||
".mgz" => "application/vnd.proteus.magazine",
|
||||
".mid" => "audio/midi",
|
||||
".midi" => "audio/midi",
|
||||
".mif" => "application/vnd.mif",
|
||||
".mime" => "message/rfc822",
|
||||
".mj2" => "video/mj2",
|
||||
".mlp" => "application/vnd.dolby.mlp",
|
||||
".mmd" => "application/vnd.chipnuts.karaoke-mmd",
|
||||
".mmf" => "application/vnd.smaf",
|
||||
".mml" => "application/mathml+xml",
|
||||
".mmr" => "image/vnd.fujixerox.edmics-mmr",
|
||||
".mng" => "video/x-mng",
|
||||
".mny" => "application/x-msmoney",
|
||||
".mov" => "video/quicktime",
|
||||
".movie" => "video/x-sgi-movie",
|
||||
".mp3" => "audio/mpeg",
|
||||
".mp4" => "video/mp4",
|
||||
".mp4a" => "audio/mp4",
|
||||
".mp4s" => "application/mp4",
|
||||
".mp4v" => "video/mp4",
|
||||
".mpc" => "application/vnd.mophun.certificate",
|
||||
".mpeg" => "video/mpeg",
|
||||
".mpg" => "video/mpeg",
|
||||
".mpga" => "audio/mpeg",
|
||||
".mpkg" => "application/vnd.apple.installer+xml",
|
||||
".mpm" => "application/vnd.blueice.multipass",
|
||||
".mpn" => "application/vnd.mophun.application",
|
||||
".mpp" => "application/vnd.ms-project",
|
||||
".mpy" => "application/vnd.ibm.minipay",
|
||||
".mqy" => "application/vnd.mobius.mqy",
|
||||
".mrc" => "application/marc",
|
||||
".ms" => "text/troff",
|
||||
".mscml" => "application/mediaservercontrol+xml",
|
||||
".mseq" => "application/vnd.mseq",
|
||||
".msf" => "application/vnd.epson.msf",
|
||||
".msh" => "model/mesh",
|
||||
".msi" => "application/x-msdownload",
|
||||
".msl" => "application/vnd.mobius.msl",
|
||||
".msty" => "application/vnd.muvee.style",
|
||||
".mts" => "model/vnd.mts",
|
||||
".mus" => "application/vnd.musician",
|
||||
".mvb" => "application/x-msmediaview",
|
||||
".mwf" => "application/vnd.mfer",
|
||||
".mxf" => "application/mxf",
|
||||
".mxl" => "application/vnd.recordare.musicxml",
|
||||
".mxml" => "application/xv+xml",
|
||||
".mxs" => "application/vnd.triscape.mxs",
|
||||
".mxu" => "video/vnd.mpegurl",
|
||||
".n" => "application/vnd.nokia.n-gage.symbian.install",
|
||||
".nc" => "application/x-netcdf",
|
||||
".ngdat" => "application/vnd.nokia.n-gage.data",
|
||||
".nlu" => "application/vnd.neurolanguage.nlu",
|
||||
".nml" => "application/vnd.enliven",
|
||||
".nnd" => "application/vnd.noblenet-directory",
|
||||
".nns" => "application/vnd.noblenet-sealer",
|
||||
".nnw" => "application/vnd.noblenet-web",
|
||||
".npx" => "image/vnd.net-fpx",
|
||||
".nsf" => "application/vnd.lotus-notes",
|
||||
".oa2" => "application/vnd.fujitsu.oasys2",
|
||||
".oa3" => "application/vnd.fujitsu.oasys3",
|
||||
".oas" => "application/vnd.fujitsu.oasys",
|
||||
".obd" => "application/x-msbinder",
|
||||
".oda" => "application/oda",
|
||||
".odc" => "application/vnd.oasis.opendocument.chart",
|
||||
".odf" => "application/vnd.oasis.opendocument.formula",
|
||||
".odg" => "application/vnd.oasis.opendocument.graphics",
|
||||
".odi" => "application/vnd.oasis.opendocument.image",
|
||||
".odp" => "application/vnd.oasis.opendocument.presentation",
|
||||
".ods" => "application/vnd.oasis.opendocument.spreadsheet",
|
||||
".odt" => "application/vnd.oasis.opendocument.text",
|
||||
".oga" => "audio/ogg",
|
||||
".ogg" => "application/ogg",
|
||||
".ogv" => "video/ogg",
|
||||
".ogx" => "application/ogg",
|
||||
".org" => "application/vnd.lotus-organizer",
|
||||
".otc" => "application/vnd.oasis.opendocument.chart-template",
|
||||
".otf" => "application/vnd.oasis.opendocument.formula-template",
|
||||
".otg" => "application/vnd.oasis.opendocument.graphics-template",
|
||||
".oth" => "application/vnd.oasis.opendocument.text-web",
|
||||
".oti" => "application/vnd.oasis.opendocument.image-template",
|
||||
".otm" => "application/vnd.oasis.opendocument.text-master",
|
||||
".ots" => "application/vnd.oasis.opendocument.spreadsheet-template",
|
||||
".ott" => "application/vnd.oasis.opendocument.text-template",
|
||||
".oxt" => "application/vnd.openofficeorg.extension",
|
||||
".p" => "text/x-pascal",
|
||||
".p10" => "application/pkcs10",
|
||||
".p12" => "application/x-pkcs12",
|
||||
".p7b" => "application/x-pkcs7-certificates",
|
||||
".p7m" => "application/pkcs7-mime",
|
||||
".p7r" => "application/x-pkcs7-certreqresp",
|
||||
".p7s" => "application/pkcs7-signature",
|
||||
".pas" => "text/x-pascal",
|
||||
".pbd" => "application/vnd.powerbuilder6",
|
||||
".pbm" => "image/x-portable-bitmap",
|
||||
".pcl" => "application/vnd.hp-pcl",
|
||||
".pclxl" => "application/vnd.hp-pclxl",
|
||||
".pcx" => "image/x-pcx",
|
||||
".pdb" => "chemical/x-pdb",
|
||||
".pdf" => "application/pdf",
|
||||
".pem" => "application/x-x509-ca-cert",
|
||||
".pfr" => "application/font-tdpfr",
|
||||
".pgm" => "image/x-portable-graymap",
|
||||
".pgn" => "application/x-chess-pgn",
|
||||
".pgp" => "application/pgp-encrypted",
|
||||
".pic" => "image/x-pict",
|
||||
".pict" => "image/pict",
|
||||
".pkg" => "application/octet-stream",
|
||||
".pki" => "application/pkixcmp",
|
||||
".pkipath" => "application/pkix-pkipath",
|
||||
".pl" => "text/x-script.perl",
|
||||
".plb" => "application/vnd.3gpp.pic-bw-large",
|
||||
".plc" => "application/vnd.mobius.plc",
|
||||
".plf" => "application/vnd.pocketlearn",
|
||||
".pls" => "application/pls+xml",
|
||||
".pm" => "text/x-script.perl-module",
|
||||
".pml" => "application/vnd.ctc-posml",
|
||||
".png" => "image/png",
|
||||
".pnm" => "image/x-portable-anymap",
|
||||
".pntg" => "image/x-macpaint",
|
||||
".portpkg" => "application/vnd.macports.portpkg",
|
||||
".ppd" => "application/vnd.cups-ppd",
|
||||
".ppm" => "image/x-portable-pixmap",
|
||||
".pps" => "application/vnd.ms-powerpoint",
|
||||
".ppt" => "application/vnd.ms-powerpoint",
|
||||
".prc" => "application/vnd.palm",
|
||||
".pre" => "application/vnd.lotus-freelance",
|
||||
".prf" => "application/pics-rules",
|
||||
".ps" => "application/postscript",
|
||||
".psb" => "application/vnd.3gpp.pic-bw-small",
|
||||
".psd" => "image/vnd.adobe.photoshop",
|
||||
".ptid" => "application/vnd.pvi.ptid1",
|
||||
".pub" => "application/x-mspublisher",
|
||||
".pvb" => "application/vnd.3gpp.pic-bw-var",
|
||||
".pwn" => "application/vnd.3m.post-it-notes",
|
||||
".py" => "text/x-script.python",
|
||||
".pya" => "audio/vnd.ms-playready.media.pya",
|
||||
".pyv" => "video/vnd.ms-playready.media.pyv",
|
||||
".qam" => "application/vnd.epson.quickanime",
|
||||
".qbo" => "application/vnd.intu.qbo",
|
||||
".qfx" => "application/vnd.intu.qfx",
|
||||
".qps" => "application/vnd.publishare-delta-tree",
|
||||
".qt" => "video/quicktime",
|
||||
".qtif" => "image/x-quicktime",
|
||||
".qxd" => "application/vnd.quark.quarkxpress",
|
||||
".ra" => "audio/x-pn-realaudio",
|
||||
".rake" => "text/x-script.ruby",
|
||||
".ram" => "audio/x-pn-realaudio",
|
||||
".rar" => "application/x-rar-compressed",
|
||||
".ras" => "image/x-cmu-raster",
|
||||
".rb" => "text/x-script.ruby",
|
||||
".rcprofile" => "application/vnd.ipunplugged.rcprofile",
|
||||
".rdf" => "application/rdf+xml",
|
||||
".rdz" => "application/vnd.data-vision.rdz",
|
||||
".rep" => "application/vnd.businessobjects",
|
||||
".rgb" => "image/x-rgb",
|
||||
".rif" => "application/reginfo+xml",
|
||||
".rl" => "application/resource-lists+xml",
|
||||
".rlc" => "image/vnd.fujixerox.edmics-rlc",
|
||||
".rld" => "application/resource-lists-diff+xml",
|
||||
".rm" => "application/vnd.rn-realmedia",
|
||||
".rmp" => "audio/x-pn-realaudio-plugin",
|
||||
".rms" => "application/vnd.jcp.javame.midlet-rms",
|
||||
".rnc" => "application/relax-ng-compact-syntax",
|
||||
".roff" => "text/troff",
|
||||
".rpm" => "application/x-redhat-package-manager",
|
||||
".rpss" => "application/vnd.nokia.radio-presets",
|
||||
".rpst" => "application/vnd.nokia.radio-preset",
|
||||
".rq" => "application/sparql-query",
|
||||
".rs" => "application/rls-services+xml",
|
||||
".rsd" => "application/rsd+xml",
|
||||
".rss" => "application/rss+xml",
|
||||
".rtf" => "application/rtf",
|
||||
".rtx" => "text/richtext",
|
||||
".ru" => "text/x-script.ruby",
|
||||
".s" => "text/x-asm",
|
||||
".saf" => "application/vnd.yamaha.smaf-audio",
|
||||
".sbml" => "application/sbml+xml",
|
||||
".sc" => "application/vnd.ibm.secure-container",
|
||||
".scd" => "application/x-msschedule",
|
||||
".scm" => "application/vnd.lotus-screencam",
|
||||
".scq" => "application/scvp-cv-request",
|
||||
".scs" => "application/scvp-cv-response",
|
||||
".sdkm" => "application/vnd.solent.sdkm+xml",
|
||||
".sdp" => "application/sdp",
|
||||
".see" => "application/vnd.seemail",
|
||||
".sema" => "application/vnd.sema",
|
||||
".semd" => "application/vnd.semd",
|
||||
".semf" => "application/vnd.semf",
|
||||
".setpay" => "application/set-payment-initiation",
|
||||
".setreg" => "application/set-registration-initiation",
|
||||
".sfd" => "application/vnd.hydrostatix.sof-data",
|
||||
".sfs" => "application/vnd.spotfire.sfs",
|
||||
".sgm" => "text/sgml",
|
||||
".sgml" => "text/sgml",
|
||||
".sh" => "application/x-sh",
|
||||
".shar" => "application/x-shar",
|
||||
".shf" => "application/shf+xml",
|
||||
".sig" => "application/pgp-signature",
|
||||
".sit" => "application/x-stuffit",
|
||||
".sitx" => "application/x-stuffitx",
|
||||
".skp" => "application/vnd.koan",
|
||||
".slt" => "application/vnd.epson.salt",
|
||||
".smi" => "application/smil+xml",
|
||||
".snd" => "audio/basic",
|
||||
".so" => "application/octet-stream",
|
||||
".spf" => "application/vnd.yamaha.smaf-phrase",
|
||||
".spl" => "application/x-futuresplash",
|
||||
".spot" => "text/vnd.in3d.spot",
|
||||
".spp" => "application/scvp-vp-response",
|
||||
".spq" => "application/scvp-vp-request",
|
||||
".src" => "application/x-wais-source",
|
||||
".srx" => "application/sparql-results+xml",
|
||||
".sse" => "application/vnd.kodak-descriptor",
|
||||
".ssf" => "application/vnd.epson.ssf",
|
||||
".ssml" => "application/ssml+xml",
|
||||
".stf" => "application/vnd.wt.stf",
|
||||
".stk" => "application/hyperstudio",
|
||||
".str" => "application/vnd.pg.format",
|
||||
".sus" => "application/vnd.sus-calendar",
|
||||
".sv4cpio" => "application/x-sv4cpio",
|
||||
".sv4crc" => "application/x-sv4crc",
|
||||
".svd" => "application/vnd.svd",
|
||||
".svg" => "image/svg+xml",
|
||||
".svgz" => "image/svg+xml",
|
||||
".swf" => "application/x-shockwave-flash",
|
||||
".swi" => "application/vnd.arastra.swi",
|
||||
".t" => "text/troff",
|
||||
".tao" => "application/vnd.tao.intent-module-archive",
|
||||
".tar" => "application/x-tar",
|
||||
".tbz" => "application/x-bzip-compressed-tar",
|
||||
".tcap" => "application/vnd.3gpp2.tcap",
|
||||
".tcl" => "application/x-tcl",
|
||||
".tex" => "application/x-tex",
|
||||
".texi" => "application/x-texinfo",
|
||||
".texinfo" => "application/x-texinfo",
|
||||
".text" => "text/plain",
|
||||
".tif" => "image/tiff",
|
||||
".tiff" => "image/tiff",
|
||||
".tmo" => "application/vnd.tmobile-livetv",
|
||||
".torrent" => "application/x-bittorrent",
|
||||
".tpl" => "application/vnd.groove-tool-template",
|
||||
".tpt" => "application/vnd.trid.tpt",
|
||||
".tr" => "text/troff",
|
||||
".tra" => "application/vnd.trueapp",
|
||||
".trm" => "application/x-msterminal",
|
||||
".tsv" => "text/tab-separated-values",
|
||||
".ttf" => "application/octet-stream",
|
||||
".twd" => "application/vnd.simtech-mindmapper",
|
||||
".txd" => "application/vnd.genomatix.tuxedo",
|
||||
".txf" => "application/vnd.mobius.txf",
|
||||
".txt" => "text/plain",
|
||||
".ufd" => "application/vnd.ufdl",
|
||||
".umj" => "application/vnd.umajin",
|
||||
".unityweb" => "application/vnd.unity",
|
||||
".uoml" => "application/vnd.uoml+xml",
|
||||
".uri" => "text/uri-list",
|
||||
".ustar" => "application/x-ustar",
|
||||
".utz" => "application/vnd.uiq.theme",
|
||||
".uu" => "text/x-uuencode",
|
||||
".vcd" => "application/x-cdlink",
|
||||
".vcf" => "text/x-vcard",
|
||||
".vcg" => "application/vnd.groove-vcard",
|
||||
".vcs" => "text/x-vcalendar",
|
||||
".vcx" => "application/vnd.vcx",
|
||||
".vis" => "application/vnd.visionary",
|
||||
".viv" => "video/vnd.vivo",
|
||||
".vrml" => "model/vrml",
|
||||
".vsd" => "application/vnd.visio",
|
||||
".vsf" => "application/vnd.vsf",
|
||||
".vtu" => "model/vnd.vtu",
|
||||
".vxml" => "application/voicexml+xml",
|
||||
".war" => "application/java-archive",
|
||||
".wav" => "audio/x-wav",
|
||||
".wax" => "audio/x-ms-wax",
|
||||
".wbmp" => "image/vnd.wap.wbmp",
|
||||
".wbs" => "application/vnd.criticaltools.wbs+xml",
|
||||
".wbxml" => "application/vnd.wap.wbxml",
|
||||
".webm" => "video/webm",
|
||||
".wm" => "video/x-ms-wm",
|
||||
".wma" => "audio/x-ms-wma",
|
||||
".wmd" => "application/x-ms-wmd",
|
||||
".wmf" => "application/x-msmetafile",
|
||||
".wml" => "text/vnd.wap.wml",
|
||||
".wmlc" => "application/vnd.wap.wmlc",
|
||||
".wmls" => "text/vnd.wap.wmlscript",
|
||||
".wmlsc" => "application/vnd.wap.wmlscriptc",
|
||||
".wmv" => "video/x-ms-wmv",
|
||||
".wmx" => "video/x-ms-wmx",
|
||||
".wmz" => "application/x-ms-wmz",
|
||||
".woff" => "application/font-woff",
|
||||
".woff2" => "application/font-woff2",
|
||||
".wpd" => "application/vnd.wordperfect",
|
||||
".wpl" => "application/vnd.ms-wpl",
|
||||
".wps" => "application/vnd.ms-works",
|
||||
".wqd" => "application/vnd.wqd",
|
||||
".wri" => "application/x-mswrite",
|
||||
".wrl" => "model/vrml",
|
||||
".wsdl" => "application/wsdl+xml",
|
||||
".wspolicy" => "application/wspolicy+xml",
|
||||
".wtb" => "application/vnd.webturbo",
|
||||
".wvx" => "video/x-ms-wvx",
|
||||
".x3d" => "application/vnd.hzn-3d-crossword",
|
||||
".xar" => "application/vnd.xara",
|
||||
".xbd" => "application/vnd.fujixerox.docuworks.binder",
|
||||
".xbm" => "image/x-xbitmap",
|
||||
".xdm" => "application/vnd.syncml.dm+xml",
|
||||
".xdp" => "application/vnd.adobe.xdp+xml",
|
||||
".xdw" => "application/vnd.fujixerox.docuworks",
|
||||
".xenc" => "application/xenc+xml",
|
||||
".xer" => "application/patch-ops-error+xml",
|
||||
".xfdf" => "application/vnd.adobe.xfdf",
|
||||
".xfdl" => "application/vnd.xfdl",
|
||||
".xhtml" => "application/xhtml+xml",
|
||||
".xif" => "image/vnd.xiff",
|
||||
".xls" => "application/vnd.ms-excel",
|
||||
".xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
".xml" => "application/xml",
|
||||
".xo" => "application/vnd.olpc-sugar",
|
||||
".xop" => "application/xop+xml",
|
||||
".xpm" => "image/x-xpixmap",
|
||||
".xpr" => "application/vnd.is-xpr",
|
||||
".xps" => "application/vnd.ms-xpsdocument",
|
||||
".xpw" => "application/vnd.intercon.formnet",
|
||||
".xsl" => "application/xml",
|
||||
".xslt" => "application/xslt+xml",
|
||||
".xsm" => "application/vnd.syncml+xml",
|
||||
".xspf" => "application/xspf+xml",
|
||||
".xul" => "application/vnd.mozilla.xul+xml",
|
||||
".xwd" => "image/x-xwindowdump",
|
||||
".xyz" => "chemical/x-xyz",
|
||||
".yaml" => "text/yaml",
|
||||
".yml" => "text/yaml",
|
||||
".zaz" => "application/vnd.zzazz.deck+xml",
|
||||
".zip" => "application/zip",
|
||||
".zmm" => "application/vnd.handheld-entertainment+xml",
|
||||
}
|
||||
end
|
||||
end
|
||||
@@ -1,198 +0,0 @@
|
||||
require 'uri'
|
||||
require 'stringio'
|
||||
require 'rack'
|
||||
require 'rack/lint'
|
||||
require 'rack/utils'
|
||||
require 'rack/response'
|
||||
|
||||
module Rack
|
||||
# Rack::MockRequest helps testing your Rack application without
|
||||
# actually using HTTP.
|
||||
#
|
||||
# After performing a request on a URL with get/post/put/patch/delete, it
|
||||
# returns a MockResponse with useful helper methods for effective
|
||||
# testing.
|
||||
#
|
||||
# You can pass a hash with additional configuration to the
|
||||
# get/post/put/patch/delete.
|
||||
# <tt>:input</tt>:: A String or IO-like to be used as rack.input.
|
||||
# <tt>:fatal</tt>:: Raise a FatalWarning if the app writes to rack.errors.
|
||||
# <tt>:lint</tt>:: If true, wrap the application in a Rack::Lint.
|
||||
|
||||
class MockRequest
|
||||
class FatalWarning < RuntimeError
|
||||
end
|
||||
|
||||
class FatalWarner
|
||||
def puts(warning)
|
||||
raise FatalWarning, warning
|
||||
end
|
||||
|
||||
def write(warning)
|
||||
raise FatalWarning, warning
|
||||
end
|
||||
|
||||
def flush
|
||||
end
|
||||
|
||||
def string
|
||||
""
|
||||
end
|
||||
end
|
||||
|
||||
DEFAULT_ENV = {
|
||||
"rack.version" => Rack::VERSION,
|
||||
"rack.input" => StringIO.new,
|
||||
"rack.errors" => StringIO.new,
|
||||
"rack.multithread" => true,
|
||||
"rack.multiprocess" => true,
|
||||
"rack.run_once" => false,
|
||||
}
|
||||
|
||||
def initialize(app)
|
||||
@app = app
|
||||
end
|
||||
|
||||
def get(uri, opts={}) request("GET", uri, opts) end
|
||||
def post(uri, opts={}) request("POST", uri, opts) end
|
||||
def put(uri, opts={}) request("PUT", uri, opts) end
|
||||
def patch(uri, opts={}) request("PATCH", uri, opts) end
|
||||
def delete(uri, opts={}) request("DELETE", uri, opts) end
|
||||
def head(uri, opts={}) request("HEAD", uri, opts) end
|
||||
def options(uri, opts={}) request("OPTIONS", uri, opts) end
|
||||
|
||||
def request(method="GET", uri="", opts={})
|
||||
env = self.class.env_for(uri, opts.merge(:method => method))
|
||||
|
||||
if opts[:lint]
|
||||
app = Rack::Lint.new(@app)
|
||||
else
|
||||
app = @app
|
||||
end
|
||||
|
||||
errors = env["rack.errors"]
|
||||
status, headers, body = app.call(env)
|
||||
MockResponse.new(status, headers, body, errors)
|
||||
ensure
|
||||
body.close if body.respond_to?(:close)
|
||||
end
|
||||
|
||||
# For historical reasons, we're pinning to RFC 2396. It's easier for users
|
||||
# and we get support from ruby 1.8 to 2.2 using this method.
|
||||
def self.parse_uri_rfc2396(uri)
|
||||
@parser ||= defined?(URI::RFC2396_Parser) ? URI::RFC2396_Parser.new : URI
|
||||
@parser.parse(uri)
|
||||
end
|
||||
|
||||
# Return the Rack environment used for a request to +uri+.
|
||||
def self.env_for(uri="", opts={})
|
||||
uri = parse_uri_rfc2396(uri)
|
||||
uri.path = "/#{uri.path}" unless uri.path[0] == ?/
|
||||
|
||||
env = DEFAULT_ENV.dup
|
||||
|
||||
env[REQUEST_METHOD] = opts[:method] ? opts[:method].to_s.upcase : "GET"
|
||||
env["SERVER_NAME"] = uri.host || "example.org"
|
||||
env["SERVER_PORT"] = uri.port ? uri.port.to_s : "80"
|
||||
env[QUERY_STRING] = uri.query.to_s
|
||||
env[PATH_INFO] = (!uri.path || uri.path.empty?) ? "/" : uri.path
|
||||
env["rack.url_scheme"] = uri.scheme || "http"
|
||||
env["HTTPS"] = env["rack.url_scheme"] == "https" ? "on" : "off"
|
||||
|
||||
env[SCRIPT_NAME] = opts[:script_name] || ""
|
||||
|
||||
if opts[:fatal]
|
||||
env["rack.errors"] = FatalWarner.new
|
||||
else
|
||||
env["rack.errors"] = StringIO.new
|
||||
end
|
||||
|
||||
if params = opts[:params]
|
||||
if env[REQUEST_METHOD] == "GET"
|
||||
params = Utils.parse_nested_query(params) if params.is_a?(String)
|
||||
params.update(Utils.parse_nested_query(env[QUERY_STRING]))
|
||||
env[QUERY_STRING] = Utils.build_nested_query(params)
|
||||
elsif !opts.has_key?(:input)
|
||||
opts["CONTENT_TYPE"] = "application/x-www-form-urlencoded"
|
||||
if params.is_a?(Hash)
|
||||
if data = Utils::Multipart.build_multipart(params)
|
||||
opts[:input] = data
|
||||
opts["CONTENT_LENGTH"] ||= data.length.to_s
|
||||
opts["CONTENT_TYPE"] = "multipart/form-data; boundary=#{Utils::Multipart::MULTIPART_BOUNDARY}"
|
||||
else
|
||||
opts[:input] = Utils.build_nested_query(params)
|
||||
end
|
||||
else
|
||||
opts[:input] = params
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
empty_str = ""
|
||||
empty_str.force_encoding("ASCII-8BIT") if empty_str.respond_to? :force_encoding
|
||||
opts[:input] ||= empty_str
|
||||
if String === opts[:input]
|
||||
rack_input = StringIO.new(opts[:input])
|
||||
else
|
||||
rack_input = opts[:input]
|
||||
end
|
||||
|
||||
rack_input.set_encoding(Encoding::BINARY) if rack_input.respond_to?(:set_encoding)
|
||||
env['rack.input'] = rack_input
|
||||
|
||||
env["CONTENT_LENGTH"] ||= env["rack.input"].length.to_s
|
||||
|
||||
opts.each { |field, value|
|
||||
env[field] = value if String === field
|
||||
}
|
||||
|
||||
env
|
||||
end
|
||||
end
|
||||
|
||||
# Rack::MockResponse provides useful helpers for testing your apps.
|
||||
# Usually, you don't create the MockResponse on your own, but use
|
||||
# MockRequest.
|
||||
|
||||
class MockResponse < Rack::Response
|
||||
# Headers
|
||||
attr_reader :original_headers
|
||||
|
||||
# Errors
|
||||
attr_accessor :errors
|
||||
|
||||
def initialize(status, headers, body, errors=StringIO.new(""))
|
||||
@original_headers = headers
|
||||
@errors = errors.string if errors.respond_to?(:string)
|
||||
@body_string = nil
|
||||
|
||||
super(body, status, headers)
|
||||
end
|
||||
|
||||
def =~(other)
|
||||
body =~ other
|
||||
end
|
||||
|
||||
def match(other)
|
||||
body.match other
|
||||
end
|
||||
|
||||
def body
|
||||
# FIXME: apparently users of MockResponse expect the return value of
|
||||
# MockResponse#body to be a string. However, the real response object
|
||||
# returns the body as a list.
|
||||
#
|
||||
# See spec_showstatus.rb:
|
||||
#
|
||||
# should "not replace existing messages" do
|
||||
# ...
|
||||
# res.body.should == "foo!"
|
||||
# end
|
||||
super.join
|
||||
end
|
||||
|
||||
def empty?
|
||||
[201, 204, 205, 304].include? status
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,34 +0,0 @@
|
||||
module Rack
|
||||
# A multipart form data parser, adapted from IOWA.
|
||||
#
|
||||
# Usually, Rack::Request#POST takes care of calling this.
|
||||
module Multipart
|
||||
autoload :UploadedFile, 'rack/multipart/uploaded_file'
|
||||
autoload :Parser, 'rack/multipart/parser'
|
||||
autoload :Generator, 'rack/multipart/generator'
|
||||
|
||||
EOL = "\r\n"
|
||||
MULTIPART_BOUNDARY = "AaB03x"
|
||||
MULTIPART = %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|ni
|
||||
TOKEN = /[^\s()<>,;:\\"\/\[\]?=]+/
|
||||
CONDISP = /Content-Disposition:\s*#{TOKEN}\s*/i
|
||||
DISPPARM = /;\s*(#{TOKEN})=("(?:\\"|[^"])*"|#{TOKEN})/
|
||||
RFC2183 = /^#{CONDISP}(#{DISPPARM})+$/i
|
||||
BROKEN_QUOTED = /^#{CONDISP}.*;\sfilename="(.*?)"(?:\s*$|\s*;\s*#{TOKEN}=)/i
|
||||
BROKEN_UNQUOTED = /^#{CONDISP}.*;\sfilename=(#{TOKEN})/i
|
||||
MULTIPART_CONTENT_TYPE = /Content-Type: (.*)#{EOL}/ni
|
||||
MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:.*\s+name="?([^\";]*)"?/ni
|
||||
MULTIPART_CONTENT_ID = /Content-ID:\s*([^#{EOL}]*)/ni
|
||||
|
||||
class << self
|
||||
def parse_multipart(env)
|
||||
Parser.create(env).parse
|
||||
end
|
||||
|
||||
def build_multipart(params, first = true)
|
||||
Generator.new(params, first).dump
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,93 +0,0 @@
|
||||
module Rack
|
||||
module Multipart
|
||||
class Generator
|
||||
def initialize(params, first = true)
|
||||
@params, @first = params, first
|
||||
|
||||
if @first && !@params.is_a?(Hash)
|
||||
raise ArgumentError, "value must be a Hash"
|
||||
end
|
||||
end
|
||||
|
||||
def dump
|
||||
return nil if @first && !multipart?
|
||||
return flattened_params if !@first
|
||||
|
||||
flattened_params.map do |name, file|
|
||||
if file.respond_to?(:original_filename)
|
||||
::File.open(file.path, "rb") do |f|
|
||||
f.set_encoding(Encoding::BINARY) if f.respond_to?(:set_encoding)
|
||||
content_for_tempfile(f, file, name)
|
||||
end
|
||||
else
|
||||
content_for_other(file, name)
|
||||
end
|
||||
end.join + "--#{MULTIPART_BOUNDARY}--\r"
|
||||
end
|
||||
|
||||
private
|
||||
def multipart?
|
||||
multipart = false
|
||||
|
||||
query = lambda { |value|
|
||||
case value
|
||||
when Array
|
||||
value.each(&query)
|
||||
when Hash
|
||||
value.values.each(&query)
|
||||
when Rack::Multipart::UploadedFile
|
||||
multipart = true
|
||||
end
|
||||
}
|
||||
@params.values.each(&query)
|
||||
|
||||
multipart
|
||||
end
|
||||
|
||||
def flattened_params
|
||||
@flattened_params ||= begin
|
||||
h = Hash.new
|
||||
@params.each do |key, value|
|
||||
k = @first ? key.to_s : "[#{key}]"
|
||||
|
||||
case value
|
||||
when Array
|
||||
value.map { |v|
|
||||
Multipart.build_multipart(v, false).each { |subkey, subvalue|
|
||||
h["#{k}[]#{subkey}"] = subvalue
|
||||
}
|
||||
}
|
||||
when Hash
|
||||
Multipart.build_multipart(value, false).each { |subkey, subvalue|
|
||||
h[k + subkey] = subvalue
|
||||
}
|
||||
else
|
||||
h[k] = value
|
||||
end
|
||||
end
|
||||
h
|
||||
end
|
||||
end
|
||||
|
||||
def content_for_tempfile(io, file, name)
|
||||
<<-EOF
|
||||
--#{MULTIPART_BOUNDARY}\r
|
||||
Content-Disposition: form-data; name="#{name}"; filename="#{Utils.escape(file.original_filename)}"\r
|
||||
Content-Type: #{file.content_type}\r
|
||||
Content-Length: #{::File.stat(file.path).size}\r
|
||||
\r
|
||||
#{io.read}\r
|
||||
EOF
|
||||
end
|
||||
|
||||
def content_for_other(file, name)
|
||||
<<-EOF
|
||||
--#{MULTIPART_BOUNDARY}\r
|
||||
Content-Disposition: form-data; name="#{name}"\r
|
||||
\r
|
||||
#{file}\r
|
||||
EOF
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,254 +0,0 @@
|
||||
require 'rack/utils'
|
||||
|
||||
module Rack
|
||||
module Multipart
|
||||
class MultipartPartLimitError < Errno::EMFILE; end
|
||||
|
||||
class Parser
|
||||
BUFSIZE = 16384
|
||||
|
||||
DUMMY = Struct.new(:parse).new
|
||||
|
||||
def self.create(env)
|
||||
return DUMMY unless env['CONTENT_TYPE'] =~ MULTIPART
|
||||
|
||||
io = env['rack.input']
|
||||
io.rewind
|
||||
|
||||
content_length = env['CONTENT_LENGTH']
|
||||
content_length = content_length.to_i if content_length
|
||||
|
||||
tempfile = env['rack.multipart.tempfile_factory'] ||
|
||||
lambda { |filename, content_type| Tempfile.new(["RackMultipart", ::File.extname(filename)]) }
|
||||
bufsize = env['rack.multipart.buffer_size'] || BUFSIZE
|
||||
|
||||
new($1, io, content_length, env, tempfile, bufsize)
|
||||
end
|
||||
|
||||
def initialize(boundary, io, content_length, env, tempfile, bufsize)
|
||||
@buf = ""
|
||||
|
||||
if @buf.respond_to? :force_encoding
|
||||
@buf.force_encoding Encoding::ASCII_8BIT
|
||||
end
|
||||
|
||||
@params = Utils::KeySpaceConstrainedParams.new
|
||||
@boundary = "--#{boundary}"
|
||||
@io = io
|
||||
@content_length = content_length
|
||||
@boundary_size = Utils.bytesize(@boundary) + EOL.size
|
||||
@env = env
|
||||
@tempfile = tempfile
|
||||
@bufsize = bufsize
|
||||
|
||||
if @content_length
|
||||
@content_length -= @boundary_size
|
||||
end
|
||||
|
||||
@rx = /(?:#{EOL})?#{Regexp.quote(@boundary)}(#{EOL}|--)/n
|
||||
@full_boundary = @boundary + EOL
|
||||
end
|
||||
|
||||
def parse
|
||||
fast_forward_to_first_boundary
|
||||
|
||||
opened_files = 0
|
||||
loop do
|
||||
|
||||
head, filename, content_type, name, body =
|
||||
get_current_head_and_filename_and_content_type_and_name_and_body
|
||||
|
||||
if Utils.multipart_part_limit > 0
|
||||
opened_files += 1 if filename
|
||||
raise MultipartPartLimitError, 'Maximum file multiparts in content reached' if opened_files >= Utils.multipart_part_limit
|
||||
end
|
||||
|
||||
# Save the rest.
|
||||
if i = @buf.index(rx)
|
||||
body << @buf.slice!(0, i)
|
||||
@buf.slice!(0, @boundary_size+2)
|
||||
|
||||
@content_length = -1 if $1 == "--"
|
||||
end
|
||||
|
||||
get_data(filename, body, content_type, name, head) do |data|
|
||||
tag_multipart_encoding(filename, content_type, name, data)
|
||||
|
||||
Utils.normalize_params(@params, name, data)
|
||||
end
|
||||
|
||||
# break if we're at the end of a buffer, but not if it is the end of a field
|
||||
break if (@buf.empty? && $1 != EOL) || @content_length == -1
|
||||
end
|
||||
|
||||
@io.rewind
|
||||
|
||||
@params.to_params_hash
|
||||
end
|
||||
|
||||
private
|
||||
def full_boundary; @full_boundary; end
|
||||
|
||||
def rx; @rx; end
|
||||
|
||||
def fast_forward_to_first_boundary
|
||||
loop do
|
||||
content = @io.read(@bufsize)
|
||||
raise EOFError, "bad content body" unless content
|
||||
@buf << content
|
||||
|
||||
while @buf.gsub!(/\A([^\n]*\n)/, '')
|
||||
read_buffer = $1
|
||||
return if read_buffer == full_boundary
|
||||
end
|
||||
|
||||
raise EOFError, "bad content body" if Utils.bytesize(@buf) >= @bufsize
|
||||
end
|
||||
end
|
||||
|
||||
def get_current_head_and_filename_and_content_type_and_name_and_body
|
||||
head = nil
|
||||
body = ''
|
||||
|
||||
if body.respond_to? :force_encoding
|
||||
body.force_encoding Encoding::ASCII_8BIT
|
||||
end
|
||||
|
||||
filename = content_type = name = nil
|
||||
|
||||
until head && @buf =~ rx
|
||||
if !head && i = @buf.index(EOL+EOL)
|
||||
head = @buf.slice!(0, i+2) # First \r\n
|
||||
|
||||
@buf.slice!(0, 2) # Second \r\n
|
||||
|
||||
content_type = head[MULTIPART_CONTENT_TYPE, 1]
|
||||
name = head[MULTIPART_CONTENT_DISPOSITION, 1] || head[MULTIPART_CONTENT_ID, 1]
|
||||
|
||||
filename = get_filename(head)
|
||||
|
||||
if name.nil? || name.empty? && filename
|
||||
name = filename
|
||||
end
|
||||
|
||||
if filename
|
||||
(@env['rack.tempfiles'] ||= []) << body = @tempfile.call(filename, content_type)
|
||||
body.binmode if body.respond_to?(:binmode)
|
||||
end
|
||||
|
||||
next
|
||||
end
|
||||
|
||||
# Save the read body part.
|
||||
if head && (@boundary_size+4 < @buf.size)
|
||||
body << @buf.slice!(0, @buf.size - (@boundary_size+4))
|
||||
end
|
||||
|
||||
content = @io.read(@content_length && @bufsize >= @content_length ? @content_length : @bufsize)
|
||||
raise EOFError, "bad content body" if content.nil? || content.empty?
|
||||
|
||||
@buf << content
|
||||
@content_length -= content.size if @content_length
|
||||
end
|
||||
|
||||
[head, filename, content_type, name, body]
|
||||
end
|
||||
|
||||
def get_filename(head)
|
||||
filename = nil
|
||||
case head
|
||||
when RFC2183
|
||||
filename = Hash[head.scan(DISPPARM)]['filename']
|
||||
filename = $1 if filename and filename =~ /^"(.*)"$/
|
||||
when BROKEN_QUOTED, BROKEN_UNQUOTED
|
||||
filename = $1
|
||||
end
|
||||
|
||||
return unless filename
|
||||
|
||||
if filename.scan(/%.?.?/).all? { |s| s =~ /%[0-9a-fA-F]{2}/ }
|
||||
filename = Utils.unescape(filename)
|
||||
end
|
||||
|
||||
scrub_filename filename
|
||||
|
||||
if filename !~ /\\[^\\"]/
|
||||
filename = filename.gsub(/\\(.)/, '\1')
|
||||
end
|
||||
filename
|
||||
end
|
||||
|
||||
if "<3".respond_to? :valid_encoding?
|
||||
def scrub_filename(filename)
|
||||
unless filename.valid_encoding?
|
||||
# FIXME: this force_encoding is for Ruby 2.0 and 1.9 support.
|
||||
# We can remove it after they are dropped
|
||||
filename.force_encoding(Encoding::ASCII_8BIT)
|
||||
filename.encode!(:invalid => :replace, :undef => :replace)
|
||||
end
|
||||
end
|
||||
|
||||
CHARSET = "charset"
|
||||
TEXT_PLAIN = "text/plain"
|
||||
|
||||
def tag_multipart_encoding(filename, content_type, name, body)
|
||||
name.force_encoding Encoding::UTF_8
|
||||
|
||||
return if filename
|
||||
|
||||
encoding = Encoding::UTF_8
|
||||
|
||||
if content_type
|
||||
list = content_type.split(';')
|
||||
type_subtype = list.first
|
||||
type_subtype.strip!
|
||||
if TEXT_PLAIN == type_subtype
|
||||
rest = list.drop 1
|
||||
rest.each do |param|
|
||||
k,v = param.split('=', 2)
|
||||
k.strip!
|
||||
v.strip!
|
||||
encoding = Encoding.find v if k == CHARSET
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
name.force_encoding encoding
|
||||
body.force_encoding encoding
|
||||
end
|
||||
else
|
||||
def scrub_filename(filename)
|
||||
end
|
||||
def tag_multipart_encoding(filename, content_type, name, body)
|
||||
end
|
||||
end
|
||||
|
||||
def get_data(filename, body, content_type, name, head)
|
||||
data = body
|
||||
if filename == ""
|
||||
# filename is blank which means no file has been selected
|
||||
return
|
||||
elsif filename
|
||||
body.rewind if body.respond_to?(:rewind)
|
||||
|
||||
# Take the basename of the upload's original filename.
|
||||
# This handles the full Windows paths given by Internet Explorer
|
||||
# (and perhaps other broken user agents) without affecting
|
||||
# those which give the lone filename.
|
||||
filename = filename.split(/[\/\\]/).last
|
||||
|
||||
data = {:filename => filename, :type => content_type,
|
||||
:name => name, :tempfile => body, :head => head}
|
||||
elsif !filename && content_type && body.is_a?(IO)
|
||||
body.rewind
|
||||
|
||||
# Generic multipart cases, not coming from a form
|
||||
data = {:type => content_type,
|
||||
:name => name, :tempfile => body, :head => head}
|
||||
end
|
||||
|
||||
yield data
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,34 +0,0 @@
|
||||
module Rack
|
||||
module Multipart
|
||||
class UploadedFile
|
||||
# The filename, *not* including the path, of the "uploaded" file
|
||||
attr_reader :original_filename
|
||||
|
||||
# The content type of the "uploaded" file
|
||||
attr_accessor :content_type
|
||||
|
||||
def initialize(path, content_type = "text/plain", binary = false)
|
||||
raise "#{path} file does not exist" unless ::File.exist?(path)
|
||||
@content_type = content_type
|
||||
@original_filename = ::File.basename(path)
|
||||
@tempfile = Tempfile.new([@original_filename, ::File.extname(path)])
|
||||
@tempfile.set_encoding(Encoding::BINARY) if @tempfile.respond_to?(:set_encoding)
|
||||
@tempfile.binmode if binary
|
||||
FileUtils.copy_file(path, @tempfile.path)
|
||||
end
|
||||
|
||||
def path
|
||||
@tempfile.path
|
||||
end
|
||||
alias_method :local_path, :path
|
||||
|
||||
def respond_to?(*args)
|
||||
super or @tempfile.respond_to?(*args)
|
||||
end
|
||||
|
||||
def method_missing(method_name, *args, &block) #:nodoc:
|
||||
@tempfile.__send__(method_name, *args, &block)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,37 +0,0 @@
|
||||
module Rack
|
||||
class NullLogger
|
||||
def initialize(app)
|
||||
@app = app
|
||||
end
|
||||
|
||||
def call(env)
|
||||
env['rack.logger'] = self
|
||||
@app.call(env)
|
||||
end
|
||||
|
||||
def info(progname = nil, &block); end
|
||||
def debug(progname = nil, &block); end
|
||||
def warn(progname = nil, &block); end
|
||||
def error(progname = nil, &block); end
|
||||
def fatal(progname = nil, &block); end
|
||||
def unknown(progname = nil, &block); end
|
||||
def info? ; end
|
||||
def debug? ; end
|
||||
def warn? ; end
|
||||
def error? ; end
|
||||
def fatal? ; end
|
||||
def level ; end
|
||||
def progname ; end
|
||||
def datetime_format ; end
|
||||
def formatter ; end
|
||||
def sev_threshold ; end
|
||||
def level=(level); end
|
||||
def progname=(progname); end
|
||||
def datetime_format=(datetime_format); end
|
||||
def formatter=(formatter); end
|
||||
def sev_threshold=(sev_threshold); end
|
||||
def close ; end
|
||||
def add(severity, message = nil, progname = nil, &block); end
|
||||
def <<(msg); end
|
||||
end
|
||||
end
|
||||
@@ -1,62 +0,0 @@
|
||||
require 'uri'
|
||||
|
||||
module Rack
|
||||
# Rack::ForwardRequest gets caught by Rack::Recursive and redirects
|
||||
# the current request to the app at +url+.
|
||||
#
|
||||
# raise ForwardRequest.new("/not-found")
|
||||
#
|
||||
|
||||
class ForwardRequest < Exception
|
||||
attr_reader :url, :env
|
||||
|
||||
def initialize(url, env={})
|
||||
@url = URI(url)
|
||||
@env = env
|
||||
|
||||
@env[PATH_INFO] = @url.path
|
||||
@env[QUERY_STRING] = @url.query if @url.query
|
||||
@env["HTTP_HOST"] = @url.host if @url.host
|
||||
@env["HTTP_PORT"] = @url.port if @url.port
|
||||
@env["rack.url_scheme"] = @url.scheme if @url.scheme
|
||||
|
||||
super "forwarding to #{url}"
|
||||
end
|
||||
end
|
||||
|
||||
# Rack::Recursive allows applications called down the chain to
|
||||
# include data from other applications (by using
|
||||
# <tt>rack['rack.recursive.include'][...]</tt> or raise a
|
||||
# ForwardRequest to redirect internally.
|
||||
|
||||
class Recursive
|
||||
def initialize(app)
|
||||
@app = app
|
||||
end
|
||||
|
||||
def call(env)
|
||||
dup._call(env)
|
||||
end
|
||||
|
||||
def _call(env)
|
||||
@script_name = env[SCRIPT_NAME]
|
||||
@app.call(env.merge('rack.recursive.include' => method(:include)))
|
||||
rescue ForwardRequest => req
|
||||
call(env.merge(req.env))
|
||||
end
|
||||
|
||||
def include(env, path)
|
||||
unless path.index(@script_name) == 0 && (path[@script_name.size] == ?/ ||
|
||||
path[@script_name.size].nil?)
|
||||
raise ArgumentError, "can only include below #{@script_name}, not #{path}"
|
||||
end
|
||||
|
||||
env = env.merge(PATH_INFO => path,
|
||||
SCRIPT_NAME => @script_name,
|
||||
REQUEST_METHOD => "GET",
|
||||
"CONTENT_LENGTH" => "0", "CONTENT_TYPE" => "",
|
||||
"rack.input" => StringIO.new(""))
|
||||
@app.call(env)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,109 +0,0 @@
|
||||
# Copyright (c) 2009 Michael Fellinger m.fellinger@gmail.com
|
||||
# Rack::Reloader is subject to the terms of an MIT-style license.
|
||||
# See COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
require 'pathname'
|
||||
|
||||
module Rack
|
||||
|
||||
# High performant source reloader
|
||||
#
|
||||
# This class acts as Rack middleware.
|
||||
#
|
||||
# What makes it especially suited for use in a production environment is that
|
||||
# any file will only be checked once and there will only be made one system
|
||||
# call stat(2).
|
||||
#
|
||||
# Please note that this will not reload files in the background, it does so
|
||||
# only when actively called.
|
||||
#
|
||||
# It is performing a check/reload cycle at the start of every request, but
|
||||
# also respects a cool down time, during which nothing will be done.
|
||||
class Reloader
|
||||
def initialize(app, cooldown = 10, backend = Stat)
|
||||
@app = app
|
||||
@cooldown = cooldown
|
||||
@last = (Time.now - cooldown)
|
||||
@cache = {}
|
||||
@mtimes = {}
|
||||
|
||||
extend backend
|
||||
end
|
||||
|
||||
def call(env)
|
||||
if @cooldown and Time.now > @last + @cooldown
|
||||
if Thread.list.size > 1
|
||||
Thread.exclusive{ reload! }
|
||||
else
|
||||
reload!
|
||||
end
|
||||
|
||||
@last = Time.now
|
||||
end
|
||||
|
||||
@app.call(env)
|
||||
end
|
||||
|
||||
def reload!(stderr = $stderr)
|
||||
rotation do |file, mtime|
|
||||
previous_mtime = @mtimes[file] ||= mtime
|
||||
safe_load(file, mtime, stderr) if mtime > previous_mtime
|
||||
end
|
||||
end
|
||||
|
||||
# A safe Kernel::load, issuing the hooks depending on the results
|
||||
def safe_load(file, mtime, stderr = $stderr)
|
||||
load(file)
|
||||
stderr.puts "#{self.class}: reloaded `#{file}'"
|
||||
file
|
||||
rescue LoadError, SyntaxError => ex
|
||||
stderr.puts ex
|
||||
ensure
|
||||
@mtimes[file] = mtime
|
||||
end
|
||||
|
||||
module Stat
|
||||
def rotation
|
||||
files = [$0, *$LOADED_FEATURES].uniq
|
||||
paths = ['./', *$LOAD_PATH].uniq
|
||||
|
||||
files.map{|file|
|
||||
next if file =~ /\.(so|bundle)$/ # cannot reload compiled files
|
||||
|
||||
found, stat = figure_path(file, paths)
|
||||
next unless found && stat && mtime = stat.mtime
|
||||
|
||||
@cache[file] = found
|
||||
|
||||
yield(found, mtime)
|
||||
}.compact
|
||||
end
|
||||
|
||||
# Takes a relative or absolute +file+ name, a couple possible +paths+ that
|
||||
# the +file+ might reside in. Returns the full path and File::Stat for the
|
||||
# path.
|
||||
def figure_path(file, paths)
|
||||
found = @cache[file]
|
||||
found = file if !found and Pathname.new(file).absolute?
|
||||
found, stat = safe_stat(found)
|
||||
return found, stat if found
|
||||
|
||||
paths.find do |possible_path|
|
||||
path = ::File.join(possible_path, file)
|
||||
found, stat = safe_stat(path)
|
||||
return ::File.expand_path(found), stat if found
|
||||
end
|
||||
|
||||
return false, false
|
||||
end
|
||||
|
||||
def safe_stat(file)
|
||||
return unless file
|
||||
stat = ::File.stat(file)
|
||||
return file, stat if stat.file?
|
||||
rescue Errno::ENOENT, Errno::ENOTDIR, Errno::ESRCH
|
||||
@cache.delete(file) and false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,398 +0,0 @@
|
||||
require 'rack/utils'
|
||||
|
||||
module Rack
|
||||
# Rack::Request provides a convenient interface to a Rack
|
||||
# environment. It is stateless, the environment +env+ passed to the
|
||||
# constructor will be directly modified.
|
||||
#
|
||||
# req = Rack::Request.new(env)
|
||||
# req.post?
|
||||
# req.params["data"]
|
||||
|
||||
class Request
|
||||
# The environment of the request.
|
||||
attr_reader :env
|
||||
|
||||
def initialize(env)
|
||||
@env = env
|
||||
end
|
||||
|
||||
def body; @env["rack.input"] end
|
||||
def script_name; @env[SCRIPT_NAME].to_s end
|
||||
def path_info; @env[PATH_INFO].to_s end
|
||||
def request_method; @env["REQUEST_METHOD"] end
|
||||
def query_string; @env[QUERY_STRING].to_s end
|
||||
def content_length; @env['CONTENT_LENGTH'] end
|
||||
|
||||
def content_type
|
||||
content_type = @env['CONTENT_TYPE']
|
||||
content_type.nil? || content_type.empty? ? nil : content_type
|
||||
end
|
||||
|
||||
def session; @env['rack.session'] ||= {} end
|
||||
def session_options; @env['rack.session.options'] ||= {} end
|
||||
def logger; @env['rack.logger'] end
|
||||
|
||||
# The media type (type/subtype) portion of the CONTENT_TYPE header
|
||||
# without any media type parameters. e.g., when CONTENT_TYPE is
|
||||
# "text/plain;charset=utf-8", the media-type is "text/plain".
|
||||
#
|
||||
# For more information on the use of media types in HTTP, see:
|
||||
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7
|
||||
def media_type
|
||||
content_type && content_type.split(/\s*[;,]\s*/, 2).first.downcase
|
||||
end
|
||||
|
||||
# The media type parameters provided in CONTENT_TYPE as a Hash, or
|
||||
# an empty Hash if no CONTENT_TYPE or media-type parameters were
|
||||
# provided. e.g., when the CONTENT_TYPE is "text/plain;charset=utf-8",
|
||||
# this method responds with the following Hash:
|
||||
# { 'charset' => 'utf-8' }
|
||||
def media_type_params
|
||||
return {} if content_type.nil?
|
||||
Hash[*content_type.split(/\s*[;,]\s*/)[1..-1].
|
||||
collect { |s| s.split('=', 2) }.
|
||||
map { |k,v| [k.downcase, strip_doublequotes(v)] }.flatten]
|
||||
end
|
||||
|
||||
# The character set of the request body if a "charset" media type
|
||||
# parameter was given, or nil if no "charset" was specified. Note
|
||||
# that, per RFC2616, text/* media types that specify no explicit
|
||||
# charset are to be considered ISO-8859-1.
|
||||
def content_charset
|
||||
media_type_params['charset']
|
||||
end
|
||||
|
||||
def scheme
|
||||
if @env['HTTPS'] == 'on'
|
||||
'https'
|
||||
elsif @env['HTTP_X_FORWARDED_SSL'] == 'on'
|
||||
'https'
|
||||
elsif @env['HTTP_X_FORWARDED_SCHEME']
|
||||
@env['HTTP_X_FORWARDED_SCHEME']
|
||||
elsif @env['HTTP_X_FORWARDED_PROTO']
|
||||
@env['HTTP_X_FORWARDED_PROTO'].split(',')[0]
|
||||
else
|
||||
@env["rack.url_scheme"]
|
||||
end
|
||||
end
|
||||
|
||||
def ssl?
|
||||
scheme == 'https'
|
||||
end
|
||||
|
||||
def host_with_port
|
||||
if forwarded = @env["HTTP_X_FORWARDED_HOST"]
|
||||
forwarded.split(/,\s?/).last
|
||||
else
|
||||
@env['HTTP_HOST'] || "#{@env['SERVER_NAME'] || @env['SERVER_ADDR']}:#{@env['SERVER_PORT']}"
|
||||
end
|
||||
end
|
||||
|
||||
def port
|
||||
if port = host_with_port.split(/:/)[1]
|
||||
port.to_i
|
||||
elsif port = @env['HTTP_X_FORWARDED_PORT']
|
||||
port.to_i
|
||||
elsif @env.has_key?("HTTP_X_FORWARDED_HOST")
|
||||
DEFAULT_PORTS[scheme]
|
||||
elsif @env.has_key?("HTTP_X_FORWARDED_PROTO")
|
||||
DEFAULT_PORTS[@env['HTTP_X_FORWARDED_PROTO'].split(',')[0]]
|
||||
else
|
||||
@env["SERVER_PORT"].to_i
|
||||
end
|
||||
end
|
||||
|
||||
def host
|
||||
# Remove port number.
|
||||
host_with_port.to_s.sub(/:\d+\z/, '')
|
||||
end
|
||||
|
||||
def script_name=(s); @env["SCRIPT_NAME"] = s.to_s end
|
||||
def path_info=(s); @env["PATH_INFO"] = s.to_s end
|
||||
|
||||
|
||||
# Checks the HTTP request method (or verb) to see if it was of type DELETE
|
||||
def delete?; request_method == "DELETE" end
|
||||
|
||||
# Checks the HTTP request method (or verb) to see if it was of type GET
|
||||
def get?; request_method == GET end
|
||||
|
||||
# Checks the HTTP request method (or verb) to see if it was of type HEAD
|
||||
def head?; request_method == HEAD end
|
||||
|
||||
# Checks the HTTP request method (or verb) to see if it was of type OPTIONS
|
||||
def options?; request_method == "OPTIONS" end
|
||||
|
||||
# Checks the HTTP request method (or verb) to see if it was of type LINK
|
||||
def link?; request_method == "LINK" end
|
||||
|
||||
# Checks the HTTP request method (or verb) to see if it was of type PATCH
|
||||
def patch?; request_method == "PATCH" end
|
||||
|
||||
# Checks the HTTP request method (or verb) to see if it was of type POST
|
||||
def post?; request_method == "POST" end
|
||||
|
||||
# Checks the HTTP request method (or verb) to see if it was of type PUT
|
||||
def put?; request_method == "PUT" end
|
||||
|
||||
# Checks the HTTP request method (or verb) to see if it was of type TRACE
|
||||
def trace?; request_method == "TRACE" end
|
||||
|
||||
# Checks the HTTP request method (or verb) to see if it was of type UNLINK
|
||||
def unlink?; request_method == "UNLINK" end
|
||||
|
||||
|
||||
# The set of form-data media-types. Requests that do not indicate
|
||||
# one of the media types presents in this list will not be eligible
|
||||
# for form-data / param parsing.
|
||||
FORM_DATA_MEDIA_TYPES = [
|
||||
'application/x-www-form-urlencoded',
|
||||
'multipart/form-data'
|
||||
]
|
||||
|
||||
# The set of media-types. Requests that do not indicate
|
||||
# one of the media types presents in this list will not be eligible
|
||||
# for param parsing like soap attachments or generic multiparts
|
||||
PARSEABLE_DATA_MEDIA_TYPES = [
|
||||
'multipart/related',
|
||||
'multipart/mixed'
|
||||
]
|
||||
|
||||
# Default ports depending on scheme. Used to decide whether or not
|
||||
# to include the port in a generated URI.
|
||||
DEFAULT_PORTS = { 'http' => 80, 'https' => 443, 'coffee' => 80 }
|
||||
|
||||
# Determine whether the request body contains form-data by checking
|
||||
# the request Content-Type for one of the media-types:
|
||||
# "application/x-www-form-urlencoded" or "multipart/form-data". The
|
||||
# list of form-data media types can be modified through the
|
||||
# +FORM_DATA_MEDIA_TYPES+ array.
|
||||
#
|
||||
# A request body is also assumed to contain form-data when no
|
||||
# Content-Type header is provided and the request_method is POST.
|
||||
def form_data?
|
||||
type = media_type
|
||||
meth = env["rack.methodoverride.original_method"] || env[REQUEST_METHOD]
|
||||
(meth == 'POST' && type.nil?) || FORM_DATA_MEDIA_TYPES.include?(type)
|
||||
end
|
||||
|
||||
# Determine whether the request body contains data by checking
|
||||
# the request media_type against registered parse-data media-types
|
||||
def parseable_data?
|
||||
PARSEABLE_DATA_MEDIA_TYPES.include?(media_type)
|
||||
end
|
||||
|
||||
# Returns the data received in the query string.
|
||||
def GET
|
||||
if @env["rack.request.query_string"] == query_string
|
||||
@env["rack.request.query_hash"]
|
||||
else
|
||||
p = parse_query({ :query => query_string, :separator => '&;' })
|
||||
@env["rack.request.query_string"] = query_string
|
||||
@env["rack.request.query_hash"] = p
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the data received in the request body.
|
||||
#
|
||||
# This method support both application/x-www-form-urlencoded and
|
||||
# multipart/form-data.
|
||||
def POST
|
||||
if @env["rack.input"].nil?
|
||||
raise "Missing rack.input"
|
||||
elsif @env["rack.request.form_input"].equal? @env["rack.input"]
|
||||
@env["rack.request.form_hash"]
|
||||
elsif form_data? || parseable_data?
|
||||
unless @env["rack.request.form_hash"] = parse_multipart(env)
|
||||
form_vars = @env["rack.input"].read
|
||||
|
||||
# Fix for Safari Ajax postings that always append \0
|
||||
# form_vars.sub!(/\0\z/, '') # performance replacement:
|
||||
form_vars.slice!(-1) if form_vars[-1] == ?\0
|
||||
|
||||
@env["rack.request.form_vars"] = form_vars
|
||||
@env["rack.request.form_hash"] = parse_query({ :query => form_vars, :separator => '&' })
|
||||
|
||||
@env["rack.input"].rewind
|
||||
end
|
||||
@env["rack.request.form_input"] = @env["rack.input"]
|
||||
@env["rack.request.form_hash"]
|
||||
else
|
||||
{}
|
||||
end
|
||||
end
|
||||
|
||||
# The union of GET and POST data.
|
||||
#
|
||||
# Note that modifications will not be persisted in the env. Use update_param or delete_param if you want to destructively modify params.
|
||||
def params
|
||||
@params ||= self.GET.merge(self.POST)
|
||||
rescue EOFError
|
||||
self.GET.dup
|
||||
end
|
||||
|
||||
# Destructively update a parameter, whether it's in GET and/or POST. Returns nil.
|
||||
#
|
||||
# The parameter is updated wherever it was previous defined, so GET, POST, or both. If it wasn't previously defined, it's inserted into GET.
|
||||
#
|
||||
# env['rack.input'] is not touched.
|
||||
def update_param(k, v)
|
||||
found = false
|
||||
if self.GET.has_key?(k)
|
||||
found = true
|
||||
self.GET[k] = v
|
||||
end
|
||||
if self.POST.has_key?(k)
|
||||
found = true
|
||||
self.POST[k] = v
|
||||
end
|
||||
unless found
|
||||
self.GET[k] = v
|
||||
end
|
||||
@params = nil
|
||||
nil
|
||||
end
|
||||
|
||||
# Destructively delete a parameter, whether it's in GET or POST. Returns the value of the deleted parameter.
|
||||
#
|
||||
# If the parameter is in both GET and POST, the POST value takes precedence since that's how #params works.
|
||||
#
|
||||
# env['rack.input'] is not touched.
|
||||
def delete_param(k)
|
||||
v = [ self.POST.delete(k), self.GET.delete(k) ].compact.first
|
||||
@params = nil
|
||||
v
|
||||
end
|
||||
|
||||
# shortcut for request.params[key]
|
||||
def [](key)
|
||||
params[key.to_s]
|
||||
end
|
||||
|
||||
# shortcut for request.params[key] = value
|
||||
#
|
||||
# Note that modifications will not be persisted in the env. Use update_param or delete_param if you want to destructively modify params.
|
||||
def []=(key, value)
|
||||
params[key.to_s] = value
|
||||
end
|
||||
|
||||
# like Hash#values_at
|
||||
def values_at(*keys)
|
||||
keys.map{|key| params[key] }
|
||||
end
|
||||
|
||||
# the referer of the client
|
||||
def referer
|
||||
@env['HTTP_REFERER']
|
||||
end
|
||||
alias referrer referer
|
||||
|
||||
def user_agent
|
||||
@env['HTTP_USER_AGENT']
|
||||
end
|
||||
|
||||
def cookies
|
||||
hash = @env["rack.request.cookie_hash"] ||= {}
|
||||
string = @env["HTTP_COOKIE"]
|
||||
|
||||
return hash if string == @env["rack.request.cookie_string"]
|
||||
hash.clear
|
||||
|
||||
# According to RFC 2109:
|
||||
# If multiple cookies satisfy the criteria above, they are ordered in
|
||||
# the Cookie header such that those with more specific Path attributes
|
||||
# precede those with less specific. Ordering with respect to other
|
||||
# attributes (e.g., Domain) is unspecified.
|
||||
cookies = Utils.parse_query(string, ';,') { |s| Rack::Utils.unescape(s) rescue s }
|
||||
cookies.each { |k,v| hash[k] = Array === v ? v.first : v }
|
||||
@env["rack.request.cookie_string"] = string
|
||||
hash
|
||||
end
|
||||
|
||||
def xhr?
|
||||
@env["HTTP_X_REQUESTED_WITH"] == "XMLHttpRequest"
|
||||
end
|
||||
|
||||
def base_url
|
||||
url = "#{scheme}://#{host}"
|
||||
url << ":#{port}" if port != DEFAULT_PORTS[scheme]
|
||||
url
|
||||
end
|
||||
|
||||
# Tries to return a remake of the original request URL as a string.
|
||||
def url
|
||||
base_url + fullpath
|
||||
end
|
||||
|
||||
def path
|
||||
script_name + path_info
|
||||
end
|
||||
|
||||
def fullpath
|
||||
query_string.empty? ? path : "#{path}?#{query_string}"
|
||||
end
|
||||
|
||||
def accept_encoding
|
||||
parse_http_accept_header(@env["HTTP_ACCEPT_ENCODING"])
|
||||
end
|
||||
|
||||
def accept_language
|
||||
parse_http_accept_header(@env["HTTP_ACCEPT_LANGUAGE"])
|
||||
end
|
||||
|
||||
def trusted_proxy?(ip)
|
||||
ip =~ /\A127\.0\.0\.1\Z|\A(10|172\.(1[6-9]|2[0-9]|30|31)|192\.168)\.|\A::1\Z|\Afd[0-9a-f]{2}:.+|\Alocalhost\Z|\Aunix\Z|\Aunix:/i
|
||||
end
|
||||
|
||||
def ip
|
||||
remote_addrs = split_ip_addresses(@env['REMOTE_ADDR'])
|
||||
remote_addrs = reject_trusted_ip_addresses(remote_addrs)
|
||||
|
||||
return remote_addrs.first if remote_addrs.any?
|
||||
|
||||
forwarded_ips = split_ip_addresses(@env['HTTP_X_FORWARDED_FOR'])
|
||||
|
||||
return reject_trusted_ip_addresses(forwarded_ips).last || @env["REMOTE_ADDR"]
|
||||
end
|
||||
|
||||
protected
|
||||
def split_ip_addresses(ip_addresses)
|
||||
ip_addresses ? ip_addresses.strip.split(/[,\s]+/) : []
|
||||
end
|
||||
|
||||
def reject_trusted_ip_addresses(ip_addresses)
|
||||
ip_addresses.reject { |ip| trusted_proxy?(ip) }
|
||||
end
|
||||
|
||||
def parse_query(qs)
|
||||
d = '&'
|
||||
qs, d = qs[:query], qs[:separator] if Hash === qs
|
||||
Utils.parse_nested_query(qs, d)
|
||||
end
|
||||
|
||||
def parse_multipart(env)
|
||||
Rack::Multipart.parse_multipart(env)
|
||||
end
|
||||
|
||||
def parse_http_accept_header(header)
|
||||
header.to_s.split(/\s*,\s*/).map do |part|
|
||||
attribute, parameters = part.split(/\s*;\s*/, 2)
|
||||
quality = 1.0
|
||||
if parameters and /\Aq=([\d.]+)/ =~ parameters
|
||||
quality = $1.to_f
|
||||
end
|
||||
[attribute, quality]
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def strip_doublequotes(s)
|
||||
if s[0] == ?" && s[-1] == ?"
|
||||
s[1..-2]
|
||||
else
|
||||
s
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,162 +0,0 @@
|
||||
require 'rack/request'
|
||||
require 'rack/utils'
|
||||
require 'rack/body_proxy'
|
||||
require 'time'
|
||||
|
||||
module Rack
|
||||
# Rack::Response provides a convenient interface to create a Rack
|
||||
# response.
|
||||
#
|
||||
# It allows setting of headers and cookies, and provides useful
|
||||
# defaults (a OK response containing HTML).
|
||||
#
|
||||
# You can use Response#write to iteratively generate your response,
|
||||
# but note that this is buffered by Rack::Response until you call
|
||||
# +finish+. +finish+ however can take a block inside which calls to
|
||||
# +write+ are synchronous with the Rack response.
|
||||
#
|
||||
# Your application's +call+ should end returning Response#finish.
|
||||
|
||||
class Response
|
||||
attr_accessor :length
|
||||
|
||||
CHUNKED = 'chunked'.freeze
|
||||
TRANSFER_ENCODING = 'Transfer-Encoding'.freeze
|
||||
def initialize(body=[], status=200, header={})
|
||||
@status = status.to_i
|
||||
@header = Utils::HeaderHash.new.merge(header)
|
||||
|
||||
@chunked = CHUNKED == @header[TRANSFER_ENCODING]
|
||||
@writer = lambda { |x| @body << x }
|
||||
@block = nil
|
||||
@length = 0
|
||||
|
||||
@body = []
|
||||
|
||||
if body.respond_to? :to_str
|
||||
write body.to_str
|
||||
elsif body.respond_to?(:each)
|
||||
body.each { |part|
|
||||
write part.to_s
|
||||
}
|
||||
else
|
||||
raise TypeError, "stringable or iterable required"
|
||||
end
|
||||
|
||||
yield self if block_given?
|
||||
end
|
||||
|
||||
attr_reader :header
|
||||
attr_accessor :status, :body
|
||||
|
||||
def [](key)
|
||||
header[key]
|
||||
end
|
||||
|
||||
def []=(key, value)
|
||||
header[key] = value
|
||||
end
|
||||
|
||||
def set_cookie(key, value)
|
||||
Utils.set_cookie_header!(header, key, value)
|
||||
end
|
||||
|
||||
def delete_cookie(key, value={})
|
||||
Utils.delete_cookie_header!(header, key, value)
|
||||
end
|
||||
|
||||
def redirect(target, status=302)
|
||||
self.status = status
|
||||
self["Location"] = target
|
||||
end
|
||||
|
||||
def finish(&block)
|
||||
@block = block
|
||||
|
||||
if [204, 205, 304].include?(status.to_i)
|
||||
header.delete CONTENT_TYPE
|
||||
header.delete CONTENT_LENGTH
|
||||
close
|
||||
[status.to_i, header, []]
|
||||
else
|
||||
[status.to_i, header, BodyProxy.new(self){}]
|
||||
end
|
||||
end
|
||||
alias to_a finish # For *response
|
||||
alias to_ary finish # For implicit-splat on Ruby 1.9.2
|
||||
|
||||
def each(&callback)
|
||||
@body.each(&callback)
|
||||
@writer = callback
|
||||
@block.call(self) if @block
|
||||
end
|
||||
|
||||
# Append to body and update Content-Length.
|
||||
#
|
||||
# NOTE: Do not mix #write and direct #body access!
|
||||
#
|
||||
def write(str)
|
||||
s = str.to_s
|
||||
@length += Rack::Utils.bytesize(s) unless @chunked
|
||||
@writer.call s
|
||||
|
||||
header[CONTENT_LENGTH] = @length.to_s unless @chunked
|
||||
str
|
||||
end
|
||||
|
||||
def close
|
||||
body.close if body.respond_to?(:close)
|
||||
end
|
||||
|
||||
def empty?
|
||||
@block == nil && @body.empty?
|
||||
end
|
||||
|
||||
alias headers header
|
||||
|
||||
module Helpers
|
||||
def invalid?; status < 100 || status >= 600; end
|
||||
|
||||
def informational?; status >= 100 && status < 200; end
|
||||
def successful?; status >= 200 && status < 300; end
|
||||
def redirection?; status >= 300 && status < 400; end
|
||||
def client_error?; status >= 400 && status < 500; end
|
||||
def server_error?; status >= 500 && status < 600; end
|
||||
|
||||
def ok?; status == 200; end
|
||||
def created?; status == 201; end
|
||||
def accepted?; status == 202; end
|
||||
def bad_request?; status == 400; end
|
||||
def unauthorized?; status == 401; end
|
||||
def forbidden?; status == 403; end
|
||||
def not_found?; status == 404; end
|
||||
def method_not_allowed?; status == 405; end
|
||||
def i_m_a_teapot?; status == 418; end
|
||||
def unprocessable?; status == 422; end
|
||||
|
||||
def redirect?; [301, 302, 303, 307].include? status; end
|
||||
|
||||
# Headers
|
||||
attr_reader :headers, :original_headers
|
||||
|
||||
def include?(header)
|
||||
!!headers[header]
|
||||
end
|
||||
|
||||
def content_type
|
||||
headers[CONTENT_TYPE]
|
||||
end
|
||||
|
||||
def content_length
|
||||
cl = headers[CONTENT_LENGTH]
|
||||
cl ? cl.to_i : cl
|
||||
end
|
||||
|
||||
def location
|
||||
headers["Location"]
|
||||
end
|
||||
end
|
||||
|
||||
include Helpers
|
||||
end
|
||||
end
|
||||
@@ -1,104 +0,0 @@
|
||||
# -*- encoding: binary -*-
|
||||
require 'tempfile'
|
||||
require 'rack/utils'
|
||||
|
||||
module Rack
|
||||
# Class which can make any IO object rewindable, including non-rewindable ones. It does
|
||||
# this by buffering the data into a tempfile, which is rewindable.
|
||||
#
|
||||
# rack.input is required to be rewindable, so if your input stream IO is non-rewindable
|
||||
# by nature (e.g. a pipe or a socket) then you can wrap it in an object of this class
|
||||
# to easily make it rewindable.
|
||||
#
|
||||
# Don't forget to call #close when you're done. This frees up temporary resources that
|
||||
# RewindableInput uses, though it does *not* close the original IO object.
|
||||
class RewindableInput
|
||||
def initialize(io)
|
||||
@io = io
|
||||
@rewindable_io = nil
|
||||
@unlinked = false
|
||||
end
|
||||
|
||||
def gets
|
||||
make_rewindable unless @rewindable_io
|
||||
@rewindable_io.gets
|
||||
end
|
||||
|
||||
def read(*args)
|
||||
make_rewindable unless @rewindable_io
|
||||
@rewindable_io.read(*args)
|
||||
end
|
||||
|
||||
def each(&block)
|
||||
make_rewindable unless @rewindable_io
|
||||
@rewindable_io.each(&block)
|
||||
end
|
||||
|
||||
def rewind
|
||||
make_rewindable unless @rewindable_io
|
||||
@rewindable_io.rewind
|
||||
end
|
||||
|
||||
# Closes this RewindableInput object without closing the originally
|
||||
# wrapped IO oject. Cleans up any temporary resources that this RewindableInput
|
||||
# has created.
|
||||
#
|
||||
# This method may be called multiple times. It does nothing on subsequent calls.
|
||||
def close
|
||||
if @rewindable_io
|
||||
if @unlinked
|
||||
@rewindable_io.close
|
||||
else
|
||||
@rewindable_io.close!
|
||||
end
|
||||
@rewindable_io = nil
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Ruby's Tempfile class has a bug. Subclass it and fix it.
|
||||
class Tempfile < ::Tempfile
|
||||
def _close
|
||||
@tmpfile.close if @tmpfile
|
||||
@data[1] = nil if @data
|
||||
@tmpfile = nil
|
||||
end
|
||||
end
|
||||
|
||||
def make_rewindable
|
||||
# Buffer all data into a tempfile. Since this tempfile is private to this
|
||||
# RewindableInput object, we chmod it so that nobody else can read or write
|
||||
# it. On POSIX filesystems we also unlink the file so that it doesn't
|
||||
# even have a file entry on the filesystem anymore, though we can still
|
||||
# access it because we have the file handle open.
|
||||
@rewindable_io = Tempfile.new('RackRewindableInput')
|
||||
@rewindable_io.chmod(0000)
|
||||
@rewindable_io.set_encoding(Encoding::BINARY) if @rewindable_io.respond_to?(:set_encoding)
|
||||
@rewindable_io.binmode
|
||||
if filesystem_has_posix_semantics?
|
||||
# Use ::File.unlink as 1.9.1 Tempfile has a bug where unlink closes the file!
|
||||
::File.unlink @rewindable_io.path
|
||||
raise 'Unlink failed. IO closed.' if @rewindable_io.closed?
|
||||
@unlinked = true
|
||||
end
|
||||
|
||||
buffer = ""
|
||||
while @io.read(1024 * 4, buffer)
|
||||
entire_buffer_written_out = false
|
||||
while !entire_buffer_written_out
|
||||
written = @rewindable_io.write(buffer)
|
||||
entire_buffer_written_out = written == Rack::Utils.bytesize(buffer)
|
||||
if !entire_buffer_written_out
|
||||
buffer.slice!(0 .. written - 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
@rewindable_io.rewind
|
||||
end
|
||||
|
||||
def filesystem_has_posix_semantics?
|
||||
RUBY_PLATFORM !~ /(mswin|mingw|cygwin|java)/
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,40 +0,0 @@
|
||||
module Rack
|
||||
# Sets an "X-Runtime" response header, indicating the response
|
||||
# time of the request, in seconds
|
||||
#
|
||||
# You can put it right before the application to see the processing
|
||||
# time, or before all the other middlewares to include time for them,
|
||||
# too.
|
||||
class Runtime
|
||||
def initialize(app, name = nil)
|
||||
@app = app
|
||||
@header_name = "X-Runtime"
|
||||
@header_name << "-#{name}" if name
|
||||
end
|
||||
|
||||
FORMAT_STRING = "%0.6f"
|
||||
def call(env)
|
||||
start_time = clock_time
|
||||
status, headers, body = @app.call(env)
|
||||
request_time = clock_time - start_time
|
||||
|
||||
if !headers.has_key?(@header_name)
|
||||
headers[@header_name] = FORMAT_STRING % request_time
|
||||
end
|
||||
|
||||
[status, headers, body]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
if defined?(Process::CLOCK_MONOTONIC)
|
||||
def clock_time
|
||||
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
||||
end
|
||||
else
|
||||
def clock_time
|
||||
Time.now.to_f
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,160 +0,0 @@
|
||||
require 'rack/file'
|
||||
require 'rack/body_proxy'
|
||||
|
||||
module Rack
|
||||
|
||||
# = Sendfile
|
||||
#
|
||||
# The Sendfile middleware intercepts responses whose body is being
|
||||
# served from a file and replaces it with a server specific X-Sendfile
|
||||
# header. The web server is then responsible for writing the file contents
|
||||
# to the client. This can dramatically reduce the amount of work required
|
||||
# by the Ruby backend and takes advantage of the web server's optimized file
|
||||
# delivery code.
|
||||
#
|
||||
# In order to take advantage of this middleware, the response body must
|
||||
# respond to +to_path+ and the request must include an X-Sendfile-Type
|
||||
# header. Rack::File and other components implement +to_path+ so there's
|
||||
# rarely anything you need to do in your application. The X-Sendfile-Type
|
||||
# header is typically set in your web servers configuration. The following
|
||||
# sections attempt to document
|
||||
#
|
||||
# === Nginx
|
||||
#
|
||||
# Nginx supports the X-Accel-Redirect header. This is similar to X-Sendfile
|
||||
# but requires parts of the filesystem to be mapped into a private URL
|
||||
# hierarchy.
|
||||
#
|
||||
# The following example shows the Nginx configuration required to create
|
||||
# a private "/files/" area, enable X-Accel-Redirect, and pass the special
|
||||
# X-Sendfile-Type and X-Accel-Mapping headers to the backend:
|
||||
#
|
||||
# location ~ /files/(.*) {
|
||||
# internal;
|
||||
# alias /var/www/$1;
|
||||
# }
|
||||
#
|
||||
# location / {
|
||||
# proxy_redirect off;
|
||||
#
|
||||
# proxy_set_header Host $host;
|
||||
# proxy_set_header X-Real-IP $remote_addr;
|
||||
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
#
|
||||
# proxy_set_header X-Sendfile-Type X-Accel-Redirect;
|
||||
# proxy_set_header X-Accel-Mapping /var/www/=/files/;
|
||||
#
|
||||
# proxy_pass http://127.0.0.1:8080/;
|
||||
# }
|
||||
#
|
||||
# Note that the X-Sendfile-Type header must be set exactly as shown above.
|
||||
# The X-Accel-Mapping header should specify the location on the file system,
|
||||
# followed by an equals sign (=), followed name of the private URL pattern
|
||||
# that it maps to. The middleware performs a simple substitution on the
|
||||
# resulting path.
|
||||
#
|
||||
# See Also: http://wiki.codemongers.com/NginxXSendfile
|
||||
#
|
||||
# === lighttpd
|
||||
#
|
||||
# Lighttpd has supported some variation of the X-Sendfile header for some
|
||||
# time, although only recent version support X-Sendfile in a reverse proxy
|
||||
# configuration.
|
||||
#
|
||||
# $HTTP["host"] == "example.com" {
|
||||
# proxy-core.protocol = "http"
|
||||
# proxy-core.balancer = "round-robin"
|
||||
# proxy-core.backends = (
|
||||
# "127.0.0.1:8000",
|
||||
# "127.0.0.1:8001",
|
||||
# ...
|
||||
# )
|
||||
#
|
||||
# proxy-core.allow-x-sendfile = "enable"
|
||||
# proxy-core.rewrite-request = (
|
||||
# "X-Sendfile-Type" => (".*" => "X-Sendfile")
|
||||
# )
|
||||
# }
|
||||
#
|
||||
# See Also: http://redmine.lighttpd.net/wiki/lighttpd/Docs:ModProxyCore
|
||||
#
|
||||
# === Apache
|
||||
#
|
||||
# X-Sendfile is supported under Apache 2.x using a separate module:
|
||||
#
|
||||
# https://tn123.org/mod_xsendfile/
|
||||
#
|
||||
# Once the module is compiled and installed, you can enable it using
|
||||
# XSendFile config directive:
|
||||
#
|
||||
# RequestHeader Set X-Sendfile-Type X-Sendfile
|
||||
# ProxyPassReverse / http://localhost:8001/
|
||||
# XSendFile on
|
||||
#
|
||||
# === Mapping parameter
|
||||
#
|
||||
# The third parameter allows for an overriding extension of the
|
||||
# X-Accel-Mapping header. Mappings should be provided in tuples of internal to
|
||||
# external. The internal values may contain regular expression syntax, they
|
||||
# will be matched with case indifference.
|
||||
|
||||
class Sendfile
|
||||
F = ::File
|
||||
|
||||
def initialize(app, variation=nil, mappings=[])
|
||||
@app = app
|
||||
@variation = variation
|
||||
@mappings = mappings.map do |internal, external|
|
||||
[/^#{internal}/i, external]
|
||||
end
|
||||
end
|
||||
|
||||
def call(env)
|
||||
status, headers, body = @app.call(env)
|
||||
if body.respond_to?(:to_path)
|
||||
case type = variation(env)
|
||||
when 'X-Accel-Redirect'
|
||||
path = F.expand_path(body.to_path)
|
||||
if url = map_accel_path(env, path)
|
||||
headers[CONTENT_LENGTH] = '0'
|
||||
headers[type] = url
|
||||
obody = body
|
||||
body = Rack::BodyProxy.new([]) do
|
||||
obody.close if obody.respond_to?(:close)
|
||||
end
|
||||
else
|
||||
env['rack.errors'].puts "X-Accel-Mapping header missing"
|
||||
end
|
||||
when 'X-Sendfile', 'X-Lighttpd-Send-File'
|
||||
path = F.expand_path(body.to_path)
|
||||
headers[CONTENT_LENGTH] = '0'
|
||||
headers[type] = path
|
||||
obody = body
|
||||
body = Rack::BodyProxy.new([]) do
|
||||
obody.close if obody.respond_to?(:close)
|
||||
end
|
||||
when '', nil
|
||||
else
|
||||
env['rack.errors'].puts "Unknown x-sendfile variation: '#{type}'.\n"
|
||||
end
|
||||
end
|
||||
[status, headers, body]
|
||||
end
|
||||
|
||||
private
|
||||
def variation(env)
|
||||
@variation ||
|
||||
env['sendfile.type'] ||
|
||||
env['HTTP_X_SENDFILE_TYPE']
|
||||
end
|
||||
|
||||
def map_accel_path(env, path)
|
||||
if mapping = @mappings.find { |internal,_| internal =~ path }
|
||||
path.sub(*mapping)
|
||||
elsif mapping = env['HTTP_X_ACCEL_MAPPING']
|
||||
internal, external = mapping.split('=', 2).map{ |p| p.strip }
|
||||
path.sub(/^#{internal}/i, external)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,387 +0,0 @@
|
||||
require 'optparse'
|
||||
|
||||
|
||||
module Rack
|
||||
|
||||
class Server
|
||||
|
||||
class Options
|
||||
def parse!(args)
|
||||
options = {}
|
||||
opt_parser = OptionParser.new("", 24, ' ') do |opts|
|
||||
opts.banner = "Usage: rackup [ruby options] [rack options] [rackup config]"
|
||||
|
||||
opts.separator ""
|
||||
opts.separator "Ruby options:"
|
||||
|
||||
lineno = 1
|
||||
opts.on("-e", "--eval LINE", "evaluate a LINE of code") { |line|
|
||||
eval line, TOPLEVEL_BINDING, "-e", lineno
|
||||
lineno += 1
|
||||
}
|
||||
|
||||
opts.on("-b", "--builder BUILDER_LINE", "evaluate a BUILDER_LINE of code as a builder script") { |line|
|
||||
options[:builder] = line
|
||||
}
|
||||
|
||||
opts.on("-d", "--debug", "set debugging flags (set $DEBUG to true)") {
|
||||
options[:debug] = true
|
||||
}
|
||||
opts.on("-w", "--warn", "turn warnings on for your script") {
|
||||
options[:warn] = true
|
||||
}
|
||||
opts.on("-q", "--quiet", "turn off logging") {
|
||||
options[:quiet] = true
|
||||
}
|
||||
|
||||
opts.on("-I", "--include PATH",
|
||||
"specify $LOAD_PATH (may be used more than once)") { |path|
|
||||
(options[:include] ||= []).concat(path.split(":"))
|
||||
}
|
||||
|
||||
opts.on("-r", "--require LIBRARY",
|
||||
"require the library, before executing your script") { |library|
|
||||
options[:require] = library
|
||||
}
|
||||
|
||||
opts.separator ""
|
||||
opts.separator "Rack options:"
|
||||
opts.on("-s", "--server SERVER", "serve using SERVER (thin/puma/webrick/mongrel)") { |s|
|
||||
options[:server] = s
|
||||
}
|
||||
|
||||
opts.on("-o", "--host HOST", "listen on HOST (default: localhost)") { |host|
|
||||
options[:Host] = host
|
||||
}
|
||||
|
||||
opts.on("-p", "--port PORT", "use PORT (default: 9292)") { |port|
|
||||
options[:Port] = port
|
||||
}
|
||||
|
||||
opts.on("-O", "--option NAME[=VALUE]", "pass VALUE to the server as option NAME. If no VALUE, sets it to true. Run '#{$0} -s SERVER -h' to get a list of options for SERVER") { |name|
|
||||
name, value = name.split('=', 2)
|
||||
value = true if value.nil?
|
||||
options[name.to_sym] = value
|
||||
}
|
||||
|
||||
opts.on("-E", "--env ENVIRONMENT", "use ENVIRONMENT for defaults (default: development)") { |e|
|
||||
options[:environment] = e
|
||||
}
|
||||
|
||||
opts.on("-D", "--daemonize", "run daemonized in the background") { |d|
|
||||
options[:daemonize] = d ? true : false
|
||||
}
|
||||
|
||||
opts.on("-P", "--pid FILE", "file to store PID") { |f|
|
||||
options[:pid] = ::File.expand_path(f)
|
||||
}
|
||||
|
||||
opts.separator ""
|
||||
opts.separator "Common options:"
|
||||
|
||||
opts.on_tail("-h", "-?", "--help", "Show this message") do
|
||||
puts opts
|
||||
puts handler_opts(options)
|
||||
|
||||
exit
|
||||
end
|
||||
|
||||
opts.on_tail("--version", "Show version") do
|
||||
puts "Rack #{Rack.version} (Release: #{Rack.release})"
|
||||
exit
|
||||
end
|
||||
end
|
||||
|
||||
begin
|
||||
opt_parser.parse! args
|
||||
rescue OptionParser::InvalidOption => e
|
||||
warn e.message
|
||||
abort opt_parser.to_s
|
||||
end
|
||||
|
||||
options[:config] = args.last if args.last
|
||||
options
|
||||
end
|
||||
|
||||
def handler_opts(options)
|
||||
begin
|
||||
info = []
|
||||
server = Rack::Handler.get(options[:server]) || Rack::Handler.default(options)
|
||||
if server && server.respond_to?(:valid_options)
|
||||
info << ""
|
||||
info << "Server-specific options for #{server.name}:"
|
||||
|
||||
has_options = false
|
||||
server.valid_options.each do |name, description|
|
||||
next if name.to_s.match(/^(Host|Port)[^a-zA-Z]/) # ignore handler's host and port options, we do our own.
|
||||
info << " -O %-21s %s" % [name, description]
|
||||
has_options = true
|
||||
end
|
||||
return "" if !has_options
|
||||
end
|
||||
info.join("\n")
|
||||
rescue NameError
|
||||
return "Warning: Could not find handler specified (#{options[:server] || 'default'}) to determine handler-specific options"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Start a new rack server (like running rackup). This will parse ARGV and
|
||||
# provide standard ARGV rackup options, defaulting to load 'config.ru'.
|
||||
#
|
||||
# Providing an options hash will prevent ARGV parsing and will not include
|
||||
# any default options.
|
||||
#
|
||||
# This method can be used to very easily launch a CGI application, for
|
||||
# example:
|
||||
#
|
||||
# Rack::Server.start(
|
||||
# :app => lambda do |e|
|
||||
# [200, {'Content-Type' => 'text/html'}, ['hello world']]
|
||||
# end,
|
||||
# :server => 'cgi'
|
||||
# )
|
||||
#
|
||||
# Further options available here are documented on Rack::Server#initialize
|
||||
def self.start(options = nil)
|
||||
new(options).start
|
||||
end
|
||||
|
||||
attr_writer :options
|
||||
|
||||
# Options may include:
|
||||
# * :app
|
||||
# a rack application to run (overrides :config)
|
||||
# * :config
|
||||
# a rackup configuration file path to load (.ru)
|
||||
# * :environment
|
||||
# this selects the middleware that will be wrapped around
|
||||
# your application. Default options available are:
|
||||
# - development: CommonLogger, ShowExceptions, and Lint
|
||||
# - deployment: CommonLogger
|
||||
# - none: no extra middleware
|
||||
# note: when the server is a cgi server, CommonLogger is not included.
|
||||
# * :server
|
||||
# choose a specific Rack::Handler, e.g. cgi, fcgi, webrick
|
||||
# * :daemonize
|
||||
# if true, the server will daemonize itself (fork, detach, etc)
|
||||
# * :pid
|
||||
# path to write a pid file after daemonize
|
||||
# * :Host
|
||||
# the host address to bind to (used by supporting Rack::Handler)
|
||||
# * :Port
|
||||
# the port to bind to (used by supporting Rack::Handler)
|
||||
# * :AccessLog
|
||||
# webrick access log options (or supporting Rack::Handler)
|
||||
# * :debug
|
||||
# turn on debug output ($DEBUG = true)
|
||||
# * :warn
|
||||
# turn on warnings ($-w = true)
|
||||
# * :include
|
||||
# add given paths to $LOAD_PATH
|
||||
# * :require
|
||||
# require the given libraries
|
||||
def initialize(options = nil)
|
||||
@options = options
|
||||
@app = options[:app] if options && options[:app]
|
||||
end
|
||||
|
||||
def options
|
||||
@options ||= parse_options(ARGV)
|
||||
end
|
||||
|
||||
def default_options
|
||||
environment = ENV['RACK_ENV'] || 'development'
|
||||
default_host = environment == 'development' ? 'localhost' : '0.0.0.0'
|
||||
|
||||
{
|
||||
:environment => environment,
|
||||
:pid => nil,
|
||||
:Port => 9292,
|
||||
:Host => default_host,
|
||||
:AccessLog => [],
|
||||
:config => "config.ru"
|
||||
}
|
||||
end
|
||||
|
||||
def app
|
||||
@app ||= options[:builder] ? build_app_from_string : build_app_and_options_from_config
|
||||
end
|
||||
|
||||
class << self
|
||||
def logging_middleware
|
||||
lambda { |server|
|
||||
server.server.name =~ /CGI/ || server.options[:quiet] ? nil : [Rack::CommonLogger, $stderr]
|
||||
}
|
||||
end
|
||||
|
||||
def default_middleware_by_environment
|
||||
m = Hash.new {|h,k| h[k] = []}
|
||||
m["deployment"] = [
|
||||
[Rack::ContentLength],
|
||||
[Rack::Chunked],
|
||||
logging_middleware,
|
||||
[Rack::TempfileReaper]
|
||||
]
|
||||
m["development"] = [
|
||||
[Rack::ContentLength],
|
||||
[Rack::Chunked],
|
||||
logging_middleware,
|
||||
[Rack::ShowExceptions],
|
||||
[Rack::Lint],
|
||||
[Rack::TempfileReaper]
|
||||
]
|
||||
|
||||
m
|
||||
end
|
||||
|
||||
def middleware
|
||||
default_middleware_by_environment
|
||||
end
|
||||
end
|
||||
|
||||
def middleware
|
||||
self.class.middleware
|
||||
end
|
||||
|
||||
def start &blk
|
||||
if options[:warn]
|
||||
$-w = true
|
||||
end
|
||||
|
||||
if includes = options[:include]
|
||||
$LOAD_PATH.unshift(*includes)
|
||||
end
|
||||
|
||||
if library = options[:require]
|
||||
require library
|
||||
end
|
||||
|
||||
if options[:debug]
|
||||
$DEBUG = true
|
||||
require 'pp'
|
||||
p options[:server]
|
||||
pp wrapped_app
|
||||
pp app
|
||||
end
|
||||
|
||||
check_pid! if options[:pid]
|
||||
|
||||
# Touch the wrapped app, so that the config.ru is loaded before
|
||||
# daemonization (i.e. before chdir, etc).
|
||||
wrapped_app
|
||||
|
||||
daemonize_app if options[:daemonize]
|
||||
|
||||
write_pid if options[:pid]
|
||||
|
||||
trap(:INT) do
|
||||
if server.respond_to?(:shutdown)
|
||||
server.shutdown
|
||||
else
|
||||
exit
|
||||
end
|
||||
end
|
||||
|
||||
server.run wrapped_app, options, &blk
|
||||
end
|
||||
|
||||
def server
|
||||
@_server ||= Rack::Handler.get(options[:server]) || Rack::Handler.default(options)
|
||||
end
|
||||
|
||||
private
|
||||
def build_app_and_options_from_config
|
||||
if !::File.exist? options[:config]
|
||||
abort "configuration #{options[:config]} not found"
|
||||
end
|
||||
|
||||
app, options = Rack::Builder.parse_file(self.options[:config], opt_parser)
|
||||
self.options.merge! options
|
||||
app
|
||||
end
|
||||
|
||||
def build_app_from_string
|
||||
Rack::Builder.new_from_string(self.options[:builder])
|
||||
end
|
||||
|
||||
def parse_options(args)
|
||||
options = default_options
|
||||
|
||||
# Don't evaluate CGI ISINDEX parameters.
|
||||
# http://www.meb.uni-bonn.de/docs/cgi/cl.html
|
||||
args.clear if ENV.include?(REQUEST_METHOD)
|
||||
|
||||
options.merge! opt_parser.parse!(args)
|
||||
options[:config] = ::File.expand_path(options[:config])
|
||||
ENV["RACK_ENV"] = options[:environment]
|
||||
options
|
||||
end
|
||||
|
||||
def opt_parser
|
||||
Options.new
|
||||
end
|
||||
|
||||
def build_app(app)
|
||||
middleware[options[:environment]].reverse_each do |middleware|
|
||||
middleware = middleware.call(self) if middleware.respond_to?(:call)
|
||||
next unless middleware
|
||||
klass, *args = middleware
|
||||
app = klass.new(app, *args)
|
||||
end
|
||||
app
|
||||
end
|
||||
|
||||
def wrapped_app
|
||||
@wrapped_app ||= build_app app
|
||||
end
|
||||
|
||||
def daemonize_app
|
||||
if RUBY_VERSION < "1.9"
|
||||
exit if fork
|
||||
Process.setsid
|
||||
exit if fork
|
||||
Dir.chdir "/"
|
||||
STDIN.reopen "/dev/null"
|
||||
STDOUT.reopen "/dev/null", "a"
|
||||
STDERR.reopen "/dev/null", "a"
|
||||
else
|
||||
Process.daemon
|
||||
end
|
||||
end
|
||||
|
||||
def write_pid
|
||||
::File.open(options[:pid], ::File::CREAT | ::File::EXCL | ::File::WRONLY ){ |f| f.write("#{Process.pid}") }
|
||||
at_exit { ::File.delete(options[:pid]) if ::File.exist?(options[:pid]) }
|
||||
rescue Errno::EEXIST
|
||||
check_pid!
|
||||
retry
|
||||
end
|
||||
|
||||
def check_pid!
|
||||
case pidfile_process_status
|
||||
when :running, :not_owned
|
||||
$stderr.puts "A server is already running. Check #{options[:pid]}."
|
||||
exit(1)
|
||||
when :dead
|
||||
::File.delete(options[:pid])
|
||||
end
|
||||
end
|
||||
|
||||
def pidfile_process_status
|
||||
return :exited unless ::File.exist?(options[:pid])
|
||||
|
||||
pid = ::File.read(options[:pid]).to_i
|
||||
return :dead if pid == 0
|
||||
|
||||
Process.kill(0, pid)
|
||||
:running
|
||||
rescue Errno::ESRCH
|
||||
:dead
|
||||
rescue Errno::EPERM
|
||||
:not_owned
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
@@ -1,399 +0,0 @@
|
||||
# AUTHOR: blink <blinketje@gmail.com>; blink#ruby-lang@irc.freenode.net
|
||||
# bugrep: Andreas Zehnder
|
||||
|
||||
require 'time'
|
||||
require 'rack/request'
|
||||
require 'rack/response'
|
||||
begin
|
||||
require 'securerandom'
|
||||
rescue LoadError
|
||||
# We just won't get securerandom
|
||||
end
|
||||
|
||||
module Rack
|
||||
|
||||
module Session
|
||||
|
||||
module Abstract
|
||||
ENV_SESSION_KEY = 'rack.session'.freeze
|
||||
ENV_SESSION_OPTIONS_KEY = 'rack.session.options'.freeze
|
||||
|
||||
# SessionHash is responsible to lazily load the session from store.
|
||||
|
||||
class SessionHash
|
||||
include Enumerable
|
||||
attr_writer :id
|
||||
|
||||
def self.find(env)
|
||||
env[ENV_SESSION_KEY]
|
||||
end
|
||||
|
||||
def self.set(env, session)
|
||||
env[ENV_SESSION_KEY] = session
|
||||
end
|
||||
|
||||
def self.set_options(env, options)
|
||||
env[ENV_SESSION_OPTIONS_KEY] = options.dup
|
||||
end
|
||||
|
||||
def initialize(store, env)
|
||||
@store = store
|
||||
@env = env
|
||||
@loaded = false
|
||||
end
|
||||
|
||||
def id
|
||||
return @id if @loaded or instance_variable_defined?(:@id)
|
||||
@id = @store.send(:extract_session_id, @env)
|
||||
end
|
||||
|
||||
def options
|
||||
@env[ENV_SESSION_OPTIONS_KEY]
|
||||
end
|
||||
|
||||
def each(&block)
|
||||
load_for_read!
|
||||
@data.each(&block)
|
||||
end
|
||||
|
||||
def [](key)
|
||||
load_for_read!
|
||||
@data[key.to_s]
|
||||
end
|
||||
alias :fetch :[]
|
||||
|
||||
def has_key?(key)
|
||||
load_for_read!
|
||||
@data.has_key?(key.to_s)
|
||||
end
|
||||
alias :key? :has_key?
|
||||
alias :include? :has_key?
|
||||
|
||||
def []=(key, value)
|
||||
load_for_write!
|
||||
@data[key.to_s] = value
|
||||
end
|
||||
alias :store :[]=
|
||||
|
||||
def clear
|
||||
load_for_write!
|
||||
@data.clear
|
||||
end
|
||||
|
||||
def destroy
|
||||
clear
|
||||
@id = @store.send(:destroy_session, @env, id, options)
|
||||
end
|
||||
|
||||
def to_hash
|
||||
load_for_read!
|
||||
@data.dup
|
||||
end
|
||||
|
||||
def update(hash)
|
||||
load_for_write!
|
||||
@data.update(stringify_keys(hash))
|
||||
end
|
||||
alias :merge! :update
|
||||
|
||||
def replace(hash)
|
||||
load_for_write!
|
||||
@data.replace(stringify_keys(hash))
|
||||
end
|
||||
|
||||
def delete(key)
|
||||
load_for_write!
|
||||
@data.delete(key.to_s)
|
||||
end
|
||||
|
||||
def inspect
|
||||
if loaded?
|
||||
@data.inspect
|
||||
else
|
||||
"#<#{self.class}:0x#{self.object_id.to_s(16)} not yet loaded>"
|
||||
end
|
||||
end
|
||||
|
||||
def exists?
|
||||
return @exists if instance_variable_defined?(:@exists)
|
||||
@data = {}
|
||||
@exists = @store.send(:session_exists?, @env)
|
||||
end
|
||||
|
||||
def loaded?
|
||||
@loaded
|
||||
end
|
||||
|
||||
def empty?
|
||||
load_for_read!
|
||||
@data.empty?
|
||||
end
|
||||
|
||||
def keys
|
||||
@data.keys
|
||||
end
|
||||
|
||||
def values
|
||||
@data.values
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_for_read!
|
||||
load! if !loaded? && exists?
|
||||
end
|
||||
|
||||
def load_for_write!
|
||||
load! unless loaded?
|
||||
end
|
||||
|
||||
def load!
|
||||
@id, session = @store.send(:load_session, @env)
|
||||
@data = stringify_keys(session)
|
||||
@loaded = true
|
||||
end
|
||||
|
||||
def stringify_keys(other)
|
||||
hash = {}
|
||||
other.each do |key, value|
|
||||
hash[key.to_s] = value
|
||||
end
|
||||
hash
|
||||
end
|
||||
end
|
||||
|
||||
# ID sets up a basic framework for implementing an id based sessioning
|
||||
# service. Cookies sent to the client for maintaining sessions will only
|
||||
# contain an id reference. Only #get_session and #set_session are
|
||||
# required to be overwritten.
|
||||
#
|
||||
# All parameters are optional.
|
||||
# * :key determines the name of the cookie, by default it is
|
||||
# 'rack.session'
|
||||
# * :path, :domain, :expire_after, :secure, and :httponly set the related
|
||||
# cookie options as by Rack::Response#add_cookie
|
||||
# * :skip will not a set a cookie in the response nor update the session state
|
||||
# * :defer will not set a cookie in the response but still update the session
|
||||
# state if it is used with a backend
|
||||
# * :renew (implementation dependent) will prompt the generation of a new
|
||||
# session id, and migration of data to be referenced at the new id. If
|
||||
# :defer is set, it will be overridden and the cookie will be set.
|
||||
# * :sidbits sets the number of bits in length that a generated session
|
||||
# id will be.
|
||||
#
|
||||
# These options can be set on a per request basis, at the location of
|
||||
# env['rack.session.options']. Additionally the id of the session can be
|
||||
# found within the options hash at the key :id. It is highly not
|
||||
# recommended to change its value.
|
||||
#
|
||||
# Is Rack::Utils::Context compatible.
|
||||
#
|
||||
# Not included by default; you must require 'rack/session/abstract/id'
|
||||
# to use.
|
||||
|
||||
class ID
|
||||
DEFAULT_OPTIONS = {
|
||||
:key => 'rack.session',
|
||||
:path => '/',
|
||||
:domain => nil,
|
||||
:expire_after => nil,
|
||||
:secure => false,
|
||||
:httponly => true,
|
||||
:defer => false,
|
||||
:renew => false,
|
||||
:sidbits => 128,
|
||||
:cookie_only => true,
|
||||
:secure_random => (::SecureRandom rescue false)
|
||||
}
|
||||
|
||||
attr_reader :key, :default_options
|
||||
|
||||
def initialize(app, options={})
|
||||
@app = app
|
||||
@default_options = self.class::DEFAULT_OPTIONS.merge(options)
|
||||
@key = @default_options.delete(:key)
|
||||
@cookie_only = @default_options.delete(:cookie_only)
|
||||
initialize_sid
|
||||
end
|
||||
|
||||
def call(env)
|
||||
context(env)
|
||||
end
|
||||
|
||||
def context(env, app=@app)
|
||||
prepare_session(env)
|
||||
status, headers, body = app.call(env)
|
||||
commit_session(env, status, headers, body)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def initialize_sid
|
||||
@sidbits = @default_options[:sidbits]
|
||||
@sid_secure = @default_options[:secure_random]
|
||||
@sid_length = @sidbits / 4
|
||||
end
|
||||
|
||||
# Generate a new session id using Ruby #rand. The size of the
|
||||
# session id is controlled by the :sidbits option.
|
||||
# Monkey patch this to use custom methods for session id generation.
|
||||
|
||||
def generate_sid(secure = @sid_secure)
|
||||
if secure
|
||||
secure.hex(@sid_length)
|
||||
else
|
||||
"%0#{@sid_length}x" % Kernel.rand(2**@sidbits - 1)
|
||||
end
|
||||
rescue NotImplementedError
|
||||
generate_sid(false)
|
||||
end
|
||||
|
||||
# Sets the lazy session at 'rack.session' and places options and session
|
||||
# metadata into 'rack.session.options'.
|
||||
|
||||
def prepare_session(env)
|
||||
session_was = env[ENV_SESSION_KEY]
|
||||
env[ENV_SESSION_KEY] = session_class.new(self, env)
|
||||
env[ENV_SESSION_OPTIONS_KEY] = @default_options.dup
|
||||
env[ENV_SESSION_KEY].merge! session_was if session_was
|
||||
end
|
||||
|
||||
# Extracts the session id from provided cookies and passes it and the
|
||||
# environment to #get_session.
|
||||
|
||||
def load_session(env)
|
||||
sid = current_session_id(env)
|
||||
sid, session = get_session(env, sid)
|
||||
[sid, session || {}]
|
||||
end
|
||||
|
||||
# Extract session id from request object.
|
||||
|
||||
def extract_session_id(env)
|
||||
request = Rack::Request.new(env)
|
||||
sid = request.cookies[@key]
|
||||
sid ||= request.params[@key] unless @cookie_only
|
||||
sid
|
||||
end
|
||||
|
||||
# Returns the current session id from the SessionHash.
|
||||
|
||||
def current_session_id(env)
|
||||
env[ENV_SESSION_KEY].id
|
||||
end
|
||||
|
||||
# Check if the session exists or not.
|
||||
|
||||
def session_exists?(env)
|
||||
value = current_session_id(env)
|
||||
value && !value.empty?
|
||||
end
|
||||
|
||||
# Session should be committed if it was loaded, any of specific options like :renew, :drop
|
||||
# or :expire_after was given and the security permissions match. Skips if skip is given.
|
||||
|
||||
def commit_session?(env, session, options)
|
||||
if options[:skip]
|
||||
false
|
||||
else
|
||||
has_session = loaded_session?(session) || forced_session_update?(session, options)
|
||||
has_session && security_matches?(env, options)
|
||||
end
|
||||
end
|
||||
|
||||
def loaded_session?(session)
|
||||
!session.is_a?(session_class) || session.loaded?
|
||||
end
|
||||
|
||||
def forced_session_update?(session, options)
|
||||
force_options?(options) && session && !session.empty?
|
||||
end
|
||||
|
||||
def force_options?(options)
|
||||
options.values_at(:max_age, :renew, :drop, :defer, :expire_after).any?
|
||||
end
|
||||
|
||||
def security_matches?(env, options)
|
||||
return true unless options[:secure]
|
||||
request = Rack::Request.new(env)
|
||||
request.ssl?
|
||||
end
|
||||
|
||||
# Acquires the session from the environment and the session id from
|
||||
# the session options and passes them to #set_session. If successful
|
||||
# and the :defer option is not true, a cookie will be added to the
|
||||
# response with the session's id.
|
||||
|
||||
def commit_session(env, status, headers, body)
|
||||
session = env[ENV_SESSION_KEY]
|
||||
options = session.options
|
||||
|
||||
if options[:drop] || options[:renew]
|
||||
session_id = destroy_session(env, session.id || generate_sid, options)
|
||||
return [status, headers, body] unless session_id
|
||||
end
|
||||
|
||||
return [status, headers, body] unless commit_session?(env, session, options)
|
||||
|
||||
session.send(:load!) unless loaded_session?(session)
|
||||
session_id ||= session.id
|
||||
session_data = session.to_hash.delete_if { |k,v| v.nil? }
|
||||
|
||||
if not data = set_session(env, session_id, session_data, options)
|
||||
env["rack.errors"].puts("Warning! #{self.class.name} failed to save session. Content dropped.")
|
||||
elsif options[:defer] and not options[:renew]
|
||||
env["rack.errors"].puts("Deferring cookie for #{session_id}") if $VERBOSE
|
||||
else
|
||||
cookie = Hash.new
|
||||
cookie[:value] = data
|
||||
cookie[:expires] = Time.now + options[:expire_after] if options[:expire_after]
|
||||
cookie[:expires] = Time.now + options[:max_age] if options[:max_age]
|
||||
set_cookie(env, headers, cookie.merge!(options))
|
||||
end
|
||||
|
||||
[status, headers, body]
|
||||
end
|
||||
|
||||
# Sets the cookie back to the client with session id. We skip the cookie
|
||||
# setting if the value didn't change (sid is the same) or expires was given.
|
||||
|
||||
def set_cookie(env, headers, cookie)
|
||||
request = Rack::Request.new(env)
|
||||
if request.cookies[@key] != cookie[:value] || cookie[:expires]
|
||||
Utils.set_cookie_header!(headers, @key, cookie)
|
||||
end
|
||||
end
|
||||
|
||||
# Allow subclasses to prepare_session for different Session classes
|
||||
|
||||
def session_class
|
||||
SessionHash
|
||||
end
|
||||
|
||||
# All thread safety and session retrieval procedures should occur here.
|
||||
# Should return [session_id, session].
|
||||
# If nil is provided as the session id, generation of a new valid id
|
||||
# should occur within.
|
||||
|
||||
def get_session(env, sid)
|
||||
raise '#get_session not implemented.'
|
||||
end
|
||||
|
||||
# All thread safety and session storage procedures should occur here.
|
||||
# Must return the session id if the session was saved successfully, or
|
||||
# false if the session could not be saved.
|
||||
|
||||
def set_session(env, sid, session, options)
|
||||
raise '#set_session not implemented.'
|
||||
end
|
||||
|
||||
# All thread safety and session destroy procedures should occur here.
|
||||
# Should return a new session id or nil if options[:drop]
|
||||
|
||||
def destroy_session(env, sid, options)
|
||||
raise '#destroy_session not implemented'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,188 +0,0 @@
|
||||
require 'openssl'
|
||||
require 'zlib'
|
||||
require 'rack/request'
|
||||
require 'rack/response'
|
||||
require 'rack/session/abstract/id'
|
||||
|
||||
module Rack
|
||||
|
||||
module Session
|
||||
|
||||
# Rack::Session::Cookie provides simple cookie based session management.
|
||||
# By default, the session is a Ruby Hash stored as base64 encoded marshalled
|
||||
# data set to :key (default: rack.session). The object that encodes the
|
||||
# session data is configurable and must respond to +encode+ and +decode+.
|
||||
# Both methods must take a string and return a string.
|
||||
#
|
||||
# When the secret key is set, cookie data is checked for data integrity.
|
||||
# The old secret key is also accepted and allows graceful secret rotation.
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# use Rack::Session::Cookie, :key => 'rack.session',
|
||||
# :domain => 'foo.com',
|
||||
# :path => '/',
|
||||
# :expire_after => 2592000,
|
||||
# :secret => 'change_me',
|
||||
# :old_secret => 'also_change_me'
|
||||
#
|
||||
# All parameters are optional.
|
||||
#
|
||||
# Example of a cookie with no encoding:
|
||||
#
|
||||
# Rack::Session::Cookie.new(application, {
|
||||
# :coder => Rack::Session::Cookie::Identity.new
|
||||
# })
|
||||
#
|
||||
# Example of a cookie with custom encoding:
|
||||
#
|
||||
# Rack::Session::Cookie.new(application, {
|
||||
# :coder => Class.new {
|
||||
# def encode(str); str.reverse; end
|
||||
# def decode(str); str.reverse; end
|
||||
# }.new
|
||||
# })
|
||||
#
|
||||
|
||||
class Cookie < Abstract::ID
|
||||
# Encode session cookies as Base64
|
||||
class Base64
|
||||
def encode(str)
|
||||
[str].pack('m')
|
||||
end
|
||||
|
||||
def decode(str)
|
||||
str.unpack('m').first
|
||||
end
|
||||
|
||||
# Encode session cookies as Marshaled Base64 data
|
||||
class Marshal < Base64
|
||||
def encode(str)
|
||||
super(::Marshal.dump(str))
|
||||
end
|
||||
|
||||
def decode(str)
|
||||
return unless str
|
||||
::Marshal.load(super(str)) rescue nil
|
||||
end
|
||||
end
|
||||
|
||||
# N.B. Unlike other encoding methods, the contained objects must be a
|
||||
# valid JSON composite type, either a Hash or an Array.
|
||||
class JSON < Base64
|
||||
def encode(obj)
|
||||
super(::Rack::Utils::OkJson.encode(obj))
|
||||
end
|
||||
|
||||
def decode(str)
|
||||
return unless str
|
||||
::Rack::Utils::OkJson.decode(super(str)) rescue nil
|
||||
end
|
||||
end
|
||||
|
||||
class ZipJSON < Base64
|
||||
def encode(obj)
|
||||
super(Zlib::Deflate.deflate(::Rack::Utils::OkJson.encode(obj)))
|
||||
end
|
||||
|
||||
def decode(str)
|
||||
return unless str
|
||||
::Rack::Utils::OkJson.decode(Zlib::Inflate.inflate(super(str)))
|
||||
rescue
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Use no encoding for session cookies
|
||||
class Identity
|
||||
def encode(str); str; end
|
||||
def decode(str); str; end
|
||||
end
|
||||
|
||||
attr_reader :coder
|
||||
|
||||
def initialize(app, options={})
|
||||
@secrets = options.values_at(:secret, :old_secret).compact
|
||||
warn <<-MSG unless @secrets.size >= 1
|
||||
SECURITY WARNING: No secret option provided to Rack::Session::Cookie.
|
||||
This poses a security threat. It is strongly recommended that you
|
||||
provide a secret to prevent exploits that may be possible from crafted
|
||||
cookies. This will not be supported in future versions of Rack, and
|
||||
future versions will even invalidate your existing user cookies.
|
||||
|
||||
Called from: #{caller[0]}.
|
||||
MSG
|
||||
@coder = options[:coder] ||= Base64::Marshal.new
|
||||
super(app, options.merge!(:cookie_only => true))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def get_session(env, sid)
|
||||
data = unpacked_cookie_data(env)
|
||||
data = persistent_session_id!(data)
|
||||
[data["session_id"], data]
|
||||
end
|
||||
|
||||
def extract_session_id(env)
|
||||
unpacked_cookie_data(env)["session_id"]
|
||||
end
|
||||
|
||||
def unpacked_cookie_data(env)
|
||||
env["rack.session.unpacked_cookie_data"] ||= begin
|
||||
request = Rack::Request.new(env)
|
||||
session_data = request.cookies[@key]
|
||||
|
||||
if @secrets.size > 0 && session_data
|
||||
digest, session_data = session_data.reverse.split("--", 2)
|
||||
digest.reverse! if digest
|
||||
session_data.reverse! if session_data
|
||||
session_data = nil unless digest_match?(session_data, digest)
|
||||
end
|
||||
|
||||
coder.decode(session_data) || {}
|
||||
end
|
||||
end
|
||||
|
||||
def persistent_session_id!(data, sid=nil)
|
||||
data ||= {}
|
||||
data["session_id"] ||= sid || generate_sid
|
||||
data
|
||||
end
|
||||
|
||||
def set_session(env, session_id, session, options)
|
||||
session = session.merge("session_id" => session_id)
|
||||
session_data = coder.encode(session)
|
||||
|
||||
if @secrets.first
|
||||
session_data << "--#{generate_hmac(session_data, @secrets.first)}"
|
||||
end
|
||||
|
||||
if session_data.size > (4096 - @key.size)
|
||||
env["rack.errors"].puts("Warning! Rack::Session::Cookie data size exceeds 4K.")
|
||||
nil
|
||||
else
|
||||
session_data
|
||||
end
|
||||
end
|
||||
|
||||
def destroy_session(env, session_id, options)
|
||||
# Nothing to do here, data is in the client
|
||||
generate_sid unless options[:drop]
|
||||
end
|
||||
|
||||
def digest_match?(data, digest)
|
||||
return unless data && digest
|
||||
@secrets.any? do |secret|
|
||||
Rack::Utils.secure_compare(digest, generate_hmac(data, secret))
|
||||
end
|
||||
end
|
||||
|
||||
def generate_hmac(data, secret)
|
||||
OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, secret, data)
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,93 +0,0 @@
|
||||
# AUTHOR: blink <blinketje@gmail.com>; blink#ruby-lang@irc.freenode.net
|
||||
|
||||
require 'rack/session/abstract/id'
|
||||
require 'memcache'
|
||||
|
||||
module Rack
|
||||
module Session
|
||||
# Rack::Session::Memcache provides simple cookie based session management.
|
||||
# Session data is stored in memcached. The corresponding session key is
|
||||
# maintained in the cookie.
|
||||
# You may treat Session::Memcache as you would Session::Pool with the
|
||||
# following caveats.
|
||||
#
|
||||
# * Setting :expire_after to 0 would note to the Memcache server to hang
|
||||
# onto the session data until it would drop it according to it's own
|
||||
# specifications. However, the cookie sent to the client would expire
|
||||
# immediately.
|
||||
#
|
||||
# Note that memcache does drop data before it may be listed to expire. For
|
||||
# a full description of behaviour, please see memcache's documentation.
|
||||
|
||||
class Memcache < Abstract::ID
|
||||
attr_reader :mutex, :pool
|
||||
|
||||
DEFAULT_OPTIONS = Abstract::ID::DEFAULT_OPTIONS.merge \
|
||||
:namespace => 'rack:session',
|
||||
:memcache_server => 'localhost:11211'
|
||||
|
||||
def initialize(app, options={})
|
||||
super
|
||||
|
||||
@mutex = Mutex.new
|
||||
mserv = @default_options[:memcache_server]
|
||||
mopts = @default_options.reject{|k,v| !MemCache::DEFAULT_OPTIONS.include? k }
|
||||
|
||||
@pool = options[:cache] || MemCache.new(mserv, mopts)
|
||||
unless @pool.active? and @pool.servers.any?{|c| c.alive? }
|
||||
raise 'No memcache servers'
|
||||
end
|
||||
end
|
||||
|
||||
def generate_sid
|
||||
loop do
|
||||
sid = super
|
||||
break sid unless @pool.get(sid, true)
|
||||
end
|
||||
end
|
||||
|
||||
def get_session(env, sid)
|
||||
with_lock(env) do
|
||||
unless sid and session = @pool.get(sid)
|
||||
sid, session = generate_sid, {}
|
||||
unless /^STORED/ =~ @pool.add(sid, session)
|
||||
raise "Session collision on '#{sid.inspect}'"
|
||||
end
|
||||
end
|
||||
[sid, session]
|
||||
end
|
||||
end
|
||||
|
||||
def set_session(env, session_id, new_session, options)
|
||||
expiry = options[:expire_after]
|
||||
expiry = expiry.nil? ? 0 : expiry + 1
|
||||
|
||||
with_lock(env) do
|
||||
@pool.set session_id, new_session, expiry
|
||||
session_id
|
||||
end
|
||||
end
|
||||
|
||||
def destroy_session(env, session_id, options)
|
||||
with_lock(env) do
|
||||
@pool.delete(session_id)
|
||||
generate_sid unless options[:drop]
|
||||
end
|
||||
end
|
||||
|
||||
def with_lock(env)
|
||||
@mutex.lock if env['rack.multithread']
|
||||
yield
|
||||
rescue MemCache::MemCacheError, Errno::ECONNREFUSED
|
||||
if $VERBOSE
|
||||
warn "#{self} is unable to find memcached server."
|
||||
warn $!.inspect
|
||||
end
|
||||
raise
|
||||
ensure
|
||||
@mutex.unlock if @mutex.locked?
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,76 +0,0 @@
|
||||
# AUTHOR: blink <blinketje@gmail.com>; blink#ruby-lang@irc.freenode.net
|
||||
# THANKS:
|
||||
# apeiros, for session id generation, expiry setup, and threadiness
|
||||
# sergio, threadiness and bugreps
|
||||
|
||||
require 'rack/session/abstract/id'
|
||||
require 'thread'
|
||||
|
||||
module Rack
|
||||
module Session
|
||||
# Rack::Session::Pool provides simple cookie based session management.
|
||||
# Session data is stored in a hash held by @pool.
|
||||
# In the context of a multithreaded environment, sessions being
|
||||
# committed to the pool is done in a merging manner.
|
||||
#
|
||||
# The :drop option is available in rack.session.options if you wish to
|
||||
# explicitly remove the session from the session cache.
|
||||
#
|
||||
# Example:
|
||||
# myapp = MyRackApp.new
|
||||
# sessioned = Rack::Session::Pool.new(myapp,
|
||||
# :domain => 'foo.com',
|
||||
# :expire_after => 2592000
|
||||
# )
|
||||
# Rack::Handler::WEBrick.run sessioned
|
||||
|
||||
class Pool < Abstract::ID
|
||||
attr_reader :mutex, :pool
|
||||
DEFAULT_OPTIONS = Abstract::ID::DEFAULT_OPTIONS.merge :drop => false
|
||||
|
||||
def initialize(app, options={})
|
||||
super
|
||||
@pool = Hash.new
|
||||
@mutex = Mutex.new
|
||||
end
|
||||
|
||||
def generate_sid
|
||||
loop do
|
||||
sid = super
|
||||
break sid unless @pool.key? sid
|
||||
end
|
||||
end
|
||||
|
||||
def get_session(env, sid)
|
||||
with_lock(env) do
|
||||
unless sid and session = @pool[sid]
|
||||
sid, session = generate_sid, {}
|
||||
@pool.store sid, session
|
||||
end
|
||||
[sid, session]
|
||||
end
|
||||
end
|
||||
|
||||
def set_session(env, session_id, new_session, options)
|
||||
with_lock(env) do
|
||||
@pool.store session_id, new_session
|
||||
session_id
|
||||
end
|
||||
end
|
||||
|
||||
def destroy_session(env, session_id, options)
|
||||
with_lock(env) do
|
||||
@pool.delete(session_id)
|
||||
generate_sid unless options[:drop]
|
||||
end
|
||||
end
|
||||
|
||||
def with_lock(env)
|
||||
@mutex.lock if env['rack.multithread']
|
||||
yield
|
||||
ensure
|
||||
@mutex.unlock if @mutex.locked?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,387 +0,0 @@
|
||||
require 'ostruct'
|
||||
require 'erb'
|
||||
require 'rack/request'
|
||||
require 'rack/utils'
|
||||
|
||||
module Rack
|
||||
# Rack::ShowExceptions catches all exceptions raised from the app it
|
||||
# wraps. It shows a useful backtrace with the sourcefile and
|
||||
# clickable context, the whole Rack environment and the request
|
||||
# data.
|
||||
#
|
||||
# Be careful when you use this on public-facing sites as it could
|
||||
# reveal information helpful to attackers.
|
||||
|
||||
class ShowExceptions
|
||||
CONTEXT = 7
|
||||
|
||||
def initialize(app)
|
||||
@app = app
|
||||
@template = ERB.new(TEMPLATE)
|
||||
end
|
||||
|
||||
def call(env)
|
||||
@app.call(env)
|
||||
rescue StandardError, LoadError, SyntaxError => e
|
||||
exception_string = dump_exception(e)
|
||||
|
||||
env["rack.errors"].puts(exception_string)
|
||||
env["rack.errors"].flush
|
||||
|
||||
if accepts_html?(env)
|
||||
content_type = "text/html"
|
||||
body = pretty(env, e)
|
||||
else
|
||||
content_type = "text/plain"
|
||||
body = exception_string
|
||||
end
|
||||
|
||||
[
|
||||
500,
|
||||
{
|
||||
CONTENT_TYPE => content_type,
|
||||
CONTENT_LENGTH => Rack::Utils.bytesize(body).to_s,
|
||||
},
|
||||
[body],
|
||||
]
|
||||
end
|
||||
|
||||
def prefers_plaintext?(env)
|
||||
!accepts_html(env)
|
||||
end
|
||||
|
||||
def accepts_html?(env)
|
||||
Rack::Utils.best_q_match(env["HTTP_ACCEPT"], %w[text/html])
|
||||
end
|
||||
private :accepts_html?
|
||||
|
||||
def dump_exception(exception)
|
||||
string = "#{exception.class}: #{exception.message}\n"
|
||||
string << exception.backtrace.map { |l| "\t#{l}" }.join("\n")
|
||||
string
|
||||
end
|
||||
|
||||
def pretty(env, exception)
|
||||
req = Rack::Request.new(env)
|
||||
|
||||
# This double assignment is to prevent an "unused variable" warning on
|
||||
# Ruby 1.9.3. Yes, it is dumb, but I don't like Ruby yelling at me.
|
||||
path = path = (req.script_name + req.path_info).squeeze("/")
|
||||
|
||||
# This double assignment is to prevent an "unused variable" warning on
|
||||
# Ruby 1.9.3. Yes, it is dumb, but I don't like Ruby yelling at me.
|
||||
frames = frames = exception.backtrace.map { |line|
|
||||
frame = OpenStruct.new
|
||||
if line =~ /(.*?):(\d+)(:in `(.*)')?/
|
||||
frame.filename = $1
|
||||
frame.lineno = $2.to_i
|
||||
frame.function = $4
|
||||
|
||||
begin
|
||||
lineno = frame.lineno-1
|
||||
lines = ::File.readlines(frame.filename)
|
||||
frame.pre_context_lineno = [lineno-CONTEXT, 0].max
|
||||
frame.pre_context = lines[frame.pre_context_lineno...lineno]
|
||||
frame.context_line = lines[lineno].chomp
|
||||
frame.post_context_lineno = [lineno+CONTEXT, lines.size].min
|
||||
frame.post_context = lines[lineno+1..frame.post_context_lineno]
|
||||
rescue
|
||||
end
|
||||
|
||||
frame
|
||||
else
|
||||
nil
|
||||
end
|
||||
}.compact
|
||||
|
||||
@template.result(binding)
|
||||
end
|
||||
|
||||
def h(obj) # :nodoc:
|
||||
case obj
|
||||
when String
|
||||
Utils.escape_html(obj)
|
||||
else
|
||||
Utils.escape_html(obj.inspect)
|
||||
end
|
||||
end
|
||||
|
||||
# :stopdoc:
|
||||
|
||||
# adapted from Django <djangoproject.com>
|
||||
# Copyright (c) 2005, the Lawrence Journal-World
|
||||
# Used under the modified BSD license:
|
||||
# http://www.xfree86.org/3.3.6/COPYRIGHT2.html#5
|
||||
TEMPLATE = <<'HTML'
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
|
||||
<meta name="robots" content="NONE,NOARCHIVE" />
|
||||
<title><%=h exception.class %> at <%=h path %></title>
|
||||
<style type="text/css">
|
||||
html * { padding:0; margin:0; }
|
||||
body * { padding:10px 20px; }
|
||||
body * * { padding:0; }
|
||||
body { font:small sans-serif; }
|
||||
body>div { border-bottom:1px solid #ddd; }
|
||||
h1 { font-weight:normal; }
|
||||
h2 { margin-bottom:.8em; }
|
||||
h2 span { font-size:80%; color:#666; font-weight:normal; }
|
||||
h3 { margin:1em 0 .5em 0; }
|
||||
h4 { margin:0 0 .5em 0; font-weight: normal; }
|
||||
table {
|
||||
border:1px solid #ccc; border-collapse: collapse; background:white; }
|
||||
tbody td, tbody th { vertical-align:top; padding:2px 3px; }
|
||||
thead th {
|
||||
padding:1px 6px 1px 3px; background:#fefefe; text-align:left;
|
||||
font-weight:normal; font-size:11px; border:1px solid #ddd; }
|
||||
tbody th { text-align:right; color:#666; padding-right:.5em; }
|
||||
table.vars { margin:5px 0 2px 40px; }
|
||||
table.vars td, table.req td { font-family:monospace; }
|
||||
table td.code { width:100%;}
|
||||
table td.code div { overflow:hidden; }
|
||||
table.source th { color:#666; }
|
||||
table.source td {
|
||||
font-family:monospace; white-space:pre; border-bottom:1px solid #eee; }
|
||||
ul.traceback { list-style-type:none; }
|
||||
ul.traceback li.frame { margin-bottom:1em; }
|
||||
div.context { margin: 10px 0; }
|
||||
div.context ol {
|
||||
padding-left:30px; margin:0 10px; list-style-position: inside; }
|
||||
div.context ol li {
|
||||
font-family:monospace; white-space:pre; color:#666; cursor:pointer; }
|
||||
div.context ol.context-line li { color:black; background-color:#ccc; }
|
||||
div.context ol.context-line li span { float: right; }
|
||||
div.commands { margin-left: 40px; }
|
||||
div.commands a { color:black; text-decoration:none; }
|
||||
#summary { background: #ffc; }
|
||||
#summary h2 { font-weight: normal; color: #666; }
|
||||
#summary ul#quicklinks { list-style-type: none; margin-bottom: 2em; }
|
||||
#summary ul#quicklinks li { float: left; padding: 0 1em; }
|
||||
#summary ul#quicklinks>li+li { border-left: 1px #666 solid; }
|
||||
#explanation { background:#eee; }
|
||||
#template, #template-not-exist { background:#f6f6f6; }
|
||||
#template-not-exist ul { margin: 0 0 0 20px; }
|
||||
#traceback { background:#eee; }
|
||||
#requestinfo { background:#f6f6f6; padding-left:120px; }
|
||||
#summary table { border:none; background:transparent; }
|
||||
#requestinfo h2, #requestinfo h3 { position:relative; margin-left:-100px; }
|
||||
#requestinfo h3 { margin-bottom:-1em; }
|
||||
.error { background: #ffc; }
|
||||
.specific { color:#cc3300; font-weight:bold; }
|
||||
</style>
|
||||
<script type="text/javascript">
|
||||
//<!--
|
||||
function getElementsByClassName(oElm, strTagName, strClassName){
|
||||
// Written by Jonathan Snook, http://www.snook.ca/jon;
|
||||
// Add-ons by Robert Nyman, http://www.robertnyman.com
|
||||
var arrElements = (strTagName == "*" && document.all)? document.all :
|
||||
oElm.getElementsByTagName(strTagName);
|
||||
var arrReturnElements = new Array();
|
||||
strClassName = strClassName.replace(/\-/g, "\\-");
|
||||
var oRegExp = new RegExp("(^|\\s)" + strClassName + "(\\s|$$)");
|
||||
var oElement;
|
||||
for(var i=0; i<arrElements.length; i++){
|
||||
oElement = arrElements[i];
|
||||
if(oRegExp.test(oElement.className)){
|
||||
arrReturnElements.push(oElement);
|
||||
}
|
||||
}
|
||||
return (arrReturnElements)
|
||||
}
|
||||
function hideAll(elems) {
|
||||
for (var e = 0; e < elems.length; e++) {
|
||||
elems[e].style.display = 'none';
|
||||
}
|
||||
}
|
||||
window.onload = function() {
|
||||
hideAll(getElementsByClassName(document, 'table', 'vars'));
|
||||
hideAll(getElementsByClassName(document, 'ol', 'pre-context'));
|
||||
hideAll(getElementsByClassName(document, 'ol', 'post-context'));
|
||||
}
|
||||
function toggle() {
|
||||
for (var i = 0; i < arguments.length; i++) {
|
||||
var e = document.getElementById(arguments[i]);
|
||||
if (e) {
|
||||
e.style.display = e.style.display == 'none' ? 'block' : 'none';
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function varToggle(link, id) {
|
||||
toggle('v' + id);
|
||||
var s = link.getElementsByTagName('span')[0];
|
||||
var uarr = String.fromCharCode(0x25b6);
|
||||
var darr = String.fromCharCode(0x25bc);
|
||||
s.innerHTML = s.innerHTML == uarr ? darr : uarr;
|
||||
return false;
|
||||
}
|
||||
//-->
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="summary">
|
||||
<h1><%=h exception.class %> at <%=h path %></h1>
|
||||
<h2><%=h exception.message %></h2>
|
||||
<table><tr>
|
||||
<th>Ruby</th>
|
||||
<td>
|
||||
<% if first = frames.first %>
|
||||
<code><%=h first.filename %></code>: in <code><%=h first.function %></code>, line <%=h frames.first.lineno %>
|
||||
<% else %>
|
||||
unknown location
|
||||
<% end %>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<th>Web</th>
|
||||
<td><code><%=h req.request_method %> <%=h(req.host + path)%></code></td>
|
||||
</tr></table>
|
||||
|
||||
<h3>Jump to:</h3>
|
||||
<ul id="quicklinks">
|
||||
<li><a href="#get-info">GET</a></li>
|
||||
<li><a href="#post-info">POST</a></li>
|
||||
<li><a href="#cookie-info">Cookies</a></li>
|
||||
<li><a href="#env-info">ENV</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div id="traceback">
|
||||
<h2>Traceback <span>(innermost first)</span></h2>
|
||||
<ul class="traceback">
|
||||
<% frames.each { |frame| %>
|
||||
<li class="frame">
|
||||
<code><%=h frame.filename %></code>: in <code><%=h frame.function %></code>
|
||||
|
||||
<% if frame.context_line %>
|
||||
<div class="context" id="c<%=h frame.object_id %>">
|
||||
<% if frame.pre_context %>
|
||||
<ol start="<%=h frame.pre_context_lineno+1 %>" class="pre-context" id="pre<%=h frame.object_id %>">
|
||||
<% frame.pre_context.each { |line| %>
|
||||
<li onclick="toggle('pre<%=h frame.object_id %>', 'post<%=h frame.object_id %>')"><%=h line %></li>
|
||||
<% } %>
|
||||
</ol>
|
||||
<% end %>
|
||||
|
||||
<ol start="<%=h frame.lineno %>" class="context-line">
|
||||
<li onclick="toggle('pre<%=h frame.object_id %>', 'post<%=h frame.object_id %>')"><%=h frame.context_line %><span>...</span></li></ol>
|
||||
|
||||
<% if frame.post_context %>
|
||||
<ol start='<%=h frame.lineno+1 %>' class="post-context" id="post<%=h frame.object_id %>">
|
||||
<% frame.post_context.each { |line| %>
|
||||
<li onclick="toggle('pre<%=h frame.object_id %>', 'post<%=h frame.object_id %>')"><%=h line %></li>
|
||||
<% } %>
|
||||
</ol>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</li>
|
||||
<% } %>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div id="requestinfo">
|
||||
<h2>Request information</h2>
|
||||
|
||||
<h3 id="get-info">GET</h3>
|
||||
<% if req.GET and not req.GET.empty? %>
|
||||
<table class="req">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Variable</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% req.GET.sort_by { |k, v| k.to_s }.each { |key, val| %>
|
||||
<tr>
|
||||
<td><%=h key %></td>
|
||||
<td class="code"><div><%=h val.inspect %></div></td>
|
||||
</tr>
|
||||
<% } %>
|
||||
</tbody>
|
||||
</table>
|
||||
<% else %>
|
||||
<p>No GET data.</p>
|
||||
<% end %>
|
||||
|
||||
<h3 id="post-info">POST</h3>
|
||||
<% if req.POST and not req.POST.empty? %>
|
||||
<table class="req">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Variable</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% req.POST.sort_by { |k, v| k.to_s }.each { |key, val| %>
|
||||
<tr>
|
||||
<td><%=h key %></td>
|
||||
<td class="code"><div><%=h val.inspect %></div></td>
|
||||
</tr>
|
||||
<% } %>
|
||||
</tbody>
|
||||
</table>
|
||||
<% else %>
|
||||
<p>No POST data.</p>
|
||||
<% end %>
|
||||
|
||||
|
||||
<h3 id="cookie-info">COOKIES</h3>
|
||||
<% unless req.cookies.empty? %>
|
||||
<table class="req">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Variable</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% req.cookies.each { |key, val| %>
|
||||
<tr>
|
||||
<td><%=h key %></td>
|
||||
<td class="code"><div><%=h val.inspect %></div></td>
|
||||
</tr>
|
||||
<% } %>
|
||||
</tbody>
|
||||
</table>
|
||||
<% else %>
|
||||
<p>No cookie data.</p>
|
||||
<% end %>
|
||||
|
||||
<h3 id="env-info">Rack ENV</h3>
|
||||
<table class="req">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Variable</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% env.sort_by { |k, v| k.to_s }.each { |key, val| %>
|
||||
<tr>
|
||||
<td><%=h key %></td>
|
||||
<td class="code"><div><%=h val %></div></td>
|
||||
</tr>
|
||||
<% } %>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="explanation">
|
||||
<p>
|
||||
You're seeing this error because you use <code>Rack::ShowExceptions</code>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
HTML
|
||||
|
||||
# :startdoc:
|
||||
end
|
||||
end
|
||||
@@ -1,113 +0,0 @@
|
||||
require 'erb'
|
||||
require 'rack/request'
|
||||
require 'rack/utils'
|
||||
|
||||
module Rack
|
||||
# Rack::ShowStatus catches all empty responses and replaces them
|
||||
# with a site explaining the error.
|
||||
#
|
||||
# Additional details can be put into <tt>rack.showstatus.detail</tt>
|
||||
# and will be shown as HTML. If such details exist, the error page
|
||||
# is always rendered, even if the reply was not empty.
|
||||
|
||||
class ShowStatus
|
||||
def initialize(app)
|
||||
@app = app
|
||||
@template = ERB.new(TEMPLATE)
|
||||
end
|
||||
|
||||
def call(env)
|
||||
status, headers, body = @app.call(env)
|
||||
headers = Utils::HeaderHash.new(headers)
|
||||
empty = headers[CONTENT_LENGTH].to_i <= 0
|
||||
|
||||
# client or server error, or explicit message
|
||||
if (status.to_i >= 400 && empty) || env["rack.showstatus.detail"]
|
||||
# This double assignment is to prevent an "unused variable" warning on
|
||||
# Ruby 1.9.3. Yes, it is dumb, but I don't like Ruby yelling at me.
|
||||
req = req = Rack::Request.new(env)
|
||||
|
||||
message = Rack::Utils::HTTP_STATUS_CODES[status.to_i] || status.to_s
|
||||
|
||||
# This double assignment is to prevent an "unused variable" warning on
|
||||
# Ruby 1.9.3. Yes, it is dumb, but I don't like Ruby yelling at me.
|
||||
detail = detail = env["rack.showstatus.detail"] || message
|
||||
|
||||
body = @template.result(binding)
|
||||
size = Rack::Utils.bytesize(body)
|
||||
[status, headers.merge(CONTENT_TYPE => "text/html", CONTENT_LENGTH => size.to_s), [body]]
|
||||
else
|
||||
[status, headers, body]
|
||||
end
|
||||
end
|
||||
|
||||
def h(obj) # :nodoc:
|
||||
case obj
|
||||
when String
|
||||
Utils.escape_html(obj)
|
||||
else
|
||||
Utils.escape_html(obj.inspect)
|
||||
end
|
||||
end
|
||||
|
||||
# :stopdoc:
|
||||
|
||||
# adapted from Django <djangoproject.com>
|
||||
# Copyright (c) 2005, the Lawrence Journal-World
|
||||
# Used under the modified BSD license:
|
||||
# http://www.xfree86.org/3.3.6/COPYRIGHT2.html#5
|
||||
TEMPLATE = <<'HTML'
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
|
||||
<title><%=h message %> at <%=h req.script_name + req.path_info %></title>
|
||||
<meta name="robots" content="NONE,NOARCHIVE" />
|
||||
<style type="text/css">
|
||||
html * { padding:0; margin:0; }
|
||||
body * { padding:10px 20px; }
|
||||
body * * { padding:0; }
|
||||
body { font:small sans-serif; background:#eee; }
|
||||
body>div { border-bottom:1px solid #ddd; }
|
||||
h1 { font-weight:normal; margin-bottom:.4em; }
|
||||
h1 span { font-size:60%; color:#666; font-weight:normal; }
|
||||
table { border:none; border-collapse: collapse; width:100%; }
|
||||
td, th { vertical-align:top; padding:2px 3px; }
|
||||
th { width:12em; text-align:right; color:#666; padding-right:.5em; }
|
||||
#info { background:#f6f6f6; }
|
||||
#info ol { margin: 0.5em 4em; }
|
||||
#info ol li { font-family: monospace; }
|
||||
#summary { background: #ffc; }
|
||||
#explanation { background:#eee; border-bottom: 0px none; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="summary">
|
||||
<h1><%=h message %> <span>(<%= status.to_i %>)</span></h1>
|
||||
<table class="meta">
|
||||
<tr>
|
||||
<th>Request Method:</th>
|
||||
<td><%=h req.request_method %></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Request URL:</th>
|
||||
<td><%=h req.url %></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div id="info">
|
||||
<p><%=h detail %></p>
|
||||
</div>
|
||||
|
||||
<div id="explanation">
|
||||
<p>
|
||||
You're seeing this error because you use <code>Rack::ShowStatus</code>.
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
HTML
|
||||
|
||||
# :startdoc:
|
||||
end
|
||||
end
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user