rubycas/rubycas-client

View on GitHub
lib/casclient/responses.rb

Summary

Maintainability
C
1 day
Test Coverage
module CASClient
  module XmlResponse
    attr_reader :xml, :parse_datetime
    attr_reader :failure_code, :failure_message

    def check_and_parse_xml(raw_xml)
      begin
        doc = REXML::Document.new(raw_xml, :raw => :all)
      rescue REXML::ParseException => e
        raise BadResponseException, 
          "MALFORMED CAS RESPONSE:\n#{raw_xml.inspect}\n\nEXCEPTION:\n#{e}"
      end

      unless doc.elements && doc.elements["cas:serviceResponse"]
        raise BadResponseException, 
          "This does not appear to be a valid CAS response (missing cas:serviceResponse root element)!\nXML DOC:\n#{doc.to_s}"
      end

      return doc.elements["cas:serviceResponse"].elements[1]
    end

    def to_s
      xml.to_s
    end
  end

  # Represents a response from the CAS server to a 'validate' request
  # (i.e. after validating a service/proxy ticket).
  class ValidationResponse
    include XmlResponse

    attr_reader :protocol, :user, :pgt_iou, :proxies, :extra_attributes

    def initialize(raw_text, options={})
      parse(raw_text, options)
    end

    def parse(raw_text, options)
      raise BadResponseException, 
        "CAS response is empty/blank." if raw_text.to_s.empty?
      @parse_datetime = Time.now
      if raw_text =~ /^(yes|no)\n(.*?)\n$/m
        @protocol = 1.0
        @valid = $~[1] == 'yes'
        @user = $~[2]
        return
      end

      @xml = check_and_parse_xml(raw_text)

      # if we got this far then we've got a valid XML response, so we're doing CAS 2.0
      @protocol = 2.0

      if is_success?
        cas_user = @xml.elements["cas:user"]
        @user = cas_user.text.strip if cas_user
        @pgt_iou =  @xml.elements["cas:proxyGrantingTicket"].text.strip if @xml.elements["cas:proxyGrantingTicket"]

        proxy_els = @xml.elements.to_a('//cas:authenticationSuccess/cas:proxies/cas:proxy')
        if proxy_els.size > 0
          @proxies = []
          proxy_els.each do |el|
            @proxies << el.text
          end
        end

        @extra_attributes = {}
        @xml.elements.to_a('//cas:authenticationSuccess/cas:attributes/* | //cas:authenticationSuccess/*[local-name() != \'proxies\' and local-name() != \'proxyGrantingTicket\' and local-name() != \'user\' and local-name() != \'attributes\']').each do |el|
          inner_text = el.cdatas.length > 0 ? el.cdatas.join('') : el.text
          name = el.name
          unless (attrs = el.attributes).empty?
            name       = attrs['name']
            inner_text = attrs['value']
          end
          @extra_attributes.merge! name => inner_text
        end

        # unserialize extra attributes
        @extra_attributes.each do |k, v|
          @extra_attributes[k] = parse_extra_attribute_value(v, options[:encode_extra_attributes_as])
        end
      elsif is_failure?
        @failure_code = @xml.elements['//cas:authenticationFailure'].attributes['code']
        @failure_message = @xml.elements['//cas:authenticationFailure'].text.strip
      else
        # this should never happen, since the response should already have been recognized as invalid
        raise BadResponseException, "BAD CAS RESPONSE:\n#{raw_text.inspect}\n\nXML DOC:\n#{doc.inspect}"
      end
    end

    def parse_extra_attribute_value(value, encode_extra_attributes_as)
      attr_value = if value.to_s.empty?
         nil
       elsif !encode_extra_attributes_as
         begin
           YAML.load(value)
         rescue ArgumentError => e
           raise ArgumentError, "Error parsing extra attribute with value #{value} as YAML: #{e}"
         end
       else
         if encode_extra_attributes_as == :json
           begin
             JSON.parse(value)
           rescue JSON::ParserError
             value
           end
         elsif encode_extra_attributes_as == :raw
           value
         else
           YAML.load(value)
         end
       end

      unless attr_value.kind_of?(Enumerable) || attr_value.kind_of?(TrueClass) || attr_value.kind_of?(FalseClass) || attr_value.nil?
        attr_value.to_s
      else
        attr_value
      end
    end

    def is_success?
      (instance_variable_defined?(:@valid) &&  @valid) || (protocol > 1.0 && xml.name == "authenticationSuccess")
    end

    def is_failure?
      (instance_variable_defined?(:@valid) && !@valid) || (protocol > 1.0 && xml.name == "authenticationFailure" )
    end
  end

  # Represents a response from the CAS server to a proxy ticket request 
  # (i.e. after requesting a proxy ticket).
  class ProxyResponse
    include XmlResponse

    attr_reader :proxy_ticket

    def initialize(raw_text, options={})
      parse(raw_text)
    end

    def parse(raw_text)
      raise BadResponseException, 
        "CAS response is empty/blank." if raw_text.to_s.empty?
      @parse_datetime = Time.now

      @xml = check_and_parse_xml(raw_text)

      if is_success?
        @proxy_ticket = @xml.elements["cas:proxyTicket"].text.strip if @xml.elements["cas:proxyTicket"]
      elsif is_failure?
        @failure_code = @xml.elements['//cas:proxyFailure'].attributes['code']
        @failure_message = @xml.elements['//cas:proxyFailure'].text.strip
      else
        # this should never happen, since the response should already have been recognized as invalid
        raise BadResponseException, "BAD CAS RESPONSE:\n#{raw_text.inspect}\n\nXML DOC:\n#{doc.inspect}"
      end

    end

    def is_success?
      xml.name == "proxySuccess"
    end

    def is_failure?
      xml.name == "proxyFailure"
    end
  end

  # Represents a response from the CAS server to a login request 
  # (i.e. after submitting a username/password).
  class LoginResponse
    attr_reader :tgt, :ticket, :service_redirect_url
    attr_reader :failure_message

    def initialize(http_response = nil, options={})
      parse_http_response(http_response) if http_response
    end

    def parse_http_response(http_response)
      header = http_response.to_hash

      # FIXME: this regexp might be incorrect...
      if header['set-cookie'] && 
        header['set-cookie'].first && 
        header['set-cookie'].first =~ /tgt=([^&]+);/
        @tgt = $~[1]
      end

      location = header['location'].first if header['location'] && header['location'].first
      if location =~ /ticket=([^&]+)/
        @ticket = $~[1]
      end
      
      # Legacy check. CAS Server used to return a 200 (Success) or a 302 (Found) on successful authentication.
      # This behavior should be deprecated at some point in the future.
      legacy_valid_ticket = (http_response.kind_of?(Net::HTTPSuccess) || http_response.kind_of?(Net::HTTPFound)) && @ticket.present?
      
      # If using rubycas-server 1.1.0+
      valid_ticket = http_response.kind_of?(Net::HTTPSeeOther) && @ticket.present?
      
      if !legacy_valid_ticket && !valid_ticket
        @failure = true
        # Try to extract the error message -- this only works with RubyCAS-Server.
        # For other servers we just return the entire response body (i.e. the whole error page).
        body = http_response.body
        if body =~ /<div class="messagebox mistake">(.*?)<\/div>/m
          @failure_message = $~[1].strip
        else
          @failure_message = body
        end
      end

      @service_redirect_url = location
    end

    def is_success?
      !@failure && !ticket.to_s.empty?
    end

    def is_failure?
      @failure == true
    end
  end

  class BadResponseException < CASException
  end
end