Server
啟動FTP Serverpackage ftp;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
//FTP Server
public class Server {
private int controlPort = 8888; //接收指令埠
private ServerSocket welcomeSocket;
boolean serverRunning = true;
//啟動FTP Server
public static void main(String[] args) {
new Server();
}
public Server() {
try {
welcomeSocket = new ServerSocket(controlPort);
} catch (IOException e) {
System.out.println("無法建立網路服務");
System.exit(-1);
}
System.out.println("FTP服務建立在 " + controlPort);
int noOfThreads = 0;
while (serverRunning) {
try {
Socket client = welcomeSocket.accept();
//循序遞增連接埠給新連線傳輸檔案
int dataPort = controlPort + noOfThreads + 1;
Worker w = new Worker(client, dataPort);
System.out.println("收到請求建立連線");
noOfThreads++;
w.start();
} catch (IOException e) {
System.out.println("收到請求但連線失敗");
e.printStackTrace();
}
}
try {
welcomeSocket.close();
System.out.println("已停止服務");
} catch (IOException e) {
System.out.println("無法停止服務");
System.exit(-1);
}
}
}
Worker
FTP傳輸工作package ftp;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
//FTP傳輸工作
public class Worker extends Thread {
//TODO 使用者資料應採用認証服務機制
private userStatus currentUserStatus = userStatus.NOTLOGGEDIN;
private String validUser = "john";//單使用者帳號
private String validPassword = "12345";//單使用者密碼
// 主要路徑
private String root;
private String currDirectory;
private String fileSeparator = "\\";
// 傳輸元件
private Socket controlSocket;
private PrintWriter controlOutWriter;
private BufferedReader controlIn;
private ServerSocket dataSocket;
private Socket dataConnection;
private PrintWriter dataOutWriter;
private int dataPort;
private transferType transferMode = transferType.ASCII;
private boolean quitCommandLoop = false;
public Worker(Socket client, int dataPort) {
super();
this.controlSocket = client;
this.dataPort = dataPort;
this.root = System.getProperty("user.home");// 取得server上的JVM使用者目錄
this.currDirectory = root + fileSeparator + "Desktop";// 嘗試存取桌面
}
public void run() {
try {
controlIn = new BufferedReader(new InputStreamReader(controlSocket.getInputStream()));
controlOutWriter = new PrintWriter(controlSocket.getOutputStream(), true);
sendMsgToClient("220 歡迎訊息....");
while (!quitCommandLoop) {
executeCommand(controlIn.readLine());
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
controlIn.close();
controlOutWriter.close();
controlSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
//執行指令
private void executeCommand(String c) {
int index = c.indexOf(' ');
String command = ((index == -1) ? c.toUpperCase() : (c.substring(0, index)).toUpperCase());
String args = ((index == -1) ? null : c.substring(index + 1, c.length()));
switch (command) {
case "USER":handleUser(args);break;
case "PASS":handlePass(args);break;
case "CWD":handleCwd(args);break;
case "LIST":handleNlst(args);break;
case "NLST":handleNlst(args);break;
case "PWD":handlePwd();break;
case "QUIT":handleQuit();break;
case "PASV":handlePasv();break;
case "EPSV":handleEpsv();break;
case "SYST":handleSyst();break;
case "FEAT":handleFeat();break;
case "PORT":handlePort(args);break;
case "EPRT":handlePort(parseExtendedArguments(args));break;
case "RETR":handleRetr(args);break;
case "MKD":handleMkd(args);break;
case "RMD":handleRmd(args);break;
case "TYPE":handleType(args);break;
case "STOR":handleStor(args);break;
default:sendMsgToClient("501 Unknown command");break;
}
}
//一般訊息
private void sendMsgToClient(String msg) {
controlOutWriter.println(msg);
}
//狀態訊息
private void sendDataMsgToClient(String msg) {
if (dataConnection == null || dataConnection.isClosed()) {
sendMsgToClient("425 No data connection was established");
} else {
dataOutWriter.print(msg + '\r' + '\n');
}
}
private void openDataConnectionPassive(int port) {
try {
dataSocket = new ServerSocket(port);
dataConnection = dataSocket.accept();
dataOutWriter = new PrintWriter(dataConnection.getOutputStream(), true);
} catch (IOException e) {
e.printStackTrace();
}
}
private void openDataConnectionActive(String ipAddress, int port) {
try {
dataConnection = new Socket(ipAddress, port);
dataOutWriter = new PrintWriter(dataConnection.getOutputStream(), true);
} catch (IOException e) {
e.printStackTrace();
}
}
private void closeDataConnection() {
try {
dataOutWriter.close();
dataConnection.close();
if (dataSocket != null) {
dataSocket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
dataOutWriter = null;
dataConnection = null;
dataSocket = null;
}
private void handleUser(String username) {
if (username.toLowerCase().equals(validUser)) {
sendMsgToClient("331 User name okay, need password");
currentUserStatus = userStatus.ENTEREDUSERNAME;
} else if (currentUserStatus == userStatus.LOGGEDIN) {
sendMsgToClient("530 User already logged in");
} else {
sendMsgToClient("530 Not logged in");
}
}
private void handlePass(String password) {
if (currentUserStatus == userStatus.ENTEREDUSERNAME && password.equals(validPassword)) {
currentUserStatus = userStatus.LOGGEDIN;
sendMsgToClient("230-Welcome");
sendMsgToClient("230 User logged in successfully");
}else if (currentUserStatus == userStatus.LOGGEDIN) {
sendMsgToClient("530 User already logged in");
}else {
sendMsgToClient("530 Not logged in");
}
}
private void handleCwd(String args) {
String filename = currDirectory;
if (args.equals("..")) {
int ind = filename.lastIndexOf(fileSeparator);
if (ind > 0) {
filename = filename.substring(0, ind);
}
}else if ((args != null) && (!args.equals("."))) {
filename = filename + fileSeparator + args;
}
File f = new File(filename);
if (f.exists() && f.isDirectory() && (filename.length() >= root.length())) {
currDirectory = filename;
sendMsgToClient("250 The current directory has been changed to " + currDirectory);
} else {
sendMsgToClient("550 Requested action not taken. File unavailable.");
}
}
private void handleNlst(String args) {
if (dataConnection == null || dataConnection.isClosed()) {
sendMsgToClient("425 No data connection was established");
} else {
String[] dirContent = nlstHelper(args);
if (dirContent == null) {
sendMsgToClient("550 File does not exist.");
} else {
sendMsgToClient("125 Opening ASCII mode data connection for file list.");
for (int i = 0; i < dirContent.length; i++) {
sendDataMsgToClient(dirContent[i]);
}
sendMsgToClient("226 Transfer complete.");
closeDataConnection();
}
}
}
private String[] nlstHelper(String args) {
String filename = currDirectory;
if (args != null) {
filename = filename + fileSeparator + args;
}
File f = new File(filename);
if (f.exists() && f.isDirectory()) {
return f.list();
} else if (f.exists() && f.isFile()) {
String[] allFiles = new String[1];
allFiles[0] = f.getName();
return allFiles;
} else {
return null;
}
}
private void handlePort(String args) {
String[] stringSplit = args.split(",");
String hostName = stringSplit[0] + "." + stringSplit[1] + "." + stringSplit[2] + "." + stringSplit[3];
int p = Integer.parseInt(stringSplit[4]) * 256 + Integer.parseInt(stringSplit[5]);
openDataConnectionActive(hostName, p);
sendMsgToClient("200 Command OK");
}
private void handlePwd() {
sendMsgToClient("257 \"" + currDirectory + "\"");
}
private void handlePasv() {
String myIp = "127.0.0.1";
String myIpSplit[] = myIp.split("\\.");
int p1 = dataPort / 256;
int p2 = dataPort % 256;
sendMsgToClient("227 Entering Passive Mode (" + myIpSplit[0] + "," + myIpSplit[1] + "," + myIpSplit[2] + ","+ myIpSplit[3] + "," + p1 + "," + p2 + ")");
openDataConnectionPassive(dataPort);
}
private void handleEpsv() {
sendMsgToClient("229 Entering Extended Passive Mode (|||" + dataPort + "|)");
openDataConnectionPassive(dataPort);
}
private void handleQuit() {
sendMsgToClient("221 Closing connection");
quitCommandLoop = true;
}
private void handleSyst() {
sendMsgToClient("215 COMP4621 FTP Server Homebrew");
}
private void handleFeat() {
sendMsgToClient("211 END");
}
private void handleMkd(String args) {
if (args != null && args.matches("^[a-zA-Z0-9]+$")) {
File dir = new File(currDirectory + fileSeparator + args);
if (!dir.mkdir()) {
sendMsgToClient("550 Failed to create new directory");
} else {
sendMsgToClient("250 Directory successfully created");
}
} else {
sendMsgToClient("550 Invalid name");
}
}
private void handleRmd(String dir) {
String filename = currDirectory;
if (dir != null && dir.matches("^[a-zA-Z0-9]+$")) {
filename = filename + fileSeparator + dir;
File d = new File(filename);
if (d.exists() && d.isDirectory()) {
d.delete();
sendMsgToClient("250 Directory was successfully removed");
} else {
sendMsgToClient("550 Requested action not taken. File unavailable.");
}
} else {
sendMsgToClient("550 Invalid file name.");
}
}
private void handleType(String mode) {
if (mode.toUpperCase().equals("A")) {
transferMode = transferType.ASCII;
sendMsgToClient("200 OK");
} else if (mode.toUpperCase().equals("I")) {
transferMode = transferType.BINARY;
sendMsgToClient("200 OK");
} else
sendMsgToClient("504 Not OK");
}
private void handleRetr(String file) {
File f = new File(currDirectory + fileSeparator + file);
if (!f.exists()) {
sendMsgToClient("550 File does not exist");
}else {
if (transferMode == transferType.BINARY) {
BufferedOutputStream fout = null;
BufferedInputStream fin = null;
sendMsgToClient("150 Opening binary mode data connection for requested file " + f.getName());
try {
fout = new BufferedOutputStream(dataConnection.getOutputStream());
fin = new BufferedInputStream(new FileInputStream(f));
} catch (Exception e) {
e.printStackTrace();
}
byte[] buf = new byte[1024];
int l = 0;
try {
while ((l = fin.read(buf, 0, 1024)) != -1) {
fout.write(buf, 0, l);
}
} catch (IOException e) {
e.printStackTrace();
}
try {
fin.close();
fout.close();
} catch (IOException e) {
e.printStackTrace();
}
sendMsgToClient("226 File transfer successful. Closing data connection.");
}else {
sendMsgToClient("150 Opening ASCII mode data connection for requested file " + f.getName());
BufferedReader rin = null;
PrintWriter rout = null;
try {
rin = new BufferedReader(new FileReader(f));
rout = new PrintWriter(dataConnection.getOutputStream(), true);
} catch (IOException e) {
e.printStackTrace();
}
String s;
try {
while ((s = rin.readLine()) != null) {
rout.println(s);
}
} catch (IOException e) {
e.printStackTrace();
}
try {
rout.close();
rin.close();
} catch (IOException e) {
e.printStackTrace();
}
sendMsgToClient("226 File transfer successful. Closing data connection.");
}
}
closeDataConnection();
}
private void handleStor(String file) {
if (file == null) {
sendMsgToClient("501 No filename given");
} else {
File f = new File(currDirectory + fileSeparator + file);
if (f.exists()) {
sendMsgToClient("550 File already exists");
}else {
if (transferMode == transferType.BINARY) {
BufferedOutputStream fout = null;
BufferedInputStream fin = null;
sendMsgToClient("150 Opening binary mode data connection for requested file " + f.getName());
try {
fout = new BufferedOutputStream(new FileOutputStream(f));
fin = new BufferedInputStream(dataConnection.getInputStream());
} catch (Exception e) {
e.printStackTrace();
}
byte[] buf = new byte[1024];
int l = 0;
try {
while ((l = fin.read(buf, 0, 1024)) != -1) {
fout.write(buf, 0, l);
}
} catch (IOException e) {
e.printStackTrace();
}
try {
fin.close();
fout.close();
} catch (IOException e) {
e.printStackTrace();
}
sendMsgToClient("226 File transfer successful. Closing data connection.");
}else {
sendMsgToClient("150 Opening ASCII mode data connection for requested file " + f.getName());
BufferedReader rin = null;
PrintWriter rout = null;
try {
rin = new BufferedReader(new InputStreamReader(dataConnection.getInputStream()));
rout = new PrintWriter(new FileOutputStream(f), true);
} catch (IOException e) {
e.printStackTrace();
}
String s;
try {
while ((s = rin.readLine()) != null) {
rout.println(s);
}
} catch (IOException e) {
e.printStackTrace();
}
try {
rout.close();
rin.close();
} catch (IOException e) {
e.printStackTrace();
}
sendMsgToClient("226 File transfer successful. Closing data connection.");
}
}
closeDataConnection();
}
}
private String parseExtendedArguments(String extArg) {
String[] splitArgs = extArg.split("\\|");
String ipAddress = splitArgs[2].replace('.', ',');
int port = Integer.parseInt(splitArgs[3]);
int p1 = port / 256;
int p2 = port % 256;
return ipAddress + "," + p1 + "," + p2;
}
private enum transferType {
ASCII, BINARY
}
private enum userStatus {
NOTLOGGEDIN, ENTEREDUSERNAME, LOGGEDIN
}
}
建立好的FTP Server可以用Windows內建的檔案總管檢視,但是下載客戶端軟體測試結果將較為準確。
大部份的FTP軟體對於UTF8的相容性極差,無論是客戶端或是伺服器端都有機會發生問題。本範例的客戶端以winscp為教具發生中文訊息的悲劇,因此所有訊息以原文輸出。
狀態碼
有興趣研究的人可以試著將狀態碼加入,並且嘗試更多的指令對應。private String getCodeMsg(String code) {
switch (code) {
case "100": return "The requested action is being initiated, expect another reply before proceeding with a new command.";
case "110": return "Restart marker replay . In this case, the text is exact and not left to the particular implementation; it must read: MARK yyyy = mmmm where yyyy is User-process data stream marker, and mmmm server's equivalent marker (note the spaces between markers and '=').";
case "120": return "Service ready in nnn minutes.";
case "125": return "Data connection already open; transfer starting.";
case "150": return "File status okay; about to open data connection.";
case "200": return "The requested action has been successfully completed.";
case "202": return "Command not implemented, superfluous at this site.";
case "211": return "System status, or system help reply.";
case "212": return "Directory status.";
case "213": return "File status.";
case "214": return "Help message. Explains how to use the server or the meaning of a particular non-standard command. This reply is useful only to the human user.";
case "215": return "NAME system type. Where NAME is an official system name from the registry kept by IANA.";
case "220": return "Service ready for new user.";
case "221": return "Service closing control connection.";
case "225": return "Data connection open; no transfer in progress.";
case "226": return "Closing data connection. Requested file action successful (for example, file transfer or file abort).";
case "227": return "Entering Passive Mode (h1,h2,h3,h4,p1,p2).";
case "228": return "Entering Long Passive Mode (long address, port).";
case "229": return "Entering Extended Passive Mode (|||port|).";
case "230": return "User logged in, proceed. Logged out if appropriate.";
case "231": return "User logged out; service terminated.";
case "232": return "Logout command noted, will complete when transfer done.";
case "234": return "Specifies that the server accepts the authentication mechanism specified by the client, and the exchange of security data is complete. A higher level nonstandard code created by Microsoft.";
case "250": return "Requested file action okay, completed.";
case "257": return "'PATHNAME' created.";
case "300": return "The command has been accepted, but the requested action is on hold, pending receipt of further information.";
case "331": return "User name okay, need password.";
case "332": return "Need account for login.";
case "350": return "Requested file action pending further information";
case "400": return "The command was not accepted and the requested action did not take place, but the error condition is temporary and the action may be requested again.";
case "421": return "Service not available, closing control connection. This may be a reply to any command if the service knows it must shut down.";
case "425": return "Can't open data connection.";
case "426": return "Connection closed; transfer aborted.";
case "430": return "Invalid username or password";
case "434": return "Requested host unavailable.";
case "450": return "Requested file action not taken.";
case "451": return "Requested action aborted. Local error in processing.";
case "452": return "Requested action not taken. Insufficient storage space in system.File unavailable (e.g., file busy).";
case "500": return "Syntax error, command unrecognized and the requested action did not take place. This may include errors such as command line too long.";
case "501": return "Syntax error in parameters or arguments.";
case "502": return "Command not implemented.";
case "503": return "Bad sequence of commands.";
case "504": return "Command not implemented for that parameter.";
case "530": return "Not logged in.";
case "532": return "Need account for storing files.";
case "534": return "Could Not Connect to Server - Policy Requires SSL";
case "550": return "Requested action not taken. File unavailable (e.g., file not found, no access).";
case "551": return "Requested action aborted. Page type unknown.";
case "552": return "Requested file action aborted. Exceeded storage allocation (for current directory or dataset).";
case "553": return "Requested action not taken. File name not allowed.";
case "600": return "Replies regarding confidentiality and integrity";
case "631": return "Integrity protected reply.";
case "632": return "Confidentiality and integrity protected reply.";
case "633": return "Confidentiality protected reply.";
case "10000": return "Common Winsock Error Codes[2] (These are not FTP return codes)";
case "10054": return "Connection reset by peer. The connection was forcibly closed by the remote host.";
case "10060": return "Cannot connect to remote server.";
case "10061": return "Cannot connect to remote server. The connection is actively refused by the server.";
case "10066": return "Directory not empty.";
case "10068": return "Too many users, server is full.";
default:
return "501 Unknown command";
}
}
private String getCodeMsg(String code) {
switch (code) {
case "110": return "重新啟動標記回覆。";
case "120": return "服務就緒,在 nnn 分鐘。";
case "125": return "資料連線已經開啟;傳輸開始。";
case "150": return "檔案狀態無誤;將開啟資料連線。";
case "200": return "指令已經沒有問題了。";
case "202": return "在這個站台的指令不實作、 多餘。";
case "211": return "系統狀態或系統說明回覆。";
case "212": return "目錄狀態。";
case "213": return "檔案狀態。";
case "214": return "說明訊息。";
case "215": return "名稱系統型別,其中名稱是一個正式的系統名稱從指派的數字的文件中的清單。";
case "220": return "供新使用者的服務。";
case "221": return "服務正在關閉控制連接。如果可以請登出。";
case "225": return "資料連線已開啟;沒有正在傳輸中。";
case "226": return "關閉資料連線。要求的檔案動作成功 (例如,檔案傳輸或檔案中止)。";
case "227": return "進入被動模式 (h1、 h2、 h3、 h4、 p1,p2)。";
case "229": return "延伸被動模式輸入。";
case "230": return "使用者已登入,繼續執行。";
case "232": return "已登入,使用者的權限的安全性資料交換。";
case "234": return "的-安全性資料交換完成。";
case "235": return "已順利完成安全性資料交換。";
case "250": return "要求的檔案動作無誤,完成。";
case "257": return "路徑名稱 」 建立。";
case "331": return "的使用者名稱無誤,需要密碼。";
case "332": return "需要登入帳戶。";
case "334": return "要求安全性機制 [確定]。";
case "335": return "安全性資料是可接受的。更多資料才能完成安全性資料交換。";
case "336": return "使用者名稱無誤,需要密碼。";
case "350": return "要求的檔案動作擱置中的其他相關資訊。";
case "421": return "服務無法使用,正在關閉控制連接。如果服務知道其必須關閉,這可能是對任何命令的回覆。";
case "425": return "無法開啟資料連接。";
case "426": return "連接已關閉;傳輸中止。";
case "431": return "需要某些無法使用的資源,處理安全性。";
case "450": return "未採取要求的檔案動作。檔案無法使用 (例如,檔案忙碌)。";
case "451": return "要求的動作已中止。正在處理本機錯誤。";
case "452": return "要求未採取的動作。在系統中沒有足夠的儲存空間。";
case "500": return "語法錯誤,無法辨認的命令。這可能包括命令列過長的錯誤。";
case "501": return "參數或引數中的語法錯誤。";
case "502": return "未執行命令。";
case "503": return "錯誤的命令順序。";
case "504": return "並未實作該參數的命令。";
case "521": return "無法開啟 資料連線,使用這個連接埠正常設定。";
case "522": return "伺服器不支援要求的網路通訊協定。";
case "530": return "未登入。";
case "532": return "需要帳戶以儲存檔案。";
case "533": return "命令保護層級拒絕原則的原因。";
case "534": return "原則原因拒絕要求。";
case "535": return "(雜湊、 序列等等) 的失敗的安全性檢查。";
case "536": return "要求的連接埠正常層級不支援的機制。";
case "537": return "命令保護層級不支援的安全性機制。";
case "550": return "要求未採取的動作。檔案無法使用 (例如,找不到檔案或沒有存取權)。";
case "551": return "要求的動作已中止: 頁面類型不明。";
case "552": return "要求的檔案動作已中止。超過儲存配置 (針對目前的目錄或資料集)。";
case "553": return "要求未採取的動作。不允許的檔案名稱。";
case "631": return "完整性保護回覆。";
case "632": return "機密性和完整性保護回覆。";
case "633": return "機密性保護回覆。";
/*case "150": return "FTP 使用兩個連接埠: 21 傳送命令,而 20 用於傳送資料。狀態碼 150 表示伺服器將在連接埠 20開啟新的連線,以便傳送某些資料。";
case "226": return "命令開啟資料連接在連接埠 20,要執行的動作,例如傳送檔案。已順利完成此動作,以及資料連線已關閉。";
case "230": return "用戶端傳送正確的密碼之後,就會顯示此狀態碼。它會指出使用者已成功地登入。";
case "331": return "用戶端傳送使用者名稱後,您會看到這個狀態碼。不論所提供的使用者名稱是否為系統上有效的帳戶,皆會出現相同的狀態碼。";
case "426": return "命令開啟資料連線來執行動作,該動作已取消,但資料連線已關閉。";
case "530": return "此狀態碼表示使用者無法登入,因為使用者名稱和密碼組合不正確。如果您利用使用者帳戶登入,則您可能輸入錯誤的使用者名稱或密碼,或您已選擇僅允許匿名存取。如果您使用「匿名」帳戶登入,您可能已設定 IIS 來拒絕匿名存取。";
case "550": return "命令不會執行,因為指定的檔案無法使用。例如,當您嘗試取得的檔案不存在,或當您嘗試將檔案放置在沒有寫入權限的目錄中時,就會發生這個狀態碼。";*/
default:
return "501 Unknown command";
}
}
中文的狀態輸出僅供參考...實作結果並不符合期待。



沒有留言:
張貼留言