Unpacked OpenID gem. #699

git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/trunk@2437 e93f8b46-1217-0410-a6f0-8f06a7374b81
This commit is contained in:
Eric Davis 2009-02-11 19:06:37 +00:00
parent 70efee1bc5
commit f70be197e0
191 changed files with 30002 additions and 0 deletions

View File

@ -54,4 +54,6 @@ Rails::Initializer.run do |config|
# Define your email configuration in email.yml instead. # Define your email configuration in email.yml instead.
# It will automatically turn deliveries on # It will automatically turn deliveries on
config.action_mailer.perform_deliveries = false config.action_mailer.perform_deliveries = false
config.gem 'ruby-openid', :lib => 'openid'
end end

View File

@ -0,0 +1,290 @@
--- !ruby/object:Gem::Specification
name: ruby-openid
version: !ruby/object:Gem::Version
version: 2.1.4
platform: ruby
authors:
- JanRain, Inc
autorequire: openid
bindir: bin
cert_chain:
date: 2008-12-19 00:00:00 -08:00
default_executable:
dependencies: []
description:
email: openid@janrain.com
executables: []
extensions: []
extra_rdoc_files:
- README
- INSTALL
- LICENSE
- UPGRADE
files:
- examples/README
- examples/active_record_openid_store
- examples/rails_openid
- examples/discover
- examples/active_record_openid_store/lib
- examples/active_record_openid_store/test
- examples/active_record_openid_store/init.rb
- examples/active_record_openid_store/README
- examples/active_record_openid_store/XXX_add_open_id_store_to_db.rb
- examples/active_record_openid_store/XXX_upgrade_open_id_store.rb
- examples/active_record_openid_store/lib/association.rb
- examples/active_record_openid_store/lib/nonce.rb
- examples/active_record_openid_store/lib/open_id_setting.rb
- examples/active_record_openid_store/lib/openid_ar_store.rb
- examples/active_record_openid_store/test/store_test.rb
- examples/rails_openid/app
- examples/rails_openid/components
- examples/rails_openid/config
- examples/rails_openid/db
- examples/rails_openid/doc
- examples/rails_openid/lib
- examples/rails_openid/log
- examples/rails_openid/public
- examples/rails_openid/script
- examples/rails_openid/test
- examples/rails_openid/vendor
- examples/rails_openid/Rakefile
- examples/rails_openid/README
- examples/rails_openid/app/controllers
- examples/rails_openid/app/helpers
- examples/rails_openid/app/models
- examples/rails_openid/app/views
- examples/rails_openid/app/controllers/application.rb
- examples/rails_openid/app/controllers/login_controller.rb
- examples/rails_openid/app/controllers/server_controller.rb
- examples/rails_openid/app/controllers/consumer_controller.rb
- examples/rails_openid/app/helpers/application_helper.rb
- examples/rails_openid/app/helpers/login_helper.rb
- examples/rails_openid/app/helpers/server_helper.rb
- examples/rails_openid/app/views/layouts
- examples/rails_openid/app/views/login
- examples/rails_openid/app/views/server
- examples/rails_openid/app/views/consumer
- examples/rails_openid/app/views/layouts/server.rhtml
- examples/rails_openid/app/views/login/index.rhtml
- examples/rails_openid/app/views/server/decide.rhtml
- examples/rails_openid/app/views/consumer/index.rhtml
- examples/rails_openid/config/environments
- examples/rails_openid/config/database.yml
- examples/rails_openid/config/boot.rb
- examples/rails_openid/config/environment.rb
- examples/rails_openid/config/routes.rb
- examples/rails_openid/config/environments/development.rb
- examples/rails_openid/config/environments/production.rb
- examples/rails_openid/config/environments/test.rb
- examples/rails_openid/doc/README_FOR_APP
- examples/rails_openid/lib/tasks
- examples/rails_openid/public/images
- examples/rails_openid/public/javascripts
- examples/rails_openid/public/stylesheets
- examples/rails_openid/public/dispatch.cgi
- examples/rails_openid/public/404.html
- examples/rails_openid/public/500.html
- examples/rails_openid/public/dispatch.fcgi
- examples/rails_openid/public/dispatch.rb
- examples/rails_openid/public/favicon.ico
- examples/rails_openid/public/robots.txt
- examples/rails_openid/public/images/openid_login_bg.gif
- examples/rails_openid/public/javascripts/controls.js
- examples/rails_openid/public/javascripts/dragdrop.js
- examples/rails_openid/public/javascripts/effects.js
- examples/rails_openid/public/javascripts/prototype.js
- examples/rails_openid/script/performance
- examples/rails_openid/script/process
- examples/rails_openid/script/console
- examples/rails_openid/script/about
- examples/rails_openid/script/breakpointer
- examples/rails_openid/script/destroy
- examples/rails_openid/script/generate
- examples/rails_openid/script/plugin
- examples/rails_openid/script/runner
- examples/rails_openid/script/server
- examples/rails_openid/script/performance/benchmarker
- examples/rails_openid/script/performance/profiler
- examples/rails_openid/script/process/spawner
- examples/rails_openid/script/process/reaper
- examples/rails_openid/script/process/spinner
- examples/rails_openid/test/fixtures
- examples/rails_openid/test/functional
- examples/rails_openid/test/mocks
- examples/rails_openid/test/unit
- examples/rails_openid/test/test_helper.rb
- examples/rails_openid/test/functional/login_controller_test.rb
- examples/rails_openid/test/functional/server_controller_test.rb
- examples/rails_openid/test/mocks/development
- examples/rails_openid/test/mocks/test
- lib/openid
- lib/hmac
- lib/openid.rb
- lib/openid/cryptutil.rb
- lib/openid/extras.rb
- lib/openid/urinorm.rb
- lib/openid/util.rb
- lib/openid/trustroot.rb
- lib/openid/message.rb
- lib/openid/yadis
- lib/openid/consumer
- lib/openid/fetchers.rb
- lib/openid/dh.rb
- lib/openid/kvform.rb
- lib/openid/association.rb
- lib/openid/store
- lib/openid/kvpost.rb
- lib/openid/extensions
- lib/openid/protocolerror.rb
- lib/openid/server.rb
- lib/openid/extension.rb
- lib/openid/consumer.rb
- lib/openid/yadis/htmltokenizer.rb
- lib/openid/yadis/parsehtml.rb
- lib/openid/yadis/filters.rb
- lib/openid/yadis/xrds.rb
- lib/openid/yadis/accept.rb
- lib/openid/yadis/constants.rb
- lib/openid/yadis/discovery.rb
- lib/openid/yadis/xri.rb
- lib/openid/yadis/xrires.rb
- lib/openid/yadis/services.rb
- lib/openid/consumer/html_parse.rb
- lib/openid/consumer/idres.rb
- lib/openid/consumer/associationmanager.rb
- lib/openid/consumer/discovery.rb
- lib/openid/consumer/discovery_manager.rb
- lib/openid/consumer/checkid_request.rb
- lib/openid/consumer/responses.rb
- lib/openid/store/filesystem.rb
- lib/openid/store/interface.rb
- lib/openid/store/nonce.rb
- lib/openid/store/memory.rb
- lib/openid/extensions/sreg.rb
- lib/openid/extensions/ax.rb
- lib/openid/extensions/pape.rb
- lib/hmac/hmac.rb
- lib/hmac/sha1.rb
- lib/hmac/sha2.rb
- test/data
- test/test_association.rb
- test/test_urinorm.rb
- test/testutil.rb
- test/test_util.rb
- test/test_message.rb
- test/test_cryptutil.rb
- test/test_extras.rb
- test/util.rb
- test/test_trustroot.rb
- test/test_parsehtml.rb
- test/test_fetchers.rb
- test/test_dh.rb
- test/test_kvform.rb
- test/test_openid_yadis.rb
- test/test_linkparse.rb
- test/test_stores.rb
- test/test_filters.rb
- test/test_xrds.rb
- test/test_nonce.rb
- test/test_accept.rb
- test/test_kvpost.rb
- test/test_associationmanager.rb
- test/discoverdata.rb
- test/test_server.rb
- test/test_yadis_discovery.rb
- test/test_sreg.rb
- test/test_idres.rb
- test/test_ax.rb
- test/test_xri.rb
- test/test_xrires.rb
- test/test_discover.rb
- test/test_consumer.rb
- test/test_pape.rb
- test/test_checkid_request.rb
- test/test_discovery_manager.rb
- test/test_responses.rb
- test/test_extension.rb
- test/data/test_xrds
- test/data/urinorm.txt
- test/data/n2b64
- test/data/trustroot.txt
- test/data/dh.txt
- test/data/test1-parsehtml.txt
- test/data/linkparse.txt
- test/data/accept.txt
- test/data/test_discover
- test/data/example-xrds.xml
- test/data/test1-discover.txt
- test/data/test_xrds/ref.xrds
- test/data/test_xrds/README
- test/data/test_xrds/delegated-20060809-r1.xrds
- test/data/test_xrds/delegated-20060809-r2.xrds
- test/data/test_xrds/delegated-20060809.xrds
- test/data/test_xrds/no-xrd.xml
- test/data/test_xrds/not-xrds.xml
- test/data/test_xrds/prefixsometimes.xrds
- test/data/test_xrds/sometimesprefix.xrds
- test/data/test_xrds/spoof1.xrds
- test/data/test_xrds/spoof2.xrds
- test/data/test_xrds/spoof3.xrds
- test/data/test_xrds/status222.xrds
- test/data/test_xrds/valid-populated-xrds.xml
- test/data/test_xrds/=j3h.2007.11.14.xrds
- test/data/test_xrds/subsegments.xrds
- test/data/test_discover/openid2_xrds.xml
- test/data/test_discover/openid.html
- test/data/test_discover/openid2.html
- test/data/test_discover/openid2_xrds_no_local_id.xml
- test/data/test_discover/openid_1_and_2.html
- test/data/test_discover/openid_1_and_2_xrds.xml
- test/data/test_discover/openid_and_yadis.html
- test/data/test_discover/openid_1_and_2_xrds_bad_delegate.xml
- test/data/test_discover/openid_no_delegate.html
- test/data/test_discover/yadis_0entries.xml
- test/data/test_discover/yadis_2_bad_local_id.xml
- test/data/test_discover/yadis_2entries_delegate.xml
- test/data/test_discover/yadis_2entries_idp.xml
- test/data/test_discover/yadis_another_delegate.xml
- test/data/test_discover/yadis_idp.xml
- test/data/test_discover/yadis_idp_delegate.xml
- test/data/test_discover/yadis_no_delegate.xml
- test/data/test_discover/malformed_meta_tag.html
- NOTICE
- CHANGELOG
- README
- INSTALL
- LICENSE
- UPGRADE
- admin/runtests.rb
has_rdoc: true
homepage: http://openidenabled.com/ruby-openid/
post_install_message:
rdoc_options:
- --main
- README
require_paths:
- lib
required_ruby_version: !ruby/object:Gem::Requirement
requirements:
- - ">"
- !ruby/object:Gem::Version
version: 0.0.0
version:
required_rubygems_version: !ruby/object:Gem::Requirement
requirements:
- - ">="
- !ruby/object:Gem::Version
version: "0"
version:
requirements: []
rubyforge_project:
rubygems_version: 1.3.1
signing_key:
specification_version: 1
summary: A library for consuming and serving OpenID identities.
test_files:
- admin/runtests.rb

11
vendor/gems/ruby-openid-2.1.4/CHANGELOG vendored Normal file
View File

@ -0,0 +1,11 @@
Fri Dec 19 11:50:10 PST 2008 cygnus@janrain.com
tagged 2.1.4
Fri Dec 19 11:48:25 PST 2008 cygnus@janrain.com
* Version: 2.1.4
Fri Dec 19 11:42:47 PST 2008 cygnus@janrain.com
* Normalize XRIs when doing discovery in accordance with the OpenID 2 spec
Tue Dec 16 13:14:07 PST 2008 cygnus@janrain.com
tagged 2.1.3

47
vendor/gems/ruby-openid-2.1.4/INSTALL vendored Normal file
View File

@ -0,0 +1,47 @@
= Ruby OpenID Library Installation
== Rubygems Installation
Rubygems is a tool for installing ruby libraries and their
dependancies. If you have rubygems installed, simply:
gem install ruby-openid
== Manual Installation
Unpack the archive and run setup.rb to install:
ruby setup.rb
setup.rb installs the library into your system ruby. If don't want to
add openid to you system ruby, you may instead add the *lib* directory of
the extracted tarball to your RUBYLIB environment variable:
$ export RUBYLIB=${RUBYLIB}:/path/to/ruby-openid/lib
== Testing the Installation
Make sure everything installed ok:
$> irb
irb$> require "openid"
=> true
Or, if you installed via rubygems:
$> irb
irb$> require "rubygems"
=> true
irb$> require_gem "ruby-openid"
=> true
== Run the test suite
Go into the test directory and execute the *runtests.rb* script.
== Next steps
* Run consumer.rb in the examples directory.
* Get started writing your own consumer using OpenID::Consumer
* Write your own server with OpenID::Server
* Use the OpenIDLoginGenerator! Read example/README for more info.

210
vendor/gems/ruby-openid-2.1.4/LICENSE vendored Normal file
View File

@ -0,0 +1,210 @@
The code in lib/hmac/ is Copyright 2001 by Daiki Ueno, and distributed under
the terms of the Ruby license. See http://www.ruby-lang.org/en/LICENSE.txt
lib/openid/yadis/htmltokenizer.rb is Copyright 2004 by Ben Giddings and
distributed under the terms of the Ruby license.
The remainder of this package is Copyright 2006-2008 by JanRain, Inc. and
distributed under the terms of license below:
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

2
vendor/gems/ruby-openid-2.1.4/NOTICE vendored Normal file
View File

@ -0,0 +1,2 @@
This product includes software developed by JanRain,
available from http://openidenabled.com/

82
vendor/gems/ruby-openid-2.1.4/README vendored Normal file
View File

@ -0,0 +1,82 @@
=Ruby OpenID
A Ruby library for verifying and serving OpenID identities.
==Features
* Easy to use API for verifying OpenID identites - OpenID::Consumer
* Support for serving OpenID identites - OpenID::Server
* Does not depend on underlying web framework
* Supports multiple storage mechanisms (Filesystem, ActiveRecord, Memory)
* Example code to help you get started, including:
* Ruby on Rails based consumer and server
* OpenIDLoginGenerator for quickly getting creating a rails app that uses
OpenID for authentication
* ActiveRecordOpenIDStore plugin
* Comprehensive test suite
* Supports both OpenID 1 and OpenID 2 transparently
==Installing
Before running the examples or writing your own code you'll need to install
the library. See the INSTALL file or use rubygems:
gem install ruby-openid
Check the installation:
$ irb
irb> require 'rubygems'
irb> require_gem 'ruby-openid'
=> true
The library is known to work with Ruby 1.8.4 on Unix, Max OSX and
Win32. Examples have been tested with Rails 1.1 and 1.2, and 2.0.
==Getting Started
The best way to start is to look at the rails_openid example.
You can run it with:
cd examples/rails_openid
script/server
If you are writing an OpenID Relying Party, a good place to start is:
examples/rails_openid/app/controllers/consumer_controller.rb
And if you are writing an OpenID provider:
examples/rails_openid/app/controllers/server_controller.rb
The library code is quite well documented, so don't be squeamish, and
look at the library itself if there's anything you don't understand in
the examples.
==Homepage
http://openidenabled.com/ruby-openid/
See also:
http://openid.net/
http://openidenabled.com/
==Community
Discussion regarding the Ruby OpenID library and other JanRain OpenID
libraries takes place on the the OpenID mailing list on
openidenabled.com.
http://lists.openidenabled.com/mailman/listinfo/dev
Please join this list to discuss, ask implementation questions, report
bugs, etc. Also check out the openid channel on the freenode IRC
network.
If you have a bugfix or feature you'd like to contribute, don't
hesitate to send it to us. For more detailed information on how to
contribute, see
http://openidenabled.com/contribute/
==Author
Copyright 2006-2008, JanRain, Inc.
Contact openid@janrain.com or visit the OpenID channel on pibb.com:
http://pibb.com/go/openid
==License
Apache Software License. For more information see the LICENSE file.

127
vendor/gems/ruby-openid-2.1.4/UPGRADE vendored Normal file
View File

@ -0,0 +1,127 @@
= Upgrading from the OpenID 1.x series library
== Consumer Upgrade
The flow is largely the same, however there are a number of significant
changes. The consumer example is helpful to look at:
examples/rails_openid/app/controllers/consumer_controller.rb
=== Stores
You will need to require the file for the store that you are using.
For the filesystem store, this is 'openid/stores/filesystem'
They are also now in modules. The filesystem store is
OpenID::Store::Filesystem
The format has changed, and you should remove your old store directory.
The ActiveRecord store ( examples/active_record_openid_store ) still needs
to be put in a plugin directory for your rails app. There's a migration
that needs to be run; examine the README in that directory.
Also, note that the stores now can be garbage collected with the method
store.cleanup
=== Starting the OpenID transaction
The OpenIDRequest object no longer has status codes. Instead,
consumer.begin raises an OpenID::OpenIDError if there is a problem
initiating the transaction, so you'll want something along the lines of:
begin
openid_request = consumer.begin(params[:openid_identifier])
rescue OpenID::OpenIDError => e
# display error e
return
end
#success case
Data regarding the OpenID server once lived in
openid_request.service
The corresponding object in the 2.0 lib can be retrieved with
openid_request.endpoint
Getting the unverified identifier: Where you once had
openid_request.identity_url
you will now want
openid_request.endpoint.claimed_id
which might be different from what you get at the end of the transaction,
since it is now possible for users to enter their server's url directly.
Arguments on the return_to URL are now verified, so if you want to add
additional arguments to the return_to url, use
openid_request.return_to_args['param'] = value
Generating the redirect is the same as before, but add any extensions
first.
If you need to set up an SSL certificate authority list for the fetcher,
use the 'ca_file' attr_accessor on the OpenID::StandardFetcher. This has
changed from 'ca_path' in the 1.x.x series library. That is, set
OpenID.fetcher.ca_file = '/path/to/ca.list'
before calling consumer.begin.
=== Requesting Simple Registration Data
You'll need to require the code for the extension
require 'openid/extensions/sreg'
The new code for adding an SReg request now looks like:
sreg_request = OpenID::SReg::Request.new
sreg_request.request_fields(['email', 'dob'], true) # required
sreg_request.request_fields(['nickname', 'fullname'], false) # optional
sreg_request.policy_url = policy_url
openid_request.add_extension(sreg_request)
The code for adding other extensions is similar. Code for the Attribute
Exchange (AX) and Provider Authentication Policy Extension (PAPE) are
included with the library, and additional extensions can be implemented
subclassing OpenID::Extension.
=== Completing the transaction
The return_to and its arguments are verified, so you need to pass in
the base URL and the arguments. With Rails, the params method mashes
together parameters from GET, POST, and the path, so you'll need to pull
off the path "parameters" with something like
return_to = url_for(:only_path => false,
:controller => 'openid',
:action => 'complete')
parameters = params.reject{|k,v| request.path_parameters[k] }
openid_response = consumer.complete(parameters, return_to)
The response still uses the status codes, but they are now namespaced
slightly differently, for example OpenID::Consumer::SUCCESS
In the case of failure, the error message is now found in
openid_response.message
The identifier to display to the user can be found in
openid_response.endpoint.display_identifier
The Simple Registration response can be read from the OpenID response
with
sreg_response = OpenID::SReg::Response.from_success_response(openid_response)
nickname = sreg_response['nickname']
# etc.
== Server Upgrade
The server code is mostly the same as before, with the exception of
extensions. Also, you must pass in the endpoint URL to the server
constructor:
@server = OpenID::Server.new(store, server_url)
I recommend looking at
examples/rails_openid/app/controllers/server_controller.rb
for an example of the new way of doing extensions.
--
Dag Arneson, JanRain Inc.
Please direct questions to openid@janrain.com

View File

@ -0,0 +1,36 @@
#!/usr/bin/ruby
require "logger"
require "stringio"
require "pathname"
require 'test/unit/collector/dir'
require 'test/unit/ui/console/testrunner'
def main
old_verbose = $VERBOSE
$VERBOSE = true
tests_dir = Pathname.new(__FILE__).dirname.dirname.join('test')
# Collect tests from everything named test_*.rb.
c = Test::Unit::Collector::Dir.new
if c.respond_to?(:base=)
# In order to supress warnings from ruby 1.8.6 about accessing
# undefined member
c.base = tests_dir
suite = c.collect
else
# Because base is not defined in ruby < 1.8.6
suite = c.collect(tests_dir)
end
result = Test::Unit::UI::Console::TestRunner.run(suite)
result.passed?
ensure
$VERBOSE = old_verbose
end
exit(main)

View File

@ -0,0 +1,32 @@
This directory contains several examples that demonstrate use of the
OpenID library. Make sure you have properly installed the library
before running the examples. These examples are a great place to
start in integrating OpenID into your application.
==Rails example
The rails_openid contains a fully functional OpenID server and relying
party, and acts as a starting point for implementing your own
production rails server. You'll need the latest version of Ruby on
Rails installed, and then:
cd rails_openid
./script/server
Open a web browser to http://localhost:3000/ and follow the instructions.
The relevant code to work from when writing your Rails OpenID Relying
Party is:
rails_openid/app/controllers/consumer_controller.rb
If you are working on an OpenID provider, check out
rails_openid/app/controllers/server_controller.rb
Since the library and examples are Apache-licensed, don't be shy about
copy-and-paste.
==Rails ActiveRecord OpenIDStore plugin
For various reasons you may want or need to deploy your ruby openid
consumer/server using an SQL based store. The active_record_openid_store
is a plugin that makes using an SQL based store simple. Follow the
README inside the plugin's dir for usage.

View File

@ -0,0 +1,58 @@
=Active Record OpenID Store Plugin
A store is required by an OpenID server and optionally by the consumer
to store associations, nonces, and auth key information across
requests and processes. If rails is distributed across several
machines, they must must all have access to the same OpenID store
data, so the FilesystemStore won't do.
This directory contains a plugin for connecting your
OpenID enabled rails app to an ActiveRecord based OpenID store.
==Install
1) Copy this directory and all it's contents into your
RAILS_ROOT/vendor/plugins directory. You structure should look like
this:
RAILS_ROOT/vendor/plugins/active_record_openid_store/
2) Copy the migration, XXX_add_open_id_store_to_db.rb to your
RAILS_ROOT/db/migrate directory. Rename the XXX portion of the
file to next sequential migration number.
3) Run the migration:
rake migrate
4) Change your app to use the ActiveRecordOpenIDStore:
store = ActiveRecordOpenIDStore.new
consumer = OpenID::Consumer.new(session, store)
5) That's it! All your OpenID state will now be stored in the database.
==Upgrade
If you are upgrading from the 1.x ActiveRecord store, replace your old
RAILS_ROOT/vendor/plugins/active_record_openid_store/ directory with
the new one and run the migration XXX_upgrade_open_id_store.rb.
==What about garbage collection?
You may garbage collect unused nonces and expired associations using
the gc instance method of ActiveRecordOpenIDStore. Hook it up to a
task in your app's Rakefile like so:
desc 'GC OpenID store'
task :gc_openid_store => :environment do
ActiveRecordOpenIDStore.new.cleanup
end
Run it by typing:
rake gc_openid_store
==Questions?
Contact Dag Arneson: dag at janrain dot com

View File

@ -0,0 +1,24 @@
# Use this migration to create the tables for the ActiveRecord store
class AddOpenIdStoreToDb < ActiveRecord::Migration
def self.up
create_table "open_id_associations", :force => true do |t|
t.column "server_url", :binary, :null => false
t.column "handle", :string, :null => false
t.column "secret", :binary, :null => false
t.column "issued", :integer, :null => false
t.column "lifetime", :integer, :null => false
t.column "assoc_type", :string, :null => false
end
create_table "open_id_nonces", :force => true do |t|
t.column :server_url, :string, :null => false
t.column :timestamp, :integer, :null => false
t.column :salt, :string, :null => false
end
end
def self.down
drop_table "open_id_associations"
drop_table "open_id_nonces"
end
end

View File

@ -0,0 +1,26 @@
# Use this migration to upgrade the old 1.1 ActiveRecord store schema
# to the new 2.0 schema.
class UpgradeOpenIdStore < ActiveRecord::Migration
def self.up
drop_table "open_id_settings"
drop_table "open_id_nonces"
create_table "open_id_nonces", :force => true do |t|
t.column :server_url, :string, :null => false
t.column :timestamp, :integer, :null => false
t.column :salt, :string, :null => false
end
end
def self.down
drop_table "open_id_nonces"
create_table "open_id_nonces", :force => true do |t|
t.column "nonce", :string
t.column "created", :integer
end
create_table "open_id_settings", :force => true do |t|
t.column "setting", :string
t.column "value", :binary
end
end
end

View File

@ -0,0 +1,8 @@
# might using the ruby-openid gem
begin
require 'rubygems'
rescue LoadError
nil
end
require 'openid'
require 'openid_ar_store'

View File

@ -0,0 +1,10 @@
require 'openid/association'
require 'time'
class Association < ActiveRecord::Base
set_table_name 'open_id_associations'
def from_record
OpenID::Association.new(handle, secret, Time.at(issued), lifetime, assoc_type)
end
end

View File

@ -0,0 +1,3 @@
class Nonce < ActiveRecord::Base
set_table_name 'open_id_nonces'
end

View File

@ -0,0 +1,4 @@
class OpenIdSetting < ActiveRecord::Base
validates_uniqueness_of :setting
end

View File

@ -0,0 +1,57 @@
require 'association'
require 'nonce'
require 'openid/store/interface'
# not in OpenID module to avoid namespace conflict
class ActiveRecordStore < OpenID::Store::Interface
def store_association(server_url, assoc)
remove_association(server_url, assoc.handle)
Association.create!(:server_url => server_url,
:handle => assoc.handle,
:secret => assoc.secret,
:issued => assoc.issued.to_i,
:lifetime => assoc.lifetime,
:assoc_type => assoc.assoc_type)
end
def get_association(server_url, handle=nil)
assocs = if handle.blank?
Association.find_all_by_server_url(server_url)
else
Association.find_all_by_server_url_and_handle(server_url, handle)
end
assocs.reverse.each do |assoc|
a = assoc.from_record
if a.expires_in == 0
assoc.destroy
else
return a
end
end if assocs.any?
return nil
end
def remove_association(server_url, handle)
Association.delete_all(['server_url = ? AND handle = ?', server_url, handle]) > 0
end
def use_nonce(server_url, timestamp, salt)
return false if Nonce.find_by_server_url_and_timestamp_and_salt(server_url, timestamp, salt)
return false if (timestamp - Time.now.to_i).abs > OpenID::Nonce.skew
Nonce.create!(:server_url => server_url, :timestamp => timestamp, :salt => salt)
return true
end
def cleanup_nonces
now = Time.now.to_i
Nonce.delete_all(["timestamp > ? OR timestamp < ?", now + OpenID::Nonce.skew, now - OpenID::Nonce.skew])
end
def cleanup_associations
now = Time.now.to_i
Association.delete_all(['issued + lifetime > ?',now])
end
end

View File

@ -0,0 +1,212 @@
$:.unshift(File.dirname(__FILE__) + '/../lib')
require 'test/unit'
RAILS_ENV = "test"
require File.expand_path(File.join(File.dirname(__FILE__), '../../../../config/environment.rb'))
module StoreTestCase
@@allowed_handle = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~'
@@allowed_nonce = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
def _gen_nonce
OpenID::CryptUtil.random_string(8, @@allowed_nonce)
end
def _gen_handle(n)
OpenID::CryptUtil.random_string(n, @@allowed_handle)
end
def _gen_secret(n, chars=nil)
OpenID::CryptUtil.random_string(n, chars)
end
def _gen_assoc(issued, lifetime=600)
secret = _gen_secret(20)
handle = _gen_handle(128)
OpenID::Association.new(handle, secret, Time.now + issued, lifetime,
'HMAC-SHA1')
end
def _check_retrieve(url, handle=nil, expected=nil)
ret_assoc = @store.get_association(url, handle)
if expected.nil?
assert_nil(ret_assoc)
else
assert_equal(expected, ret_assoc)
assert_equal(expected.handle, ret_assoc.handle)
assert_equal(expected.secret, ret_assoc.secret)
end
end
def _check_remove(url, handle, expected)
present = @store.remove_association(url, handle)
assert_equal(expected, present)
end
def test_store
server_url = "http://www.myopenid.com/openid"
assoc = _gen_assoc(issued=0)
# Make sure that a missing association returns no result
_check_retrieve(server_url)
# Check that after storage, getting returns the same result
@store.store_association(server_url, assoc)
_check_retrieve(server_url, nil, assoc)
# more than once
_check_retrieve(server_url, nil, assoc)
# Storing more than once has no ill effect
@store.store_association(server_url, assoc)
_check_retrieve(server_url, nil, assoc)
# Removing an association that does not exist returns not present
_check_remove(server_url, assoc.handle + 'x', false)
# Removing an association that does not exist returns not present
_check_remove(server_url + 'x', assoc.handle, false)
# Removing an association that is present returns present
_check_remove(server_url, assoc.handle, true)
# but not present on subsequent calls
_check_remove(server_url, assoc.handle, false)
# Put assoc back in the store
@store.store_association(server_url, assoc)
# More recent and expires after assoc
assoc2 = _gen_assoc(issued=1)
@store.store_association(server_url, assoc2)
# After storing an association with a different handle, but the
# same server_url, the handle with the later expiration is returned.
_check_retrieve(server_url, nil, assoc2)
# We can still retrieve the older association
_check_retrieve(server_url, assoc.handle, assoc)
# Plus we can retrieve the association with the later expiration
# explicitly
_check_retrieve(server_url, assoc2.handle, assoc2)
# More recent, and expires earlier than assoc2 or assoc. Make sure
# that we're picking the one with the latest issued date and not
# taking into account the expiration.
assoc3 = _gen_assoc(issued=2, lifetime=100)
@store.store_association(server_url, assoc3)
_check_retrieve(server_url, nil, assoc3)
_check_retrieve(server_url, assoc.handle, assoc)
_check_retrieve(server_url, assoc2.handle, assoc2)
_check_retrieve(server_url, assoc3.handle, assoc3)
_check_remove(server_url, assoc2.handle, true)
_check_retrieve(server_url, nil, assoc3)
_check_retrieve(server_url, assoc.handle, assoc)
_check_retrieve(server_url, assoc2.handle, nil)
_check_retrieve(server_url, assoc3.handle, assoc3)
_check_remove(server_url, assoc2.handle, false)
_check_remove(server_url, assoc3.handle, true)
_check_retrieve(server_url, nil, assoc)
_check_retrieve(server_url, assoc.handle, assoc)
_check_retrieve(server_url, assoc2.handle, nil)
_check_retrieve(server_url, assoc3.handle, nil)
_check_remove(server_url, assoc2.handle, false)
_check_remove(server_url, assoc.handle, true)
_check_remove(server_url, assoc3.handle, false)
_check_retrieve(server_url, nil, nil)
_check_retrieve(server_url, assoc.handle, nil)
_check_retrieve(server_url, assoc2.handle, nil)
_check_retrieve(server_url, assoc3.handle, nil)
_check_remove(server_url, assoc2.handle, false)
_check_remove(server_url, assoc.handle, false)
_check_remove(server_url, assoc3.handle, false)
assocValid1 = _gen_assoc(-3600, 7200)
assocValid2 = _gen_assoc(-5)
assocExpired1 = _gen_assoc(-7200, 3600)
assocExpired2 = _gen_assoc(-7200, 3600)
@store.cleanup_associations
@store.store_association(server_url + '1', assocValid1)
@store.store_association(server_url + '1', assocExpired1)
@store.store_association(server_url + '2', assocExpired2)
@store.store_association(server_url + '3', assocValid2)
cleaned = @store.cleanup_associations()
assert_equal(2, cleaned, "cleaned up associations")
end
def _check_use_nonce(nonce, expected, server_url, msg='')
stamp, salt = OpenID::Nonce::split_nonce(nonce)
actual = @store.use_nonce(server_url, stamp, salt)
assert_equal(expected, actual, msg)
end
def test_nonce
server_url = "http://www.myopenid.com/openid"
[server_url, ''].each{|url|
nonce1 = OpenID::Nonce::mk_nonce
_check_use_nonce(nonce1, true, url, "#{url}: nonce allowed by default")
_check_use_nonce(nonce1, false, url, "#{url}: nonce not allowed twice")
_check_use_nonce(nonce1, false, url, "#{url}: nonce not allowed third time")
# old nonces shouldn't pass
old_nonce = OpenID::Nonce::mk_nonce(3600)
_check_use_nonce(old_nonce, false, url, "Old nonce #{old_nonce.inspect} passed")
}
now = Time.now.to_i
old_nonce1 = OpenID::Nonce::mk_nonce(now - 20000)
old_nonce2 = OpenID::Nonce::mk_nonce(now - 10000)
recent_nonce = OpenID::Nonce::mk_nonce(now - 600)
orig_skew = OpenID::Nonce.skew
OpenID::Nonce.skew = 0
count = @store.cleanup_nonces
OpenID::Nonce.skew = 1000000
ts, salt = OpenID::Nonce::split_nonce(old_nonce1)
assert(@store.use_nonce(server_url, ts, salt), "oldnonce1")
ts, salt = OpenID::Nonce::split_nonce(old_nonce2)
assert(@store.use_nonce(server_url, ts, salt), "oldnonce2")
ts, salt = OpenID::Nonce::split_nonce(recent_nonce)
assert(@store.use_nonce(server_url, ts, salt), "recent_nonce")
OpenID::Nonce.skew = 1000
cleaned = @store.cleanup_nonces
assert_equal(2, cleaned, "Cleaned #{cleaned} nonces")
OpenID::Nonce.skew = 100000
ts, salt = OpenID::Nonce::split_nonce(old_nonce1)
assert(@store.use_nonce(server_url, ts, salt), "oldnonce1 after cleanup")
ts, salt = OpenID::Nonce::split_nonce(old_nonce2)
assert(@store.use_nonce(server_url, ts, salt), "oldnonce2 after cleanup")
ts, salt = OpenID::Nonce::split_nonce(recent_nonce)
assert(!@store.use_nonce(server_url, ts, salt), "recent_nonce after cleanup")
OpenID::Nonce.skew = orig_skew
end
end
class TestARStore < Test::Unit::TestCase
include StoreTestCase
def setup
@store = ActiveRecordStore.new
end
end

View File

@ -0,0 +1,49 @@
#!/usr/bin/env ruby
require "openid/consumer/discovery"
require 'openid/fetchers'
OpenID::fetcher_use_env_http_proxy
$names = [[:server_url, "Server URL "],
[:local_id, "Local ID "],
[:canonical_id, "Canonical ID"],
]
def show_services(user_input, normalized, services)
puts " Claimed identifier: #{normalized}"
if services.empty?
puts " No OpenID services found"
puts
else
puts " Discovered services:"
n = 0
services.each do |service|
n += 1
puts " #{n}."
$names.each do |meth, name|
val = service.send(meth)
if val
printf(" %s: %s\n", name, val)
end
end
puts " Type URIs:"
for type_uri in service.type_uris
puts " * #{type_uri}"
end
puts
end
end
end
ARGV.each do |openid_identifier|
puts "=" * 50
puts "Running discovery on #{openid_identifier}"
begin
normalized_identifier, services = OpenID.discover(openid_identifier)
rescue OpenID::DiscoveryFailure => why
puts "Discovery failed: #{why.message}"
puts
else
show_services(openid_identifier, normalized_identifier, services)
end
end

View File

