package org.primftpd; import android.Manifest; import android.annotation.SuppressLint; import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; import android.app.DialogFragment; import android.app.ProgressDialog; import android.content.BroadcastReceiver; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.SharedPreferences.OnSharedPreferenceChangeListener; import android.content.pm.PackageManager; import android.net.ConnectivityManager; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.view.Gravity; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; import org.apache.ftpserver.util.IoUtils; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; import org.primftpd.log.PrimFtpdLoggerBinder; import org.primftpd.prefs.AboutActivity; import org.primftpd.prefs.FtpPrefsActivityThemeDark; import org.primftpd.prefs.FtpPrefsActivityThemeLight; import org.primftpd.prefs.LoadPrefsUtil; import org.primftpd.prefs.Logging; import org.primftpd.prefs.Theme; import org.primftpd.util.KeyGenerator; import org.primftpd.util.KeyInfoProvider; import org.primftpd.util.NotificationUtil; import org.primftpd.util.PrngFixes; import org.primftpd.util.ServersRunningBean; import org.primftpd.util.ServicesStartStopUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.net.InetAddress; import java.net.NetworkInterface; import java.net.SocketException; import java.security.PublicKey; import java.security.interfaces.RSAPublicKey; import java.util.Enumeration; /** * Activity to display network info and to start FTP service. */ public class PrimitiveFtpdActivity extends Activity { private BroadcastReceiver networkStateReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { logger.debug("network connectivity changed, data str: '{}', action: '{}'", intent.getDataString(), intent.getAction()); showAddresses(); } }; // flag must be static to be avail after activity change private static boolean prefsChanged = false; private OnSharedPreferenceChangeListener prefsChangeListener = new OnSharedPreferenceChangeListener() { @Override public void onSharedPreferenceChanged( SharedPreferences sharedPreferences, String key) { logger.debug("onSharedPreferenceChanged(), key: {}", key); prefsChanged = true; } }; public static final String PUBLICKEY_FILENAME = "pftpd-pub.bin"; public static final String PRIVATEKEY_FILENAME = "pftpd-priv.pk8"; private static final int PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE = 0xBEEF; public static final String DIALOG_TAG = "dialogs"; protected Logger logger = LoggerFactory.getLogger(getClass()); private PrefsBean prefsBean; private Theme theme; private ServersRunningBean serversRunning; private boolean keyPresent = false; private String fingerprintMd5 = " - "; private String fingerprintSha1 = " - "; private String fingerprintSha256 = " - "; private long timestampOfLastEvent = 0; /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { // basic setup super.onCreate(savedInstanceState); logger.debug("onCreate()"); // fixes/workarounds for android security issue below 4.3 regarding key generation PrngFixes.apply(); // prefs change SharedPreferences prefs = LoadPrefsUtil.getPrefs(getBaseContext()); prefs.registerOnSharedPreferenceChangeListener(prefsChangeListener); // layout & theme theme = LoadPrefsUtil.theme(prefs); setTheme(theme.resourceId()); setContentView(R.layout.main); // calc keys fingerprints calcPubkeyFingerprints(); showKeyFingerprints(); // create addresses label ((TextView)findViewById(R.id.addressesLabel)).setText( String.format("%s (%s)", getText(R.string.ipAddrLabel), getText(R.string.ifacesLabel) ) ); // create ports label ((TextView)findViewById(R.id.portsLabel)).setText( String.format("%s / %s / %s", getText(R.string.protocolLabel), getText(R.string.portLabel), getText(R.string.state)) ); // listen for events EventBus.getDefault().register(this); } @Override protected void onDestroy() { super.onDestroy(); // prefs change SharedPreferences prefs = LoadPrefsUtil.getPrefs(getBaseContext()); prefs.unregisterOnSharedPreferenceChangeListener(prefsChangeListener); // server state change events EventBus.getDefault().unregister(this); } @Override protected void onStart() { super.onStart(); logger.debug("onStart()"); loadPrefs(); showUsername(); showAnonymousLogin(); } @Override protected void onResume() { super.onResume(); logger.debug("onResume()"); // register listener to reprint interfaces table when network connections change // TODO show current IP for android 7 IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION); registerReceiver(this.networkStateReceiver, filter); // e.g. necessary when ports preferences have been changed displayServersState(); } @Override protected void onPause() { super.onPause(); logger.debug("onPause()"); // unregister broadcast receiver this.unregisterReceiver(this.networkStateReceiver); } protected FileInputStream buildPublickeyInStream() throws IOException { FileInputStream fis = openFileInput(PUBLICKEY_FILENAME); return fis; } protected FileOutputStream buildPublickeyOutStream() throws IOException { FileOutputStream fos = openFileOutput(PUBLICKEY_FILENAME, Context.MODE_PRIVATE); return fos; } protected FileInputStream buildPrivatekeyInStream() throws IOException { FileInputStream fis = openFileInput(PRIVATEKEY_FILENAME); return fis; } protected FileOutputStream buildPrivatekeyOutStream() throws IOException { FileOutputStream fos = openFileOutput(PRIVATEKEY_FILENAME, Context.MODE_PRIVATE); return fos; } /** * Creates figerprints of public key. */ protected void calcPubkeyFingerprints() { FileInputStream fis = null; try { fis = buildPublickeyInStream(); // check if key is present if (fis.available() <= 0) { keyPresent = false; throw new Exception("key seems not to be present"); } KeyInfoProvider keyInfoprovider = new KeyInfoProvider(); PublicKey pubKey = keyInfoprovider.readPublicKey(fis); RSAPublicKey rsaPubKey = (RSAPublicKey) pubKey; byte[] encodedKey = keyInfoprovider.encodeAsSsh(rsaPubKey); // fingerprints String fp = keyInfoprovider.fingerprint(encodedKey, "MD5"); if (fp != null) { fingerprintMd5 = fp; } fp = keyInfoprovider.fingerprint(encodedKey, "SHA-1"); if (fp != null) { fingerprintSha1 = fp; } fp = keyInfoprovider.fingerprint(encodedKey, "SHA-256"); if (fp != null) { fingerprintSha256 = fp; } keyPresent = true; } catch (Exception e) { logger.debug("key does probably not exist"); } finally { if (fis != null) { IoUtils.close(fis); } } } /** * Creates table containing network interfaces. */ protected void showAddresses() { LinearLayout container = (LinearLayout)findViewById(R.id.addressesContainer); // clear old entries container.removeAllViews(); try { Enumeration<NetworkInterface> ifaces = NetworkInterface.getNetworkInterfaces(); while (ifaces.hasMoreElements()) { NetworkInterface iface = ifaces.nextElement(); String ifaceDispName = iface.getDisplayName(); String ifaceName = iface.getName(); Enumeration<InetAddress> inetAddrs = iface.getInetAddresses(); while (inetAddrs.hasMoreElements()) { InetAddress inetAddr = inetAddrs.nextElement(); String hostAddr = inetAddr.getHostAddress(); logger.debug("addr: '{}', iface name: '{}', disp name: '{}', loopback: '{}'", new Object[]{ inetAddr, ifaceName, ifaceDispName, inetAddr.isLoopbackAddress()}); if (inetAddr.isLoopbackAddress()) { continue; } String displayText = hostAddr + " (" + ifaceDispName + ")"; if(displayText.contains("::")) { // Don't include the raw encoded names. Just the raw IP addresses. logger.debug("Skipping IPv6 address '{}'", displayText); continue; } TextView textView = new TextView(container.getContext()); container.addView(textView); textView.setText(displayText); textView.setGravity(Gravity.CENTER_HORIZONTAL); textView.setTextIsSelectable(true); } } } catch (SocketException e) { logger.info("exception while iterating network interfaces", e); String msg = getText(R.string.ifacesError) + e.getLocalizedMessage(); Toast.makeText(this, msg, Toast.LENGTH_SHORT).show(); } } @SuppressLint("SetTextI18n") protected void showPortsAndServerState() { ((TextView)findViewById(R.id.ftpTextView)) .setText("ftp / " + prefsBean.getPortStr() + " / " + getText(serversRunning.ftp ? R.string.serverStarted : R.string.serverStopped)); ((TextView)findViewById(R.id.sftpTextView)) .setText("sftp / " + prefsBean.getSecurePortStr() + " / " + getText(serversRunning.ssh ? R.string.serverStarted : R.string.serverStopped)); } protected void showUsername() { TextView usernameView = (TextView)findViewById(R.id.usernameTextView); usernameView.setText(prefsBean.getUserName()); } protected void showAnonymousLogin() { TextView anonymousView = (TextView)findViewById(R.id.anonymousLoginTextView); anonymousView.setText(getString(R.string.isAnonymous, prefsBean.isAnonymousLogin())); } @SuppressLint("SetTextI18n") protected void showKeyFingerprints() { ((TextView)findViewById(R.id.keyFingerprintMd5Label)) .setText("MD5"); ((TextView)findViewById(R.id.keyFingerprintSha1Label)) .setText("SHA1"); ((TextView)findViewById(R.id.keyFingerprintSha256Label)) .setText("SHA256"); ((TextView)findViewById(R.id.keyFingerprintMd5TextView)) .setText(fingerprintMd5); ((TextView)findViewById(R.id.keyFingerprintSha1TextView)) .setText(fingerprintSha1); ((TextView)findViewById(R.id.keyFingerprintSha256TextView)) .setText(fingerprintSha256); // create onRefreshListener View refreshButton = findViewById(R.id.keyFingerprintsLabel); refreshButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { GenKeysAskDialogFragment askDiag = new GenKeysAskDialogFragment(); askDiag.show(getFragmentManager(), DIALOG_TAG); } }); } protected void genKeysAndShowProgressDiag(boolean startServerOnFinish) { // critical: do not pass getApplicationContext() to dialog final ProgressDialog progressDiag = new ProgressDialog(this); progressDiag.setCancelable(false); progressDiag.setMessage(getText(R.string.generatingKeysMessage)); AsyncTask<Void, Void, Void> task = new GenKeysAsyncTask( progressDiag, startServerOnFinish); task.execute(); progressDiag.show(); } public static class GenKeysAskDialogFragment extends DialogFragment { public static final String KEY_START_SERVER = "START_SERVER"; private boolean startServerOnFinish; @Override public void setArguments(Bundle args) { super.setArguments(args); startServerOnFinish = args.getBoolean(KEY_START_SERVER); } @Override public Dialog onCreateDialog(Bundle savedInstanceState) { AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); builder.setMessage(R.string.generateKeysMessage); builder.setPositiveButton(R.string.generate, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int id) { PrimitiveFtpdActivity activity = (PrimitiveFtpdActivity) getActivity(); activity.genKeysAndShowProgressDiag(startServerOnFinish); } }); builder.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int id) { // nothing } }); return builder.create(); } } class GenKeysAsyncTask extends AsyncTask<Void, Void, Void> { private final ProgressDialog progressDiag; private final boolean startServerOnFinish; public GenKeysAsyncTask( ProgressDialog progressDiag, boolean startServerOnFinish) { this.progressDiag = progressDiag; this.startServerOnFinish = startServerOnFinish; } @Override protected Void doInBackground(Void... params) { try { FileOutputStream publickeyFos = buildPublickeyOutStream(); FileOutputStream privatekeyFos = buildPrivatekeyOutStream(); try { new KeyGenerator().generate(publickeyFos, privatekeyFos); } finally { publickeyFos.close(); privatekeyFos.close(); } } catch (Exception e) { logger.error("could not generate keys", e); } return null; } @Override protected void onPostExecute(Void result) { super.onPostExecute(result); calcPubkeyFingerprints(); progressDiag.dismiss(); showKeyFingerprints(); if (startServerOnFinish) { // icon members should be set at this time handleStart(); } } } @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) public void onEvent(ServerStateChangedEvent event) { long currentTime = System.currentTimeMillis(); long offset = currentTime - timestampOfLastEvent; if (offset > 300) { logger.debug("handling event, offset: {} ms", Long.valueOf(offset)); timestampOfLastEvent = currentTime; displayServersState(); } else { logger.debug("ignoring event, offset: {} ms", Long.valueOf(offset)); } } /** * Displays UI-elements showing if servers are running. That includes * Actionbar Icon and Ports-Table. When Activity is shown the first time * this is triggered by {@link #onCreateOptionsMenu(Menu)}, when user comes back from * preferences, this is triggered by {@link #onResume()}. It may be invoked by * {@link GenKeysAsyncTask}. */ protected void displayServersState() { logger.debug("displayServersState()"); checkServicesRunning(); Boolean running = null; if (serversRunning != null) { running = Boolean.valueOf(serversRunning.atLeastOneRunning()); } // should be triggered by onCreateOptionsMenu() to avoid icon flicker // when invoked via notification updateButtonStates(running); // by checking ButtonStates we get info which services are running // that is displayed in portsTable // as there are no icons when this runs first time, // we don't get serversRunning, yet if (serversRunning != null) { showPortsAndServerState(); } } protected void checkServicesRunning() { logger.debug("checkServicesRunning()"); this.serversRunning = ServicesStartStopUtil.checkServicesRunning(this); } /** * Updates enabled state of start/stop buttons. */ protected void updateButtonStates(Boolean running) { if (startIcon == null || stopIcon == null) { logger.debug("updateButtonStates(), no icons"); return; } logger.debug("updateButtonStates()"); boolean atLeastOneRunning; if (running == null) { checkServicesRunning(); atLeastOneRunning = serversRunning.atLeastOneRunning(); } else { atLeastOneRunning = running.booleanValue(); } startIcon.setVisible(!atLeastOneRunning); stopIcon.setVisible(atLeastOneRunning); // remove status bar notification if server not running if (!atLeastOneRunning) { NotificationUtil.removeStatusbarNotification(this); } } protected MenuItem startIcon; protected MenuItem stopIcon; @Override public boolean onCreateOptionsMenu(Menu menu) { logger.debug("onCreateOptionsMenu()"); getMenuInflater().inflate(R.menu.pftpd, menu); startIcon = menu.findItem(R.id.menu_start); stopIcon = menu.findItem(R.id.menu_stop); // at least required on app start displayServersState(); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.menu_start: handleStart(); break; case R.id.menu_stop: handleStop(); break; case R.id.menu_prefs: handlePrefs(); break; case R.id.menu_about: handleAbout(); break; case R.id.menu_exit: finish(); break; } return super.onOptionsItemSelected(item); } protected void handleStart() { if (hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE)) { ServicesStartStopUtil.startServers(this, prefsBean, this); } } /** * Checks whether the app has the following permission. * @param permission The permission name * @param requestCode The request code to check against in the {@link #onRequestPermissionsResult} callback. * @return true if permission has been granted. */ protected boolean hasPermission(String permission, int requestCode) { if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if(checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) { requestPermissions(new String[]{permission}, requestCode); return false; } } return true; } @Override public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) { switch (requestCode) { case PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE: { // If request is cancelled, the result arrays are empty. boolean granted = grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED; if(granted) { ServicesStartStopUtil.startServers(this, prefsBean, this); } else { String textPara = getString(R.string.permissionNameStorage); Toast.makeText(this, getString(R.string.permissionRequired, textPara), Toast.LENGTH_LONG).show(); } } } } public boolean isKeyPresent() { return keyPresent; } public void showGenKeyDialog() { GenKeysAskDialogFragment askDiag = new GenKeysAskDialogFragment(); Bundle diagArgs = new Bundle(); diagArgs.putBoolean(GenKeysAskDialogFragment.KEY_START_SERVER, true); askDiag.setArguments(diagArgs); askDiag.show(getFragmentManager(), DIALOG_TAG); } protected void handleStop() { ServicesStartStopUtil.stopServers(this); } protected void handlePrefs() { Class<?> prefsActivityClass = theme == Theme.DARK ? FtpPrefsActivityThemeDark.class : FtpPrefsActivityThemeLight.class; Intent intent = new Intent(this, prefsActivityClass); startActivity(intent); } protected void handleAbout() { Intent intent = new Intent(this, AboutActivity.class); startActivity(intent); } /** * Loads and parses preferences. * * @return {@link PrefsBean} */ protected void loadPrefs() { logger.debug("loadPrefs()"); SharedPreferences prefs = LoadPrefsUtil.getPrefs(getBaseContext()); this.prefsBean = LoadPrefsUtil.loadPrefs(logger, prefs); handlePrefsChanged(); handleLoggingPref(prefs); } protected void handlePrefsChanged() { if (prefsChanged) { prefsChanged = false; if (serversRunning != null && serversRunning.atLeastOneRunning()) { Toast.makeText( getApplicationContext(), R.string.restartServer, Toast.LENGTH_LONG).show(); } } } protected void handleLoggingPref(SharedPreferences prefs) { String loggingStr = prefs.getString( LoadPrefsUtil.PREF_KEY_LOGGING, Logging.NONE.xmlValue()); Logging logging = Logging.byXmlVal(loggingStr); // one could argue if this makes sense :) logger.debug("got 'logging': {}", logging); PrimFtpdLoggerBinder.setLoggingPref(logging); // re-create own log, don't care about other classes this.logger = LoggerFactory.getLogger(getClass()); } }