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)