make this a modular sinatra application
authorEric Wong <normalperson@yhbt.net>
Sat, 26 Jun 2010 07:46:21 +0000 (26 07:46 +0000)
committerEric Wong <normalperson@yhbt.net>
Sat, 26 Jun 2010 07:46:21 +0000 (26 07:46 +0000)
This should make the RubyGem work more reliably.

bin/local-openid
lib/local_openid.rb [new file with mode: 0644]

dissimilarity index 100%
index 45c4ede..7fc6b33 100755 (executable)
@@ -1,300 +1,3 @@
-#!/home/ew/bin/ruby
-# A personal OpenID identity provider, authentication is done by editing
-# a YAML file on the server where this application runs
-# (~/.local-openid/config.yml by default) instead of via HTTP/HTTPS
-# form authentication in the browser.
-
-require 'tempfile'
-require 'time'
-require 'yaml'
-
-require 'sinatra'
-require 'openid'
-require 'openid/extensions/sreg'
-require 'openid/extensions/pape'
-require 'openid/store/filesystem'
-set :static, false
-set :sessions, true
-set :environment, :production
-set :logging, false # load Rack::CommonLogger in config.ru instead
-
-BEGIN {
-  $local_openid ||=
-              File.expand_path(ENV['LOCAL_OPENID_DIR'] || '~/.local-openid')
-  Dir.mkdir($local_openid) unless File.directory?($local_openid)
-}
-
-# all the sinatra endpoints:
-get('/xrds') { big_lock { render_xrds(true) } }
-get('/') { big_lock { get_or_post } }
-post('/') { big_lock { get_or_post } }
-
-private
-
-# yes, I use gsub for templating because I find it easier than erb :P
-PROMPT = %q!<html>
-<head><title>OpenID login: %s</title></head>
-<body><h1>reload this page when approved: %s</h1></body>
-</html>!
-
-XRDS_HTML = %q!<html><head>
-<link rel="openid.server" href="%s" />
-<link rel="openid2.provider" href="%s" />
-<meta http-equiv="X-XRDS-Location" content="%sxrds" />
-<title>OpenID server endpoint</title>
-</head><body>OpenID server endpoint</body></html>!
-
-XRDS_XML = %q!<?xml version="1.0" encoding="UTF-8"?>
-<xrds:XRDS
-  xmlns:xrds="xri://$xrds"
-  xmlns="xri://$xrd*($v*2.0)">
-<XRD>
-  <Service priority="0">
-    %types
-    <URI>%s</URI>
-  </Service>
-</XRD>
-</xrds:XRDS>!
-
-CONFIG_HEADER = %!
-This file may be changed by #{__FILE__} or your favorite $EDITOR
-comments will be deleted when modified by #{__FILE__}.  See the
-comments end of this file for help on the format.
-!.lstrip!
-
-CONFIG_TRAILER = %!
-Configuration file description.
-
-* allowed_ips     An array of strings representing IPs that may
-                  authenticate through local-openid.  Only put
-                  IP addresses that you trust in here.
-
-Each OpenID consumer trust root will have its own hash keyed by
-the trust root URL.  Keys in this hash are:
-
-  - expires       The time at which this login will expire.
-                  This is generally the only entry you need to edit
-                  to approve a site.  You may also delete this line
-                  and rename the "expires1m" to this.
-  - expires1m     The time 1 minute from when this entry was updated.
-                  This is provided as a convenience for replacing
-                  the default "expires" entry.  This key may be safely
-                  removed by a user editing it.
-  - updated       Time this entry was updated, strictly informational.
-  - session_id    Unique identifier in your session cookie to prevent
-                  other users from hijacking your session.  You may
-                  delete this if you've changed browsers or computers.
-  - assoc_handle  See the OpenID specs, may be empty.  Do not edit this.
-
-SReg keys supported by the Ruby OpenID implementation should be
-supported, they include (but are not limited to):
-! << OpenID::SReg::DATA_FIELDS.map do |key, value|
-  "   - #{key}: #{value}"
-end.join("\n") << %!
-SReg keys may be global at the top-level or private to each trust root.
-Per-trust root SReg entries override the global settings.
-!
-
-include OpenID::Server
-
-# this is the heart of our provider logic, adapted from the
-# Ruby OpenID gem Rails example
-def get_or_post
-  oidreq = begin
-    server.decode_request(params)
-  rescue ProtocolError => err
-    halt(500, err.to_s)
-  end
-
-  oidreq or return render_xrds
-
-  oidresp = case oidreq
-  when CheckIDRequest
-    if oidreq.id_select && oidreq.immediate
-      oidreq.answer(false)
-    elsif is_authorized?(oidreq)
-      resp = oidreq.answer(true, nil, server_root)
-      add_sreg(oidreq, resp)
-      add_pape(oidreq, resp)
-      resp
-    elsif oidreq.immediate
-      oidreq.answer(false, server_root)
-    else
-      session[:id] ||= "#{Time.now.to_i}.#$$.#{rand}"
-      session[:ip] = request.ip
-      merge_config(oidreq)
-      write_config
-
-      # here we allow our user to open $EDITOR and edit the appropriate
-      # 'expires' field in config.yml corresponding to oidreq.trust_root
-      return PROMPT.gsub(/%s/, oidreq.trust_root)
-    end
-  else
-    server.handle_request(oidreq)
-  end
-
-  finalize_response(oidresp)
-end
-
-# we're the provider for exactly one identity.  However, we do rely on
-# being proxied and being hit with an appropriate HTTP Host: header.
-# Don't expect OpenID consumers to handle port != 80.
-def server_root
-  "http://#{request.host}/"
-end
-
-def server
-  @server ||= Server.new(
-      OpenID::Store::Filesystem.new("#$local_openid/store"),
-      server_root)
-end
-
-# support the simple registration extension if possible,
-# allow per-site overrides of various data points
-def add_sreg(oidreq, oidresp)
-  sregreq = OpenID::SReg::Request.from_openid_request(oidreq) or return
-  per_site = config[oidreq.trust_root] || {}
-
-  sreg_data = {}
-  sregreq.all_requested_fields.each do |field|
-    sreg_data[field] = per_site[field] || config[field]
-  end
-
-  sregresp = OpenID::SReg::Response.extract_response(sregreq, sreg_data)
-  oidresp.add_extension(sregresp)
-end
-
-def add_pape(oidreq, oidresp)
-  papereq = OpenID::PAPE::Request.from_openid_request(oidreq) or return
-  paperesp = OpenID::PAPE::Response.new(papereq.preferred_auth_policies,
-                                        papereq.max_auth_age)
-  # since this implementation requires shell/filesystem access to the
-  # OpenID server to authenticate, we can say we're at the highest
-  # auth level possible...
-  paperesp.add_policy_uri(OpenID::PAPE::AUTH_MULTI_FACTOR_PHYSICAL)
-  paperesp.auth_time = Time.now.utc.strftime('%Y-%m-%dT%H:%M:%SZ')
-  paperesp.nist_auth_level = 4
-  oidresp.add_extension(paperesp)
-end
-
-def err(msg)
-  env['rack.errors'].write("#{msg}\n")
-  false
-end
-
-def finalize_response(oidresp)
-  server.signatory.sign(oidresp) if oidresp.needs_signing
-  web_response = server.encode_response(oidresp)
-
-  case web_response.code
-  when HTTP_OK
-    web_response.body
-  when HTTP_REDIRECT
-    location = web_response.headers['location']
-    err("redirecting to: #{location} ...")
-    redirect(location)
-  else
-    halt(500, web_response.body)
-  end
-end
-
-# the heart of our custom authentication logic
-def is_authorized?(oidreq)
-  (config['allowed_ips'] ||= []).include?(request.ip) or
-    return err("Not allowed: #{request.ip}\n" \
-               "You need to put this IP in the 'allowed_ips' array "\
-               "in:\n #$local_openid/config.yml")
-
-  request.ip == session[:ip] or
-    return err("session IP mismatch: " \
-               "#{request.ip.inspect} != #{session[:ip].inspect}")
-
-  trust_root = oidreq.trust_root
-  per_site = config[trust_root] or
-    return err("trust_root unknown: #{trust_root}")
-
-  session_id = session[:id] or return err("no session ID")
-
-  assoc_handle = per_site['assoc_handle'] # this may be nil
-  expires = per_site['expires'] or
-    return err("no expires (trust_root=#{trust_root})")
-
-  assoc_handle == oidreq.assoc_handle or
-    return err("assoc_handle mismatch: " \
-               "#{assoc_handle.inspect} != #{oidreq.assoc_handle.inspect}" \
-               " (trust_root=#{trust_root})")
-
-  per_site['session_id'] == session_id or
-    return err("session ID mismatch: " \
-               "#{per_site['session_id'].inspect} != #{session_id.inspect}" \
-               " (trust_root=#{trust_root})")
-
-  expires > Time.now or
-    return err("Expired: #{expires.inspect} (trust_root=#{trust_root})")
-
-  true
-end
-
-def config
-  @config ||= begin
-    YAML.load(File.read("#$local_openid/config.yml"))
-  rescue Errno::ENOENT
-    {}
-  end
-end
-
-def merge_config(oidreq)
-  per_site = config[oidreq.trust_root] ||= {}
-  per_site.merge!({
-      'assoc_handle' => oidreq.assoc_handle,
-      'expires' => Time.at(0).utc,
-      'updated' => Time.now.utc,
-      'expires1m' => Time.now.utc + 60, # easy edit to "expires" in $EDITOR
-      'session_id' => session[:id],
-    })
-end
-
-def write_config
-  path = "#$local_openid/config.yml"
-  tmp = Tempfile.new('config.yml', File.dirname(path))
-  tmp.syswrite(CONFIG_HEADER.gsub(/^/m, "# "))
-  tmp.syswrite(config.to_yaml)
-  tmp.syswrite(CONFIG_TRAILER.gsub(/^/m, "# "))
-  tmp.fsync
-  File.rename(tmp.path, path)
-  tmp.close!
-end
-
-# this output is designed to be parsed by OpenID consumers
-def render_xrds(force = false)
-  if force || request.accept.include?('application/xrds+xml')
-
-    # this seems to work...
-    types = request.accept.include?('application/xrds+xml') ?
-       [ OpenID::OPENID_2_0_TYPE, OpenID::OPENID_1_0_TYPE, OpenID::SREG_URI ] :
-       [ OpenID::OPENID_IDP_2_0_TYPE ]
-
-    headers['Content-Type'] = 'application/xrds+xml'
-    types = types.map { |uri| "<Type>#{uri}</Type>" }.join("\n")
-    XRDS_XML.gsub(/%s/, server_root).gsub!(/%types/, types)
-  else # render a browser-friendly page with an XRDS pointer
-    headers['X-XRDS-Location'] = "#{server_root}xrds"
-    XRDS_HTML.gsub(/%s/, server_root)
-  end
-end
-
-# if a single-user OpenID provider like us is being hit by multiple
-# clients at once, then something is seriously wrong.  Can't use
-# Mutexes here since somebody could be running this as a CGI script
-def big_lock(&block)
-  lock = "#$local_openid/lock"
-  File.open(lock, File::WRONLY|File::CREAT|File::EXCL, 0600) do |fp|
-    begin
-      yield
-    ensure
-      File.unlink(lock)
-    end
-  end
-  rescue Errno::EEXIST
-    err("Lock: #{lock} exists! Possible hijacking attempt") rescue nil
-end
+#!/usr/bin/env ruby
+require 'local_openid'
+LocalOpenID.run!
diff --git a/lib/local_openid.rb b/lib/local_openid.rb
new file mode 100644 (file)
index 0000000..34ff753
--- /dev/null
@@ -0,0 +1,301 @@
+# A personal OpenID identity provider, authentication is done by editing
+# a YAML file on the server where this application runs
+# (~/.local-openid/config.yml by default) instead of via HTTP/HTTPS
+# form authentication in the browser.
+
+require 'tempfile'
+require 'time'
+require 'yaml'
+
+require 'sinatra/base'
+require 'openid'
+require 'openid/extensions/sreg'
+require 'openid/extensions/pape'
+require 'openid/store/filesystem'
+
+class LocalOpenID < Sinatra::Base
+  set :static, false
+  set :sessions, true
+  set :environment, :production
+  set :logging, false # load Rack::CommonLogger in config.ru instead
+
+  @@dir ||= File.expand_path(ENV['LOCAL_OPENID_DIR'] || '~/.local-openid')
+  Dir.mkdir(@@dir) unless File.directory?(@@dir)
+
+  # all the sinatra endpoints:
+  get('/xrds') { big_lock { render_xrds(true) } }
+  get('/') { big_lock { get_or_post } }
+  post('/') { big_lock { get_or_post } }
+
+  private
+
+  # yes, I use gsub for templating because I find it easier than erb :P
+  PROMPT = %q!<html>
+  <head><title>OpenID login: %s</title></head>
+  <body><h1>reload this page when approved: %s</h1></body>
+  </html>!
+
+  XRDS_HTML = %q!<html><head>
+  <link rel="openid.server" href="%s" />
+  <link rel="openid2.provider" href="%s" />
+  <meta http-equiv="X-XRDS-Location" content="%sxrds" />
+  <title>OpenID server endpoint</title>
+  </head><body>OpenID server endpoint</body></html>!
+
+  XRDS_XML = %q!<?xml version="1.0" encoding="UTF-8"?>
+  <xrds:XRDS
+    xmlns:xrds="xri://$xrds"
+    xmlns="xri://$xrd*($v*2.0)">
+  <XRD>
+    <Service priority="0">
+      %types
+      <URI>%s</URI>
+    </Service>
+  </XRD>
+  </xrds:XRDS>!
+
+  CONFIG_HEADER = %!
+  This file may be changed by #{__FILE__} or your favorite $EDITOR
+  comments will be deleted when modified by #{__FILE__}.  See the
+  comments end of this file for help on the format.
+  !.lstrip!
+
+  CONFIG_TRAILER = %!
+  Configuration file description.
+
+  * allowed_ips     An array of strings representing IPs that may
+                    authenticate through local-openid.  Only put
+                    IP addresses that you trust in here.
+
+  Each OpenID consumer trust root will have its own hash keyed by
+  the trust root URL.  Keys in this hash are:
+
+    - expires       The time at which this login will expire.
+                    This is generally the only entry you need to edit
+                    to approve a site.  You may also delete this line
+                    and rename the "expires1m" to this.
+    - expires1m     The time 1 minute from when this entry was updated.
+                    This is provided as a convenience for replacing
+                    the default "expires" entry.  This key may be safely
+                    removed by a user editing it.
+    - updated       Time this entry was updated, strictly informational.
+    - session_id    Unique identifier in your session cookie to prevent
+                    other users from hijacking your session.  You may
+                    delete this if you've changed browsers or computers.
+    - assoc_handle  See the OpenID specs, may be empty.  Do not edit this.
+
+  SReg keys supported by the Ruby OpenID implementation should be
+  supported, they include (but are not limited to):
+  ! << OpenID::SReg::DATA_FIELDS.map do |key, value|
+    "   - #{key}: #{value}"
+  end.join("\n") << %!
+  SReg keys may be global at the top-level or private to each trust root.
+  Per-trust root SReg entries override the global settings.
+  !
+
+  include OpenID::Server
+
+  # this is the heart of our provider logic, adapted from the
+  # Ruby OpenID gem Rails example
+  def get_or_post
+    oidreq = begin
+      server.decode_request(params)
+    rescue ProtocolError => err
+      halt(500, err.to_s)
+    end
+
+    oidreq or return render_xrds
+
+    oidresp = case oidreq
+    when CheckIDRequest
+      if oidreq.id_select && oidreq.immediate
+        oidreq.answer(false)
+      elsif is_authorized?(oidreq)
+        resp = oidreq.answer(true, nil, server_root)
+        add_sreg(oidreq, resp)
+        add_pape(oidreq, resp)
+        resp
+      elsif oidreq.immediate
+        oidreq.answer(false, server_root)
+      else
+        session[:id] ||= "#{Time.now.to_i}.#$$.#{rand}"
+        session[:ip] = request.ip
+        merge_config(oidreq)
+        write_config
+
+        # here we allow our user to open $EDITOR and edit the appropriate
+        # 'expires' field in config.yml corresponding to oidreq.trust_root
+        return PROMPT.gsub(/%s/, oidreq.trust_root)
+      end
+    else
+      server.handle_request(oidreq)
+    end
+
+    finalize_response(oidresp)
+  end
+
+  # we're the provider for exactly one identity.  However, we do rely on
+  # being proxied and being hit with an appropriate HTTP Host: header.
+  # Don't expect OpenID consumers to handle port != 80.
+  def server_root
+    "http://#{request.host}/"
+  end
+
+  def server
+    @server ||= Server.new(
+        OpenID::Store::Filesystem.new("#@@dir/store"),
+        server_root)
+  end
+
+  # support the simple registration extension if possible,
+  # allow per-site overrides of various data points
+  def add_sreg(oidreq, oidresp)
+    sregreq = OpenID::SReg::Request.from_openid_request(oidreq) or return
+    per_site = config[oidreq.trust_root] || {}
+
+    sreg_data = {}
+    sregreq.all_requested_fields.each do |field|
+      sreg_data[field] = per_site[field] || config[field]
+    end
+
+    sregresp = OpenID::SReg::Response.extract_response(sregreq, sreg_data)
+    oidresp.add_extension(sregresp)
+  end
+
+  def add_pape(oidreq, oidresp)
+    papereq = OpenID::PAPE::Request.from_openid_request(oidreq) or return
+    paperesp = OpenID::PAPE::Response.new(papereq.preferred_auth_policies,
+                                          papereq.max_auth_age)
+    # since this implementation requires shell/filesystem access to the
+    # OpenID server to authenticate, we can say we're at the highest
+    # auth level possible...
+    paperesp.add_policy_uri(OpenID::PAPE::AUTH_MULTI_FACTOR_PHYSICAL)
+    paperesp.auth_time = Time.now.utc.strftime('%Y-%m-%dT%H:%M:%SZ')
+    paperesp.nist_auth_level = 4
+    oidresp.add_extension(paperesp)
+  end
+
+  def err(msg)
+    env['rack.errors'].write("#{msg}\n")
+    false
+  end
+
+  def finalize_response(oidresp)
+    server.signatory.sign(oidresp) if oidresp.needs_signing
+    web_response = server.encode_response(oidresp)
+
+    case web_response.code
+    when HTTP_OK
+      web_response.body
+    when HTTP_REDIRECT
+      location = web_response.headers['location']
+      err("redirecting to: #{location} ...")
+      redirect(location)
+    else
+      halt(500, web_response.body)
+    end
+  end
+
+  # the heart of our custom authentication logic
+  def is_authorized?(oidreq)
+    (config['allowed_ips'] ||= []).include?(request.ip) or
+      return err("Not allowed: #{request.ip}\n" \
+                 "You need to put this IP in the 'allowed_ips' array "\
+                 "in:\n #@@dir/config.yml")
+
+    request.ip == session[:ip] or
+      return err("session IP mismatch: " \
+                 "#{request.ip.inspect} != #{session[:ip].inspect}")
+
+    trust_root = oidreq.trust_root
+    per_site = config[trust_root] or
+      return err("trust_root unknown: #{trust_root}")
+
+    session_id = session[:id] or return err("no session ID")
+
+    assoc_handle = per_site['assoc_handle'] # this may be nil
+    expires = per_site['expires'] or
+      return err("no expires (trust_root=#{trust_root})")
+
+    assoc_handle == oidreq.assoc_handle or
+      return err("assoc_handle mismatch: " \
+                 "#{assoc_handle.inspect} != #{oidreq.assoc_handle.inspect}" \
+                 " (trust_root=#{trust_root})")
+
+    per_site['session_id'] == session_id or
+      return err("session ID mismatch: " \
+                 "#{per_site['session_id'].inspect} != #{session_id.inspect}" \
+                 " (trust_root=#{trust_root})")
+
+    expires > Time.now or
+      return err("Expired: #{expires.inspect} (trust_root=#{trust_root})")
+
+    true
+  end
+
+  def config
+    @config ||= begin
+      YAML.load(File.read("#@@dir/config.yml"))
+    rescue Errno::ENOENT
+      {}
+    end
+  end
+
+  def merge_config(oidreq)
+    per_site = config[oidreq.trust_root] ||= {}
+    per_site.merge!({
+        'assoc_handle' => oidreq.assoc_handle,
+        'expires' => Time.at(0).utc,
+        'updated' => Time.now.utc,
+        'expires1m' => Time.now.utc + 60, # easy edit to "expires" in $EDITOR
+        'session_id' => session[:id],
+      })
+  end
+
+  def write_config
+    path = "#@@dir/config.yml"
+    tmp = Tempfile.new('config.yml', File.dirname(path))
+    tmp.syswrite(CONFIG_HEADER.gsub(/^/m, "# "))
+    tmp.syswrite(config.to_yaml)
+    tmp.syswrite(CONFIG_TRAILER.gsub(/^/m, "# "))
+    tmp.fsync
+    File.rename(tmp.path, path)
+    tmp.close!
+  end
+
+  # this output is designed to be parsed by OpenID consumers
+  def render_xrds(force = false)
+    if force || request.accept.include?('application/xrds+xml')
+
+      # this seems to work...
+      types = request.accept.include?('application/xrds+xml') ?
+         [ OpenID::OPENID_2_0_TYPE,
+           OpenID::OPENID_1_0_TYPE,
+           OpenID::SREG_URI ] :
+         [ OpenID::OPENID_IDP_2_0_TYPE ]
+
+      headers['Content-Type'] = 'application/xrds+xml'
+      types = types.map { |uri| "<Type>#{uri}</Type>" }.join("\n")
+      XRDS_XML.gsub(/%s/, server_root).gsub!(/%types/, types)
+    else # render a browser-friendly page with an XRDS pointer
+      headers['X-XRDS-Location'] = "#{server_root}xrds"
+      XRDS_HTML.gsub(/%s/, server_root)
+    end
+  end
+
+  # if a single-user OpenID provider like us is being hit by multiple
+  # clients at once, then something is seriously wrong.  Can't use
+  # Mutexes here since somebody could be running this as a CGI script
+  def big_lock(&block)
+    lock = "#@@dir/lock"
+    File.open(lock, File::WRONLY|File::CREAT|File::EXCL, 0600) do |fp|
+      begin
+        yield
+      ensure
+        File.unlink(lock)
+      end
+    end
+    rescue Errno::EEXIST
+      err("Lock: #{lock} exists! Possible hijacking attempt") rescue nil
+  end
+end