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"; } }
中文的狀態輸出僅供參考...實作結果並不符合期待。
沒有留言:
張貼留言