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

Guest Upload V2 #178

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
18 changes: 18 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# TODO

## Feature: Guest Uploading
- [x] New admin page to manage guest uploading
- [x] View active tokens
- [x] Get links for guest uploads
- [x] Also allow deleting
- [x] New guest upload page /guestupload
- [x] Upload like admin
- [x] Only accept if file isn't too big

## Remaining tasks for first release
- [x] Do a good check for security holes
- [x] Show the link to the uploaded file instead of immediately going to download page
- [ ] ~~Enable E2E encrypted uploading~~
- [x] Remove E2E code from the guest upload page and JS
- [x] Display notices that guest tokens cannot use E2E encryption
- [ ] Actually delete the token after it's been used
11 changes: 9 additions & 2 deletions build/go-generate/minifyStaticContent.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ package main

import (
"fmt"
"os"
"path/filepath"

"github.com/tdewolff/minify/v2"
"github.com/tdewolff/minify/v2/css"
"github.com/tdewolff/minify/v2/js"
"os"
"path/filepath"
)

const pathPrefix = "../../internal/webserver/web/static/"
Expand Down Expand Up @@ -64,6 +65,12 @@ func getPaths() []converter {
Type: "text/javascript",
Name: "wasm_exec JS",
})
result = append(result, converter{
InputPath: pathPrefix + "js/guestupload.js",
OutputPath: pathPrefix + "js/min/guestupload.min.js",
Type: "text/javascript",
Name: "GuestUpload JS",
})
return result
}

Expand Down
3 changes: 3 additions & 0 deletions build/go-generate/updateVersionNumbers.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
const versionJsAdmin = "4"
const versionJsDropzone = "4"
const versionJsE2EAdmin = "3"
const versionGuestUpload = "1"
const versionCssMain = "2"

const fileMain = "../../cmd/gokapi/Main.go"
Expand Down Expand Up @@ -49,6 +50,7 @@ func getTemplate() string {
result = strings.ReplaceAll(result, "%jsadmin%", versionJsAdmin)
result = strings.ReplaceAll(result, "%jsdropzone%", versionJsDropzone)
result = strings.ReplaceAll(result, "%jse2e%", versionJsE2EAdmin)
result = strings.ReplaceAll(result, "%jsguestupload%", versionGuestUpload)
result = strings.ReplaceAll(result, "%css_main%", versionCssMain)
return result
}
Expand Down Expand Up @@ -84,4 +86,5 @@ const templateVersions = `// Change these for rebranding
{{define "js_admin_version"}}%jsadmin%{{end}}
{{define "js_dropzone_version"}}%jsdropzone%{{end}}
{{define "js_e2eversion"}}%jse2e%{{end}}
{{define "js_guestupload_version"}}%jsguestupload%{{end}}
{{define "css_main"}}%css_main%{{end}}`
6 changes: 6 additions & 0 deletions docs/setup.rst
Original file line number Diff line number Diff line change
Expand Up @@ -210,9 +210,12 @@ This option disables Gokapis internal authentication completely, except for API
- ``/apiKeys``
- ``/apiNew``
- ``/delete``
- ``/deleteUploadToken``
- ``/e2eInfo``
- ``/e2eSetup``
- ``/guestUploads``
- ``/logs``
- ``/newUploadToken``
- ``/uploadChunk``
- ``/uploadComplete``
- ``/uploadStatus``
Expand Down Expand Up @@ -297,6 +300,9 @@ If you are concerned that the configuration file can be read, you can also choos
.. note::
If you re-run the setup and enable encryption, unencrypted files will stay unencrypted. If you change any configuration related to encryption, all already encrypted files will be deleted.

.. note::
Files uploaded using a Guest Upload link will never be End-to-End encrypted, even if Level 3 encryption is enabled for the server. In this case these files will be uploaded without any encryption.

************************
Changing Configuration
************************
Expand Down
22 changes: 16 additions & 6 deletions internal/configuration/configupgrade/Upgrade.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
)

// CurrentConfigVersion is the version of the configuration structure. Used for upgrading
const CurrentConfigVersion = 20
const CurrentConfigVersion = 21

