diff --git a/.gitignore b/.gitignore index b6ba85a6d787ff14d6e67e403f8b2169a3b2b98d..a166d2277e213d6165f948d2d7b636193da94cbe 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,9 @@ -*.iml -.gradle -build -__MACOSX -.idea -keystore -.DS_Store -local.properties -*.apk +*.iml +.gradle +build +__MACOSX +.idea +keystore +.DS_Store +local.properties +*.apk diff --git a/LICENSE b/LICENSE index c1d636e0556d0d91a1ac3c1597d81abbdc18902d..cd7b044d781ba211ab922342ab6fe10af17dee0d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,201 +1,201 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software code code, documentation - code, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, code code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - from_content syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright {yyyy} {name of copyright owner} - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software code code, documentation + code, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, code code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + from_content syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index 5e5ae87aafe86a9b55e5423a45a4b260474eb7e8..6a56563f891f1731e4c863c511706615074ccf84 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# About -QFtplib provides basic ftp features for Android project. - -It is not a standalone project, it will be included by QPython / QPython3 as a submodule +# About +QFtplib provides basic ftp features for Android project. + +It is not a standalone project, it will be included by QPython / QPython3 as a submodule diff --git a/build.gradle b/build.gradle index cdbb42571a887ca1f48499de61cdab0f7d2d2c30..c2949f86d4623c5927ac12d95303a0dfdb17fbc0 100644 --- a/build.gradle +++ b/build.gradle @@ -1,18 +1,25 @@ -apply plugin: 'com.android.library' - -android { - compileSdkVersion rootProject.ext.compileSdkVersion - buildToolsVersion rootProject.ext.buildToolsVersion - - defaultConfig { - minSdkVersion rootProject.ext.minSdkVersion - targetSdkVersion rootProject.ext.targetSdkVersion - } - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' - } - } -} +apply plugin: 'com.android.library' + +android { + compileSdkVersion rootProject.ext.compileSdkVersion + + defaultConfig { + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + dependencies { + api 'com.android.support:documentfile:28.0.0' + } +} diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index cb375251af9b4667a123f26b27b7c34eb5a09711..c3b0df8761046393244876db727f53f1448f54cb 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -1,8 +1,8 @@ - - - - - + + + + + diff --git a/src/main/java/org/swiftp/Defaults.java b/src/main/java/org/swiftp/Defaults.java index 68884b98f3441b6fac39b7d7ca2244e18b295942..bf8b141c44fbdd7379f92c76a5739f3404f18c49 100644 --- a/src/main/java/org/swiftp/Defaults.java +++ b/src/main/java/org/swiftp/Defaults.java @@ -1,153 +1,153 @@ -/* -Copyright 2009 David Revell - -This file is part of SwiFTP. - -SwiFTP is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -SwiFTP is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with SwiFTP. If not, see . -*/ - -package org.swiftp; - -import android.content.Context; -import android.util.Log; - -public class Defaults { - protected static int inputBufferSize = 256; - public static int dataChunkSize = 65536; // do file I/O in 64k chunks - protected static int sessionMonitorScrollBack = 10; - protected static int serverLogScrollBack = 10; - protected static int uiLogLevel = Defaults.release ? Log.INFO : Log.DEBUG; - protected static int consoleLogLevel = Defaults.release ? Log.INFO : Log.DEBUG; - protected static String settingsName = "SwiFTP"; - //protected static String username = "user"; - //protected static String password = ""; - protected static int portNumber = 2121; -// protected static int ipRetrievalAttempts = 5; - public static final int tcpConnectionBacklog = 5; - public static final String chrootDir = "/"; - public static final boolean acceptWifi = true; - public static final boolean acceptNet = false; // don't incur bandwidth charges - public static final boolean stayAwake = true; - public static final int REMOTE_PROXY_PORT = 2222; - public static final String STRING_ENCODING = "UTF-8"; - public static final int SO_TIMEOUT_MS = 30000; // socket timeout millis - // FTP control sessions should start out in ASCII, according to the RFC. - // However, many clients don't turn on UTF-8 even though they support it, - // so we just turn it on by default. - public static final String SESSION_ENCODING = "UTF-8"; - - // This is a flag that should be true for public builds and false for dev builds - public static final boolean release = true; - - // Try to fix the transfer stall bug, reopen the destination file periodically - //public static final boolean do_reopen_hack = false; - //public static final int bytes_between_reopen = 4000000; - - // Try to fix the transfer stall bug, flush the file periodically - //public static final boolean do_flush_hack = false; - //public static final int bytes_between_flush = 500000; - - public static final boolean do_mediascanner_notify = true; - - -// public static int getIpRetrievalAttempts() { -// return ipRetrievalAttempts; -// } - -// public static void setIpRetrievalAttempts(int ipRetrievalAttempts) { -// Defaults.ipRetrievalAttempts = ipRetrievalAttempts; -// } - - public static int getPortNumber() { - return portNumber; - } - - public static void setPortNumber(int portNumber) { - Defaults.portNumber = portNumber; - } - - public static String getSettingsName() { - return settingsName; - } - - public static void setSettingsName(String settingsName) { - Defaults.settingsName = settingsName; - } - - public static int getSettingsMode() { - return settingsMode; - } - - public static void setSettingsMode(int settingsMode) { - Defaults.settingsMode = settingsMode; - } - - public static void setServerLogScrollBack(int serverLogScrollBack) { - Defaults.serverLogScrollBack = serverLogScrollBack; - } - - protected static int settingsMode = Context.MODE_WORLD_WRITEABLE; - - public static int getUiLogLevel() { - return uiLogLevel; - } - - public static void setUiLogLevel(int uiLogLevel) { - Defaults.uiLogLevel = uiLogLevel; - } - - public static int getInputBufferSize() { - return inputBufferSize; - } - - public static void setInputBufferSize(int inputBufferSize) { - Defaults.inputBufferSize = inputBufferSize; - } - - public static int getDataChunkSize() { - return dataChunkSize; - } - - public static void setDataChunkSize(int dataChunkSize) { - Defaults.dataChunkSize = dataChunkSize; - } - - public static int getSessionMonitorScrollBack() { - return sessionMonitorScrollBack; - } - - public static void setSessionMonitorScrollBack( - int sessionMonitorScrollBack) - { - Defaults.sessionMonitorScrollBack = sessionMonitorScrollBack; - } - - public static int getServerLogScrollBack() { - return serverLogScrollBack; - } - - public static void setLogScrollBack(int serverLogScrollBack) { - Defaults.serverLogScrollBack = serverLogScrollBack; - } - - public static int getConsoleLogLevel() { - return consoleLogLevel; - } - - public static void setConsoleLogLevel(int consoleLogLevel) { - Defaults.consoleLogLevel = consoleLogLevel; - } - - -} +/* +Copyright 2009 David Revell + +This file is part of SwiFTP. + +SwiFTP is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +SwiFTP is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with SwiFTP. If not, see . +*/ + +package org.swiftp; + +import android.content.Context; +import android.util.Log; + +public class Defaults { + protected static int inputBufferSize = 256; + public static int dataChunkSize = 65536; // do file I/O in 64k chunks + protected static int sessionMonitorScrollBack = 10; + protected static int serverLogScrollBack = 10; + protected static int uiLogLevel = Defaults.release ? Log.INFO : Log.DEBUG; + protected static int consoleLogLevel = Defaults.release ? Log.INFO : Log.DEBUG; + protected static String settingsName = "SwiFTP"; + //protected static String username = "user"; + //protected static String password = ""; + protected static int portNumber = 0;//2121; +// protected static int ipRetrievalAttempts = 5; + public static final int tcpConnectionBacklog = 5; + public static final String chrootDir = "/"; + public static final boolean acceptWifi = true; + public static final boolean acceptNet = false; // don't incur bandwidth charges + public static final boolean stayAwake = true; + public static final int REMOTE_PROXY_PORT = 2222; + public static final String STRING_ENCODING = "UTF-8"; + public static final int SO_TIMEOUT_MS = 30000; // socket timeout millis + // FTP control sessions should start out in ASCII, according to the RFC. + // However, many clients don't turn on UTF-8 even though they support it, + // so we just turn it on by default. + public static final String SESSION_ENCODING = "UTF-8"; + + // This is a flag that should be true for public builds and false for dev builds + public static final boolean release = true; + + // Try to fix the transfer stall bug, reopen the destination file periodically + //public static final boolean do_reopen_hack = false; + //public static final int bytes_between_reopen = 4000000; + + // Try to fix the transfer stall bug, flush the file periodically + //public static final boolean do_flush_hack = false; + //public static final int bytes_between_flush = 500000; + + public static final boolean do_mediascanner_notify = true; + + +// public static int getIpRetrievalAttempts() { +// return ipRetrievalAttempts; +// } + +// public static void setIpRetrievalAttempts(int ipRetrievalAttempts) { +// Defaults.ipRetrievalAttempts = ipRetrievalAttempts; +// } + + public static int getPortNumber() { + return portNumber; + } + + public static void setPortNumber(int portNumber) { + Defaults.portNumber = portNumber; + } + + public static String getSettingsName() { + return settingsName; + } + + public static void setSettingsName(String settingsName) { + Defaults.settingsName = settingsName; + } + + public static int getSettingsMode() { + return settingsMode; + } + + public static void setSettingsMode(int settingsMode) { + Defaults.settingsMode = settingsMode; + } + + public static void setServerLogScrollBack(int serverLogScrollBack) { + Defaults.serverLogScrollBack = serverLogScrollBack; + } + + protected static int settingsMode = Context.MODE_WORLD_WRITEABLE; + + public static int getUiLogLevel() { + return uiLogLevel; + } + + public static void setUiLogLevel(int uiLogLevel) { + Defaults.uiLogLevel = uiLogLevel; + } + + public static int getInputBufferSize() { + return inputBufferSize; + } + + public static void setInputBufferSize(int inputBufferSize) { + Defaults.inputBufferSize = inputBufferSize; + } + + public static int getDataChunkSize() { + return dataChunkSize; + } + + public static void setDataChunkSize(int dataChunkSize) { + Defaults.dataChunkSize = dataChunkSize; + } + + public static int getSessionMonitorScrollBack() { + return sessionMonitorScrollBack; + } + + public static void setSessionMonitorScrollBack( + int sessionMonitorScrollBack) + { + Defaults.sessionMonitorScrollBack = sessionMonitorScrollBack; + } + + public static int getServerLogScrollBack() { + return serverLogScrollBack; + } + + public static void setLogScrollBack(int serverLogScrollBack) { + Defaults.serverLogScrollBack = serverLogScrollBack; + } + + public static int getConsoleLogLevel() { + return consoleLogLevel; + } + + public static void setConsoleLogLevel(int consoleLogLevel) { + Defaults.consoleLogLevel = consoleLogLevel; + } + + +} diff --git a/src/main/java/org/swiftp/FTPServerService.java b/src/main/java/org/swiftp/FTPServerService.java index e688392395b0058ff6a973f6d6500585e8d9687f..c2cfd59685370f77ca5f24d1e3f05cec23be9a0a 100644 --- a/src/main/java/org/swiftp/FTPServerService.java +++ b/src/main/java/org/swiftp/FTPServerService.java @@ -1,705 +1,720 @@ -/* -Copyright 2009 David Revell - -This file is part of SwiFTP. - -SwiFTP is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -SwiFTP is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with SwiFTP. If not, see . - */ - -package org.swiftp; - -import java.io.File; -import java.io.IOException; -import java.lang.reflect.Method; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.NetworkInterface; -import java.net.ServerSocket; -import java.net.SocketException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Date; -import java.util.List; - -import org.swiftp.server.ProxyConnector; -import org.swiftp.server.SessionThread; -import org.swiftp.server.TcpListener; - -import android.annotation.SuppressLint; -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.app.Service; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.graphics.Bitmap; -import android.net.wifi.WifiManager; -import android.net.wifi.WifiManager.WifiLock; -import android.os.Build; -import android.os.IBinder; -import android.os.PowerManager; -import android.preference.PreferenceManager; -import android.util.Log; - -public abstract class FTPServerService extends Service implements Runnable { - - // Service will broadcast (LocalBroadcast) when server start/stop - static public final String ACTION_STARTED = "org.swiftp.FTPServerService.STARTED"; - static public final String ACTION_STOPPED = "org.swiftp.FTPServerService.STOPPED"; - static public final String ACTION_FAILEDTOSTART = "org.swiftp.FTPServerService.FAILEDTOSTART"; - public static final int BACKLOG = 21; - public static final int MAX_SESSIONS = 5; - public static final String WAKE_LOCK_TAG = "SwiFTP"; - // The server thread will check this often to look for incoming - // connections. We are forced to use non-blocking accept() and polling - // because we cannot wait forever in accept() if we want to be able - // to receive an exit signal and cleanly exit. - public static final int WAKE_INTERVAL_MS = 1000; // milliseconds - private static final int WIFI_AP_STATE_ENABLED = 13; - protected static Thread serverThread = null; - protected static MyLog staticLog = new MyLog(FTPServerService.class.getName()); - protected static WifiLock wifiLock = null; - protected static List sessionMonitor = new ArrayList(); - protected static List serverLog = new ArrayList(); - - // protected static InetAddress serverAddress = null; - protected static int uiLogLevel = Defaults.getUiLogLevel(); - protected static int port; - protected static boolean acceptWifi; - protected static boolean acceptNet; - protected static boolean fullWake; - private static SharedPreferences settings = null; - private final List sessionThreads = new ArrayList(); - protected boolean shouldExit = false; - protected MyLog myLog = new MyLog(getClass().getName()); - // protected ServerSocketChannel wifiSocket; - protected ServerSocket listenSocket; - NotificationManager notificationMgr = null; - PowerManager.WakeLock wakeLock; - private TcpListener wifiListener = null; - private ProxyConnector proxyConnector = null; - - public FTPServerService() { - } - - public static boolean isRunning() { - // return true if and only if a server Thread is running - if (serverThread == null) { - staticLog.l(Log.DEBUG, "Server is not running (null serverThread)"); - return false; - } - if (!serverThread.isAlive()) { - staticLog.l(Log.DEBUG, "serverThread non-null but !isAlive()"); - } else { - staticLog.l(Log.DEBUG, "Server is alive"); - } - return true; - } - - /** - * Gets the IP address of the wifi connection. - * - * @return The integer IP address if wifi enabled, or null if not. - */ - public static InetAddress getWifiIp() { - Context myContext = Globals.getContext().getApplicationContext(); - if (myContext == null) { - throw new NullPointerException("Global context is null"); - } - WifiManager wifiMgr = (WifiManager) myContext.getSystemService(Context.WIFI_SERVICE); - if (isWifiEnabled()) { - int ipAsInt = wifiMgr.getConnectionInfo().getIpAddress(); - if (ipAsInt == 0) { - return null; - } else { - return Util.intToInet(ipAsInt); - } - } else { - return null; - } - } - - public static InetAddress getWifiAndApIp(){ - InetAddress ip = getWifiIp(); - if (ip==null){ - try { - for (NetworkInterface intf : Collections.list(NetworkInterface.getNetworkInterfaces())) { - if (intf.getName().equals("wlan0")){ - for (InetAddress addr : Collections.list(intf.getInetAddresses())) { - if (!addr.isLoopbackAddress() && addr.getHostAddress().contains(".")){ - ip = addr; - break; - } - } - break; - } - } - } catch (SocketException ignored) { - } - } - return ip; - } - - public static boolean isWifiEnabled() { - Context myContext = Globals.getContext(); - if (myContext == null) { - throw new NullPointerException("Global context is null"); - } - @SuppressLint("WifiManagerLeak") WifiManager wifiMgr = (WifiManager) myContext - .getSystemService(Context.WIFI_SERVICE); - if (wifiMgr.getWifiState() == WifiManager.WIFI_STATE_ENABLED) { - return true; - } else { - return false; - } - } - - public static boolean isWifiAndApEnabled(){ - Context myContext = Globals.getContext(); - if (myContext == null) { - throw new NullPointerException("Global context is null"); - } - @SuppressLint("WifiManagerLeak") WifiManager wifiMgr = (WifiManager) myContext - .getSystemService(Context.WIFI_SERVICE); - if (wifiMgr.getWifiState() == WifiManager.WIFI_STATE_ENABLED) { - return true; - } else { - try { - Method method = wifiMgr.getClass().getMethod("getWifiApState"); - return (Integer) method.invoke(wifiMgr) == WIFI_AP_STATE_ENABLED; - } catch (Exception e){ - return false; - } - } - } - - public static List getSessionMonitorContents() { - return new ArrayList(sessionMonitor); - } - - public static List getServerLogContents() { - return new ArrayList(serverLog); - } - - public static void log(int msgLevel, String s) { - serverLog.add(s); - int maxSize = Defaults.getServerLogScrollBack(); - while (serverLog.size() > maxSize) { - serverLog.remove(0); - } - // updateClients(); - } - - public static void writeMonitor(boolean incoming, String s) { - } - - public static int getPort() { - return port; - } - - public static void setPort(int port) { - FTPServerService.port = port; - } - - static public SharedPreferences getSettings() { - return settings; - } - - @Override - public IBinder onBind(Intent intent) { - // We don't implement this functionality, so ignore it - return null; - } - - @Override - public void onCreate() { - myLog.l(Log.DEBUG, "SwiFTP server created"); - // Set the application-wide context global, if not already set - Context myContext = Globals.getContext(); - if (myContext == null) { - myContext = getApplicationContext(); - if (myContext != null) { - Globals.setContext(myContext); - } - } - } - - @Override - public void onStart(Intent intent, int startId) { - super.onStart(intent, startId); - - shouldExit = false; - int attempts = 10; - // The previous server thread may still be cleaning up, wait for it - // to finish. - while (serverThread != null) { - myLog.l(Log.WARN, "Won't start, server thread exists"); - if (attempts > 0) { - attempts--; - Util.sleepIgnoreInterupt(1000); - } else { - myLog.l(Log.ERROR, "Server thread already exists"); - return; - } - } - myLog.l(Log.DEBUG, "Creating server thread"); - serverThread = new Thread(this); - serverThread.start(); - } - - @Override - public void onDestroy() { - myLog.l(Log.INFO, "onDestroy() Stopping server"); - shouldExit = true; - if (serverThread == null) { - myLog.l(Log.WARN, "Stopping with null serverThread"); - return; - } else { - serverThread.interrupt(); - try { - serverThread.join(10000); // wait 10 sec for server thread to - // finish - } catch (InterruptedException e) { - } - if (serverThread.isAlive()) { - myLog.l(Log.WARN, "Server thread failed to exit"); - // it may still exit eventually if we just leave the - // shouldExit flag set - } else { - myLog.d("serverThread join()ed ok"); - serverThread = null; - } - } - try { - if (listenSocket != null) { - myLog.l(Log.INFO, "Closing listenSocket"); - listenSocket.close(); - } - } catch (IOException e) { - } - - if (wifiLock != null) { - wifiLock.release(); - wifiLock = null; - } - clearNotification(); - myLog.d("FTPServerService.onDestroy() finished"); - } - - private boolean loadSettings() { - myLog.l(Log.DEBUG, "Loading settings"); - settings = PreferenceManager.getDefaultSharedPreferences(this); - //port = Integer.valueOf(settings.getString("portNum", "2121")); - String portS = settings.getString(getString(R.string.key_port_num),""); - if (!portS.equals("")) { - port = Integer.valueOf(portS); - } else { - port = Defaults.portNumber; - } - - myLog.l(Log.DEBUG, "Using port " + port); - - acceptNet = settings.getBoolean("allowNet", Defaults.acceptNet); - acceptWifi = settings.getBoolean("allowWifi", Defaults.acceptWifi); - fullWake = settings.getBoolean(getString(R.string.key_stay_awake), Defaults.stayAwake); - - // The username, password, and chrootDir are just checked for sanity - /*String username = settings.getString("username", null); - String password = settings.getString("password", null); - String chrootDir = settings.getString("chrootDir", Defaults.chrootDir); - */ - - String username = settings.getString(getString(R.string.key_username),""); - if (username.equals("")) { - username = Util.getCode(this); - } - String password = settings.getString(getString(R.string.key_ftp_pwd),""); - if (password.equals("")) { - password = Util.getCode(this); - } - String chrootDir = settings.getString(getString(R.string.key_root_dir),""); - if (chrootDir.equals("")) { - chrootDir = "/"; - } - Log.d("FTPService", "(username):"+username+"(pwd)"+password+"(chroot)"+chrootDir); - - validateBlock: { - if (username == null || password == null) { - myLog.l(Log.ERROR, "Username or password is invalid"); - break validateBlock; - } - File chrootDirAsFile = new File(chrootDir); - if (!chrootDirAsFile.isDirectory()) { - myLog.l(Log.ERROR, "Chroot dir is invalid"); - break validateBlock; - } - - - Globals.setChrootDir(chrootDirAsFile); - Globals.setUsername(username); - return true; - } - // We reach here if the settings were not sane - return false; - } - - // This opens a listening socket on all interfaces. - void setupListener() throws IOException { - listenSocket = new ServerSocket(); - listenSocket.setReuseAddress(true); - listenSocket.bind(new InetSocketAddress(port)); - } - - private void setupNotification() { - // http://developer.android.com/guide/topics/ui/notifiers/notifications.html - - // Get NotificationManager reference - String ns = Context.NOTIFICATION_SERVICE; - notificationMgr = (NotificationManager) getSystemService(ns); - - // Instantiate a Notification - int smallIconId = R.drawable.ftp_notification; - //Bitmap largeIconId = R.drawable.ftp_notification; - CharSequence tickerText = getString(R.string.notif_server_starting); - long when = System.currentTimeMillis(); - - - CharSequence contentTitle = getString(R.string.notif_title); - CharSequence contentText = getString(R.string.notif_text); - Intent notificationIntent = new Intent(this, getSettingClass()); - PendingIntent contentIntent = PendingIntent.getActivity(this, 0, - notificationIntent, PendingIntent.FLAG_IMMUTABLE); - - Notification notification; - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) { - notification = new Notification.Builder(this) - .setContentTitle(contentTitle) - .setContentText(contentText) - .setSmallIcon(smallIconId) - //.setLargeIcon(largeIconId) - .setAutoCancel(false) - .setContentIntent(contentIntent) - .build(); - - } else if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB){ - notification = new Notification.Builder(this) - .setContentTitle(contentTitle) - .setContentText(contentText) - .setSmallIcon(smallIconId) - .setSmallIcon(smallIconId) - //.setLargeIcon(largeIconId) - .setAutoCancel(false) - .setContentIntent(contentIntent) - .getNotification(); - - } else { - notification = new Notification(smallIconId, tickerText, when); - notification.contentIntent = contentIntent; - notification.tickerText = contentTitle; - notification.flags |= Notification.FLAG_ONGOING_EVENT; - } - - notificationMgr.notify(0, notification); - - - myLog.d("Notication setup done"); - } - - private void clearNotification() { - if (notificationMgr == null) { - // Get NotificationManager reference - String ns = Context.NOTIFICATION_SERVICE; - notificationMgr = (NotificationManager) getSystemService(ns); - } - notificationMgr.cancelAll(); - myLog.d("Cleared notification"); - } - - public void run() { - // The UI will want to check the server status to update its - // start/stop server button - int consecutiveProxyStartFailures = 0; - long proxyStartMillis = 0; - - myLog.l(Log.DEBUG, "Server thread running"); - - // set our members according to user preferences - if (!loadSettings()) { - // loadSettings returns false if settings are not sane - cleanupAndStopService(); - sendBroadcast(new Intent(ACTION_FAILEDTOSTART)); - return; - } - - if (!isWifiAndApEnabled()) { - cleanupAndStopService(); - sendBroadcast(new Intent(ACTION_FAILEDTOSTART)); - return; - } - - // Initialization of wifi - if (acceptWifi) { - // If configured to accept connections via wifi, then set up the - // socket - try { - setupListener(); - } catch (IOException e) { - myLog.l(Log.WARN, "Error opening port, check your network connection."); - // serverAddress = null; - cleanupAndStopService(); - return; - } - takeWifiLock(); - } - takeWakeLock(); - - myLog.l(Log.INFO, "SwiFTP server ready"); - setupNotification(); - - // A socket is open now, so the FTP server is started, notify rest of world - sendBroadcast(new Intent(ACTION_STARTED)); - - while (!shouldExit) { - if (acceptWifi) { - if (wifiListener != null) { - if (!wifiListener.isAlive()) { - myLog.l(Log.DEBUG, "Joining crashed wifiListener thread"); - try { - wifiListener.join(); - } catch (InterruptedException e) { - } - wifiListener = null; - } - } - if (wifiListener == null) { - // Either our wifi listener hasn't been created yet, or has - // crashed, - // so spawn it - wifiListener = new TcpListener(listenSocket, this); - wifiListener.start(); - } - } - if (acceptNet) { - if (proxyConnector != null) { - if (!proxyConnector.isAlive()) { - myLog.l(Log.DEBUG, "Joining crashed proxy connector"); - try { - proxyConnector.join(); - } catch (InterruptedException e) { - } - proxyConnector = null; - long nowMillis = new Date().getTime(); - // myLog.l(Log.DEBUG, - // "Now:"+nowMillis+" start:"+proxyStartMillis); - if (nowMillis - proxyStartMillis < 3000) { - // We assume that if the proxy thread crashed within - // 3 - // seconds of starting, it was a startup or - // connection - // failure. - myLog.l(Log.DEBUG, "Incrementing proxy start failures"); - consecutiveProxyStartFailures++; - } else { - // Otherwise assume the proxy started successfully - // and - // crashed later. - myLog.l(Log.DEBUG, "Resetting proxy start failures"); - consecutiveProxyStartFailures = 0; - } - } - } - if (proxyConnector == null) { - long nowMillis = new Date().getTime(); - boolean shouldStartListener = false; - // We want to restart the proxy listener without much delay - // for the first few attempts, but add a much longer delay - // if we consistently fail to connect. - if (consecutiveProxyStartFailures < 3 - && (nowMillis - proxyStartMillis) > 5000) { - // Retry every 5 seconds for the first 3 tries - shouldStartListener = true; - } else if (nowMillis - proxyStartMillis > 30000) { - // After the first 3 tries, only retry once per 30 sec - shouldStartListener = true; - } - if (shouldStartListener) { - myLog.l(Log.DEBUG, "Spawning ProxyConnector"); - proxyConnector = new ProxyConnector(this); - proxyConnector.start(); - proxyStartMillis = nowMillis; - } - } - } - try { - // todo: think about using ServerSocket, and just closing - // the main socket to send an exit signal - Thread.sleep(WAKE_INTERVAL_MS); - } catch (InterruptedException e) { - myLog.l(Log.DEBUG, "Thread interrupted"); - } - } - - terminateAllSessions(); - - if (proxyConnector != null) { - proxyConnector.quit(); - proxyConnector = null; - } - if (wifiListener != null) { - wifiListener.quit(); - wifiListener = null; - } - shouldExit = false; // we handled the exit flag, so reset it to - // acknowledge - myLog.l(Log.DEBUG, "Exiting cleanly, returning from run()"); - - cleanupAndStopService(); - } - - private void terminateAllSessions() { - myLog.i("Terminating " + sessionThreads.size() + " session thread(s)"); - synchronized (this) { - for (SessionThread sessionThread : sessionThreads) { - if (sessionThread != null) { - sessionThread.closeDataSocket(); - sessionThread.closeSocket(); - } - } - } - } - - public void cleanupAndStopService() { - // Call the Android Service shutdown function - stopSelf(); - releaseWifiLock(); - releaseWakeLock(); - clearNotification(); - sendBroadcast(new Intent(ACTION_STOPPED)); - } - - private void takeWakeLock() { - if (wakeLock == null) { - PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); - - // Many (all?) devices seem to not properly honor a - // PARTIAL_WAKE_LOCK, - // which should prevent CPU throttling. This has been - // well-complained-about on android-developers. - // For these devices, we have a config option to force the phone - // into a - // full wake lock. - if (fullWake) { - wakeLock = pm.newWakeLock(PowerManager.FULL_WAKE_LOCK, WAKE_LOCK_TAG); - } else { - wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKE_LOCK_TAG); - } - wakeLock.setReferenceCounted(false); - } - myLog.d("Acquiring wake lock"); - wakeLock.acquire(); - } - - private void releaseWakeLock() { - myLog.d("Releasing wake lock"); - if (wakeLock != null) { - wakeLock.release(); - wakeLock = null; - myLog.d("Finished releasing wake lock"); - } else { - myLog.i("Couldn't release null wake lock"); - } - } - - // public static void writeMonitor(boolean incoming, String s) { - // if(incoming) { - // s = "> " + s; - // } else { - // s = "< " + s; - // } - // sessionMonitor.add(s.trim()); - // int maxSize = Defaults.getSessionMonitorScrollBack(); - // while(sessionMonitor.size() > maxSize) { - // sessionMonitor.remove(0); - // } - // updateClients(); - // } - - private void takeWifiLock() { - myLog.d("Taking wifi lock"); - if (wifiLock == null) { - WifiManager manager = (WifiManager) this.getApplication().getApplicationContext().getSystemService(Context.WIFI_SERVICE); - wifiLock = manager.createWifiLock("SwiFTP"); - wifiLock.setReferenceCounted(false); - } - wifiLock.acquire(); - } - - private void releaseWifiLock() { - myLog.d("Releasing wifi lock"); - if (wifiLock != null) { - wifiLock.release(); - wifiLock = null; - } - } - - public void errorShutdown() { - myLog.l(Log.ERROR, "Service errorShutdown() called"); - cleanupAndStopService(); - } - - /** - * The FTPServerService must know about all running session threads so they can be - * terminated on exit. Called when a new session is created. - */ - public void registerSessionThread(SessionThread newSession) { - // Before adding the new session thread, clean up any finished session - // threads that are present in the list. - - // Since we're not allowed to modify the list while iterating over - // it, we construct a list in toBeRemoved of threads to remove - // later from the sessionThreads list. - synchronized (this) { - List toBeRemoved = new ArrayList(); - for (SessionThread sessionThread : sessionThreads) { - if (!sessionThread.isAlive()) { - myLog.l(Log.DEBUG, "Cleaning up finished session..."); - try { - sessionThread.join(); - myLog.l(Log.DEBUG, "Thread joined"); - toBeRemoved.add(sessionThread); - sessionThread.closeSocket(); // make sure socket closed - } catch (InterruptedException e) { - myLog.l(Log.DEBUG, "Interrupted while joining"); - // We will try again in the next loop iteration - } - } - } - for (SessionThread removeThread : toBeRemoved) { - sessionThreads.remove(removeThread); - } - - // Cleanup is complete. Now actually add the new thread to the list. - sessionThreads.add(newSession); - } - myLog.d("Registered session thread"); - } - - /** Get the ProxyConnector, may return null if proxying is disabled. */ - public ProxyConnector getProxyConnector() { - return proxyConnector; - } - - abstract protected Class getSettingClass(); -} +/* +Copyright 2009 David Revell + +This file is part of SwiFTP. + +SwiFTP is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +SwiFTP is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with SwiFTP. If not, see . + */ + +package org.swiftp; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Method; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.NetworkInterface; +import java.net.ServerSocket; +import java.net.SocketException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; + +import org.swiftp.server.ProxyConnector; +import org.swiftp.server.SessionThread; +import org.swiftp.server.TcpListener; + +import android.annotation.SuppressLint; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.graphics.Bitmap; +import android.net.wifi.WifiManager; +import android.net.wifi.WifiManager.WifiLock; +import android.os.Build; +import android.os.IBinder; +import android.os.PowerManager; +import android.preference.PreferenceManager; +import android.util.Log; + +public abstract class FTPServerService extends Service implements Runnable { + + // Service will broadcast (LocalBroadcast) when server start/stop + static public final String ACTION_STARTED = "org.swiftp.FTPServerService.STARTED"; + static public final String ACTION_STOPPED = "org.swiftp.FTPServerService.STOPPED"; + static public final String ACTION_FAILEDTOSTART = "org.swiftp.FTPServerService.FAILEDTOSTART"; + public static final int BACKLOG = 21; + public static final int MAX_SESSIONS = 5; + public static final String WAKE_LOCK_TAG = "SwiFTP"; + // The server thread will check this often to look for incoming + // connections. We are forced to use non-blocking accept() and polling + // because we cannot wait forever in accept() if we want to be able + // to receive an exit signal and cleanly exit. + public static final int WAKE_INTERVAL_MS = 1000; // milliseconds + private static final int WIFI_AP_STATE_ENABLED = 13; + protected static Thread serverThread = null; + protected static MyLog staticLog = new MyLog(FTPServerService.class.getName()); + protected static WifiLock wifiLock = null; + protected static List sessionMonitor = new ArrayList(); + protected static List serverLog = new ArrayList(); + + // protected static InetAddress serverAddress = null; + protected static int uiLogLevel = Defaults.getUiLogLevel(); + protected static int port; + protected static boolean acceptWifi; + protected static boolean acceptNet; + protected static boolean fullWake; + private static SharedPreferences settings = null; + private final List sessionThreads = new ArrayList(); + protected boolean shouldExit = false; + protected MyLog myLog = new MyLog(getClass().getName()); + // protected ServerSocketChannel wifiSocket; + protected ServerSocket listenSocket; + NotificationManager notificationMgr = null; + PowerManager.WakeLock wakeLock; + private TcpListener wifiListener = null; + private ProxyConnector proxyConnector = null; + + public FTPServerService() { + } + + public static boolean isRunning() { + // return true if and only if a server Thread is running + if (serverThread == null) { + staticLog.l(Log.DEBUG, "Server is not running (null serverThread)"); + return false; + } + if (!serverThread.isAlive()) { + staticLog.l(Log.DEBUG, "serverThread non-null but !isAlive()"); + } else { + staticLog.l(Log.DEBUG, "Server is alive"); + } + return true; + } + + /** + * Gets the IP address of the wifi connection. + * + * @return The integer IP address if wifi enabled, or null if not. + */ + public static InetAddress getWifiIp() { + Context myContext = Globals.getContext().getApplicationContext(); + if (myContext == null) { + throw new NullPointerException("Global context is null"); + } + WifiManager wifiMgr = (WifiManager) myContext.getSystemService(Context.WIFI_SERVICE); + if (isWifiEnabled()) { + int ipAsInt = wifiMgr.getConnectionInfo().getIpAddress(); + if (ipAsInt == 0) { + return null; + } else { + return Util.intToInet(ipAsInt); + } + } else { + return null; + } + } + + public static ArrayList getWifiAndApIp(){ + ArrayList ip = new ArrayList<>(); + InetAddress addrWifi = getWifiIp(); + String hostAddr; + if(addrWifi!=null) + ip.add(addrWifi.getHostAddress()); + try { + for (NetworkInterface intf : Collections.list(NetworkInterface.getNetworkInterfaces())) { + for (InetAddress addr : Collections.list(intf.getInetAddresses())) { + hostAddr = addr.getHostAddress(); + if(hostAddr == null || addr.isLoopbackAddress() || ip.contains(hostAddr)) + continue; + if (hostAddr.contains(".")){ + ip.add(hostAddr); + break; + } + } + } + } catch (SocketException ignored) { + } + if(ip.size()==0) + ip = null; + return ip; + } + + public static String[] getIpPortString(){ + ArrayList address = getWifiAndApIp(); + if(address == null) + return null; + String[] ipPort = new String[address.size()]; + for(int i = 0; i getSessionMonitorContents() { + return new ArrayList(sessionMonitor); + } + + public static List getServerLogContents() { + return new ArrayList(serverLog); + } + + public static void log(int msgLevel, String s) { + serverLog.add(s); + int maxSize = Defaults.getServerLogScrollBack(); + while (serverLog.size() > maxSize) { + serverLog.remove(0); + } + // updateClients(); + } + + public static void writeMonitor(boolean incoming, String s) { + } + + public static int getPort() { + return port; + } + + public static void setPort(int port) { + FTPServerService.port = port; + } + + static public SharedPreferences getSettings() { + return settings; + } + + @Override + public IBinder onBind(Intent intent) { + // We don't implement this functionality, so ignore it + return null; + } + + @Override + public void onCreate() { + myLog.l(Log.DEBUG, "SwiFTP server created"); + // Set the application-wide context global, if not already set + Context myContext = Globals.getContext(); + if (myContext == null) { + myContext = getApplicationContext(); + if (myContext != null) { + Globals.setContext(myContext); + } + } + } + + @Override + public void onStart(Intent intent, int startId) { + super.onStart(intent, startId); + + shouldExit = false; + int attempts = 10; + // The previous server thread may still be cleaning up, wait for it + // to finish. + while (serverThread != null) { + myLog.l(Log.WARN, "Won't start, server thread exists"); + if (attempts > 0) { + attempts--; + Util.sleepIgnoreInterupt(1000); + } else { + myLog.l(Log.ERROR, "Server thread already exists"); + return; + } + } + myLog.l(Log.DEBUG, "Creating server thread"); + serverThread = new Thread(this); + serverThread.start(); + } + + @Override + public void onDestroy() { + myLog.l(Log.INFO, "onDestroy() Stopping server"); + shouldExit = true; + if (serverThread == null) { + myLog.l(Log.WARN, "Stopping with null serverThread"); + return; + } else { + serverThread.interrupt(); + try { + serverThread.join(10000); // wait 10 sec for server thread to + // finish + } catch (InterruptedException e) { + } + if (serverThread.isAlive()) { + myLog.l(Log.WARN, "Server thread failed to exit"); + // it may still exit eventually if we just leave the + // shouldExit flag set + } else { + myLog.d("serverThread join()ed ok"); + serverThread = null; + } + } + try { + if (listenSocket != null) { + myLog.l(Log.INFO, "Closing listenSocket"); + listenSocket.close(); + } + } catch (IOException e) { + } + + if (wifiLock != null) { + wifiLock.release(); + wifiLock = null; + } + clearNotification(); + myLog.d("FTPServerService.onDestroy() finished"); + } + + private boolean loadSettings() { + myLog.l(Log.DEBUG, "Loading settings"); + loadPort(this); + + myLog.l(Log.DEBUG, "Using port " + port); + + acceptNet = settings.getBoolean("allowNet", Defaults.acceptNet); + acceptWifi = settings.getBoolean("allowWifi", Defaults.acceptWifi); + fullWake = settings.getBoolean(getString(R.string.key_stay_awake), Defaults.stayAwake); + + // The username, password, and chrootDir are just checked for sanity + /*String username = settings.getString("username", null); + String password = settings.getString("password", null); + String chrootDir = settings.getString("chrootDir", Defaults.chrootDir); + */ + + String username = settings.getString(getString(R.string.key_username),""); + if (username.equals("")) { + username = Util.getCode(this); + } + String password = settings.getString(getString(R.string.key_ftp_pwd),""); + if (password.equals("")) { + password = Util.getCode(this); + } + String chrootDir = settings.getString(getString(R.string.key_root_dir),""); + if (chrootDir.equals("")) { + chrootDir = "/"; + } + Log.d("FTPService", "(username):"+username+"(pwd)"+password+"(chroot)"+chrootDir); + + validateBlock: { + if (username == null || password == null) { + myLog.l(Log.ERROR, "Username or password is invalid"); + break validateBlock; + } + File chrootDirAsFile = new File(chrootDir); + if (!chrootDirAsFile.isDirectory()) { + myLog.l(Log.ERROR, "Chroot dir is invalid"); + break validateBlock; + } + + + Globals.setChrootDir(chrootDirAsFile); + Globals.setUsername(username); + return true; + } + // We reach here if the settings were not sane + return false; + } + + public static void loadPort(Context context){ + settings = PreferenceManager.getDefaultSharedPreferences(context); + //port = Integer.valueOf(settings.getString("portNum", "2121")); + String portS = settings.getString(context.getString(R.string.key_port_num),""); + if (!portS.equals("")) { + port = Integer.valueOf(portS); + } else { + port = Defaults.portNumber; + } + } + + // This opens a listening socket on all interfaces. + void setupListener() throws IOException { + listenSocket = new ServerSocket(); + listenSocket.setReuseAddress(true); + listenSocket.bind(new InetSocketAddress(port)); + } + + private void setupNotification() { + // http://developer.android.com/guide/topics/ui/notifiers/notifications.html + + // Get NotificationManager reference + String ns = Context.NOTIFICATION_SERVICE; + notificationMgr = (NotificationManager) getSystemService(ns); + + // Instantiate a Notification + int smallIconId = R.drawable.ftp_notification; + //Bitmap largeIconId = R.drawable.ftp_notification; + CharSequence tickerText = getString(R.string.notif_server_starting); + long when = System.currentTimeMillis(); + + + CharSequence contentTitle = getString(R.string.notif_title); + CharSequence contentText = getString(R.string.notif_text); + Intent notificationIntent = new Intent(this, getSettingClass()); + PendingIntent contentIntent = PendingIntent.getActivity(this, 0, + notificationIntent, PendingIntent.FLAG_IMMUTABLE); + + Notification notification; + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) { + notification = new Notification.Builder(this) + .setContentTitle(contentTitle) + .setContentText(contentText) + .setSmallIcon(smallIconId) + //.setLargeIcon(largeIconId) + .setAutoCancel(false) + .setContentIntent(contentIntent) + .build(); + + } else if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB){ + notification = new Notification.Builder(this) + .setContentTitle(contentTitle) + .setContentText(contentText) + .setSmallIcon(smallIconId) + .setSmallIcon(smallIconId) + //.setLargeIcon(largeIconId) + .setAutoCancel(false) + .setContentIntent(contentIntent) + .getNotification(); + + } else { + notification = new Notification(smallIconId, tickerText, when); + notification.contentIntent = contentIntent; + notification.tickerText = contentTitle; + notification.flags |= Notification.FLAG_ONGOING_EVENT; + } + + notificationMgr.notify(0, notification); + + + myLog.d("Notication setup done"); + } + + private void clearNotification() { + if (notificationMgr == null) { + // Get NotificationManager reference + String ns = Context.NOTIFICATION_SERVICE; + notificationMgr = (NotificationManager) getSystemService(ns); + } + notificationMgr.cancelAll(); + myLog.d("Cleared notification"); + } + + public void run() { + // The UI will want to check the server status to update its + // start/stop server button + int consecutiveProxyStartFailures = 0; + long proxyStartMillis = 0; + + myLog.l(Log.DEBUG, "Server thread running"); + + // set our members according to user preferences + if (!loadSettings()) { + // loadSettings returns false if settings are not sane + cleanupAndStopService(); + sendBroadcast(new Intent(ACTION_FAILEDTOSTART)); + return; + } + + if (!isWifiAndApEnabled()) { + cleanupAndStopService(); + sendBroadcast(new Intent(ACTION_FAILEDTOSTART)); + return; + } + + // Initialization of wifi + if (acceptWifi) { + // If configured to accept connections via wifi, then set up the + // socket + try { + setupListener(); + } catch (IOException e) { + myLog.l(Log.WARN, "Error opening port, check your network connection."); + // serverAddress = null; + cleanupAndStopService(); + return; + } + takeWifiLock(); + } + takeWakeLock(); + + myLog.l(Log.INFO, "SwiFTP server ready"); + setupNotification(); + + // A socket is open now, so the FTP server is started, notify rest of world + sendBroadcast(new Intent(ACTION_STARTED)); + + while (!shouldExit) { + if (acceptWifi) { + if (wifiListener != null) { + if (!wifiListener.isAlive()) { + myLog.l(Log.DEBUG, "Joining crashed wifiListener thread"); + try { + wifiListener.join(); + } catch (InterruptedException e) { + } + wifiListener = null; + } + } + if (wifiListener == null) { + // Either our wifi listener hasn't been created yet, or has + // crashed, + // so spawn it + wifiListener = new TcpListener(listenSocket, this); + wifiListener.start(); + } + } + if (acceptNet) { + if (proxyConnector != null) { + if (!proxyConnector.isAlive()) { + myLog.l(Log.DEBUG, "Joining crashed proxy connector"); + try { + proxyConnector.join(); + } catch (InterruptedException e) { + } + proxyConnector = null; + long nowMillis = new Date().getTime(); + // myLog.l(Log.DEBUG, + // "Now:"+nowMillis+" start:"+proxyStartMillis); + if (nowMillis - proxyStartMillis < 3000) { + // We assume that if the proxy thread crashed within + // 3 + // seconds of starting, it was a startup or + // connection + // failure. + myLog.l(Log.DEBUG, "Incrementing proxy start failures"); + consecutiveProxyStartFailures++; + } else { + // Otherwise assume the proxy started successfully + // and + // crashed later. + myLog.l(Log.DEBUG, "Resetting proxy start failures"); + consecutiveProxyStartFailures = 0; + } + } + } + if (proxyConnector == null) { + long nowMillis = new Date().getTime(); + boolean shouldStartListener = false; + // We want to restart the proxy listener without much delay + // for the first few attempts, but add a much longer delay + // if we consistently fail to connect. + if (consecutiveProxyStartFailures < 3 + && (nowMillis - proxyStartMillis) > 5000) { + // Retry every 5 seconds for the first 3 tries + shouldStartListener = true; + } else if (nowMillis - proxyStartMillis > 30000) { + // After the first 3 tries, only retry once per 30 sec + shouldStartListener = true; + } + if (shouldStartListener) { + myLog.l(Log.DEBUG, "Spawning ProxyConnector"); + proxyConnector = new ProxyConnector(this); + proxyConnector.start(); + proxyStartMillis = nowMillis; + } + } + } + try { + // todo: think about using ServerSocket, and just closing + // the main socket to send an exit signal + Thread.sleep(WAKE_INTERVAL_MS); + } catch (InterruptedException e) { + myLog.l(Log.DEBUG, "Thread interrupted"); + } + } + + terminateAllSessions(); + + if (proxyConnector != null) { + proxyConnector.quit(); + proxyConnector = null; + } + if (wifiListener != null) { + wifiListener.quit(); + wifiListener = null; + } + shouldExit = false; // we handled the exit flag, so reset it to + // acknowledge + myLog.l(Log.DEBUG, "Exiting cleanly, returning from run()"); + + cleanupAndStopService(); + } + + private void terminateAllSessions() { + myLog.i("Terminating " + sessionThreads.size() + " session thread(s)"); + synchronized (this) { + for (SessionThread sessionThread : sessionThreads) { + if (sessionThread != null) { + sessionThread.closeDataSocket(); + sessionThread.closeSocket(); + } + } + } + } + + public void cleanupAndStopService() { + // Call the Android Service shutdown function + stopSelf(); + releaseWifiLock(); + releaseWakeLock(); + clearNotification(); + sendBroadcast(new Intent(ACTION_STOPPED)); + } + + private void takeWakeLock() { + if (wakeLock == null) { + PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); + + // Many (all?) devices seem to not properly honor a + // PARTIAL_WAKE_LOCK, + // which should prevent CPU throttling. This has been + // well-complained-about on android-developers. + // For these devices, we have a config option to force the phone + // into a + // full wake lock. + if (fullWake) { + wakeLock = pm.newWakeLock(PowerManager.FULL_WAKE_LOCK, WAKE_LOCK_TAG); + } else { + wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKE_LOCK_TAG); + } + wakeLock.setReferenceCounted(false); + } + myLog.d("Acquiring wake lock"); + wakeLock.acquire(); + } + + private void releaseWakeLock() { + myLog.d("Releasing wake lock"); + if (wakeLock != null) { + wakeLock.release(); + wakeLock = null; + myLog.d("Finished releasing wake lock"); + } else { + myLog.i("Couldn't release null wake lock"); + } + } + + // public static void writeMonitor(boolean incoming, String s) { + // if(incoming) { + // s = "> " + s; + // } else { + // s = "< " + s; + // } + // sessionMonitor.add(s.trim()); + // int maxSize = Defaults.getSessionMonitorScrollBack(); + // while(sessionMonitor.size() > maxSize) { + // sessionMonitor.remove(0); + // } + // updateClients(); + // } + + private void takeWifiLock() { + myLog.d("Taking wifi lock"); + if (wifiLock == null) { + WifiManager manager = (WifiManager) this.getApplication().getApplicationContext().getSystemService(Context.WIFI_SERVICE); + wifiLock = manager.createWifiLock("SwiFTP"); + wifiLock.setReferenceCounted(false); + } + wifiLock.acquire(); + } + + private void releaseWifiLock() { + myLog.d("Releasing wifi lock"); + if (wifiLock != null) { + wifiLock.release(); + wifiLock = null; + } + } + + public void errorShutdown() { + myLog.l(Log.ERROR, "Service errorShutdown() called"); + cleanupAndStopService(); + } + + /** + * The FTPServerService must know about all running session threads so they can be + * terminated on exit. Called when a new session is created. + */ + public void registerSessionThread(SessionThread newSession) { + // Before adding the new session thread, clean up any finished session + // threads that are present in the list. + + // Since we're not allowed to modify the list while iterating over + // it, we construct a list in toBeRemoved of threads to remove + // later from the sessionThreads list. + synchronized (this) { + List toBeRemoved = new ArrayList(); + for (SessionThread sessionThread : sessionThreads) { + if (!sessionThread.isAlive()) { + myLog.l(Log.DEBUG, "Cleaning up finished session..."); + try { + sessionThread.join(); + myLog.l(Log.DEBUG, "Thread joined"); + toBeRemoved.add(sessionThread); + sessionThread.closeSocket(); // make sure socket closed + } catch (InterruptedException e) { + myLog.l(Log.DEBUG, "Interrupted while joining"); + // We will try again in the next loop iteration + } + } + } + for (SessionThread removeThread : toBeRemoved) { + sessionThreads.remove(removeThread); + } + + // Cleanup is complete. Now actually add the new thread to the list. + sessionThreads.add(newSession); + } + myLog.d("Registered session thread"); + } + + /** Get the ProxyConnector, may return null if proxying is disabled. */ + public ProxyConnector getProxyConnector() { + return proxyConnector; + } + + abstract protected Class getSettingClass(); +} diff --git a/src/main/java/org/swiftp/Globals.java b/src/main/java/org/swiftp/Globals.java index 254661686bfa178b99f02b2d8455ff9f53f2ccbf..8def1ef3c79a7ae38366abb2ac1227f5d04c7a6f 100644 --- a/src/main/java/org/swiftp/Globals.java +++ b/src/main/java/org/swiftp/Globals.java @@ -1,68 +1,68 @@ -package org.swiftp; - -import java.io.File; - -import org.swiftp.server.ProxyConnector; - -import android.content.Context; - -// TODO: this must all be removed -// if you need a setting, get it from the settings - -public class Globals { - private static Context context; - private static String lastError; - private static File chrootDir = null; - private static ProxyConnector proxyConnector = null; - private static String username = null; - - public static ProxyConnector getProxyConnector() { - if(proxyConnector != null) { - if(!proxyConnector.isAlive()) { - return null; - } - } - return proxyConnector; - } - - public static void setProxyConnector(ProxyConnector proxyConnector) { - Globals.proxyConnector = proxyConnector; - } - - public static File getChrootDir() { - return chrootDir; - } - - public static void setChrootDir(File chrootDir) { - if(chrootDir.isDirectory()) { - Globals.chrootDir = chrootDir; - } - } - - public static String getLastError() { - return lastError; - } - - public static void setLastError(String lastError) { - Globals.lastError = lastError; - } - - public static Context getContext() { - return context; - } - - public static void setContext(Context context) { - if(context != null) { - Globals.context = context; - } - } - - public static String getUsername() { - return username; - } - - public static void setUsername(String username) { - Globals.username = username; - } - -} +package org.swiftp; + +import java.io.File; + +import org.swiftp.server.ProxyConnector; + +import android.content.Context; + +// TODO: this must all be removed +// if you need a setting, get it from the settings + +public class Globals { + private static Context context; + private static String lastError; + private static File chrootDir = null; + private static ProxyConnector proxyConnector = null; + private static String username = null; + + public static ProxyConnector getProxyConnector() { + if(proxyConnector != null) { + if(!proxyConnector.isAlive()) { + return null; + } + } + return proxyConnector; + } + + public static void setProxyConnector(ProxyConnector proxyConnector) { + Globals.proxyConnector = proxyConnector; + } + + public static File getChrootDir() { + return chrootDir; + } + + public static void setChrootDir(File chrootDir) { + if(chrootDir.isDirectory()) { + Globals.chrootDir = chrootDir; + } + } + + public static String getLastError() { + return lastError; + } + + public static void setLastError(String lastError) { + Globals.lastError = lastError; + } + + public static Context getContext() { + return context; + } + + public static void setContext(Context context) { + if(context != null) { + Globals.context = context; + } + } + + public static String getUsername() { + return username; + } + + public static void setUsername(String username) { + Globals.username = username; + } + +} diff --git a/src/main/java/org/swiftp/MyLog.java b/src/main/java/org/swiftp/MyLog.java index a512bee68f1056a7fcaf0206fdcd905e93520344..a2fd284e5aac63481693e41a9ebe98cc0a6cddda 100644 --- a/src/main/java/org/swiftp/MyLog.java +++ b/src/main/java/org/swiftp/MyLog.java @@ -1,65 +1,65 @@ -/* -Copyright 2009 David Revell - -This file is part of SwiFTP. - -SwiFTP is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -SwiFTP is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with SwiFTP. If not, see . -*/ - -package org.swiftp; - -import android.util.Log; - -public class MyLog { - protected String tag; - - public MyLog(String tag) { - this.tag = tag; - } - - public void l(int level, String str, boolean sysOnly) { - synchronized (MyLog.class) { - str = str.trim(); - // Messages of this severity are handled specially - if(level == Log.ERROR || level == Log.WARN) { - Globals.setLastError(str); - } - if(level >= Defaults.getConsoleLogLevel()) { - Log.println(level,tag, str); - } - if(!sysOnly) { // some messages only go to the Android log - if(level >= Defaults.getUiLogLevel()) { - FTPServerService.log(level, str); - } - } - } - } - - public void l(int level, String str) { - l(level, str, false); - } - - public void e(String s) { - l(Log.ERROR, s, false); - } - public void w(String s) { - l(Log.WARN, s, false); - } - public void i(String s) { - l(Log.INFO, s, false); - } - public void d(String s) { - l(Log.DEBUG, s, false); - } -} +/* +Copyright 2009 David Revell + +This file is part of SwiFTP. + +SwiFTP is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +SwiFTP is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with SwiFTP. If not, see . +*/ + +package org.swiftp; + +import android.util.Log; + +public class MyLog { + protected String tag; + + public MyLog(String tag) { + this.tag = tag; + } + + public void l(int level, String str, boolean sysOnly) { + synchronized (MyLog.class) { + str = str.trim(); + // Messages of this severity are handled specially + if(level == Log.ERROR || level == Log.WARN) { + Globals.setLastError(str); + } + if(level >= Defaults.getConsoleLogLevel()) { + Log.println(level,tag, str); + } + if(!sysOnly) { // some messages only go to the Android log + if(level >= Defaults.getUiLogLevel()) { + FTPServerService.log(level, str); + } + } + } + } + + public void l(int level, String str) { + l(level, str, false); + } + + public void e(String s) { + l(Log.ERROR, s, false); + } + public void w(String s) { + l(Log.WARN, s, false); + } + public void i(String s) { + l(Log.INFO, s, false); + } + public void d(String s) { + l(Log.DEBUG, s, false); + } +} diff --git a/src/main/java/org/swiftp/QuotaStats.java b/src/main/java/org/swiftp/QuotaStats.java index f2c5e75e7507b73d1d9f139f45bd9f2064847df1..d86b738c34d0dd5e47fdf387824c716cbd9e9816 100644 --- a/src/main/java/org/swiftp/QuotaStats.java +++ b/src/main/java/org/swiftp/QuotaStats.java @@ -1,37 +1,37 @@ -/* -Copyright 2009 David Revell - -This file is part of SwiFTP. - -SwiFTP is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -SwiFTP is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with SwiFTP. If not, see . -*/ - -package org.swiftp; - -public class QuotaStats { - private int quota; - private int used; - - public QuotaStats(int used, int quota) { - this.quota = quota; - this.used = used; - } - - public int getQuota() { - return quota; - } - public int getUsed() { - return used; - } -} +/* +Copyright 2009 David Revell + +This file is part of SwiFTP. + +SwiFTP is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +SwiFTP is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with SwiFTP. If not, see . +*/ + +package org.swiftp; + +public class QuotaStats { + private int quota; + private int used; + + public QuotaStats(int used, int quota) { + this.quota = quota; + this.used = used; + } + + public int getQuota() { + return quota; + } + public int getUsed() { + return used; + } +} diff --git a/src/main/java/org/swiftp/Settings.java b/src/main/java/org/swiftp/Settings.java index 7997fe6996dbf6ce5898086f67825375d45ad6a7..2ec7aa0ea6015fc30e755c82a3d3481ee2b7e62e 100644 --- a/src/main/java/org/swiftp/Settings.java +++ b/src/main/java/org/swiftp/Settings.java @@ -1,83 +1,83 @@ -/* -Copyright 2009 David Revell - -This file is part of SwiFTP. - -SwiFTP is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -SwiFTP is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with SwiFTP. If not, see . -*/ - -package org.swiftp; - -import android.util.Log; - -public class Settings { - protected static int inputBufferSize = 256; - protected static boolean allowOverwrite = false; - protected static int dataChunkSize = 8192; // do file I/O in 8k chunks - protected static int sessionMonitorScrollBack = 10; - protected static int serverLogScrollBack = 10; - protected static int uiLogLevel = Log.INFO; - - public static int getUiLogLevel() { - return uiLogLevel; - } - - public static void setUiLogLevel(int uiLogLevel) { - Settings.uiLogLevel = uiLogLevel; - } - - public static int getInputBufferSize() { - return inputBufferSize; - } - - public static void setInputBufferSize(int inputBufferSize) { - Settings.inputBufferSize = inputBufferSize; - } - - public static boolean isAllowOverwrite() { - return allowOverwrite; - } - - public static void setAllowOverwrite(boolean allowOverwrite) { - Settings.allowOverwrite = allowOverwrite; - } - - public static int getDataChunkSize() { - return dataChunkSize; - } - - public static void setDataChunkSize(int dataChunkSize) { - Settings.dataChunkSize = dataChunkSize; - } - - public static int getSessionMonitorScrollBack() { - return sessionMonitorScrollBack; - } - - public static void setSessionMonitorScrollBack( - int sessionMonitorScrollBack) - { - Settings.sessionMonitorScrollBack = sessionMonitorScrollBack; - } - - public static int getServerLogScrollBack() { - return serverLogScrollBack; - } - - public static void setLogScrollBack(int serverLogScrollBack) { - Settings.serverLogScrollBack = serverLogScrollBack; - } - - -} +/* +Copyright 2009 David Revell + +This file is part of SwiFTP. + +SwiFTP is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +SwiFTP is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with SwiFTP. If not, see . +*/ + +package org.swiftp; + +import android.util.Log; + +public class Settings { + protected static int inputBufferSize = 256; + protected static boolean allowOverwrite = false; + protected static int dataChunkSize = 8192; // do file I/O in 8k chunks + protected static int sessionMonitorScrollBack = 10; + protected static int serverLogScrollBack = 10; + protected static int uiLogLevel = Log.INFO; + + public static int getUiLogLevel() { + return uiLogLevel; + } + + public static void setUiLogLevel(int uiLogLevel) { + Settings.uiLogLevel = uiLogLevel; + } + + public static int getInputBufferSize() { + return inputBufferSize; + } + + public static void setInputBufferSize(int inputBufferSize) { + Settings.inputBufferSize = inputBufferSize; + } + + public static boolean isAllowOverwrite() { + return allowOverwrite; + } + + public static void setAllowOverwrite(boolean allowOverwrite) { + Settings.allowOverwrite = allowOverwrite; + } + + public static int getDataChunkSize() { + return dataChunkSize; + } + + public static void setDataChunkSize(int dataChunkSize) { + Settings.dataChunkSize = dataChunkSize; + } + + public static int getSessionMonitorScrollBack() { + return sessionMonitorScrollBack; + } + + public static void setSessionMonitorScrollBack( + int sessionMonitorScrollBack) + { + Settings.sessionMonitorScrollBack = sessionMonitorScrollBack; + } + + public static int getServerLogScrollBack() { + return serverLogScrollBack; + } + + public static void setLogScrollBack(int serverLogScrollBack) { + Settings.serverLogScrollBack = serverLogScrollBack; + } + + +} diff --git a/src/main/java/org/swiftp/Util.java b/src/main/java/org/swiftp/Util.java index 6d71ec8031c01c7f1ba2f4f302f4589ffe2482e4..70f78d925eea1377bdfbccb292436c61a36dbf94 100644 --- a/src/main/java/org/swiftp/Util.java +++ b/src/main/java/org/swiftp/Util.java @@ -1,198 +1,198 @@ -/* -Copyright 2009 David Revell - -This file is part of SwiFTP. - -SwiFTP is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -SwiFTP is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with SwiFTP. If not, see . -*/ - -package org.swiftp; - -import java.io.UnsupportedEncodingException; -import java.net.InetAddress; -import java.net.UnknownHostException; - -import org.json.JSONException; -import org.json.JSONObject; - -import android.content.ContentResolver; -import android.content.Context; -import android.content.SharedPreferences; -import android.content.SharedPreferences.Editor; -import android.content.pm.PackageManager.NameNotFoundException; -import android.media.MediaScannerConnection; -import android.media.MediaScannerConnection.MediaScannerConnectionClient; -import android.net.Uri; -import android.provider.Settings; -import android.util.Log; - -abstract public class Util { - static MyLog myLog = new MyLog(Util.class.getName()); - public static String getAndroidId() { - ContentResolver cr = Globals.getContext().getContentResolver(); - return Settings.Secure.getString(cr, Settings.Secure.ANDROID_ID); - } - - /** - * Get the SwiFTP version from the manifest. - * @return The version as a String. - */ - public static String getVersion() { - String packageName = Globals.getContext().getPackageName(); - try { - return Globals.getContext().getPackageManager().getPackageInfo(packageName, 0).versionName; - } catch ( NameNotFoundException e) { - myLog.l(Log.ERROR, "NameNotFoundException looking up SwiFTP version"); - return null; - } - } - - - public static byte byteOfInt(int value, int which) { - int shift = which * 8; - return (byte)(value >> shift); - } - - public static String ipToString(int addr, String sep) { - //myLog.l(Log.DEBUG, "IP as int: " + addr); - if(addr > 0) { - StringBuffer buf = new StringBuffer(); - buf. - append(byteOfInt(addr, 0)).append(sep). - append(byteOfInt(addr, 1)).append(sep). - append(byteOfInt(addr, 2)).append(sep). - append(byteOfInt(addr, 3)); - myLog.l(Log.DEBUG, "ipToString returning: " + buf.toString()); - return buf.toString(); - } else { - return null; - } - } - - public static InetAddress intToInet(int value) { - byte[] bytes = new byte[4]; - for(int i = 0; i<4; i++) { - bytes[i] = byteOfInt(value, i); - } - try { - return InetAddress.getByAddress(bytes); - } catch (UnknownHostException e) { - // This only happens if the byte array has a bad length - return null; - } - } - - public static String ipToString(int addr) { - if(addr == 0) { - // This can only occur due to an error, we shouldn't blindly - // convert 0 to string. - myLog.l(Log.INFO, "ipToString won't convert value 0"); - return null; - } - return ipToString(addr, "."); - } - - // This exists to avoid cluttering up other code with - // UnsupportedEncodingExceptions. - public static byte[] jsonToByteArray(JSONObject json) throws JSONException { - try { - return json.toString().getBytes(Defaults.STRING_ENCODING); - } catch (UnsupportedEncodingException e) { - return null; - } - } - - // This exists to avoid cluttering up other code with - // UnsupportedEncodingExceptions. - public static JSONObject byteArrayToJson(byte[] bytes) throws JSONException { - try { - return new JSONObject(new String(bytes, Defaults.STRING_ENCODING)); - } catch (UnsupportedEncodingException e) { - // This will never happen because we use valid encodings - return null; - } - } - - public static void newFileNotify(String path) { - if(Defaults.do_mediascanner_notify) { - myLog.l(Log.DEBUG, "Notifying others about new file: " + path); - new MediaScannerNotifier(Globals.getContext(), path); - } - } - - public static void deletedFileNotify(String path) { - // This might not work, I couldn't find an API call for this. - if(Defaults.do_mediascanner_notify) { - myLog.l(Log.DEBUG, "Notifying others about deleted file: " + path); - new MediaScannerNotifier(Globals.getContext(), path); - } - } - - // A class to help notify the Music Player and other media services when - // a file has been uploaded. Thanks to Dave Sparks in his post to the - // Android Developers mailing list on 14 Feb 2009. - private static class MediaScannerNotifier implements MediaScannerConnectionClient { - private final MediaScannerConnection connection; - private final String path; - - public MediaScannerNotifier(Context context, String path) { - this.path = path; - connection = new MediaScannerConnection(context, this); - connection.connect(); - } - - public void onMediaScannerConnected() { - connection.scanFile(path, null); // null: we don't know MIME type - } - - public void onScanCompleted(String path, Uri uri) { - connection.disconnect(); - } - } - - public static String[] concatStrArrays(String[] a1, String[] a2) { - String[] retArr = new String[a1.length + a2.length]; - System.arraycopy(a1, 0, retArr, 0, a1.length); - System.arraycopy(a2, 0, retArr, a1.length, a2.length); - return retArr; - } - - public static void sleepIgnoreInterupt(long millis) { - try { - Thread.sleep(millis); - } catch(InterruptedException e) {} - } - - public static String getCode(Context context) { - String packageName = context.getPackageName(); - String[] xcode = packageName.split("\\."); - String code = xcode[xcode.length-1]; - return code; - } - - public static String getSP(Context context, String key) { - String val; - SharedPreferences obj = context.getSharedPreferences("passinger_db",0); - val = obj.getString(key,""); - return val; - } - public static void setSP(Context context, String key,String val) { - SharedPreferences obj = context.getSharedPreferences("passinger_db",0); - Editor wobj; - wobj = obj.edit(); - wobj.putString(key, val); - wobj.commit(); - } - -} +/* +Copyright 2009 David Revell + +This file is part of SwiFTP. + +SwiFTP is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +SwiFTP is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with SwiFTP. If not, see . +*/ + +package org.swiftp; + +import java.io.UnsupportedEncodingException; +import java.net.InetAddress; +import java.net.UnknownHostException; + +import org.json.JSONException; +import org.json.JSONObject; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.SharedPreferences; +import android.content.SharedPreferences.Editor; +import android.content.pm.PackageManager.NameNotFoundException; +import android.media.MediaScannerConnection; +import android.media.MediaScannerConnection.MediaScannerConnectionClient; +import android.net.Uri; +import android.provider.Settings; +import android.util.Log; + +abstract public class Util { + static MyLog myLog = new MyLog(Util.class.getName()); + public static String getAndroidId() { + ContentResolver cr = Globals.getContext().getContentResolver(); + return Settings.Secure.getString(cr, Settings.Secure.ANDROID_ID); + } + + /** + * Get the SwiFTP version from the manifest. + * @return The version as a String. + */ + public static String getVersion() { + String packageName = Globals.getContext().getPackageName(); + try { + return Globals.getContext().getPackageManager().getPackageInfo(packageName, 0).versionName; + } catch ( NameNotFoundException e) { + myLog.l(Log.ERROR, "NameNotFoundException looking up SwiFTP version"); + return null; + } + } + + + public static byte byteOfInt(int value, int which) { + int shift = which * 8; + return (byte)(value >> shift); + } + + public static String ipToString(int addr, String sep) { + //myLog.l(Log.DEBUG, "IP as int: " + addr); + if(addr > 0) { + StringBuffer buf = new StringBuffer(); + buf. + append(byteOfInt(addr, 0)).append(sep). + append(byteOfInt(addr, 1)).append(sep). + append(byteOfInt(addr, 2)).append(sep). + append(byteOfInt(addr, 3)); + myLog.l(Log.DEBUG, "ipToString returning: " + buf.toString()); + return buf.toString(); + } else { + return null; + } + } + + public static InetAddress intToInet(int value) { + byte[] bytes = new byte[4]; + for(int i = 0; i<4; i++) { + bytes[i] = byteOfInt(value, i); + } + try { + return InetAddress.getByAddress(bytes); + } catch (UnknownHostException e) { + // This only happens if the byte array has a bad length + return null; + } + } + + public static String ipToString(int addr) { + if(addr == 0) { + // This can only occur due to an error, we shouldn't blindly + // convert 0 to string. + myLog.l(Log.INFO, "ipToString won't convert value 0"); + return null; + } + return ipToString(addr, "."); + } + + // This exists to avoid cluttering up other code with + // UnsupportedEncodingExceptions. + public static byte[] jsonToByteArray(JSONObject json) throws JSONException { + try { + return json.toString().getBytes(Defaults.STRING_ENCODING); + } catch (UnsupportedEncodingException e) { + return null; + } + } + + // This exists to avoid cluttering up other code with + // UnsupportedEncodingExceptions. + public static JSONObject byteArrayToJson(byte[] bytes) throws JSONException { + try { + return new JSONObject(new String(bytes, Defaults.STRING_ENCODING)); + } catch (UnsupportedEncodingException e) { + // This will never happen because we use valid encodings + return null; + } + } + + public static void newFileNotify(String path) { + if(Defaults.do_mediascanner_notify) { + myLog.l(Log.DEBUG, "Notifying others about new file: " + path); + new MediaScannerNotifier(Globals.getContext(), path); + } + } + + public static void deletedFileNotify(String path) { + // This might not work, I couldn't find an API call for this. + if(Defaults.do_mediascanner_notify) { + myLog.l(Log.DEBUG, "Notifying others about deleted file: " + path); + new MediaScannerNotifier(Globals.getContext(), path); + } + } + + // A class to help notify the Music Player and other media services when + // a file has been uploaded. Thanks to Dave Sparks in his post to the + // Android Developers mailing list on 14 Feb 2009. + private static class MediaScannerNotifier implements MediaScannerConnectionClient { + private final MediaScannerConnection connection; + private final String path; + + public MediaScannerNotifier(Context context, String path) { + this.path = path; + connection = new MediaScannerConnection(context, this); + connection.connect(); + } + + public void onMediaScannerConnected() { + connection.scanFile(path, null); // null: we don't know MIME type + } + + public void onScanCompleted(String path, Uri uri) { + connection.disconnect(); + } + } + + public static String[] concatStrArrays(String[] a1, String[] a2) { + String[] retArr = new String[a1.length + a2.length]; + System.arraycopy(a1, 0, retArr, 0, a1.length); + System.arraycopy(a2, 0, retArr, a1.length, a2.length); + return retArr; + } + + public static void sleepIgnoreInterupt(long millis) { + try { + Thread.sleep(millis); + } catch(InterruptedException e) {} + } + + public static String getCode(Context context) { + String packageName = context.getPackageName(); + String[] xcode = packageName.split("\\."); + String code = xcode[xcode.length-1]; + return code; + } + + public static String getSP(Context context, String key) { + String val; + SharedPreferences obj = context.getSharedPreferences("passinger_db",0); + val = obj.getString(key,""); + return val; + } + public static void setSP(Context context, String key,String val) { + SharedPreferences obj = context.getSharedPreferences("passinger_db",0); + Editor wobj; + wobj = obj.edit(); + wobj.putString(key, val); + wobj.commit(); + } + +} diff --git a/src/main/java/org/swiftp/WidgetProvider.java b/src/main/java/org/swiftp/WidgetProvider.java index 85320488dee7e7697f739bad2bdad433fc1468e4..ae661a5382ecf8582bd20fffe7040bac923cbe12 100644 --- a/src/main/java/org/swiftp/WidgetProvider.java +++ b/src/main/java/org/swiftp/WidgetProvider.java @@ -1,121 +1,121 @@ -package org.swiftp; - - -import android.annotation.TargetApi; -import android.app.PendingIntent; -import android.appwidget.AppWidgetManager; -import android.appwidget.AppWidgetProvider; -import android.content.Context; -import android.content.Intent; -import android.view.View; -import android.widget.RemoteViews; - -/** - * Class handles Widget Events. - * - */ -@TargetApi(3) -public class WidgetProvider extends AppWidgetProvider { - - public static String ACTION_WIDGET_BUTTON = "actionWidgetButton"; - - @Override - public void onUpdate(Context context, AppWidgetManager appWidgetManager, - int[] appWidgetIds) { - - // Log.d("MyActivity", "onUpdate"); - // Toast.makeText(context, "onUpdate", Toast.LENGTH_SHORT).show(); - - // register new Widgets, for them to be Updated by WidgetUiUpdater - WidgetUiUpdater.registerWidgets(appWidgetIds, context, appWidgetManager); - - // add ButtonListener - RemoteViews remoteViews = new RemoteViews(context.getPackageName(), - R.layout.ftp_widget); - - Intent active = new Intent(context, WidgetProvider.class); - active.setAction(ACTION_WIDGET_BUTTON); - - PendingIntent actionPendingIntent = PendingIntent.getBroadcast(context, 0, - active, PendingIntent.FLAG_IMMUTABLE); - remoteViews.setOnClickPendingIntent(R.id.widget_button_off, actionPendingIntent); - remoteViews.setOnClickPendingIntent(R.id.widget_button_on, actionPendingIntent); - - // set the right state, according to the FTP Server - if (FTPServerService.isRunning()) { - remoteViews.setViewVisibility(R.id.widget_button_on, View.VISIBLE); - remoteViews.setViewVisibility(R.id.widget_button_off, View.GONE); - } else { - remoteViews.setViewVisibility(R.id.widget_button_on, View.GONE); - remoteViews.setViewVisibility(R.id.widget_button_off, View.VISIBLE); - } - appWidgetManager.updateAppWidget(appWidgetIds, remoteViews); - - super.onUpdate(context, appWidgetManager, appWidgetIds); - } - - @Override - public void onReceive(Context context, Intent intent) { - - // Log.d("MyActivity", "onReceive"); - // Toast.makeText(context, "onReceive, Action: "+ intent.getAction(), - // Toast.LENGTH_SHORT).show(); - - if (intent.getAction().equals(ACTION_WIDGET_BUTTON)) { - - /* - * After handling onReceive this Object will be destroyed by the OS. - * BroadcastReceivers and WidgetProviders shouldn't handle asynchronous - * actions, like UI Updates. - */ - - // start or stop FTP Service - Intent intentFTP = new Intent(context, FTPServerService.class); - - if (!FTPServerService.isRunning()) { - context.startService(intentFTP); - } else { - context.stopService(intentFTP); - } - // TODO: Use intent to update UI - // UiUpdater.updateClients(); - } - super.onReceive(context, intent); - } - - @Override - public void onDeleted(Context context, int[] appWidgetIds) { - - // Log.d("MyActivity", "onDeleted"); - // Toast.makeText(context, "onDeleted", Toast.LENGTH_SHORT).show(); - - WidgetUiUpdater.unregisterWidgets(appWidgetIds); - super.onDeleted(context, appWidgetIds); - } - - @Override - public void onEnabled(Context context) { - - // Log.d("MyActivity", "onEnabled"); - // Toast.makeText(context, "onEnabled", Toast.LENGTH_SHORT).show(); - - // register the WidgetUiUpdater for the UiUpdater Messages - WidgetUiUpdater.registerAtUiUpdater(); - super.onEnabled(context); - } - - @Override - public void onDisabled(Context context) { - - // Log.d("MyActivity", "onDisabled"); - // Toast.makeText(context, "onDisabled", Toast.LENGTH_SHORT).show(); - - // unregister the WidgetUiUpdater from the UiUpdater Messages - WidgetUiUpdater.unregisterAtUiUpdater(); - - // and clean the WidgetIds list, if it is still not empty - WidgetUiUpdater.unregisterAllWidgets(); - super.onDisabled(context); - } - -} +package org.swiftp; + + +import android.annotation.TargetApi; +import android.app.PendingIntent; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProvider; +import android.content.Context; +import android.content.Intent; +import android.view.View; +import android.widget.RemoteViews; + +/** + * Class handles Widget Events. + * + */ +@TargetApi(3) +public class WidgetProvider extends AppWidgetProvider { + + public static String ACTION_WIDGET_BUTTON = "actionWidgetButton"; + + @Override + public void onUpdate(Context context, AppWidgetManager appWidgetManager, + int[] appWidgetIds) { + + // Log.d("MyActivity", "onUpdate"); + // Toast.makeText(context, "onUpdate", Toast.LENGTH_SHORT).show(); + + // register new Widgets, for them to be Updated by WidgetUiUpdater + WidgetUiUpdater.registerWidgets(appWidgetIds, context, appWidgetManager); + + // add ButtonListener + RemoteViews remoteViews = new RemoteViews(context.getPackageName(), + R.layout.ftp_widget); + + Intent active = new Intent(context, WidgetProvider.class); + active.setAction(ACTION_WIDGET_BUTTON); + + PendingIntent actionPendingIntent = PendingIntent.getBroadcast(context, 0, + active, PendingIntent.FLAG_IMMUTABLE); + remoteViews.setOnClickPendingIntent(R.id.widget_button_off, actionPendingIntent); + remoteViews.setOnClickPendingIntent(R.id.widget_button_on, actionPendingIntent); + + // set the right state, according to the FTP Server + if (FTPServerService.isRunning()) { + remoteViews.setViewVisibility(R.id.widget_button_on, View.VISIBLE); + remoteViews.setViewVisibility(R.id.widget_button_off, View.GONE); + } else { + remoteViews.setViewVisibility(R.id.widget_button_on, View.GONE); + remoteViews.setViewVisibility(R.id.widget_button_off, View.VISIBLE); + } + appWidgetManager.updateAppWidget(appWidgetIds, remoteViews); + + super.onUpdate(context, appWidgetManager, appWidgetIds); + } + + @Override + public void onReceive(Context context, Intent intent) { + + // Log.d("MyActivity", "onReceive"); + // Toast.makeText(context, "onReceive, Action: "+ intent.getAction(), + // Toast.LENGTH_SHORT).show(); + + if (intent.getAction().equals(ACTION_WIDGET_BUTTON)) { + + /* + * After handling onReceive this Object will be destroyed by the OS. + * BroadcastReceivers and WidgetProviders shouldn't handle asynchronous + * actions, like UI Updates. + */ + + // start or stop FTP Service + Intent intentFTP = new Intent(context, FTPServerService.class); + + if (!FTPServerService.isRunning()) { + context.startService(intentFTP); + } else { + context.stopService(intentFTP); + } + // TODO: Use intent to update UI + // UiUpdater.updateClients(); + } + super.onReceive(context, intent); + } + + @Override + public void onDeleted(Context context, int[] appWidgetIds) { + + // Log.d("MyActivity", "onDeleted"); + // Toast.makeText(context, "onDeleted", Toast.LENGTH_SHORT).show(); + + WidgetUiUpdater.unregisterWidgets(appWidgetIds); + super.onDeleted(context, appWidgetIds); + } + + @Override + public void onEnabled(Context context) { + + // Log.d("MyActivity", "onEnabled"); + // Toast.makeText(context, "onEnabled", Toast.LENGTH_SHORT).show(); + + // register the WidgetUiUpdater for the UiUpdater Messages + WidgetUiUpdater.registerAtUiUpdater(); + super.onEnabled(context); + } + + @Override + public void onDisabled(Context context) { + + // Log.d("MyActivity", "onDisabled"); + // Toast.makeText(context, "onDisabled", Toast.LENGTH_SHORT).show(); + + // unregister the WidgetUiUpdater from the UiUpdater Messages + WidgetUiUpdater.unregisterAtUiUpdater(); + + // and clean the WidgetIds list, if it is still not empty + WidgetUiUpdater.unregisterAllWidgets(); + super.onDisabled(context); + } + +} diff --git a/src/main/java/org/swiftp/WidgetUiUpdater.java b/src/main/java/org/swiftp/WidgetUiUpdater.java index 0212d0f84e884957a82f61522651c1ecb49ba400..63f1a4c012d50abff1765a787d8e9bb7ca5beeff 100644 --- a/src/main/java/org/swiftp/WidgetUiUpdater.java +++ b/src/main/java/org/swiftp/WidgetUiUpdater.java @@ -1,124 +1,124 @@ -package org.swiftp; - -import java.util.HashSet; - -import android.annotation.TargetApi; -import android.appwidget.AppWidgetManager; -import android.content.Context; -import android.os.Handler; -import android.os.Message; -import android.view.View; -import android.widget.RemoteViews; - -/** - * This class maintains Widget Updates. It obtains information about UI Updates from - * UiUpdater.class. For starting getting UiUpdates, method {@link #registerAtUiUpdater()} - * have to be executed once. - * - * Why does this Class exist? This class exists, because I was not able to store - * {@link #handler} for every single Widget. There was a Problem with unregistering - * handlers, when a Widget was deleted. - */ -public class WidgetUiUpdater { - private static HashSet widgetIds = new HashSet(); - private static Context context; - private static AppWidgetManager appWidgetManager; - - @SuppressWarnings("unused") - private static Handler handler = new Handler() { - public void handleMessage(Message msg) { - switch (msg.what) { - case 0: // We are being told to do a UI update - // If more than one UI update is queued up, we only need to do one. - removeMessages(0); - updateWidgetUi(); - break; - case 1: // We are being told to display an error message - removeMessages(1); - } - } - }; - - /** - * Registers new Widgets, for updating them when needed. UiUpdater sends UI update - * messages. - * - * @param newWidgetIds - * @param newContext - * @param newAppWidgetManager - */ - @TargetApi(3) - protected static void registerWidgets(int[] newWidgetIds, Context newContext, - AppWidgetManager newAppWidgetManager) { - context = newContext; - appWidgetManager = newAppWidgetManager; - - for (int newWidgetId : newWidgetIds) { - widgetIds.add(new Integer(newWidgetId)); - } - } - - /** - * Unregisters deleted Widgets. - * - * @param newWidgetIds - */ - protected static void unregisterWidgets(int[] newWidgetIds) { - - if (!widgetIds.isEmpty()) { - for (int newWidgetId : newWidgetIds) { - widgetIds.remove(new Integer(newWidgetId)); - } - } - - } - - /** - * Unregister all Widgets on the List. - */ - protected static void unregisterAllWidgets() { - widgetIds.clear(); - } - - /** - * Start listening for UI Updates, to know, when updating the Widgets - */ - protected static void registerAtUiUpdater() { - // TODO: fix this part of the code - // UiUpdater.registerClient(handler); - } - - /** - * Stop listening for UI Updates. - */ - protected static void unregisterAtUiUpdater() { - // TODO: fix this part of the code - // UiUpdater.unregisterClient(handler); - } - - /** - * Updates all Widgets, when static UiUpdater (Observer Pattern) tells to. - */ - private static void updateWidgetUi() { - - // tell all Widgets, to Update themselves. Right State is set in the onUpdate - // handler. - RemoteViews remoteViews = new RemoteViews(context.getPackageName(), - R.layout.ftp_widget); - - // set the right state, according to the FTP Server - if (FTPServerService.isRunning()) { - remoteViews.setViewVisibility(R.id.widget_button_on, View.VISIBLE); - remoteViews.setViewVisibility(R.id.widget_button_off, View.GONE); - } else { - remoteViews.setViewVisibility(R.id.widget_button_on, View.GONE); - remoteViews.setViewVisibility(R.id.widget_button_off, View.VISIBLE); - } - - for (Integer widgetId : widgetIds) { - appWidgetManager.updateAppWidget(widgetId.intValue(), remoteViews); - } - - } - -} +package org.swiftp; + +import java.util.HashSet; + +import android.annotation.TargetApi; +import android.appwidget.AppWidgetManager; +import android.content.Context; +import android.os.Handler; +import android.os.Message; +import android.view.View; +import android.widget.RemoteViews; + +/** + * This class maintains Widget Updates. It obtains information about UI Updates from + * UiUpdater.class. For starting getting UiUpdates, method {@link #registerAtUiUpdater()} + * have to be executed once. + * + * Why does this Class exist? This class exists, because I was not able to store + * {@link #handler} for every single Widget. There was a Problem with unregistering + * handlers, when a Widget was deleted. + */ +public class WidgetUiUpdater { + private static HashSet widgetIds = new HashSet(); + private static Context context; + private static AppWidgetManager appWidgetManager; + + @SuppressWarnings("unused") + private static Handler handler = new Handler() { + public void handleMessage(Message msg) { + switch (msg.what) { + case 0: // We are being told to do a UI update + // If more than one UI update is queued up, we only need to do one. + removeMessages(0); + updateWidgetUi(); + break; + case 1: // We are being told to display an error message + removeMessages(1); + } + } + }; + + /** + * Registers new Widgets, for updating them when needed. UiUpdater sends UI update + * messages. + * + * @param newWidgetIds + * @param newContext + * @param newAppWidgetManager + */ + @TargetApi(3) + protected static void registerWidgets(int[] newWidgetIds, Context newContext, + AppWidgetManager newAppWidgetManager) { + context = newContext; + appWidgetManager = newAppWidgetManager; + + for (int newWidgetId : newWidgetIds) { + widgetIds.add(new Integer(newWidgetId)); + } + } + + /** + * Unregisters deleted Widgets. + * + * @param newWidgetIds + */ + protected static void unregisterWidgets(int[] newWidgetIds) { + + if (!widgetIds.isEmpty()) { + for (int newWidgetId : newWidgetIds) { + widgetIds.remove(new Integer(newWidgetId)); + } + } + + } + + /** + * Unregister all Widgets on the List. + */ + protected static void unregisterAllWidgets() { + widgetIds.clear(); + } + + /** + * Start listening for UI Updates, to know, when updating the Widgets + */ + protected static void registerAtUiUpdater() { + // TODO: fix this part of the code + // UiUpdater.registerClient(handler); + } + + /** + * Stop listening for UI Updates. + */ + protected static void unregisterAtUiUpdater() { + // TODO: fix this part of the code + // UiUpdater.unregisterClient(handler); + } + + /** + * Updates all Widgets, when static UiUpdater (Observer Pattern) tells to. + */ + private static void updateWidgetUi() { + + // tell all Widgets, to Update themselves. Right State is set in the onUpdate + // handler. + RemoteViews remoteViews = new RemoteViews(context.getPackageName(), + R.layout.ftp_widget); + + // set the right state, according to the FTP Server + if (FTPServerService.isRunning()) { + remoteViews.setViewVisibility(R.id.widget_button_on, View.VISIBLE); + remoteViews.setViewVisibility(R.id.widget_button_off, View.GONE); + } else { + remoteViews.setViewVisibility(R.id.widget_button_on, View.GONE); + remoteViews.setViewVisibility(R.id.widget_button_off, View.VISIBLE); + } + + for (Integer widgetId : widgetIds) { + appWidgetManager.updateAppWidget(widgetId.intValue(), remoteViews); + } + + } + +} diff --git a/src/main/java/org/swiftp/gui/ServerPreferenceActivity.java b/src/main/java/org/swiftp/gui/ServerPreferenceActivity.java index 6f0cd4379f9a8c3b8e8530d606831eb5207056c8..5a4fa8d65c941d51eb27abab26462eb68c2d2686 100644 --- a/src/main/java/org/swiftp/gui/ServerPreferenceActivity.java +++ b/src/main/java/org/swiftp/gui/ServerPreferenceActivity.java @@ -1,56 +1,56 @@ -package org.swiftp.gui; - -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.preference.PreferenceActivity; -import android.util.Log; - -import org.swiftp.R; - -/** - * This is the main activity for swiftp, it enables the user to start the server service - * and allows the users to change the settings. - */ -public class ServerPreferenceActivity extends PreferenceActivity{ - - private static String TAG = ServerPreferenceActivity.class.getSimpleName(); - - - - public static void start(Context context) { - Intent starter = new Intent(context, ServerPreferenceActivity.class); - context.startActivity(starter); - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - addPreferencesFromResource(R.xml.ftp_preferences); - setContentView(R.layout.activity_preference); - - - } - - - - @Override - protected void onResume() { - Log.v(TAG, "onResume"); - super.onResume(); - - } - - @Override - protected void onPause() { - Log.v(TAG, "onPause"); - super.onPause(); - - Log.v(TAG, "Unregistering the FTPServer actions"); - - - } - - - -} +package org.swiftp.gui; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.preference.PreferenceActivity; +import android.util.Log; + +import org.swiftp.R; + +/** + * This is the main activity for swiftp, it enables the user to start the server service + * and allows the users to change the settings. + */ +public class ServerPreferenceActivity extends PreferenceActivity{ + + private static String TAG = ServerPreferenceActivity.class.getSimpleName(); + + + + public static void start(Context context) { + Intent starter = new Intent(context, ServerPreferenceActivity.class); + context.startActivity(starter); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + addPreferencesFromResource(R.xml.ftp_preferences); + setContentView(R.layout.activity_preference); + + + } + + + + @Override + protected void onResume() { + Log.v(TAG, "onResume"); + super.onResume(); + + } + + @Override + protected void onPause() { + Log.v(TAG, "onPause"); + super.onPause(); + + Log.v(TAG, "Unregistering the FTPServer actions"); + + + } + + + +} diff --git a/src/main/java/org/swiftp/gui/ServerPreferenceFragment.java b/src/main/java/org/swiftp/gui/ServerPreferenceFragment.java index ddff8f1c6d4e9c7e3cf3dbd056cfbe0053bfe5f5..432306c12ad5bdf6ce79851b741f4be7f89b6d8d 100644 --- a/src/main/java/org/swiftp/gui/ServerPreferenceFragment.java +++ b/src/main/java/org/swiftp/gui/ServerPreferenceFragment.java @@ -1,302 +1,308 @@ -package org.swiftp.gui; - -import android.annotation.TargetApi; -import android.app.AlertDialog; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.SharedPreferences; -import android.content.res.Resources; -import android.os.Build; -import android.os.Bundle; -import android.os.Environment; -import android.preference.CheckBoxPreference; -import android.preference.EditTextPreference; -import android.preference.Preference; -import android.preference.PreferenceFragment; -import android.preference.PreferenceManager; -import android.util.Log; -import android.view.Gravity; -import android.widget.Toast; - -import org.swiftp.FTPServerService; -import org.swiftp.Globals; -import org.swiftp.R; - -import java.io.File; -import java.net.InetAddress; - -/** - * Created by Hmei on 2017-06-07. - */ - -@TargetApi(Build.VERSION_CODES.HONEYCOMB) -public class ServerPreferenceFragment extends PreferenceFragment implements - SharedPreferences.OnSharedPreferenceChangeListener { - private static final String TAG = "SPF"; - EditTextPreference mPassWordPref; - /** - * This receiver will check FTPServer.ACTION* messages and will update the button, - * running_state, if the server is running and will also display at what url the - * server is running. - */ - BroadcastReceiver ftpServerReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - Log.v(TAG, "FTPServerService action received: " + intent.getAction()); - CheckBoxPreference running_state = (CheckBoxPreference) findPreference("running_state"); - if (intent.getAction().equals(FTPServerService.ACTION_STARTED)) { - running_state.setChecked(true); - // Fill in the FTP server address - InetAddress address = FTPServerService.getWifiAndApIp(); - if (address == null) { - Log.v(TAG, "Unable to retreive wifi ip address"); - running_state.setSummary(R.string.cant_get_url); - return; - } - String iptext = "ftp://" + address.getHostAddress() + ":" - + FTPServerService.getPort() + "/"; - Resources resources = getResources(); - String summary = resources.getString(R.string.running_summary_started, - iptext); - running_state.setSummary(summary); - } else if (intent.getAction().equals(FTPServerService.ACTION_STOPPED)) { - running_state.setChecked(false); - running_state.setSummary(R.string.running_summary_stopped); - } else if (intent.getAction().equals(FTPServerService.ACTION_FAILEDTOSTART)) { - running_state.setChecked(false); - running_state.setSummary(R.string.running_summary_failed); - } - } - }; - - static private String transformPassword(String password) { - SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(Globals - .getContext()); - Resources res = Globals.getContext().getResources(); - boolean showPassword = res.getString(R.string.show_password_default).equals("true"); - showPassword = sp.getBoolean("show_password", showPassword); - if (showPassword == true) - return password; - else { - StringBuilder sb = new StringBuilder(password.length()); - for (int i = 0; i < password.length(); ++i) - sb.append('*'); - return sb.toString(); - } - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - addPreferencesFromResource(R.xml.ftp_preferences); - - final SharedPreferences settings = PreferenceManager - .getDefaultSharedPreferences(getActivity()); - Resources resources = getResources(); - - CheckBoxPreference running_state = (CheckBoxPreference) findPreference("running_state"); - running_state.setChecked(FTPServerService.isRunning()); - running_state.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - if ((Boolean) newValue) { - startServer(); - } else { - stopServer(); - } - return true; - } - }); - - EditTextPreference username_pref = (EditTextPreference) findPreference("username"); - username_pref.setSummary(settings.getString("username", - resources.getString(R.string.username_default))); - username_pref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - String newUsername = (String) newValue; - if (preference.getSummary().equals(newUsername)) - return false; - if (!newUsername.matches("[a-zA-Z0-9]+")) { - Toast.makeText(getActivity(), - R.string.username_validation_error, Toast.LENGTH_LONG).show(); - return false; - } - preference.setSummary(newUsername); - stopServer(); - return true; - } - }); - - mPassWordPref = (EditTextPreference) findPreference("password"); - String password = resources.getString(R.string.password_default); - password = settings.getString("password", password); - mPassWordPref.setSummary(transformPassword(password)); - mPassWordPref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - String newPassword = (String) newValue; - if (!newPassword.matches("[a-zA-Z0-9]+")) { - Toast.makeText(getActivity(), - R.string.password_validation_error, Toast.LENGTH_LONG).show(); - return false; - } - preference.setSummary(transformPassword(newPassword)); - stopServer(); - return true; - } - }); - - EditTextPreference portnum_pref = (EditTextPreference) findPreference("portNum"); - portnum_pref.setSummary(settings.getString("portNum", - resources.getString(R.string.portnumber_default))); - portnum_pref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - String newPortnumString = (String) newValue; - if (preference.getSummary().equals(newPortnumString)) - return false; - int portnum = 0; - try { - portnum = Integer.parseInt(newPortnumString); - } catch (Exception e) { - } - if (portnum <= 0 || 65535 < portnum) { - Toast.makeText(getActivity(), - R.string.port_validation_error, Toast.LENGTH_LONG).show(); - return false; - } - preference.setSummary(newPortnumString); - stopServer(); - return true; - } - }); - - EditTextPreference chroot_pref = (EditTextPreference) findPreference("chrootDir"); - chroot_pref.setSummary(settings.getString("chrootDir", - resources.getString(R.string.chroot_default))); - chroot_pref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - String newChroot = (String) newValue; - if (preference.getSummary().equals(newChroot)) - return false; - // now test the new chroot directory - File chrootTest = new File(newChroot); - if (!chrootTest.isDirectory() || !chrootTest.canRead()) - return false; - preference.setSummary(newChroot); - stopServer(); - return true; - } - }); - - final CheckBoxPreference wakelock_pref = (CheckBoxPreference) findPreference("stayAwake"); - wakelock_pref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - stopServer(); - return true; - } - }); - - Preference help = findPreference("help"); - help.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { - new AlertDialog.Builder(getActivity()) - .setTitle(R.string.help_dlg_title) - .setMessage(R.string.help_dlg_message) - .setPositiveButton(getText(R.string.ok), null).show(); - return true; - } - }); - - Preference about = findPreference("about"); - about.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { - new AlertDialog.Builder(getActivity()) - .setTitle(R.string.about_dlg_title) - .setMessage(R.string.about_dlg_message) - .setPositiveButton(getText(R.string.ok), null).show(); - return true; - } - }); - } - - @Override - public void onSharedPreferenceChanged(SharedPreferences sp, String key) { - if (key.equals("show_password")) { - Resources res = Globals.getContext().getResources(); - String password = res.getString(R.string.password_default); - password = sp.getString("password", password); - mPassWordPref.setSummary(transformPassword(password)); - } - } - - @Override - public void onResume() { - Log.v(TAG, "onResume"); - super.onResume(); - - // make this class listen for preference changes - getPreferenceScreen().getSharedPreferences() - .registerOnSharedPreferenceChangeListener(this); - - Log.v(TAG, "Registering the FTP server actions"); - IntentFilter filter = new IntentFilter(); - filter.addAction(FTPServerService.ACTION_STARTED); - filter.addAction(FTPServerService.ACTION_STOPPED); - filter.addAction(FTPServerService.ACTION_FAILEDTOSTART); - getActivity().registerReceiver(ftpServerReceiver, filter); - } - - @Override - public void onPause() { - Log.v(TAG, "onPause"); - super.onPause(); - - Log.v(TAG, "Unregistering the FTPServer actions"); - getActivity().unregisterReceiver(ftpServerReceiver); - - // unregister the listener - getPreferenceScreen().getSharedPreferences() - .unregisterOnSharedPreferenceChangeListener(this); - - } - - private void startServer() { - Context context = getActivity().getApplicationContext(); - Intent serverService = new Intent(context, FTPServerService.class); - if (!FTPServerService.isRunning()) { - warnIfNoExternalStorage(); - getActivity().startService(serverService); - } - } - - private void stopServer() { - Context context = getActivity().getApplicationContext(); - Intent serverService = new Intent(context, FTPServerService.class); - getActivity().stopService(serverService); - } - - /** - * Will check if the device contains external storage (sdcard) and display a warning - * for the user if there is no external storage. Nothing more. - */ - private void warnIfNoExternalStorage() { - String storageState = Environment.getExternalStorageState(); - if (!storageState.equals(Environment.MEDIA_MOUNTED)) { - Log.v(TAG, "Warning due to storage stat" + storageState); - Toast toast = Toast.makeText(getActivity(), R.string.storage_warning, - Toast.LENGTH_LONG); - toast.setGravity(Gravity.CENTER, 0, 0); - toast.show(); - } - } - - -} +package org.swiftp.gui; + +import android.annotation.TargetApi; +import android.app.AlertDialog; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.content.res.Resources; +import android.os.Build; +import android.os.Bundle; +import android.os.Environment; +import android.preference.CheckBoxPreference; +import android.preference.EditTextPreference; +import android.preference.Preference; +import android.preference.PreferenceFragment; +import android.preference.PreferenceManager; +import android.util.Log; +import android.view.Gravity; +import android.widget.Toast; + +import org.swiftp.Defaults; +import org.swiftp.FTPServerService; +import org.swiftp.Globals; +import org.swiftp.R; + +import java.io.File; +import java.net.InetAddress; +import java.util.ArrayList; + +/** + * Created by Hmei on 2017-06-07. + */ + +@TargetApi(Build.VERSION_CODES.HONEYCOMB) +public class ServerPreferenceFragment extends PreferenceFragment implements + SharedPreferences.OnSharedPreferenceChangeListener { + private static final String TAG = "SPF"; + EditTextPreference mPassWordPref; + /** + * This receiver will check FTPServer.ACTION* messages and will update the button, + * running_state, if the server is running and will also display at what url the + * server is running. + */ + BroadcastReceiver ftpServerReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + Log.v(TAG, "FTPServerService action received: " + intent.getAction()); + CheckBoxPreference running_state = (CheckBoxPreference) findPreference("running_state"); + if (intent.getAction().equals(FTPServerService.ACTION_STARTED)) { + running_state.setChecked(true); + // Fill in the FTP server address + String[] address = FTPServerService.getIpPortString(); + if (address == null) { + Log.v(TAG, "Unable to retreive wifi ip address"); + running_state.setSummary(R.string.cant_get_url); + return; + } + StringBuilder iptext = new StringBuilder(); + for(String ip : address){ + if(iptext.length()>0) + iptext.append(", "); + iptext.append(ip); + } + Resources resources = getResources(); + String summary = resources.getString(R.string.running_summary_started, + iptext); + running_state.setSummary(summary); + } else if (intent.getAction().equals(FTPServerService.ACTION_STOPPED)) { + running_state.setChecked(false); + running_state.setSummary(R.string.running_summary_stopped); + } else if (intent.getAction().equals(FTPServerService.ACTION_FAILEDTOSTART)) { + running_state.setChecked(false); + running_state.setSummary(R.string.running_summary_failed); + } + } + }; + + static private String transformPassword(String password) { + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(Globals + .getContext()); + Resources res = Globals.getContext().getResources(); + boolean showPassword = res.getString(R.string.show_password_default).equals("true"); + showPassword = sp.getBoolean("show_password", showPassword); + if (showPassword == true) + return password; + else { + StringBuilder sb = new StringBuilder(password.length()); + for (int i = 0; i < password.length(); ++i) + sb.append('*'); + return sb.toString(); + } + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + addPreferencesFromResource(R.xml.ftp_preferences); + + final SharedPreferences settings = PreferenceManager + .getDefaultSharedPreferences(getActivity()); + Resources resources = getResources(); + + CheckBoxPreference running_state = (CheckBoxPreference) findPreference("running_state"); + running_state.setChecked(FTPServerService.isRunning()); + running_state.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + if ((Boolean) newValue) { + startServer(); + } else { + stopServer(); + } + return true; + } + }); + + EditTextPreference username_pref = (EditTextPreference) findPreference("username"); + username_pref.setSummary(settings.getString("username", + resources.getString(R.string.username_default))); + username_pref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + String newUsername = (String) newValue; + if (preference.getSummary().equals(newUsername)) + return false; + if (!newUsername.matches("[a-zA-Z0-9]+")) { + Toast.makeText(getActivity(), + R.string.username_validation_error, Toast.LENGTH_LONG).show(); + return false; + } + preference.setSummary(newUsername); + stopServer(); + return true; + } + }); + + mPassWordPref = (EditTextPreference) findPreference("password"); + String password = resources.getString(R.string.password_default); + password = settings.getString("password", password); + mPassWordPref.setSummary(transformPassword(password)); + mPassWordPref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + String newPassword = (String) newValue; + if (!newPassword.matches("[a-zA-Z0-9]+")) { + Toast.makeText(getActivity(), + R.string.password_validation_error, Toast.LENGTH_LONG).show(); + return false; + } + preference.setSummary(transformPassword(newPassword)); + stopServer(); + return true; + } + }); + + EditTextPreference portnum_pref = (EditTextPreference) findPreference("portNum"); + portnum_pref.setSummary(settings.getString("portNum", + getString(R.string.portnumber_default))); + portnum_pref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + String newPortnumString = (String) newValue; + if (preference.getSummary().equals(newPortnumString)) + return false; + int portnum = 0; + try { + portnum = Integer.parseInt(newPortnumString); + } catch (Exception e) { + } + if (portnum <= 0 || 65535 < portnum) { + Toast.makeText(getActivity(), + R.string.port_validation_error, Toast.LENGTH_LONG).show(); + return false; + } + preference.setSummary(newPortnumString); + stopServer(); + return true; + } + }); + + EditTextPreference chroot_pref = (EditTextPreference) findPreference("chrootDir"); + chroot_pref.setSummary(settings.getString("chrootDir", + resources.getString(R.string.chroot_default))); + chroot_pref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + String newChroot = (String) newValue; + if (preference.getSummary().equals(newChroot)) + return false; + // now test the new chroot directory + File chrootTest = new File(newChroot); + if (!chrootTest.isDirectory() || !chrootTest.canRead()) + return false; + preference.setSummary(newChroot); + stopServer(); + return true; + } + }); + + final CheckBoxPreference wakelock_pref = (CheckBoxPreference) findPreference("stayAwake"); + wakelock_pref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + stopServer(); + return true; + } + }); + + Preference help = findPreference("help"); + help.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + new AlertDialog.Builder(getActivity()) + .setTitle(R.string.help_dlg_title) + .setMessage(R.string.help_dlg_message) + .setPositiveButton(getText(R.string.ok), null).show(); + return true; + } + }); + + Preference about = findPreference("about"); + about.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + new AlertDialog.Builder(getActivity()) + .setTitle(R.string.about_dlg_title) + .setMessage(R.string.about_dlg_message) + .setPositiveButton(getText(R.string.ok), null).show(); + return true; + } + }); + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sp, String key) { + if (key.equals("show_password")) { + Resources res = Globals.getContext().getResources(); + String password = res.getString(R.string.password_default); + password = sp.getString("password", password); + mPassWordPref.setSummary(transformPassword(password)); + } + } + + @Override + public void onResume() { + Log.v(TAG, "onResume"); + super.onResume(); + + // make this class listen for preference changes + getPreferenceScreen().getSharedPreferences() + .registerOnSharedPreferenceChangeListener(this); + + Log.v(TAG, "Registering the FTP server actions"); + IntentFilter filter = new IntentFilter(); + filter.addAction(FTPServerService.ACTION_STARTED); + filter.addAction(FTPServerService.ACTION_STOPPED); + filter.addAction(FTPServerService.ACTION_FAILEDTOSTART); + getActivity().registerReceiver(ftpServerReceiver, filter); + } + + @Override + public void onPause() { + Log.v(TAG, "onPause"); + super.onPause(); + + Log.v(TAG, "Unregistering the FTPServer actions"); + getActivity().unregisterReceiver(ftpServerReceiver); + + // unregister the listener + getPreferenceScreen().getSharedPreferences() + .unregisterOnSharedPreferenceChangeListener(this); + + } + + private void startServer() { + Context context = getActivity().getApplicationContext(); + Intent serverService = new Intent(context, FTPServerService.class); + if (!FTPServerService.isRunning()) { + warnIfNoExternalStorage(); + getActivity().startService(serverService); + } + } + + private void stopServer() { + Context context = getActivity().getApplicationContext(); + Intent serverService = new Intent(context, FTPServerService.class); + getActivity().stopService(serverService); + } + + /** + * Will check if the device contains external storage (sdcard) and display a warning + * for the user if there is no external storage. Nothing more. + */ + private void warnIfNoExternalStorage() { + String storageState = Environment.getExternalStorageState(); + if (!storageState.equals(Environment.MEDIA_MOUNTED)) { + Log.v(TAG, "Warning due to storage stat" + storageState); + Toast toast = Toast.makeText(getActivity(), R.string.storage_warning, + Toast.LENGTH_LONG); + toast.setGravity(Gravity.CENTER, 0, 0); + toast.show(); + } + } + + +} diff --git a/src/main/java/org/swiftp/server/Account.java b/src/main/java/org/swiftp/server/Account.java index c56731e7b9fc5f5214ca3760fa4f9f3fa9388dab..28e43374f281553b4448315f473318855b8aa7c2 100644 --- a/src/main/java/org/swiftp/server/Account.java +++ b/src/main/java/org/swiftp/server/Account.java @@ -1,34 +1,34 @@ -/* -Copyright 2009 David Revell - -This file is part of SwiFTP. - -SwiFTP is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -SwiFTP is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with SwiFTP. If not, see . -*/ - -package org.swiftp.server; - -public class Account { - protected String username = null; - - public String getUsername() { - return username; - } - - public void setUsername(String username) { - this.username = username; - } - - -} +/* +Copyright 2009 David Revell + +This file is part of SwiFTP. + +SwiFTP is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +SwiFTP is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with SwiFTP. If not, see . +*/ + +package org.swiftp.server; + +public class Account { + protected String username = null; + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + +} diff --git a/src/main/java/org/swiftp/server/CmdAPPE.java b/src/main/java/org/swiftp/server/CmdAPPE.java index 108f2cb05953145b2523b76326916f41b9940d63..045f4046ef538c912a5e5047197496a003ad3c90 100644 --- a/src/main/java/org/swiftp/server/CmdAPPE.java +++ b/src/main/java/org/swiftp/server/CmdAPPE.java @@ -1,35 +1,35 @@ -/* -Copyright 2009 David Revell - -This file is part of SwiFTP. - -SwiFTP is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -SwiFTP is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with SwiFTP. If not, see . -*/ - -package org.swiftp.server; - - -public class CmdAPPE extends CmdAbstractStore implements Runnable { - protected String input; - - public CmdAPPE(SessionThread sessionThread, String input) { - super(sessionThread, CmdAPPE.class.toString()); - this.input = input; - } - - @Override - public void run() { - doStorOrAppe(getParameter(input), true); - } -} +/* +Copyright 2009 David Revell + +This file is part of SwiFTP. + +SwiFTP is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +SwiFTP is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with SwiFTP. If not, see . +*/ + +package org.swiftp.server; + + +public class CmdAPPE extends CmdAbstractStore implements Runnable { + protected String input; + + public CmdAPPE(SessionThread sessionThread, String input) { + super(sessionThread, CmdAPPE.class.toString()); + this.input = input; + } + + @Override + public void run() { + doStorOrAppe(getParameter(input), true); + } +} diff --git a/src/main/java/org/swiftp/server/CmdAbstractListing.java b/src/main/java/org/swiftp/server/CmdAbstractListing.java index 9e8fedc99a274eca1a28319e946809e72c85145d..4ff625110566becce9448f16ed5297a732318c08 100644 --- a/src/main/java/org/swiftp/server/CmdAbstractListing.java +++ b/src/main/java/org/swiftp/server/CmdAbstractListing.java @@ -1,94 +1,94 @@ -/* -Copyright 2009 David Revell - -This file is part of SwiFTP. - -SwiFTP is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -SwiFTP is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with SwiFTP. If not, see . -*/ - -/* - * Since the FTP verbs LIST and NLST do very similar things related to listing - * directory contents, the common tasks that they share have been factored - * out into this abstract class. Both CmdLIST and CmdNLST inherit from this - * class. - */ - -package org.swiftp.server; - -import java.io.File; - -import org.swiftp.MyLog; - -import android.util.Log; - -public abstract class CmdAbstractListing extends FtpCmd { - protected static MyLog staticLog = new MyLog(CmdLIST.class.toString()); - - public CmdAbstractListing(SessionThread sessionThread, String input) { - super(sessionThread, CmdAbstractListing.class.toString()); - } - - abstract String makeLsString(File file); - - // Creates a directory listing by finding the contents of the directory, - // calling makeLsString on each file, and concatenating the results. - // Returns an error string if failure, returns null on success. May be - // called by CmdLIST or CmdNLST, since they each override makeLsString - // in a different way. - public String listDirectory(StringBuilder response, File dir) { - if(!dir.isDirectory()) { - return "500 Internal error, listDirectory on non-directory\r\n"; - } - myLog.l(Log.DEBUG, "Listing directory: " + dir.toString()); - - // Get a listing of all files and directories in the path - File[] entries = dir.listFiles(); - if(entries == null) { - return "500 Couldn't list directory. Check config and mount status.\r\n"; - } - myLog.l(Log.DEBUG, "Dir len " + entries.length); - for(File entry : entries) { - String curLine = makeLsString(entry); - if(curLine != null) { - response.append(curLine); - } - } - return null; - } - - // Send the directory listing over the data socket. Used by CmdLIST and - // CmdNLST. - // Returns an error string on failure, or returns null if successful. - protected String sendListing(String listing) { - if(sessionThread.startUsingDataSocket()) { - myLog.l(Log.DEBUG, "LIST/NLST done making socket"); - } else { - sessionThread.closeDataSocket(); - return "425 Error opening data socket\r\n"; - } - String mode = sessionThread.isBinaryMode() ? "BINARY" : "ASCII"; - sessionThread.writeString( - "150 Opening "+mode+" mode data connection for file list\r\n"); - myLog.l(Log.DEBUG, "Sent code 150, sending listing string now"); - if(!sessionThread.sendViaDataSocket(listing)) { - myLog.l(Log.DEBUG, "sendViaDataSocket failure"); - sessionThread.closeDataSocket(); - return "426 Data socket or network error\r\n"; - } - sessionThread.closeDataSocket(); - myLog.l(Log.DEBUG, "Listing sendViaDataSocket success"); - sessionThread.writeString("226 Data transmission OK\r\n"); - return null; - } -} +/* +Copyright 2009 David Revell + +This file is part of SwiFTP. + +SwiFTP is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +SwiFTP is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with SwiFTP. If not, see . +*/ + +/* + * Since the FTP verbs LIST and NLST do very similar things related to listing + * directory contents, the common tasks that they share have been factored + * out into this abstract class. Both CmdLIST and CmdNLST inherit from this + * class. + */ + +package org.swiftp.server; + +import java.io.File; + +import org.swiftp.MyLog; + +import android.util.Log; + +public abstract class CmdAbstractListing extends FtpCmd { + protected static MyLog staticLog = new MyLog(CmdLIST.class.toString()); + + public CmdAbstractListing(SessionThread sessionThread, String input) { + super(sessionThread, CmdAbstractListing.class.toString()); + } + + abstract String makeLsString(File file); + + // Creates a directory listing by finding the contents of the directory, + // calling makeLsString on each file, and concatenating the results. + // Returns an error string if failure, returns null on success. May be + // called by CmdLIST or CmdNLST, since they each override makeLsString + // in a different way. + public String listDirectory(StringBuilder response, File dir) { + if(!dir.isDirectory()) { + return "500 Internal error, listDirectory on non-directory\r\n"; + } + myLog.l(Log.DEBUG, "Listing directory: " + dir.toString()); + + // Get a listing of all files and directories in the path + File[] entries = dir.listFiles(); + if(entries == null) { + return "500 Couldn't list directory. Check config and mount status.\r\n"; + } + myLog.l(Log.DEBUG, "Dir len " + entries.length); + for(File entry : entries) { + String curLine = makeLsString(entry); + if(curLine != null) { + response.append(curLine); + } + } + return null; + } + + // Send the directory listing over the data socket. Used by CmdLIST and + // CmdNLST. + // Returns an error string on failure, or returns null if successful. + protected String sendListing(String listing) { + if(sessionThread.startUsingDataSocket()) { + myLog.l(Log.DEBUG, "LIST/NLST done making socket"); + } else { + sessionThread.closeDataSocket(); + return "425 Error opening data socket\r\n"; + } + String mode = sessionThread.isBinaryMode() ? "BINARY" : "ASCII"; + sessionThread.writeString( + "150 Opening "+mode+" mode data connection for file list\r\n"); + myLog.l(Log.DEBUG, "Sent code 150, sending listing string now"); + if(!sessionThread.sendViaDataSocket(listing)) { + myLog.l(Log.DEBUG, "sendViaDataSocket failure"); + sessionThread.closeDataSocket(); + return "426 Data socket or network error\r\n"; + } + sessionThread.closeDataSocket(); + myLog.l(Log.DEBUG, "Listing sendViaDataSocket success"); + sessionThread.writeString("226 Data transmission OK\r\n"); + return null; + } +} diff --git a/src/main/java/org/swiftp/server/CmdAbstractStore.java b/src/main/java/org/swiftp/server/CmdAbstractStore.java index df193bd19dac36ec4d7bc65a15d4e5d5400a5999..cc7465a69dbe93255c48d4167852b35632e892d5 100644 --- a/src/main/java/org/swiftp/server/CmdAbstractStore.java +++ b/src/main/java/org/swiftp/server/CmdAbstractStore.java @@ -1,213 +1,217 @@ -/* -Copyright 2009 David Revell - -This file is part of SwiFTP. - -SwiFTP is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -SwiFTP is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with SwiFTP. If not, see . -*/ - -package org.swiftp.server; - -/** - * Since STOR and APPE are essentially identical except for append vs truncate, - * the common code is in this class, and inherited by CmdSTOR and CmdAPPE. - */ - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; - -import org.swiftp.Defaults; -import org.swiftp.Util; - -import android.util.Log; - - -abstract public class CmdAbstractStore extends FtpCmd { - public static final String message = "TEMPLATE!!"; - - public CmdAbstractStore(SessionThread sessionThread, String input) { - super(sessionThread, CmdAbstractStore.class.toString()); - } - - public void doStorOrAppe(String param, boolean append) { - myLog.l(Log.DEBUG, "STOR/APPE executing with append=" + append); - File storeFile = inputPathToChrootedFile(sessionThread.getWorkingDir(), param); - - String errString = null; - FileOutputStream out = null; - //DedicatedWriter dedicatedWriter = null; -// int origPriority = Thread.currentThread().getPriority(); -// myLog.l(Log.DEBUG, "STOR original priority: " + origPriority); - storing: { - // Get a normalized absolute path for the desired file - if(violatesChroot(storeFile)) { - errString = "550 Invalid name or chroot violation\r\n"; - break storing; - } - if(storeFile.isDirectory()) { - errString = "451 Can't overwrite a directory\r\n"; - break storing; - } - - try { - if(storeFile.exists()) { - if(!append) { - if(!storeFile.delete()) { - errString = "451 Couldn't truncate file\r\n"; - break storing; - } - // Notify other apps that we just deleted a file - Util.deletedFileNotify(storeFile.getPath()); - } - } - out = new FileOutputStream(storeFile, append); - } catch(FileNotFoundException e) { - try { - errString = "451 Couldn't open file \"" + param + "\" aka \"" + - storeFile.getCanonicalPath() + "\" for writing\r\n"; - } catch (IOException io_e) { - errString = "451 Couldn't open file, nested exception\r\n"; - } - break storing; - } - if(!sessionThread.startUsingDataSocket()) { - errString = "425 Couldn't open data socket\r\n"; - break storing; - } - myLog.l(Log.DEBUG, "Data socket ready"); - sessionThread.writeString("150 Data socket ready\r\n"); - byte[] buffer = new byte[Defaults.getDataChunkSize()]; - //dedicatedWriter = new DedicatedWriter(out); - //dedicatedWriter.start(); // start the writer thread executing - //myLog.l(Log.DEBUG, "Started DedicatedWriter"); - int numRead; -// Thread.currentThread().setPriority(Thread.MIN_PRIORITY); -// int newPriority = Thread.currentThread().getPriority(); -// myLog.l(Log.DEBUG, "New STOR prio: " + newPriority); - if(sessionThread.isBinaryMode() ) { - myLog.d("Mode is binary"); - } else { - myLog.d("Mode is ascii"); - } - while(true) { - /*if(dedicatedWriter.checkErrorFlag()) { - errString = "451 File IO problem\r\n"; - break storing; - }*/ - switch(numRead = sessionThread.receiveFromDataSocket(buffer)) { - case -1: - myLog.l(Log.DEBUG, "Returned from final read"); - // We're finished reading - break storing; - case 0: - errString = "426 Couldn't receive data\r\n"; - break storing; - case -2: - errString = "425 Could not connect data socket\r\n"; - break storing; - default: -// myLog.d("Read " + numRead + " bytes from socket"); - try { - //myLog.l(Log.DEBUG, "Enqueueing buffer of " + numRead); - //dedicatedWriter.enqueueBuffer(buffer, numRead); - if(sessionThread.isBinaryMode()) { - out.write(buffer, 0, numRead); - } else { - // ASCII mode, substitute \r\n to \n - int startPos=0, endPos; - for(endPos = 0; endPos < numRead; endPos++ ) { - if(buffer[endPos] == '\r') { - // Our hacky method is to drop all \r - out.write(buffer, startPos, endPos-startPos); - startPos = endPos+1; - } - } - // Write last part of buffer as long as there was something - // left after handling the last \r - if(startPos < numRead) { - out.write(buffer, startPos, endPos-startPos); - } - } - - // Attempted bugfix for transfer stalls. Reopen file periodically. - //bytesSinceReopen += numRead; - //if(bytesSinceReopen >= Defaults.bytes_between_reopen && - // Defaults.do_reopen_hack) { - // myLog.d("Closing and reopening file: " + storeFile); - // out.close(); - // out = new FileOutputStream(storeFile, true/*append*/); - // bytesSinceReopen = 0; - //} - - // Attempted bugfix for transfer stalls. Flush file periodically. - //bytesSinceFlush += numRead; - //if(bytesSinceFlush >= Defaults.bytes_between_flush && - // Defaults.do_flush_hack) { - // myLog.d("Flushing: " + storeFile); - // out.flush(); - // bytesSinceFlush = 0; - //} - - // If this transfer fails, a later APPEND operation might be - // received. In that case, we will need to have flushed the - // previous writes in order for the append to work. The - // filesystem on my G1 doesn't seem to recognized unflushed - // data when appending. - out.flush(); - - } catch (IOException e) { - errString = "451 File IO problem. Device might be full.\r\n"; - myLog.d("Exception while storing: " + e); - myLog.d("Message: " + e.getMessage()); - myLog.d("Stack trace: "); - StackTraceElement[] traceElems = e.getStackTrace(); - for(StackTraceElement elem : traceElems) { - myLog.d(elem.toString()); - } - break storing; - } - break; - } - } - } -// // Clean up the dedicated writer thread -// if(dedicatedWriter != null) { -// dedicatedWriter.exit(); // set its exit flag -// dedicatedWriter.interrupt(); // make sure it wakes up to process the flag -// } -// Thread.currentThread().setPriority(origPriority); - try { -// if(dedicatedWriter != null) { -// dedicatedWriter.exit(); -// } - if(out != null) { - out.close(); - } - } catch (IOException e) {} - - if(errString != null) { - myLog.l(Log.INFO, "STOR error: " + errString.trim()); - sessionThread.writeString(errString); - } else { - sessionThread.writeString("226 Transmission complete\r\n"); - // Notify the music player (and possibly others) that a few file has - // been uploaded. - Util.newFileNotify(storeFile.getPath()); - } - sessionThread.closeDataSocket(); - myLog.l(Log.DEBUG, "STOR finished"); - } -} +/* +Copyright 2009 David Revell + +This file is part of SwiFTP. + +SwiFTP is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +SwiFTP is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with SwiFTP. If not, see . +*/ + +package org.swiftp.server; + +/** + * Since STOR and APPE are essentially identical except for append vs truncate, + * the common code is in this class, and inherited by CmdSTOR and CmdAPPE. + */ + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; + +import org.swiftp.Defaults; +import org.swiftp.Globals; +import org.swiftp.Util; + +import android.util.Log; + +import util.DocumentUtil; +import util.FileUtil; + + +abstract public class CmdAbstractStore extends FtpCmd { + public static final String message = "TEMPLATE!!"; + + public CmdAbstractStore(SessionThread sessionThread, String input) { + super(sessionThread, CmdAbstractStore.class.toString()); + } + + public void doStorOrAppe(String param, boolean append) { + myLog.l(Log.DEBUG, "STOR/APPE executing with append=" + append); + File storeFile = inputPathToChrootedFile(sessionThread.getWorkingDir(), param); + + String errString = null; + FileOutputStream out = null; + //DedicatedWriter dedicatedWriter = null; +// int origPriority = Thread.currentThread().getPriority(); +// myLog.l(Log.DEBUG, "STOR original priority: " + origPriority); + storing: { + // Get a normalized absolute path for the desired file + if(violatesChroot(storeFile)) { + errString = "550 Invalid name or chroot violation\r\n"; + break storing; + } + if(storeFile.isDirectory()) { + errString = "451 Can't overwrite a directory\r\n"; + break storing; + } + + try { + if(storeFile.exists()) { + if(!append) { + if(!storeFile.delete()) { + errString = "451 Couldn't truncate file\r\n"; + break storing; + } + // Notify other apps that we just deleted a file + Util.deletedFileNotify(storeFile.getPath()); + } + } + out = FileUtil.getFileOutputStream(storeFile,append); + } catch(Exception e) { + try { + errString = "451 Couldn't open file \"" + param + "\" aka \"" + + storeFile.getCanonicalPath() + "\" for writing\r\n"; + } catch (IOException io_e) { + errString = "451 Couldn't open file, nested exception\r\n"; + } + break storing; + } + if(!sessionThread.startUsingDataSocket()) { + errString = "425 Couldn't open data socket\r\n"; + break storing; + } + myLog.l(Log.DEBUG, "Data socket ready"); + sessionThread.writeString("150 Data socket ready\r\n"); + byte[] buffer = new byte[Defaults.getDataChunkSize()]; + //dedicatedWriter = new DedicatedWriter(out); + //dedicatedWriter.start(); // start the writer thread executing + //myLog.l(Log.DEBUG, "Started DedicatedWriter"); + int numRead; +// Thread.currentThread().setPriority(Thread.MIN_PRIORITY); +// int newPriority = Thread.currentThread().getPriority(); +// myLog.l(Log.DEBUG, "New STOR prio: " + newPriority); + if(sessionThread.isBinaryMode() ) { + myLog.d("Mode is binary"); + } else { + myLog.d("Mode is ascii"); + } + while(true) { + /*if(dedicatedWriter.checkErrorFlag()) { + errString = "451 File IO problem\r\n"; + break storing; + }*/ + switch(numRead = sessionThread.receiveFromDataSocket(buffer)) { + case -1: + myLog.l(Log.DEBUG, "Returned from final read"); + // We're finished reading + break storing; + case 0: + errString = "426 Couldn't receive data\r\n"; + break storing; + case -2: + errString = "425 Could not connect data socket\r\n"; + break storing; + default: +// myLog.d("Read " + numRead + " bytes from socket"); + try { + //myLog.l(Log.DEBUG, "Enqueueing buffer of " + numRead); + //dedicatedWriter.enqueueBuffer(buffer, numRead); + if(sessionThread.isBinaryMode()) { + out.write(buffer, 0, numRead); + } else { + // ASCII mode, substitute \r\n to \n + int startPos=0, endPos; + for(endPos = 0; endPos < numRead; endPos++ ) { + if(buffer[endPos] == '\r') { + // Our hacky method is to drop all \r + out.write(buffer, startPos, endPos-startPos); + startPos = endPos+1; + } + } + // Write last part of buffer as long as there was something + // left after handling the last \r + if(startPos < numRead) { + out.write(buffer, startPos, endPos-startPos); + } + } + + // Attempted bugfix for transfer stalls. Reopen file periodically. + //bytesSinceReopen += numRead; + //if(bytesSinceReopen >= Defaults.bytes_between_reopen && + // Defaults.do_reopen_hack) { + // myLog.d("Closing and reopening file: " + storeFile); + // out.close(); + // out = new FileOutputStream(storeFile, true/*append*/); + // bytesSinceReopen = 0; + //} + + // Attempted bugfix for transfer stalls. Flush file periodically. + //bytesSinceFlush += numRead; + //if(bytesSinceFlush >= Defaults.bytes_between_flush && + // Defaults.do_flush_hack) { + // myLog.d("Flushing: " + storeFile); + // out.flush(); + // bytesSinceFlush = 0; + //} + + // If this transfer fails, a later APPEND operation might be + // received. In that case, we will need to have flushed the + // previous writes in order for the append to work. The + // filesystem on my G1 doesn't seem to recognized unflushed + // data when appending. + out.flush(); + + } catch (IOException e) { + errString = "451 File IO problem. Device might be full.\r\n"; + myLog.d("Exception while storing: " + e); + myLog.d("Message: " + e.getMessage()); + myLog.d("Stack trace: "); + StackTraceElement[] traceElems = e.getStackTrace(); + for(StackTraceElement elem : traceElems) { + myLog.d(elem.toString()); + } + break storing; + } + break; + } + } + } +// // Clean up the dedicated writer thread +// if(dedicatedWriter != null) { +// dedicatedWriter.exit(); // set its exit flag +// dedicatedWriter.interrupt(); // make sure it wakes up to process the flag +// } +// Thread.currentThread().setPriority(origPriority); + try { +// if(dedicatedWriter != null) { +// dedicatedWriter.exit(); +// } + if(out != null) { + out.close(); + } + } catch (IOException e) {} + + if(errString != null) { + myLog.l(Log.INFO, "STOR error: " + errString.trim()); + sessionThread.writeString(errString); + } else { + sessionThread.writeString("226 Transmission complete\r\n"); + // Notify the music player (and possibly others) that a few file has + // been uploaded. + Util.newFileNotify(storeFile.getPath()); + } + sessionThread.closeDataSocket(); + myLog.l(Log.DEBUG, "STOR finished"); + } +} diff --git a/src/main/java/org/swiftp/server/CmdCDUP.java b/src/main/java/org/swiftp/server/CmdCDUP.java index bc7ee35dd5255ee86dfe7cf09b99489c58609751..eb1602c6cc7f6bda34b99d6e3dfd77be2abe08cd 100644 --- a/src/main/java/org/swiftp/server/CmdCDUP.java +++ b/src/main/java/org/swiftp/server/CmdCDUP.java @@ -1,76 +1,76 @@ -/* -Copyright 2009 David Revell - -This file is part of SwiFTP. - -SwiFTP is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -SwiFTP is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with SwiFTP. If not, see . -*/ - -package org.swiftp.server; - -import java.io.File; -import java.io.IOException; - -import android.util.Log; - -public class CmdCDUP extends FtpCmd implements Runnable { - protected String input; - - public CmdCDUP(SessionThread sessionThread, String input) { - super(sessionThread, CmdCDUP.class.toString()); - } - - @Override - public void run() { - myLog.l(Log.DEBUG, "CDUP executing"); - File newDir; - String errString = null; - mainBlock: { - File workingDir = sessionThread.getWorkingDir(); - newDir = workingDir.getParentFile(); - if(newDir == null) { - errString = "550 Current dir cannot find parent\r\n"; - break mainBlock; - } - // Ensure the new path does not violate the chroot restriction - if(violatesChroot(newDir)) { - errString = "550 Invalid name or chroot violation\r\n"; - break mainBlock; - } - - try { - newDir = newDir.getCanonicalFile(); - if(!newDir.isDirectory()) { - errString = "550 Can't CWD to invalid directory\r\n"; - break mainBlock; - } else if(newDir.canRead()) { - sessionThread.setWorkingDir(newDir); - } else { - errString = "550 That path is inaccessible\r\n"; - break mainBlock; - } - } catch(IOException e) { - errString = "550 Invalid path\r\n"; - break mainBlock; - } - } - if(errString != null) { - sessionThread.writeString(errString); - myLog.i("CDUP error: " + errString); - } else { - sessionThread.writeString("200 CDUP successful\r\n"); - myLog.l(Log.DEBUG, "CDUP success"); - } - } -} +/* +Copyright 2009 David Revell + +This file is part of SwiFTP. + +SwiFTP is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +SwiFTP is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with SwiFTP. If not, see . +*/ + +package org.swiftp.server; + +import java.io.File; +import java.io.IOException; + +import android.util.Log; + +public class CmdCDUP extends FtpCmd implements Runnable { + protected String input; + + public CmdCDUP(SessionThread sessionThread, String input) { + super(sessionThread, CmdCDUP.class.toString()); + } + + @Override + public void run() { + myLog.l(Log.DEBUG, "CDUP executing"); + File newDir; + String errString = null; + mainBlock: { + File workingDir = sessionThread.getWorkingDir(); + newDir = workingDir.getParentFile(); + if(newDir == null) { + errString = "550 Current dir cannot find parent\r\n"; + break mainBlock; + } + // Ensure the new path does not violate the chroot restriction + if(violatesChroot(newDir)) { + errString = "550 Invalid name or chroot violation\r\n"; + break mainBlock; + } + + try { + newDir = newDir.getCanonicalFile(); + if(!newDir.isDirectory()) { + errString = "550 Can't CWD to invalid directory\r\n"; + break mainBlock; + } else if(newDir.canRead()) { + sessionThread.setWorkingDir(newDir); + } else { + errString = "550 That path is inaccessible\r\n"; + break mainBlock; + } + } catch(IOException e) { + errString = "550 Invalid path\r\n"; + break mainBlock; + } + } + if(errString != null) { + sessionThread.writeString(errString); + myLog.i("CDUP error: " + errString); + } else { + sessionThread.writeString("200 CDUP successful\r\n"); + myLog.l(Log.DEBUG, "CDUP success"); + } + } +} diff --git a/src/main/java/org/swiftp/server/CmdCWD.java b/src/main/java/org/swiftp/server/CmdCWD.java index a13e8ca22922e0cc29c6af752cf252728102e0e1..2b5777938a674ccbf323120bd659b66f121033d0 100644 --- a/src/main/java/org/swiftp/server/CmdCWD.java +++ b/src/main/java/org/swiftp/server/CmdCWD.java @@ -1,69 +1,69 @@ -/* -Copyright 2009 David Revell - -This file is part of SwiFTP. - -SwiFTP is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -SwiFTP is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with SwiFTP. If not, see . -*/ - -package org.swiftp.server; - -import java.io.File; -import java.io.IOException; - -import android.util.Log; - -public class CmdCWD extends FtpCmd implements Runnable { - protected String input; - - public CmdCWD(SessionThread sessionThread, String input) { - super(sessionThread, CmdCWD.class.toString()); - this.input = input; - } - - @Override - public void run() { - myLog.l(Log.DEBUG, "CWD executing"); - String param = getParameter(input); - File newDir; - String errString = null; - mainblock: { - newDir = inputPathToChrootedFile(sessionThread.getWorkingDir(), param); - - // Ensure the new path does not violate the chroot restriction - if(violatesChroot(newDir)) { - errString = "550 Invalid name or chroot violation\r\n"; - sessionThread.writeString(errString); - myLog.l(Log.INFO, errString); - break mainblock; - } - - try { - newDir = newDir.getCanonicalFile(); - if(!newDir.isDirectory()) { - sessionThread.writeString("550 Can't CWD to invalid directory\r\n"); - } else if(newDir.canRead()) { - sessionThread.setWorkingDir(newDir); - sessionThread.writeString("250 CWD successful\r\n"); - } else { - sessionThread.writeString("550 That path is inaccessible\r\n"); - } - } catch(IOException e) { - sessionThread.writeString("550 Invalid path\r\n"); - break mainblock; - } - } - myLog.l(Log.DEBUG, "CWD complete"); - } -} +/* +Copyright 2009 David Revell + +This file is part of SwiFTP. + +SwiFTP is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +SwiFTP is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with SwiFTP. If not, see . +*/ + +package org.swiftp.server; + +import java.io.File; +import java.io.IOException; + +import android.util.Log; + +public class CmdCWD extends FtpCmd implements Runnable { + protected String input; + + public CmdCWD(SessionThread sessionThread, String input) { + super(sessionThread, CmdCWD.class.toString()); + this.input = input; + } + + @Override + public void run() { + myLog.l(Log.DEBUG, "CWD executing"); + String param = getParameter(input); + File newDir; + String errString = null; + mainblock: { + newDir = inputPathToChrootedFile(sessionThread.getWorkingDir(), param); + + // Ensure the new path does not violate the chroot restriction + if(violatesChroot(newDir)) { + errString = "550 Invalid name or chroot violation\r\n"; + sessionThread.writeString(errString); + myLog.l(Log.INFO, errString); + break mainblock; + } + + try { + newDir = newDir.getCanonicalFile(); + if(!newDir.isDirectory()) { + sessionThread.writeString("550 Can't CWD to invalid directory\r\n"); + } else if(newDir.canRead()) { + sessionThread.setWorkingDir(newDir); + sessionThread.writeString("250 CWD successful\r\n"); + } else { + sessionThread.writeString("550 That path is inaccessible\r\n"); + } + } catch(IOException e) { + sessionThread.writeString("550 Invalid path\r\n"); + break mainblock; + } + } + myLog.l(Log.DEBUG, "CWD complete"); + } +} diff --git a/src/main/java/org/swiftp/server/CmdDELE.java b/src/main/java/org/swiftp/server/CmdDELE.java index 1613b6863009a310f94ed9896fabe446c060bcad..e17d798336ca5cc5f4298f9497820739a2c4c18e 100644 --- a/src/main/java/org/swiftp/server/CmdDELE.java +++ b/src/main/java/org/swiftp/server/CmdDELE.java @@ -1,60 +1,64 @@ -/* -Copyright 2009 David Revell - -This file is part of SwiFTP. - -SwiFTP is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -SwiFTP is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with SwiFTP. If not, see . -*/ - -package org.swiftp.server; - -import java.io.File; - -import org.swiftp.Util; - -import android.util.Log; - -public class CmdDELE extends FtpCmd implements Runnable { - protected String input; - - public CmdDELE(SessionThread sessionThread, String input) { - super(sessionThread, CmdDELE.class.toString()); - this.input = input; - } - - @Override - public void run() { - myLog.l(Log.INFO, "DELE executing"); - String param = getParameter(input); - File storeFile = inputPathToChrootedFile(sessionThread.getWorkingDir(), param); - String errString = null; - if(violatesChroot(storeFile)) { - errString = "550 Invalid name or chroot violation\r\n"; - } else if(storeFile.isDirectory()) { - errString = "550 Can't DELE a directory\r\n"; - } else if(!storeFile.delete()) { - errString = "450 Error deleting file\r\n"; - } - - if(errString != null) { - sessionThread.writeString(errString); - myLog.l(Log.INFO, "DELE failed: " + errString.trim()); - } else { - sessionThread.writeString("250 File successfully deleted\r\n"); - Util.deletedFileNotify(storeFile.getPath()); - } - myLog.l(Log.INFO, "DELE finished"); - } - -} +/* +Copyright 2009 David Revell + +This file is part of SwiFTP. + +SwiFTP is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +SwiFTP is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with SwiFTP. If not, see . +*/ + +package org.swiftp.server; + +import java.io.File; + +import org.swiftp.Globals; +import org.swiftp.Util; + +import android.util.Log; + +import util.DocumentUtil; +import util.FileUtil; + +public class CmdDELE extends FtpCmd implements Runnable { + protected String input; + + public CmdDELE(SessionThread sessionThread, String input) { + super(sessionThread, CmdDELE.class.toString()); + this.input = input; + } + + @Override + public void run() { + myLog.l(Log.INFO, "DELE executing"); + String param = getParameter(input); + File storeFile = inputPathToChrootedFile(sessionThread.getWorkingDir(), param); + String errString = null; + if(violatesChroot(storeFile)) { + errString = "550 Invalid name or chroot violation\r\n"; + } else if(storeFile.isDirectory()) { + errString = "550 Can't DELE a directory\r\n"; + } else if(!FileUtil.delete(storeFile)) { + errString = "450 Error deleting file\r\n"; + } + + if(errString != null) { + sessionThread.writeString(errString); + myLog.l(Log.INFO, "DELE failed: " + errString.trim()); + } else { + sessionThread.writeString("250 File successfully deleted\r\n"); + Util.deletedFileNotify(storeFile.getPath()); + } + myLog.l(Log.INFO, "DELE finished"); + } + +} diff --git a/src/main/java/org/swiftp/server/CmdFEAT.java b/src/main/java/org/swiftp/server/CmdFEAT.java index b3294a3d842a2e7d7e142b12479d192f104f98b8..f3943f55fa0d4f98a977c4ddecaf010d316d1220 100644 --- a/src/main/java/org/swiftp/server/CmdFEAT.java +++ b/src/main/java/org/swiftp/server/CmdFEAT.java @@ -1,40 +1,40 @@ -/* -Copyright 2009 David Revell - -This file is part of SwiFTP. - -SwiFTP is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -SwiFTP is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with SwiFTP. If not, see . -*/ - -package org.swiftp.server; - -import android.util.Log; - -public class CmdFEAT extends FtpCmd implements Runnable { - public static final String message = "TEMPLATE!!"; - - public CmdFEAT(SessionThread sessionThread, String input) { - super(sessionThread, CmdFEAT.class.toString()); - } - - @Override - public void run() { - //sessionThread.writeString("211 No extended features\r\n"); - sessionThread.writeString("211-Features supported\r\n"); - sessionThread.writeString(" UTF8\r\n"); // advertise UTF8 support (fixes bug 14) - sessionThread.writeString("211 End\r\n"); - myLog.l(Log.DEBUG, "Gave FEAT response"); - } - -} +/* +Copyright 2009 David Revell + +This file is part of SwiFTP. + +SwiFTP is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +SwiFTP is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with SwiFTP. If not, see . +*/ + +package org.swiftp.server; + +import android.util.Log; + +public class CmdFEAT extends FtpCmd implements Runnable { + public static final String message = "TEMPLATE!!"; + + public CmdFEAT(SessionThread sessionThread, String input) { + super(sessionThread, CmdFEAT.class.toString()); + } + + @Override + public void run() { + //sessionThread.writeString("211 No extended features\r\n"); + sessionThread.writeString("211-Features supported\r\n"); + sessionThread.writeString(" UTF8\r\n"); // advertise UTF8 support (fixes bug 14) + sessionThread.writeString("211 End\r\n"); + myLog.l(Log.DEBUG, "Gave FEAT response"); + } + +} diff --git a/src/main/java/org/swiftp/server/CmdLIST.java b/src/main/java/org/swiftp/server/CmdLIST.java index fe6380559c1adfd3f27958b6816b4c26f482a857..6729f0c1b23639bd5cfc7a6f88bfe3cf9b356016 100644 --- a/src/main/java/org/swiftp/server/CmdLIST.java +++ b/src/main/java/org/swiftp/server/CmdLIST.java @@ -1,165 +1,165 @@ -/* -Copyright 2009 David Revell - -This file is part of SwiFTP. - -SwiFTP is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -SwiFTP is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with SwiFTP. If not, see . -*/ - -/* The code that is common to LIST and NLST is implemented in the abstract - * class CmdAbstractListing, which is inherited here. - * CmdLIST and CmdNLST just override the - * makeLsString() function in different ways to provide the different forms - * of output. - */ - -package org.swiftp.server; - -import java.io.File; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Locale; - -import android.util.Log; - -public class CmdLIST extends CmdAbstractListing implements Runnable { - // The approximate number of milliseconds in 6 months - public final static long MS_IN_SIX_MONTHS = 6 * 30 * 24 * 60 * 60 * 1000; - private final String input; - - public CmdLIST(SessionThread sessionThread, String input) { - super(sessionThread, input); - this.input = input; - } - - @Override - public void run() { - String errString = null; - - mainblock: { - String param = getParameter(input); - myLog.d("LIST parameter: " + param); - while(param.startsWith("-")) { - // Skip all dashed -args, if present - myLog.d("LIST is skipping dashed arg " + param); - param = getParameter(param); - } - File fileToList = null; - if(param.equals("")) { - fileToList = sessionThread.getWorkingDir(); - } else { - if(param.contains("*")) { - errString = "550 LIST does not support wildcards\r\n"; - break mainblock; - } - fileToList = new File(sessionThread.getWorkingDir(), param); - if(violatesChroot(fileToList)) { - errString = "450 Listing target violates chroot\r\n"; - break mainblock; - } - } - String listing; - if(fileToList.isDirectory()) { - StringBuilder response = new StringBuilder(); - errString = listDirectory(response, fileToList); - if(errString != null) { - break mainblock; - } - listing = response.toString(); - } else { - listing = makeLsString(fileToList); - if(listing == null) { - errString = "450 Couldn't list that file\r\n"; - break mainblock; - } - } - errString = sendListing(listing); - if(errString != null) { - break mainblock; - } - } - - if(errString != null) { - sessionThread.writeString(errString); - myLog.l(Log.DEBUG, "LIST failed with: " + errString); - } else { - myLog.l(Log.DEBUG, "LIST completed OK"); - } - // The success or error response over the control connection will - // have already been handled by sendListing, so we can just quit now. - } - - // Generates a line of a directory listing in the traditional /bin/ls - // format. - @Override - protected String makeLsString(File file) { - StringBuilder response = new StringBuilder(); - - if(!file.exists()) { - staticLog.l(Log.INFO, "makeLsString had nonexistent file"); - return null; - } - - // See Daniel Bernstein's explanation of /bin/ls format at: - // http://cr.yp.to/ftp/list/binls.html - // This stuff is almost entirely based on his recommendations. - - String lastNamePart = file.getName(); - // Many clients can't handle files containing these symbols - if(lastNamePart.contains("*") || - lastNamePart.contains("/")) - { - staticLog.l(Log.INFO, "Filename omitted due to disallowed character"); - return null; - } else { - // The following line generates many calls in large directories - //staticLog.l(Log.DEBUG, "Filename: " + lastNamePart); - } - - - if(file.isDirectory()) { - response.append("drwxr-xr-x 1 owner group"); - } else { - // todo: think about special files, symlinks, devices - response.append("-rw-r--r-- 1 owner group"); - } - - // The next field is a 13-byte right-justified space-padded file size - long fileSize = file.length(); - String sizeString = new Long(fileSize).toString(); - int padSpaces = 13 - sizeString.length(); - while(padSpaces-- > 0) { - response.append(' '); - } - response.append(sizeString); - - // The format of the timestamp varies depending on whether the mtime - // is 6 months old - long mTime = file.lastModified(); - SimpleDateFormat format; - // Temporarily commented out.. trying to fix Win7 display bug - if(System.currentTimeMillis() - mTime > MS_IN_SIX_MONTHS) { - // The mtime is less than 6 months ago - format = new SimpleDateFormat(" MMM dd HH:mm ", Locale.US); - } else { - // The mtime is more than 6 months ago - format = new SimpleDateFormat(" MMM dd yyyy ", Locale.US); - } - response.append(format.format(new Date(file.lastModified()))); - response.append(lastNamePart); - response.append("\r\n"); - return response.toString(); - } - -} +/* +Copyright 2009 David Revell + +This file is part of SwiFTP. + +SwiFTP is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +SwiFTP is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with SwiFTP. If not, see . +*/ + +/* The code that is common to LIST and NLST is implemented in the abstract + * class CmdAbstractListing, which is inherited here. + * CmdLIST and CmdNLST just override the + * makeLsString() function in different ways to provide the different forms + * of output. + */ + +package org.swiftp.server; + +import java.io.File; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +import android.util.Log; + +public class CmdLIST extends CmdAbstractListing implements Runnable { + // The approximate number of milliseconds in 6 months + public final static long MS_IN_SIX_MONTHS = 6 * 30 * 24 * 60 * 60 * 1000; + private final String input; + + public CmdLIST(SessionThread sessionThread, String input) { + super(sessionThread, input); + this.input = input; + } + + @Override + public void run() { + String errString = null; + + mainblock: { + String param = getParameter(input); + myLog.d("LIST parameter: " + param); + while(param.startsWith("-")) { + // Skip all dashed -args, if present + myLog.d("LIST is skipping dashed arg " + param); + param = getParameter(param); + } + File fileToList = null; + if(param.equals("")) { + fileToList = sessionThread.getWorkingDir(); + } else { + if(param.contains("*")) { + errString = "550 LIST does not support wildcards\r\n"; + break mainblock; + } + fileToList = new File(sessionThread.getWorkingDir(), param); + if(violatesChroot(fileToList)) { + errString = "450 Listing target violates chroot\r\n"; + break mainblock; + } + } + String listing; + if(fileToList.isDirectory()) { + StringBuilder response = new StringBuilder(); + errString = listDirectory(response, fileToList); + if(errString != null) { + break mainblock; + } + listing = response.toString(); + } else { + listing = makeLsString(fileToList); + if(listing == null) { + errString = "450 Couldn't list that file\r\n"; + break mainblock; + } + } + errString = sendListing(listing); + if(errString != null) { + break mainblock; + } + } + + if(errString != null) { + sessionThread.writeString(errString); + myLog.l(Log.DEBUG, "LIST failed with: " + errString); + } else { + myLog.l(Log.DEBUG, "LIST completed OK"); + } + // The success or error response over the control connection will + // have already been handled by sendListing, so we can just quit now. + } + + // Generates a line of a directory listing in the traditional /bin/ls + // format. + @Override + protected String makeLsString(File file) { + StringBuilder response = new StringBuilder(); + + if(!file.exists()) { + staticLog.l(Log.INFO, "makeLsString had nonexistent file"); + return null; + } + + // See Daniel Bernstein's explanation of /bin/ls format at: + // http://cr.yp.to/ftp/list/binls.html + // This stuff is almost entirely based on his recommendations. + + String lastNamePart = file.getName(); + // Many clients can't handle files containing these symbols + if(lastNamePart.contains("*") || + lastNamePart.contains("/")) + { + staticLog.l(Log.INFO, "Filename omitted due to disallowed character"); + return null; + } else { + // The following line generates many calls in large directories + //staticLog.l(Log.DEBUG, "Filename: " + lastNamePart); + } + + + if(file.isDirectory()) { + response.append("drwxr-xr-x 1 owner group"); + } else { + // todo: think about special files, symlinks, devices + response.append("-rw-r--r-- 1 owner group"); + } + + // The next field is a 13-byte right-justified space-padded file size + long fileSize = file.length(); + String sizeString = new Long(fileSize).toString(); + int padSpaces = 13 - sizeString.length(); + while(padSpaces-- > 0) { + response.append(' '); + } + response.append(sizeString); + + // The format of the timestamp varies depending on whether the mtime + // is 6 months old + long mTime = file.lastModified(); + SimpleDateFormat format; + // Temporarily commented out.. trying to fix Win7 display bug + if(System.currentTimeMillis() - mTime > MS_IN_SIX_MONTHS) { + // The mtime is less than 6 months ago + format = new SimpleDateFormat(" MMM dd HH:mm ", Locale.US); + } else { + // The mtime is more than 6 months ago + format = new SimpleDateFormat(" MMM dd yyyy ", Locale.US); + } + response.append(format.format(new Date(file.lastModified()))); + response.append(lastNamePart); + response.append("\r\n"); + return response.toString(); + } + +} diff --git a/src/main/java/org/swiftp/server/CmdMKD.java b/src/main/java/org/swiftp/server/CmdMKD.java index 0b88776c5e222502f68bb6892f6edeeb8a3f0c30..893765b618b11045d70bfffccafd0dfd3d4bde6a 100644 --- a/src/main/java/org/swiftp/server/CmdMKD.java +++ b/src/main/java/org/swiftp/server/CmdMKD.java @@ -1,70 +1,76 @@ -/* -Copyright 2009 David Revell - -This file is part of SwiFTP. - -SwiFTP is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -SwiFTP is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with SwiFTP. If not, see . -*/ - -package org.swiftp.server; - -import java.io.File; - -import android.util.Log; - -public class CmdMKD extends FtpCmd implements Runnable { - String input; - - public CmdMKD(SessionThread sessionThread, String input) { - super(sessionThread, CmdMKD.class.toString()); - this.input = input; - } - - @Override - public void run() { - myLog.l(Log.DEBUG, "MKD executing"); - String param = getParameter(input); - File toCreate; - String errString = null; - mainblock: { - // If the param is an absolute path, use it as is. If it's a - // relative path, prepend the current working directory. - if(param.length() < 1) { - errString = "550 Invalid name\r\n"; - break mainblock; - } - toCreate = inputPathToChrootedFile(sessionThread.getWorkingDir(), param); - if(violatesChroot(toCreate)) { - errString = "550 Invalid name or chroot violation\r\n"; - break mainblock; - } - if(toCreate.exists()) { - errString = "550 Already exists\r\n"; - break mainblock; - } - if(!toCreate.mkdir()) { - errString = "550 Error making directory (permissions?)\r\n"; - break mainblock; - } - } - if(errString != null) { - sessionThread.writeString(errString); - myLog.l(Log.INFO, "MKD error: " + errString.trim()); - } else { - sessionThread.writeString("250 Directory created\r\n"); - } - myLog.l(Log.INFO, "MKD complete"); - } - -} +/* +Copyright 2009 David Revell + +This file is part of SwiFTP. + +SwiFTP is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +SwiFTP is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with SwiFTP. If not, see . +*/ + +package org.swiftp.server; + +import java.io.File; + +import android.support.v4.provider.DocumentFile; +import android.util.Log; + +import org.swiftp.Globals; + +import util.DocumentUtil; +import util.FileUtil; + +public class CmdMKD extends FtpCmd implements Runnable { + String input; + + public CmdMKD(SessionThread sessionThread, String input) { + super(sessionThread, CmdMKD.class.toString()); + this.input = input; + } + + @Override + public void run() { + myLog.l(Log.DEBUG, "MKD executing"); + String param = getParameter(input); + File toCreate; + String errString = null; + mainblock: { + // If the param is an absolute path, use it as is. If it's a + // relative path, prepend the current working directory. + if(param.length() < 1) { + errString = "550 Invalid name\r\n"; + break mainblock; + } + toCreate = inputPathToChrootedFile(sessionThread.getWorkingDir(), param); + if(violatesChroot(toCreate)) { + errString = "550 Invalid name or chroot violation\r\n"; + break mainblock; + } + if(toCreate.exists()) { + errString = "550 Already exists\r\n"; + break mainblock; + } + if(!FileUtil.mkdir(toCreate)) { + errString = "550 Error making directory (permissions?)\r\n"; + break mainblock; + } + } + if(errString != null) { + sessionThread.writeString(errString); + myLog.l(Log.INFO, "MKD error: " + errString.trim()); + } else { + sessionThread.writeString("250 Directory created\r\n"); + } + myLog.l(Log.INFO, "MKD complete"); + } + +} diff --git a/src/main/java/org/swiftp/server/CmdMap.java b/src/main/java/org/swiftp/server/CmdMap.java index 628c91c6b60584263cf9133b71872a1a262a4db6..b9c657a57c8890d8614a71ea2f5da070af2b0747 100644 --- a/src/main/java/org/swiftp/server/CmdMap.java +++ b/src/main/java/org/swiftp/server/CmdMap.java @@ -1,48 +1,48 @@ -/* -Copyright 2009 David Revell - -This file is part of SwiFTP. - -SwiFTP is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -SwiFTP is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with SwiFTP. If not, see . -*/ - -package org.swiftp.server; - -public class CmdMap { - protected Class cmdClass; - String name; - - - public CmdMap(String name, Class cmdClass) { - super(); - this.name = name; - this.cmdClass = cmdClass; - } - - public Class getCommand() { - return cmdClass; - } - - public void setCommand(Class cmdClass) { - this.cmdClass = cmdClass; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } -} +/* +Copyright 2009 David Revell + +This file is part of SwiFTP. + +SwiFTP is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +SwiFTP is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with SwiFTP. If not, see . +*/ + +package org.swiftp.server; + +public class CmdMap { + protected Class cmdClass; + String name; + + + public CmdMap(String name, Class cmdClass) { + super(); + this.name = name; + this.cmdClass = cmdClass; + } + + public Class getCommand() { + return cmdClass; + } + + public void setCommand(Class cmdClass) { + this.cmdClass = cmdClass; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/src/main/java/org/swiftp/server/CmdNLST.java b/src/main/java/org/swiftp/server/CmdNLST.java index 87ec292826d6b5e7edecba7a01a043f857521f9c..fd84a78a0168d3d1ff94dd13a3172b33367e5e3e 100644 --- a/src/main/java/org/swiftp/server/CmdNLST.java +++ b/src/main/java/org/swiftp/server/CmdNLST.java @@ -1,128 +1,128 @@ -/* -Copyright 2009 David Revell - -This file is part of SwiFTP. - -SwiFTP is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -SwiFTP is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with SwiFTP. If not, see . -*/ - -/* The code that is common to LIST and NLST is implemented in the abstract - * class CmdAbstractListing, which is inherited here. - * CmdLIST and CmdNLST just override the - * makeLsString() function in different ways to provide the different forms - * of output. - */ - -package org.swiftp.server; - -import java.io.File; - -import android.util.Log; - -public class CmdNLST extends CmdAbstractListing implements Runnable { - // The approximate number of milliseconds in 6 months - public final static long MS_IN_SIX_MONTHS = 6 * 30 * 24 * 60 * 60 * 1000; - private final String input; - - - public CmdNLST(SessionThread sessionThread, String input) { - super(sessionThread, input); - this.input = input; - } - - @Override - public void run() { - String errString = null; - - mainblock: { - String param = getParameter(input); - if(param.startsWith("-")) { - // Ignore options to list, which start with a dash - param = ""; - } - File fileToList = null; - if(param.equals("")) { - fileToList = sessionThread.getWorkingDir(); - } else { - if(param.contains("*")) { - errString = "550 NLST does not support wildcards\r\n"; - break mainblock; - } - fileToList = new File(sessionThread.getWorkingDir(), param); - if(violatesChroot(fileToList)) { - errString = "450 Listing target violates chroot\r\n"; - break mainblock; - } else if(fileToList.isFile()) { - // Bernstein suggests that NLST should fail when a - // parameter is given and the parameter names a regular - // file (not a directory). - errString = "550 NLST for regular files is unsupported\r\n"; - break mainblock; - } - } - String listing; - if(fileToList.isDirectory()) { - StringBuilder response = new StringBuilder(); - errString = listDirectory(response, fileToList); - if(errString != null) { - break mainblock; - } - listing = response.toString(); - } else { - listing = makeLsString(fileToList); - if(listing == null) { - errString = "450 Couldn't list that file\r\n"; - break mainblock; - } - } - errString = sendListing(listing); - if(errString != null) { - break mainblock; - } - } - - if(errString != null) { - sessionThread.writeString(errString); - myLog.l(Log.DEBUG, "NLST failed with: " + errString); - } else { - myLog.l(Log.DEBUG, "NLST completed OK"); - } - // The success or error response over the control connection will - // have already been handled by sendListing, so we can just quit now. - } - - @Override - protected String makeLsString(File file) { - if(!file.exists()) { - staticLog.l(Log.INFO, "makeLsString had nonexistent file"); - return null; - } - - // See Daniel Bernstein's explanation of NLST format at: - // http://cr.yp.to/ftp/list/binls.html - // This stuff is almost entirely based on his recommendations. - - String lastNamePart = file.getName(); - // Many clients can't handle files containing these symbols - if(lastNamePart.contains("*") || - lastNamePart.contains("/")) - { - staticLog.l(Log.INFO, "Filename omitted due to disallowed character"); - return null; - } else { - staticLog.l(Log.DEBUG, "Filename: " + lastNamePart ); - return lastNamePart + "\r\n"; - } - } -} +/* +Copyright 2009 David Revell + +This file is part of SwiFTP. + +SwiFTP is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +SwiFTP is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with SwiFTP. If not, see . +*/ + +/* The code that is common to LIST and NLST is implemented in the abstract + * class CmdAbstractListing, which is inherited here. + * CmdLIST and CmdNLST just override the + * makeLsString() function in different ways to provide the different forms + * of output. + */ + +package org.swiftp.server; + +import java.io.File; + +import android.util.Log; + +public class CmdNLST extends CmdAbstractListing implements Runnable { + // The approximate number of milliseconds in 6 months + public final static long MS_IN_SIX_MONTHS = 6 * 30 * 24 * 60 * 60 * 1000; + private final String input; + + + public CmdNLST(SessionThread sessionThread, String input) { + super(sessionThread, input); + this.input = input; + } + + @Override + public void run() { + String errString = null; + + mainblock: { + String param = getParameter(input); + if(param.startsWith("-")) { + // Ignore options to list, which start with a dash + param = ""; + } + File fileToList = null; + if(param.equals("")) { + fileToList = sessionThread.getWorkingDir(); + } else { + if(param.contains("*")) { + errString = "550 NLST does not support wildcards\r\n"; + break mainblock; + } + fileToList = new File(sessionThread.getWorkingDir(), param); + if(violatesChroot(fileToList)) { + errString = "450 Listing target violates chroot\r\n"; + break mainblock; + } else if(fileToList.isFile()) { + // Bernstein suggests that NLST should fail when a + // parameter is given and the parameter names a regular + // file (not a directory). + errString = "550 NLST for regular files is unsupported\r\n"; + break mainblock; + } + } + String listing; + if(fileToList.isDirectory()) { + StringBuilder response = new StringBuilder(); + errString = listDirectory(response, fileToList); + if(errString != null) { + break mainblock; + } + listing = response.toString(); + } else { + listing = makeLsString(fileToList); + if(listing == null) { + errString = "450 Couldn't list that file\r\n"; + break mainblock; + } + } + errString = sendListing(listing); + if(errString != null) { + break mainblock; + } + } + + if(errString != null) { + sessionThread.writeString(errString); + myLog.l(Log.DEBUG, "NLST failed with: " + errString); + } else { + myLog.l(Log.DEBUG, "NLST completed OK"); + } + // The success or error response over the control connection will + // have already been handled by sendListing, so we can just quit now. + } + + @Override + protected String makeLsString(File file) { + if(!file.exists()) { + staticLog.l(Log.INFO, "makeLsString had nonexistent file"); + return null; + } + + // See Daniel Bernstein's explanation of NLST format at: + // http://cr.yp.to/ftp/list/binls.html + // This stuff is almost entirely based on his recommendations. + + String lastNamePart = file.getName(); + // Many clients can't handle files containing these symbols + if(lastNamePart.contains("*") || + lastNamePart.contains("/")) + { + staticLog.l(Log.INFO, "Filename omitted due to disallowed character"); + return null; + } else { + staticLog.l(Log.DEBUG, "Filename: " + lastNamePart ); + return lastNamePart + "\r\n"; + } + } +} diff --git a/src/main/java/org/swiftp/server/CmdNOOP.java b/src/main/java/org/swiftp/server/CmdNOOP.java index 422ab2bf80236a1654197680b2afaab00c8fe918..224cc0cced4d39f49a0517172a3a4fd54c366178 100644 --- a/src/main/java/org/swiftp/server/CmdNOOP.java +++ b/src/main/java/org/swiftp/server/CmdNOOP.java @@ -1,36 +1,36 @@ -/* -Copyright 2009 David Revell - -This file is part of SwiFTP. - -SwiFTP is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -SwiFTP is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with SwiFTP. If not, see . -*/ - -package org.swiftp.server; - - -public class CmdNOOP extends FtpCmd implements Runnable { - public static final String message = "TEMPLATE!!"; - - public CmdNOOP(SessionThread sessionThread, String input) { - super(sessionThread, CmdNOOP.class.toString()); - } - - @Override - public void run() { - sessionThread.writeString("200 NOOP ok\r\n"); - //myLog.l(Log.INFO, "Executing NOOP, done"); - } - -} +/* +Copyright 2009 David Revell + +This file is part of SwiFTP. + +SwiFTP is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +SwiFTP is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with SwiFTP. If not, see . +*/ + +package org.swiftp.server; + + +public class CmdNOOP extends FtpCmd implements Runnable { + public static final String message = "TEMPLATE!!"; + + public CmdNOOP(SessionThread sessionThread, String input) { + super(sessionThread, CmdNOOP.class.toString()); + } + + @Override + public void run() { + sessionThread.writeString("200 NOOP ok\r\n"); + //myLog.l(Log.INFO, "Executing NOOP, done"); + } + +} diff --git a/src/main/java/org/swiftp/server/CmdOPTS.java b/src/main/java/org/swiftp/server/CmdOPTS.java index 5b0ba20682666bfca86bb163914a8b1f0ef00650..b76644fe51b13ff2fedac5e5bde28116a9da8744 100644 --- a/src/main/java/org/swiftp/server/CmdOPTS.java +++ b/src/main/java/org/swiftp/server/CmdOPTS.java @@ -1,76 +1,76 @@ -/* -Copyright 2009 David Revell - -This file is part of SwiFTP. - -SwiFTP is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -SwiFTP is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with SwiFTP. If not, see . -*/ - -package org.swiftp.server; - - -public class CmdOPTS extends FtpCmd implements Runnable { - public static final String message = "TEMPLATE!!"; - private final String input; - - public CmdOPTS(SessionThread sessionThread, String input) { - super(sessionThread, CmdOPTS.class.toString()); - this.input = input; - } - - @Override - public void run() { - String param = getParameter(input); - String errString = null; - - mainBlock: { - if(param == null) { - errString = "550 Need argument to OPTS\r\n"; - myLog.w("Couldn't understand empty OPTS command"); - break mainBlock; - } - String[] splits = param.split(" "); - if(splits.length != 2) { - errString = "550 Malformed OPTS command\r\n"; - myLog.w("Couldn't parse OPTS command"); - break mainBlock; - } - String optName = splits[0].toUpperCase(); - String optVal = splits[1].toUpperCase(); - if(optName.equals("UTF8")) { - // OK, whatever. Don't really know what to do here. We - // always operate in UTF8 mode. - if(optVal.equals("ON")) { - myLog.d("Got OPTS UTF8 ON"); - sessionThread.setEncoding("UTF-8"); - } else { - myLog.i("Ignoring OPTS UTF8 for something besides ON"); - } - break mainBlock; - } else { - myLog.d("Unrecognized OPTS option: " + optName); - errString = "502 Unrecognized option\r\n"; - break mainBlock; - } - } - if(errString != null) { - sessionThread.writeString(errString); - myLog.i("Template log message"); - } else { - sessionThread.writeString("200 OPTS accepted\r\n"); - myLog.d("Handled OPTS ok"); - } - } - -} +/* +Copyright 2009 David Revell + +This file is part of SwiFTP. + +SwiFTP is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +SwiFTP is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with SwiFTP. If not, see . +*/ + +package org.swiftp.server; + + +public class CmdOPTS extends FtpCmd implements Runnable { + public static final String message = "TEMPLATE!!"; + private final String input; + + public CmdOPTS(SessionThread sessionThread, String input) { + super(sessionThread, CmdOPTS.class.toString()); + this.input = input; + } + + @Override + public void run() { + String param = getParameter(input); + String errString = null; + + mainBlock: { + if(param == null) { + errString = "550 Need argument to OPTS\r\n"; + myLog.w("Couldn't understand empty OPTS command"); + break mainBlock; + } + String[] splits = param.split(" "); + if(splits.length != 2) { + errString = "550 Malformed OPTS command\r\n"; + myLog.w("Couldn't parse OPTS command"); + break mainBlock; + } + String optName = splits[0].toUpperCase(); + String optVal = splits[1].toUpperCase(); + if(optName.equals("UTF8")) { + // OK, whatever. Don't really know what to do here. We + // always operate in UTF8 mode. + if(optVal.equals("ON")) { + myLog.d("Got OPTS UTF8 ON"); + sessionThread.setEncoding("UTF-8"); + } else { + myLog.i("Ignoring OPTS UTF8 for something besides ON"); + } + break mainBlock; + } else { + myLog.d("Unrecognized OPTS option: " + optName); + errString = "502 Unrecognized option\r\n"; + break mainBlock; + } + } + if(errString != null) { + sessionThread.writeString(errString); + myLog.i("Template log message"); + } else { + sessionThread.writeString("200 OPTS accepted\r\n"); + myLog.d("Handled OPTS ok"); + } + } + +} diff --git a/src/main/java/org/swiftp/server/CmdPASS.java b/src/main/java/org/swiftp/server/CmdPASS.java index df205751a29fb611b00473772e25a62b6f453a4f..67d30755e780aee33f07c3de36aa0bab22c79210 100644 --- a/src/main/java/org/swiftp/server/CmdPASS.java +++ b/src/main/java/org/swiftp/server/CmdPASS.java @@ -1,96 +1,96 @@ -/* -Copyright 2009 David Revell - -This file is part of SwiFTP. - -SwiFTP is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -SwiFTP is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with SwiFTP. If not, see . -*/ - -package org.swiftp.server; - -import org.swiftp.Globals; -import org.swiftp.R; -import org.swiftp.Util; - - -import android.content.Context; -import android.content.SharedPreferences; -import android.preference.PreferenceManager; -import android.util.Log; - -public class CmdPASS extends FtpCmd implements Runnable { - String input; - - public CmdPASS(SessionThread sessionThread, String input) { - // We can just discard the password for now. We're just - // following the expected dialogue, we're going to allow - // access in any case. - super(sessionThread, CmdPASS.class.toString()); - this.input = input; - } - - @Override - public void run() { - // User must have already executed a USER command to - // populate the Account object's username - myLog.l(Log.DEBUG, "Executing PASS"); - - String attemptPassword = getParameter(input, true); // silent - String attemptUsername = sessionThread.account.getUsername(); - if(attemptUsername == null) { - sessionThread.writeString("503 Must send USER first\r\n"); - return; - } - Context ctx = Globals.getContext(); - if(ctx == null) { - // This will probably never happen, since the global - // context is configured by the Service - myLog.l(Log.ERROR, "No global context in PASS\r\n"); - } - String password; - String username; - SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(ctx); - username = settings.getString(ctx.getString(R.string.key_username), ""); - password = settings.getString(ctx.getString(R.string.key_ftp_pwd), ""); -// username = Util.getSP(ctx, "ftp.username"); -// password = Util.getSP(ctx, "ftp.pwd"); - - if (username.equals("")) { - username = Util.getCode(ctx); - } - if (password.equals("")) { - password = Util.getCode(ctx); - } - //username = settings.getString("username", null); - //password = settings.getString("password", null); - if(username == null || password == null) { - myLog.l(Log.ERROR, "Username or password misconfigured"); - sessionThread.writeString("500 Internal error during authentication"); - } else if(username.equals(attemptUsername) && - password.equals(attemptPassword)) { - sessionThread.writeString("230 Access granted\r\n"); - myLog.l(Log.INFO, "User " + username + " password verified"); - sessionThread.authAttempt(true); - } else { - try { - // If the login failed, sleep for one second to foil - // brute force attacks - Thread.sleep(1000); - } catch(InterruptedException e) {} - myLog.l(Log.INFO, "Failed authentication"); - sessionThread.writeString("530 Login incorrect.\r\n"); - sessionThread.authAttempt(false); - } - } -} +/* +Copyright 2009 David Revell + +This file is part of SwiFTP. + +SwiFTP is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +SwiFTP is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with SwiFTP. If not, see . +*/ + +package org.swiftp.server; + +import org.swiftp.Globals; +import org.swiftp.R; +import org.swiftp.Util; + + +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.util.Log; + +public class CmdPASS extends FtpCmd implements Runnable { + String input; + + public CmdPASS(SessionThread sessionThread, String input) { + // We can just discard the password for now. We're just + // following the expected dialogue, we're going to allow + // access in any case. + super(sessionThread, CmdPASS.class.toString()); + this.input = input; + } + + @Override + public void run() { + // User must have already executed a USER command to + // populate the Account object's username + myLog.l(Log.DEBUG, "Executing PASS"); + + String attemptPassword = getParameter(input, true); // silent + String attemptUsername = sessionThread.account.getUsername(); + if(attemptUsername == null) { + sessionThread.writeString("503 Must send USER first\r\n"); + return; + } + Context ctx = Globals.getContext(); + if(ctx == null) { + // This will probably never happen, since the global + // context is configured by the Service + myLog.l(Log.ERROR, "No global context in PASS\r\n"); + } + String password; + String username; + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(ctx); + username = settings.getString(ctx.getString(R.string.key_username), ""); + password = settings.getString(ctx.getString(R.string.key_ftp_pwd), ""); +// username = Util.getSP(ctx, "ftp.username"); +// password = Util.getSP(ctx, "ftp.pwd"); + + if (username.equals("")) { + username = Util.getCode(ctx); + } + if (password.equals("")) { + password = Util.getCode(ctx); + } + //username = settings.getString("username", null); + //password = settings.getString("password", null); + if(username == null || password == null) { + myLog.l(Log.ERROR, "Username or password misconfigured"); + sessionThread.writeString("500 Internal error during authentication"); + } else if(username.equals(attemptUsername) && + password.equals(attemptPassword)) { + sessionThread.writeString("230 Access granted\r\n"); + myLog.l(Log.INFO, "User " + username + " password verified"); + sessionThread.authAttempt(true); + } else { + try { + // If the login failed, sleep for one second to foil + // brute force attacks + Thread.sleep(1000); + } catch(InterruptedException e) {} + myLog.l(Log.INFO, "Failed authentication"); + sessionThread.writeString("530 Login incorrect.\r\n"); + sessionThread.authAttempt(false); + } + } +} diff --git a/src/main/java/org/swiftp/server/CmdPASV.java b/src/main/java/org/swiftp/server/CmdPASV.java index 83754fcba4b1b460c1203a1eb0358daca64c7eb7..e1da05a6c534dafec6121ebf6283e5a5d362fed4 100644 --- a/src/main/java/org/swiftp/server/CmdPASV.java +++ b/src/main/java/org/swiftp/server/CmdPASV.java @@ -1,72 +1,72 @@ -/* -Copyright 2009 David Revell - -This file is part of SwiFTP. - -SwiFTP is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -SwiFTP is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with SwiFTP. If not, see . -*/ - -package org.swiftp.server; - -import java.net.InetAddress; - -import android.util.Log; - -public class CmdPASV extends FtpCmd implements Runnable { - //public static final String message = "TEMPLATE!!"; - - public CmdPASV(SessionThread sessionThread, String input) { - super(sessionThread, CmdPASV.class.toString()); - } - - @Override - public void run() { - String cantOpen = "502 Couldn't open a port\r\n"; - myLog.l(Log.DEBUG, "PASV running"); - int port; - if((port = sessionThread.onPasv()) == 0) { - // There was a problem opening a port - myLog.l(Log.ERROR, "Couldn't open a port for PASV"); - sessionThread.writeString(cantOpen); - return; - } - InetAddress addr = sessionThread.getDataSocketPasvIp(); - - if(addr == null) { - myLog.l(Log.ERROR, "PASV IP string invalid"); - sessionThread.writeString(cantOpen); - return; - } - myLog.d("PASV sending IP: " + addr.getHostAddress()); - if(port < 1) { - myLog.l(Log.ERROR, "PASV port number invalid"); - sessionThread.writeString(cantOpen); - return; - } - StringBuilder response = new StringBuilder( - "227 Entering Passive Mode ("); - // Output our IP address in the format xxx,xxx,xxx,xxx - response.append(addr.getHostAddress().replace('.', ',')); - response.append(","); - - // Output our port in the format p1,p2 where port=p1*256+p2 - response.append(port / 256); - response.append(","); - response.append(port % 256); - response.append(").\r\n"); - String responseString = response.toString(); - sessionThread.writeString(responseString); - myLog.l(Log.DEBUG, "PASV completed, sent: " + responseString); - } -} +/* +Copyright 2009 David Revell + +This file is part of SwiFTP. + +SwiFTP is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +SwiFTP is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with SwiFTP. If not, see . +*/ + +package org.swiftp.server; + +import java.net.InetAddress; + +import android.util.Log; + +public class CmdPASV extends FtpCmd implements Runnable { + //public static final String message = "TEMPLATE!!"; + + public CmdPASV(SessionThread sessionThread, String input) { + super(sessionThread, CmdPASV.class.toString()); + } + + @Override + public void run() { + String cantOpen = "502 Couldn't open a port\r\n"; + myLog.l(Log.DEBUG, "PASV running"); + int port; + if((port = sessionThread.onPasv()) == 0) { + // There was a problem opening a port + myLog.l(Log.ERROR, "Couldn't open a port for PASV"); + sessionThread.writeString(cantOpen); + return; + } + InetAddress addr = sessionThread.getDataSocketPasvIp(); + + if(addr == null) { + myLog.l(Log.ERROR, "PASV IP string invalid"); + sessionThread.writeString(cantOpen); + return; + } + myLog.d("PASV sending IP: " + addr.getHostAddress()); + if(port < 1) { + myLog.l(Log.ERROR, "PASV port number invalid"); + sessionThread.writeString(cantOpen); + return; + } + StringBuilder response = new StringBuilder( + "227 Entering Passive Mode ("); + // Output our IP address in the format xxx,xxx,xxx,xxx + response.append(addr.getHostAddress().replace('.', ',')); + response.append(","); + + // Output our port in the format p1,p2 where port=p1*256+p2 + response.append(port / 256); + response.append(","); + response.append(port % 256); + response.append(").\r\n"); + String responseString = response.toString(); + sessionThread.writeString(responseString); + myLog.l(Log.DEBUG, "PASV completed, sent: " + responseString); + } +} diff --git a/src/main/java/org/swiftp/server/CmdPORT.java b/src/main/java/org/swiftp/server/CmdPORT.java index a78874bf45272c056fea81945acd324a6292a388..6eff5ee51b8559d14777b59d9cc27e62b9f834ba 100644 --- a/src/main/java/org/swiftp/server/CmdPORT.java +++ b/src/main/java/org/swiftp/server/CmdPORT.java @@ -1,98 +1,98 @@ -/* -Copyright 2009 David Revell - -This file is part of SwiFTP. - -SwiFTP is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -SwiFTP is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with SwiFTP. If not, see . -*/ - -package org.swiftp.server; - -import java.net.InetAddress; -import java.net.UnknownHostException; - -import android.util.Log; - -public class CmdPORT extends FtpCmd implements Runnable { - //public static final String message = "TEMPLATE!!"; - String input; - - public CmdPORT(SessionThread sessionThread, String input) { - super(sessionThread, CmdPORT.class.toString()); - this.input = input; - } - - @Override - public void run() { - myLog.l(Log.DEBUG, "PORT running"); - String errString = null; - mainBlock: { - String param = getParameter(input); - if(param.contains("|") && param.contains("::")) { - errString = "550 No IPv6 support, reconfigure your client\r\n"; - break mainBlock; - } - String[] substrs = param.split(","); - if(substrs.length != 6) { - errString = "550 Malformed PORT argument\r\n"; - break mainBlock; - } - for(int i=0; i 3) - { - errString = "550 Invalid PORT argument: " + substrs[i] + - "\r\n"; - break mainBlock; - } - } - byte[] ipBytes = new byte[4]; - for(int i=0; i<4; i++) { - try { - // We have to manually convert unsigned to signed - // byte representation. - int ipByteAsInt = Integer.parseInt(substrs[i]); - if(ipByteAsInt >= 128) { - ipByteAsInt -= 256; - } - ipBytes[i] = (byte)ipByteAsInt; - } catch (Exception e) { - errString = "550 Invalid PORT format: " - + substrs[i] + "\r\n"; - break mainBlock; - } - } - InetAddress inetAddr; - try { - inetAddr = InetAddress.getByAddress(ipBytes); - } catch (UnknownHostException e) { - errString = "550 Unknown host\r\n"; - break mainBlock; - } - - int port = Integer.parseInt(substrs[4]) * 256 + - Integer.parseInt(substrs[5]); - - sessionThread.onPort(inetAddr, port); - } - if(errString == null) { - sessionThread.writeString("200 PORT OK\r\n"); - myLog.l(Log.DEBUG, "PORT completed"); - } else { - myLog.l(Log.INFO, "PORT error: " + errString); - sessionThread.writeString(errString); - } - } -} +/* +Copyright 2009 David Revell + +This file is part of SwiFTP. + +SwiFTP is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +SwiFTP is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with SwiFTP. If not, see . +*/ + +package org.swiftp.server; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +import android.util.Log; + +public class CmdPORT extends FtpCmd implements Runnable { + //public static final String message = "TEMPLATE!!"; + String input; + + public CmdPORT(SessionThread sessionThread, String input) { + super(sessionThread, CmdPORT.class.toString()); + this.input = input; + } + + @Override + public void run() { + myLog.l(Log.DEBUG, "PORT running"); + String errString = null; + mainBlock: { + String param = getParameter(input); + if(param.contains("|") && param.contains("::")) { + errString = "550 No IPv6 support, reconfigure your client\r\n"; + break mainBlock; + } + String[] substrs = param.split(","); + if(substrs.length != 6) { + errString = "550 Malformed PORT argument\r\n"; + break mainBlock; + } + for(int i=0; i 3) + { + errString = "550 Invalid PORT argument: " + substrs[i] + + "\r\n"; + break mainBlock; + } + } + byte[] ipBytes = new byte[4]; + for(int i=0; i<4; i++) { + try { + // We have to manually convert unsigned to signed + // byte representation. + int ipByteAsInt = Integer.parseInt(substrs[i]); + if(ipByteAsInt >= 128) { + ipByteAsInt -= 256; + } + ipBytes[i] = (byte)ipByteAsInt; + } catch (Exception e) { + errString = "550 Invalid PORT format: " + + substrs[i] + "\r\n"; + break mainBlock; + } + } + InetAddress inetAddr; + try { + inetAddr = InetAddress.getByAddress(ipBytes); + } catch (UnknownHostException e) { + errString = "550 Unknown host\r\n"; + break mainBlock; + } + + int port = Integer.parseInt(substrs[4]) * 256 + + Integer.parseInt(substrs[5]); + + sessionThread.onPort(inetAddr, port); + } + if(errString == null) { + sessionThread.writeString("200 PORT OK\r\n"); + myLog.l(Log.DEBUG, "PORT completed"); + } else { + myLog.l(Log.INFO, "PORT error: " + errString); + sessionThread.writeString(errString); + } + } +} diff --git a/src/main/java/org/swiftp/server/CmdPWD.java b/src/main/java/org/swiftp/server/CmdPWD.java index 39e61204d32a410fbfbeba270e9afe896b1c7e74..5ae93711fa9274b9bdc67e60e263133d8754d0a5 100644 --- a/src/main/java/org/swiftp/server/CmdPWD.java +++ b/src/main/java/org/swiftp/server/CmdPWD.java @@ -1,64 +1,64 @@ -/* -Copyright 2009 David Revell - -This file is part of SwiFTP. - -SwiFTP is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -SwiFTP is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with SwiFTP. If not, see . -*/ - -package org.swiftp.server; - -import java.io.IOException; - -import org.swiftp.Globals; - -import android.util.Log; - -public class CmdPWD extends FtpCmd implements Runnable { -// public static final String message = "TEMPLATE!!"; - - public CmdPWD(SessionThread sessionThread, String input) { - super(sessionThread, CmdPWD.class.toString()); - } - - @Override - public void run() { - myLog.l(Log.DEBUG, "PWD executing"); - - // We assume that the chroot restriction has been applied, and that - // therefore the current directory is located somewhere within the - // chroot directory. Therefore, we can just slice of the chroot - // part of the current directory path in order to get the - // user-visible path (inside the chroot directory). - try { - String currentDir = sessionThread.getWorkingDir().getCanonicalPath(); - currentDir = currentDir.substring(Globals.getChrootDir(). - getCanonicalPath().length()); - // The root directory requires special handling to restore its - // leading slash - if(currentDir.length() == 0) { - currentDir = "/"; - } - sessionThread.writeString("257 \"" - + currentDir - + "\"\r\n"); - } catch (IOException e) { - // This shouldn't happen unless our input validation has failed - myLog.l(Log.ERROR, "PWD canonicalize"); - sessionThread.closeSocket(); // should cause thread termination - } - myLog.l(Log.DEBUG, "PWD complete"); - } - -} +/* +Copyright 2009 David Revell + +This file is part of SwiFTP. + +SwiFTP is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +SwiFTP is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with SwiFTP. If not, see . +*/ + +package org.swiftp.server; + +import java.io.IOException; + +import org.swiftp.Globals; + +import android.util.Log; + +public class CmdPWD extends FtpCmd implements Runnable { +// public static final String message = "TEMPLATE!!"; + + public CmdPWD(SessionThread sessionThread, String input) { + super(sessionThread, CmdPWD.class.toString()); + } + + @Override + public void run() { + myLog.l(Log.DEBUG, "PWD executing"); + + // We assume that the chroot restriction has been applied, and that + // therefore the current directory is located somewhere within the + // chroot directory. Therefore, we can just slice of the chroot + // part of the current directory path in order to get the + // user-visible path (inside the chroot directory). + try { + String currentDir = sessionThread.getWorkingDir().getCanonicalPath(); + currentDir = currentDir.substring(Globals.getChrootDir(). + getCanonicalPath().length()); + // The root directory requires special handling to restore its + // leading slash + if(currentDir.length() == 0) { + currentDir = "/"; + } + sessionThread.writeString("257 \"" + + currentDir + + "\"\r\n"); + } catch (IOException e) { + // This shouldn't happen unless our input validation has failed + myLog.l(Log.ERROR, "PWD canonicalize"); + sessionThread.closeSocket(); // should cause thread termination + } + myLog.l(Log.DEBUG, "PWD complete"); + } + +} diff --git a/src/main/java/org/swiftp/server/CmdQUIT.java b/src/main/java/org/swiftp/server/CmdQUIT.java index af9896b588180dc1ab606ce367d2cb95b201b0b5..23412d3f77974c2731f183a9050006009764dfdf 100644 --- a/src/main/java/org/swiftp/server/CmdQUIT.java +++ b/src/main/java/org/swiftp/server/CmdQUIT.java @@ -1,38 +1,38 @@ -/* -Copyright 2009 David Revell - -This file is part of SwiFTP. - -SwiFTP is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -SwiFTP is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with SwiFTP. If not, see . -*/ - -package org.swiftp.server; - -import android.util.Log; - -public class CmdQUIT extends FtpCmd implements Runnable { - public static final String message = "TEMPLATE!!"; - - public CmdQUIT(SessionThread sessionThread, String input) { - super(sessionThread, CmdQUIT.class.toString()); - } - - @Override - public void run() { - myLog.l(Log.DEBUG, "QUITting"); - sessionThread.writeString("221 Goodbye\r\n"); - sessionThread.closeSocket(); - } - -} +/* +Copyright 2009 David Revell + +This file is part of SwiFTP. + +SwiFTP is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +SwiFTP is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with SwiFTP. If not, see . +*/ + +package org.swiftp.server; + +import android.util.Log; + +public class CmdQUIT extends FtpCmd implements Runnable { + public static final String message = "TEMPLATE!!"; + + public CmdQUIT(SessionThread sessionThread, String input) { + super(sessionThread, CmdQUIT.class.toString()); + } + + @Override + public void run() { + myLog.l(Log.DEBUG, "QUITting"); + sessionThread.writeString("221 Goodbye\r\n"); + sessionThread.closeSocket(); + } + +} diff --git a/src/main/java/org/swiftp/server/CmdRETR.java b/src/main/java/org/swiftp/server/CmdRETR.java index 508dc0255b974bdb898d2904e94600464d8bf7d5..31bde621dc6271d64bf2d05c2ab42c78a316400f 100644 --- a/src/main/java/org/swiftp/server/CmdRETR.java +++ b/src/main/java/org/swiftp/server/CmdRETR.java @@ -1,158 +1,158 @@ -/* -Copyright 2009 David Revell - -This file is part of SwiFTP. - -SwiFTP is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -SwiFTP is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with SwiFTP. If not, see . -*/ - -package org.swiftp.server; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; - -import org.swiftp.Defaults; - -import android.util.Log; - -public class CmdRETR extends FtpCmd implements Runnable { - //public static final String message = "TEMPLATE!!"; - protected String input; - - public CmdRETR(SessionThread sessionThread, String input) { - super(sessionThread, CmdRETR.class.toString()); - this.input = input; - } - - @Override - public void run() { - myLog.l(Log.DEBUG, "RETR executing"); - String param = getParameter(input); - File fileToRetr; - String errString = null; - - mainblock: { - fileToRetr = inputPathToChrootedFile(sessionThread.getWorkingDir(), param); - if(violatesChroot(fileToRetr)) { - errString = "550 Invalid name or chroot violation\r\n"; - break mainblock; - } else if(fileToRetr.isDirectory()) { - myLog.l(Log.DEBUG, "Ignoring RETR for directory"); - errString = "550 Can't RETR a directory\r\n"; - break mainblock; - } else if(!fileToRetr.exists()) { - myLog.l(Log.INFO, "Can't RETR nonexistent file: " + - fileToRetr.getAbsolutePath()); - errString = "550 File does not exist\r\n"; - break mainblock; - } else if(!fileToRetr.canRead()) { - myLog.l(Log.INFO, "Failed RETR permission (canRead() is false)"); - errString = "550 No read permissions\r\n"; - break mainblock; - } /*else if(!sessionThread.isBinaryMode()) { - myLog.l(Log.INFO, "Failed RETR in text mode"); - errString = "550 Text mode RETR not supported\r\n"; - break mainblock; - }*/ - FileInputStream in = null; - try { - in = new FileInputStream(fileToRetr); - byte[] buffer = new byte[Defaults.getDataChunkSize()]; - int bytesRead; - if(sessionThread.startUsingDataSocket()) { - myLog.l(Log.DEBUG, "RETR opened data socket"); - } else { - errString = "425 Error opening socket\r\n"; - myLog.l(Log.INFO, "Error in initDataSocket()"); - break mainblock; - } - sessionThread.writeString("150 Sending file\r\n"); - if(sessionThread.isBinaryMode()) { - myLog.l(Log.DEBUG, "Transferring in binary mode"); - while((bytesRead = in.read(buffer)) != -1) { - //myLog.l(Log.DEBUG, - // String.format("CmdRETR sending %d bytes", bytesRead)); - if(sessionThread - .sendViaDataSocket(buffer, bytesRead) == false) - { - errString = "426 Data socket error\r\n"; - myLog.l(Log.INFO, "Data socket error"); - break mainblock; - } - } - } else { // We're in ASCII mode - myLog.l(Log.DEBUG, "Transferring in ASCII mode"); - // We have to convert all solitary \n to \r\n - boolean lastBufEndedWithCR = false; - while((bytesRead = in.read(buffer)) != -1) { - int startPos = 0, endPos = 0; - byte[] crnBuf = {'\r','\n'}; - for(endPos = 0; endPos. +*/ + +package org.swiftp.server; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; + +import org.swiftp.Defaults; + +import android.util.Log; + +public class CmdRETR extends FtpCmd implements Runnable { + //public static final String message = "TEMPLATE!!"; + protected String input; + + public CmdRETR(SessionThread sessionThread, String input) { + super(sessionThread, CmdRETR.class.toString()); + this.input = input; + } + + @Override + public void run() { + myLog.l(Log.DEBUG, "RETR executing"); + String param = getParameter(input); + File fileToRetr; + String errString = null; + + mainblock: { + fileToRetr = inputPathToChrootedFile(sessionThread.getWorkingDir(), param); + if(violatesChroot(fileToRetr)) { + errString = "550 Invalid name or chroot violation\r\n"; + break mainblock; + } else if(fileToRetr.isDirectory()) { + myLog.l(Log.DEBUG, "Ignoring RETR for directory"); + errString = "550 Can't RETR a directory\r\n"; + break mainblock; + } else if(!fileToRetr.exists()) { + myLog.l(Log.INFO, "Can't RETR nonexistent file: " + + fileToRetr.getAbsolutePath()); + errString = "550 File does not exist\r\n"; + break mainblock; + } else if(!fileToRetr.canRead()) { + myLog.l(Log.INFO, "Failed RETR permission (canRead() is false)"); + errString = "550 No read permissions\r\n"; + break mainblock; + } /*else if(!sessionThread.isBinaryMode()) { + myLog.l(Log.INFO, "Failed RETR in text mode"); + errString = "550 Text mode RETR not supported\r\n"; + break mainblock; + }*/ + FileInputStream in = null; + try { + in = new FileInputStream(fileToRetr); + byte[] buffer = new byte[Defaults.getDataChunkSize()]; + int bytesRead; + if(sessionThread.startUsingDataSocket()) { + myLog.l(Log.DEBUG, "RETR opened data socket"); + } else { + errString = "425 Error opening socket\r\n"; + myLog.l(Log.INFO, "Error in initDataSocket()"); + break mainblock; + } + sessionThread.writeString("150 Sending file\r\n"); + if(sessionThread.isBinaryMode()) { + myLog.l(Log.DEBUG, "Transferring in binary mode"); + while((bytesRead = in.read(buffer)) != -1) { + //myLog.l(Log.DEBUG, + // String.format("CmdRETR sending %d bytes", bytesRead)); + if(sessionThread + .sendViaDataSocket(buffer, bytesRead) == false) + { + errString = "426 Data socket error\r\n"; + myLog.l(Log.INFO, "Data socket error"); + break mainblock; + } + } + } else { // We're in ASCII mode + myLog.l(Log.DEBUG, "Transferring in ASCII mode"); + // We have to convert all solitary \n to \r\n + boolean lastBufEndedWithCR = false; + while((bytesRead = in.read(buffer)) != -1) { + int startPos = 0, endPos = 0; + byte[] crnBuf = {'\r','\n'}; + for(endPos = 0; endPos. -*/ - -package org.swiftp.server; - -import java.io.File; - -import android.util.Log; - -public class CmdRMD extends FtpCmd implements Runnable { - public static final String message = "TEMPLATE!!"; - protected String input; - - public CmdRMD(SessionThread sessionThread, String input) { - super(sessionThread, CmdRMD.class.toString()); - this.input = input; - } - - @Override - public void run() { - myLog.l(Log.INFO, "RMD executing"); - String param = getParameter(input); - File toRemove; - String errString = null; - mainblock: { - if(param.length() < 1) { - errString = "550 Invalid argument\r\n"; - break mainblock; - } - toRemove = inputPathToChrootedFile(sessionThread.getWorkingDir(), param); - if(violatesChroot(toRemove)) { - errString = "550 Invalid name or chroot violation\r\n"; - break mainblock; - } - if(!toRemove.isDirectory()) { - errString = "550 Can't RMD a non-directory\r\n"; - break mainblock; - } - if(toRemove.equals(new File("/"))) { - errString = "550 Won't RMD the root directory\r\n"; - break mainblock; - } - if(!recursiveDelete(toRemove)) { - errString = "550 Deletion error, possibly incomplete\r\n"; - break mainblock; - } - } - if(errString != null) { - sessionThread.writeString(errString); - myLog.l(Log.INFO, "RMD failed: " + errString.trim()); - } else { - sessionThread.writeString("250 Removed directory\r\n"); - } - myLog.l(Log.DEBUG, "RMD finished"); - } - - /** - * Accepts a file or directory name, and recursively deletes the contents - * of that directory and all subdirectories. - * @param toDelete - * @return Whether the operation completed successfully - */ - protected boolean recursiveDelete(File toDelete) { - if(!toDelete.exists()) { - return false; - } - if(toDelete.isDirectory()) { - // If any of the recursive operations fail, then we return false - boolean success = true; - for(File entry : toDelete.listFiles()) { - success &= recursiveDelete(entry); - } - myLog.l(Log.DEBUG, "Recursively deleted: " + toDelete); - return success && toDelete.delete(); - } else { - myLog.l(Log.DEBUG, "RMD deleting file: " + toDelete); - return toDelete.delete(); - } - } -} +/* +Copyright 2009 David Revell + +This file is part of SwiFTP. + +SwiFTP is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +SwiFTP is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with SwiFTP. If not, see . +*/ + +package org.swiftp.server; + +import java.io.File; + +import android.support.v4.provider.DocumentFile; +import android.util.Log; + +import org.swiftp.Globals; + +import util.DocumentUtil; +import util.FileUtil; + +public class CmdRMD extends FtpCmd implements Runnable { + public static final String message = "TEMPLATE!!"; + protected String input; + + public CmdRMD(SessionThread sessionThread, String input) { + super(sessionThread, CmdRMD.class.toString()); + this.input = input; + } + + @Override + public void run() { + myLog.l(Log.INFO, "RMD executing"); + String param = getParameter(input); + File toRemove; + String errString = null; + mainblock: { + if(param.length() < 1) { + errString = "550 Invalid argument\r\n"; + break mainblock; + } + toRemove = inputPathToChrootedFile(sessionThread.getWorkingDir(), param); + if(violatesChroot(toRemove)) { + errString = "550 Invalid name or chroot violation\r\n"; + break mainblock; + } + if(!toRemove.isDirectory()) { + errString = "550 Can't RMD a non-directory\r\n"; + break mainblock; + } + if(toRemove.equals(new File("/"))) { + errString = "550 Won't RMD the root directory\r\n"; + break mainblock; + } + if(!FileUtil.delete(toRemove)) { + errString = "550 Deletion error, possibly incomplete\r\n"; + break mainblock; + } + } + if(errString != null) { + sessionThread.writeString(errString); + myLog.l(Log.INFO, "RMD failed: " + errString.trim()); + } else { + sessionThread.writeString("250 Removed directory\r\n"); + } + myLog.l(Log.DEBUG, "RMD finished"); + } + + /* * + * Accepts a file or directory name, and recursively deletes the contents + * of that directory and all subdirectories. + * @param toDelete + * @return Whether the operation completed successfully + * / + protected boolean recursiveDelete(File toDelete) { + if(!toDelete.exists()) { + return false; + } + if(toDelete.isDirectory()) { + // If any of the recursive operations fail, then we return false + boolean success = true; + for(File entry : toDelete.listFiles()) { + success &= recursiveDelete(entry); + } + myLog.l(Log.DEBUG, "Recursively deleted: " + toDelete); + return success && toDelete.delete(); + } else { + myLog.l(Log.DEBUG, "RMD deleting file: " + toDelete); + return toDelete.delete(); + } + }*/ +} diff --git a/src/main/java/org/swiftp/server/CmdRNFR.java b/src/main/java/org/swiftp/server/CmdRNFR.java index e199c5660d7546bc5e73a2b1eb248c55b3e5f789..5a3c31c0c8e87cf8c0781dc6c362c127d0a59efe 100644 --- a/src/main/java/org/swiftp/server/CmdRNFR.java +++ b/src/main/java/org/swiftp/server/CmdRNFR.java @@ -1,58 +1,58 @@ -/* -Copyright 2009 David Revell - -This file is part of SwiFTP. - -SwiFTP is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -SwiFTP is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with SwiFTP. If not, see . -*/ - -package org.swiftp.server; - -import java.io.File; - -import android.util.Log; - -public class CmdRNFR extends FtpCmd implements Runnable { - protected String input; - - public CmdRNFR(SessionThread sessionThread, String input) { - super(sessionThread, CmdRNFR.class.toString()); - this.input = input; - } - - @Override - public void run() { - String param = getParameter(input); - String errString = null; - File file = null; - mainblock: { - file = inputPathToChrootedFile(sessionThread.getWorkingDir(), param); - if(violatesChroot(file)) { - errString = "550 Invalid name or chroot violation\r\n"; - break mainblock; - } - if(!file.exists()) { - errString = "450 Cannot rename nonexistent file\r\n"; - } - } - if(errString != null) { - sessionThread.writeString(errString); - myLog.l(Log.INFO, "RNFR failed: " + errString.trim()); - sessionThread.setRenameFrom(null); - } else { - sessionThread.writeString("350 Filename noted, now send RNTO\r\n"); - sessionThread.setRenameFrom(file); - } - } -} +/* +Copyright 2009 David Revell + +This file is part of SwiFTP. + +SwiFTP is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +SwiFTP is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with SwiFTP. If not, see . +*/ + +package org.swiftp.server; + +import java.io.File; + +import android.util.Log; + +public class CmdRNFR extends FtpCmd implements Runnable { + protected String input; + + public CmdRNFR(SessionThread sessionThread, String input) { + super(sessionThread, CmdRNFR.class.toString()); + this.input = input; + } + + @Override + public void run() { + String param = getParameter(input); + String errString = null; + File file = null; + mainblock: { + file = inputPathToChrootedFile(sessionThread.getWorkingDir(), param); + if(violatesChroot(file)) { + errString = "550 Invalid name or chroot violation\r\n"; + break mainblock; + } + if(!file.exists()) { + errString = "450 Cannot rename nonexistent file\r\n"; + } + } + if(errString != null) { + sessionThread.writeString(errString); + myLog.l(Log.INFO, "RNFR failed: " + errString.trim()); + sessionThread.setRenameFrom(null); + } else { + sessionThread.writeString("350 Filename noted, now send RNTO\r\n"); + sessionThread.setRenameFrom(file); + } + } +} diff --git a/src/main/java/org/swiftp/server/CmdRNTO.java b/src/main/java/org/swiftp/server/CmdRNTO.java index f2ccfe8bd18be2d54de141197f785e9708bc1e83..ceb244e72013623851533b528a229e22cd189a92 100644 --- a/src/main/java/org/swiftp/server/CmdRNTO.java +++ b/src/main/java/org/swiftp/server/CmdRNTO.java @@ -1,67 +1,73 @@ -/* -Copyright 2009 David Revell - -This file is part of SwiFTP. - -SwiFTP is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -SwiFTP is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with SwiFTP. If not, see . -*/ - -package org.swiftp.server; - -import java.io.File; - -import android.util.Log; - -public class CmdRNTO extends FtpCmd implements Runnable { - protected String input; - - public CmdRNTO(SessionThread sessionThread, String input) { - super(sessionThread, CmdRNTO.class.toString()); - this.input = input; - } - - @Override - public void run() { - String param = getParameter(input); - String errString = null; - File toFile = null; - myLog.l(Log.DEBUG, "RNTO executing\r\n"); - mainblock: { - myLog.l(Log.INFO, "param: " + param); - toFile = inputPathToChrootedFile(sessionThread.getWorkingDir(), param); - myLog.l(Log.INFO, "RNTO parsed: " + toFile.getPath()); - if(violatesChroot(toFile)) { - errString = "550 Invalid name or chroot violation\r\n"; - break mainblock; - } - File fromFile = sessionThread.getRenameFrom(); - if(fromFile == null) { - errString = "550 Rename error, maybe RNFR not sent\r\n"; - break mainblock; - } - if(!fromFile.renameTo(toFile)) { - errString = "550 Error during rename operation\r\n"; - break mainblock; - } - } - if(errString != null) { - sessionThread.writeString(errString); - myLog.l(Log.INFO, "RNFR failed: " + errString.trim()); - } else { - sessionThread.writeString("250 rename successful\r\n"); - } - sessionThread.setRenameFrom(null); - myLog.l(Log.DEBUG, "RNTO finished"); - } -} +/* +Copyright 2009 David Revell + +This file is part of SwiFTP. + +SwiFTP is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +SwiFTP is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with SwiFTP. If not, see . +*/ + +package org.swiftp.server; + +import java.io.File; + +import android.support.v4.provider.DocumentFile; +import android.util.Log; + +import org.swiftp.Globals; + +import util.DocumentUtil; +import util.FileUtil; + +public class CmdRNTO extends FtpCmd implements Runnable { + protected String input; + + public CmdRNTO(SessionThread sessionThread, String input) { + super(sessionThread, CmdRNTO.class.toString()); + this.input = input; + } + + @Override + public void run() { + String param = getParameter(input); + String errString = null; + File toFile = null; + myLog.l(Log.DEBUG, "RNTO executing\r\n"); + mainblock: { + myLog.l(Log.INFO, "param: " + param); + toFile = inputPathToChrootedFile(sessionThread.getWorkingDir(), param); + myLog.l(Log.INFO, "RNTO parsed: " + toFile.getPath()); + if(violatesChroot(toFile)) { + errString = "550 Invalid name or chroot violation\r\n"; + break mainblock; + } + File fromFile = sessionThread.getRenameFrom(); + if(fromFile == null) { + errString = "550 Rename error, maybe RNFR not sent\r\n"; + break mainblock; + } + if(!FileUtil.rename(fromFile,toFile)) { + errString = "550 Error during rename operation\r\n"; + break mainblock; + } + } + if(errString != null) { + sessionThread.writeString(errString); + myLog.l(Log.INFO, "RNFR failed: " + errString.trim()); + } else { + sessionThread.writeString("250 rename successful\r\n"); + } + sessionThread.setRenameFrom(null); + myLog.l(Log.DEBUG, "RNTO finished"); + } +} diff --git a/src/main/java/org/swiftp/server/CmdSIZE.java b/src/main/java/org/swiftp/server/CmdSIZE.java index 25507a59317a75d145c6515bd4964f1598278a74..933beb54f7df713d39329c554a8426a9fcfdb760 100644 --- a/src/main/java/org/swiftp/server/CmdSIZE.java +++ b/src/main/java/org/swiftp/server/CmdSIZE.java @@ -1,56 +1,56 @@ -package org.swiftp.server; - -import java.io.File; -import java.io.IOException; - -public class CmdSIZE extends FtpCmd { - protected String input; - - public CmdSIZE(SessionThread sessionThread, String input) { - super(sessionThread, CmdSIZE.class.toString()); - this.input = input; - } - - @Override - public void run() { - myLog.d("SIZE executing"); - - String errString = null; - String param = getParameter(input); - long size = 0; - mainblock: { - File currentDir = sessionThread.getWorkingDir(); - if(param.contains(File.separator)) { - errString = "550 No directory traversal allowed in SIZE param\r\n"; - break mainblock; - } - File target = new File(currentDir, param); - - // We should have caught any invalid location access before now, but - // here we check again, just to be explicitly sure. - if(violatesChroot(target)) { - errString = "550 SIZE target violates chroot\r\n"; - break mainblock; - } - if(!target.exists()) { - errString = "550 Cannot get the SIZE of nonexistent object\r\n"; - try { - myLog.i("Failed getting size of: " + target.getCanonicalPath()); - } catch (IOException e) {} - break mainblock; - } - if(!target.isFile()) { - errString = "550 Cannot get the size of a non-file\r\n"; - break mainblock; - } - size = target.length(); - } - if(errString != null) { - sessionThread.writeString(errString); - } else { - sessionThread.writeString("213 " + size + "\r\n"); - } - myLog.d("SIZE complete"); - } - -} +package org.swiftp.server; + +import java.io.File; +import java.io.IOException; + +public class CmdSIZE extends FtpCmd { + protected String input; + + public CmdSIZE(SessionThread sessionThread, String input) { + super(sessionThread, CmdSIZE.class.toString()); + this.input = input; + } + + @Override + public void run() { + myLog.d("SIZE executing"); + + String errString = null; + String param = getParameter(input); + long size = 0; + mainblock: { + File currentDir = sessionThread.getWorkingDir(); + if(param.contains(File.separator)) { + errString = "550 No directory traversal allowed in SIZE param\r\n"; + break mainblock; + } + File target = new File(currentDir, param); + + // We should have caught any invalid location access before now, but + // here we check again, just to be explicitly sure. + if(violatesChroot(target)) { + errString = "550 SIZE target violates chroot\r\n"; + break mainblock; + } + if(!target.exists()) { + errString = "550 Cannot get the SIZE of nonexistent object\r\n"; + try { + myLog.i("Failed getting size of: " + target.getCanonicalPath()); + } catch (IOException e) {} + break mainblock; + } + if(!target.isFile()) { + errString = "550 Cannot get the size of a non-file\r\n"; + break mainblock; + } + size = target.length(); + } + if(errString != null) { + sessionThread.writeString(errString); + } else { + sessionThread.writeString("213 " + size + "\r\n"); + } + myLog.d("SIZE complete"); + } + +} diff --git a/src/main/java/org/swiftp/server/CmdSTOR.java b/src/main/java/org/swiftp/server/CmdSTOR.java index ce3f11edcd6370667ed42c8de60eae14135e00e8..f5015883c9be368ea0f784566ebfdb042e187332 100644 --- a/src/main/java/org/swiftp/server/CmdSTOR.java +++ b/src/main/java/org/swiftp/server/CmdSTOR.java @@ -1,35 +1,35 @@ -/* -Copyright 2009 David Revell - -This file is part of SwiFTP. - -SwiFTP is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -SwiFTP is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with SwiFTP. If not, see . -*/ - -package org.swiftp.server; - - -public class CmdSTOR extends CmdAbstractStore implements Runnable { - protected String input; - - public CmdSTOR(SessionThread sessionThread, String input) { - super(sessionThread, CmdSTOR.class.toString()); - this.input = input; - } - - @Override - public void run() { - doStorOrAppe(getParameter(input), false); - } -} +/* +Copyright 2009 David Revell + +This file is part of SwiFTP. + +SwiFTP is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +SwiFTP is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with SwiFTP. If not, see . +*/ + +package org.swiftp.server; + + +public class CmdSTOR extends CmdAbstractStore implements Runnable { + protected String input; + + public CmdSTOR(SessionThread sessionThread, String input) { + super(sessionThread, CmdSTOR.class.toString()); + this.input = input; + } + + @Override + public void run() { + doStorOrAppe(getParameter(input), false); + } +} diff --git a/src/main/java/org/swiftp/server/CmdSYST.java b/src/main/java/org/swiftp/server/CmdSYST.java index 4103edf1a05c33e03a049ff35bc57cfb66e57a3f..39b1fc91f689acefc133b16ac97e084451371edf 100644 --- a/src/main/java/org/swiftp/server/CmdSYST.java +++ b/src/main/java/org/swiftp/server/CmdSYST.java @@ -1,40 +1,40 @@ -/* -Copyright 2009 David Revell - -This file is part of SwiFTP. - -SwiFTP is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -SwiFTP is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with SwiFTP. If not, see . -*/ - -package org.swiftp.server; - -import android.util.Log; - -public class CmdSYST extends FtpCmd implements Runnable { - // This is considered a safe response to the SYST command, see - // http://cr.yp.to/ftp/syst.html - public static final String response = "215 UNIX Type: L8\r\n"; - - public CmdSYST(SessionThread sessionThread, String input) { - super(sessionThread, CmdSYST.class.toString()); - } - - - @Override - public void run() { - myLog.l(Log.DEBUG, "SYST executing"); - sessionThread.writeString(response); - myLog.l(Log.DEBUG, "SYST finished"); - } -} +/* +Copyright 2009 David Revell + +This file is part of SwiFTP. + +SwiFTP is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +SwiFTP is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with SwiFTP. If not, see . +*/ + +package org.swiftp.server; + +import android.util.Log; + +public class CmdSYST extends FtpCmd implements Runnable { + // This is considered a safe response to the SYST command, see + // http://cr.yp.to/ftp/syst.html + public static final String response = "215 UNIX Type: L8\r\n"; + + public CmdSYST(SessionThread sessionThread, String input) { + super(sessionThread, CmdSYST.class.toString()); + } + + + @Override + public void run() { + myLog.l(Log.DEBUG, "SYST executing"); + sessionThread.writeString(response); + myLog.l(Log.DEBUG, "SYST finished"); + } +} diff --git a/src/main/java/org/swiftp/server/CmdTYPE.java b/src/main/java/org/swiftp/server/CmdTYPE.java index d2d3fb7a4c4eb596d492a87e78fb32eda818b8dd..e60c66cca79c180fd9aadf78dc9252349eb0540c 100644 --- a/src/main/java/org/swiftp/server/CmdTYPE.java +++ b/src/main/java/org/swiftp/server/CmdTYPE.java @@ -1,50 +1,50 @@ -/* -Copyright 2009 David Revell - -This file is part of SwiFTP. - -SwiFTP is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -SwiFTP is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with SwiFTP. If not, see . -*/ - -package org.swiftp.server; - -import android.util.Log; - -public class CmdTYPE extends FtpCmd implements Runnable { - String input; - - public CmdTYPE(SessionThread sessionThread, String input) { - super(sessionThread, CmdTYPE.class.toString()); - this.input = input; - } - - @Override - public void run() { - String output; - myLog.l(Log.DEBUG, "TYPE executing"); - String param = getParameter(input); - if(param.equals("I") || param.equals("L 8")) { - output = "200 Binary type set\r\n"; - sessionThread.setBinaryMode(true); - } else if (param.equals("A") || param.equals("A N")) { - output = "200 ASCII type set\r\n"; - sessionThread.setBinaryMode(false); - } else { - output = "503 Malformed TYPE command\r\n"; - } - sessionThread.writeString(output); - myLog.l(Log.DEBUG, "TYPE complete"); - } - -} +/* +Copyright 2009 David Revell + +This file is part of SwiFTP. + +SwiFTP is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +SwiFTP is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with SwiFTP. If not, see . +*/ + +package org.swiftp.server; + +import android.util.Log; + +public class CmdTYPE extends FtpCmd implements Runnable { + String input; + + public CmdTYPE(SessionThread sessionThread, String input) { + super(sessionThread, CmdTYPE.class.toString()); + this.input = input; + } + + @Override + public void run() { + String output; + myLog.l(Log.DEBUG, "TYPE executing"); + String param = getParameter(input); + if(param.equals("I") || param.equals("L 8")) { + output = "200 Binary type set\r\n"; + sessionThread.setBinaryMode(true); + } else if (param.equals("A") || param.equals("A N")) { + output = "200 ASCII type set\r\n"; + sessionThread.setBinaryMode(false); + } else { + output = "503 Malformed TYPE command\r\n"; + } + sessionThread.writeString(output); + myLog.l(Log.DEBUG, "TYPE complete"); + } + +} diff --git a/src/main/java/org/swiftp/server/CmdTemplate.java b/src/main/java/org/swiftp/server/CmdTemplate.java index e56ac27bf7d431b058b957ab69b6e45e8f4d74d3..4f9a4b96b25857f75256c1887c8a97cf63c50e96 100644 --- a/src/main/java/org/swiftp/server/CmdTemplate.java +++ b/src/main/java/org/swiftp/server/CmdTemplate.java @@ -1,37 +1,37 @@ -/* -Copyright 2009 David Revell - -This file is part of SwiFTP. - -SwiFTP is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -SwiFTP is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with SwiFTP. If not, see . -*/ - -package org.swiftp.server; - -import android.util.Log; - -public class CmdTemplate extends FtpCmd implements Runnable { - public static final String message = "TEMPLATE!!"; - - public CmdTemplate(SessionThread sessionThread, String input) { - super(sessionThread, CmdTemplate.class.toString()); - } - - @Override - public void run() { - sessionThread.writeString(message); - myLog.l(Log.INFO, "Template log message"); - } - -} +/* +Copyright 2009 David Revell + +This file is part of SwiFTP. + +SwiFTP is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +SwiFTP is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with SwiFTP. If not, see . +*/ + +package org.swiftp.server; + +import android.util.Log; + +public class CmdTemplate extends FtpCmd implements Runnable { + public static final String message = "TEMPLATE!!"; + + public CmdTemplate(SessionThread sessionThread, String input) { + super(sessionThread, CmdTemplate.class.toString()); + } + + @Override + public void run() { + sessionThread.writeString(message); + myLog.l(Log.INFO, "Template log message"); + } + +} diff --git a/src/main/java/org/swiftp/server/CmdUSER.java b/src/main/java/org/swiftp/server/CmdUSER.java index 88cf60c32593e2543501407a024ad04ba178a359..338fc92479d812eacebd4a0045dfe00d28433e79 100644 --- a/src/main/java/org/swiftp/server/CmdUSER.java +++ b/src/main/java/org/swiftp/server/CmdUSER.java @@ -1,45 +1,45 @@ -/* -Copyright 2009 David Revell - -This file is part of SwiFTP. - -SwiFTP is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -SwiFTP is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with SwiFTP. If not, see . -*/ - -package org.swiftp.server; - -import android.util.Log; - -public class CmdUSER extends FtpCmd implements Runnable { - protected String input; - - public CmdUSER(SessionThread sessionThread, String input) { - super(sessionThread, CmdUSER.class.toString()); - this.input = input; - - } - - @Override - public void run() { - myLog.l(Log.DEBUG, "USER executing"); - String username = FtpCmd.getParameter(input); - if(!username.matches("[A-Za-z0-9]+")) { - sessionThread.writeString("530 Invalid username\r\n"); - return; - } - sessionThread.writeString("331 Send password\r\n"); - sessionThread.account.setUsername(username); - } - -} +/* +Copyright 2009 David Revell + +This file is part of SwiFTP. + +SwiFTP is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +SwiFTP is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with SwiFTP. If not, see . +*/ + +package org.swiftp.server; + +import android.util.Log; + +public class CmdUSER extends FtpCmd implements Runnable { + protected String input; + + public CmdUSER(SessionThread sessionThread, String input) { + super(sessionThread, CmdUSER.class.toString()); + this.input = input; + + } + + @Override + public void run() { + myLog.l(Log.DEBUG, "USER executing"); + String username = FtpCmd.getParameter(input); + if(!username.matches("[A-Za-z0-9]+")) { + sessionThread.writeString("530 Invalid username\r\n"); + return; + } + sessionThread.writeString("331 Send password\r\n"); + sessionThread.account.setUsername(username); + } + +} diff --git a/src/main/java/org/swiftp/server/DataSocketFactory.java b/src/main/java/org/swiftp/server/DataSocketFactory.java index 23d27d07dccc633f0243a6577daa8694b0df04db..1a25c824741b63097b9536d3d2e808f6568b66ed 100644 --- a/src/main/java/org/swiftp/server/DataSocketFactory.java +++ b/src/main/java/org/swiftp/server/DataSocketFactory.java @@ -1,60 +1,60 @@ -package org.swiftp.server; - -import java.net.InetAddress; -import java.net.Socket; - -import org.swiftp.MyLog; - - -abstract public class DataSocketFactory { - - /** - * A DataSocketFactory hides the implementation of the opening and closing - * of the data sockets which are used to transmit directory listings and - * file contents. This is necessary because normal FTP data sockets are - * opened and closed very differently from the abnormal sort of data sockets - * we use in conjunction with our proxy system. - */ - protected MyLog myLog = new MyLog(getClass().getName()); - - /** - * When SwiFTP receives a PORT command, this will be called. Subclasses should - * perform whatever initialization is necessary. - * @return Whether the necessary actions completed successfully - */ - abstract public boolean onPort(InetAddress dest, int port); - - /** - * When SwiFTP receives a PASV command, this will be called. Subclasses should - * perform whatever initialization is necessary. - * @return Whether the necessary actions completed successfully - */ - abstract public int onPasv(); - - /** - * When it's time for data transfer to begin, the SessionThread will call this - * method to perform any necessary actions to prepare the Socket for use and - * return it in a state that's ready for reading or writing. - * @return The opened Socket - */ - abstract public Socket onTransfer(); - - /** - * Sometimes we'll need to know the IP address at which we can be contacted. For - * instance, the response to a PASV command will be the IP and port that the - * client should use to connect it's data socket. - */ - abstract public InetAddress getPasvIp(); - - /** - * We sometimes want to track the total number of bytes that go over the - * command and data sockets. The SessionThread can call this function to - * reports its usage, and different DataSocketFactory subclasses can - * handle the data however is appropriate. For the ProxyDataSocketFactory, - * we want to present the total to the user in the UI to guilt them into - * donating. - * @param numBytes the number of bytes to add to the total - */ - abstract public void reportTraffic(long numBytes); -} - +package org.swiftp.server; + +import java.net.InetAddress; +import java.net.Socket; + +import org.swiftp.MyLog; + + +abstract public class DataSocketFactory { + + /** + * A DataSocketFactory hides the implementation of the opening and closing + * of the data sockets which are used to transmit directory listings and + * file contents. This is necessary because normal FTP data sockets are + * opened and closed very differently from the abnormal sort of data sockets + * we use in conjunction with our proxy system. + */ + protected MyLog myLog = new MyLog(getClass().getName()); + + /** + * When SwiFTP receives a PORT command, this will be called. Subclasses should + * perform whatever initialization is necessary. + * @return Whether the necessary actions completed successfully + */ + abstract public boolean onPort(InetAddress dest, int port); + + /** + * When SwiFTP receives a PASV command, this will be called. Subclasses should + * perform whatever initialization is necessary. + * @return Whether the necessary actions completed successfully + */ + abstract public int onPasv(); + + /** + * When it's time for data transfer to begin, the SessionThread will call this + * method to perform any necessary actions to prepare the Socket for use and + * return it in a state that's ready for reading or writing. + * @return The opened Socket + */ + abstract public Socket onTransfer(); + + /** + * Sometimes we'll need to know the IP address at which we can be contacted. For + * instance, the response to a PASV command will be the IP and port that the + * client should use to connect it's data socket. + */ + abstract public InetAddress getPasvIp(); + + /** + * We sometimes want to track the total number of bytes that go over the + * command and data sockets. The SessionThread can call this function to + * reports its usage, and different DataSocketFactory subclasses can + * handle the data however is appropriate. For the ProxyDataSocketFactory, + * we want to present the total to the user in the UI to guilt them into + * donating. + * @param numBytes the number of bytes to add to the total + */ + abstract public void reportTraffic(long numBytes); +} + diff --git a/src/main/java/org/swiftp/server/FtpCmd.java b/src/main/java/org/swiftp/server/FtpCmd.java index f56f576b2f21acadfb59eb3871f0ef094742992f..615e35fbdec05b006fe59b9a6a7bfd5ab83bc49f 100644 --- a/src/main/java/org/swiftp/server/FtpCmd.java +++ b/src/main/java/org/swiftp/server/FtpCmd.java @@ -1,207 +1,207 @@ -/* -Copyright 2009 David Revell - -This file is part of SwiFTP. - -SwiFTP is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -SwiFTP is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with SwiFTP. If not, see . -*/ - -package org.swiftp.server; - - -import java.io.File; -import java.lang.reflect.Constructor; - -import org.swiftp.Globals; -import org.swiftp.MyLog; - -import android.util.Log; - -public abstract class FtpCmd implements Runnable { - protected SessionThread sessionThread; - protected MyLog myLog; - protected static MyLog staticLog = new MyLog(FtpCmd.class.toString()); - - protected static CmdMap[] cmdClasses = { - new CmdMap("SYST", CmdSYST.class), - new CmdMap("USER", CmdUSER.class), - new CmdMap("PASS", CmdPASS.class), - new CmdMap("TYPE", CmdTYPE.class), - new CmdMap("CWD", CmdCWD.class), - new CmdMap("PWD", CmdPWD.class), - new CmdMap("LIST", CmdLIST.class), - new CmdMap("PASV", CmdPASV.class), - new CmdMap("RETR", CmdRETR.class), - new CmdMap("NLST", CmdNLST.class), - new CmdMap("NOOP", CmdNOOP.class), - new CmdMap("STOR", CmdSTOR.class), - new CmdMap("DELE", CmdDELE.class), - new CmdMap("RNFR", CmdRNFR.class), - new CmdMap("RNTO", CmdRNTO.class), - new CmdMap("RMD", CmdRMD.class), - new CmdMap("MKD", CmdMKD.class), - new CmdMap("OPTS", CmdOPTS.class), - new CmdMap("PORT", CmdPORT.class), - new CmdMap("QUIT", CmdQUIT.class), - new CmdMap("FEAT", CmdFEAT.class), - new CmdMap("SIZE", CmdSIZE.class), - new CmdMap("CDUP", CmdCDUP.class), - new CmdMap("APPE", CmdAPPE.class), - new CmdMap("XCUP", CmdCDUP.class), // synonym - new CmdMap("XPWD", CmdPWD.class), // synonym - new CmdMap("XMKD", CmdMKD.class), // synonym - new CmdMap("XRMD", CmdRMD.class) // synonym - }; - - public FtpCmd(SessionThread sessionThread, String logName) { - this.sessionThread = sessionThread; - myLog = new MyLog(logName); - } - - abstract public void run(); - - protected static void dispatchCommand(SessionThread session, - String inputString) { - String[] strings = inputString.split(" "); - String unrecognizedCmdMsg = "502 Command not recognized\r\n"; - if(strings == null) { - // There was some egregious sort of parsing error - String errString = "502 Command parse error\r\n"; - staticLog.l(Log.INFO, errString); - session.writeString(errString); - return; - } - if(strings.length < 1) { - staticLog.l(Log.INFO, "No strings parsed"); - session.writeString(unrecognizedCmdMsg); - return; - } - String verb = strings[0]; - if(verb.length() < 1) { - staticLog.l(Log.INFO, "Invalid command verb"); - session.writeString(unrecognizedCmdMsg); - return; - } - FtpCmd cmdInstance = null; - verb = verb.trim(); - verb = verb.toUpperCase(); - for(int i=0; i constructor; - try { - constructor = cmdClasses[i].getCommand().getConstructor( - new Class[] {SessionThread.class, String.class}); - } catch (NoSuchMethodException e) { - staticLog.l(Log.ERROR, "FtpCmd subclass lacks expected " + - "constructor "); - return; - } - try { - cmdInstance = constructor.newInstance( - new Object[] {session, inputString}); - } catch(Exception e) { - staticLog.l(Log.ERROR, - "Instance creation error on FtpCmd"); - return; - } - } - } - if(cmdInstance == null) { - // If we couldn't find a matching command, - staticLog.l(Log.DEBUG, "Ignoring unrecognized FTP verb: " + verb); - session.writeString(unrecognizedCmdMsg); - return; - } else if(session.isAuthenticated() - || cmdInstance.getClass().equals(CmdUSER.class) - || cmdInstance.getClass().equals(CmdPASS.class) - || cmdInstance.getClass().equals(CmdUSER.class)) - { - // Unauthenticated users can run only USER, PASS and QUIT - cmdInstance.run(); - } else { - session.writeString("530 Login first with USER and PASS\r\n"); - } - } - - /** - * An FTP parameter is that part of the input string that occurs - * after the first space, including any subsequent spaces. Also, - * we want to chop off the trailing '\r\n', if present. - * - * Some parameters shouldn't be logged or output (e.g. passwords), - * so the caller can use silent==true in that case. - */ - static public String getParameter(String input, boolean silent) { - if(input == null) { - return ""; - } - int firstSpacePosition = input.indexOf(' '); - if(firstSpacePosition == -1) { - return ""; - } - String retString = input.substring(firstSpacePosition+1); - - // Remove trailing whitespace - // todo: trailing whitespace may be significant, just remove \r\n - retString = retString.replaceAll("\\s+$", ""); - - if(!silent) { - staticLog.l(Log.DEBUG, "Parsed argument: " + retString); - } - return retString; - } - - /** - * A wrapper around getParameter, for when we don't want it to be silent. - */ - static public String getParameter(String input) { - return getParameter(input, false); - } - - public static File inputPathToChrootedFile(File existingPrefix, String param) { - try { - if(param.charAt(0) == '/') { - // The STOR contained an absolute path - File chroot = Globals.getChrootDir(); - return new File(chroot, param); - } - } catch (Exception e) {} - - // The STOR contained a relative path - return new File(existingPrefix, param); - } - - public boolean violatesChroot(File file) { - File chroot = Globals.getChrootDir(); - try { - String canonicalPath = file.getCanonicalPath(); - if(!canonicalPath.startsWith(chroot.toString())) { - myLog.l(Log.INFO, "Path violated folder restriction, denying"); - myLog.l(Log.DEBUG, "path: " + canonicalPath); - myLog.l(Log.DEBUG, "chroot: " + chroot.toString()); - return true; // the path must begin with the chroot path - } - return false; - } catch(Exception e) { - myLog.l(Log.INFO, "Path canonicalization problem: " + e.toString()); - myLog.l(Log.INFO, "When checking file: " + file.getAbsolutePath()); - return true; // for security, assume violation - } - } -} +/* +Copyright 2009 David Revell + +This file is part of SwiFTP. + +SwiFTP is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +SwiFTP is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with SwiFTP. If not, see . +*/ + +package org.swiftp.server; + + +import java.io.File; +import java.lang.reflect.Constructor; + +import org.swiftp.Globals; +import org.swiftp.MyLog; + +import android.util.Log; + +public abstract class FtpCmd implements Runnable { + protected SessionThread sessionThread; + protected MyLog myLog; + protected static MyLog staticLog = new MyLog(FtpCmd.class.toString()); + + protected static CmdMap[] cmdClasses = { + new CmdMap("SYST", CmdSYST.class), + new CmdMap("USER", CmdUSER.class), + new CmdMap("PASS", CmdPASS.class), + new CmdMap("TYPE", CmdTYPE.class), + new CmdMap("CWD", CmdCWD.class), + new CmdMap("PWD", CmdPWD.class), + new CmdMap("LIST", CmdLIST.class), + new CmdMap("PASV", CmdPASV.class), + new CmdMap("RETR", CmdRETR.class), + new CmdMap("NLST", CmdNLST.class), + new CmdMap("NOOP", CmdNOOP.class), + new CmdMap("STOR", CmdSTOR.class), + new CmdMap("DELE", CmdDELE.class), + new CmdMap("RNFR", CmdRNFR.class), + new CmdMap("RNTO", CmdRNTO.class), + new CmdMap("RMD", CmdRMD.class), + new CmdMap("MKD", CmdMKD.class), + new CmdMap("OPTS", CmdOPTS.class), + new CmdMap("PORT", CmdPORT.class), + new CmdMap("QUIT", CmdQUIT.class), + new CmdMap("FEAT", CmdFEAT.class), + new CmdMap("SIZE", CmdSIZE.class), + new CmdMap("CDUP", CmdCDUP.class), + new CmdMap("APPE", CmdAPPE.class), + new CmdMap("XCUP", CmdCDUP.class), // synonym + new CmdMap("XPWD", CmdPWD.class), // synonym + new CmdMap("XMKD", CmdMKD.class), // synonym + new CmdMap("XRMD", CmdRMD.class) // synonym + }; + + public FtpCmd(SessionThread sessionThread, String logName) { + this.sessionThread = sessionThread; + myLog = new MyLog(logName); + } + + abstract public void run(); + + protected static void dispatchCommand(SessionThread session, + String inputString) { + String[] strings = inputString.split(" "); + String unrecognizedCmdMsg = "502 Command not recognized\r\n"; + if(strings == null) { + // There was some egregious sort of parsing error + String errString = "502 Command parse error\r\n"; + staticLog.l(Log.INFO, errString); + session.writeString(errString); + return; + } + if(strings.length < 1) { + staticLog.l(Log.INFO, "No strings parsed"); + session.writeString(unrecognizedCmdMsg); + return; + } + String verb = strings[0]; + if(verb.length() < 1) { + staticLog.l(Log.INFO, "Invalid command verb"); + session.writeString(unrecognizedCmdMsg); + return; + } + FtpCmd cmdInstance = null; + verb = verb.trim(); + verb = verb.toUpperCase(); + for(int i=0; i constructor; + try { + constructor = cmdClasses[i].getCommand().getConstructor( + new Class[] {SessionThread.class, String.class}); + } catch (NoSuchMethodException e) { + staticLog.l(Log.ERROR, "FtpCmd subclass lacks expected " + + "constructor "); + return; + } + try { + cmdInstance = constructor.newInstance( + new Object[] {session, inputString}); + } catch(Exception e) { + staticLog.l(Log.ERROR, + "Instance creation error on FtpCmd"); + return; + } + } + } + if(cmdInstance == null) { + // If we couldn't find a matching command, + staticLog.l(Log.DEBUG, "Ignoring unrecognized FTP verb: " + verb); + session.writeString(unrecognizedCmdMsg); + return; + } else if(session.isAuthenticated() + || cmdInstance.getClass().equals(CmdUSER.class) + || cmdInstance.getClass().equals(CmdPASS.class) + || cmdInstance.getClass().equals(CmdUSER.class)) + { + // Unauthenticated users can run only USER, PASS and QUIT + cmdInstance.run(); + } else { + session.writeString("530 Login first with USER and PASS\r\n"); + } + } + + /** + * An FTP parameter is that part of the input string that occurs + * after the first space, including any subsequent spaces. Also, + * we want to chop off the trailing '\r\n', if present. + * + * Some parameters shouldn't be logged or output (e.g. passwords), + * so the caller can use silent==true in that case. + */ + static public String getParameter(String input, boolean silent) { + if(input == null) { + return ""; + } + int firstSpacePosition = input.indexOf(' '); + if(firstSpacePosition == -1) { + return ""; + } + String retString = input.substring(firstSpacePosition+1); + + // Remove trailing whitespace + // todo: trailing whitespace may be significant, just remove \r\n + retString = retString.replaceAll("\\s+$", ""); + + if(!silent) { + staticLog.l(Log.DEBUG, "Parsed argument: " + retString); + } + return retString; + } + + /** + * A wrapper around getParameter, for when we don't want it to be silent. + */ + static public String getParameter(String input) { + return getParameter(input, false); + } + + public static File inputPathToChrootedFile(File existingPrefix, String param) { + try { + if(param.charAt(0) == '/') { + // The STOR contained an absolute path + File chroot = Globals.getChrootDir(); + return new File(chroot, param); + } + } catch (Exception e) {} + + // The STOR contained a relative path + return new File(existingPrefix, param); + } + + public boolean violatesChroot(File file) { + File chroot = Globals.getChrootDir(); + try { + String canonicalPath = file.getCanonicalPath(); + if(!canonicalPath.startsWith(chroot.toString())) { + myLog.l(Log.INFO, "Path violated folder restriction, denying"); + myLog.l(Log.DEBUG, "path: " + canonicalPath); + myLog.l(Log.DEBUG, "chroot: " + chroot.toString()); + return true; // the path must begin with the chroot path + } + return false; + } catch(Exception e) { + myLog.l(Log.INFO, "Path canonicalization problem: " + e.toString()); + myLog.l(Log.INFO, "When checking file: " + file.getAbsolutePath()); + return true; // for security, assume violation + } + } +} diff --git a/src/main/java/org/swiftp/server/NormalDataSocketFactory.java b/src/main/java/org/swiftp/server/NormalDataSocketFactory.java index e6a2735b10e6e83544d92936c5a31b9f50881f74..a967a660260fbc2ef26fc02d9b7368a749514482 100644 --- a/src/main/java/org/swiftp/server/NormalDataSocketFactory.java +++ b/src/main/java/org/swiftp/server/NormalDataSocketFactory.java @@ -1,161 +1,161 @@ -/* -Copyright 2009 David Revell - -This file is part of SwiFTP. - -SwiFTP is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -SwiFTP is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with SwiFTP. If not, see . -*/ - -package org.swiftp.server; - -import java.io.IOException; -import java.net.InetAddress; -import java.net.ServerSocket; -import java.net.Socket; - -import org.swiftp.Defaults; -import org.swiftp.FTPServerService; - -import android.util.Log; - -public class NormalDataSocketFactory extends DataSocketFactory { - /** - * This class implements normal, traditional opening and closing of data sockets - * used for transmitting directory listings and file contents. PORT and PASV - * work according to the FTP specs. This is in contrast to a - * ProxyDataSocketFactory, which performs contortions to allow data sockets - * to be proxied through a server out in the cloud. - * - */ - - // Listener socket used for PASV mode - ServerSocket server = null; - // Remote IP & port information used for PORT mode - InetAddress remoteAddr; - int remotePort; - boolean isPasvMode = true; - - public NormalDataSocketFactory() { - clearState(); - } - - - private void clearState() { - /** - * Clears the state of this object, as if no pasv() or port() had occurred. - * All sockets are closed. - */ - if(server != null) { - try { - server.close(); - } catch (IOException e) {} - } - server = null; - remoteAddr = null; - remotePort = 0; - myLog.l(Log.DEBUG, "NormalDataSocketFactory state cleared"); - } - - @Override - public int onPasv() { - clearState(); - try { - // Listen on any port (port parameter 0) - server = new ServerSocket(0, Defaults.tcpConnectionBacklog); - myLog.l(Log.DEBUG, "Data socket pasv() listen successful"); - return server.getLocalPort(); - } catch(IOException e) { - myLog.l(Log.ERROR, "Data socket creation error"); - clearState(); - return 0; - } - } - - @Override - public boolean onPort(InetAddress remoteAddr, int remotePort) { - clearState(); - this.remoteAddr = remoteAddr; - this.remotePort = remotePort; - return true; - } - - @Override - public Socket onTransfer() { - if(server == null) { - // We're in PORT mode (not PASV) - if(remoteAddr == null || remotePort == 0) { - myLog.l(Log.INFO, "PORT mode but not initialized correctly"); - clearState(); - return null; - } - Socket socket; - try { - socket = new Socket(remoteAddr, remotePort); - } catch (IOException e) { - myLog.l(Log.INFO, - "Couldn't open PORT data socket to: " + - remoteAddr.toString() + ":" + remotePort); - clearState(); - return null; - } - - // Kill the socket if nothing happens for X milliseconds - try { - socket.setSoTimeout(Defaults.SO_TIMEOUT_MS); - } catch (Exception e) { - myLog.l(Log.ERROR, "Couldn't set SO_TIMEOUT"); - clearState(); - return null; - } - - return socket; - } else { - // We're in PASV mode (not PORT) - Socket socket = null; - try { - socket = server.accept(); - myLog.l(Log.DEBUG, "onTransfer pasv accept successful"); - } catch (Exception e) { - myLog.l(Log.INFO, "Exception accepting PASV socket"); - socket = null; - } - clearState(); - return socket; // will be null if error occurred - } - } - - /** - * Return the port number that the remote client should be informed of (in the body - * of the PASV response). - * @return The port number, or -1 if error. - */ - public int getPortNumber() { - if(server != null) { - return server.getLocalPort(); // returns -1 if serversocket is unbound - } else { - return -1; - } - } - - @Override - public InetAddress getPasvIp() { - //String retVal = server.getInetAddress().getHostAddress(); - return FTPServerService.getWifiIp(); - } - - @Override - public void reportTraffic(long bytes) { - // ignore, we don't care about how much traffic goes over wifi. - } -} +/* +Copyright 2009 David Revell + +This file is part of SwiFTP. + +SwiFTP is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +SwiFTP is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with SwiFTP. If not, see . +*/ + +package org.swiftp.server; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.Socket; + +import org.swiftp.Defaults; +import org.swiftp.FTPServerService; + +import android.util.Log; + +public class NormalDataSocketFactory extends DataSocketFactory { + /** + * This class implements normal, traditional opening and closing of data sockets + * used for transmitting directory listings and file contents. PORT and PASV + * work according to the FTP specs. This is in contrast to a + * ProxyDataSocketFactory, which performs contortions to allow data sockets + * to be proxied through a server out in the cloud. + * + */ + + // Listener socket used for PASV mode + ServerSocket server = null; + // Remote IP & port information used for PORT mode + InetAddress remoteAddr; + int remotePort; + boolean isPasvMode = true; + + public NormalDataSocketFactory() { + clearState(); + } + + + private void clearState() { + /** + * Clears the state of this object, as if no pasv() or port() had occurred. + * All sockets are closed. + */ + if(server != null) { + try { + server.close(); + } catch (IOException e) {} + } + server = null; + remoteAddr = null; + remotePort = 0; + myLog.l(Log.DEBUG, "NormalDataSocketFactory state cleared"); + } + + @Override + public int onPasv() { + clearState(); + try { + // Listen on any port (port parameter 0) + server = new ServerSocket(0, Defaults.tcpConnectionBacklog); + myLog.l(Log.DEBUG, "Data socket pasv() listen successful"); + return server.getLocalPort(); + } catch(IOException e) { + myLog.l(Log.ERROR, "Data socket creation error"); + clearState(); + return 0; + } + } + + @Override + public boolean onPort(InetAddress remoteAddr, int remotePort) { + clearState(); + this.remoteAddr = remoteAddr; + this.remotePort = remotePort; + return true; + } + + @Override + public Socket onTransfer() { + if(server == null) { + // We're in PORT mode (not PASV) + if(remoteAddr == null || remotePort == 0) { + myLog.l(Log.INFO, "PORT mode but not initialized correctly"); + clearState(); + return null; + } + Socket socket; + try { + socket = new Socket(remoteAddr, remotePort); + } catch (IOException e) { + myLog.l(Log.INFO, + "Couldn't open PORT data socket to: " + + remoteAddr.toString() + ":" + remotePort); + clearState(); + return null; + } + + // Kill the socket if nothing happens for X milliseconds + try { + socket.setSoTimeout(Defaults.SO_TIMEOUT_MS); + } catch (Exception e) { + myLog.l(Log.ERROR, "Couldn't set SO_TIMEOUT"); + clearState(); + return null; + } + + return socket; + } else { + // We're in PASV mode (not PORT) + Socket socket = null; + try { + socket = server.accept(); + myLog.l(Log.DEBUG, "onTransfer pasv accept successful"); + } catch (Exception e) { + myLog.l(Log.INFO, "Exception accepting PASV socket"); + socket = null; + } + clearState(); + return socket; // will be null if error occurred + } + } + + /** + * Return the port number that the remote client should be informed of (in the body + * of the PASV response). + * @return The port number, or -1 if error. + */ + public int getPortNumber() { + if(server != null) { + return server.getLocalPort(); // returns -1 if serversocket is unbound + } else { + return -1; + } + } + + @Override + public InetAddress getPasvIp() { + //String retVal = server.getInetAddress().getHostAddress(); + return FTPServerService.getWifiIp(); + } + + @Override + public void reportTraffic(long bytes) { + // ignore, we don't care about how much traffic goes over wifi. + } +} diff --git a/src/main/java/org/swiftp/server/ProxyConnector.java b/src/main/java/org/swiftp/server/ProxyConnector.java index 962d094163f5494f2e1b862c4883f7411fb3e7ca..e0f264bab0ac01ce14fc3d31d5bed9b0a8da0c9f 100644 --- a/src/main/java/org/swiftp/server/ProxyConnector.java +++ b/src/main/java/org/swiftp/server/ProxyConnector.java @@ -1,769 +1,769 @@ -/* -Copyright 2009 David Revell - -This file is part of SwiFTP. - -SwiFTP is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -SwiFTP is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with SwiFTP. If not, see . - */ - -package org.swiftp.server; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.Socket; -import java.util.Arrays; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; -import java.util.Queue; - -import org.json.JSONException; -import org.json.JSONObject; -import org.swiftp.Defaults; -import org.swiftp.FTPServerService; -import org.swiftp.Globals; -import org.swiftp.MyLog; -import org.swiftp.R; -import org.swiftp.Util; - -import android.content.Context; -import android.content.SharedPreferences; -import android.util.Log; - -public class ProxyConnector extends Thread { - public static final int IN_BUF_SIZE = 2048; - public static final String ENCODING = "UTF-8"; - public static final int RESPONSE_WAIT_MS = 10000; - public static final int QUEUE_WAIT_MS = 20000; - public static final long UPDATE_USAGE_BYTES = 5000000; - public static final String PREFERRED_SERVER = "preferred_server"; // preferences - public static final int CONNECT_TIMEOUT = 5000; - - private final FTPServerService ftpServerService; - private final MyLog myLog = new MyLog(getClass().getName()); - private JSONObject response = null; - private Thread responseWaiter = null; - private final Queue queuedRequestThreads = new LinkedList(); - private Socket commandSocket = null; - private OutputStream out = null; - private String hostname = null; - private InputStream inputStream = null; - private long proxyUsage = 0; - private State proxyState = State.DISCONNECTED; - private String prefix; - private String proxyMessage = null; - - public enum State { - CONNECTING, CONNECTED, FAILED, UNREACHABLE, DISCONNECTED - }; - - // QuotaStats cachedQuotaStats = null; // quotas have been canceled for now - - static final String USAGE_PREFS_NAME = "proxy_usage_data"; - - /* - * We establish a so-called "command session" to the proxy. New connections will be - * handled by creating addition control and data connections to the proxy. See - * proxy_protocol.txt and proxy_architecture.pdf for an explanation of how proxying - * works. Hint: it's complicated. - */ - - public ProxyConnector(FTPServerService ftpServerService) { - this.ftpServerService = ftpServerService; - this.proxyUsage = getPersistedProxyUsage(); - setProxyState(State.DISCONNECTED); - Globals.setProxyConnector(this); - } - - @Override - public void run() { - myLog.i("In ProxyConnector.run()"); - setProxyState(State.CONNECTING); - try { - String candidateProxies[] = getProxyList(); - for (String candidateHostname : candidateProxies) { - hostname = candidateHostname; - commandSocket = newAuthedSocket(hostname, Defaults.REMOTE_PROXY_PORT); - if (commandSocket == null) { - continue; - } - commandSocket.setSoTimeout(0); // 0 == forever - // commandSocket.setKeepAlive(true); - // Now that we have authenticated, we want to start the command session so - // we can - // be notified of pending control sessions. - JSONObject request = makeJsonRequest("start_command_session"); - response = sendRequest(commandSocket, request); - if (response == null) { - myLog.i("Couldn't create proxy command session"); - continue; // try next server - } - if (!response.has("prefix")) { - myLog.l(Log.INFO, - "start_command_session didn't receive a prefix in response"); - continue; // try next server - } - prefix = response.getString("prefix"); - response = null; // Indicate that response is free for other use - myLog.l(Log.INFO, "Got prefix of: " + prefix); - break; // breaking with commandSocket != null indicates success - } - if (commandSocket == null) { - myLog.l(Log.INFO, "No proxies accepted connection, failing."); - setProxyState(State.UNREACHABLE); - return; - } - setProxyState(State.CONNECTED); - preferServer(hostname); - inputStream = commandSocket.getInputStream(); - out = commandSocket.getOutputStream(); - int numBytes; - byte[] bytes = new byte[IN_BUF_SIZE]; - // spawnQuotaRequester().start(); - while (true) { - myLog.d("to proxy read()"); - numBytes = inputStream.read(bytes); - incrementProxyUsage(numBytes); - myLog.d("from proxy read()"); - JSONObject incomingJson = null; - if (numBytes > 0) { - String responseString = new String(bytes, ENCODING); - incomingJson = new JSONObject(responseString); - if (incomingJson.has("action")) { - // If the incoming JSON object has an "action" field, then it is a - // request, and not a response - incomingCommand(incomingJson); - } else { - // If the incoming JSON object does not have an "action" field, - // then - // it is a response to a request we sent earlier. - // If there's an object waiting for a response, then that object - // will be referenced by responseWaiter. - if (responseWaiter != null) { - if (response != null) { - myLog.l(Log.INFO, - "Overwriting existing cmd session response"); - } - response = incomingJson; - responseWaiter.interrupt(); - } else { - myLog.l(Log.INFO, "Response received but no responseWaiter"); - } - } - } else if (numBytes == 0) { - myLog.d("Command socket read 0 bytes, looping"); - } else { // numBytes < 0 - myLog.l(Log.DEBUG, "Command socket end of stream, exiting"); - if (proxyState != State.DISCONNECTED) { - // Set state to FAILED unless this was an intentional - // socket closure. - setProxyState(State.FAILED); - } - break; - } - } - myLog.l(Log.INFO, "ProxyConnector thread quitting cleanly"); - } catch (IOException e) { - myLog.l(Log.INFO, "IOException in command session: " + e); - setProxyState(State.FAILED); - } catch (JSONException e) { - myLog.l(Log.INFO, "Commmand socket JSONException: " + e); - setProxyState(State.FAILED); - } catch (Exception e) { - myLog.l(Log.INFO, "Other exception in ProxyConnector: " + e); - setProxyState(State.FAILED); - } finally { - Globals.setProxyConnector(null); - hostname = null; - myLog.d("ProxyConnector.run() returning"); - persistProxyUsage(); - } - } - - // This function is used to spawn a new Thread that will make a request over the - // command thread. Since the main ProxyConnector thread handles the input - // request/response de-multiplexing, it cannot also make a request using the - // sendCmdSocketRequest, since sendCmdSocketRequest will block waiting for - // a response, but the same thread is expected to deliver the response. - // The short story is, if the main ProxyConnector command session thread wants to - // make a request, the easiest way is to spawn a new thread and have it call - // sendCmdSocketRequest in the same way as any other thread. - // private Thread spawnQuotaRequester() { - // return new Thread() { - // public void run() { - // getQuotaStats(false); - // } - // }; - // } - - /** - * Since we want devices to generally stick with the same proxy server, and we may - * want to explicitly redirect some devices to other servers, we have this mechanism - * to store a "preferred server" on the device. - */ - private void preferServer(String hostname) { - SharedPreferences prefs = Globals.getContext().getSharedPreferences( - PREFERRED_SERVER, 0); - SharedPreferences.Editor editor = prefs.edit(); - editor.putString(PREFERRED_SERVER, hostname); - editor.commit(); - } - - private String[] getProxyList() { - SharedPreferences prefs = Globals.getContext().getSharedPreferences( - PREFERRED_SERVER, 0); - String preferred = prefs.getString(PREFERRED_SERVER, null); - - String[] allProxies; - - if (Defaults.release) { - allProxies = new String[] { "c1.swiftp.org", "c2.swiftp.org", - "c3.swiftp.org", "c4.swiftp.org", "c5.swiftp.org", "c6.swiftp.org", - "c7.swiftp.org", "c8.swiftp.org", "c9.swiftp.org" }; - } else { - // allProxies = new String[] { - // "cdev.swiftp.org" - // }; - allProxies = new String[] { "c1.swiftp.org", "c2.swiftp.org", - "c3.swiftp.org", "c4.swiftp.org", "c5.swiftp.org", "c6.swiftp.org", - "c7.swiftp.org", "c8.swiftp.org", "c9.swiftp.org" }; - } - - // We should randomly permute the server list in order to spread - // load between servers. Collections offers a shuffle() function - // that does this, so we'll convert to List and back to String[]. - List proxyList = Arrays.asList(allProxies); - Collections.shuffle(proxyList); - allProxies = proxyList.toArray(new String[] {}); // arg used for type - - // Return preferred server first, followed by all others - if (preferred == null) { - return allProxies; - } else { - return Util.concatStrArrays(new String[] { preferred }, allProxies); - } - } - - private boolean checkAndPrintJsonError(JSONObject json) throws JSONException { - if (json.has("error_code")) { - // The returned JSON object will have a field called "errorCode" - // if there was a problem executing our request. - StringBuilder s = new StringBuilder("Error in JSON response, code: "); - s.append(json.getString("error_code")); - if (json.has("error_string")) { - s.append(", string: "); - s.append(json.getString("error_string")); - } - myLog.l(Log.INFO, s.toString()); - - // Obsolete: there's no authentication anymore - // Dev code to enable frequent database wipes. If we fail to login, - // remove our stored account info, causing a create_account action - // next time. - // if(!Defaults.release) { - // if(json.getInt("error_code") == 11) { - // myLog.l(Log.DEBUG, "Dev: removing secret due to login failure"); - // removeSecret(); - // } - // } - return true; - } - return false; - } - - /** - * Reads our persistent storage, looking for a stored proxy authentication secret. - * - * @return The secret, if present, or null. - */ - // Obsolete, there's no authentication anymore - /* - * private String retrieveSecret() { SharedPreferences settings = - * Globals.getContext(). getSharedPreferences(Defaults.getSettingsName(), - * Defaults.getSettingsMode()); return settings.getString("proxySecret", null); } - */ - - // Obsolete, there's no authentication anymore - /* - * private void storeSecret(String secret) { SharedPreferences settings = - * Globals.getContext(). getSharedPreferences(Defaults.getSettingsName(), - * Defaults.getSettingsMode()); Editor editor = settings.edit(); - * editor.putString("proxySecret", secret); editor.commit(); } - */ - - // Obsolete, there's no authentication anymore - /* - * private void removeSecret() { SharedPreferences settings = Globals.getContext(). - * getSharedPreferences(Defaults.getSettingsName(), Defaults.getSettingsMode()); - * Editor editor = settings.edit(); editor.remove("proxySecret"); editor.commit(); } - */ - - private void incomingCommand(JSONObject json) { - try { - String action = json.getString("action"); - if (action.equals("control_connection_waiting")) { - startControlSession(json.getInt("port")); - } else if (action.equals("prefer_server")) { - String host = json.getString("host"); // throws JSONException, fine - preferServer(host); - myLog.i("New preferred server: " + host); - } else if (action.equals("message")) { - proxyMessage = json.getString("text"); - myLog.i("Got news from proxy server: \"" + proxyMessage + "\""); - // TODO: send intent to notify UI about news - // FTPServerService.updateClients(); // UI update to show message - } else if (action.equals("noop")) { - myLog.d("Proxy noop"); - } else { - myLog.l(Log.INFO, "Unsupported incoming action: " + action); - } - // If we're starting a control session register with ftpServerService - } catch (JSONException e) { - myLog.l(Log.INFO, "JSONException in proxy incomingCommand"); - } - } - - private void startControlSession(int port) { - Socket socket; - myLog.d("Starting new proxy FTP control session"); - socket = newAuthedSocket(hostname, port); - if (socket == null) { - myLog.i("startControlSession got null authed socket"); - return; - } - ProxyDataSocketFactory dataSocketFactory = new ProxyDataSocketFactory(); - SessionThread thread = new SessionThread(socket, dataSocketFactory, - SessionThread.Source.PROXY); - thread.start(); - ftpServerService.registerSessionThread(thread); - } - - /** - * Connects an outgoing socket to the proxy and authenticates, creating an account if - * necessary. - */ - private Socket newAuthedSocket(String hostname, int port) { - if (hostname == null) { - myLog.i("newAuthedSocket can't connect to null host"); - return null; - } - JSONObject json = new JSONObject(); - // String secret = retrieveSecret(); - Socket socket; - OutputStream out = null; - InputStream in = null; - - try { - myLog.d("Opening proxy connection to " + hostname + ":" + port); - socket = new Socket(); - socket.connect(new InetSocketAddress(hostname, port), CONNECT_TIMEOUT); - json.put("android_id", Util.getAndroidId()); - json.put("swiftp_version", Util.getVersion()); - json.put("action", "login"); - out = socket.getOutputStream(); - in = socket.getInputStream(); - int numBytes; - - out.write(json.toString().getBytes(ENCODING)); - myLog.l(Log.DEBUG, "Sent login request"); - // Read and parse the server's response - byte[] bytes = new byte[IN_BUF_SIZE]; - // Here we assume that the server's response will all be contained in - // a single read, which may be unsafe for large responses - numBytes = in.read(bytes); - if (numBytes == -1) { - myLog.l(Log.INFO, "Proxy socket closed while waiting for auth response"); - return null; - } else if (numBytes == 0) { - myLog.l(Log.INFO, "Short network read waiting for auth, quitting"); - return null; - } - json = new JSONObject(new String(bytes, 0, numBytes, ENCODING)); - if (checkAndPrintJsonError(json)) { - return null; - } - myLog.d("newAuthedSocket successful"); - return socket; - } catch (Exception e) { - myLog.i("Exception during proxy connection or authentication: " + e); - return null; - } - } - - public void quit() { - setProxyState(State.DISCONNECTED); - try { - sendRequest(commandSocket, makeJsonRequest("finished")); // ignore reply - - if (inputStream != null) { - myLog.d("quit() closing proxy inputStream"); - inputStream.close(); - } else { - myLog.d("quit() won't close null inputStream"); - } - if (commandSocket != null) { - myLog.d("quit() closing proxy socket"); - commandSocket.close(); - } else { - myLog.d("quit() won't close null socket"); - } - } catch (IOException e) { - } catch (JSONException e) { - } - persistProxyUsage(); - Globals.setProxyConnector(null); - } - - @SuppressWarnings("unused") - private JSONObject sendCmdSocketRequest(JSONObject json) { - try { - boolean queued; - synchronized (this) { - if (responseWaiter == null) { - responseWaiter = Thread.currentThread(); - queued = false; - myLog.d("sendCmdSocketRequest proceeding without queue"); - } else if (!responseWaiter.isAlive()) { - // This code should never run. It is meant to recover from a situation - // where there is a thread that sent a proxy request but died before - // starting the subsequent request. If this is the case, the correct - // behavior is to run the next queued thread in the queue, or if the - // queue is empty, to perform our own request. - myLog.l(Log.INFO, "Won't wait on dead responseWaiter"); - if (queuedRequestThreads.size() == 0) { - responseWaiter = Thread.currentThread(); - queued = false; - } else { - queuedRequestThreads.add(Thread.currentThread()); - queuedRequestThreads.remove().interrupt(); // start queued thread - queued = true; - } - } else { - myLog.d("sendCmdSocketRequest queueing thread"); - queuedRequestThreads.add(Thread.currentThread()); - queued = true; - } - } - // If a different thread has sent a request and is waiting for a response, - // then the current thread will be in a queue waiting for an interrupt - if (queued) { - // The current thread must wait until we are popped off the waiting queue - // and receive an interrupt() - boolean interrupted = false; - try { - myLog.d("Queued cmd session request thread sleeping..."); - Thread.sleep(QUEUE_WAIT_MS); - } catch (InterruptedException e) { - myLog.l(Log.DEBUG, "Proxy request popped and ready"); - interrupted = true; - } - if (!interrupted) { - myLog.l(Log.INFO, "Timed out waiting on proxy queue"); - return null; - } - } - // We have been popped from the wait queue if necessary, and now it's time - // to send the request. - try { - responseWaiter = Thread.currentThread(); - byte[] outboundData = Util.jsonToByteArray(json); - try { - out.write(outboundData); - } catch (IOException e) { - myLog.l(Log.INFO, "IOException sending proxy request"); - return null; - } - // Wait RESPONSE_WAIT_MS for a response from the proxy - boolean interrupted = false; - try { - // Wait for the main ProxyConnector thread to interrupt us, meaning - // that a response has been received. - myLog.d("Cmd session request sleeping until response"); - Thread.sleep(RESPONSE_WAIT_MS); - } catch (InterruptedException e) { - myLog.d("Cmd session response received"); - interrupted = true; - } - if (!interrupted) { - myLog.l(Log.INFO, "Proxy request timed out"); - return null; - } - // At this point, the main ProxyConnector thread will have stored - // our response in "JSONObject response". - myLog.d("Cmd session response was: " + response); - return response; - } finally { - // Make sure that when this request finishes, the next thread on the - // queue gets started. - synchronized (this) { - if (queuedRequestThreads.size() != 0) { - queuedRequestThreads.remove().interrupt(); - } - } - } - } catch (JSONException e) { - myLog.l(Log.INFO, "JSONException in sendRequest: " + e); - return null; - } - } - - public JSONObject sendRequest(InputStream in, OutputStream out, JSONObject request) - throws JSONException { - try { - out.write(Util.jsonToByteArray(request)); - byte[] bytes = new byte[IN_BUF_SIZE]; - int numBytes = in.read(bytes); - if (numBytes < 1) { - myLog.i("Proxy sendRequest short read on response"); - return null; - } - JSONObject response = Util.byteArrayToJson(bytes); - if (response == null) { - myLog.i("Null response to sendRequest"); - } - if (checkAndPrintJsonError(response)) { - myLog.i("Error response to sendRequest"); - return null; - } - return response; - } catch (IOException e) { - myLog.i("IOException in proxy sendRequest: " + e); - return null; - } - } - - public JSONObject sendRequest(Socket socket, JSONObject request) throws JSONException { - try { - if (socket == null) { - // The server is probably shutting down - myLog.i("null socket in ProxyConnector.sendRequest()"); - return null; - } else { - return sendRequest(socket.getInputStream(), socket.getOutputStream(), - request); - } - } catch (IOException e) { - myLog.i("IOException in proxy sendRequest wrapper: " + e); - return null; - } - } - - public ProxyDataSocketInfo pasvListen() { - try { - // connect to proxy and authenticate - myLog.d("Sending data_pasv_listen to proxy"); - Socket socket = newAuthedSocket(this.hostname, Defaults.REMOTE_PROXY_PORT); - if (socket == null) { - myLog.i("pasvListen got null socket"); - return null; - } - JSONObject request = makeJsonRequest("data_pasv_listen"); - - JSONObject response = sendRequest(socket, request); - if (response == null) { - return null; - } - int port = response.getInt("port"); - return new ProxyDataSocketInfo(socket, port); - } catch (JSONException e) { - myLog.l(Log.INFO, "JSONException in pasvListen"); - return null; - } - } - - public Socket dataPortConnect(InetAddress clientAddr, int clientPort) { - /** - * This function is called by a ProxyDataSocketFactory when it's time to transfer - * some data in PORT mode (not PASV mode). We send a data_port_connect request to - * the proxy, containing the IP and port of the FTP client to which a connection - * should be made. - */ - try { - myLog.d("Sending data_port_connect to proxy"); - Socket socket = newAuthedSocket(this.hostname, Defaults.REMOTE_PROXY_PORT); - if (socket == null) { - myLog.i("dataPortConnect got null socket"); - return null; - } - JSONObject request = makeJsonRequest("data_port_connect"); - request.put("address", clientAddr.getHostAddress()); - request.put("port", clientPort); - JSONObject response = sendRequest(socket, request); - if (response == null) { - return null; // logged elsewhere - } - return socket; - } catch (JSONException e) { - myLog.i("JSONException in dataPortConnect"); - return null; - } - } - - /** - * Given a socket returned from pasvListen(), send a data_pasv_accept request over the - * socket to the proxy, which should result in a socket that is ready for data - * transfer with the FTP client. Of course, this will only work if the FTP client - * connects to the proxy like it's supposed to. The client will have already been told - * to connect by the response to its PASV command. - * - * This should only be called from the onTransfer method of ProxyDataSocketFactory. - * - * @param socket - * A socket previously returned from ProxyConnector.pasvListen() - * @return true if the accept operation completed OK, otherwise false - */ - - public boolean pasvAccept(Socket socket) { - try { - JSONObject request = makeJsonRequest("data_pasv_accept"); - JSONObject response = sendRequest(socket, request); - if (response == null) { - return false; // error is logged elsewhere - } - if (checkAndPrintJsonError(response)) { - myLog.i("Error response to data_pasv_accept"); - return false; - } - // The proxy's response will be an empty JSON object on success - myLog.d("Proxy data_pasv_accept successful"); - return true; - } catch (JSONException e) { - myLog.i("JSONException in pasvAccept: " + e); - return false; - } - } - - public InetAddress getProxyIp() { - if (this.isAlive()) { - if (commandSocket.isConnected()) { - return commandSocket.getInetAddress(); - } - } - return null; - } - - private JSONObject makeJsonRequest(String action) throws JSONException { - JSONObject json = new JSONObject(); - json.put("action", action); - return json; - } - - /* - * Quotas have been canceled for now public QuotaStats getQuotaStats(boolean - * canUseCached) { if(canUseCached) { if(cachedQuotaStats != null) { - * myLog.d("Returning cachedQuotaStats"); return cachedQuotaStats; } else { - * myLog.d("Would return cached quota stats but none retrieved"); } } // If there's no - * cached quota stats, or if the called wants fresh stats, // make a JSON request to - * the proxy, assuming the command session is open. try { JSONObject response = - * sendCmdSocketRequest(makeJsonRequest("check_quota")); int used, quota; if(response - * == null) { myLog.w("check_quota got null response"); return null; } used = - * response.getInt("used"); quota = response.getInt("quota"); - * myLog.d("Got quota response of " + used + "/" + quota); cachedQuotaStats = new - * QuotaStats(used, quota) ; return cachedQuotaStats; } catch (JSONException e) { - * myLog.w("JSONException in getQuota: " + e); return null; } } - */ - - // We want to track the total amount of data sent via the proxy server, to - // show it to the user and encourage them to donate. - void persistProxyUsage() { - if (proxyUsage == 0) { - return; // This shouldn't happen, but just for safety - } - SharedPreferences prefs = Globals.getContext().getSharedPreferences( - USAGE_PREFS_NAME, 0); // 0 == private - SharedPreferences.Editor editor = prefs.edit(); - editor.putLong(USAGE_PREFS_NAME, proxyUsage); - editor.commit(); - myLog.d("Persisted proxy usage to preferences"); - } - - long getPersistedProxyUsage() { - // This gets the last persisted value for bytes transferred through - // the proxy. It can be out of date since it doesn't include data - // transferred during the current session. - SharedPreferences prefs = Globals.getContext().getSharedPreferences( - USAGE_PREFS_NAME, 0); // 0 == private - return prefs.getLong(USAGE_PREFS_NAME, 0); // Default count of 0 - } - - long getProxyUsage() { - // This gets the running total of all proxy usage, which may not have - // been persisted yet. - return proxyUsage; - } - - void incrementProxyUsage(long num) { - long oldProxyUsage = proxyUsage; - proxyUsage += num; - if (proxyUsage % UPDATE_USAGE_BYTES < oldProxyUsage % UPDATE_USAGE_BYTES) { - // TODO: Use intent to update UI - // FTPServerService.updateClients(); - persistProxyUsage(); - } - } - - public State getProxyState() { - return proxyState; - } - - private void setProxyState(State state) { - proxyState = state; - myLog.l(Log.DEBUG, "Proxy state changed to " + state, true); - // TODO: Use intent to update UI - // FTPServerService.updateClients(); - } - - static public String stateToString(State s) { - Context ctx = Globals.getContext(); - switch (s) { - case DISCONNECTED: - return ctx.getString(R.string.pst_disconnected); - case CONNECTING: - return ctx.getString(R.string.pst_connecting); - case CONNECTED: - return ctx.getString(R.string.pst_connected); - case FAILED: - return ctx.getString(R.string.pst_failed); - case UNREACHABLE: - return ctx.getString(R.string.pst_unreachable); - default: - return ctx.getString(R.string.unknown); - } - } - - /** - * The URL to which users should point their FTP client. - */ - public String getURL() { - if (proxyState == State.CONNECTED) { - String username = Globals.getUsername(); - if (username != null) { - return "ftp://" + prefix + "_" + username + "@" + hostname; - } - } - return Globals.getContext().getString(R.string.unknown); - } - - /** - * If the proxy sends a human-readable message, it can be retrieved by calling this - * function. Returns null if no message has been received. - */ - public String getProxyMessage() { - return proxyMessage; - } - -} +/* +Copyright 2009 David Revell + +This file is part of SwiFTP. + +SwiFTP is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +SwiFTP is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with SwiFTP. If not, see . + */ + +package org.swiftp.server; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; + +import org.json.JSONException; +import org.json.JSONObject; +import org.swiftp.Defaults; +import org.swiftp.FTPServerService; +import org.swiftp.Globals; +import org.swiftp.MyLog; +import org.swiftp.R; +import org.swiftp.Util; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; + +public class ProxyConnector extends Thread { + public static final int IN_BUF_SIZE = 2048; + public static final String ENCODING = "UTF-8"; + public static final int RESPONSE_WAIT_MS = 10000; + public static final int QUEUE_WAIT_MS = 20000; + public static final long UPDATE_USAGE_BYTES = 5000000; + public static final String PREFERRED_SERVER = "preferred_server"; // preferences + public static final int CONNECT_TIMEOUT = 5000; + + private final FTPServerService ftpServerService; + private final MyLog myLog = new MyLog(getClass().getName()); + private JSONObject response = null; + private Thread responseWaiter = null; + private final Queue queuedRequestThreads = new LinkedList(); + private Socket commandSocket = null; + private OutputStream out = null; + private String hostname = null; + private InputStream inputStream = null; + private long proxyUsage = 0; + private State proxyState = State.DISCONNECTED; + private String prefix; + private String proxyMessage = null; + + public enum State { + CONNECTING, CONNECTED, FAILED, UNREACHABLE, DISCONNECTED + }; + + // QuotaStats cachedQuotaStats = null; // quotas have been canceled for now + + static final String USAGE_PREFS_NAME = "proxy_usage_data"; + + /* + * We establish a so-called "command session" to the proxy. New connections will be + * handled by creating addition control and data connections to the proxy. See + * proxy_protocol.txt and proxy_architecture.pdf for an explanation of how proxying + * works. Hint: it's complicated. + */ + + public ProxyConnector(FTPServerService ftpServerService) { + this.ftpServerService = ftpServerService; + this.proxyUsage = getPersistedProxyUsage(); + setProxyState(State.DISCONNECTED); + Globals.setProxyConnector(this); + } + + @Override + public void run() { + myLog.i("In ProxyConnector.run()"); + setProxyState(State.CONNECTING); + try { + String candidateProxies[] = getProxyList(); + for (String candidateHostname : candidateProxies) { + hostname = candidateHostname; + commandSocket = newAuthedSocket(hostname, Defaults.REMOTE_PROXY_PORT); + if (commandSocket == null) { + continue; + } + commandSocket.setSoTimeout(0); // 0 == forever + // commandSocket.setKeepAlive(true); + // Now that we have authenticated, we want to start the command session so + // we can + // be notified of pending control sessions. + JSONObject request = makeJsonRequest("start_command_session"); + response = sendRequest(commandSocket, request); + if (response == null) { + myLog.i("Couldn't create proxy command session"); + continue; // try next server + } + if (!response.has("prefix")) { + myLog.l(Log.INFO, + "start_command_session didn't receive a prefix in response"); + continue; // try next server + } + prefix = response.getString("prefix"); + response = null; // Indicate that response is free for other use + myLog.l(Log.INFO, "Got prefix of: " + prefix); + break; // breaking with commandSocket != null indicates success + } + if (commandSocket == null) { + myLog.l(Log.INFO, "No proxies accepted connection, failing."); + setProxyState(State.UNREACHABLE); + return; + } + setProxyState(State.CONNECTED); + preferServer(hostname); + inputStream = commandSocket.getInputStream(); + out = commandSocket.getOutputStream(); + int numBytes; + byte[] bytes = new byte[IN_BUF_SIZE]; + // spawnQuotaRequester().start(); + while (true) { + myLog.d("to proxy read()"); + numBytes = inputStream.read(bytes); + incrementProxyUsage(numBytes); + myLog.d("from proxy read()"); + JSONObject incomingJson = null; + if (numBytes > 0) { + String responseString = new String(bytes, ENCODING); + incomingJson = new JSONObject(responseString); + if (incomingJson.has("action")) { + // If the incoming JSON object has an "action" field, then it is a + // request, and not a response + incomingCommand(incomingJson); + } else { + // If the incoming JSON object does not have an "action" field, + // then + // it is a response to a request we sent earlier. + // If there's an object waiting for a response, then that object + // will be referenced by responseWaiter. + if (responseWaiter != null) { + if (response != null) { + myLog.l(Log.INFO, + "Overwriting existing cmd session response"); + } + response = incomingJson; + responseWaiter.interrupt(); + } else { + myLog.l(Log.INFO, "Response received but no responseWaiter"); + } + } + } else if (numBytes == 0) { + myLog.d("Command socket read 0 bytes, looping"); + } else { // numBytes < 0 + myLog.l(Log.DEBUG, "Command socket end of stream, exiting"); + if (proxyState != State.DISCONNECTED) { + // Set state to FAILED unless this was an intentional + // socket closure. + setProxyState(State.FAILED); + } + break; + } + } + myLog.l(Log.INFO, "ProxyConnector thread quitting cleanly"); + } catch (IOException e) { + myLog.l(Log.INFO, "IOException in command session: " + e); + setProxyState(State.FAILED); + } catch (JSONException e) { + myLog.l(Log.INFO, "Commmand socket JSONException: " + e); + setProxyState(State.FAILED); + } catch (Exception e) { + myLog.l(Log.INFO, "Other exception in ProxyConnector: " + e); + setProxyState(State.FAILED); + } finally { + Globals.setProxyConnector(null); + hostname = null; + myLog.d("ProxyConnector.run() returning"); + persistProxyUsage(); + } + } + + // This function is used to spawn a new Thread that will make a request over the + // command thread. Since the main ProxyConnector thread handles the input + // request/response de-multiplexing, it cannot also make a request using the + // sendCmdSocketRequest, since sendCmdSocketRequest will block waiting for + // a response, but the same thread is expected to deliver the response. + // The short story is, if the main ProxyConnector command session thread wants to + // make a request, the easiest way is to spawn a new thread and have it call + // sendCmdSocketRequest in the same way as any other thread. + // private Thread spawnQuotaRequester() { + // return new Thread() { + // public void run() { + // getQuotaStats(false); + // } + // }; + // } + + /** + * Since we want devices to generally stick with the same proxy server, and we may + * want to explicitly redirect some devices to other servers, we have this mechanism + * to store a "preferred server" on the device. + */ + private void preferServer(String hostname) { + SharedPreferences prefs = Globals.getContext().getSharedPreferences( + PREFERRED_SERVER, 0); + SharedPreferences.Editor editor = prefs.edit(); + editor.putString(PREFERRED_SERVER, hostname); + editor.commit(); + } + + private String[] getProxyList() { + SharedPreferences prefs = Globals.getContext().getSharedPreferences( + PREFERRED_SERVER, 0); + String preferred = prefs.getString(PREFERRED_SERVER, null); + + String[] allProxies; + + if (Defaults.release) { + allProxies = new String[] { "c1.swiftp.org", "c2.swiftp.org", + "c3.swiftp.org", "c4.swiftp.org", "c5.swiftp.org", "c6.swiftp.org", + "c7.swiftp.org", "c8.swiftp.org", "c9.swiftp.org" }; + } else { + // allProxies = new String[] { + // "cdev.swiftp.org" + // }; + allProxies = new String[] { "c1.swiftp.org", "c2.swiftp.org", + "c3.swiftp.org", "c4.swiftp.org", "c5.swiftp.org", "c6.swiftp.org", + "c7.swiftp.org", "c8.swiftp.org", "c9.swiftp.org" }; + } + + // We should randomly permute the server list in order to spread + // load between servers. Collections offers a shuffle() function + // that does this, so we'll convert to List and back to String[]. + List proxyList = Arrays.asList(allProxies); + Collections.shuffle(proxyList); + allProxies = proxyList.toArray(new String[] {}); // arg used for type + + // Return preferred server first, followed by all others + if (preferred == null) { + return allProxies; + } else { + return Util.concatStrArrays(new String[] { preferred }, allProxies); + } + } + + private boolean checkAndPrintJsonError(JSONObject json) throws JSONException { + if (json.has("error_code")) { + // The returned JSON object will have a field called "errorCode" + // if there was a problem executing our request. + StringBuilder s = new StringBuilder("Error in JSON response, code: "); + s.append(json.getString("error_code")); + if (json.has("error_string")) { + s.append(", string: "); + s.append(json.getString("error_string")); + } + myLog.l(Log.INFO, s.toString()); + + // Obsolete: there's no authentication anymore + // Dev code to enable frequent database wipes. If we fail to login, + // remove our stored account info, causing a create_account action + // next time. + // if(!Defaults.release) { + // if(json.getInt("error_code") == 11) { + // myLog.l(Log.DEBUG, "Dev: removing secret due to login failure"); + // removeSecret(); + // } + // } + return true; + } + return false; + } + + /** + * Reads our persistent storage, looking for a stored proxy authentication secret. + * + * @return The secret, if present, or null. + */ + // Obsolete, there's no authentication anymore + /* + * private String retrieveSecret() { SharedPreferences settings = + * Globals.getContext(). getSharedPreferences(Defaults.getSettingsName(), + * Defaults.getSettingsMode()); return settings.getString("proxySecret", null); } + */ + + // Obsolete, there's no authentication anymore + /* + * private void storeSecret(String secret) { SharedPreferences settings = + * Globals.getContext(). getSharedPreferences(Defaults.getSettingsName(), + * Defaults.getSettingsMode()); Editor editor = settings.edit(); + * editor.putString("proxySecret", secret); editor.commit(); } + */ + + // Obsolete, there's no authentication anymore + /* + * private void removeSecret() { SharedPreferences settings = Globals.getContext(). + * getSharedPreferences(Defaults.getSettingsName(), Defaults.getSettingsMode()); + * Editor editor = settings.edit(); editor.remove("proxySecret"); editor.commit(); } + */ + + private void incomingCommand(JSONObject json) { + try { + String action = json.getString("action"); + if (action.equals("control_connection_waiting")) { + startControlSession(json.getInt("port")); + } else if (action.equals("prefer_server")) { + String host = json.getString("host"); // throws JSONException, fine + preferServer(host); + myLog.i("New preferred server: " + host); + } else if (action.equals("message")) { + proxyMessage = json.getString("text"); + myLog.i("Got news from proxy server: \"" + proxyMessage + "\""); + // TODO: send intent to notify UI about news + // FTPServerService.updateClients(); // UI update to show message + } else if (action.equals("noop")) { + myLog.d("Proxy noop"); + } else { + myLog.l(Log.INFO, "Unsupported incoming action: " + action); + } + // If we're starting a control session register with ftpServerService + } catch (JSONException e) { + myLog.l(Log.INFO, "JSONException in proxy incomingCommand"); + } + } + + private void startControlSession(int port) { + Socket socket; + myLog.d("Starting new proxy FTP control session"); + socket = newAuthedSocket(hostname, port); + if (socket == null) { + myLog.i("startControlSession got null authed socket"); + return; + } + ProxyDataSocketFactory dataSocketFactory = new ProxyDataSocketFactory(); + SessionThread thread = new SessionThread(socket, dataSocketFactory, + SessionThread.Source.PROXY); + thread.start(); + ftpServerService.registerSessionThread(thread); + } + + /** + * Connects an outgoing socket to the proxy and authenticates, creating an account if + * necessary. + */ + private Socket newAuthedSocket(String hostname, int port) { + if (hostname == null) { + myLog.i("newAuthedSocket can't connect to null host"); + return null; + } + JSONObject json = new JSONObject(); + // String secret = retrieveSecret(); + Socket socket; + OutputStream out = null; + InputStream in = null; + + try { + myLog.d("Opening proxy connection to " + hostname + ":" + port); + socket = new Socket(); + socket.connect(new InetSocketAddress(hostname, port), CONNECT_TIMEOUT); + json.put("android_id", Util.getAndroidId()); + json.put("swiftp_version", Util.getVersion()); + json.put("action", "login"); + out = socket.getOutputStream(); + in = socket.getInputStream(); + int numBytes; + + out.write(json.toString().getBytes(ENCODING)); + myLog.l(Log.DEBUG, "Sent login request"); + // Read and parse the server's response + byte[] bytes = new byte[IN_BUF_SIZE]; + // Here we assume that the server's response will all be contained in + // a single read, which may be unsafe for large responses + numBytes = in.read(bytes); + if (numBytes == -1) { + myLog.l(Log.INFO, "Proxy socket closed while waiting for auth response"); + return null; + } else if (numBytes == 0) { + myLog.l(Log.INFO, "Short network read waiting for auth, quitting"); + return null; + } + json = new JSONObject(new String(bytes, 0, numBytes, ENCODING)); + if (checkAndPrintJsonError(json)) { + return null; + } + myLog.d("newAuthedSocket successful"); + return socket; + } catch (Exception e) { + myLog.i("Exception during proxy connection or authentication: " + e); + return null; + } + } + + public void quit() { + setProxyState(State.DISCONNECTED); + try { + sendRequest(commandSocket, makeJsonRequest("finished")); // ignore reply + + if (inputStream != null) { + myLog.d("quit() closing proxy inputStream"); + inputStream.close(); + } else { + myLog.d("quit() won't close null inputStream"); + } + if (commandSocket != null) { + myLog.d("quit() closing proxy socket"); + commandSocket.close(); + } else { + myLog.d("quit() won't close null socket"); + } + } catch (IOException e) { + } catch (JSONException e) { + } + persistProxyUsage(); + Globals.setProxyConnector(null); + } + + @SuppressWarnings("unused") + private JSONObject sendCmdSocketRequest(JSONObject json) { + try { + boolean queued; + synchronized (this) { + if (responseWaiter == null) { + responseWaiter = Thread.currentThread(); + queued = false; + myLog.d("sendCmdSocketRequest proceeding without queue"); + } else if (!responseWaiter.isAlive()) { + // This code should never run. It is meant to recover from a situation + // where there is a thread that sent a proxy request but died before + // starting the subsequent request. If this is the case, the correct + // behavior is to run the next queued thread in the queue, or if the + // queue is empty, to perform our own request. + myLog.l(Log.INFO, "Won't wait on dead responseWaiter"); + if (queuedRequestThreads.size() == 0) { + responseWaiter = Thread.currentThread(); + queued = false; + } else { + queuedRequestThreads.add(Thread.currentThread()); + queuedRequestThreads.remove().interrupt(); // start queued thread + queued = true; + } + } else { + myLog.d("sendCmdSocketRequest queueing thread"); + queuedRequestThreads.add(Thread.currentThread()); + queued = true; + } + } + // If a different thread has sent a request and is waiting for a response, + // then the current thread will be in a queue waiting for an interrupt + if (queued) { + // The current thread must wait until we are popped off the waiting queue + // and receive an interrupt() + boolean interrupted = false; + try { + myLog.d("Queued cmd session request thread sleeping..."); + Thread.sleep(QUEUE_WAIT_MS); + } catch (InterruptedException e) { + myLog.l(Log.DEBUG, "Proxy request popped and ready"); + interrupted = true; + } + if (!interrupted) { + myLog.l(Log.INFO, "Timed out waiting on proxy queue"); + return null; + } + } + // We have been popped from the wait queue if necessary, and now it's time + // to send the request. + try { + responseWaiter = Thread.currentThread(); + byte[] outboundData = Util.jsonToByteArray(json); + try { + out.write(outboundData); + } catch (IOException e) { + myLog.l(Log.INFO, "IOException sending proxy request"); + return null; + } + // Wait RESPONSE_WAIT_MS for a response from the proxy + boolean interrupted = false; + try { + // Wait for the main ProxyConnector thread to interrupt us, meaning + // that a response has been received. + myLog.d("Cmd session request sleeping until response"); + Thread.sleep(RESPONSE_WAIT_MS); + } catch (InterruptedException e) { + myLog.d("Cmd session response received"); + interrupted = true; + } + if (!interrupted) { + myLog.l(Log.INFO, "Proxy request timed out"); + return null; + } + // At this point, the main ProxyConnector thread will have stored + // our response in "JSONObject response". + myLog.d("Cmd session response was: " + response); + return response; + } finally { + // Make sure that when this request finishes, the next thread on the + // queue gets started. + synchronized (this) { + if (queuedRequestThreads.size() != 0) { + queuedRequestThreads.remove().interrupt(); + } + } + } + } catch (JSONException e) { + myLog.l(Log.INFO, "JSONException in sendRequest: " + e); + return null; + } + } + + public JSONObject sendRequest(InputStream in, OutputStream out, JSONObject request) + throws JSONException { + try { + out.write(Util.jsonToByteArray(request)); + byte[] bytes = new byte[IN_BUF_SIZE]; + int numBytes = in.read(bytes); + if (numBytes < 1) { + myLog.i("Proxy sendRequest short read on response"); + return null; + } + JSONObject response = Util.byteArrayToJson(bytes); + if (response == null) { + myLog.i("Null response to sendRequest"); + } + if (checkAndPrintJsonError(response)) { + myLog.i("Error response to sendRequest"); + return null; + } + return response; + } catch (IOException e) { + myLog.i("IOException in proxy sendRequest: " + e); + return null; + } + } + + public JSONObject sendRequest(Socket socket, JSONObject request) throws JSONException { + try { + if (socket == null) { + // The server is probably shutting down + myLog.i("null socket in ProxyConnector.sendRequest()"); + return null; + } else { + return sendRequest(socket.getInputStream(), socket.getOutputStream(), + request); + } + } catch (IOException e) { + myLog.i("IOException in proxy sendRequest wrapper: " + e); + return null; + } + } + + public ProxyDataSocketInfo pasvListen() { + try { + // connect to proxy and authenticate + myLog.d("Sending data_pasv_listen to proxy"); + Socket socket = newAuthedSocket(this.hostname, Defaults.REMOTE_PROXY_PORT); + if (socket == null) { + myLog.i("pasvListen got null socket"); + return null; + } + JSONObject request = makeJsonRequest("data_pasv_listen"); + + JSONObject response = sendRequest(socket, request); + if (response == null) { + return null; + } + int port = response.getInt("port"); + return new ProxyDataSocketInfo(socket, port); + } catch (JSONException e) { + myLog.l(Log.INFO, "JSONException in pasvListen"); + return null; + } + } + + public Socket dataPortConnect(InetAddress clientAddr, int clientPort) { + /** + * This function is called by a ProxyDataSocketFactory when it's time to transfer + * some data in PORT mode (not PASV mode). We send a data_port_connect request to + * the proxy, containing the IP and port of the FTP client to which a connection + * should be made. + */ + try { + myLog.d("Sending data_port_connect to proxy"); + Socket socket = newAuthedSocket(this.hostname, Defaults.REMOTE_PROXY_PORT); + if (socket == null) { + myLog.i("dataPortConnect got null socket"); + return null; + } + JSONObject request = makeJsonRequest("data_port_connect"); + request.put("address", clientAddr.getHostAddress()); + request.put("port", clientPort); + JSONObject response = sendRequest(socket, request); + if (response == null) { + return null; // logged elsewhere + } + return socket; + } catch (JSONException e) { + myLog.i("JSONException in dataPortConnect"); + return null; + } + } + + /** + * Given a socket returned from pasvListen(), send a data_pasv_accept request over the + * socket to the proxy, which should result in a socket that is ready for data + * transfer with the FTP client. Of course, this will only work if the FTP client + * connects to the proxy like it's supposed to. The client will have already been told + * to connect by the response to its PASV command. + * + * This should only be called from the onTransfer method of ProxyDataSocketFactory. + * + * @param socket + * A socket previously returned from ProxyConnector.pasvListen() + * @return true if the accept operation completed OK, otherwise false + */ + + public boolean pasvAccept(Socket socket) { + try { + JSONObject request = makeJsonRequest("data_pasv_accept"); + JSONObject response = sendRequest(socket, request); + if (response == null) { + return false; // error is logged elsewhere + } + if (checkAndPrintJsonError(response)) { + myLog.i("Error response to data_pasv_accept"); + return false; + } + // The proxy's response will be an empty JSON object on success + myLog.d("Proxy data_pasv_accept successful"); + return true; + } catch (JSONException e) { + myLog.i("JSONException in pasvAccept: " + e); + return false; + } + } + + public InetAddress getProxyIp() { + if (this.isAlive()) { + if (commandSocket.isConnected()) { + return commandSocket.getInetAddress(); + } + } + return null; + } + + private JSONObject makeJsonRequest(String action) throws JSONException { + JSONObject json = new JSONObject(); + json.put("action", action); + return json; + } + + /* + * Quotas have been canceled for now public QuotaStats getQuotaStats(boolean + * canUseCached) { if(canUseCached) { if(cachedQuotaStats != null) { + * myLog.d("Returning cachedQuotaStats"); return cachedQuotaStats; } else { + * myLog.d("Would return cached quota stats but none retrieved"); } } // If there's no + * cached quota stats, or if the called wants fresh stats, // make a JSON request to + * the proxy, assuming the command session is open. try { JSONObject response = + * sendCmdSocketRequest(makeJsonRequest("check_quota")); int used, quota; if(response + * == null) { myLog.w("check_quota got null response"); return null; } used = + * response.getInt("used"); quota = response.getInt("quota"); + * myLog.d("Got quota response of " + used + "/" + quota); cachedQuotaStats = new + * QuotaStats(used, quota) ; return cachedQuotaStats; } catch (JSONException e) { + * myLog.w("JSONException in getQuota: " + e); return null; } } + */ + + // We want to track the total amount of data sent via the proxy server, to + // show it to the user and encourage them to donate. + void persistProxyUsage() { + if (proxyUsage == 0) { + return; // This shouldn't happen, but just for safety + } + SharedPreferences prefs = Globals.getContext().getSharedPreferences( + USAGE_PREFS_NAME, 0); // 0 == private + SharedPreferences.Editor editor = prefs.edit(); + editor.putLong(USAGE_PREFS_NAME, proxyUsage); + editor.commit(); + myLog.d("Persisted proxy usage to preferences"); + } + + long getPersistedProxyUsage() { + // This gets the last persisted value for bytes transferred through + // the proxy. It can be out of date since it doesn't include data + // transferred during the current session. + SharedPreferences prefs = Globals.getContext().getSharedPreferences( + USAGE_PREFS_NAME, 0); // 0 == private + return prefs.getLong(USAGE_PREFS_NAME, 0); // Default count of 0 + } + + long getProxyUsage() { + // This gets the running total of all proxy usage, which may not have + // been persisted yet. + return proxyUsage; + } + + void incrementProxyUsage(long num) { + long oldProxyUsage = proxyUsage; + proxyUsage += num; + if (proxyUsage % UPDATE_USAGE_BYTES < oldProxyUsage % UPDATE_USAGE_BYTES) { + // TODO: Use intent to update UI + // FTPServerService.updateClients(); + persistProxyUsage(); + } + } + + public State getProxyState() { + return proxyState; + } + + private void setProxyState(State state) { + proxyState = state; + myLog.l(Log.DEBUG, "Proxy state changed to " + state, true); + // TODO: Use intent to update UI + // FTPServerService.updateClients(); + } + + static public String stateToString(State s) { + Context ctx = Globals.getContext(); + switch (s) { + case DISCONNECTED: + return ctx.getString(R.string.pst_disconnected); + case CONNECTING: + return ctx.getString(R.string.pst_connecting); + case CONNECTED: + return ctx.getString(R.string.pst_connected); + case FAILED: + return ctx.getString(R.string.pst_failed); + case UNREACHABLE: + return ctx.getString(R.string.pst_unreachable); + default: + return ctx.getString(R.string.unknown); + } + } + + /** + * The URL to which users should point their FTP client. + */ + public String getURL() { + if (proxyState == State.CONNECTED) { + String username = Globals.getUsername(); + if (username != null) { + return "ftp://" + prefix + "_" + username + "@" + hostname; + } + } + return Globals.getContext().getString(R.string.unknown); + } + + /** + * If the proxy sends a human-readable message, it can be retrieved by calling this + * function. Returns null if no message has been received. + */ + public String getProxyMessage() { + return proxyMessage; + } + +} diff --git a/src/main/java/org/swiftp/server/ProxyDataSocketFactory.java b/src/main/java/org/swiftp/server/ProxyDataSocketFactory.java index 1d909a6220de37520f17333e6c6119e1d1193db3..3864580edc6031df23e7df0bbb6aafbc3065e9e1 100644 --- a/src/main/java/org/swiftp/server/ProxyDataSocketFactory.java +++ b/src/main/java/org/swiftp/server/ProxyDataSocketFactory.java @@ -1,163 +1,163 @@ -/* -Copyright 2009 David Revell - -This file is part of SwiFTP. - -SwiFTP is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -SwiFTP is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with SwiFTP. If not, see . -*/ - -package org.swiftp.server; - -import java.net.InetAddress; -import java.net.Socket; - -import org.swiftp.Globals; - -import android.util.Log; - -/** - * @author david - * - */ -public class ProxyDataSocketFactory extends DataSocketFactory { - /** - * Implements data socket connections that go through our proxy server - * out on the net. The proxy sits between the FTP client and us, the server. - * We have to build in some coordination between the server and proxy in order - * for data sockets to be handled properly. - * - * When we receive a "PASV" command from a client, we have to request that the - * proxy server open a port, accept a connection, and proxy all data on that - * socket between ourself and the FTP client. - * - * When we receive a PORT command, we store the client's connection info, - * and when it's time to being transferring data, we request that the proxy - * make a connection to the client's IP & port and then proxy all data between - * ourself and the FTP client. - */ - - private Socket socket; - private int proxyListenPort; - ProxyConnector proxyConnector; - InetAddress clientAddress; - int clientPort; - - public ProxyDataSocketFactory() { - clearState(); - } - - private void clearState() { - if(socket != null) { - try { - socket.close(); - } catch (Exception e) {} - } - socket = null; - proxyConnector = null; - clientAddress = null; - proxyListenPort = 0; - clientPort = 0; - } - - @Override - public InetAddress getPasvIp() { - ProxyConnector pc = Globals.getProxyConnector(); - if(pc == null) { - return null; - } - return pc.getProxyIp(); - } - -// public int getPortNumber() { -// if(socket == ) -// return 0; -// } - - @Override - public int onPasv() { - clearState(); - proxyConnector = Globals.getProxyConnector(); - if(proxyConnector == null) { - myLog.l(Log.INFO, "Unexpected null proxyConnector in onPasv"); - clearState(); - return 0; - } - ProxyDataSocketInfo info = proxyConnector.pasvListen(); - if(info == null) { - myLog.l(Log.INFO, "Null ProxyDataSocketInfo"); - clearState(); - return 0; - } - socket = info.getSocket(); - proxyListenPort = info.getRemotePublicPort(); - return proxyListenPort; - } - - @Override - public boolean onPort(InetAddress dest, int port) { - clearState(); - proxyConnector = Globals.getProxyConnector(); - this.clientAddress = dest; - this.clientPort = port; - myLog.d("ProxyDataSocketFactory client port settings stored"); - return true; - } - - /** - * When the it's time for the SessionThread to actually begin PASV - * data transfer with the client, it will call this function to get - * a valid socket. The socket will have been created earlier with - * a call to onPasv(). The result of calling onTransfer() will be - * to cause the proxy to accept the incoming connection from the FTP - * client and start proxying back to us (the FTP server). The socket - * can then be handed back to the SessionThread which can use it as - * if it were directly connected to the client. - */ - @Override - public Socket onTransfer() { - if(proxyConnector == null) { - myLog.w("Unexpected null proxyConnector in onTransfer"); - return null; - } - - if(socket == null) { - // We are in PORT mode (not PASV mode) - if(proxyConnector == null) { - myLog.l(Log.INFO, "Unexpected null proxyConnector in onTransfer"); - return null; - } - // May return null, that's fine. ProxyConnector will log errors. - socket = proxyConnector.dataPortConnect(clientAddress, clientPort); - return socket; - } else { - // We are in PASV mode (not PORT mode) - if(proxyConnector.pasvAccept(socket)) { - return socket; - } else { - myLog.w("proxyConnector pasvAccept failed"); - return null; - } - } - } - - @Override - public void reportTraffic(long bytes) { - ProxyConnector pc = Globals.getProxyConnector(); - if(pc == null) { - myLog.d("Can't report traffic, null ProxyConnector"); - } else { - pc.incrementProxyUsage(bytes); - } - } -} +/* +Copyright 2009 David Revell + +This file is part of SwiFTP. + +SwiFTP is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +SwiFTP is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with SwiFTP. If not, see . +*/ + +package org.swiftp.server; + +import java.net.InetAddress; +import java.net.Socket; + +import org.swiftp.Globals; + +import android.util.Log; + +/** + * @author david + * + */ +public class ProxyDataSocketFactory extends DataSocketFactory { + /** + * Implements data socket connections that go through our proxy server + * out on the net. The proxy sits between the FTP client and us, the server. + * We have to build in some coordination between the server and proxy in order + * for data sockets to be handled properly. + * + * When we receive a "PASV" command from a client, we have to request that the + * proxy server open a port, accept a connection, and proxy all data on that + * socket between ourself and the FTP client. + * + * When we receive a PORT command, we store the client's connection info, + * and when it's time to being transferring data, we request that the proxy + * make a connection to the client's IP & port and then proxy all data between + * ourself and the FTP client. + */ + + private Socket socket; + private int proxyListenPort; + ProxyConnector proxyConnector; + InetAddress clientAddress; + int clientPort; + + public ProxyDataSocketFactory() { + clearState(); + } + + private void clearState() { + if(socket != null) { + try { + socket.close(); + } catch (Exception e) {} + } + socket = null; + proxyConnector = null; + clientAddress = null; + proxyListenPort = 0; + clientPort = 0; + } + + @Override + public InetAddress getPasvIp() { + ProxyConnector pc = Globals.getProxyConnector(); + if(pc == null) { + return null; + } + return pc.getProxyIp(); + } + +// public int getPortNumber() { +// if(socket == ) +// return 0; +// } + + @Override + public int onPasv() { + clearState(); + proxyConnector = Globals.getProxyConnector(); + if(proxyConnector == null) { + myLog.l(Log.INFO, "Unexpected null proxyConnector in onPasv"); + clearState(); + return 0; + } + ProxyDataSocketInfo info = proxyConnector.pasvListen(); + if(info == null) { + myLog.l(Log.INFO, "Null ProxyDataSocketInfo"); + clearState(); + return 0; + } + socket = info.getSocket(); + proxyListenPort = info.getRemotePublicPort(); + return proxyListenPort; + } + + @Override + public boolean onPort(InetAddress dest, int port) { + clearState(); + proxyConnector = Globals.getProxyConnector(); + this.clientAddress = dest; + this.clientPort = port; + myLog.d("ProxyDataSocketFactory client port settings stored"); + return true; + } + + /** + * When the it's time for the SessionThread to actually begin PASV + * data transfer with the client, it will call this function to get + * a valid socket. The socket will have been created earlier with + * a call to onPasv(). The result of calling onTransfer() will be + * to cause the proxy to accept the incoming connection from the FTP + * client and start proxying back to us (the FTP server). The socket + * can then be handed back to the SessionThread which can use it as + * if it were directly connected to the client. + */ + @Override + public Socket onTransfer() { + if(proxyConnector == null) { + myLog.w("Unexpected null proxyConnector in onTransfer"); + return null; + } + + if(socket == null) { + // We are in PORT mode (not PASV mode) + if(proxyConnector == null) { + myLog.l(Log.INFO, "Unexpected null proxyConnector in onTransfer"); + return null; + } + // May return null, that's fine. ProxyConnector will log errors. + socket = proxyConnector.dataPortConnect(clientAddress, clientPort); + return socket; + } else { + // We are in PASV mode (not PORT mode) + if(proxyConnector.pasvAccept(socket)) { + return socket; + } else { + myLog.w("proxyConnector pasvAccept failed"); + return null; + } + } + } + + @Override + public void reportTraffic(long bytes) { + ProxyConnector pc = Globals.getProxyConnector(); + if(pc == null) { + myLog.d("Can't report traffic, null ProxyConnector"); + } else { + pc.incrementProxyUsage(bytes); + } + } +} diff --git a/src/main/java/org/swiftp/server/ProxyDataSocketInfo.java b/src/main/java/org/swiftp/server/ProxyDataSocketInfo.java index e1ffe4feddfb608e91c17a4319fb40e06e1aa007..c1280c9078b95f22148112f277fe0b9ab8b8a23e 100644 --- a/src/main/java/org/swiftp/server/ProxyDataSocketInfo.java +++ b/src/main/java/org/swiftp/server/ProxyDataSocketInfo.java @@ -1,49 +1,49 @@ -/* -Copyright 2009 David Revell - -This file is part of SwiFTP. - -SwiFTP is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -SwiFTP is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with SwiFTP. If not, see . -*/ - -package org.swiftp.server; - -import java.net.Socket; - -public class ProxyDataSocketInfo extends Socket { - private int remotePublicPort; - private Socket socket; - - public Socket getSocket() { - return socket; - } - - public void setSocket(Socket socket) { - this.socket = socket; - } - - public ProxyDataSocketInfo(Socket socket, int remotePublicPort) { - this.remotePublicPort = remotePublicPort; - this.socket = socket; - } - - public int getRemotePublicPort() { - return remotePublicPort; - } - - public void setRemotePublicPort(int remotePublicPort) { - this.remotePublicPort = remotePublicPort; - } - -} +/* +Copyright 2009 David Revell + +This file is part of SwiFTP. + +SwiFTP is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +SwiFTP is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with SwiFTP. If not, see . +*/ + +package org.swiftp.server; + +import java.net.Socket; + +public class ProxyDataSocketInfo extends Socket { + private int remotePublicPort; + private Socket socket; + + public Socket getSocket() { + return socket; + } + + public void setSocket(Socket socket) { + this.socket = socket; + } + + public ProxyDataSocketInfo(Socket socket, int remotePublicPort) { + this.remotePublicPort = remotePublicPort; + this.socket = socket; + } + + public int getRemotePublicPort() { + return remotePublicPort; + } + + public void setRemotePublicPort(int remotePublicPort) { + this.remotePublicPort = remotePublicPort; + } + +} diff --git a/src/main/java/org/swiftp/server/SessionThread.java b/src/main/java/org/swiftp/server/SessionThread.java index 8343771d1df8f4b3e371449b6419a2c0f043362a..5fcb733680ef82d00720d900bcd68389aa8b9d13 100644 --- a/src/main/java/org/swiftp/server/SessionThread.java +++ b/src/main/java/org/swiftp/server/SessionThread.java @@ -1,445 +1,445 @@ -/* -Copyright 2009 David Revell - -This file is part of SwiFTP. - -SwiFTP is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -SwiFTP is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with SwiFTP. If not, see . - */ - -package org.swiftp.server; - -import java.io.BufferedOutputStream; -import java.io.BufferedReader; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.io.UnsupportedEncodingException; -import java.net.InetAddress; -import java.net.Socket; -import java.nio.ByteBuffer; - -import org.swiftp.Defaults; -import org.swiftp.FTPServerService; -import org.swiftp.Globals; -import org.swiftp.MyLog; -import org.swiftp.Util; - -import android.util.Log; - -public class SessionThread extends Thread { - protected boolean shouldExit = false; - protected Socket cmdSocket; - protected MyLog myLog = new MyLog(getClass().getName()); - protected ByteBuffer buffer = ByteBuffer.allocate(Defaults - .getInputBufferSize()); - protected boolean pasvMode = false; - protected boolean binaryMode = false; - protected Account account = new Account(); - protected boolean authenticated = false; - protected File workingDir = Globals.getChrootDir(); - // protected ServerSocket dataServerSocket = null; - protected Socket dataSocket = null; - // protected FTPServerService service; - protected File renameFrom = null; - // protected InetAddress outDataDest = null; - // protected int outDataPort = 20; // 20 is the default ftp-data port - protected DataSocketFactory dataSocketFactory; - OutputStream dataOutputStream = null; - private boolean sendWelcomeBanner; - protected String encoding = Defaults.SESSION_ENCODING; - protected Source source; - int authFails = 0; - - public enum Source {LOCAL, PROXY}; // where did this connection come from? - public static int MAX_AUTH_FAILS = 3; - /** - * Used when we get a PORT command to open up an outgoing socket. - * - * @return - */ - // public void setPortSocket(InetAddress dest, int port) { - // myLog.l(Log.DEBUG, "Setting PORT dest to " + - // dest.getHostAddress() + " port " + port); - // outDataDest = dest; - // outDataPort = port; - // } - /** - * Sends a string over the already-established data socket - * - * @param string - * @return Whether the send completed successfully - */ - public boolean sendViaDataSocket(String string) { - try { - byte[] bytes = string.getBytes(encoding); - myLog.d("Using data connection encoding: " + encoding); - return sendViaDataSocket(bytes, bytes.length); - } catch (UnsupportedEncodingException e) { - myLog.l(Log.ERROR, "Unsupported encoding for data socket send"); - return false; - } - } - - public boolean sendViaDataSocket(byte[] bytes, int len) { - return sendViaDataSocket(bytes, 0, len); - } - - /** - * Sends a byte array over the already-established data socket - * - * @param bytes - * @param len - * @return - */ - public boolean sendViaDataSocket(byte[] bytes, int start, int len) { - - if (dataOutputStream == null) { - myLog.l(Log.INFO, "Can't send via null dataOutputStream"); - return false; - } - if (len == 0) { - return true; // this isn't an "error" - } - try { - dataOutputStream.write(bytes, start, len); - } catch (IOException e) { - myLog.l(Log.INFO, "Couldn't write output stream for data socket"); - myLog.l(Log.INFO, e.toString()); - return false; - } - dataSocketFactory.reportTraffic(len); - return true; - } - - /** - * Received some bytes from the data socket, which is assumed to already be - * connected. The bytes are placed in the given array, and the number of - * bytes successfully read is returned. - * - * @param bytes - * Where to place the input bytes - * @return >0 if successful which is the number of bytes read, -1 if no - * bytes remain to be read, -2 if the data socket was not connected, - * 0 if there was a read error - */ - public int receiveFromDataSocket(byte[] buf) { - int bytesRead; - - if (dataSocket == null) { - myLog.l(Log.INFO, "Can't receive from null dataSocket"); - return -2; - } - if (!dataSocket.isConnected()) { - myLog.l(Log.INFO, "Can't receive from unconnected socket"); - return -2; - } - InputStream in; - try { - in = dataSocket.getInputStream(); - // If the read returns 0 bytes, the stream is not yet - // closed, but we just want to read again. - while ((bytesRead = in.read(buf, 0, buf.length)) == 0) { - } - if (bytesRead == -1) { - // If InputStream.read returns -1, there are no bytes - // remaining, so we return 0. - return -1; - } - } catch (IOException e) { - myLog.l(Log.INFO, "Error reading data socket"); - return 0; - } - dataSocketFactory.reportTraffic(bytesRead); - return bytesRead; - } - - /** - * Called when we receive a PASV command. - * - * @return Whether the necessary initialization was successful. - */ - public int onPasv() { - return dataSocketFactory.onPasv(); - } - - /** - * Called when we receive a PORT command. - * - * @return Whether the necessary initialization was successful. - */ - public boolean onPort(InetAddress dest, int port) { - return dataSocketFactory.onPort(dest, port); - } - - public InetAddress getDataSocketPasvIp() { - // When the client sends PASV, our reply will contain the address and port - // of the data connection that the client should connect to. For this purpose - // we always use the same IP address that the command socket is using. - return cmdSocket.getLocalAddress(); - - // The old code, not totally correct. - // return dataSocketFactory.getPasvIp(); - } - - // public int getDataSocketPort() { - // return dataSocketFactory.getPortNumber(); - // } - - /** - * Will be called by (e.g.) CmdSTOR, CmdRETR, CmdLIST, etc. when they are - * about to start actually doing IO over the data socket. - * - * @return - */ - public boolean startUsingDataSocket() { - try { - dataSocket = dataSocketFactory.onTransfer(); - if (dataSocket == null) { - myLog.l(Log.INFO, - "dataSocketFactory.onTransfer() returned null"); - return false; - } - dataOutputStream = dataSocket.getOutputStream(); - return true; - } catch (IOException e) { - myLog.l(Log.INFO, - "IOException getting OutputStream for data socket"); - dataSocket = null; - return false; - } - } - - public void quit() { - myLog.d("SessionThread told to quit"); - closeSocket(); - } - - public void closeDataSocket() { - myLog.l(Log.DEBUG, "Closing data socket"); - if (dataOutputStream != null) { - try { - dataOutputStream.close(); - } catch (IOException e) { - } - dataOutputStream = null; - } - if (dataSocket != null) { - try { - dataSocket.close(); - } catch (IOException e) { - } - } - dataSocket = null; - } - - protected InetAddress getLocalAddress() { - return cmdSocket.getLocalAddress(); - } - - static int numNulls = 0; - @Override - public void run() { - myLog.l(Log.INFO, "SessionThread started"); - - if(sendWelcomeBanner) { - writeString("220 SwiFTP " + Util.getVersion() + " ready\r\n"); - } - // Main loop: read an incoming line and process it - try { - BufferedReader in = new BufferedReader(new InputStreamReader(cmdSocket - .getInputStream()), 8192); // use 8k buffer - while (true) { - String line; - line = in.readLine(); // will accept \r\n or \n for terminator - if (line != null) { - FTPServerService.writeMonitor(true, line); - myLog.l(Log.DEBUG, "Received line from client: " + line); - FtpCmd.dispatchCommand(this, line); - } else { - myLog.i("readLine gave null, quitting"); - break; - } - } - } catch (IOException e) { - myLog.l(Log.INFO, "Connection was dropped"); - } - closeSocket(); - } - - /** - * A static method to check the equality of two byte arrays, but only up to - * a given length. - */ - public static boolean compareLen(byte[] array1, byte[] array2, int len) { - for (int i = 0; i < len; i++) { - if (array1[i] != array2[i]) { - return false; - } - } - return true; - } - - public void closeSocket() { - if (cmdSocket == null) { - return; - } - try { - cmdSocket.close(); - } catch (IOException e) {} - } - - public void writeBytes(byte[] bytes) { - try { - // TODO: do we really want to do all of this on each write? Why? - BufferedOutputStream out = new BufferedOutputStream(cmdSocket - .getOutputStream(), Defaults.dataChunkSize); - out.write(bytes); - out.flush(); - dataSocketFactory.reportTraffic(bytes.length); - } catch (IOException e) { - myLog.l(Log.INFO, "Exception writing socket"); - closeSocket(); - return; - } - } - - public void writeString(String str) { - FTPServerService.writeMonitor(false, str); - byte[] strBytes; - try { - strBytes = str.getBytes(encoding); - } catch (UnsupportedEncodingException e) { - myLog.e("Unsupported encoding: " + encoding); - strBytes = str.getBytes(); - } - writeBytes(strBytes); - } - - protected Socket getSocket() { - return cmdSocket; - } - - public Account getAccount() { - return account; - } - - public void setAccount(Account account) { - this.account = account; - } - - public boolean isPasvMode() { - return pasvMode; - } - - public SessionThread(Socket socket, DataSocketFactory dataSocketFactory, - Source source) { - this.cmdSocket = socket; - this.source = source; - this.dataSocketFactory = dataSocketFactory; - if(source == Source.LOCAL) { - this.sendWelcomeBanner = true; - } else { - this.sendWelcomeBanner = false; - } - } - - static public ByteBuffer stringToBB(String s) { - return ByteBuffer.wrap(s.getBytes()); - } - - public boolean isBinaryMode() { - return binaryMode; - } - - public void setBinaryMode(boolean binaryMode) { - this.binaryMode = binaryMode; - } - - public boolean isAuthenticated() { - return authenticated; - } - - public void authAttempt(boolean authenticated) { - if (authenticated) { - myLog.l(Log.INFO, "Authentication complete"); - this.authenticated = true; - } else { - // There was a failed auth attempt. If the connection came - // via the proxy, then drop it now. The client can't try again - // successfully because it doesn't know its real username. What - // it knows is prefix_username. - if(source == Source.PROXY) { - quit(); - } else { - authFails++; - myLog.i("Auth failed: " + authFails + "/" + MAX_AUTH_FAILS); - } - if(authFails > MAX_AUTH_FAILS) { - myLog.i("Too many auth fails, quitting session"); - quit(); - } - } - - } - - public File getWorkingDir() { - return workingDir; - } - - public void setWorkingDir(File workingDir) { - try { - this.workingDir = workingDir.getCanonicalFile().getAbsoluteFile(); - } catch (IOException e) { - myLog.l(Log.INFO, "SessionThread canonical error"); - } - } - - /* - * public FTPServerService getService() { return service; } - * - * public void setService(FTPServerService service) { this.service = - * service; } - */ - - public Socket getDataSocket() { - return dataSocket; - } - - public void setDataSocket(Socket dataSocket) { - this.dataSocket = dataSocket; - } - - // public ServerSocket getServerSocket() { - // return dataServerSocket; - // } - - public File getRenameFrom() { - return renameFrom; - } - - public void setRenameFrom(File renameFrom) { - this.renameFrom = renameFrom; - } - - public String getEncoding() { - return encoding; - } - - public void setEncoding(String encoding) { - this.encoding = encoding; - } - -} +/* +Copyright 2009 David Revell + +This file is part of SwiFTP. + +SwiFTP is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +SwiFTP is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with SwiFTP. If not, see . + */ + +package org.swiftp.server; + +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.net.InetAddress; +import java.net.Socket; +import java.nio.ByteBuffer; + +import org.swiftp.Defaults; +import org.swiftp.FTPServerService; +import org.swiftp.Globals; +import org.swiftp.MyLog; +import org.swiftp.Util; + +import android.util.Log; + +public class SessionThread extends Thread { + protected boolean shouldExit = false; + protected Socket cmdSocket; + protected MyLog myLog = new MyLog(getClass().getName()); + protected ByteBuffer buffer = ByteBuffer.allocate(Defaults + .getInputBufferSize()); + protected boolean pasvMode = false; + protected boolean binaryMode = false; + protected Account account = new Account(); + protected boolean authenticated = false; + protected File workingDir = Globals.getChrootDir(); + // protected ServerSocket dataServerSocket = null; + protected Socket dataSocket = null; + // protected FTPServerService service; + protected File renameFrom = null; + // protected InetAddress outDataDest = null; + // protected int outDataPort = 20; // 20 is the default ftp-data port + protected DataSocketFactory dataSocketFactory; + OutputStream dataOutputStream = null; + private boolean sendWelcomeBanner; + protected String encoding = Defaults.SESSION_ENCODING; + protected Source source; + int authFails = 0; + + public enum Source {LOCAL, PROXY}; // where did this connection come from? + public static int MAX_AUTH_FAILS = 3; + /** + * Used when we get a PORT command to open up an outgoing socket. + * + * @return + */ + // public void setPortSocket(InetAddress dest, int port) { + // myLog.l(Log.DEBUG, "Setting PORT dest to " + + // dest.getHostAddress() + " port " + port); + // outDataDest = dest; + // outDataPort = port; + // } + /** + * Sends a string over the already-established data socket + * + * @param string + * @return Whether the send completed successfully + */ + public boolean sendViaDataSocket(String string) { + try { + byte[] bytes = string.getBytes(encoding); + myLog.d("Using data connection encoding: " + encoding); + return sendViaDataSocket(bytes, bytes.length); + } catch (UnsupportedEncodingException e) { + myLog.l(Log.ERROR, "Unsupported encoding for data socket send"); + return false; + } + } + + public boolean sendViaDataSocket(byte[] bytes, int len) { + return sendViaDataSocket(bytes, 0, len); + } + + /** + * Sends a byte array over the already-established data socket + * + * @param bytes + * @param len + * @return + */ + public boolean sendViaDataSocket(byte[] bytes, int start, int len) { + + if (dataOutputStream == null) { + myLog.l(Log.INFO, "Can't send via null dataOutputStream"); + return false; + } + if (len == 0) { + return true; // this isn't an "error" + } + try { + dataOutputStream.write(bytes, start, len); + } catch (IOException e) { + myLog.l(Log.INFO, "Couldn't write output stream for data socket"); + myLog.l(Log.INFO, e.toString()); + return false; + } + dataSocketFactory.reportTraffic(len); + return true; + } + + /** + * Received some bytes from the data socket, which is assumed to already be + * connected. The bytes are placed in the given array, and the number of + * bytes successfully read is returned. + * + * @param bytes + * Where to place the input bytes + * @return >0 if successful which is the number of bytes read, -1 if no + * bytes remain to be read, -2 if the data socket was not connected, + * 0 if there was a read error + */ + public int receiveFromDataSocket(byte[] buf) { + int bytesRead; + + if (dataSocket == null) { + myLog.l(Log.INFO, "Can't receive from null dataSocket"); + return -2; + } + if (!dataSocket.isConnected()) { + myLog.l(Log.INFO, "Can't receive from unconnected socket"); + return -2; + } + InputStream in; + try { + in = dataSocket.getInputStream(); + // If the read returns 0 bytes, the stream is not yet + // closed, but we just want to read again. + while ((bytesRead = in.read(buf, 0, buf.length)) == 0) { + } + if (bytesRead == -1) { + // If InputStream.read returns -1, there are no bytes + // remaining, so we return 0. + return -1; + } + } catch (IOException e) { + myLog.l(Log.INFO, "Error reading data socket"); + return 0; + } + dataSocketFactory.reportTraffic(bytesRead); + return bytesRead; + } + + /** + * Called when we receive a PASV command. + * + * @return Whether the necessary initialization was successful. + */ + public int onPasv() { + return dataSocketFactory.onPasv(); + } + + /** + * Called when we receive a PORT command. + * + * @return Whether the necessary initialization was successful. + */ + public boolean onPort(InetAddress dest, int port) { + return dataSocketFactory.onPort(dest, port); + } + + public InetAddress getDataSocketPasvIp() { + // When the client sends PASV, our reply will contain the address and port + // of the data connection that the client should connect to. For this purpose + // we always use the same IP address that the command socket is using. + return cmdSocket.getLocalAddress(); + + // The old code, not totally correct. + // return dataSocketFactory.getPasvIp(); + } + + // public int getDataSocketPort() { + // return dataSocketFactory.getPortNumber(); + // } + + /** + * Will be called by (e.g.) CmdSTOR, CmdRETR, CmdLIST, etc. when they are + * about to start actually doing IO over the data socket. + * + * @return + */ + public boolean startUsingDataSocket() { + try { + dataSocket = dataSocketFactory.onTransfer(); + if (dataSocket == null) { + myLog.l(Log.INFO, + "dataSocketFactory.onTransfer() returned null"); + return false; + } + dataOutputStream = dataSocket.getOutputStream(); + return true; + } catch (IOException e) { + myLog.l(Log.INFO, + "IOException getting OutputStream for data socket"); + dataSocket = null; + return false; + } + } + + public void quit() { + myLog.d("SessionThread told to quit"); + closeSocket(); + } + + public void closeDataSocket() { + myLog.l(Log.DEBUG, "Closing data socket"); + if (dataOutputStream != null) { + try { + dataOutputStream.close(); + } catch (IOException e) { + } + dataOutputStream = null; + } + if (dataSocket != null) { + try { + dataSocket.close(); + } catch (IOException e) { + } + } + dataSocket = null; + } + + protected InetAddress getLocalAddress() { + return cmdSocket.getLocalAddress(); + } + + static int numNulls = 0; + @Override + public void run() { + myLog.l(Log.INFO, "SessionThread started"); + + if(sendWelcomeBanner) { + writeString("220 SwiFTP " + Util.getVersion() + " ready\r\n"); + } + // Main loop: read an incoming line and process it + try { + BufferedReader in = new BufferedReader(new InputStreamReader(cmdSocket + .getInputStream()), 8192); // use 8k buffer + while (true) { + String line; + line = in.readLine(); // will accept \r\n or \n for terminator + if (line != null) { + FTPServerService.writeMonitor(true, line); + myLog.l(Log.DEBUG, "Received line from client: " + line); + FtpCmd.dispatchCommand(this, line); + } else { + myLog.i("readLine gave null, quitting"); + break; + } + } + } catch (IOException e) { + myLog.l(Log.INFO, "Connection was dropped"); + } + closeSocket(); + } + + /** + * A static method to check the equality of two byte arrays, but only up to + * a given length. + */ + public static boolean compareLen(byte[] array1, byte[] array2, int len) { + for (int i = 0; i < len; i++) { + if (array1[i] != array2[i]) { + return false; + } + } + return true; + } + + public void closeSocket() { + if (cmdSocket == null) { + return; + } + try { + cmdSocket.close(); + } catch (IOException e) {} + } + + public void writeBytes(byte[] bytes) { + try { + // TODO: do we really want to do all of this on each write? Why? + BufferedOutputStream out = new BufferedOutputStream(cmdSocket + .getOutputStream(), Defaults.dataChunkSize); + out.write(bytes); + out.flush(); + dataSocketFactory.reportTraffic(bytes.length); + } catch (IOException e) { + myLog.l(Log.INFO, "Exception writing socket"); + closeSocket(); + return; + } + } + + public void writeString(String str) { + FTPServerService.writeMonitor(false, str); + byte[] strBytes; + try { + strBytes = str.getBytes(encoding); + } catch (UnsupportedEncodingException e) { + myLog.e("Unsupported encoding: " + encoding); + strBytes = str.getBytes(); + } + writeBytes(strBytes); + } + + protected Socket getSocket() { + return cmdSocket; + } + + public Account getAccount() { + return account; + } + + public void setAccount(Account account) { + this.account = account; + } + + public boolean isPasvMode() { + return pasvMode; + } + + public SessionThread(Socket socket, DataSocketFactory dataSocketFactory, + Source source) { + this.cmdSocket = socket; + this.source = source; + this.dataSocketFactory = dataSocketFactory; + if(source == Source.LOCAL) { + this.sendWelcomeBanner = true; + } else { + this.sendWelcomeBanner = false; + } + } + + static public ByteBuffer stringToBB(String s) { + return ByteBuffer.wrap(s.getBytes()); + } + + public boolean isBinaryMode() { + return binaryMode; + } + + public void setBinaryMode(boolean binaryMode) { + this.binaryMode = binaryMode; + } + + public boolean isAuthenticated() { + return authenticated; + } + + public void authAttempt(boolean authenticated) { + if (authenticated) { + myLog.l(Log.INFO, "Authentication complete"); + this.authenticated = true; + } else { + // There was a failed auth attempt. If the connection came + // via the proxy, then drop it now. The client can't try again + // successfully because it doesn't know its real username. What + // it knows is prefix_username. + if(source == Source.PROXY) { + quit(); + } else { + authFails++; + myLog.i("Auth failed: " + authFails + "/" + MAX_AUTH_FAILS); + } + if(authFails > MAX_AUTH_FAILS) { + myLog.i("Too many auth fails, quitting session"); + quit(); + } + } + + } + + public File getWorkingDir() { + return workingDir; + } + + public void setWorkingDir(File workingDir) { + try { + this.workingDir = workingDir.getCanonicalFile().getAbsoluteFile(); + } catch (IOException e) { + myLog.l(Log.INFO, "SessionThread canonical error"); + } + } + + /* + * public FTPServerService getService() { return service; } + * + * public void setService(FTPServerService service) { this.service = + * service; } + */ + + public Socket getDataSocket() { + return dataSocket; + } + + public void setDataSocket(Socket dataSocket) { + this.dataSocket = dataSocket; + } + + // public ServerSocket getServerSocket() { + // return dataServerSocket; + // } + + public File getRenameFrom() { + return renameFrom; + } + + public void setRenameFrom(File renameFrom) { + this.renameFrom = renameFrom; + } + + public String getEncoding() { + return encoding; + } + + public void setEncoding(String encoding) { + this.encoding = encoding; + } + +} diff --git a/src/main/java/org/swiftp/server/TcpListener.java b/src/main/java/org/swiftp/server/TcpListener.java index 87680c592d6a58635662d9e9d7993e4b20a0cd76..7f199104ff9081719d0617c62ade4471bdb5b421 100644 --- a/src/main/java/org/swiftp/server/TcpListener.java +++ b/src/main/java/org/swiftp/server/TcpListener.java @@ -1,67 +1,67 @@ -/* -Copyright 2009 David Revell - -This file is part of SwiFTP. - -SwiFTP is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -SwiFTP is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with SwiFTP. If not, see . -*/ - -package org.swiftp.server; - -import java.net.ServerSocket; -import java.net.Socket; - -import org.swiftp.FTPServerService; -import org.swiftp.MyLog; - -import android.util.Log; - -public class TcpListener extends Thread { - ServerSocket listenSocket; - FTPServerService ftpServerService; - MyLog myLog = new MyLog(getClass().getName()); - - public TcpListener(ServerSocket listenSocket, FTPServerService ftpServerService) { - this.listenSocket = listenSocket; - this.ftpServerService = ftpServerService; - } - - public void quit() { - try { - listenSocket.close(); // if the TcpListener thread is blocked on accept, - // closing the socket will raise an exception - } catch (Exception e) { - myLog.l(Log.DEBUG, "Exception closing TcpListener listenSocket"); - } - } - - @Override - public void run() { - try { - while(true) { - - Socket clientSocket = listenSocket.accept(); - myLog.l(Log.INFO, "New connection, spawned thread"); - SessionThread newSession = new SessionThread(clientSocket, - new NormalDataSocketFactory(), - SessionThread.Source.LOCAL); - newSession.start(); - ftpServerService.registerSessionThread(newSession); - } - } catch (Exception e) { - myLog.l(Log.DEBUG, "Exception in TcpListener"); - } - } -} - +/* +Copyright 2009 David Revell + +This file is part of SwiFTP. + +SwiFTP is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +SwiFTP is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with SwiFTP. If not, see . +*/ + +package org.swiftp.server; + +import java.net.ServerSocket; +import java.net.Socket; + +import org.swiftp.FTPServerService; +import org.swiftp.MyLog; + +import android.util.Log; + +public class TcpListener extends Thread { + ServerSocket listenSocket; + FTPServerService ftpServerService; + MyLog myLog = new MyLog(getClass().getName()); + + public TcpListener(ServerSocket listenSocket, FTPServerService ftpServerService) { + this.listenSocket = listenSocket; + this.ftpServerService = ftpServerService; + } + + public void quit() { + try { + listenSocket.close(); // if the TcpListener thread is blocked on accept, + // closing the socket will raise an exception + } catch (Exception e) { + myLog.l(Log.DEBUG, "Exception closing TcpListener listenSocket"); + } + } + + @Override + public void run() { + try { + while(true) { + + Socket clientSocket = listenSocket.accept(); + myLog.l(Log.INFO, "New connection, spawned thread"); + SessionThread newSession = new SessionThread(clientSocket, + new NormalDataSocketFactory(), + SessionThread.Source.LOCAL); + newSession.start(); + ftpServerService.registerSessionThread(newSession); + } + } catch (Exception e) { + myLog.l(Log.DEBUG, "Exception in TcpListener"); + } + } +} + diff --git a/src/main/java/util/DocumentUtil.java b/src/main/java/util/DocumentUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..abfd1f54233728bfe11fb2e849abc4849e942a1d --- /dev/null +++ b/src/main/java/util/DocumentUtil.java @@ -0,0 +1,520 @@ +package util; +//by 乘着船 at 2021-2023 + +import android.annotation.TargetApi; +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.net.Uri; +import android.os.Build; +import android.os.Environment; +import android.os.storage.StorageManager; +import android.os.storage.StorageVolume; +import android.preference.PreferenceManager; +import android.support.v4.provider.DocumentFile; +import android.util.Log; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class DocumentUtil { + + private static final String TAG = DocumentUtil.class.getSimpleName(); + + public static final int OPEN_DOCUMENT_TREE_CODE = 8000; + public static final int MAX_BUFFER_SIZE = 5242880;//max buffer size 5MB + + public static final String SDCARD = Environment.getExternalStorageDirectory().getAbsolutePath(); + public static final String SDCARD_PATH = SDCARD + "/"; + public static final String CONTENT_PRF = "content://"; + public static final String[] SDCARD_CONTENT = new String[]{ + CONTENT_PRF + "com.android.externalstorage.documents/tree/primary%3A", "/document/primary%3A"}; + public static final String ANDROID_PATH = SDCARD_PATH + "Android/"; + public static final String[] ANDROID_CONTENT = new String[]{ + SDCARD_CONTENT[0] + "Android%2F", SDCARD_CONTENT[1] + "Android%2F"}; + + public static final int ANDROID_SAVE_INTENT = + Intent.FLAG_GRANT_READ_URI_PERMISSION | + Intent.FLAG_GRANT_WRITE_URI_PERMISSION ; + public static final int ANDROID_OPEN_INTENT = + Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | + Intent.FLAG_GRANT_PREFIX_URI_PERMISSION | + ANDROID_SAVE_INTENT ; + + public static List sExtSdCardPaths = new ArrayList<>(); + + private static String requestRootPath = null; + + private DocumentUtil() { + + } + + public static void cleanCache() { + sExtSdCardPaths.clear(); + } + + /** + * Get a list of external SD card paths. (Kitkat or higher.) + * + * @return A list of external SD card paths. + */ + @TargetApi(Build.VERSION_CODES.KITKAT) + private static String[] getExtSdCardPaths(Context context) { + if (sExtSdCardPaths.size() > 0) { + return sExtSdCardPaths.toArray(new String[0]); + } + for (File file : context.getExternalFilesDirs("external")) { + if (file != null && !file.equals(context.getExternalFilesDir("external"))) { + int index = file.getAbsolutePath().lastIndexOf("/Android/data"); + if (index < 0) { + Log.d(TAG, "Unexpected external file dir: " + file.getAbsolutePath()); + } else { + String path = file.getAbsolutePath().substring(0, index); + try { + path = new File(path).getCanonicalPath(); + } catch (IOException e) { + // Keep non-canonical path. + } + sExtSdCardPaths.add(path); + } + } + } + if (sExtSdCardPaths.isEmpty()) sExtSdCardPaths.add(SDCARD); + return sExtSdCardPaths.toArray(new String[0]); + } + + /** + * Determine the main folder of the external SD card containing the given file. + * + * @param file the file. + * @return The main folder of the external SD card containing this file, if the file is on an SD + * card. Otherwise, + * null is returned. + */ + @TargetApi(Build.VERSION_CODES.KITKAT) + private static String getExtSdCardFolder(final File file, Context context) { + String[] extSdPaths = getExtSdCardPaths(context); + try { + for (String extSdPath : extSdPaths) { + if (file.getCanonicalPath().startsWith(extSdPath)) { + return extSdPath; + } + } + } catch (IOException e) { + return null; + } + return null; + } + + /** + * Determine if a file is on external sd card. (Kitkat or higher.) + * + * @param file The file. + * @return true if on external sd card. + */ + @TargetApi(Build.VERSION_CODES.KITKAT) + public static boolean isOnExtSdCard(final File file, Context c) { + return getExtSdCardFolder(file, c) != null; + } + + /** + * Get a DocumentFile corresponding to the given file (for writing on ExtSdCard on Android 5). + * If the file is not + * existing, it is created. + * + * @param file The file. + * @param isDirectory true/false/null + * true/false - flag indicating if the file should be a directory , + * if file not exist, it will be create . + * null - do not know the file is a directory , + * if file not exist, it will not be create . + * @return The DocumentFile + */ + public static DocumentFile getDocumentFile( + final File file, final Boolean isDirectory, Context context) { + + String baseFolder = getExtSdCardFolder(file, context); + boolean originalDirectory = false; + if (baseFolder == null) { + return null; + } + + String relativePath = null; + try { + String fullPath = file.getCanonicalPath(); + if (!baseFolder.equals(fullPath)) { + relativePath = fullPath.substring(baseFolder.length() + 1); + } else { + originalDirectory = true; + } + } catch (IOException e) { + return null; + } catch (Exception f) { + originalDirectory = true; + //continue + } + String as = PreferenceManager.getDefaultSharedPreferences(context).getString(baseFolder, + null); + + Uri treeUri = null; + if (as != null) treeUri = Uri.parse(as); + if (treeUri == null) { + return null; + } + + // start with root of SD card and then parse through document tree. + DocumentFile document = DocumentFile.fromTreeUri(context, treeUri); + if (originalDirectory) return document; + String[] parts = relativePath.split("/"); + for (int i = 0; i < parts.length; i++) { + DocumentFile nextDocument = document.findFile(parts[i]); + + if (nextDocument == null) { + if(isDirectory == null) return null; + if ((i < parts.length - 1) || isDirectory) { + nextDocument = document.createDirectory(parts[i]); + } else { + nextDocument = document.createFile("image", parts[i]); + } + } + document = nextDocument; + } + + return document; + } + + public static boolean mkdirs(Context context, File dir) { + boolean res = dir.mkdirs(); + if (!res) { + if (isOnExtSdCard(dir, context)) { + DocumentFile documentFile = getDocumentFile(dir, true, context); + res = documentFile != null && documentFile.canWrite(); + } + } + return res; + } + + private static boolean FileDelete(File file) { + if (file.isFile()) return file.delete(); + File[] subFiles = file.listFiles(); + if (subFiles==null || subFiles.length==0) + return file.delete(); + boolean ret = true; + for(File subFile:subFiles){ + if (subFile.isDirectory()) ret = FileDelete(subFile) && ret; + else ret = subFile.delete() && ret; + } + return ret && file.delete(); + } + + public static boolean delete(Context context, File file) { + boolean ret = FileDelete(file); + if (ret) return ret; + //if (isOnExtSdCard(file, context)) { + DocumentFile f = getDocumentFile(file, false, context); + if (f != null) { + ret = f.delete(); + } + //} + return ret; + } + + public static boolean canWrite(File file) { + boolean res = file.exists() && file.canWrite(); + + if (!res && !file.exists()) { + try { + if (!file.isDirectory()) { + res = file.createNewFile() && file.delete(); + } else { + res = file.mkdirs() && file.delete(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + return res; + } + + /*public static boolean canWrite(Context context, File file) { + boolean res = canWrite(file); + + if (!res && isOnExtSdCard(file, context)) { + DocumentFile documentFile = getDocumentFile(file, true, context); + res = documentFile != null && documentFile.canWrite(); + } + return res; + }*/ + + private static boolean renameToCross(Context context,File src,File dest) throws Exception { + copy(context,src,dest); + boolean exist = dest.exists(); + if (!exist) { + DocumentFile Dest = getDocumentFile(dest,null,context); + if(Dest!=null && Dest.exists()) + exist = true; + } + delete(context,src); + return exist; + } + + public static boolean renameTo(Context context, File src, File dest) throws Exception { + boolean res = src.renameTo(dest); + if (res) return true; + + if (isOnExtSdCard(dest, context)) { + DocumentFile srcDoc; + if (isOnExtSdCard(src, context)) { + srcDoc = getDocumentFile(src, false, context); + } else { + srcDoc = DocumentFile.fromFile(src); + } + DocumentFile destDoc = getDocumentFile(dest.getParentFile(), true, context); + + if (srcDoc != null && destDoc != null) { + if(!srcDoc.exists()) + throw new FileNotFoundException(src.toString()); + if (Objects.equals(src.getParent(), dest.getParent())) { + DocumentFile DestDoc = getDocumentFile(dest, null, context); + if(DestDoc!=null && DestDoc.exists()){ + if(dest.isDirectory()) + return renameToCross(context,src,dest); + else if(!DestDoc.delete()) + return false; + } + res = srcDoc.renameTo(dest.getName()); + } else return renameToCross(context, src, dest); + } else { + if ((src.exists() || srcDoc!=null) && (dest.canWrite() || destDoc!=null)) + return renameToCross(context, src, dest); + else return false; + } + } else return renameToCross(context, src, dest); + + return res; + } + + public static InputStream getInputStream(Context context, File destFile) { + InputStream in = null; + try { + if (!canWrite(destFile) && isOnExtSdCard(destFile, context)) { + DocumentFile file = getDocumentFile(destFile, false, context); + if (file != null && file.canWrite()) { + in = context.getContentResolver().openInputStream(file.getUri()); + } + } else { + in = new FileInputStream(destFile); + + } + } catch (FileNotFoundException e) { + e.printStackTrace(); + } + return in; + } + + public static OutputStream getOutputStream(Context context, File destFile, boolean append) { + OutputStream out = null; + try { + if (!canWrite(destFile) && isOnExtSdCard(destFile, context)) { + DocumentFile file = getDocumentFile(destFile, false, context); + if (file != null && file.canWrite()) { + String mode; + if(append) + mode = "wa"; + else mode = "wt"; + out = context.getContentResolver().openOutputStream(file.getUri(), mode); + } + } else { + out = new FileOutputStream(destFile, append); + + } + } catch (FileNotFoundException e) { + e.printStackTrace(); + } + return out; + } + + public static OutputStream getOutputStream(Context context, File destFile) { + OutputStream out = null; + try { + if (!canWrite(destFile) && isOnExtSdCard(destFile, context)) { + DocumentFile file = getDocumentFile(destFile, false, context); + if (file != null && file.canWrite()) + out = context.getContentResolver().openOutputStream(file.getUri(),"wt"); + } else { + out = new FileOutputStream(destFile); + + } + } catch (FileNotFoundException e) { + e.printStackTrace(); + } + return out; + } + + public static void saveTreeUri(Context context, String rootPath, Uri uri) { + DocumentFile file = DocumentFile.fromTreeUri(context, uri); + if (file != null && file.canWrite()) { + SharedPreferences perf = PreferenceManager.getDefaultSharedPreferences(context); + perf.edit().putString(rootPath, uri.toString()).apply(); + } else { + Log.e(TAG, "no write permission: " + rootPath); + } + } + + public static boolean checkWritableRootPath(Context context, String rootPath) { + File root = new File(rootPath); + if (!root.canWrite()) { + + if (isOnExtSdCard(root, context)) { + DocumentFile documentFile = getDocumentFile(root, true, context); + return documentFile == null || !documentFile.canWrite(); + } else { + SharedPreferences perf = PreferenceManager.getDefaultSharedPreferences(context); + + String documentUri = perf.getString(rootPath, ""); + + if (documentUri == null || documentUri.isEmpty()) { + return true; + } else { + DocumentFile file = DocumentFile.fromTreeUri(context, Uri.parse(documentUri)); + return !(file != null && file.canWrite()); + } + } + } + return false; + } + + private static void copyFile ( + Context context, File srcFile, File destFile) throws Exception { + InputStream fis=getInputStream(context,srcFile); + if (fis==null) fis=new FileInputStream(srcFile); + OutputStream fos=getOutputStream(context,destFile); + if (fos==null) fos=new FileOutputStream(destFile); + int len = fis.available(); + if (len>MAX_BUFFER_SIZE) len=MAX_BUFFER_SIZE; + byte[] flush =new byte [len]; + while((len=fis.read(flush))>0) { + fos.write(flush,0,len); + } + fos.flush(); + fis.close(); + fos.close(); + } + + private static void copyTree ( + Context context,File srcFolder,File destFolder) + throws Exception{ + DocumentFile DestFolder=getDocumentFile(destFolder,true,context); + if (DestFolder==null) destFolder.mkdirs(); + DocumentFile SrcFolder=getDocumentFile(srcFolder,true,context); + String name; + File srcSub,destSub; + if (SrcFolder != null) { + DocumentFile[] SrcSubs = SrcFolder.listFiles(); + for(DocumentFile SrcSub:SrcSubs) { + name = SrcSub.getName(); + srcSub=new File(srcFolder.getAbsolutePath(), name); + destSub=new File(destFolder.getAbsolutePath(), name); + if (SrcSub.isDirectory()){ + copyTree(context,srcSub,destSub); + } else { + copyFile(context,srcSub,destSub); + } + } + } else { + File[] SrcSubs = srcFolder.listFiles(); + for(File SrcSub:SrcSubs) { + name = SrcSub.getName(); + srcSub=new File(srcFolder.getAbsolutePath(), name); + destSub=new File(destFolder.getAbsolutePath(), name); + if (SrcSub.isDirectory()){ + copyTree(context,srcSub,destSub); + } else { + copyFile(context,srcSub,destSub); + } + } + } + } + + public static void copy ( + Context context, File src, File dest) + throws Exception{ + if (isDirectory(context,src)) + copyTree(context,src,dest); + else copyFile(context,src,dest); + } + + public static String[] listFiles ( + Context context,File folder) + throws Exception{ + DocumentFile Folder=getDocumentFile(folder,true,context); + if (Folder == null) { + File[] Subs = folder.listFiles(); + if (Subs==null) return null; + String[] subs=new String[Subs.length]; + for (int i=0;i fileUtils = Class.forName("android.os.FileUtils"); + Method setPermissions = + fileUtils.getMethod("setPermissions", String.class, int.class, int.class, int.class); + return (Integer) setPermissions.invoke(null, path.getAbsolutePath(), mode, -1, -1); + } + + @RequiresApi(api = Build.VERSION_CODES.O) + public static void setPermission(File file) throws IOException { + Set perms = new HashSet<>(); + perms.add(PosixFilePermission.OWNER_READ); + perms.add(PosixFilePermission.OWNER_WRITE); + perms.add(PosixFilePermission.OWNER_EXECUTE); + + perms.add(PosixFilePermission.OTHERS_READ); + perms.add(PosixFilePermission.OTHERS_WRITE); + perms.add(PosixFilePermission.OWNER_EXECUTE); + + perms.add(PosixFilePermission.GROUP_READ); + perms.add(PosixFilePermission.GROUP_WRITE); + perms.add(PosixFilePermission.GROUP_EXECUTE); + + Files.setPosixFilePermissions(file.toPath(), perms); + } + + public static boolean recursiveChmod(File root, int mode) throws Exception { + boolean success = chmod(root, mode) == 0; + for (File path : root.listFiles()) { + if (path.isDirectory()) { + success = recursiveChmod(path, mode); + } + success &= (chmod(path, mode) == 0); + } + return success; + } + + public static boolean delete(String path) { + return delete(new File(path)); + } + + public static boolean delete(File path) { + boolean result = true; + if (path.canWrite()) { + if (path.isDirectory()) { + for (File child : path.listFiles()) { + result &= delete(child); + } + result &= path.delete(); // Delete empty directory. + } else if (path.isFile()) { + result = path.delete(); + } + //if (!result) { + //Log.e(TAG, "Delete failed;"); + //} + return result; + } else { + if (path.exists()) { + DocumentFile documentFile = DocumentUtil.getDocumentFile(path,null,activity); + if(documentFile!=null) + return documentFile.delete(); + else return false; + } else { + //Log.e(TAG, "File does not exist."); + return false; + } + } + } + + public static File copyFromStream(String name, InputStream input) { + if (name == null || name.length() == 0) { + //Log.e(TAG, "No script name specified."); + return null; + } + File file = new File(name); + if (!makeDirectories(file.getParentFile(), 0755)) { + return null; + } + try { + OutputStream output = new FileOutputStream(file); + IoUtil.copy(input, output); + } catch (Exception e) { + //Log.e(TAG, e); + return null; + } + return file; + } + + public static boolean makeDirectories(File directory, int mode) { + File parent = directory; + while (parent.getParentFile() != null && !parent.exists()) { + parent = parent.getParentFile(); + } + if (!directory.exists()) { + //Log.d(TAG, "Creating directory: " + directory.getName()); + if (!directory.mkdirs()) { + //Log.e(TAG, "Failed to create directory."); + return false; + } + } + try { + recursiveChmod(parent, mode); + } catch (Exception e) { + //Log.e(TAG, e); + return false; + } + return true; + } + + public static boolean mkdir(File directory) { + boolean result; + if (directory.exists()) + return false; + result = directory.mkdirs(); + if(!result) { + DocumentFile documentFile = DocumentUtil.getDocumentFile(directory, true, activity); + result = documentFile != null && documentFile.exists(); + } + return result; + } + + public static File getExternalDownload() { + try { + Class c = Class.forName("android.os.Environment"); + Method m = c.getDeclaredMethod("getExternalStoragePublicDirectory", String.class); + String download = c.getDeclaredField("DIRECTORY_DOWNLOADS").get(null).toString(); + return (File) m.invoke(null, download); + } catch (Exception e) { + return new File(Environment.getExternalStorageDirectory(), "Download"); + } + } + + public static boolean rename(String oldPath, String newPath) { + return rename(new File(oldPath),new File(newPath)); + } + + public static boolean rename(File file, String name) { + return rename(file,new File(file.getParent(), name)); + } + public static boolean rename(File oldFile,File newFile) { + boolean result =oldFile.renameTo(newFile); + if(!result && oldFile.exists()) { + try { + DocumentFile documentFile = DocumentUtil.getDocumentFile(oldFile,null,activity); + if (documentFile != null) + result = documentFile.renameTo(newFile.getName()); + } catch (Exception exception) { + exception.printStackTrace(); + } + } + return result; + } + + public static String readToString(File file) throws IOException { + if (file == null || !file.exists()) { + return null; + } + FileReader reader = new FileReader(file); + StringBuilder out = new StringBuilder(); + char[] buffer = new char[1024 * 4]; + int numRead = 0; + while ((numRead = reader.read(buffer)) > -1) { + out.append(String.valueOf(buffer, 0, numRead)); + } + reader.close(); + return out.toString(); + } + + public static String readFromAssetsFile(Context context, String name) throws IOException { + AssetManager am = context.getAssets(); + BufferedReader reader = new BufferedReader(new InputStreamReader(am.open(name))); + String line; + StringBuilder builder = new StringBuilder(); + while ((line = reader.readLine()) != null) { + builder.append(line); + } + reader.close(); + return builder.toString(); + } + + public static void lnOrcopy(File src, File dst, int sdk) throws IOException, ErrnoException { + + if (sdk>=21) { + Os.symlink(src.getAbsolutePath(), dst.getAbsolutePath()); + } else { + FileInputStream inStream = new FileInputStream(src); + FileOutputStream outStream = new FileOutputStream(dst); + FileChannel inChannel = inStream.getChannel(); + FileChannel outChannel = outStream.getChannel(); + inChannel.transferTo(0, inChannel.size(), outChannel); + inStream.close(); + outStream.close(); + } + } + + public static String getFileContents(String filename) { + + File scriptFile = new File( filename ); + StringBuilder tContent = new StringBuilder(); + if (scriptFile.exists()) { + BufferedReader in; + try { + in = new BufferedReader(new FileReader(scriptFile)); + String line; + + while ((line = in.readLine())!=null) { + tContent.append(line).append("\n"); + } + in.close(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + } + return tContent.toString(); + } + + public static String getFileContents(String filename, int pos) { + + File scriptFile = new File( filename ); + StringBuilder tContent = new StringBuilder(); + if (scriptFile.exists()) { + BufferedReader in; + try { + in = new BufferedReader(new FileReader(scriptFile)); + String line; + + while ((line = in.readLine())!=null) { + tContent.append(line).append("\n"); + if (tContent.length()>=pos) { + in.close(); + return tContent.toString(); + } + } + in.close(); + } catch (FileNotFoundException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + } + return tContent.toString(); + } + + public static boolean canWrite(File file){ + boolean canWrite = file.canWrite(); + if(!canWrite){ + DocumentFile documentFile = DocumentUtil.getDocumentFile(file,false, activity); + if(documentFile!=null) + canWrite = documentFile.canWrite(); + } + return canWrite; + } + + public static FileOutputStream getFileOutputStream(File file,boolean append){ + FileOutputStream fileOutputStream; + try { + fileOutputStream = new FileOutputStream(file, append); + } catch (IOException e){ + fileOutputStream = (FileOutputStream) DocumentUtil.getOutputStream(activity,file,append); + } + return fileOutputStream; + } + + public static FileOutputStream getFileOutputStream(File file){ + FileOutputStream fileOutputStream; + try { + fileOutputStream = new FileOutputStream(file); + } catch (IOException e){ + fileOutputStream = (FileOutputStream) DocumentUtil.getOutputStream(activity,file); + } + return fileOutputStream; + } + + public static void writeToFile(String filePath, String text, boolean append) { + FileOutputStream fOut; + try{ + try { + fOut = new FileOutputStream(filePath,append); + fOut.write(text.getBytes()); + } catch (IOException e) { + DocumentFile file = DocumentUtil.getDocumentFile(new File(filePath), false, activity); + String mode; + if(append) + mode = "wa"; + else mode = "wt"; + fOut = (FileOutputStream) activity.getContentResolver().openOutputStream(file.getUri(),mode); + fOut.write(text.getBytes()); + } + fOut.flush(); + fOut.close(); + } catch (IOException iox) { + iox.printStackTrace(); + } + } + + public static void writeToFile(String filePath, String text) { + FileOutputStream fOut; + try{ + try { + fOut = new FileOutputStream(filePath); + fOut.write(text.getBytes()); + } catch (IOException e) { + DocumentFile file = DocumentUtil.getDocumentFile(new File(filePath), false, activity); + fOut = (FileOutputStream) activity.getContentResolver().openOutputStream(file.getUri(),"wt"); + fOut.write(text.getBytes()); + } + fOut.flush(); + fOut.close(); + } catch (IOException iox) { + iox.printStackTrace(); + } + } + + public static String getAbsolutePath(Context context){ +// return context.getExternalFilesDir(null).getPath() + "/qpython"; + //return Environment.getExternalStorageDirectory().getPath() + "/qpython"; + return context.getExternalFilesDir("").getPath(); + } + + public static File getPath(Context context){ + return context.getExternalFilesDir(null); + } + + public static String getQyPath(){ +// return context.getExternalFilesDir(null).getAbsolutePath(); + return Environment.getExternalStorageDirectory().getPath(); + } + + public static String getLibDownloadTempPath(Context context){ + return getAbsolutePath(context) + "/cache"; + } + + public static String getAbsoluteLogPath(Context context){ + return getAbsolutePath(context) + "/log/last.log"; + } + + public static String getPyCachePath(Context context){ + return getAbsolutePath(context) + ".qpyc"; + } +} diff --git a/src/main/java/util/IoUtil.java b/src/main/java/util/IoUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..2c33628a1bd3851eb4914d136f03e3843f81592f --- /dev/null +++ b/src/main/java/util/IoUtil.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package util; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public class IoUtil { + private static final int BUFFER_SIZE = 1024 * 8; + + private IoUtil() { + // Utility class. + } + + public static int copy(InputStream input, OutputStream output) throws Exception, IOException { + byte[] buffer = new byte[BUFFER_SIZE]; + + BufferedInputStream in = new BufferedInputStream(input, BUFFER_SIZE); + BufferedOutputStream out = new BufferedOutputStream(output, BUFFER_SIZE); + int count = 0, n = 0; + try { + while ((n = in.read(buffer, 0, BUFFER_SIZE)) != -1) { + out.write(buffer, 0, n); + count += n; + } + out.flush(); + } finally { + try { + out.close(); + } catch (IOException e) { + //Log.e(e.getMessage(), e); + } + try { + in.close(); + } catch (IOException e) { + //Log.e(e.getMessage(), e); + } + } + return count; + } + +} diff --git a/src/main/res/layout/activity_preference.xml b/src/main/res/layout/activity_preference.xml index 902b8ed1f8ab8a0e87b4be52b02f5cb4f1835f56..353016bea815c62631875f591b65c27b00ee2358 100644 --- a/src/main/res/layout/activity_preference.xml +++ b/src/main/res/layout/activity_preference.xml @@ -1,35 +1,35 @@ - - - - - - - - - - - + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/res/layout/ftp_widget.xml b/src/main/res/layout/ftp_widget.xml index 5b618383b9753296702503171551d62a2186283c..fa3c309e8443532acec9d3a6883a759f49e943e2 100644 --- a/src/main/res/layout/ftp_widget.xml +++ b/src/main/res/layout/ftp_widget.xml @@ -1,29 +1,29 @@ - - - - - - - - - - - - + + + + + + + + + + + + diff --git a/src/main/res/values-v11/styles.xml b/src/main/res/values-v11/styles.xml index 3c02242ad044be9b8c7c09e7c90c5d427763fe97..cab48cd670ce9b9e84847a3fa477b05245b09cd0 100644 --- a/src/main/res/values-v11/styles.xml +++ b/src/main/res/values-v11/styles.xml @@ -1,11 +1,11 @@ - - - - - - + + + + + + diff --git a/src/main/res/values-v14/styles.xml b/src/main/res/values-v14/styles.xml index a91fd0372b20f85e284fc2fa2ce949176dfdf6e5..b6dd86ae00edeb2e33b8a72b472043a8f36c0236 100644 --- a/src/main/res/values-v14/styles.xml +++ b/src/main/res/values-v14/styles.xml @@ -1,12 +1,12 @@ - - - - - - + + + + + + diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml index 053a878ae3e1cf76de85bd94657333a1073873ea..7f9e5fb9b158be60eee2f985de793ab6361b7024 100644 --- a/src/main/res/values-zh-rCN/strings.xml +++ b/src/main/res/values-zh-rCN/strings.xml @@ -1,58 +1,57 @@ - -QFtplib -FTP 服务器 -运行 -启动 FTP 服务器 -FTP 服务运行在 %s -无法启动 FTP 服务器 -设置 -用户名 -ftp -密码 -ftp - - 显示密码 - - - 假 - -高级设置... -端口号 -2121 -停留在文件夹中 -/ -接受来自 WiFi 连接 -true -接受来自网络代理连接 -false -保持设备处于唤醒状态(全CPU速度) -true -额外 -帮助 -关于 -FTP 服务器帮助 -帮助信息 -FTP 服务器关于 -有关消息 - - -不能解析网址 -未知 -OK -用户名必须由1个或多个字母,不允许使用其他字符。 -该密码必须由1个或多个字母数字字符,不允许使用其他字符。 -该端口号必须在范围为1~65535 -WifiStateReceiver -FTP服务器启动 -FTP服务器正在运行 -服务器接受 FTP 连接 -连接 -已连接 -失败 -无法访问 -断开的 -警告:存储不可用,你可能想卸载它 -FTP服务器控件 -扩展功能 - - + +QFtplib +FTP 服务器 +运行 +启动 FTP 服务器 +FTP 服务运行在 %s +无法启动 FTP 服务器 +设置 +用户名 +ftp +密码 +ftp + + 显示密码 + + + 假 + +高级设置... +端口号 +停留在文件夹中 +/ +接受来自 WiFi 连接 +true +接受来自网络代理连接 +false +保持设备处于唤醒状态(全CPU速度) +true +额外 +帮助 +关于 +FTP 服务器帮助 +帮助信息 +FTP 服务器关于 +有关消息 + + +不能解析网址 +未知 +OK +用户名必须由1个或多个字母,不允许使用其他字符。 +该密码必须由1个或多个字母数字字符,不允许使用其他字符。 +该端口号必须在范围为1~65535 +WifiStateReceiver +FTP服务器启动 +FTP服务器正在运行 +服务器接受 FTP 连接 +连接 +已连接 +失败 +无法访问 +断开的 +警告:存储不可用,你可能想卸载它 +FTP服务器控件 +扩展功能 + + diff --git a/src/main/res/values/colors.xml b/src/main/res/values/colors.xml index 32f90ce0571ac184dc14a7cd99cd7bc517118604..17016d2d28afdff698336a7c11eab593a3a4b292 100644 --- a/src/main/res/values/colors.xml +++ b/src/main/res/values/colors.xml @@ -1,7 +1,7 @@ - - - #FF4A4A4A - #FF363636 - #FF4BAC07 - #FFFFFF + + + #FF4A4A4A + #FF363636 + #FF4BAC07 + #FFFFFF \ No newline at end of file diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index fad07f2f80134163c682e4966dfb4e2383fbc8b7..6405565dc140535064cdbf48ce58b0153567eddb 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -1,72 +1,72 @@ - - QFtplib - FTP Server - Running - Start FTP server - FTP server running at %s - Failed to start the FTP server - Settings - Username - ftp - Password - ftp - - Display password - - - false - - Advanced Settings... - Port Number - 2121 - Stay in folder - / - Accept connection from wifi - true - Accept connection from net proxy - false - Keep device awake (full CPU speed) - true - Extra - Help... - About... - FTP Server Help - HELP Message - FTP Server About - ABOUT Message - - - Can\'t retreive url - Notice - - unknown - OK - The username must consist of 1 or more letters. No other characters are allowed. - The password must consist of 1 or more alphanumeric characters. No other characters are allowed. - The port number must be in the range 1 to 65535. - WifiStateReceiver - FTP Server started - FTP Server is running - Server is accepting FTP connections. - connecting - connected - failed - unreachable - disconnected - Warning: storage is not available. You may want to unmount it. - FTP Server Widget - FTP setting - FTP Server Setting - Extensions - - - reset_storage - running_state - username - password - portNum - show_password - chrootDir - stayAwake - about - + + QFtplib + FTP Server + Running + Start FTP server + FTP server running at %s + Failed to start the FTP server + Settings + Username + ftp + Password + ftp + + Display password + + + false + + Advanced Settings... + Port Number + 2121 + Stay in folder + / + Accept connection from wifi + true + Accept connection from net proxy + false + Keep device awake (full CPU speed) + true + Extra + Help... + About... + FTP Server Help + HELP Message + FTP Server About + ABOUT Message + + + Can\'t retreive url + Notice + + unknown + OK + The username must consist of 1 or more letters. No other characters are allowed. + The password must consist of 1 or more alphanumeric characters. No other characters are allowed. + The port number must be in the range 1 to 65535. + WifiStateReceiver + FTP Server started + FTP Server is running + Server is accepting FTP connections. + connecting + connected + failed + unreachable + disconnected + Warning: storage is not available. You may want to unmount it. + FTP Server Widget + FTP setting + FTP Server Setting + Extensions + + + reset_storage + running_state + username + password + portNum + show_password + chrootDir + stayAwake + about + diff --git a/src/main/res/values/styles.xml b/src/main/res/values/styles.xml index 59aca12c932f78b2095522e27bd9852fb6ef938b..c6d69f5039b5a3f71556eaf9c7b9c577641db018 100644 --- a/src/main/res/values/styles.xml +++ b/src/main/res/values/styles.xml @@ -1,26 +1,26 @@ - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/res/xml/ftp_preferences.xml b/src/main/res/xml/ftp_preferences.xml index d85d42fd936851f9ae68e6c6378896b6b5c1657e..aeb50739145c92d10ec8f380fbcb0cd155a06fe5 100644 --- a/src/main/res/xml/ftp_preferences.xml +++ b/src/main/res/xml/ftp_preferences.xml @@ -1,18 +1,18 @@ - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/res/xml/header_preferences.xml b/src/main/res/xml/header_preferences.xml index a856f7b83a3fcd97d1c15b92b32fe66c258819c0..ea336944976ba9f8006365849357d9fa5bedc1be 100644 --- a/src/main/res/xml/header_preferences.xml +++ b/src/main/res/xml/header_preferences.xml @@ -1,9 +1,9 @@ - - - -
- + + + +
+ \ No newline at end of file