Ratchet 웹 소켓 서버를 실행하면 소켓의 정보만 있다면 로컬에서만 접속가능한 환경이 아닐 경우 TCP URI 정보만 있다면 누구든 클라이언트에서 웹 소켓을 연결하여 접속을 할 수 있게된다. 우리는 이를 사전에 막아 보안강화를 할 필요가 있다.
기본적으로 포트를 막는 방법엔 아래와 같이 두가지 방법이 있다.
2. 접속은 허용하되 Ratchet 소켓 프로그램의 사용자 정의 클래스 Chat.php 파일에서 막는방법
우선 1번째 iptables 에서 막는 방법은 아래와 같다. (웹 소켓 포트가 4343이고, 127.0.0.1 아이피만 허용할 경우)
-A INPUT -p tcp --dport 4343 -s 127.0.0.1 -j ACCEPT
-A INPUT -p tcp --dport 4343 -j DROP
위와 같이 iptables 에서 특정 아이피만 접속할 수 있도록 만드는 방법인데 이방법을 사용하면 특정 아이피가 아닌경우 그 누구도 접속을 못하게되기때문에 특수한 경우가 아니라면 Ratchet 의 Chat.php 파일의 사용자 정의 프로세서에서 처리하게 좋다.
아래는 앞선 포스팅에서 작성한 Chat.php이며 총 두가지로 제어를 하고 있다. 하나는 특정 유저의 아이피와 특정 호스트 접근 제어로 자세한 내용은 아래 소스를 확인해보도록 하자
<?php # 파일경로: /library/Ratchet/src/Chat.php
namespace MyApp;
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;
class Chat implements MessageComponentInterface{
protected $clients;
// 연결을 허용할 클라이언트 host 를 설정
protected $allowHost = array(
'blog.redinfo.co.kr',
);
// 접근을 차단할 아이피
protected $DenyIps = array(
'198.155.23.66',
'198.155.23.65',
);
// 생성자
public function __construct() {
// 내장 SQL객체스토리지 연결 (접속한 클라이언트의 정보가 이곳에 계속 담긴다.)
$this->clients = new \SplObjectStorage();
}
// 유저 접속 시
public function onOpen(ConnectionInterface $conn)
{
// 연결 요청된 호스트를 체크
$origin = '';
if( !empty($conn->httpRequest->getHeader('Origin')) && is_array($conn->httpRequest->getHeader('Origin'))){
$origin = $conn->httpRequest->getHeader('Origin');
$origin = end($origin);
$origin = str_replace(array("http://","https://"),"",$origin);
}
$origin = empty($origin) ? '알수없음': $origin;
if($origin == '알수없음' || in_array($origin,$this->allowHost) < 1){
$conn->send('<strong style="color:red;">연결실패.</strong>');
$conn->send('<strong style="color:red;">연결 호스트: '.$origin.'</strong>');
$conn->close();
return false;
}
// 접속한 유저의 아이피를 체크
if( in_array($conn->remoteAddress, $this->DenyIps) > 0){
$conn->send('<strong style="color:red;">차단된 IP 접근입니다.</strong>');
$conn->send('<strong style="color:red;">유저IP: '.$conn->remoteAddress.'</strong>');
$conn->close();
return false;
}
// 접속한 사용자 커넥션 추가
$this->clients->attach($conn);
// 접속한 클라이언트 총 수
$clientCount = count($this->clients);
// 환영 메시지 전송
$conn->send('환영합니다 (고유번호: '.$conn->resourceId.', '.number_format($clientCount).'명 접속중)');
// 현재 접속한 클라이언트에게 상대방 입장 메시지를 전달 (접속한 사용자는 제외)
foreach ($this->clients as $client) {
// 접속한 사용자는 제외
if( $client != $conn){
$resMsg = '';
$client->send('상대방 입장 (고유번호: '.$conn->resourceId.', '.number_format($clientCount).'명 접속중)');
}
}
}
// 응답전문: 메세지 수신
public function onMessage(ConnectionInterface $from, $msg)
{
// 현재 접속한 클라이언트에게 상대방 입장 메시지를 전달 (상대방과 나를 분리, $form의 경우 메시지 전달한 사람의 정보다.)
foreach ($this->clients as $client) {
$resMsg = '';
if( $from == $client){ $resMsg = '[나('.$from->resourceId.')] '; }
else{ $resMsg = '[상대방('.$from->resourceId.')] '; }
$resMsg .= $msg;
$client->send($resMsg);
}
}
// 응답전문: 사용자 웹 소켓 종료 시
public function onClose(ConnectionInterface $conn)
{
// 연결이 종료되면 해당 사용자의 연결을 내장 SQL객체스토리지에서 제외한다.
$this->clients->detach($conn);
// 클라이언트 총 개수
$clientCount = count($this->clients);
// 현재 접속한 클라이언트에게 상대방 퇴장 메시지를 전달 (퇴장한 사용자는 제외)
foreach ($this->clients as $client) {
if( $client != $conn){
$resMsg = '';
$client->send('상대방 퇴장 (고유번호: '.$conn->resourceId.', '.number_format($clientCount).'명 접속중)');
}
}
}
// 응답전문: 사용자 웹 소켓 오류 발생 시
public function onError(ConnectionInterface $conn, \Exception $e)
{
// 해당 커넥션을 끊는다.
$conn->close();
}
}
위의 소스를 보면 가장 먼저 연결을 허용할 클라이언트 호스트와 접속을 차단할 아이피에 대한 배열 객체 변수를 설정해 두었다.
// 연결을 허용할 클라이언트 host 를 설정
protected $allowHost = array(
'blog.redinfo.co.kr',
);
// 접근을 차단할 아이피
protected $DenyIps = array(
'198.155.23.66',
'198.155.23.65',
);
`protected $allowHost`는 연결을 허용할 클라이언트의 호스트 주소를 배열로 추가해주었다.
`protected $DenyIps`는 접근을 차단할 아이피를 배열로 추가해주었다.
두번째로는 연결 요청된 호스트를 체크하여 허용된 호스트가 아닐 경우 차단하는 부분이다.
// 연결 요청된 호스트를 체크
$origin = '';
if( !empty($conn->httpRequest->getHeader('Origin')) && is_array($conn->httpRequest->getHeader('Origin'))){
$origin = $conn->httpRequest->getHeader('Origin');
$origin = end($origin);
$origin = str_replace(array("http://","https://"),"",$origin);
}
$origin = empty($origin) ? '알수없음': $origin;
if($origin == '알수없음' || in_array($origin,$this->allowHost) < 1){
$conn->send('<strong style="color:red;">연결실패.</strong>');
$conn->send('<strong style="color:red;">연결 호스트: '.$origin.'</strong>');
$conn->close();
return false;
}
`$conn->httpRequest->getHeader('Origin')`객체 접근을 통해 접속한 클라이언트의 호스트 주소를 배열로 받을 수 있다. 배열의 경우 여러번 테스트해봐도 항상 1개만 있기에 왜 배열로 받는지는 정확하게 모르겠지만 혹시몰라서 배열 원소에서 마지막 키에 있는 값을 가져오도록 구성하였다.
또한 Origin 에서 받은 값은 http:// 또는 https:// 가 포함되어있기에 이를 제거하여 비교하였는데 이방법 이외 `parse_url` 함수를 통해 host 를 별도 추출하여 처리할 수 있으니 필요할 경우 확인 후 커스텀하면된다.
마지막으로는 접속한 유저의 아이피를 차단하는 구간이다.
// 접속한 유저의 아이피를 체크
if( in_array($conn->remoteAddress, $this->DenyIps) > 0){
$conn->send('<strong style="color:red;">차단된 IP 접근입니다.</strong>');
$conn->send('<strong style="color:red;">유저IP: '.$conn->remoteAddress.'</strong>');
$conn->close();
return false;
}
`$conn->remoteAddress` 객체는 접속한 유저의 아이피를 return 해주며 이 객체를 이용하여 차단이 가능하다.
위와 같이 소켓 접속 시 사용자가 접속한 클라이언트 호스트와 아이피를 이용한 제어 방법에 대해 알아보았으며 이를 이용하여 한층더 업그레이드된 웹 소켓 서버를 구축할 수 있다.
다만 지금까지의 포스팅에서는 DB 연결을 사용하지 않았기때문에 허용 호스트 또는 차단 아이피가 변경될때마다 소스코드를 수정하고 웹 소켓서버를 재시작 해주어야 하는 불편함이 있다. DB 연결은 추후 포스팅에서 소개할 예정이니 참고 바란다.
마지막으로 다음편에서는 접속한 호스트의 httpRequest 의 uri 객체를 활용하여 채널분리와 사용자인증 방법에 대해 포스팅할 예정이며 해당 방식을 미리 간단하게 설명하자면 아래와 같다.
| 웹 소켓 클라이언트 연결 소스
// 웹소켓연결함수
function WebSocketConnect(){
socket = new WebSocket("wss://chat.redinfo.co.kr:4343/{채널코드}/{인증토큰}");
socket.onopen = function(e){ view('접속이 연결되었습니다.');}
socket.onmessage = function(e){ view(e.data); };
socket.onclose = function(e){
console.log(e);
view('연결종료');
if(confirm("연결이 종료되었습니다.\n다시 접속하시겠습니까?")){
WebSocketConnect();
}
};
socket.onerror = function(e){
view('오류로 인한 소켓연결 종료');
console.log(e.message);
};
}
위와 같이 접속을 하면 Chat.php 파일에서는 접속한 채널을 분리하고, 미리 정의된 공통 인증토큰을 체크하는 방법이다. 자세한 내용은 다음 포스팅에서 알아보도록하자.