Update: (Sorry, this thread is beginning to turn into my little diary of events)
The stuff to fake a TCP socket connection:
Code: Select all
<?php
class FakeStream
{
private $processor;
public function stream_open($path, $mode, $options, &$opened_path)
{
$this->processor = FakeStream_CommandProcessor::getInstance();
$this->processor->isOpen = true;
return $this->processor->isOpen;
}
public function stream_read($size)
{
return $this->processor->getResponse($size);
}
public function stream_write($string)
{
if ($this->processor->isOpen)
{
$this->processor->setCommand($string);
return strlen($string);
}
else return 0;
}
public function stream_eof()
{
//Does nothing... SMTP doesn't implement it
// It's vital we can work this out ourselves
}
public function stream_close()
{
$this->processor->destroy();
}
}
stream_wrapper_register('fake', 'FakeStream');
?>
Code: Select all
<?php
class FakeStream_CommandProcessor
{
static private $instance = null;
public $command;
public $response = "";
private $observers = array();
public $isOpen = false;
public $hanging = false;
private function __construct() {}
public function addObserver($observer)
{
$this->observers[] = $observer;
}
static public function getInstance()
{
if (self::$instance === null) self::$instance = new FakeStream_CommandProcessor();
return self::$instance;
}
public function setCommand($command)
{
$this->command .= $command;
foreach ($this->observers as $i => $o) $this->observers[$i]->command($this->command);
}
public function setResponse($response)
{
$this->response .= $response."\r\n";
}
public function getResponse($size)
{
$ret = substr($this->response, 0, $size);
$this->response = substr($this->response, $size);
//Fake SMTP's behaviour of hanging past EOF
if (!strlen($ret)) $this->hanging = true;
else $this->hanging = false;
return $ret;
}
public function isHanging()
{
return $this->hanging;
}
public function destroy()
{
self::$instance = null;
$this->isOpen = false;
}
}
?>
The test for that Fake connection stuff
Code: Select all
<?php
require_once '../config.php';
require_once SIMPLETEST_BASE . '/unit_tester.php';
require_once SIMPLETEST_BASE . '/reporter.php';
require_once 'FakeStream.php';
require_once 'FakeStream/CommandProcessor.php';
class TestOfFakeStream extends UnitTestCase
{
private $stream;
private $processor;
public function setup()
{
$this->processor = FakeStream_CommandProcessor::getInstance();
}
public function testCreatingStream()
{
$this->assertFalse($this->processor->isOpen);
$this->stream = fopen('fake://esmtp', 'w+');
$this->assertTrue($this->processor->isOpen);
$this->assertTrue(is_resource($this->stream));
}
public function testReading()
{
$this->processor->setResponse("250 TESTING");
$response = fgets($this->stream);
$this->assertEqual($response, "250 TESTING\r\n");
}
public function testExceedEofHang()
{
$this->assertFalse($this->processor->isHanging());
$response = fgets($this->stream);
$this->assertTrue($this->processor->isHanging());
}
public function testWriting()
{
$written = fwrite($this->stream, "TEST WRITING\r\n");
$this->assertTrue($written);
$this->assertEqual($this->processor->command, "TEST WRITING\r\n");
}
public function testClosingStream()
{
$this->assertTrue(is_resource($this->stream));
$this->assertTrue($this->processor->isOpen);
fclose($this->stream);
$this->assertFalse($this->processor->isOpen);
$this->assertFalse(is_resource($this->stream));
}
}
$test = new TestOfFakeStream();
$test->run(new HtmlReporter());
?>
The observer that makes it SMTP-like
Code: Select all
<?php
class SmtpMsg
{
static public function greeting()
{
return "220 fakestream.spoofed ESMTP FakeStream 0.0.0 ".date('r', strtotime('20060817'));
}
static public function badCommand()
{
return "500 bad command";
}
static public function EHLO()
{
return "250-fakestream.spoofed Hello localhost.localdomain [127.0.0.1]".
"\r\n250-PIPELINING".
"\r\n250 AUTH LOGIN PLAIN CRAM-MD5";
}
static public function OK()
{
return "250 OK";
}
static public function dataGoAhead()
{
return "354 Go Ahead";
}
}
class FakeStream_SmtpStub
{
private $stream;
private $ending = "\r\n";
private $validCommandSent = false;
private $rcptSent = false;
private $mailSent = false;
private $dataSent = false;
public function __construct($stream)
{
$this->stream = $stream;
$this->stream->setResponse($this->getGreeting());
}
public function getGreeting()
{
$this->validCommandSent = false;
return SmtpMsg::greeting();
}
public function getBadCommand()
{
return SmtpMsg::badCommand();
}
public function getEHLO()
{
if (!$this->validCommandSent)
{
$this->validCommandSent = true;
return SmtpMsg::EHLO();
}
else return $this->getBadCommand();
}
public function getOK()
{
$this->validCommandSent = true;
return SmtpMsg::OK();
}
public function getMAIL()
{
if ($this->validCommandSent && !$this->rcptSent && !$this->dataSent)
{
$dot_atom_re = '[-!#\$%&\'\*\+\/=\?\^_`{}\|~0-9A-Z]+(?:\.[-!#\$%&\'\*\+\/=\?\^_`{}\|~0-9A-Z]+)*';
$implemented_domain_re = '[-0-9A-Z]+(?:\.[-0-9A-Z]+)*';
$full_pattern = $dot_atom_re.'(?:@'.$implemented_domain_re.')?';
if (preg_match("/^\s*mail\ +from:\s*(?:(<".$full_pattern.">)|(".$full_pattern.")|(<\ *>))\s*\r\n$/i",
$this->stream->command))
{
$this->mailSent = true;
return $this->getOK();
}
else return $this->getBadCommand();
}
else return $this->getBadCommand();
}
public function getRCPT()
{
if ($this->validCommandSent && $this->mailSent && !$this->dataSent)
{
$dot_atom_re = '[-!#\$%&\'\*\+\/=\?\^_`{}\|~0-9A-Z]+(?:\.[-!#\$%&\'\*\+\/=\?\^_`{}\|~0-9A-Z]+)*';
$implemented_domain_re = '[-0-9A-Z]+(?:\.[-0-9A-Z]+)*';
$full_pattern = $dot_atom_re.'(?:@'.$implemented_domain_re.')?';
if (preg_match("/^\s*rcpt\ +to:\s*(?:(<".$full_pattern.">)|(".$full_pattern.")|(<\ *>))\s*\r\n$/i",
$this->stream->command))
{
$this->rcptSent = true;
return $this->getOK();
}
else return $this->getBadCommand();
}
else return $this->getBadCommand();
}
public function getQUIT()
{
$this->validCommandSent = false;
$this->dataSent = false;
$this->mailSent = false;
$this->rcptSent = false;
return SmtpMsg::OK();
}
public function getDataGoAhead()
{
if ($this->validCommandSent && $this->rcptSent && !$this->dataSent)
{
if (preg_match("/^\s*data\s*\r\n/i", $this->stream->command))
{
$this->validCommandSent = true;
$this->dataSent = true;
$this->mailSent = false;
$this->rcptSent = false;
$this->ending = "\r\n.\r\n";
return SmtpMsg::dataGoAhead();
}
else return $this->getBadCommand();
}
else return $this->getBadCommand();
}
public function getMessageSent()
{
$this->ending = "\r\n";
$this->dataSent = false;
return SmtpMsg::OK();
}
public function command(&$string)
{
$this->stream->hanging = true;
if (substr($string, -(strlen($this->ending))) != $this->ending) return;
$keyword = @strtolower(preg_replace('/^\s*([A-Z]+)\s*.*\r\n$/i', '$1', $string));
switch ($keyword)
{
case 'ehlo':
$this->stream->setResponse($this->getEHLO());
break;
case 'mail':
$this->stream->setResponse($this->getMAIL());
break;
case 'rcpt':
$this->stream->setResponse($this->getRCPT());
break;
case 'data':
$this->stream->setResponse($this->getDataGoAhead());
break;
case 'quit':
$this->stream->setResponse($this->getQUIT());
break;
default:
if ($this->dataSent) $this->stream->setResponse($this->getMessageSent());
else $this->stream->setResponse($this->getBadCommand());
break;
}
$string = "";
$this->stream->hanging = false;
}
}
?>
And the test for the observer:
Code: Select all
<?php
require_once '../config.php';
require_once SIMPLETEST_BASE . '/unit_tester.php';
require_once SIMPLETEST_BASE . '/reporter.php';
require_once 'FakeStream.php';
require_once 'FakeStream/CommandProcessor.php';
require_once 'FakeStream/SmtpStub.php';
class TestOfSmtpStub extends UnitTestCase
{
private function readFullResponse($stream)
{
$ret = "";
do
{
$line = fgets($stream);
if ($line == "") return;
$ret .= $line;
} while (substr($line, 3, 1) != " ");
return $ret;
}
public function testWaitingForEOL()
{
$stream = fopen('fake://esmtp', 'w+');
$processor = FakeStream_CommandProcessor::getInstance();
$processor->addObserver(new FakeStream_SmtpStub($processor));
$this->assertFalse($processor->isHanging());
fwrite($stream, "Command has no EOL");
$this->assertTrue($processor->isHanging());
$this->assertEqual($processor->command, "Command has no EOL");
fwrite($stream, " yet\r\n");
$this->assertFalse($processor->isHanging());
//The command should have been flushed
$this->assertEqual($processor->command, "");
fclose($stream);
}
public function testValidSendSequence()
{
$stream = fopen('fake://esmtp', 'w+');
$processor = FakeStream_CommandProcessor::getInstance();
$processor->addObserver(new FakeStream_SmtpStub($processor));
$response = $this->readFullResponse($stream);
$this->assertEqual($response, SmtpMsg::greeting()."\r\n");
//We can only say this once
fwrite($stream, "EHLO localhost\r\n");
$this->assertFalse($processor->isHanging());
$response = $this->readFullResponse($stream);
$this->assertEqual($response, SmtpMsg::EHLO()."\r\n");
//We can get right down to DATA fine now
fwrite($stream, "MAIL FROM: <foo@bar>\r\n");
$this->assertFalse($processor->isHanging());
$response = $this->readFullResponse($stream);
$this->assertEqual($response, SmtpMsg::OK()."\r\n");
//We can send these many times in succession
fwrite($stream, "RCPT TO: <foo@bar>\r\n");
$this->assertFalse($processor->isHanging());
$response = $this->readFullResponse($stream);
$this->assertEqual($response, SmtpMsg::OK()."\r\n");
fwrite($stream, "RCPT TO: <foo@bar>\r\n");
$this->assertFalse($processor->isHanging());
$response = $this->readFullResponse($stream);
$this->assertEqual($response, SmtpMsg::OK()."\r\n");
//We can only send a string ending with \r\n.\r\n now
fwrite($stream, "DATA\r\n");
$this->assertFalse($processor->isHanging());
$response = $this->readFullResponse($stream);
$this->assertEqual($response, SmtpMsg::dataGoAhead()."\r\n");
//Note! The correct behaviour is to keep waiting for the CRLF.CRLF
fwrite($stream, "Test message line 1\r\n");
$this->assertTrue($processor->isHanging());
fwrite($stream, "Test message line 2\r\n.\r\n");
$this->assertFalse($processor->isHanging());
$response = $this->readFullResponse($stream);
$this->assertEqual($response, SmtpMsg::OK()."\r\n");
//We can send another message
fwrite($stream, "MAIL FROM: <foo@bar>\r\n");
$this->assertFalse($processor->isHanging());
$response = $this->readFullResponse($stream);
$this->assertEqual($response, SmtpMsg::OK()."\r\n");
fwrite($stream, "RCPT TO: <foo@bar>\r\n");
$this->assertFalse($processor->isHanging());
$response = $this->readFullResponse($stream);
$this->assertEqual($response, SmtpMsg::OK()."\r\n");
fwrite($stream, "DATA\r\n");
$this->assertFalse($processor->isHanging());
$response = $this->readFullResponse($stream);
$this->assertEqual($response, SmtpMsg::dataGoAhead()."\r\n");
fwrite($stream, "Test message 2\r\n.\r\n");
$this->assertFalse($processor->isHanging());
$response = $this->readFullResponse($stream);
$this->assertEqual($response, SmtpMsg::OK()."\r\n");
fwrite($stream, "QUIT\r\n");
$this->assertFalse($processor->isHanging());
$response = $this->readFullResponse($stream);
$this->assertEqual($response, SmtpMsg::OK()."\r\n");
fclose($stream);
}
public function testInvalidSequence()
{
$stream = fopen('fake://esmtp', 'w+');
$processor = FakeStream_CommandProcessor::getInstance();
$processor->addObserver(new FakeStream_SmtpStub($processor));
$response = $this->readFullResponse($stream);
$this->assertEqual($response, SmtpMsg::greeting()."\r\n");
//We can only say this once
fwrite($stream, "EHLO localhost\r\n");
$this->assertFalse($processor->isHanging());
$response = $this->readFullResponse($stream);
$this->assertEqual($response, SmtpMsg::EHLO()."\r\n");
//This shouldn't work because we need MAIL FROM
fwrite($stream, "RCPT TO: <foo@bar>\r\n");
$this->assertFalse($processor->isHanging());
$response = $this->readFullResponse($stream);
$this->assertEqual($response, SmtpMsg::badCommand()."\r\n");
//Even though it screamed at us before, it should forgive us now
fwrite($stream, "MAIL FROM: <foo@bar>\r\n");
$this->assertFalse($processor->isHanging());
$response = $this->readFullResponse($stream);
$this->assertEqual($response, SmtpMsg::OK()."\r\n");
//We shouldn't be able to do this yet!
fwrite($stream, "DATA\r\n");
$this->assertFalse($processor->isHanging());
$response = $this->readFullResponse($stream);
$this->assertEqual($response, SmtpMsg::badCommand()."\r\n");
//And the RCPT TO should work ok
fwrite($stream, "RCPT TO: <foo@bar>\r\n");
$this->assertFalse($processor->isHanging());
$response = $this->readFullResponse($stream);
$this->assertEqual($response, SmtpMsg::OK()."\r\n");
//NOW, and only now should DATA work
fwrite($stream, "DATA\r\n");
$this->assertFalse($processor->isHanging());
$response = $this->readFullResponse($stream);
$this->assertEqual($response, SmtpMsg::dataGoAhead()."\r\n");
fwrite($stream, "Test message 2\r\n.\r\n");
$this->assertFalse($processor->isHanging());
$response = $this->readFullResponse($stream);
$this->assertEqual($response, SmtpMsg::OK()."\r\n");
fwrite($stream, "QUIT\r\n");
$this->assertFalse($processor->isHanging());
$response = $this->readFullResponse($stream);
$this->assertEqual($response, SmtpMsg::OK()."\r\n");
//The connection would technically be closed so this should fail
fwrite($stream, "MAIL FROM: <foo@bar>\r\n");
$this->assertFalse($processor->isHanging());
$response = $this->readFullResponse($stream);
$this->assertEqual($response, SmtpMsg::badCommand()."\r\n");
fclose($stream);
}
public function testInvalidSyntax()
{
$stream = fopen('fake://esmtp', 'w+');
$processor = FakeStream_CommandProcessor::getInstance();
$processor->addObserver(new FakeStream_SmtpStub($processor));
$response = $this->readFullResponse($stream);
$this->assertEqual($response, SmtpMsg::greeting()."\r\n");
//We can only say this once
fwrite($stream, "EHLO localhost\r\n");
$this->assertFalse($processor->isHanging());
$response = $this->readFullResponse($stream);
$this->assertEqual($response, SmtpMsg::EHLO()."\r\n");
//Too many >>>'s, should bail out!
fwrite($stream, "MAIL FROM: <foo@bar>>>>\r\n");
$this->assertFalse($processor->isHanging());
$response = $this->readFullResponse($stream);
$this->assertEqual($response, SmtpMsg::badCommand()."\r\n");
//But it'll forgive us
fwrite($stream, "MAIL FROM: <foo@bar>\r\n");
$this->assertFalse($processor->isHanging());
$response = $this->readFullResponse($stream);
$this->assertEqual($response, SmtpMsg::OK()."\r\n");
//And the RCPT TO should work ok
fwrite($stream, "RCPT TO: <foo@bar>\r\n");
$this->assertFalse($processor->isHanging());
$response = $this->readFullResponse($stream);
$this->assertEqual($response, SmtpMsg::OK()."\r\n");
//This envelope will be rejected, but we can still send cos the first is OK
fwrite($stream, "RCPT TO: <foo@&&&^bar>\r\n");
$this->assertFalse($processor->isHanging());
$response = $this->readFullResponse($stream);
$this->assertEqual($response, SmtpMsg::badCommand()."\r\n");
//This should work fine
fwrite($stream, "DATA\r\n");
$this->assertFalse($processor->isHanging());
$response = $this->readFullResponse($stream);
$this->assertEqual($response, SmtpMsg::dataGoAhead()."\r\n");
//This shouldn't do anything
fwrite($stream, "QUIT\r\n");
$this->assertTrue($processor->isHanging());
fclose($stream);
}
}
$test = new TestOfSmtpStub();
$test->run(new HtmlReporter());
?>
All works well thank god, and despite it's length it's actually an extremely basic system. The only thing I haven't added (yet) is authentication.