Skip to content

Commit 1974932

Browse files
committed
replace opentracker with built in js tracker
1 parent 1b1ab2f commit 1974932

10 files changed

Lines changed: 1394 additions & 1978 deletions

File tree

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)

api/src/index.js

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,13 @@ import cors from 'cors'
77
import mongoose from 'mongoose'
88
import nodemailer from 'nodemailer'
99
import ratelimit from 'express-rate-limit'
10+
import Tracker from 'bittorrent-tracker'
1011
import * as Sentry from '@sentry/node'
1112
import * as Tracing from '@sentry/tracing'
1213
import config from '../../config'
1314
import validateConfig from './utils/validateConfig'
14-
import handleAnnounce from './middleware/announce'
15+
import createTrackerRoute from './tracker/routes'
1516
import auth from './middleware/auth'
16-
import {
17-
createUserTrackerRoutes,
18-
createOtherTrackerRoutes,
19-
} from './routes/tracker'
2017
import {
2118
register,
2219
login,
@@ -173,15 +170,15 @@ validateConfig(config).then(() => {
173170
})
174171
app.use(limiter)
175172

176-
// custom logic implementing user tracking, ratio control etc
177-
app.use('/sq/*/announce', handleAnnounce)
178-
179-
// proxy and manipulate tracker routes
180-
const userTrackerRoutes = createUserTrackerRoutes()
181-
const otherTrackerRoutes = createOtherTrackerRoutes()
182-
app.use('/sq/*/announce', userTrackerRoutes)
183-
app.use('/sq/*/scrape', userTrackerRoutes)
184-
app.use('/stats', otherTrackerRoutes)
173+
const tracker = new Tracker.Server({
174+
http: false,
175+
udp: false,
176+
ws: false,
177+
trustProxy: true,
178+
})
179+
const onTrackerRequest = tracker._onRequest.bind(tracker)
180+
app.get('/sq/*/announce', createTrackerRoute('announce', onTrackerRequest))
181+
app.get('/sq/*/scrape', createTrackerRoute('scrape', onTrackerRequest))
185182

186183
app.use(bodyParser.json({ limit: '5mb' }))
187184
app.use(cookieParser())
@@ -200,7 +197,7 @@ validateConfig(config).then(() => {
200197
app.post('/verify-email', verifyUserEmail)
201198

202199
// rss feed (auth handled in cookies)
203-
app.get('/rss', rssFeed)
200+
app.get('/rss', rssFeed(tracker))
204201

205202
// torrent file download (can download without auth, will not be able to announce)
206203
app.get('/torrent/download/:infoHash/:userId', downloadTorrent)
@@ -216,7 +213,7 @@ validateConfig(config).then(() => {
216213
app.get('/account/get-role', getUserRole)
217214
app.get('/account/get-verified', getUserVerifiedEmailStatus)
218215
app.post('/account/buy', buyItems)
219-
app.get('/user/:username', fetchUser)
216+
app.get('/user/:username', fetchUser(tracker))
220217
app.post('/user/ban/:username', banUser)
221218
app.post('/user/unban/:username', unbanUser)
222219
app.get('/account/totp/generate', generateTotpSecret)
@@ -225,15 +222,15 @@ validateConfig(config).then(() => {
225222

226223
// torrent routes
227224
app.post('/torrent/upload', uploadTorrent)
228-
app.get('/torrent/info/:infoHash', fetchTorrent)
225+
app.get('/torrent/info/:infoHash', fetchTorrent(tracker))
229226
app.delete('/torrent/delete/:infoHash', deleteTorrent)
230227
app.post('/torrent/comment/:infoHash', addCommentTorrent)
231228
app.post('/torrent/vote/:infoHash/:vote', addVote)
232229
app.post('/torrent/unvote/:infoHash/:vote', removeVote)
233230
app.post('/torrent/report/:infoHash', createReport)
234231
app.post('/torrent/toggle-freeleech/:infoHash', toggleFreeleech)
235-
app.get('/torrents/latest', listLatest)
236-
app.get('/torrents/search', searchTorrents)
232+
app.get('/torrents/latest', listLatest(tracker))
233+
app.get('/torrents/search', searchTorrents(tracker))
237234

238235
// announcement routes
239236
app.post('/announcements/new', createAnnouncement)
@@ -249,7 +246,7 @@ validateConfig(config).then(() => {
249246
app.get('/reports/page/:page', getReports)
250247
app.post('/reports/resolve/:reportId', setReportResolved)
251248
app.get('/reports/:reportId', fetchReport)
252-
app.get('/admin/stats', getStats)
249+
app.get('/admin/stats', getStats(tracker))
253250

254251
// request routes
255252
app.post('/requests/new', createRequest)

0 commit comments

Comments
 (0)