Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions grails-app/conf/SecurityFilters.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package streama

class SecurityFilters {
def springSecurityService

def filters = {
all(controller: '*', action: '*') {
before = {
if (!controllerName) {
return true
}

if (isPublicController(controllerName)) {
return true
}

if (!springSecurityService.isLoggedIn()) {
redirect(controller: 'login', action: 'index')
return false
}

if (isAdminController(controllerName) && !isAdminUser()) {
redirect(controller: 'login', action: 'denied')
return false
}
}
}
}

private boolean isPublicController(String controllerName) {
def publicControllers = [
'login',
'static',
'asset',
'errors'
]
return publicControllers.contains(controllerName)
}

private boolean isAdminController(String controllerName) {
def adminControllers = [
'admin',
'settings',
'userManagement'
]
return adminControllers.contains(controllerName)
}

private boolean isAdminUser() {
def principal = springSecurityService.getPrincipal()
if (!principal) {
return false
}
def authorities = principal.getAuthorities()
return authorities.any { it.getAuthority() == 'ROLE_ADMIN' }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package streama

import grails.plugin.springsecurity.annotation.Secured

import static org.springframework.http.HttpStatus.NOT_MODIFIED

@Secured('permitAll')
abstract class AbstractByteStreamaController {

def fileService
def springSecurityService

def streamFile(String filePath, String contentType, String fileName) {
def file = new File(filePath)

if (!file.exists() || !file.isFile()) {
response.status = 404
return
}

response.contentType = contentType
response.setHeader('Content-Disposition', "inline; filename=\"${fileName}\"")
response.setHeader('Content-Length', "${file.length()}")

def rangeHeader = request.getHeader('Range')

if (rangeHeader) {
handleRangeRequest(file, rangeHeader)
} else {
response.outputStream << file.bytes
}
}

def downloadFile(String filePath, String contentType, String fileName) {
def file = new File(filePath)

if (!file.exists() || !file.isFile()) {
response.status = 404
return
}

response.contentType = contentType
response.setHeader('Content-Disposition', "attachment; filename=\"${fileName}\"")
response.setHeader('Content-Length', "${file.length()}")
response.outputStream << file.bytes
}

protected void handleRangeRequest(File file, String rangeHeader) {
def totalBytes = file.length()
def rangeParts = rangeHeader.replace('bytes=', '').split('-')
def start = rangeParts[0].toLong()
def end = rangeParts.length > 1 && rangeParts[1] ? rangeParts[1].toLong() : totalBytes - 1

if (start >= totalBytes || end >= totalBytes) {
response.setHeader('Content-Range', "bytes */${totalBytes}")
response.status = 416
return
}

response.setHeader('Content-Range', "bytes ${start}-${end}/${totalBytes}")
response.setHeader('Content-Length', "${end - start + 1}")
response.status = 206

def fileInputStream = new FileInputStream(file)
fileInputStream.skip(start)

def buffer = new byte[8192]
def remaining = end - start + 1
def outputStream = response.outputStream

while (remaining > 0) {
def bytesToRead = Math.min(buffer.length, remaining as int)
def bytesRead = fileInputStream.read(buffer, 0, bytesToRead)
if (bytesRead == -1) break
outputStream.write(buffer, 0, bytesRead)
remaining -= bytesRead
}

fileInputStream.close()
outputStream.flush()
}

protected boolean validateRequest(Object videoInstance) {
if (!videoInstance) {
response.status = 404
return false
}

def user = springSecurityService.currentUser
def canAccess = fileService.canUserAccessMedia(user, videoInstance)

if (!canAccess) {
response.status = 403
return false
}

return true
}
}
2 changes: 2 additions & 0 deletions grails-app/controllers/streama/VideoController.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,8 @@ class VideoController {
return
}
viewingStatus.delete()
respond status: OK
}gStatus.delete()
render status: 200
}

Expand Down
61 changes: 61 additions & 0 deletions grails-app/controllers/streama/VideoFileController.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package streama

import grails.converters.JSON
import grails.transaction.Transactional
import org.springframework.http.HttpStatus

class VideoFileController {
def VideoFileService
def VideoController
def springSecurityService

def serve() {
def file = videoFileService.getVideoFile(params.id)
if (!file) {
render status: 404
return
}

if (file.uploadStatus == 'pending') {
response.sendError(403, 'File is still processing')
return
}

if (!file.hasViewingRights(springSecurityService.currentUser)) {
response.sendError(403, 'No viewing rights')
return
}

def absolutePath = videoFileService.getAbsolutePath(file)
def fileToServe = new File(absolutePath)

if (!fileToServe.exists()) {
render status: 404
return
}

// Prevent path traversal attacks by validating the canonical path
def canonicalPath = fileToServe.canonicalPath
def baseDir = grailsApplication.config.streama.videoFilesDirectory
def canonicalBaseDir = new File(baseDir).canonicalPath

if (!canonicalPath.startsWith(canonicalBaseDir)) {
log.warn "Potential path traversal attempt detected: ${params.id}"
response.sendError(403, 'Access denied')
return
}

response.setHeader('Accept-Ranges', 'bytes')
response.setContentType(file.contentType ?: 'application/octet-stream')
response.setHeader('Content-Disposition', "attachment;filename=${file.name}".toString())
response.setContentLength(fileToServe.size().toInteger())

if (request.getHeader('Range')) {
response.setStatus(206)
DataSourceUtils.rangeSupport(fileToServe, response.getHeader('Range'), response)
} else {
response.setStatus(200)
DataSourceUtils.streamFile(fileToServe, response)
}
}
}