Add shared code support to protect shared link

This commit is contained in:
Allan 2021-01-30 16:50:08 +08:00 committed by Allan
parent f3afd5cb79
commit 4a6fdb34ff
19 changed files with 506 additions and 286 deletions

View File

@ -1,15 +0,0 @@
FROM alpine:latest as alpine
RUN apk --update add ca-certificates
RUN apk --update add mailcap
FROM scratch
COPY --from=alpine /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
COPY --from=alpine /etc/mime.types /etc/mime.types
VOLUME /srv
EXPOSE 80
COPY .docker.json /.filebrowser.json
COPY filebrowser /filebrowser
ENTRYPOINT [ "/filebrowser" ]

1
Dockerfile Symbolic link
View File

@ -0,0 +1 @@
Dockerfile.debian

View File

@ -1,4 +1,5 @@
FROM alpine:latest as alpine
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
RUN apk --update add ca-certificates
RUN apk --update add mailcap

View File

@ -47,6 +47,7 @@ func init() {
flags.Bool("noauth", false, "use the noauth auther when using quick setup")
flags.String("username", "admin", "username for the first user when using quick config")
flags.String("password", "", "hashed password for the first user when using quick config (default \"admin\")")
flags.String("salt", "", "The salt to use when for hashing share password. Can be any value. If changed, existing password-protected shares wil stop working.") //nolint:lll
addServerFlags(flags)
}
@ -250,6 +251,10 @@ func getRunParams(flags *pflag.FlagSet, st *storage.Storage) *settings.Server {
_, disableExec := getParamB(flags, "disable-exec")
server.EnableExec = !disableExec
if val, set := getParamB(flags, "salt"); set {
server.Salt = val
}
return server
}

View File

