stellar 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
@@ -0,0 +1,18 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <projectDescription>
3
+ <name>stellar</name>
4
+ <comment></comment>
5
+ <projects>
6
+ </projects>
7
+ <buildSpec>
8
+ <buildCommand>
9
+ <name>com.aptana.ide.core.unifiedBuilder</name>
10
+ <arguments>
11
+ </arguments>
12
+ </buildCommand>
13
+ </buildSpec>
14
+ <natures>
15
+ <nature>com.aptana.ruby.core.rubynature</nature>
16
+ <nature>com.aptana.projects.webnature</nature>
17
+ </natures>
18
+ </projectDescription>
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --backtrace
2
+ --color
3
+ --debugger
data/Gemfile ADDED
@@ -0,0 +1,21 @@
1
+ source :rubygems
2
+
3
+ # Add dependencies required to use your gem here.
4
+ gem 'mechanize', '>= 2.0.2'
5
+ gem 'nokogiri', '>= 1.5.0'
6
+
7
+ # Add dependencies to develop your gem here.
8
+ # Include everything needed to run rake, tests, features, etc.
9
+ group :development do
10
+ gem 'rdoc', '>= 3.9.4'
11
+ gem 'rspec', '>= 2.6.0'
12
+ gem 'yard', '>= 0.7.2'
13
+ gem 'yard-rspec', '>= 0.1'
14
+ gem 'bundler', '>= 1.0.21'
15
+ gem 'jeweler', '>= 1.6.4'
16
+ gem 'rcov', '>= 0'
17
+ gem 'ruby-debug', :platform => :mri_18
18
+ gem 'ruby-debug19', :platform => :mri_19
19
+
20
+ gem 'highline', '>= 1.6.2'
21
+ end
@@ -0,0 +1,75 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ archive-tar-minitar (0.5.2)
5
+ columnize (0.3.4)
6
+ diff-lcs (1.1.3)
7
+ git (1.2.5)
8
+ highline (1.6.2)
9
+ jeweler (1.6.4)
10
+ bundler (~> 1.0)
11
+ git (>= 1.2.5)
12
+ rake
13
+ json (1.6.1)
14
+ linecache (0.46)
15
+ rbx-require-relative (> 0.0.4)
16
+ linecache19 (0.5.12)
17
+ ruby_core_source (>= 0.1.4)
18
+ mechanize (2.0.2)
19
+ net-http-digest_auth (~> 1.1, >= 1.1.1)
20
+ net-http-persistent (~> 1.8)
21
+ nokogiri (~> 1.4)
22
+ webrobots (~> 0.0, >= 0.0.9)
23
+ net-http-digest_auth (1.1.1)
24
+ net-http-persistent (1.9)
25
+ nokogiri (1.5.0)
26
+ rake (0.9.2)
27
+ rbx-require-relative (0.0.5)
28
+ rcov (0.9.11)
29
+ rdoc (3.10)
30
+ json (~> 1.4)
31
+ rspec (2.6.0)
32
+ rspec-core (~> 2.6.0)
33
+ rspec-expectations (~> 2.6.0)
34
+ rspec-mocks (~> 2.6.0)
35
+ rspec-core (2.6.4)
36
+ rspec-expectations (2.6.0)
37
+ diff-lcs (~> 1.1.2)
38
+ rspec-mocks (2.6.0)
39
+ ruby-debug (0.10.4)
40
+ columnize (>= 0.1)
41
+ ruby-debug-base (~> 0.10.4.0)
42
+ ruby-debug-base (0.10.4)
43
+ linecache (>= 0.3)
44
+ ruby-debug-base19 (0.11.25)
45
+ columnize (>= 0.3.1)
46
+ linecache19 (>= 0.5.11)
47
+ ruby_core_source (>= 0.1.4)
48
+ ruby-debug19 (0.11.6)
49
+ columnize (>= 0.3.1)
50
+ linecache19 (>= 0.5.11)
51
+ ruby-debug-base19 (>= 0.11.19)
52
+ ruby_core_source (0.1.5)
53
+ archive-tar-minitar (>= 0.5.2)
54
+ webrobots (0.0.12)
55
+ nokogiri (>= 1.4.4)
56
+ yard (0.7.2)
57
+ yard-rspec (0.1)
58
+ yard
59
+
60
+ PLATFORMS
61
+ ruby
62
+
63
+ DEPENDENCIES
64
+ bundler (>= 1.0.21)
65
+ highline (>= 1.6.2)
66
+ jeweler (>= 1.6.4)
67
+ mechanize (>= 2.0.2)
68
+ nokogiri (>= 1.5.0)
69
+ rcov
70
+ rdoc (>= 3.9.4)
71
+ rspec (>= 2.6.0)
72
+ ruby-debug
73
+ ruby-debug19
74
+ yard (>= 0.7.2)
75
+ yard-rspec (>= 0.1)
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Victor Costan
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,18 @@
1
+ = stellar
2
+
3
+ Automated access to MIT's Stellar data, so we don't have to put up with
4
+ Stellar's craptastic UI.
5
+
6
+ == Contributing to stellar
7
+
8
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
9
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
10
+ * Fork the project
11
+ * Start a feature/bugfix branch
12
+ * Commit and push until you are happy with your contribution
13
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
14
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
15
+
16
+ == Copyright
17
+
18
+ Copyright (c) 2011 Victor Costan. See LICENSE.txt for further details.
@@ -0,0 +1,76 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+ begin
6
+ Bundler.setup(:default, :development)
7
+ rescue Bundler::BundlerError => e
8
+ $stderr.puts e.message
9
+ $stderr.puts "Run `bundle install` to install missing gems"
10
+ exit e.status_code
11
+ end
12
+ require 'rake'
13
+
14
+ require 'jeweler'
15
+ Jeweler::Tasks.new do |gem|
16
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
17
+ gem.name = "stellar"
18
+ gem.homepage = "http://github.com/pwnall/stellar"
19
+ gem.license = "MIT"
20
+ gem.summary = %Q{Automated access to MIT's Stellar data}
21
+ gem.description = %Q{So we don't have to put up with Stellar's craptastic ui}
22
+ gem.email = "victor@costan.us"
23
+ gem.authors = ["Victor Costan"]
24
+ # dependencies defined in Gemfile
25
+ end
26
+ Jeweler::RubygemsDotOrgTasks.new
27
+
28
+ require 'rspec/core'
29
+ require 'rspec/core/rake_task'
30
+ RSpec::Core::RakeTask.new(:spec) do |spec|
31
+ spec.pattern = FileList['spec/**/*_spec.rb']
32
+ end
33
+
34
+ RSpec::Core::RakeTask.new(:rcov) do |spec|
35
+ spec.pattern = 'spec/**/*_spec.rb'
36
+ spec.rcov = true
37
+ end
38
+
39
+ task :default => :spec
40
+
41
+ require 'yard'
42
+ YARD::Rake::YardocTask.new
43
+
44
+ # Fixtures.
45
+
46
+ require 'highline/import'
47
+ require 'readline'
48
+ require 'yaml'
49
+
50
+ task :spec => :fixtures
51
+
52
+ krb_file = 'spec/fixtures/kerberos.b64'
53
+ file krb_file do
54
+ kerberos = {}
55
+ kerberos[:user] = ask('MIT Kerberos Username: ') { |q| q.echo = true }
56
+ kerberos[:pass] = ask('MIT Kerberos Password: ') { |q| q.echo = '*' }
57
+ kerberos[:mit_id] = ask('MIT ID: ') { |q| q.echo = true }
58
+ File.open(krb_file, 'w') {|f| f.write [kerberos.to_yaml].pack('m') }
59
+ end
60
+ task :fixtures => krb_file
61
+
62
+ cert_file = 'spec/fixtures/mit_cert.yml'
63
+ file cert_file => krb_file do
64
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), 'lib'))
65
+ require 'stellar'
66
+
67
+ kerberos = YAML.load File.read(krb_file).unpack('m').first
68
+ cert = Stellar::Auth.get_certificate kerberos
69
+ yaml = {:cert => cert[:cert].to_pem, :key => cert[:key].to_pem}.to_yaml
70
+ File.open(cert_file, 'wb') { |f| f.write yaml }
71
+
72
+ # Write the certificate in PKCS#12 format for debugging purposes.
73
+ p12 = OpenSSL::PKCS12.create(nil, 'MIT Stellar', cert[:key], cert[:cert])
74
+ File.open('spec/fixtures/mit_cert.p12', 'wb') { |f| f.write p12.to_der }
75
+ end
76
+ task :fixtures => cert_file
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,18 @@
1
+ # Standard library.
2
+ require 'openssl'
3
+ require 'uri'
4
+
5
+ # Gems.
6
+ require 'nokogiri'
7
+ require 'mechanize'
8
+
9
+ # TODO(pwnall): documentation
10
+ module Stellar
11
+
12
+ end # namespace Stellar
13
+
14
+ # Code.
15
+ require 'stellar/auth.rb'
16
+ require 'stellar/client.rb'
17
+ require 'stellar/courses.rb'
18
+ require 'stellar/homework.rb'
@@ -0,0 +1,130 @@
1
+ require 'logger'
2
+
3
+ # :nodoc: namespace
4
+ module Stellar
5
+
6
+ # Support for authentication on MIT systems.
7
+ module Auth
8
+ # Path to the MIT CA self-signed certificate.
9
+ def mitca_path
10
+ File.join File.dirname(__FILE__), 'mitca.crt'
11
+ end
12
+
13
+ # Authenticates using some credentials, e.g. an MIT certificate.
14
+ #
15
+ # @param [Hash] options credentials to be used for authentication
16
+ # @option options [String] :cert path to MIT client certificate for the user
17
+ # @option options [Hash] :kerberos:: Kerberos credentials, encoded as a Hash
18
+ # with :user and :pass keys
19
+ # @return [Stellar::Client] self, for convenient method chaining
20
+ def auth(options = {})
21
+ # Reset any prior credentials.
22
+ @mech = mech
23
+ if options[:cert]
24
+ log = Logger.new(STDERR)
25
+ log.level = Logger::INFO
26
+ @mech.log = log
27
+
28
+ key = options[:cert][:key]
29
+ if key.respond_to?(:to_str)
30
+ if File.exist?(key)
31
+ @mech.key = key
32
+ else
33
+ @mech.key = OpenSSL::PKey::RSA.new key
34
+ end
35
+ else
36
+ @mech.key = key
37
+ end
38
+ cert = options[:cert][:cert]
39
+ if cert.respond_to?(:to_str)
40
+ if File.exist?(cert)
41
+ @mech.cert = cert
42
+ else
43
+ @mech.cert = OpenSSL::X509::Certificate.new cert
44
+ end
45
+ else
46
+ @mech.cert = cert
47
+ end
48
+ end
49
+
50
+ # Go to a page that is guaranteed to redirect to shitoleth.
51
+ step1_page = get '/atstellar'
52
+ # Fill in the form.
53
+ step1_form = step1_page.form_with :action => /WAYF/
54
+ step1_form.checkbox_with(:name => /perm/).checked = :checked
55
+ step2_page = step1_form.submit
56
+ # Click through the stupid confirmation form.
57
+ step2_form = step2_page.form_with :action => /WAYF/
58
+ cred_page = step2_form.submit
59
+
60
+ # Fill in the credentials form.
61
+ if options[:cert]
62
+ cred_form = cred_page.form_with :action => /certificate/i
63
+ cred_form.checkbox_with(:name => /pref/).checked = :checked
64
+ puts cred_form
65
+ puts cred_form.submit(cred_form.buttons.first).body
66
+ return
67
+ elsif options[:kerberos]
68
+ cred_form = cred_page.form_with :action => /username/i
69
+ cred_form.field_with(:name => /user/).value = options[:kerberos][:user]
70
+ cred_form.field_with(:name => /pass/).value = options[:kerberos][:pass]
71
+ else
72
+ raise 'Unsupported credentials'
73
+ end
74
+
75
+ # Click through the SAML response form.
76
+ saml_page = cred_form.submit
77
+ unless saml_form = saml_page.form_with(:action => /SAML/)
78
+ raise ArgumentError, 'Authentication failed due to invalid credentials'
79
+ end
80
+ saml_form.submit
81
+
82
+ self
83
+ end
84
+ end # module Stellar::Auth
85
+
86
+ # :nodoc: class methods
87
+ module Auth
88
+ class <<self
89
+ include Auth
90
+
91
+ # Obtains a certificate using a Kerberos credentials.
92
+ #
93
+ # @param [Hash] kerberos MIT Kerberos credentials
94
+ # @option kerberos [String] :user the Kerberos username (e.g. "costan")
95
+ # @option kerberos [String] :pass the Kerberos password (handle with care!)
96
+ # @option kerberos [String] :mit_id 9-character string or 9-digit number
97
+ # starting with 9 (9........)
98
+ # @option kerberos [Fixnum] :ttl certificate lifetime, in days (optional;
99
+ # defaults to 1 day)
100
+ #
101
+ # @return [Hash] a Hash with a :cert key (the OpenSSL::X509::Certificate)
102
+ # and a :key key (the matching OpenSSL::PKey::PKey private key)
103
+ def get_certificate(kerberos)
104
+ mech = Mechanize.new
105
+ mech.ca_file = mitca_path
106
+ mech.user_agent_alias = 'Linux Firefox'
107
+ login_page = mech.get 'https://ca.mit.edu/ca/'
108
+ login_form = login_page.form_with :action => /login/
109
+ login_form.field_with(:name => /login/).value = kerberos[:user]
110
+ login_form.field_with(:name => /pass/).value = kerberos[:pass]
111
+ login_form.field_with(:name => /mitid/).value = kerberos[:mit_id]
112
+ keygen_page = login_form.submit login_form.buttons.first
113
+
114
+ keygen_form = keygen_page.form_with(:action => /ca/)
115
+ if /login/ =~ keygen_form.action
116
+ raise ArgumentError, 'Invalid Kerberos credentials'
117
+ end
118
+ keygen_form.field_with(:name => /life/).value = kerberos[:ttl] || 1
119
+ key_pair = keygen_form.keygens.first.key
120
+ response_page = keygen_form.submit keygen_form.buttons.first
121
+
122
+ cert_frame = response_page.frame_with(:name => /download/)
123
+ cert_bytes = mech.get_file cert_frame.uri
124
+ cert = OpenSSL::X509::Certificate.new cert_bytes
125
+ {:key => key_pair, :cert => cert}
126
+ end
127
+ end
128
+ end # module Stellar::Auth
129
+
130
+ end # namespace Stellar
@@ -0,0 +1,66 @@
1
+ # :nodoc: namespace
2
+ module Stellar
3
+
4
+ # Client session for accessing the Stellar API.
5
+ class Client
6
+ include Stellar::Auth
7
+
8
+ # Client for accessing public information.
9
+ #
10
+ # Call auth to authenticate as a user and access restricted functionality.
11
+ def initialize
12
+ @mech = mech
13
+
14
+ @courses = nil
15
+ end
16
+
17
+ # New Mechanize instance.
18
+ def mech
19
+ m = Mechanize.new
20
+ m.ca_file = mitca_path
21
+ m.user_agent_alias = 'Linux Firefox'
22
+ m
23
+ end
24
+
25
+ # Fetches a page from the Stellar site.
26
+ #
27
+ # @param [String] path relative URL of the page to be fetched
28
+ # @return [Mechanize::Page] the desired page, wrapped in the Mechanize API
29
+ def get(path)
30
+ uri = URI.join('https://stellar.mit.edu', path)
31
+ page_bytes = @mech.get uri
32
+ end
33
+
34
+ # Fetches a page from the Stellar site.
35
+ #
36
+ # @param [String] path relative URL of the page to be fetched
37
+ # @return [Nokogiri::HTML::Document] the desired page, parsed with Nokogiri
38
+ def get_nokogiri(path)
39
+ uri = URI.join('https://stellar.mit.edu', path)
40
+ raw_html = @mech.get_file uri
41
+ Nokogiri.HTML raw_html, uri.to_s
42
+ end
43
+
44
+ # Fetches a file from the Stellar site.
45
+ #
46
+ # @param [String] path relative URL of the file to be fetched
47
+ # @return [String] raw contents of the file
48
+ def get_file(path)
49
+ uri = URI.join('https://stellar.mit.edu', path)
50
+ @mech.get_file uri
51
+ end
52
+
53
+ # A Stellar client specialized to answer course queries.
54
+ #
55
+ # @return [Stellar::Courses] client specialized to course queries
56
+ def courses
57
+ @courses ||= Stellar::Courses.new self
58
+ end
59
+
60
+ # (see Stellar::Course#for)
61
+ def course(number, year, semester)
62
+ Stellar::Course.for number, year, semester, self
63
+ end
64
+ end # class Stellar::Client
65
+
66
+ end # namespace Stellar