main.zig (5430B)
1 const std = @import("std"); 2 const MetaInfo = @import("metainfo.zig"); 3 const bencode = @import("bencode.zig"); 4 const AnyWriter = @import("anywriter.zig"); 5 const peerproto = @import("peer_protocol.zig"); 6 const trackproto = @import("tracker_protocol.zig"); 7 8 // TODO figure this out. It's not that important, I think, unless 9 // other clients have special handling for different patterns. 10 // Spec looks like a bit of a free-for-all here. 11 const peer_id = [20]u8{ 0x30, 0x30, 0x31, 0x31, 0x32, 0x32, 0x33, 0x33, 0x34, 0x34, 0x35, 0x35, 0x36, 0x36, 0x37, 0x37, 0x38, 0x38, 0x39, 0x39 }; 12 // To make this a practical torrent app... 13 // Add some UI? or keep it a console app? I might keep it as a console app for now 14 const Args = struct { 15 torrent_file: []const u8, 16 }; 17 18 fn read_args() !Args { 19 var args = std.process.args(); 20 if (!args.skip()) return error.NotEnoughArgs; 21 const nxt = args.next() orelse return error.NotEnoughArgs; 22 if (std.mem.eql(u8, nxt, "-h") or std.mem.eql(u8, nxt, "--help")) { 23 try printUsage(false); 24 return error.Help; 25 } 26 return .{ 27 .torrent_file = nxt, 28 }; 29 } 30 31 fn do_main() !void { 32 var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 33 defer _ = gpa.deinit(); 34 const a = gpa.allocator(); 35 36 const args = read_args() catch |e| switch (e) { 37 error.Help => return, 38 else => |err| return err, 39 }; 40 41 const f = try std.fs.cwd().openFile(args.torrent_file, .{}); 42 defer f.close(); 43 var fr = f.reader(); 44 var mib = try bencode.bdecode(a, fr); 45 defer mib.deinit(a); 46 var mi = try MetaInfo.parse(a, mib); 47 defer mi.deinit(a); 48 const info_hash = try mi.info.hash(a); 49 50 var c = std.http.Client{ 51 .allocator = a, 52 }; 53 defer c.deinit(); 54 55 const url = try trackproto.trackerRequestUrl(a, info_hash, peer_id, mi.info.files[0].length, mi.announce); 56 defer a.free(url); 57 var res = try c.fetch(a, .{ .location = .{ .url = url } }); 58 defer res.deinit(); 59 if (res.status != .ok) { 60 return error.TrackerHttpError; 61 } 62 63 var trb = try bencode.bdecodeBuf(a, res.body.?); 64 defer trb.deinit(a); 65 var tr = try trackproto.TrackerResp.parse(a, trb); 66 defer tr.deinit(a); 67 68 if (tr.peers.len == 0) { 69 std.log.info("no peers", .{}); 70 return; 71 } 72 73 for (tr.peers) |peer| { 74 std.log.info("peer: {}", .{peer}); 75 } 76 77 // Handle peers, PoC we're just going to handle 1 peer and download everything from them very simplistically. 78 const p = tr.peers[0]; 79 const file = mi.info.files[0]; 80 var ps = try std.net.tcpConnectToAddress(p); 81 defer ps.close(); 82 var pw = ps.writer(); 83 var pr = ps.reader(); 84 var hs: peerproto.Handshake = .{ 85 .info_hash = info_hash, 86 .peer_id = peer_id, 87 }; 88 89 try hs.write(pw); 90 var phs = try peerproto.Handshake.read(pr); 91 std.log.info("peer at {} peer_id {s}", .{ p, std.fmt.fmtSliceHexLower(&phs.peer_id) }); 92 93 var bf = try peerproto.readMessage(a, pr, peerproto.Bitfield); 94 _ = bf; // ignore it for now. 95 try peerproto.Interested.write(pw); 96 _ = try peerproto.readMessage(a, pr, peerproto.Unchoke); 97 98 var of = try std.fs.cwd().createFile(file.name, .{}); 99 defer of.close(); 100 errdefer { 101 // try to truncate the now-bad file... 102 of.setEndPos(0) catch {}; 103 } 104 105 // Read the piece into memory, we'll check the hash before it goes to disk... 106 var piece_buf = try a.alloc(u8, mi.info.piece_length); 107 defer a.free(piece_buf); 108 109 for (0..mi.info.pieceCount()) |pi| { 110 const piece_length = @min(mi.info.piece_length, file.length - (pi * mi.info.piece_length)); 111 var s1 = std.crypto.hash.Sha1.init(.{}); 112 113 // Send a request message for each 16KiB block of the first piece 114 const blklen: u32 = 16 * 1024; 115 var blkcount = try std.math.divCeil(u32, piece_length, blklen); 116 for (0..blkcount) |i| { 117 const begin = std.math.cast(u32, i * blklen).?; 118 const len = @min(blklen, piece_length - begin); 119 const req = peerproto.Request{ 120 .index = @intCast(pi), 121 .begin = begin, 122 .length = len, 123 }; 124 std.log.info("Request {any}", .{req}); 125 try req.write(pw); 126 var piece = try peerproto.readMessage(a, pr, peerproto.Piece); 127 defer piece.deinit(a); 128 if (piece.index != req.index) return error.ProtocolError; 129 if (piece.begin != req.begin) return error.ProtocolError; 130 if (piece.block.len != req.length) return error.ProtocolError; 131 s1.update(piece.block); 132 @memcpy(piece_buf[piece.begin .. piece.begin + piece.block.len], piece.block); 133 } 134 var ah = s1.finalResult(); 135 var ph0 = mi.info.pieceHash(pi).?; 136 if (std.mem.eql(u8, &ah, &ph0)) { 137 try of.writeAll(piece_buf[0..piece_length]); 138 } else { 139 return error.BadHash; 140 } 141 } 142 std.log.info("fin", .{}); 143 } 144 145 pub fn main() !void { 146 do_main() catch |e| { 147 try printUsage(true); 148 return e; 149 }; 150 } 151 152 fn printUsage(err: bool) !void { 153 var f = if (err) std.io.getStdErr() else std.io.getStdOut(); 154 try f.writeAll( 155 \\ Usage: zbt TORRENTFILE 156 \\ Download the data specfied by TORRENTFILE to the current directory 157 \\ 158 ); 159 } 160 161 test { 162 _ = bencode; 163 _ = MetaInfo; 164 _ = peerproto; 165 }