Windows Service Call SignalR Server 導致 Port 被佔滿 …

我們有一支 Windows Service 程式,會定期呼叫 SingalR Server(也可以在 SignalR Server 中使用 Timer 定期呼叫似乎更方便些) 。

最近忽然發現那台 Server 掛了,透過 netstat -a -o -n 去看,發現我們的 Windows Service 程式佔滿了所有的 Port 。

寫範例程式來測試一下,在 Button Click Event 中,模擬 Service 程式的 Timer 行為,果然發生一模一樣的狀況,如下,

var signalRServerPath = "http://localhost:62600/SignalR/signalR";
using (var hubConnection = new HubConnection(signalRServerPath, false))
{
var hubProxy = hubConnection.CreateHubProxy("informHub");
hubConnection.Start().Wait();
await hubProxy.Invoke("sendNoticeToAllUser");
hubConnection.Stop();
}

我們可以在 cmd 視窗中輸入 netstat -a -o -n | find “:62600″ ,來看連到 SingalR Server的佔了多少 Port。

singalRClientPorts

註:

1.nuget package 要加入 SingalR Client 哦!

2.invoke 要使用 async/await 哦! 我使用 .wait() ,呼叫第2個 method 時,會有 hang 住的狀況哦!

雖然 HubConnection 有呼叫 Stop 了,但 Port 還是會佔住。

所以改法就是將 HubConnection 及 IHubProxy 建立成全域變數,一開始就先建立好,在 Timer 的事件中,就直接透過 IHubProxy 物件去呼叫 SingalR Server 就可以了哦!

宣告全域變數

private HubConnection hubConnection = null;
private IHubProxy hubProxy = null;

 

程式啟動時,針對 HubConnection 及 IHubProxy 初始化,

var signalRServerPath = "http://localhost:62600/SignalR/signalR";
hubConnection = new HubConnection(signalRServerPath, false);
hubProxy = hubConnection.CreateHubProxy("informHub");
hubConnection.Start().Wait();

 

Button Click 時,只要直接透過 hubProxy 呼叫 SignalR Server

await hubProxy.Invoke("signalR_Server_Method1");
await hubProxy.Invoke("signalR_Server_Method2");

 

程式最後呼叫 HubConnection Stop Method

hubConnection.Stop();
hubConnection.Dispose();

 

參考資料

C# Client hangs when invoking a hub method and waiting on the invocation #2153

ASP.NET 驗證上傳檔案的實際檔案格式

Web 系統有時會需要使用者上傳檔案,我們通常會去卡檔案的附檔名,例如只限制圖檔可以上傳,但是如果將有害的 exe 改成了 jpg ,一樣可以上傳到 Server 上。

目前找到的方式大多是透過檔案的 signatures (俗稱的 magic numbers) 來判斷,詳細可以參考「List of file signatures」。

所以如果只是要判斷是否為圖檔的話,可參考「Validate uploaded image content in ASP.NET」,判斷檔案前幾個 Byte 來判斷它。

但是如果我們還要判斷其他檔案呢? 例如 PDF , 文字檔 等等的要怎麼辦呢?

這時可以參考「Validating MIME of a File Before Uploading in ASP.Net」這篇文章,使用 urlmon.dll 中的 FindMimeFromData 這個 Method,

FindMimeFromData 是依檔案前 256 bytes 來判斷檔案的 MIME Type,它所對應的 MIME Type 可以參考 MIME Type Detection in Windows Internet Explorer

所以我們建立一個 ASPX 網頁來做簡單的測試,

<asp:FileUpload ID="FileUpload1″ runat="server" />

<asp:Button ID="btnUload" runat="server" Text="Upload" <asp:Button ID="btnUload" runat="server" Text="Upload"  OnClick="btnUload_OnClick"/>

<asp:Label ID="lblMessage" runat="server" Text="mime type"></asp:Label>

ASPX.CS 中

1.加入 namespace
using System.Runtime.InteropServices;

