#!/usr/bin/env ruby
#
# Welcome to the OpenID protect CGI script. This is designed to make
# directories on your webserver availble only to OpenID users. It only
# requires that you have Ruby, the Ruby OpenID library and the ability
# to run CGI scripts on your server.
#
# This code is freely available under the BSD license. USE AT YOUR OWN RISK.
# Copyright 2006 JanRain Inc. by Brian Ellin - brianellin at gmail.com
#
# `-:
# `-/oo+
# ./ooooo+
# -oooooo+
# -oooooo+
# -oooooo+
# :oooooo+`
# `````````:oooooo+.````````
# `````````````:oooooo+`````````````` ````
# ``````````` -oooooo+ ``````````````
# `````````` -oooooo+ ```````````
# ```````` -oooooo+ `````````````
# ```````` -oooooo+
# ```````` -oooooo+
# ```````` -oooooo+
# ```````` -oooooo+
# ```````` -oooooo+
# ````````` -oooooo+
# `````````` -oooooo+
# ````````````` -oooooo+
# `````````````:oooo/-`
# ````````:+:.`
# `
# Installation.
#
# Put this cgi script somehwere visible, and chmod +x it. Make sure you
# have ruby >= 1.8.4 installed and the latest version of the ruby-openid
# library installed. The library is available at:
#
# http://www.openidenabled.com/openid/libraries/ruby
#
# Cofiguration options.
#
# 1) RESOURCES. The RESOURCES variable below contains a an Array of Arrays
# describing the directories you'd like to make visible to your users.
# The first element is a human-readable name of the dir, second is a brief
# description, and third is the full path to the directory. You may add
# as many resources as you like.
RESOURCES = [
['Stuff', 'Some stuff to share', '/path/to/stuff']
]
#
# 2) USERS. The USERS variable below is an Array of URLS of openid users
# who may access the RESOURCES. Verified users who are not in the array
# will not have access to your resources. You may allow anyone with an
# openid to access your resources by setting this variable to nil.
USERS = ['http://brianellin.com/']
# 3) The HEADER is what users see at the top of the page.
HEADER = "Protected by OpenID. "
# 4) OpenID Store Location. OpenID needs to store some infomation between
# requests, and this data needs to live in a secure directory. It should
# exist in a place that is not readable by other users of your system.
STORE_DIR = '/tmp/openid-store'
# 5) LOG_FILE. This variable contains the file which events will be logged to.
LOG_FILE = '/dev/null'
# 6) WEBMASTER. Email address or string indicating how to contact the person
# running the server.
WEBMASTER = 'brianellin at gmail.com'
# 7) APP_URL. The full url of this cgi script.
APP_URL = consumer_url = 'http://brianellin.com/cgi-bin/safe.cgi'
###############################################################
# Don't modify code below unless you know what you are doing! #
###############################################################
require 'cgi'
require 'cgi/session'
require 'pathname'
begin
require 'rubygems'
require_gem 'ruby-openid'
rescue LoadError
require 'openid'
end
# some common mime type defs
mime = < '302', 'location' => url})
exit(0)
end
end
# extend FileStore to use Marshal so it can store interesting objects other
# than strings. We need to store a real object in the session state which
# represents the yadis discovery.
class MarshallingFileStore < CGI::Session::FileStore
def restore
unless @hash
begin
f = File.open(@path, 'r')
f.flock(File::LOCK_SH)
@hash = Marshal.load(f)
ensure
f.close unless f.nil?
end
end
@hash
end
def update
return unless @hash
begin
f = File.open(@path, File::CREAT|File::TRUNC|File::RDWR, 0600)
f.flock File::LOCK_EX
Marshal.dump(@hash, f)
ensure
f.close unless f.nil?
end
end
end
# the CGI object provides an incompatabile interface w/ the
# the OpenID library query object interface. This method maps
# the cgi object to a regular hash object.
def query_from_cgi(cgi)
query = {}
cgi.keys.each {|k| query[k] = cgi[k]}
return query
end
# setup
cgi = CGI.new('html4')
session = CGI::Session.new(cgi,
'session_key' => 'openid_safe',
'session_prefix' => 'openid_session',
'database_manager' => MarshallingFileStore)
store = OpenID::FilesystemStore.new(STORE_DIR)
consumer = OpenID::Consumer.new(session, store)
logout = ''
def burl(args)
args['action'] = 'browse'
OpenID::Util.append_args(APP_URL, args)
end
def barf(msg)
print msg
exit(0)
end
def logged_in_content
content = 'You have access to:
'
RESOURCES.each do |name, desc, path|
link = OpenID::Util.append_args(APP_URL,
{'action'=>'browse',
'name' => name})
content += "- #{name}
- #{desc}
"
end
content += '
'
end
def log(msg)
$log.puts(Time.now.to_s + ' ' + msg)
end
# dispatch
case cgi['action']
when 'login'
request = consumer.begin(cgi['openid_url'])
return_to = OpenID::Util.append_args(consumer_url,
{'action' => 'openid-response'})
redirect_url = request.redirect_url(consumer_url, return_to)
cgi.redirect(redirect_url)
when 'openid-response'
query = query_from_cgi(cgi)
response = consumer.complete(query)
case response.status
when OpenID::SUCCESS
openid_url = response.identity_url
allowed = USERS.find {|u| OpenID::Util.urls_equal?(u, openid_url)}
if USERS.nil? or allowed
session['openid'] = openid_url
log("VERIFIED - #{openid_url}")
cgi.redirect(consumer_url)
else
log("VERIFIED BUT NOT ALLOWED- #{openid_url}")
page = "Sorry, #{openid_url}. Your identity has been verified, but I don't trust you with my data.
You may contact me: #{WEBMASTER}
"+cgi.a(APP_URL) {'Home'}
end
when OpenID::FAILURE
page = "Unable to verify your idenity."
when OpenID::CANCEL
page = "Verification cancelled."
else
page = "Got a bogus response from the OpenID server."
end
when 'logout'
session.delete
cgi.redirect(consumer_url)
when 'browse'
unless session['openid']
cgi.redirect(APP_URL)
else
logout = cgi.a(OpenID::Util.append_args(consumer_url,
{'action'=>'logout'})) {'Logout'}
end
suffix = cgi['suffix']
# someone is trying to do bad things
if suffix.include?('..') or suffix.index('/') == 0
barf('play nice!')
end
name, desc, path = hashed_resources[cgi['name']]
full_path = Pathname.new(path)
full_path += Pathname.new(suffix) if suffix
if File.directory?(full_path)
page = cgi.a(APP_URL) {'Home'} +' - '+ cgi.a(burl('name'=>name)) {name}
stack = []
suffix.split('/').each do |s|
stack << s
page += ' / ' + cgi.a(burl('name'=>name,'suffix'=>stack.join('/'))) {s}
end
page += cgi.br*2
begin
Dir.foreach(full_path) do |f|
unless ['.','..'].member?(f)
if suffix and suffix != ''
slink = suffix+'/'+f
else
slink = f
end
url = burl('name'=>name,'suffix'=>slink)
is_dir = File.directory?(full_path + f) ? '/' : ''
page += "\n#{f}#{is_dir}
\n"
end
end
rescue Errno::EACCES
page += 'Sorry, you cannot see inside here.'
end
else
headers = {
'status' => 'OK',
'content-disposition' => 'attachment; filename='+suffix,
'content-length' => File.stat(full_path).size.to_s
}
ext = suffix.split('.')[-1]
headers['content-type'] = mimes[ext] if mimes[ext]
log("DOWNLOAD - #{session['openid']} - #{name} - #{suffix}")
# set the headers
print cgi.header(headers)
# stream the file... this should work for files of very large sized
# without bringing down the server
File.open(full_path, 'r') do |f|
data = f.read(4096)
while data
print data # print has no \n at the end, puts does
data = f.read(4096)
end
end
exit(0)
end
else
if session['openid']
logout = cgi.a(OpenID::Util.append_args(consumer_url,
{'action'=>'logout'})) {'Logout'}
page = logged_in_content
else
page = ""
end
end
session.close
# render page
cgi.out {
cgi.html {
cgi.head{
cgi.title {'Protected by OpenID'}
} + \
cgi.body {
HEADER + logout + cgi.br*2 + page
}
}
}