diff --git a/codex/bittorrent/torrentparser.nim b/codex/bittorrent/torrentparser.nim new file mode 100644 index 00000000..0050fd31 --- /dev/null +++ b/codex/bittorrent/torrentparser.nim @@ -0,0 +1,41 @@ +import std/strutils +import std/re + +import pkg/questionable/results +import pkg/stew/byteutils +import pkg/stew/base10 + +import ../errors + +proc extractInfoFromTorrent*(torrentBytes: seq[byte]): ?!seq[byte] = + ## Extract the info from a torrent file + ## + ## params: + ## torrentBytes: the torrent file bytes + ## + ## returns: the bytes containing only the content of the info dictionary + ## or a failure if info is not found or invalid + let torrentStr = string.fromBytes(torrentBytes) + if torrentStr.contains("file tree") or torrentStr.contains("piece layers"): + return failure("Torrent v2 provided. Only v1 is currently supported.") + let infoKeyPos = torrentStr.find("info") + if infoKeyPos == -1: + return failure("Torrent file does not contain info dictionary.") + let infoStartPos = infoKeyPos + "info".len + if torrentStr[infoStartPos] != 'd': + return failure("Torrent file does not contain valid info dictionary.") + + var matches = newSeq[tuple[first, last: int]](1) + let (_, piecesEndIndex) = torrentStr.findBounds(re"pieces(\d+):", matches) + if matches.len == 1: + let (first, last) = matches[0] + let piecesLenStr = torrentStr[first .. last] + without piecesLen =? Base10.decode(uint, piecesLenStr).mapFailure, err: + return failure("Error decoding pieces length: " & err.msg) + let piecesEndMarkerPos = piecesEndIndex + 1 + piecesLen.int + if torrentStr[piecesEndMarkerPos] != 'e': + return failure("Torrent file does not contain valid pieces.") + let infoDirStr = torrentStr[infoStartPos .. piecesEndMarkerPos] + infoDirStr.toBytes().success + else: + return failure("Torrent file does not contain valid pieces.") diff --git a/codex/rest/api.nim b/codex/rest/api.nim index d41781f3..c75361c4 100644 --- a/codex/rest/api.nim +++ b/codex/rest/api.nim @@ -43,6 +43,7 @@ import ../utils/options import ../bittorrent/manifest import ../bittorrent/torrentdownloader import ../bittorrent/magnetlink +import ../bittorrent/torrentparser import ../tarballs/[directorymanifest, directorydownloader, tarballnodeextensions] @@ -681,6 +682,57 @@ proc initDataApi(node: CodexNodeRef, repoStore: RepoStore, router: var RestRoute Http422, "The magnet link does not contain a valid info hash." ) await node.retrieveInfoHash(infoHash, resp = resp) + # return + # RestApiResponse.response($magnetLink, Http200, "text/plain") + + router.api(MethodOptions, "/api/codex/v1/torrent/torrent-file") do( + resp: HttpResponseRef + ) -> RestApiResponse: + if corsOrigin =? allowedOrigin: + resp.setCorsHeaders("POST", corsOrigin) + resp.setHeader( + "Access-Control-Allow-Headers", "content-type, content-disposition" + ) + + resp.status = Http204 + await resp.sendBody("") + + router.api(MethodPost, "/api/codex/v1/torrent/torrent-file") do( + contentBody: Option[ContentBody], resp: HttpResponseRef + ) -> RestApiResponse: + let mimeType = request.headers.getString(ContentTypeHeader) + echo "mimeType: ", mimeType + if mimeType != "application/json" and mimeType != "application/octet-stream": + return RestApiResponse.error( + Http422, + "Missing \"Content-Type\" header: expected either \"application/json\" or \"application/octet-stream\".", + ) + without torrentBytes =? contentBody .? data: + return RestApiResponse.error(Http422, "No torrent file content provided.") + var infoBytes: seq[byte] + if mimeType == "application/json": + without torrentManifest =? BitTorrentManifest.fromJson(torrentBytes): + return RestApiResponse.error(Http422, "Invalid torrent JSON file content.") + echo "torrentManifest: ", torrentManifest + let torrentInfo = torrentManifest.info + # very basic validation for now + if torrentInfo.length == 0 or torrentInfo.pieceLength == 0 or + torrentInfo.pieces.len == 0: + return + RestApiResponse.error(Http422, "The torrent file is invalid or incomplete.") + # return RestApiResponse.response($torrentInfo, contentType = "text/plain") + infoBytes = bencode(torrentInfo) + else: + without infoBencoded =? extractInfoFromTorrent(torrentBytes), err: + return RestApiResponse.error( + Http422, "Failed extracting info directory from the torrent file." + ) + infoBytes = infoBencoded + without infoHash =? MultiHash.digest($Sha1HashCodec, infoBytes).mapFailure, err: + return RestApiResponse.error( + Http422, "The torrent file does not contain a valid info hash." + ) + return await node.retrieveInfoHash(infoHash, resp = resp) router.api(MethodGet, "/api/codex/v1/torrent/{infoHash}/network/stream") do( infoHash: MultiHash, resp: HttpResponseRef diff --git a/tests/codex/bittorrent/testtorrentparser.nim b/tests/codex/bittorrent/testtorrentparser.nim new file mode 100644 index 00000000..76369fd8 --- /dev/null +++ b/tests/codex/bittorrent/testtorrentparser.nim @@ -0,0 +1,48 @@ +import std/sequtils + +import pkg/unittest2 + +import pkg/libp2p/[multicodec, multihash] +import pkg/questionable/results +import pkg/stew/byteutils + +import ../examples + +import pkg/codex/bittorrent/manifest/manifest +import pkg/codex/bittorrent/torrentParser + +suite "torrentParser": + test "extracts info directory bytes from the torrent binary data": + let pieces = @[ + "21FEBA308CD51E9ACF88417193A9EA60F0F84646", + "3D4A8279853DA2DA355A574740217D446506E8EB", + "1AD686B48B9560B15B8843FD00E7EC1B59624B09", + "5015E7DA0C40350624C6B5A1FED1DB39720B726C", + ].map( + proc(hash: string): MultiHash = + let bytes = hash.hexToSeqByte.catch.tryGet() + MultiHash.init($Sha1HashCodec, bytes).mapFailure.tryGet() + ) + + let info = BitTorrentInfo( + length: 1048576, pieceLength: 262144, pieces: pieces, name: some("data1M.bin") + ) + let encodedInfo = info.bencode() + let infoHash = MultiHash.digest($Sha1HashCodec, encodedInfo).mapFailure.tryGet() + let torrentBytes = ("d4:info" & string.fromBytes(encodedInfo) & "e").toBytes() + # let torrentBytesHex = byteutils.toHex(torrentBytes) + + # check torrentBytesHex == "64343a696e666f64363a6c656e677468693130343835373665343a6e616d6531303a64617461314d2e62696e31323a7069656365206c656e6774686932363231343465363a70696563657338303a21feba308cd51e9acf88417193a9ea60f0f846463d4a8279853da2da355a574740217d446506e8eb1ad686b48b9560b15b8843fd00e7ec1b59624b095015e7da0c40350624c6b5a1fed1db39720b726c6565" + + let infoBytes = extractInfoFromTorrent(torrentBytes).tryGet() + + # echo string.fromBytes(infoBytes) + + # let infoBytesHex = byteutils.toHex(infoBytes) + + # check infoBytesHex == "64363a6c656e677468693130343835373665343a6e616d6531303a64617461314d2e62696e31323a7069656365206c656e6774686932363231343465363a70696563657338303a21feba308cd51e9acf88417193a9ea60f0f846463d4a8279853da2da355a574740217d446506e8eb1ad686b48b9560b15b8843fd00e7ec1b59624b095015e7da0c40350624c6b5a1fed1db39720b726c65" + + let extractedInfoHash = + MultiHash.digest($Sha1HashCodec, infoBytes).mapFailure.tryGet() + + check extractedInfoHash == infoHash