-
Notifications
You must be signed in to change notification settings - Fork 2.1k
feat(webdav): initial implementation for a WebDAV simple auth provider #5528
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
Closed
Closed
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
105b2ed
feat(webdav): initial implementation for a WebDAV simple auth provider
dschmidt 1bbe4f8
fixup! feat(webdav): initial implementation for a WebDAV simple auth …
dschmidt 8f3cbe5
refactor(webdav): remove left overs from oauth provider
dschmidt f657f79
try to fix e2e pipelines
dschmidt a4797cc
Update .github/workflows/e2e.yml
dschmidt 1738194
incorporate @mifi's feedback
dschmidt 8e09b3e
fix last commit
dschmidt 4fa287f
remove incorrect export
dschmidt File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
182 changes: 182 additions & 0 deletions
182
packages/@uppy/companion/src/server/provider/webdav/index.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,182 @@ | ||
|
|
||
| const Provider = require('../Provider') | ||
| const { getProtectedHttpAgent, validateURL } = require('../../helpers/request') | ||
| const { ProviderApiError, ProviderAuthError } = require('../error') | ||
| const { ProviderUserError } = require('../error') | ||
| const logger = require('../../logger') | ||
|
|
||
| const defaultDirectory = '/' | ||
|
|
||
| /** | ||
| * Adapter for WebDAV servers that support simple auth (non-OAuth). | ||
| */ | ||
| class WebdavProvider extends Provider { | ||
| static get hasSimpleAuth () { | ||
| return true | ||
| } | ||
|
|
||
| // eslint-disable-next-line class-methods-use-this | ||
| isAuthenticated ({ providerUserSession }) { | ||
| return providerUserSession.webdavUrl != null | ||
| } | ||
|
|
||
| async getClient ({ providerUserSession }) { | ||
| const webdavUrl = providerUserSession?.webdavUrl | ||
| const { allowLocalUrls } = this | ||
| if (!validateURL(webdavUrl, allowLocalUrls)) { | ||
| throw new Error('invalid public link url') | ||
| } | ||
|
|
||
| // dynamic import because Companion currently uses CommonJS and webdav is shipped as ESM | ||
| // todo implement as regular require as soon as Node 20.17 or 22 is required | ||
| // or as regular import when Companion is ported to ESM | ||
| const { AuthType } = await import('webdav') // eslint-disable-line import/no-unresolved | ||
|
|
||
| // Is this an ownCloud or Nextcloud public link URL? e.g. https://example.com/s/kFy9Lek5sm928xP | ||
| // they have specific urls that we can identify | ||
| // todo not sure if this is the right way to support nextcloud and other webdavs | ||
| if (/\/s\/([^/]+)/.test(webdavUrl)) { | ||
| const [baseURL, publicLinkToken] = webdavUrl.split('/s/') | ||
|
|
||
| return this.getClientHelper({ | ||
| url: `${baseURL.replace('/index.php', '')}/public.php/webdav/`, | ||
| authType: AuthType.Password, | ||
| username: publicLinkToken, | ||
| password: 'null', | ||
| }) | ||
| } | ||
|
|
||
| // normal public WebDAV urls | ||
| return this.getClientHelper({ | ||
| url: webdavUrl, | ||
| authType: AuthType.None, | ||
| }) | ||
| } | ||
|
|
||
| async logout () { // eslint-disable-line class-methods-use-this | ||
| return { revoked: true } | ||
| } | ||
|
|
||
| async simpleAuth ({ requestBody }) { | ||
| try { | ||
| const providerUserSession = { webdavUrl: requestBody.form.webdavUrl } | ||
|
|
||
| const client = await this.getClient({ providerUserSession }) | ||
| // call the list operation as a way to validate the url | ||
| await client.getDirectoryContents(defaultDirectory) | ||
|
|
||
| return providerUserSession | ||
| } catch (err) { | ||
| logger.error(err, 'provider.webdav.error') | ||
| if (['ECONNREFUSED', 'ENOTFOUND'].includes(err.code)) { | ||
| throw new ProviderUserError({ message: 'Cannot connect to server' }) | ||
| } | ||
| // todo report back to the user what actually went wrong | ||
| throw err | ||
| } | ||
| } | ||
|
|
||
| async getClientHelper ({ url, ...options }) { | ||
| const { allowLocalUrls } = this | ||
| if (!validateURL(url, allowLocalUrls)) { | ||
| throw new Error('invalid webdav url') | ||
| } | ||
| const { protocol } = new URL(url) | ||
| const HttpAgentClass = getProtectedHttpAgent({ protocol, allowLocalIPs: !allowLocalUrls }) | ||
|
|
||
| // dynamic import because Companion currently uses CommonJS and webdav is shipped as ESM | ||
| // todo implement as regular require as soon as Node 20.17 or 22 is required | ||
| // or as regular import when Companion is ported to ESM | ||
| const { createClient } = await import('webdav') | ||
| return createClient(url, { | ||
| ...options, | ||
| [`${protocol}Agent`] : new HttpAgentClass(), | ||
| }) | ||
| } | ||
|
|
||
| async list ({ directory, providerUserSession }) { | ||
| return this.withErrorHandling('provider.webdav.list.error', async () => { | ||
| // @ts-ignore | ||
| if (!this.isAuthenticated({ providerUserSession })) { | ||
| throw new ProviderAuthError() | ||
| } | ||
|
|
||
| const data = { items: [] } | ||
| const client = await this.getClient({ providerUserSession }) | ||
|
|
||
| /** @type {any} */ | ||
| const dir = await client.getDirectoryContents(directory || '/') | ||
|
|
||
| dir.forEach(item => { | ||
| const isFolder = item.type === 'directory' | ||
| const requestPath = encodeURIComponent(`${directory || ''}/${item.basename}`) | ||
|
|
||
| let modifiedDate | ||
| try { | ||
| modifiedDate = new Date(item.lastmod).toISOString() | ||
| } catch (e) { | ||
| // ignore invalid date from server | ||
| } | ||
|
|
||
| data.items.push({ | ||
| isFolder, | ||
| id: requestPath, | ||
| name: item.basename, | ||
| modifiedDate, | ||
| requestPath, | ||
| ...(!isFolder && { | ||
| mimeType: item.mime, | ||
| size: item.size, | ||
| thumbnail: null, | ||
|
|
||
| }), | ||
| }) | ||
| }) | ||
|
|
||
| return data | ||
| }) | ||
| } | ||
|
|
||
| async download ({ id, providerUserSession }) { | ||
| return this.withErrorHandling('provider.webdav.download.error', async () => { | ||
| const client = await this.getClient({ providerUserSession }) | ||
| const stream = client.createReadStream(`/${id}`) | ||
| return { stream } | ||
| }) | ||
| } | ||
|
|
||
| // eslint-disable-next-line | ||
| async thumbnail ({ id, providerUserSession }) { | ||
| // not implementing this because a public thumbnail from webdav will be used instead | ||
| logger.error('call to thumbnail is not implemented', 'provider.webdav.thumbnail.error') | ||
| throw new Error('call to thumbnail is not implemented') | ||
| } | ||
|
|
||
| // eslint-disable-next-line | ||
| async size ({ id, token, providerUserSession }) { | ||
| return this.withErrorHandling('provider.webdav.size.error', async () => { | ||
| const client = await this.getClient({ providerUserSession }) | ||
|
|
||
| /** @type {any} */ | ||
| const stat = await client.stat(id) | ||
| return stat.size | ||
| }) | ||
| } | ||
|
|
||
| // eslint-disable-next-line class-methods-use-this | ||
| async withErrorHandling (tag, fn) { | ||
| try { | ||
| return await fn() | ||
| } catch (err) { | ||
| let err2 = err | ||
| if (err.status === 401) err2 = new ProviderAuthError() | ||
| if (err.response) { | ||
| err2 = new ProviderApiError('WebDAV API error', err.status) // todo improve (read err?.response?.body readable stream and parse response) | ||
| } | ||
| logger.error(err2, tag) | ||
| throw err2 | ||
| } | ||
| } | ||
| } | ||
|
|
||
| module.exports = WebdavProvider |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| { | ||
| "name": "@uppy/webdav", | ||
| "description": "Import files from WebDAV into Uppy.", | ||
| "version": "0.1.0", | ||
| "license": "MIT", | ||
| "main": "lib/index.js", | ||
| "types": "types/index.d.ts", | ||
| "type": "module", | ||
| "keywords": [ | ||
| "file uploader", | ||
| "uppy", | ||
| "uppy-plugin", | ||
| "webdav", | ||
| "provider", | ||
| "photos", | ||
| "videos" | ||
| ], | ||
| "homepage": "https://uppy.io", | ||
| "bugs": { | ||
| "url": "https://github.com/transloadit/uppy/issues" | ||
| }, | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "git+https://github.com/transloadit/uppy.git" | ||
| }, | ||
| "dependencies": { | ||
| "@uppy/companion-client": "workspace:^", | ||
| "@uppy/provider-views": "workspace:^", | ||
| "@uppy/utils": "workspace:^", | ||
| "preact": "^10.5.13" | ||
| }, | ||
| "peerDependencies": { | ||
| "@uppy/core": "workspace:^" | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,116 @@ | ||
| import { h } from 'preact' | ||
| import { useState, useCallback } from 'preact/hooks' | ||
|
|
||
| import { UIPlugin } from '@uppy/core' | ||
| import { Provider, tokenStorage } from '@uppy/companion-client' | ||
| import { ProviderViews } from '@uppy/provider-views' | ||
|
|
||
| import packageJson from '../package.json' | ||
| import locale from './locale.ts' | ||
|
|
||
| class WebdavSimpleAuthProvider extends Provider { | ||
| async login({ authFormData, uppyVersions, signal }) { | ||
| return this.loginSimpleAuth({ uppyVersions, authFormData, signal }) | ||
| } | ||
|
|
||
| async logout() { | ||
| this.removeAuthToken() | ||
| return { ok: true, revoked: true } | ||
| } | ||
| } | ||
|
|
||
| const AuthForm = ({ loading, i18n, onAuth }) => { | ||
| const [webdavUrl, setWebdavUrl] = useState('') | ||
|
|
||
| const onSubmit = useCallback( | ||
| (e) => { | ||
| e.preventDefault() | ||
| onAuth({ webdavUrl: webdavUrl.trim() }) | ||
| }, | ||
| [onAuth, webdavUrl], | ||
| ) | ||
|
|
||
| return ( | ||
| <form onSubmit={onSubmit}> | ||
| <label htmlFor="uppy-Provider-publicLinkURL"> | ||
| <span style={{ display: 'block' }}>{i18n('publicLinkURLLabel')}</span> | ||
| <input | ||
| id="uppy-Provider-publicLinkURL" | ||
| name="webdavUrl" | ||
| type="text" | ||
| value={webdavUrl} | ||
| onChange={(e) => setWebdavUrl(e.target.value)} | ||
| disabled={loading} | ||
| /> | ||
| </label> | ||
| <span style={{ display: 'block' }}> | ||
| {i18n('publicLinkURLDescription')} | ||
| </span> | ||
|
|
||
| <button style={{ display: 'block' }} disabled={loading} type="submit"> | ||
| Submit | ||
| </button> | ||
| </form> | ||
| ) | ||
| } | ||
|
|
||
| export default class Webdav extends UIPlugin { | ||
| static VERSION = packageJson.version | ||
|
|
||
| constructor(uppy, opts) { | ||
| super(uppy, opts) | ||
| this.id = this.opts.id || 'webdav' | ||
| this.type = 'acquirer' | ||
| this.storage = this.opts.storage || tokenStorage | ||
|
|
||
| this.defaultLocale = locale | ||
|
|
||
| this.i18nInit() | ||
|
|
||
| this.title = this.i18n('pluginNameWebdav') | ||
|
|
||
| this.provider = new WebdavSimpleAuthProvider(uppy, { | ||
| companionUrl: this.opts.companionUrl, | ||
| companionHeaders: this.opts.companionHeaders, | ||
| companionKeysParams: this.opts.companionKeysParams, | ||
| companionCookiesRule: this.opts.companionCookiesRule, | ||
| provider: 'webdav', | ||
| pluginId: this.id, | ||
| supportsRefreshToken: false, | ||
| }) | ||
|
|
||
| this.onFirstRender = this.onFirstRender.bind(this) | ||
| this.render = this.render.bind(this) | ||
| } | ||
|
|
||
| install() { | ||
| this.view = new ProviderViews(this, { | ||
| provider: this.provider, | ||
| viewType: 'list', | ||
| showTitles: true, | ||
| showFilter: true, | ||
| showBreadcrumbs: true, | ||
| renderAuthForm: ({ i18n, loading, onAuth }) => ( | ||
| <AuthForm loading={loading} onAuth={onAuth} i18n={i18n} /> | ||
| ), | ||
| }) | ||
|
|
||
| const { target } = this.opts | ||
| if (target) { | ||
| this.mount(target, this) | ||
| } | ||
| } | ||
|
|
||
| uninstall() { | ||
| this.view.tearDown() | ||
| this.unmount() | ||
| } | ||
|
|
||
| onFirstRender() { | ||
| return this.view.getFolder() | ||
| } | ||
|
|
||
| render(state) { | ||
| return this.view.render(state) | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { default } from './Webdav.tsx' |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| export default { | ||
| strings: { | ||
| pluginNameWebdav: 'WebDAV', | ||
| publicLinkURLLabel: 'URL', | ||
| publicLinkURLDescription: | ||
| 'WebDAV url or an ownCloud or Nextcloud public link', | ||
| }, | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.