Open-Source-Software-Entwicklung und Downloads

Browse Subversion Repository

Annotation of /trunk/doc/jp/html/reference/sourcecode.html

Parent Directory Parent Directory | Revision Log Revision Log


Revision 3227 - (hide annotations) (download) (as text)
Tue Mar 24 15:10:33 2009 UTC (15 years, 2 months ago) by maya
File MIME type: text/html
File size: 55577 byte(s)
CVS から SVN へ移行: 改行コードを LF から CR+LF へ変換
1 maya 3227 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
2     "http://www.w3.org/TR/html4/strict.dtd">
3     <HTML>
4     <HEAD>
5     <meta http-equiv="Content-Type" content="text/html; charset=Shift_JIS">
6     <TITLE>Tera Termソースコード解説</TITLE>
7     <META http-equiv="Content-Style-Type" content="text/css">
8     <link rel="stylesheet" href="../style.css" type="text/css">
9     </HEAD>
10     <BODY>
11    
12     <h1 class="center">Tera Termソースコード解説</h1>
13    
14     <hr width=80% align=center>
15    
16     <ol>
17     <li><a href="#foreword">はじめに</a></li>
18     <li><a href="#skillset">必要スキル</a></li>
19     <li><a href="#module">モジュール構成</a></li>
20     <li><a href="#library">ライブラリ構成</a></li>
21     <li><a href="#plugin">プラグインサポート</a></li>
22     <li><a href="#configuration">設定ファイルの読み書き</a></li>
23     <li><a href="#secure">セキュアプログラミング</a></li>
24     <li><a href="#compatibility">古いバージョンのWindowsとの互換性維持</a></li>
25     <li><a href="#debug">デバッグ手法</a></li>
26     <li><a href="#thread">マルチスレッド</a></li>
27     <li><a href="#dde">DDEによるプロセス間通信</a></li>
28     <li><a href="#ttssh">TTSSHによるSSHの設計と実装</a></li>
29     <li><a href="#macro">マクロ言語の設計と実装</a></li>
30     <li><a href="#caret">キャレット制御</a></li>
31     </ol>
32    
33     <hr width=80% align=center>
34    
35     <h2><a name="foreword">はじめに</a></h2>
36      本文書では、Tera Termのソースコードについて解説をします。解説対象とするソースコードはバージョン"4.58"(2008年2月現在)のものをベースとしています。
37     <hr>
38    
39    
40     <h2><a name="skillset">必要スキル</a></h2>
41      Tera Termのパッケージに含まれるほとんどのプログラムは、C言語で記述されています。一部のコードはC++言語で、MFC(Microsoft Foundation Class)が利用されています。Windows特有の処理を行うために、Win32 APIが多用されているため、APIの知識が必要となってきます。<br>
42      ソースコードをビルドするためには、Microsoft Visual Studio 2005 Standard Edition以上が必要です。Express EditionではMFCが利用できないため、ビルドができません。また、C++BuilderやTurbo C++ Explorer、gccなどのコンパイラにおいても、ビルドすることはできません。<br>
43      Windowsプログラミングに関する情報の源は、Microsoftが提供する「MSDNライブラリ」にあります。開発を行う際は、MSDNライブラリを頻繁に参照することになります。<br>
44      
45     <ul>
46     <li><a href="http://msdn2.microsoft.com/en-us/library/default.aspx">MSDNライブラリ</a></li>
47     <li><a href="http://msdn2.microsoft.com/ja-jp/library/default.aspx">MSDNライブラリ(日本語版)</a></li>
48     </ul>
49    
50     <p>
51      ただし、CygTermのみはCygwinのgccでコンパイルをします。ゆえに、CygTermはgccの機能を使った実装になっています。言語はC++です。
52     </p>
53    
54      Tera TermのメインエンジンはC++で実装されていますが、C言語的なコーディングがなされているため、Tera Termのソースコードを読み解くには、C言語に関する基礎知識があれば問題ないと言えます。ただし、Microsoft Visual C++(VC++)はANSI C準拠(C89)とはいえ、C99には未対応であるために、本来のC99相当の機能が独自に拡張されている部分もあります。そうした独自拡張された関数には、頭文字にアンダースコア(_)が付いているために、区別が付けやすくなっています。たとえば、VC++の_snprintf()は、ANSI C(C99)のsnprintf()とは似て非なるものです。<br>
55    
56     <hr>
57    
58    
59     <h2><a name="module">モジュール構成</a></h2>
60      Tera Termパッケージに含まれる実行モジュール(.exeと.dll)の関連図を以下に示します。実行ファイルの拡張子は".exe"になっており、必要に応じてDLLが動的リンクされます。いずれも32ビットプログラム(x86)であるために、x86-64やIA-64といった64ビット環境ではそのまま動作するかどうかは評価されていません。
61      
62     <div align="center">
63     <img src="image/module_relation.png" width=720 height=540>
64     </div>
65    
66      通常、ユーザがデスクトップやスタートメニューからアプリケーションを起動するときに、呼び出される実行ファイルは"ttermpro.exe"になります。実行ファイルはさらに5つのDLLとダイナミックリンクしています。静的リンクを行い、単一のEXEファイルにしていないのは、1つのプロセスのメモリ占有率を抑えるためです。Tera Termでは多数の起動が行われることが想定されるため、初期設計段階からDLLに分割されています。一度読み込まれたDLLは、複数のプロセス間で共有することができます。<br>
67      <br>
68      
69      マクロスクリプトを実行する際は、"ttpmacro.exe"というまったく別のプロセスが呼び出されます。"ttermpro.exe"とプロセス単位で分けられているのは、マクロを単体で実行できるようにするためです。両プロセス間で、データのやりとりを行うためには、プロセス間通信が必要です。Tera Termでは、DDE(Dynamic Data Exchange)と呼ばれる現在ではレガシーとなってしまったしくみが採用されています。将来のWindowsではDDEがサポートされなくなる可能性があり、その場合Tera Term上でマクロを実行することは一切できなくなります。<br>
70      <br>
71      
72      TTSSHやTTProxy、TTXKanjiMenuといったプラグイン形式のDLLは、Tera Termの起動時に明示的に LoadLibrary() を使ってダイナミックロードされます。ロード対象となるDLLのファイル名は、TTXInit()#ttplug.c において、"TTX*.DLL"というパターンにマッチしたものとなります。<br>
73      <br>
74      
75      "keycode.exe"と"ttpmenu.exe"、"LogMeTT.exe"は単体アプリケーションです。<br>
76      <br>
77      
78      Cygwin接続のしくみについては、別の節で説明します。
79    
80     <hr>
81    
82    
83     <h2><a name="library">ライブラリ構成</a></h2>
84      高度な機能を実現するために、フルスクラッチで実装することは効率がいいとは言えません。Tera Termでは開発効率化を図るために、オープンソースのライブラリを積極的に利用しています。ただし、オープンソース製品のライセンスによる競合には注意を払う必要があります(特にGPL)。<br>
85      下図に、オープンソースのライブラリをリンクしているモジュールと、そのリンク状況を示します。Tera Termマクロプログラムにおいて、"waitregex"や"sprintf"コマンドにおいて正規表現を利用するために、Onigurumaと呼ばれる正規表現ライブラリをリンクしています。Tera Term本体では、バージョンダイアログにOnigurumaのバージョンを表示するためだけにリンクをしています。
86      
87     <p>
88      SSHモジュールである"TTSSH"は、暗号処理を行うためにOpenSSLを利用しています。"OpenSSL"というネーミングからWebアクセスに使われるSSL(Secure Socket Layer)プロトコル専用のライブラリかと思われがちですが、基本的な暗号アルゴリズムをサポートしていることから、TTSSHではOpenSSLに含まれる低レイヤのルーチンを利用するだけに留まっています。このことは、すなわちOpenSSLライブラリにセキュリティホールが発見されたとしても、TTSSHへの影響は極めて低いということです。<br>
89      zlibライブラリは、SSHパケットの圧縮を行うために利用しています。ただし、ダイヤルアップ回線などの低速度なネットワークにおいては、パケット圧縮は有効ですが、昨今の高速回線ではむしろ速度低下を招く足かせとなります。ゆえに、デフォルトではパケット圧縮機能は無効化されています。
90      PuTTYは世界標準であるフリーのターミナルエミュレータです。PuTTYに含まれるPageantと呼ばれるSSH認証エージェントがあるのですが、TTSSHでPageantによる公開鍵認証をサポートするために、PuTTYのソースコードを利用しています。
91     </p>
92      
93      なお、いずれのライブラリも静的リンク(static link)としています。ライブラリのコンパイルオプションには"/MT"を付加しています。動的リンク(dynamic link)を行うと、一部のユーザ環境でTera Termが起動できないという現象が発生したために、現在では動的リンクは行っていません。
94      
95    
96     <div align="center">
97     <img src="image/library_relation.png" width=720 height=540>
98     </div>
99    
100     <hr>
101    
102    
103     <h2><a name="plugin">プラグインサポート</a></h2>
104      Tera Termでは、DLLという形式でプラグインのしくみをサポートしています。プラグイン形式のDLLファイルを、Tera Termがインストールされているディレクトリへ設置するだけで、Tera Termのソースコードを修正することなく、機能追加を行うことができます。代表的なプラグインとして、TTSSHがあります。<br>
105      プラグインを作成するためのサンプルコードとして、TTXSamples\ttxtest\ttxtest.c というソースファイルが用意されています。プラグインを開発するときは、このソースファイルをひな形とするとよいでしょう。実践的なプラグインとして、"TTX KanjiMenu"のソースコード(TTXKanjiMenu\配下)がシンプルで分かりやすいです。<br><br>
106    
107      プラグインは、Tera Term("ttermpro.exe")の起動時に読み込まれます。TTXInit()#ttplug.c が読み込みを行う関数で、カレントディレクトリから"TTX*.DLL"というワイルドカードに合致するDLLファイルが読み込み対象となります。<br>
108      複数のDLLが存在する場合は、Tera Term本体からチェインするような形で、各DLLのエクスポート関数が連結されます。連結される順番は、それぞれのDLLが定義するオーダー値(TTXExports構造体のloadOrderメンバ)で決定され、現状下記の通りとなっています。 
109     <p>
110     <table border=1 align=center>
111     <tr>
112     <th>モジュール</th>
113     <th>オーダー</th>
114     </tr>
115    
116     <tr>
117     <td>TTProxy</td>
118     <td>0</td>
119     </tr>
120    
121     <tr>
122     <td>TTSSH</td>
123     <td>2500</td>
124     </tr>
125    
126     <tr>
127     <td>TTX Kanji Menu</td>
128     <td>5000</td>
129     </tr>
130     </table>
131     </p>
132    
133     オーダー値が小さいほど、Tera Term本体側に近くなります。たとえば、Tera Term本体からTTXModifyMenu()が呼び出された場合、
134      
135      <ul>
136       <li>TTXModifyMenu()#ttplug.c → TTProxyのTTXModifyMenu() → TTSSHのTTXModifyMenu() → TTX Kanji MenuのTTXModifyMenu()</li>
137      </ul><br>
138      
139     という順番で、各DLLの関数が呼び出されていくことになります。
140      <br>
141    
142      各DLLが、Tera Term本体側から呼び出してもらうためにエクスポートする関数群は、TTXExports構造体で定義し、TTXBind()で渡します。たとえば、TTX Kanji Menuのエクスポート関数は以下のとおりです。不要な関数は NULL で定義してあります。
143    
144     <pre class=code>
145     static TTXExports Exports = {
146     /* This must contain the size of the structure. See below for its usage. */
147     sizeof(TTXExports),
148    
149     /* This is the load order number of this DLL. */
150     ORDER,
151    
152     /* Now we just list the functions that we've implemented. */
153     TTXInit,
154     NULL, /* TTXGetUIHooks */
155     NULL, /* TTXGetSetupHooks */
156     NULL, /* TTXOpenTCP */
157     NULL, /* TTXCloseTCP */
158     NULL, /* TTXSetWinSize */
159     TTXModifyMenu,
160     TTXModifyPopupMenu,
161     TTXProcessCommand,
162     NULL, /* TTXEnd */
163     NULL /* TTXSetCommandLine */
164     };
165     </pre>
166    
167      原則、プラグインのエクスポート関数は、他のプラグインと干渉しないように設計をするべきです。また、Tera Term本体側からの呼び出しが、自分宛てであるかどうかを判断する必要がある場合もあります。<br>
168      プラグインがエクスポートする関数について、以下に示します。
169      
170     <p>
171     <table border=1 align=center>
172     <tr>
173     <th>関数</th>
174     <th>意味</th>
175     </tr>
176    
177     <tr>
178     <td>TTXBind</td>
179     <td>一番始めに呼び出される関数であり、エクスポート関数のテーブルを渡す。</td>
180     </tr>
181    
182     <tr>
183     <td>TTXInit</td>
184     <td>TTXBind()の呼び出し後にすぐに実行される関数で、Tera Term本体のグローバル変数(ts, cv)を受け取り、プラグインの初期化を行う。</td>
185     </tr>
186    
187     <tr>
188     <td>TTXGetUIHooks</td>
189     <td>ダイアログのハンドルをフックするための関数。Tera Term本体のダイアログインターフェイスを変更したい場合に使う。フック対象の関数は以下のとおり。<br>
190     &SetupTerminal, &SetupWin, &SetupKeyboard, &SetupSerialPort,
191     &SetupTCPIP, &GetHostName, &ChangeDirectory, &AboutDialog,
192     &ChooseFontDlg, &SetupGeneral, &WindowWindow
193     </td>
194     </tr>
195    
196     <tr>
197     <td>TTXGetSetupHooks</td>
198     <td>セットアップルーチンをフックするための関数。フックした側は、元の関数も責任を持って呼び出す必要がある。複数のプラグインが存在する場合、関数がチェインされていく。フック対象の関数は以下のとおり。<br>
199     &ReadIniFile, &WriteIniFile, &ReadKeyboardCnf, &CopyHostList,
200     &AddHostToList, &ParseParam
201     </td>
202     </tr>
203    
204     <tr>
205     <td>TTXOpenTCP</td>
206     <td>TCP接続を行うときに呼び出される関数。シリアル接続のときは呼び出されない。また、以下のソケットインターフェイスをフックすることもできる。<br>
207     &Pclosesocket, &Pconnect, &Phtonl, &Phtons, &Pinet_addr,
208     &Pioctlsocket, &Precv, &Pselect, &Psend, &Psetsockopt,
209     &Psocket, &PWSAAsyncSelect, &PWSAAsyncGetHostByName,
210     &PWSACancelAsyncRequest, &PWSAGetLastError
211     </td>
212     </tr>
213    
214     <tr>
215     <td>TTXCloseTCP</td>
216     <td>TCPコネクションが切断されるときに呼び出される関数。シリアル接続のときは呼び出されない。下記のうちフックしたインターフェイスがあるならば、元に戻す必要がある。<br>
217     &Pclosesocket, &Pconnect, &Phtonl, &Phtons, &Pinet_addr,
218     &Pioctlsocket, &Precv, &Pselect, &Psend, &Psetsockopt,
219     &Psocket, &PWSAAsyncSelect, &PWSAAsyncGetHostByName,
220     &PWSACancelAsyncRequest, &PWSAGetLastError
221     </td>
222     </tr>
223    
224     <tr>
225     <td>TTXSetWinSize</td>
226     <td>Tera Termウィンドウの画面サイズが変更されたときに呼び出される関数。</td>
227     </tr>
228    
229     <tr>
230     <td>TTXModifyMenu</td>
231     <td>Tera Termのメニューが初期化されるときに呼び出される関数。プラグイン用のメニューを挿入したい場合に使われる。
232     </td>
233     </tr>
234    
235     <tr>
236     <td>TTXModifyPopupMenu</td>
237     <td>Tera Termのポップアップメニューが初期化されるときに呼び出される関数。プラグイン用のポップアップメニューを挿入したい場合に使われる。</td>
238     </tr>
239    
240     <tr>
241     <td>TTXProcessCommand</td>
242     <td>メニューが呼び出されたときに実行される関数。プラグイン用のメニューを処理したいときに使われる。
243     </td>
244     </tr>
245    
246     <tr>
247     <td>TTXEnd</td>
248     <td>Tera Term本体が終了するときに呼び出される関数。</td>
249     </tr>
250    
251     <tr>
252     <td>TTXSetCommandLine</td>
253     <td>新規接続やセッションの複製を行うときに、コマンドラインパラメータの処理を行うときに呼び出される関数。プラグイン独自のオプションを追加したときは、ここで処理される。
254     </td>
255     </tr>
256    
257     </table>
258     </p>
259      
260    
261     <hr>
262    
263    
264    
265     <h2><a name="configuration">設定ファイルの読み書き</a></h2>
266      Windowsではアプリケーションのデータ保存のために、レジストリが伝統的に利用されていますが、Tera Termではその誕生がWindows 3.1までに遡るために、.iniファイルによるローカルディレクトリへの保存方法が標準となっています。<br>
267      パッケージに同梱されるCollectorやLogMeTT、CygTermに関してもローカルディレクトリへデータが保存されます。<br>
268      例外として、TeraTerm Menuはデフォルトでレジストリへ保存をします。カレントディレクトリに"ttpmenu.ini"(0バイトで可)を設置することで、レジストリの代わりに.iniファイルを使うようにすることもできます。<br>
269      <br>
270      
271      teraterm.iniファイルにエントリを追加した場合は、ReadIniFile()#ttset.cに設定を読み込みするようにします。
272    
273     <pre class=code>
274     ts->ConfirmChangePaste =
275     GetOnOff(Section, "ConfirmChangePaste", FName, TRUE);
276     </pre>
277    
278      WriteIniFile()#ttset.c に設定を書き込みするようにします。
279    
280     <pre class=code>
281     WriteOnOff(Section, "ConfirmChangePaste", FName,
282     ts->ConfirmChangePaste);
283     </pre>
284    
285      エントリに文字列を設定する場合は、Win32APIのGetPrivateProfileString()とWritePrivateProfileString()を使います。数値を扱いたい場合は、GetPrivateProfileInt()とWriteInt()を使います。
286    
287     <hr>
288    
289    
290    
291     <h2><a name="secure">セキュアプログラミング</a></h2>
292      WindowsのデフォルトアカウントはAdministrator権限を保持するために(ただし、Windows Vistaには当てはまらない)、アプリケーションにバッファオーバーフローの不具合があると、管理者権限を第三者に奪取されてしまう危険性があります。<br>
293      従来、C言語の文字列処理は開発者のミスにより、バッファオーバーフローが発生しやすいという状況にありました。そこで、MicrosoftはVisual Studio 2005から文字列処理関数のセキュリティ強化バージョンを提供するようになりました。<br>
294      <br>
295    
296     <ul>
297     <li><a href="http://msdn2.microsoft.com/ja-jp/library/8ef0s5kh(VS.80).aspx">CRT のセキュリティ強化(MSDNライブラリ)</a></li>
298     </ul>
299     <br>
300    
301      Tera Termではセキュリティ強化を図るため、文字列操作のほとんどをセキュリティ強化バージョンに置き換えています。以下に代替関数を示します。<br>
302      <br>
303    
304     <table border=1 align=center>
305     <tr>
306     <th></th>
307     <th></th>
308     </tr>
309    
310     <tr>
311     <td>sprintf(), _snprintf()</td>
312     <td>_snprintf_s()</td>
313     </tr>
314    
315     <tr>
316     <td>strcat(), strncat()</td>
317     <td>strncat_s()</td>
318     </tr>
319    
320     <tr>
321     <td>strcpy(), strncpy()</td>
322     <td>strncpy_s()</td>
323     </tr>
324     </table>
325      <br>
326      
327      デフォルトのロケールが適用されると、期待する動作とならないケースにおいては、_snprintf_s_l()を使用しています。<br>
328      いずれの関数においても、_s("secure")という接尾辞が付くため、見た目に区別が付きやすくなっています。当然のことながら、これらの関数はANSI C非互換です。<br>
329      <br>
330      なお、これらの関数を利用する際、Count引数(格納する最大文字数)には"_TRUNCATE"マクロを指定しており、バッファオーバーフローが発生する場合は、強制的にバッファの切り詰めを行っています。
331    
332     <hr>
333    
334    
335    
336     <h2><a name="compatibility">古いバージョンのWindowsとの互換性維持</a></h2>
337      Windowsのアプリケーションプログラムは、単一のバイナリファイルを変更することなく、新旧のバージョンのWindows上で起動できるようにするためには、アプリケーションプログラム側での工夫が必要です。<br>
338      たとえば、Windows2000で導入された SetLayeredWindowAttributes() APIを直接呼び出すと、WindowsNT4.0や98などではアプリケーションの起動に失敗するようになります。そのため、新しいAPIを呼び出すときは、LoadLibrary()を使って動的ロードするようにします。<br>
339    
340     <pre class=code>
341     static BOOL MySetLayeredWindowAttributes(HWND hwnd, COLORREF crKey, BYTE bAlpha, DWORD dwFlags)
342     {
343     typedef BOOL (WINAPI *func)(HWND,COLORREF,BYTE,DWORD);
344     static HMODULE g_hmodUser32 = NULL;
345     static func g_pSetLayeredWindowAttributes = NULL;
346    
347     if (g_hmodUser32 == NULL) {
348     g_hmodUser32 = LoadLibrary("user32.dll");
349     if (g_hmodUser32 == NULL)
350     return FALSE;
351    
352     g_pSetLayeredWindowAttributes =
353     (func)GetProcAddress(g_hmodUser32, "SetLayeredWindowAttributes");
354     }
355    
356     if (g_pSetLayeredWindowAttributes == NULL)
357     return FALSE;
358    
359     return g_pSetLayeredWindowAttributes(hwnd, crKey,
360     bAlpha, dwFlags);
361     }
362     </pre>
363    
364      いちいち、手で関数プロトタイプを書いていくのは面倒である場合は、「DLLの遅延読み込み」というしくみを利用すると、上記のような手順は不要です。いきなり、関数を呼び出すことができます。ダイレクトに呼び出したい関数がある場合、それが古いWindowsではサポートされていないものであるならば、Visual Studioのプロジェクト設定で、「DLLの遅延読み込み」に該当するDLLを指定しておきます。
365    
366     <hr>
367    
368    
369     <h2><a name="debug">デバッグ手法</a></h2>
370     <h3>debug printf</h3>
371      Windowsアプリケーションでは printf() が使えません。標準出力がどこにも割り当てられていないからです。AllocConsole()とfreopen()を使えば、Windowsアプリケーションにおいても printf() を利用することができます。<br>
372      OutputDebugString()というAPIがあります。これは Visual Studio のデバッグコンソールにメッセージ出力することができる関数です。当該APIは、"Debug build"および"Release build"に関係なく、デバッガが存在すれば、メッセージを送信します。ゆえに、 Visual Studioがなくとも、<a href="http://www.vector.co.jp/soft/win95/prog/se046776.html">DBCon</a>のようなツールを使えば、アプリケーションの単体起動においても、OutputDebugString()によるメッセージを拾うことができます。<br>
373      Tera Termでは、可変長引数を扱えるようにラッパー関数を用意しています。
374      
375     <pre class=code>
376     void OutputDebugPrintf(char *fmt, ...) {
377     char tmp[1024];
378     va_list arg;
379     va_start(arg, fmt);
380     _vsnprintf(tmp, sizeof(tmp), fmt, arg);
381     OutputDebugString(tmp);
382     }
383     </pre>
384    
385     <h3>memory leak</h3>
386      malloc()等による確保したヒープメモリの解放し忘れによる「メモリリーク」を、自動で検出するしくみが Visual Studio には用意されています。プログラムの起動時に、以下のコードを挿入するだけです。プログラムの終了時に、解放していないヒープメモリがあれば、 Visual Studio の「出力」ウィンドウにリストアップされます。
387    
388     <pre class=code>
389     #ifdef _DEBUG
390     _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
391     #endif
392     </pre>
393    
394      なお、Windowsのように仮想記憶で動くアプリケーションプログラムに関しては、プログラムの終了時に解放されていないメモリが存在した場合、OSが面倒を見て、メモリが解放されるようになっています。
395    
396     <hr>
397    
398    
399     <h2><a name="thread">マルチスレッド</a></h2>
400      Windowsのアプリケーションはマルチスレッドで設計されることがほとんどですが、Windows 3.1から95の時代ではあまり一般的ではありませんでした。そのため、元々Tera Termはマルチスレッド化されていません。ソースコードを見ると分かるように、グローバル変数が多用されているため、ほとんどの処理がスレッドセーフではありません。<br>
401      ただし、一部の処理においては _beginthreadex() API を使ってスレッドが生成されています。以下にスレッド生成箇所を示します。
402    
403     <p>
404     <div align=center><b>Tera Term</b></div>
405     <table border=1 align=center>
406     <tr>
407     <th>生成箇所</th>
408     <th>ソースファイル</th>
409     </tr>
410    
411     <tr>
412     <td>シリアル接続</td>
413     <td>CommStart()#commlib.c</td>
414     </tr>
415    
416     <tr>
417     <td>TELNETキープアライブ</td>
418     <td>TelStartKeepAliveThread()#telnet.c</td>
419     </tr>
420    
421     <tr>
422     <td>IPv4/v6ソケットの生成</td>
423     <td>WSAAsyncGetAddrInfo()#WSAAsyncGetAddrInfo.c</td>
424     </tr>
425     </table>
426    
427     <br>
428    
429     <div align=center><b>TTSSH</b></div>
430     <table border=1 align=center>
431     <tr>
432     <th>生成箇所</th>
433     <th>ソースファイル</th>
434     </tr>
435    
436     <tr>
437     <td>SSHキープアライブ</td>
438     <td>start_ssh_heartbeat_thread()#ssh.c</td>
439     </tr>
440    
441     <tr>
442     <td>SCP送信処理</td>
443     <td>SSH2_scp_tolocal()#ssh.c</td>
444     </tr>
445    
446     <tr>
447     <td>SCP受信処理</td>
448     <td>SSH2_scp_fromremote()#ssh.c</td>
449     </tr>
450     </table>
451     </p>
452    
453      すでに説明したとおり、Tera Term(TTSSH含む)の内部処理はスレッドセーフではないため、シンプルにスレッドを生成し、スレッド内から送受信処理等を行おうとすると、不具合が発生してしまいます。<br>
454      TELNETやSSHのキープアライブ(ハートビート)処理を実現するためには、定期的にパケットの送信処理を行う必要があります。また、SCPによるファイル送受信を行う際においても、ファイルの送信処理中に、ユーザの端末操作のレスポンスを落とさないために、スレッドの使用が不可欠です。<br>
455      そこで、マルチスレッドを使う場合は、モードレスダイアログを非表示で作成したあとに、_beginthreadex() APIでスレッドを生成し、実際の処理はモードレスダイアログに行わせるという手段を使用しています。このしくみにより、マルチスレッドを使いながら、スレッドセーフを保つことができます。以下に、コード例を示します。<br>
456    
457     <pre class=code>
458     #define WM_SEND_HEARTBEAT (WM_USER + 1)
459    
460     static LRESULT CALLBACK telnet_heartbeat_dlg_proc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp)
461     {
462    
463     switch (msg) {
464     case WM_INITDIALOG:
465     return FALSE;
466    
467     case WM_SEND_HEARTBEAT:
468     TelSendNOP();
469     return TRUE;
470     break;
471    
472     case WM_COMMAND:
473     break;
474    
475     case WM_CLOSE:
476     return TRUE;
477    
478     case WM_DESTROY:
479     return TRUE;
480    
481     default:
482     return FALSE;
483     }
484     return TRUE;
485     }
486    
487     static unsigned _stdcall TelKeepAliveThread(void *dummy) {
488     static int instance = 0;
489    
490     if (instance > 0)
491     return 0;
492     instance++;
493    
494     while (cv.Open && nop_interval > 0) {
495     if (time(NULL) >= cv.LastSendTime + nop_interval) {
496     SendMessage(keepalive_dialog, WM_SEND_HEARTBEAT, 0, 0);
497     }
498    
499     Sleep(100);
500     }
501     instance--;
502     return 0;
503     }
504    
505     void TelStartKeepAliveThread() {
506     unsigned tid;
507    
508     if (ts.TelKeepAliveInterval > 0) {
509     nop_interval = ts.TelKeepAliveInterval;
510    
511     keepalive_dialog = CreateDialog(hInst, MAKEINTRESOURCE(IDD_BROADCAST_DIALOG),
512     HVTWin, (DLGPROC)telnet_heartbeat_dlg_proc);
513    
514     keepalive_thread = (HANDLE)_beginthreadex(NULL, 0, TelKeepAliveThread, NULL, 0, &tid);
515     if (keepalive_thread == (HANDLE)-1) {
516     nop_interval = 0;
517     }
518     }
519     }
520     </pre>
521    
522    
523    
524     <hr>
525    
526    
527     <h2><a name="dde">DDEによるプロセス間通信</a></h2>
528     <h3>概要</h3>
529      DDE(Dynamic Data Exchange)の誕生は、1987年のWindows 2.0までに遡ります。DDEはプロセス間通信を行うためのしくみですが、現在ではレガシーな方式であり、ほとんどのアプリケーションでは利用されていません。Windowsにおけるプロセス間通信といえば、メールスロットや名前付きパイプ、OLEなどが定番です。<br>
530      かつては、DDEによるプロセス間の通信データをキャプチャすることができる「DDEスパイ」(DDESPY.EXE)というツールがVisual Studioに付属していましたが、現在のVisual Studioにはもはや含まれていません。<br>
531      DDEに関するリファレンスはMSDNライブラリから参照することができます。<br>
532    
533     <p>
534     <ul>
535     <li><a href="http://msdn2.microsoft.com/en-us/library/ms648711(VS.85).aspx">Dynamic Data Exchange(MSDNライブラリ)</a></li>
536     <li><a href="http://msdn2.microsoft.com/en-us/library/ms648712(VS.85).aspx">Dynamic Data Exchange Management Library(MSDNライブラリ)</a></li>
537     </ul>
538     </p>
539    
540      DDEは、TCPによるネットワーク通信と似ており、サーバとクライアント間を一対一で接続し、通信を行います。アプリケーションがDDEによる通信を行うために、DDEML(Dynamic Data Exchange Management Library)と呼ばれるライブラリをWin32 APIとして提供されています。<br>
541      DDE通信を行うために、一方がサーバとなり、他方がクライアントになる必要があります。また、通信のセッションをシステム全体でユニークとするために、識別情報が必要です。TCP通信ではIPアドレスとポート番号が使われますが、DDE通信では「サービス名」と「トピック名」の組み合わせが使われます。Tera Termではサービス名は"TERATERM"という文字列が使われ、トピック名はTera Term本体のウィンドウハンドル(HVTWin)の16進数値を文字列化したものが使われています。<br>
542      このようなしくみになっているために、マクロスクリプトからまったく別のTera Termへコマンドを送ることはできません。<br>
543    
544     <div align="center">
545     <img src="image/dde.png" width=720 height=540>
546     </div>
547    
548      上図に示すように、Tera Term本体("ttermpro.exe")がDDEサーバとなり、マクロプログラム("ttpmacro.exe")がDDEクライアントとなります。DDEでは、やりとりするデータの塊のことを「トランザクション」と呼びます。トランザクションには以下に示すような何種類かがあります。タイプは"ddeml.h"でマクロ定義されています。<br>
549    
550     <p>
551     <table border=1 align=center>
552     <tr>
553     <th>タイプ</th>
554     <th>意味</th>
555     </tr>
556    
557     <tr>
558     <td>XTYP_ADVREQ</td>
559     <td>DDEサーバがクライアントへデータを送るために、DDEサーバが自分自身に送るメッセージ。</td>
560     </tr>
561    
562     <tr>
563     <td>XTYP_POKE</td>
564     <td>DDEクライアントからサーバへデータを送る。</td>
565     </tr>
566    
567     <tr>
568     <td>XTYP_ADVSTART</td>
569     <td>DDEサーバに対してアドバイズループの開始を指示する。</td>
570     </tr>
571    
572     <tr>
573     <td>XTYP_ADVDATA</td>
574     <td>DDEクライアントにデータを定期的に送る。</td>
575     </tr>
576    
577     <tr>
578     <td>XTYP_EXECUTE</td>
579     <td>DDEサーバに文字列を送り、何らかの処理をサーバに指示する。</td>
580     </tr>
581    
582     </table>
583     </p>
584    
585      DDE通信の特徴として、アドバイズループ(advise loop)という概念があります。DDEサーバがアドバイズループに入ると、クライアントはサーバから定期的にデータを受け取り続けることができます。Tera Termでは、リモートホストからの受信データを、マクロプログラムへ渡すために、アドバイズループが使われています。<br>
586    
587     <h3>ライブラリ</h3>
588      Tera Termで使われているDDEMLについて、以下に示します。
589      
590      
591     <p>
592     <table border=1 align=center>
593     <tr>
594     <th>関数名</th>
595     <th>機能</th>
596     </tr>
597    
598     <tr>
599     <td>DdeInitialize</td>
600     <td>DDEを初期化し、コールバック関数を登録する。初期化できるとインスタンスを返す。</td>
601     </tr>
602    
603     <tr>
604     <td>DdeCreateStringHandle</td>
605     <td>文字列リテラルからハンドルを作成する。ハンドルはサーバとクライアントの通信用に使われる。</td>
606     </tr>
607    
608     <tr>
609     <td>DdeNameService</td>
610     <td>インスタンスとサービス名("TERATERM")をサーバに登録する。登録後、XTYP_REGISTERトランザクションがクライアントへ送られる。登録解除する際にも使われる。</td>
611     </tr>
612    
613     <tr>
614     <td>DdeCmpStringHandles</td>
615     <td>2つの文字列ハンドルを比較する。</td>
616     </tr>
617    
618     <tr>
619     <td>DdeClientTransaction</td>
620     <td>クライアントからサーバへトランザクションを送ることができる。トランザクションタイプとして、XTYP_REQUEST・XTYP_EXECUTE・XTYP_ADVSTART・XTYP_POKEなどが指定できる。サーバからのACKを待つまでのタイムアウト時間を指定することができ、Tera Termではほとんど"1000ミリ秒(1秒)"が指定されている。ただし、ACKを確認するケースにおいては"5000ミリ秒(5秒)"が指定されている。</td>
621     </tr>
622    
623     <tr>
624     <td>DdeAccessData</td>
625     <td>DDEハンドルから実際のデータへのポインタを取得する。データの取り出しが終わったら、DdeUnaccessData()を呼び出すこと。</td>
626     </tr>
627    
628     <tr>
629     <td>DdeCreateDataHandle</td>
630     <td>DDEオブジェクトを作成し、ハンドルを返す。DDEサーバのアドバイズループや、XTYP_REQUESTトランザクション受信時に、DDEクライアントへデータを送るために使われている。</td>
631     </tr>
632    
633     <tr>
634     <td>DdeGetData</td>
635     <td>DDEオブジェクトからバッファへコピーする。</td>
636     </tr>
637    
638     <tr>
639     <td>DdeDisconnect</td>
640     <td>DDE通信を終了する</td>
641     </tr>
642    
643     <tr>
644     <td>DdePostAdvise</td>
645     <td>DDEサーバ側で使われる関数で、自分自身に XTYP_ADVREQ トランザクションを送る。</td>
646     </tr>
647    
648     </table>
649     </p>
650    
651    
652    
653     <h3>実装</h3>
654      DDEサーバ側の実装について見ていきます。Tera Term本体("ttermpro.exe")がDDEサーバとなり、かならずDDEサーバから起動されます。マクロプログラム("ttpmacro.exe")から直接マクロスクリプトが実行されるケースにおいても、"connect"マクロによりDDE接続をしないと、通信が開始できません。<br>
655      Tera TermのControlメニューからMacroを呼び出した場合、RunMacro()#ttdde.c がコールされます。<br>
656      HVTWinウィンドウハンドルからトピック名(8バイト)を作成し、DDEの初期化とサーバの登録を行います。また、このタイミングでDDEバッファ(1KB)を作成しています。その後、"ttpmacro.exe"を /D= オプションでトピック名を渡しつつ、起動をします。<br>
657      
658     <pre class=code>
659     SetTopic();
660     if (! InitDDE()) return;
661     strncpy_s(Cmnd, sizeof(Cmnd),"TTPMACRO /D=", _TRUNCATE);
662     strncat_s(Cmnd,sizeof(Cmnd),TopicName,_TRUNCATE);
663     </pre>
664    
665      DDEサーバに、DDEクライアントからトランザクションが送られてきたときは、DdeCallbackProcコールバック関数が呼び出されます。コールバック関数は、DdeInitialize()でDDEの初期化を行うときに登録されます。<br><br>
666      
667      次に、DDEクライアントについて見てみましょう。マクロプログラムの起動時、InitDDE()#ttmdde.c が呼び出され、DDEクライアントとして初期化が行われます。DDEの初期化は、DdeInitialize()で行われ、同時にDdeCallbackProcコールバック関数が登録されます。DDEサーバから届いたトランザクションは、コールバック関数で処理されます。<br>
668      DDE通信を始めるためには、DdeConnect()を呼び出し、サーバと接続する必要があります。次に、"ttpmacro.exe"のウィンドウハンドル(HWin)をサーバへ通知するために、XTYP_EXECUTEトランザクションで送ります。最後に、XTYP_ADVSTARTトランザクションをサーバへ送り、アドバイズループを開始します。<br>
669    
670     <pre class=code>
671     ConvH = DdeConnect(Inst, Service, Topic, NULL);
672     if (ConvH == 0) return FALSE;
673     Linked = TRUE;
674    
675     Cmd[0] = CmdSetHWnd;
676     w = HIWORD(HWin);
677     Word2HexStr(w,&(Cmd[1]));
678     w = LOWORD(HWin);
679     Word2HexStr(w,&(Cmd[5]));
680    
681     DdeClientTransaction(Cmd,strlen(Cmd)+1,ConvH,0,
682     CF_OEMTEXT,XTYP_EXECUTE,1000,NULL);
683    
684     DdeClientTransaction(NULL,0,ConvH,Item,
685     CF_OEMTEXT,XTYP_ADVSTART,1000,NULL);
686     </pre>
687    
688    
689     <h3>バッファの管理</h3>
690      マクロプログラムでは"wait"コマンド等で、リモートホストから送られてきたデータを監視するための機能が用意されています。この機能を実現するためには、Tera Term本体とマクロプログラムのそれぞれにおいて、バッファを用意する必要があり、プロセス間通信(DDEトランザクション)により、Tera Term本体からマクロプログラムへリモートホストからの受信データを送らなければなりません。<br>
691    
692     <div align="center">
693     <img src="image/dde_flowcontrol.png" width=720 height=540>
694     </div>
695    
696      まず、Tera Term本体におけるリモートホストからのTCPパケット受信は、アイドルループ OnIdle()#teraterm.cpp にて行われます。OnIdle()から呼び出される CommReceive()#commlib.c において、TCPパケットデータをバッファ(cv->InBuff[])に格納します。このバッファは 1KB の大きさを持ちます。また、リングバッファではないため、バッファフルになった場合は、TCPパケットの受信をしません。ただし、バッファフル状態が長く続くと、Windowsカーネル内にTCPパケットが溜まっていき、いずれはリモートホストからのパケットを受信できなくなる可能性があります。<br>
697      エスケープシーケンスの解析処理を行う過程で、「ログ採取」か「マクロ実行」を行っている場合は、LogPut1()が呼び出され、DDEバッファ(cv.LogBuf[])へ受信データが格納されます。このバッファは1KBの大きさを持つリングバッファであり、バッファフルになった場合は、最古のデータから上書きされてゆきます。<br>
698      Tera Term本体のDDEバッファのデータは、エスケープシーケンスの解析処理が完了後、DDEAdv()#ttdde.c がすぐに呼び出され、自分自身(DDEサーバ)へ XTYP_ADVREQ トランザクションを送ります。XTYP_ADVREQを受け取ったら、DDEコールバック関数 DdeCallbackProc() が呼び出され、マクロプログラムへのデータ送信を行います。ここでアドバイズループが使われています。<br>
699    
700     <div align="center">
701     <img src="image/dde_buffer.png" width=720 height=540>
702     </div>
703    
704      アドバイズループによりDDEサーバよりデータが送られてくると、DDEクライアントであるマクロプログラムにおいては、XTYP_ADVDATAトランザクションがDDEコールバック関数 DdeCallbackProc()#ttmdde.c により処理されます。<br>
705      
706      なお、Tera Term本体において、DDE通信用のバッファと、ログ採取用のバッファは cv.LogBuf[] で共有されています。バッファの先頭とデータサイズを表すインデックスは、DDE通信の場合は"DStart"と"Dcount"、ログ採取の場合は"LStart"と"Lcount"と区別されています。実際には、1つのバッファを共有しているわけなので、それぞれのインデックスが食い違うと、誤動作する原因となるため、常に同期を取っておくことになります。<br>
707     <hr>
708    
709    
710     <h2><a name="ttssh">TTSSHによるSSHの設計と実装</a></h2>
711     <h3>概要</h3>
712      オリジナルのTTSSHは<a href="http://www.cs.cmu.edu/People/roc/">Robert O'Callahan</a>氏(現在は<a href="http://weblogs.mozillazine.org/roc/">Mozilla hacker</a>として活躍)により開発されたプラグインです。SSH1へ対応しており、ポートフォワーディングやzlibによるパケット圧縮もサポートしていました。TTSSHは、Tera Termをセキュア通信に対応させるためのプラグインであったために、SCPやSFTP等には未対応でした。オリジナルTera Termが1998年に開発凍結後も、2001年ごろまでメンテナンスが続けられていました。<br>
713      TTSSHのSSH2対応を実現するために、TeraTerm Projectにより2004年から設計と実装が始められました。3年の歳月をかけて、ほぼSSH2プロトコルのフルサポートを実現しました。現在ではSCPへも対応しています。将来的にはSFTPへも対応されるかもしれません。<br>
714      原則、TTSSHの実装は<a href="http://www.openssh.com/">OpenSSH</a>を参考にしています。一部、コードをそのまま流用しているところもあります。ただし、OpenSSHはUNIXのコマンドライン向けに設計されているため、Tera TermのようなWindowsアプリケーションにはそのまま適合しない箇所も多く、フレームワークとしてはOpenSSHと大きく異なったものとなっています。<br>
715    
716    
717     <h3>SSHプロトコル</h3>
718      SSH(Secure Shell)は、バージョン1(厳密には1.5)とバージョン2が存在し、略して"SSH1"および"SSH2"と呼ばれます。それらのバージョン間にはプロトコル仕様としての互換性はありません。SSH1にはセキュリティ上の問題があるために、現在はほとんど利用されません。<br>
719      SSH2プロトコルの仕様に関しては、RFC化されています。
720      
721     <p>
722     <ul>
723     <li><a href="http://www.ietf.org/rfc/rfc4250.txt">RFC4250: The Secure Shell (SSH) Protocol Assigned Numbers</a></li>
724     <li><a href="http://www.ietf.org/rfc/rfc4251.txt">RFC4251: The Secure Shell (SSH) Protocol Architecture</a></li>
725     <li><a href="http://www.ietf.org/rfc/rfc4252.txt">RFC4252: The Secure Shell (SSH) Authentication Protocol</a></li>
726     <li><a href="http://www.ietf.org/rfc/rfc4253.txt">RFC4253: The Secure Shell (SSH) Transport Layer Protocol</a></li>
727     <li><a href="http://www.ietf.org/rfc/rfc4254.txt">RFC4254: The Secure Shell (SSH) Connection Protocol</a></li>
728     <li><a href="http://www.ietf.org/rfc/rfc4255.txt">RFC4255: Using DNS to Securely Publish Secure Shell (SSH) Key Fingerprints</a></li>
729     <li><a href="http://www.ietf.org/rfc/rfc4256.txt">RFC4256: Generic Message Exchange Authentication for the Secure Shell Protocol (SSH)</a></li>
730     <li><a href="http://www.ietf.org/rfc/rfc4344.txt">RFC4344: The Secure Shell (SSH) Transport Layer Encryption Modes</a></li>
731    
732     </ul>
733     </p>
734    
735    
736     <h3>接続処理</h3>
737      TTSSHは、Tera Termの一部のコードでもあるため、ネットワーク接続処理はTera TermとTTSSHの間を行き来することになり、処理の流れが複雑になっています。また、SSHプロトコルそのもののフローを熟知していないと、TTSSHのシーケンスを追っていくのが難しくなっています。以下に、リモートホストへの接続を行うまでのフローを示します。<br>
738    
739     <div align="center">
740     <img src="image/ssh.png" width=720 height=540>
741     </div>
742    
743    
744     <h3>送信パケット処理</h3>
745     SSH2プロトコルに載せて、パケットをサーバへ送るときのコードは以下のような書き方となります。begin_send_packet()の呼び出しで、「pvar->ssh_state.outbuf + 12」が返り値となり、それがペイロードを表します。ペイロードは純粋にサーバへ送りたいデータのことで、サイズやパディング等を含みません。<br>
746    
747     <pre class=code>
748     buffer_t *msg;
749     int len;
750     char *s;
751     unsigned char *outmsg;
752    
753     msg = buffer_init();
754     if (msg != NULL) {
755     buffer_put_int(msg, SSH2_DISCONNECT_PROTOCOL_ERROR);
756     s = "disconnected by server request";
757     buffer_put_string(msg, s, strlen(s));
758     s = "";
759     buffer_put_string(msg, s, strlen(s));
760    
761     len = buffer_len(msg);
762     outmsg = begin_send_packet(pvar, SSH2_MSG_DISCONNECT, len);
763     memcpy(outmsg, buffer_ptr(msg), len);
764     finish_send_packet(pvar);
765     buffer_free(msg);
766     }
767     </pre>
768    
769      SSH通信に載せられて、実際にパケットが送出されるのは、finish_send_packet()から呼び出される finish_send_packet_special() です。パケットを送信するときのフォーマットについて、以下に示します。共通鍵暗号でパケットデータを暗号化する前に、ヘッダとフッタを付ける必要があります。<br>
770      パケットサイズはHMACを除く長さです。パケットサイズそのものはビッグエンディアン形式で、4バイト分格納しますが、その"4"バイトは含まれません。ペイロードの直後にパディングを埋めるのは、共通鍵暗号で暗号化するときに「ブロックサイズ単位」になっていなければ、アルゴリズム的に暗号化できないからです。ブロックサイズは暗号アルゴリズムにより異なり、たとえば3DES-CBCならば24バイト、AES128ならば16バイトです。<br>
771      HMAC(Keyed-Hashing for Message Authentication)は、暗号化本文に対するハッシュです。ハッシュのアルゴリズムは選択可能であり、"MD5"や"SHA-1"がよく使われています。HMACを付加することにより、「第三者によるデータの改ざん」を検出することができます。HMACは、暗号化対象となる本文を秘密鍵とシーケンス番号を加え、ハッシュ値を計算します。秘密鍵とシーケンス番号を加えることにより、第三者がデータをまるごと差し替えたとしても、送信者が生成したハッシュ値を復元することは理論上できません。<br>
772      
773    
774     <div align="center">
775     <img src="image/ssh_packet_format1.png" width=720 height=540>
776     </div>
777    
778      zlibによるパケット圧縮を行う場合における、パケットを送信するときのフォーマットについて、以下に示します。パケット圧縮を行うのは、「ペイロード」の部分のみで、残りは通常の送信パケットとフォーマットは同じです。なお、パケットを圧縮したとしても、かならずしも元のサイズよりも小さくなるとは限らないので、そのことを考慮したバッファ管理が必要です。<br>
779      パケット圧縮送信で難しいのは、圧縮を開始するタイミングです。ローカルホストからリモートホストへのSSH接続を開始すると、実にたくさんのネゴシエーションが行われますが、パケットを圧縮してよいのは決められたタイミングであり、このタイミングを間違えると、サーバとまったく通信ができなくなります。<br>
780      通常のパケット圧縮の場合は、"SSH2_MSG_KEXINIT"を受信したタイミングです。遅延パケット圧縮の場合は、ユーザ認証が成功したタイミング("SSH2_MSG_USERAUTH_SUCCESS"を受信した時)です。遅延パケット圧縮というのは、それまで"SSH2_MSG_KEXINIT"を受信したタイミングで圧縮を開始していたのを、ユーザ認証が完了するまで延長する方式です。遅延パケット圧縮は、zlibライブラリのセキュリティホールにより、不正なSSHサーバへ接続しただけで、クライアント側に影響が出るのを回避するためのしくみです。
781    
782    
783     <div align="center">
784     <img src="image/ssh_packet_format2.png" width=720 height=540>
785     </div>
786    
787    
788     <h3>受信パケット処理</h3>
789      パケットの受信は、TeraTerm本体側からは recv ソケット関数を呼び出した場合に、それがTELNETなのかSSHなのかを意識させないような設計になっていることと、 recv ソケット関数の呼び出しでは、かならずしも十分なバッファサイズが指定されてくるとは限らないため、少々実装が複雑になっています。<br>
790    
791     <div align="center">
792     <img src="image/ssh_recv_packet.png" width=720 height=540>
793     </div>
794    
795      TeraTerm本体側は OnIdle()#teraterm.cpp というアイドルループにおいて、常時パケットの受信がないかをポーリングしています。それが CommReceive() で、recv()を呼び出します。recv()はTTSSHによりフックされているので、ソケット関数ではなく、TTXrecv()#ttxssh.c が呼び出されます。<br>
796      CommReceive()は recv() を呼び出す際に、バッファ(cv->InBuff[])の空きポインタとサイズを引数に渡します。バッファサイズは 1KB です。つまり、TTXrecv()のサイズには、1〜1024 までの数値が渡される可能性があるということです。<br>
797      TTXrecv()から呼び出される PKT_recv() は、少々複雑なループ処理となっています。SSH接続を初めて行うときのシーケンスを以下に示します。
798      
799     <ol>
800     <li>recv_data() で本当の recv() を呼び出し、サーバからの受信パケットをカーネルから受け取る。pvar->pkt_state.datalenが更新される。 </li>
801     <li>SSH_handle_server_ID() でSSHサーバのバージョンチェックが行われる。pvar->pkt_state.datastart と pvar->pkt_state.datalen を更新する。</li>
802     <li>再度、recv_data() が呼ばれるが、サーバからの受信データがもうないので、connection_closed=TRUE として while ループを抜ける。</li>
803     <li>TeraTermの recv() は"0"で返ってくる。すなわち、受信データなし。</li>
804     </ol>
805    
806      次に、SSH通信のための共通鍵生成までのシーケンスを以下に示します。
807    
808     <ol>
809     <li>recv_data() で本当の recv() を呼び出し、サーバからの受信パケットをカーネルから受け取る。pvar->pkt_state.datalenが更新される。 </li>
810     <li>SSH_predecrpyt_packet() で、受信パケットの先頭ブロックのみを復号化する。SSHパケットのサイズを取得する。</li>
811     <li>妥当なSSHパケットサイズならば、SSH_handle_packet() を呼び出し、メッセージタイプに応じたハンドラを呼び出す。pvar->ssh_state.payload と pvar->ssh_state.payloadlen を設定する。</li>
812     <li>pvar->pkt_state.datastart と pvar->pkt_state.datalen を更新する。</li>
813     <li>pvar->pkt_state.datalen がゼロになるまで、SSH_predecrpyt_packet() の処理を繰り返す。</li>
814     <li>recv_data() が呼ばれるが、サーバからの受信データがもうないので、connection_closed=TRUE として while ループを抜ける。</li>
815     <li>TeraTermの recv() は"0"で返ってくる。すなわち、受信データなし。</li>
816     </ol>
817    
818      次に、端末データ通信のシーケンスを以下に示します。
819    
820     <ol>
821     <li>recv_data() で本当の recv() を呼び出し、サーバからの受信パケットをカーネルから受け取る。pvar->pkt_state.datalenが更新される。 </li>
822     <li>SSH_predecrpyt_packet() で、受信パケットの先頭ブロックのみを復号化する。SSHパケットのサイズを取得する。</li>
823     <li>妥当なSSHパケットサイズならば、SSH_handle_packet() を呼び出し、メッセージタイプに応じたハンドラを呼び出す。pvar->ssh_state.payload と pvar->ssh_state.payloadlen を設定する。</li>
824     <li>メッセージタイプがSSH2_MSG_CHANNEL_DATAなので、handle_SSH2_channel_data() を呼び出す。pvar->ssh_state.payload_datalen と pvar->ssh_state.payload_datastart を設定する。</li>
825     <li>pvar->pkt_state.datastart と pvar->pkt_state.datalen を更新する。</li>
826     <li>SSH_is_any_payload() が真を返すようになり、PKT_recv()に渡されてきたバッファへデータをコピーする。</li>
827     <li>TeraTerm側のバッファサイズがいっぱいになった場合は、SSH端末データが残っていたとしても、PKT_recv()は返る。</li>
828     <li>TeraTerm側のバッファサイズに余裕がある場合は、recv_data()を呼び出し、サーバからの受信データを取得する。</li>
829     <li>TeraTermの recv() は「受信データサイズ」で返ってくる。</li>
830     </ol>
831    
832    
833     <h3>シーケンス制御</h3>
834      SSH2接続を行うことで、通信経路を暗号化することができるのが特徴ですが、パケットの暗号化を行うためには、「鍵」が必要です。通信経路の暗号化には、共通鍵による共通鍵暗号が利用されます。公開鍵暗号のほうがセキュリティ強度は高いのですが、暗号処理に多大な時間がかかるため、SSHのような通信性能が要求されるしくみでは採用されません。SSH2では、共通鍵暗号アルゴリズムとして、AES(Advanced Encryption Standard:Rijndaelアルゴリズム)や3DES(Triple DES)などが利用されます。<br>
835      共通鍵は通信を行う二者間でのみに共有される情報であり、第三者に知られてはなりません。SSH2では、クライアントがリモートホスト(SSHサーバ)へTCP接続した時に、"Diffie-Hellman"アルゴリズムをベースとした独自の方式により、クライアントとサーバでしか知り得ないDH(Diffie-Hellman)鍵を生成します。DH鍵生成までの過程は、ネットワーク上をパケットが平文で流れるため、第三者によるパケットキャプチャが可能となっていますが、パケットを覗かれても、DH鍵は理論上第三者には分からないようになっています。<br>
836      共通鍵が生成できたあとは、その鍵を使ってパケットを暗号化します。SSH2では、送受信されるパケットは種類があるため、それぞれに「メッセージ番号」を割り振っています。RFC4250にメッセージ番号の一覧があります。メッセージ名は"SSH2_MSG_xxxx"というネーミングになっており、TTSSH内部でも同じ名前でマクロ定義しています。<br>
837      以下に、クライアントからサーバへTCP接続(ポート22番)してから、パスワード認証でユーザ認証されるまでの流れを示します。<br>
838    
839    
840     <div align="center">
841     <img src="image/ssh2_sequence1.png" width=720 height=540>
842     </div>
843    
844     <div align="center">
845     <img src="image/ssh2_sequence2.png" width=720 height=540>
846     </div>
847    
848      以下は、リモートホストのシェル上で"exit"や"logout"として、クライアントから明示的にシェルをクローズするときの、パケットの流れを示しています。<br>
849    
850     <div align="center">
851     <img src="image/ssh2_sequence3.png" width=720 height=540>
852     </div>
853    
854      TTSSHは、SSH2でパスワード認証のほかにkeyboard-interactive認証、publickey認証、Pageantを利用したpublickey認証をサポートしています。それぞれの認証方式でどのようなシーケンスで認証が行われるのか、以下に示します。
855    
856     <div align="center">
857     <img src="image/ssh2_auth1.png" width=720 height=540>
858     </div>
859     <div align="center">
860     <img src="image/ssh2_auth2.png" width=720 height=540>
861     </div>
862    
863    
864    
865     <h3>疑似端末のしくみ</h3>
866      SSH2では、新しく「フロー制御」という概念が取り込まれています。TCPのウィンドウと同じ考え方で、「ウィンドウサイズ」というしくみを導入しています。この機能により、クライアント(Tera Term)とサーバ(SSHデーモン)間において、フロー制御が働くため、原則データが溢れることはありません。<br>
867      ところで、SSH2におけるフロー制御があるにも関わらず、大量のクリップボードをTeraTermの端末へペーストすると、サーバ側での「データの取りこぼし」が発生することがあります。この現象を理解するためには、UNIXにおける疑似端末(PTY: pseudo-terminal)の動作原理を知る必要があります。
868    
869     <div align="center">
870     <img src="image/pty.png" width=720 height=540>
871     </div>
872    
873      SSHデーモン(sshd)はクライアントに対して、あたかもサーバ側のシェルが直接接続されているかのように見せる必要があります。逆に、シェル上で動くプログラムは、文字を送りたいときは printf(3) を、文字を受け取りたい場合は scanf(3) といったCライブラリ関数を呼び出すだけでよく、その先がシリアルコンソールなのか、VGAコンソールなのか、SSH接続されているのかは、一切気にしなくてよいようになっています。<br>
874      sshdは、クライアントからの接続要求があったタイミングで、openpty(3)を使って、疑似端末の初期化を行います。疑似端末では、カーネル空間でクライアントとサーバをつなぐために、「マスターデバイスドライバ」と「スレーブデバイスドライバ」が用意されます。マスターデバイスドライバが担当するデバイスファイルは"/dev/ptyXX"、スレーブデバイスドライバでは"/dev/ttyXX"です。つまり、sshdはマスターデバイスドライバへアクセスすることで、シェルとお話をすることができます。シェルは、sshdからforkされて子プロセスとなり、親プロセス(sshd)が初期化済みのスレーブデバイスドライバとお話をすることになります。この疑似端末のしくみにより、sshdとシェルが接続されます。<br>
875      なお、端末ラインディシプリン(line discipline: 回線規約)というのは、たとえばプログラムが getchar() を呼び出したときに、Enterキーを押下するまで、プログラムに制御が渡りません。端末ラインディシプリンは、プログラム実行中での「行内編集」を可能とするためのモジュールです。Linuxでは、端末ラインディシプリンは /proc/tty/ldiscs で確認できます(N_TTYが標準的に利用される)。
876    
877    
878     <hr>
879    
880    
881     <h2><a name="macro">マクロ言語の設計と実装</a></h2>
882     <h3>概要</h3>
883      Tera Termのマクロスクリプトは、BASIC風の言語仕様となっています。BisonやFlexといったしくみは利用しておらず、力業的な独自の構文解析(再帰的下降法)により実装されています。そのため、本格的なスクリプト言語としての記述はできない側面があります。<br>
884      
885     <h3>ファイルの読み込み</h3>
886      ttpmacro.exeの起動時に、マクロファイル(.ttl)が一括してバッファへ読み込まれます。
887      
888     <p><ul>
889     <li>OnInitDialog()#ttmmain.cpp -> InitTTL() -> InitBuff() -> LoadMacroFile()</li>
890     </ul></p>
891    
892      初めて読み込まれるマクロファイルの全内容は Buff[0] # ttmbuff.c に格納されます。この時点で、ファイルの内容は一括して読み込まれるため、マクロ実行中はファイルを削除してしまっても問題はありません。ただし、"include"で別のファイルを読み込む場合は、includeを実行する時点で、include対象となるファイルの読み込みが発生します。
893      
894     <pre class=code>
895     #define MAXNESTLEVEL 10 /* 扱えるファイル数(includeは9つまで)*/
896    
897     static int INest; /* 現在のネスト位置 */
898     static HANDLE BuffHandle[MAXNESTLEVEL]; /* GlobalAlloc()によるバッファ */
899     static PCHAR Buff[MAXNESTLEVEL]; /* バッファ領域 */
900     static BINT BuffLen[MAXNESTLEVEL]; /* ファイルサイズ(バッファサイズ) */
901     static BINT BuffPtr[MAXNESTLEVEL]; /* バッファのオフセット(読み込み位置)*/
902     </pre>
903    
904    
905     <h3>マクロエンジン</h3>
906      マクロ処理はアイドルループ OnIdle()#ttmmain.cpp で行われます。アイドルループでは TTLStatus 変数により、マクロエンジンの動作を変えています。通常の実行状態は IdTTLRun がセットされています。以下に、動作一覧を示します。
907    
908     <p>
909     <table border=1 align=center>
910     <tr>
911     <th>条件</th>
912     <th>処理</th>
913     </tr>
914    
915     <tr>
916     <td>TTLStatus==IdTTLEnd</td>
917     <td>マクロプログラムを終了する</td>
918     </tr>
919    
920     <tr>
921     <td>送信データがある場合(OutLen > 0)</td>
922     <td>Tera Term本体へデータを送る</td>
923     </tr>
924    
925     <tr>
926     <td>TTLStatus==IdTTLRun</td>
927     <td>一行ずつマクロを実行する</td>
928     </tr>
929    
930     <tr>
931     <td>TTLStatus==IdTTLWait</td>
932     <td>ウェイトする('wait'コマンド)</td>
933     </tr>
934    
935     <tr>
936     <td>TTLStatus==IdTTLWaitLn</td>
937     <td>ウェイトする('waitln'コマンド)</td>
938     </tr>
939    
940     <tr>
941     <td>TTLStatus==IdTTLWaitNL</td>
942     <td>一行受信する('recvln'コマンド)</td>
943     </tr>
944    
945     <tr>
946     <td>TTLStatus==IdTTLWait2</td>
947     <td>文字列を待つ('waitrecv'コマンド)</td>
948     </tr>
949    
950     </table>
951     </p>
952    
953    
954     <h3>インタープリタ処理</h3>
955      アイドルループから Exec()#ttl.c が定期的に呼び出される度に、マクロファイルが一行ずつ処理されてゆきます。GetNewLine() では、バッファから一行分を取り出し、LineBuff[]#ttmparse.c へ格納します。行の終わりかどうかは、「ASCIIコードが0x20未満で、かつタブ(0x09)以外」のコードが出現したタイミングで判定しています。先頭の空白やタブは無視されます。セミコロン(;)が出現すると、以降の処理をスキップするため、コメントは行の途中でも付けられることになります。<br>
956    
957     <pre class=code>
958     char LineBuff[MaxLineLen]; /* 1つの行は500バイトまで格納可能 */
959     WORD LinePtr; /* バッファオフセット */
960     WORD LineLen; /* バッファサイズ */
961     </pre>
962    
963      Exec()から呼ばれる ExecCmnd() で、字句解析を行います。字句解析は単純な文字列検索であり、LineBuff[]を1バイトずつ参照していきます。大まかな処理の流れは以下のとおりです。
964    
965     <p><ol>
966     <li>endwhileの判定</li>
967     <li>break処理</li>
968     <li>endifの判定</li>
969     <li>elseの判定</li>
970     <li>マクロコマンドの実行</li>
971     <li>識別子の判定</li>
972     <li>文法エラー(上記のいずれでもない場合)</li>
973     </ol></p>
974    
975      マクロコマンドかどうかは、GetReservedWord()で判別しています。_stricmp()で比較しているので、アルファベットの大文字・小文字は区別されません(case-insensitive)。マクロコマンドの場合は、TTLxxx() の関数を呼び出します。<br>
976      識別子の判定は、GetIdentifier() で行います。アルファベット(a-z, A-Z)および数値(0-9)、アンダースコア(_)から構成されるトークンを切り出します。トークンは32文字までです。トークンは「変数」として扱われます。左辺値に変数が来る場合は、「変数への代入」しかありえないので、その直後に「イコール(=)」があるかどうかを調べます。<br>
977      イコール以降の判定処理は、以下の順番となります。
978      
979     <p><ol>
980     <li>文字列の判定</li>
981     <li>計算式の判定</li>
982     </ol></p>
983    
984      文字列かどうかは GetString() で判定します。文字列は’か”でクォートされているため、取り出すのは容易です。<br>
985      計算式の判定は、GetExpression() で行います。ここでは再帰的下降法により、構文解析されます。<br>
986      左辺値が定義済みの変数かどうかは CheckVar() でチェックし、数値もしくは文字列をセットします。そうではない場合は NewStrVar() で、新しい変数として登録します。
987      
988    
989     <hr>
990    
991    
992    
993     <h2><a name="caret">キャレット制御</a></h2>
994     <h3>概要</h3>
995      ユーザが端末上でキーボード入力を行うと、カーソルが移動しますが、サーバからのエスケープシーケンスにより、キーボード入力なしにカーソルを移動させる必要があります。また、ウィンドウが非アクティブ状態の場合においても、カーソルを表示させることにより、ブロードキャストモードにおいて、複数端末の同時操作性を向上させています。
996     <br>
997    
998     <h3>システムキャレット</h3>
999      Tera Termにおけるカーソル描画には、システムキャレットを利用しています。Tera Termで使用されているシステムキャレットを制御するAPIを以下に示します。
1000    
1001     <p><ul>
1002     <li>CreateCaret</li>
1003     <li>DestroyCaret</li>
1004     <li>GetCaretBlinkTime</li>
1005     <li>HideCaret</li>
1006     <li>SetCaretBlinkTime</li>
1007     <li>SetCaretPos</li>
1008     <li>ShowCaret</li>
1009     </ul></p>
1010    
1011      <a href="http://msdn.microsoft.com/library/ja/default.asp?url=/library/ja/jpwinui/html/_win32_createcaret.asp">CreateCaretのドキュメント</a>によると、
1012    
1013     <pre>
1014     システムは 1 つのキューにつき 1 つのキャレットを提供します。ウィンドウが
1015     キーボードフォーカスを備えているとき、またはアクティブな状態のときにだけ、
1016     キャレットを作成するべきです。また、キーボードフォーカスを失ったり非アク
1017     ティブになる前に、キャレットを破棄するべきです。
1018     </pre>
1019    
1020     とあるため、ウィンドウがアクティブになったタイミングで CreateCaret() を呼び出し、フォーカスが外れ、非アクティブになるタイミングで DestroyCaret() を呼び出す必要があることを意味しています。<br>
1021      キャレットの表示は CaretOn()#vtdisp.c で、消去は CaretOff()#vtdisp.c で実装されています。CaretOn()やCaretOff()が呼び出されるタイミングは、エスケープシーケンス処理 VTParse() の箇所以外にも、マウスボタンを押したときやウィンドウのリサイズを行っているときなどがあります。<br>
1022    
1023    
1024    
1025     <h3>非アクティブ時のカーソル表示</h3>
1026      ウィンドウが非アクティブの場合は、カーソルが消滅します。Windowsの上ではユーザが操作できうるウィンドウは1つであるため、システムキャレットも1つのみ用意されています。通常のオペレーションにおいては、この動作で問題がありません。<br>
1027      しかし、ブロードキャストモードを利用する場合、非アクティブのTera Termウィンドウに対して、コマンドを投入することになります。特に、viなどで複数の端末を同時操作するときは、カーソルが消えていると不都合があります。<br>
1028      そこで、ウィンドウが非アクティブの場合においても、カーソルを描画するようにしています。ただし、システムキャレットは使えないので、自前でカーソルを描画する必要があります。Tera Termのウィンドウが非アクティブの場合においても、リモートホストから送られてくるエスケープシーケンスを処理するためにメインエンジンは動いており、常にカーソル位置は更新されています。現在のカーソル位置は、CursorXとCursorYに設定されています。<br>
1029      非アクティブ時のカーソル表示は CaretKillFocus() で行っています。このときに表示されるカーソルを「ポリゴンカーソル」と呼んでいます。ts.VTColor[0] は Text color です。非アクティブ状態でカーソル位置が更新されるときは、以前に描いたカーソルを消す必要があるので、そのときは ts.VTColor[1] で表される Background color で再描画することで、以前のカーソルを消去しています。<br>
1030      Background colorでポリゴンカーソルを描画すると、ちょうどそのとき背景にあった文字の一部が欠けることがあります。そのため、その文字の再描画を行う必要があり、UpdateCaretKillFocus() で実現しています。当該関数では InvalidateRect() で WM_PAINT を送ることにより、文字の再描画を促しています。<br>
1031    
1032     <pre class=code>
1033     void CaretKillFocus(BOOL show)
1034     {
1035     int CaretX, CaretY;
1036     POINT p[5];
1037     HPEN oldpen;
1038     HDC hdc;
1039    
1040     DispInitDC();
1041     hdc = VTDC;
1042    
1043     CaretX = (CursorX-WinOrgX)*FontWidth;
1044     CaretY = (CursorY-WinOrgY)*FontHeight;
1045    
1046     p[0].x = CaretX;
1047     p[0].y = CaretY;
1048     p[1].x = CaretX;
1049     p[1].y = CaretY + FontHeight - 1;
1050     if (CursorOnDBCS)
1051     p[2].x = CaretX + FontWidth*2 - 1;
1052     else
1053     p[2].x = CaretX + FontWidth - 1;
1054     p[2].y = CaretY + FontHeight - 1;
1055     if (CursorOnDBCS)
1056     p[3].x = CaretX + FontWidth*2 - 1;
1057     else
1058     p[3].x = CaretX + FontWidth - 1;
1059     p[3].y = CaretY;
1060     p[4].x = CaretX;
1061     p[4].y = CaretY;
1062    
1063     if (show) { // ポリゴンカーソルを表示(非フォーカス時)
1064     oldpen = SelectObject(hdc, CreatePen(PS_SOLID, 0, ts.VTColor[0]));
1065     } else {
1066     oldpen = SelectObject(hdc, CreatePen(PS_SOLID, 0, ts.VTColor[1]));
1067     }
1068     Polyline(VTDC, p, 5);
1069     oldpen = SelectObject(hdc, oldpen);
1070     DeleteObject(oldpen);
1071    
1072     DispReleaseDC();
1073     }
1074     </pre>
1075    
1076    
1077     <h3>非アクティブ時のカーソル表示タイミング</h3>
1078      非アクティブ時のカーソル表示のタイミングは、いくつかのパターンがあるため、漏れなく対処しておく必要があります。表示タイミングとしては以下のとおりです。
1079    
1080     <p>
1081     <ul>
1082     <li>ウィンドウがアクティブ(Active == TRUE)の場合は、ポリゴンキャレット描画関数(CaretKillFocus)を一切呼ばないようにする。</li>
1083     <li>CaretOn()では、非アクティブ(Active == FALSE)の場合、ShowCaret()を呼ぶタイミングで、ポリゴンキャレット描画関数(true)を呼ぶ。</li>
1084     <li>CaretOff()では、非アクティブ(Active == FALSE)の場合、HideCaret()を呼ぶタイミングで、ポリゴンキャレット描画関数(false)を呼ぶ。</li>
1085     <li>IsCaretOn()の判定論理に、(!Active && (CaretStatus==0)) のORを追加する。</li>
1086     <li>ChangeCaret()は何もしない</li>
1087     <li>WM_KILLFOCUSされるタイミングでは、IsCaretOn()が真であれば、ポリゴンキャレット描画関数(true)を呼ぶ。</li>
1088     <li>WM_ACTIVEされるタイミングでは、IsCaretOn()が真であれば、ポリゴンキャレット描画関数(false)を呼ぶ。</li>
1089     </ul>
1090     </p>
1091    
1092     <br>
1093    
1094     <hr>
1095    
1096    
1097    
1098     </BODY>
1099     </HTML>

Back to OSDN">Back to OSDN
ViewVC Help
Powered by ViewVC 1.1.26