2.在 Page Class 中加入使用 FindMimeFromData 的宣告
[DllImport("urlmon.dll", CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = false)]
static extern int FindMimeFromData(IntPtr pBC,
[MarshalAs(UnmanagedType.LPWStr)] string pwzUrl,
[MarshalAs(UnmanagedType.LPArray, ArraySubType=UnmanagedType.I1, SizeParamIndex=3)]
byte[] pBuffer,
int cbSize,
[MarshalAs(UnmanagedType.LPWStr)] string pwzMimeProposed,
int dwMimeFlags,
out IntPtr ppwzMimeOut,
int dwReserved);

3.在 Upload 的 Button 中呼叫 FindMimeFromData 來判斷上傳檔案的 MIME Type
if (FileUpload1.HasFile){
var byteSize = 255;

var bbyteSize = byteSize + 1;

var ambiguousMimeType = “application/octet-stream";

var file = FileUpload1.PostedFile;

var fileStreamLength = file.ContentLength;

var buffer = new byte[bbyteSize];

var fileStreamIsLessThanBByteSize = fileStreamLength < byteSize; file.InputStream.Read(buffer, 0, fileStreamIsLessThanBByteSize ? fileStreamLength : bbyteSize);

IntPtr mimeTypePtr;

FindMimeFromData(IntPtr.Zero, null, buffer, bbyteSize, null, 0, out mimeTypePtr, 0);

var mime = Marshal.PtrToStringUni(mimeTypePtr);

if (mime != null && mime.Equals(ambiguousMimeType) && fileStreamIsLessThanBByteSize && fileStreamLength > 0) {

//txt少於 256 會判斷成 octet-stream

var currentBuffer = buffer.Take(fileStreamLength);

var repeatCount =  (int)(bbyteSize / fileStreamLength) + 1;

var bBuferLit = new List<byte>();

while (repeatCount > 0) {

bBuferLit.AddRange(currentBuffer);

–repeatCount;

}

var bbuffer = bBuferLit.Take(bbyteSize).ToArray();

FindMimeFromData(IntPtr.Zero, null, bbuffer, bbyteSize, null, 0, out mimeTypePtr, 0); mime = Marshal.PtrToStringUni(mimeTypePtr);

}

Marshal.FreeCoTaskMem(mimeTypePtr); lblMessage.Text = mime;

}

請注意, FindMimeFromData 的宣告,請使用本文的宣告方式,不然在 x64 OS 中程式會 Crash 的哦!

詳細可以參考「Application pool crashes with URLMoniker urlmon.dll during MIME type checking」。

如果您覺得麻煩的話,也可以從 nuget 安裝 MimeTypeDetective 這個套件試試!

註: 因為文字檔的內容如果少於 256 個 bytes 會被判斷為 application/octet-stream ,所以程式中會判斷如果內容少於 256 而且第一次判斷出來是 octet-stream 時,會將內容補足到 256 bytes,這樣子判斷就會是正確的哦!

後來測試發現 Unicode 的文字檔,它 check 也會有問題。

所以直接用以下 github 的 MimeTypeDetection.cs 就可以了哦!

https://github.com/icsharpcode/SharpDevelop/blob/master/src/Main/Base/Project/Util/MimeTypeDetection.cs

 

參考資料

List of file signatures

Validate uploaded image content in ASP.NET

Validating MIME of a File Before Uploading in ASP.Net

FindMimeFromData

Application pool crashes with URLMoniker urlmon.dll during MIME type checking

Using .NET, how can you find the mime type of a file based on the file signature not the extension

將 SQL 中的 數值 轉成 時間 字串

今天看到一些將數值轉成時間字串的文章「SQL SERVER – Convert Decimal to Time Format in String」,覺得蠻有趣的,所以就改用其他的方式來轉換。

作者是使用數值相加後再 Replace , 我們也可以使用 SQL 2012 提供的 Format Function 來處理。

一開始覺得為什麼會有這種需求呢? 或許在當下因為某些原因,儲存時間時,使用數值來記錄,例如 5.07 就表示 05:07。

測試的SQL如下,

