require "lib/rwd/ruby" require "lib/rwd/ftools" require "lib/rwd/mime" 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=~ /^\#/ end end def uri2txt(s) # ??? Werkt niet goed 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 @path = "/" if (not @path.nil? and @path.empty? and @protocol == "http") @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 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 localname protocol = @protocol userpass = @userpass host = @host port = @port 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? path = "#{path}." if path =~ /[\/\\]$/ f = MD5.new(protocol.to_s + userpass.to_s + host.to_s + port.to_s + File.dirname(path.to_s) + vars.to_s).to_s e = File.basename(path.to_s).gsub(/[^\w\.\-]/, "_").gsub(/_+/, "_") res = f + "." + e res.gsub!(/[^\w]+$/, "") 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 class NoAddressException < StandardError end def self.getaddress(host) if not @@hosts.include?(host) @@hosts[host] = "" evtimeout(5) do # ??? Doet 'ut niet?... @@hosts[host] = IPSocket.getaddress(host) end end raise NoAddressException, host if @@hosts[host].empty? @@hosts[host] end def self.head(uri, form={}, recursive=true) header = Header.new(nil) begin while not uri.nil? uri = EVURI.new(uri) if uri.kind_of? String host = uri.host port = uri.port if $proxy.nil? or $proxy.empty? or host == "localhost" io = nil @@mutex.synchronize do io = TCPSocket.new(getaddress(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) io = TCPSocket.new(proxy.host, proxy.port.zero? ? 8080 : proxy.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 io.close_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 Errno::ECONNRESET, Errno::EHOSTUNREACH => e $stderr.puts e.message sleep 1 retry rescue Errno::ECONNREFUSED => e data = nil rescue NoAddressException => e $stderr.puts e.message header = Header.new(nil) end GC.start return header end def self.get(uri, httpheader={}, 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? uri = EVURI.new(uri) if uri.kind_of? String host = uri.host port = uri.port if $proxy.nil? or $proxy.empty? or host == "localhost" io = nil @@mutex.synchronize do io = TCPSocket.new(getaddress(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) io = TCPSocket.new(proxy.host, proxy.port.zero? ? 8080 : proxy.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: xyz\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 "Accept-Charset: ISO-8859-1\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? httpheader.each do |k, v| $stderr.puts "%s: %s\r\n" % [k, v] io.write "%s: %s\r\n" % [k, v] end io.write "\r\n" io.write post unless post.empty? io.close_write res = io.read io.close_read header, data = nil, nil header, data = res.split(/\r*\n\r*\n/, 2) if not res.nil? header = Header.new(header) length = header.header["content-length"] data = "" if length == "0" 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 #if header.header["content-encoding"] == "gzip" #data = "gzip -d".exec(data) if not data.nil? #end data = nil unless header.code == 200 end rescue Errno::ECONNRESET, Errno::EHOSTUNREACH => e $stderr.puts e.message sleep 1 retry rescue Errno::ECONNREFUSED => e data = nil rescue NoAddressException, Errno::ECONNREFUSED => e $stderr.puts e.message data = nil end GC.start 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 File.mkpath(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 end end end class RequestPost < Hash def initialize(data) CGI.parse(data).each do |k, v| self[k] = v 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 def url_unescape(string) string.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n) do [$1.delete('%')].pack('H*') end end class Request < Hash attr_reader :peeraddr attr_reader :request attr_reader :cookies attr_reader :vars attr_reader :user attr_writer :user 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 "") data = url_unescape(data) @vars = RequestPost.new((self["content-type"] == "application/x-www-form-urlencoded") ? data : "") else $stderr.puts "Unknown request ('#{firstline}')." end $stderr.puts data 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_writer :file 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{} @file = nil end def flush sync if @file File.open(@file, "rb") do |f| while data = f.read(10_000) @io.write data end end end @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 size = (@data or "").length if @file ext = @file.scan(/\.[^\.]*$/) ext = ext.shift ext = ext[1..-1] unless ext.nil? mimetype = EVMime::MimeType[ext] self["Content-Type"] = mimetype unless mimetype.nil? size += File.size(@file) if File.file?(@file) end self["Content-Length"] = size @io.write("#{to_s}\r\n") unless @syncd @io.write(@data) @data = "" @syncd = true end def << (s) @data << s end def clean @data = "" 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.user} #{req.request.to_s.strip}" 1 == 1 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 resp["Content-Type"] = "text/plain" resp.response = "HTTP/1.0 200 ???" resp.clean resp << e.class.to_s + ": " + e.message resp << "\n" resp << "\n" resp << e.backtrace.collect{|s| "\t"+s}.join("\n") resp << "\n" resp << "\n" resp << "(You can use the back button and stop the application properly, if appropriate.)" 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) unless ok resp["WWW-Authenticate"] = "Basic realm=\"#{realm}\"" resp.response = "HTTP/1.0 401 Unauthorized" end req.user = u return ok end end