zbt

CLI Bittorrent client, written in Zig
Log | Files | Refs | README

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 }