DECLARE @value DECIMAL(4,2) = 5.10;
SELECT @value AS OriginalValue,
REPLACE(FORMAT(@value, '0#.00'), '.', ':') AS NewValue,
REPLACE(FORMAT(IIF(@value>=12, @value-12, @value), '0#.00'), '.', ':') + IIF(@value>=12, ' PM', ' AM') AS NewAPMValue;

SET @value = 17.03
SELECT @value AS OriginalValue,
REPLACE(FORMAT(@value, '0#.00'), '.', ':') AS NewValue,
REPLACE(FORMAT(IIF(@value>=12, @value-12, @value), '0#.00'), '.', ':') + IIF(@value>=12, ' PM', ' AM') AS NewAPMValue;

結果如下,

sqlDecimal2Time

先將數值 Format 成我們要的格式後,再將 . 換成 : 就可以了。

如果需要加上 AM/PM 的話,也可以用 IIF 來判斷哦!

Client_Cross_Frame_Scripting_Attack

有時白箱工具會掃出 Client Cross Frame Scripting Attack ,

可以在 Header 中加入設定 X-FRAME-OPTIONS

但是這樣有些 白箱工具並不知道,

客戶還是會要你改到 Report 看不到,

那怎麼辦呢?

 

這時候一般會在js中加入

if (top != self) {top.location = self.location;} 

但這樣子,又會引發另一個 issue,

Client DOM XSRF 問題,

所以解法可以改成

if (top.frames.length != 0) alert(‘error’)

或是加上 encode 去避掉 XSRF 的問題,如下,

if (top != self) top.location=encodeURI(self.location)

 

感謝同事 Fenny & 江佩珊 的分享 ^_^

IIS ERR_CONNECTION_RESET

同事反應更版程式並重新啟動機器後,要測試網頁時,居然出現「ERR_CONNECTION_RESET」這個錯誤,如下圖,

ERR_CONNECTION_RESET

連 localhost 的預設網頁是出的來的,但一 RUN 到我們的系統就出現 ERR_CONNECTION_RESET 的錯誤。在 IIS 上重新再建立一個新的應用程式,並放一支空的 aspx 來執行,卻是可以的。 把目錄指到我們系統的目錄去執行,又是出現 ERR_CONNECTION_RESET 。 因為程式是 asp.net mvc 寫的,所以是 IIS 無法執行 mvc 嗎?

在沒有任何頭緒時,開啟「事件顯示器」來看一下是否有什麼蛛絲馬跡,結果看到一堆的 15021 Event, 「使用端點 0.0.0.0:443 的 SSL 設定時發生錯誤。傳回資料包含了錯誤狀態碼。」,如下圖,

sslevent

再認真看一下網頁,原本我是打 http 的,結果它出錯時,URL上卻變成了 https 。立馬來檢查一下 IIS 繫結部份,結果 https 443 居然是「未選取」,如下圖,

sslemp

將它設定成對的憑證,再執行系統,就可以正常執行了。

後來另一台重開機後,居然也出現 SSL 憑證變成了「未選取」的狀況,需要再設定一次。

 

 

 

 

express deprecated sendfile: Use sendFile instead

在使用 express 的 Response.sendfile 時,會出現以下的警語,

express deprecated res.sendfile: Use res.sendFile instead

程式如下,

app.get('/', (req, res)=>{
res.sendfile('socketio.html');
});

程式可以正常運作,只是會出現過時的警語而已。

那如果將它改成 sendFile 這個 Method 呢? 就會出現以下的錯誤,

TypeError: path must be absolute or specify root to res.sendFile

也就是我們要給它絕對的路徑或是要指定檔案 root 的目錄。

這時就可以透過 __dirname 來組出 fullpath,如下,

app.get('/', (req, res)=>{
res.sendFile(path.join(__dirname, 'socketio.html'));
});

或是

app.get('/', (req, res)=>{
res.sendFile('socketio.html',{root: __dirname } )
});

註:如果檔案不目前目錄,就使用 path.join 去串接吧!

使用 HttpClient PostAsync 資料時,要記得 Encode 哦!

最近將 Chatbot 透過 Webchat 的方式給公司同仁使用,在使用之前都需要透過登入可以使用請假或是訂會議室的功能。

