morhekil-ipcauthpipe 0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README ADDED
@@ -0,0 +1,13 @@
1
+ This is implementation of Courier's authpipe protocol over Invision Power Board
2
+ and Converge.
3
+
4
+ This gem acts as an interface between Courier's authlib with Invision's members
5
+ database and allows to authenticate with authlib against Invision's DB.
6
+
7
+ To run it you have to take sample config file included in the gem sources and
8
+ modify it to match your environment. Then configure your courier-authlib
9
+ to run ipcauthpipe with full path to your config file as a parameter, i.e.:
10
+ /usr/local/ipcauthpipe /etc/ipcauthpipe-config.yml
11
+
12
+ See more about Courier's authpipe authentication module and protocol at:
13
+ http://www.courier-mta.org/authlib/README_authlib.html#authpipe
data/bin/ipcauthpipe ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.dirname(__FILE__) + '/../lib/ipcauthpipe'
4
+
5
+ # Start IpcAuthpipe with config from the first CLI argument of with ../config.yml as a default one
6
+ IpcAuthpipe.start( ARGV[0] || "#{File.dirname(__FILE__)}/../config.yml" )
data/config.yml ADDED
@@ -0,0 +1,19 @@
1
+ database:
2
+ adapter: mysql
3
+ database: pokerru_mail
4
+ username: root
5
+ password: Awycehe2iS
6
+ socket: /var/run/mysqld/mysqld.sock
7
+
8
+ log:
9
+ level: debug
10
+ file: ipcauthpipe.log
11
+
12
+ invision:
13
+ tables_prefix: ibf_
14
+
15
+ mail:
16
+ address_format: %s@poker.ru
17
+ owner_username: saruman
18
+ owner_gid: 1000
19
+ home_dir: /tmp/%s
@@ -0,0 +1,48 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "ipcauthpipe"
3
+ s.version = "0.1"
4
+ s.date = "2008-09-10"
5
+ s.summary = "Implementation of Courier's authpipe protocol over Invision Power Board / Converge."
6
+ s.email = "tom@rubyisawesome.com"
7
+ s.homepage = "http://github.com/schacon/grit"
8
+ s.description = "ipcauthpipe gem implements Courier's authpipe protocol to interface Courier POP/IMAP server with Invision Power Board / Converge members database."
9
+ s.has_rdoc = false
10
+
11
+ s.author = "Oleg Ivanov"
12
+ s.email = "morhekil@morhekil.net"
13
+ s.homepage = "http://twitter.com/morhekil"
14
+
15
+ s.files = ['lib/ipcauthpipe/handler/auth.rb',
16
+ 'lib/ipcauthpipe/handler/enumerate.rb',
17
+ 'lib/ipcauthpipe/handler/passwd.rb',
18
+ 'lib/ipcauthpipe/handler/pre.rb',
19
+ 'lib/ipcauthpipe/handler.rb',
20
+ 'lib/ipcauthpipe/log.rb',
21
+ 'lib/ipcauthpipe/processor.rb',
22
+ 'lib/ipcauthpipe/reader.rb',
23
+ 'lib/models/member.rb',
24
+ 'lib/models/member_converge.rb',
25
+ 'lib/ipcauthpipe.rb',
26
+ 'bin/ipcauthpipe',
27
+ 'ipcauthpipe.gemspec',
28
+ 'README',
29
+ 'config.yml'
30
+ ]
31
+
32
+ s.test_files = ['test/ipcauthpipe/handler/auth_spec.rb',
33
+ 'test/ipcauthpipe/handler/pre_spec.rb',
34
+ 'test/ipcauthpipe/log_spec.rb',
35
+ 'test/ipcauthpipe/processor_spec.rb',
36
+ 'test/models/member_spec.rb',
37
+ 'test/models/member_converge_spec.rb',
38
+ 'test/spec_helper.rb',
39
+ 'test/config.yml'
40
+ ]
41
+
42
+ s.bindir = 'bin'
43
+ s.executables = ['ipcauthpipe']
44
+
45
+ s.require_path = 'lib'
46
+
47
+ s.add_dependency("activerecord", ">= 2.1.0")
48
+ end
@@ -0,0 +1,55 @@
1
+ $:.unshift File.dirname(__FILE__)
2
+
3
+ require 'rubygems'
4
+ require 'activerecord'
5
+ require 'ostruct'
6
+
7
+ require 'ipcauthpipe/processor'
8
+ require 'ipcauthpipe/handler'
9
+ require 'ipcauthpipe/reader'
10
+ require 'ipcauthpipe/log'
11
+
12
+ module IpcAuthpipe
13
+
14
+ # Failed authentication (unknown username/password) exception class
15
+ class AuthenticationFailed < StandardError
16
+ end
17
+
18
+ class << self
19
+ attr_reader :config
20
+
21
+ # Starts the processing - initializes the configuration and feeds incoming requests
22
+ # to request processor
23
+ def start(cfgfile)
24
+ init cfgfile
25
+ Log::info 'ipcauthpipe is started'
26
+
27
+ ipc = IpcAuthpipe::Processor.new
28
+ while (line = IpcAuthpipe::Reader.getline) do
29
+ puts ipc.process(line.strip) unless line.strip.empty?
30
+ end
31
+ end
32
+
33
+ # Reads and stores config file and uses it's data to initialize ActiveRecord connection
34
+ def init(cfgfile)
35
+ # Read and parse YAML config
36
+ cfgdata = YAML::load_file(cfgfile)
37
+
38
+ # Create the global config object
39
+ @config = OpenStruct.new cfgdata
40
+
41
+ # Init logger - set up it's level and log file
42
+ IpcAuthpipe::Log.init(@config.log['file'], @config.log['level'])
43
+
44
+ # And init the ActiveRecord
45
+ ActiveRecord::Base.establish_connection(@config.database)
46
+ ActiveRecord::Base.logger = IpcAuthpipe::Log.logger
47
+
48
+ # and require model classes (we can't do it before as we need initialized config to be available)
49
+ require 'models/member_converge'
50
+ require 'models/member'
51
+ end
52
+
53
+ end
54
+
55
+ end
@@ -0,0 +1,23 @@
1
+ module IpcAuthpipe
2
+ module Handler
3
+
4
+ # A basic class that outlines handlers' API.
5
+ # Right now it features only the single process method that accepts command's parameters
6
+ # as an argument and goes on with processing the specific handler's command.
7
+ class Base
8
+
9
+ # Every handler access command's parameters as a string argument
10
+ # and should return as a string it's answer that will be returned back (to STDOUT generally)
11
+ def process(request)
12
+ raise NotImplementedError, 'request processing should be overriden by concrete handlers'
13
+ end
14
+
15
+ end
16
+ end
17
+ end
18
+
19
+ # Concrete handlers are following
20
+ require 'ipcauthpipe/handler/auth'
21
+ require 'ipcauthpipe/handler/pre'
22
+ require 'ipcauthpipe/handler/passwd'
23
+ require 'ipcauthpipe/handler/enumerate'
@@ -0,0 +1,77 @@
1
+ module IpcAuthpipe
2
+ module Handler
3
+
4
+ # AUTH command handler performs actual authentication of user's data.
5
+ # It gets authentication type and parameters from the input stream and
6
+ # responds with FAIL on failure or user data on success
7
+ class Auth < IpcAuthpipe::Handler::Base
8
+
9
+ # Main point of entry - accepts additional command's parameters
10
+ # (in case of AUTH - number of data bytes following the command)
11
+ # and proceeds with processing the command
12
+ def self.process(request)
13
+ Log.debug "Processing request [#{request}] in AUTH handler"
14
+ auth = Auth.new
15
+ auth.validate auth.getdata(request.to_i)
16
+ end
17
+
18
+ # Reads the given number of bytes from the input stream and splits
19
+ # them up into a hash of parameters ready for further processing
20
+ def getdata(count)
21
+ Log.debug "Reading [#{count}] bytes from input stream"
22
+ splits = Reader::getbytes(count).strip.split(/\s+/m)
23
+ raise ArgumentError, 'Invalid AUTH payload' unless splits.size == 3
24
+
25
+ Log.debug "Analyzing splits [#{splits.inspect}]"
26
+ auth_method(splits)
27
+ end
28
+
29
+ # Analyzes splitted AUTH payload and converts splits into hash
30
+ # of :method, :username and :password for LOGIN authentication and
31
+ # :method, :challenge and :response for CRAM-style authentications
32
+ def auth_method(splits)
33
+ result = { :method => splits[0].strip.downcase }
34
+ result.merge!(
35
+ result[:method] == 'login' ?
36
+ { :username => splits[1].strip, :password => splits[2].strip } :
37
+ { :challenge => splits[1].strip, :response => splits[2].strip }
38
+ )
39
+
40
+ Log.debug "Converted splits into [#{result.inspect}]"
41
+ result
42
+ end
43
+
44
+ # Accepts analyzed AUTH payload hash and delegated processing onto the
45
+ # specific authentication method's handler. In case of not implemented
46
+ # auth method raises NotImplementedError
47
+ def validate(authdata)
48
+ Log.debug "Validating #{authdata.inspect}"
49
+ begin
50
+ # convert auth type name to a handler's symbol
51
+ method_sym = ( 'validate_with_'+authdata[:method].gsub( /[- ]/, '_' ) ).to_sym
52
+ # and raise an error if it's not implemented
53
+ raise NotImplementedError, "Authentication type #{authdata[:method]} is not supported" unless
54
+ self.respond_to?(method_sym)
55
+ # or delegate processing to the handler if it's here
56
+ Log.debug "Delegating validation to #{method_sym.to_s}"
57
+ self.send(method_sym, authdata)
58
+
59
+ rescue NotImplementedError
60
+ # requested authentication type is not supported
61
+ Log.error "Unsupported authentication type requested with #{authdata.inspect}"
62
+ "FAIL\n"
63
+ rescue AuthenticationFailed
64
+ Log.info "Authentication failed for #{authdata.inspect}"
65
+ "FAIL\n"
66
+ end
67
+ end
68
+
69
+ # LOGIN type authentication handler
70
+ def validate_with_login(authdata)
71
+ Log.debug "Authenticating through type LOGIN with #{authdata.inspect}"
72
+ Member.find_by_name_and_password(authdata[:username], authdata[:password]).to_authpipe
73
+ end
74
+ end
75
+
76
+ end
77
+ end
@@ -0,0 +1,14 @@
1
+ module IpcAuthpipe
2
+ module Handler
3
+
4
+ # AUTH command handler performs actual authentication of user's data.
5
+ # It gets authentication type and parameters from the input stream and
6
+ # responds with FAIL on failure or user data on success
7
+ class Enumerate < IpcAuthpipe::Handler::Base
8
+ def self.process(request)
9
+ Log.warn "Unsupported command ENUMERATE received with request #{request.inspect}"
10
+ "FAIL\n"
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ module IpcAuthpipe
2
+ module Handler
3
+
4
+ # AUTH command handler performs actual authentication of user's data.
5
+ # It gets authentication type and parameters from the input stream and
6
+ # responds with FAIL on failure or user data on success
7
+ class Passwd < IpcAuthpipe::Handler::Base
8
+ def self.process(request)
9
+ Log.warn "Unsupported command PASSWD received with request #{request.inspect}"
10
+ "FAIL\n"
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,30 @@
1
+ module IpcAuthpipe
2
+ module Handler
3
+
4
+ # AUTH command handler performs actual authentication of user's data.
5
+ # It gets authentication type and parameters from the input stream and
6
+ # responds with FAIL on failure or user data on success
7
+ class Pre < IpcAuthpipe::Handler::Base
8
+ def self.process(request)
9
+ Log.debug "Processing request [#{request}] in AUTH handler"
10
+ handler = Pre.new
11
+ handler.user_details handler.split_request(request)
12
+ end
13
+
14
+ # Splits request into service and username parts, raises
15
+ # ArgumentError if request string is invalid
16
+ def split_request(request)
17
+ raise ArgumentError, "Invalid PRE request #{request.inspect}" unless /^\. (\w+) (\w+)$/.match( request )
18
+ { :service => $~[1], :username => $~[2] }
19
+ end
20
+
21
+ # Finds member by his username and dumps his details, returns FAIL if not member were found
22
+ def user_details(request)
23
+ member = Member.find_by_name(request[:username])
24
+
25
+ member.nil? ? "FAIL\n" : member.to_authpipe
26
+ end
27
+
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,22 @@
1
+ require 'forwardable'
2
+ require 'logger'
3
+
4
+ module IpcAuthpipe
5
+ module Log
6
+
7
+ class << self
8
+ extend Forwardable
9
+ attr_reader :logger
10
+
11
+ def init(filename, loglevel)
12
+ @logger = Logger.new(filename)
13
+ @logger.level = Logger.const_get(loglevel.upcase)
14
+ @logger.formatter = Logger::Formatter.new
15
+ @logger.info "Logger is started"
16
+ end
17
+
18
+ def_delegators :@logger, :add, :debug, :info, :warn, :error, :fatal, :unknown, :close
19
+ end
20
+
21
+ end
22
+ end
@@ -0,0 +1,37 @@
1
+ module IpcAuthpipe
2
+
3
+ # This is the main entry point that accepts incoming request strings,
4
+ # validates and splits them into command/parameters parts and delegates processing
5
+ # to the command's handler
6
+ class Processor
7
+
8
+ # Accepts request as a single string, splits it into parts goes onto delegating.
9
+ # Returns handler's response back to the caller
10
+ def process(request)
11
+ Log.debug "Processing request: #{request.rstrip}"
12
+ begin
13
+ call_handler_for split_request(request)
14
+ rescue Exception => excp
15
+ Log.fatal "#{excp.class}, #{excp.message}"
16
+ raise
17
+ end
18
+ end
19
+
20
+ # Splits request into a command and it's parameters, validating command on the way.
21
+ # Raises RuntimeError on invalid command or (that's actually the same thing) unparsable request
22
+ def split_request(req)
23
+ raise RuntimeError, 'Invalid request received' unless /^(PRE|AUTH|PASSWD|ENUMERATE)(?:$| (.*)$)/.match(req)
24
+ {
25
+ :command => $1,
26
+ :params => $2.nil? || $2.empty? ? nil : $2
27
+ }
28
+ end
29
+
30
+ # Delegates processing to a concrete handler of request's command
31
+ def call_handler_for(request)
32
+ Log.debug "Calling #{request[:command].capitalize} handler with [#{request[:params]}] as a parameter"
33
+ IpcAuthpipe::Handler.const_get(request[:command].capitalize).process(request[:params])
34
+ end
35
+ end
36
+
37
+ end
@@ -0,0 +1,22 @@
1
+ require 'readbytes'
2
+
3
+ module IpcAuthpipe
4
+
5
+ # Abstracting read operations to substitute them with mocks in our tests and also
6
+ # to give potentially a way to replace STDIN operation with something different - file based,
7
+ # for example
8
+ class Reader
9
+
10
+ # Returns next line waiting on the input
11
+ def self.getline
12
+ STDIN.gets("\n")
13
+ end
14
+
15
+ # Returns exactly count number of bytes waiting on the input
16
+ def self.getbytes(count)
17
+ STDIN.readbytes(count)
18
+ end
19
+
20
+ end
21
+
22
+ end
@@ -0,0 +1,28 @@
1
+ class Member < ActiveRecord::Base
2
+ set_table_name IpcAuthpipe::config.invision['tables_prefix'] + 'members'
3
+
4
+ has_one :converge, :class_name => 'MemberConverge', :foreign_key => 'converge_id'
5
+
6
+ def self.find_by_name_and_password(username, password)
7
+ member = find_by_name(username)
8
+ raise(
9
+ IpcAuthpipe::AuthenticationFailed, 'invalid password'
10
+ ) unless member.kind_of?(Member) && member.converge.valid_password?(password)
11
+
12
+ member
13
+ end
14
+
15
+ def to_authpipe
16
+ IpcAuthpipe::Log.debug "Dumping authpipe string for member data #{inspect}"
17
+ stringdump = [
18
+ "USERNAME=#{IpcAuthpipe::config.mail['owner_username']}",
19
+ "GID=#{IpcAuthpipe::config.mail['owner_gid']}",
20
+ "HOME=#{IpcAuthpipe::config.mail['home_dir'] % name}",
21
+ "ADDRESS=#{IpcAuthpipe::config.mail['address_format'] % name}",
22
+ "."
23
+ ].join("\n")+"\n"
24
+ IpcAuthpipe::Log.debug "Authpipe dump: #{stringdump.inspect}"
25
+
26
+ stringdump
27
+ end
28
+ end
@@ -0,0 +1,19 @@
1
+ require 'digest/md5'
2
+
3
+ class MemberConverge < ActiveRecord::Base
4
+ set_table_name IpcAuthpipe::config.invision['tables_prefix'] + 'members_converge'
5
+ set_primary_key 'converge_id'
6
+
7
+ # Verifies if the given clear password matches hash and salt stored in IPB's database,
8
+ # returns true/false depending on the result
9
+ def valid_password?(cleartext)
10
+ return salted_hash(cleartext) == converge_pass_hash
11
+ end
12
+
13
+ # Calculates and returns IPB-style salted hash for a given text string
14
+ def salted_hash(text)
15
+ return Digest::MD5.hexdigest(
16
+ Digest::MD5.hexdigest(converge_pass_salt) + Digest::MD5.hexdigest(text)
17
+ )
18
+ end
19
+ end
data/test/config.yml ADDED
@@ -0,0 +1,19 @@
1
+ database:
2
+ adapter: mysql
3
+ database: pokerru_mail_test
4
+ username: root
5
+ password: Awycehe2iS
6
+ socket: /var/run/mysqld/mysqld.sock
7
+
8
+ log:
9
+ level: debug
10
+ file: ipcauthpipe_test.log
11
+
12
+ invision:
13
+ tables_prefix: ibf_
14
+
15
+ mail:
16
+ address_format: %s@poker.ru
17
+ owner_username: saruman
18
+ owner_gid: 1000
19
+ home_dir: /home/vmail/%s
@@ -0,0 +1,84 @@
1
+ require File.dirname(__FILE__) + '/../../spec_helper'
2
+
3
+ require 'ipcauthpipe/handler'
4
+ require 'ipcauthpipe/handler/auth'
5
+ require 'ipcauthpipe/reader'
6
+ require 'models/member'
7
+
8
+ describe "AUTH handler" do
9
+
10
+ before(:all) do
11
+ IpcAuthpipe::Log.logger = stub_everything
12
+ end
13
+
14
+ before(:each) do
15
+ @auth = IpcAuthpipe::Handler::Auth.new
16
+ end
17
+
18
+ it "should read given number of bytes from the input stream and pass them onto the analyzer splitted into an array" do
19
+ IpcAuthpipe::Reader.should_receive(:getbytes).once.with(23).and_return("login \n vasya \n parol ")
20
+ # we're expecting array as an argument for auth_method
21
+ @auth.should_receive(:auth_method).once.with( duck_type(:is_array) )
22
+ @auth.getdata(23)
23
+ end
24
+
25
+ it "should raise ArgumentError exception on invalid payload" do
26
+ IpcAuthpipe::Reader.should_receive(:getbytes).once.with(27).and_return("login \n vasya \n parol \n wtf")
27
+ lambda { @auth.getdata(27) }.should raise_error(ArgumentError)
28
+ end
29
+
30
+ it "should properly handle both CR-delimited and non-CR-delimited payloads" do
31
+ @auth.should_receive(:auth_method).twice.with(['login', 'vasya', 'parol'])
32
+
33
+ IpcAuthpipe::Reader.should_receive(:getbytes).once.with(23).and_return("login \n vasya \n parol ")
34
+ @auth.getdata(23)
35
+
36
+ IpcAuthpipe::Reader.should_receive(:getbytes).once.with(24).and_return("login \n vasya \n parol \n")
37
+ @auth.getdata(24)
38
+ end
39
+
40
+ it "should convert LOGIN's payload into a hash with username and password" do
41
+ @auth.auth_method( ['login', 'vasya', 'parol'] ).should == {
42
+ :method => 'login', :username => 'vasya', :password => 'parol'
43
+ }
44
+ end
45
+
46
+ it "should convert CRAM's payload into a hash with challenge and response" do
47
+ @auth.auth_method( ['cram-md5', 'vasya', 'parol'] ).should == {
48
+ :method => 'cram-md5', :challenge => 'vasya', :response => 'parol'
49
+ }
50
+ end
51
+
52
+ it "should return FAIL for unsupported authentication types" do
53
+ @auth.validate(:method => 'foobar').should == "FAIL\n"
54
+ end
55
+
56
+ it "should delegate processing onto a handler for supported authentication types" do
57
+ authdata = { :method => 'login', :username => 'vasya', :password => 'parol' }
58
+ @auth.should_receive(:validate_with_login).once.with(authdata).and_return('LOGIN-success')
59
+ @auth.validate(authdata).should == "LOGIN-success"
60
+ end
61
+
62
+ it "should return FAIL for failed authentication" do
63
+ authdata = { :method => 'login', :username => 'vasya', :password => 'parol' }
64
+ @auth.should_receive(:validate_with_login).once.with(authdata).and_raise(IpcAuthpipe::AuthenticationFailed)
65
+ @auth.validate(authdata).should == "FAIL\n"
66
+ end
67
+
68
+ describe "with LOGIN auth type" do
69
+
70
+ it "should find member and return it formatted" do
71
+ member = mock('member')
72
+ Member.should_receive(:find_by_name_and_password).with('vasya', 'parol').once.and_return(member)
73
+ member.should_receive(:to_authpipe).once.and_return("TEXT\nDUMP\nUSER\n.")
74
+
75
+ @auth.validate_with_login(:username => 'vasya', :password => 'parol').should == "TEXT\nDUMP\nUSER\n."
76
+ end
77
+
78
+ end
79
+
80
+
81
+ # it "should detect authentication method being used" do
82
+ # IpcAuthpipe::Handler::Auth.
83
+ # end
84
+ end
@@ -0,0 +1,50 @@
1
+ require File.dirname(__FILE__) + '/../../spec_helper'
2
+
3
+ require 'ipcauthpipe/handler'
4
+ require 'ipcauthpipe/handler/auth'
5
+ require 'ipcauthpipe/reader'
6
+ require 'models/member'
7
+
8
+ describe "PRE handler" do
9
+
10
+ before(:all) do
11
+ IpcAuthpipe::Log.logger = stub_everything
12
+ end
13
+
14
+ before(:each) do
15
+ @pre = IpcAuthpipe::Handler::Pre.new
16
+ end
17
+
18
+ it "should split request and find a member" do
19
+ request = '. imap tester'
20
+ splitted_request = { :service => 'imap', :username => 'tester' }
21
+ @pre.should_receive(:split_request).with(request).once.and_return(splitted_request)
22
+ @pre.should_receive(:user_details).with(splitted_request).once.and_return("MEMBER\nDUMP\n.")
23
+ IpcAuthpipe::Handler::Pre.should_receive(:new).once.and_return(@pre)
24
+
25
+ IpcAuthpipe::Handler::Pre.process(request)
26
+ end
27
+
28
+ it "should split request into authservice and username parts" do
29
+ @pre.split_request('. pop3 tester').should == { :service => 'pop3', :username => 'tester' }
30
+ end
31
+
32
+ it "should raise ArgumentError if request string is invalid" do
33
+ lambda { @pre.split_request('wtfisthis') }.should raise_error(ArgumentError)
34
+ end
35
+
36
+ it "should find and return user's info by his name" do
37
+ member = mock('member')
38
+ Member.should_receive(:find_by_name).once.with('tester').and_return(member)
39
+ member.should_receive(:to_authpipe).once.and_return("MEMBER\nINFO\n.")
40
+
41
+ request = { :service => 'pop3', :username => 'tester' }
42
+ @pre.user_details(request).should == "MEMBER\nINFO\n."
43
+ end
44
+
45
+ it "should return FAIL for invalid username" do
46
+ Member.should_receive(:find_by_name).once.with('foobar').and_return(nil)
47
+ request = { :service => 'pop3', :username => 'foobar' }
48
+ @pre.user_details(request).should == "FAIL\n"
49
+ end
50
+ end
@@ -0,0 +1,37 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ require 'ipcauthpipe/log'
4
+
5
+ describe 'Log' do
6
+
7
+ before(:each) do
8
+ # stubbing new instance and returning our mock instead
9
+ @logger = mock('log_instance', :level= => nil, :info => nil, :formatter= => nil)
10
+ Logger.should_receive(:new).with('test.log').once.and_return(@logger)
11
+ end
12
+
13
+ it "should init logger with given filename and log level" do
14
+ # log level should be set to debug
15
+ @logger.should_receive(:level=).with(Logger::DEBUG).once
16
+
17
+ # formatter should be initialized with our mock
18
+ formatter = mock('log_formatter')
19
+ Logger::Formatter.should_receive(:new).once.and_return(formatter)
20
+ @logger.should_receive(:formatter=).once.with(formatter)
21
+
22
+ # and info message should be logged
23
+ @logger.should_receive(:info).once
24
+
25
+ IpcAuthpipe::Log.init('test.log', 'debug')
26
+ end
27
+
28
+ it "should delegate all common logging methods to stdlib's logger" do
29
+ IpcAuthpipe::Log.init('test.log', 'debug')
30
+
31
+ [ :debug, :info, :warn, :error, :fatal, :add, :unknown].each do |msg|
32
+ @logger.should_receive(msg).with('test').once
33
+ IpcAuthpipe::Log.send(msg, 'test')
34
+ end
35
+ end
36
+
37
+ end
@@ -0,0 +1,45 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ require 'ipcauthpipe/processor'
4
+
5
+ describe 'Request processor' do
6
+
7
+ before(:each) do
8
+ @processor = IpcAuthpipe::Processor.new
9
+ end
10
+
11
+ it "should split a request into a command and it's parameters" do
12
+ @processor.split_request('PRE . pop3 myname').should == { :command => 'PRE', :params => '. pop3 myname'}
13
+ @processor.split_request('ENUMERATE').should == { :command => 'ENUMERATE', :params => nil}
14
+ @processor.split_request('ENUMERATE ').should == { :command => 'ENUMERATE', :params => nil}
15
+ end
16
+
17
+ it "should raise an exception for invalid request and log it" do
18
+ lambda { @processor.split_request('FOOBAR') }.should raise_error(RuntimeError)
19
+ lambda { @processor.split_request('NO . COMMAND') }.should raise_error(RuntimeError)
20
+ end
21
+
22
+ it "should log failed requests" do
23
+ IpcAuthpipe::Log.should_receive(:debug).once
24
+ IpcAuthpipe::Log.should_receive(:fatal).once
25
+ lambda { @processor.process('FOOBAR') }.should raise_error(RuntimeError)
26
+ end
27
+
28
+ it "should delegate processing to the command's handler" do
29
+ # Set up a fake Auth handler
30
+ module IpcAuthpipe
31
+ module Handler
32
+ class Auth
33
+ def self.process
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ # Expect the action to be logged
40
+ IpcAuthpipe::Log.should_receive(:debug).once
41
+ # And try to call it
42
+ IpcAuthpipe::Handler::Auth.should_receive(:process).with('53').once
43
+ @processor.call_handler_for(:command => 'AUTH', :params => '53')
44
+ end
45
+ end
@@ -0,0 +1,25 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ require 'activerecord'
4
+ require 'models/member_converge'
5
+
6
+ describe 'Member Converge' do
7
+
8
+ before(:each) do
9
+ salt = '/yU(t'
10
+ password = 'testtest'
11
+ @converge = MemberConverge.new(
12
+ # password is testtest
13
+ :converge_pass_hash => Digest::MD5.hexdigest(
14
+ Digest::MD5.hexdigest(salt) + Digest::MD5.hexdigest(password)
15
+ ),
16
+ :converge_pass_salt => salt
17
+ )
18
+ end
19
+
20
+ it "should verify cleartext password against hashed one" do
21
+ @converge.valid_password?('foobar').should == false
22
+ @converge.valid_password?('testtest').should == true
23
+ end
24
+
25
+ end
@@ -0,0 +1,56 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ require 'activerecord'
4
+ require 'models/member'
5
+ require 'models/member_converge'
6
+
7
+ describe 'Member' do
8
+
9
+ before(:all) do
10
+ IpcAuthpipe::Log.logger = stub_everything
11
+ end
12
+
13
+ before(:each) do
14
+ Member.delete_all
15
+ @tester_converge = mock('member_converge') #,
16
+ @member = Member.create(:email => 'test@test.com', :name => 'tester')
17
+ MemberConverge.stub!(:find).and_return(@tester_converge)
18
+ #:pass_hash => '3a063c7f0d62df2ff444ca22455a7232', :pass_salt => '/yU(t') # password is 'testtest'
19
+ end
20
+
21
+ it "should find a member by his username and cleartext password" do
22
+ MemberConverge.should_receive(:find).once.and_return(@tester_converge)
23
+ @tester_converge.should_receive(:valid_password?).with('testtest').once.and_return(true)
24
+
25
+ Member.find_by_name_and_password( @member.name, 'testtest' ).should == @member
26
+ end
27
+
28
+ it "should raise AuthenticationFailed exception on invalid password" do
29
+ MemberConverge.should_receive(:find).once.and_return(@tester_converge)
30
+ @tester_converge.should_receive(:valid_password?).with('wrongpassword').once.and_return(false)
31
+
32
+ lambda { Member.find_by_name_and_password( @member.name, 'wrongpassword' ) }.should raise_error(IpcAuthpipe::AuthenticationFailed)
33
+ end
34
+
35
+ it "should raise AuthenticationFailed exception on invalid username" do
36
+ MemberConverge.should_receive(:find).never
37
+ @tester_converge.should_receive(:valid_password?).never
38
+
39
+ lambda { Member.find_by_name_and_password( 'foobarname', 'wrongpassword' ) }.should raise_error(IpcAuthpipe::AuthenticationFailed)
40
+ end
41
+
42
+ it "should dump itself into text string for authlib" do
43
+ member = Member.new(
44
+ :name => 'tester'
45
+ )
46
+
47
+ member.to_authpipe.should == [
48
+ "USERNAME=#{IpcAuthpipe::config.mail['owner_username']}",
49
+ "GID=#{IpcAuthpipe::config.mail['owner_gid']}",
50
+ "HOME=/home/vmail/tester",
51
+ "ADDRESS=tester@poker.ru",
52
+ "."
53
+ ].join("\n") + "\n"
54
+ end
55
+
56
+ end
@@ -0,0 +1,23 @@
1
+ # $TESTING=true
2
+ $:.push File.join(File.dirname(__FILE__), '..', 'lib')
3
+ #require 'rubygems'
4
+ #require 'bacon'
5
+ #require 'github_post_receive_server'
6
+
7
+ require 'ipcauthpipe'
8
+ require 'ipcauthpipe/log'
9
+ # stubbing out Log's methods
10
+ module IpcAuthpipe::Log
11
+ class << self
12
+ attr_accessor :logger
13
+ end
14
+ end
15
+ # and init test config
16
+ IpcAuthpipe.init('test/config.yml')
17
+
18
+ # helper method for Array class to use in rspec's duck_type expectation
19
+ class Array
20
+ def is_array
21
+ true
22
+ end
23
+ end
metadata ADDED
@@ -0,0 +1,82 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: morhekil-ipcauthpipe
3
+ version: !ruby/object:Gem::Version
4
+ version: "0.1"
5
+ platform: ruby
6
+ authors:
7
+ - Oleg Ivanov
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-09-10 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: activerecord
17
+ version_requirement:
18
+ version_requirements: !ruby/object:Gem::Requirement
19
+ requirements:
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 2.1.0
23
+ version:
24
+ description: ipcauthpipe gem implements Courier's authpipe protocol to interface Courier POP/IMAP server with Invision Power Board / Converge members database.
25
+ email: morhekil@morhekil.net
26
+ executables:
27
+ - ipcauthpipe
28
+ extensions: []
29
+
30
+ extra_rdoc_files: []
31
+
32
+ files:
33
+ - lib/ipcauthpipe/handler/auth.rb
34
+ - lib/ipcauthpipe/handler/enumerate.rb
35
+ - lib/ipcauthpipe/handler/passwd.rb
36
+ - lib/ipcauthpipe/handler/pre.rb
37
+ - lib/ipcauthpipe/handler.rb
38
+ - lib/ipcauthpipe/log.rb
39
+ - lib/ipcauthpipe/processor.rb
40
+ - lib/ipcauthpipe/reader.rb
41
+ - lib/models/member.rb
42
+ - lib/models/member_converge.rb
43
+ - lib/ipcauthpipe.rb
44
+ - bin/ipcauthpipe
45
+ - ipcauthpipe.gemspec
46
+ - README
47
+ - config.yml
48
+ has_rdoc: false
49
+ homepage: http://twitter.com/morhekil
50
+ post_install_message:
51
+ rdoc_options: []
52
+
53
+ require_paths:
54
+ - lib
55
+ required_ruby_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: "0"
60
+ version:
61
+ required_rubygems_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: "0"
66
+ version:
67
+ requirements: []
68
+
69
+ rubyforge_project:
70
+ rubygems_version: 1.2.0
71
+ signing_key:
72
+ specification_version: 2
73
+ summary: Implementation of Courier's authpipe protocol over Invision Power Board / Converge.
74
+ test_files:
75
+ - test/ipcauthpipe/handler/auth_spec.rb
76
+ - test/ipcauthpipe/handler/pre_spec.rb
77
+ - test/ipcauthpipe/log_spec.rb
78
+ - test/ipcauthpipe/processor_spec.rb
79
+ - test/models/member_spec.rb
80
+ - test/models/member_converge_spec.rb
81
+ - test/spec_helper.rb
82
+ - test/config.yml