mirror of
https://github.com/Chocobozzz/PeerTube.git
synced 2026-09-03 20:53:09 -05:00
Merge branch 'release/v8.1.x' into develop
This commit is contained in:
@@ -1,5 +1,21 @@
|
||||
# Changelog
|
||||
|
||||
## v8.1.6
|
||||
|
||||
### IMPORTANT NOTES
|
||||
|
||||
* Follow v8.1.0 IMPORTANT NOTES if you upgrade from PeerTube <= v8.0.2
|
||||
|
||||
### SECURITY
|
||||
|
||||
* Fix SQL injection coming from actor inbox URL when updating actor follow scores
|
||||
* Reject JSON-LD objects with special properties
|
||||
* Restricts role assignment to administrators only
|
||||
* Prevent external auth token replay
|
||||
* Prevent SSRF on import and channel sync
|
||||
* Stricter rate limit to ask password reset
|
||||
|
||||
|
||||
## v8.2.0-rc.1
|
||||
|
||||
### IMPORTANT NOTES
|
||||
|
||||
+1
-1
@@ -185,7 +185,7 @@
|
||||
"proxy-addr": "^2.0.7",
|
||||
"pug": "^3.0.4",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"sanitize-html": "~2.17.3",
|
||||
"sanitize-html": "~2.17.4",
|
||||
"sequelize": "~6.37.8",
|
||||
"sequelize-typescript": "^2.1.6",
|
||||
"sharp": "^0.34.5",
|
||||
|
||||
@@ -100,6 +100,27 @@ describe('Test ActivityPub security', function () {
|
||||
headers: HTTP_SIGNATURE.HEADERS_TO_SIGN_WITH_PAYLOAD
|
||||
})
|
||||
|
||||
async function postActivity (activity: any) {
|
||||
const signer: any = { privateKey: keys.privateKey, url: servers[2].url + '/accounts/peertube' }
|
||||
const signedBody: any = await signAndContextify({
|
||||
byActor: signer,
|
||||
data: activity,
|
||||
contextType: 'Announce',
|
||||
contextFilter: fakeFilter(),
|
||||
signerFunction: signJsonLDObjectWithoutAssertion
|
||||
})
|
||||
|
||||
const headers = buildGlobalHTTPHeaders(signedBody, buildDigest)
|
||||
|
||||
try {
|
||||
await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
|
||||
|
||||
return { fail: false }
|
||||
} catch (err) {
|
||||
return { fail: true, statusCode: err.statusCode }
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
before(async function () {
|
||||
@@ -297,23 +318,173 @@ describe('Test ActivityPub security', function () {
|
||||
}
|
||||
})
|
||||
|
||||
it('Should fail with an activity using @graph payload', async function () {
|
||||
const activity: any = {
|
||||
'id': 'https://victim.example/users/alice/statuses/123/activity',
|
||||
'type': 'Announce',
|
||||
'actor': servers[2].url + '/accounts/peertube',
|
||||
'published': '2026-03-30T07:18:30Z',
|
||||
'to': [ 'https://www.w3.org/ns/activitystreams#Public' ],
|
||||
'cc': [ 'https://victim.example/users/alice/followers' ],
|
||||
'object': 'https://target.example/users/bob/statuses/456',
|
||||
'@graph': [
|
||||
{
|
||||
id: 'https://victim.example/users/alice#announces/123/undo',
|
||||
type: 'Undo',
|
||||
actor: servers[2].url + '/accounts/peertube',
|
||||
to: [ 'https://www.w3.org/ns/activitystreams#Public' ],
|
||||
object: 'https://victim.example/users/alice/statuses/123/activity'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const { fail, statusCode } = await postActivity(activity)
|
||||
expect(fail).to.be.true
|
||||
expect(statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
|
||||
})
|
||||
|
||||
it('Should fail with an activity using @reverse payload', async function () {
|
||||
const activity: any = {
|
||||
'id': 'https://victim.example/users/alice/statuses/123/activity',
|
||||
'type': 'Announce',
|
||||
'actor': servers[2].url + '/accounts/peertube',
|
||||
'published': '2026-03-30T07:18:30Z',
|
||||
'to': [ 'https://www.w3.org/ns/activitystreams#Public' ],
|
||||
'cc': [ 'https://victim.example/users/alice/followers' ],
|
||||
'object': 'https://target.example/users/bob/statuses/456',
|
||||
'@reverse': {
|
||||
object: {
|
||||
id: 'https://victim.example/users/alice#announces/123/undo',
|
||||
type: 'Undo',
|
||||
actor: servers[2].url + '/accounts/peertube',
|
||||
to: [ 'https://www.w3.org/ns/activitystreams#Public' ]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { fail, statusCode } = await postActivity(activity)
|
||||
expect(fail).to.be.true
|
||||
expect(statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
|
||||
})
|
||||
|
||||
it('Should fail with an activity using @included payload', async function () {
|
||||
const activity: any = {
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
'id': 'https://alice.example/notes/42/activity',
|
||||
'type': 'Create',
|
||||
'actor': servers[2].url + '/accounts/peertube',
|
||||
|
||||
'cc': 'https://www.w3.org/ns/activitystreams#Public',
|
||||
'object': {
|
||||
id: 'https://alice.example/notes/42',
|
||||
type: 'Note',
|
||||
attributedTo: 'https://alice.example/actors/1',
|
||||
cc: 'https://www.w3.org/ns/activitystreams#Public',
|
||||
content: 'Welcome to Fediverse!'
|
||||
},
|
||||
'@included': {
|
||||
id: 'https://alice.example/notes/42/activity',
|
||||
to: 'https://bob.example/actors/1',
|
||||
object: {
|
||||
id: 'https://alice.example/notes/42',
|
||||
to: 'https://bob.example/actors/1',
|
||||
inReplyTo: 'https://bob.example/notes/1'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { fail, statusCode } = await postActivity(activity)
|
||||
expect(fail).to.be.true
|
||||
expect(statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
|
||||
})
|
||||
|
||||
it('Should fail with an activity using aliased @included payload', async function () {
|
||||
const activity: any = {
|
||||
'@context': [ 'https://www.w3.org/ns/activitystreams', { included: '@included' } ],
|
||||
'id': 'https://alice.example/notes/42/activity',
|
||||
'type': 'Create',
|
||||
'actor': servers[2].url + '/accounts/peertube',
|
||||
'cc': 'https://www.w3.org/ns/activitystreams#Public',
|
||||
'object': {
|
||||
id: 'https://alice.example/notes/42',
|
||||
type: 'Note',
|
||||
attributedTo: 'https://alice.example/actors/1',
|
||||
cc: 'https://www.w3.org/ns/activitystreams#Public',
|
||||
content: 'Welcome to Fediverse!'
|
||||
},
|
||||
'included': {
|
||||
id: 'https://alice.example/notes/42/activity',
|
||||
to: 'https://bob.example/actors/1',
|
||||
object: {
|
||||
id: 'https://alice.example/notes/42',
|
||||
to: 'https://bob.example/actors/1',
|
||||
inReplyTo: 'https://bob.example/notes/1'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { fail, statusCode } = await postActivity(activity)
|
||||
expect(fail).to.be.true
|
||||
expect(statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
|
||||
})
|
||||
|
||||
it('Should fail with an activity using inner @included payload', async function () {
|
||||
const activity: any = {
|
||||
'@context': [ 'https://www.w3.org/ns/activitystreams', { included: '@included' } ],
|
||||
'id': 'https://alice.example/notes/42/activity',
|
||||
'type': 'Create',
|
||||
'actor': servers[2].url + '/accounts/peertube',
|
||||
'cc': 'https://www.w3.org/ns/activitystreams#Public',
|
||||
'object': {
|
||||
id: 'https://alice.example/notes/42',
|
||||
type: 'Note',
|
||||
attributedTo: 'https://alice.example/actors/1',
|
||||
cc: 'https://www.w3.org/ns/activitystreams#Public',
|
||||
content: 'Welcome to Fediverse!',
|
||||
included: {
|
||||
id: 'https://alice.example/notes/42/activity',
|
||||
to: 'https://bob.example/actors/1',
|
||||
object: {
|
||||
id: 'https://alice.example/notes/42',
|
||||
to: 'https://bob.example/actors/1',
|
||||
inReplyTo: 'https://bob.example/notes/1'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { fail, statusCode } = await postActivity(activity)
|
||||
expect(fail).to.be.true
|
||||
expect(statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
|
||||
})
|
||||
|
||||
it('Should succeed with a valid signature', async function () {
|
||||
const body = getAnnounceWithoutContext(servers[1])
|
||||
body.actor = servers[2].url + '/accounts/peertube'
|
||||
{
|
||||
const activity = getAnnounceWithoutContext(servers[1])
|
||||
|
||||
const signer: any = { privateKey: keys.privateKey, url: servers[2].url + '/accounts/peertube' }
|
||||
const signedBody = await signAndContextify({
|
||||
byActor: signer,
|
||||
data: body,
|
||||
contextType: 'Announce',
|
||||
contextFilter: fakeFilter(),
|
||||
signerFunction: signJsonLDObjectWithoutAssertion
|
||||
})
|
||||
const { fail } = await postActivity(activity)
|
||||
expect(fail).to.be.false
|
||||
}
|
||||
|
||||
const headers = buildGlobalHTTPHeaders(signedBody, buildDigest)
|
||||
{
|
||||
const activity: any = {
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
'id': 'https://alice.example/notes/42/activity',
|
||||
'type': 'Create',
|
||||
'actor': servers[2].url + '/accounts/peertube',
|
||||
'cc': 'https://www.w3.org/ns/activitystreams#Public',
|
||||
'object': {
|
||||
id: 'https://alice.example/notes/42',
|
||||
type: 'Note',
|
||||
attributedTo: 'https://alice.example/actors/1',
|
||||
cc: 'https://www.w3.org/ns/activitystreams#Public',
|
||||
content: 'Welcome to Fediverse!'
|
||||
}
|
||||
}
|
||||
|
||||
const { statusCode } = await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
|
||||
expect(statusCode).to.equal(HttpStatusCode.NO_CONTENT_204)
|
||||
const { fail, statusCode } = await postActivity(activity)
|
||||
expect(fail).to.be.false
|
||||
}
|
||||
})
|
||||
|
||||
it('Should refresh the actor keys', async function () {
|
||||
|
||||
@@ -465,6 +465,18 @@ describe('Test users admin API validators', function () {
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail to update user role for a moderator', async function () {
|
||||
const fields = { role: UserRole.MODERATOR }
|
||||
|
||||
await makePutBodyRequest({
|
||||
url: server.url,
|
||||
path: path + userId,
|
||||
token: moderatorToken,
|
||||
fields,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed to update a user with a moderator', async function () {
|
||||
const fields = {
|
||||
videoQuota: 42
|
||||
@@ -479,12 +491,12 @@ describe('Test users admin API validators', function () {
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
it('Should succeed with the correct params for an admin', async function () {
|
||||
const fields = {
|
||||
email: 'email@example.com',
|
||||
emailVerified: true,
|
||||
videoQuota: 42,
|
||||
role: UserRole.USER
|
||||
role: UserRole.MODERATOR
|
||||
}
|
||||
|
||||
await makePutBodyRequest({
|
||||
|
||||
Generated
+20
-5
@@ -312,8 +312,8 @@ importers:
|
||||
specifier: ^0.2.2
|
||||
version: 0.2.2
|
||||
sanitize-html:
|
||||
specifier: ~2.17.3
|
||||
version: 2.17.3
|
||||
specifier: ~2.17.4
|
||||
version: 2.17.4
|
||||
sequelize:
|
||||
specifier: ~6.37.8
|
||||
version: 6.37.8(pg@8.20.0)
|
||||
@@ -4985,6 +4985,7 @@ packages:
|
||||
'@xmldom/xmldom@0.8.13':
|
||||
resolution: {integrity: sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
deprecated: this version has critical issues, please update to the latest version
|
||||
|
||||
'@yarnpkg/lockfile@1.1.0':
|
||||
resolution: {integrity: sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==}
|
||||
@@ -5403,6 +5404,7 @@ packages:
|
||||
basic-ftp@5.3.1:
|
||||
resolution: {integrity: sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
deprecated: Security vulnerability fixed in 5.2.1, please upgrade
|
||||
|
||||
bcrypt@6.0.0:
|
||||
resolution: {integrity: sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==}
|
||||
@@ -6148,6 +6150,9 @@ packages:
|
||||
dateformat@4.6.3:
|
||||
resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==}
|
||||
|
||||
dayjs@1.11.20:
|
||||
resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==}
|
||||
|
||||
debug@2.6.9:
|
||||
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
|
||||
peerDependencies:
|
||||
@@ -8168,6 +8173,9 @@ packages:
|
||||
launch-editor@2.13.2:
|
||||
resolution: {integrity: sha512-4VVDnbOpLXy/s8rdRCSXb+zfMeFR0WlJWpET1iA9CQdlZDfwyLjUuGQzXU4VeOoey6AicSAluWan7Etga6Kcmg==}
|
||||
|
||||
launder@1.7.1:
|
||||
resolution: {integrity: sha512-mU6WRz5EusL9ZZuiZ5SO4Y6C0P9PAUR9iwdb6bzj4KDihm28DiHFw+/yk9DBH4f+Pv1wuzQ4e2jV3oQ7mkIqvw==}
|
||||
|
||||
lazystream@1.0.1:
|
||||
resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==}
|
||||
engines: {node: '>= 0.6.3'}
|
||||
@@ -10163,8 +10171,8 @@ packages:
|
||||
safer-buffer@2.1.2:
|
||||
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||
|
||||
sanitize-html@2.17.3:
|
||||
resolution: {integrity: sha512-Kn4srCAo2+wZyvCNKCSyB2g8RQ8IkX/gQs2uqoSRNu5t9I2qvUyAVvRDiFUVAiX3N3PNuwStY0eNr+ooBHVWEg==}
|
||||
sanitize-html@2.17.4:
|
||||
resolution: {integrity: sha512-2HW7v2ol/uAM7sX4hbD8Z59OGWmAPrvjL8E71UWlBcj6m+kcF6ilQBLny+cIgY214QJeJT5tQuxKKqX0SQqjGQ==}
|
||||
|
||||
sass-embedded-all-unknown@1.99.0:
|
||||
resolution: {integrity: sha512-qPIRG8Uhjo6/OKyAKixTnwMliTz+t9K6Duk0mx5z+K7n0Ts38NSJz2sjDnc7cA/8V9Lb3q09H38dZ1CLwD+ssw==}
|
||||
@@ -17791,6 +17799,8 @@ snapshots:
|
||||
|
||||
dateformat@4.6.3: {}
|
||||
|
||||
dayjs@1.11.20: {}
|
||||
|
||||
debug@2.6.9:
|
||||
dependencies:
|
||||
ms: 2.0.0
|
||||
@@ -20298,6 +20308,10 @@ snapshots:
|
||||
picocolors: 1.1.1
|
||||
shell-quote: 1.8.3
|
||||
|
||||
launder@1.7.1:
|
||||
dependencies:
|
||||
dayjs: 1.11.20
|
||||
|
||||
lazystream@1.0.1:
|
||||
dependencies:
|
||||
readable-stream: 2.3.8
|
||||
@@ -22701,12 +22715,13 @@ snapshots:
|
||||
|
||||
safer-buffer@2.1.2: {}
|
||||
|
||||
sanitize-html@2.17.3:
|
||||
sanitize-html@2.17.4:
|
||||
dependencies:
|
||||
deepmerge: 4.3.1
|
||||
escape-string-regexp: 4.0.0
|
||||
htmlparser2: 10.1.0
|
||||
is-plain-object: 5.0.0
|
||||
launder: 1.7.1
|
||||
parse-srcset: 1.0.2
|
||||
postcss: 8.5.14
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { pick } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode, UserCreate, UserCreateResult, UserRight, UserUpdate } from '@peertube/peertube-models'
|
||||
import { tokensRouter } from '@server/controllers/api/users/token.js'
|
||||
import { retryTransactionWrapper } from '@server/helpers/database-utils.js'
|
||||
import { CONFIG } from '@server/initializers/config.js'
|
||||
import { getResetPasswordUrl } from '@server/lib/client-urls.js'
|
||||
import { Hooks } from '@server/lib/plugins/hooks.js'
|
||||
import { OAuthTokenModel } from '@server/models/oauth/oauth-token.js'
|
||||
@@ -19,6 +21,7 @@ import {
|
||||
asyncMiddleware,
|
||||
asyncRetryTransactionMiddleware,
|
||||
authenticate,
|
||||
buildRateLimiter,
|
||||
ensureUserHasRight,
|
||||
paginationValidator,
|
||||
setDefaultPagination,
|
||||
@@ -48,11 +51,15 @@ import { registrationsRouter } from './registrations.js'
|
||||
import { twoFactorRouter } from './two-factor.js'
|
||||
import { userExportsRouter } from './user-exports.js'
|
||||
import { userImportRouter } from './user-imports.js'
|
||||
import { retryTransactionWrapper } from '@server/helpers/database-utils.js'
|
||||
|
||||
const auditLogger = auditLoggerFactory('users')
|
||||
const lTags = loggerTagsFactory('api', 'users')
|
||||
|
||||
const askResetPasswordRateLimiter = buildRateLimiter({
|
||||
windowMs: CONFIG.RATES_LIMIT.ASK_SEND_EMAIL.WINDOW_MS,
|
||||
max: CONFIG.RATES_LIMIT.ASK_SEND_EMAIL.MAX
|
||||
})
|
||||
|
||||
const usersRouter = express.Router()
|
||||
|
||||
usersRouter.use(apiRateLimiter)
|
||||
@@ -126,9 +133,18 @@ usersRouter.delete(
|
||||
asyncMiddleware(removeUser)
|
||||
)
|
||||
|
||||
usersRouter.post('/ask-reset-password', asyncMiddleware(usersAskResetPasswordValidator), asyncMiddleware(askResetUserPassword))
|
||||
usersRouter.post(
|
||||
'/ask-reset-password',
|
||||
askResetPasswordRateLimiter,
|
||||
asyncMiddleware(usersAskResetPasswordValidator),
|
||||
asyncMiddleware(askResetUserPassword)
|
||||
)
|
||||
|
||||
usersRouter.post('/:id/reset-password', asyncMiddleware(usersResetPasswordValidator), asyncMiddleware(resetUserPassword))
|
||||
usersRouter.post(
|
||||
'/:id/reset-password',
|
||||
asyncMiddleware(usersResetPasswordValidator),
|
||||
asyncMiddleware(resetUserPassword)
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { buildUUID } from '@peertube/peertube-node-utils'
|
||||
import { logger } from '@server/helpers/logger.js'
|
||||
import { CONFIG } from '@server/initializers/config.js'
|
||||
import { OTP } from '@server/initializers/constants.js'
|
||||
import { getAuthNameFromRefreshGrant, getBypassFromExternalAuth, getBypassFromPasswordGrant } from '@server/lib/auth/external-auth.js'
|
||||
import { getAuthNameFromRefreshGrant, consumeBypassFromExternalAuth, getBypassFromPasswordGrant } from '@server/lib/auth/external-auth.js'
|
||||
import { BypassLogin, revokeToken } from '@server/lib/auth/oauth-model.js'
|
||||
import { handleOAuthToken, MissingTwoFactorError } from '@server/lib/auth/oauth.js'
|
||||
import { Hooks } from '@server/lib/plugins/hooks.js'
|
||||
@@ -196,7 +196,7 @@ async function buildByPassLogin (req: express.Request, grantType: string): Promi
|
||||
|
||||
if (req.body.externalAuthToken) {
|
||||
// Consistency with the getBypassFromPasswordGrant promise
|
||||
return getBypassFromExternalAuth(req.body.username, req.body.externalAuthToken)
|
||||
return consumeBypassFromExternalAuth(req.body.username, req.body.externalAuthToken)
|
||||
}
|
||||
|
||||
return getBypassFromPasswordGrant(req.body.username, req.body.password)
|
||||
|
||||
@@ -29,8 +29,8 @@ export async function compactJSONLDAndCheckRSA2017Signature (fromActor: MActor,
|
||||
|
||||
req.body = { ...compacted, signature: req.body.signature }
|
||||
|
||||
if (compacted['@include']) {
|
||||
logger.warn('JSON-LD @include is not supported')
|
||||
if (containInvalidJsonldKeys(compacted)) {
|
||||
logger.warn('JSON-LD @included, @graph or @reverse are not supported')
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -151,3 +151,17 @@ function createDocWithoutSignatureHash (doc: any) {
|
||||
|
||||
return hashObject(docWithoutSignature)
|
||||
}
|
||||
|
||||
function containInvalidJsonldKeys (obj: any, depth = 1) {
|
||||
if (depth > 20) return true
|
||||
|
||||
if (typeof obj !== 'object' || obj === null) return false
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.some(item => containInvalidJsonldKeys(item, depth + 1))
|
||||
}
|
||||
|
||||
if ('@included' in obj || '@graph' in obj || '@reverse' in obj) return true
|
||||
|
||||
return Object.values(obj).some(value => containInvalidJsonldKeys(value, depth + 1))
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { readdir } from 'fs/promises'
|
||||
import { dirname, join } from 'path'
|
||||
import { inspect } from 'util'
|
||||
import { isVideoFileExtnameValid } from '../custom-validators/videos.js'
|
||||
import { isResolvingToUnicastOnly } from '../dns.js'
|
||||
import { t } from '../i18n.js'
|
||||
import { logger, loggerTagsFactory } from '../logger.js'
|
||||
import { generateVideoImportTmpPath } from '../utils.js'
|
||||
@@ -103,6 +104,8 @@ export class YoutubeDLWrapper {
|
||||
}): Promise<YoutubeDLInfo> {
|
||||
const { userLanguage, youtubeDLArgs = [] } = options
|
||||
|
||||
await this.checkUnicastOrThrow(userLanguage)
|
||||
|
||||
const youtubeDL = await YoutubeDLCLI.safeGet()
|
||||
|
||||
try {
|
||||
@@ -145,6 +148,8 @@ export class YoutubeDLWrapper {
|
||||
}) {
|
||||
const { userLanguage } = options
|
||||
|
||||
await this.checkUnicastOrThrow(userLanguage)
|
||||
|
||||
const youtubeDL = await YoutubeDLCLI.safeGet()
|
||||
|
||||
const list = await youtubeDL.getListInfo({
|
||||
@@ -165,7 +170,11 @@ export class YoutubeDLWrapper {
|
||||
return list.map(info => info.webpage_url)
|
||||
}
|
||||
|
||||
async getSubtitles (): Promise<YoutubeDLSubs> {
|
||||
async getSubtitles (options: { userLanguage: string }): Promise<YoutubeDLSubs> {
|
||||
const { userLanguage } = options
|
||||
|
||||
await this.checkUnicastOrThrow(userLanguage)
|
||||
|
||||
const cwd = CONFIG.STORAGE.TMP_DIR
|
||||
|
||||
const youtubeDL = await YoutubeDLCLI.safeGet()
|
||||
@@ -192,7 +201,15 @@ export class YoutubeDLWrapper {
|
||||
return subtitles
|
||||
}
|
||||
|
||||
async downloadVideo (fileExt: string, timeout: number): Promise<string> {
|
||||
async downloadVideo (options: {
|
||||
fileExt: string
|
||||
timeout: number
|
||||
userLanguage: string
|
||||
}): Promise<string> {
|
||||
const { fileExt, timeout, userLanguage } = options
|
||||
|
||||
await this.checkUnicastOrThrow(userLanguage)
|
||||
|
||||
// Leave empty the extension, youtube-dl will add it
|
||||
const pathWithoutExtension = generateVideoImportTmpPath(this.url, '')
|
||||
|
||||
@@ -250,4 +267,13 @@ export class YoutubeDLWrapper {
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
private async checkUnicastOrThrow (userLanguage: string) {
|
||||
if (!await isResolvingToUnicastOnly(this.url)) {
|
||||
throw new YoutubeDlImportError({
|
||||
message: t(`URL {targetUrl} is not a unicast URL.`, userLanguage, { targetUrl: this.url }),
|
||||
code: YoutubeDlImportErrorCode.NOT_ONLY_UNICAST_URL
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,10 +172,13 @@ async function getBypassFromPasswordGrant (username: string, password: string):
|
||||
return undefined
|
||||
}
|
||||
|
||||
function getBypassFromExternalAuth (username: string, externalAuthToken: string): BypassLogin {
|
||||
function consumeBypassFromExternalAuth (username: string, externalAuthToken: string): BypassLogin {
|
||||
const obj = authBypassTokens.get(externalAuthToken)
|
||||
if (!obj) throw new Error('Cannot authenticate user with unknown bypass token')
|
||||
|
||||
// Prevent replaying the same token
|
||||
authBypassTokens.delete(externalAuthToken)
|
||||
|
||||
const { expires, user, authName, npmName } = obj
|
||||
|
||||
const now = new Date()
|
||||
@@ -245,7 +248,7 @@ function buildUserResult (pluginResult: RegisterServerAuthenticatedResult) {
|
||||
|
||||
export {
|
||||
onExternalUserAuthenticated,
|
||||
getBypassFromExternalAuth,
|
||||
consumeBypassFromExternalAuth,
|
||||
getAuthNameFromRefreshGrant,
|
||||
getBypassFromPasswordGrant
|
||||
}
|
||||
|
||||
@@ -91,11 +91,15 @@ export {
|
||||
async function processTorrentImport (job: Job, videoImport: MVideoImportDefault, payload: VideoImportTorrentPayload) {
|
||||
logger.info('Processing torrent video import in job %s.', job.id)
|
||||
|
||||
return processFile(
|
||||
() => downloadWebTorrentVideo({ torrentPath: payload.torrentPath, uri: videoImport.magnetUri }, JOB_TTL['video-import']),
|
||||
const user = await UserModel.loadById(videoImport.userId)
|
||||
|
||||
return processFile({
|
||||
downloader: () => downloadWebTorrentVideo({ torrentPath: payload.torrentPath, uri: videoImport.magnetUri }, JOB_TTL['video-import']),
|
||||
videoImport,
|
||||
{ type: payload.type, generateTranscription: payload.generateTranscription, videoImportId: payload.videoImportId }
|
||||
)
|
||||
type: payload.type,
|
||||
generateTranscription: payload.generateTranscription,
|
||||
user
|
||||
})
|
||||
}
|
||||
|
||||
async function processYoutubeDLImport (job: Job, videoImport: MVideoImportDefault, payload: VideoImportYoutubeDLPayload) {
|
||||
@@ -107,11 +111,17 @@ async function processYoutubeDLImport (job: Job, videoImport: MVideoImportDefaul
|
||||
CONFIG.TRANSCODING.ALWAYS_TRANSCODE_ORIGINAL_RESOLUTION
|
||||
)
|
||||
|
||||
return processFile(
|
||||
() => youtubeDL.downloadVideo(payload.fileExt, JOB_TTL['video-import']),
|
||||
const user = await UserModel.loadById(videoImport.userId)
|
||||
|
||||
return processFile({
|
||||
downloader: () => {
|
||||
return youtubeDL.downloadVideo({ fileExt: payload.fileExt, timeout: JOB_TTL['video-import'], userLanguage: user.getLanguage() })
|
||||
},
|
||||
videoImport,
|
||||
{ type: payload.type, generateTranscription: payload.generateTranscription, videoImportId: videoImport.id }
|
||||
)
|
||||
type: payload.type,
|
||||
generateTranscription: payload.generateTranscription,
|
||||
user
|
||||
})
|
||||
}
|
||||
|
||||
async function getVideoImportOrDie (payload: VideoImportPayload) {
|
||||
@@ -131,12 +141,15 @@ async function getVideoImportOrDie (payload: VideoImportPayload) {
|
||||
return videoImport
|
||||
}
|
||||
|
||||
type ProcessFileOptions = {
|
||||
async function processFile (options: {
|
||||
downloader: () => Promise<string>
|
||||
videoImport: MVideoImportDefault
|
||||
user: MUserId
|
||||
type: VideoImportYoutubeDLPayloadType | VideoImportTorrentPayloadType
|
||||
generateTranscription: boolean
|
||||
videoImportId: number
|
||||
}
|
||||
async function processFile (downloader: () => Promise<string>, videoImport: MVideoImportDefault, options: ProcessFileOptions) {
|
||||
}) {
|
||||
const { downloader, videoImport, user, type, generateTranscription } = options
|
||||
|
||||
let tmpVideoPath: string
|
||||
let videoFile: MVideoFile
|
||||
|
||||
@@ -146,7 +159,6 @@ async function processFile (downloader: () => Promise<string>, videoImport: MVid
|
||||
|
||||
// Get information about this video
|
||||
const stats = await stat(tmpVideoPath)
|
||||
const user = await UserModel.loadByVideoId(videoImport.videoId)
|
||||
if (!user) throw new Error('Video does not exist anymore')
|
||||
|
||||
const isAble = await isUserQuotaValid({ channelUserId: user.id, uploadSize: stats.size })
|
||||
@@ -166,7 +178,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: MVid
|
||||
videoFile = await buildNewFile({ mode: 'web-video', ffprobe, path: tmpVideoPath })
|
||||
videoFile.videoId = videoImport.videoId
|
||||
|
||||
const hookName = options.type === 'youtube-dl'
|
||||
const hookName = type === 'youtube-dl'
|
||||
? 'filter:api.video.post-import-url.accept.result'
|
||||
: 'filter:api.video.post-import-torrent.accept.result'
|
||||
|
||||
@@ -248,7 +260,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: MVid
|
||||
video,
|
||||
videoFile,
|
||||
user: videoImport.User,
|
||||
generateTranscription: options.generateTranscription
|
||||
generateTranscription
|
||||
})
|
||||
} finally {
|
||||
videoFileLockReleaser()
|
||||
|
||||
@@ -263,7 +263,7 @@ export async function buildYoutubeDLImport (options: {
|
||||
})
|
||||
|
||||
// Get video subtitles
|
||||
await processYoutubeSubtitles(youtubeDL, targetUrl, video)
|
||||
await processYoutubeSubtitles({ youtubeDL, targetUrl, video, userLanguage: user.getLanguage() })
|
||||
|
||||
let fileExt = `.${youtubeDLInfo.ext}`
|
||||
if (!isVideoFileExtnameValid(fileExt)) fileExt = '.mp4'
|
||||
@@ -316,9 +316,16 @@ async function processThumbnails (options: {
|
||||
return []
|
||||
}
|
||||
|
||||
async function processYoutubeSubtitles (youtubeDL: YoutubeDLWrapper, targetUrl: string, video: MVideo) {
|
||||
async function processYoutubeSubtitles (options: {
|
||||
youtubeDL: YoutubeDLWrapper
|
||||
targetUrl: string
|
||||
video: MVideo
|
||||
userLanguage: string
|
||||
}) {
|
||||
const { youtubeDL, targetUrl, video, userLanguage } = options
|
||||
|
||||
try {
|
||||
const subtitles = await youtubeDL.getSubtitles()
|
||||
const subtitles = await youtubeDL.getSubtitles({ userLanguage })
|
||||
|
||||
logger.info('Found %s subtitles candidates from youtube-dl import %s.', subtitles.length, targetUrl)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { forceNumber } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode, ServerErrorCode, UserRole, UserUpdateMe } from '@peertube/peertube-models'
|
||||
import { HttpStatusCode, ServerErrorCode, UserRole, UserRoleType, UserUpdateMe } from '@peertube/peertube-models'
|
||||
import { isStringArray } from '@server/helpers/custom-validators/search.js'
|
||||
import { isNSFWFlagsValid } from '@server/helpers/custom-validators/videos.js'
|
||||
import { loadReservedActorName } from '@server/lib/local-actor.js'
|
||||
@@ -96,13 +96,7 @@ export const usersAddValidator = [
|
||||
if (areValidationErrors(req, res, { omitBodyLog: true })) return
|
||||
if (!await checkUsernameOrEmailDoNotAlreadyExist({ username: req.body.username, email: req.body.email, req, res })) return
|
||||
|
||||
const authUser = res.locals.oauth.token.User
|
||||
if (authUser.role !== UserRole.ADMINISTRATOR && req.body.role !== UserRole.USER) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'You can only create users (and not administrators or moderators)'
|
||||
})
|
||||
}
|
||||
if (!checkCanSetRole(req.body.role, res)) return
|
||||
|
||||
if (req.body.channelName) {
|
||||
if (req.body.channelName === req.body.username) {
|
||||
@@ -213,6 +207,7 @@ export const usersUpdateValidator = [
|
||||
}
|
||||
|
||||
if (!checkCanModerate(user, res)) return
|
||||
if (req.body.role && !checkCanSetRole(req.body.role, res)) return
|
||||
|
||||
if (
|
||||
req.body.email &&
|
||||
@@ -524,3 +519,18 @@ function checkCanModerate (onUser: MUser, res: express.Response) {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
function checkCanSetRole (role: UserRoleType, res: express.Response) {
|
||||
const authUser = res.locals.oauth.token.User
|
||||
|
||||
if (authUser.role !== UserRole.ADMINISTRATOR && role !== UserRole.USER) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Only administrators can assign admin or moderator roles'
|
||||
})
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -658,14 +658,19 @@ export class ActorFollowModel extends SequelizeModel<ActorFollowModel> {
|
||||
}
|
||||
|
||||
static updateScore (inboxUrl: string, value: number, t?: Transaction) {
|
||||
const query = `UPDATE "actorFollow" SET "score" = LEAST("score" + ${value}, ${ACTOR_FOLLOW_SCORE.MAX}) ` +
|
||||
const query = 'UPDATE "actorFollow" SET "score" = LEAST("score" + $value, $maxScore) ' +
|
||||
'WHERE id IN (' +
|
||||
'SELECT "actorFollow"."id" FROM "actorFollow" ' +
|
||||
'INNER JOIN "actor" ON "actor"."id" = "actorFollow"."actorId" ' +
|
||||
`WHERE "actor"."inboxUrl" = '${inboxUrl}' OR "actor"."sharedInboxUrl" = '${inboxUrl}'` +
|
||||
'WHERE "actor"."inboxUrl" = $inboxUrl OR "actor"."sharedInboxUrl" = $inboxUrl' +
|
||||
')'
|
||||
|
||||
const options = {
|
||||
bind: {
|
||||
inboxUrl,
|
||||
maxScore: ACTOR_FOLLOW_SCORE.MAX,
|
||||
value
|
||||
},
|
||||
type: QueryTypes.BULKUPDATE,
|
||||
transaction: t
|
||||
}
|
||||
|
||||
@@ -388,19 +388,24 @@ export class UserNotificationModel extends SequelizeModel<UserNotificationModel>
|
||||
forUserId?: number
|
||||
}) {
|
||||
const id = forceNumber(options.id)
|
||||
const bind: { id: number, forUserId?: number } = { id }
|
||||
|
||||
if (options.forUserId !== undefined) {
|
||||
bind.forUserId = options.forUserId
|
||||
}
|
||||
|
||||
function buildAccountWhereQuery (base: string) {
|
||||
const whereSuffix = options.forUserId
|
||||
? ` AND "userNotification"."userId" = ${options.forUserId}`
|
||||
const whereSuffix = options.forUserId !== undefined
|
||||
? ' AND "userNotification"."userId" = $forUserId'
|
||||
: ''
|
||||
|
||||
if (options.type === 'account') {
|
||||
return base +
|
||||
` WHERE "account"."id" = ${id} ${whereSuffix}`
|
||||
` WHERE "account"."id" = $id ${whereSuffix}`
|
||||
}
|
||||
|
||||
return base +
|
||||
` WHERE "actor"."serverId" = ${id} ${whereSuffix}`
|
||||
` WHERE "actor"."serverId" = $id ${whereSuffix}`
|
||||
}
|
||||
|
||||
const queries = [
|
||||
@@ -455,7 +460,7 @@ export class UserNotificationModel extends SequelizeModel<UserNotificationModel>
|
||||
|
||||
const query = `DELETE FROM "userNotification" WHERE id IN (${queries.join(' UNION ')})`
|
||||
|
||||
return UserNotificationModel.sequelize.query(query)
|
||||
return UserNotificationModel.sequelize.query(query, { bind })
|
||||
}
|
||||
|
||||
toFormattedJSON (this: UserNotificationModelForApi): UserNotification {
|
||||
|
||||
Reference in New Issue
Block a user