// DoUpgrade checks if an old version is present and updates it to the current version if required
func DoUpgrade(settings *models.Configuration, env *environment.Environment) bool {
Expand Down Expand Up @@ -50,11 +50,21 @@ func updateConfig(settings *models.Configuration, env *environment.Environment)
// < v1.8.5
if settings.ConfigVersion < 20 {
err := database.RawSqlite(`DROP TABLE UploadStatus; CREATE TABLE "UploadStatus" (
"ChunkId" TEXT NOT NULL UNIQUE,
"CurrentStatus" INTEGER NOT NULL,
"CreationDate" INTEGER NOT NULL,
PRIMARY KEY("ChunkId")
) WITHOUT ROWID;`)
"ChunkId" TEXT NOT NULL UNIQUE,
"CurrentStatus" INTEGER NOT NULL,
"CreationDate" INTEGER NOT NULL,
PRIMARY KEY("ChunkId")
) WITHOUT ROWID;`)
helper.Check(err)
}
// < v1.8.6
if settings.ConfigVersion < 21 {
err := database.RawSqlite(`CREATE TABLE IF NOT EXISTS "UploadTokens" (
"Id" TEXT NOT NULL UNIQUE,
"LastUsed" INTEGER NOT NULL,
"LastUsedString" TEXT NOT NULL,
PRIMARY KEY("Id")
) WITHOUT ROWID;`)
helper.Check(err)
}
}
Expand Down
13 changes: 11 additions & 2 deletions internal/configuration/database/Database.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ package database
import (
"database/sql"
"fmt"
"github.com/forceu/gokapi/internal/helper"
"log"

"github.com/forceu/gokapi/internal/helper"

// Required for sqlite driver
_ "modernc.org/sqlite"
"os"
"path/filepath"

_ "modernc.org/sqlite"
)

var sqliteDb *sql.DB
Expand Down Expand Up @@ -99,6 +102,12 @@ func createNewDatabase() {
"Permissions" INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY("Id")
) WITHOUT ROWID;
CREATE TABLE "UploadTokens" (
"Id" TEXT NOT NULL UNIQUE,
"LastUsed" INTEGER NOT NULL,
"LastUsedString" TEXT NOT NULL,
PRIMARY KEY("Id")
) WITHOUT ROWID;
CREATE TABLE "E2EConfig" (
"id" INTEGER NOT NULL UNIQUE,
"Config" BLOB NOT NULL,
Expand Down
50 changes: 45 additions & 5 deletions internal/configuration/database/Database_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,19 @@ package database
import (
"database/sql"
"errors"
"github.com/DATA-DOG/go-sqlmock"
"github.com/forceu/gokapi/internal/helper"
"github.com/forceu/gokapi/internal/models"
"github.com/forceu/gokapi/internal/test"
"golang.org/x/exp/slices"
"math"
"os"
"regexp"
"sync"
"testing"
"time"

"github.com/DATA-DOG/go-sqlmock"
"golang.org/x/exp/slices"

"github.com/forceu/gokapi/internal/helper"
"github.com/forceu/gokapi/internal/models"
"github.com/forceu/gokapi/internal/test"
)

func TestMain(m *testing.M) {
Expand Down Expand Up @@ -152,6 +154,44 @@ func TestApiKey(t *testing.T) {
test.IsEqualString(t, key.FriendlyName, "Old Key")
}

func TestGuestUploadToken(t *testing.T) {
SaveUploadToken(models.UploadToken{
Id: "newtoken",
LastUsedString: "LastUsed",
LastUsed: 100,
})
SaveUploadToken(models.UploadToken{
Id: "newtoken2",
LastUsedString: "LastUsed2",
LastUsed: 200,
})

tokens := GetAllUploadTokens()
test.IsEqualInt(t, len(tokens), 2)
test.IsEqualString(t, tokens["newtoken"].Id, "newtoken")
test.IsEqualString(t, tokens["newtoken"].LastUsedString, "LastUsed")
test.IsEqualInt64(t, tokens["newtoken"].LastUsed, 100)

test.IsEqualInt(t, len(GetAllUploadTokens()), 2)
DeleteUploadToken("newtoken2")
test.IsEqualInt(t, len(GetAllUploadTokens()), 1)

token, ok := GetUploadToken("newtoken")
test.IsEqualBool(t, ok, true)
test.IsEqualString(t, token.Id, "newtoken")
_, ok = GetUploadToken("newtoken2")
test.IsEqualBool(t, ok, false)

SaveUploadToken(models.UploadToken{
Id: "newtoken",
LastUsed: 100,
LastUsedString: "RecentlyUsed",
})
token, ok = GetUploadToken("newtoken")
test.IsEqualBool(t, ok, true)
test.IsEqualString(t, token.LastUsedString, "RecentlyUsed")
}

func TestSession(t *testing.T) {
renewAt := time.Now().Add(1 * time.Hour).Unix()
SaveSession("newsession", models.Session{
Expand Down
79 changes: 79 additions & 0 deletions internal/configuration/database/guestuploads.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package database

import (
"database/sql"
"errors"

"github.com/forceu/gokapi/internal/helper"
"github.com/forceu/gokapi/internal/models"
)

type schemaUploadTokens struct {
Id string
FriendlyName string
LastUsed int64
LastUsedString string
Permissions int
}

// GetAllUploadTokens returns a map with all upload tokens
func GetAllUploadTokens() map[string]models.UploadToken {
result := make(map[string]models.UploadToken)

rows, err := sqliteDb.Query("SELECT * FROM UploadTokens")
helper.Check(err)
defer rows.Close()
for rows.Next() {
rowData := schemaUploadTokens{}
err = rows.Scan(&rowData.Id, &rowData.LastUsed, &rowData.LastUsedString)
helper.Check(err)
result[rowData.Id] = models.UploadToken{
Id: rowData.Id,
LastUsed: rowData.LastUsed,
LastUsedString: rowData.LastUsedString,
}
}
return result
}

// GetUploadToken returns a models.UploadToken if valid or false if the ID is not valid
func GetUploadToken(id string) (models.UploadToken, bool) {
var rowResult schemaUploadTokens
row := sqliteDb.QueryRow("SELECT * FROM UploadTokens WHERE Id = ?", id)
err := row.Scan(&rowResult.Id, &rowResult.LastUsed, &rowResult.LastUsedString)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return models.UploadToken{}, false
}
helper.Check(err)
return models.UploadToken{}, false
}

result := models.UploadToken{
Id: rowResult.Id,
LastUsed: rowResult.LastUsed,
LastUsedString: rowResult.LastUsedString,
}

return result, true
}

// SaveUploadToken saves the upload token to the database
func SaveUploadToken(uploadToken models.UploadToken) {
_, err := sqliteDb.Exec("INSERT OR REPLACE INTO UploadTokens (Id, LastUsed, LastUsedString) VALUES (?, ?, ?)",
uploadToken.Id, uploadToken.LastUsed, uploadToken.LastUsedString)
helper.Check(err)
}

// UpdateTimeUploadToken writes the content of LastUsage to the database
func UpdateTimeUploadToken(uploadToken models.UploadToken) {
_, err := sqliteDb.Exec("UPDATE UploadTokens SET LastUsed = ?, LastUsedString = ? WHERE Id = ?",
uploadToken.LastUsed, uploadToken.LastUsedString, uploadToken.Id)
helper.Check(err)
}

// DeleteUploadToken deletes an upload token with the given ID
func DeleteUploadToken(id string) {
_, err := sqliteDb.Exec("DELETE FROM UploadTokens WHERE Id = ?", id)
helper.Check(err)
}
2 changes: 1 addition & 1 deletion internal/configuration/setup/ProtectedUrls.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ package setup

// protectedUrls contains a list of URLs that need to be protected if authentication is disabled.
// This list will be displayed during the setup
var protectedUrls = []string{"/admin", "/apiDelete", "/apiKeys", "/apiNew", "/delete", "/e2eInfo", "/e2eSetup", "/logs", "/uploadChunk", "/uploadComplete", "/uploadStatus"}
var protectedUrls = []string{"/admin", "/apiDelete", "/apiKeys", "/apiNew", "/delete", "/deleteUploadToken", "/e2eInfo", "/e2eSetup", "/guestUploads", "/logs", "/newUploadToken", "/uploadChunk", "/uploadComplete", "/uploadStatus"}
1 change: 1 addition & 0 deletions internal/configuration/setup/templates/setup.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,7 @@ function TestAWS(button, isManual) {
<li>Does not support download progress bar</li>
<li>Gokapi starts without user input</li>
<li>Files uploaded through the API have to be unencrypted</li>
<li>Files uploaded through a Guest Upload link have to be unencrypted</li>
<li>Password cannot be read with access to Gokapi configuration</li>
<li><b>Warning:</b> Download and upload might be significantly slower</li>
<li><b>Warning:</b> Encryption keys are stored in plain-text on this machine</li>
Expand Down
9 changes: 9 additions & 0 deletions internal/models/GuestUpload.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package models

// UploadToken contains data of a single guest upload token.
// It is essntially a single-use API key for uploading one item.
type UploadToken struct {
Id string `json:"Id"`
LastUsedString string `json:"LastUsedString"`
LastUsed int64 `json:"LastUsed"`
}
2 changes: 1 addition & 1 deletion internal/test/testconfiguration/TestConfiguration.go
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,7 @@ var configTestFile = []byte(`{
"ServerUrl": "http://127.0.0.1:53843/",
"RedirectUrl": "https://test.com/",
"PublicName": "Gokapi Test Version",
"ConfigVersion": 20,
"ConfigVersion": 21,
"LengthId": 20,
"DataDir": "test/data",
"MaxFileSizeMB": 25,
Expand Down
Loading
Loading