Compare commits

...

64 commits

Author SHA1 Message Date
Dániel Szabó
24be6cdeb6
Update README.md 2024-11-02 12:43:56 +02:00
Dániel Szabó
c5ae7d306f
Update README.md 2024-11-02 12:42:52 +02:00
Dániel Szabó
84e8f5ac5e
Merge pull request #203 from fastfailure/patch-1
Update broken documentation link
2024-10-24 13:55:45 +09:00
Dániel Szabó
92d2ac9c19
Merge pull request #211 from luk1337/luk/fixup
Minor fixups
2024-10-24 13:55:16 +09:00
Dániel Szabó
7c6660960d
Merge pull request #239 from jixunmoe/fix/32-bit-animal-numbers
fix: division by zero on 32-bit platform (#107, #118)
2024-10-24 13:49:45 +09:00
Dániel Szabó
8c4b039a03
Merge pull request #255 from Timshel/absolute
Attachments compatible with absolute path
2024-10-24 13:49:23 +09:00
Dániel Szabó
54e839ce6f
Merge pull request #246 from luk1337/luk/charset
Set charset=utf-8 for /raw/{id} response
2024-10-24 13:49:01 +09:00
Dániel Szabó
d3a7eaf072
Merge pull request #277 from dvdsk/streaming_file_up_down
Awnser Range requests and stream files downloads
2024-10-24 13:48:41 +09:00
Dániel Szabó
d6c06c0550
Merge pull request #267 from luochen1990/fix-privacy
Fix privacyDropdown is null issue
2024-10-24 13:48:04 +09:00
Dániel Szabó
043eb67f23
Merge pull request #260 from runofthemillgeek/fix/never-expire-condition
Fix never expire condition
2024-10-24 13:46:53 +09:00
Dániel Szabó
6f460ecd72
Merge pull request #268 from isaacasensio/patch-1
Fix default value comments for some environment variables
2024-10-24 13:46:28 +09:00
Dániel Szabó
f477bae5a8
Merge pull request #228 from secondubly/issue-221-fix
MICROBIN_UPLOADER_PASSWORD was missing from compose.yaml
2024-10-24 13:45:56 +09:00
Dániel Szabó
e841fdd320
Merge pull request #279 from dvdsk/no-c-deps
Adds a feature no-c-deps which makes microbin easy to crosscompile
2024-10-24 13:45:42 +09:00
Dániel Szabó
6e08eed32a
Merge pull request #281 from dvdsk/fix-json-db
Fix Json db losing already saved pasta's on crash/power failure
2024-10-24 13:43:46 +09:00
dvdsk
d0e0907ca9
Json db can no longer lose already saved pasta's
The database file was truncated before writes the complete list of
pastas from memory back into it. If power failed in between or microbin
crashed all pasta's would be lost.

This creates a new file then replaces the old with it once the writing
is done. Note the replace (rename) is usually (definitly on linux)
atomic. Therefore it either succeeds or fails and only the new changes
are lost.
2024-10-22 19:13:50 +02:00
dvdsk
28463e48c1
Adds a feature no-c-deps which makes microbin easy to crosscompile
With `no-c-deps` enabled microbin can be compiled for aarch64 by simply
passing in `--no-default-features`, `--no-c-deps` & `--target
aarch64-unknown-linux-musl`. The resulting binary is fully static and
does *not* depend on glibc. Therefore it can be deployed to any linux
target running an aarch64 cpu (arm). There is no need for any
containers.

There where four obstactles to easily statically linking microbin
with musl.

- The syntect library depended on the onigura regex engine. With
  `no-c-deps` it uses `fancy-regex` a slower alternative fully written
  in rust.
- Dependency actix-web supported zstd compression requiring the system
  zstd library to be present.
- The rusqlite library build and linked the C-library sqlite. That needs
  a crosscompiler and various crosscompile packages. With `no-c-deps`
  enabled the sqlite db option is disabled and the -json-db arg must be
  used. If the user does not pass -json-db microbin will panic when
  starting with a clear error message.
- reqwest was using openssl for reporting telemetry and checking for new
  versions. With `no-c-deps` it uses rustls with crypto provider
  rustcrypto. While rustcrypto has not been formally verified it should
  be good enough for sending telemetry (which should not contain any
  secure/personal data anyway) and checking versions.
2024-10-22 18:14:54 +02:00
dvdsk
b86adc7ac9
update deps + rustls 2024-10-22 01:40:41 +02:00
dvdsk
96de6125c9
Upgrade deps, builds upon pr #254 by Timshel 2024-10-22 01:14:26 +02:00
dvdsk
577d6246dc
Awnser Range requests and stream files downloads
Uses NamedFile from actix_files for unencrypted files instead of
reading them into memory and setting them as body.

Note the content_type is set by NamedFile now. The code for it is about
identical to what was previously there.
2024-10-21 17:38:05 +02:00
Isaac Asensio
5168d56491
Fix default value comments for some environment variables 2024-07-20 07:59:27 +02:00
LuoChen
5b4ce44743 Fix privacyDropdown is null issue 2024-07-09 19:57:25 +08:00
Sangeeth Sudheer
92c0b544ff
Fix never expire condition 2024-04-10 03:02:15 +05:30
Timshel
7d66d32ec4 attachments compatible with absolute path 2024-03-13 21:29:13 +01:00
Timshel
3a256305e7 Upgrade deps 2024-03-13 21:23:24 +01:00
LuK1337
70a666a854 Set charset=utf-8 for /raw/{id} response
This matches `<meta charset="utf-8">` in HTML and thus lets browser
decode content properly.
2023-12-23 19:02:49 +01:00
Jixun Wu
885c5c74b3 fix: division by zero on 32-bit platform (#107, #118) 2023-11-13 23:01:24 +00:00
secondubly
544f1d0bf6 MICROBIN_UPLOADER_PASSWORD was missing from compose.yaml 2023-10-09 13:53:38 -04:00
LuK1337
3b0c025e9b Hide "Privacy" if only public is supported 2023-08-11 00:32:29 +02:00
LuK1337
b028339b11 Make new pastas set editable to ARGS.editable 2023-08-10 23:29:03 +02:00
LuK1337
0223ead312 Fix #auth-form background color 2023-08-10 23:15:25 +02:00
LuK1337
c574282601 Unbreak password protected pastes when file upload is disabled 2023-08-10 22:43:14 +02:00
Lan Quil
b65fe28cec
Update broken documentation link
Replace documentation link (https://microbin.eu/documentation) with new one:
https://microbin.eu/docs/intro
2023-07-25 17:45:46 +02:00
Dániel Szabó
b8a0c5490d
Update FUNDING.yml 2023-07-15 11:06:43 +03:00
Dániel Szabó
c1fc2e22e5
Update compose.yaml
Fixes #181
2023-07-12 08:04:43 +03:00
Dániel Szabó
9688f913da
Fix typo in README 2023-07-11 22:05:57 +03:00
Daniel Szabo
4c57c27851 Fixed removal bug
Fixed a bug that caused private and secret uploads not to accept the correct password when being deleted
2023-07-11 21:22:26 +03:00
Daniel Szabo
7fdc89a48d Disabled ediiting for secrets
Realistically this privacy level should not allow modifying the data, but even if we did support that, the UX would be very annoying - it is better to make a new upload
2023-07-11 21:21:41 +03:00
Daniel Szabo
4a7360b90e Replaced "pasta" on all user-facing places with "upload"
- We understand what a pasta is, but let's avoid the situation when you send a link to your mom that ends with microbin.eu/pasta/dog-bat-cat and they misunderstand it.
- Also replaced /pastalist with just /list
- Internally kept "pasta" instead of "upload" to confuse everyone adopting MicroBin after v2
2023-07-11 20:58:34 +03:00
Daniel Szabo
a46312bf62 Fix upload list on mobile devices
Fixed bug that caused the table on /pastalist to break on narrow screens
2023-07-11 20:33:21 +03:00
Daniel Szabo
571bfbff1f Bump version for v2 release 2023-07-11 20:16:05 +03:00
Daniel Szabo
c7c54e35b4 Implemented uploader password
Minimal implementation of an auth mode that is read-only unless a password is provided. Enable MICROBIN_READONLY and set MICROBIN_UPLOADER_PASSWORD to try it out.

Fixes #106
2023-07-11 20:04:52 +03:00
Daniel Szabo
638f1bf510 Enabled HTML for footer text
Fixes #110
2023-07-11 19:16:47 +03:00
Daniel Szabo
8382457fc3 Merge branch 'master' of https://github.com/szabodanika/microbin 2023-07-11 17:36:37 +03:00
Daniel Szabo
917ce3c713 Update checking and telemetry improvements
- Implemented configuration telemetry
- Added option to disable version checking
- Updated URL for versioning and telemetry endpoint
- Added option to opt-in for a public MicroBin server list in the future
2023-07-11 17:36:33 +03:00
Dániel Szabó
6beb094d52
Update issue templates 2023-07-11 13:52:41 +03:00
Daniel Szabo
9e03864090 set openssl dependency as vendored
Fixes build github action error caused by dependency of a non-included openssl system binary in the action used to build MicroBin
2023-07-10 14:45:21 +03:00
Dániel Szabó
6e76cb750b
Install libss-dev for build action 2023-07-10 14:33:29 +03:00
Dániel Szabó
6505bdb262
Update website links 2023-07-10 14:25:19 +03:00
Daniel Szabo
590e3022e8 Bump version for v2 beta 4 2023-07-10 14:21:14 +03:00
Daniel Szabo
05126fee68 Improved password protection
- Implemented password protection for removals
- Added success message on password-protected upload creation and editing for clarity
- Made auth page focus password field when it's left empty
2023-07-10 14:19:11 +03:00
Daniel Szabo
4372158ed3 Polish Readme wording and ordering 2023-07-10 13:56:56 +03:00
Daniel Szabo
d06e3aca07 Update LICENSE 2023-07-10 13:56:42 +03:00
Daniel Szabo
6322d6cbb0 Added simple update checker
By parsing some json served by the MicroBin website, MicroBin can now check whether there is a newer version out there, and display the update information on the admin screen.
2023-07-09 14:00:29 +03:00
Daniel Szabo
f6c6908ea2 Update .env 2023-07-08 23:19:25 +03:00
Daniel Szabo
2e87b41d37 Added file upload progress reporting
The "Save" button will now show file upload progress as a percentage if submission is taking more than 1000 milliseconds
2023-07-08 22:53:21 +03:00
Daniel Szabo
a4ab03322c update sqlite directory and gitignore
- Missed one place where the data directory changes in commit 668b460
- Updated gitignore to reflect new default data directory path
2023-07-08 22:42:02 +03:00
Daniel Szabo
668b4608ac Added option to change data directory
Fixes #46
2023-07-08 22:26:26 +03:00
Daniel Szabo
664c4495e0 Fixed pasta creation bug
Fixed a bug that caused new uploads not to save their text content if the server had the encryption features turned off
2023-07-08 21:54:13 +03:00
Daniel Szabo
efdcf0f5e2 Bump versioning for new beta release 2023-07-08 19:18:58 +03:00
Daniel Szabo
b051ceff62 Fixed 2 bugs on index.html
- Fixes extra letter "a" before Never Expire option
- Fixes #180
2023-07-08 19:17:19 +03:00
Daniel Szabo
dcc3f37f8c enabled video embedding 2023-07-08 19:16:16 +03:00
Daniel Szabo
6253ede41c Improved client-side file encryption reliability
Still not perfect, but works better with non-text files as well finally. Worked on presenting the proper UI elements as well, as sometimes the wrong download button was showing or the password field was misisng.
2023-07-08 15:47:11 +03:00
Daniel Szabo
5d2007fe32 Added missing Incorrect Password status
Added missing Incorrect Password status to pasta_auth screen when opening client-side encrypted upload
2023-07-08 15:45:00 +03:00
Daniel Szabo
33dc7cc02a Update .env
- Deprecated  PURE_HTML arg
- Negated NO_ETERNAL_PASTA
2023-07-08 15:44:02 +03:00
44 changed files with 3105 additions and 1345 deletions

10
.cargo/config.toml Normal file
View file

@ -0,0 +1,10 @@
# specifies the linker for compiling to these targets
# this needs to be done to allow cross compiling
# may need more entries for more architectures, if you run into
# issues on something else then aarch64 musl open an issue and
# point to this comment. This will no longer be necessary when
# rust-lld is stabilizes.
[target.aarch64-unknown-linux-musl]
rustflags = ["-Clinker=rust-lld"]

50
.env
View file

@ -38,29 +38,31 @@ export MICROBIN_ADMIN_PASSWORD=m1cr0b1n
# finalised pastas but there will be an extra checkbox to
# make your new pasta editable from the pasta list or the
# pasta view page.
# Default value: 8080
# Default value: true
export MICROBIN_EDITABLE=true
# Replaces the default footer text with your own. If you
# want to hide the footer, use the hide footer option instead.
# Note that you can also embed HTML here, so you may want to escape
# '<', '>' and so on.
# export MICROBIN_FOOTER_TEXT=
# Hides the navigation bar on every page.
# Default value: 8080
# Default value: false
export MICROBIN_HIDE_HEADER=false
# Hides the footer on every page.
# Default value: 8080
# Default value: false
export MICROBIN_HIDE_FOOTER=false
# Hides the MicroBin logo from the navigation bar on every
# page.
# Default value: 8080
# Default value: false
export MICROBIN_HIDE_LOGO=false
# Disables the /pastalist endpoint, essentially making all
# pastas private.
# Default value: 8080
# Default value: false
export MICROBIN_NO_LISTING=false
# Enables syntax highlighting support. When creating a new
@ -82,14 +84,20 @@ export MICROBIN_BIND="0.0.0.0"
# pasta private, which then won't show up on the pastalist
# page. With the URL to your pasta, it will still be
# accessible.
# Default value: false
# Default value: true
export MICROBIN_PRIVATE=true
# DEPRECATED: Will be removed soon. If you want to change styling (incl. removal), use custom CSS variable instead.
# Disables main CSS styling, just uses a few in-line
# stylings for the layout. With this option you will lose
# dark-mode support.
# dark-mode support.
export MICROBIN_PURE_HTML=false
# Sets the name of the directory where MicroBin creates
# its database and stores attachments.
# Default value: microbin_data
export MICROBIN_DATA_DIR="microbin_data"
# Enables storing pasta data (not attachments and files) in
# a JSON file instead of the SQLite database.
# Default value: false
@ -109,8 +117,11 @@ export MICROBIN_JSON_DB=false
# unset.Example value: https://b.in/ export
# MICROBIN_SHORT_PATH=
# If set to true, disables adding/editing/removing pastas
# entirely.
# The password required for uploading, if read-only mode is enabled
# Default value: unset
# export MICROBIN_UPLOADER_PASSWORD=
# If set to true, authentication required for uploading
# Default value: false
export MICROBIN_READONLY=false
@ -155,9 +166,9 @@ export MICROBIN_WIDE=false
# Default value: false
export MICROBIN_QR=true
# Disables "Never" expiry settings for pastas. Default
# Toggles "Never" expiry settings for pastas. Default
# value: false
export MICROBIN_NO_ETERNAL_PASTA=true
export MICROBIN_ETERNAL_PASTA=false
# Enables "Read-only" uploads. These are unlisted and
# unencrypted, but can be viewed without password if you
@ -208,4 +219,19 @@ export MICROBIN_MAX_FILE_SIZE_ENCRYPTED_MB=256
# encryption (more strain on your server than without
# encryption, so the limit should be lower. Secrets tend to
# be tiny files usually anyways.) Default value: 2048.
export MICROBIN_MAX_FILE_SIZE_UNENCRYPTED_MB=2048
export MICROBIN_MAX_FILE_SIZE_UNENCRYPTED_MB=2048
# Disables the feature that checks for available updates
# when opening the admin screen.
# Default value: false
export MICROBIN_DISABLE_UPDATE_CHECKING=false
# Disables telemetry if set to true.
# Telemetry includes your configuration and helps development.
# It does not include any sensitive data.
# Default value: false
export MICROBIN_DISABLE_TELEMETRY=false
# Enables listing your server in the public MicroBin server list.
# Default value: false
export MICROBIN_LIST_SERVER=false

2
.github/FUNDING.yml vendored
View file

@ -1,4 +1,4 @@
# These are supported funding model platforms
github: szabodanika
ko_fi: dani_sz

38
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View file

@ -0,0 +1,38 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

View file

@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View file

@ -93,6 +93,10 @@ jobs:
target: ${{ matrix.target }}
toolchain: stable
profile: minimal # minimal component installation (ie, no documentation)
- name: Install OpenSSL
if: runner.os == 'Linux'
run: sudo apt-get install -y libssl-dev
- name: Show Version Information (Rust, cargo, GCC)
shell: bash
@ -189,4 +193,4 @@ jobs:
linux/arm64
push: ${{ github.ref_type == 'tag' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
labels: ${{ steps.meta.outputs.labels }}

1
.gitignore vendored
View file

@ -10,5 +10,6 @@ target/
*.pdb
pasta_data/*
microbin_data/*
*.env
**/**/microbin-data

2549
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,43 +1,70 @@
[package]
name = "microbin"
version = "2.0.1"
version = "2.0.4"
edition = "2021"
authors = ["Daniel Szabo <daniel.szabo99@outlook.com>"]
rust-version = "1.74.0"
authors = ["Daniel Szabo <daniel@microbin.eu>"]
license = "BSD-3-Clause"
description = "Simple, performant, configurable, entirely self-contained Pastebin and URL shortener."
readme = "README.md"
homepage = "https://microbin.eu"
repository = "https://github.com/szabodanika/microbin"
keywords = ["pastebin", "pastabin", "microbin", "actix", "selfhosted"]
keywords = ["pastebin", "filesharing", "microbin", "actix", "selfhosted"]
categories = ["pastebins"]
[dependencies]
actix-web = "4"
actix-files = "0.6.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.80"
bytesize = { version = "1.1", features = ["serde"] }
actix-files = "0.6.6"
actix-multipart = "0.7.2"
actix-web = { version = "4", default-features = false, features = [
"compat","compress-brotli", "compress-gzip", "cookies", "http2", "macros", "unicode"] }
actix-web-httpauth = "0.8.2"
askama = "0.10"
askama-filters = { version = "0.1.3", features = ["chrono"] }
bytesize = { version = "1.1", features = ["serde"] }
chrono = "0.4.19"
rand = "0.8.5"
linkify = "0.8.1"
clap = { version = "3.1.12", features = ["derive", "env"] }
actix-multipart = "0.4.0"
futures = "0.3"
sanitize-filename = "0.3.0"
log = "0.4"
env_logger = "0.9.0"
actix-web-httpauth = "0.6.0"
lazy_static = "1.4.0"
syntect = "5.0"
qrcode-generator = "4.1.6"
rust-embed = "6.4.2"
mime_guess = "2.0.4"
futures = "0.3"
harsh = "0.2"
html-escape = "0.2.13"
magic-crypt = "3.1.12"
rusqlite = { version = "0.29.0", features = ["bundled"] }
lazy_static = "1.4.0"
linkify = "0.10.0"
log = "0.4.21"
magic-crypt = "3.1.13"
mime_guess = "2.0.4"
once_cell = "1.19.0"
qrcode-generator = "4.1.9"
rand = "0.8.5"
reqwest = { version = "0.12", default-features = false, features = ["charset",
"http2", "macos-system-configuration", "json", "blocking"] }
rusqlite = { version = "0.32", features = ["bundled"], optional = true }
rust-embed = "8.3.0"
# The rustls-rustcrypto version must support the rustls version and the
# rustls version must match the one expected by reqwest;
rustls = { version = "0.23", default-features = false, features = ["custom-provider"], optional = true }
rustls-rustcrypto = { version = "0.0.2-alpha", optional = true }
sanitize-filename = "0.5.0"
serde_json = "1.0.114"
serde = { version = "1.0.197", features = ["derive"] }
syntect = { version = "5.2.0", default-features = false }
webpki-roots = { version = "0.26", optional = true }
[dependencies.openssl]
version = "0.10.64"
features = ["vendored"]
optional = true
[features]
default = ["__default-tls", "__zstd", "__syntect-fast", "dep:rusqlite"]
no-c-deps = ["__rustcrypto-tls", "__syntect-rust"]
__default-tls = ["reqwest/default-tls", "dep:openssl"]
__rustcrypto-tls = ["reqwest/rustls-tls-manual-roots-no-provider", "dep:rustls", "dep:rustls-rustcrypto", "webpki-roots"]
__syntect-fast = ["syntect/default-onig"]
__syntect-rust = ["syntect/default-fancy"]
__zstd = ["actix-web/compress-zstd"]
[profile.release]
lto = true

View file

@ -1,6 +1,6 @@
BSD 3-Clause License
Copyright (c) 2022, Dániel Szabó
Copyright (c) 2022-2023, Dániel Szabó
All rights reserved.
Redistribution and use in source and binary forms, with or without

View file

@ -7,9 +7,8 @@
[![crates.io](https://img.shields.io/crates/v/microbin.svg)](https://crates.io/crates/microbin)
[![Docker Image](https://github.com/szabodanika/microbin/actions/workflows/release.yml/badge.svg)](https://hub.docker.com/r/danielszabo99/microbin)
[![Docker Pulls](https://img.shields.io/docker/pulls/danielszabo99/microbin?label=Docker%20pulls)](https://img.shields.io/docker/pulls/danielszabo99/microbin?label=Docker%20pulls)
[![Support Server](https://img.shields.io/discord/662017309162078267.svg?color=7289da&label=Discord&logo=discord&style=flat-square)](https://discord.gg/3DsyTN7T)
MicroBin is a super tiny, feature rich, configurable, self-contained and self-hosted paste bin web application. It is very easy to set up and use, and will only require a few megabytes of memory and disk storage. It takes only a couple minutes to set it up, why not give it a try now?
MicroBin is a super tiny, feature-rich, configurable, self-contained and self-hosted paste bin web application. It is very easy to set up and use, and will only require a few megabytes of memory and disk storage. It takes only a couple minutes to set it up, why not give it a try now?
### Check out the Public Test Server at [pub.microbin.eu](https://pub.microbin.eu)!
@ -24,58 +23,55 @@ Or install it manually from [Cargo](https://crates.io/crates/microbin):
```bash
cargo install microbin;
curl -L -O https://raw.githubusercontent.com/;szabodanika/microbin/master/.env;
curl -L -O https://raw.githubusercontent.com/szabodanika/microbin/master/.env;
source .env;
microbin
```
On our website [microbin.eu](https://microbin.eu) you will find the following:
On our website [microbin.eu](https://microbin.eu), you will find the following:
- [Screenshots](https://microbin.eu/screenshots/)
- [Quickstart Guide](https://microbin.eu/quickstart/)
- [Documentation](https://microbin.eu/documentation/)
- [Donations and Sponsorships](https://microbin.eu/donate/)
- [Community](https://microbin.eu/community/)
- [Guide and Documentation](https://microbin.eu/docs/intro)
- [Donations and Sponsorships](https://microbin.eu/sponsorship)
- [Roadmap](https://microbin.eu/roadmap)
## Features
- Is very small
- Entirely self-contained executable, MicroBin is a single file!
- Animal names instead of random numbers for pasta identifiers (64 animals)
- Server-side and client-side encryption
- File uploads (eg. `server.com/file/pig-dog-cat`)
- Raw text serving (eg. `server.com/raw/pig-dog-cat`)
- URL shortening and redirection
- File uploads (e.g. `server.com/file/pig-dog-cat`)
- Raw text serving (e.g. `server.com/raw/pig-dog-cat`)
- QR code support
- Very simple database (JSON + files) for portability, easy backups and integration
- SQLite support
- Private and public, editable and final, automatically and never expiring uploads
- Syntax highlighting
- URL shortening and redirection
- Animal names instead of random numbers for upload identifiers (64 animals)
- SQLite and JSON database support
- Private and public, editable and uneditable, automatically and never expiring uploads
- Automatic dark mode and custom styling support with very little CSS and only vanilla JS (see [`water.css`](https://github.com/kognise/water.css))
- Most of the above can be toggled on and off!
- And much more!
## What is an upload?
In MicroBin, an upload can be:
- A text that you want to paste from one machine to another, eg. some code,
- A file that you want to share, eg. a video that is too large for Discord, a zip with a code project in it or an image,
- A URL redirect.
- A text that you want to paste from one machine to another, e.g. some code,
- A file that you want to share, e.g. a video that is too large for Discord, a zip with a code project in it or an image,
- A URL redirection.
## When is MicroBin useful?
You can use MicroBin:
- As a URL shortener/redirect service,
- To send long texts to other people,
- To send large files to other people,
- To serve content on the web, eg. configuration files for testing, images, or any other file content using the Raw functionality,
- To share secrets or sensitive documents securely,
- As a URL shortener/redirect service,
- To serve content on the web, eg . configuration files for testing, images, or any other file content using the Raw functionality,
- To move files between your desktop and a server you access from the console,
- As a "postbox" service where people can upload their files or texts, but they cannot see or remove what others sent you - just disable the upload page
- To take notes! Simply create an editable upload.
- As a "postbox" service where people can upload their files or texts, but they cannot see or remove what others sent you,
- Or even to take quick notes.
...and many other things, why not get creative?
MicroBin and MicroBin.eu are available under the [BSD 3-Clause License](LICENSE).
© Dániel Szabó 2022-2023
© Dániel Szabó 2022-2024

View file

@ -1,11 +1,11 @@
services:
microbin:
image: danielszabo99/microbin:2.0.0-beta1
image: danielszabo99/microbin:latest
restart: always
ports:
- "${MICROBIN_PORT}:8080"
volumes:
- ./microbin-data:/app/pasta_data
- ./microbin-data:/app/microbin_data
environment:
MICROBIN_BASIC_AUTH_USERNAME: ${MICROBIN_BASIC_AUTH_USERNAME}
MICROBIN_BASIC_AUTH_PASSWORD: ${MICROBIN_BASIC_AUTH_PASSWORD}
@ -21,10 +21,12 @@ services:
MICROBIN_BIND: ${MICROBIN_BIND}
MICROBIN_PRIVATE: ${MICROBIN_PRIVATE}
MICROBIN_PURE_HTML: ${MICROBIN_PURE_HTML}
MICROBIN_DATA_DIR: ${MICROBIN_DATA_DIR}
MICROBIN_JSON_DB: ${MICROBIN_JSON_DB}
MICROBIN_PUBLIC_PATH: ${MICROBIN_PUBLIC_PATH}
MICROBIN_SHORT_PATH: ${MICROBIN_SHORT_PATH}
MICROBIN_READONLY: ${MICROBIN_READONLY}
MICROBIN_UPLOADER_PASSWORD: ${MICROBIN_UPLOADER_PASSWORD}
MICROBIN_SHOW_READ_STATS: ${MICROBIN_SHOW_READ_STATS}
MICROBIN_TITLE: ${MICROBIN_TITLE}
MICROBIN_THREADS: ${MICROBIN_THREADS}
@ -42,4 +44,4 @@ services:
MICROBIN_ENCRYPTION_CLIENT_SIDE: ${MICROBIN_ENCRYPTION_CLIENT_SIDE}
MICROBIN_ENCRYPTION_SERVER_SIDE: ${MICROBIN_ENCRYPTION_SERVER_SIDE}
MICROBIN_MAX_FILE_SIZE_ENCRYPTED_MB: ${MICROBIN_MAX_FILE_SIZE_ENCRYPTED_MB}
MICROBIN_MAX_FILE_SIZE_UNENCRYPTED_MB: ${MICROBIN_MAX_FILE_SIZE_UNENCRYPTED_MB}
MICROBIN_MAX_FILE_SIZE_UNENCRYPTED_MB: ${MICROBIN_MAX_FILE_SIZE_UNENCRYPTED_MB}

View file

@ -1,5 +1,6 @@
use clap::Parser;
use lazy_static::lazy_static;
use serde::Serialize;
use std::convert::Infallible;
use std::fmt;
use std::net::IpAddr;
@ -9,7 +10,7 @@ lazy_static! {
pub static ref ARGS: Args = Args::parse();
}
#[derive(Parser, Debug, Clone)]
#[derive(Parser, Debug, Clone, Serialize)]
#[clap(author, version, about, long_about = None)]
pub struct Args {
#[clap(long, env = "MICROBIN_BASIC_AUTH_USERNAME")]
@ -66,6 +67,9 @@ pub struct Args {
#[clap(long, env = "MICROBIN_SHORT_PATH")]
pub short_path: Option<PublicUrl>,
#[clap(long, env = "MICROBIN_UPLOADER_PASSWORD")]
pub uploader_password: Option<String>,
#[clap(long, env = "MICROBIN_READONLY")]
pub readonly: bool,
@ -102,6 +106,9 @@ pub struct Args {
#[clap(long, env = "MICROBIN_DEFAULT_EXPIRY", default_value = "24hour")]
pub default_expiry: String,
#[clap(long, env = "MICROBIN_DATA_DIR", default_value = "microbin_data")]
pub data_dir: String,
#[clap(short, long, env = "MICROBIN_NO_FILE_UPLOAD")]
pub no_file_upload: bool,
@ -111,6 +118,15 @@ pub struct Args {
#[clap(long, env = "MICROBIN_HASH_IDS")]
pub hash_ids: bool,
#[clap(long, env = "MICROBIN_LIST_SERVER")]
pub list_server: bool,
#[clap(long, env = "MICROBIN_DISABLE_TELEMETRY")]
pub disable_telemetry: bool,
#[clap(long, env = "MICROBIN_DISABLE_UPDATE_CHECKING")]
pub disable_update_checking: bool,
#[clap(long, env = "MICROBIN_ENCRYPTION_CLIENT_SIDE")]
pub encryption_client_side: bool,
@ -150,9 +166,56 @@ impl Args {
String::from("")
}
}
pub fn without_secrets(self) -> Args {
Args {
auth_basic_username: None,
auth_basic_password: None,
auth_admin_username: String::from(""),
auth_admin_password: String::from(""),
editable: self.editable,
footer_text: self.footer_text,
hide_footer: self.hide_footer,
hide_header: self.hide_header,
hide_logo: self.hide_logo,
no_listing: self.no_listing,
highlightsyntax: self.highlightsyntax,
port: self.port,
bind: self.bind,
private: self.private,
pure_html: self.pure_html,
json_db: self.json_db,
public_path: self.public_path,
short_path: self.short_path,
uploader_password: None,
readonly: self.readonly,
show_read_stats: self.show_read_stats,
title: self.title,
list_server: self.list_server,
threads: self.threads,
gc_days: self.gc_days,
enable_burn_after: self.enable_burn_after,
default_burn_after: self.default_burn_after,
wide: self.wide,
qr: self.qr,
eternal_pasta: self.eternal_pasta,
enable_readonly: self.enable_readonly,
default_expiry: self.default_expiry,
data_dir: String::from(""),
no_file_upload: self.no_file_upload,
custom_css: self.custom_css,
hash_ids: self.hash_ids,
disable_telemetry: self.disable_telemetry,
encryption_client_side: self.encryption_client_side,
encryption_server_side: self.encryption_server_side,
max_file_size_encrypted_mb: self.max_file_size_encrypted_mb,
max_file_size_unencrypted_mb: self.max_file_size_unencrypted_mb,
disable_update_checking: self.disable_update_checking,
}
}
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Serialize)]
pub struct PublicUrl(pub String);
impl fmt::Display for PublicUrl {

View file

@ -1,6 +1,7 @@
use crate::args::{Args, ARGS};
use crate::pasta::Pasta;
use crate::util::misc::remove_expired;
use crate::util::version::{fetch_latest_version, Version, CURRENT_VERSION};
use crate::AppState;
use actix_multipart::Multipart;
use actix_web::{get, post, web, Error, HttpResponse};
@ -15,6 +16,7 @@ struct AdminTemplate<'a> {
status: &'a String,
version_string: &'a String,
message: &'a String,
update: &'a Option<Version>,
}
#[get("/admin")]
@ -33,11 +35,11 @@ pub async fn post_admin(
let mut password = String::from("");
while let Some(mut field) = payload.try_next().await? {
if field.name() == "username" {
if field.name() == Some("username") {
while let Some(chunk) = field.try_next().await? {
username.push_str(std::str::from_utf8(&chunk).unwrap().to_string().as_str());
}
} else if field.name() == "password" {
} else if field.name() == Some("password") {
while let Some(chunk) = field.try_next().await? {
password.push_str(std::str::from_utf8(&chunk).unwrap().to_string().as_str());
}
@ -71,13 +73,32 @@ pub async fn post_admin(
message = "Warning: You are using the default admin login details. This is a security risk, please change them."
}
let update;
if !ARGS.disable_update_checking {
let latest_version_res = fetch_latest_version().await;
if latest_version_res.is_ok() {
let latest_version = latest_version_res.unwrap();
if latest_version.newer_than_current() {
update = Some(latest_version);
} else {
update = None;
}
} else {
update = None;
}
} else {
update = None;
}
Ok(HttpResponse::Ok().content_type("text/html").body(
AdminTemplate {
pastas: &pastas,
args: &ARGS,
status: &String::from(status),
version_string: &String::from("2.0.1-20230704"),
version_string: &format!("{}", CURRENT_VERSION.long_title),
message: &String::from(message),
update: &update,
}
.render()
.unwrap(),

View file

@ -8,7 +8,7 @@ use actix_web::{get, web, HttpResponse};
use askama::Template;
#[derive(Template)]
#[template(path = "auth_pasta.html")]
#[template(path = "auth_upload.html")]
struct AuthPasta<'a> {
args: &'a Args,
id: String,
@ -19,7 +19,7 @@ struct AuthPasta<'a> {
}
#[get("/auth/{id}")]
pub async fn auth_pasta(data: web::Data<AppState>, id: web::Path<String>) -> HttpResponse {
pub async fn auth_upload(data: web::Data<AppState>, id: web::Path<String>) -> HttpResponse {
// get access to the pasta collection
let mut pastas = data.pastas.lock().unwrap();
@ -40,7 +40,7 @@ pub async fn auth_pasta(data: web::Data<AppState>, id: web::Path<String>) -> Htt
status: String::from(""),
encrypted_key: pasta.encrypted_key.to_owned().unwrap_or_default(),
encrypt_client: pasta.encrypt_client,
path: String::from("pasta"),
path: String::from("upload"),
}
.render()
.unwrap(),
@ -54,7 +54,7 @@ pub async fn auth_pasta(data: web::Data<AppState>, id: web::Path<String>) -> Htt
}
#[get("/auth/{id}/{status}")]
pub async fn auth_pasta_with_status(
pub async fn auth_upload_with_status(
data: web::Data<AppState>,
param: web::Path<(String, String)>,
) -> HttpResponse {
@ -80,7 +80,7 @@ pub async fn auth_pasta_with_status(
status,
encrypted_key: pasta.encrypted_key.to_owned().unwrap_or_default(),
encrypt_client: pasta.encrypt_client,
path: String::from("pasta"),
path: String::from("upload"),
}
.render()
.unwrap(),
@ -317,3 +317,78 @@ pub async fn auth_file_with_status(
.content_type("text/html")
.body(ErrorTemplate { args: &ARGS }.render().unwrap())
}
#[get("/auth_remove_private/{id}")]
pub async fn auth_remove_private(data: web::Data<AppState>, id: web::Path<String>) -> HttpResponse {
// get access to the pasta collection
let mut pastas = data.pastas.lock().unwrap();
remove_expired(&mut pastas);
let intern_id = if ARGS.hash_ids {
hashid_to_u64(&id).unwrap_or(0)
} else {
to_u64(&id).unwrap_or(0)
};
for (_, pasta) in pastas.iter().enumerate() {
if pasta.id == intern_id {
return HttpResponse::Ok().content_type("text/html").body(
AuthPasta {
args: &ARGS,
id: id.into_inner(),
status: String::from(""),
encrypted_key: pasta.encrypted_key.to_owned().unwrap_or_default(),
encrypt_client: pasta.encrypt_client,
path: String::from("remove"),
}
.render()
.unwrap(),
);
}
}
HttpResponse::Ok()
.content_type("text/html")
.body(ErrorTemplate { args: &ARGS }.render().unwrap())
}
#[get("/auth_remove_private/{id}/{status}")]
pub async fn auth_remove_private_with_status(
data: web::Data<AppState>,
param: web::Path<(String, String)>,
) -> HttpResponse {
// get access to the pasta collection
let mut pastas = data.pastas.lock().unwrap();
remove_expired(&mut pastas);
let (id, status) = param.into_inner();
let intern_id = if ARGS.hash_ids {
hashid_to_u64(&id).unwrap_or(0)
} else {
to_u64(&id).unwrap_or(0)
};
for (_i, pasta) in pastas.iter().enumerate() {
if pasta.id == intern_id {
return HttpResponse::Ok().content_type("text/html").body(
AuthPasta {
args: &ARGS,
id,
status,
encrypted_key: pasta.encrypted_key.to_owned().unwrap_or_default(),
encrypt_client: pasta.encrypt_client,
path: String::from("remove"),
}
.render()
.unwrap(),
);
}
}
HttpResponse::Ok()
.content_type("text/html")
.body(ErrorTemplate { args: &ARGS }.render().unwrap())
}

View file

@ -19,13 +19,33 @@ use std::time::{SystemTime, UNIX_EPOCH};
#[template(path = "index.html")]
struct IndexTemplate<'a> {
args: &'a ARGS,
status: String,
}
#[get("/")]
pub async fn index() -> impl Responder {
HttpResponse::Ok()
.content_type("text/html")
.body(IndexTemplate { args: &ARGS }.render().unwrap())
HttpResponse::Ok().content_type("text/html").body(
IndexTemplate {
args: &ARGS,
status: String::from(""),
}
.render()
.unwrap(),
)
}
#[get("/{status}")]
pub async fn index_with_status(param: web::Path<String>) -> HttpResponse {
let status = param.into_inner();
return HttpResponse::Ok().content_type("text/html").body(
IndexTemplate {
args: &ARGS,
status,
}
.render()
.unwrap(),
);
}
pub fn expiration_to_timestamp(expiration: &str, timenow: i64) -> i64 {
@ -38,9 +58,9 @@ pub fn expiration_to_timestamp(expiration: &str, timenow: i64) -> i64 {
"1week" => timenow + 60 * 60 * 24 * 7,
"never" => {
if ARGS.eternal_pasta {
timenow + 60 * 60 * 24 * 7
} else {
0
} else {
timenow + 60 * 60 * 24 * 7
}
}
_ => {
@ -50,16 +70,14 @@ pub fn expiration_to_timestamp(expiration: &str, timenow: i64) -> i64 {
}
}
/// receives a file through http Post on url /upload/a-b-c with a, b and c
/// different animals. The client sends the post in response to a form.
// TODO: form field order might need to be changed. In my testing the attachment
// data is nestled between password encryption key etc <21-10-24, dvdsk>
pub async fn create(
data: web::Data<AppState>,
mut payload: Multipart,
) -> Result<HttpResponse, Error> {
if ARGS.readonly {
return Ok(HttpResponse::Found()
.append_header(("Location", format!("{}/", ARGS.public_path_as_str())))
.finish());
}
let mut pastas = data.pastas.lock().unwrap();
let timenow: i64 = match SystemTime::now().duration_since(UNIX_EPOCH) {
@ -77,7 +95,7 @@ pub async fn create(
extension: String::from(""),
private: false,
readonly: false,
editable: true,
editable: ARGS.editable,
encrypt_server: false,
encrypted_key: Some(String::from("")),
encrypt_client: false,
@ -91,9 +109,20 @@ pub async fn create(
let mut random_key: String = String::from("");
let mut plain_key: String = String::from("");
let mut uploader_password = String::from("");
while let Some(mut field) = payload.try_next().await? {
match field.name() {
let Some(field_name) = field.name() else {
continue;
};
match field_name {
"uploader_password" => {
while let Some(chunk) = field.try_next().await? {
uploader_password
.push_str(std::str::from_utf8(&chunk).unwrap().to_string().as_str());
}
continue;
}
"random_key" => {
while let Some(chunk) = field.try_next().await? {
random_key = std::str::from_utf8(&chunk).unwrap().to_string();
@ -179,7 +208,7 @@ pub async fn create(
}
continue;
}
"syntax-highlight" => {
"syntax_highlight" => {
while let Some(chunk) = field.try_next().await? {
new_pasta.extension = std::str::from_utf8(&chunk).unwrap().to_string();
}
@ -190,7 +219,7 @@ pub async fn create(
continue;
}
let path = field.content_disposition().get_filename();
let path = field.content_disposition().and_then(|cd| cd.get_filename());
let path = match path {
Some("") => continue,
@ -207,13 +236,15 @@ pub async fn create(
};
std::fs::create_dir_all(format!(
"./pasta_data/attachments/{}",
"{}/attachments/{}",
ARGS.data_dir,
&new_pasta.id_as_animals()
))
.unwrap();
let filepath = format!(
"./pasta_data/attachments/{}/{}",
"{}/attachments/{}/{}",
ARGS.data_dir,
&new_pasta.id_as_animals(),
&file.name()
);
@ -242,6 +273,14 @@ pub async fn create(
}
}
if ARGS.readonly && ARGS.uploader_password.is_some() {
if uploader_password != ARGS.uploader_password.as_ref().unwrap().to_owned() {
return Ok(HttpResponse::Found()
.append_header(("Location", "/incorrect"))
.finish());
}
}
let id = new_pasta.id;
if plain_key != *"" && new_pasta.readonly {
@ -258,7 +297,8 @@ pub async fn create(
if new_pasta.file.is_some() && new_pasta.encrypt_server && !new_pasta.readonly {
let filepath = format!(
"./pasta_data/attachments/{}/{}",
"{}/attachments/{}/{}",
ARGS.data_dir,
&new_pasta.id_as_animals(),
&new_pasta.file.as_ref().unwrap().name()
);
@ -269,6 +309,8 @@ pub async fn create(
}
}
let encrypt_server = new_pasta.encrypt_server;
pastas.push(new_pasta);
for (_, pasta) in pastas.iter().enumerate() {
@ -283,10 +325,16 @@ pub async fn create(
to_animal_names(id)
};
Ok(HttpResponse::Found()
.append_header((
"Location",
format!("{}/pasta/{}", ARGS.public_path_as_str(), slug),
))
.finish())
if encrypt_server {
Ok(HttpResponse::Found()
.append_header(("Location", format!("/auth/{}/success", slug)))
.finish())
} else {
Ok(HttpResponse::Found()
.append_header((
"Location",
format!("{}/upload/{}", ARGS.public_path_as_str(), slug),
))
.finish())
}
}

View file

@ -136,7 +136,7 @@ pub async fn post_edit_private(
let mut password = String::from("");
while let Some(mut field) = payload.try_next().await? {
if field.name() == "password" {
if field.name() == Some("password") {
while let Some(chunk) = field.try_next().await? {
password.push_str(std::str::from_utf8(&chunk).unwrap().to_string().as_str());
}
@ -157,7 +157,7 @@ pub async fn post_edit_private(
}
}
if found {
if found && !pastas[index].encrypt_client {
let original_content = pastas[index].content.to_owned();
// decrypt content temporarily
@ -224,12 +224,12 @@ pub async fn post_submit_edit_private(
let mut new_content = String::from("");
while let Some(mut field) = payload.try_next().await? {
if field.name() == "content" {
if field.name() == Some("content") {
while let Some(chunk) = field.try_next().await? {
new_content.push_str(std::str::from_utf8(&chunk).unwrap().to_string().as_str());
}
}
if field.name() == "password" {
if field.name() == Some("password") {
while let Some(chunk) = field.try_next().await? {
password = std::str::from_utf8(&chunk).unwrap().to_string();
}
@ -250,7 +250,7 @@ pub async fn post_submit_edit_private(
}
}
if found && pastas[index].editable {
if found && pastas[index].editable && !pastas[index].encrypt_client {
if pastas[index].readonly {
let res = decrypt(pastas[index].encrypted_key.as_ref().unwrap(), &password);
if res.is_ok() {
@ -289,11 +289,7 @@ pub async fn post_submit_edit_private(
return Ok(HttpResponse::Found()
.append_header((
"Location",
format!(
"{}/pasta/{}",
ARGS.public_path_as_str(),
pastas[index].id_as_animals()
),
format!("/auth/{}/success", pastas[index].id_as_animals()),
))
.finish());
}
@ -308,12 +304,6 @@ pub async fn post_edit(
id: web::Path<String>,
mut payload: Multipart,
) -> Result<HttpResponse, Error> {
if ARGS.readonly {
return Ok(HttpResponse::Found()
.append_header(("Location", format!("{}/", ARGS.public_path_as_str())))
.finish());
}
let id = if ARGS.hash_ids {
hashid_to_u64(&id).unwrap_or(0)
} else {
@ -328,12 +318,12 @@ pub async fn post_edit(
let mut password = String::from("");
while let Some(mut field) = payload.try_next().await? {
if field.name() == "content" {
if field.name() == Some("content") {
while let Some(chunk) = field.try_next().await? {
new_content.push_str(std::str::from_utf8(&chunk).unwrap().to_string().as_str());
}
}
if field.name() == "password" {
if field.name() == Some("password") {
while let Some(chunk) = field.try_next().await? {
password = std::str::from_utf8(&chunk).unwrap().to_string();
}
@ -342,7 +332,7 @@ pub async fn post_edit(
for (i, pasta) in pastas.iter().enumerate() {
if pasta.id == id {
if pasta.editable {
if pasta.editable && !pasta.encrypt_client {
if pastas[i].readonly || pastas[i].encrypt_server {
if password != *"" {
let res = decrypt(pastas[i].encrypted_key.as_ref().unwrap(), &password);
@ -376,7 +366,7 @@ pub async fn post_edit(
.append_header((
"Location",
format!(
"{}/pasta/{}",
"{}/upload/{}",
ARGS.public_path_as_str(),
pastas[i].id_as_animals()
),

View file

@ -1,20 +1,21 @@
use std::fs::{self, File};
use std::fs::File;
use std::path::PathBuf;
use crate::args::ARGS;
use crate::util::auth;
use crate::util::hashids::to_u64 as hashid_to_u64;
use crate::util::misc::remove_expired;
use crate::util::{animalnumbers::to_u64, misc::decrypt_file};
use crate::AppState;
use actix_multipart::Multipart;
use actix_web::http::header;
use actix_web::{get, post, web, Error, HttpResponse};
use futures::TryStreamExt;
#[post("/secure_file/{id}")]
pub async fn post_secure_file(
data: web::Data<AppState>,
id: web::Path<String>,
mut payload: Multipart,
payload: Multipart,
) -> Result<HttpResponse, Error> {
// get access to the pasta collection
let mut pastas = data.pastas.lock().unwrap();
@ -39,23 +40,18 @@ pub async fn post_secure_file(
}
}
let mut password = String::from("");
while let Some(mut field) = payload.try_next().await? {
if field.name() == "password" {
while let Some(chunk) = field.try_next().await? {
password.push_str(std::str::from_utf8(&chunk).unwrap().to_string().as_str());
}
}
}
let password = auth::password_from_multipart(payload).await?;
if found {
if let Some(ref pasta_file) = pastas[index].file {
let file = File::open(format!(
"./pasta_data/attachments/{}/data.enc",
"{}/attachments/{}/data.enc",
ARGS.data_dir,
pastas[index].id_as_animals()
))?;
// Not compatible with NamedFile from actix_files (it needs a File
// to work therefore secure files do not support streaming
let decrypted_data: Vec<u8> = decrypt_file(&password, &file)?;
// Set the content type based on the file extension
@ -70,6 +66,7 @@ pub async fn post_secure_file(
"Content-Disposition",
format!("attachment; filename=\"{}\"", pasta_file.name()),
))
// TODO: make streaming <21-10-24, dvdsk>
.body(decrypted_data);
return Ok(response);
}
@ -79,8 +76,9 @@ pub async fn post_secure_file(
#[get("/file/{id}")]
pub async fn get_file(
data: web::Data<AppState>,
request: actix_web::HttpRequest,
id: web::Path<String>,
data: web::Data<AppState>,
) -> Result<HttpResponse, Error> {
// get access to the pasta collection
let mut pastas = data.pastas.lock().unwrap();
@ -118,34 +116,25 @@ pub async fn get_file(
// Construct the path to the file
let file_path = format!(
"./pasta_data/attachments/{}/{}",
"{}/attachments/{}/{}",
ARGS.data_dir,
pastas[index].id_as_animals(),
pasta_file.name()
);
let file_path = PathBuf::from(file_path);
// Read the contents of the file into memory
// let mut file_content = Vec::new();
// let mut file = File::open(&file_path)?;
// file.read_exact(&mut file_content)?;
let file_contents = fs::read(&file_path)?;
// Set the content type based on the file extension
let content_type = mime_guess::from_path(&file_path)
.first_or_octet_stream()
.to_string();
// Create an HttpResponse object with the file contents as the response body
let response = HttpResponse::Ok()
.content_type(content_type)
.append_header((
"Content-Disposition",
format!("attachment; filename=\"{}\"", pasta_file.name()),
))
.body(file_contents);
return Ok(response);
// This will stream the file and set the content type based on the
// file path
let file_reponse = actix_files::NamedFile::open(file_path)?;
let file_reponse = file_reponse.set_content_disposition(header::ContentDisposition {
disposition: header::DispositionType::Attachment,
parameters: vec![header::DispositionParam::Filename(
pasta_file.name().to_string(),
)],
});
// This takes care of streaming/seeking using the Range
// header in the request.
return Ok(file_reponse.into_response(&request));
}
}

View file

@ -7,13 +7,13 @@ use crate::util::misc::remove_expired;
use crate::AppState;
#[derive(Template)]
#[template(path = "pastalist.html")]
struct PastaListTemplate<'a> {
#[template(path = "list.html")]
struct ListTemplate<'a> {
pastas: &'a Vec<Pasta>,
args: &'a Args,
}
#[get("/pastalist")]
#[get("/list")]
pub async fn list(data: web::Data<AppState>) -> HttpResponse {
if ARGS.no_listing {
return HttpResponse::Found()
@ -29,7 +29,7 @@ pub async fn list(data: web::Data<AppState>) -> HttpResponse {
pastas.sort_by(|a, b| b.created.cmp(&a.created));
HttpResponse::Ok().content_type("text/html").body(
PastaListTemplate {
ListTemplate {
pastas: &pastas,
args: &ARGS,
}

View file

@ -2,6 +2,7 @@ use crate::args::{Args, ARGS};
use crate::endpoints::errors::ErrorTemplate;
use crate::pasta::Pasta;
use crate::util::animalnumbers::to_u64;
use crate::util::auth;
use crate::util::db::update;
use crate::util::hashids::to_u64 as hashid_to_u64;
use crate::util::misc::remove_expired;
@ -9,12 +10,11 @@ use crate::AppState;
use actix_multipart::Multipart;
use actix_web::{get, post, web, Error, HttpResponse};
use askama::Template;
use futures::TryStreamExt;
use magic_crypt::{new_magic_crypt, MagicCryptTrait};
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Template)]
#[template(path = "pasta.html", escape = "none")]
#[template(path = "upload.html", escape = "none")]
struct PastaTemplate<'a> {
pasta: &'a Pasta,
args: &'a Args,
@ -121,22 +121,13 @@ fn pastaresponse(
.body(ErrorTemplate { args: &ARGS }.render().unwrap())
}
#[post("/pasta/{id}")]
#[post("/upload/{id}")]
pub async fn postpasta(
data: web::Data<AppState>,
id: web::Path<String>,
mut payload: Multipart,
payload: Multipart,
) -> Result<HttpResponse, Error> {
let mut password = String::from("");
while let Some(mut field) = payload.try_next().await? {
if field.name() == "password" {
while let Some(chunk) = field.try_next().await? {
password.push_str(std::str::from_utf8(&chunk).unwrap().to_string().as_str());
}
}
}
let password = auth::password_from_multipart(payload).await?;
Ok(pastaresponse(data, id, password))
}
@ -144,22 +135,13 @@ pub async fn postpasta(
pub async fn postshortpasta(
data: web::Data<AppState>,
id: web::Path<String>,
mut payload: Multipart,
payload: Multipart,
) -> Result<HttpResponse, Error> {
let mut password = String::from("");
while let Some(mut field) = payload.try_next().await? {
if field.name() == "password" {
while let Some(chunk) = field.try_next().await? {
password.push_str(std::str::from_utf8(&chunk).unwrap().to_string().as_str());
}
}
}
let password = auth::password_from_multipart(payload).await?;
Ok(pastaresponse(data, id, password))
}
#[get("/pasta/{id}")]
#[get("/upload/{id}")]
pub async fn getpasta(data: web::Data<AppState>, id: web::Path<String>) -> HttpResponse {
pastaresponse(data, id, String::from(""))
}
@ -305,7 +287,7 @@ pub async fn getrawpasta(
// send raw content of pasta
let response = Ok(HttpResponse::NotFound()
.content_type("text/plain")
.content_type("text/plain; charset=utf-8")
.body(pastas[index].content.to_owned()));
return response;
@ -314,24 +296,16 @@ pub async fn getrawpasta(
// otherwise send pasta not found error as raw text
Ok(HttpResponse::NotFound()
.content_type("text/html")
.body(String::from("Pasta not found! :-(")))
.body(String::from("Upload not found! :-(")))
}
#[post("/raw/{id}")]
pub async fn postrawpasta(
data: web::Data<AppState>,
id: web::Path<String>,
mut payload: Multipart,
payload: Multipart,
) -> Result<HttpResponse, Error> {
let mut password = String::from("");
while let Some(mut field) = payload.try_next().await? {
if field.name() == "password" {
while let Some(chunk) = field.try_next().await? {
password.push_str(std::str::from_utf8(&chunk).unwrap().to_string().as_str());
}
}
}
let password = auth::password_from_multipart(payload).await?;
// get access to the pasta collection
let mut pastas = data.pastas.lock().unwrap();
@ -421,7 +395,7 @@ pub async fn postrawpasta(
// otherwise send pasta not found error as raw text
Ok(HttpResponse::NotFound()
.content_type("text/html")
.body(String::from("Pasta not found! :-(")))
.body(String::from("Upload not found! :-(")))
}
fn decrypt(text_str: &str, key_str: &str) -> Result<String, magic_crypt::MagicCryptError> {

View file

@ -42,13 +42,13 @@ pub async fn getqr(data: web::Data<AppState>, id: web::Path<String>) -> HttpResp
}
if found {
// generate the QR code as an SVG - if its a file or text pastas, this will point to the /pasta endpoint, otherwise to the /url endpoint, essentially directly taking the user to the url stored in the pasta
// generate the QR code as an SVG - if its a file or text pastas, this will point to the /upload endpoint, otherwise to the /url endpoint, essentially directly taking the user to the url stored in the pasta
let svg: String = match pastas[index].pasta_type.as_str() {
"url" => misc::string_to_qr_svg(
format!("{}/url/{}", &ARGS.public_path_as_str(), &id).as_str(),
),
_ => misc::string_to_qr_svg(
format!("{}/pasta/{}", &ARGS.public_path_as_str(), &id).as_str(),
format!("{}/upload/{}", &ARGS.public_path_as_str(), &id).as_str(),
),
};

View file

@ -1,24 +1,20 @@
use actix_web::{get, web, HttpResponse};
use actix_multipart::Multipart;
use actix_web::{get, post, web, Error, HttpResponse};
use crate::args::ARGS;
use crate::endpoints::errors::ErrorTemplate;
use crate::pasta::PastaFile;
use crate::util::animalnumbers::to_u64;
use crate::util::auth;
use crate::util::db::delete;
use crate::util::hashids::to_u64 as hashid_to_u64;
use crate::util::misc::remove_expired;
use crate::util::misc::{decrypt, remove_expired};
use crate::AppState;
use askama::Template;
use std::fs;
#[get("/remove/{id}")]
pub async fn remove(data: web::Data<AppState>, id: web::Path<String>) -> HttpResponse {
if ARGS.readonly {
return HttpResponse::Found()
.append_header(("Location", format!("{}/", ARGS.public_path_as_str())))
.finish();
}
let mut pastas = data.pastas.lock().unwrap();
let id = if ARGS.hash_ids {
@ -29,10 +25,21 @@ pub async fn remove(data: web::Data<AppState>, id: web::Path<String>) -> HttpRes
for (i, pasta) in pastas.iter().enumerate() {
if pasta.id == id {
// if it's encrypted or read-only, it needs password to be deleted
if pasta.encrypt_server || pasta.readonly {
return HttpResponse::Found()
.append_header((
"Location",
format!("/auth_remove_private/{}", pasta.id_as_animals()),
))
.finish();
}
// remove the file itself
if let Some(PastaFile { name, .. }) = &pasta.file {
if fs::remove_file(format!(
"./pasta_data/attachments/{}/{}",
"{}/attachments/{}/{}",
ARGS.data_dir,
pasta.id_as_animals(),
name
))
@ -43,7 +50,8 @@ pub async fn remove(data: web::Data<AppState>, id: web::Path<String>) -> HttpRes
// and remove the containing directory
if fs::remove_dir(format!(
"./pasta_data/attachments/{}/",
"{}/attachments/{}/",
ARGS.data_dir,
pasta.id_as_animals()
))
.is_err()
@ -58,10 +66,7 @@ pub async fn remove(data: web::Data<AppState>, id: web::Path<String>) -> HttpRes
delete(Some(&pastas), Some(id));
return HttpResponse::Found()
.append_header((
"Location",
format!("{}/pastalist", ARGS.public_path_as_str()),
))
.append_header(("Location", format!("{}/list", ARGS.public_path_as_str())))
.finish();
}
}
@ -72,3 +77,99 @@ pub async fn remove(data: web::Data<AppState>, id: web::Path<String>) -> HttpRes
.content_type("text/html")
.body(ErrorTemplate { args: &ARGS }.render().unwrap())
}
#[post("/remove/{id}")]
pub async fn post_remove(
data: web::Data<AppState>,
id: web::Path<String>,
payload: Multipart,
) -> Result<HttpResponse, Error> {
let id = if ARGS.hash_ids {
hashid_to_u64(&id).unwrap_or(0)
} else {
to_u64(&id.into_inner()).unwrap_or(0)
};
let mut pastas = data.pastas.lock().unwrap();
remove_expired(&mut pastas);
let password = auth::password_from_multipart(payload).await?;
for (i, pasta) in pastas.iter().enumerate() {
if pasta.id == id {
if pastas[i].readonly || pastas[i].encrypt_server {
if password != *"" {
let res = decrypt(pastas[i].content.to_owned().as_str(), &password);
if res.is_ok() {
// remove the file itself
if let Some(PastaFile { name, .. }) = &pasta.file {
if fs::remove_file(format!(
"{}/attachments/{}/{}",
ARGS.data_dir,
pasta.id_as_animals(),
name
))
.is_err()
{
log::error!("Failed to delete file {}!", name)
}
// and remove the containing directory
if fs::remove_dir(format!(
"{}/attachments/{}/",
ARGS.data_dir,
pasta.id_as_animals()
))
.is_err()
{
log::error!("Failed to delete directory {}!", name)
}
}
// remove it from in-memory pasta list
pastas.remove(i);
delete(Some(&pastas), Some(id));
return Ok(HttpResponse::Found()
.append_header((
"Location",
format!("{}/list", ARGS.public_path_as_str()),
))
.finish());
} else {
return Ok(HttpResponse::Found()
.append_header((
"Location",
format!("/auth_remove_private/{}/incorrect", pasta.id_as_animals()),
))
.finish());
}
} else {
return Ok(HttpResponse::Found()
.append_header((
"Location",
format!("/auth_remove_private/{}/incorrect", pasta.id_as_animals()),
))
.finish());
}
}
return Ok(HttpResponse::Found()
.append_header((
"Location",
format!(
"{}/upload/{}",
ARGS.public_path_as_str(),
pastas[i].id_as_animals()
),
))
.finish());
}
}
Ok(HttpResponse::Ok()
.content_type("text/html")
.body(ErrorTemplate { args: &ARGS }.render().unwrap()))
}

View file

@ -2,11 +2,12 @@ extern crate core;
use crate::args::ARGS;
use crate::endpoints::{
admin, auth_admin, auth_pasta, create, edit, errors, file, guide, pasta as pasta_endpoint,
pastalist, qr, remove, static_resources,
admin, auth_admin, auth_upload, create, edit, errors, file, guide, list,
pasta as pasta_endpoint, qr, remove, static_resources,
};
use crate::pasta::Pasta;
use crate::util::db::read_all;
use crate::util::telemetry::start_telemetry_thread;
use actix_web::middleware::Condition;
use actix_web::{middleware, web, App, HttpServer};
use actix_web_httpauth::middleware::HttpAuthentication;
@ -25,23 +26,27 @@ pub mod util {
pub mod auth;
pub mod db;
pub mod db_json;
#[cfg(feature = "default")]
pub mod db_sqlite;
pub mod hashids;
pub mod misc;
pub mod syntaxhighlighter;
pub mod telemetry;
pub mod version;
pub mod http_client;
}
pub mod endpoints {
pub mod admin;
pub mod auth_admin;
pub mod auth_pasta;
pub mod auth_upload;
pub mod create;
pub mod edit;
pub mod errors;
pub mod file;
pub mod guide;
pub mod list;
pub mod pasta;
pub mod pastalist;
pub mod qr;
pub mod remove;
pub mod static_resources;
@ -72,16 +77,17 @@ async fn main() -> std::io::Result<()> {
ARGS.port.to_string()
);
match fs::create_dir_all("./pasta_data/public") {
match fs::create_dir_all(format!("{}/public", ARGS.data_dir)) {
Ok(dir) => dir,
Err(error) => {
log::error!(
"Couldn't create data directory ./pasta_data/attachments/: {:?}",
"Couldn't create data directory {}/attachments/: {:?}",
ARGS.data_dir,
error
);
panic!(
"Couldn't create data directory ./pasta_data/attachments/: {:?}",
error
"Couldn't create data directory {}/attachments/: {:?}",
ARGS.data_dir, error
);
}
};
@ -90,6 +96,10 @@ async fn main() -> std::io::Result<()> {
pastas: Mutex::new(read_all()),
});
if !ARGS.disable_telemetry {
start_telemetry_thread();
}
HttpServer::new(move || {
App::new()
.app_data(data.clone())
@ -97,15 +107,17 @@ async fn main() -> std::io::Result<()> {
.service(create::index)
.service(guide::guide)
.service(auth_admin::auth_admin)
.service(auth_upload::auth_file_with_status)
.service(auth_admin::auth_admin_with_status)
.service(auth_pasta::auth_pasta_with_status)
.service(auth_pasta::auth_raw_pasta_with_status)
.service(auth_pasta::auth_edit_private_with_status)
.service(auth_pasta::auth_file)
.service(auth_pasta::auth_pasta)
.service(auth_pasta::auth_raw_pasta)
.service(auth_pasta::auth_edit_private)
.service(auth_pasta::auth_file_with_status)
.service(auth_upload::auth_upload_with_status)
.service(auth_upload::auth_raw_pasta_with_status)
.service(auth_upload::auth_edit_private_with_status)
.service(auth_upload::auth_remove_private_with_status)
.service(auth_upload::auth_file)
.service(auth_upload::auth_upload)
.service(auth_upload::auth_raw_pasta)
.service(auth_upload::auth_edit_private)
.service(auth_upload::auth_remove_private)
.service(pasta_endpoint::getpasta)
.service(pasta_endpoint::postpasta)
.service(pasta_endpoint::getshortpasta)
@ -129,7 +141,9 @@ async fn main() -> std::io::Result<()> {
.default_service(web::route().to(errors::not_found))
.wrap(middleware::Logger::default())
.service(remove::remove)
.service(pastalist::list)
.service(remove::post_remove)
.service(list::list)
.service(create::index_with_status)
.wrap(Condition::new(
ARGS.auth_basic_username.is_some()
&& ARGS.auth_basic_username.as_ref().unwrap().trim() != "",

View file

@ -10,7 +10,7 @@ use crate::util::animalnumbers::to_animal_names;
use crate::util::hashids::to_hashids;
use crate::util::syntaxhighlighter::html_highlight;
#[derive(Serialize, Deserialize, PartialEq, Eq)]
#[derive(Serialize, Deserialize, PartialEq, Debug, Eq)]
pub struct PastaFile {
pub name: String,
pub size: ByteSize,
@ -49,11 +49,11 @@ impl PastaFile {
}
pub fn embeddable(&self) -> bool {
self.is_image() && !self.is_video()
self.is_image() || self.is_video()
}
}
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, Debug)]
pub struct Pasta {
pub id: u64,
pub content: String,
@ -111,21 +111,7 @@ impl Pasta {
}
pub fn created_as_string(&self) -> String {
let date = Local.timestamp(self.created, 0);
format!(
"{:02}-{:02} {:02}:{:02}",
date.month(),
date.day(),
date.hour(),
date.minute(),
)
}
pub fn expiration_as_string(&self) -> String {
if self.expiration == 0 {
String::from("Never")
} else {
let date = Local.timestamp(self.expiration, 0);
Local.timestamp_opt(self.created, 0).map(|date| {
format!(
"{:02}-{:02} {:02}:{:02}",
date.month(),
@ -133,6 +119,28 @@ impl Pasta {
date.hour(),
date.minute(),
)
}).earliest().unwrap_or_else(|| {
log::error!("Failed to process created date");
String::from("Unknow")
})
}
pub fn expiration_as_string(&self) -> String {
if self.expiration == 0 {
String::from("Never")
} else {
Local.timestamp_opt(self.expiration, 0).map(|date| {
format!(
"{:02}-{:02} {:02}:{:02}",
date.month(),
date.day(),
date.hour(),
date.minute(),
)
}).earliest().unwrap_or_else(|| {
log::error!("Failed to process expiration");
String::from("Never")
})
}
}

View file

@ -6,48 +6,57 @@ const ANIMAL_NAMES: &[&str] = &[
"deer", "horse", "rat", "wasp", "dog", "jaguar", "raven", "whale", "dove", "koala", "seal",
"wolf", "duck", "lion", "shark", "worm", "eagle", "lizard", "sheep", "zebra",
];
const ANIMAL_COUNT: u64 = ANIMAL_NAMES.len() as u64;
pub fn to_animal_names(mut number: u64) -> String {
pub fn to_animal_names(number: u64) -> String {
let mut result: Vec<&str> = Vec::new();
if number == 0 {
return ANIMAL_NAMES[0].parse().unwrap();
}
let mut power = 6;
loop {
let digit = number / ANIMAL_NAMES.len().pow(power) as u64;
if !(result.is_empty() && digit == 0) {
result.push(ANIMAL_NAMES[digit as usize]);
}
number -= digit * ANIMAL_NAMES.len().pow(power) as u64;
if power > 0 {
power -= 1;
} else if power == 0 || number == 0 {
break;
}
let mut value = number;
while value != 0 {
let digit = (value % ANIMAL_COUNT) as usize;
value /= ANIMAL_COUNT;
result.push(ANIMAL_NAMES[digit]);
}
// We calculated the numbers in Little-Endian,
// now convert to Big-Endian for backwards compatibility with old data.
result.reverse();
result.join("-")
}
#[test]
fn test_to_animal_names() {
assert_eq!(to_animal_names(0), "ant");
assert_eq!(to_animal_names(1), "eel");
assert_eq!(to_animal_names(64), "eel-ant");
assert_eq!(to_animal_names(12345), "sloth-ant-lion");
}
pub fn to_u64(animal_names: &str) -> Result<u64, &str> {
let mut result: u64 = 0;
let animals: Vec<&str> = animal_names.split('-').collect();
let mut pow = animals.len();
for animal in animals {
pow -= 1;
for animal in animal_names.split('-') {
let animal_index = ANIMAL_NAMES.iter().position(|&r| r == animal);
match animal_index {
None => return Err("Failed to convert animal name to u64!"),
Some(_) => {
result += (animal_index.unwrap() * ANIMAL_NAMES.len().pow(pow as u32)) as u64
Some(idx) => {
result = result * ANIMAL_COUNT + (idx as u64);
}
}
}
Ok(result)
}
#[test]
fn test_animal_name_to_u64() {
assert_eq!(to_u64("ant"), Ok(0));
assert_eq!(to_u64("eel"), Ok(1));
assert_eq!(to_u64("eel-ant"), Ok(64));
assert_eq!(to_u64("sloth-ant-lion"), Ok(12345));
}

View file

@ -1,29 +1,38 @@
use actix_multipart::Multipart;
use actix_web::dev::ServiceRequest;
use actix_web::web::Bytes;
use actix_web::{error, Error};
use actix_web_httpauth::extractors::basic::BasicAuth;
use futures::TryStreamExt;
use crate::args::ARGS;
pub async fn auth_validator(
req: ServiceRequest,
credentials: BasicAuth,
) -> Result<ServiceRequest, Error> {
// check if username matches
if credentials.user_id().as_ref() == ARGS.auth_basic_username.as_ref().unwrap() {
return match ARGS.auth_basic_password.as_ref() {
Some(cred_pass) => match credentials.password() {
None => Err(error::ErrorBadRequest("Invalid login details.")),
Some(arg_pass) => {
if arg_pass == cred_pass {
Ok(req)
} else {
Err(error::ErrorBadRequest("Invalid login details."))
}
}
},
None => Ok(req),
};
} else {
Err(error::ErrorBadRequest("Invalid login details."))
creds: BasicAuth,
) -> Result<ServiceRequest, (Error, ServiceRequest)> {
match (
ARGS.auth_basic_username.as_ref(),
ARGS.auth_basic_password.as_ref(),
creds.password(),
) {
(Some(conf_user), Some(conf_pwd), Some(cred_pwd))
if creds.user_id() == conf_user && conf_pwd == cred_pwd =>
{
Ok(req)
}
_ => Err((error::ErrorBadRequest("Invalid login details."), req)),
}
}
pub async fn password_from_multipart(mut payload: Multipart) -> Result<String, Error> {
let mut password = String::new();
while let Some(mut field) = payload.try_next().await? {
if field.name() == Some("password") {
let password_bytes = field.bytes(1024).await.unwrap_or(Ok(Bytes::new()))?;
password = String::from_utf8_lossy(&password_bytes).to_string();
}
}
Ok(password)
}

View file

@ -1,5 +1,9 @@
use crate::{args::ARGS, pasta::Pasta};
#[cfg(not(feature = "default"))]
const PANIC_MSG: &'static str = "Can not run without argument json-db, this version of microbin was compiled without rusqlite support. Make sure you do not pass in no-default-features during compilation";
#[cfg(feature = "default")]
pub fn read_all() -> Vec<Pasta> {
if ARGS.json_db {
super::db_json::read_all()
@ -8,34 +12,59 @@ pub fn read_all() -> Vec<Pasta> {
}
}
#[cfg(not(feature = "default"))]
pub fn read_all() -> Vec<Pasta> {
if ARGS.json_db {
super::db_json::read_all()
} else {
panic!("{}", PANIC_MSG);
}
}
#[allow(unused)]
pub fn insert(pastas: Option<&Vec<Pasta>>, pasta: Option<&Pasta>) {
if ARGS.json_db {
super::db_json::update_all(pastas.expect("Called insert() without passing Pasta vector"));
} else {
#[cfg(feature = "default")]
super::db_sqlite::insert(pasta.expect("Called insert() without passing new Pasta"));
#[cfg(not(feature = "default"))]
panic!();
}
}
#[allow(unused)]
pub fn update(pastas: Option<&Vec<Pasta>>, pasta: Option<&Pasta>) {
if ARGS.json_db {
super::db_json::update_all(pastas.expect("Called update() without passing Pasta vector"));
} else {
#[cfg(feature = "default")]
super::db_sqlite::update(pasta.expect("Called insert() without passing Pasta to update"));
#[cfg(not(feature = "default"))]
panic!("{}", PANIC_MSG);
}
}
#[allow(unused)]
pub fn update_all(pastas: &Vec<Pasta>) {
if ARGS.json_db {
super::db_json::update_all(pastas);
} else {
#[cfg(feature = "default")]
super::db_sqlite::update_all(pastas);
#[cfg(not(feature = "default"))]
panic!("{}", PANIC_MSG);
}
}
#[allow(unused)]
pub fn delete(pastas: Option<&Vec<Pasta>>, id: Option<u64>) {
if ARGS.json_db {
super::db_json::update_all(pastas.expect("Called delete() without passing Pasta vector"));
} else {
#[cfg(feature = "default")]
super::db_sqlite::delete_by_id(id.expect("Called delete() without passing Pasta id"));
#[cfg(not(feature = "default"))]
panic!("{}", PANIC_MSG);
}
}

View file

@ -15,31 +15,19 @@ pub fn update_all(pastas: &Vec<Pasta>) {
}
fn save_to_file(pasta_data: &Vec<Pasta>) {
let mut file = File::create(DATABASE_PATH);
match file {
Ok(_) => {
let writer = BufWriter::new(file.unwrap());
serde_json::to_writer(writer, &pasta_data).expect("Failed to create JSON writer");
}
Err(_) => {
log::info!("Database file {} not found!", DATABASE_PATH);
file = File::create(DATABASE_PATH);
match file {
Ok(_) => {
log::info!("Database file {} created.", DATABASE_PATH);
save_to_file(pasta_data);
}
Err(err) => {
log::error!(
"Failed to create database file {}: {}!",
&DATABASE_PATH,
&err
);
panic!("Failed to create database file {}: {}!", DATABASE_PATH, err)
}
}
}
}
// This uses a two stage write. First we write to a new file, if this fails
// only the new pasta's are lost. Then we replace the current database with
// the new file. This either succeeds or fails. The database is never left
// in an undefined state.
let tmp_file_path = DATABASE_PATH.to_string() + ".tmp";
let tmp_file = File::create(&tmp_file_path).expect(&format!(
"failed to create temporary database file for writing. path: {tmp_file_path}"
));
let writer = BufWriter::new(tmp_file);
serde_json::to_writer(writer, &pasta_data)
.expect("Should be able to write out data to database file");
std::fs::rename(tmp_file_path, DATABASE_PATH).expect("Could not update database");
}
fn load_from_file() -> io::Result<Vec<Pasta>> {

View file

@ -1,9 +1,7 @@
use bytesize::ByteSize;
use rusqlite::{params, Connection};
use crate::{pasta::PastaFile, Pasta};
static DATABASE_PATH: &str = "pasta_data/database.sqlite";
use crate::{args::ARGS, pasta::PastaFile, Pasta};
pub fn read_all() -> Vec<Pasta> {
select_all_from_db()
@ -14,7 +12,8 @@ pub fn update_all(pastas: &[Pasta]) {
}
pub fn rewrite_all_to_db(pasta_data: &[Pasta]) {
let conn = Connection::open(DATABASE_PATH).expect("Failed to open SQLite database!");
let conn = Connection::open(format!("{}/database.sqlite", ARGS.data_dir))
.expect("Failed to open SQLite database!");
conn.execute(
"
@ -95,7 +94,8 @@ pub fn rewrite_all_to_db(pasta_data: &[Pasta]) {
}
pub fn select_all_from_db() -> Vec<Pasta> {
let conn = Connection::open(DATABASE_PATH).expect("Failed to open SQLite database!");
let conn = Connection::open(format!("{}/database.sqlite", ARGS.data_dir))
.expect("Failed to open SQLite database!");
conn.execute(
"
@ -167,7 +167,8 @@ pub fn select_all_from_db() -> Vec<Pasta> {
}
pub fn insert(pasta: &Pasta) {
let conn = Connection::open(DATABASE_PATH).expect("Failed to open SQLite database!");
let conn = Connection::open(format!("{}/database.sqlite", ARGS.data_dir))
.expect("Failed to open SQLite database!");
conn.execute(
"
@ -238,7 +239,8 @@ pub fn insert(pasta: &Pasta) {
}
pub fn update(pasta: &Pasta) {
let conn = Connection::open(DATABASE_PATH).expect("Failed to open SQLite database!");
let conn = Connection::open(format!("{}/database.sqlite", ARGS.data_dir))
.expect("Failed to open SQLite database!");
conn.execute(
"UPDATE pasta SET
@ -283,7 +285,8 @@ pub fn update(pasta: &Pasta) {
}
pub fn delete_by_id(id: u64) {
let conn = Connection::open(DATABASE_PATH).expect("Failed to open SQLite database!");
let conn = Connection::open(format!("{}/database.sqlite", ARGS.data_dir))
.expect("Failed to open SQLite database!");
conn.execute(
"DELETE FROM pasta

45
src/util/http_client.rs Normal file
View file

@ -0,0 +1,45 @@
#[cfg(not(any(feature = "default", feature = "__rustcrypto-tls")))]
compile_error! {"You must either have the default feature enabled (remove
the no-default-features rust argument) or the no-c-deps feature"}
#[cfg(feature = "default")]
pub fn new() -> reqwest::blocking::Client {
reqwest::blocking::Client::new()
}
#[cfg(feature = "default")]
pub fn new_async() -> reqwest::Client {
reqwest::Client::new()
}
#[cfg(feature = "__rustcrypto-tls")]
pub fn new() -> reqwest::blocking::Client {
reqwest::blocking::Client::builder()
.use_preconfigured_tls(tls_config())
.build()
.expect("Could not create HTTP client.")
}
#[cfg(feature = "__rustcrypto-tls")]
pub fn new_async() -> reqwest::Client {
reqwest::Client::builder()
.use_preconfigured_tls(tls_config())
.build()
.expect("Could not create HTTP client.")
}
#[cfg(feature = "__rustcrypto-tls")]
fn tls_config() -> rustls::ClientConfig {
use std::sync::Arc;
let root_store = rustls::RootCertStore {
roots: webpki_roots::TLS_SERVER_ROOTS.into(),
};
let provider = Arc::new(rustls_rustcrypto::provider());
rustls::ClientConfig::builder_with_provider(provider)
.with_safe_default_protocol_versions()
.expect("Should support safe default protocols")
.with_root_certificates(root_store)
.with_no_client_auth()
}

View file

@ -41,7 +41,8 @@ pub fn remove_expired(pastas: &mut Vec<Pasta>) {
// remove the file itself
if let Some(file) = &p.file {
if fs::remove_file(format!(
"./pasta_data/attachments/{}/{}",
"{}/attachments/{}/{}",
ARGS.data_dir,
p.id_as_animals(),
file.name()
))
@ -51,8 +52,12 @@ pub fn remove_expired(pastas: &mut Vec<Pasta>) {
}
// and remove the containing directory
if fs::remove_dir(format!("./pasta_data/attachments/{}/", p.id_as_animals()))
.is_err()
if fs::remove_dir(format!(
"{}/attachments/{}/",
ARGS.data_dir,
p.id_as_animals()
))
.is_err()
{
log::error!("Failed to delete directory {}!", file.name())
}
@ -138,7 +143,6 @@ pub fn decrypt_file(
let res = mc.decrypt_bytes_to_bytes(&ciphertext[..]);
if res.is_err() {
println!("{}", res.err().unwrap());
return Err("Failed to decrypt file".into());
}

40
src/util/telemetry.rs Normal file
View file

@ -0,0 +1,40 @@
use std::{
thread,
time::{Duration, Instant},
};
use serde_json::json;
use crate::args::ARGS;
pub fn start_telemetry_thread() {
// Start a new thread that calls the send_telemetry function every 24 hours
thread::spawn(|| {
let mut last_run = Instant::now();
loop {
let _ = send_telemetry();
// Wait for 24 hours since the last run
let next_run = last_run + Duration::from_secs(60 * 60 * 24);
let now = Instant::now();
if next_run > now {
thread::sleep(next_run - now);
}
last_run = Instant::now();
}
});
}
fn send_telemetry() -> Result<(), reqwest::Error> {
// Convert the telemetry object to JSON
let json_body = json!(ARGS.to_owned().without_secrets().to_owned()).to_string();
// Send the telemetry data to the API
crate::util::http_client::new()
.post("https://api.microbin.eu/telemetry/")
.header("Content-Type", "application/json")
.body(json_body)
.send()?;
Ok(())
}

51
src/util/version.rs Normal file
View file

@ -0,0 +1,51 @@
use std::borrow::Cow;
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize)]
pub struct Version {
pub major: u32,
pub minor: u32,
pub patch: u32,
pub title: Cow<'static, str>,
pub long_title: Cow<'static, str>,
pub description: Cow<'static, str>,
pub date: Cow<'static, str>,
pub update_type: Cow<'static, str>,
}
pub static CURRENT_VERSION: Version = Version {
major: 2,
minor: 0,
patch: 4,
title: Cow::Borrowed("2.0.4"),
long_title: Cow::Borrowed("Version 2.0.4, Build 20230711"),
description: Cow::Borrowed("This version includes bug fixes and performance improvements."),
date: Cow::Borrowed("2023-07-11"),
update_type: Cow::Borrowed("beta"),
};
impl Version {
pub fn newer_than(&self, other: &Version) -> bool {
if self.major != other.major {
self.major > other.major
} else if self.minor != other.minor {
self.minor > other.minor
} else {
self.patch > other.patch
}
}
pub fn newer_than_current(&self) -> bool {
self.newer_than(&CURRENT_VERSION)
}
}
pub async fn fetch_latest_version() -> Result<Version, reqwest::Error> {
let url = "https://api.microbin.eu/version/";
let http_client = crate::util::http_client::new_async();
let response = http_client.get(url).send().await?;
let version = response.json::<Version>().await?;
Ok(version)
}

View file

@ -4,7 +4,7 @@
<div style="height: 200px;">
<div style="float: left">
<h4>Links</h4>
<a href="https://microbin.eu/documentation" style="margin-right: 1rem">Documentation and Help</a>
<a href="https://microbin.eu/docs/intro" style="margin-right: 1rem">Documentation and Help</a>
<br>
<a href="https://github.com/szabodanika/microbin" style="margin-right: 1rem">Source Code</a>
<br>
@ -25,13 +25,24 @@
<td>{{status}} </td>
</tr>
<tr>
<td><b>Pastas</b></td>
<td><b>Uploads</b></td>
<td>{{pastas.len()}} </td>
</tr>
</table>
</div>
</div>
<h4>Update</h4>
{% if update.is_some() %}
<p><b>Update available</b> {{update.as_ref().unwrap().long_title}}</p>
<p><b>Date</b> {{update.as_ref().unwrap().date}}</p>
<p><b>Update type</b> {{update.as_ref().unwrap().update_type}}</p>
<p><b>Description</b> {{update.as_ref().unwrap().description}}</p>
{%- else %}
<p>No updates available.</p>
{%- endif %}
{% if message != "" %}
<h4>Messages</h4>
<p>{{message}}</p>
@ -79,7 +90,7 @@
<tr>
<td>
<a
href="{{ args.public_path_as_str()}}/pasta/{{pasta.id_as_animals()}}">{{pasta.id_as_animals()}}</a>
href="{{ args.public_path_as_str()}}/upload/{{pasta.id_as_animals()}}">{{pasta.id_as_animals()}}</a>
</td>
<td>
{{pasta.created_as_string()}}
@ -189,7 +200,7 @@
<tr>
<td>
<a
href="{{ args.public_path_as_str()}}/pasta/{{pasta.id_as_animals()}}">{{pasta.id_as_animals()}}</a>
href="{{ args.public_path_as_str()}}/upload/{{pasta.id_as_animals()}}">{{pasta.id_as_animals()}}</a>
</td>
<td>
{{pasta.created_as_string()}}
@ -273,14 +284,14 @@
</thead>
<tbody>
<tr>
<td>auth_username</td>
<td>auth_basic_username</td>
{% if args.auth_basic_username.as_ref().is_some() %}
<td>set</td>
{% else %}
<td>unset</td>
{% endif %}
<td>auth_password</td>
<td>auth_basic_password</td>
{% if args.auth_basic_password.as_ref().is_some() %}
<td>set</td>
{% else %}
@ -407,6 +418,12 @@
<tr>
<td>max_file_size_unencrypted_mb</td>
<td>{{ args.max_file_size_unencrypted_mb }} MB</td>
<td>uploader_password</td>
{% if args.uploader_password.as_ref().is_some() %}
<td>set</td>
{% else %}
<td>unset</td>
{% endif %}
</tr>
</tbody>
</table>
@ -428,4 +445,4 @@
</script>
<style>
</style>
</style>

View file

@ -16,7 +16,7 @@
{% include "footer.html" %} {% if !args.pure_html %}
<style>
#auth-form {
background-color: #f7f7f7;
background-color: var(--background-alt);
border-radius: 6px;
padding: 10px;
width: fit-content;

View file

@ -3,11 +3,23 @@
{% if encrypt_client %}
<form id="auth-form" method="POST" action="/{{path}}/{{id}}" enctype="multipart/form-data">
<label for="password"> Please enter the password to open the upload. <sup>
{% if status == "success" %}
<b>
Success!
</b> <br>
{% endif %}
<label for="password"> Please enter the
password to access or modify this upload. <sup>
<a href="/guide#encryption"></a></sup></label>
<input id="password-field" placeholder="Password" type="password" autocomplete="off">
<input id="password-field" required placeholder="Password" type="password" autocomplete="off">
<input id="password-hidden" name="password" type="hidden">
<button>Open</button>
<button>Okay</button>
{% if status == "incorrect" %}
<b>
Incorrect password.
</b>
{% endif %}
</form>
<script>
@ -17,11 +29,18 @@
const passwordHiddenField = document.getElementById("password-hidden");
form.onsubmit = function () {
if (passwordField.value.trim() == "") {
passwordField.focus();
return false;
}
let key = decryptWithPassword(passwordField.value, "{{ encrypted_key }}");
if (key) {
passwordHiddenField.value = key;
}
};
function decryptWithPassword(password, encryptedHex) {
@ -42,6 +61,11 @@
{% else %}
<form id="auth-form" method="POST" action="/{{path}}/{{id}}" enctype="multipart/form-data">
{% if status == "success" %}
<b>
Success!
</b> <br>
{% endif %}
<label for="password" style="margin-bottom: 0.5rem;"> Please enter the
password to access or modify this upload. <sup>
<a href="/guide#encryption"></a></sup></label>
@ -49,19 +73,41 @@
<button>Okay</button>
{% if status == "incorrect" %}
<p>
<b>
Incorrect password.
</p>
</b>
{% endif %}
</form>
<script>
const form = document.getElementById("auth-form");
const passwordField = document.getElementById("password-field");
form.onsubmit = function () {
if (passwordField.value.trim() == "") {
passwordField.focus();
return false;
}
let key = decryptWithPassword(passwordField.value, "{{ encrypted_key }}");
if (key) {
passwordHiddenField.value = key;
}
};
</script>
{% endif %}
{% include "footer.html" %} {% if !args.pure_html %}
<style>
#auth-form {
background-color: #f7f7f7;
background-color: var(--background-alt);
border-radius: 6px;
padding: 10px;
width: fit-content;

View file

@ -1,7 +1,7 @@
{% include "header.html" %}
<form action="/{{ path }}/{{ pasta.id_as_animals() }}" method="POST" enctype="multipart/form-data">
<h4>
Editing pasta '{{ pasta.id_as_animals() }}'
Editing upload '{{ pasta.id_as_animals() }}'
</h4>
<label>Content</label>
<br>

View file

@ -4,7 +4,7 @@
{% if args.footer_text.as_ref().is_none() %}
<a href="https://microbin.eu">MicroBin</a> by Dániel Szabó and the FOSS
Community. Let's keep the Web <b>compact</b>, <b>accessible</b> and
<b>humane</b>! {%- else %} {{ args.footer_text.as_ref().unwrap() }} {%-
<b>humane</b>! {%- else %} {{ args.footer_text.as_ref().unwrap()|safe }} {%-
endif %}
</p>
@ -12,4 +12,4 @@
</body>
</html>
</html>

View file

@ -48,8 +48,7 @@ padding-right:0.5rem; line-height: 1.5; font-size: 1.1em; padding-top: 2rem;">
margin-left: 0.5rem">New</a>
{% if !args.no_listing %}
<a href="{{ args.public_path_as_str() }}/pastalist"
style="margin-right: 0.5rem; margin-left: 0.5rem">List</a>
<a href="{{ args.public_path_as_str() }}/list" style="margin-right: 0.5rem; margin-left: 0.5rem">List</a>
{%- endif %}
<a href="{{ args.public_path_as_str() }}/guide" style="margin-right: 0.5rem;

View file

@ -47,7 +47,7 @@
"never" %}
<option selected value="never">
{%- else %}
<option value="never">a {%- endif %} Never Expire
<option value="never">{%- endif %} Never Expire
</option>
{%- endif %}
</select>
@ -100,8 +100,8 @@
{% if args.highlightsyntax %}
<div>
<label for="syntax-highlight">Syntax <sup> <a href="/guide#syntax"></a></sup></label><br>
<select style="width: 100%;" name="syntax-highlight" id="syntax-highlight">
<label for="syntax_highlight">Syntax <sup> <a href="/guide#syntax"></a></sup></label><br>
<select style="width: 100%;" name="syntax_highlight" id="syntax_highlight">
<option value="none">None</option>
<optgroup label="Client-Rendered">
<option value="auto">Automatic</option>
@ -138,27 +138,39 @@
</select>
</div>
{%- else %}
<input type="hidden" name="syntax-highlight" value="none">
<input type="hidden" name="syntax_highlight" value="none">
{%- endif %}
{% if args.encryption_client_side || args.encryption_server_side || args.enable_readonly || args.private %}
<div>
<label for="syntax-highlight">Privacy <sup> <a href="/guide#privacy"></a></sup></label><br>
<label for="privacy">Privacy <sup> <a href="/guide#privacy"></a></sup></label><br>
<select style="width: 100%;" name="privacy" id="privacy">
<optgroup label="Unencrypted (no password)">
<option value="public">Public</option>
{% if args.private %}
<option value="unlisted">Unlisted</option>
{%- endif %}
</optgroup>
{% if args.enable_readonly %}
<optgroup label="Unencrypted (protected)">
<option value="readonly">Read-only</option>
</optgroup>
{%- endif %}
{% if args.encryption_client_side || args.encryption_server_side %}
<optgroup label="Encrypted">
{% if args.encryption_server_side %}
<option value="private">Private</option>
{%- endif %}
{% if args.encryption_client_side%}
<option value="secret">Secret</option>
{%- endif %}
</optgroup>
{%- endif %}
</select>
</div>
{%- endif %}
{% if args.encryption_client_side || args.encryption_server_side %}
{% if args.encryption_client_side || args.encryption_server_side || args.enable_readonly %}
<div>
<label for="password_field">Password <sup><a href="/guide#password"></a></sup></label><br>
<input style="width: 130px; height: 28px;" type="password" id="password_field" autocomplete="off" />
@ -178,21 +190,28 @@
<br>
<input type="file" id="file" name="file" />
</div>
{% endif %} {% if args.readonly %}
<b>
<input style="width: 140px; float: right; background-color:
#2975D2; color: white;" disabled type="submit" value="Read Only" /></b>
{%- else %}
{% endif %}
<b>
<input style="width: 140px; float: right; background-color:
#2975D2; color: white;" id="submit-button" type="submit" value="Save" />
{% if args.readonly %}
{% if status == "incorrect" %}
<input style="width: 160px; float: right; background-color: rgba(255, 0, 0, 0.137);" type="password"
id="uploader_password" name="uploader_password" placeholder="Incorrect password!" />
{% else %}
<input style="width: 160px; float: right;" type="password" id="uploader_password" name="uploader_password"
placeholder="Uploader Password" />
{% endif %}
{% endif %}
</b>
{%- endif %}
</div>
<input type="hidden" name="content" id="content">
<input type="hidden" name="encrypt_client" id="encrypt_client">
{% if args.encryption_server_side %}
<input name="encrypted_random_key" type="hidden" id="encrypted_random_key" autocomplete="off" /> {%- endif %}
{% if args.encryption_server_side || args.enable_readonly %}
<input name="encrypted_random_key" type="hidden" id="encrypted_random_key" autocomplete="off" />
{%- endif %}
<input type="hidden" name="random_key" id="random_key">
<input type="hidden" name="plain_key" id="plain_key">
</form>
@ -218,8 +237,11 @@
form.onsubmit = async function (event) {
event.preventDefault(); // prevent default form submission
// {% if args.encryption_client_side || args.encryption_server_side || args.enable_readonly %}
if (passwordField.value.trim() != "") {
// {% if !args.no_file_upload %}
if (fileOversized()) return false;
// {%- endif %}
if (privacyDropdown.value == "secret") {
let randomKey = Array.from(Array(16), () => Math.floor(Math.random() * 36).toString(36)).join('');
@ -231,7 +253,9 @@
content.value = contentInput.value;
}
hiddenPlainKeyField.name = "";
// {% if !args.no_file_upload %}
await encryptFile();
// {%- endif %}
} else {
hiddenPlainKeyField.value = passwordField.value;
hiddenEncryptedClientSide.name = "";
@ -247,13 +271,47 @@
hiddenEncryptedClientSide.name = "";
content.value = contentInput.value;
}
// {%- else %}
hiddenEncryptedClientSide.name = "";
content.value = contentInput.value;
// {%- endif %}
if (contentInput.value.trim() == "" && hiddenFileButton.files.length == 0) {
if (contentInput.value.trim() == "" && (hiddenFileButton == undefined || hiddenFileButton.files.length == 0)) {
contentInput.focus();
return false;
}
form.submit();
let showProgress = false;
submitButton.disabled = true;
submitButton.textContent = 'Uploading...';
const xhr = new XMLHttpRequest();
xhr.open('POST', '/upload', true);
xhr.upload.onprogress = function (event) {
if (showProgress) {
const progressPercent = Math.round((event.loaded / event.total) * 100);
submitButton.value = `${progressPercent}%`;
}
};
xhr.onreadystatechange = function () {
if (xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status === 200 || xhr.status === 302) {
window.location.href = xhr.responseURL;
} else {
console.log('Request failed with status:', xhr.status);
}
}
};
const formData = new FormData(form);
xhr.send(formData);
showProgressTimeout = setTimeout(() => {
showProgress = true;
}, 1000);
};
function encryptWithPassword(password, plaintext) {
@ -264,6 +322,14 @@
return aesjs.utils.hex.fromBytes(encryptedBytes);
}
// {% if !args.no_file_upload %}
function encryptFileWithPassword(password, bytes) {
const passwordBytes = aesjs.utils.utf8.toBytes(password.padStart(32, "#"));
const aesCtr = new aesjs.ModeOfOperation.ctr(passwordBytes);
const encryptedBytes = aesCtr.encrypt(bytes);
return aesjs.utils.hex.fromBytes(encryptedBytes);
}
function fileOversized() {
if (hiddenFileButton.files.length > 0) {
const fileSize = hiddenFileButton.files.item(0).size;
@ -295,8 +361,7 @@
const reader = new FileReader();
reader.onload = function (event) {
const fileContents = event.target.result;
const encryptedContents = encryptWithPassword(passwordField.value.trim(), fileContents);
const encryptedContents = encryptFileWithPassword(passwordField.value.trim(), new Uint8Array(event.target.result));
// Replace selected file with its encrypted version
const encryptedFile = new File([encryptedContents], file.name, { type: file.type });
@ -306,7 +371,7 @@
hiddenFileButton.files = container.files;
resolve(encryptedFile);
};
reader.readAsText(file);
reader.readAsArrayBuffer(file);
} else {
resolve();
}
@ -334,6 +399,7 @@
attachFileButton.textContent = "Attached: " + hiddenFileButton.files[0].name;
evt.preventDefault();
};
// {%- endif %}
</script>
@ -379,4 +445,4 @@
}
</style>
{% include "footer.html" %}
{% include "footer.html" %}

197
templates/list.html Normal file
View file

@ -0,0 +1,197 @@
{% include "header.html" %}
{% if pastas.is_empty() %}
<br>
<p>
No uploads yet. 😔 Create one <a href="{{ args.public_path_as_str() }}/">here</a>.
</p>
<br>
{%- else %}
<h3>Uploads</h3>
<div style="width: 100%; overflow-x: auto;">
{% if args.pure_html %}
<table border="1" style="width: 100%; min-width: 720px; white-space: nowrap;">
{% else %}
<table style="width: 100%; min-width: 720px;">
{% endif %}
<thead>
<th style="width: 25%">
Key
</th>
<th style="width: 10%">
</th>
<th style="width: 15%">
Created
</th>
<th style="width: 15%">
Expiration
</th>
<th style="width: 15%">
Contents
</th>
<th style="width: 10%">
</th>
<th style="width: 10%">
</th>
</thead>
<tbody>
{% for pasta in pastas %}
{% if pasta.pasta_type == "text" && !pasta.private %}
<tr>
<td>
<a
href="{{ args.public_path_as_str()}}/upload/{{pasta.id_as_animals()}}">{{pasta.id_as_animals()}}</a>
</td>
<td>
{% if args.public_path_as_str() != "" %}
{% if args.short_path_as_str() == "" %}
<a style="margin-right:1rem; cursor: pointer;" class="copy-button" null
data-url="{{ args.public_path_as_str()}}/upload/{{pasta.id_as_animals()}}">Copy</a>
{% else %}
<a style="margin-right:1rem; cursor: pointer;" class="copy-button" data-url="{{ args.short_path_as_str()
}}/p/{{pasta.id_as_animals()}}">Copy</a>
{% endif %}
{%- endif %}
</td>
<td>
{{pasta.created_as_string()}}
</td>
<td>
{{pasta.expiration_as_string()}}
</td>
<td>
{% if pasta.content != "" %}
<a style="margin-right:1rem"
href="{{ args.public_path_as_str()}}/raw/{{pasta.id_as_animals()}}">Text</a>
{%- endif %}
{% if pasta.file.is_some() %}
<a style="margin-right:1rem"
href="{{ args.public_path_as_str() }}/file/{{pasta.id_as_animals()}}">
{% if pasta.file.as_ref().unwrap().is_image() %}
Image
{%- else if pasta.file.as_ref().unwrap().is_video() %}
Video
{%- else %}
File
{%- endif %}
</a>
{%- endif %}
</td>
<td>
{% if pasta.editable %}
<a style="margin-right:1rem"
href="{{ args.public_path_as_str() }}/edit/{{pasta.id_as_animals()}}">Edit</a>
{%- endif %}
</td>
<td>
<a href="{{ args.public_path_as_str() }}/remove/{{pasta.id_as_animals()}}">Remove</a>
</td>
</tr>
{%- endif %}
{% endfor %}
</tbody>
</table>
<br>
<h3>URL Redirects</h3>
{% if args.pure_html %}
<table border="1" style="width: 100%; min-width: 720px; ">
{% else %}
<table style="width: 100%; min-width: 720px;">
{% endif %}
<thead>
<th style=" width: 25%">
Key
</th>
<th style="width: 10%">
</th>
<th style="width: 15%">
Created
</th>
<th style="width: 15%">
Expiration
</th>
<th style="width: 15%">
</th>
<th style="width: 10%">
</th>
<th style="width: 10%">
</th>
</thead>
{% for pasta in pastas %}
{% if pasta.pasta_type == "url" && !pasta.private %}
<tr>
<td>
<a
href="{{ args.public_path_as_str() }}/upload/{{pasta.id_as_animals()}}">{{pasta.id_as_animals()}}</a>
</td>
<td>
{% if args.short_path_as_str() == "" %}
<a style="margin-right:1rem; cursor: pointer;" class="copy-button"
data-url="{{ args.public_path_as_str() }}/url/{{pasta.id_as_animals()}}">Copy</a>
{% else %}
<a style="margin-right:1rem; cursor: pointer;" class="copy-button" data-url="{{ args.short_path_as_str()
}}/u/{{pasta.id_as_animals()}}">Copy</a>
{% endif %}
</td>
<td>
{{pasta.created_as_string()}}
</td>
<td>
{{pasta.expiration_as_string()}}
</td>
<td>
<a style="margin-right:1rem"
href="{{ args.public_path_as_str() }}/url/{{pasta.id_as_animals()}}">Redirect</a>
</td>
<td>
{% if pasta.editable %}
<a style="margin-right:1rem"
href="{{ args.public_path_as_str() }}/edit/{{pasta.id_as_animals()}}">Edit</a>
{%- endif %}
</td>
<td>
<a href="{{ args.public_path_as_str() }}/remove/{{pasta.id_as_animals()}}">Remove</a>
</td>
</tr>
{%- endif %}
{% endfor %}
</tbody>
</table>
<br>
{%- endif %}
</div>
<script>
const copyURLBtns = document.getElementsByClassName("copy-button");
for (var i = 0; i < copyURLBtns.length; i++) {
copyURLBtns.item(i).addEventListener("click", event => {
event.srcElement
navigator.clipboard.writeText(event.srcElement.getAttribute("data-url"))
event.srcElement.innerHTML = "Copied"
setTimeout(() => {
event.srcElement.innerHTML = "Copy"
}, 1000)
})
}
</script>
<style>
.copy-url-button {
font-size: small;
padding: 4px;
padding-left: 0.8rem;
padding-right: 0.8rem;
cursor: pointer;
}
th,
td {
white-space: nowrap;
}
</style>
{% include "footer.html" %}

View file

@ -1,194 +0,0 @@
{% include "header.html" %}
{% if pastas.is_empty() %}
<br>
<p>
No uploads yet. 😔 Create one <a href="{{ args.public_path_as_str() }}/">here</a>.
</p>
<br>
{%- else %}
<h3>Uploads</h3>
{% if args.pure_html %}
<table border="1" style="width: 100%; white-space: nowrap;">
{% else %}
<table style="width: 100%">
{% endif %}
<thead>
<th style="width: 25%">
Key
</th>
<th style="width: 10%">
</th>
<th style="width: 15%">
Created
</th>
<th style="width: 15%">
Expiration
</th>
<th style="width: 15%">
Contents
</th>
<th style="width: 10%">
</th>
<th style="width: 10%">
</th>
</thead>
<tbody>
{% for pasta in pastas %}
{% if pasta.pasta_type == "text" && !pasta.private %}
<tr>
<td>
<a
href="{{ args.public_path_as_str()}}/pasta/{{pasta.id_as_animals()}}">{{pasta.id_as_animals()}}</a>
</td>
<td>
{% if args.public_path_as_str() != "" %}
{% if args.short_path_as_str() == "" %}
<a style="margin-right:1rem; cursor: pointer;" class="copy-button" null
data-url="{{ args.public_path_as_str()}}/pasta/{{pasta.id_as_animals()}}">Copy</a>
{% else %}
<a style="margin-right:1rem; cursor: pointer;" class="copy-button" data-url="{{ args.short_path_as_str()
}}/p/{{pasta.id_as_animals()}}">Copy</a>
{% endif %}
{%- endif %}
</td>
<td>
{{pasta.created_as_string()}}
</td>
<td>
{{pasta.expiration_as_string()}}
</td>
<td>
{% if pasta.content != "" %}
<a style="margin-right:1rem"
href="{{ args.public_path_as_str()}}/raw/{{pasta.id_as_animals()}}">Text</a>
{%- endif %}
{% if pasta.file.is_some() %}
<a style="margin-right:1rem" href="{{ args.public_path_as_str() }}/file/{{pasta.id_as_animals()}}">
{% if pasta.file.as_ref().unwrap().is_image() %}
Image
{%- else if pasta.file.as_ref().unwrap().is_video() %}
Video
{%- else %}
File
{%- endif %}
</a>
{%- endif %}
</td>
<td>
{% if pasta.editable %}
<a style="margin-right:1rem"
href="{{ args.public_path_as_str() }}/edit/{{pasta.id_as_animals()}}">Edit</a>
{%- endif %}
</td>
<td>
<a href="{{ args.public_path_as_str() }}/remove/{{pasta.id_as_animals()}}">Remove</a>
</td>
</tr>
{%- endif %}
{% endfor %}
</tbody>
</table>
<br>
<h3>URL Redirects</h3>
{% if args.pure_html %}
<table border="1" style="width: 100%">
{% else %}
<table style="width: 100%">
{% endif %}
<thead>
<th style="width: 25%">
Key
</th>
<th style="width: 10%">
</th>
<th style="width: 15%">
Created
</th>
<th style="width: 15%">
Expiration
</th>
<th style="width: 15%">
</th>
<th style="width: 10%">
</th>
<th style="width: 10%">
</th>
</thead>
{% for pasta in pastas %}
{% if pasta.pasta_type == "url" && !pasta.private %}
<tr>
<td>
<a
href="{{ args.public_path_as_str() }}/pasta/{{pasta.id_as_animals()}}">{{pasta.id_as_animals()}}</a>
</td>
<td>
{% if args.short_path_as_str() == "" %}
<a style="margin-right:1rem; cursor: pointer;" class="copy-button"
data-url="{{ args.public_path_as_str() }}/url/{{pasta.id_as_animals()}}">Copy</a>
{% else %}
<a style="margin-right:1rem; cursor: pointer;" class="copy-button" data-url="{{ args.short_path_as_str()
}}/u/{{pasta.id_as_animals()}}">Copy</a>
{% endif %}
</td>
<td>
{{pasta.created_as_string()}}
</td>
<td>
{{pasta.expiration_as_string()}}
</td>
<td>
<a style="margin-right:1rem"
href="{{ args.public_path_as_str() }}/url/{{pasta.id_as_animals()}}">Redirect</a>
</td>
<td>
{% if pasta.editable %}
<a style="margin-right:1rem"
href="{{ args.public_path_as_str() }}/edit/{{pasta.id_as_animals()}}">Edit</a>
{%- endif %}
</td>
<td>
<a href="{{ args.public_path_as_str() }}/remove/{{pasta.id_as_animals()}}">Remove</a>
</td>
</tr>
{%- endif %}
{% endfor %}
</tbody>
</table>
<br>
{%- endif %}
<script>
const copyURLBtns = document.getElementsByClassName("copy-button");
for (var i = 0; i < copyURLBtns.length; i++) {
copyURLBtns.item(i).addEventListener("click", event => {
event.srcElement
navigator.clipboard.writeText(event.srcElement.getAttribute("data-url"))
event.srcElement.innerHTML = "Copied"
setTimeout(() => {
event.srcElement.innerHTML = "Copy"
}, 1000)
})
}
</script>
<style>
.copy-url-button {
font-size: small;
padding: 4px;
padding-left: 0.8rem;
padding-right: 0.8rem;
cursor: pointer;
}
th,
td {
white-space: nowrap;
}
</style>
{% include "footer.html" %}

View file

@ -1,7 +1,7 @@
{% include "header.html" %}
<div style="float: left">
<a href="{{ args.public_path_as_str() }}/pasta/{{pasta.id_as_animals()}}">Back to Pasta</a>
<a href="{{ args.public_path_as_str() }}/upload/{{pasta.id_as_animals()}}">Back to Upload</a>
</div>
@ -11,7 +11,7 @@
{{qr}}
</a>
{% else %}
<a href="{{ args.public_path_as_str() }}/pasta/{{pasta.id_as_animals()}}">
<a href="{{ args.public_path_as_str() }}/upload/{{pasta.id_as_animals()}}">
{{qr}}
</a>
{% endif %}

View file

@ -16,14 +16,14 @@
Content</a>
{%- endif %} {% if args.qr && args.public_path_as_str() != "" %}
<a style="margin-right: 1rem" href="{{ args.public_path_as_str() }}/qr/{{pasta.id_as_animals()}}">QR</a>
{%- endif %} {% if pasta.editable %}
{%- endif %} {% if pasta.editable && !pasta.encrypt_client %}
<a style="margin-right: 1rem" href="{{ args.public_path_as_str() }}/edit/{{pasta.id_as_animals()}}">Edit</a>
{%- endif %}
<a style="margin-right: 1rem" href="{{ args.public_path_as_str() }}/remove/{{pasta.id_as_animals()}}">Remove</a>
</div>
<div style="float: right">
<a style="margin-right: 0.5rem"
href="{{ args.public_path_as_str() }}/pasta/{{pasta.id_as_animals()}}"><i>{{pasta.id_as_animals()}}</i></a>
href="{{ args.public_path_as_str() }}/upload/{{pasta.id_as_animals()}}"><i>{{pasta.id_as_animals()}}</i></a>
{% if args.public_path_as_str() != "" %}
<button id="copy-url-button" class="small-button" style="margin-right: 0">
Copy URL
@ -33,23 +33,26 @@
<br>
<br>
{% if pasta.encrypt_client && pasta.file.is_some() && !pasta.file_embeddable() %}
{% if pasta.encrypt_client %}
<span style="margin-left: auto; margin-right: auto; display: flex;
justify-content: center; align-items: center;">
<div id="decryption">
{% if pasta.encrypt_client %}
<label for="password-field" style="margin-bottom: 0.5em;">
Please enter your key to decrypt this upload. <sup> <a href="/guide#encryption"></a></sup>
</label>
<input class="small-button" placeholder="Key" style="margin-right: 0.5rem" type="password" id="password-field"
autocomplete="off" />
{% if pasta.content != "" %}
<button class="small-button" id="decrypt-button" style="margin-right:
0.5rem">
0.5rem">
<b>
Decrypt text
</b>
</button>
{%- endif %}
{%- endif %}
{% if pasta.file.is_some() && !pasta.file_embeddable() %}
<button class="small-button" id="download-button" style="margin-right:
0.5rem">
<b>
@ -57,10 +60,11 @@ justify-content: center; align-items: center;">
[{{pasta.file.as_ref().unwrap().size}}]
</b>
</button>
{%- endif %}
</div>
</span>
{%- endif %}
<br>
{% if pasta.content != "" %}
@ -77,6 +81,19 @@ justify-content: center; align-items: center;">
</div>
{%- endif %}
{% if pasta.file.is_some() && !pasta.file_embeddable() && !pasta.encrypt_client %}
<span style="margin-left: auto; margin-right: auto; display: flex;
justify-content: center; align-items: center;">
<p style="font-size: small;">{{pasta.file.as_ref().unwrap().name()}}
[{{pasta.file.as_ref().unwrap().size}}]</p>
<a href="{{ args.public_path_as_str()}}/file/{{pasta.id_as_animals()}}" id="download-link">
<button class="download-button" autofocus>
Download
</button>
</a>
</span>
{%- endif %}
{% if pasta.file.is_some() && pasta.file.as_ref().unwrap().is_image() &&
pasta.file_embeddable() && !pasta.encrypt_client %}
@ -93,6 +110,7 @@ pasta.file_embeddable() && !pasta.encrypt_client %}
</span>
{%- endif %}
{% if pasta.file.is_some() && pasta.file.as_ref().unwrap().is_video() &&
pasta.file_embeddable() && !pasta.encrypt_client %}
<video id="embed" controls src="{{ args.public_path_as_str()}}/file/{{pasta.id_as_animals()}}" height="300"></video>
@ -130,7 +148,7 @@ pasta.file_embeddable() && !pasta.encrypt_client %}
const copyRedirectBtn = document.getElementById("copy-redirect-button")
var content = `{{ pasta.content_escaped() }}`
const contentElement = document.getElementById("code");
const url = (`{{ args.short_path_as_str()}}` === "") ? `{{ args.public_path_as_str() }}/pasta/{{pasta.id_as_animals()}}` : `{{ args.short_path_as_str()}}/p/{{pasta.id_as_animals()}}`
const url = (`{{ args.short_path_as_str()}}` === "") ? `{{ args.public_path_as_str() }}/upload/{{pasta.id_as_animals()}}` : `{{ args.short_path_as_str()}}/p/{{pasta.id_as_animals()}}`
const redirect_url = (`{{ args.short_path_as_str()}}` === "") ? `{{ args.public_path_as_str() }}/url/{{pasta.id_as_animals()}}` : `{{ args.short_path_as_str()}}/u/{{pasta.id_as_animals()}}`
const te = new TextEncoder();
@ -205,6 +223,7 @@ pasta.file_embeddable() && !pasta.encrypt_client %}
const passwordField = document.getElementById("password-field");
const downloadButton = document.getElementById('download-button');
// {% if pasta.file.is_some() %}
// Set up event listener for download link click
downloadButton.addEventListener('click', async (event) => {
event.preventDefault(); // prevent default click behavior
@ -231,26 +250,29 @@ pasta.file_embeddable() && !pasta.encrypt_client %}
const encryptedFile = await response.text();
// Decrypt file contents
const decryptedContents = decryptWithPassword(passwordField.value.trim(), encryptedFile);
const decryptedContents = decryptFileWithPassword(passwordField.value.trim(), encryptedFile);
if (!decryptedContents) {
throw new Error('Failed to decrypt file');
}
// Create blob from decrypted file contents
const decryptedBlob = new Blob([decryptedContents], { type: 'application/octet-stream' });
// Create data URI for decrypted file
const dataUri = `data:text/plain;charset=utf-8,${encodeURIComponent(decryptedContents)}`;
// const dataUri = `data:application/octet-stream;${encodeURIComponent(decryptedContents)}`;
// Create temporary anchor element
const tempAnchorEl = document.createElement('a');
tempAnchorEl.href = dataUri;
// tempAnchorEl.href = dataUri;
tempAnchorEl.href = URL.createObjectURL(decryptedBlob);
tempAnchorEl.download = '{{pasta.file.as_ref().unwrap().name()}}';
// Programmatically click anchor element to trigger download
tempAnchorEl.click();
// Remove temporary anchor element
document.re(tempAnchorEl);
// {%- endif %}
});
// {% endif %}
decryptButton.addEventListener("click", () => {
password = passwordField.value;
@ -272,12 +294,21 @@ pasta.file_embeddable() && !pasta.encrypt_client %}
const aesCtr = new aesjs.ModeOfOperation.ctr(passwordBytes);
const decryptedBytes = aesCtr.decrypt(encryptedBytes);
const res = aesjs.utils.utf8.fromBytes(decryptedBytes);
if (res.endsWith("!0K")) {
return res.substring(0, res.length - "!0K".length);
} else {
return null;
}
}
function decryptFileWithPassword(password, encryptedHex) {
const passwordBytes = aesjs.utils.utf8.toBytes(password.padStart(32, "#"));
const encryptedBytes = aesjs.utils.hex.toBytes(encryptedHex);
const aesCtr = new aesjs.ModeOfOperation.ctr(passwordBytes);
const decryptedBytes = aesCtr.decrypt(encryptedBytes);
return decryptedBytes;
}
// {% endif %}
</script>