概要

Winsockを用い、FTPサーバーへパッシブモードで接続しファイルの一覧を取得しファイルftplist.txtに保存するプログラムである。
SSLは用いていないので、ユーザー名およびパスワードは平文のまま送付されるので注意が必要である。

テスト環境

コンパイラ

Visual C++ 2008/2013 Express 32/64bit マルチバイト/UNICODE

実行環境

telnetやFFFTPによるサーバ・ファイアウォールの動作の確認の他、以下の3つの環境で動作確認を行った。
項目 環境1 環境2 環境3
サーバOS クライアントと同一 Windows 8.1 VirutalBox上のWindows XP
サーバーIP クライアントと同一 192.168.0.120 192.168.0.200
FTP FileZilla FileZilla Tiny FTP Daemon 0.52d
クライアントIP 127.0.0.1 192.168.0.250 192.168.0.250
クライアントOS Windows 7 Windows 7 Windows 7

プログラムソースの概要

接続するサーバー名・ユーザー名・パスワード・ポート番号はプログラムソースの最初の方のマクロにより設定しています。
ご自身の環境に合わせて修正後コンパイルしてください。
本ページのプログラムソースでは以下のように設定しています。
設定項目 マクロ名 プログラムソース上での定義
サーバー名 SERVER_NAME 127.0.0.1
ユーザー名 USER_NAME usename
パスワード PASSWORD password
ポート PORT (FTPの標準値) 21

Winsockのrecv関数

ソケットからデータを受信する関数です。
recv関数の第3引数はバッファのサイズを指定します。
戻り値は受信した文字数です。
データを文字列として扱い、文字列の終了を\0で扱う場合は、recv関数を呼び出す前にあらかじめバッファ全体を\0でクリアします。
文字列の最後に\0が必要となりますので第3引数にバッファサイズを指定すると文字列の終了がわからなくなるので、バッファサイズ-1を指定します。

main

Winsockの初期化

WSAStartup関数によりWinsockのライブラリを初期化します。

FTPサーバーに接続

gethostbyname関数によりホスト名が存在するかチェックします。
socket関数により制御用のソケットを作成します。
connects関数によりFTPサーバーに接続します。
サーバーからのメッセージを受信し、先頭の文字が220以外の場合はエラーですので、WAS_ERROR_MSGマクロを呼び出し、エラーメッセージを表示し、ソケットを閉じ、Winsockをクリーンアップ後、プログラムを終了します。

FTPサーバーにログイン

send関数を使用してFTPサーバーへUSERコマンドを送信しログインするユーザー名を知らせます。以下が送信する文字列の例です。
USER ユーザー名\n\r
上記のユーザー名は実際にログイン時に使用するユーザー名に置き換えてください。
recv関数を使用してFTPサーバーからのメッセージを受信し、先頭文字列が331以外であればユーザー名は正常でないので、WAS_ERROR_MSGマクロを呼び出し、エラーメッセージを表示し、ソケットを閉じ、Winsockをクリーンアップ後、プログラムを終了します。
send関数を使用してFTPサーバーへPASSコマンドを送信しログインするパスワードを知らせます。以下が送信する文字列の例です。
PASS パスワード\n\r
上記のパスワードは実際にログイン時に使用するパスワードに置き換えてください。
recv関数を使用してFTPサーバーからのメッセージを受信し、先頭文字列が230以外であればパスワードは正常でないので、WAS_ERROR_MSGマクロを呼び出し、エラーメッセージを表示し、ソケットを閉じ、Winsockをクリーンアップ後、プログラムを終了します。

FTPサーバーのカレントディレクトリを指定

send関数を使用してFTPサーバーへCWDコマンドを送信しカレントディレクトリを指定します。以下が送信する文字列の例です。
CWD ディレクトリ\n\r
上記のディレクトリがFTPクライアントの操作対象となるカレントディレクトリになります。./を指定した場合、公開される最上位のディレクトリを示します。
recv関数を使用してFTPサーバーからのメッセージを受信し、先頭文字列が250以外であればエラーが発生しています。

パッシブモードを指定してデータセッション用のアドレスとポート番号を取得する