其中有一位同事就是登入不進去,但是其他人卻沒有這個問題。於是看錯誤訊息,的確是密碼驗證不過。再詢問他的密碼是否有特別的字元,他回答其中有 “&" 這個字。

再看一下同事 Alice 寫的程式中,透過 HttpClient 去 Post 資料時,使用的是 StringContent ,將各別的欄位透過 & 去串接,內容如下,


var userInfo = "grant_type=password&username=" + usrname + "&password=" + pwd;
var postContent = new StringContent(userInfo);
var response = client.PostAsync(tokenUrl, postContent).Result;

謎題解開了,因為密碼中有 & ,而程式碼又沒加以 Encode ,所以就造成了密碼錯誤的問題。

解法就是 Encode ,

1.使用 UrlEncode 去包每個參數,如下,

var userInfo = "grant_type=password&username=" + HttpUtility.UrlEncode(usrname) + "&password=" + HttpUtility.UrlEncode(pwd);
var postContent = new StringContent(userInfo);
var response = client.PostAsync(tokenUrl, postContent).Result;

2.使用 FormUrlEncodedContent ,這樣裡面的值,它會幫我們 Encode ,如下,

var formContent = new FormUrlEncodedContent(new[]
{
new KeyValuePair("grant_type", "password"),
new KeyValuePair("username", usrname),
new KeyValuePair("password", pwd)
});
var response = client.PostAsync(tokenUrl, formContent).Result;

註: PostAsync , ReadAsStringAsync 您也可以使用 async/await 方式哦!

遠端伺服器不存在或無法使用

我們有一個VB6的系統,
Client端使用 COM+ 的 Application Proxy 方式,
有些Client端執行系統時遇到 CreateObject ,就會發生以下的錯誤,

沒有使用權限’CreateObject’
800A0046

COM+ 的 Application Proxy方式是

A電腦(Client) 建立的物件,是在 B電腦(Server) 生成。

做法是在 元件服務中 將某個 應用程式 匯出成 msi 檔,

然後 Client 再安裝這個 msi,就會像以下這個樣子。

所以在 Client 呼叫 CreateObject ,instance 就會建立在 Server 端,如下,

那為何會出現 沒有使用權限’CreateObject’ 呢?

因為 Client 端的登入者,沒有權限去呼叫 Server 端將物件建立起來。

所以您可以試的方式是,將 Client 端的登入者加入 Server 電腦的群組之中。

另外也可以調整那個套件的「呼叫的驗證等級(L)」設定成「無」,來試看看是否為驗證問題哦! 如下,

但最近客戶的網路不知做了什麼設定,導致程式發生了「遠端伺服器不存在或無法使用」的錯誤。

防火牆也沒有開, RPC tcp 135 port 也是通的…

後來將 Client 端的 COM+ 那 啟用的 Tab 中,將 遠端伺服器 欄位值,

從「電腦名稱」改成「ip」,運作就正常的… 天呀!  這…

結論,如果有 伺服器不存在 的問題,但確定它是存在的,或許可以改用 IP 試看看哦!

 

 

Hosting node.js express app in IIS

當「Local DirectLine」寫好了之後要部署到IIS上面要如何進行呢?

1.安裝 Node.js

首先當然要在 Windows Server 上安裝所需要的 Node.JS 的版本,目前我們使用 v6.11.2 LTS 版本。

2.安裝 iisnode

請到 Azure/iisnode 下載 IIS 版本對應的 iisnode 並安裝起來。

iisnodesetup

3.安裝 IIS URL Rewrite Module

請到 IIS URL Rewrite 下載,在最下面會有各語系及x86/x64 可供安裝。安裝完成後,在 IIS 中就會多了 URL Rewrite ,如下圖,

urlrewrite

4.建立 IIS 應用程式,並新增 iisnode 處理常式對應

在建立的應用程式的「處理常式對應」中,新增模組對應(我的入口程式是 index.js ,您也可以用 *.js 或是您的 js 檔),如下,

iishandler

