1 // WOOT network protocol
3 // coded by Ketmar // Invisible Vector <ketmar@ketmar.no-ip.org>
4 // Understanding is not required. Only obedience.
6 // Permission is hereby granted, free of charge, to any person obtaining a copy
7 // of this software and associated documentation files (the "Software"), to deal
8 // in the Software without restriction, including without limitation the rights
9 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 // copies of the Software, and to permit persons to whom the Software is
11 // furnished to do so, subject to the following conditions:
13 // The above copyright notice and this permission notice shall be included in
14 // all copies or substantial portions of the Software.
16 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24 // WARNING! crypto here is TOTALLY INVALID! don't do it like that!
27 //version = woot_net_chatty;
30 // ////////////////////////////////////////////////////////////////////////// //
34 import std
.digest
.sha
;
37 import iv
.chachasimple
;
39 import iv
.prng
.seeder
;
46 // ////////////////////////////////////////////////////////////////////////// //
48 import core
.sys
.windows
.winsock2
;
49 enum MSG_NOSIGNAL
= 0;
51 import core
.sys
.posix
.netinet
.in_
;
52 import core
.sys
.posix
.sys
.select
;
53 import core
.sys
.posix
.sys
.socket
;
57 // ////////////////////////////////////////////////////////////////////////// //
58 public string
toString() (in auto ref sockaddr_in it
) {
59 import std
.format
: format
;
61 "%u.%u.%u.%u:%u".format(
62 it
.sin_addr
.s_addr
&0xff, (it
.sin_addr
.s_addr
>>8)&0xff, (it
.sin_addr
.s_addr
>>16)&0xff, (it
.sin_addr
.s_addr
>>24)&0xff,
67 // ////////////////////////////////////////////////////////////////////////// //
68 align(1) struct WootNetOp
{
71 string
toString () const { return op
.toString
; }
73 pure nothrow @safe @nogc:
75 ubyte[ChaCha20
.IVSize
] nonce
;
78 WStrOp.OpType opType = WStrOp.OpType.None;
79 UUID id; // the id assigned at creation-time that this character keeps forever
81 dchar ch; // the user-visible character that this WChar represents; uint.max means "invisible"
82 // as per the algorithm outlines in the document, each character specifies which two characters it belongs between
83 // these are the ids of the chars that must go somewhere before and somewhere after this character respectively
90 // ////////////////////////////////////////////////////////////////////////// //
91 // we will use UUIDs to identify clients in the network,
94 * WOOT network packet:
96 * 4 bytes: client id (self for send, remote for recv)
97 * ubyte operation (see PktType)
101 * 8 bytes: char id (UUID, opid)
102 * 8 bytes: prev id (UUID, opid)
103 * 8 bytes: next id (UUID, opid)
105 * 8 bytes: char id (UUID, opid)
111 * pktseq is used as acked packet id
122 enum PktType
: ubyte {
132 static struct Packet
{
137 @property bool valid () const pure nothrow @safe @nogc { pragma(inline
, true); return (seq
!= 0 && data
.length
>= ChaCha20
.IVSize
); }
139 void clear () { delete data
; seq
= 0; ack
= false; }
141 void setup (uint aseqid
, bool aack
=false) {
144 data
.unsafeArraySetLength(ChaCha20
.IVSize
);
148 void put(T
) (T v
) if (__traits(isIntegral
, T
)) {
149 auto dp
= cast(const(ubyte)*)&v
;
150 foreach (ubyte b
; dp
[0..T
.sizeof
]) data
.unsafeArrayAppend(b
);
154 void put(T:UUID) (in auto ref T v) {
155 foreach (ubyte b; v.data[]) data.unsafeArrayAppend(b);
161 uint cid
; // client id [1..]
162 Packet
[] queue
; // send queue
165 MonoTime lastActivity
;
169 foreach (ref pkt
; queue
) pkt
.clear();
175 ubyte[ChaCha20
.KeySize
] mKey
;
180 uint[uint] cid2peerMap
;
182 public sockaddr_in myaddr
;
185 static struct HelloQueue
{
189 HelloQueue
[] mHelloQ
;
191 int xsend
= -1; // <0: hellos; >=0: peers
194 Packet
buildHello (ref HelloQueue hq
, out sockaddr_in aout
) {
195 if (sk
< 0) return Packet();
196 if (hq
.addr
.sin_family
!= AF_INET
) return Packet();
197 if (hq
.addr
.sin_port
== 0) return Packet();
198 if (hq
.addr
.sin_addr
.s_addr
== 0 || hq
.addr
.sin_addr
.s_addr
== uint.max
) return Packet();
204 hq
.pkt
.put(cast(ubyte)PktType
.Hello
);
206 encodePacket(hq
.pkt
);
208 //return (sendto(sk, hq.pkt.data.ptr, hq.pkt.data.length, MSG_NOSIGNAL, cast(sockaddr*)&hq.addr, hq.addr.sizeof) == hq.pkt.data.length);
213 void gotPacketFrom (in ref sockaddr_in addr
) {
214 foreach (immutable idx
, const ref hq
; mHelloQ
) {
215 if (hq
.addr
.sin_addr
.s_addr
== addr
.sin_addr
.s_addr
&& hq
.addr
.sin_port
== addr
.sin_port
) {
216 mHelloQ
.unsafeArrayRemove(cast(int)idx
);
222 void advanceXSend () {
225 if (-xsend
<= mHelloQ
.length
) return;
231 if (xsend
>= mPeers
.length
) break;
232 if (mPeers
[xsend
].netvalid
&& mPeers
[xsend
].queue
.length
) return;
239 void encodePacket (ref Packet pkt
) {
240 if (!pkt
.valid
) assert(0, "internal error");
243 foreach (ref ubyte b
; pkt
.data
[0..ChaCha20
.IVSize
]) {
244 b
= cast(ubyte)prng
.front
;
249 auto cyph
= ChaCha20(mKey
, pkt
.data
[0..ChaCha20
.IVSize
]);
250 cyph
.processBuf(pkt
.data
[ChaCha20
.IVSize
..$]);
253 void initPacket (ref Packet pkt
) {
254 if (pktseq
== 0) ++pktseq
;
258 Packet
encodePacketOp (in ref WootNetOp op
) {
261 if (op
.op
.opType
== WStrOp
.OpType
.None || op
.op
.opType
< WStrOp
.OpType
.min || op
.op
.opType
> WStrOp
.OpType
.max
) return pkt
;
268 final switch (op
.op
.opType
) {
269 case WStrOp
.OpType
.Insert
:
270 pkt
.put(cast(ubyte)PktType
.Insert
);
271 pkt
.put(cast(uint)op
.op
.ch
.ch
);
272 pkt
.put(op
.op
.ch
.id
.uid
);
273 pkt
.put(op
.op
.ch
.prev
.uid
);
274 pkt
.put(op
.op
.ch
.next
.uid
);
276 case WStrOp
.OpType
.Delete
:
277 pkt
.put(cast(ubyte)PktType
.Delete
);
278 pkt
.put(op
.op
.ch
.id
.uid
);
280 case WStrOp
.OpType
.None
: assert(0);
287 Packet
encodePacketPeer() (in auto ref Peer peer
) {
289 if (!peer
.netvalid
) return pkt
;
295 pkt
.put(cast(ubyte)PktType
.Peer
);
296 pkt
.put(peer
.addr
.sin_addr
.s_addr
);
297 pkt
.put(peer
.addr
.sin_port
);
299 //conprintfln("sending peer %s %s", peer.uuid, peer.addr.sin_port);
304 Packet
encodePacketAck() (uint seqn
) {
306 if (seqn
== 0) return pkt
;
308 pkt
.setup(seqn
, true);
312 pkt
.put(cast(ubyte)PktType
.Ack
);
318 Packet
encodePacketPing () {
325 pkt
.put(cast(ubyte)PktType
.Ping
);
331 void peerAck (ref Peer peer
, uint seqn
) {
332 if (!peer
.netvalid || seqn
== 0 ||
!peer
.netvalid
) return;
333 // check if we already have this ack
334 foreach (immutable idx
, ref pkt
; peer
.queue
) {
337 auto pk
= encodePacketAck(seqn
);
338 if (pk
.valid
) peer
.queue
.unsafeArrayInsertBefore(cast(int)idx
, pk
);
341 if (pkt
.seq
== seqn
) return;
343 auto pk
= encodePacketAck(seqn
);
344 if (pk
.valid
) peer
.queue
.unsafeArrayInsertBefore(0, pk
);
348 Peer
* newPeer() (uint peercid
) {
349 if (peercid
== 0) return null;
350 if (peercid
== mMyCId
) return null;
351 if (auto pip
= peercid
in cid2peerMap
) return &mPeers
[*pip
];
352 mPeers
.unsafeArrayAppend(Peer(peercid
));
353 cid2peerMap
[peercid
] = cast(uint)(mPeers
.length
-1);
354 mPeers
[$-1].lastActivity
= MonoTime
.currTime
;
358 Peer
* findPeer() (uint peercid
) {
359 if (auto pip
= peercid
in cid2peerMap
) return &mPeers
[*pip
];
363 Peer
* findPeerByAddr() (in auto ref sockaddr_in addr
) {
364 foreach (immutable idx
, ref peer
; mPeers
) {
365 if (!peer
.netvalid
) continue;
366 if (peer
.addr
.sin_port
!= addr
.sin_port
) continue;
367 if (peer
.addr
.sin_addr
.s_addr
!= addr
.sin_addr
.s_addr
) continue;
374 this (const(char)[] akey
, ushort aport
) {
375 import core
.stdc
.string
: memset
;
376 memset(&myaddr
, 0, myaddr
.sizeof
);
379 mKey
= sha256Of(akey
);
380 prng
.seed(getUlongSeed
, getUlongSeed
);
383 foreach (ref ubyte b
; (cast(ubyte*)&mMyCId
)[0..mMyCId
.sizeof
]) {
384 b
= cast(ubyte)prng
.front
;
389 sk
= socket(AF_INET
, SOCK_DGRAM
, IPPROTO_UDP
);
390 if (sk
< -1) throw new Exception("cannot create socket");
391 myaddr
.sin_family
= AF_INET
;
392 myaddr
.sin_port
= htons(aport
);
393 myaddr
.sin_addr
.s_addr
= htonl(INADDR_ANY
);
394 if (bind(sk
, cast(sockaddr
*)&myaddr
, myaddr
.sizeof
) == -1) {
396 throw new Exception("cannot bind socket");
400 ~this () { closeSocket(); }
402 final private void closeSocket () nothrow @trusted @nogc {
403 import core
.stdc
.string
: memset
;
407 import core
.sys
.posix
.unistd
: close
;
412 memset(&myaddr
, 0, myaddr
.sizeof
);
415 void addPeer (const(char)[] host
, const(char)[] port
) {
416 import std
.socket
, std
.conv
: to
;
417 auto nport
= port
.to
!ushort;
418 auto addr
= new InternetAddress(host
, nport
);
419 scope(exit
) delete addr
;
420 addPeer(cast(sockaddr_in
)*addr
.name
);
423 void addPeer() (in auto ref sockaddr_in sin
) {
425 if (sin
.sin_port
== 0 || sin
.sin_addr
.s_addr
== 0 || sin
.sin_addr
.s_addr
== uint.max
) return;
426 foreach (const ref hq
; mHelloQ
) {
427 if (hq
.addr
.sin_addr
.s_addr
== sin
.sin_addr
.s_addr
&& hq
.addr
.sin_port
== sin
.sin_port
) return; // known peer
432 hq
.addr
.sin_family
= AF_INET
;
433 mHelloQ
.unsafeArrayAppend(hq
);
436 private final Packet
createOpPacket() (in auto ref WStrOp op
) {
437 if (op
.empty
) return Packet();
440 return encodePacketOp(netop
);
443 final void queue (const(WStrOp
)[] ops
...) {
444 if (mPeers
.length
== 0) return;
445 foreach (const ref op
; ops
[]) {
446 if (op
.empty
) continue;
447 auto pk
= createOpPacket(op
);
448 if (!pk
.valid
) continue;
449 // don't send to ourself
450 foreach (ref peer
; mPeers
[]) {
451 if (!peer
.netvalid
) continue;
452 peer
.queue
.unsafeArrayAppend(pk
);
457 final void processSends () {
458 import core
.stdc
.string
: memset
;
462 foreach (immutable _
; 0..mHelloQ
.length
+mPeers
.length
) {
463 // check if we can send something
464 memset(&to
, 0, to
.sizeof
);
467 if (select(sk
+1, null, &wrset
, null, &to
) < 1) return;
468 if (!FD_ISSET(sk
, &wrset
)) return;
469 //conwriteln("can send! xsend=", xsend);
477 if (-xsend
<= mHelloQ
.length
) {
478 auto pk
= buildHello(mHelloQ
[-xsend
-1], aout
);
480 pkt
[pktpos
..pktpos
+pk
.data
.length
] = pk
.data
[];
481 pktpos
+= cast(int)pk
.data
.length
;
484 } else if (xsend
>= 0 && xsend
< mPeers
.length
&& mPeers
[xsend
].queue
.length
&& mPeers
[xsend
].netvalid
) {
485 auto peer
= &mPeers
[xsend
];
486 version(woot_net_chatty
) conwriteln("sending packet to peer ", peer
.cid
);
489 while (cnum
< peer
.queue
.length
) {
490 if (pkt
.length
-pktpos
<= peer
.queue
[cnum
].data
.length
) break;
491 pkt
[pktpos
..pktpos
+peer
.queue
[cnum
].data
.length
] = peer
.queue
[cnum
].data
;
492 pktpos
+= cast(int)peer
.queue
[cnum
].data
.length
;
493 // remove ack packets (peers will resend non-acked packets anyway)
494 if (peer
.queue
[cnum
].ack
) {
495 peer
.queue
[cnum
].clear();
496 peer
.queue
.unsafeArrayRemove(cnum
);
503 version(woot_net_chatty
) conwriteln("NET: sending packet to ", aout
.toString
);
504 if (sendto(sk
, pkt
.ptr
, pktpos
, MSG_NOSIGNAL
, cast(sockaddr
*)&aout
, aout
.sizeof
) != pktpos
) return;
509 final void checkTimeouts () {
510 auto ct
= MonoTime
.currTime
;
511 foreach (ref peer
; mPeers
[]) {
512 if (!peer
.netvalid
) continue;
515 if ((ct
-peer
.lastActivity
).total
!"msecs" > 1000) {
517 conwriteln("NET: disconnected ", peer
.cid
);
523 if ((ct
-peer
.lastActivity
).total
!"msecs" > 500) {
524 auto pk
= encodePacketPing();
526 peer
.pingSent
= true;
527 peer
.queue
.unsafeArrayInsertBefore(0, pk
);
534 final void recv (WString wstr
, void delegate (in WStrOp op
) cb
) {
535 import core
.stdc
.string
: memset
;
538 assert(wstr
!is null);
546 if (select(sk
+1, &rdset
, null, null, &to
) < 1) return;
547 if (!FD_ISSET(sk
, &rdset
)) return;
549 sockaddr_in it
= void;
550 socklen_t itlen
= it
.sizeof
;
551 memset(&it
, 0, it
.sizeof
);
555 auto rd
= recvfrom(sk
, pkt
.ptr
, pkt
.length
, 0/*MSG_DONTWAIT*/, cast(sockaddr
*)&it
, &itlen
);
557 gotPacketFrom(it
); // clear hellos
559 version(woot_net_chatty
) conwriteln("NET: packet from ", it
.toString
);
562 datagram
: while (ccpktpos
< rd
&& rd
-ccpktpos
>= ChaCha20
.IVSize
) {
563 ubyte[MTU
] xdata
= void;
564 xdata
[0..rd
-ccpktpos
] = pkt
[ccpktpos
..rd
];
567 auto cyph
= ChaCha20(mKey
, xdata
[0..ChaCha20
.IVSize
]);
568 ccpktpos
+= ChaCha20
.IVSize
;
569 auto data
= xdata
[ChaCha20
.IVSize
..rd
];
570 cyph
.processBuf(data
[]);
572 if (data
.length
< 4+4+1) break;
574 T
get(T
) () if (__traits(isIntegral
, T
)) {
575 if (data
.length
< T
.sizeof
) throw new Exception("invalid packed received");
576 ccpktpos
+= cast(int)T
.sizeof
;
577 T res
= *(cast(T
*)data
.ptr
);
578 data
= data
[T
.sizeof
..$];
583 uint rseq
= get
!uint;
584 uint ruuid
= get
!uint;
585 ubyte opcode
= get
!ubyte;
587 if (opcode
< PktType
.min || opcode
> PktType
.max
) break; // alas
589 auto peer
= findPeer(ruuid
);
592 version(woot_net_chatty
) conwriteln("NET: new peer comes: ", ruuid
);
593 peer
= newPeer(ruuid
);
594 if (peer
is null) break; // oops
595 version(woot_net_chatty
) conwriteln(" ", peer
.cid
, " : ", (peer
.cid
== ruuid
));
598 version(woot_net_chatty
) conprintfln("NET: msg from %u.%u.%u.%u:%u",
599 it
.sin_addr
.s_addr
&0xff, (it
.sin_addr
.s_addr
>>8)&0xff, (it
.sin_addr
.s_addr
>>16)&0xff, (it
.sin_addr
.s_addr
>>24)&0xff,
602 it
.sin_family
= AF_INET
;
604 peer
.netvalid
= true;
605 peer
.lastActivity
= MonoTime
.currTime
;
606 peer
.pingSent
= false;
611 // new client arrived
612 version(woot_net_chatty
) conwriteln("NET: HELLO, port=", htons(peer
.addr
.sin_port
));
614 peerAck(*peer
, rseq
);
615 // send all known peers to it
616 foreach (ref pr
; mPeers
[]) {
617 if (pr
.cid
!= peer
.cid
) {
618 auto pk
= encodePacketPeer(pr
);
619 if (pk
.valid
) peer
.queue
.unsafeArrayAppend(pk
);
622 // send all the text to it
624 foreach (immutable idx
; 0..wstr
.length
) {
627 auto op
= WStrOp(WStrOp
.OpType
.Insert
, wstr
[idx
]);
628 auto pk
= createOpPacket(op
);
629 if (pk
.valid
) peer
.queue
.unsafeArrayAppend(pk
);
631 ch
= WChar(ch
.id
, ' ', ch
.prev
, ch
.next
);
632 auto op
= WStrOp(WStrOp
.OpType
.Insert
, ch
);
633 auto pk
= createOpPacket(op
);
634 if (pk
.valid
) peer
.queue
.unsafeArrayAppend(pk
);
635 op
= WStrOp(WStrOp
.OpType
.Delete
, wstr
[idx
]);
636 pk
= createOpPacket(op
);
637 if (pk
.valid
) peer
.queue
.unsafeArrayAppend(pk
);
643 version(woot_net_chatty
) conwriteln("NET: INSERT");
645 peerAck(*peer
, rseq
);
647 dchar ch
= cast(dchar)get
!uint;
649 auto chid
= WCharId(get
!ulong);
651 auto previd
= WCharId(get
!ulong);
653 auto nextid
= WCharId(get
!ulong);
654 auto wc
= WChar(chid
, ch
, previd
, nextid
);
655 cb(WStrOp(WStrOp
.OpType
.Insert
, wc
));
658 version(woot_net_chatty
) conwriteln("NET: DELETE");
660 peerAck(*peer
, rseq
);
662 auto chid
= WCharId(get
!ulong);
663 auto wc
= WChar(chid
, 0);
664 cb(WStrOp(WStrOp
.OpType
.Delete
, wc
));
667 // ping request comes
668 version(woot_net_chatty
) conwriteln("NET: PING");
670 peerAck(*peer
, rseq
);
674 version(woot_net_chatty
) conwriteln("NET: ACK");
675 // check if this is "hello" ack
676 foreach (immutable c
, ref hq
; mHelloQ
) {
677 if (hq
.pkt
.valid
&& hq
.pkt
.seq
== rseq
) {
678 version(woot_net_chatty
) conwriteln(" hello acked!");
680 mHelloQ
.unsafeArrayRemove(cast(int)c
);
684 // check peer packets
685 foreach (immutable c
, ref pk
; peer
.queue
) {
686 if (pk
.seq
== rseq
) {
687 peer
.queue
.unsafeArrayRemove(cast(int)c
);
694 peerAck(*peer
, rseq
);
695 it
.sin_family
= AF_INET
;
696 it
.sin_addr
.s_addr
= get
!uint;
697 it
.sin_port
= get
!ushort;
698 if (it
.sin_port
== myaddr
.sin_port
&& it
.sin_addr
.s_addr
== myaddr
.sin_addr
.s_addr
) break;
699 version(woot_net_chatty
) conwriteln("NET: PEER ", it
.toString
);
700 peer
= findPeerByAddr(it
);
701 if (peer
is null) addPeer(it
);
703 default: throw new Exception("invalid packed received");
705 } catch (Exception e
) {
709 } // one received datagram
714 final @property uint mycid () const pure nothrow @safe @nogc { pragma(inline
, true); return mMyCId
; }
719 shared static this () {
721 if (WSAStartup(0x0202, &wsa
) != 0) assert(0, "cannot initialize shitdoze sockets");