Skip to content
This repository has been archived by the owner on Dec 11, 2019. It is now read-only.

Support .torrent files in Torrent Viewer #7351

Merged
merged 13 commits into from
Apr 3, 2017
Merged
68 changes: 67 additions & 1 deletion app/browser/webtorrent.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
const electron = require('electron')
const ipc = electron.ipcMain
const appUrlUtil = require('../../js/lib/appUrlUtil')
const messages = require('../../js/constants/messages')
const Filtering = require('../filtering')

// Set to see communication between WebTorrent and torrent viewer tabs
const DEBUG_IPC = false
if (DEBUG_IPC) console.log('WebTorrent IPC debugging enabled')

var VIEWER_URL = appUrlUtil.getTorrentExtUrl('webtorrent.html')

function getViewerURL (torrentUrl) {
return VIEWER_URL + '#' + encodeURIComponent(torrentUrl)
}

// Connects to the BitTorrent network
// Communicates with the WebTorrentRemoteClients via message passing
let server = null
Expand All @@ -23,7 +31,7 @@ function init (state, action) {
channels[msg.clientKey] = e.sender
server.receive(msg)
})

setupFiltering()
return state
}

Expand All @@ -43,6 +51,64 @@ function send (msg) {
channel.send(messages.TORRENT_MESSAGE, msg)
}

function setupFiltering () {
Filtering.registerHeadersReceivedFilteringCB(function (details, isPrivate) {
if (details.method !== 'GET') {
return {}
}
if (!isTorrentFile(details)) {
return {}
}

var viewerUrl = getViewerURL(details.url)

return {
responseHeaders: {
'Location': [ viewerUrl ]
},
statusLine: 'HTTP/1.1 301 Moved Permanently',
resourceName: 'webtorrent'
}
})
}

/**
* Check if the request is a torrent file.
* @param {Object} details First argument of the webRequest.onHeadersReceived
* event. The properties "responseHeaders" and "url"
* are read.
* @return {boolean} True if the resource is a torrent file.
*/
function isTorrentFile (details) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This .torrent file detection is pretty conservative. It looks for a torrent mimetype, and only uses the ".torrent" extension to indicate a torrent if the server sets the 'content-type' to 'application/octet-stream'.