5.設定 URL 對應

我們需要將本應用程式目錄中的程式 Request 轉給我們程式 index.js 來執行,所以要設定 URL Rewrite,web.config 設定如下,

<rewrite> <rewrite> <rules> <rule name="all"> <match url="/*" /> <action type="Rewrite" url="index.js" /> </rule> </rules></rewrite>

6.web.config中設定應用程式目錄(假設我們建立的目錄是 LDL) ,web.config 設定如下,

<appSettings><appSettings> <add key="virtualDirPath" value="/LDL" /></appSettings>

註.1:測試的 index.js 內容如下,一般來說 app.get 都會直接用 / 開始,但是在 iis 中我們需要加入 virtualDirPath 哦!
var virtualDirPath = process.env.virtualDirPath || '';
var app = require('express')();
app.get(virtualDirPath + '/', (req, res) => {
res.send(virtualDirPath: ${virtualDirPath});
});
app.get(virtualDirPath + '/directline', (req, res) => {
res.send(virtualDirPath: ${virtualDirPath}/directline);
});
app.listen(process.env.PORT);

透過 Browser 開啟 http://localhost/ldl 就可以發現會正常的顯示出目錄的名稱就表示 OK 了。

註.2:如果您跟筆者使用 nvm 來管理 Node.JS 的版本,則要在 nvm 的目錄設定權限給IIS的執行者哦,如下圖,

nvmSecurity

註.3:在 iisnode 中執行的一些問題參考

3.1. process.env.PORT 的內容會類似是 .\pipe\88731ae5-10c4-42dd-8685-91ecf6f13861 ,所以它不是數值哦!
3.2. 有時 console.log 會造成它 GG 哦,如下圖 ! 如何更快速的找到錯誤的方式,筆者有找到時,再跟大家分享哦! iisnodeGG

所以 Local DirectLine 設定到 IIS 上運作是正常的哦! 如下圖,

ldlIIS

整個的 web.config,及最新的 index.js ,請參考 Local DirectLine 內容哦!

參考資料

Hosting Node.js projects in IIS virtual directories

Local DirectLine

在Window IIS中安装运行node.js应用—你疯了吗

 

2017/09/05 更新

在註 3.2 關於使用 console.log 時,會發生錯誤的問題終於找到解法了 ^_^  。

原本以為不能這樣子用,一度懷疑是 node.js 的版本。

後來用 webchat 去連它取得一個 conversation 號碼時,居然也 GG 了,出現以下的錯誤訊息

Error: EPERM: operation not permitted, open ‘C:\Program Files\Galaxy Software Services\LocalDirectline\iisnode\myservername-4220-stdout-1504572946040.txt'<br> &nbsp; &nbsp;at Error (native)<br> &nbsp; &nbsp;at Object.fs.openSync (fs.js:641:18)<br> &nbsp; &nbsp;at Object.fs.writeFileSync (fs.js:1347:33)<br> &nbsp; &nbsp;at SyncWriteStream.stream.write.stream.end (C:\Program Files\iisnode\interceptor.js:180:20)<br> &nbsp; &nbsp;at Console.log (console.js:43:16)<br> &nbsp; &nbsp;at app.post (C:\Program Files\Galaxy Software Services\LocalDirectline\dist\bridge.js:47:17)<br> &nbsp; &nbsp;at Layer.handle [as handle_request] (C:\Program Files\Galaxy Software Services\LocalDirectline\node_modules\express\lib\router\layer.js:95:5)<br> &nbsp; &nbsp;at next (C:\Program Files\Galaxy Software Services\LocalDirectline\node_modules\express\lib\router\route.js:137:13)<br> &nbsp; &nbsp;at Route.dispatch (C:\Program Files\Galaxy Software Services\LocalDirectline\node_modules\express\lib\router\route.js:112:3)<br> &nbsp; &nbsp;at Layer.handle [as handle_request] (C:\Program Files\Galaxy Software Services\LocalDirectline\node_modules\express\lib\router\layer.js:95:5)

去那個目錄看,居然沒有那個檔案。

