diff --git a/shadowsocks-csharp/Controller/Service/NamedPipeServer.cs b/shadowsocks-csharp/Controller/Service/NamedPipeServer.cs new file mode 100644 index 00000000..23689231 --- /dev/null +++ b/shadowsocks-csharp/Controller/Service/NamedPipeServer.cs @@ -0,0 +1,47 @@ +using System; +using System.IO; +using System.IO.Pipes; +using System.Net; +using System.Text; + +namespace Shadowsocks.Controller +{ + class RequestAddUrlEventArgs : EventArgs + { + public readonly string Url; + + public RequestAddUrlEventArgs(string url) + { + this.Url = url; + } + } + + internal class NamedPipeServer + { + public event EventHandler AddUrlRequested; + public async void Run(string path) + { + byte[] buf = new byte[4096]; + while (true) + { + using (NamedPipeServerStream stream = new NamedPipeServerStream(path)) + { + stream.WaitForConnection(); + await stream.ReadAsync(buf, 0, 4); + int opcode = IPAddress.NetworkToHostOrder(BitConverter.ToInt32(buf, 0)); + if (opcode == 1) + { + await stream.ReadAsync(buf, 0, 4); + int strlen = IPAddress.NetworkToHostOrder(BitConverter.ToInt32(buf, 0)); + + await stream.ReadAsync(buf, 0, strlen); + string url = Encoding.UTF8.GetString(buf, 0, strlen); + + AddUrlRequested?.Invoke(this, new RequestAddUrlEventArgs(url)); + } + stream.Close(); + } + } + } + } +} diff --git a/shadowsocks-csharp/Controller/ShadowsocksController.cs b/shadowsocks-csharp/Controller/ShadowsocksController.cs index f4ea1294..e1df6186 100644 --- a/shadowsocks-csharp/Controller/ShadowsocksController.cs +++ b/shadowsocks-csharp/Controller/ShadowsocksController.cs @@ -218,6 +218,25 @@ namespace Shadowsocks.Controller StatisticsStrategyConfiguration.Save(configuration); } + public bool AskAddServerBySSURL(string ssURL) + { + var dr = MessageBox.Show(I18N.GetString("Import from URL: {0} ?", ssURL), I18N.GetString("Shadowsocks"), MessageBoxButtons.YesNo); + if (dr == DialogResult.Yes) + { + if (AddServerBySSURL(ssURL)) + { + MessageBox.Show(I18N.GetString("Successfully imported from {0}", ssURL)); + return true; + } + else + { + MessageBox.Show(I18N.GetString("Failed to import. Please check if the link is valid.")); + } + } + return false; + } + + public bool AddServerBySSURL(string ssURL) { try diff --git a/shadowsocks-csharp/Controller/System/ProtocolHandler.cs b/shadowsocks-csharp/Controller/System/ProtocolHandler.cs new file mode 100644 index 00000000..2a0112cb --- /dev/null +++ b/shadowsocks-csharp/Controller/System/ProtocolHandler.cs @@ -0,0 +1,107 @@ +using Microsoft.Win32; +using NLog; +using Shadowsocks.Util; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace Shadowsocks.Controller +{ + static class ProtocolHandler + { + const string ssURLRegKey = @"SOFTWARE\Classes\ss"; + + private static Logger logger = LogManager.GetCurrentClassLogger(); + + // Don't use Application.ExecutablePath + // see https://stackoverflow.com/questions/12945805/odd-c-sharp-path-issue + private static readonly string ExecutablePath = Assembly.GetEntryAssembly().Location; + + // TODO: Elevate when necessary + public static bool Set(bool enabled) + { + RegistryKey ssURLAssociation = null; + + try + { + ssURLAssociation = Registry.CurrentUser.CreateSubKey(ssURLRegKey, RegistryKeyPermissionCheck.ReadWriteSubTree); + if (ssURLAssociation == null) + { + logger.Error(@"Failed to create HKCU\SOFTWARE\Classes\ss to register ss:// association."); + return false; + } + if (enabled) + { + ssURLAssociation.SetValue("", "URL:Shadowsocks"); + ssURLAssociation.SetValue("URL Protocol", ""); + var shellOpen = ssURLAssociation.CreateSubKey("shell").CreateSubKey("open").CreateSubKey("command"); + shellOpen.SetValue("", $"{ExecutablePath} --open-url %1"); + logger.Info(@"Successfully added ss:// association."); + } + else + { + Registry.CurrentUser.DeleteSubKeyTree(ssURLRegKey); + logger.Info(@"Successfully removed ss:// association."); + } + return true; + } + catch (Exception e) + { + logger.LogUsefulException(e); + return false; + } + finally + { + if (ssURLAssociation != null) + { + try + { + ssURLAssociation.Close(); + ssURLAssociation.Dispose(); + } + catch (Exception e) + { logger.LogUsefulException(e); } + } + } + } + + public static bool Check() + { + RegistryKey ssURLAssociation = null; + try + { + ssURLAssociation = Registry.CurrentUser.OpenSubKey(ssURLRegKey, true); + if (ssURLAssociation == null) + { + //logger.Info(@"ss:// links not associated."); + return false; + } + + var shellOpen = ssURLAssociation.OpenSubKey("shell").OpenSubKey("open").OpenSubKey("command"); + return (string)shellOpen.GetValue("") == $"{ExecutablePath} --open-url %1"; + } + catch (Exception e) + { + logger.LogUsefulException(e); + return false; + } + finally + { + if (ssURLAssociation != null) + { + try + { + ssURLAssociation.Close(); + ssURLAssociation.Dispose(); + } + catch (Exception e) + { logger.LogUsefulException(e); } + } + } + } + + } +} diff --git a/shadowsocks-csharp/Data/i18n.csv b/shadowsocks-csharp/Data/i18n.csv index d526b2dc..dec8aa8b 100644 --- a/shadowsocks-csharp/Data/i18n.csv +++ b/shadowsocks-csharp/Data/i18n.csv @@ -18,6 +18,7 @@ Servers,Серверы,服务器,伺服器,サーバー,서버 Edit Servers...,Редактировать серверы…,编辑服务器...,編輯伺服器...,サーバーの編集...,서버 수정… Statistics Config...,Настройки статистики…,统计配置...,統計設定檔...,統計情報の設定...,통계 설정 Start on Boot,Автозагрузка,开机启动,開機啟動,システムと同時に起動,시스템 시작 시에 시작하기 +Associate ss:// Links,Ассоциированный ss:// Ссылки,关联 ss:// 链接,關聯 ss:// 鏈接,, Forward Proxy...,Прямой прокси…,正向代理设置...,正向 Proxy 設定...,フォワードプロキシの設定...,포워드 프록시 Allow other Devices to connect,Общий доступ к подключению,允许其他设备连入,允許其他裝置連入,他のデバイスからの接続を許可する,다른 기기에서 연결 허용 Local PAC,Локальный PAC,使用本地 PAC,使用本機 PAC,ローカル PAC,로컬 PAC @@ -184,6 +185,9 @@ Find Shadowsocks icon in your notify tray.,Значок Shadowsocks можно "If you want to start multiple Shadowsocks, make a copy in another directory.","Если вы хотите запустить несколько копий Shadowsocks одновременно, создайте отдельную папку на каждую копию.",如果想同时启动多个,可以另外复制一份到别的目录。,如果想同時啟動多個,可以另外複製一份至別的目錄。,複数起動したい場合は、プログラムファイルを別のフォルダーにコピーしてから、もう一度実行して下さい。,"만약 여러 개의 Shadowsocks를 사용하고 싶으시다면, 파일을 다른 곳에 복사해주세요." Failed to decode QRCode,Не удалось распознать QRCode,无法解析二维码,QR 碼解碼失敗,QR コードの読み取りに失敗しました。,QR코드를 해석하는데에 실패했습니다. Failed to update registry,Не удалось обновить запись в реестре,无法修改注册表,無法修改登錄檔,レジストリの更新に失敗しました。,레지스트리를 업데이트하는데에 실패했습니다. +Import from URL: {0} ?,импортировать из адреса: {0} ?,从URL导入: {0} ?,從URL匯入: {0} ?,, +Successfully imported from {0},Успешно импортировано из {0},导入成功:{0},導入成功:{0},, +Failed to import. Please check if the link is valid.,,导入失败,请检查链接是否有效。,導入失敗,請檢查鏈接是否有效。,, System Proxy On: ,Системный прокси:,系统代理已启用:,系統 Proxy 已啟用:,システム プロキシが有効:,시스템 프록시 활성화됨: Running: Port {0},Запущен на порту {0},正在运行:端口 {0},正在執行:連接埠號碼 {0},実行中:ポート {0},실행 중: 포트 {0}번 "Unexpected error, shadowsocks will exit. Please report to","Непредвиденная ошибка, пожалуйста сообщите на",非预期错误,Shadowsocks将退出。请提交此错误到,非預期錯誤,Shadowsocks 將結束。請報告此錯誤至,予想外のエラーが発生したため、Shadowsocks を終了します。詳しくは下記までお問い合わせ下さい:,알 수 없는 오류로 Shadowsocks가 종료될 것입니다. 오류를 제보해주세요: diff --git a/shadowsocks-csharp/Program.cs b/shadowsocks-csharp/Program.cs index 5b723acd..3f181105 100755 --- a/shadowsocks-csharp/Program.cs +++ b/shadowsocks-csharp/Program.cs @@ -1,21 +1,26 @@ -using System; -using System.Diagnostics; -using System.IO; -using System.Threading; -using System.Windows.Forms; +using Microsoft.Win32; using NLog; -using Microsoft.Win32; - using Shadowsocks.Controller; using Shadowsocks.Controller.Hotkeys; using Shadowsocks.Util; using Shadowsocks.View; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.IO.Pipes; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Forms; namespace Shadowsocks { - static class Program + internal static class Program { - private static Logger logger = LogManager.GetCurrentClassLogger(); + private static readonly Logger logger = LogManager.GetCurrentClassLogger(); public static ShadowsocksController MainController { get; private set; } public static MenuViewController MenuController { get; private set; } public static string[] Args { get; private set; } @@ -24,15 +29,15 @@ namespace Shadowsocks /// /// [STAThread] - static void Main(string[] args) + private static void Main(string[] args) { Directory.SetCurrentDirectory(Application.StartupPath); // todo: initialize the NLog configuartion Model.NLogConfig.TouchAndApplyNLogConfig(); // .NET Framework 4.7.2 on Win7 compatibility - System.Net.ServicePointManager.SecurityProtocol |= - System.Net.SecurityProtocolType.Tls | System.Net.SecurityProtocolType.Tls11 | System.Net.SecurityProtocolType.Tls12; + ServicePointManager.SecurityProtocol |= + SecurityProtocolType.Tls | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12; // store args for further use Args = args; @@ -50,27 +55,58 @@ namespace Shadowsocks if (DialogResult.OK == MessageBox.Show(I18N.GetString("Unsupported .NET Framework, please update to {0} or later.", "4.7.2"), "Shadowsocks Error", MessageBoxButtons.OKCancel, MessageBoxIcon.Error)) { - //Process.Start("https://www.microsoft.com/download/details.aspx?id=53344"); // 4.6.2 Process.Start("https://dotnet.microsoft.com/download/dotnet-framework/net472"); } return; } + string pipename = $"Shadowsocks\\{Application.StartupPath.GetHashCode()}"; - Utils.ReleaseMemory(true); - using (Mutex mutex = new Mutex(false, $"Global\\Shadowsocks_{Application.StartupPath.GetHashCode()}")) + string addedUrl = null; + + using (NamedPipeClientStream pipe = new NamedPipeClientStream(pipename)) { - Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException); - // handle UI exceptions - Application.ThreadException += Application_ThreadException; - // handle non-UI exceptions - AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; - Application.ApplicationExit += Application_ApplicationExit; - SystemEvents.PowerModeChanged += SystemEvents_PowerModeChanged; - Application.EnableVisualStyles(); - Application.SetCompatibleTextRenderingDefault(false); - AutoStartup.RegisterForRestart(true); - - if (!mutex.WaitOne(0, false)) + bool pipeExist = false; + try + { + pipe.Connect(10); + pipeExist = true; + } + catch (TimeoutException) + { + pipeExist = false; + } + + // TODO: switch to better argv parser when it's getting complicate + List alist = Args.ToList(); + // check --open-url param + int urlidx = alist.IndexOf("--open-url") + 1; + if (urlidx > 0) + { + if (Args.Length <= urlidx) + { + return; + } + + // --open-url exist, and no other instance, add it later + if (!pipeExist) + { + addedUrl = Args[urlidx]; + } + // has other instance, send url via pipe then exit + else + { + byte[] b = Encoding.UTF8.GetBytes(Args[urlidx]); + byte[] opAddUrl = BitConverter.GetBytes(IPAddress.HostToNetworkOrder(1)); + byte[] blen = BitConverter.GetBytes(IPAddress.HostToNetworkOrder(b.Length)); + pipe.Write(opAddUrl, 0, 4); // opcode addurl + pipe.Write(blen, 0, 4); + pipe.Write(b, 0, b.Length); + pipe.Close(); + return; + } + } + // has another instance, and no need to communicate with it return + else if (pipeExist) { Process[] oldProcesses = Process.GetProcessesByName("Shadowsocks"); if (oldProcesses.Length > 0) @@ -83,21 +119,44 @@ namespace Shadowsocks I18N.GetString("Shadowsocks is already running.")); return; } - Directory.SetCurrentDirectory(Application.StartupPath); - + } + + Utils.ReleaseMemory(true); + + Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException); + // handle UI exceptions + Application.ThreadException += Application_ThreadException; + // handle non-UI exceptions + AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; + Application.ApplicationExit += Application_ApplicationExit; + SystemEvents.PowerModeChanged += SystemEvents_PowerModeChanged; + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + AutoStartup.RegisterForRestart(true); + + Directory.SetCurrentDirectory(Application.StartupPath); + #if DEBUG - // truncate privoxy log file while debugging - string privoxyLogFilename = Utils.GetTempPath("privoxy.log"); - if (File.Exists(privoxyLogFilename)) - using (new FileStream(privoxyLogFilename, FileMode.Truncate)) { } + // truncate privoxy log file while debugging + string privoxyLogFilename = Utils.GetTempPath("privoxy.log"); + if (File.Exists(privoxyLogFilename)) + using (new FileStream(privoxyLogFilename, FileMode.Truncate)) { } #endif - MainController = new ShadowsocksController(); - MenuController = new MenuViewController(MainController); + MainController = new ShadowsocksController(); + MenuController = new MenuViewController(MainController); - HotKeys.Init(MainController); - MainController.Start(); - Application.Run(); - } + HotKeys.Init(MainController); + MainController.Start(); + + NamedPipeServer namedPipeServer = new NamedPipeServer(); + Task.Run(() => namedPipeServer.Run(pipename)); + namedPipeServer.AddUrlRequested += (_1, e) => MainController.AskAddServerBySSURL(e.Url); + if (!addedUrl.IsNullOrEmpty()) + { + MainController.AskAddServerBySSURL(addedUrl); + } + + Application.Run(); } private static int exited = 0; @@ -135,7 +194,7 @@ namespace Shadowsocks logger.Info("os wake up"); if (MainController != null) { - System.Threading.Tasks.Task.Factory.StartNew(() => + Task.Factory.StartNew(() => { Thread.Sleep(10 * 1000); try diff --git a/shadowsocks-csharp/View/MenuViewController.cs b/shadowsocks-csharp/View/MenuViewController.cs index ddcf21f4..253c54c2 100644 --- a/shadowsocks-csharp/View/MenuViewController.cs +++ b/shadowsocks-csharp/View/MenuViewController.cs @@ -36,6 +36,7 @@ namespace Shadowsocks.View private ContextMenu contextMenu1; private MenuItem disableItem; private MenuItem AutoStartupItem; + private MenuItem ProtocolHandlerItem; private MenuItem ShareOverLANItem; private MenuItem SeperatorItem; private MenuItem ConfigItem; @@ -315,6 +316,7 @@ namespace Shadowsocks.View this.proxyItem = CreateMenuItem("Forward Proxy...", new EventHandler(this.proxyItem_Click)), new MenuItem("-"), this.AutoStartupItem = CreateMenuItem("Start on Boot", new EventHandler(this.AutoStartupItem_Click)), + this.ProtocolHandlerItem = CreateMenuItem("Associate ss:// Links", new EventHandler(this.ProtocolHandlerItem_Click)), this.ShareOverLANItem = CreateMenuItem("Allow other Devices to connect", new EventHandler(this.ShareOverLANItem_Click)), new MenuItem("-"), this.hotKeyItem = CreateMenuItem("Edit Hotkeys...", new EventHandler(this.hotKeyItem_Click)), @@ -442,6 +444,7 @@ namespace Shadowsocks.View VerboseLoggingToggleItem.Checked = config.isVerboseLogging; ShowPluginOutputToggleItem.Checked = config.showPluginOutput; AutoStartupItem.Checked = AutoStartup.Check(); + ProtocolHandlerItem.Checked = ProtocolHandler.Check(); onlinePACItem.Checked = onlinePACItem.Enabled && config.useOnlinePac; localPACItem.Checked = !onlinePACItem.Checked; secureLocalPacUrlToggleItem.Checked = config.secureLocalPac; @@ -821,8 +824,7 @@ namespace Shadowsocks.View private void ImportURLItem_Click(object sender, EventArgs e) { - var success = controller.AddServerBySSURL(Clipboard.GetText(TextDataFormat.Text)); - if (success) + if (controller.AskAddServerBySSURL(Clipboard.GetText(TextDataFormat.Text))) { ShowConfigForm(); } @@ -845,6 +847,16 @@ namespace Shadowsocks.View { MessageBox.Show(I18N.GetString("Failed to update registry")); } + LoadCurrentConfiguration(); + } + private void ProtocolHandlerItem_Click(object sender, EventArgs e) + { + ProtocolHandlerItem.Checked = !ProtocolHandlerItem.Checked; + if (!ProtocolHandler.Set(ProtocolHandlerItem.Checked)) + { + MessageBox.Show(I18N.GetString("Failed to update registry")); + } + LoadCurrentConfiguration(); } private void LocalPACItem_Click(object sender, EventArgs e) diff --git a/shadowsocks-csharp/shadowsocks-csharp.csproj b/shadowsocks-csharp/shadowsocks-csharp.csproj index bf562337..978e58d3 100644 --- a/shadowsocks-csharp/shadowsocks-csharp.csproj +++ b/shadowsocks-csharp/shadowsocks-csharp.csproj @@ -113,6 +113,8 @@ + +