Issues (1)

src/SIP2Client.php (1 issue)

Labels
Severity
1
<?php
2
3
namespace lordelph\SIP2;
4
5
/**
6
 * SIP2Client Class
7
 *
8
 * This class provides a method of communicating with an Integrated
9
 * Library System using 3M's SIP2 standard.
10
 *
11
 * @licence    https://opensource.org/licenses/MIT
12
 * @copyright  John Wohlers <[email protected]>
13
 * @copyright  Paul Dixon <[email protected]>
14
 */
15
16
use lordelph\SIP2\Exception\RuntimeException;
17
use lordelph\SIP2\Request\SIP2Request;
18
use lordelph\SIP2\Response\SIP2Response;
19
use Psr\Log\LoggerAwareInterface;
20
use Psr\Log\LoggerAwareTrait;
21
use Psr\Log\LoggerInterface;
22
use Psr\Log\NullLogger;
23
use Socket\Raw\Factory;
24
use Socket\Raw\Socket;
25
26
/**
27
 * SIP2Client provides a simple client for SIP2 library services
28
 *
29
 * In the specification, and the comments below, 'SC' (or Self Check) denotes the client, and ACS (or Automated
30
 * Circulation System) denotes the server.
31
 */
32
class SIP2Client implements LoggerAwareInterface
33
{
34
    use LoggerAwareTrait;
35
36
    //-----------------------------------------------------
37
    // request options
38
    //-----------------------------------------------------
39
40
    /**
41
     * @var array name=>value request defaults used for every request
42
     */
43
    private $default=[];
44
45
    //-----------------------------------------------------
46
    // connection handling
47
    //-----------------------------------------------------
48
49
    /** @var int maximum number of resends in the event of CRC failure */
50
    public $maxretry = 3;
51
52
    /** @var Socket */
53
    private $socket;
54
55
    /** @var Factory injectable factory for creating socket connections */
56
    private $socketFactory;
57
58
    /** @var bool CRC checks can be disabled by setting this to false */
59
    private static $enableCRCChecks = true;
60
61
    /**
62
     * Constructor allows you to provide a PSR-3 logger, but you can also use the setLogger method
63
     * later on.
64 5
     *
65
     * @param LoggerInterface|null $logger
66 5
     */
67 5
    public function __construct(LoggerInterface $logger = null)
68 5
    {
69
        $this->logger = $logger ?? new NullLogger();
70 5
        $this->setDefault('InstitutionId', 'WohlersSIP');
71
    }
72 5
73 5
    public function setDefault($name, $value)
74
    {
75
        $this->default[$name] = $value;
76
    }
77
78
    /**
79
     * Can be used to disable CRC checks
80
     *
81 5
     * Some SIP2 services have been observed producing invalid CRCs when extended/UTF-8 chars are involved. As
82
     * the CRC was originally part of the protocol for noisy serial lines, it serves little purpose in error
83 5
     * checking when connecting via TCP/IP, but disable at your own risk!
84 5
     *
85
     * @param bool $enable
86
     */
87
    public static function enableCRCCheck($enable)
88
    {
89
        self::$enableCRCChecks = $enable;
90 5
    }
91
92 5
    public static function isCRCCheckEnabled()
93
    {
94
        return self::$enableCRCChecks;
95 5
    }
96
97
98
    /**
99
     * Allows an alternative socket factory to be injected. The allows us to
100
     * mock socket connections for testing
101
     *
102
     * @param Factory $factory
103
     */
104 3
    public function setSocketFactory(Factory $factory)
105
    {
106 3
        $this->socketFactory = $factory;
107 3
    }
108
109
    /**
110 3
     * Get the current socket factory, creating a default one if necessary
111 2
     * @return Factory
112
     */
113
    private function getSocketFactory()
114 3
    {
115
        if (is_null($this->socketFactory)) {
116 3
            $this->socketFactory = new Factory(); //@codeCoverageIgnore
117 3
        }
118
        return $this->socketFactory;
119 3
    }
120
121 3
122 3
    /**
123
     * @param SIP2Request $request
124 3
     * @return SIP2Response
125
     * @throws RuntimeException if server fails to produce a valid response
126 3
     */
127
    public function sendRequest(SIP2Request $request) : SIP2Response
128
    {
129
        foreach ($this->default as $name => $value) {
130
            $request->setDefault($name, $value);
131
        }
132
133
        $raw = $this->getRawResponse($request);
134
        return SIP2Response::parse($raw);
135 3
    }
136
137
    private function getRawResponse(SIP2Request $request, $depth = 0)
138
    {
139 3
        $result = '';
140
        $terminator = '';
141 3
142
        $message = $request->getMessageString();
143
144 3
        $this->logger->debug('SIP2: Sending SIP2 request '.trim($message));
0 ignored issues
show
The method debug() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

144
        $this->logger->/** @scrutinizer ignore-call */ 
145
                       debug('SIP2: Sending SIP2 request '.trim($message));

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
145 2
        $this->socket->write($message);
146
147
        $this->logger->debug('SIP2: Request Sent, Reading response');
148 2
149 2
        while ($terminator != "\x0D") {
150 2
            //@codeCoverageIgnoreStart
151 2
            try {
152
                $terminator = $this->socket->recv(1, 0);
153 1
            } catch (\Exception $e) {
154 1
                break;
155 1
            }
156
            //@codeCoverageIgnoreEnd
157
158
            $result = $result . $terminator;
159 2
        }
160
        //if the remote end uses CRLF as a terminator, we might have a stray LF in our buffer - by trimming
161
        //the responses, we ensure those don't upset us
162
        $result=trim($result);
163
164
        $this->logger->info("SIP2: result={$result}");
165
166
        // test message for CRC validity
167
        if (SIP2Response::checkCRC($result)) {
168
            $this->logger->debug("SIP2: Message from ACS passed CRC check");
169
        } else {
170
            //CRC check failed, we resend the request
171
            if ($depth < $this->maxretry) {
172 5
                $depth++;
173
                $this->logger->warning("SIP2: Message failed CRC check, retry {$depth})");
174 5
                $result = $this->getRawResponse($request, $depth);
175
            } else {
176
                $errMsg="SIP2: Failed to get valid CRC after {$this->maxretry} retries.";
177 5
                $this->logger->critical($errMsg);
178 1
                throw new RuntimeException($errMsg);
179 1
            }
180 1
        }
181 1
182
        return $result;
183
    }
184 4
185 4
    /**
186
     * Connect to ACS via SIP2
187
     *
188
     * The $bind parameter can be useful where a machine which has multiple outbound connections and its important
189
     * to control which one is used (normally because the remote SIP2 service is firewalled to particular IPs
190
     *
191
     * @param string $address ip:port of remote SIP2 service
192
     * @param string|null $bind local ip to bind socket to
193
     * @param int $timeout number of seconds to allow for connection to succeed
194
     */
195 5
    public function connect($address, $bind = null, $timeout = 15)
196
    {
197 5
        $this->logger->debug("SIP2Client: Attempting connection to $address");
198
199 5
        try {
200
            $this->socket = $this->createClient($address, $bind, $timeout);
201
        } catch (\Exception $e) {
202 5
            $this->socket = null;
203 1
            $this->logger->error("SIP2Client: Failed to connect: " . $e->getMessage());
204 1
            throw new RuntimeException("Connection failure", 0, $e);
205
        }
206
207 5
        $this->logger->debug("SIP2Client: connected");
208
    }
209
210
    /**
211 5
     * A 'missing' factory method which allows us to establish a socket bound to a particular interface
212 4
     * @param $address
213
     * @param $bind
214 1
     * @param $timeout
215 1
     * @return Socket
216 1
     * @throws \Exception
217
     */
218
    private function createClient($address, $bind, $timeout) : Socket
219 4
    {
220
        $factory = $this->getSocketFactory();
221
222
        $socket = $factory->createFromString($address, $scheme);
223
224
        try {
225
            if (!empty($bind)) {
226 1
                $this->logger->debug("SIP2Client: binding socket to $bind");
227
                $socket->bind($bind);
228 1
            }
229 1
230 1
            if ($timeout === null) {
231
                $socket->connect($address);
232
            } else {
233
                // connectTimeout enables non-blocking mode, so turn blocking on again
234
                $socket->connectTimeout($address, $timeout);
235
                $socket->setBlocking(true);
236
            }
237
        } catch (\Exception $e) {
238
            $socket->close();
239
            throw $e;
240
        }
241
242
        return $socket;
243
    }
244
245
246
    /**
247
     * Disconnect from ACS
248
     */
249
    public function disconnect()
250
    {
251
        $this->socket->close();
252
        $this->socket = null;
253
    }
254
}
255