femtows 1.2.0
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/CHANGELOG.txt +5 -0
- data/README.md +69 -0
- data/VERSION +1 -0
- data/bin/femtows.bat +2 -0
- data/bin/femtows.sh +1 -0
- data/demo.rb +33 -0
- data/femtows.gemspec +30 -0
- data/femtows.log +7876 -0
- data/gitc.bat +20 -0
- data/guidemo.rb +63 -0
- data/lib/femtows.rb +256 -0
- metadata +58 -0
data/gitc.bat
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
@echo off
|
2
|
+
IF DEFINED %1=="" (
|
3
|
+
call giti
|
4
|
+
ruby -e "a=File.read('VERSION').split('.') ; a[-1]=(a.last.to_i+1).to_s; puts r=a.join('.'); File.open('VERSION','w') {|f| f.write(r)}"
|
5
|
+
echo %1 %2 %3 %4 %5 %6 %7 %8 %9 >> CHANGELOG.txt
|
6
|
+
git commit -a -m "%1 %2 %3 %4 %5 %6 %7 %8 %9"
|
7
|
+
git push
|
8
|
+
echo
|
9
|
+
echo call gitc.bat without args for make/post rubygem
|
10
|
+
goto :eof
|
11
|
+
)
|
12
|
+
:gem
|
13
|
+
rem ==== no args, generate gem and push it to rubygems.org
|
14
|
+
|
15
|
+
ruby -e "a=File.read('VERSION').split('.');a.pop ; a[-1]=(a.last.to_i+1).to_s; puts r=(a+[0]).join('.'); File.open('VERSION','w') {|f| f.puts(r)}"
|
16
|
+
cat VERSION >> CHANGELOG.txt
|
17
|
+
|
18
|
+
ruby -e "Dir.glob('femtows*.gem').each {|f| File.delete(f) }"
|
19
|
+
call gem build femtows.gemspec
|
20
|
+
call gem push femtows*.gem
|
data/guidemo.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
require_relative '../../Ruiby/lib/ruiby.rb'
|
2
|
+
require_relative "lib/femtows.rb"
|
3
|
+
#require 'ruiby'
|
4
|
+
ruiby_require 'erubis','xxx'
|
5
|
+
|
6
|
+
class Server < Ruiby_gtk
|
7
|
+
def initialize(t,w,h)
|
8
|
+
$server=self
|
9
|
+
@port=8100
|
10
|
+
@root="/"
|
11
|
+
super
|
12
|
+
threader(50)
|
13
|
+
end
|
14
|
+
def component
|
15
|
+
stack {
|
16
|
+
flowi {
|
17
|
+
table(2,2) {
|
18
|
+
row { cell_right label("root") ; cell(@eroot=entry(@root)) }
|
19
|
+
row { cell_right label("port") ; cell(@eport=ientry(@port,:min=>0,:max=>65535,:by=>10)) }
|
20
|
+
}
|
21
|
+
button("Restart") {
|
22
|
+
@root,@port=@eroot.text,@eport.text.to_i
|
23
|
+
$ws.stop_browser if $ws
|
24
|
+
run_server
|
25
|
+
deflog("","","restart ok")
|
26
|
+
}
|
27
|
+
button("reset log") { @logt.buffer.text="resetted" }
|
28
|
+
}
|
29
|
+
@logt= slot(text_area(800,160,{font: "courier new 8"}))
|
30
|
+
}
|
31
|
+
end
|
32
|
+
def run_server
|
33
|
+
after(1) do
|
34
|
+
begin
|
35
|
+
$ws=WebserverRoot.new(@port,@root,"femto ws",10,300, {
|
36
|
+
"logg" => proc {|*par| deflog(*par) }
|
37
|
+
})
|
38
|
+
$ws.serve("/info") {|p|
|
39
|
+
[200,".html", "Femto demo<hr><a href='/'>site</a><hr>#{$ws.to_table(p)}" ]
|
40
|
+
}
|
41
|
+
deflog('','',"restarted!")
|
42
|
+
rescue
|
43
|
+
deflog(["","",$!.to_s])
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
def logs(mess)
|
48
|
+
@logt.append(mess)
|
49
|
+
if @logt.text.size>100*1000
|
50
|
+
@logt.text=@logt.text[500..-1]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
def deflog(name,adr,*res)
|
55
|
+
mess= "%s |%15s | %s\n" % [Time.now.strftime("%Y-%m-%d %H:%M:%S"),adr,res.join(" ")]
|
56
|
+
gui_invoke { logs(mess) rescue print $!.to_s+ $!.backtrace.join("\n")+"\n /// "+mess}
|
57
|
+
end
|
58
|
+
|
59
|
+
|
60
|
+
########################### main
|
61
|
+
Thread.abort_on_exception = false
|
62
|
+
BasicSocket.do_not_reverse_lookup = true
|
63
|
+
Ruiby.start { Server.new("Web server",800,200).run_server }
|
data/lib/femtows.rb
ADDED
@@ -0,0 +1,256 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# FemtoWebServer : 232 LOC web server
|
3
|
+
#
|
4
|
+
# $ws=WebserverRoot.new(port,"/home/www","femto ws",10,300)
|
5
|
+
# ws.serve "/FOO" do |params|
|
6
|
+
# data=params["HEAD-DATA"]|| ""
|
7
|
+
# puts "Recu data len=#{data.length} : <#{data[0..1000]}>" if data
|
8
|
+
# [200,".json",""]
|
9
|
+
# end
|
10
|
+
|
11
|
+
|
12
|
+
require 'thread'
|
13
|
+
require 'socket'
|
14
|
+
require 'timeout'
|
15
|
+
|
16
|
+
#################### Tiny embeded webserver
|
17
|
+
|
18
|
+
|
19
|
+
|
20
|
+
|
21
|
+
class WebserverAbstract
|
22
|
+
def logg(*args)
|
23
|
+
if @cb_log then @cb_log.call(@name,*args) else puts(args.join(" ")) end
|
24
|
+
rescue
|
25
|
+
puts(args.join(" "))
|
26
|
+
end
|
27
|
+
def info(txt) ; logg("nw>i>",txt) ; end
|
28
|
+
def error(txt) ; logg("nw>e>",txt) ; end
|
29
|
+
def unescape(string) ; string.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2}))/n) { [$1.delete('%')].pack('H*') } ; end
|
30
|
+
def escape(string) ; string.gsub(/([^ \/a-zA-Z0-9_.-]+)/) { '%' + $1.unpack('H2' * $1.size).join('%').upcase }.tr(' ', '+'); end
|
31
|
+
def hescape(string) ; escape(string.gsub("/./","/").gsub("//","/")) ; end
|
32
|
+
def observe(sleeping,delta)
|
33
|
+
@tho=Thread.new do loop do
|
34
|
+
sleep(sleeping)
|
35
|
+
nowDelta=Time.now-delta
|
36
|
+
l=@th.select { |th,tm| (tm[0]<nowDelta) }
|
37
|
+
l.each { |th,tm| info("killing thread") ; th.kill; @th.delete(th) ; tm[1].close rescue nil }
|
38
|
+
end ; end
|
39
|
+
end
|
40
|
+
def initialize(port,root,name,cadence,timeout,options)
|
41
|
+
raise("tcp port illegal #{port}") unless port.to_i>=80
|
42
|
+
raise("root not exist #{root}") unless File.exists?(root)
|
43
|
+
@cb_log= options["logg"]
|
44
|
+
@last_mtime=File.mtime(__FILE__)
|
45
|
+
@port=port.to_i
|
46
|
+
@root=root
|
47
|
+
@name=name
|
48
|
+
@rootd=root[-1,1]=="/" ? root : root+"/"
|
49
|
+
@timeout=timeout
|
50
|
+
@th={}
|
51
|
+
@cb={}
|
52
|
+
@redirect={}
|
53
|
+
info(" serveur http #{port} on #{@rootd} ready!")
|
54
|
+
observe(cadence,timeout*2)
|
55
|
+
@thm=Thread.new {
|
56
|
+
loop {
|
57
|
+
nbError=0
|
58
|
+
begin
|
59
|
+
session=nil
|
60
|
+
@server = TCPServer.new('0.0.0.0', @port)
|
61
|
+
@server.setsockopt(Socket::SOL_SOCKET,Socket::SO_REUSEADDR, true)
|
62
|
+
while (session = @server.accept)
|
63
|
+
nbError=0
|
64
|
+
run(session)
|
65
|
+
end
|
66
|
+
rescue Exception => e
|
67
|
+
nbError+=1
|
68
|
+
error($!.to_s + " " + $!.backtrace[0..2].join(" "))
|
69
|
+
session.close rescue nil
|
70
|
+
@server.close rescue nil
|
71
|
+
end
|
72
|
+
sleep(3); info("restart accept")
|
73
|
+
}
|
74
|
+
}
|
75
|
+
end
|
76
|
+
def run(session)
|
77
|
+
if ! File.exists?(@root)
|
78
|
+
sendError(session,500,txt="root directory unknown: #{@root}") rescue nil
|
79
|
+
session.close rescue nil
|
80
|
+
else
|
81
|
+
Thread.new(session) do |sess|
|
82
|
+
@th[Thread.current]=[Time.now,sess]
|
83
|
+
request(sess)
|
84
|
+
@th.delete(Thread.current)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
def serve(uri,&blk)
|
89
|
+
@cb[uri] = blk
|
90
|
+
end
|
91
|
+
def request(session)
|
92
|
+
request = session.gets
|
93
|
+
uri = (request.split(/\s+/)+['','',''])[1]
|
94
|
+
#info uri
|
95
|
+
service,param,*bidon=(uri+"?").split(/\?/)
|
96
|
+
params=Hash[*(param.split(/#/)[0].split(/[=&]/))] rescue {}
|
97
|
+
params.each { |k,v| params[k]=unescape(v) }
|
98
|
+
uri=unescape(service)[1..-1].gsub(/\.\./,"")
|
99
|
+
userpass=nil
|
100
|
+
if (buri=uri.split(/@/)).size>1
|
101
|
+
uri=buri[1..-1].join("@")
|
102
|
+
userpass=buri[0].split(/:/)
|
103
|
+
end
|
104
|
+
read_header(session,params)
|
105
|
+
do_service(session,request,uri,userpass,params)
|
106
|
+
rescue Exception => e
|
107
|
+
error("Error Web get on #{request}: \n #{$!.to_s} \n #{$!.backtrace.join("\n ")}" ) rescue nil
|
108
|
+
session.write "HTTP/1.0 501 NOK\r\nContent-type: text/html\r\n\r\n<html><head><title>WS</title></head><body>Error : #{$!}" rescue nil
|
109
|
+
ensure
|
110
|
+
session.close rescue nil
|
111
|
+
end
|
112
|
+
def read_header(session,params)
|
113
|
+
head=session.gets("\r\n\r\n")
|
114
|
+
head.split(/\r\n/m).each { |line| name,data=line.split(": ",2) ; params["HEAD-"+name.upcase]=data }
|
115
|
+
if params["HEAD-CONTENT-LENGTH"]
|
116
|
+
len= params["HEAD-CONTENT-LENGTH"].split(/\s+/).last.to_i
|
117
|
+
params["HEAD-CONTENT-LENGTH"]=len
|
118
|
+
data=""
|
119
|
+
while len>0
|
120
|
+
d=session.read(len>64*1024 ? 64*1024 : len)
|
121
|
+
raise("closed") if !d
|
122
|
+
len -= d.length
|
123
|
+
data+=d
|
124
|
+
end
|
125
|
+
params["HEAD-DATA"]=data
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def redirect(o,d)
|
130
|
+
@redirect[o]=d
|
131
|
+
end
|
132
|
+
def do_service(session,request,service,user_passwd,params)
|
133
|
+
logg(session.peeraddr.last,request.chomp)
|
134
|
+
redir=@redirect["/"+service]
|
135
|
+
service=redir.gsub(/^\//,"") if @redirect[redir]
|
136
|
+
aservice=to_absolute(service)
|
137
|
+
if redir && ! @redirect[redir]
|
138
|
+
do_service(session,request,redir.gsub(/^\//,""),user_passwd,params)
|
139
|
+
elsif @cb["/"+service]
|
140
|
+
begin
|
141
|
+
code,type,data= @cb["/"+service].call(params)
|
142
|
+
if code==0 && data != '/'+service
|
143
|
+
do_service(session,request,data[1..-1],user_passwd,params)
|
144
|
+
else
|
145
|
+
code==200 ? sendData(session,type,data) : sendError(session,code,data)
|
146
|
+
end
|
147
|
+
rescue
|
148
|
+
logg session.peeraddr.last,"Error in get /#{service} : #{$!}"
|
149
|
+
sendError(session,501,$!.to_s)
|
150
|
+
end
|
151
|
+
elsif service =~ /^stop/
|
152
|
+
sendData(session,".html","Stopping...");
|
153
|
+
Thread.new() { sleep(0.1); stop_browser() }
|
154
|
+
elsif File.directory?(aservice)
|
155
|
+
sendData(session,".html",makeIndex(aservice))
|
156
|
+
elsif File.exists?(aservice)
|
157
|
+
sendFile(session,aservice)
|
158
|
+
else
|
159
|
+
info("unknown request serv=#{service} params=#{params.inspect} #{File.exists?(service)}")
|
160
|
+
sendError(session,500,"unknown request serv=#{aservice} params=#{params.inspect} #{File.exists?(service)}");
|
161
|
+
end
|
162
|
+
end
|
163
|
+
def stop_browser
|
164
|
+
info "exit on web demand !"
|
165
|
+
[@tho,@thm].each { |th| th.kill }
|
166
|
+
@server.close rescue nil
|
167
|
+
end
|
168
|
+
def makeIndex(adir)
|
169
|
+
dir=to_relative(adir)
|
170
|
+
dirs,files=Dir.glob(adir==@rootd ? "#{@rootd}*" : "#{adir}/*").sort.partition { |f| File.directory?(f)}
|
171
|
+
|
172
|
+
updir = hescape( dir.split(/\//)[0..-2].join("/"))
|
173
|
+
updir="/" if updir.length==0
|
174
|
+
up=(dir!="/") ? "<input type='button' onclick='location.href=\"#{updir}\"' value='Parent'>" : ""
|
175
|
+
"<html><head><title>#{dir}</title></head>\n<body><h3><center>#{@name} : #{dir[0..-1]}</center></h3>\n<hr>#{up}<br>#{to_table(dirs.map {|s| " <a href='#{hescape(to_relative(s))}'>"+File.basename(s)+"/"+"</a>\n"})}<hr>#{to_tableb(files) {|f| [" <a href='#{hescape(to_relative(f))}'>"+File.basename(f)+"</a>",n3(File.size(f)),File.mtime(f).strftime("%d/%m/%Y %H:%M:%S")]}}</body></html>"
|
176
|
+
end
|
177
|
+
def to_relative(f) f.gsub(/^#{@rootd}/,"/") end
|
178
|
+
def to_absolute(f) "#{@rootd}#{f.gsub(/^\//,'')}" end
|
179
|
+
def n3(n)
|
180
|
+
u=" B"
|
181
|
+
if n> 10000000
|
182
|
+
n=n/(1024*1024)
|
183
|
+
u=" MB"
|
184
|
+
elsif n> 100000
|
185
|
+
n=n/1024
|
186
|
+
u=" KB"
|
187
|
+
end
|
188
|
+
"<div style='width:100px;text-align:right;'>#{(n.round.to_i.to_s.reverse.gsub(/(\d\d\d)(?=\d)/,'\1 ' ).reverse) +u} | </div>"
|
189
|
+
end
|
190
|
+
def to_table(l)
|
191
|
+
"<table><tr>#{l.map {|s| "<td>#{s}</td>"}.join("</tr><tr>")}</tr></table>"
|
192
|
+
end
|
193
|
+
def to_tableb(l,&bl)
|
194
|
+
"<table><tr>#{l.map {|s| "<td>#{bl.call(s).join("</td><td>")}</td>"}.join("</tr><tr>")}</tr></table>"
|
195
|
+
end
|
196
|
+
def sendError(sock,no,txt=nil)
|
197
|
+
if txt
|
198
|
+
txt="<html><body><code><pre></pre>#{txt}</code></body></html>"
|
199
|
+
end
|
200
|
+
sock.write "HTTP/1.0 #{no} NOK\r\nContent-type: #{mime(".html")}\r\n\r\n <html><p>Error #{no} : #{txt}</p></html>"
|
201
|
+
end
|
202
|
+
def sendData(sock,type,content)
|
203
|
+
sock.write "HTTP/1.0 200 OK\r\nContent-Type: #{mime(type)}\r\nContent-Length: #{content.size}\r\n\r\n"
|
204
|
+
sock.write(content)
|
205
|
+
end
|
206
|
+
def sendFile(sock,filename)
|
207
|
+
s=File.size(filename)
|
208
|
+
if s < 0 || s>60_000_000 || File.extname(filename).downcase==".lnk"
|
209
|
+
logg @name,"Error reading file/File not downloadable #{File.basename(filename)} : (size=#{s})"
|
210
|
+
sendError(sock,500,"Error reading file/File not downloadable #{filename} : (size=#{s})" )
|
211
|
+
return
|
212
|
+
end
|
213
|
+
logg @name,filename," #{s/(1024*1024)} Mo" if s>10*1000_000
|
214
|
+
timeout([s/(512*1024),30.0].max.to_i) {
|
215
|
+
sock.write "HTTP/1.0 200 OK\r\nContent-Type: #{mime(filename)}\r\nContent-Length: #{File.size(filename)}\r\nLast-Modified: #{httpdate(File.mtime(filename))}\r\nDate: #{httpdate(Time.now)}\r\n\r\n"
|
216
|
+
File.open(filename,"rb") do |f|
|
217
|
+
f.binmode; sock.binmode;
|
218
|
+
( sock.write(f.read(32*1024)) while (! f.eof? && ! sock.closed?) ) rescue nil
|
219
|
+
end
|
220
|
+
}
|
221
|
+
end
|
222
|
+
def httpdate( aTime ); (aTime||Time.now).gmtime.strftime( "%a, %d %b %Y %H:%M:%S GMT" ); end
|
223
|
+
def mime(string)
|
224
|
+
MIME[string.split(/\./).last] || "application/octet-stream"
|
225
|
+
end
|
226
|
+
LICON="☀☃☎☑☑☠☣☮☺☂".split(/;/).map {|c| c+";"}
|
227
|
+
MIME={"png" => "image/png", "gif" => "image/gif", "html" => "text/html","htm" => "text/html",
|
228
|
+
"js" => "text/javascript" ,"css" => "text/css","jpeg" => "image/jpeg" ,"jpg" => "image/jpeg",
|
229
|
+
".json" => "applicatipon/json",
|
230
|
+
"pdf"=> "application/pdf" , "svg" => "image/svg+xml","svgz" => "image/svg+xml",
|
231
|
+
"xml" => "text/xml" ,"xsl" => "text/xml" ,"bmp" => "image/bmp" ,"txt" => "text/plain" ,
|
232
|
+
"rb" => "text/plain" ,"pas" => "text/plain" ,"tcl" => "text/plain" ,"java" => "text/plain" ,
|
233
|
+
"c" => "text/plain" ,"h" => "text/plain" ,"cpp" => "text/plain", "xul" => "application/vnd.mozilla.xul+xml",
|
234
|
+
"doc" => "application/msword", "docx" => "application/msword","dot"=> "application/msword",
|
235
|
+
"xls" => "application/vnd.ms-excel","xla" => "application/vnd.ms-excel","xlt" => "application/vnd.ms-excel","xlsx" => "application/vnd.ms-excel",
|
236
|
+
"ppt" => "application/vnd.ms-powerpoint", "pptx" => "application/vnd.ms-powerpoint"
|
237
|
+
}
|
238
|
+
end # 220 loc webserver :)
|
239
|
+
|
240
|
+
class Webserver < WebserverAbstract
|
241
|
+
def initialize(port=7080,cadence=10,timeout=120)
|
242
|
+
super(port,Dir.getwd(),"",cadence,timeout)
|
243
|
+
end
|
244
|
+
end
|
245
|
+
class WebserverRoot < WebserverAbstract
|
246
|
+
def initialize(port=7080,root=".",name="wwww",cadence=10,timeout=120,options={})
|
247
|
+
super(port,root,name,cadence,timeout,options)
|
248
|
+
end
|
249
|
+
end
|
250
|
+
def cliweb(root=Dir.getwd,port=59999)
|
251
|
+
Thread.abort_on_exception = false
|
252
|
+
BasicSocket.do_not_reverse_lookup = true
|
253
|
+
$ws=WebserverRoot.new(port,root,'femto ws',10,300, {});
|
254
|
+
puts "Server root path #{root} with port #{port}"
|
255
|
+
sleep
|
256
|
+
end
|
metadata
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: femtows
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.2.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Regis d'Aubarede
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-03-14 00:00:00.000000000Z
|
13
|
+
dependencies: []
|
14
|
+
description: ! "a tiny web server, for local file transfert, \nembededded, http experimentations\n"
|
15
|
+
email: regis.aubarede@gmail.com
|
16
|
+
executables:
|
17
|
+
- femtows.bat
|
18
|
+
- femtows.sh
|
19
|
+
extensions: []
|
20
|
+
extra_rdoc_files: []
|
21
|
+
files:
|
22
|
+
- bin/femtows.bat
|
23
|
+
- bin/femtows.sh
|
24
|
+
- CHANGELOG.txt
|
25
|
+
- demo.rb
|
26
|
+
- femtows.gemspec
|
27
|
+
- femtows.log
|
28
|
+
- gitc.bat
|
29
|
+
- guidemo.rb
|
30
|
+
- lib/femtows.rb
|
31
|
+
- README.md
|
32
|
+
- VERSION
|
33
|
+
homepage: http://github.com/raubarede/femtows
|
34
|
+
licenses: []
|
35
|
+
post_install_message:
|
36
|
+
rdoc_options: []
|
37
|
+
require_paths:
|
38
|
+
- lib
|
39
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
40
|
+
none: false
|
41
|
+
requirements:
|
42
|
+
- - ! '>='
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: '0'
|
45
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
46
|
+
none: false
|
47
|
+
requirements:
|
48
|
+
- - ! '>='
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
version: '0'
|
51
|
+
requirements: []
|
52
|
+
rubyforge_project:
|
53
|
+
rubygems_version: 1.8.15
|
54
|
+
signing_key:
|
55
|
+
specification_version: 3
|
56
|
+
summary: a tiny webserver
|
57
|
+
test_files: []
|
58
|
+
has_rdoc:
|