mirror of
https://github.com/logos-storage/logos-storage-nim.git
synced 2026-01-14 03:18:17 +00:00
convenience feature - download content using magnet links
This commit is contained in:
parent
108370ebc9
commit
8ac8f941c4
108
codex/bittorrent/magnetlink.nim
Normal file
108
codex/bittorrent/magnetlink.nim
Normal file
@ -0,0 +1,108 @@
|
||||
import std/strutils
|
||||
import std/sequtils
|
||||
|
||||
import pkg/stew/byteutils
|
||||
import pkg/libp2p/[multicodec, multihash]
|
||||
import pkg/questionable
|
||||
import pkg/questionable/results
|
||||
|
||||
import ../errors
|
||||
import ../codextypes
|
||||
import ./manifest/manifest
|
||||
|
||||
type
|
||||
TorrentVersion* = enum
|
||||
v1
|
||||
v2
|
||||
hybrid
|
||||
|
||||
MagnetLink* = ref object
|
||||
version: TorrentVersion
|
||||
infoHashV1: ?MultiHash
|
||||
infoHashV2: ?MultiHash
|
||||
|
||||
proc version*(self: MagnetLink): TorrentVersion =
|
||||
## Get the version of the magnet link
|
||||
##
|
||||
## returns: the version of the magnet link
|
||||
result = self.version
|
||||
|
||||
proc infoHashV1*(self: MagnetLink): ?MultiHash =
|
||||
## Get the info hash of the magnet link
|
||||
##
|
||||
## returns: the info hash of the magnet link
|
||||
result = self.infoHashV1
|
||||
|
||||
proc infoHashV2*(self: MagnetLink): ?MultiHash =
|
||||
## Get the info hash of the magnet link
|
||||
##
|
||||
## returns: the info hash of the magnet link
|
||||
result = self.infoHashV2
|
||||
|
||||
proc parseMagnetLink(link: string): ?!MagnetLink =
|
||||
let prefix = "magnet:?"
|
||||
if not link.startsWith(prefix):
|
||||
return failure("Invalid magnet link format (missing 'magnet:?' prefix)")
|
||||
let infoHashParts = link[prefix.len .. ^1].split("&").filterIt(it.startsWith("xt="))
|
||||
if infoHashParts.len < 1:
|
||||
return
|
||||
failure("Invalid magnet link format (at least one info hash part is required)")
|
||||
let v1Prefix = "xt=urn:btih:"
|
||||
let v2Prefix = "xt=urn:btmh:"
|
||||
var infoHashV1 = none(MultiHash)
|
||||
var infoHashV2 = none(MultiHash)
|
||||
for infoHashPart in infoHashParts:
|
||||
# var a = infoHashPart[v1Prefix.len .. ^1]
|
||||
if infoHashPart.startsWith(v1Prefix):
|
||||
without infoHash =? BitTorrentInfo.buildMultiHash(
|
||||
infoHashPart[v1Prefix.len .. ^1]
|
||||
), err:
|
||||
return failure("Error parsing info hash: " & err.msg)
|
||||
infoHashV1 = some(infoHash)
|
||||
elif infoHashPart.startsWith(v2Prefix):
|
||||
without infoHash =? BitTorrentInfo.buildMultiHash(
|
||||
infoHashPart[v2Prefix.len .. ^1]
|
||||
), err:
|
||||
return failure("Error parsing info hash: " & err.msg)
|
||||
infoHashV2 = some(infoHash)
|
||||
|
||||
if infoHashV1.isNone and infoHashV2.isNone:
|
||||
return failure("Invalid magnet link format (missing info hash part)")
|
||||
|
||||
var version: TorrentVersion
|
||||
if infoHashV1.isSome and infoHashV2.isSome:
|
||||
version = TorrentVersion.hybrid
|
||||
elif infoHashV1.isSome:
|
||||
version = TorrentVersion.v1
|
||||
else:
|
||||
version = TorrentVersion.v2
|
||||
|
||||
let magnetLink =
|
||||
MagnetLink(version: version, infoHashV1: infoHashV1, infoHashV2: infoHashV2)
|
||||
return success(magnetLink)
|
||||
|
||||
proc getHashHex(multiHash: MultiHash): string =
|
||||
## Get the info hash of the magnet link as a hex string
|
||||
result = byteutils.toHex(multiHash.data.buffer[multiHash.dpos .. ^1]).toUpperAscii()
|
||||
|
||||
proc `$`*(self: MagnetLink): string =
|
||||
## Convert the magnet link to a string
|
||||
##
|
||||
## returns: the magnet link as a string
|
||||
if self.version == TorrentVersion.hybrid:
|
||||
result =
|
||||
"magnet:?xt=urn:btih:" & (!self.infoHashV1).getHashHex() & "&xt=urn:btmh:" &
|
||||
(!self.infoHashV2).hex
|
||||
elif self.version == v1:
|
||||
result = "magnet:?xt=urn:btih:" & (!self.infoHashV1).getHashHex()
|
||||
else:
|
||||
result = "magnet:?xt=urn:btmh:" & (!self.infoHashV2).hex
|
||||
|
||||
proc newMagnetLink*(magnetLinkString: string): ?!MagnetLink =
|
||||
## Create a new magnet link
|
||||
##
|
||||
## version: the version of the magnet link
|
||||
## magnetLinkString: text containing the magnet link
|
||||
##
|
||||
## returns: a Result containing a magnet link object or a failure
|
||||
parseMagnetLink(magnetLinkString)
|
||||
@ -42,6 +42,7 @@ import ../utils/safeasynciter
|
||||
import ../utils/options
|
||||
import ../bittorrent/manifest
|
||||
import ../bittorrent/torrentdownloader
|
||||
import ../bittorrent/magnetlink
|
||||
|
||||
import ../tarballs/[directorymanifest, directorydownloader, tarballnodeextensions]
|
||||
|
||||
@ -644,6 +645,43 @@ proc initDataApi(node: CodexNodeRef, repoStore: RepoStore, router: var RestRoute
|
||||
resp.setHeader("Access-Control-Expose-Headers", "Content-Disposition")
|
||||
await node.retrieveDirectory(cid.get(), resp = resp)
|
||||
|
||||
router.api(MethodOptions, "/api/codex/v1/torrent/magnet") 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/magnet") do(
|
||||
contentBody: Option[ContentBody], resp: HttpResponseRef
|
||||
) -> RestApiResponse:
|
||||
let mimeType = request.headers.getString(ContentTypeHeader)
|
||||
echo "mimeType: ", mimeType
|
||||
if mimeType != "text/plain" and mimeType != "application/octet-stream":
|
||||
return RestApiResponse.error(
|
||||
Http422,
|
||||
"Missing \"Content-Type\" header: expected either \"text/plain\" or \"application/octet-stream\".",
|
||||
)
|
||||
without magnetLinkBytes =? contentBody .? data:
|
||||
return RestApiResponse.error(Http422, "No magnet link provided.")
|
||||
echo "magnetLinkBytes: ", bytesToString(magnetLinkBytes).strip
|
||||
without magnetLink =? newMagnetLink(bytesToString(magnetLinkBytes).strip), err:
|
||||
return RestApiResponse.error(Http422, err.msg)
|
||||
if magnetLink.version != TorrentVersion.v1:
|
||||
return RestApiResponse.error(
|
||||
Http422, "Only torrents version 1 are currently supported!"
|
||||
)
|
||||
without infoHash =? magnetLink.infoHashV1:
|
||||
return RestApiResponse.error(
|
||||
Http422, "The magnet link does not contain a valid info hash."
|
||||
)
|
||||
await node.retrieveInfoHash(infoHash, resp = resp)
|
||||
|
||||
router.api(MethodGet, "/api/codex/v1/torrent/{infoHash}/network/stream") do(
|
||||
infoHash: MultiHash, resp: HttpResponseRef
|
||||
) -> RestApiResponse:
|
||||
|
||||
113
tests/codex/bittorrent/testmagnetlink.nim
Normal file
113
tests/codex/bittorrent/testmagnetlink.nim
Normal file
@ -0,0 +1,113 @@
|
||||
import std/strformat
|
||||
|
||||
import pkg/unittest2
|
||||
|
||||
import pkg/libp2p/[multicodec, multihash]
|
||||
import pkg/questionable/results
|
||||
import pkg/stew/byteutils
|
||||
|
||||
import ../examples
|
||||
|
||||
import pkg/codex/bittorrent/magnetlink
|
||||
|
||||
suite "bittorrent magnet links":
|
||||
test "tt":
|
||||
let magnetLinkStr = "magnet:?xt=urn:btih:1902d602db8c350f4f6d809ed01eff32f030da95"
|
||||
let magnetLink = newMagnetLink(magnetLinkStr).tryGet()
|
||||
check $magnetLink == magnetLinkStr
|
||||
test "correctly parses magnet link version 1":
|
||||
let multiHash = MultiHash.example(Sha1HashCodec)
|
||||
let hash = multiHash.data.buffer[multiHash.dpos .. ^1]
|
||||
# echo byteutils.toHex(hash)
|
||||
# echo multiHash.hex
|
||||
let magnetLinkStr =
|
||||
fmt"magnet:?xt=urn:btih:{byteutils.toHex(hash).toUpperAscii}&dn=example.txt&tr=udp://tracker.example.com/announce&x.pe=31.205.250.200:8080"
|
||||
let magnetLink = newMagnetLink(magnetLinkStr).tryGet()
|
||||
check $magnetLink == magnetLinkStr.split("&")[0]
|
||||
|
||||
test "correctly parses magnet link version 2":
|
||||
let multiHash = MultiHash.example()
|
||||
let magnetLinkStr =
|
||||
fmt"magnet:?xt=urn:btmh:{multihash.hex}&dn=example.txt&tr=udp://tracker.example.com/announce&x.pe=31.205.250.200:8080"
|
||||
let magnetLink = newMagnetLink(magnetLinkStr).tryGet()
|
||||
check $magnetLink == magnetLinkStr.split("&")[0]
|
||||
|
||||
test "correctly parses hybrid magnet links":
|
||||
let multiHashV1 = MultiHash.example(Sha1HashCodec)
|
||||
let hash = multiHashV1.data.buffer[multiHashV1.dpos .. ^1]
|
||||
let multiHash = MultiHash.example()
|
||||
let magnetLinkStr =
|
||||
fmt"magnet:?xt=urn:btih:{byteutils.toHex(hash).toUpperAscii}&xt=urn:btmh:{multihash.hex}&dn=example.txt&tr=udp://tracker.example.com/announce&x.pe=31.205.250.200:8080"
|
||||
let magnetLink = newMagnetLink(magnetLinkStr).tryGet()
|
||||
check $magnetLink == magnetLinkStr.split("&")[0 .. 1].join("&")
|
||||
|
||||
test "accepts hybrid magnet links with one info hash part incorrect (v1 part correct)":
|
||||
let multiHashV1 = MultiHash.example(Sha1HashCodec)
|
||||
let hash = multiHashV1.data.buffer[multiHashV1.dpos .. ^1]
|
||||
let magnetLinkStr =
|
||||
fmt"magnet:?xt=urn:btih:{byteutils.toHex(hash).toUpperAscii}&xt=urn:btmh&dn=example.txt&tr=udp://tracker.example.com/announce&x.pe=31.205.250.200:8080"
|
||||
let magnetLink = newMagnetLink(magnetLinkStr).tryGet()
|
||||
check $magnetLink == magnetLinkStr.split("&")[0]
|
||||
|
||||
test "accepts hybrid magnet links with one info hash part incorrect (v2 part correct)":
|
||||
let multiHash = MultiHash.example()
|
||||
let magnetLinkStr =
|
||||
fmt"magnet:?xt=urn:btih&xt=urn:btmh:{multihash.hex}&dn=example.txt&tr=udp://tracker.example.com/announce&x.pe=31.205.250.200:8080"
|
||||
let magnetLink = newMagnetLink(magnetLinkStr).tryGet()
|
||||
check $magnetLink == "magnet:?" & magnetLinkStr.split("&")[1]
|
||||
|
||||
test "fails for magnet links without 'magnet' prefix":
|
||||
let magnetLinkStr = "invalid_magnet_link"
|
||||
let magnetLink = newMagnetLink(magnetLinkStr)
|
||||
check magnetLink.isFailure
|
||||
check magnetLink.error.msg ==
|
||||
"Invalid magnet link format (missing 'magnet:?' prefix)"
|
||||
|
||||
test "fails for magnet links without 'infoHash' part":
|
||||
let magnetLinkStr =
|
||||
"magnet:?dn=example.txt&tr=udp://tracker.example.com/announce&x.pe=31.205.250.200:8080"
|
||||
let magnetLink = newMagnetLink(magnetLinkStr)
|
||||
check magnetLink.isFailure
|
||||
check magnetLink.error.msg ==
|
||||
"Invalid magnet link format (at least one info hash part is required)"
|
||||
|
||||
for (magnetLinkStr, errorMsg) in [
|
||||
(
|
||||
"magnet:?xt=urn:btih:",
|
||||
"Error parsing info hash: given bytes is not a correct multihash",
|
||||
),
|
||||
(
|
||||
"magnet:?xt=urn:btmh:",
|
||||
"Error parsing info hash: given bytes is not a correct multihash",
|
||||
),
|
||||
(
|
||||
"magnet:?xt=urn:btih:1234567890&xt=urn:btmh:",
|
||||
"Error parsing info hash: given bytes is not a correct multihash",
|
||||
),
|
||||
(
|
||||
"magnet:?xt=urn:btih:1234567890&xt=urn:btmh:1234567890",
|
||||
"Error parsing info hash: given bytes is not a correct multihash",
|
||||
),
|
||||
(
|
||||
"magnet:?xt=urn:btmh:1234567890&xt=urn:btih:",
|
||||
"Error parsing info hash: given bytes is not a correct multihash",
|
||||
),
|
||||
(
|
||||
"magnet:?xt=urn:btmh:1234567890&xt=urn:btih:1234567890",
|
||||
"Error parsing info hash: given bytes is not a correct multihash",
|
||||
),
|
||||
(
|
||||
"magnet:?xt=urn:btmh:&xt=urn:btih:1234567890",
|
||||
"Error parsing info hash: given bytes is not a correct multihash",
|
||||
),
|
||||
(
|
||||
"magnet:?xt=urn:btih:&xt=urn:btmh:1234567890",
|
||||
"Error parsing info hash: given bytes is not a correct multihash",
|
||||
),
|
||||
("magnet:?xt=urn:btih", "Invalid magnet link format (missing info hash part)"),
|
||||
("magnet:?xt=urn:btmh", "Invalid magnet link format (missing info hash part)"),
|
||||
]:
|
||||
test fmt"fails for magnet links with invalid hashes: {magnetLinkStr}":
|
||||
let magnetLink = newMagnetLink(magnetLinkStr)
|
||||
check magnetLink.isFailure
|
||||
check magnetLink.error.msg == errorMsg
|
||||
Loading…
x
Reference in New Issue
Block a user