@ -0,0 +1,153 @@
== Welcome to Rails
Rails is a web-application and persistence framework that includes everything
needed to create database-backed web-applications according to the
Model-View-Control pattern of separation. This pattern splits the view (also
called the presentation) into "dumb" templates that are primarily responsible
for inserting pre-built data in between HTML tags. The model contains the
"smart" domain objects (such as Account, Product, Person, Post) that holds all
the business logic and knows how to persist themselves to a database. The
controller handles the incoming requests (such as Save New Account, Update
Product, Show Post) by manipulating the model and directing data to the view.
In Rails, the model is handled by what's called an object-relational mapping
layer entitled Active Record. This layer allows you to present the data from
database rows as objects and embellish these data objects with business logic
methods. You can read more about Active Record in
link:files/vendor/rails/activerecord/README.html.
The controller and view are handled by the Action Pack, which handles both
layers by its two parts: Action View and Action Controller. These two layers
are bundled in a single package due to their heavy interdependence. This is
unlike the relationship between the Active Record and Action Pack that is much
more separate. Each of these packages can be used independently outside of
Rails. You can read more about Action Pack in
link:files/vendor/rails/actionpack/README.html.
== Getting started
1. Run the WEBrick servlet: <tt>ruby script/server</tt> (run with --help for options)
...or if you have lighttpd installed: <tt>ruby script/lighttpd</tt> (it's faster)
2. Go to http://localhost:3000/ and get "Congratulations, you've put Ruby on Rails!"
3. Follow the guidelines on the "Congratulations, you've put Ruby on Rails!" screen
== Example for Apache conf
<VirtualHost *:80>
ServerName rails
DocumentRoot /path/application/public/
ErrorLog /path/application/log/server.log
<Directory /path/application/public/>
Options ExecCGI FollowSymLinks
AllowOverride all
Allow from all
Order allow,deny
</Directory>
</VirtualHost>
NOTE: Be sure that CGIs can be executed in that directory as well. So ExecCGI
should be on and ".cgi" should respond. All requests from 127.0.0.1 go
through CGI, so no Apache restart is necessary for changes. All other requests
go through FCGI (or mod_ruby), which requires a restart to show changes.
== Debugging Rails
Have "tail -f" commands running on both the server.log, production.log, and
test.log files. Rails will automatically display debugging and runtime
information to these files. Debugging info will also be shown in the browser
on requests from 127.0.0.1.
== Breakpoints
Breakpoint support is available through the script/breakpointer client. This
means that you can break out of execution at any point in the code, investigate
and change the model, AND then resume execution! Example:
class WeblogController < ActionController::Base
def index
@posts = Post.find_all
breakpoint "Breaking out from the list"
end
end
So the controller will accept the action, run the first line, then present you
with a IRB prompt in the breakpointer window. Here you can do things like:
Executing breakpoint "Breaking out from the list" at .../webrick_server.rb:16 in 'breakpoint'
>> @posts.inspect
=> "[#<Post:0x14a6be8 @attributes={\"title\"=>nil, \"body\"=>nil, \"id\"=>\"1\"}>,
#<Post:0x14a6620 @attributes={\"title\"=>\"Rails you know!\", \"body\"=>\"Only ten..\", \"id\"=>\"2\"}>]"
>> @posts.first.title = "hello from a breakpoint"
=> "hello from a breakpoint"
...and even better is that you can examine how your runtime objects actually work:
>> f = @posts.first
=> #<Post:0x13630c4 @attributes={"title"=>nil, "body"=>nil, "id"=>"1"}>
>> f.
Display all 152 possibilities? (y or n)
Finally, when you're ready to resume execution, you press CTRL-D
== Console
You can interact with the domain model by starting the console through script/console.
Here you'll have all parts of the application configured, just like it is when the
application is running. You can inspect domain models, change values, and save to the
database. Starting the script without arguments will launch it in the development environment.
Passing an argument will specify a different environment, like <tt>console production</tt>.
== Description of contents
app
Holds all the code that's specific to this particular application.
app/controllers
Holds controllers that should be named like weblog_controller.rb for
automated URL mapping. All controllers should descend from
ActionController::Base.
app/models
Holds models that should be named like post.rb.
Most models will descend from ActiveRecord::Base.
app/views
Holds the template files for the view that should be named like
weblog/index.rhtml for the WeblogController#index action. All views use eRuby
syntax. This directory can also be used to keep stylesheets, images, and so on
that can be symlinked to public.
app/helpers
Holds view helpers that should be named like weblog_helper.rb.
config
Configuration files for the Rails environment, the routing map, the database, and other dependencies.
components
Self-contained mini-applications that can bundle together controllers, models, and views.
lib
Application specific libraries. Basically, any kind of custom code that doesn't
belong under controllers, models, or helpers. This directory is in the load path.
public
The directory available for the web server. Contains subdirectories for images, stylesheets,
and javascripts. Also contains the dispatchers and the default HTML files.
script
Helper scripts for automation and generation.
test
Unit and functional tests along with fixtures.
vendor
External libraries that the application depends on. Also includes the plugins subdirectory.
This directory is in the load path.

View File

@ -0,0 +1,10 @@
# Add your own tasks in files placed in lib/tasks ending in .rake,
# for example lib/tasks/switchtower.rake, and they will automatically be available to Rake.
require(File.join(File.dirname(__FILE__), 'config', 'boot'))
require 'rake'
require 'rake/testtask'
require 'rake/rdoctask'
require 'tasks/rails'

View File

@ -0,0 +1,4 @@
# Filters added to this controller will be run for all controllers in the application.
# Likewise, all the methods added will be available for all controllers.
class ApplicationController < ActionController::Base
end

View File

@ -0,0 +1,122 @@
require 'pathname'
require "openid"
require 'openid/extensions/sreg'
require 'openid/extensions/pape'
require 'openid/store/filesystem'
class ConsumerController < ApplicationController
layout nil
def index
# render an openid form
end
def start
begin
identifier = params[:openid_identifier]
if identifier.nil?
flash[:error] = "Enter an OpenID identifier"
redirect_to :action => 'index'
return
end
oidreq = consumer.begin(identifier)
rescue OpenID::OpenIDError => e
flash[:error] = "Discovery failed for #{identifier}: #{e}"
redirect_to :action => 'index'
return
end
if params[:use_sreg]
sregreq = OpenID::SReg::Request.new
# required fields
sregreq.request_fields(['email','nickname'], true)
# optional fields
sregreq.request_fields(['dob', 'fullname'], false)
oidreq.add_extension(sregreq)
oidreq.return_to_args['did_sreg'] = 'y'
end
if params[:use_pape]
papereq = OpenID::PAPE::Request.new
papereq.add_policy_uri(OpenID::PAPE::AUTH_PHISHING_RESISTANT)
papereq.max_auth_age = 2*60*60
oidreq.add_extension(papereq)
oidreq.return_to_args['did_pape'] = 'y'
end
if params[:force_post]
oidreq.return_to_args['force_post']='x'*2048
end
return_to = url_for :action => 'complete', :only_path => false
realm = url_for :action => 'index', :only_path => false
if oidreq.send_redirect?(realm, return_to, params[:immediate])
redirect_to oidreq.redirect_url(realm, return_to, params[:immediate])
else
render :text => oidreq.html_markup(realm, return_to, params[:immediate], {'id' => 'openid_form'})
end
end
def complete
# FIXME - url_for some action is not necessarily the current URL.
current_url = url_for(:action => 'complete', :only_path => false)
parameters = params.reject{|k,v|request.path_parameters[k]}
oidresp = consumer.complete(parameters, current_url)
case oidresp.status
when OpenID::Consumer::FAILURE
if oidresp.display_identifier
flash[:error] = ("Verification of #{oidresp.display_identifier}"\
" failed: #{oidresp.message}")
else
flash[:error] = "Verification failed: #{oidresp.message}"
end
when OpenID::Consumer::SUCCESS
flash[:success] = ("Verification of #{oidresp.display_identifier}"\
" succeeded.")
if params[:did_sreg]
sreg_resp = OpenID::SReg::Response.from_success_response(oidresp)
sreg_message = "Simple Registration data was requested"
if sreg_resp.empty?
sreg_message << ", but none was returned."
else
sreg_message << ". The following data were sent:"
sreg_resp.data.each {|k,v|
sreg_message << "<br/><b>#{k}</b>: #{v}"
}
end
flash[:sreg_results] = sreg_message
end
if params[:did_pape]
pape_resp = OpenID::PAPE::Response.from_success_response(oidresp)
pape_message = "A phishing resistant authentication method was requested"
if pape_resp.auth_policies.member? OpenID::PAPE::AUTH_PHISHING_RESISTANT
pape_message << ", and the server reported one."
else
pape_message << ", but the server did not report one."
end
if pape_resp.auth_time
pape_message << "<br><b>Authentication time:</b> #{pape_resp.auth_time} seconds"
end
if pape_resp.nist_auth_level
pape_message << "<br><b>NIST Auth Level:</b> #{pape_resp.nist_auth_level}"
end
flash[:pape_results] = pape_message
end
when OpenID::Consumer::SETUP_NEEDED
flash[:alert] = "Immediate request failed - Setup Needed"
when OpenID::Consumer::CANCEL
flash[:alert] = "OpenID transaction cancelled."
else
end
redirect_to :action => 'index'
end
private
def consumer
if @consumer.nil?
dir = Pathname.new(RAILS_ROOT).join('db').join('cstore')
store = OpenID::Store::Filesystem.new(dir)
@consumer = OpenID::Consumer.new(session, store)
end
return @consumer
end
end

View File

@ -0,0 +1,45 @@
# Controller for handling the login, logout process for "users" of our
# little server. Users have no password. This is just an example.
require 'openid'
class LoginController < ApplicationController
layout 'server'
def base_url
url_for(:controller => 'login', :action => nil, :only_path => false)
end
def index
response.headers['X-XRDS-Location'] = url_for(:controller => "server",
:action => "idp_xrds",
:only_path => false)
@base_url = base_url
# just show the login page
end
def submit
user = params[:username]
# if we get a user, log them in by putting their username in
# the session hash.
unless user.nil?
session[:username] = user unless user.nil?
session[:approvals] = []
flash[:notice] = "Your OpenID URL is <b>#{base_url}user/#{user}</b><br/><br/>Proceed to step 2 below."
else
flash[:error] = "Sorry, couldn't log you in. Try again."
end
redirect_to :action => 'index'
end
def logout
# delete the username from the session hash
session[:username] = nil
session[:approvals] = nil
redirect_to :action => 'index'
end
end

View File

@ -0,0 +1,265 @@
require 'pathname'
# load the openid library, first trying rubygems
#begin
# require "rubygems"
# require_gem "ruby-openid", ">= 1.0"
#rescue LoadError
require "openid"
require "openid/consumer/discovery"
require 'openid/extensions/sreg'
require 'openid/extensions/pape'
require 'openid/store/filesystem'
#end
class ServerController < ApplicationController
include ServerHelper
include OpenID::Server
layout nil
def index
begin
oidreq = server.decode_request(params)
rescue ProtocolError => e
# invalid openid request, so just display a page with an error message
render :text => e.to_s, :status => 500
return
end
# no openid.mode was given
unless oidreq
render :text => "This is an OpenID server endpoint."
return
end
oidresp = nil
if oidreq.kind_of?(CheckIDRequest)
identity = oidreq.identity
if oidreq.id_select
if oidreq.immediate
oidresp = oidreq.answer(false)
elsif session[:username].nil?
# The user hasn't logged in.
show_decision_page(oidreq)
return
else
# Else, set the identity to the one the user is using.
identity = url_for_user
end
end
if oidresp
nil
elsif self.is_authorized(identity, oidreq.trust_root)
oidresp = oidreq.answer(true, nil, identity)
# add the sreg response if requested
add_sreg(oidreq, oidresp)
# ditto pape
add_pape(oidreq, oidresp)
elsif oidreq.immediate
server_url = url_for :action => 'index'
oidresp = oidreq.answer(false, server_url)
else
show_decision_page(oidreq)
return
end
else
oidresp = server.handle_request(oidreq)
end
self.render_response(oidresp)
end
def show_decision_page(oidreq, message="Do you trust this site with your identity?")
session[:last_oidreq] = oidreq
@oidreq = oidreq
if message
flash[:notice] = message
end
render :template => 'server/decide', :layout => 'server'
end
def user_page
# Yadis content-negotiation: we want to return the xrds if asked for.
accept = request.env['HTTP_ACCEPT']
# This is not technically correct, and should eventually be updated
# to do real Accept header parsing and logic. Though I expect it will work
# 99% of the time.
if accept and accept.include?('application/xrds+xml')
user_xrds
return
end
# content negotiation failed, so just render the user page
xrds_url = url_for(:controller=>'user',:action=>params[:username])+'/xrds'
identity_page = <<EOS
<html><head>
<meta http-equiv="X-XRDS-Location" content="#{xrds_url}" />
<link rel="openid.server" href="#{url_for :action => 'index'}" />
</head><body><p>OpenID identity page for #{params[:username]}</p>
</body></html>
EOS
# Also add the Yadis location header, so that they don't have
# to parse the html unless absolutely necessary.
response.headers['X-XRDS-Location'] = xrds_url
render :text => identity_page
end
def user_xrds
types = [
OpenID::OPENID_2_0_TYPE,
OpenID::OPENID_1_0_TYPE,
OpenID::SREG_URI,
]
render_xrds(types)
end
def idp_xrds
types = [
OpenID::OPENID_IDP_2_0_TYPE,
]
render_xrds(types)
end
def decision
oidreq = session[:last_oidreq]
session[:last_oidreq] = nil
if params[:yes].nil?
redirect_to oidreq.cancel_url
return
else
id_to_send = params[:id_to_send]
identity = oidreq.identity
if oidreq.id_select
if id_to_send and id_to_send != ""
session[:username] = id_to_send
session[:approvals] = []
identity = url_for_user
else
msg = "You must enter a username to in order to send " +
"an identifier to the Relying Party."
show_decision_page(oidreq, msg)
return
end
end
if session[:approvals]
session[:approvals] << oidreq.trust_root
else
session[:approvals] = [oidreq.trust_root]
end
oidresp = oidreq.answer(true, nil, identity)
add_sreg(oidreq, oidresp)
add_pape(oidreq, oidresp)
return self.render_response(oidresp)
end
end
protected
def server
if @server.nil?
server_url = url_for :action => 'index', :only_path => false
dir = Pathname.new(RAILS_ROOT).join('db').join('openid-store')
store = OpenID::Store::Filesystem.new(dir)
@server = Server.new(store, server_url)
end
return @server
end
def approved(trust_root)
return false if session[:approvals].nil?
return session[:approvals].member?(trust_root)
end
def is_authorized(identity_url, trust_root)
return (session[:username] and (identity_url == url_for_user) and self.approved(trust_root))
end
def render_xrds(types)
type_str = ""
types.each { |uri|
type_str += "<Type>#{uri}</Type>\n "
}
yadis = <<EOS
<?xml version="1.0" encoding="UTF-8"?>
<xrds:XRDS
xmlns:xrds="xri://$xrds"
xmlns="xri://$xrd*($v*2.0)">
<XRD>
<Service priority="0">
#{type_str}
<URI>#{url_for(:controller => 'server', :only_path => false)}</URI>
</Service>
</XRD>
</xrds:XRDS>
EOS
response.headers['content-type'] = 'application/xrds+xml'
render :text => yadis
end
def add_sreg(oidreq, oidresp)
# check for Simple Registration arguments and respond
sregreq = OpenID::SReg::Request.from_openid_request(oidreq)
return if sregreq.nil?
# In a real application, this data would be user-specific,
# and the user should be asked for permission to release
# it.
sreg_data = {
'nickname' => session[:username],
'fullname' => 'Mayor McCheese',
'email' => 'mayor@example.com'
}
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)
return if papereq.nil?
paperesp = OpenID::PAPE::Response.new
paperesp.nist_auth_level = 0 # we don't even do auth at all!
oidresp.add_extension(paperesp)
end
def render_response(oidresp)
if oidresp.needs_signing
signed_response = server.signatory.sign(oidresp)
end
web_response = server.encode_response(oidresp)
case web_response.code
when HTTP_OK
render :text => web_response.body, :status => 200
when HTTP_REDIRECT
redirect_to web_response.headers['location']
else
render :text => web_response.body, :status => 400
end
end
end

View File

@ -0,0 +1,3 @@
# Methods added to this helper will be available to all templates in the application.
module ApplicationHelper
end

View File

@ -0,0 +1,2 @@
module LoginHelper
end

View File

@ -0,0 +1,9 @@
module ServerHelper
def url_for_user
url_for :controller => 'user', :action => session[:username]
end
end

View File

@ -0,0 +1,81 @@
<html>
<head>
<title>Rails OpenID Example Relying Party</title>
</head>
<style type="text/css">
* {
font-family: verdana,sans-serif;
}
body {
width: 50em;
margin: 1em;
}
div {
padding: .5em;
}
.alert {
border: 1px solid #e7dc2b;
background: #fff888;
}
.error {
border: 1px solid #ff0000;
background: #ffaaaa;
}
.success {
border: 1px solid #00ff00;
background: #aaffaa;
}
#verify-form {
border: 1px solid #777777;
background: #dddddd;
margin-top: 1em;
padding-bottom: 0em;
}
input.openid {
background: url( /images/openid_login_bg.gif ) no-repeat;
background-position: 0 50%;
background-color: #fff;
padding-left: 18px;
}
</style>
<body>
<h1>Rails OpenID Example Relying Party</h1>
<% if flash[:alert] %>
<div class='alert'>
<%= h(flash[:alert]) %>
</div>
<% end %>
<% if flash[:error] %>
<div class='error'>
<%= h(flash[:error]) %>
</div>
<% end %>
<% if flash[:success] %>
<div class='success'>
<%= h(flash[:success]) %>
</div>
<% end %>
<% if flash[:sreg_results] %>
<div class='alert'>
<%= flash[:sreg_results] %>
</div>
<% end %>
<% if flash[:pape_results] %>
<div class='alert'>
<%= flash[:pape_results] %>
</div>
<% end %>
<div id="verify-form">
<form method="get" accept-charset="UTF-8"
action='<%= url_for :action => 'start' %>'>
Identifier:
<input type="text" class="openid" name="openid_identifier" />
<input type="submit" value="Verify" /><br />
<input type="checkbox" name="immediate" id="immediate" /><label for="immediate">Use immediate mode</label><br/>
<input type="checkbox" name="use_sreg" id="use_sreg" /><label for="use_sreg">Request registration data</label><br/>
<input type="checkbox" name="use_pape" id="use_pape" /><label for="use_pape">Request phishing-resistent auth policy (PAPE)</label><br/>
<input type="checkbox" name="force_post" id="force_post" /><label for="force_post">Force the transaction to use POST by adding 2K of extra data</label>
</form>
</div>
</body>
</html>

View File

@ -0,0 +1,68 @@
<html>
<head><title>OpenID Server Example</title></head>
<style type="text/css">
* {
font-family: verdana,sans-serif;
}
body {
width: 50em;
margin: 1em;
}
div {
padding: .5em;
}
table {
margin: none;
padding: none;
}
.notice {
border: 1px solid #60964f;
background: #b3dca7;
}
.error {
border: 1px solid #ff0000;
background: #ffaaaa;
}
#login-form {
border: 1px solid #777777;
background: #dddddd;
margin-top: 1em;
padding-bottom: 0em;
}
table {
padding: 1em;
}
li {margin-bottom: .5em;}
span.openid:before {
content: url(<%= @base_url %>images/openid_login_bg.gif) ;
}
span.openid {
font-size: smaller;
}
</style>
<body>
<% if session[:username] %>
<div style="float:right;">
Welcome, <%= session[:username] %> | <%= link_to('Log out', :controller => 'login', :action => 'logout') %><br />
<span class="openid"><%= @base_url %>user/<%= session[:username] %></span>
</div>
<% end %>
<h3>Ruby OpenID Server Example</h3>
<hr/>
<% if flash[:notice] or flash[:error] %>
<div class="<%= flash[:notice].nil? ? 'error' : 'notice' %>">
<%= flash[:error] or flash[:notice] %>
</div>
<% end %>
<%= @content_for_layout %>
</body>
</html>

View File

@ -0,0 +1,56 @@
<% if session[:username].nil? %>
<div id="login-form">
<form method="get" action="<%= url_for :controller => 'login', :action => 'submit' %>">
Type a username:
<input type="text" name="username" />
<input type="submit" value="Log In" />
</form>
</div>
<% end %>
<p> Welcome to the Ruby OpenID example. This code is a starting point
for developers wishing to implement an OpenID provider or relying
party. We've used the <a href="http://rubyonrails.org/">Rails</a>
platform to demonstrate, but the library code is not Rails specific.</p>
<h2>To use the example provider</h2>
<p>
<ol>
<li>Enter a username in the form above. You will be "Logged In"
to the server, at which point you may authenticate using an OpenID
consumer. Your OpenID URL will be displayed after you log
in.<p>The server will automatically create an identity page for
you at <%= @base_url %>user/<i>name</i></p></li>
<li><p>Because WEBrick can only handle one thing at a time, you'll need to
run another instance of the example on another port if you want to use
a relying party to use with this example provider:</p>
<blockquote>
<code>script/server --port=3001</code>
</blockquote>
<p>(The RP needs to be able to access the provider, so unless you're
running this example on a public IP, you can't use the live example
at <a href="http://openidenabled.com/">openidenabled.com</a> on
your local provider.)</p>
</li>
<li>Point your browser to this new instance and follow the directions
below.</li>
<!-- Fun fact: 'url_for :port => 3001' doesn't work very well. -->
</ol>
</p>
<h2>To use the example relying party</h2>
<p>Visit <a href="<%= url_for :controller => 'consumer' %>">/consumer</a>
and enter your OpenID.</p>
</p>

View File

@ -0,0 +1,26 @@
<form method="post" action="<%= url_for :controller => 'server', :action => 'decision' %>">
<table>
<tr><td>Site:</td><td><%= @oidreq.trust_root %></td></tr>
<% if @oidreq.id_select %>
<tr>
<td colspan="2">
You entered the server identifier at the relying party.
You'll need to send an identifier of your choosing. Enter a
username below.
</td>
</tr>
<tr>
<td>Identity to send:</td>
<td><input type="text" name="id_to_send" size="25" /></td>
</tr>
<% else %>
<tr><td>Identity:</td><td><%= @oidreq.identity %></td></tr>
<% end %>
</table>
<input type="submit" name="yes" value="yes" />
<input type="submit" name="no" value="no" />
</form>

View File

@ -0,0 +1,19 @@
# Don't change this file. Configuration is done in config/environment.rb and config/environments/*.rb
unless defined?(RAILS_ROOT)
root_path = File.join(File.dirname(__FILE__), '..')
unless RUBY_PLATFORM =~ /mswin32/
require 'pathname'
root_path = Pathname.new(root_path).cleanpath(true).to_s
end
RAILS_ROOT = root_path
end
if File.directory?("#{RAILS_ROOT}/vendor/rails")
require "#{RAILS_ROOT}/vendor/rails/railties/lib/initializer"
else
require 'rubygems'
require 'initializer'
end
Rails::Initializer.run(:set_load_path)

View File

@ -0,0 +1,54 @@
# Be sure to restart your web server when you modify this file.
# Uncomment below to force Rails into production mode when
# you don't control web/app server and can't set it the proper way
# ENV['RAILS_ENV'] ||= 'production'
# Bootstrap the Rails environment, frameworks, and default configuration
require File.join(File.dirname(__FILE__), 'boot')
Rails::Initializer.run do |config|
# Settings in config/environments/* take precedence those specified here
# Skip frameworks you're not going to use
# config.frameworks -= [ :action_web_service, :action_mailer ]
# Add additional load paths for your own custom dirs
# config.load_paths += %W( #{RAILS_ROOT}/extras )
# Force all environments to use the same logger level
# (by default production uses :info, the others :debug)
# config.log_level = :debug
# Use the database for sessions instead of the file system
# (create the session table with 'rake create_sessions_table')
# config.action_controller.session_store = :active_record_store
# Enable page/fragment caching by setting a file-based store
# (remember to create the caching directory and make it readable to the application)
# config.action_controller.fragment_cache_store = :file_store, "#{RAILS_ROOT}/cache"
# Activate observers that should always be running
# config.active_record.observers = :cacher, :garbage_collector
# Make Active Record use UTC-base instead of local time
# config.active_record.default_timezone = :utc
# Use Active Record's schema dumper instead of SQL when creating the test database
# (enables use of different database adapters for development and test environments)
# config.active_record.schema_format = :ruby
# See Rails::Configuration for more options
end
# Add new inflection rules using the following format
# (all these examples are active by default):
# Inflector.inflections do |inflect|
# inflect.plural /^(ox)$/i, '\1en'
# inflect.singular /^(ox)en/i, '\1'
# inflect.irregular 'person', 'people'
# inflect.uncountable %w( fish sheep )
# end
# Include your application configuration below
ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS[:session_key] = '_session_id_2'

View File

@ -0,0 +1,19 @@
# Settings specified here will take precedence over those in config/environment.rb
# In the development environment your application's code is reloaded on
# every request. This slows down response time but is perfect for development
# since you don't have to restart the webserver when you make code changes.
config.cache_classes = false
# Log error messages when you accidentally call methods on nil.
config.whiny_nils = true
# Enable the breakpoint server that script/breakpointer connects to
config.breakpoint_server = true
# Show full error reports and disable caching
config.action_controller.consider_all_requests_local = true
config.action_controller.perform_caching = false
# Don't care if the mailer can't send
config.action_mailer.raise_delivery_errors = false

View File

@ -0,0 +1,19 @@
# Settings specified here will take precedence over those in config/environment.rb
# The production environment is meant for finished, "live" apps.
# Code is not reloaded between requests
config.cache_classes = true
# Use a different logger for distributed setups
# config.logger = SyslogLogger.new
# Full error reports are disabled and caching is turned on
config.action_controller.consider_all_requests_local = false
config.action_controller.perform_caching = true
# Enable serving of images, stylesheets, and javascripts from an asset server
# config.action_controller.asset_host = "http://assets.example.com"
# Disable delivery errors if you bad email addresses should just be ignored
# config.action_mailer.raise_delivery_errors = false

View File

@ -0,0 +1,19 @@
# Settings specified here will take precedence over those in config/environment.rb
# The test environment is used exclusively to run your application's
# test suite. You never need to work with it otherwise. Remember that
# your test database is "scratch space" for the test suite and is wiped
# and recreated between test runs. Don't rely on the data there!
config.cache_classes = true
# Log error messages when you accidentally call methods on nil.
config.whiny_nils = true
# Show full error reports and disable caching
config.action_controller.consider_all_requests_local = true
config.action_controller.perform_caching = false
# Tell ActionMailer not to deliver emails to the real world.
# The :test delivery method accumulates sent emails in the
# ActionMailer::Base.deliveries array.
config.action_mailer.delivery_method = :test

View File

@ -0,0 +1,24 @@
ActionController::Routing::Routes.draw do |map|
# Add your own custom routes here.
# The priority is based upon order of creation: first created -> highest priority.
# Here's a sample route:
# map.connect 'products/:id', :controller => 'catalog', :action => 'view'
# Keep in mind you can assign values other than :controller and :action
# You can have the root of your site routed by hooking up ''
# -- just remember to delete public/index.html.
# map.connect '', :controller => "welcome"
map.connect '', :controller => 'login'
map.connect 'server/xrds', :controller => 'server', :action => 'idp_xrds'
map.connect 'user/:username', :controller => 'server', :action => 'user_page'
map.connect 'user/:username/xrds', :controller => 'server', :action => 'user_xrds'
# Allow downloading Web Service WSDL as a file with an extension
# instead of a file named 'wsdl'
map.connect ':controller/service.wsdl', :action => 'wsdl'
# Install the default route as the lowest priority.
map.connect ':controller/:action/:id'
end

View File

@ -0,0 +1,2 @@
Use this README file to introduce your application and point to useful places in the API for learning more.
Run "rake appdoc" to generate API documentation for your models and controllers.

View File

@ -0,0 +1,8 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<body>
<h1>File not found</h1>
<p>Change this error message for pages not found in public/404.html</p>
</body>
</html>

View File

@ -0,0 +1,8 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<body>
<h1>Application error (Apache)</h1>
<p>Change this error message for exceptions thrown outside of an action (like in Dispatcher setups or broken Ruby code) in public/500.html</p>
</body>
</html>

View File

@ -0,0 +1,12 @@
#!/usr/bin/ruby1.8
#!/usr/local/bin/ruby
require File.dirname(__FILE__) + "/../config/environment" unless defined?(RAILS_ROOT)
# If you're using RubyGems and mod_ruby, this require should be changed to an absolute path one, like:
# "/usr/local/lib/ruby/gems/1.8/gems/rails-0.8.0/lib/dispatcher" -- otherwise performance is severely impaired
require "dispatcher"
ADDITIONAL_LOAD_PATHS.reverse.each { |dir| $:.unshift(dir) if File.directory?(dir) } if defined?(Apache::RubyRun)
Dispatcher.dispatch

View File

@ -0,0 +1,26 @@
#!/usr/bin/ruby1.8
#!/usr/local/bin/ruby
#
# You may specify the path to the FastCGI crash log (a log of unhandled
# exceptions which forced the FastCGI instance to exit, great for debugging)
# and the number of requests to process before running garbage collection.
#
# By default, the FastCGI crash log is RAILS_ROOT/log/fastcgi.crash.log
# and the GC period is nil (turned off). A reasonable number of requests
# could range from 10-100 depending on the memory footprint of your app.
#
# Example:
# # Default log path, normal GC behavior.
# RailsFCGIHandler.process!
#
# # Default log path, 50 requests between GC.
# RailsFCGIHandler.process! nil, 50
#
# # Custom log path, normal GC behavior.
# RailsFCGIHandler.process! '/var/log/myapp_fcgi_crash.log'
#
require File.dirname(__FILE__) + "/../config/environment"
require 'fcgi_handler'
RailsFCGIHandler.process!

View File

@ -0,0 +1,12 @@
#!/usr/bin/ruby1.8
#!/usr/local/bin/ruby
require File.dirname(__FILE__) + "/../config/environment" unless defined?(RAILS_ROOT)
# If you're using RubyGems and mod_ruby, this require should be changed to an absolute path one, like:
# "/usr/local/lib/ruby/gems/1.8/gems/rails-0.8.0/lib/dispatcher" -- otherwise performance is severely impaired
require "dispatcher"
ADDITIONAL_LOAD_PATHS.reverse.each { |dir| $:.unshift(dir) if File.directory?(dir) } if defined?(Apache::RubyRun)
Dispatcher.dispatch

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 B

View File

