morhekil-ipcauthpipe 0.1

Sign up to get free protection for your applications and to get access to all the features.
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