Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add to the hubBlob().serve() method options object with the property onlyStream to prevent downloading videos #444

Closed
serhii-chernenko opened this issue Jan 29, 2025 · 10 comments
Labels
enhancement New feature or request

Comments

@serhii-chernenko
Copy link

Is your feature request related to a problem? Please describe.
I have a simple component with an HTML5 video tag:

<ClientOnly>
  <video
    controls
    playsinline
    preload="preload"
    controlsList="noplaybackrate"
    src="/api/v1/videos/stream/custom-name.mp4"
  />
</ClientOnly>

I prepared an API endpoint to stream video blobs by name:

server/api/v1/videos/stream/[pathname].get.ts
import { z } from 'zod'

const rules = z.object({
  pathname: z.string().nonempty().max(255),
})

export default defineEventHandler(async (event) => {
  const params = await getValidatedRouterParams(
    event,
    rules.safeParse,
  )

  if (params.error) {
    throw createError({
      statusCode: 400,
      statusMessage: 'Invalid URL parameters',
      data: params.error,
    })
  }

  const { pathname } = params.data

  setHeader(event, 'Content-Security-Policy', 'default-src \'none\';')

  return hubBlob().serve(event, pathname)
})

When the video is loaded, I can download it:
Image
Image

Describe the solution you'd like
I decided to create a middleware

server/middleware/stream.ts

And check range and referrer headers in the request. If they are missed, throw an Unauthorized 403 error:

export default defineEventHandler((event) => {
  const url = getRequestURL(event).toString()

  if (url.includes('videos/stream')) {
    const referrer = getHeader(event, 'referer')
    const range = getHeader(event, 'range')

    if (!referrer || !range) {
      throw createError({
        statusCode: 403,
        statusMessage: 'Unauthorized',
      })
    }
  }
})

The issue has solved for me:
Image

I expect similar behavior for the serve function when the onlyStream option is provided:

return hubBlob().serve(event, pathname, {
  onlyStream: true,
})

Additional context
Please don't suggest adding nodownload to the controlsList of the video tag because it could be simply “hacked” by any user who knows how to open DevTools.

@serhii-chernenko serhii-chernenko added the enhancement New feature or request label Jan 29, 2025
@serhii-chernenko serhii-chernenko changed the title Add to the server method an options object with the property onlyStream to prevent downloading videos Add to the hubBlob().serve() method options object with the property onlyStream to prevent downloading videos Jan 29, 2025
@serhii-chernenko
Copy link
Author

Prepared PR: #445

@atinux
Copy link
Contributor

atinux commented Jan 29, 2025

I am not sure that this will prevent downloading the video as seen on https://stackoverflow.com/questions/9756837/prevent-html5-video-from-being-downloaded-right-click-saved

I believe a true stream option would chop up the video into chunks and serve it one after the other.

(I really appreciate the idea and energy though ❤ )

@serhii-chernenko
Copy link
Author

serhii-chernenko commented Jan 29, 2025

@atinux I believe any pre-defined “hacks” won't help if a user tries to get a direct video URL from the Network tab. So, I decided to block such requests, even if the user finds a way how to do that.
Without the referrer and range request headers, the request will be blocked. Ideally, it has to check if the referrer equals to BASE_URL but it's not set in Nuxt by default and requires more logic and documentation notes. I decided not to do that, overcomplicated.

But you are right, currently the stream option requires more work, I prepared a new issue. Perhaps they could be combined:
#446

It'd be nice to have something like HLS under the stream option.

@serhii-chernenko
Copy link
Author

@atinux just wanted to confirm, does streaming potentially possible with the current implementation? Does it require minor fixes, or it's about a massive change? I want to understand because I'm building an MVP with Nuxthub and CF infra because it covers the most required areas like storage, database, and deployment. But my product is about videos for visitors. Those videos don't have to be downloaded, only streamed. Without streaming, I can't use this solution, but I really want. So, let me know, please, if I can move forward with this.

Copy link
Contributor

atinux commented Jan 30, 2025

I believe for video streaming, Cloudflare has a service for it that may directly fit your needs (and should work with NuxtHub as well): https://www.cloudflare.com/developer-platform/products/cloudflare-stream/

@serhii-chernenko
Copy link
Author

serhii-chernenko commented Jan 30, 2025

@atinux yeah, I also found that service. But initially, I got it as a possibility to upload videos for customers as content creators, like YouTube/Instagram/TikTok analogue needs, not only for the platform admins like in video courses platforms. But it seems it's created for both cases. Sadly, there's no a free plan just to check how it works. Did you plan to have any Nuxt integration for the service?

Copy link
Contributor

atinux commented Jan 30, 2025

Once I got the time to work on it, so far I am working on moving to CF workers so you might be the first one to help us integrating this service 😄

@serhii-chernenko
Copy link
Author

@atinux I will request a trial period for development and check how to build a custom solution with their API of Cloudflare Stream service. If I get it works, I'll let you know.
I actually love everything that I already have between Nuxt <--> NuxtHub <--> Cloudflare. Don't want to change the whole infrastructure for AWS or other solutions. But it seems I can request a demo only as an enterprise, not a solo developer. If not, I will pay it for 1 month just to check what it can offer. Will see

@serhii-chernenko
Copy link
Author

hey @atinux, just an update.

I tried to request a trial period, but they ignored me, as I expected. But while I was awaiting, I checked their documentation deeply and found this one:
https://developers.cloudflare.com/stream/uploading-videos/resumable-uploads/#nodejs-example

Uploading videos which size exceeds 200 MB has to be done via the NPM tus-js-client package:
https://www.npmjs.com/package/tus-js-client

There are 2 ways I see to handle the uploaded videos and save them to a DB:

  1. Upload videos directly to CF endpoints from any Vue components. And just save a record to the DB on the onSuccess callback from TUS.
  2. Upload to a custom server handler, save it locally, save to the DB, run a Nuxt task to upload the file via TUS to the CF endpoint, remove the file locally on success. Moreover, it'd be nice to display a message for admin users that a background process is running. I'm not sure if this option is a suitable solution because it just doubles the uploading time and requires more logic.

For now, I guess these issues:

And the PR:

… could be closed as not planned because all of them are not related to the serve method anymore. It has to be an entirely new composables, handlers, types, and docs changes to handle the functionality.

@atinux
Copy link
Contributor

atinux commented Feb 4, 2025

Thank you very much for looking at it and giving me back your feedback @serhii-chernenko

Feel free to open another issue for the new functionality you imagine.

@atinux atinux closed this as not planned Won't fix, can't repro, duplicate, stale Feb 4, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants