160525 signalr self-host + SSL
160525 signalr self-host + SSL
황현동 노트북
signalr
blog
c#
.net
asp.net
katana
owin
개요
다수의 윈도우 PC들이 인트라넷에서 통신을 해야 할때 어떤 솔루션이 최선일까?
WebAPI, WCF, ZMQ, System.Net.Socket, winsock(ㅠㅠ) 까지 여러 솔루션들을 리서치해 봤지만 여러가지 면을 고려할때 signalr이 최선인것 같다.
signalr에 대한 소개는 관련 링크를 통해 대신하기로 하고 장점, 단점을 바로 설명한다.
WebAPI, WCF, ZMQ, System.Net.Socket, winsock(ㅠㅠ) 까지 여러 솔루션들을 리서치해 봤지만 여러가지 면을 고려할때 signalr이 최선인것 같다.
signalr에 대한 소개는 관련 링크를 통해 대신하기로 하고 장점, 단점을 바로 설명한다.
signalr 이란
http://www.asp.net/signalr
signalr 공식 사이트
signalr 공식 사이트
http://www.asp.net/signalr/overview/getting-started/tutorial-getting-started-with-signalr
signalr getting started
signalr getting started
http://www.asp.net/signalr/overview/guide-to-the-api/hubs-api-guide-net-client
signalr .net client
signalr .net client
http://www.egocube.pe.kr/Translation/Index/asp-net-signalr
signalr 한글번역 사이트
signalr 한글번역 사이트
양방향 통신 지원
사실 양방향 통신 지원 때문에 웹서버 이외의 솔루션을 찾게 된 것이다.
웹서버가 가장 널리 사용되니 쉽고 안정적인 솔루션이 많고, https를 지원하면 대개 충분한 보안도 확보할 수 있기 때문에 단방향만 필요하다면 그냥 웹서버나 이에서 파생된 솔루션을 쓰는게 좋을것이다.
하지만 signalr은 웹서버 파생기술이라 웹서버의 장점은 모두 취하면서 양방향 통신또한 가능하다.
signalr은 통신방식을 추상화하였고, 내부적으로 websocket을 기본으로 하며, long-polling, server-sent-message, forever-frame 방식들을 websocket이 불가한환경에서 자동 혹은 수동선택하여 사용할 수 있게 지원한다.
웹서버가 가장 널리 사용되니 쉽고 안정적인 솔루션이 많고, https를 지원하면 대개 충분한 보안도 확보할 수 있기 때문에 단방향만 필요하다면 그냥 웹서버나 이에서 파생된 솔루션을 쓰는게 좋을것이다.
하지만 signalr은 웹서버 파생기술이라 웹서버의 장점은 모두 취하면서 양방향 통신또한 가능하다.
signalr은 통신방식을 추상화하였고, 내부적으로 websocket을 기본으로 하며, long-polling, server-sent-message, forever-frame 방식들을 websocket이 불가한환경에서 자동 혹은 수동선택하여 사용할 수 있게 지원한다.
다양한 클라이언트 OS 지원
signalr은 여러가지 클라이언트 OS 를 지원한다.
(signalr 서버는 윈도우만 된다. 착오 없기를 바란다.)
웹서버 파생기술 이라 javascript를 당연히 지원하며,
ASP.NET 재단에서 나온 기술인 만큼 .NET, Silverlight, UWP를 잘 지원한다.
http를 프로토콜로 쓰는 오픈스펙이기 때문에 android, ios도 지원한다.
UWP를 지원하기 때문에 윈도우폰10, Windows10 IoT Core에서도 잘 동작한다.
아마도 javascript 웹 클라이언트와 .NET, android, ios 등의 네이티브 클라이언트 를 동시에 지원하는 양방향 통신 프레임워크는 signalr이 유일하지 않을까 싶다.
(signalr 서버는 윈도우만 된다. 착오 없기를 바란다.)
웹서버 파생기술 이라 javascript를 당연히 지원하며,
ASP.NET 재단에서 나온 기술인 만큼 .NET, Silverlight, UWP를 잘 지원한다.
http를 프로토콜로 쓰는 오픈스펙이기 때문에 android, ios도 지원한다.
UWP를 지원하기 때문에 윈도우폰10, Windows10 IoT Core에서도 잘 동작한다.
아마도 javascript 웹 클라이언트와 .NET, android, ios 등의 네이티브 클라이언트 를 동시에 지원하는 양방향 통신 프레임워크는 signalr이 유일하지 않을까 싶다.
http://www.asp.net/signalr/overview/guide-to-the-api/hubs-api-guide-javascript-client
signalr javascript client
signalr javascript client
http://www.asp.net/signalr/overview/guide-to-the-api/hubs-api-guide-net-client
signalr .net client
signalr .net client
https://github.com/SignalR/java-client
signalr android client
signalr android client
https://github.com/DyKnow/SignalR-ObjC
signalr ios client
signalr ios client
http://dotnetbyexample.blogspot.kr/2015/05/using-windows-10-uwp-app-and-signalr-on.html
signalr UWP client sample
signalr UWP client sample
강력한 보안
웹서버처럼 SSL인증서를 셋팅해 두고 https로 호스팅하고 접속할 수 있다.
SSL 인증서 셋팅하는 방법도 간단하다.
Socket이나 ZMQ에서 SSL을 사용하는 것에 비하면 정말 간단하다.
클라에 공개키인증서 유효성 검사하는 부분도 상당히 간단하게 구현할 수 있어서 SSL을 잘 활용할 수 있게 준비되어 있다.
샘플코드에서 self-signed SSL 인증서를 만들고 등록하고 특정포트에 바인딩하고, 클라이언트에서 이를 사용하면서 인증서 검사까지 하는 총 과정을 모두 기술할 것이다.
SSL 인증서 셋팅하는 방법도 간단하다.
Socket이나 ZMQ에서 SSL을 사용하는 것에 비하면 정말 간단하다.
클라에 공개키인증서 유효성 검사하는 부분도 상당히 간단하게 구현할 수 있어서 SSL을 잘 활용할 수 있게 준비되어 있다.
샘플코드에서 self-signed SSL 인증서를 만들고 등록하고 특정포트에 바인딩하고, 클라이언트에서 이를 사용하면서 인증서 검사까지 하는 총 과정을 모두 기술할 것이다.
쉬운 구현
다른 솔루션에 비해 압도적으로 간단하고 짧은 코드를 유지할 수 있는 장점이 있다.
항상 첫예제로 나오는 jquery 채팅예제는 처음보더라도 이해하는데 10분이면 충분하다.
대신 설정으로 건드릴수 있는 옵션이 적은 편이다.
항상 첫예제로 나오는 jquery 채팅예제는 처음보더라도 이해하는데 10분이면 충분하다.
대신 설정으로 건드릴수 있는 옵션이 적은 편이다.
쉬운 scale-out
signalr 서버는 scale-out도 간단하다.
클라들이 연결유지 방식이라 일반 웹서버보다 더 scale-out이 필요한 상황인데 아주 간단하게 해결된다.
총 3가지 방법이 있는데 구조와 설정방식은 대략 비슷하다.
클라들이 연결유지 방식이라 일반 웹서버보다 더 scale-out이 필요한 상황인데 아주 간단하게 해결된다.
총 3가지 방법이 있는데 구조와 설정방식은 대략 비슷하다.
Azure Service Bus 를 이용한 scale-out
http://www.asp.net/signalr/overview/performance/scaleout-with-windows-azure-service-bus
MS 솔루션인데 안나오면 섭섭하다! 기승전azure!
MS 솔루션인데 안나오면 섭섭하다! 기승전azure!
SQL Server 를 이용한 scale-out
http://www.asp.net/signalr/overview/performance/scaleout-with-sql-server
개인적으로는 이게 가장 마음에 든다.^^
개인적으로는 이게 가장 마음에 든다.^^
단순한 배포
IIS나 Azure WebRole 에 배포하는것도 간단하지만 강조하고 싶은 것은 self-host 방식이다.
signalr self-host 를 이용하면 별도의 서버용 미들웨어가 없어도 네트워크에 연결된 PC위에서 독립적으로 웹서버 기능을 포함하여 동작할 수 있다.
즉 서버가 될 PC에 서버용 exe와 dll들을 배포하고 실행시키기만 하면 바로 서버로서 동작한다.
self-host 기능을 이용하면서도 ASP.NET을 이용할때와 다음없이 클래스 라이브러리들을 거의 모두 사용할 수 있다.
IIS가 몇개의 dll안으로 들어가 버린 셈인데 이런 일이 가능하기 위해서 무대 뒤에서 OWIN 과 Katana라는 프로젝트가 있어 엄청난 양의 작업을 해 두고 있었다.
signalr self-host 를 이용하면 별도의 서버용 미들웨어가 없어도 네트워크에 연결된 PC위에서 독립적으로 웹서버 기능을 포함하여 동작할 수 있다.
즉 서버가 될 PC에 서버용 exe와 dll들을 배포하고 실행시키기만 하면 바로 서버로서 동작한다.
self-host 기능을 이용하면서도 ASP.NET을 이용할때와 다음없이 클래스 라이브러리들을 거의 모두 사용할 수 있다.
IIS가 몇개의 dll안으로 들어가 버린 셈인데 이런 일이 가능하기 위해서 무대 뒤에서 OWIN 과 Katana라는 프로젝트가 있어 엄청난 양의 작업을 해 두고 있었다.
OWIN, Katana
OWIN, Katana 이란?
영문 : http://www.asp.net/aspnet/overview/owin-and-katana/an-overview-of-project-katana
한글 : http://www.egocube.pe.kr/Translation/Content/owin-and-katana/201507130001
영문 : http://www.asp.net/aspnet/overview/owin-and-katana/an-overview-of-project-katana
한글 : http://www.egocube.pe.kr/Translation/Content/owin-and-katana/201507130001
OWIN, Katana 시작하기!
영문 : http://www.asp.net/aspnet/overview/owin-and-katana/getting-started-with-owin-and-katana
한글 : http://www.egocube.pe.kr/Translation/Content/owin-and-katana/201507200001
영문 : http://www.asp.net/aspnet/overview/owin-and-katana/getting-started-with-owin-and-katana
한글 : http://www.egocube.pe.kr/Translation/Content/owin-and-katana/201507200001
예제 소개
signalr 을 이용해서 서버, 클라이언트간 양방향 통신을 수행.
SSL 설정을 통한 https 가능.
특히 서버는 signalr self-host를 사용했기 때문에 IIS등 미들웨어 의존성이 없다.
제로베이스에서 예제를 구현하는게 목표이기 때문에 서버, 클라이언트 모두 C# 콘솔응용프로그램으로 구현했다.
SSL 설정을 통한 https 가능.
특히 서버는 signalr self-host를 사용했기 때문에 IIS등 미들웨어 의존성이 없다.
제로베이스에서 예제를 구현하는게 목표이기 때문에 서버, 클라이언트 모두 C# 콘솔응용프로그램으로 구현했다.
PS C:\project\160525_mysignalrtest\mysignalrserver\bin\Debug> pwd
Path
----
C:\project\160525_mysignalrtest\mysignalrserver\bin\Debug
PS C:\project\160525_mysignalrtest\mysignalrserver\bin\Debug> .\MySignalRServer.exe
Server running on url : http://*:8079
OnConnected
this.Context.ConnectionId : edf22f64-b2dd-47d4-96b1-9fde8b4e9963
this.Context.Request.Url : http://localhost:8079/signalr/start?clientProtocol=1.4&transport=webSockets&connectionData=[{"Name":"MyHub"}]&connecti
onToken=AQAAANCMnd8BFdERjHoAwE%2FCl%2BsBAAAAmKf%2BR3aXx02%2Ft490h0OgWAAAAAACAAAAAAAQZgAAAAEAACAAAAD6xtafs3W2BeWnXpHe%2BqihbzLPQZ7GRYHReV8KhzeW8wA
AAAAOgAAAAAIAACAAAAD6v6whjlw3f0llFtC7WGjHOkrE8bOp9kp%2FfatLOoC2RzAAAAAfjunva2P6%2F2Qleo8gKazx2UCpxq6gh%2BA9t6cTwAlZ9WumPbSLnLwI5%2Fdt0suQqhRAAAAA
Kph71QfORlGJaIXy1KiLa8aUxGjC6VwEGmHKJtNLO9VeNlVxa9GkxsQxOcOrKAS9EtvCFP4zW1ngvSEqyUTpxw%3D%3D
this.Context.Headers : [{"Key":"Host","Value":"localhost:8079"},{"Key":"User-Agent","Value":"SignalR.Client.NET45/2.2.0.0 (Microsoft Windows NT 6
.2.9200.0)"}]
Send name : william message :hello
Send name : william message :hello
StartTimer count : 10
타이머 시작됨...
타이머 카운트 0/10...
타이머 카운트 1/10...
타이머 카운트 2/10...
타이머 카운트 3/10...
타이머 카운트 4/10...
타이머 카운트 5/10...
타이머 카운트 6/10...
타이머 카운트 7/10...
타이머 카운트 8/10...
타이머 카운트 9/10...
타이머 종료됨...
PS C:\project\160525_mysignalrtest\mysignalrserver\bin\Debug>
<클라이언트>
PS C:\project\160525_mysignalrtest\MySignalRClient\bin\Debug> pwd
Path
----
C:\project\160525_mysignalrtest\MySignalRClient\bin\Debug
PS C:\project\160525_mysignalrtest\MySignalRClient\bin\Debug> .\MySignalRClient.exe
commands
------------------------------------
Q : quit
S : Send("william", "hello")
T : StartTimer(10)
s
RECV addMessage : william : hello
commands
------------------------------------
Q : quit
S : Send("william", "hello")
T : StartTimer(10)
s
RECV addMessage : william : hello
commands
------------------------------------
Q : quit
S : Send("william", "hello")
T : StartTimer(10)
t
RECV showMsg : 타이머 시작됨...
RECV showMsg : 타이머 카운트 0/10...
RECV showMsg : 타이머 카운트 1/10...
RECV showMsg : 타이머 카운트 2/10...
RECV showMsg : 타이머 카운트 3/10...
RECV showMsg : 타이머 카운트 4/10...
RECV showMsg : 타이머 카운트 5/10...
RECV showMsg : 타이머 카운트 6/10...
RECV showMsg : 타이머 카운트 7/10...
RECV showMsg : 타이머 카운트 8/10...
RECV showMsg : 타이머 카운트 9/10...
RECV showMsg : 타이머 종료됨...
commands
------------------------------------
Q : quit
S : Send("william", "hello")
T : StartTimer(10)
q
ended!!!
PS C:\project\160525_mysignalrtest\MySignalRClient\bin\Debug>
서버 프로젝트 생성
제로 베이스에서 시작하기 위해서 C# 콘솔 어플리케이션 스타일로 프로젝트를 시작한다.
당연히 signalr의 디펜던시들은 nuget으로 잘 관리 되고 있다.
아래 두가지 라이브러리를 설치 하면 되겠다.
당연히 signalr의 디펜던시들은 nuget으로 잘 관리 되고 있다.
아래 두가지 라이브러리를 설치 하면 되겠다.
Install-Package Microsoft.AspNet.SignalR.SelfHost
self-host 라이브러리이다.
실수로 추가했으면
Microsoft.AspNet.SignalR
는 IIS용 이므로 이를 추가하지 않도록 주의하자. 실수로 추가했으면
Uninstall-Package Microsoft.AspNet.SignalR
로 삭제되니깐 걱정말자.Install-Package Microsoft.Owin.Cors
닷넷 클라이언트를 만들예정이므로 필요없지만 웹클라이언트 인경우에 크로스도메인 문제를 해결하는데 필요한 라이브러리이다.
이 라이브러리를 셋팅해 두면 signalr 서버와 웹클라이언트가 호스팅되는 서버가 http스킴, 도메인, 포트번호가 달라도 된다.
이 라이브러리를 셋팅해 두면 signalr 서버와 웹클라이언트가 호스팅되는 서버가 http스킴, 도메인, 포트번호가 달라도 된다.
<App.config>
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
...
<system.net>
<defaultProxy>
<proxy bypassonlocal="false"
usesystemdefault="true" />
</defaultProxy>
</system.net>
</configuration>
디버깅시 fiddler를 사용할 수 있도록 하는 설정이다.
fiddler는 WININET의 경계에서 http세션을 후킹할때 http proxy기능을 사용하게 되는데 이 어플리케이션이 이를 허용한다는 의미가 된다.
fiddler는 WININET의 경계에서 http세션을 후킹할때 http proxy기능을 사용하게 되는데 이 어플리케이션이 이를 허용한다는 의미가 된다.
서버 구현
서버기능은 2가지를 구현할려고 한다.
- Send
- 수신받은 메시지를 그대로 에코응답
- StartTimer
- 수신받은 count만큼 매초마다 클라이언트를 호출해줌.
코드를 살펴보면,
Program.Main
: 단순히 base url으로 서버를 시작할뿐 별다른 기능은 없고,Startup
: signalr의 초기설정을 할 수 있는 구성이 되어 있지만, 여기서는 그냥 기본설정만 쓰기로 한다.MyHub
: 서버기능이 모두 구현이 되어 있다.
OnConnected, OnDisconnected, OnReconnected
: 클라이언트의 연결, 연결끊김, 재연결을 확인할 수 있다._PrintContext
' : 클라이언트 세션정보는 ConnectionId, url query string, http header에 포함되기 때문에 테스트 삼아 클라이언트의 연결상태 변경시마다 모든정보를 자세히 출력해 보았다.
class Program
{
static void Main(string[] args)
{
// server host url
var url = "";
url = "http://*:8079";
using (WebApp.Start<Startup>(url))
{
Console.WriteLine($"Server running on url : {url}");
Console.ReadLine();
}
}
}
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.UseCors(CorsOptions.AllowAll);
app.MapSignalR();
}
}
public class MyHub : Hub
{
// server side method #1 : Send
// echo name and message
public void Send(string name, string message)
{
Console.WriteLine($"Send name : {name} message :{message}");
Clients.All.addMessage(name, message);
}
// server side method #2 : StartTimer
// send msgs every seconds until count variable ...
public void StartTimer(int count)
{
Console.WriteLine($"StartTimer count : {count}");
Task.Run(async () =>
{
var msg = $"타이머 시작됨...";
Console.WriteLine(msg);
Clients.Caller.showMsg(msg);
for (int i = 0; i < count; i++)
{
await Task.Delay(1000);
msg = $"타이머 카운트 {i}/{count}...";
Console.WriteLine(msg);
Clients.Caller.showMsg(msg);
}
msg = $"타이머 종료됨...";
Console.WriteLine(msg);
Clients.Caller.showMsg(msg);
});
}
public override Task OnConnected()
{
Console.WriteLine("OnConnected");
_PrintContext();
return base.OnConnected();
}
public override Task OnDisconnected(bool stopCalled)
{
Console.WriteLine("OnDisconnected");
_PrintContext();
return base.OnDisconnected(stopCalled);
}
public override Task OnReconnected()
{
Console.WriteLine("OnReconnected");
_PrintContext();
return base.OnReconnected();
}
/// <summary>
/// print context object
/// we can know about additional information (like account, auth, ...) in this.Context.
/// </summary>
private void _PrintContext()
{
Console.WriteLine($"this.Context.ConnectionId : {this.Context.ConnectionId}");
Console.WriteLine($"this.Context.Request.Url : {this.Context.Request.Url}");
Console.WriteLine($"this.Context.Headers : {JsonConvert.SerializeObject(this.Context.Headers)}");
}
}
클라이언트 프로젝트 생성
클라이언트도 역시 제로베이스로 C# 콘솔어플리케이션으로 시작한다.
signalr 닷넷 클라이언트 라이브러리를 nuget으로 추가하도록 한다.
(역시
signalr 닷넷 클라이언트 라이브러리를 nuget으로 추가하도록 한다.
(역시
Microsoft.AspNet.SignalR
과 구분하도록 주의해야 한다.)Install-Package Microsoft.AspNet.SignalR.Client
클라이언트 구현
역시 코드가 아주 간단하다.
코드를 살펴보면…
코드를 살펴보면…
Main
hubConnection
: 서버측 base url을 이용하여 커넥션을 생성.var myHubProxy = hubConnection.CreateHubProxy("MyHub");
: 서버측 hub 클래스이름을 정확히 입력하여 프록시 객체를 만듬.myHubProxy.On<string, string>("addMessage", _OnAddMessage);
: "addMessage" 이름으로 클라이언트측 콜백함수를 정의한다. 여기서는<string, string>
으로 입력인자가 string 변수 2개로 정의했는데, 사용자 타입이 지정되면 json 문자열을 수신하여 객체로 직렬화까지 해준다.while (hubConnection.State != ConnectionState.Connected)
: 커넥션의 상태를 파악해서 연결끊김시 재시도 하는 것이 가능하다.hubConnection.Start().Wait();
: 연결을 실제로 수행하는 작업은 당연히 비동기가 된다. Start 함수가 비동기라서 비동기 대기인 await해도 되지만 Main 함수에 async를 붙이는 변형이 금지되어 있어서 여기서는 그냥 동기적으로 Wait 하도록 간단히 구현했다. 실무에서는 await으로 비동기 대기를 하는것을 권장한다.
_RunServerLoop
myHubProxy.Invoke("Send", new object[] { "william", "hello" });
: 서버측의 프록시 객체를 이용하여 Send 메소드를 문자열 객체 2개를 인자로 호출하는 코드이다.
class Program
{
private static string _embedCertHash;
static void Main(string[] args)
{
HubConnection hubConnection = null;
// init hub connection with url ...
hubConnection = new HubConnection("http://localhost:8079/");
var myHubProxy = hubConnection.CreateHubProxy("MyHub");
// attach event handler from server sent message.
myHubProxy.On<string, string>("addMessage", _OnAddMessage);
myHubProxy.On<string>("showMsg", _OnShowMsg);
// retry connection every 3 seconds ...
while (hubConnection.State != ConnectionState.Connected)
{
try
{
hubConnection.Start().Wait();
}
catch (Exception ex)
{
Console.WriteLine(@"
connection failed !
sleep 3 sec ...
try reconnect ...
");
Task.Delay(3000).Wait();
}
}
// run actions (Send, StartTimer)
_RunServerLoop(myHubProxy);
// exit program
Console.WriteLine("ended!!!");
Console.ReadLine();
}
private static void _OnShowMsg(string msg)
{
Console.WriteLine($"RECV showMsg : {msg}");
}
private static void _OnAddMessage(string name, string message)
{
Console.WriteLine($"RECV addMessage : {name} : {message}");
}
private static void _RunServerLoop(IHubProxy myHubProxy)
{
while (true)
{
Console.WriteLine(@"
commands
------------------------------------
Q : quit
S : Send(""william"", ""hello"")
T : StartTimer(10)
");
var cmd = Console.ReadKey();
if (cmd.Key == ConsoleKey.Q)
{
break;
}
else if (cmd.Key == ConsoleKey.S)
{
myHubProxy.Invoke("Send", new object[] { "william", "hello" });
}
else if (cmd.Key == ConsoleKey.T)
{
myHubProxy.Invoke("StartTimer", new object[] { 10 });
}
}
}
}
self-signed SSL 인증서 생성, 등록, 바인딩
이제까지 보안없는 환경에서의 일반적인 양방향통신 시나리오에 대해서 설명했다.
이후부터는 SSL을 설정하여 https를 사용하는 보안통신 시나리오에 대해 설명하겠다.
보안이 필요없는 시나리오라면 이부분은 스킵해도 된다.
이후부터는 SSL을 설정하여 https를 사용하는 보안통신 시나리오에 대해 설명하겠다.
보안이 필요없는 시나리오라면 이부분은 스킵해도 된다.
signalr 서버도 IIS와 마찬가지로 SSL 서버 인증서를 생성하고, 등록하고, 포트에 바인딩하는 방식은 기본적으로는 동일하다. 그렇기 때문에 IIS에 익숙하신 분들은 IIS GUI 관리자를 실행해서 같은 과정을 수행해도 된다. 하지만 GUI 도구가 이러한 전 과정을 정확히 이해하는데 방해가 되기도 하고, OS버전마다 GUI가 조금씩 달라 혼란스럽기 때문에 여기서는 powershell을 이용해서 전과정을 스크립트로 실행해 보도록 하겠다.
여담이지만 powershell을 사용하면 윈도우환경에서 개발시 얻는 이점이 상당하다. 아직도 cmd만을 이용하거나 복잡한 작업이라도 GUI툴에만 의존하고 있다면 powershell을 배워보는 것을 강추한다. powershell을 배우기 위해서는 이 블로그의 powershell 강좌를 활용 할 수 있다.
여담이지만 powershell을 사용하면 윈도우환경에서 개발시 얻는 이점이 상당하다. 아직도 cmd만을 이용하거나 복잡한 작업이라도 GUI툴에만 의존하고 있다면 powershell을 배워보는 것을 강추한다. powershell을 배우기 위해서는 이 블로그의 powershell 강좌를 활용 할 수 있다.
아래는 self-signed SSL 인증서 생성, 등록, 바인딩, 공개키파일 배포 를 하는 스크립트다.
스크립트에 단계별로 자세한 주석이 있기 때문에 따로 설명은 생략하도록 하겠다.
스크립트에 단계별로 자세한 주석이 있기 때문에 따로 설명은 생략하도록 하겠다.
<create-bind-ssl-certificate.ps1>
Write-Debug "configuration ..."
# Set the CNAME with the same name as the company name
$cname = "my company"
# HttpListener Host's AppID, this is fixed!
$httpHandlerAppID = "12345678-db90-4b66-8b01-88f7af2e36bf"
# Setting ssl port number you want to enter
$sslPort = 8080
pause
Write-Debug "delete certificate in machine ..."
ls cert:\ -Recurse | where { $_.Subject -like "*$cname*" } | rm
pause
Write-Debug "delete cer file ..."
rm *.cer
rm *.pvk
pause
Write-Debug "create new certificate ..."
makecert -n "CN=$cname" my.cer -sr localmachine -ss my
pause
Write-Debug "check new certificate ..."
ls cert:\ -Recurse | where { $_.Subject -like "*$cname*" }
pause
Write-Debug "get certificate hash ..."
$hash = (ls cert:\ -Recurse | where { $_.Subject -like "*$cname*" } | select -First 1).Thumbprint
$hash
pause
Write-Debug "delete ssl binding ..."
netsh http delete sslcert ipport=0.0.0.0:$sslPort
pause
Write-Debug "add new binding ..."
$cmd = "http add sslcert ipport=0.0.0.0:$sslPort certhash=$hash appid={$httpHandlerAppID}"
$cmd
$cmd | netsh
pause
Write-Debug "check new binding ..."
netsh http show sslcert ipport=0.0.0.0:$sslPort
pause
Write-Debug "copy cer file ..."
if (Test-Path ..\MySignalRClient)
{
cp *.cer ..\MySignalRClient
}
pause
이 스크립트의 실행결과 는 아래와 같다.
- 새롭게 SSL 인증서가 생성되었으며, cert:\localmachine\my 에 설치된것이 확인된다.
- 이 인증서는 BC7E643A64A411F87B60975186C00700C2379180 로 thumbprint값도 확인된다.
- 이 인증서가 8080포트에 바인딩 된것이 확인된다.
PS C:\project\160525_MySignalRTest\MySignalRServer> pwd
Path
----
C:\project\160525_MySignalRTest\MySignalRServer
PS C:\project\160525_MySignalRTest\MySignalRServer> .\create-bind-ssl-certificate.ps1
디버그: configuration ...
계속하려면 <Enter> 키를 누르십시오.:
디버그: delete certificate in machine ...
계속하려면 <Enter> 키를 누르십시오.:
디버그: delete cer file ...
계속하려면 <Enter> 키를 누르십시오.:
디버그: create new certificate ...
Succeeded
계속하려면 <Enter> 키를 누르십시오.:
디버그: check new certificate ...
디렉터리: Microsoft.PowerShell.Security\Certificate::LocalMachine\My
Thumbprint Subject
---------- -------
BC7E643A64A411F87B60975186C00700C2379180 CN=my company
계속하려면 <Enter> 키를 누르십시오.:
디버그: get certificate hash ...
BC7E643A64A411F87B60975186C00700C2379180
계속하려면 <Enter> 키를 누르십시오.:
디버그: delete ssl binding ...
SSL 인증서를 삭제했습니다.
계속하려면 <Enter> 키를 누르십시오.:
디버그: add new binding ...
http add sslcert ipport=0.0.0.0:8080 certhash=BC7E643A64A411F87B60975186C00700C2379180 appid={12345678-db90-4b66-8b01-88f7af2e36bf}
netsh>
SSL 인증서를 추가했습니다.
netsh>계속하려면 <Enter> 키를 누르십시오.:
디버그: check new binding ...
SSL 인증서 바인딩:
-------------------------
IP:포트 : 0.0.0.0:8080
인증서 해시 : bc7e643a64a411f87b60975186c00700c2379180
응용 프로그램 ID : {12345678-db90-4b66-8b01-88f7af2e36bf}
인증서 저장소 이름: (null)
클라이언트 인증서 해지 확인 : Enabled
캐시된 클라이언트 인증서만 사용하여 해지 확인 : Disabled
사용 확인 : Enabled
해지 유효 시간 : 0
URL 검색 제한 시간 : 0
Ctl 식별자 : (null)
Ctl 저장소 이름 : (null)
DS 매퍼 사용 : Disabled
클라이언트 인증서 협상 : Disabled
거부 연결 : Disabled
계속하려면 <Enter> 키를 누르십시오.:
디버그: copy cer file ...
계속하려면 <Enter> 키를 누르십시오.:
PS C:\project\160525_MySignalRTest\MySignalRServer>
class Program
{
static void Main(string[] args)
{
// server host url
var url = "";
url = "https://*:8080";
using (WebApp.Start<Startup>(url))
{
...
}
}
}
SSL 적용 클라이언트 구현
클라이언트도 기본적으로는 간단하지만 그래도 서버보다는 할일이 더 있다.
할일이 좀더 많은 이유는 현재 사용하는 인증서가 self-signed SSL 인증서이다보니 해당 인증서의 공개키를(=X509 cer 파일)을 클라이언트가 내장하고 있다가 연결을 생성하기 전에 등록해주고 연결이 수행된 이후에 이 인증서를 서로 비교하는 작업이 필요해서이다. self-signed SSL 인증서라도 기본 보안기능은 똑같기 때문에 이렇게 클라이언트에 공개키를 임베딩하는 시나리오만 생각해도 된다면 값비싼 SSL 신뢰기관 인증서를 사용하지 않아도 된다.
할일이 좀더 많은 이유는 현재 사용하는 인증서가 self-signed SSL 인증서이다보니 해당 인증서의 공개키를(=X509 cer 파일)을 클라이언트가 내장하고 있다가 연결을 생성하기 전에 등록해주고 연결이 수행된 이후에 이 인증서를 서로 비교하는 작업이 필요해서이다. self-signed SSL 인증서라도 기본 보안기능은 똑같기 때문에 이렇게 클라이언트에 공개키를 임베딩하는 시나리오만 생각해도 된다면 값비싼 SSL 신뢰기관 인증서를 사용하지 않아도 된다.
사설이 길었지만, 코드는 역시 짧다.
그래도 코드를 해설 하자면…
그래도 코드를 해설 하자면…
var myCerStream = Assembly.GetExecutingAssembly().GetManifestResourceStream("MySignalRClient.my.cer")
: 공개키 X509 cer 파일을 클라이언트 프로젝트에 embeding resource로 포함시켜 놓았는데, 이 파일을 스트림으로 로딩하는 구현이다. cer 파일이 배포디렉토리에 파일로 포함되도 상관은 없다._embedCertHash = embedCert.GetCertHashString();
: 로딩된 공개키 정보에서 썸네일를 추출해서 저장해 놓는다._ValidateCert
: 서버와 연결시도중 인증서 상호 유효성 검사를 하는 로직이 제공되서 악의적인 MIM 공격시도가 탐지되면 인증서가 변경된것으로 판단하여 서버접속이 끊기게 된다.
class Program
{
private static string _embedCertHash;
static void Main(string[] args)
{
HubConnection hubConnection = null;
// init hub connection with url ...
hubConnection = new HubConnection("https://localhost:8080/");
// init cert from cer resource
using (var myCerStream = Assembly.GetExecutingAssembly().GetManifestResourceStream("MySignalRClient.my.cer"))
using (var ms = new MemoryStream())
{
myCerStream.CopyTo(ms);
var buf = ms.ToArray();
var embedCert = new X509Certificate(buf);
_embedCertHash = embedCert.GetCertHashString();
hubConnection.AddClientCertificate(embedCert);
}
// validate certificate from MIM attack
ServicePointManager.ServerCertificateValidationCallback = _ValidateCert;
...
}
private static bool _ValidateCert(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
var result = _embedCertHash == certificate.GetCertHashString();
if (!result)
{
Console.WriteLine($@"
certificate is modified !
_certHashStr : {_embedCertHash}
certificate.GetCertHashString() : {certificate.GetCertHashString()}
");
}
return result;
}
}
Appendix - signalr이 노출하는 API 목록
signalr은 웹서버 파생기술이기 때문에 클라이언트도 javascript가 주요 타겟으로 개발이 되었다. javascript 클라이언트에서도 서버 프록시를 사용할 수 있는데 이때 구현코드에 앞서서 proxy 객체들이 초기화되어 준비 되어 있어야 한다. 이 때문에 signalr 서버가 띄워지면 이 javascript 프록시를 준비해 놓기 위해서 이내용이 구현된 javacript를 동적생성해서 클라이언트로 내려주게 되어 있다.
네이티브 클라이언트들이야 필요없을 수도 있지만, 예를 들어 서버측 특히 XXXHub 들이 노출하는 메소드를 구하지 못하는 즉 소스코드가 없는 상황이라면 서버를 호출할 수 있는 API 목록을 확인하는 용도로 활용할 수 있을것이다. 물론 더 좋은 API 문서 생성툴들이 있을테지만, 아무것도 준비 안된 상황이라면 개발시작지점 으로 좋을 것이다.
http://localhost:8079/signalr/hubs 에서 모든 등록된 hub API들이 노출된다.
PS C:\project\160525_MySignalRTest> $wc = New-Object System.Net.WebClient
>> $wc.DownloadString("http://localhost:8079/signalr/hubs")
/*!
* ASP.NET SignalR JavaScript Library v2.2.0
* http://signalr.net/
*
* Copyright Microsoft Open Technologies, Inc. All rights reserved.
* Licensed under the Apache 2.0
* https://github.com/SignalR/SignalR/blob/master/LICENSE.md
*
*/
/// <reference path="..\..\SignalR.Client.JS\Scripts\jquery-1.6.4.js" />
/// <reference path="jquery.signalR.js" />
(function ($, window, undefined) {
/// <param name="$" type="jQuery" />
"use strict";
if (typeof ($.signalR) !== "function") {
throw new Error("SignalR: SignalR is not loaded. Please ensure jquery.signalR-x.js is referenced before ~/signalr/js.");
}
var signalR = $.signalR;
function makeProxyCallback(hub, callback) {
return function () {
// Call the client hub method
callback.apply(hub, $.makeArray(arguments));
};
}
function registerHubProxies(instance, shouldSubscribe) {
var key, hub, memberKey, memberValue, subscriptionMethod;
for (key in instance) {
if (instance.hasOwnProperty(key)) {
hub = instance[key];
if (!(hub.hubName)) {
// Not a client hub
continue;
}
if (shouldSubscribe) {
// We want to subscribe to the hub events
subscriptionMethod = hub.on;
} else {
// We want to unsubscribe from the hub events
subscriptionMethod = hub.off;
}
// Loop through all members on the hub and find client hub functions to subscribe/unsubscribe
for (memberKey in hub.client) {
if (hub.client.hasOwnProperty(memberKey)) {
memberValue = hub.client[memberKey];
if (!$.isFunction(memberValue)) {
// Not a client hub function
continue;
}
subscriptionMethod.call(hub, memberKey, makeProxyCallback(hub, memberValue));
}
}
}
}
}
$.hubConnection.prototype.createHubProxies = function () {
var proxies = {};
this.starting(function () {
// Register the hub proxies as subscribed
// (instance, shouldSubscribe)
registerHubProxies(proxies, true);
this._registerSubscribedHubs();
}).disconnected(function () {
// Unsubscribe all hub proxies when we "disconnect". This is to ensure that we do not re-add functional call backs.
// (instance, shouldSubscribe)
registerHubProxies(proxies, false);
});
proxies['myHub'] = this.createHubProxy('myHub');
proxies['myHub'].client = { };
proxies['myHub'].server = {
send: function (name, message) {
return proxies['myHub'].invoke.apply(proxies['myHub'], $.merge(["Send"], $.makeArray(arguments)));
},
startTimer: function (count) {
return proxies['myHub'].invoke.apply(proxies['myHub'], $.merge(["StartTimer"], $.makeArray(arguments)));
}
};
return proxies;
};
signalR.hub = $.hubConnection("/signalr", { useDefaultPath: false });
$.extend(signalR, signalR.hub.createHubProxies());
}(window.jQuery, window));
PS C:\project\160525_MySignalRTest>
이 글은 Evernote에서 작성되었습니다. Evernote는 하나의 업무 공간입니다. Evernote를 다운로드하세요. |
댓글
댓글 쓰기