パッシブモードの概要
FTPサーバーがデーターセッション用のアドレスとポート番号をクライアントに教えて、クライアント側からサーバーへ接続する方法です。
データーセッション用のアドレスとポート番号はFTPサーバーが決め、クライアント側から接続します。
ファイアーウォールは、相手に接続するのは寛容で、自分に接続されるのは嫌いますので、パッシブモードの場合、クライアント側は制御・データセッションともサーバへ接続する形態なので、クライアント側のファイアーウォールの設定が簡単となります。
アクティブモードの概要
パッシブモードと対となすのがアクティブモードです。
データーセッション用のアドレスとポート番号はクライアント側が指定し、FTPサーバーに通知します。
通知に従い、FTPサーバーがクライアントに接続します。
ファイアーウォール用はデーターセッションはクライアント側が接続される側なので、クライアント側で設定する必要があります。
パッシブモードの指定
send関数を使用してFTPサーバーへPASVコマンドを送信します。以下が送信する文字列の例です。
PASV\n\r
recv関数を使用してFTPサーバーからのメッセージを受信します。以下のように()内にアドレスとポート番号が受信されます。
227 Entering Passive Mode (127,0,0,1,251,231)
,で区切られた最初の4個の数字が、IPアドレス、残りの2個の数字がポート番号を示します。
上記の受信結果からIPアドレスとポート番号は以下のようになります。
IPアドレス 127.0.0.1
ポート番号 251*256+231=64487
数字を区切り1個ずつ数字を取り出すためにstrtok_s関数を連続して用います。

データーセッションへ接続

制御用のソケットと別の新しいソケットを作成し前項で取得されたIPアドレス、ポート番号を使用して、サーバーへ接続します。
接続の仕方は制御セッションと同一です。

ファイル一覧の取得するためにLISTコマンドを送信

send関数を使用してFTPサーバーへLISTコマンドを送信します。使用するソケットは制御用です。以下が送信する文字列の例です。
recv関数を使用してFTPサーバーからのメッセージを受信し、先頭文字列が150以外であればエラーが発生しています。以下が送信した文字列の例です。
150 Sending file list.

ファイル一覧を受信する

recv関数を使用してFTPサーバーからのデータセクション用のソケットよりファイルの一覧を受信します。
recv関数の戻り値が取得された文字数となります。recv関数の第3引数と同じ個数の文字列が取得された場合は、バッファがいっぱいでありまだ残りの文字列がある可能性があるので、再度recv関数を呼び出します。
recv関数によって取得された文字列はfwrite関数によりファイルの書き込みます。
ファイルの書き込みにfwrite関数を用いており、この関数は、データのバイト数を引数で指定できますので、recv関数の第3引数はバッファサイズをそのまま指定しています。 受信が終了したらデーターセクション用のソケットをclosesocket関数により閉じます。

制御用のセクションを閉じる

制御セクション用のソケットをclosesocket関数により閉じます。

Winsockライブラリを終了する

WSACleanup関数によりWinsockライブラリをクリーンアップします。

WSA_error_msg

この関数はWinsockのエラーコードを取得し、エラーコードに対するエラーメッセージを取得し標準出力に出力します。
WSAGetLastError関数によりWinsockのエラーコードを取得します。
FormatMessage関数によりエラーコードに対する文字列を取得します。
文字列のバッファはFormatMessage関数側で取得するように引数で指定していますので、バッファが不要になった時点で、LocalFree関数によりメモリを開放します。

プログラムソース

//	Winsockを使用してFTPサーバへPASVモードでアクセスしファイルの一覧を取得

//	以下のマクロの定義を使用される環境に応じて修正してください。

#define SERVER_NAME  "127.0.0.1"
#define USER_NAME  "username"
#define PASSWORD "password"
#define PORT 21	//	FTPサーバーポート番号

#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <locale.h>
#include <tchar.h>

#pragma comment(lib, "ws2_32.lib" )	//	winsockのライブラリ

char szFtpServer[256], szUserName[64], szPass[64];

static int __WSA_ERROR_CODE__=0;