@ -26,28 +26,30 @@ import (
// FileInfo describes a file.
type FileInfo struct {
*Listing
Fs afero.Fs `json:"-"`
Path string `json:"path"`
Name string `json:"name"`
Size int64 `json:"size"`
Extension string `json:"extension"`
ModTime time.Time `json:"modified"`
Mode os.FileMode `json:"mode"`
IsDir bool `json:"isDir"`
Type string `json:"type"`
Subtitles []string `json:"subtitles,omitempty"`
Content string `json:"content,omitempty"`
Checksums map[string]string `json:"checksums,omitempty"`
Fs afero.Fs `json:"-"`
Path string `json:"path"`
Name string `json:"name"`
Size int64 `json:"size"`
Extension string `json:"extension"`
ModTime time.Time `json:"modified"`
Mode os.FileMode `json:"mode"`
IsDir bool `json:"isDir"`
Type string `json:"type"`
Subtitles []string `json:"subtitles,omitempty"`
Content string `json:"content,omitempty"`
Checksums map[string]string `json:"checksums,omitempty"`
SharedCodeToken string `json:"sharedCodeToken,omitempty"`
}
// FileOptions are the options when getting a file info.
type FileOptions struct {
Fs afero.Fs
Path string
Modify bool
Expand bool
ReadHeader bool
Checker rules.Checker
Fs afero.Fs
Path string
Modify bool
Expand bool
ReadHeader bool
SharedCodeToken string
Checker rules.Checker
}
// NewFileInfo creates a File object from a path and a given user. This File
@ -64,14 +66,15 @@ func NewFileInfo(opts FileOptions) (*FileInfo, error) {
}
file := &FileInfo{
Fs: opts.Fs,
Path: opts.Path,
Name: info.Name(),
ModTime: info.ModTime(),
Mode: info.Mode(),
IsDir: info.IsDir(),
Size: info.Size(),
Extension: filepath.Ext(info.Name()),
Fs: opts.Fs,
Path: opts.Path,
Name: info.Name(),
ModTime: info.ModTime(),
Mode: info.Mode(),
IsDir: info.IsDir(),
Size: info.Size(),
Extension: filepath.Ext(info.Name()),
SharedCodeToken: opts.SharedCodeToken,
}
if opts.Expand {

View File

@ -4,8 +4,10 @@ export async function list() {
return fetchJSON('/api/shares')
}
export async function getHash(hash) {
return fetchJSON(`/api/public/share/${hash}`)
export async function getHash(hash, shared_code = "") {
return fetchJSON(`/api/public/share/${hash}`, {
headers: { 'X-SHARED-CODE': shared_code },
})
}
export async function get(url) {
@ -23,14 +25,24 @@ export async function remove(hash) {
}
}
export async function create(url, expires = '', unit = 'hours') {
export async function create(url, shared_code = '', expires = '', unit = 'hours') {
url = removePrefix(url)
url = `/api/share${url}`
if (expires !== '') {
url += `?expires=${expires}&unit=${unit}`
if (shared_code !== '' || expires !== '') {
url += '?'
var params = ''
if (expires !== '') {
params += `expires=${expires}&unit=${unit}`
}
if (shared_code !== '') {
if (params != '') {
params += "&"
}
params += `shared_code=${shared_code}`
}
url += params
}
return fetchJSON(url, {
method: 'POST'
method: 'POST',
})
}

View File

@ -1,167 +1,220 @@
<template>
<div class="card floating" id="share">
<div
class="card floating"
style="max-width: max-content; width: auto"
id="share"
>
<div class="card-title">
<h2>{{ $t('buttons.share') }}</h2>
<h2>{{ $t("buttons.share") }}</h2>
</div>
<div class="card-content">
<ul>
<li v-if="!hasPermanent">
<a @click="getPermalink" :aria-label="$t('buttons.permalink')">{{ $t('buttons.permalink') }}</a>
</li>
<li v-for="link in links" :key="link.hash">
<a :href="buildLink(link.hash)" target="_blank">
<template v-if="link.expire !== 0">{{ humanTime(link.expire) }}</template>
<template v-else>{{ $t('permanent') }}</template>
<template v-if="link.expire !== 0">{{
humanTime(link.expire)
}}</template>
<template v-else>{{ $t("permanent") }}</template>
</a>
<button class="action"
<button
class="action"
@click="deleteLink($event, link)"
:aria-label="$t('buttons.delete')"
:title="$t('buttons.delete')"><i class="material-icons">delete</i></button>
:title="$t('buttons.delete')"
>
<i class="material-icons">delete</i>
</button>
<button class="action copy-clipboard"
:data-clipboard-text="buildLink(link.hash)"
<button
class="action copy-clipboard"
:data-clipboard-text="
$t(downloadPromptsWithSharedCode(link.shared_code), [
buildLink(link.hash),
link.shared_code,
])
"
:aria-label="$t('buttons.copyToClipboard')"
:title="$t('buttons.copyToClipboard')"><i class="material-icons">content_paste</i></button>
:title="$t('buttons.copyToClipboard')"
>
<i class="material-icons">content_paste</i>
</button>
</li>
<li v-if="!hasPermanent">
<div style="text-align: right">
<input
type="text"
:placeholder="$t('buttons.optionalSharedCode')"
v-model="shared_code_permalink"
/>
<a @click="getPermalink" :aria-label="$t('buttons.permalink')">{{
$t("buttons.permalink")
}}</a>
</div>
</li>
<li>
<input v-focus
<input
v-focus
type="number"
max="2147483647"
min="0"
@keyup.enter="submit"
v-model.trim="time">
v-model.trim="time"
/>
<select v-model="unit" :aria-label="$t('time.unit')">
<option value="seconds">{{ $t('time.seconds') }}</option>
<option value="minutes">{{ $t('time.minutes') }}</option>
<option value="hours">{{ $t('time.hours') }}</option>
<option value="days">{{ $t('time.days') }}</option>
<option value="seconds">{{ $t("time.seconds") }}</option>
<option value="minutes">{{ $t("time.minutes") }}</option>
<option value="hours">{{ $t("time.hours") }}</option>
<option value="days">{{ $t("time.days") }}</option>
</select>
<button class="action"
<input
type="text"
:placeholder="$t('buttons.optionalSharedCode')"
v-model="shared_code"
/>
<button
class="action"
@click="submit"
:aria-label="$t('buttons.create')"
:title="$t('buttons.create')"><i class="material-icons">add</i></button>
:title="$t('buttons.create')"
>
<i class="material-icons">add</i>
</button>
</li>
</ul>
</div>
<div class="card-action">
<button class="button button--flat"
<button
class="button button--flat"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.close')"
:title="$t('buttons.close')">{{ $t('buttons.close') }}</button>
:title="$t('buttons.close')"
>
{{ $t("buttons.close") }}
</button>
</div>
</div>
</template>
<script>
import { mapState, mapGetters } from 'vuex'
import { share as api } from '@/api'
import { baseURL } from '@/utils/constants'
import moment from 'moment'
import Clipboard from 'clipboard'
import { mapState, mapGetters } from "vuex";
import { share as api } from "@/api";
import { baseURL } from "@/utils/constants";
import moment from "moment";
import Clipboard from "clipboard";
export default {
name: 'share',
name: "share",
data: function () {
return {
time: '',
unit: 'hours',
time: "",
unit: "hours",
hasPermanent: false,
links: [],
clip: null
}
clip: null,
};
},
computed: {
...mapState([ 'req', 'selected', 'selectedCount' ]),
...mapGetters([ 'isListing' ]),
url () {
...mapState(["req", "selected", "selectedCount"]),
...mapGetters(["isListing"]),
url() {
if (!this.isListing) {
return this.$route.path
return this.$route.path;
}
if (this.selectedCount === 0 || this.selectedCount > 1) {
// This shouldn't happen.
return
return;
}
return this.req.items[this.selected[0]].url
}
return this.req.items[this.selected[0]].url;
},
},
async beforeMount () {
async beforeMount() {
try {
const links = await api.get(this.url)
this.links = links
this.sort()
const links = await api.get(this.url);
this.links = links;
this.sort();
for (let link of this.links) {
if (link.expire === 0) {
this.hasPermanent = true
break
this.hasPermanent = true;
break;
}
}
} catch (e) {
this.$showError(e)
this.$showError(e);
}
},
mounted () {
this.clip = new Clipboard('.copy-clipboard')
this.clip.on('success', () => {
this.$showSuccess(this.$t('success.linkCopied'))
})
mounted() {
this.clip = new Clipboard(".copy-clipboard");
this.clip.on("success", () => {
this.$showSuccess(this.$t("success.linkCopied"));
});
},
beforeDestroy () {
this.clip.destroy()
beforeDestroy() {
this.clip.destroy();
},
methods: {
submit: async function () {
if (!this.time) return
if (!this.time) return;
try {
const res = await api.create(this.url, this.time, this.unit)
this.links.push(res)
this.sort()
const res = await api.create(
this.url,
this.shared_code,
this.time,
this.unit
);
this.links.push(res);
this.sort();
} catch (e) {
this.$showError(e)
this.$showError(e);
}
},
getPermalink: async function () {
try {
const res = await api.create(this.url)
this.links.push(res)
this.sort()
this.hasPermanent = true
const res = await api.create(this.url, this.shared_code_permalink);
this.links.push(res);
this.sort();
this.hasPermanent = true;
} catch (e) {
this.$showError(e)
this.$showError(e);
}
},
deleteLink: async function (event, link) {
event.preventDefault()
try {
await api.remove(link.hash)
if (link.expire === 0) this.hasPermanent = false
this.links = this.links.filter(item => item.hash !== link.hash)
event.preventDefault();
try {
await api.remove(link.hash);
if (link.expire === 0) this.hasPermanent = false;
this.links = this.links.filter((item) => item.hash !== link.hash);
} catch (e) {
this.$showError(e)
this.$showError(e);
}
},
humanTime (time) {
return moment(time * 1000).fromNow()
humanTime(time) {
return moment(time * 1000).fromNow();
},
buildLink (hash) {
return `${window.location.origin}${baseURL}/share/${hash}`
buildLink(hash) {
return `${window.location.origin}${baseURL}/share/${hash}`;
},
sort () {
downloadPromptsWithSharedCode(shared_code) {
return shared_code != undefined && shared_code != ""
? "prompts.downloadPromptsWithSharedCode"
: "prompts.downloadPrompts";
},
sort() {
this.links = this.links.sort((a, b) => {
if (a.expire === 0) return -1
if (b.expire === 0) return 1
return new Date(a.expire) - new Date(b.expire)
})
}
}
}
if (a.expire === 0) return -1;
if (b.expire === 0) return 1;
return new Date(a.expire) - new Date(b.expire);
});
},
},
};
</script>

View File

@ -62,4 +62,12 @@
.share__box__items #listing.list .item .modified {
width: 25%;
}
.share_wrong {
background: var(--red);
color: #fff;
padding: .5em;
text-align: center;
animation: .2s opac forwards;
}

View File

@ -19,6 +19,7 @@
"permalink": "Get Permanent Link",
"previous": "Previous",
"publish": "Publish",
"password": "Password",
"rename": "Rename",
"replace": "Replace",
"reportIssue": "Report Issue",
@ -29,10 +30,13 @@
"selectMultiple": "Select multiple",
"share": "Share",
"shell": "Toggle shell",
"submit": "Submit",
"switchView": "Switch view",
"toggleSidebar": "Toggle sidebar",
"update": "Update",
"upload": "Upload"
"upload": "Upload",
"sharedCode": "Shared code",
"optionalSharedCode": "Shared code(optional)"
},
"download": {
"downloadFile": "Download File",
@ -142,7 +146,10 @@
"show": "Show",
"size": "Size",
"upload": "Upload",
"uploadMessage": "Select an option to upload."
"uploadMessage": "Select an option to upload.",
"downloadPrompts": "Download url: {0}",
"downloadPromptsWithSharedCode": "Download url: {0} Shared code: {1}",
"invalidSharedCode": "Invalid shared code."
},
"search": {
"images": "Images",

View File

@ -13,7 +13,7 @@
"more": "更多",
"move": "移动",
"moveFile": "移动文件",
"new": "新",
"new": "新",
"next": "下一个",
"ok": "确定",
"permalink": "获取永久链接",
@ -32,7 +32,9 @@
"switchView": "切换显示方式",
"toggleSidebar": "切换侧边栏",
"update": "更新",
"upload": "上传"
"upload": "上传",
"sharedCode": "提取码",
"optionalSharedCode": "提取码(可选)"
},
"download": {
"downloadFile": "下载文件",
@ -142,7 +144,10 @@
"show": "点击以显示",
"size": "大小",
"upload": "上传",
"uploadMessage": "选择上传项。"
"uploadMessage": "选择上传项。",
"downloadPrompts": "下载链接:{0}",
"downloadPromptsWithSharedCode": "下载链接:{0} 提取码:{1}",
"invalidSharedCode": "提取码无效!"
},
"search": {
"images": "图像",

View File

@ -2,7 +2,7 @@ import { sync } from 'vuex-router-sync'
import store from '@/store'
import router from '@/router'
import i18n from '@/i18n'
import Vue from '@/utils/vue'
import Vue from 'vue'
import { recaptcha, loginPage } from '@/utils/constants'
import { login, validateLogin } from '@/utils/auth'
import App from '@/App'

View File

@ -1,45 +1,61 @@
<template>
<div v-if="!loading">
<div id="breadcrumbs">
<router-link :to="'/share/' + hash" :aria-label="$t('files.home')" :title="$t('files.home')">
<router-link
:to="'/share/' + hash"
:aria-label="$t('files.home')"
:title="$t('files.home')"
>
<i class="material-icons">home</i>
</router-link>
<span v-for="(link, index) in breadcrumbs" :key="index">
<span class="chevron"><i class="material-icons">keyboard_arrow_right</i></span>
<router-link :to="link.url">{{ link.name }}</router-link>
</span>
<span class="chevron"
><i class="material-icons">keyboard_arrow_right</i></span
>
<router-link :to="link.url">{{ link.name }}</router-link>
</span>
</div>
<div class="share">
<div class="share__box share__box__info">
<div class="share__box__header">
{{ req.isDir ? $t('download.downloadFolder') : $t('download.downloadFile') }}
</div>
<div class="share__box__element share__box__center share__box__icon">
<i class="material-icons">{{ icon }}</i>
</div>
<div class="share__box__element">
<strong>{{ $t('prompts.displayName') }}</strong> {{ req.name }}
</div>
<div class="share__box__element">
<strong>{{ $t('prompts.lastModified') }}:</strong> {{ humanTime }}
</div>
<div class="share__box__element">
<strong>{{ $t('prompts.size') }}:</strong> {{ humanSize }}
</div>
<div class="share__box__element share__box__center">
<a target="_blank" :href="link" class="button button--flat">{{ $t('buttons.download') }}</a>
</div>
<div class="share__box__element share__box__center">
<qrcode-vue :value="fullLink" size="200" level="M"></qrcode-vue>
</div>
<div class="share__box__header">
{{
req.isDir
? $t("download.downloadFolder")
: $t("download.downloadFile")
}}
</div>
<div class="share__box__element share__box__center share__box__icon">
<i class="material-icons">{{ icon }}</i>
</div>
<div class="share__box__element">
<strong>{{ $t("prompts.displayName") }}</strong> {{ req.name }}
</div>
<div class="share__box__element">
<strong>{{ $t("prompts.lastModified") }}:</strong> {{ humanTime }}
</div>
<div class="share__box__element">
<strong>{{ $t("prompts.size") }}:</strong> {{ humanSize }}
</div>
<div class="share__box__element share__box__center">
<a target="_blank" :href="link" class="button button--flat">{{
$t("buttons.download")
}}</a>
</div>
<div class="share__box__element share__box__center">
<qrcode-vue :value="fullLink" size="200" level="M"></qrcode-vue>
</div>
</div>
<div v-if="req.isDir && req.items.length > 0" class="share__box share__box__items">
<div
v-if="req.isDir && req.items.length > 0"
class="share__box share__box__items"
>
<div class="share__box__header" v-if="req.isDir">
{{ $t('files.files') }}
{{ $t("files.files") }}
</div>
<div id="listing" class="list">
<item v-for="(item) in req.items.slice(0, this.showLimit)"
<item
v-for="item in req.items.slice(0, this.showLimit)"
:key="base64(item.name)"
v-bind:index="item.index"
v-bind:name="item.name"
@ -47,26 +63,40 @@
v-bind:url="item.url"
v-bind:modified="item.modified"
v-bind:type="item.type"
v-bind:size="item.size">
v-bind:size="item.size"
>
</item>
<div v-if="req.items.length > showLimit" class="item">
<div>
<p class="name"> + {{ req.items.length - showLimit }} </p>
<p class="name">+ {{ req.items.length - showLimit }}</p>
</div>
</div>
<div :class="{ active: $store.state.multiple }" id="multiple-selection">
<p>{{ $t('files.multipleSelectionEnabled') }}</p>
<div @click="$store.commit('multiple', false)" tabindex="0" role="button" :title="$t('files.clear')" :aria-label="$t('files.clear')" class="action">
<div
:class="{ active: $store.state.multiple }"
id="multiple-selection"
>
<p>{{ $t("files.multipleSelectionEnabled") }}</p>
<div
@click="$store.commit('multiple', false)"
tabindex="0"
role="button"
:title="$t('files.clear')"
:aria-label="$t('files.clear')"
class="action"
>
<i class="material-icons">clear</i>
</div>
</div>
</div>
</div>
<div v-else-if="req.isDir && req.items.length === 0" class="share__box share__box__items">
<div
v-else-if="req.isDir && req.items.length === 0"
class="share__box share__box__items"
>
<h2 class="message">
<i class="material-icons">sentiment_dissatisfied</i>
<span>{{ $t('files.lonely') }}</span>
<span>{{ $t("files.lonely") }}</span>
</h2>
</div>
</div>
@ -74,151 +104,202 @@
<div v-else-if="error">
<not-found v-if="error.message === '404'"></not-found>
<forbidden v-else-if="error.message === '403'"></forbidden>
<div v-else-if="error.message === '401'">
<div class="card floating" id="shared_code">
<div class="card-title">
<h2>{{ $t("buttons.sharedCode") }}</h2>
</div>
<div class="share_wrong" v-if="shared_code != undefined && shared_code != ''">
{{ $t("prompts.invalidSharedCode") }}
</div>
<div class="card-content">
<input
v-focus
type="text"
:placeholder="$t('buttons.sharedCode')"
v-model="shared_code"
@keyup.enter="fetchData"
/>
</div>
<div class="card-action">
<button
class="button button--flat"
@click="fetchData"
:aria-label="$t('buttons.submit')"
:title="$t('buttons.submit')"
>
{{ $t("buttons.submit") }}
</button>
</div>
</div>
</div>
<internal-error v-else></internal-error>
</div>
</template>
<script>
import {mapState, mapMutations, mapGetters} from 'vuex';
import { share as api } from '@/api'
import { baseURL } from '@/utils/constants'
import filesize from 'filesize'
import moment from 'moment'
import QrcodeVue from 'qrcode.vue'
import Item from "@/components/files/ListingItem"
import Forbidden from './errors/403'
import NotFound from './errors/404'
import InternalError from './errors/500'
import { mapState, mapMutations, mapGetters } from "vuex";
import { share as api } from "@/api";
import { baseURL } from "@/utils/constants";
import filesize from "filesize";
import moment from "moment";
import QrcodeVue from "qrcode.vue";
import Item from "@/components/files/ListingItem";
import Forbidden from "./errors/403";
import NotFound from "./errors/404";
import InternalError from "./errors/500";
export default {
name: 'share',
name: "share",
components: {
Item,
Forbidden,
NotFound,
InternalError,
QrcodeVue
QrcodeVue,
},
data: () => ({
error: null,
path: '',
showLimit: 500
path: "",
showLimit: 500,
}),
watch: {
'$route': 'fetchData'
$route: "fetchData",
},
created: async function () {
const hash = this.$route.params.pathMatch.split('/')[0]
this.setHash(hash)
await this.fetchData()
const hash = this.$route.params.pathMatch.split("/")[0];
this.setHash(hash);
await this.fetchData();
},
mounted () {
window.addEventListener('keydown', this.keyEvent)
mounted() {
window.addEventListener("keydown", this.keyEvent);
},
beforeDestroy () {
window.removeEventListener('keydown', this.keyEvent)
beforeDestroy() {
window.removeEventListener("keydown", this.keyEvent);
},
computed: {
...mapState(['hash', 'req', 'loading', 'multiple']),
...mapGetters(['selectedCount']),
...mapState(["hash", "req", "loading", "multiple"]),
...mapGetters(["selectedCount"]),
icon: function () {
if (this.req.isDir) return 'folder'
if (this.req.type === 'image') return 'insert_photo'
if (this.req.type === 'audio') return 'volume_up'
if (this.req.type === 'video') return 'movie'
return 'insert_drive_file'
if (this.req.isDir) return "folder";
if (this.req.type === "image") return "insert_photo";
if (this.req.type === "audio") return "volume_up";
if (this.req.type === "video") return "movie";
return "insert_drive_file";
},
link: function () {
return `${baseURL}/api/public/dl/${this.hash}${this.path}`
return `${baseURL}/api/public/dl/${this.hash}${this.path}?shared_code_token=${this.sharedCodeToken}`;
},
fullLink: function () {
return window.location.origin + this.link
return window.location.origin + this.link;
},
humanSize: function () {
if (this.req.isDir) {
return this.req.items.length
return this.req.items.length;
}
return filesize(this.req.size)
return filesize(this.req.size);
},
humanTime: function () {
return moment(this.req.modified).fromNow()
return moment(this.req.modified).fromNow();
},
breadcrumbs () {
let parts = this.path.split('/')
breadcrumbs() {
let parts = this.path.split("/");
if (parts[0] === '') {
parts.shift()
if (parts[0] === "") {
parts.shift();
}
if (parts[parts.length - 1] === '') {
parts.pop()
if (parts[parts.length - 1] === "") {
parts.pop();
}
let breadcrumbs = []
let breadcrumbs = [];
for (let i = 0; i < parts.length; i++) {
if (i === 0) {
breadcrumbs.push({ name: decodeURIComponent(parts[i]), url: '/share/' + this.hash + '/' + parts[i] + '/' })
} else {
breadcrumbs.push({ name: decodeURIComponent(parts[i]), url: breadcrumbs[i - 1].url + parts[i] + '/' })
breadcrumbs.push({
name: decodeURIComponent(parts[i]),
url: "/share/" + this.hash + "/" + parts[i] + "/",
});
} else {
breadcrumbs.push({
name: decodeURIComponent(parts[i]),
url: breadcrumbs[i - 1].url + parts[i] + "/",
});
}
}
if (breadcrumbs.length > 3) {
while (breadcrumbs.length !== 4) {
breadcrumbs.shift()
breadcrumbs.shift();
}
breadcrumbs[0].name = '...'
breadcrumbs[0].name = "...";
}
return breadcrumbs
}
return breadcrumbs;
},
},
methods: {
...mapMutations([ 'setHash', 'resetSelected', 'updateRequest', 'setLoading' ]),
...mapMutations([
"setHash",
"resetSelected",
"updateRequest",
"setLoading",
]),
base64: function (name) {
return window.btoa(unescape(encodeURIComponent(name)))
return window.btoa(unescape(encodeURIComponent(name)));
},
fetchData: async function () {
// Reset view information.
this.$store.commit('setReload', false)
this.$store.commit('resetSelected')
this.$store.commit('multiple', false)
this.$store.commit('closeHovers')
this.$store.commit("setReload", false);
this.$store.commit("resetSelected");
this.$store.commit("multiple", false);
this.$store.commit("closeHovers");
// Set loading to true and reset the error.
this.setLoading(true)
this.error = null
this.setLoading(true);
this.error = null;
try {
let file = await api.getHash(encodeURIComponent(this.$route.params.pathMatch))
this.path = file.path
if (file.isDir) file.items = file.items.map((item, index) => {
item.index = index
item.url = `/share/${this.hash}${this.path}/${encodeURIComponent(item.name)}`
return item
})
this.updateRequest(file)
this.setLoading(false)
const shared_code = this.shared_code || "";
let file = await api.getHash(
encodeURIComponent(this.$route.params.pathMatch),
shared_code
);
this.path = file.path;
this.sharedCodeToken = file.sharedCodeToken || "";
if (file.isDir)
file.items = file.items.map((item, index) => {
item.index = index;
item.url = `/share/${this.hash}${this.path}/${encodeURIComponent(
item.name
)}`;
return item;
});
this.updateRequest(file);
this.setLoading(false);
} catch (e) {
this.error = e
console.log(e);
this.error = e;
}
},
keyEvent (event) {
keyEvent(event) {
// Esc!
if (event.keyCode === 27) {
// If we're on a listing, unselect all
// files and folders.
if (this.selectedCount > 0) {
this.resetSelected()
this.resetSelected();
}
}
},
toggleMultipleSelection () {
this.$store.commit('multiple', !this.multiple)
}
}
}
toggleMultipleSelection() {
this.$store.commit("multiple", !this.multiple);
},
},
};
</script>

7
go.mod
View File

@ -2,7 +2,7 @@ module github.com/filebrowser/filebrowser/v2
require (
github.com/DataDog/zstd v1.4.0 // indirect
github.com/GeertJohan/go.rice v1.0.0
github.com/GeertJohan/go.rice v1.0.2
github.com/Sereal/Sereal v0.0.0-20190430203904-6faf9605eb56 // indirect
github.com/asdine/storm v2.1.2+incompatible
github.com/caddyserver/caddy v1.0.3
@ -10,6 +10,7 @@ require (
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/disintegration/imaging v1.6.2
github.com/dsnet/compress v0.0.1 // indirect
github.com/golang/protobuf v1.3.3 // indirect
github.com/golang/snappy v0.0.1 // indirect
github.com/gorilla/mux v1.7.3
github.com/gorilla/websocket v1.4.1
@ -33,11 +34,11 @@ require (
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8
golang.org/x/net v0.0.0-20200528225125-3c3fba18258b // indirect
golang.org/x/sys v0.0.0-20200523222454-059865788121 // indirect
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c // indirect
golang.org/x/text v0.3.2 // indirect
google.golang.org/appengine v1.5.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0
gopkg.in/yaml.v2 v2.2.7
gopkg.in/yaml.v2 v2.2.8
)
go 1.14

22
go.sum
View File

@ -5,8 +5,8 @@ github.com/DataDog/zstd v1.4.0 h1:vhoV+DUHnRZdKW1i5UMjAk2G4JY8wN4ayRfYDNdEhwo=
github.com/DataDog/zstd v1.4.0/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
github.com/GeertJohan/go.incremental v1.0.0 h1:7AH+pY1XUgQE4Y1HcXYaMqAI0m9yrFqo/jt0CW30vsg=
github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0=
github.com/GeertJohan/go.rice v1.0.0 h1:KkI6O9uMaQU3VEKaj01ulavtF7o1fWT7+pk/4voiMLQ=
github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0=
github.com/GeertJohan/go.rice v1.0.2 h1:PtRw+Tg3oa3HYwiDBZyvOJ8LdIyf6lAovJJtr7YOAYk=
github.com/GeertJohan/go.rice v1.0.2/go.mod h1:af5vUNlDNkCjOZeSGFgIJxDje9qdjsO6hshx0gTmZt4=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/Sereal/Sereal v0.0.0-20190430203904-6faf9605eb56 h1:3trCIB5GsAOIY8NxlfMztCYIhVsW9V5sZ+brsecjaiI=
github.com/Sereal/Sereal v0.0.0-20190430203904-6faf9605eb56/go.mod h1:D0JMgToj/WdxCgd30Kc1UcA9E+WdZoJqeVOuYW7iTBM=
@ -33,6 +33,7 @@ github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8Nz
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E=
github.com/daaku/go.zipexe v1.0.1 h1:wV4zMsDOI2SZ2m7Tdz1Ps96Zrx+TzaK15VbUaGozw0M=
@ -70,6 +71,8 @@ github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
@ -107,6 +110,7 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/cpuid v1.2.0 h1:NMpwD2G9JSFOE1/TJjGSo5zG7Yb2bTe7eq1jH+irmeE=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
@ -143,8 +147,8 @@ github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/naoina/go-stringutil v0.1.0/go.mod h1:XJ2SJL9jCtBh+P9q5btrd/Ylo8XwT/h1USek5+NqSA0=
github.com/naoina/toml v0.1.1/go.mod h1:NBIhNtsFMo3G2szEBne+bO4gS192HuIYRqfvOWb4i1E=
github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229 h1:E2B8qYyeSgv5MXpmzZXRNp8IAQ4vjxIjhpAf5hv/tAg=
github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E=
github.com/nkovacs/streamquote v1.0.0 h1:PmVIV08Zlx2lZK5fFZlMZ04eHcDTIFJCv/5/0twVUow=
github.com/nkovacs/streamquote v1.0.0/go.mod h1:BN+NaZ2CmdKqUuTUXUEm9j95B2TRbpOWpxbJYzzgUsc=
github.com/nwaples/rardecode v1.0.0 h1:r7vGuS5akxOnR4JQSkko62RJ1ReCMXxQRPtxsiFMBOs=
github.com/nwaples/rardecode v1.0.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
@ -172,7 +176,9 @@ github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7z
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/russross/blackfriday v0.0.0-20170610170232-067529f716f4/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
@ -280,8 +286,8 @@ golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e h1:ZytStCyV048ZqDsWHiYDdoI2V
golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121 h1:rITEj+UZHYC927n8GT97eC3zrpzXdb/voyeOuVKS46o=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
@ -318,8 +324,8 @@ gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@ -1,6 +1,7 @@
package http
import (
"errors"
"net/http"
"path"
"path/filepath"
@ -9,6 +10,7 @@ import (
"github.com/spf13/afero"
"github.com/filebrowser/filebrowser/v2/files"
"github.com/filebrowser/filebrowser/v2/share"
)
var withHashFile = func(fn handleFunc) handleFunc {
@ -19,6 +21,11 @@ var withHashFile = func(fn handleFunc) handleFunc {
return errToStatus(err), err
}
status, err := authenticateShareRequest(r, link, d.server.Salt)
if err != nil {
return status, err
}
user, err := d.store.Users.Get(d.server.Root, link.UserID)
if err != nil {
return errToStatus(err), err
@ -27,12 +34,13 @@ var withHashFile = func(fn handleFunc) handleFunc {
d.user = user
file, err := files.NewFileInfo(files.FileOptions{
Fs: d.user.Fs,
Path: link.Path,
Modify: d.user.Perm.Modify,
Expand: true,
ReadHeader: d.server.TypeDetectionByHeader,
Checker: d,
Fs: d.user.Fs,
Path: link.Path,
Modify: d.user.Perm.Modify,
Expand: true,
ReadHeader: d.server.TypeDetectionByHeader,
Checker: d,
SharedCodeToken: link.SharedCodeToken,
})
if err != nil {
return errToStatus(err), err
@ -43,11 +51,12 @@ var withHashFile = func(fn handleFunc) handleFunc {
d.user.Fs = afero.NewBasePathFs(d.user.Fs, filepath.Dir(link.Path))
file, err = files.NewFileInfo(files.FileOptions{
Fs: d.user.Fs,
Path: path,
Modify: d.user.Perm.Modify,
Expand: true,
Checker: d,
Fs: d.user.Fs,
Path: path,
Modify: d.user.Perm.Modify,
Expand: true,
Checker: d,
SharedCodeToken: link.SharedCodeToken,
})
if err != nil {
return errToStatus(err), err
@ -94,3 +103,20 @@ var publicDlHandler = withHashFile(func(w http.ResponseWriter, r *http.Request,
return rawDirHandler(w, r, d, file)
})
func authenticateShareRequest(r *http.Request, l *share.Link, salt string) (int, error) {
if l.SharedCode == "" {
return 0, nil
}
sharedCodeToken := r.URL.Query().Get("shared_code_token")
if sharedCodeToken == l.SharedCodeToken {
return 0, nil
}
sharedCode := r.Header.Get("X-SHARED-CODE")
if l.SharedCode != sharedCode {
return http.StatusUnauthorized, errors.New("invalid shared code")
}
return 0, nil
}

View File

@ -82,6 +82,10 @@ var sharePostHandler = withPermShare(func(w http.ResponseWriter, r *http.Request
rawExpire := r.URL.Query().Get("expires")
unit := r.URL.Query().Get("unit")
if r.Body != nil {
defer r.Body.Close()
}
if rawExpire == "" {
var err error
s, err = d.store.Share.GetPermanent(r.URL.Path, d.user.ID)
@ -104,6 +108,7 @@ var sharePostHandler = withPermShare(func(w http.ResponseWriter, r *http.Request
var expire int64 = 0
if rawExpire != "" {
//nolint:govet
num, err := strconv.Atoi(rawExpire)
if err != nil {
return http.StatusInternalServerError, err
@ -124,11 +129,30 @@ var sharePostHandler = withPermShare(func(w http.ResponseWriter, r *http.Request
expire = time.Now().Add(add).Unix()
}
s = &share.Link{
Path: r.URL.Path,
Hash: str,
Expire: expire,
UserID: d.user.ID,
sharedCode := r.URL.Query().Get("shared_code")
if sharedCode == "" {
s = &share.Link{
Path: r.URL.Path,
Hash: str,
Expire: expire,
UserID: d.user.ID,
SharedCodeToken: "",
SharedCode: "",
}
} else {
tokenBuffer := make([]byte, 96)
if _, err := rand.Read(tokenBuffer); err != nil {
return http.StatusInternalServerError, err
}
token := base64.URLEncoding.EncodeToString(tokenBuffer)
s = &share.Link{
Path: r.URL.Path,
Hash: str,
Expire: expire,
UserID: d.user.ID,
SharedCodeToken: token,
SharedCode: sharedCode,
}
}
if err := d.store.Share.Save(s); err != nil {

View File

@ -38,6 +38,7 @@ type Server struct {
Port string `json:"port"`
Address string `json:"address"`
Log string `json:"log"`
Salt string `json:"salt"`
EnableThumbnails bool `json:"enableThumbnails"`
ResizePreview bool `json:"resizePreview"`
EnableExec bool `json:"enableExec"`

View File

@ -2,8 +2,10 @@ package share
// Link is the information needed to build a shareable link.
type Link struct {
Hash string `json:"hash" storm:"id,index"`
Path string `json:"path" storm:"index"`
UserID uint `json:"userID"`
Expire int64 `json:"expire"`
Hash string `json:"hash" storm:"id,index"`
Path string `json:"path" storm:"index"`
UserID uint `json:"userID"`
Expire int64 `json:"expire"`
SharedCode string `json:"shared_code,omitempty"`
SharedCodeToken string `json:"token,omitempty"`
}

View File

@ -102,7 +102,9 @@ func (s *Storage) Gets(path string, id uint) ([]*Link, error) {
if err := s.Delete(link.Hash); err != nil {
return nil, err
}
links = append(links[:i], links[i+1:]...)
if len(links) > i+1 {
links = append(links[:i], links[i+1:]...)
}
}
}

View File

@ -1,4 +1,4 @@
#!/usr/bin/env sh
#!/bin/bash
set -e
@ -35,7 +35,8 @@ buildAssets () {
buildBinary () {
if ! [ -x "$(command -v rice)" ]; then
go install github.com/GeertJohan/go.rice/rice
go get github.com/GeertJohan/go.rice
go get github.com/GeertJohan/go.rice/rice
fi
cd $REPO/http
@ -55,28 +56,24 @@ release () {
echo "❌ This release script requires a single argument corresponding to the semver to be released. See semver.org"
exit 1
fi
echo "1"
GREP="grep"
if [ -x "$(command -v ggrep)" ]; then
GREP="ggrep"
fi
semver=$(echo "$1" | $GREP -P '^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)')
if [ $? -ne 0 ]; then
semver=$(echo "$1" | $GREP -P "^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)")
err=$?
if [ $err -ne 0 ]; then
echo "❌ Not valid semver format. See semver.org"
exit 1
fi
echo "🧼 Tidying up go modules"
go mod tidy
echo "🐑 Creating a new commit for the new release"
git commit --allow-empty -am "chore: version $semver"
git tag "$1"
git push
git push --tags origin
echo "📦 Done! $semver released."
}