diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6f4c369..ed1e174 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -21,6 +21,10 @@ + + 0) && (Integer.parseInt(txt_server_port.getText().toString()) < 65535)) { // check for valid port txt_server_port.setText(String.valueOf(Integer.parseInt(txt_server_port.getText().toString()))); - } else txt_server_port.setText(String.valueOf(62201)); + } else { + txt_server_port.setText(String.valueOf(62201)); + } } catch (NumberFormatException ex) { txt_server_port.setText(String.valueOf(62201)); } if (txt_NickName.getText().toString().equalsIgnoreCase("")) { // Need to create a new Nick toast.setText("You Must choose a unique Nickname."); // choosing a used nick will just overwrite it. So really toast.show(); + } else if (!(txt_ports.getText().toString().matches("tcp/\\d.*") || txt_ports.getText().toString().matches("udp/\\d.*"))) { + toast.setText("Access ports must be in the form of 'tcp/22'"); + toast.show(); } else if (spn_allowip.getSelectedItem().toString().equalsIgnoreCase("Allow IP") && (!ipValidate.isValid(txt_allowIP.getText().toString()))){ //Have to have a valid ip to allow, if using allow ip toast.setText("You Must supply a valid IP address to 'Allow IP'."); toast.show(); @@ -219,7 +227,6 @@ public boolean onOptionsItemSelected(MenuItem item) { } else if (spn_ssh.getSelectedItem().toString().equalsIgnoreCase("Juicessh")) { config.SSH_CMD = "juice:" + juice_adapt.getConnectionName(spn_juice.getSelectedItemPosition()); config.juice_uuid = juice_adapt.getConnectionId(spn_juice.getSelectedItemPosition()); - Log.v("fwknop2", "Connection Name is: " + juice_adapt.getConnectionName(spn_juice.getSelectedItemPosition())); } else { config.juice_uuid = UUID.fromString("00000000-0000-0000-0000-000000000000"); config.SSH_CMD = ""; @@ -235,8 +242,14 @@ public boolean onOptionsItemSelected(MenuItem item) { if(activity instanceof ConfigListActivity) { ConfigListActivity myactivity = (ConfigListActivity) activity; myactivity.onItemSaved(); + + } else { + ConfigDetailActivity myactivity = (ConfigDetailActivity) activity; + myactivity.onBackPressed(); } } + } else { + return false; } return super.onOptionsItemSelected(item); } @@ -244,11 +257,8 @@ public boolean onOptionsItemSelected(MenuItem item) { @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { // This handles the qrcode results - Log.v("fwknop2", "Detail fragment activity result"); if (requestCode == 0) { - if (resultCode == Activity.RESULT_OK) { - String contents = data.getStringExtra("SCAN_RESULT"); for (String stanzas: contents.split(" ")){ String[] tmp = stanzas.split(":"); @@ -287,8 +297,6 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, View rootView = inflater.inflate(R.layout.fragment_config_detail, container, false); active_Nick = getArguments().getString("item_id"); myJuice = new PluginContract(); - // do something here to get the list of nicks - //Handlers for the input fields txt_NickName = (TextView) rootView.findViewById(R.id.NickName); @@ -317,8 +325,6 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, juice_adapt = new ConnectionSpinnerAdapter(getActivity()); spn_juice.setAdapter(juice_adapt); - - //connectionListLoader.setOnLoadedListener(connectionListLoader.OnLoadedListener()); spn_allowip = (Spinner) rootView.findViewById(R.id.allowip); ArrayAdapter adapter = ArrayAdapter.createFromResource(getActivity(), R.array.spinner_options, android.R.layout.simple_spinner_item); @@ -348,7 +354,6 @@ public void onItemSelected(AdapterView parent, View view, int pos, long id) { // An item was selected. You can retrieve the selected item using // parent.getItemAtPosition(pos) - if (parent.getItemAtPosition(pos).toString().equalsIgnoreCase("None")) { lay_sshcmd.setVisibility(View.GONE); spn_juice.setVisibility(View.GONE); @@ -433,7 +438,7 @@ public void onNothingSelected(AdapterView parent) {} }); //Below is the loading of a saved config - if (active_Nick.equalsIgnoreCase("New Config")) { + if (active_Nick.equalsIgnoreCase("")) { txt_NickName.setText(""); config.SSH_CMD = ""; txt_HMAC.setInputType(InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD); diff --git a/app/src/main/java/biz/incomsystems/fwknop2/ConfigListActivity.java b/app/src/main/java/biz/incomsystems/fwknop2/ConfigListActivity.java index a628311..2ae6d16 100644 --- a/app/src/main/java/biz/incomsystems/fwknop2/ConfigListActivity.java +++ b/app/src/main/java/biz/incomsystems/fwknop2/ConfigListActivity.java @@ -16,12 +16,13 @@ */ package biz.incomsystems.fwknop2; +import android.content.Context; import android.content.Intent; +import android.content.SharedPreferences; import android.os.Bundle; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentActivity; import android.support.v4.app.FragmentTransaction; -import android.util.Log; import java.util.List; @@ -44,7 +45,7 @@ public class ConfigListActivity extends FragmentActivity implements ConfigListFragment.Callbacks { ConfigDetailFragment fragment; - private boolean mTwoPane; // Whether in two-pane mode. + public boolean mTwoPane; // Whether in two-pane mode. @Override protected void onCreate(Bundle savedInstanceState) { @@ -64,12 +65,18 @@ protected void onCreate(Bundle savedInstanceState) { .findFragmentById(R.id.config_list)) .setActivateOnItemClick(true); } + SharedPreferences prefs = getSharedPreferences("MyPreferences", Context.MODE_PRIVATE); + boolean haveWeShownPreferences = prefs.getBoolean("HaveShownPrefs", false); + + if (!haveWeShownPreferences) { + Intent detailIntent = new Intent(this, HelpActivity.class); + startActivity(detailIntent); + } } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); - Log.v("fwknop2", "onActivityResult in activity"); for (Fragment fragment : getSupportFragmentManager().getFragments()) // this overcomes what may be a bug in the android framework. Pushes the result into fragment so it can get to the nested class. { diff --git a/app/src/main/java/biz/incomsystems/fwknop2/ConfigListFragment.java b/app/src/main/java/biz/incomsystems/fwknop2/ConfigListFragment.java index 2cebc2f..0621365 100644 --- a/app/src/main/java/biz/incomsystems/fwknop2/ConfigListFragment.java +++ b/app/src/main/java/biz/incomsystems/fwknop2/ConfigListFragment.java @@ -17,11 +17,13 @@ package biz.incomsystems.fwknop2; import android.app.Activity; +import android.app.AlertDialog; +import android.content.DialogInterface; import android.content.Intent; import android.os.Bundle; import android.support.v4.app.ListFragment; -import android.util.Log; import android.view.ContextMenu; +import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; @@ -45,6 +47,7 @@ */ public class ConfigListFragment extends ListFragment { public ArrayAdapter customAdapter; + private AdapterView.AdapterContextMenuInfo info; ArrayList array_list = new ArrayList(); DBHelper mydb; SendSPA OurSender; @@ -100,6 +103,7 @@ public ConfigListFragment() { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + setHasOptionsMenu(true); mydb = new DBHelper(getActivity()); array_list = mydb.getAllConfigs(); OurSender = new SendSPA(); @@ -111,6 +115,12 @@ public void onCreate(Bundle savedInstanceState) { setListAdapter(customAdapter); } + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + // Inflate the menu; this adds items to the action bar if it is present. + super.onCreateOptionsMenu(menu, inflater); + inflater.inflate(R.menu.list_menu, menu); + } + @Override public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); @@ -120,8 +130,31 @@ public void onViewCreated(View view, Bundle savedInstanceState) { && savedInstanceState.containsKey(STATE_ACTIVATED_POSITION)) { setActivatedPosition(savedInstanceState.getInt(STATE_ACTIVATED_POSITION)); } + } + public boolean onOptionsItemSelected(MenuItem item) { + int id = item.getItemId(); + if (id == R.id.detail_help) { + Intent detailIntent = new Intent(getActivity(), HelpActivity.class); + startActivity(detailIntent); + } else if (id == R.id.new_item) { + Activity activity = getActivity(); + ConfigListActivity myactivity = (ConfigListActivity) activity; + if (myactivity.mTwoPane){ + mCallbacks.onItemSelected(""); + getListView().setItemChecked(mActivatedPosition, false); + } else { + Intent detailIntent = new Intent(getActivity(), ConfigDetailActivity.class); + detailIntent.putExtra(ConfigDetailFragment.ARG_ITEM_ID, ""); + startActivity(detailIntent); + } + + } else { + return false; + } + return true; + } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); @@ -140,23 +173,23 @@ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMen super.onCreateContextMenu(menu, v, menuInfo); MenuInflater inflater = this.getActivity().getMenuInflater(); - inflater.inflate(R.menu.listmenu, menu); + inflater.inflate(R.menu.list_longtap_menu, menu); } @Override public boolean onContextItemSelected(MenuItem item) { customAdapter = (ArrayAdapter) getListAdapter(); - AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo(); - String nick = ((TextView) info.targetView).getText().toString(); + info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo(); switch (item.getItemId()) { case R.id.delete: // Deleting the selected option - mydb.deleteConfig(nick); - array_list.remove(info.position); - customAdapter.notifyDataSetChanged(); - mydb.close(); + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setMessage("Are you sure?").setPositiveButton("Yes", dialogClickListener) + .setNegativeButton("No", dialogClickListener).show(); + return true; case R.id.knock: + String nick = ((TextView) info.targetView).getText().toString(); OurSender.send(nick, getActivity()); default: @@ -170,8 +203,27 @@ public void onUpdate() { array_list.addAll(mydb.getAllConfigs()); customAdapter.notifyDataSetChanged(); mydb.close(); - Log.v("fwknop2", "onUpdate runs"); } + DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + switch (which){ + case DialogInterface.BUTTON_POSITIVE: + //Yes button clicked + + String nick = ((TextView) info.targetView).getText().toString(); + mydb.deleteConfig(nick); + array_list.remove(info.position); + customAdapter.notifyDataSetChanged(); + + break; + + case DialogInterface.BUTTON_NEGATIVE: + //No button clicked + break; + } + } + }; @Override public void onAttach(Activity activity) { @@ -198,9 +250,6 @@ public void onDetach() { @Override public void onListItemClick(ListView listView, View view, int position, long id) { super.onListItemClick(listView, view, position, id); - - // Notify the active callbacks interface (the activity, if the - // fragment is attached to one) that an item has been selected. mCallbacks.onItemSelected(this.getListAdapter().getItem(position).toString()); } diff --git a/app/src/main/java/biz/incomsystems/fwknop2/ConnectionListLoader.java b/app/src/main/java/biz/incomsystems/fwknop2/ConnectionListLoader.java index 78cef26..bc69d0d 100644 --- a/app/src/main/java/biz/incomsystems/fwknop2/ConnectionListLoader.java +++ b/app/src/main/java/biz/incomsystems/fwknop2/ConnectionListLoader.java @@ -8,7 +8,6 @@ import android.support.v4.content.Loader; import com.sonelli.juicessh.pluginlibrary.PluginContract; -import biz.incomsystems.fwknop2.ConnectionSpinnerAdapter; public class ConnectionListLoader implements LoaderManager.LoaderCallbacks { diff --git a/app/src/main/java/biz/incomsystems/fwknop2/DBHelper.java b/app/src/main/java/biz/incomsystems/fwknop2/DBHelper.java index 2e70d14..a114082 100644 --- a/app/src/main/java/biz/incomsystems/fwknop2/DBHelper.java +++ b/app/src/main/java/biz/incomsystems/fwknop2/DBHelper.java @@ -151,7 +151,7 @@ public ArrayList getAllConfigs() // This returns an array of Nick Name } res.close(); db.close(); - array_list.add("New Config"); + //array_list.add("New Config"); return array_list; } diff --git a/app/src/main/java/biz/incomsystems/fwknop2/HelpActivity.java b/app/src/main/java/biz/incomsystems/fwknop2/HelpActivity.java new file mode 100644 index 0000000..cac22c9 --- /dev/null +++ b/app/src/main/java/biz/incomsystems/fwknop2/HelpActivity.java @@ -0,0 +1,25 @@ +package biz.incomsystems.fwknop2; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.support.v4.app.FragmentActivity; +import android.text.Html; +import android.text.method.LinkMovementMethod; +import android.widget.TextView; + +public class HelpActivity extends FragmentActivity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_help); + TextView textView = (TextView) findViewById (R.id.help_page); + textView.setMovementMethod(LinkMovementMethod.getInstance()); + textView.setTextColor(getResources().getColor(android.R.color.black)); + textView.setText(Html.fromHtml(getString(R.string.help_page))); + SharedPreferences prefs = getSharedPreferences("MyPreferences", Context.MODE_PRIVATE); + SharedPreferences.Editor ed = prefs.edit(); + ed.putBoolean("HaveShownPrefs", true); + ed.apply(); + } +} diff --git a/app/src/main/java/biz/incomsystems/fwknop2/SendSPA.java b/app/src/main/java/biz/incomsystems/fwknop2/SendSPA.java index f00cedb..bf642c1 100644 --- a/app/src/main/java/biz/incomsystems/fwknop2/SendSPA.java +++ b/app/src/main/java/biz/incomsystems/fwknop2/SendSPA.java @@ -40,7 +40,6 @@ public class SendSPA implements OnSessionStartedListener, OnSessionFinishedListener { DBHelper mydb; public static Config config; - Activity ourAct; ProgressDialog pdLoading; Boolean ready; public PluginClient client; @@ -66,7 +65,6 @@ public class SendSPA implements OnSessionStartedListener, OnSessionFinishedListe // @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { - Log.v("fwknop2", "onActivityResult"); if(requestCode == 2585){ client.gotActivityResult(requestCode, resultCode, data); } @@ -75,11 +73,10 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { @Override public void onSessionStarted(int i, String s) { SendSPA.this.isConnected = true; - Log.v("fwknop2", "Trying to attach"); try { client.attach(i,s); } catch (ServiceNotConnectedException ex){ - Log.v("fwknop2", "Error attaching"); + Log.e("fwknop2", "Error attaching"); } } @@ -124,11 +121,7 @@ public int send(String nick, final Activity ourAct) { } else { hmac_b64 = "false"; } - Log.v("fwknop2", config.juice_uuid.toString()); - final getExternalIP task = new getExternalIP(ourAct); - - task.execute(); return 0; } @@ -208,11 +201,10 @@ protected void onPostExecute(String result) { @Override public void onClientStarted() { SendSPA.this.isConnected = true; - Log.v("fwknop2", SendSPA.config.juice_uuid.toString()); try { client.connect(mActivity, config.juice_uuid, SendSPA.this, 2585); } catch (ServiceNotConnectedException ex) { - Log.v("fwknop2", "not connected error"); + Log.e("fwknop2", "not connected error"); } } @Override @@ -223,15 +215,12 @@ public void onClientStopped() { } pdLoading.dismiss(); - if (!config.SSH_CMD.equalsIgnoreCase("") && !(config.SSH_CMD.contains("juice:")) ) { - String ssh_uri = "ssh://" + config.SSH_CMD +"@" + config.SERVER_IP + ":" + config.SERVER_PORT + "/#" + config.NICK_NAME; + if (!config.SSH_CMD.equalsIgnoreCase("") && !(config.SSH_CMD.contains("juice:")) ) { + + String ssh_uri = "ssh://" + config.SSH_CMD + "/#" + config.NICK_NAME; Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(ssh_uri)); - Log.v("fwknop2", ssh_uri); - ourAct.startActivity(i); + mActivity.startActivity(i); } - } - } - } \ No newline at end of file diff --git a/app/src/main/jni/fwknop_client.c b/app/src/main/jni/fwknop_client.c index 8441ded..933805f 100644 --- a/app/src/main/jni/fwknop_client.c +++ b/app/src/main/jni/fwknop_client.c @@ -66,7 +66,7 @@ jstring Java_biz_incomsystems_fwknop2_SendSPA_sendSPAPacket(JNIEnv* env, fid = (*env)->GetFieldID(env, c, "allowip_str", "Ljava/lang/String;"); jstring jallowip = (*env)->GetObjectField(env, thiz, fid); const char *allowip_str = (*env)->GetStringUTFChars(env, jallowip, 0); - LOGV("%s", allowip_str); + fid = (*env)->GetFieldID(env, c, "destip_str", "Ljava/lang/String;"); jstring jdestip = (*env)->GetObjectField(env, thiz, fid); const char *destip_str = (*env)->GetStringUTFChars(env, jdestip, 0); @@ -96,12 +96,12 @@ jstring Java_biz_incomsystems_fwknop2_SendSPA_sendSPAPacket(JNIEnv* env, const char *fw_timeout_str = (*env)->GetStringUTFChars(env, jfwtimeout, 0); fid = (*env)->GetFieldID(env, c, "nat_access_str", "Ljava/lang/String;"); - jstring jnat_access_str = (*env)->GetObjectField(env, thiz, fid); - const char *nat_access_str = (*env)->GetStringUTFChars(env, jnat_access_str, 0); + jstring jnat_access_str = (*env)->GetObjectField(env, thiz, fid); + const char *nat_access_str = (*env)->GetStringUTFChars(env, jnat_access_str, 0); - fid = (*env)->GetFieldID(env, c, "server_cmd_str", "Ljava/lang/String;"); - jstring jserver_cmd = (*env)->GetObjectField(env, thiz, fid); - const char *server_cmd_str = (*env)->GetStringUTFChars(env, jserver_cmd, 0); + fid = (*env)->GetFieldID(env, c, "server_cmd_str", "Ljava/lang/String;"); + jstring jserver_cmd = (*env)->GetObjectField(env, thiz, fid); + const char *server_cmd_str = (*env)->GetStringUTFChars(env, jserver_cmd, 0); /* Sanity checks */ @@ -148,12 +148,10 @@ jstring Java_biz_incomsystems_fwknop2_SendSPA_sendSPAPacket(JNIEnv* env, } else { - LOGV("now to memcpy"); memcpy(hmac_str, hmac_key_tmp, hmac_str_len); } } - LOGV("%s", passwd_b64); if(strcmp(passwd_b64, "true") == 0) { LOGV("Detected key b64"); key_len = fko_base64_decode(passwd_str, @@ -169,7 +167,6 @@ jstring Java_biz_incomsystems_fwknop2_SendSPA_sendSPAPacket(JNIEnv* env, memcpy(passwd_str, key_tmp, key_len); } } - LOGV("%i", hmac_str_len); /* Using an HMAC is optional (currently) */ @@ -194,7 +191,6 @@ jstring Java_biz_incomsystems_fwknop2_SendSPA_sendSPAPacket(JNIEnv* env, message_type = FKO_COMMAND_MSG; fko_set_spa_message_type(ctx, message_type); res = fko_set_spa_message(ctx, server_cmd_str); - LOGV("server command: %s", server_cmd_str); if (res != FKO_SUCCESS) { strcpy(res_msg, fko_errmsg("Error setting SPA request message", res)); goto cleanup; @@ -234,7 +230,6 @@ jstring Java_biz_incomsystems_fwknop2_SendSPA_sendSPAPacket(JNIEnv* env, */ if (nat_access_str[0] != 0x0){ // if nat_access_str is not blank, push it into fko context - LOGV("Nat Access string is: %s", nat_access_str); res = fko_set_spa_nat_access(ctx, nat_access_str); if (res != FKO_SUCCESS) { strcpy(res_msg, fko_errmsg("Error setting NAT string", res)); diff --git a/app/src/main/res/layout/activity_help.xml b/app/src/main/res/layout/activity_help.xml new file mode 100644 index 0000000..ac43cc0 --- /dev/null +++ b/app/src/main/res/layout/activity_help.xml @@ -0,0 +1,28 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_config_detail.xml b/app/src/main/res/layout/fragment_config_detail.xml index 5463bf1..fe1a1b7 100644 --- a/app/src/main/res/layout/fragment_config_detail.xml +++ b/app/src/main/res/layout/fragment_config_detail.xml @@ -333,7 +333,7 @@ android:layout_height="wrap_content" android:text="" android:singleLine="true" - android:textSize="30sp" + android:textSize="20sp" /> diff --git a/app/src/main/res/layout/spinner_list_item.xml b/app/src/main/res/layout/spinner_list_item.xml index f74be48..11b1b8c 100644 --- a/app/src/main/res/layout/spinner_list_item.xml +++ b/app/src/main/res/layout/spinner_list_item.xml @@ -25,11 +25,12 @@ diff --git a/app/src/main/res/menu/mainmenu.xml b/app/src/main/res/menu/detail_menu.xml similarity index 99% rename from app/src/main/res/menu/mainmenu.xml rename to app/src/main/res/menu/detail_menu.xml index 9642a32..285845b 100644 --- a/app/src/main/res/menu/mainmenu.xml +++ b/app/src/main/res/menu/detail_menu.xml @@ -9,4 +9,6 @@ + + \ No newline at end of file diff --git a/app/src/main/res/menu/listmenu.xml b/app/src/main/res/menu/list_longtap_menu.xml similarity index 99% rename from app/src/main/res/menu/listmenu.xml rename to app/src/main/res/menu/list_longtap_menu.xml index c755185..07a8dd7 100644 --- a/app/src/main/res/menu/listmenu.xml +++ b/app/src/main/res/menu/list_longtap_menu.xml @@ -3,7 +3,6 @@ - diff --git a/app/src/main/res/menu/list_menu.xml b/app/src/main/res/menu/list_menu.xml new file mode 100644 index 0000000..50bf3ee --- /dev/null +++ b/app/src/main/res/menu/list_menu.xml @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a71e0a9..dc1ead6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2,6 +2,7 @@ Fwknop2 Config Detail Capture qr + Help Save config Delete config Send knock @@ -20,4 +21,35 @@ SSH Uri Juicessh + + + Welcome to fwknop2! This app is for the sending of SPA Packets to a Linux host or router, mainly + for opening ports in an iptables firewall. More info + at the Cipherdyne web site. +

+

+ Get started by selecting "New Config" from the menu, and then filling in at least "Nickname", "Server Address", + and "Rijndael Key." If you are using luci-app-fwknop on an Openwrt router, you can select the "Capture qr" option from the menu, + and use a qr code generated on the router to auto fill the key and hmac data. +

+

If Juicessh is installed, you can autostart a saved Juicessh connection by sellecting Juicessh from the + "Run SSH App" menu, and after a brief delay, choosing the desired connection. If you want to use a different + SSH app, select "SSH Uri", and then type in your desired Username to connect as. This will use the system\'s + default ssh client to try to connect to the port you are opening. +

+

+ Once the data is filled in, select "Save Config" from the menu. Then, to send a SPA knock, long tap the + Saved config you want to use, and select "Send Knock" +

+ +

Credits: Based on fwknop and libfko written by Michael Rash

+

This app was written by Jonathan Bennett

+

This app is open source software. The source can be found on github. Please report any bugs here.

+

The fwknop2 icon is derived from on a photo by Peter O\'Connor

+ + Tap the back button to get started. +

+ ]]> +