1 module libunboundctl;
2 
3 import std.stdio;
4 import std.process : Pipe, pipeCreate = pipe;
5 import std.conv : to, ConvException;
6 import core.sys.posix.unistd;
7 import core.sys.posix.sys.wait;
8 import std.string : strip;
9 import std.string : toLower, toUpper, cmp;
10 import std.array;
11 
12 private ulong cStringLen(char* cString)
13 {
14 	int idx = 0;
15 	while(*(cString+idx))
16 	{
17 		idx++;
18 	}
19 	return idx;
20 }
21 
22 private char* makeCString(string dString)
23 {
24 	char[] cString = cast(char[])dString;
25 
26 	cString.length = cString.length+1;
27 	cString[cString.length-1] = 0;
28 
29 	return cString.ptr;
30 }
31 
32 public final class UnboundControl
33 {
34 	private string endpoint;
35 	private string unboundCTLPath;
36 	
37 	this(string endpoint = "::1@8953", string unboundCTLPath = "/usr/sbin/unbound-control")
38 	{
39 		this.endpoint = endpoint;
40 		this.unboundCTLPath = unboundCTLPath;
41 	}
42 
43 	/** 
44 	 * Runs the command using `unbound-control`
45 	 * 
46 	 * Params:
47 	 *   command = The command to run
48 	 *   data = Arguments to the command
49 	 *   cmdOut = A ref string where to place the output from the command
50 	 *
51 	 * Returns: true if successful, false otherwise
52 	 */
53 	private final bool ctl(string command, string data, ref string cmdOut)
54 	{
55 		// Creates a pipe to collect stdout from `unbound-control`
56 		Pipe pipe = pipeCreate();
57 		File readEnd = pipe.readEnd();
58 		File writeEnd = pipe.writeEnd();
59 
60 		// Creates a pipe to collect stderr from `unbound-control`
61 		// TODO: Add this in
62 
63 		// Fork from here
64 		pid_t unboundCtlPid = fork();
65 
66 		// Child process
67 		if(cast(int)unboundCtlPid == 0)
68 		{
69 			// Close file descriptor 1, make a new file descriptor 1 with fdptr as writeEnd.fileno()
70 			dup2(writeEnd.fileno(), 1);
71 
72 			char*[] arguments = [makeCString(unboundCTLPath), makeCString("-s"), makeCString(endpoint), makeCString(command), makeCString(data), null];
73 			if(execv(makeCString(unboundCTLPath), arguments.ptr) == -1)
74 			{
75 				writeln("Baad");
76 				_exit(0);
77 			}
78 		}
79 		// Us (parent process)
80 		else
81 		{
82 			int wstatus;
83 			int pid = waitpid(unboundCtlPid, &wstatus, 0);
84 
85 			// Close the write end of the pipe, allowing us to not block for eternity
86 			writeEnd.close();
87 
88 			if(WEXITSTATUS(wstatus) != 0)
89 			{
90 				// TODO: Set `cmdOut` to stderr text
91 				cmdOut = "TODO Stderr text";
92 				return false;
93 			}
94 			else
95 			{
96 				// FIXME: This should read till EOF (-1) - so put this in a loop
97 				ubyte[] fullResponse;
98 				ubyte[] temp;
99 
100 				while(true)
101 				{
102 					// Read 500 chunks at a time
103 					temp.length = 500;
104 					long cnt = read(readEnd.fileno(), temp.ptr, temp.length); //D's read blocks forever, it passes some flags I don't vaab with
105 					writeln(cnt);
106 
107 
108 					if(cnt <= 0)
109 					{
110 						break;
111 					}
112 					else
113 					{
114 						fullResponse ~= temp[0..cnt];
115 					}
116 				}
117 
118 			
119 				// Strip newline
120 				cmdOut = strip(cast(string)fullResponse);
121 				
122 
123 				return true;
124 			}
125 		}
126 
127 		return false;
128 	}
129 
130 	public void addLocalData(Record record)
131 	{
132 		// local_data deavmi.hax. IN A 1.1.1.1
133 		string domain = record.domain;
134 		RecordType recordType = record.recordType;
135 		string value = record.value;
136 
137 		string dataOut;
138 		bool status = ctl("local_data", domain~" IN "~to!(string)(recordType)~" "~value, dataOut);
139 
140 		if(!status)
141 		{
142 			debug(dgb)
143 			{
144 				writeln("Handle error");
145 			}
146 		}
147 	}
148 
149 	public void addLocalZone(string zone, ZoneType zoneType)
150 	{
151 		string dataOut;
152 		
153 		// Convert zonetype from (e.g. `STATIC`) to (e.g. `static`)
154 		string zoneTypeStr = toLower(to!(string)(zoneType));
155 
156 		bool status = ctl("local_zone", zone~" "~zoneTypeStr, dataOut);
157 	}
158 
159 	public void removeLocalZone(string zone)
160 	{
161 		string dataOut;
162 		
163 		bool status = ctl("local_zone_remove", zone, dataOut);
164 	}
165 
166 	public void removeLocalData(string domain)
167 	{
168 		string dataOut;
169 		
170 		bool status = ctl("local_data_remove", domain, dataOut);
171 	}
172 
173 	public void verbosity(ulong level)
174 	{
175 		string dataOut;
176 
177 		bool status = ctl("verbosity", to!(string)(level), dataOut);
178 	}
179 
180 	public Zone[] listLocalZones()
181 	{
182 		string zoneData;
183 
184 		bool status = ctl("list_local_zones", "", zoneData);
185 
186 		// If the records were returned into the `zoneData` string
187 		if(status)
188 		{
189 			Zone[] zones;
190 			foreach(string zoneInfo; split(zoneData, "\n"))
191 			{
192 				string[] zoneInfoSegments = split(zoneInfo, " ");
193 
194 				Zone curZone;
195 				curZone.zone = zoneInfoSegments[0];
196 				curZone.zoneType = to!(ZoneType)(toUpper(zoneInfoSegments[1]));
197 				zones ~= curZone;
198 			}
199 
200 			return zones;
201 		}
202 		// If an error occurred
203 		else
204 		{
205 			// TODO: Throw an exception here
206 			throw new Exception("Error occurred");
207 		}
208 	}
209 
210 	public Record[] listLocalData()
211 	{
212 		string recordData;
213 
214 		bool status = ctl("list_local_data", "", recordData);
215 
216 		// If the records were returned into the `zoneData` string
217 		if(status)
218 		{
219 			Record[] records;
220 			foreach(string recordInfo; split(recordData, "\n"))
221 			{
222 				// SKip the empty lines
223 				if(cmp(recordInfo, "") == 0)
224 				{
225 					continue;
226 				}
227 				else
228 				{
229 					string[] recordInfoSegments = split(recordInfo, "\t");
230 
231 					Record curRecord;
232 					string domain = recordInfoSegments[0];
233 					ulong ttl = to!(ulong)(recordInfoSegments[1]);
234 
235 					try
236 					{
237 						RecordType recordType = to!(RecordType)(recordInfoSegments[3]);
238 
239 						curRecord.domain = domain;
240 						curRecord.ttl = ttl;
241 						curRecord.recordType = recordType;
242 
243 						if(recordType == RecordType.NS || recordType == RecordType.A || 
244 						  recordType == RecordType.AAAA || recordType == RecordType.CNAME ||
245 						  recordType == RecordType.PTR
246 						)
247 						{
248 							curRecord.value = recordInfoSegments[4];
249 						}
250 						else if(recordType == RecordType.SOA)
251 						{
252 							string[] soaSegments = split(recordInfoSegments[4], " ");
253 							curRecord.value = soaSegments[0];
254 
255 							// TODO: Implement SOA handling
256 							string soaEmail = soaSegments[1];
257 							ulong[] soaTuple = [to!(ulong)(soaSegments[2]),
258 												to!(ulong)(soaSegments[3]),
259 												to!(ulong)(soaSegments[4]),
260 												to!(ulong)(soaSegments[5]),
261 												to!(ulong)(soaSegments[6])];
262 							
263 							curRecord.soaEmail = soaEmail;
264 							curRecord.soaTuple = soaTuple;
265 						}
266 						else
267 						{
268 							writeln("This should never happen");
269 							assert(false);
270 						}
271 					}
272 					catch(ConvException e)
273 					{
274 						// TODO: Throw an exception here
275 						throw new Exception("Error occurred");
276 					}
277 					
278 					records ~= curRecord;
279 				}
280 				
281 			}
282 
283 			return records;
284 		}
285 		// If an error occurred
286 		else
287 		{
288 			// TODO: Throw an exception here
289 			throw new Exception("Error occurred");
290 		}
291 	}
292 }
293 
294 public enum RecordType
295 {
296 	A,
297 	AAAA,
298 	CNAME,
299 	NS,
300 	SOA,
301 	PTR
302 }
303 
304 public enum ZoneType
305 {
306 	STATIC,
307 	REDIRECT
308 }
309 
310 public struct Zone
311 {
312 	ZoneType zoneType;
313 	string zone;
314 }
315 
316 public struct Record
317 {
318 	string domain;
319 	RecordType recordType;
320 	string value;
321 	ulong ttl;
322 	string soaEmail;
323 	ulong[] soaTuple;
324 }
325 
326 unittest
327 {
328 	UnboundControl unboundCtl = new UnboundControl("::1@8953");
329 	unboundCtl.verbosity(5);
330 	unboundCtl.addLocalZone("hax.", ZoneType.STATIC);
331 	unboundCtl.addLocalData(Record("deavmi.hax.", RecordType.A, "127.0.0.1"));
332 	unboundCtl.addLocalData(Record("deavmi.hax.", RecordType.AAAA, "::1"));
333 
334 	unboundCtl.removeLocalData("deavmi.hax.");
335 
336 	unboundCtl.removeLocalZone("hax.");
337 }
338 
339 unittest
340 {
341 	UnboundControl unboundCtl = new UnboundControl("::1@8953");
342 
343 	try
344 	{
345 		Zone[] zones = unboundCtl.listLocalZones();
346 		writeln(zones);
347 
348 		Record[] records = unboundCtl.listLocalData();
349 		writeln(records);
350 	}
351 	catch(Exception e)
352 	{
353 		assert(false);
354 	}
355 	
356 }
357 
358 unittest
359 {
360 	UnboundControl unboundCtl = new UnboundControl("::1@8952");
361 	try
362 	{
363 		unboundCtl.listLocalZones();
364 		assert(false);
365 	}
366 	catch(Exception e)
367 	{
368 		assert(true);
369 	}
370 }