require "ev/ruby" require "ev/ftools" require "net/http" require "socket" require "uri" require "cgi" # require "md5" require "thread" $proxy = ENV["PROXY"] if $proxy.nil? file = "#{home}/.evnet" if File.file?(file) Hash.file(file).each do |k, v| eval "$#{k} = '#{v}'" unless k=~ /^\#/ #$proxy_auth = [$proxy_auth].pack("m").chomp if k == "proxy_auth" end end def uri2txt(s) i = s.index(/%[[:digit:]]{2}/) while not i.nil? s = s[0..(i-1)] + s[(i+1)..(i+2)].unpack('H2').shift.to_i.chr + s[(i+3)..-1] i = s.index(/%[[:digit:]]{2}/) end s end class TCPServer def self.freeport(from, to, remote=false) if windows? or cygwin? TCPServer.freeport_windows(from, to, remote) else TCPServer.freeport_linux(from, to, remote) end end def self.freeport_linux(from, to, remote) ports = (from..to).to_a port = nil res = nil while res.nil? and not ports.empty? begin port = ports[0] ports.delete(port) io = TCPServer.new(remote ? "0.0.0.0" : "localhost", port) res = [port, io] rescue end end res = [nil, nil] if res.nil? port, io = res return port, io end def self.freeport_windows(from, to, remote) ports = (from..to).to_a port = nil res = nil while res.nil? and not ports.empty? begin port = ports.any ports.delete(port) io = TCPSocket.new("localhost", port) io.close rescue res = port end end port, io = res return port, io end def self.freeport_windows2(from, to, remote) res = nil port = from while res.nil? and port <= to begin io = TCPSocket.new("localhost", port) io.close port += 1 rescue res = port end end return res end def self.usedports(from, to) threads = [] res = [] from.upto(to) do |port| threads << Thread.new do begin io = TCPSocket.new("localhost", port) io.close port rescue nil end end end threads.each do |thread| port = thread.value res << port unless port.nil? end return res end end class EVURI attr_reader :protocol attr_writer :protocol attr_reader :userpass attr_writer :userpass attr_reader :host attr_writer :host attr_reader :port attr_writer :port attr_reader :path attr_writer :path attr_reader :vars attr_writer :vars attr_reader :anchor attr_writer :anchor def initialize(url) begin @protocol, @userpass, @host, @port, d1, @path, d2, @vars, @anchor = URI.split(url.to_s) rescue end @protocol = "" if @protocol.nil? @userpass = "" if @userpass.nil? @host = "" if @host.nil? @port = 0 if @port.nil? @path = "" if @path.nil? @vars = "" if @vars.nil? @anchor = "" if @anchor.nil? res = {} @varsvolgorde = [] @vars.split(/&/).each{|var| k, v = var.split(/=/) ; res[k] = v ; @varsvolgorde << k} @vars = res @port = @port.to_i end def + (url2) url1 = self.to_s url2 = url2.to_s if url2.kind_of?(self.class) return EVURI.new((URI::Generic.new(*URI.split(url1)) + URI::Generic.new(*URI.split(url2))).to_s) rescue nil end def to_s protocol = @protocol userpass = @userpass host = @host port = @port.to_s path = @path vars = varstring anchor = @anchor protocol = nil if @protocol.empty? userpass = nil if @userpass.empty? host = nil if @host.empty? port = nil if @port.zero? path = nil if @path.empty? vars = nil if @vars.empty? anchor = nil if @anchor.empty? res = URI::HTTP.new(@protocol, @userpass, @host, port, nil, @path, nil, vars, @anchor).to_s.from_html res.gsub!(/@/, "") if (@userpass.nil? or @userpass.empty?) res.gsub!(/\#$/, "") return res end def varstring res = [] vars = @vars.dup @varsvolgorde.each do |k| if vars.include?(k) v = vars[k] vars.delete(k) res << (v.nil? ? k : "#{k}=#{v}") end end res.concat(vars.collect{|k, v| v.nil? ? k : "#{k}=#{v}"}) return res.join("&") end end class HTTPClient @@versie = 1 @@mutex = Mutex.new @@hosts = {} class Header attr_reader :header attr_reader :protocol attr_reader :code attr_reader :text def initialize(header) @header = {} if not header.nil? firstline, rest = header.split(/\r*\n/, 2) @protocol, @code, @text = firstline.split(/ */, 3) @code = @code.to_i if not rest.nil? rest.split(/\r*\n/).each do |line| key, value = line.split(/ /, 2) @header[key.sub(/:$/, "").downcase] = value end end end end def to_s res = "" res << "%s %s %s\n" % [@protocol, @code, @text] @header.each do |k, v| res << "%s=%s\n" % [k, v] end return res end end class Chunk def initialize(data) @data = "" line, data = data.split(/\r*\n/, 2) size, ext = line.split(/;/, 2) size = size.hex while not size.zero? and not data.nil? @data += data[0..(size-1)] data = data[size..-1] if not data.nil? data.gsub!(/^\r*\n/, "") line, data = data.split(/\r*\n/, 2) size, ext = line.split(/;/, 2) size = size.hex end end end def to_s @data end end def self.head(uri, form={}, recursive=true) header = Header.new(nil) begin while not uri.nil? if $proxy.nil? or $proxy.empty? uri = EVURI.new(uri) if uri.kind_of? String host = uri.host port = uri.port io = nil @@mutex.synchronize do @@hosts[host] = IPSocket.getaddress(host) if not @@hosts.include?(host) io = TCPSocket.new(@@hosts[host], port.zero? ? 80 : port) end io.write("HEAD #{uri.path or '/'}#{uri.varstring.empty? ? '' : '?' + uri.varstring} HTTP/1.0\r\nHost: #{host}\r\n\r\n") else proxy = EVURI.new($proxy) host = proxy.host port = proxy.port io = TCPSocket.new(host, port.zero? ? 8080 : port) io.write("HEAD #{uri} HTTP/1.0\r\n#{"Proxy-Authorization: Basic "+$proxy_auth+"\r\n" if not $proxy_auth.nil?}\r\n\r\n") end io.close_write res = io.read header, data = nil, nil header, data = res.split(/\r*\n\r*\n/, 2) if not res.nil? header = Header.new(header) if recursive and header.header["location"] != uri.to_s uri = EVURI.new(uri) + header.header["location"] else uri = nil end end rescue header = Header.new(nil) end return header end def self.get(uri, form={}) post = Array.new form.each_pair do |var, value| post << "#{var.to_html}=#{value.to_html}" end post = post.join("?") data = nil begin while not uri.nil? if $proxy.nil? or $proxy.empty? uri = EVURI.new(uri) if uri.kind_of? String host = uri.host port = uri.port io = nil @@mutex.synchronize do @@hosts[host] = IPSocket.getaddress(host) if not @@hosts.include?(host) io = TCPSocket.new(@@hosts[host], port.zero? ? 80 : port) end if post.empty? io.write "GET %s%s HTTP/1.0\r\n" % [(uri.path or '/'), (uri.varstring.empty? ? '' : '?' + uri.varstring)] else io.write "POST %s%s HTTP/1.0\r\n" % [(uri.path or '/'), (uri.varstring.empty? ? '' : '?' + uri.varstring)] end else proxy = EVURI.new($proxy) host = proxy.host port = proxy.port io = TCPSocket.new(host, port.zero? ? 8080 : port) if post.empty? io.write "GET %s HTTP/1.0\r\n" % uri else io.write "POST %s HTTP/1.0\r\n" % uri end end io.write "Host: %s\r\n" % host io.write "User-Agent: evwget\r\n" io.write "Proxy-Authorization: Basic %s\r\n" % $proxy_auth unless $proxy_auth.nil? #io.write "Accept-Encoding: deflate\r\n" #io.write "Connection: close\r\n" io.write "Content-Type: application/x-www-form-urlencoded\r\n" unless post.empty? io.write "Content-Length: %s\r\n" % post.length unless post.empty? io.write "\r\n" io.write post unless post.empty? io.close_write res = io.read header, data = nil, nil header, data = res.split(/\r*\n\r*\n/, 2) if not res.nil? header = Header.new(header) if header.header["location"] != uri.to_s uri = EVURI.new(uri) + header.header["location"] else uri = nil end if header.header["transfer-encoding"] == "chunked" data = Chunk.new(data).to_s if not data.nil? end data = nil unless header.code == 200 end rescue data = nil end return data end def self.head_from_cache(uri, form={}) from_cache("head", uri, form) end def self.get_from_cache(uri, form={}) from_cache("get", uri, form) end def self.from_cache(action, uri, form) loc = uri.to_s + form.sort.inspect hash = MD5.new("#{@@versie} #{loc}") dir = "#{temp}/evcache.#{user}/httpclient.#{action}" file = "#{dir}/#{hash}" data = nil Dir.mkdirrec(dir) expire = 356*24*60*60 if File.file?(file) and (Time.new.to_f - File.stat(file).mtime.to_f < expire) @@mutex.synchronize do File.open(file, "rb") {|f| data = f.read} end else data = method(action).call(uri, form) if not data.nil? @@mutex.synchronize do File.open(file, "wb") {|f| f.write data} end end end return data end end class RequestGet < Hash def initialize(data) CGI.parse(data).each do |k, v| self[k] = v.join(" ") end end end class RequestPost < Hash def initialize(data) CGI.parse(data).each do |k, v| self[k] = v.join(" ") end end end class RequestRequest attr_reader :method attr_reader :uri attr_reader :path attr_reader :data attr_reader :protocol def initialize(firstline) @method, @uri, @protocol = firstline.split(/ /) @path, @data = @uri.split(/\?/) @data = "" if @data.nil? # TODO # i = @path.index(/%[[:digit:]]{2}/) # while not i.nil? # @path = @path[0..(i-1)] + @path[(i+1)..(i+2)].unpack('H2').shift.to_i.chr + @path[(i+3)..-1] # i = @path.index(/%[[:digit:]]{2}/) # end end def to_s "#{@method} #{@uri} #{@protocol}\r\n" end def inspect "(RequestRequest: %s)" % [@method, @path, @data, @protocol].join(", ") end end class Request < Hash attr_reader :peeraddr attr_reader :request attr_reader :cookies attr_reader :vars def initialize(io) @io = io firstline = @io.gets return if firstline.nil? @request = RequestRequest.new(firstline.strip) line = @io.gets line = line.strip unless line.nil? while not line.nil? and not line.empty? key, value = line.split(" ", 2) self[key.sub(/:$/, "").downcase] = value line = @io.gets line = line.strip unless line.nil? end cookie = self["cookie"] cookie = "" if cookie.nil? @cookies = {} cookie.split(/;/).each do |s| k, v = s.strip.split(/=/, 2) @cookies[k] = v end if not @request.method.nil? case @request.method.upcase when "HEAD" when "GET" @vars = RequestGet.new(@request.data.nil? ? "" : @request.data) when "POST" data = (@io.read(self["content-length"].to_i) or "") @vars = RequestPost.new((self["content-type"] == "application/x-www-form-urlencoded") ? data : "") else $stderr.puts "Unknown request ('#{firstline}')." end end @peeraddr = @io.peeraddr @pda = false @pda = true if (self.include?("user-agent") and self["user-agent"].downcase.include?("windows ce")) @pda = true if (self.include?("user-agent") and self["user-agent"].downcase.include?("handhttp")) @io.close_read end def pda? @pda end def to_s res = @request.to_s self.each do |k, v| res << "#{k}: #{v}\r\n" end res end def inspect "(Request: %s)" % [@peeraddr, @request.inspect, @vars.inspect, @cookies.inspect, super].join(", ") end end class Response < Hash attr_writer :response attr_reader :cookies attr_reader :stop attr_reader :at_stop def initialize(io) @io = io @response = "HTTP/1.0 200 OK" @cookies = {} @data = "" @syncd = false @stop = false @at_stop = lambda{} end def flush sync @io.close end def to_s res = "#{@response}\r\n" self.each do |k, v| res << "#{k}: #{v}\r\n" end @cookies.each do |k, v| res << "Set-Cookie: %s=%s;\r\n" % [k, v] end res end def sync @io.write("#{to_s}\r\n") unless @syncd @io.write(@data) @data = "" @syncd = true end def << (s) @data << s end def inspect "(Response: %s)" % [@response, @data].join(", ") end def stop(&block) @stop = true @at_stop = block unless block.nil? end def stop? @stop end end class HTTPServerException < Exception end class HTTPServer def self.serve(portio=80, remote=false, auth=nil, realm="ev/net") port, server = portio begin server = TCPServer.new(remote ? "0.0.0.0" : "localhost", port) if server.nil? $stderr.puts "Just point your browser to http://localhost:#{port}/ ..." rescue server = nil $stderr.puts "Port #{port} is in use." end if not server.nil? count = 0 at_exit do $stderr.puts "Received #{count} requests" end serverthread = Thread.new do mutex = Mutex.new Thread.current["threads"] = [] every(1, Thread.current) do |thread| mutex.synchronize do thread["threads"].delete_if{|t| (not t.alive?)} end end loop do io = server.accept count += 1 thread = Thread.new(Thread.current, count) do |parentthread, count2| stop = false begin begin req = Request.new(io) resp = Response.new(io) rescue raise HTTPServerException end begin ip = req.peeraddr[3] rescue NameError raise HTTPServerException end if (not remote) or (remote and (auth.nil? or auth.empty? or authenticate(auth, realm, req, resp))) $stderr.puts "#{count2}, #{Time.new.strftime("%Y-%m-%d.%H:%M:%S")}, #{ip}, #{req.request.to_s.strip}" begin yield(req, resp) rescue Exception => e mutex.synchronize do $stderr.puts e.class.to_s + ": " + e.message $stderr.puts e.backtrace.collect{|s| "\t"+s}.join("\n") end end stop = true if resp.stop? end begin resp.flush rescue raise HTTPServerException end rescue HTTPServerException end parentthread["stop"] = resp if stop end mutex.synchronize do Thread.current["threads"] << thread end end end sleep 0.1 while not serverthread["stop"] serverthread["threads"].each {|t| t.join} serverthread["stop"].at_stop.call serverthread.kill end end def self.authenticate(auth, realm, req, resp) if auth.kind_of? String file = "#{home}/#{auth}" auths = {} auths = Hash.file(file) if File.file?(file) else auths = auth end authuserpassword = req["authorization"] if not authuserpassword.nil? authtype, userpassword = authuserpassword.split(/ /) if authtype == "Basic" and not userpassword.nil? u, p = userpassword.unpack("m").shift.split(/:/) end end ok = (auths.include?(u) and auths[u] == p) if ok else resp["WWW-Authenticate"] = "Basic realm=\"#{realm}\"" resp.response = "HTTP/1.0 401 Unauthorized" end return ok end end