-
Notifications
You must be signed in to change notification settings - Fork 2k
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
Draft: Webdav provider poc #4621
Closed
Closed
Changes from all commits
Commits
Show all changes
55 commits
Select commit
Hold shift + click to select a range
df98cca
remove useless line
mifi 9ccf2ae
fix broken cookie removal logic
mifi 04128f9
fix mime type of thumbnails
mifi 9ceac7c
simplify/speedup token generation
mifi 600b2d0
use instanceof instead of prop check
mifi da67ac9
Implement alternative provider auth
mifi 7d59489
refactor
mifi ab06da6
use respondWithError
mifi 16d4e3f
fix prepareStream
mifi 2092387
don't throw when missing i18n key
mifi f575dbc
fix bugged try/catch
mifi 754e2e0
allow aborting login too
mifi 6441123
add json http error support
mifi f503d1f
don't tightly couple auth form with html form
mifi f0ba00f
implement webdav poc
mifi 272e3d1
fix i18n
mifi f2e5aa4
make contentType parameterized
mifi 8016a5d
merge main
mifi 9f160f4
Merge branch 'main' into provider-user-sessions
mifi a28fcec
allow sending certain errors to the user
mifi 3166db9
don't have default content-type
mifi 5c20186
make a loginSimpleAuth api too
mifi b10edf9
Merge branch 'provider-user-sessions' into webdav-provider-poc
mifi 367a0de
implement `size` for webdav
mifi 9600104
use new loginSimpleAuth method
mifi 55a9351
todo-fixme
mifi 13642fb
hopefully fix url replacer
mifi 59262c1
upgrade webdav
mifi b33f5b1
Merge branch 'main' into provider-user-sessions
mifi 47c4025
Merge branch 'provider-user-sessions' into webdav-provider-poc
mifi 4be2b6f
make removeAuthToken protected
mifi ff13823
support both nextcloud and normal webdavs
mifi 63d0b0c
trim inpiut
mifi 7549da4
make removeAuthToken protected
mifi a795d05
Merge branch 'provider-user-sessions' into webdav-provider-poc
mifi 0542371
rename webdav to webdavOauth, pass providerOptions
arturi 1302574
Fix incorrect options nesting when creating the webdav client
arturi 8794f63
Merge branch 'main' into provider-user-sessions
mifi 42f74b2
fix lint
mifi 7a84c8d
run yarn format
mifi 38eee72
Apply suggestions from code review
mifi 70a7a48
fix broken merge conflict
mifi 0d81a9b
improve inheritance
mifi 96eb565
fix bug
mifi 67d8595
fix bug with dynamic grant config
mifi 5025a73
use duck typing for error checks
mifi 761dcdc
Apply suggestions from code review
mifi 95c8e38
Merge branch 'main' into provider-user-sessions
mifi 64ce471
fix broken lint fix script
mifi 654da1a
fix broken merge code
mifi a69a1fe
try to fix flakey tets
mifi 6d766cb
fix lint
mifi 2af7b24
Merge branch 'main' into provider-user-sessions
mifi c6cafdc
Merge branch 'provider-user-sessions' into webdav-provider-poc
mifi 828c0ae
Merge branch 'main' into webdav-provider-poc
mifi 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 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 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 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 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 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 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
92 changes: 92 additions & 0 deletions
92
packages/@uppy/companion/src/server/provider/webdav/WebdavOauthProvider.js
This file contains 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,92 @@ | ||
// eslint-disable-next-line import/no-extraneous-dependencies | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this needed? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't know. I think something wrong with eslint or its import resolver (eslint gives an error if removed) |
||
const { XMLParser } = require('fast-xml-parser') | ||
|
||
const WebdavProvider = require('./common') | ||
const { getProtectedGot, validateURL } = require('../../helpers/request') | ||
|
||
const cloudTypePathMappings = { | ||
nextcloud: { | ||
manual_revoke_url: '/settings/user/security', | ||
}, | ||
owncloud: { | ||
manual_revoke_url: '/settings/personal?sectionid=security', | ||
}, | ||
} | ||
|
||
class WebdavOauth extends WebdavProvider { | ||
constructor (options) { | ||
super(options) | ||
this.authProvider = WebdavOauth.authProvider | ||
} | ||
|
||
// for "grant" | ||
static getExtraConfig () { | ||
return {} | ||
} | ||
|
||
// eslint-disable-next-line class-methods-use-this | ||
static grantDynamicToUserSession ({ grantDynamic }) { | ||
return { | ||
subdomain: grantDynamic.subdomain, | ||
} | ||
} | ||
|
||
static get authProvider () { | ||
return 'webdav' | ||
} | ||
|
||
#getBaseUrl ({ providerUserSession: { subdomain } }) { | ||
const { protocol } = this.providerOptions | ||
|
||
return `${protocol}://${subdomain}` | ||
} | ||
|
||
// eslint-disable-next-line class-methods-use-this | ||
isAuthenticated ({ providerUserSession }) { | ||
return providerUserSession.subdomain != null | ||
} | ||
|
||
async getUsername ({ token, providerUserSession }) { | ||
const { allowLocalUrls } = this | ||
|
||
const url = `${this.#getBaseUrl({ providerUserSession })}/ocs/v1.php/cloud/user` | ||
if (!validateURL(url, allowLocalUrls)) { | ||
throw new Error('invalid user url') | ||
} | ||
|
||
const response = await getProtectedGot({ url, blockLocalIPs: !allowLocalUrls }).get(url, { | ||
headers: { | ||
Authorization: `Bearer ${token}`, | ||
}, | ||
}).text() | ||
|
||
const parser = new XMLParser() | ||
const data = parser.parse(response) | ||
return data?.ocs?.data?.id | ||
} | ||
|
||
async getClient ({ username, token, providerUserSession }) { | ||
const url = `${this.#getBaseUrl({ providerUserSession })}/remote.php/dav/files/${username}` | ||
|
||
const { AuthType } = await import('webdav') // eslint-disable-line import/no-unresolved | ||
return this.getClientHelper({ | ||
url, | ||
authType: AuthType.Token, | ||
token: { | ||
access_token: token, | ||
token_type: 'Bearer', | ||
}, | ||
}) | ||
} | ||
|
||
async logout ({ providerUserSession }) { | ||
const { cloudType } = providerUserSession | ||
const manual_revoke_url = cloudTypePathMappings[cloudType]?.manual_revoke_url | ||
return { | ||
revoked: false, | ||
...(manual_revoke_url && { manual_revoke_url: `${this.#getBaseUrl({ providerUserSession })}${manual_revoke_url}` }), | ||
} | ||
} | ||
} | ||
|
||
module.exports = WebdavOauth |
79 changes: 79 additions & 0 deletions
79
packages/@uppy/companion/src/server/provider/webdav/WebdavSimpleAuthProvider.js
This file contains 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,79 @@ | ||
const { validateURL } = require('../../helpers/request') | ||
const WebdavProvider = require('./common') | ||
const { ProviderUserError } = require('../error') | ||
const logger = require('../../logger') | ||
|
||
const defaultDirectory = '/' | ||
|
||
/** | ||
* Adapter for WebDAV servers that support simple auth (non-OAuth). | ||
*/ | ||
mifi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
class WebdavSimpleAuthProvider extends WebdavProvider { | ||
static get hasSimpleAuth () { | ||
return true | ||
} | ||
|
||
async getUsername () { // eslint-disable-line class-methods-use-this | ||
return null | ||
} | ||
|
||
// 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') | ||
} | ||
|
||
const { AuthType } = await import('webdav') // eslint-disable-line import/no-unresolved | ||
|
||
// Is this a nextcloud 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.simpleAuth.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 | ||
} | ||
} | ||
} | ||
|
||
module.exports = WebdavSimpleAuthProvider |
123 changes: 123 additions & 0 deletions
123
packages/@uppy/companion/src/server/provider/webdav/common.js
This file contains 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,123 @@ | ||
const Provider = require('../Provider') | ||
const logger = require('../../logger') | ||
const { getProtectedHttpAgent, validateURL } = require('../../helpers/request') | ||
const { ProviderApiError, ProviderAuthError } = require('../error') | ||
|
||
/** | ||
* WebdavProvider base class provides implementations shared by simple and oauth providers | ||
*/ | ||
class WebdavProvider extends Provider { | ||
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, blockLocalIPs: !allowLocalUrls }) | ||
|
||
const { createClient } = await import('webdav') // eslint-disable-line import/no-unresolved | ||
return createClient(url, { | ||
...options, | ||
[`${protocol}Agent`] : new HttpAgentClass(), | ||
}) | ||
} | ||
|
||
async getClient ({ username, token, providerUserSession }) { // eslint-disable-line no-unused-vars,class-methods-use-this | ||
logger.error('call to getUsername is not implemented', 'provider.webdav.getUsername.error') | ||
throw new Error('call to getUsername is not implemented') | ||
// todo: use @returns to specify the return type | ||
return this.getClientHelper() // eslint-disable-line | ||
} | ||
|
||
async getUsername ({ token, providerUserSession }) { // eslint-disable-line no-unused-vars,class-methods-use-this | ||
logger.error('call to getUsername is not implemented', 'provider.webdav.getUsername.error') | ||
throw new Error('call to getUsername is not implemented') | ||
} | ||
|
||
/** @protected */ | ||
// eslint-disable-next-line class-methods-use-this | ||
isAuthenticated () { | ||
throw new Error('Not implemented') | ||
} | ||
|
||
async list ({ directory, token, providerUserSession }) { | ||
return this.withErrorHandling('provider.webdav.list.error', async () => { | ||
// @ts-ignore | ||
if (!this.isAuthenticated({ providerUserSession })) { | ||
throw new ProviderAuthError() | ||
} | ||
|
||
const username = await this.getUsername({ token, providerUserSession }) | ||
const data = { username, items: [] } | ||
const client = await this.getClient({ username, token, providerUserSession }) | ||
|
||
/** @type {any} */ | ||
const dir = await client.getDirectoryContents(directory || '/') | ||
|
||
dir.forEach(item => { | ||
const isFolder = item.type === 'directory' | ||
const requestPath = encodeURIComponent(`${directory || ''}/${item.basename}`) | ||
data.items.push({ | ||
isFolder, | ||
id: requestPath, | ||
name: item.basename, | ||
requestPath, // TODO FIXME | ||
modifiedDate: item.lastmod, // TODO FIXME: convert 'Tue, 04 Jul 2023 13:09:47 GMT' to ISO 8601 | ||
...(!isFolder && { | ||
mimeType: item.mime, | ||
size: item.size, | ||
thumbnail: null, | ||
|
||
}), | ||
}) | ||
}) | ||
|
||
return data | ||
}) | ||
} | ||
|
||
async download ({ id, token, providerUserSession }) { | ||
return this.withErrorHandling('provider.webdav.download.error', async () => { | ||
// maybe we can avoid this by putting the username in front of the request path/id | ||
const username = await this.getUsername({ token, providerUserSession }) | ||
const client = await this.getClient({ username, token, 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') | ||
} | ||
|
||
// todo fixme implement | ||
// eslint-disable-next-line | ||
async size ({ id, token, providerUserSession }) { | ||
return this.withErrorHandling('provider.webdav.size.error', async () => { | ||
const username = await this.getUsername({ token, providerUserSession }) | ||
const client = await this.getClient({ username, token, providerUserSession }) | ||
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 |
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure we should really set this as default/encourage it, it might be possible to craft a malicious redirect url with that and use it for phishing... haven't come up with anything clever for that so far (one of the reasons we did not enable this in production so far)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, good point, but I wonder why grant does it for Mastodon if it's possible to abuse it for malicious purposes... Do you have any example of how one could abuse it?