var header = getHeader(details.responseHeaders, 'content-type')
if (header) {
var headerValue = header.toLowerCase().split(';', 1)[0].trim()
if (headerValue === 'application/x-bittorrent') {
return true
}
if (headerValue === 'application/octet-stream') {
if (details.url.toLowerCase().indexOf('.torrent') > 0) {
return true
}
var cdHeader =
getHeader(details.responseHeaders, 'content-disposition')
if (cdHeader && /\.torrent(["']|$)/i.test(cdHeader)) {
return true
}
}
}
return false
}

function getHeader (headers, headerName) {
var headerNames = Object.keys(headers)
for (var i = 0; i < headerNames.length; ++i) {
if (headerNames[i].toLowerCase() === headerName) {
return headers[headerNames[i]][0]
}
}
}

module.exports = {
init,
resourceName: 'webtorrent'
Expand Down
4 changes: 3 additions & 1 deletion app/extensions/torrent/locales/en-US/app.properties
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
startPrompt=Start Downloading "{{name}}"?
startPromptUntitled=Start Downloading?
startDownload=Start Download
saveTorrentFile=Save Torrent File
saveTorrentFile=Save Torrent File...
legalNotice=When you start a torrent, its data will be made available to others by means of upload. You are responsible for abiding by your local laws.
missingFilesList=Click "Start Download" to load the torrent file list.
loadingFilesList=Loading the torrent file list...
Expand All @@ -18,3 +18,5 @@ downloadFile=Save File
torrentStatus=Torrent Status
torrentLoadingInfo=Loading torrent info...
torrentLoadingMedia=Loading...
copyMagnetLink=Copy Magnet Link
webtorrentPage=WebTorrent
2 changes: 1 addition & 1 deletion app/extensions/torrent/webtorrent.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@
<link rel="shortcut icon" type="image/x-icon" href="img/webtorrent-128.png">
<title data-l10n-id="webtorrentPage"></title>
<script src="ext/l20n.min.js"></script>
<script src='gen/webtorrentPage.entry.js'></script>
<link rel="localization" href="locales/{locale}/app.properties">
</head>
<body>
<div id="appContainer" />
<script src='gen/webtorrentPage.entry.js'></script>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This let's us use the DOM node #appContainer right away, without waiting for DOM ready.

</body>
</html>
6 changes: 4 additions & 2 deletions app/filtering.js
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,6 @@ function registerForBeforeSendHeaders (session, partition) {
*/
function registerForHeadersReceived (session, partition) {
const isPrivate = module.exports.isPrivate(partition)
// Note that onBeforeRedirect listener doesn't take a callback
session.webRequest.onHeadersReceived(function (details, cb) {
// Using an electron binary which isn't from Brave
if (shouldIgnoreUrl(details)) {
Expand All @@ -306,7 +305,10 @@ function registerForHeadersReceived (session, partition) {
continue
}
if (results.responseHeaders) {
cb({responseHeaders: results.responseHeaders})
cb({
responseHeaders: results.responseHeaders,
statusLine: results.statusLine
})
return
}
}
Expand Down
35 changes: 35 additions & 0 deletions js/webtorrent/components/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
const React = require('react')
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pulled this into its own component


const MediaViewer = require('./mediaViewer')
const TorrentViewer = require('./torrentViewer')

class App extends React.Component {
render () {
const {
ix,
name,
torrentId,
torrentIdProtocol,
torrent,
serverUrl,
errorMessage
} = this.props.store

if (torrent && ix != null) {
return <MediaViewer torrent={torrent} serverUrl={serverUrl} ix={ix} />
} else {
return (
<TorrentViewer
name={name}
torrentId={torrentId}
torrentIdProtocol={torrentIdProtocol}
torrent={torrent}
serverUrl={serverUrl}
errorMessage={errorMessage}
dispatch={this.props.dispatch} />
)
}
}
}

module.exports = App
12 changes: 7 additions & 5 deletions js/webtorrent/components/mediaViewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,24 +19,26 @@ const SUPPORTED_AUDIO_EXTENSIONS = [

module.exports = class MediaViewer extends React.Component {
render () {
const torrent = this.props.torrent
const ix = this.props.ix
const { torrent, serverUrl, ix } = this.props

const file = torrent.files[ix]
const fileURL = serverUrl && (serverUrl + '/' + ix)

const fileExt = file && getExtension(file.name)
const isVideo = SUPPORTED_VIDEO_EXTENSIONS.includes(fileExt)
const isAudio = SUPPORTED_AUDIO_EXTENSIONS.includes(fileExt)
const fileURL = torrent.serverURL && (torrent.serverURL + '/' + ix)

let content
if (torrent.serverURL == null) {
if (!file || !serverUrl) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix race condition where media is briefly added to <iframe>. If there is no file object yet, but there is a serverUrl, then an iframe is added to the page, instead of a "Loading..." message.

content = <div data-l10n-id='torrentLoadingMedia' />
} else if (isVideo) {
content = <video src={fileURL} autoPlay controls />
} else if (isAudio) {
content = <audio src={fileURL} autoPlay controls />
} else {
// For security, sandbox and disallow scripts.
// We need allow-same-origin so that the iframe can load from http://localhost:...
// We need allow-same-origin so that the iframe can load from
// http://localhost:...
content = <iframe src={fileURL} sandbox='allow-same-origin' />
}

Expand Down
17 changes: 10 additions & 7 deletions js/webtorrent/components/torrentFileList.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const SortableTable = require('../../components/sortableTable')

class TorrentFileList extends React.Component {
render () {
const torrent = this.props.torrent
const { torrent, stateOwner } = this.props
const files = torrent && torrent.files

let content
Expand All @@ -27,7 +27,7 @@ class TorrentFileList extends React.Component {
rowObjects={files}
columnClassNames={['num', 'name', 'downloadFile', 'size']}
addHoverClass
stateOwner={this.props.stateOwner} />
stateOwner={stateOwner} />
]
}

Expand All @@ -40,18 +40,21 @@ class TorrentFileList extends React.Component {
}

renderFileLink (file, isDownload) {
const { torrent, torrentID } = this.props
const { torrentId, torrent, serverUrl } = this.props
const ix = torrent.files.indexOf(file)
if (isDownload) {
if (torrent.serverURL) {
const httpURL = torrent.serverURL + '/' + ix
if (serverUrl) {
const httpURL = serverUrl + '/' + ix
return <a href={httpURL} download={file.name}>⇩</a>
} else {
return <div /> // No download links until the server is ready
}
} else {
const magnetURL = torrentID + '&ix=' + ix
return <a href={magnetURL}>{file.name}</a>
const suffix = /^https?:/.test(torrentId)
? '#ix=' + ix
: '&ix=' + ix
Copy link
Contributor Author

@feross feross Mar 18, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For http torrent links, we use a hash symbol instead of a query param to indicate the file that is selected, since we don't want to add random query params to the URL, which could cause the server to 404 if it doesn't like it.

For magnet links, adding a query param is acceptable since unknown keys are ignored by all torrent clients.

const href = torrentId + suffix
return <a href={href}>{file.name}</a>
}
}
}
Expand Down
43 changes: 35 additions & 8 deletions js/webtorrent/components/torrentViewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,18 @@ class TorrentViewer extends React.Component {
}

render () {
const {torrent, torrentID, name, errorMessage, dispatch} = this.props
const {
name,
torrentId,
torrent,
serverUrl,
errorMessage,
torrentIdProtocol,
dispatch
} = this.props

let titleElem, mainButtonId, saveButton

let titleElem, mainButtonId
if (torrent) {
if (name) {
// No localization, just use the torrent name
Expand All @@ -36,6 +45,24 @@ class TorrentViewer extends React.Component {
mainButtonId = 'startDownload'
}

if (torrentIdProtocol === 'magnet:') {
saveButton = (
<Button
l10nId='copyMagnetLink'
className='whiteButton copyMagnetLink'
onClick={() => dispatch('copyMagnetLink')}
/>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Magnet links get a "Copy Magnet Link" button, instead of "Save Torrent File" which made no sense.

)
} else {
saveButton = (
<Button
l10nId='saveTorrentFile'
className='whiteButton saveTorrentFile'
onClick={() => dispatch('saveTorrentFile')}
/>
)
}

const legalNotice = torrent != null
? <a className='legalNotice' data-l10n-id='poweredByWebTorrent' href='https://webtorrent.io' target='_blank' />
: <div className='legalNotice' data-l10n-id='legalNotice' />
Expand All @@ -49,20 +76,20 @@ class TorrentViewer extends React.Component {
l10nId={mainButtonId}
className='primaryButton mainButton'
disabled={!!torrent}
onClick={() => dispatch('start')} />
<Button
l10nId='saveTorrentFile'
className='whiteButton saveTorrentFile'
onClick={() => dispatch('saveTorrentFile')} />
onClick={() => dispatch('start')}
/>
{saveButton}
</div>
</div>

<div className='siteDetailsPageContent'>
<TorrentStatus torrent={torrent} errorMessage={errorMessage} />
<TorrentFileList
torrentId={torrentId}
torrent={torrent}
serverUrl={serverUrl}
stateOwner={this}
torrentID={torrentID} />
/>
{legalNotice}
</div>
</div>
Expand Down
Loading