Skip to content

Commit 0f882b9

Browse files
authored
Merge pull request tdjsnelling#1 from tdjsnelling/feature/native-tracker
implement bittorrent tracker in API service, remove opentracker
2 parents 1b1ab2f + 72378f9 commit 0f882b9

15 files changed

Lines changed: 1404 additions & 1999 deletions

README.md

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,19 +35,15 @@ It implements all of the features required to run a private (or public) tracker
3535

3636
### Components
3737

38-
An sqtracker deployment is made up of 5 separate components. These are:
39-
40-
#### A BitTorrent tracker
41-
42-
sqtracker does not implement the BitTorrent tracker spec itself. Instead, it works alongside a tracker server such as [opentracker](https://erdgeist.org/arts/software/opentracker/). In theory, other generic BitTorrent tracker software should work, but opentracker is recommended for the time being.
38+
An sqtracker deployment is made up of 4 separate components. These are:
4339

4440
#### A MongoDB database
4541

4642
[MongoDB](https://www.mongodb.com/) is a popular and powerful document-oriented database. Version 5.2 or higher is required.
4743

4844
#### The sqtracker API service
4945

50-
The sqtracker API service handles all actions taken by users (authentication, uploads, searching etc.), provides the RSS feed, and proxies announce requests to the tracker server.
46+
The sqtracker API service handles all actions taken by users (authentication, uploads, searching etc.), implements the BitTorrent tracker specification to handle announces and scrapes, and provides the RSS feed.
5147

5248
#### The sqtracker client service
5349

@@ -88,7 +84,6 @@ If your configuration is not valid, sqtracker will fail to start.
8884
| SQ_TORRENT_CATEGORIES | envs | `["Movies", "TV"]` | An array of categories available on your tracker site |
8985
| SQ_BASE_URL | envs | https://demo.sqtracker.dev | The URL of your tracker site |
9086
| SQ_API_URL | envs | https://demo.sqtracker.dev/api | The URL of your API. Under the recommended setup, it should be `${SQ_BASE_URL}/api` |
91-
| SQ_TRACKER_URL | envs | http://sq_opentracker:6969 | The URL of your tracker server. Under the recommended setup, it should be `http://sq_opentracker:6969` |
9287
| SQ_MONGO_URL | envs | mongodb://sq_mongodb/sq | The URL of your MongoDB server. Under the recommended setup, it should be `mongodb://sq_mongodb/sq` |
9388
| SQ_MAIL_FROM_ADDRESS | envs | mail@sqtracker.dev | The address that mail will be sent from |
9489
| SQ_SMTP_HOST | envs | smtp.example.com | The hostname of your SMTP server |

api/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,14 @@
1515
"@sentry/tracing": "^7.36.0",
1616
"bcrypt": "^5.0.1",
1717
"bencode": "^2.0.1",
18+
"bittorrent-tracker": "9.19.0",
1819
"body-parser": "^1.19.0",
1920
"chalk": "^4.1.1",
2021
"cookie-parser": "^1.4.6",
2122
"cors": "^2.8.5",
2223
"dotenv": "^10.0.0",
2324
"express": "^4.17.1",
2425
"express-rate-limit": "^6.7.0",
25-
"http-proxy-middleware": "^2.0.1",
2626
"jsonwebtoken": "^8.5.1",
2727
"memoizee": "^0.4.15",
2828
"mongoose": "^5.13.2",

api/src/controllers/moderation.js

Lines changed: 43 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import fetch from 'node-fetch'
21
import Report from '../schema/report'
32
import Torrent from '../schema/torrent'
43
import User from '../schema/user'
54
import Progress from '../schema/progress'
65
import Invite from '../schema/invite'
76
import Request from '../schema/request'
87
import Comment from '../schema/comment'
8+
import { all } from 'express/lib/application'
99

1010
export const createReport = async (req, res) => {
1111
if (req.body.reason) {
@@ -158,7 +158,7 @@ export const setReportResolved = async (req, res) => {
158158
}
159159
}
160160

161-
export const getStats = async (req, res) => {
161+
export const getStats = (tracker) => async (req, res) => {
162162
try {
163163
if (req.userRole !== 'admin') {
164164
res.status(401).send('You do not have permission to view tracker stats')
@@ -177,7 +177,37 @@ export const getStats = async (req, res) => {
177177
})
178178
const totalComments = await Comment.countDocuments()
179179

180-
const statsData = {
180+
const allPeers = {}
181+
let activeTorrents = 0
182+
183+
Object.keys(tracker.torrents).forEach((infoHash) => {
184+
const { peers } = tracker.torrents[infoHash]
185+
const keys = peers.keys
186+
if (keys.length > 0) activeTorrents++
187+
188+
keys.forEach((peerId) => {
189+
// Don't mark the peer as most recently used for stats
190+
const peer = peers.peek(peerId)
191+
if (peer == null) return // peers.peek() can evict the peer
192+
193+
if (!allPeers[peerId]) {
194+
allPeers[peerId] = {
195+
seeder: false,
196+
leecher: false,
197+
}
198+
}
199+
200+
if (peer.complete) {
201+
allPeers[peerId].seeder = true
202+
} else {
203+
allPeers[peerId].leecher = true
204+
}
205+
206+
allPeers[peerId].peerId = peer.peerId
207+
})
208+
})
209+
210+
res.json({
181211
registeredUsers,
182212
bannedUsers,
183213
uploadedTorrents,
@@ -187,38 +217,17 @@ export const getStats = async (req, res) => {
187217
totalRequests,
188218
filledRequests,
189219
totalComments,
190-
}
191-
192-
try {
193-
const trackerRes = await fetch(`${process.env.SQ_TRACKER_URL}/stats`)
194-
195-
if (!trackerRes.ok) {
196-
const body = await trackerRes.text()
197-
throw new Error(
198-
`Error performing tracker scrape: ${trackerRes.status} ${body}`
199-
)
200-
}
201-
202-
const body = await trackerRes.text()
203-
const [peers, seeds, activeTorrentsLine] = body.split('\n')
204-
205-
const leechers = parseInt(peers) - parseInt(seeds)
206-
207-
const activeTorrentsRegex = /opentracker serving ([0-9]+) torrents/
208-
const [, activeTorrents] = activeTorrentsLine.match(activeTorrentsRegex)
209-
210-
res.json({ ...statsData, peers, seeds, leechers, activeTorrents })
211-
} catch (e) {
212-
console.error('[DEBUG] Error: could not fetch stats from tracker')
213-
res.json({
214-
...statsData,
215-
peers: '?',
216-
seeds: '?',
217-
leechers: '?',
218-
activeTorrents: '?',
219-
})
220-
}
220+
activeTorrents,
221+
peers: Object.keys(allPeers).length,
222+
seeders: Object.values(allPeers).filter(
223+
(peer) => peer.seeder && !peer.leecher
224+
).length,
225+
leechers: Object.values(allPeers).filter(
226+
(peer) => peer.leecher && !peer.seeder
227+
).length,
228+
})
221229
} catch (e) {
230+
console.error(e)
222231
res.status(500).send(e.message)
223232
}
224233
}

api/src/controllers/rss.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ const getTorrentXml = (torrent, userId) => {
2626
</item>`
2727
}
2828

29-
export const rssFeed = async (req, res) => {
29+
export const rssFeed = (tracker) => async (req, res) => {
3030
const { username, password } = req.cookies
3131
const { query } = req.query
3232

@@ -67,6 +67,7 @@ export const rssFeed = async (req, res) => {
6767
}
6868

6969
const torrentsWithScrape = await embellishTorrentsWithTrackerScrape(
70+
tracker,
7071
torrents
7172
)
7273

api/src/controllers/torrent.js

Lines changed: 16 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,22 @@
11
import bencode from 'bencode'
22
import crypto from 'crypto'
3-
import fetch from 'node-fetch'
43
import mongoose from 'mongoose'
5-
import qs from 'qs'
64
import slugify from 'slugify'
75
import Torrent from '../schema/torrent'
86
import User from '../schema/user'
97
import Comment from '../schema/comment'
10-
import { hexToBinary } from '../middleware/announce'
8+
import { tracker } from '../index'
119

12-
export const embellishTorrentsWithTrackerScrape = async (torrents) => {
10+
export const embellishTorrentsWithTrackerScrape = async (tracker, torrents) => {
1311
if (!torrents.length) return []
1412

1513
try {
16-
const infoHashes = torrents.map((torrent) => hexToBinary(torrent.infoHash))
17-
const query = qs.stringify(
18-
{ info_hash: infoHashes },
19-
{ encoder: escape, indices: false }
20-
)
21-
22-
const trackerRes = await fetch(
23-
`${process.env.SQ_TRACKER_URL}/scrape?${query}`
24-
)
25-
26-
if (!trackerRes.ok) {
27-
const body = await trackerRes.text()
28-
throw new Error(
29-
`[DEBUG] Error performing tracker scrape: ${trackerRes.status} ${body}`
30-
)
31-
}
32-
33-
const bencoded = await trackerRes.arrayBuffer()
34-
const scrape = bencode.decode(bencoded)
35-
3614
return torrents.map((torrent) => {
37-
const scrapeForInfoHash =
38-
scrape.files[Buffer.from(hexToBinary(torrent.infoHash), 'binary')]
15+
const torrentFromTracker = tracker.torrents[torrent.infoHash]
3916
return {
4017
...torrent,
41-
seeders: scrapeForInfoHash?.complete || 0,
42-
leechers: scrapeForInfoHash?.incomplete || 0,
18+
seeders: torrentFromTracker?.complete || 0,
19+
leechers: torrentFromTracker?.incomplete || 0,
4320
}
4421
})
4522
} catch (e) {
@@ -169,7 +146,7 @@ export const downloadTorrent = async (req, res) => {
169146
res.end()
170147
}
171148

172-
export const fetchTorrent = async (req, res) => {
149+
export const fetchTorrent = (tracker) => async (req, res) => {
173150
const { infoHash } = req.params
174151

175152
try {
@@ -264,9 +241,10 @@ export const fetchTorrent = async (req, res) => {
264241

265242
if (torrent.anonymous) delete torrent.uploadedBy
266243

267-
const [embellishedTorrent] = await embellishTorrentsWithTrackerScrape([
268-
torrent,
269-
])
244+
const [embellishedTorrent] = await embellishTorrentsWithTrackerScrape(
245+
tracker,
246+
[torrent]
247+
)
270248

271249
res.json(embellishedTorrent)
272250
} catch (e) {
@@ -309,6 +287,7 @@ export const getTorrentsPage = async ({
309287
category,
310288
tag,
311289
userId,
290+
tracker,
312291
}) => {
313292
const torrents = await Torrent.aggregate([
314293
{
@@ -442,31 +421,32 @@ export const getTorrentsPage = async ({
442421
])
443422

444423
return {
445-
torrents: await embellishTorrentsWithTrackerScrape(torrents),
424+
torrents: await embellishTorrentsWithTrackerScrape(tracker, torrents),
446425
...count,
447426
}
448427
}
449428

450-
export const listLatest = async (req, res) => {
429+
export const listLatest = (tracker) => async (req, res) => {
451430
let { count } = req.query
452431
count = parseInt(count) || 25
453432
count = Math.min(count, 100)
454433
try {
455-
const { torrents } = await getTorrentsPage({ limit: count })
434+
const { torrents } = await getTorrentsPage({ limit: count, tracker })
456435
res.json(torrents)
457436
} catch (e) {
458437
res.status(500).send(e.message)
459438
}
460439
}
461440

462-
export const searchTorrents = async (req, res) => {
441+
export const searchTorrents = (tracker) => async (req, res) => {
463442
const { query, category, tag, page } = req.query
464443
try {
465444
const torrents = await getTorrentsPage({
466445
skip: page ? parseInt(page) : 0,
467446
query: decodeURIComponent(query),
468447
category,
469448
tag,
449+
tracker,
470450
})
471451
res.json(torrents)
472452
} catch (e) {

api/src/controllers/user.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import Progress from '../schema/progress'
99
import { getTorrentsPage } from './torrent'
1010
import { getUserRatio } from '../utils/ratio'
1111
import { mail } from '../index'
12-
import { BYTES_GB } from '../middleware/announce'
12+
import { BYTES_GB } from '../tracker/announce'
1313

1414
export const sendVerificationEmail = async (address, token) => {
1515
await mail.sendMail({
@@ -285,7 +285,7 @@ export const generateInvite = async (req, res) => {
285285
const createdInvite = await invite.save()
286286

287287
if (createdInvite) {
288-
mail.sendMail({
288+
await mail.sendMail({
289289
from: `"${process.env.SQ_SITE_NAME}" <${process.env.SQ_MAIL_FROM_ADDRESS}>`,
290290
to: email,
291291
subject: 'Invite',
@@ -451,7 +451,7 @@ export const finalisePasswordReset = async (req, res) => {
451451
}
452452
}
453453

454-
export const fetchUser = async (req, res) => {
454+
export const fetchUser = (tracker) => async (req, res) => {
455455
try {
456456
const { username } = req.params
457457

@@ -676,7 +676,7 @@ export const fetchUser = async (req, res) => {
676676
const { ratio } = await getUserRatio(user._id)
677677
user.ratio = ratio
678678

679-
const { torrents } = await getTorrentsPage({ userId: user._id })
679+
const { torrents } = await getTorrentsPage({ userId: user._id, tracker })
680680
user.torrents = torrents
681681

682682
res.json(user)

0 commit comments

Comments
 (0)