//	Winsockのエラーコードに対応するメッセージを標準出力に表示する
void WSA_erro_msg(void);

//	データーコネクションへ接続
SOCKET FtpDataConnect(in_addr ipa, unsigned int port);

//	LISTコマンドの結果を保存
void FtpList(SOCKET s);

//	Winsockのエラーメッセージを表示する
#define WSA_ERROR_MSG(msg) { _tprintf(_TEXT("%s %d行:error code %d %s\n"),__FILE__,__LINE__, __WSA_ERROR_CODE__,msg); WSA_erro_msg();  _gettchar(); }


void _tmain(void){
	char szStr[256];	//	送信バッファ
	char szStrRcv[1024];	//	受信バッファ

	_tsetlocale(LC_ALL, _TEXT(""));	//	UNICODE文字を標準出力に表示できるよう設定

	//	FTPサーバーの定義
	strcpy_s(::szFtpServer, sizeof(::szFtpServer), SERVER_NAME);
	strcpy_s(::szUserName, sizeof(::szUserName), USER_NAME);
	strcpy_s(::szPass, sizeof(::szPass), PASSWORD);

	WSADATA wsaData;
	LPHOSTENT lpHost;

	SOCKET s;
	SOCKADDR_IN sockadd;

	//	Winsockの初期化
	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
		WSA_ERROR_MSG(_TEXT("関数WSAStartup:error"));
		return;
	}
	lpHost = gethostbyname(::szFtpServer);
	if (lpHost == NULL) {
		WSA_ERROR_MSG(_TEXT("関数gethostbyname:error"));
		return;
	}
	//	コントロールコネクション用のソケット作成
	s = socket(AF_INET, SOCK_STREAM, 0);
	if (s == INVALID_SOCKET) {
		WSA_ERROR_MSG(_TEXT("関数socket:error"));
		WSACleanup();
		return;
	}

	sockadd.sin_family = AF_INET;
	sockadd.sin_port = htons(PORT);	// ホストバイトオーダーをネットワークバイトオーダーに変換
	sockadd.sin_addr.S_un.S_addr = inet_addr(::szFtpServer);	//	サーバーのIPアドレス取得
	if (connect(s, (PSOCKADDR)&sockadd, sizeof(sockadd))) {	//	コントロールコネクションサーバーへ接続
		WSA_ERROR_MSG(_TEXT("関数connect:error"));
		closesocket(s);
		WSACleanup();
		return;
	}

	memset(szStrRcv, '\0', sizeof(szStrRcv));
	recv(s, szStrRcv, sizeof(szStrRcv)-1, 0);	//	サーバーからのメッセージを取得

	if (strncmp(szStrRcv, "220", 3) != 0){
		WSA_ERROR_MSG(_TEXT("error"));
		closesocket(s);
		WSACleanup();
		return;
	}
	//	ユーザー名を送信
	sprintf_s(szStr, sizeof(szStr), "USER %s\r\n", ::szUserName);
	send(s, szStr, (int)strlen(szStr), 0);
	memset(szStrRcv, 0, sizeof(szStrRcv));
	recv(s, szStrRcv, sizeof(szStrRcv)-1, 0);

	if (strncmp(szStrRcv, "331", 3) != 0){
		WSA_ERROR_MSG(_TEXT("error"));
		closesocket(s);
		WSACleanup();
		return;
	}

	//	パスワードを送信

	sprintf_s(szStr, sizeof(szStr), "PASS %s\r\n", ::szPass);
	send(s, szStr, (int)strlen(szStr), 0);
	memset(szStrRcv, 0, sizeof(szStrRcv));
	recv(s, szStrRcv, sizeof(szStrRcv)-1, 0);

	if (strncmp(szStrRcv, "230", 3) != 0){
		WSA_ERROR_MSG(_TEXT("error"));
		closesocket(s);
		WSACleanup();
		return;
	}

	//	サーバーのカレントディレクトリを指定
	sprintf_s(szStr, sizeof(szStr), "CWD ./\r\n");
	send(s, szStr, (int)strlen(szStr), 0);
	memset(szStrRcv, 0, sizeof(szStrRcv));
	recv(s, szStrRcv, sizeof(szStrRcv)-1, 0);

	//	PASVモードを指定
	sprintf_s(szStr, sizeof(szStr), "PASV\r\n");
	send(s, szStr, (int)strlen(szStr), 0);
	memset(szStrRcv, 0, sizeof(szStrRcv));
	//	データコネクション用のIPアドレスとポート番号を取得
	recv(s, szStrRcv, sizeof(szStrRcv)-1, 0);
	char* p = 0;
	in_addr ipa;
	unsigned long low, hi;
	if ((p = strchr(szStrRcv, '(')) != 0){
		char* next=0;
		char* tok = strtok_s(p, "(,)", &next);	//	1個目のトークン IPアドレス
		ipa.S_un.S_un_b.s_b1 = atoi(tok);
		tok = strtok_s(0, "(,)", &next);	//	2個目のトークン IPアドレス
		ipa.S_un.S_un_b.s_b2 = atoi(tok);
		tok = strtok_s(0, "(,)", &next);	//	3個目のトークン IPアドレス
		ipa.S_un.S_un_b.s_b3 = atoi(tok);
		tok = strtok_s(0, "(,)", &next);	//	4個目のトークン IPアドレス
		ipa.S_un.S_un_b.s_b4 = atoi(tok);
		tok = strtok_s(0, "(,)", &next);	//	5個目のトークン ポート番号上位
		hi = atoi(tok);
		tok = strtok_s(0, "(,)", &next);	//	6個目のトークン ポート番号下位
		low = atoi(tok);
	}
	//	データーコネクションサーバーへ接続
	SOCKET s2 = FtpDataConnect(ipa, hi * 256 + low);
	//	LISTコマンドを送信
	sprintf_s(szStr, sizeof(szStr), "LIST\r\n");
	send(s, szStr, (int)strlen(szStr), 0);
	memset(szStrRcv, '\0', sizeof(szStr));
	recv(s, szStrRcv, sizeof(szStrRcv)-1, 0);

	//	データーコネクションサーバーからLISTコマンドの結果を受け取る
	FtpList(s2);
	closesocket(s2);

	closesocket(s);
	WSACleanup();
	return;
}

