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 }