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 }