原來我們的 console.log 會寫到我們的應用程式目錄中 iisnode 的目錄之中,而我們 application pool 設定的帳號沒有權限去寫 log 檔,所以就 GG 了。

所以您在測試是不是權限問題,您可以先將 application pool 的帳號改成 LocalSystem 試看看是不是可以 work 哦!

因為我的 application pool 的帳號是使用 network service ,所以我們需要在 iisnode 目錄設定它允許「修改、讀取和執行 …」如下圖,

iisnodeSecurity

以上的說明希望對大家使用 node.js 在 iis 上運行有所幫助。

使用 Microsoft Bot Framework 地端的 WebChat 機器人(企業內)

網路上有一堆在介紹如何透過 Microsoft Bot Framework 來建立訊息機器人。

它有提供 .NETNode.jsBotBuilder SDK 讓我們可以快速地建置。

前題是我們一定要在 Bot Framework developer portal 中註冊一個 Bot。

而連接所有的 IM 則是透過 Microsoft Bot Connector 這個服務幫我們轉接。

在有些環境不允許使用外面的服務時,是不是就無法使用 BotBuilder 來開發訊息機器人了呢?

註:

因為目前的版本是 Preview ,或是正式 Release 後就有 Support 企業內部的 Bot Connector 也不一定。

不知大家有沒有發現,我們在自已的開發環境中,使用 VS.NET 連接 BotFramework-Emulator 來測試,是不需要在 Bot Framework developer portal 先註冊過。

疑 …

沒錯,如果您是想要享受 BotBuilder SDK 所帶來的好處,而 訊息機器人 只想要在企業內部的話,就可以用這樣子的架構。

下圖是在 Local 開發,Bot 與 Emulator 訊息傳送的過程,

b1

其實 Emulator 有包含一個 DirectLine 及 Webchat , Emulator = Webchat + Local DirectLine 。

ryanvolum/offline_dlsebnema/offline_dl 有實作 Local DirectLine。

當您使用 sebnema/offline_dl 來測試時,會發現它可以做單人的測試,但如果開另一個 Browser 時,會發現 Bot 的訊息都會回到最新連線的那個 WebChat 上。

b2

原因是因為它目前只用一個公用的 ConversationId,而 history 是一個 Array, 所以筆者將原本的 history 改成 history: {[key: string]: IActivity[]} ,這樣就可以 Support 多個使用者,因為每個使用者依建立的 ConversationId 去存放它們的 Activity。

另外還有一個問題是,我們並不知道目前這個 Conversation 是對應到那一個 Bot ,所以目前我是取得Header 中的 Authorization 欄位中的值 (透過 WebChat 的 s 參數設定),這樣可以 Support 不同的 Bot了。

那有時,Bot裡面會寫建立一個新的 Conversation 並將訊息傳送給它,所以我們也需要記錄使用者所使用的 ConversationId (userConversations:{[key:string]: IUserConversation[]}) 這樣當使用者要跟 Bot 建立新的對話時,就給它最近一個 ConversationId 就可以了。

b3

b4

相關的程式及說明在 rainmakerho/offline_dl ,也有將套件 offline-directline-gss 放到 npm 之中。

如果您是想讓目前 Web 應用程式增加 Bot Enabling (WebChat),可以考慮用這樣子的架構哦!

b5

詳細可以參考 https://github.com/rainmakerho/offline_dl

啟動 Local DirectLine

const url = require(“url");
const directline = require(“./dist/bridge");
const express = require(“express");
const app = express();

const config = {
localDirectLine: {
hostUrl: “http://localhost",
port: “3000″
},
apbots:[
{
botId:"mybot",
botUrl:"http://localhost:3979/api/messages",
“msaAppId": “",
“msaPassword": “"
},
{
botId:"mybot2″,
botUrl:"http://localhost:3978/api/messages",
“msaAppId": “",
“msaPassword": “"
},
]
};

directline.initializeRoutes(app, config);

再啟動 Local WebChat

然後在 browser 中輸入

http://localhost:8000/samples/fullwindow/?domain=http://localhost:3000/directline

就可以了哦!