ssh-handler.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  1. // SSH Handler
  2. //
  3. // Douglas Thrift
  4. //
  5. // ssh-handler.cs
  6. /* Copyright 2013 Douglas Thrift
  7. *
  8. * Licensed under the Apache License, Version 2.0 (the "License");
  9. * you may not use this file except in compliance with the License.
  10. * You may obtain a copy of the License at
  11. *
  12. * http://www.apache.org/licenses/LICENSE-2.0
  13. *
  14. * Unless required by applicable law or agreed to in writing, software
  15. * distributed under the License is distributed on an "AS IS" BASIS,
  16. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  17. * See the License for the specific language governing permissions and
  18. * limitations under the License.
  19. */
  20. using System;
  21. using System.Collections.Generic;
  22. using System.Diagnostics;
  23. using System.IO;
  24. using System.Linq;
  25. using System.Text;
  26. using System.Text.RegularExpressions;
  27. using System.Windows.Forms;
  28. using Microsoft.Win32;
  29. public enum MatchOption
  30. {
  31. None,
  32. Set,
  33. Option,
  34. }
  35. public enum AutoYesNoOption
  36. {
  37. Auto,
  38. Yes,
  39. No,
  40. }
  41. public interface Handler
  42. {
  43. IList<string> Usages
  44. {
  45. get;
  46. }
  47. MatchOption DoMatch(string arg);
  48. bool Find();
  49. void Execute(Uri uri, string user, string password);
  50. }
  51. public abstract class AbstractHandler
  52. {
  53. protected string FindInPath(string program)
  54. {
  55. foreach (string location in Environment.GetEnvironmentVariable("PATH").Split(Path.PathSeparator))
  56. {
  57. string path = Path.Combine(location, program);
  58. if (File.Exists(path))
  59. return path;
  60. }
  61. return null;
  62. }
  63. protected void SetYesNoValue(string input, out AutoYesNoOption option, out string value)
  64. {
  65. value = null;
  66. switch (input.ToLowerInvariant())
  67. {
  68. case "yes":
  69. option = AutoYesNoOption.Yes;
  70. break;
  71. case "no":
  72. option = AutoYesNoOption.No;
  73. break;
  74. default:
  75. value = input;
  76. goto case "yes";
  77. }
  78. }
  79. }
  80. public class PuttyHandler : AbstractHandler, Handler
  81. {
  82. private Regex regex = new Regex(@"^(?:/|--?)putty(?:[:=](?<putty_path>.*))?$", RegexOptions.IgnoreCase);
  83. private string path = null;
  84. public IList<string> Usages
  85. {
  86. get
  87. {
  88. return new string[] { "/putty[:<putty-path>] -- Use PuTTY to connect" };
  89. }
  90. }
  91. public MatchOption DoMatch(string arg)
  92. {
  93. Match match;
  94. if ((match = regex.Match(arg)).Success)
  95. {
  96. Group group = match.Groups["putty_path"];
  97. if (group.Success)
  98. path = group.Value;
  99. return MatchOption.Set;
  100. }
  101. else
  102. return MatchOption.None;
  103. }
  104. public bool Find()
  105. {
  106. if (path != null)
  107. goto Found;
  108. foreach (var hive in new RegistryHive[] { RegistryHive.CurrentUser, RegistryHive.LocalMachine })
  109. {
  110. var views = new List<RegistryView>(new RegistryView[] { RegistryView.Registry32 });
  111. if (Environment.Is64BitOperatingSystem)
  112. views.Insert(0, RegistryView.Registry64);
  113. foreach (RegistryView view in views)
  114. using (RegistryKey baseKey = RegistryKey.OpenBaseKey(hive, view), key = baseKey.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\PuTTY_is1"))
  115. if (key != null)
  116. {
  117. string location = (string)key.GetValue("InstallLocation");
  118. if (location == null)
  119. continue;
  120. path = Path.Combine(location, "putty.exe");
  121. if (File.Exists(path))
  122. {
  123. Debug.WriteLine("Found PuTTY in registry: {0}", path, null);
  124. goto Found;
  125. }
  126. else
  127. path = null;
  128. }
  129. }
  130. if ((path = FindInPath("putty.exe")) != null)
  131. {
  132. Debug.WriteLine("Found PuTTY in path: {0}", path, null);
  133. goto Found;
  134. }
  135. return false;
  136. Found:
  137. path = path.Trim();
  138. return true;
  139. }
  140. public void Execute(Uri uri, string user, string password)
  141. {
  142. if (!Find())
  143. throw new Exception("Could not find PuTTY executable.");
  144. StringBuilder args = new StringBuilder();
  145. if (password != null)
  146. args.AppendFormat("-pw {0} ", password);
  147. if (uri.Port != -1)
  148. args.AppendFormat("-P {0} ", uri.Port);
  149. if (user != null)
  150. args.AppendFormat("{0}@", user);
  151. args.Append(uri.Host);
  152. Debug.WriteLine("Running PuTTY command: {0} {1}", path, args);
  153. Process.Start(path, args.ToString());
  154. }
  155. }
  156. public class OpensshHandler : AbstractHandler, Handler
  157. {
  158. private Regex regex = new Regex(@"^(?:/|--?)openssh(?:[:=](?<openssh_path>.*))?$", RegexOptions.IgnoreCase);
  159. private Regex cygwinRegex = new Regex(@"^(?:/|--?)cygwin(?:[:=](?<cygwin_path>.*))?$", RegexOptions.IgnoreCase);
  160. private Regex minttyRegex = new Regex(@"^(?:/|--?)mintty(?:[:=](?<mintty_path>.*))?$", RegexOptions.IgnoreCase);
  161. private AutoYesNoOption cygwin = AutoYesNoOption.Auto;
  162. private AutoYesNoOption mintty = AutoYesNoOption.Auto;
  163. private string path = null;
  164. string cygwinPath = null;
  165. string minttyPath = null;
  166. public IList<string> Usages
  167. {
  168. get
  169. {
  170. return new string[]
  171. {
  172. "/openssh[:<openssh-path>] -- Use OpenSSH to connect",
  173. "/cygwin[:(yes|no|<cygwin-path>)] -- Use Cygwin for OpenSSH (by default, Cygwin will be used for OpenSSH if detected)",
  174. "/mintty[:(yes|no|<mintty-path>)] -- Use MinTTY for OpenSSH (by default, MinTTY will be used for OpenSSH if detected)",
  175. };
  176. }
  177. }
  178. public MatchOption DoMatch(string arg)
  179. {
  180. Match match;
  181. if ((match = regex.Match(arg)).Success)
  182. {
  183. Group group = match.Groups["openssh_path"];
  184. if (group.Success)
  185. path = group.Value;
  186. return MatchOption.Set;
  187. }
  188. else if ((match = cygwinRegex.Match(arg)).Success)
  189. {
  190. Group group = match.Groups["cygwin_path"];
  191. if (group.Success)
  192. SetYesNoValue(group.Value, out cygwin, out cygwinPath);
  193. return MatchOption.Option;
  194. }
  195. else if ((match = minttyRegex.Match(arg)).Success)
  196. {
  197. Group group = match.Groups["mintty_path"];
  198. if (group.Success)
  199. SetYesNoValue(group.Value, out mintty, out minttyPath);
  200. return MatchOption.Option;
  201. }
  202. else
  203. return MatchOption.None;
  204. }
  205. public bool Find()
  206. {
  207. if (path != null)
  208. goto Found;
  209. switch (cygwin)
  210. {
  211. case AutoYesNoOption.Auto:
  212. case AutoYesNoOption.Yes:
  213. if (FindCygwin())
  214. goto Found;
  215. break;
  216. }
  217. if ((path = FindInPath("ssh.exe")) != null)
  218. {
  219. Debug.WriteLine("Found OpenSSH in path: {0}", path, null);
  220. goto Found;
  221. }
  222. return false;
  223. Found:
  224. path = path.Trim();
  225. switch (mintty)
  226. {
  227. case AutoYesNoOption.Auto:
  228. case AutoYesNoOption.Yes:
  229. FindMintty();
  230. break;
  231. }
  232. return true;
  233. }
  234. public void Execute(Uri uri, string user, string password)
  235. {
  236. if (!Find())
  237. throw new Exception("Could not find OpenSSH executable.");
  238. string command = path;
  239. StringBuilder args = new StringBuilder();
  240. if (minttyPath != null)
  241. {
  242. command = minttyPath;
  243. if (cygwinPath != null)
  244. {
  245. string icon = Path.Combine(cygwinPath, "Cygwin-Terminal.ico");
  246. if (File.Exists(icon))
  247. args.AppendFormat("-i {0} ", icon);
  248. }
  249. args.AppendFormat("-e {0} ", path);
  250. }
  251. if (password != null)
  252. Debug.WriteLine("Warning: OpenSSH does not support passing a password.");
  253. if (uri.Port != -1)
  254. args.AppendFormat("-p {0} ", uri.Port);
  255. if (user != null)
  256. args.AppendFormat("{0}@", user);
  257. args.Append(uri.Host);
  258. Debug.WriteLine("Running OpenSSH command: {0} {1}", command, args);
  259. Process.Start(command, args.ToString());
  260. }
  261. private bool FindCygwin()
  262. {
  263. if (cygwinPath != null)
  264. goto Found;
  265. foreach (var hive in new RegistryHive[] { RegistryHive.CurrentUser, RegistryHive.LocalMachine })
  266. {
  267. var views = new List<RegistryView>(new RegistryView[] { RegistryView.Registry32 });
  268. if (Environment.Is64BitOperatingSystem)
  269. views.Insert(0, RegistryView.Registry64);
  270. foreach (RegistryView view in views)
  271. using (RegistryKey baseKey = RegistryKey.OpenBaseKey(hive, view), key = baseKey.OpenSubKey(@"SOFTWARE\Cygwin\setup"))
  272. if (key != null)
  273. {
  274. cygwinPath = (string)key.GetValue("rootdir");
  275. if (cygwinPath == null)
  276. continue;
  277. if (Directory.Exists(cygwinPath))
  278. {
  279. Debug.WriteLine("Found Cygwin in registry: {0}", cygwinPath, null);
  280. goto Found;
  281. }
  282. else
  283. cygwinPath = null;
  284. }
  285. }
  286. if (cygwin == AutoYesNoOption.Yes)
  287. throw new Exception("Could not find Cygwin in registry.");
  288. return false;
  289. Found:
  290. cygwinPath = cygwinPath.Trim();
  291. path = Path.Combine(cygwinPath, "bin", "ssh.exe");
  292. if (File.Exists(path))
  293. {
  294. Debug.WriteLine("Found OpenSSH in Cygwin directory: {0}", path, null);
  295. return true;
  296. }
  297. else if (cygwin == AutoYesNoOption.Yes)
  298. throw new Exception("Could not find OpenSSH in Cygwin directory.");
  299. else
  300. {
  301. path = null;
  302. return false;
  303. }
  304. }
  305. private void FindMintty()
  306. {
  307. if (minttyPath != null)
  308. goto Found;
  309. if (cygwinPath != null)
  310. {
  311. minttyPath = Path.Combine(cygwinPath, "bin", "mintty.exe");
  312. if (File.Exists(minttyPath))
  313. {
  314. Debug.WriteLine("Found MinTTY in Cygwin directory: {0}", minttyPath, null);
  315. goto Found;
  316. }
  317. else
  318. minttyPath = null;
  319. }
  320. if ((minttyPath = FindInPath("mintty.exe")) != null)
  321. {
  322. Debug.WriteLine("Found MinTTY in path: {0}", minttyPath, null);
  323. goto Found;
  324. }
  325. if (mintty == AutoYesNoOption.Yes)
  326. throw new Exception("Could no find MinTTY executable.");
  327. return;
  328. Found:
  329. minttyPath = minttyPath.Trim();
  330. }
  331. }
  332. public class SshHandler
  333. {
  334. private static IList<Handler> handlers = new Handler[]
  335. {
  336. new PuttyHandler(),
  337. new OpensshHandler(),
  338. };
  339. private static Handler handler = null;
  340. public static int Main(string[] args)
  341. {
  342. Application.EnableVisualStyles();
  343. try
  344. {
  345. Regex usage = new Regex(@"^(?:/|--?)(?:h|help|usage|\?)$", RegexOptions.IgnoreCase);
  346. IList<string> uriParts = null;
  347. foreach (string arg in args)
  348. if (uriParts == null)
  349. {
  350. if (usage.IsMatch(arg))
  351. return Usage(0);
  352. if (!MatchHandler(arg))
  353. uriParts = new List<string>(new string[] { arg });
  354. }
  355. else
  356. uriParts.Add(arg);
  357. if (uriParts != null)
  358. {
  359. Uri uri = new Uri(string.Join(" ", uriParts), UriKind.Absolute);
  360. string user, password;
  361. SetUserPassword(uri, out user, out password);
  362. if (handler == null)
  363. handler = FindHandler();
  364. handler.Execute(uri, user, password);
  365. }
  366. else
  367. return Usage(1);
  368. }
  369. catch (Exception exception)
  370. {
  371. MessageBox.Show(exception.Message, "SSH Handler Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
  372. return 2;
  373. }
  374. return 0;
  375. }
  376. private static int Usage(int code)
  377. {
  378. MessageBox.Show("ssh-handler [/putty[:<putty-path>]] <ssh-url>\n\n" +
  379. string.Join("\n\n", handlers.SelectMany(handler => handler.Usages)), "SSH Handler Usage",
  380. MessageBoxButtons.OK, MessageBoxIcon.Information);
  381. return code;
  382. }
  383. private static bool MatchHandler(string arg)
  384. {
  385. foreach (Handler handler in handlers)
  386. switch (handler.DoMatch(arg))
  387. {
  388. case MatchOption.Set:
  389. Debug.WriteLine("Setting handler: {0}", handler, null);
  390. SshHandler.handler = handler;
  391. goto case MatchOption.Option;
  392. case MatchOption.Option:
  393. return true;
  394. }
  395. return false;
  396. }
  397. private static void SetUserPassword(Uri uri, out string user, out string password)
  398. {
  399. if (uri.UserInfo.Length != 0)
  400. {
  401. string[] userInfo = uri.UserInfo.Split(new char[] { ':' }, 2);
  402. user = userInfo[0];
  403. password = userInfo.Length == 2 ? userInfo[1] : null;
  404. }
  405. else
  406. {
  407. user = null;
  408. password = null;
  409. }
  410. }
  411. private static Handler FindHandler()
  412. {
  413. foreach (Handler handler in handlers)
  414. if (handler.Find())
  415. return handler;
  416. throw new Exception("Could not find a suitable SSH application.");
  417. }
  418. }