@ -0,0 +1,750 @@
// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
// (c) 2005 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
// (c) 2005 Jon Tirsen (http://www.tirsen.com)
// Contributors:
// Richard Livsey
// Rahul Bhargava
// Rob Wills
//
// See scriptaculous.js for full license.
// Autocompleter.Base handles all the autocompletion functionality
// that's independent of the data source for autocompletion. This
// includes drawing the autocompletion menu, observing keyboard
// and mouse events, and similar.
//
// Specific autocompleters need to provide, at the very least,
// a getUpdatedChoices function that will be invoked every time
// the text inside the monitored textbox changes. This method
// should get the text for which to provide autocompletion by
// invoking this.getToken(), NOT by directly accessing
// this.element.value. This is to allow incremental tokenized
// autocompletion. Specific auto-completion logic (AJAX, etc)
// belongs in getUpdatedChoices.
//
// Tokenized incremental autocompletion is enabled automatically
// when an autocompleter is instantiated with the 'tokens' option
// in the options parameter, e.g.:
// new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' });
// will incrementally autocomplete with a comma as the token.
// Additionally, ',' in the above example can be replaced with
// a token array, e.g. { tokens: [',', '\n'] } which
// enables autocompletion on multiple tokens. This is most
// useful when one of the tokens is \n (a newline), as it
// allows smart autocompletion after linebreaks.
var Autocompleter = {}
Autocompleter.Base = function() {};
Autocompleter.Base.prototype = {
baseInitialize: function(element, update, options) {
this.element = $(element);
this.update = $(update);
this.hasFocus = false;
this.changed = false;
this.active = false;
this.index = 0;
this.entryCount = 0;
if (this.setOptions)
this.setOptions(options);
else
this.options = options || {};
this.options.paramName = this.options.paramName || this.element.name;
this.options.tokens = this.options.tokens || [];
this.options.frequency = this.options.frequency || 0.4;
this.options.minChars = this.options.minChars || 1;
this.options.onShow = this.options.onShow ||
function(element, update){
if(!update.style.position || update.style.position=='absolute') {
update.style.position = 'absolute';
Position.clone(element, update, {setHeight: false, offsetTop: element.offsetHeight});
}
Effect.Appear(update,{duration:0.15});
};
this.options.onHide = this.options.onHide ||
function(element, update){ new Effect.Fade(update,{duration:0.15}) };
if (typeof(this.options.tokens) == 'string')
this.options.tokens = new Array(this.options.tokens);
this.observer = null;
this.element.setAttribute('autocomplete','off');
Element.hide(this.update);
Event.observe(this.element, "blur", this.onBlur.bindAsEventListener(this));
Event.observe(this.element, "keypress", this.onKeyPress.bindAsEventListener(this));
},
show: function() {
if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update);
if(!this.iefix &&
(navigator.appVersion.indexOf('MSIE')>0) &&
(navigator.userAgent.indexOf('Opera')<0) &&
(Element.getStyle(this.update, 'position')=='absolute')) {
new Insertion.After(this.update,
'<iframe id="' + this.update.id + '_iefix" '+
'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' +
'src="javascript:false;" frameborder="0" scrolling="no"></iframe>');
this.iefix = $(this.update.id+'_iefix');
}
if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50);
},
fixIEOverlapping: function() {
Position.clone(this.update, this.iefix);
this.iefix.style.zIndex = 1;
this.update.style.zIndex = 2;
Element.show(this.iefix);
},
hide: function() {
this.stopIndicator();
if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update);
if(this.iefix) Element.hide(this.iefix);
},
startIndicator: function() {
if(this.options.indicator) Element.show(this.options.indicator);
},
stopIndicator: function() {
if(this.options.indicator) Element.hide(this.options.indicator);
},
onKeyPress: function(event) {
if(this.active)
switch(event.keyCode) {
case Event.KEY_TAB:
case Event.KEY_RETURN:
this.selectEntry();
Event.stop(event);
case Event.KEY_ESC:
this.hide();
this.active = false;
Event.stop(event);
return;
case Event.KEY_LEFT:
case Event.KEY_RIGHT:
return;
case Event.KEY_UP:
this.markPrevious();
this.render();
if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
return;
case Event.KEY_DOWN:
this.markNext();
this.render();
if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
return;
}
else
if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN)
return;
this.changed = true;
this.hasFocus = true;
if(this.observer) clearTimeout(this.observer);
this.observer =
setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000);
},
onHover: function(event) {
var element = Event.findElement(event, 'LI');
if(this.index != element.autocompleteIndex)
{
this.index = element.autocompleteIndex;
this.render();
}
Event.stop(event);
},
onClick: function(event) {
var element = Event.findElement(event, 'LI');
this.index = element.autocompleteIndex;
this.selectEntry();
this.hide();
},
onBlur: function(event) {
// needed to make click events working
setTimeout(this.hide.bind(this), 250);
this.hasFocus = false;
this.active = false;
},
render: function() {
if(this.entryCount > 0) {
for (var i = 0; i < this.entryCount; i++)
this.index==i ?
Element.addClassName(this.getEntry(i),"selected") :
Element.removeClassName(this.getEntry(i),"selected");
if(this.hasFocus) {
this.show();
this.active = true;
}
} else {
this.active = false;
this.hide();
}
},
markPrevious: function() {
if(this.index > 0) this.index--
else this.index = this.entryCount-1;
},
markNext: function() {
if(this.index < this.entryCount-1) this.index++
else this.index = 0;
},
getEntry: function(index) {
return this.update.firstChild.childNodes[index];
},
getCurrentEntry: function() {
return this.getEntry(this.index);
},
selectEntry: function() {
this.active = false;
this.updateElement(this.getCurrentEntry());
},
updateElement: function(selectedElement) {
if (this.options.updateElement) {
this.options.updateElement(selectedElement);
return;
}
var value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');
var lastTokenPos = this.findLastToken();
if (lastTokenPos != -1) {
var newValue = this.element.value.substr(0, lastTokenPos + 1);
var whitespace = this.element.value.substr(lastTokenPos + 1).match(/^\s+/);
if (whitespace)
newValue += whitespace[0];
this.element.value = newValue + value;
} else {
this.element.value = value;
}
this.element.focus();
if (this.options.afterUpdateElement)
this.options.afterUpdateElement(this.element, selectedElement);
},
updateChoices: function(choices) {
if(!this.changed && this.hasFocus) {
this.update.innerHTML = choices;
Element.cleanWhitespace(this.update);
Element.cleanWhitespace(this.update.firstChild);
if(this.update.firstChild && this.update.firstChild.childNodes) {
this.entryCount =
this.update.firstChild.childNodes.length;
for (var i = 0; i < this.entryCount; i++) {
var entry = this.getEntry(i);
entry.autocompleteIndex = i;
this.addObservers(entry);
}
} else {
this.entryCount = 0;
}
this.stopIndicator();
this.index = 0;
this.render();
}
},
addObservers: function(element) {
Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this));
Event.observe(element, "click", this.onClick.bindAsEventListener(this));
},
onObserverEvent: function() {
this.changed = false;
if(this.getToken().length>=this.options.minChars) {
this.startIndicator();
this.getUpdatedChoices();
} else {
this.active = false;
this.hide();
}
},
getToken: function() {
var tokenPos = this.findLastToken();
if (tokenPos != -1)
var ret = this.element.value.substr(tokenPos + 1).replace(/^\s+/,'').replace(/\s+$/,'');
else
var ret = this.element.value;
return /\n/.test(ret) ? '' : ret;
},
findLastToken: function() {
var lastTokenPos = -1;
for (var i=0; i<this.options.tokens.length; i++) {
var thisTokenPos = this.element.value.lastIndexOf(this.options.tokens[i]);
if (thisTokenPos > lastTokenPos)
lastTokenPos = thisTokenPos;
}
return lastTokenPos;
}
}
Ajax.Autocompleter = Class.create();
Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.prototype), {
initialize: function(element, update, url, options) {
this.baseInitialize(element, update, options);
this.options.asynchronous = true;
this.options.onComplete = this.onComplete.bind(this);
this.options.defaultParams = this.options.parameters || null;
this.url = url;
},
getUpdatedChoices: function() {
entry = encodeURIComponent(this.options.paramName) + '=' +
encodeURIComponent(this.getToken());
this.options.parameters = this.options.callback ?
this.options.callback(this.element, entry) : entry;
if(this.options.defaultParams)
this.options.parameters += '&' + this.options.defaultParams;
new Ajax.Request(this.url, this.options);
},
onComplete: function(request) {
this.updateChoices(request.responseText);
}
});
// The local array autocompleter. Used when you'd prefer to
// inject an array of autocompletion options into the page, rather
// than sending out Ajax queries, which can be quite slow sometimes.
//
// The constructor takes four parameters. The first two are, as usual,
// the id of the monitored textbox, and id of the autocompletion menu.
// The third is the array you want to autocomplete from, and the fourth
// is the options block.
//
// Extra local autocompletion options:
// - choices - How many autocompletion choices to offer
//
// - partialSearch - If false, the autocompleter will match entered
// text only at the beginning of strings in the
// autocomplete array. Defaults to true, which will
// match text at the beginning of any *word* in the
// strings in the autocomplete array. If you want to
// search anywhere in the string, additionally set
// the option fullSearch to true (default: off).
//
// - fullSsearch - Search anywhere in autocomplete array strings.
//
// - partialChars - How many characters to enter before triggering
// a partial match (unlike minChars, which defines
// how many characters are required to do any match
// at all). Defaults to 2.
//
// - ignoreCase - Whether to ignore case when autocompleting.
// Defaults to true.
//
// It's possible to pass in a custom function as the 'selector'
// option, if you prefer to write your own autocompletion logic.
// In that case, the other options above will not apply unless
// you support them.
Autocompleter.Local = Class.create();
Autocompleter.Local.prototype = Object.extend(new Autocompleter.Base(), {
initialize: function(element, update, array, options) {
this.baseInitialize(element, update, options);
this.options.array = array;
},
getUpdatedChoices: function() {
this.updateChoices(this.options.selector(this));
},
setOptions: function(options) {
this.options = Object.extend({
choices: 10,
partialSearch: true,
partialChars: 2,
ignoreCase: true,
fullSearch: false,
selector: function(instance) {
var ret = []; // Beginning matches
var partial = []; // Inside matches
var entry = instance.getToken();
var count = 0;
for (var i = 0; i < instance.options.array.length &&
ret.length < instance.options.choices ; i++) {
var elem = instance.options.array[i];
var foundPos = instance.options.ignoreCase ?
elem.toLowerCase().indexOf(entry.toLowerCase()) :
elem.indexOf(entry);
while (foundPos != -1) {
if (foundPos == 0 && elem.length != entry.length) {
ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" +
elem.substr(entry.length) + "</li>");
break;
} else if (entry.length >= instance.options.partialChars &&
instance.options.partialSearch && foundPos != -1) {
if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) {
partial.push("<li>" + elem.substr(0, foundPos) + "<strong>" +
elem.substr(foundPos, entry.length) + "</strong>" + elem.substr(
foundPos + entry.length) + "</li>");
break;
}
}
foundPos = instance.options.ignoreCase ?
elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) :
elem.indexOf(entry, foundPos + 1);
}
}
if (partial.length)
ret = ret.concat(partial.slice(0, instance.options.choices - ret.length))
return "<ul>" + ret.join('') + "</ul>";
}
}, options || {});
}
});
// AJAX in-place editor
//
// see documentation on http://wiki.script.aculo.us/scriptaculous/show/Ajax.InPlaceEditor
// Use this if you notice weird scrolling problems on some browsers,
// the DOM might be a bit confused when this gets called so do this
// waits 1 ms (with setTimeout) until it does the activation
Field.scrollFreeActivate = function(field) {
setTimeout(function() {
Field.activate(field);
}, 1);
}
Ajax.InPlaceEditor = Class.create();
Ajax.InPlaceEditor.defaultHighlightColor = "#FFFF99";
Ajax.InPlaceEditor.prototype = {
initialize: function(element, url, options) {
this.url = url;
this.element = $(element);
this.options = Object.extend({
okText: "ok",
cancelText: "cancel",
savingText: "Saving...",
clickToEditText: "Click to edit",
okText: "ok",
rows: 1,
onComplete: function(transport, element) {
new Effect.Highlight(element, {startcolor: this.options.highlightcolor});
},
onFailure: function(transport) {
alert("Error communicating with the server: " + transport.responseText.stripTags());
},
callback: function(form) {
return Form.serialize(form);
},
handleLineBreaks: true,
loadingText: 'Loading...',
savingClassName: 'inplaceeditor-saving',
loadingClassName: 'inplaceeditor-loading',
formClassName: 'inplaceeditor-form',
highlightcolor: Ajax.InPlaceEditor.defaultHighlightColor,
highlightendcolor: "#FFFFFF",
externalControl: null,
ajaxOptions: {}
}, options || {});
if(!this.options.formId && this.element.id) {
this.options.formId = this.element.id + "-inplaceeditor";
if ($(this.options.formId)) {
// there's already a form with that name, don't specify an id
this.options.formId = null;
}
}
if (this.options.externalControl) {
this.options.externalControl = $(this.options.externalControl);
}
this.originalBackground = Element.getStyle(this.element, 'background-color');
if (!this.originalBackground) {
this.originalBackground = "transparent";
}
this.element.title = this.options.clickToEditText;
this.onclickListener = this.enterEditMode.bindAsEventListener(this);
this.mouseoverListener = this.enterHover.bindAsEventListener(this);
this.mouseoutListener = this.leaveHover.bindAsEventListener(this);
Event.observe(this.element, 'click', this.onclickListener);
Event.observe(this.element, 'mouseover', this.mouseoverListener);
Event.observe(this.element, 'mouseout', this.mouseoutListener);
if (this.options.externalControl) {
Event.observe(this.options.externalControl, 'click', this.onclickListener);
Event.observe(this.options.externalControl, 'mouseover', this.mouseoverListener);
Event.observe(this.options.externalControl, 'mouseout', this.mouseoutListener);
}
},
enterEditMode: function(evt) {
if (this.saving) return;
if (this.editing) return;
this.editing = true;
this.onEnterEditMode();
if (this.options.externalControl) {
Element.hide(this.options.externalControl);
}
Element.hide(this.element);
this.createForm();
this.element.parentNode.insertBefore(this.form, this.element);
Field.scrollFreeActivate(this.editField);
// stop the event to avoid a page refresh in Safari
if (evt) {
Event.stop(evt);
}
return false;
},
createForm: function() {
this.form = document.createElement("form");
this.form.id = this.options.formId;
Element.addClassName(this.form, this.options.formClassName)
this.form.onsubmit = this.onSubmit.bind(this);
this.createEditField();
if (this.options.textarea) {
var br = document.createElement("br");
this.form.appendChild(br);
}
okButton = document.createElement("input");
okButton.type = "submit";
okButton.value = this.options.okText;
this.form.appendChild(okButton);
cancelLink = document.createElement("a");
cancelLink.href = "#";
cancelLink.appendChild(document.createTextNode(this.options.cancelText));
cancelLink.onclick = this.onclickCancel.bind(this);
this.form.appendChild(cancelLink);
},
hasHTMLLineBreaks: function(string) {
if (!this.options.handleLineBreaks) return false;
return string.match(/<br/i) || string.match(/<p>/i);
},
convertHTMLLineBreaks: function(string) {
return string.replace(/<br>/gi, "\n").replace(/<br\/>/gi, "\n").replace(/<\/p>/gi, "\n").replace(/<p>/gi, "");
},
createEditField: function() {
var text;
if(this.options.loadTextURL) {
text = this.options.loadingText;
} else {
text = this.getText();
}
if (this.options.rows == 1 && !this.hasHTMLLineBreaks(text)) {
this.options.textarea = false;
var textField = document.createElement("input");
textField.type = "text";
textField.name = "value";
textField.value = text;
textField.style.backgroundColor = this.options.highlightcolor;
var size = this.options.size || this.options.cols || 0;
if (size != 0) textField.size = size;
this.editField = textField;
} else {
this.options.textarea = true;
var textArea = document.createElement("textarea");
textArea.name = "value";
textArea.value = this.convertHTMLLineBreaks(text);
textArea.rows = this.options.rows;
textArea.cols = this.options.cols || 40;
this.editField = textArea;
}
if(this.options.loadTextURL) {
this.loadExternalText();
}
this.form.appendChild(this.editField);
},
getText: function() {
return this.element.innerHTML;
},
loadExternalText: function() {
Element.addClassName(this.form, this.options.loadingClassName);
this.editField.disabled = true;
new Ajax.Request(
this.options.loadTextURL,
Object.extend({
asynchronous: true,
onComplete: this.onLoadedExternalText.bind(this)
}, this.options.ajaxOptions)
);
},
onLoadedExternalText: function(transport) {
Element.removeClassName(this.form, this.options.loadingClassName);
this.editField.disabled = false;
this.editField.value = transport.responseText.stripTags();
},
onclickCancel: function() {
this.onComplete();
this.leaveEditMode();
return false;
},
onFailure: function(transport) {
this.options.onFailure(transport);
if (this.oldInnerHTML) {
this.element.innerHTML = this.oldInnerHTML;
this.oldInnerHTML = null;
}
return false;
},
onSubmit: function() {
// onLoading resets these so we need to save them away for the Ajax call
var form = this.form;
var value = this.editField.value;
// do this first, sometimes the ajax call returns before we get a chance to switch on Saving...
// which means this will actually switch on Saving... *after* we've left edit mode causing Saving...
// to be displayed indefinitely
this.onLoading();
new Ajax.Updater(
{
success: this.element,
// don't update on failure (this could be an option)
failure: null
},
this.url,
Object.extend({
parameters: this.options.callback(form, value),
onComplete: this.onComplete.bind(this),
onFailure: this.onFailure.bind(this)
}, this.options.ajaxOptions)
);
// stop the event to avoid a page refresh in Safari
if (arguments.length > 1) {
Event.stop(arguments[0]);
}
return false;
},
onLoading: function() {
this.saving = true;
this.removeForm();
this.leaveHover();
this.showSaving();
},
showSaving: function() {
this.oldInnerHTML = this.element.innerHTML;
this.element.innerHTML = this.options.savingText;
Element.addClassName(this.element, this.options.savingClassName);
this.element.style.backgroundColor = this.originalBackground;
Element.show(this.element);
},
removeForm: function() {
if(this.form) {
if (this.form.parentNode) Element.remove(this.form);
this.form = null;
}
},
enterHover: function() {
if (this.saving) return;
this.element.style.backgroundColor = this.options.highlightcolor;
if (this.effect) {
this.effect.cancel();
}
Element.addClassName(this.element, this.options.hoverClassName)
},
leaveHover: function() {
if (this.options.backgroundColor) {
this.element.style.backgroundColor = this.oldBackground;
}
Element.removeClassName(this.element, this.options.hoverClassName)
if (this.saving) return;
this.effect = new Effect.Highlight(this.element, {
startcolor: this.options.highlightcolor,
endcolor: this.options.highlightendcolor,
restorecolor: this.originalBackground
});
},
leaveEditMode: function() {
Element.removeClassName(this.element, this.options.savingClassName);
this.removeForm();
this.leaveHover();
this.element.style.backgroundColor = this.originalBackground;
Element.show(this.element);
if (this.options.externalControl) {
Element.show(this.options.externalControl);
}
this.editing = false;
this.saving = false;
this.oldInnerHTML = null;
this.onLeaveEditMode();
},
onComplete: function(transport) {
this.leaveEditMode();
this.options.onComplete.bind(this)(transport, this.element);
},
onEnterEditMode: function() {},
onLeaveEditMode: function() {},
dispose: function() {
if (this.oldInnerHTML) {
this.element.innerHTML = this.oldInnerHTML;
}
this.leaveEditMode();
Event.stopObserving(this.element, 'click', this.onclickListener);
Event.stopObserving(this.element, 'mouseover', this.mouseoverListener);
Event.stopObserving(this.element, 'mouseout', this.mouseoutListener);
if (this.options.externalControl) {
Event.stopObserving(this.options.externalControl, 'click', this.onclickListener);
Event.stopObserving(this.options.externalControl, 'mouseover', this.mouseoverListener);
Event.stopObserving(this.options.externalControl, 'mouseout', this.mouseoutListener);
}
}
};
// Delayed observer, like Form.Element.Observer,
// but waits for delay after last key input
// Ideal for live-search fields
Form.Element.DelayedObserver = Class.create();
Form.Element.DelayedObserver.prototype = {
initialize: function(element, delay, callback) {
this.delay = delay || 0.5;
this.element = $(element);
this.callback = callback;
this.timer = null;
this.lastValue = $F(this.element);
Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this));
},
delayedListener: function(event) {
if(this.lastValue == $F(this.element)) return;
if(this.timer) clearTimeout(this.timer);
this.timer = setTimeout(this.onTimerEvent.bind(this), this.delay * 1000);
this.lastValue = $F(this.element);
},
onTimerEvent: function() {
this.timer = null;
this.callback(this.element, $F(this.element));
}
};

View File

@ -0,0 +1,584 @@
// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
//
// See scriptaculous.js for full license.
/*--------------------------------------------------------------------------*/
var Droppables = {
drops: [],
remove: function(element) {
this.drops = this.drops.reject(function(d) { return d.element==$(element) });
},
add: function(element) {
element = $(element);
var options = Object.extend({
greedy: true,
hoverclass: null
}, arguments[1] || {});
// cache containers
if(options.containment) {
options._containers = [];
var containment = options.containment;
if((typeof containment == 'object') &&
(containment.constructor == Array)) {
containment.each( function(c) { options._containers.push($(c)) });
} else {
options._containers.push($(containment));
}
}
if(options.accept) options.accept = [options.accept].flatten();
Element.makePositioned(element); // fix IE
options.element = element;
this.drops.push(options);
},
isContained: function(element, drop) {
var parentNode = element.parentNode;
return drop._containers.detect(function(c) { return parentNode == c });
},
isAffected: function(point, element, drop) {
return (
(drop.element!=element) &&
((!drop._containers) ||
this.isContained(element, drop)) &&
((!drop.accept) ||
(Element.classNames(element).detect(
function(v) { return drop.accept.include(v) } ) )) &&
Position.within(drop.element, point[0], point[1]) );
},
deactivate: function(drop) {
if(drop.hoverclass)
Element.removeClassName(drop.element, drop.hoverclass);
this.last_active = null;
},
activate: function(drop) {
if(drop.hoverclass)
Element.addClassName(drop.element, drop.hoverclass);
this.last_active = drop;
},
show: function(point, element) {
if(!this.drops.length) return;
if(this.last_active) this.deactivate(this.last_active);
this.drops.each( function(drop) {
if(Droppables.isAffected(point, element, drop)) {
if(drop.onHover)
drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element));
if(drop.greedy) {
Droppables.activate(drop);
throw $break;
}
}
});
},
fire: function(event, element) {
if(!this.last_active) return;
Position.prepare();
if (this.isAffected([Event.pointerX(event), Event.pointerY(event)], element, this.last_active))
if (this.last_active.onDrop)
this.last_active.onDrop(element, this.last_active.element, event);
},
reset: function() {
if(this.last_active)
this.deactivate(this.last_active);
}
}
var Draggables = {
drags: [],
observers: [],
register: function(draggable) {
if(this.drags.length == 0) {
this.eventMouseUp = this.endDrag.bindAsEventListener(this);
this.eventMouseMove = this.updateDrag.bindAsEventListener(this);
this.eventKeypress = this.keyPress.bindAsEventListener(this);
Event.observe(document, "mouseup", this.eventMouseUp);
Event.observe(document, "mousemove", this.eventMouseMove);
Event.observe(document, "keypress", this.eventKeypress);
}
this.drags.push(draggable);
},
unregister: function(draggable) {
this.drags = this.drags.reject(function(d) { return d==draggable });
if(this.drags.length == 0) {
Event.stopObserving(document, "mouseup", this.eventMouseUp);
Event.stopObserving(document, "mousemove", this.eventMouseMove);
Event.stopObserving(document, "keypress", this.eventKeypress);
}
},
activate: function(draggable) {
window.focus(); // allows keypress events if window isn't currently focused, fails for Safari
this.activeDraggable = draggable;
},
deactivate: function(draggbale) {
this.activeDraggable = null;
},
updateDrag: function(event) {
if(!this.activeDraggable) return;
var pointer = [Event.pointerX(event), Event.pointerY(event)];
// Mozilla-based browsers fire successive mousemove events with
// the same coordinates, prevent needless redrawing (moz bug?)
if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return;
this._lastPointer = pointer;
this.activeDraggable.updateDrag(event, pointer);
},
endDrag: function(event) {
if(!this.activeDraggable) return;
this._lastPointer = null;
this.activeDraggable.endDrag(event);
},
keyPress: function(event) {
if(this.activeDraggable)
this.activeDraggable.keyPress(event);
},
addObserver: function(observer) {
this.observers.push(observer);
this._cacheObserverCallbacks();
},
removeObserver: function(element) { // element instead of observer fixes mem leaks
this.observers = this.observers.reject( function(o) { return o.element==element });
this._cacheObserverCallbacks();
},
notify: function(eventName, draggable, event) { // 'onStart', 'onEnd', 'onDrag'
if(this[eventName+'Count'] > 0)
this.observers.each( function(o) {
if(o[eventName]) o[eventName](eventName, draggable, event);
});
},
_cacheObserverCallbacks: function() {
['onStart','onEnd','onDrag'].each( function(eventName) {
Draggables[eventName+'Count'] = Draggables.observers.select(
function(o) { return o[eventName]; }
).length;
});
}
}
/*--------------------------------------------------------------------------*/
var Draggable = Class.create();
Draggable.prototype = {
initialize: function(element) {
var options = Object.extend({
handle: false,
starteffect: function(element) {
new Effect.Opacity(element, {duration:0.2, from:1.0, to:0.7});
},
reverteffect: function(element, top_offset, left_offset) {
var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02;
element._revert = new Effect.MoveBy(element, -top_offset, -left_offset, {duration:dur});
},
endeffect: function(element) {
new Effect.Opacity(element, {duration:0.2, from:0.7, to:1.0});
},
zindex: 1000,
revert: false,
snap: false // false, or xy or [x,y] or function(x,y){ return [x,y] }
}, arguments[1] || {});
this.element = $(element);
if(options.handle && (typeof options.handle == 'string'))
this.handle = Element.childrenWithClassName(this.element, options.handle)[0];
if(!this.handle) this.handle = $(options.handle);
if(!this.handle) this.handle = this.element;
Element.makePositioned(this.element); // fix IE
this.delta = this.currentDelta();
this.options = options;
this.dragging = false;
this.eventMouseDown = this.initDrag.bindAsEventListener(this);
Event.observe(this.handle, "mousedown", this.eventMouseDown);
Draggables.register(this);
},
destroy: function() {
Event.stopObserving(this.handle, "mousedown", this.eventMouseDown);
Draggables.unregister(this);
},
currentDelta: function() {
return([
parseInt(this.element.style.left || '0'),
parseInt(this.element.style.top || '0')]);
},
initDrag: function(event) {
if(Event.isLeftClick(event)) {
// abort on form elements, fixes a Firefox issue
var src = Event.element(event);
if(src.tagName && (
src.tagName=='INPUT' ||
src.tagName=='SELECT' ||
src.tagName=='BUTTON' ||
src.tagName=='TEXTAREA')) return;
if(this.element._revert) {
this.element._revert.cancel();
this.element._revert = null;
}
var pointer = [Event.pointerX(event), Event.pointerY(event)];
var pos = Position.cumulativeOffset(this.element);
this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) });
Draggables.activate(this);
Event.stop(event);
}
},
startDrag: function(event) {
this.dragging = true;
if(this.options.zindex) {
this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0);
this.element.style.zIndex = this.options.zindex;
}
if(this.options.ghosting) {
this._clone = this.element.cloneNode(true);
Position.absolutize(this.element);
this.element.parentNode.insertBefore(this._clone, this.element);
}
Draggables.notify('onStart', this, event);
if(this.options.starteffect) this.options.starteffect(this.element);
},
updateDrag: function(event, pointer) {
if(!this.dragging) this.startDrag(event);
Position.prepare();
Droppables.show(pointer, this.element);
Draggables.notify('onDrag', this, event);
this.draw(pointer);
if(this.options.change) this.options.change(this);
// fix AppleWebKit rendering
if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);
Event.stop(event);
},
finishDrag: function(event, success) {
this.dragging = false;
if(this.options.ghosting) {
Position.relativize(this.element);
Element.remove(this._clone);
this._clone = null;
}
if(success) Droppables.fire(event, this.element);
Draggables.notify('onEnd', this, event);
var revert = this.options.revert;
if(revert && typeof revert == 'function') revert = revert(this.element);
var d = this.currentDelta();
if(revert && this.options.reverteffect) {
this.options.reverteffect(this.element,
d[1]-this.delta[1], d[0]-this.delta[0]);
} else {
this.delta = d;
}
if(this.options.zindex)
this.element.style.zIndex = this.originalZ;
if(this.options.endeffect)
this.options.endeffect(this.element);
Draggables.deactivate(this);
Droppables.reset();
},
keyPress: function(event) {
if(!event.keyCode==Event.KEY_ESC) return;
this.finishDrag(event, false);
Event.stop(event);
},
endDrag: function(event) {
if(!this.dragging) return;
this.finishDrag(event, true);
Event.stop(event);
},
draw: function(point) {
var pos = Position.cumulativeOffset(this.element);
var d = this.currentDelta();
pos[0] -= d[0]; pos[1] -= d[1];
var p = [0,1].map(function(i){ return (point[i]-pos[i]-this.offset[i]) }.bind(this));
if(this.options.snap) {
if(typeof this.options.snap == 'function') {
p = this.options.snap(p[0],p[1]);
} else {
if(this.options.snap instanceof Array) {
p = p.map( function(v, i) {
return Math.round(v/this.options.snap[i])*this.options.snap[i] }.bind(this))
} else {
p = p.map( function(v) {
return Math.round(v/this.options.snap)*this.options.snap }.bind(this))
}
}}
var style = this.element.style;
if((!this.options.constraint) || (this.options.constraint=='horizontal'))
style.left = p[0] + "px";
if((!this.options.constraint) || (this.options.constraint=='vertical'))
style.top = p[1] + "px";
if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering
}
}
/*--------------------------------------------------------------------------*/
var SortableObserver = Class.create();
SortableObserver.prototype = {
initialize: function(element, observer) {
this.element = $(element);
this.observer = observer;
this.lastValue = Sortable.serialize(this.element);
},
onStart: function() {
this.lastValue = Sortable.serialize(this.element);
},
onEnd: function() {
Sortable.unmark();
if(this.lastValue != Sortable.serialize(this.element))
this.observer(this.element)
}
}
var Sortable = {
sortables: new Array(),
options: function(element){
element = $(element);
return this.sortables.detect(function(s) { return s.element == element });
},
destroy: function(element){
element = $(element);
this.sortables.findAll(function(s) { return s.element == element }).each(function(s){
Draggables.removeObserver(s.element);
s.droppables.each(function(d){ Droppables.remove(d) });
s.draggables.invoke('destroy');
});
this.sortables = this.sortables.reject(function(s) { return s.element == element });
},
create: function(element) {
element = $(element);
var options = Object.extend({
element: element,
tag: 'li', // assumes li children, override with tag: 'tagname'
dropOnEmpty: false,
tree: false, // fixme: unimplemented
overlap: 'vertical', // one of 'vertical', 'horizontal'
constraint: 'vertical', // one of 'vertical', 'horizontal', false
containment: element, // also takes array of elements (or id's); or false
handle: false, // or a CSS class
only: false,
hoverclass: null,
ghosting: false,
format: null,
onChange: Prototype.emptyFunction,
onUpdate: Prototype.emptyFunction
}, arguments[1] || {});
// clear any old sortable with same element
this.destroy(element);
// build options for the draggables
var options_for_draggable = {
revert: true,
ghosting: options.ghosting,
constraint: options.constraint,
handle: options.handle };
if(options.starteffect)
options_for_draggable.starteffect = options.starteffect;
if(options.reverteffect)
options_for_draggable.reverteffect = options.reverteffect;
else
if(options.ghosting) options_for_draggable.reverteffect = function(element) {
element.style.top = 0;
element.style.left = 0;
};
if(options.endeffect)
options_for_draggable.endeffect = options.endeffect;
if(options.zindex)
options_for_draggable.zindex = options.zindex;
// build options for the droppables
var options_for_droppable = {
overlap: options.overlap,
containment: options.containment,
hoverclass: options.hoverclass,
onHover: Sortable.onHover,
greedy: !options.dropOnEmpty
}
// fix for gecko engine
Element.cleanWhitespace(element);
options.draggables = [];
options.droppables = [];
// make it so
// drop on empty handling
if(options.dropOnEmpty) {
Droppables.add(element,
{containment: options.containment, onHover: Sortable.onEmptyHover, greedy: false});
options.droppables.push(element);
}
(this.findElements(element, options) || []).each( function(e) {
// handles are per-draggable
var handle = options.handle ?
Element.childrenWithClassName(e, options.handle)[0] : e;
options.draggables.push(
new Draggable(e, Object.extend(options_for_draggable, { handle: handle })));
Droppables.add(e, options_for_droppable);
options.droppables.push(e);
});
// keep reference
this.sortables.push(options);
// for onupdate
Draggables.addObserver(new SortableObserver(element, options.onUpdate));
},
// return all suitable-for-sortable elements in a guaranteed order
findElements: function(element, options) {
if(!element.hasChildNodes()) return null;
var elements = [];
$A(element.childNodes).each( function(e) {
if(e.tagName && e.tagName.toUpperCase()==options.tag.toUpperCase() &&
(!options.only || (Element.hasClassName(e, options.only))))
elements.push(e);
if(options.tree) {
var grandchildren = this.findElements(e, options);
if(grandchildren) elements.push(grandchildren);
}
});
return (elements.length>0 ? elements.flatten() : null);
},
onHover: function(element, dropon, overlap) {
if(overlap>0.5) {
Sortable.mark(dropon, 'before');
if(dropon.previousSibling != element) {
var oldParentNode = element.parentNode;
element.style.visibility = "hidden"; // fix gecko rendering
dropon.parentNode.insertBefore(element, dropon);
if(dropon.parentNode!=oldParentNode)
Sortable.options(oldParentNode).onChange(element);
Sortable.options(dropon.parentNode).onChange(element);
}
} else {
Sortable.mark(dropon, 'after');
var nextElement = dropon.nextSibling || null;
if(nextElement != element) {
var oldParentNode = element.parentNode;
element.style.visibility = "hidden"; // fix gecko rendering
dropon.parentNode.insertBefore(element, nextElement);
if(dropon.parentNode!=oldParentNode)
Sortable.options(oldParentNode).onChange(element);
Sortable.options(dropon.parentNode).onChange(element);
}
}
},
onEmptyHover: function(element, dropon) {
if(element.parentNode!=dropon) {
var oldParentNode = element.parentNode;
dropon.appendChild(element);
Sortable.options(oldParentNode).onChange(element);
Sortable.options(dropon).onChange(element);
}
},
unmark: function() {
if(Sortable._marker) Element.hide(Sortable._marker);
},
mark: function(dropon, position) {
// mark on ghosting only
var sortable = Sortable.options(dropon.parentNode);
if(sortable && !sortable.ghosting) return;
if(!Sortable._marker) {
Sortable._marker = $('dropmarker') || document.createElement('DIV');
Element.hide(Sortable._marker);
Element.addClassName(Sortable._marker, 'dropmarker');
Sortable._marker.style.position = 'absolute';
document.getElementsByTagName("body").item(0).appendChild(Sortable._marker);
}
var offsets = Position.cumulativeOffset(dropon);
Sortable._marker.style.left = offsets[0] + 'px';
Sortable._marker.style.top = offsets[1] + 'px';
if(position=='after')
if(sortable.overlap == 'horizontal')
Sortable._marker.style.left = (offsets[0]+dropon.clientWidth) + 'px';
else
Sortable._marker.style.top = (offsets[1]+dropon.clientHeight) + 'px';
Element.show(Sortable._marker);
},
serialize: function(element) {
element = $(element);
var sortableOptions = this.options(element);
var options = Object.extend({
tag: sortableOptions.tag,
only: sortableOptions.only,
name: element.id,
format: sortableOptions.format || /^[^_]*_(.*)$/
}, arguments[1] || {});
return $(this.findElements(element, options) || []).map( function(item) {
return (encodeURIComponent(options.name) + "[]=" +
encodeURIComponent(item.id.match(options.format) ? item.id.match(options.format)[1] : ''));
}).join("&");
}
}

View File

