From 3e4d8f433239c40311037616b1b8833a06651ae0 Mon Sep 17 00:00:00 2001 From: Arne Schwabe Date: Mon, 16 Apr 2012 19:21:14 +0200 Subject: Initial import --- src/com/lamerman/FileDialog.java | 389 +++++++++++++++++++ src/com/lamerman/SelectionMode.java | 7 + src/de/blinkt/openvpn/AboutActivity.java | 22 ++ src/de/blinkt/openvpn/FileSelectLayout.java | 75 ++++ src/de/blinkt/openvpn/LogWindow.java | 37 ++ src/de/blinkt/openvpn/OpenVPN.java | 78 ++++ src/de/blinkt/openvpn/OpenVPNClient.java | 567 ++++++++++++++++++++++++++++ src/de/blinkt/openvpn/OpenVpnService.java | 203 ++++++++++ src/de/blinkt/openvpn/VpnProfile.java | 8 + 9 files changed, 1386 insertions(+) create mode 100644 src/com/lamerman/FileDialog.java create mode 100644 src/com/lamerman/SelectionMode.java create mode 100644 src/de/blinkt/openvpn/AboutActivity.java create mode 100644 src/de/blinkt/openvpn/FileSelectLayout.java create mode 100644 src/de/blinkt/openvpn/LogWindow.java create mode 100644 src/de/blinkt/openvpn/OpenVPN.java create mode 100644 src/de/blinkt/openvpn/OpenVPNClient.java create mode 100644 src/de/blinkt/openvpn/OpenVpnService.java create mode 100644 src/de/blinkt/openvpn/VpnProfile.java (limited to 'src') diff --git a/src/com/lamerman/FileDialog.java b/src/com/lamerman/FileDialog.java new file mode 100644 index 00000000..821b1fbf --- /dev/null +++ b/src/com/lamerman/FileDialog.java @@ -0,0 +1,389 @@ +package com.lamerman; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.TreeMap; + +import de.blinkt.openvpn.R; + +import android.app.AlertDialog; +import android.app.ListActivity; +import android.content.DialogInterface; +import android.os.Bundle; +import android.view.KeyEvent; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.inputmethod.InputMethodManager; +import android.widget.Button; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.ListView; +import android.widget.SimpleAdapter; +import android.widget.TextView; + +/** + * Activity para escolha de arquivos/diretorios. + * + * @author android + * + */ +public class FileDialog extends ListActivity { + + /** + * Chave de um item da lista de paths. + */ + private static final String ITEM_KEY = "key"; + + /** + * Imagem de um item da lista de paths (diretorio ou arquivo). + */ + private static final String ITEM_IMAGE = "image"; + + /** + * Diretorio raiz. + */ + private static final String ROOT = "/"; + + /** + * Parametro de entrada da Activity: path inicial. Padrao: ROOT. + */ + public static final String START_PATH = "START_PATH"; + + /** + * Parametro de entrada da Activity: filtro de formatos de arquivos. Padrao: + * null. + */ + public static final String FORMAT_FILTER = "FORMAT_FILTER"; + + /** + * Parametro de saida da Activity: path escolhido. Padrao: null. + */ + public static final String RESULT_PATH = "RESULT_PATH"; + + /** + * Parametro de entrada da Activity: tipo de selecao: pode criar novos paths + * ou nao. Padrao: nao permite. + * + * @see {@link SelectionMode} + */ + public static final String SELECTION_MODE = "SELECTION_MODE"; + + /** + * Parametro de entrada da Activity: se e permitido escolher diretorios. + * Padrao: falso. + */ + public static final String CAN_SELECT_DIR = "CAN_SELECT_DIR"; + + private List path = null; + private TextView myPath; + private EditText mFileName; + private ArrayList> mList; + + private Button selectButton; + + private LinearLayout layoutSelect; + private LinearLayout layoutCreate; + private InputMethodManager inputManager; + private String parentPath; + private String currentPath = ROOT; + + private int selectionMode = SelectionMode.MODE_CREATE; + + private String[] formatFilter = null; + + private boolean canSelectDir = false; + + private File selectedFile; + private HashMap lastPositions = new HashMap(); + + /** + * Called when the activity is first created. Configura todos os parametros + * de entrada e das VIEWS.. + */ + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setResult(RESULT_CANCELED, getIntent()); + + setContentView(R.layout.file_dialog_main); + myPath = (TextView) findViewById(R.id.path); + mFileName = (EditText) findViewById(R.id.fdEditTextFile); + + inputManager = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); + + selectButton = (Button) findViewById(R.id.fdButtonSelect); + selectButton.setEnabled(false); + selectButton.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + if (selectedFile != null) { + getIntent().putExtra(RESULT_PATH, selectedFile.getPath()); + setResult(RESULT_OK, getIntent()); + finish(); + } + } + }); + + final Button newButton = (Button) findViewById(R.id.fdButtonNew); + newButton.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + setCreateVisible(v); + + mFileName.setText(""); + mFileName.requestFocus(); + } + }); + + selectionMode = getIntent().getIntExtra(SELECTION_MODE, SelectionMode.MODE_CREATE); + + formatFilter = getIntent().getStringArrayExtra(FORMAT_FILTER); + + canSelectDir = getIntent().getBooleanExtra(CAN_SELECT_DIR, false); + + if (selectionMode == SelectionMode.MODE_OPEN) { + newButton.setEnabled(false); + newButton.setVisibility(View.GONE); + } + + layoutSelect = (LinearLayout) findViewById(R.id.fdLinearLayoutSelect); + layoutCreate = (LinearLayout) findViewById(R.id.fdLinearLayoutCreate); + layoutCreate.setVisibility(View.GONE); + + final Button cancelButton = (Button) findViewById(R.id.fdButtonCancel); + cancelButton.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + setSelectVisible(v); + } + + }); + final Button createButton = (Button) findViewById(R.id.fdButtonCreate); + createButton.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + if (mFileName.getText().length() > 0) { + getIntent().putExtra(RESULT_PATH, currentPath + "/" + mFileName.getText()); + setResult(RESULT_OK, getIntent()); + finish(); + } + } + }); + + String startPath = getIntent().getStringExtra(START_PATH); + startPath = startPath != null ? startPath : ROOT; + if (canSelectDir) { + File file = new File(startPath); + selectedFile = file; + selectButton.setEnabled(true); + } + getDir(startPath); + } + + private void getDir(String dirPath) { + + boolean useAutoSelection = dirPath.length() < currentPath.length(); + + Integer position = lastPositions.get(parentPath); + + getDirImpl(dirPath); + + if (position != null && useAutoSelection) { + getListView().setSelection(position); + } + + } + + /** + * Monta a estrutura de arquivos e diretorios filhos do diretorio fornecido. + * + * @param dirPath + * Diretorio pai. + */ + private void getDirImpl(final String dirPath) { + + currentPath = dirPath; + + final List item = new ArrayList(); + path = new ArrayList(); + mList = new ArrayList>(); + + File f = new File(currentPath); + File[] files = f.listFiles(); + if (files == null) { + currentPath = ROOT; + f = new File(currentPath); + files = f.listFiles(); + } + myPath.setText(getText(R.string.location) + ": " + currentPath); + + if (!currentPath.equals(ROOT)) { + + item.add(ROOT); + addItem(ROOT, R.drawable.folder); + path.add(ROOT); + + item.add("../"); + addItem("../", R.drawable.folder); + path.add(f.getParent()); + parentPath = f.getParent(); + + } + + TreeMap dirsMap = new TreeMap(); + TreeMap dirsPathMap = new TreeMap(); + TreeMap filesMap = new TreeMap(); + TreeMap filesPathMap = new TreeMap(); + for (File file : files) { + if (file.isDirectory()) { + String dirName = file.getName(); + dirsMap.put(dirName, dirName); + dirsPathMap.put(dirName, file.getPath()); + } else { + final String fileName = file.getName(); + final String fileNameLwr = fileName.toLowerCase(); + // se ha um filtro de formatos, utiliza-o + if (formatFilter != null) { + boolean contains = false; + for (int i = 0; i < formatFilter.length; i++) { + final String formatLwr = formatFilter[i].toLowerCase(); + if (fileNameLwr.endsWith(formatLwr)) { + contains = true; + break; + } + } + if (contains) { + filesMap.put(fileName, fileName); + filesPathMap.put(fileName, file.getPath()); + } + // senao, adiciona todos os arquivos + } else { + filesMap.put(fileName, fileName); + filesPathMap.put(fileName, file.getPath()); + } + } + } + item.addAll(dirsMap.tailMap("").values()); + item.addAll(filesMap.tailMap("").values()); + path.addAll(dirsPathMap.tailMap("").values()); + path.addAll(filesPathMap.tailMap("").values()); + + SimpleAdapter fileList = new SimpleAdapter(this, mList, R.layout.file_dialog_row, new String[] { + ITEM_KEY, ITEM_IMAGE }, new int[] { R.id.fdrowtext, R.id.fdrowimage }); + + for (String dir : dirsMap.tailMap("").values()) { + addItem(dir, R.drawable.folder); + } + + for (String file : filesMap.tailMap("").values()) { + addItem(file, R.drawable.file); + } + + fileList.notifyDataSetChanged(); + + setListAdapter(fileList); + + } + + private void addItem(String fileName, int imageId) { + HashMap item = new HashMap(); + item.put(ITEM_KEY, fileName); + item.put(ITEM_IMAGE, imageId); + mList.add(item); + } + + /** + * Quando clica no item da lista, deve-se: 1) Se for diretorio, abre seus + * arquivos filhos; 2) Se puder escolher diretorio, define-o como sendo o + * path escolhido. 3) Se for arquivo, define-o como path escolhido. 4) Ativa + * botao de selecao. + */ + @Override + protected void onListItemClick(ListView l, View v, int position, long id) { + + File file = new File(path.get(position)); + + setSelectVisible(v); + + if (file.isDirectory()) { + selectButton.setEnabled(false); + if (file.canRead()) { + lastPositions.put(currentPath, position); + getDir(path.get(position)); + if (canSelectDir) { + selectedFile = file; + v.setSelected(true); + selectButton.setEnabled(true); + } + } else { + new AlertDialog.Builder(this).setIcon(R.drawable.icon) + .setTitle("[" + file.getName() + "] " + getText(R.string.cant_read_folder)) + .setPositiveButton("OK", new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + + } + }).show(); + } + } else { + selectedFile = file; + v.setSelected(true); + selectButton.setEnabled(true); + } + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if ((keyCode == KeyEvent.KEYCODE_BACK)) { + selectButton.setEnabled(false); + + if (layoutCreate.getVisibility() == View.VISIBLE) { + layoutCreate.setVisibility(View.GONE); + layoutSelect.setVisibility(View.VISIBLE); + } else { + if (!currentPath.equals(ROOT)) { + getDir(parentPath); + } else { + return super.onKeyDown(keyCode, event); + } + } + + return true; + } else { + return super.onKeyDown(keyCode, event); + } + } + + /** + * Define se o botao de CREATE e visivel. + * + * @param v + */ + private void setCreateVisible(View v) { + layoutCreate.setVisibility(View.VISIBLE); + layoutSelect.setVisibility(View.GONE); + + inputManager.hideSoftInputFromWindow(v.getWindowToken(), 0); + selectButton.setEnabled(false); + } + + /** + * Define se o botao de SELECT e visivel. + * + * @param v + */ + private void setSelectVisible(View v) { + layoutCreate.setVisibility(View.GONE); + layoutSelect.setVisibility(View.VISIBLE); + + inputManager.hideSoftInputFromWindow(v.getWindowToken(), 0); + selectButton.setEnabled(false); + } +} diff --git a/src/com/lamerman/SelectionMode.java b/src/com/lamerman/SelectionMode.java new file mode 100644 index 00000000..3c05dfa8 --- /dev/null +++ b/src/com/lamerman/SelectionMode.java @@ -0,0 +1,7 @@ +package com.lamerman; + +public class SelectionMode { + public static final int MODE_CREATE = 0; + + public static final int MODE_OPEN = 1; +} diff --git a/src/de/blinkt/openvpn/AboutActivity.java b/src/de/blinkt/openvpn/AboutActivity.java new file mode 100644 index 00000000..0361d67c --- /dev/null +++ b/src/de/blinkt/openvpn/AboutActivity.java @@ -0,0 +1,22 @@ +package de.blinkt.openvpn; + +import android.app.Activity; +import android.os.Bundle; +import android.view.View; +import android.view.View.OnClickListener; + +public class AboutActivity extends Activity implements OnClickListener { + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.about); + + //findViewById(R.layout.about).setOnClickListener(this); + } + + @Override + public void onClick(View v) { + finish(); + } +} diff --git a/src/de/blinkt/openvpn/FileSelectLayout.java b/src/de/blinkt/openvpn/FileSelectLayout.java new file mode 100644 index 00000000..0615b2ab --- /dev/null +++ b/src/de/blinkt/openvpn/FileSelectLayout.java @@ -0,0 +1,75 @@ +package de.blinkt.openvpn; + +import com.lamerman.FileDialog; +import com.lamerman.SelectionMode; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.TextView; + +import android.view.View; +import android.view.View.OnClickListener; + +public class FileSelectLayout extends LinearLayout implements OnClickListener { + + private TextView mData; + private Activity mActivity; + private int mTaskId; + private Button mSelectButton; + + public FileSelectLayout( Context context,AttributeSet attrset) { + super(context,attrset); + inflate(getContext(), R.layout.file_select, this); + + TypedArray ta = context.obtainStyledAttributes(attrset,R.styleable.FileSelectLayout); + + String title = ta.getString(R.styleable.FileSelectLayout_title); + + TextView tview = (TextView) findViewById(R.id.file_title); + tview.setText(title); + + mData = (TextView) findViewById(R.id.file_selected_item); + mSelectButton = (Button) findViewById(R.id.file_select_button); + mSelectButton.setOnClickListener(this); + + } + + public void setActivity(Activity a, int i) + { + mTaskId = i; + mActivity = a; + } + + public void getCertificateFileDialog() { + Intent startFC = new Intent(getContext(),FileDialog.class); + startFC.putExtra(FileDialog.START_PATH, "/sdcard"); + startFC.putExtra(FileDialog.SELECTION_MODE, SelectionMode.MODE_OPEN); + + mActivity.startActivityForResult(startFC,mTaskId); + } + + + public String getData() { + return mData.getText().toString(); + } + + public void setData(String data) { + mData.setText(data); + + } + + @Override + public void onClick(View v) { + if(v == mSelectButton) { + getCertificateFileDialog(); + } + } + + + +} diff --git a/src/de/blinkt/openvpn/LogWindow.java b/src/de/blinkt/openvpn/LogWindow.java new file mode 100644 index 00000000..a26da2ba --- /dev/null +++ b/src/de/blinkt/openvpn/LogWindow.java @@ -0,0 +1,37 @@ +package de.blinkt.openvpn; + +import android.app.ListActivity; +import android.os.Bundle; +import android.view.View; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.ArrayAdapter; +import android.widget.ListView; + + + +public class LogWindow extends ListActivity implements OnItemClickListener { + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + //setListAdapter(new ArrayAdapter(this, R.layout.log_entry, COUNTRIES)); + setListAdapter(new ArrayAdapter(this, R.layout.log_entry, OpenVPN.getlogbuffer())); + //setListAdapter(new ArrayAdapter(this, android.R.layout.simple_list_item_1, OpenVPN.logbuffer)); + + ListView lv = getListView(); + lv.setTextFilterEnabled(true); + + lv.setOnItemClickListener(this); + } + + @Override + public void onItemClick(AdapterView parent, View view, + int position, long id) { + // When clicked, show a toast with the TextView text + //Toast.makeText(getApplicationContext(), ((TextView) view).getText(), + //Toast.LENGTH_SHORT).show(); + + setListAdapter(new ArrayAdapter(this, R.layout.log_entry, OpenVPN.getlogbuffer())); + } +} diff --git a/src/de/blinkt/openvpn/OpenVPN.java b/src/de/blinkt/openvpn/OpenVPN.java new file mode 100644 index 00000000..7385726b --- /dev/null +++ b/src/de/blinkt/openvpn/OpenVPN.java @@ -0,0 +1,78 @@ +package de.blinkt.openvpn; + +import java.util.LinkedList; + +import android.os.ParcelFileDescriptor; +import android.util.Log; + +public class OpenVPN { + private static OpenVpnService mOpenVpnService; + private static String localip; + private static final int MAXLOGENTRIES = 500; + public static native int startOpenVPNThread(); + public static native int startOpenVPNThreadArgs(String argv[]); + private static final String TAG = "OpenVpn"; + + + public static LinkedList logbuffer = new LinkedList(); + private static int counter=0; + + + static { + System.loadLibrary("crypto"); + System.loadLibrary("ssl"); + System.loadLibrary("lzo"); + System.loadLibrary("openvpn"); + } + + static void addRoute(String dest,String mask, String gw) { + Log.i("openvpn" ,"Got Routing information " + dest + " " + mask + " " + gw ); + } + + synchronized static void logMessage(int level,String prefix, String message) + { + logbuffer.addFirst(prefix + " " + message); + if(logbuffer.size()>MAXLOGENTRIES) + logbuffer.removeLast(); + + // The garbage collector does not collect the String from native + // but kills me for logging 100 messages with too many references :( + // Force GC how and then to kill loose ends + if(counter++ % 50==0) + System.gc(); + } + + + static void addInterfaceInfo(int mtu, String local, String remote) + { + Log.i("openvpn","Got interface info M" + mtu + " L: " + local + "R: " + remote); + localip=local; + } + + public static void setCallback(OpenVpnService openVpnService) { + mOpenVpnService = openVpnService; + } + + public static boolean protectSocket (int sockfd) + { + boolean p = mOpenVpnService.protect(sockfd); + if(p) + Log.d("openvpn","Protected socket "+ sockfd); + else + Log.e("openvpn","Error protecting socket "+ sockfd); + return p; + } + + public static int openTunDevice() { + Log.d(TAG,"Opening tun device"); + ParcelFileDescriptor pfd = mOpenVpnService.openTun(localip); + return pfd.detachFd(); + } + //! Dummy method being called to force loading of JNI Libraries + public static void foo() { } + + synchronized public static String[] getlogbuffer() { + return (String[]) logbuffer.toArray(new String[logbuffer.size()]); + + } +} diff --git a/src/de/blinkt/openvpn/OpenVPNClient.java b/src/de/blinkt/openvpn/OpenVPNClient.java new file mode 100644 index 00000000..af949c2d --- /dev/null +++ b/src/de/blinkt/openvpn/OpenVPNClient.java @@ -0,0 +1,567 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * 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 de.blinkt.openvpn; + +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.HashMap; +import java.util.Random; +import java.util.Vector; + +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.net.VpnService; +import android.os.Bundle; +import android.os.Handler; +import android.os.Handler.Callback; +import android.os.Message; +import android.security.KeyChain; +import android.security.KeyChainAliasCallback; +import android.security.KeyChainException; +import android.util.Log; +import android.view.View; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemSelectedListener; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.CompoundButton.OnCheckedChangeListener; +import android.widget.Spinner; +import android.widget.TextView; +import android.widget.ToggleButton; + +import com.lamerman.FileDialog; + +import de.blinkt.openvpn.R.id; + +public class OpenVPNClient extends Activity implements View.OnClickListener, OnItemSelectedListener, Callback, OnCheckedChangeListener { + private static final String TAG = "OpenVpnClient"; + + + private static final int START_OPENVPN = 0; + private static final int CHOOSE_FILE_OFFSET = 1000; + private static final int UPDATE_ALIAS = 20; + + private static final String PREFS_NAME = "OVPN_SERVER"; + + private static final String OVPNCONFIGFILE = "android.conf"; + private static final String OVPNCONFIGPKCS12 = "android.pkcs12"; + + + private TextView mServerAddress; + private TextView mServerPort; + private FileSelectLayout mClientCert; + private FileSelectLayout mCaCert; + private FileSelectLayout mClientKey; + private TextView mAliasName; + private CheckBox mUseLzo; + private ToggleButton mTcpUdp; + private Spinner mType; + private String certalias; + private FileSelectLayout mpkcs12; + private TextView mPKCS12Password; + + private Handler mHandler; + + + private CheckBox mUseTlsAuth; + + + private CheckBox mShowAdvanced; + + + private FileSelectLayout mTlsFile; + + private HashMap fileselects = new HashMap(); + + + private Spinner mTLSDirection; + + @Override + protected void onStop(){ + super.onStop(); + savePreferences(); + } + + + public void writeConfigFile() + { + + try { + FileWriter cfg = new FileWriter(getCacheDir().getAbsolutePath() + "/" + OVPNCONFIGFILE); + + + + // TODO "--remote-cert-eku", "TLS Web Server Authentication" + + + // The stoned way of java to return an array from a vector + // brought to you by eclipse auto complete + + cfg.write("client\n"); + cfg.write("verb 2\n"); + + + // /tmp does not exist on Android + cfg.write("tmp-dir "); + cfg.write(getCacheDir().getAbsolutePath()); + cfg.write("\n"); + + // quit after 5 tries + cfg.write("--connect-retry-max 5\n"); + cfg.write("--resolv-retry 5\n"); + + + + // We cannot use anything else than tun + cfg.write("dev tun\n"); + + // Server Address + cfg.write("remote "); + cfg.write(mServerAddress.getText().toString()); + cfg.write(" "); + cfg.write(mServerPort.getText().toString()); + if(mTcpUdp.isChecked()) + cfg.write(" udp\n"); + else + cfg.write(" tcp\n"); + + + + switch(mType.getSelectedItemPosition()) { + case VpnProfile.TYPE_CERTIFICATES: + // Ca + cfg.write("ca "); + cfg.write(mCaCert.getData()); + cfg.write("\n"); + + // Client Cert + Key + cfg.write("key "); + cfg.write(mClientKey.getData()); + cfg.write("\n"); + cfg.write("cert "); + cfg.write(mClientCert.getData()); + cfg.write("\n"); + break; + case VpnProfile.TYPE_PKCS12: + cfg.write("pkcs12 "); + cfg.write(mpkcs12.getData()); + cfg.write("\n"); + cfg.write("management-query-passwords\n"); + break; + + case VpnProfile.TYPE_KEYSTORE: + cfg.write("pkcs12 "); + cfg.write(getCacheDir().getAbsolutePath() + "/" + OVPNCONFIGPKCS12); + cfg.write("\n"); + cfg.write("management-query-passwords\n"); + break; + + } + + if(mUseLzo.isChecked()) { + cfg.write("comp-lzo\n"); + } + + if(mUseTlsAuth.isChecked()) { + cfg.write("tls-auth "); + cfg.write(mTlsFile.getData()); + int tlsdir= mTLSDirection.getSelectedItemPosition(); + // 2 is unspecified + if(tlsdir == 1 || tlsdir==2) { + cfg.write(" "); + cfg.write(new Integer(tlsdir).toString()); + } + cfg.write("\n"); + } + cfg.flush(); + cfg.close(); + + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + } + + private void addFileSelectLayout (FileSelectLayout fsl) { + int i = fileselects.size() + CHOOSE_FILE_OFFSET; + fileselects.put(i, fsl); + fsl.setActivity(this,i); + } + + public String[] buildOpenvpnArgv() + { + Vector args = new Vector(); + + // Add fixed paramenters + args.add("openvpn"); + + // Enable managment interface to + // stop openvpn + args.add("--management"); + + args.add(getCacheDir().getAbsolutePath() + "/" + "mgmtsocket"); + args.add("unix"); + //args.add("--management-hold"); + + args.add("--config"); + args.add(getCacheDir().getAbsolutePath() + "/" + OVPNCONFIGFILE); + + + return (String[]) args.toArray(new String[args.size()]); + } + + + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.config); + + // Forces early JNI Load + OpenVPN.foo(); + + mServerAddress = (TextView) findViewById(R.id.address); + mServerPort = (TextView) findViewById(R.id.port); + mClientCert = (FileSelectLayout) findViewById(R.id.certselect); + mClientKey = (FileSelectLayout) findViewById(R.id.keyselect); + mCaCert = (FileSelectLayout) findViewById(R.id.certselect); + mpkcs12 = (FileSelectLayout) findViewById(R.id.pkcs12select); + mUseLzo = (CheckBox) findViewById(R.id.lzo); + mTcpUdp = (ToggleButton) findViewById(id.tcpudp); + mType = (Spinner) findViewById(R.id.type); + mPKCS12Password = (TextView) findViewById(R.id.pkcs12password); + mAliasName = (TextView) findViewById(R.id.aliasname); + mUseTlsAuth = (CheckBox) findViewById(R.id.useTLSAuth); + mTLSDirection = (Spinner) findViewById(R.id.tls_direction); + + mShowAdvanced = (CheckBox) findViewById(R.id.show_advanced); + mTlsFile = (FileSelectLayout) findViewById(R.id.tlsAuth); + + + + addFileSelectLayout(mCaCert); + addFileSelectLayout(mClientCert); + addFileSelectLayout(mClientKey); + addFileSelectLayout(mTlsFile); + addFileSelectLayout(mpkcs12); + + loadPreferences(); + + mType.setOnItemSelectedListener(this); + + mShowAdvanced.setOnCheckedChangeListener(this); + mUseTlsAuth.setOnCheckedChangeListener(this); + + + findViewById(R.id.select_keystore_button).setOnClickListener(this); + findViewById(R.id.about).setOnClickListener(this); + findViewById(R.id.connect).setOnClickListener(this); + + if (mHandler == null) { + mHandler = new Handler(this); + } + } + + + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + if (parent == mType) { + changeType(position); + } + } + + + + private void changeType(int type){ + // hide everything + findViewById(R.id.pkcs12).setVisibility(View.GONE); + findViewById(R.id.certs).setVisibility(View.GONE); + findViewById(R.id.commonsecret).setVisibility(View.GONE); + findViewById(R.id.keystore).setVisibility(View.GONE); + + switch(type) { + case VpnProfile.TYPE_CERTIFICATES: + findViewById(R.id.certs).setVisibility(View.VISIBLE); + break; + case VpnProfile.TYPE_PKCS12: + findViewById(R.id.pkcs12).setVisibility(View.VISIBLE); + break; + case VpnProfile.COMMON_SECRET: + findViewById(R.id.commonsecret).setVisibility(View.VISIBLE); + break; + case VpnProfile.TYPE_KEYSTORE: + findViewById(R.id.keystore).setVisibility(View.VISIBLE); + break; + } + + + } + + private void loadPreferences() { + SharedPreferences settings = getSharedPreferences(PREFS_NAME,Activity.MODE_PRIVATE); + + mClientCert.setData(settings.getString("clientcert", "")); + mClientKey.setData(settings.getString("clientkey", "")); + mCaCert.setData(settings.getString("ca", "")); + + mUseLzo.setChecked(settings.getBoolean("useLzo", true)); + mServerPort.setText(settings.getString("port", "1194")); + mServerAddress.setText(settings.getString("server", "openvpn.blinkt.de")); + mTcpUdp.setChecked(settings.getBoolean("udp", true)); + mType.setSelection(settings.getInt("type", VpnProfile.TYPE_PKCS12)); + mpkcs12.setData(settings.getString("pkcs12file", "")); + mPKCS12Password.setText(settings.getString("pkcs12password", "")); + certalias = settings.getString("alias", null); + mUseTlsAuth.setChecked(settings.getBoolean("tlsauth", false)); + onCheckedChanged(mUseTlsAuth,mUseTlsAuth.isChecked()); + + mTlsFile.setData(settings.getString("tlsfile","")); + mTLSDirection.setSelection(settings.getInt("tls-direction", 2)); // Unspecified + setAlias(); + + } + + private void savePreferences() { + // We need an Editor object to make preference changes. + // All objects are from android.context.Context + SharedPreferences settings = getSharedPreferences(PREFS_NAME,Activity.MODE_PRIVATE); + SharedPreferences.Editor editor = settings.edit(); + + + editor.putString("ca" , mCaCert.getData()); + editor.putString("clientcert", mClientCert.getData()); + editor.putString("clientkey", mClientKey.getData()); + + editor.putBoolean("useLzo",mUseLzo.isChecked()); + editor.putString("port", mServerPort.getText().toString()); + editor.putString("server", mServerAddress.getText().toString()); + editor.putBoolean("udp", mTcpUdp.isChecked()); + + editor.putInt("type",mType.getSelectedItemPosition()); + editor.putString("pkcs12file", mpkcs12.getData()); + editor.putString("pkcs12password", mPKCS12Password.getText().toString()); + editor.putString("alias", certalias); + editor.putBoolean("tlsauth", mUseTlsAuth.isChecked()); + editor.putString("tlsfile", mTlsFile.getData()); + editor.putInt("tls-direction", mTLSDirection.getSelectedItemPosition()); + // Commit the edits! + editor.commit(); + } + + + private void setAlias() { + if(certalias == null) { + mAliasName.setText(R.string.client_no_certificate); + } else { + mAliasName.setText(certalias); + } + } + + public void showCertDialog () { + KeyChain.choosePrivateKeyAlias(this, + new KeyChainAliasCallback() { + + public void alias(String alias) { + // Credential alias selected. Remember the alias selection for future use. + certalias=alias; + mHandler.sendEmptyMessage(UPDATE_ALIAS); + } + + + }, + new String[] {"RSA", "DSA"}, // List of acceptable key types. null for any + null, // issuer, null for any + "internal.example.com", // host name of server requesting the cert, null if unavailable + 443, // port of server requesting the cert, -1 if unavailable + null); // alias to preselect, null if unavailable + } + + private String getRandomPW() { + String pw= ""; + // Put enough digits togher to make a password :) + Random r = new Random(); + for(int i=0;i < 4;i++) { + pw += new Integer(r.nextInt(1000)).toString(); + } + + return pw; + + } + + private String savePKCS12() { + Context context = getBaseContext(); + PrivateKey privateKey = null; + X509Certificate[] cachain=null; + try { + privateKey = KeyChain.getPrivateKey(context,certalias); + cachain = KeyChain.getCertificateChain(context, certalias); + + KeyStore ks = KeyStore.getInstance("PKCS12"); + ks.load(null, null); + ks.setKeyEntry("usercert", privateKey, null, cachain); + String mypw = getRandomPW(); + FileOutputStream fout = new FileOutputStream(getCacheDir().getAbsolutePath() + "/" + OVPNCONFIGPKCS12); + ks.store(fout,mypw.toCharArray()); + fout.flush(); fout.close(); + return mypw; + } catch (KeyChainException e) { + e.printStackTrace(); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (KeyStoreException e) { + e.printStackTrace(); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } catch (CertificateException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + return "ERROR"; + + } + + public void testGetallCerts() throws NoSuchAlgorithmException, KeyStoreException { + TrustManagerFactory tmf = TrustManagerFactory + .getInstance(TrustManagerFactory.getDefaultAlgorithm()); + + tmf.init((KeyStore) null); + X509TrustManager xtm = (X509TrustManager) tmf.getTrustManagers()[0]; + + X509Certificate[] foo = xtm.getAcceptedIssuers(); + for (X509Certificate cert : xtm.getAcceptedIssuers()) { + String certStr = "S:" + cert.getSubjectDN().getName() + "\nI:" + + cert.getIssuerDN().getName(); + Log.d(TAG, certStr); + } + System.out.println(foo); + } + + @Override + public void onClick(View v) { + if(v == findViewById(R.id.connect)) { + Intent intent = VpnService.prepare(this); + if (intent != null) { + startActivityForResult(intent, 0); + } else { + onActivityResult(START_OPENVPN, RESULT_OK, null); + } + } else if (v == findViewById(R.id.about)) { + Intent intent = new Intent(getBaseContext(),AboutActivity.class); + startActivity(intent); + } else if (v == findViewById(R.id.select_keystore_button)) { + showCertDialog(); + } + } + + void startOpenVpn() { + String prefix = getPackageName(); + writeConfigFile(); + + Intent intent = new Intent(this, OpenVpnService.class) + .putExtra(prefix + ".ARGV" , buildOpenvpnArgv()); + + if(mType.getSelectedItemPosition()== VpnProfile.TYPE_PKCS12){ + intent.putExtra(prefix + ".PKCS12PASS", + mPKCS12Password.getText().toString()); + } + + if(mType.getSelectedItemPosition() == VpnProfile.TYPE_KEYSTORE) { + String pkcs12pw = savePKCS12(); + intent.putExtra(prefix + ".PKCS12PASS", pkcs12pw); + } + + + startService(intent); + Intent startLW = new Intent(getBaseContext(),LogWindow.class); + startActivity(startLW); + } + + /* (non-Javadoc) + * @see android.app.Activity#onActivityResult(int, int, android.content.Intent) + */ + @Override + protected void onActivityResult(int request, int result, Intent data) { + if (request== START_OPENVPN) { + if (result == RESULT_OK) { + // It always crashes and never saves ;) + savePreferences(); + new startOpenVpnThread().start(); + } + + } else if (request >= CHOOSE_FILE_OFFSET) { + String filepath = data.getStringExtra(FileDialog.RESULT_PATH); + FileSelectLayout fsl = fileselects.get(request); + fsl.setData(filepath); + } + savePreferences(); + } + + + + private class startOpenVpnThread extends Thread { + + @Override + public void run() { + startOpenVpn(); + } + + } + + @Override + public void onNothingSelected(AdapterView parent) { + } + + + @Override + public boolean handleMessage(Message msg) { + setAlias(); + return true; + } + + + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + int visibility; + if(isChecked) + visibility =View.VISIBLE; + else + visibility =View.GONE; + + if(buttonView==mShowAdvanced) { + findViewById(R.id.advanced_options).setVisibility(visibility); + } else if (buttonView == mUseTlsAuth) { + findViewById(R.id.tlsauth_options).setVisibility(visibility); + } + } +} diff --git a/src/de/blinkt/openvpn/OpenVpnService.java b/src/de/blinkt/openvpn/OpenVpnService.java new file mode 100644 index 00000000..e8174bcb --- /dev/null +++ b/src/de/blinkt/openvpn/OpenVpnService.java @@ -0,0 +1,203 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * 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 de.blinkt.openvpn; + +import java.io.IOException; +import java.net.UnknownHostException; +import java.util.Arrays; + +import android.app.PendingIntent; +import android.content.Intent; +import android.net.LocalSocket; +import android.net.LocalSocketAddress; +import android.net.VpnService; +import android.os.Handler; +import android.os.Message; +import android.os.ParcelFileDescriptor; +import android.util.Log; +import android.widget.Toast; + +public class OpenVpnService extends VpnService implements Handler.Callback, Runnable { + private static final String TAG = "OpenVpnService"; + + private String[] mArgv; + + private Handler mHandler; + private Thread mThread; + + private ParcelFileDescriptor mInterface; + + @Override + public void onRevoke() { + managmentCommand("signal SIGINT\n"); + mThread=null; + stopSelf(); + }; + + + public void managmentCommand(String cmd) { + LocalSocket mgmtsocket; + try { + byte[] buffer = new byte[400]; + mgmtsocket = new LocalSocket(); + + mgmtsocket.connect(new LocalSocketAddress(getCacheDir().getAbsolutePath() + "/" + "mgmtsocket", + LocalSocketAddress.Namespace.FILESYSTEM)); + //mgmtsocket = new Dat("127.0.0.1",OpenVPNClient.MANAGMENTPORT)); + + //OutputStreamWriter outw = new OutputStreamWriter(mgmtsocket.getOutputStream()); + mgmtsocket.getInputStream().read(buffer); + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + } + //outw.write(cmd); + mgmtsocket.getOutputStream().write(cmd.getBytes()); + //outw.flush(); + try { + Thread.sleep(400); + } catch (InterruptedException e) { + } + + mgmtsocket.close(); + } catch (UnknownHostException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + // The handler is only used to show messages. + if (mHandler == null) { + mHandler = new Handler(this); + } + + // Stop the previous session by interrupting the thread. + if (mThread != null) { + managmentCommand("signal SIGINT\n"); + mThread.interrupt(); + } + + // Thread already running, reuse existing, + + // Extract information from the intent. + String prefix = getPackageName(); + mArgv = intent.getStringArrayExtra(prefix + ".ARGV"); + + + // Start a new session by creating a new thread. + mThread = new Thread(this, "OpenVPNThread"); + mThread.start(); + + if(intent.hasExtra(prefix +".PKCS12PASS")) + { + try { + String pkcs12password = intent.getStringExtra(prefix +".PKCS12PASS"); + Thread.sleep(3000); + + managmentCommand("password 'Private Key' " + pkcs12password + "\n"); + } catch (InterruptedException e) { + } + + } + + + + return START_STICKY; + } + + @Override + public void onDestroy() { + if (mThread != null) { + managmentCommand("signal SIGINT\n"); + + mThread.interrupt(); + } + } + + @Override + public boolean handleMessage(Message message) { + if (message != null) { + Toast.makeText(this, message.what, Toast.LENGTH_SHORT).show(); + } + return true; + } + + @Override + public synchronized void run() { + try { + Log.i(TAG, "Starting o"); + + + OpenVPN.setCallback(this); + + + // We try to create the tunnel for several times. The better way + // is to work with ConnectivityManager, such as trying only when + // the network is avaiable. Here we just use a counter to keep + // things simple. + //for (int attempt = 0; attempt < 10; ++attempt) { + mHandler.sendEmptyMessage(R.string.connecting); + + // Log argv + + OpenVPN.logMessage(0, "argv:" , Arrays.toString(mArgv)); + + OpenVPN.startOpenVPNThreadArgs(mArgv); + + + + // Sleep for a while. This also checks if we got interrupted. + Thread.sleep(3000); + //} + Log.i(TAG, "Giving up"); + } catch (Exception e) { + Log.e(TAG, "Got " + e.toString()); + } finally { + try { + mInterface.close(); + } catch (Exception e) { + // ignore + } + mInterface = null; + + + mHandler.sendEmptyMessage(R.string.disconnected); + Log.i(TAG, "Exiting"); + } + } + + + public ParcelFileDescriptor openTun(String localip) { + // FIXME: hardcoded assumptions + Builder builder = new Builder(); + builder.addRoute("0.0.0.0", 0); + builder.addAddress(localip, 24 ); + builder.addDnsServer("131.234.137.23"); + builder.addSearchDomain("blinkt.de"); + builder.setSession("OpenVPN - " + localip); + Intent intent = new Intent(getBaseContext(),LogWindow.class); + PendingIntent startLW = PendingIntent.getActivity(getApplicationContext(), 0, intent, 0); + builder.setConfigureIntent(startLW); + mInterface = builder.establish(); + return mInterface; + + } +} diff --git a/src/de/blinkt/openvpn/VpnProfile.java b/src/de/blinkt/openvpn/VpnProfile.java new file mode 100644 index 00000000..d01b1104 --- /dev/null +++ b/src/de/blinkt/openvpn/VpnProfile.java @@ -0,0 +1,8 @@ +package de.blinkt.openvpn; + +public class VpnProfile { + static final int TYPE_CERTIFICATES=0; + static final int TYPE_PKCS12=1; + static final int COMMON_SECRET=3; + static final int TYPE_KEYSTORE=2; +} -- cgit v1.2.3