これは、インターネット上の別の記事「長い散歩」であり、再び、著者として、私は再投稿します。 ここで役に立つと思います。
はじめに
この記事では、FTPプロトコルに関連するすべてのRFCを改定するという目標を設定していませんが、その中にはさらに多くの情報があります。FTPプロトコルと、クライアントからそれを操作するための基本的なテクニックについて、一般的な用語で紹介するだけです。
FTPの概要
したがって、FTP(ファイル転送プロトコル)はTCP / IPネットワークのファイル転送プロトコルです。 このプロトコルは、クライアントとサーバー間のファイル転送アルゴリズムのプログラミングを容易にし、標準化するために特別に作成されました。 すべての高レベルプロトコルと同様に、データを直接送信することはありません(これは低レベルプロトコル-TCP、および以下のプロトコルによって行われます)が、「通信」クライアントサーバーの方法のみを説明します。
プロトコルの説明に直接進みます。 その特徴的な機能は、サーバーとクライアント間の2つの接続の使用です。 1つの接続(コマンドまたはコントロール)を使用して、サーバーにコマンドを送信し、これらのコマンドに対する応答を受信します。 2番目の接続(データ接続)は、データの送受信に直接使用されます。 制御接続は常にクライアントからサーバーポート21に発生し、セッション全体を通して開いたままになります。 データ接続は、データを受信または受信するために必要に応じて開いたり閉じたりします。
制御接続が確立された後、クライアントはそれを介してサーバーにさまざまなコマンドを送信できます。 各コマンドは3つまたは4つの大文字のASCII文字で構成され、その後に1つ以上のスペースが続きます。一部のコマンドにはオプションの引数があります。 コマンドはCR、LFのペアで終了します-これは間違いなくすべての0dh、0ahに知られています-DOS / Windowsの場合です。 一般的に、コマンドスキームは次のとおりです。
コマンド[引数(s)] CR、LF。合計で、サーバーに送信できるコマンド(RFC959-33)は30を超えていますが、これはサーバーがそれらすべてをサポートするという意味ではありません。 最も頻繁に使用されるコマンドの例を示します。
ユーザー名ユーザー名を指定します
パスワードを渡すユーザーのパスワードを指定します
LISTファイルリストファイルリストリクエスト
ポートn1、n2、n3、n4、n5、n6データ接続用のIPとポートを指定する
RETRファイル名サーバーからファイルを取得する
STORファイル名サーバーにファイルを置く
TYPEタイプ送信されるデータのタイプ
やめてサーバーから切断する
アボー前のコマンドをキャンセルします。 データ転送の終了。
要求を受信すると、サーバーは同じ制御接続を介して応答を送信します。 サーバー応答は、ASCII形式の3文字(数字)で構成され、その後にオプションのテキストが続き、通常は数字の応答コードを説明し、その後に変更されていないCR、LFが続きます。 たとえば、答えは次のとおりです。226 File send OK。 -この例では、サーバーは、ファイルがその側から送信されたことを示しています(これは、クライアントからすでに受信されているという意味ではありません)。 サーバーの応答の1桁目が最も重要であり、コマンドがどのように実行された(または失敗した)かを明確に示しています。 値は次のとおりです。
- 1xxコマンドは実行中です。次のコマンドを発行する前に別のメッセージを待つ必要があります。
- 2xxチームが完了しました。 サーバーは次のサーバーを待っています。
- 3xxコマンドは完了しましたが、続行するにはもう1つのコマンドが必要です
- 4xxコマンドは完了しませんでした。待機してコマンドを繰り返す必要があります
- 5xxコマンドは実行されておらず、繰り返し実行しても実行されません。
応答の2桁目では、どの状況が応答につながったかを判断できます。
- x0x構文エラー。
- x1x情報。
- x2x応答は、マネージャーまたはデータ接続の状態を指します。
- x3x応答は、ユーザー認証または予算ステータスを指します。
- x4x間違いありません。
- x5x応答は、ファイルシステムの状態を指します。
最後に、応答の3桁目に追加情報が含まれます。
サーバーは1つの応答でほとんどのコマンドに応答しますが、サーバーがいくつかの応答を生成するために広く使用されているコマンドもあるという事実に特に注意を払う必要があります。 この場合、最初の応答の最初の数字は「1」、つまり 上記の表を見ると、サーバーは、次のコマンドを送信する前に、サーバーからの別のメッセージを待つ必要があることを伝えます。 このようなコマンドの例はRETRコマンドです。サーバーがそれを受信し、データの送信を開始すると、「150 HIDE.ASMのBINARYモードデータ接続を開く(958バイト)」などのメッセージを返します。メッセージの意味は「データ転送が開始されました」 「。 次に、データが既に送信されている場合(ただし、クライアントがそれを受信したという事実ではなく、注意を払いたい)、彼は制御接続を介して別の応答「226 File send OK」を送信します。 「ファイルが送信されました。」 ただし、この場合、2番目のメッセージを受信した後にのみ、サーバーは次のコマンドを実行する準備ができます。 最後のメッセージの代わりに、「4」で始まるエラーメッセージが表示される場合があります-ファイル転送に問題がある場合。
一般的に言えば、これは制御接続に関するものです。
次に、データ接続について説明します。 前述のように、データ接続は必要に応じて編成され、データの送信または受信後に毎回閉じます。 これは、クライアントとサーバー間のデータ転送モードがストリーミングであり、このモードではデータ転送の終わりが接続を閉じるためです。 上記から、1つの重要な結論を出す必要があります。接続を閉じることで、サーバーからのデータ転送の終了を判断できます。
通常、データ接続は次のように開かれます。
- クライアントは、ホスト上の空きポートを選択し、そのポートでパッシブオープンを実行します。
- クライアントは、制御接続を介してサーバーに、IPアドレスと、パッシブオープニングを行ったポート番号を伝えます。
- ポートとIPアドレスを受信すると、サーバーはそれをアクティブに開きます。
- データが送信または受信されます。
- 誰がデータを送信し、誰がデータを受信するかに応じて、ポートは閉じられます。
小さな余談:2番目の段落を注意深く読んだ場合、「サーバーにダミーのアドレスとポートを与えるとどうなりますか?」という疑問が生じるかもしれません。 答えはあいまいで、サーバーはIPアドレスをチェックできますが、これは常に発生するわけではないため、ダミーアドレスを使用する興味深い「トラブル」がいくつかあります。
クライアントがデータ接続用に選択したポートについて。 通常、動的に割り当てられたOSポートが使用されます。 システムに要求が行われ、最初の無料の要求が与えられます。 クライアントがサーバーへの接続用のポートを指定しない場合、制御接続が行われたポートで発生します(これは推奨されません)。 サーバーは常にポート20からデータを接続します。
これがすべて、データ接続についてお話ししたかった主なことです。
両方の接続がなぜ、どのように機能するかがわかったので、もう1点注意しておきます(最初の読みはスキップできます)。 LISTコマンドは、現在のディレクトリ内のファイルのリストを返し、データ接続ごとに返します。 リストは、CR、LFの文字で終わるASCII文字列のセットです。 各行には、要求されたカタログの要素の1つに関する情報が含まれています。 この行の一般的なパターンは次のとおりです。
Txxxxxxxxx [] uk []ユーザー[]グループ[]サイズ[] mm [] dd [] yytt []名前CR、LFどこで
T-要素のタイプ(「d」-ディレクトリ、「-」-ファイル、「l」-リンクなど);
xxxxxxxxx-ファイル保護属性。
user-ユーザー、ファイル所有者。
group-所有者グループ。
size-要素のサイズ。
mm-「jul」など、テキスト形式で要素を作成した月。
dd-アイテムが作成された月の日。
yytt-アイテムが作成された年または時刻です。
name-要素の名前(ファイル、ディレクトリ、リンク);
[] -1つ以上のスペース。
はい、これらの要素の間に異なる数のスペースが存在する可能性があります。異なるサーバー実装では、重要な列を1つ残していることに感謝する必要があります。したがって、ファイルテーブルを分析するとき、これを考慮する必要があります。 また、テーブルの最初の行がカタログの最初の要素に関する情報を保持する重要な行であるとは限らないことを考慮することも価値があります。 FTPサーバーの一部の実装(たとえば、FreeBSDのftpd)では、リストの最初の行は「total NN」という行です。
これはどのように機能しますか?
少し脱線して、「内部から」ファイルを受信するFTPセッションがどのように見えるかを見てみましょう。 そこで、クライアントを起動します。 この時点でサーバーはすでに受動的に開かれており、21番目のポートでリッスンしています。 まず、制御接続を作成する必要があります-ポート21でサーバーに接続します。次に何をしますか? 作成された制御接続を介してサーバーに正常にログインすると、サーバーから挨拶を受け取ります。これは、「Alt Linux 2.2の220 VSFTPデーモンベース、Shpakovsky」のようなものになります。
次のステップは登録です-匿名サーバーに接続するとしましょう-制御接続を介して、クライアントはサーバーにUSER匿名コマンドを送信し、サーバーが匿名ユーザーをサポートしている場合、「331パスワードを指定してください」-「パスワードを入力してください」サーバーの応答の数字「3」は、続行するにはさらにコマンドが必要であることを意味します。これは、クライアントが実際に行うことです-PASS 1 @ 1コマンドを送信します-ダミーの電子メールをパスワードとして指定します サーバーの応答を取得するもの«230ログイン成功。 「-」登録が成功しました。
これで、アクションは必要なものに依存するようになりました。前述のように、サーバーからファイルを取得します。たとえば、サーバーのルートディレクトリにあるファイル「HIDE.EXE」にします。 サーバーにデータを送受信する前に、どのタイプのデータを送信するかを示す必要があります。これはTYPE Nコマンドで行います。タイプがASCIIの場合はN = "A"、ファイルがバイナリの場合はN = "I"です。 クライアントは、サーバーにTYPE Iコマンドを送信します。このコマンドに対して、「200バイナリモードへの切り替え」という応答が返されます。
そのため、ファイルを取得するだけです。 これを行うには、クライアントはデータ接続を開く必要があります。 空きポートがクライアントによって選択され、パッシブオープニングが実行されます。 クライアントは彼に「耳を傾ける」。 次に、クライアントはサーバーに自身のIPアドレスとパッシブに開いたポート番号を伝える必要があります(クライアントのホストIPアドレスが10.21.23.10で、ポート番号が2000であると仮定します)。 クライアントは、PORT接続10,21,23,10,7,208-「どのような7,208?」-コントロール接続を介してサーバーに送信します-あなたは尋ねます。 これはポート番号です。このように構築されます-7 * 256 + 208 =2000。このコマンドを受信した後、サーバーは指定されたポートを積極的に開き、成功した場合、「200 PORTコマンドが成功しました。 PASVの使用を検討してください。 "。
すべて、データ接続が確立され、サーバーにデータを転送するコマンドを与えるために残ります。クライアントはこれを実行します-RETR HIDE.EXE。すべてに問題がなければ(ファイルが存在し、転送可能)、サーバーは「150 Opening BINARY mode data connection for HIDE.EXE」で応答します(4096バイト)。」そして、データ接続を介してファイルのマージを開始します。 繰り返しますが、答えの最初の桁に注意を向けます。 ファイルが完全に送信されると、サーバーは「226 File send OK」というメッセージを送信し、データ接続を閉じます。
クライアントは、サーバーからのメッセージの受信+データ接続を閉じることによって証明されるように、自分の側からのデータの受信の終了を待ち、微妙な違いがありますが、それらについては後で詳しく説明します)。
したがって、ファイルはクライアントによって受信され、制御接続を切断するために残り、クライアントはQUITコマンドを送信し、サーバーは「221 Goodbye」で応答し、切断します。
プロトコルに関する最も重要な理論情報を以下に示します。 練習を始める前に、telnetを使用してFTPサーバーへの制御接続を行うことを強くお勧めします。データ接続を作成することはできませんが、それらに対するコマンドと回答は表示されます。 また、いくつかのコンソールFTPクライアントで作業し、この間、何らかの種類のユーティリティを使用して接続の作成と終了を観察することをお勧めします。
実装。
次に、実装自体について説明します。 このクライアント実装では、非ブロッキング(非ブロッキング)ソケットを使用しているため、クライアントモデルはイベント駆動型です。 クライアントは、対応するイベントが発生したときにクライアントが使用するソケットに関する特定のアクションのみを実行します(たとえば、接続のクローズ、データの受信の通知など)。 イベントとして、メインウィンドウプロシージャに到達するメッセージが使用されます。 さらに、プログラムモデルはストリーミングであり、ストリームはデータ接続の読み取りに使用され、ストリームは制御接続の読み取りに使用され、メインクライアントストリームは「接続」ボタンがクリックされたときに開始されます。 これらの3つのスレッド(およびメインウィンドウメッセージプロシージャ)の動作を同期するためにプログラムがマルチスレッド化されているため、「イベント」が使用され(「イベント」)、プログラムで使用されるこれらのイベントをセンサー1または0として混同しないでください-イベントが発生したか、発生していないメインウィンドウプロシージャに来るソケット)。
それでは始めましょう。 メインアプリケーションウィンドウを作成するとき、プログラムのメイン初期化を実行し、主なポイントを説明します。
call VirtualAlloc,ebx,1024000,MEM_COMMIT+MEM_RESERVE,PAGE_READWRITE
mov ReciveDataBufferOffset,eax
call VirtualAlloc,ebx,10240,MEM_COMMIT+MEM_RESERVE,PAGE_READWRITE
mov ReciveCommandBufferOffset,eax
(1 ) (10 ).
call CreateEventA,ebx,ebx,ebx,ebx
mov HDataReciveEvent,eax
……
event () .
call CreateThread,ebx,ebx,offset ReciveThread,offset ReciveDataThreadStruc, \
NORMAL_PRIORITY_CLASS,offset ThreadID_data
call CreateThread,ebx,ebx,offset ReciveThread,offset ReciveCommandThreadStruc,\
NORMAL_PRIORITY_CLASS,offset ThreadID_command
2 – , . , .
call gethostname, offset HostName,64
call gethostbyname,offset HostName
…..
mov PortInPort,esi
ret 0
上記の行の意味は、ホストのIPアドレスを取得し、少し変換して別の場所に書き込むことです。PORTコマンドを実行するにはホストアドレスが必要です。
これで初期化プロセスが完了し、プログラムはユーザーコマンドを待機する状態になります。 ユーザーが接続ボタンをクリックするとどうなるか見てみましょう。
メインウィンドウの手順では、アプリケーションのメインフローが作成されます。そのキーポイントを考慮してください。
最初に、データの受信に関連する変数を初期化し、ユーザーが入力した接続パラメーター(サーバー、パスワードなど)をダイアログボックスから取得します。 その後、サーバーとの制御接続を作成する必要があります。
- ;
call socket, AF_INET, SOCK_STREAM, IPPROTO_TCP
mov ReciveCommandSock,eax
- ,
,
.
call WSAAsyncSelect, ReciveCommandSock, newhwnd, WM_COMMANDSOCK,FD_READ+FD_CONNECT
-
…..
call connect,ReciveCommandSock,offset sockaddr_in,16
- FD_CONNECT,
call SetEvent,HWaitConnectEvent ,
,
5 , .
call WaitForSingleObject,HWaitConnectEvent,5000
call ResetEvent,HWaitConnectEvent
- , 5
, - . WaitAnswerRecive .
call WaitAnswerRecive,5000
or eax,eax
jnz errorwithregisration
-関数への入力パラメーターは、関数が実行される間隔です
指定された間隔で応答が受信されない場合、サーバーの応答を待つ
エラーメッセージを表示し、eaxレジスタのゼロ以外の値で終了します。
WaitAnswerRecive proc TimeToWait:dword
call WaitForSingleObject,HWaitCommandEvent,TimeToWait
- HWaitCommandEvent,
, .
or eax,eax
jz NoTimeOutGet
call MessageBoxA,newhwnd,offset ErrTimeOutCommand,offset ErrorCap,40h
call ResetEvent,HWaitCommandEvent
- HWaitCommandEvent .. ,
.
NoTimeOutGet:
ret
WaitAnswerRecive endp
上記のように、これらのストリームはメインウィンドウを初期化するプロセスで作成され、常に新しいデータを待機しているプロセスです。ストリームは、新しいデータがあるというメッセージ、マネージャーへのメッセージを受信するとメインウィンドウプロシージャでアクティブになりますメインスレッドの最初の部分でWSAAsyncSelect関数を使用して接続を定義しました。データ接続のメッセージは、後で説明するように、この接続の作成時に決定されます。
制御およびデータ接続でデータを受信するための普遍的な取引を以下に示します。
- ReciveDataThreadStruc
ReciveCommandThreadStruc .
ReciveCommandThreadStruc :
- ;
HCommandReciveEvent dd ?
- , ;
HWaitCommandEvent dd ?
- ;
ReciveCommandBufferOffset dd ?
- ;
BytesCommandRecived dd 0
- , ;
ReciveCommandSock dd ?
ReciveThread proc parametr:dword
mov edi,parametr
InfinityLoop:
- , ;
call WaitForSingleObject,dword ptr [edi],-1
- esi , - +
;
mov esi,[edi+8]
add esi,[edi+12]
- 4096 ;
call recv,dword ptr [edi+16],esi,4096,0
- , ;
add [edi+12],eax
- ebx , , ;
mov ebx,[edi+4]
-
, -
;
cmp edi,offset ReciveDataThreadStruc
je comparefordata
-
0dh, 0ah, ;
mov eax,[edi+12]
mov esi,[edi+8]
cmp byte ptr [esi+eax-1],10
je short CallEvent
jmp InfinityLoop
comparefordata:
- , = ;
mov eax,[edi+12]
cmp FileLenght,eax
jne InfinityLoop
CallEvent:
- ;
call SetEvent,ebx
jmp InfinityLoop
ReciveThread endp
メインスレッドに戻って、サーバーからの応答を正常に受信しました。コマンドを受信する準備ができているので、コマンドを送信できるようになりました。この実装では、SendCommandInSocket関数はサーバーにコマンドを送信し、この関数を呼び出してこの関数を呼び出しますサーバーは、USER、PASS、TYPE、CWD、PORT、LISTの順にコマンドを実行します。 関数自体は次のようになります。
- , , ,
;
SendCommandInSocket proc uses ebx ecx esi edi, hSocket:dword, OutBufOffset:dword
- ;
mov edi,OutBufOffset
push edi
mov eax,0ah
mov ecx,100
repne scasb
sub edi,OutBufOffset
mov ecx,edi
pop esi
push edi
- , ,
;
mov edi,ReciveCommandBufferOffset
add edi,BytesCommandRecived
rep movsb
pop edi
add BytesCommandRecived,edi
- ;
call send,hSocket,OutBufOffset,edi,ebx
- , WaitAnswerRecive;
mov eax,5001
Wait2Answer:
dec eax
push eax
call WaitAnswerRecive
or eax,eax
jnz ErrorProcessed
- , , ,
, ,
.
.
mov edi,ReciveCommandBufferOffset
mov ecx,BytesCommandRecived
dec ecx
dec ecx
add edi,ecx
mov al,0ah
std
repne scasb
cld
xor eax,eax
- ;
mov cl,[edi+2]
cmp cl,'1'
- "1"
jz Wait2Answer
cmp cl,'3'
- "3" - ;
jna NoErrorProcessed
call MessageBoxA,newhwnd,edi,offset ErrorCap,40h
ErrorProcessed:
xor eax,eax
inc eax
NoErrorProcessed:
ret
SendCommandInSocket endp
もう1つ考慮すべき点は、PORTコマンドを送信する前に、リスニングソケットを作成する必要があることです。これを行うには、CreateListenSockプロシージャを呼び出します。
CreateListenSock proc
pushad
- ;
call socket, AF_INET, SOCK_STREAM, IPPROTO_TCP
mov datasock,eax
- - , ,
,
, ;
call WSAAsyncSelect, datasock, newhwnd, WM_DATASOCK, FD_ACCEPT+FD_READ+FD_CLOSE
- ;
mov sin_port,0 ; ,
;
mov sin_family,AF_INET
mov sin_addr,INADDR_ANY
call bind, datasock, offset sockaddr_in, 16
- ;
call getsockname,datasock,offset sockaddr_in,offset szSockaddr_in
- ;
xor eax,eax
mov ax,sin_port
call ntohs,eax
push eax
shr eax,8
- ASCII;
call DECtoASCII,eax,PortInPort
- PORT
mov al,','
stosb
pop eax
and eax,0ffh
call DECtoASCII,eax,edi
mov ax,0a0dh
stosw
mov esi,PortInPort
- ;
call listen, datasock, 1
popad
ret
CreateListenSock endp
したがって、最後に送信されたコマンドはLISTコマンドであり、現在のディレクトリ内のファイルのリストがデータ接続に到達する必要があるため、メッセージを送信した後、このリストを取得するまで待機する必要があります。 サーバーがすべてのデータの送信が正常に完了したことを示すメッセージを送信した場合でも、これはストリームがすべて機能し、すべてを受信したことを意味するものではないため、WaitTransferComplete関数が受信を終了することを期待しています。
-
, , .
WaitTransferComplete proc uses ecx edi, TimeToWaitEndTransfer:dword
WaitProgress:
- ,
;
call WaitForSingleObject,HWaitCloseEvent,-1
- , ,
;
call WaitForSingleObject,HWaitDataEvent,TimeToWaitEndTransfer
or eax,eax
jz CloseDataSocks
- , , , ..
, ,
;
cmp TimeToWaitEndTransfer,1000 ;
jz CloseDataSocks
call MessageBoxA,newhwnd,offset ErrTimeOutCommand,offset ErrorCap,40h
CloseDataSocks:
- ;
call ResetEvent,HWaitDataEvent
- ;
call closesocket,ReciveDataSock
call closesocket,datasock
ret
WaitTransferComplete endp
上記の手順が正常に完了した場合、ディレクトリテーブルはデータ受信バッファにあります。 プログラムの下で、結果のテーブルを処理し、見つかったすべてのファイルを順番に受け取ります。ファイルを受け取ることはディレクトリを受け取ることと同じなので、ここでは説明しません。 すべてのファイルを受信して保存したら、制御接続を閉じてストリームを終了します。
おわりに
もちろん、クライアント側でFTPプロトコルを使用する基本原則を調べましたが、このタスクのすべての側面が影響を受けることはありませんでした。 たとえば、ファイルをサーバーに送信することは考慮されていませんでしたが、添付のソースコードと同様に上記の資料を慎重に検討したので、問題なくこれを行うことができます。サーバーからのFTPプロトコルのさらなる研究を「宿題」にしましょう。