@ -0,0 +1,854 @@
// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
// Contributors:
// Justin Palmer (http://encytemedia.com/)
// Mark Pilgrim (http://diveintomark.org/)
// Martin Bialasinki
//
// See scriptaculous.js for full license.
/* ------------- element ext -------------- */
// converts rgb() and #xxx to #xxxxxx format,
// returns self (or first argument) if not convertable
String.prototype.parseColor = function() {
var color = '#';
if(this.slice(0,4) == 'rgb(') {
var cols = this.slice(4,this.length-1).split(',');
var i=0; do { color += parseInt(cols[i]).toColorPart() } while (++i<3);
} else {
if(this.slice(0,1) == '#') {
if(this.length==4) for(var i=1;i<4;i++) color += (this.charAt(i) + this.charAt(i)).toLowerCase();
if(this.length==7) color = this.toLowerCase();
}
}
return(color.length==7 ? color : (arguments[0] || this));
}
Element.collectTextNodesIgnoreClass = function(element, ignoreclass) {
var children = $(element).childNodes;
var text = '';
var classtest = new RegExp('^([^ ]+ )*' + ignoreclass+ '( [^ ]+)*$','i');
for (var i = 0; i < children.length; i++) {
if(children[i].nodeType==3) {
text+=children[i].nodeValue;
} else {
if((!children[i].className.match(classtest)) && children[i].hasChildNodes())
text += Element.collectTextNodesIgnoreClass(children[i], ignoreclass);
}
}
return text;
}
Element.setStyle = function(element, style) {
element = $(element);
for(k in style) element.style[k.camelize()] = style[k];
}
Element.setContentZoom = function(element, percent) {
Element.setStyle(element, {fontSize: (percent/100) + 'em'});
if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);
}
Element.getOpacity = function(element){
var opacity;
if (opacity = Element.getStyle(element, 'opacity'))
return parseFloat(opacity);
if (opacity = (Element.getStyle(element, 'filter') || '').match(/alpha\(opacity=(.*)\)/))
if(opacity[1]) return parseFloat(opacity[1]) / 100;
return 1.0;
}
Element.setOpacity = function(element, value){
element= $(element);
if (value == 1){
Element.setStyle(element, { opacity:
(/Gecko/.test(navigator.userAgent) && !/Konqueror|Safari|KHTML/.test(navigator.userAgent)) ?
0.999999 : null });
if(/MSIE/.test(navigator.userAgent))
Element.setStyle(element, {filter: Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'')});
} else {
if(value < 0.00001) value = 0;
Element.setStyle(element, {opacity: value});
if(/MSIE/.test(navigator.userAgent))
Element.setStyle(element,
{ filter: Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'') +
'alpha(opacity='+value*100+')' });
}
}
Element.getInlineOpacity = function(element){
return $(element).style.opacity || '';
}
Element.childrenWithClassName = function(element, className) {
return $A($(element).getElementsByTagName('*')).select(
function(c) { return Element.hasClassName(c, className) });
}
Array.prototype.call = function() {
var args = arguments;
this.each(function(f){ f.apply(this, args) });
}
/*--------------------------------------------------------------------------*/
var Effect = {
tagifyText: function(element) {
var tagifyStyle = 'position:relative';
if(/MSIE/.test(navigator.userAgent)) tagifyStyle += ';zoom:1';
element = $(element);
$A(element.childNodes).each( function(child) {
if(child.nodeType==3) {
child.nodeValue.toArray().each( function(character) {
element.insertBefore(
Builder.node('span',{style: tagifyStyle},
character == ' ' ? String.fromCharCode(160) : character),
child);
});
Element.remove(child);
}
});
},
multiple: function(element, effect) {
var elements;
if(((typeof element == 'object') ||
(typeof element == 'function')) &&
(element.length))
elements = element;
else
elements = $(element).childNodes;
var options = Object.extend({
speed: 0.1,
delay: 0.0
}, arguments[2] || {});
var masterDelay = options.delay;
$A(elements).each( function(element, index) {
new effect(element, Object.extend(options, { delay: index * options.speed + masterDelay }));
});
}
};
var Effect2 = Effect; // deprecated
/* ------------- transitions ------------- */
Effect.Transitions = {}
Effect.Transitions.linear = function(pos) {
return pos;
}
Effect.Transitions.sinoidal = function(pos) {
return (-Math.cos(pos*Math.PI)/2) + 0.5;
}
Effect.Transitions.reverse = function(pos) {
return 1-pos;
}
Effect.Transitions.flicker = function(pos) {
return ((-Math.cos(pos*Math.PI)/4) + 0.75) + Math.random()/4;
}
Effect.Transitions.wobble = function(pos) {
return (-Math.cos(pos*Math.PI*(9*pos))/2) + 0.5;
}
Effect.Transitions.pulse = function(pos) {
return (Math.floor(pos*10) % 2 == 0 ?
(pos*10-Math.floor(pos*10)) : 1-(pos*10-Math.floor(pos*10)));
}
Effect.Transitions.none = function(pos) {
return 0;
}
Effect.Transitions.full = function(pos) {
return 1;
}
/* ------------- core effects ------------- */
Effect.Queue = {
effects: [],
_each: function(iterator) {
this.effects._each(iterator);
},
interval: null,
add: function(effect) {
var timestamp = new Date().getTime();
switch(effect.options.queue) {
case 'front':
// move unstarted effects after this effect
this.effects.findAll(function(e){ return e.state=='idle' }).each( function(e) {
e.startOn += effect.finishOn;
e.finishOn += effect.finishOn;
});
break;
case 'end':
// start effect after last queued effect has finished
timestamp = this.effects.pluck('finishOn').max() || timestamp;
break;
}
effect.startOn += timestamp;
effect.finishOn += timestamp;
this.effects.push(effect);
if(!this.interval)
this.interval = setInterval(this.loop.bind(this), 40);
},
remove: function(effect) {
this.effects = this.effects.reject(function(e) { return e==effect });
if(this.effects.length == 0) {
clearInterval(this.interval);
this.interval = null;
}
},
loop: function() {
var timePos = new Date().getTime();
this.effects.invoke('loop', timePos);
}
}
Object.extend(Effect.Queue, Enumerable);
Effect.Base = function() {};
Effect.Base.prototype = {
position: null,
setOptions: function(options) {
this.options = Object.extend({
transition: Effect.Transitions.sinoidal,
duration: 1.0, // seconds
fps: 25.0, // max. 25fps due to Effect.Queue implementation
sync: false, // true for combining
from: 0.0,
to: 1.0,
delay: 0.0,
queue: 'parallel'
}, options || {});
},
start: function(options) {
this.setOptions(options || {});
this.currentFrame = 0;
this.state = 'idle';
this.startOn = this.options.delay*1000;
this.finishOn = this.startOn + (this.options.duration*1000);
this.event('beforeStart');
if(!this.options.sync) Effect.Queue.add(this);
},
loop: function(timePos) {
if(timePos >= this.startOn) {
if(timePos >= this.finishOn) {
this.render(1.0);
this.cancel();
this.event('beforeFinish');
if(this.finish) this.finish();
this.event('afterFinish');
return;
}
var pos = (timePos - this.startOn) / (this.finishOn - this.startOn);
var frame = Math.round(pos * this.options.fps * this.options.duration);
if(frame > this.currentFrame) {
this.render(pos);
this.currentFrame = frame;
}
}
},
render: function(pos) {
if(this.state == 'idle') {
this.state = 'running';
this.event('beforeSetup');
if(this.setup) this.setup();
this.event('afterSetup');
}
if(this.state == 'running') {
if(this.options.transition) pos = this.options.transition(pos);
pos *= (this.options.to-this.options.from);
pos += this.options.from;
this.position = pos;
this.event('beforeUpdate');
if(this.update) this.update(pos);
this.event('afterUpdate');
}
},
cancel: function() {
if(!this.options.sync) Effect.Queue.remove(this);
this.state = 'finished';
},
event: function(eventName) {
if(this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this);
if(this.options[eventName]) this.options[eventName](this);
},
inspect: function() {
return '#<Effect:' + $H(this).inspect() + ',options:' + $H(this.options).inspect() + '>';
}
}
Effect.Parallel = Class.create();
Object.extend(Object.extend(Effect.Parallel.prototype, Effect.Base.prototype), {
initialize: function(effects) {
this.effects = effects || [];
this.start(arguments[1]);
},
update: function(position) {
this.effects.invoke('render', position);
},
finish: function(position) {
this.effects.each( function(effect) {
effect.render(1.0);
effect.cancel();
effect.event('beforeFinish');
if(effect.finish) effect.finish(position);
effect.event('afterFinish');
});
}
});
Effect.Opacity = Class.create();
Object.extend(Object.extend(Effect.Opacity.prototype, Effect.Base.prototype), {
initialize: function(element) {
this.element = $(element);
// make this work on IE on elements without 'layout'
if(/MSIE/.test(navigator.userAgent) && (!this.element.hasLayout))
Element.setStyle(this.element, {zoom: 1});
var options = Object.extend({
from: Element.getOpacity(this.element) || 0.0,
to: 1.0
}, arguments[1] || {});
this.start(options);
},
update: function(position) {
Element.setOpacity(this.element, position);
}
});
Effect.MoveBy = Class.create();
Object.extend(Object.extend(Effect.MoveBy.prototype, Effect.Base.prototype), {
initialize: function(element, toTop, toLeft) {
this.element = $(element);
this.toTop = toTop;
this.toLeft = toLeft;
this.start(arguments[3]);
},
setup: function() {
// Bug in Opera: Opera returns the "real" position of a static element or
// relative element that does not have top/left explicitly set.
// ==> Always set top and left for position relative elements in your stylesheets
// (to 0 if you do not need them)
Element.makePositioned(this.element);
this.originalTop = parseFloat(Element.getStyle(this.element,'top') || '0');
this.originalLeft = parseFloat(Element.getStyle(this.element,'left') || '0');
},
update: function(position) {
Element.setStyle(this.element, {
top: this.toTop * position + this.originalTop + 'px',
left: this.toLeft * position + this.originalLeft + 'px'
});
}
});
Effect.Scale = Class.create();
Object.extend(Object.extend(Effect.Scale.prototype, Effect.Base.prototype), {
initialize: function(element, percent) {
this.element = $(element)
var options = Object.extend({
scaleX: true,
scaleY: true,
scaleContent: true,
scaleFromCenter: false,
scaleMode: 'box', // 'box' or 'contents' or {} with provided values
scaleFrom: 100.0,
scaleTo: percent
}, arguments[2] || {});
this.start(options);
},
setup: function() {
this.restoreAfterFinish = this.options.restoreAfterFinish || false;
this.elementPositioning = Element.getStyle(this.element,'position');
this.originalStyle = {};
['top','left','width','height','fontSize'].each( function(k) {
this.originalStyle[k] = this.element.style[k];
}.bind(this));
this.originalTop = this.element.offsetTop;
this.originalLeft = this.element.offsetLeft;
var fontSize = Element.getStyle(this.element,'font-size') || '100%';
['em','px','%'].each( function(fontSizeType) {
if(fontSize.indexOf(fontSizeType)>0) {
this.fontSize = parseFloat(fontSize);
this.fontSizeType = fontSizeType;
}
}.bind(this));
this.factor = (this.options.scaleTo - this.options.scaleFrom)/100;
this.dims = null;
if(this.options.scaleMode=='box')
this.dims = [this.element.offsetHeight, this.element.offsetWidth];
if(/^content/.test(this.options.scaleMode))
this.dims = [this.element.scrollHeight, this.element.scrollWidth];
if(!this.dims)
this.dims = [this.options.scaleMode.originalHeight,
this.options.scaleMode.originalWidth];
},
update: function(position) {
var currentScale = (this.options.scaleFrom/100.0) + (this.factor * position);
if(this.options.scaleContent && this.fontSize)
Element.setStyle(this.element, {fontSize: this.fontSize * currentScale + this.fontSizeType });
this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale);
},
finish: function(position) {
if (this.restoreAfterFinish) Element.setStyle(this.element, this.originalStyle);
},
setDimensions: function(height, width) {
var d = {};
if(this.options.scaleX) d.width = width + 'px';
if(this.options.scaleY) d.height = height + 'px';
if(this.options.scaleFromCenter) {
var topd = (height - this.dims[0])/2;
var leftd = (width - this.dims[1])/2;
if(this.elementPositioning == 'absolute') {
if(this.options.scaleY) d.top = this.originalTop-topd + 'px';
if(this.options.scaleX) d.left = this.originalLeft-leftd + 'px';
} else {
if(this.options.scaleY) d.top = -topd + 'px';
if(this.options.scaleX) d.left = -leftd + 'px';
}
}
Element.setStyle(this.element, d);
}
});
Effect.Highlight = Class.create();
Object.extend(Object.extend(Effect.Highlight.prototype, Effect.Base.prototype), {
initialize: function(element) {
this.element = $(element);
var options = Object.extend({ startcolor: '#ffff99' }, arguments[1] || {});
this.start(options);
},
setup: function() {
// Prevent executing on elements not in the layout flow
if(Element.getStyle(this.element, 'display')=='none') { this.cancel(); return; }
// Disable background image during the effect
this.oldStyle = {
backgroundImage: Element.getStyle(this.element, 'background-image') };
Element.setStyle(this.element, {backgroundImage: 'none'});
if(!this.options.endcolor)
this.options.endcolor = Element.getStyle(this.element, 'background-color').parseColor('#ffffff');
if(!this.options.restorecolor)
this.options.restorecolor = Element.getStyle(this.element, 'background-color');
// init color calculations
this._base = $R(0,2).map(function(i){ return parseInt(this.options.startcolor.slice(i*2+1,i*2+3),16) }.bind(this));
this._delta = $R(0,2).map(function(i){ return parseInt(this.options.endcolor.slice(i*2+1,i*2+3),16)-this._base[i] }.bind(this));
},
update: function(position) {
Element.setStyle(this.element,{backgroundColor: $R(0,2).inject('#',function(m,v,i){
return m+(Math.round(this._base[i]+(this._delta[i]*position)).toColorPart()); }.bind(this)) });
},
finish: function() {
Element.setStyle(this.element, Object.extend(this.oldStyle, {
backgroundColor: this.options.restorecolor
}));
}
});
Effect.ScrollTo = Class.create();
Object.extend(Object.extend(Effect.ScrollTo.prototype, Effect.Base.prototype), {
initialize: function(element) {
this.element = $(element);
this.start(arguments[1] || {});
},
setup: function() {
Position.prepare();
var offsets = Position.cumulativeOffset(this.element);
if(this.options.offset) offsets[1] += this.options.offset;
var max = window.innerHeight ?
window.height - window.innerHeight :
document.body.scrollHeight -
(document.documentElement.clientHeight ?
document.documentElement.clientHeight : document.body.clientHeight);
this.scrollStart = Position.deltaY;
this.delta = (offsets[1] > max ? max : offsets[1]) - this.scrollStart;
},
update: function(position) {
Position.prepare();
window.scrollTo(Position.deltaX,
this.scrollStart + (position*this.delta));
}
});
/* ------------- combination effects ------------- */
Effect.Fade = function(element) {
var oldOpacity = Element.getInlineOpacity(element);
var options = Object.extend({
from: Element.getOpacity(element) || 1.0,
to: 0.0,
afterFinishInternal: function(effect) { with(Element) {
if(effect.options.to!=0) return;
hide(effect.element);
setStyle(effect.element, {opacity: oldOpacity}); }}
}, arguments[1] || {});
return new Effect.Opacity(element,options);
}
Effect.Appear = function(element) {
var options = Object.extend({
from: (Element.getStyle(element, 'display') == 'none' ? 0.0 : Element.getOpacity(element) || 0.0),
to: 1.0,
beforeSetup: function(effect) { with(Element) {
setOpacity(effect.element, effect.options.from);
show(effect.element); }}
}, arguments[1] || {});
return new Effect.Opacity(element,options);
}
Effect.Puff = function(element) {
element = $(element);
var oldStyle = { opacity: Element.getInlineOpacity(element), position: Element.getStyle(element, 'position') };
return new Effect.Parallel(
[ new Effect.Scale(element, 200,
{ sync: true, scaleFromCenter: true, scaleContent: true, restoreAfterFinish: true }),
new Effect.Opacity(element, { sync: true, to: 0.0 } ) ],
Object.extend({ duration: 1.0,
beforeSetupInternal: function(effect) { with(Element) {
setStyle(effect.effects[0].element, {position: 'absolute'}); }},
afterFinishInternal: function(effect) { with(Element) {
hide(effect.effects[0].element);
setStyle(effect.effects[0].element, oldStyle); }}
}, arguments[1] || {})
);
}
Effect.BlindUp = function(element) {
element = $(element);
Element.makeClipping(element);
return new Effect.Scale(element, 0,
Object.extend({ scaleContent: false,
scaleX: false,
restoreAfterFinish: true,
afterFinishInternal: function(effect) { with(Element) {
[hide, undoClipping].call(effect.element); }}
}, arguments[1] || {})
);
}
Effect.BlindDown = function(element) {
element = $(element);
var oldHeight = Element.getStyle(element, 'height');
var elementDimensions = Element.getDimensions(element);
return new Effect.Scale(element, 100,
Object.extend({ scaleContent: false,
scaleX: false,
scaleFrom: 0,
scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
restoreAfterFinish: true,
afterSetup: function(effect) { with(Element) {
makeClipping(effect.element);
setStyle(effect.element, {height: '0px'});
show(effect.element);
}},
afterFinishInternal: function(effect) { with(Element) {
undoClipping(effect.element);
setStyle(effect.element, {height: oldHeight});
}}
}, arguments[1] || {})
);
}
Effect.SwitchOff = function(element) {
element = $(element);
var oldOpacity = Element.getInlineOpacity(element);
return new Effect.Appear(element, {
duration: 0.4,
from: 0,
transition: Effect.Transitions.flicker,
afterFinishInternal: function(effect) {
new Effect.Scale(effect.element, 1, {
duration: 0.3, scaleFromCenter: true,
scaleX: false, scaleContent: false, restoreAfterFinish: true,
beforeSetup: function(effect) { with(Element) {
[makePositioned,makeClipping].call(effect.element);
}},
afterFinishInternal: function(effect) { with(Element) {
[hide,undoClipping,undoPositioned].call(effect.element);
setStyle(effect.element, {opacity: oldOpacity});
}}
})
}
});
}
Effect.DropOut = function(element) {
element = $(element);
var oldStyle = {
top: Element.getStyle(element, 'top'),
left: Element.getStyle(element, 'left'),
opacity: Element.getInlineOpacity(element) };
return new Effect.Parallel(
[ new Effect.MoveBy(element, 100, 0, { sync: true }),
new Effect.Opacity(element, { sync: true, to: 0.0 }) ],
Object.extend(
{ duration: 0.5,
beforeSetup: function(effect) { with(Element) {
makePositioned(effect.effects[0].element); }},
afterFinishInternal: function(effect) { with(Element) {
[hide, undoPositioned].call(effect.effects[0].element);
setStyle(effect.effects[0].element, oldStyle); }}
}, arguments[1] || {}));
}
Effect.Shake = function(element) {
element = $(element);
var oldStyle = {
top: Element.getStyle(element, 'top'),
left: Element.getStyle(element, 'left') };
return new Effect.MoveBy(element, 0, 20,
{ duration: 0.05, afterFinishInternal: function(effect) {
new Effect.MoveBy(effect.element, 0, -40,
{ duration: 0.1, afterFinishInternal: function(effect) {
new Effect.MoveBy(effect.element, 0, 40,
{ duration: 0.1, afterFinishInternal: function(effect) {
new Effect.MoveBy(effect.element, 0, -40,
{ duration: 0.1, afterFinishInternal: function(effect) {
new Effect.MoveBy(effect.element, 0, 40,
{ duration: 0.1, afterFinishInternal: function(effect) {
new Effect.MoveBy(effect.element, 0, -20,
{ duration: 0.05, afterFinishInternal: function(effect) { with(Element) {
undoPositioned(effect.element);
setStyle(effect.element, oldStyle);
}}}) }}) }}) }}) }}) }});
}
Effect.SlideDown = function(element) {
element = $(element);
Element.cleanWhitespace(element);
// SlideDown need to have the content of the element wrapped in a container element with fixed height!
var oldInnerBottom = Element.getStyle(element.firstChild, 'bottom');
var elementDimensions = Element.getDimensions(element);
return new Effect.Scale(element, 100, Object.extend({
scaleContent: false,
scaleX: false,
scaleFrom: 0,
scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
restoreAfterFinish: true,
afterSetup: function(effect) { with(Element) {
makePositioned(effect.element);
makePositioned(effect.element.firstChild);
if(window.opera) setStyle(effect.element, {top: ''});
makeClipping(effect.element);
setStyle(effect.element, {height: '0px'});
show(element); }},
afterUpdateInternal: function(effect) { with(Element) {
setStyle(effect.element.firstChild, {bottom:
(effect.dims[0] - effect.element.clientHeight) + 'px' }); }},
afterFinishInternal: function(effect) { with(Element) {
undoClipping(effect.element);
undoPositioned(effect.element.firstChild);
undoPositioned(effect.element);
setStyle(effect.element.firstChild, {bottom: oldInnerBottom}); }}
}, arguments[1] || {})
);
}
Effect.SlideUp = function(element) {
element = $(element);
Element.cleanWhitespace(element);
var oldInnerBottom = Element.getStyle(element.firstChild, 'bottom');
return new Effect.Scale(element, 0,
Object.extend({ scaleContent: false,
scaleX: false,
scaleMode: 'box',
scaleFrom: 100,
restoreAfterFinish: true,
beforeStartInternal: function(effect) { with(Element) {
makePositioned(effect.element);
makePositioned(effect.element.firstChild);
if(window.opera) setStyle(effect.element, {top: ''});
makeClipping(effect.element);
show(element); }},
afterUpdateInternal: function(effect) { with(Element) {
setStyle(effect.element.firstChild, {bottom:
(effect.dims[0] - effect.element.clientHeight) + 'px' }); }},
afterFinishInternal: function(effect) { with(Element) {
[hide, undoClipping].call(effect.element);
undoPositioned(effect.element.firstChild);
undoPositioned(effect.element);
setStyle(effect.element.firstChild, {bottom: oldInnerBottom}); }}
}, arguments[1] || {})
);
}
// Bug in opera makes the TD containing this element expand for a instance after finish
Effect.Squish = function(element) {
return new Effect.Scale(element, window.opera ? 1 : 0,
{ restoreAfterFinish: true,
beforeSetup: function(effect) { with(Element) {
makeClipping(effect.element); }},
afterFinishInternal: function(effect) { with(Element) {
hide(effect.element);
undoClipping(effect.element); }}
});
}
Effect.Grow = function(element) {
element = $(element);
var options = Object.extend({
direction: 'center',
moveTransistion: Effect.Transitions.sinoidal,
scaleTransition: Effect.Transitions.sinoidal,
opacityTransition: Effect.Transitions.full
}, arguments[1] || {});
var oldStyle = {
top: element.style.top,
left: element.style.left,
height: element.style.height,
width: element.style.width,
opacity: Element.getInlineOpacity(element) };
var dims = Element.getDimensions(element);
var initialMoveX, initialMoveY;
var moveX, moveY;
switch (options.direction) {
case 'top-left':
initialMoveX = initialMoveY = moveX = moveY = 0;
break;
case 'top-right':
initialMoveX = dims.width;
initialMoveY = moveY = 0;
moveX = -dims.width;
break;
case 'bottom-left':
initialMoveX = moveX = 0;
initialMoveY = dims.height;
moveY = -dims.height;
break;
case 'bottom-right':
initialMoveX = dims.width;
initialMoveY = dims.height;
moveX = -dims.width;
moveY = -dims.height;
break;
case 'center':
initialMoveX = dims.width / 2;
initialMoveY = dims.height / 2;
moveX = -dims.width / 2;
moveY = -dims.height / 2;
break;
}
return new Effect.MoveBy(element, initialMoveY, initialMoveX, {
duration: 0.01,
beforeSetup: function(effect) { with(Element) {
hide(effect.element);
makeClipping(effect.element);
makePositioned(effect.element);
}},
afterFinishInternal: function(effect) {
new Effect.Parallel(
[ new Effect.Opacity(effect.element, { sync: true, to: 1.0, from: 0.0, transition: options.opacityTransition }),
new Effect.MoveBy(effect.element, moveY, moveX, { sync: true, transition: options.moveTransition }),
new Effect.Scale(effect.element, 100, {
scaleMode: { originalHeight: dims.height, originalWidth: dims.width },
sync: true, scaleFrom: window.opera ? 1 : 0, transition: options.scaleTransition, restoreAfterFinish: true})
], Object.extend({
beforeSetup: function(effect) { with(Element) {
setStyle(effect.effects[0].element, {height: '0px'});
show(effect.effects[0].element); }},
afterFinishInternal: function(effect) { with(Element) {
[undoClipping, undoPositioned].call(effect.effects[0].element);
setStyle(effect.effects[0].element, oldStyle); }}
}, options)
)
}
});
}
Effect.Shrink = function(element) {
element = $(element);
var options = Object.extend({
direction: 'center',
moveTransistion: Effect.Transitions.sinoidal,
scaleTransition: Effect.Transitions.sinoidal,
opacityTransition: Effect.Transitions.none
}, arguments[1] || {});
var oldStyle = {
top: element.style.top,
left: element.style.left,
height: element.style.height,
width: element.style.width,
opacity: Element.getInlineOpacity(element) };
var dims = Element.getDimensions(element);
var moveX, moveY;
switch (options.direction) {
case 'top-left':
moveX = moveY = 0;
break;
case 'top-right':
moveX = dims.width;
moveY = 0;
break;
case 'bottom-left':
moveX = 0;
moveY = dims.height;
break;
case 'bottom-right':
moveX = dims.width;
moveY = dims.height;
break;
case 'center':
moveX = dims.width / 2;
moveY = dims.height / 2;
break;
}
return new Effect.Parallel(
[ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: options.opacityTransition }),
new Effect.Scale(element, window.opera ? 1 : 0, { sync: true, transition: options.scaleTransition, restoreAfterFinish: true}),
new Effect.MoveBy(element, moveY, moveX, { sync: true, transition: options.moveTransition })
], Object.extend({
beforeStartInternal: function(effect) { with(Element) {
[makePositioned, makeClipping].call(effect.effects[0].element) }},
afterFinishInternal: function(effect) { with(Element) {
[hide, undoClipping, undoPositioned].call(effect.effects[0].element);
setStyle(effect.effects[0].element, oldStyle); }}
}, options)
);
}
Effect.Pulsate = function(element) {
element = $(element);
var options = arguments[1] || {};
var oldOpacity = Element.getInlineOpacity(element);
var transition = options.transition || Effect.Transitions.sinoidal;
var reverser = function(pos){ return transition(1-Effect.Transitions.pulse(pos)) };
reverser.bind(transition);
return new Effect.Opacity(element,
Object.extend(Object.extend({ duration: 3.0, from: 0,
afterFinishInternal: function(effect) { Element.setStyle(effect.element, {opacity: oldOpacity}); }
}, options), {transition: reverser}));
}
Effect.Fold = function(element) {
element = $(element);
var oldStyle = {
top: element.style.top,
left: element.style.left,
width: element.style.width,
height: element.style.height };
Element.makeClipping(element);
return new Effect.Scale(element, 5, Object.extend({
scaleContent: false,
scaleX: false,
afterFinishInternal: function(effect) {
new Effect.Scale(element, 1, {
scaleContent: false,
scaleY: false,
afterFinishInternal: function(effect) { with(Element) {
[hide, undoClipping].call(effect.element);
setStyle(effect.element, oldStyle);
}} });
}}, arguments[1] || {}));
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1 @@
# See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file

View File

@ -0,0 +1,3 @@
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../config/boot'
require 'commands/about'

View File

@ -0,0 +1,3 @@
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../config/boot'
require 'commands/breakpointer'

View File

@ -0,0 +1,3 @@
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../config/boot'
require 'commands/console'

View File

@ -0,0 +1,3 @@
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../config/boot'
require 'commands/destroy'

View File

@ -0,0 +1,3 @@
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../config/boot'
require 'commands/generate'

View File

@ -0,0 +1,3 @@
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../../config/boot'
require 'commands/performance/benchmarker'

View File

@ -0,0 +1,3 @@
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../../config/boot'
require 'commands/performance/profiler'

View File

@ -0,0 +1,3 @@
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../config/boot'
require 'commands/plugin'

View File

@ -0,0 +1,3 @@
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../../config/boot'
require 'commands/process/reaper'

View File

@ -0,0 +1,3 @@
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../../config/boot'
require 'commands/process/spawner'

View File

@ -0,0 +1,3 @@
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../../config/boot'
require 'commands/process/spinner'

View File

@ -0,0 +1,3 @@
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../config/boot'
require 'commands/runner'

View File

@ -0,0 +1,3 @@
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../config/boot'
require 'commands/server'

View File

@ -0,0 +1,18 @@
require File.dirname(__FILE__) + '/../test_helper'
require 'login_controller'
# Re-raise errors caught by the controller.
class LoginController; def rescue_action(e) raise e end; end
class LoginControllerTest < Test::Unit::TestCase
def setup
@controller = LoginController.new
@request = ActionController::TestRequest.new
@response = ActionController::TestResponse.new
end
# Replace this with your real tests.
def test_truth
assert true
end
end

View File

@ -0,0 +1,18 @@
require File.dirname(__FILE__) + '/../test_helper'
require 'server_controller'
# Re-raise errors caught by the controller.
class ServerController; def rescue_action(e) raise e end; end
class ServerControllerTest < Test::Unit::TestCase
def setup
@controller = ServerController.new
@request = ActionController::TestRequest.new
@response = ActionController::TestResponse.new
end
# Replace this with your real tests.
def test_truth
assert true
end
end

View File

@ -0,0 +1,28 @@
ENV["RAILS_ENV"] = "test"
require File.expand_path(File.dirname(__FILE__) + "/../config/environment")
require 'test_help'
class Test::Unit::TestCase
# Transactional fixtures accelerate your tests by wrapping each test method
# in a transaction that's rolled back on completion. This ensures that the
# test database remains unchanged so your fixtures don't have to be reloaded
# between every test method. Fewer database queries means faster tests.
#
# Read Mike Clark's excellent walkthrough at
# http://clarkware.com/cgi/blosxom/2005/10/24#Rails10FastTesting
#
# Every Active Record database supports transactions except MyISAM tables
# in MySQL. Turn off transactional fixtures in this case; however, if you
# don't care one way or the other, switching from MyISAM to InnoDB tables
# is recommended.
self.use_transactional_fixtures = true
# Instantiated fixtures are slow, but give you @david where otherwise you
# would need people(:david). If you don't want to migrate your existing
# test cases which use the @david style and don't mind the speed hit (each
# instantiated fixtures translates to a database query per test method),
# then set this back to true.
self.use_instantiated_fixtures = false
# Add more helper methods to be used by all tests here...
end

View File

@ -0,0 +1,112 @@
# Copyright (C) 2001 Daiki Ueno <ueno@unixuser.org>
# This library is distributed under the terms of the Ruby license.
# This module provides common interface to HMAC engines.
# HMAC standard is documented in RFC 2104:
#
# H. Krawczyk et al., "HMAC: Keyed-Hashing for Message Authentication",
# RFC 2104, February 1997
#
# These APIs are inspired by JCE 1.2's javax.crypto.Mac interface.
#
# <URL:http://java.sun.com/security/JCE1.2/spec/apidoc/javax/crypto/Mac.html>
module HMAC
class Base
def initialize(algorithm, block_size, output_length, key)
@algorithm = algorithm
@block_size = block_size
@output_length = output_length
@status = STATUS_UNDEFINED
@key_xor_ipad = ''
@key_xor_opad = ''
set_key(key) unless key.nil?
end
private
def check_status
unless @status == STATUS_INITIALIZED
raise RuntimeError,
"The underlying hash algorithm has not yet been initialized."
end
end
public
def set_key(key)
# If key is longer than the block size, apply hash function
# to key and use the result as a real key.
key = @algorithm.digest(key) if key.size > @block_size
key_xor_ipad = "\x36" * @block_size
key_xor_opad = "\x5C" * @block_size
for i in 0 .. key.size - 1
key_xor_ipad[i] ^= key[i]
key_xor_opad[i] ^= key[i]
end
@key_xor_ipad = key_xor_ipad
@key_xor_opad = key_xor_opad
@md = @algorithm.new
@status = STATUS_INITIALIZED
end
def reset_key
@key_xor_ipad.gsub!(/./, '?')
@key_xor_opad.gsub!(/./, '?')
@key_xor_ipad[0..-1] = ''
@key_xor_opad[0..-1] = ''
@status = STATUS_UNDEFINED
end
def update(text)
check_status
# perform inner H
md = @algorithm.new
md.update(@key_xor_ipad)
md.update(text)
str = md.digest
# perform outer H
md = @algorithm.new
md.update(@key_xor_opad)
md.update(str)
@md = md
end
alias << update
def digest
check_status
@md.digest
end
def hexdigest
check_status
@md.hexdigest
end
alias to_s hexdigest
# These two class methods below are safer than using above
# instance methods combinatorially because an instance will have
# held a key even if it's no longer in use.
def Base.digest(key, text)
begin
hmac = self.new(key)
hmac.update(text)
hmac.digest
ensure
hmac.reset_key
end
end
def Base.hexdigest(key, text)
begin
hmac = self.new(key)
hmac.update(text)
hmac.hexdigest
ensure
hmac.reset_key
end
end
private_class_method :new, :digest, :hexdigest
end
STATUS_UNDEFINED, STATUS_INITIALIZED = 0, 1
end

View File

@ -0,0 +1,11 @@
require 'hmac/hmac'
require 'digest/sha1'
module HMAC
class SHA1 < Base
def initialize(key = nil)
super(Digest::SHA1, 64, 20, key)
end
public_class_method :new, :digest, :hexdigest
end
end

View File

@ -0,0 +1,25 @@
require 'hmac/hmac'
require 'digest/sha2'
module HMAC
class SHA256 < Base
def initialize(key = nil)
super(Digest::SHA256, 64, 32, key)
end
public_class_method :new, :digest, :hexdigest
end
class SHA384 < Base
def initialize(key = nil)
super(Digest::SHA384, 128, 48, key)
end
public_class_method :new, :digest, :hexdigest
end
class SHA512 < Base
def initialize(key = nil)
super(Digest::SHA512, 128, 64, key)
end
public_class_method :new, :digest, :hexdigest
end
end

View File

@ -0,0 +1,20 @@
# Copyright 2006-2007 JanRain, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you
# may not use this file except in compliance with the License. You may
# obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied. See the License for the specific language governing
# permissions and limitations under the License.
module OpenID
VERSION = "2.1.4"
end
require "openid/consumer"
require 'openid/server'

View File

@ -0,0 +1,249 @@
require "openid/kvform"
require "openid/util"
require "openid/cryptutil"
require "openid/message"
module OpenID
def self.get_secret_size(assoc_type)
if assoc_type == 'HMAC-SHA1'
return 20
elsif assoc_type == 'HMAC-SHA256'
return 32
else
raise ArgumentError("Unsupported association type: #{assoc_type}")
end
end
# An Association holds the shared secret between a relying party and
# an OpenID provider.
class Association
attr_reader :handle, :secret, :issued, :lifetime, :assoc_type
FIELD_ORDER =
[:version, :handle, :secret, :issued, :lifetime, :assoc_type,]
# Load a serialized Association
def self.deserialize(serialized)
parsed = Util.kv_to_seq(serialized)
parsed_fields = parsed.map{|k, v| k.to_sym}
if parsed_fields != FIELD_ORDER
raise ProtocolError, 'Unexpected fields in serialized association'\
" (Expected #{FIELD_ORDER.inspect}, got #{parsed_fields.inspect})"
end
version, handle, secret64, issued_s, lifetime_s, assoc_type =
parsed.map {|field, value| value}
if version != '2'
raise ProtocolError, "Attempted to deserialize unsupported version "\
"(#{parsed[0][1].inspect})"
end
self.new(handle,
Util.from_base64(secret64),
Time.at(issued_s.to_i),
lifetime_s.to_i,
assoc_type)
end
# Create an Association with an issued time of now
def self.from_expires_in(expires_in, handle, secret, assoc_type)
issued = Time.now
self.new(handle, secret, issued, expires_in, assoc_type)
end
def initialize(handle, secret, issued, lifetime, assoc_type)
@handle = handle
@secret = secret
@issued = issued
@lifetime = lifetime
@assoc_type = assoc_type
end
# Serialize the association to a form that's consistent across
# JanRain OpenID libraries.
def serialize
data = {
:version => '2',
:handle => handle,
:secret => Util.to_base64(secret),
:issued => issued.to_i.to_s,
:lifetime => lifetime.to_i.to_s,
:assoc_type => assoc_type,
}
Util.assert(data.length == FIELD_ORDER.length)
pairs = FIELD_ORDER.map{|field| [field.to_s, data[field]]}
return Util.seq_to_kv(pairs, strict=true)
end
# The number of seconds until this association expires
def expires_in(now=nil)
if now.nil?
now = Time.now.to_i
else
now = now.to_i
end
time_diff = (issued.to_i + lifetime) - now
if time_diff < 0
return 0
else
return time_diff
end
end
# Generate a signature for a sequence of [key, value] pairs
def sign(pairs)
kv = Util.seq_to_kv(pairs)
case assoc_type
when 'HMAC-SHA1'
CryptUtil.hmac_sha1(@secret, kv)
when 'HMAC-SHA256'
CryptUtil.hmac_sha256(@secret, kv)
else
raise ProtocolError, "Association has unknown type: "\
"#{assoc_type.inspect}"
end
end
# Generate the list of pairs that form the signed elements of the
# given message
def make_pairs(message)
signed = message.get_arg(OPENID_NS, 'signed')
if signed.nil?
raise ProtocolError, 'Missing signed list'
end
signed_fields = signed.split(',', -1)
data = message.to_post_args
signed_fields.map {|field| [field, data.fetch('openid.'+field,'')] }
end
# Return whether the message's signature passes
def check_message_signature(message)
message_sig = message.get_arg(OPENID_NS, 'sig')
if message_sig.nil?
raise ProtocolError, "#{message} has no sig."
end
calculated_sig = get_message_signature(message)
return calculated_sig == message_sig
end
# Get the signature for this message
def get_message_signature(message)
Util.to_base64(sign(make_pairs(message)))
end
def ==(other)
(other.class == self.class and
other.handle == self.handle and
other.secret == self.secret and
# The internals of the time objects seemed to differ
# in an opaque way when serializing/unserializing.
# I don't think this will be a problem.
other.issued.to_i == self.issued.to_i and
other.lifetime == self.lifetime and
other.assoc_type == self.assoc_type)
end
# Add a signature (and a signed list) to a message.
def sign_message(message)
if (message.has_key?(OPENID_NS, 'sig') or
message.has_key?(OPENID_NS, 'signed'))
raise ArgumentError, 'Message already has signed list or signature'
end
extant_handle = message.get_arg(OPENID_NS, 'assoc_handle')
if extant_handle and extant_handle != self.handle
raise ArgumentError, "Message has a different association handle"
end
signed_message = message.copy()
signed_message.set_arg(OPENID_NS, 'assoc_handle', self.handle)
message_keys = signed_message.to_post_args.keys()
signed_list = []
message_keys.each { |k|
if k.starts_with?('openid.')
signed_list << k[7..-1]
end
}
signed_list << 'signed'
signed_list.sort!
signed_message.set_arg(OPENID_NS, 'signed', signed_list.join(','))
sig = get_message_signature(signed_message)
signed_message.set_arg(OPENID_NS, 'sig', sig)
return signed_message
end
end
class AssociationNegotiator
attr_reader :allowed_types
def self.get_session_types(assoc_type)
case assoc_type
when 'HMAC-SHA1'
['DH-SHA1', 'no-encryption']
when 'HMAC-SHA256'
['DH-SHA256', 'no-encryption']
else
raise ProtocolError, "Unknown association type #{assoc_type.inspect}"
end
end
def self.check_session_type(assoc_type, session_type)
if !get_session_types(assoc_type).include?(session_type)
raise ProtocolError, "Session type #{session_type.inspect} not "\
"valid for association type #{assoc_type.inspect}"
end
end
def initialize(allowed_types)
self.allowed_types=(allowed_types)
end
def copy
Marshal.load(Marshal.dump(self))
end
def allowed_types=(allowed_types)
allowed_types.each do |assoc_type, session_type|
self.class.check_session_type(assoc_type, session_type)
end
@allowed_types = allowed_types
end
def add_allowed_type(assoc_type, session_type=nil)
if session_type.nil?
session_types = self.class.get_session_types(assoc_type)
else
self.class.check_session_type(assoc_type, session_type)
session_types = [session_type]
end
for session_type in session_types do
@allowed_types << [assoc_type, session_type]
end
end
def allowed?(assoc_type, session_type)
@allowed_types.include?([assoc_type, session_type])
end
def get_allowed_type
@allowed_types.empty? ? nil : @allowed_types[0]
end
end
DefaultNegotiator =
AssociationNegotiator.new([['HMAC-SHA1', 'DH-SHA1'],
['HMAC-SHA1', 'no-encryption'],
['HMAC-SHA256', 'DH-SHA256'],
['HMAC-SHA256', 'no-encryption']])
EncryptedNegotiator =
AssociationNegotiator.new([['HMAC-SHA1', 'DH-SHA1'],
['HMAC-SHA256', 'DH-SHA256']])
end

View File

@ -0,0 +1,395 @@
require "openid/consumer/idres.rb"
require "openid/consumer/checkid_request.rb"
require "openid/consumer/associationmanager.rb"
require "openid/consumer/responses.rb"
require "openid/consumer/discovery_manager"
require "openid/consumer/discovery"
require "openid/message"
require "openid/yadis/discovery"
require "openid/store/nonce"
module OpenID
# OpenID support for Relying Parties (aka Consumers).
#
# This module documents the main interface with the OpenID consumer
# library. The only part of the library which has to be used and
# isn't documented in full here is the store required to create an
# Consumer instance.
#
# = OVERVIEW
#
# The OpenID identity verification process most commonly uses the
# following steps, as visible to the user of this library:
#
# 1. The user enters their OpenID into a field on the consumer's
# site, and hits a login button.
#
# 2. The consumer site discovers the user's OpenID provider using
# the Yadis protocol.
#
# 3. The consumer site sends the browser a redirect to the OpenID
# provider. This is the authentication request as described in
# the OpenID specification.
#
# 4. The OpenID provider's site sends the browser a redirect back to
# the consumer site. This redirect contains the provider's
# response to the authentication request.
#
# The most important part of the flow to note is the consumer's site
# must handle two separate HTTP requests in order to perform the
# full identity check.
#
# = LIBRARY DESIGN
#
# This consumer library is designed with that flow in mind. The
# goal is to make it as easy as possible to perform the above steps
# securely.
#
# At a high level, there are two important parts in the consumer
# library. The first important part is this module, which contains
# the interface to actually use this library. The second is
# openid/store/interface.rb, which describes the interface to use if
# you need to create a custom method for storing the state this
# library needs to maintain between requests.
#
# In general, the second part is less important for users of the
# library to know about, as several implementations are provided
# which cover a wide variety of situations in which consumers may
# use the library.
#
# The Consumer class has methods corresponding to the actions
# necessary in each of steps 2, 3, and 4 described in the overview.
# Use of this library should be as easy as creating an Consumer
# instance and calling the methods appropriate for the action the
# site wants to take.
#
# This library automatically detects which version of the OpenID
# protocol should be used for a transaction and constructs the
# proper requests and responses. Users of this library do not need
# to worry about supporting multiple protocol versions; the library
# supports them implicitly. Depending on the version of the
# protocol in use, the OpenID transaction may be more secure. See
# the OpenID specifications for more information.
#
# = SESSIONS, STORES, AND STATELESS MODE
#
# The Consumer object keeps track of two types of state:
#
# 1. State of the user's current authentication attempt. Things
# like the identity URL, the list of endpoints discovered for
# that URL, and in case where some endpoints are unreachable, the
# list of endpoints already tried. This state needs to be held
# from Consumer.begin() to Consumer.complete(), but it is only
# applicable to a single session with a single user agent, and at
# the end of the authentication process (i.e. when an OP replies
# with either <tt>id_res</tt>. or <tt>cancel</tt> it may be
# discarded.
#
# 2. State of relationships with servers, i.e. shared secrets
# (associations) with servers and nonces seen on signed messages.
# This information should persist from one session to the next
# and should not be bound to a particular user-agent.
#
# These two types of storage are reflected in the first two
# arguments of Consumer's constructor, <tt>session</tt> and
# <tt>store</tt>. <tt>session</tt> is a dict-like object and we
# hope your web framework provides you with one of these bound to
# the user agent. <tt>store</tt> is an instance of Store.
#
# Since the store does hold secrets shared between your application
# and the OpenID provider, you should be careful about how you use
# it in a shared hosting environment. If the filesystem or database
# permissions of your web host allow strangers to read from them, do
# not store your data there! If you have no safe place to store
# your data, construct your consumer with nil for the store, and it
# will operate only in stateless mode. Stateless mode may be
# slower, put more load on the OpenID provider, and trusts the
# provider to keep you safe from replay attacks.
#
# Several store implementation are provided, and the interface is
# fully documented so that custom stores can be used as well. See
# the documentation for the Consumer class for more information on
# the interface for stores. The implementations that are provided
# allow the consumer site to store the necessary data in several
# different ways, including several SQL databases and normal files
# on disk.
#
# = IMMEDIATE MODE
#
# In the flow described above, the user may need to confirm to the
# OpenID provider that it's ok to disclose his or her identity. The
# provider may draw pages asking for information from the user
# before it redirects the browser back to the consumer's site. This
# is generally transparent to the consumer site, so it is typically
# ignored as an implementation detail.
#
# There can be times, however, where the consumer site wants to get
# a response immediately. When this is the case, the consumer can
# put the library in immediate mode. In immediate mode, there is an
# extra response possible from the server, which is essentially the
# server reporting that it doesn't have enough information to answer
# the question yet.
#
# = USING THIS LIBRARY
#
# Integrating this library into an application is usually a
# relatively straightforward process. The process should basically
# follow this plan:
#
# Add an OpenID login field somewhere on your site. When an OpenID
# is entered in that field and the form is submitted, it should make
# a request to the your site which includes that OpenID URL.
#
# First, the application should instantiate a Consumer with a
# session for per-user state and store for shared state using the
# store of choice.
#
# Next, the application should call the <tt>begin</tt> method of
# Consumer instance. This method takes the OpenID URL as entered by
# the user. The <tt>begin</tt> method returns a CheckIDRequest
# object.
#
# Next, the application should call the redirect_url method on the
# CheckIDRequest object. The parameter <tt>return_to</tt> is the
# URL that the OpenID server will send the user back to after
# attempting to verify his or her identity. The <tt>realm</tt>
# parameter is the URL (or URL pattern) that identifies your web
# site to the user when he or she is authorizing it. Send a
# redirect to the resulting URL to the user's browser.
#
# That's the first half of the authentication process. The second
# half of the process is done after the user's OpenID Provider sends
# the user's browser a redirect back to your site to complete their
# login.
#
# When that happens, the user will contact your site at the URL
# given as the <tt>return_to</tt> URL to the redirect_url call made
# above. The request will have several query parameters added to
# the URL by the OpenID provider as the information necessary to
# finish the request.
#
# Get a Consumer instance with the same session and store as before
# and call its complete() method, passing in all the received query
# arguments and URL currently being handled.
#
# There are multiple possible return types possible from that
# method. These indicate the whether or not the login was
# successful, and include any additional information appropriate for
# their type.
class Consumer
attr_accessor :session_key_prefix
# Initialize a Consumer instance.
#
# You should create a new instance of the Consumer object with
# every HTTP request that handles OpenID transactions.
#
# session: the session object to use to store request information.
# The session should behave like a hash.
#
# store: an object that implements the interface in Store.
def initialize(session, store)
@session = session
@store = store
@session_key_prefix = 'OpenID::Consumer::'
end
# Start the OpenID authentication process. See steps 1-2 in the
# overview for the Consumer class.
#
# user_url: Identity URL given by the user. This method performs a
# textual transformation of the URL to try and make sure it is
# normalized. For example, a user_url of example.com will be
# normalized to http://example.com/ normalizing and resolving any
# redirects the server might issue.
#
# anonymous: A boolean value. Whether to make an anonymous
# request of the OpenID provider. Such a request does not ask for
# an authorization assertion for an OpenID identifier, but may be
# used with extensions to pass other data. e.g. "I don't care who
# you are, but I'd like to know your time zone."
#
# Returns a CheckIDRequest object containing the discovered
# information, with a method for building a redirect URL to the
# server, as described in step 3 of the overview. This object may
# also be used to add extension arguments to the request, using
# its add_extension_arg method.
#
# Raises DiscoveryFailure when no OpenID server can be found for
# this URL.
def begin(openid_identifier, anonymous=false)
manager = discovery_manager(openid_identifier)
service = manager.get_next_service(&method(:discover))
if service.nil?
raise DiscoveryFailure.new("No usable OpenID services were found "\
"for #{openid_identifier.inspect}", nil)
else
begin_without_discovery(service, anonymous)
end
end
# Start OpenID verification without doing OpenID server
# discovery. This method is used internally by Consumer.begin()
# after discovery is performed, and exists to provide an interface
# for library users needing to perform their own discovery.
#
# service: an OpenID service endpoint descriptor. This object and
# factories for it are found in the openid/consumer/discovery.rb
# module.
#
# Returns an OpenID authentication request object.
def begin_without_discovery(service, anonymous)
assoc = association_manager(service).get_association
checkid_request = CheckIDRequest.new(assoc, service)
checkid_request.anonymous = anonymous
if service.compatibility_mode
rt_args = checkid_request.return_to_args
rt_args[Consumer.openid1_return_to_nonce_name] = Nonce.mk_nonce
rt_args[Consumer.openid1_return_to_claimed_id_name] =
service.claimed_id
end
self.last_requested_endpoint = service
return checkid_request
end
# Called to interpret the server's response to an OpenID
# request. It is called in step 4 of the flow described in the
# Consumer overview.
#
# query: A hash of the query parameters for this HTTP request.
# Note that in rails, this is <b>not</b> <tt>params</tt> but
# <tt>params.reject{|k,v|request.path_parameters[k]}</tt>
# because <tt>controller</tt> and <tt>action</tt> and other
# "path parameters" are included in params.
#
# current_url: Extract the URL of the current request from your
# application's web request framework and specify it here to have it
# checked against the openid.return_to value in the response. Do not
# just pass <tt>args['openid.return_to']</tt> here; that will defeat the
# purpose of this check. (See OpenID Authentication 2.0 section 11.1.)
#
# If the return_to URL check fails, the status of the completion will be
# FAILURE.
#
# Returns a subclass of Response. The type of response is
# indicated by the status attribute, which will be one of
# SUCCESS, CANCEL, FAILURE, or SETUP_NEEDED.
def complete(query, current_url)
message = Message.from_post_args(query)
mode = message.get_arg(OPENID_NS, 'mode', 'invalid')
begin
meth = method('complete_' + mode)
rescue NameError
meth = method(:complete_invalid)
end
response = meth.call(message, current_url)
cleanup_last_requested_endpoint
if [SUCCESS, CANCEL].member?(response.status)
cleanup_session
end
return response
end
protected
def session_get(name)
@session[session_key(name)]
end
def session_set(name, val)
@session[session_key(name)] = val
end
def session_key(suffix)
@session_key_prefix + suffix
end
def last_requested_endpoint
session_get('last_requested_endpoint')
end
def last_requested_endpoint=(endpoint)
session_set('last_requested_endpoint', endpoint)
end
def cleanup_last_requested_endpoint
@session[session_key('last_requested_endpoint')] = nil
end
def discovery_manager(openid_identifier)
DiscoveryManager.new(@session, openid_identifier, @session_key_prefix)
end
def cleanup_session
discovery_manager(nil).cleanup(true)
end
def discover(identifier)
OpenID.discover(identifier)
end
def negotiator
DefaultNegotiator
end
def association_manager(service)
AssociationManager.new(@store, service.server_url,
service.compatibility_mode, negotiator)
end
def handle_idres(message, current_url)
IdResHandler.new(message, current_url, @store, last_requested_endpoint)
end
def complete_invalid(message, unused_return_to)
mode = message.get_arg(OPENID_NS, 'mode', '<No mode set>')
return FailureResponse.new(last_requested_endpoint,
"Invalid openid.mode: #{mode}")
end
def complete_cancel(unused_message, unused_return_to)
return CancelResponse.new(last_requested_endpoint)
end
def complete_error(message, unused_return_to)
error = message.get_arg(OPENID_NS, 'error')
contact = message.get_arg(OPENID_NS, 'contact')
reference = message.get_arg(OPENID_NS, 'reference')
return FailureResponse.new(last_requested_endpoint,
error, contact, reference)
end
def complete_setup_needed(message, unused_return_to)
if message.is_openid1
return complete_invalid(message, nil)
else
setup_url = message.get_arg(OPENID2_NS, 'user_setup_url')
return SetupNeededResponse.new(last_requested_endpoint, setup_url)
end
end
def complete_id_res(message, current_url)
if message.is_openid1
setup_url = message.get_arg(OPENID1_NS, 'user_setup_url')
if !setup_url.nil?
return SetupNeededResponse.new(last_requested_endpoint, setup_url)
end
end
begin
idres = handle_idres(message, current_url)
rescue OpenIDError => why
return FailureResponse.new(last_requested_endpoint, why.message)
else
return SuccessResponse.new(idres.endpoint, message,
idres.signed_fields)
end
end
end
end

View File

@ -0,0 +1,340 @@
require "openid/dh"
require "openid/util"
require "openid/kvpost"
require "openid/cryptutil"
require "openid/protocolerror"
require "openid/association"
module OpenID
class Consumer
# A superclass for implementing Diffie-Hellman association sessions.
class DiffieHellmanSession
class << self
attr_reader :session_type, :secret_size, :allowed_assoc_types,
:hashfunc
end
def initialize(dh=nil)
if dh.nil?
dh = DiffieHellman.from_defaults
end
@dh = dh
end
# Return the query parameters for requesting an association
# using this Diffie-Hellman association session
def get_request
args = {'dh_consumer_public' => CryptUtil.num_to_base64(@dh.public)}
if (!@dh.using_default_values?)
args['dh_modulus'] = CryptUtil.num_to_base64(@dh.modulus)
args['dh_gen'] = CryptUtil.num_to_base64(@dh.generator)
end
return args
end
# Process the response from a successful association request and
# return the shared secret for this association
def extract_secret(response)
dh_server_public64 = response.get_arg(OPENID_NS, 'dh_server_public',
NO_DEFAULT)
enc_mac_key64 = response.get_arg(OPENID_NS, 'enc_mac_key', NO_DEFAULT)
dh_server_public = CryptUtil.base64_to_num(dh_server_public64)
enc_mac_key = Util.from_base64(enc_mac_key64)
return @dh.xor_secret(self.class.hashfunc,
dh_server_public, enc_mac_key)
end
end
# A Diffie-Hellman association session that uses SHA1 as its hash
# function
class DiffieHellmanSHA1Session < DiffieHellmanSession
@session_type = 'DH-SHA1'
@secret_size = 20
@allowed_assoc_types = ['HMAC-SHA1']
@hashfunc = CryptUtil.method(:sha1)
end
# A Diffie-Hellman association session that uses SHA256 as its hash
# function
class DiffieHellmanSHA256Session < DiffieHellmanSession
@session_type = 'DH-SHA256'
@secret_size = 32
@allowed_assoc_types = ['HMAC-SHA256']
@hashfunc = CryptUtil.method(:sha256)
end
# An association session that does not use encryption
class NoEncryptionSession
class << self
attr_reader :session_type, :allowed_assoc_types
end
@session_type = 'no-encryption'
@allowed_assoc_types = ['HMAC-SHA1', 'HMAC-SHA256']
def get_request
return {}
end
def extract_secret(response)
mac_key64 = response.get_arg(OPENID_NS, 'mac_key', NO_DEFAULT)
return Util.from_base64(mac_key64)
end
end
# An object that manages creating and storing associations for an
# OpenID provider endpoint
class AssociationManager
def self.create_session(session_type)
case session_type
when 'no-encryption'
NoEncryptionSession.new
when 'DH-SHA1'
DiffieHellmanSHA1Session.new
when 'DH-SHA256'
DiffieHellmanSHA256Session.new
else
raise ArgumentError, "Unknown association session type: "\
"#{session_type.inspect}"
end
end
def initialize(store, server_url, compatibility_mode=false,
negotiator=nil)
@store = store
@server_url = server_url
@compatibility_mode = compatibility_mode
@negotiator = negotiator || DefaultNegotiator
end
def get_association
if @store.nil?
return nil
end
assoc = @store.get_association(@server_url)
if assoc.nil? || assoc.expires_in <= 0
assoc = negotiate_association
if !assoc.nil?
@store.store_association(@server_url, assoc)
end
end
return assoc
end
def negotiate_association
assoc_type, session_type = @negotiator.get_allowed_type
begin
return request_association(assoc_type, session_type)
rescue ServerError => why
supported_types = extract_supported_association_type(why, assoc_type)
if !supported_types.nil?
# Attempt to create an association from the assoc_type and
# session_type that the server told us it supported.
assoc_type, session_type = supported_types
begin
return request_association(assoc_type, session_type)
rescue ServerError => why
Util.log("Server #{@server_url} refused its suggested " \
"association type: session_type=#{session_type}, " \
"assoc_type=#{assoc_type}")
return nil
end
end
end
end
protected
def extract_supported_association_type(server_error, assoc_type)
# Any error message whose code is not 'unsupported-type' should
# be considered a total failure.
if (server_error.error_code != 'unsupported-type' or
server_error.message.is_openid1)
Util.log("Server error when requesting an association from "\
"#{@server_url}: #{server_error.error_text}")
return nil
end
# The server didn't like the association/session type that we
# sent, and it sent us back a message that might tell us how to
# handle it.
Util.log("Unsupported association type #{assoc_type}: "\
"#{server_error.error_text}")
# Extract the session_type and assoc_type from the error message
assoc_type = server_error.message.get_arg(OPENID_NS, 'assoc_type')
session_type = server_error.message.get_arg(OPENID_NS, 'session_type')
if assoc_type.nil? or session_type.nil?
Util.log("Server #{@server_url} responded with unsupported "\
"association session but did not supply a fallback.")
return nil
elsif !@negotiator.allowed?(assoc_type, session_type)
Util.log("Server sent unsupported session/association type: "\
"session_type=#{session_type}, assoc_type=#{assoc_type}")
return nil
else
return [assoc_type, session_type]
end
end
# Make and process one association request to this endpoint's OP
# endpoint URL. Returns an association object or nil if the
# association processing failed. Raises ServerError when the
# remote OpenID server returns an error.
def request_association(assoc_type, session_type)
assoc_session, args = create_associate_request(assoc_type, session_type)
begin
response = OpenID.make_kv_post(args, @server_url)
return extract_association(response, assoc_session)
rescue HTTPStatusError => why
Util.log("Got HTTP status error when requesting association: #{why}")
return nil
rescue Message::KeyNotFound => why
Util.log("Missing required parameter in response from "\
"#{@server_url}: #{why}")
return nil
rescue ProtocolError => why
Util.log("Protocol error processing response from #{@server_url}: "\
"#{why}")
return nil
end
end
# Create an association request for the given assoc_type and
# session_type. Returns a pair of the association session object
# and the request message that will be sent to the server.
def create_associate_request(assoc_type, session_type)
assoc_session = self.class.create_session(session_type)
args = {
'mode' => 'associate',
'assoc_type' => assoc_type,
}
if !@compatibility_mode
args['ns'] = OPENID2_NS
end
# Leave out the session type if we're in compatibility mode
# *and* it's no-encryption.
if !@compatibility_mode ||
assoc_session.class.session_type != 'no-encryption'
args['session_type'] = assoc_session.class.session_type
end
args.merge!(assoc_session.get_request)
message = Message.from_openid_args(args)
return assoc_session, message
end
# Given an association response message, extract the OpenID 1.X
# session type. Returns the association type for this message
#
# This function mostly takes care of the 'no-encryption' default
# behavior in OpenID 1.
#
# If the association type is plain-text, this function will
# return 'no-encryption'
def get_openid1_session_type(assoc_response)
# If it's an OpenID 1 message, allow session_type to default
# to nil (which signifies "no-encryption")
session_type = assoc_response.get_arg(OPENID1_NS, 'session_type')
# Handle the differences between no-encryption association
# respones in OpenID 1 and 2:
# no-encryption is not really a valid session type for
# OpenID 1, but we'll accept it anyway, while issuing a
# warning.
if session_type == 'no-encryption'
Util.log("WARNING: #{@server_url} sent 'no-encryption'"\
"for OpenID 1.X")
# Missing or empty session type is the way to flag a
# 'no-encryption' response. Change the session type to
# 'no-encryption' so that it can be handled in the same
# way as OpenID 2 'no-encryption' respones.
elsif session_type == '' || session_type.nil?
session_type = 'no-encryption'
end
return session_type
end
def self.extract_expires_in(message)
# expires_in should be a base-10 string.
expires_in_str = message.get_arg(OPENID_NS, 'expires_in', NO_DEFAULT)
if !(/\A\d+\Z/ =~ expires_in_str)
raise ProtocolError, "Invalid expires_in field: #{expires_in_str}"
end
expires_in_str.to_i
end
# Attempt to extract an association from the response, given the
# association response message and the established association
# session.
def extract_association(assoc_response, assoc_session)
# Extract the common fields from the response, raising an
# exception if they are not found
assoc_type = assoc_response.get_arg(OPENID_NS, 'assoc_type',
NO_DEFAULT)
assoc_handle = assoc_response.get_arg(OPENID_NS, 'assoc_handle',
NO_DEFAULT)
expires_in = self.class.extract_expires_in(assoc_response)
# OpenID 1 has funny association session behaviour.
if assoc_response.is_openid1
session_type = get_openid1_session_type(assoc_response)
else
session_type = assoc_response.get_arg(OPENID2_NS, 'session_type',
NO_DEFAULT)
end
# Session type mismatch
if assoc_session.class.session_type != session_type
if (assoc_response.is_openid1 and session_type == 'no-encryption')
# In OpenID 1, any association request can result in a
# 'no-encryption' association response. Setting
# assoc_session to a new no-encryption session should
# make the rest of this function work properly for
# that case.
assoc_session = NoEncryptionSession.new
else
# Any other mismatch, regardless of protocol version
# results in the failure of the association session
# altogether.
raise ProtocolError, "Session type mismatch. Expected "\
"#{assoc_session.class.session_type}, got "\
"#{session_type}"
end
end
# Make sure assoc_type is valid for session_type
if !assoc_session.class.allowed_assoc_types.member?(assoc_type)
raise ProtocolError, "Unsupported assoc_type for session "\
"#{assoc_session.class.session_type} "\
"returned: #{assoc_type}"
end
# Delegate to the association session to extract the secret
# from the response, however is appropriate for that session
# type.
begin
secret = assoc_session.extract_secret(assoc_response)
rescue Message::KeyNotFound, ArgumentError => why
raise ProtocolError, "Malformed response for "\
"#{assoc_session.class.session_type} "\
"session: #{why.message}"
end
return Association.from_expires_in(expires_in, assoc_handle, secret,
assoc_type)
end
end
end
end

View File

@ -0,0 +1,186 @@
require "openid/message"
require "openid/util"
module OpenID
class Consumer
# An object that holds the state necessary for generating an
# OpenID authentication request. This object holds the association
# with the server and the discovered information with which the
# request will be made.
#
# It is separate from the consumer because you may wish to add
# things to the request before sending it on its way to the
# server. It also has serialization options that let you encode
# the authentication request as a URL or as a form POST.
class CheckIDRequest
attr_accessor :return_to_args, :message
attr_reader :endpoint
# Users of this library should not create instances of this
# class. Instances of this class are created by the library
# when needed.
def initialize(assoc, endpoint)
@assoc = assoc
@endpoint = endpoint
@return_to_args = {}
@message = Message.new(endpoint.preferred_namespace)
@anonymous = false
end
attr_reader :anonymous
# Set whether this request should be made anonymously. If a
# request is anonymous, the identifier will not be sent in the
# request. This is only useful if you are making another kind of
# request with an extension in this request.
#
# Anonymous requests are not allowed when the request is made
# with OpenID 1.
def anonymous=(is_anonymous)
if is_anonymous && @message.is_openid1
raise ArgumentError, ("OpenID1 requests MUST include the "\
"identifier in the request")
end
@anonymous = is_anonymous
end
# Add an object that implements the extension interface for
# adding arguments to an OpenID message to this checkid request.
#
# extension_request: an OpenID::Extension object.
def add_extension(extension_request)
extension_request.to_message(@message)
end
# Add an extension argument to this OpenID authentication
# request. You probably want to use add_extension and the
# OpenID::Extension interface.
#
# Use caution when adding arguments, because they will be
# URL-escaped and appended to the redirect URL, which can easily
# get quite long.
def add_extension_arg(namespace, key, value)
@message.set_arg(namespace, key, value)
end
# Produce a OpenID::Message representing this request.
#
# Not specifying a return_to URL means that the user will not be
# returned to the site issuing the request upon its completion.
#
# If immediate mode is requested, the OpenID provider is to send
# back a response immediately, useful for behind-the-scenes
# authentication attempts. Otherwise the OpenID provider may
# engage the user before providing a response. This is the
# default case, as the user may need to provide credentials or
# approve the request before a positive response can be sent.
def get_message(realm, return_to=nil, immediate=false)
if !return_to.nil?
return_to = Util.append_args(return_to, @return_to_args)
elsif immediate
raise ArgumentError, ('"return_to" is mandatory when using '\
'"checkid_immediate"')
elsif @message.is_openid1
raise ArgumentError, ('"return_to" is mandatory for OpenID 1 '\
'requests')
elsif @return_to_args.empty?
raise ArgumentError, ('extra "return_to" arguments were specified, '\
'but no return_to was specified')
end
message = @message.copy
mode = immediate ? 'checkid_immediate' : 'checkid_setup'
message.set_arg(OPENID_NS, 'mode', mode)
realm_key = message.is_openid1 ? 'trust_root' : 'realm'
message.set_arg(OPENID_NS, realm_key, realm)
if !return_to.nil?
message.set_arg(OPENID_NS, 'return_to', return_to)
end
if not @anonymous
if @endpoint.is_op_identifier
# This will never happen when we're in OpenID 1
# compatibility mode, as long as is_op_identifier()
# returns false whenever preferred_namespace returns
# OPENID1_NS.
claimed_id = request_identity = IDENTIFIER_SELECT
else
request_identity = @endpoint.get_local_id
claimed_id = @endpoint.claimed_id
end
# This is true for both OpenID 1 and 2
message.set_arg(OPENID_NS, 'identity', request_identity)
if message.is_openid2
message.set_arg(OPENID2_NS, 'claimed_id', claimed_id)
end
end
if @assoc
message.set_arg(OPENID_NS, 'assoc_handle', @assoc.handle)
assoc_log_msg = "with assocication #{@assoc.handle}"
else
assoc_log_msg = 'using stateless mode.'
end
Util.log("Generated #{mode} request to #{@endpoint.server_url} "\
"#{assoc_log_msg}")
return message
end
# Returns a URL with an encoded OpenID request.
#
# The resulting URL is the OpenID provider's endpoint URL with
# parameters appended as query arguments. You should redirect
# the user agent to this URL.
#
# OpenID 2.0 endpoints also accept POST requests, see
# 'send_redirect?' and 'form_markup'.
def redirect_url(realm, return_to=nil, immediate=false)
message = get_message(realm, return_to, immediate)
return message.to_url(@endpoint.server_url)
end
# Get html for a form to submit this request to the IDP.
#
# form_tag_attrs is a hash of attributes to be added to the form
# tag. 'accept-charset' and 'enctype' have defaults that can be
# overridden. If a value is supplied for 'action' or 'method',
# it will be replaced.
def form_markup(realm, return_to=nil, immediate=false,
form_tag_attrs=nil)
message = get_message(realm, return_to, immediate)
return message.to_form_markup(@endpoint.server_url, form_tag_attrs)
end
# Get a complete HTML document that autosubmits the request to the IDP
# with javascript. This method wraps form_markup - see that method's
# documentation for help with the parameters.
def html_markup(realm, return_to=nil, immediate=false,
form_tag_attrs=nil)
Util.auto_submit_html(form_markup(realm,
return_to,
immediate,
form_tag_attrs))
end
# Should this OpenID authentication request be sent as a HTTP
# redirect or as a POST (form submission)?
#
# This takes the same parameters as redirect_url or form_markup
def send_redirect?(realm, return_to=nil, immediate=false)
if @endpoint.compatibility_mode
return true
else
url = redirect_url(realm, return_to, immediate)
return url.length <= OPENID1_URL_LIMIT
end
end
end
end
end

View File

@ -0,0 +1,498 @@
# Functions to discover OpenID endpoints from identifiers.
require 'uri'
require 'openid/util'
require 'openid/fetchers'
require 'openid/urinorm'
require 'openid/message'
require 'openid/yadis/discovery'
require 'openid/yadis/xrds'
require 'openid/yadis/xri'
require 'openid/yadis/services'
require 'openid/yadis/filters'
require 'openid/consumer/html_parse'
require 'openid/yadis/xrires'
module OpenID
OPENID_1_0_NS = 'http://openid.net/xmlns/1.0'
OPENID_IDP_2_0_TYPE = 'http://specs.openid.net/auth/2.0/server'
OPENID_2_0_TYPE = 'http://specs.openid.net/auth/2.0/signon'
OPENID_1_1_TYPE = 'http://openid.net/signon/1.1'
OPENID_1_0_TYPE = 'http://openid.net/signon/1.0'
OPENID_1_0_MESSAGE_NS = OPENID1_NS
OPENID_2_0_MESSAGE_NS = OPENID2_NS
# Object representing an OpenID service endpoint.
class OpenIDServiceEndpoint
# OpenID service type URIs, listed in order of preference. The
# ordering of this list affects yadis and XRI service discovery.
OPENID_TYPE_URIS = [
OPENID_IDP_2_0_TYPE,
OPENID_2_0_TYPE,
OPENID_1_1_TYPE,
OPENID_1_0_TYPE,
]
# the verified identifier.
attr_accessor :claimed_id
# For XRI, the persistent identifier.
attr_accessor :canonical_id
attr_accessor :server_url, :type_uris, :local_id, :used_yadis
def initialize
@claimed_id = nil
@server_url = nil
@type_uris = []
@local_id = nil
@canonical_id = nil
@used_yadis = false # whether this came from an XRDS
@display_identifier = nil
end
def display_identifier
return @display_identifier if @display_identifier
return @claimed_id if @claimed_id.nil?
begin
parsed_identifier = URI.parse(@claimed_id)
rescue URI::InvalidURIError
raise ProtocolError, "Claimed identifier #{claimed_id} is not a valid URI"
end
return @claimed_id if not parsed_identifier.fragment
disp = parsed_identifier
disp.fragment = nil
return disp.to_s
end
def display_identifier=(display_identifier)
@display_identifier = display_identifier
end
def uses_extension(extension_uri)
return @type_uris.member?(extension_uri)
end
def preferred_namespace
if (@type_uris.member?(OPENID_IDP_2_0_TYPE) or
@type_uris.member?(OPENID_2_0_TYPE))
return OPENID_2_0_MESSAGE_NS
else
return OPENID_1_0_MESSAGE_NS
end
end
def supports_type(type_uri)
# Does this endpoint support this type?
#
# I consider C{/server} endpoints to implicitly support C{/signon}.
(
@type_uris.member?(type_uri) or
(type_uri == OPENID_2_0_TYPE and is_op_identifier())
)
end
def compatibility_mode
return preferred_namespace() != OPENID_2_0_MESSAGE_NS
end
def is_op_identifier
return @type_uris.member?(OPENID_IDP_2_0_TYPE)
end
def parse_service(yadis_url, uri, type_uris, service_element)
# Set the state of this object based on the contents of the
# service element.
@type_uris = type_uris
@server_url = uri
@used_yadis = true
if !is_op_identifier()
# XXX: This has crappy implications for Service elements that
# contain both 'server' and 'signon' Types. But that's a
# pathological configuration anyway, so I don't think I care.
@local_id = OpenID.find_op_local_identifier(service_element,
@type_uris)
@claimed_id = yadis_url
end
end
def get_local_id
# Return the identifier that should be sent as the
# openid.identity parameter to the server.
if @local_id.nil? and @canonical_id.nil?
return @claimed_id
else
return (@local_id or @canonical_id)
end
end
def self.from_basic_service_endpoint(endpoint)
# Create a new instance of this class from the endpoint object
# passed in.
#
# @return: nil or OpenIDServiceEndpoint for this endpoint object"""
type_uris = endpoint.match_types(OPENID_TYPE_URIS)
# If any Type URIs match and there is an endpoint URI specified,
# then this is an OpenID endpoint
if (!type_uris.nil? and !type_uris.empty?) and !endpoint.uri.nil?
openid_endpoint = self.new
openid_endpoint.parse_service(
endpoint.yadis_url,
endpoint.uri,
endpoint.type_uris,
endpoint.service_element)
else
openid_endpoint = nil
end
return openid_endpoint
end
def self.from_html(uri, html)
# Parse the given document as HTML looking for an OpenID <link
# rel=...>
#
# @rtype: [OpenIDServiceEndpoint]
discovery_types = [
[OPENID_2_0_TYPE, 'openid2.provider', 'openid2.local_id'],
[OPENID_1_1_TYPE, 'openid.server', 'openid.delegate'],
]
link_attrs = OpenID.parse_link_attrs(html)
services = []
discovery_types.each { |type_uri, op_endpoint_rel, local_id_rel|
op_endpoint_url = OpenID.find_first_href(link_attrs, op_endpoint_rel)
if !op_endpoint_url
next
end
service = self.new
service.claimed_id = uri
service.local_id = OpenID.find_first_href(link_attrs, local_id_rel)
service.server_url = op_endpoint_url
service.type_uris = [type_uri]
services << service
}
return services
end
def self.from_xrds(uri, xrds)
# Parse the given document as XRDS looking for OpenID services.
#
# @rtype: [OpenIDServiceEndpoint]
#
# @raises L{XRDSError}: When the XRDS does not parse.
return Yadis::apply_filter(uri, xrds, self)
end
def self.from_discovery_result(discoveryResult)
# Create endpoints from a DiscoveryResult.
#
# @type discoveryResult: L{DiscoveryResult}
#
# @rtype: list of L{OpenIDServiceEndpoint}
#
# @raises L{XRDSError}: When the XRDS does not parse.
if discoveryResult.is_xrds()
meth = self.method('from_xrds')
else
meth = self.method('from_html')
end
return meth.call(discoveryResult.normalized_uri,
discoveryResult.response_text)
end
def self.from_op_endpoint_url(op_endpoint_url)
# Construct an OP-Identifier OpenIDServiceEndpoint object for
# a given OP Endpoint URL
#
# @param op_endpoint_url: The URL of the endpoint
# @rtype: OpenIDServiceEndpoint
service = self.new
service.server_url = op_endpoint_url
service.type_uris = [OPENID_IDP_2_0_TYPE]
return service
end
def to_s
return sprintf("<%s server_url=%s claimed_id=%s " +
"local_id=%s canonical_id=%s used_yadis=%s>",
self.class, @server_url, @claimed_id,
@local_id, @canonical_id, @used_yadis)
end
end
def self.find_op_local_identifier(service_element, type_uris)
# Find the OP-Local Identifier for this xrd:Service element.
#
# This considers openid:Delegate to be a synonym for xrd:LocalID
# if both OpenID 1.X and OpenID 2.0 types are present. If only
# OpenID 1.X is present, it returns the value of
# openid:Delegate. If only OpenID 2.0 is present, it returns the
# value of xrd:LocalID. If there is more than one LocalID tag and
# the values are different, it raises a DiscoveryFailure. This is
# also triggered when the xrd:LocalID and openid:Delegate tags are
# different.
# XXX: Test this function on its own!
# Build the list of tags that could contain the OP-Local
# Identifier
local_id_tags = []
if type_uris.member?(OPENID_1_1_TYPE) or
type_uris.member?(OPENID_1_0_TYPE)
# local_id_tags << Yadis::nsTag(OPENID_1_0_NS, 'openid', 'Delegate')
service_element.add_namespace('openid', OPENID_1_0_NS)
local_id_tags << "openid:Delegate"
end
if type_uris.member?(OPENID_2_0_TYPE)
# local_id_tags.append(Yadis::nsTag(XRD_NS_2_0, 'xrd', 'LocalID'))
service_element.add_namespace('xrd', Yadis::XRD_NS_2_0)
local_id_tags << "xrd:LocalID"
end
# Walk through all the matching tags and make sure that they all
# have the same value
local_id = nil
local_id_tags.each { |local_id_tag|
service_element.each_element(local_id_tag) { |local_id_element|
if local_id.nil?
local_id = local_id_element.text
elsif local_id != local_id_element.text
format = 'More than one %s tag found in one service element'
message = sprintf(format, local_id_tag)
raise DiscoveryFailure.new(message, nil)
end
}
}
return local_id
end
def self.normalize_xri(xri)
# Normalize an XRI, stripping its scheme if present
m = /^xri:\/\/(.*)/.match(xri)
xri = m[1] if m
return xri
end
def self.normalize_url(url)
# Normalize a URL, converting normalization failures to
# DiscoveryFailure
begin
normalized = URINorm.urinorm(url)
rescue URI::Error => why
raise DiscoveryFailure.new("Error normalizing #{url}: #{why.message}", nil)
else
defragged = URI::parse(normalized)
defragged.fragment = nil
return defragged.normalize.to_s
end
end
def self.best_matching_service(service, preferred_types)
# Return the index of the first matching type, or something higher
# if no type matches.
#
# This provides an ordering in which service elements that contain
# a type that comes earlier in the preferred types list come
# before service elements that come later. If a service element
# has more than one type, the most preferred one wins.
preferred_types.each_with_index { |value, index|
if service.type_uris.member?(value)
return index
end
}
return preferred_types.length
end
def self.arrange_by_type(service_list, preferred_types)
# Rearrange service_list in a new list so services are ordered by
# types listed in preferred_types. Return the new list.
# Build a list with the service elements in tuples whose
# comparison will prefer the one with the best matching service
prio_services = []
service_list.each_with_index { |s, index|
prio_services << [best_matching_service(s, preferred_types), index, s]
}
prio_services.sort!
# Now that the services are sorted by priority, remove the sort
# keys from the list.
(0...prio_services.length).each { |i|
prio_services[i] = prio_services[i][2]
}
return prio_services
end
def self.get_op_or_user_services(openid_services)
# Extract OP Identifier services. If none found, return the rest,
# sorted with most preferred first according to
# OpenIDServiceEndpoint.openid_type_uris.
#
# openid_services is a list of OpenIDServiceEndpoint objects.
#
# Returns a list of OpenIDServiceEndpoint objects.
op_services = arrange_by_type(openid_services, [OPENID_IDP_2_0_TYPE])
openid_services = arrange_by_type(openid_services,
OpenIDServiceEndpoint::OPENID_TYPE_URIS)
if !op_services.empty?
return op_services
else
return openid_services
end
end
def self.discover_yadis(uri)
# Discover OpenID services for a URI. Tries Yadis and falls back
# on old-style <link rel='...'> discovery if Yadis fails.
#
# @param uri: normalized identity URL
# @type uri: str
#
# @return: (claimed_id, services)
# @rtype: (str, list(OpenIDServiceEndpoint))
#
# @raises DiscoveryFailure: when discovery fails.
# Might raise a yadis.discover.DiscoveryFailure if no document
# came back for that URI at all. I don't think falling back to
# OpenID 1.0 discovery on the same URL will help, so don't bother
# to catch it.
response = Yadis.discover(uri)
yadis_url = response.normalized_uri
body = response.response_text
begin
openid_services = OpenIDServiceEndpoint.from_xrds(yadis_url, body)
rescue Yadis::XRDSError
# Does not parse as a Yadis XRDS file
openid_services = []
end
if openid_services.empty?
# Either not an XRDS or there are no OpenID services.
if response.is_xrds
# if we got the Yadis content-type or followed the Yadis
# header, re-fetch the document without following the Yadis
# header, with no Accept header.
return self.discover_no_yadis(uri)
end
# Try to parse the response as HTML.
# <link rel="...">
openid_services = OpenIDServiceEndpoint.from_html(yadis_url, body)
end
return [yadis_url, self.get_op_or_user_services(openid_services)]
end
def self.discover_xri(iname)
endpoints = []
iname = self.normalize_xri(iname)
begin
canonical_id, services = Yadis::XRI::ProxyResolver.new().query(
iname, OpenIDServiceEndpoint::OPENID_TYPE_URIS)
if canonical_id.nil?
raise Yadis::XRDSError.new(sprintf('No CanonicalID found for XRI %s', iname))
end
flt = Yadis.make_filter(OpenIDServiceEndpoint)
services.each { |service_element|
endpoints += flt.get_service_endpoints(iname, service_element)
}
rescue Yadis::XRDSError => why
Util.log('xrds error on ' + iname + ': ' + why.to_s)
end
endpoints.each { |endpoint|
# Is there a way to pass this through the filter to the endpoint
# constructor instead of tacking it on after?
endpoint.canonical_id = canonical_id
endpoint.claimed_id = canonical_id
endpoint.display_identifier = iname
}
# FIXME: returned xri should probably be in some normal form
return [iname, self.get_op_or_user_services(endpoints)]
end
def self.discover_no_yadis(uri)
http_resp = OpenID.fetch(uri)
if http_resp.code != "200" and http_resp.code != "206"
raise DiscoveryFailure.new(
"HTTP Response status from identity URL host is not \"200\". "\
"Got status #{http_resp.code.inspect}", http_resp)
end
claimed_id = http_resp.final_url
openid_services = OpenIDServiceEndpoint.from_html(
claimed_id, http_resp.body)
return [claimed_id, openid_services]
end
def self.discover_uri(uri)
# Hack to work around URI parsing for URls with *no* scheme.
if uri.index("://").nil?
uri = 'http://' + uri
end
begin
parsed = URI::parse(uri)
rescue URI::InvalidURIError => why
raise DiscoveryFailure.new("URI is not valid: #{why.message}", nil)
end
if !parsed.scheme.nil? and !parsed.scheme.empty?
if !['http', 'https'].member?(parsed.scheme)
raise DiscoveryFailure.new(
"URI scheme #{parsed.scheme} is not HTTP or HTTPS", nil)
end
end
uri = self.normalize_url(uri)
claimed_id, openid_services = self.discover_yadis(uri)
claimed_id = self.normalize_url(claimed_id)
return [claimed_id, openid_services]
end
def self.discover(identifier)
if Yadis::XRI::identifier_scheme(identifier) == :xri
normalized_identifier, services = discover_xri(identifier)
else
return discover_uri(identifier)
end
end
end

View File

@ -0,0 +1,123 @@
module OpenID
class Consumer
# A set of discovered services, for tracking which providers have
# been attempted for an OpenID identifier
class DiscoveredServices
attr_reader :current
def initialize(starting_url, yadis_url, services)
@starting_url = starting_url
@yadis_url = yadis_url
@services = services.dup
@current = nil
end
def next
@current = @services.shift
end
def for_url?(url)
[@starting_url, @yadis_url].member?(url)
end
def started?
!@current.nil?
end
def empty?
@services.empty?
end
end
# Manages calling discovery and tracking which endpoints have
# already been attempted.
class DiscoveryManager
def initialize(session, url, session_key_suffix=nil)
@url = url
@session = session
@session_key_suffix = session_key_suffix || 'auth'
end
def get_next_service
manager = get_manager
if !manager.nil? && manager.empty?
destroy_manager
manager = nil
end
if manager.nil?
yadis_url, services = yield @url
manager = create_manager(yadis_url, services)
end
if !manager.nil?
service = manager.next
store(manager)
else
service = nil
end
return service
end
def cleanup(force=false)
manager = get_manager(force)
if !manager.nil?
service = manager.current
destroy_manager(force)
else
service = nil
end
return service
end
protected
def get_manager(force=false)
manager = load
if force || manager.nil? || manager.for_url?(@url)
return manager
else
return nil
end
end
def create_manager(yadis_url, services)
manager = get_manager
if !manager.nil?
raise StandardError, "There is already a manager for #{yadis_url}"
end
if services.empty?
return nil
end
manager = DiscoveredServices.new(@url, yadis_url, services)
store(manager)
return manager
end
def destroy_manager(force=false)
if !get_manager(force).nil?
destroy!
end
end
def session_key
'OpenID::Consumer::DiscoveredServices::' + @session_key_suffix
end
def store(manager)
@session[session_key] = manager
end
def load
@session[session_key]
end
def destroy!
@session[session_key] = nil
end
end
end
end

View File

@ -0,0 +1,134 @@
require "openid/yadis/htmltokenizer"
module OpenID
# Stuff to remove before we start looking for tags
REMOVED_RE = /
# Comments
<!--.*?-->
# CDATA blocks
| <!\[CDATA\[.*?\]\]>
# script blocks
| <script\b
# make sure script is not an XML namespace
(?!:)
[^>]*>.*?<\/script>
/mixu
def OpenID.openid_unescape(s)
s.gsub('&amp;','&').gsub('&lt;','<').gsub('&gt;','>').gsub('&quot;','"')
end
def OpenID.unescape_hash(h)
newh = {}
h.map{|k,v|
newh[k]=openid_unescape(v)
}
newh
end
def OpenID.parse_link_attrs(html)
stripped = html.gsub(REMOVED_RE,'')
parser = HTMLTokenizer.new(stripped)
links = []
# to keep track of whether or not we are in the head element
in_head = false
in_html = false
saw_head = false
begin
while el = parser.getTag('head', '/head', 'link', 'body', '/body',
'html', '/html')
# we are leaving head or have reached body, so we bail
return links if ['/head', 'body', '/body', '/html'].member?(el.tag_name)
# enforce html > head > link
if el.tag_name == 'html'
in_html = true
end
next unless in_html
if el.tag_name == 'head'
if saw_head
return links #only allow one head
end
saw_head = true
unless el.to_s[-2] == 47 # tag ends with a /: a short tag
in_head = true
end
end
next unless in_head
return links if el.tag_name == 'html'
if el.tag_name == 'link'
links << unescape_hash(el.attr_hash)
end
end
rescue Exception # just stop parsing if there's an error
end
return links
end
def OpenID.rel_matches(rel_attr, target_rel)
# Does this target_rel appear in the rel_str?
# XXX: TESTME
rels = rel_attr.strip().split()
rels.each { |rel|
rel = rel.downcase
if rel == target_rel
return true
end
}
return false
end
def OpenID.link_has_rel(link_attrs, target_rel)
# Does this link have target_rel as a relationship?
# XXX: TESTME
rel_attr = link_attrs['rel']
return (rel_attr and rel_matches(rel_attr, target_rel))
end
def OpenID.find_links_rel(link_attrs_list, target_rel)
# Filter the list of link attributes on whether it has target_rel
# as a relationship.
# XXX: TESTME
matchesTarget = lambda { |attrs| link_has_rel(attrs, target_rel) }
result = []
link_attrs_list.each { |item|
if matchesTarget.call(item)
result << item
end
}
return result
end
def OpenID.find_first_href(link_attrs_list, target_rel)
# Return the value of the href attribute for the first link tag in
# the list that has target_rel as a relationship.
# XXX: TESTME
matches = find_links_rel(link_attrs_list, target_rel)
if !matches or matches.empty?
return nil
end
first = matches[0]
return first['href']
end
end

View File

@ -0,0 +1,523 @@
require "openid/message"
require "openid/protocolerror"
require "openid/kvpost"
require "openid/consumer/discovery"
require "openid/urinorm"
module OpenID
class TypeURIMismatch < ProtocolError
attr_reader :type_uri, :endpoint
def initialize(type_uri, endpoint)
@type_uri = type_uri
@endpoint = endpoint
end
end
class Consumer
@openid1_return_to_nonce_name = 'rp_nonce'
@openid1_return_to_claimed_id_name = 'openid1_claimed_id'
# Set the name of the query parameter that this library will use
# to thread a nonce through an OpenID 1 transaction. It will be
# appended to the return_to URL.
def self.openid1_return_to_nonce_name=(query_arg_name)
@openid1_return_to_nonce_name = query_arg_name
end
# See openid1_return_to_nonce_name= documentation
def self.openid1_return_to_nonce_name
@openid1_return_to_nonce_name
end
# Set the name of the query parameter that this library will use
# to thread the requested URL through an OpenID 1 transaction (for
# use when verifying discovered information). It will be appended
# to the return_to URL.
def self.openid1_return_to_claimed_id_name=(query_arg_name)
@openid1_return_to_claimed_id_name = query_arg_name
end
# See openid1_return_to_claimed_id_name=
def self.openid1_return_to_claimed_id_name
@openid1_return_to_claimed_id_name
end
# Handles an openid.mode=id_res response. This object is
# instantiated and used by the Consumer.
class IdResHandler
attr_reader :endpoint, :message
def initialize(message, current_url, store=nil, endpoint=nil)
@store = store # Fer the nonce and invalidate_handle
@message = message
@endpoint = endpoint
@current_url = current_url
@signed_list = nil
# Start the verification process
id_res
end
def signed_fields
signed_list.map {|x| 'openid.' + x}
end
protected
# This method will raise ProtocolError unless the request is a
# valid id_res response. Once it has been verified, the methods
# 'endpoint', 'message', and 'signed_fields' contain the
# verified information.
def id_res
check_for_fields
verify_return_to
verify_discovery_results
check_signature
check_nonce
end
def server_url
@endpoint.nil? ? nil : @endpoint.server_url
end
def openid_namespace
@message.get_openid_namespace
end
def fetch(field, default=NO_DEFAULT)
@message.get_arg(OPENID_NS, field, default)
end
def signed_list
if @signed_list.nil?
signed_list_str = fetch('signed', nil)
if signed_list_str.nil?
raise ProtocolError, 'Response missing signed list'
end
@signed_list = signed_list_str.split(',', -1)
end
@signed_list
end
def check_for_fields
# XXX: if a field is missing, we should not have to explicitly
# check that it's present, just make sure that the fields are
# actually being used by the rest of the code in
# tests. Although, which fields are signed does need to be
# checked somewhere.
basic_fields = ['return_to', 'assoc_handle', 'sig', 'signed']
basic_sig_fields = ['return_to', 'identity']
case openid_namespace
when OPENID2_NS
require_fields = basic_fields + ['op_endpoint']
require_sigs = basic_sig_fields +
['response_nonce', 'claimed_id', 'assoc_handle',]
when OPENID1_NS
require_fields = basic_fields + ['identity']
require_sigs = basic_sig_fields
else
raise RuntimeError, "check_for_fields doesn't know about "\
"namespace #{openid_namespace.inspect}"
end
require_fields.each do |field|
if !@message.has_key?(OPENID_NS, field)
raise ProtocolError, "Missing required field #{field}"
end
end
require_sigs.each do |field|
# Field is present and not in signed list
if @message.has_key?(OPENID_NS, field) && !signed_list.member?(field)
raise ProtocolError, "#{field.inspect} not signed"
end
end
end
def verify_return_to
begin
msg_return_to = URI.parse(URINorm::urinorm(fetch('return_to')))
rescue URI::InvalidURIError
raise ProtocolError, ("return_to is not a valid URI")
end
verify_return_to_args(msg_return_to)
if !@current_url.nil?
verify_return_to_base(msg_return_to)
end
end
def verify_return_to_args(msg_return_to)
return_to_parsed_query = {}
if !msg_return_to.query.nil?
CGI.parse(msg_return_to.query).each_pair do |k, vs|
return_to_parsed_query[k] = vs[0]
end
end
query = @message.to_post_args
return_to_parsed_query.each_pair do |rt_key, rt_val|
msg_val = query[rt_key]
if msg_val.nil?
raise ProtocolError, "Message missing return_to argument '#{rt_key}'"
elsif msg_val != rt_val
raise ProtocolError, ("Parameter '#{rt_key}' value "\
"#{msg_val.inspect} does not match "\
"return_to's value #{rt_val.inspect}")
end
end
@message.get_args(BARE_NS).each_pair do |bare_key, bare_val|
rt_val = return_to_parsed_query[bare_key]
if not return_to_parsed_query.has_key? bare_key
# This may be caused by your web framework throwing extra
# entries in to your parameters hash that were not GET or
# POST parameters. For example, Rails has been known to
# add "controller" and "action" keys; another server adds
# at least a "format" key.
raise ProtocolError, ("Unexpected parameter (not on return_to): "\
"'#{bare_key}'=#{rt_val.inspect})")
end
if rt_val != bare_val
raise ProtocolError, ("Parameter '#{bare_key}' value "\
"#{bare_val.inspect} does not match "\
"return_to's value #{rt_val.inspect}")
end
end
end
def verify_return_to_base(msg_return_to)
begin
app_parsed = URI.parse(URINorm::urinorm(@current_url))
rescue URI::InvalidURIError
raise ProtocolError, "current_url is not a valid URI: #{@current_url}"
end
[:scheme, :host, :port, :path].each do |meth|
if msg_return_to.send(meth) != app_parsed.send(meth)
raise ProtocolError, "return_to #{meth.to_s} does not match"
end
end
end
# Raises ProtocolError if the signature is bad
def check_signature
if @store.nil?
assoc = nil
else
assoc = @store.get_association(server_url, fetch('assoc_handle'))
end
if assoc.nil?
check_auth
else
if assoc.expires_in <= 0
# XXX: It might be a good idea sometimes to re-start the
# authentication with a new association. Doing it
# automatically opens the possibility for
# denial-of-service by a server that just returns expired
# associations (or really short-lived associations)
raise ProtocolError, "Association with #{server_url} expired"
elsif !assoc.check_message_signature(@message)
raise ProtocolError, "Bad signature in response from #{server_url}"
end
end
end
def check_auth
Util.log("Using 'check_authentication' with #{server_url}")
begin
request = create_check_auth_request
rescue Message::KeyNotFound => why
raise ProtocolError, "Could not generate 'check_authentication' "\
"request: #{why.message}"
end
response = OpenID.make_kv_post(request, server_url)
process_check_auth_response(response)
end
def create_check_auth_request
signed_list = @message.get_arg(OPENID_NS, 'signed', NO_DEFAULT).split(',')
# check that we got all the signed arguments
signed_list.each {|k|
@message.get_aliased_arg(k, NO_DEFAULT)
}
ca_message = @message.copy
ca_message.set_arg(OPENID_NS, 'mode', 'check_authentication')
return ca_message
end
# Process the response message from a check_authentication
# request, invalidating associations if requested.
def process_check_auth_response(response)
is_valid = response.get_arg(OPENID_NS, 'is_valid', 'false')
invalidate_handle = response.get_arg(OPENID_NS, 'invalidate_handle')
if !invalidate_handle.nil?
Util.log("Received 'invalidate_handle' from server #{server_url}")
if @store.nil?
Util.log('Unexpectedly got "invalidate_handle" without a store!')
else
@store.remove_association(server_url, invalidate_handle)
end
end
if is_valid != 'true'
raise ProtocolError, ("Server #{server_url} responds that the "\
"'check_authentication' call is not valid")
end
end
def check_nonce
case openid_namespace
when OPENID1_NS
nonce =
@message.get_arg(BARE_NS, Consumer.openid1_return_to_nonce_name)
# We generated the nonce, so it uses the empty string as the
# server URL
server_url = ''
when OPENID2_NS
nonce = @message.get_arg(OPENID2_NS, 'response_nonce')
server_url = self.server_url
else
raise StandardError, 'Not reached'
end
if nonce.nil?
raise ProtocolError, 'Nonce missing from response'
end
begin
time, extra = Nonce.split_nonce(nonce)
rescue ArgumentError => why
raise ProtocolError, "Malformed nonce: #{nonce.inspect}"
end
if !@store.nil? && !@store.use_nonce(server_url, time, extra)
raise ProtocolError, ("Nonce already used or out of range: "\
"#{nonce.inspect}")
end
end
def verify_discovery_results
begin
case openid_namespace
when OPENID1_NS
verify_discovery_results_openid1
when OPENID2_NS
verify_discovery_results_openid2
else
raise StandardError, "Not reached: #{openid_namespace}"
end
rescue Message::KeyNotFound => why
raise ProtocolError, "Missing required field: #{why.message}"
end
end
def verify_discovery_results_openid2
to_match = OpenIDServiceEndpoint.new
to_match.type_uris = [OPENID_2_0_TYPE]
to_match.claimed_id = fetch('claimed_id', nil)
to_match.local_id = fetch('identity', nil)
to_match.server_url = fetch('op_endpoint')
if to_match.claimed_id.nil? && !to_match.local_id.nil?
raise ProtocolError, ('openid.identity is present without '\
'openid.claimed_id')
elsif !to_match.claimed_id.nil? && to_match.local_id.nil?
raise ProtocolError, ('openid.claimed_id is present without '\
'openid.identity')
# This is a response without identifiers, so there's really no
# checking that we can do, so return an endpoint that's for
# the specified `openid.op_endpoint'
elsif to_match.claimed_id.nil?
@endpoint =
OpenIDServiceEndpoint.from_op_endpoint_url(to_match.server_url)
return
end
if @endpoint.nil?
Util.log('No pre-discovered information supplied')
discover_and_verify(to_match.claimed_id, [to_match])
else
begin
verify_discovery_single(@endpoint, to_match)
rescue ProtocolError => why
Util.log("Error attempting to use stored discovery "\
"information: #{why.message}")
Util.log("Attempting discovery to verify endpoint")
discover_and_verify(to_match.claimed_id, [to_match])
end
end
if @endpoint.claimed_id != to_match.claimed_id
@endpoint = @endpoint.dup
@endpoint.claimed_id = to_match.claimed_id
end
end
def verify_discovery_results_openid1
claimed_id =
@message.get_arg(BARE_NS, Consumer.openid1_return_to_claimed_id_name)
if claimed_id.nil?
if @endpoint.nil?
raise ProtocolError, ("When using OpenID 1, the claimed ID must "\
"be supplied, either by passing it through "\
"as a return_to parameter or by using a "\
"session, and supplied to the IdResHandler "\
"when it is constructed.")
else
claimed_id = @endpoint.claimed_id
end
end
to_match = OpenIDServiceEndpoint.new
to_match.type_uris = [OPENID_1_1_TYPE]
to_match.local_id = fetch('identity')
# Restore delegate information from the initiation phase
to_match.claimed_id = claimed_id
to_match_1_0 = to_match.dup
to_match_1_0.type_uris = [OPENID_1_0_TYPE]
if !@endpoint.nil?
begin
begin
verify_discovery_single(@endpoint, to_match)
rescue TypeURIMismatch
verify_discovery_single(@endpoint, to_match_1_0)
end
rescue ProtocolError => why
Util.log('Error attempting to use stored discovery information: ' +
why.message)
Util.log('Attempting discovery to verify endpoint')
else
return @endpoint
end
end
# Either no endpoint was supplied or OpenID 1.x verification
# of the information that's in the message failed on that
# endpoint.
discover_and_verify(to_match.claimed_id, [to_match, to_match_1_0])
end
# Given an endpoint object created from the information in an
# OpenID response, perform discovery and verify the discovery
# results, returning the matching endpoint that is the result of
# doing that discovery.
def discover_and_verify(claimed_id, to_match_endpoints)
Util.log("Performing discovery on #{claimed_id}")
_, services = OpenID.discover(claimed_id)
if services.length == 0
# XXX: this might want to be something other than
# ProtocolError. In Python, it's DiscoveryFailure
raise ProtocolError, ("No OpenID information found at "\
"#{claimed_id}")
end
verify_discovered_services(claimed_id, services, to_match_endpoints)
end
def verify_discovered_services(claimed_id, services, to_match_endpoints)
# Search the services resulting from discovery to find one
# that matches the information from the assertion
failure_messages = []
for endpoint in services
for to_match_endpoint in to_match_endpoints
begin
verify_discovery_single(endpoint, to_match_endpoint)
rescue ProtocolError => why
failure_messages << why.message
else
# It matches, so discover verification has
# succeeded. Return this endpoint.
@endpoint = endpoint
return
end
end
end
Util.log("Discovery verification failure for #{claimed_id}")
failure_messages.each do |failure_message|
Util.log(" * Endpoint mismatch: " + failure_message)
end
# XXX: is DiscoveryFailure in Python OpenID
raise ProtocolError, ("No matching endpoint found after "\
"discovering #{claimed_id}")
end
def verify_discovery_single(endpoint, to_match)
# Every type URI that's in the to_match endpoint has to be
# present in the discovered endpoint.
for type_uri in to_match.type_uris
if !endpoint.uses_extension(type_uri)
raise TypeURIMismatch.new(type_uri, endpoint)
end
end
# Fragments do not influence discovery, so we can't compare a
# claimed identifier with a fragment to discovered information.
defragged_claimed_id =
case Yadis::XRI.identifier_scheme(endpoint.claimed_id)
when :xri
endpoint.claimed_id
when :uri
begin
parsed = URI.parse(endpoint.claimed_id)
rescue URI::InvalidURIError
endpoint.claimed_id
else
parsed.fragment = nil
parsed.to_s
end
else
raise StandardError, 'Not reached'
end
if defragged_claimed_id != endpoint.claimed_id
raise ProtocolError, ("Claimed ID does not match (different "\
"subjects!), Expected "\
"#{defragged_claimed_id}, got "\
"#{endpoint.claimed_id}")
end
if to_match.get_local_id != endpoint.get_local_id
raise ProtocolError, ("local_id mismatch. Expected "\
"#{to_match.get_local_id}, got "\
"#{endpoint.get_local_id}")
end
# If the server URL is nil, this must be an OpenID 1
# response, because op_endpoint is a required parameter in
# OpenID 2. In that case, we don't actually care what the
# discovered server_url is, because signature checking or
# check_auth should take care of that check for us.
if to_match.server_url.nil?
if to_match.preferred_namespace != OPENID1_NS
raise StandardError,
"The code calling this must ensure that OpenID 2 "\
"responses have a non-none `openid.op_endpoint' and "\
"that it is set as the `server_url' attribute of the "\
"`to_match' endpoint."
end
elsif to_match.server_url != endpoint.server_url
raise ProtocolError, ("OP Endpoint mismatch. Expected"\
"#{to_match.server_url}, got "\
"#{endpoint.server_url}")
end
end
end
end
end

View File

@ -0,0 +1,148 @@
module OpenID
class Consumer
# Code returned when either the of the
# OpenID::OpenIDConsumer.begin_auth or OpenID::OpenIDConsumer.complete_auth
# methods return successfully.
SUCCESS = :success
# Code OpenID::OpenIDConsumer.complete_auth
# returns when the value it received indicated an invalid login.
FAILURE = :failure
# Code returned by OpenIDConsumer.complete_auth when the user
# cancels the operation from the server.
CANCEL = :cancel
# Code returned by OpenID::OpenIDConsumer.complete_auth when the
# OpenIDConsumer instance is in immediate mode and ther server sends back a
# URL for the user to login with.
SETUP_NEEDED = :setup_needed
module Response
attr_reader :endpoint
def status
self.class::STATUS
end
# The identity URL that has been authenticated; the Claimed Identifier.
# See also display_identifier.
def identity_url
@endpoint ? @endpoint.claimed_id : nil
end
# The display identifier is related to the Claimed Identifier, but the
# two are not always identical. The display identifier is something the
# user should recognize as what they entered, whereas the response's
# claimed identifier (in the identity_url attribute) may have extra
# information for better persistence.
#
# URLs will be stripped of their fragments for display. XRIs will
# display the human-readable identifier (i-name) instead of the
# persistent identifier (i-number).
#
# Use the display identifier in your user interface. Use identity_url
# for querying your database or authorization server, or other
# identifier equality comparisons.
def display_identifier
@endpoint ? @endpoint.display_identifier : nil
end
end
# A successful acknowledgement from the OpenID server that the
# supplied URL is, indeed controlled by the requesting agent.
class SuccessResponse
include Response
STATUS = SUCCESS
attr_reader :message, :signed_fields
def initialize(endpoint, message, signed_fields)
# Don't use :endpoint=, because endpoint should never be nil
# for a successfull transaction.
@endpoint = endpoint
@identity_url = endpoint.claimed_id
@message = message
@signed_fields = signed_fields
end
# Was this authentication response an OpenID 1 authentication
# response?
def is_openid1
@message.is_openid1
end
# Return whether a particular key is signed, regardless of its
# namespace alias
def signed?(ns_uri, ns_key)
@signed_fields.member?(@message.get_key(ns_uri, ns_key))
end
# Return the specified signed field if available, otherwise
# return default
def get_signed(ns_uri, ns_key, default=nil)
if singed?(ns_uri, ns_key)
return @message.get_arg(ns_uri, ns_key, default)
else
return default
end
end
# Get signed arguments from the response message. Return a dict
# of all arguments in the specified namespace. If any of the
# arguments are not signed, return nil.
def get_signed_ns(ns_uri)
msg_args = @message.get_args(ns_uri)
msg_args.each_key do |key|
if !signed?(ns_uri, key)
return nil
end
end
return msg_args
end
# Return response arguments in the specified namespace.
# If require_signed is true and the arguments are not signed,
# return nil.
def extension_response(namespace_uri, require_signed)
if require_signed
get_signed_ns(namespace_uri)
else
@message.get_args(namespace_uri)
end
end
end
class FailureResponse
include Response
STATUS = FAILURE
attr_reader :message, :contact, :reference
def initialize(endpoint, message, contact=nil, reference=nil)
@endpoint = endpoint
@message = message
@contact = contact
@reference = reference
end
end
class CancelResponse
include Response
STATUS = CANCEL
def initialize(endpoint)
@endpoint = endpoint
end
end
class SetupNeededResponse
include Response
STATUS = SETUP_NEEDED
def initialize(endpoint, setup_url)
@endpoint = endpoint
@setup_url = setup_url
end
end
end
end

View File

@ -0,0 +1,97 @@
require "openid/util"
require "digest/sha1"
require "digest/sha2"
begin
require "digest/hmac"
rescue LoadError
require "hmac/sha1"
require "hmac/sha2"
end
module OpenID
# This module contains everything needed to perform low-level
# cryptograph and data manipulation tasks.
module CryptUtil
# Generate a random number, doing a little extra work to make it
# more likely that it's suitable for cryptography. If your system
# doesn't have /dev/urandom then this number is not
# cryptographically safe. See
# <http://www.cosine.org/2007/08/07/security-ruby-kernel-rand/>
# for more information. max is the largest possible value of such
# a random number, where the result will be less than max.
def CryptUtil.rand(max)
Kernel.srand()
return Kernel.rand(max)
end
def CryptUtil.sha1(text)
return Digest::SHA1.digest(text)
end
def CryptUtil.hmac_sha1(key, text)
if Digest.const_defined? :HMAC
Digest::HMAC.new(key,Digest::SHA1).update(text).digest
else
return HMAC::SHA1.digest(key, text)
end
end
def CryptUtil.sha256(text)
return Digest::SHA256.digest(text)
end
def CryptUtil.hmac_sha256(key, text)
if Digest.const_defined? :HMAC
Digest::HMAC.new(key,Digest::SHA256).update(text).digest
else
return HMAC::SHA256.digest(key, text)
end
end
# Generate a random string of the given length, composed of the
# specified characters. If chars is nil, generate a string
# composed of characters in the range 0..255.
def CryptUtil.random_string(length, chars=nil)
s = ""
unless chars.nil?
length.times { s << chars[rand(chars.length)] }
else
length.times { s << rand(256).chr }
end
return s
end
# Convert a number to its binary representation; return a string
# of bytes.
def CryptUtil.num_to_binary(n)
bits = n.to_s(2)
prepend = (8 - bits.length % 8)
bits = ('0' * prepend) + bits
return [bits].pack('B*')
end
# Convert a string of bytes into a number.
def CryptUtil.binary_to_num(s)
# taken from openid-ruby 0.0.1
s = "\000" * (4 - (s.length % 4)) + s
num = 0
s.unpack('N*').each do |x|
num <<= 32
num |= x
end
return num
end
# Encode a number as a base64-encoded byte string.
def CryptUtil.num_to_base64(l)
return OpenID::Util.to_base64(num_to_binary(l))
end
# Decode a base64 byte string to a number.
def CryptUtil.base64_to_num(s)
return binary_to_num(OpenID::Util.from_base64(s))
end
end
end

View File

@ -0,0 +1,89 @@
require "openid/util"
require "openid/cryptutil"
module OpenID
# Encapsulates a Diffie-Hellman key exchange. This class is used
# internally by both the consumer and server objects.
#
# Read more about Diffie-Hellman on wikipedia:
# http://en.wikipedia.org/wiki/Diffie-Hellman
class DiffieHellman
# From the OpenID specification
@@default_mod = 155172898181473697471232257763715539915724801966915404479707795314057629378541917580651227423698188993727816152646631438561595825688188889951272158842675419950341258706556549803580104870537681476726513255747040765857479291291572334510643245094715007229621094194349783925984760375594985848253359305585439638443
@@default_gen = 2
attr_reader :modulus, :generator, :public
# A new DiffieHellman object, using the modulus and generator from
# the OpenID specification
def DiffieHellman.from_defaults
DiffieHellman.new(@@default_mod, @@default_gen)
end
def initialize(modulus=nil, generator=nil, priv=nil)
@modulus = modulus.nil? ? @@default_mod : modulus
@generator = generator.nil? ? @@default_gen : generator
set_private(priv.nil? ? OpenID::CryptUtil.rand(@modulus-2) + 1 : priv)
end
def get_shared_secret(composite)
DiffieHellman.powermod(composite, @private, @modulus)
end
def xor_secret(algorithm, composite, secret)
dh_shared = get_shared_secret(composite)
packed_dh_shared = OpenID::CryptUtil.num_to_binary(dh_shared)
hashed_dh_shared = algorithm.call(packed_dh_shared)
return DiffieHellman.strxor(secret, hashed_dh_shared)
end
def using_default_values?
@generator == @@default_gen && @modulus == @@default_mod
end
private
def set_private(priv)
@private = priv
@public = DiffieHellman.powermod(@generator, @private, @modulus)
end
def DiffieHellman.strxor(s, t)
if s.length != t.length
raise ArgumentError, "strxor: lengths don't match. " +
"Inputs were #{s.inspect} and #{t.inspect}"
end
if String.method_defined? :bytes
s.bytes.zip(t.bytes).map{|sb,tb| sb^tb}.pack('C*')
else
indices = 0...(s.length)
chrs = indices.collect {|i| (s[i]^t[i]).chr}
chrs.join("")
end
end
# This code is taken from this post:
# <http://blade.nagaokaut.ac.jp/cgi-bin/scat.\rb/ruby/ruby-talk/19098>
# by Eric Lee Green.
def DiffieHellman.powermod(x, n, q)
counter=0
n_p=n
y_p=1
z_p=x
while n_p != 0
if n_p[0]==1
y_p=(y_p*z_p) % q
end
n_p = n_p >> 1
z_p = (z_p * z_p) % q
counter += 1
end
return y_p
end
end
end

View File

@ -0,0 +1,39 @@
require 'openid/message'
module OpenID
# An interface for OpenID extensions.
class Extension < Object
def initialize
@ns_uri = nil
@ns_alias = nil
end
# Get the string arguments that should be added to an OpenID
# message for this extension.
def get_extension_args
raise NotImplementedError
end
# Add the arguments from this extension to the provided
# message, or create a new message containing only those
# arguments. Returns the message with added extension args.
def to_message(message = nil)
if message.nil?
# warnings.warn('Passing None to Extension.toMessage is deprecated. '
# 'Creating a message assuming you want OpenID 2.',
# DeprecationWarning, stacklevel=2)
Message.new(OPENID2_NS)
end
message = Message.new if message.nil?
implicit = message.is_openid1()
message.namespaces.add_alias(@ns_uri, @ns_alias, implicit)
# XXX python ignores keyerror if m.ns.getAlias(uri) == alias
message.update_args(@ns_uri, get_extension_args)
return message
end
end
end

View File

@ -0,0 +1,516 @@
# Implements the OpenID attribute exchange specification, version 1.0
require 'openid/extension'
require 'openid/trustroot'
require 'openid/message'
module OpenID
module AX
UNLIMITED_VALUES = "unlimited"
MINIMUM_SUPPORTED_ALIAS_LENGTH = 32
# check alias for invalid characters, raise AXError if found
def self.check_alias(name)
if name.match(/(,|\.)/)
raise Error, ("Alias #{name.inspect} must not contain a "\
"comma or period.")
end
end
# Raised when data does not comply with AX 1.0 specification
class Error < ArgumentError
end
# Abstract class containing common code for attribute exchange messages
class AXMessage < Extension
attr_accessor :ns_alias, :mode, :ns_uri
NS_URI = 'http://openid.net/srv/ax/1.0'
def initialize
@ns_alias = 'ax'
@ns_uri = NS_URI
@mode = nil
end
protected
# Raise an exception if the mode in the attribute exchange
# arguments does not match what is expected for this class.
def check_mode(ax_args)
actual_mode = ax_args['mode']
if actual_mode != @mode
raise Error, "Expected mode #{mode.inspect}, got #{actual_mode.inspect}"
end
end
def new_args
{'mode' => @mode}
end
end
# Represents a single attribute in an attribute exchange
# request. This should be added to an Request object in order to
# request the attribute.
#
# @ivar required: Whether the attribute will be marked as required
# when presented to the subject of the attribute exchange
# request.
# @type required: bool
#
# @ivar count: How many values of this type to request from the
# subject. Defaults to one.
# @type count: int
#
# @ivar type_uri: The identifier that determines what the attribute
# represents and how it is serialized. For example, one type URI
# representing dates could represent a Unix timestamp in base 10
# and another could represent a human-readable string.
# @type type_uri: str
#
# @ivar ns_alias: The name that should be given to this alias in the
# request. If it is not supplied, a generic name will be
# assigned. For example, if you want to call a Unix timestamp
# value 'tstamp', set its alias to that value. If two attributes
# in the same message request to use the same alias, the request
# will fail to be generated.
# @type alias: str or NoneType
class AttrInfo < Object
attr_reader :type_uri, :count, :ns_alias
attr_accessor :required
def initialize(type_uri, ns_alias=nil, required=false, count=1)
@type_uri = type_uri
@count = count
@required = required
@ns_alias = ns_alias
end
def wants_unlimited_values?
@count == UNLIMITED_VALUES
end
end
# Given a namespace mapping and a string containing a
# comma-separated list of namespace aliases, return a list of type
# URIs that correspond to those aliases.
# namespace_map: OpenID::NamespaceMap
def self.to_type_uris(namespace_map, alias_list_s)
return [] if alias_list_s.nil?
alias_list_s.split(',').inject([]) {|uris, name|
type_uri = namespace_map.get_namespace_uri(name)
raise IndexError, "No type defined for attribute name #{name.inspect}" if type_uri.nil?
uris << type_uri
}
end
# An attribute exchange 'fetch_request' message. This message is
# sent by a relying party when it wishes to obtain attributes about
# the subject of an OpenID authentication request.
class FetchRequest < AXMessage
attr_reader :requested_attributes
attr_accessor :update_url
def initialize(update_url = nil)
super()
@mode = 'fetch_request'
@requested_attributes = {}
@update_url = update_url
end
# Add an attribute to this attribute exchange request.
# attribute: AttrInfo, the attribute being requested
# Raises IndexError if the requested attribute is already present
# in this request.
def add(attribute)
if @requested_attributes[attribute.type_uri]
raise IndexError, "The attribute #{attribute.type_uri} has already been requested"
end
@requested_attributes[attribute.type_uri] = attribute
end
# Get the serialized form of this attribute fetch request.
# returns a hash of the arguments
def get_extension_args
aliases = NamespaceMap.new
required = []
if_available = []
ax_args = new_args
@requested_attributes.each{|type_uri, attribute|
if attribute.ns_alias
name = aliases.add_alias(type_uri, attribute.ns_alias)
else
name = aliases.add(type_uri)
end
if attribute.required
required << name
else
if_available << name
end
if attribute.count != 1
ax_args["count.#{name}"] = attribute.count.to_s
end
ax_args["type.#{name}"] = type_uri
}
unless required.empty?
ax_args['required'] = required.join(',')
end
unless if_available.empty?
ax_args['if_available'] = if_available.join(',')
end
return ax_args
end
# Get the type URIs for all attributes that have been marked
# as required.
def get_required_attrs
@requested_attributes.inject([]) {|required, (type_uri, attribute)|
if attribute.required
required << type_uri
else
required
end
}
end
# Extract a FetchRequest from an OpenID message
# message: OpenID::Message
# return a FetchRequest or nil if AX arguments are not present
def self.from_openid_request(oidreq)
message = oidreq.message
ax_args = message.get_args(NS_URI)
return nil if ax_args == {}
req = new
req.parse_extension_args(ax_args)
if req.update_url
realm = message.get_arg(OPENID_NS, 'realm',
message.get_arg(OPENID_NS, 'return_to'))
if realm.nil? or realm.empty?
raise Error, "Cannot validate update_url #{req.update_url.inspect} against absent realm"
end
tr = TrustRoot::TrustRoot.parse(realm)
unless tr.validate_url(req.update_url)
raise Error, "Update URL #{req.update_url.inspect} failed validation against realm #{realm.inspect}"
end
end
return req
end
def parse_extension_args(ax_args)
check_mode(ax_args)
aliases = NamespaceMap.new
ax_args.each{|k,v|
if k.index('type.') == 0
name = k[5..-1]
type_uri = v
aliases.add_alias(type_uri, name)
count_key = 'count.'+name
count_s = ax_args[count_key]
count = 1
if count_s
if count_s == UNLIMITED_VALUES
count = count_s
else
count = count_s.to_i
if count <= 0
raise Error, "Invalid value for count #{count_key.inspect}: #{count_s.inspect}"
end
end
end
add(AttrInfo.new(type_uri, name, false, count))
end
}
required = AX.to_type_uris(aliases, ax_args['required'])
required.each{|type_uri|
@requested_attributes[type_uri].required = true
}
if_available = AX.to_type_uris(aliases, ax_args['if_available'])
all_type_uris = required + if_available
aliases.namespace_uris.each{|type_uri|
unless all_type_uris.member? type_uri
raise Error, "Type URI #{type_uri.inspect} was in the request but not present in 'required' or 'if_available'"
end
}
@update_url = ax_args['update_url']
end
# return the list of AttrInfo objects contained in the FetchRequest
def attributes
@requested_attributes.values
end
# return the list of requested attribute type URIs
def requested_types
@requested_attributes.keys
end
def member?(type_uri)
! @requested_attributes[type_uri].nil?
end
end
# Abstract class that implements a message that has attribute
# keys and values. It contains the common code between
# fetch_response and store_request.
class KeyValueMessage < AXMessage
attr_reader :data
def initialize
super()
@mode = nil
@data = {}
@data.default = []
end
# Add a single value for the given attribute type to the
# message. If there are already values specified for this type,
# this value will be sent in addition to the values already
# specified.
def add_value(type_uri, value)
@data[type_uri] = @data[type_uri] << value
end
# Set the values for the given attribute type. This replaces
# any values that have already been set for this attribute.
def set_values(type_uri, values)
@data[type_uri] = values
end
# Get the extension arguments for the key/value pairs
# contained in this message.
def _get_extension_kv_args(aliases = nil)
aliases = NamespaceMap.new if aliases.nil?
ax_args = new_args
@data.each{|type_uri, values|
name = aliases.add(type_uri)
ax_args['type.'+name] = type_uri
ax_args['count.'+name] = values.size.to_s
values.each_with_index{|value, i|
key = "value.#{name}.#{i+1}"
ax_args[key] = value
}
}
return ax_args
end
# Parse attribute exchange key/value arguments into this object.
def parse_extension_args(ax_args)
check_mode(ax_args)
aliases = NamespaceMap.new
ax_args.each{|k, v|
if k.index('type.') == 0
type_uri = v
name = k[5..-1]
AX.check_alias(name)
aliases.add_alias(type_uri,name)
end
}
aliases.each{|type_uri, name|
count_s = ax_args['count.'+name]
count = count_s.to_i
if count_s.nil?
value = ax_args['value.'+name]
if value.nil?
raise IndexError, "Missing #{'value.'+name} in FetchResponse"
elsif value.empty?
values = []
else
values = [value]
end
elsif count_s.to_i == 0
values = []
else
values = (1..count).inject([]){|l,i|
key = "value.#{name}.#{i}"
v = ax_args[key]
raise IndexError, "Missing #{key} in FetchResponse" if v.nil?
l << v
}
end
@data[type_uri] = values
}
end
# Get a single value for an attribute. If no value was sent
# for this attribute, use the supplied default. If there is more
# than one value for this attribute, this method will fail.
def get_single(type_uri, default = nil)
values = @data[type_uri]
return default if values.empty?
if values.size != 1
raise Error, "More than one value present for #{type_uri.inspect}"
else
return values[0]
end
end
# retrieve the list of values for this attribute
def get(type_uri)
@data[type_uri]
end
# retrieve the list of values for this attribute
def [](type_uri)
@data[type_uri]
end
# get the number of responses for this attribute
def count(type_uri)
@data[type_uri].size
end
end
# A fetch_response attribute exchange message
class FetchResponse < KeyValueMessage
attr_reader :update_url
def initialize(update_url = nil)
super()
@mode = 'fetch_response'
@update_url = update_url
end
# Serialize this object into arguments in the attribute
# exchange namespace
# Takes an optional FetchRequest. If specified, the response will be
# validated against this request, and empty responses for requested
# fields with no data will be sent.
def get_extension_args(request = nil)
aliases = NamespaceMap.new
zero_value_types = []
if request
# Validate the data in the context of the request (the
# same attributes should be present in each, and the
# counts in the response must be no more than the counts
# in the request)
@data.keys.each{|type_uri|
unless request.member? type_uri
raise IndexError, "Response attribute not present in request: #{type_uri.inspect}"
end
}
request.attributes.each{|attr_info|
# Copy the aliases from the request so that reading
# the response in light of the request is easier
if attr_info.ns_alias.nil?
aliases.add(attr_info.type_uri)
else
aliases.add_alias(attr_info.type_uri, attr_info.ns_alias)
end
values = @data[attr_info.type_uri]
if values.empty? # @data defaults to []
zero_value_types << attr_info
end
if attr_info.count != UNLIMITED_VALUES and attr_info.count < values.size
raise Error, "More than the number of requested values were specified for #{attr_info.type_uri.inspect}"
end
}
end
kv_args = _get_extension_kv_args(aliases)
# Add the KV args into the response with the args that are
# unique to the fetch_response
ax_args = new_args
zero_value_types.each{|attr_info|
name = aliases.get_alias(attr_info.type_uri)
kv_args['type.' + name] = attr_info.type_uri
kv_args['count.' + name] = '0'
}
update_url = (request and request.update_url or @update_url)
ax_args['update_url'] = update_url unless update_url.nil?
ax_args.update(kv_args)
return ax_args
end
def parse_extension_args(ax_args)
super
@update_url = ax_args['update_url']
end
# Construct a FetchResponse object from an OpenID library
# SuccessResponse object.
def self.from_success_response(success_response, signed=true)
obj = self.new
if signed
ax_args = success_response.get_signed_ns(obj.ns_uri)
else
ax_args = success_response.message.get_args(obj.ns_uri)
end
begin
obj.parse_extension_args(ax_args)
return obj
rescue Error => e
return nil
end
end
end
# A store request attribute exchange message representation
class StoreRequest < KeyValueMessage
def initialize
super
@mode = 'store_request'
end
def get_extension_args(aliases=nil)
ax_args = new_args
kv_args = _get_extension_kv_args(aliases)
ax_args.update(kv_args)
return ax_args
end
end
# An indication that the store request was processed along with
# this OpenID transaction.
class StoreResponse < AXMessage
SUCCESS_MODE = 'store_response_success'
FAILURE_MODE = 'store_response_failure'
attr_reader :error_message
def initialize(succeeded = true, error_message = nil)
super()
if succeeded and error_message
raise Error, "Error message included in a success response"
end
if succeeded
@mode = SUCCESS_MODE
else
@mode = FAILURE_MODE
end
@error_message = error_message
end
def succeeded?
@mode == SUCCESS_MODE
end
def get_extension_args
ax_args = new_args
if !succeeded? and error_message
ax_args['error'] = @error_message
end
return ax_args
end
end
end
end

View File

@ -0,0 +1,179 @@
# An implementation of the OpenID Provider Authentication Policy
# Extension 1.0
# see: http://openid.net/specs/
require 'openid/extension'
module OpenID
module PAPE
NS_URI = "http://specs.openid.net/extensions/pape/1.0"
AUTH_MULTI_FACTOR_PHYSICAL =
'http://schemas.openid.net/pape/policies/2007/06/multi-factor-physical'
AUTH_MULTI_FACTOR =
'http://schemas.openid.net/pape/policies/2007/06/multi-factor'
AUTH_PHISHING_RESISTANT =
'http://schemas.openid.net/pape/policies/2007/06/phishing-resistant'
TIME_VALIDATOR = /\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\dZ/
# A Provider Authentication Policy request, sent from a relying
# party to a provider
class Request < Extension
attr_accessor :preferred_auth_policies, :max_auth_age, :ns_alias, :ns_uri
def initialize(preferred_auth_policies=[], max_auth_age=nil)
@ns_alias = 'pape'
@ns_uri = NS_URI
@preferred_auth_policies = preferred_auth_policies
@max_auth_age = max_auth_age
end
# Add an acceptable authentication policy URI to this request
# This method is intended to be used by the relying party to add
# acceptable authentication types to the request.
def add_policy_uri(policy_uri)
unless @preferred_auth_policies.member? policy_uri
@preferred_auth_policies << policy_uri
end
end
def get_extension_args
ns_args = {
'preferred_auth_policies' => @preferred_auth_policies.join(' ')
}
ns_args['max_auth_age'] = @max_auth_age.to_s if @max_auth_age
return ns_args
end
# Instantiate a Request object from the arguments in a
# checkid_* OpenID message
# return nil if the extension was not requested.
def self.from_openid_request(oid_req)
pape_req = new
args = oid_req.message.get_args(NS_URI)
if args == {}
return nil
end
pape_req.parse_extension_args(args)
return pape_req
end
# Set the state of this request to be that expressed in these
# PAPE arguments
def parse_extension_args(args)
@preferred_auth_policies = []
policies_str = args['preferred_auth_policies']
if policies_str
policies_str.split(' ').each{|uri|
add_policy_uri(uri)
}
end
max_auth_age_str = args['max_auth_age']
if max_auth_age_str
@max_auth_age = max_auth_age_str.to_i
else
@max_auth_age = nil
end
end
# Given a list of authentication policy URIs that a provider
# supports, this method returns the subset of those types
# that are preferred by the relying party.
def preferred_types(supported_types)
@preferred_auth_policies.select{|uri| supported_types.member? uri}
end
end
# A Provider Authentication Policy response, sent from a provider
# to a relying party
class Response < Extension
attr_accessor :ns_alias, :auth_policies, :auth_time, :nist_auth_level
def initialize(auth_policies=[], auth_time=nil, nist_auth_level=nil)
@ns_alias = 'pape'
@ns_uri = NS_URI
@auth_policies = auth_policies
@auth_time = auth_time
@nist_auth_level = nist_auth_level
end
# Add a policy URI to the response
# see http://openid.net/specs/openid-provider-authentication-policy-extension-1_0-01.html#auth_policies
def add_policy_uri(policy_uri)
@auth_policies << policy_uri unless @auth_policies.member?(policy_uri)
end
# Create a Response object from an OpenID::Consumer::SuccessResponse
def self.from_success_response(success_response)
args = success_response.get_signed_ns(NS_URI)
return nil if args.nil?
pape_resp = new
pape_resp.parse_extension_args(args)
return pape_resp
end
# parse the provider authentication policy arguments into the
# internal state of this object
# if strict is specified, raise an exception when bad data is
# encountered
def parse_extension_args(args, strict=false)
policies_str = args['auth_policies']
if policies_str and policies_str != 'none'
@auth_policies = policies_str.split(' ')
end
nist_level_str = args['nist_auth_level']
if nist_level_str
# special handling of zero to handle to_i behavior
if nist_level_str.strip == '0'
nist_level = 0
else
nist_level = nist_level_str.to_i
# if it's zero here we have a bad value
if nist_level == 0
nist_level = nil
end
end
if nist_level and nist_level >= 0 and nist_level < 5
@nist_auth_level = nist_level
elsif strict
raise ArgumentError, "nist_auth_level must be an integer 0 through 4, not #{nist_level_str.inspect}"
end
end
auth_time_str = args['auth_time']
if auth_time_str
# validate time string
if auth_time_str =~ TIME_VALIDATOR
@auth_time = auth_time_str
elsif strict
raise ArgumentError, "auth_time must be in RFC3339 format"
end
end
end
def get_extension_args
ns_args = {}
if @auth_policies.empty?
ns_args['auth_policies'] = 'none'
else
ns_args['auth_policies'] = @auth_policies.join(' ')
end
if @nist_auth_level
unless (0..4).member? @nist_auth_level
raise ArgumentError, "nist_auth_level must be an integer 0 through 4, not #{@nist_auth_level.inspect}"
end
ns_args['nist_auth_level'] = @nist_auth_level.to_s
end
if @auth_time
unless @auth_time =~ TIME_VALIDATOR
raise ArgumentError, "auth_time must be in RFC3339 format"
end
ns_args['auth_time'] = @auth_time
end
return ns_args
end
end
end
end

View File

@ -0,0 +1,277 @@
require 'openid/extension'
require 'openid/util'
require 'openid/message'
module OpenID
module SReg
DATA_FIELDS = {
'fullname'=>'Full Name',
'nickname'=>'Nickname',
'dob'=>'Date of Birth',
'email'=>'E-mail Address',
'gender'=>'Gender',
'postcode'=>'Postal Code',
'country'=>'Country',
'language'=>'Language',
'timezone'=>'Time Zone',
}
NS_URI_1_0 = 'http://openid.net/sreg/1.0'
NS_URI_1_1 = 'http://openid.net/extensions/sreg/1.1'
NS_URI = NS_URI_1_1
begin
Message.register_namespace_alias(NS_URI_1_1, 'sreg')
rescue NamespaceAliasRegistrationError => e
Util.log(e)
end
# raise ArgumentError if fieldname is not in the defined sreg fields
def OpenID.check_sreg_field_name(fieldname)
unless DATA_FIELDS.member? fieldname
raise ArgumentError, "#{fieldname} is not a defined simple registration field"
end
end
# Does the given endpoint advertise support for simple registration?
def OpenID.supports_sreg?(endpoint)
endpoint.uses_extension(NS_URI_1_1) || endpoint.uses_extension(NS_URI_1_0)
end
# Extract the simple registration namespace URI from the given
# OpenID message. Handles OpenID 1 and 2, as well as both sreg
# namespace URIs found in the wild, as well as missing namespace
# definitions (for OpenID 1)
def OpenID.get_sreg_ns(message)
[NS_URI_1_1, NS_URI_1_0].each{|ns|
if message.namespaces.get_alias(ns)
return ns
end
}
# try to add an alias, since we didn't find one
ns = NS_URI_1_1
begin
message.namespaces.add_alias(ns, 'sreg')
rescue IndexError
raise NamespaceError
end
return ns
end
# The simple registration namespace was not found and could not
# be created using the expected name (there's another extension
# using the name 'sreg')
#
# This is not <em>illegal</em>, for OpenID 2, although it probably
# indicates a problem, since it's not expected that other extensions
# will re-use the alias that is in use for OpenID 1.
#
# If this is an OpenID 1 request, then there is no recourse. This
# should not happen unless some code has modified the namespaces for
# the message that is being processed.
class NamespaceError < ArgumentError
end
# An object to hold the state of a simple registration request.
class Request < Extension
attr_reader :optional, :required, :ns_uri
attr_accessor :policy_url
def initialize(required = nil, optional = nil, policy_url = nil, ns_uri = NS_URI)
super()
@policy_url = policy_url
@ns_uri = ns_uri
@ns_alias = 'sreg'
@required = []
@optional = []
if required
request_fields(required, true, true)
end
if optional
request_fields(optional, false, true)
end
end
# Create a simple registration request that contains the
# fields that were requested in the OpenID request with the
# given arguments
# Takes an OpenID::CheckIDRequest, returns an OpenID::Sreg::Request
# return nil if the extension was not requested.
def self.from_openid_request(request)
# Since we're going to mess with namespace URI mapping, don't
# mutate the object that was passed in.
message = request.message.copy
ns_uri = OpenID::get_sreg_ns(message)
args = message.get_args(ns_uri)
return nil if args == {}
req = new(nil,nil,nil,ns_uri)
req.parse_extension_args(args)
return req
end
# Parse the unqualified simple registration request
# parameters and add them to this object.
#
# This method is essentially the inverse of
# getExtensionArgs. This method restores the serialized simple
# registration request fields.
#
# If you are extracting arguments from a standard OpenID
# checkid_* request, you probably want to use fromOpenIDRequest,
# which will extract the sreg namespace and arguments from the
# OpenID request. This method is intended for cases where the
# OpenID server needs more control over how the arguments are
# parsed than that method provides.
def parse_extension_args(args, strict = false)
required_items = args['required']
unless required_items.nil? or required_items.empty?
required_items.split(',').each{|field_name|
begin
request_field(field_name, true, strict)
rescue ArgumentError
raise if strict
end
}
end
optional_items = args['optional']
unless optional_items.nil? or optional_items.empty?
optional_items.split(',').each{|field_name|
begin
request_field(field_name, false, strict)
rescue ArgumentError
raise if strict
end
}
end
@policy_url = args['policy_url']
end
# A list of all of the simple registration fields that were
# requested, whether they were required or optional.
def all_requested_fields
@required + @optional
end
# Have any simple registration fields been requested?
def were_fields_requested?
!all_requested_fields.empty?
end
# Request the specified field from the OpenID user
# field_name: the unqualified simple registration field name
# required: whether the given field should be presented
# to the user as being a required to successfully complete
# the request
# strict: whether to raise an exception when a field is
# added to a request more than once
# Raises ArgumentError if the field_name is not a simple registration
# field, or if strict is set and a field is added more than once
def request_field(field_name, required=false, strict=false)
OpenID::check_sreg_field_name(field_name)
if strict
if (@required + @optional).member? field_name
raise ArgumentError, 'That field has already been requested'
end
else
return if @required.member? field_name
if @optional.member? field_name
if required
@optional.delete field_name
else
return
end
end
end
if required
@required << field_name
else
@optional << field_name
end
end
# Add the given list of fields to the request.
def request_fields(field_names, required = false, strict = false)
raise ArgumentError unless field_names.respond_to?(:each) and
field_names[0].is_a?(String)
field_names.each{|fn|request_field(fn, required, strict)}
end
# Get a hash of unqualified simple registration arguments
# representing this request.
# This method is essentially the inverse of parse_extension_args.
# This method serializes the simple registration request fields.
def get_extension_args
args = {}
args['required'] = @required.join(',') unless @required.empty?
args['optional'] = @optional.join(',') unless @optional.empty?
args['policy_url'] = @policy_url unless @policy_url.nil?
return args
end
def member?(field_name)
all_requested_fields.member?(field_name)
end
end
# Represents the data returned in a simple registration response
# inside of an OpenID id_res response. This object will be
# created by the OpenID server, added to the id_res response
# object, and then extracted from the id_res message by the Consumer.
class Response < Extension
attr_reader :ns_uri, :data
def initialize(data = {}, ns_uri=NS_URI)
@ns_alias = 'sreg'
@data = data
@ns_uri = ns_uri
end
# Take a Request and a hash of simple registration
# values and create a Response object containing that data.
def self.extract_response(request, data)
arf = request.all_requested_fields
resp_data = data.reject{|k,v| !arf.member?(k) || v.nil? }
new(resp_data, request.ns_uri)
end
# Create an Response object from an
# OpenID::Consumer::SuccessResponse from consumer.complete
# If you set the signed_only parameter to false, unsigned data from
# the id_res message from the server will be processed.
def self.from_success_response(success_response, signed_only = true)
ns_uri = OpenID::get_sreg_ns(success_response.message)
if signed_only
args = success_response.get_signed_ns(ns_uri)
return nil if args.nil? # No signed args, so fail
else
args = success_response.message.get_args(ns_uri)
end
args.reject!{|k,v| !DATA_FIELDS.member?(k) }
new(args, ns_uri)
end
# Get the fields to put in the simple registration namespace
# when adding them to an id_res message.
def get_extension_args
return @data
end
# Read-only hashlike interface.
# Raises an exception if the field name is bad
def [](field_name)
OpenID::check_sreg_field_name(field_name)
data[field_name]
end
def empty?
@data.empty?
end
# XXX is there more to a hashlike interface I should add?
end
end
end

View File

@ -0,0 +1,11 @@
class String
def starts_with?(other)
head = self[0, other.length]
head == other
end
def ends_with?(other)
tail = self[-1 * other.length, other.length]
tail == other
end
end

View File

@ -0,0 +1,238 @@
require 'net/http'
require 'openid'
require 'openid/util'
begin
require 'net/https'
rescue LoadError
OpenID::Util.log('WARNING: no SSL support found. Will not be able ' +
'to fetch HTTPS URLs!')
require 'net/http'
end
MAX_RESPONSE_KB = 1024
module Net
class HTTP
def post_connection_check(hostname)
check_common_name = true
cert = @socket.io.peer_cert
cert.extensions.each { |ext|
next if ext.oid != "subjectAltName"
ext.value.split(/,\s+/).each{ |general_name|
if /\ADNS:(.*)/ =~ general_name
check_common_name = false
reg = Regexp.escape($1).gsub(/\\\*/, "[^.]+")
return true if /\A#{reg}\z/i =~ hostname
elsif /\AIP Address:(.*)/ =~ general_name
check_common_name = false
return true if $1 == hostname
end
}
}
if check_common_name
cert.subject.to_a.each{ |oid, value|
if oid == "CN"
reg = Regexp.escape(value).gsub(/\\\*/, "[^.]+")
return true if /\A#{reg}\z/i =~ hostname
end
}
end
raise OpenSSL::SSL::SSLError, "hostname does not match"
end
end
end
module OpenID
# Our HTTPResponse class extends Net::HTTPResponse with an additional
# method, final_url.
class HTTPResponse
attr_accessor :final_url
attr_accessor :_response
def self._from_net_response(response, final_url, headers=nil)
me = self.new
me._response = response
me.final_url = final_url
return me
end
def method_missing(method, *args)
@_response.send(method, *args)
end
def body=(s)
@_response.instance_variable_set('@body', s)
# XXX Hack to work around ruby's HTTP library behavior. @body
# is only returned if it has been read from the response
# object's socket, but since we're not using a socket in this
# case, we need to set the @read flag to true to avoid a bug in
# Net::HTTPResponse.stream_check when @socket is nil.
@_response.instance_variable_set('@read', true)
end
end
class FetchingError < OpenIDError
end
class HTTPRedirectLimitReached < FetchingError
end
class SSLFetchingError < FetchingError
end
@fetcher = nil
def self.fetch(url, body=nil, headers=nil,
redirect_limit=StandardFetcher::REDIRECT_LIMIT)
return fetcher.fetch(url, body, headers, redirect_limit)
end
def self.fetcher
if @fetcher.nil?
@fetcher = StandardFetcher.new
end
return @fetcher
end
def self.fetcher=(fetcher)
@fetcher = fetcher
end
# Set the default fetcher to use the HTTP proxy defined in the environment
# variable 'http_proxy'.
def self.fetcher_use_env_http_proxy
proxy_string = ENV['http_proxy']
return unless proxy_string
proxy_uri = URI.parse(proxy_string)
@fetcher = StandardFetcher.new(proxy_uri.host, proxy_uri.port,
proxy_uri.user, proxy_uri.password)
end
class StandardFetcher
USER_AGENT = "ruby-openid/#{OpenID::VERSION} (#{RUBY_PLATFORM})"
REDIRECT_LIMIT = 5
TIMEOUT = 60
attr_accessor :ca_file
attr_accessor :timeout
# I can fetch through a HTTP proxy; arguments are as for Net::HTTP::Proxy.
def initialize(proxy_addr=nil, proxy_port=nil,
proxy_user=nil, proxy_pass=nil)
@ca_file = nil
@proxy = Net::HTTP::Proxy(proxy_addr, proxy_port, proxy_user, proxy_pass)
@timeout = TIMEOUT
end
def supports_ssl?(conn)
return conn.respond_to?(:use_ssl=)
end
def make_http(uri)
http = @proxy.new(uri.host, uri.port)
http.read_timeout = @timeout
http.open_timeout = @timeout
return http
end
def set_verified(conn, verify)
if verify
conn.verify_mode = OpenSSL::SSL::VERIFY_PEER
else
conn.verify_mode = OpenSSL::SSL::VERIFY_NONE
end
end
def make_connection(uri)
conn = make_http(uri)
if !conn.is_a?(Net::HTTP)
raise RuntimeError, sprintf("Expected Net::HTTP object from make_http; got %s",
conn.class)
end
if uri.scheme == 'https'
if supports_ssl?(conn)
conn.use_ssl = true
if @ca_file
set_verified(conn, true)
conn.ca_file = @ca_file
else
Util.log("WARNING: making https request to #{uri} without verifying " +
"server certificate; no CA path was specified.")
set_verified(conn, false)
end
else
raise RuntimeError, "SSL support not found; cannot fetch #{uri}"
end
end
return conn
end
def fetch(url, body=nil, headers=nil, redirect_limit=REDIRECT_LIMIT)
unparsed_url = url.dup
url = URI::parse(url)
if url.nil?
raise FetchingError, "Invalid URL: #{unparsed_url}"
end
headers ||= {}
headers['User-agent'] ||= USER_AGENT
begin
conn = make_connection(url)
response = nil
response = conn.start {
# Check the certificate against the URL's hostname
if supports_ssl?(conn) and conn.use_ssl?
conn.post_connection_check(url.host)
end
if body.nil?
conn.request_get(url.request_uri, headers)
else
headers["Content-type"] ||= "application/x-www-form-urlencoded"
conn.request_post(url.request_uri, body, headers)
end
}
rescue RuntimeError => why
raise why
rescue OpenSSL::SSL::SSLError => why
raise SSLFetchingError, "Error connecting to SSL URL #{url}: #{why}"
rescue FetchingError => why
raise why
rescue Exception => why
# Things we've caught here include a Timeout::Error, which descends
# from SignalException.
raise FetchingError, "Error fetching #{url}: #{why}"
end
case response
when Net::HTTPRedirection
if redirect_limit <= 0
raise HTTPRedirectLimitReached.new(
"Too many redirects, not fetching #{response['location']}")
end
begin
return fetch(response['location'], body, headers, redirect_limit - 1)
rescue HTTPRedirectLimitReached => e
raise e
rescue FetchingError => why
raise FetchingError, "Error encountered in redirect from #{url}: #{why}"
end
else
return HTTPResponse._from_net_response(response, unparsed_url)
end
end
end
end

View File

@ -0,0 +1,136 @@
module OpenID
class KVFormError < Exception
end
module Util
def Util.seq_to_kv(seq, strict=false)
# Represent a sequence of pairs of strings as newline-terminated
# key:value pairs. The pairs are generated in the order given.
#
# @param seq: The pairs
#
# returns a string representation of the sequence
err = lambda { |msg|
msg = "seq_to_kv warning: #{msg}: #{seq.inspect}"
if strict
raise KVFormError, msg
else
Util.log(msg)
end
}
lines = []
seq.each { |k, v|
if !k.is_a?(String)
err.call("Converting key to string: #{k.inspect}")
k = k.to_s
end
if !k.index("\n").nil?
raise KVFormError, "Invalid input for seq_to_kv: key contains newline: #{k.inspect}"
end
if !k.index(":").nil?
raise KVFormError, "Invalid input for seq_to_kv: key contains colon: #{k.inspect}"
end
if k.strip() != k
err.call("Key has whitespace at beginning or end: #{k.inspect}")
end
if !v.is_a?(String)
err.call("Converting value to string: #{v.inspect}")
v = v.to_s
end
if !v.index("\n").nil?
raise KVFormError, "Invalid input for seq_to_kv: value contains newline: #{v.inspect}"
end
if v.strip() != v
err.call("Value has whitespace at beginning or end: #{v.inspect}")
end
lines << k + ":" + v + "\n"
}
return lines.join("")
end
def Util.kv_to_seq(data, strict=false)
# After one parse, seq_to_kv and kv_to_seq are inverses, with no
# warnings:
#
# seq = kv_to_seq(s)
# seq_to_kv(kv_to_seq(seq)) == seq
err = lambda { |msg|
msg = "kv_to_seq warning: #{msg}: #{data.inspect}"
if strict
raise KVFormError, msg
else
Util.log(msg)
end
}
lines = data.split("\n")
if data.length == 0
return []
end
if data[-1].chr != "\n"
err.call("Does not end in a newline")
# We don't expect the last element of lines to be an empty
# string because split() doesn't behave that way.
end
pairs = []
line_num = 0
lines.each { |line|
line_num += 1
# Ignore blank lines
if line.strip() == ""
next
end
pair = line.split(':', 2)
if pair.length == 2
k, v = pair
k_s = k.strip()
if k_s != k
msg = "In line #{line_num}, ignoring leading or trailing whitespace in key #{k.inspect}"
err.call(msg)
end
if k_s.length == 0
err.call("In line #{line_num}, got empty key")
end
v_s = v.strip()
if v_s != v
msg = "In line #{line_num}, ignoring leading or trailing whitespace in value #{v.inspect}"
err.call(msg)
end
pairs << [k_s, v_s]
else
err.call("Line #{line_num} does not contain a colon")
end
}
return pairs
end
def Util.dict_to_kv(d)
return seq_to_kv(d.entries.sort)
end
def Util.kv_to_dict(s)
seq = kv_to_seq(s)
return Hash[*seq.flatten]
end
end
end

View File

@ -0,0 +1,58 @@
require "openid/message"
require "openid/fetchers"
module OpenID
# Exception that is raised when the server returns a 400 response
# code to a direct request.
class ServerError < OpenIDError
attr_reader :error_text, :error_code, :message
def initialize(error_text, error_code, message)
super(error_text)
@error_text = error_text
@error_code = error_code
@message = message
end
def self.from_message(msg)
error_text = msg.get_arg(OPENID_NS, 'error',
'<no error message supplied>')
error_code = msg.get_arg(OPENID_NS, 'error_code')
return self.new(error_text, error_code, msg)
end
end
class KVPostNetworkError < OpenIDError
end
class HTTPStatusError < OpenIDError
end
class Message
def self.from_http_response(response, server_url)
msg = self.from_kvform(response.body)
case response.code.to_i
when 200
return msg
when 206
return msg
when 400
raise ServerError.from_message(msg)
else
error_message = "bad status code from server #{server_url}: "\
"#{response.code}"
raise HTTPStatusError.new(error_message)
end
end
end
# Send the message to the server via HTTP POST and receive and parse
# a response in KV Form
def self.make_kv_post(request_message, server_url)
begin
http_response = self.fetch(server_url, request_message.to_url_encoded)
rescue Exception
raise KVPostNetworkError.new("Unable to contact OpenID server: #{$!.to_s}")
end
return Message.from_http_response(http_response, server_url)
end
end

View File

@ -0,0 +1,553 @@
require 'openid/util'
require 'openid/kvform'
module OpenID
IDENTIFIER_SELECT = 'http://specs.openid.net/auth/2.0/identifier_select'
# URI for Simple Registration extension, the only commonly deployed
# OpenID 1.x extension, and so a special case.
SREG_URI = 'http://openid.net/sreg/1.0'
# The OpenID 1.x namespace URIs
OPENID1_NS = 'http://openid.net/signon/1.0'
OPENID11_NS = 'http://openid.net/signon/1.1'
OPENID1_NAMESPACES = [OPENID1_NS, OPENID11_NS]
# The OpenID 2.0 namespace URI
OPENID2_NS = 'http://specs.openid.net/auth/2.0'
# The namespace consisting of pairs with keys that are prefixed with
# "openid." but not in another namespace.
NULL_NAMESPACE = :null_namespace
# The null namespace, when it is an allowed OpenID namespace
OPENID_NS = :openid_namespace
# The top-level namespace, excluding all pairs with keys that start
# with "openid."
BARE_NS = :bare_namespace
# Limit, in bytes, of identity provider and return_to URLs,
# including response payload. See OpenID 1.1 specification,
# Appendix D.
OPENID1_URL_LIMIT = 2047
# All OpenID protocol fields. Used to check namespace aliases.
OPENID_PROTOCOL_FIELDS = [
'ns', 'mode', 'error', 'return_to',
'contact', 'reference', 'signed',
'assoc_type', 'session_type',
'dh_modulus', 'dh_gen',
'dh_consumer_public', 'claimed_id',
'identity', 'realm', 'invalidate_handle',
'op_endpoint', 'response_nonce', 'sig',
'assoc_handle', 'trust_root', 'openid',
]
# Sentinel used for Message implementation to indicate that getArg
# should raise an exception instead of returning a default.
NO_DEFAULT = :no_default
# Raised if the generic OpenID namespace is accessed when there
# is no OpenID namespace set for this message.
class UndefinedOpenIDNamespace < Exception; end
# Raised when an alias or namespace URI has already been registered.
class NamespaceAliasRegistrationError < Exception; end
# Raised if openid.ns is not a recognized value.
# See Message class variable @@allowed_openid_namespaces
class InvalidOpenIDNamespace < Exception; end
class Message
attr_reader :namespaces
# Raised when key lookup fails
class KeyNotFound < IndexError ; end
# Namespace / alias registration map. See
# register_namespace_alias.
@@registered_aliases = {}
# Registers a (namespace URI, alias) mapping in a global namespace
# alias map. Raises NamespaceAliasRegistrationError if either the
# namespace URI or alias has already been registered with a
# different value. This function is required if you want to use a
# namespace with an OpenID 1 message.
def Message.register_namespace_alias(namespace_uri, alias_)
if @@registered_aliases[alias_] == namespace_uri
return
end
if @@registered_aliases.values.include?(namespace_uri)
raise NamespaceAliasRegistrationError,
'Namespace uri #{namespace_uri} already registered'
end
if @@registered_aliases.member?(alias_)
raise NamespaceAliasRegistrationError,
'Alias #{alias_} already registered'
end
@@registered_aliases[alias_] = namespace_uri
end
@@allowed_openid_namespaces = [OPENID1_NS, OPENID2_NS, OPENID11_NS]
# Raises InvalidNamespaceError if you try to instantiate a Message
# with a namespace not in the above allowed list
def initialize(openid_namespace=nil)
@args = {}
@namespaces = NamespaceMap.new
if openid_namespace
implicit = OPENID1_NAMESPACES.member? openid_namespace
self.set_openid_namespace(openid_namespace, implicit)
else
@openid_ns_uri = nil
end
end
# Construct a Message containing a set of POST arguments.
# Raises InvalidNamespaceError if you try to instantiate a Message
# with a namespace not in the above allowed list
def Message.from_post_args(args)
m = Message.new
openid_args = {}
args.each do |key,value|
if value.is_a?(Array)
raise ArgumentError, "Query dict must have one value for each key, " +
"not lists of values. Query is #{args.inspect}"
end
prefix, rest = key.split('.', 2)
if prefix != 'openid' or rest.nil?
m.set_arg(BARE_NS, key, value)
else
openid_args[rest] = value
end
end
m._from_openid_args(openid_args)
return m
end
# Construct a Message from a parsed KVForm message.
# Raises InvalidNamespaceError if you try to instantiate a Message
# with a namespace not in the above allowed list
def Message.from_openid_args(openid_args)
m = Message.new
m._from_openid_args(openid_args)
return m
end
# Raises InvalidNamespaceError if you try to instantiate a Message
# with a namespace not in the above allowed list
def _from_openid_args(openid_args)
ns_args = []
# resolve namespaces
openid_args.each { |rest, value|
ns_alias, ns_key = rest.split('.', 2)
if ns_key.nil?
ns_alias = NULL_NAMESPACE
ns_key = rest
end
if ns_alias == 'ns'
@namespaces.add_alias(value, ns_key)
elsif ns_alias == NULL_NAMESPACE and ns_key == 'ns'
set_openid_namespace(value, false)
else
ns_args << [ns_alias, ns_key, value]
end
}
# implicitly set an OpenID 1 namespace
unless get_openid_namespace
set_openid_namespace(OPENID1_NS, true)
end
# put the pairs into the appropriate namespaces
ns_args.each { |ns_alias, ns_key, value|
ns_uri = @namespaces.get_namespace_uri(ns_alias)
unless ns_uri
ns_uri = _get_default_namespace(ns_alias)
unless ns_uri
ns_uri = get_openid_namespace
ns_key = "#{ns_alias}.#{ns_key}"
else
@namespaces.add_alias(ns_uri, ns_alias, true)
end
end
self.set_arg(ns_uri, ns_key, value)
}
end
def _get_default_namespace(mystery_alias)
# only try to map an alias to a default if it's an
# OpenID 1.x namespace
if is_openid1
@@registered_aliases[mystery_alias]
end
end
def set_openid_namespace(openid_ns_uri, implicit)
if !@@allowed_openid_namespaces.include?(openid_ns_uri)
raise InvalidOpenIDNamespace, "Invalid null namespace: #{openid_ns_uri}"
end
@namespaces.add_alias(openid_ns_uri, NULL_NAMESPACE, implicit)
@openid_ns_uri = openid_ns_uri
end
def get_openid_namespace
return @openid_ns_uri
end
def is_openid1
return OPENID1_NAMESPACES.member?(@openid_ns_uri)
end
def is_openid2
return @openid_ns_uri == OPENID2_NS
end
# Create a message from a KVForm string
def Message.from_kvform(kvform_string)
return Message.from_openid_args(Util.kv_to_dict(kvform_string))
end
def copy
return Marshal.load(Marshal.dump(self))
end
# Return all arguments with "openid." in from of namespaced arguments.
def to_post_args
args = {}
# add namespace defs to the output
@namespaces.each { |ns_uri, ns_alias|
if @namespaces.implicit?(ns_uri)
next
end
if ns_alias == NULL_NAMESPACE
ns_key = 'openid.ns'
else
ns_key = 'openid.ns.' + ns_alias
end
args[ns_key] = ns_uri
}
@args.each { |k, value|
ns_uri, ns_key = k
key = get_key(ns_uri, ns_key)
args[key] = value
}
return args
end
# Return all namespaced arguments, failing if any non-namespaced arguments
# exist.
def to_args
post_args = self.to_post_args
kvargs = {}
post_args.each { |k,v|
if !k.starts_with?('openid.')
raise ArgumentError, "This message can only be encoded as a POST, because it contains arguments that are not prefixed with 'openid.'"
else
kvargs[k[7..-1]] = v
end
}
return kvargs
end
# Generate HTML form markup that contains the values in this
# message, to be HTTP POSTed as x-www-form-urlencoded UTF-8.
def to_form_markup(action_url, form_tag_attrs=nil, submit_text='Continue')
form_tag_attr_map = {}
if form_tag_attrs
form_tag_attrs.each { |name, attr|
form_tag_attr_map[name] = attr
}
end
form_tag_attr_map['action'] = action_url
form_tag_attr_map['method'] = 'post'
form_tag_attr_map['accept-charset'] = 'UTF-8'
form_tag_attr_map['enctype'] = 'application/x-www-form-urlencoded'
markup = "<form "
form_tag_attr_map.each { |k, v|
markup += " #{k}=\"#{v}\""
}
markup += ">\n"
to_post_args.each { |k,v|
markup += "<input type='hidden' name='#{k}' value='#{v}' />\n"
}
markup += "<input type='submit' value='#{submit_text}' />\n"
markup += "\n</form>"
return markup
end
# Generate a GET URL with the paramters in this message attacked as
# query parameters.
def to_url(base_url)
return Util.append_args(base_url, self.to_post_args)
end
# Generate a KVForm string that contains the parameters in this message.
# This will fail is the message contains arguments outside of the
# "openid." prefix.
def to_kvform
return Util.dict_to_kv(to_args)
end
# Generate an x-www-urlencoded string.
def to_url_encoded
args = to_post_args.map.sort
return Util.urlencode(args)
end
# Convert an input value into the internally used values of this obejct.
def _fix_ns(namespace)
if namespace == OPENID_NS
unless @openid_ns_uri
raise UndefinedOpenIDNamespace, 'OpenID namespace not set'
else
namespace = @openid_ns_uri
end
end
if namespace == BARE_NS
return namespace
end
if !namespace.is_a?(String)
raise ArgumentError, ("Namespace must be BARE_NS, OPENID_NS or "\
"a string. Got #{namespace.inspect}")
end
if namespace.index(':').nil?
msg = ("OpenID 2.0 namespace identifiers SHOULD be URIs. "\
"Got #{namespace.inspect}")
Util.log(msg)
if namespace == 'sreg'
msg = "Using #{SREG_URI} instead of \"sreg\" as namespace"
Util.log(msg)
return SREG_URI
end
end
return namespace
end
def has_key?(namespace, ns_key)
namespace = _fix_ns(namespace)
return @args.member?([namespace, ns_key])
end
# Get the key for a particular namespaced argument
def get_key(namespace, ns_key)
namespace = _fix_ns(namespace)
return ns_key if namespace == BARE_NS
ns_alias = @namespaces.get_alias(namespace)
# no alias is defined, so no key can exist
return nil if ns_alias.nil?
if ns_alias == NULL_NAMESPACE
tail = ns_key
else
tail = "#{ns_alias}.#{ns_key}"
end
return 'openid.' + tail
end
# Get a value for a namespaced key.
def get_arg(namespace, key, default=nil)
namespace = _fix_ns(namespace)
@args.fetch([namespace, key]) {
if default == NO_DEFAULT
raise KeyNotFound, "<#{namespace}>#{key} not in this message"
else
default
end
}
end
# Get the arguments that are defined for this namespace URI.
def get_args(namespace)
namespace = _fix_ns(namespace)
args = {}
@args.each { |k,v|
pair_ns, ns_key = k
args[ns_key] = v if pair_ns == namespace
}
return args
end
# Set multiple key/value pairs in one call.
def update_args(namespace, updates)
namespace = _fix_ns(namespace)
updates.each {|k,v| set_arg(namespace, k, v)}
end
# Set a single argument in this namespace
def set_arg(namespace, key, value)
namespace = _fix_ns(namespace)
@args[[namespace, key].freeze] = value
if namespace != BARE_NS
@namespaces.add(namespace)
end
end
# Remove a single argument from this namespace.
def del_arg(namespace, key)
namespace = _fix_ns(namespace)
_key = [namespace, key]
@args.delete(_key)
end
def ==(other)
other.is_a?(self.class) && @args == other.instance_eval { @args }
end
def get_aliased_arg(aliased_key, default=nil)
if aliased_key == 'ns'
return get_openid_namespace()
end
ns_alias, key = aliased_key.split('.', 2)
if ns_alias == 'ns'
uri = @namespaces.get_namespace_uri(key)
if uri.nil? and default == NO_DEFAULT
raise KeyNotFound, "Namespace #{key} not defined when looking "\
"for #{aliased_key}"
else
return (uri.nil? ? default : uri)
end
end
if key.nil?
key = aliased_key
ns = nil
else
ns = @namespaces.get_namespace_uri(ns_alias)
end
if ns.nil?
key = aliased_key
ns = get_openid_namespace
end
return get_arg(ns, key, default)
end
end
# Maintains a bidirectional map between namespace URIs and aliases.
class NamespaceMap
def initialize
@alias_to_namespace = {}
@namespace_to_alias = {}
@implicit_namespaces = []
end
def get_alias(namespace_uri)
@namespace_to_alias[namespace_uri]
end
def get_namespace_uri(namespace_alias)
@alias_to_namespace[namespace_alias]
end
# Add an alias from this namespace URI to the alias.
def add_alias(namespace_uri, desired_alias, implicit=false)
# Check that desired_alias is not an openid protocol field as
# per the spec.
Util.assert(!OPENID_PROTOCOL_FIELDS.include?(desired_alias),
"#{desired_alias} is not an allowed namespace alias")
# check that there is not a namespace already defined for the
# desired alias
current_namespace_uri = @alias_to_namespace.fetch(desired_alias, nil)
if current_namespace_uri and current_namespace_uri != namespace_uri
raise IndexError, "Cannot map #{namespace_uri} to alias #{desired_alias}. #{current_namespace_uri} is already mapped to alias #{desired_alias}"
end
# Check that desired_alias does not contain a period as per the
# spec.
if desired_alias.is_a?(String)
Util.assert(desired_alias.index('.').nil?,
"#{desired_alias} must not contain a dot")
end
# check that there is not already a (different) alias for this
# namespace URI.
_alias = @namespace_to_alias[namespace_uri]
if _alias and _alias != desired_alias
raise IndexError, "Cannot map #{namespace_uri} to alias #{desired_alias}. It is already mapped to alias #{_alias}"
end
@alias_to_namespace[desired_alias] = namespace_uri
@namespace_to_alias[namespace_uri] = desired_alias
@implicit_namespaces << namespace_uri if implicit
return desired_alias
end
# Add this namespace URI to the mapping, without caring what alias
# it ends up with.
def add(namespace_uri)
# see if this namepace is already mapped to an alias
_alias = @namespace_to_alias[namespace_uri]
return _alias if _alias
# Fall back to generating a numberical alias
i = 0
while true
_alias = 'ext' + i.to_s
begin
add_alias(namespace_uri, _alias)
rescue IndexError
i += 1
else
return _alias
end
end
raise StandardError, 'Unreachable'
end
def member?(namespace_uri)
@namespace_to_alias.has_key?(namespace_uri)
end
def each
@namespace_to_alias.each {|k,v| yield k,v}
end
def namespace_uris
# Return an iterator over the namespace URIs
return @namespace_to_alias.keys()
end
def implicit?(namespace_uri)
return @implicit_namespaces.member?(namespace_uri)
end
def aliases
# Return an iterator over the aliases
return @alias_to_namespace.keys()
end
end
end

View File

@ -0,0 +1,8 @@
require 'openid/util'
module OpenID
# An error in the OpenID protocol
class ProtocolError < OpenIDError
end
end

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,271 @@
require 'fileutils'
require 'pathname'
require 'tempfile'
require 'openid/util'
require 'openid/store/interface'
require 'openid/association'
module OpenID
module Store
class Filesystem < Interface
@@FILENAME_ALLOWED = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-".split("")
# Create a Filesystem store instance, putting all data in +directory+.
def initialize(directory)
p_dir = Pathname.new(directory)
@nonce_dir = p_dir.join('nonces')
@association_dir = p_dir.join('associations')
@temp_dir = p_dir.join('temp')
self.ensure_dir(@nonce_dir)
self.ensure_dir(@association_dir)
self.ensure_dir(@temp_dir)
end
# Create a unique filename for a given server url and handle. The
# filename that is returned will contain the domain name from the
# server URL for ease of human inspection of the data dir.
def get_association_filename(server_url, handle)
unless server_url.index('://')
raise ArgumentError, "Bad server URL: #{server_url}"
end
proto, rest = server_url.split('://', 2)
domain = filename_escape(rest.split('/',2)[0])
url_hash = safe64(server_url)
if handle
handle_hash = safe64(handle)
else
handle_hash = ''
end
filename = [proto,domain,url_hash,handle_hash].join('-')
@association_dir.join(filename)
end
# Store an association in the assoc directory
def store_association(server_url, association)
assoc_s = association.serialize
filename = get_association_filename(server_url, association.handle)
f, tmp = mktemp
begin
begin
f.write(assoc_s)
f.fsync
ensure
f.close
end
begin
File.rename(tmp, filename)
rescue Errno::EEXIST
begin
File.unlink(filename)
rescue Errno::ENOENT
# do nothing
end
File.rename(tmp, filename)
end
rescue
self.remove_if_present(tmp)
raise
end
end
# Retrieve an association
def get_association(server_url, handle=nil)
# the filename with empty handle is the prefix for the associations
# for a given server url
filename = get_association_filename(server_url, handle)
if handle
return _get_association(filename)
end
assoc_filenames = Dir.glob(filename.to_s + '*')
assocs = assoc_filenames.collect do |f|
_get_association(f)
end
assocs = assocs.find_all { |a| not a.nil? }
assocs = assocs.sort_by { |a| a.issued }
return nil if assocs.empty?
return assocs[-1]
end
def _get_association(filename)
begin
assoc_file = File.open(filename, "r")
rescue Errno::ENOENT
return nil
else
begin
assoc_s = assoc_file.read
ensure
assoc_file.close
end
begin
association = Association.deserialize(assoc_s)
rescue
self.remove_if_present(filename)
return nil
end
# clean up expired associations
if association.expires_in == 0
self.remove_if_present(filename)
return nil
else
return association
end
end
end
# Remove an association if it exists, otherwise do nothing.
def remove_association(server_url, handle)
assoc = get_association(server_url, handle)
if assoc.nil?
return false
else
filename = get_association_filename(server_url, handle)
return self.remove_if_present(filename)
end
end
# Return whether the nonce is valid
def use_nonce(server_url, timestamp, salt)
return false if (timestamp - Time.now.to_i).abs > Nonce.skew
if server_url and !server_url.empty?
proto, rest = server_url.split('://',2)
else
proto, rest = '',''
end
raise "Bad server URL" unless proto && rest
domain = filename_escape(rest.split('/',2)[0])
url_hash = safe64(server_url)
salt_hash = safe64(salt)
nonce_fn = '%08x-%s-%s-%s-%s'%[timestamp, proto, domain, url_hash, salt_hash]
filename = @nonce_dir.join(nonce_fn)
begin
fd = File.new(filename, File::CREAT | File::EXCL | File::WRONLY, 0200)
fd.close
return true
rescue Errno::EEXIST
return false
end
end
# Remove expired entries from the database. This is potentially expensive,
# so only run when it is acceptable to take time.
def cleanup
cleanup_associations
cleanup_nonces
end
def cleanup_associations
association_filenames = Dir[@association_dir.join("*").to_s]
count = 0
association_filenames.each do |af|
begin
f = File.open(af, 'r')
rescue Errno::ENOENT
next
else
begin
assoc_s = f.read
ensure
f.close
end
begin
association = OpenID::Association.deserialize(assoc_s)
rescue StandardError
self.remove_if_present(af)
next
else
if association.expires_in == 0
self.remove_if_present(af)
count += 1
end
end
end
end
return count
end
def cleanup_nonces
nonces = Dir[@nonce_dir.join("*").to_s]
now = Time.now.to_i
count = 0
nonces.each do |filename|
nonce = filename.split('/')[-1]
timestamp = nonce.split('-', 2)[0].to_i(16)
nonce_age = (timestamp - now).abs
if nonce_age > Nonce.skew
self.remove_if_present(filename)
count += 1
end
end
return count
end
protected
# Create a temporary file and return the File object and filename.
def mktemp
f = Tempfile.new('tmp', @temp_dir)
[f, f.path]
end
# create a safe filename from a url
def filename_escape(s)
s = '' if s.nil?
filename_chunks = []
s.split('').each do |c|
if @@FILENAME_ALLOWED.index(c)
filename_chunks << c
else
filename_chunks << sprintf("_%02X", c[0])
end
end
filename_chunks.join("")
end
def safe64(s)
s = OpenID::CryptUtil.sha1(s)
s = OpenID::Util.to_base64(s)
s.gsub!('+', '_')
s.gsub!('/', '.')
s.gsub!('=', '')
return s
end
# remove file if present in filesystem
def remove_if_present(filename)
begin
File.unlink(filename)
rescue Errno::ENOENT
return false
end
return true
end
# ensure that a path exists
def ensure_dir(dir_name)
FileUtils::mkdir_p(dir_name)
end
end
end
end

View File

@ -0,0 +1,75 @@
require 'openid/util'
module OpenID
# Stores for Associations and nonces. Used by both the Consumer and
# the Server. If you have a database abstraction layer or other
# state storage in your application or framework already, you can
# implement the store interface.
module Store
# Abstract Store
# Changes in 2.0:
# * removed store_nonce, get_auth_key, is_dumb
# * changed use_nonce to support one-way nonces
# * added cleanup_nonces, cleanup_associations, cleanup
class Interface < Object
# Put a Association object into storage.
# When implementing a store, don't assume that there are any limitations
# on the character set of the server_url. In particular, expect to see
# unescaped non-url-safe characters in the server_url field.
def store_association(server_url, association)
raise NotImplementedError
end
# Returns a Association object from storage that matches
# the server_url. Returns nil if no such association is found or if
# the one matching association is expired. (Is allowed to GC expired
# associations when found.)
def get_association(server_url, handle=nil)
raise NotImplementedError
end
# If there is a matching association, remove it from the store and
# return true, otherwise return false.
def remove_association(server_url, handle)
raise NotImplementedError
end
# Return true if the nonce has not been used before, and store it
# for a while to make sure someone doesn't try to use the same value
# again. Return false if the nonce has already been used or if the
# timestamp is not current.
# You can use OpenID::Store::Nonce::SKEW for your timestamp window.
# server_url: URL of the server from which the nonce originated
# timestamp: time the nonce was created in seconds since unix epoch
# salt: A random string that makes two nonces issued by a server in
# the same second unique
def use_nonce(server_url, timestamp, salt)
raise NotImplementedError
end
# Remove expired nonces from the store
# Discards any nonce that is old enough that it wouldn't pass use_nonce
# Not called during normal library operation, this method is for store
# admins to keep their storage from filling up with expired data
def cleanup_nonces
raise NotImplementedError
end
# Remove expired associations from the store
# Not called during normal library operation, this method is for store
# admins to keep their storage from filling up with expired data
def cleanup_associations
raise NotImplementedError
end
# Remove expired nonces and associations from the store
# Not called during normal library operation, this method is for store
# admins to keep their storage from filling up with expired data
def cleanup
return cleanup_nonces, cleanup_associations
end
end
end
end

View File

@ -0,0 +1,84 @@
require 'openid/store/interface'
module OpenID
module Store
# An in-memory implementation of Store. This class is mainly used
# for testing, though it may be useful for long-running single
# process apps. Note that this store is NOT thread-safe.
#
# You should probably be looking at OpenID::Store::Filesystem
class Memory < Interface
def initialize
@associations = {}
@associations.default = {}
@nonces = {}
end
def store_association(server_url, assoc)
assocs = @associations[server_url]
@associations[server_url] = assocs.merge({assoc.handle => deepcopy(assoc)})
end
def get_association(server_url, handle=nil)
assocs = @associations[server_url]
assoc = nil
if handle
assoc = assocs[handle]
else
assoc = assocs.values.sort{|a,b| a.issued <=> b.issued}[-1]
end
return assoc
end
def remove_association(server_url, handle)
assocs = @associations[server_url]
if assocs.delete(handle)
return true
else
return false
end
end
def use_nonce(server_url, timestamp, salt)
return false if (timestamp - Time.now.to_i).abs > Nonce.skew
nonce = [server_url, timestamp, salt].join('')
return false if @nonces[nonce]
@nonces[nonce] = timestamp
return true
end
def cleanup_associations
count = 0
@associations.each{|server_url, assocs|
assocs.each{|handle, assoc|
if assoc.expires_in == 0
assocs.delete(handle)
count += 1
end
}
}
return count
end
def cleanup_nonces
count = 0
now = Time.now.to_i
@nonces.each{|nonce, timestamp|
if (timestamp - now).abs > Nonce.skew
@nonces.delete(nonce)
count += 1
end
}
return count
end
protected
def deepcopy(o)
Marshal.load(Marshal.dump(o))
end
end
end
end

View File

@ -0,0 +1,68 @@
require 'openid/cryptutil'
require 'date'
require 'time'
module OpenID
module Nonce
DEFAULT_SKEW = 60*60*5
TIME_FMT = '%Y-%m-%dT%H:%M:%SZ'
TIME_STR_LEN = '0000-00-00T00:00:00Z'.size
@@NONCE_CHRS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
TIME_VALIDATOR = /\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\dZ/
@skew = DEFAULT_SKEW
# The allowed nonce time skew in seconds. Defaults to 5 hours.
# Used for checking nonce validity, and by stores' cleanup methods.
def Nonce.skew
@skew
end
def Nonce.skew=(new_skew)
@skew = new_skew
end
# Extract timestamp from a nonce string
def Nonce.split_nonce(nonce_str)
timestamp_str = nonce_str[0...TIME_STR_LEN]
raise ArgumentError if timestamp_str.size < TIME_STR_LEN
raise ArgumentError unless timestamp_str.match(TIME_VALIDATOR)
ts = Time.parse(timestamp_str).to_i
raise ArgumentError if ts < 0
return ts, nonce_str[TIME_STR_LEN..-1]
end
# Is the timestamp that is part of the specified nonce string
# within the allowed clock-skew of the current time?
def Nonce.check_timestamp(nonce_str, allowed_skew=nil, now=nil)
allowed_skew = skew if allowed_skew.nil?
begin
stamp, foo = split_nonce(nonce_str)
rescue ArgumentError # bad timestamp
return false
end
now = Time.now.to_i unless now
# times before this are too old
past = now - allowed_skew
# times newer than this are too far in the future
future = now + allowed_skew
return (past <= stamp and stamp <= future)
end
# generate a nonce with the specified timestamp (defaults to now)
def Nonce.mk_nonce(time = nil)
salt = CryptUtil::random_string(6, @@NONCE_CHRS)
if time.nil?
t = Time.now.getutc
else
t = Time.at(time).getutc
end
time_str = t.strftime(TIME_FMT)
return time_str + salt
end
end
end

View File

@ -0,0 +1,349 @@
require 'uri'
require 'openid/urinorm'
module OpenID
class RealmVerificationRedirected < Exception
# Attempting to verify this realm resulted in a redirect.
def initialize(relying_party_url, rp_url_after_redirects)
@relying_party_url = relying_party_url
@rp_url_after_redirects = rp_url_after_redirects
end
def to_s
return "Attempting to verify #{@relying_party_url} resulted in " +
"redirect to #{@rp_url_after_redirects}"
end
end
module TrustRoot
TOP_LEVEL_DOMAINS = %w'
ac ad ae aero af ag ai al am an ao aq ar arpa as asia at
au aw ax az ba bb bd be bf bg bh bi biz bj bm bn bo br bs bt
bv bw by bz ca cat cc cd cf cg ch ci ck cl cm cn co com coop
cr cu cv cx cy cz de dj dk dm do dz ec edu ee eg er es et eu
fi fj fk fm fo fr ga gb gd ge gf gg gh gi gl gm gn gov gp gq
gr gs gt gu gw gy hk hm hn hr ht hu id ie il im in info int
io iq ir is it je jm jo jobs jp ke kg kh ki km kn kp kr kw
ky kz la lb lc li lk lr ls lt lu lv ly ma mc md me mg mh mil
mk ml mm mn mo mobi mp mq mr ms mt mu museum mv mw mx my mz
na name nc ne net nf ng ni nl no np nr nu nz om org pa pe pf
pg ph pk pl pm pn pr pro ps pt pw py qa re ro rs ru rw sa sb
sc sd se sg sh si sj sk sl sm sn so sr st su sv sy sz tc td
tel tf tg th tj tk tl tm tn to tp tr travel tt tv tw tz ua
ug uk us uy uz va vc ve vg vi vn vu wf ws xn--0zwm56d
xn--11b5bs3a9aj6g xn--80akhbyknj4f xn--9t4b11yi5a
xn--deba0ad xn--g6w251d xn--hgbk6aj7f53bba
xn--hlcj6aya9esc7a xn--jxalpdlp xn--kgbechtv xn--zckzah ye
yt yu za zm zw'
ALLOWED_PROTOCOLS = ['http', 'https']
# The URI for relying party discovery, used in realm verification.
#
# XXX: This should probably live somewhere else (like in
# OpenID or OpenID::Yadis somewhere)
RP_RETURN_TO_URL_TYPE = 'http://specs.openid.net/auth/2.0/return_to'
# If the endpoint is a relying party OpenID return_to endpoint,
# return the endpoint URL. Otherwise, return None.
#
# This function is intended to be used as a filter for the Yadis
# filtering interface.
#
# endpoint: An XRDS BasicServiceEndpoint, as returned by
# performing Yadis dicovery.
#
# returns the endpoint URL or None if the endpoint is not a
# relying party endpoint.
def TrustRoot._extract_return_url(endpoint)
if endpoint.matchTypes([RP_RETURN_TO_URL_TYPE])
return endpoint.uri
else
return nil
end
end
# Is the return_to URL under one of the supplied allowed
# return_to URLs?
def TrustRoot.return_to_matches(allowed_return_to_urls, return_to)
allowed_return_to_urls.each { |allowed_return_to|
# A return_to pattern works the same as a realm, except that
# it's not allowed to use a wildcard. We'll model this by
# parsing it as a realm, and not trying to match it if it has
# a wildcard.
return_realm = TrustRoot.parse(allowed_return_to)
if (# Parses as a trust root
!return_realm.nil? and
# Does not have a wildcard
!return_realm.wildcard and
# Matches the return_to that we passed in with it
return_realm.validate_url(return_to)
)
return true
end
}
# No URL in the list matched
return false
end
# Given a relying party discovery URL return a list of return_to
# URLs.
def TrustRoot.get_allowed_return_urls(relying_party_url)
rp_url_after_redirects, return_to_urls = services.get_service_endpoints(
relying_party_url, _extract_return_url)
if rp_url_after_redirects != relying_party_url
# Verification caused a redirect
raise RealmVerificationRedirected.new(
relying_party_url, rp_url_after_redirects)
end
return return_to_urls
end
# Verify that a return_to URL is valid for the given realm.
#
# This function builds a discovery URL, performs Yadis discovery
# on it, makes sure that the URL does not redirect, parses out
# the return_to URLs, and finally checks to see if the current
# return_to URL matches the return_to.
#
# raises DiscoveryFailure when Yadis discovery fails returns
# true if the return_to URL is valid for the realm
def TrustRoot.verify_return_to(realm_str, return_to, _vrfy=nil)
# _vrfy parameter is there to make testing easier
if _vrfy.nil?
_vrfy = self.method('get_allowed_return_urls')
end
if !(_vrfy.is_a?(Proc) or _vrfy.is_a?(Method))
raise ArgumentError, "_vrfy must be a Proc or Method"
end
realm = TrustRoot.parse(realm_str)
if realm.nil?
# The realm does not parse as a URL pattern
return false
end
begin
allowable_urls = _vrfy.call(realm.build_discovery_url())
rescue RealmVerificationRedirected => err
Util.log(err.to_s)
return false
end
if return_to_matches(allowable_urls, return_to)
return true
else
Util.log("Failed to validate return_to #{return_to} for " +
"realm #{realm_str}, was not in #{allowable_urls}")
return false
end
end
class TrustRoot
attr_reader :unparsed, :proto, :wildcard, :host, :port, :path
@@empty_re = Regexp.new('^http[s]*:\/\/\*\/$')
def TrustRoot._build_path(path, query=nil, frag=nil)
s = path.dup
frag = nil if frag == ''
query = nil if query == ''
if query
s << "?" << query
end
if frag
s << "#" << frag
end
return s
end
def TrustRoot._parse_url(url)
begin
url = URINorm.urinorm(url)
rescue URI::InvalidURIError => err
nil
end
begin
parsed = URI::parse(url)
rescue URI::InvalidURIError
return nil
end
path = TrustRoot._build_path(parsed.path,
parsed.query,
parsed.fragment)
return [parsed.scheme || '', parsed.host || '',
parsed.port || '', path || '']
end
def TrustRoot.parse(trust_root)
trust_root = trust_root.dup
unparsed = trust_root.dup
# look for wildcard
wildcard = (not trust_root.index('://*.').nil?)
trust_root.sub!('*.', '') if wildcard
# handle http://*/ case
if not wildcard and @@empty_re.match(trust_root)
proto = trust_root.split(':')[0]
port = proto == 'http' ? 80 : 443
return new(unparsed, proto, true, '', port, '/')
end
parts = TrustRoot._parse_url(trust_root)
return nil if parts.nil?
proto, host, port, path = parts
# check for URI fragment
if path and !path.index('#').nil?
return nil
end
return nil unless ['http', 'https'].member?(proto)
return new(unparsed, proto, wildcard, host, port, path)
end
def TrustRoot.check_sanity(trust_root_string)
trust_root = TrustRoot.parse(trust_root_string)
if trust_root.nil?
return false
else
return trust_root.sane?
end
end
# quick func for validating a url against a trust root. See the
# TrustRoot class if you need more control.
def self.check_url(trust_root, url)
tr = self.parse(trust_root)
return (!tr.nil? and tr.validate_url(url))
end
# Return a discovery URL for this realm.
#
# This function does not check to make sure that the realm is
# valid. Its behaviour on invalid inputs is undefined.
#
# return_to:: The relying party return URL of the OpenID
# authentication request
#
# Returns the URL upon which relying party discovery should be
# run in order to verify the return_to URL
def build_discovery_url
if self.wildcard
# Use "www." in place of the star
www_domain = 'www.' + @host
port = (!@port.nil? and ![80, 443].member?(@port)) ? (":" + @port.to_s) : ''
return "#{@proto}://#{www_domain}#{port}#{@path}"
else
return @unparsed
end
end
def initialize(unparsed, proto, wildcard, host, port, path)
@unparsed = unparsed
@proto = proto
@wildcard = wildcard
@host = host
@port = port
@path = path
end
def sane?
return true if @host == 'localhost'
host_parts = @host.split('.')
# a note: ruby string split does not put an empty string at
# the end of the list if the split element is last. for
# example, 'foo.com.'.split('.') => ['foo','com']. Mentioned
# because the python code differs here.
return false if host_parts.length == 0
# no adjacent dots
return false if host_parts.member?('')
# last part must be a tld
tld = host_parts[-1]
return false unless TOP_LEVEL_DOMAINS.member?(tld)
return false if host_parts.length == 1
if @wildcard
if tld.length == 2 and host_parts[-2].length <= 3
# It's a 2-letter tld with a short second to last segment
# so there needs to be more than two segments specified
# (e.g. *.co.uk is insane)
return host_parts.length > 2
end
end
return true
end
def validate_url(url)
parts = TrustRoot._parse_url(url)
return false if parts.nil?
proto, host, port, path = parts
return false unless proto == @proto
return false unless port == @port
return false unless host.index('*').nil?
if !@wildcard
if host != @host
return false
end
elsif ((@host != '') and
(!host.ends_with?('.' + @host)) and
(host != @host))
return false
end
if path != @path
path_len = @path.length
trust_prefix = @path[0...path_len]
url_prefix = path[0...path_len]
# must be equal up to the length of the path, at least
if trust_prefix != url_prefix
return false
end
# These characters must be on the boundary between the end
# of the trust root's path and the start of the URL's path.
if !@path.index('?').nil?
allowed = '&'
else
allowed = '?/'
end
return (!allowed.index(@path[-1]).nil? or
!allowed.index(path[path_len]).nil?)
end
return true
end
end
end
end

View File

@ -0,0 +1,75 @@
require 'uri'
require "openid/extras"
module OpenID
module URINorm
public
def URINorm.urinorm(uri)
uri = URI.parse(uri)
raise URI::InvalidURIError.new('no scheme') unless uri.scheme
uri.scheme = uri.scheme.downcase
unless ['http','https'].member?(uri.scheme)
raise URI::InvalidURIError.new('Not an HTTP or HTTPS URI')
end
raise URI::InvalidURIError.new('no host') unless uri.host
uri.host = uri.host.downcase
uri.path = remove_dot_segments(uri.path)
uri.path = '/' if uri.path.length == 0
uri = uri.normalize.to_s
uri = uri.gsub(PERCENT_ESCAPE_RE) {
sub = $&[1..2].to_i(16).chr
reserved(sub) ? $&.upcase : sub
}
return uri
end
private
RESERVED_RE = /[A-Za-z0-9._~-]/
PERCENT_ESCAPE_RE = /%[0-9a-zA-Z]{2}/
def URINorm.reserved(chr)
not RESERVED_RE =~ chr
end
def URINorm.remove_dot_segments(path)
result_segments = []
while path.length > 0
if path.starts_with?('../')
path = path[3..-1]
elsif path.starts_with?('./')
path = path[2..-1]
elsif path.starts_with?('/./')
path = path[2..-1]
elsif path == '/.'
path = '/'
elsif path.starts_with?('/../')
path = path[3..-1]
result_segments.pop if result_segments.length > 0
elsif path == '/..'
path = '/'
result_segments.pop if result_segments.length > 0
elsif path == '..' or path == '.'
path = ''
else
i = 0
i = 1 if path[0].chr == '/'
i = path.index('/', i)
i = path.length if i.nil?
result_segments << path[0...i]
path = path[i..-1]
end
end
return result_segments.join('')
end
end
end

Some files were not shown because too many files have changed in this diff Show More