User Tools

Site Tools


Sinatra+Thin ruby daemon, beta template

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:

  • Session authentication with htpassword file
  • SSL/TLS
  • Full unix daemon with start/stop (freebsd rc, linux systemd/init.d)
  • With GNU autoconf/automake configuration

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. =)

si4.rb
#!/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

error.erb

templ/error.erb
<h3>Ups... Exception...</h3>
 
<%= env['sinatra.error'].message %>

index.erb

templ/index.erb
<table>
    <thead>
        <td>#</td>
        <td></td>
        <td>goto</td>
    </thead>
<% n = 0 %>
<% user.list.each do |row| %>
    <% n += 1 %>
    <tr>
    <td><%= n %>
    <td><%= row['name'] %>@<%= row['domain'] %></td>
    <td><%= row['password'] %></td>
<% end %>
</table>

layout.erb

templ/layout.erb
<!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">&nbsp;</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 ->

login.erb

templ/login.erb
<!- $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">&nbsp;</div>
        <div class="row">
            <div class="small-3 columns hide-for-small">&nbsp;</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">&nbsp;</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 ->

not_found.erb

templ/not_found.erb
<h3>Ups... Page not found</h3>

Benchmarks

On FreeBSD 11/ Intel(R) Core(TM) i5-4300U CPU @ 1.90GHz

  • pure http
# 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)
  • with TLS/SSL
# 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)

First PagePrevious PageBack to overviewNext PageLast Page