rffw 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +5 -0
- data/.rvmrc +1 -0
- data/Gemfile +4 -0
- data/README.md +60 -0
- data/Rakefile +41 -0
- data/bin/rffw +16 -0
- data/lib/rffw.rb +7 -0
- data/lib/rffw/app.rb +36 -0
- data/lib/rffw/app/app_handler.rb +59 -0
- data/lib/rffw/app/data/images/bg.png +0 -0
- data/lib/rffw/app/data/index.html +56 -0
- data/lib/rffw/app/data/javascripts/application.js +110 -0
- data/lib/rffw/app/data/stylesheets/style.css +338 -0
- data/lib/rffw/app/data/template.html +42 -0
- data/lib/rffw/app/db.rb +19 -0
- data/lib/rffw/app/description_handler.rb +12 -0
- data/lib/rffw/app/dir_handler.rb +36 -0
- data/lib/rffw/app/record.rb +43 -0
- data/lib/rffw/app/show_handler.rb +25 -0
- data/lib/rffw/app/upload_handler.rb +32 -0
- data/lib/rffw/app/upload_status_handler.rb +40 -0
- data/lib/rffw/app/urlencode_chars.data +224 -0
- data/lib/rffw/app/view_helpers.rb +30 -0
- data/lib/rffw/parser.rb +9 -0
- data/lib/rffw/parser/http_request.rb +137 -0
- data/lib/rffw/parser/http_response.rb +29 -0
- data/lib/rffw/parser/mime_parser.rb +46 -0
- data/lib/rffw/server.rb +15 -0
- data/lib/rffw/server/buffered_client.rb +57 -0
- data/lib/rffw/server/client.rb +53 -0
- data/lib/rffw/server/http_client.rb +49 -0
- data/lib/rffw/server/server.rb +46 -0
- data/lib/rffw/version.rb +3 -0
- data/rffw.db +0 -0
- data/rffw.gemspec +21 -0
- data/test/fixtures/image.jpg +0 -0
- data/test/fixtures/mime_image.data +0 -0
- data/test/fixtures/mime_image.png +0 -0
- data/test/fixtures/post_form_data.http +14 -0
- data/test/fixtures/raw_request.txt +8 -0
- data/test/fixtures/request.http +9 -0
- data/test/fixtures/upload.http +0 -0
- data/test/fixtures/upload_status_request.http +10 -0
- data/test/helper.rb +97 -0
- data/test/test_buffered_client.rb +50 -0
- data/test/test_client.rb +47 -0
- data/test/test_db.rb +21 -0
- data/test/test_http_client.rb +26 -0
- data/test/test_http_request.rb +52 -0
- data/test/test_mime_parser.rb +22 -0
- data/test/test_record.rb +49 -0
- data/test/test_upload_status_handler.rb +18 -0
- data/test/test_uploader_handler.rb +10 -0
- data/test/test_view_helpers.rb +24 -0
- metadata +122 -0
@@ -0,0 +1,30 @@
|
|
1
|
+
module RFFW::App::ViewHelpers
|
2
|
+
|
3
|
+
CHARS = File.read(File.expand_path('../urlencode_chars.data',__FILE__)).split("\n")
|
4
|
+
|
5
|
+
def uri_escape(string)
|
6
|
+
string.gsub(/[^a-zA-Z0-9 ]/) do |char|
|
7
|
+
CHARS.include?(char) ? "%#{"%.2X" % (CHARS.index(char)+32)}" : char
|
8
|
+
end.tr(" ", "+")
|
9
|
+
end
|
10
|
+
|
11
|
+
def uri_unescape(string)
|
12
|
+
string.gsub(/\%([a-f0-9]{2})/i) do |char|
|
13
|
+
value = $1.to_i(16)
|
14
|
+
case value
|
15
|
+
when 32..(CHARS.size+32)
|
16
|
+
CHARS[value - 32] || char
|
17
|
+
when 13
|
18
|
+
"\n"
|
19
|
+
when 10
|
20
|
+
'' #LF
|
21
|
+
else
|
22
|
+
"#{char}"
|
23
|
+
end
|
24
|
+
end.gsub("+", " ")
|
25
|
+
end
|
26
|
+
|
27
|
+
def strip_tags(string)
|
28
|
+
string.gsub(/<(.|\n)*?>/m, '')
|
29
|
+
end
|
30
|
+
end
|
data/lib/rffw/parser.rb
ADDED
@@ -0,0 +1,137 @@
|
|
1
|
+
module RFFW::Parser
|
2
|
+
class HttpRequest
|
3
|
+
|
4
|
+
class MalformedReqest < StandardError ; end
|
5
|
+
class IncompleteRequest < StandardError ; end
|
6
|
+
class OnlyForPosts < StandardError; end
|
7
|
+
|
8
|
+
attr_reader :headers
|
9
|
+
|
10
|
+
def initialize(raw_request)
|
11
|
+
@headers={}
|
12
|
+
@headers_are_parsed = false
|
13
|
+
scan(raw_request)
|
14
|
+
rescue IncompleteRequest
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
def finish?
|
19
|
+
if get? && @headers_are_parsed
|
20
|
+
true
|
21
|
+
elsif post? && @headers_are_parsed && upload_content_lenght <= body_size
|
22
|
+
true
|
23
|
+
else
|
24
|
+
false
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def body
|
29
|
+
post? ? @body : @body || ""
|
30
|
+
end
|
31
|
+
|
32
|
+
def attachments
|
33
|
+
raise OnlyForPosts unless post?
|
34
|
+
MimeParser.parse(@body)
|
35
|
+
end
|
36
|
+
|
37
|
+
def method
|
38
|
+
@headers["HTTP_METHOD"]
|
39
|
+
end
|
40
|
+
|
41
|
+
def path
|
42
|
+
uri && uri.path
|
43
|
+
end
|
44
|
+
|
45
|
+
def query
|
46
|
+
uri && uri.query || ""
|
47
|
+
end
|
48
|
+
|
49
|
+
def url
|
50
|
+
@headers["HTTP_PATH"]
|
51
|
+
end
|
52
|
+
|
53
|
+
def uri
|
54
|
+
URI.parse(url)
|
55
|
+
rescue
|
56
|
+
nil
|
57
|
+
end
|
58
|
+
|
59
|
+
def version
|
60
|
+
@headers["HTTP_VERSION"]
|
61
|
+
end
|
62
|
+
|
63
|
+
def keep_alive?
|
64
|
+
!!(version == '1.1' && @headers["Connection"] =~ /keep-alive/)
|
65
|
+
end
|
66
|
+
|
67
|
+
def post?
|
68
|
+
method && method.downcase == "post"
|
69
|
+
end
|
70
|
+
|
71
|
+
def get?
|
72
|
+
method && method.downcase == 'get'
|
73
|
+
end
|
74
|
+
|
75
|
+
def uplad_info
|
76
|
+
@headers["Content-Length"].to_i - @body.size
|
77
|
+
end
|
78
|
+
|
79
|
+
def upload_content_lenght
|
80
|
+
raise OnlyForPosts unless post?
|
81
|
+
@headers["Content-Length"].to_i
|
82
|
+
end
|
83
|
+
|
84
|
+
def body_size
|
85
|
+
raise OnlyForPosts unless post?
|
86
|
+
@body.size
|
87
|
+
end
|
88
|
+
|
89
|
+
def upload_progress
|
90
|
+
raise OnlyForPosts unless post?
|
91
|
+
"#{body_size}/#{upload_content_lenght}"
|
92
|
+
end
|
93
|
+
|
94
|
+
|
95
|
+
def post_form_data
|
96
|
+
body.split("&").inject({}){|memo, value|
|
97
|
+
key,value = value.split("=")
|
98
|
+
memo[key] = value
|
99
|
+
memo
|
100
|
+
}
|
101
|
+
end
|
102
|
+
|
103
|
+
def inspect
|
104
|
+
"#{method} #{path} #{query} #{version}"
|
105
|
+
end
|
106
|
+
|
107
|
+
protected
|
108
|
+
def scan(raw)
|
109
|
+
return nil unless raw.is_a?(String)
|
110
|
+
full_scanner = StringScanner.new(raw.force_encoding("BINARY"))
|
111
|
+
|
112
|
+
headers = full_scanner.scan_until(/\r\n\r\n/) or raise IncompleteRequest
|
113
|
+
scan_headers(headers)
|
114
|
+
|
115
|
+
@body = full_scanner.rest
|
116
|
+
end
|
117
|
+
|
118
|
+
def scan_headers(headers)
|
119
|
+
scanner = StringScanner.new(headers)
|
120
|
+
|
121
|
+
@headers["HTTP_METHOD"] = scanner.scan(/\w+/) or raise MalformedReqest
|
122
|
+
scanner.scan(/\s+/)
|
123
|
+
@headers["HTTP_PATH"] = scanner.scan(/\S+/) or raise MalformedReqest
|
124
|
+
scanner.scan(/\s+HTTP\//) or raise MalformedReqest
|
125
|
+
@headers["HTTP_VERSION"] = scanner.scan(/\S+/) or raise MalformedReqest
|
126
|
+
begin
|
127
|
+
scanner.scan(/\r\n/)
|
128
|
+
break if scanner.scan(/\r\n/) || scanner.eos?
|
129
|
+
key = scanner.scan(/\A[^:]+/) or break
|
130
|
+
scanner.scan(/\:\s*/) or break
|
131
|
+
value = scanner.scan(/.*\r\n/) or break
|
132
|
+
@headers[key] = value.strip if key && value
|
133
|
+
end while (key && value)
|
134
|
+
@headers_are_parsed = true
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module RFFW::Parser
|
2
|
+
class HttpResponse
|
3
|
+
|
4
|
+
def initialize(message, status = 200, headers = {"Content-Type" => 'text/html'}, version = "1.1" )
|
5
|
+
@message, @status, @headers, @version = message, status, headers, version
|
6
|
+
end
|
7
|
+
|
8
|
+
def to_s
|
9
|
+
headers = @headers.map{|k,v|
|
10
|
+
"#{k}: #{v}\r\n"
|
11
|
+
}.join('')
|
12
|
+
|
13
|
+
response = ""
|
14
|
+
response << "HTTP/#{@version} #{@status.to_s}\r\n"
|
15
|
+
response << "Server: rffw/#{RFFW::VERSION}\r\n"
|
16
|
+
response << "Date: #{Time.now.httpdate}\r\n"
|
17
|
+
response << headers
|
18
|
+
|
19
|
+
if @headers["Transfer-Encoding"] && @headers["Transfer-Encoding"] == "chunked"
|
20
|
+
response << "\r\n"
|
21
|
+
else
|
22
|
+
response << "Content-Length: #{@message.size}\r\n" <<
|
23
|
+
"\r\n#{@message}"
|
24
|
+
end
|
25
|
+
response
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module RFFW::Parser
|
2
|
+
module MimeParser
|
3
|
+
extend self
|
4
|
+
class Attachment < Struct.new(:boundary, :header, :data, :headers)
|
5
|
+
|
6
|
+
def header=(header)
|
7
|
+
return if header.nil?
|
8
|
+
@headers = {}
|
9
|
+
header.split("\r\n").each{|l|
|
10
|
+
head = l.split(":")
|
11
|
+
@headers[head[0]] = head[1].strip
|
12
|
+
}
|
13
|
+
end
|
14
|
+
|
15
|
+
def content_disposition
|
16
|
+
@headers.find{|k,v| k.downcase == 'content-disposition' }.last
|
17
|
+
end
|
18
|
+
|
19
|
+
def content_type
|
20
|
+
@headers.find{|k,v| k.downcase == 'content-type' }.last
|
21
|
+
end
|
22
|
+
|
23
|
+
def filename
|
24
|
+
content_disposition[/filename\=\"([^"]*)\"/i,1]
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_s
|
28
|
+
data
|
29
|
+
end
|
30
|
+
end
|
31
|
+
def parse(string)
|
32
|
+
scanner = StringScanner.new(string.force_encoding("BINARY"))
|
33
|
+
attachments = []
|
34
|
+
while !scanner.eos?
|
35
|
+
attachment = Attachment.new
|
36
|
+
attachment.boundary = scanner.scan_until(/\r\n/).strip
|
37
|
+
attachment.header = scanner.scan_until(/\r\n\r\n/)
|
38
|
+
pos = scanner.pos
|
39
|
+
scanner.scan_until(/\r\n#{Regexp.escape attachment.boundary}/)
|
40
|
+
attachment.data = scanner.pre_match.to_s[pos..-1]
|
41
|
+
attachments << attachment unless attachment.data.nil?
|
42
|
+
end
|
43
|
+
attachments
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/lib/rffw/server.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'delegate'
|
3
|
+
require 'tempfile'
|
4
|
+
require 'time'
|
5
|
+
require 'strscan'
|
6
|
+
require 'uri'
|
7
|
+
|
8
|
+
module RFFW
|
9
|
+
module Server
|
10
|
+
require 'rffw/server/client'
|
11
|
+
require 'rffw/server/buffered_client'
|
12
|
+
require 'rffw/server/http_client'
|
13
|
+
require 'rffw/server/server'
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module RFFW::Server
|
2
|
+
class BufferedClient < Client
|
3
|
+
|
4
|
+
@@buffer_size = 1024
|
5
|
+
|
6
|
+
def initialize(*args)
|
7
|
+
super
|
8
|
+
@buffer = ''
|
9
|
+
@file = nil
|
10
|
+
end
|
11
|
+
|
12
|
+
def on_data
|
13
|
+
data = self.read_nonblock 1024*1024*1024
|
14
|
+
if !@file && @buffer.size + data.size > @@buffer_size
|
15
|
+
@file = Tempfile.new('rffw_request')
|
16
|
+
@file.write @buffer
|
17
|
+
@file.write data
|
18
|
+
@buffer = nil
|
19
|
+
elsif @file
|
20
|
+
@file.write data
|
21
|
+
else
|
22
|
+
@buffer << data
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def usign_temp_file?
|
27
|
+
!!@file
|
28
|
+
end
|
29
|
+
|
30
|
+
def buffer_io
|
31
|
+
usign_temp_file? ? @file : @buffer
|
32
|
+
end
|
33
|
+
|
34
|
+
def on_disconnect
|
35
|
+
clear_buffer
|
36
|
+
super
|
37
|
+
end
|
38
|
+
|
39
|
+
def buffer
|
40
|
+
if @file
|
41
|
+
@file.rewind
|
42
|
+
@file.read
|
43
|
+
else
|
44
|
+
@buffer
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def clear_buffer
|
49
|
+
if @file
|
50
|
+
@file.close
|
51
|
+
@file.unlink
|
52
|
+
@file = nil
|
53
|
+
end
|
54
|
+
@buffer = ''
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module RFFW::Server
|
2
|
+
|
3
|
+
class Client < DelegateClass(TCPSocket)
|
4
|
+
extend Enumerable
|
5
|
+
|
6
|
+
@@clients = []
|
7
|
+
|
8
|
+
def initialize(socket)
|
9
|
+
Thread.exclusive {
|
10
|
+
@@clients << self
|
11
|
+
}
|
12
|
+
super(socket)
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.all
|
16
|
+
Thread.exclusive {
|
17
|
+
@@clients.uniq!
|
18
|
+
}
|
19
|
+
@@clients
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.each(&block)
|
23
|
+
@@clients.each(&block)
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.find(client)
|
27
|
+
@@clients.find{|c| c == client}
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.find_by_io(client)
|
31
|
+
@@clients.find{ |c| c.to_i == client.to_i }
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.disconnect_all!
|
35
|
+
while client = Thread.exclusive{@@clients.pop}
|
36
|
+
client.on_disconnect
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def finished?
|
41
|
+
return ((closed? || eof?) ? true : false)
|
42
|
+
rescue Errno::ECONNRESET
|
43
|
+
return true
|
44
|
+
end
|
45
|
+
|
46
|
+
def on_disconnect
|
47
|
+
close unless closed?
|
48
|
+
Thread.exclusive{ @@clients.delete self }
|
49
|
+
end
|
50
|
+
|
51
|
+
alias disconnect! on_disconnect
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module RFFW::Server
|
2
|
+
class HttpClient < BufferedClient
|
3
|
+
include RFFW::Parser
|
4
|
+
attr_reader :request
|
5
|
+
|
6
|
+
def initialize(*args)
|
7
|
+
super
|
8
|
+
end
|
9
|
+
|
10
|
+
def on_data
|
11
|
+
super
|
12
|
+
@request = process_request(self.buffer)
|
13
|
+
on_request(request) if @request.finish?
|
14
|
+
end
|
15
|
+
|
16
|
+
def on_request(request)
|
17
|
+
message = "Not found"
|
18
|
+
write ["HTTP/1.1 404 Not found",
|
19
|
+
"Server: rffw/#{RFFW::VERSION}",
|
20
|
+
"Date: #{Time.now.httpdate}",
|
21
|
+
"Content-Type: text/html",
|
22
|
+
"Content-Length: #{message.size}",
|
23
|
+
"Last-Modified: #{Time.now.httpdate}",
|
24
|
+
"Connection: keep-alive",
|
25
|
+
"Accept-Ranges: bytes",
|
26
|
+
"",
|
27
|
+
"#{message}"].join("\r\n")
|
28
|
+
end
|
29
|
+
|
30
|
+
def write_chunk(message)
|
31
|
+
unless defined?(@headers_sent) && @headers_sent == true
|
32
|
+
write HttpResponse.new("", 200, {"Transfer-Encoding" => "chunked"},'1.1')
|
33
|
+
@headers_sent = true
|
34
|
+
end
|
35
|
+
write generate_chunk(message)
|
36
|
+
end
|
37
|
+
|
38
|
+
protected
|
39
|
+
|
40
|
+
def generate_chunk(text)
|
41
|
+
"#{text.size.to_s(16).upcase}\r\n#{text}\r\n\r\n"
|
42
|
+
end
|
43
|
+
|
44
|
+
def process_request(raw_request)
|
45
|
+
HttpRequest.new(raw_request)
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module RFFW::Server
|
2
|
+
class Server < TCPServer
|
3
|
+
include RFFW::Server
|
4
|
+
|
5
|
+
def initialize(handler = HttpClient, port = 8080, listen = "0.0.0.0" )
|
6
|
+
@handler = handler
|
7
|
+
super(listen,port)
|
8
|
+
catch :ctrl_c do
|
9
|
+
loop do
|
10
|
+
read, write, error = Kernel.select(@handler.all << self)
|
11
|
+
read.each do |io|
|
12
|
+
io == self ? process_server : process_client(io)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def process_server
|
21
|
+
begin
|
22
|
+
socket = accept_nonblock
|
23
|
+
@handler.new(socket)
|
24
|
+
rescue Errno::EAGAIN
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def process_client(io)
|
29
|
+
begin
|
30
|
+
if client = @handler.find_by_io(io)
|
31
|
+
client.finished? ? client.on_disconnect : client.on_data
|
32
|
+
else
|
33
|
+
raise "Sorry. I don't know how to manage this situation"
|
34
|
+
end
|
35
|
+
rescue IOError => e
|
36
|
+
$stderr.puts "Closed Stream"
|
37
|
+
client.disconnect!
|
38
|
+
rescue => e
|
39
|
+
client.disconnect!
|
40
|
+
$stderr.puts e.inspect
|
41
|
+
$stderr.puts e.backtrace
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|