Added command-line client (for debug only).
[BoarSSL] / CLI / Client.cs
1 /*
2 * Copyright (c) 2018 Thomas Pornin <pornin@bolet.org>
3 *
4 * Permission is hereby granted, free of charge, to any person obtaining
5 * a copy of this software and associated documentation files (the
6 * "Software"), to deal in the Software without restriction, including
7 * without limitation the rights to use, copy, modify, merge, publish,
8 * distribute, sublicense, and/or sell copies of the Software, and to
9 * permit persons to whom the Software is furnished to do so, subject to
10 * the following conditions:
11 *
12 * The above copyright notice and this permission notice shall be
13 * included in all copies or substantial portions of the Software.
14 *
15 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
19 * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
20 * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
21 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 * SOFTWARE.
23 */
24
25 using System;
26 using System.Collections.Generic;
27 using System.Diagnostics;
28 using System.IO;
29 using System.Net.Sockets;
30 using System.Text;
31 using System.Threading;
32
33 using Asn1;
34 using Crypto;
35 using IO;
36 using SSLTLS;
37 using XKeys;
38
39 /*
40 * A simple command-line application that runs a client that connects
41 * to a provided server. This is meant for debug purposes.
42 */
43
44 public class Client {
45
46 public static void Main(string[] args)
47 {
48 try {
49 new Client().Run(args);
50 } catch (Exception e) {
51 Console.WriteLine(e.ToString());
52 Environment.Exit(1);
53 }
54 }
55
56 void Run(string[] args)
57 {
58 bool verbose = true;
59 bool trace = false;
60 string host = null;
61 string sni = null;
62 List<string> csNames = null;
63 List<string> hsNames = null;
64 int vmin = 0;
65 int vmax = 0;
66 for (int i = 0; i < args.Length; i ++) {
67 string a = args[i];
68 if (!a.StartsWith("-")) {
69 if (host != null) {
70 throw new Exception(
71 "duplicate host name");
72 }
73 host = a;
74 continue;
75 }
76 a = a.Substring(1).ToLowerInvariant();
77 switch (a) {
78 case "v":
79 verbose = true;
80 break;
81 case "q":
82 verbose = false;
83 break;
84 case "trace":
85 trace = true;
86 break;
87 case "sni":
88 if (sni != null) {
89 throw new Exception(
90 "duplicate SNI");
91 }
92 if (++ i >= args.Length) {
93 throw new Exception(
94 "no SNI provided");
95 }
96 sni = args[i];
97 break;
98 case "nosni":
99 if (sni != null) {
100 throw new Exception(
101 "duplicate SNI");
102 }
103 sni = "";
104 break;
105 case "cs":
106 if (++ i >= args.Length) {
107 throw new Exception(
108 "no cipher names provided");
109 }
110 if (csNames == null) {
111 csNames = new List<string>();
112 }
113 AddNames(csNames, args[i]);
114 break;
115 case "hs":
116 if (++ i >= args.Length) {
117 throw new Exception(
118 "no hash-and-sign provided");
119 }
120 if (hsNames == null) {
121 hsNames = new List<string>();
122 }
123 AddNames(hsNames, args[i]);
124 break;
125 case "vmin":
126 if (vmin != 0) {
127 throw new Exception(
128 "duplicate minimum version");
129 }
130 if (++ i >= args.Length) {
131 throw new Exception(
132 "no minimum version provided");
133 }
134 vmin = SSL.GetVersionByName(args[i]);
135 break;
136 case "vmax":
137 if (vmax != 0) {
138 throw new Exception(
139 "duplicate maximum version");
140 }
141 if (++ i >= args.Length) {
142 throw new Exception(
143 "no maximum version provided");
144 }
145 vmax = SSL.GetVersionByName(args[i]);
146 break;
147 default:
148 throw new Exception(string.Format(
149 "Unknown option: '-{0}'", a));
150 }
151 }
152
153 if (host == null) {
154 throw new Exception("no host name provided");
155 }
156 int j = host.LastIndexOf(':');
157 int port;
158 if (j < 0) {
159 port = 443;
160 } else {
161 if (!Int32.TryParse(host.Substring(j + 1), out port)
162 || port <= 0 || port > 65535)
163 {
164 throw new Exception("invalid port number");
165 }
166 host = host.Substring(0, j);
167 }
168 if (sni == null) {
169 sni = host;
170 }
171 int[] css = null;
172 if (csNames != null) {
173 css = new int[csNames.Count];
174 for (int i = 0; i < css.Length; i ++) {
175 css[i] = SSL.GetSuiteByName(csNames[i]);
176 }
177 }
178 int[] hss = null;
179 if (hsNames != null) {
180 hss = new int[hsNames.Count];
181 for (int i = 0; i < hss.Length; i ++) {
182 hss[i] = SSL.GetHashAndSignByName(hsNames[i]);
183 }
184 }
185 if (vmin != 0 && vmax != 0 && vmin > vmax) {
186 throw new Exception("invalid version range");
187 }
188
189 /*
190 * Connect to the designated server.
191 */
192 TcpClient tc = new TcpClient(host, port);
193 Socket sock = tc.Client;
194 Stream ns = tc.GetStream();
195 if (trace) {
196 MergeStream ms = new MergeStream(ns, ns);
197 ms.Debug = Console.Out;
198 ns = ms;
199 }
200 SSLClient ssl = new SSLClient(ns);
201 if (sni != "") {
202 ssl.ServerName = sni;
203 }
204 if (css != null) {
205 ssl.SupportedCipherSuites = css;
206 }
207 if (hss != null) {
208 ssl.SupportedHashAndSign = hss;
209 }
210 if (vmin != 0) {
211 ssl.VersionMin = vmin;
212 }
213 if (vmax != 0) {
214 ssl.VersionMax = vmax;
215 }
216
217 /*
218 * This is a debug tool; we accept the server certificate
219 * without validation.
220 */
221 ssl.ServerCertValidator = SSLClient.InsecureCertValidator;
222
223 /*
224 * Force a Flush. There is no application data to flush
225 * at this point, but as a side-effect it forces the
226 * handshake to complete.
227 */
228 ssl.Flush();
229
230 if (verbose) {
231 Console.WriteLine("Handshake completed:");
232 Console.WriteLine(" Version = {0}",
233 SSL.VersionName(ssl.Version));
234 Console.WriteLine(" Cipher suite = {0}",
235 SSL.CipherSuiteName(ssl.CipherSuite));
236 }
237
238 /*
239 * Now relay data back and forth between the connection
240 * and the console. Since the underlying SSL stream does
241 * not support simultaneous reads and writes, we use
242 * the following approximation:
243 *
244 * - We poll on the socket for incoming data. When there
245 * is some activity, we assume that some application
246 * data (or closure) follows, and we read it. It is
247 * then immediately written out (synchronously) on
248 * standard output.
249 *
250 * - When waiting for read activity on the socket, we
251 * regularly (every 200 ms) check for data to read on
252 * standard input. If there is, we read it, and send
253 * it synchronously on the SSL stream.
254 *
255 * - The data reading from console is performed by
256 * another thread.
257 *
258 * Since SSL records are read one by one, we know that,
259 * by using a buffer larger than 16 kB, a single Read()
260 * call cannot leave any buffered application data.
261 */
262 ssl.CloseSub = false;
263 Thread t = new Thread(new ThreadStart(CRThread));
264 t.IsBackground = true;
265 t.Start();
266 byte[] buf = new byte[16384];
267 Stream stdout = Console.OpenStandardOutput();
268 for (;;) {
269 if (sock.Poll(200000, SelectMode.SelectRead)) {
270 int rlen = ssl.Read(buf, 0, buf.Length);
271 if (rlen < 0) {
272 Console.WriteLine(
273 "Connection closed.\n");
274 break;
275 }
276 stdout.Write(buf, 0, rlen);
277 } else {
278 while (CRHasData()) {
279 int rlen = CRRead(buf, 0, buf.Length);
280 if (rlen < 0) {
281 ssl.Close();
282 break;
283 }
284 if (rlen > 0) {
285 ssl.Write(buf, 0, rlen);
286 }
287 }
288 }
289 }
290 sock.Close();
291 }
292
293 static void AddNames(List<string> d, string str)
294 {
295 foreach (string name in str.Split(
296 new char[] { ',', ':', ';' },
297 StringSplitOptions.RemoveEmptyEntries))
298 {
299 d.Add(name.Trim());
300 }
301 }
302
303 object consoleReadLock = new object();
304 byte[] crBuf = new byte[16384];
305 int crPtr = 0;
306 bool crClosed = false;
307
308 bool CRHasData()
309 {
310 lock (consoleReadLock) {
311 return crPtr != 0 || crClosed;
312 }
313 }
314
315 int CRRead(byte[] buf, int off, int len)
316 {
317 lock (consoleReadLock) {
318 if (crPtr == 0 && crClosed) {
319 return -1;
320 }
321 int rlen = Math.Min(len, crPtr);
322 Array.Copy(crBuf, 0, buf, off, rlen);
323 if (rlen > 0 && rlen < crPtr) {
324 Array.Copy(crBuf, rlen, crBuf, 0, crPtr - rlen);
325 }
326 crPtr -= rlen;
327 Monitor.PulseAll(consoleReadLock);
328 return rlen;
329 }
330 }
331
332 void CRThread()
333 {
334 byte[] buf = new byte[crBuf.Length];
335 Stream stdin = Console.OpenStandardInput();
336
337 for (;;) {
338 lock (consoleReadLock) {
339 while (crPtr == crBuf.Length) {
340 Monitor.Wait(consoleReadLock);
341 }
342 }
343 int rlen = stdin.Read(buf, 0, buf.Length);
344 lock (consoleReadLock) {
345 Monitor.PulseAll(consoleReadLock);
346 if (rlen < 0) {
347 crClosed = true;
348 break;
349 }
350 Array.Copy(buf, 0, crBuf, crPtr, rlen);
351 crPtr += rlen;
352 }
353 }
354 }
355 }