Target: base for writing “one-two-three-day development” micro web-application.
After some days of per-1min-monitoring work good and stable. Short benchmark in bottom the page.
Released:
Mail account database/objects included for example/testing. There I use shared/static model object across sinatra helpers.
CSS style framework used, of course, Zurb Foundation.
#!/usr/local/bin/ruby require 'json' require 'logger' require 'sqlite3' require 'htauth' require 'sinatra/base' require 'thin' class DB def initialize(dbname) @dbname = dbname end def query(query) db = SQLite3::Database.open @dbname db.results_as_hash = true res = db.execute(query) db.close unless db.closed? res end end class Domain < DB def count res = self.query("select count(id) as count from domain") res.first['count'] end def nextid return 1 if self.count == 0 res = self.query("select id from domain order by id desc limit 1") res.first['id'] += 1 end def list self.query("select * from domain order by id") end def name?(name) res = self.query("select * from domain where name = '#{name}' order by id limit 1") return true if res.count > 0 false end def id?(id) res = self.query("select * from domain where id = '#{id}' limit 1") return true if res.count > 0 false end def id(name) res = self.query("select id from domain where name = '#{name}' order by id limit 1") return nil if res.count == 0 res.first['id'] end def name(id) res = self.query("select name from domain where id = '#{id}' limit 1") return nil if res.count == 0 res.first['name'] end def add(name) return false if self.name?(name) id = self.nextid self.query("insert into domain (id, name) values (#{id}, '#{name}')") self.name?(name) end def delete(id) return true unless self.id?(id) self.query("delete from domain where id = '#{id}'") !self.id?(id) end def update(id, newname) return false unless self.id?(id) return false if self.name?(newname) self.query("update domain set name = '#{newname}' where id = '#{id}'") self.name?(newname) end end class User < DB def initialize(db) super(db) @domain = Domain.new(db) end def count res = self.query("select count(id) as count from user") res.first['count'] end def nextid return 1 if self.count == 0 res = self.query("select id from user order by id desc limit 1") res.first['id'] += 1 end def list self.query("select u.id, u.name, u.domainid, d.name as domain, u.password from user u, domain d where u.domainid = d.id order by d.name, u.name") end def name?(name, domainid) return false unless @domain.id?(domainid) res = self.query("select u.id from user u, domain d where u.name = '#{name}' and u.domainid = d.id limit 1") return true if res.count > 0 false end def id?(userid) res = self.query("select id from user where id = '#{userid}' limit 1") return true if res.count > 0 false end def add(name, domainid, password) return false if self.name?(name, domainid) return false unless @domain.id?(domainid) id = self.nextid self.query("insert into user(id, name, domainid, password) values (#{id}, '#{name}', #{domainid}, '#{password}')") self.name?(name, domainid) end def delete(userid) return true if not self.id?(userid) self.query("delete from user where id = #{userid}") !self.id?(userid) end end class Alias < DB def initialize(db) super(db) @domain = Domain.new(db) end def count res = self.query("select count(id) as count from alias") res.first['count'] end def nextid return 1 if self.count == 0 res = self.query("select id from alias order by id desc limit 1") res.first['id'] += 1 end def list self.query("select a.id, a.name, a.domainid, d.name as domain, goto from alias a, domain d where a.domainid = d.id order by d.name, a.name") end def name?(name, domainid) return false unless @domain.id?(domainid) res = self.query("select a.id from alias a, domain d where a.name = '#{name}' and a.domainid = d.id limit 1") return true if res.count > 0 false end def id?(userid) res = self.query("select id from alias where id = '#{userid}' limit 1") return true if res.count > 0 false end def add(name, domainid, goto) return false if self.name?(name, domainid) return false unless @domain.id?(domainid) id = self.nextid self.query("insert into add(id, name, domainid, goto) values (#{id}, '#{name}', #{domainid}, '#{goto}')") self.name?(name, domainid) end def delete(id) return true if not self.id?(id) self.query("delete from alias where id = #{id}") !self.id?(id) end end class SecureThinBackend < Thin::Backends::TcpServer def initialize(host, port, options) super(host, port) @ssl = true @ssl_options = options end end class App < Sinatra::Base @@pwfile = "/usr/local/etc/si4/pw" @@weblog = "/var/log/si4/access.log" @@errlog = "/var/log/si4/error.log" @@pidfile = "/var/run/si4/pid" @@crtfile = "/usr/local/etc/si4/crt" @@keyfile = "/usr/local/etc/si4/key" @@dbname = "/var/db/si4/db" @@user = User.new(@@dbname) @@domain = Domain.new(@@dbname) @@alias = Alias.new(@@dbname) def self.pidfile @@pidfile end def self.errlog @@errlog end def user?(user, password) logger.info("Auth: User #{user} try get access") unless File.readable?(@@pwfile) then logger.warning("Auth: Cannot read #{@@pwfile}") return false end File.open(@@pwfile).each do | line | htuser, digest = line.strip.split(':') next unless htuser == user if digest.match(/apr1/) then dummy, apr, salt = digest.split('$') md5 = HTAuth::Md5.new( 'salt' => salt ) if md5.encode(password) == digest then logger.info("Auth: User #{user} access granted") return true end elsif digest.match(/SHA/) then sha1 = HTAuth::Sha1.new if sha1.encode(password) == digest then logger.info("Auth: User #{user} access granted") return true end end end return false end configure do # logfile = File.new(@@weblog, 'a') # logfile.sync = true # use Rack::CommonLogger, logfile set :public_folder, '/usr/local/share/si4/public' set :views, '/usr/local/share/si4/templ' set :server, "thin" set :secret, '3d04dd2b1403a7ed52373953e8bbf921' set :port, 8081 set :bind, '0.0.0.0' set :sessions, true set :show_exceptions, true set :logging, true set :dump_errors, true set :raise_errors, true set :quiet, true class << settings def server_settings { :backend => SecureThinBackend, :private_key_file => @@keyfile, :cert_chain_file => @@crtfile, :verify_peer => false } end end end not_found do erb :not_found end error do erb :error end error 401 do redirect to '/login' end helpers do def auth? redirect to '/login' unless session[:user] # halt 401 unless session[:user] end def user @@user end def domain @@domain end def alias @@alias end end get '/login' do erb :login, :layout => false end post '/login' do if user?(params['username'], params['password']) then session[:user] = params['username'] redirect to "/" else erb :login, :layout => false end end get '/logout' do session.clear redirect to '/login' end get '/hello' do content_type :json { message: "hello" }.to_json end get '/' do auth? erb :index end before do logger.datetime_format = '%Y-%m-%d %H:%M:%S' logger.formatter = proc do |severity, datetime, progname, msg| "SINATRA: #{datetime}: #{severity} #{msg}\n" end user = session[:user] or 'undef' logger.info("#{request.request_method} #{request.url} from #{request.ip} as #{session[:user]}") end end Process.euid = Etc.getpwnam("root").uid Process.daemon errlog = File.new(App.errlog, "a+") errlog.sync = true $stdout.reopen(errlog) $stderr.reopen(errlog) begin File.open(App.pidfile, 'w') do |file| file.write Process.pid end rescue puts "Cannot write pid file #{App.pidfile}\n" exit end App.run! File.delete App.pidfile if File.exist? App.pidfile #EOF
<h3>Ups... Exception...</h3> <%= env['sinatra.error'].message %>
<!doctype html> <html class="no-js" lang="en" dir="ltr"> <head> <meta charset="utf-8"> <meta http-equiv="x-ua-compatible" content="ie=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title></title> <link rel="stylesheet" href="/css/foundation-float.min.css"> <link rel="stylesheet" href="/css/app.css"> <script src="/js/jquery.min.js"></script> <script src="/js/foundation.min.js"></script> </head> <body> <div class="top-bar" id="topbar-menu"> <div class="top-bar-left"> <ul class="dropdown menu" data-dropdown-menu> <li class="menu-text">Tmpl</li> <li><a href="/logout">Logout</a></li> </ul> </div> </div> <div class="row"> </div> <div class="row"> <!- end of head template -> <%= yield %> <!- begin of tail template -> </div> </div> <hr/> <div class="row"> <p class="text-center">Made by <a href="http://wiki.unix7.org">Borodin Oleg</a></p> </div> <script src="/js/app.js"></script> </body> </html> <!- end of tail template -> <!- EOF ->
<!- $Id$ -> <html class="no-js" lang="en" dir="ltr"> <head> <meta charset="utf-8"> <meta http-equiv="x-ua-compatible" content="ie=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Login</title> <link rel="stylesheet" href="/css/foundation-float.min.css"> <link rel="stylesheet" href="/css/app.css"> <script src="/js/jquery.min.js"></script> <script src="/js/foundation.min.js"></script> </head> <body> <div class="row"> </div> <div class="row"> <div class="small-3 columns hide-for-small"> </div> <div class="small-6 columns text-center"> <div class="row"> <div class="columns"> <form accept-charset="UTF-8" method="post" action="/login"> <div class="row column"> <h4 class="text-center">Login with your username</h4> <label>Username <input type="text" name="username" placeholder="username" /> </label> <label>Password <input type="password" name="password" placeholder="password" /> </label> <p> <button type="submit" class="button">Log In</button> </p> <p class="text-center"></p> </div> </form> </div> </div> </div> <div class="small-3 columns hide-for-small"> </div> </div> <hr/> <div class="row"> <p class="text-center">Made by <a href="http://wiki.unix7.org">Borodin Oleg</a></p> </div> <script src="/js/app.js"></script> </body> </html> <!- EOF ->
<h3>Ups... Page not found</h3>
On FreeBSD 11/ Intel(R) Core(TM) i5-4300U CPU @ 1.90GHz
# ab -c 12 -n1000 http://localhost:8081/
This is ApacheBench, Version 2.3 <$Revision: 1663405 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking localhost (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Completed 1000 requests
Finished 1000 requests
Server Software: thin
Server Hostname: localhost
Server Port: 8081
Document Path: /
Document Length: 1357 bytes
Concurrency Level: 12
Time taken for tests: 3.941 seconds
Complete requests: 1000
Failed requests: 0
Total transferred: 2093000 bytes
HTML transferred: 1357000 bytes
Requests per second: 253.74 [#/sec] (mean)
Time per request: 47.292 [ms] (mean)
Time per request: 3.941 [ms] (mean, across all concurrent requests)
Transfer rate: 518.64 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.1 0 1
Processing: 8 47 26.0 41 181
Waiting: 7 47 26.0 41 181
Total: 8 47 26.0 41 181
Percentage of the requests served within a certain time (ms)
50% 41
66% 53
75% 60
80% 66
90% 83
95% 98
98% 110
99% 124
100% 181 (longest request)
# ab -c 12 -n1000 https://localhost:8081/
This is ApacheBench, Version 2.3 <$Revision: 1663405 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking localhost (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Completed 1000 requests
Finished 1000 requests
Server Software: thin
Server Hostname: localhost
Server Port: 8081
SSL/TLS Protocol: TLSv1.2,AES256-GCM-SHA384,2048,256
Document Path: /
Document Length: 1357 bytes
Concurrency Level: 12
Time taken for tests: 11.122 seconds
Complete requests: 1000
Failed requests: 0
Total transferred: 2093000 bytes
HTML transferred: 1357000 bytes
Requests per second: 89.91 [#/sec] (mean)
Time per request: 133.460 [ms] (mean)
Time per request: 11.122 [ms] (mean, across all concurrent requests)
Transfer rate: 183.78 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 8 53 21.6 60 102
Processing: 9 79 47.2 64 248
Waiting: 9 76 46.4 60 248
Total: 20 132 51.2 125 316
Percentage of the requests served within a certain time (ms)
50% 125
66% 141
75% 156
80% 168
90% 206
95% 240
98% 266
99% 281
100% 316 (longest request)