bin/local-openid in local-openid-0.1.1 vs bin/local-openid in local-openid-0.2.0
- old
+ new
@@ -1,300 +1,19 @@
-#!/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)
+#!/usr/bin/env ruby
+require 'local_openid'
+require 'optparse'
+require 'socket'
+BasicSocket.do_not_reverse_lookup = true
+opts = {
+ :server => 'webrick', # webrick is standard, and plenty fast enough
}
-
-# 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)
+OptionParser.new { |op|
+ op.on('-s <mongrel|thin|webrick>') { |v| opts[:server] = v }
+ op.on('-p port') { |val| opts[:port] = val.to_i }
+ op.on('-o addr') { |val| opts[:bind] = val }
+ op.on('-h', '--help', 'Show this message') do
+ puts op.to_s
+ exit
end
+}.parse!(ARGV)
- 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
+LocalOpenID.run!(opts)