1 /** 2 Copyright: Copyright (c) 2020, Joakim Brännström. All rights reserved. 3 License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost Software License 1.0) 4 Author: Joakim Brännström (joakim.brannstrom@gmx.com) 5 6 ssh connection for both normal and multiplex. 7 */ 8 module distssh.connection; 9 10 import logger = std.experimental.logger; 11 import std.exception : collectException; 12 import std.file : thisExePath; 13 import std.process : escapeShellFileName; 14 15 import my.path; 16 17 import distssh.types; 18 19 // arguments to ssh that turn off warning that a host key is new or requies a 20 // password to login 21 immutable sshNoLoginArgs = [ 22 "-oStrictHostKeyChecking=no", "-oPasswordAuthentication=no" 23 ]; 24 25 immutable sshMultiplexClient = ["-oControlMaster=auto", "-oControlPersist=300"]; 26 27 SshArgs sshArgs(Host host, string[] ssh, string[] cmd) { 28 return SshArgs("ssh", ssh ~ sshNoLoginArgs ~ [ 29 host, thisExePath.escapeShellFileName 30 ], cmd); 31 } 32 33 SshArgs sshCmdArgs(Host host, string[] cmd) { 34 // must ensure it exists 35 setupMultiplexDir; 36 return sshArgs(host, MultiplexPath(multiplexDir).toArgs ~ sshMultiplexClient.dup, cmd); 37 } 38 39 SshArgs sshShellArgs(Host host, Path workDir) { 40 // two -t forces a tty to be created and used which mean that the remote 41 // shell will *think* it is an interactive shell 42 return sshArgs(host, ["-q", "-t", "-t"], [ 43 "localshell", "--workdir", workDir.toString.escapeShellFileName 44 ]); 45 } 46 47 SshArgs sshLoadArgs(Host host) { 48 return sshArgs(host, ["-q"], ["localload"]); 49 } 50 51 /// Arguments for creating a ssh connection and execute a command. 52 struct SshArgs { 53 string ssh; 54 string[] sshArgs; 55 string[] cmd; 56 57 /// 58 /// 59 /// Params: 60 /// ssh = command to use for establishing the ssh connection 61 /// sshArgs = arguments to the `ssh` 62 /// cmd = command to execute 63 this(string ssh, string[] sshArgs, string[] cmd) @safe pure nothrow @nogc { 64 this.ssh = ssh; 65 this.sshArgs = sshArgs; 66 this.cmd = cmd; 67 } 68 69 string[] toArgs() @safe pure nothrow const { 70 return [ssh] ~ sshArgs.dup ~ cmd.dup; 71 } 72 } 73 74 AbsolutePath multiplexDir() @safe { 75 import my.xdg : xdgRuntimeDir; 76 77 return (xdgRuntimeDir ~ "distssh/multiplex").AbsolutePath; 78 } 79 80 struct MultiplexPath { 81 AbsolutePath dir; 82 string tokens = `%C`; 83 84 string[] toArgs() @safe pure const { 85 return ["-S", toString]; 86 } 87 88 import std.range : isOutputRange; 89 90 string toString() @safe pure const { 91 import std.array : appender; 92 93 auto buf = appender!string; 94 toString(buf); 95 return buf.data; 96 } 97 98 void toString(Writer)(ref Writer w) const if (isOutputRange!(Writer, char)) { 99 import std.format : formattedWrite; 100 101 formattedWrite(w, "%s/%s", dir, tokens); 102 } 103 } 104 105 struct MultiplexMaster { 106 import core.time : dur; 107 import std.array : array, empty; 108 import proc; 109 110 MultiplexPath socket; 111 SshArgs ssh; 112 113 void connect() @safe { 114 SshArgs a = ssh; 115 a.cmd = ["true"]; 116 a.sshArgs = ["-oControlMaster=yes"] ~ a.sshArgs; 117 auto p = pipeProcess(a.toArgs); 118 const ec = p.wait; 119 if (ec != 0) { 120 logger.trace("Failed starting multiplex master. Exit code ", ec); 121 logger.trace(p.drainByLineCopy); 122 } 123 } 124 125 bool isAlive() @trusted { 126 import std.algorithm : filter; 127 import std..string : startsWith, toLower; 128 129 SshArgs a = ssh; 130 a.sshArgs = ["-O", "check"] ~ a.sshArgs; 131 auto p = pipeProcess(a.toArgs).timeout(10.dur!"seconds").rcKill; 132 133 auto lines = p.drainByLineCopy.filter!(a => !a.empty).array; 134 logger.trace(lines); 135 136 auto ec = p.wait; 137 if (ec != 0 || lines.empty) { 138 return false; 139 } 140 141 return lines[0].toLower.startsWith("master"); 142 } 143 } 144 145 MultiplexMaster makeMaster(Host host) { 146 setupMultiplexDir; 147 148 MultiplexMaster master; 149 master.socket = MultiplexPath(multiplexDir); 150 master.ssh = SshArgs("ssh", master.socket.toArgs ~ [ 151 "-oControlPersist=300", host 152 ], null); 153 154 return master; 155 } 156 157 void setupMultiplexDir() @safe nothrow { 158 import std.file : mkdirRecurse, exists; 159 160 try { 161 const p = multiplexDir; 162 if (!exists(p)) { 163 mkdirRecurse(p); 164 } 165 } catch (Exception e) { 166 logger.warning(e.msg).collectException; 167 } 168 }