//	データーコネクションサーバーへ接続

SOCKET FtpDataConnect(in_addr ipa, unsigned int port){
	SOCKET s;

	s = socket(AF_INET, SOCK_STREAM, 0);
	if (s == INVALID_SOCKET) {
		WSA_ERROR_MSG(_TEXT("関数socket:error"));
		return 0;
	}

	struct sockaddr_in sockadd = { AF_INET, htons(port) };
	sockadd.sin_addr = ipa;

	if (connect(s, (PSOCKADDR)&sockadd, sizeof(sockadd)) == -1) {
		WSA_ERROR_MSG(_TEXT("関数connect:error"));
		closesocket(s);
		return 0;
	}
	return s;
}

//	LISTコマンドの結果をファイルftplist.txtに保存

void FtpList(SOCKET s){
	char buf[16];

	FILE* fp;
	if (_tfopen_s(&fp, _TEXT("ftplist.txt"), _TEXT("w") )){
		_tprintf(TEXT("ファイルが開けません\n"));
		return;
	}
	int len;
	do{
		len = recv(s, buf, sizeof(buf), 0);
		fwrite(buf, sizeof(char), len, fp);
	} while (len == sizeof(buf));
	fclose(fp);
}


//	Winsockのエラーコードに対応するメッセージを標準出力に表示する
void WSA_erro_msg(void){
	__WSA_ERROR_CODE__ = WSAGetLastError();
	LPVOID lpMsgBuf;

	FormatMessage(
		FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM |
		FORMAT_MESSAGE_IGNORE_INSERTS,
		NULL,
		__WSA_ERROR_CODE__,
		MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // 既定の言語
		(TCHAR*)&lpMsgBuf,
		0,
		NULL
		);

	_tprintf(_TEXT("%s\n"), lpMsgBuf);

	// バッファを解放する。
	LocalFree(lpMsgBuf);
}

ソースファイル