1 module libyggdrasil.libyggdrasil; 2 3 import libyggdrasil.utils : attemptString; 4 5 import std.stdio; 6 import std.json; 7 import std.socket; 8 import std.string; 9 import std.conv : to; 10 import libchonky : ChonkReader; 11 12 13 14 public final class BuildInfo 15 { 16 public ubyte wellFormed = 0; 17 private string _version = "none", arch = "none", platform = "none", name = "none"; 18 19 this(JSONValue nodeInfo) 20 { 21 /* Attempt extraction */ 22 extractInfo(nodeInfo); 23 } 24 25 private void extractInfo(JSONValue nodeInfo) 26 { 27 if(attemptString(nodeInfo, &_version, "buildversion")) 28 { 29 wellFormed++; 30 } 31 if(attemptString(nodeInfo, &arch, "buildarch")) 32 { 33 wellFormed++; 34 } 35 if(attemptString(nodeInfo, &platform, "buildplatform")) 36 { 37 wellFormed++; 38 } 39 if(attemptString(nodeInfo, &name, "buildname")) 40 { 41 wellFormed++; 42 } 43 } 44 45 public bool isWellFormed() 46 { 47 return wellFormed == 4; 48 } 49 50 public string getVersion() 51 { 52 return _version; 53 } 54 55 public string getName() 56 { 57 return name; 58 } 59 60 public string getPlatform() 61 { 62 return platform; 63 } 64 65 public string getArchitecture() 66 { 67 return arch; 68 } 69 70 } 71 72 public final class NodeService 73 { 74 private string serviceName; 75 private string protocol; 76 private ushort[] ports; 77 78 this(string serviceName, string protocol, ushort[] ports) 79 { 80 this.serviceName = serviceName; 81 this.protocol = protocol; 82 this.ports = ports; 83 } 84 85 public string getName() 86 { 87 return serviceName; 88 } 89 90 public string getProtocol() 91 { 92 return protocol; 93 } 94 95 public ushort[] getPorts() 96 { 97 return ports; 98 } 99 } 100 101 public class NodeInfo 102 { 103 /* Key of the node this NodeInfo is associated with */ 104 private string key; 105 106 /** 107 * NodeInfo data 108 */ 109 public ubyte wellFormed = 0; 110 private JSONValue nodeInfoJSON; 111 private string name = "<no name>"; 112 private string group = "<no name>"; 113 private string location = "<no name>"; 114 private string contact = "<no name>"; 115 116 117 /** 118 * Given the response JSON this will extract the 119 * key, NodeInfo as a whole and also attempt to extract 120 * standardized aspects of the NodeInfo 121 */ 122 this(JSONValue nodeInfoJSON) 123 { 124 /* Save the key from the response */ 125 key = nodeInfoJSON.object().keys[0]; 126 127 /* Extract the entry */ 128 this.nodeInfoJSON = nodeInfoJSON[key]; 129 130 /* Attempt to parse the standardized parts */ 131 parse(); 132 } 133 134 public string getName() 135 { 136 return name; 137 } 138 139 public string getGroup() 140 { 141 return group; 142 } 143 144 public string getLocation() 145 { 146 return location; 147 } 148 149 public string getContact() 150 { 151 return contact; 152 } 153 154 public string getKey() 155 { 156 return key; 157 } 158 159 public JSONValue getFullJSON() 160 { 161 return nodeInfoJSON; 162 } 163 164 private void parse() 165 { 166 167 if(attemptString(nodeInfoJSON, &name, "name")) 168 { 169 wellFormed++; 170 } 171 172 if(attemptString(nodeInfoJSON, &contact, "contact")) 173 { 174 wellFormed++; 175 } 176 177 if(attemptString(nodeInfoJSON, &group, "group")) 178 { 179 wellFormed++; 180 } 181 182 if(attemptString(nodeInfoJSON, &location, "location")) 183 { 184 wellFormed++; 185 } 186 187 188 189 } 190 191 public bool isWellFormed() 192 { 193 return wellFormed == 4; 194 } 195 196 public BuildInfo getBuildInfo() 197 { 198 return new BuildInfo(nodeInfoJSON); 199 } 200 201 202 public override string toString() 203 { 204 /* TODO: */ 205 return ""; 206 } 207 } 208 209 public final class DHTInfo 210 { 211 this() 212 { 213 /* TODO: Implement me */ 214 } 215 } 216 217 /** 218 * YggdrasilNode 219 * 220 * Given a key 221 */ 222 public class YggdrasilNode 223 { 224 private YggdrasilPeer peer; 225 226 private string key; 227 private JSONValue selfInfo; 228 private JSONValue nodeInfo; 229 230 231 this(YggdrasilPeer peer, string key) 232 { 233 this.peer = peer; 234 this.key = key; 235 } 236 237 public NodeInfo getNodeInfo() 238 { 239 /* Create the NodeInfo request */ 240 YggdrasilRequest req = new YggdrasilRequest(RequestType.NODEINFO, key); 241 242 /* Make the request */ 243 YggdrasilResponse resp = makeRequest(peer, req); 244 245 /* Create a new NodeInfo object */ 246 return new NodeInfo(resp.getJSON()); 247 } 248 249 public DHTInfo getDHT() 250 { 251 /* Create the getDHT request */ 252 YggdrasilRequest req = new YggdrasilRequest(RequestType.GETDHT, key); 253 254 /* Make the request */ 255 YggdrasilResponse resp = makeRequest(peer, req); 256 257 /* TODO: Implement me */ 258 return null; 259 } 260 261 public YggdrasilNode[] getPeers() 262 { 263 /* Peers */ 264 YggdrasilNode[] peers; 265 266 /* Create the getPeers request */ 267 YggdrasilRequest req = new YggdrasilRequest(RequestType.GETPEERS, key); 268 269 /* Make the request */ 270 YggdrasilResponse resp = makeRequest(peer, req); 271 272 /* Get the JSON and process the list */ 273 JSONValue respJSON = resp.getJSON(); 274 275 foreach(JSONValue ckey; respJSON[respJSON.object().keys[0]]["keys"].array()) 276 { 277 string ckeyStr = ckey.str(); 278 peers ~= new YggdrasilNode(peer, ckeyStr); 279 } 280 281 return peers; 282 } 283 284 public string getKey() 285 { 286 return key; 287 } 288 289 public override string toString() 290 { 291 /* TODO: Fetch getNodeInfo, if possible, else leave key */ 292 return getNodeInfo().toString(); 293 } 294 295 296 /** 297 * Checks if the node is online 298 * 299 * This is implemented by doing a getDHT() 300 */ 301 public bool ping() 302 { 303 try 304 { 305 /* Attempt to do getDHT */ 306 getDHT(); 307 return true; 308 } 309 catch(YggdrasilException) 310 { 311 return false; 312 } 313 } 314 } 315 316 /** 317 * YggdrasilNode 318 * 319 * This represents a peer of which we can 320 * connect to its control socket using TCP 321 */ 322 public class YggdrasilPeer 323 { 324 private Address yggdrasilAddress; 325 326 this(Address yggdrasilAddress) 327 { 328 this.yggdrasilAddress = yggdrasilAddress; 329 } 330 331 public Address getAddress() 332 { 333 return yggdrasilAddress; 334 } 335 336 private void initData() 337 { 338 /* TODO: Add exception throwing here */ 339 340 341 } 342 343 public YggdrasilNode fetchNode(string key) 344 { 345 return new YggdrasilNode(this, key); 346 } 347 } 348 349 public enum RequestType 350 { 351 NODEINFO, GETDHT, GETPEERS, GETSELF 352 } 353 354 public final class YggdrasilRequest 355 { 356 private RequestType requestType; 357 private string key; 358 359 this(RequestType requestType, string key) 360 { 361 this.requestType = requestType; 362 this.key = key; 363 } 364 365 366 public JSONValue generateJSON() 367 { 368 JSONValue requestBlock; 369 370 /* Set the key of the node to request from */ 371 requestBlock["key"] = key; 372 373 if(requestType == RequestType.NODEINFO) 374 { 375 requestBlock["request"] = "getnodeinfo"; 376 } 377 else if(requestType == RequestType.GETSELF) 378 { 379 requestBlock["request"] = "debug_remotegetself"; 380 } 381 else if(requestType == RequestType.GETPEERS) 382 { 383 requestBlock["request"] = "debug_remotegetpeers"; 384 } 385 else if(requestType == RequestType.GETDHT) 386 { 387 requestBlock["request"] = "debug_remotegetdht"; 388 } 389 390 391 392 return requestBlock; 393 } 394 } 395 396 public final class YggdrasilResponse 397 { 398 private JSONValue responseBlock; 399 400 this(JSONValue responseBlock) 401 { 402 this.responseBlock = responseBlock; 403 } 404 405 public JSONValue getJSON() 406 { 407 return responseBlock; 408 } 409 } 410 411 public final class YggdrasilException : Exception 412 { 413 public enum ErrorType 414 { 415 CONTROL_SOCKET_ERROR, 416 JSON_PARSE_ERROR, 417 TIMED_OUT 418 } 419 420 private ErrorType errType; 421 422 this(ErrorType errType) 423 { 424 super("YggdrasilError: "~to!(string)(errType)); 425 this.errType = errType; 426 } 427 428 public ErrorType getError() 429 { 430 return errType; 431 } 432 } 433 434 public YggdrasilResponse makeRequest(YggdrasilPeer peer, YggdrasilRequest request) 435 { 436 /* The response */ 437 YggdrasilResponse response; 438 439 /* Communication socket */ 440 Socket controlSocket; 441 442 try 443 { 444 /* Attempt to create the socket and connect it */ 445 controlSocket = new Socket(AddressFamily.INET6, SocketType.STREAM, ProtocolType.TCP); 446 controlSocket.connect(peer.getAddress()); 447 448 /* Make the request */ 449 JSONValue requestBlock = request.generateJSON(); 450 controlSocket.send(cast(byte[])toJSON(requestBlock)); 451 452 /* Await reply till socket closes */ 453 ChonkReader reader = new ChonkReader(controlSocket); 454 byte[] buffer; 455 reader.receiveUntilClose(buffer); 456 457 /* Parse the response */ 458 JSONValue responseBlock; 459 try 460 { 461 /* Parse the JSON */ 462 responseBlock = parseJSON(cast(string)buffer); 463 464 /* Check status of request */ 465 if(cmp(responseBlock["status"].str(), "success") == 0) 466 { 467 /* Extract response */ 468 JSONValue reuqestResponse = responseBlock["response"]; 469 470 /* Create the YggdrasilResponse object */ 471 response = new YggdrasilResponse(reuqestResponse); 472 473 /* Close the socket */ 474 controlSocket.close(); 475 } 476 else 477 { 478 throw new YggdrasilException(YggdrasilException.ErrorType.TIMED_OUT); 479 } 480 } 481 catch(JSONException e) 482 { 483 throw new YggdrasilException(YggdrasilException.ErrorType.JSON_PARSE_ERROR); 484 } 485 } 486 catch(SocketOSException e) 487 { 488 throw new YggdrasilException(YggdrasilException.ErrorType.CONTROL_SOCKET_ERROR); 489 } 490 491 return response; 492 }