gruhn / vue-qrcode-reader

A set of Vue.js components for detecting and decoding QR codes.
https://gruhn.github.io/vue-qrcode-reader
MIT License
2.09k stars 335 forks source link

Allow to provide URL to `zxing-wasm` #354

Closed raimund-schluessler closed 1 year ago

raimund-schluessler commented 1 year ago

Since vue-qrcode-reader@4.0.0 the library uses @sec-ant/barcode-detector. This in turn loads a @sec-ant/zxing-wasm web assembly file via a HTTP request to a third-party server. This is problematic for servers with a strict CSP policy preventing such requests.

I see that @sec-ant/barcode-detector allows to set a custom URL to load the wasm file from, via setZXingModuleOverrides, see https://github.com/Sec-ant/barcode-detector#setzxingmoduleoverrides.

Is there a way to use vue-qrcode-reader and set the custom URL for zxing?

gruhn commented 1 year ago

Good catch, thanks :+1: This is very annoying. Users shouldn't have to deal with this. There's a similar issue with providing web workers in packages. Ideally you would just bundle the wasm file with the package and somehow "include" it opaquely. But I don't know if there is an easy way to do that.

cc @dargmuesli @Sec-ant

Sec-ant commented 1 year ago

Yep I didn't bundle the wasm file inside @sec-ant/zxing-wasm or @sec-ant/barcode-detector libs because the inline base64 data url would make the packages size very huge. I cannot provide a runtime fallback option either, because that also means I have to include the base64 url in some branch of the code. So I provided a default, relatively short and common cdn location, and exposed a function to let the users decide where to load their wasm files if they want to change this behavior.

If vue-qrcode-reader wants to hide this option away from the users and to not use a third-party server, the easy way would be downloading the wasm file from the CDN (version pinning is recommended), saving it somewhere in the project considered as assets, and importing it as a base64 url with Vite's asset importing query strings.

A small demo looks like this:

import wasmFile from "../public/zxing_reader.wasm?url";
import { setZXingModuleOverrides } from "@sec-ant/barcode-detector";

setZXingModuleOverrides({
  locateFile: (path, prefix) => {
    if (path.endsWith(".wasm")) {
      return wasmFile;
    }
    return prefix + path;
  },
});

const barcodeDetector: BarcodeDetector = new BarcodeDetector({
  formats: ["qr_code"],
});

fetch(
  "https://api.qrserver.com/v1/create-qr-code/?size=150x150&data=Hello%20world!"
)
  .then((resp) => resp.blob())
  .then((imageFile) => barcodeDetector.detect(imageFile))
  .then(console.log);

However, I suggest reexporting this setZXingModuleOverrides function or encapsulating it as a simpler form to let the users have the ability to control where they want to serve the wasm file may be a better option. Additionally, you can also keep the package size small.

PS: You may notice I have updated @sec-ant/zxing-wasm and @sec-ant/barcode-detector just now. That's because when I was testing, I realized I put @types/emscripten in the dev depencies, so the argument of setZXingModuleOverrides is not properly typed. The recent version updates (@sec-ant/zxing-wasm@2.1.2 and @sec-ant/barcode-detector@1.1.2) fixed this problem.

raimund-schluessler commented 1 year ago

@gruhn @Sec-ant Thanks for taking care and the extensive explanation and example code. I think exposing the setZXingModuleOverrides function would be the best option, as it keeps the package size small and allows for the highest flexibility.

If I see it correctly, the wasm files are available as GitHub releases from https://github.com/Sec-ant/zxing-wasm-build/releases. So maybe it would even be possible to configure vite/webpack to handle the downloading automatically, somehow, for people that don't want to use the CDN, but their custom server.

Sec-ant commented 1 year ago

@raimund-schluessler Those wasm files are automatically built by GitHub actions, they are kept to sync with the upstream ZXing Cpp source code. That means not all of the builds are consumed in the downstream @sec-ant/zxing-wasm and they don't have a clear version mapping. So downloading those releases is a bad choice (If the version is mismatched, the wasm build may use a different api from what the js code expected). Actually the whole building and consuming process is somewhat amateur because I wasn't expecting other people will use this package πŸ˜‚. Maybe I should use stricter version rules, content-hash filenames and some more powerful build tools.

Back to the subject, actually, you don't have to manually download the wasm file. The package manager should've already downloaded them for you when you install any of the downstream packages, e.g. vue-qrcode-reader. You should find the file under this path: node_modules/@sec-ant/zxing-wasm/dist/reader/zxing_reader.wasm. So you can import it like this and no longer worry about wrong versions:

import wasmFile from "../node_modules/@sec-ant/zxing-wasm/dist/reader/zxing_reader.wasm?url";
gruhn commented 1 year ago

@Sec-ant Thanks a lot for the insights. I followed your suggestions and prepared a PR with updated packages and setZXingModuleOverrides re-export. Still gotta do some testing.

gruhn commented 1 year ago

released in v5.2.0: https://github.com/gruhn/vue-qrcode-reader/releases/tag/v5.2.0

raimund-schluessler commented 1 year ago

@gruhn @Sec-ant Thanks a lot for your effort. It works nicely now.

With a strict CSP enabled, one just needs to adjust it to allow wasm-unsafe-eval.

raimund-schluessler commented 1 year ago

Just a little addendum how to copy the required wasm file to the output directory with webpack:

//webpack.config.js
const CopyPlugin = require("copy-webpack-plugin")

webpackConfig.plugins.push(
    new CopyPlugin({
        patterns: [
          { from: "node_modules/@sec-ant/zxing-wasm/dist/reader/zxing_reader.wasm", to: "zxing_reader.wasm" },
        ],
    }),
)
Sec-ant commented 1 year ago

I think there's an underlying problem regarding the versions of vue-qrcode-reader, and the zxing_reader.wasm file (from the package @sec-ant/zxing-wasm):

vue-qrcode-reader doesn't pin the version of @sec-ant/barcode-detector in its deps list, which could result in users having a newer version (with minor/patch updates because of the ^ caret sign) of @sec-ant/barcode-detector and @sec-ant/zxing-wasm in their node_modules folder.

As vue-qrcode-reader is published with everything bundled, including the js glue code (the part of code that interoperates with the .wasm binary file) from @sec-ant/zxing-wasm. As a result, if a user wants to host the zxing_reader.wasm himself/herself and copies it from the node_modules folder, the user may end up hosting a mismatched newer version of the .wasm file, because the js glue code is hardcoded in the vue-qrcode-reader dist bundle and therefore is immune from any updates inside node_modules.

I don't think this is an issue of vue-qrcode-reader, because "bundle and publish” is actually quite the common way to publish a package in the js world. And package managers + semver should have already taken into account of the breaking changes for us appropriately.

But wasm is still a rather foreign object for most front end tooling, applications, and most importantly, for me. So I'm not very confident that I can always mark the breaking changes correctly in each update of @sec-ant/zxing-wasm. And I cannot be sure the .wasm and js glue code from different versions can work together even if there're only "minor" or "patch" differences between them.

So, what I want to suggest here is that:

dargmuesli commented 1 year ago

Pinning dependencies rocks

gruhn commented 1 year ago

Sounds good πŸ‘